2021-10-20 剑指offer2:59~75题目+思路+多种题解

写在前面

本文是采用python为编程语言,作者自行练习使用,题目列表为:剑指 Offer(第 2 版),未使用实体书,难度未标注的均为“简单”,我也不是很清楚为什么有几个编号没有提供。“《剑指 Offer(第 2 版)》通行全球的程序员经典面试秘籍。剖析典型的编程面试题,系统整理基础知识、代码质量、解题思路、优化效率和综合能力这 5 个面试要点。”,本文中的思路来源于每道题目中的题解部分,争取提供全面,优化后的题解,其中所有代码已通过题目检验。

剑指 Offer 59 - I. 滑动窗口的最大值(困难)

题目

在这里插入图片描述

思路

看到难度就已经排除暴力法拉(暴力法复杂度为O(k(n-k+1))),下面思考这样一个问题即可:加入一个新元素,减少一个元素,如何在更短的时间内找到最大值?

  • 堆:维护一个大根堆,每次插入需要O(logk),删除需要O(1)
  • 优先队列:维护一个队列,从目的推导队列行为,希望满足队列头永远是最大值,且由于队列的性质,下标逐渐增加。则队列的删除规则:1. 只要下标处于后面的数 大于 下标处于前面的数,则前面的数可以删去 2. 下标移动,已经不在滑动窗口中

题解

  • 最大堆:
import heapq
class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        
        n = len(nums)
        # 默认按照元组的第一个值进行heapify,所以后存索引
        # python中的堆默认小根堆,取负号
        B_heap = [(-nums[index], index) for index in range(k)]
        heapq.heapify(B_heap)

        res = [-B_heap[0][0]] if nums else []

        for index in range(k, n):
            heapq.heappush(B_heap,(-nums[index], index))
            while B_heap[0][1] <= index-k:
                heapq.heappop(B_heap)
            res.append(-B_heap[0][0])
        return res
        
  • 优先队列:
import heapq
class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        
        n = len(nums)
        queue = []
        # 只需要存index即可,因为在添加时已经判断了大小
        for index in range(k):
            while queue and nums[index] >=nums[queue[-1]]:
                queue.pop()
            queue.append(index)
        
        res = [nums[queue[0]]] if nums else [] 
        for index in range(k,n):
            # 维持队列的递减
            while queue and nums[index] >=nums[queue[-1]]:
                queue.pop()
            queue.append(index)
            while queue[0]<= index-k:
                queue.pop(0)
            res.append(nums[queue[0]])
        return res

剑指 Offer 59 - II. 队列的最大值(中等)

题目

在这里插入图片描述

思路

类似于题目剑指 Offer 30. 包含min函数的栈,将最大值的操作放在每一个动作(插入or删除)中以避免一次性全部取出+排序的耗费,考虑使用变量维护最大值->在pop后最大值信息丢失,所以使用数据结构进行辅助。而这类题的关键key在于:同步更新。

  • 插入:如果新插入的值更大,则该值之前的更小的值没有意义了,因为更小的值一定先出队。维护的是一个单调递减的数据结构,队尾操作。
  • 删除:如果删除的值恰为当前最大的值,需要同步删除,队头操作。

所以在这个题中,我们的辅助结构是双端队列,而那个题中,使用栈就可以完成同步更新(原数据结构就是栈,出栈也在队尾判断)。

题解

class MaxQueue:
    """2个数组"""
    def __init__(self):
        self.queue = []
        self.help_queue = []

    def max_value(self) -> int:
        return self.help_queue[0] if self.help_queue else -1

    def push_back(self, value: int) -> None:
        self.queue.append(value)
        while self.help_queue and self.help_queue[-1] < value:
            self.help_queue.pop()
        self.help_queue.append(value)

    def pop_front(self) -> int:
        if not self.queue: return -1
        res = self.queue.pop(0)
        if res == self.help_queue[0]:
            self.help_queue.pop(0)
        return res

        

