数据结构:哈希表
哈希表基础知识
概念
哈希表,又叫散列表Hash tabel,是一种根据关键码(Key)的值直接进行访问的数据结构。它通过将关键码映射到表中的位置来快速定位、访问和操作数据,从而实现高效的查找、插入和删除操作。
在哈希表中,关键码通常用来确定元素存储在表中的位置,类似于数组的索引下标。哈希表的设计目标是通过合适的哈希函数将关键码映射到不同的位置,避免冲突,提高查找效率。
在一定程度上,数组可以被视为最简单的哈希表,其中关键码即为数组索引。通过数组索引,可以直接访问数组中的元素,实现了基本的哈希表功能。
哈希表的主要功能是快速查找、插入和删除元素,常用于需要频繁进行数据检索的场景。与暴力枚举方法(时间复杂度 O(n))相比,哈希表操作的时间复杂度通常为 O(1),实现了更高效的数据处理。(哈希法可以解决的问题,暴力求解一般也可以解决,所以如果遇到可以暴力求解的问题时,考虑是否可以用哈希法优化)
如果遇到“快速判断一个元素是否在集合中”此类问题时,考虑使用哈希表求解;
哈希函数实现
哈希表的核心在于哈希函数,它将不同的关键码映射到不同的哈希表位置。好的哈希函数应该具有均匀性,能够最大限度地避免冲突,使得元素均匀分布在哈希表中。
哈希表中建立了关键码与值之间的映射关系,通过哈希函数计算出的哈希值来确定元素在表中的位置,实现了快速查找。
常见哈希函数
- 直接地址法(Division Method):取关键码 k 的某个函数值 f(k) 或者直接取 k 的某个线性函数值 f(k) mod p (p 一般为素数) 作为关键码 k 的哈希地址。
- 除留余数法(Division Hashing):哈希函数的计算公式为 h(k) = k mod p,其中 p 通常设为一个素数。
- 平方取中法:取关键字的平方值的中间几位作为哈希地址;
- 折叠法(Folding Method):将关键字分割成位数相等的几部分,然后取这几部分的叠加和。
- 位运算法:结合位运算(如异或、与、或等)将关键码转换为哈希地址。
- 字符串哈希函数:对字符串中的每个字符进行处理,例如取 ASCII 码值的和、乘积或异或结果来生成哈希码。
- MD5、SHA等加密哈希函数:通常用于数据加密,生成唯一且分布均匀的哈希码。
哈希函数首先要保证关键字可以映射到哈希表中;如果哈希函数得到的结果已经被其他关键字占据了,则发生哈希碰撞(哈希冲突),需要解决哈希冲突;
解决哈希冲突方法
拉链法(Chaining):
- 原理:在哈希表的每个槽中放置一个链表或其他数据结构,将哈希冲突的元素存储在同一槽位对应的链表中。
- 特点:当出现冲突时,不同键值的元素会以链表形式存储在同一槽位上。
- 优点:实现简单,适用于处理较多冲突的情况。
- 缺点:如果链表过长,会导致查找效率降低,需要在实际应用中提前估计哈希表的负载因子,以动态调整表格大小或调整哈希函数来降低冲突发生率。
拉链法需要权衡数组大小(哈希表槽的多少),如果数组小,则可能导致槽的链表过长,导致查询效率低下;如果数组大,则空间消耗大,需要连续的内存空间;
线性探测法(Linear Probing):
- 原理:在发生哈希冲突时,依次检查下一个空槽位,直到找到空位置为止,将冲突元素存放在第一个空位。
- 特点:使用哈希表中的空位来解决冲突,将数据存储在紧挨着原槽位的下一个空槽位上。
- 优点:简单高效,节省内存空间,适用于哈希表载荷因子较小的情况。
- 缺点:容易产生聚集效应,当冲突较多时,可能导致性能下降(线性探测法的二次探测、双重散列等方法可以缓解这一问题)。
必须在一开始就申请好足够大的空间,以保证哈希表中有足够多的空位;
C++中的哈希结构
C++中常见的3种哈希结构
数组:数组可以被认为是最简单的哈希结构,也可以理解为一组桶,其中每个桶对应一个索引位置。在关键字为有限范围的数组或者字母时很有效;内存消耗最小;
set集合:集合
map映射:键值对key-value
数组:
- 描述:数组可以被认为是最简单的哈希结构,也可以理解为一组桶,其中每个桶对应一个索引位置。
- 优势:适用于关键字取值范围有限的情况,例如存储不同字母的出现频次、值范围有限的数字出现次数等;内存消耗相对较小。
- 应用场景:适用于快速索引和查找,特别在知道范围的情况下效果显著。
Set集合:
- 描述:Set 是一种集合容器,其中每个元素都是独一无二的,没有重复元素。
- 特点:基于哈希表实现,在插入和查找操作上具有较好的性能。
- 优势:能够快速判断元素是否存在,适用于需要维护唯一值集合的情况。
- 应用场景:常用于去重,判重等操作,通常查找效率较高。
Map映射:
- 描述:Map 是一种键值对容器,提供了通过键快速查找对应值的功能。
- 特点:Map 也是基于哈希表实现的,每个元素都包含一个键和一个值。
- 优势:能够快速根据键查找对应的值,适用于需要存储键值对信息的场景。
- 应用场景:常用于存储和快速查找键值对数据,如存储用户名和其对应的权限等信息。
理论上Map可以实现一切的哈希表,而数组可以实现的哈希表Set也可以实现,但是他们的空间消耗差异很大,所以在使用时,确保选择空间效率最高的可行实现方式;
哈希表的本质就是空间换取时间,所以在考虑选择哪一种实现哈希表时,着重考虑空间效率;
C++具体实现接口
set集合
C++中三种实现集合的方式,使用案例;
集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否修改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::set | 红黑树 | 有序 | 否 | 否 | O(logn) | O(logn) |
std::multiset | 红黑树 | 有序 | 是 | 否 | O(logn) | O(logn) |
std::unordered_set | 哈希表 | 无序 | 否 | 否 | O(1) | O(1) |
std::set
:(#include <set>
)
insert(const value_type& value)
:向集合中插入元素value
。erase(const key_type& key)
:从集合中删除指定值为key
的元素,如果存在的话。find(const key_type& key) -> const_iterator
:查找集合中是否存在值为key
的元素,返回指向找到元素的迭代器,如果未找到则返回end()
。size() -> size_type
:返回集合中元素的数量。empty() -> bool
:检查集合是否为空,返回布尔值。clear()
:清空集合中的所有元素。count(const key_type& key) -> size_type
:统计集合中值为key
的元素的个数,返回 0 或 1。lower_bound(const key_type& key) -> const_iterator
:返回指向第一个不小于key
的元素的迭代器。upper_bound(const key_type& key) -> const_iterator
:返回指向第一个大于key
的元素的迭代器。equal_range(const key_type& key) -> std::pair<const_iterator, const_iterator>
:返回指向与key
匹配的元素范围的两个迭代器。
返回迭代器是许多 C++ 标准库容器及算法函数常见的设计选择,这是因为迭代器具有以下几个优点:
- 通用性:迭代器是一个通用的抽象概念,可以访问容器中的元素,而不依赖于具体的容器实现。这种通用性使得算法和容器之间的接口更加灵活,从而提高代码的复用性和通用性。
- 泛型编程:返回迭代器符合 C++ 泛型编程的理念。泛型编程强调以一种独立于数据类型的方式来编写代码,迭代器正是实现这个理念的重要工具之一。
- 轻量级:迭代器通常是相对轻量级的对象,返回迭代器可以有效地避免传递和复制大型集合或容器的开销。相比直接返回集合的秩(比如返回一个元素的引用),返回迭代器可能更为高效。
- 统一接口:使用迭代器返回集合元素的方式为容器提供了统一的访问接口,这样不同的容器就可以使用相同的方式来访问元素,加强了代码的一致性和可读性。
- 迭代器的更多功能:迭代器本身提供了丰富的功能和操作,例如解引用、前进、后退等,这些操作可以直接在返回的迭代器上进行,方便对集合中元素进行操作。
尽管返回迭代器有其优点,但有时也可能会选择返回集合的秩(如返回元素的引用),这取决于具体的情况和需求。在 C++ 标准库中,迭代器被广泛应用于容器和算法之间的交互,因此返回迭代器通常被视为一种良好的设计选择。但在某些特定的场景下,直接返回集合的秩可能也是合适的,特别是对于小型的容器或需要频繁访问元素的情况。
std::multiset
:(#include <set>
)
基本API和std::set
类似;注意set
内部元素不能重复(即元素是唯一的),但是multiset
内部元素可以重复,插入重复的元素时,重复元素会按照插入顺序保留;
multiset.insert(i)
并不会因为i
在集合中已经存在而拒绝插入;同时,multiset.find(i)
会返回第一个插入的i
的迭代器,如果要找所有值为i
的元素在集合中的位置,需要结合使用lower_bound()
或者upper_bound()
函数(也可以直接使用equal_range()
函数);
std::unordered_set
:(#include <unordered_set>
)
std::unordered_set
是 C++ 标准库提供的无序集合容器,内部使用哈希表实现,具有快速的查找、插入和删除操作。与有序集合 std::set
不同,std::unordered_set
中的元素不会根据某种顺序自动排序,而是根据元素的哈希值进行存储和检索。
使用 std::unordered_set
可以在大多数情况下以常数时间复杂度(O(1))进行插入、删除和查找操作,适用于需要高效查找和唯一性的场景。请注意元素的哈希值计算和冲突解决可能会影响性能,选择合适的哈希函数和解决冲突的策略很重要。
自定义集合类:
需要实现的功能API:insert插入值到集合中,find根据值在集合中查找,display打印集合中的所有元素:
#include <iostream>
#include <vector>
template <class T>
class MySet {
private:
std::vector<T> elements;
public:
void insert(const T& element) { // 集合中数据不能重复,所以插入之前必须查看是否重复;
if (find(element) == elements.end()) {
elements.push_back(element);
}
}
// 返回迭代器;
typename std::vector<T>::iterator find(const T& element) {
return std::find(elements.begin(), elements.end(), element);
}
void display() {
for (const auto& elem : elements) {
std::cout << elem << " ";
}
std::cout << std::endl;
}
};
int main() {
MySet<int> myCustomSet;
// 添加元素到自定义集合
myCustomSet.insert(10);
myCustomSet.insert(20);
myCustomSet.insert(10); // 重复元素不会被插入
// 显示自定义集合中的元素
myCustomSet.display();
return 0;
}
map映射
集合 | 底层实现 | 是否有序 | key是否可以重复 | key能否修改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::map | 红黑树 | key有序 | 否 | 否 | O(logn) | O(logn) |
std::multimao | 红黑树 | key有序 | 是 | 否 | O(logn) | O(logn) |
std::unordered_map | 哈希表 | key无序 | 否 | 否 | O(1) | O(1) |
关于键值对:键值对(Key-Value Pair)是指包含两个部分的数据结构,一个部分称为键(Key),用于唯一标识这个数据结构,另一个部分称为值(Value),存储与该键相关联的数据。在 C++ 中,键值对可以通过 std::pair
或 std::make_pair
来构造和表示。
std::pair
和 std::make_pair
:
std::pair
:std::pair
是一个模板类,用于存储两个不同类型的数据,其中第一个数据是键(Key),第二个数据是值(Value)。std::make_pair
:std::make_pair
是一个辅助函数模板,用于创建并返回一个std::pair
对象,可以方便地构造键值对,避免显式指定模板参数类型。
构造示例:
#include <utility> // 使用make_pair、pair需要引入的头文件;
// 创建并初始化一个键值对
std::pair<int, std::string> myPair(1, "apple");
// 访问键值对的键和值
int key = myPair.first;
std::string value = myPair.second;
// 使用 std::make_pair 创建键值对(使用自动类型auto)
auto myPair = std::make_pair(2, "banana");
// 访问键值对的键和值
int key = myPair.first;
std::string value = myPair.second;
注意访问键值对的方法,用.first
访问key,用.second
访问value;
下面列出了 std::map
、std::multimap
和 std::unordered_map
这三个关联容器共同常用的一些 API,包括插入元素、查找键等操作,以及适用的数据类型:
插入键值对 | insert(const value_type& value) |
---|---|
查找键 | find(const key_type& key) -> iterator |
删除元素 | erase(const key_type& key) -> size_type |
获取元素数量 | size() -> size_type |
检查是否为空 | empty() -> bool |
清空容器 | clear() |
#include <map>
#include <unordered_map>
std::map<int, int> myMap;
std::multimap<int, int> myMultiMap;
std::unordered_map<int, int> myUnorderedMap;
底层原理分析
红黑树
std::set
,std::multiset
,std::map
,std::multimap
的底层原理都是红黑树,查找效率和删除效率都是O(logn)
级别;
红黑树(Red-Black Tree)是一种自平衡的二叉搜索树,具有以下特性:
- 节点颜色:每个节点要么是红色,要么是黑色。
- 根节点是黑色:根节点始终为黑色。
- 红色节点的子节点是黑色:红色节点不能连续,即红色节点的子节点必须是黑色。
- 从任一节点到其每个叶子节点的路径上,黑色节点数量相同:确保了每条路径上的黑色节点数目相等,保证了红黑树的平衡。
- 叶子节点是黑色:叶子节点被视为NIL节点,并且被认为是黑色的。
红黑树的底层原理:
- 插入操作:在插入新节点时,首先按照二叉搜索树的规则插入节点,然后根据红黑树的特性进行调整,包括变色和旋转操作,以维持红黑树的平衡性。
- 删除操作:删除节点后,也需要通过旋转和重新着色的操作来维护红黑树的平衡。
- 旋转操作:红黑树的旋转操作包括左旋和右旋,在插入或删除节点时使用旋转来保持树的平衡。
- 复杂度:红黑树的高度始终保持在对数范围内(最长路径不超过最短路径的两倍),因此查找、插入和删除的时间复杂度均为
O(log n)
。
红黑树就是有颜色的平衡二叉树,在插入删除时,首先保持是平衡二叉树,所以要左旋和右旋,其次保持颜色有一定的规则,所以可能要重新着色;
红黑树的平衡性:由于红黑树是一个平衡二叉树,所以树的高度小于等于logn
,所以树的插入和删除操作都是O(logn)
级别;具体表现就是使用红黑树作为底层原理的set和map的查询效率和删除效率都是O(logn)
;
红黑树的有序性:红黑树是一个平衡有序二叉树,所以节点按照Key的大小有序排序;所以红黑树实现的set和map都是有序;
哈希表
std::unordered_set
和std::unordered_map
的底层原理都是哈希表,查找效率和删除效率都是O(1)
级别;
对比效率
底层原理 | 红黑树 | 哈希表 |
---|---|---|
有序/无序 | 有序性 | 无序性 |
查找删除效率 | O(logn) | O(1) |
速度 | 始终保持稳定的速度 | 部分产生哈希碰撞速度变慢,但是平均情况更快 |
使用场景 | 对顺序有要求,稳定性高,适合动态数据 | 对查找性能有要求,大规模数据集合 |
C++中几种哈希表(题目)
题目 | 使用方法 | 选择方法原因 | 思路简述 |
---|---|---|---|
有效的字母异位词 | 数组实现哈希表 | 字母数量有限、桶原理 | 查找某些字符是否在另一个字符串里出现,使用哈希表; |
两个数组的交集 | unordered_set实现哈希表 | 无序、集合、不能重复 | 查找某些数是否在另一个集合中出现,使用哈希表 |
快乐数 | unordered_set实现哈希表 | 无序、集合、不能重复 | 查找某个数是否重复出现使用哈希表; |
两数之和 | unordered_map实现哈希表 | 键值对、无序 | 查找某个数是否出现使用哈希表; 需要返回数组索引,所以要使用键值对; |
四数相加II | unordered_map实现哈希表 | 键值对、 | 查找两个数组中数和的相反数是否在两外两个数组和的集合中出现,使用哈希表; 返回数组索引使用键值对; |
赎金信 | 数组实现哈希表 | 字母数量有限、桶原理 | 查找一个字符串中所有字母是否在另一个字符串中出现,使用哈希表; |
三数之和 | 双指针法 | 返回三元组,哈希法要复杂去重,不选择; | 先排序,然后使用双指针法; 哈希法要求最后有复杂的去重逻辑,不如双指针法; |
四数之和 | 双指针法 | 返回三元组,哈希法要复杂去重,不选择; | 先排序,然后使用双指针法; 哈希法要求最后有复杂的去重逻辑,不如双指针法; |
有效的字母异位词:用数组实现哈希表;
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
示例 1: 输入: s = "anagram", t = "nagaram" 输出: true
示例 2: 输入: s = "rat", t = "car" 输出: false
说明: 你可以假设字符串只包含小写字母。
英文小写字母,数量有限,只有26个,字母可以轻易变成数字作为数组的索引(通过ASCII码,例如'z' - 'a'
即使字母'z'
的数组索引;
两个数组的交集:看一个数组中元素是否在另一个数组中出现;
给定两个数组,编写一个函数来计算它们的交集。
示例 1
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的
题目中要求找数组的交集,直接用集合;集合不要求有序,则使用unordered_set
实现;
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set; // 存放结果,之所以用set是为了给结果集去重
unordered_set<int> nums_set(nums1.begin(), nums1.end());
for (int num : nums2) { // 基于范围的for循环
// 发现nums2的元素 在nums_set里又出现过
if (nums_set.find(num) != nums_set.end()) {
result_set.insert(num);
}
}
return vector<int>(result_set.begin(), result_set.end());
}
};
如果数组中元素的值的范围确定,也可以用数组实现(桶原理),数组实现和集合set实现相比,unordered_set
要进行哈希运算求出哈希值,所以速度慢一些,但是数组内很多元素位置用不上,可能有空间浪费;
快乐数:看结果集中元素是否重复出现;
快乐数定义为:
- 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
- 然后重复这个过程直到这个数变为 1,也可能是无限循环但始终变不到 1。
- 如果这个过程结果为1,那么这个数就是快乐数。
如果 n 是 快乐数 就返回 true ;不是,则返回 false 。
示例:
输入:n = 19
输出:true
解释:
12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1
找规律,如果结果集中出现了1,则说明是快乐数;如果结果集中出现了重复的元素,则说明不是快乐数;结果集中出现了重复元素,即代表结果集.find(当前结果)
时返回的迭代器不是end()
;
class Solution {
public:
// 取数值各个位上的单数之和
int getSum(int n) {
int sum = 0;
while (n) {
sum += (n % 10) * (n % 10);
n /= 10;
}
return sum;
}
bool isHappy(int n) {
unordered_set<int> set;
while(1) {
int sum = getSum(n);
if (sum == 1) {
return true;
}
// 如果这个sum曾经出现过,说明已经陷入了无限循环了,立刻return false
if (set.find(sum) != set.end()) {
return false;
} else {
set.insert(sum);
}
n = sum;
}
}
};
两数之和:转换思想,两数之和为target,则两个数分别为n
,target-n
;
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
转换思想,目标值为target
,找两个值相加为目标值;不如转换为a, target-a
都出现在数组中;
则问题转换为遍历到a
时,判断target - a
是否在哈希表中存在,如果存在,则有一对两数之和为target
的存在;
注意返回,不是返回有几个满足条件的数,而是返回下标,所以在找到数字在数组中出现之后还要找到数组下标,所以不能使用set
使用map
,设置键值对key-value
,key
的值为数字的值,value
的值为数字在数组中的下标;
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
std::unordered_map <int,int> map;
for(int i = 0; i < nums.size(); i++) {
// 遍历当前元素,并在map中寻找是否有匹配的key
auto iter = map.find(target - nums[i]);
if(iter != map.end()) {
return {iter->second, i}; // 由于只有一组答案,所以不需要保存,直接返回
}
// 如果没找到匹配对,就把访问过的元素和下标加入到map中
map.insert(pair<int, int>(nums[i], i));
}
return {}; // 没有找到
}
};
此处选择了一边遍历,一边找,一边加入,即只用一次循环就可以完成所有过程;
直观的思路是:先将数组中所有数字加入到哈希表中,然后再次遍历数组,看是否存在target - nums[i]
在哈希表中,如果存在,则将两个数的下标组合后保存;(升级题目,返回所有满足目标和为target的数组下标)
可是如果按照直观的思路走,不仅要遍历两边,而且还要去重,任务量翻了一倍;
事实上,我们根本不需要先创建哈希表,而是一边遍历一边创建一边查找,如果遍历到nums[i]
时,target - nums[i]
还未被遍历到,则不会命中哈希,而之后遍历到target - nums[i]
时,则会命中哈希,所以nums[i]
和target - nums[i]
这一对只会命中一次哈希后保存一次!
四数之和II:和两数之和一样,要转换成哈希思想,将目标和为target转换为两数之和的相反数在哈希表中;
给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:
0 <= i, j, k, l < n
nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
示例 1:
输入:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2]
输出:2
解释:
两个元组如下:
1. (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0
2. (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0
由局部到全局,由小到大,由少到多的思想;四数之和看上去很难入手,很难入手的原因是四个数,而两个数我们就很好入手了,之前已经用哈希法解决了两数之和的问题;所以先要考虑能不能将问题简化,将四个数之和转换为两个数之和,然后用哈希法解决两个数之和;
先将四数之和转换为两数之和;假设有四个数组nums1、nums2、nums3、nums4
;则首先将nums1和nums2
中数任意两两相加到一个哈希表中,然后暴力循环另外两个数组,也让另外两个数组两两相加,相加之后看相反数是否在哈希表中出现,如果出现,则说明四数之和为0;
注意事项,四数之和中nums1和nums2
中两两相加的结果可能重复,如果使用unordered_set
,就会导致重复的结果丢失;所以这里使用unordered_map
,重复结果出现时让Value
的值加一;这样在后面命中哈希的Key
时,不是找到了一个目标和为0的四数之和,而是找到了Value
个目标和为0的四数之和;
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int, int> umap; //key为a+b的值,value为该值出现次数;
for (int a : nums1) { //遍历A数组,注意遍历的写法;
for (int b : nums2) {
umap[a + b]++;
}
}
int count = 0;
for (int c : nums3) {
for (int d : nums4) {
if (umap.find(0 - (c + d)) != umap.end()) {
count += umap[0 - (c + d)];
}
}
}
return count;
}
};
赎金信:数组实现哈希表,用数组当桶;
给定一个赎金信 (ransom) 字符串和一个杂志(magazine)字符串,判断第一个字符串 ransom 能不能由第二个字符串 magazines 里面的字符构成。如果可以构成,返回 true ;否则返回 false。
(题目说明:为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思。杂志字符串中的每个字符只能在赎金信字符串中使用一次。)
注意:
你可以假设两个字符串均只含有小写字母。
canConstruct("a", "b") -> false
canConstruct("aa", "ab") -> false
canConstruct("aa", "aab") -> true
题目中的:**“小写字母、使用一次、从第二个字符串中找第一个字符串中的字符等”**信息,直接告诉我们使用哈希表;
/*
判断是否在一个字符串中出现另一个字符串所需的字符,字符串可以看成集合,
即判断集合中是否有所需的元素,用哈希表;
选择什么样的数据结构来实现哈希表?
数组(桶):26个小写字母,空间并不大而且固定;
map:要维护key和value,key是字符,value是出现次数;空间消耗比数组大;
*/
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int record[26] = {0}; //注意:不用字符数组,字符数组不好维护;
if(ransomNote.size() > magazine.size()) {
return false; //提前判断,提前退出;
}
for(int i = 0; i < ransomNote.size(); i++) {
record[ransomNote[i] - 'a']++;
}
for(int i = 0; i < magazine.size(); i++) {
record[magazine[i] - 'a']--;
}
for(int i = 0; i < 26; i++) {
if(record[i] > 0) {
return false;
}
}
return true;
}
};
三数之和:使用双指针法;
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
注意: 答案中不可以包含重复的三元组。
示例:
给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为: [ [-1, 0, 1], [-1, -1, 2] ]
和之前的两数之和以及四数之和II不同,之前是从不同数组中找,每个数组中只找一个数;此时是从一个数组中找,难度加大;
从一个数组中找,首先可以使用暴力循环,直接三层遍历数组,时间复杂度为 O ( n 3 ) O(n^3) O(n3)级别;
其次,也可以向之前的两数之和一样使用哈希表,但是要考虑去重逻辑:任取两个不同的数求和后加入哈希表,同时需要记录两个数的下标,因为返回的是下标;Key是两数的和,Value是一个二维数组,保存和为Key的两个数的下标;之后再遍历数组,查找第三个数的相反数是否在哈希表中,即使命中了哈希,也要判断三个数的下标都要不相同,如果三个数的下标都不相同,则将三个数的下标组合后加入结果中;最终得到的结果数组还要进行去重,此处去重逻辑较为复杂,需要考虑的情况很多;即使去重之后,还要将数组下标转换为数组的值后返回,因为要求返回的是数组中的数而不是下标;
双指针法也可以在数组中应用,而且不用考虑去重逻辑,更好(三指针法):
- 对数组进行排序(有序之后,指针的移动方向就可以确定,如果三数之和大于0,说明指针要想左移动(取更小的数相加),如果小于0,则指针要想右移动)
- 选定一个基准
nums[i]
,之后设置left
和right
指针,left
和right
都大于i
且right
大于left
; - 如果
nums[i] + nums[left] + nums[right] = result
,如果result
大于0,则right
左移;如果result
小于0,则left
右移; - 注意考虑数组中可能有重复元素,所以左移和右移的步长可以不设置为1;包括i的增长也可以不设置为1;
双指针法将暴力破解的 O ( n 3 ) O(n^3) O(n3)变成了 O ( n 2 ) O(n^2) O(n2),同样,如果是四数之和,就是将暴力破解的 O ( n 4 ) O(n^4) O(n4)变成了 O ( n 3 ) O(n^3) O(n3);
// 双指针法:
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort (nums.begin(), nums.end()); //先排序
//采用双指针法(三个指针: i,left, right)
for (int i = 0; i < nums.size(); i++) {
if (nums[i] > 0) {
return result; //如果全部都大于0,不可能加起来等于0;
}
if (i > 0 && nums[i] == nums[i-1]) { //left重复;
continue;
}
int left = i + 1;
int right = nums.size() - 1;
// 寻找三数之和为0的;
while (right > left) {
if (nums[i] + nums[left] + nums[right] > 0) {
right--;
} else if (nums[i] + nums[left] + nums[right] < 0) {
left++;
} else {
result.push_back (vector<int>{nums[i], nums[left], nums[right]});
//去重逻辑应该放在找到一个三元组之后;
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
right--;
left++; //找到一个三元组之后,指针都向下一位移动;
}
}
}
return result;
}
};
四数之和:三数之和的进阶版,也是使用双指针法;
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> result; //创建保存结果的四元组;
sort (nums.begin(), nums.end()); //排序;双指针法要先进行排序;
for (int i = 0; i < nums.size(); i++) {
if (i > 0 && nums[i] == nums[i - 1]) { // 重复元素中,只选择一个,避免了复杂的去重逻辑;
continue;
}
for (int j = i + 1; j < nums.size(); j++) { // 两个定值:nums[i]和nums[j];
if (j > i + 1 && nums[j] == nums[j - 1]) { // 相比三数之和多了一层循环;
continue;
}
int left = j + 1;
int right = nums.size() - 1;
while (right > left) { // 设置双指针移动规律;
if ((long)nums[i] + nums[j] + nums[left] + nums[right] > target) {
right--;
} else if ((long)nums[i] + nums[j] + nums[left] + nums[right] < target) {
left++;
} else {
result.push_back(vector<int>{nums[i], nums[j], nums[left], nums[right]});
while (right > left && nums[right] == nums[right - 1]) right--;
while( right > left && nums[left] == nums[left + 1]) left++;
right--;
left++;
}
}
}
}
return result;
}
};
哈希表总结
适用情况:哈希表适合在集合中快速查找元素,包括找单个元素或匹配另一个集合中的元素。有时候问题并不直接要求查找元素,而是需要将问题转换为寻找某个元素的形式。处理元素出现次数或简单的返回下标等情况,哈希表很实用,但对于复杂的去重、返回数组值元组等操作,需综合考虑哈希表及后续逻辑复杂性。
三种实现:哈希表可以用数组、集合和映射来实现,每种都有其优劣。选择时需根据具体情况斟酌。映射(Map)功能强大但可能带来较高的内存开销,尤其相较数组。
桶算法:桶算法是一种简单、高效的哈希表实现,即利用数组来构建哈希表。适用于元素值范围有限或元素类型是字母的情况。
红黑树和哈希算法构造的哈希表对比:红黑树实现的哈希表稳定,查询和删除效率都是平衡搜索二叉树的插入和删除效率,即O(logn)
。而哈希算法实现的哈希表的查询删除效率都是O(1)
。这不代表哈希表实现的哈希表就好,哈希算法会带来哈希计算冲突问题,时间效率上不稳定,虽然平均情况可能好,但是个别哈希冲突可能导致计算哈希值时间长。其次,红黑树的有序性导致了在解决需要哈希表有序的问题时我们不得不使用红黑树实现的哈希表,而不能使用哈希算法实现的哈希表;