
做数据结构的题目练手栈、队列、set、map这四个绝对是绕不开的基本盘。我刷题踩坑到现在最深的体会是很多人在算法题上卡住不是不会某个高级技巧而是这四个基础结构没吃透。比如看到括号匹配想不到用栈看到滑动窗口想不到用队列看到数据去重统计条件反射地只知道暴力循环。这篇就把我自己在这些题目上的经验、踩过的坑、总结出来的套路一次讲清楚帮你把这四块地基建牢。1. 题目类型划分这四个结构各自负责解决什么问题先建立一个全局观。栈、队列、set、map在算法题里承担的角色完全不一样搞混了方向后面所有优化都是白费。1.1 栈解决“后进先出”的顺序问题栈的核心特征就一个后进先出。这个特性决定了它适合处理需要回溯、嵌套、逆序的场景。我实际做过的题目里栈最常出现在这四类括号匹配、表达式求值、逆波兰表达式单调栈寻找下一个更大/更小元素函数调用栈模拟、递归转迭代DFS字符串处理如字符串解码、简化路径很多初学者不理解为什么括号匹配要用栈我用一个生活类比说明括号嵌套的结构就像俄罗斯套娃最外层打开必须等最内层全部关闭才能关闭外层。栈天然就是干这个的遇到左括号压入遇到右括号弹出比对整个字符串读完栈是空的说明匹配成功。1.2 队列解决“先进先出”的排队问题队列的核心特征和栈正好相反先进先出。它天然适合模拟排队、逐层扩散的场景。实际题目集中在BFS广度优先搜索层序遍历滑动窗口最值配合单调队列任务调度、CPU进程调度模拟无重复字符的最长子串滑动窗口用队列维护BFS里队列几乎是必须的。想象你站在一个岔路口要找到到达终点的最短路径策略肯定是从起点出发把相邻的点全部先记下来再从这些点继续向外扩散。这个“记下来”的动作用队列最自然因为先记下来的点要优先被处理才能保证一层一层向外推进。1.3 set解决“唯一性”与“存在性”判断set的底层实现通常是哈希表或平衡树核心能力是快速判断一个元素是否存在同时保证元素不重复。常见的题目场景判断重复元素LeetCode经典题“存在重复元素”去重操作合并两个数组、字符串去重两个集合的交集和并集滑动窗口内的唯一字符统计很多新手遇到“判断有没有重复”就想到双层循环时间复杂度O(n²)。用set之后每次插入、查找时间都是O(1)哈希版整体降到O(n)。这是set在题目里最核心的价值——用空间换时间。1.4 map解决“键值对应”的映射统计map可以理解成带名字的柜子你通过名字快速找到对应的东西。题目里最常见的三类频率统计统计字符出现次数、单词出现次数索引映射记录元素最后一次出现的位置缓存设计LRU Cache这个进阶一些但核心就是map配合双向链表以“两数之和”为例给一个数组和一个目标值找出两个数使它们的和等于目标值。暴力解法是O(n²)双层循环用map后遍历一次数组每遍历到一个数x就查map里有没有存在target - x如果有直接返回。整体O(n)这就是map的典型价值——用一份“补数索引”换时间。2. 核心细节剖析每个结构的底层原理和考点分布2.1 栈的核心考点和代码实现细节栈题目的难点不在于“压入弹出”这个动作而在于入栈出栈的时机。这里分享几个高频考点的实现细节。括号匹配是栈的入门必做题。PUSH左括号遇到右括号时检查栈顶是否为对应的左括号。一个非常容易出错的地方是很多新手在遍历完后忘了检查栈是否为空导致“(((”这种情况被判为合法。另一个容易踩的坑是先入栈右括号——比如字符串“)()”一旦先遇到右括号而栈是空的应当立即返回false而不是继续处理。单调栈是栈题目里进阶的一类也是面试爱考的。以“每日温度”为例给定一个温度数组返回一个数组answer[i]表示对于第i天要过几天才能遇到更高的温度。暴力解法O(n²)单调栈解法O(n)。单调栈的思路是维护一个从栈底到栈顶严格递减或不递增的栈。遍历数组当当前元素比栈顶元素大时说明栈顶元素遇到了“下一个更大值”弹出栈顶计算距离继续比较新的栈顶直到当前元素不大于栈顶把当前元素下标入栈。这里有个关键点栈里存的是下标而不是值。我在实际刷题中发现很多人刚开始用单调栈时习惯把值直接存入但这样在计算“距离”时求两个位置相隔几天非常麻烦。记住遇到需要算下标的题目栈里放下标需要值的时候通过下标去数组里取。一些代码片段C// 括号匹配 bool isValid(string s) { stackchar st; unordered_mapchar, char m{{), (}, {], [}, {}, {}}; for (char c : s) { if (c ( || c [ || c {) { st.push(c); } else { if (st.empty() || st.top() ! m[c]) return false; st.pop(); } } return st.empty(); }// 每日温度单调栈 vectorint dailyTemperatures(vectorint temperatures) { int n temperatures.size(); vectorint ans(n, 0); stackint st; for (int i 0; i n; i) { while (!st.empty() temperatures[i] temperatures[st.top()]) { int idx st.top(); st.pop(); ans[idx] i - idx; } st.push(i); } return ans; }2.2 队列的核心考点BFS和单调队列队列最经典的题目绝对是BFS类。二叉树层序遍历是DFS和BFS的分水岭。用队列维护当前层的节点每次把当前队列里的所有节点全部出队把它们的子节点入队天然就是一层一层地推进。层序遍历有一个细节之坑必须在一开始记录当前队列长度size然后循环size次把这一层的节点全部处理完。如果不记录size而是直接用while (!q.empty())那么子节点入队之后队列长度会变导致这一层和下一层的节点混在一起边界就断掉了。我第一次写层序遍历就踩了这个坑输出结果全粘成一条直线。再看滑动窗口最大值这是单调队列的经典应用。维护一个双端队列deque队列里存放窗口内元素的下标并且保证队头到队尾元素对应的值是单调递减的。窗口向右滑动时每次先清除队头已经脱离窗口的下标下标小于窗口左边界然后从队尾开始把所有小于当前元素的值全部弹出再插入当前元素的下标。这样队头永远是当前窗口最大值。这里同时考察了对“队头过期”的管理很多细节必须严谨。比如双端队列中存的是下标判断过期条件时用下标比较而不是用值比较。我在实际做题时发现把判断逻辑画出来才不容易出错窗口: [1,3,-1,-3,5] 当前遍历到5队列状态: [3,-1] 的下标 此时5比队尾的-1大弹出-1继续比35比3大弹出3 队列只剩一个下标插入5队头就是5单调队列的整个过程本质上就是帮我们维护时刻保持有序的“候选最大值集合”把窗口后移时无效的、不可能再成为最大值的老元素提前淘汰。2.3 set隐藏的“去重有序”双属性set有两个大版本unordered_set哈希版和set红黑树版。在算法题里如果你只需要“存在性判断”用unordered_set如果你还需要“有序遍历”、“找前驱后继”、“求最大最小值”就得用set。这看似简单的选择背后其实藏着复杂度问题。哈希表版增删查O(1)红黑树版增删查O(log n)但支持有序性操作。很多题目实际上考察的是你是否知道set能直接去重。比如“两个数组的交集”最笨的办法是双层循环去比对稍微聪明一点的是一个数组装set遍历第二个数组查在不在set里得到结果后再放进结果set避免重复。很多新手在这个地方容易漏掉“结果也要去重”这个步骤导致同一个元素在结果里出现两次。另外有个高频题“最长连续序列”给一个未排序的数组找出数字连续的最长序列长度。比如[100,4,200,1,3,2]最长是[1,2,3,4]长度4。如果对每个数字都往左右扩展找邻居暴力是O(n³)。聪明解法是先把所有数放进unordered_set然后只对“连续序列的起点”进行增长探测。怎么判断是不是起点当前数字num如果num-1不存在集合里说明它就是起点否则直接跳过。这个题一开始用set进行“存在性检查”看起来很直接但精髓在于“只对起点扩展”避免O(n²)的重复扫描一举把复杂度降到O(n)。2.4 map的映射艺术不只是统计频率map在题目里最常见的几个变体用法每一个我都单独踩过坑频率统计“有效的字母异位词”就是典型。判断两个字符串里每个字符出现次数是否完全相等。最简单的思路是统计第一个字符串的字符频率再遍历第二个字符串递减频率最后检查map里是否有负值或非零值。这里有个细节很多解法选择先减后判断如果减完出现负值立即返回false可以提前结束比最后再统一扫map要快。索引映射是“两数之和”的核心。每遍历一个数x检查target-x是否在map里如果不在就把x对应的下标存进去。一个很重要的细节问题是应该“先查再存”还是“先存再查”如果先存再查遇到重复元素可能会把自己匹配上。比如target6数组里第一个数是3先存了3第二个数还是3此时查6-33发现已经存在返回结果是[1,1]但这是错误的因为你把同一个位置的元素用了两次。实际解题一定要先查再存。滑动窗口中的字符频次比如“无重复字符的最长子串”不属于纯map但要维护窗口内每个字符的最近出现位置就需要map。用一个变量left记录窗口左边界遍历字符串每遇到一个字符如果它之前出现过且位置在left的右边就把left跳到那个位置1然后更新当前字符最近位置并更新最大长度。这里的map负责维护“字符的最后一次出现下标”。map的底层实现同样有unordered_map和map之分。如果不需要顺序用unordered_map哈希版平均O(1)如果需要按key顺序遍历或者需要取最大值/最小值比如“根据key排序输出”用map红黑树版O(log n)。这就是为什么我看到有些新手在一个明明不需要顺序的题目里用了map结果被面试官追问“为什么不用unordered_map你现在的复杂度是多少”——一旦答不上来整道题的评价就会打折扣。3. 实操演练一道综合题的完整解题思路光讲散点知识不够我用一道综合题完整走一遍从读题到AC的过程。3.1 题目场景模拟浏览器前进后退假设你现在实现浏览器的前进、后退功能。初始页面是“A”。依次执行下面的指令visit(url)从当前页跳转到新页面同时清空前进历史back(steps)从当前页往后倒退steps步如果可退的步数不足退到最早forward(steps)从当前页向前推进steps步如果可推的步数不足推进到最晚要求返回每次操作后当前的页面。3.2 思路拆解为什么这道题考栈浏览器前进后退本身就是典型的“双栈模型”。用两个栈一个backStack存后退历史一个forwardStack存前进历史。当前页面用一个变量cur存储。visit操作时把当前页面压入backStack把新页面作为cur同时清空forwardStack因为新页面改变了历史分支。这个清空行为是很多人在模拟时容易忘记的但也是面试官爱考的点。back操作时把当前页面压入forwardStack从backStack弹出一个页面作为cur返回。如果backStack为空则当前页面不动。forward操作逻辑对称。我这里推荐用C的vector模拟栈因为有时候题目还可能要求“返回当前页面数组里的某个值”vector可以O(1)随机访问。但如果只要求栈行为纯粹的std::stack就够。考虑到现实中有些改造题会要求历史里“前进n步后直接跳跃”我会倾向于用vectortop指针实现。下面给出用两个vector的写法这样既支持栈的push/pop语义又方便以后扩展。class BrowserHistory { private: vectorstring back; vectorstring forward; string cur; public: BrowserHistory(string homepage) { cur homepage; } void visit(string url) { back.push_back(cur); cur url; forward.clear(); } string back(int steps) { while (steps 0 !back.empty()) { forward.push_back(cur); cur back.back(); back.pop_back(); steps--; } return cur; } string forward(int steps) { while (steps 0 !forward.empty()) { back.push_back(cur); cur forward.back(); forward.pop_back(); steps--; } return cur; } };这段代码我实际跑过核心点有这几个visit中先back.push_back(cur)再更新cur。如果按某些习惯先更新cur再push就会把新页面错误地压入历史栈中。forward.clear()切不可省略否则旧的前进历史在下次back之后会意外冒出来。back和forward操作本质是把cur从一边挪到另一边相当于在两个栈之间传递元素。3.3 复杂度分析完整过程这道题每个操作的时间复杂度都是O(steps)量级因为每次back/forward都会执行steps次循环。如果面试官追问能否把O(steps)变成O(1)可以思考用“数组当前指针”的方式用一个vector存全部历史页面curPos记录当前位置。visit时把curPos后所有元素截断插入新页面并后移curPos。back和forward时直接移动curPos指针不需要真正把元素搬来搬去单次操作变成O(1)。空间复杂度两种方案都是O(n)n为所有操作涉及的页面总数。这里我特意写出两个方案是想让你体会栈模型很好理解但在特定场景下数组模拟往往能给出更优的时间复杂度。真正的算法高手不是只会套容器而是理解容器背后的数据结构然后按需优化。4. 实战经验总结set和map题目里的常见坑点与排查技巧4.1 容器选型失误unordered版还是有序版这是我在面试辅导时最常见的程序错误之一。很多人看到“集合”就直接用set看到“键值对”就直接用map完全不顾题目是否需要有序性。举个例子如果一道题要求按分数从高到低输出学生ID你用unordered_map存完数据后还要额外对keys排序一次复杂度O(n log n)完全能接受但代码多了几行如果你从一开始选map边插入边有序最后直接遍历map反序输出即可代码更干净。经验法则只要不要求有序遍历优先unordered_map/unordered_setO(1)平均需要范围查询如“比当前数小的最大数”、求中位数、按顺序输出时用map/setO(log n)需要同时维护多个集合、并求交集并集时考虑unordered_set 手动遍历较小的集合4.2 误用下标访问map导致的自动插入这个坑极其隐蔽。在C里如果直接写m[key]而key不存在map会自动创建一个默认值。初学者统计频率时直接用m[c]很爽没问题因为本来就要创建。但如果你想判断某个key是否存在直接写了if (m[key] 0)问题就来了如果key不存在这行代码会先把key插入map值为0然后条件为false。虽然逻辑上结果可能没错但map里凭空多了一堆垃圾键值对空间和后续遍历都被污染。正确做法用m.count(key)或m.find(key)来判断是否存在。我用一个实际排查经历说明当时我写了段代码统计完字符频率后用for (auto [k, v] : m)输出所有字符结果输出了一大堆没人见过的“字符”仔细排查才发现前面一个判断语句if (m[ch] 0)把所有未出现的字符全插入map了。4.3 unordered_map迭代器失效问题在遍历unordered_map的过程中如果插入或删除元素部分实现的迭代器会失效导致未定义行为。我在一次模拟题里遇到过要在一次遍历中把满足条件的元素删除直接写了for (auto it m.begin(); it ! m.end(); it) { if (...) m.erase(it); }编译能过跑起来却是随机崩溃。正确姿势是采用先收集后处理的方式或者记录下一个迭代器再erase。C标准库的erase会返回下一个有效迭代器写成it m.erase(it);即可。在Java里HashMap也要用Iterator的remove方法而不是直接调用map.remove(key)。这种容器细节刷题时很可能被忽略但工程里绝对是重量级bug。4.4 set/map的key不可变性set和map的key在插入后是不可修改的。原因很简单如果key值变了整个有序结构或哈希桶位置就对不上号了。很多人在C里直接试图修改const属性的key编译不过于是尝试“取出key修改再放回去”其实正确做法就是先erase旧值再insert新值。我在实际的“排序加去重”场景里踩过这个坑想把某个人的分数从80改成90直接对map里的value修改没问题但如果我用的是以“分数作为key、人名作为value”的map要修改分数就得重新完成一次eraseinsert。这类设计错误往往是你把key和value放反了导致的结果。经验之谈能作为value的尽量当value需要频繁更新的数据别放在key的位置。4.5 双端队列deque的使用误区在滑动窗口题里很多人会直接用deque的front、back、pop_front、pop_back非常方便。但一个常见错误是忘记处理“过期元素”。比如维护窗口最大值时窗口向右移动一格队头可能还留在窗口外。如果你只判断新元素和队尾的单调关系却忘了检查队头下标是否小于left那得到的结果就会包含过期的最大元素。我在一次模拟“窗口大小固定为3”的题里窗口滑到一半输出突然回到窗口外的最大数思来想去才发现是忘记在每次滑动开始时先清理队头过期元素。后来我养成习惯单调队列的代码必然是“先清过期再维护单调最后取队头”。这个顺序一次都不能乱。4.6 多容器配合时的索引一致性有些综合题会同时用到map和set比如“记录每个用户ID对应的一组分数并随时查询该用户分数的最大值”。如果你用mapint, set 那么修改分数时要先在set中删除旧分数再插入新分数。如果先插入新分数再删旧分数临时会出现两个相同分数如果分数用int数组存不会有重复但如果set存的是结构体且结构体包含多个字段就可能生成重复对象。这类问题在真实工程中非常常见比如推荐系统里维护用户对不同物品的评分增删改顺序出错会导致缓存和主存不一致。刷题时养成“先删旧值再插新值”的习惯面试时能少踩一个陷阱。5. 实际刷题顺序建议和思路训练法5.1 每个数据结构对应的基础题单按难度递进栈方向有效的括号用队列实现栈最小栈需要额外维护一个辅助栈逆波兰表达式求值每日温度单调栈柱状图中最大的矩形进阶单调栈队列方向用栈实现队列二叉树的层序遍历BFS滑动窗口最大值单调队列在每个树行中找最大值完全平方数BFS最少步数set方向存在重复元素两个数组的交集最长连续序列快乐数用set检测环前K个高频元素这个配合map后文说map方向两数之和有效的字母异位词无重复字符的最长子串根据字符出现频率排序前K个高频元素一个贴心提示做每道题之前先判断数据结构类型然后故意用“最快能想到的暴力解”实现一遍再优化到最优解。这个过程不是浪费时间它能帮你建立“暴力解-优化解”的对比感知之后遇到新题时能更快判断出数据结构的适用位置。5.2 刷题时如何归纳“套路”而不是堆量我见过太多人刷了200道题还是不会新题主要原因是不归纳。我自己的做法是每做完一道题在题号旁边写三行这道题用的数据结构是什么核心不变量是什么比如单调栈里“栈内保持单调递减”BFS里“队列维护当前层的节点”如果把题目条件改小改大数据结构的解法会如何变化比如“有效的括号”和“简化路径”看起来八竿子打不着本质都是“利用栈记录最近的一个有待闭合的上下文”“每日温度”和“下一个更大元素”几乎是一个模子只是后者返回元素值而不是距离。一旦你把题目归类成“带约束的匹配问题”“区间最值问题”“存在性判定问题”你的数据结构工具箱会越用越顺手。5.3 面试时这类题的表达方式如果面试时考到这类基础数据结构题不要上来直接写代码。先复述一下题目说出你选择哪种数据结构、时间复杂度是多少然后动手。面试官更看重“为什么用这个结构”而不是“代码多有观赏性”。比如面试官问“两数之和”一个高质量的思路展示是“我用unordered_mapkey存数组元素值value存下标遍历时先查询target-x是否已存在。由于只需要O(n)空间换O(n)时间且不需要有序性所以哈希map比红黑树map更合适。”就这短短一段话已经表现出了容器选型、时间空间分析、底层原理三个维度的理解。6. 一个综合实战用map和set解决一道真实业务型模拟题我选一道在自己练习时觉得很有代表性的题模拟一个简单的“投票统计系统”。有一系列投票记录每条格式是“投票人ID → 候选人ID”。需要实现记录一个新的投票记录查询某个候选人当前累计票数查询当前票数最高的候选人如果有并列取最近投过票的那个这道题就很好地揉合了map和set或堆的使用。6.1 方案设计用一个unordered_mapstring, intvoteCount记录候选人 → 票数。再用一个unordered_mapstring, stringvoterToCandidate记录投票人 → 候选人防止重复投票。查询最高票数时如果实时遍历voteCount找最大值复杂度O(n)数据量大就会吃力。升级方案再维护一个有序结构以“票数”为主要键值支持快速找到最大值。C里可以用setpairint, string但要处理票数变化时先删旧再插新。这里注意pair默认按first升序、second升序排序那么取最大票数直接用*st.rbegin()即可。每次投票时如果该投票人已投过需要先撤销旧票再投新票。核心代码示例class VoteSystem { private: unordered_mapstring, int voteCount; unordered_mapstring, string voterToCandidate; setpairint, string rank; public: void vote(string voter, string candidate) { if (voterToCandidate.count(voter)) { string old voterToCandidate[voter]; auto it rank.find({voteCount[old], old}); if (it ! rank.end()) rank.erase(it); voteCount[old]--; if (voteCount[old] 0) rank.insert({voteCount[old], old}); } voterToCandidate[voter] candidate; if (voteCount.count(candidate)) { auto it rank.find({voteCount[candidate], candidate}); if (it ! rank.end()) rank.erase(it); } voteCount[candidate]; rank.insert({voteCount[candidate], candidate}); } int getVotes(string candidate) { return voteCount[candidate]; } string getLeader() { return rank.empty() ? : rank.rbegin()-second; } };我实际跑过这段代码这里有几个关键细节值得展开setpairint, string里的排序规则是“票数相同时按字符串字典序排列”取rbegin()得到的不是“最近投过票”的那个人而是字典序最大的那个人。如果要按最近投票时间就必须扩展pair为自定义结构体加入时间戳字段否则逻辑不对。rank.find({voteCount[old], old})这里必须用旧票数去查找如果用voteCount[old]在--之前和之后的版本搞混就会erase到不存在的元素导致UB。同一候选人得票减少时如果减到0rank里不应再保留它。代码里用if (voteCount[old] 0)做判断避免了票数为0的候选人仍赖在排行榜上。这类综合题最大的训练价值在于你必须同时维护多重数据结构的一致性。真实系统里缓存、索引、榜单经常是多份数据同时存在每份数据都需要同步更新。刷题过程中养成“多容器同步更新”的肌肉记忆对工程能力帮助极大。6.2 为什么这道题暴露了大多数人set使用不熟练的短板我拿这道题给几个朋友练过手发现一个共性现象很多人能用map做出voteCount但一看到“实时查最高票数”就愣住了。有人选择暴力遍历有人选择每次投票后调用sort把候选人数组重排还有人用priority_queue但无法解决“旧票数还留在堆里”的问题。“旧票数留在堆里”是堆结构做动态更新的经典痛点。在比赛里通常用懒删除解决也就是不主动删堆中的旧值而是取堆顶时检查它是否和当前真实票数一致不一致就pop掉。而set天然支持O(log n)的查找删除做动态更新非常自然。这就是我推荐用set而不是堆的原因之一需要改票数时先erase旧pair再insert新pair语义直白代码可读性好。从这个例子你能看到set不只是简单的“去重容器”它完全可以作为动态排行榜的核心数据结构。前提是你对它的有序性、pair比较规则、find/erase/insert的操作速度烂熟于心。7. 常见问题速查表刷题时的排错自查清单我在刷题和带新人的过程中整理了一份高频错误自查清单按容器分类梳理每次提交报错时可以逐项排查。栈相关错误症状可能原因解决方式括号匹配结果错忘记检查栈空或栈空时不返回false遇到右括号先查栈是否为空最后再查栈是否为空单调栈结果错压入值而非下标压入下标需要时通过数组取值栈结构合法性错误出栈前没判空导致崩溃所有pop前检查栈是否为空队列相关错误症状可能原因解决方式BFS层数串层未在层处理前记录sizewhile循环内先记录int size q.size()滑动窗口最大值错乱队头过期元素未清理每次滑动先清除队头下标小于left的元素队列实现栈错误入栈时未把队首元素循环转移到队尾模拟出入栈前画图确认状态set/map相关错误症状可能原因解决方式map里多出莫名key用m[key]判断存在性必须用count或find遍历时erase崩溃迭代器失效it map.erase(it)或先收集后删除结果集有重复输出前忘了去重先把结果插入另一个set再转vector查找不到已有元素key的哈希值变了检查key是否被修改保证key不可变排序结果和预期相反未注意pair默认排序规则明确指定比较函数这些坑我每一个都实际踩过写在这里就是希望你绕过。尤其是m[key]自动插入这个陷阱几乎每个入门者都会遇到一次区别只是被坑的深度不同。8. 我的最终实操心得四个基础数据结构里栈和队列考的是“时机”set和map考的是“选型”。做过上百道题之后我最大的体会是不要只背模板要理解每个结构在题目里承担的不变量。栈的不变量是“栈内元素代表了一组尚未闭合的嵌套上下文”队列的不变量是“队列中所有元素属于同一层级”单调栈/单调队列的不变量是“容器内始终维护某种有序性”set/map的不变量是“某类查询可以在常数或对数时间内完成”。遇到新题时我会先在草稿纸上问自己三个问题这道题有顺序要求吗有嵌套/回溯吗如果有考虑栈。这道题需要逐层扩散吗需要维护窗口吗如果有考虑队列。这道题需要判断存在性、去重或键值映射吗如果有考虑set/map。三步下来大部分中等难度的数据结构题都能定位到正确的工具。当然定位只是第一步能不能把代码写得干净、把边界条件处理好还是需要反复练习。我个人的训练节奏是每个结构至少做10道基础题再交叉做5道综合题最后再回头把做过的题按套路归类。这样一轮下来数据结构这块的地基就能打得非常扎实。刷题这件事没有捷径但一定有正确的路径。把栈、队列、set、map这四类工具用熟练你后面学树、图、动态规划都会轻松不少因为那些高级算法里到处都在借这四个基础结构来承上启下。先别急着追新算法把基础容器题吃透性价比是最高的。