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。

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

代码

python 复制代码
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:排序

python 复制代码
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:候选人算法

python 复制代码
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]例子来查看。

代码

python 复制代码
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)

代码

python 复制代码
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

思路

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

代码

python 复制代码
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] 就会被覆盖(即丢失),而此丢失的元素就无法被写入到旋转后的索引位置了。

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

代码

python 复制代码
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不大,才百级,所以要么暴力,要么遍历行,然后每行二分

代码

python 复制代码
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

数组中的第k个最大元素

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

思路

感觉和堆也没多大关系,当然,可以用堆。但是我选择快排(

前k个高频元素

面试有考到这个场景题。

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

提示:

  • 1 <= nums.length <= 10^5
  • k 的取值范围是 [1, 数组中不相同的元素的个数]
  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的

进阶:你所设计算法的时间复杂度 必须 优于 O(n log n) ,其中 n 是数组大小。

思路

使用 Counter 计算每个数字的频率,然后创建大小为k的小顶堆。如果堆未满,直接添加元素。如果当前元素的频率大于堆顶元素的频率,则弹出堆顶元素,并将当前元素加入堆中。最后返回堆中的元素,因为可能有相同频次的元素。

数据流的中位数

中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。

例如 arr = [2,3,4] 的中位数是 3 。

例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5 。

实现 MedianFinder 类:

MedianFinder() 初始化 MedianFinder 对象。

void addNum(int num) 将数据流中的整数 num 添加到数据结构中。

double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10-5 以内的答案将被接受。

提示:

  • -10^5 <= num <= 10^5
  • 在调用 findMedian 之前,数据结构中至少有一个元素
  • 最多 5 * 10^4 次调用 addNum 和 findMedian

思路

为什么不能直接快排?因为快排不能保证插入有序。而我们维护小顶堆可以保证插入有序。

建立一个 小顶堆 A 和 大顶堆 B ,各保存列表的一半元素,且规定:

A 保存 较大 的一半,长度为M/2 ( N 为偶数)或 (M+1)/2 ( M 为奇数)。

B 保存 较小 的一半,长度为N/2 ( N 为偶数)或 (N+1)/2 ( N 为奇数)。

随后,中位数可仅根据 A,B 的堆顶元素计算得到

中位数为 A的堆顶元素(M≠N)或者 (A的堆顶元素+B的堆顶元素)/ 2(M==N)

时间复杂度 O(logN) :

  • 查找中位数 O(1) : 获取堆顶元素使用 O(1) 时间。
  • 添加数字 O(logN) : 堆的插入和弹出操作使用 O(logN) 时间。

空间复杂度 O(N) : 其中 N 为数据流中的元素数量,小顶堆 A 和大顶堆 B 最多同时保存 N 个元素。

哈希

两数之和

字母异位词分组

最长连续序列

相关推荐
未来可期叶1 小时前
如何用Python批量解压ZIP文件?快速解决方案
python
张槊哲1 小时前
ROS2架构介绍
python·架构
智驱力人工智能1 小时前
AI移动监测:仓储环境安全的“全天候守护者”
人工智能·算法·安全·边缘计算·行为识别·移动监测·动物检测
风逸hhh2 小时前
python打卡day29@浙大疏锦行
开发语言·前端·python
浩皓素2 小时前
深入理解For循环及相关关键字原理:以Python和C语言为例
c语言·python
英英_2 小时前
详细介绍一下Python连接MySQL数据库的完整步骤
数据库·python·mysql
水花花花花花2 小时前
GloVe 模型讲解与实战
python·深度学习·conda·pip
C_VuI2 小时前
如何安装cuda版本的pytorch
人工智能·pytorch·python
Star abuse2 小时前
机器学习基础课程-6-课程实验
人工智能·python·机器学习
代码小将2 小时前
力扣992做题笔记
算法·leetcode