数据结构手册007:集合结构 - set与unordered_set的专精解析
集合的数学本质:从康托尔到计算机
集合是数学中最基础的概念之一,描述了确定的、互不相同的对象的整体。在计算机科学中,集合结构继承了这一数学思想,成为处理唯一性元素的强大工具。
cpp
// 数学集合与计算机集合的对应关系
// 数学: A = {1, 2, 3, 4, 5}
// C++: std::set<int> A = {1, 2, 3, 4, 5};
// 数学: B = {x | x 是偶数}
// C++: 通过条件判断动态构建集合
集合的基本特性:理解唯一性与无序性
集合的核心特征
集合结构具有三个基本特性:
- 唯一性:集合中不允许重复元素
- 确定性:元素要么属于集合,要么不属于集合
- 无序性:集合中的元素没有特定顺序(数学定义)
cpp
#include <set>
#include <unordered_set>
#include <iostream>
void setBasicProperties() {
// 唯一性演示
std::set<int> uniqueNumbers;
uniqueNumbers.insert(1);
uniqueNumbers.insert(2);
uniqueNumbers.insert(2); // 重复元素,不会被插入
uniqueNumbers.insert(3);
std::cout << "集合大小: " << uniqueNumbers.size() << std::endl; // 3
std::cout << "集合内容: ";
for (int num : uniqueNumbers) {
std::cout << num << " "; // 1 2 3
}
std::cout << std::endl;
// 确定性演示
if (uniqueNumbers.find(2) != uniqueNumbers.end()) {
std::cout << "2 属于集合" << std::endl;
}
if (uniqueNumbers.find(4) == uniqueNumbers.end()) {
std::cout << "4 不属于集合" << std::endl;
}
}
std::set:基于红黑树的有序集合
set的底层实现与特性
set的底层使用红黑树实现,保证元素的有序性和操作的对数时间复杂度。
cpp
void setOrderedCharacteristics() {
std::set<std::string> sortedSet;
// 插入元素(自动排序)
sortedSet.insert("orange");
sortedSet.insert("apple");
sortedSet.insert("banana");
sortedSet.insert("grape");
std::cout << "有序集合内容:" << std::endl;
for (const auto& fruit : sortedSet) {
std::cout << fruit << " "; // apple banana grape orange
}
std::cout << std::endl;
// 查找操作 O(log n)
auto it = sortedSet.find("banana");
if (it != sortedSet.end()) {
std::cout << "找到: " << *it << std::endl;
}
// 范围查找
auto lower = sortedSet.lower_bound("b");
auto upper = sortedSet.upper_bound("g");
std::cout << "b到g之间的元素: ";
for (auto iter = lower; iter != upper; ++iter) {
std::cout << *iter << " "; // banana grape
}
std::cout << std::endl;
}
set的迭代器稳定性
cpp
void setIteratorStability() {
std::set<int> stableSet = {1, 2, 3, 4, 5};
auto it = stableSet.find(3);
std::cout << "初始迭代器指向: " << *it << std::endl;
// 插入和删除其他元素不会使迭代器失效
stableSet.insert(6);
stableSet.erase(1);
// 迭代器仍然有效
std::cout << "操作后迭代器指向: " << *it << std::endl; // 3
// 但删除当前元素会使迭代器失效
it = stableSet.find(2);
stableSet.erase(2);
// 此时 it 失效,不能再使用
}
std::unordered_set:基于哈希表的无序集合
unordered_set的哈希实现
unordered_set使用哈希表实现,提供平均O(1)时间复杂度的查找操作。
cpp
void unorderedSetHashCharacteristics() {
std::unordered_set<std::string> hashSet;
// 插入元素(无序存储)
hashSet.insert("dog");
hashSet.insert("cat");
hashSet.insert("elephant");
hashSet.insert("bird");
std::cout << "哈希集合内容:" << std::endl;
for (const auto& animal : hashSet) {
std::cout << animal << " "; // 顺序不确定
}
std::cout << std::endl;
// 快速查找 O(1) 平均情况
if (hashSet.contains("cat")) { // C++20
std::cout << "集合包含猫" << std::endl;
}
// 哈希表统计信息
std::cout << "桶数量: " << hashSet.bucket_count() << std::endl;
std::cout << "负载因子: " << hashSet.load_factor() << std::endl;
}
自定义类型的哈希集合
cpp
class Product {
public:
std::string name;
int id;
double price;
Product(const std::string& n, int i, double p)
: name(n), id(i), price(p) {}
// 相等比较运算符
bool operator==(const Product& other) const {
return id == other.id; // 根据ID判断相等
}
};
// 自定义哈希函数
struct ProductHash {
std::size_t operator()(const Product& p) const {
return std::hash<int>{}(p.id); // 基于ID计算哈希
}
};
void customTypeSet() {
std::unordered_set<Product, ProductHash> productSet;
Product p1("Laptop", 1001, 999.99);
Product p2("Mouse", 1002, 29.99);
Product p3("Keyboard", 1003, 79.99);
productSet.insert(p1);
productSet.insert(p2);
productSet.insert(p3);
// 查找测试
Product searchProduct("", 1002, 0.0);
if (productSet.find(searchProduct) != productSet.end()) {
std::cout << "找到ID为1002的产品" << std::endl;
}
}
集合运算:数学操作的代码实现
基本集合运算
cpp
template<typename T>
std::set<T> setUnion(const std::set<T>& A, const std::set<T>& B) {
std::set<T> result = A;
result.insert(B.begin(), B.end()); // 并集
return result;
}
template<typename T>
std::set<T> setIntersection(const std::set<T>& A, const std::set<T>& B) {
std::set<T> result;
std::set_intersection(A.begin(), A.end(),
B.begin(), B.end(),
std::inserter(result, result.begin())); // 交集
return result;
}
template<typename T>
std::set<T> setDifference(const std::set<T>& A, const std::set<T>& B) {
std::set<T> result;
std::set_difference(A.begin(), A.end(),
B.begin(), B.end(),
std::inserter(result, result.begin())); // 差集
return result;
}
template<typename T>
bool isSubset(const std::set<T>& A, const std::set<T>& B) {
// A 是 B 的子集
return std::includes(B.begin(), B.end(), A.begin(), A.end());
}
void setOperationsDemo() {
std::set<int> A = {1, 2, 3, 4, 5};
std::set<int> B = {3, 4, 5, 6, 7};
auto unionSet = setUnion(A, B);
auto intersectionSet = setIntersection(A, B);
auto differenceSet = setDifference(A, B);
std::cout << "A: "; for (int x : A) std::cout << x << " "; std::cout << std::endl;
std::cout << "B: "; for (int x : B) std::cout << x << " "; std::cout << std::endl;
std::cout << "A ∪ B: "; for (int x : unionSet) std::cout << x << " "; std::cout << std::endl;
std::cout << "A ∩ B: "; for (int x : intersectionSet) std::cout << x << " "; std::cout << std::endl;
std::cout << "A - B: "; for (int x : differenceSet) std::cout << x << " "; std::cout << std::endl;
std::cout << "A ⊆ B: " << isSubset(A, B) << std::endl;
}
高效集合运算算法
cpp
class EfficientSetOperations {
public:
// 使用归并思想求交集
template<typename T>
static std::vector<T> sortedIntersection(const std::vector<T>& sortedA,
const std::vector<T>& sortedB) {
std::vector<T> result;
size_t i = 0, j = 0;
while (i < sortedA.size() && j < sortedB.size()) {
if (sortedA[i] < sortedB[j]) {
++i;
} else if (sortedA[i] > sortedB[j]) {
++j;
} else {
result.push_back(sortedA[i]);
++i;
++j;
}
}
return result;
}
// 布隆过滤器模拟(用于大数据量去重)
class BloomFilter {
private:
std::vector<bool> bits;
size_t size;
public:
BloomFilter(size_t n) : size(n), bits(n, false) {}
void add(const std::string& element) {
size_t hash1 = std::hash<std::string>{}(element) % size;
size_t hash2 = std::hash<std::string>{}(element + "salt") % size;
bits[hash1] = true;
bits[hash2] = true;
}
bool mightContain(const std::string& element) const {
size_t hash1 = std::hash<std::string>{}(element) % size;
size_t hash2 = std::hash<std::string>{}(element + "salt") % size;
return bits[hash1] && bits[hash2]; // 可能有假阳性
}
};
};
void efficientOperationsDemo() {
// 有序数组求交集
std::vector<int> sortedA = {1, 2, 3, 4, 5};
std::vector<int> sortedB = {3, 4, 5, 6, 7};
auto intersection = EfficientSetOperations::sortedIntersection(sortedA, sortedB);
std::cout << "高效交集: ";
for (int x : intersection) std::cout << x << " "; // 3 4 5
std::cout << std::endl;
// 布隆过滤器演示
EfficientSetOperations::BloomFilter bloom(1000);
bloom.add("hello");
bloom.add("world");
std::cout << "布隆过滤器检查:" << std::endl;
std::cout << "包含'hello': " << bloom.mightContain("hello") << std::endl;
std::cout << "包含'test': " << bloom.mightContain("test") << std::endl;
}
性能深度分析:set vs unordered_set
时间复杂度对比
cpp
#include <chrono>
#include <random>
void performanceBenchmark() {
const int ELEMENT_COUNT = 100000;
std::vector<int> elements;
// 生成测试数据
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(1, ELEMENT_COUNT * 10);
for (int i = 0; i < ELEMENT_COUNT; ++i) {
elements.push_back(dis(gen));
}
// 测试set插入性能
auto start = std::chrono::high_resolution_clock::now();
std::set<int> orderedSet;
for (int elem : elements) {
orderedSet.insert(elem);
}
auto end = std::chrono::high_resolution_clock::now();
auto setInsertTime = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
// 测试unordered_set插入性能
start = std::chrono::high_resolution_clock::now();
std::unordered_set<int> unorderedSet;
for (int elem : elements) {
unorderedSet.insert(elem);
}
end = std::chrono::high_resolution_clock::now();
auto unorderedSetInsertTime = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
// 测试查找性能
start = std::chrono::high_resolution_clock::now();
for (int elem : elements) {
orderedSet.find(elem);
}
end = std::chrono::high_resolution_clock::now();
auto setLookupTime = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
start = std::chrono::high_resolution_clock::now();
for (int elem : elements) {
unorderedSet.find(elem);
}
end = std::chrono::high_resolution_clock::now();
auto unorderedSetLookupTime = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << "性能测试结果 (" << ELEMENT_COUNT << " 个元素):" << std::endl;
std::cout << "set 插入时间: " << setInsertTime.count() << "ms" << std::endl;
std::cout << "unordered_set 插入时间: " << unorderedSetInsertTime.count() << "ms" << std::endl;
std::cout << "set 查找时间: " << setLookupTime.count() << "ms" << std::endl;
std::cout << "unordered_set 查找时间: " << unorderedSetLookupTime.count() << "ms" << std::endl;
std::cout << "set 最终大小: " << orderedSet.size() << std::endl;
std::cout << "unordered_set 最终大小: " << unorderedSet.size() << std::endl;
}
内存使用分析
cpp
void memoryUsageAnalysis() {
std::set<int> orderedSet;
std::unordered_set<int> unorderedSet;
const int COUNT = 1000;
for (int i = 0; i < COUNT; ++i) {
orderedSet.insert(i);
unorderedSet.insert(i);
}
std::cout << "内存使用分析:" << std::endl;
std::cout << "set 大小: " << orderedSet.size() << std::endl;
std::cout << "unordered_set 大小: " << unorderedSet.size() << std::endl;
std::cout << "unordered_set 桶数量: " << unorderedSet.bucket_count() << std::endl;
std::cout << "unordered_set 负载因子: " << unorderedSet.load_factor() << std::endl;
// 内存开销分析:
// set: 每个节点需要存储左右指针、颜色标志等,内存开销相对固定
// unordered_set: 需要维护桶数组和链表节点,存在未使用桶的内存浪费
}
实战应用场景
场景1:数据去重与统计
cpp
class DataDeduplicator {
public:
// 基本去重功能
static std::vector<int> removeDuplicates(const std::vector<int>& data) {
std::unordered_set<int> seen;
std::vector<int> result;
for (int value : data) {
if (seen.insert(value).second) { // 插入成功说明是首次出现
result.push_back(value);
}
}
return result;
}
// 统计唯一元素数量
static size_t countUniqueElements(const std::vector<std::string>& elements) {
return std::unordered_set<std::string>(elements.begin(), elements.end()).size();
}
// 查找第一个重复元素
static std::optional<std::string> findFirstDuplicate(const std::vector<std::string>& elements) {
std::unordered_set<std::string> seen;
for (const auto& elem : elements) {
if (!seen.insert(elem).second) { // 插入失败说明已存在
return elem;
}
}
return std::nullopt;
}
};
void deduplicationDemo() {
std::vector<int> numbers = {1, 2, 2, 3, 4, 4, 4, 5};
std::vector<std::string> words = {"apple", "banana", "apple", "orange", "banana"};
auto uniqueNumbers = DataDeduplicator::removeDuplicates(numbers);
auto uniqueCount = DataDeduplicator::countUniqueElements(words);
auto firstDuplicate = DataDeduplicator::findFirstDuplicate(words);
std::cout << "去重后数字: ";
for (int num : uniqueNumbers) std::cout << num << " "; // 1 2 3 4 5
std::cout << std::endl;
std::cout << "唯一单词数量: " << uniqueCount << std::endl; // 3
if (firstDuplicate) {
std::cout << "第一个重复单词: " << *firstDuplicate << std::endl; // apple
}
}
场景2:访问控制与权限管理
cpp
class AccessControlSystem {
private:
std::unordered_set<std::string> adminUsers;
std::unordered_set<std::string> bannedIPs;
std::set<std::string> privilegedResources; // 有序,便于显示
public:
AccessControlSystem() {
// 初始化管理员
adminUsers = {"alice", "bob", "charlie"};
// 初始化特权资源
privilegedResources = {"/admin", "/config", "/logs", "/backup"};
}
bool isAdmin(const std::string& username) const {
return adminUsers.find(username) != adminUsers.end();
}
bool isIPBanned(const std::string& ip) const {
return bannedIPs.find(ip) != bannedIPs.end();
}
bool canAccessResource(const std::string& username, const std::string& resource) const {
if (privilegedResources.find(resource) != privilegedResources.end()) {
return isAdmin(username);
}
return true; // 非特权资源所有人都可以访问
}
void banIP(const std::string& ip) {
bannedIPs.insert(ip);
}
void unbanIP(const std::string& ip) {
bannedIPs.erase(ip);
}
void displayPrivilegedResources() const {
std::cout << "特权资源列表:" << std::endl;
for (const auto& resource : privilegedResources) {
std::cout << " - " << resource << std::endl;
}
}
};
void accessControlDemo() {
AccessControlSystem acs;
std::cout << "Alice 是管理员: " << acs.isAdmin("alice") << std::endl; // true
std::cout << "Dave 是管理员: " << acs.isAdmin("dave") << std::endl; // false
std::cout << "Alice 访问 /admin: " << acs.canAccessResource("alice", "/admin") << std::endl; // true
std::cout << "Dave 访问 /admin: " << acs.canAccessResource("dave", "/admin") << std::endl; // false
acs.banIP("192.168.1.100");
std::cout << "IP 192.168.1.100 是否被禁: " << acs.isIPBanned("192.168.1.100") << std::endl; // true
acs.displayPrivilegedResources();
}
场景3:图论中的邻接表表示
cpp
class Graph {
private:
std::unordered_map<int, std::unordered_set<int>> adjacencyList;
public:
void addEdge(int from, int to) {
adjacencyList[from].insert(to);
adjacencyList[to].insert(from); // 无向图
}
void removeEdge(int from, int to) {
adjacencyList[from].erase(to);
adjacencyList[to].erase(from);
}
bool hasEdge(int from, int to) const {
auto it = adjacencyList.find(from);
return it != adjacencyList.end() && it->second.find(to) != it->second.end();
}
const std::unordered_set<int>& getNeighbors(int vertex) const {
static const std::unordered_set<int> emptySet;
auto it = adjacencyList.find(vertex);
return it != adjacencyList.end() ? it->second : emptySet;
}
void displayGraph() const {
for (const auto& [vertex, neighbors] : adjacencyList) {
std::cout << vertex << " -> ";
for (int neighbor : neighbors) {
std::cout << neighbor << " ";
}
std::cout << std::endl;
}
}
};
void graphDemo() {
Graph socialNetwork;
// 添加朋友关系
socialNetwork.addEdge(1, 2);
socialNetwork.addEdge(1, 3);
socialNetwork.addEdge(2, 4);
socialNetwork.addEdge(3, 4);
socialNetwork.addEdge(4, 5);
std::cout << "社交网络图:" << std::endl;
socialNetwork.displayGraph();
std::cout << "用户1的朋友: ";
for (int friendId : socialNetwork.getNeighbors(1)) {
std::cout << friendId << " "; // 2 3
}
std::cout << std::endl;
std::cout << "用户1和用户4是否是朋友: " << socialNetwork.hasEdge(1, 4) << std::endl; // false
}
特殊变体:multiset与unordered_multiset
允许重复元素的集合
cpp
void multiSetDemo() {
// multiset:允许重复元素的有序集合
std::multiset<int> multiNumbers;
multiNumbers.insert(3);
multiNumbers.insert(1);
multiNumbers.insert(4);
multiNumbers.insert(1); // 允许重复
multiNumbers.insert(2);
std::cout << "multiset 内容: ";
for (int num : multiNumbers) {
std::cout << num << " "; // 1 1 2 3 4
}
std::cout << std::endl;
// 统计特定元素的出现次数
std::cout << "1 出现次数: " << multiNumbers.count(1) << std::endl; // 2
// 查找所有等于某个值的元素
auto range = multiNumbers.equal_range(1);
std::cout << "所有等于1的元素: ";
for (auto it = range.first; it != range.second; ++it) {
std::cout << *it << " "; // 1 1
}
std::cout << std::endl;
// unordered_multiset:允许重复元素的无序集合
std::unordered_multiset<std::string> multiWords;
multiWords.insert("apple");
multiWords.insert("banana");
multiWords.insert("apple");
std::cout << "'apple' 出现次数: " << multiWords.count("apple") << std::endl; // 2
}
性能优化与最佳实践
1. 预留空间优化
cpp
void reserveOptimization() {
// 对于unordered_set,预先分配空间可以避免重新哈希
std::unordered_set<int> optimizedSet;
optimizedSet.reserve(10000); // 预先分配空间
for (int i = 0; i < 10000; ++i) {
optimizedSet.insert(i);
}
std::cout << "优化后桶数量: " << optimizedSet.bucket_count() << std::endl;
std::cout << "负载因子: " << optimizedSet.load_factor() << std::endl;
}
2. 使用emplace避免拷贝
cpp
void emplaceOptimization() {
std::set<std::string> stringSet;
// 传统insert - 可能产生临时对象
stringSet.insert("hello world"); // 构造临时string
// emplace - 直接在集合中构造
stringSet.emplace("hello world"); // 更高效
// 对于复杂类型,优势更明显
struct ComplexData {
int id;
std::string name;
std::vector<double> values;
ComplexData(int i, const std::string& n, std::vector<double> v)
: id(i), name(n), values(std::move(v)) {}
// 定义比较运算符用于set排序
bool operator<(const ComplexData& other) const {
return id < other.id;
}
};
std::set<ComplexData> complexSet;
// emplace直接构造,避免拷贝
complexSet.emplace(1, "test", std::vector<double>{1.0, 2.0, 3.0});
}
3. 键类型选择优化
cpp
void keyTypeOptimization() {
// 使用小且简单的键类型
std::unordered_set<int> good; // 好:整型键
std::unordered_set<std::string> ok; // 可接受:字符串键
// 避免使用复杂键类型,或提供高效哈希函数
class EfficientKey {
private:
int id;
std::string name;
public:
// 提供高效的哈希函数
std::size_t hash() const {
return std::hash<int>{}(id) ^
(std::hash<std::string>{}(name) << 1);
}
bool operator==(const EfficientKey& other) const {
return id == other.id && name == other.name;
}
};
struct EfficientKeyHash {
std::size_t operator()(const EfficientKey& key) const {
return key.hash();
}
};
std::unordered_set<EfficientKey, EfficientKeyHash> efficientSet;
}
现代C++特性应用
C++17结构化绑定
cpp
void structuredBindingDemo() {
std::set<std::pair<int, std::string>> data = {
{1, "Alice"},
{2, "Bob"},
{3, "Charlie"}
};
// C++17 结构化绑定
for (const auto& [id, name] : data) {
std::cout << "ID: " << id << ", Name: " << name << std::endl;
}
// 传统方式
for (const auto& pair : data) {
std::cout << "ID: " << pair.first << ", Name: " << pair.second << std::endl;
}
}
C++20新特性
cpp
void cpp20Features() {
std::set<int> numbers = {1, 2, 3, 4, 5};
// C++20 contains 方法
if (numbers.contains(3)) {
std::cout << "集合包含3" << std::endl;
}
// C++20 范围适配器
std::vector<int> vec = {1, 2, 2, 3, 3, 3, 4, 5};
// 使用 ranges 去重
#if __has_include(<ranges>)
#include <ranges>
auto uniqueView = vec | std::views::common | std::ranges::to<std::set<int>>();
std::cout << "去重后大小: " << uniqueView.size() << std::endl;
#endif
}
选择指南:set vs unordered_set
决策流程图
需要元素有序遍历?
↓是
选择 std::set
↓
↓否
需要范围查询或最近邻查找?
↓是
选择 std::set
↓
↓否
性能是关键因素,且哈希质量好?
↓是
选择 std::unordered_set
↓
↓否
需要稳定的性能表现?
↓是
选择 std::set (避免哈希冲突的最坏情况)
↓
↓否
内存使用是关键因素?
↓是
测试两种结构,选择更优者
↓
↓否
选择 std::unordered_set (通常更快)
具体场景建议
cpp
void usageRecommendations() {
// 使用set的场景:
// 1. 需要有序遍历或范围查询
std::set<int> sortedUniqueIds;
// 2. 元素比较便宜,但哈希计算昂贵
std::set<std::tuple<int, int, int>> coordinateSet;
// 3. 需要稳定的性能表现
std::set<std::string> sortedDictionary;
// 使用unordered_set的场景:
// 1. 只需要快速存在性检查
std::unordered_set<std::string> wordLookup;
// 2. 哈希质量好且计算快速
std::unordered_set<int> cacheKeys;
// 3. 数据量很大且主要进行查找操作
std::unordered_set<uint64_t> largeDataset;
}
与映射结构的对比
set vs map 的关系
cpp
void setVsMapComparison() {
// set 可以看作是只有键的map
std::set<int> numberSet = {1, 2, 3, 4, 5};
std::map<int, bool> numberMap = {{1, true}, {2, true}, {3, true}, {4, true}, {5, true}};
// 两者在查找性能上相似
// 但set内存开销更小,因为不需要存储值
// 选择原则:
// - 只需要检查存在性 → set
// - 需要存储关联数据 → map
// - 内存敏感 → set
// - 需要键值对 → map
}
总结
set和unordered_set作为集合抽象的具体实现,在不同的应用场景中展现了各自的优势:
std::set的优势:
- 保证元素有序,支持范围查询
- 性能稳定可预测
- 内存使用相对高效
- 迭代器稳定性好
std::unordered_set的优势:
- 平均情况下更快的查找性能
- 适合纯存在性检查场景
- 大数据量时性能优势明显
- 接口更简单直观
核心设计原则:
- 唯一性需求 → 考虑集合结构
- 有序性重要 → 选择set
- 纯性能追求 → 选择unordered_set
- 内存敏感 → 测试两种结构
理解集合结构的特性和适用场景,能够帮助我们在面对去重、存在性检查、集合运算等问题时,选择最合适的数据结构,编写出高效可靠的代码。
下一章预告:《数据结构手册008:高级数据结构实战 - 综合应用与性能优化》
我们将综合运用所学数据结构,解决复杂的实际问题,探索数据结构组合使用的艺术,并深入性能优化技巧,构建高效可靠的系统架构。