leetcode hot100:一、解题思路大全:技巧(只出现一次的数字、多数元素、颜色分类、下一个排列、寻找重复数)、矩阵(矩阵置零、螺旋矩阵、旋转图像、搜索二维矩阵Ⅱ)

因为某大厂的算法没有撕出来,怒而整理该贴。部分题目有AC代码。

技巧

只出现一次的数字

给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。

1 <= nums.length <= 3 * 10^4
-3 * 10^4 <= nums[i] <= 3 * 10^4
除了某个元素只出现一次以外,其余每个元素均出现两次。

思路

我们可以利用 异或运算(XOR) 的特性:

异或的性质:
a ^ a = 0(相同数字异或结果为 0)
a ^ 0 = a(任何数字与 0 异或仍是它本身)
异或满足交换律和结合律,即 a ^ b ^ a = (a ^ a) ^ b = 0 ^ b = b。

因此,对整个数组进行异或运算,最终结果就是只出现一次的数字。

代码

class Solution:
    def singleNumber(self, nums: List[int]) -> int:
        res = 0
        for num in nums:
            res ^= num
        return res

多数元素

给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

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

n == nums.length
1 <= n <= 5 * 10^4
-10^9 <= nums[i] <= 10^9

进阶:尝试设计时间复杂度为 O(n)、空间复杂度为 O(1) 的算法解决此问题。

思路1:排序

假设整个数组元素个数为n,因为多数元素的个数一定大于n//2
所以排序后,下标为n//2的元素一定是多数元素。
因为假设排序后的数组构成如下:

前x个比多数元素小的元素+k个多数元素+后y个比多数元素大的元素
其中x一定小于n//2,y一定小于n//2,不然就和多数元素的定义违背了
所以构成就是
x+k+y=n
其中x<n//2,y<n//2,k>n//2
画线段长度,找到中间的点,那么一定是在k那部分出现的。

代码复杂度为O(nlogn),因为python底层的nums.sort()时间复杂度是这个。
空间复杂度为O(1)(原地排序)O(n)(非原地排序)

代码1:排序

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

0ms,击败100.00%

思路2:候选人算法

维护一个候选人数字和候选人数字对应的选票,然后遍历数组。
如果遍历的当前数字和候选人数字不同的话,候选人数字对应的选票-1.
如果遍历的当前数字和候选人数字相同的话,候选人数字对应的选票+1.
如果选票为0,候选人数字被替代成当前数字。

这个算法正确是因为多数元素的选票最后一定>=0,所以最后候选人数字一定是多数数字。时间复杂度为O(n),空间复杂度为O(2)

代码2:候选人算法

class Solution:
    def majorityElement(self, nums: List[int]) -> int:
        candidate = nums[0]
        vote = 1
        for i in range(1, len(nums)):
            if candidate != nums[i]:
                vote -= 1
            else:
                vote += 1
            if vote == 0:
                candidate = nums[i]
                vote = 1
        return candidate

6ms,击败54.58%。
明明时间复杂度更低,但是实际运行时间更长hhh

颜色分类

思路:荷兰国旗解法/三指针法/三分类问题

荷兰国旗问题就是该题目的问题。
三指针法适用于该类的所有变种,就是需要划分为三个部分
<x和=x和>x的三个部分的问题。

我们维护三个指针:

  • left:0的右边界(指向最终数组最后一个0的下标+1)
  • right:2的左边界(指向最终数组第一个2的下标-1)
  • cur:当前遍历的数字。

初始时left为0,cur为0,right为len(nums)-1,然后随着cur的向右遍历,
left逐步向右扩大,right逐步向左扩大,直到我们cur超过right指针,表示所有的0和2都已经排序好,那么相应地,1也会排序好。

注意:当nums[right]==2时,我们不应该有cur+=1。
即right和cur位置进行交换,因为right位置的数字可能是0,1,2。
所以cur不能向右移动,因为需要二次检查。
而如果nums[cur]==0,那么就是left和cur位置进行交换。
又因为left永远指向第一个非0位置,并且left永远在cur的左边。
所以left位置都是排列好的数字,所以left位置只会是1.所以不需要二次检查。
可以通过[1,2,0]例子来查看。

代码

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        left, cur = 0, 0
        right = len(nums)-1
        while cur <= right:
            if nums[cur] == 0 :
                nums[left], nums[cur] = nums[cur], nums[left]
                left += 1
                cur += 1
            elif nums[cur] == 2:
                nums[right], nums[cur] = nums[cur], nums[right]
                right -= 1
                """
                注意这里不能有cur += 1。right和cur位置进行交换,因为right位置的数字可能是0,1,2
                所以cur不能向右移动,因为需要二次检查。而如果nums[cur]==0,那么就是left和cur位置进行交换
                又因为left永远指向第一个非0位置+left永远在cur的左边,所以left位置都是排列好的数字
                所以left位置只会是1.所以不需要二次检查。可以通过[1,2,0]例子来查看。           
                """
            else:
                cur += 1
        return 

0ms,击败100.00%

下一个排列

整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。

例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3]、[1,3,2]、[3,1,2]、[2,3,1] 。

整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。

例如,arr = [1,2,3] 的下一个排列是 [1,3,2] 。
类似地,arr = [2,3,1] 的下一个排列是 [3,1,2] 。
而 arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。
给你一个整数数组 nums ,找出 nums 的下一个排列。

必须 原地 修改,只允许使用额外常数空间。

提示:
1 <= nums.length <= 100
0 <= nums[i] <= 100

思路

错误思路

最开始我想的思路是:

从左到右遍历数组,对每个数找到其右边第一个大于它的数。
        - 找得到,冒泡到当前数的前面,并进行输出
        - 找不到,继续向右遍历下一个数
        如果直到遍历完所有数,都找不到,则直接输出升序排列的数组。

为此我们可以预处理整个数组,得到rightMax,如果rightMax都是-1,
则意味着直到遍历完所有的数,都会找不到右边更大的数,那么直接输出升序排列的数组。

但实际上这样的思路是错的,归根结底错在从左到右遍历这上面。
因为越左的数权重越大,越右的数权重越小,我们要找的下一个更大的字典序排列应该是尽可能修改越右的数的。

其次,错误的点在于,不应该找右边第一个大于它的数。而应该找右边第一个大于它且最接近它的数。

譬如对于例子1,3,2,按照我的思路一开始修改的是
3,1,2,但实际上对于这个例子的正确答案应该是2,1,3


正确思路

正确的思路应该是,我们将整个数组根据上升/下降趋势划分为不同的区间。
如果排除掉题目的特殊规定,即如果找不到下一个区间,那么将字典序最小的区间(完全上升)认为是其下一个区间,那么我们发现:
如果一个区间是上升的(从左往右看),那么它存在字典序更大的下一个区间。
如果一个区间是下降的(从左往右看),那么它不会存在字典序更大的下一个区间。

所以我们如果要找到题目的下一个区间,首先我们得找到从左往右看的最后一个上升区间,修改它,修改的权重小,如果是从左往右看的第一个上升区间的话,修改的权重大。
所以反过来,就是我们要找到从右往左看的第一个下降区间,我们要修改它。
那么具体修改这个区间的哪个数呢?同样的,根据我们应该修改权重最小的那个数,那么就是这个区间的右边界这个数(从左往右看)。
所以准确来说,我们要修改的是,从右往左看的第一个下降点。
具体应该怎么修改呢?刚刚也有提到,从左往右看的话,对于要修改的数,不应该找右边第一个大于它的数,而应该找右边第一个大于它且最接近它的数。
那么在这里,从右往左看的话,我们需要找到下降点右边第一个大于它且最接近它的数,又因为这个数肯定处在上升区间(从右往左看),所以从右往左遍历到的第一个大于下降点的数就是了。
我们交换这两个数,之后从左往右看,反转下降点右边的下降区间,将其都变为上升区间,从而保证了字典序最小。


