数据结构与算法面试知识点汇总(超全)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/CSDN_dzh/article/details/86724458


哈希函数和哈希表

哈希函数

哈希函数(散列函数):类似于函数调用一样,给一个字符串作为输入,返回一个哈希码。
哈希表(散列表):把哈希码映射到表中的一个位置来访问记录,存放记录的数组就叫散列表

  • 直接寻址法:hash(key) = a * key + b
  • 数字分析法
  • 平方取中法
  • 折叠法
  • 随机数法
  • 除留余数法

性质:
1)输入域是无穷大的
2)输出域是有穷的,输出域要比输入域小
3)哈希函数不是一个随机函数,只要输入一样,输出就一样
4)输入域这么大而输出域这么小,而由于特性3的关系,就会导致输入不一样也有可能得到输出一样的值(哈希碰撞)
5)虽然有哈希碰撞,如果输入够多,将在整个输出域上均匀地出现返回值,不一定完全平分,但输出域中的每个哈希码一定被差不多个输入域中的值对应
6)因为要做到均匀分布,于是哈希函数跟输入的规律是没有关系的,可以打乱输入规律

推广:
当输入域足够大,经过一个哈希函数计算完后保证输出域是均匀分布时,
对输出域的每一个哈希码模%上一个m,最终的域会缩减成0至m-1,也均匀分布

工程应用:
如何找到1k个相对独立的哈希函数,一个哈希函数的规律跟另一个哈希函数的规律是无关的
只要用1个哈希函数就能改出1k个来,如:哈希f1 + k * 哈希f2 = 哈希fn

哈希表

实现原理:
在这里插入图片描述

解决哈希冲突(key1和key2称为同义词)的方法:

  • 开放寻址法(包括线性探测再散列、二次探测再散列(改善堆积现象)、伪随机探测再散列、双散列法),容易造成堆积现象
  • 链地址法:在原地址新建一个空间,以链表结点的形式插入到该空间。链地址算法的基本思想是将所有哈希地址为 i 的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。插入新数据项的时间随装载因子线性增长。

哈希表扩容:

如果发现某个位置上的链的长度到达k了,那么可以认为其它位置上链的长度也到达k了。此时,可能下一次加记录的话效率就不行了,这时就要经历哈希表的扩容

  • 首先要将原来的哈希域(如前面的17),扩展到104(或别的)。
  • 扩完之后如果要模104,则原来的记录都会失效。此时要将每一个记录都拿出来,用哈希函数计算完之后去模上104,再分配其在哪个位置。

那么,扩容代价不用计算吗?为什么能做到O(1)呢?
一方面样本量是N,若每次扩容都让哈希域长度增加k倍,则扩容平均代价是O(logkN)可很低
另一方面还可以离线扩容。
于是哈希表的增删改查是O(1)的。

Q:有一个大文件100T,每一行是一个字符串,是无序的,让你打印重复的字符串
A:如果单核单CPU单台机器去搞这个事情,那头都大了
Q:说得对,那你怎么办?
A:这是一个大数据问题,可以通过哈希来分流,你给我多少台机器呢?大文件存在哪呢?
Q:给你1000台机器干这个事情,大文件存在一个分布式文件系统上,按行读有很快的工具
A:

  • 1)首先对机器标号0-999,把每一行作为一个文本读出来,
    利用哈希函数去算一个哈希码,去模1000,
    当然也不一定要你提供的1k台机器,只要我能知道里面有m种字符串就好了,就模m
  • 2)然后将结果放到对应标号的机器中,用一个文件记录下来。
    这样就把这个大文件分到了m台机器上去,而且相同的重复文本一定会分配到同一台机器上(哈希函数的性质:相同输入相同输出,不同输入均匀分布)
  • 3)然后在单台机器上去统计有哪些重复的,最后就是1k台机器并发
    如果单台机器上的文件还是太大,再在这个机器上做相同的步骤,在单台机器上并行计算即可

布隆过滤器

主要用于解决爬虫去重问题以及黑名单问题,其短板是它有失误率。

这个失误是指:宁可错杀,绝不放过1个的类型。如果是坏人,一定报警。如果是好人,也可能报警,但几率较小

以黑名单问题来举例:

有这么个需求,100亿个URL是黑名单,假设每个URL是64字节,
当用户想搜索某个URL时,如果它是黑名单里的URL,就要对它进行过滤

解决方案:
1)首先它可以通过哈希表实现,但是需要多少个空间呢?

  • 由于这里面不用存value,只用存key,于是它是一个hashSet,key是URL字符串
  • 这里起码需要6400亿个字节才能把这些URL装下,还不算指针、索引等所占的空间,实际上是比6400亿字节(640G)大的, 如果做成哈希表全放内存中,代价是很高的,而且服务器还要用分布式内存
  • 这个设计就耗费了巨大的代价。即便用哈希函数分流到多个机器上,这浪费了机器

2)那怎么能仅适用一台机器就能提供服务呢?——布隆过滤器

  • 在哈希来哈希去的经典方法说了以后,应该问允许有很低的失误率吗?——允许。这个时候就可以使用布隆过滤器。它是一个bit类型的map。

实现方法:
第一步:
准备一个数组,从0到m-1位置,每个位置上不是整数也不是串,是一个bit,只有两种状态0/1
int *arr = new int[1000]; // 分配4*1000字节的连续内存,共4*1000*8个bit
在这里插入图片描述
这样,就用基础类型int做出了一个0到m-1长度为m的bit类型的数组。如果想省空间,就用long类型,8个字节64位。此外还能做成矩阵的形式。
如果想让第300000个bit位置置1,此时int index = 300000,

  • ① 定位这个bit来自于哪个桶 int intIndex = index / 32;
  • ② 定位这个位置来自于这个桶的哪个bit位 int bitIndex = index % 32;
  • arr[intIndex] = (arr[intIndex] | (1 << bitIndex));

第二步:
有了这个数组以后,

  • ① 将黑名单内的所有URL,输入k个相互独立的哈希函数中得到k个哈希码分别取模上m,算出来的对应数组中的那个位置1。若产生哈希碰撞,不管,还是置1。这个过程结束以后,URL进入布隆过滤器。注意:这个数组应当长一些,如果太短,则很容易每个位置都变成1,这样用户输入的URL就全都是黑名单了。
  • ② 查URL的过程:将用户提供的URL,也做步骤①,如果算出来的k个位置都已经置1,则说这个URL在黑名单中。如果仅有一个甚至几个没被置1,说明这个URL不在黑名单中(哈希函数特性:相同输入一定有相同输出)。失误的时候就出现在数组开的比较小。
  • 通过调整数组的大小m和哈希函数的个数k,可以得到不同的失误率。
    在这里插入图片描述

一致性哈希

经典服务器抗压结构:
比如提供一个根据用户QQ号(key)返回其网龄(value)的服务。如何使其负载均衡呢?

  • 首先会有一个前端(Nginx),用于接受请求,请求可以发送到任意一个前端上,提供的服务相同
  • 然后有一个后端集群组,假设有3台机器m1,m2,m3
  • 首先信息会发送到前端上,经前端的同样一份哈希函数算出哈希码,模上3得到对应的机器,将这个信息存到对应的机器中。如果数据足够多,数据是均匀地存在三台机器中的,负载均衡:每台机器处理的东西是差不多的,CPU占用差不多,内存使用差不多,相关系统性能指标差不多。
  • 在查的时候,依然是经过上面的步骤,由于相同输入一定导致相同输出,就会到相应的机器上去拿出这个记录,返回给前端,然后前端返回给用户。

上面提到的经典服务器有什么问题呢?——当增加机器或减少机器时,它就炸了

原因:
这和哈希表扩容是一样的,如果加机器加到100,原来模的是3,现在要模100,所有的数据归属全变了。所以必须要把原来的记录都拿出来再算一次,然后迁移到相应的机器上。代价很高

于是有了一致性哈希结构,可实现把原来全部数据迁移的代价变得很低,并负载均衡

具体是这样的:
在这里插入图片描述
那么如何实现它呢?

  • 对于后端——还是那三台机器
  • 对于前端——还是无差别的负载的服务器

然后,对三台机器对应的哈希值排好序之后做成一个数组存到前端中去
这样,不管请求发送到前端的哪一个机器上,由于每个机器上都有这么一个数组,二分的方式找到第一个大于等于算出来的哈希值的那个位置,就知道这个请求应当由哪个机器处理了。
即便你有10亿台机器,由于采用二分的方法,log以后复杂度也是很低的。

那为什么说它克服了传统服务器结构中加减机器的问题呢?

比如加了一台m4,同样也映射到环上。
在这里插入图片描述
这样,

  • 在增加机器时,不需要去迁移m1和m3全部的数据,只用迁移原本属于m2的数据即可完成
  • 在减少机器时,只需要把m4的数据迁移到m3上即可

于是,这种结构,加减机器数据迁移的代价是非常低的

但是,存在什么问题呢

  1. 在机器数量比较少时,这个环是不能够保证其均分的(哈希函数的性质:量在多的时候,才是均匀分布的)。这样负载就会出现问题,一台机器很忙,一台机器却闲得发慌
  2. 即便可以保证均匀,在加一个机器之后负载就不均了

把上述两个问题解决了,一致性哈希就能既做到数据迁移的代价小,又能保证负载均衡

虚拟结点技术:

  • 真实机器m1,给它分配1k个虚拟结点
  • m2,m3也同样都分配1k个虚拟结点

此时,不让m1,m2,m3各自的ip去映射到环上,

  • 准备一张路由表,这个路由表可以由真实机器去查它有哪些虚拟结点,同样也可以由虚拟结点去查它属于哪个真实机器
  • 让这3k个虚拟结点经哈希函数映射到环上,某个虚拟结点负责的域均给它的真实机器处理
    3k个虚拟结点占据了这个环,于是三台机器负责的数据就是差不多的了,负载均衡!

