【leetcode】剑指 Offer(专项突击版)汇总

002. 二进制加法(反转相加 / 位运算)

题目:剑指 Offer II 002. 二进制加法

给定两个 01 字符串 a 和 b ,请计算它们的和,并以二进制字符串的形式输出。
输入为 非空 字符串且只包含数字 1 和 0。

示例 1:

输入: a = "11", b = "10"
输出: "101"

示例 2:

输入: a = "1010", b = "1011"
输出: "10101"

提示:

  • 每个字符串仅由字符 ‘0’ 或 ‘1’ 组成。
  • 1 < = a . l e n g t h , b . l e n g t h < = 1 0 4 1 <= a.length, b.length <= 10^4 1<=a.length,b.length<=104
  • 字符串如果不是 “0” ,就都不含前导零。

方法一:从后到前每位依次加

类似于把多位数字以链表的形式存储后来实现加法(如123+456=579,但是存储数字是以链表的形式存,即 1 → 2 → 3 + 4 → 5 → 6 = 5 → 7 → 9 1\to2\to3 + 4\to5\to6 = 5\to7\to9 123+456=579)的一种思路。

步骤(以 a = “11”, b = “10” 为例):

  1. 把数字反转 —— a = “11”, b = “01”
  2. 将反转后的两个数字从前往后 每位 进行加。

之所以需要反转就是因为可能出现最前面进位的情况,就如这个例子,加完后进位需要前面再补个 1。当然这道题以字符串的形式存储数字,自然不反转倒也可以,因为在前面补 1很方便,每位计算也不难。但如果是链表存多位数的话,首先每位计算时都要遍历链表到后面的位,假如两个4位数相加,就需要遍历4+3+2+1次两个链表,这样时间复杂度很高;其次,在进位时也比较复杂。不过这道题的话这么做确实没有必要,可以更简化一些,这里只是提供个思路。

class Solution:
    def addBinary(self, a: str, b: str) -> str:
        a = a[::-1]
        b = b[::-1]
        sum = ""
        jinwei = 0
        min_len = min(len(a), len(b))
        for i in range(min_len):
            c = int(a[i]) + int(b[i]) + jinwei
            if c < 2:
                sum += str(c)
                jinwei = 0
            else:
                sum += str(c % 2)
                jinwei = 1
        if len(a) == len(b):
            if jinwei:
                sum += str(jinwei)
            return sum[::-1]
        longer = a if len(a) > len(b) else b
        for i in range(min_len, len(longer)):
            c = int(longer[i]) + jinwei
            if c < 2:
                sum += str(c)
                jinwei = 0
            else:
                sum += str(c % 2)
                jinwei = 1
        if jinwei:
            sum += str(jinwei)
        return sum[::-1]

上面的代码有些繁杂,改变一下,这样可能更容易理解。不过经过多次提交后这个用时一直都大于上面代码的用时,上面的代码一般是击败80%-90%(32ms,36ms),下面这个只能击败30%左右(40ms,44ms)

class Solution:
    def addBinary(self, a: str, b: str) -> str:
        i = len(a) - 1
        j = len(b) - 1
        sum = ""
        jinwei = 0
        while(i >= 0 or j >= 0):
            add_a = int(a[i]) if i >= 0 else 0
            add_b = int(b[j]) if j >= 0 else 0
            c = add_a + add_b + jinwei
            if c < 2:
                sum += str(c)
                jinwei = 0
            else:
                sum += str(c % 2)
                jinwei = 1
            i -= 1
            j -= 1
        if jinwei:
            sum += "1"
        return sum[::-1]

方法二:位运算

我们可以设计这样的算法来计算:

  • 把 a 和 b 转换成整型数字 x 和 y,在接下来的过程中,x 保存结果,y 保存进位。
  • 当进位不为 0 时
    计算当前 x 和 y 的无进位相加结果:answer = x ^ y
    计算当前 x 和 y 的进位:carry = (x & y) << 1
    完成本次循环,更新 x = answer,y = carry
  • 返回 x 的二进制形式

为什么这个方法是可行的呢?在第一轮计算中,answer 的最后一位是 x 和 y 相加之后的结果,carry 的倒数第二位是 x 和 y 最后一位相加的进位。接着每一轮中,由于 carry 是由 x 和 y 按位与并且左移得到的,那么最后会补零,所以在下面计算的过程中后面的数位不受影响,而每一轮都可以得到一个低 i 位的答案和它向低 i + 1 位的进位,也就模拟了加法的过程。

class Solution:
    def addBinary(self, a: str, b: str) -> str:
        x, y = int(a, 2), int(b, 2)
        while y:
            sum = x ^ y
            jinwei = (x & y) << 1
            x, y = sum, jinwei
        return bin(x)[2:]

006. 排序数组中两个数字之和(二分查找 / 双指针)

题目:剑指 Offer II 006. 排序数组中两个数字之和

给定一个已按照 升序排列 的整数数组 numbers ,请你从数组中找出两个数满足相加之和等于目标数 target

函数应该以长度为 2 的整数数组的形式返回这两个数的下标值。numbers 的下标 从 0 开始计数 ,所以答案数组应当满足 0 < = a n s w e r [ 0 ] < a n s w e r [ 1 ] < n u m b e r s . l e n g t h 0 <= answer[0] < answer[1] < numbers.length 0<=answer[0]<answer[1]<numbers.length

假设数组中存在且只存在一对符合条件的数字,同时一个数字不能使用两次。

