1. 哈希表
1.1 数组与链表的查询问题
在计算机中,数组和链表都可以用于数据的存储,既然有数据存储,那么必然要有数据的查询。因此我们在将数据存储进数组和链表中之后,必然要对它们进行查询操作。
一个链表的查询时间复杂度为O(n),而一个无序数组的查询时间复杂度也是O(n):
思考:链表与数组的查询效率能不能再提升一下?
- 数组来说:数组的查询时间,是可以进行改进的,当数组中数据是有序的,我们就可以使用二分查找的方式查找数组中的数据,进而能大量节省查询时间,对于二分查找,并不是一个困难的问题,因为二分法每次都能甩掉一般的数据,因此其时间复杂度肯定比O(n)低很多,它的时间复杂度是O(logn),随着查询次数的增长,其时间复杂度会明显比O(n)低很多。
- 链表的查询时间肯定是更长的,因为链表是不连续的空间,它只能一个接一个的遍历查询,不管链表是否有序。
总结:对于数组而言,我们的研究方向便不再是如何找更优的查找方案,而是如何将其更快的排序,因此引出了排序算法。
- 这种思路其实可以解决很多场景的问题,很多系统上电将表排序一次,后续直接用二分法就可以了。
- 目前主流的排序算法有八种,实际上大家比较认可的排序方法还有更多,值得学习的排序方法至少有十种。
- 现实告诉我们,矛盾很难消除,它只会转移,有了二分查找的数组,查询时间长的矛盾并不能直接消除,因为排序算法也是耗费时间的,查询的时间跑到了排序的时间里去了。
- 对于排序的时间复杂度,最低的为O(nlogn),看上去也不是特别的少,这也就直接导致了两个算法加起来的时间复杂度,比直接无序状态查还耗费时间。
1.2 新思想引入
有一天有个人想:既然排序算法库耗时,那我再数据新增的时候不要直接无序的新增,我按照顺序将其插入,那么我查询的时候就不需要再重新排序了。
哈希表的思路:在存储数据的时候,不再来数就存,而是使用一种巧妙的分类方法,将数据们进行分类,进而达到像二分查找一样的大规模缩减查询范围的效果,这就是哈希存储。
1.3 新概念引入 — 索引
引入生活当中的例子:
在一个图书馆中,存放着很多各种类别的书,有小说,字典,杂志,专业书籍等若干本。
在早期,图书管理员并不怎么好好打理这些书籍,它将这些书籍完全无序的堆放在一起,来了借阅的人,就要直接挨个翻找,直到找到自己想要看的书籍为止。
在之后的某一天,来了一个新的图书管理员,这个图书管理员为这些书籍进行了分类,他按照这四种类别将这些书放到了不同的区域中,之后,来了借阅的人,首先会报出书名,然后图书管理员按照书名判断这本书的类别。
来了一个人想借阅《人民文学》,图书管理员就会告诉这名读者:该书籍属于杂志,请去杂志区寻找,这名读者就会直接步行至杂志区,这样就他就不用再对整堆书进行翻找,很大程度上的节省了时间。
哈希表的工作原理,实际上就是这样的一种过程。
其使用到的主要思想,就是索引存储,它按照某一种规则,为数据分类,然后将这些数据放入不同的类别下,这些类别会有相应的索引值,在我们进行查询的时候,首先会查询索引值,查询到索引值之后,便直接将这个数据与索引值对应的存储结构中进行查询,这样就直接缩减了查询范围,缩小了查询规模。
4. 关联容器
关联容器是不是只能有一个key ?
是的,在关联容器(如哈希表、字典、映射等)中,每个键(key)通常是唯一的。
这是因为关联容器的设计是为了根据键快速查找相应的值,因此每个键只能与一个值对应。多个相同的键会造成冲突,无法区分出对应哪个值。
- 在哈希表中,哈希函数根据键计算出一个唯一的索引(理想情况下),并将值存储在该位置。如果多个键相同,哈希函数将无法区分它们。
- 在树形结构(如C++中的std::map)中,键用于确定元素的位置,并保持排序。如果允许多个相同的键,树结构将无法有效维护键值对的顺序。
尽管大多数关联容器要求键唯一,但有时我们需要处理重复键的情况。不同的编程语言和数据结构提供了不同的机制来解决这个问题:
有些容器允许多个相同的键,但通常以某种方式处理这些冲突,例如:
// C++ 的 std::multimap: 与 std::map 不同,std::multimap 允许多个相同的键。它可以存储多个相同键的键值对,用户可以通过迭代器遍历这些值。
std::multimap<int, std::string> my_multimap;
my_multimap.insert({1, "apple"});
my_multimap.insert({1, "orange"});