• 选择排序
• 简单选择排序
如下图所示为简单选择排序一趟的过程,对于n个元素的序列A[0]~A[n-1]进行n趟操作。每一趟都是从待排序的部分选择最小的元素 与当前待排序序列的第一个元素进行交换,这样就形成了新的有序部分和待排序部分。
代码如下:
//简单选择排序
void selectSort(int A[], int n){
for(int i=0; i<n; i++){
int min = i;
//从待排序的部分中找到最小的元素,与A[i]交换
for(int j=i+1; j<n; j++){
if(A[j] < A[min]){
min = j;
}
}
if(min != i){
swap(A[i], A[min]);
}
}
}
可见,简单选择排序的时间复杂度是O(n²),空间复杂度是O(1);并且简单选择排序是一种不稳定的排序算法。(如下图,元素4 和 4*的前后位置发生了变化)
• 堆排序
学习堆排序之前,先明确一下什么是小根堆和大根堆:
堆通常是一个可以被看做一棵完全二叉树的。
小根堆:父节点的值小于孩子节点的值;用线性表来表示即为 A[i] ≤ A[2 * i] && A[i] ≤ A[2 * i +1];可见小根堆的根节点是整个序列中最小的值。
大根堆:父节点的值大于孩子节点的值;用线性表来表示即为A[i]≥A[2* i] && A[i]≥A[2* i +1];可见大根堆的根节点是整个序列中最大的值。
再了解了大根堆和小根堆的概念以后,我们来看如何初始化一个堆,这里我们以大根堆来举例子,小根堆同理,只需要改变符号即可。
对于n个序列的堆A[1]~A[n],我们需要从后往前进行堆的初始化,但不是从最后一个元素开始,而是从 n/2(取下界)的位置开始调整。为什么呢?从图中可以看出若是我们想要初始化堆,只需要从最后一个非叶子节点开始进行堆的调整即可,而这个节点就是下标为n/2(取下界)的位置。
具体向下调整的过程,来看一个图解,这里我们假设从k=1的位置进行一趟向下调整(i>1的部分已经调整为大根堆):
- 首先用A[0]保存需要调整的值A[k],A[0] = 7;
- 令i=2*k找到它的左孩子,它一定是有左孩子的,若有右孩子且右孩子的值大于左孩子的值,则将i++(当前指向左孩子,自增后指向右孩子) ;此时有两种情况:①孩子节点中的最大值小于当前节点的值,说明当前节点已经为大根堆,不需要进行操作了;②孩子节点中的最大值大于当前节点的值,此时不满足大根堆的条件,需要进行调整,将A[k] = A[i],同时调整k为i,应继续判断是否接着向下调整,重复上面的过程;
- 最后将A[k] = A[0];即完成一趟向下调整。
堆的初始化代码如下:
//向下调整
void AdjustDown(int A[], int k, int n){
A[0] = A[k];
int i;
for(i=2*k; i<=n; i*=2){
if(i<n && A[i+1] > A[i]){
i++;
}
if(A[0] > A[i]) {
break;
}
else{
A[k] = A[i];
k=i;
}
}
A[k] = A[0];
}
//堆的初始化(大堆)
void heapInit(int A[], int n){
for(int i=n/2; i>0; i--){
AdjustDown(A, i, n);
}
}
明白了堆的初始化,那么堆排序就已经很轻松了,我们这里还是以大堆为例。首先对序列初始化为一个大根堆,大根堆的第一个元素即为该序列中元素的最大值,我们那它与最后一个元素交换,然后对第一个元素进行向下调整(不对已经排好序的元素再次调整,即向下调整的时候忽略后面已经有序的序列),重复此过程,即为堆排序。
下图为一个完整的堆排序过程。
代码如下:
//向下调整
void AdjustDown(int A[], int k, int n){
A[0] = A[k];
int i;
for(i=2*k; i<=n; i*=2){
if(i<n && A[i+1] > A[i]){
i++;
}
if(A[0] > A[i]) {
break;
}
else{
A[k] = A[i];
k=i;
}
}
A[k] = A[0];
}
//堆的初始化(大堆)
void heapInit(int A[], int n){
for(int i=n/2; i>0; i--){
AdjustDown(A, i, n);
}
}
//堆排序
void heapSort(int A[], int n){
heapInit(A, n);
for(int i=n; i>1; i--){
swap(A[1], A[i]);
AdjustDown(A, 1, i-1);
}
}
堆排序时间复杂度是O(nlog2^n);
堆排序的空间复杂度为O(1);
堆排序是一种不稳定的排序算法。
• 交换排序
• 冒泡排序
顾名思义,冒泡排序就是向金鱼吐泡泡一样,每次将最大泡泡浮到最顶端。对于n个元素的序列,冒泡排序需要进行n-1趟,每趟冒泡排序从第一个元素开始,依次比较相邻两个元素的大小,若前面的值大于后面的值,则进行交换。每趟冒泡都会将一个值放到自己的位置上。
代码如下:
//冒泡排序
void bubbleSort(int A[], int n){
for(int i=0; i<n-1; i++){
bool flag = false;//标志位
for(int j=1; j<n-i; j++){
if(A[j-1] > A[j]){
swap(A[j-1], A[j]);
flag = true;
}
}
//如果本次冒泡排序未进行交换,说明已经有序,直接返回
if(flag == false){
return;
}
}
}
冒泡排序排序最好的时间复杂度是O(n);
冒泡排序最坏的时间复杂度是O(n²);
冒泡排序平均的时间复杂度是O(n²);
空间复杂度为O(1);
冒泡排序是一种稳定的排序算法。
• 快速排序
快速排序是对冒泡排序的一种改进。一趟快速排序的基本思想是:首先要确立一个中心点(pivot),将该序列划分为两个部分,一部分中的所有元素都比pivot值小,另一部分中的所有元素的值都比pivot大。
我们用low来表示序列的一个元素的下标,high来表示最后一个元素的下标;这里我们为了方便起见,选择每次需要排序的第一个元素(下标low的元素)保存到pivot中,然后先从后先前找,找到第一个比pivot值小的元素放在low的位置处;再从前往后找,找到第一个比pivot值大的元素,将它放到high位置处;重复这个过程,直到low与high最终指向同一个位置,即pivot应该存放的位置。见图解,即为一趟快速排序的过程。
可见,每趟快排的划分都会将一个元素(pivot)确定在它最终的位置上。之后再通过递归的方式将划分出来的两部分继续进行快排。
代码如下:
//快速排序的一趟(快排的主体部分)
int Partition(int A[], int low ,int high){
int pivot = A[low];
while(low < high){
//从后往前
while(low < high && A[high] >= pivot){
high--;
}
A[low] = A[high];
//从前往后
while(low < high && A[low] <= pivot){
low++;
}
A[high] = A[low];
}
A[high] = pivot;
return high;//返回已经确定的位子的下标
}
//快速排序
void quickSort(int A[], int low, int high){
if(low < high){
int mid = Partition(A, low, high);
//递归
quickSort(A, low, mid-1);
quickSort(A, mid+1, high);
}
}
快速排序最好的时间复杂度是O(nlog2^n);
快速排序平均的时间复杂度是O(nlog2^n);
快速排序最坏的时间复杂度是O(n²);
快速排序最好的空间复杂度为O(log2^n);
快速排序平均的空间复杂度为O(log2^n);
快速排序最坏的空间复杂度为O(n);
快速排序是一种不稳定的排序算法(如图可见)。
• 插入排序
• 直接插入排序
对n个元素的序列A[1]~A[n],进行n-1趟排序。插入排序从第二个元素A[2]开始进行,每一趟插入排序的过程是:先将此时需要插入的元素A[i]保存到哨兵当中,然后在已有序部分进行查找,为了保证元素后移的时候不被覆盖掉,这里我们从有序部分的后面向前查找,若有序部分的元素大于哨兵中的值,则将这个元素后移,直到找到合适的位子,再将需要插入的元素放进去即可。
这里设置哨兵不仅能存放当前需要插入元素的值,以免元素后移时将它覆盖;还可以作为判断的条件,因为我们再后移的时候总是要求比该元素大的时候移动,这样即使比较到了A[0]的位置,由于哨兵中存放的与我们需要插入的元素相等,因此可以直接作为判断的条件,而不用判断是否比较到第一个节点。
代码如下:
// 直接插入排序
void insertSort(int A[], int n){
int i,j;
for(i=2; i<=n; i++){
A[0] = A[i];//A[0]为哨兵
j=i-1;
while(A[j] > A[0]){
A[j+1] = A[j];
j--;
}
A[j+1] = A[0];
}
}
直接插入排序最好的时间复杂度是O(n); 因为当序列为正序的时候不需要移动元素。
直接插入排序最坏的时间复杂度是O(n²);
直接插入排序平均的时间复杂度是O(n²);
空间复杂度为O(1); 并且直接插入排序是一种稳定的排序算法。
• 折半插入排序
顾名思义,就是在插入排序的过程中,使用二分查找来更快速地定位到该插入的位子,其余部分与插入排序一样,就不赘述了。
代码如下:
//折半插入排序
void BinsertSort(int A[], int n){
int i,j;
for(i=2; i<=n; i++){
A[0] = A[i];
int low=1, high=i-1;
while(low <= high){
int mid= (low+high)/2;
if(A[mid] > A[0]){
high = mid-1;
}
else{
low = mid+1;
}
}
for(j=i-1; j>=high; j--){
A[j+1] = A[j];
}
A[high+1] = A[0];
}
}
折半插入排序的时间复杂度是O(n²);
折半插入排序的空间复杂度是O(1);
并且折半插入排序是一种稳定的排序算法。
• 希尔排序
希尔排序又称为缩小增量排序,希尔排序的思想是把待排序的序列一个步长d 分为一组,先进行组内的插入排序,然后逐步缩小步长d,当整个表中的元素基本有序时,再对整体进行一次插入排序。
代码如下:
//希尔排序
void shellSort(int A[], int n){
for(int d=n/2; d>=1; d=d/2){
int i,j;
//分组内直接插入排序
for(i=d+1; i<=n; i++){
if(A[i] < A[i-d]){
A[0] = A[i];//哨兵
for(j=i-d; j>0 && A[0]<A[j]; j-=d){
A[j+d] = A[j];
}
A[j+d] = A[0];
}
}
}
}
希尔排序的时间复杂度是O(n^1.3);
希尔排序的空间复杂度是O(1);
希尔排序是一个不稳定的排序算法(如下图所示)。
• 归并排序
归并排序是一种稳定的排序方法。
//合并两个有序的线性表(这里把一个线性表分为前后两个部分,来进行合并)
//low,mid,high三个下标将线性表分为两部分【low~mid】【mid+1~high】
void Merge(int A[], int low, int mid, int high){
//1.申请一个与A数组大小相等的辅助数组B(下标为0不使用)
int n = high-low+1;
int* B = new int[n];
//2.将A中的数据按下标复制过去
for(int i=low; i<=high; i++){
B[i] = A[i];
}
//3.合并
int i,j,k;
for(i=low,j=mid+1,k=low; i<=mid && j<=high; k++){
if(B[i]<B[j]){
A[k] = B[i++];
}
else{
A[k] = B[j++];
}
}
//4.将剩余的直接赋过去
while(i<=mid){
A[k++] = B[i++];
}
while(j<=high){
A[k++] = B[j++];
}
}
//归并排序
void MergeSort(int A[], int low, int high){
if(low<high) {
int mid = (low+high)/2;
MergeSort(A, low, mid);
MergeSort(A, mid+1, high);
Merge(A, low, mid, high);
}
}