剑指 Offer 60. n个骰子的点数(中等)

题目

在这里插入图片描述

思路

动态归划,状态转移方程是dp加入一个新筛子[index] = sum(dp之前的筛子数[index-k]*1/6),其中k是1 ~ 6。因为加上新的筛子后,只有1到6种可能性,而他们分别和上一个状态点数相加,所有的sum的情况概率之和即是新的概率。
注意开辟数组的大小,n个筛子的涉及和的范围为n到6n,共5n+1种。

题解

  • 动态规划:注意不是使用二维数组dp来记录之前所有的状态,而是使用两个数组(dp代表上一个筛子,tmp代表加入当前筛子,循环记录)
class Solution:
    def dicesProbability(self, n: int) -> List[float]:
        dp = [1 / 6] * 6
        for cnt in range(2, n + 1):
            tmp = [0] * (5 * cnt + 1)
            for index in range(len(dp)):
                # 对上一个状态的每一个数而言,都可以再多1~6个数字
                for k in range(6):
                    tmp[index + k] += dp[index] / 6
            dp = tmp
        return dp
                

剑指 Offer 61. 扑克牌中的顺子

题目

在这里插入图片描述

思路

直接统计和排序统计都可以完成题目要求,比较简单,注意一定要充分挖掘题目信息:

  • 大小王:遇见0则跳过
  • 若干副扑克牌:需要去重操作
  • 顺子:最大值减去最小值不超过5

题解

  • 遍历+去重:
class Solution:
    def isStraight(self, nums: List[int]) -> bool:
        repeat = set()
        maxnum, minnum = 0, 14
        for num in nums:
            # 跳过大小王
            if num == 0: continue 
            maxnum = max(maxnum, num) # 最大牌
            minnum = min(minnum, num) # 最小牌
            # 若有重复,提前返回 false
            if num in repeat: return False 
            repeat.add(num)
        return maxnum - minnum < 5
        
  • 遍历+排序:
class Solution:
    def isStraight(self, nums: List[int]) -> bool:
        joker = 0
        nums.sort()
        for i in range(4):
        	# 统计大小王数量
            if nums[i] == 0: joker += 1 
            # 若有重复,提前返回 false
            elif nums[i] == nums[i + 1]: return False 
        return nums[4] - nums[joker] < 5 

剑指 Offer 62. 圆圈中最后剩下的数字

题目

在这里插入图片描述

思路

约瑟夫问题:
这里我们先给出递推公式f(n, m) = ( f(n-1, m) + m) %n,其中这个公式的结果是最终胜利者的编号,下面借助图片(图片是n=11,m=3的情况,绿色为下标)进行推导:
请添加图片描述

  • 假设已知n个人且m不超过n的情况,胜利者所处下标为index(之所以这么定义是因为每次淘汰的人是不同的,但是在只有一个人的情况下,胜利者下标是确定的0),则n-1个人的情况,胜利者所处下标为?
    • 比较容易得出的一个结论是,当index处于被去掉的编号之后时,新的坐标为index-m,因为重新计数使得原来为m+1坐标的变成了1,后面的依次挪,对每一个而言都是m位
    • 观察index处于被去掉的编号之前的情况,其坐标与当前人数(数组长度)有关,为(index+n-m)
  • 下面我们从n-1个人的情况反推回n个人的情况,再加上m大于n的情况,需要取余。从而得到递推关系index-n+m)%n=(index+m)%n,简而言之就是返回自己之前(未移动)的位置,再取余

题解

  • 递归(便于理解):
# Python 默认的递归深度不够,需要手动设置
sys.setrecursionlimit(100000)

def f(n, m):
    if n == 0:
        return 0
    x = f(n - 1, m)
    return (m + x) % n

class Solution:
    def lastRemaining(self, n: int, m: int) -> int:
        return f(n, m)

  • 迭代:递推公式的方法
