系统性学习数据结构-第五讲-排序
系统性学习数据结构-第五讲-排序
- 1. 排序概念及运用
- 1.1 排序
- 1.2 运用
- 1.3 常见排序算法
- 2. 实现常见排序算法
- 2.1 插入排序
- 2.1.1 直接插入排序
- 2.1.2 希尔排序
- 2.1.2.1 希尔排序的时间复杂度计算
- 2.2 选择排序
- 2.2.1 直接选择排序
- 2.2.2 堆排序
- 2.3 交换排序
- 2.3.1 冒泡排序
- 2.3.2 快速排序
- 2.3.2.1 hoare 版本
- 2.3.2.2 挖坑法
- 2.3.2.3 lomuto 前后指针
- 2.3.2.4 非递归版本
- 2.4 归并排序
- 2.5 测试代码:排序性能对比
- 2.6 非比较排序
- 2.6.1 计数排序
- 3. 排序算法复杂度及稳定性分析
1. 排序概念及运用
1.1 排序
所谓排序,就是使⼀串记录,按照其中的某个或某些关键字的⼤⼩,递增或递减的排列起来的操作。
1.2 运用
购物筛选排序:
院校排名:
1.3 常见排序算法
2. 实现常见排序算法
int a[] = {5, 3, 9, 6, 2, 4, 7, 1, 8};
2.1 插入排序
基本思想:
直接插入排序是⼀种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,
直到所有的记录插入完为止,得到⼀个新的有序序列。
实际中我们玩扑克牌时,就用了插入排序的思想。
2.1.1 直接插入排序
当插入第 i(i >= 1) 个元素时,前面的 array[0], array[1], …, array[i-1] 已经排好序,
此时用 array[i] 的排序码与 array[i-1], array[i-2], … 的排序码顺序进行比较,找到插入位置即将 array[i] 插入,
原来位置上的元素顺序后移。
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 (a[end] > tmp) {a[end + 1] = a[end];end--;}else {break;}}a[end + 1] = tmp;}
}
直接插入排序的特性总结
-
元素集合越接近有序,直接插入排序算法的时间效率越高
-
时间复杂度: O(N2)
-
空间复杂度: O(1)
2.1.2 希尔排序
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定⼀个整数(通常是 gap = n / 3 + 1 ),
把待排序文件所有记录分成各组,所有的距离相等的记录分在同⼀组内,并对每⼀组内的记录进行排序,
然后 gap = gap / 3 + 1 得到下⼀个整数,再将数组分成各组,进行插入排序,当 gap = 1 时,就相当于直接插入排序。
它是在直接插入排序算法的基础上进行改进而来的,综合来说它的效率肯定是要高于直接插入排序算法的。
希尔排序的特性总结
-
希尔排序是对直接插入排序的优化。
-
当 gap > 1 时都是预排序,目的是让数组更接近于有序。当 gap == 1 时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。
代码实现:
代码实现:
void ShellSort(int* a, int n)
{int gap = n;while (gap > 1){//推荐写法:除3 gap = gap / 3 + 1;for (int i = 0; i < n - gap; i++){int end = i;int tmp = a[end + gap];while (end >= 0){if (a[end] > tmp) { a[end + gap] = a[end];end -= gap;}else {break;}}a[end + gap] = tmp;}}}
2.1.2.1 希尔排序的时间复杂度计算
希尔排序的时间复杂度估算:
外层循环:
外层循环的时间复杂度可以直接给出为: O(log2 n) 或者 O(log3n) ,即 O(log n)
内层循环:
因此,希尔排序在最初和最后的排序的次数都为 n ,即前⼀阶段排序次数是逐渐上升的状态,
当到达某⼀顶点时,排序次数逐渐下降至 n ,而该顶点的计算暂时⽆法给出具体的计算过程,希尔排序时间复杂度不好计算,
因为 gap 的取值很多,导致很难去计算,
因此很多书中给出的希尔排序 的时间复杂度都不固定。《 数据结构(C语言版) 》— 严蔚敏书中给出的时间复杂度为:
2.2 选择排序
选择排序的基本思想:
每⼀次从待排序的数据元素中选出最小(或最大)的⼀个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
2.2.1 直接选择排序
-
在元素集合 array[i] – array[n-1] 中选择关键码最大(小)的数据元素
-
若它不是这组元素中的最后⼀个(第⼀个)元素,则将它与这组元素中的最后⼀个(第⼀个)元素交换
-
在剩余的 array[i] – array[n-2](array[i+1] – array[n-1]) 集合中,重复上述步骤,直到集合剩余 1 个元素
void SelectSort(int* a, int n)
{int begin = 0, end = n - 1;while (begin < end){int 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 (begin == maxi) {maxi = mini;}swap(&a[mini], &a[begin]);swap(&a[maxi], &a[end]);++begin;--end;}
}
直接选择排序的特性总结:
-
直接选择排序思考⾮常好理解,但是效率不是很好,实际中很少使用。
-
时间复杂度: O(N2)
-
空间复杂度: O(1)
2.2.2 堆排序
堆排序 (Heapsort) 是指利用堆积树(堆)这种数据结构所设计的⼀种排序算法,它是选择排序的⼀种。它是通过堆来进行选择数据。
需要注意的是排升序要建大堆,排降序建小堆。在二叉树章节我们已经实现过堆排序,这里不再赘述。
2.3 交换排序
交换排序基本思想:
所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置。
交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
2.3.1 冒泡排序
前面在算法题中我们已经接触过冒泡排序的思路了,冒泡排序是⼀种最基础的交换排序。
之所以叫做冒泡排序,因为每⼀个元素都可以像小气泡⼀样,根据自身大小⼀点⼀点向数组的⼀侧移动。
代码实现:
void BubbleSort(int* a, int n)
{int exchange = 0;for (int i = 0; i < n; i++){for (int j = 0; j <n-i-1 ; j++){if (a[j] > a[j + 1]) {exchange = 1;swap(&a[j], &a[j + 1]);}}if (exchange == 0) {break;}}
}
冒泡排序的特性总结:
-
时间复杂度: O(N2)
-
空间复杂度: O(1)
2.3.2 快速排序
快速排序是 Hoare 于 1962 年提出的⼀种二叉树结构的交换排序方法,
其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,
左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,
直到所有元素都排列在相应位置上为止。
快速排序实现主框架:
//快速排序
void QuickSort(int* a, int left, int right)
{if (left >= right) {return;}//_QuickSort⽤于按照基准值将区间[left,right)中的元素进⾏划分 int meet = _QuickSort(a, left, right);QuickSort(a, left, meet - 1);QuickSort(a, meet + 1, right);
}
将区间中的元素进行划分的 _QuickSort 方法主要有以下几种实现方式:
2.3.2.1 hoare 版本
算法思路:
-
创建左右指针,确定基准值
-
右指针从右向左找出比基准值小的数据,左指针从左向右找出比基准值大的数据,左右指针数据交换,进入下次循环。
❓问题1:为什么跳出循环后 right 位置的值⼀定不大于 key ?
当 left > right 时,即 right 走到 left 的左侧,而 left 扫描过的数据均不⼤于 key ,
因此 right 此时指向的数据⼀定不大于 key
❓问题2:为什么 left 和 right 指定的数据和 key 值相等时也要交换么 ?
相等的值参与交换确实有⼀些额外消耗。实际还有各种复杂的场景,假设数组中的数据大量重复时,无法进行有效的分割排序。
代码实现:
int _QuickSort(int* a, int left, int right)
{int begin = left;int end = right;int keyi = left;++left;while (left <= right){// 右边找⼩ while (left <= right && a[right] > a[keyi]){--right;}// 左边找大 while (left <= right && a[left] < a[keyi]){++left;}if (left <= right){swap(&a[left++], &a[right--]);}}swap(&a[keyi], &a[right]);return right;
}
2.3.2.2 挖坑法
思路:
创建左右指针。首先右指针从右向左找出比基准小的数据,找到后立即放入左边坑中,当前位置变为新的 “坑” ,
然后从左向右找出比基准大的数据,找到后立即放入右边坑中,当前位置变为新的 “坑” ,
结束循环后将最开始存储的分界值放入当前的 “坑” 中,返回当前 “坑” 下标(即分界值下标)
int _QuickSort(int* a, int left, int right)
{int mid = a[left];int hole = left;int key = a[hole];while (left < right){while (left < right && a[right] >= key) {--right;}a[hole] = a[right];hole = right;while (left < right && a[left] <= key){++left;}a[hole] = a[left];hole = left;}a[hole] = key;return hole;
}
2.3.2.3 lomuto 前后指针
创建前后指针,从左往右找比基准值小的进行交换,使得小的都排在基准值的左边。
int _QuickSort(int* a, int left, int right)
{int prev = left, cur = left + 1;int key = left;while (cur <= right){if (a[cur] < a[key] && ++prev != cur) {swap(&a[cur], &a[prev]);}++cur;}swap(&a[key], &a[prev]);return prev;
}
快速排序特性总结:
-
时间复杂度: O(nlogn)
-
空间复杂度: O(logn)
2.3.2.4 非递归版本
非递归版本的快速排序需要借助数据结构:栈
代码实现:
void QuickSortNonR(int* a, int left, int right)
{ST st;STInit(&st);STPush(&st, right);STPush(&st, left);while (!STEmpty(&st)){int begin = STTop(&st);STPop(&st);int end = STTop(&st);STPop(&st);// 单趟 int keyi = begin;int prev = begin;int cur = begin + 1;while (cur <= end){if (a[cur] < a[keyi] && ++prev != cur)Swap(&a[prev], &a[cur]);++cur;}Swap(&a[keyi], &a[prev]);keyi = prev;// [begin, keyi-1] keyi [keyi+1, end] if (keyi + 1 < end){STPush(&st, end);STPush(&st, keyi + 1);}if (begin < keyi-1){STPush(&st, keyi-1);STPush(&st, begin);}}STDestroy(&st);
}
2.4 归并排序
归并排序算法思想:
归并排序(MERGE-SORT)是建立归并操作上的⼀种有效的排序算法,
该算法是采用分治法(Divide and Conquer)的⼀个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;
即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成⼀个有序表,称为⼆路归并。
归并排序核心步骤:
void _MergeSort(int* a, int left, int right, int* tmp)
{if (left >= right) {return;}int mid = (right + left) / 2;//[left,mid] [mid+1,right]_MergeSort(a, left, mid, tmp);_MergeSort(a, mid + 1, right, tmp);int begin1 = left, end1 = mid;int begin2 = mid + 1, end2 = right;int index = begin1;//合并两个有序数组为⼀个数组 while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]) {tmp[index++] = a[begin1++];}else {tmp[index++] = a[begin2++];}}while (begin1 <= end1){tmp[index++] = a[begin1++];}while (begin2 <= end2){tmp[index++] = a[begin2++];}for (int i = left; i <= right; i++){a[i] = tmp[i];}
}void MergeSort(int* a, int n)
{int* tmp = new int[n];_MergeSort(a, 0, n - 1, tmp);delete[] tmp;
}
归并排序特性总结:
-
时间复杂度: O(nlogn)
-
空间复杂度: O(n)
2.5 测试代码:排序性能对比
// 测试排序的性能对⽐
void TestOP()
{ srand(time(0)); const int N = 100000; int* a1 = (int*)malloc(sizeof(int)*N); int* a2 = (int*)malloc(sizeof(int)*N); int* a3 = (int*)malloc(sizeof(int)*N); int* a4 = (int*)malloc(sizeof(int)*N); int* a5 = (int*)malloc(sizeof(int)*N); int* a6 = (int*)malloc(sizeof(int)*N); int* a7 = (int*)malloc(sizeof(int)*N); for (int i = 0; i < N; ++i) { a1[i] = rand(); a2[i] = a1[i]; a3[i] = a1[i]; a4[i] = a1[i]; a5[i] = a1[i]; a6[i] = a1[i];a7[i] = a1[i]; } int begin1 = clock(); InsertSort(a1, N); int end1 = clock(); int begin2 = clock(); ShellSort(a2, N); int end2 = clock(); int begin3 = clock(); SelectSort(a3, N); int end3 = clock(); int begin4 = clock(); HeapSort(a4, N); int end4 = clock(); int begin5 = clock(); QuickSort(a5, 0, N-1); int end5 = clock(); int begin6 = clock(); MergeSort(a6, N); int end6 = clock(); int begin7 = clock(); BubbleSort(a7, N); int end7 = clock();printf("InsertSort:%d\n", end1 - begin1); printf("ShellSort:%d\n", end2 - begin2); printf("SelectSort:%d\n", end3 - begin3); printf("HeapSort:%d\n", end4 - begin4); printf("QuickSort:%d\n", end5 - begin5); printf("MergeSort:%d\n", end6 - begin6); printf("BubbleSort:%d\n", end7 - begin7); free(a1); free(a2); free(a3); free(a4); free(a5); free(a6); free(a7);
}
2.6 非比较排序
2.6.1 计数排序
计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。
操作步骤:
-
统计相同元素出现次数
-
根据统计的结果将序列回收到原来的序列中
代码实现:
void CountSort(int* a, int n)
{int min = a[0], max = a[0];for (int i = 1; i < n; i++){if (a[i] > max)max = a[i];if (a[i] < min)min = a[i];}int range = max - min + 1;int* count = (int*)malloc(sizeof(int) * range);if (count == NULL){perror("malloc fail");return;}memset(count, 0, sizeof(int) * range);// 统计次数 for (int i = 0; i < n; i++){count[a[i] - min]++;`}// 排序 int j = 0;for (int i = 0; i < range; i++){while (count[i]--){a[j++] = i + min;}}
}
计数排序的特性:
计数排序在数据范围集中时,效率很高,但是适⽤范围及场景有限。
时间复杂度: O(N + range)
空间复杂度: O(range)
稳定性:稳定
3. 排序算法复杂度及稳定性分析
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,
即在原序列中, r[i] = r[j] ,且 r[i] 在 r[j] 之前,而在排序后的序列中, r[i] 仍在r[j]之前,则称这种排序算法是稳定的;
否则称为不稳定的。