尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

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

算法修炼入门:复杂度分析、数据结构与核心算法思想精讲

算法修炼入门:复杂度分析、数据结构与核心算法思想精讲 1. 项目概述从“练气”到“筑基”的算法修炼之路最近在社区里看到不少朋友在讨论算法学习感觉大家普遍存在一种焦虑刷了不少LeetCode看了很多教程但遇到新问题还是无从下手或者面试时被问到原理就卡壳。这让我想起了自己早年学习算法的经历那种感觉就像武侠小说里练功招式学了一堆但内功心法没跟上实战起来总是差那么一口气。所以我想用“算法修炼”这个系列来系统地聊聊如何真正掌握算法而不仅仅是“刷题”。今天这篇“练气篇——练气八层”就是整个修炼体系的地基对应的是算法与数据结构中最核心、最基础的八个概念层。这八层练好了你才算真正“引气入体”为后续的“筑基”、“金丹”乃至更高境界打下不可动摇的基础。无论你是准备求职面试的学生还是希望提升工程能力的开发者这套心法都能帮你构建起清晰的算法知识图谱告别盲目刷题实现从“记忆解法”到“掌握思想”的本质飞跃。2. 修炼总纲为什么是这八层在开始具体修炼之前我们必须先统一思想明确修炼的目标和路径。很多初学者会陷入一个误区认为算法学习就是“题目”和“答案”的对应关系热衷于收集各种“解题模板”和“高频题解”。这种学法短期或许能应付一些固定模式的笔试但长期来看知识是零散且脆弱的无法应对复杂多变的实际问题。我提出的“练气八层”心法其核心在于构建自顶向下的理解框架和培养问题分解与抽象的能力。它不是八个孤立的知识点而是一个环环相扣、逐层递进的认知体系。每一层都解决一类根本性的问题并为下一层提供支撑。第一层“复杂度分析”是衡量算法优劣的标尺。不懂复杂度就像比武不知轻重无法评价一个解法是“巧劲”还是“蛮力”。第二层“数组与链表”是数据存储的两种最基本形态理解了它们的物理特性才能明白后续所有高级数据结构为何被设计出来。第三层“栈与队列”和第四层“哈希表”是处理特定操作模式后进先出、先进先出、快速查找的利器它们是许多算法流程中的“标准件”。第五层“递归”是一种强大的思维模式是理解树、图、分治、回溯的钥匙。第六层“排序”和第七层“二分查找”则是算法世界中经久不衰的“经典范式”其思想渗透在无数问题之中。最后的第八层“双指针”是一种简洁高效的编程技巧是优化暴力解法的常见手段。这八层从理论复杂度到基础结构再到经典范式与技巧构成了一个完整的入门闭环。掌握它们意味着你拥有了分析问题、选择工具、设计解决方案的基本功。3. 第一层复杂度分析——内功的度量衡复杂度分析是算法能力的基石也是面试中必考的内容。但很多人只记住了O(n), O(n²)这些符号却不理解其背后的意义和计算方法。3.1 大O记号究竟在表达什么大O时间复杂度表示的是一种渐进趋势或者说增长级别。它关注的是当输入规模n趋于无穷大时算法执行时间或空间增长的主要矛盾。我们常说O(n)的算法比O(n²)的好其深层含义是随着数据量n的增大前者的资源消耗增长速度远慢于后者。计算大O的核心原则是“抓大放小”忽略常数项O(2n 10) 简化为 O(n)。因为当n很大时“10”和系数“2”的影响微乎其微。忽略低阶项O(n² n 100) 简化为 O(n²)。因为n²的增长速度最终会远远超过n和100。关注最坏情况我们通常用最坏情况复杂度来保证算法的性能下限这比平均情况更具参考价值。注意大O描述的是最坏情况下的增长趋势但实际工程中平均时间复杂度和均摊时间复杂度也很有价值。例如哈希表插入操作虽然单次最坏是O(n)但均摊下来是O(1)。3.2 实战心法如何一眼看穿复杂度死记公式不如掌握分析方法。这里分享一个我常用的“快速判断法”看循环这是最直接的线索。单层循环且循环次数与n成线性关系 →O(n)。双层嵌套循环且每层都与n相关 →O(n²)。如果是“外层n次内层每次递减”的遍历如冒泡排序也是O(n²)。循环变量以倍数增长i * 2或衰减i / 2 →O(log n)。这是二分查找、堆操作的特征。看递归递归复杂度分析稍复杂主要看递归调用次数和每次递归的成本。二分递归每次递归分成两半且每次操作是O(1)如归并排序 →O(n log n)。计算方式递归树深度为log n每层总工作量为n。线性递归递归深度为n每次操作O(1)如计算斐波那契数列低效版 →O(2^n)。这是一个指数灾难务必警惕。看数据结构操作了解常用数据结构的操作成本是关键。数组的随机访问是O(1)但中间插入/删除是O(n)。链表的插入/删除已知节点是O(1)但随机访问是O(n)。哈希表的查找、插入、删除平均是O(1)。平衡二叉搜索树如AVL、红黑树的各项操作是O(log n)。实操心得面试时被问到复杂度不要只丢出一个结果。最好能边走查代码边解释“这里有一个外层循环遍历n次内层循环在最坏情况下也遍历n次所以是O(n²)。” 这种分析过程比答案本身更能体现你的功底。4. 第二层数组与链表——数据的筋骨与脉络数组和链表是物理存储的两种元模型理解它们的差异是选择一切数据结构的出发点。4.1 数组秩序井然的“连续军营”数组在内存中占用一块连续的空间。这个特性带来了它的核心优势支持O(1)时间的随机访问。因为地址是连续的通过“基地址 索引 * 元素大小”的公式可以瞬间计算出任何一个元素的位置。优势高速访问随机访问是O(1)。缓存友好由于内存连续性CPU缓存预取机制效率高访问相邻元素速度极快。空间开销小只存储数据本身无需额外指针。劣势大小固定静态数组需要在编译时或初始化时确定大小不够灵活。动态数组如C的vectorJava的ArrayList通过在容量不足时申请新的更大连续内存并拷贝数据来实现扩容这个扩容操作的时间复杂度是O(n)。插入删除低效在数组中间插入或删除元素需要移动后续所有元素以保持连续性平均时间复杂度为O(n)。应用场景当你需要频繁按索引访问元素且数据集合大小相对稳定时数组是首选。例如存储一个图片的像素矩阵、实现一个大小固定的循环缓冲区。4.2 链表灵活多变的“离散驿站”链表中的元素节点在内存中不是连续存储的每个节点除了存储数据还存储指向下一个节点单链表或前后节点双链表的指针。优势动态大小可以非常方便地添加或移除节点无需担心容量问题。高效插入删除在已知节点位置的情况下插入或删除一个节点只需要修改几个指针时间复杂度是O(1)。这是链表相对于数组最大的优势。劣势随机访问低效要访问第i个元素必须从头节点开始逐个遍历时间复杂度是O(n)。空间开销大每个节点都需要额外的空间来存储指针。缓存不友好节点分散在内存各处对CPU缓存不友好访问速度可能慢于数组。应用场景适用于需要频繁在任意位置插入和删除元素的场景而随机访问需求很少。例如实现LRU缓存淘汰算法、管理浏览器历史记录前进后退、多项式相加等。4.3 核心对决与选择策略如何选择我总结了一个简单的决策流是否需要频繁按索引随机访问是 → 优先考虑数组或基于数组的动态数组。数据规模是否频繁剧烈变化且插入删除操作远多于访问操作是 → 优先考虑链表。是否对内存使用非常敏感是 →数组通常更节省空间。代码是否追求极致的局部性能缓存效率是 →数组更优。常见问题与排查链表操作中指针丢失在插入或删除节点时务必注意修改指针的顺序。一个经典的错误是在单链表插入时先断开了前驱节点的next导致链表断裂。正确的顺序通常是新节点指向目标节点然后前驱节点再指向新节点。数组越界这是数组编程中最常见的错误之一。始终牢记数组索引从0开始有效范围是[0, size-1]。在循环中务必检查边界条件。动态数组的“摊销”成本虽然动态数组单次扩容代价高O(n)但将扩容成本平摊到多次插入操作上其均摊时间复杂度仍然是O(1)。理解这一点就不会因为害怕扩容而不敢使用vector或ArrayList。5. 第三层栈与队列——有规矩的流水线栈和队列是限制元素操作顺序的线性数据结构它们体现了“特定顺序”的抽象。5.1 栈后进先出的“叠盘子”栈是一种LIFOLast-In-First-Out结构。只允许在一端栈顶进行插入入栈/push和删除出栈/pop操作。核心操作push(x): 将元素x压入栈顶。pop(): 弹出并返回栈顶元素。peek()/top(): 返回栈顶元素但不弹出。isEmpty(): 判断栈是否为空。实现方式既可以用数组需要维护一个栈顶指针也可以用链表将链表头部作为栈顶。数组实现更简单、缓存友好链表实现则无需担心容量。应用场景函数调用栈这是栈最经典的应用。每次函数调用系统会将返回地址、局部变量等压入调用栈函数返回时再依次弹出。表达式求值如逆波兰表达式。括号匹配检查遍历字符串遇左括号入栈遇右括号则检查栈顶是否匹配。浏览器的前进后退通常使用两个栈来实现。深度优先搜索DFS的非递归实现本质上就是用栈来模拟递归过程。5.2 队列先进先出的“排队”队列是一种FIFOFirst-In-First-Out结构。允许在一端队尾插入入队/enqueue在另一端队头删除出队/dequeue。核心操作enqueue(x): 将元素x加入队尾。dequeue(): 移除并返回队头元素。front(): 获取队头元素但不移除。isEmpty(): 判断队列是否为空。实现难点——循环队列用数组实现简单队列时随着元素出队队头指针后移数组前端会空出无法使用的空间造成“假溢出”。循环队列通过将数组视为一个环来解决这个问题。判断队满和队空的条件需要仔细设计通常采用“牺牲一个存储单元”或“维护一个计数变量”的方法。应用场景广度优先搜索BFS中队列用于按层遍历节点。任务调度如CPU的任务队列、打印机的打印队列。消息队列在分布式系统中异步处理消息。缓存如最近使用缓存的一种简单实现。5.3 栈与队列的混合与变体双端队列两端都可以进行插入和删除操作它融合了栈和队列的能力非常灵活可以用来实现滑动窗口最大值等问题。单调栈/单调队列这是栈和队列的“高级玩法”。它们维护栈内或队列内元素的单调性递增或递减常用于解决“下一个更大元素”、“滑动窗口最值”等一类问题能将O(n²)的暴力解法优化到O(n)。理解其“及时排除无用元素”的思想是关键。实操心得在解决算法问题时当你发现需要“回溯”到之前的某个状态或者操作顺序是“后来先到”就应该立刻想到栈。当你需要“按顺序处理”、“分层遍历”时队列就是你的工具。对于循环队列我建议在纸上画一个环形数组手动模拟几次入队出队彻底理解队头、队尾指针移动以及判空判满的条件这比死记硬背公式有效得多。6. 第四层哈希表——瞬间定位的魔法字典哈希表是练气八层中实现查找操作效率的质变点它提供了平均情况下O(1)时间的查找、插入和删除能力。6.1 哈希函数从键到地址的映射哈希表的核心思想是通过一个哈希函数将任意大小的键Key映射到一个固定范围的数组索引桶位置。理想情况下不同的键映射到不同的索引。但现实是不同的键可能映射到相同的索引这就是哈希冲突。一个好的哈希函数应该满足确定性相同的键必须产生相同的哈希值。高效性计算速度快。均匀性键的哈希值应尽可能均匀地分布在数组空间中减少冲突。6.2 冲突解决当两个键挤进同一个房间冲突不可避免主要有两种解决方法链地址法这是最常用的方法。数组的每个位置桶不再直接存储一个元素而是存储一个链表或红黑树的头节点。所有哈希到同一位置的元素都放在这个链表中。查找时先计算哈希值找到桶再在桶内的链表中进行线性查找。优点实现简单对装载因子不敏感。缺点需要额外的指针空间如果链表过长性能会退化为O(n)。Java 8中的HashMap在链表长度超过一定阈值默认为8时会将其转换为红黑树以保证最坏情况下的性能为O(log n)。开放地址法当发生冲突时按照某种探测序列如线性探测、二次探测、双重哈希在哈希表中寻找下一个空闲位置。线性探测如果位置i被占则尝试i1, i2, … 直到找到空位。优点所有数据都存储在数组中缓存性能更好没有指针开销。缺点容易产生“聚集”现象删除操作复杂需要特殊标记且装载因子必须保持在较低水平通常0.7否则性能急剧下降。6.3 关键参数装载因子与扩容装载因子 已存元素个数 / 哈希表桶的总数。它衡量了哈希表的拥挤程度。装载因子越高冲突概率越大性能越差。扩容当装载因子超过某个阈值如0.75为了维持O(1)的操作性能就需要进行扩容。通常创建一个新的、更大的数组例如两倍大小然后遍历旧表中的所有元素用哈希函数重新计算它们在新表中的位置并插入。这是一个O(n)的操作但由于是偶尔发生其均摊成本仍是O(1)。应用场景哈希表适用于所有需要快速查找、去重或统计频率的场景。例如实现缓存、数据库索引、统计单词频率、检查重复元素等。常见问题与排查自定义对象作为键在Java或C中如果你用自定义类的对象作为HashMap或unordered_map的键必须重写hashCode()和equals()方法Java或特化std::hash和重载运算符C。确保逻辑上相等的对象具有相同的哈希值否则会导致无法正确查找。哈希攻击如果哈希函数容易被预测恶意攻击者可以构造大量具有相同哈希值的键使哈希表退化为链表从而导致服务拒绝。因此在安全敏感的场景需要使用加密哈希函数或随机化哈希种子。7. 第五层递归——自己调用自己的艺术递归是一种强大的编程技巧和思维方式它让代码变得简洁优雅但也是初学者最容易感到困惑和出错的地方。7.1 递归三要素一个有效的递归必须包含三个部分缺一不可递归终止条件也称为基线条件。这是递归的出口防止无限调用导致栈溢出。必须有一个或多个最简单的情况可以直接得出答案而不再进行递归调用。递归调用函数直接或间接地调用自身但每次调用都应该是向终止条件靠近的一步。参数通常会发生变化例如规模减小。向终止条件演进每一次递归调用都必须使问题规模缩小或者状态朝着终止条件变化。以经典的阶乘函数为例def factorial(n): # 1. 终止条件 if n 0 or n 1: return 1 # 2. 递归调用 3. 向终止条件演进 (n 在减小) return n * factorial(n - 1)7.2 理解递归从“递”到“归”理解递归的关键在于放弃跟踪每一层调用的细节而是相信递归函数已经能解决更小规模的问题。这种思维称为“递归信念”。以二叉树深度为例def maxDepth(root): if not root: # 终止条件空树深度为0 return 0 # 相信 maxDepth 能正确求出左子树和右子树的深度 left_depth maxDepth(root.left) right_depth maxDepth(root.right) # 当前树的深度 左右子树深度的最大值 1 (当前节点) return max(left_depth, right_depth) 1你不必在脑子里展开所有递归调用只需要相信maxDepth(root.left)返回的就是左子树的深度。你的任务只是定义好如何用更小问题的解来组合成当前问题的解。7.3 递归的代价与优化递归虽然简洁但也有代价函数调用开销每次递归调用都会在调用栈上分配空间保存返回地址、参数、局部变量等。栈溢出风险递归深度过大如处理超长链表或深度极大的树会耗尽栈空间导致程序崩溃。优化策略——尾递归 如果递归调用是函数体执行的最后一步操作并且返回值直接就是递归调用的结果那么这种递归称为尾递归。某些编译器如GCC的某些优化级别可以对尾递归进行优化将其转换为循环从而避免栈帧的累积消除栈溢出风险。但很多语言如Python、Java的编译器并不支持尾递归优化。实操心得初学递归时我建议多用纸笔画一画“递归树”或“调用栈”这对于理解执行流程和调试非常有帮助。当遇到复杂的递归问题如回溯、DFS时先明确“当前层做什么”再定义好“需要下一层返回什么”最后想清楚“如何组合子问题的结果”。对于深度可能很大的递归一定要优先考虑是否能用迭代循环栈来改写特别是在生产环境中。8. 第六层排序——让混乱数据归位的法则排序是算法世界的经典课题不同的排序算法体现了不同的设计思想分治、减治、插入、选择等。我们不仅要会写代码更要理解每种算法的思想和适用场景。8.1 比较排序的“天花板”O(n log n)基于比较的排序算法如快排、归并、堆排其时间复杂度下限是O(n log n)。这是通过决策树模型可以证明的。理解这个下限很重要它告诉我们不基于特殊假设如数据范围有限不可能有比O(n log n)更快的一般性比较排序算法。8.2 三大O(n log n)排序算法深度对比特性快速排序归并排序堆排序平均时间复杂度O(n log n)O(n log n)O(n log n)最坏时间复杂度O(n²)O(n log n)O(n log n)空间复杂度O(log n) ~ O(n) (递归栈)O(n) (辅助数组)O(1) (原地)是否稳定通常不稳定稳定不稳定核心思想分治选取pivot分区分治先分后合利用堆数据结构优势平均性能最好缓存友好稳定性能稳定原地排序空间效率高劣势最坏情况性能差不稳定需要额外O(n)空间缓存不友好实际常数项大选择策略追求综合性能通常选择快速排序。通过随机化选择pivot或三数取中法可以极大避免最坏情况O(n²)的发生。C的std::sort、Java的Arrays.sort()对对象数组底层都使用了快速排序的变体。需要稳定性选择归并排序。稳定性是指相等元素的相对顺序在排序后保持不变。这在多关键字排序时很重要。对空间有严格限制选择堆排序或希尔排序。堆排序是唯一的原地、最坏情况下也是O(n log n)的排序算法但实际运行速度通常不如优化过的快排。8.3 线性排序突破比较的限制当数据满足特殊条件时我们可以突破O(n log n)的限制实现线性时间排序。计数排序适用于数据范围k不大如0-100的分数的整数排序。思想是统计每个值出现的次数然后按顺序输出。时间复杂度O(n k)。桶排序将数据分到有限数量的有序桶里每个桶内再单独排序。适用于数据均匀分布的情况。基数排序从最低位到最高位依次对每一位进行稳定排序通常用计数排序。适用于整数或字符串排序。实操心得面试中手写排序代码是常考题。我建议至少熟练掌握快速排序和归并排序的递归与非递归写法。写快排时特别注意分区函数的实现这是最容易出错的地方。一个稳健的分区函数如Lomuto分区或Hoare分区是写好快排的关键。另外要能清晰解释算法的稳定性、时间/空间复杂度以及优化方法。例如对于小规模数据如n15插入排序通常比快排更快因此工业级的排序实现如std::sort往往是混合算法。9. 第七层二分查找——劈开搜索空间的利刃二分查找的思想极其简洁而强大在有序集合中通过每次比较将搜索范围缩小一半从而以O(log n)的时间复杂度找到目标。9.1 标准模板与细节魔鬼二分查找的代码看似简单但边界条件left right还是mid如何计算更新left和right时用mid还是mid±1是著名的“坑点”。我推荐一个统一、不易出错的模板def binary_search(nums, target): left, right 0, len(nums) - 1 # 搜索区间为闭区间 [left, right] while left right: # 当区间不为空时继续 mid left (right - left) // 2 # 防止(leftright)溢出 if nums[mid] target: return mid # 找到目标 elif nums[mid] target: left mid 1 # 目标在右半部分调整左边界 else: # nums[mid] target right mid - 1 # 目标在左半部分调整右边界 return -1 # 未找到关键点解析循环条件left right这表示搜索区间是[left, right]一个有效的闭区间。当left right时区间为空循环终止。中间值计算mid left (right - left) // 2这是计算中点最安全的方式避免了(left right) // 2在left和right很大时可能发生的整数溢出。边界更新left mid 1和right mid - 1因为我们已经明确判断了nums[mid]不等于target所以可以放心地将mid排除在下一轮搜索区间之外。9.2 二分查找的变体与应用二分查找不仅用于查找确切值更常用于解决边界问题和判定性问题。寻找左侧边界在有序数组中找到第一个等于或大于等于target的元素位置。def left_bound(nums, target): left, right 0, len(nums) # 搜索区间为左闭右开 [left, right) while left right: mid left (right - left) // 2 if nums[mid] target: # 关键即使等于也收缩右边界 right mid else: left mid 1 return left # left 是第一个 target 的位置这种写法搜索区间是[left, right)循环条件是left right更新时right mid或left mid 1。它最终返回的是target的插入位置第一个大于等于target的索引。寻找右侧边界找到最后一个等于或小于等于target的元素位置。思路类似调整判断条件。二分答案法这是二分查找思想更高级的应用。当问题的答案具有单调性并且我们可以设计一个判定函数check(mid)来判断某个候选答案mid是否可行时就可以在答案的可能范围内进行二分搜索。经典问题在有序矩阵中查找、寻找旋转排序数组中的最小值、在D天内运送包裹的能力、分割数组的最大值等。步骤确定答案的搜索范围[low, high]。设计check(mid)函数判断mid作为答案是否“可行”。如果check(mid)为真说明答案可能更小或更大取决于单调性调整搜索边界。最终low或high即为所求答案。实操心得死记硬背模板容易混淆。我的建议是明确你定义的搜索区间是开区间还是闭区间并在整个循环中保持这个定义不变。在纸上画一个数轴标出left,right,mid模拟更新过程是调试二分查找代码最有效的方法。对于“二分答案”类问题难点往往在于check函数的编写这需要你对问题本身有深刻的理解。10. 第八层双指针——并肩而行的遍历艺术双指针不是一种具体的数据结构而是一种广泛使用的编程技巧。它通过使用两个或多个指针协同遍历数组或链表通常能将O(n²)的暴力解法优化到O(n)。10.1 双指针的三种主要形式同向快慢指针两个指针从同一侧开始一快一慢向前移动。应用移除有序数组中的重复项快指针探路慢指针维护新数组、判断链表是否有环快指针每次两步慢指针每次一步相遇则有环、找到链表的中间节点。相向对撞指针一个指针在起始位置一个指针在末尾同时向中间移动直到相遇。应用两数之和在有序数组中、反转数组、验证回文串、盛最多水的容器。滑动窗口这是同向指针的一种特殊形式两个指针维护一个区间窗口通过移动左右指针来动态调整窗口大小以满足特定条件。应用寻找满足条件的最短/最长子数组、字符串覆盖问题、频率统计问题。这是解决子串/子数组问题的利器。10.2 滑动窗口模板详解滑动窗口的难点在于弄清楚何时移动右指针扩大窗口何时移动左指针收缩窗口。以下是一个寻找最小覆盖子串问题的通用思想框架def sliding_window_template(s, t): need {} # 记录目标字符需求 window {} # 记录窗口中字符计数 # 初始化need for c in t: need[c] need.get(c, 0) 1 left right 0 # 窗口左右指针初始窗口 [left, right) 为空 valid 0 # 窗口中满足need条件的字符个数 # 记录结果 start, length 0, float(inf) while right len(s): # c 是将移入窗口的字符 c s[right] # 右移窗口 right 1 # 进行窗口内数据的一系列更新 if c in need: window[c] window.get(c, 0) 1 if window[c] need[c]: valid 1 # 判断左侧窗口是否要收缩 while valid len(need): # 当窗口满足条件时 # 更新最优解 if right - left length: start left length right - left # d 是将移出窗口的字符 d s[left] # 左移窗口 left 1 # 进行窗口内数据的一系列更新 if d in need: if window[d] need[d]: valid - 1 window[d] - 1 # 返回结果...核心思想right指针负责扩大窗口直到窗口内的子串满足要求。left指针负责收缩窗口在满足要求的前提下寻找最优解如最短长度并更新窗口状态。常见问题与排查窗口收缩条件不清这是最容易出错的地方。务必明确收缩窗口是为了寻找下一个可能的最优解或者使窗口重新变得“不满足条件”以便右指针可以继续探索。状态更新错误在移动左右指针时对window和valid等状态变量的更新必须对称且准确。特别是左指针移动时减少计数的操作要在判断是否影响valid之后进行。指针越界始终确保指针在有效范围内移动。修炼至此“练气八层”的心法已传授完毕。这八层功力层层递进构成了算法世界的入门基石。复杂度分析让你心中有尺数组链表让你知悉根本栈队列哈希让你手握利器递归让你思维升维排序查找让你掌握经典范式双指针让你优化遍历。真正的掌握不在于背诵而在于理解每一个选择背后的“为什么”并在大量的实战中形成肌肉记忆和条件反射。当你拿到一个新问题能下意识地分析其数据特征与操作需求并迅速从这八层“兵器库”中组合出合适的工具时你便算真正“练气”圆满可以准备向更复杂的“筑基”境界如树、图、动态规划、贪心算法等进发了。记住所有的高楼大厦都离不开扎实的地基。反复琢磨这八层动手实现每一个基础数据结构和算法理解它们的每一行代码你的算法之路必将越走越稳。
返回列表