LeetCode刷题笔记【二】

#046 全排列

https://leetcode-cn.com/problems/permutations/

题目考察回溯思想

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        if len(nums) == 0:
            return []
        used_set = set()  # 保存排列过程中已经使用过的元素
        outs = []
        self.backtrack(nums, 0, [], used_set, outs)
        return outs

    def backtrack(self, nums, index, pres, used_set, outs):
        # 终止条件
        if index == len(nums):
            outs.append(pres[:])
            return

        for i in range(len(nums)):
            # 前面使用过的元素不再使用
            if nums[i] not in used_set:
                used_set.add(nums[i])
                pres.append(nums[i])
                self.backtrack(nums, index + 1, pres, used_set, outs)
                # 状态重置
                used_set.remove(nums[i])
                pres.pop()

以对nums=[3 1 2]全排列为例,画出其递归树:

  1. 调用backtrack(),递归终止没有终止则继续执行
  2. 当前节点有哪些路径可选:循环选则,初始节点3 2 1均可选,第一次循环选则nums[0]=3
  3. 已选元素标记占用:将nums[0]添加到used_set,标记nums[0]占用,将选则的元素添加到pres
  4. 调用backtrack()进入下一个节点:(当前节点有哪些路径可选:循环选则,nums[0]标记为占用,1 2可选,第一次循环选则nums[1]=1;将选则的元素添加到pres,将nums[1]添加到used_set,标记nums[1]占用;调用backtrack()进入下一个节点。(重复1234步,即一直向下,递归到更深的节点))
  5. 直到满足终止条件,退出递归,返回上一节点(回溯):执行状态重置下面的代码,解除对元素的占用,继续上一层节点的循环选则

#053 最大子序和

https://leetcode-cn.com/problems/maximum-subarray/

题目考察动态规划思想

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        max_sum = nums[0]
        local_sum = 0
        for curr_num in nums:
            if local_sum > 0:  # 以上一个节点为结尾的所有子序列中最大的和
                local_sum += curr_num  # 以当前节点为结尾的所有子序列中最大的和
            else:
                local_sum = curr_num  
            # 状态转移max_sum[i] = max(max_sum[i-1], local_sum[i])
            max_sum = max(max_sum, local_sum)
        return max_sum

"""class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        # 暴力法求解超时O(n^2)
        max_sum = nums[0]
        for i in range(len(nums)):
            curr_sum = nums[i]
            if curr_sum >= max_sum:
                max_sum = curr_sum
            for j in range(i + 1, len(nums)):
                curr_sum += nums[j]
                if curr_sum >= max_sum:
                    max_sum = curr_sum
        return max_sum"""

暴力解法的思路是,遍历以当前节点开头的所有子序列,寻找和最大的子序;

换一个思路,遍历以当前节点为结尾的子序列,寻找最大子序该怎么做呢?

这就可以划分子问题:对序列[a b c d],假设以节点d为结尾的子序列[... d]有最大和,那么,当前节点定位到d:

  1. 要么,以c为结尾的子序列[... c]中存在和大于0的序列,最终的结果是max_sum=...+c+d
  2. 要么,以c为结尾的子序列[... c]的和都小于或等于0,最终的结果是max_sum=d
  3. 接着当前节点定位到c,继续1 2过程,找出这个子序列

可以看出,上面的过程是一个递归的思想(自顶向下)。如果我们从最小的子问题开始,就变成了一个自下向上的动态规划问题,从节点a开始,将当前节点视为子序列的结尾:

  1. 初始状态max_sum=a,local_sum=0(是以当前节点为结尾的所有子序列中最大的和)
  2. 当前节点指向a
  3. 前一个节点为null,以null为结尾的子序列最大和是初始化的值local_sum=0
  4. local_sum不大于0(即local_sum不能使以下一个节点b为结尾的子序列的和变大),舍弃local_sum,使它等于当前节点的值;local_sum>0,(即local_sum能使以下一个节点b为结尾的子序列的和变大),local_sum加上当前节点a的值。此时的local_sum是以当前节点为结尾的所有子序列中最大的和。
  5. 状态转移,max_sum[i] = max(max_sum[i-1], local_sum[i]),本次最大和max_sum等于上次最大和与本次local_sum之间更大的那一个;
  6. 当前节点指向b
  7. 前一个节点为a,以a为结尾的子序列最大和是local_sum
  8. 重复上述过程,直到得到最终状态(以各个节点为结尾的子序列中最大的和)

