当前位置:首页>python>剑指offer专题(二):例题详解(Python版)

剑指offer专题(二):例题详解(Python版)

  • 2026-09-10 02:59:31
剑指offer专题(二):例题详解(Python版)

1. 295. 数据流的中位数(困难)

题目详情

中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。实现MedianFinder 类:MedianFinder()初始化MedianFinder 对象;void addNum(int num) 将数据流中的整数num 添加到数据结构中;double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差10⁻⁵ 以内的答案将被接受。

示例

输入:["MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"],[[], [1], [2], [], [3], []]

输出:[null, null, null, 1.5, null, 2.0]

解释:MedianFinder medianFinder = new MedianFinder(); medianFinder.addNum(1); // arr = [1];medianFinder.addNum(2); // arr = [1, 2];medianFinder.findMedian(); // 返回 1.5 ((1 + 2) / 2);medianFinder.addNum(3); // arr[1, 2, 3];medianFinder.findMedian(); // return 2.0

提示

•-10⁵ <= num <= 10⁵

•在调用 findMedian 之前,数据结构中至少有一个元素

•最多 5 * 10⁴ 次调用 addNum 和 findMedian

核心解题思路

采用「双堆」技巧(面试高频,困难题核心解法),用两个堆维护数据流:最大堆(max_heap)存储左半部分元素,最小堆(min_heap)存储右半部分元素,保证两个堆的大小差不超过1。添加元素时,先插入最大堆,再调整到最小堆,最后平衡两个堆的大小;求中位数时,若最大堆size 大于最小堆,中位数为最大堆顶;否则为两个堆顶的平均值。

Python面试手撕代码

class MedianFinder(object):    def __init__(self):        # 最大堆:存储左半部分元素(用负号实现,因为Python默认是最小堆)        self.max_heap = []        # 最小堆:存储右半部分元素        self.min_heap = []    def addNum(self, num):        """        :type num: int        :rtype: None        """        # 先将元素插入最大堆(插入负号,模拟最大堆)        heapq.heappush(self.max_heap, -num)        # 将最大堆的堆顶元素(左半部分最大值)移到最小堆,保证左<=右        heapq.heappush(self.min_heap, -heapq.heappop(self.max_heap))        # 平衡两个堆的大小,确保最大堆size >= 最小堆size,且差不超过1        if len(self.max_heap) < len(self.min_heap):            heapq.heappush(self.max_heap, -heapq.heappop(self.min_heap))    def findMedian(self):        """        :rtype: float        """        # 最大堆size更大,中位数是最大堆顶(还原负号)        if len(self.max_heap) > len(self.min_heap):            return -self.max_heap[0]        # 两个堆size相等,中位数是两个堆顶的平均值        else:            return (-self.max_heap[0] + self.min_heap[0]) / 2.0

2. 53. 最大子数组和(中等)

题目详情

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组是数组中的一个连续部分。

示例

•示例 1:输入nums = [-2,1,-3,4,-1,2,1,-5,4],输出6;解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

•示例 2:输入nums = [1],输出1。

•示例 3:输入nums = [5,4,-1,7,8],输出23。

提示

•1 <= nums.length <= 10⁵

•-10⁴ <= nums[i] <= 10⁴

进阶

如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。

核心解题思路

采用「动态规划」(贪心思想优化,空间O(1)),核心思路:遍历数组,对于每个元素,判断其前一个元素的和是否为正,若为正,则将当前元素加上前一个元素的和(延长连续子数组);若为负,则当前元素作为新的连续子数组起点。最后返回数组中的最大值,即为最大子数组和。

Python面试手撕代码

class Solution:    def maxSubArray(self, nums: List[int]) -> int:        # 遍历数组,从第二个元素开始(索引1)        for i in range(1, len(nums)):            # 若前一个元素的和为正,说明加上当前元素能增大子数组和,进行累加            if nums[i-1] > 0:                nums[i] += nums[i-1]        # 数组经过修改后,最大值即为最大子数组和        return max(nums)

