数据结构

数组

基础概念

  • 将数据码成一排进行存放。

  • 连续的,支持根据index快速检索数据(随机访问),但是对于任意位置添加或任意位置删除时间复杂度存在最好和最坏的情况。

  • 栈是一种线性结构,相比数组,栈对应的操作是数组的子集,只能从一端添加元素,也只能从一端取出元素,此端为栈顶。

  • 后进先出的数据结构,Last In First Out(LIFO)

  • 程序调用的系统栈,A方法调用B方法调用C方法,AB都在系统栈中,当C方法调用完后,下次调用的是B方法。

  • 可以利用数组和队列来实现,利用数组实现可以向数组尾部插入数据,并且从尾部读取数据。

队列

  • 队列是一种线性结构,底层可以用动态数组实现,是一种FIFO(先进先出)的数据结构。

循环队列

  • 利用index来记录队头和队位标示来记录队列的情况。

链表

  • 线性结构,动态的数据结构不需要处理固定容量的问题,更深入的理解引入(或者指针)。

  • 深入理解的递归结构,数据存储在"节点(Node)"中

  • 不能够随机访问的能力

二分搜索树(非线性数据结构)

特点

  • 二叉树具有天然的递归结构

    • 每个节点的左子树也是二叉树

    • 每个节点的右子树也是二叉树

  • 二分搜索数时二叉树

    • 二分搜索数的每个节点的值都大于其左子树所有节点的值,小于其右子树的所有节点的值。

    • 每颗子树也是一个二分搜索树

遍历

前序遍历

中序遍历

  • 中序遍历后得到的元素是顺序排列的

后序遍历

层序遍历

  • 根据树的深度去遍历(BFS,广度优先遍历),利用队列一层一层进行遍历,能够更快的找到搜索的元素

  • 解决算法设计中的最短路径问题

删除节点

删除左右都有孩子的节点d

  • 找到s=min(d->right),s是d的后继

  • s->right=delMin(d->right)

  • s->left=d->left

  • 删除d后,d的后继就是新的d,使用前驱也可以

堆和优先队列

优先队列

  • 普通队列:先进先出;后进后出

  • 出队顺序和入队顺序无关;和优先级相关

  • 二叉堆是一颗满二叉树

  • 堆中某节点的值总是不大于其父节点的值,最大堆

  • 父亲节点index是(childIndex-1)/2,LeftChildIndex=parentIndex * 2+1,RightChildIndex=parentIndex * 2+2

线段树

  • 每个节点表示的都是一个区间内的数据,子节点都是区间的拆分,直到最终的子叶节点存储的为一个长度的区间

  • 线段树不是完全二叉树,也不一定是满二叉树

  • 线段树是平衡二叉树,可以用数组来表示。

字典树

  • 每个节点有26个指向下个节点的指针

并查集

概念

  • 由孩子节点指向父亲节点。

  • 主要用于解决俩个节点的连接问题。

实现

Quick Union Find

Tree Uinon Find

rank和路径压缩

AVL树

平衡二叉树

  • 对于任意一个节点,左子树和右子树的高度差不能为超过1。

  • 标注节点的高度,计算平衡因子,平衡因子为高度差,一旦其绝对值大于等于2则不为平衡二叉树。

左旋转和右旋转

  • 添加节点后,沿着节点向上维护平衡性。

右旋转

  • LL:新插入节点在left子树的left

  • 插入的元素在不平衡的节点的左侧的左侧,此时可以使用右旋转保证树的平衡

  • 将原本的根节点y顺时针旋转到x的右子树,x为新的根节点。

左旋转

  • RR:插入的节点在right树的right

  • 插入的元素在不平衡的节点的右侧的右侧

LR

![](./img/AVL LR.jpg)

![](./img/AVL LR2.jpg)

  • 新插入的节点在Left子树的Right侧。

  • 先对x节点左旋转后,转换为LL,在进行右旋转。

RL

![](./img/AVL RL1.jpg)

![](./img/AVL RL2.jpg)

  • 先对x节点进行右旋转,转换成了RR,再进行左旋转。

红黑树

特点

  • 每个节点或者是红色或者是黑色

  • 根节点是黑色

  • 每一个叶子结点(最后的空节点)是黑色

  • 如果一个节点是红色的,那么它的孩子节点都是黑色的

  • 从任意一个节点到叶子节点,经过的黑色节点是一样的

  • 红黑树是保持"黑平衡"的二叉树,严格意义上不是平衡二叉树,最大高度是:2logn O(logn)

2-3树

特点

  • 满足二分搜索树的基本性质

  • 节点可以存放一个元素或者两个元素

红黑树和2-3树

  • 每个三节点都会产生一个红色节点。

哈希表

哈希函数的设计

  • 整型

    • 小范围正整数直接使用

    • 小范围负整数进行偏移

原则

  • 一致性:如果a==b,hash(a)==hash(b)

  • 高效性:计算高效简便

  • 均匀性:哈希值均匀分布

解决哈希冲突

链表地址法

BitMap分析1

实现原理

