常用数据结构及其实现(链表、栈、队列、Trie树、哈希表)
数据结构
单链表
通常链表的实现方式
struct Node {
int val;
Node *next;
};
用结构体加指针实现单链表效率不高,因为每次添加一个节点,需要new一个 Node()
,这个操作非常慢。
数组模拟单链表
用数组实现单链表,数组e[N]
用来存储值val
,数组 ne[N]
来表示next
指针,这两个数组用下标关联,空节点用-1表示。
链表:
head -> Node(3) -> Node(5) -> Node(7) -> Node(9) -> Nullptr
e[N]: e[0] = 3, e[1] = 5, e[2] = 7, e[3] = 9;
ne[N]: ne[0] = 1, ne[1] = 2, ne[2] = 3, ne[3] = -1;
解释: 第n个节点的值为 e[n], 下一个节点的值为 e[ne[n]];
代码实现
const int N = 100010;
int e[N], ne[N], head, idx;
// head 表示头结点的下标
// e[i] 表示节点i的值
// ne[i] 表示节点i的next指针是多少
// idx 存储当前已经用到了哪个点,相当于新空间的地址
void init() // 链表的初始化
{
idx = 0, head = -1;
}
void add_to_head(int x) // 将x插入到头节点
{ // 将x存入idx指向的空位,之前头节点的下标存入ne[idx],头节点下标等于idx,idx指向下一个空位
e[idx] = x, ne[idx] = head, head = idx ++;
}
void add(int k, int x) // 将x插入到第k个节点之后
{
e[idx] = x, ne[idx] = ne[k], ne[k] = idx ++;
}
void remove(int k) // 删除k点后面的点
{
ne[k] = ne[ne[k]];
}
双链表
双链表相比于单链表存储两个指针,一个指向前一个节点,一个指向后一个节点。用处:优化某些问题
数组模拟双链表
代码实现
const int N = 100010;
int e[N], l[N], r[N], idx;
// e存储节点值,l[N]用来存储左节点的下标,r[N]用来存储右节点的下标,idx上同
// 避免额外空间记录头节点和尾节点,用idx0代表左端点,idx1代表右端点
void init() // 链表的初始化
{
l[1] = 0, r[0] = 1;
idx = 2; // 新空间从2开始
}
void add_right(int k, int x) // 在第k个点右边插入x
{
e[idx] = x; // 先把值存下来
l[idx] = k, r[idx] = r[k]; // 将x连接左指针连接k,右指针连接k的下一个点
l[r[k]] = idx; // 先将x的下一个节点左指针指向x
r[k] = idx ++; // 再将k的右指针指向idx
// 不能写反,因为要通过k的右指针找到插入前k的右节点
}
void add_left(int k, int x) // 在第k个点的左边插入x
{
add_right(l[k], x); // 相当于在k - 1个点的右边插入一个点
}
void remove(int k) // 删除第k个节点
{
r[l[k]] = r[k];
l[r[k]] = l[k];
}
栈
先进后出
数组模拟栈
| |
| 3 | stk[N]: stk[0] = 7, stk[1] = 5, stk[2] = 3;
| 5 | tt此时指向2
| 7 | 插入时 stk[++ tt] = x, 弹出时直接 tt --
—————
代码实现
const int N = 100010;
int stk[N], tt = 0;
// tt是栈顶的指针,后续插入直接覆盖原数组即可
void push(int x)
{
stk[++ tt] = x;
}
void pop()
{
tt --;
}
int top()
{
return stk[tt];
}
bool empty()
{
if (tt) return false;
else return true;
}
队列
先进先出
数组模拟队列
4 2 6 7 q[N]: q[0] = 4, q[1] = 2, q[2] = 6, q[3] = 7;
hh tt hh = 0, tt = 3
q[hh]是头节点, q[tt]是尾节点
代码实现
const int N = 100010;
int q[N], hh, tt = -1;
void push(int x)
{
q[++ tt] = x;
}
void pop()
{
hh ++;
}
bool empty()
{
if (hh <= tt) return false;
else return true;
}
单调栈
单调栈顾名思义就是在入栈时遵循单调原则,可以求出一个元素向左(或向右)所能扩展到的最大长度,并不是说在这一段区间内是单调的,而是保证在该区间内该元素一定是最大或最小。
场景:输出数列中第i
个数左边最小或者最大的数
const int N = 100010;
vector<int> minStack(vector<int>& a) // 求a数组中每位左边最小的数
{
int stk[N], tt = 0;
vector<int> ans;
for (int i = 0; i < a.size(); i ++)
{
while (tt && stk[tt] >= a[i]) tt --;
if (!tt) ans[i] = -1; // 如果栈空,则输出-1
else ans[i] = stk[tt];// 否则,栈顶就是最小值
stk[ ++ tt] = a[i];
}
return ans;
}
单调队列
滑动窗口求最大值或者最小值
const int N = 100010;
vector<int> minQueue(vector<int>& a, int window)
{
int hh = 0, tt = -1; // 数组模拟队列
int q[N]; // 存的是在原数组中的下标
vector<int> ans;
for (int i = 0; i < a.size(); i ++)
{ // 队列不为空并且队头已经划出窗口
if (hh <= tt && q[hh] < i - window + 1) hh ++;
while (hh <= tt && a[q[tt]] >= a[i]) tt --;
q[++ tt] = i;
ans[i] = a[q[hh]];
}
return ans;
}
KMP
KMP算法应用于字符串匹配,并且可以快速在源字符串中找到所有与之匹配的模式串。
// 求ne数组
char p[N];
int ne[N];
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;
}
Trie树
是用来快速存储和查找字符串集合的一种数据结构。
数组模拟Trie树
const int N = 100010;
// son数组用来存储字符串集合。cnt表示当前节点是否存在字符串
int son[N][26], cnt[N], idx;
char str[N];
void insert(char *str)
{
int p = 0; // 从头节点开始遍历
for (int i = 0; str[i]; i ++)
{
int u = str[i] - 'a'; // 取出每个字符对应的数值。 0~25
if (!son[p][u]) son[p][u] = ++ idx; // 如果当前没有节点,则创建一个节点
p = son[p][u]; // 走到下一层
}
cnt[p] ++; // 结束时的标记,也是记录以此节点结束的字符串个数
}
int query(char *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];
}
return cnt[p]; // 返回字符串出现的次数
}
Trie树是从0层开始,但是下一层不一定是1层,这个是由son[p][u]里的编号idx的值决定
并查集
支持两个重要操作:
- 将两个集合合并
- 询问两个元素是否在一个集合当中
基本原理:每个集合用一颗树来表示。树根的编号就是整个集合的编号。每个节点存储它的父节点。p[x]表示x的父节点
const int N=100010;
int p[N]; // 定义多个集合
int find(int x)
{
if(p[x] != x) p[x] = find(p[x]);
/*
经上述可以发现,每个集合中只有祖宗节点的p[x]值等于他自己,即:
p[x]=x;
*/
return p[x];
// 找到了便返回祖宗节点的值
}
堆
本质维护一个数据集合,最大或最小的数据$O(1)$获得
可以实现如下操作:
- 插入一个数值 heap[size ++] = x;
- 求集合当中的最小值 heap[1];
- 删除最小值 heap[1] = heap[size --]; down(1);
- 删除任意一个元素 heap[k] = heap[size --]; down(k); up(k);
- 修改任意一个元素 heap[k] = x; down(k); up(k);
后两个操作STL无法直接实现。
有两种建堆的方法:
- 一种是从一个空树开始,每次输入一个数,就按照插入操作插入这个树,时间复杂度
nlogn
(一共n个元素,每个元素logn)。 - 还有一种建堆方法是已经把数据都输入到了数组a[N],怎么根据这个数组直接建堆。就是从n/2开始down(),因为n/2是倒数第二层,如果是倒数第一层其实每个节点自然成堆了,所以从倒数第二层开始,是最简单的一个二层树结构,down完形成一个堆。从右下角往左边开始建堆,这样从下往上之后,轮到每个节点他下面的子树肯定已经成堆了,满足down的条件。
const int N = 100010;
int h[N], ph[N], hp[N], cnt;
// h[N]是存储用数组模拟的堆:下标是大小顺序,值是实际的数;
// ph[N]是存储第K个插入元素存在的下标:下标是插入顺序,值是当前存在堆的下标
// hp[N]是存储当前堆的数是第几个插入的下标:下标是当前的堆大小顺序,值是插入的顺序
void heap_swap(int a, int b)
{
swap(ph[hp[a]], ph[hp[b]]);
swap(hp[a], hp[b]);
swap(h[a], h[b]);
}
void down(int u)
{
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)
{
while (u / 2 && h[u] < h[u / 2])
{
heap_swap(u, u / 2);
u >>= 1;
}
}
哈希表
哈希函数可以把 -109 ~ 109的数映射到 0 ~ 105 之间,解决哈希冲突的两种方法开放寻址法和拉链法。
拉链法就是将哈希冲突的数依然放在对应的槽上,可以放置在已有的数的前面或后边。一般只用支持插入和查找操作,如果要实现删除操作可用额外的布尔变量标记该数是否删除。
拉链法模拟散列表
const int N = 100003;
int h[N], e[N], ne[N], idx; // 用数组模拟单链表拉链
memset(h, -1, sizeof h); // 单链表初始化为-1
void insert(int x)
{
int k = (x % N + N) % N; // 为什么要 + N 再 % N,为的是让它的余数变成正数,因为负数 % N的余数仍为负数
e[idx] = x;
ne[idx] = h[k];
h[k] = idx ++; // h[k]是散列后对应的槽,存的是该槽上链表第一个点的下标
}
bool find(int x)
{
int k = (x % N + N) % N;
for (int i = h[k]; i != -1; i = ne[i])
if (e[i] == x)
return true;
return false;
}
开放寻址法模拟散列表
只用开一个数组,不用模拟单链表。但是数组的范围一般开到题目的2 ~ 3倍。
const int N = 200003, null = 0x3f3f3f3f;
int h[N];
memset(h, 0x3f, sizeof h);
int find(int x) // find函数找到x所在的位置
{
int k = (x % N + N) % N;
while (h[k] != null && h[k] != x)
{
k ++;
if (k == N) k = 0;
}
return k;
}
// 插入的时候
int k = find(x); // 先找到对应的槽
if (insert) h[k] = x;
字符串哈希
把字符串变成一个p进制数字(哈希值),实现不同的字符串映射到不同的数字。
typedef unsigned long long ULL;
const int N = 100010, P = 131;
int n; // 字符数组的长度
char str[N];
ULL h[N], p[N]; // p数组用来存储P的多少次方
p[0] = 1;
for (int i = 1; i <= n; i ++)
{
p[i] = p[i - 1] * P; // 预处理出来P的多少次方
h[i] = h[i - 1] * P + str[i];
}
ULL get(int l, int r) // 获取区间的字符串哈希值
{
return h[r] - h[l - 1] * p[r - l + 1];
}