数据结构与算法系列第二篇——树(二叉搜索树)

8大数据结构

在这里插入图片描述

定义

树是由结点组成的(可能是非线性的)且不存在着任何环的一种数据结构。
形状如下图:
在这里插入图片描述

能解决什么问题

既要有链表那样快速的插入和删除,又要有数组那样的快速查找。树实现了这些特点。因此,树是数据结构中至关重要的一个技术知识点,也是我们要重点学习的对象。

二叉树

定义

每个节点最多有两个子节点的树就是二叉树。
形状如下图:
在这里插入图片描述

二叉搜索树

定义

一个节点的左子节点的关键字值小于这个节点,右子节点的关键字值大于或等于这个节点的二叉树
形状如下图:
在这里插入图片描述

package test11;

public class Node {
    //数据项 key
    int iData;
    //数据项 该节点其他数据
    double fData;
    //左子节点
    Node leftChild;
    //右子节点
    Node rightChild;
}

查找节点

在这里插入图片描述

public Node find(int key){
    //查找从根节点开始,因为只有根节点可以直接访问
    Node current = root;
    while(current.iData != key){
        if(key < current.iData){
            current = current.leftChild;
        }else{
            current = current.rightChild;
        }
        //找不到节点
        if(current == null){
            return null;
        }
    }
    //找到返回
    return current;
}

插入节点

在这里插入图片描述

思路

  • 1.将插入的数据作为节点进行创建。
  • 2.查找创建的节点的位置(和查找节点的思路一致)。
  • 3.将创建的节点插入到树中。
public void insert(int id,double dd){
        //将数据包装成节点
        Node newNode = new Node();
        newNode.iData =id;
        newNode.fData = dd;
        if(root == null){
            //如果该树为空树,将新创建的节点直接作为根节点
            root = newNode;
        }else{
            //从根节点开始查找
            Node current = root;
            //定义临时节点变量,后续要保存待插入新节点位置的父节点(重点理解)
            Node parent;
            while(true){
                //先保存
                parent = current;
                if(id<current.iData){
                    current = current.leftChild;
                    //找到了叶子节点(即找到了待插入新节点的位置)
                    if(current == null){
                        parent.leftChild = newNode;
                        return;
                    }
                }else{
                    current = current.rightChild;
                    if(current == null){
                        parent.rightChild = newNode;
                        return;
                    }
                }

            }
        }
    }

遍历

根据一种特定的顺序访问树的每一个节点。这个过程不如查找,插入和删除节点常用,因为速度不是特别快。
有三种方法可以遍历树,前序,中序,后序。二叉搜索树最常用的是中序遍历。

中序遍历

中序遍历二叉搜索树会使所有的节点按关键字值升序被访问到。如果需要在二叉树中创建有序的数据序列,这是一种方法。
遍历树的最简单的方法是使用递归(上一篇介绍过递归)。用递归的方法遍历整棵树要用一个节点作为参数。初始化时这个节点是根。这个方法需要做三件事:
1.调用自身来遍历节点的左子树
2.访问这个节点。
3.调用自身来遍历节点的右子树。
如下图所示:
在这里插入图片描述
遍历可以应用任何二叉树,而不只是二叉搜索树。这个遍历的原理不关心节点的关键字值,他只是看这个节点是否有子节点。

public void inOrder(Node localRoot){
        //基值情况是参数传入localRoot等于null
        if(localRoot != null){
            inOrder(localRoot.leftChild);
            System.out.println(localRoot.iData + " ");
            inOrder(localRoot.rightChild);
        }
    }

前序和后序遍历

二叉树(非二叉搜索树)可以用于表示包括二元运算符号+,-,*,/的算术表达式。根节点保存运算符号,其他节点存变量名(A、B或C)或者保存运算符号。每一棵子树都是合法的代数表达式。
在这里插入图片描述
A*(B+C)这样写是中缀表达法;中序遍历树得到正确的中序序列A*B+C,但是需要自己加上小括号。
前序和后序遍历:
这两种遍历方法和中序遍历的三个步骤相同,知识步骤的顺序不同。
前序:
1.访问节点。
2.调用自身遍历该节点的左子树
3.调用自身遍历该节点的右子树
因此:用前序遍历图8.11的树得到的结果:*A+BC
后序:
1.调用自身遍历该节点的左子树
2.调用自身遍历该节点的右子树
3.访问节点。
因此:用后序遍历图8.11的树得到的结果:ABC+*

查找最大值和最小值

在二叉搜索树中得到最大值和最小值是很容易的事情
思路:
先走到根的左子节点处,然后接着走到那个子节点的左子节点,如此类推,直到找到一个没有左子节点的节点。这个节点就是最小值的节点。
如下图所示:
在这里插入图片描述