#056 合并区间

https://leetcode-cn.com/problems/merge-intervals/

题目考察合并区间的方法,先排序再合并,可以降低时间复杂度

class Solution:
    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        if not intervals:
            return
        # 按每个区间的第一个元素对列表升序排序
        intervals.sort()
        outs = [intervals[0]]
        for interval in intervals[1:]:
            # 用[[x1, y1], [x2, y2], ...]
            # 按x排序后,相邻区间,前一个区间的y大于等于后一个区间的x就可以合并
            if outs[-1][-1] >= interval[0]:
                outs[-1][-1] = max(interval[-1], outs[-1][-1])
            else:
                outs.append(interval)
        return outs

如果不排序,直接合并,需要对区间两两比较。

先按每个区间的x升序排序之后,相邻区间,前一个区间的y大于等于后一个区间的x就可以合并,选择前一个区间的x和两个区间较大的y合并为一个新的区间。

sort() 函数用于对原列表进行排序:list.sort( key=None, reverse=False)

  • key -- 是带一个参数的函数,指定可迭代对象中的一个元素来进行排序。
  • reverse -- 排序规则,reverse = True 降序, reverse = False 升序(默认)。

例如对二维、三维列表排序:

test = [[1, 5], [4, 3], [2, 6]]
test.sort(key=lambda x: x[0])   # 按第一维每个区间的第一个元素排序
print(test)
test.sort(key=lambda x: x[1])  # # 按第一维每个区间的第二个元素排序排序
print(test)

test = [[[1, 5], [4, 3]], [[6, 6], [3, 4]], [[3, 4], [2, 4]]]
test.sort(key=lambda x: x[0])   # 按第二维每个区间的第一个元素的第一个元素排序x[0][0]
print(test)
test.sort(key=lambda x: x[1][0])  # 按第二维每个区间的第二个元素的第一个元素排序
print(test)

结果依次是:

[[1, 5], [2, 6], [4, 3]]
[[4, 3], [1, 5], [2, 6]]
[[[1, 5], [4, 3]], [[3, 4], [2, 4]], [[6, 6], [3, 4]]]
[[[3, 4], [2, 4]], [[6, 6], [3, 4]], [[1, 5], [4, 3]]]

#011 盛水最多的容器

https://leetcode-cn.com/problems/container-with-most-water/

class Solution:
    def maxArea(self, height: List[int]) -> int:
        # 双指针,指向列表开始和结尾
        p_left, p_right = 0, (len(height) - 1)
        max_area = 0
        while p_left < p_right:
            curr_area = (p_right - p_left) * min(height[p_left], height[p_right])
            max_area = max(max_area, curr_area)
            if height[p_left] < height[p_right]:
                p_left += 1
            else:
                p_right -= 1
        return max_area

求a1到an任意两边与x轴围成的面积,暴力解法依旧是双循环遍历所有组合。

注意到,使面积S最大,要使宽和高都最大。

  1. 首先最大的宽是双指针的初始位置p_left和p_right,而矩形的高由a1、an中较小的高决定,如图所示。
  2. 接着,要使S有可能变大,将指向较小高的指针向中移动,寻找一个更大的高。(如果移动指向较大高的指针图中p_right,矩形宽变小,而高最大依旧是图中p_left指向的值,面积不可能变大)

#020 有效括号

https://leetcode-cn.com/problems/valid-parentheses/

题目考察对栈先入后出思想的灵活应用

class Solution:
    def isValid(self, s: str) -> bool:
        if s is None:
            return True
        dict_c = {'(': ')', '[': ']', '{': '}', 'none': 'none'}
        strs = []
        for char in s:
            if char in dict_c:
                strs.append(char)
            else:
                if dict_c[strs.pop() if strs else 'none'] is not char:
                    return False
        return not strs

括号的匹配规则恰好是:从左至右,在检索到右括号之前,最后出现的左括号,匹配第一个右括号,而最出现的左括号,最后匹配右括号。与栈后入先出的规则类似。

#055 跳跃游戏

https://leetcode-cn.com/problems/jump-game/

题目考察对贪心规则的理解,能够结合问题特征巧妙确定贪心规则。

