Leetcode题解记录-hot100(81-100)

81、70爬楼梯(动态规划,简单)

70. 爬楼梯 - 力扣(LeetCode)

思路:就是类似斐波那契数列

复制代码
def climbStairs(self, n: int) -> int:
        if n == 1:
            return 1
        a, b = 1, 1
        res = 0
        for i in range(2, n + 1):
            res = a + b
            a, b = res, a
        return res

82、118杨辉三角(动态规划,简单)

118. 杨辉三角 - 力扣(LeetCode)

思路:

  • 外层循环 i 表示当前行号(从 0 开始)。

  • 每行先创建临时列表并放入开头的 1

  • 内层循环 for j in range(1, i): 处理中间元素(当 i >= 2 时才执行),利用上一行 res[i-1] 的相邻两数求和。

  • 最后,如果 i >= 1(即不是第一行),补上末尾的 1

复制代码
def generate(self, numRows: int) -> List[List[int]]:
        res = []
        for i in range(numRows):
            tmp = []
            tmp.append(1)
            for j in range(1, i):
                tmp.append(res[i - 1][j - 1] + res[i - 1][j])
            if i >= 1:
                tmp.append(1)
            res.append(tmp)
        return res

83、198打家劫舍(动态规划,中等)

198. 打家劫舍 - 力扣(LeetCode)

定义二维状态:dpi0 = 前 i 间房屋,且 不偷第 i 间 的最高金额

dpi1 = 前 i 间房屋,且 偷第 i 间 的最高金额

在第 i 间房屋前,思考"最后一步"的两种可能:

  • 不偷第 i 间(状态 0)

    既然不偷 i,那 i-1 可以偷也可以不偷,取最大值即可:

    dp[i][0] = max(dp[i-1][0], dp[i-1][1])

  • 偷第 i 间(状态 1)

    如果偷 i,则 i-1 一定不能偷 ,所以只能从 i-1 的不偷状态转移,并加上当前房屋的金额:

    dp[i][1] = dp[i-1][0] + nums[i]

  • 该代码空间 O(n),但 dpi 只依赖 dpi-1,所以可以用两个变量滚动更新,降为 O(1)

复制代码
def rob(self, nums: List[int]) -> int:
        n = len(nums)
        dp = [[0 for _ in range(2)] for _ in range(n)]
        dp[0][0] = 0
        dp[0][1] = nums[0]
        for i in range(1, n):
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1])
            dp[i][1] = dp[i - 1][0] + nums[i]
        return max(dp[n - 1][0], dp[n - 1][1])

84、279完全平方数(动态规划,中等)

279. 完全平方数 - 力扣(LeetCode)

思路:完全背包问题

dp[i] 表示凑出整数 i 的最少平方数个;对于当前的i,dpi = min( dpi - j² + 1 ) ,对所有 k² ≤ i

可以枚举这些数,假设当前枚举到 j,那么还需要取若干数的平方,构成 i−j2。此时发现该子问题和原问题类似,只是规模变小了

复制代码
def numSquares(self, n: int) -> int:
        dp = [inf] * (n + 1)
        dp[1] = 1
        dp[0] = 0
        x = math.isqrt(n)
        count = [0] * (x)
        for i in range(1, x + 1):
            count[i - 1] = i**2
        for i in range(2, n + 1):
            for k in count:
                if k <= i:
                    dp[i] = min(dp[i - k] + 1, dp[i])
        return dp[n]

85、322零钱兑换(动态规划,中等)

322. 零钱兑换 - 力扣(LeetCode)

思路:这道题和"完全平方数"完全同模型------都是无限物品求最小数量。

dp[i] 表示凑出金额 i 所需的最少硬币数

考虑最后一步用了哪一枚硬币 coin

dp[i] = min(dp[i], dp[i - coin] + 1),对所有 coin <= i

其他 dp[i] 初始化为一个足够大的值 (如 float('inf') 或 amount+1),表示暂时不可达。用 inf 的好处是:min 操作永远安全;检查最终答案时,若 dp[amount] 还是 inf,说明无法凑出,返回 -1.

