Chapter 2 数据结构
知识
模板
1. 链表
C++ int head ; // 头指针: 指向一个node (init 时指向 NaN, -1)
int idx ; // 全局变量: 从 点集 中取点, 当前取到哪一个了
// 单链表
e [ idx ] = x ; // 序号为idx的node, 值是x
ne [ idx ] = idx_nxt ; // 序号为idx的node, 指向 序号为idx_nxt的node
// 双链表
e [ idx ] = x ;
l [ idx ] = idx_left ;
r [ idx ] = idx_right ;
单链表
初始化: init()
头节点插入: add2head(x)
. "head_node" 指向 "值为x的node"
中间节点插入: add(k, x)
. "序号为k的node" 后面加一个 "值为x的node"
中间节点删除: remove(k)
. 删除 "序号为k的node" 的 "后继node"
遍历输出: for (int i=head; i!=-1; i=ne[i]) cout << e[i] << " ";
双向链表
初始化: init()
中间节点插入: add(k, x)
. "序号为k的node" 后面加一个 "值为x的node"
中间节点删除: remove(k)
. 删除 "序号为k的node" 的 "后继node"
遍历输出: for (int i=head; i!=-1; i=ne[i]) cout << e[i] << " ";
(1) 单链表模板:
C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72 #include <iostream>
using namespace std ;
const int N = 100010 ;
// head 表示头结点的下标
// e[i] 表示节点i的值
// ne[i] 表示节点i的next指针是多少
// idx 存储当前已经用到了哪个点
int head , e [ N ], ne [ N ], idx ;
// 初始化
void init () {
head = -1 ; // head -> NaN
idx = 0 ; // 当前还没开始取点
}
// 将x插到头结点
void add_to_head ( int x ) {
e [ idx ] = x ; // 给node赋值
ne [ idx ] = head ;
head = idx ;
idx ++ ; // 当前点已使用, 点用量++
}
// 将x插到下标是k的点后面
void add ( int k , int x ) {
e [ idx ] = x ; // 给node赋值
ne [ idx ] = ne [ k ];
ne [ k ] = idx ;
idx ++ ; // 当前点已使用, 点用量++
}
// 将下标是k的点后面的点删掉
void remove ( int k ) {
ne [ k ] = ne [ ne [ k ]];
// idx不变, 因为“点池”里node数量没增加
}
int main ()
{
int m ;
cin >> m ;
init ();
while ( m -- ) {
int k , x ;
char op ;
cin >> op ;
if ( op == 'H' ) {
cin >> x ;
add_to_head ( x );
}
else if ( op == 'D' ) {
cin >> k ;
if ( ! k ) head = ne [ head ];
else remove ( k - 1 );
}
else {
cin >> k >> x ;
add ( k - 1 , x );
}
}
for ( int i = head ; i != -1 ; i = ne [ i ]) cout << e [ i ] << ' ' ;
cout << endl ;
return 0 ;
}
(2) 双向链表模板:
C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69 #include <iostream>
using namespace std ;
const int N = 100010 ;
int m ;
int e [ N ], l [ N ], r [ N ], idx ;
// 初始化
void init () {
// 0: 左端点 1: 右端点
r [ 0 ] = 1 ;
l [ 1 ] = 0 ;
// 初始化. 就已经消耗了2个node作为 LEFT and RIGHT
idx = 2 ;
}
// 在节点a的右边插入一个数x
void insert ( int a , int x ) {
e [ idx ] = x ; // 赋值
l [ idx ] = a , r [ idx ] = r [ a ];
l [ r [ a ]] = idx , r [ a ] = idx ++ ;
}
// 删除节点a
void remove ( int a ) {
l [ r [ a ]] = l [ a ];
r [ l [ a ]] = r [ a ];
}
int main ()
{
cin >> m ;
init ();
while ( m -- )
{
string op ;
cin >> op ;
int k , x ;
if ( op == "L" ) {
cin >> x ;
insert ( 0 , x );
}
else if ( op == "R" ) {
cin >> x ;
insert ( l [ 1 ], x );
}
else if ( op == "D" ) {
cin >> k ;
remove ( k + 1 );
}
else if ( op == "IL" ) {
cin >> k >> x ;
insert ( l [ k + 1 ], x );
}
else {
cin >> k >> x ;
insert ( k + 1 , x );
}
}
for ( int i = r [ 0 ]; i != 1 ; i = r [ i ]) cout << e [ i ] << ' ' ;
cout << endl ;
return 0 ;
}
2. 栈 && 队列
个人习惯是基于STL直接调用
(1) stack:
C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14 #include <stack>
stack < int > st ;
// 返回栈顶元素
st . top ();
// 入栈 && 弹出栈顶元素
st . push ( 1 );
st . pop ();
// 判空 && 当前栈内元素个数
st . empty ();
st . size ();
(2) queue:
C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 #include <queue>
queue < int > q ;
// 队首元素 && 队尾元素
q . front ()
q . back ()
// 入队 && 队首元素弹出
q . push ()
q . pop ()
// // 判空 && 当前queue内元素个数
q . empty ()
q . size ()
(3) vector:
比起用 queue / deque
, 我更喜欢用vector
来模拟
C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54 #include <vector>
/*
以 vector<int> v1 = {1,2,3,4,5,6,7,8,9,10}; 为例
*/
// (1) 初始化 1D | 2D 数组
vector < int > vec ;
vector < vector < int > > vec_2d ;
// (2) 插入
// push_back: 尾插
v1 . push_back ( 1 );
// insert(ptr, value): 任意位置插值
// v1.begin() 返回一个指向字符串第一个元素的迭代器
v1 . insert ( v1 . begin () + 3 , 30 ); // 在 v1[2] ~ v1[3] 之间加上30. now: {1, 2, 3, 30, 4, 5, 6, 7, 8, 9, 10}
// (3) 访问
cout << v1 [ 0 ] << endl ;
cout << v1 [ 1 ] << endl ;
cout << v1 . front () << endl ; // 获取第一个元素
cout << v1 . back () << endl ; // 获取最后一个元素
// (4) 删除
// pop_back: 尾删
v1 . pop_back ();
// erase(ptr): 任意位置删除
v1 . erase ( v1 . begin () + 3 ); // 删除 v1[3] (上例, 4). now: {1, 2, 3, 5, 6, 7, 8, 9, 10}
v . erase ( v1 . begin () + 1 , v1 . begin () + 3 ); // 删除 [ v1[1] ~ v1[3] ). now: {1,4,5,6,7,8,9,10}
// (5) 特殊
v1 . empty () // 判空
v1 . clear () // 清空 vector 中的所有元素
// (6) 遍历
for ( int i = 0 ; i < v1 . size (); i ++ ) // 下标访问式
{
cout << v1 [ i ] << ' ' ;
}
for ( vector < int >:: iterator i = v1 . begin (); i != v1 . end (); i ++ ) // 纯迭代器式
{
cout << * i << ' ' ;
}
for ( auto e : v1 ) // 迭代器式 (auto)
{
cout << e << ' ' ;
}
3. 单调栈 && 单调队列
算不上是数据结构,只是基于 stack && queue 进行了一系列维护单调性的操作
单调栈
单调递增栈:在保持栈内元素单调递增的前提下(如果栈顶元素大于要入栈的元素,将将其弹出),将新元素入栈
单调递减栈:在保持栈内元素单调递减的前提下(如果栈顶元素小于要入栈的元素,则将其弹出),将新元素入栈
单调队列
(1) 单调栈: 给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1
C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 // 可以使用 单调stack 的原因:
// 当 i < j 时, if a[i] > a[j], 则 a[i] 肯定不是答案
#include <iostream>
#include <stack>
using namespace std ;
const int N = 100010 ;
int n ;
int a [ N ];
stack < int > stk ;
int main ()
{
cin >> n ;
for ( int i = 0 ; i < n ; ++ i ) {
int s ;
cin >> s ;
while ( ! stk . empty () && s <= stk . top ()) stk . pop ();
a [ i ] = stk . empty () ? -1 : stk . top ();
stk . push ( s );
}
for ( int i = 0 ; i < n ; i ++ ) cout << a [ i ] << " " ;
cout << endl ;
return 0 ;
}
(2) 单调队列: 确定滑动窗口位于每个位置时,窗口中的"最大值"和"最小值" (滑动窗口问题)
C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78 /*
分析:
这道题目, 我们就维护两个队列, 一个是最小值, 一个是最大值. 这里唯一的重点就是, 每一次入队的时候, 不需要管是不是比队头小, 因为也许他现在小, 但是在队头出队列后, 他还在, 而且是最小的值.
seeking for MIN:
发现一个性质:
如果队列中存在两个元素, 满足 a[i] >= a[j] 且 i < j, 那么无论在什么时候我们都不会取 a[i] 作为最小值了, 所以可以直接将 a[i] 删掉
*/
#include <iostream>
#include <vector>
using namespace std ;
const int N = 1000010 ;
int a [ N ];
int main ()
{
int n , k ;
cin >> n >> k ;
for ( int i = 0 ; i < n ; i ++ ) cin >> a [ i ];
// (1) 使用 vector 求解滑动窗口最小值
vector < int > q ; // q: 装的是下标
for ( int i = 0 ; i < n ; i ++ )
{
// 1. 队头检查: 如果队头元素的下标超出了窗口范围,则从队头出队
if ( ! q . empty () && i - k + 1 > q . front ()) {
q . erase ( q . begin ());
}
// 2. 队尾维护单调性: 从队尾开始,移除所有比 a[i] 大的元素
while ( ! q . empty () && a [ q . back ()] >= a [ i ]) {
q . pop_back ();
}
// 3. 入队: 将当前元素的下标加入队尾
q . push_back ( i );
// 4. 取答案: 当窗口形成后,队头即为当前窗口的最小值
if ( i >= k - 1 ) {
cout << a [ q . front ()] << " " ;
}
}
cout << endl ;
// (2) 使用 vector 求解滑动窗口最大值
q . clear (); // 清空 vector,为求最大值做准备
for ( int i = 0 ; i < n ; i ++ )
{
// 1. 队头检查
if ( ! q . empty () && i - k + 1 > q . front ()) {
q . erase ( q . begin ());
}
// 2. 队尾维护单调性: 逻辑与求最小值时相反
while ( ! q . empty () && a [ q . back ()] <= a [ i ]) {
q . pop_back ();
}
// 3. 入队
q . push_back ( i );
// 4. 取答案
if ( i >= k - 1 ) {
cout << a [ q . front ()] << " " ;
}
}
cout << endl ;
return 0 ;
}
4. KMP
直接放弃了,考到就背模板吧,大概率认栽...
给定一个字符串 S,以及一个模式串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。 模式串 P 在字符串 S 中多次作为子串出现。求出模式串 P 在字符串 S 中所有出现的位置的起始下标。
s[ ]
是模式串,即比较长的字符串
p[ ]
是模板串,即比较短的字符串
next[ ]
是“部分匹配值表”,即next数组,它存储的是每一个下标对应的“部分匹配值”
核心思想: 在每次失配时,不是把p串往后移一位,而是把p串往后移动至下一次可以和前面部分匹配的位置,这样就可以跳过大多数的失配步骤。而每次p串移动的步数就是通过查找next[ ]
数组确定的。
C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 #include <iostream>
using namespace std ;
const int N = 100010 , M = 1000010 ;
int n , m ;
int ne [ N ];
char s [ M ], p [ N ];
int main ()
{
cin >> n >> p + 1 >> m >> s + 1 ;
for ( int i = 2 , j = 0 ; i <= n ; i ++ )
{
while ( j && p [ i ] != p [ j + 1 ]) j = ne [ j ];
if ( p [ i ] == p [ j + 1 ]) j ++ ;
ne [ i ] = j ;
}
for ( int i = 1 , j = 0 ; i <= m ; i ++ )
{
while ( j && s [ i ] != p [ j + 1 ]) j = ne [ j ];
if ( s [ i ] == p [ j + 1 ]) j ++ ;
if ( j == n )
{
printf ( "%d " , i - n );
j = ne [ j ];
}
}
return 0 ;
}
5. Trie 树 (Trie数组)
快速 存储 / 查找 字符串集合
只有找到以 *
结尾的, 才算一个真正的“字符串匹配”, find
才能 ++
.
son[x][num] = y
: nodeX
的一个 值为num的子节点 为 nodeY
.
init: son[N][26]
.
(num=0
: a
) | (num=1
: b
) | ... | (num=25
: z
).
eg. son[1][0]=2
: 表示 1结点
的一个值为a的子结点为 结点2
.
son[1][0] = 0
: 意味着没有值为a子结点
cnt[x]
: 以 x
结尾的单词数量
idx
: 全局变量, 相当于一个 node 分配计数器
题面:
维护一个字符串集合,支持两种操作:
I x
向集合中插入一个字符串 x
Q x
询问一个字符串在集合中出现了多少次
共有 N 个操作,所有输入的字符串总长度不超过 105,字符串仅包含小写英文字母。
代码:
C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60 #include <iostream>
#include <string>
using namespace std ;
const int N = 100010 ;
int son [ N ][ 26 ], cnt [ N ], idx ;
string input , str ;
void insert ( string str )
{
int p = 0 ;
for ( int i = 0 ; str [ i ]; i ++ )
{
// 将当前字符转换成数字 (a->0, b->1,...)
int u = str [ i ] - 'a' ;
// 如果数中不能走到当前字符 (!son[p][u])
// 为当前字符创建新的节点,保存该字符
if ( ! son [ p ][ u ]) son [ p ][ u ] = ++ idx ; // 建立 父子节点 连接
p = son [ p ][ u ]; // 指针下移, 类似 nxt[]
}
// p 等于字符串 s 的尾字符所对应的 idx
// cnt[p] 保存的是字符串 s 出现的次数
cnt [ p ] ++ ; // 以p结尾的string个数++
}
int query ( string str )
{
int p = 0 ;
for ( int i = 0 ; str [ i ]; i ++ )
{
int u = str [ i ] - 'a' ;
if ( ! son [ p ][ u ]) return 0 ;
p = son [ p ][ u ]; // 指针下移, 类似 nxt[]
}
return cnt [ p ];
}
int main ()
{
int n ;
cin >> n ;
while ( n -- ) {
cin >> input >> str ;
/*
在这题里不对, 因为 <blankspace> 会将string分成两个:
string str = "";
int len = input.length();
for (int i=2; i<=len-1; i++) str += input[i];
*/
if ( input [ 0 ] == 'I' ) insert ( str );
else cout << query ( str ) << endl ;
}
return 0 ;
}
6. 并查集
合并集合: p[ find(a) ] = find(b)
.
询问两个元素是否在一个集合中: find(a) == find(b)
.
查询一个集合中的元素个数: 开个 cnt
数组, 为每个集合的root维护, 更新方式 cnt[find(a)]
.
C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45 /*
M a b: 将编号为 a 和 b 的两个数所在的集合合并, 如两个数已经在同一个集合中, 则忽略操作
Q a b: 询问编号为 a 和 b 的两个数是否在同一个集合中
*/
#include <iostream>
#include <string>
using namespace std ;
const int N = 100010 ;
int p [ N ];
int find ( int x ) // 查找x的祖先节点 (root)
{
// 并查集: 只有 root 节点的 p[x]==x
if ( p [ x ] != x ) p [ x ] = find ( p [ x ]); // 上行
return p [ x ];
}
int main ()
{
int n , m ;
cin >> n >> m ;
// 并查集初始化 ("每个node都是root")
for ( int i = 1 ; i <= n ; i ++ ) p [ i ] = i ;
while ( m -- )
{
string op ;
int a , b ;
cin >> op >> a >> b ; // <blankspace> 自动划分
if ( op == "M" ) p [ find ( a )] = find ( b ); // "集合合并"
else
{
// "判断两个元素是否属于同一个集合"
if ( find ( a ) == find ( b )) puts ( "Yes" );
else puts ( "No" );
}
}
return 0 ;
}
C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65 /*
连通块中点的数量
C a b: 在点 a 和点 b 之间连一条边, a 和 b 可能相等
Q1 a b: 询问点 a 和点 b 是否在同一个连通块中, a 和 b 可能相等
Q2 a: 询问点 a 所在连通块中点的数量
*/
#include <iostream>
using namespace std ;
const int N = 100010 ;
int n , m ;
int p [ N ], cnt [ N ];
int find ( int x )
{
if ( p [ x ] != x ) p [ x ] = find ( p [ x ]);
return p [ x ];
}
int main ()
{
cin >> n >> m ;
// 并查集初始化
// 1. 每个node都是root
// 2. 每个set里都只有自己这一个元素
for ( int i = 1 ; i <= n ; i ++ ) {
p [ i ] = i ;
cnt [ i ] = 1 ;
}
while ( m -- ) {
string op ;
int a , b ;
cin >> op ;
if ( op == "C" )
{
cin >> a >> b ;
a = find ( a ), b = find ( b );
if ( a != b ) { // "集合合并"
p [ a ] = b ;
cnt [ b ] += cnt [ a ];
}
}
else if ( op == "Q1" )
{
// 集合判等
cin >> a >> b ;
if ( find ( a ) == find ( b )) puts ( "Yes" );
else puts ( "No" );
}
else
{
// 输出集合内元素个数
cin >> a ;
cout << cnt [ find ( a )] << endl ;
}
}
return 0 ;
}
7. 堆
应用题目我喜欢STL, 但是heap的模拟很重要, 后面平衡树之类的会用到
(1) 定义:
C++ #include <queue> // 只需要这一个头文件就管够了
// 定义 大根堆 (default)
priority_queue < int > max_heap ;
// 定义 小根堆
priority_queue < int , vector < int > , greater < int >> min_heap ;
结构体的大根堆/小根堆:
C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 // 结构体定义
struct node {
int ve ;
int dist ;
};
// ===========================
// 建立小根堆
// ===========================
struct node {
int ve ; // 顶点
int dist ; // 距离出发点的min_dist
bool operator > ( const node & other ) const {
return dist > other . dist ;
}
};
priority_queue < node , vector < node > , greater < node >> min_heap ;
// ===========================
// 建立大根堆
// ===========================
struct node {
int ve ;
int dist ;
bool operator < ( const node & other ) const {
return this -> dist < other . dist ;
}
};
priority_queue < node > max_heap ;
(2) 常用操作:
C++ heap . top () // 访问堆顶元素(此时优先队列不能为空)
heap . empty () // 询问容器是否为空
heap . size () // 查询容器中的元素数量
heap . push ( x ) // 插入元素,并对底层容器排序
heap . pop () // 删除堆顶元素(此时优先队列不能为空)
(3) 数组模拟heap:
Text Only 1
2
3
4
5
6
7
8
9
10
11
12
13 1. ph[] && hp[] && idx
堆中的每次插入都是在堆尾,但是堆中经常有up和down操作。所以节点与节点的关系并不是用一个ne[idx][2]可以很好地维护的。但是好在堆是个完全二叉树。子父节点的关系可以通过下标来联系(左儿子2n,右儿子2n+1)
就数组模拟来说,知道数组的下标就知道结点在堆中的位置。所以核心就在于即使有down和up操作也能维护堆数组的下标(k)和结点(idx)的映射关系。 比如说:h[k] = x, h数组存的是节点的值,按理来说应该h[idx]来存,但是节点位置总是在变的,因此维护k和idx的映射关系就好啦
举例: 用ph数组来表示ph[idx] = k(idx到下标), 那么结点值为h[ph[idx]], 儿子为ph[idx] * 2和ph[idx] * 2 + 1, 这样值和儿子结点就可以通过idx联系在一起了!
2. 为什么还要 hp[]
h数组主要用于帮助从idx映射到下标k,似乎有了ph数组就可以完成所有操作了,但为什么还要有一个hp数组呢?
原因就在于在swap操作中我们输入是堆数组的下标,无法知道每个堆数组的k下标对应idx(第idx个插入),所以需要hp数组方便查找idx
C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84 #include <iostream>
#include <algorithm>
#include <string>
using namespace std ;
const int N = 100010 ;
int h [ N ], ph [ N ], hp [ N ], cnt ;
void heap_swap ( int a , int b )
{
// 第a个插入的数 && 第b个插入的数
swap ( ph [ hp [ a ]], ph [ hp [ b ]]);
swap ( hp [ a ], hp [ b ]);
swap ( h [ a ], h [ b ]);
}
void down ( int u )
{
// NodeU 下移
int t = u ;
if ( u * 2 <= cnt && h [ u * 2 ] < h [ t ]) t = u * 2 ;
if ( u * 2 + 1 <= cnt && h [ u * 2 + 1 ] < h [ t ]) t = u * 2 + 1 ;
if ( u != t )
{
heap_swap ( u , t );
down ( t );
}
}
void up ( int u )
{
// NodeU 上移
while ( u / 2 && h [ u ] < h [ u / 2 ])
{
heap_swap ( u , u / 2 );
u >>= 1 ;
}
}
int main ()
{
int n , m = 0 ;
cin >> n ;
while ( n -- ) {
string op ;
int k , x ;
cin >> op ;
if ( op == "I" ) {
cin >> x ;
cnt ++ ;
m ++ ;
ph [ m ] = cnt , hp [ cnt ] = m ; // 插入序号 <-> heap index
h [ cnt ] = x ;
up ( cnt );
}
else if ( op == "PM" ) cout << h [ 1 ] << endl ;
else if ( op == "DM" ) {
heap_swap ( 1 , cnt );
cnt -- ;
down ( 1 );
}
else if ( op == "D" ) {
cin >> k ; // 第k个插入的数
k = ph [ k ];
heap_swap ( k , cnt );
cnt -- ;
up ( k );
down ( k );
}
else {
cin >> k >> x ;
k = ph [ k ];
h [ k ] = x ;
up ( k );
down ( k );
}
}
return 0 ;
}
8. 哈希表
哈希表的作用:把庞大复杂的数据映射到较小的范围
拉链法: 纯邻接表
开放寻址法: 多开几倍, 一直向后看, 如果有坑就坐, 如果坑被占就继续向后看
题面:
维护一个集合,支持如下几种操作:
I x
,插入一个整数 x
Q x
,询问整数 x 是否在集合中出现过
现在要进行 N 次操作, 对于每个询问操作输出对应的结果
数据范围:
\(1\) ≤ \(N\) ≤ \(10^5\)
−\(10^9\) ≤ \(x\) ≤ \(10^9\)
(1) 拉链法
C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55 #include <cstring>
#include <string>
#include <iostream>
using namespace std ;
// 选择一个大于数据范围N的素数 (玄学理论), 这里 1e5+3 即可
const int N = 100003 ; // 如何筛素数? 见后面数学的chapter
int h [ N ], e [ N ], ne [ N ], idx ;
void insert ( int x ) {
// 定位 h[]
int k = ( x % N + N ) % N ;
// 像链表那样插入node即可
e [ idx ] = x ;
ne [ idx ] = h [ k ];
h [ k ] = idx ++ ;
}
bool find ( int x ) {
// 定位 h[]
int k = ( x % N + N ) % N ;
// 遍历链表进行查询
for ( int i = h [ k ]; i != -1 ; i = ne [ i ])
if ( e [ i ] == x )
return true ;
return false ;
}
int main ()
{
int n ;
cin >> n ;
// 初始化: 跟链表一样 (HEAD指向空指针)
memset ( h , -1 , sizeof h ); // 一整排 head[]
while ( n -- )
{
string op ;
int x ;
cin >> op >> x ;
if ( op == "I" ) insert ( x );
else
{
if ( find ( x )) puts ( "Yes" );
else puts ( "No" );
}
}
return 0 ;
}
(2) 开放寻址法
C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44 #include <cstring>
#include <iostream>
using namespace std ;
// 选择一个大于数据范围 2~3倍 的素数 (玄学理论), 这里 2e5+3 即可
const int N = 200003 , null = 0x3f3f3f3f ;
int h [ N ];
int find ( int x )
{
int t = ( x % N + N ) % N ;
while ( h [ t ] != null && h [ t ] != x )
{
t ++ ;
if ( t == N ) t = 0 ;
}
return t ;
}
int main ()
{
// 初始化: 确保在取值范围1e9以外即可
memset ( h , 0x3f , sizeof h );
int n ;
scanf ( "%d" , & n );
while ( n -- )
{
char op [ 2 ];
int x ;
scanf ( "%s%d" , op , & x );
if ( * op == 'I' ) h [ find ( x )] = x ;
else
{
if ( h [ find ( x )] == null ) puts ( "No" );
else puts ( "Yes" );
}
}
return 0 ;
}
9. C++ STL
除了上面的那些,我们还需要着重说一下这几个:
set && unordered_set
map && unordered_map
string
pair
(1) set && unordered_set
头文件: #include <set>
集合三要素
解释
set
multiset
unordered_set
确定性
一个元素要么在集合中,要么不在
✔
✔
✔
互异性
一个元素仅可以在集合中出现一次
✔
❌(任意次)
✔
无序性
集合中的元素是没有顺序的
❌(从小到大)
❌(从小到大)
✔
初始化定义:
C++ // 默认从小到大
set < int > st1 ; // 储存int的集合(从小到大)
set < int , greater < int >> st2 ; // 储存int的集合(从大到小)
遍历:
C++ // method 1:
for ( set < int >:: iterator it = st . begin (); it != st . end (); ++ it )
cout << * it << endl ;
// method 2:
for ( auto & ele : st )
cout << ele << endl ;
其他:
作用
用法
示例
插入元素
.insert(元素)
st.insert(1);
删除元素
.erase(元素)
st.erase(2);
查找元素
.find(元素)
auto it = st.find(1);
判断元素是否存在
.count(元素)
st.count(3);
查看大小 / 清空 / 判空
略
st.clear();
使用场景:
元素去重:\([1,1,3,2,4,4]\to[1,2,3,4]\)
维护顺序:\([1,5,3,7,9]\to[1,3,5,7,9]\)
元素是否出现过:元素大小 \([-10^{18},10^{18}]\) ,元素数量 \(10^6\) ,vis 数组无法实现,通过 set 可以完成。
count(x)
: 返回 set
内键为 x 的元素数量
find(x)
: 在 set
内存在键为 x 的元素时会返回该元素的迭代器,否则返回 end()
lower_bound(x)
: 返回指向首个不小于给定键的元素的迭代器。如果不存在这样的元素,返回 end()
upper_bound(x)
: 返回指向首个大于给定键的元素的迭代器。如果不存在这样的元素,返回 end()
empty()
: 返回容器是否为空
size()
: 返回容器内元素个数
注意事项:
[1]
可使用迭代器进行遍历,它不存在下标这一概念
[2]
set 的迭代器取到的元素是只读的(因为是 const 迭代器),不可修改其值。如果要改,需要先 erase 再 insert. 下面是错误用法:
C++ cout << * st . begin () << endl ; // 正确。可读。
* st . begin () = 1 ; // 错误!不可写!
(2) map && unordered_map
头文件: #include <map>
性质
解释
map
multimap
unordered_map
互异性
一个键仅可以在映射中出现一次
✔
❌(任意次)
✔
无序性
键是没有顺序的
❌(从小到大)
❌(从小到大)
✔
初始化定义:
C++ // map<key_type, value_type> map_instance;
// 默认key从小到大
map < int , int > mp1 ; // int->int 的映射(键从小到大)
map < int , int , greater < int >> st2 ; // int->int 的映射(键从大到小)
遍历:
C++ // 可使用迭代器进行遍历
for ( map < int , int >:: iterator it = mp . begin (); it != mp . end (); ++ it )
cout << it -> first << ' ' << it -> second << endl ;
// 基于范围的循环(C++11)
for ( auto & pr : mp )
cout << pr . first << ' ' << pr . second << endl ;
// 结构化绑定 + 基于范围的循环(C++17)
for ( auto & [ key , val ] : mp )
cout << key << ' ' << val << endl ;
其他:
作用
用法
示例
增 / 改 / 查元素
中括号
mp[1] = 2;
查元素(返回迭代器)
.find(元素)
auto it = mp.find(1);
删除元素
.erase(元素)
mp.erase(2);
判断元素是否存在
.count(元素)
mp.count(3);
查看大小 / 清空 / 判空
略
mp.clear();
类似于insert()
.insert(PairType)
mp.insert({"Bob", 95});
插入与删除操作:
可以直接通过下标访问来进行查询或插入操作。例如 mp["Alan"]=100
通过向 map
中插入一个类型为 pair<Key, T>
的值可以达到插入元素的目的,例如 mp.insert(pair<string,int>("Alan",100));
erase(key)
函数会删除键为 key
的 所有 元素。返回值为删除元素的数量
erase(pos)
: 删除迭代器为 pos 的元素,要求迭代器必须合法
erase(first,last)
: 删除迭代器在 [first, last)
范围内的所有元素
clear()
函数会清空整个容器
查询操作:
count(x)
: 返回容器内键为 x 的元素数量
find(x)
: 若容器内存在键为 x 的元素,会返回该元素的迭代器;否则返回 end()
lower_bound(x)
: 返回指向首个不小于给定键的元素的迭代器
upper_bound(x)
: 返回指向首个大于给定键的元素的迭代器。若容器内所有元素均小于或等于给定键,返回 end()
empty()
: 返回容器是否为空
size()
: 返回容器内元素个数
注意事项:
[1]
如果使用中括号访问 map 时对应的键不存在,那么会新增这个键,并且值为默认值,因此中括号会影响键的存在性
C++ map < char , int > mp ;
cout << mp . count ( 'a' ) << endl ; // 0
mp [ 'a' ]; // 即使什么都没做,此时mp['a']=0已经插入了
cout << mp . count ( 'a' ) << endl ; // 1
cout << mp [ 'a' ] << endl ; // 0
(3) string
头文件: #include <string>
常用操作:
C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36 // (0) 转 char 数组
s . c_str ()
// (1) 获取长度
str . length ();
str . size ();
// (2) 寻找某字符(串)第一次出现的位置
// find(str,pos)
string s = "OI Wiki" , t = "OI" , u = "i" ;
int pos = 5 ;
printf ( "字符 I 在 s 的 %lu 位置第一次出现 \n " , s . find ( 'I' ));
printf ( "字符 a 在 s 的 %lu 位置第一次出现 \n " , s . find ( 'a' ));
printf ( "字符 a 在 s 的 %d 位置第一次出现 \n " , s . find ( 'a' ));
printf ( "字符串 t 在 s 的 %lu 位置第一次出现 \n " , s . find ( t ));
printf ( "在 s 中自 pos 位置起字符串 u 第一次出现在 %lu 位置" , s . find ( u , pos ));
/*
字符 I 在 s 的 1 位置第一次出现
字符 a 在 s 的 18446744073709551615 位置第一次出现 // 即为 size_t(-1),具体数值与平台有关。
字符 a 在 s 的 -1 位置第一次出现 // 强制转换为 int 类型则正常输出 -1
字符串 t 在 s 的 0 位置第一次出现
在 s 中自 pos 位置起字符串 u 第一次出现在 6 位置
*/
// (3) 截取子串
// substr(pos, len) 函数的参数返回从 pos位置 开始截取最多 len 个字符组成的字符串
// 不会忽略空格
string s = "OI Wiki" , t = "OI" ;
cout << s . substr ( 3 , 3 ). c_str () << endl ;
cout << t . substr ( 1 , 3 ). c_str () << endl ;
cout << s . substr ( 1 , 3 ). c_str () << endl ;
/*
Wik
I
I W
*/
冷门备份:
C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 // 插入 / 删除字符(串)
string s = "OI Wiki" , t = " Wiki" ;
char u = '!' ;
s . erase ( 2 );
printf ( "从字符串 s 的第三位开始删去所有字符后得到的字符串是 %s \n " , s . c_str ());
s . insert ( 2 , t );
printf ( "在字符串 s 的第三位处插入字符串 t 后得到的字符串是 %s \n " , s . c_str ());
s . insert ( 7 , 3 , u );
printf ( "在字符串 s 的第八位处连续插入 3 次字符串 u 后得到的字符串是 %s" ,
s . c_str ());
/*
从字符串 s 的第三位开始删去所有字符后得到的字符串是 OI
在字符串 s 的第三位处插入字符串 t 后得到的字符串是 OI Wiki
在字符串 s 的第八位处连续插入 3 次字符串 u 后得到的字符串是 OI Wiki!!!
*/
// 替换字符(串)
string s = "OI Wiki" ;
s . replace ( 2 , 5 , "" );
printf ( "将字符串 s 的第 3~7 位替换为空串后得到的字符串是 %s \n " , s . c_str ());
s . replace ( s . begin (), s . begin () + 2 , "NOI" );
printf ( "将字符串 s 的前两位替换为 NOI 后得到的字符串是 %s" , s . c_str ());
/*
将字符串 s 的第 3~7 位替换为空串后得到的字符串是 OI
将字符串 s 的前两位替换为 NOI 后得到的字符串是 NOI
*/
(4) pair
初始化:
C++ // (1)
pair < int , double > p0 ( 1 , 2.0 );
// (2)
pair < int , double > p1 ;
p1 . first = 1 ;
p1 . second = 2.0 ;
// (3)
pair < int , double > p2 = make_pair ( 1 , 2.0 );
auto p3 = make_pair ( 1 , 2.0 );
访问:
C++ int i = p0 . first ;
double d = p0 . second ;
// 可以直接对其进行修改
p1 . first ++ ;
赋值与交换:
C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36 #include <iostream>
#include <utility> // pair 所在的头文件 (no need)
#include <string>
int main ()
{
// 创建并初始化两个 pair
std :: pair < int , std :: string > lockerA = { 101 , "苹果" };
std :: pair < int , std :: string > lockerB = { 202 , "香蕉" };
std :: cout << "--- 赋值前 ---" << std :: endl ;
std :: cout << "A号柜: " << lockerA . first << ", " << lockerA . second << std :: endl ;
std :: cout << "B号柜: " << lockerB . first << ", " << lockerB . second << std :: endl ;
// 执行赋值操作
std :: cout << " \n 执行 lockerA = lockerB; \n " << std :: endl ;
lockerA = lockerB ;
std :: cout << "--- 赋值后 ---" << std :: endl ;
std :: cout << "A号柜: " << lockerA . first << ", " << lockerA . second << std :: endl ;
std :: cout << "B号柜: " << lockerB . first << ", " << lockerB . second << std :: endl ;
return 0 ;
}
/*
--- 赋值前 ---
A号柜: 101, 苹果
B号柜: 202, 香蕉
执行 lockerA = lockerB;
--- 赋值后 ---
A号柜: 202, 香蕉
B号柜: 202, 香蕉
*/
C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36 #include <iostream>
#include <utility>
#include <string>
int main ()
{
// 创建并初始化两个 pair
std :: pair < int , std :: string > lockerC = { 303 , "樱桃" };
std :: pair < int , std :: string > lockerD = { 404 , "榴莲" };
std :: cout << "--- 交换前 ---" << std :: endl ;
std :: cout << "C号柜: " << lockerC . first << ", " << lockerC . second << std :: endl ;
std :: cout << "D号柜: " << lockerD . first << ", " << lockerD . second << std :: endl ;
// 执行交换操作
std :: cout << " \n 执行 swap(lockerC, lockerD); \n " << std :: endl ;
swap ( lockerC , lockerD );
std :: cout << "--- 交换后 ---" << std :: endl ;
std :: cout << "C号柜: " << lockerC . first << ", " << lockerC . second << std :: endl ;
std :: cout << "D号柜: " << lockerD . first << ", " << lockerD . second << std :: endl ;
return 0 ;
}
/*
--- 交换前 ---
C号柜: 303, 樱桃
D号柜: 404, 榴莲
执行 swap(lockerC, lockerD);
--- 交换后 ---
C号柜: 404, 榴莲
D号柜: 303, 樱桃
*/
pair 与 map:
C++ // map 的是 C++ 中存储键值对的数据结构
// 很多情况下,map 中存储的键值对通过 pair 向外暴露
map < int , double > m ;
m . insert ( make_pair ( 1 , 2.0 ));