2021-10-11 剑指offer2:25~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的根节点相同的节点
    • 遍历这两个节点下的节点,直到:
      1. A和B相同位置的节点值不同
      2. 遍历完B树,此时所有节点都满足条件,返回True
      3. 遍历完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=ba.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
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值