☰
数组算法训练:从底层内存到双指针与滑动窗口
2026/10/3 9:03:01 网站建设 项目流程

1. 为什么数组是算法训练的第一课:它的地位和底层逻辑

很多刚进训练营的同学问我:第一天就讲数组,是不是太简单了?说实话,我当年带第一届训练营的时候也这么想过,直到后来发现一个残酷的事实——数组题做不好的人,后面学链表、树、动态规划全都磕磕绊绊。原因很简单:数组是所有数据结构里最接近硬件存储模型的一种,你如果连数组的存取逻辑都没吃透,后面理解LRU Cache这种基于哈希表+链表的东西就是空中楼阁。

先摆一个结论:数组在算法里不是"入门玩具",而是"地基"。你可以不懂红黑树、不懂AVL,但你不可能绕开数组去写任何一段像样的业务代码。职场里最常见的算法场景,比如分页、去重、排序、聚合统计、区间汇总,底层全是数组操作。你去看各大厂面试题,哪怕挂在链表、树、图的名下,最终落地还是要靠数组来存储和索引节点。所以第一天把数组掰开揉碎,收益是长期复利式的。

数组为什么重要,根子在于它和内存的关系。现代计算机的内存是一段连续编址的线性空间,从地址0x00到0xFF...,每个字节都有自己的门牌号。数组就是直接在这段连续空间里划一块区域,按顺序摆数据。你声明int arr[5],编译器会帮你申请5 * sizeof(int)字节的连续空间,arr[0]占据最低地址,arr[4]占据最高地址。这听起来平平无奇,但它是数组一切性能优势的源头。

这种连续内存模型带来两个核心性质,很多同学到毕业都不见得真正理解:

性质一:随机访问的时间复杂度是 O(1)。因为底层知道首地址base,要访问下标i只需要做一次加法:base + i * sizeof(元素类型)。CPU 直接就能算出目标地址,不需要遍历任何东西。这就是为什么数组查询极快,也是为什么面试官总爱问"数组和链表的区别",核心差异就在这。

性质二:插入和删除必须搬移数据。因为数组要求连续,你在中间挖掉一个坑,后面的元素必须整体前移把坑填上;在中间插一个元素,后面的必须整体后移腾位置。这个"搬移"操作的成本是 O(n)。很多新手不理解为什么数组插入慢,其实就是在内存里做了一次批量 memmove。

关于第一天的训练,我建议你先在自己电脑上写一段探针代码,打印出数组元素的内存地址,感受一下"连续"到底长什么样。下面是我常用的一段 C 语言示例:

#include <stdio.h> int main() { int arr[5] = {10, 20, 30, 40, 50}; for (int i = 0; i < 5; i++) { printf("arr[%d] = %d, address = %p\n", i, arr[i], &arr[i]); } return 0; }

你运行后会看到地址是递增的,相邻元素之间相差4个字节(因为 int 占 4 字节)。我见过太多人只是"知道"数组是连续的,但从没亲眼看一眼那个递增的地址,这种直觉上的缺失,会在你做二分查找边界、二维数组索引换算时反复坑你。

2. 从数组的增删改查:随机访问的甜头与搬移的代价

2.1 增删改查每一招的时间复杂度

数组的基本操作就四类:增、删、改、查。每一类的复杂度都不一样,先看表格:

操作时间复杂度前提条件说明
随机访问arr[i]O(1)知道下标直接地址计算
按值查找O(n)无序数组必须逐个比较
按值查找O(log n)有序数组配合二分查找
尾部插入/删除O(1)容量够/不用保持顺序只在末尾操作
中间插入/删除O(n)无后续元素搬移
修改arr[i]O(1)知道下标直接写内存

这里有个常见的认知误区:提到数组查找,很多回答说"数组查找是 O(1)"。严格说,随机访问是 O(1),按值查找是 O(n)。只有在有序前提下,你才能通过二分把查找降到 O(log n),但二分本身又是一个高频考点,我会在后面章节单讲它的边界细节。

2.2 插入和删除:为什么必须搬移,以及什么时候可以不搬

数组在中间插入的机制,我用一句话概括:腾位置。假设数组是[1, 2, 3, 4, 5],要在下标 2 的位置插入99,步骤是:

  1. 检查容量是否够(不够就得扩容,后面讲动态数组时会展开);
  2. 从末尾开始,把5移到下标 4,4移到下标 3,3移到下标 2... 注意必须从后往前搬,如果你从头往后搬,数据会被覆盖掉;
  3. 下标 2 空出来后,写入99。

