Code Monkey home page Code Monkey logo

codeinterview's People

Stargazers

 avatar

Watchers

 avatar  avatar

codeinterview's Issues

[剑指 offer] 03. 数组中重复的数字

数组中重复的数字

找出数组中重复的数字。

在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

示例 1:

输入:
[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3
 

限制:

2 <= n <= 100000

来源:面试题03. 数组中重复的数字 - 力扣(LeetCode)

题解

Hash 法

用hash表来记录各个数字出现的次数,如果发现之前出现过则返回。
时间复杂度:O(n). 遍历一次
空间复杂度:O(n) 额外的 hash 表

class Solution:
    def findRepeatNumber(self, nums: List[int]) -> int:
        if len(nums) == 0: return False
        for x in nums:
            if x > len(nums) - 1 or x < 0:
                return None
        
        rec = dict()
        
        for a in nums:
            if a not in rec:
                rec[a] = 1
            else:
                return a
        return None

交换位置法

由于数组大小为 n,且数字值在 0~n-1,那么假如没有重复数字,这个数组应该可以重新组织成一个有序的数组,其每个位置 i 上的数字为 i。组织的方法为:如果位置 i 上的数字 nums[i] 不为 i,则将nums[i] 与 nums[nums[i]] 交换,即将nums[i] 放置到应该属于它的位置上;此时对于位置i,如果刚刚交换过来的数字还不等于 i,则重复以上过程,直到 nums[i] == i.

如果在组织的过程中发现 nums[nums[i]] == nums[i],即nums[i]其原本对应的位置的值已经是正确的了,那么说明nums[i] 就是重复的数字。

时间复杂度:O(n). 遍历一遍为 O(n),而每个数字最多被交换 2 次(被动交换到当前检索位置,发现与当前位置不匹配后,下一次交换一定会放置到该数字正确的位置,所以最多 2 次。),所以为O(n)
空间复杂度:O(1)

class Solution:
    def findRepeatNumber(self, nums: List[int]) -> int:
        if len(nums) == 0: return False
        for x in nums:
            if x > len(nums) - 1 or x < 0:
                return None
        
        for i in range(len(nums)):
            while nums[i] != i:
                if nums[i] == nums[nums[i]]:
                    return nums[i]
                nums[nums[i]], nums[i] = nums[i], nums[nums[i]]
        return None

变形:不修改数组找出重复的数字

在一个长度为n+1的数组里的所有数字都在1~n的范围内,所以数组中至少存在一个数字是重复的。请找出数组中任意一个重复的数字,但不能修改输入的数组。例如输入长度为8的数组{2,3,5,4,3,2,6,7},那么对应的输出是重复的数字2或者3。

测试用例:

  1. 长度为 n 的数组中包含1 个或多个重复数字
  2. 数组中不包含重复数字
  3. 无效输入用例,如数组长度为 0,数值范围不在 1~n 之间

题解

和原题类似,可以直接使用 Hash 法来解
然而,Hash 法需要额外 O(n) 的空间开销。如果想要避免额外的空间开销呢?

可以使用二分查找的办法。考虑到一共有 n+1 个数字,而每个数字范围在1~n 之间,说明一定为重复数字。那么,如果这个数组 nums 在某个区间 $[a, b] \subsete [1, n]$ 内的数字数量大于区间长度,即$b-a+1$,则说明该区间内有重复数字,否则重复数字在该区间的补集上。这就很自然的可以利用二分查找。

class Solution:
    def findRepeatNumber(self, nums: List[int]) -> int:
        if len(nums) == 0: return False
        n = len(nums) - 1 #  1<= a <= n while len(nums) == n+1

        for x in nums:
            if x > n or x < 1:  
                return None
        
        # binary search
        # start, high
        start = 1 # start from 1 !!!
        end = n
        
        while start <= end:
            # split into two half
            mid = (end - start) // 2 + start
            # count how many elements in this interval 
            count = sum([1 for x in nums if start <= x <= mid])
            
            # if the interval is one-element length, then 
            # check if count > 1
            if start == end:
                if count > 1: return start
                else:
                    break
            
            if count > (mid - start + 1): # in this interval
                end = mid
            else:
                start = mid + 1 # in the left interval
            
        return None

时间复杂度:O(nlogn). 二分查找 O(logn),其中每次要执行统计次数,需要遍历数组,O(n)
空间复杂度:O(1)

[Leetcode] 221. 最大正方形

在一个由 0 和 1 组成的二维矩阵内,找到只包含 1 的最大正方形,并返回其面积。

示例:

输入:

1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0

输出: 4
通过次数55,975提

221. 最大正方形 - 力扣(LeetCode)


定义状态和确定状态转移方程还是很 tricky 的

class Solution:
    def maximalSquare(self, matrix: List[List[str]]) -> int:
        if not matrix or not matrix[0]: return 0
        m, n = len(matrix), len(matrix[0])
        dp = [[0] * (n+1) for _ in range(m+1)]
        
        # dp 表示以 matrix[i-1][j-1]为右下角的最大正方形的边长
        # matrix[i-1][j-1] 为 1 时,它的边长向上延伸,向左延伸
        # 显然是由 dp[i-1][j], dp[i][j-1], dp[i-1][j-1] 
        # 对应的三个正方形的最短边长决定的(画图更容易理解)
        # dp[i][j] = matrix[i-1][j-1] == 1 && min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1

        max_edge = 0
        for i in range(1, m+1):
            for j in range(1, n+1):
                if matrix[i-1][j-1] == '1':
                    dp[i][j] = min(
                        dp[i-1][j],
                        dp[i][j-1], 
                        dp[i-1][j-1]
                    ) + 1
                    if dp[i][j] > max_edge: 
                        max_edge = dp[i][j]
        return max_edge ** 2

[Leetcode] 42. 接雨水

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

img

上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 感谢 M
arcos 贡献此图。

示例:

输入: [0,1,0,2,1,0,1,3,2,1,2,1]
输出: 6

42. 接雨水 - 力扣(LeetCode)


这道题还蛮难的,一开始我想从左到右扫描,但是没有想清楚什么情况下才能合法地接雨水

看了题解后了解了两种做法

双指针+逐层扫描

逐层向上扫描,记录每一层水+柱子的体积(黑/蓝,非空白部分),最后减去柱子体积即积水了
用双指针扫描,对某一层来说,指针分别指向最左边和最右边第一个非空的位置,也就是第一个高度大于等于层数(level) 的位置。高度小于 level 的对于该层来说就是空白了。

最后 l==r 时,一定指向最高的 bar,此时没必要再增加 level 了,因为 level 会一直增加到大于最高的 bar,如果最高的 bar 非常高,那会白白消耗时间。所以做了一点特殊处理

class Solution:
    def trap(self, height: List[int]) -> int:
        if not height: return 0

        l, r = 0, len(height) - 1

        total = 0
        barsum = 0
        level = 1
        maxlevel = max(height)
        while l <= r:
            # 当 l==r 时,都指向最高的柱子,
            # 没必要再 level++ 了
            # 此时 total 已经在上一轮统计完毕了
            # 只需要再统计一下 barsum
            if l == r:
                barsum += level - 1
                break
            while l <= r and height[l] < level: 
                barsum += height[l]
                l += 1
            while l <= r and height[r] < level:
                barsum += height[r]
                r -= 1
            total += r - l + 1
            level += 1
        # print(total, barsum)

        return total - barsum
           

时间复杂度 O(n): 双指针走完整个数组

双指针+动态规划记录某位置的左边和右边最高 bar

对于一个 bar 来说,它能不能贡献积水,取决于它的高度与它两侧最高 bar 中最矮的那个 bar 之间的关系。
如果对于 i,每次都遍历向左向右寻找,就比较浪费时间 O(n^2)。
我们发现,i 的左边最高 bar,只能 i-1 的左边最高bar 和 i-1 的高度有关。同理 i 右边的最高 bar 只和 i+1 右边的最高 bar和 i+1 的高度有关。
用两个数组 left[i], right[i] 来记录 bar i 左右的最高 bar。

class Solution:
    def trap(self, height: List[int]) -> int:
        if not height: return 0

        n = len(height)

        leftmax = [0] * n
        for i in range(1, n): leftmax[i] = max(leftmax[i-1], height[i-1])
        
        rightmax = [0] * n
        for i in range(n-2, -1, -1): rightmax[i] = max(rightmax[i+1], height[i+1])
        
        total = 0
        for i in range(n):
            sidemin = min(leftmax[i], rightmax[i])
            if sidemin - height[i] > 0:
                total += sidemin - height[i]
        return total

时间复杂度: O(n)
空间复杂度:O(n),2 个辅助数组

由于 leftmax[i], rightmax[i] 只和前一个/后一个相关,可以进一步用滚动数组来优化空间

[剑指 offer] 26. 树的子结构

输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)

