手撕代码
链表
定义
1 | class ListNode: |
哨兵节点
- 说明:虚拟头节点是一个哨兵节点,它位于真正头节点的前面。在处理链表头可能被删除、修改或需要统一操作时,使用虚拟头节点可以避免单独处理头节点的特殊情况,使代码更简洁统一。
- 题型:几乎所有涉及链表头部可能变化的题目(如删除节点、反转部分链表、合并链表等)。
1 | def dummy_example(head: ListNode) -> ListNode: |
双指针
快慢指针
- 说明:快指针每次走两步,慢指针每次走一步。利用速度差可以找到中点、判断环等。
- 题型:寻找链表中点(力扣 876. 链表的中间结点),判断链表是否有环(力扣 141. 环形链表),在判断有环的基础上,进一步找到环的入口(力扣 142. 环形链表 II)
1 | def f(head: ListNode) -> ListNode: |
1 | def has_cycle(head: ListNode) -> bool: |
1 | def detect_cycle(head: ListNode) -> ListNode: |
间隔指针
- 说明:两个指针保持固定间隔 k 同时移动,常用于找倒数第 k 个节点。
- 题型:寻找倒数第 k 个节点(力扣 面试题 02.02. 返回倒数第 k 个节点)
1 | def find_from_end(head: ListNode, k: int) -> ListNode: |
链表归并排序
- 说明:利用快慢指针找到链表中点,并将链表从中间断开,形成左右两个子链表。递归排序左右子链表。合并两个有序子链表,合并时借助哨兵节点简化操作。
- 题型:排序链表(力扣 148. 排序链表)
1 | def sort_list(head: ListNode) -> ListNode: |
栈
操作
- 入栈(push):stack.append(x),O(1)
- 出栈(pop):stack.pop(),O(1)
- 查看栈顶(peek):stack[-1],O(1)
- 判空:not stack,O(1)
括号匹配
- 说明:查字符串中的括号是否合法匹配(如 ()、{}、[]),或计算最长有效括号的长度。核心思想是遇到左括号压栈,遇到右括号时检查栈顶是否匹配,匹配则弹出,否则无效。
- 题型:有效的括号(力扣 20. 有效的括号),最长有效括号(力扣 32. 最长有效括号)
有效的括号
- 说明:给定一个只包括 ‘(‘,’)’,’{‘,’}’,’[‘,’]’ 的字符串 s,判断字符串是否有效。有效字符串需满足:左括号必须用相同类型的右括号闭合;左括号必须以正确的顺序闭合。使用栈:遇到左括号入栈,遇到右括号时检查栈顶是否与之匹配,若匹配则弹出,否则直接返回 False。遍历结束后栈应为空。
1 | def is_valid(s: str) -> bool: |
最长有效括号
- 说明:给定一个只包含 ‘(‘ 和 ‘)’ 的字符串,找出最长有效(格式正确且连续)括号子串的长度。一种方法是使用栈记录所有匹配括号对的索引,然后对这些索引排序,寻找最长的连续递增序列(相邻索引差 1),其长度即为最长有效括号的长度。
1 | def longest_valid_parentheses(s: str) -> int: |
单调栈
- 说明:栈内元素(通常存下标)保持单调递增或递减。例如找「下一个更大元素」时,维护一个递减栈(栈底到栈顶递减),意味着栈中元素都还没遇到比它们大的数。需要快速查找数组中每个元素左边或右边第一个比它大(或小)的元素。通过维护栈内元素的单调性(递增或递减),可以在一次遍历中得到答案。常用于解决“下一个更大元素”、“每日温度”、“接雨水”等问题。
- 题型:下一个更高温度出现在几天后(力扣 739. 每日温度),下一个更大元素(力扣 496. 下一个更大元素 I)接雨水(力扣 42. 接雨水(经典困难题,可用单调栈优化))
每日温度
- 说明:给定一个温度列表 temperatures,对于每一天,需要至少多少天才能等到更高的温度。如果之后没有更高的温度,则输出 0。使用单调递减栈(栈底到栈顶递减),栈中存放索引。遍历温度,当当前温度高于栈顶索引对应的温度时,弹出栈顶并计算天数差。
1 | def daily_temperatures(temperatures): |
下一个更大元素
- 说明:给定两个没有重复元素的数组 nums1 和 nums2,其中 nums1 是 nums2 的子集。找出 nums1 中每个元素在 nums2 中的下一个比其大的元素。使用单调栈预先计算 nums2 中每个元素的下一个更大元素,存入哈希表,然后查询结果。
1 | def next_greater_element(nums1, nums2): |
接雨水
- 说明:给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子下雨后能接多少雨水。使用单调递减栈,当遇到比栈顶高的柱子时,弹出栈顶作为底部,左边界为新的栈顶,右边界为当前柱子,计算当前凹槽的雨水量并累加。
1 | def trap(height): |
表达式求值
- 说明:计算后缀表达式(逆波兰表达式)的值,或将中缀表达式转为后缀表达式(调度场算法)。栈用于保存操作数或运算符,根据运算规则进行压栈和弹栈。
- 题型:逆波兰表达式求值(力扣 150. 逆波兰表达式求值),中缀表达式(力扣 772. 基本计算器 III)
逆波兰表达式求值
- 说明:根据逆波兰表示法,求表达式的值。有效运算符包括 +、-、*、/。每个操作数可以是整数或其他表达式。使用栈:遇到数字入栈,遇到运算符则弹出两个操作数计算后入栈。注意除法需向零截断(即 int(b / a) 或 int(b / a) 在 Python 中需特殊处理)。
1 | def eval_rpn(tokens): |
基本计算器 III(力扣 772)
- 说明:实现一个基本的计算器来计算简单的表达式字符串。表达式可以包含括号、加、减、乘、除,以及非负整数(可能有多位数)。使用双栈法:操作数栈 nums 和运算符栈 ops。遍历字符串,遇到数字时累加得到完整数字;遇到 ‘(‘ 直接入 ops;遇到 ‘)’ 则计算直到遇到 ‘(‘;遇到运算符时,先处理栈顶优先级不低于当前运算符的运算,再将当前运算符入栈。遍历结束后清空 ops。需注意处理负号(如 -2+1 或表达式开头为 -),可在遇到 - 时检查前一个字符是否为 ‘(‘ 或开头,若是则先在 nums 中补 0。
1 | def calculate(s: str) -> int: |
队列
操作
1 | from collections import deque |
双端队列
- 说明:deque 支持两端高效插入和删除,可作为队列或栈使用。常用于需要两端操作的场景,如滑动窗口、回文检查、设计循环双端队列等。
- 题型:力扣 641. 设计循环双端队列,力扣 933. 最近的请求次数
循环双端队列
- 思路:可以使用数组或双向链表实现,这里使用数组(列表)模拟循环队列。
1 | class MyCircularDeque: |
最近的请求次数
- 说明:用队列存储请求时间,每次调用时先将新时间入队,然后移除队首所有小于 t-3000 的时间,最后返回队列长度。
1 | class RecentCounter: |
优先队列(堆)
- 说明:需要动态维护集合的最值,并支持插入和删除最值。Python 的 heapq 模块实现最小堆,可通过取负数模拟最大堆。常用于 TopK 问题、合并有序链表、任务调度、Dijkstra 算法等。
- 题型:力扣 215. 数组中的第K个最大元素(用堆或快速排序),力扣 23. 合并K个升序链表(用堆维护每个链表的头节点),力扣 347. 前 K 个高频元素(用堆统计频率),力扣 295. 数据流的中位数(用两个堆维护),
| 函数 | 描述 |
|---|---|
heappush(heap, item) |
将元素 item 压入堆中,并保持堆的不变性。 |
heappop(heap) |
弹出并返回堆中的最小元素,同时保持堆的不变性。 |
heapify(x) |
将列表 x 原地转换为一个合法的堆。 |
数组中的第K个最大元素
- 说明:维护一个大小为 k 的最小堆,堆顶即为第 k 大的元素。
1 | import heapq |
合并K个升序链表
- 说明:合并 k 个升序链表,返回合并后的升序链表。使用最小堆,将每个链表的头节点(值和节点)存入堆,每次弹出最小节点,然后将该节点的下一个节点入堆。
1 | import heapq |
前 K 个高频元素
- 说明:返回数组中出现频率前 k 高的元素。先用哈希表统计频率,然后维护一个大小为 k 的最小堆(按频率排序),最后堆中元素即为答案。
1 | import heapq |
数据流的中位数
- 说明:设计一个数据结构,支持添加整数并返回当前所有元素的中位数。使用两个堆:一个大顶堆(存较小的一半)和一个小顶堆(存较大的一半)。保持两个堆大小平衡,即可随时获取中位数。
单调队列
- 说明:维护一个队列,使其元素保持单调递增或递减,常用于解决滑动窗口最值问题。单调队列的核心思想是在入队时删除破坏单调性的元素,从而保证队首始终是当前窗口的最值。与单调栈类似,但这里是队列结构(先进先出)。
- 题型:力扣 239. 滑动窗口最大值,力扣 1438. 绝对差不超过限制的最长连续子数组
滑动窗口最大值
- 说明:给定数组 nums 和滑动窗口大小 k,返回每个窗口中的最大值。使用双端队列存储元素下标,保持队列中下标对应的值单调递减(队首最大)。每次移动窗口时,移除队首不在窗口内的下标,并保持单调性。
1 | class Solution: |
绝对差不超过限制的最长连续子数组
- 说明:给定数组 nums 和限制 limit,求最长连续子数组的长度,使得子数组中任意两个元素的绝对差 ≤ limit。使用两个单调队列(一个维护最大值,一个维护最小值),滑动窗口扩张右边界时更新队列,当窗口内最大值与最小值差超过 limit 时收缩左边界。
1 | class Solution: |
哈希表
定义
- 哈希表(字典)是一种键值对存储结构,通过哈希函数将键映射到存储位置,实现 O(1) 平均时间复杂度的插入、删除和查找。
- Python 中
dict和set分别实现哈希表和哈希集合。 - 适用场景:需要快速判断元素是否存在、统计频率、建立映射关系、去重等。
哈希表注意事项
- 哈希函数冲突:Python 字典已内置处理,一般无需关心。
- 键的类型必须可哈希(不可变类型),如
int、str、tuple。若需使用列表等作为键,可转为元组。 - 使用
defaultdict或Counter可以简化计数代码。
两数之和类(查找表优化)
- 说明:遍历数组时,将已经访问过的元素及其下标存入哈希表。对于当前元素
nums[i],检查target - nums[i]是否在哈希表中,若存在则找到答案。这种“空间换时间”的思想将暴力 O(n²) 降为 O(n)。 - 题型:力扣 1. 两数之和,力扣,力扣 15. 三数之和(哈希辅助去重)
两数之和
- 说明:给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那两个整数,并返回它们的数组下标。假设每种输入只对应一个答案,且不能重复使用同一个元素。
1 | def two_sum(nums: List[int], target: int) -> List[int]: |
三数之和
- 说明:给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0。找出所有不重复的三元组。哈希表可用于去重和查找,但主流解法是排序+双指针。这里展示哈希辅助去重的思路(需先排序,并注意跳过重复元素)。
1 | class Solution: |
计数统计
- 说明:利用哈希表统计元素出现的次数(频率),是解决“出现次数”相关问题的核心方法。例如判断两个字符串是否为字母异位词、求数组中出现次数最多的元素等。
- 题型:力扣 242. 有效的字母异位词,力扣 169. 多数元素,力扣 383. 赎金信,力扣 387. 字符串中的第一个唯一字符
有效的字母异位词
- 说明:给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。即两个字符串中字符出现的种类和次数均相同。
1 | class Solution: |
多数元素
- 说明:给定一个大小为 n 的数组,找到其中的多数元素,即出现次数大于 ⌊ n/2 ⌋ 的元素。可以假设数组非空,且多数元素一定存在。使用哈希表统计频率,遍历找出次数最大的。
1 | class Solution: |
赎金信
- 说明:给定一个赎金信 (ransom) 字符串和一个杂志(magazine)字符串,判断第一个字符串 ransom 能不能由第二个字符串 magazines 里面的字符构成。如果可以构成,返回 true;否则返回 false。magazine 中的每个字符只能在 ransom 中使用一次。
1 | class Solution: |
字符串中的第一个唯一字符
- 说明:给定一个字符串数组,将字母异位词组合在一起。可以按排序后的字符串作为键。
1 | class Solution: |
分组归类
- 说明:将具有某种共同特征的元素归为一类,常用哈希表将“特征”映射到对应列表。例如字母异位词分组,特征可以是排序后的字符串,也可以是字符计数的元组。
- 题型:力扣 49. 字母异位词分组,力扣 249. 移位字符串分组,力扣 336. 回文对
字母异位词分组
- 说明:给定一个字符串数组,将字母异位词组合在一起。可以按排序后的字符串作为键。
1 | class Solution: |
移位字符串分组
- 说明:
1 | def group_strings(strings: List[str]) -> List[List[str]]: |
回文对
- 说明:对于每个单词 word,我们尝试将它拆分成前缀 prefix 和后缀 suffix,检查两种可能形成回文的情况:
- 前缀是回文:如果 prefix 本身是回文,那么只需要 suffix 的逆序字符串存在于单词列表中,且不是当前单词本身,就可以将那个单词放在前面,当前单词放在后面,拼接成回文。
- 后缀是回文:如果 suffix 本身是回文,那么只需要 prefix 的逆序字符串存在于单词列表中,且不是当前单词本身,就可以将当前单词放在前面,那个单词放在后面,拼接成回文。
为了快速查找逆序字符串,我们预先将所有单词及其下标存入哈希表 mapping。
1 | def palindrome_pairs(words: List[str]) -> List[List[int]]: |
集合与连续性
- 说明:利用哈希集合(
set)存储元素,可以快速判断一个数是否存在。对于最长连续序列问题,通过集合去重后,只从连续段的起点开始向后枚举,避免重复计算,达到 O(n) 时间复杂度。 - 题型:力扣 128. 最长连续序列
最长连续序列
- 说明:
1 | def longest_consecutive(nums: List[int]) -> int: |
映射关系
- 说明:哈希表可以建立一对一的映射关系,常用于判断两个结构是否同构、模式匹配等。例如同构字符串中,需要建立字符到字符的双向映射,避免出现多对一的情况。
- 题型:力扣 205. 同构字符串,力扣 290. 单词规律,力扣 953. 验证外星语词典
同构字符串
- 说明:给定两个字符串 s 和 t,判断它们是否是同构的。即 s 中的字符可以映射到 t 中的字符,且映射是双射(一一对应)。
1 | class Solution: |
树
图
字符串
查找
适用情况
- 数据具有有序性
- 问题可以转化为判定问题,且判定结果在某个分界点前后呈现“是/否”的单调变化
- 答案在一个明确的范围内,且可以快速验证
复杂度
- 时间复杂度: O(log n)
- 空间复杂度: O(1)
模版
1 | def half_search(nums, target): |
变种
查找左右边界
- 说明:数组有序但可能包含重复元素,需要找到目标值的第一个出现位置和最后一个出现位置。核心是改变 nums[mid] == target 时的处理逻辑:
- 找左边界:当相等时,不立即返回,而是继续向左收缩(right = mid - 1),最后退出时 left 指向第一个 ≥ target 的位置。
- 找右边界:当相等时,继续向右收缩(left = mid + 1),最后退出时 right 指向最后一个 ≤ target 的位置。
- 题型:查找左右边界(力扣 34. 在排序数组中查找元素的第一个和最后一个位置)
1 | def search_range(nums, target): |
寻找峰值
- 说明:数组不一定有序,但相邻元素不等。峰值是指大于相邻左右的元素(可认为 nums[-1] = nums[n] = -∞)。通过比较 mid 和 mid+1 的值,决定往哪一侧收缩:
- 如果 nums[mid] > nums[mid+1],说明峰值在左侧(包括 mid),移动右指针。
- 否则峰值在右侧(mid+1 更大),移动左指针。
- 题型:寻找峰值(力扣 162. 寻找峰值)
1 | def find_peak_element(nums): |
搜索旋转排序数组
- 说明:在旋转后的有序数组(无重复)中搜索目标值。数组原本是升序的,但在某个未知点进行了旋转,例如 [4,5,6,7,0,1,2]。核心思路是每次二分后,先判断哪一部分是有序的,然后根据目标值是否在有序区间内决定搜索方向。
- 题型:搜索旋转排序数组(力扣 33. 搜索旋转排序数组)
1 | def search_rotated(nums, target): |
搜索旋转排序数组 II(力扣 81. 搜索旋转排序数组II)
- 说明:与 33 题类似,但数组中存在重复元素。当 nums[left] == nums[mid] 时无法判断哪边有序,需要收缩左边界去重。
- 题型:搜索旋转排序数组 II(力扣 81. 搜索旋转排序数组II)
1 | def search_rotated_ii(nums, target): |
排序
冒泡排序
- 说明:重复遍历数组,比较相邻元素,若顺序错误则交换,直到没有需要交换的元素。每轮将最大元素“冒泡”到末尾。适用于小规模数据。
- 时间复杂度:O(n²)(平均和最坏),O(n)(最好,已有序)
- 空间复杂度:O(1)
- 稳定性:稳定
1 | def bubble_sort(arr): |
选择排序
- 说明:每次从未排序部分选择最小(或最大)元素,放到已排序部分的末尾。简单直观,但不论数据如何,比较次数固定。
- 时间复杂度:O(n²)(所有情况)
- 空间复杂度:O(1)
- 稳定性:不稳定
1 | def selection_sort(arr): |
插入排序
- 说明:将数组分为已排序和未排序两部分,每次将未排序的第一个元素插入到已排序部分的正确位置。适合小规模或基本有序的数据。
- 时间复杂度:O(n²)(平均和最坏),O(n)(最好,已有序)
- 空间复杂度:O(1)
- 稳定性:稳定
1 | def insertion_sort(arr): |
希尔排序
- 说明:插入排序的改进版,通过将数组按一定间隔分组进行插入排序,逐步缩小间隔,最后间隔为1时进行一次完整插入排序。间隔序列常取 gap = gap//2。
- 时间复杂度:取决于间隔序列,平均约 O(n^1.3),最坏 O(n²)
- 空间复杂度:O(1)
- 稳定性:不稳定
1 | def shell_sort(arr): |
归并排序
- 说明:采用分治法,将数组分成两半分别排序,然后合并两个有序数组。递归实现,需要额外空间。
- 时间复杂度:O(n log n)(所有情况)
- 空间复杂度:O(n)
- 稳定性:稳定
1 | def merge_sort(arr): |
快速排序
- 说明:分治法,选择一个基准(pivot),将小于基准的放左边,大于基准的放右边,然后递归排序左右子数组。平均性能最好,但最坏情况(已有序且选最左为基准)退化为 O(n²)。
- 时间复杂度:平均 O(n log n),最坏 O(n²)
- 空间复杂度:O(log n)(递归栈)
- 稳定性:不稳定
1 | def quick_sort(arr, left, right): |
数组中的第K个最大元素
- 说明:调用 partition 将数组分为两部分,左侧元素 ≤ 基准,右侧元素 > 基准。若基准位置 pivot 恰好等于目标索引 target,则直接返回该元素;若 pivot < target,说明目标在右侧,移动左边界;否则目标在左侧,移动右边界。
- 题型:数组中的第K个最大元素(力扣 215. 数组中的第K个最大元素)
1 | def find_kth_largest(nums, k): |
堆排序
- 说明:利用堆(优先队列)进行排序。先将数组构建成最大堆,然后反复将堆顶(最大值)与末尾交换,并调整堆。原地排序。
- 时间复杂度:O(n log n)(所有情况)
- 空间复杂度:O(1)
- 稳定性:不稳定
1 | def heap_sort(arr): |
计数排序
- 说明:非比较排序,适用于数据范围不大的整数。统计每个值出现的次数,然后按顺序输出。要求输入数据是非负整数。
- 时间复杂度:O(n + k),k 为数据范围
- 空间复杂度:O(k)
- 稳定性:稳定
1 | def counting_sort(arr): |
桶排序
- 说明:将数据分到有限数量的桶里,每个桶内分别排序(可使用其他排序),然后合并。适用于数据均匀分布的情况。
- 时间复杂度:平均 O(n + n²/k + k)(取决于桶内排序),最坏 O(n²)
- 空间复杂度:O(n + k)
- 稳定性:取决于桶内排序算法
1 | def bucket_sort(arr, bucket_size=5): |
基数排序
- 说明:非比较排序,按低位到高位依次进行计数排序(或桶排序)。适用于整数或定长字符串。
- 时间复杂度:O(d * (n + k)),d 为位数,k 为基数(如10)
- 空间复杂度:O(n + k)
- 稳定性:稳定
1 | def radix_sort(arr): |
排序总结
| 排序算法 | 最好时间复杂度 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 | 适用条件 |
|---|---|---|---|---|---|---|
| 冒泡排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 | 1. 适用于小规模数据 2. 对稳定性有要求的场景 |
| 选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 | 1. 适用于小规模数据 2. 对空间限制严格(原地排序) 3. 不要求稳定性 |
| 插入排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 | 1. 适用于小规模或基本有序的数据 2. 数据基本有序时效率高 |
| 希尔排序 | O(n log n) | O(n^1.3) | O(n²) | O(1) | 不稳定 | 1. 适用于中等规模数据 2. 对稳定性无要求 3. 需要原地排序 |
| 归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 | 1. 适用于大规模数据 2. 需要稳定排序 3. 允许 O(n) 额外空间 |
| 快速排序 | O(n log n) | O(n log n) | O(n²) | O(log n) | 不稳定 | 1. 适用于大规模数据 2. 平均性能最优 3. 对最坏情况不敏感(可优化) |
| 堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 不稳定 | 1. 适用于大规模数据 2. 需要原地排序 3. 对稳定性无要求 4. 常数时间要求不高 |
| 计数排序 | O(n + k) | O(n + k) | O(n + k) | O(n + k) | 稳定 | 1. 输入为非负整数 2. 数据范围(k)较小,否则空间消耗大 3. 需要稳定排序 |
| 桶排序 | O(n + k) | O(n + k) | O(n²) | O(n + k) | 稳定 | 1. 数据均匀分布 2. 数据范围已知且适合分桶 3. 可结合其他排序进行桶内排序 |
| 基数排序 | O(d·(n + k)) | O(d·(n + k)) | O(d·(n + k)) | O(n + k) | 稳定 | 1. 适用于整数或定长字符串 2. 数据位数(d)较小 3. 需要稳定排序 |