3. 179. 最大数(中等)

题目详情

给定一组非负整数 nums,重新排列每个数的顺序(每个数不可拆分)使之组成一个最大的整数。注意:输出结果可能非常大,所以你需要返回一个字符串而不是整数。

示例

•示例 1:输入nums = [10,2],输出"210"。

•示例 2:输入nums = [3,30,34,5,9],输出"9534330"。

提示

•1 <= nums.length <= 100

•0 <= nums[i] <= 10⁹

核心解题思路

核心是「自定义排序规则」:将数字转为字符串,比较两个字符串 a 和 b 的拼接结果(a+b 和 b+a),若 a+b < b+a,则 b 应排在 a 前面;反之 a 排在前面。采用快速排序实现自定义排序,排序后拼接字符串,若拼接结果以"0"开头(全为0的情况),返回"0",否则返回拼接结果。

Python面试手撕代码

class Solution(object):    def largestNumber(self, nums):        """        :type nums: List[int]        :rtype: str        """        # 快速排序的分区函数,自定义排序规则        def partition(strs, left, right):            # 随机选择基准值,避免极端情况(如数组已排序)            idx = random.randint(left, right)            strs[idx], strs[left] = strs[left], strs[idx]            pivot = strs[left]            while left < right:                # 比较b+a和a+b,若pivot+b <= b+pivot,说明b应在pivot前面,右指针左移                while left < right and int(pivot + strs[right]) <= int(strs[right] + pivot):                    right -= 1                strs[left] = strs[right]                # 若strs[left]+pivot <= pivot+strs[left],说明strs[left]应在pivot后面,左指针右移                while left < right and int(strs[left] + pivot) <= int(pivot + strs[left]):                    left += 1                strs[right] = strs[left]            strs[left] = pivot            return left        # 快速排序主函数        def quicksort(nums, left, right):            if left >= right:                return            mid = partition(nums, left, right)            quicksort(nums, left, mid-1)            quicksort(nums, mid+1, right)        # 将所有数字转为字符串,便于拼接比较        strs = [str(num) for num in nums]        n = len(strs)        # 对字符串数组进行自定义快速排序        quicksort(strs, 0, n-1)        # 特殊情况:全为0时,返回"0"(避免出现"000...")        if strs[-1] == '0':            return "0"        # 排序后反转(因为分区规则是从小到大,反转后为从大到小),再拼接        return ''.join(strs[::-1])

4. 剑指 Offer 46. 把数字翻译成字符串

题目详情

给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 a,1 翻译成 b,……,11 翻译成 l,……,25 翻译成 z。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。

示例

•示例 1:输入num = 12258,输出5;解释:12258有5种不同的翻译方法,分别是bccfi、bwfi、bczi、mcfi、mzi。

•示例 2:输入num = 0,输出1;解释:0只能翻译为a,仅1种方法。

•示例 3:输入num = 10,输出2;解释:10可翻译为ba(1→b,0→a)或k(10→k),共2种方法。

提示

•0 <= num <= 2^31 - 1

核心解题思路

采用「动态规划」,将数字转为字符串,定义 dp[i] 表示前 i 个字符的翻译方法数。边界条件:dp[0] = 1(空字符串有一种翻译方法),dp[1] = 1(单个字符只有一种翻译方法)。遍历字符串,若当前两位字符组成的数字在 10~25 之间,则 dp[i] = dp[i-1] + dp[i-2](两种翻译方式:单独翻译当前字符、与前一个字符组合翻译);否则dp[i] = dp[i-1](只能单独翻译当前字符)。

Python面试手撕代码