B是A的子结构, 即 A中有出现和B相同的结构和节点值。

例如:
给定的树 A:

     3
    /
   4   5
  /
 1   2
给定的树 B:

   4 
  /
 1
返回 true,因为 B 与 A 的一个子树拥有相同的结构和节点值。

示例 1:

输入:A = [1,2,3], B = [3,1]
输出:false
示例 2:

输入:A = [3,4,5,1,2], B = [4,1]
输出:true
限制:

0 <= 节点个数 <= 10000

来源


这题主要是边界条件有点狗血。由于 B 是 A 的一部分,所以 B 不需要匹配完 A,因此 B 为空 A 不为空时,可以认为 A 包含 B。

逻辑上就是先序遍历树,判断B 是否是A当前节点作为根节点的子树的同构,不是的话再判断它与 A 的左右子树。
判断同构时,也是一个先序遍历。

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

class Solution:
    def isSubStructure(self, A: TreeNode, B: TreeNode) -> bool:
        def check(a, b):
            if not b:
                return True
            if not a or a.val != b.val:
                return False
            return check(a.left, b.left) and check(a.right, b.right)

        return bool(A and B) \
            and (check(A, B) \
            or self.isSubStructure(A.left, B) or self.isSubStructure(A.right, B))

时间复杂度:O(|A||B|)。遍历 A 的所有节点,然后 check 同构,需要遍历 B 的所有节点。
空间复杂度:O(|A|): 当树 A 和树 B 都退化为链表时,递归调用深度最大。当 |A| < |B| 时,遍历树 A 与递归判断的总递归深度为 |A| ;当|A| > |B| 时,最差情况为遍历至树 A 叶子节点,此时总递归深度为 |A|.

但是这么写速度会更快,真的服了。理论上不应该

class Solution:
    def isSubStructure(self, A: TreeNode, B: TreeNode) -> bool:
        def check(a, b):
            if not b:
                return True
            if not a or a.val != b.val:
                return False
            return check(a.left, b.left) and check(a.right, b.right)

        if not A or not B: return False
        if check(A, B): return True
        return self.isSubStructure(A.left, B) or self.isSubStructure(A.right, B)

[Leetcode] 440. 字典序的第K小数字

给定整数 n 和 k,找到 1 到 n 中字典序第 k 小的数字。

注意:1 ≤ k ≤ n ≤ 109。

示例 :

输入:
n: 13 k: 2

输出:
10

解释:
字典序的排列是 [1, 10, 11, 12, 13, 2, 3, 4, 5, 6, 7, 8, 9],所以第二小的数字是 10。

440. 字典序的第K小数字 - 力扣(LeetCode)


好难的!

首先需要理解字典序是什么概念,这个图极大的帮助了我

img
来源

于是,这个题目可以看成 10 叉前缀树的遍历,找第 k 个节点。
对于某个前缀 cur,统计其作为根节点的合法10 叉树的节点数目。何为合法?即值小于等于 n。

  • 如果包含 cur 为前缀的 10 叉树的节点数小于 k,说明第 k 个节点不在 cur 树中,那就cur++,继续统计下一棵 10 叉树
  • 如果包含 cur 为前缀的 10 叉树的节点数目大于等于 k,说明第 k 个节点在 cur 树中!那就顺着 cur 的子树去缩小考察范围,cur 的第一棵最小的子树是 cur * 10
  • 重复上述过程,不停累计访问到的节点数目,直到统计到第 k 个。
class Solution:
    def findKthNumber(self, n: int, k: int) -> int:
        """ 相当于 10 叉前缀树的遍历,找第 k 个节点 """
        cur = 1
        k = k - 1 # 扣除当前节点 1

        while k:
            count = self.get_count(cur, n)

            # 如果 k 不在cur 为前缀的子树里
            # 下一步考察 cur+1 为前缀的子树
            if count <= k:
                k -= count
                cur += 1
            # 如果 k 在 cur 为前缀的子树里
            # 下一步往 cur 的子树继续考察
            # 第一个先考察 10*cur 为前缀的子树
            else:
                k -= 1
                cur *= 10

        return cur

    def get_count(self, prefix, n):
        """ 统计以 prefix 为前缀的这一层有多少节点 """
        # nxt 可以看做以 cur 前缀的 10 叉树的层的上界
        cur, nxt = prefix, prefix + 1
        cnt = 0
        while cur <= n:
            # 如果 n+1 不如 nxt 大,说明这一层不是满 10 叉树
            # 这一层的节点数为 n - cur + 1
            cnt += min(n+1, nxt) - cur
            cur *= 10
            nxt *= 10
        return cnt 

[剑指 offer]

给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。

 
示例 1:

输入: 12258
输出: 5
解释: 12258有5种不同的翻译,分别是"bccfi", "bwfi", "bczi", "mcfi"和"mzi"
 

提示:

0 <= num < 231

面试题46. 把数字翻译成字符串 - 力扣(LeetCode)


有一点像上楼梯/斐波那契数列问题

设 dp[i] 是以 i 位置结尾的数字串可以组成的翻译数。
可以分解成两个子问题:

  • 如果此时只选取 i 来翻译,则翻译数量为 dp[i] = dp[i-1]
  • 如果 i 和 i-1 能构成合法翻译( 10 <= num[i-1][i] <=25 ),则翻译数量为 dp[i] = dp[i-1] + dp[i-2]

初始化:dp[1] = dp[0] = 1

为什么 dp[0] = 1?
因为假设数字长 2 位,如果这两个数字能有合法翻译,则 dp[2] = dp[1] + dp[0] = 2。为了能处理这种边界情况,初始化 dp[0] = 1

class Solution:
    def translateNum(self, num: int) -> int:
        # dp[i] = dp[i-1] + dp[i-2] if (i-1,i) valid else dp[i+1]

        nums = str(num)
        n = len(nums)
        dp = [0] * (n+1)
        dp[0] = dp[1] = 1

        for i in range(2, n+1):
            dp[i] = dp[i-1] + dp[i-2] if 10 <= int(nums[i-2:i]) < 26 \
                else dp[i-1]

        return dp[n]

时间复杂度:O(n)
空间复杂度:O(n),使用了 dp 数组以及把 num 转换成了字符串

我们可以注意到,dp[i] 只和 dp[i-1] 与 dp[i-2] 有关,所以我们可以使用滚动数组方法,用 a, b来表示i-1 和 i两个变量,从而压缩空间复杂度

class Solution:
    def translateNum(self, num: int) -> int:
        # dp[i] = dp[i-1] + dp[i-2] if (i-1,i) valid else dp[i-1]
        #    b = b + a | a = b
        #    b = b | a = b
        nums = str(num)
        n = len(nums)

        a, b = 1, 1
        for i in range(2, n+1):
            b, a = (a+b, b) if "10" <= nums[i-2:i] < "26" \
                else (b, b)

        return b

我们还可以使用求余来得到各个数位,继续压缩掉字符串的开销

因为这个问题是对称的,我们可以通过求余从右到左扫描。和 a, b 一样,用 x, y 来滚动表示 i-1 和 i 的数位

class Solution:
    def translateNum(self, num: int) -> int:
        # dp[i] = dp[i-1] + dp[i-2] if (i-1,i) valid else dp[i-1]
        #    b = b + a | a = b
        #    b = b | a = b
        #    x, y : i-1, i
        a, b = 1, 1
        
        x = num % 10 # i-1

        while num:
            num //= 10
            y = num % 10 # i
            b, a = (b+a, b) if 10 <= 10*y+x < 26 \
                else (b, b)
            x = y
        return b

空间复杂度: O(1)

[剑指 offer] 12. 矩阵中的路径

请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。例如,在下面的3×4的矩阵中包含一条字符串“bfce”的路径(路径中的字母用加粗标出)。

[["a","b","c","e"],
["s","f","c","s"],
["a","d","e","e"]]

但矩阵中不包含字符串“abfb”的路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入这个格子。

 

示例 1:

输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true
示例 2:

输入:board = [["a","b"],["c","d"]], word = "abcd"
输出:false
提示:

1 <= board.length <= 200
1 <= board[i].length <= 200

面试题12. 矩阵中的路径 - 力扣(LeetCode)


这个题就是回溯,回溯和 dfs 感觉没有本质的区别, dfs 一般对于显式的数或图结构,回溯可以看做是对一个隐式树 dfs+剪枝。

使用 visited 数组来记录访问过的节点。

class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        rows, cols = len(board), len(board[0])
        visited = [[False] * cols for _ in range(rows)]
        delta = ((0, 1), (1, 0), (0, -1), (-1, 0))

        def _dfs(i, j, visited, cur_idx):
            # check boundary
            if 0 <= i < rows and 0 <= j < cols \
                and not visited[i][j] and board[i][j] == word[cur_idx]:

                visited[i][j] = True
                if cur_idx == len(word) - 1: return True

                exist_path = any(
                    _dfs(i+di, j+dj, visited, cur_idx+1)
                    for di, dj in delta
                )
                if not exist_path:
                    visited[i][j] = False
                return exist_path
            return False

        for i in range(rows):
            for j in range(cols):
                if _dfs(i, j, visited, 0):
                    return True
        return False

时间复杂度:O(n^2)
空间复杂度:O(n^2),额外的 visited 数组

为了减小空间复杂度,还可以考虑原地修改访问过得board数组为特殊记号,回溯的时候再换回来,这样就可以避免使用额外的 visited 数组。另外,在搜索下一个位置前先判断合法性可以减少调用次数,加快速度

class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        rows, cols = len(board), len(board[0])
        delta = ((0, 1), (1, 0), (0, -1), (-1, 0))

        def _dfs(i, j, cur_idx):
            # check boundary
            if board[i][j] == word[cur_idx]:
                if cur_idx == len(word) - 1: return True

                tmp, board[i][j] = board[i][j], " "
                exist_path = any(
                    _dfs(i+di, j+dj, cur_idx+1)
                    for di, dj in delta 
                    if 0 <= i+di < rows and 0 <= j+dj < cols
                )
                board[i][j] = tmp
                return exist_path
            return False

        for i in range(rows):
            for j in range(cols):
                if _dfs(i, j, 0):
                    return True
        return False

[剑指 offer] 24. 反转链表

定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。

 

示例:

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
 

限制:

0 <= 节点个数 <= 5000

来源:反转链表


常规操作,双指针。维护 prev 和 cur,遍历链表,逐个反转。最后返回尾节点

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution:
    def reverseList(self, head: ListNode) -> ListNode:
        prev, cur = None, head
        
        while cur is not None:
            # save next node
            nxt = cur.next

            cur.next = prev
            prev = cur
            cur = nxt

        return prev

在讨论区看到了比较新奇的递归法,值得记录一下。

递归地执行反转,一直递归最后一个节点(当节点的 next 为空时,或节点本身为空,即空的 head),该节点就是头结点,返回,记为 rev_head。(后序遍历)
每一个递归的操作是,将当前节点的下一个节点的 next 指向自己,然后当前节点的 next 指向空,实际上就是当前节点变成了尾节点。 实现链表的局部反转。
当递归函数全部出栈后,链表反转就完成了。

class Solution:
    def reverseList(self, head: ListNode) -> ListNode:
        if head is None or head.next is None:
            return head
        
        rev_head = self.reverseList(head.next)
        head.next.next = head
        head.next = None

        return rev_head

[剑指 offer] 11. 旋转数组的最小数字

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一个旋转,该数组的最小值为1。  

示例 1:

输入:[3,4,5,1,2]
输出:1
示例 2:

输入:[2,2,2,0,1]
输出:0

测试用例:

  • 有重复数字,或没有重复数字
  • 边界情况:升序数组本身是自己的一个旋转,只包含一个数字的数组
  • 特殊输入:空数组

来源:面试题11. 旋转数组的最小数字 - 力扣(LeetCode)


这个题有点意思。
最直接的想法就是直接遍历整个数组,找到最小的数字,也就是 O(n). 但是这样的方法就没有很好的利用好旋转数组的特点。

旋转数组的特点是,这个数组可以分成两个子数组,[3,4,5; 1,2], 每个数组内部是递增的,而第一个数组中的数一定是大于等于第二个数组。我们可以看到,最小的数字就是第二个数组的第一个数组或第一个个数组最后一个数字的下一个。

