STL之二分查找 (Binary search in STL)

正確區分不同的查找算法count,find,binary_search,lower_bound,upper_bound,equal_range 
本文是對Effective STL45條的一個總結,闡述了各種查找算法的異同以及使用他們的時機。

首先可供查找的算法大致有count,find,binary_search,lower_bound,upper_bound,equal_range。帶有判別式的如count_if,find_if或者binary_search的派別式版本,其用法大致相同,不影響選擇,所以不作考慮。
注意這些查找算法需要序列式容器,或者數組。關聯容器有相應的同名成員函數except binary_search

首先,選擇查找算法時,區間是否排序是一個至關重要的因素。
可以按是否需要排序區間分爲兩組:
 A. count,find
 B. binary_search,lower_bound,upper_bound,equal_range
A
組不需排序區間, B組需要排序區間。
當一個區間被排序,優先選擇B組,因爲他們提供對數時間的效率。而A則是線性時間。

另外AB組所依賴的查找判斷法則不同,A使用相等性法則(查找對象需要定義operator==), B使用等價性法則(查找對象需要定義operator<,必須在相等時返回false)

A組的區別
count:
計算對象區間中的數目。
find:
返回第一個對象的位置。
查找成功的話,find會立即返回,count不會立即返回(直到查找完整個區間),此時find效率較高。
因此除非是要計算對象的數目,否則不考慮count

B組的區別 {13456}
binary_search
:判斷是否存在某個對象
lower_bound:
 返回>=對象的第一個位置,lower_bound(2)=3, lower_bound(3)=3
 
目標對象存在即爲目標對象的位置,不存在則爲後一個位置.
upper_bound:
 返回>對象的第一個位置, upper_bound(2)=3,upper_bound(3)=4
 
無論是否存在都爲後一個位置.
equal_bound:
 返回由lower_boundupper_bound返回值構成的pair,也就是所有等價元素區間。
equal_bound
有兩個需要注意的地方:
 1.
 如果返回的兩個迭代器相同,說明查找區間爲空,沒有這樣的值
 2.
 返回迭代器間的距離與迭代器中對象數目是相等的,對於排序區間,他完成了countfind的雙重任務

Section II binary search in STL    

如果在C++ STL容器中包含了有序的序列,STL提供了四個函數進行搜索,他們是利用二分查找實現的(Binary search).
其中:
假定相同值的元素可能有多個
lower_bound
 返回第一個符合條件的元素位置
upper_bound
 返回最後一個符合條件的元素位置
equal_range
 返回所有等於指定值的頭/尾元素的位置,其實就是lower_boundupper_bound
binary_search
 返回是否有需要查找的元素。

Section II Effect STL #45

條款45:注意countfindbinary_searchlower_boundupper_boundequal_range的區別

你要尋找什麼,而且你有一個容器或者你有一個由迭代器劃分出來的區間——你要找的東西就在裏面。你要怎麼完成搜索呢?你箭袋中的箭有這些:countcount_iffindfind_ifbinary_searchlower_boundupper_boundequal_range。面對着它們,你要怎麼做出選擇?

簡單。你尋找的是能又快又簡單的東西。越快越簡單的越好。

暫時,我假設你有一對指定了搜索區間的迭代器。然後,我會考慮到你有的是一個容器而不是一個區間的情況。

要選擇搜索策略,必須依賴於你的迭代器是否定義了一個有序區間。如果是,你就可以通過binary_searchlower_boundupper_boundequal_range來加速(通常是對數時間——參見條款34)搜索。如果迭代器並沒有劃分一個有序區間,你就只能用線性時間的算法countcount_iffindfind_if。在下文中,我會忽略掉countfind是否有_if的不同,就像我會忽略掉binary_searchlower_boundupper_boundequal_range是否帶有判斷式的不同。你是依賴默認的搜索謂詞還是指定一個自己的,對選擇搜索算法的考慮是一樣的。

如果你有一個無序區間,你的選擇是count或着find。它們分別可以回答略微不同的問題,所以值得仔細去區分它們。count回答的問題是:是否存在這個值,如果有,那麼存在幾份拷貝?find回答的問題是:是否存在,如果有,那麼它在哪兒?

假設你想知道的東西是,是否有一個特定的Widgetwlist中。如果用count,代碼看起來像這樣:



list<Widget> lw;   // Widgetlist
Widget w;    // 特定的Widget
...
if (count(lw.begin(), lw.end(), w)) {
 ...   // wlw
else {
 ...   // 不在
}

 

這裏示範了一種慣用法:把count用來作爲是否存在的檢查。count返回零或者一個正數,所以我們把非零轉化爲true而把零轉化爲false。如果這樣能使我們要做的更加顯而易見:
if (count(lw.begin(), lw.end(), w) != 0) ...

而且有些程序員這樣寫,但是使用隱式轉換則更常見,就像最初的例子。

和最初的代碼比較,使用find略微更難懂些,因爲你必須檢查find的返回值和listend迭代器是否相等:
if (find(lw.begin(), lw.end(), w) != lw.end()) {
 ...    //
 找到了
} else {
 ...    //
 沒找到
}

如果是爲了檢查是否存在,count這個慣用法編碼起來比較簡單。但是,當搜索成功時,它的效率比較低,因爲當找到匹配的值後find就停止了,而count必須繼續搜索,直到區間的結尾以尋找其他匹配的值。對大多數程序員來說,find在效率上的優勢足以證明略微增加複雜度是合適的。

通常,只知道區間內是否有某個值是不夠的。取而代之的是,你想獲得區間中的第一個等於該值的對象。比如,你可能想打印出這個對象,你可能想在它前面插入什麼,或者你可能想要刪除它(但當迭代時刪除的引導參見條款9)。當你需要知道的不止是某個值是否存在,而且要知道哪個對象(或哪些對象)擁有該值,你就得用find
list<Widget>::iterator i = find(lw.begin(), lw.end(), w);
if (i != lw.end()) {
 ...    //
 找到了,i指向第一個
} else {
 ...    //
 沒有找到
}

對於有序區間,你有其他的選擇,而且你應該明確的使用它們。countfind是線性時間的,但有序區間的搜索算法(binary_searchlower_boundupper_boundequal_range)是對數時間的。

從無序區間遷移到有序區間導致了另一個遷移:從使用相等來判斷兩個值是否相同到使用等價來判斷。條款19由一個詳細地講述了相等和等價的區別,所以我在這裏不會重複。取而代之的是,我會簡單地說明countfind算法都用相等來搜索,而binary_searchlower_boundupper_boundequal_range則用等價。

要測試在有序區間中是否存在一個值,使用binary_search。不像標準C庫中的(因此也是標準C++庫中的)bsearchbinary_search只返回一個bool:這個值是否找到了。binary_search回答這個問題:它在嗎?它的回答只能是是或者否。如果你需要比這樣更多的信息,你需要一個不同的算法。

這裏有一個binary_search應用於有序vector的例子(你可以從條款23中知道有序vector的優點):

span style="COLOR: #000000">vector<Widget> vw;   // 建立vector,放入
...    // 數據,
sort(vw.begin(), vw.end());  // 把數據排序
<="" span=""> align=top v:shapes="_x0000_i1035"> 
Widget w;    // 要找的值
...
if (binary_search(vw.begin(), vw.end(), w)) {
 ...   // wvw
else {
align=top v:shapes="_x0000_i1040">  ...   // 不在
}

 

如果你有一個有序區間而且你的問題是:它在嗎,如果是,那麼在哪兒?你就需要equal_range,但你可能想要用lower_bound。我會很快討論equal_range,但首先,讓我們看看怎麼用lower_bound來在區間中定位某個值。

當你用lower_bound來尋找一個值的時候,它返回一個迭代器,這個迭代器指向這個值的第一個拷貝(如果找到的話)或者到可以插入這個值的位置(如果沒找到)。因此lower_bound回答這個問題:它在嗎?如果是,第一個拷貝在哪裏?如果不是,它將在哪裏?find一樣,你必須測試lower_bound的結果,來看看它是否指向你要尋找的值。但又不像find,你不能只是檢測lower_bound的返回值是否等於end迭代器。取而代之的是,你必須檢測lower_bound所標示出的對象是不是你需要的值。

很多程序員這麼用lower_bound



vector<Widget>::iterator i = lower_bound(vw.begin(), vw.end(), w);
if (i != vw.end() && *i == w) { // 保證i指向一個對象;
    // 也就保證了這個對象有正確的值。
    // 這是個bug 
="COLOR: #008000">// 找到這個值,i指向
    // 第一個等於該值的對象
else {
 ...   // 沒找到
}

 

大部分情況下這是行得通的,但不是真的完全正確。再看一遍檢測需要的值是否找到的代碼:
if (i != vw.end() && *i == w) ...

這是一個相等的測試,但lower_bound搜索用的是等價。大部分情況下,等價測試和相等測試產生的結果相同,但就像條款19論證的,相等和等價的結果不同的情況並不難見到。在這種情況下,上面的代碼就是錯的。

要完全完成,你就必須檢測lower_bound返回的迭代器指向的對象的值是否和你要尋找的值等價。你可以手動完成(條款19演示了你該怎麼做,當它值得一做時條款24提供了一個例子),但可以更狡猾地完成,因爲你必須確認使用了和lower_bound使用的相同的比較函數。一般而言,那可以是一個任意的函數(或函數對象)。如果你傳遞一個比較函數給lower_bound,你必須確認和你的手寫的等價檢測代碼使用了相同的比較函數。這意味着如果你改變了你傳遞給lower_bound的比較函數,你也得對你的等價檢測部分作出修改。保持比較函數同步不是火箭發射,但卻是另一個要記住的東西,而且我想你已經有很多需要你記的東西了。

這兒有一個簡單的方法:使用equal_rangeequal_range返回一對迭代器,第一個等於lower_bound返回的迭代器,第二個等於upper_bound返回的(也就是,等價於要搜索值區間的末迭代器的下一個)。因此,equal_range,返回了一對劃分出了和你要搜索的值等價的區間的迭代器。一個名字很好的算法,不是嗎?(當然,也許叫equivalent_range會更好,但叫equal_range也非常好。)

對於equal_range的返回值,有兩個重要的地方。第一,如果這兩個迭代器相同,就意味着對象的區間是空的;這個只沒有找到。這個結果是用equal_range來回答它在嗎?這個問題的答案。你可以這麼用:



vector<Widget> vw;
...
sort(vw.begin(), vw.end());
typedef vector<Widget>::iterator VWIter; // 方便的typedef
typedef pair<VWIter, VWIter> VWIterPair;
VWIterPair p = equal_range(vw.begin(), vw.end(), w);
if (p.first != p.second) {   // 如果equal_range不返回
     // 空的區間...
 ...    // 說明找到了,p.first指向
     // 第一個而p.second
     // 指向最後一個的下一個
<="" span=""> onclick="this.style.display=''none''; document.getElementByIdx_x(''_339_388_Open_Text'').style.display=''none''; document.getElementByIdx_x(''_339_388_Closed_Image'').style.display=''inline''; document.getElementByIdx_x(''_339_388_Closed_Text'').style.display=''inline'';" align=top v:shapes="_339_388_Open_Image"> 
else {
<="" span=""> align=top v:shapes="_x0000_i1062">  ...    // 沒找到,p.first
     // p.second都指向搜索值
}   &

這段代碼只用等價,所以總是正確的。

第二個要注意的是equal_range返回的東西是兩個迭代器,對它們作distance就等於區間中對象的數目,也就是,等價於要尋找的值的對象。結果,equal_range不光完成了搜索有序區間的任務,而且完成了計數。比如說,要在vw中找到等價於wWidget,然後打印出來有多少這樣的Widget存在,你可以這麼做:
VWIterPair p = equal_range(vw.begin(), vw.end(), w);
cout << "There are " << distance(p.first, p.second)
  << " elements in vw equivalent to w.";

到目前爲止,我們所討論的都是假設我們要在一個區間內搜索一個值,但是有時候我們更感興趣於在區間中尋找一個位置。比如,假設我們有一個Timestamp類和一個Timestampvector,它按照老的timestamp放在前面的方法排序:
class Timestamp { ... };
bool operator<(const Timestamp& lhs,  //
 返回在時間上lhs
 const Timestamp& rhs);  //
 是否在rhs前面
vector<Timestamp> vt;   //
 建立vector,填充數據,
...     //
 排序,使老的時間
sort(vt.begin(), vt.end());   //
 在新的前面

現在假設我們有一個特殊的timestamp——ageLimit,而且我們從vt中刪除所有比ageLimit老的timestamp。在這種情況下,我們不需要在vt中搜索和ageLimit等價的Timestamp,因爲可能不存在任何等價於這個精確值的元素。 取而代之的是,我們需要在vt中找到一個位置:第一個不比ageLimit更老的元素。這是再簡單不過的了,因爲lower_bound會給我們答案的:
Timestamp ageLimit;
...
vt.erase(vt.begin(), lower_bound(vt.begin(), //
 vt中排除所有
 vt.end(),    //
 排在ageLimit的值
 ageLimit));   //
 前面的對象

如果我們的需求稍微改變了一點,我們要排除所有至少和ageLimit一樣老的timestamp,也就是我們需要找到第一個比ageLimit年輕的timestamp的位置。這是一個爲upper_bound特製的任務:
vt.erase(vt.begin(), upper_bound(vt.begin(), //
 vt中除去所有
 vt.end(),    //
 排在ageLimit的值前面
 ageLimit));   //
 或者等價的對象

如果你要把東西插入一個有序區間,而且對象的插入位置是在有序的等價關係下它應該在的地方時,upper_bound也很有用。比如,你可能有一個有序的Person對象的list,對象按照name排序:
class Person {
public:
 ...
 const string& name() const;
 ...
};

struct PersonNameLess:
public binary_function<Person, Person, bool> { //
 參見條款40
 bool operator()(const Person& lhs, const Person& rhs) const
 {
  return lhs.name() < rhs.name();
 }
};

list<Person> lp;
...
lp.sort(PersonNameLess());   //
 使用PersonNameLess排序lp

要保持list仍然是我們希望的順序(按照name,插入後等價的名字仍然按順序排列),我們可以用upper_bound來指定插入位置:
Person newPerson;
...
lp.insert(upper_bound(lp.begin(),  //
 lp中排在newPerson
 lp.end(),    //
 之前或者等價
 newPerson,   //
 的最後一個
 PersonNameLess()),   //
 對象後面
 newPerson);   //
 插入newPerson

這工作的很好而且很方便,但很重要的是不要被誤導——錯誤地認爲upper_bound的這種用法讓我們魔術般地在一個list裏在對數時間內找到了插入位置。我們並沒有——條款34解釋了因爲我們用了list,查找花費線性時間,但是它只用了對數次的比較。

一直到這裏,我都只考慮我們有一對定義了搜索區間的迭代器的情況。通常我們有一個容器,而不是一個區間。在這種情況下,我們必須區別序列和關聯容器。對於標準的序列容器(vectorstringdequelist),你應該遵循我在本條款提出的建議,使用容器的beginend迭代器來劃分出區間。

這種情況對標準關聯容器(setmultisetmapmultimap)來說是不同的,因爲它們提供了搜索的成員函數,它們往往是比用STL算法更好的選擇。條款44詳細說明了爲什麼它們是更好的選擇,簡要地說,是因爲它們更快行爲更自然。幸運的是,成員函數通常和相應的算法有同樣的名字,所以前面的討論推薦你使用的算法countfindequal_rangelower_boundupper_bound,在搜索關聯容器時你都可以簡單的用同名的成員函數來代替。

調用binary_search的策略不同,因爲這個算法沒有提供對應的成員函數。要測試在setmap中是否存在某個值,使用count的慣用方法來對成員進行檢測:
set<Widget> s;  //
 建立set,放入數據 
...
Widget w;   // w
仍然是保存要搜索的值
...
if (s.count(w)) {
 ...  //
 存在和w等價的值
} else {
 ...  //
 不存在這樣的值
}

要測試某個值在multisetmultimap中是否存在,find往往比count好,因爲一旦找到等於期望值的單個對象,find就可以停下了,而count,在最遭的情況下,必須檢測容器裏的每一個對象。(對於setmap,這不是問題,因爲set不允許重複的值,而map不允許重複的鍵。)

但是,count給關聯容器計數是可靠的。特別,它比調用equal_range然後應用distance到結果迭代器更好。首先,它更清晰:count 意味着計數。第二,它更簡單;不用建立一對迭代器然後把它的組成(譯註:就是firstsecond)傳給distance。第三,它可能更快一點。

要給出所有我們在本條款中所考慮到的,我們的從哪兒着手?下面的表格道出了一切。

你想知道的 

在無序區間

在有序區間 

在set或map上

在multiset或multimap上

期望值是否存在?

find

binary_search 

count

find

期望值是否存在?如果有,第一個等於這個值的對象在哪裏?

find 

equal_range 

find 

find或lower_bound(參見下面)

第一個不在期望值之前的對象在哪裏?

find_if

lower_bound

lower_bound

lower_bound

第一個在期望值之後的對象在哪裏?

find_if

upper_bound

upper_bound

upper_bound

有多少對象等於期望值?

count

equal_range,然後distance

count

count

等於期望值的所有對象在哪裏?

find(迭代)

equal_range

equal_range

equal_range

  
   













 上表總結了要怎麼操作有序區間,equal_range的出現頻率可能令人吃驚。當搜索時,這個頻率因爲等價檢測的重要性而上升了。對於lower_bound和upper_bound,它很容易在相等檢測中退卻,但對於equal_range,只檢測等價是很自然的。在第二行有序區間,equal_range打敗了find還因爲一個理由:equal_range花費對數時間,而find花費線性時間。

對於multisetmultimap,當你在搜索第一個等於特定值的對象的那一行,這個表列出了findlower_bound兩個算法作爲候選。 已對於這個任務find是通常的選擇,而且你可能已經注意到在setmap那一列裏,這項只有find。但是對於multi容器,如果不只有一個值存在,find並不保證能識別出容器裏的等於給定值的第一個元素;它只識別這些元素中的一個。如果你真的需要找到等於給定值的第一個元素,你應該使用lower_bound,而且你必須手動的對第二部分做等價檢測,條款19的內容可以幫你確認你已經找到了你要找的值。(你可以用equal_range來避免作手動等價檢測,但是調用equal_range的花費比調用lower_bound多得多。)

countfindbinary_searchlower_boundupper_boundequal_range中做出選擇很簡單。當你調用時,選擇算法還是成員函數可以給你需要的行爲和性能,而且是最少的工作。按照這個建議做(或參考那個表格),你就不會再有困惑。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章