哈希表

两数之和

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

示例 1:

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

示例 2:

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

示例 3:

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

暴力解法

遍历,时间复杂度为$O(N^2)$,空间复杂度为$O(1)$

1
2
3
4
5
6
7
8
9
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
n = len(nums)
for i in range(n):
for j in range(i+1, n):
if nums[i] + nums[j] == target:
return [i, j]

return []

哈希表

时间复杂度$O(N)$,空间复杂度$O(N)$,为哈希表的开销

1
2
3
4
5
6
7
8
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
hashtable = dict()
for i, num in enumerate(nums):
if target - num in hashtable:
return [hashtable[target - num], i]
hashtable[nums[i]] = i
return []

python 的 dict 和 C++ 的 unordered_map 基本一致

字母异位词分组

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

示例 1:

输入: strs = [“eat”, “tea”, “tan”, “ate”, “nat”, “bat”]

输出: [[“bat”],[“nat”,”tan”],[“ate”,”eat”,”tea”]]

解释:

  • 在 strs 中没有字符串可以通过重新排列来形成 "bat"
  • 字符串 "nat""tan" 是字母异位词,因为它们可以重新排列以形成彼此
  • 字符串 "ate""eat""tea" 是字母异位词,因为它们可以重新排列以形成彼此

示例 2:

输入: strs = [“”]

输出: [[“”]]

示例 3:

输入: strs = [“a”]

输出: [[“a”]]


可以重构的字符串在排序后结果相同,可以以此作为哈希表的键,原字符串作为值,最后输出哈希表值的元组

哈希表排序

1
2
3
4
5
6
7
8
9
10
class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
# key 不存在时,自动给一个空列表
mp = defaultdict(list)

for st in strs:
key = "".join(sorted(st))
mp[key].append(st)

return list(mp.values())
  • 会写 dict[key].xxx(),而且 key 可能第一次出现 → 用 defaultdict
  • 写的是 dict[key] = value,并且读之前会判断 → 不要用 defaultdict

最长连续序列

给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度

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

示例 1:

1
2
3
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。

示例 2:

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

示例 3:

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

既然要时间复杂度为O(n)那必然不能暴力枚举了

如果对每个数都向左、向右扩展,那最坏会退化成O(n²)

破解方法:只有当一个数是“连续序列起点”时,才从它开始向右数

不需要重复数字,直接用set快速剔除,保留不同数字

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution:
def longestConsecutive(self, nums: List[int]) -> int:
nums_set = set(nums) # 转成set
maxlen = 0

for num in nums_set:
if (num - 1) not in nums_set:
len = 1

while (num + len) in nums_set:
len += 1
maxlen = max(maxlen, len)
return maxlen

双指针

移动零

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序

请注意 ,必须在不复制数组的情况下原地对数组进行操作

示例 1:

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

示例 2:

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

不复制数组只能是直接交换,末尾都是0,头部都是非0值

使用双指针,左指针指向当前已经处理好的序列的尾部,右指针指向待处理序列的头部[快慢指针]

右指针走的更快,以其为循环结束标志

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
n = len(nums)
left = right = 0
while right < n:
if nums[right] != 0:
nums[left], nums[right] = nums[right], nums[left]
left += 1
right += 1

盛最多水的容器

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0)(i, height[i])

找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量

示例 1:

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

示例 2:

1
2
输入:height = [1,1]
输出:1

发现盛水量等于(right-left)*min(height[left],height[right])

直接用双指针走就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
int maxArea(vector<int>& height) {
int left = 0, right = height.size() - 1;
int ans = 0;
while (left < right) {
int water = min(height[left], height[right]) * (right - left);
ans = max(ans, water);
if (height[left] <= height[right])
left++;
else
right--;
}
return ans;
}
};

三数之和

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

**注意:**答案中不可以包含重复的三元组

示例 1:

1
2
3
4
5
6
7
8
输入: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] 。
注意,输出的顺序和三元组的顺序并不重要。

示例 2:

1
2
3
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。

示例 3:

1
2
3
输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。

首先注意到不能有重复输出(不同排列方式),所以最好的去重就是先排序,后续只需要移动指针判断是否值相等跳过自动就实现去重

固定一个,从左往右走,注意,固定的这个数必须小于0才有可能实现3数和为0,同时需要判断是否重复

