Leetcode 5-5

LCR 124. 推理二叉树(从前序与中序遍历序列构造二叉树)

中等

某二叉树的先序遍历结果记录于整数数组 preorder,它的中序遍历结果记录于整数数组 inorder。请根据 preorderinorder 的提示构造出这棵二叉树并返回其根节点。

注意:preorderinorder 中均不含重复数字。

示例 1:

img

输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]

输出: [3,9,20,null,null,15,7]

示例 2:

输入: preorder = [-1], inorder = [-1]

输出: [-1]

提示:

  • 1 <= preorder.length <= 3000
  • inorder.length == preorder.length
  • -3000 <= preorder[i], inorder[i] <= 3000
  • inorder 均出现在 preorder
  • preorder 保证 为二叉树的前序遍历序列
  • inorder 保证 为二叉树的中序遍历序列

注意:本题与主站 105 题重复:https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/

超高效写法(哈希)

1. 避免了频繁的数组切片操作
  • 第一个方法中使用了大量的数组切片(如 preorder[1:1+left_length], inorder[:left_length] 等),每次切片都会创建新的子数组,这在时间和空间上都有开销。
  • 第二个方法使用索引范围(leftright)来标记当前子树在 inorder 数组中的位置,避免了数组切片的开销。
2. 预处理了 inorder 的索引映射
  • 第一个方法每次查找根节点在 inorder 中的位置时,都要调用 inorder.index(root_val),时间复杂度是 O(n),最坏情况下整个算法的时间复杂度会达到 O(n²)(例如当树退化成链表时)。
  • 第二个方法预先构建了一个 inorder_map(哈希表),存储了每个值对应的索引,这样每次查找只需要 O(1) 时间,使得整个算法的时间复杂度降低到 O(n)
3. 使用 pre_idx 全局指针代替 preorder 切片
  • 第一个方法递归时传递的是 preorder 的子数组,这需要额外的空间和时间。
  • 第二个方法维护一个 pre_idx 指针,按 preorder 的顺序逐个处理节点,避免了传递子数组的开销。
4. 空间复杂度优化
  • 第一个方法由于频繁切片,最坏情况下空间复杂度是 O(n²)(递归栈 + 切片存储)。
  • 第二个方法的空间复杂度是 O(n)inorder_map 存储 + 递归栈深度)。
总结
优化点方法1(原始方法)方法2(优化方法)
数组切片频繁切片,O(n²) 时间无切片,O(1) 时间
索引查找每次 inorder.index(),O(n)哈希表查找,O(1)
空间使用递归+切片存储,O(n²)仅递归栈+哈希表,O(n)
总时间复杂度O(n²)O(n)
总空间复杂度O(n²)O(n)
class Solution:
    def deduceTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
        inorder_map = {val: idx for idx, val in enumerate(inorder)}
        pre_idx = 0
        
        def helper(left, right):
            nonlocal pre_idx
            if left > right:
                return None
            
            root_val = preorder[pre_idx]
            root = TreeNode(root_val)
            pre_idx += 1
            
            root.left = helper(left, inorder_map[root_val] - 1)
            root.right = helper(inorder_map[root_val] + 1, right)
            
            return root
        
        return helper(0, len(inorder) - 1)

普通递归写法

class Solution:
    def deduceTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
        n = len(preorder)
        if n == 0: return None
        if n == 1: return TreeNode(preorder[0])
        root = TreeNode(preorder[0])
        index = inorder.index(root.val)
        root.left = self.deduceTree(preorder[1:index+1], inorder[:index])
        root.right = self.deduceTree(preorder[index+1:], inorder[index+1:])
        return root

nextpage

147. 对链表进行插入排序

中等

给定单个链表的头 head ,使用 插入排序 对链表进行排序,并返回 排序后链表的头

插入排序 算法的步骤:

  1. 插入排序是迭代的,每次只移动一个元素,直到所有元素可以形成一个有序的输出列表。
  2. 每次迭代中,插入排序只从输入数据中移除一个待排序的元素,找到它在序列中适当的位置,并将其插入。
  3. 重复直到所有输入数据插入完为止。

下面是插入排序算法的一个图形示例。部分排序的列表(黑色)最初只包含列表中的第一个元素。每次迭代时,从输入数据中删除一个元素(红色),并就地插入已排序的列表中。

对链表进行插入排序。

img

示例 1:

img

输入: head = [4,2,1,3]
输出: [1,2,3,4]

示例 2:

img

输入: head = [-1,5,3,4,0]
输出: [-1,0,3,4,5]

*提示:

  • 列表中的节点数在 [1, 5000]范围内
  • -5000 <= Node.val <= 5000

插入排序

class Solution:
    def insertionSortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        # 第一大罪:我没有处理边界情况
        if not head or not head.next:
            return head

        # 第二大罪:我没有使用dummy哨兵节点,所以head还需要重置
        dummy = ListNode(0)  # 使用 dummy 节点简化头部插入
        dummy.next = head
        iterator_pre = head
        iterator = head.next

        while iterator:
            # 从头开始找插入位置
            cur = dummy  # 从 dummy 开始遍历,方便处理头部插入
            while cur.next != iterator:
                if iterator.val < cur.next.val:
                    iterator_pre.next = iterator.next  # 先摘除 iterator
                    iterator.next = cur.next  # 插入 iterator
                    # 第三大罪:只顾后链接不顾前链接
                    cur.next = iterator
                    iterator = iterator_pre.next
                    break
                cur = cur.next
            else:
                # 如果 iterator 不需要移动,则直接后移
                iterator_pre = iterator
                iterator = iterator.next

        return dummy.next

nextpage

206. 反转链表

简单

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例 1:

img

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

示例 2:

img

输入:head = [1,2]
输出:[2,1]

示例 3:

输入:head = []
输出:[]

提示:

  • 链表中节点的数目范围是 [0, 5000]
  • -5000 <= Node.val <= 5000

**进阶:**链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?

迭代法

class Solution:
    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        dummy = ListNode()
        dummy.next = head
        pre = None
        cur = dummy.next
        
        while cur:
            nxt = cur.next
            cur.next = pre
            pre = cur
            cur = nxt
        
        return pre

nextpage

790. 多米诺和托米诺平铺

中等

有两种形状的瓷砖:一种是 2 x 1 的多米诺形,另一种是形如 "L" 的托米诺形。两种形状都可以旋转。

img

给定整数 n ,返回可以平铺 2 x n 的面板的方法的数量。返回对 109 + 7 取模 的值。

平铺指的是每个正方形都必须有瓷砖覆盖。两个平铺不同,当且仅当面板上有四个方向上的相邻单元中的两个,使得恰好有一个平铺有一个瓷砖占据两个正方形。

示例 1:

img

输入: n = 3
输出: 5
解释: 五种不同的方法如上所示。

示例 2:

输入: n = 1
输出: 1

提示:

  • 1 <= n <= 1000

方法一:动态规划

class Solution:
    def numTilings(self, n: int) -> int:
        MOD = 10 ** 9 + 7
        dp = [[0] * 4 for _ in range(n + 1)]
        dp[0][3] = 1
        for i in range(1, n + 1):
            dp[i][0] = dp[i - 1][3]
            dp[i][1] = (dp[i - 1][0] + dp[i - 1][2]) % MOD
            dp[i][2] = (dp[i - 1][0] + dp[i - 1][1]) % MOD
            dp[i][3] = (((dp[i - 1][0] + dp[i - 1][1]) % MOD + dp[i - 1][2]) % MOD + dp[i - 1][3]) % MOD
        return dp[n][3]

fig1

方法二:矩阵快速幂

class Solution:
    def numTilings(self, n: int) -> int:
        MOD = 10 ** 9 + 7

        def multiply(a: List[List[int]], b: List[List[int]]) -> List[List[int]]:
            rows, columns, temp = len(a), len(b[0]), len(b)
            c = [[0] * columns for _ in range(rows)]
            for i in range(rows):
                for j in range(columns):
                    for k in range(temp):
                        c[i][j] = (c[i][j] + a[i][k] * b[k][j]) % MOD
            return c

        def matrixPow(mat: List[List[int]], n: int) -> List[List[int]]:
            ret = [
                [1, 0, 0, 0],
                [0, 1, 0, 0],
                [0, 0, 1, 0],
                [0, 0, 0, 1],
            ]
            while n:
                if n & 1:
                    ret = multiply(ret, mat)
                n >>= 1
                mat = multiply(mat, mat)
            return ret

        mat = [
            [0, 0, 0, 1],
            [1, 0, 1, 0],
            [1, 1, 0, 0],
            [1, 1, 1, 1],
        ]
        res = matrixPow(mat, n)
        return res[3][3]

nextpage

889. 根据前序和后序遍历构造二叉树

中等

给定两个整数数组,preorderpostorder ,其中 preorder 是一个具有 无重复 值的二叉树的前序遍历,postorder 是同一棵树的后序遍历,重构并返回二叉树。

如果存在多个答案,您可以返回其中 任何 一个。

示例 1:

img

输入:preorder = [1,2,4,5,3,6,7], postorder = [4,5,2,6,7,3,1]
输出:[1,2,3,4,5,6,7]

示例 2:

输入: preorder = [1], postorder = [1]
输出: [1]

提示:

  • 1 <= preorder.length <= 30
  • 1 <= preorder[i] <= preorder.length
  • preorder 中所有值都 不同
  • postorder.length == preorder.length
  • 1 <= postorder[i] <= postorder.length
  • postorder 中所有值都 不同
  • 保证 preorderpostorder 是同一棵二叉树的前序遍历和后序遍历

解题思路

通过口诀"前序遍历根左右"和”后序遍历左右根“,可以看出前序遍历的下一层递归就是【根(根左右)(根左右)】,说明前序遍历的第一个左子树的根节点就在根节点:preorder[0]的下一个位置:preorder[1],那么后序遍历中呢就是【(左右根)(左右根)根】,即后序遍历中第一个左子树的根节点正好可以用于分割左右子树,那么就可以和前面的两道题目的思路联系起来了【105. 从前序与中序遍历序列构造二叉树】【889. 根据前序和后序遍历构造二叉树】,通过postorder.index(preorder[1]) + 1就可以分割左右子树了,想必到这里大家都知道怎么解了吧。

递归写法

class Solution:
    def constructFromPrePost(self, preorder: List[int], postorder: List[int]) -> Optional[TreeNode]:
        n = len(postorder)
        if n == 0: return None
        if n == 1: return TreeNode(preorder[0])
        root = TreeNode(preorder[0])
        index = postorder.index(preorder[1]) + 1
        root.left = self.constructFromPrePost(preorder[1:index+1], postorder[:index])
        root.right = self.constructFromPrePost(preorder[index+1:], postorder[index:-1])
        return root

nextpage

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值