LeetCode剑指offer题目记录7

leetcode刷题开始啦, 每天记录几道题.

剑指 Offer 30. 包含min函数的栈

题目描述

定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。

思路

push, top, pop都是容易的, 注意 top 在栈为空的时候要返回空值. 关键问题是要让min的时间复杂度为 O(1), 并且如果 pop 了最小值能回退到上一个min. 所以要动态维护一个列表, 记录曾经有过的 min.

  • 如果 push 进来的值 <= 当前最小值, 就把它添加到列表尾部;
  • 如果 pop 走的元素是当前的最小值, 就把min列表的最后一个元素删掉;
  • 调用 min 时, 返回 min 列表的最后一个.

python

class MinStack:

    def __init__(self):
        """
        initialize your data structure here.
        """
        self.list1 = []
        self.Min = []


    def push(self, x: int) -> None:
        self.list1.append(x)
        if not self.Min or self.Min[-1] >= x:
            self.Min.append(x)

    def pop(self) -> None:
        res = self.list1.pop()
        if res == self.Min[-1]:
            self.Min.pop()
        return res

    def top(self) -> int:
        if self.list1:
            return self.list1[-1] 
        else:
            return None

    def min(self) -> int:
        return self.Min[-1]

剑指 Offer 31. 栈的压入、弹出序列

题目描述

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {1,2,3,4,5} 是某栈的压栈序列,序列 {4,5,3,2,1} 是该压栈序列对应的一个弹出序列,但 {4,3,5,1,2} 就不可能是该压栈序列的弹出序列。

思路

用另一个栈, 按照压栈序列的顺序压栈, 每次压入后都判断当前栈顶是否与弹出序列首项相等, 如果相等就弹出, 把弹出序列首项去掉, 然后继续判断. 一直弹到栈顶和弹出序列首项不相同. 遍历完压栈序列, 看看是不是所有压入的都弹出了, 是的话说明这个弹出序列可行.

为了实现去掉弹出序列首项, 可以用一个指标指向当前的 “首项”, 也可以把弹出序列反序, 每次比较尾部元素, 然后pop掉被弹出的尾项.

python

class Solution:
    def validateStackSequences(self, pushed: List[int], popped: List[int]) -> bool:
        trans = list()
        popped = popped[::-1]
        for item in pushed:
            trans.append(item)
            while trans and trans[-1] == popped[-1]:
                trans.pop()
                popped.pop()
        
        if trans:
            return False
        else:
            return True

剑指 Offer 32 - I. 从上到下打印二叉树

题目描述

从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。

思路

先排除空树. 如果树不是空的, 用一个列表 result 来存储要返回的值, 用一个队列 level 来存储要访问的节点. 先存储根节点的值和根节点, 如果 level 非空, 就 pop 它第一个元素, 检查这个元素的左子节点和右子节点, 如果不是空节点就把值和节点分别添加到 result 和 level 里. 循环下去, 一直到 level 里没有元素.

这好像时广度优先搜索的策略?

python

class Solution:
    def levelOrder(self, root: TreeNode) -> List[int]:
        result = []
        if not root:
            return result

        level = collections.deque([root])
        result.append(root.val)
        while level:
            node = level.popleft()
            if node.left:
                result.append(node.left.val)
                level.append(node.left)
            if node.right:
                result.append(node.right.val)
                level.append(node.right)

        return result

剑指 Offer 32 - II. 从上到下打印二叉树 II

题目描述

从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。

思路

还是和刚刚差不多. 只是我们这次要打印完一行后换一个列表. 所以我们最好一层一层的检查. 队列的元素为每一层的节点列表, 每次弹出队列首项, 依次检查队列首项中每个节点的子节点, 如果有, 就添加到下一层节点列表中. 检查完这一层后,

  • 如果下一层节点列表非空, 说明还要继续检查. 把子节点列表添加到队列尾部, 把子节点列表中每个节点的值添加到一个列表里再附到 result 的尾部.
  • 如果下一层节点列表是空的, 说明我们已经检查完了. 这个时候直接返回 result 即可.

python

