二叉树详解

我们先了解有序数组和链表两种数据结构:有序数组,可以通过二分查找法快速的查询特定的值,时间复杂度为O(logN),可是插入删除时效率低,平均要移动N/2个元素,时间复杂度为O(N)。链表:查询效率低,平均要比较N/2个元素,时间复杂度O(N),插入和删除效率较高,O(1)。二叉树的特点是结合了有序数组和链表的优点,能像有序数组那样快速的查找,又能像链表那样快速的插入和删除。操作二叉搜索树的时间复杂度是O(logN)。

1.定义

二叉树:树中的每个节点最多只能有两个子节点,这样的树是二叉树。

二叉搜索树:一个节点的左子节点的关键字值小于这个父节点,右子节点的关键字值大于等于这个父节点。

平衡二叉搜索树:它是一颗裸空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两棵子树都是平衡二叉树。它的实现方式有:红黑树,AVL

2.结构图

二叉树由节点和边构成,每个节点包含的有关键字值,以及其他数据。接下来我们通过代码看如何对二叉搜索树进行插入,删除,查找,遍历等操作。

3.查找操作

先定义节点Node

public class Node {
    int key; //key 关键字值
    double data; //存储的数据
    Node leftChild; //左子节点的引用
    Node rightChild; //右子节点的引用

    //显示该节点内容
    public void displayNode(){
        System.out.println("key="+key+",data="+data);
    }
}

定义二叉树Tree

public class Tree {
    
    //根节点
    public Node root;
    
}

 查找方法:将关键字值key与根节点的关键字值做比较,小于root节点的关键字值,进入root节点的左子树进行比较;否则如果大于,进入root节点的右子树进行比较。依次比较下去,直到key的值等于某个节点的关键字值,则将该节点Node返回。如果没有找到

符合条件的节点,那么返回null

//查询效率和有序数组中的二分查找法一样,时间复杂度为O(log2N)
    public Node find(int key){
        Node current = root;
        while (current.key != key) {
            if(key < current.key)
                current = current.leftChild;
            else{
                current = current.rightChild;
            }
            //表示没有找到符合条件的节点
            if (null == current)
                return null;
        }
        return current;
    }

4.插入操作

插入操作,先要确定新节点要插入的位置,这个就是查找的过程;然后确定位置后,将新节点newNode作为父节点parent的的左子节点或者右子节点

   //插入方法:查找最后一个不为null的节点parent作为要插入节点的父节点,newNode作为它的左子节点或者右子节点
    public void insert(int key,double data){
        Node newNode = new Node();
        newNode.key = key;
        newNode.data = data;
        if(null == root){
            root = newNode;
        }else{
            Node current = root;
            Node parent;
            while(true){
                parent = current;
                if(key < current.key){
                    //go left
                    current = current.leftChild;
                    if(null == current){
                        parent.leftChild = newNode;
                        return;
                    }
                    
                }else{
                    //go right
                    current = current.rightChild;
                    if(null == current){
                        parent.rightChild = newNode;
                        return;
                    }
                }
                
            }
        }
    }

5.遍历操作

遍历操作,是指根据特定的顺序访问树的每一个节点。遍历方法有:中序遍历,前序遍历,后序遍历

中序遍历,会使所有节点按照关键字值的升序被访问到。遍历时,通常使用递归调用的方法,初始参数是树的根节点。中序遍历会有三个步骤:

调用自身来遍历该节点的左子树

* 访问这个节点

* 调用自身来遍历该节点的右子树

    //中序遍历二叉树:按key值的升序排序
    public void orderByMiddle(Node localRoot){
        if(null != localRoot){
            orderByMiddle(localRoot.leftChild);
            System.out.println(localRoot.data);
            orderByMiddle(localRoot.rightChild);
        }
    }
    
    //前序遍历二叉树
    public void preOrder(Node localRoot){
        if(null != localRoot){
            System.out.println(localRoot.data);
            preOrder(localRoot.leftChild);
            preOrder(localRoot.rightChild);
        }
    }
    
    //后序遍历二叉树
    public void postOrder(Node localRoot){
        if(null != localRoot){
            postOrder(localRoot.leftChild);
            postOrder(localRoot.rightChild);
            System.out.println(localRoot.data);
        }
    }

6.查找最大值和最小值

