一、算法概述
1.算法分类
常见的十大经典排序算法可以分为两大类:
- 非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序。
- 线性时间非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。
2.算法复杂度
3.相关概念
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
二、参考博客
- 算法——堆和堆排序介绍 https://www.cnblogs.com/xiugeng/p/9645972.html
- 堆排序 https://www.cnblogs.com/fortunely/p/10254161.html
- 【排序】堆排序,C++实现 https://www.cnblogs.com/wanglei5205/p/8733524.html
- 归并排序详解 https://www.cnblogs.com/agui521/p/6918229.html
- 排序(7):归并排序 https://cuijiahua.com/blog/2018/01/algorithm_7.html
- 排序(8):基数排序 https://cuijiahua.com/blog/2018/01/algorithm_8.html
- 【排序(C++实现)】:桶排序 https://blog.csdn.net/bqw18744018044/article/details/81738883
- 算法:排序算法之桶排序 https://blog.csdn.net/developer1024/article/details/79770240
- 排序算法——桶排序 https://blog.csdn.net/houapple/article/details/6480100
三、算法详解
6.选择排序之堆排序
堆是一种特殊的完全二叉树。完全二叉树是满二叉树最后一层右边部分连续缺失形成的二叉树。如下图所示:
堆又分为大根堆和小根堆。如下图所示:
大根堆:一棵完全二叉树,满足任一节点都比其孩子节点大;
小根堆:一棵完全二叉树,满足任一节点都比其他孩子节点小。
大根堆通常对应升序序列,小根堆通常对应降序排列
核心步骤:
1) 构建堆(大堆/小堆)
从最后一个非终端结点开始,向前进行调整,保证当前结点及其子树符合堆特性;
2) 输出有序序列
交换堆顶与末尾叶子结点,堆顶输出到数组的有序序列末尾,而不参与堆的调整。从交换后的堆顶开始调整,以确保当前结点及其子树符合堆特性。
举例:
1) 初始序列对应初始堆
下面标号为序列的index, “‘”表示第二个相同的元素值
从最后一个非叶子结点开始,向前进行调整,确保符合特性
最后一个非叶子结点位置:(n-1) / 2 = 3, n=8
总共调整次数:(n-1)/2 +1 = 4
第1次调整:选择最后一个非叶子结点元素为97(位置3)为当前父结点,与其子结点进行比较,选择最小的结点作为当前父结点。
第2次调整:选择上一次结点的前一个结点65(位置2)为当前结点进行调整。
第3次调整:选择上一次结点的前一个结点38(位置1)为当前结点进行调整。
第4次调整:选择上一次结点的前一个结点49(位置0)为当前结点进行调整。(这里需要迭代进行调整)
2) 输出堆顶元素
交换堆顶和末尾(堆的末尾,不包括已经排好序的部分),并将交换后的堆末尾作为有序序列的一部分,而不再属于堆。
交换后不满足堆的特性。故回到第一步再次调整次序,直至满足堆的特性。
第一二步循环操作最终输出有序序列。97 76 65 49 49’ 38 27 13
实现代码:
// 递归方式构建小根堆(len是arr的长度,index是第一个非叶子节点的下标) void adjust(vector<int> &arr, int len, int index) { int left = 2*index + 1; // index的左子节点 int right = 2*index + 2;// index的右子节点 int minIdx = index; if(left<len && arr[left] < arr[minIdx]) minIdx = left; if(right<len && arr[right] < arr[minIdx]) minIdx = right; if(minIdx != index) { swap(arr[minIdx], arr[index]); adjust(arr, len, minIdx); } } // 堆排序 void heapSort(vector<int> &arr, int size) { // 构建小根堆(从最后一个非叶子节点向上) for(int i=size/2 - 1; i >= 0; i--) { adjust(arr, size, i); } // 调整大根堆 for(int len = size - 1; len >= 1; len--) { swap(arr[0], arr[len]); // 将当前最大的放置到数组末尾 adjust(arr, len, 0); // 将未完成排序的部分继续进行堆排序 } }
7.归并排序
和快速排序一样,都是采用“分治法”的思想
归并排序的核心思想是将两个有序的数列合并成一个大的有序的序列。通过递归,层层合并,即为归并。具体如下图所示(以二路归并为例):
归并操作的工作原理如下:
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 重复步骤3直到某一指针超出序列尾将另一序列剩下的所有元素直接复制到合并序列尾
实现代码:
#include <iostream> void merge(int a[], int b[], int l, int m, int r) { int i = l; //起始位置 int j = m + 1; //中间位置 int k = l; while (k <= r) { //遍历新建的数组,根据排序后的结果赋值 if (i > m) { //左边子序列已经被遍历完毕,将剩下的右子序列直接加到新建数组后面 b[k++] = a[j++]; } else if (j > r) { ////右边子序列已经被遍历完毕,将剩下的左子序列直接加到新建数组后面 b[k++] = a[i++]; } else { if (a[i] > a[j]) { //取左右子序列中较小的一个添加到新建数组中 b[k++] = a[j++]; } else { b[k++] = a[i++]; } } } for (int k = l; k <= r; k++) { a[k] = b[k]; //更新原数组 } } void merge_sort_helper(int a[], int b[], int l, int r) { if (l >= r) { return; } int m = (l + r) / 2; merge_sort_helper(a, b, l, m); //递归操作,分左子序列 merge_sort_helper(a, b, m + 1, r);//递归操作,分右子序列 merge(a, b, l, m, r); //合并序列 } void merge_sort(int a[], int len) { int *b = new int[len]; merge_sort_helper(a, b, 0, len - 1); //注意最后一个index是len-1 delete[] b; } int main(int argc, const char * argv[]) { int a[] = { 1, 1, 6, 6, 8, 3, 5, 100, 300, 200, 99, 99 }; merge_sort(a, sizeof(a) / sizeof(a[0])); for (int i : a) { std::cout << i << ' '; } std::cout << std::endl; return 0; }
8.线性时间非比较类之基数排序
算法思想:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。这样从最低位开始的方法称为最低位优先
算法步骤:
- 将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。
- 从最低位开始,依次进行一次排序。
- 这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
设有一个初始序列为: R {50, 123, 543, 187, 49, 30, 0, 2, 11, 100}。我们知道,任何一个阿拉伯数,它的各个位数上的基数都是以 0~9 来表示的,所以我们不妨把 0~9 视为 10 个桶。示例图如下:
实现代码:
#include <iostream> #include <vector> using namespace std; // 求出数组中最大数的位数的函数 int MaxBit(vector<int> input) { // 数组最大值 int max_data = input[0]; for (int i = 1; i < input.size(); i++) { if (input[i] > max_data) { max_data = input[i]; } } // 数组最大值的位数 int bits_num = 0; while (max_data) { bits_num++; max_data /= 10; } return bits_num; } // 取数xxx上的第d位数字 int digit(int num, int d) { int pow = 1; while (--d > 0) { pow *= 10; } return num / pow % 10; } // 基数排序 vector<int> RadixSort(vector<int> input, int n) { // 临时数组,用来存放排序过程中的数据 vector<int> bucket(n); // 位记数器,从第0个元素到第9个元素依次用来记录当前比较位是0的有多少个...是9的有多少个数 vector<int> count(10); // 从低位往高位循环 for (int d = 1; d <= MaxBit(input); d++) { // 计数器清0 for (int i = 0; i < 10; i++) { count[i] = 0; } // 统计各个桶中的个数 for (int i = 0; i < n; i++) { count[digit(input[i], d)]++; } /* * 比如序列:23 22 11 32 13 42 21 33 * 经过上面统计后结果为:[0, 2, 3, 3, 0, 0, 0, 0, 0, 0]则经过下面计算后 结果为: [0, 2, * 5, 8, 8, 8, 8, 8, 8, 8]但实质上只有如下[0, 2, 5, 8, 0, 0, 0, 0, 0, 0]中 * 非零数才用到,因为其他位不存在,它们分别表示如下:2表示比较位为1的元素可以存放在索引为1、0的 * 位置,5表示比较位为2的元素可以存放在4、3、2三个(5-2=3)位置,8表示比较位为3的元素可以存放在 * 7、6、5三个(8-5=3)位置 */ for (int i = 1; i < 10; i++) { count[i] += count[i - 1]; } /* * 注,这里只能从数组后往前循环,因为排序时还需保持以前的已排序好的顺序,不应该打 * 乱原来已排好的序,如果从前往后处理,则会把原来在前面会摆到后面去,因为在处理某个 * 元素的位置时,位记数器是从大到到小(count[digit(arr[i], d)]--)的方式来处 * 理的,即先存放索引大的元素,再存放索引小的元素,所以需从最后一个元素开始处理。 * 还是上面的例子 一次排序后不改变位数相同数字的相对位置 即 23仍在13前面 * 一次排序后 11 21 22 32 42 23 13 33 */ for (int i = n - 1; i >= 0; i--) { int k = digit(input[i], d); bucket[count[k] - 1] = input[i]; count[k]--; } // 临时数组复制到 input 中 for (int i = 0; i < n; i++) { input[i] = bucket[i]; } } return input; } void main() { int arr[] = { 50, 123, 543, 187, 49, 30, 0, 2, 11, 100 }; vector<int> test(arr, arr + sizeof(arr) / sizeof(arr[0])); cout << "排序前:"; for (int i = 0; i < test.size(); i++) { cout << test[i] << " "; } cout << endl; vector<int> result = test; result = RadixSort(result, result.size()); cout << "排序后:"; for (int i = 0; i < result.size(); i++) { cout << result[i] << " "; } cout << endl; system("pause"); }
ps:我认为基数排序的时间复杂度为O(d(n+k)),空间复杂度为O(n+k) 。其中d为最大位的位数,n为数据规模,k为每一位的取值范围。
9.桶排序
算法思想:
桶排序的基本思想是假设数据在[min,max]之间均匀分布,其中min、max分别指数据中的最小值和最大值。那么将区间[min,max]等分成n份,这n个区间便称为n个桶。将数据加入对应的桶中,然后每个桶内单独排序。由于桶之间有大小关系,因此可以从大到小(或从小到大)将桶中元素放入到数组中。
例如要对大小为[1..1000]范围内的n个整数A[1..n]排序,可以把桶设为大小为10的范围,具体而言,设集合B[1]存储[1..10]的整数,集合B[2]存储(10..20]的整数,……集合B[i]存储((i-1)*10, i*10]的整数,i = 1,2,..100。总共有100个桶。然后对A[1..n]从头到尾扫描一遍,把每个A[i]放入对应的桶B[j]中。 然后再对这100个桶中每个桶里的数字排序,这时可用冒泡,选择,乃至快排,一般来说任何排序法都可以。最后依次输出每个桶里面的数字,且每个桶中的数字从小到大输出,这样就得到所有数字排好序的一个序列了。
特例[0,1]之间,见桶排序。
假设有n个数字,有m个桶,如果数字是平均分布的,则每个桶里面平均有n/m个数字。如果对每个桶中的数字采用快速排序,那么整个算法的复杂度是O(n+m*n/m*log(n/m))=O(n+nlogn-nlogm)
从上式看出,当m接近n的时候,桶排序复杂度接近O(n)
当然,以上复杂度的计算是基于输入的n个数字是平均分布这个假设的。这个假设是很强的,实际应用中效果并没有这么好。如果所有的数字都落在同一个桶中,那就退化成一般的排序了。
- 平均时间复杂度:O(n + k)
- 最佳时间复杂度:O(n + k)
- 最差时间复杂度:O(n ^ 2)
- 空间复杂度:O(n * k) ps:要按最差的情况来算
- 稳定性:稳定
代码实现:
/*算法:桶排序*/ #include <iostream> #include <vector> #include <algorithm> using namespace std; void bksort(float A[], int l, int h, vector<float> b[]) { int size = h - l + 1; for (int i = l; i <= h; i++) { int bi = size*A[i];//元素A[i]的桶编号 b[bi].push_back(A[i]);//将元素A[i]压入桶中 } for (int i = 0; i<size; i++) sort(b[i].begin(), b[i].end());//桶内排序 int idx = l;//指向数组A的下标 for (int i = 0; i<size; i++) {//遍历桶 for (int j = 0; j<b[i].size(); j++) {//遍历桶内元素 A[idx++] = b[i][j]; } } } int main() { float A[] = { 0.78,0.17,0.39,0.26,0.72,0.94,0.21,0.12,0.23,0.68 }; //已知在[0,1]之间均匀分布 vector<float> b[sizeof(A)/sizeof(A[0])];//有size个数据,就分配size个桶 bksort(A, 0, 9, b); for (int i = 0; i<10; i++) cout << A[i] << " "; }