Posted in

LeetCode Top 100 Go语言题解(含完整代码模板)

第一章:LeetCode Top 100 概述与学习路径

LeetCode Top 100 是全球技术公司面试中高频出现的经典算法题集合,被广泛认为是准备数据结构与算法面试的黄金标准。这些题目覆盖了数组、链表、树、动态规划、图论等核心主题,难度分布合理,适合从初级到高级开发者系统性提升编码与问题拆解能力。

学习价值与适用人群

该题库不仅帮助巩固基础算法思想,还能训练在限定时间内写出高效、可读代码的能力。适用于准备技术面试的应届生、转行者,以及希望提升系统设计与逻辑思维的在职工程师。

高效学习路径建议

遵循“分类刷题 + 复盘总结”的模式效果最佳:

  • 第一阶段:按类型集中突破
    将题目分为数组/字符串、链表、二叉树、回溯、动态规划等类别,逐个攻克。
  • 第二阶段:限时模拟
    使用 LeetCode 的计时功能,每题控制在25分钟内完成,模拟真实面试压力。
  • 第三阶段:反复回顾错题
    建立个人题解笔记,记录易错点与优化思路。

常见题型分布示例:

类型 题目数量 典型代表
数组/双指针 20 Two Sum, Container With Most Water
18 Invert Binary Tree, Maximum Depth of Binary Tree
动态规划 15 Climbing Stairs, House Robber

工具与实践建议

使用以下命令初始化本地刷题环境,便于保存与测试代码:

# 创建刷题目录
mkdir leetcode-top100 && cd leetcode-top100

# 为每道题创建独立文件(以Two Sum为例)
touch 001_two_sum.py

# 在文件中编写带注释的解法

坚持每日一题,结合提交后的运行结果与最优解对比,逐步形成自己的解题直觉与模板库,是掌握 Top 100 的关键。

第二章:数组与字符串高频题解析

2.1 数组遍历与双指针技巧理论基础

数组遍历是算法设计中最基础的操作之一。传统的单指针遍历通过线性扫描访问每个元素,时间复杂度为 O(n)。但在某些场景下,使用双指针技巧可以显著提升效率或简化逻辑。

双指针的基本模式

双指针法通常分为两类:

  • 同向指针:两个指针从同一端出发,常用于滑动窗口问题;
  • 相向指针:指针从两端向中间移动,适用于有序数组的两数之和等问题。
# 相向双指针示例:在有序数组中查找两数之和等于目标值
def two_sum_sorted(arr, target):
    left, right = 0, len(arr) - 1
    while left < right:
        current_sum = arr[left] + arr[right]
        if current_sum == target:
            return [left, right]
        elif current_sum < target:
            left += 1  # 左指针右移增大和
        else:
            right -= 1 # 右指针左移减小和

该代码利用数组有序特性,每次比较后都能安全地移动一个指针,避免暴力枚举,将时间复杂度优化至 O(n)。

空间与时间权衡

方法 时间复杂度 空间复杂度 适用场景
暴力遍历 O(n²) O(1) 无序数据,小规模输入
哈希表辅助 O(n) O(n) 允许额外空间
双指针 O(n) O(1) 数组有序

mermaid 流程图可表示相向双指针的决策过程:

graph TD
    A[初始化 left=0, right=n-1] --> B{left < right?}
    B -->|否| C[结束]
    B -->|是| D[计算 arr[left] + arr[right]]
    D --> E{等于目标?}
    E -->|是| F[返回索引]
    E -->|小于| G[left++]
    E -->|大于| H[right--]
    G --> B
    H --> B

2.2 实战:两数之和与三数之和优化方案

两数之和:哈希表加速查找

使用哈希表将时间复杂度从 $O(n^2)$ 优化至 $O(n)$。遍历数组时,检查目标差值是否已在表中。

def two_sum(nums, target):
    seen = {}
    for i, num in enumerate(nums):
        diff = target - num
        if diff in seen:
            return [seen[diff], i]
        seen[num] = i
  • seen 存储已遍历的数值与索引;
  • 每次计算 target - num,若存在即返回索引对。

三数之和:排序 + 双指针

先排序,固定一个数,用双指针在剩余区间找互补对,避免重复组合。

方法 时间复杂度 空间复杂度
暴力枚举 O(n³) O(1)
排序+双指针 O(n²) O(1)

