1. 程式人生 > >【演算法】演算法初步:聊一聊常見排序的演算法

【演算法】演算法初步:聊一聊常見排序的演算法

在一個工程中一旦建立了某一個數據庫後,就可能需要對資料庫中資料進行不同方式的排序,比如對姓名進行字母排序,年齡進行大小排序等等。排序在程式設計中非常的重要,但又可能十分的複雜。這篇博文主要介紹一下幾種簡單而且常見的排序演算法。

如何排序

讓我們假設一個場景。體育課上,同學們排成一列。現在要按照身高從高到底排隊(最矮的在最左邊),應該怎麼排隊呢?
如果在現實生活中這是很簡單的事情,我們可以一眼看到哪個最高,從而毫不費力的把順序排好。而對計算機而言這是不行的,計算機程式不能像人一樣通覽所有的資料,它只能通過比較一步一步的解決問題。
一般最簡單的排序都包括一下步驟:

  • 比較兩個資料
  • 交換兩個資料,或者複製其中一個數據
    但是每種演算法的細節有所不同。

氣泡排序

氣泡排序是一種十分簡單,但是運算起來又非常慢的一種演算法。但是對於一個剛開始研究演算法的程式設計師來說又是一種非常好的演算法。
假設上述場景中有N個同學,我們給每個同學的位置進行按照從左到右進行編號。從0到N-1。
氣泡排序執行過程如下:從佇列最左邊開始即0位置開始,比較0號位置和1號位置的隊員。如果0號位置高一些,就把0號位置的同學和1號位置的同學互換;否則,什麼也不做,比較1號位置和2號位置的同學。這個排序的過程如下所示:BubbleSort

以下是這個排序的準則:

  • 比較兩個資料
  • 如果左邊的資料較大(小),那麼交換兩個資料的位置。
  • 向右移動一個位置,比較接下來兩個資料
    按照以上的準則進行一輪排序,我們會發現最高的同學現在已經在最右邊了。但是前N-1的同學的順序是否正確我們很難保證,所以需要再進行排序,直到同學們按由高到低的順序站成一列。假如一組資料中有N資料,那麼用氣泡排序的進行排列的話要進行N-1次排序。關於氣泡排序的Java程式碼詳見
    氣泡排序Java程式碼

氣泡排序效率分析

一般來說,陣列中有N個數據,則第一趟排序中有N-1次比較,第二趟中有N-2次比較,這樣算下來一個有N個數據的陣列,用冒牌演算法進行排序的話約做了N²/2(確切來說是N*(N-1)/2)次比較。時間複雜度是O(n²)級別的。所以效率是很慢的。假如我們的資料中前i項是無序的,後N-i項是有序的,那麼使用氣泡排序的後N-1-i次排序都是毫無意義的。

選擇排序

選擇排序是改進了氣泡排序的一種演算法。將時間複雜度從O(n²)降低到O(n)。
讓我們迴歸到我們的場景中。在選擇排序中,不再只比較兩個相鄰的同學,而是把所有同學的身高都掃描一遍。選出最矮的和站在佇列最左邊的同學交換位置。現在0號位置的同學是有序的了。然後我們再掃描佇列的時候就從1號位置開始,還是搜尋最矮的那個,然後和1號位置進行交換。然後重複這個過程直到所有同學都排定。這個排序過程如下所示:
SelectSort

更詳細的描述

排序從隊員的最左邊開始,記錄同學身高。並在0號同學面前放一面旗子,然後和右邊的同學的依次進行身高比較,如果遇到比這個同學矮的,則把旗子放在該同學位置前,並把兩個人交換順序。現在0好同學依然是當前最矮的,然後0好同學繼續和以後的同學進行比較,遇到更矮的就重複移動旗子和交換位置的動作,一輪比較下來,0號位置的同學就是最矮的那位。具體程式碼詳見選擇排序的Java程式碼實現

選擇排序效率分析

選擇排序和氣泡排序執行了相同的比較次數:N*(N-1)/2。所以時間複雜度和氣泡排序一樣是O(n²)。但是選擇排序無疑更快,因為選擇排序的交換要少的多。

插入排序

