参考
- 《算法通关手册》-哈希表篇
- 【博文】散列表(上)- 数据结构与算法之美 - 极客时间
哈希表(Hash Table):也叫做散列表。是根据关键码值(Key Value)直接进行访问的数据结构。
哈希表通过「键 key
」和「映射函数 Hash(key)
」计算出对应的「值 value
」,把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做「哈希函数(散列函数)」,存放记录的数组叫做「哈希表(散列表)」。
哈希表的关键思想是使用哈希函数,将键 key
映射到对应表的某个区块中。我们可以将算法思想分为两个部分:
哈希表的原理示例图如下所示:
在上图例子中,我们使用 value = Hash(key) = key // 1000
作为哈希函数。//
符号代表整除。我们以这个例子来说明一下哈希表的插入和查找策略。
0138
通过哈希函数 Hash(key) = 0138 // 100 = 0
,得出应将 0138
分配到0
所在的区块中。2321
,通过哈希函数,得出 2321
应该在 2
所对应的区块中。然后我们从 2
对应的区块中继续搜索,并在 2
对应的区块中成功找到了 2321
。3214
,通过哈希函数,得出 3214
应该在 3
所对应的区块中。然后我们从 3
对应的区块中继续搜索,但并没有找到对应值,则说明 3214
不在哈希表中。哈希表在生活中的应用也很广泛,其中一个常见例子就是「查字典」。
比如为了查找 赞
这个字的具体意思,我们在字典中根据这个字的拼音索引 zan
,查找到对应的页码为 599
。然后我们就可以翻到字典的第 599
页查看 赞
字相关的解释了。
在这个例子中:
赞
字的拼音索引 zan
可以看做是哈希表中的 「关键字 key
」。zan
来确定字对应页码的过程可以看做是哈希表中的 「哈希函数 Hash(key)
」。599
可以看做是哈希表中的 「哈希地址 value
」。哈希函数(Hash Function):将哈希表中元素的关键键值映射为元素存储位置的函数。
哈希函数是哈希表中最重要的部分。一般来说,哈希函数会满足以下几个条件:
Hash(key1)
不等于 Hash(key2)
,那么 key1
、key2
一定不相等。Hash(key1)
等于 Hash(key2)
,那么 key1
、key2
可能相等,也可能不相等(会发生哈希碰撞)。在哈希表的实际应用中,关键字的类型除了数字类,还有可能是字符串类型、浮点数类型、大整数类型,甚至还有可能是几种类型的组合。一般我们会将各种类型的关键字先转换为整数类型,再通过哈希函数,将其映射到哈希表中。
而关于整数类型的关键字,通常用到的哈希函数方法有:直接定址法、除留余数法、平方取中法、基数转换法、数字分析法、折叠法、随机数法、乘积法、点积法等。下面我们介绍几个常用的哈希函数方法。
Hash(key) = key
或者 Hash(key) = a * key + b
,其中 a
和 b
为常数。这种方法计算最简单,且不会产生冲突。适合于关键字分布基本连续的情况,如果关键字分布不连续,空位较多,则会造成存储空间的浪费。
举一个例子,假设我们有一个记录了从 1
岁到 100
岁的人口数字统计表。其中年龄为关键字,哈希函数取关键字自身,如下表所示。
年龄 | 1 | 2 | 3 | … | 25 | 26 | 27 | … | 100 |
---|---|---|---|---|---|---|---|---|---|
人数 | 3000 | 2000 | 5000 | … | 1050 | … | … | … | … |
比如我们想要查询 25
岁的人有多少,则只要查询表中第 25
项即可。
m
,取一个不大于 m
但接近或等于 m
的质数 p
,利用取模运算,将关键字转换为哈希地址。即:Hash(key) = key % p
,其中 p
为不大于 m
的质数。 这也是一种简单且常用的哈希函数方法。其关键点在于 p
的选择。根据经验而言,一般 p
取素数或者 m
,这样可以尽可能的减少冲突。
比如我们需要将 7
个数 [432, 5, 128, 193, 92, 111, 88]
存储在 11
个区块中(长度为 11
的数组),通过除留余数法将这 7
个数应分别位于如下地址:
索引 | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
数据 | 88 | 111 | 432 | 92 | 5 | 193 | 128 |
Hash(key) = (key * key) // 100 % 1000
,先计算平方,去除末尾的 2 位数,再取中间 3 位数作为哈希地址。这种方法因为关键字平方值的中间几位数和原关键字的每一位数都相关,所以产生的哈希地址也比较均匀,有利于减少冲突的发生。
13
进制的数,再将其转变为 10
进制的数,将其作为哈希地址。以 343246
为例,哈希地址计算方式如下:
34324 6 13 = 3 × 1 3 5 + 4 × 1 3 4 + 3 × 1 3 3 + 2 × 1 3 2 + 4 × 1 3 1 + 6 × 1 3 0 = 123511 0 10 343246_{13} = 3 \times 13^5 + 4 \times 13^4 + 3 \times 13^3 + 2 \times 13^2 + 4 \times 13^1 + 6 \times 13^0 = 1235110_{10} 34324613=3×135+4×134+3×133+2×132+4×131+6×130=123511010
哈希冲突(Hash Collision):不同的关键字通过同一个哈希函数可能得到同一哈希地址,即
key1 ≠ key2
,而Hash(key1) = Hash(key2)
,这种现象称为哈希冲突。
理想状态下,我们的哈希函数是完美的一对一映射,即一个关键字(key)对应一个值(value),不需要处理冲突。但是一般情况下,不同的关键字 key
可能对应了同一个值 value
,这就发生了哈希冲突。
设计再好的哈希函数也无法完全避免哈希冲突。所以就需要通过一定的方法来解决哈希冲突问题。常用的哈希冲突解决方法主要是两类:「开放地址法(Open Addressing)」 和 「链地址法(Chaining)」。
开放地址法(Open Addressing):指的是将哈希表中的「空地址」向处理冲突开放。当哈希表未满时,处理冲突时需要尝试另外的单元,直到找到空的单元为止。
当发生冲突时,开放地址法按照下面的方法求得后继哈希地址:H(i) = (Hash(key) + F(i)) % m
,i = 1, 2, 3, ..., n (n ≤ m - 1)
。
H(i)
是在处理冲突中得到的地址序列。即在第 1 次冲突(i = 1
)时经过处理得到一个新地址 H(1)
,如果在 H(1)
处仍然发生冲突(i = 2
)时经过处理时得到另一个新地址 H(2)
…… 如此下去,直到求得的 H(n)
不再发生冲突。Hash(key)
是哈希函数,m
是哈希表表长,对哈希表长取余的目的是为了使得到的下一个地址一定落在哈希表中。F(i)
是冲突解决方法,取法可以有以下几种:
举个例子说说明一下如何用以上三种冲突解决方法处理冲突,并得到新地址 H(i)
。例如,在长度为 11
的哈希表中已经填有关键字分别为 28
、49
、18
的记录(哈希函数为 Hash(key) = key % 11
)。现在将插入关键字为 38
的新纪录。根据哈希函数得到的哈希地址为 5
,产生冲突。接下来分别使用这三种冲突解决方法处理冲突。
H(1) = (5 + 1) % 11 = 6
,仍然冲突;继续求出 H(2) = (5 + 2) % 11 = 7
,仍然冲突;继续求出 H(3) = (5 + 3) % 11 = 8
,8
对应的地址为空,处理冲突过程结束,记录填入哈希表中序号为 8
的位置。H(1) = (5 + 1*1) % 11 = 6
,仍然冲突;继续求出 H(2) = (5 - 1*1) % 11 = 4
,4
对应的地址为空,处理冲突过程结束,记录填入哈希表中序号为 4
的位置。9
,则得到下一个地址 H(1) = (9 + 5) % 11 = 3
,3
对应的地址为空,处理冲突过程结束,记录填入哈希表中序号为 3
的位置。使用这三种方法处理冲突的结果如下图所示:
链地址法(Chaining):将具有相同哈希地址的元素(或记录)存储在同一个线性链表中。
链地址法是一种更加常用的哈希冲突解决方法。相比于开放地址法,链地址法更加简单。
我们假设哈希函数产生的哈希地址区间为 [0, m - 1]
,哈希表的表长为 m
。则可以将哈希表定义为一个有 m
个头节点组成的链表指针数组 T
。
这样在插入关键字的时候,我们只需要通过哈希函数 Hash(key)
计算出对应的哈希地址 i
,然后将其以链表节点的形式插入到以 T[i]
为头节点的单链表中。在链表中插入位置可以在表头或表尾,也可以在中间。如果每次插入位置为表头,则插入操作的时间复杂度为 O ( 1 ) O(1) O(1)。
而在在查询关键字的时候,我们只需要通过哈希函数 Hash(key)
计算出对应的哈希地址 i
,然后将对应位置上的链表整个扫描一遍,比较链表中每个链节点的键值与查询的键值是否一致。查询操作的时间复杂度跟链表的长度 k
成正比,也就是 O ( k ) O(k) O(k)。对于哈希地址比较均匀的哈希函数来说,理论上讲,k = n // m
,其中 n
为关键字的个数,m
为哈希表的表长。
举个例子来说明如何使用链地址法处理冲突。假设现在要存入的关键字集合 keys = [88, 60, 65, 69, 90, 39, 07, 06, 14, 44, 52, 70, 21, 45, 19, 32]
。再假定哈希函数为 Hash(key) = key % 13
,哈希表的表长 m = 13
,哈希地址范围为 [0, m - 1]
。将这些关键字使用链地址法处理冲突,并按顺序加入哈希表中(图示为插入链表表尾位置),最终得到的哈希表如下图所示。
相对于开放地址法,采用链地址法处理冲突要多占用一些存储空间(主要是链节点占用空间)。但它可以减少在进行插入和查找具有相同哈希地址的关键字的操作过程中的平均查找长度。这是因为在链地址法中,待比较的关键字都是具有相同哈希地址的元素,而在开放地址法中,待比较的关键字不仅包含具有相同哈希地址的元素,而且还包含哈希地址不相同的元素。
本文讲解了一些比较基础、偏理论的哈希表知识。包含哈希表的定义,哈希函数、哈希冲突以及哈希冲突的解决方法。
key
和一个映射函数 Hash(key)
计算出对应的值 value
,把关键码值映射到表中一个位置来访问记录,以加快查找的速度。哈希表的两个核心问题是:「哈希函数的构建」 和 「哈希冲突的解决方法」。
题号 | 标题 | 题解 | 标签 | 难度 |
---|---|---|---|---|
0705 | 设计哈希集合 | Python | 哈希表 | 简单 |
0706 | 设计哈希映射 | Python | 哈希表 | 简单 |
0217 | 存在重复元素 | Python | 数组、哈希表 | 简单 |
0219 | 存在重复元素 II | Python | 数组、哈希表 | 简单 |
0220 | 存在重复元素 III | Python | 排序、有序集合、哈希表 | 中等 |
1941 | 检查是否所有字符出现次数相同 | Python | 哈希表、字符串、计数 | 简单 |
0136 | 只出现一次的数字 | Python | 位运算、数组 | 简单 |
0383 | 赎金信 | Python | 哈希表、字符串、计数 | 简单 |
0349 | 两个数组的交集 | Python | 数组、哈希表 | 简单 |
0350 | 两个数组的交集 II | Python | 数组、哈希表 | 简单 |
0036 | 有效的数独 | Python | 哈希表 | 中等 |
0001 | 两数之和 | Python | 数组、哈希表 | 简单 |
0015 | 三数之和 | Python | 数组、双指针 | 中等 |
0018 | 四数之和 | Python | 数组、哈希表、双指针 | 中等 |
0454 | 四数相加 II | Python | 哈希表 | 中等 |
0041 | 缺失的第一个正数 | Python | 数组、哈希表 | 困难 |
0128 | 最长连续序列 | Python | 并查集、数组、哈希表 | 中等 |
0202 | 快乐数 | Python | 哈希表、数学 | 简单 |
0242 | 有效的字母异位词 | Python | 字符串、哈希表、排序 | 简单 |
0205 | 同构字符串 | Python | 哈希表 | 简单 |
0442 | 数组中重复的数据 | |||
剑指 Offer 61 | 扑克牌中的顺子 | Python | 数组、排序 | 简单 |
0268 | 丢失的数字 | Python | 位运算、数组、数学 | 简单 |
剑指 Offer 03 | 数组中重复的数字 | Python | 数组、哈希表、排序 | 简单 |
0451 | 根据字符出现频率排序 | Python | 哈希表、字符串、桶排序、计数、排序、堆(优先队列) | 中等 |
0049 | 字母异位词分组 | Python | 字符串、哈希表 | 中等 |
0599 | 两个列表的最小索引总和 | Python | 哈希表 | 简单 |
0387 | 字符串中的第一个唯一字符 | Python | 字符串、哈希表 | 简单 |
0447 | 回旋镖的数量 | Python | 哈希表、数学 | 中等 |
0149 | 直线上最多的点数 | Python | 哈希表、数学 | 困难 |
0359 | 日志速率限制器 | Python | 设计、哈希表 | 简单 |
0811 | 子域名访问计数 | Python | 数组、哈希表、字符串、计数 | 中等 |
- 0705. 设计哈希集合
- 0706 设计哈希映射
要求不使用内建的哈希表库,自行实现一个哈希集合(HashSet),满足以下操作:
示例:
#输入:
["MyHashSet", "add", "add", "contains", "contains", "add", "contains", "remove", "contains"]
[[], [1], [2], [1], [3], [2], [2], [2], [2]]
#输出:
[null, null, null, true, false, null, true, null, false]
#解释:
MyHashSet myHashSet = new MyHashSet();
myHashSet.add(1); // set = [1]
myHashSet.add(2); // set = [1, 2]
myHashSet.contains(1); // 返回 True
myHashSet.contains(3); // 返回 False ,(未找到)
myHashSet.add(2); // set = [1, 2]
myHashSet.contains(2); // 返回 True
myHashSet.remove(2); // set = [1]
myHashSet.contains(2); // 返回 False ,(已移除)
思路:利用「数组+链表」的方式实现哈希集合
定义一个一维长度为 buckets 的二维数组 table。第一维度用于计算哈希函数,为 key 分桶。第二个维度用于寻找 key 存放的具体位置。第二维度的数组会根据 key 值动态增长,模拟真正的链表。
class MyHashSet:
def __init__(self):
self.buckets = 1003
self.table = [[] for _ in range(self.buckets)]
def hash(self, key):
return key % self.buckets
def add(self, key: int) -> None:
hash_key = self.hash(key)
if key in self.table[hash_key]:
return self.table[hash_key].append(key)
def remove(self, key: int) -> None:
hash_key = self.hash(key)
if key not in self.table[hash_key]:
return self.table[hash_key].remove(key)
def contains(self, key: int) -> bool:
hash_key = self.hash(key)
return key in self.table[hash_key]
设计哈希映射和上一题其实是一样的,可以直接套用代码:
class MyHashMap:
def __init__(self):
self.buckets=1003 # 使用质数
self.table=[[] for _ in range(self.buckets)]
def put(self, key: int, value: int) -> None:
hash_key,pos=self.hash(key)
if not self.table[hash_key]:
self.table[hash_key]=[-1]*self.buckets
self.table[hash_key][pos]=value
def hash(self,key):
#哈希函数,等同于将数字分为1000个桶
return key%self.buckets,key//self.buckets
def remove(self, key: int) -> None:
hash_key,pos=self.hash(key)
if self.table[hash_key]:
self.table[hash_key][pos]=-1
def get(self, key: int) -> bool:
hash_key,pos=self.hash(key)
return -1 if self.table[hash_key]==[] else self.table[hash_key][pos]
# Your MyHashSet object will be instantiated and called as such:
# obj = MyHashSet()
# obj.add(key)
# obj.remove(key)
# param_3 = obj.contains(key)
# Your MyHashMap object will be instantiated and called as such:
# obj = MyHashMap()
# obj.put(key,value)
# param_2 = obj.get(key)
# obj.remove(key)
- 0217 存在重复元素
- 0219 存在重复元素 II
- 0220 存在重复元素 III
给你一个整数数组 nums 。如果任一值在数组中出现 至少两次 ,返回 true ;如果数组中每个元素互不相同,返回 false
return False if len(nums)==len(set(nums)) else True
给你一个整数数组 nums
和一个整数 k
,判断数组中是否存在两个 不同的索引 i
和 j
,满足 nums[i] == nums[j]
且 abs(i - j) <= k
。如果存在,返回 true ;否则,返回 false 。
解法一:
class Solution:
def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
d=set()
for i in range(len(nums)):
if nums[i] in d:
return True
d.add(nums[i])
if len(d)>k:
d.remove(nums[i-k])
return False
解法二:
class Solution:
def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
d=dict()
# 如果k值超过数组长度,将其赋值为数组长度-1,然后统计前k个数字的出现此数。
# 如果某个元素已经出现在字典中,直接返回True
if k>len(nums)-1:
k=len(nums)-1
l,r=0,k
for i in range(k+1):
if nums[i] not in d:
d[nums[i]]=d.get(nums[i],0)+1
else:
return True
# 依次往右滑动窗口,每次删除左边界元素,加入右边界元素(次数+1)
while r<len(nums)-1:
l+=1
r+=1
del d[nums[l-1]]
if nums[r] not in d:
d[nums[r]]=d.get(nums[r],0)+1
else:
return True
return False
给你一个整数数组 nums
和两个整数 k
和 t
。请你判断是否存在 两个不同下标 i
和 j
,使得 abs(nums[i] - nums[j]) <= t
,同时又满足 abs(i - j) <= k
。
思路 1:滑动窗口(固定长度)
k
,右指针为i
的滑动窗口,每次遍历到 nums[i]
时,只需要检查前 k
个元素是否有某个元素在 [nums[i] - t, nums[i + t]
区间内即可。k
个元素是否在 [nums[i] - t, nums[i + t]
区间,可以借助有序数组(比如 SortedList
)+ 二分查找来解决,从而减少时间复杂度。如果是无序数组,则滑动窗口内只能对于每个元素都遍历一次队列来检查是否有元素符合条件。如果数组的长度为
n
,则使用队列的时间复杂度为O(nk)
,会超出时间限制。
具体步骤如下:
使用有序数组类 window
维护一个长度为 k
的窗口,满足数组内元素有序,且支持增加和删除操作。
将当前元素填入窗口中,即 window.add(nums[i])
。
当窗口元素大于 k
个时,移除窗口最左侧元素。
当窗口元素小于等于 k
个时(即i
windows.bisect_left
,查找 nums[i]
在 window
中的位置 idx
(windows是有序数组,每次遍历的元素nums[i]
会放到窗口中的合适位置)。window[idx]
与相邻位置上元素差值绝对值,若果满足 abs(window[idx] - window[idx - 1]) <= t
或者 abs(window[idx + 1] - window[idx]) <= t
时返回 True
。
重复 2
~ 4
步,直到遍历至数组末尾,如果还没找到满足条件的情况,则返回 False
。
class Solution:
def containsNearbyAlmostDuplicate(self, nums: List[int], indexDiff: int, valueDiff: int) -> bool:
from sortedcontainers import SortedList
k,t=indexDiff,valueDiff
windows=SortedList()
for i in range(len(nums)):
if len(windows)>k:
windows.remove(nums[i-k-1])
windows.add(nums[i])
# idx和i的区别是,idx是nums[i]在有序数组windows中的位置
idx=windows.bisect_left(nums[i])
# 判断nums[i]和它在窗口中相邻的两个元素只差的绝对值,是否在t之内
if idx>0 and abs(windows[idx]-windows[idx-1])<=t: # idx>0则idx-1最小为0
return True
if idx<len(windows)-1 and abs(windows[idx+1]-windows[idx])<=t:
return True
return False
SortedList详情请参考《SortedList【python类】》
思路 2:桶排序
述解法无法做到线性的原因是:我们需要在大小为 k
的滑动窗口所在的「有序集合」中找到与 nums[i]
接近的数。如果我们能够将 k
个数字分到 k
个桶的话,那么我们就能 在O(1) 的复杂度确定是否有 [nums[i]−t,nums[i]+t]
的数字(检查目标桶是否有元素)。
t + 1
。只需要使用一重循环遍历位置 i
,然后根据 nums[i] // (t + 1)
,从而决定将 nums[i]
放入哪个桶中。t
。而相邻桶之间的元素,只需要校验一下两个桶之间的差值是否不超过 t
。这样就可以以 O ( 1 ) O(1) O(1) 的时间复杂度检测相邻 2 * k
个元素是否满足 abs(nums[i] - nums[j]) <= t
。abs(i - j) <= k
条件则可以通过在一重循环遍历时,将超出范围的 nums[i - k]
从对应桶中删除,从而保证桶中元素一定满足 abs(i - j) <= k
。具体步骤如下:
t + 1
。我们将元素按照大小依次放入不同的桶中。nums
中的元素,对于元素 nums[i]
:
nums[i]
放入桶之前桶里已经有元素了,那么这两个元素必然满足 abs(nums[i] - nums[j]) <= t
,nums[i]
放入对应桶中。abs(nums[i] - nums[j]) <= t
。nums[i - k]
之前的桶清空,因为这些桶中的元素与 nums[i]
已经不满足 abs(i - j) <= k
了。True
,最终遍历完仍不满足条件就返回 False
。 class Solution:
def containsNearbyAlmostDuplicate(self, nums: List[int], k: int, t: int) -> bool:
bucket_dict = dict()
for i in range(len(nums)):
# 将 nums[i] 划分到大小为 t + 1 的不同桶中
num = nums[i] // (t + 1)
# 桶中已经有元素了
if num in bucket_dict:
return True
# 把 nums[i] 放入桶中
bucket_dict[num] = nums[i]
# 判断左侧桶是否满足条件
if (num - 1) in bucket_dict and abs(bucket_dict[num - 1] - nums[i]) <= t:
return True
# 判断右侧桶是否满足条件
if (num + 1) in bucket_dict and abs(bucket_dict[num + 1] - nums[i]) <= t:
return True
# 将 i-k 之前的旧桶清除,因为之前的桶已经不满足条件了
if i >= k:
bucket_dict.pop(nums[i-k] // (t + 1))
return False
136 只出现一次的数字
给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
如果没有时间复杂度和空间复杂度的限制,可以使用哈希表 / 集合来存储每个元素出现的次数,如果哈希表中没有该数字,则将该数字加入集合,如果集合中有了该数字,则从集合中删除该数字,最终成对的数字都被删除了,只剩下单次出现的元素。
但是题目要求不使用额外的存储空间,就需要用到位运算中的异或运算。
异或运算 ⊕ \oplus ⊕ 的三个性质:
- 任何数和 0 0 0 做异或运算,结果仍然是原来的数,即 a ⊕ 0 = a a \oplus 0 = a a⊕0=a。
- 数和其自身做异或运算,结果是 0 0 0,即 a ⊕ a = 0 a \oplus a = 0 a⊕a=0。
- 异或运算满足交换率和结合律: a ⊕ b ⊕ a = b ⊕ a ⊕ a = b ⊕ ( a ⊕ a ) = b ⊕ 0 = b a \oplus b \oplus a = b \oplus a \oplus a = b \oplus (a \oplus a) = b \oplus 0 = b a⊕b⊕a=b⊕a⊕a=b⊕(a⊕a)=b⊕0=b。
根据异或运算的性质,对 n n n 个数不断进行异或操作,最终可得到单次出现的元素。
class Solution(object):
def singleNumber(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
sum=0
for i in nums:
sum^=i
return sum
0036. 有效的数独
请你判断一个 9 x 9 的数独是否有效。
输入:board =
[["5","3",".",".","7",".",".",".","."]
,["6",".",".","1","9","5",".",".","."]
,[".","9","8",".",".",".",".","6","."]
,["8",".",".",".","6",".",".",".","3"]
,["4",".",".","8",".","3",".",".","1"]
,["7",".",".",".","2",".",".",".","6"]
,[".","6",".",".",".",".","2","8","."]
,[".",".",".","4","1","9",".",".","5"]
,[".",".",".",".","8",".",".","7","9"]]
输出:true
思路一:哈希表
判断数独是否有效,需要分别看每一行、每一列、每一个 3 * 3
的小方格是否出现了重复数字,如果都没有出现重复数字就是一个有效的数独,如果出现了重复数字则不是有效的数独。
3
个 9 * 9
的数组分别来表示该数字是否在所在的行,所在的列,所在的方格出现过。其中方格角标的计算用 box[(i / 3) * 3 + (j / 3)][n]
来表示。False
。Ture
。
- 九宫格1-3坐标:(0-2,0-2),(0-2,3-5),(0-2,6-8)
- 九宫格4-6坐标:(3-5,0-2),(3-5,3-5),(3-5,6-8)
- 九宫格7-9坐标:(6-8,0-2),(6-8,3-5),(6-8,6-8)
- 所以每个元素属于第几个九宫格,是
i//3
的三倍+j//3
class Solution(object):
def isValidSudoku(self, board):
"""
:type board: List[List[str]]
:rtype: bool
"""
row=[dict() for _ in range(9)] # 创建一个包含9个字典的列表,其中每个字典统计每一行元素出现的次数
col=[dict() for _ in range(9)] # 统计每一列元素出现的次数
box=[dict() for _ in range(9)] # 统计每个九宫格元素出现的次数
for i in range(9):
for j in range(9):
if board[i][j]=='.':
continue
num=int(board[i][j])
idx=(i//3)*3+j//3
# 查找各行、各列、各九宫格中,是否出现过num这个元素。没有出现就返回0
a,b,c=row[i].get(num,0),col[j].get(num,0),box[idx].get(num,0)
if a!=0 or b!=0 or c!=0:
return False
# 在各行、列、九宫格中,将出现的数字次数计为1
row[i][num]=1
col[j][num]=1
box[idx][num]=1
return True
思路二:列表查找
class Solution(object):
def isValidSudoku(self, board):
"""
:type board: List[List[str]]
:rtype: bool
"""
for i in range(9): #对每一行进行判断
row = []
for j in range(9):
if board[i][j] == '.':
continue
if board[i][j] in row:
return False
else:
row.append(board[i][j])
for i in range(9): #对每一列进行判断
col = []
for j in range(9):
if board[j][i] == '.':
continue
if board[j][i] in col:
return False
else:
col.append(board[j][i])
for i in range(0,9,3): # 对每个九宫格进行判断
for j in range(0,9,3):
box=[]
for x in range(3):
for y in range(3):
if board[i+x][j+y]=='.' :
continue
if board[i+x][j+y] in box:
return False
else:
box.append(board[i+x][j+y])
return True
- 0001. 两数之和
- 0015. 三数之和
- 0018. 四数之和
- 0454. 四数相加 II
给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为 target
的那 两个 整数,并返回它们的数组下标(任意顺序)。
class Solution(object):
def twoSum(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: List[int]
"""
d=dict()
for i in range(len(nums)):
if target-nums[i] in d:
return i,d[target-nums[i]]
else:
d[nums[i]]=i
return False
思路 1:对撞指针
直接三重遍历查找 a
、b
、c
的时间复杂度是: O ( n 3 ) O(n^3) O(n3)。我们可以通过一些操作来降低复杂度。
先将数组进行排序,再使用对撞指针,从而保证所找到的三个元素是不重复的,且使用双指针减少一重遍历。时间复杂度为: O ( n l o g n ) O(nlogn) O(nlogn)。
第一重循环遍历 i
,对于每个 nums[i]
元素,从 i
元素的下一个位置开始,使用对撞指针 left
,right
。left
指向 i
的下一个位置,right
指向末尾位置。先将 left
右移、right
左移去除重复元素,再进行下边的判断。
nums[a] + nums[left] + nums[right] = 0
,则得到一个解,将其加入答案数组中,并继续将 left
右移,right
左移;nums[a] + nums[left] + nums[right] > 0
,说明 nums[right]
值太大,将 right
向左移;nums[a] + nums[left] + nums[right] < 0
,说明 nums[left]
值太小,将 left
右移。class Solution(object):
def threeSum(self, nums):
"""
:type nums: List[int]
:rtype: List[List[int]]
"""
nums.sort()
ans=[]
le=len(nums)
for i in range(le):
if nums[i]>0:
break
if i>0 and nums[i]==nums[i-1]: # 如果下一位置和前一位置元素重复,就不需要再遍历
continue
left,right=i+1,le-1#左指针起点i+1
while left<right:#判断条件
if nums[i]+nums[left]+nums[right]>0:
right-=1
elif nums[i]+nums[left]+nums[right]<0:
left+=1
else:
ans.append([nums[i],nums[left],nums[right]])
# 找到解之后,如果left或right下一个元素重复,则需要跳过重复位置
while left!=right and nums[left]==nums[left+1]:left+=1
while left!=right and nums[right]==nums[right-1]:right-=1
# 找到解后也要移动指针
left+=1
right-=1
return ans
给你一个由 n
个整数组成的数组 nums
,和一个目标值 target
。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]]
(若两个四元组元素一一对应,则认为两个四元组重复):
输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
基本上就是套用三数之和的思路,也使用排序数组和对撞指针,去除一次循环遍历。
class Solution(object):
def fourSum(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: List[List[int]]
"""
nums.sort()
le=len(nums)
ans=[]
for a in range(le):
#if nums[a]>target:
# break
# 这里和三数之和不一样。三数之和nums[i]>0,所有都是正数不用计算了。这里第一个数>target,还可以有负数使和变得更小
if a>0 and nums[a]==nums[a-1]:
continue
for b in range(a+1,le):
if b>a+1 and nums[b]==nums[b-1]:
continue
left,right=b+1,le-1
while left<right:
if nums[a]+nums[b]+nums[left]+nums[right]>target:
right-=1
elif nums[a]+nums[b]+nums[left]+nums[right]<target:
left+=1
else:
ans.append([nums[a],nums[b],nums[left],nums[right]])
while left<right and nums[left]==nums[left+1]:left+=1
while left<right and nums[right]==nums[right-1]:right-=1
left+=1
right-=1
return ans
直接暴力搜索的时间复杂度是 O ( n 4 ) O(n^4) O(n4)。我们可以降低一下复杂度。将四个数组分为两组。nums1 和 nums2 分为一组,nums3 和 nums4 分为一组。
已知 n u m s 1 [ i ] + n u m s 2 [ j ] + n u m s 3 [ k ] + n u m s 4 [ l ] = 0 nums1[i] + nums2[j] + nums3[k] + nums4[l] = 0 nums1[i]+nums2[j]+nums3[k]+nums4[l]=0,可以得到 n u m s 1 [ i ] + n u m s 2 [ j ] = − ( n u m s 3 [ k ] + n u m s 4 [ l ] ) nums1[i] + nums2[j] = -(nums3[k] + nums4[l]) nums1[i]+nums2[j]=−(nums3[k]+nums4[l])
建立一个哈希表。两重循环遍历数组 nums1
、nums2
,先将 n u m s [ i ] + n u m s [ j ] nums[i] + nums[j] nums[i]+nums[j] 的和记录到哈希表中,然后再用两重循环遍历数组 nums3
、nums4
。如果 − ( n u m s 3 [ k ] + n u m s 4 [ l ] ) -(nums3[k] + nums4[l]) −(nums3[k]+nums4[l]) 的结果出现在哈希表中,则将结果数累加到答案中。最终输出累加之后的答案。
class Solution(object):
def fourSumCount(self, nums1, nums2, nums3, nums4):
d=dict()
count=0
for i in nums1:
for j in nums2:
sum=i+j
d[sum]=d.get(sum,0)+1
for k in nums3:
for l in nums4:
if -k-l in d:
count+=d[-k-l]
return count
0041. 缺失的第一个正数
给你一个未排序的整数数组 nums
,请你找出其中没有出现的最小的正整数。请你实现时间复杂度为 O(n)
并且只使用常数级别额外空间的解决方案。
输入:nums = [1,2,0]
输出:3
如果本题没有额外的时空复杂度要求,那么就很容易实现:
我们可以将数组所有的数放入哈希表,随后从 1 开始依次枚举正整数,并判断其是否在哈希表中。这种解法时空复杂度都是O(n)
;
我们可以从 1 开始依次枚举正整数,并遍历数组,判断其是否在数组中。这种解法时间复杂度 O ( n 2 ) O(n^2) O(n2),空间复杂度O(1)
所以可以考虑利用给定数组中的空间来存储一些状态。即:如果题目给定的数组是不可修改的,那么就不存在满足时空复杂度要求的算法;但如果我们可以修改给定的数组,那么是存在满足要求的算法的。
思路 1:原地哈希
我们可以将当前数组视为哈希表。一个长度为 n
的数组,对应存储的元素值应该为 [1, n + 1]
之间,其中还包含一个缺失的元素。
遍历当前数组,将当前元素放到其对应位置上(比如元素值为 1
的元素放到数组第 0
个位置上、元素值为 2
的元素放到数组第 1
个位置上,等等)。
然后再次遍历一遍数组。遇到第一个元素值不等于下标 + 1 的元素,就是答案要求的缺失的第一个正数。
如果遍历完没有在数组中找到缺失的第一个正数,则缺失的第一个正数是 n + 1
。
class Solution:
def firstMissingPositive(self, nums: List[int]) -> int:
size = len(nums)
for i in range(size):
# 对于1到size范围内的数,才进行交换,放到正确位置
while 1 <= nums[i] <= size and nums[i] != nums[nums[i] - 1]:
index1 = i
index2 = nums[i] - 1
nums[index1], nums[index2] = nums[index2], nums[index1]
for i in range(size):
if nums[i] != i + 1:
return i + 1
return size + 1
128 最长连续序列
思路一:排序+滑动窗口,时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)
class Solution(object):
def longestConsecutive(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
if len(nums)==0:
return 0
else:
nums=list(set(nums)) # 去除重复元素
nums.sort()
left,right=0,1
ans=1 # 默认起始的连续序列长度为1(只有一个元素时)
# 如果差值不为1,左右指针右移;否则差值为1,右指针不停右移,同时记录连续序列的长度
while left<right and right<len(nums):
if nums[right]!=nums[left]+1:
left+=1
right+=1
else:
while right<len(nums) and nums[right]==nums[right-1]+1:
ans=max(ans,right-left+1)
right+=1
# 右移到差值不为1时右指针停止移动,left跳到right前一个位置
left=right-1
return ans
思路二:哈希表
curr_streak
维护当前连续序列长度,使用 ans
维护最长连续序列长度。num - 1
在集合中),则跳过。num - 1
不在集合中,说明 num
是序列的开始,判断 num + 1
、nums + 2
、...
是否在哈希表中,并不断更新当前连续序列长度 curr_streak
。并在遍历结束之后更新最长序列的长度。class Solution(object):
def longestConsecutive(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
ans=0 # 记录最长序列长度
nums=set(nums) # 去重
for num in nums:
if num-1 not in nums: # 比当前元素小1的不在集合中,则连续序列需要从头开始计算
cur=num
res=1 # 初始化连续序列长度=1
while cur+1 in nums:
cur+=1
res+=1
ans=max(ans,res)
return ans
451 根据字符出现频率排序
给定一个字符串 s ,根据字符出现的 频率 对其进行 降序排序 。一个字符出现的 频率 是它出现在字符串中的次数。返回 已排序的字符串 。如果有多个答案,返回其中任何一个。
输入: s = "tree"
输出: "eert"
解释: 'e'出现两次,'r'和't'都只出现一次。
因此'e'必须出现在'r'和't'之前。此外,"eetr"也是一个有效的答案。
class Solution(object):
def frequencySort(self, s):
"""
:type s: str
:rtype: str
"""
from collections import Counter
d=dict(Counter(s)) # 统计每个字母出现的次数
# 根据次数对字母进行降序排列,结果是一个列表
d=sorted(d.items(),key=lambda x:x[1],reverse=True)
ans=''
for k,v in d:
ans+=k*v
return ans
49 字母异位词分组
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。字母异位词 是由重新排列源单词的字母得到的一个新单词,所有源单词中的字母通常恰好只用一次。
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
解题思路:
对每一个字符串先进行排序,然后使用哈希表记录字母相同的子字符串。(排序后结果一样的子串,存储在一个列表里)
最终将哈希表的值转换为对应数组返回结果。
class Solution(object):
def groupAnagrams(self, strs):
"""
:type strs: List[str]
:rtype: List[List[str]]
"""
d=dict()
for ch in strs:
# 子串进行字母排序,sorted的结果是一个列表,str将其转为字符串
sort_ch=str(sorted(ch))
if sort_ch not in d:
d[sort_ch]=[ch]
else:
d[sort_ch]+=[ch]
ans=[]
for k,v in d.items():
ans+=[v]
return ans
599 两个列表的最小索引总和
假设 Andy 和 Doris 想在晚餐时选择一家餐厅,并且他们都有一个表示最喜爱餐厅的列表,每个餐厅的名字用字符串表示。
你需要帮助他们用最少的索引和找出他们共同喜爱的餐厅。 如果答案不止一个,则输出所有答案并且不考虑顺序。 你可以假设答案总是存在。
输入:list1 = ["Shogun", "Tapioca Express", "Burger King", "KFC"],list2 = ["KFC", "Shogun", "Burger King"]
输出: ["Shogun"]
解释: 他们共同喜爱且具有最小索引和的餐厅是“Shogun”,它有最小的索引和1(0+1)。
思路1
遍历 list1
,建立一个哈希表d1
,以 list1[i] : i
键值对的方式,将 list1
的下标存储起来。
遍历 list2
,判断 list2[i]
是否在哈希表中,如果在,则根据 i + list1_dict[i]
和 min_sum
的比较,判断是否需要更新最小索引和。
i + list1_dict[i] < min_sum
,则更新最小索引和,并清空答案数据,添加新的答案;i + list1_dict[i] == min_sum
,则更新最小索引和,并添加答案。class Solution:
def findRestaurant(self, list1: List[str], list2: List[str]) -> List[str]:
d1 = dict()
len1 = len(list1)
len2 = len(list2)
for i in range(len1):
d1[list1[i]] = i
min_sum = len1 + len2
res = []
for i in range(len2):
if list2[i] in d1:
sum = i + d1[list2[i]]
if sum < min_sum:
res = [list2[i]]
min_sum = sum
elif sum == min_sum:
res.append(list2[i])
return res
思路2:
list1
,建立一个哈希表d1
,以 list1[i] : i
键值对的方式,将 list1
的下标存储起来。list2
,如果list2[j]
在哈希表中,将这个字符和其在两个列表中的下标和分别存储在列表ls
和res
中res
,当遍历到最小下标和时,将ls对应位置的字符取出,存到最终结果中class Solution(object):
def findRestaurant(self, list1, list2):
"""
:type list1: List[str]
:type list2: List[str]
:rtype: List[str]
"""
d1=dict()
ls=[] # 存储相同字符
res=[] # 存储相同字符的下标和
ans=[] # 存储最终答案
for i in range(len(list1)):
d1[list1[i]]=i
for j in range(len(list2)):
if list2[j] in d1:
idx=d1[list2[j]]+j # 相同字符的下标和
ls.append(list2[j]) # 同步存储这个字符
res.append(idx)
min_idx=min(res) # 最小下标和
for i in range(len(res)):
if res[i]==min_idx:
ans.append(ls[i])
return ans
387 字符串中的第一个唯一字符
给定一个字符串 s ,找到 它的第一个不重复的字符,并返回它的索引 。如果不存在,则返回 -1 。
输入: s = "leetcode"
输出: 0
class Solution(object):
def firstUniqChar(self, s):
"""
:type s: str
:rtype: int
"""
d=dict()
for ch in s:
d[ch]=d.get(ch,0)+1 # 统计每个字符出现的次数
for i in range(len(s)):
if d[s[i]]==1:
return i
return -1
149 直线上最多的点数
给你一个数组 points ,其中 points[i] = [xi, yi] 表示 X-Y 平面上的一个点。求最多有多少个点在同一条直线上。
输入:points = [[1,1],[3,2],[5,3],[4,1],[2,3],[1,4]]
输出:4
两个点可以确定一条直线,固定其中一个点,求其他点与该点的斜率,斜率相同的点则在同一条直线上。可以考虑把斜率当做哈希表的键值,存储经过该点,不同斜率的直线上经过的点数目。
对于点 i
,查找经过该点的直线只需要考虑 (i+1,n-1)
位置上的点即可,因为 i-1
之前的点已经在遍历点 i-2
的时候考虑过了。
斜率的计算公式为 d y d x = y j − y i x j − x i \frac{dy}{dx} = \frac{y_j - y_i}{x_j - x_i} dxdy=xj−xiyj−yi。因为斜率是小数会有精度误差,所以我们考虑使用 (dx, dy)
的元组作为哈希表的 key
。
注意: 需要处理倍数关系,dy、dx 异号情况,以及处理垂直直线(两点横坐标差为 0)的水平直线(两点横坐标差为 0)的情况。
class Solution:
def maxPoints(self, points: List[List[int]]) -> int:
n = len(points)
if n < 3:
return n
ans = 0
for i in range(n):
line_dict = dict()
line_dict[0] = 0
same = 1
for j in range(i+1, n):
dx = points[j][0] - points[i][0]
dy = points[j][1] - points[i][1]
if dx == 0 and dy == 0:
same += 1
continue
gcd_dx_dy = math.gcd(abs(dx), abs(dy))
if (dx > 0 and dy > 0) or (dx < 0 and dy < 0):
dx = abs(dx) // gcd_dx_dy
dy = abs(dy) // gcd_dx_dy
elif dx < 0 and dy > 0:
dx = -dx // gcd_dx_dy
dy = -dy // gcd_dx_dy
elif dx > 0 and dy < 0:
dx = dx // gcd_dx_dy
dy = dy // gcd_dx_dy
elif dx == 0 and dy != 0:
dy = 1
elif dx != 0 and dy == 0:
dx = 1
key = (dx, dy)
if key in line_dict:
line_dict[key] += 1
else:
line_dict[key] = 1
ans = max(ans, same + max(line_dict.values()))
return ans
811 子域名访问计数
网站域名 “discuss.leetcode.com” 由多个子域名组成。顶级域名为 “com” ,二级域名为 “leetcode.com” ,最低一级为 “discuss.leetcode.com” 。当访问域名 “discuss.leetcode.com” 时,同时也会隐式访问其父域名 “leetcode.com” 以及 “com” 。
计数配对域名 是遵循 “rep d1.d2.d3” 或 “rep d1.d2” 格式的一个域名表示,其中 rep 表示访问域名的次数,d1.d2.d3 为域名本身。例如,“9001 discuss.leetcode.com” 就是一个 计数配对域名 ,表示 discuss.leetcode.com 被访问了 9001 次。
给你一个 计数配对域名 组成的数组 cpdomains ,解析得到输入中每个子域名对应的 计数配对域名 ,并以数组形式返回。可以按 任意顺序 返回答案。
输入:cpdomains = ["900 google.mail.com", "50 yahoo.com", "1 intel.mail.com", "5 wiki.org"]
输出:["901 mail.com","50 yahoo.com","900 google.mail.com","5 wiki.org","5 org","1 intel.mail.com","951 com"]
解释:按照前文描述,会访问 "google.mail.com" 900 次,"yahoo.com" 50 次,"intel.mail.com" 1 次,"wiki.org" 5 次。
而对于父域名,会访问 "mail.com" 900 + 1 = 901 次,"com" 900 + 50 + 1 = 951 次,和 "org" 5 次。
解题思路
这道题求解的是不同层级的域名的次数汇总,很容易想到使用哈希表。我们可以使用哈希表来统计不同层级的域名访问次数。具体做如下:
cpdomains
为空,直接返回空数组。times_dict
存储不同层级的域名访问次数。cpdomains
。对于每一个计算机配对域名 cpdomain
:
times
和域名 domain
进行分割。domain_list
,逆序拼接不同等级的子域名 sub_domain
。sub_domain
没有出现在哈希表 times_dict
中,则在哈希表中存入 sub_domain
和访问次数 times
的键值对。sub_domain
曾经出现在哈希表 times_dict
中,则在哈希表对应位置加上 times
。times_dict
,将所有域名和访问次数拼接为字符串,存入答案数组中。class Solution:
def subdomainVisits(self, cpdomains: List[str]) -> List[str]:
if not cpdomains:
return []
times_dict = dict()
for cpdomain in cpdomains:
tiems, domain = cpdomain.split()
tiems = int(tiems)
domain_list = domain.split('.')
for i in range(len(domain_list) - 1, -1, -1):
sub_domain = '.'.join(domain_list[i:])
if sub_domain not in times_dict:
times_dict[sub_domain] = tiems
else:
times_dict[sub_domain] += tiems
res = []
for key in times_dict.keys():
res.append(str(times_dict[key]) + ' ' + key)
return res