class Solution:
    def canJump(self, nums: List[int]) -> bool:
        bound = 0  # 可以跳到的最远位置
        for index in range(len(nums)):
            # 如果当前位置index超出了可以跳到的最远位置,则失败
            if index > bound:
                return False
            # 最远位置更新规则是,“历史最远位置”与“当前节点可跳到的最远位置”的较大者
            bound = max(bound, index + nums[index])
        return True

从起始位置开始,每个索引都对应一个可以到达的最远位置:index+nums[index];

那么当前索引所能到达的最远位置是:“已经走过的索引中能到达的最远位置”bound和:“当前索引可以到达的最远位置”中较大的那一个

设计贪心规则:

  • 可行性:如果当前索引超出了历史记录的可以到达的最远位置,就失败;
  • 局部最优:否则,选择“历史最远位置”与“当前索引可跳到的最远位置”的较大者,作为本次记录的历史最远位置,bound = max(bound, index + nums[index]);

从左至右遍历列表,按上述规则,index可以到达列表末尾,则说明可以从开始跳到末尾。

#075 颜色分类

https://leetcode-cn.com/problems/sort-colors/

题目考察数组排序的方法

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        start = 0
        # 将0排好
        for i in range(len(nums)):
            if nums[i] == 0:
                nums[start], nums[i] = nums[i], nums[start]
                start += 1
        # 接着0之后的位置,排1
        for i in range(start, len(nums)):
            if nums[i] == 1:
                nums[start], nums[i] = nums[i], nums[start]
                start += 1
        return None

利用快速排序和双指针的思想,先确定待分割的数字是0,遍历列表,将所有是0的数字放在列表左侧;

再确定待分割的数字是1,从0之后开始遍历,将所有是1的数字放在0之后。

另一种思想是利用三指针,一次遍历即可,如下:

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        start = 0  # 待交换为0的位置
        end = len(nums) - 1  # 待交换为2的位置
        update_pos = 0  # 当前位置
        while update_pos <= end:
            if nums[update_pos] == 0:
                nums[start], nums[update_pos] = nums[update_pos], nums[start]
                start += 1
                update_pos += 1
            elif nums[update_pos] == 2:
                nums[end], nums[update_pos] = nums[update_pos], nums[end]
                end -= 1
            # 当前位置是1,向后移动当前位置
            else:
                update_pos += 1

#078 子集

https://leetcode-cn.com/problems/subsets/

题目考察访问和创建列表的方法,通过列表推导可以方便的创建列表

class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        res = [[]]
        for num in nums:
            # 列表推导
            res += [([num] + old_num) for old_num in res]
        return res

列表推导创建子集就是:从前向后遍历列表,每遇到一个新元素,就将这个新元素分别添加到每个已经得到的子集上,作为一组新的子集加入到列表子集中,如图所示:

class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        res = []
        pre = []
        self.find_subset(nums, 0, pre, res)
        return res

    def find_subset(self, nums, start, pre, res):
        # 将每层递归得到的子集添加到结果列表
        res.append(pre[:])
        # 终止条件
        if start == len(nums):
            return
        # 从前向后遍历每层列表
        for index in range(start, len(nums)):
            # 当前占用(访问过)的元素加入子集
            pre.append(nums[index])
            self.find_subset(nums, index + 1, pre, res)
            # 解除占用,回溯
            pre.pop()

另一种方法是回溯的思想:

#051 N皇后

https://leetcode-cn.com/problems/n-queens/

题目考察回溯思想,是回溯思想的一个经典案例

class Solution:
    main_diags = set()  # 不可放置皇后的主对角线
    para_diags = set()  # 不可放置皇后的副对角线
    cols = set()  # 不可放置皇后的列

    def solveNQueens(self, n: int):
        res = []
        if n == 0:
            return res

        self.backtrack(0, n, [], res)
        return res

    def backtrack(self, row, size, pre, res):
        """递归 得到当前row的皇后放置位置"""
        # 若存在解,那么每行必有一个皇后
        # 递归到终止条件,返回一个解
        if row == size:
            res.append(self.place_queen(pre, size))
            return
        for col in range(size):
            # 当前row的col列可以放置皇后
            if self.is_set_queen(row, col):
                pre.append(col)  # 有序(pre列表索引为行号,对应元素为列号)记录放置皇后的位置
                self.main_diags.add(row - col)
                self.para_diags.add(row + col)
                self.cols.add(col)
                
                # 进入判断下一行的皇后位置
                self.backtrack(row + 1, size, pre, res)
                # 回溯 恢复状态
                # 释放占用的主副对角线和列
                self.cols.remove(col)
                self.para_diags.remove(row + col)
                self.main_diags.remove(row - col)

                pre.pop()

    def is_set_queen(self, row, col) -> bool:
        """(row,col)位置是否可以放置皇后"""
        if ((row - col) not in self.main_diags) and (
                (row + col) not in self.para_diags) and (
                col not in self.cols):
            return True
        else:
            return False

    def place_queen(self, pre, size):
        """放置一张图上的皇后"""
        return ["." * pre[index] + "Q" + "." * (size - pre[index] - 1) for index in range(size)]

n皇后问题:在n×n格的国际象棋上摆放n个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。

满足这样的要求,要么每行必定会有的一个皇后,得到一个解;要么不存在解(例如n=2)

可以通过递归,从第一行row=0开始

  1. 遍历本行的每一列,该列是否满足放置皇后的条件
  2. 如果满足,记录这个位置,并将这个皇后占据的列、主对角线、副对角线标记为“不可放置”
  3. 递归进入下一行,执行1,2步,直到满足递归终止条件(或一层遍历结束),退出
  4. 退出时,回溯到上一次的状态,解除本次对列、主对角线、副对角线的占用

其中,如果Q在(r,c)位置,那么col=c的列,row+col=r+c的副对角线,row-col=r-c的主对角线被占用。

#059 螺旋矩阵Ⅱ

https://leetcode-cn.com/problems/spiral-matrix-ii/

题目考察对过程的模拟

class Solution:
    def generateMatrix(self, n: int) -> List[List[int]]:
        left, top, right, bottom = 0, 0, n-1, n-1  # 记录左上右下的边界
        step, all_step = 1, n*n  # 1-n*n的数字
        num_map = [[0 for _ in range(n)] for _ in range(n)]
        while step <= all_step:
            # 从左至右,在边界范围内填充一行,填充完毕上边界下移
            for i in range(left, right+1):
                num_map[top][i] = step
                step += 1
            top += 1
            # 从上至下,在边界范围内填充一行,填充完毕右边界左移
            for i in range(top, bottom+1):
                num_map[i][right] = step
                step += 1
            right -= 1
            # 从右至左,在边界范围内填充一行,填充完毕下边界上移
            for i in range(right, left-1, -1):
                num_map[bottom][i] = step
                step += 1
            bottom -= 1
            # 从下至上,在边界范围内填充一行,填充完毕左边界右移
            for i in range(bottom, top-1, -1):
                num_map[i][left] = step
                step += 1
            left += 1
        return num_map

先产生n*n的矩阵,按照填充过程,填入数字:

  1. 从左至右,在边界范围内填充一行,填充完毕上边界下移
  2. 从上至下,在边界范围内填充一行,填充完毕右边界左移
  3. 从右至左,在边界范围内填充一行,填充完毕下边界上移
  4. 从下至上,在边界范围内填充一行,填充完毕左边界右移
  5. 数字没有填充完就返回第一步接着填充

#062 不同路径

https://leetcode-cn.com/problems/unique-paths/

题目考察动态规划,空间换时间的思想

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        d = [[1]*m] + [[1]+[0]*(m-1) for _ in range(n-1)]  # 存储到达每个位置的路径数,O(m*n)的空间复杂度
        for i in range(1, n):
            for j in range(1, m):
                d[i][j] = d[i-1][j] + d[i][j-1]
        return d[-1][-1]

动态规划的关键在于将问题分解为子问题,找到问题的状态转移方程。

  • 记d[i][j]为一个状态,表示可以到达(i,j)位置的路径总数
  • 要到达(i,j)位置,可以从(i-1,j)右移一步或者(i,j-1)下移一步
  • 划分子问题:到达(i,j)位置的路径总数=到达(i-1,j)的路径数与到达(i,j-1)位置的路径数的和
  • 得到状态转移方程:d[i][j] = d[i-1][j] + d[i][j-1]

使用一维列表记忆,还可以降低空间复杂度

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        d = [1]*m  # O(m)空间复杂度
        for i in range(1, n):
            for j in range(1, m):
                d[j] = d[j] + d[j-1]
        return d[-1]

#094 二叉树的中序遍历

https://leetcode-cn.com/problems/binary-tree-inorder-traversal/

题目考察二叉树的中序遍历方法

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