查找最大值,从根节点开始走向右子节点,然后一直走向右子节点,直到最后一个不为null的右子节点,就是最大值。查找最小值,是类似的,一直走向左子节点。

    //查找最大值
    public Node maxNum() {
        Node current,last = null;
        current = root;
        while (null != current) {
            last = current;
            current = current.rightChild;
        }
        return last;
    }
    
    //查找最小值
    public Node miniNum() {
        Node current,last = null;
        current = root;
        while (null != current) {
            last = current;
            current = current.leftChild;
        }
        return last;
    }

 7.删除操作

对于删除操作的逻辑如下:

* 要先判断被删除的节点的情况,然后分别处理,被删除节点可能是:是叶节点,只有一个子节点,有两个子节点

* 如果被删除节点有两个子节点:找到要删除节点的后继节点,然后让后继节点替换该节点。由于二叉搜索树要满足的特性是一个节点的左子节点的key值要比该节点小,右子节点的key值要比该节点大,所以,一个节点的后继节点就是比该节点key值大的最小的那个节点。也就是该节点的右子节点的左子节点,再左子节点,直到最后一个不是null的左子节点,就是该节点的后继节点。找到后继节点后,就将后继节点替换当前节点,涉及到各个节点引用的改变,有四个地方的引用改变,也就是代码中的abcd四个步骤。

如果要删除的节点有两个子节点,看图:

//删除方法
    //1.要删除的节点是叶节点 2.只有一个字节点  3.有两个子节点
    //步骤:1.先找到要删除的节点
    public boolean delete(int key){
        //1.先查找到要删除的节点
        Node current = root;
        Node parent = root;
        //标识被删除的节点是否是左子节点
        boolean isLeftChild = true;
        while(key != current.key){
            parent = current;
            if(key < current.key){
                current = current.leftChild;
                isLeftChild = true;
            }else{
                current = current.rightChild;
                isLeftChild = false;
            }
            
            //没有找到要删除的节点
            if(null == current){
                return false;
            }
        }
        
        //2.如果该节点没有子节点
        if(null == current.leftChild && null == current.rightChild){
            //判断该节点是否是根节点
            if(current == root){
                root = null;
            }else if(isLeftChild) {
                parent.leftChild = null;
            }else{
                parent.rightChild = null;
            }
            //3.如果该节点只有一个子节点:左子节点
        }else if(null == current.rightChild){
            //被删除的节点如果是根节点    
            if(current == root){
                root = current.leftChild;
            }else if(isLeftChild){
                parent.leftChild = current.leftChild;
            }else{
                parent.rightChild = current.leftChild;
            }
            //4.如果该节点只有一个子节点:右子节点
        }else if(null == current.leftChild){
            if(current == root){
                root = current.rightChild;
            }else if(isLeftChild){
                parent.leftChild = current.rightChild;
            }else{
                parent.rightChild = current.rightChild;
            }
            //5.如果要删除的节点有两个子节点:需要找到该节点的后继节点代替该节点,后继节点就是key值比当前节点大的那个最小的值
        }else{
            //先找到后继节点successor
            Node successor = getSuccessor(current);
            if(current == root){
                root = successor;
            }else if(isLeftChild){
                //c 将后继节点赋值给要删除节点的父节点的左子节点 
                parent.leftChild = successor;
            }else{
                //c 将后继节点赋值给要删除节点的父节点的右子节点
                parent.rightChild = successor;
            }
            //d 将要删除节点的左子节点赋值给后继节点的左子节点
            successor.leftChild = current.leftChild;
        }
        
        return true;
        
    }
    
    
    //查找后继节点,并替换部分引用 a,b
    private Node getSuccessor(Node delNode){
        Node successorParent = delNode;
        Node successor = delNode;
        Node current = delNode.rightChild;
        while (current != null) {
            successorParent = successor;
            successor = current;
            current = current.leftChild;
        }
        
        if(successor != delNode.rightChild){
            successorParent.leftChild = successor.rightChild; //a 把后继节点的右子节点赋值给后继节点的父节点的左子节点
            successor.rightChild = delNode.rightChild; //b 把要删除节点的右子节点赋值给后继节点的右子节点 
        }
        
        return successor;
    }
    

8.用数组表示二叉树

其实,除了以上那种方式表示二叉树外,还可以用数组表示二叉树。用数组时,节点存储在数组中,节点再数组中的位置对应于它在树中的位置,通过下图可以理解它的存储方式。

9.重复关键字 

由于二叉搜索树的特性是:右子节点的key值大于等于父节点,所以在insert操作中,关键字key值相同的节点插入到与它相同的节点的右子节点处。在查询操作时,获取到的是多个相同节点的第一个节点。

注:以上图片摘自《Java数据结构和算法》这本书

原文地址:https://www.cnblogs.com/51life/p/9305328.html