影響排序性能的要素:
時間性能;
輔助空間
算法的復雜度
簡單排序【n^2】
算法思想:
第一趟:
從第一個記錄開始,通過n-1次關鍵字比較,從n個記錄中選出最小的並和第一個記錄交換;
第二趟:
從第二個記錄開始,通過n-2次關鍵字比較,從n -1個記錄中選出最小的並和第二個記錄交換;
復雜度穩穩的是0(n2),幾乎被拋棄
1 void SimpleSort() 2 { 3 for (int i = 0; i < n; ++i) 4 { 5 int minV = v[i], index = i; 6 for (int j = i + 1; j < n; ++j) 7 { 8 if (minV > v[j]) 9 { 10 minV = v[j]; 11 index = j; 12 } 13 } 14 swap(v[i], v[index]); 15 } 16 }
冒泡排序
冒泡排序(Bubble Sort),是一種計算機科學領域的較簡單的排序算法。
它重復地走訪過要排序的元素列,依次比較兩個相鄰的元素,如果他們的順序(如從大到小、首字母從A到Z)錯誤就把他們交換過來。走訪元素的工作是重復地進行直到沒有相鄰元素需要交換,也就是說該元素列已經排序完成。
這個算法的名字由來是因為越大的元素會經由交換慢慢“浮”到數列的頂端(升序或降序排列),故名“冒泡排序”。每一次排序都將最大的數排到最后【最前】
算法穩定性:
冒泡排序就是把小的元素往前調或者把大的元素往后調。比較是相鄰的兩個元素比較,交換也發生在這兩個元素之間。所以,如果兩個元素相等,是不會再交換的;如果兩個相等的元素沒有相鄰,那么即使通過前面的兩兩交換把兩個相鄰起來,這時候也不會交換,所以相同元素的前后順序並沒有改變,所以冒泡排序是一種穩定排序算法。
1 void bubble_sort(T arr[], int len) 2 { 3 int i, j; T temp; 4 for (i = 0; i < len; i++) 5 for (j = 0; j < len - i - 1; j++)//之所以是-i,是因為每一次j的循環都將最大的數冒出到最后,所以后面的i個數是已經完成了排序的 6 if (arr[j] > arr[j + 1]) 7 { 8 temp = arr[j]; 9 arr[j] = arr[j + 1]; 10 arr[j + 1] = temp; 11 } 12 }
改進:
使用一個標記,一旦某次j循環遍歷中沒有進行數據交換,那么數據是提前有序了,使用flag進行標記提前結束循環。
選擇排序
選擇排序(Selection sort)是一種簡單直觀的排序算法。
它的工作原理是:第一次從待排序的數據元素中選出最小(或最大)的一個元素,存放在序列的起始位置,然后再從剩余的未排序元素中尋找到最小(大)元素,然后放到已排序的序列的末尾。以此類推,直到全部待排序的數據元素的個數為零。選擇排序是不穩定的排序方法。【好像就是前面的簡單排序】
穩定性:
選擇排序是給每個位置選擇當前元素最小的,比如給第一個位置選擇最小的,在剩余元素里面給第二個元素選擇第二小的,依次類推,直到第n-1個元素,第n個元素不用選擇了,因為只剩下它一個最大的元素了。那么,在一趟選擇,如果一個元素比當前元素小,而該小的元素又出現在一個和當前元素相等的元素后面,那么交換后穩定性就被破壞了。比較拗口,舉個例子,序列5 8 5 2 9,我們知道第一遍選擇第1個元素5會和2交換,那么原序列中兩個5的相對前后順序就被破壞了,所以選擇排序是一個不穩定的排序算法。
1 void SelectSort() 2 { 3 for (int i = 0; i < n; ++i) 4 { 5 int index = i; 6 for (int j = i + 1; j < n; ++j) 7 if (v[index] > v[j]) 8 index = j; 9 if (index != i) 10 { 11 int temp = v[i]; 12 v[i] = v[index]; 13 v[index] = temp; 14 } 15 } 16 }
插入排序
插入排序(Insertion sort)是一種簡單直觀且穩定的排序算法。如果有一個已經有序的數據序列,要求在這個已經排好的數據序列中插入一個數,但要求插入后此數據序列仍然有序,這個時候就要用到一種新的排序方法——插入排序法,插入排序的基本操作就是將一個數據插入到已經排好序的有序數據中,從而得到一個新的、個數加一的有序數據,算法適用於少量數據的排序,時間復雜度為O(n^2)。是穩定的排序方法。
插入算法把要排序的數組分成兩部分:第一部分包含了這個數組的所有元素,但將最后一個元素除外(讓數組多一個空間才有插入的位置),而第二部分就只包含這一個元素(即待插入元素)。在第一部分排序完成后,再將這個最后元素插入到已排好序的第一部分中。
插入排序的基本思想是:
一般來說,插入排序都采用in-place在數組上實現。具體算法描述如下:
·從第一個元素開始,該元素可以認為已經被排序;
·取出下一個元素,在已經排序的元素序列中從后向前掃描;
·如果該元素(已排序)大於新元素,將該元素移到下一位置;
·重復步驟3,直到找到已排序的元素小於或者等於新元素的位置;
·將新元素插入到該位置后;
直接插入排序:
直接插入排序是一種簡單的插入排序法,其基本思想是:把待排序的記錄按其關鍵碼值的大小逐個插入到一個已經排好序的有序序列中,直到所有的記錄插入完為止,得到一個新的有序序列。
直接插入排序的算法思路:
(1) 設置監視哨temp,將待插入記錄的值賦值給temp;
(2) 設置開始查找的位置j = i-1,從判斷比較的i位置的前一個數開始比較;
(3) 在數組中進行搜索,搜索中將第j個記錄后移,直至temp≥r[j].key為止;
(4) 將temp插入r[j+1]的位置上。
直接插入排序算法:
1 void InsertSort() 2 { 3 int temp;//臨時哨兵 4 for (int i = 1; i < n; ++i) 5 { 6 temp = v[i]; 7 int j = i - 1; 8 for (; j >= 0; --j)//從i的前一位開始從后向前比較 9 if (temp < v[j]) 10 v[j + 1] = v[j];//向后移 11 else 12 break;//找到位置了 13 v[j+1] = temp;//注意,j前移動了 14 } 15 }
算法的基本過程:折半插入排序(二分插入排序)
(1)計算 0 ~ i-1 的中間點,用 i 索引處的元素與中間值進行比較,如果 i 索引處的元素大,說明要插入的這個元素應該在中間值和剛加入i索引之間,反之,就是在剛開始的位置到中間值的位置,這樣很簡單的完成了折半;
(2)在相應的半個范圍里面找插入的位置時,不斷的用(1)步驟縮小范圍,不停的折半,范圍依次縮小為 1/2 1/4 1/8 .......快速的確定出第 i 個元素要插在什么地方;
(3)確定位置之后,將整個序列后移,並將元素插入到相應位置。
1 #include<iterator> 2 template<typename biIter> 3 void insertion_sort (biIter begin, biIter end) 4 { 5 typedef typename std::iterator_traits<biIter>::value_type value_type; 6 biIter bond = begin; 7 std::advance(bond, 1); 8 for (; bond != end; std::advance(bond, 1)) 9 { 10 value_type key = *bond; 11 biIter ins = bond; 12 biIter pre = ins; 13 std::advance(pre, -1); 14 while (ins != begin && *pre > key) 15 { 16 *ins = *pre; 17 std::advance(ins, -1); 18 std::advance(pre, -1); 19 } 20 *ins = key; 21 } 22 }
希爾排序
希爾排序(Shell's Sort)是插入排序的一種又稱“縮小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一種更高效的改進版本。希爾排序是非穩定排序算法。該方法因D.L.Shell於1959年提出而得名。
希爾排序是把記錄按下標的一定增量分組,對每組使用直接插入排序算法排序;隨着增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至1時,整個文件恰被分成一組,算法便終止。
先取一個小於n的整數d1作為第一個增量,把文件的全部記錄分組。所有距離為d1的倍數的記錄放在同一個組中。先在各組內進行直接插入排序;
然后,取第二個增量d2<d1重復上述的分組和排序,直至所取的增量 =1( < …<d2<d1),即所有記錄放在同一組中進行直接插入排序為止。該方法實質上是一種分組插入方法
一般的初次取序列的一半為增量,以后每次減半,直到增量為1。
穩定性:
由於多次插入排序,我們知道一次插入排序是穩定的,不會改變相同元素的相對順序,但在不同的插入排序過程中,相同的元素可能在各自的插入排序中移動,最后其穩定性就會被打亂,所以shell排序是不穩定的。
1 void ShellSort() 2 { 3 for (int gap = n / 2; gap > 0; gap /= 2)//gap為組的跨度,初始取長度的一半,此后每一次都折半取 4 { 5 //對於每個跨度為gap的數據進行插入排序 6 for (int i = 0; i < gap; ++i)//每次i與跨度為gap的j一起比較 7 { 8 for (int j = i + gap; j < n; j+=gap)//j對應的是i的跨度為gap的數值 9 { 10 if (v[j] < v[j - gap])//后比前小,應該向前插入 11 { 12 int k = j - gap, temp = v[j];//temp哨兵 13 while (k >= 0 && v[k] > temp) 14 { 15 v[k + gap] = v[k];//后移 16 k -= gap; 17 } 18 19 v[k + gap] = temp;//k-gap了,故需加上gap 20 } 21 } 22 } 23 } 24 }
歸並排序
歸並排序(MERGE-SORT)是建立在歸並操作上的一種有效的排序算法,該算法是采用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合並,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合並成一個有序表,稱為二路歸並。
歸並操作(merge),也叫歸並算法,指的是將兩個順序序列合並成一個順序序列的方法。
算法穩定性:
在歸並排序中,相等的元素的順序不會改變,所以它是穩定的算法。
歸並操作的工作原理如下:[數組1小,數組1的數字上,否則數組2的數上]
第一步:申請空間,使其大小為兩個已經排序序列之和,該空間用來存放合並后的序列
第二步:設定兩個指針,最初位置分別為兩個已經排序序列的起始位置
第三步:比較兩個指針所指向的元素,選擇相對小的元素放入到合並空間,並移動指針到下一位置
重復步驟3直到某一指針超出序列尾,將另一序列剩下的所有元素直接復制到合並序列尾
兩個數組進行歸並:
1 //兩個數組進行歸並 2 void Merge(int iL, int iR, int jL, int jR)//此處的L,R分別為兩個小數組的邊界 3 { 4 vector<int>temp(jR - iL + 1);//開辟大小為兩個數組的大小空間 5 int k = 0, i = iL, j = jL; 6 while (i <= iR && j <= jR) 7 { 8 if (v[i] <= v[j])//此處的等號保證了算法的穩定性,使得相同數值前后位置不變 9 temp[k++] = v[i++]; 10 else 11 temp[k++] = v[j++]; 12 } 13 while (i <= iR)//數組1未完 14 temp[k++] = v[i++]; 15 while (j <= jR)//數組2未完 16 temp[k++] = v[j++]; 17 18 for (i = iL, k = 0; i <= jR; ++k, ++i) 19 20 v[i] = temp[k]; 21 22 } 23 24
自底向上:非遞歸版【小數組到大數組】 :
1 void MergeSort() 2 { 3 //step為小數組的大小,此處step的代表為兩個小數組的大小,故定是2的倍數 4 for (int step = 2; step / 2 < n; step *= 2)//1&1組,2&2組,4&4組。。。。 5 {//一定是從1與1的數組開始!!!不然就沒法保證排序了 6 for (int i = 0; i < n; i += step) 7 //sort(v + i, v + min(i + step, n));//直接使用自帶的sort函數進行排序 8 if ((i + step / 2 ) < n)//中間節點 9 Merge(i, i + step / 2 - 1, i + step / 2, min(i + step - 1, n - 1));//從i開始,在跨度為step中分為兩個小數組進行歸並 10 } 11 }
自頂向下:遞歸版【大數組到小數組】 :
1 void MergeSort(int L, int R) 2 { 3 if (L < R)//一定不能等於 4 { 5 int mid = L + (R - L) / 2;//求中點 6 MergeSort(L, mid);//對左邊進行遞歸切分成小數組 7 MergeSort(mid + 1, R);//對右邊進行遞歸切分成小數組 8 Merge(L, mid, mid + 1, R);//將左右兩邊進行歸並 9 } 10 }
快速排序:
快速排序(Quicksort)是對冒泡排序的一種改進。快速排序由C. A. R. Hoare在1960年提出。 快速排序
它的基本思想是:
通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,然后再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。
設要排序的數組是A[0]……A[N-1],首先任意選取一個數據(通常選用數組的第一個數)作為關鍵數據,然后將所有比它小的數都放到它左邊,所有比它大的數都放到它右邊,這個過程稱為一趟快速排序。值得注意的是,快速排序不是一種穩定的排序算法,也就是說,多個相同的值的相對位置也許會在算法結束時產生變動。
一趟快速排序的算法是:
1)設置兩個變量i、j,排序開始的時候:i=0,j=N-1;
2)以第一個數組元素作為關鍵數據,賦值給key,即key=A[0];
3)從j開始向前搜索,即由后開始向前搜索(j--),直到找到第一個小於key的值A[j],將A[j]和A[i]的值交換;
4)從i開始向后搜索,即由前開始向后搜索(i++),直到找到第一個大於key的A[i],將A[i]和A[j]的值交換;
5)重復第3、4步,直到i==j; (3,4步中,沒找到符合條件的值,即3中A[j]不小於key,4中A[i]不大於key的時候改變j、i的值,使得j=j-1,i=i+1,直至找到為止。找到符合條件的值,進行交換的時候i, j指針位置不變。另外,i==j這一過程一定正好是i+或j-完成的時候,此時令循環結束)
1 //對區間進行划分 2 int Partition(int L, int R) 3 { 4 //int p = round(1.0*rand() / RAND_MAX * (R - L) + L);//選取隨機位置的數為基准值 5 //swap(v[L], v[p]);//將基准值換到最左邊 6 int key = v[L];//一般默認使用最左端的值為基准值 7 while (L < R) 8 { 9 while (L < R && v[R]>key)--R;//從右向左,直到找到比key小的數 10 v[L] = v[R];//將小的數移到左邊 11 while (L < R && v[L] <= key)++L;//從左向右,直到找到比key大數 12 v[R] = v[L];//將大的數移到右邊 13 } 14 v[L] = key;//key在中間的位置 15 return L;//返回中點坐標 16 } 17 18 void QuickSort(int L, int R) 19 { 20 if (L < R) 21 { 22 int pos = Partition(L, R); 23 QuickSort(L, pos - 1);//對左子區間進行快速排序 24 QuickSort(pos + 1, R);//對右子區間進行快速排序 25 } 26 }
三平均分區法
關於這一改進的最簡單的描述大概是這樣的:與一般的快速排序方法不同,它並不是選擇待排數組的第一個數作為中軸,而是選用待排數組最左邊、最右邊和最中間的三個元素的中間值作為中軸。這一改進對於原來的快速排序算法來說,主要有兩點優勢:
(1) 首先,它使得最壞情況發生的幾率減小了。
(2) 其次,未改進的快速排序算法為了防止比較時數組越界,在最后要設置一個哨點。
根據分區大小調整算法
這一方面的改進是針對快速排序算法的弱點進行的。快速排序對於小規模的數據集性能不是很好。可能有人認為可以忽略這個缺點不計,因為大多數排序都只要考慮大規模的適應性就行了。但是快速排序算法使用了分治技術,最終來說大的數據集都要分為小的數據集來進行處理。由此可以得到的改進就是,當數據集較小時,不必繼續遞歸調用快速排序算法,而改為調用其他的對於小規模數據集處理能力較強的排序算法來完成。
不同的分區方案考慮
對於快速排序算法來說,實際上大量的時間都消耗在了分區上面,因此一個好的分區實現是非常重要的。尤其是當要分區的所有的元素值都相等時,一般的快速排序算法就陷入了最壞的一種情況,也即反復的交換相同的元素並返回最差的中軸值。無論是任何數據集,只要它們中包含了很多相同的元素的話,這都是一個嚴重的問題,因為許多“底層”的分區都會變得完全一樣。
對於這種情況的一種改進辦法就是將分區分為三塊而不是原來的兩塊:一塊是小於中軸值的所有元素,一塊是等於中軸值的所有元素,另一塊是大於中軸值的所有元素。
另一種簡單的改進方法是,當分區完成后,如果發現最左和最右兩個元素值相等的話就避免遞歸調用而
采用其他的排序算法來完成。
並行的快速排序
由於快速排序算法是采用分治技術來進行實現的,這就使得它很容易能夠在多台處理機上並行處理。
在大多數情況下,創建一個線程所需要的時間要遠遠大於兩個元素比較和交換的時間,因此,快速排序的並行算法不可能為每個分區都創建一個新的線程。一般來說,會在實現代碼中設定一個閥值,如果分區的元素數目多於該閥值的話,就創建一個新的線程來處理這個分區的排序,否則的話就進行遞歸調用來排序。
隨機化快排
快速排序的最壞情況基於每次划分對主元的選擇。基本的快速排序選取第一個元素作為主元。這樣在數組已經有序的情況下,每次划分將得到最壞的結果。一種比較常見的優化方法是隨機化算法,即隨機選取一個元素作為主元。這種情況下雖然最壞情況仍然是O(n^2),但最壞情況不再依賴於輸入數據,而是由於隨機函數取值不佳。實際上,隨機化快速排序得到理論最壞情況的可能性僅為1/(2^n)。所以隨機化快速排序可以對於絕大多數輸入數據達到O(nlogn)的期望時間復雜度。一位前輩做出了一個精辟的總結:“隨機化快速排序可以滿足一個人一輩子的人品需求。”
隨機化快速排序的唯一缺點在於,一旦輸入數據中有很多的相同數據,隨機化的效果將直接減弱。對於極限情況,即對於n個相同的數排序,隨機化快速排序的時間復雜度將毫無疑問的降低到O(n^2)。解決方法是用一種方法進行掃描,使沒有交換的情況下主元保留在原位置。
平衡快排
每次盡可能地選擇一個能夠代表中值的元素作為關鍵數據,然后遵循普通快排的原則進行比較、替換和遞歸。通常來說,選擇這個數據的方法是取開頭、結尾、中間3個數據,通過比較選出其中的中值。取這3個值的好處是在實際問題中,出現近似順序數據或逆序數據的概率較大,此時中間數據必然成為中值,而也是事實上的近似中值。萬一遇到正好中間大兩邊小(或反之)的數據,取的值都接近最值,那么由於至少能將兩部分分開,實際效率也會有2倍左右的增加,而且利於將數據略微打亂,破壞退化的結構。
外部快排
與普通快排不同的是,關鍵數據是一段buffer,首先將之前和之后的M/2個元素讀入buffer並對該buffer中的這些元素進行排序,然后從被排序數組的開頭(或者結尾)讀入下一個元素,假如這個元素小於buffer中最小的元素,把它寫到最開頭的空位上;假如這個元素大於buffer中最大的元素,則寫到最后的空位上;否則把buffer中最大或者最小的元素寫入數組,並把這個元素放在buffer里。保持最大值低於這些關鍵數據,最小值高於這些關鍵數據,從而避免對已經有序的中間的數據進行重排。完成后,數組的中間空位必然空出,把這個buffer寫入數組中間空位。然后遞歸地對外部更小的部分,循環地對其他部分進行排序。
三路基數快排
(Three-way Radix Quicksort,也稱作Multikey Quicksort、Multi-key Quicksort):結合了基數排序(radix sort,如一般的字符串比較排序就是基數排序)和快排的特點,是字符串排序中比較高效的算法。該算法被排序數組的元素具有一個特點,即multikey,如一個字符串,每個字母可以看作是一個key。算法每次在被排序數組中任意選擇一個元素作為關鍵數據,首先僅考慮這個元素的第一個key(字母),然后把其他元素通過key的比較分成小於、等於、大於關鍵數據的三個部分。然后遞歸地基於這一個key位置對“小於”和“大於”部分進行排序,基於下一個key對“等於”部分進行排序。
堆排序
堆排序(英語:Heapsort)是指利用堆這種數據結構所設計的一種排序算法。堆是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於等於(或者大於等於)它的父節點。
每個結點的值都大於或等於其左右孩子結點的值,稱為大頂堆;
或者每個結點的值都小於或等於其左右孩子結點的值,稱為小頂堆。
堆具有完全二叉樹的概念:
即:樹的葉子節點必須從左向右依次補充!中間不能有空葉子!
用數組來表示一棵完全二叉樹:
arry[]; //不越界的情況下! 【下角標從0開始】
i的左節點:2*i+1;
i的右節點:2*i+2;
i的父節點:(i-1)/2;
堆排序算法思想:
將向量中存儲的數據看成一棵完全二叉樹,利用完全二叉樹中雙親節點和孩子節點之間的內在關系選擇關鍵字最小的記錄。
·將待排序的序列構造成一個大頂堆【或小頂堆】,稱為建堆的過程。
·此時,整個序列的最大值【最小值】就是堆頂的根結點。將它移走(其實就是將其與堆數組的末尾元素交換,此時末尾元素就是最大值),即交換v[0], v[n-1]
·然后將剩余的n-1個序列重新構造成一個堆,這樣就會得到n個元素中的次大值。
·如此反復執行,直到交換v[0], v[1]。便能得到一個有序序列了。
將一個數組中的數字按大根堆的順序排列:
(1)換父節點:
遍歷數組,比較array[i]與其父節點array[(i-1)/2 ]的大小,若大於父節點,則與父節點交換,並且同樣向回比,比較父節點與祖父節點的大小,知道頭部。。。。
(2)換子節點:
在准備將數字加入樹之前,與自己未來的孩子比較。
即,當array[i]准備入樹時,找到自己的兩個孩子,array[2*i+1],array[2*i+2],與孩子中最大的值進行比較,若自己小於孩子中的最大值,則交換!然后孩子繼續與自己的孩子比較!
大根堆排序:
1 //向下調整 2 void downAdjust(int L, int R) 3 { 4 int i = L, j = 2 * L + 1;//i為父節點,j為左子節點 5 while (j <= R) 6 { 7 if (j + 1 <= R && v[j + 1] > v[j])//若有右節點,且右節點大,那么就選右節點,即選取最大的子節點與父節點對比 8 ++j;//選取了右節點 9 if (v[j] <= v[i])//孩子節點都比父節點小,滿足條件,無需調整 10 break; 11 //不滿足的話,那么我就將最大孩子節點j與父節點i對調, 12 swap(v[i], v[j]); 13 i = j; 14 j = 2 * i + 1;//繼續向下遍歷 15 } 16 } 17 //建堆 18 void createHeap() 19 { 20 for (int i = n / 2; i >= 0; --i) 21 downAdjust(i, n - 1); 22 } 23 void HeapSort() 24 { 25 createHeap();//建堆 26 for (int i = n - 1; i > 0; --i)//從最后開始交換,直到只剩下最后一個數字 27 { 28 swap(v[i], v[0]);//每次都將最大值放到最后 29 downAdjust(0, i - 1);//將前0-i個數字重新構成大根堆 30 } 31 }
小根堆排序:
與大根堆排序是一樣的【但排序結果為從大到小排序】
只需要在downAdjust()中將父節點與子節點的大小比較改變一下
刪除堆頂元素:
1 //刪除堆頂元素 2 void deleteTop() 3 { 4 v[0] = v[n - 1];//也就是堆頂使用最后一個數值來替代 5 downAdjust(0, n - 2);//然后對前n-1個數進行排序 6 }
添加元素:
1 //向上調整 2 void upAdjust(int L, int R) 3 { 4 int i = R, j = (i - 1) / 2;//i為欲調整結點,j為其父親 5 while (j >= L) 6 { 7 if (v[j] < v[i])//父節點小了,那么就將孩子節點調上來 8 { 9 swap(v[i], v[j]); 10 i = j; 11 j = (i - 1) / 2;//繼續向上遍歷 12 } 13 else//無需調整 14 break; 15 } 16 } 17 void insert(int x) 18 { 19 v[n] = x;//將新加入的值放置在數組的最后,切記保證數組空間充足 20 upAdjust(0, n);//向上調整新加入的結點n 21 }
計數排序
計數排序是一個非基於比較的排序算法,該算法於1954年由 Harold H. Seward 提出。
它的優勢在於在對一定范圍內的整數排序時,它的復雜度為Ο(n+k)(其中k是整數的范圍),快於任何比較排序算法。 當然這是一種犧牲空間換取時間的做法,而且當O(k)>O(n*log(n))的時候其效率反而不如基於比較的排序(基於比較的排序的時間復雜度在理論上的下限是O(n*log(n)), 如歸並排序,堆排序)
計數排序對輸入的數據有附加的限制條件:
1、輸入的線性表的元素屬於有限偏序集S;
2、設輸入的線性表的長度為n,|S|=k(表示集合S中元素的總數目為k),則k=O(n)。
在這兩個條件下,計數排序的復雜性為O(n)。
找出待排序的數組中最大和最小的元素;
統計數組中每個值為i的元素出現的次數,存入數組C的第i項;[計數]
對所有的計數累加(從C中的第一個元素開始,每一項和前一項相加);
反向填充目標數組:將每個元素i放在新數組的第C(i)項,每放一個元素就將C(i)減去1。[放出去一個,那么就計數減少一個]
計數排序算法是一個穩定的排序算法。
1 void CountSort() 2 { 3 int minN = v[0], maxN = v[0]; 4 for (auto a : v)//找出最大值與最小值 5 { 6 minN = minN < a ? minN : a; 7 maxN = maxN > a ? maxN : a; 8 } 9 vector<int>nums(maxN - minN + 1, 0);//以空間換取時間,用來計算每個數的數量 10 for (auto a : v) 11 ++nums[a - minN]; 12 for (int i = 0, k = 0; i < nums.size(); ++i)//將數賦給原數組 13 while (nums[i]--) 14 v[k++] = i + minN; 15 }
桶排序
桶排序 (Bucket sort)或所謂的箱排序,是一個排序算法,工作的原理是將數組分到有限數量的桶子里。每個桶子再個別排序(有可能再使用別的排序算法或是以遞歸方式繼續使用桶排序進行排序)。
桶排序是計數排序的升級版。它利用了函數的映射關系,高效與否的關鍵就在於這個映射函數的確定。
桶排序 (Bucket sort)的工作的原理:
假設輸入數據服從均勻分布,將數據分到有限數量的桶里,每個桶再分別排序(有可能再使用別的排序算法或是以遞歸方式繼續使用桶排序進行排
數據結構設計:
鏈表可以采用很多種方式實現,通常的方法是動態申請內存建立結點,但是針對這個算法,桶里面的鏈表結果每次掃描后都不同,就有很多鏈表的分離和重建。如果使用動態分配內存,則由於指針的使用,安全性低。
所以,使用了數組來模擬鏈表(當然犧牲了部分的空間,但是操作卻是簡單了很多,穩定性也大大提高了)。共十個桶,所以建立一個二維數組,行向量的下標0—9代表了10個桶,每個行形成的一維數組則是桶的空間。
平均情況下桶排序以線性時間運行。像基數排序一樣,桶排序也對輸入作了某種假設,因而運行得很快。具體來說,基數排序假設輸入是由一個小范圍內的整數構成,而桶排序則假設輸入由一個隨機過程產生,該過程將元素一致地分布在區間[0,1)上。 桶排序的思想就是把區間[0,1)划分成n個相同大小的子區間,或稱桶,然后將n個輸入數分布到各個桶中去。因為輸入數均勻分布在[0,1)上,所以一般不會有很多數落在一個桶中的情況。為得到結果,先對各個桶中的數進行排序,然后按次序把各桶中的元素列出來即可。
在桶排序算法的代碼中,假設輸入是含n個元素的數組A,且每個元素滿足0≤ A[i]<1。
另外還需要一個輔助數組B[O..n-1]來存放鏈表實現的桶,並假設可以用某種機制來維護這些表。
算法思想:
人為設置一個BucketSize,作為每個桶所能放置多少個不同數值(例如當BucketSize==5時,該桶可以存放{1,2,3,4,5}這幾種數字,但是容量不限,即可以存放100個3);
遍歷輸入數據,並且把數據一個一個放到對應的桶里去;
對每個不是空的桶進行排序,可以使用其它排序方法,也可以遞歸使用桶排序;
從不是空的桶里把排好序的數據拼接起來。
注意,如果遞歸使用桶排序為各個桶排序,則當桶數量為1時要手動減小BucketSize增加下一循環桶的數量,否則會陷入死循環,導致內存溢出。
對N個關鍵字進行桶排序的時間復雜度分為兩個部分:
(1) 循環計算每個關鍵字的桶映射函數,這個時間復雜度是O(N)。
(2) 利用先進的比較排序算法對每個桶內的所有數據進行排序,其時間復雜度為 ∑ O(Ni*logNi) 。
其中Ni 為第i個桶的數據量。
很顯然,第(2)部分是桶排序性能好壞的決定因素。盡量減少桶內數據的數量是提高效率的唯一辦法(因為基於比較排序的最好平均時間復雜度只能達到O(N*logN)了)。
因此,我們需要盡量做到下面兩點:
(1) 映射函數f(k)能夠將N個數據平均的分配到M個桶中,這樣每個桶就有[N/M]個數據量。
(2) 盡量的增大桶的數量。極限情況下每個桶只能得到一個數據,這樣就完全避開了桶內數據的“比較”排序操作。 當然,做到這一點很不容易,數據量巨大的情況下,f(k)函數會使得桶集合的數量巨大,空間浪費嚴重。這就是一個時間代價和空間代價的權衡問題了。
對於N個待排數據,M個桶,平均每個桶[N/M]個數據的桶排序平均時間復雜度為:
O(N)+O(M*(N/M)*log(N/M))=O(N+N*(logN-logM))=O(N+N*logN-N*logM)
當N=M時,即極限情況下每個桶只有一個數據時。桶排序的最好效率能夠達到O(N)。
總結:桶排序的平均時間復雜度為線性的O(N+C),其中C=N*(logN-logM)。如果相對於同樣的N,桶數量M越大,其效率越高,最好的時間復雜度達到O(N)。當然桶排序的空間復雜度為O(N+M),如果輸入數據非常龐大,而桶的數量也非常多,則空間代價無疑是昂貴的。此外,桶排序是穩定的。
1 void BucketSort() 2 { 3 int minN = v[0], maxN = v[0]; 4 for (auto a : v)//找出最大值與最小值 5 { 6 minN = minN < a ? minN : a; 7 maxN = maxN > a ? maxN : a; 8 } 9 vector<vector<int>>bucket((maxN-minN)/10+1);//除數是按照數據范圍進行調整的 10 for (auto a : v)//將數據放入對應的桶中 11 bucket[(a - minN) / 10].push_back(a); 12 for (int i = 0; i < bucket.size(); ++i) 13 sort(bucket[i].begin(), bucket[i].end());//分別對每個桶進行排序,可以使用任意的排序算法,個人感覺沒必要使用復雜的排序算法 14 int k = 0; 15 for (auto a : bucket)//將數據賦予原數組 16 for (auto b : a) 17 v[k++] = b; 18 }
基數排序
基數排序(radix sort)屬於“分配式排序”(distribution sort),基數排序也是非比較的排序算法,對每一位進行排序,從最低位開始排序,復雜度為O(kn),為數組長度,k為數組中的數的最大的位數;
基數排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次類推,直到最高位。有時候有些屬性是有優先級順序的,先按低優先級排序,再按高優先級排序。最后的次序就是高優先級高的在前,高優先級相同的低優先級高的在前。基數排序基於分別排序,分別收集,所以是穩定的。
過程:
取得數組中的最大數,並取得位數;
arr為原始數組,從最低位開始取每個位組成radix數組;
對radix進行計數排序(利用計數排序適用於小范圍數的特點);
源數據:
第一次排序:【按個位數】
還原:【底下的先出來】
再排:【按十位數】
再次還原:【底下的先出來】
最高位優先(Most Significant Digit first)法,簡稱MSD法:先按k1排序分組,同一組中記錄,關鍵碼k1相等,再對各組按k2排序分成子組,之后,對后面的關鍵碼繼續這樣的排序分組,直到按最次位關鍵碼kd對各子組排序后。再將各組連接起來,便得到一個有序序列。
最低位優先(Least Significant Digit first)法,簡稱LSD法:先從kd開始排序,再對kd-1進行排序,依次重復,直到對k1排序后便得到一個有序序列。
1 //基數排序 2 void RadixSort() 3 { 4 int maxBit = 0;//最大的位數 5 int bit = 1;//先從個位開始 6 for (auto a : v) 7 { 8 int len = to_string(a).length();//這里我就偷懶直接使用string來轉換 9 maxBit = maxBit > len ? maxBit : len; 10 } 11 for (int i = 1; i <= maxBit; ++i)//最大的數有多少位就進行多少次排序 12 { 13 vector<vector<int>>count(10);//存放位數上數值相同的數據 14 for (auto a : v) 15 count[(a % (bit * 10) / bit)].push_back(a);//按照第bit位上進行排序 16 int k = 0; 17 for (auto a : count) 18 for (auto b : a) 19 v[k++] = b;//將數據放回 20 bit *= 10;//向前一位 21 } 22 }
排序算法總結:
·比較類排序:通過比較來決定元素間的相對次序,由於其時間復雜度不能突破O(nlogn),因此也稱為非線性時間比較類排序。
·非比較類排序:不通過比較來決定元素間的相對次序,它可以突破基於比較排序的時間下界,以線性時間運行,因此也稱為線性時間非比較類排序。
名詞及數據解釋:
·n: 數據規模
·k: “桶”的個數
·In-place: 占用常數內存,不占用額外內存
·Out-place: 占用額外內存
·log為log2
·穩定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
·不穩定:如果a原本在b的前面,而a=b,排序之后 a 可能會出現在 b 的后面。
·時間復雜度:對排序數據的總的操作次數。反映當n變化時,操作次數呈現什么規律。
·空間復雜度:是指算法在計算機內執行時所需存儲空間的度量,它也是數據規模n的函數。
從算法的簡單性來看,我們將7種算法分為兩類:
·簡單算法:冒泡、簡單選擇、直接插入。
·改進算法:希爾、堆、歸並、快速。
·比較排序:快速排序、歸並排序、堆排序、冒泡排序。
在排序的最終結果里,元素之間的次序依賴於它們之間的比較。每個數都必須和其他數進行比較,才能確定自己的位置。
·非比較排序:計數排序、基數排序、桶排序
- 穩定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
- 不穩定:如果a原本在b的前面,而a=b,排序之后a可能會出現在b的后面;
- 內排序:所有排序操作都在內存中完成;
- 外排序:由於數據太大,因此把數據放在磁盤中,而排序通過磁盤和內存的數據傳輸才能進行;
- 時間復雜度: 一個算法執行所耗費的時間。
- 空間復雜度:運行完一個程序所需內存的大小。
所謂的穩定性:
就是維持相同數字在排序過程中的相對位置。
是穩定的,以為111的相對位置未被打亂。
不是穩定的,因為555的相對位置打亂了。
意義:
在比較數據的屬性時,比如年齡、身高、體重
若按身高排序,然后再按年齡排序,在穩定性下,相同年齡的兩人會安上次身高的排序放置!!!
怎么選用排序算法
·在排序數據<60時,會選擇插入排序,當數據量很大時,先選擇歸並等算法,當數據分支小於60時,立馬使用插入排序。
·從空間復雜度來考慮:首選堆排序,其次是快速排序,最后是歸並排序。
·若從穩定性來考慮,應選取歸並排序,因為堆排序和快速排序都是不穩定的。
·若從平均情況下的排序速度考慮,應該選擇快速排序。