条款23:考虑用排序的vector替代关联容器
标准关联容器通常实现为平衡的二叉查找树,它适合如下应用场景:频繁的插入、删除,查找等操作,这些操作基本都在同步交叉进行(以下简称场景一)。
但是很多应用程序使用数据结果的方式并没有这么混乱,它们使用的过程明显的分为3个阶段(以下简称场景二):
1)设置阶段:创建一个新的数据结构,并插入大量数据,这个阶段几乎所有的操作都是插入和删除操作,几乎没有查找操作,有也很少;
2)查找阶段:查询该数据结构特定信息,这个阶段几乎所有的操作都是查询操作,几乎没有插入和删除操作,有也很少;
3)重组阶段:改变数据结构内容,或许是删除当前数据,插入新数据,在行为上与第一个阶段类似,当这个阶段结束后,重新进入第二阶段;
针对上面描述的两种场景,结论如下:
场景一适合使用关联容器
场景二适合使用 vector 存储,排序后使用二分查找法 binary_search 查找:因为这样更节省内存(没有树节点额外的指针),且查询可能更快(跨页面更少)
200万int数据,分别采用set / vector / unordered_set进行测试,结果如下,通过数据可以看到,场景二使用vector先排序,然后二分查找的方案确实比关联容器set快,甚至比unordered_set散列容器都快,但是set和unordered_set本身在两种场景中的使用差距不大....,另外特别注意,场景一完全不适合用vector的方案,看,我写博客的时候程序还没运行完成
容器 | 场景 | 耗时(milliseconds) |
set | 场景一 | 6675 |
set | 场景二 | 6845 |
unordered_set | 场景一 | 7655 |
unordered_set | 场景二 | 7613 |
vector | 场景一 | 截止我写博客的时候还没运行完..... |
vector | 场景二 | 2867 |
我的测试代码
void test_23() {
const int N = 2000000;
// 一次性插入数据,然后查找的场景
std::default_random_engine generator;
std::uniform_int_distribution<int> distribution(0, N);
Common::TimeClock tc; // 时间打点
set<int> iset1;
for (int i = 0; i < N; ++i) {
iset1.insert(distribution(generator));
}
int findCount = 0;
for (int i = 0; i < N; ++i) {
findCount += iset1.count(i);
}
std::cout << "findCount: " << findCount << std::endl;
tc.end();
// 频繁插入、查找数据场景
tc.start();
set<int> iset2;
findCount = 0;
for (int i = 0; i < N; ++i) {
// 不断插入和查找数据
iset2.insert(distribution(generator));
findCount += iset2.count(i);
}
std::cout << "findCount: " << findCount << std::endl;
tc.end();
}
void test_23() {
const int N = 2000000;
// 一次性插入数据,然后查找的场景
std::default_random_engine generator;
std::uniform_int_distribution<int> distribution(0, N);
Common::TimeClock tc;
vector<int> ivec1;
ivec1.reserve(N);
for (int i = 0; i < N; ++i) {
ivec1.push_back(distribution(generator)); // 插入数据
}
std::sort(ivec1.begin(), ivec1.end()); // 排序
int findCount = 0;
for (int i = 0; i < N; ++i) {
findCount += std::binary_search(ivec1.cbegin(), ivec1.cend(), i); // 二分查找
}
std::cout << "findCount: " << findCount << std::endl;
tc.end();
// 频繁插入、查找、删除数据场景
tc.start();
vector<int> ivec2;
ivec2.reserve(N);
findCount = 0;
for (int i = 0; i < N; ++i) {
ivec2.push_back(distribution(generator));
findCount += std::find(ivec2.cbegin(), ivec2.cend(), i) != ivec2.end();
}
std::cout << "findCount: " << findCount << std::endl;
tc.end();
}
另外测试了map / vector / unordered_map的场景二,vector略高于map,可能是pair对的原因,效果没有set对边明显, unordered_map明显高于另外两者
容器 | 场景 | 耗时(milliseconds) |
map | 场景二 | 18459 |
unordered_map | 场景二 | 11874 |
vector | 场景二 | 15776 |
我的测试代码
using data = std::pair<string, int>;
struct DataCompare {
public:
// 两个pair的比较
bool operator()(const data &lhs, const data &rhs) const {
return lhs.first < rhs.first;
}
// operator<(pair, string)
bool operator()(const data &lhs, const data::first_type &rhs) const {
return keyLess(lhs.first, rhs);
}
// operator(string, pair)
bool operator()(const data::first_type &lhs, const data &rhs) const {
return keyLess(lhs, rhs.first);
}
private:
// operator<(string, string)
bool keyLess(const data::first_type &k1, const data::first_type &k2) const {
return k1 < k2;
}
};
void test_23_1() {
const int N = 2000000;
// map测试一次性插入数据,然后查找的场景
std::default_random_engine generator;
std::uniform_int_distribution<int> distribution(0, N);
Common::TimeClock tc; // 时间打点
map<string, int> imap1;
for (int i = 0; i < N; ++i) {
imap1.emplace(std::to_string(distribution(generator)), i);
}
int findCount = 0;
for (int i = 0; i < N; ++i) {
findCount += imap1.count(std::to_string(i));
}
std::cout << "findCount: " << findCount << std::endl;
tc.end();
// vector测试一次性插入数据,然后查找的场景
tc.start(); // 时间打点
vector<std::pair<string, int>> vec;
vec.reserve(N);
for (int i = 0; i < N; ++i) {
vec.emplace_back(std::to_string(distribution(generator)), i);
}
findCount = 0;
std::sort(vec.begin(), vec.end(), DataCompare());
for (int i = 0; i < N; ++i) {
// binary_search
if (std::binary_search(vec.cbegin(), vec.cend(), std::to_string(i), DataCompare())) {
++findCount;
}
// lower_bound
// auto lit = std::lower_bound(vec.begin(), vec.end(), std::to_string(i), DataCompare());
// if (lit != vec.end() && !DataCompare()(std::to_string(i), *lit)) {
// ++findCount;
// }
// upper_bound
// auto uit = std::upper_bound(vec.begin(), vec.end(), std::to_string(i), DataCompare());
// if (uit != vec.end() && !DataCompare()(*uit, std::to_string(i))) {
// ++findCount;
// }
// equal_range
// auto p = std::equal_range(vec.begin(), vec.end(), std::to_string(i), DataCompare());
// if (p.first != p.second) {
// ++findCount;
// }
}
std::cout << "findCount: " << findCount << std::endl;
tc.end();
// unordered_map一次性插入数据,然后查找的场景
tc.start();
std::unordered_map<string, int> uimap1;
for (int i = 0; i < N; ++i) {
uimap1.emplace(std::to_string(distribution(generator)), i);
}
findCount = 0;
for (int i = 0; i < N; ++i) {
findCount += uimap1.count(std::to_string(i));
}
std::cout << "findCount: " << findCount << std::endl;
tc.end();
}
参考:《Effective STL中文版》