這裏對筆試面試最常涉及到的12種排序算法(包括插入排序、二分插入排序、希爾排序、選擇排序、冒泡排序、雞尾酒排序、快速排序、堆排序、歸併排序、桶排序、計數排序和基數排序)進行了詳解。每一種算法都有基本介紹、算法原理分析、圖解/flash演示/視頻演示、算法代碼、筆試面試重點分析、筆試面試題等板塊,希望能幫助大家真正理解這些排序算法,並能使用這些算法的思想解決一些題。不多說了,下面就進入正題了。
一、插入排序
1)算法簡介
插入排序(Insertion Sort)的算法描述是一種簡單直觀的排序算法。它的工作原理是通過構建有序序列,對於未排序數據,在已排序序列中從後向前掃描,找到相應位置並插入。插入排序在實現上,通常採用in-place排序(即只需用到O(1)的額外空間的排序),因而在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,爲最新元素提供插入空間。
2)算法描述和分析
一般來說,插入排序都採用in-place在數組上實現。具體算法描述如下:
1、從第一個元素開始,該元素可以認爲已經被排序
2、取出下一個元素,在已經排序的元素序列中從後向前掃描
3、如果該元素(已排序)大於新元素,將該元素移到下一位置
4、重複步驟3,直到找到已排序的元素小於或者等於新元素的位置
5、將新元素插入到該位置後
6、重複步驟2~5
如果目標是把n個元素的序列升序排列,那麼採用插入排序存在最好情況和最壞情況。最好情況就是,序列已經是升序排列了,在這種情況下,需要進行的比較操作需(n-1)次即可。最壞情況就是,序列是降序排列,那麼此時需要進行的比較共有n(n-1)/2次。插入排序的賦值操作是比較操作的次數減去(n-1)次。平均來說插入排序算法複雜度爲O(n^2)。因而,插入排序不適合對於數據量比較大的排序應用。但是,如果需要排序的數據量很小,例如,量級小於千,那麼插入排序還是一個不錯的選擇。 插入排序在工業級庫中也有着廣泛的應用,在STL的sort算法和stdlib的qsort算法中,都將插入排序作爲快速排序的補充,用於少量元素的排序(通常爲8個或以下)。
3)算法圖解、flash演示、視頻演示
圖解:
Flash:
視頻:插入排序舞蹈
http://v.youku.com/v_show/id_XMjU4NTY5MzEy.html
4)算法代碼
- void insertion_sort(int array[], int first, int last)
- {
- int i,j;
- int temp;
- for (i = first+1; i<=last;i++)
- {
- temp = array[i];
- j=i-1;
- //與已排序的數逐一比較,大於temp時,該數後移
- while((j>=first) && (array[j] > temp)) //當first=0,j循環到-1時,由於[[短路求值]],不會運算array[-1]
- {
- array[j+1] = array[j];
- j--;
- }
- array[j+1] = temp; //被排序數放到正確的位置
- }
- }
5)考察點,重點和頻度分析
把插入排序放在第一個的原因是因爲其出現的頻度不高,尤其是這裏提到的直接排序算法,基本在筆試的選擇填空問時間空間複雜度的時候纔可能出現。畢竟排序速度比較慢,因此算法大題中考察的次數比較比較少。
6)筆試面試例題
例題1、
請寫出鏈表的插入排序程序
- template<typename T>
- struct list_node
- {
- struct list_node<T> *next;
- T value;
- };
- template<typename T>
- struct _list
- {
- struct list_node<T> *head;
- int size;
- };
- template<typename T>
- void SortLink(struct _list<T> * link) {
- struct list_node<T> *pHead,*pRear,*p,*tp;
- if (!link) return;
- for (pHead=link->head,pRear=0;pHead;pHead=pHead->next) {
- for (tp=pHead,p=pHead->next;p;tp=p,p=p->next)
- if (pHead->value>=p->value)
- tp->next=p->next,p->next=pHead,pHead=p,p=tp;
- if (!pRear) link->head=pHead;
- else pRear->next=pHead;
- pRear=pHead;
- }
- }
例題2、
下列排序算法中最壞複雜度不是n(n-1)/2的是 D
A.快速排序 B.冒泡排序 C.直接插入排序 D.堆排序
二、二分插入排序
1)算法簡介
二分(折半)插入(Binary insert sort)排序是一種在直接插入排序算法上進行小改動的排序算法。其與直接排序算法最大的區別在於查找插入位置時使用的是二分查找的方式,在速度上有一定提升。
2)算法描述和分析
一般來說,插入排序都採用in-place在數組上實現。具體算法描述如下:
1、從第一個元素開始,該元素可以認爲已經被排序
2、取出下一個元素,在已經排序的元素序列中二分查找到第一個比它大的數的位置
3、將新元素插入到該位置後
4、重複上述兩步
1)穩定
2)空間代價:O(1)
3)時間代價:插入每個記錄需要O(log i)比較,最多移動i+1次,最少2次。最佳情況O(n log n),最差和平均情況O(n^2)。
二分插入排序是一種穩定的排序。當n較大時,總排序碼比較次數比直接插入排序的最差情況好得多,但比最好情況要差,所元素初始序列已經按排序碼接近有序時,直接插入排序比二分插入排序比較次數少。二分插入排序元素移動次數與直接插入排序相同,依賴於元素初始序列。
3)算法圖解、flash演示、視頻演示
圖解:
視頻:二分插入排序
http://v.youku.com/v_show/id_XMTA1MTkwMTEy.html
4)算法代碼
- void BinInsertSort(int a[], int n)
- {
- int key, left, right, middle;
- for (int i=1; i<n; i++)
- {
- key = a[i];
- left = 0;
- right = i-1;
- while (left<=right)
- {
- middle = (left+right)/2;
- if (a[middle]>key)
- right = middle-1;
- else
- left = middle+1;
- }
- for(int j=i-1; j>=left; j--)
- {
- a[j+1] = a[j];
- }
- a[left] = key;
- }
- }
5)考察點,重點和頻度分析
這個排序算法在筆試面試中出現的頻度也不高,但畢竟是直接排序算法的一個小改進算法,同時二分查找又是很好的思想,有可能會在面試的時候提到,算法不難,留心一下就會了。
6)筆試面試例題
例題1、
下面的排序算法中,初始數據集的排列順序對算法的性能無影響的是(B)
A、二分插入排序 B、堆排序 C、冒泡排序 D、快速排序
例題2、
寫出下列算法的時間複雜度。
(1)冒泡排序;(2)選擇排序;(3)插入排序;(4)二分插入排序;(5)快速排序;(6)堆排序;(7)歸併排序;
三、希爾排序
1)算法簡介
希爾排序,也稱遞減增量排序算法,因DL.Shell於1959年提出而得名,是插入排序的一種高速而穩定的改進版本。
2)算法描述
1、先取一個小於n的整數d1作爲第一個增量,把文件的全部記錄分成d1個組。
2、所有距離爲d1的倍數的記錄放在同一個組中,在各組內進行直接插入排序。
3、取第二個增量d2<d1重複上述的分組和排序,
4、直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有記錄放在同一組中進行直接插入排序爲止。
希爾排序的時間複雜度與增量序列的選取有關,例如希爾增量時間複雜度爲O(n^2),而Hibbard增量的希爾排序的時間複雜度爲O(N^(5/4)),但是現今仍然沒有人能找出希爾排序的精確下界。
3)算法圖解、flash演示、視頻演示
圖解:
Flash:
http://ds.fzu.edu.cn/fine/resources/FlashContent.asp?id=92
視頻:希爾排序Shell Sort 舞蹈
http://v.youku.com/v_show/id_XMjU4NTcwMDIw.html
4)算法代碼
- #include <stdio.h>
- int main()
- {
- const int n = 5;
- int i, j, temp;
- int gap = 0;
- int a[] = {5, 4, 3, 2, 1};
- while (gap<=n)
- {
- gap = gap * 3 + 1;
- }
- while (gap > 0)
- {
- for ( i = gap; i < n; i++ )
- {
- j = i - gap;
- temp = a[i];
- while (( j >= 0 ) && ( a[j] > temp ))
- {
- a[j + gap] = a[j];
- j = j - gap;
- }
- a[j + gap] = temp;
- }
- gap = ( gap - 1 ) / 3;
- }
- }
5)考察點,重點和頻度分析
事實上希爾排序算法在筆試面試中出現的頻度也不比直接插入排序高,但它的時間複雜度並不是一個定值,所以偶爾會被面試官問到選擇的步長和時間複雜度的關係,要稍微有點了解吧。算法大題中使用該方法或者其思想的題也不多。
6)筆試面試例題
例題1、
寫出希爾排序算法程序,並說明最壞的情況下需要進行多少次的比較和交換。
程序略,需要O(n^2)次的比較
例題2、
設要將序列(Q, H, C, Y, P, A, M, S, R, D, F, X)中的關鍵碼按字母序的升序重新排列,則:
冒泡排序一趟掃描的結果是 H, C, Q, P, A, M, S, R, D, F, X ,Y ;
初始步長爲4的希爾(shell)排序一趟的結果是 P, A, C, S, Q, D, F, X , R, H,M, Y ;
二路歸併排序一趟掃描的結果是 H, Q, C, Y,A, P, M, S, D, R, F, X ;
快速排序一趟掃描的結果是 F, H, C, D, P, A, M, Q, R, S, Y,X ;
堆排序初始建堆的結果是 A, D, C, R, F, Q, M, S, Y,P, H, X 。
四、選擇排序
1)算法簡介
選擇排序(Selection sort)是一種簡單直觀的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。
2)算法描述和分析
n個記錄的文件的直接選擇排序可經過n-1趟直接選擇排序得到有序結果:
1、初始狀態:無序區爲R[1..n],有序區爲空。
2、第i趟排序(i=1,2,3...n-1)
第i趟排序開始時,當前有序區和無序區分別爲R[1..i-1]和R(i..n)。該趟排序從當前無序區中選出關鍵字最小的記錄 R[k],將它與無序區的第1個記錄R交換,使R[1..i]和R分別變爲記錄個數增加1個的新有序區和記錄個數減少1個的新無序區。
3、前n-1趟結束,數組有序化了
選擇排序的交換操作介於0和(n-1)次之間。選擇排序的比較操作爲n(n-1)/2次之間。選擇排序的賦值操作介於0和3(n-1)次之間。比較次數O(n^2),比較次數與關鍵字的初始狀態無關,總的比較次數N=(n-1)+(n-2)+...+1=n*(n-1)/2。 交換次數O(n),最好情況是,已經有序,交換0次;最壞情況是,逆序,交換n-1次。 交換次數比冒泡排序少多了,由於交換所需CPU時間比比較所需的CPU時間多,n值較小時,選擇排序比冒泡排序快。
最差時間複雜度 |
О(n²) |
最優時間複雜度 |
О(n²) |
平均時間複雜度 |
О(n²) |
最差空間複雜度 |
О(n) total, O(1) |
3)算法圖解、flash演示、視頻演示
圖解:
Flash:
http://ds.fzu.edu.cn/fine/resources/FlashContent.asp?id=85
視頻:選擇排序Select Sort排序舞蹈
http://v.youku.com/v_show/id_XMjU4NTY5NTcy.html
4)算法代碼
- void selection_sort(int *a, int len)
- {
- register int i, j, min, t;
- for(i = 0; i < len - 1; i ++)
- {
- min = i;
- //查找最小值
- for(j = i + 1; j < len; j ++)
- if(a[min] > a[j])
- min = j;
- //交換
- if(min != i)
- {
- t = a[min];
- a[min] = a[i];
- a[i] = t;
- }
- }
- }
5)考察點,重點和頻度分析
就博主看過的筆試面試題而言,選擇算法也大多出現在選擇填空中,要熟悉其時間和空間複雜度,最好最壞的情況分別是什麼,以及在那種情況下,每一輪的比較次數等。
6)筆試面試例題
例題1、
在插入和選擇排序中,若初始數據基本正序,則選用 插入排序(到尾部) ;若初始數據基本反序,則選用 選擇排序 。
例題2、
下述幾種排序方法中,平均查找長度(ASL)最小的是
A. 插入排序 B.快速排序 C. 歸併排序 D. 選擇排序
五、冒泡排序
1)算法簡介
冒泡排序是一種簡單的排序算法。它重複地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個算法的名字由來是因爲越小的元素會經由交換慢慢“浮”到數列的頂端。
2)算法描述
1、比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
2、對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。在這一點,最後的元素應該會是最大的數。
3、針對所有的元素重複以上的步驟,除了最後一個。
4、持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。
冒泡排序是與插入排序擁有相等的執行時間,但是兩種法在需要的交換次數卻很大地不同。在最壞的情況,冒泡排序需要O(n^2)次交換,而插入排序只要最多O(n)交換。冒泡排序的實現(類似下面)通常會對已經排序好的數列拙劣地執行(O(n^2)),而插入排序在這個例子只需要O(n)個運算。因此很多現代的算法教科書避免使用冒泡排序,而用插入排序取代之。冒泡排序如果能在內部循環第一次執行時,使用一個旗標來表示有無需要交換的可能,也有可能把最好的複雜度降低到O(n)。在這個情況,在已經排序好的數列就無交換的需要。若在每次走訪數列時,把走訪順序和比較大小反過來,也可以稍微地改進效率。有時候稱爲往返排序,因爲算法會從數列的一端到另一端之間穿梭往返。
最差時間複雜度 |
O(n^2) |
最優時間複雜度 |
O(n) |
平均時間複雜度 |
O(n^2) |
最差空間複雜度 |
總共O(n),需要輔助空間O(1) |
3)算法圖解、flash演示、視頻演示
圖解:
Flash:
http://student.zjzk.cn/course_ware/data_structure/web/flashhtml/maopaopaixu.htm
視頻:舞動的排序算法 冒泡排序
http://v.youku.com/v_show/id_XMzMyOTAyMzQ0.html
4)算法代碼
- #include <stdio.h>
- void bubbleSort(int arr[], int count)
- {
- int i = count, j;
- int temp;
- while(i > 0)
- {
- for(j = 0; j < i - 1; j++)
- {
- if(arr[j] > arr[j + 1])
- { temp = arr[j];
- arr[j] = arr[j + 1];
- arr[j + 1] = temp;
- }
- }
- i--;
- }
- }
- int main()
- {
- //測試數據
- int arr[] = {5, 4, 1, 3, 6};
- //冒泡排序
- bubbleSort(arr, 5);
- //打印排序結果
- int i;
- for(i = 0; i < 5; i++)
- printf("%4d", arr[i]);
- }
5)考察點,重點和頻度分析
一般我們學到的第一個排序算法就是冒泡排序,不得不說,這個還真是一個很常見的考點,平均時間空間複雜度,最好最壞情況下的時間空間複雜度,在不同情況下每一趟的比較次數,以及加標誌位減少比較次數等,都是需要注意的地方。
6)筆試面試例題
例題1、
對於整數序列100,99,98,…3,2,1,如果將它完全倒過來,分別用冒泡排序,它們的比較次數和交換次數各是多少?
答:冒泡排序的比較和交換次數將最大,都是1+2+…+n-1=n(n-1)/2=50×99=4545次。
例題2、
把一個字符串的大寫字母放到字符串的後面,各個字符的相對位置不變,不能申請額外的空間。
事實上,這道題放到冒泡排序這裏不知道是不是特別合適,只是有一種解法是類似冒泡的思想,如下解法一
解法一、
每次遇到大寫字母就往後冒,最後結果即爲所求
- #include <stdio.h>
- #include <string.h>
- //題目以及要求:把一個字符串的大寫字母放到字符串的後面,
- //各個字符的相對位置不變,不能申請額外的空間。
- //判斷是不是大寫字母
- int isUpperAlpha(char c){
- if(c >= 'A' && c <= 'Z'){
- return 1;
- }
- return 0;
- }
- //交換兩個字母
- void swap(char *a, char *b){
- char temp = *a;
- *a = *b;
- *b = temp;
- }
- char * mySort(char *arr, int len){
- if(arr == NULL || len <= 0){
- return NULL;
- }
- int i = 0, j = 0, k = 0;
- for(i = 0; i < len; i++){
- for(j = len - 1 - i; j >= 0; j--){
- if(isUpperAlpha(arr[j])){
- for(k = j; k < len - i - 1; k++){
- swap(&arr[k], &arr[k + 1]);
- }
- break;
- }
- //遍歷完了字符數組,但是沒發現大寫字母,所以沒必要再遍歷下去
- if(j == 0 && !isUpperAlpha(arr[j])){
- //結束;
- return arr;
- }
- }
- }
- //over:
- return arr;
- }
- int main(){
- char arr[] = "aaaaaaaaaaaaaaaaaaaaaaaAbcAdeBbDc";
- printf("%s\n", mySort(arr, strlen(arr)));
- return 0;
- }
解法二。
步驟如下
1、兩個指針p1和p2,從後往前掃描
2、p1遇到一個小寫字母時停下, p2遇到大寫字母時停下,兩者所指向的char交換
3、p1, p2同時往前一格
代碼如下:
- #include <stdio.h>
- #include <string.h>
- //判斷是不是大寫字母
- int isUpperAlpha(char c){
- if(c >= 'A' && c <= 'Z'){
- return 1;
- }
- return 0;
- }
- //交換兩個字母
- void swap(char *a, char *b){
- char temp = *a;
- *a = *b;
- *b = temp;
- }
- char * Reorder(char *arr, int len){
- if(arr == NULL || len <= 0){
- return NULL;
- }
- int *p1 = arr;
- int *p2 = arr;
- While(p1<arr+len && p2<arr+len){
- While( isUpperAlpha(*p1) ){
- P1++;
- }
- While( !isUpperAlpha(*p2) ){
- P2++;
- }
- swap(p1, p2)
- }
- //結束
- return arr;
- }
- int main(){
- char arr[] = "aaaaaaaaaaaaaaaaaaaaaaaAbcAdeBbDc";
- printf("%s\n", Reorder(arr, strlen(arr)));
- return 0;
- }
六、雞尾酒排序/雙向冒泡排序
1)算法簡介
雞尾酒排序等於是冒泡排序的輕微變形。不同的地方在於從低到高然後從高到低,而冒泡排序則僅從低到高去比較序列裏的每個元素。他可以得到比冒泡排序稍微好一點的效能,原因是冒泡排序只從一個方向進行比對(由低到高),每次循環只移動一個項目。
2)算法描述和分析
1、依次比較相鄰的兩個數,將小數放在前面,大數放在後面;
2、第一趟可得到:將最大數放到最後一位。
3、第二趟可得到:將第二大的數放到倒數第二位。
4、如此下去,重複以上過程,直至最終完成排序。
雞尾酒排序最糟或是平均所花費的次數都是O(n^2),但如果序列在一開始已經大部分排序過的話,會接近O(n)。
最差時間複雜度 |
O(n^2) |
最優時間複雜度 |
O(n) |
平均時間複雜度 |
O(n^2) |
3)算法圖解、flash演示、視頻演示
圖解:
Flash:
參見http://zh.wikipedia.org/zh-cn/%E9%B8%A1%E5%B0%BE%E9%85%92%E6%8E%92%E5%BA%8F右側flash動畫
4)算法代碼
- void CocktailSort(int *a,int nsize)
- {
- int tail=nsize-1;
- for (int i=0;i<tail;)
- {
- for (int j=tail;j>i;--j) //第一輪,先將最小的數據排到前面
- {
- if (a[j]<a[j-1])
- {
- int temp=a[j];
- a[j]=a[j-1];
- a[j-1]=temp;
- }
- }
- ++i; //原來i處數據已排好序,加1
- for (j=i;j<tail;++j) //第二輪,將最大的數據排到後面
- {
- if (a[j]>a[j+1])
- {
- int temp=a[j];
- a[j]=a[j+1];
- a[j+1]=temp;
- }
- }
- tail--; //原tail處數據也已排好序,將其減1
- }
- }
5)考察點,重點和頻度分析
雞尾酒排序在博主印象中出現的頻度也不高,用到它的算法題大題很少,選擇填空出現的話多以雙向冒泡排序的名稱出現,注意注意時間空間複雜度,理解理解算法應該問題就不大了。
6)筆試面試例題
考點基本類似冒泡排序,請參考上一節
七、快速排序
恩,重頭戲開始了,快速排序是各種筆試面試最愛考的排序算法之一,且排序思想在很多算法題裏面被廣泛使用。是需要重點掌握的排序算法。
1)算法簡介
快速排序是由東尼·霍爾所發展的一種排序算法。其基本思想是基本思想是,通過一趟排序將待排記錄分隔成獨立的兩部分,其中一部分記錄的關鍵字均比另一部分的關鍵字小,則可分別對這兩部分記錄繼續進行排序,以達到整個序列有序。
2)算法描述和分析
快速排序使用分治法來把一個串(list)分爲兩個子串行(sub-lists)。
步驟爲:
1、從數列中挑出一個元素,稱爲 "基準"(pivot),
2、重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分區退出之後,該基準就處於數列的中間位置。這個稱爲分區(partition)操作。
3、遞歸地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。
遞歸的最底部情形,是數列的大小是零或一,也就是永遠都已經被排序好了。雖然一直遞歸下去,但是這個算法總會退出,因爲在每次的迭代(iteration)中,它至少會把一個元素擺到它最後的位置去。
算法僞代碼描述:
function quicksort(q)
var list less, pivotList, greater
if length(q) ≤ 1 {
return q
} else {
select a pivot value pivot from q
for each x in q except the pivot element
if x < pivot then add x to less
if x ≥ pivot then add x to greater
add pivot to pivotList
return concatenate(quicksort(less), pivotList, quicksort(greater))
}
在平均狀況下,排序 n 個項目要Ο(n log n)次比較。在最壞狀況下則需要Ο(n^2)次比較,但這種狀況並不常見。事實上,快速排序通常明顯比其他Ο(n log n) 算法更快,因爲它的內部循環(inner loop)可以在大部分的架構上很有效率地被實現出來。
最差時間複雜度 |
O(n^2) |
最優時間複雜度 |
O(n log n) |
平均時間複雜度 |
O(n log n) |
最差空間複雜度 |
根據實現的方式不同而不同 |
3)算法圖解、flash演示、視頻演示
圖解:
快速排序會遞歸地進行很多輪,其中每一輪稱之爲快排的partition算法,即上述算法描述中的第2步,非常重要,且在各種筆試面試中用到該思想的算法題層出不窮,下圖爲第一輪的partition算法的一個示例。
Flash:
可一步步參見http://ds.fzu.edu.cn/fine/resources/FlashContent.asp?id=86中的快排過程
視頻 舞動的排序算法
http://v.youku.com/v_show/id_XMzMyODk4NTQ4.html
4)算法代碼
事實上,這個地方需要提一下的是,快排有很多種版本。例如,我們“基準數”的選擇方法不同就有不同的版本,但重要的是快排的思想,我們熟練掌握一種版本,在最後的筆試面試中也夠用了,我這裏羅列幾種最有名的版本C代碼。
1、版本一
我們選取數組的第一個元素作爲主元,每一輪都是和第一個元素比較大小,通過交換,分成大於和小於它的前後兩部分,再遞歸處理。
代碼如下
- /**************************************************
- 函數功能:對數組快速排序
- 函數參數:指向整型數組arr的首指針arr;
- 整型變量left和right左右邊界的下標
- 函數返回值:空
- /**************************************************/
- void QuickSort(int *arr, int left, int right)
- {
- int i,j;
- if(left<right)
- {
- i=left;j=right;
- arr[0]=arr[i]; //準備以本次最左邊的元素值爲標準進行劃分,先保存其值
- do
- {
- while(arr[j]>arr[0] && i<j)
- j--; //從右向左找第1個小於標準值的位置j
- if(i<j) //找到了,位置爲j
- {
- arr[i] = arr[j];
- i++;
- } //將第j個元素置於左端並重置i
- while(arr[i]<arr[0] && i<j)
- i++; //從左向右找第1個大於標準值的位置i
- if(i<j) //找到了,位置爲i
- {
- arr[j] = arr[i];
- j--;
- } //將第i個元素置於右端並重置j
- }while(i!=j);
- arr[i] = arr[0]; //將標準值放入它的最終位置,本次劃分結束
- quicksort(arr, left, i-1); //對標準值左半部遞歸調用本函數
- quicksort(arr, i+1, right); //對標準值右半部遞歸調用本函數
- }
- }
2、版本二
隨機選基準數的快排
- //使用引用,完成兩數交換
- void Swap(int& a , int& b)
- {
- int temp = a;
- a = b;
- b = temp;
- }
- //取區間內隨機數的函數
- int Rand(int low, int high)
- {
- int size = hgh - low + 1;
- return low + rand()%size;
- }
- //快排的partition算法,這裏的基準數是隨機選取的
- int RandPartition(int* data, int low , int high)
- {
- swap(data[rand(low,high)], data[low]);//
- int key = data[low];
- int i = low;
- for(int j=low+1; j<=high; j++)
- {
- if(data[j]<=key)
- {
- i = i+1;
- swap(data[i], data[j]);
- }
- }
- swap(data[i],data[low]);
- return i;
- }
- //遞歸完成快速排序
- void QuickSort(int* data, int low, int high)
- {
- if(low<high)
- {
- int k = RandPartition(data,low,high);
- QuickSort(data,low,k-1);
- QuickSort(data,k+1,high);
- }
- }
5)考察點,重點和頻度分析
完全考察快排算法本身的題目,多出現在選擇填空,基本是關於時間空間複雜度的討論,最好最壞的情形交換次數等等。倒是快排的partition算法需要特別注意!頻度極高地被使用在各種算法大題中!詳見下小節列舉的面試小題。
6)筆試面試例題
這裏要重點強調的是快排的partition算法,博主當年面試的時候就遇到過數道用該思路的算法題,舉幾道如下:
例題1、
最小的k個數,輸入n個整數,找出其中最下的k個數,例如輸入4、5、1、6、2、7、3、8、1、2,輸出最下的4個數,則輸出1、1、2、2。
當然,博主也知道這題可以建大小爲k的大頂堆,然後用堆的方法解決。
但是這個題目可也以仿照快速排序,運用partition函數進行求解,不過我們完整的快速排序分割後要遞歸地對前後兩段繼續進行分割,而這裏我們需要做的是判定分割的位置,然後再確定對前段還是後段進行分割,所以只對單側分割即可。代碼如下:
- void GetLeastNumbers_by_partition(int* input, int n, int* output, int k)
- {
- if(input == NULL || output == NULL || k > n || n <= 0 || k <= 0)
- return;
- int start = 0;
- int end = n - 1;
- int index = Partition(input, n, start, end);
- while(index != k - 1)
- {
- if(index > k - 1)
- {
- end = index - 1;
- index = Partition(input, n, start, end);
- }
- else
- {
- start = index + 1;
- index = Partition(input, n, start, end);
- }
- }
- for(int i = 0; i < k; ++i)
- output[i] = input[i];
- }
例題2、
判斷數組中出現超過一半的數字
當然,這道題很多人都見過,而且最通用的一種解法是數對對消的思路。這裏只是再給大家提供一種思路,快排partition的方法在很多地方都能使用,比如這題。我們也可以選擇合適的判定條件進行遞歸。代碼如下:
- bool g_bInputInvalid = false;
- bool CheckInvalidArray(int* numbers, int length)
- {
- g_bInputInvalid = false;
- if(numbers == NULL && length <= 0)
- g_bInputInvalid = true;
- return g_bInputInvalid;
- }
- bool CheckMoreThanHalf(int* numbers, int length, int number)
- {
- int times = 0;
- for(int i = 0; i < length; ++i)
- {
- if(numbers[i] == number)
- times++;
- }
- bool isMoreThanHalf = true;
- if(times * 2 <= length)
- {
- g_bInputInvalid = true;
- isMoreThanHalf = false;
- }
- return isMoreThanHalf;
- }
- int MoreThanHalfNum_Solution1(int* numbers, int length)
- {
- if(CheckInvalidArray(numbers, length))
- return 0;
- int middle = length >> 1;
- int start = 0;
- int end = length - 1;
- int index = Partition(numbers, length, start, end);
- while(index != middle)
- {
- if(index > middle)
- {
- end = index - 1;
- index = Partition(numbers, length, start, end);
- }
- else
- {
- start = index + 1;
- index = Partition(numbers, length, start, end);
- }
- }
- int result = numbers[middle];
- if(!CheckMoreThanHalf(numbers, length, result))
- result = 0;
- return result;
- }
例題3、
有一個由大小寫組成的字符串,現在需要對他進行修改,將其中的所有小寫字母排在大寫字母的前面(不要求保持原順序)
這題可能大家都能想到的方法是:設置首尾兩個指針,首指針向後移動尋找大寫字母,尾指針向前移動需找小寫字母,找到後都停下,交換。之後繼續移動,直至相遇。這種方法在這裏我就不做討論寫代碼了。
但是這題也可以採用類似快排的partition。這裏使用從左往後掃描的方式。字符串在調整的過程中可以分成兩個部分:已排好的小寫字母部分、待調整的剩餘部分。用兩個指針i和j,其中i指向待調整的剩餘部分的第一個元素,用j指針遍歷待調整的部分。當j指向一個小寫字母時,交換i和j所指的元素。向前移動i、j,直到字符串末尾。代碼如下:
- #include <iostream>
- using namespace std;
- void Proc( char *str )
- {
- int i = 0;
- int j = 0;
- //移動指針i, 使其指向第一個大寫字母
- while( str[i] != '\0' && str[i] >= 'a' && str[i] <= 'z' ) i++;
- if( str[i] != '\0' )
- {
- //指針j遍歷未處理的部分,找到第一個小寫字母
- for( j=i; str[j] != '\0'; j++ )
- {
- if( str[j] >= 'a' && str[j] <= 'z' )
- {
- char tmp = str[i];
- str[i] = str[j];
- str[j] = tmp;
- i++;
- }
- }
- }
- }
- int main()
- {
- char data[] = "SONGjianGoodBest";
- Proc( data );
- return 0;
- }
八、堆排序
不得不說,堆排序太容易出現了,選擇填空問答算法大題都會出現。建堆的過程,堆調整的過程,這些過程的時間複雜度,空間複雜度,以及如何應用在海量數據Top K問題中等等,都是需要重點掌握的。
1)算法簡介
堆排序(Heapsort)是指利用堆這種數據結構所設計的一種排序算法。堆積是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。
2)算法描述
我們這裏介紹幾個問題,一步步推到堆排序的算法。
1、什麼是堆?
我們這裏提到的堆一般都指的是二叉堆,它滿足二個特性:
1---父結點的鍵值總是大於或等於(小於或等於)任何一個子節點的鍵值。
2---每個結點的左子樹和右子樹都是一個二叉堆(都是最大堆或最小堆)。
如下爲一個最小堆(父結點的鍵值總是小於任何一個子節點的鍵值)
2、什麼是堆調整(Heap Adjust)?
這是爲了保持堆的特性而做的一個操作。對某一個節點爲根的子樹做堆調整,其實就是將該根節點進行“下沉”操作(具體是通過和子節點交換完成的),一直下沉到合適的位置,使得剛纔的子樹滿足堆的性質。
例如對最大堆的堆調整我們會這麼做:
1、在對應的數組元素A[i], 左孩子A[LEFT(i)], 和右孩子A[RIGHT(i)]中找到最大的那一個,將其下標存儲在largest中。
2、如果A[i]已經就是最大的元素,則程序直接結束。
3、否則,i的某個子結點爲最大的元素,將A[largest]與A[i]交換。
4、再從交換的子節點開始,重複1,2,3步,直至葉子節點,算完成一次堆調整。
這裏需要提一下的是,一般做一次堆調整的時間複雜度爲log(n)。
如下爲我們對4爲根節點的子樹做一次堆調整的示意圖,可幫我們理解。
3、如何建堆
建堆是一個通過不斷的堆調整,使得整個二叉樹中的數滿足堆性質的操作。在數組中的話,我們一般從下標爲n/2的數開始做堆調整,一直到下標爲0的數(因爲下標大於n/2的數都是葉子節點,其子樹已經滿足堆的性質了)。下圖爲其一個圖示:
很明顯,對葉子結點來說,可以認爲它已經是一個合法的堆了即20,60, 65, 4, 49都分別是一個合法的堆。只要從A[4]=50開始向下調整就可以了。然後再取A[3]=30,A[2] = 17,A[1] = 12,A[0] = 9分別作一次向下調整操作就可以了。
4、如何進行堆排序
堆排序是在上述3中對數組建堆的操作之後完成的。
數組儲存成堆的形式之後,第一次將A[0]與A[n - 1]交換,再對A[0…n-2]重新恢復堆。第二次將A[0]與A[n-2]交換,再對A[0…n-3]重新恢復堆,重複這樣的操作直到A[0]與A[1]交換。由於每次都是將最小的數據併入到後面的有序區間,故操作完成後整個數組就有序了。
如下圖所示:
最差時間複雜度 |
O(n log n) |
最優時間複雜度 |
O(n log n) |
平均時間複雜度 |
O(n log n) |
最差空間複雜度 |
O(n) |
3)算法圖解、flash演示、視頻演示
圖解:
略,見上一節。
Flash:
可參見http://ds.fzu.edu.cn/fine/resources/FlashContent.asp?id=88中的flash動畫,幫助理解
視頻 堆排序
http://v.youku.com/v_show/id_XMzQzNzAwODQ=.html
4)算法代碼
直接上代碼吧,重點注意HeapAdjust,BuildHeap和HeapSort的實現。
- #include <cstdio>
- #include <cstdlib>
- #include <cmath>
- using namespace std;
- int parent(int);
- int left(int);
- int right(int);
- void HeapAdjust(int [], int, int);
- void BuildHeap(int [], int);
- void print(int [], int);
- void HeapSort(int [], int);
- /*返回父節點*/
- int parent(int i)
- {
- return (int)floor((i - 1) / 2);
- }
- /*返回左孩子節點*/
- int left(int i)
- {
- return (2 * i + 1);
- }
- /*返回右孩子節點*/
- int right(int i)
- {
- return (2 * i + 2);
- }
- /*對以某一節點爲根的子樹做堆調整(保證最大堆性質)*/
- void HeapAdjust(int A[], int i, int heap_size)
- {
- int l = left(i);
- int r = right(i);
- int largest;
- int temp;
- if(l < heap_size && A[l] > A[i])
- {
- largest = l;
- }
- else
- {
- largest = i;
- }
- if(r < heap_size && A[r] > A[largest])
- {
- largest = r;
- }
- if(largest != i)
- {
- temp = A[i];
- A[i] = A[largest];
- A[largest] = temp;
- HeapAdjust(A, largest, heap_size);
- }
- }
- /*建立最大堆*/
- void BuildHeap(int A[],int heap_size)
- {
- for(int i = (heap_size-2)/2; i >= 0; i--)
- {
- HeapAdjust(A, i, heap_size);
- }
- }
- /*輸出結果*/
- void print(int A[], int heap_size)
- {
- for(int i = 0; i < heap_size;i++)
- {
- printf("%d ", A[i]);
- }
- printf("\n");
- }
- /*堆排序*/
- void HeapSort(int A[], int heap_size)
- {
- BuildHeap(A, heap_size);
- int temp;
- for(int i = heap_size - 1; i >= 0; i--)
- {
- temp = A[0];
- A[0] = A[i];
- A[i] = temp;
- HeapAdjust(A, 0, i);
- }
- print(A, heap_size);
- }
- /*測試,對給定數組做堆排序*/
- int main(int argc, char* argv[])
- {
- const int heap_size = 13;
- int A[] = {19, 1, 10, 14, 16, 4, 7, 9, 3, 2, 8, 5, 11};
- HeapSort(A, heap_size);
- system("pause");
- return 0;
- }
5)考察點,重點和頻度分析
堆排序相關的考察太多了,選擇填空問答算法大題都會出現。建堆的過程,堆調整的過程,這些過程的時間複雜度,空間複雜度,需要比較交換多少次,以及如何應用在海量數據Top K問題中等等。堆又是一種很好做調整的結構,在算法題裏面使用頻度很高。
6)筆試面試題
例題1、
編寫算法,從10億個浮點數當中,選出其中最大的10000個。
典型的Top K問題,用堆是最典型的思路。建10000個數的小頂堆,然後將10億個數依次讀取,大於堆頂,則替換堆頂,做一次堆調整。結束之後,小頂堆中存放的數即爲所求。代碼如下(爲了方便,這裏直接使用了STL容器):
- #include "stdafx.h"
- #include <vector>
- #include <iostream>
- #include <algorithm>
- #include <functional> // for greater<>
- using namespace std;
- int _tmain(int argc, _TCHAR* argv[])
- {
- vector<float> bigs(10000,0);
- vector<float>::iterator it;
- // Init vector data
- for (it = bigs.begin(); it != bigs.end(); it++)
- {
- *it = (float)rand()/7; // random values;
- }
- cout << bigs.size() << endl;
- make_heap(bigs.begin(),bigs.end(), greater<float>()); // The first one is the smallest one!
- float ff;
- for (int i = 0; i < 1000000000; i++)
- {
- ff = (float) rand() / 7;
- if (ff > bigs.front()) // replace the first one ?
- {
- // set the smallest one to the end!
- pop_heap(bigs.begin(), bigs.end(), greater<float>());
- // remove the last/smallest one
- bigs.pop_back();
- // add to the last one
- bigs.push_back(ff);
- // mask heap again, the first one is still the smallest one
- push_heap(bigs.begin(),bigs.end(),greater<float>());
- }
- }
- // sort by ascent
- sort_heap(bigs.begin(), bigs.end(), greater<float>());
- return 0;
- }
例題2、
設計一個數據結構,其中包含兩個函數,1.插入一個數字,2.獲得中數。並估計時間複雜度。
使用大頂堆和小頂堆存儲。
使用大頂堆存儲較小的一半數字,使用小頂堆存儲較大的一半數字。
插入數字時,在O(logn)時間內將該數字插入到對應的堆當中,並適當移動根節點以保持兩個堆數字相等(或相差1)。
獲取中數時,在O(1)時間內找到中數。
九、歸併排序
1)算法簡介
歸併排序是建立在歸併操作上的一種有效的排序算法。該算法是採用分治法(Divide and Conquer)的一個非常典型的應用。歸併排序是一種穩定的排序方法。
將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱爲2-路歸併。
2)算法描述
歸併排序具體算法描述如下(遞歸版本):
1、Divide: 把長度爲n的輸入序列分成兩個長度爲n/2的子序列。
2、Conquer: 對這兩個子序列分別採用歸併排序。
3、Combine: 將兩個排序好的子序列合併成一個最終的排序序列。
歸併排序的效率是比較高的,設數列長爲N,將數列分開成小數列一共要logN步,每步都是一個合併有序數列的過程,時間複雜度可以記爲O(N),故一共爲O(N*logN)。因爲歸併排序每次都是在相鄰的數據中進行操作,所以歸併排序在O(N*logN)的幾種排序方法(快速排序,歸併排序,希爾排序,堆排序)也是效率比較高的。
3)算法圖解、flash演示、視頻演示
圖解:
Flash:
可以參考http://ds.fzu.edu.cn/fine/resources/FlashContent.asp?id=93中的過程
視頻:舞動的排序算法 歸併排序
http://video.sina.com.cn/v/b/80012415-1642346981.html
4)算法代碼
- //將有二個有序數列a[first...mid]和a[mid...last]合併。
- void MergeArray(int a[], int first, int mid, int last, int temp[])
- {
- int i = first, j = mid + 1;
- int m = mid, n = last;
- int k = 0;
- while (i <= m && j <= n)
- {
- if (a[i] <= a[j])
- temp[k++] = a[i++];
- else
- temp[k++] = a[j++];
- }
- 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); //右邊有序
- mergearray(a, first, mid, last, temp); //再將二個有序數列合併
- }
- }
5)考察點、重點和頻度分析
歸併排序本身作爲一種高效的排序算法,也是常會被問到的。尤其是歸併排序體現的遞歸思路很重要,在遞歸的過程中可以完成很多事情,很多算法題也是使用的這個思路,可見下面7)部分的筆試面試算法題。
6)筆試面試題
例題1、
題目輸入一個數組,數組元素的大小在0->999.999.999的範圍內,元素個數爲0-500000範圍。題目要求通過相鄰的元素的交換,使得輸入的數組變爲有序,要求輸出交換的次數
這題求解的其實就是一個逆序對。我們回想一下歸併排序的過程:
歸併排序是用分治思想,分治模式在每一層遞歸上有三個步驟:
分解:將n個元素分成個含n/2個元素的子序列。
解決:用合併排序法對兩個子序列遞歸的排序。
合併:合併兩個已排序的子序列已得到排序結果。
在歸併排序算法中稍作修改,就可以在n log n的時間內求逆序對。
將數組A[1...size],劃分爲A[1...mid] 和 A[mid+1...size].那麼逆序對數的個數爲 f(1, size) = f(1, mid) + f(mid+1, size) + s(1, mid, size),這裏s(1, mid, size)代表左值在[1---mid]中,右值在[mid+1, size]中的逆序對數。由於兩個子序列本身都已經排序,所以查找起來非常方便。
代碼如下:
- #include<iostream>
- #include<stdlib.h>
- using namespace std;
- void printArray(int arry[],int len)
- {
- for(int i=0;i<len;i++)
- cout<<arry[i]<<" ";
- cout<<endl;
- }
- int MergeArray(int arry[],int start,int mid,int end,int temp[])//數組的歸併操作
- {
- //int leftLen=mid-start+1;//arry[start...mid]左半段長度
- //int rightLlen=end-mid;//arry[mid+1...end]右半段長度
- int i=mid;
- int j=end;
- int k=0;//臨時數組末尾座標
- int count=0;
- //設定兩個指針ij分別指向兩段有序數組的頭元素,將小的那一個放入到臨時數組中去。
- while(i>=start&&j>mid)
- {
- if(arry[i]>arry[j])
- {
- temp[k++]=arry[i--];//從臨時數組的最後一個位置開始排序
- count+=j-mid;//因爲arry[mid+1...j...end]是有序的,如果arry[i]>arry[j],那麼也大於arry[j]之前的元素,從a[mid+1...j]一共有j-(mid+1)+1=j-mid
- }
- else
- {
- temp[k++]=arry[j--];
- }
- }
- cout<<"調用MergeArray時的count:"<<count<<endl;
- while(i>=start)//表示前半段數組中還有元素未放入臨時數組
- {
- temp[k++]=arry[i--];
- }
- while(j>mid)
- {
- temp[k++]=arry[j--];
- }
- //將臨時數組中的元素寫回到原數組當中去。
- for(i=0;i<k;i++)
- arry[end-i]=temp[i];
- printArray(arry,8);//輸出進過一次歸併以後的數組,用於理解整體過程
- return count;
- }
- int InversePairsCore(int arry[],int start,int end,int temp[])
- {
- int inversions = 0;
- if(start<end)
- {
- int mid=(start+end)/2;
- inversions+=InversePairsCore(arry,start,mid,temp);//找左半段的逆序對數目
- inversions+=InversePairsCore(arry,mid+1,end,temp);//找右半段的逆序對數目
- inversions+=MergeArray(arry,start,mid,end,temp);//在找完左右半段逆序對以後兩段數組有序,然後找兩段之間的逆序對。最小的逆序段只有一個元素。
- }
- return inversions;
- }
- int InversePairs(int arry[],int len)
- {
- int *temp=new int[len];
- int count=InversePairsCore(arry,0,len-1,temp);
- delete[] temp;
- return count;
- }
- void main()
- {
- //int arry[]={7,5,6,4};
- int arry[]={1,3,7,8,2,4,6,5};
- int len=sizeof(arry)/sizeof(int);
- //printArray(arry,len);
- int count=InversePairs(arry,len);
- //printArray(arry,len);
- //cout<<count<<endl;
- system("pause");
- }
例題2、
有10個文件,每個文件1G,每個文件的每一行存放的都是用戶的query,每個文件的query都可能重複。要求你按照query的頻度排序。
1、hash映射:順序讀取10個文件,按照hash(query)%10的結果將query寫入到另外10個文件(記爲)中。這樣新生成的文件每個的大小大約也1G(假設hash函數是隨機的)。
2、hash統計:找一臺內存在2G左右的機器,依次對用hash_map(query, query_count)來統計每個query出現的次數。注:hash_map(query,query_count)是用來統計每個query的出現次數,不是存儲他們的值,出現一次,則count+1。
3、堆/快速/歸併排序:利用快速/堆/歸併排序按照出現次數進行排序。將排序好的query和對應的query_cout輸出到文件中。這樣得到了10個排好序的文件(記爲)。對這10個文件進行歸併排序(內排序與外排序相結合)。
例題3、
歸併一個左右兩邊分別排好序的數組,空間複雜度要求O(1)。
使用原地歸併,能夠讓歸併排序的空間複雜度降爲O(1),但是速度上會有一定程度的下降。代碼如下:
- #include<iostream>
- #include<cmath>
- #include<cstdlib>
- #include<Windows.h>
- using namespace std;
- void insert_sort(int arr[],int n)
- {
- for(int i=1;i<n;++i)
- {
- int val=arr[i];
- int j=i-1;
- while(arr[j]>val&&j>=0)
- {
- arr[j+1]=arr[j];
- --j;
- }
- arr[j+1]=val;
- }
- }
- void aux_merge(int arr[],int n,int m,int aux[])
- {
- for(int i=0;i<m;++i)
- swap(aux[i],arr[n+i]);
- int p=n-1,q=m-1;
- int dst=n+m-1;
- for(int i=0;i<n+m;++i)
- {
- if(p>=0)
- {
- if(q>=0)
- {
- if(arr[p]>aux[q])
- {
- swap(arr[p],arr[dst]);
- p--;
- }
- else
- {
- swap(aux[q],arr[dst]);
- q--;
- }
- }
- else
- break;
- }
- else
- {
- swap(aux[q],arr[dst]);
- q--;
- }
- dst--;
- }
- }
- void local_merge(int arr[],int n)
- {
- int m=sqrt((float)n);
- int k=n/m;
- for(int i=0;i<m;++i)
- swap(arr[k*m-m+i],arr[n/2/m*m+i]);
- for(int i=0;i<k-2;++i)
- {
- int index=i;
- for(int j=i+1;j<k-1;++j)
- if(arr[j*m]<arr[index*m])
- index=j;
- if(index!=i)
- for(int j=0;j<m;++j)
- swap(arr[i*m+j],arr[index*m+j]);
- }
- for(int i=0;i<k-2;++i)
- aux_merge(arr+i*m,m,m,arr+(k-1)*m);
- int s=n%m+m;
- insert_sort(arr+(n-2*s),2*s);
- aux_merge(arr,n-2*s,s,arr+(k-1)*m);
- insert_sort(arr+(k-1)*m,s);
- }
- void local_merge_sort(int arr[],int n)
- {
- if(n<=1)
- return;
- if(n<=10)
- {
- insert_sort(arr,n);
- return;
- }
- local_merge_sort(arr,n/2);
- local_merge_sort(arr+n/2,n-n/2);
- local_merge(arr,n);
- }
- void merge_sort(int arr[],int temp[],int n)
- {
- if(n<=1)
- return;
- if(n<=10)
- {
- insert_sort(arr,n);
- return;
- }
- merge_sort(arr,temp,n/2);
- merge_sort(arr+n/2,temp,n-n/2);
- for(int i=0;i<n/2;++i)
- temp[i]=arr[i];
- for(int i=n/2;i<n;++i)
- temp[n+n/2-i-1]=arr[i];
- int left=0,right=n-1;
- for(int i=0;i<n;++i)
- if(temp[left]<temp[right])
- arr[i]=temp[left++];
- else
- arr[i]=temp[right--];
- }
- const int n=2000000;
- int arr1[n],arr2[n];
- int temp[n];
- int main()
- {
- for(int i=0;i<n;++i)
- arr1[i]=arr2[i]=rand();
- int begin=GetTickCount();
- merge_sort(arr1,temp,n);
- cout<<GetTickCount()-begin<<endl;
- begin=GetTickCount();
- local_merge_sort(arr2,n);
- cout<<GetTickCount()-begin<<endl;
- for(int i=0;i<n;++i)
- if(arr1[i]!=arr2[i])
- cout<<"ERROR"<<endl;
- system("pause");
- }
十、桶排序
1)算法簡介
桶排序 (Bucket sort)或所謂的箱排序,是一個排序算法,工作的原理是將數組分到有限數量的桶子裏。每個桶子再個別排序(有可能再使用別的排序算法或是以遞歸方式繼續使用桶排序進行排序)。
桶排序是穩定的,且在大多數情況下常見排序裏最快的一種,比快排還要快,缺點是非常耗空間,基本上是最耗空間的一種排序算法,而且只能在某些情形下使用。
2)算法描述和分析
桶排序具體算法描述如下:
1、設置一個定量的數組當作空桶子。
2、尋訪串行,並且把項目一個一個放到對應的桶子去。
3、對每個不是空的桶子進行排序。
4、從不是空的桶子裏把項目再放回原來的串行中。
桶排序最好情況下使用線性時間O(n),很顯然桶排序的時間複雜度,取決與對各個桶之間數據進行排序的時間複雜度,因爲 其它部分的時間複雜度都爲O(n);很顯然,桶劃分的越小,各個桶之間的數據越少,排 序所用的時間也會越少。但相應的空間消耗就會增大。
可以證明,即使選用插入排序作爲桶內排序的方法,桶排序的平均時間複雜度爲線性。 具體證明,請參考算法導論。其空間複雜度也爲線性。
3)算法圖解、flash演示、視頻演示
圖解
Flash:
可以參考http://ds.fzu.edu.cn/fine/resources/FlashContent.asp?id=90中的過程
視頻:
這裏就不給出桶排序的視頻了,見上flash吧
4)算法代碼
- #include <time.h>
- #include <iostream>
- #include <iomanip>
- using namespace std;
- /*initial arr*/
- void InitialArr(double *arr,int n)
- {
- srand((unsigned)time(NULL));
- for (int i = 0; i<n;i++)
- {
- arr[i] = rand()/double(RAND_MAX+1); //(0.1)
- }
- }
- /* print arr*/
- void PrintArr(double *arr,int n)
- {
- for (int i = 0;i < n; i++)
- {
- cout<<setw(15)<<arr[i];
- if ((i+1)%5 == 0 || i == n-1)
- {
- cout<<endl;
- }
- }
- }
- void BucketSort(double * arr,int n)
- {
- double **bucket = new double*[10];
- for (int i = 0;i<10;i++)
- {
- bucket[i] = new double[n];
- }
- int count[10] = {0};
- for (int i = 0 ; i < n ; i++)
- {
- double temp = arr[i];
- int flag = (int)(arr[i]*10); //flag標識小樹的第一位
- bucket[flag][count[flag]] = temp; //用二維數組的每個向量來存放小樹第一位相同的數據
- int j = count[flag]++;
- /* 利用插入排序對每一行進行排序 */
- for(;j > 0 && temp < bucket[flag][j - 1]; --j)
- {
- bucket[flag][j] = bucket[flag][j-1];
- }
- bucket[flag][j] =temp;
- }
- /* 所有數據重新鏈接 */
- int k=0;
- for (int i = 0 ; i < 10 ; i++)
- {
- for (int j = 0 ; j< count[i];j++)
- {
- arr[k] = bucket[i][j];
- k++;
- }
- }
- for (int i = 0 ; i<10 ;i++)
- {
- delete bucket[i];
- bucket[i] =NULL;
- }
- delete []bucket;
- bucket = NULL;
- }
- void main()
- {
- double *arr=new double[10];
- InitialArr(arr, 10);
- BucketSort(arr, 10);
- PrintArr(arr,10);
- delete [] arr;
- }
5)考察點、重點和頻度分析
桶排序是一種很巧妙的排序方法,在處理密集型數排序的時候有比較好的效果(主要是這種情況下空間複雜度不高),其思想也可用在很多算法題上,詳見後續筆試面試算法例題。
6)筆試面試題
例題1、
一年的全國高考考生人數爲500 萬,分數使用標準分,最低100 ,最高900 ,沒有小數,你把這500 萬元素的數組排個序。
對500W數據排序,如果基於比較的先進排序,平均比較次數爲O(5000000*log5000000)≈1.112億。但是我們發現,這些數據都有特殊的條件: 100=<score<=900。那麼我們就可以考慮桶排序這樣一個“投機取巧”的辦法、讓其在毫秒級別就完成500萬排序。
創建801(900-100)個桶。將每個考生的分數丟進f(score)=score-100的桶中。這個過程從頭到尾遍歷一遍數據只需要500W次。然後根據桶號大小依次將桶中數值輸出,即可以得到一個有序的序列。而且可以很容易的得到100分有***人,501分有***人。
實際上,桶排序對數據的條件有特殊要求,如果上面的分數不是從100-900,而是從0-2億,那麼分配2億個桶顯然是不可能的。所以桶排序有其侷限性,適合元素值集合並不大的情況。
例題2、
在一個文件中有 10G 個整數,亂序排列,要求找出中位數。內存限制爲 2G。只寫出思路即可(內存限制爲 2G的意思就是,可以使用2G的空間來運行程序,而不考慮這臺機器上的其他軟件的佔用內存)。
分析: 既然要找中位數,很簡單就是排序的想法。那麼基於字節的桶排序是一個可行的方法。
思想:將整型的每1byte作爲一個關鍵字,也就是說一個整形可以拆成4個keys,而且最高位的keys越大,整數越大。如果高位keys相同,則比較次高位的keys。整個比較過程類似於字符串的字典序。按以下步驟實施:
1、把10G整數每2G讀入一次內存,然後一次遍歷這536,870,912即(1024*1024*1024)*2 /4個數據。每個數據用位運算">>"取出最高8位(31-24)。這8bits(0-255)最多表示255個桶,那麼可以根據8bit的值來確定丟入第幾個桶。最後把每個桶寫入一個磁盤文件中,同時在內存中統計每個桶內數據的數量,自然這個數量只需要255個整形空間即可。
2、繼續以內存中的整數的次高8bit進行桶排序(23-16)。過程和第一步相同,也是255個桶。
3、一直下去,直到最低字節(7-0bit)的桶排序結束。我相信這個時候完全可以在內存中使用一次快排就可以了。
例題3、
給定n個實數x1,x2,...,xn,求這n個實數在實軸上相鄰2個數之間的最大差值M,要求設計線性的時間算法
典型的最大間隙問題。
要求線性時間算法。需要使用桶排序。桶排序的平均時間復發度是O(N).如果桶排序的數據分佈不均勻,假設都分配到同一個桶中,最壞情況下的時間複雜度將變爲O(N^2).
桶排序: 最關鍵的建桶,如果桶設計得不好的話桶排序是幾乎沒有作用的。通常情況下,上下界有兩種取法,第一種是取一個10^n或者是2^n的數,方便實現。另一種是取數列的最大值和最小值然後均分作桶。
對於這個題,最關鍵的一步是:由抽屜原理知:最大差值M>= (Max(V[n])-Min(V[n]))/(n-1)!所以,假如以(Max(V[n])-Min(V[n]))/(n-1)爲桶寬的話,答案一定不是屬於同一個桶的兩元素之差。因此,這樣建桶,每次只保留桶裏面的最大值和最小值即可。
代碼如下:
- //距離平均值爲offset = (arrayMax - arrayMin) / (n - 1), 則距離最大的數必然大於這個值
- //每個桶只要記住桶中的最大值和最小值,依次比較上一個桶的最大值與下一個桶的最小值的差值,找最大的即可.
- #include <iostream>
- #define MAXSIZE 100 //實數的個數
- #define MAXNUM 32767
- using namespace std;
- struct Barrel
- {
- double min; //桶中最小的數
- double max; //桶中最大的數
- bool flag; //標記桶中有數
- };
- int BarrelOperation(double* array, int n)
- {
- Barrel barrel[MAXSIZE]; //實際使用的桶
- int nBarrel = 0; //實際使用桶的個數
- Barrel tmp[MAXSIZE]; //臨時桶,用於暫存數據
- double arrayMax = -MAXNUM, arrayMin = MAXNUM;
- for(int i = 0; i < n; i++) {
- if(array[i] > arrayMax)
- arrayMax = array[i];
- if(array[i] < arrayMin)
- arrayMin = array[i];
- }
- double offset = (arrayMax - arrayMin) / (n - 1); //所有數的平均間隔
- //對桶進行初始化
- for(i = 0; i < n; i++) {
- tmp[i].flag = false;
- tmp[i].max = arrayMin;
- tmp[i].min = arrayMax;
- }
- //對數據進行分桶
- for(i = 0; i < n; i++) {
- int pos = (int)((array[i] - arrayMin) / offset);
- if(!tmp[pos].flag) {
- tmp[pos].max = tmp[pos].min = array[i];
- tmp[pos].flag = true;
- } else {
- if(array[i] > tmp[pos].max)
- tmp[pos].max = array[i];
- if(array[i] < tmp[pos].min)
- tmp[pos].min = array[i];
- }
- }
- for(i = 0; i <= n; i++) {
- if(tmp[i].flag)
- barrel[nBarrel++] = tmp[i];
- }
- int maxOffset = 0.0;
- for(i = 0; i < nBarrel - 1; i++) {
- if((barrel[i+1].min - barrel[i].max) > maxOffset)
- maxOffset = barrel[i+1].min - barrel[i].max;
- }
- return maxOffset;
- }
- int main()
- {
- double array[MAXSIZE] = {1, 8, 6, 11, 7, 13, 16, 5}; //所需處理的數據
- int n = 8; //數的個數
- //double array[MAXSIZE] = {8, 6, 11};
- //int n = 3;
- int maxOffset = BarrelOperation(array, n);
- cout << maxOffset << endl;
- return 0;
- }
十一、計數排序
1)算法簡介
計數排序(Counting sort)是一種穩定的排序算法。計數排序使用一個額外的數組C,其中第i個元素是待排序數組A中值等於i的元素的個數。然後根據數組C來將A中的元素排到正確的位置。它只能對整數進行排序。
2)算法描述和分析
算法的步驟如下:
1、找出待排序的數組中最大和最小的元素
2、統計數組中每個值爲i的元素出現的次數,存入數組C的第i項
3、對所有的計數累加(從C中的第一個元素開始,每一項和前一項相加)
4、反向填充目標數組:將每個元素i放在新數組的第C(i)項,每放一個元素就將C(i)減去1
當輸入的元素是n 個0到k之間的整數時,它的運行時間是 O(n + k)。計數排序不是比較排序,排序的速度快於任何比較排序算法。
由於用來計數的數組C的長度取決於待排序數組中數據的範圍(等於待排序數組的最大值與最小值的差加上1),這使得計數排序對於數據範圍很大的數組,需要大量時間和內存。例如:計數排序是用來排序0到100之間的數字的最好的算法,但是它不適合按字母順序排序人名。但是,計數排序可以用在基數排序中的算法來排序數據範圍很大的數組。
3)算法圖解、flash演示、視頻演示
圖解:
我們使用計數排序對一個亂序的整數數組進行排序。
首先創建一個臨時數組(長度爲輸入數據的最大間隔),對於每一個輸入數組的整數k,我們在臨時數組的第k位置"1"。如下圖
上圖中,第一行表示輸入數據,第二行表示創建的臨時數據,臨時數組的下標代表輸入數據的某一個值,臨時數組的值表示輸入數據中某一個值的數量。
如果輸入數據中有重複的數值,那麼我們增加臨時數組相應的值(比如上圖中5有3個,所以小標爲5的數組的值是3)。在“初始化”臨時數組以後,我們就得到了一個排序好的輸入數據。
我們順序遍歷這個數組,將下標解釋成數據, 將該位置的值表示該數據的重複數量,記得得到一個排序好的數組。
Flash:
可參見http://ds.fzu.edu.cn/fine/resources/FlashContent.asp?id=89中的flash過程
視頻:
前面的flash已經能夠清晰地表示出整個計數排序的過程了,這裏就不推薦視頻了
4)算法代碼
- #include <stdlib.h>
- #include <string.h>
- #include <stdio.h>
- /**************************************************************
- 功能:計數排序。
- 參數: data : 要排序的數組
- size :數組元素的個數
- k :數組中元素數組最大值 +1 (這個需要+1)
- 返回值: 成功0;失敗-1.
- *************************************************************/
- int ctsort(int *data, int size, int k)
- {
- int * counts = NULL,/*計數數組*/
- * temp = NULL;/*保存排序後的數組*/
- int i = 0;
- /*申請數組空間*/
- if ((counts = (int *) malloc( k * sizeof(int))) == NULL)
- return -1;
- if ((temp = (int *) malloc( k * sizeof(int))) == NULL)
- return -1;
- /*初始化計數數組*/
- for (i = 0; i < k; i ++)
- counts[i] = 0;
- /*數組中出現的元素,及出現次數記錄*/
- for(i = 0; i < size; i++)
- counts[data[i]] += 1;
- /*調整元素計數中,加上前一個數*/
- for (i = 1; i < k; i++)
- counts[i] += counts[i - 1];
- /*使用計數數組中的記錄數值,來進行排序,排序後保存的temp*/
- for (i = size -1; i >= 0; i --){
- temp[counts[data[i]] - 1] = data[i];
- counts[data[i]] -= 1;
- }
- memcpy(data,temp,size * sizeof(int));
- free(counts);
- free(temp);
- return 0;
- }
- int main()
- {
- int a[8] = {2,0,2,1,4,6,7,4};
- int max = a[0],
- i = 0;
- /*獲得數組中中的數值*/
- for ( i = 1; i < 8; i++){
- if (a[i] > max)
- max = a[i];
- }
- ctsort(a,8,max+1);
- for (i = 0;i < 8;i ++)
- printf("%d\n",a[i]);
- }
5)考察點、重點和頻度分析
計數排序在處理密集整數排序的問題的時候非常有限,尤其是有時候題目對空間並不做太大限制,那使用計數排序能夠達到O(n)的時間複雜度,遠快於所有基於比較的其他排序方法。
6)筆試面試題
例題1、
某地區年齡排序問題
夠典型的計數排序吧,年齡的區間也就那麼大,代碼就不上了,請參照上述參照計數排序算法。
十二、基數排序
1)算法簡介
基數排序是一種非比較型整數排序算法,其原理是將整數按位數切割成不同的數字,然後按每個位數分別比較。由於整數也可以表達字符串(比如名字或日期)和特定格式的浮點數,所以基數排序也不是隻能使用於整數。基數排序的發明可以追溯到1887年赫爾曼·何樂禮在打孔卡片製表機(Tabulation Machine)上的貢獻。
2)算法描述和分析
整個算法過程描述如下:
1、將所有待比較數值(正整數)統一爲同樣的數位長度,數位較短的數前面補零。
2、從最低位開始,依次進行一次排序。
3、這樣從最低位排序一直到最高位排序完成以後, 數列就變成一個有序序列。
基數排序的時間複雜度是 O(k•n),其中n是排序元素個數,k是數字位數。
注意這不是說這個時間複雜度一定優於O(n·log(n)),因爲k的大小一般會受到n的影響。 以排序n個不同整數來舉例,假定這些整數以B爲底,這樣每位數都有B個不同的數字,k就一定不小於logB(n)。由於有B個不同的數字,所以就需要B個不同的桶,在每一輪比較的時候都需要平均n·log2(B) 次比較來把整數放到合適的桶中去,所以就有:
k 大於或等於 logB(n)
每一輪(平均)需要 n·log2(B) 次比較
所以,基數排序的平均時間T就是:
T ≥ logB(n)·n·log2(B) = log2(n)·logB(2)·n·log2(B) = log2(n)·n·logB(2)·log2(B) = n·log2(n)
所以和比較排序相似,基數排序需要的比較次數:T ≥ n·log2(n)。 故其時間複雜度爲 Ω(n·log2(n)) = Ω(n·log n) 。
3)算法圖解、flash演示、視頻演示
圖解:
Flash:
可參見http://ds.fzu.edu.cn/fine/resources/FlashContent.asp?id=91中的flash過程
視頻:
http://www.tudou.com/programs/view/vfoUHC-tgi0
4)算法代碼
- #include <stdio.h>
- #include <stdlib.h>
- void radixSort(int data[]) {
- int temp[10][10] = {0};
- int order[10] = {0};
- int n = 1;
- while(n <= 10) {
- int i;
- for(i = 0; i < 10; i++) {
- int lsd = ((data[i] / n) % 10);
- temp[lsd][order[lsd]] = data[i];
- order[lsd]++;
- }
- // 重新排列
- int k = 0;
- for(i = 0; i < 10; i++) {
- if(order[i] != 0) {
- int j;
- for(j = 0; j < order[i]; j++, k++) {
- data[k] = temp[i][j];
- }
- }
- order[i] = 0;
- }
- n *= 10;
- }
- }
- int main(void) {
- int data[10] = {73, 22, 93, 43, 55, 14, 28, 65, 39, 81};
- printf("\n排序前: ");
- int i;
- for(i = 0; i < 10; i++)
- printf("%d ", data[i]);
- putchar('\n');
- radixSort(data);
- printf("\n排序後: ");
- for(i = 0; i < 10; i++)
- printf("%d ", data[i]);
- return 0;
- }
5)考察點、重點和頻度分析
計數排序在處理密集整數排序的問題的時候非常有限,尤其是有時候題目對空間並不做太大限制,那使用計數排序能夠達到O(n)的時間複雜度,遠快於所有基於比較的其他排序方法。
總結
總結一下各種排序算法如下:
名稱 |
時間複雜度 |
額外空間 |
穩定性 |
考點 |
插入排序 |
平均O(n^2) 最優O(n) 最差O(n^2) |
O(1) |
穩定 |
選擇填空 各種時間複雜度 移動元素個數 |
二分插入排序 |
平均 O(n^2) |
O(1) |
穩定 |
同上 |
希爾排序 |
最差O(n log n) 最優 O(n) |
O(n) |
不穩定 |
時間複雜度 比較次數 |
選擇排序 |
O(n^2) |
O(1) |
不穩定 |
同插入排序 |
冒泡排序 |
O(n^2) 最優O(n) 最差O(n^2) |
O(1) |
穩定 |
時間複雜度 比較次數 單輪冒泡 |
雞尾酒排序 |
O(n^2) |
O(1) |
穩定 |
同上 |
快速排序 |
O(n log n) |
O(1) |
不穩定 |
時間複雜度 快排partition算法 |
堆排序 |
O(n log n) |
O(n) |
不穩定 |
時間複雜度 堆調整,建堆,堆排序,Top K問題 |
歸併排序 |
平均O(nlogn) 最差O(nlogn) 最優O(n) |
O(n) |
穩定 |
時間複雜度 遞歸思想 |