内部排序算法
-
排序(Sorting):将杂乱无章的数据元素(或记录),通过一定的方法按关键字顺序排列的过程叫做排序。
-
稳定排序算法:一般情况下,称某个排序算法是稳定的,指的是当待排序序列中有相同的元素时,它们的相对位置在排序前后不会发生改变。要证明一个算法不是稳定的,只需要举例排序后有相等的元素位置关系发生变化即可。
-
排序分类:
排序记录数量不同,存储在不同的存储器(内存和外存)中,由此可以将排序算法分为内部排序和外部排序。这里主要介绍内部排序。
非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破 O ( n l o g 2 n ) O(nlog_2^n) O(nlog2n),因此称为非线性时间比较类排序。
线性时间非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。
-
希尔增量序列:目前为止尚未有人求得最好的增量序列,但是需要注意:增量元素不互质,小增量可能根本不起作用,并且最有一个增量值必须是1。
-
多关键字排序:1)最高位优先法(MSD法 Most Significant Digit first);2)最低位优先法(LSD法 Least Significant Digit first)。
-
各个算法复杂度及稳定性比较
冒泡排序(Bubble Sort)
-
基本思想:假设从小到大排序,重复扫描待排序序列,并比较每一对相邻元素,当这对元素顺序不当进行交换,一直重复直到全部有序。即两个数比较大小,较大的数下沉,较小的数冒起来。
-
动图
-
算法实现:
template<class T=int> T *bubble_sort(T *arr, int len) { // 每遍历一趟,将一个最大的数移动到序列末尾。 T temp; for (int i = 0; i < len - 1; i++) { // 最多循环len - 1次 for (int j = 1; j < len - i; j++) { // 每次比较前len-i个 if (arr[j] < arr[j - 1]) { // 前边元素大,交换元素 temp = arr[j]; arr[j] = arr[j - 1]; arr[j - 1] = temp; } } } return arr; } int main() { // 学生结构体 typedef struct Stu{ int age; char name[16]; void operator=(Stu& s) { age = s.age; strcpy_s(name, s.name); } bool operator<(Stu& c) { return age < c.age; } bool operator>(Stu& c) { return age > c.age; } }STU_T; STU_T array[5] = { {7, "1"}, {0, "2"}, {4, "3"}, {6, "4"}, {0, "5"}}; STU_T* res = bubble_sort(array, 5); // 冒泡排序 for (int i=0; i < 5; i++) { printf("%d %s\t", res[i].age, res[i].name); } printf("\n"); return 0; }
-
算法复杂度
时间复杂度:O(N^2)
空间复杂度: O(1)
稳定性:稳定 -
算法改进
- 设置标志遍历pos,用于记录每趟排序中最后一次进行交换的位置。由于pos位置之后的记录均已交换到位,故在进行下一趟排序时只要扫描到pos位置即可。
template<class T = int> T* bubble_sort1(T arr[], int len) { int i = len - 1; //初始为最后一个数据元素的位置 while (i > 0) { int pos = 0; //每趟开始时,无记录交换,当遍历结束没有交货则结束排序 for (int j = 0; j < i; j++) if (arr[j] > arr[j + 1]) { pos = j; //记录交换的位置 T tmp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = tmp; } i = pos; //为下一趟排序作准备 } return arr; }
- 传统冒泡排序中每一趟排序操作只能找到一个最大值或最小值,我们考虑利用在每趟排序中进行正向和反向两遍冒泡的方法一次可以得到两个最终值(最大者和最小者) , 从而使排序趟数几乎减少了一半。
template<class T = int> T* bubble_sort2(T arr[], int len) { int low = 0, j = 0; int high = len - 1; T tmp; while (low < high) { for (j = low; j < high; ++j) //正向冒泡,找到最大者,并下沉 if (arr[j] > arr[j + 1]) { tmp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = tmp; } --high; //修改high值, 前移一位 for (j = high; j > low; --j) //反向冒泡,找到最小者,并上浮 if (arr[j] < arr[j - 1]) { tmp = arr[j]; arr[j] = arr[j - 1]; arr[j - 1] = tmp; } ++low; //修改low值,后移一位 } return arr; }
直接插入排序(Straight Insertion Sort)
-
基本思想:将数据分为有序部分和无序部分,每一步将一个无序的数据插入到前面已经排好的有序序列中,直到插完所有元素为止。
-
动图
-
算法实现
template<class T=int> T* insertion_sort(T* arr, int len) { // 插入排序(假设从小到大排序) // 第一个元素已经排好序,从第二个开始一直到末尾 for (int i = 1; i < len; i++) { T cur = arr[i]; // 哨兵位 记录正在插入的元素 int j = i; for (; j > 0; j--) { if (arr[j - 1] > cur) { // 插入 插入后的元素统一向后移动 arr[j] = arr[j - 1]; // 如果大 向后移动 } else { // 如果小于等于 插入 break; } } arr[j] = cur; } return arr; } int main() { int array[10] = { 9, 18, 32, 65, 43, 89, 99, 88, 54, 90 }; int* res = insertion_sort(array, 10); // 插入排序 for (int i=0; i < 10; i++) { printf("%d\t", res[i]); } printf("\n"); return 0; }
-
算法复杂度
正序时关键字间需要比较次数最小,为n-1,即 ∑ i = 2 n 1 \sum_{i=2}^{n}{1} i=2∑n1记录不需要移动;反之当逆序时,比较次数达到最大值(n+2)(n-1)/2,即 ∑ i = 2 n i \sum_{i=2}^{n}{i} i=2∑ni记录需要移动最大值(n+4)(n-1)/2,即 ∑ i = 2 n i + 1 \sum_{i=2}^{n}{i+1} i=2∑ni+1时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:稳定
简单选择排序(Simple Select Sort)
一种非常简单易懂的排序方法,也称为选择排序或者直接选择排序。
-
基本思想:每趟从未排序序列中找出最小(最大)元素,插入到已排序序列末尾,直到全部变为有序序列为止。
-
动图
-
算法实现
template<class T = int> T* simple_select_sort(T arr[], int len) { // 第i趟从n-i+1个记录中选出最小值和第i个记录交换,比较次数 n-i (假设升序排列) for (int i = 0; i < len; i++) { int min = i; // 记录最小值下标 初始为当前位置最小 for (int j = min + 1; j < len; j++) { if (arr[min] > arr[j]) { min = j; } } if (min != i) {// 基于交换的选择排序 交换 T temp = arr[i]; arr[i] = arr[min]; arr[min] = temp; } } return arr; } int main() { int array[15] = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48}; int* res = simple_select_sort(array, 15); for (int i = 0; i < 15; i++) { printf("%d\t", res[i]); } printf("\n"); return 0; }
-
算法复杂度
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:不确定。 基于交换的选择排序是不稳定的;基于插入的选择排序是稳定的。如果没有说明默认是基于交换的选择排序即不稳定。
希尔排序(Shell’s Sort)
希尔排序又称“缩小增量排序”(Diminishing Increment Sort),直接插入排序在序列基本有序 时或序列长度n很小时效率比较高,希尔排序正是基于这两点对直接插入排序的改进得到的排序方法。
子序列构成不是简单地逐段分割,而是将相隔一定增量的记录组成一个子序列。例如:增量取5时,序列{49 38 65 97 76 13 27 49 55 04}组成子序列为{49 13}、{38 27}、{65 49}、{97 55}、{76 04}
-
基本思想:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
-
动图
-
算法实现
template<class T=int> T* shell_sort(T arr[], int len, int dlta[], int t) { // dlta 增量序列 t 增量序列长度 for (int k = 0; k < t; k++) { int dk = dlta[k]; // 遍历增量序列 for (int i = dk; i < len; i++) { T cur = arr[i]; // 哨兵位 记录正在插入的元素 int j = i; for (; j >= dk; j-= dk) { if (arr[j - dk] > cur) { // 插入 插入后的元素统一向后移动 arr[j] = arr[j - dk]; // 如果大 向后移动 } else { // 如果小于等于 插入 break; } } arr[j] = cur; } // 打印每个增量排序后的结果 printf("第%d趟:", k+1); for (int c = 0; c < len; c++) { printf("%d\t", arr[c]); } printf("\n"); } return arr; } int main() { int array[10] = {49, 38, 65, 97, 76, 13, 27, 49, 55, 04}; int dlta[] = { 5, 3, 2, 1 }; // 增量序列 最好增量序列中没有除1之外的公因子 int* res = shell_sort(array, 10, dlta, 4); printf("排序结果:"); // 打印排序结果 for (int i = 0; i < 10; i++) { printf("%d\t", res[i]); } printf("\n"); return 0; }
-
算法复杂度
由于它的时间是所取增量序列的函数,它的分析很复杂。它的效率要比直接插入排序好很多。不稳定。
快速排序(Quick Sort)
快速排序是对冒泡排序的一种改进。
-
基本思想:通过一趟排序将待排记录按一个基准分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
-
动图
-
算法实现
template<class T> int Partition(T arr[], int low, int high) { T poivtkey = arr[low]; // 以子序列低位元素为基准 // 这么写好处是不用交换 这里保存了基准(地位),开始把其他小的元素填充到这里, // 然后交换把大元素填充到刚才小的元素,如此交替,最后在结束的地方填充基准 // 也可以以高位为基准,相应的要先用大的元素填充高位。(即先在low段找大值) while (low < high) { while (low < high && poivtkey <= arr[high]) { --high; } arr[low] = arr[high]; while (low < high && poivtkey >= arr[low]) { ++low; } arr[high] = arr[low]; } arr[low] = poivtkey; return high; // 返回基准的位置 这里low==high } template<class T> void QSort(T arr[], int low, int high) { if (low < high) {// 当low==high就是递归的出口 int pivotloc = Partition(arr, low, high); // 划分并找到基准位置下标 QSort(arr, low, pivotloc - 1); // 递归排序比基准值左边部分 QSort(arr, pivotloc + 1, high); // 递归排序比基准右边部分 } } template<class T> T* quick_sort(T arr[], int len) { // 快速排序 默认升序 QSort(arr, 0, len - 1); return arr; } int main() { int array[10] = {84, 83, 88, 87, 61, 50, 70, 60, 80, 99}; int* res = quick_sort(array, 10); printf("排序结果:"); // 打印排序结果 for (int i = 0; i < 10; i++) { printf("%d\t", res[i]); } printf("\n"); return 0; }
-
算法复杂度
时间复杂度: O ( n l o g 2 n ) O(nlog_2^n) O(nlog2n)
空间复杂度: O ( n l o g 2 n ) O(nlog_2^n) O(nlog2n)
稳定性:不稳定
归并排序
-
基本思想:归并就是将两个(或者两个以上)有序序列合并为一个新的有序表;即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。(分治)
2-路归并排序:假设初始序列含有n个记录,则可看做n个有序序列,两两归并,得到⌈n/2⌉个长度为2或1的有序子序列,以此类推,最终得到一个长度为n的有序序列。 -
动图
-
算法实现
template<class T> void Merge(T arr[], int l,int m, int r) { // 合并 将T[l...m] 和T[m+1, r]两个数组归并为T[l..r] T *p_arr = (T*)malloc(sizeof(T)*(r + 1 - l)); // 这里使用了辅助空间 int k = 0, i = l, j = m + 1; for (; i <= m && j <= r; k++) { // 找完一个子序列为止 if (arr[i] <= arr[j]) { p_arr[k] = arr[i++]; } else { p_arr[k] = arr[j++]; } } while (i <= m) // 插入没有进入辅助空间的剩余元素 { p_arr[k++] = arr[i++]; } while (j <= r) { p_arr[k++] = arr[j++]; } i = l, k = 0; printf("\nmerge%d-%d-%d", l, m, r); while (i<=r) { arr[i++] = p_arr[k++]; printf("\t%d", arr[i-1]); // 打印刚才排序的序列 } free(p_arr); // 释放堆内存空间 } template<class T> void MSort(T arr[], int low, int high) { // 划分 这里为了方便看递归调用顺序打印了提示信息, 可以自行注释 if (low < high) { // 如果low==high表示一个元素,直接返回停止递归 int m = (low + high) / 2; printf("\n划分 b %d %d", low, m); MSort(arr, low, m); printf("\n划分 m %d %d", m+1, high); MSort(arr, m + 1, high); printf("\n合并%d %d %d", low, m, high); Merge(arr, low, m, high); } } template<class T> T* merge_sort(T arr[], int len) { // 归并排序 递归写法 可以转为迭代方法后续加进来 MSort(arr, 0, len - 1); return arr; } int main() { int array[10] = {84, 83, 88, 87, 61, 50, 70, 60, 80, 99}; printf("原始数据:"); for (int i = 0; i < 10; i++) { printf("%d\t", array[i]); } int* res = merge_sort(array, 10); printf("\n排序结果:"); // 打印排序结果 for (int i = 0; i < 10; i++) { printf("%d\t", res[i]); } printf("\n"); return 0; }
-
算法复杂度
时间复杂度: O ( n l o g 2 n ) O(nlog_2^n) O(nlog2n)
空间复杂度: O ( n ) O(n) O(n)
稳定性:稳定
堆排序(Heap Sort)
堆排序是一种树形选择排序,是对直接选择排序的有效改进。
-
基本思想:将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆; 将堆顶元素与末尾元素交换,将最大(小)元素"沉"到数组末端; 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
-
动图
-
算法实现
template<class T>
void adjust_heap(T* arr, int cur, int len) {
T temp = arr[cur];
for (int i = 2 * cur + 1; i < len; i = 2 * i + 1) {
if (i + 1 < len && arr[i + 1] > arr[i]) {// 存在右子结点 且右子结点大
i = i + 1; // 指向右子结点
}
if (arr[i] > temp) {
arr[cur] = arr[i];
cur = i;
}
else {
break;
}
}
arr[cur] = temp;
}
template<class T>
T* heap_sort(T arr[], int len) {
// 堆排序
// 将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
// 将堆顶元素与末尾元素交换,将最大(小)元素"沉"到数组末端;
// 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
// 构建大顶堆 这里是升序排列
for (int i = len / 2 - 1; i >= 0; --i) {
adjust_heap(arr, i, len);
}
// 交换,重新调整结构
T temp = 0;
for (int j = len - 1; j > 0; --j) {
temp = arr[0]; arr[0] = arr[j]; arr[j] = temp;
adjust_heap(arr, 0, j);
}
return arr;
}
int main()
{
int array[10] = {84, 83, 8, 187, 661, 50, 70, 60, 8, 999};
printf("原始数据:");
for (int i = 0; i < 10; i++) {
printf("%d\t", array[i]);
}
int* res = heap_sort(array, 10);
printf("\n排序结果:"); // 打印排序结果
for (int i = 0; i < 10; i++) {
printf("%d\t", res[i]);
}
printf("\n");
return 0;
}
- 算法复杂度
时间复杂度: O ( n l o g 2 n ) O(nlog_2^n) O(nlog2n)
空间复杂度: O ( 1 ) O(1) O(1)
稳定性:不稳定
基数排序(Radix Sorting)
基数排序和其他排序方法不同 ,一般排序方法主要通过关键字间比较和移动记录这两种操作排序,而基数排序不需要进行记录关键字间的比较。
-
基本思想:将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
-
动图
-
算法实现
int* radix_sort(int arr[], int len) { // 基数排序 // 是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位 int max = -1; // 这里排序的数字都是非负数, 所以初始为-1 for (int i = 0; i < len; ++i) { if (max < arr[i]) { max = arr[i]; } } int order = 1; // 从10^0开始,一直到最大位 int index = 0; // std::list<int> bukct[10] = {}; while (order <= max) { // 低位排序 即放入0-9不同的桶 for (int i = 0; i < len; ++i) { index = arr[i] / order % 10; // 这样计算位数小的自然填0 bukct[index].push_back(arr[i]); } // 收集 即从桶里依次取出到数组 for (int i = 0, j = 0; i < 10; ++i) { while (!bukct[i].empty()) { arr[j++] = bukct[i].front(); bukct[i].pop_front(); } } order *= 10; } return arr; } int main() { int array[10] = {84, 83, 8, 187, 661, 50, 70, 60, 8, 999}; printf("原始数据:"); for (int i = 0; i < 10; i++) { printf("%d\t", array[i]); } int* res = radix_sort(array, 10); printf("\n排序结果:"); // 打印排序结果 for (int i = 0; i < 10; i++) { printf("%d\t", res[i]); } printf("\n"); return 0; }
-
算法复杂度
时间复杂度:O(nk)
空间复杂度:O(nk)
稳定性:稳定
参考:
说明:图片来自网络和以上博客,如有侵权请联系删除