下面使用的图解资源来自于 我的第一本算法书
,里面的内容都比较基础简单、容易理解,下面排序算法中有提及到的,大家不理解的可以自己下载,学习学习,我觉得算法还是很重要的,但是搞得全是概念的话,会让我们学渣觉得没啥意思,学霸就不要看了,再见来不及挥手,鬼刀一开看不见走位走位,直接看算法导论去吧。
链接:https://pan.baidu.com/s/1pykWDv36c4Itux3fJjjYsQ
提取码:xsul
希尔排序、计数排序、基数排序、桶排序都来自于 趣谈编程 的相关博客,因为觉得讲的比较简单生动,当然有兴趣的同学可以关注他,学习更多有趣的编程知识。
下面算法中用到的交换两个元素位置的swap方法,代码如下:
private static void swap(Object[] arr, int i, int j) {
Object t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
冒泡排序
代码实现:
public static void sort(Comparable[] arr){
int n = arr.length;
boolean swapped = false;
do{
swapped = false;
for( int i = 1 ; i < n ; i ++ ){
if( arr[i-1].compareTo(arr[i]) > 0 ){
swap( arr , i-1 , i );
swapped = true;
}
}
// 优化, 每一趟Bubble Sort都将最大的元素放在了最后的位置
// 所以下一次排序, 最后的元素可以不再考虑
n --;
}while(swapped);
}
进一步优化:
public static void sort(Comparable[] arr){
int n = arr.length;
int newn; // 使用newn进行优化
do{
newn = 0;
for( int i = 1 ; i < n ; i ++ ){
if( arr[i-1].compareTo(arr[i]) > 0 ){
swap( arr , i-1 , i );
// 记录最后一次的交换位置,在此之后的元素在下一轮扫描中均不考虑
newn = i;
}
}
n = newn;
} while(newn > 0);
}
选择排序
代码实现:
public static void sort(Comparable[] arr){
int n = arr.length;
for( int i = 0 ; i < n ; i ++ ){
// 寻找[i, n)区间里的最小值的索引
int minIndex = i;
for( int j = i + 1 ; j < n ; j ++ ){
// 使用compareTo方法比较两个Comparable对象的大小
if( arr[j].compareTo( arr[minIndex] ) < 0 ){
minIndex = j;
}
}
swap( arr , i , minIndex);
}
}
进一步优化:
public static void sort(Comparable[] arr){
int left = 0, right = arr.length - 1;
while(left < right){
int minIndex = left;
int maxIndex = right;
// 在每一轮查找时, 要保证arr[minIndex] <= arr[maxIndex]
if(arr[minIndex].compareTo(arr[maxIndex]) > 0){
swap(arr, minIndex, maxIndex);
}
for(int i = left + 1 ; i < right; i ++){
if(arr[i].compareTo(arr[minIndex]) < 0){
minIndex = i;
} else if(arr[i].compareTo(arr[maxIndex]) > 0){
maxIndex = i;
}
}
swap(arr, left, minIndex);
swap(arr, right, maxIndex);
left ++;
right --;
}
}
插入排序
代码实现:
public static void sort(Comparable[] arr){
int n = arr.length;
for (int i = 0; i < n; i++) {
// 寻找元素arr[i]合适的插入位置
// 写法1
for( int j = i ; j > 0 ; j -- ){
if( arr[j].compareTo( arr[j-1] ) < 0 ){
swap( arr, j , j-1 );
} else{
break;
}
}
// 写法2
for( int j = i; j > 0 && arr[j].compareTo(arr[j-1]) < 0 ; j--){
swap(arr, j, j-1);
}
// 写法3
Comparable e = arr[i];
int j = i;
for( ; j > 0 && arr[j-1].compareTo(e) > 0 ; j--)
arr[j] = arr[j-1];
arr[j] = e;
}
}
希尔排序
希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一。
怎样可以对插入排序算法做出优化呢?我们不妨从插入排序的两个特点入手:
1.在大多数元素已经有序的情况下,插入排序的工作量较小
这个结论很明显,如果一个数组大部分元素都有序,那么数组中的元素自然不需要频繁地进行比较和交换。
2.在元素数量较少的情况下,插入排序的工作量较小
这个结论更加显而易见,插入排序的工作量和n的平方成正比,如果n比较小,那么排序的工作量自然要小得多。
如何对原始数组进行预处理呢?聪明的科学家想到了一种分组 排序的方法,以此对数组进行一定的“粗略调整”。
所谓分组,就是让元素两两一组,同组两个元素之间的跨度,都是数组总长度的一半,也就是跨度为4。
如图所示,元素5和元素9一组,元素8和元素2一组,元素6和元素1一组,元素3和元素7一组,一共4组。
接下来,我们让每组元素进行独立排序,排序方式用直接插入排序即可。由于每一组的元素数量很少,只有两个,所以插入排序的工作量很少。每组排序完成后的数组如下:
这样一来,仅仅经过几次简单的交换,数组整体的有序程度得到了显著提高,使得后续再进行直接插入排序的工作量大大减少。这种做法,可以理解为对原始数组的“粗略调整”。
但是这样还不算完,我们可以进一步缩小分组跨度,重复上述工作。把跨度缩小为原先的一半,也就是跨度为2,重新对元素进行分组:
如图所示,元素5,1,9,6一组,元素2,3,8,7一组,一共两组。
接下来,我们继续让每组元素进行独立排序,排序方式用直接插入排序即可。每组排序完成后的数组如下:
此时,数组的有序程度进一步提高,为后续将要进行的排序铺平了道路。
最后,我们把分组跨度进一步减小,让跨度为1,也就等同于做直接插入排序。经过之前的一系列粗略调整,直接插入排序的工作量减少了很多,排序结果如下:
让我们重新梳理一下分组排序的整个过程:
像这样逐步分组进行粗调,再进行直接插入排序的思想,就是希尔排序,根据该算法的发明者,计算机科学家Donald Shell的名字所命名。
上面示例中所使用的分组跨度(4,2,1),被称为希尔排序的增量,增量的选择可以有很多种,我们在示例中所用的逐步折半的增量方法,是Donald Shell在发明希尔排序时提出的一种朴素方法,被称为希尔增量。
在某些极端的情况下,希尔排序的时间复杂度仍然是O(n^2),甚至比直接插入排序更慢,比如下面的例子:
上面这个数组,如果我们照搬之前的分组思路,无论是以4为增量,还是以2为增量,每组内部的元素都没有任何交换。一直到我们把增量缩减为1,数组才会按照直接插入排序的方式进行调整。
对于这样的数组,希尔排序不但没有减少直接插入排序的工作量,反而白白增加了分组操作的成本。
每一轮希尔增量之间都是等比的,这就导致了希尔增量存在盲区,为了避免这种极端情况,科学家们发明了更为严谨的增量方式。
如何为希尔排序选择更有效的增量方式呢?
为了保证分组粗调没有盲区,每一轮的增量需要彼此“互质”,也就是没有除1之外的公约数。
于是,人们相继提出了很多种增量方式,其中最具代表性的是Hibbard增量和Sedgewick增量。
Hibbard的增量序列如下:
1,3,7,15…
通项公式 2^k-1
利用此种增量方式的希尔排序,最坏时间复杂度是O(n^(3/2))
Sedgewick的增量序列如下:
1, 5, 19, 41, 109…
通项公式 9*4^k - 9*2^k + 1
或者4^k - 3*2^k + 1
利用此种增量方式的希尔排序