class Solution:
    def inorderTraversal(self, root: TreeNode) -> List[int]:
        res = []
        stack = [(False, root)]  # 先加入头结点,标记为未访问
        while stack:
            used, node = stack.pop()
            if node is None:
                continue
            # 当前节点没有被访问过,则将其右子节点、自身和左子节点加入栈
            if not used:
                stack.append((False, node.right))
                stack.append((True, node))  # 当前节点已访问,标记为True
                stack.append((False, node.left))
            else:
                res.append(node.val)
        return res

首先中序遍历的意思是:对于当前结点,先遍历它的左子树,访问根节点,然后输出该节点,遍历它的右子树,访问根节点。如下图所示:

  • 1-->2-->4,4 的左子树为空,输出 4,接着右子树;
  • 6 的左子树为空,输出 6,接着右子树;
  • 7 的左子树为空,输出 7,右子树也为空,此时 2 的左子树全部输出,输出 2,2 的右子树为空,此时 1 的左子树全部输出,输出 1,接着 1 的右子树;
  • 3-->5,5 左子树为空,输出 5,右子树也为空,此时 3 的左子树全部输出,而 3 的右子树为空,至此 1 的右子树全部输出,结束。

这种节点的访问顺序与栈先入后出一致,

  • 将节点1的右子节点3、节点1、左子节点2依次压入栈,然后弹出节点2继续这样的遍历过程,直到遇到子节点为null,不再添加节点,弹出栈中的上一节点。
  • 将访问过的节点标记为True,未访问的节点标记为False
  • 如果弹出的节点是访问过的,那么将它加入中序遍历结果列表,不再访问它

#032最长有效括号

https://leetcode-cn.com/problems/longest-valid-parentheses/

题目考察栈的思想

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        maxlen = 0
        stack = [-1]  # 若从s[0]开始的括号都匹配,其长度为最后一个括号的索引-(-1)
        for index in range(len(s)):
            if s[index] == '(':
                stack.append(index)
            elif s[index] == ')':
                stack.pop()
                # 栈空,从index=0开始的一组括号匹配完成;
                # 并且把下一组第一个不匹配的索引压入栈底;
                # 作为计算下一组括号可能的最大长度的基准
                if not stack:
                    stack.append(index)
                maxlen = max(maxlen, index-stack[-1])
        return maxlen
  • 对于遇到的每个"(" ,将它的下标放入栈中
  • 对于遇到的每个")",弹出栈顶的元素(即弹出与")"匹配的那个"(")
  • 并将当前元素的下标与剩余的栈顶元素下标作差,得出当前有效括号字符串的长度

通过这种方法,我们继续计算有效子字符串的长度,并最终返回最长有效子字符串的长度。

#091解码方法

https://leetcode-cn.com/problems/decode-ways/

题目考察动态规划的思想,类似爬楼梯问题

class Solution:
    def numDecodings(self, s: str) -> int:
        if not s or s[0] == '0':
            return 0
        pre, curr = 1, 1  # 动态规划中上一次和上上一次的状态(解码的方法数)
        for i in range(1, len(s)):
            if s[i] == '0':
                # 直接字符比较,而不是转换成整数,降低运行实间
                # if s[i-1] == '1' or (s[i-1] == '2' and s[i] <= '6'):
                if s[i-1] != '0' and int(s[i - 1])*10+int(s[i]) <= 26:
                    pre, curr = curr, pre
                else:
                    return 0
            else:
                if s[i-1] != '0' and int(s[i - 1])*10+int(s[i]) <= 26:
                    pre, curr = curr, curr + pre
                else:
                    pre, curr = curr, curr
        return curr

这个问题与爬楼梯问题(从起点开始,一次可以跨一级台阶或两级台阶,求到最后一级台阶的行走方法)类似,只不过在解码的每一步都多了一些条件需要判断。

首先,s为空或者s[0]=='0'是没有解的,

1. s[i] == '0' 即当前数字不能单独解码

1)s[i-1]s[i] <= 26 但与前一个数组合可以解码

pre, curr = curr, pre

2)与前一个数组合可不以解码,解码失败

2. s[i] != '0' 当前数字本身可以解码

1)s[i-1]s[i] <= 26 并且与前一个数组合也可以解码

pre, curr = curr, curr + pre

2)与前一个数组合后就不可以解码

pre, curr = curr, curr

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值