赞
踩
上一期内容中我们了解了基本排序中的插入与选择排序,今天我将为大家带来剩下的几种排序算法
快速排序是Hoare于1962年提出的⼀种⼆叉树结构的交换排序⽅法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两⼦序列,左⼦序列中所有元素均⼩于基准值,右⼦序列中所有元素均⼤于基准值,然后最左右⼦序列重复该过程,直到所有元素都排列在相应位置上为⽌。
快速排序有着许多种实现方式,今天我们就hoare、挖坑法以及lomuto前后指针法来对其进行递归实现,同时还将运用数据结构中的栈来实现快速排序的非递归实现方法。
- //快速排序
- 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 ⽅法主要有以下⼏种实现⽅式:
我们可以将这些方法看做一个找基准值的过程:
思路:
1)创建左右指针,确定基准值
2)从右向左找出⽐基准值⼩的数据,从左向右找出⽐基准值⼤的数据,左右指针数据交换,进⼊下次循环
问题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;
- }

思路:
创建左右指针。⾸先从右向左找出⽐基准⼩的数据,找到后⽴即放⼊左边坑中,当前位置变为新的"坑",然后从左向右找出⽐基准⼤的数据,找到后⽴即放⼊右边坑中,当前位置变为新的"坑",结束循环后将最开始存储的分界值放⼊当前的"坑"中,返回当前"坑"下标(即分界值下标)
我们通过一张图来了解:
- 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;
- }

思路:
创建前后指针,从左往右找⽐基准值⼩的进⾏交换,使得⼩的都排在基准值的左边。
如图:
- 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;
- }

1. 时间复杂度:O(nlogn)
2. 空间复杂度:O(logn)
⾮递归版本的快速排序需要借助数据结构:栈
根据栈结构先进后出的原则,我们先将待排序数组的末尾元素先入栈,头元素后入栈。
随后分别取栈顶与栈底元素,利用循环来实现递归操作。
- 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);
- }

归并排序的思想:
归并排序(MERGE-SORT)是建⽴在归并操作上的⼀种有效的排序算法,该算法是采⽤分治法(Divide and Conquer)的⼀个⾮常典型的应⽤。将已有序的⼦序列合并,得到完全有序的序列;即先使每个⼦序列有序,再使⼦序列段间有序。若将两个有序表合并成⼀个有序表,称为⼆路归并。归并排序核⼼步骤(如下图):
利用递归分别将待排序的数组二等分割成n份,每次取一半,随后就分割后的两两数组进行合并操作(先比大小后放置)
- 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;
- }

1. 时间复杂度:O(nlogn)
2. 空间复杂度:O(n)
通过以下代码,我们可以得出各种排序算法的时间效率。
- // 测试排序的性能对⽐
- 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);
- }

学习了以上的代码,那么有没有不通过比较大小就能实现的排序算法呢?有!
计数排序⼜称为鸽巢原理,是对哈希直接定址法的变形应⽤。
操作步骤:
1)统计相同元素出现次数
2)根据统计的结果将序列回收到原来的序列中
通过两张图片来了解一下:
为了避免空间的无端浪费,我们将遍历待排数组中的最大最小值,后得出其间的差值,用其差值来申请空间大小以达到排序的效果。
- 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)
稳定性:稳定
*计数排序仅适用于整数的排序
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的 相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,⽽在排序后的序列中,r[i]仍在r[j]之 前,则称这种排序算法是稳定的;否则称为不稳定的。
以上便是数据结构初阶的全部内容,感谢各位的支持!后面我将为大家带来C++有关的知识分享,敬请期待!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。