剩下两个使用左右指针,和小于0加左边,和大于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
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
nums.sort()
ans = list()
n = len(nums)

for c in range(n - 2):
if c > 0 and nums[c] == nums[c - 1]:
continue
if nums[c] > 0:
break
l, r = c + 1, n - 1
while l < r:
sum = nums[c] + nums[l] + nums[r]
if sum == 0:
ans.append([nums[c], nums[l], nums[r]])
l += 1
r -= 1
while l < r and nums[l] == nums[l - 1]:
l += 1
while l < r and nums[r] == nums[r + 1]:
r -= 1
elif sum < 0:
l += 1
else:
r -= 1
return ans

接雨水

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

示例 1:

1
2
3
输入: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 个单位的雨水(蓝色部分表示雨水)。

示例 2:

1
2
输入:height = [4,2,0,3,2,5]
输出:9

首先要知道雨水怎么接的,看图可以发现是凹处能接到水,转为代码语言就是高度小于左侧右侧最大值其中较小值的地方

方法1:动态规划

具体来说,对于下标i,能接的雨水量等于下标i处的水能到达的最大高度减去heigh[i]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution:
def trap(self, height: List[int]) -> int:
n = len(height)
leftmax = list()
rightmax = list()
max_l, max_r = 0, 0 # 当前最大值
for i in range(n):
max_l = max(max_l, height[i])
max_r = max(max_r, height[n - 1 - i])
leftmax.append(max_l)
rightmax.append(max_r)

rightmax = rightmax[::-1] # 反转right

water = 0
for i in range(n):
water += min(leftmax[i], rightmax[i]) - height[i] # 两者小值-高度

return water

时间复杂度和空间复杂度都是O(n)

能否降低空间复杂度呢?

回到凹处的定义,是高度小于左侧右侧最大值其中较小值的地方

那如果先找到一个最大值,以其为分界,那么左右两块都只要看当前的最大值就能得知左右侧最大值的较小值,这样就不需要建立数组,使得空间复杂度降低为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
class Solution:
def trap(self, height: List[int]) -> int:
max_loc = -1
max_height = 0
for i in range(len(height)):
if height[i] > max_height:
max_loc = i
max_height = height[i]

if max_loc == -1:
return 0

water = 0
max_cur = 0
for i in range(max_loc):
if height[i] > max_cur:
max_cur = height[i]
water += max_cur - height[i]

max_cur = 0
for i in range(len(height) - 1, max_loc, -1):
if height[i] > max_cur:
max_cur = height[i]
water += max_cur - height[i]

return water

滑动窗口

无重复字符的最长子串

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

示例 1:

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

示例 2:

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

示例 3:

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

提示:

  • 0 <= s.length <= 5 * 104
  • s 由英文字母、数字、符号和空格组成

既然出现了不重复,肯定第一下要想到set,但是还需要记录长度,所以又要用到左右指针了,左指针记录无重复字符串的开端,右指针记录无重复字符串的尾端,长度为right-left+1

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
seen = set()
left = 0
max_len = 0
for right in range(len(s)):
while s[right] in seen:
seen.remove(s[left]) # 删除直到无s[right]
left += 1
seen.add(s[right])
max_len = max(max_len, right - left + 1)
return max_len

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

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

示例 1:

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

示例 2:

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

提示:

  • 1 <= s.length, p.length <= 3 * 104
  • sp 仅包含小写字母

异位词其实就是组成的字母相同,如果枚举匹配那么复杂度直接爆炸,构建一个和p相同长度的滑动窗口,统计数量就行(注意,这是因为这里只有小写字母),如果有字符或者空格就比较麻烦了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
s_len, p_len = len(s), len(p)
if s_len < p_len: # p长度必须小于等于s
return []
ans = []
s_count = [0] * 26
p_count = [0] * 26
for i in range(p_len):
p_count[ord(p[i]) - ord("a")] += 1
s_count[ord(s[i]) - ord("a")] += 1
if p_count == s_count:
ans.append(0)

for i in range(s_len - p_len):
s_count[ord(s[i]) - ord("a")] -= 1 # 剔除窗口头部字母
s_count[ord(s[i + p_len]) - ord("a")] += 1 # 加入窗口下一个字母

if s_count == p_count:
ans.append(i + 1)

return ans

子串

和为 K 的子数组

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数

子数组是数组中元素的连续非空序列。

