题目1: 最长回文子串(难度:中高)
问题描述:
给定一个字符串 s
,找出其中最长的回文子串(子串是原字符串中连续的一段)。例如,输入 s = "babad"
,输出可能是 "bab"
或 "aba"
。输入保证非空。
解题思路:
使用动态规划(DP)优化暴力解法。定义二维DP数组 dp[i][j]
表示子串 s[i..j]
是否为回文。状态转移方程为:
$$dp[i][j] = (s[i] == s[j]) \land dp[i+1][j-1]$$
其中,当子串长度小于等于2时,直接判断字符是否相等。初始化时,所有长度为1的子串为回文(即 dp[i][i] = True
)。遍历所有可能子串长度,记录最大回文子串的起始位置和长度。
代码实现:
def longest_palindrome(s: str) -> str:
n = len(s)
if n < 2:
return s
# 初始化DP数组,dp[i][j]表示s[i..j]是否为回文
dp = [[False] * n for _ in range(n)]
start, max_len = 0, 1 # 记录最长回文起始位置和长度
# 所有长度为1的子串都是回文
for i in range(n):
dp[i][i] = True
# 遍历子串长度L从2到n
for L in range(2, n + 1):
for i in range(n - L + 1):
j = i + L - 1 # 子串结束位置
if s[i] == s[j]:
if L == 2: # 长度为2的子串
dp[i][j] = True
else:
dp[i][j] = dp[i + 1][j - 1] # 状态转移
if dp[i][j] and L > max_len: # 更新最长回文
start = i
max_len = L
else:
dp[i][j] = False
return s[start:start + max_len]
# 测试示例
print(longest_palindrome("babad")) # 输出: "bab" 或 "aba"
时间复杂度分析:
$O(n^2)$,其中 $n$ 是字符串长度。空间复杂度 $O(n^2)$,可通过Manacher算法优化到 $O(n)$,但DP解法更易理解。
题目2: 合并K个排序链表(难度:高)
问题描述:
给定一个包含 $k$ 个排序链表的数组 lists
,每个链表已按升序排列。合并所有链表为一个排序链表并返回。例如,输入 lists = [[1,4,5],[1,3,4],[2,6]]
,输出 [1,1,2,3,4,4,5,6]
。
解题思路:
使用最小堆(优先队列)高效合并。关键点:
- 堆中存储每个链表的当前节点,比较节点值。
- 每次从堆中弹出最小值,添加到结果链表,并推进该链表的指针。
- 堆的大小为 $k$,确保每次操作高效。
数学上,堆的插入和弹出操作时间复杂度为 $O(\log k)$,总时间复杂度优化为 $O(n \log k)$,其中 $n$ 是总节点数。
代码实现:
import heapq
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def merge_k_lists(lists: list[ListNode]) -> ListNode:
if not lists:
return None
# 使用最小堆,存储元组 (节点值, 链表索引, 节点)
heap = []
for i, node in enumerate(lists):
if node:
heapq.heappush(heap, (node.val, i, node))
dummy = ListNode(0) # 哑节点简化操作
curr = dummy
while heap:
val, idx, node = heapq.heappop(heap)
curr.next = node
curr = curr.next
if node.next: # 推进链表指针
heapq.heappush(heap, (node.next.val, idx, node.next))
return dummy.next
# 辅助函数:创建链表和打印(面试中可能不需要,但为完整性添加)
def create_list(arr):
dummy = ListNode()
curr = dummy
for num in arr:
curr.next = ListNode(num)
curr = curr.next
return dummy.next
def print_list(node):
res = []
while node:
res.append(node.val)
node = node.next
print(res)
# 测试示例
lists = [create_list([1,4,5]), create_list([1,3,4]), create_list([2,6])]
merged = merge_k_lists(lists)
print_list(merged) # 输出: [1,1,2,3,4,4,5,6]
时间复杂度分析:
$O(n \log k)$,其中 $n$ 是总节点数,$k$ 是链表数。空间复杂度 $O(k)$ 用于堆。
题目3: 编辑距离(难度:高)
问题描述:
给定两个单词 word1
和 word2
,计算将 word1
转换为 word2
所需的最小操作数。操作包括:插入一个字符、删除一个字符或替换一个字符。例如,输入 word1 = "horse"
, word2 = "ros"
,输出 3
(删除 'h'、替换 'r' 为 'o'、删除 'e')。
解题思路:
动态规划是标准解法。定义 dp[i][j]
为将 word1[0..i-1]
转换为 word2[0..j-1]
的最小编辑距离。状态转移方程为:
$$dp[i][j] = \begin{cases} \min(dp[i-1][j] + 1, dp[i][j-1] + 1, dp[i-1][j-1] + \text{cost}) & \text{if } i>0, j>0 \ j & \text{if } i=0 \ i & \text{if } j=0 \end{cases}$$
其中,cost
为0如果 word1[i-1] == word2[j-1]
,否则为1(表示替换操作)。
代码实现:
def min_distance(word1: str, word2: str) -> int:
m, n = len(word1), len(word2)
# 初始化DP数组,dp[i][j]表示word1前i字符到word2前j字符的距离
dp = [[0] * (n + 1) for _ in range(m + 1)]
# 边界条件:空字符串转换
for i in range(m + 1):
dp[i][0] = i # 删除所有字符
for j in range(n + 1):
dp[0][j] = j # 插入所有字符
# 填充DP表
for i in range(1, m + 1):
for j in range(1, n + 1):
if word1[i - 1] == word2[j - 1]:
cost = 0
else:
cost = 1
# 状态转移:取删除、插入、替换的最小值
dp[i][j] = min(
dp[i-1][j] + 1, # 删除word1[i-1]
dp[i][j-1] + 1, # 插入word2[j-1]
dp[i-1][j-1] + cost # 替换或匹配
)
return dp[m][n]
# 测试示例
print(min_distance("horse", "ros")) # 输出: 3
print(min_distance("intention", "execution")) # 输出: 5
时间复杂度分析:
$O(m \times n)$,其中 $m$ 和 $n$ 是单词长度。空间复杂度 $O(m \times n)$,可优化到 $O(\min(m, n))$。
题目4: 二叉树的序列化与反序列化(难度:中高)
问题描述:
设计一个算法来序列化和反序列化二叉树。序列化是将二叉树转换为字符串,反序列化是将字符串还原为二叉树。例如,输入二叉树:
1
/ \
2 3
/ \
4 5
序列化输出可能为 "1,2,None,None,3,4,None,None,5,None,None"
(使用先序遍历)。
解题思路:
使用深度优先搜索(DFS)进行先序遍历。序列化时,遍历节点并记录值,空节点用特殊标记(如 "None"
)。反序列化时,根据序列重建树结构。关键点:
- 序列化:递归先序遍历,拼接字符串。
- 反序列化:分割字符串为列表,递归构建子树。
数学上,树的高度 $h$ 影响时间复杂度,平均情况下为 $O(n)$。
代码实现:
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class Codec:
def serialize(self, root: TreeNode) -> str:
"""Encodes a tree to a single string."""
if not root:
return "None"
# 先序遍历:根-左-右
left = self.serialize(root.left)
right = self.serialize(root.right)
return f"{root.val},{left},{right}" # 使用逗号分隔
def deserialize(self, data: str) -> TreeNode:
"""Decodes your encoded data to tree."""
nodes = data.split(',') # 分割字符串为列表
return self._deserialize_helper(nodes)
def _deserialize_helper(self, nodes: list) -> TreeNode:
if nodes[0] == "None":
nodes.pop(0)
return None
# 构建当前节点
root = TreeNode(int(nodes[0]))
nodes.pop(0)
root.left = self._deserialize_helper(nodes) # 递归左子树
root.right = self._deserialize_helper(nodes) # 递归右子树
return root
# 测试示例
# 构建树: 1 -> 2, 3; 3 -> 4, 5
root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.right.left = TreeNode(4)
root.right.right = TreeNode(5)
codec = Codec()
serialized = codec.serialize(root)
print(serialized) # 输出类似: "1,2,None,None,3,4,None,None,5,None,None"
deserialized = codec.deserialize(serialized)
# 验证:可通过遍历检查树结构
时间复杂度分析:
序列化和反序列化均为 $O(n)$,其中 $n$ 是节点数。空间复杂度 $O(n)$ 用于递归栈。
亚马逊高频高难度算法题解析:矩阵中的最长递增路径
题目描述
给定一个 $m \times n$ 整数矩阵,找出最长严格递增路径的长度(每一步可向上下左右移动,但不能重复访问单元格)。
示例
输入矩阵:
$$\begin{bmatrix}
9 & 9 & 4 \
6 & 6 & 8 \
2 & 1 & 1
\end{bmatrix}$$
输出:$4$(路径 $1 \to 2 \to 6 \to 9$)
解题思路(DFS + 记忆化)
1. 问题分析
- 核心挑战:路径方向不固定,暴力DFS会重复计算子路径。
- 关键优化:为每个单元格 $(i,j)$ 存储最长路径长度 $dp[i][j]$,避免重复计算。
2. 算法设计
- 状态定义:$dp[i][j]$ 表示以 $(i,j)$ 为起点的最长递增路径长度。
- 转移方程:
$$dp[i][j] = 1 + \max_{\text{dir} \in {\text{上,下,左,右}}} \left( dp[\text{next}] \right)$$
其中 $\text{next}$ 需满足值大于 $matrix[i][j]$。 - 终止条件:当所有邻居均小于当前值时,$dp[i][j] = 1$。
3. 复杂度分析
- 时间复杂度:$O(mn)$(每个单元格计算一次)
- 空间复杂度:$O(mn)$(存储 $dp$ 矩阵)
代码实现(Python)
def longestIncreasingPath(matrix):
if not matrix: return 0
m, n = len(matrix), len(matrix[0])
dp = [[0] * n for _ in range(m)] # 记忆化数组
max_len = 0
def dfs(i, j):
if dp[i][j]: return dp[i][j] # 已计算则直接返回
directions = [(0,1), (1,0), (0,-1), (-1,0)]
max_path = 1
for dx, dy in directions:
x, y = i + dx, j + dy
if 0 <= x < m and 0 <= y < n and matrix[x][y] > matrix[i][j]:
max_path = max(max_path, 1 + dfs(x, y))
dp[i][j] = max_path # 存储结果
return max_path
for i in range(m):
for j in range(n):
max_len = max(max_len, dfs(i, j))
return max_len
关键考点
- DFS剪枝:通过 $dp$ 数组避免重复计算子问题。
- 方向处理:四方向移动需检查边界和递增条件。
- 初始化技巧:$dp$ 初始为 $0$,首次访问时计算并存储。
应用场景:亚马逊物流路径优化、游戏地图寻路等动态规划高频题型。练习时需重点掌握状态转移设计和记忆化搜索的平衡。