八大排序算法詳解(動圖演示 思路分析 實例代碼java 複雜度分析 適用場景)

1、分類html

1.內部排序和外部排序 java

  • 內部排序:待排序記錄存放在計算機隨機存儲器中(說簡單點,就是內存)進行的排序過程。
  • 外部排序:待排序記錄的數量很大,以至於內存不能一次容納所有記錄,因此在排序過程當中須要對外存進行訪問的排序過程。

2.比較類排序和非比較排序算法

  • 比較類排序:經過比較來決定元素間的相對次序,因爲其時間複雜度不能突破O(nlogn),所以也稱爲非線性時間比較類排序。
  • 非比較類排序:不經過比較來決定元素間的相對次序,它能夠突破基於比較排序的時間下界,以線性時間運行,所以也稱爲線性時間非比較類排序。 

 

 2、複雜度分析,算法穩定性和適用場景shell

  • 穩定:若是a本來在b前面,而a=b,排序以後a仍然在b的前面。
  • 不穩定:若是a本來在b的前面,而a=b,排序以後 a 可能會出如今 b 的後面。
  • 時間複雜度:對排序數據的總的操做次數。反映當n變化時,操做次數呈現什麼規律。
  • 空間複雜度:是指算法在計算機內執行時所需存儲空間的度量,它也是數據規模n的函數。 

 

3、八大排序算法詳解數組

1.選擇排序函數

1.1 動圖演示ui

 

 

1.2 思路分析spa

1.  第一個跟後面的全部數相比,若是小於(或小於)第一個數的時候,暫存較小數的下標,第一趟結束後,將第一個數,與暫存的那個最小數進行交換,第一個數就是最小(或最大的數)3d

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.  空間複雜度:

  快速排序使用的空間是O(1)的,也就是個常數級;而真正消耗空間的就是遞歸調用了,由於每次遞歸就要保持一些數據:
  最優的狀況下空間複雜度爲:O(log2n);每一次都平分數組的狀況
  最差的狀況下空間複雜度爲:O( n );退化爲冒泡排序的狀況
因此平均空間複雜度爲O(log2n)

 

 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 Main { public static void main(String[] args) { int[] arr = new int[]{4,6,8,5,9}; //從最後一個非葉節點開始構建大頂堆 for (int i = arr.length/2-1; i >=0; i--) { maximumHeap(i,arr); } //從最小的葉子節點開始與根節點進行交換並從新構建大頂堆 for (int i = arr.length-1; i >=0; i--) { swap(arr,0,i); maximumHeap(0,arr); } System.out.println(Arrays.toString(arr)); } //構建大頂堆 public static void maximumHeap(int i,int[] arr){ int temp = arr[i]; for (int j = i*2+1; j < arr.length; j=j*2+1) { //若是右孩子大於作孩子,則指向右孩子 if(j+1<arr.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; } }

 

參考:https://www.cnblogs.com/onepixel/articles/7674659.html

參考:http://www.javashuo.com/article/p-wtyhbkog-dg.html

相關文章
相關標籤/搜索