第二章 数据结构

ios::sync_with_stdio(false)可以优化运算效率。当测试样例很大时,推荐使用 scanf 和 printf 进行输入输出比较快速。

链表

单向链表

使用数组(顺序存储)实现该逻辑结构的方式称为邻接表,这种静态链表结构适用于存储图和树等数据结构。以下是数组模拟单链表时常用的函数实现:

链表本质上是由多个节点通过单向指针依次连接而成。其中,head指针始终指向头节点,便于链表遍历操作;idx变量则用于动态分配新节点,实现链表结构的扩展功能。

cpp 复制代码
#include <iostream>
using namespace std;

const int N = 10010;

// 链表节点结构
// head - 头结点下标
// e[i] - 节点i的值
// ne[i] - 节点i的next指针
// idx - 当前可用节点索引
int head, e[N], ne[N], idx;

// 初始化链表
void init()
{
    head = -1;
    idx = 0;
}

// 在链表头部插入节点x
void add_to_head(int x)
{
    e[idx] = x;
    ne[idx] = head;
    head = idx;
    idx++;
}

// 在节点k后插入节点x
void add(int k, int x)
{
    e[idx] = x;
    ne[idx] = ne[k];
    ne[k] = idx;
    idx++;
}

// 删除节点k的后继节点
void remove(int k)
{
    ne[k] = ne[ne[k]];
}
 

双向链表

该数据结构通常用于优化某些特定问题。数组模拟双链表的算法结构如下:

cpp 复制代码
#include <iostream>
using namespace std;

const int N = 10010;

// e[i] 表示i节点的值
// l[i] 表示i节点的左边指针指向值
// r[i] 表示i节点的右边指针指向值
// idx表示当前指向节点,通常用于扩充节点结构
// 默认0和1分别表示head和tail,故不再单独设置
int e[N], l[N], r[N], idx;

// 初始化
void init()
{
    // 0表示左端点,1表示右端点
    r[0] = 1, l[1] = 0;
    idx = 2;
}

// 在下标是k的点的右边插入x
void add(int k, int x)
{
    e[idx] = x;
    r[idx] = r[k];
    l[idx] = k;
    l[r[idx]] = idx;
    r[k] = idx;
}

// 删除第k个点
void remove(int k)
{
    r[l[k]] = r[k];
    l[r[k]] = l[k];
}

栈是后进先出的数据结构,以下提供代码用数组实现栈:

cpp 复制代码
#include <iostream>
using namespace std;

const int N = 10010;

int stk[N], tt;

// 插入
stk[++ tt] = x;

// 弹出
tt--;

// 判断栈是否为空
if(tt > 0) not empty
else empty

// 栈顶
stk[tt];

单调栈

该算法是对朴素暴力解法的一种优化,其核心思路在于:在暴力解法的基础上,通过分析每轮是否可以剔除部分元素,以及元素间是否存在单调关系来提升效率。以经典问题"寻找每个数左侧最近的比其大/小的数"为例,优化解法采用单调栈结构:依次将数组元素入栈,并在每轮操作后确保栈内元素保持单调性。(理解这一操作的关键在于逆向思考:若不维持单调性,就会出现逆序元素。显然,在当前问题中,这些逆序元素可以直接移除且不会影响最终结果。)以下是算法模板:

cpp 复制代码
int tt = 0;
for(int i=1; i<=n; i++)
{
    while(tt && check_out(q[tt],i)) tt--;
    stk[++tt] = i;
}

队列

队列是先进先出的数据结构,用数组实现队列结构模版如下:

cpp 复制代码
#include <iostream>
using namespace std;

const int N = 10010;

// 在队尾插入元素,在队头弹出元素
int q[N], hh, tt = -1;

// 插入
q[++ tt] = x;

// 弹出
hh ++;

// 判断队列是否为空
if(hh <= tt) not empty
else empty

// 取出队头元素
q[hh]
q[tt]

单调队列

单调队列和单调栈的探索思路是一致的,参考上文即可,典型题目如找出滑动窗口的最值,用单调队列来存储滑动窗口内元素,下文提供模版:

cpp 复制代码
const int N = 10010;

int hh = 0, tt = -1;
for(int i=0; i<n; i++)
{
    while(hh <= tt && check_out(q[hh])) hh++;
    while(hh <= tt && check(q[tt], i)) tt--;
    q[++tt] = i;
}

字符串匹配算法

朴素模式匹配算法

这是最原始的暴力解算法,匹配模版逐个比较,一旦不匹配则原数组前进一位,再重头开始模版匹配。代码模版如下:

cpp 复制代码
s[N], p[N];
for(int i=0; i<n; i++)
{
    bool flag = true;
    for(int j=0; j<m; j++)
    {
        if(s[i] != p[j])
        {
            flag = false;
            break;
        }
    }
}        

KMP算法

KMP 算法的核心在于利用 next 数组(部分匹配表)记录模式串自身的重复规律,从而在匹配失败时让主串指针不回溯,模式串指针智能跳转。代码模版如下:

cpp 复制代码
const int N = 1010, M = 1010;
int n, m;
char p[N], s[M];
int ne[N];

int main()
{
    cin >> n >> p+1 >> m >> s+1;
    
    // 求next数组
    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;
    }
    
    
    // KMP匹配
    for(int i=1; i<=m; i++)
    {
        while(j && s[i] != p[j+1]) j = ne[j];
        if(s[i] == p[j+1]) j++;
        if(j==n)
        {
            // 匹配成功
        }
    }
}

Trie

这是用于高效存储和查找字符串集合的数据结构。以下提供该数据结构一些封装函数代码:

cpp 复制代码
#include <iostream>
using namespace std;

const int N = 10010;
int son[N][26], cnt[N], idx;  // son数组存储Trie树结构,cnt记录词频,idx表示节点编号

void insert(char str[]) 
{
    int p = 0;  // 从根节点开始
    for (int i = 0; str[i]; i++) {
        int u = str[i] - 'a';  // 获取字符对应的索引
        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];  // 返回查询结果
}
 

并查集

该数据结构主要用于实现集合合并及查询元素是否同属一个集合的功能,采用并查集实现这两种操作可获得近似O(1)的时间复杂度。其核心原理是将每个集合表示为树形结构,其中树根编号代表集合编号,每个节点存储其父节点信息。使用顺序存储方式时,p[x]表示x的父节点,且规定根节点满足p[x]=x的条件。以下是核心find函数的实现代码:

cpp 复制代码
// 查找x的根节点并进行路径压缩优化
int find(int x) {
    while (p[x] != x) {
        p[x] = find(p[x]);
    }
    return p[x];
}
 

该数据结构实现目的是便于插入一个数、求集合当中的最小值、删除最小值、删除任意一个元素、修改任意一个元素。这里采用顺序存储的物理存储,当前节点索引为x,则他的左儿子为2x,右儿子为2x+1,以下提供堆最核心两个函数down和up代码:

cpp 复制代码
// 下沉操作:将节点u下沉到合适位置以维护堆性质
void down(int u) {
    int min_idx = u; // 记录当前子树最小值的索引
    
    // 检查左子节点是否更小
    if (u * 2 <= size && h[u * 2] < h[min_idx]) {
        min_idx = u * 2;
    }
    
    // 检查右子节点是否更小
    if (u * 2 + 1 <= size && h[u * 2 + 1] < h[min_idx]) {
        min_idx = u * 2 + 1;
    }
    
    // 若最小值不是当前节点,则交换并继续下沉
    if (u != min_idx) {
        swap(h[u], h[min_idx]);
        down(min_idx);
    }
}

// 上浮操作:将节点u上浮到合适位置以维护堆性质
void up(int u) {
    // 不断与父节点比较,若更小则交换位置
    while (u / 2 && h[u / 2] > h[u]) {
        swap(h[u / 2], h[u]);
        u /= 2;
    }
}
 

哈希表

哈希表存储结构有开放寻址法和拉链法两种,它的作用主要是将一个较大空间映射到较小空间,以下提供拉链法的两个核心函数代码实现:

cpp 复制代码
// 使用数组模拟链表实现哈希表
void insert(int x) 
{
    // 计算哈希值,处理负数情况
    int k = (x % N + N) % N;  
    e[idx] = x;
    ne[idx] = h[k];
    h[k] = idx++;
}

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;
}
 

以下提供开放定址法的核心函数代码实现:

c 复制代码
int find(int x)
{
    int k = (x % N + N) % N;  // 计算初始哈希位置
    
    // 线性探测解决冲突
    while(h[k] != x && h[k] != NULL)
    {
        k++;
        if(k == N) k = 0;  // 循环搜索
    }
    return k;
}
 

字符串哈希

散列函数的输入通常是数字,当处理字符串时需要先进行预处理转换。常见的P进制计算方法如下:其中Q通常取2^64,p一般取131或13331(正好对应unsigned long long的模长)。需要注意的是,字母取值不能为0(否则无法区分A和AA)。通过选择这些特定的p和Q值,在实践中能有效避免哈希冲突。

STL使用技巧

c 复制代码
/*

vector, 变长数组,倍增思想(系统为进程动态数组分配空间所需时间与申请空间大小无关,只与申请次数有关)
    size() 返回元素个数
    empty() 返回是否为空
    clear() 清空
    front()/back() 
    push_back()/pop_back()
    begin()/end()
    []
    支持比较运算,按字典序
    
pair<int, int>
    first, 第一个元素
    second, 第二个元素
    支持比较运算,以first为第一关键字,以second为第二个关键字(字典序)

string, 字符串, substr(), c_str()
    size()
    empty()
    clear()

queue, 队列
    size()
    empty()
    push() 向队尾插入一个元素
    front() 返回队头元素
    back() 返回队尾元素
    pop() 弹出队头元素
    

priority_queue, 优先队列, 默认是大根堆
    push() 插入第一个元素
    top() 返回堆顶元素
    pop() 弹出堆顶元素
    priority_queue<int, vector<int>, greater<int>> 小根堆定义

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的最大的迭代器
        upper_bound() 返回大于等于x的最小的迭代器
    map/multimap
        insert() 插入的数是pair
        erase() 输入的参数是pair或迭代器
        find()
        [] o(logn)
        lower_bound()/upper_bound()
    
unordered_set, unordered_map, unordered_multiset, unordered_multimap, 哈希表
    和上面类似,增删改查的时间复杂度是o(1)
    不支持lower_bound()/upper_bound()   迭代器 ++ --

bitset, 压位
    bitset<100> s;
    ~, &, |, ^
    >>, <<
    ==, !=
    []
    count  返回多少个1
    any() 判断是否至少有一个1
    none() 判断是否全为0
    set() 把所有位置设为1
    set(k, v) 将第k位设为v
    reset() 把所有为设为0
    flip() 等价于~
    flip() 把第k位翻转

*/
相关推荐
不知名的忻1 小时前
关键路径(Java)
java·数据结构·算法·关键路径
C雨后彩虹1 小时前
SpringBoot整合Redis String,全套原生API讲解,覆盖80%缓存业务场景
java·数据结构·spring boot·redis·string
孬甭_1 小时前
顺序表详解
c语言·数据结构
qeen872 小时前
【算法笔记】各种常见排序算法详细解析(上)
c语言·数据结构·c++·学习·算法·排序算法
青山师2 小时前
数组与链表深度解析:从内存布局到工业级实践
数据结构·算法·链表·数组·算法与数据结构
AI机器学习算法5 小时前
机器学习基础知识
数据结构·人工智能·python·深度学习·算法·机器学习·ai学习路线
刀法如飞12 小时前
Ontology本体论是什么数据结构?Palantir 技术原理介绍
数据结构·人工智能·ai编程·图论
平行侠13 小时前
024多精度大整数 - 突破硬件精度限制的任意精度运算
数据结构·算法
洛水水14 小时前
【力扣100题】32.将有序数组转换为二叉搜索树
数据结构·算法·leetcode