class Solution:
    def lastRemaining(self, n: int, m: int) -> int:
        # 剩最后一个人的情况,存活者的下标一定为0
        index = 0
        # 递推的计算剩下cnt个人的情况下存活者的下标
        for cnt in range(2, n + 1):
            index = (index + m) % cnt
        return index

剑指 Offer 63. 股票的最大利润(中等)

题目

在这里插入图片描述

思路

还是动态规划,但是可以省去两遍循环,最小值直接使用变量维护即可

题解

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        minprice, maxgain = float('+inf'), 0
        for num in prices:
            minprice = min(minprice, num)
            maxgain = max(maxgain, num-minprice)
        return maxgain

剑指 Offer 64. 求1+2+…+n(中等)

题目

在这里插入图片描述

思路

  • 首先想到的就是求和公式,但是这样就使用了乘除法。但是我们知道,计算机中的本来就没有乘,乘是使用加法和位运算表示的(这里需要复习一下机组知识,我还没写),具体做法为:将B二进制展开,如果某一位index为1,那么这一位对答案的贡献为A*(1<<index)。而题目中规定了n的范围,可推出二进制的位数,将for循环全部展开写即可。
  • 考虑for循环遍历相加,不可行,于是考虑所有的循环都可以用递归表示。 那么问题在于,如何不使用if就能完成条件判断,以终止递归?采用与运算,当一个条件不满足时,不再进行。

题解

  • 模拟乘法(位运算):python中无法将表达式作为一个值,下面是java代码
class Solution {
    public int sumNums(int n) {
        int ans = 0, A = n, B = n + 1;
        boolean flag;

        flag = ((B & 1) > 0) && (ans += A) > 0;
        A <<= 1;
        B >>= 1;

        flag = ((B & 1) > 0) && (ans += A) > 0;
        A <<= 1;
        B >>= 1;

        flag = ((B & 1) > 0) && (ans += A) > 0;
        A <<= 1;
        B >>= 1;

        flag = ((B & 1) > 0) && (ans += A) > 0;
        A <<= 1;
        B >>= 1;

        flag = ((B & 1) > 0) && (ans += A) > 0;
        A <<= 1;
        B >>= 1;

        flag = ((B & 1) > 0) && (ans += A) > 0;
        A <<= 1;
        B >>= 1;

        flag = ((B & 1) > 0) && (ans += A) > 0;
        A <<= 1;
        B >>= 1;

        flag = ((B & 1) > 0) && (ans += A) > 0;
        A <<= 1;
        B >>= 1;

        flag = ((B & 1) > 0) && (ans += A) > 0;
        A <<= 1;
        B >>= 1;

        flag = ((B & 1) > 0) && (ans += A) > 0;
        A <<= 1;
        B >>= 1;

        flag = ((B & 1) > 0) && (ans += A) > 0;
        A <<= 1;
        B >>= 1;

        flag = ((B & 1) > 0) && (ans += A) > 0;
        A <<= 1;
        B >>= 1;

        flag = ((B & 1) > 0) && (ans += A) > 0;
        A <<= 1;
        B >>= 1;

        flag = ((B & 1) > 0) && (ans += A) > 0;
        A <<= 1;
        B >>= 1;

        return ans >> 1;
    }
}

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/qiu-12n-lcof/solution/qiu-12n-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  • 递归+与运算终止:
class Solution:
    def sumNums(self, n):
        if n == 1: return 1
        n += self.sumNums(n - 1)
        return n

剑指 Offer 65. 不用加减乘除做加法

题目

在这里插入图片描述