def translate_num(num: int) -> int:    s = str(num)  # 将数字转为字符串,便于处理每一位    n = len(s)    if n == 0:  # 空数字,返回0(实际题目中num为有效数字,此判断用于鲁棒性)        return 0    # dp[i] 表示前 i 个字符的翻译方法数    dp = [0] * (n + 1)    dp[0] = 1  # 边界:空字符串有一种翻译方法(用于组合翻译时的递推)    dp[1] = 1  # 边界:单个字符只有一种翻译方法    # 从第2个字符开始遍历(i从2到n)    for i in range(2, n + 1):        # 检查当前两位字符(s[i-2]和s[i-1])是否能组成合法的翻译数字(10-25)        if s[i-2] == '1' or (s[i-2] == '2' and s[i-1] <= '5'):            # 两种翻译方式:单独翻译当前字符、与前一个组合翻译            dp[i] = dp[i-1] + dp[i-2]        else:            # 只能单独翻译当前字符,方法数与前一个一致            dp[i] = dp[i-1]    return dp[n]  # 前n个字符(整个数字)的翻译方法数

5. 剑指 Offer 47. 礼物的最大价值

题目详情

在一个 m * n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?

示例

•示例 1:输入grid = [[1,3,1],[1,5,1],[4,2,1]],输出12;解释:路径1→3→5→2→1的礼物价值总和最大,为1+3+5+2+1=12。

•示例 2:输入grid = [[1,2,5],[3,2,1]],输出9;解释:路径1→3→2→1的礼物价值总和最大,为1+3+2+1=7(修正:正确路径1→3→2→1总和为7,或1→2→5→1总和为9,以题目标准示例为准)。

提示

•0 < grid.length <= 200

•0 < grid[0].length <= 200

•grid[i][j] > 0

核心解题思路

采用「动态规划」,定义 dp[i][j] 表示到达 (i,j) 位置时能拿到的最大礼物价值。边界条件:dp[0][0] = grid[0][0](起点价值);第一行只能从左向右移动,dp[0][j] = dp[0][j-1] + grid[0][j];第一列只能从上下移动,dp[i][0] = dp[i-1][0] + grid[i][0]。对于其他位置,dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + grid[i][j](取上方或左方的最大价值,加上当前礼物价值)。

Python面试手撕代码

def maxValue(grid: list[list[int]]) -> int:    # 边界判断:棋盘为空或第一行为空,返回0    if not grid or not grid[0]:        return 0    m, n = len(grid), len(grid[0])  # m行n列    # 初始化DP数组,与棋盘大小一致    dp = [[0] * n for _ in range(m)]    dp[0][0] = grid[0][0]  # 起点的最大价值就是自身价值    # 初始化第一行:只能从左向右移动,价值累加    for j in range(1, n):        dp[0][j] = dp[0][j-1] + grid[0][j]    # 初始化第一列:只能从上向下移动,价值累加    for i in range(1, m):        dp[i][0] = dp[i-1][0] + grid[i][0]    # 填充剩余位置:每个位置的最大价值 = 上方或左方的最大价值 + 当前礼物价值    for i in range(1, m):        for j in range(1, n):            dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + grid[i][j]    # 右下角的价值即为最大礼物价值    return dp[-1][-1]

6. 剑指 Offer 50. 第一个只出现一次的字符

题目详情

在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。s 只包含小写字母。

示例

•示例 1:输入s = "abaccdeff",输出"b";解释:字符b只出现1次,且是第一个只出现一次的字符。

•示例 2:输入s = "",输出" ";解释:空字符串无字符,返回单空格。

•示例 3:输入s = "aabb",输出" ";解释:所有字符均出现2次,无只出现一次的字符。

提示

•0 <= s.length <= 10000

•s 仅由小写英文字母组成

核心解题思路

采用「哈希表」(字典)统计字符出现次数,遍历字符串,将每个字符存入字典:若字符未出现过,标记为 True(表示出现1次);若已出现过,标记为 False(表示出现多次)。再遍历字典,找到第一个标记为True 的字符,即为答案;若没有,返回空格。

Python面试手撕代码

classSolution:    def firstUniqChar(self, s):        # 哈希字典,用于标记字符是否只出现一次        hash_dict = {}        # 第一次遍历:统计每个字符的出现情况        for char in s:                if char not in hash_dict:                        hash_dict[char] = True  # 首次出现,标记为True                else:                        hash_dict[char] = False  # 再次出现,标记为False        # 第二次遍历:找到第一个标记为True(只出现一次)的字符        for k, v in hash_dict.items():                if v:                        return k        # 若没有只出现一次的字符,返回单空格        return ' '

7. LCR 170. 交易逆序对的总数(困难)

题目详情

在股票交易中,如果前一天的股价高于后一天的股价,则可以认为存在一个「交易逆序对」。请设计一个程序,输入一段时间内的股票交易记录record,返回其中存在的「交易逆序对」总数。

示例

示例 1:输入record = [9, 7, 5, 4, 6],输出8;解释:交易中的逆序对为 (9, 7), (9, 5), (9, 4), (9, 6), (7, 5), (7, 4), (7, 6), (5, 4)。

提示

0 <= record.length <= 50000

核心解题思路

采用「归并排序」(困难题核心解法),在归并排序的合并阶段统计逆序对数量。合并两个有序子数组时,若左子数组的元素大于右子数组的元素,则左子数组中剩余的所有元素都与当前右子数组元素构成逆序对,统计数量;最后将排序后的数组还原,返回统计的逆序对总数,时间复杂度O(n log n),适配大数据量。

Python面试手撕代码

class Solution(object):    def reversePairs(self, record):        """        :type record: List[int]        :rtype: int        """        self.cnt = 0  # 用于统计逆序对总数,定义为类属性,方便递归修改        # 归并排序函数:nums为原数组,res为临时数组,left、right为当前排序区间        def merge_sort(nums, res, left, right):            if left >= right:  # 单个元素或空,无需排序,返回                return            mid = left + (right - left) // 2  # 中间位置,避免溢出            merge_sort(nums, res, left, mid)  # 递归排序左子数组            merge_sort(nums, res, mid+1, right)  # 递归排序右子数组            # 合并两个有序子数组,同时统计逆序对            i, j, k = left, mid+1, left  # i左子数组指针,j右子数组指针,k临时数组指针            while i <= mid and j <= right:                if nums[i] <= nums[j]:  # 左元素 <= 右元素,无逆序对,左指针后移                    res[k] = nums[i]                    i += 1                    k += 1                else:  # 左元素 > 右元素,左子数组剩余元素均与当前右元素构成逆序对                    res[k] = nums[j]                    self.cnt += mid - i + 1  # 统计逆序对数量                    j += 1                    k += 1            # 将左子数组剩余元素存入临时数组            while i <= mid:                res[k] = nums[i]                i += 1                k += 1            # 将右子数组剩余元素存入临时数组            while j <= right:                res[k] = nums[j]                j += 1                k += 1            # 将临时数组的有序元素复制回原数组,完成当前区间排序            for k in range(left, right+1):                nums[k] = res[k]        n = len(record)        res = [0] * n  # 临时数组,用于归并排序的合并操作        merge_sort(record, res, 0, n-1)  # 启动归并排序,统计逆序对        return self.cnt

8. LCR 173. 点名(简单)

题目详情

某班级 n 位同学的学号为 0 ~ n-1。点名结果记录于升序数组 records。假定仅有一位同学缺席,请返回他的学号。

示例

•示例 1:输入records = [0,1,2,3,5],输出4。

•示例 2:输入records = [0, 1, 2, 3, 4, 5, 6, 8],输出7。

提示

1 <= records.length <= 10000

核心解题思路

利用「升序数组特性 + 二分查找」,正常情况下,学号 i 应对应 records[i] = i。若 records[mid] == mid,说明左半部分无缺席同学,缺席者在右半部分;若 records[mid] != mid,说明缺席者在左半部分(包括mid)。遍历结束后,left 即为缺席同学的学号。

