1 栈
栈,操作受限的线性表,FILO,栈顶插入和删除,只有两种操作,入栈Push(插入)、出栈POP(删除)。
2 队列
队列,操作受限的线性表,FIFO,队尾入队(插入),队首出队(删除)。
3 链表
链表跟数组同级别的数据结构,数组遍历效率高,插入删除效率低,链表插入删除效率高、遍历效率低。ArrayList底层实现是数组,LinkedList底层实现是链表。
4 散列表/哈希表
4.1 hash定义
- 哈希表又叫散列表或者Hash table,它通过把关键码值key通过一个函数f(key)映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数/哈希函数,存放记录的数组叫做哈希表/散列表/hashtable。
- 任意key经过哈希函数映射到数组中的任何一个地址的概率相等,这样的散列函数为均匀散列函数。
- 存在k1不等于k2,使得f(key1)=f(key2),就叫哈希冲突。哈希冲突理论上不可能完全消除,但是应该尽量避免。
4.2 为什么理想情况下hash表的时间复杂度是O(1)?
- hash表物理存储是个数组,极端情况下,如果所有key都发生了哈希冲突,则数组退化成链表,时间复杂度为O(n)。
- 如果不考虑哈希冲突,key可以通过哈希函数快速定位到数组下标找到记录value,时间复杂度为O(1)。
4.3 hash函数&hashcode
hash函数(哈希),也叫散列/杂凑函数,将任意长度的输入转换为固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,散列值的空间通常小于输入空间,不同的输入可能得到相同的输出。
hash函数的基础是hash算法,hash算法也可以认为是一种思想,没有固定的公式,只要符合散列思想的算法都可以成为hash算法,hash算法很难找到逆向规律,可以提高空间利用率,提高数据查询效率,常用于数字签名。
hashcode,哈希码,Java的Object.hashCode()记录在Markword中,Hotspot虚拟机由xor-shift算法(异或和移位)实现,用于哈希表的查找
4.4 构造hash函数的方法
-
直接定址法
- H(key) = a·key + b,其中a和b为常数,这种线性散列函数也叫做自身函数
- 此法仅适合于:地址集合的大小 = = 关键字集合的大小,其中a和b为常数。
- 实际生活中,关键字的元素很少是连续的。用该方法产生的哈希表会造成空间大量的浪费,因此这种方法适应性并不强。
-
数字分析法
- 因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。
- 此法适于:能预先估计出全体关键字的每一位上各种数字出现的频度。
-
平方取中法
- 因为这种方法的原理是通过取平方扩大差别,平方值的中间几位和这个数的每一位都相关,则对不同的关键字得到的哈希函数值不易产生冲突,由此产生的哈希地址也较为均匀。
- 此法适于:关键字中的每一位都有某些数字重复出现频度很高的现象。
-
折叠法
- 将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位),这方法称为折叠法。
- 这种方法适用于关键字位数较多,而且关键字中每一位上数字分布大致均匀的情况。
- 数位叠加可以有移位叠加和间界叠加两种方法:
- 移位叠加是将分割后的每一部分的最低位对齐,然后相加;
- 间界叠加是从一端向另一端沿分割界来回折叠,然后对齐相加。
-
随机数法
- 设定哈希函数为:H(key) = Random(key)其中,Random 为伪随机函数
- 此法适于:对长度不等的关键字构造哈希函数。
-
除留余数法
- 取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p,p<=m。不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,除留余数法的模p取不大于表长且最接近表长m素数时效果最好,且p最好取1.1n~1.7n之间的一个素数(n为存在的数据元素个数),若p选的不好,容易产生同义词。
-
基数转换法
- 将十进制数X看作其他进制,比如十三进制,再按照十三进制数转换成十进制数,提取其中若干为作为X的哈希值。一般取大于原来基数的数作为转换的基数,并且两个基数应该是互素的。
- 为了获得良好的哈希函数,可以将几种方法联合起来使用,比如先变基,再折叠或平方取中等等,只要散列均匀,就可以随意拼凑。
-
随机乘数法
- 亦称为“乘余取整法”。随机乘数法使用一个随机实数f,0≤f<1,乘积fk的分数部分在0~1之间,用这个分数部分的值与n(哈希表的长度)相乘,乘积的整数部分就是对应的哈希值,显然这个哈希值落在0~n-1之间。其表达公式为:Hash(k)=「n(fk%1)」其中“fk%1”表示fk 的小数部分,即fk%1=fk-「fk」
- 此方法的优点是对n的选择不很关键。通常若地址空间为p位就是选n=2p.Knuth对常数f的取法做了仔细的研究,他认为f取任何值都可以,但某些值效果更好。如f=(-1)/2=0.6180329...比较理想。
-
字符串数值哈希法
- 把字符串的前10个字符的ASCⅡ值之和对N取摸作为Hash地址,只要N较小,Hash地址将较均匀分布[0,N]区间内。
-
旋转法
- 旋转法是将数据的键值中进行旋转。旋转法通常并不直接使用在哈希函数上,而是搭配其他哈希函数使用。
-
减去法
- 减去法是数据的键值减去一个特定的数值以求得数据存储的位置。
4.5 解决hash冲突的方法
-
开放定址法(封闭散列)
- Hi=(H(key) + di) MOD m,i=1,2,…,k(k<=m-1),其中H(key)为散列函数,m为散列表长,di为增量序列,可有下列三种取法:
- 线性探测再散列:di=1,2,3,…,m-1;
- 二次探测再散列:di=12,-12,22,-22,⑶2,…,±(k)2,(k<=m/2);
- 伪随机探测再散列:di=伪随机数序列。
- 优点:
- 记录更容易进行序列化(serialize)操作
- 如果记录总数可以预知,可以创建完美哈希函数,此时处理数据的效率是非常高的
- 缺点:
- 存储记录的数目不能超过桶数组的长度,如果超过就需要扩容,而扩容会导致某次操作的时间成本飙升,这在实时或者交互式应用中可能会是一个严重的缺陷;
- 使用探测序列,有可能其计算的时间成本过高,导致哈希表的处理性能降低;
- 由于记录是存放在桶数组中的,而桶数组必然存在空槽,所以当记录本身尺寸(size)很大并且记录总数规模很大时,空槽占用的空间会导致明显的内存浪费;
- 删除记录时,比较麻烦。比如需要删除记录a,记录b是在a之后插入桶数组的,但是和记录a有冲突,是通过探测序列再次跳转找到的地址,所以如果直接删除a,a的位置变为空槽,而空槽是查询记录失败的终止条件,这样会导致记录b在a的位置重新插入数据前不可见,所以不能直接删除a,而是设置删除标记。这就需要额外的空间和操作。
- Hi=(H(key) + di) MOD m,i=1,2,…,k(k<=m-1),其中H(key)为散列函数,m为散列表长,di为增量序列,可有下列三种取法:
-
再散列法
- Hi=RHi(key),i=1,2,…,k RHi均是不同的散列函数,即在同义词产生地址冲突时计算另一个散列函数地址,直到冲突不再发生,这种方法不易产生“聚集”,但增加了计算时间。
-
链地址法/拉链法(开放散列)
- 链地址法的基本思想是:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来。
- 这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
- 优点:
- 对于记录总数频繁可变的情况,处理的比较好(也就是避免了动态调整的开销);
- 由于记录存储在结点中,而结点是动态分配,不会造成内存的浪费,所以尤其适合那种记录本身尺寸(size)很大的情况,因为此时指针的开销可以忽略不计了;
- 删除记录时,比较方便,直接通过指针操作即可。
- 缺点:
- 存储的记录是随机分布在内存中的,这样在查询记录时,相比结构紧凑的数据类型(比如数组),哈希表的跳转访问会带来额外的时间开销;
- 如果所有的 key-value 对是可以提前预知,并之后不会发生变化时(即不允许插入和删除),可以人为创建一个不会产生冲突的完美哈希函数(perfect hash function),此时封闭散列的性能将远高于开放散列;
- 由于使用指针,记录不容易进行序列化(serialize)操作。
-
建立一个公共溢出区
- 这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。
4.6 hash表的查找性能
查找过程中,关键码的比较次数,取决于产生冲突的多少,产生的冲突少,查找效率就高,产生的冲突多,查找效率就低。因此,影响产生冲突多少的因素,也就是影响查找效率的因素。影响产生冲突多少有以下三个因素:
- 散列函数是否均匀;
- 处理冲突的方法;
- 散列表的装填因子。
- 散列表的装填因子定义为:α= 填入表中的元素个数 / 散列表的长度
- α是散列表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,所以,α越大,填入表中的元素较多,产生冲突的可能性就越大;α越小,填入表中的元素较少,产生冲突的可能性就越小。
4.7 著名的hash算法
MD5 和 SHA-1 可以说是目前应用最广泛的Hash算法,而它们都是以 MD4 为基础设计的。
- MD4
MD4(RFC 1320)是 MIT 的 Ronald L. Rivest 在 1990 年设计的,MD 是 Message Digest 的缩写。它适用在32位字长的处理器上用高速软件实现--它是基于 32 位操作数的位操作来实现的。
- MD5
MD5(RFC 1321)是 Rivest 于1991年对MD4的改进版本。它对输入仍以512位分组,其输出是4个32位字的级联,与 MD4 相同。MD5比MD4来得复杂,并且速度较之要慢一点,但更安全,在抗分析和抗差分方面表现更好
- SHA-1 及其他
SHA1是由NIST NSA设计为同DSA一起使用的,它对长度小于264的输入,产生长度为160bit的散列值,因此抗穷举(brute-force)性更好。SHA-1 设计时基于和MD4相同原理,并且模仿了该算法。
4.8 hash算法在信息安全方面的应用
-
文件校验
- 我们比较熟悉的校验算法有奇偶校验和CRC校验,这2种校验并没有抗数据篡改的能力,它们一定程度上能检测出数据传输中的信道误码,但却不能防止对数据的恶意破坏。
- MD5 Hash算法的"数字指纹"特性,使它成为目前应用最广泛的一种文件完整性校验和(Checksum)算法,不少Unix系统有提供计算md5 checksum的命令。
-
数字签名
- Hash 算法也是现代密码体系中的一个重要组成部分。由于非对称算法的运算速度较慢,所以在数字签名协议中,单向散列函数扮演了一个重要的角色。对 Hash 值,又称"数字摘要"进行数字签名,在统计上可以认为与对文件本身进行数字签名是等效的。而且这样的协议还有其他的优点。
-
鉴权协议
- 如下的鉴权协议又被称作挑战--认证模式:在传输信道是可被侦听,但不可被篡改的情况下,这是一种简单而安全的方法。
5 二叉树
- 每个节点最多只有2个子树的树结构叫二叉树。二叉树有五种基本形态。
- 二叉树4大性质:
- 二叉树的第i层上的结点数目最多为2(i-1),i>=1
- 深度为k的二叉树至多有2k-1个结点,k>=1
- 包含n个结点的二叉树的高度至少为log2(n+1)
- 度为0的结点数为n0,度为2的结点数为n2,则n0=n2+1
6 BST树
-
BST树,Binary Search Tree,二叉查找树、二叉搜索树、排序二叉树。
- 左子树所有结点值小于根结点值,右子树所有结点值大于根结点值;
- 任意结点的左右子树都是BST树;
- 没有键值相等的结点。
-
BST插入操作(从根节点开始递归):
- 新插入的节点与当前节点比较,相同则说明已存在不用重复插入;
- 新插入的节点 < 当前节点,则到当前节点左子树比较,直到插入成功;
- 新插入的节点 > 当前节点,则到当前节点右子树比较,直到插入成功。
-
BST删除操作(从根节点开始递归):
- 待删除节点无子节点,则直接删除;
- 待删除节点只有一个子节点,则直接删除;
- 待删除节点有两个子节点,则先找到待删除节点的替代节点(右子树的最小节点),然后替换节点,再删除替换节点。
-
BST查询操作(递归遍历):
- 深度优先遍历
- 前序遍历:最常见,根左右
- 中序遍历:左根右,整棵BST树采用中序遍历即从小到大排序
- 后序遍历:左右根
- 广度优先遍历/层序遍历:从上层到下层,每层从左到右
- 深度优先遍历
7 AVL树
-
AVL树是平衡的BST,在BST的特点上还有如下特点:
- AVL树的左右子树高度之差的绝对值不超过1;
- AVL树的左右子树都是AVL树
- AVL树任意节点的平衡因子只能是-1/0/1,平衡因子=右子树高度-左子树高度
AVL树经历插入或删除操作后,如果平衡条件被破坏,需要进行旋转操作来恢复平衡。
8 红黑树
-
红黑树是自平衡的BST。红黑树有5大特点:
- 节点有红色或黑色;
- 根节点是黑色;
- 叶子结点是黑色;
- 每个红色节点的子节点都是黑色;
- 从任意节点到其叶子结点的所有路径都包含相同数目的黑色节点。
红黑树通过变色、左旋、右旋来保持平衡,任何不平衡都会在三次旋转之内解决。
红黑树通过为节点增加颜色换取增删节点时候旋转次数的降低,AVL树旋转次数比红黑树要多,红黑树的插入效率更高。
红黑树的左旋操作(左比右低)
- 红黑树的右旋操作(左比右高)
-
红黑树增加节点:
按照BST的方式,将节点插入;
-
将插入的节点着色为红色
- 插入的节点是根节点:直接涂为黑色
- 插入的节点的父节点是黑色:什么也不做
- 插入的节点的父节点是红色:说明插入节点存在非空祖父节点和叔叔节点,分3种情况讨论
-
一系列旋转和着色,使之成为一颗红黑树。
-
红黑树删除节点:
- 按照BST的方式,将节点删除;
- 一系列旋转和着色,使之成为一颗红黑树:
- 被删除节点是红+黑:直接涂为黑色;
- 被删除节点是黑+黑,且是根节点:什么都不做
- 被删除节点是黑+黑,且不是根节点:分4种情况讨论。
-
9 B Tree
- 数据库索引文件存储在磁盘上,当数据量比较大的时候,索引文件很大,不能一次性装载到内存,只能每次装载磁盘页,磁盘页里面就是索引树的节点。
- 每次磁盘IO就是将树的节点装载到磁盘页加载到内存,BST在最坏情况下,磁盘IO次数等于索引树的高度。
- 为了减少磁盘IO次数,要把树从"瘦高"变得"矮胖",BST是二叉查找树,B树是多路平衡查找树。B树的比较次数不比BST少,但是相对磁盘IO的速度,内存中比较耗时几乎可以忽略,只要树的高度足够低,磁盘IO次数足够小,就可以提升查找性能。
- B树每个节点最多包含m个孩子,m称为B树的阶,m的大小取决于磁盘页的大小。一个m阶的B树具有如下几个特征:
- 根节点至少有两个孩子;
- 每个叶子结点都包含k-1个元素,其中k取值[m/2,m];
- 所有的叶子结点都处于同一层;
- 每个节点中的元素从小到大排列;
- 每个中间节点都包含k-1个元素和k个孩子,其中k取值[m/2,m];
10 B+ Tree
- B+树是B树的变体,比B树有更高的查询性能,在B树基础上还有如下特点:
- 非叶子结点不保存数据只用于索引,所有数据都保存在叶子结点,叶子结点之间指针相连,叶子结点元素从小到大排序;
- 有k个子树的中间节点包含有k个元素(B树中是k-1个元素);
- B+树中间节点只用来索引,所以同样大小的磁盘页可以容纳更多的节点元素,数据量相同情况下,B+树的结构比B-树更加"矮胖",因此查询时IO次数更少,查询效率更高。
- B+树每次查询必须到叶子结点结束,B树只要匹配到元素即可,因此B+树的查找比B树更稳定。
- B+树所有叶子节点形成有序链表,便于范围查询。
11 线段树
12 Trie Tree(字典树)
13 并查集(Union Find)
14 SkipList(跳表)
-
SkipList,跳表/跳跃表,是一种空间换时间的算法,是一种随机化的数据结构,跳表的原理比较简单,但是效率和红黑树及AVL树不相上下,开源软件Redis和LevelDB都有使用到跳表。
- 跳表由很多层结构组成;
- 每一层都是一个有序的链表;
- 最底层(Level 1)链表包含所有的元素;
- 如果一个元素出现在Level i的链表中,则它一定也会出现在Level i下层的链表中;
- 每个节点都包含两个指针,一个指向同一层链表中的下一个元素,一个指向下面一层的元素。
丢硬币决定K,随机变量K满足参数为p=1/2的几何分布,K的期望值为2层。
跳表的高度等于n次实验中产生的最大K
跳表的空间复杂度:每个元素的期望高度是2,一个大小为n的跳表,其节点数目的期望值是2n
-
跳表的查询
- 从top层开始,依次跟链表中元素比较;
- 待查找元素比当前节点大,同时比链表中最大值小的时候,跳到下一层继续比较,依次进行;
- 最坏情况会到Level 1层比较,直到查找到所需元素,否则没有此元素。
-
跳表的插入
- 先确定待插入元素会插入到第K层,K是丢硬币随机决定的;
- 然后在第K层的以下各层都插入该元素;
- 如果K大于跳表的层数,则要添加新的层。
-
跳表的删除
- 先在每一层查找到待删除的节点;
- 然后在每一层使用链表中删除节点的方式删除该节点。
15 位图(bitmap)
位图通常基于数组来实现,数组每个元素存储一个数据,该元素只占一个bit位,用一个bit标识该元素是否存在,可以大大节省内存空间。
-
位图常用业务场景:
- 布隆过滤器bloom filter
- 快速去重
- 快速排序
- 快速查询
-
Bitmap实现快速去重
- 2.5亿个整数中找出不重复的整数的个数,内存空间不足以容纳这2.5亿个整数。 首先,根据“内存空间不
足以容纳这2.5亿个整数”我们可以快速的联想到Bit-map。 - 下边关键的问题就是怎么设计我们的Bit-map来表示这2.5亿个数字的状态了。其实这个问题很简单,一个数字的状态只有三种,分别为不存在,只有一个,有重复。因此,我们只需要2bits就可以对一个数字的状态进行存储了,假设我们设定一个数字不存在为00,存在一次01,存在两次及其以上为11。那我们大概需要存储空间几十兆左右。 接下来的任务就是遍历一次这2.5亿个数字,如果对应的状态位为00,则将其变为01;如果对应的状态位为01,则将其变为11;如果为11,,对应的转态位保持不变。
- 最后,我们将状态位为01的进行统计,就得到了不重复的数字个数,时间复杂度为O(n)。
- 2.5亿个整数中找出不重复的整数的个数,内存空间不足以容纳这2.5亿个整数。 首先,根据“内存空间不
-
Bitmap实现快速排序
- 假设我们要对0-7内的5个元素(4,7,2,5,3)排序(这里假设这些元素没有重复),我们就可以采用Bit-map的方法来达到排序的目的。要表示8个数,我们就只需要8个Bit(1Bytes),首先我们开辟1Byte的空间,将这些空间的所有Bit位都置为0, 对应位设置为1:
- 遍历一遍Bit区域,将该位是一的位的编号输出(2,3,4,5,7),这样就达到了排序的目的,时间复杂度O(n)。
- 优点: 运算效率高,不需要进行比较和移位;占用内存少,比如N=10000000;只需占用内存为N/8=1250000Byte=1.25M。
- 缺点: 所有的数据不能重复。即不可对重复的数据进行排序和查找。
-
Bitmap实现快速查询
- 利用Bit-map也可以进行快速查询,这种情况下对于一个数字只需要一个bit位就可以了,0表示不
存在,1表示存在。假设上述的题目改为,如何快速判断一个数字是够存在于上述的2.5亿个数字集合中。 - 首先我们先对所有的数字进行一次遍历,然后将相应的转态位改为1。遍历完以后就是查询,由于我们的Bit-map采取的是连续存储(整型数组形式,一个数组元素对应32bits),我们实际上是采用了一种分桶的思想。一个数组元素可以存储32个状态位,那将待查询的数字除以32,定位到对应的数组元素(桶),然后再求余(%32),就可以定位到相应的状态位。如果为1,则代表改数字存在;否则,该数字不存在。
- 利用Bit-map也可以进行快速查询,这种情况下对于一个数字只需要一个bit位就可以了,0表示不
-
Bitmap实现布隆过滤器(Bloom Filter)
- Bloom Filter减少误判概率方法:
- 让Bitmap的空间更大,hash次数更大(一般是8次),空间和准确率上折中;
- 加一个白名单,专门存储误判的URL或邮箱等数据。
- Bloom Filter减少误判概率方法: