第一章:Python算法面试TOP 10高频题全收录(1024程序员节权威发布)
在Python算法面试中,掌握核心题型是突破技术面的关键。以下精选的高频题目覆盖数组、字符串、动态规划等核心知识点,助你高效备战一线大厂技术考核。
两数之和
给定一个整数数组和一个目标值,找出数组中和为目标值的两个整数的索引。
# 使用哈希表存储已遍历元素及其索引
def two_sum(nums, target):
hashmap = {}
for i, num in enumerate(nums):
complement = target - num
if complement in hashmap:
return [hashmap[complement], i] # 返回匹配的两个索引
hashmap[num] = i # 将当前数值和索引存入哈希表
该解法时间复杂度为 O(n),优于暴力双重循环。
最长无重复子串
使用滑动窗口技术维护一个不含重复字符的最长子串区间。
def length_of_longest_substring(s):
char_set = set()
left = 0
max_len = 0
for right in range(len(s)):
while s[right] in char_set:
char_set.remove(s[left])
left += 1
char_set.add(s[right])
max_len = max(max_len, right - left + 1)
return max_len
常见高频题汇总
- 反转链表 —— 指针操作经典题
- 合并两个有序数组 —— 双指针技巧
- 爬楼梯 —— 动态规划入门
- 有效的括号 —— 栈结构应用
- 二叉树层序遍历 —— BFS模板题
| 题目名称 | 考察点 | 推荐解法 |
|---|
| 两数之和 | 哈希查找 | 字典映射 |
| 最大子数组和 | 动态规划 | Kadane算法 |
| 合并K个有序链表 | 堆/分治 | 优先队列 |
第二章:数组与字符串处理经典题型解析
2.1 两数之和问题的哈希优化解法
在解决“两数之和”问题时,暴力枚举的时间复杂度为 O(n²),效率较低。通过引入哈希表,可将查找配对元素的操作优化至 O(1) 平均时间。
核心思路
遍历数组过程中,对于每个元素 `nums[i]`,检查目标差值 `target - nums[i]` 是否已存在于哈希表中。若存在,则立即返回两数下标;否则将当前元素及其索引存入哈希表。
def two_sum(nums, target):
hash_map = {}
for i, num in enumerate(nums):
complement = target - num
if complement in hash_map:
return [hash_map[complement], i]
hash_map[num] = i
上述代码中,`hash_map` 存储已访问元素的值与索引映射。`complement` 表示当前所需配对值,查找操作平均耗时 O(1),整体时间复杂度降为 O(n),空间复杂度为 O(n)。
性能对比
- 暴力解法:时间 O(n²),空间 O(1)
- 哈希表解法:时间 O(n),空间 O(n)
2.2 最长无重复子串的滑动窗口实践
在处理字符串中的最长无重复子串问题时,滑动窗口是一种高效策略。通过维护一个动态窗口,确保其中元素不重复,并实时更新最大长度。
算法核心思路
使用双指针定义窗口边界,左指针控制起始位置,右指针扩展搜索范围。借助哈希表记录字符最新出现的位置,便于快速跳过重复字符。
代码实现
func lengthOfLongestSubstring(s string) int {
lastSeen := make(map[byte]int)
left, maxLen := 0, 0
for right := 0; right < len(s); right++ {
if idx, found := lastSeen[s[right]]; found && idx >= left {
left = idx + 1
}
lastSeen[s[right]] = right
if newLen := right - left + 1; newLen > maxLen {
maxLen = newLen
}
}
return maxLen
}
上述代码中,
lastSeen 存储每个字符最近索引,当发现重复且在当前窗口内时,移动
left 指针。时间复杂度为 O(n),空间复杂度为 O(min(m,n)),其中 m 是字符集大小。
2.3 旋转数组最小值的二分查找策略
在旋转排序数组中寻找最小值,可通过改进的二分查找算法高效实现。该策略利用数组局部有序特性,在每次比较中缩小搜索范围。
核心思路
当数组被旋转后,最小值必定位于无序的一侧。通过比较中间元素与右端元素的大小关系,可判断哪一侧存在断点。
- 若
nums[mid] > nums[right],说明左半部分有序,最小值在右半; - 若
nums[mid] < nums[right],说明右半部分有序,最小值在左半; - 相等时,右边界减一以排除重复干扰。
func findMin(nums []int) int {
left, right := 0, len(nums)-1
for left < right {
mid := left + (right-left)/2
if nums[mid] > nums[right] {
left = mid + 1 // 最小值在右半
} else if nums[mid] < nums[right] {
right = mid // 最小值在左半
} else {
right-- // 处理重复元素
}
}
return nums[left]
}
上述代码时间复杂度为 O(log n),最坏情况退化为 O(n)(大量重复元素)。算法关键在于正确维护搜索区间边界,确保不遗漏最小值位置。
2.4 字符串反转与回文判定的双指针技巧
在处理字符串相关算法问题时,双指针技巧是一种高效且直观的方法。通过维护两个分别指向字符串首尾的索引,可以在原地完成字符交换或对称性判断,显著降低空间复杂度。
字符串反转实现
使用双指针从两端向中心靠拢,逐个交换字符即可完成反转。
func reverseString(s []byte) {
left, right := 0, len(s)-1
for left < right {
s[left], s[right] = s[right], s[left]
left++
right--
}
}
该函数中,
left 从 0 开始,
right 指向末尾,循环条件为
left < right,每次交换后向中间移动一位,时间复杂度为 O(n/2),即 O(n)。
回文串判定逻辑
基于相同思想,判断字符串是否为回文只需在移动过程中比较对应字符是否相等。
- 初始化左指针为 0,右指针为长度减 1
- 当左右字符相等时继续收缩
- 一旦不匹配则返回 false
2.5 合并区间问题的排序+贪心实现
在处理区间合并问题时,核心思想是通过排序与贪心策略减少冗余重叠。首先将所有区间按左端点升序排列,便于后续线性扫描。
算法步骤
- 对原始区间数组按起始位置排序
- 初始化结果列表,加入第一个区间
- 遍历后续区间,若当前区间的左端点小于等于结果中最后一个区间的右端点,则发生重叠,合并为一个新区间(更新右端点为两者最大值)
- 否则,将当前区间直接加入结果列表
代码实现
func merge(intervals [][]int) [][]int {
sort.Slice(intervals, func(i, j int) bool {
return intervals[i][0] < intervals[j][0]
})
var result [][]int
result = append(result, intervals[0])
for i := 1; i < len(intervals); i++ {
last := &result[len(result)-1]
cur := intervals[i]
if cur[0] <= (*last)[1] {
(*last)[1] = max((*last)[1], cur[1]) // 扩展右边界
} else {
result = append(result, cur)
}
}
return result
}
上述代码中,排序确保了区间的有序性,贪心策略保证每次合并都尽可能延展当前连续覆盖范围,从而达到全局最优解。时间复杂度为 O(n log n),主要消耗在排序阶段。
第三章:链表操作核心考点突破
3.1 反转链表的递归与迭代实现对比
核心思路解析
反转链表是经典的数据结构操作,常见实现方式包括递归与迭代。两者目标一致:将原链表指针方向逆序,但实现逻辑和空间效率存在显著差异。
迭代实现
func reverseListIterative(head *ListNode) *ListNode {
var prev *ListNode
curr := head
for curr != nil {
next := curr.Next // 临时保存下一节点
curr.Next = prev // 反转当前指针
prev = curr // 移动 prev 前进
curr = next // 移动 curr 前进
}
return prev // 新头节点
}
该方法使用三个指针遍历链表,时间复杂度为 O(n),空间复杂度为 O(1),适合对内存敏感的场景。
递归实现
func reverseListRecursive(head *ListNode) *ListNode {
if head == nil || head.Next == nil {
return head
}
newHead := reverseListRecursive(head.Next)
head.Next.Next = head
head.Next = nil
return newHead
}
递归版本代码更简洁,利用调用栈回溯完成指针反转,时间复杂度 O(n),但空间复杂度为 O(n),存在栈溢出风险。
性能对比
| 实现方式 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|
| 迭代 | O(n) | O(1) | 长链表、内存受限环境 |
| 递归 | O(n) | O(n) | 教学演示、代码简洁性优先 |
3.2 环形链表检测的快慢指针原理剖析
在链表结构中,环的存在可能导致遍历无限循环。快慢指针法是一种高效的空间优化解法。
核心思想
使用两个指针:慢指针(slow)每次前进一步,快指针(fast)每次前进两步。若链表无环,快指针将率先到达尾部;若存在环,快指针最终会追上慢指针。
代码实现
func hasCycle(head *ListNode) bool {
if head == nil || head.Next == nil {
return false
}
slow, fast := head, head
for fast != nil && fast.Next != nil {
slow = slow.Next // 慢指针走一步
fast = fast.Next.Next // 快指针走两步
if slow == fast { // 相遇说明有环
return true
}
}
return false
}
上述代码通过双指针移动速度差捕捉环状结构。初始时两者均指向头节点,循环中快指针以两倍速前进。当二者相遇时,证明链表中存在环。时间复杂度为 O(n),空间复杂度仅为 O(1)。
3.3 合并两个有序链表的简洁代码模式
在处理链表问题时,合并两个有序链表是一个经典场景。通过双指针技术,可以高效实现有序合并。
核心思路
维护两个指针分别指向两个链表的当前节点,比较值大小,将较小的节点接入结果链表,并移动对应指针。
代码实现
func mergeTwoLists(l1, l2 *ListNode) *ListNode {
dummy := &ListNode{}
cur := dummy
for l1 != nil && l2 != nil {
if l1.Val <= l2.Val {
cur.Next = l1
l1 = l1.Next
} else {
cur.Next = l2
l2 = l2.Next
}
cur = cur.Next
}
if l1 != nil {
cur.Next = l1
} else {
cur.Next = l2
}
return dummy.Next
}
上述代码中,
dummy 节点简化了头节点的处理逻辑,循环部分完成主段比较,最后拼接剩余节点,整体时间复杂度为 O(m+n)。
第四章:树与图的遍历算法精讲
4.1 二叉树层序遍历与BFS模板应用
层序遍历是广度优先搜索(BFS)在二叉树上的典型应用,能够按层级从上到下、从左到右访问节点。该遍历方式依赖队列的先进先出特性,确保同一层的节点被依次处理。
核心算法模板
func levelOrder(root *TreeNode) [][]int {
result := [][]int{}
if root == nil {
return result
}
queue := []*TreeNode{root}
for len(queue) > 0 {
levelSize := len(queue)
currentLevel := []int{}
for i := 0; i < levelSize; i++ {
node := queue[0]
queue = queue[1:]
currentLevel = append(currentLevel, node.Val)
if node.Left != nil {
queue = append(queue, node.Left)
}
if node.Right != nil {
queue = append(queue, node.Right)
}
}
result = append(result, currentLevel)
}
return result
}
上述代码通过维护一个队列实现BFS,
levelSize 记录每层节点数,确保分层输出。内层循环处理当前层所有节点,并将子节点加入队列,外层循环推进层级。
应用场景对比
- 求二叉树高度:每完成一层遍历,高度加一
- 判断是否为完全二叉树:检查最后一层前是否无空缺
- 找每一层最右侧节点:每层最后一个入队元素即为目标
4.2 二叉树最大深度的DFS递归实现
算法核心思想
二叉树的最大深度是指从根节点到最远叶子节点的最长路径上的节点数。使用深度优先搜索(DFS)递归策略,可自然地遍历每条路径并返回最大值。
递归实现代码
func maxDepth(root *TreeNode) int {
if root == nil {
return 0
}
left := maxDepth(root.Left)
right := maxDepth(root.Right)
if left > right {
return left + 1
}
return right + 1
}
逻辑分析:当节点为空时,深度为0;否则分别计算左右子树的深度,取较大者加1作为当前深度。递归调用保证了对所有路径的完整探索。
时间与空间复杂度
- 时间复杂度:
O(n),其中 n 为节点总数,每个节点访问一次 - 空间复杂度:
O(h),h 为树的高度,由递归栈深度决定
4.3 图的拓扑排序与入度表实际运用
拓扑排序是针对有向无环图(DAG)的一种线性排序,使得对于每一条有向边 (u, v),节点 u 在排序中总是位于节点 v 之前。该算法广泛应用于任务调度、依赖解析等场景。
入度表与队列结合实现 Kahn 算法
核心思想是维护每个节点的入度,将入度为 0 的节点加入队列,依次处理并更新其邻接节点的入度。
func topologicalSort(graph map[int][]int, n int) []int {
indegree := make([]int, n)
for u, neighbors := range graph {
for _, v := range neighbors {
indegree[v]++
}
}
var queue []int
for i := 0; i < n; i++ {
if indegree[i] == 0 {
queue = append(queue, i)
}
}
var result []int
for len(queue) > 0 {
u := queue[0]
queue = queue[1:]
result = append(result, u)
for _, v := range graph[u] {
indegree[v]--
if indegree[v] == 0 {
queue = append(queue, v)
}
}
}
if len(result) != n {
return nil // 存在环,无法拓扑排序
}
return result
}
上述代码中,
indegree 数组记录每个节点的入度,初始将所有入度为 0 的节点入队。每次从队列取出节点,将其加入结果集,并遍历其邻接节点,减少对应入度;若某邻接节点入度降为 0,则加入队列。最终若结果集中节点数等于总节点数,则说明图无环,排序成功。
典型应用场景
- 课程学习顺序安排
- 软件构建依赖解析
- 数据同步机制中的执行次序控制
4.4 路径总和问题的回溯思路拆解
在二叉树中求解路径总和问题,核心在于遍历过程中维护当前路径和目标值的差值。通过深度优先搜索(DFS)结合回溯策略,可以系统性地探索所有从根到叶子的路径。
回溯的基本流程
- 从根节点开始递归遍历左右子树
- 将当前节点值加入路径累加和
- 若到达叶子节点且路径和等于目标值,记录该路径
- 递归返回时移除当前节点(回溯)
代码实现与解析
func hasPathSum(root *TreeNode, targetSum int) bool {
if root == nil {
return false
}
// 到达叶子节点,检查路径和是否匹配
if root.Left == nil && root.Right == nil {
return targetSum == root.Val
}
// 递归检查左、右子树,目标值减去当前节点值
return hasPathSum(root.Left, targetSum - root.Val) ||
hasPathSum(root.Right, targetSum - root.Val)
}
上述代码通过递归方式实现路径和判断。每次向下传递更新后的目标值(targetSum - root.Val),避免额外空间存储路径和。当且仅当叶子节点处剩余目标值等于节点值时,才返回 true。
第五章:高频算法题的思维跃迁与总结
从暴力到最优解的演化路径
许多高频题如“两数之和”看似简单,但其背后隐藏着思维跃迁的关键。例如,暴力枚举时间复杂度为 O(n²),而使用哈希表可优化至 O(n):
func twoSum(nums []int, target int) []int {
hash := make(map[int]int)
for i, num := range nums {
if j, found := hash[target-num]; found {
return []int{j, i}
}
hash[num] = i
}
return nil
}
模式识别与模板化求解
刷题的本质是识别问题模式。以下常见类型对应不同策略:
- 双指针:适用于有序数组中的和问题或区间压缩
- 滑动窗口:解决最长/最短子串问题,如“最小覆盖子串”
- DFS/BFS:图或树的遍历,典型如岛屿数量问题
- 动态规划:状态转移明确的问题,如“打家劫舍”系列
实际面试中的变体处理
企业常对经典题做变形。例如,在“环形链表”基础上要求返回入环节点,需结合数学推导。此时快慢指针相遇后,另设一指针从头出发,二者再次相遇即为入口。
| 原题 | 变体 | 应对策略 |
|---|
| 反转链表 | 反转第 m 到 n 个节点 | 定位 + 局部反转 + 连接 |
| 二分查找 | 在旋转排序数组中查找 | 判断有序侧,缩小范围 |