这样,我们就可以考虑使用二分法。用两个指针分别指向第一个数组和第二个数组,每次判断两个指针中间位置的数字属于哪个数组(大于第一个指针则说明在第一个数组,小于第二个指针则说明在第二个数组),再移动相应的指针过去,直到这两个指针相邻,此时第二个指针就指向最小数。

这里有一个特例是,如果第一个指针,中间位置及第二个指针上的值都相等,那就没办法判断中间位置属于哪个子数组,这个时候就转为直接遍历这两个指针中间的区间去找最小值。

class Solution:
    def minArray(self, numbers: List[int]) -> int:
        low, high = 0, len(numbers) -1
        mid = low
        while numbers[low] >= numbers[high]:
            if high - low == 1:
                mid = high
                break
            
            mid = (low + high) >> 1

            # 如果 low,high,mid 相同,即无法判断 mid 在哪个数组
            # 需要顺序查找最小数字
            if numbers[low] == numbers[mid] == numbers[high]:
                tmp = numbers[low]
                for i in range(low+1, high+1):
                    if numbers[i] < tmp: tmp = numbers[i]
                return tmp

            if numbers[mid] >= numbers[low]:
                low = mid
            elif numbers[mid] <= numbers[high]:
                high = mid

        return numbers[mid]

时间复杂度:O(logn). 二分查找

这里还有另一钟更简洁的二分法写法,所谓的减治法.
直接二分,把问题规模减小,我们要找到右子数组的第一个数

  • 当中间数大于右边界数时,中间数一定在左子数组,最小值一定不在它左边以及是它自己,接着查找 [low = mid + 1, high]
  • 当中间数小于右边界数时,中间数一定在右子数组,最小值一定在它右边,也可能包括它,接着查找[low, high = mid]
  • 当中间数等于右边数时,无法判断它在哪个数组 ([1, 2, 3; 1, 1, 1, 1]),此时将右边界左移一位不会有影响(其实我觉得直接移到 mid 不会有影响,相当于去重只有在右子数组才能去重,最左边不行,这时候右边界就跑到左数组里了!)
class Solution:
    def minArray(self, numbers: List[int]) -> int:
        low, high = 0, len(numbers) -1

        while low < high:
            mid = (high + low) >> 1
            
            if numbers[mid] > numbers[high]:
                low = mid + 1
            elif numbers[mid] < numbers[high]:
                high = mid
            else:
                high -= 1

        return numbers[low] # low == high

时间复杂度:O(logn)

总结一下:
有序数组或者这种半有序数组都可以用二分法,即减治(减而治之)
二分法关键在于确定好边界条件,如何移动边界,最后要返回的是哪个边界

[Leetcode] 32. 最长有效括号

给定一个只包含 '(' 和 ')' 的字符串,找出最长的包含有效括号的子串的长度。

示例 1:

输入: "(()"
输出: 2
解释: 最长有效括号子串为 "()"
示例 2:

输入: ")()())"
输出: 4
解释: 最长有效括号子串为 "()()"

32. 最长有效括号 - 力扣(LeetCode)


又学习了。

这道题可以用动态规划做。

状态:dp[i] 表示以第 i 个字符结尾的最长有效括号的长度
状态转移:

  • 如果 s[i] == '(',此时不可能形成有效括号
  • 只有 s[i] == ')' 的时候能形成有效括号,其中
    1. 如果 s[i-1] == '(',说明 i 可以和 i-1 组成有效括号,此时以 i 结尾的有效括号长度应该为以 i-2 结尾的的长度+2:dp[i] = dp[i-2] + 2
    2. 如果 s[i-1] == ')',也就是说此时字符串是 ...)),如果 i-1 位置组成了有效的括号,且 i 也有对应的左括号和它匹配,即 ...( s[i-dp[i-1]...i-1] )s[i-dp[i-1]-1] == '(',这一段的有效括号长度应该为dp[i-1] + 2。考虑到s[i-dp[i-1]-1]左边可能还有邻接的有效括号,还需要加上dp[i-dp[i-1]-2]
class Solution:
    def longestValidParentheses(self, s: str) -> int:
        dp = [0] * len(s)
        maxlen = 0

        for i in range(len(s)):
            if s[i] == ')':
                if i > 0 and s[i-1] == '(': 
                    dp[i] = (dp[i-2] if i > 1 else 0) + 2
                elif (lp := i - dp[i-1] - 1) >= 0 and s[lp] == '(' and s[i-1] == ')':
                    dp[i] = dp[i-1] + (dp[lp-1] if lp > 0 else 0) + 2
                maxlen = max(maxlen, dp[i])
        return maxlen

[剑指 offer] 28. 对称的二叉树

面试题28. 对称的二叉树
请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。

例如,二叉树 [1,2,2,3,4,4,3] 是对称的。

    1
   / \
  2   2
 / \ / \
3  4 4  3

但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:

    1
   / \
  2   2
   \   \
   3    3

示例 1:

输入:root = [1,2,2,3,4,4,3]
输出:true
示例 2:

输入:root = [1,2,2,null,3,null,3]
输出:false

限制:

0 <= 节点个数 <= 1000

面试题28. 对称的二叉树 - 力扣(LeetCode)


递归法(前序遍历)

一颗对称的二叉树,自己和自己是对称的。

判断两个树是否对称:

  • 根节点相同
  • t1 的左子树和 t2 的右子树对称,t1 的右子树和 t1 的左子树对称
  • 如果两棵树都是空(都越过了叶子),则是对称
  • 如果一棵树是空(越过了叶子),另一棵不是,则不对称
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

class Solution:
    def isSymmetric(self, root: TreeNode) -> bool:
        def check_sym(t1, t2):
            # 判断 t1, t2 两棵树是否对称

            # 同时越过叶节点
            if not t1 and not t2: return True
            # 一个越过了叶节点一个没有
            # 或者都没越过叶节点,但是该根节点值不相同
            if not t1 or not t2 or t1.val != t2.val: return False
            return check_sym(t1.left, t2.right) and check_sym(t1.right, t2.left)
        
        # 对称二叉树自己和自己对称
        return check_sym(root, root) if root else False
        # 直接判断左右子树会更快
        # return check_sym(root.left, root.right) if root else False

时间复杂度:O(n),因为我们遍历整个输入树一次,所以总的运行时间为O(n),其中n是树中结点的总数。
空间复杂度:递归调用的次数受树的高度限制。在最糟糕情况下,树是线性的,其高度为O(n)。因此,在最糟糕的情况下,由栈上的递归调用造成的空间复杂度为O(n)。

基于 BFS 的方法

逐层遍历,插入队列。首先插入根节点的左右孩子

  • 每次比较队列头两个节点
    • 或者两个节点值相同,说明至少满足了以这两个节点为根的两棵对称树的根节点相同,下面判断他们的左右子树是否对称,将elem1 的左孩子和 elem2 的右孩子依次入队,elem1 的右孩子和 elem2 的左孩子依次入队,将
    • 如果是两个空节点,说明之前顺利取到的叶子,跳过
    • 如果有一个空一个非空,或根节点不同,则显然不符合对称要求,False
      成功遍历完后都没有任何非法的结果,直接返回True
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