我写时候的错误逻辑漏洞:

if j<=i:

if dpi==-1 and dpi-j!=-1:

dpi=dpi-j+1

else:

dpi=min(dpi, dpi-j+1)

如果dpi不是-1,而dpi-j是-1,那么更新dpi时可能用min(dpi, dpi-j+1),但dpi-j是-1,dpi-j+1 = 0,这会导致错误地将dpi改成0。

复制代码
def coinChange(self, coins: List[int], amount: int) -> int:
        dp = [-1] * (amount + 1)
        dp[0] = 0
        for i in range(1, amount + 1):
            for j in coins:
                if j <= i and dp[i - j] != -1:
                    if dp[i] == -1:
                        dp[i] = dp[i - j] + 1
                    else:
                        dp[i] = min(dp[i], dp[i - j] + 1)
        return dp[amount]

86、139单词拆分(动态规划,中等)

139. 单词拆分 - 力扣(LeetCode)

思路:

  • 状态dp[i] 表示 s 的前 i 个字符能否被拆分(用 0/1 表示)。

  • 初始化dp[0] = 1,空字符串可拼。

  • 转移 :对每个 i,寻找一个切分点 j,使得前缀 dp[j] 可拼且 s[j:i] 在字典里。

复制代码
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        dp = [0] * (len(s) + 1)
        dp[0] = 1
        for i in range(1, len(s) + 1):
            for j in range(0, i):
                if dp[j] == 1 and s[j:i] in wordDict:
                    dp[i] = 1
        return True if dp[len(s)] == 1 else False

87、300最长递增子序列(动态规划,中等)

300. 最长递增子序列 - 力扣(LeetCode)

思路:注意不要求连续

