1 目标
结合一道简单的题目Leetcode-两数之和,学习HashTable、和函数对象。
2 题意
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
3 思路
author's blog == http://www.cnblogs.com/toulanboy/
3.1 思路出发点
这是昨天的打卡题(2020年10月3日),虽然之前做过,但是知道有更好解法,故昨晚学习了一下。
谈下暴力法:只需双重循环,两两尝试匹配。复杂度是0(n^2)
但,如果能拥有常数级别时间复杂度的查找find和插入insert的数据结构,那么结合该数据结构,我们可以使用以下逻辑来实现O(n)
的解题。
具体逻辑:从前往后遍历nums数组。对于nums[i],用O(1)查找该数据结构,查看之前是否出现过他的匹配数字。
- 若有,找到答案,退出。
- 若没有,则把当前数字用O(1)放入到数据结构,然后继续nums[i+1]。
总体复杂度:O(n)。
而hashtable就是能满足我们需求的数据机构!
3.2 HashTable
概述:通过数组+链表
的形式,结合hash
算法,查找和插入的时间复杂度为常数级。
3.2.1 HashTable 结构
(1)表面认识
注释的内容会在后面解析,刚开始学习,我们先看大体,再看细节。
组成架构:该数据结构包含多个桶bucket[1],然后每个桶里面可以放很多数值[2]。
插入逻辑:对于一个新来的数值key
[3],通过一个简单的运算[4],确定该数值key
应该放那个桶,然后把它丢进去[5]即可。
查找逻辑:对于需要被查找的数值key
,参考插入逻辑(先通过一个运算确定它在哪个桶),再去这个桶里面逐一遍历出来。
(2)稍微深入的学习
[1] 多个桶bucket:这是通过顺序数组来实现的。
[2] 每个桶里面可以放很多数值:实际上,每个桶存储的都是一个指针,该指针指向一条链表。
[3] key:放入的数值,不限定类型。在C++层面,如果是单一类型,那么可以对应标准库的unodered_set。如果是键值对(结构体),那么可以对应标准库的unodered_map。
[4] 简单的运算:这个是hash运算。给定指定数据,hash运算会将其转换为一串数字
[5] 把它丢进去:这个是hash冲突
的处理方法,如果多个数据都hash到同一个桶,那么我们将这个视为hash冲突
。而这里处理冲突的方法,就是使用一条链表,将所有hash到这个桶的数据都串起来,然后只需把链表头指针放到桶里面就行,这个处理方案的方法被称为链地址法
。
(3)结构总结
hashtable的结构利用hash运算,将数值映射到某个桶。如果出现冲突,那么就使用链表处理冲突。由于hash运算不需要复杂的运算,所以使得他的查找效率和插入效率非常高。
(4)其他
Q:后期数据太多,链表太长影响效率?
A:可以设定阈值,当达到阈值时,则进行重哈希rehash(),将当前数组数据迁移到更大的数组。
(5)上面内容主要从以下博文学习得到,建议感兴趣的同学可以细看下面的文章。
3.2.2 HashTable 对应的标准库
C++新标准中有2个STL容器是用hashtable作为底层实现的:
(1)unodered_set
,能够存储单类型的容器。例如建立一个字符串类型的hashtable。
(2)unodered_map
,能够存储键值对的容器。例如建立一个 <姓名,年龄>的hashtable。
3.2.3 unodered_set使用示例
关于标准库的使用,如果是int,string,float这些基本类型,那么STL自带的hash函数能够处理,那么建立时只需传递数据类型。如:
unodered_set<int> age_set;//建立1个int类型的hashtable
unodered_map<string, age> person_map;//建立1个<string, age>类型的hashtable
若是其他类型,则还需要传递hash函数
以及比较函数
。而这2个函数一般通过函数对象
的形式的传递。
下面代码使用了函数对象。若暂时不知道的,可以先看下一小节。
/*
unordered_set的样例代码。
*/
# include<iostream>
# include<unordered_set>
using namespace std;
//定义1个类
class Point{
public:
int x;
int y;
Point(int x, int y){
this->x = x;
this->y = y;
}
};
//定义Point的hash类
//由于其重载了(),故其实例化后的对象,类似于函数指针。
class PointHash{
public:
size_t operator()(const Point& p)const{
//这里调用STL的hash为我们计算中间值
return hash<int>()(p.x) + hash<int>()(p.y);
}
};
//定义Point的equal类
//由于其重载了(),故其实例化后的对象,类似于函数指针。
class PointEqual{
public:
bool operator()(const Point& a, const Point& b)const{
return a.x == b.x;
}
};
int main(){
unordered_set<Point, PointHash, PointEqual> my_set;
my_set.insert(Point(11, 22));
for(auto it = my_set.begin(); it != my_set.end(); ++it){
cout << it->x << ","<< it->y << endl;
}
/*
输出:11, 22
*/
auto result = my_set.find(Point(11, 22));
if(result != my_set.end()){
cout << result->x << ","<< result->y << endl;
}
/*
输出:11, 22
author's blog == http://www.cnblogs.com/toulanboy/
*/
return 0;
}
3.3.4 参考文章
该部分参考文章如下,作者写得太好了,感谢。
3.3 函数对象
3.3.1 基本概念
函数对象,也被称为伪函数,在STL容器中经常被使用。
本质是一个类,该类重载了()
,其实例化的对象
可以实现函数调用的效果。
举个例子:
class My_Lovely_Add{
public:
//重载()
int operator()(int a, int b){
return a+b;
}
};
int main(){
My_Lovely_Add f;
cout << f(11, 22) << endl;
//输出:33
return 0;
}
上述My_Lovely_Add
类由于重载了()
,故其实例化的对象
可以实现函数调用的效果。
3.3.2 与函数指针的异同
他们两者都能实现具体函数的传递,从目前的学习来看,函数对象具备以下优点:
- 可以使用inline。
- 可以通过类成员记录调用情况。
3.3.3 参考文章
4 代码
然后,就可以使用hashtable
的STL之一 unodered_map
来解题了!
class Solution {
public:
//学习了官方题解:https://leetcode-cn.com/problems/two-sum/solution/liang-shu-zhi-he-by-leetcode-solution/
vector<int> twoSum(vector<int>& nums, int target) {
//创建unordered_map,利用其O(1)的插入和查找进行快匹配
unordered_map<int, int> u_map;
//创建unordered_map的迭代器
unordered_map<int, int>::iterator it;
for(int i=0; i<nums.size(); ++i){
//看看前面是否出现有匹配的数字
it = u_map.find(target-nums[i]);
//有则输出
if(it != u_map.end())
return {it->second, i};
//否则,把当前数字放进Map,继续往下
u_map.insert(pair(nums[i], i));
}
return {};
}
};
写到最后:
(1)整理这个简短的内容,不知不觉已经过去2小时,午饭时间都过了。。can~
(2)这个只是简单的概述,没有特别具体深入,但希望对你有帮助~