【Leetcode】
文章目录
- 题目一:合并K个升序链表(LeetCode 23,困难)
- 题目分析
- 解题思路(优先队列+虚拟头节点)
- 示例代码
- 代码解析
- 题目二:戳气球(LeetCode 312,困难)
- 题目分析
- 解题思路(区间动态规划+逆向思维)
- 示例代码
- 代码解析
- 题目三:编辑距离(LeetCode 72,困难)
- 题目分析
- 解题思路(动态规划+状态细分)
- 示例代码(空间优化版)
- 代码解析
🌈你好呀!我是 山顶风景独好
🎈欢迎踏入我的博客世界,能与您在此邂逅,真是缘分使然!😊
🌸愿您在此停留的每一刻,都沐浴在轻松愉悦的氛围中。
📖这里不仅有丰富的知识和趣味横生的内容等您来探索,更是一个自由交流的平台,期待您留下独特的思考与见解。🌟
🚀让我们一起踏上这段探索与成长的旅程,携手挖掘更多可能,共同进步!💪✨
题目一:合并K个升序链表(LeetCode 23,困难)
题目分析
给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中,返回合并后的链表。例如:
- 输入
lists = [[1,4,5],[1,3,4],[2,6]],输出[1,1,2,3,4,4,5,6]; - 输入
lists = [],输出[]; - 输入
lists = [[]],输出[]; - 输入
lists = [[-1,5,11],[6,10]],输出[-1,5,6,10,11]。
解题思路(优先队列+虚拟头节点)
核心是用最小堆(优先队列) 维护所有链表的当前头节点,每次取出最小节点接入结果链表,再将该节点的下一个节点加入堆,实现高效合并,具体步骤如下:
-
优先队列初始化
- 因 Python 优先队列默认是最小堆,存储元素为
(节点值, 链表索引, 节点)——加入“链表索引”是为了避免节点值相同时直接比较节点对象报错; - 遍历链表数组,若链表非空,将其头节点加入优先队列。
- 因 Python 优先队列默认是最小堆,存储元素为
-
构建结果链表
- 创建虚拟头节点
dummy和当前指针curr:虚拟头节点用于简化“结果链表为空时接入第一个节点”的逻辑,curr用于遍历结果链表; - 循环提取堆顶(最小节点):
- 将堆顶节点接入
curr的next指针; - 若该节点的
next非空,将next节点加入优先队列; - 移动
curr指针到下一个节点。
- 将堆顶节点接入
- 创建虚拟头节点
-
结果返回
合并完成后,返回dummy.next(虚拟头节点的下一个节点即为合并后链表的头)。
示例代码
import heapq# Definition for singly-linked list.
class ListNode:def __init__(self, val=0, next=None):self.val = valself.next = nextclass Solution:def mergeKLists(self, lists):heap = []# 初始化堆:将非空链表的头节点加入堆for idx, head in enumerate(lists):if head:heapq.heappush(heap, (head.val, idx, head))# 虚拟头节点简化拼接dummy = ListNode()curr = dummywhile heap:# 取出堆顶最小节点val, idx, node = heapq.heappop(heap)curr.next = nodecurr = curr.next# 若当前节点有下一个节点,加入堆if node.next:heapq.heappush(heap, (node.next.val, idx, node.next))return dummy.next# 辅助函数:数组转链表
def array_to_list(arr):dummy = ListNode()curr = dummyfor num in arr:curr.next = ListNode(num)curr = curr.nextreturn dummy.next# 辅助函数:链表转数组(用于测试输出)
def list_to_array(head):arr = []while head:arr.append(head.val)head = head.nextreturn arr# 测试示例
lists1 = [array_to_list([1,4,5]), array_to_list([1,3,4]), array_to_list([2,6])]
sol = Solution()
merged1 = sol.mergeKLists(lists1)
print("合并后链表1:", list_to_array(merged1)) # 输出:[1,1,2,3,4,4,5,6]lists2 = [array_to_list([-1,5,11]), array_to_list([6,10])]
merged2 = sol.mergeKLists(lists2)
print("合并后链表2:", list_to_array(merged2)) # 输出:[-1,5,6,10,11]
代码解析
- 时间复杂度:每个节点入堆和出堆各一次,堆操作时间为
O(log k)(k为链表个数),总复杂度O(N log k)(N为所有节点总数),远优于“两两合并”的O(N k); - 空间复杂度:堆中最多存储
k个节点,空间复杂度O(k),虚拟头节点和指针仅用常数空间; - 关键细节:“链表索引”的加入解决了“节点值相同导致比较报错”的问题,确保堆操作正常执行。
题目二:戳气球(LeetCode 312,困难)
题目分析
有 n 个气球,编号为 0 到 n-1,每个气球上标有数字 nums[i]。戳破第 i 个气球可获得 nums[i-1] * nums[i] * nums[i+1] 个硬币(i-1/i+1 超出边界时视为 1)。求戳破所有气球能获得的最大硬币数。例如:
- 输入
nums = [3,1,5,8],输出167(戳破顺序1→5→3→8,总硬币3×1×5 + 3×5×8 + 1×3×8 + 1×8×1 = 15+120+24+8=167); - 输入
nums = [1,5],输出10(戳破顺序1→5,总硬币1×1×5 + 1×5×1=10); - 输入
nums = [2],输出2(戳破后获得1×2×1=2)。
解题思路(区间动态规划+逆向思维)
核心是逆向思维:将“戳破气球”转化为“添加气球”,用区间 DP 记录“区间内戳破所有气球的最大硬币数”,具体步骤如下:
-
问题转化与数组预处理
- 逆向思维:若将“戳破气球
i”改为“在区间[left, right]中最后一个添加气球i”,则此时nums[left]和nums[right]是i的左右邻居(区间内其他气球已添加),硬币数可通过子区间结果计算; - 预处理:在
nums首尾各加1(模拟边界虚拟气球),新数组长度为n+2,原数组元素对应索引1~n。
- 逆向思维:若将“戳破气球
-
DP 状态定义
定义dp[left][right]:戳破区间(left, right)内所有气球(不包含left和right)能获得的最大硬币数。 -
状态转移逻辑
按区间长度len = right - left从小到大遍历(len ≥ 2,确保区间内至少有一个气球):- 枚举区间内最后一个添加的气球
mid(left < mid < right); - 此时获得的硬币数 = 左区间硬币(
dp[left][mid]) + 右区间硬币(dp[mid][right]) + 最后添加mid的硬币(nums[left] * nums[mid] * nums[right]); - 状态转移方程:
dp[left][right] = max(dp[left][right], 左区间硬币 + 右区间硬币 + 新增硬币)。
- 枚举区间内最后一个添加的气球
-
结果输出
最终结果为dp[0][n+1](戳破原数组所有气球,左右边界为虚拟气球1)。
示例代码
def maxCoins(nums) -> int:n = len(nums)# 首尾加1,模拟边界虚拟气球nums = [1] + nums + [1]# dp[left][right]:戳破(left, right)内所有气球的最大硬币数dp = [[0] * (n + 2) for _ in range(n + 2)]# 按区间长度遍历(len至少为2,确保区间内有气球)for length in range(2, n + 2):for left in range(n + 2 - length):right = left + length# 枚举最后一个添加的气球midfor mid in range(left + 1, right):current = dp[left][mid] + dp[mid][right] + nums[left] * nums[mid] * nums[right]if current > dp[left][right]:dp[left][right] = currentreturn dp[0][n + 1]# 测试示例
print("最大硬币数1:", maxCoins([3,1,5,8])) # 输出:167
print("最大硬币数2:", maxCoins([1,5])) # 输出:10
print("最大硬币数3:", maxCoins([2])) # 输出:2
代码解析
- 逆向思维的价值:解决了“戳破气球后邻居变化导致状态难以追踪”的问题,将依赖未知状态的问题转化为依赖已知子区间的问题;
- 时间复杂度:三重循环(区间长度、左边界、mid),复杂度
O(n³),在n ≤ 500约束下可高效运行; - 空间复杂度:
O(n²),用于存储 DP 表,无额外冗余空间。
题目三:编辑距离(LeetCode 72,困难)
题目分析
给你两个单词 word1 和 word2,返回将 word1 转换成 word2 所需的最少操作数。允许的操作有:插入一个字符、删除一个字符、替换一个字符。例如:
- 输入
word1 = "horse", word2 = "ros",输出3(horse → rorse → rose → ros); - 输入
word1 = "intention", word2 = "execution",输出5; - 输入
word1 = "", word2 = "",输出0; - 输入
word1 = "a", word2 = "",输出1。
解题思路(动态规划+状态细分)
核心是用 DP 记录“word1 前 i 个字符转 word2 前 j 个字符的最少操作数”,针对三种操作细分状态转移,具体步骤如下:
-
DP 状态定义
定义dp[i][j]:将word1[0..i-1]转换成word2[0..j-1]所需的最少操作数。 -
边界初始化
dp[i][0] = i:将word1前i个字符全部删除,得到空字符串word2;dp[0][j] = j:在空字符串word1中插入j个字符,得到word2前j个字符。
-
状态转移逻辑
分两种情况讨论word1[i-1]与word2[j-1]:- 若
word1[i-1] == word2[j-1]:无需操作,dp[i][j] = dp[i-1][j-1]; - 若
word1[i-1] != word2[j-1]:取三种操作的最小值 + 1:- 替换:
dp[i-1][j-1] + 1(替换word1[i-1]为word2[j-1]); - 删除:
dp[i-1][j] + 1(删除word1[i-1]); - 插入:
dp[i][j-1] + 1(在word1[i-1]后插入word2[j-1])。
- 替换:
- 若
示例代码(空间优化版)
def minDistance(word1: str, word2: str) -> int:m, n = len(word1), len(word2)# 优化为一维数组:dp[j] 表示当前行(word1前i个字符)转word2前j个字符的最少操作数dp = [0] * (n + 1)# 初始化第一行(word1为空,需插入j个字符)for j in range(n + 1):dp[j] = jfor i in range(1, m + 1):prev = dp[0] # 保存dp[i-1][0]的值(删除i个字符)dp[0] = i # 更新dp[i][0]for j in range(1, n + 1):temp = dp[j] # 保存dp[i-1][j]的值if word1[i-1] == word2[j-1]:dp[j] = prev # 无需操作,继承dp[i-1][j-1]else:# 取替换、删除、插入的最小值 + 1dp[j] = min(prev, dp[j], dp[j-1]) + 1prev = temp # 更新prev为下一轮的dp[i-1][j-1]return dp[n]# 测试示例
print("编辑距离1:", minDistance("horse", "ros")) # 输出:3
print("编辑距离2:", minDistance("intention", "execution")) # 输出:5
print("编辑距离3:", minDistance("", "")) # 输出:0
print("编辑距离4:", minDistance("a", "")) # 输出:1
代码解析
- 空间优化:利用“
dp[i][j]仅依赖dp[i-1][j-1]、dp[i-1][j]、dp[i][j-1]”的特性,将二维数组优化为一维数组,空间复杂度从O(mn)降至O(n); - 时间复杂度:双重循环遍历所有状态,复杂度
O(mn),是该问题的最优时间复杂度; - 实际应用:编辑距离是字符串相似度计算的核心算法,广泛用于拼写检查、DNA序列比对等场景。
✨ 本次分享的3道题均为LeetCode中“思维深度高、工程价值强”的困难题,覆盖“优先队列(合并链表)”“区间DP+逆向思维(戳气球)”“动态规划(编辑距离)”三大核心方向。它们的共同突破点在于:通过问题转化或数据结构选择,将复杂问题拆解为可分步求解的子问题,尤其适合提升“算法建模与优化”的能力。
若你对某题的拓展场景(如合并K个降序链表、多维度戳气球、带权重的编辑距离)或其他经典困难题有需求,随时告诉我!😊
🏠 更多算法解析欢迎到CSDN主页交流:山顶风景独好
