排序总结

冒泡排序
冒泡排序⽆疑是最为出名的排序算法之⼀,从序列的⼀端开始往另⼀端冒泡(你可以从左
往右冒泡,也可以从右往左冒泡,看⼼情),依次⽐较相邻的两个数的⼤⼩(到底是⽐⼤
还是⽐⼩也看你⼼情)。
图解冒泡
[ 8 2 5 9 7 ] 这组数字来做示例,上图来战:
从左往右依次冒泡,将⼩的往右移动 ⾸先⽐较第⼀个数和第⼆个数的⼤⼩,我们发现 2 8 要⼩,那么保持原位,不做改动。
位置还是 8 2 5 9 7
指针往右移动⼀格,接着⽐较:
⽐较第⼆个数和第三个数的⼤⼩,发现 2 5 要⼩,所以位置交换,交换后数组更新为: [
8 5 2 9 7 ]
指针再往右移动⼀格,继续⽐较: ⽐较第三个数和第四个数的⼤⼩,发现 2 9 要⼩,所以位置交换,交换后数组更新为: [
8 5 9 2 7 ]
同样,指针再往右移动,继续⽐较:
⽐较第 4 个数和第 5 个数的⼤⼩,发现 2 7 要⼩,所以位置交换,交换后数组更新为: [
8 5 9 7 2 ]
下⼀步,指针再往右移动,发现已经到底了,则本轮冒泡结束,处于最右边的 2 就是已经
排好序的数字。
通过这⼀轮不断的对⽐交换,数组中最⼩的数字移动到了最右边。
接下来继续第⼆轮冒泡: 由于右边的 2 已经是排好序的数字,就不再参与⽐较,所以本轮冒泡结束,本轮冒泡最终
冒到顶部的数字 5 也归于有序序列中,现在数组已经变化成了 [ 8 9 7 5 2 ] 让我们开始第三轮冒泡吧! 由于 8 7 ⼤,所以位置不变,此时第三轮冒泡也已经结束,第三轮冒泡的最后结果是 [
9 8 7 5 2 ]
紧接着第四轮冒泡:
9 8 ⽐,位置不变,即确定了 8 进⼊有序序列,那么最后只剩下⼀个数字 9 ,放在末
尾,⾃此排序结束。
代码实现 冒泡的代码还是相当简单的,两层循环,外层冒泡轮数,⾥层依次⽐较,江湖中⼈⼈尽皆
知。
我们看到嵌套循环,应该⽴⻢就可以得出这个算法的时间复杂度为 O(n2)
冒泡优化
冒泡有⼀个最⼤的问题就是这种算法不管不管你有序还是没序,闭着眼睛把你循环⽐较了
再说。
⽐如我举个数组例⼦: [ 9 8 7 6 5 ] ,⼀个有序的数组,根本不需要排序,它仍然是
双层循环⼀个不少的把数据遍历⼲净,这其实就是做了没必要做的事情,属于浪费资源。
针对这个问题,我们可以设定⼀个临时遍历来标记该数组是否已经有序,如果有序了就不
⽤遍历了。
public static void sort ( int arr []){
for ( int i = 0 ; i < arr . length - 1 ; i ++ ){
for ( int j = 0 ; j < arr . length - 1 - i ; j ++ ){
int temp = 0 ;
if ( arr [ j ] < arr [ j + 1 ]){
temp = arr [ j ];
arr [ j ] = arr [ j + 1 ];
arr [ j + 1 ] = temp ;
}
}
}
}
public static void sort ( int arr []){
for ( int i = 0 ; i < arr . length - 1 ; i ++ ){
boolean isSort = true ;
for ( int j = 0 ; j < arr . length - 1 - i ; j ++ ){
int temp = 0 ;
if ( arr [ j ] < arr [ j + 1 ]){
temp = arr [ j ];
arr [ j ] = arr [ j + 1 ];
arr [ j + 1 ] = temp ;
isSort = false ;
}
}
if ( isSort ){
break ;
}
}
选择排序
选择排序的思路是这样的:⾸先,找到数组中最⼩的元素,拎出来,将它和数组的第⼀个
元素交换位置,第⼆步,在剩下的元素中继续寻找最⼩的元素,拎出来,和数组的第⼆个
元素交换位置,如此循环,直到整个数组排序完成。
⾄于选⼤还是选⼩,这个都⽆所谓,你也可以每次选择最⼤的拎出来排,也可以每次选择
最⼩的拎出来的排,只要你的排序的⼿段是这种⽅式,都叫选择排序。
图解选排
我们还是以 [ 8 2 5 9 7 ] 这组数字做例⼦。
第⼀次选择,先找到数组中最⼩的数字 2 ,然后和第⼀个数字交换位置。(如果第⼀个数
字就是最⼩值,那么⾃⼰和⾃⼰交换位置,也可以不做处理,就是⼀个 if 的事情)
} 第⼆次选择,由于数组第⼀个位置已经是有序的,所以只需要查找剩余位置,找到其中最
⼩的数字 5 ,然后和数组第⼆个位置的元素交换。
第三次选择,找到最⼩值 7 ,和第三个位置的元素交换位置。
第四次选择,找到最⼩值 8 ,和第四个位置的元素交换位置。 最后⼀个到达了数组末尾,没有可对⽐的元素,结束选择。
如此整个数组就排序完成了。
代码实现
双层循环,时间复杂度和冒泡⼀模⼀样,都是 O(n2)
插⼊排序
插⼊排序的思想和我们打扑克摸牌的时候⼀样,从牌堆⾥⼀张⼀张摸起来的牌都是乱序
的,我们会把摸起来的牌插⼊到左⼿中合适的位置,让左⼿中的牌时刻保持⼀个有序的状
态。
那如果我们不是从牌堆⾥摸牌,⽽是左⼿⾥⾯初始化就是⼀堆乱牌呢? ⼀样的道理,我们
把牌往⼿的右边挪⼀挪,把⼿的左边空出⼀点位置来,然后在乱牌中抽⼀张出来,插⼊到
左边,再抽⼀张出来,插⼊到左边,再抽⼀张,插⼊到左边,每次插⼊都插⼊到左边合适
的位置,时刻保持左边的牌是有序的,直到右边的牌抽完,则排序完毕。
public static void sort ( int arr []){
for ( int i = 0 ; i < arr . length ; i ++ ){
int min = i ; // 最⼩元素的下标
for ( int j = i + 1 ; j < arr . length ; j ++ ){
if ( arr [ j ] < arr [ min ]){
min = j ; // 找最⼩值
}
}
// 交换位置
int temp = arr [ i ];
arr [ i ] = arr [ min ];
arr [ min ] = temp ;
}
}
图解插排
数组初始化: [ 8 2 5 9 7 ] ,我们把数组中的数据分成两个区域,已排序区域和未排
序区域,初始化的时候所有的数据都处在未排序区域中,已排序区域是空。
第⼀轮,从未排序区域中随机拿出⼀个数字,既然是随机,那么我们就获取第⼀个,然后
插⼊到已排序区域中,已排序区域是空,那么就不做⽐较,默认⾃身已经是有序的了。
(当然了,第⼀轮在代码中是可以省略的,从下标为 1 的元素开始即可) 第⼆轮,继续从未排序区域中拿出⼀个数,插⼊到已排序区域中,这个时候要遍历已排序
区域中的数字挨个做⽐较,⽐⼤⽐⼩取决于你是想升序排还是想倒序排,这⾥排升序:
第三轮,排 5
第四轮,排 9 第五轮,排 7
排序结束。
代码实现
从代码⾥我们可以看出,如果找到了合适的位置,就不会再进⾏⽐较了,就好⽐牌堆⾥抽
出的⼀张牌本身就⽐我⼿⾥的牌都⼩,那么我只需要直接放在末尾就⾏了,不⽤⼀个⼀个
去移动数据腾出位置插⼊到中间。
所以说,最好情况的时间复杂度是 O(n) ,最坏情况的时间复杂度是 O(n2) ,然⽽时间复杂
度这个指标看的是最坏的情况,⽽不是最好的情况,所以插⼊排序的时间复杂度是
O(n2)
希尔排序
public static void sort ( int [] arr ) {
int n = arr . length ;
for ( int i = 1 ; i < n ; ++ i ) {
int value = arr [ i ];
int j = 0 ; // 插⼊的位置
for ( j = i - 1 ; j >= 0 ; j -- ) {
if ( arr [ j ] > value ) {
arr [ j + 1 ] = arr [ j ]; // 移动数据
} else {
break ;
}
}
arr [ j + 1 ] = value ; // 插⼊数据
}
} 希尔排序这个名字,来源于它的发明者希尔,也称作 缩⼩增量排序 ,是插⼊排序的⼀种
更⾼效的改进版本。
我们知道,插⼊排序对于⼤规模的乱序数组的时候效率是⽐较慢的,因为它每次只能将数
据移动⼀位,希尔排序为了加快插⼊的速度,让数据移动的时候可以实现跳跃移动,节省
了⼀部分的时间开⽀。
图解希尔排序
待排序数组 10 个数据:
假设计算出的排序区间为 4 ,那么我们第⼀次⽐较应该是⽤第 5 个数据与第 1 个数据相⽐
较。 调换后的数据为 [ 7 2 5 9 8 10 1 15 12 3 ] ,然后指针右移,第 6 个数据与第 2
个数据相⽐较。
指针右移,继续⽐较。 如果交换数据后,发现减去区间得到的位置还存在数据,那么继续⽐较,⽐如下⾯这张
图, 12 8 相⽐较,原地不动后,指针从 12 跳到 8 身上,继续减去区间发现前⾯还有⼀
个下标为 0 的数据 7 ,那么 8 7 相⽐较。 ⽐较完之后的效果是 7 8 12 三个数为有序排列。 当最后⼀个元素⽐较完之后,我们会发现⼤部分值⽐较⼤的数据都似乎调整到数组的中后
部分了。
假设整个数组⽐较⻓的话,⽐如有 100 个数据,那么我们的区间肯定是四五⼗,调整后区
间再缩⼩成⼀⼆⼗还会重新调整⼀轮,直到最后区间缩⼩为 1 ,就是真正的排序来了。 指针右移,继续⽐较:
重复步骤,即可完成排序,重复的图就不多画了。
我们可以发现,当区间为 1 的时候,它使⽤的排序⽅式就是插⼊排序。
代码实现
public static void sort ( int [] arr ) {
int length = arr . length ;
// 区间
int gap = 1 ;
while ( gap < length ) {
gap = gap * 3 + 1 ;
}
while ( gap > 0 ) {
for ( int i = gap ; i < length ; i ++ ) {
int tmp = arr [ i ]; 可能你会问为什么区间要以 gap = gap*3 + 1 去计算,其实最优的区间计算⽅法是没有答案
的,这是⼀个⻓期未解决的问题,不过差不多都会取在⼆分之⼀到三分之⼀附近。
归并排序
归并字⾯上的意思是合并,归并算法的核⼼思想是分治法,就是将⼀个数组⼀⼑切两半,
递归切,直到切成单个元素,然后重新组装合并,单个元素合并成⼩数组,两个⼩数组合
并成⼤数组,直到最终合并完成,排序完毕。
图解归并排序
int j = i - gap ;
// 跨区间排序
while ( j >= 0 && arr [ j ] > tmp ) {
arr [ j + gap ] = arr [ j ];
j -= gap ;
}
arr [ j + gap ] = tmp ;
}
gap = gap / 3 ;
}
} 我们以 [ 8 2 5 9 7 ] 这组数字来举例
⾸先,⼀⼑切两半:
再切: 再切 粒度切到最⼩的时候,就开始归并 数据量设定的⽐较少,是为了⽅便图解,数据量为单数,是为了让你看到细节,下⾯我画
了⼀张更直观的图可能你会更喜欢:
代码实现
我们上⾯讲过,归并排序的核⼼思想是分治,分⽽治之,将⼀个⼤问题分解成⽆数的⼩问
题进⾏处理,处理之后再合并,这⾥我们采⽤递归来实现:
public static void sort ( int [] arr ) {
int [] tempArr = new int [ arr . length ];
sort ( arr tempArr 0 arr . length - 1 );
}
/**
* 归并排序
* @param arr 排序数组
* @param tempArr 临时存储数组 * @param startIndex 排序起始位置
* @param endIndex 排序终⽌位置
*/
private static void sort ( int [] arr int [] tempArr int startIndex int
endIndex ){
if ( endIndex <= startIndex ){
return ;
}
// 中部下标
int middleIndex = startIndex + ( endIndex - startIndex ) / 2 ;
// 分解
sort ( arr tempArr startIndex middleIndex );
sort ( arr tempArr middleIndex + 1 endIndex );
// 归并
merge ( arr tempArr startIndex middleIndex endIndex );
}
/**
* 归并
* @param arr 排序数组
* @param tempArr 临时存储数组
* @param startIndex 归并起始位置
* @param middleIndex 归并中间位置
* @param endIndex 归并终⽌位置
*/
private static void merge ( int [] arr int [] tempArr int startIndex
int middleIndex int endIndex ) {
// 复制要合并的数据
for ( int s = startIndex ; s <= endIndex ; s ++ ) {
tempArr [ s ] = arr [ s ];
}
int left = startIndex ; // 左边⾸位下标
int right = middleIndex + 1 ; // 右边⾸位下标
for ( int k = startIndex ; k <= endIndex ; k ++ ) {
if ( left > middleIndex ){
// 如果左边的⾸位下标⼤于中部下标,证明左边的数据已经排完了。
arr [ k ] = tempArr [ right ++ ];
} else if ( right > endIndex ){
// 如果右边的⾸位下标⼤于了数组⻓度,证明右边的数据已经排完了。
arr [ k ] = tempArr [ left ++ ];
} else if ( tempArr [ right ] < tempArr [ left ]){ 我们可以发现 merge ⽅法中只有⼀个 for 循环,直接就可以得出每次合并的时间复杂度为
O(n) ,⽽分解数组每次对半切割,属于对数时间 O(log n) ,合起来等于 O(log2n) ,也就是
说,总的时间复杂度为 O(nlogn)
关于空间复杂度,其实⼤部分⼈写的归并都是在 merge ⽅法⾥⾯申请临时数组,⽤临时数
组来辅助排序⼯作,空间复杂度为 O(n) ,⽽我这⾥做的是原地归并,只在最开始申请了⼀
个临时数组,所以空间复杂度为 O(1)
快速排序
快速排序的核⼼思想也是分治法,分⽽治之。它的实现⽅式是每次从序列中选出⼀个基准
值,其他数依次和基准值做⽐较,⽐基准值⼤的放右边,⽐基准值⼩的放左边,然后再对
左边和右边的两组数分别选出⼀个基准值,进⾏同样的⽐较移动,重复步骤,直到最后都
变成单个元素,整个数组就成了有序的序列。
arr [ k ] = tempArr [ right ++ ]; // 将右边的⾸位排⼊,然后右边的下标指
+1
} else {
arr [ k ] = tempArr [ left ++ ]; // 将左边的⾸位排⼊,然后左边的下标指针
+1
}
}
} 图解快排
我们以 [ 8 2 5 0 7 4 6 1 ] 这组数字来进⾏演示
⾸先,我们随机选择⼀个基准值:
与其他元素依次⽐较,⼤的放右边,⼩的放左边:
然后我们以同样的⽅式排左边的数据: 继续排 0 1
由于只剩下⼀个数,所以就不⽤排了,现在的数组序列是下图这个样⼦: 右边以同样的操作进⾏,即可排序完成。
单边扫描
快速排序的关键之处在于切分,切分的同时要进⾏⽐较和移动,这⾥介绍⼀种叫做单边扫
描的做法。
我们随意抽取⼀个数作为基准值,同时设定⼀个标记 mark 代表左边序列最右侧的下标位
置,当然初始为 0 ,接下来遍历数组,如果元素⼤于基准值,⽆操作,继续遍历,如果元
素⼩于基准值,则把 mark + 1 ,再将 mark 所在位置的元素和遍历到的元素交换位置,
mark 这个位置存储的是⽐基准值⼩的数据,当遍历结束后,将基准值与 mark 所在元素交
换位置即可。
代码实现:
public static void sort ( int [] arr ) {
sort ( arr 0 arr . length - 1 );
}
private static void sort ( int [] arr int startIndex int endIndex ) {
if ( endIndex <= startIndex ) {
return ;
}
// 切分
int pivotIndex = partitionV2 ( arr startIndex endIndex );
sort ( arr startIndex pivotIndex - 1 );
sort ( arr pivotIndex + 1 endIndex );
}
private static int partition ( int [] arr int startIndex int endIndex ) {
int pivot = arr [ startIndex ]; // 取基准值
int mark = startIndex ; //Mark 初始化为起始下标 双边扫描
另外还有⼀种双边扫描的做法,看起来⽐较直观:我们随意抽取⼀个数作为基准值,然后
从数组左右两边进⾏扫描,先从左往右找到⼀个⼤于基准值的元素,将下标指针记录下
来,然后转到从右往左扫描,找到⼀个⼩于基准值的元素,交换这两个元素的位置,重复
步骤,直到左右两个指针相遇,再将基准值与左侧最右边的元素交换。
我们来看⼀下实现代码,不同之处只有 partition ⽅法:
for ( int i = startIndex + 1 ; i <= endIndex ; i ++ ){
if ( arr [ i ] < pivot ){
// ⼩于基准值 则 mark+1 ,并交换位置。
mark ++ ;
int p = arr [ mark ];
arr [ mark ] = arr [ i ];
arr [ i ] = p ;
}
}
// 基准值与 mark 对应元素调换位置
arr [ startIndex ] = arr [ mark ];
arr [ mark ] = pivot ;
return mark ;
}
public static void sort ( int [] arr ) {
sort ( arr 0 arr . length - 1 );
}
private static void sort ( int [] arr int startIndex int endIndex ) {
if ( endIndex <= startIndex ) {
return ;
}
// 切分
int pivotIndex = partition ( arr startIndex endIndex );
sort ( arr startIndex pivotIndex - 1 );
sort ( arr pivotIndex + 1 endIndex );
}
private static int partition ( int [] arr int startIndex int endIndex ) {
int left = startIndex ;
int right = endIndex ;
int pivot = arr [ startIndex ]; // 取第⼀个元素为基准值 极端情况
快速排序的时间复杂度和归并排序⼀样, O(n log n) ,但这是建⽴在每次切分都能把数组⼀
⼑切两半差不多⼤的前提下,如果出现极端情况,⽐如排⼀个有序的序列,如 [ 9 8 7
6 5 4 3 2 1 ] ,选取基准值 9 ,那么需要切分 n - 1 次才能完成整个快速排序的过
程,这种情况下,时间复杂度就退化成了 O(n2) ,当然极端情况出现的概率也是⽐较低
的。
while ( true ) {
// 从左往右扫描
while ( arr [ left ] <= pivot ) {
left ++ ;
if ( left == right ) {
break ;
}
}
// 从右往左扫描
while ( pivot < arr [ right ]) {
right -- ;
if ( left == right ) {
break ;
}
}
// 左右指针相遇
if ( left >= right ) {
break ;
}
// 交换左右数据
int temp = arr [ left ];
arr [ left ] = arr [ right ];
arr [ right ] = temp ;
}
// 将基准值插⼊序列
int temp = arr [ startIndex ];
arr [ startIndex ] = arr [ right ];
arr [ right ] = temp ;
return right ;
} 所以说,快速排序的时间复杂度是 O(nlogn) ,极端情况下会退化成 O(n2) ,为了避免极端
情况的发⽣,选取基准值应该做到随机选取,或者是打乱⼀下数组再选取。
另外,快速排序的空间复杂度为 O(1)
堆排序
堆排序顾名思义,是利⽤堆这种数据结构来进⾏排序的算法。
如果你不了解堆这种数据结构,可以查看⼩吴之前的数据结构系列⽂章 --- 看动画轻松理解
如果你了解堆这种数据结构,你应该知道堆是⼀种优先队列,两种实现,最⼤堆和最⼩
堆,由于我们这⾥排序按升序排,所以就直接以最⼤堆来说吧。
我们完全可以把堆(以下全都默认为最⼤堆)看成⼀棵完全⼆叉树,但是位于堆顶的元素
总是整棵树的最⼤值,每个⼦节点的值都⽐⽗节点⼩,由于堆要时刻保持这样的规则特
性,所以⼀旦堆⾥⾯的数据发⽣变化,我们必须对堆重新进⾏⼀次构建。
既然堆顶元素永远都是整棵树中的最⼤值,那么我们将数据构建成堆后,只需要从堆顶取
元素不就好了吗? 第⼀次取的元素,是否取的就是最⼤值?取完后把堆重新构建⼀下,然
后再取堆顶的元素,是否取的就是第⼆⼤的值? 反复的取,取出来的数据也就是有序的数
据。 图解堆排
我们以 [ 8 2 5 9 7 3 ] 这组数据来演示。
⾸先,将数组构建成堆。
既然构建成堆结构了,那么接下来,我们取出堆顶的数据,也就是数组第⼀个数 9 ,取法
是将数组的第⼀位和最后⼀位调换,然后将数组的待排序范围 -1 现在的待排序数据是 [ 3 8 5 2 7 ] ,我们继续将待排序数据构建成堆。
取出堆顶数据,这次就是第⼀位和倒数第⼆位交换了,因为待排序的边界已经减 1
继续构建堆 从堆顶取出来的数据最终形成⼀个有序列表,重复的步骤就不再赘述了,我们来看⼀下代
码实现。
代码实现
public static void sort ( int [] arr ) {
int length = arr . length ;
// 构建堆
buildHeap ( arr length );
for ( int i = length - 1 ; i > 0 ; i -- ) {
// 将堆顶元素与末位元素调换
int temp = arr [ 0 ];
arr [ 0 ] = arr [ i ];
arr [ i ] = temp ;
// 数组⻓度 -1 隐藏堆尾元素
length -- ;
// 将堆顶元素下沉 ⽬的是将最⼤的元素浮到堆顶来
sink ( arr 0 length );
}
}
private static void buildHeap ( int [] arr int length ) {
for ( int i = length / 2 ; i >= 0 ; i -- ) {
sink ( arr i length );
}
}
/**
* 下沉调整 堆排序和快速排序的时间复杂度都⼀样是 O(nlogn)
计数排序
计数排序是⼀种⾮基于⽐较的排序算法,我们之前介绍的各种排序算法⼏乎都是基于元素
之间的⽐较来进⾏排序的,计数排序的时间复杂度为 O(n + m ) m 指的是数据量,说的简
单点,计数排序算法的时间复杂度约等于 O(n) ,快于任何⽐较型的排序算法。
* @param arr 数组
* @param index 调整位置
* @param length 数组范围
*/
private static void sink ( int [] arr int index int length ) {
int leftChild = 2 * index + 1 ; // 左⼦节点下标
int rightChild = 2 * index + 2 ; // 右⼦节点下标
int present = index ; // 要调整的节点下标
// 下沉左边
if ( leftChild < length && arr [ leftChild ] > arr [ present ]) {
present = leftChild ;
}
// 下沉右边
if ( rightChild < length && arr [ rightChild ] > arr [ present ]) {
present = rightChild ;
}
// 如果下标不相等 证明调换过了
if ( present != index ) {
// 交换值
int temp = arr [ index ];
arr [ index ] = arr [ present ];
arr [ present ] = temp ;
// 继续下沉
sink ( arr present length );
}
} 图解计数
以下以 [ 3 5 8 2 5 4 ] 这组数字来演示。
⾸先,我们找到这组数字中最⼤的数,也就是 8 ,创建⼀个最⼤下标为 8 的空数组 arr
遍历数据,将数据的出现次数填⼊ arr 中对应的下标位置中。 遍历 arr ,将数据依次取出即可。 代码实现
public static void sort ( int [] arr ) {
// 找出数组中的最⼤值
int max = arr [ 0 ];
for ( int i = 1 ; i < arr . length ; i ++ ) {
if ( arr [ i ] > max ) {
max = arr [ i ];
}
}
// 初始化计数数组
int [] countArr = new int [ max + 1 ];
// 计数
for ( int i = 0 ; i < arr . length ; i ++ ) {
countArr [ arr [ i ]] ++ ; 稳定排序
有⼀个需求就是当对成绩进⾏排名次的时候,如何在原来排前⾯的⼈,排序后还是处于相
同成绩的⼈的前⾯。
解题的思路是对 countArr 计数数组进⾏⼀个变形,变来和名次挂钩,我们知道 countArr
放的是分数的出现次数,那么其实我们可以算出每个分数的最⼤名次,就是将 countArr
的每个元素顺序求和。
如下图:
arr [ i ] = 0 ;
}
// 排序
int index = 0 ;
for ( int i = 0 ; i < countArr . length ; i ++ ) {
if ( countArr [ i ] > 0 ) {
arr [ index ++ ] = i ;
}
}
} 变形之后是什么意思呢?
我们把原数组 [ 2 5 8 2 5 4 ] 中的数据依次拿来去 countArr 去找,你会发现 3 这个数
countArr[3] 中的值是 2 ,代表着排名第⼆名,(因为第⼀名是最⼩的 2 ,对吧?), 5
这个数在 countArr[5] 中的值是 5 ,为什么是 5 呢?我们来数数,排序后的数组应该是 [ 2
3 4 5 5 8 ] 5 的排名是第五名,那 4 的排名是第⼏名呢?对应 countArr[4] 的值是 3
,第三名, 5 的排名是第五名是因为 5 这个数有两个,⾃然占据了第 4 名和第 5 名。
所以我们取排名的时候应该特别注意,原数组中的数据要从右往左取,从 countArr 取出排
名后要把 countArr 中的排名减 1 ,以便于再次取重复数据的时候排名往前⼀位。
对应代码实现:
public static void sort ( int [] arr ) {
// 找出数组中的最⼤值
int max = arr [ 0 ];
for ( int i = 1 ; i < arr . length ; ++ i ) {
if ( arr [ i ] > max ) {
max = arr [ i ];
}
}
// 初始化计数数组
int [] countArr = new int [ max + 1 ];
// 计数
for ( int i = 0 ; i < arr . length ; ++ i ) {
countArr [ arr [ i ]] ++ ;
}
// 顺序累加
for ( int i = 1 ; i < max + 1 ; ++ i ) {
countArr [ i ] = countArr [ i - 1 ] + countArr [ i ];
}
// 排序后的数组
int [] sortedArr = new int [ arr . length ];
// 排序
for ( int i = arr . length - 1 ; i >= 0 ; -- i ) {
sortedArr [ countArr [ arr [ i ]] - 1 ] = arr [ i ];
countArr [ arr [ i ]] -- ;
} 计数局限性
计数排序的⽑病很多,我们来找找 bug
如果我要排的数据⾥有 0 呢? int[] 初始化内容全是 0 ,排⽑线。
如果我要排的数据范围⽐较⼤呢?⽐如 [ 1 9999 ] ,我排两个数你要创建⼀个 int[10000]
数组来计数?
对于第⼀个 bug ,我们可以使⽤偏移量来解决,⽐如我要排 [ -1 0 -3 ] 这组数字,这个简
单,我全给你们加 10 来计数,变成 [ 9 10 7 ] 计完数后写回原数组时再减 10 。不过有可
能也会踩到坑,万⼀你数组⾥恰好有⼀个 -10 ,你加上 10 后⼜变 0 了,排⽑线。
对于第⼆个 bug ,确实解决不了,如果是 [ 9998 9999 ] 这种虽然值⼤但是相差范围不⼤的
数据我们也可以使⽤偏移量解决,⽐如这两个数据,我减掉 9997 后只需要申请⼀个 int[3]
的数组就可以进⾏计数。
由此可⻅,计数排序只适⽤于正整数并且取值范围相差不⼤的数组排序使⽤,它的排序的
速度是⾮常可观的。
桶排序
桶排序可以看成是计数排序的升级版,它将要排的数据分到多个有序的桶⾥,每个桶⾥的
数据再单独排序,再把每个桶的数据依次取出,即可完成排序。
// 将排序后的数据拷⻉到原数组
for ( int i = 0 ; i < arr . length ; ++ i ) {
arr [ i ] = sortedArr [ i ];
}
} 图解桶排序
我们拿⼀组计数排序啃不掉的数据 [ 500 6123 1700 10 9999 ] 来举例。
第⼀步,我们创建 10 个桶,分别来装 0-1000 1000-2000 2000-3000 3000-4000
4000-5000 5000-6000 6000-7000 7000-8000 8000-9000 区间的数据。
第⼆步,遍历原数组,对号⼊桶。 第三步,对桶中的数据进⾏单独排序,只有第⼀个桶中的数量⼤于 1 ,显然只需要排第⼀
个桶。
最后,依次将桶中的数据取出,排序完成。 代码实现
这个桶排序乍⼀看好像挺简单的,但是要敲代码就需要考虑⼏个问题了。
桶这个东⻄怎么表示?
怎么确定桶的数量?
桶内排序⽤什么⽅法排?
代码如下:
public static void sort ( int [] arr ){
// 最⼤最⼩值
int max = arr [ 0 ];
int min = arr [ 0 ];
int length = arr . length ;
for ( int i = 1 ; i < length ; i ++ ) {
if ( arr [ i ] > max ) {
max = arr [ i ];
} else if ( arr [ i ] < min ) {
min = arr [ i ];
} }
// 最⼤值和最⼩值的差
int diff = max - min ;
// 桶列表
ArrayList < ArrayList < Integer >> bucketList = new ArrayList <> ();
for ( int i = 0 ; i < length ; i ++ ){
bucketList . add ( new ArrayList <> ());
}
// 每个桶的存数区间
float section = ( float ) diff / ( float ) ( length - 1 );
// 数据⼊桶
for ( int i = 0 ; i < length ; i ++ ){
// 当前数除以区间得出存放桶的位置 减 1 后得出桶的下标
int num = ( int ) ( arr [ i ] / section ) - 1 ;
if ( num < 0 ){
num = 0 ;
}
bucketList . get ( num ). add ( arr [ i ]);
}
// 桶内排序
for ( int i = 0 ; i < bucketList . size (); i ++ ){
//jdk 的排序速度当然信得过
Collections . sort ( bucketList . get ( i ));
}
// 写⼊原数组
int index = 0 ;
for ( ArrayList < Integer > arrayList : bucketList ){
for ( int value : arrayList ){
arr [ index ] = value ;
index ++ ;
}
}
}
桶当然是⼀个可以存放数据的集合,我这⾥使⽤ arrayList ,如果你使⽤ LinkedList 那其实
也是没有问题的。
桶的数量我认为设置为原数组的⻓度是合理的,因为理想情况下每个数据装⼀个桶。 数据⼊桶的映射算法其实是⼀个开放性问题,我承认我这⾥写的⽅案并不佳,因为我测试
过不同的数据集合来排序,如果你有什么更好的⽅案或想法,欢迎留⾔讨论。
桶内排序为了⽅便起⻅使⽤了当前语⾔提供的排序⽅法,如果对于稳定排序有所要求,可
以选择使⽤⾃定义的排序算法。
桶排序的思考及其应⽤
在额外空间充⾜的情况下,尽量增⼤桶的数量,极限情况下每个桶只有⼀个数据时,或者
是每只桶只装⼀个值时,完全避开了桶内排序的操作,桶排序的最好时间复杂度就能够达
O(n)
⽐如⾼考总分 750 分,全国⼏百万⼈,我们只需要创建 751 个桶,循环⼀遍挨个扔进去,
排序速度是毫秒级。
但是如果数据经过桶的划分之后,桶与桶的数据分布极不均匀,有些数据⾮常多,有些数
据⾮常少,⽐如 [ 8 2 9 10 1 23 53 22 12 9000 ] 这⼗个数据,我们分成⼗个桶
装,结果发现第⼀个桶装了 9 个数据,这是⾮常影响效率的情况,会使时间复杂度下降到
O(nlogn) ,解决办法是我们每次桶内排序时判断⼀下数据量,如果桶⾥的数据量过⼤,那
么应该在桶⾥⾯回调⾃身再进⾏⼀次桶排序。
基数排序
基数排序是⼀种⾮⽐较型整数排序算法,其原理是将数据按位数切割成不同的数字,然后
按每个位数分别⽐较。 假设说,我们要对 100 万个⼿机号码进⾏排序,应该选择什么排序
算法呢?排的快的有归并、快排时间复杂度是 O(nlogn) ,计数排序和桶排序虽然更快⼀
些,但是⼿机号码位数是 11 位,那得需要多少桶?内存条表示不服。
这个时候,我们使⽤基数排序是最好的选择。 图解基排
我们以 [ 892 846 821 199 810 700 ] 这组数字来做例⼦演示。
⾸先,创建⼗个桶,⽤来辅助排序。
先排个位数,根据个位数的值将数据放到对应下标值的桶中。 排完后,我们将桶中的数据依次取出。
那么接下来,我们排⼗位数。 最后,排百位数。
排序完成。
代码实现 基数排序可以看成桶排序的扩展,也是⽤桶来辅助排序,代码如下:
public static void sort ( int [] arr ){
int length = arr . length ;
// 最⼤值
int max = arr [ 0 ];
for ( int i = 0 ; i < length ; i ++ ){
if ( arr [ i ] > max ){
max = arr [ i ];
}
}
// 当前排序位置
int location = 1 ;
// 桶列表
ArrayList < ArrayList < Integer >> bucketList = new ArrayList <> ();
// ⻓度为 10 装⼊余数 0-9 的数据
for ( int i = 0 ; i < 10 ; i ++ ){
bucketList . add ( new ArrayList ());
}
while ( true )
{
// 判断是否排完
int dd = ( int ) Math . pow ( 10 ( location - 1 ));
if ( max < dd ){
break ;
}
// 数据⼊桶
for ( int i = 0 ; i < length ; i ++ )
{
// 计算余数 放⼊相应的桶
int number = (( arr [ i ] / dd ) % 10 );
bucketList . get ( number ). add ( arr [ i ]);
}
// 写回数组
int nn = 0 ;
for ( int i = 0 ; i < 10 ; i ++ ){
int size = bucketList . get ( i ). size ();
for ( int ii = 0 ; ii < size ; ii ++ ){
arr [ nn ++ ] = bucketList . get ( i ). get ( ii ); }
bucketList . get ( i ). clear ();
}
location ++ ;
}
}
其实它的思想很简单,不管你的数字有多⼤,按照⼀位⼀位的排, 0 - 9 最多也就⼗个桶:
先按权重⼩的位置排序,然后按权重⼤的位置排序。
当然,如果你有需求,也可以选择从⾼位往低位排。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值