数组基础与二分查找双指针实战:从连续存储到O(1)随机访问
今天是代码随想录算法训练营的 Day01主题是数组 part01。说实话数组这个知识点我自认为早就“会了”但真跟着训练营重新过一遍才发现从前很多理解都是浮在表面——比如为什么数组的增删是 O(n) 但查询是 O(1)为什么二分查找的边界条件能写错一整天为什么明明“会双指针”却做不对移除元素这篇笔记我打算把 Day01 的完整思路、代码实现和踩坑记录都摊开讲清楚给同样在刷算法、准备面试或者纯粹想打牢数据结构地基的朋友一个可以直接照着走的地图。如果你正准备刷 LeetCode或者被各种笔试面试题虐得怀疑人生我强烈建议你从头把数组这块的地基夯实。别急着去刷什么难题怪题二分查找能闭着眼写对、双指针能讲明白复杂度你后面学链表、哈希表、滑动窗口都会轻松很多。这篇文章不假设你有任何基础但也不会废话连篇——每个概念我都会拆开揉碎配上代码、对比表和我自己实际写错过的现场争取让你看完就能上手。1. 数组理论基础学算法前先把底层逻辑盘明白1.1 为什么数组是这个世界上“最自然”的数据结构数组的底层原理一句话就能讲完内存中一段连续的空间按顺序排布着同类型的数据。听起来简单但这三个关键词——“连续”“有序”“同类型”——决定了数组的一切优缺点。先说“连续”。你可以把内存想象成一排编好号的储物柜数组就是在其中“包”下连续的一整排柜子。因为连续所以知道了第一个元素的位置首地址再知道每个元素占多大空间第 i 个元素的位置可以直接算出来首地址 i × 单个元素大小。这个计算没有任何循环、没有任何跳转所以数组的随机访问时间复杂度是 O(1)。这就是为什么数组的“查询”快得离谱。但“连续”也带来了最大的代价如果你想在数组中间插入或删除一个元素你没法只动那一个位置必须把后面的所有元素整体往后挪或往前挪。这意味着增删操作的时间复杂度是 O(n)。这就好比电影院连坐票你买了一个中间的座位后面来个人非要坐你旁边所有人都得起身挪一个位。再说“同类型”。数组里存的每个元素大小必须一样否则“首地址 i × 元素大小”这个公式就不成立了。这也是为什么在 C/C 里数组不能混存 int 和 string而在 JS 这类弱类型语言里数组可以混存——因为 JS 数组本质上已经不是传统意义上的数组了这个我后面会专门讲。1.2 数组初始化与内存布局静态、动态、堆区三兄弟“数组初始化”这个热搜词看着基础实际里面满是坑。很多初学者面试的时候被问“int arr[10] 和 int* arr new int[10] 有什么区别”当场就懵。我帮你把三种常见写法一次理清静态数组栈区int arr[5] {1, 2, 3, 4, 5};。这是在栈上分配内存大小必须是编译期常量函数执行完自动释放不需要你手动管。动态数组堆区int* arr new int[5];。这是在堆上分配内存运行期决定大小用完必须delete[] arr否则内存泄漏。这里有个经典考点new int[5]返回的是 int* 指针所以很多人误以为“指针就是数组”这是天大的误会。静态存储区数组static int arr[5];或者全局数组。不在栈上也不在堆上而是放在静态存储区默认值会被初始化为 0。还有一个大坑是“数组大小必须是编译期常量”。int n; cin n; int arr[n];这种写法在 C 里其实是非标准的 VL A 变长数组是 C99 的特性C 并不支持你用某些编译器可能侥幸通过了但换台机器就崩。正规做法是int n; cin n; int* arr new int[n]; // 堆区动态数组 // 用完记得 delete[] arr;或者更 C 风格的做法直接用容器vectorint arr(n); // vector 本质上就是动态数组我漏过这个坑一次实习时用int arr[n]写了个功能本机 GCC 跑得好好的上 Linux 服务器一编译直接报错 “expression must have a constant value”。从那时候起我就记住了运行时才知道大小的数组老老实实用 vector 或 new。说到 vector它就是“动态数组”的典型代表。和普通数组比vector 最大的好处是能自动扩容、自动管理内存而且它仍然保证元素在内存中是连续存放的——这意味着你依然可以用 O(1) 随机访问同时享受“不用手动管理内存”的现代 C 体验。扩容的时候vector 会重新找一块更大的连续内存把所有元素拷贝过去这个过程均摊下来是 O(1)但单次的代价其实是 O(n)。这也是为什么高频插入时很多人会直接预分配reserve。1.3 二维数组真的“二维”吗一维数组是线性的二维数组就变成了“表格”。但二维数组底层到底怎么存这是面试高频题。C/C 的二维数组int arr[3][4]物理上其实就是一维排列12 个 int 按行优先row-major连续排在内存里先存第 0 行的 4 个再存第 1 行的 4 个最后存第 2 行的 4 个。所以二维数组arr[i][j]的地址计算公式是首地址 (i × 列数 j) × 元素大小。这也是为什么“多维数组”可以通过指针伪装成一维数组来遍历因为内存上它本来就是连续的。Java 的二维数组int[][] arr new int[3][4]这其实是一个“数组的数组”——arr本身是一个保存了 3 个引用的数组每个引用指向一个长度为 4 的一维 int 数组。所以 Java 的二维数组每一行的内存地址可能是分散的不是一整块连续内存。这就产生了一个十分经典的面试题C 的二维数组能通过int* p arr[0]然后线性遍历 12 个元素Java 为什么不能答案就是 Java 的二维数组不是连续存储的无法用“首地址 偏移量”的方式去线性计算整块位置。这个差异直接影响了你在不同语言里做算法题的策略。比如要用动态规划处理二维 DP 数组Java 里你创建一个int[n][m]其实创建了 n1 个对象GC 压力比 C 的连续内存数组要大但写起来确实方便。面试的时候能把这段差异讲清楚绝对是一个加分项。1.4 指针数组与数组指针C 系语言的世纪难题热词里出现了“指针数组”和“c 多维数组 指针”这其实是同一个知识块里的两个概念很多人混了三年还没搞清楚。指针数组Array of Pointers本质是一个“元素是指针”的数组。写法是int* arr[5]读法是“arr 是一个数组数组里有 5 个 int*”。比如字符串数组const char* names[3] {Alice, Bob, Cindy};这里names就是一个指针数组每个元素指向一个字符串常量。数组指针Pointer to Array本质是一个“指向数组”的指针。写法是int (*arr)[5]读法是“arr 是一个指针指向一个含 5 个 int 的数组”。它的典型应用场景是二维数组的函数传参void printMatrix(int (*matrix)[4], int rows) { for (int i 0; i rows; i) { for (int j 0; j 4; j) { cout matrix[i][j] ; } cout endl; } } int main() { int matrix[3][4] {0}; printMatrix(matrix, 3); // 数组名退化为指向首个元素的指针首元素是 int[4] 数组 return 0; }怎么区分这两种要命的写法我教你一个野路子先找变量名然后看它先跟谁结合。int *arr[5]变量名是 arr它先跟[5]结合因为中括号优先级高于星号所以 arr 先是一个数组然后数组的元素是 int*。int (*arr)[5]因为强制加了括号arr 先跟*结合所以 arr 先是一个指针这个指针指向 int[5] 类型。一句话总结记忆指针数组是“装着指针的数组”数组指针是“指向数组的指针”。做题的时候遇到二维数组传参优先用数组指针遇到要存多个动态分配的数组地址用指针数组。1.5 语言特性对照JS 数组和 C 数组不是一回事热词里有大量 JS 相关的内容比如“js数组排序的几种方法”“数组转字符串”“数组去重”“数组方法”等。我必须强调一句在做算法题的时候不同语言的“数组”根本不是同一个物种。C/C 数组定长、连续、同类型是真正的传统数组。Python 的 list本质上是一个“对象指针数组”存的是元素的引用所以它可以混合类型。list的连续性是“引用连续”不是“元素对象连续”。JavaScript 的 Array那就更是“万物皆可存”了底层实现甚至可能是哈希表。V8 引擎会针对不同情况自动在“快数组连续存储”和“慢数组字典存储”之间切换。所以遇到数组题别被语言特性带偏。比如 JS 的sort()默认是把元素转成字符串再按字典序排序你排序数字[10, 9, 100]会得到[10, 100, 9]不传比较函数直接翻车。我刚开始刷题时用 JS 写排序题查了半天 bug最后发现是sort的默认行为坑了我。另外C 的数组是“值类型”的存储实体Java 的数组是“引用类型”的对象JS 的数组是一个“对象”。这直接决定了你在函数里修改数组时到底传的是值还是引用。C 传数组给函数时数组名会退化为指针函数内修改直接影响原数组Java 传数组其实传的是引用地址也一样影响原数组但 JS 里你如果把整个数组重新赋值给一个新数组原数组是不变的。2. 二分查找十倍速刷题法先攻克最经典的 7042.1 题面与核心思路二分不是“猜”是不断缩小搜索空间Day01 的数组 part01 通常配套的经典题目是 LeetCode 704二分查找和 27移除元素。我们先看 704给定一个升序的整数数组和一个目标值返回目标值的下标不存在则返回 -1。很多新手看到“升序 查找”第一反应是直接遍历嘛O(n) 也不慢。但如果数组有十亿个元素呢遍历十亿次和二十次差距是天上地下。二分的核心逻辑其实就是一个“猜数字游戏”的计算机化版本你在 1 到 100 之间猜一个数每次告诉你猜大了还是小了最优策略永远是猜中间值一次能排除一半的选项。这就是二分查找——每比较一次搜索区间缩小一半所以时间复杂度 O(log n)。但为什么这么“简单”的算法LeetCode 评论区却被称为“思路十秒调 bug 一小时”因为二分查找的难点从来不是“懂不懂折半”而是边界条件——当左右指针撞在一起的时候到底是left right还是left right区间收缩时是mid还是mid 1这些细节错了程序要么死循环要么漏掉目标值。2.2 左闭右闭还是左闭右开边界条件的唯一正解写二分查找之前第一件事是明确区间的定义。所谓区间就是你的搜索范围。两种最主流的写法写法一左闭右闭 [left, right]初始化left 0; right nums.size() - 1;循环条件while (left right)因为 left 和 right 都是有效的下标二者相等时当前元素仍然要检查收缩规则当nums[mid] target时说明目标在左边right mid - 1当nums[mid] target时left mid 1退出循环时left right说明整个区间已经被搜空return -1写法二左闭右开 [left, right)初始化left 0; right nums.size();循环条件while (left right)因为 right 本身不指向有效元素当 left 和 right 相等时区间为空收缩规则当nums[mid] target时right mid因为 mid 已经在右边区间之外但 mid 本身不包含在 [left, mid) 中当nums[mid] target时left mid 1退出循环时left right区间为空return -1两种写法都完全正确区别只是区间的哲学。但你要记住不要混用。如果你初始化是左闭右闭收缩却用right mid那么当mid right时会陷入死循环如果你初始化是左闭右开收缩却用right mid - 1那你可能会错过边界元素。我个人推荐新手用“左闭右闭”因为它的定义最直白所有下标都在数组有效范围内调试的时候打印left和right也不会出界。左闭右开的好处在于和 C STL 的迭代器区间风格一致很多标准库算法都这么写。你只需要选一种练到形成肌肉记忆。2.3 核心细节mid 计算、循环条件、区间更新三件套二分查找有三个核心细节任何一个写错都是灾难细节一mid 的计算。很多人写mid (left right) / 2。这在整数溢出的情况下会出问题——如果 left 和 right 都是很大的 int两者相加可能超过 int 能表示的最大值2^31 - 1。正确写法是mid left (right - left) / 2。这个公式的本质是先算区间长度的一半再加到 left 上避免直接相加溢出。很多老工程师写二分也是这个习惯。细节二循环条件。左闭右闭写while (left right)左闭右开写while (left right)。判断自己写没写错的办法是想想循环退出时区间里还有没有元素。左闭右闭里left right时区间里还有一个元素必须检查所以用左闭右开里left right时区间已经空了所以用。细节三区间更新。核心心法mid 已经被检查过了所以无论如何都要把它排除在新区间之外。左闭右闭时既然 mid 不在新区间那么如果 target 在左边新区间的右边界只能是mid - 1如果 target 在右边左边界只能是mid 1。左闭右开时右边界本来就是开区间所以可以是mid但左边界是闭区间所以还是要mid 1。我把 704 的完整代码写一遍C 左闭右闭版class Solution { public: int search(vectorint nums, int target) { int left 0; int right nums.size() - 1; // 左闭右闭区间 [left, right] while (left right) { // 区间不为空就继续 int mid left (right - left) / 2; // 防溢出写法 if (nums[mid] target) { right mid - 1; // target 在左半边mid 排除 } else if (nums[mid] target) { left mid 1; // target 在右半边mid 排除 } else { return mid; // 找到了 } } return -1; // 区间空没找到 } };有的同学问为什么数组是升序的才能二分因为只有升序才能保证“mid 左边都比它小、右边都比它大”这样你才能通过一次比较排除一半。如果是无序数组二分就失效了得先排序。这也是“二分查找”类题目的前提条件——有序。2.4 常见变体与延伸搜索左边界右边界的问题热词里提到“算法工程师面试”那你就应该知道面试考二分绝不会只考 704 这种原题。常见的变体有三个变体一查找第一个等于 target 的位置左边界。思路是即使nums[mid] target也不急着返回而是把 right 继续往左缩直到区间为空最后 left 就是第一个等于 target 的位置。左闭右开写法// 搜索左边界没找到返回 -1 int leftBound(vectorint nums, int target) { int left 0, right nums.size(); while (left right) { int mid left (right - left) / 2; if (nums[mid] target) { right mid; // 等于 target 也继续往左收缩 } else { left mid 1; } } if (left nums.size() nums[left] target) return left; return -1; }变体二查找最后一个等于 target 的位置右边界。反过来等于 target 时把 left 往右缩最后 right - 1 就是最后位置。变体三查找第一个大于 target 的位置。这是二分法在很多排序场景中的“母题”比如 C 标准库的lower_bound就干这个事。面试中被问“二分查找”相关的题目你要能主动说出二分不是只能查“等于”还能查“第一个/最后一个满足某条件的元素”。这就是“二分答案”思想的起点——不光是数组只要问题的解空间是单调的都可以用二分来逼近。LeetCode 上 35搜索插入位置、34在排序数组中查找元素的第一个和最后一个位置都是这套思路的直接应用。3. 移除元素暴力到双指针的进化之路3.1 题目分析与暴力解法的效率陷阱第二道经典题是 LeetCode 27 移除元素给你一个数组 nums 和一个值 val你需要原地移除所有数值等于 val 的元素返回移除后数组的新长度且不需要考虑数组中超出新长度后面的元素。注意题目的两个关键词原地和不考虑超出新长度后面的元素。这就意味着你不能开一个新数组来装结果必须直接在原数组上“动手”。很多新手第一反应是用库函数比如 C 的std::remove或 JS 的splice但刷题的核心目的是练思想不是调 API所以我建议你先自己实现一遍。暴力解法其实很直白从头遍历遇到等于 val 的元素就把后面的所有元素整体往前挪一位然后数组逻辑长度减 1。每删一个元素最坏情况下要移动 O(n) 个元素外层遍历又是 O(n)所以暴力解法的时间复杂度是 O(n²)。我在没学双指针以前用这种写法写 27 题提交能过但是耗时惨不忍睹因为 LeetCode 的测试用例可能有一个几万长度的数组只要 val 在开头附近后面全是地动山摇的移动。3.2 快慢指针法一个循环干完两件事双指针法里的“快慢指针”思路极其优雅用两个下标一个慢指针 slow 指向“下一个可以放新元素的位置”一个快指针 fast 遍历整个数组。fast 每次往前走如果发现nums[fast] ! val就把nums[fast]的值写到nums[slow]的位置然后 slow 也往前走一步如果nums[fast] valslow 原地不动fast 继续扫描。代码是这样的class Solution { public: int removeElement(vectorint nums, int val) { int slow 0; for (int fast 0; fast nums.size(); fast) { if (nums[fast] ! val) { nums[slow] nums[fast]; } } return slow; // slow 恰好就是新数组的长度 } };你品一下这段代码的精髓快慢指针本质上就是用“一个循环”同时完成“扫描”和“写回”两件事。按常理你要先扫描找出哪些该保留再决定把它们放哪——那至少得两个循环或者一堆临时数组。但慢指针提供了一个“游标”快指针每发现一个不该删的值就直接把它放到慢指针指向的位置慢指针再往前推进。因为慢指针永远领先不过快指针所以永远不会覆盖还没处理的元素。时间复杂度 O(n)空间复杂度 O(1)。跟暴力解法的 O(n²) 相比完全是降维打击。这也解释了为什么“ removeElement ”这类题是双指针的入门代表——你写完这个后面链表的快慢指针、滑动窗口的双指针基本都能慢慢理解了。我想提醒一个细节返回的 slow 既是新数组的长度也是下一轮操作可用的“写入位置”这类“长度即指针”的写法在 C/C 里很常见。很多同学会写成slow 1放最后逻辑没错但不如nums[slow] nums[fast]这种一步到位干净训练营打卡阶段建议尽早养成这种紧凑但清晰的编码习惯。3.3 相向双指针极致优化的另一个视角快慢指针解决了“保留元素相对顺序不变”的需求。但 27 题里并没有说“相对顺序必须保持不变”只要求移除所有等于 val 的元素。这时候还有另一种写法——相向双指针左右指针。思路是把左指针 left 放在数组开头右指针 right 放在数组末尾。从左往右找第一个等于 val 的位置从右往左找第一个不等于 val 的位置然后交换或覆盖。本质上是用右边的“好元素”去填补左边的“坏元素”位置。class Solution { public: int removeElement(vectorint nums, int val) { int left 0, right nums.size(); while (left right) { if (nums[left] val) { nums[left] nums[right - 1]; // 用右边的元素覆盖 right--; } else { left; } } return left; } };这种写法的最坏情况时间复杂度也是 O(n)但优势是交换次数更少——右边那些不等于 val 的元素直接搬到左边“补位”等于 val 的元素则被甩到数组末尾不管了不需要像快慢指针那样把每个非 val 元素都搬一次。不过它的代价是改变了元素的相对顺序。实际面试中如果题目没有对顺序有要求用相向双指针其实更高效如果题目要求保持相对顺序比如“stable remove”就必须用快慢指针。这个判断能力本身就是面试官想考察的点。3.4 为什么不能依赖库函数以 JS 的 splice/filter 为例很多 JS 选手写移除元素会想到splice或filter// 错误示范在循环中 splice 删除元素 for (let i 0; i nums.length; i) { if (nums[i] val) { nums.splice(i, 1); i--; // 删完后下标要回退否则会跳过一个元素 } }这里至少有两个坑。第一splice本身是 O(n) 操作每次删除都会把后面的元素集体搬移最坏情况还是 O(n²)跟你手写暴力解法没有本质区别。第二循环里删除后如果不手动i--会跳过被删除位置后移过来的那个元素造成漏删。我当年就是这么翻车的查了半天才想明白。filter倒是简洁nums nums.filter(x x ! val);但题目要求“原地”filter返回的是一个全新的数组虽然代码短但本质上不符合题意空间复杂度是 O(n)。所以刷算法题时请记住API 能用但你要能讲清楚它内部干了什么、复杂度是多少。训练营打卡的意义就是逼你从底层实现一遍把内功练好。等面试考“你会不会数组去重”的时候你如果能手写双指针版本再补一句“如果用 JS 内置的 Set/Array.from 也能去重但空间 O(n)双指针排序后可以做到 O(1) 空间”那就是降维打击。4. 刷题路上的坑与排查数组相关的常见问题速查4.1 越界、死循环、忘更新指针三个高频 Bug 现场数组题最常见的 bug 就三类越界、死循环、指针没更新。我把真实的踩坑现场摆出来越界现场。C/C 的数组越界不一定会直接崩溃因为系统不检查边界你读的是“那块地址上的内存”但如果恰好碰上内存保护页程序就段错误。更可怕的是“写越界”——它可能不会立刻报错而是悄悄破坏相邻地址的数据调试的时候你根本找不到源头。所以自己写代码时一定要提前想清楚边界for (int i 0; i nums.size(); i)里如果循环体里出现了nums[i 1]就要小心 i 到size() - 1时会不会越界。二分查找里mid left (right - left) / 2也要保证 left 和 right 的区间始终有效。大部分二分死循环的根因就是 mid 没排除干净导致区间始终不变。死循环现场。我写二分时最常见的问题是while (left right)里更新写成right mid而不是right mid - 1。假设left 0, right 1, mid 0如果nums[mid] target理论上新区间应该是 [0, -1] 空区间但你写成right mid后新区间还是 [0, 0] 非空下一次还是一样的状态无限循环。解决口诀就一句检查过的 mid 必须排除左闭右闭用 mid ± 1左闭右开右边界才能用 mid。忘更新指针现场。移除元素的双指针法里很多新手写了nums[slow] nums[fast]却忘了slow结果数组是被覆盖了但 slow 一直停在 0返回的长度永远是 1。调试时打印 slow 和 fast 的值立刻就能看出来关键是写代码时心里要有“游标”的概念slow 和 fast 不是摆设每次动作都要动。4.2 数组去重、转字符串、排序面试中的高频小操作热词里有一堆数组小操作——去重、排序、转字符串、切片这些在面试里经常被当作“前菜”随手考但最容易被基本功不扎实的人卡住。我把主流语言的做法和复杂度整理一遍数组去重JS[...new Set(arr)]O(n) 时间 O(n) 空间代价是 Set 本身有额外开销面试时如果被要求“原地去重且有序”实际上是对已排序数组用双指针——一个指针扫描一个指针指向“下一个不重复元素的位置”。Pythonlist(set(arr))会丢失顺序想要保持顺序可以用dict.fromkeys(arr)或者列表推导 set 判断。Csort(arr.begin(), arr.end()); arr.erase(unique(arr.begin(), arr.end()), arr.end());这里面 unique 只负责把不重复的挪到前面并返回迭代器真正“删尾巴”的是 erase。这个组合是 C 面试高频套路。数组转字符串JSarr.join(,)注意arr.toString()和join(,)在嵌套数组时表现不同toString会把所有嵌套层都拍平join只处理当前层。这细节在笔试时坑过我好几次。Python,.join(map(str, arr))注意 join 要求所有元素都是字符串否则要先 map。CC 标准库没有直接的 “join”需要自己写循环拼接或者用 accumulate 加函数对象。数组排序重点讲 JSarr.sort()的默认规则是把元素转字符串再比字典序所以数字排序必须写arr.sort((a, b) a - b)。这个坑我已经说过但每次看到还是有人踩因为它太反直觉了。另外要注意 sort 是原地排序会改变原数组如果不想影响原数组先[...arr].sort(...)。数组切片Pythonarr[1:4]是左闭右开范围是下标 1 到 3。这个语法太顺滑以至于我转写 JS 时老是把arr.slice(1, 4)也当成左闭右开——好消息是 JS 的 slice 一样是左闭右开。JSarr.slice(1, 4)不修改原数组返回新数组arr.splice(1, 3)是删除并修改原数组别把这两个搞混。面试手写题前一定先确认题目让不让你修改原数组。4.3 一页纸速查表数组核心操作与复杂度对照我把 Day01 涉及的数组操作整理成了一张速查表刷题时可以直接对照操作静态数组 / C 数组C vectorJava ArrayListJS Array时间复杂度随机访问 arr[i]支持支持支持支持O(1)末尾追加 push_back / add / push不支持定长支持支持支持O(1) 均摊中间插入 insert不支持支持O(n)支持O(n)spliceO(n)O(n)中间删除 erase不支持支持O(n)支持O(n)spliceO(n)O(n)查找指定值手写循环findO(n)indexOf / containsO(n)indexOf / includesO(n)O(n)排序手写快排sortO(n log n)Collections.sortO(n log n)sort 比较函数O(n log n)O(n log n)从上表可以看出一条主线凡是要在数组中间动元素的操作代价都是 O(n)因为连续存储决定了你需要挪动后续所有元素。理解了这一条你就能解释为什么很多算法题要追求“原地”“双指针”“一次遍历”这些优化——因为大多数时候我们其实不需要真的“删”元素只需要用指针把逻辑上的新数组划分出来从而把 O(n²) 变成 O(n)。我还想补充一句关于“循环队列”的热词有同学搜“假设以数组 q[m] 存放循环队列中的元素”这是数据结构考试里的经典应用题。它本质上是把一维数组“掰弯”成环形通过(rear 1) % m这样的模运算实现队尾和队头的循环追赶。循环队列存在的意义就是避免假溢出——线性队列出队后前面的空间没法复用而循环队列能把“逻辑上已删除”的位置重新用于入队。这块基础对后面学栈、队列、滑动窗口非常有帮助如果你连数组都不熟看到% m取模操作会一头雾水。5. 学习打卡节奏与训练营复盘心得Day01 的内容看起来不多就两题加一堆理论但训练营真正的意义在于把散装知识点串成体系。我建议第一天不要贪多老老实实把这两道题的两种写法二分左右边界、快慢和相向双指针各写三遍以上写到不卡壳为止。算法能力不是看会的是写会的。我个人体会最深的一点是“看懂了”和“能写对”中间隔着一条巨大的鸿沟。看题解五分钟就懂了合上书自己写边界条件、指针更新全崩。这不是因为你笨是因为算法题的输出不仅是“思路”还是“对细节的肌肉记忆”。所以哪怕你今天只是把数组理论复习了一遍、把两题各 AC 了一遍已经比 90% 只收藏不看的人强很多了。还有一个每天都能用上的小技巧写完题之后顺手在代码注释里写一句“这道题的坑在哪”比如“二分右边界注意 mid-1 还是 mid”。隔一周再回头看这句话比任何笔记都管用。我翻自己两周前的注释常常会心一笑——当时卡了一小时的点现在一眼就能看穿。这就是成长。如果你今天也在跟着训练营打卡建议给自己定个规矩每天学完后用一句话说出今天最核心的心得比如“数组是连续内存所以中间操作 O(n)但随机访问 O(1)”。别小看这一句话坚持三十天你积累下来的就是一套自己的算法知识图谱。Day01 的数组 part01 就到这里我准备去做 Day02 了希望这份拆解对你有实打实的帮助。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →