链表
存储
在机试中不要使用动态创建节点再组成链表的方式,在链表较大时甚至直接超时:
通常使用数组模拟链表的方式,这样的优势就是快,这种方式也称为静态链表,能够做到指针式的动态链表的所有操作:
单链表
单链表一般是邻接表的形式,用于存储图和树
常用于优化特殊问题。
栈
同样使用数组模拟栈,并且栈的操作非常简单,使用栈顶指针完全能够实现:
队列
单调队列
数组实现的线性表,比 STL 的优势就是速度快、占用小,但是编译器开 O2 优化的话,STL 的速度也相差无几。
解题思路(以最大值为例):
由于我们需要求出的是滑动窗口的最大值。
- 如果当前的滑动窗口中有两个下标
i
和 j ,其中 i
在 j
的左侧(i<j),并且 i
对应的元素不大于 j
对应的元素(nums[i]≤nums[j]
),则:
- 当滑动窗口向右移动时,只要
i
还在窗口中,那么 j
一定也还在窗口中。这是由于 i
在 j
的左侧所保证的。
- 因此,由于
nums[j]
的存在,nums[i]
一定不会是滑动窗口中的最大值了,我们可以将 nums[i]
永久地移除。
- 因此我们可以使用一个队列存储所有还没有被移除的下标。在队列中,这些下标按照从小到大的顺序被存储,并且它们在数组
nums
中对应的值是严格单调递减的。
- 当滑动窗口向右移动时,我们需要把一个新的元素放入队列中。
- 为了保持队列的性质,我们会不断地将新的元素与队尾的元素相比较,如果新元素大于等于队尾元素,那么队尾的元素就可以被永久地移除,我们将其弹出队列。我们需要不断地进行此项操作,直到队列为空或者新的元素小于队尾的元素。
- 由于队列中下标对应的元素是严格单调递减的,因此此时队首下标对应的元素就是滑动窗口中的最大值。
- 窗口向右移动的时候。因此我们还需要不断从队首弹出元素保证队列中的所有元素都是窗口中的,因此当队头元素在窗口的左边的时候,弹出队头。
字符串模式匹配
Trie 树
高效地存储和查找字符串集合的数据结构:
- 存储:从 root 开始,逐个存储字符串,每层存放一个字符,在字符串结束时做结束标记;
- 查找:从 root 开始,从上到下检验节点是否与给定字符串的对应字符匹配;
并查集
优势:
- 将两个集合合并
- 询问两个元素是否在一个集合当中
二叉堆
手动建堆
Priority_Queue
哈希表
哈希操作
在 main 函数开始处,应当首先对 h[N]
数组进行初始化,否则会导致 insert
和 find
函数行为异常:在独立链表法实现的哈希表中,h[]
数组用于存储各个链表的头节点索引,因此在使用之前需要将其初始化为某个不可能的索引值,通常是-1
,表示空链表。如果不进行这一步,h[]
数组的初始值将是未定义的,这可能导致你的find
函数遍历未初始化的链表,从而极大地增加计算时间,特别是当h[k]
偶然被初始化为一个非常大的值时。
如何进行初始化?
在 C++中建议使用 fill(h, h + N, -1);
语句进行初始化。在C++中,fill
函数是一个标准库函数,用于给容器或数组中的所有元素赋相同的值。其原型定义在头文件<algorithm>
中,形式如下:
first
和 last
是迭代器,分别指向要填充范围的起始位置和终点位置(不包括 last
)。
value
是要赋给指定范围内所有元素的值。
fill
函数与memset
的优势
C 中有一个臭名昭著的初始化函数 memset
,为什么不用它?主要考虑以下几点:
- 类型安全:
fill
是类型安全的,可以用于任何数据类型的容器或数组,而memset
仅适用于字符数组或需要按字节操作的场景。memset
在处理非POD(Plain Old Data)类型时可能会导致未定义行为。
- 通用性:
fill
可以用于非连续存储的容器(如std::list
),而memset
只能用于连续存储的内存块(如C风格数组和std::vector
的底层存储)。
- 易用性:
fill
直接作用于容器和数组,不需要考虑数据的字节大小,而memset
需要指定要填充的字节数,使用不当容易出错。
C++11以后更推荐使用fill
自C++11以来,标准库更推荐使用std::fill
和其他标准算法,原因如下:
- 现代C++的编程范式:C++11及后续标准鼓励使用标准库提供的高级抽象,以编写更安全、更简洁和更易于理解的代码。
- 自动类型推导和范围循环:C++11引入的自动类型推导(如
auto
关键字)和范围for
循环与标准库算法(如std::fill
)搭配使用,可以简化代码和提高可读性。
- 泛型编程:
std::fill
等算法支持泛型编程,使代码更加灵活和通用。
核心思想:将字符串看成 P 进制数,P 的经验值是 131 或 13331,取这两个值的冲突概率低
小技巧:取模的数用 2^64,这样直接用 unsigned long long 存储,溢出的结果就是取模的结果
注意,不能将字母映射为 0 。
STL
vector
size () 返回元素个数
empty () 返回是否为空
clear () 清空
front ()/back ()
push_back ()/pop_back ()
begin ()/end ()
[] 支持随机寻址
支持比较运算,按元素的字典序比较
pair<int, int>
first, 第一个元素
second, 第二个元素
支持比较运算,以 first 为第一关键字,以 second 为第二关键字(字典序)
make_pair ()
string,字符串
size ()/length () 返回字符串长度
empty ()
clear ()
substr (起始下标,(子串长度)) 返回子串
c_str () 返回字符串所在字符数组的起始地址
queue, 队列
size ()
empty ()
push () 向队尾插入一个元素
front () 返回队头元素
back () 返回队尾元素
pop () 弹出队头元素
priority_queue, 优先队列,默认是大根堆
size ()
empty ()
push () 插入一个元素
top () 返回堆顶元素
pop () 弹出堆顶元素
定义成小根堆的方式:priority_queue<int, vector<int>, greater<int>> q;
stack, 栈
size ()
empty ()
push () 向栈顶插入一个元素
top () 返回栈顶元素
pop () 弹出栈顶元素
deque, 双端队列
size ()
empty ()
clear ()
front ()/back ()
push_back ()/pop_back ()
push_front ()/pop_front ()
begin ()/end ()
[]
set, map, multiset, multimap,
基于平衡二叉树(红黑树),动态维护有序序列
size ()
empty ()
clear ()
begin ()/end ()
++, -- 返回前驱和后继,时间复杂度 O (logn)
set/multiset
insert () 插入一个数
find () 查找一个数
count () 返回某一个数的个数
erase ()
(1) 输入是一个数 x,删除所有 x O (k + logn)
(2) 输入一个迭代器,删除这个迭代器
lower_bound ()/upper_bound ()
lower_bound (x) 返回大于等于 x 的最小的数的迭代器
upper_bound (x) 返回大于 x 的最小的数的迭代器
map/multimap
insert () 插入的数是一个 pair
erase () 输入的参数是 pair 或者迭代器
find ()
[] 注意 multimap 不支持此操作。 时间复杂度是 O (logn)
lower_bound ()/upper_bound ()
unordered_set, unordered_map, unordered_multiset, unordered_multimap
哈希表
和上面类似,增删改查的时间复杂度是 O (1)
不支持 lower_bound ()/upper_bound (), 迭代器的++,--
bitset, 圧位
bitset<10000> s;
~, &, |, ^
>>, <<
==, !=
[]
count () 返回有多少个 1
any () 判断是否至少有一个 1
none () 判断是否全为 0
set () 把所有位置成 1
set (k, v) 将第 k 位变成 v
reset () 把所有位变成 0
flip () 等价于~
flip (k) 把第 k 位取反
![[1-bitset#init-syntax|Init Syntax]]