1. 程式人生 > 其它 >「乾貨」程式語言十大經典演算法,你知道幾個?

「乾貨」程式語言十大經典演算法,你知道幾個?

演算法與資料結構是計算機學習路上的內功心法,也是學好程式語言的重要基礎。今天給大家介紹一下十大經典演算法。

十大經典演算法分別是:氣泡排序,插入排序,選擇排序,希爾排序,快速排序,歸併排序,桶排序,堆排序,計數排序,基數排序。

預備知識:演算法穩定性

如果 a==b,排序前 ab 的前面,排序後 ab 的後面,只要會出現這種現象,我們則說這個演算法不穩定(即使兩個相等的數,在排序的過程中不斷交換,有可能將後面的 b 交換到 a 的前面去)。

一、氣泡排序

氣泡排序(Bubble Sort)是基於交換的排序,它重複走過需要排序的元素,依次比較相鄰的兩個元素的大小,保證最後一個數字一定是最大的,即它的順序已經排好,下一輪只需要保證前面 n-1

個元素的順序即可。

之所以稱為冒泡,是因為最大/最小的數,每一次都往後面冒,就像是水裡面的氣泡一樣。

排序的步驟如下:

  1. 從頭開始,比較相鄰的兩個數,如果第一個數比第二個數,那麼就交換它們位置。
  2. 從開始到最後一對比較完成,一輪結束後,最後一個元素的位置已經確定。
  3. 除了最後一個元素以外,前面的所有未排好序的元素重複前面兩個步驟。
  4. 重複前面 1 ~ 3 步驟,直到都已經排好序。

例如,我們需要對陣列 [98,90,34,56,21] 進行從小到大排序,每一次都需要將陣列最大的移動到陣列尾部。那麼排序的過程如下動圖所示:

二、選擇排序

前面說的氣泡排序是每一輪比較確定最後一個元素,中間過程不斷地交換。而選擇排序就是每次選擇剩下的元素中最小的那個元素,直到選擇完成。

排序的步驟如下:

  • 從第一個元素開始,遍歷其後面的元素,找出其後面比它更小的元素,若有,則兩者交換,保證第一個元素最小。
  • 對第二個元素一樣,遍歷其後面的元素,找出其後面比它更小的元素,若存在,則兩者交換,保證第二個元素在未排序的數中(除了第一個元素)最小。
  • 依次類推,直到最後一個元素,那麼陣列就已經排好序了。

比如,現在我們需要對 [98,90,34,56,21] 進行排序,動態排序過程如下:

三、插入排序

選擇排序是每次選擇出最小的放到已經排好的陣列後面,而插入排序是依次選擇一個元素,插入到前面已經排好序的陣列中間,當然,這是需要已經排好的順序陣列不斷移動。步驟描述如下:

  1. 從第一個元素開始,可以認為第一個元素已經排好順序。
  2. 取出後面一個元素 n,在前面已經排好順序的數組裡從尾部往頭部遍歷,假設取出來的元素為 nums[i],如果 num[i]>n,那麼將 nums[i] 移動到後面一個位置,直到找到已經排序的元素小於或者等於新元素的位置,將 n 放到新空出來的位置上。如果沒有找到,那麼 nums[i] 就是最小的元素,放在第一個位置。
  3. 重複上面的步驟 2,直到所有元素都插入到正確的位置。

以陣列 [98,90,34,56,21] 為例,動態排序過程如下:

四、希爾排序