// An highlighted block
var foo = 'bar';

删除节点

删除节点是二叉搜索树中最复杂的操作,但是,删除节点在树的应用中非常重要。
删除节点要考虑三种情况
1.该节点是叶子节点(没有子节点)
2.该节点有一个子节点。
3.该节点有两个子节点。

情况1:删除没有子节点的节点(叶子节点)

要删除叶子节点,只需要改变该节点的父节点的对应子字段的值,由指向该节点改为null就可以了。要删除的节点依然存在,但它已经不是树的一部分了。
在这里插入图片描述

public boolean delete(int key){
        Node current = root;
        Node parent = root;
        //标记最后找到,即将删除的是左叶子节点还是右叶子节点
        boolean isLeftChild = true;
        while (current.iData != key){
            parent = current;
            if(key <current.iData){
                isLeftChild = true;
                current = current.leftChild;
            }else{
                isLeftChild = false;
                current= current.rightChild;
            }
            //未找到,退出方法
            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;
            }
        }
        //未完继续
    }

情况2:删除有一个子节点的节点

这个节点有两个连接:连向父节点的和连向它唯一的子节点的。需要从这个序列中“剪断”这个节点,把它的子节点直接连到它的父节点上。这个过程要求改变父节点适当的引用(左子节点或者右子节点),指向要删除节点的子节点。
如下图所示:
在这里插入图片描述
思路:
有四种情况:
要删除节点的子节点可能有左子节点或右子节点,并且每种情况中的要删除节点也可能是自己父节点的左子节点或者右子节点。
还有一种特殊情况:被删除节点可能是根,它没有父节点,只是被合适的子树所代替。
只展示关键代码部分:

//查找到之后进行删除操作
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){
        //重点理解 isLeftChild标记当前要删除的节点是左子节点,并且由上文可知,该节点下还有一个左子节点
        parent.leftChild = current.leftChild;
    }else{
        //isLeftChild为false标记当前要删除的节点是右子节点,并且由上文可知,该节点下还有一个左子节点
        //这种情况正是图中画的情况
        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;
    }
}

只关注要删除节点的子节点位置,要删除节点的位置。这两个因素是删除逻辑的关键

情况3:删除有两个子节点的节点

思路:
在二叉搜索树中的节点是按照升序的关键字值排列的,对每一个节点来说,比该节点的关键字值次高的节点是它的中序后继,可以简称该节点的后继。
删除有两个子节点的节点,用它的中序后继来代替该节点。
在这里插入图片描述

查找后继节点的算法

先找到待删除节点的右子节点,它的关键字值一定比待删除节点大。然后转到待删除节点的右子节点的左子节点那里(如果有的话),然后到这个左子节点的左子节点,以此类推,顺着左子节点的路径一直找下去。这个路径上的最后一个左子节点就是待删除节点的后继。
如果待删除节点的右子节点没有左子节点,那么这个右子节点本身就是后继。
如下图所示:
在这里插入图片描述

后继节点是delNode的右子节点

在这里插入图片描述

后继节点是delNode的右子节点的左后代

在这里插入图片描述

部分代码展示

只展示关键代码部分:

public boolean delete(int key){
        Node current = root;
        Node parent = root;
        //标记最后找到,即将删除的是左叶子节点还是右叶子节点
        boolean isLeftChild = true;
        while (current.iData != key){
            parent = current;
            if(key <current.iData){
                isLeftChild = true;
                current = current.leftChild;
            }else{
                isLeftChild = false;
                current= current.rightChild;
            }
            //未找到,退出方法
            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){
                //重点理解 isLeftChild标记当前要删除的节点是左子节点,并且由上文可知,该节点下还有一个左子节点
                parent.leftChild = current.leftChild;
            }else{
                //isLeftChild为false标记当前要删除的节点是右子节点,并且由上文可知,该节点下还有一个左子节点
                //这种情况正是图中画的情况
                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;
    }

    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;
            //组装后继节点的右子树,将删除节点的右子树赋给了后继节点。(重点理解)
            successor.rightChild = delNode.rightChild;
        }
        return successor;
    }

二叉树的时间复杂度

树的大部分操作都需要从上到下一层一层查找某个节点。一棵满二叉树,最底层的节点个数比树的其他节点多 1。因此,查找,插入或删除节点的操作大约有一半都需要找到最底层的节点。
设满二叉树的节点个数为N,层数为L。则节点和层数的关系为N=2的L次方-1。
那给定层数的树,查找的时间为L=log2(N+1)
在大O表示法中。表示为O(logN)

完整代码演示

Node.java

package test11;

