树的基本概念
树的组成
树(tree)是一种抽象数据类型(ADT),用来模拟具有树状结构性质的数据集合。它是由n(n>0)个有限节点通过连接它们的边组成一个具有层次关系的集合。
- 节点:上图的圆圈,都是表示节点。节点一般代表一些实体,在java面向对象编程中,节点一般代表对象。
- 边:连接节点的线称为边,边表示节点的关联关系。一般从一个节点到另一个节点的唯一方法就是沿着一条顺着有边的道路前进。在Java当中通常表示引用。
树的常用术语
- 路径:顺着节点的边从一个节点走到另一个节点,所经过的节点的顺序排列就称为“路径”。
- 根:树顶端的节点称为根。一棵树只有一个根,如果要把一个节点和边的集合称为树,那么从根到其他任何一个节点都必须有且只有一条路径。A是根节点。
- 父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;B是D的父节点。
- 子节点:一个节点含有的子树的根节点称为该节点的子节点;D是B的子节点。
- 兄弟节点:具有相同父节点的节点互称为兄弟节点;比如上图的D和E就互称为兄弟节点。
- 叶节点:没有子节点的节点称为叶节点,也叫叶子节点,比如上图的H、E、F、G都是叶子节点。
- 子树:每个节点都可以作为子树的根,它和它所有的子节点、子节点的子节点等都包含在子树中。
- 节点的层次:从根开始定义,根为第0层,根的子节点为第1层,以此类推。
- 高度:对于任意节点n,n的高度为从n到一片树叶的最长路径长。
- 深度:对于任意节点n,n的深度为从根到n的唯一路径长;
二叉树和二叉搜索树
二叉树
二叉树:树的每个节点最多只能有两个子节点。
且二叉树的子节点称为“左子节点”和“右子节点”。上图的D,E分别是B的左子节点和右子节点。
二叉搜索树
若二叉树的左子树不空,则左子树上所有结点的值均小于它的根结点的值;若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;它的左、右子树也分别为二叉排序树。
二叉搜索树的操作
遍历数:
根据特定的顺序访问树的每一个节点,比较常用的有前序遍历、中序遍历和后序遍历。
- 中序遍历:左子树–>根节点–>右子树
- 前序遍历:根节点–>左子树–>右子树
- 后序遍历:左子树–>右子树–>根节点
二叉搜索树常用的是中序遍历(数值从小到大)。
删除节点:
删除节点是二叉搜索树中最复杂的操作,删除的节点有三种情况,前两种比较简单,但是第三种却很复杂。
- 该节点是叶节点(没有子节点) 。那么直接将该节点删除就好。
- 该节点有一个子节点 。直接删除该节点,用该节点唯一的子节点代替它的位置。
- 该节点有两个子节点。该节点右子树的左子…子节点替换当前节点。
通过上面的删除分类讨论,我们发现删除其实是挺复杂的,那么其实我们可以不用真正的删除该节点,只需要在Node类中增加一个标识字段isDelete,当该字段为true时,表示该节点已经删除,反之没有删除。那么我们在做比如find()等操作的时候,要先判断 isDelete字段是否为true。这样删除的节点并不会改变树的结构。
二叉搜索树代码实现
封装节点的类
package tree;
/**
* 封装节点的类
*/
public class Node {
// 二叉树中的节点
private int data;//节点的数据
private Node leftChild;//左子节点指针
private Node rightChild;//右子节点指针
//无参数构造方法
public Node(){}
//有参数构造方法
public Node(int data){
this.data = data;
}
//添加辅助方法
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
public Node getLeftChild() {
return leftChild;
}
public void setLeftChild(Node leftChild) {
this.leftChild = leftChild;
}
public Node getRightChild() {
return rightChild;
}
public void setRightChild(Node rightChild) {
this.rightChild = rightChild;
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
", leftChild=" + leftChild +
", rightChild=" + rightChild +
'}';
}
}
二叉搜索树的结构类:
package tree;
/**
* 二搜索树基本的类结构
*/
public class BinaryTree {
private Node root;
//构造方法
public BinaryTree(){
root = null;
}
//查找某个节点
public Node find(int data){
Node current = root;
while (current!=null){
//当前节点不是null的时候才继续循环
if(data < current.getData()){
current = current.getLeftChild();
}else if (data > current.getData()){
current = current.getRightChild();
}else {
return current;
}
}
//循环完了,证明树里根本没有我们要找的节点
return null;
}
//插入节点
public boolean insert(int data){
Node newNode = new Node(data);
if(root == null){
//空树
root = newNode;
return true;
}else {
//树不为空的时候需要找到正确的插入位置
Node current = root;
Node parentNode = null;
while (current != null){
if(data < current.getData()){
//数据小于当前节点的数据,就考虑当前节点的左子节点
parentNode = current;
current = current.getLeftChild();
if(current == null){
//找到了要插入的位置
parentNode.setLeftChild(newNode);//把新节点设置为父节点的左子节点
return true;
}
}else {
//数据大于等于当前节点的数据,就考虑当前节点的右子节点
//也包含了data=current.getData()的情况
parentNode = current;
current = current.getRightChild();
if(current == null){
//找到了要插入的位置
parentNode.setRightChild(newNode);//把新节点设置为父节点的左子节点
return true;
}
}
}
}
return false; //如果到这一步,就证明插入失败了
}
//遍历节点
//中序
public void midOrder(Node current){
//使用递归
if (current == null){
//边界条件
return;
}else {
midOrder(current.getLeftChild());//遍历左子树
System.out.print(current.getData()+" ");//输出当前节点
midOrder(current.getRightChild());//遍历右子树
}
}
//前序
public void preOrder(Node current){
//使用递归
if (current == null){
//边界条件
return;
}else {
System.out.print(current.getData()+" ");//输出当前节点
preOrder(current.getLeftChild());//遍历左子树
preOrder(current.getRightChild());//遍历右子树
}
}
//后序
public void afterOrder(Node current){
//使用递归
if (current == null){
//边界条件
return;
}else {
afterOrder(current.getLeftChild());//遍历左子树
afterOrder(current.getRightChild());//遍历右子树
System.out.print(current.getData()+" ");//输出当前节点
}
}
//查找最大最小值
public Node getMaxNode(){
Node current = root;
Node maxNode = current;
while (current != null){
maxNode = current;
current=current.getRightChild();
}
return maxNode;
}
public Node getMinNode(){
Node current = root;
Node minNode = current;
while (current != null){
minNode = current;
current = current.getLeftChild();
}
return minNode;
}
//删除节点
public boolean delete(int data){
//找到要删除的节点
Node current = root;
Node parent = null;
boolean isLeftChild = false;//判断当前节点是其父节点的左子节点还是右子节点
while (current.getData() != data){
parent = current;
if (data <current.getData()){
current = current.getLeftChild();
isLeftChild = true;
}else {
//大于的情况。等于的情况不会出现,因为等于的情况会结束循环
current = current.getRightChild();
isLeftChild =false;
}
if (current == null){
return false;//没有找到节点,返回false
}
}
//正常结束循环,表示找到了要删除的节点
//删除找到的节点
if (current.getLeftChild() == null && current.getRightChild() == null){
//找到的节点没有子节点
if(current == root){
root = null;
}else {
if (isLeftChild){
//当前的节点是父节点的左节点
parent.setLeftChild(null);
}else {
//当前的节点是父节点的右节点
parent.setRightChild(null);
}
}
return true;
}else if (current.getLeftChild() != null && current.getRightChild() == null){
//找到的节点有左子节点没有右子节点
if (current == root){
root = current.getLeftChild();
}else {
if (isLeftChild){
parent.setLeftChild(current.getLeftChild());
}else {
parent.setRightChild(current.getLeftChild());
}
}
return true;
}else if (current.getLeftChild() == null && current.getRightChild() != null){
//找到的节点有右子节点没有左子节点
if (current == root){
root = current.getRightChild();
}else {
if (isLeftChild){
parent.setLeftChild(current.getRightChild());
}else {
parent.setRightChild(current.getRightChild());
}
}
return true;
}else {
//该节点有左子节点也有右子节点
//先找到被删除节点的右子树中最小的节点
Node replacedNode = getReplacedNode(current);
//用右子树中的最小节点替换要删除的节点
if(current == root){
root = replacedNode;
}else {
if(isLeftChild){
parent.setLeftChild(replacedNode);
}else {
parent.setRightChild(replacedNode);
}
}
//把要删除的节点的左子节点赋给替换节点的左子节点
replacedNode.setLeftChild(current.getLeftChild());
return true;
}
}
public Node getReplacedNode( Node delNode){
Node replacedNode = delNode;
Node replacedNodeParent = delNode;
//找到替换节点:被删除节点右子树的最左节点
Node grnCurrent = delNode.getRightChild();
while (grnCurrent != null){
replacedNodeParent = replacedNode;
replacedNode = grnCurrent;
grnCurrent = grnCurrent.getLeftChild();
}
//把替换节点变为被删除节点右子树的根节点
if (replacedNode != delNode.getRightChild()){//替换节点不是被删除节点的右节点
//把替换节点的右子节点变成它父节点的左子节点
replacedNodeParent.setLeftChild(replacedNode.getRightChild());
//把被删除节点的右子节点设置为替换节点的右节点
replacedNode.setRightChild(delNode.getRightChild());
//替换节点没有左子节点
}
//如果替换节点是被删除节点的右子节点,则不需要上面的if操作
return replacedNode;//我们要找的右子树中的最小的节点
}
}
测试类:
package tree;
public class BinaryTreeTest {
public static void main(String[] args) {
BinaryTree bt = new BinaryTree();
//插入
bt.insert(40);
System.out.println(bt.insert(36));
bt.insert(37);
bt.insert(85);
bt.insert(49);
bt.insert(88);
Node root = bt.find(40);
bt.midOrder(root);
System.out.println();
bt.delete(85);
bt.midOrder(root);
System.out.println();
bt.delete(49);
bt.midOrder(root);
System.out.println();
bt.delete(40);
root = bt.find(88);
bt.midOrder(root);
}
}
二叉搜索树的效率
二叉搜索树在查找、插入或删除节点等操作的时间复杂度为O(logn),底数为2。
在有1000000 个数据项的无序数组和链表中,查找数据项平均会比较500000 次,但是在有1000000个节点的二叉树中,只需要20次或更少的比较即可。
有序数组可以很快的找到数据项,但是插入数据项的平均需要移动 500000 次数据项,在1000000个节点的二叉树中插入数据项需要20次或更少比较,在加上很短的时间来连接数据项。
同样,从1000000个数据项的数组中删除一个数据项平均需要移动 500000 个数据项,而在1000000个节点的二叉树中删除节点只需要20次或更少的次数来找到他,然后在花一点时间来找到它的后继节点,一点时间来断开节点以及连接后继节点。
所以,树的所有常用数据结构的操作都有很高的效率。遍历可能不如其他操作快,但是在大型数据库中,遍历是很少使用的操作,它更常用于程序中的辅助算法来解析算术或其它表达式。