一、数组
(一)什么是数组
数组是一种线性表数据结构,它用一组连续的内存空间,来存储一组具有相同类型的树。
1、线性表,分为顺序表和链表,除了数组链表,队列,栈都是线性表结构。
顺序表:存储方式(连续的存储单元),时间复杂度(根据下表O(1)查询,插入和删除是O(n)),空间性能(初始化时,就要确定长度,大了浪费,小了发生上溢,不支持扩容)。
链表:存储方式(采用链式存储,一组任意的存储单元存储数据),时间复杂度(查找O(n),插入和删除O(1)),空间性能(初始化不需要确定长度,按实际需要,支持扩容)。
线性表(数组,队列,链表,栈)。与之相对应的是非线性表,比如二叉树,堆,图等,非线性表中的数据 之间并不是简单的前后关系。
2、连续的内存空间和相同类型的数据,数组的连续性使得数组具有随机访问的特性,但是新增和插入需要大量的搬移工作。
(二)数组是如何实现根据下表访问数组元素的
计算机会给每个内存单元分配一个地址,计算机通过地址来访问内存中的数据。但计算机需要随机访问数组中的某个元素时,他会首先通过寻址公示,获得存储地址。
a[i]_address = base_address + i * data_type_size
其中data_type_size表示数组中每个元素的大小,data_type_size是数组中存储的数据自己字节大小。
数组根据下表访问的时间复杂度为O(1),查找时间复杂度是O(lgn)。
(三)为什么大多数编程语言都是从0开始编号,而不是开始的?
从数组存储的内存模型上看,[下标]最确切的定义应该是[偏移(Offset)]。如果用数组a来表示数组的首地址,a[0]就是偏移为0的位置,也就是首地址,a[k]就是偏移k个type_size
的位置,所以计算a[k]的内存地址只需要用下面这个公式
a[k]_address = base_address + k * type_size
但是,如果从1开始计数,那我们计算数据元素a[k]的内存地址就会变为
a[k]_address = base_address + (k-1)*type_size
对比这两个公式可以发现,从1开始,每次随机访问都多了一次减法运算,对cpu来说,就是多了一次减法指令。数组作为非常基础的数据结构,通过下标随机访问数组元素又是非常基础的,效率优化当然也要极致。
所以为了减少一次减法操作,选择从0开始。
(四)低效率的插入和删除
为了保证数据的连续性,会到导致效率低下。
插入操作
如果在数组的末尾插入元素,就不需要移动数据了,最好的时间复杂度为O(1);如果在数组开头插入元素,那所有元素都要依次往后移动一位,最坏的时间复杂度是O(n)。因为数组中的每个位置插入元素的
概率都是一样的,所以平局情况时间复杂度为O(n)。如果数组中的数据是有序的,在某个位置插入一个新的元素,就必须按照刚才的方法搬移K之后的数据。如果数组中的数据没有任何规律,数组只被当做一个存储
数据的集合,那么在数组第K个位置插入某个数据,为了避免大规模的搬移,可以直接将第K为的数据搬移到数组的最后,把新的元素放在第ke个位置。利用这种技巧,在特定的场景下,在第K位置插入一个元素的
时间复杂度将变为O(1)。
删除操作
和插入类似,如果删除数据末尾的数据,则最好的时间复杂度为O(1);如果删除首地址的数据则时间复杂度为O(n),平均时间复杂度为O(n)。在某些特殊场景下,并一定非得追求数据总数据的连续性。
可以将数据多次删除操作集中在一起执行,删除的效率会提高。
这其实JVM标记-清除算法的核心思想。
首先标记所有需要回收的对象
在标记完成后统一回收所有被标记的对象,其中判断对象是否需要回收用的是可达性分析算法,基本思路是通过一系列的“GC Roots”的对象作为起始点,从这些节点开始向下搜素,搜索走过的路径
就是引用链,当一个GC Roots对象没有任何引用链时,则证明此对象不可用,JVM会将这些对象回收。
在java中可作为GC Roots的对象包括下面几种:
虚拟机栈(栈帧中的本地变量)中的引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地房发展中JNI(Native)引用的对象。
(五)数组的访问越界问题
Java语言做了越界检查,会抛出异常,java.lang.ArrayIndexOutOfBoundsException。并终止方法运行。不然会出现死循环。
二、栈
(一)什么是栈
先进后出(FILO),典型的栈结构。
从栈的操作性上看,栈是一种操作受限的线性表,只允许在一端进行插入和删除操作。
(二)如何实现一个栈
栈主要包括,入栈和出栈,也就是在栈顶插入和删除数据。实际上,栈既可以用数组来实现,也可以用链表来实现。数组实现的栈叫做顺序栈;链表实现的栈,叫做链式栈。
数组实现(入栈下标++,出栈--下标)
链表实现(入栈进入链表头,出栈从列表头出)
(三)实现一个特殊的栈,在基本功能的基础上,在实现返回栈中最小元素功能
pop,push,getMin操作的时间复杂度都是O(1)
设计栈类型可以使用现成的栈结构
1、第一种
压入数据规则,假设当前数据为value,先将其压如stackData。然后判断stackMin是否为空。如果为空,则value也压入stackMin。如果不为空,则比较value和stackMin的栈顶元素中哪个更小
如果value<=this.getMin(),则value压入stackMin。反之不做任何操作。
2、第二种
压入数据规则,假设当前数据为 value,先将其压入 stackData。然后判断 stackMin 是否为空:如果为空,则 value 也压入 stackMin。如果不为空,则比较 value 和 stackMin 的栈顶元素
中哪一个更小:如果 value <= this.getMin(),则 value 压入 stackMin;反之,则把 stackMin 栈顶元素重复压入 stackMin,即在栈顶元素上再压入一个栈顶元素。
三、队列
(一)什么是队列
队列是数据结构中比较重要的一种类型,它支持先进先出(FIFO),尾部添加,头部删除。和栈一样也是操作受限的线性表
(二)队列的种类
单队列,单队列就是常见的队列,每次队尾添加,队头删除,存在“假溢出”问题,也就是明明有位置却不能添加。
循环队列(避免“假溢出”)
(三)如何实现一个队列
1、顺序队列
队列需要两个指针,一个head指针,指向对头,一个tail指针,指向队尾。
当tail移动到最右边的时候,即时数组有空闲空间,也无法继续往队列中添加数据。当head=0,tail移动最右边时,数组无可用空间,其他清空,都有。如果出现这种情况,只需要在入队的时候
集中进行一次搬移操作。
2、链式队列
基于链表的实现,我们同样需要连个指针:head和tail。它们分别指向链表的第一个结点和最后一个结点。如图所示:入队时,tail.next = new Node(); tail = tail.next;出队时,head = head.next;。
3、循环队列
如果顺序队列将数据组收尾相连,形成一个环,就构成了循环队列。
四、链表
(一)什么是链表
相对于数组,链表是一种稍微复杂的数据结构,和数组同级。比如ArrayList其实原理就是数组。而LinkedList实现原理就是链表,链表在循环遍历时效率不高,但插入删除优势明显。
(二)几种常见的链表
单链表
双向链表
循环链表
五、散列表
(一)什么是散列表
散列表(Hash table 也叫哈希表)是一种查找算法,与链表,树等算法不同的是,散列算法在查找时不需要进行一系列和键值(关键字是数据元素中某个数据项的值)的比较操作。
散列算法希望能尽量做到不经过任何比较,通过一次存取就能得到所查找的数据元素。因而要在数据元素的存储位置和它的Key之间建立一个确定的对应关系,是每个Key和散列中一个唯一
存储位置相对应。因此在查找时,只要根据这个对应关系找到给定Key在散列中的位置即可。
(二)散列函数
直接定制法:取Key或Key的某个线性函数值为散列地址。即:h(key)=key或h(key)=a*key+b;
数据分析法:
平方取值发:取Key平方后的中间几位散列地址
折叠法:将Key分成位数相同几部分,然后去这几部分的叠加和作为散列地址。
除留余数法:取Key被某个不大于散列表表长M的数p除后所得的余数为散列地址,即h(Key)=key Mod p p<=m
随机数发:选择随机数,取Key的随机函数值为它的散列地址。即h(Key)=random(Key)。
(三)散列冲突
再好的散列函数也无法避免冲突,常用的冲突解决方法有两种,开发寻址和链表法。
1、开放寻址法:核心思想是如果出现了散列冲突,就重新探测一个空闲位置将其插入。如何探测新的位置有线性探测,二次探测和双重探测。
线性探测,如果位置已经被占用,则从当前位置开始,依次往后查找,直到找到空闲位置为止。
二次探测,二次探测和线性探测很像,线性探测每次探测的步长为1,而二次探测的步长就变成了原来的二次发。
双重探测,使用两个散列函数,如果第一个被占用,则用第二个在散列一次。
2、链表法:
链表法是一种更加常用的散列冲突解决方案,他要简单的多。在散列表中,每个[桶]或者[槽]会对应一条链表,所有散列值相同的元素都会放在相同槽位对应链表中。
六、排序二叉树
(一)什么是排序二叉树
首先如果普通二叉树每个节点满足:左子树所有节点小于它的根节点,且有字数所有节点大于它的根节点,则这样的二叉树为排序二叉树。
(二)操作
插入操作:首先要从根节点可爱是往下插座自己要插入的位置;具体流程是:新节点与当前节点比较,如果相同则表示已经存在且不能再重复插入;如果小于当前节点,则在左子树中寻找,如果左子树为空则当前节点为要找的
父节点,新节点插入到当前节点的左子树即可;如果大于当前节点,则到右子树中寻找,如果右子树为空则当前节点为要找的父节点,新节点插入到当前节点的右子树即可。
删除操作:删除操作分三种情况,即要删除的节点无子节点,要删除的节点只有一个子节点,要删除的节点有两个子节点。1.对于要删除的节点无子节点可以直接删除,即让其父节点将孩子节点置空。2.对于要删除的节点有
一个子节点,则替换要删除的节点为其子节点。3.对于要删除的节点有两个子节点,则首先找该节点的替换节点(即右子树中最小节点),接着替换要删除的节点为替换节点,然后删除替换节点。
查询操作:查询操作主要流程为:先和根节点比较,如果相同就返回,如果小于根节点则到左子树中递归查找,如果大于根节点则到右子树中递归查找。因此排序二叉树中可以很容易获取到最大(最优最深子节点)最小
(最左最深子节点)值。
七、红黑树
(一)什么是红黑树
R-B Tree,它是一种特殊的二叉树,平衡二叉树。红黑树的每个节点上都有存储位标识节点颜色,可以是红或黑。
(二)红黑树特性
每个节点或者黑色或者红色。根节点是黑色。每个为空的叶子节点是黑色。如果一个节点是红色的,则它的子节点必须是黑色的。从任一节点到其每个叶子节点的所有路径包含相同数目的黑色节点。从根节点到叶子节点
的最长路径不会超过最短路径的2倍。
左旋:对X进行左旋,意味着,将“x的右孩子”设为“x的父亲节点”;即,将x变成了一个左节点(x成了为z的左孩子)。因此左旋中的“左”意味着“被旋转的节点将变成一个左节点”
右旋:对x进行右旋,意味着,将“x的左孩子”设为“x的父亲节点”;即,将x变成一个右节点。因此右旋中“右”意味着“被旋转的节点将变成一个右节点”。
插入:
1、将红黑树当做一颗二叉查找树,将节点插入。
2、将插入的节点着色为“红色”。
3、通过一系列旋转或着色,使之重新成为红黑树。
删除:
1、将红黑树当做二叉树,将节点删除
2、通过一系列旋转或着色,使之重新成为红黑树。
红黑树的应用,TreeMap,TreeSet以及jdk1.8HashMap底层。
八、B-Tree与B+Tree
(一)B-Tree(文件系统)
B-Tree又叫做平衡多路查找树。一棵m阶的B-Tree特性如下
1.树中每个节点至多有m-1个子节点。
2.除根节点和叶子节点外,其他每个几点至少有ceil(向上取整)(m/2)-1个子节点。
3.若根节点不是叶子节点,则至少有2个子节点。
4.所有叶子节点都出现在同一层。
5.每个节点中的元素key从小到大排列,元素key的左节点的所有元素key值都小于等于元素Key,右节点的所有元素key值都大于等于元素key。
6.插入数据,向兄弟节点借,兄弟节点不够则向父节点借。
(二)B+Tree(Mysql索引)
B+Tree是B-Tree的优化,使其更适合实现外存储索引结构。等同数量的数据存储到B+Tree,树的高度更低,查询效率更高。
1.所有非叶子节点只存储key信息,不存储data(数据信息)
2.所有data都存储在叶子节点上
3.所有叶子节点之间都有一个链指针。
4.所有叶子节点包含了全部元素(key+data)的信息。
(三)一棵m阶的B+Tree与B-Tree的差异
1.同等树高时,B-Tree查询效率比B+Tree高。由于B-Tree节点中存储了Key+data的整个信息,找到Key就等于获取了数据。而B+Tree的data只存储在叶子节点,因此要遍寻到叶子节点才能获取到data信息。
2.同等数量是,B+Tree查询效率比B-Tree高。由于B+Tree的非叶子几点只存储Key信息,而B-Tree的节点存储了Key+data,而每个页(一个节点)的大小是固定的,所以B+Tree的树高会更低一下。数据量越大,B+Tree的层高优势越明显。
3.查找大于或者等于某个key(关键字)的数据时,B+Tree的效率远高于B-Tree,由于B+Tree的叶子节点存储了所有key+data信息,而且叶子节点之间有链指针,所有查询的时候B+Tree更快。
九、LSM树
B+Tree最大的性能问题是会产生大量的磁盘随机IO,大量的磁盘随机IO会严重影响索引建立的速度。
对于那些索引数据大的情况,插入速度是对性能影响的重要指标,而读取相对来说就比较少。譬如在一个无缓存的情况下,B-Tree首先要进行一次磁盘读写将数据读取到内存中,然后进行修改,最后在进行一次IO写会到磁盘中。
为了克服B+Tree的弱点Hbase引入了LSM树。LSM Tree采用读写分离的策略,会优先保真写操作的性能;其数据首先存储内存中,而后需要定期flush到磁盘中。Lsm-Tree通过内存插入与磁盘的顺序写,来达到最优的写性能,因为这会大大
降级磁盘的寻道次数,一次磁盘IO可以写入很多个索引块,Hbase等都是基于LSM-Tree来构建索引的数据库;一般而言LSM-Treed的写更加高效(追加顺序写),B-Tree的读更加高效。L
LSM-Tree的三个组件SSTable(保存在磁盘上的树节点),Memtable(保存在内存中的树节点),Write-Ahead-log(防止内存刷到磁盘数据丢失,在磁盘上维护了一个只追加写的log文件)
十、位图
位图的原理就是用一个bit来标识一个数字是否存在,采用一个bit来存储一个数据,这样可以大大节省空间。bitmap是很常用的数据结构,比如用于bloom filter中;用于无重复整数的排序等等。bitmap通常基于数组来实现,数组中每个元素可以
看成一系列二进制数,所有元素组成更大的二进制集合。
十一、BFS与DFS
BFS(广度优先遍历,Brandth First Search)与DFS(深度优先遍历,Depth First Search)是遍历树或图的两种常用方法。
DFS是以深度,不断去查找是否有下级节点,如果有就继续递归向下查找,否则回到上级,再由未遍历的下级节点进入。
BFS是以广度,从一个节点,查找出它的所有子节点,再依次从所有子节点中向下查找所有子节点。