class Solution:
    def levelOrder(self, root: TreeNode) -> List[List[int]]:
        result = []
        if not root:
            return result
        
        result.append([root.val])
        levels = collections.deque([[root]])
        while levels:
            cur_level = levels.popleft()
            next_level = []
            for node in cur_level:
                if node.left:
                    next_level.append(node.left)
                if node.right:
                    next_level.append(node.right)
            if next_level:
                result.append([node.val for node in next_level])
                levels.append(next_level)
            else: 
                return result

剑指 Offer 32 - III. 从上到下打印二叉树 III

题目描述

请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。

思路

这和刚刚的没什么大区别, 只是要加一个判断层的 flag 来确定打印下一层节点列表的顺序. 每打印一次, 就改变 flag 的值.

python

class Solution:
    def levelOrder(self, root: TreeNode) -> List[List[int]]:
        if not root:
            return []
        
        result = [[root.val]]
        levels = collections.deque([[root]])
        next_level_flag = 1

        while levels:
            cur_level = levels.popleft()
            next_level = []
            next_level_num = 0
            for node in cur_level:
                if node.left:
                    next_level.append(node.left)
                    next_level_num += 1
                if node.right:
                    next_level.append(node.right)
                    next_level_num += 1
            if next_level_num:
                levels.append(next_level)
                if next_level_flag == 1:
                    result.append([next_level[i].val for i in range(next_level_num-1, -1, -1)])
                    next_level_flag = 0
                else:
                    result.append([node.val for node in next_level])
                    next_level_flag = 1
            else: 
                return result

剑指 Offer 33. 二叉搜索树的后序遍历序列

题目描述

输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。如果是则返回 true,否则返回 false。假设输入的数组的任意两个数字都互不相同。

思路

一个二叉搜索树, 满足每个节点的左子树的所有节点的值小于该节点的值, 该节点的值小于它的右子树的所有节点的值.

后序遍历, 顺序是左, 右, 根. 如果是二叉搜索树, 序列的最后一个元素是根节点, 前面几个元素是左子树, 从第一个大于根节点的元素开始直到倒数第二个元素为右子树, 就把这个序列拆分开, 检查左子树是否小于根节点, 右子树是否大于根节点. 然后递归地检查左子树右子树是否满足条件.

  • 基线条件: 一个树只有一个节点或者没有节点, 那它满足条件.

python

class Solution:
    def verifyPostorder(self, postorder: List[int]) -> bool:

        if len(postorder) < 2:
            return True

        right_idx, root_idx = 0, len(postorder)-1
        root = postorder[root_idx]

        while postorder[right_idx] < root:
                right_idx += 1
        
        for i in range(right_idx, root_idx):
            if postorder[i] < root:
                return False
        
        check_left = self.verifyPostorder(postorder[0:right_idx])
        check_right = self.verifyPostorder(postorder[right_idx:root_idx])
        return check_left and check_right

剑指 Offer 34. 二叉树中和为某一值的路径

题目描述

给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

叶子节点 是指没有子节点的节点。

思路

从根节点出发一路走到叶节点, 沿途记录节点的值, 一边把 target 减去对应的值, 如果走到叶节点时刚好消完, 就把这条路径添加进去. 然后要往回退, 一边退一边把之前添加进路径的节点值吐掉, 退到能进入没检查过的子树时, 进入子树做检查.

题目没有说二叉树全是正的, 所以没办法在中途用判断剩余target 和当前节点 value 的办法剪枝.

python

class Solution:
    def pathSum(self, root: TreeNode, target: int) -> List[List[int]]:
        res = []
        path = []

        def dfs(root, target):
            if not root:
                return 

            path.append(root.val)
            if not root.left and not root.right and target == root.val:
                res.append(path[:])

            dfs(root.left, target - root.val)
            dfs(root.right, target- root.val)
            path.pop()

        dfs(root, target)
        return res

剑指 Offer 35. 复杂链表的复制

题目描述

请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。

思路

关键的问题是复制某个链表节点时, 该节点的随机指针指向的节点可能还未创建. 我可以先做一个原节点和新节点的镜像对应, 把整个链表都新建好, 然后再遍历一遍链表, 根据原链表节点的 next 和 random 构建镜像链表节点的 next 和 random.