看不懂的话看一下这一版deepseek的解释:

  1. 排列的“递增”和“递减”趋势:

    • 如果我们从后向前看,排列的末尾部分通常是降序的(比如 […, 3, 2, 1]),这时候已经是这部分的最大排列,无法再增大。
    • 如果末尾部分是升序的(比如 […, 1, 2, 3]),我们可以通过交换来生成更大的排列。
  2. 关键点:第一个下降的位置:

    • 从后向前遍历,找到第一个满足 nums[i] < nums[i+1] 的位置 i。这意味着 nums[i] 是可以增大的,而 nums[i+1…n-1] 是降序的(即这部分已经最大,无法再增大)。
    • 例如,[1, 3, 2] 中 i=0(nums[0]=1 < nums[1]=3),而 nums[1…2]=[3,2] 是降序的。
  3. 交换和反转:
    为了生成最小的更大排列,我们需要:

    • 在 nums[i+1…n-1] 中找到最小的比 nums[i] 大的数 nums[j](因为这部分是降序的,所以从后向前找第一个比 nums[i] 大的数即可)。
    • 交换 nums[i] 和 nums[j],这样 nums[i] 变大了,但 nums[i+1…n-1] 仍然是降序的。
    • 反转 nums[i+1…n-1],使其变成升序(即最小的排列),从而保证新排列是严格的下一个排列。

寻找重复数

给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。

假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。

你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。

提示:

  • 1 <= n <= 10^5
  • nums.length == n + 1
  • 1 <= nums[i] <= n
  • nums 中 只有一个整数 出现 两次或多次 ,其余整数均只出现 一次

进阶:

  • 如何证明 nums 中至少存在一个重复的数字?
  • 你可以设计一个线性级时间复杂度 O(n) 的解决方案吗?

思路

最关键点在于1 <= nums[i] <= n,所以将数组视为一个链表,其中 nums[i] 表示节点 i 指向的下一个节点是 nums[i]。由于存在重复数字,链表一定存在环,且环的入口就是重复的数字。
确定这个链表不会存在独立节点的关键点就是因为数字范围是 [1, n],而数组长度是 n + 1,因此可以将 nums[i] 看作指针。
那么就转换为了快慢指针问题。

  1. 第一阶段:检测环:
    用快慢指针,慢指针每次走一步(slow = nums[slow]),快指针每次走两步(fast = nums[nums[fast]])。直到快慢指针相遇。
  2. 第二阶段:找到环的入口(重复数字):
    将快指针重置到起点(0),然后快慢指针每次都走一步。
    再次相遇的点就是环的入口(重复数字)。

矩阵

矩阵置零

给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。

提示:

  • m == matrix.length
  • n == matrix[0].length
  • 1 <= m, n <= 200
  • -2^31 <= matrix[i][j] <= 2^31 - 1

进阶:

一个直观的解决方案是使用 O(mn) 的额外空间,但这并不是一个好的解决方案。
一个简单的改进方案是使用 O(m + n) 的额外空间,但这仍然不是最好的解决方案。
你能想出一个仅使用常量空间的解决方案吗?

思路

一开始想的是多起点bfs,但是这样的话空间复杂度最坏是O(m*n),而且bfs也会让同一行或者同一列被多次访问,性能不算很高。

所以想的是,用第一行来记录哪些列需要被置零,第一列来记录哪些行需要被置零,并且用两个变量来记录本来第一行是否就存在0,第一列是否就存在0,这样的话空间复杂度为O(1)

代码

class Solution:
    def setZeroes(self, matrix: List[List[int]]) -> None:
        """
        Do not return anything, modify matrix in-place instead.
        """
        m, n = len(matrix), len(matrix[0])
        firstRowHasZero = any(matrix[0][j] == 0 for j in range(n))
        firstColHasZero = any(matrix[i][0] == 0 for i in range(m))

        # 标记需要置零的行和列
        for i in range(1, m):
            for j in range(1, n):
                if matrix[i][j] == 0:
                    matrix[i][0] = 0
                    matrix[0][j] = 0

        # 根据标记置零
        for i in range(1, m):
            for j in range(1, n):
                if matrix[i][0] == 0 or matrix[0][j] == 0:
                    matrix[i][j] = 0

        # 处理第一行和第一列
        if firstRowHasZero:
            for j in range(n):
                matrix[0][j] = 0
        if firstColHasZero:
            for i in range(m):
                matrix[i][0] = 0

        return matrix  # 返回修改后的矩阵    

螺旋矩阵

又是一道做过的笔试题

给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。

提示:

  • m == matrix.length
  • n == matrix[i].length
  • 1 <= m, n <= 10
  • -100 <= matrix[i][j] <= 100

思路

没什么好说的,这个就是一个模拟。

代码

DIRS = (0, 1), (1, 0), (0, -1), (-1, 0)  # 右下左上

class Solution:
    def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
        m, n = len(matrix), len(matrix[0])
        ans = []
        i = j = di = 0
        for _ in range(m * n):  # 一共走 mn 步
            ans.append(matrix[i][j])
            matrix[i][j] = None  # 标记,表示已经访问过(已经加入答案)
            x, y = i + DIRS[di][0], j + DIRS[di][1]  # 下一步的位置
            # 如果 (x, y) 出界或者已经访问过
            if x < 0 or x >= m or y < 0 or y >= n or matrix[x][y] is None:
                di = (di + 1) % 4  # 右转 90°
            i += DIRS[di][0]
            j += DIRS[di][1]  # 走一步
        return ans

旋转图像

给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。

提示:

  • n = matrix.length = matrix[i].length
  • 1 <= n <= 20
  • -1000 <= matrix[i][j] <= 1000

思路

又是一道做过的笔试题

矩阵顺时针旋转 90º 后,可找到以下规律:

「第 i 行」元素旋转到「第 n−1−i 列」元素;
「第 j 列」元素旋转到「第 j 行」元素;

根据以上「元素旋转公式」,考虑遍历矩阵,将各元素依次写入到旋转后的索引位置。但仍存在问题:在写入一个元素 matrix[i][j]→matrix[j][n−1−i] 后,原矩阵元素 matrix[j][n−1−i] 就会被覆盖(即丢失),而此丢失的元素就无法被写入到旋转后的索引位置了。

为解决此问题,考虑借助一个「辅助矩阵」暂存原矩阵,通过遍历辅助矩阵所有元素,将各元素填入「原矩阵」旋转后的新索引位置即可。

代码

class Solution:
    def rotate(self, matrix: List[List[int]]) -> None:
        n = len(matrix)
        # 深拷贝 matrix -> tmp
        tmp = copy.deepcopy(matrix)
        # 根据元素旋转公式,遍历修改原矩阵 matrix 的各元素
        for i in range(n):
            for j in range(n):
                matrix[j][n - 1 - i] = tmp[i][j]

搜索二维矩阵Ⅱ

又是一道做过的笔试题

编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:

每行的元素从左到右升序排列。
每列的元素从上到下升序排列。

提示:

  • m == matrix.length
  • n == matrix[i].length
  • 1 <= n, m <= 300
  • -10^9 <= matrix[i][j] <= 10^9
  • -10^9 <= target <= 10^9

思路

因为m和n不大,才百级,所以要么暴力,要么遍历行,然后每行二分

代码

class Solution:
    def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
        m = len(matrix)
        n = len(matrix[0])
        # 遍历每行,对行使用二分
        for i in range(m):
            # 要确保该行的第一个数字小于目标值,最后一个数字大于目标值,则答案才可能在该行
            # 所以如果matrix[i][0]>target或者matrix[i][n-1]<target,都直接跳过
            if matrix[i][0] > target or matrix[i][n-1] < target:
                continue
            left, right = 0, n-1
            while left <= right:
                mid = (left+right)//2
                if target < matrix[i][mid]:
                    right -= 1
                elif target > matrix[i][mid]:
                    left += 1
                else:
                    return True
        return False
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

shanshandeisu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值