​ 在java中,一个int类型占32个比特,我们用一个int数组来表示时未new int[32],总计占用内存32*32bit,现假如我们用int字节码的每一位表示一个数字的话,那么32个数字只需要一个int类型所占内存空间大小就够了,这样在大数据量的情况下会节省很多内存。

具体思路:

 1个int占4字节即4*8=32位,那么我们只需要申请一个int数组长度为 int tmp[1+N/32]即可存储完这些数据,其中N代表要进行查找的总数,tmp中的每个元素在内存在占32位可以对应表示十进制数0~31,所以可得到BitMap表:

    tmp[0]:可表示0~31

    tmp[1]:可表示32~63

    tmp[2]可表示64~95

    .......

  那么接下来就看看十进制数如何转换为对应的bit位:

  假设这40亿int数据为:6,3,8,32,36,......,那么具体的BitMap表示为:

bitMap.jpg

  如何判断int数字在tmp数组的哪个下标,这个其实可以通过直接除以32取整数部分,例如:整数8除以32取整等于0,那么8就在tmp[0]上。另外,我们如何知道了8在tmp[0]中的32个位中的哪个位,这种情况直接mod上32就ok,又如整数8,在tmp[0]中的第8mod上32等于8,那么整数8就在tmp[0]中的第八个bit位(从右边数起)。

bitMap应用

  1. 看个小场景 > 在3亿个整数中找出不重复的整数,限制内存不足以容纳3亿个整数。

    对于这种场景我可以采用2-BitMap来解决,即为每个整数分配2bit,用不同的0、1组合来标识特殊意思,如00表示此整数没有出现过,01表示出现一次,11表示出现过多次,就可以找出重复的整数了,其需要的内存空间是正常BitMap的2倍,为:3亿*2/8/1024/1024=71.5MB

  具体的过程如下:

扫描着3亿个整数,组BitMap,先查看BitMap中的对应位置,如果00则变成01,是01则变成11,是11则保持不变,当将3亿个整数扫描完之后也就是说整个BitMap已经组装完毕。最后查看BitMap将对应位为11的整数输出即可。

  1. 已知某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数。

    8位最多99 999 999,大概需要99 999 999个bit,大概10几m字节的内存即可。 (可以理解为从0-99 999 999的数字,每个数字对应一个Bit位,所以只需要99M个Bit==1.2MBytes,这样,就用了小小的1.2M左右的内存表示了所有的8位数的电话)  

bitMap问题

 BitMap 的思想在面试的时候还是可以用来解决不少问题的,然后在很多系统中也都会用到,算是一种不错的解决问题的思路。

  但是 BitMap 也有一些局限,因此会有其它一些基于 BitMap 的算法出现来解决这些问题。

  • 数据碰撞。比如将字符串映射到 BitMap 的时候会有碰撞的问题,那就可以考虑用 Bloom Filter 来解决,Bloom Filter 使用多个 Hash 函数来减少冲突的概率。

  • 数据稀疏。又比如要存入(10,8887983,93452134)这三个数据,我们需要建立一个 99999999 长度的 BitMap ,但是实际上只存了3个数据,这时候就有很大的空间浪费,碰到这种问题的话,可以通过引入 Roaring BitMap 来解决。

BitMap分析2

  • bitMap是位图,其实准确的来说,翻译成基于位的映射,举一个例子,有一个无序有界int数组{1,2,5,7},初步估计占用内存4*4=16字节,这倒是没什么奇怪的,但是假如有10亿个这样的数呢,10亿4字节/(1024 * 1024 * 1024)=3.72G左右(1GB=1024MB 、1MB=1024KB 、1KB=1024B 、1B=8b)。如果这样的一个大的数据做查找和排序,那估计内存也崩溃了,有人说,这些数据可以不用一次性加载,那就是要存盘了,存盘必然消耗IO。我们提倡的是高性能,这个方案直接不考虑。

问题分析

  如果用BitMap思想来解决的话,就好很多,解决方案如下:

  • 一个byte是占8个bit,如果每一个bit的值就是有或者没有,也就是二进制的0或者1,如果用bit的位置代表数组值有还是没有, 那么0代表该数值没有出现过,1代表该数组值出现过。不也能描述数据了吗?具体如下图:

img
  • 现在假如10亿的数据所需的空间就是3.72G/32了吧,一个占用32bit的数据现在只占用了1bit,节省了不少的空间,排序就更不用说了,一切显得那么顺利。这样的数据之间没有关联性,要是读取的,你可以用多线程的方式去读取。时间复杂度方面也是O(Max/n),其中Max为byte[]数组的大小,n为线程大小。

应用与代码实现

  • 一个数怎么快速定位它的索引号,也就是说搞清楚byte[index]的index是多少,position是哪一位。举个例子吧,例如add(14)。14已经超出byte[0]的映射范围,在byte[1]范围之类。那么怎么快速定位它的索引呢。如果找到它的索引号,又怎么定位它的位置呢。Index(N)代表N的索引号,Position(N)代表N的所在的位置号。

add(int num)

  • 你要向bitmap里add数据该怎么办呢,不用担心,很简单,也很神奇。上面已经分析了,add的目的是为了将所在的位置从0变成1.其他位置不变.

img

clear(int num)

  • 对1进行左移,然后取反,最后与byte[index]作与操作。

img

contain(int num)

img

完整代码

稀疏位图

位图(bitmap/bitset)在工程中应用广泛(如搜索引擎中的posting list),同时也是面试中一个重要考点

位图在求交(introsect),求并(union)计算时有很好的性能,但如果数据集分布稀疏时,也会浪费较多空间。例如,当数据取值范围为[0, 2^32-1],数据个数在1000w左右时,位图占用512MB,其中0的个数只占0.2%,空间浪费相当严重。

为了解决空间浪费,显然位图需要进行压缩。Daniel Lemire的Roaring Bitmaps是众多压缩(稀疏)位图实现中的性能最好的一种:

  1. 支持动态修改位图(静态的位图有其它压缩方式)

  2. 利用SIMD加速位图操作

Roaring的数据结构

  • Roaring Bitmaps明确面向稀疏位图,使用之前你要明确数据的分布/数量,否则Roaring可能占用比未压缩位图更多的空间和耗费更多的操作时间。

  • Roaring Bitmaps思路是将取值范围分片,每片大小相同,可容纳2^16个数。根据范围内实际数据的个数/分布选择最紧凑的存储形式:

    • 区间内数据较多,且分布零散,则选择(未压缩)位图

    • 区间内数据较少,且分布零散,则选择使用有序数组

    • 区间内数据连续分布,则选择用Run Length Encoding编码

img
  • Roaring将Bitmap从一层的连续存储,转换为一个二级的存储结构

    • 第一层称之为Chunk,每个Chunk表示该区间取值范围的base(n2^16, 0<= n < 2^16),如果该取值范围内没有数据则Chunk不会创建

    • 第二层称之为Container,会依据数据分布进行创建(Container内的值实际是区间内的offset)

      • Roaring并不是一种静态数据结构,随着数据的增删,Container选择的存储格式也会随之自动调整

  • 这里另一个重要参数是如何依据数据个数来判定使用位图还是数组。Roaring 选择4096作为阈值。

    • 当区间内数量少于4096时,数组占用更紧凑;多于4096,则使用位图更经济

img

Roaring 提供O(logn)的查找性能:

  • 首先二分查找key值的高16位是否在分片(chunk)

  • 如果分片存在,则查找分片对应的Container是否存在

    • 如果Bitmap Container,查找性能是O(1)

    • 其它两种Container,需要进行二分查找

Roaring的性能

  • Roaring使用了SIMD对操作进行了加速,但是SIMD是一门相当trick的技术,有机会可以单独再分析SIMD对性能的影响(尤其是introset/union)。

  • Lucene在5.0中引入了Roaring缓存filter,并对Roaring进行了性能评测,得到了一些有趣的结论。

遍历

img
  • 当数据比较稀疏时,Roaring相比Bitmap性能更好,但弱于int数组

  • 当数据比较密集时,Roaring性能会比Bitmap差,最好的是int数组

内存占用

img
  • 数据极其稀疏时,int数组更节省空间,但到了某一拐点后,Roaring会更加节省空间

  • 数据很密集时,int数组占用空间呈线性增长会快速超越Bitmap占用,而Roaring甚至可能由于其紧凑的编码格式(Run Length Encoding)反而比Bitmap更节省空间

构建时间

img
  • 当数据极其稀疏时,位图构建速度远远快过int数组和Roaring

  • 当数据超过全量1%时,Roaring成为构建速度更快的格式

skiplist

  • skiplist就是普通单向链表的基础上增加了分层索引,可以快速地根据分层索引定位数据。

查找

  • 比如我们要查找key为19的结点,那么我们不需要逐个遍历,而是按照如下步骤:

    • 从header出发,从高到低的level进行查找,先索引到9这个结点,发现9 < 19,继续查找(然后在level==2这层),查找到21这个节点,由于21 > 19, 所以结点不往前走,而是level由2降低到1

    • 然后索引到17这个节点,由于17 < 19, 所以继续往后,索引到21这个结点,发现21>19, 所以level由1降低到0

    • 在结点17上,level==0索引到19,查找完毕。

    • 如果在level==0这层没有查找到,那么说明不存在key为19的节点,查找失败

插入

  • 其实插入节点的关键就是找到合适的插入位置,即从所有小于待插入节点key值的节点中,找出最大的那个,所以插入节点的过程如下:

    • 查找合适的插入位置,比如上图中要插入key为17的结点,就需要一路查找到12,由于12 < 17,而12的下一个结点19 > 17,因而满足条件

    • 创建新结点,并且产生一个在1~MAX_LEVEL之间的随机level值作为该结点的level

    • 调整指针指向

移除

  • 移除结点其实很简单,就分以下3步:

    • 查找到指定的结点,如果没找到则返回

    • 调整指针指向

    • 释放结点空间

最后更新于