class Solution:
    def isSymmetric(self, root: TreeNode) -> bool:
        if not root: return True

        q = []
        q.append(root.left)
        q.append(root.right)
        
        while q:
            elem1, elem2 = q.pop(0), q.pop(0)
            if not elem1 and not elem2:
                continue
            if not elem1 or not elem2 or elem1.val != elem2.val:
                return False
            
            q.extend([elem1.left, elem2.right, elem1.right, elem2.left])
        
        return True

[剑指offer] 40. 最小的k个数

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

示例 1:

输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]
示例 2:

输入:arr = [0,1,2,1], k = 1
输出:[0]

限制:

0 <= k <= arr.length <= 10000
0 <= arr[i] <= 10000
通过次数56,959提交次数97,863

面试题40. 最小的k个数 - 力扣(LeetCode)


这题可以利用快排中 partition 算法,找到第 m 大的数,如果 m 不等于 k,则可以缩小找的范围。

还可以用最小堆/红黑树来实现:遍历一遍数组,用最小堆/红黑树来维护前 k 小。(如果用最大堆的话,可以对数组取相反数)

我被 partition 的实现坑死了,记录一下两种 partition,以后只用第二种实现

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

        start, end = 0, len(arr) -1
        index = self.partition(arr, start, end)

        while index != k-1:
            if index > k-1:
                end = index - 1
                index = self.partition(arr, start, index-1)
            if index < k-1:
                start = index + 1
                index = self.partition(arr,start, end)

        # print(arr)
        return arr[:k]

    def partition(self, arr, start, end):
        low = start
        high = end
        temp = arr[start]
        while low < high:
            while low < high and arr[high] >= temp:
                high -= 1
            # arr[low], arr[high] = arr[high], arr[low]
            while low < high and arr[low] <= temp:
                low += 1
            arr[low], arr[high] = arr[high], arr[low]
        arr[low], arr[start] = arr[start], arr[low]
        return low

    def partition2(self, array, l, r):
        x = array[r]
        i = l - 1
        for j in range(l, r):
            # find the one smaller than right
            if array[j] <= x:
                # small position
                i += 1
                array[i], array[j] = array[j], array[i]
        # i-th is the biggest small one, i+1 th is a big one
        # put right on this position cause right is the i+i th
        array[i+1], array[r] = array[r], array[i+1]
        return i + 1

时间复杂度: O(n),第一次 partition 是遍历 N,之后缩小范围,假设最坏情况下刚好是均分,则范围缩小一半,因此有 N + N/2 + N/4 + ... + N/N = 2N, 因此时间复杂度是 O(N)

[剑指 offer] 35. 复杂链表的复制

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

面试题35. 复杂链表的复制 - 力扣(LeetCode)


看成图,用 bfs 或 bfs 遍历。
使用一个 hash 表来记录被访问过得原节点和他对应的复制节点,防止重复访问

DFS

"""
# 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':
        def dfs(head):
            if not head:
                return None
            if head in visited:
                return visited[head] # copy of head
            
            copy = Node(head.val, None, None)
            visited[head] = copy
            copy.random = dfs(head.random)
            copy.next = dfs(head.next)

            return copy

        visited = {}
        return dfs(head)

BFS

"""
# 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':
        def bfs(head):
            if not head: return None
            q = collections.deque()
            q.append(head)
            clone = Node(head.val, None, None)
            visited[head] = clone
            while q:
                node = q.popleft()
                if node.next and node.next not in visited: 
                    q.append(node.next)
                    visited[node.next] = Node(node.next.val, None, None)
                visited[node].next = visited.get(node.next)
                if node.random and node.random not in visited: 
                    q.append(node.random)
                    visited[node.random] = Node(node.random.val, None, None)
                visited[node].random = visited.get(node.random)
            return visited[head]

        visited = {}
        return bfs(head)

[剑指 offer] 47. 礼物的最大价值

在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?

 

示例 1:

输入:
[
  [1,3,1],
  [1,5,1],
  [4,2,1]
]
输出: 12
解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物
 

提示:

0 < grid.length <= 200
0 < grid[0].length <= 200

面试题47. 礼物的最大价值 - 力扣(LeetCode)


典型动态规划题,转移方程:
dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + gird[i][j]

class Solution:
    def maxValue(self, grid: List[List[int]]) -> int:
        if not grid or not grid[0]: return 0

        m, n = len(grid), len(grid[0])
        dp = [[0] * n for _ in range(m)]

        dp[0][0] = grid[0][0]

        for i in range(m):
            for j in range(n):
                left = dp[i-1][j] if i - 1 >= 0 else 0
                top = dp[i][j-1] if j - 1 >= 0 else 0
                dp[i][j] = max(left, top) + grid[i][j]
        return dp[m-1][n-1]

时间复杂度:O(mn)
空间复杂度:O(mn),需要一个二维的 dp 数组

记录这道题主要是想记录一下对空间的优化,将二维 dp 用一维 dp 来表示。
我们可以观察到,dp[i, j] 只和 dp[i-1, j] 和 dp[i, j-1] 有关。如果我们用一维数组 dp[j] 来表示前一行上的最大价值,那么我们对每一行从左到右更新列,当更新到某一行的第 j 列时,它的左边就是 dp[j-1],上面就是未更新过得 dp[j].

class Solution:
    def maxValue(self, grid: List[List[int]]) -> int:
        if not grid or not grid[0]: return 0

        m, n = len(grid), len(grid[0])
        dp = [0] * n

        for i in range(m):
            for j in range(n):
                left = dp[j-1] if j > 0 else 0
                dp[j] = max(left, dp[j]) + grid[i][j]

        return dp[n-1]

这样空间复杂度就降到 O(n) 了

[剑指 offer] 59 - I. 滑动窗口的最大值

给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。

示例:

输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7]
解释:

  滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

提示:

你可以假设 k 总是有效的,在输入数组不为空的情况下,1 ≤ k ≤ 输入数组的大小。

面试题59 - I. 滑动窗口的最大值 - 力扣(LeetCode)


暴力的做法是,滑动窗口,然后在窗口中的k 个数里找最大,O(kn)

由于线性找最大代价是 O(k),我们是不是有办法可以找到 O(1) 的方法来得到最大值?

这里可以使用双向队列来维护一个单调队列,队列单调递减,队头就是最大值。
用该队列来维护窗口内数字的排序。

  • 如果左边移出的元素是队头最大元素,则移出队头(不可能再成为当前和未来窗口的最大值)
  • 每次移动窗口时,加入新的元素,将小于这个元素的数都出队(不可能再成为当前和未来窗口的最大值了,连新加入的这个都比不过),保持单调性。
