在這篇文章中,我會向大家展示一些排序演算法的視覺化過程。我還寫了一個工具,大家可對比檢視某兩種排序演算法。
引言
首先,我認為是最重要的是要理解什麼是“排序演算法”。根據維基百科,排序演算法(Sorting algorithm)是一種能將一串資料依照特定排序方式進行排列的一種演算法。最常用到的排序方式是數值順序以及字典順序。有效的排序演算法在一些演算法(例如搜尋演算法與合併演算法)中是重要的,如此這些演算法才能得到正確解答。排序演算法也用在處理文字資料以及產生人類可讀的輸出結果。
接下來,我會說明一些演算法。所有演算法皆由C#程式碼實現,大部分的演算法思想都可以在維基百科上找到。 所呈現的演算法有:
- 雙向氣泡排序
- 氣泡排序
- 桶排序
- 梳排序
- 迴圈排序
- 地精排序
- 堆排序
- 插入排序
- 歸併排序
- 奇偶排序
- 鴿籠排序
- 快速排序
- 使用冒泡的快排
- 選擇排序
- 希爾排序
我已經決定要建立GUI視覺化的排序演算法。該專案還允許使用者儲存為GIF影象及設定動畫輸出排序速度。
使用程式碼
該解決方案由兩個專案組成。第一個專案稱為元件提供的建立GIF動畫影象類。該專案是基於NGIF專案的。關於這個專案的更多資訊可以在這裡找到。
第二個專案可以稱為排序比較,它是解決方案的主要組成部分。其中,通過一個名為frmMain的結構可以選擇排序演算法,設定你想要排序,排序的速度,排序數量,並選擇是否要建立動態圖片。在窗體上放置兩個皮膚稱為pnlSort1和pnlSort2,其中分揀視覺化的呈現方式。
每個演算法都都通過自己的排序方式進行命名,並接受一個IList引數,並返回一個IList物件。
DrawSamples方法可以在皮膚上進行繪圖。產生的隨機樣本之後就會呼叫它。通過點選隨機按鈕生成的樣本會儲存在陣列中。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
private void DrawSamples() { g.Clear(Color.White); for (int i = 0; i < array.Count; i++) { int x = (int)(((double)pnlSamples.Width / array.Count) * i); Pen pen = new Pen(Color.Black); g.DrawLine(pen, new Point(x, pnlSamples.Height), new Point(x, (int)(pnlSamples.Height - (int)array[i]))); } } |
該方法隨機產生資料放於陣列中。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public void Randomize(IList list) { for (int i = list.Count - 1; i > 0; i--) { int swapIndex = rng.Next(i + 1); if (swapIndex != i) { object tmp = list[swapIndex]; list[swapIndex] = list[i]; list[i] = tmp; } } } |
在排序過程中,當核取方塊建立的動畫被選中,陣列中兩個數交換的話就會產生影象。此影象索引從0到n,其中n代表交換的次數。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
private void SavePicture() { ImageCodecInfo myImageCodecInfo = this.getEncoderInfo("image/gif"); EncoderParameter myEncoderParameter = new EncoderParameter( System.Drawing.Imaging.Encoder.Compression, (long)EncoderValue.CompressionLZW); EncoderParameter qualityParam = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 0L); EncoderParameters myEncoderParameters = new EncoderParameters(1); EncoderParameters encoderParams = new EncoderParameters(2); encoderParams.Param[0] = qualityParam; encoderParams.Param[1] = myEncoderParameter; string destPath = System.IO.Path.Combine(txtOutputFolder.Text, imgCount + ".gif"); bmpsave.Save(destPath, myImageCodecInfo, encoderParams); imgCount++; } |
排序演算法
氣泡排序
氣泡排序也被稱為下沉排序,是一個簡單的排序演算法,通過多次重複比較每對相鄰的元素,並按規定的順序交換他們,最終把數列進行排好序。一直重複下去,直到結束。該演算法得名於較小元素“氣泡”會“浮到”列表頂部。由於只使用了比較操作,所以這是一個比較排序。
氣泡排序演算法的運作如下:
- 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
- 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。這步做完後,最後的元素會是最大的數。
- 針對所有的元素重複以上的步驟,除了最後一個。
- 持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。
由於它的簡潔,氣泡排序通常被用來對於程式設計入門的學生介紹演算法的概念。但同樣簡單的插入排序比氣泡排序效能更好,所以有些人認為不需要再教氣泡排序了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public IList BubbleSort(IList arrayToSort) { int n = arrayToSort.Count - 1; for (int i = 0; i < n; i++) { for (int j = n; j > i; j--) { if (((IComparable)arrayToSort[j - 1]).CompareTo(arrayToSort[j]) > 0) { object temp = arrayToSort[j - 1]; arrayToSort[j - 1] = arrayToSort[j]; arrayToSort[j] = temp; RedrawItem(j); RedrawItem(j - 1); pnlSamples.Refresh(); if (chkCreateAnimation.Checked) SavePicture(); } } } return arrayToSort; } |
Worst case performance: | O(n2) |
Best case performance: | O(n) |
Average case performance: | O(n2) |
Worst case space complexity: | O(1) auxiliary |
雙向氣泡排序
雞尾酒排序,也稱為雙向氣泡排序、調酒器排序、攪拌排序(可以參考選擇排序的一個變種)、紋波排序、接送排序,或歡樂時光排序。它由氣泡排序變化而來,是一種穩定的比較排序演算法。該演算法不同於氣泡排序,它在排序上由兩個方向同時進行。該排序演算法只是比氣泡排序稍難實現,但解決了氣泡排序中的“烏龜問題”(陣列尾部的小值)。
首先從左向右方向為大元素移動方向,從右向左方向為小元素移動方向,然後每個元素都依次執行。在第 i 次移動後,前 i 個元素和後個 i 元素都放到了正確的位置,也不需要在檢查一次。每次縮短已排序的那部分列表,都可以減半操作次數。
但是在亂數序列的狀態下,雙向氣泡排序與氣泡排序的效率都很差勁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
public IList BiDerectionalBubleSort(IList arrayToSort) { int limit = arrayToSort.Count; int st = -1; bool swapped = false; do { swapped = false; st++; limit--; for (int j = st; j < limit; j++) { if (((IComparable)arrayToSort[j]).CompareTo(arrayToSort[j + 1]) > 0) { object temp = arrayToSort[j]; arrayToSort[j] = arrayToSort[j + 1]; arrayToSort[j + 1] = temp; swapped = true; RedrawItem(j); RedrawItem(j + 1); pnlSamples.Refresh(); if(chkCreateAnimation.Checked) SavePicture(); } } for (int j = limit - 1; j >= st; j--) { if (((IComparable)arrayToSort[j]).CompareTo(arrayToSort[j + 1]) > 0) { object temp = arrayToSort[j]; arrayToSort[j] = arrayToSort[j + 1]; arrayToSort[j + 1] = temp; swapped = true; RedrawItem(j); RedrawItem(j + 1); pnlSamples.Refresh(); if (chkCreateAnimation.Checked) SavePicture(); } } } while (st < limit && swapped); return arrayToSort; } |
Worst case performance: | O(n2) |
Best case performance: | O(n) |
Average case performance: | O(n2) |
Worst case space complexity: | O(1) auxiliary |
桶排序
桶排序,或稱為箱排序,是一種把數列劃分成若干個桶的排序演算法。在每個桶內各自排序,或者使用不同的排序演算法,或通過遞迴方式繼續使用桶排序演算法。這是一個分佈排序,是最能體現出數字意義的一種基數排序。桶排序是鴿巢排序的一種歸納結果。當要被排序的陣列內的數值是均勻分配的時候,桶排序使用線性時間(Θ(n))。但桶排序並不是 比較排序,他不受到 O(n log n) 下限的影響。
桶排序的流程:
- 設定一個定量的陣列當作空桶子。
- 尋訪序列,並且把專案一個一個放到對應的桶子去。
- 對每個不是空的桶子進行排序。
- 從不是空的桶子裡把專案再放回原來的序列中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
public IList BucketSort(IList arrayToSort) { if (arrayToSort == null || arrayToSort.Count == 0) return arrayToSort; object max = arrayToSort[0]; object min = arrayToSort[0]; for (int i = 0; i 0) { max = arrayToSort[i]; } if (((IComparable)arrayToSort[i]).CompareTo(min) < 0) { min = arrayToSort[i]; } } ArrayList[] holder = new ArrayList[(int)max - (int)min + 1]; for (int i = 0; i < holder.Length; i++) { holder[i] = new ArrayList(); } for (int i = 0; i < arrayToSort.Count; i++) { holder[(int)arrayToSort[i] - (int)min].Add(arrayToSort[i]); } int k = 0; for (int i = 0; i 0) { for (int j = 0; j < holder[i].Count; j++) { arrayToSort[k] = holder[i][j]; RedrawItem(k); pnlSamples.Refresh(); if (chkCreateAnimation.Checked) SavePicture(); k++; } } } return arrayToSort; } |
Worst case performance: | O(n2.k) |
Best case performance: | – |
Average case performance: | O(n.k) |
Worst case space complexity: | O(n.k) |
梳排序
梳排序是一個相對簡單的排序演算法,最初它由Wlodzimierz Dobosiewicz於1980年設計出來。後來由斯蒂芬和理查德重新發現和推廣。他們的文章在1991年4月發表在位元組雜誌上。梳排序改善了氣泡排序和類似快速排序的競爭演算法。其要旨在於消除烏龜,亦即在陣列尾部的小數值,這些數值是造成氣泡排序緩慢的主因。相對地,兔子,亦即在陣列前端的大數值,不影響氣泡排序的效能。
在氣泡排序中,任何兩個元素進行比較時,他們總是距離彼此為1。梳排序的基本思想是可以不是1。
梳排序中,開始時的間距設定為列表長度,然後每一次都會除以損耗因子(一般為1.3)。必要的時候,間距可以四捨五入,不斷重複,直到間距變為1。然後間距就保持為1,並排完整個數列。最後階段就相當於一個氣泡排序,但此時大多數烏龜已經處理掉,此時的氣泡排序就高效了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
public IList CombSort(IList arrayToSort) { int gap = arrayToSort.Count; int swaps = 0; do { gap = (int)(gap / 1.247330950103979); if (gap 0) { object temp = arrayToSort[i]; arrayToSort[i] = arrayToSort[i + gap]; arrayToSort[i + gap] = temp; RedrawItem(i); RedrawItem(i + gap); pnlSamples.Refresh(); if (chkCreateAnimation.Checked) SavePicture(); swaps = 1; } i++; } while (!(i + gap >= arrayToSort.Count)); } while (!(gap == 1 && swaps == 0)); return arrayToSort; } |
Worst case performance: | – |
Best case performance: | – |
Average case performance: | – |
Worst case space complexity: | O(1) |
圈排序
Cycle sort is an in-place, unstable sorting algorithm, a comparison sort that is theoretically optimal in terms of the total number of writes to the original array, unlike any other in-place sorting algorithm. It is based on the idea that the permutation to be sorted can be factored into cycles, which can individually be rotated to give a sorted result.
Unlike nearly every other sort, items are never written elsewhere in the array simply to push them out of the way of the action. Each value is either written zero times, if it’s already in its correct position, or written one time to its correct position. This matches the minimal number of overwrites required for a completed in-place sort.
它是一個就地、不穩定的排序演算法,根據原始的陣列,一種理論上最優的比較,並且與其它就地排序演算法不同。它的思想是把要排的數列分解為圈,即可以分別旋轉得到排序結果。
不同於其它排序的是,元素不會被放入陣列的中任意位置從而推動排序。每個值如果它已經在其正確的位置則不動,否則只需要寫一次即可。也就是說僅僅最小覆蓋就能完成排序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
public IList CycleSort(IList arrayToSort) { int writes = 0; for (int cycleStart = 0; cycleStart < arrayToSort.Count; cycleStart++) { object item = arrayToSort[cycleStart]; int pos = cycleStart; do { int to = 0; for (int i = 0; i < arrayToSort.Count; i++) { if (i != cycleStart && ((IComparable)arrayToSort[i]).CompareTo(item) < 0) { to++; } } if (pos != to) { while (pos != to && ((IComparable)item).CompareTo(arrayToSort[to]) == 0) { to++; } object temp = arrayToSort[to]; arrayToSort[to] = item; RedrawItem(to); item = temp; RedrawItem(cycleStart); pnlSamples.Refresh(); if (chkCreateAnimation.Checked) SavePicture(); writes++; pos = to; } } while (cycleStart != pos); } return arrayToSort; } |
Worst case performance: | O(n2) |
Best case performance: | – |
Average case performance: | O(n2) |
Worst case space complexity: | O(1) |
地精排序
地精排序(gnome sorting,大部分地方翻成“侏儒排序”,“地精排序”明明更好聽呀,本文就這麼用了。)最初由哈米德在2000年的時候提出,當時稱為傻瓜排序,之後被迪克說明並且命名為“地精排序”。除了某個元素是經過一系列的互換(類似氣泡排序)才到了它的正確位置之外,它和插入排序挺相似。
它在概念上很簡單,不需要巢狀迴圈。執行時間是O(n2),如果列表基本有序,則時間為O(n)。實際上,它和插入排序一樣,平均執行時O(n2)。
The algorithm always finds the first place where two adjacent elements are in the wrong order, and swaps them. It takes advantage of the fact that performing a swap can introduce a new out-of-order adjacent pair only right before or after the two swapped elements. It does not assume that elements forward of the current position are sorted, so it only needs to check the position directly before the swapped elements.
地精演算法總是發現第一個 【兩個相鄰元素存在錯誤的順序】,然後把他們交換。原理是,交換一對亂序元素後,會產生一對新的無序相鄰元素,而這兩個元素要麼交換前有序,要麼交換後有序。它不認為元素當前的位置是有序的,所以它只需要在交換元素前直接檢查位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public IList GnomeSort(IList arrayToSort) { int pos = 1; while (pos = 0) { pos++; } else { object temp = arrayToSort[pos]; arrayToSort[pos] = arrayToSort[pos - 1]; RedrawItem(pos); arrayToSort[pos - 1] = temp; RedrawItem(pos - 1); RefreshPanel(pnlSamples); if (savePicture) SavePicture(); if (pos > 1) { pos--; } } } return arrayToSort; } |
Worst case performance: | O(n2) |
Best case performance: | – |
Average case performance: | – |
Worst case space complexity: | O(1) |
堆排序
堆排序是從資料集構建一個資料堆,然後提取最大元素,把它放到部分有序的陣列的末尾。提取最大元素後,重新構建新堆,然後又接著提取新堆這的最大元素。重複這個過程,直到堆中沒有元素並且陣列已排好。堆排序的基本實現需要兩個陣列:一個用於構建堆,另一個是存放有序的元素。
堆排序把輸入陣列插入到一個二叉堆的資料結構中。最大值(大根堆)或最小值(小根堆)會被提取出來,直到堆空為止,提取出來的元素,是已經排好序的。每次提取後,堆中沒變換的元素依然保留了,所以(堆排序的)唯一消耗就是提取過程。
在提取過程中,所需要的空間,就是用於存放堆的空間。為了實現恆定的空間開銷,堆是儲存在輸入陣列中還沒有完成排序的那部分空間中。堆排序使用了兩個堆操作:插入和根刪除。每個提取的元素放到陣列的最後一個空位置。陣列剩餘位置存放待排元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
public IList HeapSort(IList list) { for (int i = (list.Count - 1) / 2; i >= 0; i--) Adjust(list, i, list.Count - 1); for (int i = list.Count - 1; i >= 1; i--) { object Temp = list[0]; list[0] = list[i]; list[i] = Temp; RedrawItem(0); RedrawItem(i); pnlSamples.Refresh(); if (chkCreateAnimation.Checked) SavePicture(); Adjust(list, 0, i - 1); } return list; } public void Adjust(IList list, int i, int m) { object Temp = list[i]; int j = i * 2 + 1; while (j <= m) { if (j < m) if (((IComparable)list[j]).CompareTo(list[j + 1]) < 0) j = j + 1; if (((IComparable)Temp).CompareTo(list[j]) < 0) { list[i] = list[j]; RedrawItem(i); pnlSamples.Refresh(); if (chkCreateAnimation.Checked) SavePicture(); i = j; j = 2 * i + 1; } else { j = m + 1; } } list[i] = Temp; RedrawItem(i); pnlSamples.Refresh(); if (chkCreateAnimation.Checked) SavePicture(); } |
Worst case performance: | O(n log n) |
Best case performance: | O(n log n) |
Average case performance: | O(n log n) |
Worst case space complexity: | O(1) |
插入排序
插入排序的工作原理是通過構建有序序列,對於未排序資料,在已排序序列中從後向前掃描,找到相應位置並插入。插入排序在實現上,通常採用in-place排序(即只需用到O(1)的額外空間的排序),因而在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,為最新元素提供插入空間。
具體演算法描述如下:
- 從第一個元素開始,該元素可以認為已經被排序
- 取出下一個元素,在已經排序的元素序列中從後向前掃描
- 如果該元素(已排序)大於新元素,將該元素移到下一位置
- 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置
- 將新元素插入到該位置後
- 重複步驟2~5
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
public IList InsertionSort(IList arrayToSort) { for (int i = 1; i < arrayToSort.Count; i++) { object val = arrayToSort[i]; int j = i - 1; bool done = false; do { if (((IComparable)arrayToSort[j]).CompareTo(val) > 0) { arrayToSort[j + 1] = arrayToSort[j]; RedrawItem(j + 1); pnlSamples.Refresh(); if (chkCreateAnimation.Checked) SavePicture(); j--; if (j < 0) { done = true; } } else { done = true; } } while (!done); arrayToSort[j + 1] = val; RedrawItem(j + 1); pnlSamples.Refresh(); if (chkCreateAnimation.Checked) SavePicture(); } return arrayToSort; } |
Worst case performance: | O(n2) |
Best case performance: | O(n) |
Average case performance: | O(n2) |
Worst case space complexity: | O(1) |
歸併排序
從概念上講,歸併排序的工作原理如下:
- 如果列表的長度是0或1,那麼它已經有序。否則:
- 未排序的部分平均劃分為兩個子序列。
- 每個子序列,遞迴使用歸併排序。
- 合併兩個子列表,使之整體有序。
歸併排序包含兩個主要觀點,以改善其執行時:
- 一個小列表排序的花費少於大列表。
- 把兩個有序表合併,要比直接排列一個無序表花費更少的步驟。例如,您只需要遍歷每個有序列表一次即可(見下面的合併功能)。
歸併操作的過程如下:
- 申請空間,使其大小為兩個已經排序序列之和,該空間用來存放合併後的序列
- 設定兩個指標,最初位置分別為兩個已經排序序列的起始位置
- 比較兩個指標所指向的元素,選擇相對小的元素放入到合併空間,並移動指標到下一位置
- 重複步驟3直到某一指標到達序列尾
- 將另一序列剩下的所有元素直接複製到合併序列尾
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
public IList MergeSort(IList a, int low, int height) { int l = low; int h = height; if (l >= h) { return a; } int mid = (l + h) / 2; MergeSort(a, l, mid); MergeSort(a, mid + 1, h); int end_lo = mid; int start_hi = mid + 1; while ((l <= end_lo) && (start_hi <= h)) { if (((IComparable)a[l]).CompareTo(a[start_hi]) < 0) { l++; } else { object temp = a[start_hi]; for (int k = start_hi - 1; k >= l; k--) { a[k + 1] = a[k]; RedrawItem(k + 1); pnlSamples.Refresh(); if (chkCreateAnimation.Checked) SavePicture(); } a[l] = temp; RedrawItem(l); pnlSamples.Refresh(); if (chkCreateAnimation.Checked) SavePicture(); l++; end_lo++; start_hi++; } } return a; } |
Worst case performance: | O(n log n) |
Best case performance: | O(n log n) |
Average case performance: | O(n log n) |
Worst case space complexity: |
奇偶排序
Odd-even sort is a relatively simple sorting algorithm. It is a comparison sort based on bubble sort with which it shares many characteristics. It functions by comparing all (odd, even)-indexed pairs of adjacent elements in the list and, if a pair is in the wrong order (the first is larger than the second) the elements are switched. The next step repeats this for (even, odd)-indexed pairs (of adjacent elements). Then it alternates between (odd, even) and (even, odd) steps until the list is sorted. It can be thought of as using parallel processors, each using bubble sort but starting at different points in the list (all odd indices for the first step). This sorting algorithm is only marginally more difficult than bubble sort to implement.
奇偶排序是一個相對簡單的排序演算法。它是一種基於氣泡排序的比較演算法,它們有著許多共同點。它通過比較所有相鄰的(奇數偶)對進行排序,如果某對存在錯誤的順序(第一個元素大於第二個),則交換。下一步針對{偶奇對}重複這一操作。然後序列就在(奇,偶)和(偶,奇)之間變換,直到列表有序。它可以看作是是使用並行處理器,每個都用了氣泡排序,但只是起始點在列表的不同位置(所有奇數位置可做第一步)。這個排序演算法實現起來只是略微比氣泡排序複雜一些。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
public IList OddEvenSort(IList arrayToSort) { bool sorted = false; while (!sorted) { sorted = true; for (var i = 1; i < arrayToSort.Count - 1; i += 2) { if (((IComparable)arrayToSort[i]).CompareTo(arrayToSort[i + 1]) > 0) { object temp = arrayToSort[i]; arrayToSort[i] = arrayToSort[i + 1]; RedrawItem(i); arrayToSort[i + 1] = temp; RedrawItem(i+1); pnlSamples.Refresh(); if (chkCreateAnimation.Checked) SavePicture(); sorted = false; } } for (var i = 0; i < arrayToSort.Count - 1; i += 2) { if (((IComparable)arrayToSort[i]).CompareTo(arrayToSort[i + 1]) > 0) { object temp = arrayToSort[i]; arrayToSort[i] = arrayToSort[i + 1]; arrayToSort[i + 1] = temp; RedrawItem(i); RedrawItem(i+1); pnlSamples.Refresh(); if (chkCreateAnimation.Checked) SavePicture(); sorted = false; } } } return arrayToSort; } |
鴿巢排序
鴿巢排序,也被稱為 count sort (不要和 counting sort 搞混了),當陣列元素的元素數量(n)和可能的鍵值數(key value,N)大約相同時,這種排序演算法實用。它的時間複雜度為O(n+N)。
(在不可避免遍歷每一個元素並且排序的情況下效率最好的一種排序演算法。但它只有在差值(或者可被對映在差值)很小的範圍內的數值排序的情況下實用。)
演算法流程如下:
- Given an array of values to be sorted, set up an auxiliary array of initially empty “pigeonholes”, one pigeonhole for each key through the range of the original array. 假設有個一個待排序的陣列,給它建立了一個空的輔助陣列(稱為“鴿巢”)。把原始陣列中的每個值作為一個key(“格子”)。
- Going over the original array, put each value into the pigeonhole corresponding to its key, such that each pigeonhole eventually contains a list of all values with that key. 遍歷原始陣列,根據每個值放入輔助陣列對應的“格子”
- Iterate over the pigeonhole array in order, and put elements from non-empty pigeonholes back into the original array. 順序遍歷“鴿巢”陣列(輔助陣列),把非空鴿巢中的元素放回原始陣列。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
public IList PigeonHoleSort(IList list) { object min = list[0], max = list[0]; foreach (object x in list) { if (((IComparable)min).CompareTo(x) > 0) { min = x; } if (((IComparable)max).CompareTo(x) < 0) { max = x; } Thread.Sleep(speed); } int size = (int)max - (int)min + 1; int[] holes = new int[size]; foreach (int x in list) holes[x - (int)min]++; int i = 0; for (int count = 0; count < size; count++) while (holes[count]-- > 0) { list[i] = count + (int)min; RedrawItem(i); i++; RefreshPanel(pnlSamples); Thread.Sleep(speed); } return list; } |
Worst case performance: | O(n+2k) |
Best case performance: | – |
Average case performance: | O(n+2k) |
Worst case space complexity: | O(2k) |
快速排序
快速排序採用分而治之的策略,把一個列表劃分為兩個子列表。步驟是:
- 從列表中,選擇一個元素,稱為基準(pivot)。
- 重新排序列表,把所有數值小於樞軸的元素排到基準之前,所有數值大於基準的排基準之後(相等的值可以有較多的選擇)。在這個分割槽退出之後,該基準就處於數列的中間位置。這個稱為分割槽(partition)操作。
- 分別遞迴排序較大元素的子列表和較小的元素的子列表。
遞迴的結束條件是列表元素為零或一個,即已不需要排序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
public IList QuickSort(IList a, int left, int right) { int i = left; int j = right; double pivotValue = ((left + right) / 2); int x = (int)a[int.Parse(pivotValue.ToString())]; while (i <= j) { while (((IComparable)a[i]).CompareTo(x) < 0) { i++; } while (((IComparable)x).CompareTo(a[j]) < 0) { j--; } if (i <= j) { object temp = a[i]; a[i] = a[j]; RedrawItem(i); i++; a[j] = temp; RedrawItem(j); j--; pnlSamples.Refresh(); if (chkCreateAnimation.Checked) SavePicture(); } } if (left < j) { QuickSort(a, left, j); } if (i < right) { QuickSort(a, i, right); } return a; } |
Worst case performance: | O(n2) |
Best case performance: | O(n log n) |
Average case performance: | O(n log n) |
Worst case space complexity: | O(log n) |
使用冒泡的快速排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
public IList BubbleSort(IList arrayToSort, int left, int right) { for (int i = left; i < right; i++) { for (int j = right; j > i; j--) { if (((IComparable)arrayToSort[j - 1]).CompareTo(arrayToSort[j]) > 0) { object temp = arrayToSort[j - 1]; arrayToSort[j - 1] = arrayToSort[j]; RedrawItem(j-1); arrayToSort[j] = temp; RedrawItem(j); pnlSamples.Refresh(); if (chkCreateAnimation.Checked) SavePicture(); } } } return arrayToSort; } public IList QuickSortWithBubbleSort(IList a, int left, int right) { int i = left; int j = right; if (right - left <= 6) { BubbleSort(a, left, right); return a; } double pivotValue = ((left + right) / 2); int x = (int)a[int.Parse(pivotValue.ToString())]; a[(left + right) / 2] = a[right]; a[right] = x; RedrawItem(right); pnlSamples.Refresh(); if (chkCreateAnimation.Checked) SavePicture(); while (i <= j) { while (((IComparable)a[i]).CompareTo(x) < 0) { i++; } while (((IComparable)x).CompareTo(a[j]) < 0) { j--; } if (i <= j) { object temp = a[i]; a[i++] = a[j]; RedrawItem(i - 1); a[j--] = temp; RedrawItem(j + 1); pnlSamples.Refresh(); if (chkCreateAnimation.Checked) SavePicture(); } } if (left < j) { QuickSortWithBubbleSort(a, left, j); } if (i < right) { QuickSortWithBubbleSort(a, i, right); } return a; } |
選擇排序
原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。
演算法過程如下:
- 找到列表中的最小值,
- 把它和第一個位置的元素交換,
- 列表其餘部分重複上面的步驟(從第二個位置開始,且每次加1).
列表被有效地分為兩個部分:從左到右的有序部分,和餘下待排序部分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public IList SelectionSort(IList arrayToSort) { int min; for (int i = 0; i < arrayToSort.Count; i++) { min = i; for (int j = i + 1; j < arrayToSort.Count; j++) { if (((IComparable)arrayToSort[j]).CompareTo(arrayToSort[min]) < 0) { min = j; } } object temp = arrayToSort[i]; arrayToSort[i] = arrayToSort[min]; arrayToSort[min] = temp; RedrawItem(i); RedrawItem(min); pnlSamples.Refresh(); if (chkCreateAnimation.Checked) SavePicture(); } return arrayToSort; } |
Worst case performance: | O(n2) |
Best case performance: | O(n2) |
Average case performance: | O(n2) |
Worst case space complexity: | O(1) |
希爾排序
希爾排序通過將比較的全部元素分為幾個區域來提升插入排序的效能。這樣可以讓一個元素可以一次性地朝最終位置前進一大步。然後演算法再取越來越小的步長進行排序,演算法的最後一步就是普通的插入排序,但是到了這步,需排序的資料幾乎是已排好的了(此時插入排序較快)。
假設有一個很小的資料在一個已按升序排好序的陣列的末端。如果用複雜度為O(n2)的排序(氣泡排序或插入排序),可能會進行n次的比較和交換才能將該資料移至正確位置。而希爾排序會用較大的步長移動資料,所以小資料只需進行少數比較和交換即可到正確位置。
一個更好理解的希爾排序實現:將陣列列在一個表中並對列排序(用插入排序)。重複這過程,不過每次用更長的列來進行。最後整個表就只有一列了。將陣列轉換至表是為了更好地理解這演算法,演算法本身僅僅對原陣列進行排序(通過增加索引的步長,例如是用i += step_size
而不是i++
)。
例如,假設有這樣一組數[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],如果我們以步長為5開始進行排序,我們可以通過將這列表放在有5列的表中來更好地描述演算法,這樣他們就應該看起來是這樣:
1 2 3 4 |
13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 |
然後我們對每列進行排序:
1 2 3 4 |
10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 |
將上述四行數字,依序接在一起時我們得到:[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ].這時10已經移至正確位置了,然後再以3為步長進行排序:
1 2 3 4 5 6 |
10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 |
排序之後變為:
1 2 3 4 5 6 |
10 14 13 25 23 33 27 25 59 39 65 73 45 94 82 94 |
最後以1步長進行排序(此時就是簡單的插入排序了)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
public IList ShellSort(IList arrayToSort) { int i, j, increment; object temp; increment = arrayToSort.Count / 2; while (increment > 0) { for (i = 0; i < arrayToSort.Count; i++) { j = i; temp = arrayToSort[i]; while ((j >= increment) && (((IComparable)arrayToSort[j - increment]).CompareTo(temp) > 0)) { arrayToSort[j] = arrayToSort[j - increment]; RedrawItem(j); pnlSamples.Refresh(); if (chkCreateAnimation.Checked) SavePicture(); j = j - increment; } arrayToSort[j] = temp; RedrawItem(j); pnlSamples.Refresh(); if (chkCreateAnimation.Checked) SavePicture(); } if (increment == 2) increment = 1; else increment = increment * 5 / 11; } return arrayToSort; } |
Worst case performance: | – |
Best case performance: | n |
Average case performance: | O(n log2 n) |
Worst case space complexity: | O(1) |
最後,感謝給與我幫助的人,有了你們的幫助,本文的質量有了更大的提高,謝謝!