这个镜像可以用字典来存, 也可以把新旧链表错位并在一起 (这个时候新旧链表各节点的 random 都存在), 再遍历. 前一个办法少遍历一次链表省时间, 后一个办法不需要开辟一个字典省空间. 各有好处.

python

"""
# Definition for a Node.
class Node:
    def __init__(self, x: int, next: 'Node' = None, random: 'Node' = None):
        self.val = int(x)
        self.next = next
        self.random = random
"""
class Solution:
    def copyRandomList(self, head: 'Node') -> 'Node':
        if not head:
            return None

        pointer = head
        dic = {}
        while pointer:
            new_pointer = Node(pointer.val)
            dic[pointer] = new_pointer
            pointer = pointer.next
        pointer = head 
        while pointer:
            if pointer.next:
                dic[pointer].next = dic[pointer.next]
            if pointer.random:
                dic[pointer].random = dic[pointer.random]
            pointer = pointer.next
        
        return dic[head]

剑指 Offer 36. 二叉搜索树与双向链表

题目描述

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。

思路

这题我没想出来, 看了别人的题解. 唉.

二叉搜索树的中序遍历可以让元素升序, 我们在中序遍历的中间添加把相邻两个元素双向链接的操作, 并标记头节点, 在最后把头节点和尾节点双向链接. 在递归函数里需要完成对 head 和 pre 的修改, 我们需要让这两个变量全局可操作, 这可以通过把它们变成类的成员变量来实现.

  1. 递归的终点是根节点为空, 此时直接返回.
  2. 中序处理左子树
    • 如果当前处理的节点 root 有前驱 pre, 就把 pre 和 root 链起来: self.pre.right = root; root.left = self.pre
    • 如果当前处理的节点 root 没有前驱, 说明它是整个二叉搜索树的最左边, 也就是最小的元素, 也就是最后要返回的 head. self.head = root
    • root 是中序处理好的右子树的前驱, 所有 self.pre = root
  3. 然后中序处理右子树
  4. 遍历完后, pre 节点走到了右子树的最右端, 也就是这个双向链表的尾节点. 把head 和 pre 链接起来, 就完成循环链表. 返回头节点即可.

题目说的不能创建任何新的节点, 我没有很明白. 空节点不算新节点吗?

python

class Solution:
    def __init__(self):
        self.pre = None
        self.head = None

    def treeToDoublyList(self, root: 'Node') -> 'Node':
        if not root:
            return root

        def dfs(root):
            if not root:
                return 
            dfs(root.left)
            if self.pre:
                self.pre.right = root
                root.left = self.pre
            else:
                self.head = root
            self.pre = root
            dfs(root.right)

        dfs(root)
        self.head.left = self.pre
        self.pre.right = self.head

        return self.head

剑指 Offer 37. 序列化二叉树

题目描述

请实现两个函数,分别用来序列化和反序列化二叉树。

你需要设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。

提示:输入输出格式与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。

思路

这个题我又不会. 干. 我只会序列化二叉树, 但是我没有发现树中节点和它在序列中的位置的关系, 不知道怎么恢复. 唉, 感谢这个世界上有那么多比我聪明的人.

首先是序列化二叉树, 这个比较容易, 很多种方法都可以. 我们可以直接套用之前 从上到下打印二叉树 的办法, 只是不同的是, 要把空节点也打印出来, 便于之后反序列时对元素定位.

这样打印二叉树, 序列中的非空节点 node 索引为 n, 它前面的 null 节点个数为 m

  • node.left 的索引是 2(n-m) + 1;
  • node.right 的索引是 2(n-m) + 2.

但好像恢复的时候没有用到这个对应关系? 如果字符串是空的, 那就是空树, return 空树. 照上面的遍历, 第一个节点就是根节点, 重复刚刚遍历的策略, 依次把字符串转换出来的列表的每个元素沾给对应的节点, 空节点就不沾, 非空节点就创建并且添加到队列里. 直到遍历完这个列表, 所有的非空节点都被创建.

python

