C++中的std::lower_bound()和std::upper_bound()函數

前言

問題是躲不掉的,該來的總會來,這不是代碼中又遇到了 std::upper_bound() 函數,再來學習一遍好了,在我的印象中每次看到這 lower_boundupper_bound 兩個函數都有些彆扭,凡是見到他們必須查一遍,因爲我記得它們兩個函數的作用不對稱,這一點記得很清楚,但是它們兩個函數查找的細節卻記不住,這次總結一下,強化記憶,下次回憶起來應該會快一點。

函數定義

今天看到這兩個函數時撓撓頭又打開了搜索引擎,看到文章裏寫到 std::lower_bound() 是返回大於等於 value 值的位置,而 std::upper_bound() 是返回第一個大於 value 值的位置,第一反應真是瞎寫,怎麼倆都是大於,肯定應該是一個大於一個小於啊,這樣才“合理”嘛!

但是當看到多個文章中採用相同的說法時,剛剛還“堅定”的想法開始動搖,然後開始查C++標準文檔,一遍遍讀着那有些拗口的文字:

std::lower_bound returns an iterator pointing to the first element in the range [first, last) that is not less than (i.e. greater or equal to) value, or last if no such element is found.

std::upper_bound returns an iterator pointing to the first element in the range [first, last) that is greater than value, or last if no such element is found.

這些標準文檔上的文字印證了剛剛查詢到的結果,兩個函數返回的結果都是迭代器,std::lower_bound() 是在區間內找到第一個大於等於 value 的值的位置並返回,如果沒找到就返回 end() 位置。而 std::upper_bound() 是找到第一個大於 value 值的位置並返回,如果找不到同樣返回 end() 位置。

兩個函數都提到了大於操作,而沒有涉及到小於操作,這就是我前面提到的不對稱,也是我感覺不合理的地方,但是當嘗試使用了幾次這兩個函數之後,我發現這兩個函數的設計的恰到好處,這樣的設計很方便我們來做一些具體的操作。

實際例子

首先說明這兩個函數內部使用了二分查找,所以必須用在有序的區間上,滿足有序的結構中有兩個常見的面孔:std::mapstd::set,他們本身就是有序的,所以提供了 std::map::lower_bound()std::set::lower_bound() 這種類似的成員函數,但是原理都是一樣的,我們可以弄明白一個,另外類似的函數就都清楚了。

自己設計

如果你看了這兩個函數的具體含義也和我一樣不太理解爲什麼這樣設計,可以思考一下接下來這個需求,找出數組內所有值爲2和3的元素,圖例如下:

lower_bound()

對於一個有序數組,我們在實現 lower_bound() 函數和 upper_bound() 函數時可以讓它返回指定的位置來確定取值區間,第①種情況就是標準函數庫的實現方式,而第②種和第③種就是我第一印象中感覺應該對稱的樣子,這樣看起來也沒什麼問題,下面具體來分析下後兩種設計有哪些不好的地方。

具體分析

假如我們採用第②種實現方式,那麼實現打印元素2和3的代碼要寫成下面這樣:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
    std::vector<int> v{1,1,2,2,3,3,3,5,7,8};
    std::vector<int>::const_iterator itorLower = std::lower_bound(v.begin(), v.end(), 2);
    std::vector<int>::const_iterator itorUpper = std::upper_bound(v.begin(), v.end(), 3);
    while(true)
    {
        std::cout << *itorLower << std::endl;
        if (itorLower == itorUpper)
            break;
        ++itorLower;
    }
    return 0;
}

代碼看起來還可以,打印完元素後判斷到達了結尾直接跳出循環,但是如果要是數組中不包含元素2和3呢,那麼也會打印出一個元素,還有可能導致程序崩潰。

如果我們採用第③種實現方式,那麼實現打印元素2和3的代碼要寫成下面這樣:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
    std::vector<int> v{1,1,2,2,3,3,3,5,7,8};
    std::vector<int>::const_iterator itorLower = std::lower_bound(v.begin(), v.end(), 2);
    std::vector<int>::const_iterator itorUpper = std::upper_bound(v.begin(), v.end(), 3);
    for(++itorLower; itorLower != itorUpper; ++itorLower)
    {
        std::cout << *itorLower << std::endl;
    }
    return 0;
}

這代碼看起來簡潔了很多,但是在循環開始前需要先調用 ++itorLower,因爲第一個元素並不是需要找到的元素,所以要先跳過它,這樣看來確實多做了一步操作,一開始就讓 itorLow 指向第一個2就好了呀。

最終版本

當你嘗試幾種實現方式就會發現,還是標準庫提供的這種方式使用起來更加方便,雖然採取的不是對稱的方式,但是統一了存在查找元素和不存在查找元素的的情況,寫出的代碼也比較簡潔,沒有多餘的步驟,代碼如下:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
    std::vector<int> v{1,1,2,2,3,3,3,5,7,8};
    auto itorUpper = std::upper_bound(v.begin(), v.end(), 3);
    for(auto itorLower = std::lower_bound(v.begin(), v.end(), 2); itorLower != itorUpper; ++itorLower)
    {
        std::cout << *itorLower << std::endl;
    }
    return 0;
}

總結

  • 有些函數的實現方式和我們想象的並不一樣,但是我們可以通過熟練使用來了解它爲什麼這樣設計
  • 對稱結構雖然是很美的,但是非對稱的結構在編程中常常出現,同樣有其美麗所在
  • 遇到類似的問題可以動筆畫一畫,列舉出各種情況會有利於你做出正確的判斷

==>> 反爬鏈接,請勿點擊,原地爆炸,概不負責!<<==

有時會很焦慮,看到優秀的人比你還努力時總讓人感到急迫,但是一味的憂患是無意義的,腳下邁出的每一步纔是真真正正的前進,不要去憂慮可能根本就不會發生的事情,那樣你會輕鬆許多

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