看完兩個求最大值算法之后的一些感想。
如果想直接看算法的可以跳過。但是我覺得我這些想法還是比較有用的,至少對我將來的算法設計是這樣的。
算法的功能越強大,必然意味着速度慢,因為根據叢林法則,那種慢又功能少的算法會被淘汰。
所以,(注意了!!),如果我們在使用一個算法的時候感覺到它造成的結果滿足我們的使用,而且超出了,我們的使用,那么我們就很可能浪費了時間,降低了效率。
例如這個1000個數中求最大的10個的算法:
- 如果排序,取前10個。發現后面的白排序了,根本沒用到。參照加粗行,也許可以有更快的方法。
- 於是我們想到堆。堆之所以建立比排序快,是因為它並沒有排成一隊,而是多隊,這個更散亂一些。也可以說,熵大一些,必然會好實現。但是我們發現,我們還是多要了條件,就是我們取出來了一個有序序列,其實並不用,這10個數的順序我們也不要。
- 於是就有了(4)給出的,找到第10大的元素這樣的算法。因為什么都沒有浪費,所以不會再有快的了。這是肯定的。
- 最后還要說一點,最快的往往不是通用方法,要自己實現。
題目描述:輸入n個整數,輸出其中最小的k個元素。
例如:輸入1,2,3,4,5,6,7,8這8個數字,則最小的4個數字為1,2,3,4。
思路1:最容易想到的方法:先對這個序列從小到大排序,然后輸出前面的最小的k個數即可。如果選擇快速排序法來進行排序,則時間復雜度:O(n*logn)
思路2:在思路1的基礎上更進一步想想,題目並沒有要求要查找的k個數,甚至后n-k個數是有序的,既然如此,咱們又何必對所有的n個數都進行排序列?如此,我們能想打的一個方法是:遍歷n個數,先把最先遍歷到得k個數存入大小為k的數組之中,對這k個數,利用選擇或交換排序,找到k個數中的最大數kmax(kmax設為k個元素的數組中最大元素),用時O(k)(你應該知道,插入或選擇排序查找操作需要O(k)的時間),后再繼續遍歷后n-k個數,x與kmax比較:如果x<kmax,則x代替kmax,並再次重新找出k個元素的數組中最大元素kmax‘;如果x>kmax,則不更新數組。這樣,每次更新或不更新數組的所用的時間為O(k)或O(0),整趟下來,總的時間復雜度平均下來為:n*O(k)=O(n*k)
思路3:與思路2方法類似,只是用容量為k的最大堆取代思路2中數組的作用(從數組中找最大數需要O(k)次查找,而從更新一個堆使之成為最大堆只需要O(logk)次操作)。具體做法如下:用容量為k的最大堆存儲最先遍歷到的k個數,並假設它們即是最小的k個數,建堆費時O(k)后,有k1<k2<…<kmax(kmax設為大頂堆中最大元素)。繼續遍歷數列,每次遍歷一個元素x,與堆頂元素比較,x<kmax,更新堆(用時logk),否則不更新堆。這樣下來,總費時O(k+(n-k)*logk)=O(n*logk)。
思路4:按編程之美中給出的描述,類似快速排序的划分方法,N個數存儲在數組S中,再從數組中隨機選取一個數X(隨機選取樞紐元,可做到線性期望時間O(N)的復雜度),把數組划分為Sa和Sb倆部分,Sa<=X<=Sb,如果要查找的k個元素小於Sa的元素個數,則返回Sa中較小的k個元素,否則返回Sa中所有元素+Sb中小的k-|Sa|個元素。像上述過程一樣,這個運用類似快速排序的partition的快速選擇SELECT算法尋找最小的k個元素,在最壞情況下亦能做到O(N)的復雜度。oh,太酷了,有沒有!
思路5:仍然用到數據結構:堆。具體做法為:針對整個數組序列建最小堆,建堆所用時間為O(n),然后取堆中的前k個數,總的時間復雜度即為:O(n+k*logn)。
思路6:與上述思路5類似,不同的是在對元素數組原地建最小堆O(n)后,然后提取K次,但是每次提取時,換到頂部的元素只需要下移頂多k次就足夠了,下移次數逐次減少(而上述思路5每次提取都需要logn,所以提取k次,思路7需要k*logn。而本思路只需要K^2)。此種方法的復雜度為O(n+k^2)。此方法可能不太直觀,一個更直觀的理解是:每次取出堆頂元素后,最小堆的性質被破壞了,我們需要調整最小堆使之滿足最小堆的性質。由於我們只需要求取前k個數,我們無需將整個堆都完整的調整好,只需保證堆的最上面k個數是最小的就可以,即第一趟調整保持第0層到第k層是最小堆,第二趟調整保持第0層到第k-1層是最小堆…,依次類推。
思路3的一個實現:
1 #include <stdio.h> 2 #include <stdio.h> 3 #include <stdlib.h> 4 #define PARENT(i) (i)/2 5 #define LEFT(i) 2*(i)+1 6 #define RIGHT(i) 2*(i+1) 7 8 void swap(int *a,int *b) 9 { 10 *a=*a^*b; 11 *b=*a^*b; 12 *a=*a^*b; 13 } 14 void max_heapify(int *arr,int index,int len) 15 { 16 int l=LEFT(index); 17 int r=RIGHT(index); 18 int largest; 19 if(l<len && arr[l]>arr[index]) 20 largest=l; 21 else 22 largest=index; 23 if(r<len && arr[r]>arr[largest]) 24 largest=r; 25 if(largest != index){//將最大元素提升,並遞歸 26 swap(&arr[largest],&arr[index]); 27 max_heapify(arr,largest,len); 28 } 29 } 30 31 void build_maxheap(int *arr,int len) 32 { 33 int i; 34 if(arr==NULL || len<=1) 35 return; 36 for(i=len/2+1;i>=0;--i) 37 max_heapify(arr,i,len); 38 } 39 40 void k_min(int *arr,int len,int k) 41 { 42 int i; 43 build_maxheap(arr,k); 44 for (i = k;i < len;i++) 45 { 46 if (arr[i] < arr[0]) 47 { 48 arr[0] = arr[i]; 49 max_heapify(arr,0,k); 50 } 51 } 52 } 53 /* 54 void heap_sort(int *arr,int len) 55 { 56 int i; 57 if(arr==NULL || len<=1) 58 return; 59 build_maxheap(arr,len); 60 61 for(i=len-1;i>=1;--i){ 62 swap(&arr[0],&arr[i]); 63 max_heapify(arr,0,--len); 64 } 65 } 66 */ 67 68 69 int main() 70 { 71 int arr[10]={91,8,6,82,15,18,7,46,29,12}; 72 int i; 73 k_min(arr,10,4); 74 for(i=0;i<10;++i) 75 printf("%d ",arr[i]); 76 system("pause"); 77 }
思路4的實現
首先給出《編程之美》上給出的偽代碼:
1 Kbig(S, k): 2 if(k <= 0): 3 return [] // 返回空數組 4 if(length S <= k): 5 return S 6 (Sa, Sb) = Partition(S) 7 return Kbig(Sa, k).Append(Kbig(Sb, k – length Sa) 8 9 Partition(S): 10 Sa = [] // 初始化為空數組 11 Sb = [] // 初始化為空數組 12 Swap(s[1], S[Random()%length S]) // 隨機選擇一個數作為分組標准,以 13 // 避免特殊數據下的算法退化,也可 14 // 以通過對整個數據進行洗牌預處理 15 // 實現這個目的 16 p = S[1] 17 for i in [2: length S]: 18 S[i] > p ? Sa.Append(S[i]) : Sb.Append(S[i]) 19 // 將p加入較小的組,可以避免分組失敗,也使分組 20 // 更均勻,提高效率 21 length Sa < length Sb ? Sa.Append(p) : Sb.Append(p) 22 return (Sa, Sb)
一個簡化實現的如下:
#include <stdio.h> #include <stdlib.h> void swap(int *a,int *b) { *a=*a^*b; *b=*a^*b; *a=*a^*b; } /* 為了簡單起見,這里只是單純的選取第一個元素作為樞紐元素。這樣選取樞紐,就難避免使得算法容易退化。 */ int k_big(int arr[],int low,int high,int k) { int pivot = arr[low]; int high_tmp = high; int low_tmp = low; while(low < high){ //從右向左查找,直到找到第一個小於樞紐元素為止 while (low < high && arr[high] >= pivot) { --high; } arr[low] = arr[high]; //從左向右查找,直到找到第一個大於樞紐元素為止 while (low < high && arr[low] <= pivot) { ++low; } arr[high] = arr[low]; } arr[low] = pivot; if (low == k - 1) { return arr[low]; }else if(low > k - 1) { return k_big(arr,low_tmp,low-1,k); }else { return k_big(arr,low+1,high_tmp,k); } } int main() { int arr[10]={-91,0,6,82,15,18,7,46,-29,12}; int i; k_big(arr,0,9,4); for(i=0;i<10;++i) printf("%d ",arr[i]); system("pause"); }
代碼出處:http://www.cricode.com/968.html