Contents

LeetCode 100题-1

参考资料

热身

整数除法

求两个数的商,不能用乘、除、取余符号 要求截去小数部分,即只取整数部分。数值范围是 [−2^31, 2^31−1] 思路: 先考虑易出错的边界情况,被除数为-2^31 或者除数为-2^31 时候 结合 快速乘 算法和 二分查找

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 递归快速幂算法
# https://zhuanlan.zhihu.com/p/95902286
# 7的10次方 = 10个7相乘 = (5个7相乘)的平方 = 7的平方的平方再乘以7
# 发现乘法的次数越来越少


# 非递归快速幂算法
# 结合二进制的思想 7^10=7^(1010)=7^(1000)*7(0010)
def qpow(a, n):
    ans = 1
    while n:
        if n&1:        //如果n的当前末位为1
            ans *= a  //ans乘上当前的a
        a *= a        //因为右移所以a先自乘
        n >>= 1       //n往右移一位
    return ans
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 二分查找
left, right = 1, INT_MAX
while left <= right:
    mid = left + ((right - left) >> 1)    #>>1相当于除以2
    if 。。。:
        ans = mid
        if mid == INT_MAX:
            break
        left = mid + 1
    else:
        right = mid - 1

二进制加法

1
2
3
a = 6
'{0:.2f}'.format(a)    #输出 6.00
'{0:b}'.format(a)    #输出 '110'
1
2
int(二进制表示, 2)    #将二进制转为十进制
bin(十进制)[2:]    #将十进制转为二进制
1
2
3
4
5
6
7
x = 10    #'1010'
y = 11    #'1011'
# result = 21    #'10101'
while y:
    a = x^y
    b = (x&y) << 1
    x, y = a, b

计算二进制数中的 1 的个数

1
2
3
4
5
6
def countOnes(x: int) -> int:
    ones = 0
    while x > 0:
        x &= (x - 1)    #相邻两个数的二进制表示做逻辑与运算可以去掉最后面的1
        ones += 1
    return ones

数组(已升序排序)中哪两个的和等于目标值

1、可以首先固定第一个数,然后寻找第二个数(排好序的数组寻找值一般想到的就是二分排序),第二个数等于目标值减去第一个数的差 2、由于是排好序的数组,所以直接设置双指针即可。两个指针分别指向第一个元素位置和最后一个元素的位置

寻找数组中心下标

中心下标意味着:其左侧所有元素相加的和等于右侧所有元素相加的和 前缀和算法

验证 s 是否是回文串(忽略大小写)

一种思路是先转为小写,再直接切片处理 二是使用双指针,可以考虑先全部转小写再双指针;还是双指针一边比较一边转换

最多删除一个字符得到回文

1、枚举法,遍历删除每一个字符再看看能否回文 2、双指针,左右指针不断向中间靠近,若出现不是回文的情况则只需要去判断 s[low+1:high] 或者 s[low:high-1] 的情况是否成立

两个链表的第一个重合节点

1、创建哈希表来存其中一个链表的节点,然后遍历另一个链表看看是否有相同的节点 2、双指针的思想,比如两个链表 A、B,A 遍历完再去遍历 B 的,同理 B 遍历完再去遍历 A 的,直到两个指针指向同一个节点

反转链表

1、需要新建一个新的节点 prev,然后遍历链表的节点每次将其接在 prev 的前面 2、递归求解,从后往前使得节点 curr->next->next = curr; curr->next = nullptr;

判断回文链表

1、将链表的值复制到列表中,再去判断是否是回文 2、递归检查

1
2
3
4
5
6
7
8
def recursively_check(current_node=head):
    if current_node is not None:
        if not recursively_check(current_node.next):    #层层深入到尾节点
            return False
        if self.front_pointer.val != current_node.val:    #比较前后两个节点的值
            return False
        self.front_pointer = self.front_pointer.next    #前节点向前移动
    return True    #return到第二个if那里

3、通过快慢指针找到中间的节点,然后对后半截的链表进行反转,再对两截链表进行回文判断

判断变位词

两个字符串中的每个字符出现的次数都相同且字符顺序不完全相同 所以若字符串长度不等或者两个字符串完全相同,则可以直接返回 False 1、排序后比较 2、哈希表,可以维护一个长度为 26 的频次数组,先遍历其中的一个字符串记录各个字符出现的频次,然后遍历另一个字符串,减去哈希表中的频次,若出现了某一个字符的频次没有减到 0,则说明字符出现的次数不同,返回 False

判断列表中的字符串是否按照 某个给定的顺序 进行排序

将给定的顺序转换成索引,再将字符串转换成索引,之后就是比较大小

Python 的 pairwise 函数

1
2
3
s = 'abcde'
itertools.pairwise(s)    #获取重叠对
>> ab, bc, cd, de

滑动窗口的平均值

利用队列,控制队列的长度,左出右进

按中序遍历展平二叉搜索树

中序遍历

1
2
3
4
5
6
7
8
9
inorder(node){
    inorder(node.left)
    """
    中间这里可以进行节点的存储 restorelist.append(node.val)
    或者
    控制链表节点的指向
    """
    inorder(node.right)
}

判断该二叉搜索树中是否存在两个节点它们的值之和等于 k

假设二叉搜索树中节点的值均唯一 1、dfs + 哈希表 遍历节点存储值,检查哈希表中的值之和 重点是怎么在递归里面穿插检查值的和

往集合 set 里面添加元素用 add

2、bfs + 哈希表 广度优先搜索就想到->队列 根节点先放入队列,每次循环最左侧元素出队列,将值插入哈希表中,再将当前节点的左右节点入队列

3、中序遍历 + 双指针 需要中序遍历存储节点的值,然后排序再双指针查找

查找有序列表中值的插入位置

二分查找 注意边界问题,结果的初始值可以设置为 数组长度

求平方根

求一个非负整数 x 的正数平方根,只保留整数的部分,小数部分将被舍去 1、用自然对数 e 进行换底

$$ \sqrt{x} = x^{\frac{1}{2}} = (e^{\text{ln}x})^{\frac{1}{2}} = e^{\frac{1}{2}\text{ln}x} $$

2、二分查找 下界为 0,上界可以粗略地设定为 x

1
2
3
4
5
if mid * mid <= x:
    ans = mid
    l = mid + 1
else:
    r = mid - 1

3、牛顿迭代 牛顿迭代法可以快速求解函数零点 本质是借助 泰勒级数,从初始值开始快速向零点逼近 开始时任取一点 x0 为作为初始点,计算 f(x0),绘制函数在点(x0, f(x0))位置的切线,这条切线与横轴交于新的一点称为 x1,之后的步骤与前面类似 令 $y = f(x) = x^{2}-C$ 函数 f(x) 的零点就是 C 的平方根,初始点 x0 可以取 C,计算出 xi 迭代式,设置好循环终止的精度

数组相对排序

给定两个数组,其中一个数组 a1 按照另一个数组 a2 的相对顺序进行排序,a2 中的元素唯一且包含在 a1 中,a1 中的元素若未在 a2 中则在末尾按照大小升序排序 1、可以使用哈希表对 a2 中的元素的顺序进行映射,哈希表的键为 a2[i],值为 i 2、维护一个长度为 max(a1)的数组,每一位放置数字出现的次数,再遍历 a2 的同时创建一个新的数组存放结果

爬楼梯的最少成本

给定长度为 n 的数组,里面的元素表示爬一个阶梯所需要的消耗,一次可以上 1 或 2 个阶梯,求爬上楼梯顶部的最小消耗 动态规划 创建一个长度为 n+1 的数组 dp,dp[i]表示到第 i 个阶梯所需要的最少消耗 dp[i] = min(dp[i−1]+cost[i−1], dp[i−2]+cost[i−2]) 可以使用滚动数组的思想,无需闯将数组 dp,只需要两个指针即可

只出现一次的数字

