【Leetcode】随笔 - 教程

你好呀!我是 山顶风景独好
欢迎踏入我的博客世界,能与您在此邂逅,真是缘分使然!
愿您在此停留的每一刻,都沐浴在轻松愉悦的氛围中。
这里不仅有丰富的知识和趣味横生的内容等您来探索,更是一个自由交流的平台,期待您留下独特的思考与见解。
让我们一起踏上这段探索与成长的旅程,携手挖掘更多可能,共同进步!✨

题目一:正则表达式匹配(LeetCode 10,困难)

题目分析

给你一个字符串 s 和一个字符规律 p,实现支持 '.''*' 的正则表达式匹配。规则如下:

  • '.' 匹配任意单个字符;
  • '*' 匹配零个或多个前面的那一个元素。
    匹配需覆盖整个字符串 s,而非部分匹配。例如:输入 s = "aa", p = "a",输出 false;输入 s = "aa", p = "a*",输出 true;输入 s = "ab", p = ".*",输出 true

解题思路

采用动态规划法,核心是定义 dp[i][j] 表示“s 的前 i 个字符与 p 的前 j 个字符是否匹配”,通过分析 p[j-1] 的类型推导状态转移方程:

  1. 状态定义dp[i][j] 对应子问题“s[0..i-1]p[0..j-1] 是否完全匹配”(索引偏移处理空字符串边界)。
  2. 初始化边界
    • dp[0][0] = true(空字符串与空模式匹配);
    • dp[i][0] = false(非空字符串与空模式不匹配);
    • dp[0][j]:仅当 p[j-1] == '*'dp[0][j-2] = true 时为 true* 匹配零个前序字符)。
  3. 状态转移逻辑
    • p[j-1] 是普通字符或 '.':若 s[i-1] == p[j-1]p[j-1] == '.',则 dp[i][j] = dp[i-1][j-1];否则为 false
    • p[j-1] == '*'
      • 子情况1(* 匹配零个前序字符):dp[i][j] = dp[i][j-2]
      • 子情况2(* 匹配一个或多个前序字符):若 s[i-1] == p[j-2]p[j-2] == '.',则 dp[i][j] = dp[i-1][j]
      • 最终 dp[i][j] = 子情况1 或 子情况2
  4. 结果获取dp[len(s)][len(p)] 即为匹配结果。

示例代码

def isMatch(s: str, p: str) ->
bool:
m, n = len(s), len(p)
dp = [[False] * (n + 1) for _ in range(m + 1)]
dp[0][0] = True # 空字符串匹配空模式
# 初始化空字符串与非空模式的匹配(处理*)
for j in range(1, n + 1):
if p[j-1] == '*':
dp[0][j] = dp[0][j-2]
# 填充dp数组
for i in range(1, m + 1):
for j in range(1, n + 1):
if p[j-1] != '*':
# 普通字符或.,直接匹配
if s[i-1] == p[j-1] or p[j-1] == '.':
dp[i][j] = dp[i-1][j-1]
else:
# *匹配零个前序字符
case1 = dp[i][j-2]
# *匹配一个或多个前序字符
case2 = False
if s[i-1] == p[j-2] or p[j-2] == '.':
case2 = dp[i-1][j]
dp[i][j] = case1 or case2
return dp[m][n]
# 测试示例
s1, p1 = "aa", "a"
print(f"{s1
}{p1
} 匹配:", isMatch(s1, p1)) # 输出:False
s2, p2 = "aa", "a*"
print(f"{s2
}{p2
} 匹配:", isMatch(s2, p2)) # 输出:True
s3, p3 = "ab", ".*"
print(f"{s3
}{p3
} 匹配:", isMatch(s3, p3)) # 输出:True

代码解析

  • 索引偏移的意义dp[i][j] 对应“前 i/j 个字符”,避免空字符串处理时的索引越界,同时自然覆盖边界场景。
  • * 的灵活处理:通过两种子情况覆盖 * 的所有匹配可能性,无需回溯,确保动态规划的无后效性。
  • 复杂度分析:时间复杂度 O(mn),空间复杂度 O(mn)(可优化为 O(n) 滚动数组)。

题目二:天际线问题(LeetCode 218,困难)

题目分析

城市的天际线是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓。给你所有建筑物的位置和高度,请返回由这些建筑物形成的天际线。每个建筑物的描述由数组 buildings 表示,其中 buildings[i] = [lefti, righti, heighti]lefti 是建筑物左边缘的 x 坐标,righti 是右边缘的 x 坐标,heighti 是建筑物的高度。天际线由“关键点”组成,关键点是水平线段的左端点,格式为 [[x1,y1],[x2,y2],...],关键点按 x 坐标升序排列。例如:输入 buildings = [[2,9,10],[3,7,15],[5,12,12],[15,20,10],[19,24,8]],输出 [[2,10],[3,15],[7,12],[12,0],[15,10],[20,8],[24,0]]

解题思路

