6. 折半查找 請點評如果不是從一組隨機的序列里查找,而是從一組排好序的序列里找出某個元素的位置,則可以有更快的算法: 例 11.4. 折半查找 #include <stdio.h> #define LEN 8 int a[LEN] = { 1, 2, 2, 2, 5, 6, 8, 9 }; int binarysearch(int number) { int mid, start = 0, end = LEN - 1; while (start <= end) { mid = (start + end) / 2; if (a[mid] < number) start = mid + 1; else if (a[mid] > number) end = mid - 1; else return mid; } return -1; } int main(void) { printf("%d\n", binarysearch(5)); return 0; } 由于這個序列已經從小到大排好序了,每次取中間的元素和待查找的元素比較,如果中間的元素比待查找的元素小,就說明“如果待查找的元素存在,一定位于序列的后半部分”,這樣可以把搜索范圍縮小到后半部分,然后再次使用這種算法迭代。這種“每次將搜索范圍縮小一半”的思想稱為折半查找(Binary Search)。思考一下,這個算法的時間復雜度是多少? 這個算法的思想很簡單,不是嗎?可是[編程珠璣]上說作者在課堂上講完這個算法的思想然后讓學生寫程序,有90%的人寫出的程序中有各種各樣的Bug,讀者不信的話可以不看書自己寫一遍試試。這個算法容易出錯的地方很多,比如 怎樣才能保證程序的正確性呢?在第 2 節 “插入排序”我們講過借助Loop Invariant證明循環的正確性,
注意這個算法有一個非常重要的前提-- a 是排好序的。缺了這個前提,“如果a[mid] < number ,那么a[start..mid]應該都比number 小”這一步推理就不能成立,這個函數就不能正確地完成查找。從更普遍的意義上說,函數的調用者(Caller)和函數的實現者(Callee,被調用者)之間訂立了一個契約(Contract),在調用函數之前,Caller要為Callee提供某些條件,比如確保a 是排好序的,確保a[start..end]都是有效的數組元素而沒有訪問越界,這稱為Precondition,然后Callee對一些Invariant進行維護(Maintenance),這些Invariant保證了Callee在函數返回時能夠對Caller盡到某些義務,比如確保“如果number 在數組a 中存在,一定能找出來并返回它的位置,如果number 在數組a 中不存在,一定能返回-1”,這稱為Postcondition。如果每個函數的文檔都非常清楚地記錄了Precondition、Maintenance和Postcondition是什么,那么每個函數都可以獨立編寫和測試,整個系統就會易于維護。這種編程思想是由Eiffel語言的設計者Bertrand Meyer提出來的,稱為Design by Contract(DbC)。測試一個函數是否正確需要把Precondition、Maintenance和Postcondition這三方面都測試到,比如 例 11.5. 帶有測試代碼的折半查找 #include <stdio.h> #include <assert.h> #define LEN 8 int a[LEN] = { 1, 2, 2, 2, 5, 6, 8, 9 }; int is_sorted(void) { int i; for (i = 1; i < LEN; i++) if (a[i-1] > a[i]) return 0; return 1; } int mustbe(int start, int end, int number) { int i; for (i = 0; i < start; i++) if (a[i] == number) return 0; for (i = end+1; i < LEN; i++) if (a[i] == number) return 0; return 1; } int contains(int n) { int i; for (i = 0; i < LEN; i++) if (a[i] == n) return 1; return 0; } int binarysearch(int number) { int mid, start = 0, end = LEN - 1; assert(is_sorted()); /* Precondition */ while (start <= end) { assert(mustbe(start, end, number)); /* Maintenance */ mid = (start + end) / 2; if (a[mid] < number) start = mid + 1; else if (a[mid] > number) end = mid - 1; else { assert(mid >= start && mid <= end && a[mid] == number); /* Postcondition 1 */ return mid; } } assert(!contains(number)); /* Postcondition 2 */ return -1; } int main(void) { printf("%d\n", binarysearch(5)); return 0; }
main: main.c:33: binarysearch: Assertion `is_sorted()' failed. Aborted 在代碼中適當的地方使用斷言(Assertion)可以有效地幫助我們測試程序。也許有人會問:我們用幾個測試函數來測試 測試代碼只在開發和調試時有用,如果正式發布(Release)的軟件也要運行這些測試代碼就會嚴重影響性能了,如果在包含 #define NDEBUG #include <stdio.h> #include <assert.h> ... 注意 還有另一種辦法,不必修改源文件,在編譯命令行加上選項 習題 請點評1、本節的折半查找算法有一個特點:如果待查找的元素在數組中有多個則返回其中任意一個,以本節定義的數組 2、編寫一個函數 3、編寫一個函數 double product = 1; for (i = 0; i < n; i++) product *= x; 這個算法的時間復雜度是Θ(n)。其實有更好的辦法,比如 從以上幾題可以看出,折半查找的思想有非常廣泛的應用,不僅限于從一組排好序的元素中找出某個元素的位置,還可以解決很多類似的問題。[編程珠璣]對于折半查找的各種應用和優化技巧有非常詳細的介紹。 |
|
來自: fruitapple > 《C語言》