養成習慣,先贊後看!!!
你的點贊與關注真的對我非常有幫助.如果可以的話,動動手指,一鍵三連吧!!!
十大經典排序算法-堆排序,計數排序,桶排序,基數排序
前言
這是十大經典排序算法詳解的最後一篇了.
還沒有看多之前兩篇文章的小夥伴可以先去看看之前的兩篇文章:
十大經典排序算法詳解(一)冒泡排序,選擇排序,插入排序
十大經典排序算法詳解(二)希爾排序,歸併排序,快速排序
這一篇文章真的耗費了我巨大的時間和精力,由於 堆排序是基於二叉樹
的,所以寫的篇幅比較大並且由於是關於樹的,所以畫圖動態演示的工程量就進一步增加,其次就是因爲計數排序,桶排序以及基數排序並不是基於比較的,所以算法的思想講解相對於之前的基於比較的算法而言會稍微難一點.
其次就是這次的調試過程也比之前多了很多需要注意的地方,這些我都會在下面的代碼中通過註釋的方式提醒大家.
最後如果大家覺得我的文章寫得還可以或者覺得文章對你有幫助的話,可以選擇關注我的公衆號:萌萌噠的瓤瓤
,或者你也可以幫忙給文章來個一鍵三連
吧.你的支持對我真的很有用.
1-堆排序
算法思想
:
在介紹算法之前我們首先需要了解一下下面這些概念:什麼是二叉樹,什麼是完全二叉樹,什麼是大根堆,什麼是小根堆.
二叉樹
學過數據結構的小夥伴肯定知道什麼是二叉樹,這部分主要是爲那些可能還不太瞭解數據結構的小夥伴們說的.
二叉樹的定義就是每個結點至多能有兩個子樹,二叉樹是一種最簡單的樹,下面我們舉幾個樹的例子:
我們可以來哦稍微區分一下,很明顯只有4號樹並不是我們所說的二叉樹,因爲它的1號結點下有三棵子樹,所以4號樹並不是我們所說的二叉樹.到這裏,相信大家也已經基本瞭解二叉樹得出概念了,那麼接下來我們再來了解一下另一個概念完全二叉樹.
完全二叉樹
說到完全二叉樹,就應該知道它首先應該滿足二叉樹的一些條件,就比如說每個節點至多隻能有兩個子樹,那麼除了這個條件以外,還需要什麼條件才能稱得上是完全二叉樹呢.
官方是這樣說的: 一棵深度爲k的有n個結點的二叉樹,對樹中的結點按從上至下、從左到右的順序進行編號,如果編號爲i(1≤i≤n)的結點與滿二叉樹中編號爲i的結點在二叉樹中的位置相同,則這棵二叉樹稱爲完全二叉樹。
但是很明顯說的太官方了,我們肯定需要簡化一下概念.其實說白了就是在二叉樹的基礎上,所有的節點順序必須要按照下面這樣的順序進行排列,否則就不能說是完全二叉樹
並且每次必須是已經放滿2的冪數之後才能放到下一層,並且必須是從每層最左邊的節點開始添加節點,並且必須是先添加左節點在添加右節點.否則就不能稱爲是完全二叉樹,這裏呢,我們舉幾個反例,大家就知道我說的是什麼意思了:
上面的這兩棵樹就是最明顯的反例,看完這兩棵樹之後,相信大家就能更加了解什麼是完全二叉樹了.
大根堆
大根堆其實很容易理解,大根堆在完全二叉樹的基礎上就一條判定條件就是:每個根節點的值必須大於它的左孩子節點已經右孩子節點的值.滿足這樣條件的二叉樹,我們就稱這個二叉樹是大根堆.當然了只要有一個節點不滿足這樣的情況,那麼就不能稱這是大根堆.
舉個例子,下面這棵二叉樹就是一個大根堆:
舉完正確的例子之後,我們當然也需要來舉幾個反例來幫助我們更好的理解什麼是大根堆:
看完這兩個反例之後相信大家就能更加理解什麼是大根堆了.
小根堆
當然了,理解完什麼是大根堆了之後,大家就能舉一反三的瞭解什麼叫做小根堆了.這裏就不再給大家廢話.
在瞭解完上面的這些概念之後,我們就可以來講解什麼是堆排序了.
堆排序的算法步驟其實很簡單,總共就三步.
-
1.將數組重構成大根堆
-
2.將數組的
隊頭
元素與隊尾
元素交換位置 -
3.對去除了隊尾元素的數組進行重構,再次重構成大根堆
之後重複上述2,3步驟
,直到需要重構成大根堆的數組爲空爲止.
算法的步驟的確簡潔明瞭,其實大家看了也應該已經懂了.
因爲每次重構成大根堆之後,根據大根堆的特性,每個節點的值一定大於左右孩子節點的值,所以很明顯大根堆的根節點
就是二叉樹中值最大的值同時也就是數組中最大的值
.所以重構成大根堆之後交換數組隊頭與隊尾元素的操作就是在將最大的元素進行定位.也就意味着這一輪結束之後,數組中已經確定了一個元素的最終位置了.
算法的思想就是這樣,誰都能說的出來,但是呢,堆排序的難點就是在於我們如何將數組重構成我們大根堆
.這個就是堆排序的難點.
那麼接下來,我們就着重講解一下重構大根堆的過程是怎麼樣的.
首先我們需要明白一點就是我們一開始構建二叉樹的時候遵循的是這樣的原則: 從上往下,從左往右 .但是到了將二叉樹重構成大根堆的時候我們的原則就必須要反過來
了:從下往上,從右往左.這個大家動動小腦瓜應該就能理解了.
顯然我們每次對小子樹進行重構成大根堆的操作時,最後都會使得最大的元素上移,對不對,既然大的元素是在上移的,那麼很顯然我們就應該從下往上開始構建.
既然我們已經知道重構的順序是什麼樣的之後,我們就需要再明白一點,那就是我們應該對哪些元素進行重構的操作呢?上面我們已經說過了,大根堆的一個硬性條件就是每個節點必需要大於它的左右孩子節點,那麼很顯然如果節點本身沒有孩子節點的話,就不需要進行重構了.所以我們需要進行重構的元素必定包含孩子節點.並且結合我們上面所說重構順序基本就可以得出一個結論:重構的元素就是最後一個非葉子節點之前的所有節點,包括該節點本身.
,就比方下面這張圖中紅色圈圈的元素.
之後我們只需要通過循環,依次進行下面的操作:
比較節點及其左右孩子節點
,如果有孩子節點的值大於
該節點,那麼就交換兩者的位置
.
這裏需要大家特別注意!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
這裏需要大家特別注意!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
這裏需要大家特別注意!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
交換兩者的位置之後我們還需要進行一個步驟 重新校驗
特別注意!!!特別注意!!!重複這個過程,直到最後一個元素爲止.
到這裏堆排序的基本思想也就已經基本講解完畢了,接下來就是通過圖來動態的演示一下堆排序的過程,這樣能夠讓大家更好的理解堆排序:
這是第一輪堆排序:
)
第二次堆排序:
)
第三次堆排序:
)
這裏給大家模擬三次,大家應該就差不多懂這個流程了.主要是這圖圖畫起來實在是太麻煩了.能力有限就只畫這三次的了.在下面的代碼裏面,我還會着重講重新校驗
的過程,大家如果這裏還沒理解的,也可以接着往下面看.
算法的基本思想大家應該基本上就能理解了.那麼我們再來稍微聊聊堆排序的一些特點:
-
堆排序是
不穩定
的,這個其實還是比較好理解的.因爲我們在進行小分支的調整時時有序的,但是之後可能會出現小分支擴大到大分支進行重新的調整,那麼這時候就有可能會出現元素的相對位置混亂的情況.這個混亂的過程其實有點像我們之前所說的
希爾排序
,希爾排序也是小區間內的插入排序是有序的,但是一旦擴散到更大的區間進行二次的插入排序時就有可能造成相對位置混亂的情況.說的差不多了,那麼我們接下來還是舉一個例子來幫助大家更好的理解:
通過上面這張圖,相信大家就能更好的理解堆排序爲啥是不穩定
的了. -
堆排序每輪排序都可以確定
一個
元素的最終
位置,這個大家看我上面的演示圖也能看出來.
算法圖解
:
示例代碼
:
這是我寫的第一版的代碼
:
//交換數組中的元素
public static void swap(int[]num ,int i,int j) {
int temp=num[i];
num[i]=num[j];
num[j]=temp;
}
//將待排序的數組構建成大根堆
public static void buildbigheap(int []num,int end) {
//從最後一個非葉子節點開始構建,依照從下往上,從右往左的順序
for(int i=end/2;i>=0;i--) {
int left=2*i+1;
int right=2*i+2;
int big=i;
//判斷小分支那個是大元素
if(left<end&&num[i]<num[left])
// swap(num, i, left);
i=left;
if(right<end&&num[i]<num[right])
// swap(num, i, right);
i=right;
swap(num, i, big);
}
}
public static void main(String[] args) {
int []num ={
7,4,9,3,2,1,8,6,5,10};
long startTime=System.currentTimeMillis();
//第一次構建大根堆
buildbigheap(num, num.length);
for(int j=num.length-1;j>0;j--) {
//交換隊頭已經排序得到的最大元素與隊尾元素
swap(num, 0, j);
System.out.print("第"+(num.length-j)+"次排序: ");
for(int k=0;k<num.length;k++) {
System.out.print(num[k]+" ");
}
System.out.println();
//交換結束之後,大根堆已經內破壞,需要開始重新構建大根堆
buildbigheap(num,j);
}
long endTime=System.currentTimeMillis();
System.out.println("程序運行時間: "+(endTime-startTime)+"ms");
}
一開始我覺得我的代碼是對的,並且運行出來的結果也和我預期的一樣,但是當我自己畫圖以後畫到這張圖的時候我就知道算法還是有BUG的,這個BUG就是每次構建大根堆的時候:我們的確能夠在每次構建大根堆的時候將最大的元素挑選出來,但是,我們在挑選出當前最大的元素之後,我們的大根堆真的還是大根堆嗎,這裏用上面畫的圖,我們就能看出來了:
很明顯這個這一步我們的確已經將最大的元素挑選出來了,但是我們當前的已經不是大根堆了,所以我就在想我到底是哪一步做錯了呢.之後我參考了網上的資料發現,該算法還有一個重點就是:如果我們發現根節點與孩子節點交換順序
之後,我們就需要重新檢查交換之後的孩子節點
以下的所有節點
是否還滿足大根堆的定義,因爲可能我們交換後的孩子節點的值還是比他的孩子節點要小的.就比方上面那張圖裏我們所看到的.所以修改後的代碼主要就是加上了重新校驗
的過程.
修改後的第二版代碼
:
//交換數組中的元素
public static void swap(int[]num ,int i,int j) {
int temp=num[i];
num[i]=num[j];
num[j]=temp;
}
//將待排序的數組構建成大根堆
public static void buildbigheap(int []num,int end) {
//從最後一個非葉子節點開始構建,依照從下往上,從右往左的順序
for(int i=end/2;i>=0;i--) {
adjustnode(i, end, num);
}
}
//調整該節點及其以下的所有節點
public static void adjustnode(int i,int end,int []num) {
int left=2*i+1;
int right=2*i+2;
int big=i;
//判斷小分支那個是大元素
if(left<end&&num[i]<num[left])
i=left;
if(right<end&&num[i]<num[right])
i=right;
if(i!=big) {
//交換順序之後需要繼續校驗
swap(num, i, big);
//重新校驗,防止出現交換之後根節點小於孩子節點的情況
adjustnode(i, end, num);
}
}
public static void main(String[] args) {
int []num ={
5,3,7,1,4,6,2};
long startTime=System.currentTimeMillis();
//第一次構建大根堆
buildbigheap(num, num.length);
for(int j=num.length-1;j>0;j--) {
System.out.print("第"+(num.length-j)+"次排序前: ");
for(int k=0;k<num.length;k++) {
System.out.print(num[k]+" ");
}
//交換隊頭已經排序得到的最大元素與隊尾元素
swap(num, 0, j);
System.out.print("第"+(num.length-j)+"次排序後: ");
for(int k=0;k<num.length;k++) {
System.out.print(num[k]+" ");
}
System.out.println();
//交換結束之後,大根堆已經被破壞,需要開始重新構建大根堆
buildbigheap(num,j);
}
long endTime=System.currentTimeMillis();
System.out.println("程序運行時間: "+(endTime-startTime)+"ms");
}
這裏我們將這兩個排序結果對比一下,大家就更加能瞭解重新校驗步驟的重要性了.
相信經過我這樣的講解之後,大家一定能夠更好的理解堆排序了.
複雜度分析
:
理解完堆排序的基本思想之後,我們就需要來分析一下他的時間複雜度,空間複雜度.
-
時間複雜度
堆排序的本質思想也是利用了二叉樹的特性,所以根據他的遍歷次數以及二叉樹的層數可以得到堆排序的時間複雜度爲
O(N*logn)
,不僅僅是平情況是這樣最好與最壞的情況都是如此. -
空間複雜度
這個我們可以看到我們整個排序的過程中只增加一個存儲交換元素的
temp
,所以堆排序的空間複雜是常量級別的僅爲O(1)
.
2-計數排序
算法思想
:
計數排序最核心的思想就是計數序列中每個元素出現的次數
,我們將每個元素的數量都記錄下來之後.我們就可以通過按
瞭解完計數排序的基本思想之後,我們就來看看看這個算法的實現步驟又是怎麼樣的呢?主要就是下面這幾個步驟:
-
1.第一次遍歷序列,找出序列中的最大值以及最小值,然後根據
最大值MAX
與最小值MIN
創建一個MAX-MIN+1長度
的數組.爲什麼創建這樣長度的數組呢,因爲只有創建了這樣長度的數組,MIN-MAX區間內的每個元素纔有對應的位置進行存放,如下圖所示:
-
2.第二次遍歷序列,我們每次遍歷一個元素都將該元素所對應的區間數組對應的位置進行
+1
操作,這個步驟其實就是我們計數排序的核心----計數了.遍歷結束之後,區間數組中的元素值就代表相應元素出現的次數
,如下圖所示:
-
3.最後一步就只需要按照區間數組中的次數一次將該元素打印出來即可.如下圖所示:
計數排序的基本思想基本就是這樣,按照慣例,還是通過下面的圖來幫助大家更好的理解計數排序的基本思想:
瞭解完計數排序的基本思想之後,我們還是按照慣例分析一下計數排序算法的一些特點:
-計數排序是穩定
的 ,這個大家應該能很明顯的看出來,因爲計數排序本身並不是基於比較的算法.
-計數排序需要的額外空間比較大
,這個大家很明顯的看出來,並且空間浪費的情況也會比較嚴重,因爲一旦序列中MAX與MIN的差距過大,那麼需要的內存空間就會非常大.並且假如序列中的元素都是散佈在一個特定的區間內,那麼內存空間浪費的情況也會非常明顯.
算法圖解
:
示例代碼
:
public static void main(String[] args) {
int []num ={
7,4,9,3,2,1,8,6,5,10};
long startTime=System.currentTimeMillis();
int min=Integer.MAX_VALUE;
int max=Integer.MIN_VALUE;
//先找出數組中的最大值與最小值
for(int i=0;i<num.length;i++) {
if(num[i]<min)
min=num[i];
if(num[i]>max)
max=num[i];
}
//創建一個長度爲max-min+1長度的數組來進行計數
int []figure=new int [max-min+1];
for(int i=0;i<num.length;i++) {
//計算每個數據出現的次數
figure[num[i]-min]++;
}
int begin=0;
//創建一個新的數組來存儲已經排序完成的結果
int []num1=new int [num.length];
for(int i=0;i<figure.length;i++) {
//循環將數據pop出來
if(figure[i]!=0) {
for(int j=0;j<figure[i];j++) {
num1[begin++]=min+i;
}
}
}
System.out.println("數據範圍:"+min+"~"+max);
System.out.println("計數結果: ");
for(int i=0;i<num.length;i++)
System.out.println(" "+num[i]+"出現"+figure[num[i]-min]+"次");
System.out.print("排序結果: ");
for(int i=0;i<num1.length;i++)
System.out.print(num1[i]+" ");
System.out.println();
long endTime=System.currentTimeMillis();
System.out.println("程序運行時間: "+(endTime-startTime)+"ms");
}
複雜度分析
:
理解完計數排序的基本思想之後,我們就需要來分析一下他的時間複雜度,空間複雜度.
-
時間複雜度
計數排序很明顯是一種通過空間來換時間的算法,因爲我們可以很明顯的看到計數排序需要三次遍歷,兩次遍歷我們的原序列,第三次是遍歷我們的區間數組.那麼很明顯
時間複雜度一定是線性級別的
但是因爲第三次遍歷的並不是我們的原序列,而是我們的區間數組,所以時間複雜度並不是我們的平常的O(n),而是應該加上我們遍歷區間數組的時間,假設我們的區間數組長度爲k的話,那麼我們的時間複雜度就是O(n+k)
-
空間複雜度
上面我們已經說過了,計數排序本身就是一個通過空間來換取時間的算法,所以很明顯他的空間複雜度就會很高.並且這個空間複雜度主要就取決於我們區間數組的長度,所以假設我們的區間數組長度爲k的話,那麼我們的空間複雜度就爲
O(k)
3-桶排序
算法思想
:
大家第一眼看到這個算法的名字時相信大家的反應應該和我是一樣的,桶排序?排序怎麼還需要用到桶呢?桶排序裏的桶又是主要是幹什麼的呢?
其實這個大家類比到我們平常生活中就能基本知道桶排序的桶是幹嘛的呢?在我們的日常生活中,我們的桶一般都是用來裝東西的,我們可能是用來裝水,又或者是裝錢的反正不管怎麼樣,我們的桶最後都是一個容器,是用來存儲相應的物質的.
顯然我們當前存在的只有我們的待排序的序列,那麼我們的桶就是用來存儲我們的序列中的元素的.就像下圖所示:
可以看到我們把相應的元素放入相應的桶裏面了.這個放入的規則是這樣的:桶是從大到小排列的,並且每一個桶都會有一個數據範圍,意思就是0號桶存放是1~ 2數據範圍的數據,1號桶存放3~4數據範圍的數據,2號桶存放吧5 ~6數據範圍內的數據.詳細的放入規則我會在下面的實現步驟裏面說.這裏大家先簡單瞭解一下.
這裏大家要注意的一點就是,我們在把元素放入各自相應的桶裏面的時候,是需要對桶內的序列進行排序
的,意思就是等到每個元素都放入相應的桶裏面之後,桶裏面相應的序列本身也已經有序了.就如下圖所示:
可以看到上面,每個桶內的序列都已經排好序了,那麼很顯然我們最後就只需要按照桶的序號大小將桶內的元素打印出來,那麼我們的序列就已經排好序了.
說完桶排序的基本思想之後,我們接下來就說一下桶排序在代碼上是如何實現的,大致有下面這幾步:
-
1.我們首先需要第一次遍歷我們的序列,得到我們序列中的最大值
MAX
以及序列中的最小值MIN
,找到我們序列中的最大值與最小值之後,那麼我們就可以確定序列中的所有都是在MIN~MAX
這個數據範圍區間之中. -
2.第二步我們就是需要根據序列的數據範圍來確定我們到底需要幾個桶來存放我們的元素,這一步其實是比較關鍵的,因爲桶的數量太多或者太少都會降低桶排序的效率.
我們舉兩個例子:
假設我們桶的數量太少,就比如說只有一個桶:
那麼很顯然我們的桶排序就又重新退化成我們前兩篇內容裏介紹的比較算法了.又假設我們桶的數量太多,就比如說有
MAX-MIN+1
個桶:
那麼很顯然這時候的桶排序又重新退化成了我們上面剛剛講解過的計數排序了.所以說我們需要確定好一個適中的桶的數量,不然回就會出現我們上面所說到的幾種情況.但是有沒有一個特定的公式來確定桶的數量.所以我們還是隻能自己確定桶的數量.但是有一個規則我們還是可以考慮進去的,那就是
最好讓元素平均的分散到每一個桶裏
. -
3.確定完桶的數量之後,我們就可以給每個桶來劃分數據範圍了.一般是這樣劃分的,
(MAX-MIN+1)/桶的數量
,得到的結果就是桶長.之後每個桶的數據範圍就通過桶的編號以及桶長就可以確定每個桶的數據範圍.就如下面的公式:左閉右開
桶的數據範圍=[MIN+(桶的編號-1)*桶長,MIN+桶的編號 *桶長)
有了每個桶的數據範圍時候,我們第二次遍歷序列將每個元素存到相應的桶裏面了.這個過程我們要注意,在往桶裏面添加元素的時候,就需要在每個桶裏面將元素排好序. -
4.當我們第二次遍歷結束之後,我們就只需要按照桶的編號,在將該編號的桶裏面的元素打印出來,桶排序就已經完成了.
接下來我們還是通過下面的圖來動態演示一下桶排序的過程:
瞭解完桶排序的基本思想之後,按照慣例我們還是來簡單分析一下他的一些特點:
- 桶排序是
穩定
的,原因和上面計數排序的理由是一樣的. - 桶排序也是一個
通過空間換取時間
的算法,但是他的空間複雜度是可以控制的.就像我們上面說的主要就是控制桶的數量.如果桶的數量
設置的合理
,既能降低時間複雜度,也能降低空間複雜度.
算法圖解
:
示例代碼
:
//在鏈表中添加元素的同時需要進行元素的排序
public static void sort(ArrayList<Integer>list,int i) {
if(list==null)
list.add(i);
//這裏採用的排序方式爲插入排序
else {
int flag=list.size()-1;
while(flag>=0&&list.get(flag)>i) {
if(flag+1>=list.size())
list.add(list.get(flag));
else
list.set(flag+1, list.get(flag));
flag--;
}
if(flag != (list.size()-1))
//注意這裏是flag+1,自己可以嘗試將這裏換成flag看看,會出現數組越界的情況
list.set(flag+1, i);
else
list.add(i);
}
}
public static void Bucketsort(int []num,int sum) {
//遍歷得到數組中的最大值與最小值
int min=Integer.MAX_VALUE;
int max=Integer.MIN_VALUE;
for(int i=0;i<num.length;i++) {
min = min <= num[i] ? min: num[i];
max = max >= num[i] ? max: num[i];
}
//求出每個桶的長度,這裏必須使用Double
double size=(double)(max-min+1)/sum;
ArrayList<Integer>list[]=new ArrayList[sum];
for(int i=0;i<sum;i++) {
list[i]=new ArrayList<Integer>();
}
//將每個元素放入對應的桶之中同時進行桶內元素的排序
for(int i=0;i<num.length;i++) {
System.out.println("元素:"+String.format("%-2s", num[i])+", 被分配到"+(int)Math.floor((num[i]-min)/size)+"號桶");
sort(list[(int)Math.floor((num[i]-min)/size)], num[i]);
}
System.out.println();
for(int i=0;i<sum;i++) {
System.out.println(String.format("%-1s", i)+"號桶內排序:"+list[i]);
}
System.out.println();
//順序遍歷各個桶,得出我們 已經排序號的序列
for(int i=0;i<list.length;i++) {
if(list[i]!=null){
for(int j=0;j<list[i].size();j++) {
System.out.print(list[i].get(j)+" ");
}
}
}
System.out.println();
System.out.println();
}
public static void main(String[] args) {
int []num ={
7,4,9,3,2,1,8,6,5,10};
long startTime=System.currentTimeMillis();
//這裏桶的數量可以你自己定義,這裏我就定義成了3
Bucketsort(num, 3);
long endTime=System.currentTimeMillis();
System.out.println("程序運行時間: "+(endTime-startTime)+"ms");
}
這裏的時間是不準確的,因爲我需要將每輪排序的結果打印出來給大家看,所以會多花費一些時間,如果大家想要看真實的時間的話,大家可以把我打印結果的代碼註釋掉再看看算法的執行時間.
複雜度分析
:
理解完桶排序的基本思想之後,我們就需要來分析一下他的時間複雜度,空間複雜度.
-
時間複雜度
桶排序的時間複雜度和上面的計數排序是一樣的,同樣也是線性級別的,但是也是增加了桶的時間,所以也是
O(n+k)
-
空間複雜度
上面我們已經說過了,桶排序本身也是一個通過空間來換取時間的算法,所以很明顯他的空間複雜度就會很高.並且這個空間複雜度主要就取決於桶的數量以及桶的範圍,所以假設有k個桶的話,那麼空間複雜度就爲
O(n+k)
4-基數排序
算法思想
:
基數排序的實現步驟非常好理解,但是想要真正理解他的算法思想就稍微有點難度了.那麼接下來就來講解基數排序的算法思想.
首先基數排序是根據數位來進行排序的.他是從個位開始,然後按照每一位的數進行排序,如下圖所示:
排完序之後就往前進一位,然後再將所有的數按照這一位所在的數進行排序,如下圖所示:
重複這個過程直到所有的位數都已經被排過序了.如下圖所示:
並且如果這個過程中碰到某個數在這個爲上沒有數的話就進行補零操作,將該位看成是0.就比方下圖我用紅框圈出來的部分:
等到所有的位數都已經排序完畢之後,我們就可以看到我們已經排序好的序列了.
這個過程相信大家肯定都很好理解,但是相信大家如果是第一次看這個算法的肯定會有和我當初一樣的困惑,那就是爲什麼基數排序選擇的是從低位到高位
來進行排序的呢,而不是像我們平常比較數據的大小一樣,從高位到低位
來比較呢?
這裏呢我們先不講爲什麼,我們先看下面這兩個案例:
- 從高位到低位進行比較
原序列 | 百位排好序後 | 十位排好序後 | 個位排好序後 |
---|---|---|---|
329 | 839 | 657 | 839 |
457 | 720 | 457 | 329 |
657 | 657 | 355 | 657 |
839 | 457 | 839 | 457 |
436 | 436 | 436 | 436 |
720 | 329 | 720 | 355 |
355 | 355 | 329 | 720 |
- 從低位到高位進行比較
原序列 | 個位排好序後 | 十位排好序後 | 百位排好序後 |
---|---|---|---|
329 | 720 | 720 | 329 |
457 | 355 | 329 | 355 |
657 | 436 | 436 | 436 |
839 | 457 | 839 | 457 |
436 | 657 | 355 | 657 |
720 | 329 | 451 | 720 |
355 | 839 | 657 | 839 |
對比看了上面兩個例子之後相信大家就知道了,很明顯我們看到如果是從該我到低位來進行比較的話,我們會發現比較到最後之後序列仍然是無序的,但是從低位到高位進行比較的話,我們就會發現序列到最後已經排好序了.
是不是很奇怪,同樣的執行過程,只不過執行的順序發生了改變,爲什麼結果就回產生這樣的結果呢?產生這個差異的重點就是在我們忽略了一個問題,那就是我們在進行高位到低位比較的時候,高位的權重是高於低位的
.就比方下圖所示:
正是因爲這個原因,所以我們採取的是從低位到高位比較的過程.
說完基本思想之後,我們來說一下算法的實現步驟:
-
1.我們第一次遍歷序列.找出序列中的最大值MAX,找到MAX之後我們可以確定我們需要比較多少數位了.
-
2.這時候我們就需要按照元素在該位數對應的數字將元素存入到相應的容器之中.如下圖所示:
-
3.之後我們再按照容器的順序將元素重新彈出構成我們接下來需要排序的序列,如下圖所示:
這個從容器彈出的過程需要注意一點,那就是遵循先進先出
的原則,所以這個容器選擇隊列或者是鏈表
比較合適,不能選擇棧,因爲棧是先進後出,拿取元素的時候回非常麻煩. -
4.最後只需要
重複2,3步驟
,直到最高位也比較完畢,那麼整個基數排序就已經完成了.
接下來我們還是通過下面的圖來動態演示一下基數排序的過程:
個位排序:
十位排序:
百位排序:
千位排序:
在瞭解完基數排序的基本思想之後,按照慣例我們還是來簡單分析一下基數排序的特點:
- 基數排序是
穩定
的,原因和桶排序以及計數排序的原因一樣.
算法圖解
:
示例代碼
:
//將所有的數組合併成原來的數組
public static void merge(ArrayList<Integer> list[],int num[]) {
int k=0;
for(int i=0;i<list.length;i++) {
if(list[i]!=null) {
for(int j=0;j<list[i].size();j++) {
num[k++]=list[i].get(j);
System.out.print(num[k-1]+" ");
}
}
//合併完成之後需要將鏈表清空,否則元素會越來越多
list[i].clear();
}
System.out.println();
}
//將所有的元素分散到各個鏈表之中
public static void split(ArrayList<Integer> list[],int num[],int k) {
for(int j=0;j<num.length;j++) {
list[num[j]/k%10].add(num[j]);
}
System.out.println("-----------------------------------------------------------------------");
System.out.println("個位開始數,第"+(String.valueOf(k).length())+"位排序結果:");
for(int j=0;j<10;j++) {
System.out.println((String.valueOf(k).length())+"號位,數值爲"+j+"的鏈表結果:"+list[j]);
}
}
public static void main(String[] args) {
ArrayList<Integer>list[]=new ArrayList [10];
for(int i=0;i<10;i++) {
list[i]=new ArrayList<Integer>();
}
int []num ={
7,14,9,333,201,1,88,6,57,10,56,74,36,234,456};
long startTime=System.currentTimeMillis();
int max=Integer.MIN_VALUE;
//第一次遍歷獲得序列中的最大值
for(int i=0;i<num.length;i++) {
if(num[i]>max)
max=num[i];
}
int k=1;
for(int i=0;i<String.valueOf(max).length();i++) {
split(list, num, k);
System.out.println("第"+(i+1)+"次排序");
merge(list, num);
k=k*10;
}
long endTime=System.currentTimeMillis();
System.out.println("程序運行時間: "+(endTime-startTime)+"ms");
}
複雜度分析
:
理解完基數排序的基本思想之後,我們就需要來分析一下他的時間複雜度,空間複雜度.
-
時間複雜度
看完我們上面的動圖演示之後我們可以看到基數排序只需要根據最大元素的位數,遍歷相應次數的序列即可,所以我們可以確定基數排序的時間複雜度一定是線性級別的,但是雖然是線性級別的,但是有一個係數的,這個係數就是最大元素的位數K,所以時間複雜度應該是
O(n*k)
-
空間複雜度
空間複雜度我們也可以看出來,主要就是取決於
鏈表的數量以及序列元素的數量
,所以空間複雜度爲O(n+k)
到這裏十大經典排序算法詳解的內容就已經全部講解完畢了.這一次文章不管是在內容的質量上或者是在文章的排版上,都是目前工作量比較大的一期.所以如果大家覺得文章還行或者覺得文章對你有幫助的話,UP希望能關注一下我的公衆號:萌萌噠的瓤瓤
,或者覺得關注公衆號麻煩的話,也可以給我的文章一鍵三連
.新人UP真的很需要你的支持!!!
不點在看,你也好看!
點點在看,你更好看!