- 排序算法是基础算法之一,属于常见题型。虽然,我们的STL提供了
sort()
函数,但是这依然不妨碍在机试的考察裸的排序算法,比如输出排序过程中的序列,所以必须拿下! - 下面讨论的都是形成非递减序列的排序算法,即
A[i-1]
≤A[i]
; - 学习排序算法的最好方式就是手动模拟一下排序算法的实现~
0. 引言
排序算法可以分为内部排序和外部排序:
- 内部排序是数据记录在内存中进行排序;
- 外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。
常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。
注意:最好,最坏和平均时间复杂度主要是看该算法 是否受初始序列的影响,如果不受影响,那么它们之间就是没有区别的。
排序算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度(辅助存储) | 排序方式 | 稳定性 | 类别 | 存储方式 |
---|---|---|---|---|---|---|---|---|
直接插入排序 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | in-place | 稳定 | 插入排序 | 顺序存储和链式存储 |
折半插入排序 | O ( n 2 ) ? O(n^2)? O(n2)? | O ( n ) ? O(n)? O(n)? | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | in-place | 稳定 | 插入排序 | 顺序存储 |
希尔排序 | O ( n 1.3 ) ? ( 难 以 证 明 ) O(n^{1.3})?(难以证明) O(n1.3)?(难以证明) | O ( n ) ? O(n)? O(n)? | O ( n 2 ) O(n^2) O(n2)? | O ( 1 ) O(1) O(1) | in-place | 不稳定 | 插入排序 | 顺序存储 |
冒泡排序 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n)(设置flag变量,提前返回的情况下) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | In-place | 稳定 | 交换排序 | 顺序存储和链式存储 |
快速排序 | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n 2 ) O(n^2) O(n2) | O ( l o g n ) O(logn) O(logn) | In-place | 不稳定 | 交换排序 | 顺序存储和链式存储 |
直接选择排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | In-place | 不稳定 | 选择排序 | 顺序存储和链式存储 |
堆排序 | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( 1 ) O(1) O(1) | In-place | 不稳定 | 选择排序 | 顺序存储和链式存储 |
归并排序 | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n ) O(n) O(n) | Out-place | 稳定 | 归并排序 | 顺序存储和链式存储 |
基数排序 | O ( d ∗ n ) O(d*n) O(d∗n) | O ( d ∗ n ) O(d*n) O(d∗n) | O ( d ∗ n ) O(d*n) O(d∗n) | O ( r ) O(r) O(r) | Out-place | 稳定 | 基数排序 | 不基于比较 |
所谓的 In-place 是指的 原地排序,而不是指内部排序 or2。
快速记忆的方法:
选泡插,
快归堆希统计基,
恩方恩老恩一三,
对恩加K恩乘K,
不稳稳稳不稳稳,
不稳不稳稳稳稳!
1. 直接插入排序
直接插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。直接插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中 从后向前扫描 ,找到相应位置并插入。
- 时间复杂度:逆序时,需要 O ( n ² ) O(n²) O(n²);当正序时,每次都是直接插入到序列的最后面,因此是 O ( n ) O(n) O(n);
- 空间复杂度:对于任何规模的 n n n,都只要那几个临时变量,不随规模变化,因此为 O ( 1 ) O(1) O(1);
- 稳定性:对于原始序列 [2,2,3,1],它是稳定的。
(1) 算法步骤
- 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
- 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)。
(2) 算法演示
(3) 代码实现
int A[maxn],n;
void insertSort(){
for(int i=2;i<=n;i++){ //进行n-1趟排序
int temp = A[i],j=i;
while(j > 1 && temp < A[j-1]){ //只要temp小于前一个元素A[j-1]
A[j] = A[j-1]; //把A[j-1]后移一位至A[j]
j--;
}
A[j] = temp; //插入位置为j
}
}
2. 冒泡排序
冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
- 时间复杂度:逆序时,需要 O ( n ² ) O(n²) O(n²);当正序时,在第一次冒泡结束后,发现数组中没有元素位置发生交换,立刻停止冒泡排序,因此是 O ( n ) O(n) O(n);
- 空间复杂度:对于任何规模的 n n n,都只要那几个临时变量,不随规模变化,因此为 O ( 1 ) O(1) O(1);
- 稳定性:对于原始序列 [2,2,3,1],它是稳定的。
(1) 算法步骤
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
(2) 算法演示
(3) 代码实现
//交换数组元素
void swap(int *a,int i,int j){
int t = a[i];
a[i] = a[j];
a[j] = t;
}
void bubbleSort(int *a,int len){
int max = len-1;
int i,j;
for(i=0;i<max;++i){
for(j=0;j<max-i;++j){
if(a[j+1] < a[j]){
swap(a,j,j+1);
}
}
}
}
可以设置一个bool
类型的flag
变量用于提前返回。
3. 快速排序
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 O ( n l o g n ) Ο(nlogn) O(nlogn) 次比较 。在最坏状况下则需要 O ( n 2 ) Ο(n^2) O(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
- 时间复杂度:一般情况下为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn),但是最坏情况下,假设数组的顺序是正序或者逆序时,会退化成
O
(
n
2
)
O(n^2)
O(n2)。
- 最好、平均的时间复杂度:是因为每一层递归调用栈,都会涉及到n个元素的比较,所以为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn) 。如下图的递归栈:
- 最坏时间复杂度:为 O ( n 2 ) O(n^2) O(n2)
- 最好、平均的时间复杂度:是因为每一层递归调用栈,都会涉及到n个元素的比较,所以为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn) 。如下图的递归栈:
- 空间复杂度:
- 最好、平均空间复杂度:首先就地快速排序使用的空间是O(1)的,也就是个常数级;而真正消耗空间的就是 递归调用 了,因为每次递归就要保持一些数据,因此时间复杂度为 O ( l o g n ) O(logn) O(logn);
- 最坏空间复杂度:为 O ( n ) O(n) O(n)
- 稳定性:对于原始序列 [2,2,3,1],它是不稳定的。
(1) 算法步骤
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive) 把小于基准值元素的子数列和大于基准值元素的子数列排序;
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
(2) 算法演示
(3) 代码实现
//对区间[left,right]进行划分
//这里总是以left为主元,可能会导致出现n2的情况
//优化的方法是随机快速排序
int Partition(int A[],int left,int right){
int temp = A[left]; //将A[left]存放至临时变量temp
while(left < right){ //只要left与right不相遇
while(left < right && A[right] > temp){
right--; //反复左移right
}
A[left] = A[right]; //将A[right]挪到A{left]
while(left < right && A[left] <= temp){ //反复右移left
left++; //将A[left]挪到A[right]
}
A[right] = A[left];
}
A[left] = temp;
return left; //返回相遇的下标
}
//快速排序,left与right初始值为序列的首位下标
void quickSort(int A[],int left,int right){
if(left < right){ //当前区间长度大于1
//将[left,right]按A[left]一分为二
int pos = Partition(A,left,right);
quickSort(A,left,pos-1); //对左子区间递归快速排序
quickSort(A,pos+1,right); //对右子区间递归快速排序
}
}
代码注意点:
- 大部分的快排写法中都有一个
while(left < right && A[left] <= temp) left++;
类似这样的过程,请注意这个while循环中一定要使用>=或<=,如果你去掉了=号,你可以试试看是什么结果。。。 超时! 这个=号在细节上是非常重要的。比如对于序列[1,1]
,他就会一直交换,从而超时。- 一定要判断三次
while(left < right)
,目的是为了避免对于[1,1]
这种序列出现数组下标越界。- 注意
quickSort()
函数的递归边界是left < right
。这里的递归边界不太明显。
(4) 优化
快速排序算法当序列中元素的排列比较随机时效率最高,但是当序列中元素接近于有序时,会达到最坏时间复杂度
O
(
n
2
)
O(n^2)
O(n2),产生这种情况的主要原因在于主元pivot
没有把当前区间划分为两个长度接近的子区间。有什么办法可以解决这个问题呢?其中一个办法就是 随机选择主元 ,也就是对A[left,right]
来说,不总是用A[left]
作为主元,而是从A[left]、A[left+1]、...、A[right]
中随机选择一个作为主元,这样虽然算法的最坏时间复杂度仍然是
O
(
n
2
)
O(n^2)
O(n2)(例如,总是选择了A[left]
作为主元),但是对于任意输入数据的期望时间复杂度都能达到
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)。
下面看看怎么生成随机数。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(){
srand((unsigned)time(NULL));
for(int i=0;i<10;i++){
printf("%d ",rand());
}
return 0;
}
在此基础上继续讨论随机快速排序的写法。由于现在需要在A[left...right]
中随机选取一个主元,因此不妨生成一个范围在[left,right]
内的随机数p
,然后以A[p]
作为主元来划分。具体做法就是将A[p]
与A[left]
交换,然后按原先Partition函数的写法即可,代码如下。可以注意到,randPartition
函数只需要在原有函数上新增两句话就可以了。
int randPartition(int A[],int left,int right){
//生成[left,right]内的随机数
int p = (int)(round(1.0*rand()/RAND_MAX*(right-left)+left));
swap(A[p],A[left]);
int temp = A[left];
while(left < right){
while(left < right && A[right] >= temp){
right--;
}
A[left] = A[right];
while(left < right && A[left] < temp){
left++;
}
A[right] = A[left];
}
A[right] = temp;
return right;
}
4. 直接选择排序
直接选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度!所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
- 时间复杂度:无论是正序还是逆序,都需要 O ( n ² ) O(n²) O(n²);
- 空间复杂度:对于任何规模的 n n n,都只要那几个临时变量,不随规模变化,因此为 O ( 1 ) O(1) O(1);
- 稳定性:对于原始序列 [2,2,3,1],它是不稳定的。
(1) 算法步骤
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
(2) 算法演示
(3) 代码实现
void selectSort(){
for(int i=1;i<=n;i++){ //进行n次操作
int k = i;
for(int j = i;j<=n;j++){ //选出[i,j]中最小的元素,下标为k
if(A[j] < A[k]){
k = j;
}
}
int temp = A[i];
A[i] = A[k];
A[k] = temp;
//swap(A[i],A[k]);
}
}
5. 堆排序
- 向上调整
- 向下调整
6. 归并排序
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用 分治法(Divide and Conquer,先分后治) 的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
- 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
- 自下而上的迭代;
和直接选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O ( n l o g n ) O(nlogn) O(nlogn) 的时间复杂度。代价是需要 额外的内存空间。
- 关于性能受不受输入数据的影响,主要是看算法对于逆序和正序时两者的性能是否一样
- 时间复杂度:无论是正序还是逆序,都需要
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn),有
l
o
g
n
logn
logn次
merge
,每次merge
为 n n n次; - 空间复杂度:由于每次
merge
都需要一个数组,而且这个数组规模随 n n n成线性变化,因此空间复杂度为 O ( n ) O(n) O(n); - 稳定性:对于原始序列 [2,2,3,1],它是稳定的。
(1) 算法步骤
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
- 重复步骤 3 直到某一指针达到序列尾;
- 将另一序列剩下的所有元素直接复制到合并序列尾。
(2) 算法演示
(3) 代码实现
//递归实现
const int maxn = 100;
//将数组A的[L1,R1]与[L2,R2]区间合并为有序区间(此处L2即为R1+1)
void merge(int A[],int L1,int R1,int L2,int R2){
//其实可以直接用STL里的sort函数偷懒。。。
int i = L1; //i指向A[L1]
int j = L2; //j指向A[L2]
int temp[maxn],index = 0; //temp临时存放合并后的数组,index为其下标
while(i <= R1 && j <= R2){
if(A[i] <= A[j]){
temp[index++] = A[i++];
}else{
temp[index++] = A[j++];
}
}
while(i <= R1){
temp[index++] = A[i++];
}
while(j <= R2){
temp[index++] = A[j++];
}
for(i = 0;i<index;i++){
A[L1+i] = temp[i];
}
}
//将array数组当前区间[left,right]进行归并排序
void mergeSort(int A[],int left,int right){
if(left < right){ //只要left小于right
int mid = (left + right) / 2; //取[left,right]的中点
mergeSort(A,left,mid); //递归,将左子区间[left,mid]归并排序
mergeSort(A,mid+1,right); //递归,将右子区间[mid+1.right]归并排序
merge(A,left,mid,mid+1,right); //将左子区间和右子区间合并
}
}
非递归实现
2-路归并排序的非递归实现主要考虑到这样一点:每次分组时,组内元素个数上限都是2的幂次。于是就可以想到这样的思路:令步长step
的初值为2,然后将数组中每step
个元素作为一组,将其内部进行排序(即把左step/2
个元素与右step/2
个元素合并,而若元素个数不超过step/2
,则不操作);再令step
乘以2,重复上面的操作,直到step/2
超过元素个数n(结合代码想一想为什么此处是step/2
)。代码如下(想一想如果数组下标从0开始,应该修改哪些地方?)
void mergeSort(int A[]){
//step为组内元素个数,step/2为左子区间元素的个数,注意等号可以不取
for(int step=2;step/2<=n;step*=2){
//每step个元素一组,组内前step/2和后step/2个元素进行合并
for(int i=1;i<=n;i+=step){
int mid = i+step/2-1; //左子区间元素个事故为step/2
if(mid+1<=n){ //右子区间存在元素,那么需要合并
//左子区间为[i,mid],右子区间为[mid+1,min(i+step-1,n)]
merge(A,i,mid,mid+1,min(i+step-1,n));
}
}
}
}
//其实可以直接使用sort函数
void mergeSort(){
for(int step=0;step/2<=n;step*=2){
for(int i=1;i<=n;i+=step){
sort(A+i,A+min(i+step,n+1));
}
}
}
7. 基数排序——非比较型整数排序算法
算法思想
基数排序 (Radix Sort) 是一种 非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。基数排序的发明可以追溯到 1887 年赫尔曼·何乐礼在打孔卡片制表机 (Tabulation Machine)上的贡献。
基数排序法会使用到桶 (Bucket),顾名思义,通过将要比较的位(个位、十位、百位…),将要排序的元素分配至 0~9 个桶中,借以达到排序的作用,在某些时候,基数排序法的效率高于其它的比较性排序法。
算法步骤
- 将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零
- 从最低位开始,依次进行一次排序
- 从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列
(2) 算法演示
排序动画过程解释
- 基数排序的方式可以采用 LSD (Least sgnificant digital) 或 MSD (Most sgnificant digital), 所谓 最高位优先 和 最低位优先 是指分配收集操作是从最低位开始还是从最高位开始。【排序算法】基数排序:LSD 与 MSD
- 在本例中使用的是 LSD
- 首先创建编号 0 , 1 ,2 ,3 ,4 ,5 , 6 ,7 ,8 ,9 这 10 个桶
- 遍历整个数列,查看数字的个位数,按照先后顺序存放在对应编号的桶中
- 比如 321 个位数 为 1 ,存放在编号 1 桶中
- 数字 1 个位数 为 1 ,存放在编号 1 桶中,同时存放在 321 上面
- 遍历完整个数列的个位数,将数字存放在桶中后,按照编号顺序取出数字,先放入桶中的数字先取出
- 然后依次遍历整个数列的十位数,按照上述个位数的操作整理数列
- 依次遍历整个数列的百位数,按照上述个位数的操作整理数列
- 这样就完成了 基数排序
(3) 代码实现
2 #include<string.h>
3 #include <algorithm>
4 using namespace std;
5 int maxbit(int data[], int n)
6 {
7 int d = 1;
8 int p = 10;
9 for (int i = 0; i < n; i++)
10 {
11 while (data[i]>=p)
12 {
13 p *= 10;
14 ++d;
15 }
16 }
17 return d;
18 }
19 void radixsort(int data[],int m)
20 {
21 int temp[10][m];
22 int order[10];
23 memset(temp, 0, sizeof(temp));
24 memset(order, 0, sizeof(order));
25 int d = maxbit(data, m);
26
27 int n = 1;
28 for(int x=0;x<d;x++)29 {
30 int i;
31 for (i = 0; i < m; i++)
32 {
33 int lsd = (data[i] / n) % 10;
34 temp[lsd][order[lsd]] = data[i];
35 order[lsd]++;
36 }
37 //
38 int k = 0;
39 for (i = 0; i < 10; i++)
40 {
41 if (order[i] != 0)
42 {
43 int j = 0;
44 for (j = 0; j < order[i]; j++)
45 {
46 data[k] = temp[i][j];
47 k++;
48 }
49 }
50 order[i] = 0;
51 }
52 n *= 10;
53 }
54 }
55 int main()
56 {
57 int a[] = { 1, 2, 8, 6, 44, 88, 63, 22, 633 };
58 int n = sizeof(a) / sizeof(a[0]);
59 for (int i = 0; i < n; i++)
60 cout << a[i] << " ";
61 cout << endl;
62 radixsort(a,n);
63 for (int i = 0; i < n; i++)
64 cout << a[i] << " ";
65 cout << endl;
66 return 0;
67 }
8. 内部排序的应用
考虑因素:
元素数目、元素大小、关键字结构(基数排序)、分布(初始序列是顺序还是逆序)、稳定性、存储结构、辅助空间等
- 若n较小时(n≤50),可采用直接插入排序或简单选择排序。若n较大时,则采用快排、堆排或归并排序;
- 当n很大时,记录关键字位数较少且可分解,采用基数排序;
- 当文件的n个关键字随机分布时,任何借助于“比较”的排序,至少需要 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) 的时间;
- 若初始基本有序,则采用直接插入排序或冒泡排序;
- 当记录元素比较大,应避免大量移动的排序算法,尽量采用链式存储。
9. 外部排序
- 外部排序主要是归并排序方法;
- 归并段概念;
- r r r是初始划分的归并段的个数;
- t I S t_{IS} tIS是一次内部排序需要的时间;
- d d d是进行磁盘操作的次数;
- t I O t_{IO} tIO是一个归并段需要的读写时间;
- S S S是归并的趟数;
- n n n是元素数量;
- t m g t_{mg} tmg是每作一次内部归并,取得一个关键子最小记录的时间;
⭐⭐⭐⭐⭐
- m m m是 m m m路归并排序;
- r r r是 归并段的个数;
接下来讲解一下如果通过减少m
或者是r
来减少外部排序的时间。
(1) 失败树
失败树是树形选择排序的一种变体,可视为一棵 完全二叉树。
(2) 置换-选择排序
划分成 长度不等的归并段。
下面是一个示例:
(3) 最佳归并树
计数排序——非比较排序
算法思想
计数排序是一种 非基于比较 的排序算法,其空间复杂度和时间复杂度均为
O
(
n
+
k
)
O(n+k)
O(n+k) ,其中k
是整数的范围。基于比较的排序算法时间复杂度最小是
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)的。该算法于1954年由 Harold H. Seward 提出。
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是 有确定范围的整数。
算法步骤
- 花O(n)的时间扫描一下整个序列 A,获取最小值 min 和最大值 max
- 开辟一块新的空间创建新的数组 B,长度为 ( max - min + 1)
- 数组 B 中 index 的元素记录的值是 A 中某元素出现的次数
- 最后输出目标整数序列,具体的逻辑是遍历数组 B,输出相应元素以及对应的个数
算法演示
桶排序
算法思想
桶排序(Bucket sort) 是一种基于计数的排序算法(计数排序可参考上节的内容),工作的原理是将数据分到有限数量的桶子里,然后每个桶再分别排序(有可能再使用别的排序算法或是以递回方式继续使用桶排序进行排序)
算法步骤
- 设置固定数量的空桶。
- 把数据放到对应的桶中。
- 对每个不为空的桶中数据进行排序。
- 拼接不为空的桶中数据,得到结果。
算法演示
排序动画过程解释
- 首先,设置固定数量的空桶,在这里为了方便演示,设置桶的数量为 5 个空桶
- 遍历整个数列,找到最大值为 56 ,最小值为 2 ,每个桶的范围为 ( 56 - 2 + 1 )/ 5 = 11
- 再次遍历整个数列,按照公式 floor((数字 – 最小值) / 11) 将数字放到对应的桶中
- 比如,数字 7 代入公式 floor (( 7 – 2 ) / 11 ) = 0 放入 0 号桶
- 数字 12 代入公式 floor((12 – 2) / 11) = 0 放入 0 号桶
- 数字 56 代入公式 floor((56 – 2) / 11) = 4 放入 4 号桶
- 当向同一个索引的桶,第二次插入数据时,判断桶中已存在的数字与新插入数字的大小,按照左到右,从小到大的顺序插入(可以使用前面讲解的插入排序)实现
- 比如,插入数字 19 时, 1 号桶中已经有数字 23 ,在这里使用插入排序,让 19 排在 23 前面
- 遍历完整个数列后,合并非空的桶,按从左到右的顺序合并 0 ,1 ,2 ,3 ,4 桶。
- 这样就完成了 桶排序
时间复杂度分析
9. sort()函数
sort(start,end,cmp)
,其中对于cmp(a,b)
函数,当cmp(a,b)
返回true
时,表示a
在b
的前面~