class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        # deque 内只包含窗口内的值,维护的是有可能成为当前或未来窗口最大值的元素
        # deque 是一个单调递减队列,头元素是该窗口内最大的值
        # 每次移动窗口,如果队列中最大元素是左移出窗口的元素,删除掉他
        # 将新移入窗口的元素放入队列合适位置,小于它的元素都删除掉,
        # 因为有了这个比他们大的新元素,它们不可能成为未来任何窗口(k-1个)的最大值
        # 这样一来,队列中从左到右递增,并且数字排列顺序也和 nums 中一样(中间太小的已经被移出)
        # 所以窗口中最大值如果是左边界,它也只可能出现在队列的最左边
        if not nums: return []

        deque = collections.deque()

        # 未形成窗口 0~k-1
        for i in range(k):
            while deque and deque[-1] < nums[i]: deque.pop()
            deque.append(nums[i])

        res = [deque[0]]

        for r in range(k, len(nums)):
            # 队列中弹出移出窗口了的元素
            l = r - k + 1
            if deque[0] == nums[l-1]: deque.popleft()
            while deque and deque[-1] < nums[r]: deque.pop()
            deque.append(nums[r])
            res.append(deque[0])

        return res

时间复杂度: O(n)
空间复杂度: O(k) 队列中最多k 个窗口内元素

[剑指 offer] 25. 合并两个排序链表

输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。

示例1:

输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
限制:

0 <= 链表长度 <= 1000

来源


递归法:
如果l1小,则 l1 是合并节点,再将 l1.next 与 l2 合并后接到 l1 后面
反之亦然。
递归边界条件是,当l1或 l2 是空的时候,显然返回另一条链

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution:
    def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
        if l1 is None:
            return l2
        elif l2 is None:
            return l1

        # recursive
        if l1.val > l2.val:
            l1, l2 = l2, l1

        l1.next = self.mergeTwoLists(l1.next, l2)

        return l1

伪头结点迭代法:
设立一个伪头结点,以及一个 cur 指针开始指向它。然后判断两个链的大小,不停地接到 cur 后面。直到某一个链空了,此时直接接上另外一条链

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution:
    def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
        dumy = cur = ListNode(0)

        while l1 and l2:
            if l1.val < l2.val:
                cur.next, l1 = l1, l1.next
            else:
                cur.next, l2 = l2, l2.next
            cur = cur.next

        cur.next = l1 or l2
        
        return dumy.next

[Leetcode] 5. 最长回文子串

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

示例 1:

输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
示例 2:

输入: "cbbd"
输出: "bb"

5. 最长回文子串 - 力扣(LeetCode)


经典 dp 题。

注意到回文串的性质是,如果s[i...j] 是回文串,即 s[i] == s[j] 且 s[i+1...j-1]一定是回文串。
即,p(i, j) = s[i] == s[j] && p(i+1, j-1).

因此,可以定义状态为: dp[i][j]表示 s[i...j] 是否是回文串
状态转移方程为:dp[i][j] = s[i] == s[j] && dp[i+1][j-1]
边界条件:当 s[i] == s[j] 时,如果 s[i+1...j-1] 是空串或者只有一个元素,那 s[i...j] 一定是回文串(e.g., aa, aba),即 (j-1) - (i+1) + 1 < 2. 另外,如果 i == j 即 s[i...j] 只有一个元素,那它一定是回文串。这条可以合并在 (j-1) - (i+1) + 1 < 2 (j - i < 3) 中。

填表顺序
注意到 dp[i][j] 依赖于 dp[i+1][j-1],即二维表格左下角的值。
所以可以考虑

for j in range(1, n):
    for i in range(0, j)

这样会从左到右逐列(j)来从上往下(i)填写 dp[i][j],保证在填写 dp[i][j](i != j-1i != j-2) 的时候,dp[0...j-1][j-1] 都已经填写过了,自然包括 dp[i+1][j-1] (极端情况下,因为 j-i < 3 可以直接决定,所以 i=j-2 时,dp[j-1][j-1] 可以直接确定;i != j-1时,dp[j-2][j-1] 可以直接确定)。

class Solution:
    def longestPalindrome(self, s: str) -> str:
        size = len(s)

        dp = [[False] * size for _ in range(size)]

        # dp[i][j] == (s[i] == s[j]) and (dp[i+1][j-1])
        maxlen = 1
        start = 0
        for j in range(1, size):
            for i in range(j):
                if s[i] == s[j]:
                    if (j-1) - (i+1) + 1 < 2:
                        dp[i][j] = True
                    else:
                        dp[i][j] = dp[i+1][j-1]
                else:
                    dp[i][j] = False
                
                if dp[i][j]:
                    curlen = j - i + 1
                    if curlen > maxlen:
                        maxlen = curlen
                        start = i

        return s[start:start+maxlen]

时间复杂度:O(n^2)
空间复杂度:O(n^2)

[剑指 offer] 04. 二维数组中的查找

在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

 
示例:

现有矩阵 matrix 如下:

[
  [1,   4,  7, 11, 15],
  [2,   5,  8, 12, 19],
  [3,   6,  9, 16, 22],
  [10, 13, 14, 17, 24],
  [18, 21, 23, 26, 30]
]

给定 target = 5,返回 true。

给定 target = 20,返回 false。

 

限制:

0 <= n <= 1000

0 <= m <= 1000

来源:面试题04. 二维数组中的查找 - 力扣(LeetCode)


这道题看起来简单,比如很自然的想法就是用搜索算法,从左上角搜起,如果矩阵中的数字比目标数字大,则往右或下继续搜索。这样最坏的情况就是每个格子都访问了一遍,O(nm). 而仔细观察发现,如果从右上角开始比较,如果矩阵数字比目标大,则目标数字一定在下方而不可能在同一行的左边,于是可以直接忽略不看这一行。同样的,如果矩阵数字比目标小,则目标数字一定在左边而不可能在同一列的下方,于是可以直接忽略不看这一列。

class Solution:
    def findNumberIn2DArray(self, matrix: List[List[int]], target: int) -> bool:
        if not matrix or not matrix[0]:
            return False
        n, m = len(matrix), len(matrix[0])

        row, col = 0, m-1
        while row < n and col >= 0:  # row is increasing
            elem = matrix[row][col]
            if elem == target:
                return True
            elif target > elem:
                row += 1    # delete this row
            else:
                col -= 1    # delete this column
        return False

时间复杂度:O(n+m). 每次循环删除掉一行或一列,最差情况是全部删掉整个矩阵,即 n 行 m 列。
空间复杂度:O(1).

[Leetcode] 72. 编辑距离

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

插入一个字符
删除一个字符
替换一个字符
 

示例 1:

输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
示例 2:

输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')

72. 编辑距离 - 力扣(LeetCode)


