很多时候,我们都能遇到在一对迭代器标识的区间中查找一些信息,stl提供了比较多的查找算法,而我们也要根据不同的情况选择不同的算法进行查找,但基于的目的总是快速、简单和高效。
这样也就有了我们在选择具体的查找策略时,指定的区间是否排序是选择算法的一个至关因素。如果区间是排序的,那么选择binary_search/lower_bound/upper_bound
和equal_range
(运行时间是对数)则能够获得更快的效率。但如果不是排序的,那就只能在count、find
及其衍生算法(线性时间消耗)中选择。
虽然count和find
都能在一段已经排序的区间中查找,但是具体的选择还是需要根据具体的实际需求来确定,并不是一股脑的选择其中一个算法。
比如:count是找到这段区间中有相同值的个数,而find是找到这端区间中第一次出现的位置,两者之间的时间消耗和遍历次数是不一样的,就像count无论如何都需要对这段区间进行全部的遍历,以便找到所有的匹配,但是find不一定,甚至可能只遍历第一个便可返回。
#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>
using namespace std;
int main()
{
typedef vector<string> VecString;
typedef vector<string>::iterator VecStrIter;
VecString v;
v.push_back("zhang san");
v.push_back("wang wu");
v.push_back("xiao ming");
v.push_back("wang wu");
v.push_back("xiao ming");
int times = count(v.begin(), v.end(), "xiao ming");
if(times)
{
cout << "v contain xiao ming count : " << times <<endl;
}
VecStrIter it = find(v.begin(), v.end(), "wang wu");
if(it != v.end())
{
cout << "v first wang wu : " << *it;
}
return 0;
}
上面的例子是对count和find的一次调用,作为count的很常见的一种普通用法,经常将其用作性能测试。count返回的是一个int类型的数,进行if判断的时候可以进行隐式的转换,但是find不能这样,我们必须得判断find的返回值是不是等于容器的end()迭代器。
同时,通过上面的例子,我们也能够得出,count和find算法是通过相等性来进行比较的。
如果我们面对的是一个排序的区间,选择相对来说比非排序的区间会很多。比如binary_search、lower_bound、upper_bound、和equal_range
,并且这些算法都是以对数时间消耗有运行的。所以相比较count和find,这些算法更高效。
同时,从区间的非排序到排序的转变中还有另外一种变化,那就是:比较的判断依据发生了改变,前者是通过相等性来进行判断,而后者是通过等价性来判断的。
template <class _ForwardIter, class _Tp, class _Compare>
bool binary_search(_ForwardIter __first, _ForwardIter __last,
const _Tp& __val,
_Compare __comp) {
__STL_REQUIRES(_ForwardIter, _ForwardIterator);
__STL_REQUIRES_SAME_TYPE(_Tp,
typename iterator_traits<_ForwardIter>::value_type);
__STL_BINARY_FUNCTION_CHECK(_Compare, bool, _Tp, _Tp);
_ForwardIter __i = lower_bound(__first, __last, __val, __comp);
return __i != __last && !__comp(__val, *__i);
}
算法binary_search
返回值是bool类型,也就是说,该算法只会返回在排序区间中是否存在一个特定的值,存在或者不存在,并不会告诉你该值在区间中的其他信息。
#include <iostream>
#include <set>
#include <algorithm>
using namespace std;
void print(const string& ptr)
{
cout << ptr << " " << endl;
}
int main()
{
typedef set<string> SetString;
SetString set;
set.insert("zhangsan");
set.insert("wangwu");
set.insert("xiaoming");
set.insert("Zhangsan");
set.insert("WangWu");
for_each(set.begin(), set.end(), print);
if(binary_search(set.begin(), set.end(), "ZhangSan"))
{
cout << "contain..." << endl;
}
return 0;
}
如果我们不仅仅是想知道该值是不是在区间中,如果在,并且想要知道它在区间中的具体位置。如果我们不能正确的选择算法,则可能会有问题。
看到这个需求的时候,我们首先可能会觉得lower_bound算法能够帮助我们解决问题,所以,我们就有了下面的操作。
#include <iostream>
#include <set>
#include <algorithm>
using namespace std;
void print(const string& ptr)
{
cout << ptr << " " << endl;
}
int main()
{
typedef set<string> SetString;
SetString set;
set.insert("zhangsan");
set.insert("wangwu");
set.insert("xiaoming");
set.insert("Zhangsan");
set.insert("WangWu");
for_each(set.begin(), set.end(), print);
SetStrIter it = lower_bound(set.begin(), set.end(), "ZhangSan");
if(it != set.end() && *it == "ZhangSan")
{
cout << "contain..." << endl;
}
return 0;
}
上面的例子在大多数情况下其实没有问题的,因为我们一般的关联容器的默认排序方式都是less<T>
。
我们能够看到lower_bound算法的判断条件语句。
if(it != set.end() && *it == "ZhangSan")
不仅仅对迭代器进行了有效性的判断,并且想要判断该迭代器所指元素的值,是一个相等性判断,但是算法lower_bound是一个通过用等价性来搜索的算法。
在下面的这种例子中就会有问题,也就是在相等性和等价性不一致的情况下。
bool ciCharLess(char c1, char c2)
{
return tolower(static_cast<unsigned char>(c1)) < tolower(static_cast<unsigned char>(c2));
}
struct StringPtrLess : public binary_function<const string, const string, bool>
{
bool operator()(const string& p1, const string& p2)
{
//return strcmp(p1.c_str(), p2.c_str());
return lexicographical_compare(p1.begin(), p1.end(), p2.begin(), p2.end(), ciCharLess);
}
};
typedef set<string, StringPtrLess> SetString;
typedef set<string, StringPtrLess>::iterator SetStrIter;
SetString set;
set.insert("ZhangSan");
set.insert("wangwu");
set.insert("xiaoming");
set.insert("Zhangsan");
set.insert("WangWu");
SetStrIter it = lower_bound(set.begin(), set.end(), "ZhangSan");
if(it != set.end() & *it == "ZhangSan")
{
cout << "contain..." << *it << endl;
}
上面的这个例子是忽略了字符串的大小写,并且自定义了比较函数子的容器。如果我们还用以前的那种方式肯定是找不到我们希望能够找到的值。
所以我们必须要判断判断lower_bound返回的迭代器所指的值是不是和我们想要查找的值等价。这也就意味着我们必须保证传给lower_bound的比较函数和我们手写等价性测试代码的比较函数保持一致。
看下下面的这个例子。
#include <iostream>
#include <set>
#include <algorithm>
#include <iterator>
using namespace std;
void print(const string& ptr)
{
cout << ptr << " " << endl;
}
bool ciCharLess(char c1, char c2)
{
return tolower(static_cast<unsigned char>(c1)) < tolower(static_cast<unsigned char>(c2));
}
struct StringPtrLess : public binary_function<const string, const string, bool>
{
bool operator()(const string& p1, const string& p2)
{
return lexicographical_compare(p1.begin(), p1.end(), p2.begin(), p2.end(), ciCharLess);
}
};
int main()
{
typedef set<string, StringPtrLess> SetString;
typedef set<string, StringPtrLess>::iterator SetStrIter;
SetString set;
set.insert("ZhangSan");
set.insert("wangwu");
set.insert("xiaoming");
set.insert("Zhangsan");
set.insert("WangWu");
for_each(set.begin(), set.end(), print);
SetStrIter it = lower_bound(set.begin(), set.end(), "zhangSan", StringPtrLess());
if(it != set.end())
{
cout << "contain..." << *it << endl;
}
return 0;
}
上面这个例子我们传给了lower_bound和容器相同的比较函数。
如果有兴趣的话,可以看一看STL中lower_bound算法的源码实现。
template <class _ForwardIter, class _Tp, class _Compare, class _Distance>
_ForwardIter __lower_bound(_ForwardIter __first, _ForwardIter __last,
const _Tp& __val, _Compare __comp, _Distance*)
{
_Distance __len = 0;
distance(__first, __last, __len);
_Distance __half;
_ForwardIter __middle;
while (__len > 0) {
__half = __len >> 1;
__middle = __first;
advance(__middle, __half);
if (__comp(*__middle, __val)) {
__first = __middle;
++__first;
__len = __len - __half - 1;
}
else
__len = __half;
}
return __first;
}
其实上面的这个需求有更简单的解决方式,那就是使用算法equal_range
,以为equal_range
算法总是使用等价性来进行比较,并且该算法返回一对迭代器,第一个迭代器时lower_bound
的返回值,第二个迭代器upper_bound
返回值。也就说,该算法返回值标识了一个区间,这个区间中的元素就是和我们需要查找的值等价的元素。
typedef set<string, StringPtrLess> SetString;
typedef set<string, StringPtrLess>::iterator SetStrIter;
SetString set;
set.insert("ZhangSan");
set.insert("wangwu");
set.insert("xiaoming");
set.insert("Zhangsan");
set.insert("WangWu");
set.insert("zhangwu");
typedef pair<SetStrIter, SetStrIter> SetIterPair;
SetIterPair p = equal_range(set.begin(), set.end(), "zhangSan", StringPtrLess());
if(p.first != p.second)
{
cout << "contain..."<< endl;
}
其实我们上面已经说过了,equal_range算法返回的一对迭代器其实是lower_bound和upper_bound返回的迭代器,所以如果我们自定义了比较函数,在调用算法的时候还是需要给定我们自定义的比较函数。
而改算法的判断条件 p.first != p.second
是只进行了等价性的判断,并不会做相等性。
对于upper_bound算法的效果刚好和lower_bound相反,我们也就不再赘述。
上面所有的例子中,我们指明的都是一对迭代器标定的一个区间,但是我么通常面对的不是区间,而是容器,容器又有序列容器和关联容器之分。相对于序列容器,一般情况下我们都是使用上面的例子一样,在其begin()和end()标定的区间之间进行查找等。而对于关联容器,一般都是有和算法相同名称的查找成员函数,而这些成员函数的效率往往比STL的算法更好一些。上面所有的查找算法,在关联容器的成员函数中几乎都有,只有binary_search是个例外,没有。
在 count、find、binary_search、lower_bound、upper_bound和equal_range
中选择合适的算法或者成员函数是很容易的。选择能符合我们行为和性能要求的算法或者成员函数,同时当调用所选择的算法或者成员函数的时候,所需要做的工作尽可能最少。