数据结构之二叉树

来源:互联网 发布:手机dns修复软件 编辑:程序博客网 时间:2024/06/05 04:47

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

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

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

树的相关概念

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

树其实是范畴更广的的特例

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


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

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

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

如果树的每个节点最多有两个子节点,则称为二叉树。如果节点的子节点可以多余两个,称为多路树

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


需要注意的是,从树的根到任意节点有且只有一条路径可以到达,下图所示就不是一棵树,它违背了这一原则


二叉搜索树

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

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


关于树,还有一个平衡树非平衡树的概念。非平衡就是说树的大部分节点在根的一边,如下图所示:


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

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

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

[java] view plain copy print?
  1.   //树节点的封装类  
  2. public class Node {  
  3.       int age;  
  4.       String name;  
  5.       Node leftChild;  //左子节点的引用  
  6.       Node rightChild; //右子节点的引用  
  7.        
  8.       public Node(int age,String name){  
  9.              this.age = age;  
  10.              this.name = name;  
  11.       }  
  12.        
  13.       //打印该节点的信息  
  14.       public void displayNode(){  
  15.              System.out.println("name:"+name+",age:"+age);  
  16.       }  
  17. }  


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

[java] view plain copy print?
  1. Person{  
  2.        private int age;  
  3.        private String name;  
  4.   
  5.        public void setAge(int age){  
  6.               this.age = age;  
  7.        }  
  8.   
  9.        public int getAge(){  
  10.               return this.age;  
  11.        }  
  12.   
  13.        public void setName(String name){  
  14.               this.name = name;  
  15.        }  
  16.   
  17.        public String getName(){  
  18.               return this.name;  
  19.        }  
  20.   
  21. }  

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

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

查找

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

比如我们要在树中查找57,执行的搜索路线如下图所示:

 

插入

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


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

遍历

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

有三种简单的方法遍历树:前序遍历、中序遍历、后序遍历。二叉搜索树最常用的方法是中序遍历,中序遍历二叉搜索树会使所有的节点按关键字升序被访问到

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

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

2、访问这个节点

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

遍历可以应用于任何二叉树,而不只是二叉搜索树。遍历的节点并不关心节点的关键字值,它只看这个节点是否有子节点

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


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

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

查找最值

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

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

 

删除节点

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

1、该节点没有子节点

2、该节点有一个子节点

3、该节点有两个子节点

第一种没有子节点的情况很简单,只需将父节点指向它的引用设置为null即可:


第二种情况也不是很难,这个节点有两个连接需要处理:父节点指向它的引用和它指向子节点的引用。无论要删除的节点下面有多复杂的子树,只需要将它的子树上移:


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

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


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


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

5    15   20  25   30   35   40

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


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


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

 

上图中,75为欲删除节点,77为它的后继节点,树变化的步骤如下:

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

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

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

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

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

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

 

下面我们就根据上面的分析,写出一个完整的二叉搜索树类,该类中,如果有重复值,插入到右子节点,查找时也只返回第一个找到的节点

