一、冒泡排序
基本思想:兩個數比較大小,較大的數下沉,較小的數冒起來。
過程:
①、比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
②、對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最后一對。這步做完后,最后的元素會是最大的數(也就是第一波冒泡完成)。
③、針對所有的元素重復以上的步驟,除了最后一個。
④、持續每次對越來越少的元素重復上面的步驟,直到沒有任何一對數字需要比較。
平均時間復雜度:O(n2)
代碼實現:
public class BubbleSort {
public static int[] sort(int[] array){
int temp;//臨時變量
boolean flag;//是否交換的標識
//這里for循環表示總共需要比較多少輪
for(int i=1;i<array.length;i++){
flag = false;
//j的范圍很關鍵,這個范圍是在逐步縮小的,因為每輪比較都會將最大的放在右邊
for(int j=0;j<array.length-i;j++){
if(array[j]>array[j+1]){
temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
flag = true;
}
}
//如果flag為false,表示此次循環沒有數據交換,退出循環
if(!flag){
break;
}
}
return array;
}
}
二、選擇排序
基本思想:選擇排序是每一次從待排序的數據元素中選出最小的一個元素,存放在序列的起始位置,直到全部待排序的數據元素排完。
過程:
①、從待排序序列中,找到關鍵字最小的元素
②、如果最小元素不是待排序序列的第一個元素,將其和第一個元素互換
③、從余下的 N - 1 個元素中,找出關鍵字最小的元素,重復(1)、(2)步,直到排序結束
平均時間復雜度:O(n2)
代碼實現:
public class ChoiceSort {
public static int[] sort(int[] array){
//總共要經過N-1輪比較
for(int i=0;i<array.length-1;i++){
int minIndex = i;
//每輪要比較的次數
for(int j=i+1;j<array.length;j++){
if(array[j]<array[minIndex]){
minIndex = j;//記錄目前能找到的最小值元素的下標
}
}
//將找到的最小值和i位置所在的值進行位置交換
if(minIndex != i){
int temp = array[minIndex];
array[minIndex] = array[i];
array[i] = temp;
}
}
return array;
}
}
三、插入排序
基本思想:直接插入排序基本思想是每一步將一個待排序的記錄,插入到前面已經排好序的有序序列中去,直到插完所有元素為止。
過程:
平均時間復雜度:O(n2)
代碼實現:
public class InsertSort {
public static int[] sort(int[] array){
int j;
//從下標為1的元素開始開始選擇合適的位置插入,因為下標為0的只有一個元素,默認是有序的
for(int i=1;i<array.length;i++){
int temp =array[i];//記錄要插入的數據
j = i;
while (j>0 && temp<array[j-1]){
array[j] = array[j-1];//向后挪動
j--;
}
array[j] = temp;//插入此數
}
return array;
}
}
四、希爾排序
基本思想:在要排序的一組數中,根據某一增量分為若干子序列,并對子序列分別進行插入排序。
然后逐漸將增量減小,并重復上述過程。直至增量為1,此時數據序列基本有序,最后進行插入排序。
過程:
當我們完成4-增量排序之后,在進行普通的插入排序,即1-增量排序,會比前面直接執行簡單插入排序要快很多。
平均時間復雜度:O(n1.3)
代碼實現:
//希爾排序 間隔為2h
public class ShellSort {
public static int[] sort(int[] array){
int step;
int length = array.length;
for(step = length/2;step>0;step=step/2){
//分別對每個增量間隔進行排序
for (int i=step;i<array.length;i++){
int j=i;
int temp = array[j];
while (j-step>=0 && temp<array[j-step]){
array[j] = array[j-step];
j -= step;
}
array[j] = temp;
}
}
return array;
}
}
五、快速排序
基本思想:(分治)
①、先從數列中取出一個數作為key值;
②、將比這個數小的數全部放在它的左邊,大于或等于它的數全部放在它的右邊;
③、對左右兩個小數列重復第二步,直至各區間只有1個數。
過程:
上面表示的是一個無序數組,選取第一個元素 6 作為基準元素。左游標是 i 哨兵,右游標是 j 哨兵。然后左游標向左移動,右游標向右移動,它們遵循的規則如下:
一、左游標向右掃描, 跨過所有小于基準元素的數組元素, 直到遇到一個大于或等于基準元素的數組元素, 在那個位置停下。
二、右游標向左掃描, 跨過所有大于基準元素的數組元素, 直到遇到一個小于或等于基準元素的數組元素,在那個位置停下。
第一步:哨兵 j 先開始出動。因為此處設置的基準數是最左邊的數,所以需要讓哨兵 j 先開始出動,哨兵 j 一步一步的向左挪動,直到找到一個小于 6 的元素停下來。接下來,哨兵 i 再一步一步的向右挪動,直到找到一個大于 6 的元素停下來。最后哨兵 i 停在了數字 7 面前,哨兵 j 停在了數字 5 面前。
到此,第一次交換結束,接著哨兵 j 繼續向左移動,它發現 4 比基準數 6 要小,那么在數字4面前停下來。哨兵 i 也接著向右移動,然后在數字 9 面前停下來,然后哨兵 i 和 哨兵 j 再次進行交換。
第二次交換結束,哨兵 j 繼續向左移動,然后在數字 3 面前停下來;哨兵 i 繼續向右移動,但是它發現和哨兵 j 相遇了。那么此時說明探測結束,將數字 3 和基準數字 6 進行交換,如下:
到此,第一次探測真正結束,此時已基準點 6 為分界線,6 左邊的數組元素都小于等于6,6右邊的數組元素都大于等于6。
左邊序列為【3,1,2,5,4】,右邊序列為【9,7,10,8】。接著對于左邊序列而言,以數字 3 為基準元素,重復上面的探測操作,探測完畢之后的序列為【2,1,3,5,4】;對于右邊序列而言,以數字 9 位基準元素,也重復上面的探測操作。然后一步一步的劃分,最后排序完全結束。
平均時間復雜度:O(N*logN)
代碼實現:
public class QuickSort {
public static void sort(int[] array,int left,int right){
//區間只有1個數,就退出
if(left>=right){
return;
}
int i = left,j = right,key = array[left];//key是基準數
while (i<j){
//順序很重要,要先從右邊開始找
while (i<j && array[j]>=key){
j--;
}
while (i<j && array[i]<=key){
i++;
}
if(i<j){//交換兩個數在數組中的位置
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
//最終將基準數歸位
array[left] = array[i];
array[i] = key;
sort(array,left,i-1);//遞歸調用
sort(array,i+1,right);
}
public static void main(String[] args) {
int[] array = {2,3,1,3,1,2,3,4,5,1,2,-4,99};
sort(array,0,array.length-1);
System.out.println(Arrays.toString(array));
}
}
六、歸并排序
基本思想:歸并排序是建立在歸并操作上的一種有效的排序算法。該算法是采用分治法的一個非常典型的應用。歸并算法的中心是歸并兩個已經有序的數組。歸并兩個有序數組A和B,就生成了第三個有序數組C。數組C包含數組A和B的所有數據項。
過程:比較a[i]和b[j]的大小,若a[i]≤b[j],則將第一個有序表中的元素a[i]復制到r[k]中,并令i和k分別加上1;否則將第二個有序表中的元素b[j]復制到r[k]中,并令j和k分別加上1,如此循環下去,直到其中一個有序表取完,然后再將另一個有序表中剩余的元素復制到r中從下標k到下標t的單元。歸并排序的算法我們通常用遞歸實現,先把待排序區間[s,t]以中點二分,接著把左邊子區間排序,再把右邊子區間排序,最后把左區間和右區間用一次歸并操作合并成有序的區間[s,t]。
平均時間復雜度:O(N*logN)
代碼實現:
public class MergeSort {
public static int[] sort(int[] array,int start,int last){
if(last > start){
int mid = (start+last)/2;
sort(array,start,mid);//左邊數組排序
sort(array,mid+1,last);//右邊數組排序
merge(array,start,mid,last);//合并左右數組
}
return array;
}
public static void merge(int[] array,int start,int mid,int last){
int[] temp = new int[last-start+1];//定義臨時數組
int i = start;//定義左邊數組的下標
int j = mid + 1;//定義右邊數組的下標
int k = 0;
while(i <= mid && j <= last){
if(array[i] < array[j]){
temp[k++] = array[i++];
}else{
temp[k++] = array[j++];
}
}
//把左邊剩余數組元素移入新數組中
while(i <= mid){
temp[k++] = array[i++];
}
//把右邊剩余數組元素移入到新數組中
while(j <= last){
temp[k++] = array[j++];
}
//把新數組中的數覆蓋到array數組中
for(int m = 0 ; m < temp.length ; m++){
array[m+start] = temp[m];
}
}
public static void main(String[] args) {
int[] array = {2,3,1,3,1,2,3,4,5,1,2,-4,99};
sort(array,0,array.length-1);
System.out.println(Arrays.toString(array));
}
}
七、堆排序
基本思想: 堆排序是指利用堆這種數據結構所設計的一種排序算法。
過程:
①將待排序的序列構造成一個大頂堆,根據大頂堆的性質,當前堆的根節點(堆頂)就是序列中最大的元素。
②將堆頂元素和最后一個元素交換,然后將剩下的節點重新構造成一個大頂堆。
③重復步驟②,如此反復,從第一次構建大頂堆開始,每一次構建,我們都能獲得一個序列的最大值,然后把它放到大頂堆的尾部。最后,就得到一個有序的序列了。
假設給定的無序序列arr是:
4 5 8 2 3 9 7 1
1、將無序序列構建成一個大頂堆。
首先我們將現在的無序序列看成一個堆結構,一個沒有規則的二叉樹,將序列里的值按照從上往下,從左到右依次填充到二叉樹中。
根據大頂堆的性質,每個節點的值都大于或者等于它的左右子節點的值。所以我們需要找到所有包含子節點的節點,也就是非葉子節點,然后調整他們的父子關系,非葉子節點遍歷的順序應該是從下往上,這比從上往下的順序遍歷次數少很多,因為,大頂堆的性質要求父節點的值要大于或者等于子節點的值,如果從上往下遍歷,當某個節點即是父節點又是子節點并且它的子節點仍然有子節點的時候,因為子節點還沒有遍歷到,所以子節點不符合大頂堆性質,當子節點調整后,必然會影響其父節點需要二次調整。但是從下往上的方式不需要考慮父節點,因為當前節點調整完之后,當前節點必然比它的所有子節點都大,所以,只會影響到子節點二次調整。相比之下,從下往上的遍歷方式比從上往下的方式少了父節點的二次調整。
那么,該如何知道最后一個非葉子節點的位置,也就是索引值?
對于一個完全二叉樹,在填滿的情況下(非葉子節點都有兩個子節點),每一層的元素個數是上一層的二倍,根節點數量是1,所以最后一層的節點數量,一定是之前所有層節點總數+1,所以,我們能找到最后一層的第一個節點的索引,即節點總數/2(根節點索引為0),這也就是第一個葉子節點,所以第一個非葉子節點的索引就是第一個葉子結點的索引-1。那么對于填不滿的二叉樹呢?這個計算方式仍然適用,當我們從上往下,從左往右填充二叉樹的過程中,第一個葉子節點,一定是序列長度/2,所以第一個非葉子節點的索引就是arr.length / 2 -1。
現在找到了最后一個非葉子節點,即元素值為2的節點,比較它的左右節點的值,是否比他大,如果大就換位置。這里因為1<2,所以,不需要任何操作,繼續比較下一個,即元素值為8的節點,它的左節點值為9比它本身大,所以需要交換
交換后的序列為:
4 5 9 2 3 8 7 1
因為元素8沒有子節點,所以繼續比較下一個非葉子節點,元素值為5的節點,它的兩個子節點值都比本身小,不需要調整;然后是元素值為4的節點,也就是根節點,因為9>4,所以需要調整位置
交換后的序列為:
9 5 4 2 3 8 7 1
此時,原來元素值為9的節點值變成4了,而且它本身有兩個子節點,所以,這時需要再次調整該節點
交換后的序列為:
9 5 8 2 3 4 7 1
到此,大頂堆就構建完畢了。滿足大頂堆的性質。
2、排序序列,將堆頂的元素值和尾部的元素交換
交換后的序列為:
1 5 8 2 3 4 7 9
然后將剩余的元素重新構建大頂堆,其實就是調整根節點以及其調整后影響的子節點,因為其他節點之前已經滿足大頂堆性質。
交換后的序列為:
8 5 7 2 3 4 1 9
然后,繼續交換,堆頂節點元素值為8與當前尾部節點元素值為1的進行交換
一直重復此步驟
參考鏈接:https://blog.csdn.net/qq_28063811/article/details/93034625
平均時間復雜度:O(N*logN)
代碼實現:
public class HeapSort {
private void heapSort(int[] arr){
//1、構建大根堆 從第一個非葉子節點開始
for(int i=arr.length/2-1;i>=0;i--){
adjustHeap(arr,i,arr.length);
}
//2、交換第一個節點和最后一個節點值,繼續下沉調整
for(int i=arr.length-1;i>0;i--){
swap(arr,0,i);
adjustHeap(arr,0,i);
}
}
private void adjustHeap(int[] arr,int index,int length){
int maxIndex = index;//記錄最大值下標
int leftIndex = 2*index+1;//左子節點下標
int rightIndex = 2*index+2;//右子節點下標
//1、如果存在左子節點并且比父節點值大
if(leftIndex<length && arr[maxIndex]<arr[leftIndex]){
maxIndex = leftIndex;
}
//2、如果存在右子節點并且比父節點值大
if(rightIndex<length && arr[maxIndex]<arr[rightIndex]){
maxIndex = rightIndex;
}
//3、如果交換了值,那么繼續向下比較
if(maxIndex != index){
swap(arr,index,maxIndex);
adjustHeap(arr,maxIndex,length);
}
}
private void swap(int[] arr,int i,int j){
int value = arr[i];
arr[i] = arr[j];
arr[j] = value;
}
}
八、桶排序
桶排序需要兩個輔助空間:
- 第一個輔助空間,是桶空間B
- 第二個輔助空間,是桶內的元素鏈表空間
總的來說,空間復雜度是O(n)。
桶排序有兩個關鍵步驟:
- 掃描待排序數據arr[N],對于元素arr[i],放入對應的桶X
- arr[i]放入桶X,如果桶X已經有了若干元素,使用插入排序,將arr[i]放到桶內合適的位置
畫外音:
(1)桶X內的所有元素,是一直有序的;
(2)插入排序是穩定的,因此桶內元素順序也是穩定的;
當arr[N]中的所有元素,都按照上述步驟放入對應的桶后,就完成了全量的排序。
桶排序的偽代碼是:
bucketSort(arr[N]){
for i =1 to n{
將arr[i]放入對應的桶B[X];
使用插入排序,將arr[i]插入到B[X]中正確的位置;
}
將B[X]中的所有元素,按順序合并,排序完畢;
}
舉個例子:
假設待排序的數組均勻分布在[0, 99]之間:
{5,18,27,33,42,66,90,8,81,47,13,67,9,36,62,22}
可以設定10個桶,申請額外的空間bucket[10]來作為輔助空間。其中,每個桶bucket[i]來存放[10i, 10i+9]的元素鏈表。
上圖所示:
- 待排序的數組為arr[16]
- 桶空間是buket[10]
- 掃描所有元素之后,元素被放到了自己對應的桶里
- 每個桶內,使用插入排序,保證一直是有序的
例如,標紅的元素66, 67, 62最終會在一個桶里,并且使用插入排序桶內保持有序。
最終,每個桶按照次序輸出,排序完畢。
桶排序(Bucket Sort),總結:
- 桶排序,是一種復雜度為O(n)的排序
- 桶排序,是一種穩定的排序
- 桶排序,適用于數據均勻分布在一個區間內的場景
參考鏈接:來自微信公眾號58沈劍 架構師之路
九、基數排序
舉個例子:
假設待排序的數組arr={72, 11, 82, 32, 44, 13, 17, 95, 54, 28, 79, 56}
基數排序的兩個關鍵要點:
(1)基:被排序的元素的“個位”“十位”“百位”,暫且叫“基”,栗子中“基”的個數是2(個位和十位);
(2)桶:“基”的每一位,都有一個取值范圍,栗子中“基”的取值范圍是0-9共10種,所以需要10個桶(bucket),來存放被排序的元素;
基數排序的算法步驟為:
for (每一個基) {
//循環內,以某一個“基”為依據
第一步:遍歷數據集arr,將元素放入對應的桶bucket
第二步:遍歷桶bucket,將元素放回數據集arr
}
更具體的,對應到上面的例子,“基”有個位和十位,所以,for循環會執行兩次。
【第一次:以“個位”為依據】
畫外音:上圖中標紅的部分,個位為“基”。
第一步:遍歷數據集arr,將元素放入對應的桶bucket;
操作完成之后,各個桶會變成上面這個樣子,即:個位數相同的元素,會在同一個桶里。
第二步:遍歷桶bucket,將元素放回數據集arr;
畫外音:需要注意,先入桶的元素要先出桶。
操作完成之后,數據集會變成上面這個樣子,即:整體按照個位數排序了。
畫外音:個位數小的在前面,個位數大的在后面。
【第二次:以“十位”為依據】
畫外音:上圖中標紅的部分,十位為“基”。
第一步:依然遍歷數據集arr,將元素放入對應的桶bucket;
操作完成之后,各個桶會變成上面這個樣子,即:十位數相同的元素,會在同一個桶里。
第二步:依然遍歷桶bucket,將元素放回數據集arr;
操作完成之后,數據集會變成上面這個樣子,即:整體按照十位數也排序了。
畫外音:十位數小的在前面,十位數大的在后面。
首次按照個位從小到大,第二次按照十位從小到大,即:排序結束。
幾個小點:
(1)基的選取,可以先從個位開始,也可以先從十位開始,結果是一樣的;
(2)基數排序,是一種穩定的排序;
(3)時間復雜度,可以認為是線性的O(n);
十、計數排序
計數排序的適用范圍?
待排序的元素在某一個范圍[MIN, MAX]之間。
畫外音:很多業務場景是符合這一場景,例如uint32的數字排序位于[0, 2^32]之間。
計數排序的空間復雜度?
計數排序需要一個輔助空間,空間大小為O(MAX-MIN),用來存儲所有元素出現次數(“計數”)。
畫外音:計數排序的核心是,空間換時間。
計數排序的關鍵步驟?
步驟一:掃描待排序數據arr[N],使用計數數組counting[MAX-MIN],對每一個arr[N]中出現的元素進行計數;
步驟二:掃描計數數組counting[],還原arr[N],排序結束;
舉個例子:
假設待排序的數組,
arr={5, 3, 7, 1, 8, 2, 9, 4, 7, 2, 6, 6, 2, 6, 6}
很容易發現,待排序的元素在[0, 10]之間,可以用counting[0,10]來存儲計數。
第一步:統計計數
掃描未排序的數組arr[N],對每一個出現的元素進行計數。
掃描完畢后,計數數組counting[0, 10]會變成上圖中的樣子,如粉色示意,6這個元素在arr[N]中出現了4次,在counting[0, 10]中,下標為6的位置計數是4。
第二步:還原數組
掃描計數數組counting[0, 10],通過每個元素的計數,還原arr[N]。
如上圖粉色示意,count[0, 10]下標為6的位置計數是4,排完序是4個連續的6。
從counting下標MIN到MAX,逐個還原,填滿arr[N]時,排序結束。
計數排序(Counting Sort),總結:
- 計數排序,時間復雜度為O(n);
- 當待排序元素個數很多,但值域范圍很窄時,計數排序是很節省空間的;
穩定:冒泡排序、插入排序、歸并排序、桶排序、基數排序、計數排序
不穩定:選擇排序、快速排序、希爾排序、堆排序