树与二叉树
1.树
1.1 树的定义
树(Tree)是n(n≥0)个结点的有限集。n=0 时称为空树。在任意一棵非空树中:
- 有且仅有一个特定的称为根(Root)的结点;
- 当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、… 、Tm其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)
如下图所示:
树的相关概念:
- 节点:树中的一个独立单元。如图中的A、B、C等。
- 节点的度:节点拥有子树的个数。如A有2个度,D有3个度。
- 树的度:树中最大的节点的度即为树的度。如图中树的度为D节点的度为3
- 叶子:度为0的节点称为叶子或终端节点。如图中的G,H,I,J,F。
- 非终端节点:度不为0的节点称为非终端节点或分支节点。除根节点外,非终端节点也称为内部节点
- 双亲和孩子:某节点的子结点称为该节点的孩子;该节点称为孩子的双亲。
- 兄弟:同一个双亲的孩子节点称为兄弟。如E和F是兄弟。
- 祖先:从根到该节点的双亲所经过的所有节点称为该节点的祖先。如J的祖先为A,C,E。
- 子孙:某节点的子树上任意一个节点都为该节点的子孙。如C的子孙E,F,J。
- 层次:从根节点开始定义,根节点为第一层,树中任一节点层次为其双亲层次加1。
- 树的深度:树的深度为树中节点的最大层次
- 有序树和无序树:如果树中节点的各子树看成从左至右是有次序的不能互换,则称该树为有序树,否则称为无序树。
对于树的定义还需要强调两点:
- n>0时根结点是唯一的,不可能存在多个根结点,别和现实中的大树混在一起,现实中的树有很多根须,那是真实的树,数据结构中的树是只能有一个根结点。
- m>0时,子树的个数没有限制,但它们一定是互不相交的。像下图中的两个结构就不符合树的定义,因为它们都有相交的子树。
2.二叉树
2.1 二叉树的定义与性质
二叉树的定义:
二叉树是有n(n>=0)个节点的有限集,有空树(n=0)和非空树。
对于非空树来说,它有且仅有一个称之为根的节点;除了根节点外的其余节点可分为两个互不相交的子集T1和T2,分别称为T的左子树和右子树,且T1和T2本身也是二叉树
二叉树与树的区别:
1.二叉树的每个节点至多只有两颗子树
2.二叉树是有序树,有左右子树之分
五中不同形态的二叉树:
二叉树的性质:
- 在二叉树的第i层上至多有2^(i-1)个节点
- 深度为k的二叉树至多有2^k-1个节点,最少有k个节点
- 对任意一颗二叉树T,如果其终端节点树为n0,度为2的节点数为n2,则n0=n2+1
原因:一颗二叉树有n个节点,度为0的节点为n0个,度为1的节点为n1个,度为2的节点为n2个,有x个分支。则(式一)n=n0+n1+n2;(式二)n=x+1;(式三)x=n1+2n2,由一,二,三式可得(式四)n=n1+2n2+1,由一,四式可得n0=n2+1 - 满二叉树:深度为k且含有2^k-1个节点的二叉树
- 完全二叉树:深度为k且含有n个节点的二叉树,当且仅当其每一个节点都与深度为k的满二叉树中的节点编号一致时,称为完全二叉树
特点:对深度为k的完全二叉树来说
- 叶子节点仅出现在第k和第k-1层
- 对于任一节点,若其右分支的最大层次为L,则其左分支的最大层次必为L或L+1。
2.4 二叉树的遍历
2.4.1前序遍历
先序遍历(PreOrder) 的操作过程如下:
1)访问根结点;
2)先序遍历左子树;
3)先序遍历右子树。
先序遍历结果为:A B D H I E J C F K G
代码实现:
void preOrderTraversal(Node root) {
if(root == null) {//若为空直接返回
return;
}
System.out.print(root.val+" ");//先打印
preOrderTraversal(root.left);//向左子树递归
preOrderTraversal(root.right);//向右子树递归
}
2.4.2中序遍历
中序遍历( InOrder)的操作过程如下:
1)中序遍历左子树;
2)访问根结点;
3)中序遍历右子树。
中遍历结果为:H D I B E J A F K C G
代码实现:
void inOrderTraversal(Node root) {
if(root == null) {
return;
}
inOrderTraversal(root.left);//先向左子树递归
System.out.print(root.val+" ");//打印
inOrderTraversal(root.right);//向右子树递归
}
2.4.3后序遍历
后序遍历(PostOrder) 的操作过程如下:
1)后序遍历左子树;
2)后序遍历右子树;
3)访问根结点。
后序遍历结果:H I D J E B K F G C A
代码实现:
void postOrderTraversal(Node root) {
if(root == null) {
return;
}
postOrderTraversal(root.left);//向左子树递归
postOrderTraversal(root.right);//向右子树递归
System.out.print(root.val+" ");//打印
}
2.4.4层序遍历
层次遍历很好理解,就是从根节点开始,一层一层,从上到下,每层从左到右,依次写值就可以了
遍历结构:A B C D E F G H I J K
代码实现:
进行层次遍历时构建一个辅助队列,先将二叉树根节点入队,然后出队,访问出队结点,若它有左子树,将左子树根节点入队,然后出队,访问出队结点…,右子树也同样如此,循环往复直到队列为空。
public void levelOrderTraversal(Node root) {
if(root == null) return;
Queue<Node> queue = new LinkedList<>();
queue.offer(root);//将根节点入队
while (!queue.isEmpty()) {
Node top = queue.poll();//弹出一个元素,记录节点
System.out.print(top.val+" ");//打印该元素
//每弹出一个元素,就将该元素的左右子树入队
if(top.left != null) {
queue.offer(top.left);
}
if(top.right!=null) {
queue.offer(top.right);
}
}
}
排序二叉树
排序二叉树(Binary SearchTree) :在此树中,每个节点的数值比左子树上的每个节点都大,比所有右子树上的节点都小。
如下图所示:
构造二叉排序树
public class BST {
static class TreeNode {
public int value;
public TreeNode left;//左子树
public TreeNode right;//右子树
public TreeNode(int value) {
this.value = value;
}
}
private TreeNode root;
}
二叉排序树的查找
代码实现:
public TreeNode get(int key) {
TreeNode current = root;//从根路径开始
while(current != null && current.value != key){//终止条件
//进行迭代
if(key < current.value){//key偏小,从左子树中找
current = current.left;
}else if(key > current.value){//key偏大,从右子树中找
current =current.right;
}
}
return current== null ? null : current;
}
二叉排序树的插入
public void insert(int key) {
if(root == null){//如果根节点为null则把根节点变为该值
root = new TreeNode(key);
return;
}
TreeNode current = root;//你要插入的位置
TreeNode parent = null;//你要插入位置的父亲
while(true){//遍历循环,迭代寻找要插入的位置
parent = current;
if(key < parent.value){//插入的值小于父节点
current = parent.left;
if(current == null) {
parent.left = new TreeNode(key);
return;
}
}else if(key > parent.value){//插入的值大于父节点
current = parent.right;
if(current == null) {
parent.right = new TreeNode(key);
return;
}
} else {//插入的值等于父节点,已经将key插入,直接返回即可
return; //二叉搜索树没有相同的节点
}
}
}
二叉排序树的删除
第一种情况:要删除的节点就是叶子结点,直接删除即可
第二种情况:只有左孩子或者右孩子,让孩子代替其位置即可
第三种情况:同时有左孩子和右孩子
public boolean delete(int key) {
TreeNode parent = root;
TreeNode current = root;
boolean isLeftChild = false;//是否是左孩子
while(current != null && current.value != key){//寻找要删除的节点
parent = current;
if(current.value > key){
isLeftChild = true;
current = current.left;//向左边寻找
} else {//向右边寻找
isLeftChild = false;
current = current.right;
}
}
if(current == null) {//没找到要删除的节点直接返回
return false;
}
// Case 1: 第一种情况:要删除的节点就是叶子结点,直接删除即可
if(current.left == null && current.right == null){
if(current == root) {//若为根节点直接将其置为null
root = null;
} else if(isLeftChild){//若要删除的节点为左孩子,设为null
parent.left = null;
} else {//同理
parent.right = null;
}
// Case 2:第二种情况:只有左孩子或者只有右孩子,让孩子代替其位置即可
} else if (current.right == null){//若右孩子为null
if(current == root) {//当前节点为根节点
root = current.left;//直接让左孩子替代其位置
} else if (isLeftChild){//向左子树迭代
parent.left = current.left;
} else {//向右子树迭代
parent.right = current.left;
}
} else if (current.left == null){//若左孩子为null,同上
if(current == root) {
root = current.right;
} else if (isLeftChild) {
parent.left = current.right;
} else {
parent.right = current.right;
}
// Case 3: current.left != null && current.right != null
//同时有左孩子和右孩子
} else {
//找到要删除节点的右子树上最小的那一个
//将要删除节点的右子树最小的那个节点对要删除的节点进行覆盖
TreeNode successor = getSuccessor(current);
if (current == root) {
root = successor;
} else if (isLeftChild) {
parent.left = successor;
} else {
parent.right = successor;
}
successor.left = current.left;
}
return true;
}
private TreeNode getSuccessor(TreeNode node) {
TreeNode successor = null;//最后要覆盖删除位置的节点
TreeNode successorParent = null;//用于记录父亲节点
TreeNode current = node.right;
while (current != null){
//找到要删除节点的右子树上最小的那一个(右孩子的最左边的那一个)
successorParent = successor;
successor = current;
current = current.left;
}
if (successor != node.right){//说明要删除节点是有左孩子的
successorParent.left = successor.right;//节点交换
successor.right = node.right;
}
return successor;
}
二叉排序树总结
二叉排序树以链接的方式存储,保持了链接存储结构在执行插入或删除操作时不用移动元素的优点,只要找到合适的插入和删除位置后,仅需修改链接指针即可。插入删除的时间性能比较好。而对于二叉排序树的查找,走的就是从根结点到要查找的结点的路径,其比较次数等于给定值的结点在二叉排序树中的层数。极端情况,最少为1次,即根结点就是要找的结点,最多也不会超过树的深度。也就是说,二叉排序树的查找性能取决于二叉排序树的形状。可问题就在于,二叉排序树的形状是不确定的。
例如{62,88,58,47,35,73,51,99,37,93}这样的数组,我们可以构建如左下图所示的二叉排序树。但如果数组元素的次序是从小到大有序,如{35,37,47,51,58,62,73,88,93,99},则二叉排序树就成了极端的右斜树,注意它依然是一棵二叉排序树,如右下图。此时,同样是查找结点99,左下图只需要两次比较,而右下图就需要10次比较才可以得到结果,二者差异很大。
也就是说,我们希望二叉排序树是比较平衡的,即其深度与完全二叉树相同,均为[log2n]+1,那么查找的时间复杂也就为O(logn),近似于折半查找,事实上,左上图也不够平衡,明显的左重右轻。
不平衡的最坏情况就是像右上图的斜树,查找时间复杂度为O(n),这等同于顺序查找。
因此,如果我们希望对一个集合按二叉排序树查找,最好是把它构建成一棵平衡的二叉排序树。这样我们就引申出另一个问题,如何让二叉排序树平衡的问题。
二叉排序树的查找性能取决于二叉排序树的形状,在O(log2n)和O(n)之间。
平衡二叉树
平衡二叉树(AVL Tree)任何节点的两颗子树的高度差不大于1的排序二叉树。
平衡二叉树:或者是一棵空的二叉排序树,或者是具有下列性质的二叉排序树:
(1)根结点的左子树和右子树的深度最多相差1;
(2)根结点的左子树和右子树也都是平衡二叉树。
平衡因子:结点的平衡因子是该结点的左子树的深度与右子树的深度之差。
在平衡树中,结点的平衡因子可以是1,0, -1。
最小不平衡子树:在平衡二叉树的构造过程中,以距离插入结点最近的、且平衡因子的绝对值大于1的结点为根的子树。
B树
B树(B-Tree):B树和平衡二插树一样,只不过它是一种多叉树(一个节点的子节点数量可以超过二)
红黑树
红黑树(Red- -Black Tree)是一种自平衡二叉寻找树。