Python面试手撕代码

class Solution(object):    def takeAttendance(self, records):        """        :type records: List[int]        :rtype: int        """        left ,right = 0, len(records)-1  # 二分查找双指针        while left <= right:            mid = (left + right) // 2  # 中间位置            if records[mid] == mid:                # 中间位置学号匹配,缺席者在右半部分                left = mid + 1            else:                # 中间位置学号不匹配,缺席者在左半部分                right = mid - 1        # 循环结束,left即为缺席同学的学号        return left

9. 230. 二叉搜索树中第 K 小的元素(中等)

题目详情

给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 小的元素(k 从 1 开始计数)。

示例

•示例 1:输入root = [3,1,4,null,2], k = 1,输出1。

•示例 2:输入root = [5,3,6,2,4,null,null,1], k = 3,输出3。

提示

•树中的节点数为 n 。

•1 <= k <= n <= 10⁴

•0 <= Node.val <= 10⁴

进阶

如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第 k 小的值,你将如何优化算法?

核心解题思路

利用「二叉搜索树的中序遍历特性」(中序遍历结果为升序),采用递归中序遍历,遍历过程中计数,当计数达到 k时,当前节点的值即为第 k小的元素,立即返回,避免多余遍历。

Python面试手撕代码

# Definition for a binary tree node.# class TreeNode(object):#     def __init__(self, val=0, left=None, right=None):#         self.val = val#         self.left = left#         self.right = rightclass Solution(object):    def kthSmallest(self, root, k):        """        :type root: Optional[TreeNode]        :type k: int        :rtype: int        """        self.result = None  # 存储第k小的元素值        self.cnt = 0        # 计数,记录当前遍历到第几个元素        # 中序遍历函数(左→根→右,结果升序)        def dfs(root):            if not root:  # 节点为空,返回                return            dfs(root.left)  # 先遍历左子树            self.cnt += 1   # 遍历到当前节点,计数+1            if self.cnt == k:  # 计数达到k,当前节点即为第k小元素                self.result = root.val                return  # 找到后直接返回,避免多余遍历            dfs(root.right)  # 遍历右子树        dfs(root)  # 启动中序遍历        return self.result

10. 扑克牌顺子(简单)

题目详情

从若干副扑克牌中随机抽 5 张牌,判断是不是一个顺子,即这 5 张牌是不是连续的。2 ~ 10 为数字本身,A 为 1,J 为 11,Q 为 12,K 为 13,而大、小王为 0,可以看成任意数字。A 不能视为 14。

核心解题思路

核心条件:1. 除0外,无重复牌(重复牌无法构成顺子);2. 非0牌的最大值与最小值之差小于5(0可填补空缺)。用哈希集合记录非0牌,判断是否有重复;同时记录非0牌的最大值和最小值,最后验证差值是否小于5,满足则为顺子。

Python面试手撕代码

class Solution:    def isStraight(self, nums: list[int]) -> bool:        # 哈希集合,记录非0的扑克牌,用于判断重复        cards = set()        # 初始化最小值为不可能存在的扑克牌值(最大牌为13,设为14)        min_val = 14        # 初始化最大值为不可能存在的扑克牌值(最小非0牌为1,设为0)        max_val = 0        for num in nums:            # num 为 0 代表大小王,可视为任意数字,跳过处理            if num == 0:                continue            # 更新非0牌的最大值            max_val = max(max_val, num)            # 更新非0牌的最小值            min_val = min(min_val, num)            # 若集合中已存在该数,说明有重复牌,无法构成顺子,返回false            if num in cards:                return False            # 将当前非0牌加入集合            cards.add(num)        # 最终判断:最大值与最小值的差是否小于5(0可填补中间的空缺)        return max_val - min_val < 5

11. 121. 买卖股票的最佳时机(简单)