public class Node {
    //数据项 key
    int iData;
    //数据项 该节点其他数据
    double fData;
    //左子节点
    Node leftChild;
    //右子节点
    Node rightChild;

    public void displayNode(){
        System.out.print("{");
        System.out.print(iData);
        System.out.print(", ");
        System.out.print(fData);
        System.out.print("} ");
    }
}

tree.java

package test11;

import java.util.Stack;

public class Tree {
    private Node root;

    public Tree() {
        this.root = null;
    }

    /**
     * 根据key查找
     * @param key
     * @return
     */
    public Node find(int key) {
        //查找从根节点开始,因为只有根节点可以直接访问
        Node current = root;
        while (current.iData != key) {
            if (key < current.iData) {
                current = current.leftChild;
            } else {
                current = current.rightChild;
            }
            //找不到节点
            if (current == null) {
                return null;
            }
        }
        //找到返回
        return current;
    }

    /**
     * 插入
     * @param id 节点key值
     * @param dd 节点所带的其它数据
     */
    public void insert(int id, double dd) {
        //将数据包装成节点
        Node newNode = new Node();
        newNode.iData = id;
        newNode.fData = dd;
        if (root == null) {
            //如果该树为空树,将新创建的节点直接作为根节点
            root = newNode;
        } else {
            //从根节点开始查找
            Node current = root;
            //定义临时节点变量,后续要保存待插入新节点位置的父节点(重点理解)
            Node parent;
            while (true) {
                //先保存
                parent = current;
                if (id < current.iData) {
                    current = current.leftChild;
                    //找到了叶子节点(即找到了待插入新节点的位置)
                    if (current == null) {
                        parent.leftChild = newNode;
                        return;
                    }
                } else {
                    current = current.rightChild;
                    if (current == null) {
                        parent.rightChild = newNode;
                        return;
                    }
                }

            }
        }

    }

    /**
     * 遍历方式
     * @param traverseType
     */
    public void traverse(int traverseType) {
        switch (traverseType) {
            case 1:
                System.out.println("前序遍历");
                preOrder(root);
                break;
            case 2:
                System.out.println("中序遍历");
                inOrder(root);
                break;
            case 3:
                System.out.println("后序遍历");
                postOrder(root);
                break;
        }
        System.out.println();
    }

    /**
     * 前序
     * @param localRoot
     */
    public void preOrder(Node localRoot) {
        //基值情况是参数传入localRoot等于null
        if (localRoot != null) {
            System.out.print(localRoot.iData + " ");
            preOrder(localRoot.leftChild);
            preOrder(localRoot.rightChild);
        }
    }

    /**
     * 中序
     * @param localRoot
     */
    public void inOrder(Node localRoot) {
        //基值情况是参数传入localRoot等于null
        if (localRoot != null) {
            inOrder(localRoot.leftChild);
            System.out.print(localRoot.iData + " ");
            inOrder(localRoot.rightChild);
        }
    }

    /**
     * 后序
     * @param localRoot
     */
    public void postOrder(Node localRoot) {
        //基值情况是参数传入localRoot等于null
        if (localRoot != null) {
            postOrder(localRoot.leftChild);
            postOrder(localRoot.rightChild);
            System.out.print(localRoot.iData + " ");
        }
    }

    /**
     * 删除节点
     * @param key
     * @return
     */
    public boolean delete(int key) {
        Node current = root;
        Node parent = root;
        //标记最后找到,即将删除的是左叶子节点还是右叶子节点
        boolean isLeftChild = true;
        while (current.iData != key) {
            parent = current;
            if (key < current.iData) {
                isLeftChild = true;
                current = current.leftChild;
            } else {
                isLeftChild = false;
                current = current.rightChild;
            }
            //未找到,退出方法
            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) {
                //重点理解 isLeftChild标记当前要删除的节点是左子节点,并且由上文可知,该节点下还有一个左子节点
                parent.leftChild = current.leftChild;
            } else {
                //isLeftChild为false标记当前要删除的节点是右子节点,并且由上文可知,该节点下还有一个左子节点
                //这种情况正是图中画的情况
                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;
    }

    /**
     * 获取删除节点的后继节点
     * @param delNode
     * @return
     */
    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;
            //组装后继节点的右子树,将删除节点的右子树赋给了后继节点。(重点理解)
            successor.rightChild = delNode.rightChild;
        }
        return successor;
    }

    /**
     * 展示
     */
    public void displayTree() {
        Stack globalStack = new Stack();
        globalStack.push(root);
        int nBlanks = 32;
        boolean isRowEmpty = false;
        System.out.println(
                "......................................................");
        while (isRowEmpty == false) {
            Stack localStack = new Stack();
            isRowEmpty = true;

            for (int j = 0; j < nBlanks; j++)
                System.out.print(' ');

            while (globalStack.isEmpty() == false) {
                Node temp = (Node) globalStack.pop();
                if (temp != null) {
                    System.out.print(temp.iData);
                    localStack.push(temp.leftChild);
                    localStack.push(temp.rightChild);

                    if (temp.leftChild != null ||
                            temp.rightChild != null)
                        isRowEmpty = false;
                } else {
                    System.out.print("--");
                    localStack.push(null);
                    localStack.push(null);
                }
                for (int j = 0; j < nBlanks * 2 - 2; j++)
                    System.out.print(' ');
            }  // end while globalStack not empty
            System.out.println();
            nBlanks /= 2;
            while (localStack.isEmpty() == false)
                globalStack.push(localStack.pop());
        }  // end while isRowEmpty is false
        System.out.println(
                "......................................................");
    }
}