示例 1:

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

示例 2:

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

子串肯定不能直接去获取然后计算,这个时候需要一点数学技巧

比如需要从下标i到j的子串,那么是不是可以等价于从0到j的子串减去从0到i的子串,这样只要遍历一次就能获得所有的子串

prefix2 - prefix1 = k 转变为 查找已经存进哈希表的值里有没有等于 prefix2 - k 的

注意,要在哈希表里默认加一个0,这样才能准确认到自身单元素的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
ans = 0
prefix_sum = 0 # 当前前缀和
mp = {0: 1} # 前缀和 -> 出现次数

for x in nums:
prefix_sum += x

if prefix_sum - k in mp:
ans += mp[prefix_sum - k]

mp[prefix_sum] = mp.get(prefix_sum, 0) + 1

return ans

如果用枚举也有一种方法,起始指针不断向右移动,取起始指针往左计算和,如果累积和匹配则计数+1

但这种方法的复杂度在O(n^2)

1
2
3
4
5
6
7
8
9
10
class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
ans = 0
for start in range(len(nums)):
sum = 0
for end in range(start, -1, -1):
sum += nums[end]
if sum == k:
ans += 1
return ans

滑动窗口最大值

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

返回滑动窗口中的最大值

示例 1:

1
2
3
4
5
6
7
8
9
10
11
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[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

示例 2:

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

对于每个滑动窗口可以使用O(k)时间遍历其中的每一个元素,找出其中的最大值

对于长度为n的数组而言,窗口的数量为n*−*k+1,因此该算法的时间复杂度为O((n-l+1)k)=O(nk),k大的情况下复杂度太大,不能用普通的左右指针

方法1:

对于最大值的存储,可以想到优先队列

优先队列里存储一组pair,第一个数是确切值,第二个数是索引值

在判断最大值时只需要让最大值的索引在滑动窗口的范围内,就是窗口的最大值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int n = nums.size();
priority_queue<pair<int, int>> q;
// 创建第一个最大值
for (int i = 0; i < k; i++) {
q.emplace(nums[i], i);
}
vector<int> ans = {q.top().first};
for (int i = k; i < n; i++) {
q.emplace(nums[i], i);
// 移出直到最大值在滑动窗口内
while (q.top().second <= i - k) {
q.pop();
}
ans.push_back(q.top().first);
}
return ans;
}
};

相比暴力解法,时间复杂度为O(nlogn),每次放入和移出优先队列的复杂度为O(logn),n个元素

空间复杂度为O(n),只有一个存储的优先队列

在python中

结构 C++ Python
priority_queue heapq
队列 deque deque
哈希 unordered_map dict

并且不太一样的是,c++中是最大值优先队列,在py中是最小堆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
n = len(nums)
# Python 默认的优先队列是小根堆
# 写成负值,找最大值变为找最小值
q = [(-nums[i],i) for i in range(k)]
heapq.heapify(q) # O(n)

ans = [-q[0][0]] # 送入结果时要带上"-"转回
for i in range(k,n):
heapq.heappush(q, (-nums[i],i))
while q[0][1] <= i-k
heapq.heappop(q)
ans.append(-q[0][0])

return ans

方法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
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> dq;
int n = nums.size();
// 获取第一个窗口内最大值的索引
for (int i = 0; i < k; i++) {
// 没有优先队列的自动排序需要比较后进值与尾端值
while (!dq.empty() && nums[i] >= nums[dq.back()]) {
dq.pop_back();
}
dq.push_back(i);
}
vector<int> ans = {nums[dq.front()]};

for (int i = k; i < n; i++) {
while (!dq.empty() && nums[i] >= nums[dq.back()]) {
dq.pop_back();
}
// 保存当前值的索引到队列尾端
dq.push_back(i);
// 移出队列头部直到索引在窗口内
while (dq.front() <= i - k) {
dq.pop_front();
}
ans.push_back(nums[dq.front()]);
}
return ans;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
n = len(nums)
dq = deque()
for i in range(k):
while dq and nums[i] >= nums[dq[-1]]:
dq.pop()
dq.append(i)

ans = [nums[dq[0]]]

for i in range(k, n):
while dq and nums[i] >= nums[dq[-1]]:
dq.pop()
dq.append(i)
while dq[0] <= i - k:
dq.popleft()
ans.append(nums[dq[0]])
return ans