第九章 红黑树和 set 与 map
1. 二叉搜索树

所以中序遍历的结果是从小到大的


1 查找


如果查找24 那么就查找失败 找到的是空节点 为失败

时间复杂度:树的高h
2 插入

3 构造 BST 树
⼆叉搜索树的构造就是不断向原来的树中插入新的结点即可。

4 删除操作







时间复杂度:查找前驱和后继的操作,会把树进行查找完,然后进行删除,最差也会遍历整个二叉树的高,因此时间复杂度为 O (N) 。(树高)
2.平衡二叉树
cpp
在上⼀⼩节中提到,在某些特定的情况下,⼆叉搜索树是会退化成单链表的,并且各种操作的效率也
会明显的下降,因此我们需要⼀些特别的⼿段保证这个⼆叉搜索树的"平衡",进⽽保证各种操作的
效率。这就是我们接下来要学习的平衡⼆叉树。



1 查找
cpp
与⼆叉搜索树的查找⼀样:从根结点开始,沿某个分⽀逐层向下⽐较的过程,若⾮空,先将给定值与
根结点的关键字⽐较,若相等,则查找成功;若不等,如果⼩于根结点的关键字,则在根结点的左⼦
树上查找,否则在根结点的右⼦树上查找。

左旋:



右旋:


时间复杂度:由于平衡二叉树会限制树的高度不会过高,趋近于 log n 级别,因此时间复杂度为 O (log N) 。
2 插入



LL 型 - 右单旋


RR 型 - 左单旋
RR 表示:新结点由于插⼊在 T 结点的右孩⼦(R)的右子树(RR)中,从而导致失衡。

LR 型 - 左右双旋




RL 型 - 右左双旋
RL 表示:新结点由于插入在 T 结点的右孩子(R)的左子树(RL)中,从而导致失衡。




插入操作的时间复杂度: 旋转操作仅需修改指针,因此最大的时间开销就是先把结点插入到空结点的位置,时间复杂度和查找⼀致,因此为 O (log N) 。
3 构造 AVL 树
平衡二叉树的构造,就是不断向树中插入新的结点。
案例:根据序列 a= {15, 6, 10, 17, 11, 13, 9, 20, 16, 22} ,构造⼀棵二叉排序树



4 删除







时间复杂度:由于可能会调整很多次,最差情况下会从叶子结点开始向上到根,整个过程遍历⼀个树高,因此时间复杂度为 O (log N) 。
3. 红黑树

1 红黑树的规则


左<根<右 根叶黑 不红红 黑路同


2 红黑树的查找

3 红黑树的插入



情况二:叔叔是红色


情况三:叔叔是黑色





4 红黑树的构造



4. set / multiset




因为返回的是迭代器 所以需要*
cpp
#include <iostream>
#include <set>
using namespace std;
int a[] = {10, 60, 20, 70, 80, 30, 90, 40, 100, 50};
int main()
{
set<int> mp;
// 插入
for(auto x : a)
{
mp.insert(x);
}
// 遍历 set,最终的结果应该是有序的
for(auto x : mp)
{
cout << x << " ";
}
cout << endl;
// if(mp.count(1)) cout << "1" << endl;
// if(mp.count(99)) cout << "99" << endl;
// if(mp.count(30)) cout << "30" << endl;
// if(mp.count(10)) cout << "10" << endl;
// mp.erase(30);
// mp.erase(10);
// if(mp.count(30)) cout << "30" << endl;
// else cout << "no:30" << endl;
// if(mp.count(10)) cout << "10" << endl;
// else cout << "no:10" << endl;
auto x = mp.lower_bound(20);
auto y = mp.upper_bound(20);
cout << *x << " " << *y << endl;
return 0;
}
5.map / multimap