优化路径对比

graph TD
    A[两数之和暴力解] --> B[引入哈希表]
    C[三数之和暴力解] --> D[排序+双指针]
    B --> E[线性时间求解]
    D --> F[减少一层循环]

2.3 字符串操作核心方法与性能分析

字符串是编程中最常用的数据类型之一,其操作效率直接影响程序整体性能。掌握核心方法并理解其底层机制,是优化代码的关键。

常见字符串操作方法对比

Python 提供了丰富的字符串方法,其中 join()format()、f-string 和 + 拼接在使用频率和性能上差异显著。

方法 示例 时间复杂度 适用场景
+ 拼接 s = a + b + c O(n²) 少量拼接
join() ''.join([a,b,c]) O(n) 多字符串拼接
f-string f"{a}{b}{c}" O(1) per var 格式化输出

性能关键:避免重复创建对象

由于字符串不可变,每次 + 操作都会创建新对象。大量循环中应避免使用:

# 低效:O(n²)
result = ""
for item in data:
    result += str(item)

# 高效:O(n)
result = ''.join(str(item) for item in data)

join() 将所有元素一次性合并,减少内存分配次数,显著提升性能。

2.4 实战:最长无重复子串与回文串判断

滑动窗口解决最长无重复子串

使用滑动窗口算法可高效求解最长无重复字符子串。维护一个哈希表记录字符最近出现的位置,动态调整窗口左边界。

def lengthOfLongestSubstring(s):
    char_index = {}
    left = 0
    max_len = 0
    for right in range(len(s)):
        if s[right] in char_index and char_index[s[right]] >= left:
            left = char_index[s[right]] + 1  # 缩小窗口
        char_index[s[right]] = right
        max_len = max(max_len, right - left + 1)
    return max_len

char_index 存储字符最新索引,left 标记窗口起始位置。当字符重复且在窗口内时,移动 left 至上次出现位置的下一位。

中心扩展法判断回文串

回文串可通过中心扩展法验证,枚举每个可能的中心点并向外扩展。

中心类型 示例(”aba”) 扩展方向
单字符中心 ‘b’为中心 向左右对称扩展
双字符中心 ‘ab’间缝隙 处理偶数长度情况

该方法时间复杂度为 O(n²),适合短字符串场景。

2.5 边界处理与算法鲁棒性设计实践

在实际系统开发中,边界条件往往是引发异常的根源。良好的鲁棒性设计需从输入校验、异常兜底和容错机制三方面入手。

输入校验与防御式编程

对所有外部输入进行类型、范围和格式验证,是防止非法数据破坏逻辑的第一道防线:

def divide(a: float, b: float) -> float:
    if abs(b) < 1e-10:
        raise ValueError("除数不能为零")
    return a / b

该函数通过阈值判断避免浮点数精度导致的“近零”误判,1e-10作为容差边界,提升数值稳定性。

异常传播与降级策略

采用分层异常处理机制,关键操作应具备自动降级能力。例如缓存失效时回退至数据库查询。

场景 处理方式 降级方案
网络超时 重试3次 返回本地缓存
数据格式错误 捕获并记录日志 使用默认配置

容错流程设计

使用流程图明确异常流转路径:

graph TD
    A[接收请求] --> B{参数合法?}
    B -->|是| C[执行核心逻辑]
    B -->|否| D[返回400错误]
    C --> E{成功?}
    E -->|是| F[返回结果]
    E -->|否| G[触发降级]
    G --> H[返回默认值]

第三章:链表与树的经典题目剖析

3.1 链表反转与环检测算法原理

链表反转是基础但关键的操作,常用于优化遍历路径。通过迭代方式,逐个调整节点的指针方向:

def reverse_list(head):
    prev = None
    curr = head
    while curr:
        next_temp = curr.next  # 临时保存下一个节点
        curr.next = prev       # 反转当前节点指针
        prev = curr            # 移动prev和curr
        curr = next_temp
    return prev  # 新的头节点

该算法时间复杂度为 O(n),空间复杂度 O(1),核心在于避免指针丢失。

环检测:Floyd判圈算法

使用快慢双指针检测链表中是否存在环:

def has_cycle(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast:
            return True
    return False

slow 每步走一格,fast 走两格,若两者相遇则存在环。该方法高效且无需额外存储。

方法 时间复杂度 空间复杂度 是否可定位环入口
哈希表法 O(n) O(n)
Floyd算法 O(n) O(1) 是(扩展实现)

mermaid 流程图如下:

graph TD
    A[开始] --> B{head为空?}
    B -- 是 --> C[返回None]
    B -- 否 --> D[prev = None, curr = head]
    D --> E{curr不为空}
    E -- 是 --> F[next_temp = curr.next]
    F --> G[curr.next = prev]
    G --> H[prev = curr]
    H --> I[curr = next_temp]
    I --> E
    E -- 否 --> J[返回prev]

3.2 实战:合并两个有序链表与LRU缓存实现

合并两个有序链表

在链表操作中,合并两个升序链表是典型双指针应用场景。通过维护一个虚拟头节点,可简化边界处理:

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def mergeTwoLists(l1: ListNode, l2: ListNode) -> ListNode:
    dummy = ListNode()
    current = dummy
    while l1 and l2:
        if l1.val <= l2.val:
            current.next = l1
            l1 = l1.next
        else:
            current.next = l2
            l2 = l2.next
        current = current.next
    current.next = l1 or l2
    return dummy.next

该算法时间复杂度为 O(m+n),空间复杂度 O(1)。dummy 节点避免了对首节点的特殊判断,current 指针驱动归并过程。

LRU缓存机制设计

LRU(Least Recently Used)需满足:

  • 快速查找 → 哈希表
  • 维护访问顺序 → 双向链表
操作 时间复杂度 数据结构支持
get O(1) 哈希表
put O(1) 哈希表+双向链表
graph TD
    A[Put Operation] --> B{Key Exists?}
    B -->|Yes| C[Update Value & Move to Head]
    B -->|No| D{Reach Capacity?}
    D -->|Yes| E[Remove Tail Node]
    D -->|No| F[Create New Node]
    F --> G[Add to Head]

3.3 二叉树递归与迭代遍历统一框架

统一思想:栈与状态标记

无论是先序、中序还是后序遍历,递归的本质是函数调用栈的自动管理。而迭代实现的关键在于手动模拟栈行为,并通过状态标记控制节点访问顺序。

核心策略:节点与状态绑定

使用栈存储元组 (node, status),其中 status 表示该节点是否已被“处理过”。例如:

  • status = False:未处理,需将其子节点入栈;
  • status = True:已展开,可输出值。
def inorderTraversal(root):
    stack = [(root, False)]
    result = []
    while stack:
        node, visited = stack.pop()
        if not node:
            continue
        if visited:
            result.append(node.val)
        else:
            # 按逆序入栈:右 → 自身(标记为True) → 左
            stack.append((node.right, False))
            stack.append((node, True))
            stack.append((node.left, False))
    return result

逻辑分析:通过显式控制访问顺序,将三种遍历方式统一为“延迟处理”模式。改变入栈顺序即可切换遍历类型,具备高度可扩展性。

遍历类型 入栈顺序(右→根→左)
先序 根 → 左 → 右
中序 右 → 根 → 左
后序 根 → 右 → 左

流程图示意

graph TD
    A[开始] --> B{栈非空?}
    B -->|否| C[结束]
    B -->|是| D[弹出节点与状态]
    D --> E{是否已访问?}
    E -->|是| F[加入结果]
    E -->|否| G[右子入栈(未访问)]
    G --> H[自身入栈(已访问)]
    H --> I[左子入栈(未访问)]
    F --> B
    I --> B

第四章:动态规划与回溯算法精讲

4.1 动态规划状态定义与转移方程构建

动态规划的核心在于状态的合理定义转移方程的准确构建。恰当的状态设计能将复杂问题拆解为可递推的子问题。

状态定义的关键原则

  • 无后效性:当前状态仅依赖于之前状态,不受未来决策影响。
  • 完备性:状态需涵盖所有可能情形,确保覆盖全部解空间。

转移方程构建步骤

  1. 分析问题的最优子结构
  2. 明确状态变量的物理意义
  3. 推导状态间的依赖关系

以经典的0-1背包问题为例:

# dp[i][w] 表示前i个物品在容量w下的最大价值
dp = [[0] * (W + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
    for w in range(W + 1):
        if weights[i-1] <= w:
            dp[i][w] = max(dp[i-1][w], dp[i-1][w-weights[i-1]] + values[i-1])
        else:
            dp[i][w] = dp[i-1][w]

该代码中,dp[i][w] 的状态转移体现了“选或不选”第 i 个物品的决策过程。若物品重量超过当前容量,则继承前一状态;否则取两种选择的最大值,确保每一步都保留最优解路径。

4.2 实战:最大子数组和与背包问题变种

动态规划在实际问题中展现出极强的建模能力,本节通过两个经典问题的变种揭示其灵活应用。

最大子数组和的扩展

当要求子数组和最大且长度至少为k时,需调整状态定义。维护前缀和并限制窗口大小:

def maxSubArraySumWithLengthK(nums, k):
    n = len(nums)
    pre_sum = [0] * (n + 1)
    for i in range(n):
        pre_sum[i+1] = pre_sum[i] + nums[i]

    # 初始化前k个元素的和
    max_sum = pre_sum[k] - pre_sum[0]
    min_prefix = pre_sum[0]  # 可用于更新的最小前缀和

    for i in range(k, n):
        # 更新可选的最小前缀(至少距离当前k)
        min_prefix = min(min_prefix, pre_sum[i - k + 1])
        current_sum = pre_sum[i + 1] - min_prefix
        max_sum = max(max_sum, current_sum)
    return max_sum

该算法通过维护滑动窗口内的最小前缀和,确保子数组长度合规,时间复杂度O(n)。

有容量限制的背包变形

考虑物品可重复使用但总重量不超过W,且价值最大化:

物品 重量 价值
A 2 3
B 3 5
C 4 6

使用一维DP数组,dp[w]表示重量w下的最大价值,转移方程: dp[w] = max(dp[w], dp[w-weight[i]] + value[i])

4.3 回溯算法框架与剪枝优化策略

回溯算法是一种系统性搜索解空间的递归技术,常用于组合、排列、子集等问题。其核心思想是“尝试所有可能路径,遇到不满足条件的情况立即回退”。

基本框架

def backtrack(path, choices, result):
    if 满足结束条件:
        result.append(path[:])  # 保存副本
        return
    for choice in choices:
        if 剪枝条件: continue   # 提前终止无效分支
        path.append(choice)     # 做选择
        backtrack(path, choices, result)
        path.pop()              # 撤销选择

上述模板中,path 记录当前路径,choices 表示可选列表,result 收集合法解。关键在于“做选择”与“撤销选择”之间的对称操作,确保状态正确回滚。

剪枝优化策略

有效的剪枝能显著降低时间复杂度:

  • 约束剪枝:在进入递归前检查是否满足约束条件;
  • 限界剪枝:预判后续路径无法产生更优解时提前终止;
  • 去重剪枝:通过排序避免重复枚举相同组合。
剪枝类型 触发时机 效果
约束剪枝 递归前判断 减少无效调用
限界剪枝 搜索过程中 提升最优解效率
去重剪枝 遍历选择时 避免重复解

执行流程可视化

graph TD
    A[开始] --> B{满足结束条件?}
    B -->|是| C[保存结果]
    B -->|否| D[遍历可选列表]
    D --> E{需剪枝?}
    E -->|是| F[跳过该分支]
    E -->|否| G[做选择]
    G --> H[递归下一层]
    H --> I[撤销选择]
    F --> J[继续下一选项]
    I --> J
    J --> B

4.4 实战:全排列与N皇后问题Go实现

全排列的回溯实现

在算法实践中,全排列是回溯法的经典应用。通过递归交换元素位置,可生成所有可能的排列组合。

func permute(nums []int) [][]int {
    var result [][]int
    backtrack(nums, 0, &result)
    return result
}

func backtrack(nums []int, start int, result *[][]int) {
    if start == len(nums) {
        temp := make([]int, len(nums))
        copy(temp, nums)
        *result = append(*result, temp)
        return
    }
    for i := start; i < len(nums); i++ {
        nums[start], nums[i] = nums[i], nums[start] // 交换
        backtrack(nums, start+1, result)           // 递归
        nums[start], nums[i] = nums[i], nums[start] // 回溯
    }
}

上述代码通过 backtrack 函数在每个位置尝试所有未使用的元素。参数 start 表示当前决策层,当其等于数组长度时记录结果。每次交换后递归进入下一层,返回后恢复原状以保证状态正确。

N皇后问题建模

N皇后问题要求在 N×N 棋盘上放置 N 个皇后,使其互不攻击。使用列、主对角线和副对角线集合剪枝,提升搜索效率。

func solveNQueens(n int) [][]string {
    var res [][]string
    board := make([][]byte, n)
    for i := range board {
        board[i] = make([]byte, n)
        for j := range board[i] {
            board[i][j] = '.'
        }
    }
    cols, diag1, diag2 := map[int]bool{}, map[int]bool{}, map[int]bool{}
    dfs(&res, board, cols, diag1, diag2, 0, n)
    return res
}

func dfs(res *[][]string, board [][]byte, cols, diag1, diag2 map[int]bool, row, n int) {
    if row == n {
        solution := make([]string, n)
        for i, row := range board {
            solution[i] = string(row)
        }
        *res = append(*res, solution)
        return
    }
    for col := 0; col < n; col++ {
        d1, d2 := row-col, row+col
        if cols[col] || diag1[d1] || diag2[d2] {
            continue
        }
        board[row][col] = 'Q'
        cols[col], diag1[d1], diag2[d2] = true, true, true
        dfs(res, board, cols, diag1, diag2, row+1, n)
        cols[col], diag1[d1], diag2[d2] = false, false, false
        board[row][col] = '.'
    }
}

该实现中,cols[col] 标记列占用,diag1[row−col]diag2[row+col] 分别标记两条对角线。每行尝试每一列,若位置安全则放置皇后并递归处理下一行,回溯时清除状态。

算法对比分析

问题 状态空间 剪枝策略 时间复杂度
全排列 排列树 无重复选择 O(n!)
N皇后 子集树 列与对角线约束 O(N^N)(最坏)

两者均采用回溯框架,但约束条件不同导致搜索结构差异。N皇后因剪枝更高效,实际运行远快于理论上限。

第五章:高效刷题策略与代码模板总结

在高强度的算法训练中,单纯刷题数量并不能保证能力提升。真正高效的刷题策略需要系统性方法与可复用的代码模板相结合。以下是一些经过验证的实战路径。

刷题阶段划分与目标设定

将刷题过程划分为三个阶段:基础巩固、专项突破、模拟冲刺。

  • 基础巩固阶段主攻数组、链表、栈、队列等数据结构的经典题目,每类完成15~20题;
  • 专项突破聚焦动态规划、回溯、图论等难点,按子类(如背包问题、区间DP)逐个击破;
  • 模拟冲刺则采用限时套题训练,模拟真实面试或竞赛环境。

建议使用如下表格追踪进度:

类别 目标题数 已完成 正确率
二分查找 20 18 83%
DFS/BFS 25 20 76%
动态规划 40 28 65%

高频题型代码模板库建设

建立个人专属的代码模板库能显著提升编码速度与准确性。例如,二分查找的统一模板可封装为:

def binary_search(nums, target):
    left, right = 0, len(nums) - 1
    while left <= right:
        mid = (left + right) // 2
        if nums[mid] == target:
            return mid
        elif nums[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return -1

对于回溯算法,通用结构如下:

def backtrack(path, options):
    if base_condition:
        result.append(path[:])
        return
    for opt in options:
        path.append(opt)
        backtrack(path, remaining_options)
        path.pop()

错题归因分析流程

每次提交失败后,应执行标准化归因流程。通过mermaid绘制决策流图辅助定位:

graph TD
    A[提交失败] --> B{是语法错误?}
    B -- 是 --> C[检查缩进/括号]
    B -- 否 --> D{是逻辑错误?}
    D -- 是 --> E[打印中间变量调试]
    D -- 否 --> F[检查边界条件]
    E --> G[修正递归终止条件]

时间优化技巧实战

在LeetCode 146. LRU Cache中,结合哈希表与双向链表实现O(1)操作。关键点在于维护key -> node映射,并抽象出move_to_headremove_node两个函数。实际编码时优先实现核心逻辑,再封装细节。

高频考点如滑动窗口,可套用如下结构:

left = 0
for right in range(n):
    update_window(state, right)
    while invalid(window):
        update_window(state, left, remove=True)
        left += 1
    update_result()

守护服务器稳定运行,自动化是喵的最爱。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注