在C++开发中,我们经常需要处理大量数据的高效存储和检索。假设你正在开发一个社交网络应用,需要快速判断某个用户是否已经注册,或者需要维护一个按积分排序的玩家排行榜。这时候,set/multiset就会成为你的得力助手。
set和multiset是STL中的关联容器,基于红黑树(一种自平衡二叉查找树)实现。它们的主要特点是:
提示:如果你只需要判断元素是否存在而不关心顺序,unordered_set可能是更好的选择,因为它基于哈希表实现,查找速度更快(平均O(1))。
红黑树是一种近似平衡的二叉查找树,它确保了最坏情况下基本操作(插入、删除、查找)的时间复杂度都是O(log n)。这比普通二叉查找树在最坏情况下退化为链表(O(n))要好得多。
红黑树的五个核心特性:
这些特性保证了红黑树的高度始终保持在log n量级。
虽然两者都基于红黑树,但它们的节点结构有所不同:
cpp复制// set的节点结构(伪代码)
struct SetNode {
Key key;
Color color;
SetNode* left;
SetNode* right;
SetNode* parent;
};
// multiset的节点结构(伪代码)
struct MultisetNode {
Key key;
size_t count; // 记录相同键值的数量
Color color;
MultisetNode* left;
MultisetNode* right;
MultisetNode* parent;
};
这种实现差异导致了两者在插入相同元素时的不同行为:set会忽略重复元素,而multiset会增加计数器。
set/multiset的插入操作分为几个步骤:
cpp复制// 插入操作示例
set<int> s;
s.insert(5); // 第一次插入5,成功
s.insert(5); // 第二次插入5,set会忽略,size仍为1
multiset<int> ms;
ms.insert(5); // 第一次插入5,计数器=1
ms.insert(5); // 第二次插入5,计数器增加到2
时间复杂度分析:
虽然find()方法已经很快(O(log n)),但在特定场景下可以进一步优化:
cpp复制set<int> s = {1, 2, 4, 5, 7};
auto it_low = s.lower_bound(3); // 返回第一个>=3的元素,即4
auto it_up = s.upper_bound(5); // 返回第一个>5的元素,即7
cpp复制auto range = s.equal_range(4); // 返回等于4的范围
for (auto it = range.first; it != range.second; ++it) {
cout << *it << endl;
}
cpp复制multiset<int> ms = {1, 2, 2, 3};
cout << ms.count(2); // 输出2
删除操作是set/multiset中最复杂的操作之一,需要注意以下几点:
cpp复制set<int> s = {1, 2, 3, 4, 5};
auto it = s.find(3);
s.erase(it); // 正确:通过迭代器删除
// 此时it已经失效,不能再使用
s.erase(4); // 正确:通过值删除
cpp复制multiset<int> ms = {1, 2, 2, 2, 3};
ms.erase(2); // 删除所有值为2的元素
cout << ms.count(2); // 输出0
cpp复制set<int> s = {1, 2, 3, 4, 5};
for (auto it = s.begin(); it != s.end(); ) {
if (*it % 2 == 0) {
it = s.erase(it); // erase返回下一个有效迭代器
} else {
++it;
}
}
默认情况下,set/multiset使用less
cpp复制// 方法1:函数对象
struct CaseInsensitiveCompare {
bool operator()(const string& a, const string& b) const {
return strcasecmp(a.c_str(), b.c_str()) < 0;
}
};
set<string, CaseInsensitiveCompare> caseInsensitiveSet;
// 方法2:lambda表达式(C++11及以上)
auto cmp = [](const string& a, const string& b) {
return a.length() < b.length();
};
set<string, decltype(cmp)> lengthSet(cmp);
注意:比较函数必须满足严格弱序关系,否则会导致未定义行为。
| 操作 | set/multiset | vector (排序) | unordered_set |
|---|---|---|---|
| 插入 | O(log n) | O(n) | O(1)平均 |
| 删除 | O(log n) | O(n) | O(1)平均 |
| 查找 | O(log n) | O(log n) | O(1)平均 |
| 范围查询 | 优秀 | 优秀 | 差 |
| 内存使用 | 中等 | 低 | 高 |
选择建议:
cpp复制class PlayerRanking {
private:
struct Player {
int id;
int score;
bool operator<(const Player& other) const {
return score > other.score; // 降序排列
}
};
set<Player> ranking;
public:
void updateScore(int playerId, int newScore) {
// 先删除旧记录(如果存在)
ranking.erase({playerId, 0}); // 利用临时对象查找
// 插入新记录
ranking.insert({playerId, newScore});
}
void printTopN(int n) {
int count = 0;
for (const auto& player : ranking) {
if (++count > n) break;
cout << "Player " << player.id << ": " << player.score << endl;
}
}
};
cpp复制void wordFrequency(const vector<string>& words) {
map<string, int> freqMap;
for (const auto& word : words) {
++freqMap[word];
}
// 按频率降序排列
multiset<pair<int, string>, greater<>> sortedFreq;
for (const auto& [word, freq] : freqMap) {
sortedFreq.emplace(freq, word);
}
// 输出结果
for (const auto& [freq, word] : sortedFreq) {
cout << word << ": " << freq << endl;
}
}
问题场景:
cpp复制set<int> s = {1, 2, 3, 4, 5};
for (auto it = s.begin(); it != s.end(); ++it) {
if (*it % 2 == 0) {
s.erase(it); // 错误:迭代器失效后继续使用
}
}
解决方案:
cpp复制for (auto it = s.begin(); it != s.end(); ) {
if (*it % 2 == 0) {
s.erase(it++); // 先递增,再删除
} else {
++it;
}
}
cpp复制for (auto it = s.begin(); it != s.end(); ) {
if (*it % 2 == 0) {
it = s.erase(it); // erase返回下一个有效迭代器
} else {
++it;
}
}
常见错误:
cpp复制struct Point {
int x, y;
};
set<Point> points; // 编译错误:没有定义比较运算符
解决方案:
cpp复制struct Point {
int x, y;
bool operator<(const Point& other) const {
return x < other.x || (x == other.x && y < other.y);
}
};
cpp复制struct PointCompare {
bool operator()(const Point& a, const Point& b) const {
return a.x < b.x || (a.x == b.x && a.y < b.y);
}
};
set<Point, PointCompare> points;
cpp复制set<int> s;
s.reserve(1000); // 错误:set没有reserve方法
// 正确做法:无法预分配,但可以一次性插入多个元素
vector<int> data(1000);
// ...填充data...
s.insert(data.begin(), data.end()); // 比多次insert更高效
cpp复制set<pair<int, string>> s;
s.insert(make_pair(1, "hello")); // 需要构造临时对象
s.emplace(1, "hello"); // 直接在容器内构造,更高效
cpp复制set<string> s;
string largeStr = "..."s; // 很大的字符串
s.insert(largeStr); // 拷贝构造
s.insert(move(largeStr)); // 移动构造,效率更高
理解set的内部实现后,我们可以尝试实现一个简化版的MySet:
cpp复制template <typename Key, typename Compare = std::less<Key>>
class MySet {
private:
enum Color { RED, BLACK };
struct Node {
Key key;
Color color;
Node* left;
Node* right;
Node* parent;
Node(Key k, Color c, Node* p)
: key(k), color(c), left(nullptr), right(nullptr), parent(p) {}
};
Node* root;
Compare comp;
size_t count;
// 旋转和平衡辅助函数
void leftRotate(Node* x) { /*...*/ }
void rightRotate(Node* y) { /*...*/ }
void insertFixup(Node* z) { /*...*/ }
public:
MySet() : root(nullptr), count(0) {}
bool insert(const Key& key) {
Node* parent = nullptr;
Node** current = &root;
while (*current) {
parent = *current;
if (comp(key, parent->key)) {
current = &parent->left;
} else if (comp(parent->key, key)) {
current = &parent->right;
} else {
return false; // 键已存在
}
}
*current = new Node(key, RED, parent);
insertFixup(*current);
++count;
return true;
}
// 其他成员函数...
};
这个简化实现包含了set的核心功能,但省略了删除操作、迭代器等复杂部分。通过这个练习,可以更深入理解STL set的工作原理。