如果此时增加一台机器m4,让它也分配1k个虚拟结点,让它们直接进环,再做数据迁移即可
拿走的数据(由m1、m2、m3的部分数据组成)是由虚拟结点决定的

虚拟结点导致的哈希冲突:冲冲呗,碰撞的概率很小,两个机器共同存一份。


并查集

解决的是

  • 1)非常快地检查两个元素各自所在的集合是否属于一个集合。
  • 2)两个元素各自所在的集合,合并在一块

例子:有元素A,元素B,其中A∈集合set1,B∈集合set2

  • 对于1),有函数isSameSet(A, B),用于查set1和set2是否一个集合。
  • 对于2),有函数union(A, B),用于set1和set2合并。

那么可以怎么实现呢?

  • 方案1:
    使用链表list,把每个集合当做一个list,每个元素是一个node,放在list中
    对isSameSet而言,可以遍历list1,看其中有没有B(缺点是需要遍历,慢
    对union而言,就把两个list接起来即可
  • 方案2:
    使用哈希表set,把每个集合当做一个set,每个元素是一个node,放在set中
    对isSameSet而言,可以再set1中以O(1)去查有没有B。
    对union而言,要把set2所有的东西都扔到A中(缺点是也需要遍历

初始化:
初始化时必须一次性把所有的数据样本给它,不能动态地过来(如

具体实现

代表结点代表整个集合在这里插入图片描述

  • 对于isSameSet,只需要通过元素A和元素B不断往上找直到找到各自代表结点,查它们的代表结点是否相同就可以判断它们是否在桶一个集合中。
  • 对于union,首先要通过元素A和元素B不断往上找直到找到各自代表结点,让数量少的那个set挂在数量多的那个set上。合并set的代表结点是原来数量多的那个set的代表结点。

优化

看出每一次查询操作的时间复杂度为O(H),H为树的高度
于是只要去查某个元素的代表结点时,都涉及下面的优化

在这里插入图片描述

代码实现

刷题笔记32——STL::map实现并查集、岛问题


前缀树(trie树)

效率很高:

  • 每个节点保存一个字符
  • 根节点不保存字符
  • 每个节点最多有n个子节点(n是所有可能出现字符的个数)
  • 查询的复杂度为O(k),k为查询字符串长度

功能举例:
在这里插入图片描述

B树和B+树

B树和B+树的出现是因为一个问题——磁盘IO。所以在大量数据存储中,查询时不能一下将所有数据加载到内存中,只能逐一加载磁盘页,每个磁盘页对应树的结点。造成大量磁盘IO操作(最坏情况下为树的高度)。

  • 所以,这里将树变得矮胖来减少磁盘IO的次数——采用多叉树,每个结点存储多个元素,又根据AVL得到一棵平衡多路查找树可以使得数据查找效率在O(logN)

B树:

  • 每个节点都存储key和value,所有的节点组成这棵树,并且叶子结点指针为null
  • 一个m阶B树最多有m个孩子,若根结点不是叶子结点,则至少有 2 个孩子
  • 所有叶子结点都在一层
  • 先由根结点找磁盘块1,读入内存(IO第一次);
  • 比较,找指针p2;
  • 根据p2指针找到磁盘块3,读入内存(IO第二次),在磁盘块3中的关键字列表中找到关键字30

综上,需2次磁盘IO操作,2次内存查找操作;由于内存在红的关键字是一个有序表结构,可以用二分法提高效率
故 3 次磁盘IO操作是影响整个B树查找效率的决定因素,它相对于AVL树缩减了结点的个数
在这里插入图片描述

B+树非叶子节点只存储key,只有叶子结点存储value,叶子结点包含了这棵树的所有键值,每个叶子结点有一个指向相邻叶子结点的指针,这样可以降低B树的高度
在这里插入图片描述
为什么B+树是数据库实现索引的首选数据结构呢?——评价一个数据结构作为索引的指标就是在查找时IO操作的次数

1、这棵树比较矮胖,树的高度越小,IO次数越少
2、一般来说,索引很大,往往以文件的形式存储在磁盘上,索引查找时产生磁盘IO消耗,相对于内存读取,IO存取的消耗比较高

聚集索引:data存数据本身,索引也是数据。数据和索引存在一个文件中。
非聚集索引:data存的是数据地址,索引是索引,数据是数据

根据B+树的结构,我们可以发现B+树相比于B树,在文件系统,数据库系统当中,更有优势,原因如下:

  1. B+树单一节点存储更多的元素,使得查询的IO次数更少
    B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说I/O读写次数也就降低了。

  2. B+树所有查询都要查找到叶子节点,查询效率更加稳定
    由于内部结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

  3. B+树所有叶子节点形成有序链表,便于范围查询,更有利于对数据库的扫描
    B树在提高了磁盘IO性能的同时并没有解决元素遍历的效率低下的问题,而B+树只需要遍历叶子节点就可以解决对全部关键字信息的扫描,所以对于数据库中频繁使用的range query,B+树有着更高的性能

展开阅读全文

没有更多推荐了,返回首页