cpp
#include <iostream>
#include <map>
using namespace std;
void print(map<string, int>& mp)
{
for(auto& p : mp)
{
cout << p.first << " " << p.second << endl;
}
}
// 统计一堆字符串中,每一个字符串出现的次数
void fun()
{
string s;
map<string, int> mp; // <字符串,字符串出现的次数>
for(int i = 1; i <= 10; i++)
{
cin >> s;
mp[s]++; // 体现了 operator 的强大
}
print(mp);
}
int main()
{
fun();
// map<string, int> mp;
// // 插入
// mp.insert({"张三", 1});
// mp.insert({"李四", 2});
// mp.insert({"王五", 3});
// // print(mp);
// // operator[] 可以让 map 像数组一样使用
// cout << mp["张三"] << endl;
// mp["张三"] = 110;
// cout << mp["张三"] << endl;
// // 注意事项:operator[] 有可能会向 map 中插入本不想插入的元素
// // [] 里面的内容如果不存在 map 中,会先插入,然后再拿值
// // 插入的时候:第一个关键字就是 [] 里面的内容,第二个关键字是一个默认值
if(mp.count("赵六") && mp["赵六"] == 4) cout << "yes" << endl; //这样就不会插入了
// else cout << "no" << endl;
// // 删除
// mp.erase("张三");
// print(mp);
return 0;
}

1 英语作文


