备战蓝桥杯----C/C++组 (一)数据结构与STL讲解(上):顺序表、链表、栈与队列——从手写到调用,一文搞懂四种线性结构

个人主页:
wengqidaifeng

✨ 永远在路上,永远向前走

个人专栏:
数据结构
C语言
嵌入式小白启动!
重要OJ算法题详解

文章目录


前言:为什么我们要从"容器"开始?

在正式踏上蓝桥杯C++组的备赛之路前,我们首先要面对一个基础却至关重要的问题:如何高效地组织和操作数据?

无论你是在解决一道模拟题、搜索题,还是动态规划题,程序的核心往往都离不开对数据的存储、访问与修改。选择合适的数据结构,不仅能让代码更简洁,更可能直接决定算法能否在时间与空间限制下通过。可以说,数据结构是算法的基石,而STL(标准模板库)则是C++选手手中最锋利的武器

数据结构:从何而来?

"数据结构"这个词,听起来有些理论化,但它其实源于一个非常朴素的现实需求------如何用计算机更聪明地管理数据

早期的计算机科学家在处理问题时发现,仅仅依靠基本变量和数组,难以高效应对复杂场景。比如:

  • 如何动态地插入、删除大量数据?
  • 如何保证数据"先来先服务"或"后来先服务"?
  • 如何让数据在内存中既节省空间,又便于访问?

于是,一系列经典的数据结构应运而生:顺序表、链表、栈、队列......它们并非凭空创造,而是针对不同操作场景下"数据组织方式"的抽象与优化。时至今日,它们依然是算法竞赛中最基础、最核心的组成部分。

为什么在C++中更要重视它们?

在C++中,STL为我们提供了这些数据结构的成熟实现:vector(顺序表)、list(链表)、stack(栈)、queue(队列)等。这意味着我们不需要从零手写底层逻辑,而是可以站在巨人的肩膀上,专注于算法本身的实现。

但"会用"不等于"理解"。在蓝桥杯的赛场上,只有真正理解每种结构的特点------比如随机访问与插入删除的权衡、连续内存与链式存储的差异------才能在解题时做出正确的选择,避免踩进"超时"或"内存超限"的坑。

本篇内容预告

