ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

程序等于算法加数据结构:从公式到工程实践,彻底理解编程核心

程序等于算法加数据结构:从公式到工程实践,彻底理解编程核心 我最早真正看懂这句话是在一个很尴尬的场合面试官让我现场设计一个“用户最近浏览记录”的功能我张口就是HashMap存userId、LinkedList存记录结果被追问了一句“你知道为什么选这两个结构而不是别的吗”我当时能背出这句话——程序 算法 数据结构但那一刻我才发现自己根本没理解它。后来做了多年开发又面试过不少人我越来越确认这句话是整个计算机软件世界的总纲只是太多人把它当成了考试口号而不是干活指南。这篇内容我会掰开揉碎讲清楚这个公式到底在说什么、算法和数据结构为什么是绑定的、怎么用这个思维去解实际问题以及为什么面试和考试翻来覆去就是考这一点。不论你是刚学编程的学生、准备面试的求职者还是写了几年代码但总觉得差点意思的在职开发者这篇都适合你。1. 这个经典公式到底在说什么1.1 公式的出处和两个常见误读“程序 算法 数据结构”不是某篇博客的标题而是瑞士计算机科学家尼古拉斯·沃斯一本书的名字——《算法 数据结构 程序》这本书出版于1976年沃斯本人也在1984年拿到了图灵奖。这本书影响了几代程序员直到今天它仍然是很多高校计算机专业的必读书目。但这句话被传播得太广之后出现了两个典型的误读。第一个误读是把它当成一个数学等式觉得“算法”和“数据结构”是两个独立零件程序就是把它们焊在一起。实际上它们没法独立存在你写一个“二分查找”算法前提是数据已经按某种有序方式排列在数组里你写一个“遍历链表”的操作算法本身就是顺着指针一个个跳——你不可能用“递归下降法”去遍历链表因为没有那根指针给你递归下降的依据。算法总是针对某种数据组织方式设计的数据结构也总是为某种操作服务的。第二个误读是觉得这句话过时了现在是面向对象、函数式编程、微服务的时代算法和数据结构没那么重要了。这个观点我在工作中听得太多但现实恰恰相反你写出的一行行业务代码翻译成计算机执行层面就是在操作数据、控制流程。你调用一个框架API底层是红黑树还是哈希表直接决定了这个接口的响应时间。即便你写的是胶水代码数据怎么组织、流程怎么走这两件事也躲不掉。所以这个公式真正想说的是写程序本质上只有两件事一是搞清楚你要处理的数据长什么样、怎么存放二是搞清楚从输入到输出中间怎么一步步变换。前者是数据结构后者是算法。两者互为前提缺一不可。1.2 算法和数据结构是一体两面我常说一句话算法是数据结构上长出来的花。还是拿“查找一本书”来打比方。假如图书馆把书按编号整整齐齐排在书架上你找一本指定编号的书最靠谱的办法就是“二分查找”从中间翻开比大小缩小区间。这套流程能跑通前提是书架上的书“按顺序排放”——这个前提就是数据结构。假如图书馆的图书是按主题分区的每个区里没有固定顺序你找一本书就只能“逐本翻阅”这就是线性扫描。再假设图书馆给你一本索引目录按书号记录了每本书的具体位置你查目录直接就能定位到书架——这就是哈希索引。同样一个“找书”的动作因为数据结构不同算法完全不同效率也完全不同。这就是“算法 数据结构”的真实含义它们是同一枚硬币的正反两面。你在选择数据结构的时候实际上就已经在选择算法的路线你在设计算法的时候实际上就是在利用数据结构提供的某种性质。我经常在面试中观察到一个现象候选人背了很多算法模板KMP、快排、堆排张口就来但一问他“为什么哈希表查找是O(1)”他只会说“因为有哈希函数”。再追问“哈希冲突怎么解决冲突多了会退化吗”很多人就卡住了。这说明他对“哈希表”这个数据结构只有模糊印象没有真正理解“数据的组织方式决定了操作的复杂度上限”这件事。2. 用实际代码看懂“数据结构决定算法”这一节我会带着你用非常具体的代码和例子看一下数据结构的差异是如何决定算法选择的。这些都是工程里天天用到的场景。2.1 遍历和查找从数组到链表路线完全不同先看两种最基础的线性结构数组和链表。数组在内存中是连续的一段空间支持随机访问——意思是你可以直接通过下标 arr[5] 跳到第6个元素不需要经过前面5个。链表不一样每个节点除了数据还有一个 next 指针指向下一个节点访问第6个元素必须从头节点开始next 五次才能到达。这个差异直接决定了查找算法怎么写// 数组可以用下标直接定位 int getByIndex(int arr[], int n, int index) { return arr[index]; // O(1) } // 链表只能从头部往后找 typedef struct Node { int data; struct Node *next; } Node; Node* getByIndex(Node *head, int index) { Node *cur head; for (int i 0; i index; i) { if (cur NULL) return NULL; cur cur-next; } return cur; // O(n) }反过来看插入和删除数组在中间插入一个元素需要把后面的元素全部往后搬一格最坏是O(n)链表只需要改两个指针是O(1)。这就是为什么业务上“频繁增删”的场景优先考虑链表而“按下标访问”的场景优先数组。再举一个更贴近业务的例子。假设你要做一个聊天软件新消息来了要追加到会话末尾用户上滑要能按时间从新到旧翻历史记录。如果你用数组存消息列表每次新消息append其实是很快的——数组尾部的插入是O(1)均摊但如果用户要“加载更早的消息”需要往头部插那就是O(n)消息一多就卡。如果换成一个双向链表头插尾插都是O(1)但如果你想按消息ID随机跳到某一条链表就无能为力了。实际工程里这类需求往往是“双向链表 哈希表”一起上——哈希表负责按ID快速定位节点双向链表负责增删。比如Redis的LRU淘汰策略底层就是这种组合结构。你看数据结构一组合算法能力就翻倍了。2.2 排序选哪个排序算法先看你的数据长什么样排序是算法入门的必修课但很多人把排序算法当成八股来背。我建议反过来想每个排序算法其实都是某种数据结构在支撑它的思路。冒泡排序和插入排序本质上是在“数组”上做原地比较和交换每轮保证一个元素归位。它们的代码好理解但最坏时间复杂度是O(n²)数据量一上万就开始吃力。快速排序还是用数组但思路变成了分治。它需要一个支持“随机访问”的结构来做partition——左右两个指针向中间扫描并交换元素。如果给快排一个链表你会发现partition变得非常别扭虽然也能实现但复杂度远高于数组版本。归并排序反而更适合链表。因为它从中间劈成两半、分别排序、再合并这种“拆分成子问题再合并”的思路在链表上甚至比数组更自然不需要像数组那样准备额外空间交替写回。再来看堆排序。它的核心是“堆”这种数据结构——一颗完全二叉树用数组来存储。第一次建堆是O(n)每次取出堆顶并调整是O(log n)整体是O(n log n)。但它的优势不在于跑得快而在于它可以在“只拿到一个大数据集中的前K个最大元素”这种场景下不需要把所有元素都排好序就能工作。实际上工程中最常见的做法是直接调用语言内置排序比如C的std::sort。但理解底层的数据结构逻辑能帮助你在一个Java的PriorityQueue和C std::priority_queue之间明确知道它们都基于堆、插入和弹出堆顶都是O(log n)而不是茫然地“用就完事了”。2.3 哈希表用空间换时间的极致哈希表是算法面试和实际工程中出镜率最高的数据结构没有之一。它之所以查找快核心原理是通过哈希函数把“键”直接映射到“数组下标”一次定位拿到值。在这里“数组”就是那个底层的桶数组哈希函数决定了把元素放到哪个桶里。这就是一个非常典型的“选择数据结构决定算法复杂度”的例子如果不用哈希表你需要遍历所有元素才能找到目标用哈希表理想情况下一次哈希定位就到了复杂度是O(1)。当然哈希冲突无法彻底避免。C的unordered_map和Java的HashMap遇到哈希冲突时会在同一个桶后面挂链表或红黑树。一旦冲突严重查找就从O(1)退化成O(n)甚至O(log n)。做工程时如果数据量巨大设计一个分布均匀的哈希函数比多写几十行业务代码都重要。哈希表这种“用空间换时间”的思维也延伸出了很多高级用法。比如布隆过滤器本质上是“多个哈希函数 一个位数组”用来判断一个元素是否“一定不存在”。它在缓存穿透防护、垃圾邮件过滤里非常常用。你看搞懂底层的数据结构组合就能玩出很多花样。3. 热词背后的算法为什么总在考这些这几年入行的人越来越多大家在网上搜“算法”“数据结构”的频率明显高了。我结合一些热门算法来聊聊它们为什么常考、常聊以及它们的底层逻辑到底是什么。3.1 KMP算法字符串匹配里藏着“自我结构”字符串匹配是最朴素的需求你要在一段文字中找某个关键词。最直观的办法是暴力匹配——在主串每个位置尝试匹配模式串失配就往后移一位再试最坏O(n*m)。KMP算法的高明之处在于它发现失配时主串的指针不用回溯只需要把模式串的指针移动到“此前已匹配部分的最长相等前后缀”的下一位。换而言之它对模式串做了一次“自我分析”预处理好一个next数组也就是前缀函数。很多初学者觉得KMP难是因为这个next数组抽象。它本质上就是一个“模式串的失配时回退表”而这个表是模式串自身前缀和后缀的匹配信息。这里面的数据结构思维是你需要把模式串当文本对它自己做一遍“匹配分析”得到一张表。这张表就是一种很有价值的数据结构。实际工程中KMP用的地方反而不多因为大部分字符串搜索会交给内置的高效函数。但面试爱考它因为它非常考验候选人对“状态转移、预处理、指针不回退”这些思想的掌握程度。把KMP理解透了很多类似“状态机”“AC自动机”的前缀匹配问题都会变得容易很多。3.2 Prim算法与最小生成树图论里的“贪心堆”Prim算法是图论中求最小生成树的经典算法。通俗说就是在带权无向图中找到一棵连接所有节点、且总边权最小的树。Prim的思路很直接从一个起点开始维护一个“已连通的点集合”在连接集合内与集合外的所有边里挑一条最小的把对应的新点加入集合重复直到覆盖全部节点。这个“挑最小边”的操作正是“优先队列最小堆”大显身手的地方。每一次从堆里弹出最小边是O(log V)遍历所有边总共是O(E log V)。如果不用堆每次线性扫描所有边找最小值复杂度就变成O(V²)。所以很多人没意识到Prim算法不是一个孤立的“算法模板”它是“图结构 贪心思想 堆结构”三者结合的产物。你在LeetCode上刷最小生成树其实就是在训练这种“把算法思想落到合适数据结构上”的能力。3.3 粒子群等智能优化算法也是在某种数据结构上迭代粒子群算法、遗传算法、模拟退火这一类的“启发式算法”这几年特别火因为工程里很多优化问题没有解析解只能靠迭代搜索近似解。粒子群算法特别有意思一群候选解也就是粒子每个粒子有位置向量和速度向量。每次迭代每个粒子根据自己历史最优位置、群体历史最优位置来更新速度再更新位置。最后在解空间里逐渐收敛。你仔细想想粒子群算法的数据结构是什么呢其实就是一个由“位置 速度”组成的粒子数组外加用于记录个体最优和全局最优的变量。算法本身不过是一套更新公式但整套流程跑在哪个数据结构上决定了它能不能并行、能不能扩展到高维问题。这类算法的共性启发是当你面临一个没有明确公式的问题时先去构建一个“解的表示结构”再去想搜索策略。这个思考顺序仍然先是数据结构后是算法。3.4 哈希链一种改变真实世界的数据结构聊到“数据结构能产生多大影响”我特别想提一下“哈希链”这个概念。传统的链表每个节点通过指针指向下一个节点。而哈希链的特殊之处在于每个区块里不仅存数据还存了前一个区块内容的哈希值。也就是说后一个区块通过哈希值“锁住”前一个区块的完整性。想篡改任何一个历史区块必须把后续所有区块的哈希全部重算出来。比特币的区块链数据结构就是这种哈希链。它之所以号称“难以篡改”不是靠某个高大上的算法而是靠这个极度简洁的数据结构设计。你看一个新数据结构的诞生甚至能支撑起一个全新的应用生态。这就是“程序 算法 数据结构”这句话在宏观层面的力量——它不止关乎面试也关乎你能否设计出有生命力的系统。4. 面试和考试反复考算法与数据结构到底在考什么4.1 从“高频核心知识点”看考核逻辑有很多人在搜“数据结构高频核心知识点面试”说明大家都想把面试范围压到最小。那我说句实在话面试官不是真要你背红黑树旋转的每行代码而是在考察四件事。第一抽象建模能力。给你一个模糊需求你能不能把它提炼成“什么东西、什么关系、什么操作”。比如“设计一个支持随机获取元素的集合且删除时也要O(1)”如果你能想到“数组 哈希表”组合那说明你完成了建模。第二复杂度意识。面试官很在意你写的代码在数据量放大十倍、百倍后还撑不撑得住。第三代码落地能力。即使你背了思路能不能在十几分钟内写出无Bug的代码这是另一回事。第四沟通与权衡。你选择了这个方案能不能说清楚它牺牲了什么、换来什么。高频知识点其实很集中数组、链表、栈、队列、哈希表、二叉树、堆、图再加字符串处理和查找排序。如果你认真啃过这8类结构的“特性、适用场景、常见操作复杂度”再配合刷上几十道经典题应付绝大多数面试绰绰有余。4.2 一种实战验证过的学习路线很多初学者一上来就抱着《算法导论》啃结果看了一个月还在数学证明里打转代码根本写不出来。我走过的弯路不少现在回头看比较有效的路线是这样的。先用一门语言把基础数据结构亲手实现一遍。不是用库而是自己写动态数组、单链表/双链表、栈、队列、哈希表、二叉搜索树、大顶堆/小顶堆。写的时候你会深刻理解“指针怎么移动、内存怎么分配、为什么有的操作是O(1)”。这一步很多人跳过了所以我强烈建议补上。再学针对性算法每学一个都问自己“它依赖结构的哪个特性”。比如二分查找依赖有序数组的随机访问归并排序依赖递归分治和合并两个有序序列BFS依赖队列的先进先出DFS依赖栈或递归。把算法和结构绑定记忆比孤立背模板有用得多。最后刷题时不要只追求数量。做完一道题认真写注释标注用了什么结构、为什么用这个、时空复杂度多少、能不能优化。这比一天做十道题而第二天全忘掉有效十倍。参考书的话我读书时对《大话数据结构》和严蔚敏的《数据结构C语言版》印象最深前者轻松入门、后者系统严谨适合互补着看。5. 实际工程中如何运用“算法 数据结构”思维5.1 从需求出发反推数据结构无论是给小程序做“动态设置标题”的功能还是设计一个订单系统你都会发现需求永远是“有什么数据要支持哪些操作有多大数据量”。把这些弄清楚该用什么数据结构就呼之欲出了。举一个我实际做过的例子一个后台运营系统需要展示订单列表要求支持按时间倒序分页、按订单号精确查询、按用户ID聚合统计。假如只用一个数组存订单查询得全扫假如只用哈希表存订单就没法按时间倒序拿到序列。最终我的方案是订单核心数据放在一张MySQL表主键索引就是B树天然支持范围查询和排序同时启动时把订单号到主键的关系加载进哈希表用来做O(1)的精确查找。大型系统里常见的“关系库 缓存”架构本质上也是这种“不同数据结构服务不同操作”的思路。所以下次接到需求先别急着写代码。在纸上列一下我的数据是什么形态用户最频繁的操作是什么一次请求最多能忍受多慢。这步想透了技术选型自然清楚。5.2 选择数据结构时的实际考量很多人觉得只要复杂度低就选它但实际工程里远没这么简单。我整理了一个对比表能帮你快速做取舍数据结构优势代价适合场景数组随机访问快O(1)、内存连续、CPU缓存友好插入删除慢O(n)、扩容有拷贝成本读多写少、按索引访问链表插入删除快O(1)、天然支持动态扩容随机访问慢O(n)、内存不连续缓存不友好频繁增删、LRU等哈希表查找/插入/删除均摊O(1)占空间、哈希函数和冲突处理需设计精确匹配、去重、计数二叉搜索树有序性、插入删除查找O(log n)可能退化成链表需平衡红黑树/AVL有序集合、区间查找堆取最值O(1)、插入O(log n)无法快速查找任意元素优先队列、TopK、调度图表达复杂关系结构复杂、遍历代价高网络分析、推荐系统在工程里还有一个容易被忽略的因素——实现的复杂度和可维护性。如果团队里别人不熟悉平衡树你非要写一棵红黑树来管理一个只有几千条数据的配置表那就是过度设计。反过来说如果数据量达到百万级别还用一个O(n)的线性扫描来实现“按唯一键查找”那就是给自己埋坑。先估算数据规模再选结构这才叫工程思维。5.3 常见误区与踩坑经验我见过太多人在“算法 数据结构”上踩坑这里挑几个典型的分享出来。第一个坑只学高级结构忽略基础结构。有人一上来就刷LRU、跳表、红黑树但最基本的ArrayList和LinkedList区别都说不清楚。其实90%的业务场景用数组、哈希表、队列、堆就足够了。把基础搞扎实比什么都强。第二个坑把“复杂度低”等同于“性能好”。复杂度只是理论模型真实性能还受缓存命中率、内存分配频率、ILP指令级并行等因素影响。比如数组的线性遍历可能比哈希表的散列跳转还快特别是数据量小的时候。我处理过一个性能问题用了vector后反而比list慢因为频繁在中间插入导致大量元素搬迁而数据量又不大搬迁成本不可忽略。这种情况就需要用数据说话而不是空谈复杂度。第三个坑写完代码不验证边界。做算法题时数组越界、空输入、只有一个元素这些边界条件往往就是Bug的藏身之所。我给代码写单元测试时一定会覆盖空场景和极端大场景。很多线上事故起因都是边界条件没处理干净。第四个坑不会分析数据规模。你问候选人“这个功能用什么数据结构”如果对方上来就答“用红黑树”却没说为什么我反而会害怕。因为“产品要求查询小于100毫秒”和“数据量一年能到1亿条”这两者的方案完全不同。先问数据量、再问读写比、再问延迟要求这个分析顺序才是从业者的基本功。最后分享一点我的个人体会做了这些年开发和面试官我越来越觉得“程序 算法 数据结构”不是一句要背的公式而是一种观察系统的视角。当你能从每个需求里看到“数据”和“操作”这两条线你就不会再被各种花哨的框架名称迷惑——那些框架本质上也是在特定数据结构上提供了一组封装好的算法。如果你正在学这个方向我的建议只有一条不要怕慢亲手把每个基础数据结构实现一遍再回头去刷题你会发现以前看不懂的题目突然都有了解题直觉。那些你踩过的坑、调过的内存越界、排查过的线上卡顿都会变成你真正理解这句话的养分。
返回列表