排序,是每一本數據結構的書都繞不開的重要部分。
排序的算法也是琳琅滿目、五花八門。
每一個算法的背后都是智慧的結晶,思想精華的沉淀。
個人覺得排序算法沒有絕對的孰優孰劣,用對了場景,就是最有的排序算法。
當然,撇開這些業務場景,排序算法本身有一些自己的衡量指標,比如我們經常提到的復雜度分析。
我們如何分析一個算法?
排序算法的執行效率
1、最好、最壞和平均情況的時間復雜度
2、時間復雜度的系數、常數和低階
一般來說,在數據規模n很大的時候,可以忽略這些,但是如果我們需要排序的數據規模在幾百、幾千,那么這些指標就變的更加重要。
3、比較的次數和移動的次數
排序的過程涉及數據的比較和交換(移動)
排序算法的內存消耗
除了時間復雜度,我們還有空間復雜度,用來衡量內存消耗。這里我們引入原地排序的概念。原地排序即特指空間復雜度為O(1)的排序算法。
排序算法的穩定性
什么是穩定性,這比較抽象。
舉個例子,現在有一組集合1,3,5,3,7
按照從小打到的順序進行排序,結果應該是1,3,3,5,7
穩定指的是原集合的第二個3仍然在第四個3前面。不穩定則情況相反。
冒泡排序
原理
相鄰元素兩兩比較,如果滿足大小關系就保持不動,如果不滿足,則兩兩交換位置,以此類推,直到集合有序為止。
之所以叫冒泡排序,因為其過程就猶如水中的氣泡,泡泡越大的就在上面,越小的就在下面。
舉例
現在給定一個集合4,5,6,3,2,1
第一次冒泡過程如下所示
可以看出在這趟冒泡中,最大的泡泡6已經到達最高的位置,要讓集合中所有元素都有序,還要繼續冒泡,如下圖:
代碼
package com.jackie.algo.geek.time.chapter11_sort;
/**
* @Author: Jackie
* @date 2019/1/12
*/
public class BubbleSort {
public static void main(String[] args) {
int[] arr = new int[]{100,82,74,62,54,147};
bubbleSort(arr);
bubbleSort2(arr);
}
/**
* 外層i的循環代表比較的趟數,內層j的循環代表的元素位置
* a[0],a[1],a[2],a[3],a[4],a[5]
* 第一趟走完,最大的元素冒泡到最后a[5]的位置,需要比較的位置即為:
* a[0],a[1],a[2],a[3],a[4]
* 所以可以看到j的終止條件是動態變化的,與i的位置相關,趟數每增加一次,終止的位置就往前挪一個,因為每次都能固定一個元素
*
* 注意這里的邊界條件,是<還是<=
* 第一層是小於,因為是從0開始,對於上面的例子來說,是比較length-1=6-1=5趟,因為總共6個元素,只要5趟就能比較完成
* 好比有兩個元素,只要一趟就能比較完成
* 第二層是同樣的道理,假設在i=0時,length-i-1=6-0-1=5,
* 但是這里<,所以只會到j=4,乍一看你會覺得之比較到了a[j]=a[4],最后a[5]是不是就丟了
* 其實不是,仔細看下面的比較條件就會發現有a[j+1]即a[5]
* 所以,綜上內層和外層都是從0開始,且都是<而不是<=
*/
public static void bubbleSort(int[] arr) {
int length = arr.length;
if (length <= 0) {
return;
}
int temp;
for (int i = 0; i < length - 1; i++) {
boolean flag = false;
for (int j = 0; j < length - i - 1; j++) {
if (arr[j] > arr[j+1]) {
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
flag = true;
}
}
if (!flag) {
System.out.println("total loop: " + (i+1) + " times, stop at index:" + i);
break;
}
}
for (int i = 0; i < length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
/**
* 和上面的不同之處在於,上面的是保證數組從后往前有序,這里的是保證從前往后的有序
* 上面的做法如下所示,每次要遍歷的元素如下
* a[0],a[1],a[2],a[3],a[4],a[5]
* a[0],a[1],a[2],a[3],a[4] (這里不再遍歷a[5]的位置,因為a[5]在第一輪遍歷已是最大,不需要參與遍歷,下面遍歷同理)
* a[0],a[1],a[2],a[3]
* a[0],a[1],a[2]
* a[0],a[1]
* a[0]
*
* 下面的做法如下所示,每次要遍歷的元素如下
* a[0],a[1],a[2],a[3],a[4],a[5]
* a[1],a[2],a[3],a[4],a[5] (這里不再遍歷a[0]的位置,因為a[0]在第一輪遍歷已是最小,不需要參與遍歷,下面遍歷同理)
* a[2],a[3],a[4],a[5]
* a[3],a[4],a[5]
* a[4],a[5]
* a[5]
*/
public static void bubbleSort2(int[] arr) {
int length = arr.length;
if (length <= 0) {
return;
}
int temp;
for (int i = 0; i < length - 1; i++) {
boolean flag = false;
for (int j = length - 1; j > i; j--) {
if (arr[j] < arr[j-1]) {
temp = arr[j];
arr[j] = arr[j-1];
arr[j-1] = temp;
flag = true;
}
}
if (!flag) {
System.out.println("total loop: " + (length - i - 1) + " times, stop at index:" + i);
break;
}
}
for (int i = 0; i < length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
}
寫這類算法對於邊界判定、起始條件和結束條件要非常謹慎,比如是用<
還是用<=
;是從0開始還是從1開始;是到length結束還是到length-1結束。
看似惺忪平常,有時候弄錯一個符號就無法得到正確的排序結果。
冒泡排序的這些注意事項已經寫在代碼的注釋中,參見如上代碼。
同時,代碼已經上傳至Github
各項指標
1、是否是原地排序
是,因為冒泡排序只涉及兩兩元素交換,空間復雜度為O(1)
2、是否是穩定排序
是,對於元素相等的情況,不會交換順序
3、時間復雜度
平均時間復雜度是O(n2), 這里是n的平方
插入排序
原理
對於給定集合,從左至右,依次保證當前元素的左邊集合有序。然后依次順延當前位置,直至遍歷完所有集合元素,保證整個集合有序。
有點抽象,沒有關系,看舉例。
舉例
借用文章https://www.cnblogs.com/bjh1117/p/8335628.html中的例子說明插入排序的過程。
待比較數據:7, 6, 9, 8, 5,1
第一輪:指針指向第二個元素6,假設6左面的元素為有序的,將6抽離出來,形成7,_,9,8,5,1,從7開始,6和7比較,發現7>6。將7右移,形成_,7,9,8,5,1,6插入到7前面的空位,結果:6,7,9,8,5,1
第二輪:指針指向第三個元素9,此時其左面的元素6,7為有序的,將9抽離出來,形成6,7,_,8,5,1,從7開始,依次與9比較,發現9左側的元素都比9小,於是無需移動,把9放到空位中,結果仍為:6,7,9,8,5,1
第三輪:指針指向第四個元素8,此時其左面的元素6,7,9為有序的,將8抽離出來,形成6,7,9,_,5,1,從9開始,依次與8比較,發現8<9,將9向后移,形成6,7,_,9,5,1,8插入到空位中,結果為:6,7,8,9,5,1
第四輪:指針指向第五個元素5,此時其左面的元素6,7,8,9為有序的,將5抽離出來,形成6,7,8,9,_,1,從9開始依次與5比較,發現5比其左側所有元素都小,5左側元素全部向右移動,形成_,6,7,8,9,1,將5放入空位,結果5,6,7,8,9,1。
第五輪:同上,1被移到最左面,最后結果:1,5,6,7,8,9。
代碼
package com.jackie.algo.geek.time.chapter11_sort;
/**
* @Author: Jackie
* @date 2019/1/13
*/
public class InsertSort {
public static void main(String[] args) {
int[] arr = new int[]{100,82,74,62,54,147};
insertSort(arr);
}
/**
* 借用https://www.cnblogs.com/bjh1117/p/8335628.html文中的舉例,我們可以看到一個完整的插入排序的過程
* 通過這個過程,我們可以更好的理解插入排序的思想
* 待比較數據:7, 6, 9, 8, 5,1
*
* 第一輪:指針指向第二個元素6,假設6左面的元素為有序的,將6抽離出來,形成7,_,9,8,5,1,從7開始,6和7比較,發現7>6。將7右移,形成_,7,9,8,5,1,6插入到7前面的空位,結果:6,7,9,8,5,1
*
* 第二輪:指針指向第三個元素9,此時其左面的元素6,7為有序的,將9抽離出來,形成6,7,_,8,5,1,從7開始,依次與9比較,發現9左側的元素都比9小,於是無需移動,把9放到空位中,結果仍為:6,7,9,8,5,1
*
* 第三輪:指針指向第四個元素8,此時其左面的元素6,7,9為有序的,將8抽離出來,形成6,7,9,_,5,1,從9開始,依次與8比較,發現8<9,將9向后移,形成6,7,_,9,5,1,8插入到空位中,結果為:6,7,8,9,5,1
*
* 第四輪:指針指向第五個元素5,此時其左面的元素6,7,8,9為有序的,將5抽離出來,形成6,7,8,9,_,1,從9開始依次與5比較,發現5比其左側所有元素都小,5左側元素全部向右移動,形成_,6,7,8,9,1,將5放入空位,結果5,6,7,8,9,1。
*
* 第五輪:同上,1被移到最左面,最后結果:1,5,6,7,8,9。
*
* 所以插入排序是保證一個元素的左邊所有元素都是有序的,然后逐漸右移,直到遍歷完所有的元素來保證整個數據是有序的
* 下面i從1開始,是表示以a[1]作為哨兵,第一次比較是a[0]和其比較,這里的j的其實位置都是小於i一個位移,即j=i-1
* 然后依次從右向左挨個比較,如果發現哨兵值小於左側有序集合,則一直位移,以此保證始終留有一個位置用於插入待排序的值
* 一旦發現哨兵值如果大於等於(保證穩定性,即不會跑到等於某個值的左側)左側集合中的某個值,
* 則跳出內層循環,仔細想想左側集合是有序的就明白了
* 至於最后為什么是a[j+1]=value,直覺上更應該是a[j]=value,但是記得,在跳出內層循環的時候進行了一次j--操作,
* 所以需要把這個操作補償進來,變成了j+1
*/
public static void insertSort(int arr[]) {
int length = arr.length;
if (length <= 0) {
return;
}
for (int i = 1; i < length; i++) {
int value = arr[i];
int j = i - 1;
for (; j >= 0; j--) {
if (arr[j] > value) {
arr[j+1] = arr[j]; // 位移
} else {
break;
}
}
arr[j+1] = value;
}
for (int i = 0; i < length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
}
同冒泡排序,有關邊界判定、起始條件和結束條件也都寫在注釋中,不再贅述。
各項指標
1、是否是原地排序
是,同冒泡排序,空間復雜度為O(1)
2、是否是穩定排序
是,對於元素相等的情況,不會交換順序
3、時間復雜度
平均時間復雜度是O(n2), 這里是n的平方
選擇排序
原理
選擇排序思想和插入排序思想比較接近。每次排序從未排序的集合中找到最小的元素放進有序集合,通過這樣的遍歷排序保證整個集合有序。
舉例
代碼
package com.jackie.algo.geek.time.chapter11_sort;
/**
* @Author: Jackie
* @date 2019/1/13
*/
public class SelectionSort {
public static void main(String[] args) {
int[] arr = new int[]{100,82,74,62,54,147};
selectionSort(arr);
}
public static void selectionSort(int[] arr) {
int length = arr.length;
if (length <= 1) return;
for (int i = 0; i < length - 1; ++i) {
// 查找最小值
int minIndex = i;
for (int j = i + 1; j < length; ++j) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 交換
int tmp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = tmp;
}
for (int i = 0; i < length; i++) {
System.out.print(arr[i] + " ");
}
}
}
各項指標
1、是否是原地排序
是,同冒泡排序,空間復雜度為O(1)
2、是否是穩定排序
否,通過元素的交換可能改變原來的穩定結構,比如5,8,5,2,9,第一次排序后,5和2交換,則第一個5就跑到第二個5后面了,破壞了穩定結構。
3、時間復雜度
平均時間復雜度是O(n2), 這里是n的平方,且最好最壞都是O(n2)。
聲明:
文中圖片來自極客時間王爭老師專題《數據結構與算法之美》