news 2026/10/10 3:22:53

C++栈与队列深度解析:从底层原理到面试与工程实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++栈与队列深度解析:从底层原理到面试与工程实战

1. 栈和队列到底在解决什么问题:先别急着写代码

先说句实在话:很多人在学C++的时候,栈和队列这两个结构是"背定义"过去的——后进先出、先进先出,背得滚瓜烂熟,但到了真正写项目、刷题、做期末复习的时候,发现还是不知道什么时候该用栈,什么时候该用队列。我自己当年也有这个阶段,直到被一个括号匹配的Bug折磨了一下午,才真正把栈刻进脑子里。

栈和队列是数据结构里最基础、也最容易被小看的两种线性结构。它们不涉及复杂的指针旋转,也没有红黑树那种让人头皮发麻的平衡逻辑,但它们几乎是所有复杂系统里绕不开的积木。函数调用栈、表达式求值、Undo/Redo操作、浏览器后退、任务调度、消息队列、线程池,凡是有"临时存放"和"顺序处理"需求的场景,背后基本都是它们在撑场面。

这篇文章不打算只讲定义。我会从实际使用者的角度,把栈和队列在C++里的用法、手写实现、常见坑、以及面试和竞赛里那些变形玩法都过一遍。无论你是刚学数据结构的在校生,还是在准备复试、准备实习面试,或者工作上需要用到线程池、消息队列这些重型组件,这篇内容都能给你可以直接上手的参考。

1.1 从一摞盘子理解栈:后进先出不是文字游戏

栈的行为,最贴切的类比就是食堂里的一摞盘子。新洗好的盘子总是放在最上面,你要拿的时候也只能从最上面拿。最后放上去的,最先被拿走。

这个"后进先出"(LIFO,Last In First Out)的特性,决定了栈最适合处理"嵌套"和"回溯"类的问题。比如函数调用:main函数调用funcA,funcA调用funcB,那么执行完funcB之后要先回到funcA,再回到main,这个过程天然就是栈。C++的函数栈帧(也就是热词里那个"函数栈帧的创建与销毁")就是依赖这个机制工作的。

再举个实际的业务例子。你在文本框里输入文字,每敲一个字,系统把当前状态压入撤销栈。你执行一次撤销,就是从栈顶弹出最近一次的状态。如果你撤销了三次然后继续输入新内容,栈里那些被撤销掉的状态通常会被清空,这也是为什么有些编辑器不能"重做"到撤销之前某个状态——因为那个分支已经不在栈里了。

用C++写一个最朴素的栈操作,看起来就是这样的:

#include <vector> #include <cassert> template <typename T> class SimpleStack { private: std::vector<T> data; public: void push(const T& value) { data.push_back(value); } void pop() { assert(!data.empty()); data.pop_back(); } T& top() { assert(!data.empty()); return data.back(); } size_t size() const { return data.size(); } bool empty() const { return data.empty(); } };

看到没有,用std::vector实现栈几乎是在"作弊",因为push_back和pop_back天然就是栈操作。但理解这个底层映射很重要:栈不是一个需要专门发明的神奇结构,而是一种访问规则。你规定了只能从一端操作,它就是栈。

1.2 从排队结账理解队列:先进先出保证公平

队列的类比更生活化——收银台排队。先来的人先结账,后来的人排在后面。在几乎所有需要"公平处理"的调度场景里,这个"先进先出"(FIFO,First In First Out)原则都是默认选择。

队列在计算机体系里无处不在。操作系统的进程调度、网络数据包的收发缓冲、打印机任务队列、消息队列中间件,全是队列思想。甚至我们C++里用的std::queue底层默认用的是std::deque,而不是std::vector,原因后面会说。

队列的C++实现如果用std::deque来做,更是简单到不好意思写:

#include <deque> #include <cassert> template <typename T> class SimpleQueue { private: std::deque<T> data; public: void enqueue(const T& value) { data.push_back(value); } void dequeue() { assert(!data.empty()); data.pop_front(); } T& front() { assert(!data.empty()); return data.front(); } size_t size() const { return data.size(); } bool empty() const { return data.empty(); } };

