📌 开篇:为什么是哈希和双指针?
大家好,这是算法刷题专栏的第 1 篇。
很多同学开始刷题的时候,看到「哈希」「双指针」「滑动窗口」这些词就头疼。其实它们没你想的那么可怕,今天咱们就用最土的话,把这两个套路讲明白。
先说哈希,它就像一个超级快的「字典」,能在一堆数里瞬间找到你要的那个。
再说双指针,它就像两个人在一条路上走,可以同向走、可以相向走,走到一起问题就解决了。
好,废话不多说,咱们直接上题!
第一题:两数之和(简单)
题目
给定一个整数数组 nums 和一个目标值 target,请你在数组中找出和为目标值的两个整数,返回它们的下标。假设每种输入只会对应一个答案,且同样的元素不能被重复使用。
思路过程
第一反应:暴力枚举
拿到这个题,你可能会想:这还不简单?我把数组从头到尾遍历两遍,找到和为 target 的两个数不就完了?
// 暴力解法:两层循环
for (int i = 0; i < nums.length; i++) {
for (int j = i + 1; j < nums.length; j++) {
if (nums[i] + nums[j] == target) {
return new int[]{i, j};
}
}
}
这样确实能做出来,但是!时间复杂度是 O(n²)。如果数组有 10000 个元素,就要执行 5000 万次比较,太慢了。
换个思路
想想看,我们找的是 target - nums[i],也就是「当前数」还差多少才能凑成 target。
如果我一边遍历,一边把这个「差值」记下来,下次遇到的时候不就能直接配对了?
举个例子:
- 数组 [2, 7, 11, 15],target = 9
- 遍历到 2 时,差值是 7,把 7 存起来
- 遍历到 7 时,发现 7 已经在字典里了,说明之前存过 2,配对成功!
这就是「哈希」的思想:用空间换时间,把查找从 O(n) 变成 O(1)。
最终解法
java
public int[] twoSum(int[] nums, int target) {
// 创建一个哈希表,key 是数组的值,value 是对应的下标
// 为什么用值做 key?因为我们要快速查找「差值」存不存在
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
// 计算当前数还差多少
int complement = target - nums[i];
// 去哈希表里找这个差值
if (map.containsKey(complement)) {
// 找到了!返回差值的下标和当前下标
return new int[]{map.get(complement), i};
}
// 没找到,就把当前数存进哈希表
// 存的是「值 -> 下标」的映射
map.put(nums[i], i);
}
// 题目说一定有解,所以不会走到这里
throw new IllegalArgumentException("No solution");
}
复杂度分析
- 时间复杂度:O(n),遍历一次就够
- 空间复杂度:O(n),最坏情况要把所有数都存进哈希表
一句话总结
两数之和的核心就是「边走边存」,把已遍历的数存进哈希表,后面遇到能配对的直接查表。
第二题:字母异位词分组(中等)
题目
给你一个字符串数组,请你将 字母相同的词放在一起,可以按任意顺序返回结果。字母异位词:组成字符相同,但排列不同的字符串。
思路过程
什么是字母异位词?
简单说就是「打散了重新排」还是一样的。比如:
- "eat" 和 "tea" 和 "ate" 是一组
- "tan" 和 "nat" 是一组
第一反应:两两比较
最笨的办法,两两比较字符串,判断它们是不是异位词。判断方法也简单:把两个字符串排序,如果相等就是异位词。
// 判断两个字符串是否是异位词
public boolean isAnagram(String s1, String s2) {
if (s1.length() != s2.length()) return false;
char[] c1 = s1.toCharArray();
char[] c2 = s2.toCharArray();
Arrays.sort(c1);
Arrays.sort(c2);
return Arrays.equals(c1, c2);
}
然后用双重循环把所有字符串分组。这样能跑通,但是太慢了。
灵光一现
你有没有发现:不管是 "eat"、"tea" 还是 "ate",它们排序后都是 "aet"!
所以我们可以用排序后的字符串当「key」,相同的 key 就是同一组异位词。
最终解法
public List<List<String>> groupAnagrams(String[] strs) {
// 哈希表,key 是排序后的字符串,value 是属于这一组的字符串列表
Map<String, List<String>> map = new HashMap<>();
for (String str : strs) {
// 把字符串转成字符数组,然后排序
char[] chars = str.toCharArray();
Arrays.sort(chars);
// 排序后的字符串作为 key
String key = new String(chars);
// 看看这个 key 有没有出现过
if (!map.containsKey(key)) {
// 没出现过,创建新的列表
map.put(key, new ArrayList<>());
}
// 把当前字符串加入对应 key 的列表
map.get(key).add(str);
}
// 返回哈希表中所有的 value(就是所有分组)
return new ArrayList<>(map.values());
}
复杂度分析
- 时间复杂度:O(n × klogk),n 是字符串个数,k 是字符串平均长度(排序的代价)
- 空间复杂度:O(n × k),存储所有字符串
一句话总结
异位词的「身份证」就是排序后的字符串,相同身份证的住同一间房。
第三题:最长连续序列(中等)
题目
给定一个未排序的整数数组 nums,请你找出数字连续的最长序列(不重复)的长度。要求时间复杂度是 O(n)。
思路过程
第一印象:排序
数组排个序,然后找最长的连续序列,这不是很简单吗?
Arrays.sort(nums);
int maxLen = 1, curLen = 1;
for (int i = 1; i < nums.length; i++) {
if (nums[i] == nums[i-1] + 1) {
curLen++;
} else if (nums[i] != nums[i-1]) {
curLen = 1;
}
maxLen = Math.max(maxLen, curLen);
}
这样确实能跑,但是排序的时间复杂度是 O(nlogn),题目要求 O(n)。
换个思路:哈希 Set
我们能不能不排序,直接判断「某个数有没有邻居」?
关键点:只从连续序列的「起点」开始数
什么意思?比如数组 [4, 2, 6, 1, 3, 5]:
- 从 1 开始数:1, 2, 3, 4, 5, 6,长度 6 ✓
- 从 2 开始数:2, 3, 4, 5, 6,长度 5 ✗(但我们不该从 2 开始,因为 1 在它前面)
所以判断一个数是不是「起点」:就看它有没有 num - 1 这个邻居。如果没有,说明它是某个序列的起点,从这里开始数。
最终解法
public int longestConsecutive(int[] nums) {
// 边界情况
if (nums == null || nums.length == 0) return 0;
// 把所有数扔进哈希 Set,查找是 O(1)
Set<Integer> set = new HashSet<>();
for (int num : nums) {
set.add(num);
}
int longest = 0;
for (int num : set) {
// 只从「起点」开始数
// 判断方法:num - 1 不存在,就是起点
if (!set.contains(num - 1)) {
// 从这个起点开始,数连续有几个
int currentNum = num;
int currentLen = 1;
while (set.contains(currentNum + 1)) {
currentNum++;
currentLen++;
}
// 更新最长长度
longest = Math.max(longest, currentLen);
}
}
return longest;
}
复杂度分析
- 时间复杂度:O(n),每个元素最多被访问两次(一次判断是否是起点,一次被数到)
- 空间复杂度:O(n),哈希 Set 的存储空间
一句话总结
Set 找邻居,只从「没有左邻居」的数开始数,这样每个连续序列只数一遍。
第四题:移动零(简单)
题目
给定一个数组 nums,将数组中的所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。要求在原地修改,不能拷贝额外的数组。
思路过程
第一反应:再用一个数组
最简单的方法,创建一个新数组,把非零元素先放进去,再把剩余位置填零。
int[] result = new int[nums.length];
int index = 0;
for (int num : nums) {
if (num != 0) {
result[index++] = num;
}
}
// 剩余位置自动是 0
nums = result;
这样能跑,但题目要求「原地修改」,所以不能真的创建新数组。
双指针登场
想想看:我们要做的是把非零元素「挤」到前面去。
用两个指针:
p指向下一个非零元素应该放的位置(慢指针)i遍历数组,找非零元素(快指针)
图解一下:[0, 1, 0, 3, 12]
- i=0, num=0,什么都不做
- i=1, num=1,放到 p=0 的位置,p 变成 1
- i=2, num=0,什么都不做
- i=3, num=3,放到 p=1 的位置,p 变成 2
- i=4, num=12,放到 p=2 的位置,p 变成 3
- 最后把 p 之后的位置都填 0
最终解法
public void moveZeroes(int[] nums) {
// p 是「下一个非零元素应该放的位置」
int p = 0;
// i 负责遍历,找到非零元素就搬到 p 的位置
for (int i = 0; i < nums.length; i++) {
if (nums[i] != 0) {
// 把非零元素放到 p 的位置
nums[p] = nums[i];
// 如果 p 和 i 不一样,说明移动了,需要把原位置清零
if (p != i) {
nums[i] = 0;
}
// p 向后移动一位
p++;
}
}
}
复杂度分析
- 时间复杂度:O(n),遍历一遍
- 空间复杂度:O(1),原地操作
一句话总结
双指针一边「找非零」,一边「占位置」,最后剩下的位置自动归零。
第五题:盛最多水的容器(中等)
题目
给定一个长度为 n 的整数数组 height,有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i])。找出其中的两条线,使得它们与 x 轴共同构成的容器能容纳最多的水。
思路过程
先理解题意
把每条线想象成容器的一面墙,两条墙之间的距离是宽度,两条墙中较短的那个是高度,容量就是「宽度 × 高度」。
第一反应:暴力枚举
两层循环,枚举所有可能的左右墙组合,计算面积取最大。
int maxArea = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
int width = j - i;
int height = Math.min(height[i], height[j]);
maxArea = Math.max(maxArea, width * height);
}
}
时间复杂度 O(n²),太慢了。
对撞指针的灵感
想象左右两边有两堵墙,现在往中间移动。
问题是:移动哪边?
如果移动较高的那堵墙,宽度虽然变小了,但高度一定不会增加(因为由较矮的墙决定)。
如果移动较矮的那堵墙,说不定能遇到更高的墙,高度可能会增加,宽度虽然小了,但高度增加有可能让面积变大。
所以,贪心地移动较矮的那堵墙!
最终解法
public int maxArea(int[] height) {
int left = 0; // 左指针
int right = height.length - 1; // 右指针
int maxArea = 0;
while (left < right) {
// 计算当前两条线组成的面积
int width = right - left;
int h = Math.min(height[left], height[right]);
int area = width * h;
// 更新最大面积
maxArea = Math.max(maxArea, area);
// 关键:移动较矮的那一端
if (height[left] < height[right]) {
// 左边的墙矮,向右移动左指针
left++;
} else {
// 右边的墙矮(包括相等的情况),向左移动右指针
right--;
}
}
return maxArea;
}
复杂度分析
- 时间复杂度:O(n),左右指针只遍历一遍
- 空间复杂度:O(1),只用了两个指针
一句话总结
对撞指针两边往中间走,贪心移动较矮的墙,因为只有「短板变高」才有可能让面积变大。
第六题:三数之和(中等)
题目
给你一个整数数组 nums,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k,同时 nums[i] + nums[j] + nums[k] == 0。要求返回所有不重复的三元组。
思路过程
比两数之和多了一个数
还记得第一题两数之和吗?现在变成三数之和了。
第一反应:暴力枚举
三层循环,枚举所有可能的组合,时间复杂度 O(n³),铁定超时。
能不能降一维?
先固定一个数,然后剩下两个数就是「两数之和」的问题了!
但是!要求是「不重复的三元组」,直接这样做会产生大量重复结果。
排序 + 双指针
关键技巧:先排序,排序后相等的元素会挨在一起,方便去重。
步骤:
- 排序数组
- 遍历数组,用 i 固定第一个数
- 在 i 后面,用双指针 left 和 right 找另外两个数
- 如果三数之和等于 0,记录结果
- 去重:跳过相同的元素
图解:[-4, -1, -1, 0, 1, 2]
用 i = 0(-4)时,双指针找另外两个数:
- left = 1, right = 5
- -4 + (-1) + 2 = -3 < 0,说明小了,left++
- -4 + (-1) + 1 = -4 < 0,继续 left++
- ...以此类推
最终解法
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
// 先排序,排序后方便去重
Arrays.sort(nums);
// 固定第一个数
for (int i = 0; i < nums.length - 2; i++) {
// 剪枝:如果第一个数已经大于 0,后面的和不可能等于 0
if (nums[i] > 0) break;
// 去重:如果当前数和上一个数相同,跳过
// 因为以它为开头的组合,上一轮已经找过了
if (i > 0 && nums[i] == nums[i - 1]) continue;
// 双指针找另外两个数
int left = i + 1;
int right = nums.length - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) {
// 找到一个解
result.add(Arrays.asList(nums[i], nums[left], nums[right]));
// 去重:跳过相等的 left
while (left < right && nums[left] == nums[left + 1]) left++;
// 去重:跳过相等的 right
while (left < right && nums[right] == nums[right - 1]) right--;
// 移动指针继续找
left++;
right--;
} else if (sum < 0) {
// 和太小,left 向右移,增大
left++;
} else {
// 和太大,right 向左移,减小
right--;
}
}
}
return result;
}
复杂度分析
- 时间复杂度:O(n²),排序 O(nlogn) 加上双层循环
- 空间复杂度:O(1),不算结果存储
一句话总结
三数之和 = 排序 + 固定一个数 + 双指针找另外两个,排序是为了方便去重。
第七题:接雨水(困难)
题目
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子下雨之后能接多少雨水。
思路过程
先理解题意
每个柱子宽度是 1,高度是给定的。雨后能接多少水,取决于每个位置左右两边最高的柱子。
图解:
plaintext
#
# #
# # # #
# # # # #
每个位置能接的水 = min(左边最高, 右边最高) - 当前高度
第一反应:暴力
对每个位置,往左找最高,往右找最高。时间复杂度 O(n²)。
优化:预处理最高高度
先预处理出每个位置左边最高的柱子和右边最高的柱子,然后直接计算。
java
// 预处理:计算每个位置左边最高的柱子
int[] leftMax = new int[n];
leftMax[0] = height[0];
for (int i = 1; i < n; i++) {
leftMax[i] = Math.max(leftMax[i - 1], height[i]);
}
// 预处理:计算每个位置右边最高的柱子
int[] rightMax = new int[n];
rightMax[n - 1] = height[n - 1];
for (int i = n - 2; i >= 0; i--) {
rightMax[i] = Math.max(rightMax[i + 1], height[i]);
}
// 计算每个位置能接多少水
int sum = 0;
for (int i = 0; i < n; i++) {
sum += Math.min(leftMax[i], rightMax[i]) - height[i];
}
这样是 O(n) 了,但空间也是 O(n)。
对撞指针:空间优化
观察:每个位置能接的水取决于 min(左边最高, 右边最高)。
用双指针:
- left 从左往右走,right 从右往左走
- 维护 leftMax 和 rightMax
如果 leftMax < rightMax,说明左边最高比较矮,水量由左边决定,直接用 leftMax 计算,left++
为什么?因为此时「短板在左边」,不管 rightMax 多大,水量都由较小的 leftMax 决定。
最终解法
java
public int trap(int[] height) {
int left = 0;
int right = height.length - 1;
int leftMax = 0; // 左边最高的柱子
int rightMax = 0; // 右边最高的柱子
int sum = 0;
while (left < right) {
// 更新左边最高的柱子
leftMax = Math.max(leftMax, height[left]);
// 更新右边最高的柱子
rightMax = Math.max(rightMax, height[right]);
// 关键判断:哪边是短板?
if (leftMax < rightMax) {
// 左边是短板,水量由左边决定
sum += leftMax - height[left];
left++;
} else {
// 右边是短板(包括相等的情况)
sum += rightMax - height[right];
right--;
}
}
return sum;
}
复杂度分析
- 时间复杂度:O(n),一次遍历
- 空间复杂度:O(1),只用了几个变量
一句话总结
对撞指针的核心思想是「木桶效应」:哪个短板短,就由它决定水量。
第八题:无重复字符的最长子串(中等)
题目
给定一个字符串 s,请你找出其中不含有重复字符的最长子串的长度。
思路过程
什么是「子串」?
子串是连续的字符序列。比如 "abc" 的子串有 "a"、"ab"、"abc"、"b"、"bc"、"c"。
第一反应:暴力枚举
枚举所有子串,检查每个子串有没有重复字符。
int maxLen = 0;
for (int i = 0; i < s.length(); i++) {
Set<Character> set = new HashSet<>();
for (int j = i; j < s.length(); j++) {
if (set.contains(s.charAt(j))) break;
set.add(s.charAt(j));
maxLen = Math.max(maxLen, j - i + 1);
}
}
时间复杂度 O(n²),太慢。
滑动窗口
想象一个窗口在字符串上滑动:
plaintext
a b c a b c b b
^ ^
left right
我们维护一个[left, right]的窗口:
- right 向右扩展,遇到重复字符就停
- 然后 left 向右收缩,直到不重复为止
- 重复这个过程
怎么判断重复?
用哈希 Set 记录窗口里的字符,遇到 Set 里有的字符就说明重复了。
最终解法
public int lengthOfLongestSubstring(String s) {
// 用 Set 存储当前窗口里的字符
Set<Character> set = new HashSet<>();
int left = 0; // 窗口左边界
int maxLen = 0; // 记录最大长度
for (int right = 0; right < s.length(); right++) {
// 获取当前要加入的字符
char c = s.charAt(right);
// 如果字符已经在窗口里,说明有重复
// 收缩左边界,直到把这个重复字符移出去
while (set.contains(c)) {
// 移除左边的字符
set.remove(s.charAt(left));
left++;
}
// 把当前字符加入窗口
set.add(c);
// 更新最大长度
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
复杂度分析
- 时间复杂度:O(n),每个字符最多被加入和移除各一次
- 空间复杂度:O(min(n, m)),m 是字符集大小
一句话总结
滑动窗口像拉窗帘一样,右边拉开(加入字符),左边收紧(移除重复),不断找最长。
第九题:找到字符串中所有字母异位词(中等)
题目
给定两个字符串 s 和 p,找到 s 中所有 p 的异位词的起始索引,并返回这些索引的列表。
思路过程
异位词 = 字母相同 + 排列不同
还记得第二题的「字母异位词分组」吗?判断方法是一样的。
第一反应:枚举每个起点
枚举 s 中每个长度为 len(p) 的子串,判断是不是 p 的异位词。
判断方法可以用字符数组排序(太慢),或者用数组计数。
数组计数
我们不关心字符顺序,只关心每种字符出现了几次。
用两个数组分别统计 p 的字符频率和当前窗口的字符频率,如果相等就是异位词。
滑动窗口优化
窗口大小固定为 p 的长度,往右滑动:
- 右边进来一个字符
- 左边出去一个字符
- 检查两个频率数组是否相等
最终解法
public List<Integer> findAnagrams(String s, String p) {
List<Integer> result = new ArrayList<>();
// 边界情况
if (s.length() < p.length()) return result;
// 字符集只有小写字母,用数组计数比 HashMap 快
int[] count = new int[26]; // p 中每个字符出现的次数
int[] window = new int[26]; // 当前窗口中每个字符出现的次数
// 统计 p 中每个字符出现的次数
for (int i = 0; i < p.length(); i++) {
count[p.charAt(i) - 'a']++;
}
// 滑动窗口
int left = 0;
for (int right = 0; right < s.length(); right++) {
// 把右边的字符加入窗口
window[s.charAt(right) - 'a']++;
// 当窗口大小等于 p 的长度时,检查是否匹配
if (right - left + 1 == p.length()) {
// 检查两个计数数组是否相等
if (matches(count, window)) {
result.add(left);
}
// 左边字符移出窗口
window[s.charAt(left) - 'a']--;
left++;
}
}
return result;
}
// 判断两个计数数组是否相等
private boolean matches(int[] count, int[] window) {
for (int i = 0; i < 26; i++) {
if (count[i] != window[i]) {
return false;
}
}
return true;
}
复杂度分析
- 时间复杂度:O(n × 26) = O(n),n 是 s 的长度,26 是字符集大小
- 空间复杂度:O(1),只用到了固定大小的数组
一句话总结
固定大小的滑动窗口配合数组计数,右进左出,检查频率是否匹配。
第十题:和为 K 的子数组(中等)
题目
给你一个整数数组 nums 和一个整数 k,请你统计并返回该数组中和为 k 的连续子数组的个数。
思路过程
第一反应:枚举所有子数组
两层循环,枚举所有起点和终点,计算和。
int count = 0;
for (int i = 0; i < nums.length; i++) {
int sum = 0;
for (int j = i; j < nums.length; j++) {
sum += nums[j];
if (sum == k) count++;
}
}
时间复杂度 O(n²),太慢。
前缀和的灵感
想象一下:我要找以 i 结尾、和为 k 的子数组。
假设子数组是 [j, i](j 到 i),那么:
- 前缀和[i] = nums[0] + ... + nums[i]
- 前缀和[j-1] = nums[0] + ... + nums[j-1]
- 子数组和 = 前缀和[i] - 前缀和[j-1] = k
- 所以前缀和[j-1] = 前缀和[i] - k
也就是说:如果前缀和[i] - k 在之前出现过,说明存在以 i 结尾、和为 k 的子数组!
用哈希表存储前缀和出现的次数
遍历数组,一边计算前缀和,一边查表:
Map<Long, Integer> prefixCount = new HashMap<>();
// 初始时,前缀和 0 出现一次
prefixCount.put(0L, 1);
long prefix = 0;
int count = 0;
for (int num : nums) {
prefix += num;
// 找有多少个前缀和等于 prefix - k
// 那些前缀和对应的子数组 + 当前数 = k
count += prefixCount.getOrDefault(prefix - k, 0);
// 把当前前缀和加入表
prefixCount.put(prefix, prefixCount.getOrDefault(prefix, 0) + 1);
}
最终解法
public int subarraySum(int[] nums, int k) {
// 哈希表:key 是前缀和,value 是这个前缀和出现的次数
Map<Integer, Integer> prefixCount = new HashMap<>();
// 初始状态:前缀和为 0 出现过 1 次
// (空数组的前缀和为 0)
prefixCount.put(0, 1);
int count = 0; // 计数
int prefix = 0; // 当前前缀和
for (int num : nums) {
// 计算当前前缀和
prefix += num;
// 关键:找有多少个「之前的」前缀和满足:
// prefix - previousPrefix = k
// 即 previousPrefix = prefix - k
if (prefixCount.containsKey(prefix - k)) {
// 加上这个前缀和出现的次数
// 因为每个这样的前缀和位置,都能形成一个和为 k 的子数组
count += prefixCount.get(prefix - k);
}
// 把当前前缀和加入哈希表
prefixCount.put(prefix, prefixCount.getOrDefault(prefix, 0) + 1);
}
return count;
}
复杂度分析
- 时间复杂度:O(n),遍历一次
- 空间复杂度:O(n),最坏情况所有前缀和都不一样
一句话总结
前缀和 + 哈希表的组合:用空间换时间,查「前缀和 - k」出现的次数。
总结
今天咱们学了这些题,用到了两个核心套路:
表格
| 技巧 | 适用场景 | 核心思想 |
|---|---|---|
| 哈希 | 需要快速查找、配对 | 把已经遍历的东西存起来,后面直接查 |
| 双指针 | 区间、连续序列问题 | 两边往中间靠,或者滑动窗口 |
表格
| 题目 | 解法 | 关键点 |
|---|---|---|
| 两数之和 | 哈希 | 一边遍历一边存,O(n) 解决 |
| 字母异位词分组 | 哈希 + 排序 | 排序后的字符串作为 key |
| 最长连续序列 | 哈希 Set | 只从「起点」开始数 |
| 移动零 | 双指针 | 快慢指针,非零往前搬 |
| 盛最多水的容器 | 对撞指针 | 移动较矮的墙 |
| 三数之和 | 排序 + 双指针 | 固定一个,双指针找两个 |
| 接雨水 | 对撞指针 | 木桶效应,短板决定水量 |
| 无重复字符最长子串 | 滑动窗口 | 右进左出,找最长 |
| 所有字母异位词 | 滑动窗口 | 固定窗口,数组计数 |
| 和为 K 的子数组 | 前缀和 + 哈希 | prefix - k 出现的次数 |
记住:刷题不是背答案,是学套路。把这些套路刻进脑子里,遇到新题就能举一反三了。
下期预告:链表篇,咱们继续用大白话讲链表的各种操作。
有问题欢迎评论区交流,咱们下期见!