排序有內部排序和外部排序之分,內部排序是數據記錄在內存中進行排序,而外部排序是因排序的數據很大,一次不能容納所有的排序記錄,在排序過程當中須要訪問外存。咱們這裏說的八大排序算法均爲內部排序。html
下圖爲排序算法體系結構圖:git
常見的分類算法還能夠根據排序方式分爲兩大類:比較排序和非比較排序。本文中前七種算法都是比較排序,非比較排序有三種,分別爲:算法
1)計數排序(Count Sort)(複雜度O(n+k)(其中k是待排序的n個數字中最大值),參見《計數排序-Counting Sort》)數組
2)基數排序(Bucket Sort)(複雜度O(nk)(其中k是最大數字的位數),參見《最快最簡單的排序—桶排序》)數據結構
3)桶排序(Radix Sort)(複雜度O(n+k)(其中k是待排序的n個數字中最大值),參見《基數排序(Radix Sorting)》)函數
非比較排序的特色是時間複雜度很低,都是線性複雜度O(n),可是非比較排序受到的限制比較多,不是通用的排序算法。本文主要講解七種比較排序算法,最後單獨介紹一下非比較排序算法。性能
基本思想:將待排序的無序數列當作是一個僅含有一個元素的有序數列和一個無序數列,將無序數列中的元素逐次插入到有序數列中,從而得到最終的有序數列。大數據
算法流程:優化
1)初始時, a[0]自成一個有序區, 無序區爲a[1, ... , n-1], 令i=1;動畫
2)將a[i]併入當前的有序區a[0, ... , i-1];
3)i++並重復2)直到i=n-1, 排序完成。
時間複雜度:O(n^2)。
示意圖:初始無序數列爲 49, 38, 65, 97, 76, 13, 27 ,49
說明:若是遇見一個和插入元素相等的,那麼插入元素把想插入的元素放在相等元素的後面。因此,相等元素的先後順序沒有改變,從原無序序列出去的順序就是排好序後的順序,因此插入排序是穩定的。
C++實現源碼:
//直接插入排序,版本1 void StraightInsertionSort1(int a[], int n) { int i, j, k; for(i=1; i<n; i++) { //找到要插入的位置 for(j=0; j<i; j++) if(a[i] < a[j]) break; //插入,並後移剩餘元素 if(j != i) { int temp = a[i]; for(int k=i-1; k>=j; k--) a[k+1] = a[k]; a[j] = temp; } } PrintDataArray(a, n); }
兩種簡化版本,推薦第三版本。
//直接插入法,版本2:搜索和後移同時進行 void StraightInsertionSort2(int a[], int n) { int i, j, k; for(i=1; i<n; i++) if(a[i] < a[i-1]) { int temp = a[i]; for(j=i-1; j>=0 && a[j]>temp; j--) a[j+1] = a[j]; a[j+1] = temp; } PrintDataArray(a, n); } //插入排序,版本3:用數據交換代替版本2的數據後移(比較對象只考慮兩個元素) void StraightInsertionSort3(int a[], int n) { for(int i=1; i<n; i++) for(int j=i-1; j>=0 && a[j]>a[j+1]; j--) Swap(a[j], a[j+1]); PrintDataArray(a, n); }
希爾排序是1959 年由D.L.Shell 提出來的,相對直接排序有較大的改進。希爾排序又叫縮小增量排序
基本思想:先將整個待排序的記錄序列分割成爲若干子序列分別進行直接插入排序,待整個序列中的記錄「基本有序」時,再對全體記錄進行依次直接插入排序。
算法流程:
1)選擇一個增量序列t1,t2,…,tk,其中ti>tj,tk=1;
2)按增量序列個數k,對序列進行k 趟排序;
3)每趟排序,根據對應的增量ti,將待排序列分割成若干長度爲m 的子序列,分別對各子表進行直接插入排序。僅增量因子爲1 時,整個序列做爲一個表來處理,表長度即爲整個序列的長度。
時間複雜度:O(n^(1+e))(其中0<e<1),在元素基本有序的狀況下,效率很高。希爾排序是一種不穩定的排序算法。
希爾排序的示例:
C++實現源碼:
//希爾排序
void ShellSort(int a[], int n) { int i, j, gap; //分組 for(gap=n/2; gap>0; gap/=2) //直接插入排序 for(i=gap; i<n; i++) for(j=i-gap; j>=0 && a[j]>a[j+gap]; j-=gap) Swap(a[j], a[j+gap]); PrintDataArray(a, n); }
經過源代碼咱們也能看出來,希爾排序就是在直接插入排序的基礎上加入了分組策略。
基本思想:在要排序的一組數中,選出最小(或者最大)的一個數與第1個位置的數交換;而後在剩下的數當中再找最小(或者最大)的與第2個位置的數交換,依次類推,直到第n-1個元素(倒數第二個數)和第n個元素(最後一個數)比較爲止。
算法流程:
1)初始時,數組全爲無序區a[0, ... , n-1], 令i=0;
2)在無序區a[i, ... , n-1]中選取一個最小的元素與a[i]交換,交換以後a[0, ... , i]即爲有序區;
3)重複2),直到i=n-1,排序完成。
時間複雜度分析:O(n^2),直接選擇排序是一種不穩定的排序算法。
直接選擇排序的示例:
C++實現源碼:
//直接選擇排序 void StraightSelectionSort(int a[], int n) { int i, j, minIndex; for(i=0; i<n; i++) { minIndex=i; for(j=i+1; j<n; j++) if(a[j]<a[minIndex]) minIndex=j; Swap(a[i], a[minIndex]); } PrintDataArray(a, n); }
堆排序是一種樹形選擇排序,是對直接選擇排序的有效改進。
堆的定義以下:具備n個元素的序列(k1,k2,...,kn),當且僅當知足
時稱之爲堆。由堆的定義能夠看出,堆頂元素(即第一個元素)必爲最小項(小頂堆)。
若以一維數組存儲一個堆,則堆對應一棵徹底二叉樹,且全部非葉結點的值均不大於(或不小於)其子女的值,根結點(堆頂元素)的值是最小(或最大)的。如:
(a)大頂堆序列:(96, 83,27,38,11,09)
(b)小頂堆序列:(12,36,24,85,47,30,53,91)
基本思想:初始時把要排序的n個數的序列看做是一棵順序存儲的二叉樹(一維數組存儲二叉樹),調整它們的存儲序,使之成爲一個堆,將堆頂元素輸出,獲得n 個元素中最小(或最大)的元素,這時堆的根節點的數最小(或者最大)。而後對前面(n-1)個元素從新調整使之成爲堆,輸出堆頂元素,獲得n 個元素中次小(或次大)的元素。依此類推,直到只有兩個節點的堆,並對它們做交換,最後獲得有n個節點的有序序列。稱這個過程爲堆排序。
時間複雜度分析:O(nlog(n)),堆排序是一種不穩定的排序算法。
所以,實現堆排序需解決兩個問題:
1. 如何將n 個待排序的數建成堆?
2. 輸出堆頂元素後,怎樣調整剩餘n-1 個元素,使其成爲一個新堆?
首先討論第二個問題:輸出堆頂元素後,怎樣對剩餘n-1元素從新建成堆?
調整小頂堆的方法:
1)設有m 個元素的堆,輸出堆頂元素後,剩下m-1 個元素。將堆底元素送入堆頂((最後一個元素與堆頂進行交換),堆被破壞,其緣由僅是根結點不知足堆的性質。
2)將根結點與左、右子樹中較小元素的進行交換。
3)若與左子樹交換:若是左子樹堆被破壞,即左子樹的根結點不知足堆的性質,則重複方法 (2).
4)若與右子樹交換,若是右子樹堆被破壞,即右子樹的根結點不知足堆的性質。則重複方法 (2).
5)繼續對不知足堆性質的子樹進行上述交換操做,直到葉子結點,堆被建成。
稱這個自根結點到葉子結點的調整過程爲篩選。如圖:
再討論第一個問題,如何將n 個待排序元素初始建堆?
建堆方法:對初始序列建堆的過程,就是一個反覆進行篩選的過程。
1)n 個結點的徹底二叉樹,則最後一個結點是第n/2個結點的子樹。
2)篩選從第n/2個結點爲根的子樹開始,該子樹成爲堆。
3)以後向前依次對各結點爲根的子樹進行篩選,使之成爲堆,直到根結點。
如圖建堆初始過程:無序序列:(49,38,65,97,76,13,27,49)
C++實現源碼:
//堆排序問題二:如何調整一個堆? void HeapAdjusting(int a[], int root, int n) { int temp = a[root]; int child = 2*root+1; //左孩子的位置 while(child<n) { //找到孩子節點中較小的那個 if(child+1<n && a[child+1]<a[child]) child++; //若是較大的孩子節點小於父節點,用較小的子節點替換父節點,並從新設置下一個須要調整的父節點和子節點。 if(a[root]>a[child]) { a[root] = a[child]; root = child; child = 2*root+1; } else break; //將調整前父節點的值賦給調整後的位置。 a[root] = temp; } } //堆排序問題一:如何初始化建堆? void HeapBuilding(int a[], int n) { //從最後一個有孩子節點的位置開始調整,最後一個有孩子節點的位置爲(n-1)/2 for(int i=(n-1)/2; i>=0; i--) HeapAdjusting(a, i, n); } //堆排序 void HeapSort(int a[], int n) { //初始化堆 HeapBuilding(a, n); //從最後一個節點開始進行調整 for(int i=n-1; i>0; i--) { //交換堆頂元素和最後一個元素 Swap(a[0], a[i]); //每次交換後都要進行調整 HeapAdjusting(a, 0, i); } }
在這裏多說幾句堆排序的強大之處,堆排序能夠當作是一種算法,也能夠當作是一種數據結構。它能夠分爲小頂堆和大頂堆。與堆這種數據結構聯繫緊密的一種典型問題就是咱們常常遇到的top-K問題。
咱們先看一個大數據top-K示例:
例子:搜索引擎會經過日誌文件把用戶每次檢索使用的全部檢索串都記錄下來,每一個查詢串的長度爲1-255字節。假設目前有一千萬個記錄(這些查詢串的重複度比較高,雖然總數是1千萬,但若是除去重複後,不超過3百萬個。一個查詢串的重複度越高,說明查詢它的用戶越多,也就是越熱門。),請你統計最熱門的10個查詢串,要求使用的內存不能超過1G。
首先,咱們知道這是一個典型的top-K問題。
針對大數據問題進行統計首先應該想到的就是Hash_map。因此第一步就是先遍歷所有的1千萬Query,構建出一個大小爲3百萬的Hash_map,其中的key值爲某條Query,對應的value值爲該條Query的查詢次數。
建好Hash_map之後,咱們接下來的問題就是如何在3百萬的Query中找出10個最熱門的Query,也就是要用到排序算法。排序算法中效率最高的時間複雜度爲O(n*log(n)),這是最簡單粗暴的方法,也是最直接的方法。或者咱們進一步優化,該題目是要求尋找top-K問題,那麼咱們能夠直接去前K個Query構建一個數組,而後對其進行排序。遍歷剩餘的所有Query,若是某條Query的查詢次數大於數組中最小的一個,將數組中最小的Query剔除,加入這條新的Query。接着調整數組順序,依次進行遍歷,這樣的最壞狀況下的複雜度爲O(n*K)。
可是還能夠繼續優化尋找top-K的操做,那就是藉助小根堆來實現。基於以上的分析,咱們想一想,有沒有一種既能快速查找,又能快速移動元素的數據結構呢?回答是確定的,那就是堆。
具體過程是,堆頂存放的是整個堆中最小的數,如今遍歷N個數,把最早遍歷到的k個數存放到最小堆中,並假設它們就是咱們要找的最大的k個數,X1>X2...Xmin(堆頂),然後遍歷後續的(n-K)個數,一一與堆頂元素進行比較,若是遍歷到的Xi大於堆頂元素Xmin,則把Xi放入堆中,然後更新整個堆,更新的時間複雜度爲logK,若是Xi<Xmin,則不更新堆,整個過程的複雜度爲O(K)+O((N-K)*logK)=O(N*logK)。
一個有關小根堆解決top-K問題的小動畫,請點擊這個連接。
思想與上述算法二一致,只是算法在算法三,咱們採用了最小堆這種數據結構代替數組,把查找目標元素的時間複雜度有O(K)降到了O(logK)。那麼這樣,採用堆數據結構,算法三,最終的時間複雜度就降到了O(n*logK),和算法二相比,又有了比較大的改進。
基本思想:在要排序的一組數中,對當前還未排好序的範圍內的所有數,自上而下對相鄰的兩個數依次進行比較和調整,讓較大的數往下沉,較小的往上冒。即:每當兩相鄰的數比較後發現它們的排序與排序要求相反時,就將它們互換。每一趟排序後的效果都是講沒有沉下去的元素給沉下去。
算法流程:
1)比較相鄰的兩個元素,若是前面的數據大於後面的數據,就將兩個數據進行交換;這樣對數組第0個元素到第n-1個元素進行一次遍歷後,最大的一個元素就沉到數組的第n-1個位置;
2)重複第2)操做,直到i=n-1。
時間複雜度分析:O(n^2),冒泡排序是一種不穩定排序算法。
冒泡排序的示例:
C++實現源碼:
//冒泡排序 void BubbleSort(int a[], int n) { int i, j; for(i=0; i<n; i++) //j的起始位置爲1,終止位置爲n-i for(j=1; j<n-i; j++) if(a[j]<a[j-1]) Swap(a[j-1], a[j]); PrintDataArray(a, n); }
基本思想:快速排序算法的基本思想爲分治思想。
1)先從數列中取出一個數做爲基準數;
2)根據基準數將數列進行分區,小於基準數的放左邊,大於基準數的放右邊;
3)重複分區操做,知道各區間只有一個數爲止。
算法流程:(遞歸+挖坑填數)
1)i=L,j=R,將基準數挖出造成第一個坑a[i];
2)j--由後向前找出比它小的數,找到後挖出此數a[j]填到前一個坑a[i]中;
3)i++從前向後找出比它大的數,找到後也挖出此數填到前一個坑a[j]中;
4)再重複2,3),直到i=j,將基準數填到a[i]。
時間複雜度:O(nlog(n)),但若初始數列基本有序時,快排序反而退化爲冒泡排序。
快速排序的示例:
(a)一趟排序的過程:
(b)排序的全過程
C++實現源碼:
//快速排序 void QuickSort(int a[], int L, int R) { if(L<R) { int i=L, j=R, temp=a[i]; while(i<j) { //從右向左找小於基準值a[i]的元素 while(i<j && a[j]>=temp) j--; if(i<j) a[i++]=a[j]; //從左向右找大於基準值a[i]的元素 while(i<j && a[i]<temp) i++; if(i<j) a[j--]=a[i]; } //將基準值填入最後的坑中 a[i]=temp; //遞歸調用,分治法的思想 QuickSort(a, L, i-1); QuickSort(a, i+1, R); } }
基本思想:歸併(Merge)排序法是將兩個(或兩個以上)有序表合併成一個新的有序表,即把待排序序列分爲若干個子序列,每一個子序列是有序的。而後再把有序子序列合併爲總體有序序列。
算法流程:(迭代+兩個有序數列合併爲一個有序數列)
時間複雜度:O(nlog(n)),歸併算法是一種穩定排序算法。
歸併排序示例:
C++實現源碼:
//merge兩個有序數列爲一個有序數列 void MergeArr(int a[], int first, int mid, int last, int temp[]) { int i = first, j = mid+1; int m = mid, n = last; int k=0; //經過比較,歸併數列a和b while(i<=m && j<=n) { if(a[i]<a[j]) temp[k++] = a[i++]; else temp[k++] = a[j++]; } //將數列a或者b剩餘的元素直接插入到新數列後邊 while(i<=m) temp[k++] = a[i++]; while(j<=n) temp[k++] = a[j++]; for(i=0; i<k; i++) a[first+i] = temp[i]; } //歸併排序 void MergeSort(int a[], int first, int last, int temp[]) { if(first<last) { int mid = (first+last)/2; MergeSort(a, first, mid, temp); MergeSort(a, mid+1, last, temp); MergeArr(a, first, mid, last, temp); } }
說基數排序以前,咱們先說桶排序:
基本思想:是將數列分到有限數量的桶裏。每一個桶再個別排序(有可能再使用別的排序算法或是以遞迴方式繼續使用桶排序進行排序)。桶排序是鴿巢排序的一種概括結果。當要被排序的陣列內的數值是均勻分配的時候,桶排序使用線性時間O(n)。但桶排序並非比較排序,不受到O(n*log n)下限的影響。
簡單來講,就是把數據分組,放在一個個的桶中,而後對每一個桶裏面的在進行排序。
例如要對大小爲[1..1000]範圍內的n個整數A[1..n]排序
首先,能夠把桶設爲大小爲10的範圍,具體而言,設集合B[1]存儲[1..10]的整數,集合B[2]存儲(10..20]的整數,…… ,集合B[i]存儲((i-1)*10, i*10]的整數,i=1,2,..100,總共有100個桶。
而後,對A[1, ... , n]從頭至尾掃描一遍,把每一個A[i]放入對應的桶B[j]中。 再對這100個桶中每一個桶裏的數字排序,這時可用冒泡,選擇,乃至快排,通常來講任何排序法均可以。
最後,依次輸出每一個桶裏面的數字,且每一個桶中的數字從小到大輸出,這樣就獲得全部數字排好序的一個序列了。
假設有n個數字,有m個桶,若是數字是平均分佈的,則每一個桶裏面平均有n/m個數字。若是對每一個桶中的數字採用快速排序,那麼整個算法的複雜度是 O(n+m*n/m*log(n/m))=O(n+n*logn-n*logm)。
從上式看出,當m接近n的時候,桶排序複雜度接近O(n)
固然,以上覆雜度的計算是基於輸入的n個數字是平均分佈這個假設的。這個假設是很強的 ,實際應用中效果並無這麼好。若是全部的數字都落在同一個桶中,那就退化成通常的排序了。
一個有關桶排序的圖文講解,強力推薦:阿哈磊的《最快最簡單的排序—桶排序》。
桶排序的一個重要的應用場景:Bit-map:
所謂的Bit-map就是用一個bit位來標記某個元素對應的Value,而Key便是該元素。因爲採用了Bit爲單位來存儲數據,所以在存儲空間方面,能夠大大節省。
若是說了這麼多還沒明白什麼是Bit-map,那麼咱們來看一個具體的例子,假設咱們要對0-7內的5個元素(4,7,2,5,3)排序(這裏假設這些元素沒有重複)。那麼咱們就能夠採用Bit-map的方法來達到排序的目的。要表示8個數,咱們就只須要8個Bit(1Bytes),首先咱們開闢1Byte的空間,將這些空間的全部Bit位都置爲0(以下圖):
而後遍歷這5個元素,首先第一個元素是4,那麼就把4對應的位置爲1(能夠這樣操做 p+(i/8)|(0×01<<(i%8)) 固然了這裏的操做涉及到Big-ending和Little-ending的狀況,這裏默認爲Big-ending),由於是從零開始的,因此要把第五位置爲一(以下圖):
而後再處理第二個元素7,將第八位置爲1,,接着再處理第三個元素,一直到最後處理完全部的元素,將相應的位置爲1,這時候的內存的Bit位的狀態以下:
而後咱們如今遍歷一遍Bit區域,將該位是一的位的編號輸出(2,3,4,5,7),這樣就達到了排序的目的。
其實Bit-map還有不少用途,這裏只是用排序進行了Bit-map的介紹,Bit-map能夠進行查重的操做,尤爲是在大數據上應用更爲普遍,它能夠將存儲空間下降10倍左右。
前面說的幾大排序算法 ,大部分時間複雜度都是O(n2),也有部分排序算法時間複雜度是O(nlogn)。而桶式排序卻能實現O(n)的時間複雜度。但桶排序的缺點是:
1)首先是空間複雜度比較高,須要的額外開銷大。排序有兩個數組的空間開銷,一個存放待排序數組,一個就是所謂的桶,好比待排序值是從0到m-1,那就須要m個桶,這個桶數組就要至少m個空間。
2)其次待排序的元素都要在必定的範圍內等等。
桶式排序是一種分配排序。分配排序的特定是不須要進行關鍵碼的比較,但前提是要知道待排序列的一些具體狀況。
分配排序的基本思想:說白了就是進行屢次的桶式排序。
基數排序過程無須比較關鍵字,而是經過「分配」和「收集」過程來實現排序。它們的時間複雜度可達到線性階:O(n)。
實例:
撲克牌中52 張牌,可按花色和麪值分紅兩個字段,其大小關係爲:
花色: 梅花< 方塊< 紅心< 黑心
面值: 2 < 3 < 4 < 5 < 6 < 7 < 8 < 9 < 10 < J < Q < K < A
若對撲克牌按花色、面值進行升序排序,獲得以下序列:
即兩張牌,若花色不一樣,不論面值怎樣,花色低的那張牌小於花色高的,只有在同花色狀況下,大小關係才由面值的大小肯定。這就是多關鍵碼排序。
爲獲得排序結果,咱們討論兩種排序方法。
方法1:先對花色排序,將其分爲4 個組,即梅花組、方塊組、紅心組、黑心組。再對每一個組分別按面值進行排序,最後,將4 個組鏈接起來便可。
方法2:先按13 個面值給出13 個編號組(2 號,3 號,...,A 號),將牌按面值依次放入對應的編號組,分紅13 堆。再按花色給出4 個編號組(梅花、方塊、紅心、黑心),將2號組中牌取出分別放入對應花色組,再將3 號組中牌取出分別放入對應花色組,……,這樣,4 個花色組中均按面值有序,而後,將4 個花色組依次鏈接起來便可。
設n 個元素的待排序列包含d 個關鍵碼{k1,k2,…,kd},則稱序列對關鍵碼{k1,k2,…,kd}有序是指:對於序列中任兩個記錄r[i]和r[j](1≤i≤j≤n)都知足下列有序關係:
其中k1 稱爲最主位關鍵碼,kd 稱爲最次位關鍵碼。
兩種多關鍵碼排序方法:
多關鍵碼排序按照從最主位關鍵碼到最次位關鍵碼或從最次位到最主位關鍵碼的順序逐次排序,分兩種方法:
最高位優先(Most Significant Digit first)法,簡稱MSD 法:
1)先按k1排序分組,將序列分紅若干子序列,同一組序列的記錄中,關鍵碼k1相等。
2)再對各組按k2排序分紅子組,以後,對後面的關鍵碼繼續這樣的排序分組,直到按最次位關鍵碼kd對各子組排序後。
3)再將各組鏈接起來,便獲得一個有序序列。撲克牌按花色、面值排序中介紹的方法一便是MSD法。
最低位優先(Least Significant Digit first)法,簡稱LSD法:
1) 先從kd 開始排序,再對kd-1進行排序,依次重複,直到按k1排序分組分紅最小的子序列後。
2) 最後將各個子序列鏈接起來,即可獲得一個有序的序列, 撲克牌按花色、面值排序中介紹的方法二便是LSD法。
基於LSD方法的鏈式基數排序的基本思想:
「多關鍵字排序」的思想實現「單關鍵字排序」。對數字型或字符型的單關鍵字,能夠看做由多個數位或多個字符構成的多關鍵字,此時能夠採用「分配-收集」的方法進行排序,這一過程稱做基數排序法,其中每一個數字或字符可能的取值個數稱爲基數。好比,撲克牌的花色基數爲4,面值基數爲13。在整理撲克牌時,既能夠先按花色整理,也能夠先按面值整理。按花色整理時,先按紅、黑、方、花的順序分紅4摞(分配),再按此順序再疊放在一塊兒(收集),而後按面值的順序分紅13摞(分配),再按此順序疊放在一塊兒(收集),如此進行二次分配和收集便可將撲克牌排列有序。
基數排序:是按照低位先排序,而後收集;再按照高位排序,而後再收集;依次類推,直到最高位。有時候有些屬性是有優先級順序的,先按低優先級排序,再按高優先級排序。最後的次序就是高優先級高的在前,高優先級相同的低優先級高的在前。基數排序基於分別排序,分別收集,因此是穩定的。
1)各類排序的穩定性,時間複雜度和空間複雜度總結:
改錯:上述快速排序算法的空間複雜度應改成O(log2n)。
爲何快速排序算法的空間複雜度爲O(log2n)~O(n)?
快速排序算法的實現須要棧的輔助,棧的遞歸深度爲O(log2n);當整個數列均有序時,棧的深度會達到O(n)。
咱們比較時間複雜度函數的狀況:
2)時間複雜度來講:
(1)平方階(O(n2))排序
各種簡單排序:直接插入、直接選擇和冒泡排序;
(2)線性對數階(O(n*logn))排序
快速排序、堆排序和歸併排序;
(3)O(n1+§))排序,§是介於0和1之間的常數
希爾排序
(4)線性階(O(n))排序
基數排序,此外還有桶、箱排序。
說明:
(1)當原表有序或基本有序時,直接插入排序和冒泡排序將大大減小比較次數和移動記錄的次數,時間複雜度可降至O(n);
(2)而快速排序則相反,當原表基本有序時,將蛻化爲冒泡排序,時間複雜度提升爲O(n^2);
(3)原表是否有序,對簡單選擇排序、堆排序、歸併排序和基數排序的時間複雜度影響不大。
3)穩定性:排序算法的穩定性:若待排序的序列中,存在多個具備相同關鍵字的記錄,通過排序, 這些記錄的相對次序保持不變,則稱該算法是穩定的;若經排序後,記錄的相對次序發生了改變,則稱該算法是不穩定的。
穩定性的好處:排序算法若是是穩定的,那麼從一個鍵上排序,而後再從另外一個鍵上排序,第一個鍵排序的結果能夠爲第二個鍵排序所用。基數排序就是這樣,先按低位排序,逐次按高位排序,低位相同的元素其順序再高位也相同時是不會改變的。另外,若是排序算法穩定,能夠避免多餘的比較。
穩定的排序算法:冒泡排序、插入排序、歸併排序和基數排序。
不是穩定的排序算法:選擇排序、快速排序、希爾排序、堆排序。
4)選擇排序算法準則:
每種排序算法都各有優缺點。所以,在實用時需根據不一樣狀況適當選用,甚至能夠將多種方法結合起來使用。
選擇排序算法的依據:
影響排序的因素有不少,平均時間複雜度低的算法並不必定就是最優的。相反,有時平均時間複雜度高的算法可能更適合某些特殊狀況。同時,選擇算法時還得考慮它的可讀性,以利於軟件的維護。通常而言,須要考慮的因素有如下四點:
(1)待排序的記錄數目n的大小;
(2)記錄自己數據量的大小,也就是記錄中除關鍵字外的其餘信息量的大小;
(3)關鍵字的結構及其分佈狀況;
(4)對排序穩定性的要求。
設待排序元素的個數爲n.
(1)當n較大,則應採用時間複雜度爲O(n*logn)的排序方法:快速排序、堆排序或歸併排序。
快速排序:是目前基於比較的內部排序中被認爲是最好的方法,當待排序的關鍵字是隨機分佈時,快速排序的平均時間最短;
堆排序:若是內存空間容許且要求穩定性的;
歸併排序:它有必定數量的數據移動,因此咱們可能過與插入排序組合,先得到必定長度的序列,而後再合併,在效率上將有所提升。
(2)當n較大,內存空間容許,且要求穩定性:歸併排序
(3)當n較小,可採用直接插入或直接選擇排序。
直接插入排序:當元素分佈有序,直接插入排序將大大減小比較次數和移動記錄的次數。
直接選擇排序:當元素分佈有序,若是不要求穩定性,選擇直接選擇排序。
(4)通常不使用或不直接使用傳統的冒泡排序。
(5)基數排序
它是一種穩定的排序算法,但有必定的侷限性:
一、關鍵字可分解;
二、記錄的關鍵字位數較少,若是密集更好;
三、若是是數字時,最好是無符號的,不然將增長相應的映射覆雜度,可先將其正負分開排序。