C++ STL Stack与Queue深度解析:从容器适配器到实战应用 1. 项目概述为什么STL的Stack和Queue值得你花时间如果你正在学习C或者已经写过一些代码那么“容器”这个概念你一定不陌生。数组、链表、向量vector……这些都是存放数据的“盒子”。但今天要聊的stack和queue是两种非常特殊、也极其有用的容器。它们不像vector那样可以让你随心所欲地访问任意位置的元素恰恰相反它们对数据的访问和操作施加了严格的规则。这种“限制”不是缺陷而是它们强大力量的来源。简单来说stack栈遵循“后进先出”LIFO原则就像一摞盘子你总是拿走最上面的那个最后放上去的。而queue队列遵循“先进先出”FIFO原则就像排队买票先来的人先得到服务。这两种数据结构是计算机科学的基础从函数调用、表达式求值到任务调度、消息传递无处不在。STL标准模板库为我们封装好了这两种数据结构的实现让我们无需从零开始造轮子就能安全、高效地使用它们。这篇文章我会从一个有十多年C开发经验的老兵视角带你彻底吃透STL中的stack和queue。我不会只给你罗列几个函数接口那太枯燥了。我会深入它们的底层实现逻辑解释为什么它们被设计成这样分享在实际项目中如何选择和使用它们以及那些官方文档里不会写的“坑”和实战技巧。无论你是正在准备面试还是想在项目中更优雅地处理数据流相信这篇详解都能给你带来实实在在的帮助。2. Stack后进先出的艺术与实现2.1 Stack的核心特性与设计哲学stack的设计哲学非常纯粹它只允许在一端称为栈顶进行插入压栈和删除弹栈操作。这种“自缚手脚”的设计带来了几个关键优势操作简单高效由于接口极度精简主要就push,pop,top,empty,size它的时间复杂度是常数级的O(1)非常快。数据安全你无法直接修改栈中间的元素这避免了在复杂逻辑中意外破坏数据结构完整性的风险。语义清晰任何使用stack的代码其意图都非常明确——你在处理一个具有“最近相关性”的问题。比如浏览器的“后退”按钮、文本编辑器的“撤销”操作其底层就是一个历史记录的栈。在STL中stack被实现为一个容器适配器Container Adapter。这是一个非常重要的概念。它意味着stack本身并不是一个完整的、从头实现的容器而是“站在巨人的肩膀上”——它底层封装了一个已有的序列容器默认是deque并限制其接口只暴露栈应有的操作。这种设计体现了STL的高度复用性和灵活性。2.2 Stack的接口详解与实战演练让我们抛开枯燥的列表直接通过代码和场景来理解每个接口。首先要使用stack必须包含头文件stack。#include iostream #include stack int main() { // 声明一个存储int类型的栈 std::stackint myStack; // 1. push() - 压栈操作 myStack.push(10); // 栈底[10] myStack.push(20); // 栈底[10] - [20] (栈顶) myStack.push(30); // 栈底[10, 20] - [30] (栈顶) std::cout After pushes, size is: myStack.size() std::endl; // 输出: 3 // 2. top() - 访问栈顶元素只读不删除 // 这是获取栈顶元素的唯一方式。务必注意在调用top()之前一定要检查栈是否为空 if (!myStack.empty()) { std::cout Top element is: myStack.top() std::endl; // 输出: 30 } // 3. pop() - 弹栈操作 // pop()函数只删除栈顶元素但不返回该元素的值。这是一个常见的“坑”。 myStack.pop(); // 删除30 std::cout After one pop, top is: myStack.top() std::endl; // 输出: 20 std::cout Size is now: myStack.size() std::endl; // 输出: 2 // 4. empty() - 判断栈是否为空 while (!myStack.empty()) { std::cout Popping: myStack.top() std::endl; myStack.pop(); // 循环清空栈 } std::cout Stack is empty: std::boolalpha myStack.empty() std::endl; // 输出: true // 5. size() - 获取元素个数 myStack.push(1); myStack.push(2); std::cout Final size: myStack.size() std::endl; // 输出: 2 return 0; }实战心得与避坑指南top()和pop()的分离设计为什么STL不设计一个pop()函数同时返回栈顶值并删除它这是出于异常安全性的考虑。如果pop()需要返回元素就必须在删除元素前进行拷贝或移动构造这个操作可能会抛出异常。如果异常发生元素既被弹出状态改变又没有成功返回给用户就会导致数据丢失。将top()访问和pop()删除分离保证了操作的原子性和异常安全。你需要先top()获取值再pop()删除它。空栈操作是未定义行为对空栈调用top()或pop()是严重的错误会导致程序崩溃如段错误。务必养成在调用top()或pop()前用empty()检查的习惯。这是编写健壮代码的基本功。“遍历”栈的误区栈本身不支持迭代器因为它违背了LIFO的原则。如果你发现自己需要遍历一个栈来查找某个元素那么99%的情况是你选错了数据结构应该考虑使用vector、list或deque。栈的典型使用场景是你只关心最近的那个或那几个元素。2.3 Stack的底层容器与自定义如前所述stack是一个适配器。它的完整模板声明是template class T, class Container dequeT class stack;T: 栈中存储的元素类型。Container: 底层容器类型必须提供back()、push_back()、pop_back()、empty()和size()操作。默认是deque双端队列。你可以指定其他满足条件的容器作为底层实现#include stack #include vector #include list int main() { // 使用vector作为底层容器 std::stackint, std::vectorint stack_using_vector; // 使用list作为底层容器 std::stackint, std::listint stack_using_list; stack_using_vector.push(1); // 底层调用 vector::push_back stack_using_list.push(2); // 底层调用 list::push_back return 0; }如何选择底层容器默认的deque在大多数情况下是最佳选择。deque在头部和尾部插入删除都是O(1)且内存分配是分段连续的能较好地平衡性能和内存使用。它是STL为stack和queue选择的“全能型”默认底层容器。vectorvector的push_back和pop_back也是O(1)均摊时间复杂度且内存连续访问局部性好。但是vector在扩容时需要重新分配内存和拷贝元素这个操作是O(n)的。如果你的栈大小变化非常剧烈可能会有性能波动。另外vector没有提供标准的pop_back返回值与stack的pop设计一致所以完全适配。listlist的插入删除永远是O(1)且没有扩容开销。但它的内存不连续缓存不友好每个元素都有额外的前后指针开销内存占用较大。除非有非常特殊的理由比如需要在栈中频繁插入删除中间元素——但这本身违背栈的用途否则一般不选。个人建议除非你经过性能剖析Profiling证明默认的deque是瓶颈否则不要轻易更改底层容器。deque是STL设计者精心挑选的默认选项在绝大多数场景下都足够优秀。3. Queue先进先出的秩序与内涵3.1 Queue的核心特性与设计哲学如果说stack是“后来者居上”那么queue就是“先到先得”。它模拟了现实世界中的排队行为元素从一端队尾加入从另一端队头离开。这种FIFO的特性使其成为处理任务、消息、请求等需要按序执行场景的天然选择。queue同样是一个容器适配器。它的底层容器需要提供front()、back()、push_back()、pop_front()、empty()和size()操作。默认的底层容器也是deque因为deque在头尾的操作都是高效的。3.2 Queue的接口详解与实战演练包含头文件queue让我们开始使用队列。#include iostream #include queue int main() { // 声明一个存储int类型的队列 std::queueint taskQueue; // 1. push() - 入队操作在队尾插入 taskQueue.push(100); // 队头[100] - 队尾 taskQueue.push(200); // 队头[100, 200] - 队尾 taskQueue.push(300); // 队头[100, 200, 300] - 队尾 std::cout Queue size after pushes: taskQueue.size() std::endl; // 输出: 3 // 2. front() - 访问队头元素最早加入的 // 和stack的top()一样调用前必须检查队列是否为空 if (!taskQueue.empty()) { std::cout Front (next to process): taskQueue.front() std::endl; // 输出: 100 } // 3. back() - 访问队尾元素最新加入的 // 这个接口是queue独有的stack没有因为栈顶既是“头”也是“尾”。 std::cout Back (most recent): taskQueue.back() std::endl; // 输出: 300 // 4. pop() - 出队操作删除队头元素 // 注意pop()同样只删除不返回值。 taskQueue.pop(); // 删除100 std::cout After pop, front is now: taskQueue.front() std::endl; // 输出: 200 std::cout Size is now: taskQueue.size() std::endl; // 输出: 2 // 模拟一个简单的任务处理器 std::cout \n--- Processing all tasks ---\n; while (!taskQueue.empty()) { int currentTask taskQueue.front(); std::cout Processing task: currentTask std::endl; // ... 执行任务的实际操作 ... taskQueue.pop(); // 任务完成出队 } std::cout All tasks processed. Queue empty: taskQueue.empty() std::endl; return 0; }实战心得与避坑指南front()/back()与pop()的分离和stack一样出于异常安全考虑访问和删除操作是分离的。牢记pop()不返回值的特性。空队列操作对空队列调用front()、back()或pop()是未定义行为。防御性编程要求每次访问前用empty()判断。queue的“遍历”和stack类似queue也不提供迭代器。标准的遍历方式就是用while (!empty())循环结合front()和pop()。如果你需要基于某种条件查找或处理队列中的特定元素而非严格按FIFO顺序可能需要重新评估数据结构的选择或者考虑使用deque或list。3.3 Queue的底层容器选择queue的模板声明为template class T, class Container dequeT class queue;它对底层容器的要求与stack略有不同需要支持pop_front()。因此vector不能直接作为queue的底层容器因为vector没有提供pop_front()操作该操作在vector上是O(n)的不符合queue对效率的要求。可用的底层容器主要有deque默认同样是最佳通用选择头尾操作都是O(1)。list同样满足所有要求头尾插入删除都是O(1)。在需要频繁在队列中间进行插入删除的极端特殊场景下这通常意味着你的设计可能需要反思list是唯一选择。4. Stack与Queue的经典应用场景剖析理解了基本操作我们来看看它们在实际中如何大放异彩。这些场景能帮你真正建立“数据结构思维”。4.1 Stack的典型应用1. 函数调用栈Call Stack这是栈最根本的应用。每次函数调用时系统会将返回地址、局部变量、参数等压入一个专门的栈调用栈。函数返回时再从栈顶弹出这些信息恢复到调用者的上下文。递归函数就是利用调用栈的经典例子。2. 表达式求值与语法解析中缀表达式转后缀表达式逆波兰表达式这是栈的招牌应用。算法核心是使用一个操作符栈来处理优先级。遇到数字直接输出遇到操作符则与栈顶操作符比较优先级决定是入栈还是出栈。后缀表达式求值遇到数字就入栈遇到操作符就从栈顶弹出两个数字进行计算结果再压回栈中。最后栈顶就是表达式结果。括号匹配检查遍历字符串遇到左括号(, {, [就压栈遇到右括号就检查栈顶是否是对应的左括号是则弹出否则不匹配。最后栈为空则匹配成功。3. 浏览器的前进/后退功能浏览器将访问的页面URL压入一个栈后退栈。点击后退时当前页面URL被压入另一个栈前进栈并从后退栈弹出上一个页面。点击前进则相反。两个栈协同工作。4. 编辑器的撤销Undo/重做Redo每进行一次编辑操作就将该操作的状态压入“撤销栈”。当用户撤销时从撤销栈弹出操作并执行逆操作同时将该操作压入“重做栈”。重做操作则从重做栈弹出并执行。实战代码示例括号匹配#include iostream #include stack #include string #include unordered_map bool isBalanced(const std::string expression) { std::stackchar s; std::unordered_mapchar, char matchingBracket { {), (}, {], [}, {}, {} }; for (char ch : expression) { // 如果是左括号压栈 if (ch ( || ch [ || ch {) { s.push(ch); } // 如果是右括号 else if (ch ) || ch ] || ch }) { // 如果栈为空或栈顶不匹配则失败 if (s.empty() || s.top() ! matchingBracket[ch]) { return false; } // 匹配成功弹出左括号 s.pop(); } // 其他字符如字母、数字忽略 } // 最后栈必须为空才说明所有括号都匹配完毕 return s.empty(); } int main() { std::string test1 (a b) * {c - [d / e]}; std::string test2 ((()); std::string test3 ([)]; std::cout Test1 is balanced: std::boolalpha isBalanced(test1) std::endl; // true std::cout Test2 is balanced: isBalanced(test2) std::endl; // false std::cout Test3 is balanced: isBalanced(test3) std::endl; // false return 0; }4.2 Queue的典型应用1. 任务调度与消息队列这是队列最直观的应用。在多线程、网络服务器、操作系统内核中到处都有队列的身影。线程池任务队列主线程将需要执行的任务函数对象放入一个任务队列。工作线程从队列头部取出任务执行。std::queue常与互斥锁std::mutex和条件变量std::condition_variable结合使用实现生产者-消费者模型。网络请求缓冲服务器收到大量客户端请求时可能无法立即处理。可以将请求放入一个队列再由工作线程按顺序处理起到缓冲和流量整形的作用。打印队列操作系统管理多个打印任务时使用队列确保先提交的文档先被打印。2. 广度优先搜索BFS在图和树的遍历中BFS算法天然需要队列。从起点开始将其放入队列。只要队列不为空就取出队头节点访问它并将其所有未访问的邻居节点加入队尾。这样可以确保按“层次”或“距离”逐层遍历。3. 缓存淘汰策略如FIFO Cache一种简单的缓存实现是设定固定大小。当缓存满且需要加入新数据时就淘汰最早进入缓存的数据队头。虽然FIFO策略不如LRU最近最少使用高效但实现简单。4. 数据流处理在实时数据处理管道中队列可以作为不同处理阶段之间的缓冲区。例如数据采集模块将数据放入队列分析模块从队列取出数据进行分析解耦了生产者和消费者的速度差异。实战代码示例简单的BFS框架#include iostream #include queue #include vector #include unordered_set // 假设图用邻接表表示 void bfs(int startNode, const std::vectorstd::vectorint graph) { int numNodes graph.size(); std::vectorbool visited(numNodes, false); // 访问标记数组 std::queueint q; // BFS队列 // 从起始节点开始 visited[startNode] true; q.push(startNode); std::cout BFS traversal starting from node startNode : ; while (!q.empty()) { int currentNode q.front(); q.pop(); std::cout currentNode ; // 处理当前节点这里简单打印 // 遍历当前节点的所有邻居 for (int neighbor : graph[currentNode]) { if (!visited[neighbor]) { visited[neighbor] true; q.push(neighbor); // 未访问的邻居入队 } } } std::cout std::endl; } int main() { // 一个简单的无向图示例 (0-1-2, 1-3, 2-4) // 邻接表表示 std::vectorstd::vectorint graph { {1}, // 节点0的邻居 {0, 2, 3}, // 节点1的邻居 {1, 4}, // 节点2的邻居 {1}, // 节点3的邻居 {2} // 节点4的邻居 }; bfs(0, graph); // 从节点0开始BFS // 期望输出: 0 1 2 3 4 (顺序可能因邻居顺序略有不同但层次一致) return 0; }5. 进阶话题性能、线程安全与自定义适配器5.1 Stack与Queue的性能考量对于stack和queue我们主要关心插入和删除操作的时间复杂度。由于它们是适配器其性能完全取决于底层容器的性能。操作底层容器为deque(默认)底层容器为vector(仅stack)底层容器为listpush/push_backO(1)(均摊)O(1)(均摊可能触发扩容)O(1)pop/pop_back(stack)O(1)O(1)O(1)pop/pop_front(queue)O(1)不支持O(1)top/front/backO(1)O(1)O(1)内存连续性分段连续完全连续不连续额外开销中等低高每个元素两个指针适用场景通用默认选择stack专用需超大连续内存时需频繁在中间插入删除时罕见结论对于绝大多数应用使用默认的deque底层容器即可获得最佳的综合性能。除非你有非常确凿的证据通过性能剖析工具表明deque是瓶颈并且你的使用模式极度契合vector栈大小稳定或预知或list需要中间操作否则不要轻易更换。5.2 线程安全与并发访问STL的stack和queue本身不是线程安全的这是很多新手容易忽略的关键点。如果多个线程同时读写同一个栈或队列而不加任何同步控制会导致数据竞争Data Race引发未定义行为程序可能崩溃或产生错误结果。如何在多线程环境下安全使用外部加锁最常用的方法。使用std::mutex等互斥锁在每次调用push、pop、top、front等操作前后进行加锁和解锁。#include queue #include mutex #include condition_variable templatetypename T class ThreadSafeQueue { private: std::queueT queue_; mutable std::mutex mutex_; std::condition_variable cond_; public: void push(T value) { std::lock_guardstd::mutex lock(mutex_); queue_.push(std::move(value)); cond_.notify_one(); // 通知一个等待的消费者 } bool try_pop(T value) { std::lock_guardstd::mutex lock(mutex_); if (queue_.empty()) { return false; } value std::move(queue_.front()); queue_.pop(); return true; } // 阻塞等待直到有元素可弹出 void wait_and_pop(T value) { std::unique_lockstd::mutex lock(mutex_); cond_.wait(lock, [this]{ return !queue_.empty(); }); value std::move(queue_.front()); queue_.pop(); } // ... 其他方法如 empty(), size() 也需要加锁 };使用并发容器C标准库目前没有提供官方的线程安全stack或queue。但第三方库如Intel TBB, Microsoft PPL或一些开源库提供了并发队列如tbb::concurrent_queue。在C17及以后你也可以考虑std::optional与锁结合设计更优雅的接口。无锁队列在极端高性能要求的场景下可以考虑实现或使用无锁lock-free队列。但这属于高级话题实现复杂且需要处理内存序等底层问题一般只在特定性能瓶颈处使用。重要提醒即使像empty()和size()这样的只读操作在多线程环境下也必须加锁。因为在你调用empty()判断为非空后、调用front()之前另一个线程可能已经pop()了最后一个元素导致front()访问空队列。5.3 实现一个自定义的容器适配器理解适配器模式最好的方式就是自己动手实现一个简化版。假设我们想实现一个FixedSizeStack它在栈满时拒绝push操作。#include iostream #include vector #include stdexcept // for std::overflow_error, std::underflow_error template typename T, size_t Capacity class FixedSizeStack { private: std::vectorT data_; // 底层容器 size_t topIndex_; // 栈顶索引指向下一个可插入位置 public: FixedSizeStack() : data_(Capacity), topIndex_(0) {} // 预分配空间 void push(const T value) { if (topIndex_ Capacity) { throw std::overflow_error(FixedSizeStack is full!); } data_[topIndex_] value; // 在topIndex位置赋值然后索引1 } void pop() { if (topIndex_ 0) { throw std::underflow_error(FixedSizeStack is empty!); } --topIndex_; // 只需减小索引“删除”元素实际内存还在但逻辑上已移除 // 注意对于非平凡类型可能需要调用析构函数。这里简化处理。 } T top() { if (topIndex_ 0) { throw std::underflow_error(FixedSizeStack is empty!); } return data_[topIndex_ - 1]; // 栈顶元素在topIndex-1的位置 } const T top() const { // const版本用于const对象 if (topIndex_ 0) { throw std::underflow_error(FixedSizeStack is empty!); } return data_[topIndex_ - 1]; } bool empty() const { return topIndex_ 0; } bool full() const { return topIndex_ Capacity; } // 自定义方法 size_t size() const { return topIndex_; } size_t capacity() const { return Capacity; } // 自定义方法 }; int main() { FixedSizeStackint, 5 stack; try { for (int i 0; i 6; i) { // 故意多push一次 stack.push(i * 10); std::cout Pushed i * 10 , size stack.size() std::endl; } } catch (const std::overflow_error e) { std::cerr Error: e.what() std::endl; } std::cout \nPopping elements:\n; while (!stack.empty()) { std::cout Top: stack.top() std::endl; stack.pop(); } return 0; }这个简单的例子展示了适配器的核心思想基于一个已有的容器这里用了vector通过封装和限制接口提供一种新的、特定的数据结构抽象。STL的stack和queue做得更完善、更通用、更高效。6. 常见问题、面试考点与排查技巧6.1 高频面试题解析Q:stack的pop()函数为什么设计成不返回栈顶元素A:主要出于异常安全考虑。如果pop()需要返回元素就必须在删除元素前进行拷贝或移动构造T value std::move(topElem);这个操作可能抛出异常如拷贝构造函数抛出。如果异常发生元素已经从栈中移除状态已改变但用户没有成功接收到值数据就永久丢失了。将top()访问和pop()删除分离保证了操作的强异常安全性。Q:stack和queue的底层默认容器是什么为什么选择它A:默认都是deque双端队列。选择deque是因为它在头部和尾部进行插入和删除操作的时间复杂度都是O(1)均摊且内存分配是分段连续的既避免了vector扩容时的大规模拷贝又比list的内存局部性好、开销小。它是一个在栈和队列这种只关心序列两端的场景下性能综合表现最好的容器。Q: 可以用vector作为queue的底层容器吗A:不可以。queue需要底层容器提供pop_front()操作而vector的pop_front()效率是O(n)需要移动所有后续元素不符合queue对高效出队的要求。STL的queue适配器要求底层容器提供pop_front()vector不满足这个接口要求。Q: 如何用两个栈实现一个队列如何用两个队列实现一个栈A:这是经典的算法设计题。双栈实现队列维护两个栈stackIn和stackOut。入队时直接push到stackIn。出队时如果stackOut为空则将stackIn中的所有元素依次弹出并压入stackOut这样stackOut的栈顶就是最早进入的元素然后从stackOut弹出。如果stackOut不为空则直接弹出其栈顶。双队列实现栈维护两个队列q1和q2。入栈时将元素加入非空的队列假设为q1。出栈时将非空队列q1的前n-1个元素依次出队并入队到另一个空队列q2然后将q1中剩下的最后一个元素出队这就是栈顶元素。之后交换q1和q2的角色。Q:stack和queue支持迭代器吗为什么A:不支持。迭代器的本质是提供一种顺序访问容器中所有元素的能力。stack和queue被设计为限制访问模式LIFO/FIFO的适配器提供迭代器会让用户能够绕过这些限制访问中间元素从而破坏其抽象语义和数据完整性。如果你需要遍历说明你应该使用deque、list或vector。6.2 实战中的典型错误与排查错误对空容器调用top()/front()/pop()现象程序运行时崩溃报错如“Segmentation fault”或“Assertion failed”。排查立即检查调用这些函数前是否用empty()判断了容器状态。这是使用栈和队列时必须进行的防御性检查。// 错误示范 std::stackint s; int val s.top(); // 崩溃 s.pop(); // 崩溃 // 正确做法 if (!s.empty()) { int val s.top(); s.pop(); // ... 处理val }错误误以为pop()会返回值现象代码逻辑错误期望的值丢失。排查记住pop()的返回值是void。你需要先用top()或front()获取值再调用pop()删除它。std::queueint q; q.push(42); // 错误int x q.pop(); // 编译错误pop()返回void // 正确 int x q.front(); // 获取值 q.pop(); // 删除值错误在多线程环境中不加锁直接使用现象数据竞争导致程序行为不确定、崩溃或结果错误。这种Bug通常难以复现和调试。排查审查所有可能被多个线程访问的stack或queue对象。如果存在共享访问必须使用互斥锁std::mutex等同步机制保护所有操作包括empty()和size()。错误需要遍历却选择了栈或队列现象代码写得很别扭需要复制容器或使用临时变量来模拟遍历性能不佳且代码丑陋。排查重新审视需求。如果你需要频繁地查找、访问或处理容器中的任意元素那么stack或queue很可能是错误的选择。考虑使用vector、deque、list、set或map等支持迭代器的容器。6.3 性能优化小贴士预分配空间针对stack使用vector底层时如果你使用std::stackT, std::vectorT并且能预估栈的最大大小可以在构造后立即调用底层vector的reserve()方法避免多次扩容带来的开销。std::stackint, std::vectorint s; s.c.get_container().reserve(1000); // c是stack底层的容器对象标准命名 // 注意直接访问底层容器不是标准接口依赖于实现。更可移植的做法是自定义一个适配器。使用emplace代替pushC11及以上对于包含非平凡构造函数的对象类型使用emplace可以直接在容器内部构造对象避免不必要的拷贝或移动操作。struct Task { int id; std::string name; Task(int i, const std::string n) : id(i), name(n) {} }; std::queueTask q; // q.push(Task(1, Job)); // 需要构造一个临时Task对象然后移动 q.emplace(1, Job); // 更高效直接在队列内存中构造Task对象对于简单数据类型默认的deque通常足够快不要陷入“过早优化”的陷阱。在证明性能瓶颈确实在于容器之前坚持使用最简单、最标准的std::stack和std::queue。在我多年的C开发经历中stack和queue是那种“简单但深刻”的工具。它们的接口简单到几分钟就能学会但真正理解其设计哲学和适用场景才能让你在架构设计时做出优雅的选择。记住选择合适的数据结构往往比优化算法更能提升代码的清晰度和效率。下次当你需要处理“最近相关”或“先来后到”的问题时不妨先想想是不是该请出stack或queue这两位老朋友了。

相关新闻

最新新闻

CANNBot直调算子工作流状态结构

CANNBot直调算子工作流状态结构

状态结构 【免费下载链接】cannbot-skills CANNBot 是面向 CANN 开发的用于提升开发效率的系列智能体,本仓库为其提供可复用的 Skills 模块。 项目地址: https://gitcode.com/cann/cannbot-skills .cannbot/state.json 的字段约定,支撑「状态可观测…

2026/7/15 9:39:17
CANN/asc-devkit: asc_copy函数文档

CANN/asc-devkit: asc_copy函数文档

asc_copy 【免费下载链接】asc-devkit 本项目是CANN 推出的昇腾AI处理器专用的算子程序开发语言,原生支持C和C标准规范,主要由类库和语言扩展层构成,提供多层级API,满足多维场景算子开发诉求。 项目地址: https://gitcode.com/c…

2026/7/15 9:39:17
RNN模型训练动态日志监控:Python日志、TensorBoard与回调函数实践

RNN模型训练动态日志监控:Python日志、TensorBoard与回调函数实践

在实际的机器学习项目开发中,尤其是在处理序列数据(如文本、时间序列)的 RNN 模型训练过程中,我们常常会遇到模型训练时间漫长、中间状态难以直观把握的问题。仅仅依赖训练结束后的最终评估指标(如准确率、损失值&…

2026/7/15 9:39:17
51单片机驱动1602LCD:从时序图解析到动态显示实战

51单片机驱动1602LCD:从时序图解析到动态显示实战

1. 1602LCD基础认知与硬件连接1602液晶屏是嵌入式开发中最常见的显示模块之一,它的命名规则很直观——"16"代表每行能显示16个字符,"2"表示共有2行显示区域。我刚开始接触电子设计时,第一个项目就是用这个屏幕做温湿度显…

2026/7/15 9:39:17
CANN/ops-transformer quant_lightning_indexer_v2算子测试框架

CANN/ops-transformer quant_lightning_indexer_v2算子测试框架

quant_lightning_indexer_v2算子测试框架 【免费下载链接】ops-transformer 本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。 项目地址: https://gitcode.com/cann/ops-transformer 功能说明 基于pytest测试框架,实现qu…

2026/7/15 9:39:16
如何快速入门dhara-250m-OptiQ-8bit:在苹果芯片上部署三模式LLM

如何快速入门dhara-250m-OptiQ-8bit:在苹果芯片上部署三模式LLM

如何快速入门dhara-250m-OptiQ-8bit:在苹果芯片上部署三模式LLM 【免费下载链接】dhara-250m-OptiQ-4bit 项目地址: https://ai.gitcode.com/hf_mirrors/mlx-community/dhara-250m-OptiQ-4bit dhara-250m-OptiQ-8bit是一款基于Apple Silicon优化的三模式LLM…

2026/7/15 9:34:16

月新闻