1.树的基本概念:
度:结点拥有的子树数目称为结点的度。
深度/高度:树中结点的最大层次数称为树的深度或高度。
2.二叉树:
二叉树是n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树组成。
根节点中记录了左子树和右子树的引用。 left - data - right
由二叉树定义以及图示分析得出二叉树有以下特点:
1)每个结点最多有两颗子树,所以二叉树中不存在度大于2的结点。
2)左子树和右子树是有顺序的,次序不能任意颠倒。
3)即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
满二叉树:在一棵二叉树中。如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。
1)叶子只能出现在最下一层。出现在其它层就不可能达成平衡。
2)非叶子结点的度一定是2。
3)在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多。
完全二叉树:对一颗具有n个结点的二叉树按层编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树。
1)叶子结点只能出现在最下层和次下层。
2)最下层的叶子结点集中在树的左部。
3)倒数第二层若存在叶子结点,一定在右部连续位置。
4)如果结点度为1,则该结点只有左孩子,即没有右子树。
5)同样结点数目的二叉树,完全二叉树深度最小。
注:满二叉树一定是完全二叉树,但反过来不一定成立。
二叉树的存储结构:
顺序存储:特点:可能出现不连续,查找快。浪费空间。
链表存储:特点:节省空间,增删快,查找慢。
二叉树的遍历:遍历的主体是围绕根节点。 前序遍历、中序遍历、后序遍历、层序遍历。
3.二叉查找树:
二叉查找树的演变:二叉查找树结合了数组和链表的优点:达到了高性能快速的查找、还达到了数据的高性能增删特点。
数组结构:优点:空间连续,具有索引,查找数据速度快。缺点:增删慢,牵一发而动全身,可能还需要扩容。
链表结构:优点:增删快,更改节点的前置后置引用即可,存储可连续可不连续。缺点:查找慢,需要通过引用寻找next。
二叉查找树是二叉树中最常用的一种类型,是为了实现快速查找,且还支持快速插入和删除数据。二叉查找树的这些性能都依赖于二叉查找树的特殊结构
1)二叉查找树要求:在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都要大于这个节点的值。 得出:如果它的左子树不为空,则该左子树上的所有节点都小于它的根节点。如果它的右子树不为空,则该右子树上的所有节点都大于它的根节点。
2)二叉查找树的遍历:中序遍历、前序遍历、后序遍历。用的最多的就是中序遍历,因为中序遍历的结果是有序的。相当于投影式树的顺序,左边最小,右边最大。
3)二叉查找树的最小最大值:
最大:从根节点开始,一直寻找它的右子树,直到它没有右子树为止,也就是说最终右子树是叶子节点为止。
最小:从根节点开始,一直寻找它的左子树,直到它没有左子树为止,也就是说最终左子树是叶子节点为止。
4)二叉查找树的时间复杂度:
极端情况:二叉查找树退化成了链表,O(n)。
理想状态:二叉查找树属于完全二叉查找树,比较平衡,O(logn)。
5)二叉查找树的插入:遍历查找到需要插入的位置
6)二叉查找树的删除:一般不采用真正的删除,使用逻辑删除,添加标记即可,不用改变树的结构。
1)删除没有叶子节点的节点:将它的根节点指向该删除该节点的引用值设置为null。游离的节点等待GC回收。
2)删除有一个节点的节点:将它的根节点指向需要删除的子节点。子承父位。
3)删除有两个节点的节点:采用后继节点来代替被删除的节点。什么是后继节点:删除的树种大于要删除节点的最小节点。也就是查找这个删除节点的以右几点为根的子树中的最小节点。
4.平衡二叉树:为了避免二叉查找树频繁插入、删除退化为链表式二叉查找树,导致时间复杂度上升。设计出了平衡二叉树。所以平衡二叉树也称平衡二叉查找树。
要求:每个节点的左子树和右子树的高度差不能大于1.
时间复杂度:平衡二叉树的时间复杂度控制在了O(logn)。
平衡性:平衡二叉树插入或者删除之后,需要通过旋转来维护平衡性。LL,LR,RL,RR。左节点左旋等等。左旋:逆时针,降低右侧树的高度。右旋:顺时针,降低左侧树
演变:因为平衡二叉树的严格要求,导致插入或者删除之后需要维护树的平衡性,成本太高,所以有了红黑树。
5、红黑树:平衡二叉查找树演变而来。因为平衡二叉查找树的深度差不能超过1。红黑树是一个近似平衡的二叉树。时间复杂度近似:O(logn)
a:虽然平衡二叉树解决了二叉查找树可能退化了链表的极端情况,并且能够把查找时间控制在O(logn),所以”平衡“的意思就是为了使性能不退化。但是由于平衡二叉树严格的定义:每个节点的左子树和右子树的高度差至多等于1,导致每次在插入或者删除的时候,为了符合平衡二叉树严格的条件,需要进行一些操作来进行调整,比如左旋、右旋等操作,使其再次符合平衡二叉树的条件。
b:很明显,如果要是再插入和删除非常频繁的场景下,平衡二叉树每一次更新都必须要进行调整,这样太耗时了,性能也会受到很大影响。因此,为了避免平衡二叉树在频繁更新过程中,所带来的维持树结构的时间消耗,从而引入了红黑树。
c:特点:
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点都是不存储数据的黑色节点。当然可以省略。
(4)任何相邻的两个节点不能同时为红色。也就是说红色节点被黑色节点隔开,红色节点的孩子节点必然都是黑色。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。 确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树。
d: 平衡二叉查找树要求:每个节点的左右子树高度差不能超过1,红黑树要求:高度差不能超过两倍。
e:在平衡二叉查找树中频繁的增删,需要经过旋转来维持树的平衡性,带来得时间成本比较大。红黑树对于频繁插入和删除比较简单。所以在有频繁插入和删除的情况先选择数据结构可以选择红黑树。
f:平衡性:红黑树的平衡性通过,左旋、右旋、变色来实现。
g:插入:
(1)插入必须是红色,因为如果插入的是黑色就会破会,黑等高的特性。
(2)如果红黑树是空树,直接插入,变黑色即可。
(3)如果插入的节点存在,找到直接替换节点上的data。
(4)如果插入的父节点是一个黑色节点。直接插入。
(5)如果插入的父节点是红色,推理出,插入的父节点一定存在,并且是黑色。并且如果父节点是红色,那么他的两个子节点必然是黑色。
(6).............
6、B树、B+树:为了减少和磁盘的IO次数,我们尽可能将多的数据加载进内存,读取一个节点数据进行一次IO。又矮又胖。
a、数据库的索引存储再磁盘上,如果数据量不大,当然可以将所有索引数据加载进内存,如果数据量特别大是无法将数据库索引一次性全部加载进内存的,
b、B树要求:
(1)分支树:每个结点最多有m个分支(子树),而最少分支树要看是否为根节点,如果是根节点也不是叶子结点则至少有两个分支。非根非叶子结点至少有m/2向上取整。M表示阶数。m阶树,m-1个关键字。
(2)有n(k<=n<=m)个分支的结点有n-1个关键字,他们按递增顺序排列。k=2(根节点)或m/2向上取整(非根节点)。
(3)节点内的关键字互不相等。
(4)叶子结点处于同一层。可以用空指针表示。是查找失败到达的位置。
c、真正的B树图:例如:5阶B树
d、B树的阶数:就是存储每个结点的数组长度+1;B树的增删操作不会更改B树的阶数值。
B树的阶数是人为规定的,为每个结点开辟了多少存储空间,定义出了最大可以引出的分支。
从上图中就可以看出,每个结点的存储空间是固定的。
e、 查找关键字的方法:
首先读取根节点的所有关键字,指针开始从左向右扫描寻找,没有找到,扫描的指针落到最后一个关键字的后面,接下来沿着右侧的结点开始扫描,当然先读取这个结点上的所有数据,查找第一个比他大的关键字,找到之后沿着他的左边分支开始同理查找。
一般来说,索引很大,往往以索引文件的形式存储在磁盘上,索引查找时产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的时间复杂度。树高度越小,I/O次数越少。
索引结构的选择:那为什么是B+树而不是B树呢,因为B+树内节点不存储data,这样一个节点就可以存储更多的key。也就是一个节点可以存储更多的关键字,从而树低,IO少。
B+树:
7、B树和B+树的区别:
a:B树,每个节点都存储key和data,所有节点组成这棵树,并且叶子节点指针为null。B+树,只有叶子节点存储data,叶子节点包含了这棵树的所有键值,叶子节点不存储指针。所以B+树的时间复杂度固定为O(logn),B树节点存储数据,所以时间复杂度不固定,O(1) ->O(logn)。
b:B+树中有两个头指针,一个指向根节点,另一个指向最小的关键字叶子结点,叶子节点包含了这棵树的所有数据,所有的叶子节点使用链表相连,便于区间查找和遍历,所有非叶子起到了索引的作用。
c:B树中n个关键字包含n+1个子树。B+树,具有n个关键字的节点包含n个子树。B+树中查找,无论是否成功,都是一条从根节点到叶子节点的路径。
6、查找的方式:
1.线性查找:一个个对比查询数据。时间复杂度:O(n);
2.二分查找:将数据排序之后,对比查找。时间复杂度:O(logn)。插入元素和删除要保证顺序性。插入之后的元素都需要向后移动。删除之后的都需要向前移动。
public static void main(String[] args) {
Integer[] arr = {1,3,9,15,18,32,45,67,93};
System.out.println("index = "+getDataIndex(arr, 67));
}
public static Integer getDataIndex(Integer[] arr, Integer data){
//开始节点索引
int start = 0;
//结束节点索引
int end = arr.length - 1;
//数轴中开始节点和结束节点彼此靠近,直到开始的>结束停止,整个数组都被查找完全。
while (start <= end){
//这样处理是防止end+start超出 int的范围而溢出
int mid = start + ((end - start) >> 1);
if(arr[mid] > data){
end = mid - 1;
}else if(arr[mid] < data){
start = mid + 1;
}else{
return mid;
}
}
return -1;
}
注意:二分查找算法中的求中间值: int mid = start + ((end - start) >> 1); 等同于 int mid = (start + end) >> 1,但是前者能够避免Int范围的内存溢出。
3.二叉查找:为了解决二分查找中增删慢的特点,演变除了二叉树的结构,采用二叉查找的方式。二叉查找树种增删不需要移动大量的数据。