经典 DP,直接看注释吧

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        len1, len2 = len(word1), len(word2)

        dp = [[0] * (len2 + 1) for _ in range(len1 + 1)]

        # base cases
        # word1 长度为 0 时,变成 word2的某个前缀 word2[:j] 需要 j 次插入
        for j in range(1, len2+1): dp[0][j] = j
        # 同理,word2 长度为 0 时,
        # word1的某个前缀 word1[:i] 变成变成 word2("") 需要 i 次删除
        for i in range(1, len1+1): dp[i][0] = i

        for i in range(1, len1+1):
            for j in range(1, len2+1):
                # 对应的两个字符相等,什么编辑都不需要做
                # 编辑距离等于 word1[:i-1] 与 word2[:j-1] 的编辑距离
                if word1[i-1] == word2[j-1]:
                    dp[i][j] = dp[i-1][j-1]
                else:
                    dp[i][j] = min(
                        # 可以通过在 word1[:i] 后插入 w2[j] 变成 word2[:j], 
                        # 那首先至少需要先通过将 word[:i] 编辑成 word2[:j-1]
                        # 因此,编辑距离等于 word1[:i] 与 word2[:j-1]的编辑距离 + 1
                        dp[i][j-1] + 1, 
                        # 可以通过在 word1[:i] 后删除 w1[i] 变成 word2[:j], 
                        # 那首先至少需要先通过将 word[:i-1] 编辑成 word2[:j]
                        # 因此,编辑距离等于 word1[:i-1] 与 word2[:j]的编辑距离 + 1
                        dp[i-1][j] + 1,     
                        # 可以通过把 word1[:i] 的 w1[i] 替换成 w2[j] 使其变成 word2[:j], 
                        # 那首先至少需要先通过将 word[:i-1] 编辑成 word2[:j-1]
                        # 因此,编辑距离等于 word1[:i-1] 与 word2[:j-1]的编辑距离 + 1
                        dp[i-1][j-1] + 1,                            
                    )

        return dp[-1][-1]

时间复杂度:O(len1 * len2)
空间复杂度:O(len1 * len2)。注意到 dp[i][j] 只和 dp[i-1][j], dp[i][j-1], dp[i-1][j-1] 有关,其实可以压缩空间

空间优化

对每一行 i,我们可以用一位数组 dp[0...len2] 来表示上一行 dp[i-1][0...len2] 填过的结果。
dp[j] == dp[i-1][j] (top), dp[j-1] = dp[i-1][j-1] (topleft)。难点在于 dp[i][j-1] (left),因为如果将dp[j-1]更新成当前行的结果(dp[i][j-1]),那么前一行对应的dp[i-1][j-1]就消失了。
所以,我们可以用 left 来表示 dp[i][j-1],正确的计算出当前对应 dp[i][j] 的值 cur 后,再更新 dp[j-1] 成 left,也就是 dp[i][j-1],然后 left 再重新赋值成 cur,j 向右移动一列。

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        len1, len2 = len(word1), len(word2)

        dp = [j for j in range(len2+1)] # 0, 1, 2, 3...

        for i in range(1, len1+1):
            left = i
            for j in range(1, len2+1):
                # 对应的两个字符相等,什么编辑都不需要做
                # 编辑距离等于 word1[:i-1] 与 word2[:j-1] 的编辑距离
                if word1[i-1] == word2[j-1]:
                    cur = dp[j-1]
                else:
                    top, topleft = dp[j], dp[j-1]
                    cur = min(
                        # 可以通过在 word1[:i] 后插入 w2[j] 变成 word2[:j], 
                        # 那首先至少需要先通过将 word[:i] 编辑成 word2[:j-1]
                        # 因此,编辑距离等于 word1[:i] 与 word2[:j-1]的编辑距离 + 1
                        left + 1, 
                        # 可以通过在 word1[:i] 后删除 w1[i] 变成 word2[:j], 
                        # 那首先至少需要先通过将 word[:i-1] 编辑成 word2[:j]
                        # 因此,编辑距离等于 word1[:i-1] 与 word2[:j]的编辑距离 + 1
                        top + 1,     
                        # 可以通过把 word1[:i] 的 w1[i] 替换成 w2[j] 使其变成 word2[:j], 
                        # 那首先至少需要先通过将 word[:i-1] 编辑成 word2[:j-1]
                        # 因此,编辑距离等于 word1[:i-1] 与 word2[:j-1]的编辑距离 + 1
                        topleft + 1,                            
                    )
                dp[j-1], left = left, cur
            dp[-1] = left

        return dp[-1]

空间复杂度: O(len2)

[剑指 offer] 36. 二叉搜索树与双向链表

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

面试题36. 二叉搜索树与双向链表 - 力扣(LeetCode)


二叉搜索树转双向循环链表

BST 的特点是根节点大于左子树的最大节点,小于右子树的最小节点。
那么,其对应的双向链表,根节点的左孩子应该指向左子树的最大节点,右孩子指向右子树的最小节点。这实际上,就是在做中序遍历。
我们可以维护一个指针 pre 指向目前处理过链表的最后一个节点,然后中序遍历不停地与这个节点进行链接。

终止条件:越过叶子节点,直接返回

递归中序遍历左子树

处理当前节点

  • 如果处理左子树得不到 self.pre,则当前节点是 head,记录
  • 否则与 self.pre 链接(左子树的最大节点)
  • self.pre 指向当前节点

递归中序遍历右子树

