一、 緣起
看到CSDN社區有篇《3秒搞定一億資料的前K個最大值》, 想想堆排序不是可以用來做這事嗎,於是就動手做做,看看堆排序能夠達到怎樣的效率。堆的原理就不多說了,網上有好多, 如果想參閱比較權威性的材料,可參見《演算法導論》第六章堆排序。 本文的程式即是建立其上。
二、 程式
KthMax.c
/* * 使用堆排序求解前K個最大值問題 * */ #include "HeapUtil.c" /* 計算 list 中前 k 個最大值 */Elem* findkthMax(Elem *list, int k, long num); void testkthMax(long NUM, int K);void measure(long NUM , int K);void testValid();void testPerf();int main(){ srand(time(NULL)); //test(" Test Valid ", testValid); // test(" Measure Performace ", testPerf); measure(100000000, 100); getchar(); return 0;}void test(char *msg, void (*test)()){ printf("\n----------- %s ----------\n", msg); (*test)(); printf("\n\n");}void testValid(){ long num; int k; for (num = 0; num <= 8; num += 1) { for (k = 0; k <= num; k+=1) { testkthMax(num, k); } }}/* 測試找出前K個最大值 */void testkthMax(long NUM, int K){ Elem *kthmax; Elem* list = myalloc(NUM+1); creatList(list, NUM); printf("\nThe original list:\n"); printList(list, NUM); kthmax = findkthMax(list, K, NUM); printListInternal(kthmax, 0, K, K); free(kthmax); free(list); printf("\n"); }void testPerf(){ long num ; int k; for (num = 1; num <= 100000000; num*=10) { for (k = 1; k <= 1000 && k <= num ; k*=10) { measure(num, k); } }}void measure(long NUM , int K){ clock_t start, end; Elem *kthmax; Elem* list = myalloc(NUM+1); creatList(list, NUM); start = clock(); kthmax = findkthMax(list, K, NUM); end = clock(); free(kthmax); free(list); printf("\nNUM = %-10ld, K = %-10d, Eclipsed time: %6.3f\n", NUM, K, ((double)(end-start))/CLOCKS_PER_SEC);}/* 計算 list 中前 k 個最大值 */Elem* findkthMax(Elem *list, int k, long n){ long i; long heapsize = n; Elem *kthmax = myalloc(k); Elem *p = kthmax; buildInitMaxHeap(list, n); for (i = n; i > n-k; i--) { (p++)->num = list[1].num; swap(&list[1], &list[i]); heapsize--; maxHeapify(list, 1, heapsize); } return kthmax;}
HeapUtil.c
/* HeapUtil.c * 實現建堆過程的函數 * * NOTE: 數組 list 的 0 號單位未用,元素應放置於[1:num]而不是[0:num-1] * num 是應用中的資料數目, 因此必須給數組分配至少 num+1 個元素空間, */#include "common.c"#define LEFT(i) (2*(i))#define RIGHT(i) (2*(i)+1)/* * maxHeapify: 使以結點i為根的子樹為最大堆. * 前置條件:結點i的左右子樹都滿足最大堆性質 */void maxHeapify(Elem list[], long i, long heapsize); void buildInitMaxHeap(Elem list[], long num); /* BuildMaxHeap: 構造初始最大堆 */void swap(Elem *e1, Elem *e2);void maxHeapify(Elem list[], long i, long heapsize){ long largest = i; /* 結點i與其左右孩子節點中關鍵詞最大的那個結點的下標 */ long lch = LEFT(i); long rch = RIGHT(i); if (lch <= heapsize && list[lch].num > list[largest].num) { largest = lch; } if (rch <= heapsize && list[rch].num > list[largest].num) { largest = rch; } if (largest != i) { swap(&list[largest], &list[i]); maxHeapify(list, largest, heapsize); }}/* * buildMaxHeap: 構造初始最大堆 */void buildInitMaxHeap(Elem list[], long num){ long i; for (i = (num+1)/2; i >= 1; i--) maxHeapify(list, i, num);}void swap(Elem *e1, Elem *e2){ Elem e; e = *e1; *e1 = *e2; *e2 = e;}
common.c
/* * common.c 存放公用結構與常式 */ #include <stdio.h>#include <stdlib.h>#include <time.h>#include <limits.h> typedef struct { long num; /* 設為關鍵詞 */ } Elem;void creatListInternal(Elem *list, long offset, long num, long len);void printListInternal(Elem *list, long offset, long num, long len);void creatList(Elem *list, long num);void printList(Elem *list, long num);Elem *myalloc(long size);long randNum();/* * 隨機產生列表元素,並存入從offset開始的 num 個元素, len 是為列表長度 * 若 len < offset + num 則只存入 len-offset個元素 */void creatListInternal(Elem *list, long offset, long num, long len){ long i, endIndex = offset+num; srand(time(NULL)); if (offset < 0 || offset >= len) { return ; } if (endIndex > len) { endIndex = len ; } for (i = offset; i < endIndex; i++) (*(list+i)).num = randNum();}/* * 列印從offset開始的num個元素, len 是為列表長度 * 若 len < offset + num 則只列印 len-offset個元素 */ void printListInternal(Elem *list, long offset, long num, long len){ long i, endIndex = offset+num; if (offset < 0 || offset >= len) { return ; } if (endIndex > len) { endIndex = len ; } for (i = offset; i < endIndex; i++) printf("%3d%c", (*(list+i)).num, ((i-offset+1)%10 == 0) ? '\n': ' '); printf("\n");}void creatList(Elem *list, long num){ creatListInternal(list, 1, num, num+1);}void printList(Elem *list, long num){ printListInternal(list, 1, num, num+1);}Elem *myalloc(long size){ Elem* list = (Elem *)malloc(size*sizeof(Elem)); if (!list) { fprintf(stderr, "fail to allocate memory."); exit(1); } return list;}long randNum(){ return ((1 + rand()) % INT_MAX) * ((1 + rand()) % INT_MAX);}
HeapSort.c
/* * HeapSort.c 實現堆排序 */ #include "HeapUtil.c"#define NUM 10 void heapSort(Elem list[], long num);void testHeapSort(); int main(){ printf("*********** test heap sort ********"); testHeapSort(); getchar(); return 0;} void heapSort(Elem list[], long num){ long i, heapsize = num; buildInitMaxHeap(list, num); for (i = num; i >= 1; i--) { swap(&list[1], &list[i]); heapsize--; maxHeapify(list, 1, heapsize); }} /* 測試堆排序 */void testHeapSort(){ Elem list[NUM+1]; creatList(list, NUM); printf("\nThe original list:\n"); printList(list, NUM); printf("\n"); heapSort(list, NUM); printf("\nThe sorted list:\n"); printList(list, NUM); printf("\n"); }
三、 時間測量
四、 效率分析
使用堆的方法在n個數值中尋找前K個最大值的時間效率為 O(n + k*log2n) ,其中建立初始最大堆(即buildInitMaxHeap)的時間為O(n) ,而每次尋找均需要約 O(log2n) , 合起來即是前述結果。 因此, 總時間是隨總元素數目線性增長的, 而當K遠小於 n 時, 尋找前K個最大值所耗費的時間差別不大。
五、 程式最佳化的起點
從結果中可以看到,比別人的整整慢一倍! 還有的能達到0.1s層級,當然,這可能與機器硬體設定有關,不過,無論如何,也阻擋不住我進行程式最佳化的決心了。之前學過一些最佳化的方法手段,正好在這程式上練練手,鞏固下。 請期待下文。