数据结构 / 01
数组与字符串
数组和字符串是算法面试中出现频率最高的数据结构。几乎所有其他数据结构和算法都建立在它们之上。扎实掌握数组与字符串的基本操作和常见题型,是刷题之路的第一步。
什么是数组
数组是一块连续的内存空间,用于存储相同类型(或在 Python 中为任意类型)的元素。
核心特性:
- O(1) 随机访问:通过索引直接计算内存地址,瞬间定位元素。
- O(n) 插入 / 删除:在中间位置操作时,需要移动后续所有元素。
- 缓存友好:连续内存布局使 CPU 缓存命中率高,实际性能优于链表。
Python 中的 list 本质上是一个动态数组:当容量不够时会自动扩容(通常扩为原来的约 1.125 倍),均摊 append 时间复杂度为 O(1)。
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
按索引访问 a[i] |
O(1) | 随机访问 |
末尾追加 a.append(x) |
均摊 O(1) | 可能触发扩容 |
中间插入 a.insert(i, x) |
O(n) | 需要移动元素 |
删除元素 a.pop(i) |
O(n) | pop() 末尾删除为 O(1) |
查找元素 x in a |
O(n) | 线性扫描 |
切片 a[i:j] |
O(j-i) | 创建新列表 |
数组常用操作
遍历
nums = [1, 2, 3, 4, 5]
# 遍历值
for num in nums:
print(num)
# 同时获取索引和值
for i, num in enumerate(nums):
print(i, num)
切片与反转
a = [1, 2, 3, 4, 5]
a[1:4] # [2, 3, 4] —— 左闭右开
a[::-1] # [5, 4, 3, 2, 1] —— 反转
a[::2] # [1, 3, 5] —— 步长为 2
原地操作 vs 新建数组
面试中经常要求原地修改(in-place),即空间复杂度 O(1),不允许新建等长数组。关键技巧是用指针 / 索引标记写入位置。
# 原地移除所有值为 val 的元素,返回新长度
def remove_element(nums: list[int], val: int) -> int:
slow = 0
for fast in range(len(nums)):
if nums[fast] != val:
nums[slow] = nums[fast]
slow += 1
return slow
字符串基础
Python 字符串是不可变对象:任何”修改”操作都会创建新字符串。频繁拼接时应先收集到列表,再用 join 合并。
常用方法速查
s = " hello, world "
s.strip() # "hello, world" 去除首尾空白
s.split(", ") # ["hello", "world"] 按分隔符拆分
", ".join(["a","b"]) # "a, b" 用分隔符连接
s.replace("o", "0") # " hell0, w0rld " 替换子串
s.find("world") # 9 查找子串位置,未找到返回 -1
字符串与列表互转
s = "hello"
chars = list(s) # ['h', 'e', 'l', 'l', 'o']
chars.reverse() # 原地反转列表
result = "".join(chars) # "olleh"
这是面试中处理”原地修改字符串”类题目的标准做法:先转 list,操作完再转回。
高频题型与解题模式
双指针
双指针是数组 / 字符串题目中最常用的技巧,核心思想是用两个指针协作遍历,将 O(n^2) 降为 O(n)。
对撞指针
两个指针分别从数组两端向中间移动,常用于有序数组或需要考虑区间的场景。
# 模板:对撞指针
def two_pointer(nums):
left, right = 0, len(nums) - 1
while left < right:
# 根据条件移动指针
if condition(nums[left], nums[right]):
left += 1
else:
right -= 1
典型题目:
- 两数之和 II(有序数组):和太小则左指针右移,和太大则右指针左移。
- 盛最多水的容器:每次移动较短的那一边,因为移动较长边不可能得到更大面积。
快慢指针
两个指针同向移动,慢指针标记”已处理”区域的边界,快指针负责扫描。
# 经典题:移动零到末尾(保持非零元素相对顺序)
def move_zeroes(nums: list[int]) -> None:
slow = 0 # slow 指向下一个非零元素应放的位置
for fast in range(len(nums)):
if nums[fast] != 0:
nums[slow], nums[fast] = nums[fast], nums[slow]
slow += 1
滑动窗口
滑动窗口适用于连续子数组 / 子串问题。维护一个窗口 [left, right),右端扩张探索、左端收缩维持约束。
# 模板:最长满足条件的子串/子数组
def sliding_window(s):
left = 0
window = {} # 窗口内的状态(如字符计数)
result = 0
for right in range(len(s)):
# 1. 扩张:将 s[right] 加入窗口
window[s[right]] = window.get(s[right], 0) + 1
# 2. 收缩:当窗口不满足条件时,移动左端
while not valid(window):
window[s[left]] -= 1
if window[s[left]] == 0:
del window[s[left]]
left += 1
# 3. 更新答案
result = max(result, right - left + 1)
return result
典型题目:
- 最长无重复子串:窗口内不允许重复字符,出现重复时收缩左端。
- 最小覆盖子串:窗口需包含目标所有字符,满足时尝试收缩。
前缀和
前缀和将”区间求和”从 O(n) 降为 O(1),是处理子数组求和问题的利器。
# 构建前缀和数组
nums = [1, 2, 3, 4, 5]
prefix = [0] * (len(nums) + 1)
for i in range(len(nums)):
prefix[i + 1] = prefix[i] + nums[i]
# prefix = [0, 1, 3, 6, 10, 15]
# 区间 [i, j] 的和 = prefix[j+1] - prefix[i]
# 例:nums[1:4] 的和 = prefix[4] - prefix[1] = 10 - 1 = 9
结合哈希表可以解决”和为 k 的子数组个数”等问题:遍历时记录已出现的前缀和,查找 当前前缀和 - k 是否存在。
经典题目
以下是数组与字符串章节最值得练习的高频题目,按难度分组:
Easy
| # | 题目 | 核心技巧 |
|---|---|---|
| 283 | 移动零 | 快慢指针、原地操作 |
| 26 | 删除有序数组中的重复项 | 快慢指针 |
| 88 | 合并两个有序数组 | 逆向双指针 |
Medium
| # | 题目 | 核心技巧 |
|---|---|---|
| 11 | 盛最多水的容器 | 对撞指针 |
| 15 | 三数之和 | 排序 + 对撞指针 + 去重 |
| 3 | 无重复字符的最长子串 | 滑动窗口 |
| 56 | 合并区间 | 排序 + 贪心 |
| 238 | 除自身以外数组的乘积 | 前缀积 + 后缀积 |
Hard
| # | 题目 | 核心技巧 |
|---|---|---|
| 42 | 接雨水 | 对撞指针 / 单调栈 |
建议按 Easy -> Medium -> Hard 的顺序练习。每道题先独立思考 15 分钟,想不出来再看提示。
小结
- 数组的核心优势是 O(1) 随机访问;面试中重点考察原地操作能力。
- 字符串在 Python 中不可变,修改时先转 list 再转回是常见套路。
- 双指针是数组题的第一解题直觉:对撞指针处理有序/区间问题,快慢指针处理原地操作。
- 滑动窗口专攻连续子数组/子串的最优值问题,掌握”扩张-收缩”模板即可覆盖大部分变体。
- 前缀和将区间求和降为 O(1),配合哈希表可以解决更复杂的子数组求和问题。
熟练掌握以上模式后,数组与字符串类题目的解题速度会有质的提升。