作为"数据结构与STL"系列的第一篇,我将从最基础的 线性结构 讲起,依次介绍:

  • 顺序表(vector:连续存储,随机访问快,但中间插入删除慢
  • 链表(list:离散存储,插入删除快,但不支持随机访问
  • 栈(stack:后进先出,解决回溯、表达式求值等经典问题
  • 队列(queue:先进先出,广度优先搜索(BFS)的基石

对于每一种结构,我都会从 底层原理、适用场景、常用操作、注意事项 四个方面展开,并结合蓝桥杯常见的题目类型,帮助你在实战中融会贯通。

------ 工欲善其事,必先利其器。理解数据结构,便是利其器的开始。


一、什么是数据结构?

官方定义:数据结构是一种数据组织、管理和存储的格式。

通俗理解:数据结构就是"数据的组织形式"------研究如何把数据存储在计算机中,以便高效地使用它们。

为什么需要数据结构?

随着计算机处理的数据量越来越大、类型越来越多、关系越来越复杂,我们必须系统地研究数据的特性、数据之间的关系,以及如何有效地组织和管理数据。数据结构这门学科正是为此而生。


二、数据结构的三要素

1. 逻辑结构

逻辑结构描述数据元素之间的逻辑关系,不关心数据在内存中如何存储

常见的有四种:

逻辑结构 关系特点 例子
集合 元素间无特殊关系 班级学生名单
线性结构 一对一 排队、数组、链表
树形结构 一对多 文件夹系统、家族谱
图结构 多对多 社交网络、地图导航




2. 存储结构(物理结构)

数据在计算机中实际存储的方式:

  • 顺序存储:逻辑相邻的元素在物理上也相邻(如数组)
  • 链式存储:通过指针建立元素间的联系(如链表)

3. 数据的运算

有了结构和存储方式后,我们需要对数据进行的操作:

  • 创建(Create)
  • 增(Insert)
  • 删(Delete)
  • 查(Search)
  • 改(Update)
  • 排序(Sort)
  • 输出(Output)

简单记忆:创 + 增删查改 + 其他


三、算法与复杂度

什么是算法?

算法是解决问题的清晰指令序列,将输入转化为输出。

简单理解:你在C++阶段写的每一个程序,都可以称为一个算法。不要把它想得太高深。

如何衡量算法好坏?

两个核心指标:

指标 含义 关注点
时间复杂度 算法执行时间随问题规模的增长趋势 运行快慢
空间复杂度 算法占用内存随问题规模的增长趋势 内存消耗

大O表示法

大O表示法用于粗略估计算法的时间复杂度------只看影响最大的那一项

推导规则

  1. 只保留最高阶项,去掉低阶项
  2. 去掉最高阶项的常数系数
  3. 若只有常数项,用1代替

示例

  • T ( N ) = N 2 + 2 N + 10 T(N) = N^2 + 2N + 10 T(N)=N2+2N+10 → O ( N 2 ) O(N^2) O(N2)
  • T ( N ) = 1000 T(N) = 1000 T(N)=1000 → O ( 1 ) O(1) O(1)
  • T ( N ) = 2 N T(N) = 2N T(N)=2N → O ( N ) O(N) O(N)

常见复杂度对比


复制代码
O(1) < O(logN) < O(N) < O(NlogN) < O(N²) < O(2^N) < O(N!)

竞赛小贴士 :C++通常1-2秒的时间限制,能承受约 10 7 10^7 107 到 10 8 10^8 108 次运算。


四、STL:站在巨人的肩膀上

什么是STL?

STL(Standard Template Library) 是C++标准库的一部分,包含模板化的通用数据结构和算法。

简单理解:C++已经帮你实现好了很多常用数据结构和算法,直接用就行,避免重复造轮子

常用的STL组件

类别 常用组件 用途
容器 vector 动态数组
stack 栈(后进先出)
queue 队列(先进先出)
map 键值对映射
算法 sort() 排序
find() 查找
reverse() 反转

怎么学STL?

模仿使用,不求甚解。

STL的实现涉及类、模板、容器适配器等高级知识,竞赛中用不到底层原理。现阶段只需:

  1. 知道有什么组件可用
  2. 知道怎么用
  3. 知道用了会有什么效果

数据结构是算法的基石,而STL是C++选手最锋利的武器。

五、顺序表:最熟悉的陌生人

5.1 什么是顺序表?

如果你学过C语言,一定对数组不陌生。顺序表,就是用数组实现的线性表------数据在逻辑上是连续的,在内存中也是连续存放的。

想象一下电影院的座位:一排椅子紧挨着,每个座位都有固定的编号(下标)。你可以直接找到第3排第5个座位(随机访问),但如果要在中间插入一个人,后面所有人都得往后挪一个位置(插入慢)。

这就是顺序表的核心特征:

  • 优点:支持随机访问,按下标取元素的时间复杂度是O(1)
  • 缺点:在中间插入或删除元素时,需要移动大量数据,时间复杂度O(n)

5.2 手写一个静态顺序表

虽然比赛中我们通常直接用STL,但理解底层实现能帮你更深刻地掌握它的特性。

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

const int N = 1e6 + 10;  // 预先分配足够大的空间
int a[N], n;  // n表示当前元素个数(从下标1开始存)

// 尾插:在末尾添加元素
void push_back(int x) {
    a[++n] = x;  // O(1)
}

// 头插:在开头插入元素(需要整体右移)
void push_front(int x) {
    for(int i = n; i >= 1; i--) {
        a[i + 1] = a[i];
    }
    a[1] = x;
    n++;  // O(n)
}

// 按位查找
int at(int p) {
    return a[p];  // O(1)
}

// 按值查找
int find(int x) {
    for(int i = 1; i <= n; i++) {
        if(a[i] == x) return i;
    }
    return 0;  // O(n)
}

// 任意位置插入
void insert(int p, int x) {
    for(int i = n; i >= p; i--) {
        a[i + 1] = a[i];
    }
    a[p] = x;
    n++;  // O(n)
}

// 任意位置删除
void erase(int p) {
    for(int i = p + 1; i <= n; i++) {
        a[i - 1] = a[i];
    }
    n--;  // O(n)
}

// 清空
void clear() {
    n = 0;  // O(1)
}

观察上面的代码,你会发现:凡是在中间或头部操作的,都需要移动元素;而只在尾部操作或按下标访问的,都非常快。

5.3 STL中的顺序表:vector

vector 是C++标准库提供的动态顺序表,它会根据需求自动扩容,我们只需要关心如何使用。

常用操作一览
操作 代码 时间复杂度 说明
创建空vector vector<int> v; O(1)
创建指定大小 vector<int> v(n); O(n) 默认初始化为0
创建并初始化 vector<int> v = {1,2,3}; O(n)
尾部添加 v.push_back(x); O(1) 均摊
尾部删除 v.pop_back(); O(1)
按下标访问 v[i] O(1) 不检查越界
返回首元素 v.front() O(1)
返回尾元素 v.back() O(1)
获取元素个数 v.size() O(1)
判断是否为空 v.empty() O(1)
改变大小 v.resize(n); O(n)
清空 v.clear(); O(n)
代码示例
cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int main() {
    // 多种创建方式
    vector<int> v1;                           // 空vector
    vector<int> v2(5);                        // 5个元素,默认0
    vector<int> v3(5, 10);                    // 5个元素,都是10
    vector<int> v4 = {1, 2, 3, 4, 5};         // 列表初始化
    
    // 尾部操作
    v1.push_back(1);
    v1.push_back(2);
    v1.push_back(3);   // v1 = {1, 2, 3}
    
    // 访问元素
    cout << v1.front() << " " << v1.back() << endl;  // 输出: 1 3
    cout << v1[1] << endl;  // 输出: 2
    
    // 遍历(三种方式)
    // 方式一:下标
    for(int i = 0; i < v1.size(); i++) cout << v1[i] << " ";
    // 方式二:迭代器
    for(auto it = v1.begin(); it != v1.end(); it++) cout << *it << " ";
    // 方式三:范围for(最简洁)
    for(auto x : v1) cout << x << " ";
    
    // 弹出尾部元素
    v1.pop_back();  // v1 = {1, 2}
    
    // 改变大小
    v1.resize(5);   // v1 = {1, 2, 0, 0, 0}
    v1.resize(2);   // v1 = {1, 2}
    
    // 清空
    v1.clear();     // v1为空
    
    return 0;
}

5.4 算法题实战:询问学号

题目:有n个同学按顺序进入教室,老师想知道第i个进入教室的同学的学号。

分析:这是一道典型的顺序表按位查找问题。由于我们只关心"第几个进入",不需要中间插入删除,用数组或vector存储后直接按下标访问即可。

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

int main() {
    int n, m;
    cin >> n >> m;
    
    vector<int> a(n + 1);  // 从下标1开始存,方便理解
    for(int i = 1; i <= n; i++) cin >> a[i];
    
    while(m--) {
        int x;
        cin >> x;
        cout << a[x] << endl;  // O(1)随机访问
    }
    
    return 0;
}

5.5 使用场景小结

什么时候用vector?

  • 需要频繁随机访问元素
  • 主要在尾部进行插入/删除
  • 元素数量不确定但总体可控

什么时候不用vector?

  • 需要在头部或中间频繁插入/删除(考虑链表)
  • 需要频繁查找某个值(考虑哈希表)

六、链表:灵活的动态结构

6.1 什么是链表?

如果说顺序表是电影院的一排座位,那链表就是一群人手拉手排成一列。每个人只知道自己后面是谁(单链表),或者既知道前面也知道后面(双向链表)。

链表的每个元素叫结点,包含两部分:

  • 数据域:存放实际数据
  • 指针域:存放指向下一个(或上一个)结点的地址

核心特征:

  • 优点:插入和删除非常快,只需修改指针,时间复杂度O(1)
  • 缺点 :不支持随机访问,查找某个元素需要从头遍历,时间复杂度O(n)

6.2 手写静态链表

在算法竞赛中,我们通常用数组模拟链表 ,因为用new动态申请结点非常慢,容易超时。

单链表的实现
cpp 复制代码
#include <iostream>
using namespace std;

const int N = 1e5 + 10;

// 静态链表:e[i]存数据,ne[i]存下一个结点的位置
int e[N], ne[N], h, id;  // h是头指针,id是当前分配的位置
int mp[N];  // 标记数组,记录每个值对应的下标(可选优化)

// 初始化(通常用0作为哨兵位)
void init() {
    h = 0;      // 哨兵位,不存数据
    ne[0] = 0;  // 0表示空指针
    id = 0;
}

// 头插:在链表头部插入元素
void push_front(int x) {
    id++;
    e[id] = x;
    mp[x] = id;
    ne[id] = ne[h];  // 新结点指向原头结点
    ne[h] = id;      // 哨兵位指向新结点
    // 时间复杂度 O(1)
}

// 遍历链表
void print() {
    for(int i = ne[h]; i; i = ne[i]) {
        cout << e[i] << " ";
    }
    cout << endl;
}

// 按值查找(遍历)
int find_by_value(int x) {
    for(int i = ne[h]; i; i = ne[i]) {
        if(e[i] == x) return i;
    }
    return 0;
}

// 按值查找(标记数组优化,O(1))
int find_fast(int x) {
    return mp[x];
}

// 在结点p之后插入新元素
void insert_after(int p, int x) {
    id++;
    e[id] = x;
    mp[x] = id;
    ne[id] = ne[p];
    ne[p] = id;  // O(1)
}

// 删除结点p之后的元素
void erase_after(int p) {
    if(ne[p]) {
        mp[e[ne[p]]] = 0;
        ne[p] = ne[ne[p]];  // O(1)
    }
}
双向链表的实现

双向链表比单链表多了一个前驱指针pre[],操作稍微复杂但思路一致:

cpp 复制代码
const int N = 1e5 + 10;
int e[N], pre[N], ne[N], id, h;
int mp[N];

// 头插
void push_front(int x) {
    id++;
    e[id] = x;
    mp[x] = id;
    
    pre[id] = h;
    ne[id] = ne[h];
    pre[ne[h]] = id;
    ne[h] = id;
}

// 在结点p之后插入
void insert_back(int p, int x) {
    id++;
    e[id] = x;
    mp[x] = id;
    
    pre[id] = p;
    ne[id] = ne[p];
    pre[ne[p]] = id;
    ne[p] = id;
}

// 在结点p之前插入
void insert_front(int p, int x) {
    id++;
    e[id] = x;
    mp[x] = id;
    
    pre[id] = pre[p];
    ne[id] = p;
    ne[pre[p]] = id;
    pre[p] = id;
}

// 删除结点p
void erase(int p) {
    mp[e[p]] = 0;
    ne[pre[p]] = ne[p];
    pre[ne[p]] = pre[p];
}

6.3 STL中的链表:list

STL的list底层是双向循环链表 ,但在竞赛中很少使用,因为动态申请内存(new/delete)效率较低。不过了解它的接口还是很有必要的。

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

int main() {
    list<int> lt;
    
    // 尾部插入
    lt.push_back(1);
    lt.push_back(2);   // lt = {1, 2}
    
    // 头部插入
    lt.push_front(0);  // lt = {0, 1, 2}
    
    // 遍历
    for(auto x : lt) cout << x << " ";
    
    // 删除
    lt.pop_front();    // lt = {1, 2}
    lt.pop_back();     // lt = {1}
    
    return 0;
}

6.4 算法题实战:约瑟夫问题

题目:n个人围成一圈,从第一个人开始报数,数到m的人出列,求依次出列的顺序。

分析 :这是一个经典的循环链表模拟问题。用数组模拟循环链表,每次删除当前结点即可。

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

const int N = 110;
int ne[N];  // 记录下一个人的编号

int main() {
    int n, m;
    cin >> n >> m;
    
    // 构建循环链表
    for(int i = 1; i < n; i++) ne[i] = i + 1;
    ne[n] = 1;  // 首尾相连
    
    int cur = n;  // 从n开始,这样第一个移动就会到1
    for(int i = 1; i <= n; i++) {
        // 移动m-1步,找到要删除的人的前一个
        for(int j = 1; j < m; j++) {
            cur = ne[cur];
        }
        // 输出并删除
        cout << ne[cur] << " ";
        ne[cur] = ne[ne[cur]];
    }
    
    return 0;
}

6.5 顺序表 vs 链表:如何选择?

场景 推荐 原因
需要随机访问 顺序表 O(1)访问,链表需要O(n)遍历
主要在尾部操作 顺序表 push_back是O(1)
频繁在头部/中间插入删除 链表 O(1)修改指针
需要快速查找某个值 顺序表+哈希 链表查找是O(n)
内存要求高 顺序表 链表需要额外指针空间

七、栈:后进先出的神奇结构

7.1 什么是栈?

栈是一种只能在某一端进行插入和删除的线性表。就像一摞盘子,你只能从顶部取盘子或放盘子。

  • 栈顶:允许操作的一端
  • 栈底:不允许操作的一端
  • 特性:后进先出(LIFO, Last In First Out)

生活中有很多栈的例子:浏览器的后退功能、编辑器的撤销操作、函数调用栈...

7.2 手写栈

栈的实现非常简单,用数组和一个指针即可:

cpp 复制代码
const int N = 1e6 + 10;
int stk[N], top;  // top指向栈顶元素的下标

// 进栈
void push(int x) {
    stk[++top] = x;  // O(1)
}

// 出栈
void pop() {
    top--;  // O(1)
}

// 获取栈顶元素
int top_element() {
    return stk[top];  // O(1)
}

// 判空
bool empty() {
    return top == 0;
}

// 元素个数
int size() {
    return top;
}

7.3 STL中的栈:stack

stack是容器适配器,底层默认用deque实现,使用非常直观:

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

int main() {
    stack<int> st;
    
    // 入栈
    st.push(1);
    st.push(2);
    st.push(3);
    
    // 访问栈顶
    cout << st.top() << endl;  // 输出: 3
    
    // 出栈
    st.pop();  // 移除栈顶3
    
    // 遍历(需要边pop边输出)
    while(!st.empty()) {
        cout << st.top() << " ";
        st.pop();
    }  // 输出: 2 1
    
    return 0;
}

7.4 算法题实战:有效的括号

题目 :给定一个只包含(, ), {, }, [, ]的字符串,判断括号是否有效匹配。

分析:遇到左括号就压栈,遇到右括号就检查栈顶是否匹配。这是栈最经典的场景之一。

cpp 复制代码
class Solution {
public:
    bool isValid(string s) {
        stack<char> st;
        
        for(char ch : s) {
            if(ch == '(' || ch == '[' || ch == '{') {
                st.push(ch);  // 左括号入栈
            } else {
                // 右括号但栈为空,无法匹配
                if(st.empty()) return false;
                
                char top = st.top();
                st.pop();
                
                // 检查是否匹配
                if(ch == ')' && top != '(') return false;
                if(ch == ']' && top != '[') return false;
                if(ch == '}' && top != '{') return false;
            }
        }
        
        return st.empty();  // 全部匹配完栈应该为空
    }
};

7.5 栈的其他经典应用

  • 表达式求值:中缀表达式转后缀表达式
  • 括号匹配(已演示)
  • 函数调用栈:递归的底层实现
  • 单调栈:找下一个更大/更小的元素
  • 浏览器的后退功能

八、队列:先进先出的排队系统

8.1 什么是队列?

队列是只能在一端插入、另一端删除的线性表。就像排队买票,先来的人先买到票。

  • 队头:删除元素的一端(front)
  • 队尾:插入元素的一端(back)
  • 特性:先进先出(FIFO, First In First Out)

生活中的队列:打印机任务队列、银行叫号系统、消息队列...

8.2 手写队列

队列通常用两个指针实现:h指向队头前一个位置,t指向队尾位置。

cpp 复制代码
const int N = 1e6 + 10;
int q[N], h, t;  // h指向队头前一个位置,t指向队尾

// 入队
void push(int x) {
    q[++t] = x;  // O(1)
}

// 出队
void pop() {
    h++;  // O(1)
}

// 获取队头元素
int front() {
    return q[h + 1];  // O(1)
}

// 获取队尾元素
int back() {
    return q[t];  // O(1)
}

// 判空
bool empty() {
    return h == t;
}

// 元素个数
int size() {
    return t - h;
}

8.3 STL中的队列:queue

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

int main() {
    queue<int> q;
    
    // 入队
    q.push(1);
    q.push(2);
    q.push(3);
    
    // 访问队头/队尾
    cout << q.front() << endl;  // 输出: 1
    cout << q.back() << endl;   // 输出: 3
    
    // 出队
    q.pop();  // 移除队头1
    
    // 遍历(需要边pop边输出)
    while(!q.empty()) {
        cout << q.front() << " ";
        q.pop();
    }  // 输出: 2 3
    
    return 0;
}

8.4 算法题实战:机器翻译

题目:内存有M个单元,每次查找单词若内存中没有,则从外存查找并放入内存。内存满时移除最早进入的单词。求查词典次数。

分析 :这是一个经典的队列模拟问题。用队列记录内存中的单词顺序,用布尔数组标记单词是否在内存中。

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

const int N = 1010;
queue<int> q;
bool in_memory[N];  // 标记单词是否在内存中

int main() {
    int m, n;
    cin >> m >> n;
    
    int cnt = 0;  // 查词典次数
    for(int i = 1; i <= n; i++) {
        int x;
        cin >> x;
        
        if(in_memory[x]) continue;  // 内存中有,跳过
        
        cnt++;  // 需要查词典
        q.push(x);
        in_memory[x] = true;
        
        // 内存满了,移除最早进入的单词
        if(q.size() > m) {
            in_memory[q.front()] = false;
            q.pop();
        }
    }
    
    cout << cnt << endl;
    return 0;
}

8.5 队列的经典应用

  • 广度优先搜索(BFS):图的层序遍历
  • 消息队列:生产者消费者模式
  • 滑动窗口:维护窗口内的数据
  • CPU调度:时间片轮转

九、栈和队列的总结对比

特性 栈 (Stack) 队列 (Queue)
操作端 一端 两端(一端进,一端出)
特性 后进先出 (LIFO) 先进先出 (FIFO)
插入 push (栈顶) push (队尾)
删除 pop (栈顶) pop (队头)
访问 top (栈顶) front/back
典型应用 括号匹配、表达式求值、递归 BFS、消息队列、缓冲

十、写在最后:如何选择数据结构?

在算法竞赛中,选择合适的数据结构往往比写出完美算法更重要。这里给你三个建议:

1. 分析操作需求

  • 需要随机访问?→ 顺序表(vector)
  • 需要频繁插入/删除?→ 链表(但竞赛中少用)
  • 需要后进先出?→
  • 需要先进先出?→ 队列

2. 关注时间复杂度

  • 读题时预估数据范围:n ≤ 10^5 时 O(n²) 不可行
  • 选择容器前思考:每次操作的时间复杂度是否可接受

3. 优先使用STL

除非特别要求手写,否则能用STL就用STLvectorstackqueue都是经过高度优化的,比自己手写更快更安全。


下一篇预告:我们将深入探讨树形结构------二叉树、堆与优先队列,这些将是解决更复杂问题的利器。

理解数据结构的本质,不是背诵它们的接口,而是明白"为什么这个结构适合这个问题"。当你真正理解这一点,你就能在赛场上游刃有余。

练习建议

  1. 用vector完成洛谷P3156【深基15.例1】询问学号
  2. 用栈完成洛谷P1739 表达式括号匹配
  3. 用队列完成洛谷P1540 机器翻译
  4. 尝试手写静态链表解决洛谷P1160 队列安排

祝你在蓝桥杯的备赛路上越走越远!

相关推荐
inputA2 小时前
C语言可变参数(va_list、va_start、va_end、va_arg)
c语言·笔记
01二进制代码漫游日记2 小时前
动态顺序表的实现(修改)
数据结构·算法
酉鬼女又兒2 小时前
零基础入门前端JavaScript 基础语法详解(可用于备赛蓝桥杯Web应用开发)
开发语言·前端·javascript·chrome·蓝桥杯
计算机安禾2 小时前
【C语言程序设计】第38篇:链表数据结构(二):链表的插入与删除操作
c语言·开发语言·数据结构·c++·算法·链表
jing-ya2 小时前
day 57 图论part9
java·开发语言·数据结构·算法·图论
cjforever142 小时前
数据结构整理-二叉树
数据结构
专注API从业者2 小时前
淘宝商品详情 API 的 Webhook 回调机制设计与实现:实现数据主动推送
大数据·前端·数据结构·数据库
cpp_25012 小时前
P1203 [IOI 1993 / USACO1.1] 坏掉的项链 Broken Necklace
数据结构·c++·算法·线性dp
qyzm3 小时前
天梯赛练习题
数据结构·python·算法·贪心算法