2021-10-09 剑指offer2:13~24题目+思路+多种题解
写在前面
本文是采用python为编程语言,作者自行练习使用,题目列表为:剑指 Offer(第 2 版),未使用实体书,难度未标注的均为“简单”,我也不是很清楚为什么有几个编号没有提供。“《剑指 Offer(第 2 版)》通行全球的程序员经典面试秘籍。剖析典型的编程面试题,系统整理基础知识、代码质量、解题思路、优化效率和综合能力这 5 个面试要点。”,本文中的思路来源于每道题目中的题解部分,争取提供全面,优化后的题解,其中所有代码已通过题目检验。
剑指 Offer 13. 机器人的运动范围(中等)
题目
思路
- 一定要仔细读题😭一开始我以为只要满足数位之和小于k就可以,列了半天的递归方程,而实际上题目存在三个限制条件:1. 一次只能移动一格 2. 满足数位之和 3. 提示中的限制
- 我的第一个思路是找规律,用数学方法计算,比如
i,j
都小于10的时候,对每个i
有cnt += min( k+1-i, rows)
,当i的十位数是num_i
的时候,相当于k-num_i
带入此式子计算。但写着代码意识到这是个二维的问题,也就是说cnt
的值还取决于j
,随着每个i
中j
的位数增长,就变成了二重循环。当然,对同样的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. 数值的整数次方(中等)
题目
思路
- 还是快速幂,贴一个K神给出的非常明了的图:
题解
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上主要考大数问题,需要考虑以下问题:
- 利用String类型表示大数,以防止变量类型的溢出
- 如何使用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. 表示数值的字符串(中等)
题目
思路
-
第一思路是使用正则表达式,写出来了一份很不优美的代码,再一步步简化和合并
-
有限状态机,两步走,最终会得到如下一张图:
- 可能遇到的情况列出来
- 从一个状态起,可能遇到的情况及该情况下转移的状态写出来,从而得出状态图
-
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