从Java看数据结构之——树和他的操作集

写在前面

树这种数据结构在计算机世界中有广泛的应用,比如操作系统中用到了红黑树,数据库用到了B+树,编译器中的语法树,内存管理用到了堆(本质上也是树),信息论中的哈夫曼编码等等等等。而树的实现和他的操作集也是笔试面试中常见的考核项目。

树的实现

与C语言的结构体+指针的实现方式不同,Java中树的实现当然是基于类。以二叉树为例,树的实现可以用下面这样的形式:

 1 public class BinaryTree<T extends Comparable<T>> {
 2     private BinaryTree<T> root;
 3     //定义存储内容:内容、左子树、右子树
 4     public class Node<T extends Comparable<T>>{
 5         T data;
 6         BinaryTree<T> left;
 7         BinaryTree<T> right;
 8         //也可以有父节点
 9         BinaryTree<T> parent;
10 
11         public Node(T data, BinaryTree<T> left, BinaryTree<T> right, BinaryTree<T> parent) {
12             this.data = data;
13             this.left = left;
14             this.right = right;
15             this.parent = parent;
16         }
17     }
18     
19     //定义方法:增、删、查、改。
20 
21     public boolean insert(int x){
22         ...
23     }
24     public boolean delete(int x){
25         ...
26     }
27 }

要注意,为了清晰树中数据的含义,我们一般不直接把树节点当做其成员变量,而是在其中新建内部类将其打包。针对子节点不止2个的多叉树,只需要把上面的left,right等合并成一个BinaryTree<T>类型的数组即可。

树的遍历

树的遍历应当是我们的基本功,里面涉及了后面图论算法中的深度优先搜索(DFS)广度优先搜索(BFS),因此虽然简单但也很重要。在树的遍历中,往往对应着递归算法和非递归算法(层序遍历除外)。递归算法往往能写出很精简的代码,但是运行过程中会占用大量的系统资源。因此,非递归算法在这一点上还是更有优势。我们也应该更着重记忆他的非递归算法。面试过程中,我们甚至可以和面试官讨论所要求的写法。

先来看看前中后序遍历递归写法

1 public void preOrderTraversal(BinaryTree<T> tree){
2     if(tree==null) return;
3     System.out.print(tree.node.data+" ");
4     preOrderTraversal(tree.node.left);
5     preOrderTraversal(tree.node.right);
6 }

 

 

1 //中序遍历
2 public void inOrderTraversal(BinaryTree<T> tree){
3     if(tree==null) return;
4     inOrderTraversal(tree.node.left);
5     System.out.print(tree.node.data+" ");
6     inOrderTraversal(tree.node.right);
7 }

 

1 //后序遍历
2 public void pastOrderTraversal(BinaryTree<T> tree){
3     if(tree==null) return;
4     pastOrderTraversal(tree.node.left);
5     pastOrderTraversal(tree.node.right);
6     System.out.print(tree.node.data+" ");
7 }

 

看起来相当简单,也相当好记对吧。再来看非递归怎么在Java中实现:

来自陈越姥姥的算法课件

树的前序遍历和中序遍历的非递归算法都是用堆栈来实现,具体实现如下。

 1 public void preOrderTraversal(){
 2     Stack<BinaryTree<T>> st = new Stack<>();
 3     BinaryTree<T> tmp = this;
 4     while(tmp!=null || !st.empty()){
 5         while(tmp!=null){
 6             System.out.print(tmp.node.data+" ");
 7             st.push(tmp);
 8             tmp=tmp.node.left;//一路向左
 9         }
10         if(!st.empty()){
11             tmp=st.pop();
12             tmp=tmp.node.right;//偶尔向右
13         }
14     }
15     System.out.println();
16 }

 

 1 public void inOrderTraversal(){
 2     Stack<BinaryTree<T>> st = new Stack<>();
 3     BinaryTree<T> tmp = this;
 4     while(tmp!=null || !st.empty()){
 5         while(tmp!=null){
 6             st.push(tmp);
 7             tmp=tmp.node.left;
 8         }
 9         if(!st.empty()){
10             tmp=st.pop();
11             //输出变到这里,仅此而已
12             System.out.println(tmp.node.data);
13             tmp=tmp.node.right;
14         }
15     }
16 }

后序遍历思路相似,由于后序遍历是第三次经过才进行输出(见上图),而上面的while循环和if代码块分别表示第一次见和第二次见,因此需要增加一个标志位在每一个节点,用来标明是不是在if代码块中第一次看见它。这么说并不明显,自己实现一遍应该会印象深刻一点。

 1 //后续非递归
 2 public void PastOrderTraversal(){
 3     BinaryTree<T> Tmp = this;
 4     Stack<BinaryTree<T>> st = new Stack<>();
 5     while(Tmp!=null || !st.empty()){
 6         while(Tmp!=null){
 7             st.push(Tmp);
 8             Tmp=Tmp.node.left;
 9         }
10         if(!st.empty()){//if代码块
11             Tmp=st.pop();
12             if(Tmp.node.IsFirstTraversal){//这里判断是不是第一次进if代码块
13                 Tmp.node.IsFirstTraversal=false;//进来过了,置位false,下一次就可以输出了
14                 st.push(Tmp);
15                 Tmp=Tmp.node.right;
16             }
17             else{//不是第一次,输出
18                 System.out.print(Tmp.node.data+" ");
19             }
20         }
21     }
22 }

 

层序遍历只有非递归写法,没有递归的方法。层序遍历利用的数据结构是队列遍历应该先从根节点开始,首先将根节点入队,然后开始执行循环:节点出队、访问节点(print)、左右儿子入队。

 

 1 //层序遍历
 2 public void levelOrderTraversal(){
 3     Queue<BinaryTree<T>> tQueue = new ArrayDeque<>();
 4     BinaryTree<T> tmp = this;
 5     tQueue.add(tmp);
 6     while(!tQueue.isEmpty()){
 7         tmp = tQueue.remove();
 8         System.out.print(tmp.node.data+" ");
 9         if(tmp.node.left!=null) tQueue.add(tmp.node.left);
10         if(tmp.node.right!=null) tQueue.add(tmp.node.right);
11     }
12 }

操作集

1.插入

 1     private BinaryTree<T> insert(BinaryTree<T> t, Node<T> n){
 2         if(t==null) {
 3             t = new BinaryTree<>();
 4             t.node = n;
 5             return t;
 6         }
 7         int cmp = n.data.compareTo(t.node.data);
 8         if(cmp==0) return null;//节点存在
 9         else if(cmp<0) t.node.left = insert(t.node.left, n);
10         else t.node.right = insert(t.node.right, n);
11         return t;
12     }
13 
14     public void insert(T x){
15         Node<T> n = new Node<>(x,null, null, null);
16         if(this.node==null){
17             this.node = n;
18             return;
19         }
20         insert(this,n);
21     }

2.查找

普通查找比较好实现,这个就是利用了二分查找的思想。

 1    /**
 2      * 查找
 3      * @param t 树
 4      * @param x 查找的值
 5      * @return 值所在树
 6      */
 7     private BinaryTree<T> find(BinaryTree<T> t, T x){
 8         int cmp = x.compareTo(t.node.data);
 9         if(cmp==0){//找到了
10             return t;
11         }
12         else if(cmp<0) return find(t.node.left,x);
13         else return find(t.node.right,x);
14     }
15 
16     public BinaryTree<T> find(T x){
17         return find(this, x);
18     }

 

posted @ 2019-03-07 20:59  狄利克雷  阅读(319)  评论(0编辑  收藏  举报