2021-10-09 剑指offer2:13~24题目+思路+多种题解

94 阅读9分钟

2021-10-09 剑指offer2:13~24题目+思路+多种题解

写在前面

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

剑指 Offer 13. 机器人的运动范围(中等)

题目

在这里插入图片描述

思路

  • 一定要仔细读题😭一开始我以为只要满足数位之和小于k就可以,列了半天的递归方程,而实际上题目存在三个限制条件:1. 一次只能移动一格 2. 满足数位之和 3. 提示中的限制
  • 我的第一个思路是找规律,用数学方法计算,比如i,j都小于10的时候,对每个icnt += min( k+1-i, rows),当i的十位数是num_i的时候,相当于k-num_i带入此式子计算。但写着代码意识到这是个二维的问题,也就是说cnt的值还取决于j,随着每个ij的位数增长,就变成了二重循环。当然,对同样的i的范围,j每增加10,相对减少的cnt也是可以表达的,但是找规律太不优雅了,遂放弃
  • 想找捷径失败,回归最原始的dfs和bfs搜索+剪枝。关于DFS的两种方式可见上一篇文章的最后一题:2021-10-06 剑指offer2:01~12题目+思路+多种题解,而这个题目的关键就是如何把数量统计和dfs的次数联系到一起,实际上是每成功dfs一次,就要加一

题解

  • 递归的DFS+剪枝:看到题解中有一种优化是只比较增量即可,这里没有这么写。
class Solution:

    def movingCount(self, m: int, n: int, k: int) -> int:
        def calnum(x):
            sum = 0
            while x != 0:
                sum += x % 10
                x = x // 10
            return sum

        def dfs(row, col):
            if (row>=m or col>=n) or ((row, col) in visited) or (calnum(row)+calnum(col)>k):
                return 0
            else:
                visited.add((row,col))
                return 1+ dfs(row+1, col)+ dfs(row, col+1)

        visited = set()
        return dfs(0,0)
        
  • 非递归的DFS(栈)+剪枝:
class Solution:

    def movingCount(self, m: int, n: int, k: int) -> int:
        def calnum(x):
            sum = 0
            while x != 0:
                sum += x % 10
                x = x // 10
            return sum

        def dfs(row, col):
            sum = 0
            stack.append((0,0))
            while stack:
                row, col = stack.pop()    
                if ((row, col) in visited) or calnum(row)+calnum(col)>k:
                    continue
                visited.add((row,col))
                sum+=1
                if row+1< m:
                    stack.append((row+1, col))
                if col+1< n:
                    stack.append((row, col+1))
            return sum

        stack = list()
        visited = set()
        return dfs(0,0)
        
  • BFS+剪枝:BFS其实就只是把栈换成队列,这样先放进去的(同一深度的)先被访问
class Solution:

    def movingCount(self, m: int, n: int, k: int) -> int:
        def calnum(x):
            sum = 0
            while x != 0:
                sum += x % 10
                x = x // 10
            return sum

        def dfs(row, col):
            sum = 0
            queue.append((0,0))
            while queue:
                row, col = queue.pop(0)    
                if ((row, col) in visited) or calnum(row)+calnum(col)>k:
                    continue
                visited.add((row,col))
                sum+=1
                if row+1< m:
                    queue.append((row+1, col))
                if col+1< n:
                    queue.append((row, col+1))
            return sum

        queue = list()
        visited = set()
        return dfs(0,0)
        

剑指 Offer 14- I. 剪绳子(中等)

题目

在这里插入图片描述

思路

  • 动态规划:状态转移方程为dp[n] = max( len*dp[n-len] ),即将绳子分成len和n-len两部分,len从0取至n。而针对初始化问题,我们可以使用len*(n-len)来完成,代表了无法再拆的情况。
  • 数论:这个题是可以根据不等式和求导推出切分规则的。。。思路见:这是一个链接。又因为任何大于1的数都可以有2和3构成(奇偶性),最终规律如下图:
    在这里插入图片描述

题解

  • 动态规划:
class Solution:
    def cuttingRope(self, n: int) -> int:
        dp = [0]*(n+1)
        for i in range(2,n+1):
            for len in range(1,i):
                dp[i] = max(dp[i],len*(i-len),len*dp[i-len])
        return dp[n]
        
  • 数论:注意一个小细节:python中的三种幂计算中,*pow()的时间复杂度为O(logn) 。而math.pow()执行浮点取幂,时间复杂度为 O(1),但对于大数存在溢出问题,所以在下一题中如果直接使用python的特性无长度,则需要使用**而非pow()