同理,删除下标 2 的元素时,要从3开始从前往后搬:3移到下标 2,4移到下标 3,5移到下标 4,末尾置零或不管。

这两句"从后往前""从前往后"看起来是细节,但我在实际评审代码时,见过太多人写错方向导致数据错乱。还有一个优化点很多老手才知道:如果题目不要求保持元素相对顺序,删除中间元素时可以不搬移全量。具体做法是把最后一个元素复制到要删除的位置,然后把size减一。这样删除从 O(n) 降到 O(1)。这个技巧在"数组去重""移除元素"这类 LeetCode 题里经常能用上,就是经典的"快慢指针+覆盖"思路。

2.3 初始化与越界:两个让你的程序"莫名其妙"的凶手

热搜词里有人搜"数组初始化""c++字符串数组初始化",说明初始化是新手高频困惑点。这里分语言讲一下:

C 语言没初始化的局部数组,里面是乱值(栈上残留数据);全局数组默认全 0。所以 C 程序员写int arr[5];后直接读,结果是不可预测的。

C++如果写vector<int> v(5),默认会初始化为 0;但原生数组int arr[5]仍然和 C 一样是乱值。

Java的int[] arr = new int[5]默认全 0,但Integer[]这种包装类数组默认是null。

Python的list本身就是动态数组,写[0] * 5得到 5 个 0。

这些初始化细节看似琐碎,实际写算法题时,如果你不清空状态数组,上一次运行的数据会污染下一次结果。我在训练营见过一个同学调试回溯算法,反复出现诡异结果,最后发现是全局状态数组没在递归入口重置。

越界是另一个经典杀手。C/C++ 的数组越界是未定义行为,不会直接报错,可能覆盖了其他变量,导致"明明没改这个变量,它却变成了奇怪的值"。有一次我帮学生排查 bug,发现他for循环里写i <= n,把数组最后一个元素之后的垃圾值读出来当成合法数据,程序行为完全随机。后来的习惯是:涉及数组下标的地方,永远用size变量而不是写死常量,循环条件里能写成< size就不要写成<= size。

3. 双指针与滑动窗口:数组题最常用的两类破局思路

第一天训练如果只做"读、写、遍历",那确实太简单。真正拉开差距的是基于数组的双指针和滑动窗口。这两类技巧覆盖了 LeetCode 上大量数组题的解法,训练营第一天我就会带学员把它们过一遍。

3.1 双指针:一快一慢,一左一右

双指针的核心思想,是通过维护两个下标来减少不必要的遍历次数。最经典的三个形态:

左右指针:左指针left从最左边出发,右指针right从最右边出发,相向而行。典型应用是"两数之和(有序数组)":如果arr[left] + arr[right] > target,说明右边太大,右指针左移;如果< target,说明左边太小,左指针右移。整个过程在一次遍历内完成,复杂度从暴力 O(n²) 降到 O(n),而且不需要额外空间。

快慢指针:一个指针走得快,一个走得慢。典型应用是"移除指定元素"或"数组去重"。核心逻辑是维护一个slow指针表示"有效区的边界",fast指针负责探路。当fast遇到一个合法值,就赋值给slow位置并把slow前进一位;遇到不合法的值,slow原地不动,fast继续向前。这样一轮下来,前半段全部是合法数据,后半段是废弃数据,整体复杂度 O(n)。

同向并行指针:两个指针都从头开始,但移动节奏不同步,常用于合并两个有序数组。这题面试出现频率极高,核心坑点是从后往前填,避免覆盖未处理的元素。我面试候选人的时候就常拿这道题考察"边界意识",能一次写对的人不到三成。

为了让你直观理解双指针的价值,我写一段快慢指针去重的 C++ 示例:

#include <vector> #include <iostream> int removeDuplicates(std::vector<int>& nums) { if (nums.empty()) return 0; int slow = 0; for (int fast = 1; fast < nums.size(); fast++) { if (nums[fast] != nums[slow]) { slow++; nums[slow] = nums[fast]; } } return slow + 1; // 有效长度 } int main() { std::vector<int> nums = {0, 0, 1, 1, 1, 2, 2, 3, 3, 4}; int len = removeDuplicates(nums); std::cout << "len = " << len << std::endl; for (int i = 0; i < len; i++) { std::cout << nums[i] << " "; } std::cout << std::endl; return 0; }

