排序
1)插入
①直接插入排序
思路
当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移
特性
- 元素集合越接近有序,直接插入排序算法的时间效率越高
- 时间复杂度:O(N^2)
- 空间复杂度:O(1),它是一种稳定的排序算法
- 稳定性:稳定
代码如下
void InsertSort(int* a, int n) { for (int i = 0; i < n - 1; i++) { int end = i; int tmp = a[end + 1]; while (end >= 0) { if (tmp < a[end]) { a[end + 1] = a[end]; //这里没有a[end]=tmp加上了就不是插入排序的思路了 end--; } else { break; } a[end + 1] = tmp; //注意这里不要写为了tmp=a[end + 1] 插入排序思路梳理清楚再写代码 } } }
②希尔排序
思路
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。
特性
- 希尔排序是对直接插入排序的优化
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比
- 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定(
一般约等于O(N^1.3)
)- 稳定性:
不稳定
代码如下:(
经过测试 gap = gap / 3 + 1 稍快
,当然gap=gap/2也可
)// 希尔排序 void ShellSort(int* a, int n) { int gap = n; while (gap > 1) { gap = gap / 3 + 1; //gap=gap/2; for (int i = 0; i < n - gap; i++) { int end = i; int tmp = a[end + gap]; while (end >= 0) { if (tmp < a[end]) { a[end + gap] = a[end]; end = end - gap; } else { break; } a[end + gap] = tmp; } } } }
2)选择
①选择排序
思路
前后分别定义一个begin,end作为待排序的数据的区间,每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,begin加一,end减一,只要begin小于end就一直循环操作,直到全部待排序的数据元素排完
特性
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
代码如下
// 选择排序 void SelectSort(int* a, int n) { int begin = 0; int end = n - 1; int mini, maxi = begin; while (begin < end) { mini = begin; maxi = begin; for (int i = begin; i <= end; i++) { if (a[i] > a[maxi]) { maxi = i; } if (a[i] < a[mini]) { mini = i; } } if (maxi == begin) //如果最大值在begin,此时最小值交换了后,最大值记录的maxi就不在begin>上了,而在mini上 。所以要加一个且仅需if判断 { int tmp = a[begin]; a[begin] = a[mini]; a[mini] = tmp; tmp = a[end]; a[end] = a[mini]; a[mini] = tmp; } else { int tmp = a[begin]; a[begin] = a[mini]; a[mini] = tmp; tmp = a[end]; a[end] = a[maxi]; a[maxi] = tmp; } ++begin; --end; } }
②堆排序
特性
- 堆排序使用堆来选数,效率就高了很多
- 时间复杂度:O(N*logN)
- 空间复杂度:O(1)
- 稳定性:不稳定
代码如下
// 堆排序 void AdjustDown(int* a, int n, int parent) { int child = parent * 2 + 1; while (child<n) { if (child<n-1 && a[child] < a[child + 1])//child<n-1防止越界 { child = child + 1; } if (a[child] > a[parent]) { int tmp = a[child]; a[child] = a[parent]; a[parent] = tmp; parent = child; child = parent * 2 + 1; } else break; } } void HeapSort(int* a, int n) { for (int i = ((n - 1) - 1) / 2; i >= 0; i--) { AdjustDown(a, n, i); } int end = n - 1; while(end>0) { int tmp = a[end]; a[end] = a[0]; a[0] = tmp; end--; AdjustDown(a, end + 1, 0); } }
3)交换
①冒泡排序
思路
:算法之冒泡排序(优化)
特性
- 冒泡排序是一种非常容易理解的排序
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
②快速排序
Ⅰ:递归法(三种)(均需先找一个key值)
1. hoare版本(最初版)
思路
:
最左边为key就左边先走找比key大,右边后走找比key小,找到就停下来交换在继续直到左右相遇
最右边为key反之
下图为以右边为key
(图片为互联网搜集)
代码如下:// 快速排序hoare版本 void PartSort1(int* a, int left, int right) { int Right = right; if (right - left <= 1) return; ///三数取中法优化部分 int tmpK = FindTheKey(a, left, right);//三数取中法找key 用tmpK记录 int tmp = a[left]; a[left] = a[tmpK]; a[tmpK] = tmp;//记住要交换 保证key在最左边(使key在left) ///三数取中法优化部分 int keyi = left; int key = a[keyi];//记录key while (left < right) { //右边找小 while (a[right] >= key && left < right)//包含等于情况 { right--; } //左边找大 while (a[left] <= key && left < right) { left++; } tmp = a[left]; a[left] = a[right]; a[right] = tmp; } tmp = a[left];//left right相遇,left==right a[left] = a[keyi]; a[keyi] = tmp; PartSort1(a, 0, keyi - 1); PartSort1(a, keyi + 1, Right); }
2. 挖坑法
思路
:
- 与第一种方法一样也是先找key(三数取中法取出的数)与最左边(最右边也可以)
- 记录下key
- key的位置记为坑(keyi)
- 从right开始向左找小,找到就填入坑里,此时坑(keyi)更新为right。right停下来后left就向右开始找大,找到就添入坑里,此时坑(keyi)更新为left,
- 直到left和right相遇,此时right==left,此时把最开始记录下来的key添入坑里
代码如下:
} // 快速排序挖坑法 void PartSort2(int* a, int left, int right) { int Right = right; if (right - left <= 1) return; int tmpK= FindTheKey(a, left, right);//三数取中法找坑 用tmpK记录 int tmp = a[left]; a[left] = a[tmpK]; a[tmpK] = tmp;//记住要交换(使坑在首位) int kengi = left; int key = a[kengi];//记录key while (left < right) { //右边找小,填坑 while (a[right] >= key && left < right)//包含等于情况???? { right--; } a[kengi] = a[right]; kengi = right; //左边找大,填坑 while (a[left] <= key && left < right) { left++; } a[kengi] = a[left]; kengi = left; } a[kengi] = key;//最后把记录的key填坑 PartSort2(a, 0, kengi - 1); PartSort2(a, kengi+1, Right); }
3. 前后指针版本(key在左边和右边有少许区别)
思路(key在左边)
:
- prev下标为0,cur下标1开始
- cur往前走,找比key小的数据
- 找到此key小的数据以后,停下来,++prev
- 交换prev和cur指向位置的值,直到cur走到数组尾
- 最后prev位置再和key交换
思路(key在右边与左边区别)
:
- prev开始在下标-1位置,cur在下标0位置
- cur遇到key停下来,此时prev还要先++再和key交换
注意
cur还没遇到比key大的值前,prev要么紧跟着cur
cur遇到比key大的值后,prev和cur之 间间隔一段比key大的数据
优化
:
所以优化在if判断a[cur] < a[keyi]
时加个a[++prev]!=a[cur]
在prev紧跟cur时避免多余的交换
代码如下(
key在左边
)// 快速排序前后指针法(keyi在最左边) void PartSort3_left(int* a, int left, int right) { int keyi = left; int prev = left; int cur = prev + 1; if (cur > right) return; while (cur <= right) { if (a[cur] < a[keyi]&&a[++prev]!=a[cur]) //a[++prev]!=a[cur]是优化 如果cur一直都是找到比a[keyi]小的,prev++每次都会和cur一样,这样if内的交换就会很多余 { int tmp = a[prev]; a[prev] = a[cur]; a[cur] = tmp; } cur++; } int tmp = a[keyi]; a[keyi] = a[prev]; a[prev] = tmp; PartSort3_left(a, 0, prev-1); PartSort3_left(a, prev+1, right); }
代码如下(
key在右边
)// 快速排序前后指针法(keyi在最右边) void PartSort3_right(int* a, int left, int right) { int keyi = right; int prev = left-1; int cur = left; if (cur > keyi-1) return; while (cur < keyi) { if (a[cur] < a[keyi] && a[++prev] != a[cur]) //a[++prev]!=a[cur]是对冗余交换的优化 如果cur一直都是找到比a[keyi]小的,prev++每次都会和cur一样,这样if内的交换就会很多余 { int tmp = a[prev]; a[prev] = a[cur]; a[cur] = tmp; } cur++; } prev++; int tmp = a[keyi]; a[keyi] = a[prev]; a[prev] = tmp; PartSort3_right(a, 0, prev - 1); PartSort3_right(a, prev + 1, right); }
附:递归
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
// 小区间优化,当分割到小区间时,不再用递归分割思路让这段子区间有序
// 对于递归快排,减少递归次数
if (right - left + 1 < 10)
{
InsertSort(a + left, right - left + 1);
}
else
{
//类似二叉树遍历,根左右
int keyi = Partion(a, left, right);
// [left, keyi-1] keyi [keyi+1, right]
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
}
Ⅱ:非递归法
参考:递归与非递归的转换
递归转为非递归写法一般有两种
:
- 直接循环迭代
- 栈+循环迭代
非递归快排思路:
- 建立一个栈
- 由于在递归法中每次我们都是以key为中间,两边分别作为一个数组进行递归操作
- 第一次入栈时是吧此数组的首位下标压进去(
先压尾再压首
),记为begin end,- 进入循环只要栈不为空,就继续调用快排函数(单趟版 没有递归),以前后指针版为例,每次排完prev位置就是新key的位置
- 分别判断prev左边(begin是否小于prev-1)右边(prev+1是否小于end)成立就继续以两个新数组进行压栈操作(
先压尾再压首
)- 直到prev左右两边分别只有一个数截止
注意:
每次取栈顶后要pop栈
代码如下:
// 快速排序前后指针法(栈+迭代循环key在左边) void Stack_While_PartSort3_left(int* a, int left, int right) { ST st; StackInit(&st); StackPush(&st, right); StackPush(&st, left); while (!StackEmpty(&st)) { int begin= StackTop(&st); StackPop(&st); int end= StackTop(&st); StackPop(&st); //调用的快排函数(单趟版 没有递归) //PartSort1(int* a, int left, int right) //PartSort2(int* a, int left, int right) //PartSort3_left(int* a, int left, int right) //调用的快排函数(单趟版 没有递归) //keyi = prev; if (prev - 1 > begin) { StackPush(&st, prev-1); StackPush(&st, begin); } if (prev + 1 < end) { StackPush(&st, end); StackPush(&st, prev + 1); } } }
Ⅲ:三数取中法(对选key的优化)
思路
对数组的前中后三个位置选择,选出值中等的那个值返回下标
代码如下:
//三数取中法 int FindTheKey(int *a, int left, int right) { unsigned int mid = left + (right - left) / 2;//为了防止left+right溢出int同int mid = (left+right)/2 也可以mid = >(begin + end) >> 1; if (a[left] > a[mid]) { if (a[right] < a[mid]) return mid; else if (a[right] > a[left]) return left; else return right; } else // a[left] < a[mid] { if (a[right] > a[mid]) return mid; else if (a[right] < a[left]) return left; else return right; } }
快排特性
:
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
- 时间复杂度:O(N*logN)
- 空间复杂度:O(logN)
- 稳定性:不稳定
4)归并
①归并排序
Ⅰ:递归版本
思路
:
该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。如下图所示
特性
:
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
代码如下
归并排序递归实现 void MergeSort(int* a, int n) { int *tmp = (int *)malloc(sizeof(int)*n);//临时数组 _MergeSort(a, tmp, 0, n-1);//right是下标 free(tmp); } void _MergeSort(int* a, int *tmp, int left, int right) { if (left >= right) return; int mid = (left + right) / 2; _MergeSort(a, tmp, left, mid); _MergeSort(a, tmp, mid+1, right); int begin1 = left; int end1 = mid; int begin2 = mid + 1; int end2 = right; int tmpi = left; while ((begin1 <= end1) && (begin2 <= end2)) { if (a[begin1] < a[begin2]) { tmp[tmpi++] = a[begin1++]; } else { tmp[tmpi++] = a[begin2++]; } } while (begin1 <= end1) { tmp[tmpi++] = a[begin1++]; } while (begin2 <= end2) { tmp[tmpi++] = a[begin2++]; } for (int i = 0; i < tmpi; i++) { a[i] = tmp[i]; } }
Ⅱ:非递归版本
参考:递归与非递归的转换
非递归版本思路
:
- 主体交换部分和递归一样
- 非递归版本不需要
分解
操作- 定义一个MergeNum第一次while循环(a[0] a[1]) (a[2] a[3])… 归并
- 第二个while循环(a[0]a[1] a[2]a[3]) (a[4]a[5] a[6]a[7])… 归并
- 以此类推
注意
:需要注意控制的地方
- while中的for循环
- begin1 end1 begin2 end2
- 奇数数组情况,begin2,end2超出n-1情况
加粗样式
归并排序非递归实现 void MergeSortNonR(int* a, int n) { int *tmp = (int *)malloc(sizeof(int)*n); int MergeNum = 1; while (MergeNum < n) { for (int i = 0; i < n; i += (2*MergeNum)) //注意控制 { int begin1 = i; int end1 = begin1+MergeNum - 1; int begin2 = begin1+ MergeNum; int end2 = begin2 + MergeNum - 1;//注意控制 int tmpi = begin1; if (begin2 > n-1) { break; } if (end2 > n - 1) { end2 = n - 1; } while ((begin1 <= end1) && (begin2 <= end2)) { if (a[begin1] < a[begin2]) { tmp[tmpi++] = a[begin1++]; } else { tmp[tmpi++] = a[begin2++]; } } while (begin1 <= end1) { tmp[tmpi++] = a[begin1++]; } while (begin2 <= end2) { tmp[tmpi++] = a[begin2++]; } for (int j = 0; j < tmpi; j++) { a[j] = tmp[j]; } } MergeNum *= 2;//注意控制 } free(tmp); }
②归并排序外排序实现
5)非比较排序
①计数排序
计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。
思路
:
- 统计相同元素出现次数 (
元素就是下标所以省去的所有的比较,交换
)- 根据统计的结果将序列回收到原来的序列中
特性
:
- 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
- 时间复杂度:O(MAX(N,范围))
- 空间复杂度:O(范围)
代码如下
计数排序 void CountSort(int* a, int n) { int min = a[0]; int max = a[0]; for (int i = 1; i < n; i++) { if (a[i] < min) min = a[i]; if (a[i] > max) max = a[i]; } int maxi = max-min; int *tmp = calloc(maxi+1, sizeof(int)); //calloc第一个参数是开辟的个数,所以注意要下标加一 int ai = 0; for (int i = 0; i < n; i++) { tmp[a[i] - min]++; } for (int i = 0; i <=maxi; i++) { if (tmp[i] != 0) { while (tmp[i]--) { a[ai++] = i + min; } } } }
分析:
如代码所示由于可能出现待排序的数组元素过大情况,会浪费很多空间,所以我们找出最小的数让(每个元素-最小数)作为此元素的映射,最后再加上此最小数即可
注意:
当待排序的数组元素差值过大时会很不实用,此算法只适用于特定场景
②基数排序
基数排序相当于桶排序的升级版,这里省略桶排序
Ⅰ:LSD(Least Significant Digit)
思路
:
- 先计算出最高位(以最大的数为基准)
- 建10个队列相当于10个桶
- 每次遍历数组将digit位为i的数放入下标为i的桶,遍历完再取出放入数组(
注意先进先出
)- 重复直到最高位进行完
特性
:
- LSD的基数排序适用于位数少的数列,
- 最好、最坏、平均时间复杂度:
O(d*(n+k))
(d是最大数的位数k是进制)- 空间复杂度:O(n+k)
- 稳定性:稳定
代码如下:
//基数排序LSD(Least Significant Digit) void LSD_RadixSort(int *a, int n) { int max = a[0];//有多少位数就进行几次,所以先找最大的数 for (int i = 0; i < n; i++) { if (a[i] > max) max = a[i]; } int digit = 0; while (max)//求位数 { max /= 10; digit++; } Queue* QueueArr = (Queue*)malloc(sizeof(Queue)*n); for (int i = 0; i < n; i++) { QueueInit(&(QueueArr[i])); } int* tmp = (int *)malloc(sizeof(int)*n); for (int i = 0; i < digit; i++)//进行位数次,0也要算 { int QAi = 0; for (int jj = 0; jj < n; jj++) { QAi= (int)(a[jj] / pow(10, i)) % 10;//求a[jj]此时位数上的值 QueuePush(&(QueueArr[QAi]), a[jj]); } int tmpi = 0; for (int ii = 0; ii < n; ii++) { while(!QueueEmpty(&(QueueArr[ii]))) { int ret = QueueFront(&(QueueArr[ii])); tmp[tmpi] = ret; QueuePop(&(QueueArr[ii])); tmpi++; } } for (int i = 0; i < n; i++)//复制回a数组 { a[i] = tmp[i]; } } free(QueueArr); free(tmp); }
Ⅱ:MSD(Most Significant Digit)
思路
:如图所示
MSD从高位开始排到低位,排完一位后不合并桶,相同的高位划分子桶继续分配,最后递归合并
特性
:如果位数多的话,使用MSD的效率会比较好
代码如下://基数排序MSD(Most Significant Digit)递归 int max_digit(int *a, int n) { int max = a[0];//有多少位数就进行几次,所以先找最大的数 for (int i = 0; i < n; i++) { if (a[i] > max) max = a[i]; } int digit = 0; while (max)//求位数 { max /= 10; digit++; } } void MSD_RadixSort(Queue *arr, int n,int digit) { Queue QueueArr[10]; if (digit >= 0 && n > 1) { for (int i = 0; i < 10; i++) { QueueInit(&(QueueArr[i])); } while(!QueueEmpty(arr)) { int front = QueueFront(arr); int QAi = (int)(front / pow(10, digit)) % 10; QueuePush(&(QueueArr[QAi]), QueueFront(arr)); QueuePop(arr); } for (int i = 0; i < 10; i++) { if (!QueueEmpty(&(QueueArr[i]))) { MSD_RadixSort(&(QueueArr[i]), QueueSize(&(QueueArr[i])), digit - 1); } while(!QueueEmpty(&(QueueArr[i]))) { QueuePush(arr, QueueFront(&(QueueArr[i]))); QueuePop(&(QueueArr[i])); } } } } void Main_MSD_RadixSort(int *a, int n) { Queue arr; QueueInit(&arr); int digit = max_digit(a, n); for (int i = 0; i < n; i++)//入队 { QueuePush(&arr, a[i]); } MSD_RadixSort(&arr, n, digit - 1);//从最高位开始 for (int i = 0; i < n; i++)//出队 { a[i] = QueueFront(&arr); QueuePop(&arr); } }
6)排序总结
如果序列接近有序,所以如果是插入排序,时间复杂度逼近O(n)
快排接近O(N^2),归并和堆排还是O(n*logn)
排序稳定性
如果排好序以后可能导致相同值元素的相对元素顺序交换就是不稳定,反之稳定
稳定性场景:
按成绩排序,且如果分数一样先交卷的在数组前面 且应该排序考前