插入排序在一般情況下要比氣泡排序快一倍,比選擇排序也要快一些,雖然它比選擇排序還有氣泡排序都要麻煩一些。插入排序通常被用在較為複雜排序演算法的最後階段,如快速排序。
依然是我們上面的場景,在開始前我們假設這個佇列已經區域性有序。此時,在隊伍中有一個同學作為標記(放一面旗子在該同學面前)。在這個同學左邊的隊員已經區域性有序了。這就意味他左邊的同學已經由高到底的排序好了,但不一定是最終的位置。下面要做的在有序的部分中的適當位置插入被標記的同學,而要做到這一點我們可以讓標記的同學先出列,然後就騰出了空間,可以把有許的同學進行右移,最終把被標記的同學插到適當的位置。現在,區域性有序的部分裡多了一個隊員,而為排序的部分裡少了一個隊員。則標記(旗子)向右移動一個位置。重複這個過程知道所有為排序額隊員都插入到區域性有許佇列中的適當位置。這個排序的過程如下:
InsertSort
具體程式碼詳見插入排序Java實現

插入排序效率分析

對於已經區域性有序的資料來說,插入排序要好的多。當資料有序的時候內層迴圈的條件總是不成立的,所以它程式設計了外層迴圈中的一條簡單的豫劇,執行N-1次,這種時間複雜度為O(n)。所以插入排序的時間複雜度總是介於O(n)與O(n²)之間。相比氣泡排序和選擇排序還是一個簡單有效的方法。

歸併排序

關於遞迴

再講歸併排序之前,讓我們來聊一聊遞迴。遞迴是一種方法呼叫自己的程式設計技術。再通俗一點的解釋就是函式的自呼叫。遞迴在解決一些問題的時候十分的方便,使用遞迴會大大的縮減程式碼量,但是也會導致一些問題。比如棧的溢位。在學習python的過程中,有看到過優化遞迴的方法,通過尾遞迴優化。

解決遞迴呼叫棧溢位的方法是通過尾遞迴優化,事實上尾遞迴和迴圈的效果是一樣的,所以,把迴圈看成是一種特殊的尾遞迴函式也是可以的。尾遞迴是指,在函式返回的時候,呼叫自身本身,並且,return語句不能包含表示式。這樣,編譯器或者直譯器就可以把尾遞迴做優化,使遞迴本身無論呼叫多少次,都只佔用一個棧幀,不會出現棧溢位的情況。可是非常遺憾,大多數程式語言沒有針對尾遞迴做優化,即使把函式改成尾遞迴方式,也會導致棧溢位。

歸併排序

歸併排序要比上面提到的三種排序方法有效的多,至少速度上是這樣的。歸併排序的實現也相當容易,理解起來也不困難。
在我們的場景中,把同學分為若干組,為了方便起見我們就分為兩組,兩組的人數不一定要相同,可以任意分配。先把兩個組分別進行排序,使兩個組內部有序。然後就可以合併這兩個組了。如何合併呢,我們可以先比較兩個組中最左邊的同學,比較矮的同學單獨站一列。然後再比較兩組中最左邊的同學,較矮的同學站在之前單獨站一列同學的右邊,直到一個組的同學全部出列,另一個組的同學依次站在右邊就完成了排序。排序的過程如下所示:MergeSort
具體程式碼詳見歸併排序Java實現

歸併排序效率分析

歸併排序的執行時間是O(NlogN)。很明顯要比之前的幾種排序的方法更快,並且歸併排序的比較次數是要比資料複製的次數少一些的。但是歸併排序有一個缺點,就是它需要在儲存器中有另一個大小等於被排序的資料項數目的陣列。如初始陣列很大,沾滿了整個容器,那麼歸併排序是不能工作的。

快速排序

聊了這麼多,終於到快速排序了。毫無疑問,快速排序是當下最流行的排序演算法了。因為在大多情況下快速排序都是最快的。快速排序演算法的本質是通過把一個數組分成兩個子陣列,然後遞迴地呼叫自身為每一個子陣列進行快速排序來實現。下面先給出程式碼,然後再分析快速排序。程式碼詳見快速排序Java實現
參照程式碼可以看出快速排序有三個基本步驟:

  • 把陣列或者子陣列劃分成兩組(左右兩組)。
  • 遞迴對左邊一組組進行排序
  • 遞迴對右邊一組進行排序
    經過一次劃分後,所有在左邊的資料項都小於在右邊的陣列的資料。自要對兩個數字分別排序,證個數組自然就有序了。對與子陣列的排序自然用遞迴的方法就可以了。整個排序的過程如下:
    這裡寫圖片描述

總結

這些演算法是大一就學習了的,當時還在寫C++,冒牌排序,插入排序或許都很容易理解。但是歸併排序和快速排序理解起來是有些難度的。由於現在大三了,面臨找工作的壓力,也就複習了一下這些演算法,還是廢了不少精力的。
以上。