關於二分查詢及其上下界問題的一些思考
個人認為在程式設計的時候,我的程式碼能力應該是到位的,但是昨天參加的某公司筆試徹底把這個想法給終結了,才意識到自己是多麼的弱。其中印象最深刻的是一道關於二分查詢上下界的問題。當時洋洋得意,STL 分分鐘搞定,結果到了面試的時候他要我自己重新實現一下。這個時候就拙計了,拿著筆的我是寫了改改了寫,最後勉強算是完成。
今天反思一下,決定自己再把二分查詢重新實現一下。也作為給自己的一個警醒,不要總以為自己能力有多高,總有一天會被打臉的。
一、二分查詢思想(參照《演算法競賽入門經典》,感謝劉老師)。
在有序表中查詢元素常常使用二分查詢(Binary Search),有時也譯為折半查詢,它的基本思想就像是“猜數字遊戲”:你在心裡想一個不超過1000的正整數,我可以保證在10次以內猜到它—–只要你每次告訴我猜的數比你想的大一些、小一些,或者正好猜中。
猜的方法就是二分。首先我猜500,除了運氣特別好正好猜中外,不管你說“太大”還是“太小”,我都可以把可行範圍縮小一半:如果“太大”,那麼答案在1~499之間,那我下一次猜250;如果“太小”,那麼答案在501~1000之間,那我下一次猜750。只要每次選擇可行區間的中間點去猜,每次都可以把範圍縮小一半。由於log21000 < 10,10次內一定能猜到。
二、STL二分查詢
在STL中<algorithm>中已經有二分查詢的實現了。我在這裡只給簡單的應用,也希望讀者多去了解一下STL的強大。下面的講解參照C++ Reference,有興趣看英文原文的戳連結。
1、binary_search()函式
該函式功能是檢視某一值在一個已經排好序的序列中是否存在,當存在時返回true,否則返回false。其函式原型如下:
//default (1) template <class ForwardIterator, class T> bool binary_search (ForwardIterator first, ForwardIterator last, const T& val); //custom (2) template <class ForwardIterator, class T, class Compare> bool binary_search (ForwardIterator first, ForwardIterator last, const T& val, Compare comp);
注意序列是迭代器表示,原則上可以代入多種資料型別。其中幾個引數如下:
first:序列需要查詢的開始位置;
last:序列需要查詢的結束位置;
val:需要查詢的元素;
comp:使用者自定義比較函式。
注意到default(1)和custom(2)的區別在於使用者有沒有自定義比較函式。對於default(1)版本,使用運算子 “<” 進行元素間的比較;對於custom(2),使用使用者自定義comp進行元素間比較。
也就是說,binary_search()函式是在序列區間[first, last)中查詢是否有某一元素。其中序列一定要先排好序,排序比較函式必須和二分查詢比較函式相同。
2、lower_bound()函式
binary_search()只能告訴我們元素在序列中是否存在,卻無法定位它的確切位置。並且有時候所給的序列不一定是每個元素都不同的,同值的元素可能多次出現(因為已經排好序,所以相同的元素是相鄰的)。如果我們需要找到這些相同的元素中的第一個怎麼辦?
其實還STL中還定義了lower_bound()函式來解決這個問題,其函式原型如下:
//default (1) template <class ForwardIterator, class T> ForwardIterator lower_bound (ForwardIterator first, ForwardIterator last, const T& val); //custom (2) template <class ForwardIterator, class T, class Compare> ForwardIterator lower_bound (ForwardIterator first, ForwardIterator last, const T& val, Compare comp);
這個函式的引數和binary_search()函式是相同的,我就不再贅述,但是它的返回型別是ForwardIterator,又見迭代器,為什麼不按照我們的要求返回一個整型值表示下表呢?這個我也不解,不過沒關係,我們照樣能得到我們想要的答案。
也就是說,這個函式的功能是返回迭代器的下界。
確切的說:如果所要查詢的元素只有一個,那麼lower_lound()返回了這個元素的地址(注意這裡是地址,不是下標);
如果所要查詢的元素有多個相同,因為他們相鄰,所以可以用一個區間表示[first, last)(左閉右開)它們的位置,那麼lower_bound()函式返回的就是first的地址(再次強調是地址)。
3、upper_bound()函式
舉一反一,我們大概知道upper_bound()函式是幹嘛的了吧,那就是返回迭代器上界,也就是所查詢元素的區間的上界,但是和lower_bound()略有不同。
其函式原型如下:
//default (1) template <class ForwardIterator, class T> ForwardIterator upper_bound (ForwardIterator first, ForwardIterator last, const T& val); //custom (2) template <class ForwardIterator, class T, class Compare> ForwardIterator upper_bound (ForwardIterator first, ForwardIterator last, const T& val, Compare comp);
注意我們之前有強調過,表示相同元素的區間[first, last)是左閉右開的,為什麼要這樣子?我不知道,你去問問寫這套STL的人吧,我不敢黑他。這就造成了lower_bound()和upper_bound()的一個不同之處。那就是:lower_bound()可以相等,upper_bound()不能相等。
更加詳細:如果元素只出現一次,那麼lower_bound()找到了這個元素的地址,但是upper_bound()找到的卻是它的下一個;
如果相同元素出現了多次,那麼lower_bound()找到了第一個所找元素的地址,但是upper_bound()找到的卻是最後一個元素的下一個元素的地址。
4、簡單例子測試。
知道上面的三個函式的使用方法了,那麼我們來具體操作一下:
測試程式碼如下:
#include <iostream> #include <cstdio> #include <algorithm> using namespace std; int main() { int a[] = {0, 0, 2, 2, 2, 4, 4, 4, 4, 5}; int val; while(1) { printf(“請輸入需要查詢的數:”); scanf(“%d”, &val); if(binary_search(a, a+10, val) == false) printf(“未找到數 %d,請重新輸入!\n\n”, val); else { printf(“數 %d 已找到!\n”, val); printf(“相同個數: %d\n”, upper_bound(a, a+10, val)-lower_bound(a, a+10, val)); printf(“下界為: %d\n”, lower_bound(a, a+10, val)); printf(“上界為: %d\n\n”, upper_bound(a, a+10, val)); } } return 0; }
當然上面的是三個函式最簡單的應用了,為了讓大家看得更清楚,讀者不要噴我一個函式寫了多次。我們可以檢視所找元素是否存在,它第一次出現的位置和最後一次出現的位置,同時我們看知道了上下界就可以求出該元素出現了多少次。但是,我們看測試結果:
對,我們沒有看錯,這個上下界是地址。就拿2來看,上界減下界等於12,一個int型佔用4個位元組,那麼2的個數為3個。但是,我們要地址幹什麼?沒有用啊!
彆著急,想得到下標還不簡單。稍稍修改一下:
printf("下界為: %d\n", lower_bound(a, a+10, val)-a); //減去首地址,就找到了下標位置 printf("上界為: %d\n", upper_bound(a, a+10, val)-a); //同上 printf("該元素所在範圍: [%d, %d)\n\n", lower_bound(a, a+10, val)-a, upper_bound(a, a+10, val)-a);
程式執行結果如圖:
至此,STL二分查詢的三個函式大致介紹完成,還有另外的幾個函式讀者有興趣可以上C++ Reference去挖掘一下。
三、手動實現二分查詢三個函式。
本來今天的重點應該放在我是怎麼實現上的,不知道怎麼就跑偏了。其實講了思想,大家應該可以著手寫程式碼了。不同人有不同人實現的方法,其中的技巧還是有不少的。下面給處我個人的實現,如果有人能挑出其中的缺陷,歡迎點評。
1、binary_search()函式
STL的binary_search()返回的是bool值,不過一般演算法書或者資料結構書上都是這樣闡述的:若找到,輸出它的下標,若未找到,輸出-1。
下面呢,是我的簡單實現,功能如上所述:
#include <iostream> #include <cstdio> using namespace std; int binary_search_1(int* A, int l, int r, int val) //A為序列,l為左邊界,r為右邊界,val是元素 { //cout << l << " " << r << endl; if(l > r) return -1; //左邊界嚴格不大於右邊界,否則說明找不到 int mid = l+(r-l)/2; //從中剖分,注意這裡的一個小技巧,為何不用(l+r)/2,讀者可以去思考,下面函式的註釋會給出解答。 if(A[mid] == val) return mid; //如果找到,返回 if(A[mid] > val) r = mid-1; //否則,修改邊界 else l = mid+1; return binary_search_1(A, l, r, val); //遞迴呼叫 } int main() { int a[] = {0, 0, 2, 2, 2, 4, 4, 4, 4, 5}; cout << binary_search_1(a, 0, 10, 2); //a為陣列,0為起始位置,9為結束位置,表明你要查詢的特定區間,現在為0~9,2為元素 return 0; }
下面看非遞迴版的:
int binary_search_2(int* A, int l, int r, int val) //A為序列,l為左邊界,r為右邊界,val是元素 { int mid; while(l < r) { mid = l+(r-l)/2; if(A[mid] == val) return mid; if(A[mid] > val) r = mid-1; else l = mid+1; } return -1; }
2、lower_bound()函式和upper_bound()函式
自己實現的binary_search()在元素都互不相同的時候還挺好,但是如果存在相同元素時,那就存在不確定性,那麼,它具體返回哪一個呢,這是不確定的。那麼我們來實現一下lower_bound()函式,求一下它的下界,既然是自己實現,就可以把下標輸出來了,規避掉地址。
程式碼如下,引數與binary_search()相同:
#include <iostream> #include <cstdio> using namespace std; int lower_bound_1(int* A, int l, int r, int val) //二分求下界 { //cout << "l and r: " << l << " " << r << endl; if(l > r) return -1; if(A[l] == val) return l; int mid = l+(r-l)/2; //注意這裡是l+(r-l)/2,當l+r是奇數時,mid它是更靠近l if(A[mid] > val) r = mid-1; else if( A[mid] == val) r = mid; else l = mid+1; return lower_bound_1(A, l, r, val); //遞迴呼叫 } int upper_bound_1(int* A, int l, int r, int val) //二分求上界 { //cout << "l and r: " << l << " " << r << endl; if(l > r) return -1; if(A[r] == val) return r; int mid = l+(r-l+1)/2; //注意這裡是l+(r-l+1)/2,當l+r是奇數時,mid它是更靠近r if(A[mid] > val) r = mid-1; else if( A[mid] == val) l = mid; else l = mid+1; return upper_bound_1(A, l, r, val); //遞迴呼叫 } int main() { int a[] = {0, 0, 2, 2, 2, 4, 4, 4, 4, 5}; printf("lower_bound at %d\n", lower_bound_1(a, 0, 9, 4)); printf("upper_bound at %d\n", upper_bound_1(a, 0, 9, 4)); return 0; }
非遞迴程式碼:
int lower_bound_2(int* A, int l, int r, int val) //非遞迴版本,引數設定三個函式都相同 { int mid; while(l < r) { mid = l+(r-l)/2; if(A[mid] > val) r = mid-1; else if(A[mid] == val) r = mid; else l = mid+1; } if(A[l] == val) return l; return -1; } int upper_bound_2(int* A, int l, int r, int val) //非遞迴版本,引數設定三個函式都相同 { int mid; while(l < r) { mid = l+(r-l+1)/2; if(A[mid] > val) r = mid-1; else if(A[mid] == val) l = mid; else l = mid+1; } if(A[r] == val) return r; return -1; }
3、測試
自己已經在機器上測試了一遍,寫測試函式好累,讀者有興趣自己測試吧。偷個懶。
四、總結
二分查詢的演算法效率是非常非常高的,我相信我在這裡講的應該挺詳細了。下面有一道題就要用到二分的思想。
題目:
正整數陣列a[0], a[1], a[2], ···, a[n-1],n可以很大,大到1000000000以上,但是陣列中每個數都不超過100。現在要你求所有數的和。假設這些數已經全部讀入記憶體,因而不用考慮讀取的時間。希望你用最快的方法得到答案。
提示:二分。
相關文章
- Java中關於二分查詢的問題Java
- 關於Hibernate的查詢問題
- 關於 mysql 中的 rand () 查詢問題MySql
- 關於RxJava在業務上的一些思考RxJava
- 關於restful 查詢API設計問題RESTAPI
- 關於PWA落地問題的思考
- 二維碼問題上的一些思考
- BIEB:關於CRM系統查詢效能問題
- 二分查詢及其變種演算法演算法
- 關於資料庫查詢業務的幾點思考資料庫
- 關於分頁查詢結果的快取問題快取
- 關於下拉選單查詢資料庫的問題資料庫
- 關於查詢轉換的一些總結
- 關於Redis的一些小問題Redis
- 關於快取與資料查詢次數的問題快取
- 關於深度態勢感知問題的思考
- 查詢——二分查詢
- 二分查詢(一)——純粹的二分查詢
- 關於查詢最佳化的一些總結
- 求助:DetachedCriteria關聯查詢問題~~
- 0504關於drop表後select查詢仍有效的問題
- 關於CodeReview的一些思考View
- 關於 Masonry 的一些思考(下)
- 關於 教育孩子的 一些思考
- 關於程式碼的一些思考
- 力扣刷題-二分查詢力扣
- 二分查詢 理論 例題
- 二分查詢基礎專題——二分模板
- 關於並查集問題並查集
- 手把手教你用java實現二分查詢樹及其相關操作Java
- 關於 Elasticsearch nested field /script 的一些複雜查詢Elasticsearch
- 關於查詢轉換的一些簡單分析(一)
- 關於查詢轉換的一些簡單分析(二)
- 關於查詢轉換的一些簡單分析(三)
- 關於mysql查詢字符集不匹配問題的解決方法MySql
- 關於介面的一些問題
- 關於目標的一些思考
- 關於關聯查詢sql的一次最佳化過程及其他SQL