示例 1:

输入:numbers = [1,2,4,6,10], target = 8
输出:[1,3]
解释:2 与 6 之和等于目标数 8 。因此 index1 = 1, index2 = 3 。

示例 2:

输入:numbers = [2,3,4], target = 6
输出:[0,2]

示例 3:

输入:numbers = [-1,0], target = -1
输出:[0,1]

提示:

  • 2 <= numbers.length <= 3 * 1 0 4 10^4 104
  • -1000 <= numbers[i] <= 1000
  • numbers 按 递增顺序 排列
  • -1000 <= target <= 1000
  • 仅存在一个有效答案

方法一:穷举

具体实现略。

方法二:二分法

二分查找相关讲解:【二分查找】详细图解

class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        length = len(numbers)
        for i in range(length - 1):
            low = i + 1
            high = length - 1
            while low <= high:
                mid = (low + high) // 2
                if numbers[mid] == target - numbers[i]:
                    return [i, mid]
                elif numbers[mid] > target - numbers[i]:
                    high = mid - 1
                else:
                    low = mid + 1

时间复杂度: O ( n log ⁡ n ) O(n \log n) O(nlogn),其中 n 是数组的长度。需要遍历数组一次确定第一个数,时间复杂度是 O ( n ) O(n) O(n),寻找第二个数使用二分查找,时间复杂度是 O ( log ⁡ n ) O(\log n) O(logn),因此总时间复杂度是 O ( n log ⁡ n ) O(n \log n) O(nlogn)

空间复杂度: O ( 1 ) O(1) O(1)

方法三:双指针

class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        low = 0
        high = len(numbers) - 1
        while low < high:
            sum = numbers[low] + numbers[high]
            if sum == target:
                return [low, high]
            elif sum > target:
                high -= 1
            else:
                low += 1

时间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组的长度。两个指针移动的总次数最多为 n n n 次。

空间复杂度: O ( 1 ) O(1) O(1)

007. 数组中和为 0 的三个数(双指针)

题目:剑指 Offer II 007. 数组中和为 0 的三个数

给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a ,b ,c ,使得 a + b + c = 0 ?请找出所有和为 0 且 不重复 的三元组。

示例 1:

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]

示例 2:

输入:nums = []
输出:[]

示例 3:

输入:nums = [0]
输出:[]

提示:

  • 0 < = n u m s . l e n g t h < = 3000 0 <= nums.length <= 3000 0<=nums.length<=3000
  • − 1 0 5 < = n u m s [ i ] < = 1 0 5 -10^5 <= nums[i] <= 10^5 105<=nums[i]<=105

这道题其实是 006. 排序数组中两个数字之和 的进阶版,可以参考上道题的思路来进一步解决这道题。

方法一:穷举

具体实现略。

方法二:固定一个值 + 双指针

可以先看懂上一道题的双指针方法再看这道题。

首先把 nums 按从小到大排列,固定第一个值 i,0 - nums[i] 即为上一题的 target,然后剩下两个数的查找方法就用上一题双指针的方法即可。此外,这里还要注意去重的问题,因为题目中要求是 不重复 的三元组

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        res = []
        nums.sort()  # 从小到大排序
        length = len(nums)
        for i in range(length - 2):
            if i > 0 and nums[i] == nums[i-1]:  # 去重
                continue
            low = i + 1
            high = length - 1
            while low < high:
                if nums[i] + nums[low] + nums[high] == 0:
                    res.append([nums[i], nums[low], nums[high]])
                    while low < high:  # 去重
                        low += 1
                        if nums[low] != nums[low - 1]:
                            break
                    while low < high:  # 去重
                        high -= 1
                        if nums[high] != nums[high + 1]:
                            break
                elif nums[i] + nums[low] + nums[high] > 0:
                    high -= 1
                else:
                    low += 1
        return res

在上一道题中还有个二分查找的方法,不过这题用二分法就需要遍历两次,且去重等条件的实现有些复杂,不推荐使用。

008. 和大于等于 target 的最短子数组(滑动窗口 / 双指针)

题目:剑指 Offer II 008. 和大于等于 target 的最短子数组

给定一个含有 n 个正整数的数组和一个正整数 target 。

找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [ n u m s l , n u m s l + 1 , . . . , n u m s r − 1 , n u m s r ] [nums_l, nums_{l+1}, ..., nums_{r-1}, nums_r] [numsl,numsl+1,...,numsr1,numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。

示例 1:

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

示例 2:

输入:target = 4, nums = [1,4,4]
输出:1

示例 3:

输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0

提示:

  • 1 < = t a r g e t < = 1 0 9 1 <= target <= 10^9 1<=target<=109
  • 1 < = n u m s . l e n g t h < = 1 0 5 1 <= nums.length <= 10^5 1<=nums.length<=105
  • 1 < = n u m s [ i ] < = 1 0 5 1 <= nums[i] <= 10^5 1<=nums[i]<=105

方法一:滑动窗口

最简单的滑动窗口,类似枚举,不过这个超时了。

class Solution:
    def minSubArrayLen(self, target: int, nums: List[int]) -> int:
        for window_size in range(1, len(nums) + 1):
            for i in range(len(nums) + 1 - window_size):
                sum_window  = sum(nums[i: i + window_size])
                if sum_window >= target:
                    return window_size 
        return 0

方法二:双指针

双指针left,right

class Solution:
    def minSubArrayLen(self, target: int, nums: List[int]) -> int:
        left = 0
        sum_window = 0
        min_len = 1e5 + 1
        for right, value in enumerate(nums):
            sum_window += value
            while sum_window >= target:
                min_len = min(min_len, right - left + 1)
                sum_window -= nums[left]
                left += 1
        return min_len if min_len <= 1e5 else 0

target = 7, nums = [2,3,1,2,4,3] 为例:

[2] 3  1  2  4  3    sum_window < target
[2  3] 1  2  4  3    sum_window < target
[2  3  1] 2  4  3    sum_window < target
[2  3  1  2] 4  3    sum_window >= target, min_len = 4 
 2 [3  1  2] 4  3    sum_window < target
 2 [3  1  2  4] 3    sum_window >= target, min_len = 4 
 2  3 [1  2  4] 3    sum_window >= target, min_len = 3 
 2  3  1 [2  4] 3    sum_window < target
 2  3  1 [2  4  3]   sum_window >= target, min_len = 3 
 2  3  1  2 [4  3]   sum_window >= target, min_len = 2
 2  3  1  2  4 [3]   sum_window < target 

009. 乘积小于 K 的子数组(滑动窗口 / 双指针)

题目:剑指 Offer II 009. 乘积小于 K 的子数组

给定一个正整数数组 nums 和整数 k ,请找出该数组内乘积小于 k 的连续的子数组的个数。

示例 1:

输入: nums = [10,5,2,6], k = 100
输出: 8
解释: 8 个乘积小于 100 的子数组分别为: [10], [5], [2], [6], [10,5], [5,2], [2,6], [5,2,6]。
需要注意的是 [10,5,2] 并不是乘积小于100的子数组。

示例 2:

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

提示:

  • 1 < = n u m s . l e n g t h < = 3 ∗ 1 0 4 1 <= nums.length <= 3 * 10^4 1<=nums.length<=3104
  • 1 < = n u m s [ i ] < = 1000 1 <= nums[i] <= 1000 1<=nums[i]<=1000
  • 0 < = k < = 1 0 6 0 <= k <= 10^6 0<=k<=106

方法一:滑动窗口枚举

不出意外,这个又超时了

class Solution:
    def numSubarrayProductLessThanK(self, nums: List[int], k: int) -> int:
        def multiply(num_list):  # 累乘
            res = 1
            for num in num_list:
                res *= num
            return res
        num = 0
        for size in range(1, len(nums) + 1):
            for i in range(len(nums) + 1 - size):
                sum_window = multiply(nums[i: i + size])
                if sum_window < k:
                    num += 1
        return num

方法二:双指针

class Solution:
    def numSubarrayProductLessThanK(self, nums: List[int], k: int) -> int:
        length = len(nums)
        num = 0
        left = 0
        sum = 1
        for right, value in enumerate(nums):
            sum *= value
            while sum >= k and left <= right:
                sum /= nums[left]
                left += 1
            num += right - left + 1
        return num

nums = [10,5,2,6], k = 100 为例:

right = 0
[10] 5  2  6    sum < k    num += 0-0+1 = 1  [10]
right = 1
[10  5] 2  6    sum < k    num += 1-0+1 = 2  [5]  [10 5]
right = 2
[10  5  2] 6    sum = k
 10 [5  2] 6    sum < k    num += 2-1+1 = 2  [2]  [5 2]
right = 3 
 10 [5  2  6]   sum < k    num += 3-1+1 = 3  [6]  [2 6]  [5 2 6]

010. 和为 k 的子数组(前缀和+哈希表)

题目:剑指 Offer II 010. 和为 k 的子数组

给定一个整数数组 nums 和一个整数 k ,请找到该数组中和为 k 的连续子数组的个数。

示例 1:

输入:nums = [1,1,1], k = 2
输出: 2
解释: 此题 [1,1] 与 [1,1] 为两种不同的情况

示例 2:

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

提示:

  • 1 < = n u m s . l e n g t h < = 2 ∗ 1 0 4 1 <= nums.length <= 2 * 10^4 1<=nums.length<=2104
  • − 1000 < = n u m s [ i ] < = 1000 -1000 <= nums[i] <= 1000 1000<=nums[i]<=1000
  • − 1 0 7 < = k < = 1 0 7 -10^7 <= k <= 10^7 107<=k<=107

方法一:枚举法

这里我使用了 cumsum 计算累加和,Counter 计数。
相关讲解:Python中的numpy.cumsum()python Counter() 函数
方法很简单,不过超时了。

import numpy as np
from collections import Counter
class Solution:
    def subarraySum(self, nums: List[int], k: int) -> int:
        num = 0
        for left in range(len(nums)):
            accumulate = np.cumsum(nums[left:])
            counter = Counter(accumulate)
            num += counter[k]
        return num

方法二:前缀和+哈希表

nums = [1, 3, 2, 4, 1, 1, -2], k = 6 为例:

终点前缀和前缀和 - k解释
(0) 起点0
(1) num = 11-5
(2) num = 34-2
(3) num = 260以 (3) 为终点,从起点 (0) 开始,存在距离目标为 0 的路径为
1 → 2 → 3 1\to2\to3 123
(4) num = 4104以 (4) 为终点,从起点 (0) 开始,比目标k多出距离 4;
而从起点 (0) 开始到点 (2) 的距离正好为 4,因此解为 3 → 4 3\to4 34
(5) num = 1115
(6) num = 1126以 (6) 为终点,从起点 (0) 开始,比目标 k 多出距离 6;
而从起点 (0) 开始到点 (3) 的距离正好为 6,因此解为 4 → 5 → 6 4\to5\to6 456
(7) num = -2104以 (4) 为终点,从起点 (0) 开始,比目标 k 多出距离 4;
而从起点 (0) 开始到点 (2) 的距离正好为 4,
因此解为 3 → 4 → 5 → 6 → 7 3\to4\to5\to6\to7 34567
class Solution:
    def subarraySum(self, nums: List[int], k: int) -> int:
        num = 0
        prefix_sum = 0  # 前缀和
        prefix_dic = {0: 1}  # 统计各个前缀和的个数
        for value in nums:
            prefix_sum += value
            # 统计
            if prefix_sum - k in prefix_dic.keys():
                num += prefix_dic[prefix_sum - k]
            # 更新字典
            if prefix_sum not in prefix_dic.keys():
                prefix_dic[prefix_sum] = 1
            else:
                prefix_dic[prefix_sum] += 1
        return num

011. 0 和 1 个数相同的子数组(前缀和+哈希表)

题目:剑指 Offer II 011. 0 和 1 个数相同的子数组

给定一个二进制数组 nums , 找到含有相同数量的 01最长连续子数组,并返回该子数组的长度。

示例 1:

输入: nums = [0,1]
输出: 2
说明: [0, 1] 是具有相同数量 0 和 1 的最长连续子数组。

示例 2:

输入: nums = [0,1,0]
输出: 2
说明: [0, 1] (或 [1, 0]) 是具有相同数量 0 和 1 的最长连续子数组。

提示:

  • 1 < = n u m s . l e n g t h < = 1 0 5 1 <= nums.length <= 10^5 1<=nums.length<=105
  • n u m s [ i ] nums[i] nums[i] 不是 0 就是 1

方法一:前缀和+哈希表

思路与上一题相似。

「0 和 1 的数量相同」等价于「1 的数量减去 0 的数量等于 0」,我们可以将数组中的 0 视作 −1,则原问题转换成「求最长的连续子数组,其元素和为 0」。

nums[i] == 0 时,将其改为 nums[i] == -1nums[i] == 1 时不变,仍为 nums[i] == 1
这样就将「0 和 1 的数量相同」问题转化为「求和为 0最长 子数组的长度」

与上一道题的区别在于这题要得到 最长字数组的长度,因此字典(哈希表)中的 value 不再存储个数,而是变成 这个前缀和第一次出现的位置(即只有第一次出现时更新字典)。至于为什么是 第一次,毫无疑问,越靠前的位置长度越长。

nums = [0, 1, 1, 0, 1] 为例,等价于上一题的 nums = [-1, 1, 1, -1, 1], target = 0

终点前缀和前缀和 - target
(=前缀和)
解释
(-1)起点0
(0) num = -1-1-1
(1) num = 100以 (1) 为终点,从起点 (-1) 开始,
存在距离目标为 0 的路径为 0 → 1 0\to1 01
(2) num = 111
(3) num = -100以 (3) 为终点,从起点 (-1) 开始,存在距离目标为 0 的
路径为 0 → 1 → 2 → 3 0\to1\to2\to3 0123 2 → 3 2\to3 23,显然最长的是
0 → 1 → 2 → 3 0\to1\to2\to3 0123,这也是为什么字典中的 value 存储
的是 这个前缀和第一次出现的位置(即只有第一次出现
时才更新字典)
(4) num = 111以 (4) 为终点,从起点 (-1) 开始,比目标 target 多出距离 1;
而从起点 (-1) 开始到点 (2) 的距离正好为 1,因此满足条件的
路径为 3 → 4 3\to4 34
class Solution:
    def findMaxLength(self, nums: List[int]) -> int:
        res = 0
        for i in range(len(nums)):
            if nums[i] == 0:
                nums[i] = -1
        prefix_sum = 0
        prefix_dic = {0:-1}  # 前缀和: 首次出现的位置; 0设为-1
        for i in range(len(nums)):
            prefix_sum += nums[i]
            if prefix_sum in prefix_dic.keys():
                res = max(res, i - prefix_dic[prefix_sum])
            else:
                prefix_dic[prefix_sum] = i
        return res

012. 左右两边子数组的和相等(前缀和)

题目:剑指 Offer II 012. 左右两边子数组的和相等

给你一个整数数组 nums ,请计算数组的 中心下标

数组 中心下标 是数组的一个下标,其左侧所有元素相加的和等于右侧所有元素相加的和

如果中心下标位于数组最左端,那么左侧数之和视为 0 ,因为在下标的左侧不存在元素。这一点对于中心下标位于数组最右端同样适用。

如果数组有多个中心下标,应该返回 最靠近左边 的那一个。如果数组不存在中心下标,返回 -1 。

示例 1:

输入:nums = [1,7,3,6,5,6]
输出:3
解释:
中心下标是 3 。
左侧数之和 sum = nums[0] + nums[1] + nums[2] = 1 + 7 + 3 = 11 ,
右侧数之和 sum = nums[4] + nums[5] = 5 + 6 = 11 ,二者相等。

示例 2:

输入:nums = [1, 2, 3]
输出:-1
解释:
数组中不存在满足此条件的中心下标。

示例 3:

输入:nums = [2, 1, -1]
输出:0
解释:
中心下标是 0 。
左侧数之和 sum = 0 ,(下标 0 左侧不存在元素),
右侧数之和 sum = nums[1] + nums[2] = 1 + -1 = 0 。

提示:

  • 1 < = n u m s . l e n g t h < = 1 0 4 1 <= nums.length <= 10^4 1<=nums.length<=104
  • − 1000 < = n u m s [ i ] < = 1000 -1000 <= nums[i] <= 1000 1000<=nums[i]<=1000

方法一:枚举法

class Solution:
    def pivotIndex(self, nums: List[int]) -> int:
        length = len(nums)
        if length == 1:
            return 0
        if sum(nums[1:]) == 0:
            return 0
        for i in range(1, length-1):
            if sum(nums[:i]) == sum(nums[i+1:]):
                return i
        if sum(nums[:length-1]) == 0:
            return length - 1
        return -1

方法二:前缀和

记数组的全部元素之和为 total \textit{total} total,当遍历到第 i i i 个元素时,设其左侧元素之和为 sum \textit{sum} sum,则其右侧元素之和为 total − nums i − sum \textit{total}-\textit{nums}_i-\textit{sum} totalnumsisum。左右侧元素相等即为 sum = total − nums i − sum \textit{sum}=\textit{total}-\textit{nums}_i-\textit{sum} sum=totalnumsisum,即 2 × sum + nums i = total 2\times\textit{sum}+\textit{nums}_i=\textit{total} 2×sum+numsi=total

当中心下标左侧或右侧没有元素时,即为零个项相加,这在数学上称作「空和」( empty sum \text{empty sum} empty sum)。在程序设计中我们约定「空和是零」。

class Solution:
    def pivotIndex(self, nums: List[int]) -> int:
        total = sum(nums)
        prefix_sum = 0
        for i in range(len(nums)):
            if 2 * prefix_sum + nums[i] == total:
                return i
            prefix_sum += nums[i]
        return -1

013. 二维子矩阵的和(前缀和)

题目:剑指 Offer II 013. 二维子矩阵的和

给定一个二维矩阵 matrix,以下类型的多个请求:

计算其子矩形范围内元素的总和,该子矩阵的左上角为 (row1, col1) ,右下角为 (row2, col2) 。
实现 NumMatrix 类:

NumMatrix(int[][] matrix) 给定整数矩阵 matrix 进行初始化
int sumRegion(int row1, int col1, int row2, int col2) 返回左上角 (row1, col1) 、右下角 (row2, col2) 的子矩阵的元素总和。

示例 1:

输入: 
["NumMatrix","sumRegion","sumRegion","sumRegion"]
[[[[3,0,1,4,2],[5,6,3,2,1],[1,2,0,1,5],[4,1,0,1,7],[1,0,3,0,5]]],[2,1,4,3],[1,1,2,2],[1,2,2,4]]
输出: 
[null, 8, 11, 12]

解释:
NumMatrix numMatrix = new NumMatrix([[3,0,1,4,2],[5,6,3,2,1],[1,2,0,1,5],[4,1,0,1,7],[1,0,3,0,5]]]);
numMatrix.sumRegion(2, 1, 4, 3); // return 8 (红色矩形框的元素总和)
numMatrix.sumRegion(1, 1, 2, 2); // return 11 (绿色矩形框的元素总和)
numMatrix.sumRegion(1, 2, 2, 4); // return 12 (蓝色矩形框的元素总和)