class Solution:
    def cuttingRope(self, n: int) -> int:
        # 对应(2,1)(3,2)的情况
        if n <= 3:
        	return n - 1
        cnt, loss = n // 3, n % 3
        if loss == 0: 
            return int(math.pow(3, cnt))
        elif loss == 1: 
            return int(math.pow(3, cnt - 1) * 4)
        return int(math.pow(3, cnt) * 2)

剑指 Offer 14- II. 剪绳子 II(中等)

题目

在这里插入图片描述

思路

发现本题和上一题题干几乎完全相同,只增加了一个“取余”的条件,但如果你在最后的结果直接取余。。。发现,只能过去部分范例,这是因为大数取余问题,越界可能发生在每一步而导致结果错误,下面是大数取余的两种常见做法:
在这里插入图片描述
当然,该题也可以延续上一问使用动态规划,这源于python语言的无长度性。

题解

  • 循环取余法:
class Solution:
    def cuttingRope(self, n: int) -> int:
        MOD = 1000000007
        # 对应(2,1)(3,2)的情况
        if n <= 3:
        	return (n - 1)%MOD
        cnt, loss,res = n // 3, n % 3,1
        # 使用循环取余法,注意因loss==1的情况需要除以3
        # 在每一步都取余的情况下可能出现小数,所以循环至cnt-1,拿出最后一个3
        for i in range(cnt-1):
            res = ((res%MOD)*3)%MOD
        if loss == 0:
            return int(res*3%MOD)
        elif loss == 1: 
            return int((res*4)%MOD)
        return int((res*6)%MOD)
        
  • 快速幂法:
class Solution:
    def cuttingRope(self, n: int) -> int:
        MOD = 1000000007
        # 对应(2,1)(3,2)的情况
        if n <= 3:
        	return (n - 1)
        # 使用快速幂法,注意因loss==1的情况需要除以3
        # 在每一步都取余的情况下可能出现小数,所以循环至cnt-1,拿出最后一个3
        cnt, loss, x, res = (n // 3) -1, n % 3, 3, 1
		# 快速幂法的精髓在于,对半分指数,先算3*3=9,9*9=81...直到最后一个
		# 奇数的情况对半除仍是奇数,先乘以一个3即可
        while cnt:
            if cnt % 2:
                res = (res*x) % MOD
            x = x**2 % MOD
            cnt //= 2
            
        if loss == 0:
            return int(res*3%MOD)
        elif loss == 1: 
            return int(res*4%MOD)
        return int((res*6)%MOD)

剑指 Offer 15. 二进制中1的个数

题目

在这里插入图片描述

思路

  • 直接循环检查给定整数 n 的二进制位的每一位是否为 1
  • 观察这个运算:n & (n−1),其预算结果恰为把 n 的二进制位中的最低位的 1 变为 0 之后的结果,所以进行循环直到n中的1全部变成0的次数,即为1的个数。注意,该方法还可以用来判断 n 是否是 2 的幂
    在这里插入图片描述

题解

  • 直接循环:
class Solution:
    def hammingWeight(self, n: int) -> int:
        res = 0
        while n:
            res += n & 1
            n >>= 1
        return res
        
  • 位运算优化:
class Solution:
    def hammingWeight(self, n: int) -> int:
        res = 0
        while n:
            res += 1
            n &= n - 1
        return res
        

剑指 Offer 16. 数值的整数次方(中等)

题目

在这里插入图片描述

思路

题解

class Solution:
    def myPow(self, x: float, n: int) -> float:
        res = 1
        if n<0:
            x,n = 1/x,-n 
        while n:
            if n & 1 :
                res = res * x
            x *= x
            n >>= 1
        return res

剑指 Offer 17. 打印从1到最大的n位数

题目

在这里插入图片描述

思路

看起来很简单,但是这在剑指offer上主要考大数问题,需要考虑以下问题:

  1. 利用String类型表示大数,以防止变量类型的溢出
  2. 如何使用String类型生成数字,尤其是进位问题,利用0~9个数字的递归进行生成
    得出本题考查的是深度递归(实际上n遍for循环效果是一样的),按照位添加0~9,直到满足位数

题解

class Solution:
    def printNumbers(self, n: int):
        def dfs(x):
            if x == n:
                res.append(int(''.join(num)))   # 拼接 num,转换为int,并添加至 res 尾部
                return
            for i in range(0, 10):	# 每一位都要进行一遍0~9的循环,在每个数字的上进行深度递归,0即代表低位的,如009实际上是9 
                num[x] = str(i)					# 只使用一个num即可,因为每次到达n位结束递归都会加入到
                dfs(x + 1)

        num = [''] * n
        res = []
        dfs(0)
        res.pop(0)
        return res

剑指 Offer 18. 删除链表的节点

题目

在这里插入图片描述
补充:1.题目保证链表中节点的值互不相同 2.原题的要求是给了需要删除节点的指针,在 O(1) 的时间复杂度完成操作

思路

  • 链表的删除,注意双指针移动的顺序即可,pre先指向now,再移动now
  • 其实单指针就够了,判定的是now.next.val即可

题解

  • 双指针:
class Solution:
    def deleteNode(self, head: ListNode, val: int) -> ListNode:
        pre, now = None, head
        if head.val == val:
            return head.next
        while now.next and now.val != val:
            pre = now
            now = now.next
        pre.next = now.next
        return head
  • 单指针:
class Solution:
    def deleteNode(self, head: ListNode, val: int) -> ListNode:
        now =  head
        if head.val == val:
            return head.next
        while now.next and now.next.val != val:
            now = now.next
        now.next = now.next.next
        return head

剑指 Offer 19. 正则表达式匹配(困难)

题目

在这里插入图片描述

思路

  • 串的匹配问题,常常是动态规划问题,且转移方程存在于dp[i][j]dp[i-?][j-?]之间:

    • 最基础的情况,一直匹配字母f[i][j]=f[i-1][j-1]如果s[i]=p[i]时,否则变成false
    • 碰到*的情况,将有以下3种情况满足true:

    在这里插入图片描述

题解

真没写出来,这是leetcode用户@Krahets的代码,注意动态规划时的i位对应的是字符串中的第i位,即s[i-1]p[i-1]

class Solution:
    def isMatch(self, s: str, p: str) -> bool:
        m, n = len(s) + 1, len(p) + 1
        dp = [[False] * n for _ in range(m)]
        dp[0][0] = True
        for j in range(2, n, 2):
            dp[0][j] = dp[0][j - 2] and p[j - 1] == '*'
        for i in range(1, m):
            for j in range(1, n):
                dp[i][j] = dp[i][j - 2] or dp[i - 1][j] and (s[i - 1] == p[j - 2] or p[j - 2] == '.') \
                           if p[j - 1] == '*' else \
                           dp[i - 1][j - 1] and (p[j - 1] == '.' or s[i - 1] == p[j - 1])
        return dp[-1][-1]

作者:jyd
链接:https://leetcode-cn.com/problems/zheng-ze-biao-da-shi-pi-pei-lcof/solution/jian-zhi-offer-19-zheng-ze-biao-da-shi-pi-pei-dong/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

剑指 Offer 20. 表示数值的字符串(中等)

题目

在这里插入图片描述

思路

  • 第一思路是使用正则表达式,写出来了一份很不优美的代码,再一步步简化和合并

  • 有限状态机,两步走,最终会得到如下一张图:

    1. 可能遇到的情况列出来
    2. 从一个状态起,可能遇到的情况及该情况下转移的状态写出来,从而得出状态图
      在这里插入图片描述
  • by the way真正做工程的时候,我们一般选择try…catch以防出错(即使这样会慢一些但不常用的话无妨):

class Solution(object):
    def isNumber(self, s):
        try:
            float(s)
        except:
            return False
        return True

题解

  • 正则表达式v1.0:
import re
class Solution:
    def isNumber(self, s: str) -> bool:
        def isdou(s):
            dou_p = re.compile(r' *[+|-]?(\d*)\.(\d*)')
            obj = re.match(dou_p, s)
            if not obj or (not obj.group(1) and not obj.group(2)):
                return -1
            return obj.end()

        def isint(s):
            int_p = re.compile(r' *[+|-]?\d+')
            obj = re.match(int_p, s)
            return obj.end() if obj else -1

        cut = max(isdou(s),isint(s))
        if cut<0:
            return False
        s = s[cut:]
        # 在前面是小数or整数的基础上判断是否是指数
        obj = re.match(r'([E|e][+|-]?\d+)? *$',s)
        if not s or obj and obj.end()==len(s):
            return True
        else:
            return False
  • 正则表达式v2.0(该答案来源于Leetcode用户@无神小坏):
import re
class Solution:
    def isNumber(self, s: str) -> bool:
        obj = re.match( r' *[+-]?([0-9]*\.[0-9]*|[+-]?[0-9]+)([eE][+-]?[0-9]+)? *', s)
        return True if obj and obj.end() == len(s) and obj.group(1) != '.' else False

作者:l1ttle_bad
链接:https://leetcode-cn.com/problems/biao-shi-shu-zhi-de-zi-fu-chuan-lcof/solution/python-zheng-ze-biao-da-shi-liang-xing-j-nlwv/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  • 状态转移机:
class Solution:
    def isNumber(self, s: str) -> bool:
    	# 当前状态下,遇到的情况(key值)转移到的状态(value值)
        states = [
            { ' ': 0, 's': 1, 'd': 2, '.': 4 }, # 0. start with 'blank'
            { 'd': 2, '.': 4 } ,                # 1. 'sign' before 'e'
            { 'd': 2, '.': 3, 'e': 5, ' ': 8 }, # 2. 'digit' before 'dot'
            { 'd': 3, 'e': 5, ' ': 8 },         # 3. 'digit' after 'dot'
            { 'd': 3 },                         # 4. 'digit' after 'dot' (‘blank’ before 'dot')
            { 's': 6, 'd': 7 },                 # 5. 'e'
            { 'd': 7 },                         # 6. 'sign' after 'e'
            { 'd': 7, ' ': 8 },                 # 7. 'digit' after 'e'
            { ' ': 8 }                          # 8. end with 'blank'
        ]
        
        p = 0                      
        for c in s:
            if '0' <= c <= '9': t = 'd' 
            elif c in "+-": t = 's'
            elif c in "eE": t = 'e' 
            elif c in ". ": t = c   
            else: t = '?'              
            if t not in states[p]: return False
            p = states[p][t]
        # 满足以下状态才是正确的结尾
        return p in (2, 3, 7, 8)

剑指 Offer 21. 调整数组顺序使奇数位于偶数前面

题目

在这里插入图片描述

思路

  • 辅助数组:遇到奇数左边放,遇到偶数右边放
  • 双指针:可以仿快排,左右检查原地交换;也可以快慢指针,原理是一样的

题解

class Solution:
    def exchange(self, nums: List[int]) -> List[int]:
        # 注意长度要减去1才是最后的角标!!!!!!!!
        left, right = 0, len(nums)-1
        while left<=right:
        	# 采用与运算优化空间
            if nums[left]&1==0 and nums[right]&1==1:
                nums[left], nums[right] = nums[right],nums[left]
                # 别忘记满足条件的也要变化,否则下一次需要重复判断
                left+=1
                right-=1
            elif nums[left]&1==1:
                left+=1
            else:
                right-=1
        return nums

剑指 Offer 22. 链表中倒数第k个节点

题目

在这里插入图片描述

分析

  • 拿到题目的第一个思路就是:

    • 遍历一遍链表,统计出长度n
    • 遍历第二遍,倒数第k个对应的就是正数第n-k个
  • 快慢指针: 快指针先走k步,慢指针开始走。当快指针走到时,慢指针对应的即是答案

题解

class Solution:
    def getKthFromEnd(self, head: ListNode, k: int) -> ListNode:
        first_p = head
        for step in range(1,k):
            first_p = first_p.next
        second_p = head
        while first_p.next:
            first_p = first_p.next
            second_p = second_p.next
        return second_p

剑指 Offer 24. 反转链表

题目

在这里插入图片描述

题解

  • 双指针:遇见while有意识的找一下终止条件!
class Solution:
    def reverseList(self, head: ListNode) -> ListNode:
        prenode = None
        nownode = head
        while nownode:
            nextnode = nownode.next
            nownode.next = prenode
            prenode = nownode
            nownode = nextnode
        return prenode
  • 递推:关键是假设之前的状态已知,然后找终止条件递推公式!关注每次单独递推的细节(比如找终止条件,就关注最后一次递归),不要试图人脑迭代。。。令F(node)为问题:反转以node为头节点的单向链表。

    • 这里假设子问题F(node=2)已经解决,那么我们如何解决F(node=1):
    • 很明显,我们需要反转node=2和node=1, 即 node.next(即第二个,现在指向了null).next=node; 同时 node.next=null;
class Solution:
    def reverseList(self, head: ListNode) -> ListNode:
    	# 终止条件对应的是:输入为空 or 尾结点
        if not head or not head.next:
            return head
        newnode = self.reverseList(head.next)
        head.next.next = head
        head.next = None
        return newnode