输出结果是len = 5,数组前五个元素变成0 1 2 3 4。你注意看slow指向的位置:它始终是下一个合法写入位,而fast负责向前打探,这就是"快慢指针"的精髓。

3.2 滑动窗口:把"连续子数组"问题变成"移动区间"问题

滑动窗口处理的典型问题是:在一个数组里找满足某个条件的连续子数组/子串,比如"和大于等于 target 的最短子数组""无重复字符的最长子串"。暴力的做法是枚举所有起点和终点,复杂度 O(n²);滑动窗口把复杂度降到 O(n),原理是让窗口右边界不断扩张,左边界按需收缩,全程每个元素最多进出窗口一次。

我以一个很常见的题目"长度最小的子数组"为例来说明算法逻辑:

给定一个包含n个正整数的数组和一个正整数target,找出该数组中满足其和>= target的长度最小的连续子数组,并返回其长度。如果不存在,返回 0。

解题步骤:

  1. 定义left = 0,表示窗口左边界;sum = 0表示当前窗口内元素之和;result = INT_MAX用于记录最短长度。
  2. 用right从 0 遍历数组,每轮把nums[right]累加到sum,这是"窗口右边界扩张"。
  3. 当sum >= target时,进入内层while循环:先记录当前窗口长度right - left + 1,更新result;然后把nums[left]从sum中减去,left++,窗口左边界收缩。
  4. 内层循环重复直到sum < target,右边界继续前进。
  5. 遍历结束,如果result仍是INT_MAX,说明不存在满足条件的子数组,返回 0;否则返回result。

这里有一个新手特别容易踩的坑:内层while条件为什么要用>=而不是==?因为当sum已经超过target时,不断收缩左边界可以继续寻找更短的满足条件子数组。如果你只在sum == target时记录,就会漏掉"大于 target 也能满足条件"的情况。

3.3 数组里的"子集和"难题:暴力枚举的边界在哪

热搜词里有一句很典型的话:"已知固定数值,如何确定数组中的哪些数据和等于固定值"。这其实是子集和问题(Subset Sum),它的完整版是 NP 完全的,但给定限制条件时可以优化。

先说最简单的暴力枚举。如果数组长度n不大(比如n <= 20),可以用二进制枚举:把每个元素看成"选/不选"两种状态,用0 ~ (1 << n) - 1的每个整数对应一种组合。i的第k位是 1 表示选中第k个元素。伪代码如下:

