`
tibaloga
  • 浏览: 870513 次
文章分类
社区版块
存档分类
最新评论

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

 
阅读更多

Section I

正确区分不同的查找算法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的优点):

vector<Widget> vw; // 建立vector,放入

... // 数据,

sort(vw.begin(), vw.end()); // 把数据排序

Widget w; // 要找的值

...

if (binary_search(vw.begin(), vw.end(), w)) ...{

... // wvw

} else ...{

... // 不在

}

如果你有一个有序区间而且你的问题是:“它在吗,如果是,那么在哪儿?”你就需要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

... // 找到这个值,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

// 指向最后一个的下一个

} else ...{

... // 没找到,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。第三,它可能更快一点。

要给出所有我们在本条款中所考虑到的,我们的从哪儿着手?下面的表格道出了一切。

你想知道的 在无序区间 在有序区间 setmap multisetmultimap

期望值是否存在? find binary_search count find

期望值是否存在?如果有,第一个等于这个值的对象在哪里? find equal_range find findlower_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_boundupper_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中做出选择很简单。当你调用时,选择算法还是成员函数可以给你需要的行为和性能,而且是最少的工作。按照这个建议做(或参考那个表格),你就不会再有困惑。

分享到:
评论

相关推荐

    parse-stl-binary:STL 二进制解析器

    解析-stl-二进制 STL 二进制解析器 将 (STereoLithography) 二进制缓冲区解析为与兼容的网格。 如果您正在寻找流式解析器,请查看。 该模块与可以对不同面的相同顶点进行重复数据删除。 安装 $ npm install parse-...

    stl文件怎么打开

    stl文件怎么打开

    armadillo-V17w(binary).stl

    armadillo-V17w(binary).stlarmadillo-V17w(binary).stlarmadillo-V17w(binary).stlarmadillo-V17w(binary)....V17w(binary).stlarmadillo-V17w(binary).stlarmadillo-V17w(binary).stlarmadillo-V17w(binary).stl

    心希盼 C++ STL binaryTree

    心希盼 C++ STL 二叉树 详细请看“心希盼 binaryTree.doc”

    C++ STL开发技术导引(第5章)

    第二篇 C++ STL泛化技术基础 第4章 C++ STL泛型库概述 48 4.1 C++ STL的发展历程 48 4.2 C++ STL的各种实现版本 49 4.2.1 HP STL 49 4.2.2 SGI STL 50 4.2.3 STLport 50 4.2.4 P.J.Plauger STL 50 ...

    effective stl stl 技巧

    条款45:注意count、find、binary_search、lower_bound、upper_bound和equal_range的区别 条款46:考虑使用函数对象代替函数作算法的参数 条款47:避免产生只写代码 条款48:总是#include适当的头文件 条款49:...

    STL源码剖析.pdg

    3.4.2 迭代器相应型别之二difference_type 090 3.4.3 迭代器相应型别之三pointer_type 091 3.4.4 迭代器相应型别之四reference_type 091 3.4.5 迭代器相应型别之五iterator_category 092 以advanced() 为例 093 ...

    STL 风格的 N叉树

    The tree.hh library for C++ provides an STL-like ... If you are only interested in AVL binary search trees (Adelson,Velskii & Landis), you may want to have a look at the C++ AVL tree template page.

    3D 模型(.STL).rar

    3D模型,二进制STL格式,包括: 犰狳兽模型:armadillo-V17w(binary).stl 斯坦福兔子:bunny_V3.4w(binnay).stl 马模型:horse-V2w(binary).stl 方便大家做计算机图形编程实验使用。

    convertSTL:在ASCII和二进制编码之间转换STL文件的实用程序

    用于在ASCII和二进制编码之间转换STL文件的实用程序。 我写这是为了节省处理STL文件时的磁盘空间和带宽。 某些建模程序包括此功能,但是最好具有更轻量的功能,并且可以从命令行调用它。 用法 $ ruby convertSTL.rb...

    serialize-stl-binary:STL二进制序列化

    序列化-stl-二进制STL二进制序列化从网格生成 (STereoLithography) 二进制缓冲区。 如果未提供,则在内部计算面法线。安装$ npm install serialize-stl-binary用法 var serializeSTL = require ( 'serialize-stl-...

    STL 源码剖析(侯捷先生译著)

    3.4.2 迭代器相应型别之二difference_type 090 3.4.3 迭代器相应型别之三pointer_type 091 3.4.4 迭代器相应型别之四reference_type 091 3.4.5 迭代器相应型别之五iterator_category 092 以advanced() 为例 093 ...

    Effictive STL CHM中文版

    条款45: 注意count、find、binary_search、lower_bound、upper_bound和equal_range的区别 条款46: 考虑用函数对象代替函数作为算法的参数 条款47: 避免产生只写代码 条款48: 总是#include适当的头文件 条款49: ...

    stl详解 包括各种实例代码

    14. binary_search //要求有序区间 35 15. merge / inplace_merge 35 16. includes 36 17. set_union, set_intersection, set_difference, set_symmetric_diffrece 36 18. next_permutation / prev_permutation 36 ...

    C++ STL 开发技术导引(第6章)

    第二篇 C++ STL泛化技术基础 第4章 C++ STL泛型库概述 48 4.1 C++ STL的发展历程 48 4.2 C++ STL的各种实现版本 49 4.2.1 HP STL 49 4.2.2 SGI STL 50 4.2.3 STLport 50 4.2.4 P.J.Plauger STL 50 ...

    stl-ruby:读写STL文件

    STL 读取、写入和操作 ASCII 和二进制安装将此行添加到应用程序的 Gemfile 中: gem 'stl'然后执行: $ bundle或者自己安装: $ gem install stl用法 require 'stl'stl = STL . read ( 'my_awesome.stl' )stl . ...

    Effective STL(中文)

    fun、mem_fun和mem_fun_ref的原因 条款42:确定less表示operator使用STL编程 条款43:尽量用算法调用代替手写循环 条款44:尽量用成员函数代替同名的算法 条款45:注意count、find、binary_search、...

    STL文件的数据格式说明

    STL文件的数据格式说明,包括ASCII格式和Binary格式。

    STL数据文件的操作类

    压缩包中包括STL数据文件的读取类,包括ASCII格式和Binary格式。

    surf2stl:从表面数据写入 STL 文件。-matlab开发

    SURF2STL 从表面数据写入 STL 文件。 SURF2STL('filename',X,Y,Z)写入一个立体光刻(STL)文件对于具有由三个... 'binary' - 以 STL 二进制格式写入(默认) 'ascii' - 以 STL ASCII 格式写入例子: surf2stl('te

Global site tag (gtag.js) - Google Analytics