Initial commit: C++ courseware

This commit is contained in:
xiaolinquan committed 2026-11-05 22:51:02 +08:00
commit 4a4ee7c287
1789 files changed
+373489

No files matched your search

@@ -0,0 +1,168 @@
### **C++ 算法:哈希表**
#### **一、 引言**
##### **1.1 问题引入:字符串的查重**
【例17-3 字符串哈希】给定 $N$ 个字符串,求共有多少个**不同**的字符串。
**常规思路**:每拿到一个新字符串,都去已有的列表中从头到尾比对一遍。如果字符串数量达到十万、百万级别,这种时间复杂度为 $O(N^2)$ 的方法必将超时。
我们需要**看一眼字符串的名字,就能瞬间知道它应该放在哪里,以及它是否已经存在。**
##### **1.2 核心思想:图书管理员**
想象你是一个图书管理员,面前有海量的书(字符串)。你不想每次都跑遍整个图书馆找书。你希望有一个**规则**,让你一看到书名,就能立刻算出它应该放在编号为几的书架上。
* **字符串** -> *(通过某个神奇规则)* -> **唯一的书架编号**
这个神奇的规则,就是**哈希函数**。通过它,我们可以将查找的时间复杂度从线性的 $O(N)$ 降低到近乎 **$O(1)$**。
---
#### **二、 核心原理解析**
一个完整的哈希表系统,由两个核心部分组成:**哈希函数** 和 **桶数组(加冲突解决机制)**。
##### **2.1 哈希函数:将字符串变成数字**
我们要解决的第一个问题是:如何把一个字符串(如 `"abc"`)转换成一个唯一的数字索引(键值/哈希码)?
* **错误思路**:直接将字符的 ASCII 码相加。这样 `"abc"` 和 `"cba"` 的结果一样,会导致严重的混淆。
* **正确思路:字符串哈希 (进制转换法)**。位置很重要!就像十进制的 `123` ($1\times10^2 + 2\times10^1 + 3\times10^0$)。我们把字符串看作一个 **P进制数**。
**哈希公式**:$hash("abc") = ('a' * P^2 + 'b' * P^1 + 'c' * P^0) % MOD$
* $P$ 通常取质数(如 131),$MOD$ 取一个很大的质数(如 $10^9 + 7$)以防止溢出并压缩数值范围。
**代码实现(迭代法)**:
我们不需要真去算 $P$ 的次方,只需像从高位读数一样,每次将旧结果乘以 $P$,再加上新字符即可。
```cpp
const int P = 131;
const long long MOD = 1e9 + 7;
int hash_string(const string& str) {
long long h = 0;
for (char c : str) {
h = (h * P + c) % MOD; // 迭代公式
}
return h;
}
```
##### **2.2 桶数组与哈希冲突**
算出哈希值后,我们需要把它放入实际的存储空间——**桶数组 **。
为了把巨大的哈希值塞进有限的数组里,我们必须进行取模运算:`index = hash_val % SIZE`(`SIZE` 为桶数组的大小)。
**哈希冲突**:
根据鸽巢原理,必定会有两个**不同**的字符串,计算出的 `index` **完全相同**。这就好比图书管理员发现,两本不同的书被分配到了同一个书架号。难道我们直接扔掉一本吗?
##### **2.3 解决冲突:链地址法**
遇到冲突时,最经典的解决方案是**链地址法 **。
书架不能只放一本书了。我们在每个书架的位置放一个“箱子”或“链表”。所有被分配到这个书架号的书,统统扔进这个箱子里。
**数据结构设计**:
```cpp
// SIZE 是桶的数量
// vector<string> 就是那个"箱子",里面可以装多本哈希值冲突的书
vector<vector<string>> hashTable(SIZE);
```
**查找/插入流程**:
1. 计算新字符串 `s` 的下标:`index = hash_string(s) % SIZE`。
2. 直接走到第 `index` 个箱子前。
3. **遍历这个箱子** `hashTable[index]`。
* 如果找到了 `s`,说明重复了,什么都不做。
* 如果找遍了箱子也没找到,说明是个全新的字符串,把它装进去:`hashTable[index].push_back(s)`。
只要哈希函数设计得好,冲突就会很少,每个箱子里的书极少,查找速度依然是 **$O(1)$**。但如果哈希函数很糟,所有书都挤在一个箱子里,哈希表就会退化成一个长数组,查找速度降回 **$O(N)$**。
---
#### **三、 STL: `unordered_map`**
手写哈希表能深刻理解原理,但在实战中,C++ 标准库(STL)已经为我们准备好了封装的哈希表——**`unordered_map`**。
它隐藏了复杂的哈希函数计算、内存分配和冲突处理,提供了一套极简、高效的操作接口。
##### **3.1 定义与核心特性**
* **头文件**:`#include <unordered_map>`
* **特性**:键(Key)到值(Value)的映射,键是**唯一**的。内部元素是**无序**的。
* **定义语法**:`unordered_map<KeyType, ValueType> umap;`
* 例如:`unordered_map<string, int> count;` (通常用于统计字符串出现的次数)
##### **3.2 操作符:`[]`**
当我们写下 `umap[key]` 时,会发生:
1. **如果 `key` 已经存在**:
它返回该 `key` 对应的值(Value)的引用。你可以直接读取或修改它。
`cout << umap["apple"];` 对应输出 apple 的数量
`umap["apple"]++;` 对应 apple 的数量加 1
2. **如果 `key` 不存在(重要)**:
它会**自动**在哈希表中创建一个新的键值对 `(key, 默认值)`!如果是 `int`,默认值就是 0。
`umap["banana"]++;` // 如果原本没有 banana,它会创建并设为 `int` 类型的初始值 0,然后执行++变成1。
##### **3.3 其他常用成员函数**
* **查找**:`umap.count(key)`。存在返回1,不存在返回0。比 `[]` 更安全,因为 `count` **不会**自动创建不存在的键。
* **插入**:`umap.insert({"apple", 10})`。如果键已存在,插入会失败。
* **删除**:`umap.erase(key)`。删除指定键的元素。
* **状态**:`umap.size()`(元素个数),`umap.empty()`(是否为空)。
##### **3.4 遍历**
使用基于范围的 `for` 循环遍历,每次拿到的是一个 `pair`(包含 `.first` 和 `.second`)。
```cpp
unordered_map<string, int> fruit = {{"apple", 5}, {"banana", 3}};
for (const auto& pair : fruit) {
cout << "水果: " << pair.first << ", 数量: " << pair.second << endl;
}
```
---
#### **四、 案例:Cities and States (B0250)**
**题意简述**:寻找特殊的城市对。城市1的前两个字母 == 城市2的州代码,且 城市2的前两个字母 == 城市1的州代码,且两者不在同一个州。
```cpp
int main() {
int n;
cin >> n;
// 键:组合字符串, 值:该组合出现的次数
unordered_map<string, int> counts;
long long ans = 0;
for (int i = 0; i < n; i++) {
string city, state;
cin >> city >> state;
string city_prefix = city.substr(0, 2); // 截取前两个字母
if (city_prefix != state) { // 必须来自不同的州
string self_key = city_prefix + state; // 我自己的特征
string partner_key = state + city_prefix; // 我需要的伴侣特征
// 查找伴侣:去哈希表里找,我需要的伴侣之前出现了几次?
// 注意利用了 [] 运算符的自动创建特性,如果不存在就返回0,不会报错
ans += counts[partner_key];
// 登记自己:把自己加入哈希表,供后面的城市查找
counts[self_key]++;
}
}
cout << ans << endl;
return 0;
}
```