
1. 数据结构是什么为什么每个程序员都绕不开它我第一次接触数据结构这个词是在大二的数据结构课上当时完全不明白这门课到底在讲什么。链表、栈、队列、二叉树每一个概念都抽象得要命考试前背了一堆定义考完就忘。直到后来真正开始写项目、做系统、处理海量数据才意识到数据结构不是一门背概念的课而是一套解决实际问题的思维工具。说得直白一点数据结构就是计算机存储、组织数据的方式。同样是存一组数字你可以用数组、链表、树、哈希表不同的存法对应不同的增删改查效率。你玩手机时通讯录按字母排序能快速找人背后是某种查找树或者跳表你刷短视频时推荐流一条条往下滑背后是队列和缓存的配合你点外卖时商家按距离排序展示背后是排序算法和图的路径计算。数据结构无处不在只是大多数时候你感知不到它。这篇文章适合谁看如果你是刚接触编程的学生准备考研408或者在工作中被算法题折磨的开发者都可以读一读。我会从最基础的概念讲起逐步拆解各种常见数据结构的设计思路、适用场景、优缺点最后附上我这些年踩过的坑和排查经验。不讲废话直接上干货。2. 数据结构整体设计与核心概念拆解2.1 逻辑结构和物理结构先分清这两个层次学习数据结构第一步不是急着背代码而是搞清楚两个层次的概念逻辑结构和物理结构。逻辑结构描述的是数据元素之间的抽象关系也就是在脑子里怎么理解这些数据。常见的逻辑结构有四种集合结构数据之间没有关系只是属于同一个集合、线性结构一对一像排队、树形结构一对多像家谱、图形结构多对多像社交网络。物理结构则是数据在计算机内存里实际存储的方式只有两种顺序存储和链式存储。我当年最大的误区就是把这两个层次混在一起。比如看到线性表就以为必须是数组其实线性表是逻辑上的概念它既可以用数组顺序存储实现也可以用链表链式存储实现。这个区分很重要因为同样的逻辑结构选择不同的物理存储性能表现天差地别。顺序存储在内存里是一块连续的地址访问某个位置的时间复杂度是 O(1)但插入和删除需要移动大量元素链式存储通过指针把零散的内存串起来插入删除只要改指针就行但访问某个位置必须从头遍历时间复杂度是 O(n)。用一个生活化的类比来解释数组就像电影院里的连排座位每个人都固定在某个位置你买票时直接说第5排第3座座位号就是地址所以找位置极快。但如果你要临时加一个人进来可能整排人都得挪位置。链表就像手拉手排队的小朋友每个人只记住后面是谁队伍不需要站在一起加一个人进来只需要让前面的小朋友改拉这个新人快得很。但如果你想找队伍里的第10个人只能从第一个开始一个个数过去。2.2 线性表、栈、队列三个最基础的线性结构线性表是最基础的数据结构它描述的是一对一的线性关系。按照物理存储方式的不同分为顺序表和链表两大类。顺序表就是数组逻辑上相邻的元素在物理地址上也相邻随机访问快但插入删除需要移动元素扩容时还可能涉及整块内存复制。链表则包含单向链表、双向链表、循环链表等变体通过节点之间的指针连接插入删除操作快已知位置的前提下但查找某个元素需要遍历而且每个节点还要额外存储指针占用更多内存。栈和队列是两种特殊的线性表。栈只允许在一端进行插入和删除操作这个端叫栈顶它的特点是后进先出LIFO就像叠盘子你最后放上去的盘子最先拿走。函数调用的递归过程、浏览器的前进后退、编辑器的撤销操作底层都是栈。队列则相反只允许在一端插入、另一端删除先进先出FIFO就像银行排队叫号先来的人先服务。消息队列、任务调度、打印机缓冲池这些场景都是队列的经典应用。我在实际工作中用过双端队列Deque解决过一个很实际的问题——滑动窗口最大值。当时需要在一个不断变化的数组上实时计算窗口内的最大值如果每次都重新遍历窗口时间复杂度是 O(nk)数据量一大就卡。用双端队列维护一个单调递减的索引队列每次窗口滑动时从队尾淘汰较小元素从队头移除过期元素时间复杂度降到 O(n)。这个场景在热词里也出现了双端队列它确实是刷算法题时的高频考点也是工程中处理流式数据的神器。2.3 树、图、哈希进阶结构解决复杂问题当数据关系从一对一升级到一对多就需要树结构。二叉树是最常见的树结构每个节点最多有两个子节点。二叉搜索树BST的规则是左子树所有节点值小于根节点右子树所有节点值大于根节点因此查找效率可以达到 O(logn)。但如果插入顺序不好BST会退化成一条链查找效率掉到 O(n)。为了应对这个问题平衡二叉树AVL和红黑树被设计出来通过旋转操作保持树的平衡保证查找效率稳定。堆是一种特殊的完全二叉树它常常被用来实现优先队列。最大堆的根节点是最大值最小堆的根节点是最小值。我写任务调度器的时候需要按照任务的优先级依次处理如果每次都用普通数组找最大优先级效率太低用最大堆只需要 O(1) 时间取最大值插入和删除也都是 O(logn)整体性能提升非常明显。堆排序也是基于这个结构时间复杂度 O(nlogn)而且是原地排序只需要常数级别的额外空间。图结构处理的是多对多的关系。社交网络的人与人关系、地图导航中的路网、推荐系统中用户与物品的交互都可以用图来表示。图的存储有两种方式邻接矩阵和邻接表。邻接矩阵用二维数组存储判断两点之间是否有边只需要 O(1)但稀疏图会浪费大量空间邻接表用链表数组存储节省空间但查找边的效率稍低。广度优先搜索BFS和深度优先搜索DFS是图的基础遍历算法最短路径问题则需要 Dijkstra 或 Floyd 算法。哈希表可能是应用最广的数据结构。它通过哈希函数把键映射到数组下标理想情况下查找、插入、删除都是 O(1)。但哈希冲突是绕不开的问题常见处理方式有开放寻址法和链地址法。Java 的 HashMap 用的就是链地址法当链表过长时还会转成红黑树来优化性能。我工作中经常用哈希表做去重和快速查找比如从一堆日志里统计每个 IP 的出现次数哈希表的效率远超其他结构。设计哈希函数时要尽量避免碰撞好的哈希函数能让数据均匀分布否则哈希表会退化成链表性能急剧下降。3. 核心数据结构实操要点与避坑指南3.1 数组和链表怎么选关键看操作场景数组和链表是两种最基础的存储结构也是很多复杂结构的基础。选型时不能只看理论上的复杂度还要结合实际情况。数组的优势在于随机访问快按下标取元素 O(1)CPU 缓存友好因为数据连续存放内存预读效率高不需要额外存储指针节省内存数组的劣势在于插入和删除需要移动大量元素平均 O(n)扩容需要重新申请内存并拷贝数据固定大小的数组存在空间浪费或不足的问题链表则相反插入删除快已知节点位置不需要连续内存天然支持动态扩容但随机访问慢且每个节点需要额外存储一个或两个指针。我的经验法则是如果主要操作是读选数组如果主要操作是写插入删除选链表。但实际工程中往往比这复杂比如很多场景需要既能快速读又能灵活写这时候就要考虑更复杂的结构比如跳表、平衡树或者用数组加索引的方式做折中。有一个坑我必须提醒链表的插入删除操作已知位置和未知位置是两个完全不同的场景。如果你只知道链表头部节点却要删除第5个节点还是得先遍历找到第5个节点整体复杂度依然是 O(n)。所以链表的优势只有在你能直接拿到目标节点的引用时才真正成立。很多初学者看到链表插入 O(1)就以为链表天下无敌实际用起来找节点就废了半条命。3.2 栈和队列的实现细节别忽视这些细节栈和队列在实现上比较简单但有几个细节值得注意。栈用数组实现时需要注意栈顶指针的初始值设为 -1 还是 0这直接决定后续代码的写法。我看到很多初学者在这里栽跟头如果初始为 -1入栈就是先加后存如果初始为 0入栈就是先存后加。这种约定没有对错之分但一定要统一清晰。用链表实现栈时应该把链表头作为栈顶这样入栈出栈都是 O(1)。如果非要把链表尾当栈顶每次还要遍历找到尾节点那就亏大了。队列用数组实现时面临一个假溢出问题出队后队头指针后移导致队头之前的空间无法复用即使数组还有很多空间队列却显示满。解决办法是循环队列让队头和队尾在数组里绕圈。实现循环队列的关键是区分空和满很多教科书采用牺牲一个存储单元的方式也就是队列满的条件是 (rear 1) % capacity front。还有另一种方法是用 size 变量记录元素个数满了就是 size capacity我就喜欢这种方式判断条件更直观代码也不容易出错。用链表实现队列时需要维护两个指针头指针指向队头用于出队尾指针指向队尾用于入队。这里有个经典错误很多初学者只用一个头指针入队时要从头遍历到尾导致入队变成 O(n)。正确做法是同时维护 tail入队时把新节点接到 tail 后面再更新 tail。在实际项目中我一般优先使用现成库的栈和队列比如 Java 的 ArrayDeque、Python 的 collections.deque它们已经经过高度优化没必要自己重复造轮子。但理解底层原理仍然很重要一方面方便排查问题另一方面面对特殊需求比如固定容量队列、无锁并发队列时知道该从哪个方向改。3.3 树结构遍历的四种方式非递归怎么写才能不犯错二叉树的遍历是树结构里最常考的知识点。前序遍历、中序遍历、后序遍历三种深度优先遍历以及层序遍历广度优先遍历每种遍历的递归写法都很简洁但面试和实际工作中经常要求写非递归版本。递归版本的核心是理解访问根节点的时机前序遍历先访问根节点再递归左子树再递归右子树中序遍历先递归左子树再访问根节点再递归右子树后序遍历先递归左子树再递归右子树最后访问根节点非递归版本需要显式使用栈来模拟递归过程。前序遍历是最简单的根节点入栈出栈即访问先压右孩子再压左孩子因为栈是 LIFO要保证左孩子先被访问。中序遍历稍复杂从根节点开始一路向左把路径上的节点全部入栈直到没有左孩子然后出栈访问转向右子树继续同样的过程。后序遍历的非递归写法最麻烦常见做法是使用两个栈或者用一个栈加上上一次访问的节点来标记。层序遍历使用队列根节点入队循环出队访问同时把左右孩子入队直到队列为空。这个过程天然符合队列的先进先出特性配合前面的循环队列实现可以高效完成。写非递归遍历时最常见的问题是指针走到 null 就不知道怎么办了。我的经验是在循环体内先判断栈是否为空再考虑当前指针是否为空两个条件配合好代码逻辑就清晰了。还有一种更省心的办法用栈存 Node 对象每次出栈时同时附带状态标记虽然多占点空间但写起来不容易出错适合作为调试阶段的过渡方案。3.4 图的最短路径Dijkstra 和 Floyd 怎么选图论中最常碰到的实际问题是最短路径。单源最短路径常用 Dijkstra 算法它要求边的权重为正数。算法的核心思想是贪心每次从未访问节点中选出距离起点最近的点用这个点去松弛它的邻居重复直到所有节点都被访问。时间复杂度取决于实现方式朴素实现是 O(V²)使用最小堆优化后是 O((VE)logV)后者在稀疏图上更有优势。多源最短路径常用 Floyd 算法本质是动态规划用三重循环遍历所有中间节点。时间复杂度 O(V³)空间复杂度 O(V²)适合节点数不多的稠密图。我记得在做一个物流路径规划系统时城市节点只有两百多个用 Floyd 一次算出所有城市之间的最短距离后续查询全部变为 O(1)。如果追求极致性能也可以用 Dijkstra 跑 N 次但代码复杂度会高很多在节点少时 Floyd 的优势是简单可靠。使用 Dijkstra 时有几个容易踩的坑一是把未访问节点中距离最小错误地理解成所有节点的全局最小导致算法提前结束二是松弛操作忘记更新优先队列中已有的元素或者重复入队导致某些节点距离被算错三是没有考虑两条边权重一样的情况但这不算错误只是结果可能有多种。如果你面对的场景包含负权边Dijkstra 就不适用了要用 Bellman-Ford 算法能检测负环或 SPFA队列优化的 Bellman-Ford。在工程中遇到负权边的概率不大但做算法题时经常作为考点出现需要留意。4. 从理论到实战常见数据结构应用场景实录4.1 字符串匹配与回文判断栈和双端队列的表演时间字符串处理在面试和工作中都是高频需求。判断括号是否匹配这是栈的经典应用题遇到左括号包括方括号、花括号就入栈遇到右括号就检查栈顶是否是对应的左括号匹配则出栈不匹配则说明字符串非法。一个很容易错的点是怎么处理字符串中途就栈空了的情况比如([)]这种交错匹配直接判断右括号时栈为空就返回 false能提前结束程序。回文判断也可以用双端队列实现从两端同时取元素比较是否相等遇到不同就说明不是回文。天生适合双端队列因为回文的定义就是两端对称。这几处代码看起来简单但我建议你亲手敲一遍尤其是考虑字符大小写、去空格、去标点这类细节能在实际操作中加深理解。如果扩展到字符串的子串查找朴素的逐位比较最坏情况下是 O(n*m)n 是主串长度m 是模式串长度。KMP 算法通过计算 next 数组部分匹配表避免重复匹配把复杂度降到 O(nm)。理解 next 数组的构建是 KMP 的难点很多人死记硬背代码一旦让写 next 数组就崩溃。我的建议是下次不行就当前缀和后缀的最长公共长度来推导可以从babad这种例子一步步算等算多了自然就记住代码逻辑了。4.2 Top K 问题与堆排序大数据量下的取舍之道从海量数据里找出最大的 K 个这类 Top K 问题在实际工作中出现频率很高。最朴素的方法是排序之后取前 K 个时间复杂度 O(nlogn)如果只用小顶堆维护 K 个元素每来一个新元素就和堆顶当前第 K 大的数比较大于堆顶就替换并调整堆复杂度变为 O(nlogK)。K 远小于 n 时堆方案优势明显。我做过一个实时日志打点系统每秒要处理几十万条请求需要实时统计访问量前 10 的 URL。如果用全量排序每来一批数据就排一次系统必然卡死。后来改为小顶堆方案堆里固定放 10 个元素来时跟堆顶比大小堆顶一直是最小的所以当新元素比堆顶还小就直接丢弃否则进堆同时弹出堆顶。整个过程中堆只维护 10 个元素内存占用极小处理速度飞快。堆排序也是面试常考点把数组调整成大顶堆把堆顶最大值和数组末尾交换再对剩下的部分重新调整堆。时间复杂度 O(nlogn)而且是原地排序。代码实现中的关键在于调整堆heapify操作要写对尤其是边界条件下标要算清楚我初学时经常因为少写一个等号导致排序结果不对。排序算法里面快排的平均复杂度也是 O(nlogn)但最坏情况下会退化到 O(n²)。快排在工程中应用极广大多数语言的 sort 方法底层都混合了快排、插入排序和堆排序比如 Java 的 Dual-Pivot Quicksort 和 TimSort。理解排序算法的本质有助于你应对不稳定的排序会改变相等元素的相对顺序这类边界问题。4.3 缓存淘汰策略 LRU哈希表加双向链表的组合拳LRULeast Recently Used缓存淘汰策略是面试高频题也是工程中的常见需求。每个缓存系统Redis、CPU 缓存为了保持内存不膨胀需要淘汰最久没被使用的数据。LRU 的策略是每次访问某个数据就把它提到最前面最新使用容量不够时从尾部淘汰最久没用的数据。实现 LRU 最优雅的数据结构组合是哈希表 双向链表。哈希表提供 O(1) 的查找能力双向链表提供 O(1) 的移动和删除能力。具体结构是哈希表的 key 对应到双向链表中的某个节点双向链表头表示最近使用的数据尾表示最久未使用的数据。访问一个 key 时先在哈希表里找找到了就把它对应的节点移到链表头插入新 key 时如果容量满了先删掉链表尾和哈希表里对应的项再把新节点插入链表头。这里有个细节为什么要用双向链表而不是单向链表因为当我们要把一个节点移到链表头时需要知道它的前驱节点。用单向链表时为了找到前驱又得从头遍历那就变成 O(n) 了。而双向链表天然存储了前驱指针移动节点只需改动几个引用就行。这个设计上的取舍很能体现数据结构选型的精髓也是很多面试官考察的重点。我在实际做缓存工具时就按这个思路实现过一个精简版 LRU代码不复杂但用起来很顺手。需要注意的坑有重复 key 的情况要先把旧节点从链表里摘下来再插到头部避免出现重复节点哈希表里存的应该是指向节点的引用而不是值拷贝否则移动节点时值不同步。如果使用现成库Java 的 LinkedHashMap 稍微改一下 removeEldestEntry 就能实现 LRUPython 的 OrderedDict 也有类似功能。5. 数据结构常见问题与排查技巧实录5.1 遍历树时总是死循环可能是少改了 visited 标记图的遍历和树的遍历最大的不同是树天然没有环路严格说树是连通无环图但图可以有环。如果直接用 BFS/DFS 遍历图而不做标记就会陷入死循环——从一个节点出发遍历邻居邻居又绕回这个节点无限递归。解决方法是维持一个 visited 集合或数组每次访问一个节点时就把它标记为已访问只有未访问过的邻居才继续递归或入队。使用递归实现 DFS 时标记位置要在递归调用前设置如果递归前没设标记、而是递归内头一步设标记可能导致某个节点被重复入栈多次。我调试这种 bug 时喜欢额外加一个染色法用三个状态标记未访问、访问中、已完成能发现递归调用栈的相互依赖。在刷题平台里如果 DFS 递归突然超时十有八九就是 visited 标记没写对可以先打印所有访问过的节点确认一下。另一个隐藏坑是visited 用哈希表记录的是值还是节点引用。如果图上存在两个值相同但节点不同的节点用值做 key 就会误判为已访问正确做法应该用节点 id 或对象引用。5.2 哈希表冲突严重性能突然下降怎么排查正常情况下哈希表的基本操作是 O(1)但一旦遇到大量哈希冲突链表就会变长查询效率退化到 O(n)。最经典的场景是有人恶意构造哈希值相同的 key比如很多语言里字符串的哈希算法可以碰撞在极端情况下甚至会造成拒绝服务攻击。排查哈希表性能下降的步骤先看加载因子默认 0.75是否设置合理如果表太小、元素太多应该扩容观察链表长度分布如果某个桶后面挂了几千个元素说明哈希函数有严重问题检查哈希函数是否均匀尤其对于自定义对象最好让 equals 和 hashCode 保持一致性检查是否存在大量等值对象Equals 逻辑不对会导致哈希表无法正确区分元素在这些情况里我遇得最多的是自定义对象没重写 hashCode。Java 的 HashMap 会调用默认的 Object.hashCode()如果两个对象内容相同但内存地址不同就会得到不同的哈希值HashMap 里两个相同的对象都能存进去逻辑就乱了。记住重写 equals 的类必须同时重写 hashCode这是一条硬规矩。5.3 链表反转为什么老出错画图模拟是唯一的捷径链表反转是面试经典题也是很多人第一个写不对的题。用迭代法反转链表时核心是维护三个指针prev、current、next。每轮循环先用 next 保存 current.next不然断了后面的节点就找不到了再把 current.next 指向 prev完成反转然后 prev 和 current 都往后移动一步这个题最容易错的地方是循环结束后指针的最终位置以及边界条件比如空链表、只有一个节点。我学它的时候总是背代码一背就忘后来改成画图模拟每执行一步就画出三个指针指向哪里几十张图画完就彻底理解了。这个方法同样适用于二叉树旋转、图的深度优先遍历、复杂链表的复制等所有指针操作密集的题。递归写法理解起来更直观递归解决的是把当前节点之后的部分反转再把当前节点接到反转后链表的末尾边界条件是最后一个节点直接返回。但递归方法在链表非常长比如千万级节点时有栈溢出风险工程上优先使用迭代写法。6. 数据结构学习路线与刷题避坑心法6.1 新手起步先掌握这些核心概念再动手如果你是一个刚开始学习数据结构的新手我的建议是不要一上来就刷题或者背代码先把核心概念串起来。第一步理解逻辑结构和物理结构的区别。这一步直接决定了你后面能不能分清楚抽象和实现之间的差别。第二步把线性表、栈、队列三种线性结构吃透不仅会写代码还要能自己画图模拟每一步操作。第三步学习二叉树及相关操作遍历、翻转、最近公共祖先再到堆和优先队列。第四步学习图的表示法和遍历算法重点理解 visited 标记的思想。第五步学习哈希表重点理解哈希函数和冲突解决策略。在这个过程中每学完一个结构就问自己三个问题这个结构解决了什么问题它的核心操作时间复杂度是多少什么场景下应该选用它而不是别的结构能回答清楚这三个问题说明真理解了。热词里有数据结构c语言版数据结构cpython数据结构说明很多人都关心用哪种语言学。说实话语言不是核心障碍数据结构的思想是跨语言的。C 语言能让你看到指针和内存分配的本质Python 写起来最省心适合快速验证思路Java 在工程上用得最多。我的建议是先专心用一门语言把原理弄懂再抽空用第二门语言实现一遍同样的结构这个过程能帮你理清哪些是语言特性、哪些是数据结构本身的内在逻辑。6.2 考研 408 和期末复习怎么高效利用真题和笔记如果你的目标是考研 408 或者期末复习你需要一个更应试的策略。408 的数据结构部分分值高、记忆点多但大题的考点相对固定。复习主线是先把教材比如王道或者李春葆的《数据结构》认真过一遍每个数据结构都按定义、存储结构、基本操作、复杂度分析、典型应用这五个维度整理笔记。整理笔记的过程比笔记本身更重要因为你需要主动回忆和归纳。其次是做题。真题比模拟题更有参考价值尤其近五年的真题你要研究它考了哪些结构、哪些算法、考察角度是什么。很多学校的期末题都来源于作业题库比如电大数据结构本形考作业把这些作业反复做到自己能闭卷写出核心代码为止。李春葆《数据结构》的第五版学习指导勘误汇总在网上流传很广我当年就见过很多学习者整理过勘误文档因为教材第一版印刷偶尔有笔误对照勘误学习能避免被带偏。还有一点编程题写不出来时不要急着看答案先动手画过程图把每个变量在每个步骤的值写出来基本就能找出卡住的地方。6.3 面试刷题别只会套模板要会推导复杂度刷题和做工程题不太一样面试官更看重你分析问题、选择数据结构、推导复杂度的能力。很多同学上来就问这个题用什么解法这是误区。正确流程应该是先分析题目给出的数据范围推断期望的时间复杂度根据期望的复杂度思考可能的数据结构比如 O(logn) 查找想树O(1) 查找想哈希表O(n) 排序想快排写出思路后分析时间和空间复杂度和面试官确认再动手写代码这个流程在面试中很加分因为它展示的是工程思维而非背诵能力。我在实际刷题时发现自己推导一遍复杂度比看十道题的解析都有助于加深理解。特别是空间复杂度这个维度很多人只关注时间其实面试官经常会追问你能不能用常数级额外空间完成它这时候对数据结构存储机制的理解就派上用场了。6.4 从校园走向工程数据结构到底怎么应用到真实项目很多学生觉得数据结构和真实项目隔得很远其实完全不是。我在工作中遇到的最常见的数据结构应用包括用哈希表做用户会话管理O(1) 时间判断用户是否登录用队列做消息缓冲削峰填谷用优先级队列做任务调度保证紧急任务优先处理用树结构存储组织架构或分类目录便于层级查询用图结构做推荐系统里用户和物品的关联分析用跳表或者 B 树做数据库索引能很快范围查询可以说你写的每个稍微有点规模的程序里都有数据结构的影子。有一次我优化一个接口从 O(n²) 降到 O(nlogn)改造的关键不过是把嵌套循环里的线性查找换掉用一个映射表做索引。收益立竿见影接口响应时间从 3 秒降到 100 毫秒以内。工程中还有一个容易被忽略的点是数据结构的组合。单个结构往往不够用需要多个结构配合比如 LRU 是哈希表加链表数据库索引是 B 树加链表推荐系统是图加矩阵。你在学习每个数据结构时要有意识地问自己这个结构和其他结构组合起来能做什么 这样训练一段时间后设计系统时你就会很自然地想到各种结构的搭配而不是只会用数组硬写。7. 写在最后的几点建议回头看来数据结构学习最难的不是某个算法记不住而是建立起用结构的眼光看问题的思维方式。我踩过的坑包括只背代码不画过程图、只顾着看题解不自己推导复杂度、写链表题目不关注空指针边界、在图遍历里忘记标记已访问节点。每一个坑都让我多浪费了不少时间。如果你也想学好数据结构我给你几条最朴实的行动建议第一别怕慢先把一种结构彻底搞懂再学下一种。很多人数据结构没学好是因为他们 数组还没玩明白就去啃红黑树。第二一定要亲手实现一遍。哪怕代码写得再丑也比看十遍书有用。你可以把一个结构用两三种不同方式实现然后比较它们的代码差异和性能差异这个过程能让你真正理解为什么这么设计。第三做题时用一张白纸画过程图。无论是链表反转、树的遍历还是图的搜索画图都能让你更快定位到逻辑漏洞。第四多看多调多测。最终还是要落到实际调试中你会不断发现理论之外的边界问题。数据结构这门课会陪你很长的路甚至整个程序员生涯都会一直用到它。慢慢来把基础夯实后面越走越快。