2021-10-11 剑指offer2:25~36题目+思路+多种题解
- 写在前面
- 剑指 Offer 25. 合并两个排序的链表
- 剑指 Offer 26. 树的子结构(中等)
- 剑指 Offer 27. 二叉树的镜像
- 剑指 Offer 28. 对称的二叉树
- 剑指 Offer 29. 顺时针打印矩阵
- 剑指 Offer 30. 包含min函数的栈
- 剑指 Offer 31. 栈的压入、弹出序列(中等)
- 剑指 Offer 32 - I. 从上到下打印二叉树(中等)
- 剑指 Offer 32 - II. 从上到下打印二叉树 II
- 剑指 Offer 32 - III. 从上到下打印二叉树 III(中等)
- 剑指 Offer 33. 二叉搜索树的后序遍历序列(中等)
- 剑指 Offer 34. 二叉树中和为某一值的路径(中等)
- 剑指 Offer 35. 复杂链表的复制(中等)
- 剑指 Offer 36. 二叉搜索树与双向链表(中等)
写在前面
本文是采用python为编程语言,作者自行练习使用,题目列表为:剑指 Offer(第 2 版),未使用实体书,难度未标注的均为“简单”,我也不是很清楚为什么有几个编号没有提供。“《剑指 Offer(第 2 版)》通行全球的程序员经典面试秘籍。剖析典型的编程面试题,系统整理基础知识、代码质量、解题思路、优化效率和综合能力这 5 个面试要点。”,本文中的思路来源于每道题目中的题解部分,争取提供全面,优化后的题解,其中所有代码已通过题目检验。
剑指 Offer 25. 合并两个排序的链表
题目
思路
- 简单题还是可以重拳出击的…双指针方法即可,迭代实现or递归实现
题解
- 双指针(此处使用迭代实现):
class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
p1, p2 = l1, l2
# 增加头节点,而不是在循环外再写一个判断
head = ListNode()
p = head
while(p1 and p2):
if (p1.val<=p2.val):
p.next = p1
p1 = p1.next
else:
p.next = p2
p2 = p2.next
p = p.next
# 对剩余元素做处理
if p1:
p.next = p1
else:
p.next = p2
return head.next
- 双指针(此处使用递归实现):注意递归是自外向内求解,自内向外返回后存储答案,但按照执行顺序(自外向内)进行连接!
class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
if not l1:
return l2
if not l2:
return l1
cur = None
if l1.val < l2.val:
cur = l1
cur.next = self.mergeTwoLists(l1.next, l2)
else:
cur = l2
cur.next = self.mergeTwoLists(l1, l2.next)
return cur
剑指 Offer 26. 树的子结构(中等)
题目
思路
- 可以将问题视为两个子问题:
- 找到A中和B的根节点相同的节点
- 遍历这两个节点下的节点,直到:
- A和B相同位置的节点值不同
- 遍历完B树,此时所有节点都满足条件,返回True
- 遍历完A树,此时B树仍有剩余节点,返回False
- 针对以上两个子问题,具体的解决思路为:使用递归访问每个节点,如果相同则进行dfs验证,否则进行递归子树寻找相同节点,注意递归的返回,只要有一个子树符合,即递归的返回true
题解
- dfs未改进版:
class Solution:
def isSubStructure(self, A: TreeNode, B: TreeNode) -> bool:
def dfs(A:TreeNode,B:TreeNode):
if not B:
return True
if not A:
return False
return A.val == B.val and dfs(A.left,B.left) and dfs(A.right, B.right)
if not A or not B:
return False
if A.val == B.val and dfs(A, B):
return True
bool_l = self.isSubStructure(A.left, B)
bool_r = self.isSubStructure(A.right, B)
return bool_l or bool_r
- 代码改进:
class Solution:
# 在类内定义,好像会更快(原因未知)
def dfs(self,A,B):
if not B:return True
if not A:return False
return A.val==B.val and self.dfs(A.left,B.left) and self.dfs(A.right,B.right)
# 使用or连接,不用完成所有递归
def isSubStructure(self,A:TreeNode,B:TreeNode)->bool:
if not A or not B:return False
return self.dfs(A,B) or self.isSubStructure(A.left,B) or self.isSubStructure(A.right,B)
剑指 Offer 27. 二叉树的镜像
题目
思路
- 经典dfs和其两种实现方式:
- 递归:访问节点,然后交换,再访问子节点,直到None
- 非递归:使用栈存储,栈非空时进行操作
题解
- 递归:
class Solution:
def mirrorTree(self, root: TreeNode) -> TreeNode:
if not root: return
root.left, root.right = self.mirrorTree(root.right), self.mirrorTree(root.left)
return root
- 非递归,借助栈:
class Solution:
def mirrorTree(self, root: TreeNode) -> TreeNode:
if not root: return
stack = []
stack.append(root)
while stack:
node = stack.pop()
if node.left: stack.append(node.left)
if node.right: stack.append(node.right)
node.left, node.right = node.right, node.left
return root
剑指 Offer 28. 对称的二叉树
题目
思路
- 老样子的dfs,递归很简单,看题解即可,非递归的要注意,此时的dfs是针对左右两边的节点,所以我们要存入的是一个节点对!
题解
- 递归(练了这好几个,属实是明白了):
class Solution:
def isSymmetric(self, root: TreeNode) -> bool:
def issym(l:TreeNode, r:TreeNode):
if not l and not r:
return True
if not l or not r or l.val != r.val:
return False
return issym(l.left, r.right) and issym(l.right, r.left)
return issym(root.left, root.right) if root else True
- 非递归(栈实现):
class Solution:
def isSymmetric(self, root: TreeNode) -> bool:
if not root:
return True
stack = []
stack.append((root.right,root.left))
while(stack):
l,r = stack.pop()
if not l and not r:
# 并未完成搜索,必须搜索全部,而不是“有一个即可”
continue
if not l or not r or l.val!= r.val:
return False
stack.append((l.right,r.left))
stack.append((r.right,l.left))
return True
剑指 Offer 29. 顺时针打印矩阵
题目
思路
- 按照路径模拟的顺序,第一反应是,例如边长为4的方阵,每个边都访问3个,以完成该周的打印,但这就成了找规律问题,比较费脑。但大思路是确定了的,下一步是寻找终止条件,可以使用计数做法 or 边界限定,即将上下左右访问的边界使用四个变量表示,进行循环的访问,更新,直到边界重合
- 同样的思路,也可以使用状态机模仿转弯,其本质仍是边界的改变
- 矩阵旋转:访问一行,去掉一行,将剩下的矩阵进行转置,然后取转置后第一排,不断循环直到访问完所有元素
题解
- 路径模拟(边界限定):
class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
if not matrix:
return []
res = []
up, down, left, right = 0, len(matrix), 0, len(matrix[0])
while up <= down and left <= right:
#左至右
for i in range(left, right):
res.append(matrix[up][i])
up += 1
if up > down - 1: break
#上至下
for i in range(up, down):
res.append(matrix[i][right - 1])
right -= 1
if left > right - 1: break
#右至左
for i in range(right - 1, left - 1, -1):
res.append(matrix[down - 1][i])
down -= 1
if up > down - 1: break
#下至上
for i in range(down - 1, up - 1, -1):
res.append(matrix[i][left])
left += 1
if left > right - 1: break
return res
- 矩阵旋转(非常漂亮的代码😭不是我写的):其中
zip(*matrix)
的作用是,先将matrix外面的[]
去掉,否则zip的作用对象就是列表中的元素(每行),再对去掉[]
中的元素进行两两配对(每列)
class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
res = []
while matrix:
res += matrix.pop(0)
matrix = list(zip(*matrix))[::-1]
return res
作者:xiao-ma-nong-25
链接:https://leetcode-cn.com/problems/shun-shi-zhen-da-yin-ju-zhen-lcof/solution/shan-chu-di-yi-xing-ni-shi-zhen-xuan-zhuan-python5/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
剑指 Offer 30. 包含min函数的栈
题目
思路
- 初始化两个栈,一个记录当前的最小值,即可直接取出,最小值栈跟随栈的插入和弹出而更新
题解
class MinStack:
def __init__(self):
self.stack, self.minstack = [], []
def push(self, x: int) -> None:
self.stack.append(x)
# 等号的意义是防止重复插入的“最小值”在pop时被提前弹出
if not self.minstack or x <= self.minstack[-1]:
self.minstack.append(x)
def pop(self) -> None:
if self.stack.pop()==self.minstack[-1]:
self.minstack.pop()
def top(self) -> int:
return self.stack[-1] if self.stack else None
def min(self) -> int:
return self.minstack[-1] if self.minstack else None
剑指 Offer 31. 栈的压入、弹出序列(中等)
题目
思路
- 模拟一个栈,每次入栈后都进行检查:如果可以通过出栈达到目标序列的当前位置,则弹栈(或可以直接不加入,这样会更快,不过代码中需要多写一个分支),否则入栈。最后检查栈是否为空即可,为空即为全部弹出(或未加入)。
题解
class Solution:
def validateStackSequences(self, pushed: List[int], popped: List[int]) -> bool:
stack = []
index = 0
for num in pushed:
if not stack or num != stack[-1]:
stack.append(num)
while stack and stack[-1] == popped[index]:
stack.pop()
index += 1
return not stack
剑指 Offer 32 - I. 从上到下打印二叉树(中等)
题目
思路
- BFS的访问(递归方法):按理说BFS是不存在递归的,但是有一种很巧妙的写法,即加入参数“层数”,将原本深度递归中的(左根右)限制在同一层,最后再按照层累加起来,就相当于层次的BFS啦
- BFS常规写法(使用队列)
题解
- 递归方法:
class Solution:
def levelOrder(self, root: TreeNode) -> List[int]:
res = []
def helper(root, level):
if not root:
return
if level == len(res):
res.append([])
res[level].append(root.val)
helper(root.left, level + 1)
helper(root.right, level + 1)
helper(root, 0)
out = []
for _ in res:
out += _
return out
- 常规BFS(队列):
class Solution:
def levelOrder(self, root: TreeNode) -> List[int]:
if not root:
return []
res, queue = [],[]
queue.append(root)
while queue:
node = queue.pop(0)
res.append(node.val)
if node.left: queue.append(node.left)
if node.right: queue.append(node.right)
return res
剑指 Offer 32 - II. 从上到下打印二叉树 II
题目
思路
有了上面的题做铺垫,这个题相对简单,只要注意难点在于,非递归方法中,怎么区分层次,使用for _ in range(len(queue))
,原理如下图:
题解
- 递归方法:
class Solution:
def levelOrder(self, root: TreeNode) -> List[int]:
res = []
def helper(root, level):
if not root:
return
if level == len(res):
res.append([])
res[level].append(root.val)
helper(root.left, level + 1)
helper(root.right, level + 1)
helper(root, 0)
return res
- 常规BFS(队列):
class Solution:
def levelOrder(self, root: TreeNode) -> List[int]:
if not root:
return []
res, queue = [],[]
queue.append(root)
# queue只是为了记录后续访问的节点,tem和res才是真正的答案,也就是需要分层的
while queue:
tem = []
for _ in range(len(queue)):
node = queue.pop(0)
tem.append(node.val)
if node.left: queue.append(node.left)
if node.right: queue.append(node.right)
res.append(tem)
return res
剑指 Offer 32 - III. 从上到下打印二叉树 III(中等)
题目
思路
变来变去的,无非是增加判断条件/不同的操作,一共提供了3种思路,因为太相似了,后面仅实现一种:1. 逻辑分离(在队列中处理,遇到偶数层从右向左加入tmp)2.增加for循环,左右交替分别处理 3. 加入tmp时仍按照原顺序,合并tmp时倒序插入res(也是下文题解给出的方法)
题解
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
if not root: return []
res, queue = [], collections.deque()
queue.append(root)
while queue:
tmp = []
for _ in range(len(queue)):
node = queue.popleft()
tmp.append(node.val)
if node.left: queue.append(node.left)
if node.right: queue.append(node.right)
res.append(tmp[::-1] if len(res) % 2 else tmp)
return res
剑指 Offer 33. 二叉搜索树的后序遍历序列(中等)
题目
分析
猛一看还以为考察后续遍历…开始写函数发现给的是列表,让判断是不是某个树的后续遍历,好吧
- 递归:后序遍历满足“左 右 中”,也就是“小 大 中”,所以我们检查整个序列和分开后的子序列是否满足这样的pattern即可(即小的部分都小于最后一个元素,大的部分都大于最后一个元素)
- 栈:所有的递归都可以变成栈,只不过有的好理解有的不好理解。这里使用栈的意义是用来判断“小”的部分和根的大小关系,栈来记录所有经过的节点,从中找到该比较的
root
,大于自身且最接近的。所以相应的,需要从右向左记录,以维护root
。
题解
- 递归:也可以使用mid记录递归位置,继续对满足要求的ind++,返回值中加入ind==end判定条件
class Solution:
def verifyPostorder(self, postorder: List[int]) -> bool:
#如果为空,返回True
if not postorder:
return True
n = len(postorder)
#寻找左子树的根节点
ind = 0
while ind<n and postorder[ind]<postorder[-1]:
ind +=1
#验证右子树的节点是否符合都大于根节点的要求
for i in range(ind,n-1):
if postorder[i]<postorder[-1]:
return False
#继续递归,分治左右子树,判断是否正确
return self.verifyPostorder(postorder[:ind]) and self.verifyPostorder(postorder[ind:n-1])
- 非递归(栈):
class Solution:
def verifyPostorder(self, postorder: [int]) -> bool:
# 只有最开始的情况会出现“大”的部分>“根”,故此时将“根”设为inf。只有比较“小”的部分,才用到pop出的“根”。
stack, root = [], float("+inf")
for i in range(len(postorder) - 1, -1, -1):
if postorder[i] > root: return False
# 该栈是一个单调栈,即越来越大,所以一直pop,最后得到的就是比自己大(第二个限定条件),且最接近自己的
while(stack and postorder[i] < stack[-1]):
root = stack.pop()
# 入栈是因为自身也有可能做root
stack.append(postorder[i])
return True
剑指 Offer 34. 二叉树中和为某一值的路径(中等)
题目
分析
- DFS:
- 递归:终止条件有两个,一是到达叶子结点,二是满足条件,使用一个列表(栈)记录来路,所以每次递归的归时,需要pop出最外层刚加入的结点
- 迭代(栈)
- 需要注意的是使用直接赋值or深拷贝or浅拷贝:
- 直接赋值。如
a=b
,a.append(b)
都是对原对象进行操作,相应的,一个变另一个也变。 - 浅拷贝,指的是重新分配一块内存,创建一个新的对象,但里面的元素是原对象中各个子对象的引用。python中这两种方式都是浅拷贝:
a = list(b)
,b[:]
。 - 所谓深拷贝,是指重新分配一块内存,创建一个新的对象,并且将原对象中的元素,以递归的方式,通过创建新的子对象拷贝到新对象中。因此,新对象和原对象没有任何关联,使用
deepcopy
方法进行深拷贝。
- 直接赋值。如
题解
- 递归:
class Solution:
def pathSum(self, root: TreeNode, target: int) -> List[List[int]]:
def dfs(node, sum):
if not node:return
tmp.append(node.val)
sum += node.val
if sum == target and not node.left and not node.right:
# 这里一定要使用“浅拷贝”进行复制!不要赋值(直接append)
res.append(tmp[:])
dfs(node.left , sum)
dfs(node.right, sum)
# 实际pop的是当前结点(自己的左子树右子树都已经递归完成),而迭代的方法还未使用就给pop了,显然不可以
tmp.pop()
res , tmp = [] , []
dfs(root , 0)
return res
- 栈:
class Solution:
def pathSum(self, root: TreeNode, target: int) -> List[List[int]]:
if not root:return []
# 栈中存储的是:(当前节点,sum值,路径)
# 必须加入路径这个信息,因为栈中存储的是所有访问过的结点,而不是dfs的某一条路
# 每层加入的个数不同,也并不知道从哪里开始出现了分叉(或回退到哪一步)
res, stack = [], [(root, root.val, [root.val])]
while stack:
cur, sum, path= stack.pop()
if sum == target and not cur.left and not cur.right:
res.append(path)
if cur.left:
stack.append((cur.left, sum+cur.left.val, path+[cur.left.val]))
if cur.right:
stack.append((cur.right, sum+cur.right.val, path+[cur.right.val]))
return res
剑指 Offer 35. 复杂链表的复制(中等)
题目
分析
构建并没有什么难度,重点是关注“如何快速的找到random”:
- 先复制,再连接。使用hash表建立映射,在*O(n)*内查找,也可以使用递归的方法,一边存储一边返回(连接)。
- 先构建“旧节点-新节点”的一个大链表,对于每一个旧链表,再通过一遍循环,将新链表指向
random.next
,最后第三遍循环进行拆分。
题解
- hash连接(先存储,再取值)
class Solution:
def copyRandomList(self, head: 'Node') -> 'Node':
if not head: return
dic = {}
# 复制各节点,并建立 “原节点 -> 新节点” 的 Map 映射
cur = head
while cur:
dic[cur] = Node(cur.val)
cur = cur.next
cur = head
# 构建新节点的 next 和 random 指向
while cur:
dic[cur].next = dic.get(cur.next)
dic[cur].random = dic.get(cur.random)
cur = cur.next
return dic[head]
- hash连接(递归):
class Solution:
dict= {}
def copyRandomList(self, head: 'Node') -> 'Node':
if not head:
return None
if not self.dict.get(head):
new_head = Node(head.val)
self.dict[head] = new_head
new_head.next = self.copyRandomList(head.next)
new_head.random = self.copyRandomList(head.random)
return self.dict[head]
- “双兔傍地走”:可以将额外空间降为O(1),因为返回答案不计入额外空间:
class Solution:
def copyRandomList(self, head: 'Node') -> 'Node':
if not head: return
cur = head
# 复制各节点,并构建拼接链表
while cur:
newnode = Node(cur.val)
newnode.next = cur.next
cur.next = newnode
cur = newnode.next
# 构建各新节点的 random 指向
cur = head
while cur:
if cur.random:
cur.next.random = cur.random.next
cur = cur.next.next
# 拆分两链表
cur = res = head.next
pre = head
while cur.next:
pre.next = pre.next.next
cur.next = cur.next.next
pre = pre.next
cur = cur.next
pre.next = None # 单独处理原链表尾节点
return res # 返回新链表头节点
剑指 Offer 36. 二叉搜索树与双向链表(中等)
题目
思路
- 即排序二叉树->排序链表,会发现其实要求就是中序遍历并连接!中序遍历的三种方法:
- 递归模板:
def dfs(root): if not root: return dfs(root.left) # 左 print(root.val) # 根 dfs(root.right) # 右
- 非递归(栈)模板:先一气把左节点入栈,然后出栈访问
def dfs(node): stack = [] pos = node # 注意起始条件 while pos is not None or len(stack) > 0: if pos is not None: stack.append(pos) pos = pos.left else: pos = stack.pop() print(pos) pos = pos.right
- Morris法模板:这个方法和题解有异曲同工之妙,基本思路就是:将所有右儿子为NULL的节点的右儿子指向后继节点(因为对于右儿子不为空的节点,右儿子就是接下来要访问的节点)
def dfs(root): if not root: return cur = head while(cur): if not cur: print(cur) cur = cur.right mostright = cur.left # 找到左子树的最右边节点,相当于将小于自己的最大的那个和自己连接 while(mostright.right and nostright.right!=cur): mostright = mostright.right if (not mostright.right): # 连接成功 mostright.right = cur cur = cur.left else: print(cur) # mostright.right = null 为了恢复二叉树 cur = cur.right
题解
- 递归实现:
class Solution:
def treeToDoublyList(self, root: 'Node') -> 'Node':
def dfs(cur):
if not cur: return
# 中序遍历,最先操作的是最左侧结点
dfs(cur.left)
# pre和cur互指
if self.pre:
self.pre.right, cur.left = cur, self.pre
else: # 记录头节点
self.head = cur
self.pre = cur
dfs(cur.right)
if not root: return
self.pre = None
dfs(root)
# 返回的头节点的前继为尾结点,尾结点的前继结点为头结点
self.head.left, self.pre.right = self.pre, self.head
return self.head
- 非递归实现:空间击败了99.9%的朋友哈哈哈🤣
class Solution:
def treeToDoublyList(self, root: 'Node') -> 'Node':
if not root: return
self.pre,self.head = None, None
stack, node = [], root
while stack or node:
while(node):
stack.append(node)
node = node.left
node = stack.pop()
if self.pre:
self.pre.right, node.left = node, self.pre
else:
self.head = node
self.pre = node
node = node.right
self.head.left, self.pre.right = self.pre, self.head
return self.head
- mirrors:贴个链接,还没整明白