二分查找的基本寫法:java
#include <vector> #include <iostream> int binarySearch(std::vector<int> coll, int key) { int l = 0; int r = (int)coll.size() - 1; while (l <= r) { int m = l + (r - l) / 2; if (key == coll[m]) { return m; } if (key > coll[m]) { l = m + 1; } else { r = m - 1; } } return -1; }
int main() { int arr[] = { 2, 3, 4, 10, 40 }; std::vector<int> coll(arr, arr + sizeof(arr) / sizeof(arr[0])); int key = 10; int index = binarySearch(coll, key); if (-1 == index) { std::cout << "Element is not present in array" << std::endl; } else { std::cout << "Element is present at index " << index << std::endl; } return 0; }
歷史上,Knuth在其<<Sorting and Searching>>一書的第6.2.1節指出:儘管第一個二分搜索算法於1946年就出現,然而第一個徹底正確的二分搜索算法直到1962年纔出現。ios
而不經仔細斟酌而寫出的一個二分查找常常遭遇off by one或者無限循環的錯誤。下面將討論二分查找的理論基礎,實現應用,及如何採用何種技術保證寫出一個正確的二分程序,讓咱們免於思考麻煩的邊界及結束判斷問題。算法
在C++的STL中有以下函數 lower_bound、upper_bound、binary_search、equal_range,這些函數就是咱們要考慮如何實現的那些。經過實現這些函數,能夠檢查你是否真的掌握了二分查找。數組
理論基礎:
當咱們碰到一個問題,須要判斷它是否能夠採用二分查找來解決。對於最通常的數的查找問題,這點很容易判斷,然而對於某些好比能夠採用二分+貪心組合,二分解方程,即某些具備單調性的函數問題,也是能夠利用二分解決的,然而有時它們表現的不那麼顯然。網絡
考慮一個定義在有序集合S上的斷言,搜索空間內包含了問題的候選解。在本文中,一個斷言其實是一個返回布爾值的二值函數。這個斷言能夠用來驗證一個候選解是不是所定義的問題合法的候選解。函數
咱們把下面的一條定理稱之爲Main Theorem: Binary search can be used if and only if for all x in S, p(x) implies p(y) for all y > x. 實際上經過這個屬性,咱們能夠將搜索空間減半,也就是說若是咱們的問題的解應用這樣的一個驗證函數,驗證函數的值能夠知足上述條件,這樣這個問題就能夠用二分查找的方法來找到那個合適的解,好比最左側的那個合法解。以上定理還有一個等價的說法 !p(x) implies !p(y) for all y < x 。這個定理很容易證實,這裏省略證實。測試
實際上若是把這樣的一個p函數應用於整個序列,咱們能夠獲得以下的一個序列
fasle false false ......true true....
若是用01表示,實際上就是以下這樣的一個序列0 0 0 0......1 1 1 1.......
而全部的二分查找問題實際均可以轉化爲這樣的一個01序列中第一個1的查找問題,實際上咱們就爲二分查找找到了一個統一的模型。就像排序網絡中利用的01定理,若是能夠對全部的01序列排序,則能夠爲全部的序列排序。實際上二分查找也能夠用來解決true true....fasle false false ......即1 1 1 1...... 0 0 0 0.....序列的查找問題。固然實際若是咱們把p的定義變反,這個序列就變成了上面的那個,也就是能夠轉化爲上面的模型。spa
這樣咱們就把全部問題轉化爲求0011模式序列中第一個1出現的位置。固然實際中的問題,也多是求1100模式序列最後一個1的位置。同時要注意對應這兩種狀況下的實現有些許不一樣,而這個不一樣對於程序的正確性是很關鍵的。翻譯
下面的例子對這兩種狀況都有涉及,通常來講具備最大化要求的某些問題,它們的斷言函數每每具備1100模式,好比poj3258 River Hopscotch;而具備最小化要求的某些問題,它們的斷言函數每每具備0011模式,好比poj3273 Monthly Expense。指針
而對於數key的查找,咱們能夠利用以下一個斷言使它成爲上述模式。好比x是否大於等於key,這樣對於一個上升序列來講它的斷言函數值成爲以下模式:0 0 0 0......1 1 1 1.......,而尋找最左邊的key(相似stl中的lower_bound,則就是上述模型中尋找最左邊的1.固然問題是尋找最後出現的那個key(相似stl中的upper_bound),只須要把斷言修改爲:x是否小於等於key,就變成了1 1 1 1...... 0 0 0 0.....序列的查找問題。
可見這樣的查找問題,變成了如何尋找上述序列中的最左或最右的1的問題。
相似的一個單調函數的解的問題,只要設立一個斷言:函數值是否大於等於0?也變成了如上的序列,若是是單調上升的,則變成了0011模式,反之則是1100模式。實際上當函數的自變量取值爲實數時,這樣的一個序列實際上變成了一種無窮數列的形式,也就是1111.....0000中間是無窮的,01的邊界是無限小的。這樣尋找最右側的1,通常是尋找一個近似者,也就是採用對實數域上的二分(下面的源代碼4),而用fabs(begin-end)來控制精度,肯定是否中止迭代。好比poj 3122就是在具備1111.....0000模式的無窮序列中查找那個最右側的1,對應的自變量值。
基本例題
poj 3233 3497 2104 2413 3273 3258 1905 3122
注:
poj1905 實際上解一個超越方程 L"sinx -Lx=0,能夠利用源碼4,二分解方程
poj3258 尋找最大的可行距離,其實是111000序列中尋找最右側的1,能夠參考源碼3
poj3273 尋找最小的可行值,其實是000111序列中尋找最左側的1,能夠參考源碼2
總結
一、首先尋找進行二分查找的依據,即符合main 理論的一個斷言:0 0 0 ........111.......
二、肯定二分的上下界,儘可能的讓上下界鬆弛,防止漏掉合理的範圍,肯定上界,也能夠倍增法
三、觀察肯定該問題屬於0011仍是1100模式的查找
四、寫程序注意兩個不變性的保持
五、注意驗證程序能夠處理01這種兩個序列的用例,不會出錯
六、注意mid = begin+(end-begin)/2,用mid=(begin+end)/2是有溢出危險的。實際上早期的java的jdk裏的二分搜索就有這樣的bug,後來java大師Joshua Bloch發現,才改正的。
對二分查找進行分類:取整方式:向下取整 向上取整 (共2種)區間開閉:閉區間 左閉右開區間 左開右閉區間 開區間 (共4種)問題類型:對於不降低序列a,求最小的i,使得a[i] = key對於不降低序列a,求最大的i,使得a[i] = key對於不降低序列a,求最小的i,使得a[i] > key對於不降低序列a,求最大的i,使得a[i] < key對於不上升序列a,求最小的i,使得a[i] = key對於不上升序列a,求最大的i,使得a[i] = key對於不上升序列a,求最小的i,使得a[i] < key對於不上升序列a,求最大的i,使得a[i] > key(共8種)綜上所述,二分查找共有2*4*8=64種寫法。
重要的是要會寫一種對的。
首先有幾個數字要注意
一、中位數有兩個:
下位中位數:lowerMedian = (length - 2) / 2;
上位中位數:upperMedian = length / 2;
經常使用的是下位中位數,通用的寫法以下,語言int常常自動向下取整,
median = (length - 1) / 2;
指針的區間固然能夠開區間,也能夠閉區間,也能夠半開半閉。但老老實實兩頭取閉區間老是不會錯。上面的中位數,轉換成兩頭閉區間 [low,high] 就變成下面這樣:
median = low + (high - low) / 2;
二、不要圖快用加法,會溢出,
median = ( low + high ) / 2; // OVERFLOW
三、另一個關鍵點是「終結條件」
不要以 low == high 作終結條件,會被跳過的。
if (low == high) { return (nums[low] >= target)? low : ++low; }
不相信在 [1, 5] 裏找 0 試試?
正確的終結條件是:
low > high
也就是搜索空間爲空。
知足終結條件之後,返回值徹底不須要糾結,直接返回低位 low。
由於回過頭去放慢鏡頭,二分查找的過程就是一個 維護 low 的過程:
low從0起始。只在中位數遇到肯定小於目標數時才前進,而且永不後退。low一直在朝着第一個目標數的位置在逼近。知道最終到達。
至於高位 high,就放心大膽地縮小目標數組的空間吧。
因此最後的代碼很是簡單,
#include <vector> #include <iostream> int binarySearch(std::vector<int> coll, int key) { int low = 0; int high = (int)coll.size() - 1; while (low <= high) { int mid = low + (high - low) / 2; if (key > coll[mid]) { low = mid + 1; } else if (key < coll[mid]) { high = mid - 1; } else { return mid; } } return low; } int main() { int arr[] = { 2, 3, 4, 10, 40 }; std::vector<int> coll(arr, arr + sizeof(arr) / sizeof(arr[0])); int key = 10; int index = binarySearch(coll, key); if (-1 == index) { std::cout << "Element is not present in array" << std::endl; } else { std::cout << "Element is present at index " << index << std::endl; } return 0; }
遞歸版也同樣簡單,
#include <vector> #include <iostream> int binarySearchRecur(std::vector<int> coll, int key, int low, int high) { if (low > high) { return low; } int mid = low + (high - low) / 2; if (key > coll[mid]) { return binarySearchRecur(coll, key, mid + 1, high); } else if (key < coll[mid]) { return binarySearchRecur(coll, key, low, mid - 1); } else { return mid; } } int main() { int arr[] = { 2, 3, 4, 10, 40 }; std::vector<int> coll(arr, arr + sizeof(arr) / sizeof(arr[0])); int size = (int)coll.size(); int key = 10; int index = binarySearchRecur(coll, key, 0, size); if (-1 == index) { std::cout << "Element is not present in array" << std::endl; } else { std::cout << "Element is present at index " << index << std::endl; } return 0; }
但上面的代碼能正常工做,有一個前提條件:元素空間沒有重複值。
推廣到有重複元素的空間,二分查找問題就變成:
尋找元素第一次出現的位置。
也能夠變相理解成另外一個問題,對應C++的 lower_bound() 函數,尋找第一個大於等於目標值的元素位置。
但只要掌握了上面說的二分查找的心法,代碼反而更簡單:
#include <vector> #include <iostream> int binarySearch(std::vector<int> coll, int key) { int low = 0; int high = (int)coll.size() - 1; while (low <= high) { int mid = low + (high - low) / 2; if (key > coll[mid]) { low = mid + 1; } else { high = mid - 1; } } return low; } int main() { int arr[] = { 2, 3, 4, 10, 40 }; std::vector<int> coll(arr, arr + sizeof(arr) / sizeof(arr[0])); int key = 10; int index = binarySearch(coll, key); if (-1 == index) { std::cout << "Element is not present in array" << std::endl; } else { std::cout << "Element is present at index " << index << std::endl; } return 0; }
翻譯成遞歸版也是同樣:
#include <vector> #include <iostream> int binarySearchRecur(std::vector<int> coll, int key, int low, int high) { if (low > high) { return low; } int mid = low + (high - low) / 2; if (key > coll[mid]) { return binarySearchRecur(coll, key, mid + 1, high); } else { return binarySearchRecur(coll, key, low, mid - 1); } } int main() { int arr[] = { 2, 3, 4, 10, 40 }; std::vector<int> coll(arr, arr + sizeof(arr) / sizeof(arr[0])); int size = (int)coll.size(); int key = 10; int index = binarySearchRecur(coll, key, 0, size); if (-1 == index) { std::cout << "Element is not present in array" << std::endl; } else { std::cout << "Element is present at index " << index << std::endl; } return 0; }
以上代碼均經過leetcode測試。標準銀彈。天天早起寫一遍,鍛鍊肌肉。
最後想說,不要怕二分查找難寫,邊界狀況複雜。實際狀況是,你以爲煩躁,大牛也曾經由於這些煩躁過。一些臭名昭著的問題下面,常常是各類大牛的評論(噁心,變態,F***,等等)。並且這並不考驗什麼邏輯能力,只是仔細的推演罷了。拿個筆出來寫一寫,算一算不丟人。不少問題完全搞清楚之後,常常就是豁然開朗,而後之後妥妥觸類旁通。以上。
參考:
https://www.zhihu.com/question/36132386
https://en.wikipedia.org/wiki/Binary_search_algorithm
https://www.geeksforgeeks.org/binary-search/
https://www.topcoder.com/community/data-science/data-science-tutorials/binary-search/