【树】树的介绍
1、各类树的概念
一、概念:
节点度:节点拥有的子树数
二叉数的度:所有节点度的最大值
节点深度:指节点到根节点最长路径的边的总和
节点层级:指节点到根节点最长路径的边的总和+1
2、二叉树分类:
2.1 平衡二叉树
(1)非叶子节点最多拥有两个子节点;
(2)非叶子节值大于左边子节点、小于右边子节点;
(3)树的左右两边的层级数相差不会大于1;
(4)没有值相等重复的节点;
2.2、 B树(B-tree)
B树属于多叉树又名平衡多路查找树(查找路径不只两个)
(1)排序方式:所有节点关键字是按递增次序排列,并遵循左小右大原则;
(2)子节点数:非叶节点的子节点数>1,且<=M ,且M>=2,空树除外(注:M阶代表一个树节点最多有多少个查找路径,M=M路,当M=2则是2叉树,M=3则是3叉);
(3)关键字数:枝节点的关键字数量大于等于ceil(m/2)-1个且小于等于M-1个(注:ceil()是个朝正无穷方向取整的函数 如ceil(1.1)结果为2);
(4)所有叶子节点均在同一层、叶子节点除了包含了关键字和关键字记录的指针外也有指向其子节点的指针只不过其指针地址都为null对应下图最后一层节点的空格子;
如上图我要从上图中找到E字母,查找流程如下
(1)获取根节点的关键字进行比较,当前根节点关键字为M,E<M(26个字母顺序),所以往找到指向左边的子节点(二分法规则,左小右大,左边放小于当前节点值的子节点、右边放大于当前节点值的子节点);
(2)拿到关键字D和G,D<E<G 所以直接找到D和G中间的节点;
(3)拿到E和F,因为E=E 所以直接返回关键字和指针信息(如果树结构里面没有包含所要查找的节点则返回null);
- 特点:
B树相对于平衡二叉树的不同是,每个节点包含的关键字增多了,特别是在B树应用到数据库中的时候,数据库充分利用了磁盘块的原理(磁盘数据存储是采用块的形式存储的,每个块的大小为4K,每次IO进行数据读取时,同一个磁盘块的数据可以一次性读取出来)把节点大小限制和充分使用在磁盘快大小范围;把树的节点关键字增多后树的层级比原来的二叉树少了,减少数据查找的次数和复杂度;
2.3 B+树
B+树是B树的一个升级版,相对于B树来说B+树更充分的利用了节点的空间,让查询速度更加稳定,其速度完全接近于二分法查找。为什么说B+树查找的效率要比B树更高、更稳定;我们先看看两者的区别
(1)B+跟B树不同B+树的非叶子节点不保存关键字记录的指针,只进行数据索引,这样使得B+树每个非叶子节点所能保存的关键字大大增加;
(2)B+树叶子节点保存了父节点的所有关键字记录的指针,所有数据地址必须要到叶子节点才能获取到。所以每次数据查询的次数都一样;
(3)B+树叶子节点的关键字从小到大有序排列,左边结尾数据都会保存右边节点开始数据的指针。
(4)非叶子节点的子节点数=关键字数(来源百度百科)(根据各种资料 这里有两种算法的实现方式,另一种为非叶节点的关键字数=子节点数-1(来源维基百科),虽然他们数据排列结构不一样,但其原理还是一样的Mysql 的B+树是用第一种方式实现);
- 特点
B+树的层级更少:相较于B树B+每个非叶子节点存储的关键字数更多,树的层级更少所以查询数据更快;
B+树查询速度更稳定:B+所有关键字数据地址都存在叶子节点上,所以每次查找的次数都相同所以查询速度要比B树更稳定;
B+树天然具备排序功能:B+树所有的叶子节点数据构成了一个有序链表,在查询大小区间的数据时候更方便,数据紧密性很高,缓存的命中率也会比B树高。
B+树全节点遍历更快:B+树遍历整棵树只需要遍历所有的叶子节点即可,,而不需要像B树一样需要对每一层进行遍历,这有利于数据库做全表扫描。
B树相对于B+树的优点是,如果经常访问的数据离根节点很近,而B树的非叶子节点本身存有关键字其数据的地址,所以这种数据检索的时候会要比B+树快。
3、树的遍历
3.1、遍历方式
先序:先根节点,再左节点,最后右节点
中序:先左节点,再根节点,最后右节点
后序:先左节点,再右节点,最后根节点
层序:按照每一层的顺序读取。
还是对Java比较熟悉,方便复习,下面是复制的代码,递归的方式还是很容易想起来的,主要是非递归能不能记起来。
记录一下四种遍历的思路方便复习:
3.2、先序遍历:
递归的方法没什么好说的,左节点为空时开始回溯打印,代码也简单。
非递归的时候就需要用到栈,因为是后进先出的思路,首先一路将左子节点压入栈内,当然压入的过程要先打印根节点,然后是将栈内节点弹出,开始遍历右节点。然后这一整个过程放入循环当中。
记住:弹栈切换右节点要判断栈是否为空,因为当root节点没有右节点的时候,最后一次循环栈就已经空了。
//先序遍历
public void preorder_traversal(TreeNode root){
if(root == null) {
return;
}
System.out.print(root.value + " ");
preorder_traversal(root.left);
preorder_traversal(root.right);
}
//先序遍历非递归
public void preorder(TreeNode root){
Stack<TreeNode> stack = new Stack<>();
while (root != null || !stack.empty()) {
while (root != null) {
System.out.print(root.value + " ");
stack.push(root);
root = root.left;
}
if (!stack.empty()) {
root = stack.pop();
root = root.right;
}
}
}
3.3、中序遍历
和先序遍历的思路大同小异,因为整个树的递归思路是一样的,只不过是输入的时候变化了,得先输入左子节点。
//中序遍历
public void inorder_traversal(TreeNode root) {
if(root == null) {
return ;
}
inorder_traversal(root.left);
System.out.print(root.value + " ");
inorder_traversal(root.right);
}
//中序遍历非递归
public void inorder(TreeNode root) {
Stack<TreeNode> stack = new Stack<>();
while(root != null || !stack.isEmpty()) {
while(root != null) {
stack.push(root);
root = root.left;
}
if(!stack.isEmpty()) {
root = stack.pop();
System.out.print(root.value + " ");
root = root.right;
}
}
}
}
3.4、后续遍历
上大学的那会就记得,最难的是后序遍历的非递归,为什么?
因为虽然递归思路是一样的,但是如果在右节点未读取的时候弹出了父节点,那么就不能实现后序遍历了。前面的思路是一样的,先一路把左子节点压入栈内,然后弹出栈顶端节点,打印,最后判断其是否是当前栈的左子节点,如果是我们把节点换成右子节点去遍历。同样把这一整个过程放到循环当中。
//后续遍历
public void postorder_traversal(TreeNode root) {
if(root == null) {
return ;
}
postorder_traversal(root.left);
postorder_traversal(root.right);
System.out.print(root.value + " ");
}
//后续遍历非递归
public void postorder(TreeNode root) {
Stack<TreeNode> stack = new Stack<>();
while(root != null || !stack.isEmpty()) {
while(root != null) {
stack.push(root);
root = root.left;
}
root = stack.pop(); //为什么这里不用判断栈为空,因为最后读取的是root,root是最后弹出来的,而先序和中序会出现root弹出栈为空的时候。比如说root没有右节点
System.out.print(root.value + " ");
if(!stack.isEmpty() && root == stack.peek().left) {
root = stack.peek().right;
}
else {
root = null;
}
}
}
3.5、层序遍历
因为是先进先出所以不使用栈而是使用队列,即判断左子节点和右子节点是否为空,不为空的时候压入队列尾端,放到循环里就好了。
//层序遍历
public void LaywerTraversal(TreeNode root) {
LinkedList<TreeNode> linkedList = new LinkedList<>();
linkedList.push(root);
while(!linkedList.isEmpty()) {
TreeNode temp = linkedList.pop();
System.out.print(temp.value + " ");
if(temp.left != null) {
linkedList.add(temp.left);
}
if(temp.right != null) {
linkedList.add(temp.right);
}
}
}