但如果你试图用std::vector实现队列,就要小心了——在头部删除元素是O(n)操作,因为vector要把后面的元素全部前移。而std::deque是分段连续存储,它能在头尾两端都做到O(1)插入删除。这就是为什么标准库选它当默认容器。

1.3 别把栈和队列当成"玩具",它们是系统的骨架

很多人觉得栈和队列都写得出代码,太简单了。但我要说,你去看那些所谓的"全栈项目",前端路由栈、后端消息队列、中间件里的任务调度,骨子里全是这两种结构在来回转。

举几个真实场景:

  • 浏览器前进后退:后退是栈,前进是另一个栈。你访问A、B、C,后退到B之后访问D,那么C就被丢弃,这个逻辑用两个栈非常清晰。
  • 文本编辑器Undo/Redo:同上,要维护两个栈。
  • 函数调用与栈回溯:程序崩溃时打印的调用栈(热词里的backtrace栈回溯),就是基于栈帧信息展开的。
  • 阻塞队列:Java的BlockingQueue、C++线程池里的任务队列,本质就是一个线程安全的队列,加上"满了就等、空了就等"的阻塞语义。
  • 消息队列:从RabbitMQ到RocketMQ,核心模型就是生产者 -> 队列 -> 消费者。重复消费问题、堆积问题,全都是队列在真实世界里要面对的难题。

所以,当你觉得栈和队列简单的时候,请想一想消息队列的重复消费问题有多让人头大。基础结构的简单,不代表基于它构建的系统也简单。

2. C++里用现成的还是自己写:工具选型的门道

学习阶段,我强烈建议你两种都做一遍。先用标准库的std::stack和std::queue跑通功能,再手写一个底层,因为面试官最爱问的就是"你讲一下队列怎么实现""循环队列怎么判断空和满"。

而到了实际项目里,能用标准库就用标准库。C++标准库的容器不是摆设,它们经过了几十年的优化和无数人踩坑后的修正,你手写的版本在大多数情况下不会比它更快。

2.1 直接用std::stack和std::queue:最省心的方案

C++标准库提供的std::stack和std::queue本质上都是容器适配器——它们不是自己存储数据,而是包装一个底部容器,限制它的接口。比如std::stack默认包装的是std::deque:

std::stack<int> s; s.push(10); s.push(20); int v = s.top(); // 20 s.pop(); std::queue<int> q; q.push(10); q.push(20); int f = q.front(); // 10 q.pop();

你可以指定底层容器。std::stack可以用std::vector、std::deque、std::list;std::queue通常用std::deque或std::list,很少用std::vector,因为头部删除效率太低。

这里有一个很多人忽略的细节:std::stack<int>里有一个pop()方法,它返回的是void,而不是弹出元素本身。也就是说,你要取栈顶元素必须先用top()拿到值,再调用pop()删除。为什么这么设计?因为返回void可以避免拷贝或移动构造的额外开销。这在C++98时代尤其重要,后来C++11有了右值引用,标准库也不愿意改接口,怕破坏兼容性。所以,永远写int v = s.top(); s.pop();,不要指望int v = s.pop();能编译通过。

2.2 手写栈和队列:搞懂底层才能应对面试

面试手写栈一般要求用数组实现固定容量的栈,或者用链表实现动态栈。用std::vector当然是作弊,但思路是对的——栈的逻辑就是一个可变数组的一端操作。

手写固定容量栈的经典代码:

class FixedStack { private: int* data; int capacity; int topIndex; public: FixedStack(int cap) : capacity(cap), topIndex(-1) { data = new int[capacity]; } ~FixedStack() { delete[] data; } bool push(int val) { if (topIndex + 1 >= capacity) return false; // 栈满 data[++topIndex] = val; return true; } bool pop() { if (topIndex < 0) return false; // 栈空 --topIndex; return true; } int top() const { return data[topIndex]; // 调用前确保不空 } bool empty() const { return topIndex < 0; } bool full() const { return topIndex + 1 >= capacity; } };

而链表栈的每个节点动态分配,好处是理论上容量无限(受内存限制),坏处是每次push都要分配内存,而且缓存不友好。实际项目中,除非你明确知道要用链表,否则数组栈通常更高效。

队列的手写要复杂一点,因为如果直接用数组头部删除,需要搬移数据,很慢。于是就有了循环队列。

2.3 循环队列的容量公式与空满判断:这是面试送分题也是丢分题

循环队列的核心思想是:用一个数组,通过(rear + 1) % capacity的方式绕回起点,让数组空间被循环利用。

空和满的判断是个经典考点。通常有两种约定:

方法一:牺牲一个存储单元

  • 队空条件:front == rear
  • 队满条件:(rear + 1) % capacity == front
  • 队列元素个数:(rear - front + capacity) % capacity

这个写法浪费一个格子,但判断逻辑简单。代码:

class CircularQueue { private: int* data; int capacity; int front; // 队首下标 int rear; // 队尾下标的下一个位置 public: CircularQueue(int cap) : capacity(cap), front(0), rear(0) { data = new int[capacity]; } ~CircularQueue() { delete[] data; } bool enqueue(int val) { if ((rear + 1) % capacity == front) return false; // 队满 data[rear] = val; rear = (rear + 1) % capacity; return true; } bool dequeue() { if (front == rear) return false; // 队空 front = (front + 1) % capacity; return true; } int frontValue() const { return data[front]; } };

注意这个%运算在C++里遇到负数会得到负余数,所以公式里都用了+ capacity再取模,这个细节很多人写代码时会忽略,结果在边界情况崩溃。

方法二:单独记录size或tag不浪费格子,额外维护一个size变量。队空判断为size == 0,队满判断为size == capacity。虽然多了一个变量,但逻辑更直白,而且不牺牲容量。我实际写代码更推荐这种,因为清晰比那一个格子的空间更宝贵。

循环队列的容量和下标全程都要用% capacity,这个取模运算看似简单,但在多线程环境下,如果被多个线程同时读写front和rear,会瞬间变成竞态条件。这也是为什么线程安全的队列要考虑加锁或无锁设计,而不是直接套用单线程循环队列。

3. 核心操作与实现细节:从栈帧到单调栈,一层层剥开

3.1 栈的典型应用:括号匹配和函数调用栈

括号匹配是栈的经典练习题,也是我当年第一个"哦原来栈是这么用的"顿悟时刻。算法很简单:遍历字符串,遇到左括号就入栈,遇到右括号就检查栈顶是否是对应的左括号。

bool isValid(const std::string& s) { std::stack<char> st; for (char c : s) { if (c == '(' || c == '[' || c == '{') { st.push(c); } else { if (st.empty()) return false; char left = st.top(); if ((c == ')' && left == '(') || (c == ']' && left == '[') || (c == '}' && left == '{')) { st.pop(); } else { return false; } } } return st.empty(); }

为什么这个题必须用栈?因为右括号的匹配顺序和左括号的出现顺序严格相反,这就是嵌套结构的天然特性。用数组也能做,但栈把"只能从一端操作"的规则强制表达出来了,代码更不容易写错。

函数调用栈(也就是热词里那个"函数栈帧的创建与销毁")是另一个经典应用。当一个函数被调用,系统会为它创建栈帧,包含局部变量、返回地址、前一个栈帧指针等。函数返回时,栈帧被销毁,控制权交还给调用者。如果你用调试器在程序崩坏时查看backtrace(栈回溯),会看到一串调用层次,那正是栈帧链的直观呈现。

在实际项目中,栈溢出是个不容忽视的问题。递归深度过大、局部变量太大的数组,都容易触发栈溢出。这也是为什么热词里有"cmake使用vs时如何设置栈大小"——当你在Visual Studio里用CMake维护项目,需要调大主线程栈大小时,通常要这样设置:

if(MSVC) target_compile_options(${PROJECT_NAME} PRIVATE /STACK:8388608) endif()

或者更推荐的做法是在链接选项里设置:

set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS "/STACK:8388608,1048576")

这个数字是字节,8388608是8MB。默认的MSVC主线程栈大小是1MB,如果你在函数里定义了一个大数组,或者递归太深,很容易就爆栈了。我自己在写某些递归算法时常常会遇到这个问题,当时又不想改算法为迭代,直接用/STACK调大栈确实能解决问题,但治标不治本。后来的经验是:如果递归深度可能超过一万层,优先考虑改写成迭代,而不是无限调大栈。

3.2 队列的典型应用:任务调度与层次遍历

队列在算法中最直观的应用是BFS(广度优先搜索)。想象你在一张地图上,起点是A,你要找一条到B的最短路。你先把A放入队列,然后取出A,把A的邻居放入队列,接着取出邻居,再放入邻居的邻居……这样层层推进,每个节点第一次被访问时距离就是最短距离。

二叉树层次遍历、迷宫最短路径、拓扑排序,这些经典场景全是队列在支撑。而到了系统层面,"任务调度"就是队列的另一个名字。线程池里待执行的任务排在一个队列中,工作线程从队列头取任务,新任务加到队列尾,这就是一个典型的生产者-消费者模型。

这里不得不提到热词里那个"线程池的阻塞队列选择"。如果你要用C++写线程池,线程池的任务队列需要满足两个要求:线程安全,以及当队列空的时候消费者不能忙等待(否则CPU空转到100%)。最简单的方案是用互斥锁加条件变量包装一个std::queue。

#include <queue> #include <mutex> #include <condition_variable> template <typename T> class BlockingQueue { private: std::queue<T> q; mutable std::mutex mtx; std::condition_variable not_full; std::condition_variable not_empty; size_t capacity; public: explicit BlockingQueue(size_t cap) : capacity(cap) {} void push(const T& task) { std::unique_lock<std::mutex> lock(mtx); not_full.wait(lock, [this] { return q.size() < capacity; }); q.push(task); not_empty.notify_one(); } T pop() { std::unique_lock<std::mutex> lock(mtx); not_empty.wait(lock, [this] { return !q.empty(); }); T task = q.front(); q.pop(); not_full.notify_one(); return task; } };

这就是一个最简单的阻塞队列。它解决了两个关键问题:有界容量防止任务无限堆积把内存打爆;条件变量让消费者在队列空时睡眠而不是忙等。

3.3 竞赛和面试里那些"变形的栈和队列":单调栈与双端队列

刷题刷多了之后,你会发现基础栈和队列的题目很少直接考,更多是考它们的变形。热词里的"单调栈揭秘"——单调栈是LeetCode和面试里出镜率非常高的结构。

单调栈的核心思想是:维护一个栈,让栈内元素保持单调递增或递减。常用于解决"下一个更大元素""柱状图中最大矩形""接雨水"等问题。比如"每日温度"问题,要求你输出每个温度距离下一个更高温度还有几天:

vector<int> dailyTemperatures(vector<int>& temperatures) { int n = temperatures.size(); vector<int> ans(n, 0); stack<int> st; // 存下标 for (int i = 0; i < n; ++i) { while (!st.empty() && temperatures[i] > temperatures[st.top()]) { int prevIndex = st.top(); st.pop(); ans[prevIndex] = i - prevIndex; } st.push(i); } return ans; }

这个算法的关键在于:当遇到一个比栈顶温度更高的温度时,栈顶元素的答案就可以确定了,于是弹出。每个下标最多入栈一次、出栈一次,时间复杂度O(n)。如果不用单调栈,暴力求解需要O(n^2),数据量大一点就超时了。

双端队列(std::deque)则是另一个重要的变形,它在"滑动窗口最大值"问题里大放异彩。你需要在O(n)时间内求出每个窗口的最大值。思路是维护一个双端队列,队头永远是当前窗口的最大值下标,新元素入队时,从队尾把所有比它小的元素弹出,因为它们永远不可能是窗口最大值了。

vector<int> maxSlidingWindow(vector<int>& nums, int k) { deque<int> dq; vector<int> ans; for (int i = 0; i < nums.size(); ++i) { if (!dq.empty() && dq.front() <= i - k) dq.pop_front(); while (!dq.empty() && nums[dq.back()] <= nums[i]) dq.pop_back(); dq.push_back(i); if (i >= k - 1) ans.push_back(nums[dq.front()]); } return ans; }

如果你理解了这个代码,你就会发现deque在C++里有它的独到之处,它是队列、栈的理想底层容器,正因为头尾插入删除都是O(1),它才成为std::stack和std::queue的默认选择。

4. 实操过程:亲手实现一个可复用的栈和队列

说再多不如动手写一遍。这一章我们来完整实现一个能用于实验报告的栈和队列。我建议你在VS或VSCode里跟着做,环境配置也是很多新手卡壳的地方。

4.1 环境准备:Visual Studio、CMake与VSCode的选择

如果你只是跑跑小例子,VSCode配C/C++环境最简单。装一个编译器(Windows下通常是MinGW或MSVC),然后在VSCode里装C/C++扩展,配置好tasks.json和launch.json就能编译调试。但如果你要开展一个比较大的C++项目,用Visual Studio是最省事的,毕竟热词里那个"microsoft visual c++ 2015-2022 redistributable (x64) 下载"意味着你的程序以后跑在别的机器上还需要装运行时库。

我个人的偏好是:现代C++项目用CMake管理构建,然后在VS里打开CMake文件夹。CMake可以同时处理生成、编译、测试,还方便设置链接选项。上文提过的栈大小设置在CMake里就是添加链接标志。

4.2 模块设计:接口定义与测试用例

我们的目标不是写一个能跑的demo,而是写一个可复用、可测试的容器。所以先定义接口:

  • 栈:push、pop、top、empty、size
  • 队列:enqueue(或push)、dequeue、front、back、empty、size

我会做成模板类,支持任意类型。同时为方便实验报告,添加一个toString()方法打印内部元素(内存层面展示)。

测试用例至少覆盖:

  • 空栈调用pop或top要抛出异常
  • 顺序压入1~10,弹出后顺序是10~1
  • 队列顺序压入1~10,弹出后顺序是1~10
  • 循环队列的容量边界:压满后再次push失败,弹空后再次pop失败

4.3 完整实现:环形队列的模板版本

这里给出一个基于数组的环形队列模板实现,支持任意类型(要求可拷贝,对于普通类型足够):

#include <iostream> #include <vector> #include <stdexcept> #include <string> template <typename T> class MyQueue { private: std::vector<T> data; size_t capacity; size_t frontIdx; size_t rearIdx; size_t count; public: explicit MyQueue(size_t cap) : capacity(cap), frontIdx(0), rearIdx(0), count(0) { data.resize(capacity); } void push(const T& val) { if (full()) throw std::overflow_error("Queue is full"); data[rearIdx] = val; rearIdx = (rearIdx + 1) % capacity; ++count; } void pop() { if (empty()) throw std::underflow_error("Queue is empty"); frontIdx = (frontIdx + 1) % capacity; --count; } T& front() { if (empty()) throw std::underflow_error("Queue is empty"); return data[frontIdx]; } const T& front() const { if (empty()) throw std::underflow_error("Queue is empty"); return data[frontIdx]; } bool empty() const { return count == 0; } bool full() const { return count == capacity; } size_t size() const { return count; } };

注意这个实现用了count变量,所以不需要牺牲一个格子,容量是满的。空满判断代码清楚,不用写那些容易错的模运算来判断空满。下标还是要取模,因为随着不断出队,frontIdx会向数组尾方向移动,取模让它绕回头部。

栈的实现可以用一个std::vector直接搞定,这里就不再重复贴一遍,前面已经给过。

编译之后,建议你用下面的测试代码验证:

int main() { MyQueue<int> q(3); q.push(1); q.push(2); q.push(3); try { q.push(4); } catch (const std::overflow_error& e) { std::cout << "expected overflow: " << e.what() << std::endl; } std::cout << q.front() << std::endl; // 1 q.pop(); std::cout << q.front() << std::endl; // 2 q.push(4); std::cout << q.front() << std::endl; // 2 while (!q.empty()) { std::cout << q.front() << " "; q.pop(); } std::cout << std::endl; return 0; }

运行结果应该清晰显示队列能循环利用空间。我实际测试过,capacity=3时,push 1、2、3之后队列满了,再push会抛异常;pop掉1之后,再push 4,此时4应该入到下标0的位置,这就是循环队列的意义——不浪费前面被释放的槽位。

5. 常见问题与排查技巧:那些年我们一起踩过的坑

5.1 栈溢出:从崩溃到backtrace栈回溯

困扰新手最多的问题就是栈溢出。在Visual Studio里跑程序,突然弹出"Stack overflow"或者程序退出了。原因不外乎无限递归、局部数组过大、某个函数里分配了超大std::array。

排查手段第一是看backtrace。在VS的调试器里,中断之后打开"调用堆栈"窗口,你能看到从当前执行位置一路向上的函数调用链。如果发现有某个函数无限重复出现在调用栈里,那基本就是无限递归。

第二是看代码里有没有在函数内定义大数组。比如:

void process() { int buffer[1000000]; // 4MB,很可能爆掉1MB栈 }

修复方式不要只想到调大栈。更好的做法是改用std::vector,它的数据存储在堆上,堆比栈大得多。如果算法必须深递归,就把递归深度压到几千层以内,或者改写为显式栈模拟。

5.2 循环队列的边界条件:空与满,一个格式化但致命的问题

我这个坑踩过很惨。当时写循环队列,用了front == rear判断队空,用了(rear + 1) % n == front判断队满,逻辑看着没问题,但当我容量为1时,初始化front == rear == 0,此时(rear + 1) % 1 == 0,也就是说队满条件恒成立。一个容量为1的队列居然永远"满",是不是很离谱?

所以,如果你用"牺牲一个格子"的方案,容量必须至少为2。如果容量可能为1,或者你想让容量完全利用,就老老实实加上count变量。

另一个边界问题是删除到空后再调用front()。标准做法是抛异常。但有的人图省事直接返回一个默认值,而这种"隐式失败"会在上层掩盖错误,让调试更困难。我在自己的项目里一律用throw,把问题暴露在第一时间。

5.3 线程池里选哪种阻塞队列:有界、无界还是优先级?

热词里出现"线程池的阻塞队列选择",这其实是并发编程里一个重要的设计决策。在C++里你可以实现三种语义:

  • 无界队列:任务永远能进来,消费者不够快时任务堆积,内存迟早爆。
  • 有界队列:队列满时生产者等待或拒绝,相当于限流。
  • 优先级队列:用std::priority_queue,任务有优先级,但要注意优先级反转、公平性等问题。

我的建议是:生产环境尽量选有界队列,同时配合拒绝策略。Java的ThreadPoolExecutor里有AbortPolicy、CallerRunsPolicy等,C++里也要自己实现类似策略。如果没有积压容忍度要求,就用简单的固定容量阻塞队列,满了就让生产者等待,或者丢弃任务并记录日志。

5.4 消息队列的重复消费:为什么基础结构也会带出这种复杂问题

热词里有"消息队列重复消费问题",严格来说这已经超出了栈和队列的范畴,但它恰恰说明队列在实际系统中会遇到什么。比如RabbitMQ里的消费者处理完任务后,还没来得及确认就宕机了,消息会被重新投递给另一个消费者,于是造成重复。解决方案通常是幂等性设计——消费者端处理逻辑要支持重复执行而结果不变,或者用唯一消费记录表去重。

回到数据结构本身,我们要理解:队列只保证"先进先出"或"至少一次投递",它不保证"恰好一次"。所以凡是基于队列构建消息系统,一定要把"去重"放在业务层。

5.5 快速自查清单

最后整理一个速查表,方便你期末复习或面试前检查:

场景首选结构原因
函数调用/回溯/撤销栈需要后进先出
层次遍历/任务调度队列需要先进先出
求最近更大/更小元素单调栈用栈顶维护候选集
滑动窗口最值双端队列头尾都要弹出
消息任务缓冲阻塞队列线程安全+阻塞语义

我个人在实际操作中的体会是,栈和队列之所以值得花一整个下午去手写、去测试、去调边界,是因为它们几乎是所有C++项目里最频繁接触的容器类型。你在刷题的时候用它们,在工作的时候用它们,在排查崩溃的时候还用它们。把它们的内部机制摸透了,后面再学树、图、堆这些结构时,你会发现很多东西都是栈和队列思想的延伸。比如树的深度遍历可以显式用栈实现,广度遍历用队列实现,这些思路都是通的。所以,真的别觉得它们简单就不屑于深挖——数据结构的第一块砖,恰恰是最值得砸实的地方。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 3:22:45

链表刷题核心框架:虚拟头节点与三大经典操作实战

算法训练营进入第三天&#xff0c;今天正式从数组切到链表。昨天还在讲双指针和滑动窗口&#xff0c;今天就变成了指针的指向、节点的增删。今天安排的三道题——203. 移除链表元素、707. 设计链表、206. 反转链表&#xff0c;其实是一条非常丝滑的学习链路&#xff1a;先用最简…

作者头像 李华
网站建设 2026/10/10 3:22:30

照着用就行:2026年顶尖AI论文平台榜单,免费版也能写合规初稿

2026 年实测 10 款主流 AI 论文工具&#xff0c;千笔AI以全流程覆盖 语义级降重 免费查重领跑综合榜&#xff1b;ThouPen 稳坐留学生毕业全流程工具头把交椅&#xff1b;免费工具中DeepSeek Scholar、豆包学术版表现亮眼&#xff0c;30 分钟即可生成万字高质量初稿&#xff0…

作者头像 李华
网站建设 2026/10/10 3:22:28

SpringAI + MCP + SSE:Java后端接入AI工具调用的最佳实践

最近在忙一个 Java 后端接入大模型工具调用的项目&#xff0c;需求不复杂&#xff1a;让 AI Agent 能去查数据库、调内部 REST 接口&#xff0c;还要能实时拿结果。选型的时候卡了一下&#xff0c;最后定了SpringAI MCP SSE这条路线&#xff0c;整体跑下来比想象中省事&#…

作者头像 李华
网站建设 2026/10/10 3:21:58

三菱Q系列+FX系列多站PLC分布式控制方案:CC-Link组网与调试实践

在自动化设备里遇到“三菱Q系列程序&#xff0c;主站Q02UCPUFX3U-128MFX3S-14MFX3U-4AD”这套配置&#xff0c;其实是一个很典型的分布式控制场景&#xff1a;Q系列做主站统筹全局&#xff0c;FX系列拆到现场去干活。我最近做完的一个项目就是这种结构&#xff0c;今天把整个系…

作者头像 李华
网站建设 2026/10/10 3:21:14

.NET企业门户网站完整版:三层架构、部署实战与性能优化

简介&#xff1a;企业网站开发常面临功能完整性与可维护性的双重挑战&#xff0c;分层架构将数据访问、业务逻辑与页面表现分离&#xff0c;降低模块间耦合&#xff0c;便于独立调整与后续扩展。在.NET技术栈中&#xff0c;这种设计结合参数化查询、输出缓存、IIS部署配置等技术…

作者头像 李华