题目详情

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。你只能选择某一天买入这只股票,并选择在未来的某一个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0。

示例

•示例 1:输入[7,1,5,3,6,4],输出5;解释:在第 2 天(股票价格 = 1)买入,第 5 天(股票价格 = 6)卖出,最大利润 = 6-1 = 5。

•示例 2:输入prices = [7,6,4,3,1],输出0;解释:无盈利空间,返回0。

提示

•1 <= prices.length <= 10⁵

•0 <= prices[i] <= 10⁴

核心解题思路

采用「动态规划」(贪心思想优化,空间O(1)),定义两个变量:buy 表示当前最低买入价(初始为第一天价格的负数),sell 表示当前最大利润(初始为0)。遍历数组,不断更新最低买入价和最大利润:buy = max(buy, -prices[i])(取最低买入价),sell = max(sell, buy + prices[i])(取当前价格卖出的最大利润),最终返回sell。

Python面试手撕代码

class Solution(object):    def maxProfit(self, prices):        """        :type prices: List[int]        :rtype: int        """        # buy 表示当前最低买入价(用负数表示,便于后续计算利润)        buy = -prices[0]        # sell 表示当前能获得的最大利润        sell = 0        # 遍历数组,从第一天开始(索引0)        for i in range(len(prices)):            # 更新最低买入价:取当前买入价和之前的最低买入价中更低的            buy = max(buy, -prices[i])            # 更新最大利润:取当前利润和(当前买入价+当前价格)中更大的            sell = max(sell, buy + prices[i])        # 最终返回最大利润(无盈利则为0)        return sell

12. 238. 除了自身以外数组的乘积(中等)

题目详情

给你一个整数数组 nums,返回数组answer ,其中answer[i] 等于nums 中除了nums[i] 之外其余各元素的乘积。题目数据保证数组nums之中任意元素的全部前缀元素和后缀的乘积都在32 位整数范围内。请不要使用除法,且在 O(n) 时间复杂度内完成此题。

示例

•示例 1:输入nums = [1,2,3,4],输出[24,12,8,6]。

•示例 2:输入nums = [-1,1,0,-3,3],输出[0,0,9,0,0]。

提示

•2 <= nums.length <= 10⁵

•-30 <= nums[i] <= 30

•输入保证数组 answer[i] 在 32 位整数范围内

进阶

你可以在 O(1) 的额外空间复杂度内完成这个题目吗?(出于对空间复杂度分析的目的,输出数组不被视为额外空间。)

核心解题思路

采用「前缀乘积 + 后缀乘积」(空间O(1)优化),先用输出数组 ans 存储前缀乘积:ans[i] = 前 i-1 个元素的乘积;再用一个变量 suffix 存储后缀乘积,从后向前遍历,ans[i] *= suffix(后缀乘积,即后 n-i-1 个元素的乘积),同时更新 suffix = suffix * nums[i],最终 ans 即为所求,无需额外空间。

Python面试手撕代码

class Solution(object):    def productExceptSelf(self, nums):        """        :type nums: List[int]        :rtype: List[int]        """        n = len(nums)        ans = [1] * n  # 输出数组,初始化为1,用于存储前缀乘积        # 第一步:计算前缀乘积,ans[i] = 前i-1个元素的乘积        for i in range(1, n):            ans[i] = ans[i-1] * nums[i-1]        # 第二步:计算后缀乘积,并用后缀乘积更新ans,实现O(1)空间        suffix = 1  # 后缀乘积,初始为1(最后一个元素的后缀乘积为1)        # 从后向前遍历        for i in range(n-1, -1, -1):            ans[i] *= suffix  # ans[i] = 前缀乘积 * 后缀乘积            suffix *= nums[i]  # 更新后缀乘积,加入当前元素        return ans

例题来自力扣网https://leetcode-cn.com/

请在微信客户端打开

最新文章

随机文章