提示:

  • m = = m a t r i x . l e n g t h m == matrix.length m==matrix.length
  • n = = m a t r i x [ i ] . l e n g t h n == matrix[i].length n==matrix[i].length
  • 1 < = m , n < = 200 1 <= m, n <= 200 1<=m,n<=200
  • − 1 0 5 < = m a t r i x [ i ] [ j ] < = 1 0 5 -10^5 <= matrix[i][j] <= 10^5 105<=matrix[i][j]<=105
  • 0 < = r o w 1 < = r o w 2 < m 0 <= row1 <= row2 < m 0<=row1<=row2<m
  • 0 < = c o l 1 < = c o l 2 < n 0 <= col1 <= col2 < n 0<=col1<=col2<n
  • 最多调用 1 0 4 次 s u m R e g i o n 方法 最多调用 10^4 次 sumRegion 方法 最多调用104sumRegion方法

方法一:一维前缀和

初始化时对矩阵的每一行计算前缀和,检索时对二维区域中的每一行计算子数组和,然后对每一行的子数组和计算总和。

具体实现方面,创建 m m m n + 1 n+1 n+1 列的二维数组 sums \textit{sums} sums,其中 m m m n n n 分别是矩阵 matrix \textit{matrix} matrix 的行数和列数, sums [ i ] \textit{sums}[i] sums[i] matrix [ i ] \textit{matrix}[i] matrix[i] 的前缀和数组。将 sums \textit{sums} sums 的列数设为 n + 1 n+1 n+1 的目的是为了方便计算每一行的子数组和,不需要对 col 1 = 0 \textit{col}_1=0 col1=0 的情况特殊处理。