for (int mask = 0; mask < (1 << n); mask++) { int sum = 0; for (int k = 0; k < n; k++) { if (mask & (1 << k)) sum += arr[k]; } if (sum == target) { // 找到一组解 } }

复杂度是 O(n * 2^n),n=20时是两千多万次,勉强可接受;n=30以上就爆了。如果你真的遇到n=40这类中等规模,可以用折半枚举(meet in the middle):把数组分成两半,分别枚举两半所有子集和,然后排序+二分匹配。这样复杂度从 O(2^n) 降到 O(2^(n/2) * n),n=40也能跑。这个技巧属于进阶内容,但我觉得值得在第一天就提一嘴,因为太多人一看到题就上暴力循环,根本不知道有更优解。

4. 二维数组与矩阵问题:下标换算、遍历方向与环形队列

4.1 二维数组到底怎么存储的:行优先和列优先

二维数组在逻辑上是"表格",但在内存里依然是线性的。C/C++ 采用行优先存储,也就是先把第一行铺满,再铺第二行。arr[i][j]的地址换算公式是:

address = base + (i * 列数 + j) * sizeof(元素类型)

这个公式你必须烂熟于心。面试题里经常出现"把二维数组按行优先展开成一维,让你反过来做索引映射",这就是i * cols + j的直接应用。

Python 里没有真正的原生二维数组,一般用列表的列表[[0] * cols for _ in range(rows)]模拟。这里有个经典陷阱:[[0] * cols] * rows看似生成了二维数组,实际上每一行都是同一个列表对象的引用,改一个元素会波及其他行。训练营里几乎每年都有人踩这个坑,你如果写 Python,务必用第一种写法。

4.2 遍历边界:螺旋矩阵、对角线遍历的规律总结

二维数组的遍历题很大程度上考验的是"方向控制"和"边界判断"。

螺旋矩阵是最典型的一道题:按顺时针方向从外到内遍历所有元素。核心思路是维护四个边界top, bottom, left, right,每走完一条边就收缩对应边界,直到上下边界交错或左右边界交错。

int top = 0, bottom = rows - 1, left = 0, right = cols - 1; while (top <= bottom && left <= right) { // 从左到右遍历 top 行 for (int j = left; j <= right; j++) process(matrix[top][j]); top++; // 从上到下遍历 right 列 for (int i = top; i <= bottom; i++) process(matrix[i][right]); right--; // 从右到左遍历 bottom 行(注意防止重复) if (top <= bottom) { for (int j = right; j >= left; j--) process(matrix[bottom][j]); bottom--; } // 从下到上遍历 left 列 if (left <= right) { for (int i = bottom; i >= top; i--) process(matrix[i][left]); left++; } }

代码里那两个if判断特别关键。遍历完第一条边后,top和right已经改变了,如果此时top > bottom或left > right,说明矩阵已经全部遍历完,再走第三条边就是重复访问。很多初学者死记这套代码,却不理解这两个if的防御意义,一旦矩阵是 1 行或 1 列的边界情况立刻出 bug。

对角线遍历(之字形遍历)是另一个高频题,它考察的是方向切换规律:当下标越界时,需要同时调整行和列的方向。我不建议死记公式,更推荐的做法是分四种情况处理:向右上方走、向左下方走、撞到右边界、撞到下边界。把每种情况的坐标变化写清楚,代码再长也不容易错。

4.3 环形队列:数组模拟循环结构的经典思路

热搜词里有句描述我一看就知道出自哪道经典题:"假设以数组 q[m] 存放循环队列中的元素,同时以 rear 和 length 分别指示环形队列中的队尾和长度"。这是数据结构教科书里的循环队列实现,算法训练营第一天讲数组时经常会捎带提它,因为它充分体现了"数组下标取模"的思想。

循环队列存在的意义是复用数组空间。普通队列用数组实现时,出队后头指针前进,前面的空间就浪费了。循环队列把数组首尾相连,当rear走到m-1后再入队,rear = (rear + 1) % m就回到了 0。

用rear和length两个变量实现时,判空条件是length == 0,判满是length == m。队头位置怎么求?答案是(rear - length + m) % m。这个取模公式非常容易写错,如果你直接写(rear - length) % m,当rear - length是负数时,大多数编程语言的取模结果也是负数,下标就非法了。所以加上一个 m 再取模是标准写法。我第一次写循环队列时就在这卡了半小时,后来形成肌肉记忆:凡涉及下标回绕,一律写成(index + m) % m的格式。

5. 字符串本质是字符数组:数组方法和双指针的直接应用

5.1 字符数组与字符串的区别:终止符的边界

热搜词里有人搜"c++字符串数组初始化""指针数组存放字符串",说明字符数组和字符串的关系是个高频困惑点。

在 C 语言里,字符串就是一个以'\0'结尾的字符数组。char str[] = "hello"实际占 6 个字节,最后一个字节是'\0'。很多缓冲区溢出漏洞的根源就是忘记给'\0'留位置,比如char buf[5]; strcpy(buf, "hello");直接越界。

在 C++ 里,std::string封装了动态字符数组,c_str()方法返回的指针依然是以'\0'结尾的字符数组。在 Java 里,String是不可变对象,底层是char[],但你不能原地修改;想修改就得转成char[]或StringBuilder。Python 的str也是不可变的,转列表list(s)后元素是每个字符,修改后再''.join(...)拼回来。

5.2 数组转字符串:不同语言的姿势与踩坑

数组转字符串在热搜里也是一条,可能因为各语言 API 太多容易记混。

Python 里对字符串列表用','.join(list),对数字列表得先map(str, list)再 join。C++ 里可以用std::to_string逐个拼接,或者用stringstream。Java 里Arrays.toString(arr)返回带方括号的形式,如果你只要逗号分隔,可以借助String.join(",", arr)(Java 8+)。JavaScript 里直接用arr.join(",")最简单。

有个非常隐蔽的坑:JavaScript 的arr.join()如果不传参数,默认用逗号分隔,但如果数组元素中本身含逗号,结果会变得难以解析。所以做数据拼接时,永远明确指定分隔符。

5.3 字符处理实战:过滤、分割、去重一锅端

我拿一个热搜里提到的需求来演示:"数组分割并显示包含某一字符"。这个需求在日志分析、关键词筛选里很常见。假设你有一个字符串数组,想过滤出包含子串"abc"的所有元素,并按逗号拼接展示,Python 代码是这样:

data = ["hello-abc", "world", "xabcx", "plain"] filtered = [s for s in data if "abc" in s] print(",".join(filtered)) # 输出 hello-abc,xabcx

一行列表推导式就解决了。这在算法题里对应的是"字符串匹配过滤",如果是少量数据直接暴力in判断即可;如果数据量极大,就需要引入 KMP、AC 自动机这类模式匹配算法。热搜词里也出现"KMP 算法",这里先不展开,但你至少要有"什么规模用什么方案"的判断力。

5.4 反转字符串、判断回文:双指针在字符串数组上的练习

反转一个字符串,最简单的方式是转成字符数组后用左右指针交换:

void reverseString(char* s, int sSize) { int left = 0, right = sSize - 1; while (left < right) { char tmp = s[left]; s[left] = s[right]; s[right] = tmp; left++; right--; } }

判断回文串的思路类似:左右指针逐字符比较,一旦不等就返回 false。进一步升级版是"最多删除一个字符能否构成回文",这题需要你在遇到不等时,分别尝试删左边或删右边,属于双指针的进阶用法。第一天训练如果把这两题做熟,你对"指针移动 + 边界终止"的感知会强很多。

6. 从静态数组到动态数组:扩容机制、均摊分析与选择策略

6.1 为什么需要动态数组:固定长度的尴尬

C 语言原生的int arr[100]是静态的,长度一旦定下就不能改。实际业务里,数据量往往不是你预先能决定的:可能刚开始只有 10 条数据,后来涨到 10 万条。你开小了不够用,开大了浪费内存。于是就有了动态数组——最典型的就是 C++ 的std::vector、Java 的ArrayList、Python 的list。它们内部本质还是数组,但支持在容量不足时自动扩容。

6.2 扩容的均摊复杂度:为什么 O(1) 和 O(n) 可以共存

动态数组的容量是分阶梯增长的。以std::vector为例,很多实现的扩容因子是 2:当前容量为cap,元素个数达到cap时,申请2 * cap的新空间,把旧数据全部拷贝过去,释放旧空间,容量翻倍。

单看某一次扩容,成本是 O(n)(拷贝 n 个元素)。但如果你把整个过程摊开看:每次扩容后容量翻倍,意味着下一次扩容要等再插入 n 个元素。把每次扩容的拷贝成本"均摊"到每个插入操作上,每个插入操作的代价其实是 O(1)。这就是**均摊分析(Amortized Analysis)**的基础思想。面试官问"vector 的 push_back 时间复杂度是多少",正确答案就是"均摊 O(1)",单次可能 O(n)。

这个思想特别重要,训练营里很多同学觉得"均摊"这个词玄乎,我举一个生活例子:你每个月交房租是固定支出,但每两年要大修一次水管花一笔巨款。把大修费用平均到每个月里,你就知道自己长期现金流大概是稳定的。扩容就是这个"大修",平摊到每次插入后,插入的平均成本仍然可接受。

6.3 数组 vs 链表的选型:一个被忽略的判断标准

训练营第一天我会让学员做一个对比表格,把数组和链表从内存、访问、插入删除、缓存友好性几个维度列清楚:

维度数组(含动态数组)链表
内存布局连续分散(节点间用指针连接)
随机访问O(1)O(n)
插入/删除(已知位置)O(n)(需搬移)O(1)(改指针)
CPU 缓存友好性好(局部性原理)差(跳来跳去)
额外内存少(只需数据本身)多(每个节点要存指针)

实际工程中,很多时候链表并不比数组快。因为 CPU 对连续内存的缓存命中率很高,数组遍历极快;链表节点在内存里到处乱放,每次访问都可能缓存未命中,性能反而更差。"插入删除链表更快"只在已定位节点的前提下成立,而定位节点本身往往需要 O(n) 的遍历,这笔账要算总账。

所以我的建议是:默认选数组,除非你有明确的理由需要频繁在中间插入删除且经常要扩容。这个建议可能和一些教科书给读者的印象相悖,但我在实际项目里压测过很多次,结论稳定。

7. 数组题最容易翻车的地方:我的调试经验和训练要点清单

7.1 越界、空数组、单元素、溢出:四类边界情况逐一排查

训练营最后一天复盘时,我最常说的一句话是:"你写的代码能过测试用例,不代表能过隐藏用例。"数组题最容易翻车的边界情况就那么几类,每次写完代码,我建议你按以下清单自测:

  • 空数组:len == 0时,你的循环会不会直接崩?很多解法默认至少有一个元素,没考虑空数组。
  • 单元素数组:len == 1时,左右指针初始值会不会越界?比如left = 0, right = len - 1 = 0,循环条件left < right不会进入,这没问题,但如果你在循环外直接访问arr[right + 1]就炸了。
  • 双指针相遇:二分查找和快慢指针都涉及left == right的情况,你到底处理没有?
  • 整数溢出:left + right可能溢出 int 范围,经典二分查找的mid = left + (right - left) / 2就是为了避免left + right溢出。别小看这一行,面试时写(left + right) / 2会被面试官追问风险点。
  • 下标减一:循环里用到i - 1时,最常见的是i == 0时访问arr[-1]。Java/C++ 里这是数组越界,Python 里更坑——arr[-1]不报错,返回的是最后一个元素,逻辑完全错了但程序不崩,排查起来非常痛苦。

7.2 索引公式的推导习惯:从暴力和推论两条路验证

数组题里凡是出现i * cols + j、(rear - length + m) % m这类索引公式,我都建议你推导 + 实例验证双保险。推导是指从定义出发,自己推一遍地址换算;实例验证是拿一个具体的二维数组(比如 3 行 4 列),手写几个坐标代入公式,看看结果是否符合预期。

举个真实案例:有一次我帮一个学员看题,他写了一个二维数组的斜线遍历,行列坐标换算总是差 1。我让他把matrix[3][4]的每个元素按他的公式手算出来,再与实际内存展开对比,几分钟就找到错在哪一步——原来的公式把行优先当成了列优先。这比自己对着屏幕干瞪眼有效得多。

7.3 今日训练清单:从热身到强化的一周安排

如果你今天刚开始练数组,我按难度从低到高给一份清单,你可以按自己的节奏分配到一周内完成:

  1. 热身:实现数组的初始化、遍历、按值查找,确认你能手写快慢指针去重的完整代码。
  2. 基础巩固:移除元素、移动零、合并两个有序数组。这三道题都是快慢指针或双指针的变体。
  3. 进阶:长度最小的子数组(滑动窗口入门)、螺旋矩阵(二维数组边界处理)、反转字符串中的单词(字符串+双指针综合)。
  4. 自我挑战:和为 target 的所有子集(先做n <= 20版本,再尝试折半枚举思路)。

每个题目做完,建议做两件事:一是把题解思路用自然语言写一遍,不看代码;二是把时间复杂度和空间复杂度写在注释里。这两个习惯能帮你把"会做"变成"真懂"。

7.4 语言差异提醒:Python 的负索引、C++ 的迭代器失效、Java 的自动装箱

最后再集中说一个实操层面的事:不同语言处理数组时的细节差异,直接决定你的 bug 率。

Python 的负索引是双刃剑。方便归方便,但如果你在调试时不小心把i-1写成了-1,Python 不会告诉你越界,而是悄悄返回最后一个元素,这对算法验证是灾难。

C++ 的vector有一个经典坑叫迭代器失效。当你对vector进行插入或删除操作后,之前保存的迭代器可能全部失效,继续使用就是未定义行为。比如在循环里边遍历边删除,很容易踩中。

Java 的ArrayList里存的是Integer对象,不是原始int。当你在循环里大量读写时,自动装箱(int→Integer)和拆箱(Integer→int)会产生额外对象,这在算法竞赛或性能敏感场景下会拖慢速度。如果你要做纯数值计算,int[]原生数组往往比ArrayList<Integer>快很多。

我个人在实际使用中的体会是:数组题的核心不在语法,而在你脑海中是否有一个清晰的"内存动画"。每写一行代码,都问自己三个问题——数据存在哪、下标指向哪、边界条件是什么。如果这三个问题都能即时回答,你离数组题的举一反三就不远了。今天训练营第一天的内容,能把这些基础动作变成肌肉记忆,后续再学排序、二分、哈希表、动态规划都会轻松不少。第三天的二分查找专题,我准备拿有序数组的边界处理做一次深度拆解,到时候见。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询