数据结构(5):树
一、树
为什么要使用树,因为数组删除、插入的效率太低。而链表查找数据的速度太慢,在树中能快速的查找数据项、插入数据项和删除数据项。
从上图中我们可以发现“树”这种结构和现实中的树一样的,节点之间用线相连,形成父子关系。
知道了树,我们还需要知道以下概念
路径:顺着连接节点的边从一个节点到另一个节点,所经过的节点顺序排列称为路径。
根:树最上面的节点称为根节点。一个树只有一个根。而且从根到任何节点有且只有一条路径。
父节点:每个节点都有一条边向上连接到另一个节点,这个节点就称为是下面这个节点的父节点。
子节点:每个节点都一条边向下连接到另一个节点,下面的节点就是该节点的子节点。
叶子节点:没有子节点的节点
子树:每个节点都可以作为一个子树的根,它和它所有的子节点,子节点的子节点组合在一起就是一个子树。
访问:访问一个节点是为了在这个节点上执行一些操作,如查看节点的数据项。但是如果仅仅是经过一个节点,不认为是访问了这个节点。
高度:节点到叶子节点的最长路径(边数)
深度:根节点到这个节点所经历的边的个数
层:一个节点的层数是指从根开始到这个节点有多少代。
大家可以对比参考下图理解概念
二、二叉查找树
二叉树:顾名思义,树的每个节点最多只能有两个节点的树,称为二叉树。
二叉查找树:在二叉树的基础上实现的快速查找的树,在二叉查找树中的任意一个节点,左子树每个节点的值,都要小于这个节点的值,而右子树每个节点的值都应大于这个节点的值。
要想实现二叉查找树,首先创建一个节点类存储数据和左右子节点的引用
/**
* 二叉树节点
*/
public class Node {
//数据项
public long data;
//左子节点
public Node leftChild;
//右子节点
public Node rightChild;
public Node(long data){
this.data=data;
}
}
创建Tree类保存对根节点的引用及操作节点的相关方法
/**
* 二叉树
*/
public class Tree {
//根节点
public Node root;
}
1、插入节点
从根节点开始查找一个相应的节点,这个节点将称为新插入节点的父节点。当父节点找到后,通过判断新节点的值比父节点的值的大小来决定是连接到左子节点还是右子节点。
/**
* 插入节点
*
* @param value
*/
public void insert(long value) {
//将插入的值封装为一个节点
Node newNode = new Node(value);
//引用当前节点
Node current = root;
//引用父节点
Node parentNode;
//如果root为null,也就是第一次插入
if (root == null) {
root = newNode;
return;
} else {
while (true) {
//父节点指向当前节点
parentNode = current;
//如果当前指向的节点数据比插入的要大,则向左走
if (current.data > value) {
current = current.leftChild;
if (current == null) {
parentNode.leftChild = newNode;
return;
}
} else {
current = current.rightChild;
if (current == null) {
parentNode.rightChild = newNode;
return;
}
}
}
}
}
2、查找节点
从根节点开始查找,如果查找的节点值比当前节点的值小,则继续查找其左子树,否则查找其右子树。
/**
* 查找节点
*
* @param value
* @return
*/
public Node find(long value) {
//引用当前节点,从根节点开始
Node current = root;
//只要查找的节点不等于null 且不与查找的值相等就循环
while (current != null && current.data != value) {
//进行比较,比较查找值和当前节点的大小
if (current.data > value) {
current = current.leftChild;
} else {
current = current.rightChild;
}
}
return current;
}
3、遍历二叉查找树
遍历树:遍历树是根据一个特定的顺序访问树的每一个节点,根据顺序的不同分为前序、中序、后序遍历三种,这三种遍历方式的区别就在于打印根节点值的顺序。
前序遍历:1.访问(打印)根节点2.前序遍历左子树3.前序遍历右子树
/**
* 前序遍历
*/
public void frontOrder(Node localNode) {
if (localNode != null) {
//访问根节点
System.out.println(localNode.data);
//前序遍历左子树
frontOrder(localNode.leftChild);
//前序遍历右子树
frontOrder(localNode.rightChild);
}
}
中序遍历:1.中序遍历左子树2.访问(打印)根节点3.中序遍历右子树
/**
* 中序遍历
*
* @param localNode
*/
public void inOrder(Node localNode) {
if (localNode != null) {
//中序遍历左子树
inOrder(localNode.leftChild);
//访问根节点
System.out.println(localNode.data);
//中序右子树
inOrder(localNode.rightChild);
}
}
后序遍历:1.后序遍历左子树2.后序遍历右子树3.访问(打印)根节点
/**
* 后序遍历
*/
public void afterOrder(Node localNode) {
if (localNode != null) {
//后序遍历左子树
afterOrder(localNode.leftChild);
//后序遍历右子树
afterOrder(localNode.rightChild);
//访问根节点
System.out.println(localNode.data);
}
}
4、删除二叉树节点
删除节点是二叉树操作中最复杂的。在删除之前首先要查找要删的节点。找到节点后,这个要删除的节点可能会有三种情况需要考虑。
1.该节点是叶子节点,没有子节点
要删除叶节点,只需要改变该节点的父节点的引用值,将指向该节点的引用设置为null就可以了。
2.该节点有一个子节点
改变父节点的引用,将其直接指向要删除节点的子节点。
3.该节点有两个子节点
要删除有两个子节点的节点,就需要使用它的中序后继来替代该节点,如下图中50
有两个子节点。
那么什么是要删除的节点的中序后继呢,对比上图,我们来找要删除节点的右子节点的左子节点,直到左叶子节点。这个节点为54
那么刚好可以作为替换50
而不影响这个二叉查找树的规则。
/**
* 查找中序后继
*
* @param delNode
* @return
*/
public Node getSuccessor(Node delNode) {
Node successor = delNode;
Node successorParent = delNode;
//要删除节点的右子树
Node current = delNode.rightChild;
//向左子树查找直到最终一个左子节点
while (current != null) {
successorParent = successor;
successor = current;
current = current.leftChild;
}
//如果中序节点不为要删除的右子树
if (successor != delNode.rightChild) {
//将查到的节点的父节点的左子节点(也就是查到的节点)指向查到节点的右子树
successorParent.leftChild = successor.rightChild;
//将查到的节点的右子树指向要删除节点的右子树
successor.rightChild = delNode.rightChild;
}
return successor;
}
实现删除
/**
* 删除节点
*
* @param value
* @return
*/
public boolean delete(long value) {
//引用当前节点,从根节点开始
Node current = root;
//引用当前节点的父节点
Node parent = root;
//是否为左节点
boolean isLeftChild = true;
if (current == null) {
return false;
}
while (current.data != value) {
parent = current;
//进行比较,比较查找值和当前节点的大小
if (current.data > value) {
current = current.leftChild;
isLeftChild = true;
} else {
current = current.rightChild;
isLeftChild = false;
}
//如果查找不到
if (current == null) {
return false;
}
}
//删除叶子节点
if (current.leftChild == null && current.rightChild == null) {
if (current == root) {
root = null;
} else
//如果它是父节点的左子节点
if (isLeftChild) {
parent.leftChild = null;
} else {
parent.rightChild = null;
}
} else if (current.rightChild == null) {//如果只有一个节点,当前节点是左
if (current == root) {
root = current.leftChild;
} else
//如果当前是左节点,则为父节点的左节点赋值
if (isLeftChild) {
parent.leftChild = current.leftChild;
} else {
parent.rightChild = current.leftChild;
}
} else if (current.leftChild == null) {//只有一个子节点
if (current == root) {
root = current.rightChild;
} else if (isLeftChild) {
parent.leftChild = current.rightChild;
} else {
parent.rightChild = current.rightChild;
}
} else {
//查找要删除节点的中序后继
Node successor = getSuccessor(current);
if (current == root){
root = successor;
}else if(isLeftChild){
parent.leftChild = successor;
}else{
parent.rightChild = successor;
}
successor.leftChild = current.leftChild;
}
return true;
}
三、其他
- 使用中序遍历二叉查找树,输出数列是有序的,时间复杂度是O(n),因此是十分高效的。
- 有序的数列插入到二叉查找树中,将产生不平衡的二叉查找树,影响二叉查找树的性能。
- 学习二叉树可多画图推演