class NumMatrix:
    def __init__(self, matrix: List[List[int]]):
        m = len(matrix)
        n = len(matrix[0]) if matrix else 0
        self.sums = [[0] * (n+1) for _ in range(m)]  # m * (n+1) 的二维矩阵
        # 无法用[[0] * (n-1)] * m 替代
        # 原因可查看:https://blog.csdn.net/hq_cjj/article/details/86659589

        for i in range(m):
            for j in range(n):
                self.sums[i][j+1] = self.sums[i][j] + matrix[i][j]

    def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int:
        total = sum([self.sums[i][col2+1] - self.sums[i][col1] for i in range(row1, row2+1)])
        return total

# Your NumMatrix object will be instantiated and called as such:
# obj = NumMatrix(matrix)
# param_1 = obj.sumRegion(row1,col1,row2,col2)
  • 时间复杂度:初始化 O ( m n ) O(mn) O(mn),每次检索 O ( m ) O(m) O(m),其中 m m m n n n 分别是矩阵 matrix \textit{matrix} matrix 的行数和列数。
    初始化需要遍历矩阵 matrix \textit{matrix} matrix 计算二维前缀和,时间复杂度是 O ( m n ) O(mn) O(mn)
    每次检索需要对二维区域中的每一行计算子数组和,二维区域的行数不超过 m m m,计算每一行的子数组和的时间复杂度是 O ( 1 ) O(1) O(1),因此每次检索的时间复杂度是 O ( m ) O(m) O(m)

  • 空间复杂度: O ( m n ) O(mn) O(mn),其中 m m m n n n 分别是矩阵 matrix \textit{matrix} matrix 的行数和列数。需要创建一个 m m m n + 1 n+1 n+1 列的前缀和数组 sums \textit{sums} sums

方法二:二维前缀和

假设 m m m n n n 分别是矩阵 matrix \textit{matrix} matrix 的行数和列数。定义当 0 ≤ i < m 0 \le i<m 0i<m 0 ≤ j < n 0 \le j<n 0j<n 时, f ( i , j ) f(i,j) f(i,j) 为矩阵 matrix \textit{matrix} matrix 的以 ( i , j ) (i,j) (i,j) 为右下角的子矩阵的元素之和:

f ( i , j ) = ∑ p = 0 i ∑ q = 0 j matrix [ p ] [ q ] f(i,j)=\sum\limits_{p=0}^i \sum\limits_{q=0}^j \textit{matrix}[p][q] f(i,j)=p=0iq=0jmatrix[p][q]

f ( i , j ) f(i,j) f(i,j) 的计算如下:
f ( i , j ) = f ( i − 1 , j ) + f ( i , j − 1 ) − f ( i − 1 , j − 1 ) + matrix [ i ] [ j ] f(i,j)=f(i-1,j)+f(i,j-1)-f(i-1,j-1)+\textit{matrix}[i][j] f(i,j)=f(i1,j)+f(i,j1)f(i1,j1)+matrix[i][j]

class NumMatrix:
    def __init__(self, matrix: List[List[int]]):
        m = len(matrix)
        n = len(matrix[0]) if matrix else 0
        self.sums = [[0] * (n+1) for _ in range(m+1)]  # (m+1) * (n+1) 的二维矩阵

        for i in range(m):
            for j in range(n):
                self.sums[i+1][j+1] = self.sums[i+1][j] + self.sums[i][j+1] - self.sums[i][j] + matrix[i][j]

    def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int:
        total = self.sums[row2+1][col2+1] - self.sums[row1][col2+1] - self.sums[row2+1][col1] + self.sums[row1][col1]
        return total
  • 时间复杂度:初始化 O ( m n ) O(mn) O(mn),每次检索 O ( 1 ) O(1) O(1),其中 m m m n n n 分别是矩阵 matrix \textit{matrix} matrix 的行数和列数。
    初始化需要遍历矩阵 matrix \textit{matrix} matrix 计算二维前缀和,时间复杂度是 O ( m n ) O(mn) O(mn)
    每次检索的时间复杂度是 O ( 1 ) O(1) O(1)

  • 空间复杂度: O ( m n ) O(mn) O(mn),其中 m m m n n n 分别是矩阵 matrix \textit{matrix} matrix 的行数和列数。需要创建一个 m + 1 m+1 m+1 n + 1 n+1 n+1 列的二维前缀和数组 sums \textit{sums} sums

014. 字符串中的变位词(滑动窗口 / 双指针)

题目:剑指 Offer II 014. 字符串中的变位词

给定两个字符串 s1s2,写一个函数来判断 s2 是否包含 s1 的某个变位词。

换句话说,第一个字符串的排列之一是第二个字符串的 子串

示例 1:

输入: s1 = "ab" s2 = "eidbaooo"
输出: True
解释: s2 包含 s1 的排列之一 ("ba").

示例 2:

输入: s1= "ab" s2 = "eidboaoo"
输出: False

提示:

  • 1 < = s 1. l e n g t h , s 2. l e n g t h < = 1 0 4 1 <= s1.length, s2.length <= 10^4 1<=s1.length,s2.length<=104
  • s1 和 s2 仅包含小写字母

方法1:滑动窗口

固定窗口大小为 s1 的长度,利用 Counter 统计各个字母的数量,当两个Counter(Counter内部无先后顺序) 相同时,则返回True。

from collections import Counter
class Solution:
    def checkInclusion(self, s1: str, s2: str) -> bool:
        size = len(s1)
        counter1 = Counter(s1)
        for i in range(len(s2) - size + 1):
            counter2 = Counter(s2[i: i+size])
            if counter1 == counter2:
                return True
        return False

另一种滑动窗口,不同的是这里统计用的是长为26的列表。