假设数组中只有一个数字仅出现一次,其余的出现 3 次,找出出现一次的那个 1、哈希表 使用哈希映射统计数组中每个元素的出现次数 键表示一个元素,值表示其出现的次数 2、二进制 将所有数的二进制表示相加,每一位对 3 取余,结果必为 0 或者 1,即得到答案。注意符号位的处理 3、数字电路???

哈希

两数之和

给定一个整数数组 nums 和一个整数目标值 target,在该数组中找出 和为目标值 target 的那两个整数,并返回它们的数组下标

输入:nums = [2,7,11,15], target = 9 输出:[0,1] 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

进阶:你可以想出一个时间复杂度小于 O(n2) 的算法吗?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from typing import List


class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        # 使用哈希表存储已遍历过的数字及其下标
        # key: 数字值, value: 该数字的下标
        hashmap = {}

        # 遍历数组
        for i, num in enumerate(nums):
            # 计算需要的另一个数
            complement = target - num

            # 检查 complement 是否已经在哈希表中
            if complement in hashmap:
                # 找到了!返回另一个数的下标和当前下标
                return [hashmap[complement], i]

            # 没找到,将当前数字和下标存入哈希表
            hashmap[num] = i

        # 无解(题目保证有唯一解,所以不会走到这里)
        return []


# 测试用例
assert Solution().twoSum([2, 7, 11, 15], 9) == [0, 1]
assert Solution().twoSum([3, 2, 4], 6) == [1, 2]
assert Solution().twoSum([3, 3], 6) == [0, 1]
# 手动推导 [2, 7, 11, 15], target=9:
#   i=0, num=2, complement=7, hashmap={}, 7不在,存入 {2:0}
#   i=1, num=7, complement=2, hashmap={2:0}, 2在!返回 [0, 1]

字母异位词分组

给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。

输入: strs = [“eat”, “tea”, “tan”, “ate”, “nat”, “bat”] 输出: [[“bat”],[“nat”,“tan”],[“ate”,“eat”,“tea”]] 解释: 在 strs 中没有字符串可以通过重新排列来形成 “bat”。 字符串 “nat” 和 “tan” 是字母异位词,因为它们可以重新排列以形成彼此。 字符串 “ate” ,“eat” 和 “tea” 是字母异位词,因为它们可以重新排列以形成彼此。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from typing import List
from collections import defaultdict


class Solution:
    def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
        # 使用字典存储分组结果
        # key: 排序后的字符串(作为字母异位词的"签名")
        # value: 具有相同签名的字符串列表
        groups = defaultdict(list)

        # 遍历每个字符串
        for s in strs:
            # 1. 将字符串转换为列表并排序,得到标准形式
            # 例如: "eat" -> ['e', 'a', 't'] -> "aet"
            #      "tea" -> ['t', 'e', 'a'] -> "aet"
            # 两者排序后都是 "aet",说明它们是字母异位词
            key = ''.join(sorted(s))

            # 2. 将当前字符串加入对应的分组
            groups[key].append(s)

        # 3. 返回所有分组(字典的值列表)
        return list(groups.values())


# 测试用例
assert Solution().groupAnagrams(["eat", "tea", "tan", "ate", "nat", "bat"]) == [["bat"], ["nat", "tan"], ["ate", "eat", "tea"]]
assert Solution().groupAnagrams([""]) == [[""]]
assert Solution().groupAnagrams(["a"]) == [["a"]]

# 手动推导 ["eat", "tea", "tan", "ate", "nat", "bat"]:
#   "eat"  -> 排序为 "aet" -> groups["aet"] = ["eat"]
#   "tea"  -> 排序为 "aet" -> groups["aet"] = ["eat", "tea"]
#   "tan"  -> 排序为 "ant" -> groups["ant"] = ["tan"]
#   "ate"  -> 排序为 "aet" -> groups["aet"] = ["eat", "tea", "ate"]
#   "nat"  -> 排序为 "ant" -> groups["ant"] = ["tan", "nat"]
#   "bat"  -> 排序为 "abt" -> groups["abt"] = ["bat"]
# 结果: [["eat", "tea", "ate"], ["tan", "nat"], ["bat"]]

数字连续的最长序列

给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度
以 nums = [100, 4, 200, 1, 3, 2] 为例:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from typing import List


class Solution:
    def longestConsecutive(self, nums: List[int]) -> int:
        # 如果数组为空,直接返回0
        if not nums:
            return 0

        # 1. 将所有数字存入哈希集合,实现 O(1) 的查找
        num_set = set(nums)
        longest_streak = 0

        # 2. 遍历数组中的每一个数字
        for num in num_set:
            # 3. 只有当 num-1 不在集合中时,num 才是序列的起点
            if num - 1 not in num_set:
                current_num = num
                current_streak = 1

                # 4. 向后查找连续的数字
                while current_num + 1 in num_set:
                    current_num += 1
                    current_streak += 1

                # 5. 更新最大长度
                longest_streak = max(longest_streak, current_streak)

        return longest_streak


assert Solution().longestConsecutive([100, 4, 200, 1, 3, 2]) == 4
assert Solution().longestConsecutive([0, 3, 7, 2, 5, 8, 4, 6, 0, 1]) == 9
assert Solution().longestConsecutive([]) == 0
assert Solution().longestConsecutive([1]) == 1
assert Solution().longestConsecutive([1, 2, 0, 1]) == 3
# 存入集合:num_set = {100, 4, 200, 1, 3, 2}。
# 遍历过程:
#   遇到 100:检查 99 是否在集合中?不在。说明 100 是起点。向后找 101(不在),序列长度为 1。
#   遇到 4:检查 3 是否在集合中?在。说明 4 不是起点,跳过。
#   遇到 200:检查 199 是否在集合中?不在。说明 200 是起点。向后找 201(不在),序列长度为 1。
#   遇到 1:检查 0 是否在集合中?不在。说明 1 是起点。向后找 2(在)、3(在)、4(在)、5(不在)。序列长度为 4。
#   遇到 3:检查 2 是否在集合中?在。跳过。
#   遇到 2:检查 1 是否在集合中?在。跳过。
# 结果:最长长度为 4。

双指针

移动零

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。 请注意 ,必须在不复制数组的情况下原地对数组进行操作。

输入: nums = [0,1,0,3,12] 输出: [1,3,12,0,0]

进阶:你能尽量减少完成的操作次数吗?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from typing import List


class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        """
        使用双指针原地移动零元素

        思路:
        - left 指针指向已处理区域的最后一个非零元素
        - right 指针向后扫描,寻找非零元素
        - 找到后与 left+1 位置交换,left 右移
        - 这样所有非零元素会被移动到数组前面,保持相对顺序
        """
        left = 0  # left 指针:已处理区域的边界(不含零)

        for right in range(len(nums)):
            # 当 right 指向非零元素时,将其移动到 left 位置
            if nums[right] != 0:
                # 交换元素(如果 left 和 right 指向同一位置则无操作)
                nums[left], nums[right] = nums[right], nums[left]
                left += 1  # left 右移,指向下一个待填充位置


# 测试用例
nums1 = [0, 1, 0, 3, 12]
Solution().moveZeroes(nums1)
assert nums1 == [1, 3, 12, 0, 0]

nums2 = [0]
Solution().moveZeroes(nums2)
assert nums2 == [0]

nums3 = [1, 0, 0, 2, 3]
Solution().moveZeroes(nums3)
assert nums3 == [1, 2, 3, 0, 0]

# 手动推导 [0, 1, 0, 3, 12]:
#   left = 0, right = 0: nums[0] = 0,跳过
#   left = 0, right = 1: nums[1] = 1 != 0,交换 nums[0] 和 nums[1] -> [1, 0, 0, 3, 12],left = 1
#   left = 1, right = 2: nums[2] = 0,跳过
#   left = 1, right = 3: nums[3] = 3 != 0,交换 nums[1] 和 nums[3] -> [1, 3, 0, 0, 12],left = 2
#   left = 2, right = 4: nums[4] = 12 != 0,交换 nums[2] 和 nums[4] -> [1, 3, 12, 0, 0],left = 3
# 最终:[1, 3, 12, 0, 0]

盛最多水的容器

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。 返回容器可以储存的最大水量。 说明:你不能倾斜容器。

输入:[1,8,6,2,5,4,8,3,7] 输出:49 解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from typing import List


class Solution:
    def maxArea(self, height: List[int]) -> int:
        """
        使用双指针求最大容器容量

        思路:
        - 容器的容量 = 两端中较短的那一条 * 两端之间的距离
        - 初始时左右指针分别在两端
        - 移动较短的那一端,因为容量受限于较短的边
        - 不断更新最大容量
        """
        left = 0
        right = len(height) - 1
        max_area = 0

        while left < right:
            # 计算当前容器的容量
            # 高度取两端中的较小值,宽度是两端的距离
            current_height = min(height[left], height[right])
            current_width = right - left
            max_area = max(max_area, current_height * current_width)

            # 移动较短的那一端,以寻找更大的容量
            if height[left] < height[right]:
                left += 1
            else:
                right -= 1

        return max_area


# 测试用例
assert Solution().maxArea([1, 8, 6, 2, 5, 4, 8, 3, 7]) == 49
assert Solution().maxArea([1, 1]) == 1
assert Solution().maxArea([4, 3, 2, 1, 4]) == 16

# 手动推导 [1, 8, 6, 2, 5, 4, 8, 3, 7]:
#   left=0, right=8: 高度=min(1,7)=1,容量=1*8=8,移动left(1较小)
#   left=1, right=8: 高度=min(8,7)=7,容量=7*7=49(最大),移动right(7较小)
#   left=1, right=7: 高度=min(8,3)=3,容量=3*6=18,移动right(3较小)
#   left=1, right=6: 高度=min(8,8)=8,容量=8*5=40,移动right(相等,移动任意)
#   left=1, right=5: 高度=min(8,4)=4,容量=4*4=16...
#   最终最大容量为 49

三数之和

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。

输入:nums = [-1,0,1,2,-1,-4] 输出:[[-1,-1,2],[-1,0,1]] 解释: nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。 nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。 nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。 不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。 注意,输出的顺序和三元组的顺序并不重要。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
from typing import List


class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        """
        使用排序 + 双指针找出所有不重复的三数之和为零的三元组

        思路:
        1. 先对数组排序
        2. 固定一个数 nums[i],然后在右侧使用双指针找两数之和等于 -nums[i]
        3. 通过排序和跳过重复值来避免重复三元组
        """
        nums.sort()  # 排序
        result = []

        for i in range(len(nums) - 2):
            # 跳过重复的 nums[i],避免重复三元组
            if i > 0 and nums[i] == nums[i - 1]:
                continue

            # 如果最小的三数之和都大于 0,则不可能有三数之和为 0
            if nums[i] + nums[i + 1] + nums[i + 2] > 0:
                break

            # 如果 nums[i] 加上最大的两个数还小于 0,直接跳过
            if nums[i] + nums[-2] + nums[-1] < 0:
                continue

            # 双指针查找剩余两数之和等于 -nums[i]
            left = i + 1
            right = len(nums) - 1

            while left < right:
                total = nums[i] + nums[left] + nums[right]

                if total == 0:
                    result.append([nums[i], nums[left], nums[right]])

                    # 跳过重复的 nums[left]
                    while left < right and nums[left] == nums[left + 1]:
                        left += 1
                    # 跳过重复的 nums[right]
                    while left < right and nums[right] == nums[right - 1]:
                        right -= 1

                    left += 1
                    right -= 1

                elif total < 0:
                    left += 1  # 总和太小,增大 left
                else:
                    right -= 1  # 总和太大,减小 right

        return result


# 测试用例
assert Solution().threeSum([-1, 0, 1, 2, -1, -4]) == [[-1, -1, 2], [-1, 0, 1]]
assert Solution().threeSum([0, 1, 1]) == []
assert Solution().threeSum([0, 0, 0]) == [[0, 0, 0]]
assert Solution().threeSum([-2, 0, 0, 2, 2]) == [[-2, 0, 2]]

# 手动推导 [-1, 0, 1, 2, -1, -4] 排序后 [-4, -1, -1, 0, 1, 2]:
#   i=0, nums[i]=-4:
#       left=1, right=5: total=-4+(-1)+2=-3 < 0, left++
#       left=2, right=5: total=-4+(-1)+2=-3 < 0, left++
#       left=3, right=5: total=-4+0+2=-2 < 0, left++
#       left=4, right=5: total=-4+1+2=-1 < 0, left++ -> left=5 退出
#   i=1, nums[i]=-1:
#       left=2, right=5: total=-1+(-1)+2=0, 找到 [-1,-1,2]
#       left=3, right=4: total=-1+0+1=0, 找到 [-1,0,1]
#   i=2, nums[i]=-1,与前一个相同,跳过
#   结果:[[-1,-1,2], [-1,0,1]]

接雨水

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

输入:height = [0,1,0,2,1,0,1,3,2,1,2,1] 输出:6 解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
from typing import List


class Solution:
    def trap(self, height: List[int]) -> int:
        """
        使用单调栈(或双指针)计算能接的雨水总量

        方法一:单调栈
        思路:
        - 用栈存储高度的索引
        - 当遇到比栈顶高的高度时,说明可以形成凹槽
        - 计算凹槽能接的雨水量
        """
        stack = []  # 存储索引
        water = 0

        for i in range(len(height)):
            # 当栈不为空且当前高度大于栈顶高度时,处理凹槽
            while stack and height[i] > height[stack[-1]]:
                h = height[stack.pop()]  # 凹槽底部高度

                # 如果栈为空,说明没有左边界,无法接雨水
                if not stack:
                    break

                # 左边界是栈中新的栈顶,右边界是当前索引
                left = stack[-1]
                right = i
                curr_height = min(height[left], height[right]) - h

                # 计算雨水量:高度差 * 宽度
                water += curr_height * (right - left - 1)

            stack.append(i)

        return water

    def trap_two_pointers(self, height: List[int]) -> int:
        """
        方法二:双指针
        思路:
        - 每个位置能接的雨水,由其左边最高和右边最高中的较小值决定
        - 使用双指针从两端向中间遍历
        - 时间 O(n),空间 O(1)
        """
        if not height:
            return 0

        left = 0
        right = len(height) - 1
        left_max = height[left]
        right_max = height[right]
        water = 0

        while left < right:
            # 更新左右两侧的最大高度
            if height[left] > left_max:
                left_max = height[left]
            if height[right] > right_max:
                right_max = height[right]

            # 从较矮的一侧计算雨水
            if height[left] < height[right]:
                # 左边较矮,当前位置能接的雨水 = 左边最大高度 - 当前高度
                water += left_max - height[left]
                left += 1
            else:
                # 右边较矮或相等
                water += right_max - height[right]
                right -= 1

        return water


# 测试用例
assert Solution().trap([0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]) == 6
assert Solution().trap([4, 2, 0, 3, 2, 5]) == 9
assert Solution().trap([]) == 0
assert Solution().trap([1, 0, 1]) == 1
assert Solution().trap([3, 0, 2, 0, 4]) == 7

# 手动推导 [0,1,0,2,1,0,1,3,2,1,2,1](方法一单调栈):
#   i=0: stack=[0]
#   i=1: height[1]=1 > height[0]=0,弹出0,stack空,无法接水,入栈1
#   i=2: height[2]=0 < height[1]=1,stack=[1,2]
#   i=3: height[3]=2 > height[2]=0,弹出2,left=1,curr_height=min(1,2)-0=1,water+=1*(3-1-1)=1
#       继续:height[3]=2 > height[1]=1,弹出1,stack空,无法接水,入栈3
#   i=4: height[4]=1 < height[3]=2,stack=[3,4]
#   i=5: height[5]=0 < height[4]=1,stack=[3,4,5]
#   i=6: height[6]=1 > height[5]=0,弹出5,left=3,curr_height=min(2,1)-0=1,water+=1*(6-3-1)=2,总计3
#       继续:height[6]=1 == height[4]=1,弹出4,left=3,curr_height=min(2,2)-1=0,water不变
#       继续:height[6]=1 < height[3]=2,入栈6,stack=[3,6]
#   ...以此类推,最终water=6

滑动窗口

无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串 的长度。

输入: s = “abcabcbb” 输出: 3 解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。注意 “bca” 和 “cab” 也是正确答案。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
from typing import List


class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        """
        使用滑动窗口求无重复字符的最长子串长度

        思路:
        - 维护一个窗口 [left, right],窗口内没有重复字符
        - 用哈希集合记录窗口内出现过的字符
        - right 向右扩展窗口,遇到重复字符时 left 向右收缩窗口
        - 每次更新最大长度
        """
        char_set = set()  # 窗口内的字符集合
        left = 0
        max_len = 0

        for right in range(len(s)):
            # 如果当前字符已在窗口中,不断移动 left 收缩窗口
            while s[right] in char_set:
                char_set.remove(s[left])
                left += 1

            # 将当前字符加入窗口
            char_set.add(s[right])

            # 更新最大长度
            max_len = max(max_len, right - left + 1)

        return max_len


# 测试用例
assert Solution().lengthOfLongestSubstring("abcabcbb") == 3
assert Solution().lengthOfLongestSubstring("bbbbb") == 1
assert Solution().lengthOfLongestSubstring("pwwkew") == 3
assert Solution().lengthOfLongestSubstring("") == 0
assert Solution().lengthOfLongestSubstring(" ") == 1

# 手动推导 "abcabcbb":
#   right=0, s[0]='a': set={}, 'a'不在,加入 -> set={'a'}, max_len=1
#   right=1, s[1]='b': 'b'不在,加入 -> set={'a','b'}, max_len=2
#   right=2, s[2]='c': 'c'不在,加入 -> set={'a','b','c'}, max_len=3
#   right=3, s[3]='a': 'a'在,移除s[left]=s[0]='a', left=1 -> set={'b','c'}, 'a'不在了,加入 -> set={'a','b','c'}, max_len=3
#   right=4, s[4]='b': 'b'在,移除s[left]=s[1]='b', left=2 -> set={'a','c'}, 'b'不在了,加入 -> set={'a','b','c'}, max_len=3
#   right=5, s[5]='c': 'c'在,移除s[left]=s[2]='c', left=3 -> set={'a','b'}, 'c'不在了,加入 -> set={'a','b','c'}, max_len=3
#   right=6, s[6]='b': 'b'在,移除s[left]=s[3]='a', left=4 -> set={'b','c'}, 'b'还在,移除s[4]='b', left=5 -> set={'c'}, 'b'不在了,加入 -> set={'b','c'}, max_len=3
#   right=7, s[7]='b': 'b'在,移除s[left]=s[5]='c', left=6 -> set={'b'}, 'b'还在,移除s[6]='b', left=7 -> set={}, 'b'不在了,加入 -> set={'b'}, max_len=3
# 结果:3

找到字符串中所有字母异位词

给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

输入: s = “cbaebabacd”, p = “abc” 输出: [0,6] 解释: 起始索引等于 0 的子串是 “cba”, 它是 “abc” 的异位词。 起始索引等于 6 的子串是 “bac”, 它是 “abc” 的异位词。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
from typing import List
from collections import Counter


class Solution:
    def findAnagrams(self, s: str, p: str) -> List[int]:
        """
        使用滑动窗口找到 s 中所有 p 的异位词的起始索引

        思路:
        - 异位词 = 字符组成相同,顺序不同
        - 维护一个长度为 len(p) 的滑动窗口
        - 比较窗口内字符的频率与 p 的字符频率是否一致
        - 用计数器记录差异,避免每次都比较整个频率表
        """
        if len(s) < len(p):
            return []

        result = []
        p_len = len(p)

        # 统计 p 中每个字符的出现次数
        p_count = [0] * 26
        # 统计窗口中每个字符的出现次数
        window_count = [0] * 26

        for ch in p:
            p_count[ord(ch) - ord('a')] += 1

        # 初始化窗口:先填入前 p_len 个字符
        for i in range(p_len):
            window_count[ord(s[i]) - ord('a')] += 1

        # 检查初始窗口是否匹配
        if window_count == p_count:
            result.append(0)

        # 滑动窗口:每次右移一位
        for i in range(p_len, len(s)):
            # 窗口右端加入新字符
            window_count[ord(s[i]) - ord('a')] += 1
            # 窗口左端移除旧字符
            window_count[ord(s[i - p_len]) - ord('a')] -= 1

            # 检查当前窗口是否匹配
            if window_count == p_count:
                result.append(i - p_len + 1)

        return result


# 测试用例
assert Solution().findAnagrams("cbaebabacd", "abc") == [0, 6]
assert Solution().findAnagrams("abab", "ab") == [0, 1, 2]
assert Solution().findAnagrams("a", "ab") == []
assert Solution().findAnagrams("aa", "bb") == []

# 手动推导 s="cbaebabacd", p="abc":
#   p_count = [1,1,1,0,...](a:1, b:1, c:1)
#   初始窗口 s[0:3]="cba" -> window_count = [1,1,1,0,...],匹配!result=[0]
#   i=3: 加入'e',移除'c' -> 窗口"bae" -> [1,1,0,0,1,...],不匹配
#   i=4: 加入'b',移除'b' -> 窗口"aeb" -> [1,1,0,0,1,...],不匹配
#   i=5: 加入'a',移除'a' -> 窗口"eba" -> [1,1,0,0,1,...],不匹配
#   i=6: 加入'b',移除'e' -> 窗口"bab" -> [1,2,0,...],不匹配
#   i=7: 加入'a',移除'b' -> 窗口"aba" -> [2,1,0,...],不匹配
#   i=8: 加入'c',移除'a' -> 窗口"bac" -> [1,1,1,...],匹配!result=[0,6]
#   i=9: 加入'd',移除'b' -> 窗口"acd" -> [1,0,1,1,...],不匹配
# 结果:[0, 6]

子串

和为 K 的子数组

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。 子数组是数组中元素的连续非空序列。

输入:nums = [1,1,1], k = 2 输出:2

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from typing import List
from collections import defaultdict


class Solution:
    def subarraySum(self, nums: List[int], k: int) -> int:
        """
        使用前缀和 + 哈希表统计和为 k 的子数组个数

        思路:
        - 前缀和:prefix[i] 表示 nums[0..i-1] 的元素之和
        - 子数组 nums[i..j] 的和 = prefix[j+1] - prefix[i]
        - 如果 prefix[j+1] - prefix[i] = k,则 prefix[i] = prefix[j+1] - k
        - 用哈希表记录每个前缀和出现的次数
        - 对于每个位置,计算 prefix - k 是否在哈希表中,即有多少个子数组以当前位置结尾
        """
        # 哈希表:key=前缀和,value=该前缀和出现的次数
        prefix_count = defaultdict(int)

        # 前缀和为 0 初始化为 1(表示空数组的前缀和)
        prefix_count[0] = 1

        prefix = 0  # 当前前缀和
        result = 0  # 满足条件的子数组个数

        for num in nums:
            prefix += num  # 更新前缀和

            # 如果 prefix - k 存在于哈希表中,说明存在子数组和为 k
            # 这些子数组以当前位置结尾
            result += prefix_count[prefix - k]

            # 将当前前缀和加入哈希表
            prefix_count[prefix] += 1

        return result


# 测试用例
assert Solution().subarraySum([1, 1, 1], 2) == 2
assert Solution().subarraySum([1, 2, 3], 3) == 2
assert Solution().subarraySum([1], 0) == 0
assert Solution().subarraySum([1, -1, 1, -1], 0) == 6
assert Solution().subarraySum([0, 0, 0, 0], 0) == 10

# 手动推导 nums=[1,1,1], k=2:
#   prefix=0, prefix_count={0:1}
#   num=1: prefix=1, prefix-2=-1不在,result=0,prefix_count={0:1,1:1}
#   num=1: prefix=2, prefix-2=0在!result+=1({0:1}表示有空数组可配合),prefix_count={0:1,1:1,2:1}
#   num=1: prefix=3, prefix-2=1在!result+=1({1:1}表示[1]),prefix_count={0:1,1:1,2:1,3:1}
#   结果:2
#   满足条件的子数组:[1,1](索引0-1)和[1,1](索引1-2)

滑动窗口最大值

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 返回 滑动窗口中的最大值 。

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3 输出:[3,3,5,5,6,7] 解释: 滑动窗口的位置 最大值

1
2
3
4
5
6
[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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from typing import List
from collections import deque


class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        """
        使用单调递减队列求滑动窗口最大值

        思路:
        - 维护一个单调递减的双端队列
        - 队列中存储数组元素的索引(不是值)
        - 队首元素始终是当前窗口的最大值
        - 入队时:移除所有小于当前元素的索引(它们不可能成为最大值)
        - 出队时:如果队首索引已超出窗口范围,则移除
        - 时间复杂度:O(n),每个元素最多入队出队一次
        """
        if not nums or k == 0:
            return []

        result = []
        deque_max = deque()  # 存储索引,值单调递减

        for i in range(len(nums)):
            # 1. 入队:移除队列中所有小于当前元素的索引
            #    这些元素永远不会成为窗口最大值
            while deque_max and nums[deque_max[-1]] < nums[i]:
                deque_max.pop()

            # 2. 将当前索引加入队列
            deque_max.append(i)

            # 3. 出队:移除队首超出窗口范围的索引
            if deque_max[0] <= i - k:
                deque_max.popleft()

            # 4. 当窗口形成后(i >= k-1),记录当前窗口最大值
            if i >= k - 1:
                result.append(nums[deque_max[0]])

        return result


# 测试用例
assert Solution().maxSlidingWindow([1, 3, -1, -3, 5, 3, 6, 7], 3) == [3, 3, 5, 5, 6, 7]
assert Solution().maxSlidingWindow([1], 1) == [1]
assert Solution().maxSlidingWindow([1, -1], 1) == [1, -1]
assert Solution().maxSlidingWindow([9, 11], 2) == [11]
assert Solution().maxSlidingWindow([4, 2, 12, 12, 12, 12, 12, 12], 3) == [12, 12, 12, 12, 12, 12]

# 手动推导 nums=[1,3,-1,-3,5,3,6,7], k=3:
#   i=0: deque=[0] (值3) -> 窗口未形成
#   i=1: nums[1]=3 > nums[0]=1,移除0;deque=[1]。窗口未形成
#   i=2: nums[2]=-1 < 3,保留;deque=[1,2]。窗口[1,3,-1],最大值=3 -> result=[3]
#   i=3: nums[3]=-3 < -1,保留;deque=[1,2,3]。窗口[3,-1,-3],最大值=3 -> result=[3,3]
#   i=4: nums[4]=5 > -1 和 -3,移除2,3;deque=[4]。窗口[-1,-3,5],最大值=5 -> result=[3,3,5]
#   i=5: nums[5]=3 < 5,保留;deque=[4,5]。窗口[-3,5,3],最大值=5 -> result=[3,3,5,5]
#   i=6: nums[6]=6 > 5,3,移除5,4;deque=[6]。窗口[5,3,6],最大值=6 -> result=[3,3,5,5,6]
#   i=7: nums[7]=7 > 6,移除6;deque=[7]。窗口[3,6,7],最大值=7 -> result=[3,3,5,5,6,7]

最小覆盖子串

给定两个字符串 s 和 t,长度分别是 m 和 n,返回 s 中的 最短 window 子串,使得该子串包含 t 中的每一个字符(包括重复字符)。如果没有这样的子串,返回空字符串 “"。 测试用例保证答案唯一。

输入:s = “ADOBECODEBANC”, t = “ABC” 输出:“BANC” 解释:最小覆盖子串 “BANC” 包含来自字符串 t 的 ‘A’、‘B’ 和 ‘C’。

进阶:你能设计一个在 O(m + n) 时间内解决此问题的算法吗?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
from typing import List
from collections import defaultdict


class Solution:
    def minWindow(self, s: str, t: str) -> str:
        """
        使用滑动窗口找到 s 中包含 t 所有字符的最小子串

        思路:
        - 维护一个滑动窗口 [left, right]
        - 用两个哈希表:need(t的字符需求)和 window(窗口中的字符计数)
        - 用 valid 变量记录当前窗口中满足条件的字符种类数
        - 扩大窗口:right 右移,增加窗口中的字符
        - 收缩窗口:当 valid == len(need) 时,尝试 left 右移收缩
        - 记录过程中的最小窗口
        """
        # 统计 t 中每个字符的需求次数
        need = defaultdict(int)
        for ch in t:
            need[ch] += 1

        # 窗口中字符的计数
        window = defaultdict(int)

        # 满足条件的字符种类数
        valid = 0

        # 滑动窗口边界
        left = 0
        min_len = float('inf')  # 最小窗口长度
        min_start = 0  # 最小窗口起始位置

        for right in range(len(s)):
            # 1. 扩大窗口:将 s[right] 加入窗口
            ch = s[right]
            if ch in need:
                window[ch] += 1
                if window[ch] == need[ch]:
                    valid += 1

            # 2. 收缩窗口:当窗口满足条件时,尝试左移 left
            while valid == len(need):
                # 更新最小窗口
                if right - left + 1 < min_len:
                    min_len = right - left + 1
                    min_start = left

                # 3. 尝试移出 s[left]
                left_ch = s[left]
                if left_ch in need:
                    if window[left_ch] == need[left_ch]:
                        valid -= 1  # 移除后不再满足条件
                    window[left_ch] -= 1

                left += 1

        # 返回结果(如果没找到,返回空字符串)
        return "" if min_len == float('inf') else s[min_start:min_start + min_len]


# 测试用例
assert Solution().minWindow("ADOBECODEBANC", "ABC") == "BANC"
assert Solution().minWindow("a", "a") == "a"
assert Solution().minWindow("a", "aa") == ""
assert Solution().minWindow("ab", "b") == "b"
assert Solution().minWindow("bbaa", "ab") == "baa"

# 手动推导 s="ADOBECODEBANC", t="ABC":
#   need = {'A':1, 'B':1, 'C':1}, valid=0
#   right=0, s[0]='A' in need: window['A']=1, valid=1
#   right=1, s[1]='D' not in need
#   right=2, s[2]='O' not in need
#   right=3, s[3]='B' in need: window['B']=1, valid=2
#   right=4, s[4]='E' not in need
#   right=5, s[5]='C' in need: window['C']=1, valid=3 (满足条件!)
#   开始收缩窗口:
#     left=0, s[0]='A': window['A']=1==need['A'],valid--,window['A']=0,left=1
#     valid=2,不再等于len(need),停止收缩
#   当前窗口 "DOBEC" (left=1, right=5),长度=5
#   继续...
#   最终最小窗口为 "BANC"

普通数组

最大子数组和

给你一个整数数组 nums,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

输入:nums = [-2,1,-3,4,-1,2,1,-5,4] 输出:6 解释:连续子数组 [4,-1,2,1] 的和最大,为 6。

进阶:如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from typing import List


class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        """
        使用动态规划(Kadane 算法)求最大子数组和

        思路:
        - 遍历数组,维护两个变量:
          - current_sum:以当前元素结尾的最大子数组和
          - max_sum:全局最大子数组和
        - 对于每个元素,要么将它加入之前的子数组,要么从它开始新子数组
        - 选择两者中较大的那个:current_sum = max(num, current_sum + num)
        - 时间 O(n),空间 O(1)
        """
        current_sum = nums[0]  # 以当前元素结尾的最大子数组和
        max_sum = nums[0]      # 全局最大子数组和

        for i in range(1, len(nums)):
            # 要么把 nums[i] 加入之前的子数组,要么从 nums[i] 重新开始
            current_sum = max(nums[i], current_sum + nums[i])
            # 更新全局最大值
            max_sum = max(max_sum, current_sum)

        return max_sum


# 测试用例
assert Solution().maxSubArray([-2, 1, -3, 4, -1, 2, 1, -5, 4]) == 6
assert Solution().maxSubArray([1]) == 1
assert Solution().maxSubArray([5, 4, -1, 7, 8]) == 23
assert Solution().maxSubArray([-1]) == -1
assert Solution().maxSubArray([-2, -1]) == -1

# 手动推导 nums=[-2,1,-3,4,-1,2,1,-5,4]:
#   i=1, num=1:  current_sum = max(1, -2+1) = max(1, -1) = 1,  max_sum = max(-2, 1) = 1
#   i=2, num=-3: current_sum = max(-3, 1-3) = max(-3, -2) = -2, max_sum = max(1, -2) = 1
#   i=3, num=4:  current_sum = max(4, -2+4) = max(4, 2) = 4,   max_sum = max(1, 4) = 4
#   i=4, num=-1: current_sum = max(-1, 4-1) = max(-1, 3) = 3,  max_sum = max(4, 3) = 4
#   i=5, num=2:  current_sum = max(2, 3+2) = max(2, 5) = 5,    max_sum = max(4, 5) = 5
#   i=6, num=1:  current_sum = max(1, 5+1) = max(1, 6) = 6,    max_sum = max(5, 6) = 6
#   i=7, num=-5: current_sum = max(-5, 6-5) = max(-5, 1) = 1,  max_sum = max(6, 1) = 6
#   i=8, num=4:  current_sum = max(4, 1+4) = max(4, 5) = 5,    max_sum = max(6, 5) = 6
# 结果:6,对应子数组 [4,-1,2,1]

合并区间

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi]。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。

输入:intervals = [[1,3],[2,6],[8,10],[15,18]] 输出:[[1,6],[8,10],[15,18]] 解释:区间 [1,3] 和 [2,6] 重叠,将它们合并为 [1,6]。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from typing import List


class Solution:
    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        """
        使用排序 + 遍历合并重叠区间

        思路:
        - 先按区间左端点排序
        - 遍历每个区间,与结果中最后一个区间比较:
          - 如果当前区间的左端点 <= 上一个区间的右端点,说明重叠,合并(取右端点较大值)
          - 否则不重叠,直接加入结果
        - 时间 O(n log n),空间 O(n)
        """
        if not intervals:
            return []

        # 按左端点排序
        intervals.sort(key=lambda x: x[0])

        result = [intervals[0]]

        for i in range(1, len(intervals)):
            current = intervals[i]
            last = result[-1]

            # 当前区间的左端点 <= 上一个区间的右端点,说明重叠
            if current[0] <= last[1]:
                # 合并:右端点取较大值
                last[1] = max(last[1], current[1])
            else:
                # 不重叠,直接加入结果
                result.append(current)

        return result


# 测试用例
assert Solution().merge([[1, 3], [2, 6], [8, 10], [15, 18]]) == [[1, 6], [8, 10], [15, 18]]
assert Solution().merge([[1, 4], [4, 5]]) == [[1, 5]]
assert Solution().merge([[4, 7], [1, 4]]) == [[1, 7]]
assert Solution().merge([[1, 4], [0, 4]]) == [[0, 4]]
assert Solution().merge([[1, 4], [2, 3]]) == [[1, 4]]

# 手动推导 [[1,3],[2,6],[8,10],[15,18]]:
#   排序后:[[1,3],[2,6],[8,10],[15,18]]
#   result = [[1,3]]
#   i=1, [2,6]: 2 <= 3,重叠,合并 -> last[1] = max(3,6) = 6 -> result = [[1,6]]
#   i=2, [8,10]: 8 > 6,不重叠,加入 -> result = [[1,6],[8,10]]
#   i=3, [15,18]: 15 > 10,不重叠,加入 -> result = [[1,6],[8,10],[15,18]]
# 结果:[[1,6],[8,10],[15,18]]

轮转数组

给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

输入:nums = [1,2,3,4,5,6,7], k = 3 输出:[5,6,7,1,2,3,4] 解释: 向右轮转 1 步: [7,1,2,3,4,5,6] 向右轮转 2 步: [6,7,1,2,3,4,5] 向右轮转 3 步: [5,6,7,1,2,3,4]

进阶:你可以使用空间复杂度为 O(1) 的原地算法解决这个问题吗?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
from typing import List


class Solution:
    def rotate(self, nums: List[int], k: int) -> None:
        """
        使用数组翻转实现原地轮转

        思路(三次翻转法):
        - 向右轮转 k 位,等价于把后 k 个元素移到前面
        - 步骤:
          1. 先翻转整个数组
          2. 再翻转前 k 个元素
          3. 再翻转后 n-k 个元素
        - 例如 [1,2,3,4,5,6,7], k=3:
          1. 翻转全部:[7,6,5,4,3,2,1]
          2. 翻转前3个:[5,6,7,4,3,2,1]
          3. 翻转后4个:[5,6,7,1,2,3,4]
        - 时间 O(n),空间 O(1)
        """
        n = len(nums)
        k = k % n  # k 可能大于 n,取模

        def reverse(left, right):
            """翻转 nums[left..right]"""
            while left < right:
                nums[left], nums[right] = nums[right], nums[left]
                left += 1
                right -= 1

        # 1. 翻转整个数组
        reverse(0, n - 1)
        # 2. 翻转前 k 个元素
        reverse(0, k - 1)
        # 3. 翻转后 n-k 个元素
        reverse(k, n - 1)


# 测试用例
nums1 = [1, 2, 3, 4, 5, 6, 7]
Solution().rotate(nums1, 3)
assert nums1 == [5, 6, 7, 1, 2, 3, 4]

nums2 = [-1, -100, 3, 99]
Solution().rotate(nums2, 2)
assert nums2 == [3, 99, -1, -100]

nums3 = [1, 2]
Solution().rotate(nums3, 3)
assert nums3 == [2, 1]

nums4 = [1]
Solution().rotate(nums4, 0)
assert nums4 == [1]

# 手动推导 nums=[1,2,3,4,5,6,7], k=3:
#   k = 3 % 7 = 3
#   第1步,翻转全部 [1,2,3,4,5,6,7] -> [7,6,5,4,3,2,1]
#   第2步,翻转前3个 [7,6,5,4,3,2,1] -> [5,6,7,4,3,2,1]
#   第3步,翻转后4个 [5,6,7,4,3,2,1] -> [5,6,7,1,2,3,4]
# 结果:[5,6,7,1,2,3,4]

除自身以外数组的乘积

给你一个整数数组 nums,返回数组 answer,其中 answer[i] 等于 nums 中除了 nums[i] 之外其余各元素的乘积。请不要使用除法,且在 O(n) 时间复杂度内完成此题。

输入:nums = [1,2,3,4] 输出:[24,12,8,6] 解释:24=234, 12=134, 8=124, 6=123

进阶:你可以在 O(1) 的额外空间复杂度内完成这个题目吗?(输出数组不被视为额外空间)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
from typing import List


class Solution:
    def productExceptSelf(self, nums: List[int]) -> List[int]:
        """
        使用前缀乘积 + 后缀乘积求解

        思路:
        - answer[i] = i 左边所有元素的乘积 * i 右边所有元素的乘积
        - 第一遍从左到右:计算每个位置左边所有元素的乘积
        - 第二遍从右到左:计算每个位置右边所有元素的乘积,同时更新 answer
        - 用一个变量 right_product 代替后缀乘积数组,实现 O(1) 额外空间
        - 时间 O(n),空间 O(1)(输出数组不计入)
        """
        n = len(nums)
        answer = [1] * n

        # 第一遍:计算每个位置左侧所有元素的乘积
        # answer[i] 存储 nums[0] * nums[1] * ... * nums[i-1]
        left_product = 1
        for i in range(n):
            answer[i] = left_product
            left_product *= nums[i]

        # 第二遍:从右到左,乘以每个位置右侧所有元素的乘积
        right_product = 1
        for i in range(n - 1, -1, -1):
            answer[i] *= right_product
            right_product *= nums[i]

        return answer


# 测试用例
assert Solution().productExceptSelf([1, 2, 3, 4]) == [24, 12, 8, 6]
assert Solution().productExceptSelf([-1, 1, 0, -3, 3]) == [0, 0, 9, 0, 0]
assert Solution().productExceptSelf([2, 3]) == [3, 2]
assert Solution().productExceptSelf([1, 2, 3]) == [6, 3, 2]

# 手动推导 nums=[1,2,3,4]:
#   第一遍(从左到右,计算左侧乘积):
#     i=0: answer[0] = 1 (左侧无元素),      left_product = 1*1 = 1
#     i=1: answer[1] = 1 (左侧为1),          left_product = 1*2 = 2
#     i=2: answer[2] = 2 (左侧为1*2),        left_product = 2*3 = 6
#     i=3: answer[3] = 6 (左侧为1*2*3),      left_product = 6*4 = 24
#     answer = [1, 1, 2, 6]
#   第二遍(从右到左,乘以右侧乘积):
#     i=3: answer[3] = 6 * 1 = 6,            right_product = 1*4 = 4
#     i=2: answer[2] = 2 * 4 = 8,            right_product = 4*3 = 12
#     i=1: answer[1] = 1 * 12 = 12,          right_product = 12*2 = 24
#     i=0: answer[0] = 1 * 24 = 24,          right_product = 24*1 = 24
#     answer = [24, 12, 8, 6]
# 结果:[24, 12, 8, 6]

缺失的第一个正数

给你一个未排序的整数数组 nums,请你找出其中没有出现的最小的正整数。要求时间复杂度为 O(n) 并且只使用常数级别额外空间。

输入:nums = [3,4,-1,1] 输出:2 解释:1 在数组中,但 2 没有。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
from typing import List


class Solution:
    def firstMissingPositive(self, nums: List[int]) -> int:
        """
        使用原地哈希(标记法)求缺失的第一个正数

        思路:
        - 长度为 n 的数组,缺失的第一个正数一定在 [1, n+1] 范围内
        - 利用数组本身做哈希表:让值 i 出现在索引 i-1 的位置
        - 遍历数组,将每个在 [1, n] 范围内的数放到它应该在的位置
        - 再次遍历,第一个不满足 nums[i] == i+1 的位置,答案就是 i+1
        - 如果所有位置都满足,则答案为 n+1
        - 时间 O(n),空间 O(1)
        """
        n = len(nums)

        for i in range(n):
            # 将 nums[i] 放到正确的位置 nums[i]-1
            # 条件:nums[i] 在 [1, n] 范围内,且目标位置的值不等于当前值(避免重复导致死循环)
            while 1 <= nums[i] <= n and nums[nums[i] - 1] != nums[i]:
                # 交换 nums[i] 和 nums[nums[i]-1]
                correct_idx = nums[i] - 1
                nums[i], nums[correct_idx] = nums[correct_idx], nums[i]

        # 找到第一个不在正确位置上的数
        for i in range(n):
            if nums[i] != i + 1:
                return i + 1

        # 所有位置都正确,缺失的是 n+1
        return n + 1


# 测试用例
assert Solution().firstMissingPositive([1, 2, 0]) == 3
assert Solution().firstMissingPositive([3, 4, -1, 1]) == 2
assert Solution().firstMissingPositive([7, 8, 9, 11, 12]) == 1
assert Solution().firstMissingPositive([1]) == 2
assert Solution().firstMissingPositive([2, 1]) == 3
assert Solution().firstMissingPositive([1, 1]) == 2

# 手动推导 nums=[3,4,-1,1],n=4:
#   i=0, nums[0]=3: 1<=3<=4 且 nums[2]=-1!=3,交换 -> [-1,4,3,1]
#     此时 nums[0]=-1,不在 [1,4] 内,停止
#   i=1, nums[1]=4: 1<=4<=4 且 nums[3]=1!=4,交换 -> [-1,1,3,4]
#     此时 nums[1]=1: 1<=1<=4 且 nums[0]=-1!=1,交换 -> [1,-1,3,4]
#     此时 nums[1]=-1,不在 [1,4] 内,停止
#   i=2, nums[2]=3: nums[2]=3,nums[2]==2+1,已在正确位置
#   i=3, nums[3]=4: nums[3]=4,nums[3]==3+1,已在正确位置
#   数组变为 [1,-1,3,4]
#   检查:nums[0]=1=0+1 ✓, nums[1]=-1≠1+1 ✗
#   返回 2
# 结果:2

矩阵

矩阵置零

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

输入:matrix = [[1,1,1],[1,0,1],[1,1,1]] 输出:[[1,0,1],[0,0,0],[1,0,1]]

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
from typing import List


class Solution:
    def setZeroes(self, matrix: List[List[int]]) -> None:
        """
        使用原地标记法实现矩阵置零

        思路:
        - 使用第一行和第一列作为标记数组
        - 遍历矩阵,如果某个元素为0,则将其所在行和列的标记位置零
        - 用两个变量记录第一行和第一列是否需要置零
        - 时间 O(mn),空间 O(1)
        """
        if not matrix or not matrix[0]:
            return

        m, n = len(matrix), len(matrix[0])

        # 1. 检查第一行和第一列是否需要置零
        row_zero = any(matrix[0][j] == 0 for j in range(n))
        col_zero = any(matrix[i][0] == 0 for i in range(m))

        # 2. 遍历除第一行第一列外的所有元素,标记需要置零的行和列
        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

        # 3. 根据标记,将需要置零的行置零(不包括第一列)
        for i in range(1, m):
            if matrix[i][0] == 0:
                for j in range(1, n):
                    matrix[i][j] = 0

        # 4. 根据标记,将需要置零的列置零(不包括第一行)
        for j in range(1, n):
            if matrix[0][j] == 0:
                for i in range(1, m):
                    matrix[i][j] = 0

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


# 测试用例
matrix1 = [[1, 1, 1], [1, 0, 1], [1, 1, 1]]
Solution().setZeroes(matrix1)
assert matrix1 == [[1, 0, 1], [0, 0, 0], [1, 0, 1]]

matrix2 = [[0, 1, 2, 0], [3, 4, 5, 2], [1, 3, 1, 5]]
Solution().setZeroes(matrix2)
assert matrix2 == [[0, 0, 0, 0], [0, 4, 5, 0], [0, 3, 1, 0]]

matrix3 = [[1]]
Solution().setZeroes(matrix3)
assert matrix3 == [[1]]

matrix4 = [[0]]
Solution().setZeroes(matrix4)
assert matrix4 == [[0]]

# 手动推导 matrix = [[1,1,1],[1,0,1],[1,1,1]]:
#   row_zero = False, col_zero = False
#   遍历发现 matrix[1][1]=0,标记 matrix[1][0]=0, matrix[0][1]=0
#   根据标记置零行和列,最后 row_zero/col_zero 为 False 不需要额外处理
# 结果:[[1,0,1],[0,0,0],[1,0,1]]

螺旋矩阵

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

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]] 输出:[1,2,3,6,9,8,7,4,5]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
from typing import List


class Solution:
    def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
        """
        模拟螺旋遍历矩阵

        思路:
        - 设置四个边界:top, bottom, left, right
        - 按顺时针方向遍历:
          1. 从左到右遍历上边界,遍历完后 top++
          2. 从上到下遍历右边界,遍历完后 right--
          3. 从右到左遍历下边界(如果上下边界还未相遇),遍历完后 bottom--
          4. 从下到上遍历左边界(如果左右边界还未相遇),遍历完后 left++
        - 当边界相遇时,遍历结束
        - 时间 O(mn),空间 O(1)
        """
        if not matrix or not matrix[0]:
            return []

        result = []
        m, n = len(matrix), len(matrix[0])

        top, bottom = 0, m - 1
        left, right = 0, n - 1

        while top <= bottom and left <= right:
            # 1. 从左到右遍历上边界
            for col in range(left, right + 1):
                result.append(matrix[top][col])
            top += 1

            # 2. 从上到下遍历右边界
            for row in range(top, bottom + 1):
                result.append(matrix[row][right])
            right -= 1

            # 3. 从右到左遍历下边界
            if top <= bottom:
                for col in range(right, left - 1, -1):
                    result.append(matrix[bottom][col])
                bottom -= 1

            # 4. 从下到上遍历左边界
            if left <= right:
                for row in range(bottom, top - 1, -1):
                    result.append(matrix[row][left])
                left += 1

        return result


# 测试用例
assert Solution().spiralOrder([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) == [1, 2, 3, 6, 9, 8, 7, 4, 5]
assert Solution().spiralOrder([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]) == [1, 2, 3, 4, 8, 12, 11, 10, 9, 5, 6, 7]
assert Solution().spiralOrder([[1]]) == [1]
assert Solution().spiralOrder([[1, 2]]) == [1, 2]
assert Solution().spiralOrder([[1], [2]]) == [1, 2]
assert Solution().spiralOrder([[1, 2, 3], [4, 5, 6]]) == [1, 2, 3, 6, 5, 4]

# 手动推导 matrix = [[1,2,3],[4,5,6],[7,8,9]]:
#   初始:top=0, bottom=2, left=0, right=2
#   第1圈:
#     从左到右(上边界): 1,2,3 -> top=1
#     从上到下(右边界): 6,9 -> right=1
#     从右到左(下边界): 8,7 -> bottom=1
#     从下到上(左边界): 4 -> left=1
#   第2圈:
#     从左到右: 5 -> top=2
#   边界相遇,结束
# 结果:[1,2,3,6,9,8,7,4,5]

旋转图像

给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。 你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]] 输出:[[7,4,1],[8,5,2],[9,6,3]]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
from typing import List


class Solution:
    def rotate(self, matrix: List[List[int]]) -> None:
        """
        先转置再翻转实现矩阵旋转

        思路:
        - 顺时针旋转90度 = 先沿主对角线转置 + 再沿垂直中线左右翻转
        - 或者:先沿副对角线转置 + 再沿水平中线上下翻转
        - 这里使用第一种方法:
          1. 转置:matrix[i][j] <-> matrix[j][i](只遍历上三角)
          2. 翻转:matrix[i][j] <-> matrix[i][n-1-j](每行反转)
        - 时间 O(n^2),空间 O(1)
        """
        n = len(matrix)

        # 1. 先沿主对角线转置
        for i in range(n):
            for j in range(i + 1, n):
                matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]

        # 2. 再沿垂直中线左右翻转
        for i in range(n):
            left, right = 0, n - 1
            while left < right:
                matrix[i][left], matrix[i][right] = matrix[i][right], matrix[i][left]
                left += 1
                right -= 1


# 测试用例
matrix1 = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Solution().rotate(matrix1)
assert matrix1 == [[7, 4, 1], [8, 5, 2], [9, 6, 3]]

matrix2 = [[5, 1, 9, 11], [2, 4, 8, 10], [13, 3, 6, 7], [15, 14, 12, 16]]
Solution().rotate(matrix2)
assert matrix2 == [[15, 13, 2, 5], [14, 3, 4, 1], [12, 6, 8, 9], [16, 7, 10, 11]]

matrix3 = [[1]]
Solution().rotate(matrix3)
assert matrix3 == [[1]]

matrix4 = [[1, 2], [3, 4]]
Solution().rotate(matrix4)
assert matrix4 == [[3, 1], [4, 2]]

# 手动推导 matrix = [[1,2,3],[4,5,6],[7,8,9]]:
#   原始矩阵:
#     1 2 3
#     4 5 6
#     7 8 9
#
#   第1步,转置(沿主对角线交换):
#     交换 (0,1)<->(1,0): 2<->4
#     交换 (0,2)<->(2,0): 3<->7
#     交换 (1,2)<->(2,1): 6<->8
#     转置后:
#     1 4 7
#     4 5 8
#     7 8 9
#
#   第2步,左右翻转(沿垂直中线):
#     第0行:1 4 7 -> 翻转 -> 7 4 1
#     第1行:4 5 8 -> 翻转 -> 8 5 4
#     第2行:7 8 9 -> 翻转 -> 9 8 7
#     最终结果:
#     7 4 1
#     8 5 2
#     9 6 3

搜索二维矩阵 II

编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性: 每行的元素从左到右升序排列。 每列的元素从上到下升序排列。

输入: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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
from typing import List


class Solution:
    def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
        """
        从右上角出发的二分查找

        思路:
        - 从右上角开始,左边是更小的数,下边是更大的数
        - 如果当前值等于目标,返回 True
        - 如果当前值大于目标,说明目标在左边(列减小)
        - 如果当前值小于目标,说明目标在下边(行增大)
        - 时间 O(m+n),空间 O(1)

        也可以从左下角开始,效果相同
        """
        if not matrix or not matrix[0]:
            return False

        m, n = len(matrix), len(matrix[0])

        # 从右上角开始
        row = 0
        col = n - 1

        while row < m and col >= 0:
            current = matrix[row][col]

            if current == target:
                return True
            elif current > target:
                # 当前值大于目标,目标在左边
                col -= 1
            else:
                # 当前值小于目标,目标在下边
                row += 1

        return False


# 测试用例
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]
]

assert Solution().searchMatrix(matrix, 5) == True
assert Solution().searchMatrix(matrix, 20) == False
assert Solution().searchMatrix(matrix, 1) == True
assert Solution().searchMatrix(matrix, 15) == True
assert Solution().searchMatrix(matrix, 30) == True
assert Solution().searchMatrix(matrix, 0) == False
assert Solution().searchMatrix(matrix, 100) == False
assert Solution().searchMatrix([[1]], 1) == True
assert Solution().searchMatrix([[1]], 2) == False

# 手动推导 matrix 和 target=5(从右上角出发):
#   初始:row=0, col=4, matrix[0][4]=15
#   15 > 5,目标在左边,col=3 -> matrix[0][3]=11
#   11 > 5,目标在左边,col=2 -> matrix[0][2]=7
#   7 > 5,目标在左边,col=1 -> matrix[0][1]=4
#   4 < 5,目标在下边,row=1 -> matrix[1][1]=5
#   5 == 5,找到目标!返回 True
#
# 手动推导 target=20(从右上角出发):
#   15 < 20,目标在下边,row=1 -> matrix[1][4]=19
#   19 < 20,目标在下边,row=2 -> matrix[2][4]=22
#   22 > 20,目标在左边,col=3 -> matrix[2][3]=16
#   16 < 20,目标在下边,row=3 -> matrix[3][3]=17
#   17 < 20,目标在下边,row=4 -> matrix[4][3]=26
#   26 > 20,目标在左边,col=2 -> matrix[4][2]=23
#   23 > 20,目标在左边,col=1 -> matrix[4][1]=21
#   21 > 20,目标在左边,col=0 -> matrix[4][0]=18
#   18 < 20,目标在下边,row=5(越界)
#   退出循环,返回 False