ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

数据结构从入门到实战:线性表、树、图与算法核心脉络

数据结构从入门到实战:线性表、树、图与算法核心脉络 数据结构这东西我接触过太多人了不管是刚上大学的科班生还是半路转行的自学者十有八九都在这里卡过壳。一说起数据结构大家第一反应就是严蔚敏那本C语言版教材、408考研真题、期末考卷上的算法大题再往后就是刷题时被链表反转和动态规划按在地上摩擦。这些印象都没错但问题也恰恰出在这里——很多人把数据结构当成了一门需要背代码、背概念的应试科目结果学完一本书代码抄了好几遍真正遇到实际问题的时候还是不知道用什么结构。这篇文章我想换个角度聊。不是给你罗列各种定义也不是把每段代码从头到尾注释一遍而是把数据结构这门课从基础到高级的完整脉络捋清楚包括每个结构到底解决什么问题、为什么这么设计、你实际写代码的时候会遇到哪些坑以及怎么选教材、怎么复习、怎么把它用到真实项目里。不管你是正在学这门课的大学生还是准备考研408、准备面试的求职者又或者只是想把自己的编程基本功补齐这篇内容应该都能让你少走一段弯路。1. 数据结构到底是门什么课1.1 先看它解决什么问题学数据结构之前先想清楚一个问题程序是什么说白了就是“数据 算法”。数据就是你要处理的东西算法就是处理的方法而数据结构就是中间那一层——数据到底怎么组织、怎么存放、怎么被算法高效地访问。举个例子你就明白了。你有一堆学生信息有姓名、学号、成绩。如果把这些信息随便扔在一个数组里要找某个学号对应的学生只能从头到尾挨个比对数据多了就非常慢。但如果你把数据按照学号排好序或者用哈希表按学号建立映射查找速度就会有质的提升。这就是数据结构的作用它决定了一个算法能跑多快、能省多少内存。所以说白了数据结构不是什么高深莫测的数学理论它就是“数据存储的方式”。数组、链表、栈、队列、树、图、哈希表这些都是不同的存储方式每种方式都有它的脾气有的适合查找有的适合插入删除有的适合表达层级关系。你学这门课的任务就是把这些“脾气”摸清楚。1.2 它和算法是什么关系热词榜里天天看到“数据结构与算法”绑在一起出现很多人也搞不明白这俩到底谁是谁。我的理解是这样的数据结构是舞台算法是剧本。舞台搭得越合理你跑代码的时候就越顺畅。比如二分查找这个算法它的前提是数据必须有序。你如果用的是普通的无序数组二分查找根本没法用这就是数据结构对算法的约束。反过来如果你设计了一种“跳表”结构那查找算法就可以在一串普通链表上做到近似二分的效果这又是数据结构对算法的赋能。所以学习的时候一定要带着这个思路去看每一种数据结构出现通常是为了配合某类算法或者解决某类特定问题。比如栈结构天然配合递归和回溯队列结构天然配合广度优先遍历堆结构天然配合Top K问题。把这个对应关系记住了整个数据结构的知识体系才算串起来了。1.3 没有C语言基础要不要紧国内很多经典教材都是用C语言描述数据结构考研408也默认考察C语言版的代码理解。很多自学者就会纠结一个问题我不懂C语言能不能直接学数据结构我的建议是数据结构用C语言学一遍是值得的。原因不在于C语言本身有多厉害而在于C语言足够底层没有那么多封装指针就是地址结构体就是内存布局你能特别清楚地看到每个元素是怎么在内存里存下来的。换句话说用C语言学数据结构你能看到“数据结构”这四个字里“结构”的部分而在Java、Python里你更多看到的是现成的类库。如果你真的完全没有C基础那最稳妥的路径是先花一到两周把指针、结构体、动态内存分配这几个点学会然后跟着教材手写代码。不需要C语言掌握得有多深能看懂、能写出链表和树的基本操作就够了。2. 基础篇线性表、链表与顺序表2.1 数组和链表的本质差异线性表是数据结构里第一座山它包含两大主角顺序表底层是数组和链表。很多人以为自己懂数组其实并没有认真思考过数组和链表的区别。数组的特点是内存连续、按下标随机访问。这意味着你拿到下标X就能直接算出它所在的内存地址一步到位时间复杂度是O(1)。但代价是插入和删除很麻烦——你要在中间插入一个元素后面的所有元素都得往后挪所以最坏情况下的插入删除是O(n)。而且数组长度固定扩容要重新申请内存成本很高。链表则完全不同。它的每个节点散落在内存各处通过指针串成一条链子。插入和删除只需要修改指针指向不需要移动任何元素所以链表对插入删除的操作是O(1)。但你要查找第X个元素必须从头结点开始一个一个next过去时间复杂度是O(n)。这两个结构的取舍关系几乎贯穿整个数据结构课程。遇到问题先判断一下你的核心操作是读多还是写多读多选数组写多选链表这个判断能力比背多少代码都重要。2.2 手写链表最常见的三个坑链表虽然在考试里经常只考几个题目但手写实现的时候新手容易踩的坑还是那几个。第一个坑是头结点问题。很多人写完链表以后总是要单独为“删除第一个节点”这种情况写额外的判断逻辑因为head指针需要更新。解决这个问题的方法是引入一个空的头结点也叫哨兵节点。哨兵节点的数据域不使用只占一个位置让真正的链表从第二个节点开始。这样删除第一个业务节点就和删除其他节点一样统一了代码逻辑清爽很多。第二个坑是顺序问题。一句“先改后指”能救命。在链表上插入节点的时候你永远要先处理新节点的next也就是先把新节点和后面的节点“挂上”然后再修改前驱节点的next指向新节点。反过来就容易出现两个指针互相覆盖、链表直接断掉的情况。第三个坑是指针丢失。删除节点、反转链表这类操作动态指针比较多的时候一定要先把即将被覆盖的地址保存下来。写代码前在纸上画一遍指针是怎么流转的比在编译器里反复调试快得多这个习惯我建议任何人都要刻意培养。2.3 栈和队列别当它们是简单容器栈和队列听起来很简单一个后进先出一个先进先出代码也不复杂但很多人学到这里就飘了觉得不过如此。我要说的是这两个“简单的容器”在真实场景里的作用被严重低估了。栈最经典的应用是函数调用。你写递归函数的时候每一层调用都会把参数、返回地址压入调用栈返回的时候再弹出去。理解了栈你才能真正理解“栈溢出”是怎么回事也才能理解递归为什么能够一层一层地回溯。在不使用递归的场景里栈结构还经常被用来模拟递归这在很多高性能系统里是一种非常基础的技巧。队列的应用更加贴近生活。操作系统的进程调度队列、消息队列、任务队列本质都是先进先出的逻辑。还有你在广度优先搜索里也需要借助队列来维护“下一批要处理的节点”。学到这里不妨思考一下如果某个需求里需要“先来的先服务”大概率就是要用队列了。3. 排序与查找绕不开的两座大山3.1 排序算法怎么选才不慌期末复习和面试里排序算法永远是主角。冒泡排序、插入排序、选择排序、归并排序、快速排序、堆排序……背了一堆名字但考试的时候最常问的问题是什么场景下选哪个算法我的选择逻辑是这样的。数据量很小比如二三十个以内插入排序简单好用常数项极小性能不一定比快排慢。数据量大但要求稳定性归并排序是首选很多编程语言内置排序实现的就是归并或改进归并。数据量大、稳定性和额外内存都有严格要求那就只能用原地归并之类的技巧但这属于进阶了。没有稳定要求又想快快速排序在绝大多数乱序数据上表现最佳但它有退化风险基本等于O(n²)。如果对最坏情况有严格需求堆排序更稳但堆排序的常数项比较大实际表现往往不如快排。还有一点必须说学排序算法千万别死记硬背代码。你一定要理解其中的核心思想比如快排就是分治归并就是把两个有序序列合起来堆排序就是维护一个大根堆的调整过程。思想理解了就算考试紧张忘了代码也能在草稿纸上推出来。3.2 二分查找的细节决定成败查找这块线性表里最值得细说的是二分查找。代码看起来简单三五行就能写完但里面藏着好几个容易踩的细节。第一个细节是区间定义。你用左闭右闭[left, right]还是左闭右开[left, right)会直接影响while循环的条件和mid的更新方式。混用的结果是循环边界出错这就是传说中的“死循环和漏查并存”。第二个细节是mid的计算。很多人写int mid (left right) / 2这在left和right都很大的时候会溢出。正确的写法是int mid left (right - left) / 2这个细节面试官非常爱考。第三个细节是Python这类语言里的//运算符和C语言里的/在负数处理上有一点点区别如果你从C语言切到Python或者反过来要特别留意mid下取整和上取整对查找结果的影响。3.3 哈希表O(1)的背后是有代价的哈希表也叫散列表是查找效率的巅峰平均O(1)的时间复杂度让它在很多场景下都是首选。但它的高效是有条件的。哈希表的核心是哈希函数。你要把key值通过一定的计算映射到存储位置理想情况下每个key都能均匀散列到不同位置上。但实际上不可避免会发生冲突也就是两个不同的key映射到了同一个位置。解决冲突有几种常见方案开放地址法、链地址法在冲突位置挂一个链表、再哈希法。我面试别人的时候特别喜欢问“哈希表为什么是O(1)”。因为很多人只会回答“通过哈希函数直接定位”但如果你追问“冲突了怎么办”他就答不上来了。其实哈希表的最坏情况下会退化成一个链表查找复杂度变成O(n)。理解了这一点你才能明白为什么动态字符串、安全场景里往往会选用更复杂的结构来防止哈希冲突。4. 进阶篇树、图与高级数据结构4.1 二叉树与递归思维树形结构里的核心是二叉树二叉树的核心思想是递归。很多人在树这里卡住不是因为树有多难而是递归没练透。你可以这样理解二叉树一个节点带着它的左右孩子每个孩子又是一棵更小的二叉树。这种“自己包含自己”的结构天然适合用递归来遍历和操作。三个经典的遍历方式——前序、中序、后序区别只是访问节点的那句代码放在递归的前、中、后。理解了这一点遍历代码根本不用背。二叉树的高阶内容更值得花时间。平衡二叉树AVL和红黑树是为了解决普通二叉搜索树在极端场景下退化成链表的问题。如果你把普通二叉搜索树按递增顺序插入它会退化成一条斜线查找效率从O(log n)恶化为O(n)。红黑树通过节点颜色和旋转保持大致平衡是Java TreeMap、C map的标准实现。所以我建议学树形结构时别只盯着遍历代码一定要想清楚一个问题为什么需要平衡不均衡会怎样把这个问题想通了你对整棵树的认知就上一个台阶。4.2 图的两种主流表示方式到了图的章节难点不再是遍历而是怎么把现实问题抽象成图。图有两种主流表示方式邻接矩阵和邻接表。邻接矩阵就是一个二维数组。arr[i][j]等于1表示i和j之间有一条边等于0表示没有。这种表示方式查询两个顶点是否相连非常快O(1)就能查到但缺点是空间浪费假设你有10000个顶点矩阵就需要上亿个存储单元很多空间都是空闲的。邻接表则是每个顶点维护一个链表链里存它所有邻居。这种方式空间效率高尤其适合稀疏图。但查询两个顶点是否直接相连就要遍历其中一个顶点的邻接链表速度不如邻接矩阵。理解了这两种方式和它们的取舍图的深度优先搜索DFS和广度优先搜索BFS就好理解了。DFS有点像迷宫探险一条路走到黑不行就回头用递归或者栈实现BFS像水波扩散用队列逐层推进。最短路径问题里Dijkstra算法的核心也是对“当前距离最短的节点”不断做松弛操作这些都要在脑子里形成一个动态图景。4.3 高级结构堆、并查集与Trie树很多人学完树和图就停下来了结果面试和考研里那些高级数据结构题目让他们很受伤。实际上有几个高级结构的学习性价比超高。堆本质是一个完全二叉树但用数组存储。它在找最大或最小元素时效率极高复杂度O(1)拿到最值调整堆恢复性质的复杂度是O(log n)。Top K问题、优先队列、堆排序都是堆的经典应用。并查集我第一次看这个名字完全不知道它在干嘛。后来实际应用了才发现它解决的是“判断两个元素是不是同一个集合里的”这种问题。网络连通性判断、社交网络里的朋友圈划分、代码里的动态连通性全是并查集的活。它实现起来并不多但“路径压缩”和“按秩合并”这两个优化非常关键理解下来之后你会觉得这种结构非常巧妙。Trie树也叫字典树是处理字符串前缀问题的利器。敏感词匹配、自动补全、词频统计都可以借助Trie树来做。它的思想是把字符串的公共前缀提取出来共享存储空间换时间。5. 把数据结构用到真实场景里5.1 经典面试题背后的数据结构我会在面试里考一个题“设计一个LRU缓存”。这个题目的考点其实非常综合它要求你在O(1)时间内获取数据在O(1)时间内写入数据而且当容量满时要淘汰最久未使用的数据。用什么结构哈希表提供O(1)的查找双向链表提供O(1)的插入和删除二者结合就是LRU的标准解法。你看这就是数据结构的综合实战。没有哈希表你没法快速定位缓存项没有双向链表你没法在O(1)内把访问过的节点移到链表头部。单个结构都不够需要组合使用。类似的题目还有用两个栈实现队列、用最小堆实现Top K、用前缀树实现敏感词过滤。这些题目给了我们一个重要启示不要把数据结构当孤立的知识章节学它们是可以组合的积木。5.2 各语言里已被封装好的数据结构实际开发中你很少需要自己从零手写红黑树或者跳表绝大多数编程语言都提供了现成的数据结构实现。但前提是你得知道底层是什么。C的STL里vector顺序表、list链表、stack栈、queue队列、priority_queue最大/最小堆、map红黑树、unordered_map哈希表这些都要知道它们的时间复杂度特性。C开发中高频使用map和unordered_map如果你不清楚二者的差异有序还是无序、底层红黑树还是哈希表很容易写出性能有问题的代码。Java里ArrayList底层是动态数组LinkedList底层是双向链表HashMap底层是数组链表红黑树。很多人面试被问“HashMap的底层结构”其实就是在考察你对数据结构的理解程度。Python更典型列表list底层是动态数组dict底层是哈希表set底层也是哈希表。Python里的元组tuple和列表最大的区别在于不可变性这个不可变性背后其实是内存和线程安全上的考量。5.3 数据分析中的数据结构pandas的Series与DataFrame很多人问学数据结构是不是只对算法工程师和考研党有用其实不是的。你学数据分析天天用的pandas底层也是一套数据结构体系的演化。pandas里有两大核心结构Series和DataFrame。Series你可以理解成一个带标签的一维数组底层是NumPy的一维ndarray加上配套的索引数组。DataFrame则是带标签的二维表格底层可以理解成一个Series的字典每个列就是一个Series。你在学数据结构时理解过哈希表和索引的概念再看pandas的DataFrame就会明白为什么DataFrame按列快速取数据比按行方便因为底层的存储布局是列优先的每列的数据在内存里是连续存放的再加上索引机制取整列时走的还是类哈希的路径。这背后其实就是“结构决定性能”的典型体现。再往深里说pandas的索引对象并不是随便一个数组。它的底层使用了类似字典、多级索引、哈希表、有序列表等多种结构来做数据定位。所以具备数据结构功底的人学习pandas时会明显比完全没有基础的人快——因为你不是在“背API”而是在“理解设计”。6. 教材选择与复习策略6.1 严蔚敏教材到底怎么读严蔚敏的《数据结构C语言版》是国内高校使用率最高的教材之一但很多学生第一次打开的时候就被劝退了。原因很简单这本书的文字表述非常精炼大量代码示例用类C语言的伪代码描述阅读门槛偏高。我的建议是第一遍不要试图从头到尾把每章代码都搞懂。这本书适合作为参考书和工具书而不是适合一口气通读的小说。你完全可以根据自己的学习路径先学基础概念然后直接上网找配套的讲解视频跟着视频学一遍。遇到不清楚的细节再回书里查对应的章节。另外严蔚敏教材里的很多代码示例是教学性质的不是工业级代码。你读的时候要重点理解“思想”和“步骤”不要死抠每一个细节。比如Dijkstra算法的辅助数组比较多教材里为了严谨写得很复杂但只要你理解了“每次从未访问节点中选距离最小的点然后更新邻居距离”这个过程代码细节反而不重要。6.2 考研408和期末复习怎么抓重点如果你是在准备408考研数据结构的复习优先级我建议这样安排线性表、二叉树、排序是绝对的核心分数占比高栈、队列、哈希表是次重点图的题目通常以算法选择题和应用题形式出现但不会太偏抓最经典的BFS、DFS、Dijkstra就好。期末复习的话先看老师提供的教学大纲和ppt。很多时候老师考的就是课堂强调过的那些实现和思想。还有一道必杀技是把往年试题拿来做一遍弄清老师的命题偏好。平时做题做得再多不知道命题风格也是白搭。一个特别好用的复习方法是画思维导图。把每种结构的存储方式、时间复杂度、应用场景、经典算法画成一张大图。不需要画得复杂自己看得懂就行。这样考前就不用背几十页笔记了盯着图过一遍很快就能唤醒记忆。6.3 我从踩坑中学到的几条经验最后分享几条我自己的经验不吹不黑。代码一定要亲手敲而且要敲到每一条不带书抄也能默写出来的程度。这个说法听着像老生常谈但大多数人就是输在这一步。只看不写一周以后你就只剩下一个模糊的印象。手写链表、手写树的三种遍历、手写快排和归并这些基本功至少要在纸上默写三遍。复杂度分析是硬门槛。很多初学者写代码能跑就行从来不算时间复杂度。但数据结构的美感恰恰在于对复杂度的极致追求。遇到每道题都先把暴力解法写出来然后问自己一句这个操作能不能从O(n)优化到O(log n)甚至O(1)这个习惯一旦养成你的数据结构水平会突飞猛进。还有一条建议不要想着一口吃成胖子。数据结构不是学一遍就能精通的。我的经验是学完一遍以后隔段时间再看第二遍每次看都会发现以前没注意到的细节。第一遍理解大概第二遍吃透细节第三遍灵活运用这才是一个真实自然的学习节奏。最后遇到不懂的地方千万不要怕。数据结构里有很多反直觉的东西尤其是树和图中的递归过程手跟脑不匹配是常态。去画图、去手写、去调试、去追代码里的每一步这个过程本身就是在积累解决问题的能力。数据结构的功力说到底就是在这些看似枯燥的练习中一点一点磨出来的。
返回列表