1. 哈希表
哈希表(hash table)又称散列表,是一种可以通过“关键码”(key)直接进行访问的数据结构。
哈希表由两部分组成
- 一个数据结构,通常是链表、数组
- hash函数,输入“关键码”(key),返回数据结构的索引
对外表心啊喂可以通过关键码直接访问:hash_table[key] = value
实际上是在数据结构的hash(key)
位置存储了value: data_structure[hash(key)] = value
最简单的例子,关键码是整数,定义hash(key) = key
那这个哈希表其实就是一个数组了,key
自己就是下标
当然一般情况下,关键码key
是一个比较复杂的信息,比如很大的数、字符串,这时候key
就不能直接作为数据结构的下标了。
此时就需要设计要给Hash
函数,把复杂的信息映射到一个较小的值域内,所谓索引
例子:
对外表现为字符串lies
,实际存储为233
的整数。设计哈希函数hash_table["lies"] = ASCII ++ % 20 = 9
哈希函数令输入lies
映射到9这个索引,然后9的下标索引对应的数值为233
哈希碰撞
哈希碰撞(Collisions)指的是两个不同的key
被计算出同样的hash结果
把复杂信息映射到小的值域,发生碰撞是不可避免的
好的hash函数可以减少碰撞发生的几率,让数组尽可能的均匀分布
开散列是最常见的碰撞解决方案
- hash函数依然用于计算数组下标
- 数组的每个位置存储一个链表的表头指针(成为表头数组)
- 每个链表保存具有同样hash值的数据
形象描述:“挂链”——表头数组每个元素“挂”着一个链表。数组套链表
工程应用
- 电话号码簿
- 用户信息表
- 缓存(LRU Cache)
- 键值对存储 (Redis)
开散列完整结构图
时间复杂度
- 期望:插入、查询、删除O(1)
- 数据分布比较均匀时
- 最坏:插入、查询、删除O(n)
- 数据全部被映射为相同的hash值时
2. 集合与映射
集合(set)存储不重复的元素
- 有序集合,遍历时按元素大小排列,一般用平衡二叉搜索树实现,O(logN)
- 无序集合,一般用hash实现,O(1)
映射(map)存储关键码(key)不重复的键值对(key-value pair)
- 有序集合,遍历时按照key大小排列,一般用平衡二叉搜索树实现,O(logN)
- 无序集合,一般用哈希表实现,O(1)
对于语言内置的类型(int , string),已经有默认的优秀的hash函数,可以直接放进set/map中使用
C++ code
set与unordered_set
- 文档
Unordered_set<string> s;
insert, find, erase, clear
等方法multiset
map与unordered_map
- 文档
Unordered_map<string, int> h
h[key] = value
find(key), erase(key), clear
等方法multimap
实战
1 两数之和https://leetcode-cn.com/problems/two-sum/
/*
- 建立值到下标的映射
- 边循环查询,边插入,每次只查询i前面的映射
- 维护nums[0, i - 1]
- 防止查询到i本身
*/
874 模拟行走机器人https://leetcode-cn.com/problems/walking-robot-simulation/
/*
- 可以用set或者map存储障碍物,从而快速判断一个格子里有没有障碍
- 利用方向数组简化实现(代替if)
*/
49 字母异位词分组https://leetcode-cn.com/problems/group-anagrams/
/*
- 对字符串的分组就是用hash,让同一组的字符串拥有相同的hash值,然后用hash map分组
- 思路1:重新排序-->分组-->提取到ans
- map<string, group>
-思路2: 统计每个字符串中每个字母的出现的次数,把长度为26的计数数组作为key
- map<array[26], group>
- 熟悉map的用法
- 字符串排序
- map的插入
*/
30 串联所有单词的子串https://leetcode-cn.com/problems/substring-with-concatenation-of-all-words/
/*
长度相同:滑动窗口长度固定
中间不能有其他字符:连续判定word
不需要考虑顺序:hash
思路: 比较滑动窗口的map和输入的words的map是否相等,考虑单词的重复
滑动窗口
- 窗口size = words.size() * words[0].size()
对words建hash map */
- // words的单词到次数的映射
unordered_map<string, int> word_to_times;
for (string& word : words)
{
word_to_times[word] += 1;
}
set、map的使用及其特性和区别
STL总共实现了两种不同结构的管理式容器:树型结构与哈希结构。树型结构的关联式容器主要有四种:set,map,multiset,multimap。下面介绍一下这四种容器的简单使用。
1.set
set里面每个元素只存有一个key值,它支持高效的关键字查询操作,比如检查一个关键字是否在set中。如果这个key值之前存在的话就不插入。
简单使用如下:
插入:
set<int> s;
s.insert(2);
s.insert(1);
s.insert(4);
s.insert(5);
s.insert(3);
s.insert(5);
s.insert(5);
s.insert(5);
s.insert(5);
s.insert(5);
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
插入如上数据之后,打印出来的值为1 2 3 4 5。set容器自动对以上数据进行了排序,并且实现了去重。但是不能对set里的值进行修改。
查找:
//时间复杂度:O(logN)----底层是搜索树
set<int>::iterator pos = s.find(3);
//时间复杂度:O(N)----需要遍历一遍(不建议使用)
//set<int>::iterator pos = find(s.begin(), s.end(), 3);
if (pos != s.end())
{
cout << "找到了" << endl;
set容器中的find查找效率高,因为底层是一个二叉搜索树,比要查找的值小就去左子树查找,反之则去右子树查找。
删除:
//s.erase(3);
s.erase(pos);//找到了我就删,没找到要删的话会报错
采用s.erase(3);这种操作如果没有3并不会报错,如果有3则会删除这个结点。
找到pos位置,采用s.erase(pos);这种操作如果没有3则会报错,如果有3则会删除这个结点。
交换:
set<int> ss;
ss.insert(6);
ss.insert(9);
ss.insert(8);
ss.insert(7);
ss.insert(10);
ss.swap(s);//交换根节点的指针,效率高
两个set的交换的其实是交换结点的指针,效率高。
清空:
s.clear();//清掉所有数据
遍历方法:
//新式for循环
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
//迭代器遍历
set<int>::iterator sit = s.begin();
while (sit != s.end())
{
cout << *sit << " ";
sit++;
}
cout << endl;
推荐大家使用新式for循环~比较简单一些٩(๑❛ᴗ❛๑)۶
2. multiset
其实整体的接口和set都相同,但是multiset可以插入key相同的值。
multiset<int> ms;
ms.insert(2);
ms.insert(1);
ms.insert(4);
ms.insert(5);
ms.insert(3);
ms.insert(5);
ms.insert(5);
ms.insert(5);
ms.insert(5);
ms.insert(5);
for (auto e : ms)//可以重复插入相同key值
{
cout << e << " ";
}
cout << endl;
auto pos = ms.find(5);
if (pos != ms.end())
{
cout << "找到了" << endl;//找到的是中序的第一个5
while (*pos == 5)//往后继续找可以找到后面所有的5
{
cout << *pos << endl;
++pos;
if (pos == ms.end())//pos指向最后一个的下一个
break;
}
}
--pos;//倒数第一个5
ms.erase(pos);
for (auto e : ms)//可以重复插入相同key值
{
cout << e << " ";
}
cout << endl;
multiset允许key的冗余,如果用find查找key值时,找到的是中序遍历第一个,因此不断遍历下午可以找到这个multiset里所有的key值。
multiset和set一样不能够对数据进行修改。
3.map
有别于set的是,map是一种key(键),value(值)的形式,用来保存键和值组成的集合,键必须是唯一的,但值可以不唯一。里面的元素可以根据键进行自动排序,由于map是key_value的形式,所以map里的所有元素都是pair类型。pair里面的first被称为key(键),second被称为value(值)。
它可以通过关键字查找映射关联信息value,同时根据key值进行排序。
相关类型的返回值
//成员类型 含义
key_type The first template parameter (Key)
mapped_type The second template parameter (T)
value_type pair<const key_type,mapped_type>
简单的使用如下:
插入:
map<string, string> dict;
dict.insert(pair<string, string>("string", "字符串"));//模板类型pair:构造了一个匿名对象插入到map
dict.insert(make_pair("apple", "苹果"));//模板函数make_pair:偷懒了,实际调的是pair
dict.insert({ "left", "左边" });
dict.insert({ "left", "剩余" });//插入不进去了,因为key值已经有了
插入有三种方法:
采用创建pair的形式插入pair<string, string>(“string”, “字符串”)
采用make_pair的形式进行插入make_pair(“apple”, “苹果”)
采用大括号的形式进行插入{ “left”, “左边” }
遍历方法:
//新式for循环
for (const auto &e : dict)
{
cout << e.first << ":" << e.second << endl;
}
cout << endl;
//迭代器遍历
map<string, string>::iterator mit = dict.begin();
while (mit != dict.end())
{
cout << mit->first << ":" << mit->second << endl;
cout << (*mit).first << ":" << (*mit).second << endl;
mit++;
}
打印出来为:(根据key值进行了排序和key值的去重)
operator[ ]:
// operator[]可以通过key值找到对应的value值。并且还可以使用operator[]插入数据。
dict["banana"];
// 如上,插入一个pair,这个pair的key值为“banana”,value为空字符串(\0)
dict["key"] = "关键字";
// 如上,插入一个pair,这个pair的key值为“key”,value为“关键字”
dict["left"] = "剩余";
// 如上,因为本来map里“left”这个key值,所以operator[]找到了这个key值,将它的value改成“剩余”。
4.multimap
// multimap允许key值的冗余,因此key值相同也可以进行插入。
multimap<string, string> mmp;
mmp.insert(pair<string, string>("left", "左边"));
mmp.insert(make_pair("key","关键字"));
mmp.insert({ "map", "地图" });
mmp.insert({ "left", "剩余" });
for (const auto &e : mmp)
{
cout << e.first << ":" << e.second << endl;
}
set和map特性和区别
set是一种关联式容器,其特性如下:
-
set以RBTree作为底层容器
-
所得元素的只有key没有value,value就是key
-
不允许出现键值重复
-
所有的元素都会被自动排序
-
不能通过迭代器来改变set的值,因为set的值就是键
-
map和set一样是关联式容器,它们的底层容器都是红黑树,区别就在于map的值不作为键,键和值是分开的。它的特性如下:
- map以RBTree作为底层容器
- 所有元素都是键+值存在
- 不允许键重复
- 所有元素是通过键进行自动排序的
- map的键是不能修改的,但是其键对应的值是可以修改的