树
一)什么是树(tree)
树是n(n≥0)个节点的有限集。当n=0时,称为空树。在任意一个非空树中,有如下特点:
有且仅有一个特定的称为根的节点。
当n>1时,其余节点可分为m(m>0)个互不相交的有限集,每一个集合本身又是一个树,并称为根的子树。
术语:根节点、子树、叶子节点、父节点、孩子节点、兄弟节点、树的高度
二)二叉树
二叉树(binary tree)是树的一种特殊形式。这种树的每个节点最多有2个孩子节点。注意,这里是最多有2个,也可能只有1个,或者没有孩子节点
二叉树的存储
1.链式存储
存储数据的data变量
指向左孩子的left指针
指向右孩子的right指针
2.数组存储
使用数组存储时,会按照层级顺序把二叉树的节点放到数组中对应的位置上。如果某一个节点的左孩子或右孩子空缺,则数组的相应位置也空出来,这样可以更方便地在数组中定位二叉树的孩子节点和父节点:
父节点下标 parent,那么左孩子下标为 leftChild = 2×parent + 1;右孩子下标为 rightChild = 2×parent + 2;
思考:什么二叉树适合用数组存储?
二叉树的遍历
线性结构(数组、链表)的遍历很简单,那么非线性结构树是如何遍历的呢?
主要分为:深度优先遍历(先序遍历、中序遍历、后序遍历)和 广度优先遍历(层序遍历)。
深度优先遍历
先序(根)遍历
先序遍历输出顺序是根节点
、左子树、右子树。
public void preTraversal(TreeNode(parent,left,right,data) node) {if (null == node) {return;}// 输出节点值print(node.data);// 递归preTraversal(node.left);preTraversal(node.right);
}
中序(根)遍历
中序遍历输出顺序是左子树、根节点
、右子树。
public void middleTraversal(TreeNode(parent,left,right,data) node) {if (null == node) {return;}// 递归middleTraversal(node.left);// 输出节点值print(node.data);// 递归middleTraversal(node.right);
}
后序(根)遍历
后序遍历输出顺序是左子树、右子树、根节点
。
public void afterTraversal(TreeNode(parent,left,right,data) node) { if (null == node) {return;} // 递归 afterTraversal(node.left); afterTraversal(node.right); // 输出节点值 print(node.data);
}
广度优先遍历
层序遍历
由于二叉树同一层次的节点之间是没有直接关联的,所以层序遍历需要利用队列的方式来实现
public void levelTraversal(TreeNode(parent,left,right,data) root) {Queue<TreeNode> queue = new LinkedList<>();// 放入根节点queue.offer(root);while(!queue.isEmpty()) {// 出队TreedNode node = queue.pull();// 输出元素值print(node.data);if (node.left != null) {queue.offer(node.left);}if (node.right != null) {queue.offer(node.right);}}}
二叉树的应用
二叉树包含许多特殊的形式,每一种形式都有自己的作用,但是其最主要的应用还在于进行查找操作和维持相对顺序。
三)二叉查找树
二叉查找树在二叉树的基础上增加了以下几个条件:
如果左子树不为空,则左子树上所有节点的值均小于根节点的值
如果右子树不为空,则右子树上所有节点的值均大于根节点的值
左、右子树也都是二叉查找树
如果要查找为4的节点:
访问根节点,4 < 6 ;
访问6的左孩子节点3,4 > 3;
访问3的右孩子节点4,4 = 4;
对于一个节点分布相对均衡的二叉查找树来说,如果节点总数是n,那么搜索节点的时间复杂度就是O(logn),和树的深度是一样。
二叉查找树存在的问题:
这里涉及到二叉树的自平衡,自平衡的方式有很多,比如平衡二叉树、红黑树、树堆等。
四)平衡二叉树(AVL树)
平衡二叉树
也称为AVL树
,在二叉搜索树的基础上,平衡二叉树还需要满足如下条件:
左右两个子树的高度差(
平衡因子
)的绝对值不超过1左右两个子树都是一棵平衡二叉树
平衡二叉树的目的是为了减少二叉查找树层次,提高查找速度。平衡二叉树是如何在插入的时候保持平衡呢?
自旋
树的自旋单旋转
和双旋转
,分别作用于解决不同的情况。
LL情况(右旋或LL旋转)
在左子树的左节点上插入了新的数据,导致左右子树高度差绝对值 > 1;这种情况称为 左左(LL) 的情况,解决办法:
动图:
RR情况(左旋或RR旋转)
在右子树的右节点上插入了新的数据,导致左右子树高度差绝对值 > 1;这种情况称为 右右(RR) 的情况,解决办法:
动图:
LR情况(LR旋转-对左孩子RR旋,对根节点LL旋)
在左子树的右节点上插入了新的数据,导致左右子树高度差绝对值 > 1;这种情况称为 左右(LR) 的情况,解决办法:
RL情况(RL旋转-对右孩子LL旋,对根节点RR旋)
在右子树的左节点上插入了新的数据,导致左右子树高度差绝对值 > 1;这种情况称为 右左(RL) 的情况,解决办法:
实现代码:
// 节点对象AvlNode root;
public void insert(int data){root = insert(root,data);}// 插入节点public AvlNode insert(AvlNode n, int data) {if (n == null) {n = new AvlNode(data);}// 大于插入右子树if (data > n.data) {n.right = insert(n.right, data);
// 检查并调整平衡,由于是右边插入,右子树的高度肯定大于等于左子树的高度if (height(n.right) - height(n.left) == 2) {// 判断data是插入点的左孩子还是右孩子if (data < n.right.data) {// RL情况,RL旋转n = doubleRightLeftRotate(n);} else {// RR情况,RR旋转n = singleRightRotate(n);}}// 小于插入左子树} else if (data < n.data) {n.left = insert(n.left, data);
// 检查并调整平衡,由于是左边插入,左子树的高度肯定大于等于右子树的高度if (height(n.left) - height(n.right) == 2) {// 判断data是插入点的左孩子还是右孩子if (data < n.left.data) {// LL情况,LL旋转n = singleLeftRotate(n);} else {// LR情况,LR旋转n = doubleLeftRightRotate(n);}}}// 重新计算高度n.height = height(n);return n;}
// 计算高度public int height(AvlNode n) {int left = n.left == null ? 0 : height(n.left);int right = n.right == null ? 0 : height(n.right);return Math.max(left,right) + 1;}
// LL旋转private AvlNode singleLeftRotate(AvlNode root){// 把 root 的左节点 root_left 旋转为根结点AvlNode root_left = root.left;// 同时 root_left 的右子树变为 root 的左子树root.left = root_left.right;// root 变为 root_left 的右子树root_left.right = root;// 重新计算 root/root_left 的高度root.height = height(root);root_left.height = height(root_left);return root_left;//返回新的根结点}
// RR旋转private AvlNode singleRightRotate(AvlNode root){// 把 root 的左节点 root_right 旋转为根结点AvlNode root_right = root.right;// 同时 root_right 的左子树变为 root 的右子树root.right = root_right.left;// root 变为 root_right 的右子树root_right.left = root;// 重新计算 root/root_right 的高度root.height = height(root);root_right.height = height(root_right);return root_right;//返回新的根结点}
// RL情况(LR旋转-对右孩子LL旋,对根节点RR旋)private AvlNode doubleRightLeftRotate(AvlNode root){// 对 root 的右节点 root_right 进行LL旋转root.right = singleLeftRotate(root.right);// 对 root 进行 RR旋转return singleRightRotate(root);//返回新的根结点}
// LR情况(LR旋转-对左孩子RR旋,对根节点LL旋)private AvlNode doubleLeftRightRotate(AvlNode root){// 对 root 的右节点 root_left 进行RR旋转root.left = singleRightRotate(root.left);// 对 root 进行 LL旋转return singleLeftRotate(root);//返回新的根结点}
static class AvlNode {int data; AvlNode left; AvlNode right;// 当前节点高度int height;public AvlNode(int data) {this.data = data;}}
五)红黑树(Red-Black Tree,简称RBTree)
红黑树是一种含有红黑结点并能自平衡的二叉查找树,实际应用非常广泛,比如Linux内核中的完全公平调度器、高精度计时器、ext3文件系统等等,各种语言的函数库如Java的TreeMap和TreeSet,C++ STL的map、multimap、multiset等。
在二叉查找树的条件上,红黑树还必须满足下面性质:
class Node<T>{public T value;public Node<T> parent;public boolean isRed;public Node<T> left;public Node<T> right;
}
RBTree的插入操作
插入新节点过后,可能会导致树的不平衡,这时就需要对树进行旋转操作和颜色修复,使得它符合红黑树的定义
新插入的节点是红色的,插入修复操作如果遇到父节点的颜色为黑则修复操作结束。也就是说,只有在父节点为红色节点的时候是需要插入修复操作的。
插入修复操作分为以下的三种情况,而且新插入的节点的父节点都是红色的:
叔叔节点也为红色。
叔叔节点为空或黑色,且祖父节点、父节点和新节点处于一条斜线上。
叔叔节点为空或黑色,且祖父节点、父节点和新节点不处于一条斜线上。
插入操作-Case 1 – 叔叔节点也为红色。
case 1的操作是将父节点和叔叔节点与祖父节点的颜色互换,这样就符合了RBTRee的定义。即维持了高度的平衡,修复后颜色也符合RBTree定义的第三条和第四条。下图中,操作完成后 “90” 节点变成了新的节点。如果 “90” 节点的父节点不是黑色的话,则继续做修复操作。
插入操作-Case 2 – 叔叔节点为空或黑色,且祖父节点、父节点和新节点处于一条斜线上。(LL或RR)
case 2(LL或RR)的操作是将父节点进行右旋(左旋)操作,并且和祖父节点互换颜色。通过该修复操作RBTRee的高度和颜色都符合红黑树的定义。
插入操作-Case 3 – 叔叔节点为空或黑色,且祖父节点、父节点和新节点不处于一条斜线上。(LR或RL)
case 3的操作是将新节点进行左旋,这样就从case 3转换成case 2了。
总结
树结构是一个非常重要而且应用非常广的数据结构,作为研发人员也必须要了解的数据结构,了解了树结构你才能读懂很多组件的原理。树结构的延伸也非常的广,不止包含平衡二叉树、红黑树、还包含B树、B+树、2-3树、哈夫曼树、二叉堆等等,每一种都有相应的应用场景。