﻿<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>C++博客-网络服务器软件开发-随笔分类-STL</title><link>http://www.cppblog.com/true/category/3801.html</link><description /><language>zh-cn</language><lastBuildDate>Mon, 19 May 2008 13:43:45 GMT</lastBuildDate><pubDate>Mon, 19 May 2008 13:43:45 GMT</pubDate><ttl>60</ttl><item><title>stl算法</title><link>http://www.cppblog.com/true/archive/2007/04/19/22309.html</link><dc:creator>true</dc:creator><author>true</author><pubDate>Thu, 19 Apr 2007 07:47:00 GMT</pubDate><guid>http://www.cppblog.com/true/archive/2007/04/19/22309.html</guid><wfw:comment>http://www.cppblog.com/true/comments/22309.html</wfw:comment><comments>http://www.cppblog.com/true/archive/2007/04/19/22309.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/true/comments/commentRss/22309.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/true/services/trackbacks/22309.html</trackback:ping><description><![CDATA[<div id=banner twffan="done">
<h1><a accessKey=1 href="http://stl.winterxy.com/"><font face=Verdana color=#ffffff size=5>Center of STL Study</font></a> </h1>
<span class=description twffan="done"><strong><font color=#99ccff>——最优秀的STL学习网站
<script language=javascript src="http://www.winterxy.com/cgi-bin/js/webstats.js"></script>
</font></strong></span></div>
<h2>条款45：注意count、find、binary_search、lower_bound、upper_bound和equal_range的区别</h2>
<p>你要寻找什么，而且你有一个容器或者你有一个由迭代器划分出来的区间——你要找的东西就在里面。你要怎么完成搜索呢？你箭袋中的箭有这些：count、count_if、find、find_if、binary_search、lower_bound、upper_bound和equal_range。面对着它们，你要怎么做出选择？</p>
<p>简单。你寻找的是能又快又简单的东西。越快越简单的越好。</p>
<p>暂时，我假设你有一对指定了搜索区间的迭代器。然后，我会考虑到你有的是一个容器而不是一个区间的情况。</p>
<p>要选择搜索策略，必须依赖于你的迭代器是否定义了一个有序区间。如果是，你就可以通过binary_search、lower_bound、upper_bound和equal_range来加速（通常是对数时间——参见<a href="http://stl.winterxy.com/html/item_34.html"><font color=#2244dd size=2>条款34</font></a>）搜索。如果迭代器并没有划分一个有序区间，你就只能用线性时间的算法count、count_if、find和find_if。在下文中，我会忽略掉count和find是否有_if的不同，就像我会忽略掉binary_search、lower_bound、upper_bound和equal_range是否带有判断式的不同。你是依赖默认的搜索谓词还是指定一个自己的，对选择搜索算法的考虑是一样的。</p>
<p>如果你有一个无序区间，你的选择是count或着find。它们分别可以回答略微不同的问题，所以值得仔细去区分它们。count回答的问题是：&#8220;是否存在这个值，如果有，那么存在几份拷贝？&#8221;而find回答的问题是：&#8220;是否存在，如果有，那么它在哪儿？&#8221;</p>
<p>假设你想知道的东西是，是否有一个特定的Widget值w在list中。如果用count，代码看起来像这样：</p>
<pre>list&lt;Widget&gt; lw;			// Widget的list
Widget w;				// 特定的Widget值
...
if (count(lw.begin(), lw.end(), w)) {
...			// w在lw中
} else {
...			// 不在
}
</pre>
<p>这里示范了一种惯用法：把count用来作为是否存在的检查。count返回零或者一个正数，所以我们把非零转化为true而把零转化为false。如果这样能使我们要做的更加显而易见：</p>
<pre>if (count(lw.begin(), lw.end(), w) <span class=highlight twffan="done">!= 0</span>) ...</pre>
<p>而且有些程序员这样写，但是使用隐式转换则更常见，就像最初的例子。</p>
<p>和最初的代码比较，使用find略微更难懂些，因为你必须检查find的返回值和list的end迭代器是否相等：</p>
<pre>if (<span class=highlight twffan="done">find</span>(lw.begin(), lw.end(), w) <span class=highlight twffan="done">!= lw.end()</span>) {
...				// 找到了
} else {
...				// 没找到
}
</pre>
<p>如果是为了检查是否存在，count这个惯用法编码起来比较简单。但是，当搜索成功时，它的效率比较低，因为当找到匹配的值后find就停止了，而count必须继续搜索，直到区间的结尾以寻找其他匹配的值。对大多数程序员来说，find在效率上的优势足以证明略微增加复杂度是合适的。</p>
<p>通常，只知道区间内是否有某个值是不够的。取而代之的是，你想获得区间中的第一个等于该值的对象。比如，你可能想打印出这个对象，你可能想在它前面插入什么，或者你可能想要删除它（但当迭代时删除的引导参见<a href="http://stl.winterxy.com/html/item_09.html"><font color=#2244dd size=2>条款9</font></a>）。当你需要知道的不止是某个值是否存在，而且要知道哪个对象（或哪些对象）拥有该值，你就得用find：</p>
<pre><span class=highlight twffan="done">list&lt;Widget&gt;::iterator i = find</span>(lw.begin(), lw.end(), w);
<span class=highlight twffan="done">if (i != lw.end())</span> {
...				// 找到了，i指向第一个
} else {
...				// 没有找到
}
</pre>
<p>对于有序区间，你有其他的选择，而且你应该明确的使用它们。count和find是线性时间的，但有序区间的搜索算法（binary_search、lower_bound、upper_bound和equal_range）是对数时间的。</p>
<p>从无序区间迁移到有序区间导致了另一个迁移：从使用相等来判断两个值是否相同到使用等价来判断。<a href="http://stl.winterxy.com/html/item_19.html"><font color=#2244dd size=2>条款19</font></a>由一个详细地讲述了相等和等价的区别，所以我在这里不会重复。取而代之的是，我会简单地说明count和find算法都用相等来搜索，而binary_search、lower_bound、upper_bound和equal_range则用等价。</p>
<p>要测试在有序区间中是否存在一个值，使用binary_search。不像标准C库中的（因此也是标准C++库中的）bsearch，binary_search只返回一个bool：这个值是否找到了。binary_search回答这个问题：&#8220;它在吗？&#8221;它的回答只能是是或者否。如果你需要比这样更多的信息，你需要一个不同的算法。</p>
<p>这里有一个binary_search应用于有序vector的例子（你可以从<a href="http://stl.winterxy.com/html/item_23.html"><font color=#2244dd size=2>条款23</font></a>中知道有序vector的优点）：</p>
<pre>vector&lt;Widget&gt; vw;			// 建立vector，放入
...				// 数据，
sort(vw.begin(), vw.end());		// 把数据排序
Widget w;				// 要找的值
...
if (<span class=highlight twffan="done">binary_search</span>(vw.begin(), vw.end(), w)) {
...			// w在vw中
} else {
...			// 不在
}
</pre>
<p>如果你有一个有序区间而且你的问题是：&#8220;它在吗，如果是，那么在哪儿？&#8221;你就需要equal_range，但你可能想要用lower_bound。我会很快讨论equal_range，但首先，让我们看看怎么用lower_bound来在区间中定位某个值。</p>
<p>当你用lower_bound来寻找一个值的时候，它返回一个迭代器，这个迭代器指向这个值的第一个拷贝（如果找到的话）或者到可以插入这个值的位置（如果没找到）。因此lower_bound回答这个问题：&#8220;它在吗？如果是，第一个拷贝在哪里？如果不是，它将在哪里？&#8221;和find一样，你必须测试lower_bound的结果，来看看它是否指向你要寻找的值。但又不像find，你不能只是检测lower_bound的返回值是否等于end迭代器。取而代之的是，你必须检测lower_bound所标示出的对象是不是你需要的值。</p>
<p>很多程序员这么用lower_bound：</p>
<pre><span class=highlight twffan="done">vector&lt;Widget&gt;::iterator i = lower_bound</span>(vw.begin(), vw.end(), w);
<span class=highlight twffan="done">if (i != vw.end() &amp;&amp; *i == w)</span> {	// 保证i指向一个对象；
// 也就保证了这个对象有正确的值。
// 这是个bug！
...			// 找到这个值，i指向
// 第一个等于该值的对象
} else {
...			// 没找到
}
</pre>
<p>大部分情况下这是行得通的，但不是真的完全正确。再看一遍检测需要的值是否找到的代码：</p>
<pre>if (i != vw.end() &amp;&amp; <span class=highlight twffan="done">*i == w</span>) ...</pre>
<p>这是一个<em>相等</em>的测试，但lower_bound搜索用的是<em>等价</em>。大部分情况下，等价测试和相等测试产生的结果相同，但就像<a href="http://stl.winterxy.com/html/item_19.html"><font color=#2244dd size=2>条款19</font></a>论证的，相等和等价的结果不同的情况并不难见到。在这种情况下，上面的代码就是错的。</p>
<p>要完全完成，你就必须检测lower_bound返回的迭代器指向的对象的值是否和你要寻找的值等价。你可以手动完成（<a href="http://stl.winterxy.com/html/item_19.html"><font color=#2244dd size=2>条款19</font></a>演示了你该怎么做，当它值得一做时<a href="http://stl.winterxy.com/html/item_24.html"><font color=#2244dd size=2>条款24</font></a>提供了一个例子），但可以更狡猾地完成，因为你必须确认使用了和lower_bound使用的相同的比较函数。一般而言，那可以是一个任意的函数（或函数对象）。如果你传递一个比较函数给lower_bound，你必须确认和你的手写的等价检测代码使用了相同的比较函数。这意味着如果你改变了你传递给lower_bound的比较函数，你也得对你的等价检测部分作出修改。保持比较函数同步不是火箭发射，但却是另一个要记住的东西，而且我想你已经有很多需要你记的东西了。</p>
<p>这儿有一个简单的方法：使用equal_range。equal_range返回一对迭代器，第一个等于lower_bound返回的迭代器，第二个等于upper_bound返回的（也就是，等价于要搜索值区间的末迭代器的下一个）。因此，equal_range，返回了一对划分出了和你要搜索的值等价的区间的迭代器。一个名字很好的算法，不是吗？（当然，也许叫equivalent_range会更好，但叫equal_range也非常好。）</p>
<p>对于equal_range的返回值，有两个重要的地方。第一，如果这两个迭代器相同，就意味着对象的区间是空的；这个只没有找到。这个结果是用equal_range来回答&#8220;它在吗？&#8221;这个问题的答案。你可以这么用：</p>
<pre>vector&lt;Widget&gt; vw;
...
sort(vw.begin(), vw.end());
typedef vector&lt;Widget&gt;::iterator VWIter;	// 方便的typedef
typedef pair&lt;VWIter, VWIter&gt; VWIterPair;
<span class=highlight twffan="done">VWIterPair p = equal_range</span>(vw.begin(), vw.end(), w);
<span class=highlight twffan="done">if (p.first != p.second)</span> {			// 如果equal_range不返回
// 空的区间...
...				// 说明找到了，p.first指向
// 第一个而p.second
// 指向最后一个的下一个
} else {
...				// 没找到，p.first和
// p.second都指向搜索值
}					// 的插入位置
</pre>
<p>这段代码只用等价，所以总是正确的。</p>
<p>第二个要注意的是equal_range返回的东西是两个迭代器，对它们作distance就等于区间中对象的数目，也就是，等价于要寻找的值的对象。结果，equal_range不光完成了搜索有序区间的任务，而且完成了计数。比如说，要在vw中找到等价于w的Widget，然后打印出来有多少这样的Widget存在，你可以这么做：</p>
<pre>VWIterPair p = equal_range(vw.begin(), vw.end(), w);
cout &lt;&lt; "There are " &lt;&lt; <span class=highlight twffan="done">distance(p.first, p.second)</span>
&lt;&lt; " elements in vw equivalent to w.";
</pre>
<p>到目前为止，我们所讨论的都是假设我们要在一个区间内搜索一个值，但是有时候我们更感兴趣于在区间中寻找一个位置。比如，假设我们有一个Timestamp类和一个Timestamp的vector，它按照老的timestamp放在前面的方法排序：</p>
<pre>class Timestamp { ... };
bool operator&lt;(const Timestamp&amp; lhs,		// 返回在时间上lhs
const Timestamp&amp; rhs);		// 是否在rhs前面
vector&lt;Timestamp&gt; vt;			// 建立vector，填充数据，
...					// 排序，使老的时间
sort(vt.begin(), vt.end());			// 在新的前面
</pre>
<p>现在假设我们有一个特殊的timestamp——ageLimit，而且我们从vt中删除所有比ageLimit老的timestamp。在这种情况下，我们不需要在vt中搜索和ageLimit等价的Timestamp，因为可能不存在任何等价于这个精确值的元素。 取而代之的是，我们需要在vt中找到一个位置：第一个不比ageLimit更老的元素。这是再简单不过的了，因为lower_bound会给我们答案的：</p>
<pre>Timestamp ageLimit;
...
vt.erase(vt.begin(), <span class=highlight twffan="done">lower_bound</span>(vt.begin(),	// 从vt中排除所有
vt.end(),				// 排在ageLimit的值
ageLimit));			// 前面的对象
</pre>
<p>如果我们的需求稍微改变了一点，我们要排除所有至少和ageLimit一样老的timestamp，也就是我们需要找到第一个比ageLimit年轻的timestamp的位置。这是一个为upper_bound特制的任务：</p>
<pre>vt.erase(vt.begin(), <span class=highlight twffan="done">upper_bound</span>(vt.begin(),	// 从vt中除去所有
vt.end(),				// 排在ageLimit的值前面
ageLimit));			// 或者等价的对象
</pre>
<p>如果你要把东西插入一个有序区间，而且对象的插入位置是在有序的等价关系下它应该在的地方时，upper_bound也很有用。比如，你可能有一个有序的Person对象的list，对象按照name排序：</p>
<pre>class Person {
public:
...
const string&amp; name() const;
...
};
struct PersonNameLess:
public binary_function&lt;Person, Person, bool&gt; {	// 参见<a href="http://stl.winterxy.com/html/item_40.html"><font color=#2244dd size=2>条款40</font></a>
bool operator()(const Person&amp; lhs, const Person&amp; rhs) const
{
return lhs.name() &lt; rhs.name();
}
};
list&lt;Person&gt; lp;
...
lp.sort(PersonNameLess());			// 使用PersonNameLess排序lp
</pre>
<p>要保持list仍然是我们希望的顺序（按照name，插入后等价的名字仍然按顺序排列），我们可以用upper_bound来指定插入位置：</p>
<pre>Person newPerson;
...
lp.insert(<span class=highlight twffan="done">upper_bound</span>(lp.begin(),		// 在lp中排在newPerson
lp.end(),				// 之前或者等价
newPerson,			// 的最后一个
PersonNameLess()),			// 对象后面
newPerson);			// 插入newPerson
</pre>
<p>这工作的很好而且很方便，但很重要的是不要被误导——错误地认为upper_bound的这种用法让我们魔术般地在一个list里在对数时间内找到了插入位置。我们并没有——<a href="http://stl.winterxy.com/html/item_34.html"><font color=#2244dd size=2>条款34</font></a>解释了因为我们用了list，查找花费线性时间，但是它只用了对数次的比较。</p>
<p>一直到这里，我都只考虑我们有一对定义了搜索区间的迭代器的情况。通常我们有一个容器，而不是一个区间。在这种情况下，我们必须区别序列和关联容器。对于标准的序列容器（vector、string、deque和list），你应该遵循我在本条款提出的建议，使用容器的begin和end迭代器来划分出区间。</p>
<p>这种情况对标准关联容器（set、multiset、map和multimap）来说是不同的，因为它们提供了搜索的成员函数，它们往往是比用STL算法更好的选择。<a href="http://stl.winterxy.com/html/item_44.html"><font color=#2244dd size=2>条款44</font></a>详细说明了为什么它们是更好的选择，简要地说，是因为它们更快行为更自然。幸运的是，成员函数通常和相应的算法有同样的名字，所以前面的讨论推荐你使用的算法count、find、equal_range、lower_bound或upper_bound，在搜索关联容器时你都可以简单的用同名的成员函数来代替。</p>
<p>调用binary_search的策略不同，因为这个算法没有提供对应的成员函数。要测试在set或map中是否存在某个值，使用count的惯用方法来对成员进行检测：</p>
<pre>set&lt;Widget&gt; s;		// 建立set，放入数据
...
Widget w;			// w仍然是保存要搜索的值
...
if (<span class=highlight twffan="done">s.count(w)</span>) {
...		// 存在和w等价的值
} else {
...		// 不存在这样的值
}
</pre>
<p>要测试某个值在multiset或multimap中是否存在，find往往比count好，因为一旦找到等于期望值的单个对象，find就可以停下了，而count，在最遭的情况下，必须检测容器里的每一个对象。（对于set和map，这不是问题，因为set不允许重复的值，而map不允许重复的键。）</p>
<p>但是，count给关联容器计数是可靠的。特别，它比调用equal_range然后应用distance到结果迭代器更好。首先，它更清晰：count 意味着&#8220;计数&#8221;。第二，它更简单；不用建立一对迭代器然后把它的组成<strong>（译注：就是first和second）</strong>传给distance。第三，它可能更快一点。</p>
<p>要给出所有我们在本条款中所考虑到的，我们的从哪儿着手？下面的表格道出了一切。</p>
<table cellPadding=2>
    <tbody>
        <tr>
            <th rowSpan=2>你想知道的</th>
            <th colSpan=2>使用的算法</th>
            <th colSpan=2>使用的成员函数</th>
        </tr>
        <tr>
            <td>在无序区间</td>
            <td>在有序区间</td>
            <td>在set或map上</td>
            <td>在multiset或multimap上</td>
        </tr>
        <tr>
            <td>期望值是否存在？</td>
            <td>find</td>
            <td>binary_search</td>
            <td>count</td>
            <td>find</td>
        </tr>
        <tr>
            <td>期望值是否存在？如果有，第一个等于这个值的对象在哪里？</td>
            <td>find</td>
            <td>equal_range</td>
            <td>find</td>
            <td>find或lower_bound（参见下面）</td>
        </tr>
        <tr>
            <td>第一个不在期望值之前的对象在哪里？</td>
            <td>find_if</td>
            <td>lower_bound</td>
            <td>lower_bound</td>
            <td>lower_bound</td>
        </tr>
        <tr>
            <td>第一个在期望值之后的对象在哪里？</td>
            <td>find_if</td>
            <td>upper_bound</td>
            <td>upper_bound</td>
            <td>upper_bound</td>
        </tr>
        <tr>
            <td>有多少对象等于期望值？</td>
            <td>count</td>
            <td>equal_range，然后distance</td>
            <td>count</td>
            <td>count</td>
        </tr>
        <tr>
            <td>等于期望值的所有对象在哪里？</td>
            <td>find（迭代）</td>
            <td>equal_range</td>
            <td>equal_range</td>
            <td>equal_range</td>
        </tr>
    </tbody>
</table>
<p>上表总结了要怎么操作有序区间，equal_range的出现频率可能令人吃惊。当搜索时，这个频率因为等价检测的重要性而上升了。对于lower_bound和upper_bound，它很容易在相等检测中退却，但对于equal_range，只检测等价是很自然的。在第二行有序区间，equal_range打败了find还因为一个理由：equal_range花费对数时间，而find花费线性时间。</p>
<p>对于multiset和multimap，当你在搜索第一个等于特定值的对象的那一行，这个表列出了find和lower_bound两个算法作为候选。 已对于这个任务find是通常的选择，而且你可能已经注意到在set和map那一列里，这项只有find。但是对于multi容器，如果不只有一个值存在，find并不保证能识别出容器里的等于给定值的第一个元素；它只识别这些元素中的一个。如果你真的需要找到等于给定值的第一个元素，你应该使用lower_bound，而且你必须手动的对第二部分做等价检测，<a href="http://stl.winterxy.com/html/item_19.html"><font color=#2244dd size=2>条款19</font></a>的内容可以帮你确认你已经找到了你要找的值。（你可以用equal_range来避免作手动等价检测，但是调用equal_range的花费比调用lower_bound多得多。）</p>
<p>在count、find、binary_search、lower_bound、upper_bound和equal_range中做出选择很简单。当你调用时，选择算法还是成员函数可以给你需要的行为和性能，而且是最少的工作。按照这个建议做（或参考那个表格），你就不会再有困惑。</p>
<img src ="http://www.cppblog.com/true/aggbug/22309.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/true/" target="_blank">true</a> 2007-04-19 15:47 <a href="http://www.cppblog.com/true/archive/2007/04/19/22309.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>