往往遇到關於排序算法的問題老是不能很好的解決,對一些概念,思想以及具體實現的認識也是模棱兩可。歸根結底,仍是掌握不夠熟練。之前只是看別人寫,看了就忘。如今打算本身寫,寫些本身的東西,作個總結。本篇是這個總結的開始,因此咱們先來闡述一下本次總結中會用到的一些概念。
排序是如何分類的?能夠從不一樣的的角度對排序進行分類,這裏我是根據排序的策略對本次總結中涉及到的排序算法進行分類:git
交換排序 | 冒泡排序(Bubble Sort) |
快速排序(Quick Sort) | |
插入排序 | 簡單插入排序(Simple Insertion Sort)(也被稱爲直接插入排序) |
希爾排序(Shell Sort) | |
選擇排序 | 簡單選擇排序(Simple Selection Sort)(也被稱爲直接選擇排序) |
堆排序(Heap Sort) | |
歸併排序 | Merge Sort |
基數排序 | Radix Sort |
計數排序 | Count Sort |
算法名稱 | 時間複雜度 | 空間複雜度 | 穩定性 | 複雜性 |
---|---|---|---|---|
冒泡排序 | O(n2) | O(1) | 穩定 | 簡單 |
快速排序 | O(nlog2n) | O(log2n) | 不穩定 | 較複雜 |
簡單插入排序 | O(n2) | O(1) | 穩定 | 簡單 |
希爾排序 | O(nlog2n)(參考值,與增量有關) | O(1) | 不穩定 | 較複雜 |
簡單選擇排序 | O(n2) | O(1) | 不穩定 | 簡單 |
堆排序 | O(nlog2n) | O(1) | 不穩定 | 較複雜 |
歸併排序 | O(nlog2n) | O(n) | 穩定 | 較複雜 |
基數排序 | O(d(n + r)) | O(n + r) | 穩定 | 較複雜 |
計數排序 | O(n + k) | O(n + k) | 穩定 | 簡單 |
接下來,咱們開始進入正文github
交換排序主要包括兩種排序算法,分別是冒泡排序和快速排序算法
從後往前,兩兩比較待排序列中的元素的大小,若比較結果不符合要求,則進行交換。這樣較小的元素會一直向前移動
經過無序區中相鄰元素的比較和交換,使最小的元素如氣泡通常逐漸往上漂浮至水面c#
public void BubbleSort(int[] array){ for(int i = 0; i < array.Length - 1; i ++){ for(int j = array.Length - 1; j > i ; j --){ if(array[j] < array[j - 1]){ Swap(array, j, j - 1); } } } } // 實現將指定下標的兩個元素在數組中交換位置 public void Swap(int[] array, int i, int j){ int temp = array[i]; array[i] = array[j]; array[j] = temp; }
【算法解讀】數組
算法的外層循環中的i
能夠理解爲有序區的邊界,從0開始即表示初始時有序區是空的。當外層循環執行完一次後,會有一個無序區中最小的元素移動到有序區中,此時i + 1 = 1
表示有序區邊界擴大了,i
之內的元素都處在有序區中。當i = array.Length - 1
時,即表示全部元素都處於有序區了,算法結束。
內層循環負責將無序區中的最小元素移動到有序區中,依次對無序區中相鄰元素進行比較,若是位置靠後的元素小於位置靠前的元素,則交換兩個元素。如此一來,無序區中最小的元素就會被一步步交換到有序區的末尾。函數
【舉個栗子】性能
對於待排序列4,1,3,2
首先依次比較無序區(初始時爲全部元素)中的相鄰元素,比較3,2,位置靠後的元素小於位置靠前的元素,則交換。序列爲4,1,2,3。繼續比較1,2,位置靠後的元素大於位置靠前的元素,不交換。繼續比較4,1,須要交換,此時無序區中的元素所有比較完畢,一趟冒泡排序結束,序列爲1,4,2,3。能夠看到最小元素1已經移動到序列首部,即處於有序區內,肯定了一個元素的位置。無序區還剩下4,2,3。重複上述操做完成最終排序。優化
當待排序列是正序有序時,冒泡排序的時間複雜度仍然是O(n2),針對這點咱們能夠作一些優化
當內層循環執行完成一趟卻沒有發生交換時,說明此時的待排序列已是正序有序的了,能夠直接終止算法。ui
public void BubbleSortOptimize(int[] array){ for(int i = 0; i < array.Length - 1; i ++){ bool swapTag = false; // 標誌位,判斷每完成一趟冒牌排序是否發生交換 for(int j = array.Length - 1; j > i; j --){ if(array[j] < array[j - 1]){ Swap(array, j, j -1); swapTag = true; } } if(!swapTag){ break; } } }
快速排序又被稱爲分區交換排序,是目前已知的平均速度最快的一種排序方法,它採用了一種分治的策略,是對冒泡排序的一種改進。
在待排序列中任取其中一個元素做爲目標元素(稱其爲樞軸或分界點),以該元素爲分界點(pivot)進行分區,將待排序列分紅兩個部分,全部比分界點小的元素都放置於分界點左邊,全部比分界點大的元素都放置於分界點的右邊,而後再分別將左右兩邊做爲兩個待排序列,重複上述過程,直到每一個分區只剩下一個元素爲止。顯然,每一趟快速排序後,分界點都找到了本身在有序序列中的位置spa
public void QuickSort(int[] array){ QuickSortImpl(array, 0, array.Length - 1); } public void QuickSortImpl(int[] array, int left, int right){ if(left >= right) return; int pivot = Partition(array, left, right); QuickSortImpl(array, left, pivot - 1); QuickSortImpl(array, pivot + 1, right); } // 分區函數,實現將全部元素依據大小移動到分界點的兩邊 public int Partition(int[] array, int left, int right){ int target = array[left]; while(left < right){ while(right > left && array[right] >= target){ right --; } if(right > left){ array[left] = array[right]; // 此時left位置的元素是被移動過來的元素,確定比目標元素小 // 因此左指針掃描時就能夠不用比較該元素,進行left++操做 left ++; } while(left < right && array[left] <= target){ left ++; } if(left < right){ array[right] = array[left]; right --; } } array[left] = target; return left; }
【算法解讀】
算法首先選取待排序列的首元素做爲分界點,調用Partition
方法進行分區,將待排序列依據分界點分紅兩個部分。每一個子部分再做爲待排序列進行遞歸調用。直到每部分只剩一個元素(對應代碼:if(left >= right) return;
)。
算法的關鍵在於Partition
方法內部,經過左右指針不斷進行比較替換。首先準備移動右指針(由於當找到比分界點小的元素時,能夠先將其移動到左指針指向的位置,而左指針所指向位置的元素已經被保存到target
中,不用擔憂被覆蓋),找到比分界點小的元素後將其移動到左指針指向的位置。右指針中止。準備移動左指針,找到比目標元素大的元素後,將其移動到右指針指向的位置(此時原來在右指針位置的元素也已經被移動走了,能夠直接覆蓋),左指針中止。再次開始移動右指針,重複上述操做。直到左指針和右指針重合,它們指向的位置就是分界點元素應該在的最終位置。
【舉個栗子】
對於待排序列3,1,4,2
先看圖:
首先選取首元素3做爲目標元素,並將其保存在臨時變量target
中。起始left
指向3,right
指向2。開始移動右指針,發現2比目標元素3小,則將2移動到left
指向的位置,注意此時left
向右移動一位,指向1。右指針中止。開始移動左指針,1比3小符合要求,繼續移動,發現4比3大,不符合要求,將4移動到right
位置(即原來2的位置,同理right
也左移一位),left
指針中止。
從新準備移動右指針,發現right
與left
重合則第一次分區結束。3找到了它的最終位置,即left
,right
重合的位置,將3移動到該位置。此時序列爲2,1,3,4。
繼續遞歸重複上述操做。
這裏簡單介紹一下快速排序能夠作的一些優化
QuickSortImpl
進行遞歸,其實第二次調用可使用循環代替,即改成 left = pivot + 1
插入排序主要包括兩種排序算法,分別是簡單插入排序和和希爾排序
每次將一個無序區的元素,按其大小找到其在有序區中的位置,並插入到該位置,直到所有元素插完爲止。
插入排序不是經過交換位置而是經過比較找到合適的位置插入元素來達到排序的目的
public void SimpleInsertionSort(int[] array){ for(int i = 1; i < array.Length; i ++){ int j = i; int target = array[j]; while(j > 0 && target < array[j - 1]){ array[j] = array[j - 1]; j --; } array[j] = target; } }
【算法解讀】
算法默認待排序列的第一個元素是排好序的,處於有序區。從第二個元素開始,直到到末尾元素,依次做爲目標元素(對應代碼:for(int i = 1; i < array.Length; i ++)
),向有序區中插入。那麼如何插入呢?將目標元素依次和有序區的元素進行比較,若目標元素小於該元素,則目標元素就應處於該元素的前面,則該元素後移一個位置(對應代碼:array[j] = array[j - 1];
)。不斷比較直到找到不比目標元素小的元素,則目標元素就應在該元素的後面位置,將目標元素插入到該位置。繼續下一個目標元素,直到全部元素插入完畢。
在開始插入第i個元素時,前i-1個元素已是排好序的。
【舉個栗子】
對於待排序列3,1,4,2
首先認爲3是有序的,處於有序區。將1做爲目標元素,依次和有序區中的元素進行比較,和3進行比較,1<3,則3後移,有序區中沒有待比較的數據了,因此將1插入到3原來的位置。此時序列:1,3,4,2。有序區內元素爲1,3。繼續將4做爲目標元素,先和3比較,4>3,則插入到4的後面位置。此時序列1,3,4,2。此時有序區內元素爲1,3,4。繼續將2做爲目標元素,和4比較,2<4,4後移,和3比較,2<3,3後移,和1比較,2>1,則插入到1的後面。此時序列1,2,3,4。全部元素插入完畢,即排序完成。
希爾排序是插入排序的一種高效率的實現,又稱縮小增量式插入排序。它也是簡單插入排序算法的一種改進算法。
先選定一個數做爲第一個增量,將整個待排序列分割成若干個組,分組方式是將全部間隔爲增量值的元素放在同一個組內。各組內部進行直接插入排序。而後縮小增量值,重複上述分組和排序操做,直到所取增量減小爲1時,即全部元素放在同一個組中進行直接插入排序。
爲何希爾排序的時間性能是優於簡單插入排序的呢?
開始時,增量較大,每組中的元素少,所以組內的直接插入排序較快,當增量減小時,分組內的元素增長,但此時分組內的元素基本有序,因此使得組內的直接插入排序也較快,所以,希爾排序在效率上較簡單插入排序有較大的改進
public void ShellSort(int[] array){ int d = array.Length / 2; while(d >= 1){ for(int i = d; i < array.Length; i ++){ int j = i; int target = array[j]; while(j >= d && target < array[j -d]){ array[j] = array[j - d]; j -= d; } array[j] = target; } d /= 2; } }
【算法解讀】
希爾排序算法是對簡單插入排序的改進,因此若是對簡單插入排序還不夠理解的話,建議先去看一下上面的簡單插入排序算法。
算法首先取增量爲待排序列長度的一半,經過增量進行分組,每一個分組都進行簡單插入排序。而後,增量減半,重複上述操做直至增量減小爲1(對應代碼:while(d >= 1)
)。
算法的要點在於對每一個分組進行簡單插入排序。首先從i = d
位置上的元素開始,一直到待排序列的最後一個元素,依次做爲目標元素,找到它們應該在本身分組中的位置並進行插入。當目標元素位置爲i
時,則目標元素所在分組的有序區內的元素位置分別爲i-d
,i-d-d
,i-d-d-d
...直到該位置大於0。目標元素只須要和所在分組有序區內的元素進行比較,找到本身的最終位置便可。當增量減小爲1時,則至關於只有一個分組,此時就是對全部元素進行直接插入排序,完成最終的希爾排序。
【舉個栗子】
對於待排序列4,1,3,2
先看圖:
首先取增量爲序列長度4的一半,即爲2。此時的分組是(4,3),(1,2)。則從下標爲2的元素3開始做爲目標元素,下標2減去增量2,則找到目標元素3所在分組有序區內的元素4,和4進行比較,3<4,則4後移(這裏的後移都是相對於本身所在分組裏的元素),有序區內沒有其它元素,則目標元素3插入到元素4以前的位置。此時序列爲3,1,4,2。繼續從下標爲3的元素2開始做爲目標元素,找到目標元素2所在分組有序區內的元素1,比較2>1,符合預期都不須要移動。一趟希爾排序完成,序列仍爲3,1,4,2。增量再減半爲1,此時就是對全部元素進行簡單插入排序,再也不贅述。
希爾排序的關鍵點在於分組,咱們再舉個栗子看是如何分組的:
對於序列25,36,48,59,13,21,74,32,60
第一次增量爲序列長度9的一半,取下限是4,此時的分組狀況是(25,13,60)(36,21)(48,74)(59,32),以下圖:
那麼到這裏,交換排序中的冒泡排序,快速排序和插入排序中的簡單插入排序,希爾排序,就總結完了。
後面我會繼續總結選擇排序中的簡單選擇排序算法,堆排序算法,以及歸併,基數,計數排序算法,敬請期待。
上面算法的源碼都放在了GitHub上,感興趣的同窗能夠點擊這裏查看
更多算法的總結與代碼實現(不只僅是排序算法),歡迎你們查看個人GitHub倉庫Algorithm