数组与字符串专题
数组和字符串是算法面试的重中之重,掌握双指针、滑动窗口、哈希表三大技巧
📊 题目总览
| 题目 | 难度 | 梯队 | 关键技巧 |
|---|---|---|---|
| 两数之和 | ⭐⭐ | 第一梯队 | 哈希表 |
| 最大子序和 | ⭐⭐ | 第一梯队 | Kadane算法 |
| 无重复字符最长子串 | ⭐⭐⭐ | 第二梯队 | 滑动窗口 |
| 三数之和 | ⭐⭐ | 第三梯队 | 排序+双指针 |
| 盛水容器 | ⭐⭐ | 进阶 | 双指针 |
| 接雨水 | ⭐⭐⭐ | 进阶 | 双指针/单调栈 |
| 搜索旋转排序数组 | ⭐⭐ | 进阶 | 二分查找 |
| 最小覆盖子串 | ⭐⭐⭐ | 进阶 | 滑动窗口 |
| 岛屿数量 | ⭐⭐ | 进阶 | DFS/BFS |
1. 两数之和
LeetCode: 1. 两数之和 | 难度: 简单 | 梯队: 第一梯队 ✅
题目描述
给定一个整数数组 nums 和一个目标值 target,请找出和为目标值的两个整数的下标。
核心思路
使用哈希表存储已遍历的数字及其索引,每次检查 target - 当前值 是否存在。
代码实现
/**
* 两数之和
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
function twoSum(nums, target) {
const map = new Map();
for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i];
if (map.has(complement)) {
return [map.get(complement), i];
}
map.set(nums[i], i);
}
return [];
}示例调用
// 基础示例
console.log(twoSum([2, 7, 11, 15], 9)); // [0, 1]
// 答案不在开头
console.log(twoSum([3, 2, 4], 6)); // [1, 2]
// 两个相同的数
console.log(twoSum([3, 3], 6)); // [0, 1]复杂度分析
- 时间: O(n)
- 空间: O(n)
2. 最大子序和
LeetCode: 53. 最大子数组和 | 难度: 中等 | 梯队: 第一梯队 ✅
题目描述
给定一个整数数组 nums,找到具有最大和的连续子数组(至少包含一个元素),返回其最大和。
核心思路
Kadane 算法:遍历时维护当前子数组的最大和,若当前和为负则重新开始。
代码实现
/**
* 最大子序和 - Kadane 算法
* @param {number[]} nums
* @return {number}
*/
function maxSubArray(nums) {
let maxSum = nums[0];
let currentSum = nums[0];
for (let i = 1; i < nums.length; i++) {
// 要么从当前位置重新开始,要么加入之前的和
currentSum = Math.max(nums[i], currentSum + nums[i]);
maxSum = Math.max(maxSum, currentSum);
}
return maxSum;
}
/**
* DP 写法(更易理解)
* dp[i] = 以 nums[i] 结尾的最大子数组和
*/
function maxSubArrayDP(nums) {
const dp = new Array(nums.length);
dp[0] = nums[0];
for (let i = 1; i < nums.length; i++) {
dp[i] = Math.max(nums[i], dp[i - 1] + nums[i]);
}
return Math.max(...dp);
}示例调用
// 基础示例
console.log(maxSubArray([-2, 1, -3, 4, -1, 2, 1, -5, 4])); // 6 ([4,-1,2,1])
// 全是负数
console.log(maxSubArray([-1, -2, -3])); // -1
// 单个元素
console.log(maxSubArray([5])); // 5复杂度分析
- 时间: O(n)
- 空间: O(1)(Kadane)/ O(n)(DP)
3. 无重复字符最长子串
LeetCode: 3. 无重复字符的最长子串 | 难度: 中等 | 梯队: 第二梯队
题目描述
给定一个字符串 s,请找出其中不含重复字符的最长子串的长度。
核心思路
滑动窗口:右指针扩展窗口,遇到重复字符时移动左指针缩小窗口。
代码实现
/**
* 无重复字符最长子串
* @param {string} s
* @return {number}
*/
function lengthOfLongestSubstring(s) {
const map = new Map(); // char -> 最新索引
let left = 0;
let maxLen = 0;
for (let right = 0; right < s.length; right++) {
const char = s[right];
// 如果字符已存在且在窗口内,移动左边界
if (map.has(char) && map.get(char) >= left) {
left = map.get(char) + 1;
}
map.set(char, right);
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}示例调用
// 基础示例
console.log(lengthOfLongestSubstring("abcabcbb")); // 3 ("abc")
// 全部相同
console.log(lengthOfLongestSubstring("bbbbb")); // 1
// 最长在中间
console.log(lengthOfLongestSubstring("pwwkew")); // 3 ("wke")复杂度分析
- 时间: O(n)
- 空间: O(min(n, m)),m 为字符集大小
4. 三数之和
LeetCode: 15. 三数之和 | 难度: 中等 | 梯队: 第三梯队
题目描述
给定整数数组 nums,判断是否存在三个元素使其和为 0。找出所有满足条件且不重复的三元组。
核心思路
排序 + 双指针:固定第一个数,用双指针在剩余区间找两数之和。
代码实现
/**
* 三数之和
* @param {number[]} nums
* @return {number[][]}
*/
function threeSum(nums) {
const result = [];
nums.sort((a, b) => a - b);
for (let i = 0; i < nums.length - 2; i++) {
// 剪枝:最小值大于0,不可能和为0
if (nums[i] > 0) break;
// 跳过重复元素
if (i > 0 && nums[i] === nums[i - 1]) continue;
let left = i + 1;
let right = nums.length - 1;
while (left < right) {
const sum = nums[i] + nums[left] + nums[right];
if (sum === 0) {
result.push([nums[i], nums[left], nums[right]]);
// 跳过重复元素
while (left < right && nums[left] === nums[left + 1]) left++;
while (left < right && nums[right] === nums[right - 1]) right--;
left++;
right--;
} else if (sum < 0) {
left++;
} else {
right--;
}
}
}
return result;
}示例调用
// 基础示例
console.log(threeSum([-1, 0, 1, 2, -1, -4]));
// 输出: [[-1, -1, 2], [-1, 0, 1]]
// 全是0
console.log(threeSum([0, 0, 0, 0])); // [[0, 0, 0]]
// 无解
console.log(threeSum([0, 1, 1])); // []复杂度分析
- 时间: O(n²)
- 空间: O(log n) 排序
5. 盛水容器
LeetCode: 11. 盛最多水的容器 | 难度: 中等
题目描述
给定 n 个非负整数 height,每个数代表坐标中的一个点 (i, height[i])。找出两条线组成的容器能容纳最多的水。
核心思路
双指针:从两端向中间移动,每次移动较短的边(移动较长边只会减小面积)。
代码实现
/**
* 盛最多水的容器
* @param {number[]} height
* @return {number}
*/
function maxArea(height) {
let left = 0;
let right = height.length - 1;
let maxWater = 0;
while (left < right) {
const h = Math.min(height[left], height[right]);
const w = right - left;
maxWater = Math.max(maxWater, h * w);
// 移动较短的边
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return maxWater;
}示例调用
// 基础示例
console.log(maxArea([1, 8, 6, 2, 5, 4, 8, 3, 7])); // 49
// 两个元素
console.log(maxArea([1, 1])); // 1
// 递增数组
console.log(maxArea([1, 2, 3, 4, 5])); // 6复杂度分析
- 时间: O(n)
- 空间: O(1)
6. 接雨水
LeetCode: 42. 接雨水 | 难度: 困难
可视化演示:点击查看算法动画
题目描述
给定 n 个非负整数 height,每个数代表坐标中的一个点 (i, height[i])。计算在下雨后能接多少雨水。
核心思路
双指针的贪心思想:当前位置能接的水量取决于左侧最高和右侧最高中的较小值。双指针从两端向中间移动,每次处理较低的一侧。
代码实现
/**
* 接雨水 - 双指针解法(推荐)
* @param {number[]} height
* @return {number}
*/
function trap(height) {
if (height.length < 3) return 0;
let left = 0;
let right = height.length - 1;
let leftMax = 0;
let rightMax = 0;
let water = 0;
while (left < right) {
if (height[left] < height[right]) {
// 处理左侧
if (height[left] >= leftMax) {
leftMax = height[left];
} else {
water += leftMax - height[left];
}
left++;
} else {
// 处理右侧
if (height[right] >= rightMax) {
rightMax = height[right];
} else {
water += rightMax - height[right];
}
right--;
}
}
return water;
}示例调用
// 基础示例
console.log(trap([0,1,0,2,1,0,1,3,2,1,2,1])); // 6
// 中间低两边高
console.log(trap([4, 2, 0, 3, 2, 5])); // 9
// 无法接水(单调递增)
console.log(trap([1, 2, 3, 4])); // 0复杂度分析
- 时间: O(n),空间: O(1)(双指针最优)
- 其他解法:DP 预计算左右最大值 O(n) 空间;单调栈同样 O(n) 时间/空间
7. 搜索旋转排序数组
LeetCode: 33. 搜索旋转排序数组 | 难度: 中等
题目描述
升序数组在某个点进行了旋转(如 [0,1,2,4,5,6,7] 变为 [4,5,6,7,0,1,2])。给定目标值,若存在返回索引,否则返回 -1。
核心思路
二分查找:每次至少有一半是有序的,判断 target 在哪一半。
代码实现
/**
* 搜索旋转排序数组
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
function search(nums, target) {
let left = 0;
let right = nums.length - 1;
while (left <= right) {
const mid = left + Math.floor((right - left) / 2);
if (nums[mid] === target) return mid;
// 判断哪一半是有序的
if (nums[left] <= nums[mid]) {
// 左半部分有序
if (nums[left] <= target && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else {
// 右半部分有序
if (nums[mid] < target && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}示例调用
// 基础示例
console.log(search([4, 5, 6, 7, 0, 1, 2], 0)); // 4
// 目标在左半部分
console.log(search([4, 5, 6, 7, 0, 1, 2], 5)); // 1
// 目标不存在
console.log(search([4, 5, 6, 7, 0, 1, 2], 3)); // -1复杂度分析
- 时间: O(log n)
- 空间: O(1)
8. 最小覆盖子串
LeetCode: 76. 最小覆盖子串 | 难度: 困难
可视化演示:点击查看算法动画
题目描述
给定字符串 s 和 t,返回 s 中涵盖 t 所有字符的最小子串。
代码实现
/**
* 最小覆盖子串
* @param {string} s
* @param {string} t
* @return {string}
*/
function minWindow(s, t) {
const need = new Map();
for (const c of t) need.set(c, (need.get(c) || 0) + 1);
let left = 0, minLen = Infinity, start = 0;
let required = need.size, formed = 0;
const window = new Map();
for (let right = 0; right < s.length; right++) {
const c = s[right];
window.set(c, (window.get(c) || 0) + 1);
if (need.has(c) && window.get(c) === need.get(c)) {
formed++;
}
// 收缩窗口
while (formed === required) {
if (right - left + 1 < minLen) {
minLen = right - left + 1;
start = left;
}
const d = s[left];
window.set(d, window.get(d) - 1);
if (need.has(d) && window.get(d) < need.get(d)) {
formed--;
}
left++;
}
}
return minLen === Infinity ? '' : s.substring(start, start + minLen);
}示例调用
// 基础示例
console.log(minWindow("ADOBECODEBANC", "ABC")); // "BANC"
// 边界条件:t 包含重复字符
console.log(minWindow("aa", "aa")); // "aa"
// 边界条件:不存在
console.log(minWindow("a", "aa")); // ""
// 边界条件:s 等于 t
console.log(minWindow("abc", "abc")); // "abc"复杂度分析
- 时间: O(n)
- 空间: O(m),m 为字符集大小
9. 岛屿数量
LeetCode: 200. 岛屿数量 | 难度: 中等
可视化演示:点击查看算法动画
题目描述
给定一个由 '1'(陆地)和 '0'(水)组成的二维网格,计算岛屿数量。
代码实现
/**
* 岛屿数量 - DFS
* @param {character[][]} grid
* @return {number}
*/
function numIslands(grid) {
if (!grid.length) return 0;
let count = 0;
function dfs(i, j) {
// 边界检查
if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length) return;
if (grid[i][j] !== '1') return;
grid[i][j] = '0'; // 标记已访问
dfs(i + 1, j);
dfs(i - 1, j);
dfs(i, j + 1);
dfs(i, j - 1);
}
for (let i = 0; i < grid.length; i++) {
for (let j = 0; j < grid[0].length; j++) {
if (grid[i][j] === '1') {
count++;
dfs(i, j);
}
}
}
return count;
}示例调用
// 基础示例
const grid1 = [
['1', '1', '1', '1', '0'],
['1', '1', '0', '1', '0'],
['1', '1', '0', '0', '0'],
['0', '0', '0', '0', '0']
];
console.log(numIslands(grid1)); // 1
// 多个岛屿
const grid2 = [
['1', '1', '0', '0', '0'],
['1', '1', '0', '0', '0'],
['0', '0', '1', '0', '0'],
['0', '0', '0', '1', '1']
];
console.log(numIslands(grid2)); // 3
// 全是水
console.log(numIslands([['0', '0'], ['0', '0']])); // 0复杂度分析
- 时间: O(m × n)
- 空间: O(m × n) 最坏情况递归栈
💡 数组/字符串技巧总结
| 技巧 | 适用场景 | 时间复杂度 |
|---|---|---|
| 哈希表 | 查找配对、去重 | O(n) |
| 双指针 | 有序数组、回文 | O(n) |
| 滑动窗口 | 连续子串/子数组 | O(n) |
| 二分查找 | 有序数组搜索 | O(log n) |
| Kadane算法 | 最大子数组和 | O(n) |