class Codec:
    def serialize(self, root):
        if not root: 
            return ""
        res = []
        queue = collections.deque([root])
        while queue:
            node = queue.popleft()
            if node:
                res.append(str(node.val))
                queue.append(node.left)
                queue.append(node.right)
            else: res.append("null")
        return ','.join(res)

    def deserialize(self, data):
        if data == "": 
            return
        vals = data.split(',')
        i = 1
        root = TreeNode(int(vals[0]))
        queue = collections.deque([root])
        while queue:
            node = queue.popleft()
            if vals[i] != "null":
                node.left = TreeNode(int(vals[i]))
                queue.append(node.left)
            i += 1
            if vals[i] != "null":
                node.right = TreeNode(int(vals[i]))
                queue.append(node.right)
            i += 1
        return root

剑指 Offer 38. 字符串的排列

题目描述

输入一个字符串,打印出该字符串中字符的所有排列。

你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。

思路

首先想到的递归. 对字符串中的任一个元素, 如果已经把它以外的元素的全部排列搞出来了, 那把这个元素放在首位. 遍历字符串里的所有元素, 就得到整个字符串的排列.

  • 基线条件: 字符串为空或者只有一个元素, 那直接返回这个字符串即可, 因为它只有一个排列.

题目没说元素是否都相同, 所以要处理元素相同的情形. 在遍历时, 如果这个元素已经出现过了, 那它作首元的排列就已经有了, 所以跳过它. 因此需要用一个字典记录已经出现过的首元.

python

class Solution:
    def permutation(self, s: str) -> List[str]:
        res = []
        n = len(s)

        if n < 2:
            return [s]

        dic = {}
        for i in range(0, n):
            if s[i] not in dic:
                subres = self.permutation(s[0:i] + s[i+1:])
                dic[s[i]] = 1
                for chars in subres:
                    res.append(s[i] + chars)
        
        return res

剑指 Offer 39. 数组中出现次数超过一半的数字

题目描述

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

思路

最直观的想法, 我搞一个字典, 记录每个出现过的数字出现的次数, 最多的那个就是要的答案. 但有没有更快的方法呢? 我想了想, 我把这个数组排序, 中间的元素是不是一定就是这个答案? 好像是的. 排序后这个元素所占的连续长度应该超过这个数组的一半, 所以无论如何中间的元素都会落在这个连续长度里.

调用排序, 时间复杂度和空间复杂度就和这个排序是一样的了. 算起来, 应该比搞字典来的慢.

这里有个小技巧, 求 n // 2 可以用 n >> 1, 向右移一位. 很好想, 两个相同的二进制数相加, 就是每一位都要进一, 就是左移一位. 所以不论奇数偶数, 右移一位都是 // 2.

python

class Solution:
    def majorityElement(self, nums: List[int]) -> int:
        nums.sort()
        return nums[len(nums) >> 1]

剑指 Offer 40. 最小的k个数

题目描述

输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

思路

最笨也最直接的想法, 把数组排序, 然后输出前 k 个数字.

python

class Solution:
    def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
        if not arr or k == 0:
            return []

        arr.sort()
        return arr[:k]

改进

晚上刚和老朋友打电话, 用很浅显的语言给我讲清了什么是堆. 正好试一下.

先把前 k 个数存到一个大根堆里, 然后继续遍历, 如果遍历到的元素比堆顶小, 就把堆顶弹出, 把这个元素扔进去. 遍历完, 堆里的元素就是 k 个最小值.

python 的堆用heapq 模块来实现, q 表示队列. 只有小根堆.

  • heapify(heap) 是让列表具有小根堆的性质;
  • heappop(heap) 是弹出堆中最小的;
  • heappush(heap, x) 是将 x 压入堆中;
  • heapreplace(heap, x) 是弹出最小的元素并将 x 压入堆中.

因为只有小根堆, 所以要用相反数来处理.

python

class Solution:
    def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
        if not arr or k == 0:
            return []

        heap = [-arr[i] for i in range(k)]
        heapq.heapify(heap)
        for num in arr[k:]:
            if num < -heap[0]:
                heapq.heapreplace(heap, -num)
        return [-num for num in heap]
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值