希爾排序(Shell's Sort)又稱“縮小增量排序”(Diminishing Increment Sort),是插入排序的一種更高效的改進版本,同時該演算法是首次衝破 O(n^2*n*2) 的演算法之一。

插入排序的痛點在於不管是否是大部分有序,都會對元素進行比較,如果最小數在陣列末尾,想要把它移動到陣列的頭部是比較費勁的。希爾排序是在陣列中採用跳躍式分組,按照某個增量 gap 進行分組,分為若干組,每一組分別進行插入排序。再逐步將增量 gap 縮小,再每一組進行插入排序,迴圈這個過程,直到增量為 1。

希爾排序基本步驟如下:

  1. 選擇一個增量 gap,一般開始是陣列的一半,將陣列元素按照間隔為 gap 分為若干個小組。
  2. 對每一個小組進行插入排序。
  3. gap 縮小為一半,重新分組,重複步驟 2(直到 gap 為 1 的時候基本有序,稍微調整一下即可)。

以陣列 [98,90,34,56,21,11,43,61] 為例子,排序的動圖如下:

五、快速排序

快速排序比較有趣,選擇陣列的一個數作為基準數,一趟排序,將陣列分割成為兩部分,一部分均小於/等於基準數,另外一部分大於/等於基準數。然後分別對基準數的左右兩部分繼續排序,直到序列有序。這體現了分而治之的思想,其中還應用到挖坑填數的策略。

演算法的步驟如下:

  1. 從陣列中挑一個元素作為基準數,一半情況下我們選擇第一個 nums[i],儲存為 standardNum,可以理解為 nums[i] 坑位的數被拎出來了,留下空的坑位。
  2. 取陣列的左邊界索引為 i,右邊界索引 jj 從右邊往左邊,尋找到比 standardNum 小的數,停下來,寫到 nums[i] 的坑位,nums[j] 的坑位空出來。 i 從左邊往右邊找,尋找比 standardNum 大的數,停下來,寫到 nums[j] 的坑位,這個時候,num[i] 的坑位空出來(前提是 ij 不相撞)。
  3. 上面的 ij 迴圈步驟 2,直到 ij 相撞,將基準值 standardNum 寫到坑位 nums[i] 中,這時候,standardNum 左邊的數都小於等於它本身,右邊的數都大於等於它本身。
  4. 分別對 standardNum 左邊的子陣列和右邊的子陣列,迴圈執行前面的 1,2,3,直到不可再分,並且有序。

以陣列 [61,90,34,56,21,11,43,68] 為例,動態排序過程如下:

六、歸併排序

前面學的快速排序,體現了分治的思想,但是不夠典型,而歸併排序則是非常典型的分治策略。歸併的總體思想是先將陣列分割,再分割...分割到一個元素,然後再兩兩歸併排序,做到區域性有序,不斷地歸併,直到陣列又被全部合起來。

排序步驟大致如下:

  • 將長度為 n 的陣列分割成為 n/2 的兩個子陣列。
  • 子陣列也不斷分割成為更小的子陣列,直到不能分割。
  • 最小子陣列之間開始兩兩合併,合併之後的結果再合併。合併的時候可以申請一個臨時空間,利用兩個索引指標比較的方式,將兩個子陣列的結果合併到臨時陣列中去。
  • 迴圈 3 步驟,直到合併成為長度為 n 的已經排序的陣列。

以陣列 [61,90,34,56,21,11,43,68] 為例,每一次都是對陣列分成兩半,直至不能拆分,再兩兩合併,合併的時候相當於對有序的兩個子陣列合併。

動態執行過程如下:

七、計數排序

計數排序,不是基於比較,而是基於計數

計數排序步驟如下:

  • 遍歷陣列,找出最大值和最小值。
  • 根據最大值和最小值,初始化對應的統計元素數量的陣列。
  • 遍歷元素,統計元素個數到新的陣列。
  • 遍歷統計的陣列,按照順序輸出排序的陣列元素。

假設有幾個青少年,他們年齡很接近,分別是 11、9、11、 13、12、14、15、13,現在需要給他們按照年齡排序。首先先遍歷一遍,找出最小的 min 和最大的元素 max,建立一個大小為 max - min + 1 的陣列,再遍歷一次,統計數字個數,寫到陣列中。

然後再遍歷一次統計陣列,將每個元素置為前面一個元素加上自身,為什麼這樣做呢?

為了讓統計陣列儲存的元素值等於相應整數的最終排序位置,這樣我們就可以做到穩定排序,比如下面的 15 對應的是 8,也就是 15 在陣列中出現的是第 8 個元素,從後面開始遍歷,我們就可以保持穩定性。

比如原陣列從後往前遍歷到 13 的時候, 13 對應的位置是 6,那麼此時從後往前遍歷到的第一個 13 就是在第 6 個元素位置。後面再遇到 13,就放到第 5 個元素位置,不會打亂它們的相對位置

動態過程如下:

八、桶排序

桶排序,是指用多個桶儲存元素,每個桶有一個儲存範圍,先將元素按照範圍放到各個桶中,每個桶中是一個子陣列,然後再對每個子陣列進行排序,最後合併子陣列,成為最終有序的陣列。這其實和計數排序很相似,只不過計數排序每個桶只有一個元素,而且桶的值為元素的個數。

桶排序的具體步驟:

  • 遍歷陣列,查詢陣列的最大最小值,設定桶的區間(非必需),初始化一定數量的桶,每個桶對應一定的數值區間。
  • 遍歷陣列,將每一個數,放到對應的桶中。
  • 對每一個非空的桶進行分別排序(桶內部的排序可以選擇 JDK 自帶排序)。
  • 將桶中的子陣列拼接成為最終的排序陣列。

以陣列 [98,90,34,56,21,11,43,61] 為例,桶排序的動態過程:

先遍歷查找出 max 為 98, min 為 11,陣列大小為 8,( 98 - 11 )/8 + 1 = 11,桶的個數為 11。先把元素按照區間放進去,對每一個桶分別排序,然後再把桶的元素連起來放在陣列中,排序就完成了。

九、堆排序

堆排序,就是利用大頂堆或者小頂堆來設計的排序演算法,是一種選擇排序。堆是一種完全二叉樹:

  • 大頂堆:每個節點的數值都大於或者等於其左右孩子節點的數值。
  • 小頂堆:每個節點的數值都小於或者等於其左右孩子節點的數值。

我們一般使用陣列來對堆結構進行儲存,下面我們只說大頂堆(元素按照從小到大排序),假設陣列為 nums[],則第 i 個數滿足:num[i] >= nums[2i+1]num[i] >= nums[2i+2],第 i 個數在堆上的左節點就是陣列中下標索引 2i+1 的元素,其右節點就是陣列中下標索引 2i+2 的元素。

排序的思路為:

  • 將無序的陣列構建出一個大頂堆,也就是上面的元素比下面的元素大。
  • 將堆頂的元素和堆的最末尾的元素交換,將最大元素下沉到陣列的最後面(末端)。
  • 重新調整前面的順序,繼續交換堆頂的元素和當前末尾的元素,直到所有元素全部下沉。

倘若一個數組為 [11,21,34,43,56,61,90,98],動態的過程如下:

十、基數排序

基數排序比較特殊,特殊在它只能用在整數(自然數)排序,而且不是基於比較的,其原理是將整數按照位分成不同的數字,按照每個數各位值逐步排序。何為高位,比如 81,1 就是低位, 8 就是高位。 分為高位優先和低位優先,先比較高位就是高位優先,先比較低位就是低位優先。下面我們講高位優先。

主要的步驟如下:

  • 將所有元素統一稱為統一數位長度,前面補 0。
  • 從最高位開始,依次排序,從最高位到最低位遍歷完,陣列就是有序的。

以陣列 [98,90,34,56,21,11,43,61,39] 為例,動態的排序過程如下:


十個演算法的複雜度以及特點總結一下:

  • 氣泡排序:基本最慢,時間複雜度最好為 O(n),最壞為 O(n2),平均時間複雜度為 O(n2),空間複雜度為 O(1),穩定排序演算法。
  • 選擇排序:時間複雜度很穩定,最好最壞或者平均都是 O(n2),空間複雜度為 O(1),可以做到穩定排序。
  • 插入排序:時間複雜度最好為 O(n),最壞為 O(n2),平均時間複雜度為 O(n2),空間複雜度為 O(1),穩定排序演算法。
  • 希爾排序:希爾增量下最壞的情況時間複雜度是 O(n2),最好的時間複雜度是 O(n) (也就是陣列已經有序),平均時間複雜度是 O(n3/2),屬於不穩定排序。
  • 快速排序:時間複雜度最差的情況是 O(n2),平均時間複雜度為 O(nlogn),空間複雜度,雖然快排本身沒有申請額外的空間,但是遞迴需要使用棧空間,遞迴數的深度是 log2n,空間複雜度也就是 O( log2n),屬於不穩定排序。
  • 歸併排序:排序複雜度為 nlog2n,不存在好壞的情況,但是代價就是需要申請額外的空間,申請空間的大小最大為 n,所以空間複雜度為 O(n),屬於穩定排序。
  • 計數排序:時間複雜度為 O(n+k),申請了一個統計陣列和一個新陣列,空間複雜度為 O(n+k),沒有所謂最好最壞,都是一個複雜度,一般適用於小範圍整數排序,屬於穩定排序。
  • 桶排序:最好情況時間複雜度 O(n),最壞情況時間複雜度為 O(n2),平均的時間複雜度為 O(n+k)。由於在中間過程中會申請桶的數量 m,所以空間複雜度為 O(n+m),穩定性決定於桶內部排序。
  • 堆排序:時間複雜度為 O(nlogn),沒有申請額外的空間,空間複雜度為 O(1),屬於不穩定排序。
  • 基數排序:時間複雜度為 O(d(2n))。一般只使用於整數排序,不適合小數或者文字排序。由於需要申請桶的空間,假設有 k 個桶(上面是 10 個桶),則空間複雜度為 O(n+k),一般 k 較小,所以近似為 O(n),屬於穩定排序。

每一種排序,都有其優缺點,我們應該根據場景選擇合適的排序演算法。

資料來源

如果你覺得有用的話就給這篇文章一個贊吧!