dpi 应该表示 以第 i 个元素结尾的最长递增子序列长度(必须包含 numsi

要想得到 dp[i],需要看它之前所有元素 j < i

  • 只要 nums[j] < nums[i]nums[i] 就可以接在以 nums[j] 结尾的子序列后面

  • 所以 dp[i] = max( dp[j] + 1 ),对所有满足 j < inums[j] < nums[i] 的 j

  • 如果没有任何 j 满足,dp[i] = 1(自己单独成为一个子序列)

复制代码
def lengthOfLIS(self, nums: List[int]) -> int:
        n = len(nums)
        dp = [0] * n
        dp[0] = 1
        for i in range(1, n):
            for j in range(i):
                if nums[j] < nums[i]:
                    dp[i] = max(dp[i], dp[j] + 1)
            if dp[i] == 0:
                dp[i] = 1
        return max(dp)

88、152乘积最大子数组(动态规划,中等)

152. 乘积最大子数组 - 力扣(LeetCode)

思路:乘积问题因为负数反转符号,所以需要双状态。类似地:如果遇到"乘积最大子序列(不要求连续)",可能也得同时记录最大积和最小积。如果是求和,就不需要,因为加法不存在符号反转问题。

  • dpmax[i]:以 nums[i] 结尾的连续子数组的最大乘积

  • dpmin[i]:以 nums[i] 结尾的连续子数组的最小乘积

  • 我最初用了一堆if else来判断区分 nums[i] 的正负,其实完全没有必要,直接统一, maxmin 会自动处理好符号反转、遇零归零、单独取当前数等所有情况。

  • 因为第 i 步只依赖第 i-1 步,空间可以优化到 O(1)

复制代码
def maxProduct(self, nums: List[int]) -> int:
        n = len(nums)
        dpmax, dpmin = [0] * n, [0] * n
        dpmax[0] = dpmin[0] = nums[0]
        for i in range(1, n):
            dpmax[i] = max(nums[i] * dpmax[i - 1], nums[i] * dpmin[i - 1], nums[i])
            dpmin[i] = min(nums[i] * dpmax[i - 1], nums[i] * dpmin[i - 1], nums[i])
        return max(dpmax)

 def maxProduct(self, nums: List[int]) -> int:
        n = len(nums)
        res = pre_max = pre_min = nums[0]
        for i in range(1, n):
            cur_max, cur_min = pre_max, pre_min
            pre_max = max(nums[i] * cur_max, nums[i] * cur_min, nums[i])
            pre_min = min(nums[i] * cur_max, nums[i] * cur_min, nums[i])
            res = max(res, pre_max)
        return res

89、416分割等和子集(动态规划,中等)

416. 分割等和子集 - 力扣(LeetCode)

思路:总和必须能被 2 整除,和为奇数直接返回false;

每个子集的和 = sum(nums) / 2,设这个值为 target

→ 题目变成:能否从 nums 中选出一部分数,使它们的和正好为 target

0/1 背包的特征:每个物品只有选/不选两种可能,且最多选一次。

dp[i][j],其中 i 表示前 i 个数,j 表示某个和,前 i 个数能否凑出和 j

  • 不选第 i 个数(值为 num):dp[i][j] 取决于 dp[i-1][j]

  • 选第 i 个数:dp[i][j] 取决于 dp[i-1][j - num](前提是 j >= num

两者满足其一即可,所以用"逻辑或"连接。

一维dp:可以发现上面的二维dp任何一行的计算,只依赖于上一行同一列和更左侧的某列。

原理总结:

  • 当我们计算 dp[j] 时,需要的是 dp[i-1][j-num],也就是"上一行"更左侧的值。

  • 在一维滚动数组中,这个"上一行"的值就存储在当前的 dp[j-num] 中。

  • 如果 先更新小索引,我们会把上一行的值提前污染成这一行的值,导致同一个数被重复使用(完全背包效果)。

  • 如果 先更新大索引,当我们更新 dp[j] 时,dp[j-num] 还保留着上一行的状态,因为 j-num < j,还没被这一轮更新覆盖。这就完美模拟了 0/1 背包:每个数只用一次。

  • 对于 0/1 背包,所有二维 DP 只要满足"仅依赖上一行",都能压缩成一维

复制代码
def canPartition(self, nums: List[int]) -> bool:
        if sum(nums)%2!=0:return False
        target=int(sum(nums)/2)
        n=len(nums)
        dp=[[0 for _ in range(target+1)] for _ in range(n+1)]
        dp[0][0]=1
        for i in range(1,n+1):
            dp[i][0]=1
            for j in range(1,target+1):
                if dp[i-1][j] or (j>=nums[i-1] and dp[i-1][j-nums[i-1]]):
                    dp[i][j]=1
                else:
                    dp[i][j]=0
        return dp[n][target]==1

if sum(nums) % 2 != 0:
            return False
        target = int(sum(nums) / 2)
        n = len(nums)
        dp = [0] * (target + 1)
        dp[0] = 1
        for num in nums:
            for j in range(target, -1, -1):
                if j >= num:
                    dp[j] = dp[j - num] or dp[j]
        return dp[target] == 1

90、32最长有效括号(动态规划,困难)

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

思路:

状态定义:dp[i] = 以 s[i] 结尾的最长有效括号子串长度

  • 如果 s[i]=='(',长度只能是 0(continue 维持初始化的 0)。

  • 如果 s[i]==')',分两种情况:

情况 As[i-1]=='('

...() 结构,dp[i] = 2 + dp[i-2]

情况 Bs[i-1]==')'

→ 先找到与当前 ) 配对的候选位置 pre = i - dp[i-1] - 1

  • pre < 0s[pre] != '(',匹配失败,dp[i]=0

  • s[pre] == '(',匹配成功,有效长度 = dp[i-1] + 2 + 前面可能接上的有效长度 dp[pre-1](如果 pre-1 >= 0

复制代码
def longestValidParentheses(self, s: str) -> int:
        n = len(s)
        if n == 1 or n == 0:
            return 0
        dp = [0] * (n)
        dp[0] = 0
        dp[1] = 2 if s[1] == ")" and s[0] == "(" else 0
        for i in range(2, n):
            if s[i] == "(":
                continue
            else:
                if s[i - 1] == "(":
                    dp[i] = 2 + dp[i - 2]
                else:
                    if i - dp[i - 1] - 1 >= 0:
                        tmp = s[i - dp[i - 1] - 1]
                        if tmp == ")":
                            continue
                        else:
                            if i - dp[i - 1] - 2 >= 0:
                                dp[i] = dp[i - dp[i - 1] - 2] + dp[i - 1] + 2
                            else:
                                dp[i] = i + 1
        return max(dp)

91、62不同路径(多维动态规划,中等)

62. 不同路径 - 力扣(LeetCode)

思路:最后一步要么从上边 (i-1, j) 下来,要么从左边 (i, j-1) 过来。

dp[i][j] = dp[i-1][j] + dp[i][j-1]

优化:计算第 i 行时,只用到了第 i-1 行(上一行)以及本行左边的格子

因此可以只维护一行数组 dp[j],代表"上一行"的状态,然后一边计算一边更新。

复制代码
dp[j] = dp[j] + dp[j-1]   # dp[j] 是上一行的值,dp[j-1] 是本行刚算出的左边值
复制代码
 def uniquePaths(self, m: int, n: int) -> int:
        dp = [[0] * n for _ in range(m)]
        for i in range(0, m):
            dp[i][0] = 1
        for i in range(0, n):
            dp[0][i] = 1
        for i in range(1, m):
            for j in range(1, n):
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
        return dp[m - 1][n - 1]

92、64最小路径和(多维动态规划,中等)

64. 最小路径和 - 力扣(LeetCode)

思路:dp[j]:在当前正在处理的行中,从左上角 (0,0) 走到 (当前行, j) 的最小路径和。

复制代码
def minPathSum(self, grid: List[List[int]]) -> int:
        m = len(grid)
        n = len(grid[0])
        dp = [0] * n
        dp[0] = grid[0][0]
        for i in range(1, n):
            dp[i] = dp[i - 1] + grid[0][i]
        for i in range(1, m):
            dp[0] = dp[0] + grid[i][0]
            for j in range(1, n):
                dp[j] = min(dp[j], dp[j - 1]) + grid[i][j]
        return dp[n - 1]

93、5最长回文子串(多维动态规划,中等)

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

思路:暴力解法: 时间复杂度O(n³);以 i 结尾时,你从 j = i - dp[i-1] - 1(即"以 i-1 结尾的最长回文"的左边界再往左一位)开始,一直检查到 j = i,对每个候选的 j,切片 s[j:i+1],反转对比,如果是回文就更新 dp[i]

区间DP :时间复杂度O(n2),空间复杂度O(n2)。dp[i][j]:用 0/1 表示 s[i..j] 是否是回文(1 为真),初始化长度为 1 的所有子串为回文;外层枚举长度,内层枚举左端点。dp[i][j] 依赖 dp[i+1][j-1],在表格上就是左下角的那个格子,所以需要按照长度枚举。

**中心扩展法:**时间复杂度O(n2),空间复杂度O(1)。回文不是由端点定义的,而是由对称中心定义的。一个回文子串,本质上是从某个中心向两边对称展开的结果。

如果 sleft == sright,说明这一层是对称的;left 左移一位,right 右移一位。

  1. 遍历所有可能的中心:奇数中心:每个字符都是一个中心,共 n 个;偶数中心:每两个相邻字符之间的缝隙,共 n-1 个;总共 2n - 1 个中心

  2. 对每个中心,分别尝试扩展:奇数扩展:expand(s, i, i);偶数扩展:expand(s, i, i+1)

复制代码
class Solution:
    def longestPalindrome(self, s: str) -> str:
        n = len(s)
        dp = [1] * n
        for i in range(1, n):
            start = i - dp[i - 1] - 1
            if start < 0:
                start = 0
            for j in range(start, i + 1):
                tmp = s[j : i + 1]
                if tmp == tmp[::-1]:
                    dp[i] = max(dp[i], len(tmp))
        maxlen = 0
        maxi = 0
        for i in range(0, n):
            if dp[i] > maxlen:
                maxlen = dp[i]
                maxi = i

        return s[maxi - maxlen + 1 : maxi + 1]

class Solution:
    def longestPalindrome(self, s: str) -> str:
        n = len(s)
        if n == 1:
            return s
        dp = [[0] * n for _ in range(n)]
        maxlen = 1
        resj = 0
        for i in range(0, n):
            dp[i][i] = 1
        for i in range(2, n + 1):
            for j in range(0, n - i + 1):
                if (dp[j + 1][j + i - 2] == 1 or i == 2) and s[j] == s[j + i - 1]:
                    dp[j][j + i - 1] = 1
                    if i > maxlen:
                        maxlen = i
                        resj = j
        return s[resj : resj + maxlen]

class Solution:
    def expand(self, s, l, r):
        while l >= 0 and r < len(s) and s[l] == s[r]:
            l -= 1
            r += 1
        return l + 1, r - 1

    def longestPalindrome(self, s: str) -> str:
        n = len(s)
        maxlen, start = 1, 0
        for i in range(0, n):
            l1, r1 = self.expand(s, i, i)
            if i < n - 1 and s[i] == s[i + 1]:
                l2, r2 = self.expand(s, i, i + 1)
            else:
                l2, r2 = 0, 0
            if r1 - l1 + 1 > maxlen:
                maxlen = r1 - l1 + 1
                start = l1
            if r2 - l2 + 1 > maxlen:
                maxlen = r2 - l2 + 1
                start = l2
        return s[start : start + maxlen]

94、1143最长公共子序列(多维动态规划,中等)

1143. 最长公共子序列 - 力扣(LeetCode)

思路:

状态必须同时描述两个字符串的进度:

dp[i][j] 表示 text1前 i 个字符text2前 j 个字符之间的最长公共子序列长度。

考虑最后两个字符的关系

情况 A:text1[i-1] == text2[j-1]

这两个字符相等,它们一定可以 出现在公共子序列里,而且是作为最后一位。

那剩下的 LCS 就是 text1 的前 i-1 个和 text2 的前 j-1 个的 LCS 长度,再加上 1。

情况 B:text1[i-1] != text2[j-1]

这两个字符不相等,它们不可能同时 出现在公共子序列的最后。

只能舍弃其中一个:

  • 舍弃 text1 的最后字符:看 dp[i-1][j]

  • 舍弃 text2 的最后字符:看 dp[i][j-1]

    取两者中的较大值。

复制代码
class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        m = len(text1)
        n = len(text2)
        dp = [[0] * (n + 1) for _ in range(m + 1)]
        for i in range(1, m + 1):
            for j in range(1, n + 1):
                if text1[i - 1] == text2[j - 1]:
                    dp[i][j] = dp[i - 1][j - 1] + 1
                else:
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
        return dp[m][n]

95、72编辑距离(多维动态规划,中等)

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

思路:dp[i][j] = 将 word1[0..i-1] 转换成 word2[0..j-1] 所需的最少操作数;

情况 A:word1[i-1] == word2[j-1]

这两个字符已经相同,不需要任何操作→ dp[i][j] = dp[i-1][j-1]

情况 B:word1[i-1] != word2[j-1]

必须做一次操作,有三种选择:

  1. 替换 :把 word1[i-1] 替换成 word2[j-1],然后问题退化成 dp[i-1][j-1]。操作数 +1。

  2. 删除 :删除 word1[i-1],问题退化成 dp[i-1][j]。操作数 +1。

  3. 插入 :在 word1 的末尾插入一个 word2[j-1],这样新字符和 word2[j-1] 匹配,问题退化成 dp[i][j-1]。操作数 +1。

取这三种操作的最小值:

复制代码
class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        m = len(word1)
        n = len(word2)
        dp = [[0] * (n + 1) for _ in range(m + 1)]
        for i in range(1, n + 1):
            dp[0][i] = i
        for i in range(1, m + 1):
            dp[i][0] = i
        for i in range(1, m + 1):
            for j in range(1, n + 1):
                if word1[i - 1] == word2[j - 1]:
                    dp[i][j] = dp[i - 1][j - 1]
                else:
                    dp[i][j] = min(
                        dp[i - 1][j - 1] + 1, dp[i][j - 1] + 1, dp[i - 1][j] + 1
                    )
        return dp[m][n]

96、136只出现一次的数字(位运算,简单)

136. 只出现一次的数字 - 力扣(LeetCode)

思路:

位运算 就是直接对整数在内存中的二进制位 进行操作。 +-* 是把一个数当作整体来算,而位运算是把数拆成一个个 0/1 位来独立运算。

常见的位运算有:

  • &:两位都为 1 才得 1

  • |:只要有一位为 1 就得 1

  • 异或 ^:两位不同就得 1,相同得 0

复制代码
def singleNumber(self, nums: List[int]) -> int:
        res = 0
        for num in nums:
            res ^= num
        return res

97、169多数元素(数组,简单)

169. 多数元素 - 力扣(LeetCode)

思路:Boyer-Moore 投票算法(O(n) 时间,O(1) 空间)

不同则抵消。多数元素出现次数 > ⌊n/2⌋,如果把它和所有其他元素两两配对"消灭",最后活下来的一定是它。

复制代码
class Solution:
    def majorityElement(self, nums: List[int]) -> int:
        candidate = None
        count = 0
        for num in nums:
            if count == 0:
                candidate = num
                count = 1
            else:
                if num == candidate:
                    count += 1
                else:
                    count -= 1
        return candidate

98、75颜色分类(排序,中等)

75. 颜色分类 - 力扣(LeetCode)

思路:原地排序,也叫荷兰国旗问题。

把数组分成 三个区域,从左到右:0 的区间 1 的区间 待探查区间 2 的区间

维护三个指针:

  • low:下一个 0 应该放的位置(也是 1 区间的左边界)

  • mid:当前正在检查的元素位置(待探查区间的左端)

  • high:下一个 2 应该放的位置(2 区间的左边界)

核心逻辑

mid 从左向右移动,遇到什么值就做什么操作:

  • nums[mid] == 0

    把它和 low 位置交换,这样 0 就去了前面的区域,然后 low++, mid++(因为换过来的只可能是 0 或 1,不可能有 2)

  • nums[mid] == 1

    已经在正确区域,不用动,mid++

  • nums[mid] == 2

    把它和 high 位置交换,这样 2 就去了后面的区域,然后 high--

    注意mid 不增加 ,因为从 high 换过来的值还没检查过,可能是 0 或 1,需要下次再判断。

复制代码
class Solution:
    def sortColors(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        n=len(nums)
        low,mid,high=0,0,n-1
        while mid<=high:
            if nums[mid]==2:
                nums[high],nums[mid]=nums[mid],nums[high]
                high-=1
            elif nums[mid]==0:
                nums[low],nums[mid]=nums[mid],nums[low]
                low+=1
                mid+=1
            else:
                mid+=1

99、31下一个排列(数组,中等)

31. 下一个排列 - 力扣(LeetCode)

思路:首先解释一下这个题目:把它看成一个多位数字 (每一位就是数组的一个元素)。所有由同样数字构成的排列中,找到一个"刚刚好比当前数字大一点 "的下一个数。如果已经是最大的那个数,就回到最小的那个数。要求:原地修改,只用常数额外空间

想法:

为了让下一个数"刚好大一点",应该:尽可能保留高位不变 (就像加 1 时,优先动个位数,不动千位数);在尽可能靠右 的位置做一个微小的"增大";增大之后,把后面的所有数字重新排成最小排列(升序),这样才能让整体"尽量小"。

从右向左找第一个"下降点" pivot:希望找到可以变大的最靠右的位置。如果一个位置右侧的数字都是降序的,说明右侧部分已经是最大排列了,没法再变大;在 pivot 右侧找"刚好比它大一点"的数,因为从右向左是递增的,所以我们只需从右向左找第一个大于 nums[pivot] 的数 ,它就是那个"最小的大于它的数"。交换之后,pivot 右边的子数组仍然是降序 ,降序是最大的排列。为了使整体增幅最小,需要把后面这部分变成最小排列,也就是升序。

性质 1:为什么 pivot 右边是降序?

因为 pivot从右向左第一个 满足 nums[i] < nums[i+1] 的位置。

这意味着在 pivot 右边的所有位置 k,都满足 nums[k] >= nums[k+1](否则会在更右的位置找到另一个 i)。

性质 2:为什么"刚好大于"就是"从右向左第一个大于"?

右侧降序 → 从右向左看是升序。

在升序序列中,第一个遇到的满足 > pivot值 的元素,必然就是所有 > pivot值 的元素中最小的那个。

性质 3:为什么交换后,右侧仍然保持降序?

交换前:

  • nums[pivot] = anums[successor] = b,满足 b > a

  • successor 右边的所有元素都 ≤ a(否则 successor 就不是第一个大于 a 的数)

交换后:

  • pivot 位置变成 b,新 successor 位置变成 a

检查相邻关系:

  • successor 左边邻居和 a 的关系:原左边元素 ≥ b > a,所以降序保持。

  • a 和右边邻居的关系:右边邻居 ≤ a,所以降序保持。

  • 其余相邻关系不变。

因此整个右侧仍是降序,可以直接反转。

复制代码
class Solution:
    def nextPermutation(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        n = len(nums)
        pivot = -1
        for i in range(n - 1, 0, -1):
            if nums[i] > nums[i - 1]:
                pivot = i - 1
                break
        if pivot == -1:
            nums.reverse()
            return
        aa = 0
        for j in range(n - 1, pivot, -1):
            if nums[j] > nums[pivot]:
                aa = j
                break
        nums[pivot], nums[aa] = nums[aa], nums[pivot]

        l = pivot + 1
        r = n - 1
        while l <= r:
            nums[l], nums[r] = nums[r], nums[l]
            l += 1
            r -= 1

100、287寻找重复数(数组,中等)

287. 寻找重复数 - 力扣(LeetCode)

思路:时间复杂度 O(n),空间复杂度 O(1),不修改原数组。

题目有两个关键条件:数字都在 [1, n] 范围内,数组长度为 n + 1

这恰好建立一种映射:把数组的索引看作链表节点,把索引的值看作 next 指针 。也就是从 inums[i] 连一条有向边。每个节点的入度,就是这个节点在 nums 中的出现次数。重复元素的入度大于 1。

Floyd 快慢指针判圈算法:

让快慢指针同时从起点出发,因为有环,快指针最终会套圈慢指针,两者在环内某点相遇。相遇后,把慢指针(或另一个新指针,这里用 head)重置到起点。然后两个指针每次都只走一步,它们再次相遇的那个节点,就是环的入口,也就是要找的重复数字。

复制代码
class Solution:
    def findDuplicate(self, nums: List[int]) -> int:
        slow = fast = 0
        while True:
            slow = nums[slow]
            fast = nums[nums[fast]]
            if slow == fast:
                break
        head = 0
        while slow != head:
            slow = nums[slow]
            head = nums[head]
        return slow
相关推荐
csdn_aspnet1 小时前
Java 霍尔分区算法(Hoare‘s Partition Algorithm)
java·开发语言·算法
诸葛务农1 小时前
道路行驶条件下电动汽车永磁电机的有效使用寿命及永磁体的失效和回收再利用(下)
java·开发语言·算法
snow@li1 小时前
AI:理解 大数据、算法、算力、电力、生成式AI、token 之间的关系
大数据·人工智能·算法
小智老师PMP2 小时前
零基础能不能考PMP?零基础专属学习路径+全套扶持体系
学习·算法·职场和发展·软件工程·求职招聘·敏捷流程
Dillon Dong2 小时前
【风电控制】FPGA采集Vdc的ADC增益系数解析——从数字码到实际电压的桥梁
算法·fpga开发·变流器·风电控制
TDengine (老段)2 小时前
TDengine 压缩编码机制 — 双层压缩架构与类型特化算法
大数据·数据库·物联网·算法·时序数据库·tdengine·涛思数据
妄想出头的工业炼药师3 小时前
LVIO鲁棒
算法·开源
aini_lovee3 小时前
MATLAB 图像修复 — 偏微分方程方法
算法
Purple Coder3 小时前
MgB2论文草稿1
职场和发展