zoukankan      html  css  js  c++  java
  • 数据结构之二叉树

    通过前面的学习,我们知道,有序数组能够利用二分查找法高速的查找特定的值,时间复杂度为O(log2N),可是插入数据时非常慢,时间复杂度为O(N);链表的插入和删除速度都非常快,时间复杂度为O(1),可是查找特定值非常慢,时间复杂度为O(N)

    那么,有没有一种数据结构既能像有序数组那样高速的查找数据,又能像链表那样高速的插入数据呢?树就能满足这样的要求。只是依旧是以算法的复杂度为代价

    在编程的世界里,有一个真理叫“复杂度守恒定律”(当然,这是我杜撰的),一个程序当它减少了一个方面的复杂度,必定会在其它方面添加复杂度。这就跟谈恋爱一样,也没有无缘无故的爱,没有无缘无故的恨,当你跟程序谈恋爱时,没有无缘无故的易用性,也没有无缘无故的复杂度

    树的相关概念

    我们先从广义上来讨论一下树的概念

    树事实上是范畴更广的的特例

    以下是一个普通的非二叉树


    在程序中,节点一般用来表示实体,也就是数据结构里存储的那些数据项,在java这种面向对象的编程语言中,经常使用节点来表示对象

    节点间的边表示关联节点间的路径,沿着路径,从一个节点到还有一个节点非常easy,也非常快,在树中,从一个节点到还有一个节点的唯一方法就是顺着边前进。java语言中,经常使用引用来表示边(C/C++中一般使用指针)

    树的顶层总是仅仅有一个节点,它通过边连接到第二层的多个节点,然后第二层也能够通过边连接到第三层,以此类推。所以树的顶部小,底部大,呈倒金字塔型,这和现实世界中的树是相反的

    假设树的每一个节点最多有两个子节点,则称为二叉树。假设节点的子节点能够多余两个,称为多路树

    有非常多关于树的术语,在这里不做过多的文字解释,以下给出一个图例,通过它能够直观地理解树的路径、根、父节点、子节点、叶节点、子树、层等概念


    须要注意的是,从树的根到随意节点有且仅仅有一条路径能够到达,下图所看到的就不是一棵树,它违背了这一原则


    二叉搜索树

    我们从一种特殊的、使用非常广泛的二叉树入手:二叉搜索树

    二叉搜索树的特点是,一个节点的左子节点的keyword值小于这个节点,右子节点的keyword值大于或等于这个父节点。下图就是一个二叉搜索树的演示样例:


    关于树,另一个平衡树非平衡树的概念。非平衡就是说树的大部分节点在根的一边,例如以下图所看到的:


    树的不平衡是由数据项插入的顺序造成的。假设keyword是随机插入的,树会更趋向于平衡,假设插入顺序是升序或者降序,则全部的值都是右子节点或左子节点,这样生成的树就会不平衡了。非平衡树的效率会严重退化

    接下来我们就用java语言实现一个二叉搜索树,并给出查找、插入、遍历、删除节点的方法

    首先要有一个封装节点的类,这个类包括节点的数据以及它的左子节点和右子节点的引用

      //树节点的封装类
    public class Node {
          int age;
          String name;
          Node leftChild;  //左子节点的引用
          Node rightChild; //右子节点的引用
         
          public Node(int age,String name){
                 this.age = age;
                 this.name = name;
          }
         
          //打印该节点的信息
          public void displayNode(){
                 System.out.println("name:"+name+",age:"+age);
          }
    }


    以上agename两个属性用来代表该节点存储的信息,更好的方法是将这些属性封装成一个对象,比如:

          Person{
                 private int age;
                 private String name;
     
                 public void setAge(int age){
                        this.age = age;
                 }
     
                 public int getAge(){
                        return this.age;
                 }
     
                 public void setName(String name){
                        this.name = name;
                 }
     
                 public String getName(){
                        return this.name;
                 }
     
          }

          这样做才更符合“面向对象”的编程思想。只是如今我们的重点是数据结构而非编程思想,所以在程序中简化了

    因为树的结构和算法相对复杂,我们先逐步分析一下查找、插入等操作的思路,然后再写出整个的java

    查找

    我们已经知道,二叉搜索树的特点是左子节点小于父节点,右子节点大于或等于父节点。查找某个节点时,先从根节点入手,假设该元素值小于根节点,则转向左子节点,否则转向右子节点,以此类推,直到找到该节点,或者到最后一个叶子节点依旧没有找到,则证明树中没有该节点

    比方我们要在树中查找57,运行的搜索路线例如以下图所看到的:

     

    插入

    插入一个新节点首先要确定插入的位置,这个过程类似于查找一个不存在的节点。例如以下图所看到的:


    找到要插入的位置之后,将父节点的左子节点或者右子节点指向新节点就可以

    遍历

    遍历的意思是依据一种特定顺序訪问树的每个节点

    有三种简单的方法遍历树:前序遍历、中序遍历、后序遍历。二叉搜索树最经常使用的方法是中序遍历,中序遍历二叉搜索树会使全部的节点按keyword升序被訪问到

    遍历树最简单的方法是递归。用该方法时,仅仅须要做三件事(初始化时这个节点是根):

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

    2、訪问这个节点

    3、调用自身来遍历节点的右子树

    遍历能够应用于不论什么二叉树,而不仅仅是二叉搜索树。遍历的节点并不关心节点的keyword值,它仅仅看这个节点是否有子节点

    下图展示了中序遍历的过程:


    对于每一个节点来说,都是先訪问它的左子节点,然后訪问自己,然后在訪问右子节点

    假设是前序遍历呢?就是先訪问父节点,然后左子节点,最后右子节点;同理,后序遍历就是先訪问左子节点,在訪问右子节点,最后訪问父节点。所谓的前序、中序、后序是针对父节点的訪问顺序而言的

    查找最值

    在二叉搜索树中,查找最大值、最小是是非常easy实现的,从根循环訪问左子节点,直到该节点没有左子节点为止,该节点就是最小值;从根循环訪问右子节点,直到该节点没有右子节点为止,该节点就是最大值

    下图就展示了查找最小值的过程:

     

    删除节点

    树的删除节点操作是最复杂的一项操作。该操作须要考虑三种情况考虑:

    1、该节点没有子节点

    2、该节点有一个子节点

    3、该节点有两个子节点

    第一种没有子节点的情况非常easy,仅仅需将父节点指向它的引用设置为null就可以:


    另外一种情况也不是非常难,这个节点有两个连接须要处理:父节点指向它的引用和它指向子节点的引用。不管要删除的节点以下有多复杂的子树,仅仅须要将它的子树上移:


    另一种特殊情况须要考虑,就是要删除的是根节点,这时就须要把它唯一的子节点设置成根节点

    以下来看最复杂的第三种情况:要删除的节点由连个子节点。显然,这时候不能简单地将子节点上移,由于该节点有两个节点,右子节点上移之后,该右子节点的左子节点和右子节点又怎么安排呢?


    这是应该想起,二叉搜索树是依照关键升序排列,对每个keyword来说,比它keyword值高的节点是它的中序后继,简称后继。删除有两个子节点的节点,应该用它的中序后继来替代该节点


    上图中,我们先列出中序遍历的顺序:

    5    15   20  25   30   35   40

    能够看到,25的后继是35,所以应该用30来替代25的位置。实际上就是找到比欲删除节点的keyword值大的集合中的最小值。从树的结构上来说,就是从欲删除节点的右子节点開始,依次跳到下一层的左子节点,直到该左子节点没有左子节点为止。下图就是找后继节点的演示样例:


    从上图中能够看到,后集结点有两种情况:一种是欲删除节点的右子节点没有左子节点,那么它本身就是后继节点,此时,仅仅须要将以此后继节点为根的子树移到欲删除节点的位置:


    还有一种情况是欲删除节点的右子节点有左子节点,这样的情况就比較复杂,以下来逐步分析。首先应该意识到,后继节点是肯定没有左子节点的,可是可能会有右子节点

     

    上图中,75为欲删除节点,77为它的后继节点,树变化的过程例如以下:

    1、把87的左子节点设置为79

    2、把77的右子节点设为以87为根的子树;

    3、把50的右子节点设置为以77为根的子树;

    4、把77的左子节点设置为62

    到此为止,删除操作最终分析完成,包括了全部可能出现的情况。可见,二叉树的删除是一件很棘手的工作,那么我们就该反思了,删除是必需要做的任务吗?有没有一种方法避开这样的烦人的操作?有困难要上,没有困难创造困难也要上的二货精神是不能提倡的

    在删除操作不是非常多的情况下,能够在节点类中添加一个布尔字段,来作为该节点是否已删除的标志。在进行其它操作,比方查找时,之前对该节点是否已删除进行推断。这样的思路有点逃避责任,可是在非常多时候还是非常管用的。本例中为了更好的深入理解二叉树,会採用原始的、复杂的删除方法

     

    以下我们就依据上面的分析,写出一个完整的二叉搜索树类,该类中,假设有反复值,插入到右子节点,查找时也仅仅返回第一个找到的节点

         import java.util.ArrayList;
         import java.util.List;
     
         //二叉搜索树的封装类
         public class BinaryTree {
          private Node root;  //根节点
         
          public BinaryTree(){
                 root = null;
          }
         
          //按keyword查找节点
          public Node find(int key){
                 Node cur = root;  //从根节点開始查找
                
                 if(cur == null){  //假设树为空,直接返回null
                        return null;
                 }
                
                 while(cur.age != key){
                        if(cur.age < key){
                               cur = cur.leftChild;  //假设keyword比当前节点小,转向左子节点
                        }else{
                               cur = cur.leftChild;  //假设keyword比当前节点大,转向右子节点
                        }
                       
                        if(cur == null){  //没有找到结果,搜索结束
                               return null;
                        }
                 }
                 return cur;
          }
         
          //插入新节点
          public void insert(Node node){
                 if(root == null){
                        root = node;  //假设树为空,则新插入的节点为根节点
                 }else{
                        Node cur = root; 
                       
                        while(true){ 
                               if(node.age < cur.age){
                                      if(cur.leftChild == null){  //找到了要插入节点的父节点
                                             cur.leftChild = node;
                                             return;
                                      }
                                      cur = cur.leftChild;
                               }else{
                                      if(cur.rightChild == null){  //找到了要插入节点的父节点
                                             cur.rightChild = node;
                                             return;
                                      }
                                      cur = cur.rightChild;
                               }
                        }
                 }
          }
         
          //删除指定节点
          public boolean delete(Node node){
                 if(root == null){
                        return false;  //假设为空树,直接返回false
                 }
                
                 boolean isLeftChild = true;  //记录目标节点是否为父节点的左子节点
                 Node cur= root;  //要删除的节点
                 Node parent = null; //要删除节点的父节点
                
                 while(cur.age != node.age){  //确定要删除节点和它的父节点
                        parent = cur;
                        if(node.age < cur.age){  //目标节点小于当前节点,跳转左子节点
                               cur = cur.leftChild;
                        }else{//目标节点大于当前节点,跳转右子节点
                               isLeftChild = false;
                               cur = cur.rightChild;
                        }
                        if(cur == null){
                               return false;  //没有找到要删除的节点
                        }
                 }
         
                 if(cur.leftChild == null && cur.rightChild == null){  //目标节点为叶子节点(无子节点)
                        if(cur == root){  //要删除的为根节点
                               root = null;
                        }else if(isLeftChild){
                               //要删除的不是根节点,则该节点肯定有父节点,该节点删除后,须要将父节点指向它的引用置空
                               parent.leftChild = null;
                        }else{
                               parent.rightChild = null;
                        }
                 }else if(cur.leftChild == null){  //仅仅有一个右子节点
                        if(cur == root){
                               root = cur.rightChild;
                        }else if(isLeftChild){
                               parent.leftChild = cur.rightChild;
                        }else{
                               parent.rightChild = cur.rightChild;
                        }
                 }else if(cur.rightChild == null){  //仅仅有一个左子节点
                        if(cur == root){
                               root = cur.leftChild;
                        }else if(isLeftChild){
                               parent.leftChild = cur.leftChild;
                        }else{
                               parent.rightChild = cur.leftChild;
                        }
                 }else{  //有两个子节点
                        //第一步要找到欲删除节点的后继节点
                        Node successor = cur.rightChild; 
                        Node successorParent = null;
                        while(successor.leftChild != null){
                               successorParent = successor;
                               successor = successor.leftChild;
                        }
                        //欲删除节点的右子节点就是它的后继,证明该后继无左子节点,则将以后继节点为根的子树上移就可以
                        if(successorParent == null){ 
                               if(cur == root){  //要删除的为根节点,则将后继设置为根,且根的左子节点设置为欲删除节点的做左子节点
                                      root = successor;
                                      root.leftChild = cur.leftChild;
                               }else if(isLeftChild){
                                      parent.leftChild = successor;
                                      successor.leftChild = cur.leftChild;
                               }else{
                                      parent.rightChild = successor;
                                      successor.leftChild = cur.leftChild;
                               }
                        }else{ //欲删除节点的后继不是它的右子节点
                               successorParent.leftChild = successor.rightChild;
                               successor.rightChild = cur.rightChild;
                               if(cur == root){ 
                                      root = successor;
                                      root.leftChild = cur.leftChild;
                               }else if(isLeftChild){
                                      parent.leftChild = successor;
                                      successor.leftChild = cur.leftChild;
                               }else{
                                      parent.rightChild = successor;
                                      successor.leftChild = cur.leftChild;
                               }
                        }
                 }
                
                 return true;
          }
         
          public static final int PREORDER = 1;   //前序遍历
          public static final int INORDER = 2;    //中序遍历
          public static final int POSTORDER = 3;  //中序遍历
         
          //遍历
          public void traverse(int type){
                 switch(type){
                 case 1:
                        System.out.print("前序遍历:	");
                        preorder(root);
                        System.out.println();
                        break;
                 case 2:
                        System.out.print("中序遍历:	");
                        inorder(root);
                        System.out.println();
                        break;
                 case 3:
                        System.out.print("后序遍历:	");
                        postorder(root);
                        System.out.println();
                        break;
                 }
          }
         
          //前序遍历
          public void preorder(Node currentRoot){
                 if(currentRoot != null){
                        System.out.print(currentRoot.age+"	");
                        preorder(currentRoot.leftChild);
                        preorder(currentRoot.rightChild);
                 }
          }
         
          //中序遍历,这三种遍历都用了迭代的思想
          public void inorder(Node currentRoot){
                 if(currentRoot != null){
                        inorder(currentRoot.leftChild);  //先对当前节点的左子树对进行中序遍历
                        System.out.print(currentRoot.age+"	"); //然后訪问当前节点
                        inorder(currentRoot.rightChild);  //最后对当前节点的右子树对进行中序遍历
                 }
          }
         
          //后序遍历
          public void postorder(Node currentRoot){
                 if(currentRoot != null){
                        postorder(currentRoot.leftChild);
                        postorder(currentRoot.rightChild);
                        System.out.print(currentRoot.age+"	");
                 }
          }
         
          //私有方法,用迭代方法来获取左子树和右子树的最大深度,返回两者最大值
          private int getDepth(Node currentNode,int initDeep){
                 int deep = initDeep;  //当前节点已到达的深度
                 int leftDeep = initDeep;
                 int rightDeep = initDeep;
                 if(currentNode.leftChild != null){  //计算当前节点左子树的最大深度
                        leftDeep = getDepth(currentNode.leftChild, deep+1);
                 }
                 if(currentNode.rightChild != null){  //计算当前节点右子树的最大深度
                        rightDeep = getDepth(currentNode.rightChild, deep+1);
                 }
                
                 return Math.max(leftDeep, rightDeep);
          }
         
          //获取树的深度
          public int getTreeDepth(){
                 if(root == null){
                        return 0;
                 }
                 return getDepth(root,1);
          }
         
          //返回关键值最大的节点
          public Node getMax(){
                 if(isEmpty()){
                        return null;
                 }
                 Node cur = root;
                 while(cur.rightChild != null){
                        cur = cur.rightChild;
                 }
                 return cur;
          }
         
          //返回关键值最小的节点
          public Node getMin(){
                 if(isEmpty()){
                        return null;
                 }
                 Node cur = root;
                 while(cur.leftChild != null){
                        cur = cur.leftChild;
                 }
                 return cur;
          }
         
          //以树的形式打印出该树
          public void displayTree(){
                 int depth = getTreeDepth();
                 ArrayList<Node> currentLayerNodes = new ArrayList<Node> ();
                 currentLayerNodes.add(root);  //存储该层全部节点
                 int layerIndex = 1;
                 while(layerIndex <= depth){
                        int NodeBlankNum = (int)Math.pow(2, depth-layerIndex)-1;  //在节点之前和之后应该打印几个空位
                        for(int i = 0;i<currentLayerNodes.size();i++){
                               Node node = currentLayerNodes.get(i);
                               printBlank(NodeBlankNum);   //打印节点之前的空位
                              
                               if(node == null){
                                      System.out.print("*	");  //假设该节点为null,用空位取代
                               }else{
                                      System.out.print("*  "+node.age+"	");  //打印该节点
                               }
                              
                               printBlank(NodeBlankNum);  //打印节点之后的空位
                               System.out.print("*	");   //补齐空位
                        }
                        System.out.println();
                        layerIndex++;
                        currentLayerNodes = getAllNodeOfThisLayer(currentLayerNodes);  //获取下一层全部的节点
                 }
          }
         
          //获取指定节点集合的全部子节点
          private ArrayList getAllNodeOfThisLayer(List parentNodes){
                 ArrayList list = new ArrayList<Node>();
                 Node parentNode;
                 for(int i=0;i<parentNodes.size();i++){
                        parentNode = (Node)parentNodes.get(i);
                        if(parentNode != null){ 
                               if(parentNode.leftChild != null){  //假设上层的父节点存在左子节点,增加集合
                                      list.add(parentNode.leftChild);
                               }else{
                                      list.add(null);  //假设上层的父节点不存在左子节点,用null取代,一样增加集合
                               }
                               if(parentNode.rightChild != null){
                                      list.add(parentNode.rightChild);
                               }else{
                                      list.add(null);
                               }
                        }else{  //假设上层父节点不存在,用两个null占位,代表左右子节点
                               list.add(null);
                               list.add(null);
                        }
                 }
                 return list;
          }
         
          //打印指定个数的空位
          private void printBlank(int num){
                 for(int i=0;i<num;i++){
                        System.out.print("*	");
                 }
          }
         
          //判空
          public boolean isEmpty(){
                 return (root == null);
          }
         
          //推断是否为叶子节点
          public boolean isLeaf(Node node){
                 return (node.leftChild != null || node.rightChild != null);
          }
         
          //获取根节点
          public Node getRoot(){
                 return root;
          }
         
    }


    displayTree方法依照树的形状打印该树。对一颗深度为3的二叉树的打印效果例如以下图所看到的:

  • 相关阅读:
    升级windows 11小工具
    windows 10更新升级方法
    您需要了解的有关 Oracle 数据库修补的所有信息
    Step by Step Apply Rolling PSU Patch In Oracle Database 12c RAC Environment
    Upgrade Oracle Database Manually from 12.2.0.1 to 19c
    如何应用版本更新 12.2.0.1.210420(补丁 32507738 – 2021 年 4 月 RU)
    xtrabackup 安装、备份和恢复
    Centos_Lvm expand capacity without restarting CentOS
    Centos_Lvm_Create pv vg lv and mount
    通过全备+relaylog同步恢复被drop的库或表
  • 原文地址:https://www.cnblogs.com/zfyouxi/p/4332728.html
Copyright © 2011-2022 走看看