一、分類
1.內部排序和外部排序
- 內部排序:待排序記錄存放在計算機隨機存儲器中(說簡單點,就是內存)進行的排序過程。
- 外部排序:待排序記錄的數量很大,以致於內存不能一次容納全部記錄,所以在排序過程中需要對外存進行訪問的排序過程。
2.比較類排序和非比較排序
- 比較類排序:通過比較來決定元素間的相對次序,由於其時間復雜度不能突破O(nlogn),因此也稱為非線性時間比較類排序。
- 非比較類排序:不通過比較來決定元素間的相對次序,它可以突破基於比較排序的時間下界,以線性時間運行,因此也稱為線性時間非比較類排序。
二、復雜度分析,算法穩定性和適用場景
- 穩定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
- 不穩定:如果a原本在b的前面,而a=b,排序之后 a 可能會出現在 b 的后面。
- 時間復雜度:對排序數據的總的操作次數。反映當n變化時,操作次數呈現什么規律。
- 空間復雜度:是指算法在計算機內執行時所需存儲空間的度量,它也是數據規模n的函數。
三、八大排序算法詳解
1.選擇排序
1.1 動圖演示
1.2 思路分析
1. 第一個跟后面的所有數相比,如果小於(或小於)第一個數的時候,暫存較小數的下標,第一趟結束后,將第一個數,與暫存的那個最小數進行交換,第一個數就是最小(或最大的數)
2. 下標移到第二位,第二個數跟后面的所有數相比,一趟下來,確定第二小(或第二大)的數
重復以上步驟
直到指針移到倒數第二位,確定倒數第二小(或倒數第二大)的數,那么最后一位也就確定了,排序完成。
1.3 復雜度分析
1. 不管原始數組是否有序,時間復雜度都是O(n2),
因為沒一個數都要與其他數比較一次,(n-1)2次,分解:n2-2n+1, 去掉低次冪和常數,剩下n2,所以最后的時間復雜度是n2
2. 空間復雜度是O(1),因為只定義了兩個輔助變量,與n的大小無關,所以空間復雜度為O(1)
1.4 Java 代碼如下:
import java.util.Arrays; public class Main { public static void main(String[] args) { int[] n = new int[]{1,6,3,8,33,27,66,9,7,88}; int temp,index = -1; for (int i = 0; i < n.length-1; i++) { index=i; //如果大於,暫存較小的數的下標
for (int j = i+1; j <n.length; j++) { if(n[index]>n[j]){ index = j; } } ////將一趟下來求出的最小數,與這個數交換
if(index>0){ temp = n[i]; n[i] = n[index]; n[index] = temp; } System.out.println(Arrays.toString(n)); } System.out.println(Arrays.toString(n)); } }
2. 冒泡排序
2.1 動圖演示
2.2 思路分析
1. 相鄰兩個數兩兩相比,n[i]跟n[j+1]比,如果n[i]>n[j+1],則將連個數進行交換,
2. j++, 重復以上步驟,第一趟結束后,最大數就會被確定在最后一位,這就是冒泡排序又稱大(小)數沉底,
3. i++,重復以上步驟,直到i=n-1結束,排序完成。
2.3 復雜度分析
1. 不管原始數組是否有序,時間復雜度都是O(n2),
因為沒一個數都要與其他數比較一次,(n-1)2次,分解:n2+2n-1, 去掉低次冪和常數,剩下n2,所以最后的時間復雜度是n2
2. 空間復雜度是O(1),因為只定義了一個輔助變量,與n的大小無關,所以空間復雜度為O(1)
2.4 選擇排序和冒泡排序的比較
1. 時間負責度都是O(n2)
2. 空間復雜度都是O(1)
3. 選擇排序是從第一位開始確定最大或最小的數,保證前面的數都是有序的,且都比后面的數小或大,
冒泡排序是從最后一位開始確定最大或最小的數,保證后面的數都是有序的且都大於或小於前面的數。
2.5 Java 代碼如下
import java.util.Arrays; public class 冒泡 { public static void main(String[] args) { int[] n = new int[]{1,6,3,8,33,27,66,9,7,88}; int temp; for (int i = 0; i < n.length-1; i++) { for (int j = 0; j <n.length-1; j++) { if(n[j]>n[j+1]){ temp = n[j]; n[j] = n[j+1]; n[j+1] = temp; } } } System.out.println(Arrays.toString(n)); } }
3. 直接插入排序
3.1 動圖演示
3.2 思路分析
例如從小到大排序:
1. 從第二位開始遍歷,
2. 當前數(第一趟是第二位數)與前面的數依次比較,如果前面的數大於當前數,則將這個數放在當前數的位置上,當前數的下標-1,
3. 重復以上步驟,直到當前數不大於前面的某一個數為止,這時,將當前數,放到這個位置,
1-3步就是保證當前數的前面的數都是有序的,內層循環的目的就是將當前數插入到前面的有序序列里
4. 重復以上3步,直到遍歷到最后一位數,並將最后一位數插入到合適的位置,插入排序結束。
根據思路分析,每一趟的執行流程如下圖所示:
3.3 復雜度分析
1. 時間復雜度:插入算法,就是保證前面的序列是有序的,只需要把當前數插入前面的某一個位置即可。
所以如果數組本來就是有序的,則數組的最好情況下時間復雜度為O(n)
如果數組恰好是倒=倒序,比如原始數組是5 4 3 2 1,想要排成從小到大,則每一趟前面的數都要往后移,一共要執行n-1 + n-2 + … + 2 + 1 = n * (n-1) / 2 = 0.5 * n2 - 0.5 * n次,去掉低次冪及系數,所以最壞情況下時間復雜度為O(n2)
平均時間復雜度(n+n2 )/2,所以平均時間復雜度為O(n2)
2. 空間復雜度:插入排序算法,只需要兩個變量暫存當前數,以及下標,與n的大小無關,所以空間復雜度為:O(1)
3.4 Java 代碼如下
import java.util.Arrays; public class insertSort { public static void main(String[] args) { int[] n = new int[]{20,12,15,1,5,49,58,24,578,211,20,214,78,35,125,789,11}; int temp = 0,j; for (int i = 1; i < n.length; i++) { temp = n[i]; for (j = i; j >0; j--) { //如果當前數前面的數大於當前數,則把前面的數向后移一個位置
if(n[j-1]>temp){ n[j] = n[j-1]; //第一個數已經移到第二個數,將當前數放到第一個位置,這一趟結束
if(j==1){ n[j-1] = temp; break; } }else{//如果不大於,將當前數放到j的位置,這一趟結束
n[j] = temp; break; } } System.out.println(Arrays.toString(n)); } System.out.println(Arrays.toString(n)); } }
4. 快速排序
4.1 動圖演示
4.2 思路分析
快速排序的思想就是,選一個數作為基數(這里我選的是第一個數),大於這個基數的放到右邊,小於這個基數的放到左邊,等於這個基數的數可以放到左邊或右邊,看自己習慣,這里我是放到了左邊,
一趟結束后,將基數放到中間分隔的位置,第二趟將數組從基數的位置分成兩半,分割后的兩個的數組繼續重復以上步驟,選基數,將小數放在基數左邊,將大數放到基數的右邊,在分割數組,,,直到數組不能再分為止,排序結束。
例如從小到大排序:
1. 第一趟,第一個數為基數temp,設置兩個指針left = 0,right = n.length,
①從right開始與基數temp比較,如果n[right]>基數temp,則right指針向前移一位,繼續與基數temp比較,直到不滿足n[right]>基數temp
②將n[right]賦給n[left]
③從left開始與基數temp比較,如果n[left]<=基數temp,則left指針向后移一位,繼續與基數temp比較,直到不滿足n[left]<=基數temp
④將n[left]賦給n[rigth]
⑤重復①-④步,直到left==right結束,將基數temp賦給n[left]
2. 第二趟,將數組從中間分隔,每個數組再進行第1步的操作,然后再將分隔后的數組進行分隔再快排,
3. 遞歸重復分隔快排,直到數組不能再分,也就是只剩下一個元素的時候,結束遞歸,排序完成
根據思路分析,第一趟的執行流程如下圖所示:
4.3 復雜度分析
1. 時間復雜度:
最壞情況就是每一次取到的元素就是數組中最小/最大的,這種情況其實就是冒泡排序了(每一次都排好一個元素的順序)
這種情況時間復雜度就好計算了,就是冒泡排序的時間復雜度:T[n] = n * (n-1) = n^2 + n;
最好情況下是O(nlog2n),推導過程如下:
(遞歸算法的時間復雜度公式:T[n] = aT[n/b] + f(n) )
所以平均時間復雜度為O(nlog2n)
2. 空間復雜度:
4.4 Java 代碼如下
import java.util.Arrays; public class quick{ public static void main(String[] args) { int[] arr = new int[]{10,6,3,8,33,27,66,9,7,88}; f(arr,0,arr.length-1); System.out.println(Arrays.toString(arr)); } public static void f(int[] arr,int start,int end){ //直到start=end時結束遞歸
if(start<end){ int left = start; int right = end; int temp = arr[start]; while(left<right){ //右面的數字大於標准數時,右邊的數的位置不變,指針向左移一個位置
while(left<right && arr[right]>temp){ right--; } //右邊的數字小於或等於基本數,將右邊的數放到左邊
arr[left] = arr[right]; left++; ////左邊的數字小於或等於標准數時,左邊的數的位置不變,指針向右移一個位置
while(left<right && arr[left]<=temp){ left++; } //左邊的數字大於基本數,將左邊的數放到右邊
arr[right] = arr[left]; } //一趟循環結束,此時left=right,將基數放到這個重合的位置,
arr[left] = temp; System.out.println(Arrays.toString(arr)); //將數組從left位置分為兩半,繼續遞歸下去進行排序
f(arr,start,left); f(arr,left+1,end); } } }
5. 歸並排序
5.1 動圖演示
5.2 思路分析
歸並排序就是遞歸得將原始數組遞歸對半分隔,直到不能再分(只剩下一個元素)后,開始從最小的數組向上歸並排序
1. 向上歸並排序的時候,需要一個暫存數組用來排序,
2. 將待合並的兩個數組,從第一位開始比較,小的放到暫存數組,指針向后移,
3. 直到一個數組空,這時,不用判斷哪個數組空了,直接將兩個數組剩下的元素追加到暫存數組里,
4. 再將暫存數組排序后的元素放到原數組里,兩個數組合成一個,這一趟結束。
根據思路分析,每一趟的執行流程如下圖所示:
5.3 復雜度分析
1. 時間復雜度:遞歸算法的時間復雜度公式:T[n] = aT[n/b] + f(n)
無論原始數組是否是有序的,都要遞歸分隔並向上歸並排序,所以時間復雜度始終是O(nlog2n)
2. 空間復雜度:
每次兩個數組進行歸並排序的時候,都會利用一個長度為n的數組作為輔助數組用於保存合並序列,所以空間復雜度為O(n)
5.4 Java 代碼如下
import java.util.Arrays; public class Main { public static void main(String[] args) { int[] arr = new int[]{3,6,4,7,5,2}; merge(arr,0,arr.length-1); System.out.println(Arrays.toString(arr)); } //歸並
public static void merge(int[] arr,int low,int high){ int center = (high+low)/2; if(low<high){ //遞歸,直到low==high,也就是數組已不能再分了,
merge(arr,low,center); merge(arr,center+1,high); //當數組不能再分,開始歸並排序
mergeSort(arr,low,center,high); System.out.println(Arrays.toString(arr)); } } //排序
public static void mergeSort(int[] arr,int low,int center,int high){ //用於暫存排序后的數組的臨時數組
int[] tempArr = new int[arr.length]; int i = low,j = center+1; //臨時數組的下標
int index = 0; //循環遍歷兩個數組的數字,將小的插入到臨時數組里
while(i<=center && j<= high){ //左邊數組的數小,插入到新數組
if(arr[i]<arr[j]){ tempArr[index] = arr[i]; i++; }else{//右邊數組的數小,插入到新數組
tempArr[index] = arr[j]; j++; } index++; } //處理左半邊數組多余的數據,將左半邊多余的數據直接追加的臨時數組的后面
while(i<=center){ tempArr[index] = arr[i]; i++; index++; } //處理右半邊數組多余的數據,將右半邊多余的數據直接追加的臨時數組的后面
while(j<= high){ tempArr[index] = arr[j]; j++; index++; } //將臨時數組中的數據重新放進原數組
for (int k = 0; k < index; k++) { arr[k+low] = tempArr[k]; } } }
6. 基數排序
6.1 動圖演示
6.2 思路分析
基數排序第i趟將待排數組里的每個數的i位數放到tempj(j=1-10)隊列中,然后再從這十個隊列中取出數據,重新放到原數組里,直到i大於待排數的最大位數。
1.數組里的數最大位數是n位,就需要排n趟,例如數組里最大的數是3位數,則需要排3趟。
2.若數組里共有m個數,則需要十個長度為m的數組tempj(j=0-9)用來暫存i位上數為j的數,例如,第1趟,各位數為0的會被分配到temp0數組里,各位數為1的會被分配到temp1數組里......
3.分配結束后,再依次從tempj數組中取出數據,遵循先進先進原則,例如對數組{1,11,2,44,4},進行第1趟分配后,temp1={1,11},temp2={2},temp4={44,4},依次取出元素后{1,11,2,44,4},第一趟結束
4.循環到n趟后結束,排序完成
根據思路分析,每一趟的執行流程如下圖所示:
通過基數排序對數組{53, 3, 542, 748, 14, 214, 154, 63, 616}:
6.3 復雜度分析
1. 時間復雜度:
每一次關鍵字的桶分配都需要O(n)的時間復雜度,而且分配之后得到新的關鍵字序列又需要O(n)的時間復雜度。
假如待排數據可以分為d個關鍵字,則基數排序的時間復雜度將是O(d*2n) ,當然d要遠遠小於n,因此基本上還是線性級別的。
系數2可以省略,且無論數組是否有序,都需要從個位排到最大位數,所以時間復雜度始終為O(d*n) 。其中,n是數組長度,d是最大位數。
2. 空間復雜度:
基數排序的空間復雜度為O(n+k),其中k為桶的數量,需要分配n個數。
6.4 Java 代碼如下
import java.util.Arrays; public class Main { public static void main(String[] args) { int[] arr = new int[]{10,6,3,8,33,27,66,9,7,88}; radixSort(arr); } private static void radixSort(int[] arr) { //求出待排數的最大數
int maxLength=0; for (int i = 0; i < arr.length; i++) { if(maxLength<arr[i]) maxLength = arr[i]; } //根據最大數求最大長度
maxLength = (maxLength+"").length(); //用於暫存數據的數組
int[][] temp = new int[10][arr.length]; //用於記錄temp數組中每個桶內存的數據的數量
int[] counts = new int[10]; //用於記錄每個數的i位數
int num = 0; //用於取的元素需要放的位置
int index = 0; //根據最大長度決定排序的次數
for (int i = 0,n=1; i < maxLength; i++,n*=10) { for (int j = 0; j < arr.length; j++) { num = arr[j]/n%10; temp[num][counts[num]] = arr[j]; counts[num]++; } //從temp中取元素重新放到arr數組中
for (int j = 0; j < counts.length; j++) { for (int j2 = 0; j2 < counts[j]; j2++) { arr[index] = temp[j][j2]; index++; } counts[j]=0; } index=0; } System.out.println(Arrays.toString(arr)); } }
7. 希爾(shell)排序
7.1 動圖演示
7.2 思路分析
希爾排序是把記錄按下標的一定增量分組,對每組使用直接插入排序算法排序;隨着增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至1時,整個文件恰被分成一組,算法便終止。
簡單插入排序很循規蹈矩,不管數組分布是怎么樣的,依然一步一步的對元素進行比較,移動,插入,比如[5,4,3,2,1,0]這種倒序序列,數組末端的0要回到首位置很是費勁,比較和移動元素均需n-1次。
而希爾排序在數組中采用跳躍式分組的策略,通過某個增量將數組元素划分為若干組,然后分組進行插入排序,隨后逐步縮小增量,繼續按組進行插入排序操作,直至增量為1。希爾排序通過這種策略使得整個數組在初始階段達到從宏觀上看基本有序,小的基本在前,大的基本在后。然后縮小增量,到增量為1時,其實多數情況下只需微調即可,不會涉及過多的數據移動。
來看下希爾排序的基本步驟,在此選擇增量gap=length/2,縮小增量繼續以gap = gap/2的方式,這種增量選擇可以用一個序列來表示,{n/2,(n/2)/2...1},稱為增量序列。希爾排序的增量序列的選擇與證明是個數學難題,選擇的這個增量序列是比較常用的,也是希爾建議的增量,稱為希爾增量,但其實這個增量序列不是最優的。此處做示例使用希爾增量。
7.3 復雜度分析
1. 時間復雜度:最壞情況下,每兩個數都要比較並交換一次,則最壞情況下的時間復雜度為O(n2), 最好情況下,數組是有序的,不需要交換,只需要比較,則最好情況下的時間復雜度為O(n)。
經大量人研究,希爾排序的平均時間復雜度為O(n1.3)(這個我也不知道咋來的,書上和博客上都這樣說,也沒找到個具體的依據,,,)。
2. 空間復雜度:希爾排序,只需要一個變量用於兩數交換,與n的大小無關,所以空間復雜度為:O(1)。
7.4 Java 代碼如下
import java.util.Arrays; public class shell { public static void main(String[] args) { int[] arr = new int[]{10,6,3,8,33,27,66,9,7,88}; shellSort(arr); System.out.println(Arrays.toString(arr)); } private static void shellSort(int[] arr) { int temp; //控制增量序列,增量序列為1的時候為最后一趟
for (int i = arr.length/2; i >0; i/=2) { //根據增量序列,找到每組比較序列的最后一個數的位置
for (int j = i; j < arr.length; j++) { //根據該比較序列的最后一個數的位置,依次向前執行插入排序
for (int k = j-i; k >=0; k-=i) { if(arr[k]>arr[k+i]){ temp = arr[k]; arr[k] = arr[k+i]; arr[k+i] = temp; } } } } } }
8. 堆排序
8.1 動圖演示
8.2 思路分析
先來了解下堆的相關概念:堆是具有以下性質的完全二叉樹:每個結點的值都大於或等於其左右孩子結點的值,稱為大頂堆;或者每個結點的值都小於或等於其左右孩子結點的值,稱為小頂堆。如下圖:
同時,我們對堆中的結點按層進行編號,將這種邏輯結構映射到數組中就是下面這個樣子
該數組從邏輯上講就是一個堆結構,我們用簡單的公式來描述一下堆的定義就是:
大頂堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小頂堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
了解了這些定義。接下來看看堆排序的基本思想及基本步驟:
堆排序基本思想及步驟
堆排序的基本思想是:將待排序序列構造成一個大頂堆,此時,整個序列的最大值就是堆頂的根節點。將其與末尾元素進行交換,此時末尾就為最大值。然后將剩余n-1個元素重新構造成一個堆,這樣會得到n個元素的次小值。如此反復執行,便能得到一個有序序列了
步驟一 構造初始堆。將給定無序序列構造成一個大頂堆(一般升序采用大頂堆,降序采用小頂堆)。
a.假設給定無序序列結構如下
2.此時我們從最后一個非葉子結點開始(葉結點自然不用調整,第一個非葉子結點 arr.length/2-1=5/2-1=1,也就是下面的6結點),從左至右,從下至上進行調整。
4.找到第二個非葉節點4,由於[4,9,8]中9元素最大,4和9交換。
這時,交換導致了子根[4,5,6]結構混亂,繼續調整,[4,5,6]中6最大,交換4和6。
此時,我們就將一個無需序列構造成了一個大頂堆。
步驟二 將堆頂元素與末尾元素進行交換,使末尾元素最大。然后繼續調整堆,再將堆頂元素與末尾元素交換,得到第二大元素。如此反復進行交換、重建、交換。
a.將堆頂元素9和末尾元素4進行交換
b.重新調整結構,使其繼續滿足堆定義
c.再將堆頂元素8與末尾元素5進行交換,得到第二大元素8.
后續過程,繼續進行調整,交換,如此反復進行,最終使得整個序列有序
再簡單總結下堆排序的基本思路:
a.將無序序列構建成一個堆,根據升序降序需求選擇大頂堆或小頂堆;
b.將堆頂元素與末尾元素交換,將最大元素"沉"到數組末端;
c.重新調整結構,使其滿足堆定義,然后繼續交換堆頂元素與當前末尾元素,反復執行調整+交換步驟,直到整個序列有序。
8.3 復雜度分析
1. 時間復雜度:堆排序是一種選擇排序,整體主要由構建初始堆+交換堆頂元素和末尾元素並重建堆兩部分組成。其中構建初始堆經推導復雜度為O(n),在交換並重建堆的過程中,需交換n-1次,而重建堆的過程中,根據完全二叉樹的性質,[log2(n-1),log2(n-2)...1]逐步遞減,近似為nlogn。所以堆排序時間復雜度最好和最壞情況下都是O(nlogn)級。
2. 空間復雜度:堆排序不要任何輔助數組,只需要一個輔助變量,所占空間是常數與n無關,所以空間復雜度為O(1)。
8.4 Java 代碼如下
import java.util.Arrays; public class duipaixu { public static void main(String[] args) { int[] arr = new int[]{4,6,8,5,9}; int length = arr.length; //從最后一個非葉節點開始構建大頂堆 for (int i = arr.length/2-1; i >=0; i--) { maximumHeap(i,arr,length); } //從最小的葉子節點開始與根節點進行交換並重新構建大頂堆 for (int i = arr.length-1; i >=0; i--) { // System.out.println(Arrays.toString(arr)); swap(arr,0,i); length--; maximumHeap(0,arr,length); } System.out.println(Arrays.toString(arr)); } //構建大頂堆 public static void maximumHeap(int i,int[] arr,int length){ int temp = arr[i]; for (int j = i*2+1; j < length; j=j*2+1) { //如果右孩子大於做孩子,則指向右孩子 if(j+1<length && arr[j+1]>arr[j]){ j++; } //如果最大的孩子大於當前節點,則將大孩子賦給當前節點,修改當前節點為其大孩子節點,再向下走。 if(arr[j]>temp){ arr[i] = arr[j]; i = j; }else{ break; } } //將temp放到最終位置 arr[i] = temp; } //交換 public static void swap(int[] arr,int i,int j){ int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } }