ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

数据结构与算法分析:从逻辑结构到复杂度计算的实战指南

数据结构与算法分析:从逻辑结构到复杂度计算的实战指南 面试的时候我常问这样一个题设计一个LRU缓存你怎么做有人张嘴就是HashMap加双向链表答得飞快但追问一句“为什么必须是双向链表单向行不行”往往就卡壳了。这就是典型的背答案没把数据结构这门课吃透。我一直觉得数据结构与算法分析里最容易被忽视的就是“绪论”这一章——它不写具体代码只讲抽象概念、组织数据的方式和分析算法的工具但恰恰是整个知识体系的地基。下面这篇文章我想用过来人的身份把绪论里的几个核心问题掰开揉碎讲清楚再顺带聊聊期末考、考研408和实验报告这些实际场景下怎么复习、怎么避坑希望对正在啃这本书的同学有点实在的帮助。1. 绪论到底在讲什么数据结构这门课的核心是“组织数据”1.1 先分清逻辑结构和存储结构很多困惑都出在这里我第一次学数据结构用的是严蔚敏老师的C语言版教材开篇就在讲“数据结构是相互之间存在一种或多种特定关系的数据元素的集合”。这句话很拗口我觉得把它翻译成人话就是你手里有一堆数据怎么把它们“摆”在一起让增删改查都方便。重点在于这个“摆”有两个层面。第一个层面叫逻辑结构指的是数据元素之间抽象的关系跟计算机没关系。你打开任意一本教材都会告诉你逻辑结构分四类集合、线性结构、树形结构、图状结构。集合就是一堆元素谁也不认识谁线性结构像排队买奶茶前一个挨着后一个有且仅有一个前驱和一个后继树形结构是老板-经理-员工的层级关系一个上级管多个下级图状结构最自由任意两个节点都可以有联系比如社交网络里谁都能关注谁。第二个层面叫存储结构也就是数据在计算机内存里实际怎么放的教材里通常叫物理结构。常见的就四种顺序存储、链式存储、索引存储、散列存储。顺序存储就是把元素一块挨一块地放在连续内存里像数组链式存储是每个元素带一个指针东一个西一个散落在内存里像链表索引存储是搞一张目录按索引号找数据散列存储就是通过哈希函数直接算出存放位置。很多人学了一学期还分不清“线性表”和“链表”根源就在于把逻辑结构和存储结构搅在一起了。线性表是个逻辑概念可以用数组实现也可以用链表实现这俩是两码事。我在面试里见过不少候选人说“链表就是线性表”其实这是不对的。正确的说法应该是链表是线性表的一种存储实现方式。搞清楚这层关系后面学栈、队列、树、图都会顺很多。1.2 抽象数据类型(ADT)是“接口与实现分离”的思想源头绪论里除了逻辑结构和存储结构还有个概念叫抽象数据类型英文缩写ADT。书上会给你一个标准定义一个数学模型以及定义在该模型上的一组操作。听起来很玄乎但我愿意这么理解ADT就是告诉你“能干什么”但不告诉你“怎么干的”。举个例子栈这个ADT你只需要知道它支持push、pop、peek这几个操作并且遵循后进先出规则。至于底层是用数组还是链表用C语言还是Java实现那是实现细节使用的人不需要关心。这其实就是现代编程里“接口与实现分离”思想的雏形。你在Java里天天用List接口ArrayList和LinkedList各干各的但调用方只依赖List接口这就是ADT思想的落地。实习或者工作后你会发现很多代码设计的原则比如依赖倒置、开闭原则追根溯源都能扯到ADT这层朴素的抽象思想上。所以学绪论的时候别觉得抽象没用看得见摸不着的抽象往往才是最值钱的东西。我当年上机写实验报告时也烦过觉得“搞个栈怎么这么绕”后来写着写着才发现把一个数据类型能做什么、内部怎么做到分开考虑代码的可维护性会明显上一个台阶。2. 算法分析入门复杂度不是用来背的是用来比较方案的2.1 时间复杂度怎么算先看循环再看递归算法分析是绪论的另一半重头戏核心就是时间复杂度和空间复杂度。算法分析这门手艺本质上就是回答一个问题数据规模变大的时候程序跑的时间会怎么涨占的内存会怎么涨。计算时间复杂度我总结一个土办法先找循环再找递归。没有循环和递归的代码基本就是常量级O(1)单层循环处理n个元素一般是O(n)双层循环各跑n次通常是O(n²)循环变量每次减半比如i从n开始每次除以2那就是O(log n)。这背后其实就是一条条语句的执行次数合计然后取“增长速度最快”的那一项去掉系数。我见过太多同学背“时间复杂度排序口诀”O(1) O(log n) O(n) O(n log n) O(n²) O(2ⁿ)。背是能背下来但真要分析一段代码还是会懵。问题出在他们不知道基本规则加法规则一段代码里顺序执行两个片段复杂度取大的那个乘法规则嵌套循环和非递归调用的循环内部又调函数复杂度相乘。我建议拿归并排序当练习材料。它分治递归每一层要处理的元素总数还是n递归深度是log n层所以总复杂度是O(n log n)。阿里面试常问“为什么快排平均是O(n log n)”其实就是顺着递归树一层一层想每层把n个元素都过了一遍层数大概log n层。把分析过程在纸上画一遍比背一百遍结论都管用。2.2 空间复杂度别只盯着“变量个数”递归的栈空间最容易被忽略好多同学算空间复杂度时只关心开了多少个数组、多少个临时变量这没错但很容易漏掉一个大头递归调用占用的函数调用栈空间。一个深度为n的递归每一层调用都要保存当前函数的所有局部变量和返回地址这本身就要占O(n)的空间。经典的例子就是斐波那契数列的朴素递归写法int fib(int n) { if (n 1) return n; return fib(n-1) fib(n-2); }这个写法时间上是指数级O(2ⁿ)空间上是O(n)——因为递归最深时栈里大约有n层调用。我读研帮本科生看实验报告时每年都有人问“为什么fib(50)跑不出来”就是没意识到指数级复杂度有多恐怖。其实fib(100)这个数字算出来是3.5e20量级地球上所有服务器加一起硬算也要跑到天荒地老。再说一个常见的坑有些人以为递归改循环复杂度一定更好。递归改成迭代确实能省掉栈空间比如斐波那契用一维数组滚动更新空间降到O(1)但有时候递归天然贴合分治思想比如快速排序的空间复杂度主要来自递归栈平均是O(log n)最坏是O(n)。如果不是为了抠空间性能没必要盲目全改迭代代码可读性也很重要。3. 核心数据结构选型从双端队列到哈希表各有各的脾气3.1 线性结构实战对照数组、链表、栈、队列和双端队列学数据结构到后面你会发现大部分东西都建立在数组和链表两种存储结构上。数组的优势是随机访问O(1)缺点是插入删除要挪元素链表反过来插入删除只要改指针但访问第k个元素得从头走一遍O(n)。项目里怎么选如果读多写少选数组如果颠来倒去插入删除多选链表。Java的ArrayList和LinkedList正好对应这两种选择Java里的数据结构源码ArrayList扩容用的数组拷贝LinkedList就是双链表。栈和队列就更生活化了。栈是后进先出递归调用、括号匹配、函数调用、浏览器后退都用它。队列是先进先出任务调度、消息队列、BFS广度优先遍历都是它的地盘。还有近几年面试常考的双端队列(Deque)两头都能进能出。Java里ArrayDeque就是双端队列的经典实现。我写算法题时处理滑动窗口最大值就常用双端队列维护一个单调队列每次窗口移动队头弹出过期的队尾压入新元素并维持单调性每个元素最多进出一次整体O(n)搞定。这题用堆或者暴力扫都是O(n log n)或O(nk)双端队列这个数据结构选得就很妙。做题的时候有很多同学容易犯一个错分不清Deque该当栈用还是当队列用。Java的Deque接口同时提供了push/pop和addFirst/removeFirst等等其实底层都一样只是语义不同。用熟了自然就知道当栈用就push/pop当队列用就offer/poll别混着用就行。3.2 非线性结构树、图、散列表的选型思路树结构里最常见的就二叉搜索树、堆、Trie前缀树、并查集。二叉搜索树在平衡状态下增删改查都是O(log n)但极度不平衡时会退化成链表O(n)所以才有AVL树、红黑树这些自动平衡的版本。Java里的TreeMap、TreeSet底层就是红黑树。堆是个完全二叉树适合处理“动态取最大最小值”的问题比如优先队列。BFS在树里就是一层一层扫在队列里作为核心数据结构用。图就更复杂。图的存储结构有邻接矩阵和邻接表两种。邻接矩阵适合稠密图判断两个顶点是否相连是O(1)但空间是O(V²)邻接表适合稀疏图遍历一个顶点的所有邻接边跟边的数量成正比空间是O(VE)。考研408的图和数组经常合起来考其实就是考图的存储以及DFS、BFS、最短路径、最小生成树这些算法在不同存储结构下的复杂度。这些都是套路题关键是把邻接矩阵和邻接表对应的代码写熟。散列表也就是哈希表这个几乎是面试必问。它核心是哈希函数加冲突解决。Java的HashMap在JDK 8之后当链表长度超过8且数组长度超过64时会把链表转成红黑树把最坏情况从O(n)优化到O(log n)。我刚学的时候觉得这很复杂但后来理解到散列的本质就是“用空间换时间”加一个合理的哈希函数把任意数据映射到数组下标。用它的时候有个坑哈希冲突处理不好会导致性能急剧下降所以选一个好的哈希函数很关键Java里HashMap的hash方法是高16位异或低16位再取模就是为了让高位也参与散列减少冲突。4. 学习路线和复习策略期末、考研、实验报告三线并进4.1 考研408的数据结构复习从王道到真题刷题顺序很重要考研党应该都知道408专业课里数据结构约占45分。市面上用得最多的复习资料是王道的数据结构辅导书还有配套网课。我不建议一上来就看那种几百页的大厚教材除非你时间特别充裕。比较务实的一条路线是第一轮用王道这种辅导书配视频把每一章的知识框架拉出来然后真题分章节刷第二轮再全真模拟。我特别想提醒一点408里的数据结构很多题目是综合性的比如“图数组”存储结构结合考察它可能让你根据邻接表画邻接矩阵又根据邻接矩阵推DFS的访问序列。这种题光靠记忆是不行的必须自己动手画图、手动推一遍。我复习时用一个方法把每章的知识点画成思维导图然后对着真题标注考点分布。比如排序那块八大排序的稳定性、时间复杂度、空间复杂度几乎年年考不画表整理清楚很容易记混。4.2 期末复习和实验报告代码写不出来不是因为笨是没搞清楚“步骤”期末复习跟考研不一样考研是拉长线期末是短平快。期末数据结构复习的诀窍是优先掌握“手写代码”和“画图题”。很多学校期末卷子就三道大题线性表/二叉树/图的代码填空或手写、某种排序过程的推演、复杂度的计算。这些题目其实有套路比如树的遍历只要把递归三行代码背熟前中后序基本都能应付。再说实验报告这也是很多同学头疼的事。讲道理数据结构实验报告并不需要写出多牛的代码老师更看重的是你的实验过程是否完整。通常一个规范的实验报告要包含问题描述、数据结构设计ADT描述、算法流程图或核心代码、运行结果截图、复杂度分析、遇到的问题和解决办法。我批过不少实验报告最常见的毛病是直接贴了一大段代码前面的设计分析一字不写后面也没有复杂度分析这很要命。老师其实一眼就能看出来你有没有真正读懂代码。写实验报告的时候哪怕代码是抄的你也要把核心代码每一行的作用在注释里写明白再把时间复杂度推一遍这既是对老师的交代也是对自己的训练。4.3 Java版和C语言版教材怎么选语言不是关键思想才是搜索热词里频繁出现“数据结构与算法分析:java语言描述 pdf”和“数据结构c语言版”说明很多人在纠结选哪个版本。我觉得语言只是个壳吃透数据结构思想才重要。C语言版的好处是接近底层指针让你把内存、链表理解得更深Java版的好处是集合框架里有现成的类可以通过源码对照学习。我自己的建议如果非科班、没学过C不想跟指针纠缠直接用Java版也没问题。但要注意JAVA里的引用其实和C的指针本质上是一回事只是不让你直接操作内存地址。我之前带过一个小项目学生用Java写二叉搜索树总是把root传进方法里改了半天回到main里一看还是null这就是没理解引用传递和值传递的区别。建议练习的时候用debug模式一步步看变量变化或者干脆每个方法都返回新的根节点把修改结果带出来这种习惯比背多少教材都管用。5. 常见问题与避坑技巧都是过来人的经验5.1 算法分析最容易踩的坑混淆平均复杂度和最坏复杂度很多人分析算法时总是习惯说“这个算法是O(n)”但他没说明是平均、最坏还是最好。比如快速排序平均是O(n log n)但如果每次选基准都选到最边上最坏就是O(n²)。我们说快排是O(n log n)默认说的是平均情况和期望情况不是最坏。面试和考试的时候一定要把“平均”“最坏”“最好”说清楚。有些排序像是堆排序、归并排序无论什么输入都是O(n log n)这类“保底”特性有时候比平均性能更重要。还有一个是忽略输入规模的边界条件。比如二分查找很多人背了模板但忘了在while循环里写left right还是left right边界条件没搞清楚跑起来就是死循环或者漏元素。我建议在理解的基础上把二分查找的三种变体找第一个等于target的、找最后一个等于target的、找插入位置都手写一遍才能真掌握。5.2 学习数据结构的常见误区只刷题不看书、只看书不实现现在流行刷题文化觉得刷完几百道力扣题数据结构就过关了。我的观点是刷题是很好的巩固手段但不能完全替代系统学习和结构梳理。你要是不知道栈和队列的区别刷再多题也只能靠记忆模板题目一变就懵。反过来只捧着《大话数据结构》和教材从头看到尾一行代码不敲也是纸上谈兵。我建议搭配“看书刷题画图”三件套看书理解概念画图模拟数据结构的动态变化再用代码实现。如果你在学树就动手把前中后序遍历的非递归版本写一遍彻底搞懂栈在遍历里起了什么作用。写过非递归遍历再去写层序遍历就会发现队列的角色完全不一样。这种对比学习的效果远比孤立地记算法好。另外我特别推荐关注Pandas数据结构创建这类Python话题的同学也回来看一眼数据结构基础。Pandas的Series和DataFrame本质上就是“带索引的数组”和“二维表结构”理解了一维数组和二维表怎么存取再看Pandas的接口会清楚很多。编程语言框架千千万底层的数据结构思想就那些掌握了万变不离其宗。5.3 几个常见追问和排查思路速查表考试和面试时有些看似高级的问题其实都是绪论和算法分析的延伸直接给你整理成一张表问题场景核心考点推荐应对思路为什么HashMap有时候慢散列冲突、扩容讲清楚链表转红黑树的触发条件递归和迭代怎么取舍空间复杂度、栈溢出风险递归清晰但可能爆栈迭代省空间但可能难写数组为什么访问快顺序存储、缓存局部性连续内存CPU缓存命中率高链表为什么插入快前提是已知位置链式存储的指针修改不需要搬动元素只改变前后指针为什么快速排序在工程中仍然常用平均复杂度与缓存性能虽然最坏O(n²)但平均O(n log n)实际工程表现优秀双端队列适合什么场景两端操作都要O(1)滑动窗口、双端BFS、任务双端调度这张表也算是“数据结构与算法分析”从理论到实战的一个小小缩影。面试官问的每一个问题最终都能回溯到你在绪论里学过的存储结构、逻辑结构、算法分析这三个基本盘上。写到最后多说一句我自己一开始学数据结构的时候也觉得教材抽象、代码写不出来挫败感很强。后来想通了这门课本来就不是用来“背”的而是用来“用时间换理解”的。所有新知识第一遍看不懂很正常第二遍完全能接受多画几遍图就顺了。你可以在学完树再回头看线性表会有不同的理解。数据结构这本书学得好不好不在于记住了多少名词而在于遇到实际问题时能不能快速判断该用什么结构去组织数据、用什么策略去分析算法成本。能把绪论里那些不起眼的抽象概念想明白你已经超过大多数人了。这是我在实际闯过几次数据结构的大坑之后最真实的体会与真的在啃这本书的你共勉。
返回列表