cpp
#include <iostream>
#include <map>
using namespace std;
typedef long long LL;
int n, p;
map<string, int> mp; // <高级词汇,含金量>
// 判断 ch 是否合法
bool check(char ch)
{
if((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'))
{
return true;
}
else return false;
}
int main()
{
cin >> n >> p;
for(int i = 1; i <= n; i++)
{
string s; int x;
cin >> s >> x;
mp[s] = x;
}
LL ret = 0;
char ch; // 一个字符一个字符读
string t = "";
while(scanf("%c", &ch) != EOF)
{
if(check(ch)) t += ch;
else
{
// 读到分隔符
ret = (ret + mp[t]) % p;
t = "";
}
}
cout << ret << endl;
return 0;
}
EOF是文件结束的标识符
2 营业额统计


cpp
#include <iostream>
#include <set>
using namespace std;
const int INF = 1e7 + 10;
int n;
set<int> mp; // 存储 i 天之前的营业额
int main()
{
cin >> n;
int ret; cin >> ret;
mp.insert(ret);
// 左右护法 - 防止越界访问
mp.insert(-INF);
mp.insert(INF);
for(int i = 2; i <= n; i++)
{
int x; cin >> x;
// 找出第一个 >= x 的元素
auto it = mp.lower_bound(x);
auto tmp = it;
tmp--;
if(*it == x) continue;
ret += min(abs(*tmp - x), abs(*it - x));
mp.insert(x);
}
cout << ret << endl;
return 0;
}

INF是无穷大



3 木材仓库



cpp
#include <iostream>
#include <set>
using namespace std;
typedef long long LL;
const LL INF = 1e10 + 10;
set<LL> mp;
int main()
{
int q; cin >> q;
// 左右护法 - 处理边界情况
mp.insert(-INF);
mp.insert(INF);
while(q--)
{
LL op, len; cin >> op >> len;
if(op == 1) // 进货
{
if(mp.count(len))
cout << "Already Exist" << endl;
else
mp.insert(len);
}
else // 出货
{
if(mp.size() == 2)
{
cout << "Empty" << endl;
}
else
{
// 找到距离 len 最近的那一个
auto it = mp.lower_bound(len);
auto tmp = it;
tmp--;
if(abs(*tmp - len) <= abs(*it - len))
{
cout << *tmp << endl;
mp.erase(tmp); // 别忘了删除
}
else
{
cout << *it << endl;
mp.erase(it);
}
}
}
}
return 0;
}
第十章 哈希表和 unordered_set 与unordered_map
1. 哈希表的概念





2.常见的哈希函数



3. 处理哈希冲突


h(20)=9 的时候因为9有位置了 所以20去10的地方了 同时21去1的地方
寻找的时候也是这样 如果冲突太多的话 时间复杂度就会太大 所以需要让数组多点 为传入数量的两倍

如果都是冲突的话 那么时间复杂度是n 这个时候可以用红黑树 这样复杂度就是O(logN)
4.哈希表的模拟实现
线性探测


创建








cpp
#include <iostream>
#include <cstring>
using namespace std;
const int N = 23, INF = 0x3f3f3f3f;
int h[N]; // 哈希表数组
// 哈希表初始化
void init()
{
memset(h, 0x3f, sizeof h);
}
// 哈希定位 + 线性探测查找下标
int f(int x)
{
int id = (x % N + N) % N;
// 当前位置被占用且不是目标值,向后探测
while (h[id] != INF && h[id] != x)
{
id++;
if (id == N) id = 0; // 数组到头,循环回到开头
}
return id;
}
// 插入元素
void insert(int x)
{
int id = f(x);
h[id] = x;
}
// 查询元素是否存在
bool find(int x)
{
int id = f(x);
return h[id] == x;
}
int main()
{
init();
int n;
cin >> n;
while (n--)
{
int op, x;
cin >> op >> x;
if (op == 1)
{
insert(x);
}
else
{
if (find(x))
cout << "yes" << endl;
else
cout << "no" << endl;
}
}
return 0;
}
链地址法




cpp
#include <iostream>
using namespace std;
const int N = 23;
int h[N]; // 哈希表头结点数组
int e[N]; // 存储实际数值
int ne[N]; // 存储链表下一个节点下标
int id; // 当前可用节点编号
// 哈希函数,兼容负数
int f(int x)
{
return (x % N + N) % N;
}
// 插入元素(头插法构建链表)
void insert(int x)
{
int idx = f(x);
id++;
e[id] = x;
ne[id] = h[idx];
h[idx] = id;
}
// 遍历链表查找元素
bool find(int x)
{
int idx = f(x);
for (int i = h[idx]; i != 0; i = ne[i])
{
if (e[i] == x)
return true;
}
return false;
}
int main()
{
int n;
cin >> n;
while (n--)
{
int op, x;
cin >> op >> x;
if (op == 1)
{
insert(x);
}
else
{
if (find(x))
cout << "yes" << endl;
else
cout << "no" << endl;
}
}
return 0;
}
5.unordered_set / unordered_multiset




cpp
#include <iostream>
#include <unordered_set>
#include <string> // 字符串需要此头文件,原代码缺失补上
using namespace std;
int main()
{
string strs[] = {"张三", "李四", "王五", "赵六", "小杨", "小珂"};
unordered_set<string> mp;
// 循环插入所有姓名
for (auto& s : strs)
{
mp.insert(s);
}
// 查询张三是否存在
if (mp.count("张三"))
cout << "yes" << endl;
else
cout << "no" << endl;
// 删除张三
mp.erase("张三");
// 再次查询张三
if (mp.count("张三"))
cout << "yes" << endl;
else
cout << "no" << endl;
// 遍历输出容器内剩余所有元素
for (auto& s : mp)
{
cout << s << " ";
}
return 0;
}


cpp
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
void test()
{
// key:顶点编号,value:邻接顶点数组(邻接表写法)
unordered_map<int, vector<int>> mp;
mp[1].push_back(2);
mp[2] = {3, 4, 5};
mp[3].push_back(1);
mp[3].push_back(2);
mp[3].push_back(3);
// 遍历哈希表,打印邻接表
for (auto& [a, v] : mp)
{
cout << a << ": ";
for (auto b : v)
cout << b << " ";
cout << endl;
}
}
int main()
{
test();
unordered_map<string, int> mp;
// 方式1:insert插入键值对
// 写⼀个⼤括号,把需要放进去的元素括起来即可
mp.insert({"张三", 1});
mp.insert({"李四", 2});
mp.insert({"王五", 3});
// 方式2:[]运算符赋值插入
// operator[]:可以让 map 像数组⼀样使⽤
// 赋值
mp["赵六"] = 4;
// 查询数值
if (mp["赵六"] == 4)
cout << "yes" << endl;
else
cout << "no" << endl;
// 使⽤查询的时候要注意,如果 map 中本⾝没有该元素,它会先插⼊,然后再拿值
// 插⼊的时候,第⼆个关键字是默认值
// 如果是数,那就是 0
// 如果是字符串,那就是空串
if (mp["小美"]) //// 会把 <"⼩美", 0> 放进去
cout << "no" << endl;
// count() 查询,不会新增元素
if (mp.count("小帅"))
cout << "yes" << endl;
else
cout << "no" << endl;
// 删除键值对
mp.erase("小美");
// 遍历全部数据
for (auto& [s, num] : mp)
{
cout << s << "编号为: " << num << endl;
}
return 0;
}

1 学籍管理



cpp
#include <iostream>
#include <unordered_map>
#include <string>
using namespace std;
int main()
{
unordered_map<string, int> mp;
int T;
cin >> T;
while (T--)
{
int op;
string name;
cin >> op;
if (op == 1) // 插入/修改数据
{
cin >> name;
int x;
cin >> x;
mp[name] = x;
cout << "OK" << endl;
}
else if (op == 2) // 查询数据
{
cin >> name;
if (mp.count(name))
cout << mp[name] << endl;
else
cout << "Not found" << endl;
}
else if (op == 3) // 删除数据
{
cin >> name;
if (mp.count(name))
{
mp.erase(name);
cout << "Deleted successfully" << endl;
}
else
cout << "Not found" << endl;
}
else // 输出哈希表中元素总个数
{
cout << mp.size() << endl;
}
}
return 0;
}
2 不重复数字


cpp
#include <iostream>
#include <unordered_set>
using namespace std;
int main()
{
int T;
scanf("%d", &T);
while (T--)
{
int n;
scanf("%d", &n);
unordered_set<int> mp;
for (int i = 1; i <= n; i++)
{
int x;
scanf("%d", &x);
// 该数字没有出现过
if (!mp.count(x))
{
printf("%d ", x);
mp.insert(x);
}
}
puts("");
}
return 0;
}

cin比较慢
3 阅读理解




如果用vector的话会因为haha右两个 导致右两个1 会麻烦 但是用set会去重
cpp
#include <iostream>
#include <set>
#include <unordered_map>
#include <string>
using namespace std;
// key:单词,value:有序集合,保存该单词出现过的所有文章编号
unordered_map<string, set<int>> mp;
int main()
{
int n;
cin >> n;
// 依次读取第 1 ~ n 篇文章
for (int i = 1; i <= n; i++)
{
int l;
cin >> l;
// 读取本篇的 l 个单词
while (l--)
{
string s;
cin >> s;
mp[s].insert(i);
}
}
int m;
cin >> m;
// m 次查询单词对应的文章编号
while (m--)
{
string s;
cin >> s;
for (auto num : mp[s])
{
cout << num << " ";
}
cout << endl;
}
return 0;
}
4 A - B 数对

cpp
#include <iostream>
#include <unordered_map>
using namespace std;
typedef long long LL;
const int N = 2e5 + 10;
LL n, c;
LL a[N];
unordered_map<int, int> mp; // key:数值 value:该数值出现次数
int main()
{
cin >> n >> c;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
mp[a[i]]++; //出现一次就+1次
}
LL ret = 0;
for (int i = 1; i <= n; i++)
{
// 统计满足 a[j] - a[i] = c 的数量,等价查找 a[i]+c 的个数
ret += mp[c + a[i]];
}
cout << ret << endl;
return 0;
}
5 Cities and States



cpp
#include <iostream>
#include <unordered_map>
#include <string>
using namespace std;
int n;
int main()
{
cin >> n;
unordered_map<string, int> mp;
int ret = 0;
while (n--)
{
string a, b;
cin >> a >> b;
// 截取字符串a前两位
a = a.substr(0, 2);
// 起点、终点属于同一个州,不计入答案
if (a == b)
continue;
// 累加反向配对的数量
ret += mp[b + a];
// 把当前正向组合存入哈希表
mp[a + b]++;
}
cout << ret << endl;
return 0;
}