"""
"""
# Definition for a Node.
class Node:
    def __init__(self, val, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right
"""
class Solution:
    def treeToDoublyList(self, root: 'Node') -> 'Node':
        self.head, self.pre = None, None

        def inorder(cur):
            if not cur: return 
            inorder(cur.left)
            if not self.pre:
                # 如果 self.pre 没有值
                # 说明没有左子树可以遍历
                # 当前节点是链表的头结点(整棵树的最左边叶子,最小值)
                self.head = cur
            else:
                self.pre.right, cur.left = cur, self.pre
            self.pre = cur

            # 中序遍历,会先访问到右子树的最左节点
            # 然后与 self.pre (也就是 cur) 进行连接
            inorder(cur.right)

        if not root: return None
        inorder(root)
        self.head.left, self.pre.right = self.pre, self.head
        return self.head

时间复杂度 O(N) : N为二叉树的节点数,中序遍历需要访问所有节点。
空间复杂度 O(N) : 最差情况下,即树退化为链表时,递归深度达到 N,系统使用O(N) 栈空间。

[剑指 offer] 60. n个骰子的点数

把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。

 

你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。

 

示例 1:

输入: 1
输出: [0.16667,0.16667,0.16667,0.16667,0.16667,0.16667]
示例 2:

输入: 2
输出: [0.02778,0.05556,0.08333,0.11111,0.13889,0.16667,0.13889,0.11111,0.08333,0.05556,0.02778]
 

限制:

1 <= n <= 11

面试题60. n个骰子的点数 - 力扣(LeetCode)


n 个骰子抛出 s 点数的次数一定与 第 n 个抛出的点数 cur 与前 n-1 个抛出 s-cur 的次数有关

f(n, s) = \sum_{cur=1}^6 f(n-1, s-cur)

所以,我们可以使用动态规划解决这道题目

状态:dp[n][s] 用 n 个骰子抛出 和为 s 点数。(从最后一个状态来确定会比较直观)
转移方程:就是上述公式,dp[n][s] += dp[n-1][s-cur]
边界处理:初始化所有已知的状态。这里即只投一个骰子的状态 dp[1][1...6] = 1

class Solution:
    def twoSum(self, n: int) -> List[float]:
        dp = [[0] * (6*n+1) for _ in range(n+1)] # dp[0...n][0...6*n]

        for cur in range(1, 7):
            dp[1][cur] = 1
        

        for i in range(2, n+1):
            for s in range(i, i*6+1): # i 个骰子的点数最小为 i,最大是 6*i
                for cur in range(1, 7):
                    if s - cur > 0:
                        dp[i][s] += dp[i-1][s-cur]
        
        all = math.pow(6, n)
        res = []
        for s in range(n, 6*n+1):
            res.append(dp[n][s] / all)
        return res

时间复杂度: O(n^2),n个骰子,每个遍历了 n~n6 个可能的点数和
空间复杂度: O(n^2), dp 数组,n 个骰子,每个骰子都有 6
n个状态

DP 的空间优化

我们可以发现,dp[i][s] 只和 上一轮状态dp[i-1][...] 有关,那么我们就可能不需要使用二维数组来存储,而使用一维数组来存储。在投第 i 个骰子时,dp[s] 表示投 i-1 个骰子得到 s 点数的数量(i.e., dp[i-1][s]). 这样当前的更新就可以变成 dp[s] += dp[s-cur]
注意到,我们此时对 s 不能从 i 正向遍历更新到 i*6了,因为如果正向更新,那么当更新 dp[s] 时,dp[s-1] 就不是上一轮 i-1 个骰子时的状态了!所以,我们可以采用反向更新,这样就不会造成这样的后果

class Solution:
    def twoSum(self, n: int) -> List[float]:
        dp = [[0] * (6*n+1) for _ in range(n+1)] # dp[0...n][0...6*n]

        dp = [0] * (6*n+1)

        for cur in range(1, 7):
            dp[cur] = 1
        
        for i in range(2, n+1):
            for s in range(i*6, i-1, -1):
                dp[s] = 0 # 每次更新需要重新计算,s只与 s-1 ... s-6有关
                for cur in range(1, 7):
                    if s - cur >= i - 1: # 前i-1 个骰子不可能抛出小于 i-1 的点数
                        dp[s] += dp[s-cur]
        
        all = math.pow(6, n)
        res = []
        for s in range(n, 6*n+1):
            res.append(dp[s] / all)
        return res

空间复杂度:O(n) 一共 6*n 个状态

[剑指 offer] 27. 二叉树的镜像

请完成一个函数,输入一个二叉树,该函数输出它的镜像。

例如输入:

     4
   /   \
  2     7
 / \   / \
1   3 6   9

镜像输出:

     4
   /   \
  7     2
 / \   / \
9   6 3   1

示例 1:

输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]

限制:

0 <= 节点个数 <= 1000

来源:面试题27. 二叉树的镜像 - 力扣(LeetCode)


递归地交换左右子树即可。

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = 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

[Leetcode] 4. 寻找两个正序数组的中位数

给定两个大小为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。

请你找出这两个正序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。

你可以假设 nums1 和 nums2 不会同时为空。

 

示例 1:

nums1 = [1, 3]
nums2 = [2]

则中位数是 2.0
示例 2:

nums1 = [1, 2]
nums2 = [3, 4]

则中位数是 (2 + 3)/2 = 2.5

4. 寻找两个正序数组的中位数 - 力扣(LeetCode)


如果没有时间复杂度至多是 log(m+n),的限制,可以用双指针指向两个数组,不停比较大小遍历 (m+n) / 2 次,拿到第 k 个数字,复杂度是 O(m+n)

对数的复杂度就需要想到二分了

然后贼难,又研究了题解

两个有序数组的中位数实际上就是在两个数组中找第 (m+n+1) // 2 (odd) 或 (m+n) // 2 & (m+n)// 2 + 1 (even) 个数。更普式地讲,就是在两个数组中找第 k 个数。

记得之前有一道题是在无序数组中用 O(n) 的算法找 k-th,用了 partition

那么,我们每次可以在 nums1 和 nums2 中各取最小的 k//2 个数 (0...k//2-1),此时,对于任意一个数组来说,它的第 k//2 - 1 个数前都有 k//2 - 1 个数是小于它的 (0...k//2-2)。如果此时

  1. nums1[k//2-1] <= nums2[k//2-1],那么最多有 nums1[0...k//2-2]nums2[0...k//2-2] 一共(k//2 - 1) * 2 = k - 2个数是小于nums1[k//2-1]的,则 nums1[k//2-1] 最多最多是第 k-1 个数,反正一定不是第 k 个。因此,nums1 就可以排除掉 nums2[0...k//2-1]k//2 个数。下次只需要在剩下的两个数组范围里找 第k - k//2个数就可以了。
  2. 对于 nums1[k//2-1] < nums2[k//2-1],反之亦然(有没有等号都可以合并)

还需要注意几个特殊情况做为边界条件

  1. nums1 或 nums2 已经访问完了,那要找的数就在剩下另个数组直接根据索引找
  2. k=1 时,下一对比较数中最小的就是要找的数了

通过这样的方法,每次排除 k/2,k 一开始是 m+n / 2,时间复杂度和二分算法一样(每次排除掉当前范围的一半),是 log(m+n/2) = log(m+m)

class Solution:
    def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
        def find_kth(k):
            n1, n2 = len(nums1), len(nums2)

            idx1, idx2 = 0, 0

            while True:
                # nums1 或 nums2 比较完了
                if idx1 == n1: return nums2[idx2+k-1]
                if idx2 == n2: return nums1[idx1+k-1]
                # 如果 k == 1,那下一个数就是要找的数
                # 在 idx1 和 idx2 中取最小的
                if k == 1: return min(nums1[idx1], nums2[idx2])

                # 正常情况
                # 每个 nums 取 idx...idx + k//2 - 1,分别考察 k//2 个数
                new_idx1 = min(idx1 + k//2 - 1, n1 - 1)
                new_idx2 = min(idx2 + k//2 - 1, n2 - 1)

                # 如果 nums1[new_idx1] <= nums2[new_idx2], 说明至少可以排除掉
                # 至多有 (k//2 - 1) * 2 = k - 2 个数比 nums1[new_idx1] 小
                # nums[idx1] 最多是第 k-1 个数,肯定不是第 k 个数
                # 因此可以排除掉刚刚考察的 k//2 个 nums1 中的数,nums[idx1...new_idx1]
                # 他们肯定都比第 k 个数小
                if nums1[new_idx1] <= nums2[new_idx2]:
                    # 已经排除掉 nums1 中的那些数字了
                    # 只需要在剩下的数字里找一个更小第k位数
                    k -= new_idx1 - idx1 + 1
                    idx1 = new_idx1 + 1
                else:
                    k -= new_idx2 - idx2 + 1
                    idx2 = new_idx2 + 1

        N = len(nums1) + len(nums2)

        if N & 1: return find_kth(N + 1 >> 1)
        else: return 0.5 * (find_kth(N >> 1) + find_kth((N >> 1) + 1))

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.