思路

  • 这题位运算还是背下来吧,毕竟位运算这种模拟加法用法基本就这题,很容易就忘掉。以下解释来源于Leetcode评论区@端粒和题解区@Krahets:

    • ^ 亦或:相当于 无进位的求和, 想象10进制下的模拟情况:(如:19+1=20;无进位求和就是10,而非20;因为它不管进位情况)

    • & 与:相当于求每位的进位数, 先看定义:1&1=1;1&0=0;0&0=0;即都为1的时候才为1,正好可以模拟进位数的情况,还是想象10进制下模拟情况:(9+1=10,如果是用&的思路来处理,则9+1得到的进位数为1,而不是10,所以要用<<1向左再移动一位,这样就变为10了);

    这样公式就是:(a^b) ^ ((a&b)<<1), 即:每次无进位求 + 每次得到的进位数,我们需要不断重复这个过程,直到进位数为0为止

  • 注意python没有变量位数的概念,有无限位,在python中负数也是以补码的形式存储,因此负数的高位无限补1,正数的高位无限补0,如果使用while循环至b为0会一直运算。所以我们需要进行两个事情:

    • 在运算前,把负数的补码转换成高位为0的变换形式:将数字与十六进制数 0xffffffff 相与。可理解为舍去此数字 32 位以上的数字(将 32 位以上都变为 0 ),从无限长度变为一个 32 位整数
    • 在运算后,把负数的补码还原成高位为1的原始形式:若补码 a 为负数( 0x7fffffff 是最大的正数的补码 ),需执行 ~(a ^ x) 操作,将补码还原至 Python 的存储格式。 a ^ x 运算将 1 至 32 位按位取反; ~ 运算是将整个数字取反;因此, ~(a ^ x) 是将 32 位以上的位取反,1 至 32 位不变。

题解

class Solution:
    def add(self, a: int, b: int) -> int:
        x = 0xffffffff
        a, b = a & x, b & x
        while b != 0:
            a, b = (a ^ b), (a & b) << 1 & x
        return a if a <= 0x7fffffff else ~(a ^ x)

作者:jyd
链接:https://leetcode-cn.com/problems/bu-yong-jia-jian-cheng-chu-zuo-jia-fa-lcof/solution/mian-shi-ti-65-bu-yong-jia-jian-cheng-chu-zuo-ji-7/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

剑指 Offer 66. 构建乘积数组(中等)

题目

在这里插入图片描述

思路

经典题目,看图:
在这里插入图片描述
构建两个dp即可,一个是从前向后的累乘积,另一个是从后向前的累乘积,然后对每个下标遍历,左边乘右边即为答案

题解

  • 动态规划未优化版:
class Solution:
    def constructArr(self, a: List[int]) -> List[int]:
        n = len(a)
        l2r,r2l = [1]*n,[1]*n
        for i in range(n-1): l2r[i+1] = l2r[i]*a[i]
        for i in range(n-1,0,-1): r2l[i-1] = r2l[i]*a[i]

        res = []
        for i in range(n):res.append(l2r[i]*r2l[i])
        return res
  • 动态规划优化版:第三次的for循环可以和上面任意一个for循环合并,一边dp(直接省成一个变量拉)一边出结果
class Solution:
    def constructArr(self, a: List[int]) -> List[int]:
        n = len(a)
        l2r, res, tmp = [1]*n, [1]*n, 1
        for i in range(n-1): l2r[i+1] = l2r[i]*a[i]
        for i in range(n-1,-1,-1):
            res[i] = l2r[i]*tmp 
            tmp *= a[i]

        return res
  • 动态规划终极优化版:res[i]只用到l2r[i],那我直接在l2r上乘就好了,下面的代码统一使用res表示这个数组
class Solution:
    def constructArr(self, a: List[int]) -> List[int]:
        n = len(a)
        res, tmp = [1]*n, 1
        for i in range(n-1): res[i+1] = res[i]*a[i]
        for i in range(n-1,-1,-1):
            res[i] = res[i]*tmp 
            tmp *= a[i]

        return res

剑指 Offer 67. 把字符串转换成整数(中等)

题目

在这里插入图片描述

思路

  • 状态机:下面是官方给的图解,状态有0起始,1负号,2数字,3终止。转移条件有空格,正负号,数字,其它
    在这里插入图片描述
  • 注意对越界数字的处理:python中没有这一说,直接比较大小即可。但是如果严谨来分析,需要使用提前判断,即在这里插入图片描述

题解

  • 状态机:
INT_MAX = 2**31-1
INT_MIN = -2**31
class Solution:
    def strToInt(self, str: str) -> int:
        states = [
            {' ':0,'x':4,'s':1,'d':2},#0
            {'d':2,'x':3,' ':3,'s':3},#1
            {'d':2,'x':3,' ':3,'s':3},#2
            {'d':3,'x':3,' ':3,'s':3},#3
        ]

        flag,ans,cur = 1,0,0
        for c in str:
            if c == ' ':t=' '
            elif c in '+-':t='s'
            elif c.isdigit():t='d'
            else:t = 'x'
            # 添加一种不存在的状态代表从开始碰到字母,提前结束循环
            if cur==4: return ans
            cur = states[cur][t] 
            if cur == 2:
                ans = 10*ans + int(c)
                ans = min(INT_MAX,ans) if flag == 1 else min(-INT_MIN,ans)
            elif cur == 1:
                flag = 1 if c == '+' else -1
        return flag * ans

  • 顺序判断:因为题目状态较为简单,可以直接顺序执行写出下面的逻辑判断
INT_MAX = 2**31-1
INT_MIN = -2**31
# 提前判断
BNDRY = 2**31 // 10
class Solution:
    def strToInt(self, str: str) -> int:
        res, i, sign, length = 0, 0, 1, len(str)
        if not str: return 0
        while str[i] == ' ':
            i += 1
            # 防止空字符串的情况后续访问越界
            if i == length: return 0
        if str[i] == '-': sign = -1
        if str[i] in '+-': i += 1
        for c in str[i:]:
            if not '0' <= c <= '9' : break
            if res > BNDRY or res == BNDRY and c > '7':
                return INT_MAX if sign == 1 else INT_MIN
            # 如何从字符拼接成数字:利用乘法和ascii码的比较
            res = 10 * res + ord(c) - ord('0')
        return sign * res
        

剑指 Offer 68 - I. 二叉搜索树的最近公共祖先

题目

在这里插入图片描述

思路

线索二叉树,通过比较结点的大小进行迭代or递归寻找即可

题解

  • 迭代(遍历查找):
class Solution:
    def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
        # 直接使用root移动,无须新建结点
        # 可以先来一步判断+交换让q和p保持大小顺序一定(比如p一直大于q),下面判断更简练
        while root:
            if root.val < p.val and root.val < q.val:
                root = root.right
            elif root.val > p.val and root.val > q.val:
                root = root.left
            else: break
        return root
  • 递归:
class Solution:
    def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
        if root.val < p.val and root.val < q.val:
            return self.lowestCommonAncestor(root.right, p, q)
        if root.val > p.val and root.val > q.val:
            return self.lowestCommonAncestor(root.left, p, q)
        return root

剑指 Offer 68 - II. 二叉树的最近公共祖先

题目

在这里插入图片描述

分析

  • 当然可以选择dfs递归+存储路径,但是这个方法太笨了,且dfs在前面已经练过n遍了,不再给出咯
  • 下面我们考察一个结点的不同情况:
    1. 若树里面存在p,也存在q,则返回他们的公共祖先。
    2. 若树里面只存在p,或只存在q,则返回存在的那一个。
    3. 若树里面即不存在p,也不存在q,则返回null。

题解

  • 递归:
class Solution:
    def lowestCommonAncestor(self, root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode:
        if not root or root == p or root == q: return root
        left = self.lowestCommonAncestor(root.left, p, q)
        right = self.lowestCommonAncestor(root.right, p, q)
        if not left: return right
        if not right: return left
        # 左右都有的情况,递归的性质决定了最深层的root会层层返回
        return root
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值