TreeApp.java

package test11;

import java.util.Scanner;

public class TreeApp {
    public static void main(String[] args) {
        int value;
        Tree theTree = new Tree();
        theTree.insert(50,1.5D);
        theTree.insert(25,1.2D);
        theTree.insert(75,1.7D);
        theTree.insert(12,1.5D);
        theTree.insert(37,1.2D);
        theTree.insert(43,1.7D);
        theTree.insert(30,1.5D);
        theTree.insert(33,1.2D);
        theTree.insert(87,1.7D);
        theTree.insert(93,1.5D);
        theTree.insert(97,1.5D);
        while (true) {
            Scanner scanner = new Scanner(System.in);
            String input = scanner.next();
            switch (input){
                case "s":
                    theTree.displayTree();
                    break;
                case "i":
                    System.out.println("插入");
                    value = scanner.nextInt();
                    theTree.insert(value,value+0.9);
                    break;
                case "f":
                    System.out.println("查找");
                    value = scanner.nextInt();
                    Node find = theTree.find(value);
                    if(find != null){
                        find.displayNode();
                    }else{
                        System.out.println("未找到");
                    }
                    break;
                case "d":
                    System.out.println("删除");
                    value =scanner.nextInt();
                    boolean didDelete = theTree.delete(value);
                    if(didDelete){
                        System.out.println("已删除");
                    }else{
                        System.out.println("删除失败");
                    }
                    break;
                case "t":
                    System.out.println("测试遍历,输入1-前序,2-中序,3-后序");
                    value =scanner.nextInt();
                    theTree.traverse(value);
                    break;
                default:
                    System.out.println("不存在的指令");
            }
        }
    }
}

运行结果如下:
演示插入
在这里插入图片描述
演示查找
在这里插入图片描述
演示删除
75的后继是其右子节点
在这里插入图片描述
在这里插入图片描述
删除25, 由理论可知25的后继是30,看程序变化
在这里插入图片描述
删除叶子节点,比如45
在这里插入图片描述
演示遍历
在这里插入图片描述

小结

  • 树是由结点组成的(可能是非线性的)且不存在着任何环的一种数据结构。
  • 根是树中最顶端的节点;它没有父节点
  • 二叉树中,节点最多有两个子节点
  • 二叉搜索树中,所有A节点左边子孙节点的关键字值都比A小;所有右边子孙节点的关键字值都大于(等于)A
  • 树执行查找,插入,删除的时间复杂度都是O(logN)
  • 节点表示保存在树中的数据对象
  • 遍历树是按照某种顺序访问树中所有的节点
  • 最简单的遍历方法是前序,中序和后序
  • 非平衡树是指根左边的后代比右边多,或者相反。
  • 查找节点需要比较要找的关键字值和节点的关键字值,如果要找的关键字值小就转向那个节点的左子节点,如果大就转向右子节点。
  • 插入需要找到要插入新节点的位置并改变它的父节点的子字段来指向它。
  • 中序遍历是按照关键值的升序访问节点
  • 前序和后序遍历对解析代数表达式是有用的
  • 如果一个节点没有子节点,删除它只需要把它的父节点的子字段置为null即可
  • 如果一个节点有一个子节点,把它父节点的子字段置为它的子节点就可以删除它
  • 如果一个节点有两个子节点,删除它要用它的后继来代替它
  • A节点的后继是以A的右子节点为根的子树中关键值最小的那个节点
  • 删除操作中,如果节点有两个子节点,会根据后继是被删节点的右子节点还是被删节点右子节点的左子孙节点出现两种不同的情况。
原创文章 38 获赞 52 访问量 1万+

猜你喜欢

转载自blog.csdn.net/yemuxiaweiliang/article/details/105333500