采用扫描线+优先队列,核心是用扫描线遍历所有建筑物的左右边缘,通过优先队列维护当前最高高度,动态记录天际线的关键点:

  1. 事件点生成:将每个建筑物拆分为两个事件点:(lefti, -heighti)(左边缘,负高度标记为“加入”事件)和 (righti, heighti)(右边缘,正高度标记为“移除”事件),按 x 坐标升序排序(x 相同则“加入”事件在前)。
  2. 优先队列维护:使用最大堆(通过存入负高度模拟)维护当前所有建筑物的高度,初始加入 0(地面高度)。
  3. 扫描事件点
    • 遍历每个事件点 (x, h)
      • h < 0(加入事件):将 -h 加入堆;
      • h > 0(移除事件):标记 h 为待删除(延迟删除,避免直接从堆中删除元素的 O(n) 时间);
    • 清理堆:将堆顶已标记为待删除的高度弹出;
    • 若当前堆顶高度与上一个关键点的高度不同,说明出现新的关键点,加入结果列表。
  4. 返回结果:结果列表即为天际线的关键点。

示例代码

import heapq
def getSkyline(buildings):
# 生成事件点:左边缘(-height)、右边缘(height),按x升序排序
events = []
for left, right, height in buildings:
events.append((left, -height)) # 左边缘:加入事件
events.append((right, height)) # 右边缘:移除事件
# 排序:x相同则加入事件在前(保证先处理高建筑的左边缘)
events.sort(key=lambda x: (x[0], x[1]))
heap = [0] # 最大堆(存负高度),初始地面高度0
removed = dict() # 记录待删除的高度及次数
prev_height = 0 # 上一个关键点的高度
result = []
for x, h in events:
if h <
0:
# 加入事件:将高度加入堆
heapq.heappush(heap, h)
else:
# 移除事件:标记高度待删除
removed[h] = removed.get(h, 0) + 1
# 清理堆顶:弹出已标记待删除的高度
while heap:
current = -heap[0]
if current in removed and removed[current] >
0:
removed[current] -= 1
heapq.heappop(heap)
else:
break
# 当前最高高度
current_height = -heap[0]
# 高度变化,生成新关键点
if current_height != prev_height:
result.append([x, current_height])
prev_height = current_height
return result
# 测试示例
buildings = [[2,9,10],[3,7,15],[5,12,12],[15,20,10],[19,24,8]]
print("天际线关键点:", getSkyline(buildings))
# 输出:[[2,10],[3,15],[7,12],[12,0],[15,10],[20,8],[24,0]]

代码解析

  • 事件点排序:x 相同的事件中“加入”在前,确保高建筑物的左边缘优先处理,避免出现错误的高度断层。
  • 延迟删除:通过 removed 字典标记待删除高度,堆仅在顶部元素需删除时清理,保证堆操作的高效性(近似 O(log n) per 事件)。
  • 复杂度分析:时间复杂度 O(n log n)(事件排序 O(n log n),每个事件堆操作 O(log n));空间复杂度 O(n)(事件点和堆的存储)。

题目三:最长连续序列(LeetCode 128,困难)

题目分析

给定一个未排序的整数数组 nums,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。请设计并实现时间复杂度为 O(n) 的算法解决此问题。例如:输入 nums = [100,4,200,1,3,2],输出 4(最长序列为 [1,2,3,4]);输入 nums = [0,3,7,2,5,8,4,6,0,1],输出 9。

解题思路

采用哈希表,核心是通过哈希表存储数组元素,避免重复处理,并仅对“序列起点”(即 num-1 不在哈希表中)进行连续长度计算:

  1. 哈希表存储:将数组元素存入集合 num_set,实现 O(1) 时间的存在性判断。
  2. 遍历数组元素
    • 对每个元素 num,若 num-1 不在 num_set 中(说明 num 是某个连续序列的起点):
      • 初始化当前序列长度 current_len = 1,当前数字 current_num = num
      • 循环判断 current_num + 1 是否在 num_set 中,若是则 current_len += 1current_num += 1
      • 更新最长序列长度 max_len
  3. 返回结果max_len 即为最长连续序列的长度。

示例代码

def longestConsecutive(nums) ->
int:
num_set = set(nums)
max_len = 0
for num in num_set:
# 仅对序列起点(num-1不在集合中)进行计算
if num - 1 not in num_set:
current_num = num
current_len = 1
# 延伸序列
while current_num + 1 in num_set:
current_num += 1
current_len += 1
# 更新最大长度
max_len = max(max_len, current_len)
return max_len
# 测试示例
nums1 = [100,4,200,1,3,2]
print("最长连续序列长度1:", longestConsecutive(nums1)) # 输出:4
nums2 = [0,3,7,2,5,8,4,6,0,1]
print("最长连续序列长度2:", longestConsecutive(nums2)) # 输出:9
nums3 = []
print("最长连续序列长度3:", longestConsecutive(nums3)) # 输出:0

代码解析

  • 避免重复处理:仅对序列起点计算长度,每个元素最多被访问两次(一次作为起点,一次被后续元素延伸时访问),确保 O(n) 时间复杂度。
  • 哈希表优势:O(1) 的存在性判断是实现高效算法的关键,替代了排序的 O(n log n) 时间。
  • 复杂度分析:时间复杂度 O(n),空间复杂度 O(n)(哈希表存储数组元素),是该题的最优解法。

✨ 本次分享的3道题覆盖“动态规划(正则匹配)”“扫描线+堆(天际线)”“哈希表(最长连续序列)”,核心是结合问题特性创新算法组合。若对某题的拓展场景(如多模式正则匹配、三维天际线)或其他困难题有需求,随时告诉我!
更多算法解析欢迎到CSDN主页交流:山顶风景独好

posted @ 2025-09-10 17:14  wzzkaifa  阅读(12)  评论(0)    收藏  举报