十大排序算法Sorting algorithm(C++)
百度百科:
所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。排序算法,就是如何使得记录按照要求排列的方法。排序算法在很多领域得到相当地重视,尤其是在大量数据的处理方面。一个优秀的算法可以节省大量的资源。在各个领域中考虑到数据的各种限制和规范,要得到一个符合实际的优秀算法,得经过大量的推理和分析。
-
内部排序和外部排序
- 指在排序期间数据对象全部存放在内存的排序。
- 外部排序是因排序的数据很大,内存一次不能容纳全部的排序记录。在排序过程中需要借助外存,不断在内,外存间移动的排序。
-
稳定性
-
稳定性定义:排序前后两个相等的数相对位置不变,则算法稳定。
-
稳定性得好处:从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。(假设有此场景 —— 一个班的学生已经按照学号大小排好序了,我现在要求按照年龄从小到大再排个序,如果年龄相同的,必须按照学号从小到大的顺序排列。 那么问题来了,你选择的年龄排序方法如果是不稳定的,是不是排序完了后年龄相同的一组学生学号就乱了,你就得把这组年龄相同的学生再按照学号拍一遍。如果是稳定的排序算法,我就只需要按照年龄排一遍就好了。 这样看来稳定的排序算法是不是节省了时间。稳定性的优点就体会出来了。)
-
常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。用一张图概括:
注 : 本文除特别注明外,其它所有排序规则默认都为从小到大
冒泡排序
冒泡排序 (Bubble Sort) 也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小(大)的元素会经由交换慢慢"浮"到数列的顶端。
算法步骤:
-
起初所有数据都在无序区
-
比较相邻的元素。如果前一个元素大于后一个元素,就交换他们两个。对无序区每一对相邻元素作同样的工作。一轮做完后,最后的元素会是最大的数(无序区最后一个元素划归到有序区)。
-
针对无序区剩余的元素重复以上的步骤,直到无序区元素个数为1。
void bubble_sort(int *nums, int n) {
// nums为待排序数组 n为数组中数据的长度
for (int i = n; i > 1; --i) {
// 数组从下标1开始存数据
for (int j = 1; j < i; ++j) {
if (nums[j] > nums[j + 1]) {
swap(nums[j], nums[j + 1]);
}
}
}
return ;
}
优化:立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。此种方法优化程度有限,仅限于初始状态或排序过程中遇到了完全有序的极端情况(如初始状态为有序时可将时间复杂度降到 O ( n ) O(n) O(n))。正常排序很难触发此优化,并且还要增加flag变量状态的改变和判断的时间开销。
void bubble_sort(int *nums, int n) {
for (int i = n; i > 1; --i) {
bool flag = true;
for (int j = 1; j < i; ++j) {
if (nums[j] > nums[j + 1]) {
flag = false;
swap(nums[j], nums[j + 1]);
}
}
if (flag) return ;
}
return ;
}
选择排序
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O ( n 2 ) O(n^2) O(n2) 的时间复杂度。所以用到它的时候,数据规模越小越好。
算法步骤:
-
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
-
再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
-
重复第二步,直到所有元素均排序完毕。
数组版
稳定性:不稳定
比如A 80 B 80 C 70 这三个卷子从小到大排序
第一步会把C和A做交换 变成C B A
第二步和第三步不需要再做交换了。所以排序完是C B A
但是稳定的排序应该是C A B
void select_sort(int *nums, int n) {
for (int i = 1; i < n; ++i) {
int ind = i;
for (int j = i + 1; j <= n; ++j) {
if (nums[j] < nums[ind]) ind = j;
}
swap(nums[i], nums[ind]);
}
return ;
}
链表版
稳定性:稳定
不动值只动链表指针,那么是稳定的,而且链表指针的改动次数也是最少的
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {
}
ListNode(int x) : val(x), next(nullptr) {
}
ListNode(int x, ListNode *next) : val(x), next(next) {
}
};
class Solution {
public:
ListNode* sortList(ListNode* head) {
if (head == nullptr) return head;
ListNode *vir = new ListNode(); // 做一个虚拟头结点
vir->next = head;
head = vir;
while (head->next->next) {
ListNode *pre = head, *p = head->next; // 每轮保存一个极值以及其前驱位置
for (ListNode *pt = head->next, *t = pt->next; t;
pt = pt->next, t = t->next) {
if (t->val < p->val) {
pre = pt;
p = t;
}
}
pre->next = pre->next->next; // 从未排序区拿出,头插到已排序区末尾
p->next = head->next;
head->next = p;
head = head->next;
}
return vir->next;
}
};
插入排序
插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
算法步骤:
-
将第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
-
从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
性质:插入排序的元素移动次数为数组中逆序对个数
void insert_sort(int *nums, int n) {
for (int i = 2; i <= n; ++i) {
int t = nums[i], ind = i - 1;
while (ind > 0 && nums[ind] > t) {
nums[ind + 1] = nums[ind];
--ind;
}
nums[ind + 1] = t;
}
return ;
}
优化Ⅰ: 无监督优化法
分析上方无忧化版本第4行 ind > 0
的执行次数
判断成立次数:等价于数组中逆序对的个数,而逆序对个数的期望等价于插入排序的时间复杂度 O ( n 2 ) O(n^2) O(n2) ,极端样例如数组初始完全逆序
判断不成立次数:等价于数组元素个数-1次,即 O ( n ) O(n) O(n)量级
故 ind > 0
总的执行次数应趋近于 O ( n 2 ) O(n^2) O(n2)
那如何优化呢?
void unguarded_insert_sort(int *nums, int n) {
int min_ind = 1;
for (int i = 2; i <= n; ++i) {
if (nums[i] < nums[min_ind]) min_ind = i;
}
swap(nums[1], nums[min_ind]);
for (int i = 2; i <= n; ++i) {
int t = nums[i], ind = i - 1;
while (nums[ind] > t) {
nums[ind + 1] = nums[ind];
--ind;
}
nums[ind + 1] = t;
}
return ;
}
插入排序前通过 O ( n ) O(n) O(n)的耗损把数组首元素正确归位,从此首元素担任防越界标兵,这样就可以拿掉ind > 0
这个越界判断。
此优化虽然不会提升插入排序整体 O ( n 2 ) O(n^2) O(n2)的时间复杂度。但可以在细节上将一个 O ( n 2 ) O(n^2) O(n2)的判断语句优化到 O ( n ) O(n) O(n)。
优化Ⅱ: 折半插入排序
折半插入排序(binary insertion sort)是对插入排序算法的一种改进,由于排序算法过程中,就是不断的依次将元素插入前面已排好序的序列中。由于前半部分为已排好序的数列,这样我们不用按顺序依次寻找插入点,可以采用折半查找的方法来加快寻找插入点的速度。
折半插入排序算法是一种稳定的排序算法,比直接插入算法明显减少了关键字之间比较的次数,因此速度比直接插入排序算法快,但记录移动的次数没有变,所以折半插入排序算法的时间复杂度仍然为 O ( n 2 ) O(n^2) O(n2),与直接插入排序算法相同。
int binary_search(int *nums, int left, int right, int val) {
while (left < right) {
int mid = ((right - left + 1) >> 1) + left;
if (nums[mid] <= val) left = mid;
else right = mid - 1;
}
return left;
}
void binary_insert_sort(int *nums, int n) {
nums[0] = INT32_MIN;
for (int i = 2; i <= n; ++i) {
int t = nums[i];
int ind = binary_search(nums, 0, i - 1, t);
for (int j = i - 1; j > ind; --j) {
nums[j + 1] = nums[j];
}
nums[ind + 1] = t;
}
return ;
}
堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
- 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
- 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
堆排序的平均时间复杂度为 O ( n l o g n ) Ο(nlogn) O(nlogn)。
算法步骤:
-
创建一个堆;
-
把堆首(最大值)和堆尾互换;
-
把堆的尺寸缩小 1,并调用
shift_down(1)
,目的是把新的数组顶端数据调整到相应位置; -
重复步骤 2和3,直到堆的尺寸为 1。
void shift_down(int ind, int *nums, int n) {
while (ind << 1 <= n) {
int down = ind << 1;
if ((ind << 1 | 1) <= n && nums[ind << 1 | 1] > nums[down]) {
down = ind << 1 | 1;
}
if (nums[down] > nums[ind]) {
swap(nums[down], nums[ind]);
ind = down;
continue;
}
break;
}
return ;
}
void heap_sort(int *nums, int n) {
for (int i = n >> 1; i >= 1; --i) {
shift_down(i, nums, n);
}
for (int i = n; i > 1; --i) {
swap(nums[i], nums[1]);
shift_down(1, nums, i - 1);
}
return ;
}
关于堆的建立有两种方案 —— Ⅰ.自上向下建立( O ( n l o g n ) O(nlogn) O(nlogn)) Ⅱ.自下向上建立( O ( n ) O(n) O(n)) , 上述代码采用Ⅱ
归并排序
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
-
自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
-
自下而上的迭代;
算法步骤:
-
申请辅助空间,该空间用来存放合并后的序列;
-
设定两个指针,最初位置分别为两个已经排序序列的起始位置;
-
比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
-
重复步骤 3 直到某一指针达到序列尾;
-
将另一序列剩下的所有元素直接复制到合并序列尾。
自上而下的递归
void __merge_sort_UTD(int *nums, int left, int right) {
if (left >= right) return ;
// 分成两段 分别递归
int mid = ((right - left) >> 1) + left;
__merge_sort_UTD(nums, left, mid);
__merge_sort_UTD(nums, mid + 1, right);
int l = left, r = mid + 1, ind = 0;
// 申请辅助空间
int *temp = (int *)malloc(sizeof(int) * (right - left + 1));
// 两个有序序列合并,并存放到辅助空间中
while (l <= mid || r <= right) {
if (r > right || (l <= mid && nums[l] <= nums[r])) {
temp[ind++] = nums[l++];
} else {
temp[ind++] = nums[r++];
}
}
// 从辅助空间中取出数据,并释放辅助空间
memcpy(nums + left, temp, sizeof(int) * (right - left + 1));
free(temp);
return ;
}
void merge_sort_UTD(int *nums, int n) {
__merge_sort_UTD(nums, 1, n);
return ;
}
自下而上的迭代
void merge_sort_DTU(int *nums, int n) {
int *temp = (int *)malloc(sizeof(int) * (n + 5));
// 每轮将数据切割成长度为step的若干段,并两两进行合并(分为左右两段)
// step 取值为 2的x次方 [1,2,4,8,16,...]
// 切割后至少保证剩余两段, 故step < n
for (int step = 1; step < n; step <<= 1) {
// 两两合并故每轮i向后移动step的两倍
// 如 i 走到最后一个元素,则单个元素默认有序,故直接跳过
for (int i = 1; i < n; i += step << 1) {
int left = i, right = i + step;
// 当尾部待合并数据凑不满长度为step的两段时,注意设置边界(数据长度)
int mid = min(right, n + 1), tail = min(right + step, n + 1), ind = i;
// 左段和右段有一边不为空时,合并正常推进
while (left < mid || right < tail) {
// 只有右段为空,或者左段不为空且数据小于等于右段数据时,拿出左段数据
// 其它情况均拿出右段数据
// 注意 : nums[left] <= nums[right] 小于等于保证了归并排序的稳定性
if (right >= tail || (left < mid && nums[left] <= nums[right])) {
temp[ind++] = nums[left++];
} else {
temp[ind++] = nums[right++];
}
}
// 将辅助空间中存放有序数据的那段复制到原数组指定位置
memcpy(nums + i, temp + i, sizeof(int) * (ind - i));
}
}
// 释放辅助空间
free(temp);
return ;
}
优化: 去除辅助空间
核心在于如何用一个变量来表示两个值?
假设现在有变量a = 3, b = 2
, 如何用一个c
来表示 a 和 b
?
- 首先找到一个比
a
和b
都大的值max_val
,如max_val = 4
c = a + b * max_val = 3 + 2 * 4 = 11
- 可得:
a = c % max_val = 11 % 4 = 3
- 可得:
b = c / max_val = 11 / 4 = 2
因为a < max_val
所以相除之后不足1
,故商为b
那如何应用到归并排序中并替代辅助空间呢?
- 排序之前扫一遍数据,确定一个
max_val
的值 - 在原数组上操作的格式:
新nums[i] = 原nums[i] + 排序后该位置的值 * max_val;
- 对于某个位置
nums[i]
无论是新值还是原值,都可以通过% max_val
得到 - 两个有序列表合并之后,只需要每个元素都
/ max_val
即可得到排序后的答案
此优化的缺陷:如果值过大相乘操作就会发生数据溢出,并且取模操作时间开销也比较大。故实际开发中宁可开辟辅助空间也很少用此优化
void merge_sort_DTU_O1(int *nums, int n) {
int val_max = INT32_MIN;
for (int i = 1; i <= n; ++i) {
if (nums[i] > val_max) val_max = nums[i];
}
++val_max;
for (int step = 1; step < n; step <<= 1) {
for (int i = 1; i < n; i += step << 1) {
int left = i, right = i + step;
int mid = min(right, n + 1), tail = min(right + step, n + 1), ind = i;
while (left < mid || right < tail) {
if (right >= tail || (left < mid &&
(nums[left] % val_max) <= (nums[right] % val_max))) {
// 注意此处
nums[ind] += (nums[left++] % val_max) * val_max;
} else {
nums[ind] += (nums[right++] % val_max) * val_max;
}
++ind;
}
for (int j = i; j < tail; ++j) {
nums[j] /= val_max;
}
}
}
return ;
}
链表版
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* merge_sort(ListNode *head, int n) {
if (n <= 1) return head;
ListNode *rp = head;
for (int i = 1; i < n / 2; ++i) rp = rp->next;
ListNode *s = rp;
rp = rp->next;
s->next = NULL;
head = merge_sort(head, n / 2);
rp = merge_sort(rp, n - n / 2);
ListNode *vir = new ListNode();
s = vir;
while (head || rp) {
if (rp == NULL || (head && head->val < rp->val)) {
s->next = head;
head = head->next;
} else {
s->next = rp;
rp = rp->next;
}
s = s->next;
s->next = NULL;
}
return vir->next;
}
ListNode* sortList(ListNode* head) {
int len = 0;
ListNode *h = head;
while (h) ++len, h = h->next;
return merge_sort(head, len);
}
};
扩展:
辅助空间可以是外存。归并排序在大数据场景下是霸主地位。
问:电脑内存只有2G,如何对一个40G的文件进行排序
答:将40G的文件拆分成20个2G文件,分别进行排序。之后维护20个文件指针,对20个有序文件进行合并操作。无需将20路数据全读入到内存中(也读不下)。并且这种多路归并还可以用堆(优先队列)优化。
快速排序
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n n n 个项目要 O ( n l o g n ) Ο(nlogn) O(nlogn) 次比较。在最坏状况下则需要 O ( n 2 ) Ο(n^2) O(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 O ( n l o g n ) Ο(nlogn) O(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。虽然 Worst Case 的时间复杂度达到了 O ( n 2 ) Ο(n^2) O(n2),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn) 的排序算法表现要更好,可是这是为什么呢,我也不知道。好在我的强迫症又犯了,查了 N 多资料终于在《算法艺术与信息学竞赛》上找到了满意的答案:
快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
算法步骤
-
从数列中挑出一个元素,称为 “基准”(pivot);
-
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
-
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
void __quick_sort(int *nums, int left, int right) {
if (left >= right) return ;
int l = left, r = right, p = nums[left];
while (l < r) {
while (l < r && nums[r] >= p) --r;
if (l < r) nums[l++] = nums[r];
while (l < r && nums[l] <= p) ++l;
if (l < r) nums[r--] = nums[l];
}
nums[l] = p;
__quick_sort(nums, left, r - 1);
__quick_sort(nums, l + 1, right);
return ;
}
void quick_sort(int *nums, int n) {
__quick_sort(nums, 1, n);
return ;
}
总结:基础版的一次partition操作后,目标是让
l == r
,并且将pivot值放到 l ∣ r ( l = = r ) l | r(l==r) l∣r(l==r)这个下标处。此时该下标左侧元素都 < = p i v o t <=pivot <=pivot,右侧所有元素都 > = p i v o t >=pivot >=pivot
优化
-
无监督优化:模仿插入排序的无监督优化思想,想办法干掉诸如
l < r
这种花费开销的监督项,让程序运行的更加丝滑。 -
三点取中法:对于基础版取pivot的机制,很容易取到极值,此时就会进行一次无意义的partition。三点取中可以在
nums[left]、nums[right]、nums[mid]
中取一个中间值作为pivot 。从而尽可能保证每次partition都有价值。 -
单边递归法:对于基础版函数中两次递归调用,可以将其中一侧递归用本层循环处理。即一边递归,一边循环。这样可以减少一半的函数调用开销。
int Get_mid(int *nums, int left, int right) {
int a = nums[left];
int b = nums[right];
int c = nums[((right - left) >> 1) + left];
if (a > b) swap(a, b);
if (a > c) swap(a, c);
if (b > c) swap(b, c);
return b;
}
void __opt_quick_sort(int *nums, int left, int right) {
while (left < right) {
int l = left, r = right, p = Get_mid(nums, l, r);
// 根据外层循环的条件,保证内层循环第一次条件必成立,所以用do while可以比while少一次条件判断
do {
// 下两行的条件判断都不能有等于,不能是 <= 和 >=
// l 和 r 指针都需要一个卡点,p就是卡点
// {* 无监督边界之所以成立,是因为l走过的路,r到必卡住。反之r同理 *}
while (nums[l] < p) ++l;
while (nums[r] > p) --r;
if (l <= r) swap(nums[l++], nums[r--]);
} while (l <= r); // 目的是让l和r措开
__opt_quick_sort(nums, left, r);
left = l;
}
return ;
}
void opt_quick_sort(int *nums, int n) {
__opt_quick_sort(nums, 1, n);
return ;
}
注意:
基础版也叫填坑法一次partition后,l 和 r 相等。p 值落下的位置一定是排序后正确的位置,且 [ − , r ] < = p [-, r] <= p [−,r]<=p , [ l , + ] > = p [l, +] >= p [l,+]>=p, l = = r l==r l==r。
而优化版一次partition后,如果l 和 r 相等,不能保证 [ − , r ] < = p [-, r] <= p [−,r]<=p , [ l , + ] > = p [l, +] >= p [l,+]>=p。如:
因为无监督没有所谓的“坑”要填,是基于交换的规则实现的。所以目的单纯是要 [ − , r ] < = p [-, r] <= p [−,r]<=p , [ l , + ] > = p [l, +] >= p [l,+]>=p
希尔排序
希尔排序的一个基本思路是
- 利用了插入排序的简单
- 同时克服了插入排序每次只能交换相邻两个元素的缺点
希尔排序又叫递减增量排序算法,它是在直接插入排序算法的基础上进行改进而来的,直接插入排序在两种情况下它表现得很好,我们这里将这两种情况归纳为直接插入排序的两种性质:
- 当待排序的原序列中大多数元素都已有序的情况下,此时进行的元素比较和移动的次数较少;
- 当原序列的长度很小时,即便它的所有元素都是无序的,此时进行的元素比较和移动的次数还是很少。
希尔排序正是利用了直接插入排序算法的这两个性质。
算法步骤
- 它首先将待排序的原序列划分成很多小的序列,称为子序列。
- 然后对这些子序列进行直接插入排序,因为每个子序列中的元素较少,所以在它们上面应用直接插入排序效率较高(利用了上面的性质2)。
- 这样的过程可能会进行很多次,每一次都称为一趟,每一趟都将前一趟得到的整个序列划分为不同的子序列,并再次对这些子序列进行直接插入排序。
- 最后由这些子序列组成的整个序列中的所有元素就基本有序了,此时再在整个序列上进行最后一次的直接插入排序(利用了上面的性质1),此后整个序列的排序就完成了。
void shell_sort(int *nums, int n) {
// step 增量(步长)
for (int step = n >> 1; step > 0; step >>= 1) {
// s 起点, 超过当前增量则无意义
for (int s = 1; s <= step; ++s) {
// 根据增量进行直接插入排序
for (int i = s + step; i <= n; i += step) {
int temp = nums[i], ind = i - step;
while (ind > 0 && nums[ind] > temp) {
nums[ind + step] = nums[ind];
ind -= step;
}
nums[ind + step] = temp;
}
}
}
return ;
}
总结
希尔排序最关键的地方就是如何对整个序列进行划分,理解了这一过程就理解了整个希尔排序。子序列划分的方法必须保证对子序列进行排序后,每个元素在整个序列中的移动范围更大。这样跳跃式的位置移动,才可能让每个元素离它的最终位置较近,因而整个序列才是比较有序的。
计数排序
计数排序的核心在于将数据的值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求数据必须是有确定范围的整数。
如:在100万考生中确定某个考生的高考位次,那此组数据的值(键)的范围是[0,750],这个范围只需要开辟751个长度的计数数组即可( m a x _ v a l − m i n _ v a l + 1 max\_val - min\_val + 1 max_val−min_val+1),所以非常适合用计数排序。
注意的是,如果给定数据值的范围过大的话,那么计数排序就需要开辟大量的内存进行计数,则不适用。
算法步骤
- 找出待排序的数组中最大和最小的元素,开辟计数数组
C[max_val - min_val + 1]
- 统计数组中每个值为
i
的元素出现的次数,存入数组C
的第i - min_val
项。如果min_val
为零则无需考虑做差。 - 对所有的计数累加(从
C
中的第一个元素开始,每一项和前一项相加),为了计算出该键在排序后数组中的位置范围。 - 反向填充目标数组:将每个元素
i
放在新数组的第C(i)
项,每放一个元素就将C(i)
减去1
。反向填充是为了保证稳定性
void count_sort(int *nums, int n) {
int max_val = INT32_MIN, min_val = INT32_MAX;
for (int i = 1; i <= n; ++i) {
max_val = max(max_val, nums[i]);
min_val = min(min_val, nums[i]);
}
int m = max_val - min_val + 1;
int *C = (int *)calloc(sizeof(int), m);
for (int i = 1; i <= n; ++i) {
++C[nums[i] - min_val];
}
for (int i = 1; i < m; ++i) {
C[i] += C[i - 1];
}
int *sorted = (int *)malloc(sizeof(int) * (n + 1));
for (int i = n; i > 0; --i) {
sorted[C[nums[i] - min_val]--] = nums[i];
}
for (int i = 1; i <= n; ++i) {
nums[i] = sorted[i];
}
return ;
}
桶排序
桶排序(Bucket sort)或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序),最后依次把各个桶中的记录列出来记得到有序序列。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间( O ( n ) O(n) O(n))。并且桶排序不是比较排序,他不受到 O ( n l o g n ) O(nlogn) O(nlogn)下限的影响。
用通俗易懂的话来理解:
- 将待排序的序列分到若干个桶中,每个桶内的元素再进行个别排序。
- 时间复杂度最好可能是线性O(n),桶排序不是基于比较的排序
当然,桶排序是一种用空间换取时间的排序。
既然是排序,那么最终的结果肯定是从小到大的,桶排序借助桶的位置完成一次初步的排序(类似希尔排序那种向正确位置方向进行长距离跳跃式移动)——将待排序元素分别放至各个桶内。
桶的个数和桶内数据的范围,根据给定数据的范围进行确定,并非固定。
基数排序和计数排序都运用到了桶排序的思想
算法步骤
-
例如:数组:
1,45,32,23,22,31,47,24,4,15
-
观察知:数组的元素分布在(0-50)之间,我们可以将其分隔成五个区间分辨是[0-9],[10-19],[20-29],[30-39],[40-49];(桶的个数根据题意自定,只需确定好每个桶的存储范围就好;并非桶越多越好,也并非越少越好总之适当就好);
-
这五个区间看做五个桶;分别存放符合范围的数字;
-
将这五个区间分别排序,再输出;
基数排序
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位上的数分别比较。基数排序不仅能使用于整数,字符串和小数也同理。
核心思想:将所有待比较数值(自然数)统一为同样的数位长度,数位较短的数前面补零。或者不足按最高位计算,不足位数者之间按0计算。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
实现步骤
-
确定数组中的最大元素有几位(MAX)(确定执行的轮数)
-
创建
0-9
个桶(桶的底层是队列),因为所有的数字元素都是由0~9的十个数字组成。 -
依次判断每个元素的个位,十位至MAX位,存入对应的桶中,出队,存入原数组;直至MAX轮结束输出数组。
void radix_sort(int *nums, int n) {
queue<int> que[10];
// 初始化队列——当作个位轮排序
for (int i = 1; i <= n; ++i) {
que[nums[i] % 10].push(nums[i]);
}
// 除p模10代表当前处理的位 cnt代表上一轮每个队列中有多少元素
int p = 10, cnt[10] = {
0};
// 基数排序结束条件可以设置为当0号队列数据为n个时
while ((int)que[0].size() != n) {
// 每轮开始前都读一边上一轮各个队列中的元素个数
for (int i = 0; i < 10; ++i) cnt[i] = que[i].size();
// 依次处理每个队列,且只能处理cnt[i]个元素
for (int i = 0; i < 10; ++i) {
while (cnt[i]) {
--cnt[i];
int t = que[i].front();
que[i].pop();
que[t / p % 10].push(t);
}
}
// 处理当前位的前一位
p *= 10;
}
// 将0号队列中的元素依次放回原数组
while (!que[0].empty()) {
nums[n - que[0].size() + 1] = que[0].front();
que[0].pop();
}
return ;
}
觉得不错请点个👍,🤭
基数排序 vs 计数排序 vs 桶排序
基数排序有两种方法:
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
- 基数排序:根据键值的每位数字来分配桶;
- 计数排序:每个桶只存储单一键值;
- 桶排序:每个桶存储一定范围的数值;
全文测试程序
/*************************************************************************
> File Name: sort.cpp
> Author: Luzelin
> Mail:
> Created Time:
************************************************************************/
#include <iostream>
#include <cstdio>
#include <cinttypes>
#include <cstring>
#include <queue>
using namespace std;
void bubble_sort(int *nums, int n) {
for (int i = n; i > 1; --i) {
bool flag = true;
for (int j = 1; j < i; ++j) {
if (nums[j] > nums[j + 1]) {
flag = false;
swap(nums[j], nums[j + 1]);
}
}
if (flag) return ;
}
return ;
}
void select_sort(int *nums, int n) {
for (int i = 1; i < n; ++i) {
int ind = i;
for (int j = i + 1; j <= n; ++j) {
if (nums[j] < nums[ind]) ind = j;
}
swap(nums[i], nums[ind]);
}
return ;
}
void insert_sort(int *nums, int n) {
for (int i = 2; i <= n; ++i) {
int t = nums[i], ind = i - 1;
while (ind > 0 && nums[ind] > t) {
nums[ind + 1] = nums[ind];
--ind;
}
nums[ind + 1] = t;
}
return ;
}
void unguarded_insert_sort(int *nums, int n) {
int min_ind = 1;
for (int i = 2; i <= n; ++i) {
if (nums[i] < nums[min_ind]) min_ind = i;
}
swap(nums[1], nums[min_ind]);
for (int i = 2; i <= n; ++i) {
int t = nums[i], ind = i - 1;
while (nums[ind] > t) {
nums[ind + 1] = nums[ind];
--ind;
}
nums[ind + 1] = t;
}
return ;
}
int binary_search(int *nums, int left, int right, int val) {
while (left < right) {
int mid = ((right - left + 1) >> 1) + left;
if (nums[mid] <= val) left = mid;
else right = mid - 1;
}
return left;
}
void binary_insert_sort(int *nums, int n) {
nums[0] = INT32_MIN;
for (int i = 2; i <= n; ++i) {
int t = nums[i];
int ind = binary_search(nums, 0, i - 1, t);
for (int j = i - 1; j > ind; --j) {
nums[j + 1] = nums[j];
}
nums[ind + 1] = t;
}
return ;
}
void shift_down(int ind, int *nums, int n) {
while (ind << 1 <= n) {
int down = ind << 1;
if ((ind << 1 | 1) <= n && nums[ind << 1 | 1] > nums[down]) {
down = ind << 1 | 1;
}
if (nums[down] > nums[ind]) {
swap(nums[down], nums[ind]);
ind = down;
continue;
}
break;
}
return ;
}
void heap_sort(int *nums, int n) {
for (int i = n >> 1; i >= 1; --i) {
shift_down(i, nums, n);
}
for (int i = n; i > 1; --i) {
swap(nums[i], nums[1]);
shift_down(1, nums, i - 1);
}
return ;
}
void __merge_sort_DTU(int *nums, int n) {
int *temp = (int *)malloc(sizeof(int) * (n + 5));
for (int step = 2; step < n << 1; step <<= 1) {
for (int i = 1; i < n; i += step) {
int left = i, right = i + (step >> 1);
int mid = min(right, n + 1), tail = min(i + step, n + 1), ind = i;
while (left < mid || right < tail) {
if (right >= tail || (left < mid && nums[left] <= nums[right])) {
temp[ind++] = nums[left++];
} else {
temp[ind++] = nums[right++];
}
}
memcpy(nums + i, temp + i, sizeof(int) * (ind - i));
}
}
free(temp);
return ;
}
void merge_sort_DTU(int *nums, int n) {
int *temp = (int *)malloc(sizeof(int) * (n + 5));
for (int step = 1; step < n; step <<= 1) {
for (int i = 1; i < n; i += step << 1) {
int left = i, right = i + step;
int mid = min(right, n + 1), tail = min(right + step, n + 1), ind = i;
while (left < mid || right < tail) {
if (right >= tail || (left < mid && nums[left] <= nums[right])) {
temp[ind++] = nums[left++];
} else {
temp[ind++] = nums[right++];
}
}
memcpy(nums + i, temp + i, sizeof(int) * (ind - i));
}
}
free(temp);
return ;
}
void __merge_sort_UTD(int *nums, int left, int right) {
if (left >= right) return ;
int mid = ((right - left) >> 1) + left;
__merge_sort_UTD(nums, left, mid);
__merge_sort_UTD(nums, mid + 1, right);
int l = left, r = mid + 1, ind = 0;
int *temp = (int *)malloc(sizeof(int) * (right - left + 1));
while (l <= mid || r <= right) {
if (r > right || (l <= mid && nums[l] <= nums[r])) {
temp[ind++] = nums[l++];
} else {
temp[ind++] = nums[r++];
}
}
memcpy(nums + left, temp, sizeof(int) * (right - left + 1));
free(temp);
return ;
}
void merge_sort_UTD(int *nums, int n) {
__merge_sort_UTD(nums, 1, n);
return ;
}
void merge_sort_DTU_O1(int *nums, int n) {
int val_max = INT32_MIN;
for (int i = 1; i <= n; ++i) {
if (nums[i] > val_max) val_max = nums[i];
}
++val_max;
for (int step = 1; step < n; step <<= 1) {
for (int i = 1; i < n; i += step << 1) {
int left = i, right = i + step;
int mid = min(right, n + 1), tail = min(right + step, n + 1), ind = i;
while (left < mid || right < tail) {
if (right >= tail || (left < mid && (nums[left] % val_max) <= (nums[right] % val_max))) {
nums[ind] += (nums[left++] % val_max) * val_max;
} else {
nums[ind] += (nums[right++] % val_max) * val_max;
}
++ind;
}
for (int j = i; j < tail; ++j) {
nums[j] /= val_max;
}
}
}
return ;
}
void __quick_sort(int *nums, int left, int right) {
if (left >= right) return ;
int l = left, r = right, p = nums[left];
while (l < r) {
while (l < r && nums[r] >= p) --r;
if (l < r) nums[l++] = nums[r];
while (l < r && nums[l] <= p) ++l;
if (l < r) nums[r--] = nums[l];
}
nums[l] = p;
__quick_sort(nums, left, r - 1);
__quick_sort(nums, l + 1, right);
return ;
}
void quick_sort(int *nums, int n) {
__quick_sort(nums, 1, n);
return ;
}
int Get_mid(int *nums, int left, int right) {
int a = nums[left];
int b = nums[right];
int c = nums[((right - left) >> 1) + left];
if (a > b) swap(a, b);
if (a > c) swap(a, c);
if (b > c) swap(b, c);
return b;
}
void __opt_quick_sort(int *nums, int left, int right) {
while (left < right) {
int l = left, r = right, p = Get_mid(nums, l, r);
do {
while (nums[l] < p) ++l;
while (nums[r] > p) --r;
if (l <= r) swap(nums[l++], nums[r--]);
} while (l <= r);
__opt_quick_sort(nums, left, r);
left = l;
}
return ;
}
void opt_quick_sort(int *nums, int n) {
__opt_quick_sort(nums, 1, n);
return ;
}
void shell_sort(int *nums, int n) {
for (int step = n >> 1; step > 0; step >>= 1) {
for (int s = 1; s <= step; ++s) {
for (int i = s + step; i <= n; i += step) {
int temp = nums[i], ind = i - step;
while (ind > 0 && nums[ind] > temp) {
nums[ind + step] = nums[ind];
ind -= step;
}
nums[ind + step] = temp;
}
}
}
return ;
}
void count_sort(int *nums, int n) {
int max_val = INT32_MIN, min_val = INT32_MAX;
for (int i = 1; i <= n; ++i) {
max_val = max(max_val, nums[i]);
min_val = min(min_val, nums[i]);
}
int m = max_val - min_val + 1;
int *C = (int *)calloc(sizeof(int), m);
for (int i = 1; i <= n; ++i) {
++C[nums[i] - min_val];
}
for (int i = 1; i < m; ++i) {
C[i] += C[i - 1];
}
int *sorted = (int *)malloc(sizeof(int) * (n + 1));
for (int i = n; i > 0; --i) {
sorted[C[nums[i] - min_val]--] = nums[i];
}
for (int i = 1; i <= n; ++i) {
nums[i] = sorted[i];
}
return ;
}
void radix_sort(int *nums, int n) {
queue<int> que[10];
for (int i = 1; i <= n; ++i) {
que[nums[i] % 10].push(nums[i]);
}
int p = 10, cnt[10] = {
0};
while ((int)que[0].size() != n) {
for (int i = 0; i < 10; ++i) cnt[i] = que[i].size();
for (int i = 0; i < 10; ++i) {
while (cnt[i]) {
--cnt[i];
int t = que[i].front();
que[i].pop();
que[t / p % 10].push(t);
}
}
p *= 10;
}
while (!que[0].empty()) {
nums[n - que[0].size() + 1] = que[0].front();
que[0].pop();
}
return ;
}
int main() {
// modify array length
const int N = 10;
int nums[N + 5] = {
0};
srand(time(0));
for (int i = 1; i <= N; ++i) nums[i] = rand() % 100;
printf("原数组 : ");
for (int i = 1; i <= N; ++i) {
printf("%d ", nums[i]);
}
puts("");
// TODO e.g : bubble_sort(nums, N);
radix_sort(nums, N);
printf("排序后 : ");
for (int i = 1; i <= N; ++i) {
printf("%d ", nums[i]);
}
puts("");
return 0;
}
转载:https://blog.csdn.net/qq_40342400/article/details/128148049