class Solution:
    def checkInclusion(self, s1: str, s2: str) -> bool:
        size1, size2 = len(s1), len(s2)
        if size1 > size2:
            return False
        list1, list2 = [0] * 26, [0] * 26
        for i in range(size1):
            list1[ord(s1[i]) - ord('a')] += 1
            list2[ord(s2[i]) - ord('a')] += 1
        if list1 == list2:
            return True
        for i in range(size1, size2):
            list2[ord(s2[i]) - ord('a')] += 1
            list2[ord(s2[i-size1]) - ord('a')] -= 1
            if list1 == list2:
                return True
        return False

优化

注意到每次窗口滑动时,只统计了一进一出两个字符,却比较了整个 cnt 1 \textit{cnt}_1 cnt1 cnt 2 \textit{cnt}_2 cnt2 数组。(上面代码中的 l i s t 1 , l i s t 2 list1,list2 list1,list2 即是这里的 cnt 1 \textit{cnt}_1 cnt1 cnt 2 \textit{cnt}_2 cnt2

从这个角度出发,我们可以用一个变量 diff \textit{diff} diff 来记录 cnt 1 \textit{cnt}_1 cnt1 cnt 2 \textit{cnt}_2 cnt2 的不同值的个数,这样判断 cnt 1 \textit{cnt}_1 cnt1 cnt 2 \textit{cnt}_2 cnt2 是否相等就转换成了判断 diff \textit{diff} diff 是否为 0.

每次窗口滑动,记一进一出两个字符为 x x x y y y.

x = y x=y x=y 则对 cnt 2 \textit{cnt}_2 cnt2 无影响,可以直接跳过。

x ≠ y x\ne y x=y,对于字符 x x x,在修改 cnt 2 \textit{cnt}_2 cnt2 之前若有 cnt 2 [ x ] = cnt 1 [ x ] \textit{cnt}_2[x]=\textit{cnt}_1[x] cnt2[x]=cnt1[x],则将 diff \textit{diff} diff 加一;在修改 cnt 2 \textit{cnt}_2 cnt2 之后若有 cnt 2 [ x ] = cnt 1 [ x ] \textit{cnt}_2[x]=\textit{cnt}_1[x] cnt2[x]=cnt1[x],则将 diff \textit{diff} diff 减一。字符 y y y 同理。

此外,为简化上述逻辑,我们可以只用一个数组 cnt \textit{cnt} cntcnt,其中 cnt [ x ] = cnt 2 [ x ] − cnt 1 [ x ] \textit{cnt}[x]=\textit{cnt}_2[x]-\textit{cnt}_1[x] cnt[x]=cnt2[x]cnt1[x],将 cnt 1 [ x ] \textit{cnt}_1[x] cnt1[x] cnt 2 [ x ] \textit{cnt}_2[x] cnt2[x] 的比较替换成 cnt [ x ] \textit{cnt}[x] cnt[x] 与 0 的比较。

class Solution:
    def checkInclusion(self, s1: str, s2: str) -> bool:
        size1, size2 = len(s1), len(s2)
        if size1 > size2:
            return False
        cnt = [0] * 26
        for i in range(size1):
            cnt[ord(s1[i]) - ord('a')] -= 1
            cnt[ord(s2[i]) - ord('a')] += 1
        diff = 0
        for c in cnt:
            if c != 0:
                diff += 1
        if diff == 0:        
            return True
        for i in range(size1, size2):
            x = ord(s2[i]) - ord('a')
            y = ord(s2[i-size1]) - ord('a')
            if x == y:
                continue
            # 右边
            if cnt[x] == 0:
                diff += 1
            cnt[x] += 1
            if cnt[x] == 0:
                diff -= 1
            # 左边
            if cnt[y] == 0:
                diff += 1
            cnt[y] -= 1
            if cnt[y] == 0:
                diff -= 1

            if diff == 0:
                return True
        return False

方法2:双指针

回顾方法一的思路,我们在保证区间长度为 n n n 的情况下,去考察是否存在一个区间使得 cnt \textit{cnt} cnt 的值全为 0。

反过来,还可以在保证 cnt \textit{cnt} cnt 的值不为正的情况下,去考察是否存在一个区间,其长度恰好为 n n n

初始时,仅统计 s 1 s_1 s1​ 中的字符,则 cnt \textit{cnt} cnt 的值均不为正,且元素值之和为 − n -n n

然后用两个指针 left \textit{left} left right \textit{right} right 表示考察的区间 [ left , right ] [\textit{left},\textit{right}] [left,right] right \textit{right} right 每向右移动一次,就统计一次进入区间的字符 x x x。为保证 cnt \textit{cnt} cnt 的值不为正,若此时 cnt [ x ] > 0 \textit{cnt}[x]>0 cnt[x]>0,则向右移动左指针,减少离开区间的字符的 cnt \textit{cnt} cnt 值直到 cnt [ x ] ≤ 0 \textit{cnt}[x] \le 0 cnt[x]0

注意到 [ left , right ] [\textit{left},\textit{right}] [left,right] 的长度每增加 1, cnt \textit{cnt} cnt 的元素值之和就增加 1。当 [ left , right ] [\textit{left},\textit{right}] [left,right] 的长度恰好为 n n n 时,就意味着 cnt \textit{cnt} cnt 的元素值之和为 0。由于 cnt \textit{cnt} cnt 的值不为正,元素值之和为 0 就意味着所有元素均为 0,这样我们就找到了一个目标子串。

class Solution:
    def checkInclusion(self, s1: str, s2: str) -> bool:
        size1, size2 = len(s1), len(s2)
        if size1 > size2:
            return False
        cnt = [0] * 26
        for i in range(size1):
            cnt[ord(s1[i]) - ord('a')] -= 1
        left = 0
        for right in range(size2):
            x = ord(s2[right]) - ord('a')
            cnt[x] += 1
            while cnt[x] > 0:
                cnt[ord(s2[left]) - ord('a')] -= 1
                left += 1
            if right - left + 1 == size1:
                return True
        return False

015. 字符串中的所有变位词(滑动窗口 / 双指针)

题目:剑指 Offer II 015. 字符串中的所有变位词

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

变位词 指字母相同,但排列不同的字符串。

示例 1:

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

示例 2:

输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的变位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的变位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的变位词。

提示:

  • 1 < = s . l e n g t h , p . l e n g t h < = 3 ∗ 1 0 4 1 <= s.length, p.length <= 3 * 10^4 1<=s.length,p.length<=3104
  • s s s p p p 仅包含小写字母

方法1:滑动窗口

与上一题思路一样

class Solution:
    def findAnagrams(self, s: str, p: str) -> List[int]:
        size_s, size_p = len(s), len(p)
        if size_s < size_p:
            return []
        cnt1, cnt2 = [0] * 26, [0] * 26
        for i in range(size_p):
            cnt1[ord(p[i]) - ord('a')] += 1
            cnt2[ord(s[i]) - ord('a')] += 1
        res = []
        if cnt1 == cnt2:
            res.append(0)
        for i in range(size_p, size_s):
            cnt2[ord(s[i]) - ord('a')] += 1
            cnt2[ord(s[i-size_p]) - ord('a')] -= 1
            if cnt1 == cnt2:
                res.append(i-size_p+1)
        return res

方法2:双指针

与上一题思路相同

class Solution:
    def findAnagrams(self, s: str, p: str) -> List[int]:
        size_s, size_p = len(s), len(p)
        if size_s < size_p:
            return []
        cnt = [0] * 26
        for i in range(size_p):
            cnt[ord(p[i]) - ord('a')] -= 1
        res = []
        left = 0
        for right in range(size_s):
            x = ord(s[right]) - ord('a')
            cnt[x] += 1
            while cnt[x] > 0:
                cnt[ord(s[left]) - ord('a')] -= 1
                left += 1
            if right - left + 1 == size_p:
                res.append(left)
        return res

016. 不含重复字符的最长子字符串(滑动窗口 / 双指针)

题目:剑指 Offer II 016. 不含重复字符的最长子字符串

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

示例 1:

输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子字符串是 "abc",所以其长度为 3。

示例 2:

输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子字符串是 "b",所以其长度为 1。

示例 3:

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

示例 4:

输入: s = ""
输出: 0

提示:

  • 0 < = s . l e n g t h < = 5 ∗ 1 0 4 0 <= s.length <= 5 * 10^4 0<=s.length<=5104
  • s s s 由英文字母、数字、符号和空格组成

方法1:双指针/滑动窗口

找出从每一个字符开始的,不包含重复字符的最长子串,那么其中最长的那个字符串即为答案。对于示例一中的字符串,我们列举出这些结果,其中括号中表示选中的字符以及最长的字符串:

(a)bcabcbb \texttt{(a)bcabcbb} (a)bcabcbb 开始的最长字符串为 (abc)abcbb \texttt{(abc)abcbb} (abc)abcbb
a(b)cabcbb \texttt{a(b)cabcbb} a(b)cabcbb 开始的最长字符串为 a(bca)bcbb \texttt{a(bca)bcbb} a(bca)bcbb
ab(c)abcbb \texttt{ab(c)abcbb} ab(c)abcbb 开始的最长字符串为 ab(cab)cbb \texttt{ab(cab)cbb} ab(cab)cbb
abc(a)bcbb \texttt{abc(a)bcbb} abc(a)bcbb 开始的最长字符串为 abc(abc)bb \texttt{abc(abc)bb} abc(abc)bb
abca(b)cbb \texttt{abca(b)cbb} abca(b)cbb 开始的最长字符串为 abca(bc)bb \texttt{abca(bc)bb} abca(bc)bb
abcab(c)bb \texttt{abcab(c)bb} abcab(c)bb 开始的最长字符串为 abcab(cb)b \texttt{abcab(cb)b} abcab(cb)b
abcabc(b)b \texttt{abcabc(b)b} abcabc(b)b 开始的最长字符串为 abcabc(b)b \texttt{abcabc(b)b} abcabc(b)b
abcabcb(b) \texttt{abcabcb(b)} abcabcb(b) 开始的最长字符串为 abcabcb(b) \texttt{abcabcb(b)} abcabcb(b)

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        length = len(s)
        if length == 0:
            return 0
        min_len = 1
        left, right = 0, 0
        occ = set(s[0])
        while left < length - min_len:  # 左指针到length-min_len-1就够了,再往后min_len也不会超过先前的了,不用到length
            if left != 0:
                occ.remove(s[left - 1])  # 左指针向右移动一格
            while right + 1 < length and s[right + 1] not in occ:
                right += 1 
                occ.add(s[right])
            if right - left + 1 > min_len:
                min_len = right - left + 1
            left += 1
        return min_len
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

friedrichor

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

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

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

打赏作者

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

抵扣说明:

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

余额充值