资讯动态

数据结构与算法:爬过抽象的阶梯,才能自由穿越各层

发布时间:2026/10/13 10:36:10 来源:尧图企业网站定制
1. 为什么大部分人学数据结构与算法是卡在了楼梯中间先说我观察了很久的一个现象同样一门数据结构与算法课有人学完能随手写个跳表、讲清楚B树为什么适合磁盘有人连链表反转都要背半天。我带过的不少新人并不是不努力题也刷了书也看了但一到真正做系统设计、调接口性能的时候就露怯。以前我把这归因为“基础不牢”后来发现不对——他们其实是被一架看不见的楼梯绊住了。这架楼梯就是标题里说的“抽象的阶梯”。数据结构与算法这门学问特殊之处在于它每一层都在说同一件事数据怎么组织、操作怎么高效。但这件事从硬件、操作系统、编程语言、工具库、业务代码的角度看过去长得完全不一样。初学者最痛苦的地方不是某个结构多复杂而是没意识到这些东西是一层一层叠起来的。你今天用HashMap顺手得不行那底层是哈希函数、散列表、扩容策略再往底层是内存寻址、缓存行、分支预测。普通业务代码根本不需要碰后两层但面试题和线上事故会。所以我一直觉得学习路线上的第一课不是“什么是数组”而是“你现在站在哪一层、你该关注哪一层”。打个比方。抽象阶梯就像你住在一栋楼里数据结构是楼层编号算法是从A层到B层的路线。你查一下电梯有没有坏答案是O(1)走楼梯扫一遍每层有没有人答案是O(n)。这两种说法物理世界和代码世界都能理解这就是抽象的力量。但如果你不知道电梯内部怎么调度、什么时候会拥挤你就理解不了O(1)为什么在某些场景退化成O(n)。你不是算法不行你是没爬过楼梯。这篇文章我就是想把这架楼梯一层层画出来给你看它有几层、每层解决什么问题、为什么要这样分层、以及真正吃透它之后你怎么反过来用这种思维去写业务代码。适合三类人刚学完基础但总感觉没入门的、刷了很多题但系统设计还是发怵的、以及想给孩子或新人讲清楚这门课到底在学什么的读者。先说个反直觉的结论**抽象层级越高代码越好写但排查问题越难抽象层级越低理解越费力但抓住的本质越硬核。**高手和初学者的区别其实不在于谁待在哪一层而在于谁能在各层之间自由切换。2. 从晶体管到框架一张数据结构的四层抽象地图2.1 第一层硬件设备上的二进制真相这层几乎没有人愿意看但它决定了上层一切“为什么”。数据结构的底层真相说穿了就是内存是一排排编好号的格子每个格子能放0或1。CPU访问某个格子必须给地址地址是个数字。数组为什么能O(1)访问因为它吃定了“连续编号”首地址 索引 × 每个元素大小。链表为什么插入删除快因为它不要求连续每个节点里存一个“下一个节点的地址”代价是要跟着指针跳。很多人觉得这是计组的内容不用管。但真正排查过线上性能问题的人都懂一次引用局部性差异可能就是几十倍的耗时差。数组和链表在理论复杂度上都是O(1)插入尾部但数组是连续内存缓存友好链表每个节点散落各处缓存命中率低。你说它们是“同阶的”业务上体验完全不同。这层给你的是物理直觉。理由很简单你理解了“内存格子”“地址”“连续”这三个词后面所有结构都能用“怎么摆格子”重新推一遍。树、图、哈希表全是在格子和指针上做文章。2.2 第二层编程语言里的变量与容器到了这层你已经不看寄存器了你看到的是int、array、struct、class。语言帮你把“找地址、读格子”抽象成“给变量名赋值”。这一层的核心不是语法而是容器向你承诺的语义。数组说我是一段连续的同类型数据你可以按下标找我。链表说我是由节点串起来的东西你只能从头往后找。栈说我只有一个口后进先出。队列说我两个口一个进一个出。本质区别不只是“数据结构不同”而是它们对你承诺了不同的操作代价和顺序规则。我带新人时经常问一个问题一个普通业务系统里数据结构到底是谁选的答案很残酷——大部分时候是库和框架替你选的你只是在用。你以为你在用List其实某些语言实现里它就是数组扩容你以为你在用Set有些实现是哈希表有些是红黑树。如果你不知道第二层的容器背后各是什么花样你就只能接受默认无法根据场景做出调整。这也是为什么我强烈建议学每一门新语言时一定要看官方文档里它的集合类是用什么结构实现的。这不难但很多人一辈子没看过。2.3 第三层抽象数据类型ADT的价值这一层开始出现“接口与实现分离”它是整个计算机科学里被讲烂但又没被讲透的思想。栈这个抽象数据类型定义的是push、pop、peek三个行为它不管底下是数组还是链表。你用数组实现栈代码是这样class Stack: def __init__(self): self._data [] def push(self, item): self._data.append(item) def pop(self): if not self._data: raise IndexError(pop from empty stack) return self._data.pop() def peek(self): if not self._data: raise IndexError(peek from empty stack) return self._data[-1]底层换成链表接口完全不变调用方一行都不用改。这就是抽象的胜利我把“它是什么”和“它怎么做到”切开你只需要信任接口。可问题也出在这个“信任”上。很多新手写代码接口调用得飞起但脑子里对“这一调用背后跑了多少指令”完全没有概念。这不是抽象阶梯的错而是只爬了一层就停在原地。第三层是分水岭。理解它的人会开始用“操作”而不是“存储”来思考问题我需要后进先出就用栈我需要先进先出就用队列。你选择的依据变成业务对顺序的要求而不是“我背过哪个结构”。你在这一层获得了思考的自由。2.4 第四层框架与协议里看不见的结构这一层大多数业务开发者天天在用但完全无感。举个最常见的例子消息队列。从业务上看你只是往队列里丢一条消息另一个服务收走处理。这就是个先进先出的队列抽象得清澈见底。但往下一层看这个队列可能由一个分布式存储实现里面有分区、有副本、有索引、有空闲管理。再往下每条消息怎么落盘、怎么在节点之间复制全是数据结构的层层嵌套。再比如关系数据库的索引。你在SQL里写WHERE id 7数据库优化器决定走主键索引索引在磁盘上一般是个B树。你不需要知道B树长什么样也能用数据库这本身是抽象阶梯的善意。但一旦SQL慢了呢你建立的索引没生效因为某列上用了函数索引失效你明明加了索引但查询条件里的前导模糊匹配让它全表扫描。这时候你如果不爬下楼梯看一眼执行计划看一眼数据分布你就会面临最尴尬的局面程序在抽象的高空运行而你的认知停留在接口表面两者一脱节问题就是玄学。所以第四层的核心命题是框架把结构藏起来了但藏起来不等于没有。你要知道“隐藏的结构”在哪里才能在被问题击中时准确定位。某次我帮一个团队排查线上接口偶发超时代码层面看了无数遍逻辑没毛病。最终追到数据库里一张大表的索引碎片化严重统计信息失真优化器选了全表扫描。问题不在业务代码而在一个“看不见的树”上。那一次之后我在每次性能复盘里加了一句话先确认你在哪一层再往楼下走。3. 算法的抽象从一行行代码里抬起头来3.1 复杂度不是用来背的是用来攀比的很多人把O(n)当成一种标签“快速排序是O(n log n)的所以它快”。这个理解不够准确它少了一个关键动作——比较。复杂度分析的核心价值不在于给你一个绝对评价而在于让你在不同方案之间做相对比较。你用链表实现队列入队是O(1)出队也是O(1)很棒。但如果你在入队时为了“顺便排个序”用了插入排序那入队就变成O(n)。单看每次操作你未必能察觉但你把操作频次乘进去比如每秒入队一万次差距就爆炸了。我刚工作那阵写过一个统计模块一开始图省事每次来一条数据就排一次序放到列表里。单测全过一压测就完蛋。后来改成先攒一批、批量排序、批量写入整个模块从扛不住变成毫无压力。那件事让我彻底明白数据结构的“好”和“坏”不是看一行代码而是看你在什么频次上怎么用它。所以实操建议是每次写完一段可能会被高频调用的逻辑花两分钟列一下——最坏情况是什么平均情况是什么调用频次是多少三个数乘起来答案自己会冒出来。这不是严格的时间复杂度分析但对绝大多数业务代码已经足够救命。3.2 递归、分治与问题模式算法里最容易让人觉得“玄”的就是递归。其实递归一点也不玄它的本质是**把一个规模为n的问题转化为一个或几个规模更小的问题并给一个最小规模的答案。**你不需要在脑子里展开整个递归过程只需要相信“规模小一点时这个函数已经能正确工作”。但这里有个陷阱**递归的抽象是建立在“你信任递归出口和递推关系都正确”之上的。**很多人写递归翻车不是没理解递归而是没有验证两个东西出口对不对、递推关系能不能把问题规模降下去。阶乘人人会写def factorial(n): if n 1: return 1 return n * factorial(n - 1)这个例子里你相信factorial(n-1)是对的所以factorial(n)直接拼上去。一旦你把这种“信任”迁移到归并排序、树的遍历、动态规划的状态转移上你就拥有了算法设计的核心能力——模式识别。分治模式把大问题拆成独立的子问题分别解决再合并。减治模式每次缩小问题规模但只解决缩减后的那一个。动态规划模式重叠子问题算一次存起来以后直接用。这些模式比任何一道具体题目都值钱因为它们是算法的“抽象层”。我面试时特别喜欢问一个看似简单的问题给你一个数组找第k大的数。很多候选人第一反应是排序O(n log n)然后被问到“能不能更快”就卡住。而真正爬过抽象阶梯的人会想到快排的partition步骤本质上是在做一个规模削减——一次partition能确定一个元素的最终位置如果这个位置正好是k答案就是它如果k比它小只需要递归左半部分如果k比它大只需要递归右半部分。平均时间O(n)不是因为你背了“快速选择”而是因为你把快排抽象成“每次确定一个位置丢掉一半”的模式。这一步就是抽象的价值。3.3 一个实例电梯调度里的抽象阶梯抽象的东西必须有实例不然容易变成空谈。我常用电梯调度来教学因为它每个人都见过。假设一栋20层的楼有一台电梯你怎么安排它响应所有请求最简单粗暴的方案是谁按了就先往谁那跑跑来跑去效率低。稍微好一点的方案是来回扫描到了最高请求点再掉头。再好一点根据当前运行方向和顺路请求动态拼接路径。这三个方案对应的数据结构从“无序列表”到“队列”到“双端队列桶”。你看同一个物理问题抽象层次不同数据结构完全不同。这节想表达的核心是算法不是凭空掉下来的是你对问题不断抽象出来的。你每一次说“这个问题可以建模成XX”都是在阶梯上做了一次向上的抽象你每一次说“我要用XX结构实现”都是在做向下的落地。数据结构与算法的高手就是这两种动作切换得最流畅的人。4. 怎么练出“自动爬梯”的能力4.1 黑箱练习法我教新人时用过一种很笨但很有效的方法要求他们不看任何实现只根据接口描述写一个能用它完成任务的程序。比如我告诉他们“这里有一个栈支持push、pop、peek它的行为是后进先出别的你都不用知道。”然后让他们实现括号匹配、表达式求值。等他们写完了再打开底层实现给他们看哦原来底下是个数组原来扩容是1.5倍。这个顺序很重要。大多数教程是先给你看实现再让你用。但实际工作中你面对的永远是“接口已知实现未知”。你先在接口层思考然后落到实现层验证再回到接口层使用这才是在爬楼梯。先黑箱再白箱再黑箱。你可以在日常学习里复刻这个流程拿到一个工具函数不要立刻去看源码先根据它的说明写几个用例把“它应该干什么”验证扎实了再去读源码看“它怎么干”。这种顺序会强迫你建立两个层面的心智模型而不是只记住一个。4.2 画图画到不用再画为止说句实在话数据结构与算法是最适合用笔画的一门课。链表反转你不画三个指针硬记代码三天必忘。二叉树中序遍历你不画出访问顺序光背递归稍一变形就懵。图的最短路径你不画出松弛过程永远理解不了Dijkstra为什么成立。我自己的习惯是学任何新结构先在白板上用圆圈和箭头讲一遍给自己听。讲到“这里指针指过去”“这里要判断空”这种程度才允许自己打开编辑器。画图不是浪费时间是在大脑里建立一帧帧“动画帧”。等到你不用画也能在脑子里把这动画放出来你就进阶了。按我的经验一个结构从“需要画”到“不用画”大约是亲手实现三到五遍的量。很多人失败是因为一遍都没完整画过背完就忘进入“学不会”的死循环。4.3 把“题型库”升级成“图式库”刷题界流行一个词叫“题型分类”比如BFS、DFS、双指针、滑动窗口。这个分类有用但还不够抽象。它停留在“技巧”层面没有上升到“问题结构”层面。我建议你再往上抽一层想一个问题什么样的场景会自然产生某种结构举例来说“需要撤销操作”会产生栈的图式浏览器的回退、编辑器的Undo、函数调用栈全是对栈的运用。“需要公平排队”会产生队列的图式打印机任务、消息队列、线程池。“需要快速判断一个元素是否出现过”会产生哈希的图式去重、缓存、集合运算。“需要在一堆元素里每次找最大或最小”会产生堆的图式优先级调度、TopK、中位数维护。“需要找到两点间最短路径”会产生图的图式导航、网络路由、社交关系链。当你把题目归类成“它其实想让我建一个什么结构、满足什么操作”你就不再是刷题而是在做问题建模。这个习惯迁移到工作上就是所谓的“架构眼光”你看到一个业务需求第一反应不是写if-else而是“这本质上是一个队列/状态机/树”。举一个工作中常见的场景订单状态流转。很多人写成一坨switch-case状态一多就炸。如果抽象成状态机每个状态是一个节点每个事件是一条边合法性一目了然。你不需要任何框架只需要一张哈希表存“状态下的事件-目标状态”映射就是一张有向图。这就是用数据结构的眼光看业务。5. 适可而止不要为了爬梯而爬梯5.1 过度抽象的坏处讲了很多抽象的好处现在必须泼一盆冷水。抽象是把双刃剑。抽象得太高代码是“声明式”的看起来优雅但出了问题你连从哪查起都不知道。抽象得太低每行代码都精确控制内存但业务复杂度一上来代码量爆炸、可读性骤降、维护成本起飞。我见过最夸张的案例是有人在一个简单增删改查的项目里硬套“仓库模式工作单元事件溯源”。本来两星期能干完的活做了两个月。学习数据结构与算法的正确目的是让你多一套选择而不是强制所有代码都去追求最优结构。栈很优雅但如果你只是临时存三个变量一个数组就够了图很强大但你只有五个节点直接存二维数组没什么丢人的。我自己的经验是抽象到什么程度取决于三个变量——这个逻辑变更的频率、出错的代价、团队里其他人的认知水平。如果逻辑半年不变、出错也无所谓、就你一个人维护怎么抽象都行。如果逻辑天天改、出错会扣钱、团队有五个人要读那宁可多一点重复代码也要降低理解的台阶。抽象是为你服务的不是让你去供着的。5.2 什么时候该下楼梯日常开发里什么时候你必须主动往下爬我总结了三类情况。第一类性能排查。线上接口变慢你不能停在框架层嘴硬“框架肯定没毛病”。该看执行计划看执行计划该看GC日志看GC日志该看链表转树的条件看源码。你爬得越深越可能找到真凶。第二类设计取舍。做技术方案时你可以不写底层代码但你得有办法判断两种方案的差别。比如选ArrayList还是LinkedList不能只凭感觉。你能说出“我主要是按下标随机访问所以选数组频繁在头部插入所以选链表”这就说明你有能力下到第二层做决策。第三类教会别人。教学是逼你下楼梯的最好方式。你给别人讲清楚一个知识点前提是你自己能把每一层都打通能说它是什么能说它怎么实现能举出业务场景能讲明白如果哪里错了会出现什么现象。能完成这四条你才算真正把这层楼梯踩实了。提示判断自己是否真正掌握一个抽象层有一个简单自测法——尝试不用任何专业术语用生活语言向一个非技术朋友解释这一层。对方能听懂说明你理解了对方只能记住名词说明你还在背定义。写在最后的一点个人体会如果只让我从这篇文章里留一句话我会留这一句学数据结构与算法学的是在不同抽象层次之间自由往返的能力而不是某个具体的结构或算法。具体的东西会过时但这种“从问题到结构、从结构到实现、从实现到性能”的思维路径会让你受益很多年。我自己的学习经历也是这样当年学红黑树看四遍都记不住旋转。后来放弃直接记忆转而去理解“它是一棵平衡二叉搜索树为了在插入删除时保持高度才引入颜色和旋转”一下就通了。不是我的记忆力变好了而是我终于爬到了正确的台阶上。你会觉得数据结构与算法难大概不是因为笨只是因为还没摸到那架楼梯的扶手。希望这篇文章能帮你找到它。最后分享一个实用小习惯每周花半小时从你自己的工作代码里挑一个模块问自己三个问题——它现在用了什么数据结构如果元素量放大一百倍还会不会快换个结构能不能让代码更简单坚持一个月你再看代码的眼光会明显不一样。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