[java] view plain copy print?
  1.      import java.util.ArrayList;  
  2.      import java.util.List;  
  3.    
  4.      //二叉搜索树的封装类  
  5.      public class BinaryTree {  
  6.       private Node root;  //根节点  
  7.        
  8.       public BinaryTree(){  
  9.              root = null;  
  10.       }  
  11.        
  12.       //按关键字查找节点  
  13.       public Node find(int key){  
  14.              Node cur = root;  //从根节点开始查找  
  15.               
  16.              if(cur == null){  //如果树为空,直接返回null  
  17.                     return null;  
  18.              }  
  19.               
  20.              while(cur.age != key){  
  21.                     if(cur.age < key){  
  22.                            cur = cur.leftChild;  //如果关键字比当前节点小,转向左子节点  
  23.                     }else{  
  24.                            cur = cur.leftChild;  //如果关键字比当前节点大,转向右子节点  
  25.                     }  
  26.                      
  27.                     if(cur == null){  //没有找到结果,搜索结束  
  28.                            return null;  
  29.                     }  
  30.              }  
  31.              return cur;  
  32.       }  
  33.        
  34.       //插入新节点  
  35.       public void insert(Node node){  
  36.              if(root == null){  
  37.                     root = node;  //如果树为空,则新插入的节点为根节点  
  38.              }else{  
  39.                     Node cur = root;   
  40.                      
  41.                     while(true){   
  42.                            if(node.age < cur.age){  
  43.                                   if(cur.leftChild == null){  //找到了要插入节点的父节点  
  44.                                          cur.leftChild = node;  
  45.                                          return;  
  46.                                   }  
  47.                                   cur = cur.leftChild;  
  48.                            }else{  
  49.                                   if(cur.rightChild == null){  //找到了要插入节点的父节点  
  50.                                          cur.rightChild = node;  
  51.                                          return;  
  52.                                   }  
  53.                                   cur = cur.rightChild;  
  54.                            }  
  55.                     }  
  56.              }  
  57.       }  
  58.        
  59.       //删除指定节点  
  60.       public boolean delete(Node node){  
  61.              if(root == null){  
  62.                     return false;  //如果为空树,直接返回false  
  63.              }  
  64.               
  65.              boolean isLeftChild = true;  //记录目标节点是否为父节点的左子节点  
  66.              Node cur= root;  //要删除的节点  
  67.              Node parent = null//要删除节点的父节点  
  68.               
  69.              while(cur.age != node.age){  //确定要删除节点和它的父节点  
  70.                     parent = cur;  
  71.                     if(node.age < cur.age){  //目标节点小于当前节点,跳转左子节点  
  72.                            cur = cur.leftChild;  
  73.                     }else{//目标节点大于当前节点,跳转右子节点  
  74.                            isLeftChild = false;  
  75.                            cur = cur.rightChild;  
  76.                     }  
  77.                     if(cur == null){  
  78.                            return false;  //没有找到要删除的节点  
  79.                     }  
  80.              }  
  81.        
  82.              if(cur.leftChild == null && cur.rightChild == null){  //目标节点为叶子节点(无子节点)  
  83.                     if(cur == root){  //要删除的为根节点  
  84.                            root = null;  
  85.                     }else if(isLeftChild){  
  86.                            //要删除的不是根节点,则该节点肯定有父节点,该节点删除后,需要将父节点指向它的引用置空  
  87.                            parent.leftChild = null;  
  88.                     }else{  
  89.                            parent.rightChild = null;  
  90.                     }  
  91.              }else if(cur.leftChild == null){  //只有一个右子节点  
  92.                     if(cur == root){  
  93.                            root = cur.rightChild;  
  94.                     }else if(isLeftChild){  
  95.                            parent.leftChild = cur.rightChild;  
  96.                     }else{  
  97.                            parent.rightChild = cur.rightChild;  
  98.                     }  
  99.              }else if(cur.rightChild == null){  //只有一个左子节点  
  100.                     if(cur == root){  
  101.                            root = cur.leftChild;  
  102.                     }else if(isLeftChild){  
  103.                            parent.leftChild = cur.leftChild;  
  104.                     }else{  
  105.                            parent.rightChild = cur.leftChild;  
  106.                     }  
  107.              }else{  //有两个子节点  
  108.                     //第一步要找到欲删除节点的后继节点  
  109.                     Node successor = cur.rightChild;   
  110.                     Node successorParent = null;  
  111.                     while(successor.leftChild != null){  
  112.                            successorParent = successor;  
  113.                            successor = successor.leftChild;  
  114.                     }  
  115.                     //欲删除节点的右子节点就是它的后继,证明该后继无左子节点,则将以后继节点为根的子树上移即可  
  116.                     if(successorParent == null){   
  117.                            if(cur == root){  //要删除的为根节点,则将后继设置为根,且根的左子节点设置为欲删除节点的做左子节点  
  118.                                   root = successor;  
  119.                                   root.leftChild = cur.leftChild;  
  120.                            }else if(isLeftChild){  
  121.                                   parent.leftChild = successor;  
  122.                                   successor.leftChild = cur.leftChild;  
  123.                            }else{  
  124.                                   parent.rightChild = successor;  
  125.                                   successor.leftChild = cur.leftChild;  
  126.                            }  
  127.                     }else//欲删除节点的后继不是它的右子节点  
  128.                            successorParent.leftChild = successor.rightChild;  
  129.                            successor.rightChild = cur.rightChild;  
  130.                            if(cur == root){   
  131.                                   root = successor;  
  132.                                   root.leftChild = cur.leftChild;  
  133.                            }else if(isLeftChild){  
  134.                                   parent.leftChild = successor;  
  135.                                   successor.leftChild = cur.leftChild;  
  136.                            }else{  
  137.                                   parent.rightChild = successor;  
  138.                                   successor.leftChild = cur.leftChild;  
  139.                            }  
  140.                     }  
  141.              }  
  142.               
  143.              return true;  
  144.       }  
  145.        
  146.       public static final int PREORDER = 1;   //前序遍历  
  147.       public static final int INORDER = 2;    //中序遍历  
  148.       public static final int POSTORDER = 3;  //中序遍历  
  149.        
  150.       //遍历  
  151.       public void traverse(int type){  
  152.              switch(type){  
  153.              case 1:  
  154.                     System.out.print("前序遍历:\t");  
  155.                     preorder(root);  
  156.                     System.out.println();  
  157.                     break;  
  158.              case 2:  
  159.                     System.out.print("中序遍历:\t");  
  160.                     inorder(root);  
  161.                     System.out.println();  
  162.                     break;  
  163.              case 3:  
  164.                     System.out.print("后序遍历:\t");  
  165.                     postorder(root);  
  166.                     System.out.println();  
  167.                     break;  
  168.              }  
  169.       }  
  170.        
  171.       //前序遍历  
  172.       public void preorder(Node currentRoot){  
  173.              if(currentRoot != null){  
  174.                     System.out.print(currentRoot.age+"\t");  
  175.                     preorder(currentRoot.leftChild);  
  176.                     preorder(currentRoot.rightChild);  
  177.              }  
  178.       }  
  179.        
  180.       //中序遍历,这三种遍历都用了迭代的思想  
  181.       public void inorder(Node currentRoot){  
  182.              if(currentRoot != null){  
  183.                     inorder(currentRoot.leftChild);  //先对当前节点的左子树对进行中序遍历  
  184.                     System.out.print(currentRoot.age+"\t"); //然后访问当前节点  
  185.                     inorder(currentRoot.rightChild);  //最后对当前节点的右子树对进行中序遍历  
  186.              }  
  187.       }  
  188.        
  189.       //后序遍历  
  190.       public void postorder(Node currentRoot){  
  191.              if(currentRoot != null){  
  192.                     postorder(currentRoot.leftChild);  
  193.                     postorder(currentRoot.rightChild);  
  194.                     System.out.print(currentRoot.age+"\t");  
  195.              }  
  196.       }  
  197.        
  198.       //私有方法,用迭代方法来获取左子树和右子树的最大深度,返回两者最大值  
  199.       private int getDepth(Node currentNode,int initDeep){  
  200.              int deep = initDeep;  //当前节点已到达的深度  
  201.              int leftDeep = initDeep;  
  202.              int rightDeep = initDeep;  
  203.              if(currentNode.leftChild != null){  //计算当前节点左子树的最大深度  
  204.                     leftDeep = getDepth(currentNode.leftChild, deep+1);  
  205.              }  
  206.              if(currentNode.rightChild != null){  //计算当前节点右子树的最大深度  
  207.                     rightDeep = getDepth(currentNode.rightChild, deep+1);  
  208.              }  
  209.               
  210.              return Math.max(leftDeep, rightDeep);  
  211.       }  
  212.        
  213.       //获取树的深度  
  214.       public int getTreeDepth(){  
  215.              if(root == null){  
  216.                     return 0;  
  217.              }  
  218.              return getDepth(root,1);  
  219.       }  
  220.        
  221.       //返回关键值最大的节点  
  222.       public Node getMax(){  
  223.              if(isEmpty()){  
  224.                     return null;  
  225.              }  
  226.              Node cur = root;  
  227.              while(cur.rightChild != null){  
  228.                     cur = cur.rightChild;  
  229.              }  
  230.              return cur;  
  231.       }  
  232.        
  233.       //返回关键值最小的节点  
  234.       public Node getMin(){  
  235.              if(isEmpty()){  
  236.                     return null;  
  237.              }  
  238.              Node cur = root;  
  239.              while(cur.leftChild != null){  
  240.                     cur = cur.leftChild;  
  241.              }  
  242.              return cur;  
  243.       }  
  244.        
  245.       //以树的形式打印出该树  
  246.       public void displayTree(){  
  247.              int depth = getTreeDepth();  
  248.              ArrayList<Node> currentLayerNodes = new ArrayList<Node> ();  
  249.              currentLayerNodes.add(root);  //存储该层所有节点  
  250.              int layerIndex = 1;  
  251.              while(layerIndex <= depth){  
  252.                     int NodeBlankNum = (int)Math.pow(2, depth-layerIndex)-1;  //在节点之前和之后应该打印几个空位  
  253.                     for(int i = 0;i<currentLayerNodes.size();i++){  
  254.                            Node node = currentLayerNodes.get(i);  
  255.                            printBlank(NodeBlankNum);   //打印节点之前的空位  
  256.                             
  257.                            if(node == null){  
  258.                                   System.out.print("*\t");  //如果该节点为null,用空位代替  
  259.                            }else{  
  260.                                   System.out.print("*  "+node.age+"\t");  //打印该节点  
  261.                            }  
  262.                             
  263.                            printBlank(NodeBlankNum);  //打印节点之后的空位  
  264.                            System.out.print("*\t");   //补齐空位  
  265.                     }  
  266.                     System.out.println();  
  267.                     layerIndex++;  
  268.                     currentLayerNodes = getAllNodeOfThisLayer(currentLayerNodes);  //获取下一层所有的节点  
  269.              }  
  270.       }  
  271.        
  272.       //获取指定节点集合的所有子节点  
  273.       private ArrayList getAllNodeOfThisLayer(List parentNodes){  
  274.              ArrayList list = new ArrayList<Node>();  
  275.              Node parentNode;  
  276.              for(int i=0;i<parentNodes.size();i++){  
  277.                     parentNode = (Node)parentNodes.get(i);  
  278.                     if(parentNode != null){   
  279.                            if(parentNode.leftChild != null){  //如果上层的父节点存在左子节点,加入集合  
  280.                                   list.add(parentNode.leftChild);  
  281.                            }else{  
  282.                                   list.add(null);  //如果上层的父节点不存在左子节点,用null代替,一样加入集合  
  283.                            }  
  284.                            if(parentNode.rightChild != null){  
  285.                                   list.add(parentNode.rightChild);  
  286.                            }else{  
  287.                                   list.add(null);  
  288.                            }  
  289.                     }else{  //如果上层父节点不存在,用两个null占位,代表左右子节点  
  290.                            list.add(null);  
  291.                            list.add(null);  
  292.                     }  
  293.              }  
  294.              return list;  
  295.       }  
  296.        
  297.       //打印指定个数的空位  
  298.       private void printBlank(int num){  
  299.              for(int i=0;i<num;i++){  
  300.                     System.out.print("*\t");  
  301.              }  
  302.       }  
  303.        
  304.       //判空  
  305.       public boolean isEmpty(){  
  306.              return (root == null);  
  307.       }  
  308.        
  309.       //判断是否为叶子节点  
  310.       public boolean isLeaf(Node node){  
  311.              return (node.leftChild != null || node.rightChild != null);  
  312.       }  
  313.        
  314.       //获取根节点  
  315.       public Node getRoot(){  
  316.              return root;  
  317.       }  
  318.        
  319. }  


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

原创粉丝点击