转载请注明出处:http://www.cnblogs.com/wangyingli/p/5933257.htmlhtml
前面讲到的顺序表、栈和队列都是一对一的线性结构,这节讲一对多的线性结构——树。「一对多」就是指一个元素只能有一个前驱,但能够有多个后继。java
树(tree)是n(n>=0)个结点的有穷集。n=0时称为空树。在任意一个非空树中:(1)每一个元素称为结点(node);(2)仅有一个特定的结点被称为根结点或树根(root)。(3)当n>1时,其他结点可分为m(m≥0)个互不相交的集合T1,T2,……Tm,其中每个集合Ti(1<=i<=m)自己也是一棵树,被称做根的子树(subtree)。node
注意:算法
结点拥有的子树数被称为结点的度(Degree)。度为0的结点称为叶节点(Leaf)或终端结点,度不为0的结点称为分支结点。除根结点外,分支结点也被称为内部结点。结点的子树的根称为该结点的孩子(Child),该结点称为孩子的双亲或父结点。同一个双亲的孩子之间互称为兄弟。树的度是树中各个结点度的最大值。数组
结点的层次(Level)从根开始定义起,根为第一层,根的孩子为第二层。双亲在同一层的结点互为堂兄弟。树中结点的最大层次称为树的深度(Depth)或高度。若是将树中结点的各个子树当作从左到右是有次序的,不能互换的,则称该树为有序树,不然称为无序树。森林是m(m>=0)棵互不相交的树的集合。post
树的定义:this
因为树中每一个结点的孩子能够有多个,因此简单的顺序存储结构没法知足树的实现要求。下面介绍三种经常使用的表示树的方法:双亲表示法、孩子表示法和孩子兄弟表示法。设计
因为树中每一个结点都仅有一个双亲结点(根节点没有),咱们可使用指向双亲结点的指针来表示树中结点的关系。这种表示法有点相似于前面介绍的静态链表的表示方法。具体作法是以一组连续空间存储树的结点,同时在每一个结点中,设一个「游标」指向其双亲结点在数组中的位置。代码以下:3d
public class PTree<E> { private static final int DEFAULT_CAPACITY = 100; private int size; private Node[] nodes; private class Node() { E data; int parent; Node(E data, int parent) { this.data = data; this.parent = parent; } } public PTree() { nodes = new PTree.Node[DEFAULT_CAPACITY]; } }
因为根结点没有双亲结点,咱们约定根节点的parent域值为-1。树的双亲表示法以下所示:指针
这样的存储结构,咱们能够根据结点的parent域在O(1)的时间找到其双亲结点,可是只能经过遍历整棵树才能找到它的孩子结点。一种解决办法是在结点结构中增长其孩子结点的域,但若结点的孩子结点不少,结点结构将会变的很复杂。
因为树中每一个结点可能有多个孩子,能够考虑用多重链表,即每一个结点有多个指针域,每一个指针指向一个孩子结点,咱们把这种方法叫多重链表表示法。它有两种设计方案:
方案一:指针域的个数等于树的度。其结点结构能够表示为:
class Node() { E data; Node child1; Node child2; ... Node childn; }
对于上一节中的树,树的度为3,其实现为:
显然,当树中各结点的度相差很大时,这种方法对空间有很大的浪费。
方案二,每一个结点指针域的个数等于该结点的度,取一个位置来存储结点指针的个数。其结点结构能够表示为:
class Node() { E data; int degree; Node[] nodes; Node(int degree) { this.degree = degree; nodes = new Node[degree]; } }
对于上一节中的树,这种方法的实现为:
这种方法克服了浪费空间的缺点,但因为各结点结构不一样,在运算上会带来时间上的损耗。
为了减小空指针的浪费,同时又使结点相同。咱们能够将顺序存储结构和链式存储结构相结合。具体作法是:把每一个结点的孩子结点以单链表的形式连接起来,如果叶子结点则此单链表为空。而后将全部链表存放进一个一维数组中。这种表示方法被称为孩子表示法。其结构为:
代码表示:
public class CTree<E> { private static final int DEFAULT_CAPACITY = 100; private int size; private Node[] nodes; private class Node() { E data; ChildNode firstChild; } //链表结点 private class ChildNode() { int cur; //存放结点在nodes数组中的下标 ChildNode next; } public CTree() { nodes = new CTree.Node[DEFAULT_CAPACITY]; } }
这种结构对于查找某个结点的孩子结点比较容易,但若想要查找它的双亲或兄弟,则须要遍历整棵树,比较麻烦。能够将双亲表示法和孩子表示法相结合,这种方法被称为双亲孩子表示法。其结构以下:
其代码和孩子表示法的基本相同,只需在Node结点中增长parent域便可。
任意一棵树,它的结点的第一个孩子若是存在则是惟一的,它的右兄弟若是存在也是惟一的。所以,咱们可使用两个分别指向该结点的第一个孩子和右兄弟的指针来表示一颗树。其结点结构为:
class Node() { E data; Node firstChild; Node rightSib; }
其结构以下:
这个方法,能够方便的查找到某个结点的孩子,只需先经过firstChild找到它的第一个孩子,而后经过rightSib找到它的第二个孩子,接着一直下去,直到找到想要的孩子。若要查找某个结点的双亲和左兄弟,使用这个方法则比较麻烦。
这个方法最大的好处是将一颗复杂的树变成了一颗二叉树。这样就可使用二叉树的一些特性和算法了。
二叉树(Binary Tree)是每一个节点最多有两个子树的树结构。一般子树被称做“左子树”(left subtree)和“右子树”(right subtree)。
二叉树的特色:
以下图中,树1和树2是同一棵树,但它们是不一样的二叉树。
1)、斜树
全部的结点都只有左子树的二叉树叫左斜树。全部的结点都只有右子树的二叉树叫右斜树。这二者统称为斜树。
斜树每一层只有一个结点,结点的个数与二叉树的深度相同。其实斜树就是线性表结构。
2)、满二叉树
在一棵二叉树中,若是全部分支结点都存在左子树和右子树,而且全部叶子都在同一层上,这样的二叉树称为满二叉树。
满二叉树具备以下特色:
3)、彻底二叉树
若设二叉树的高度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h层有叶子结点,而且叶子结点都是从左到右依次排布,这就是彻底二叉树。
彻底二叉树的特色:
4)、平衡二叉树
平衡二叉树又被称为AVL树(区别于AVL算法),它是一棵二叉排序树,且具备如下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,而且左右两个子树都是一棵平衡二叉树
在二叉树的第i层上至多有2^i-1^个结点(i>=1)。
深度为k的二叉树至多有2^k^-1个结点(k>=1)。
对任何一棵二叉树T,若是其终端结点个数为n~0~,度为2的结点数为n~2~,则n~0~ = n~2~ + 1。
具备n个结点的彻底二叉树的深度为「log~2~n」+ 1(「x」表示不大于x的最大整数)。
二叉树是一种特殊的树,它的存储结构相对于前面谈到的通常树的存储结构要简单一些。
1)、顺序存储
二叉树的顺序存储结构就是用一维数组来存储二叉树中的结点。不使用数组的第一个位置。结点的存储位置反映了它们之间的逻辑关系:位置k的结点的双亲结点的位置为「k/2」,它的两个孩子结点的位置分别为2k和2k+1。
代码实现:
public class ArrayBinaryTree<E> { private static final int DEFAULT_DEPTH = 5; private int size = 0; private E[] datas; ArrayBinaryTree() { this(DEFAULT_DEPTH); } @SuppressWarnings("unchecked") ArrayBinaryTree(int depth) { datas = (E[]) new Object[(int)Math.pow(2, depth)]; } public boolean isEmpty() { return size == 0; } public int size(){ return size; } public E getRoot() { return datas[1]; } // 返回指定节点的父节点 public E getParent(int index) { checkIndex(index); if (index == 1) { throw new RuntimeException("根节点不存在父节点!"); } return datas[index/2]; } //获取右子节点 public E getRight(int index){ checkIndex(index*2 + 1); return datas[index * 2 + 1]; } //获取左子节点 public E getLeft(int index){ checkIndex(index*2); return datas[index * 2]; } //返回指定数据的位置 public int indexOf(E data){ if(data==null){ throw new NullPointerException(); } else { for(int i=0;i<datas.length;i++) { if(data.equals(datas[i])) { return i; } } } return -1; } //顺序添加元素 public void add(E element) { checkIndex(size + 1); datas[size + 1] = element; size++; } //在指定位置添加元素 public void add(E element, int parent, boolean isLeft) { if(datas[parent] == null) { throw new RuntimeException("index["+parent+"] is not Exist!"); } if(element == null) { throw new NullPointerException(); } if(isLeft) { checkIndex(2*parent); if(datas[parent*2] != null) { throw new RuntimeException("index["+parent*2+"] is Exist!"); } datas[2*parent] = element; }else { checkIndex(2*parent + 1); if(datas[(parent+1)*2]!=null) { throw new RuntimeException("index["+ parent*2+1 +"] is Exist!"); } datas[2*parent + 1] = element; } size++; } //检查下标是否越界 private void checkIndex(int index) { if(index <= 0 || index >= datas.length) { throw new IndexOutOfBoundsException(); } } public static void main(String[] args) { char[] data = {'A','B','C','D','E','F','G','H','I','J'}; ArrayBinaryTree<Character> abt = new ArrayBinaryTree<>(); for(int i=0; i<data.length; i++) { abt.add(data[i]); } System.out.print(abt.getParent(abt.indexOf('J'))); } }
一棵深度为k的右斜树,只有k个结点,但却须要分配2~k~-1个顺序存储空间。因此顺序存储结构通常只用于彻底二叉树。
2)、链式存储
二叉树每一个结点最多有两个孩子,因此为它设计一个数据域和两个指针域便可。咱们称这样的链表为二叉链表。其结构以下图:
代码以下:
import java.util.*; public class LinkedBinaryTree<E> { private List<Node> nodeList = null; private class Node { Node leftChild; Node rightChild; E data; Node(E data) { this.data = data; } } public Node getRoot() { return nodeList.get(0); } public void createBinTree(E[] array) { nodeList = new LinkedList<Node>(); for (int i = 0; i < array.length; i++) { nodeList.add(new Node(array[i])); } // 对前lasti-1个父节点按照父节点与孩子节点的数字关系创建二叉树 for (int i = 0; i < array.length / 2 - 1; i++) { nodeList.get(i).leftChild = nodeList.get(i * 2 + 1); nodeList.get(i).rightChild = nodeList.get(i * 2 + 2); } // 最后一个父节点:由于最后一个父节点可能没有右孩子,因此单独拿出来处理 int lastParent = array.length / 2 - 1; nodeList.get(lastParent).leftChild = nodeList .get(lastParent * 2 + 1); // 右孩子,若是数组的长度为奇数才创建右孩子 if (array.length % 2 == 1) { nodeList.get(lastParent).rightChild = nodeList.get(lastParent * 2 + 2); } } public static void main(String[] args) { Character[] data = {'A','B','C','D','E','F','G','H','I','J'}; LinkedBinaryTree<Character> ldt = new LinkedBinaryTree<>(); ldt.createBinTree(data); } }
二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树中全部结点,使得每一个结点被访问一次且仅被访问一次。
二叉树的遍历主要有四种:前序遍历、中序遍历、后序遍历和层序遍历。
1)、前序遍历
先访问根结点,而后遍历左子树,最后遍历右子树。
代码:
//顺序存储 public void preOrderTraverse(int index) { if (datas[index] == null) return; System.out.print(datas[index] + " "); preOrderTraverse(index*2); preOrderTraverse(index*2+1); } //链式存储 public void preOrderTraverse(Node node) { if (node == null) return; System.out.print(node.data + " "); preOrderTraverse(node.leftChild); preOrderTraverse(node.rightChild); }
2)、中序遍历
先遍历左子树,而后遍历根结点,最后遍历右子树。
//链式存储 public void inOrderTraverse(Node node) { if (node == null) return; inOrderTraverse(node.leftChild); System.out.print(node.data + " "); inOrderTraverse(node.rightChild); }
3)、后序遍历
先遍历左子树,而后遍历右子树,最后遍历根结点。
//链式存储 public void postOrderTraverse(Node node) { if (node == null) return; postOrderTraverse(node.leftChild); postOrderTraverse(node.rightChild); System.out.print(node.data + " "); }
4)、层序遍历
从上到下逐层遍历,在同一层中,按从左到右的顺序遍历。如上一节中的二叉树层序遍历的结果为ABCDEFGHIJ。
注意:
如前序遍历是ABC,后序遍历是CBA的二叉树有:
对于n个结点的二叉树,在二叉链存储结构中有n+1个空指针域,利用这些空指针域存放在某种遍历次序下该结点的前驱结点和后继结点的指针,这些指针被称为线索,加上线索的二叉树称为线索二叉树。
结点结构以下:
其中:
线索二叉树的结构图为:图中蓝色虚线为前驱,红色虚线为后继
代码以下:
public class ThreadedBinaryTree<E> { private TBTreeNode root; private int size; // 大小 private TBTreeNode pre; // 线索化的时候保存前驱 class TBTreeNode { E element; boolean lTag; //false表示指向孩子结点,true表示指向前驱或后继的线索 boolean rTag; TBTreeNode lChild; TBTreeNode rChild; public TBTreeNode(E element) { this.element = element; } } public ThreadedBinaryTree(E[] data) { this.pre = null; this.size = data.length; this.root = createTBTree(data, 1); } //构建二叉树 public TBTreeNode createTBTree(E[] data, int index) { if (index > data.length){ return null; } TBTreeNode node = new TBTreeNode(data[index - 1]); TBTreeNode left = createTBTree(data, 2 * index); TBTreeNode right = createTBTree(data, 2 * index + 1); node.lChild = left; node.rChild = right; return node; } /** * 将二叉树线索化 */ public void inThreading(TBTreeNode node) { if (node != null) { inThreading(node.lChild); // 线索化左孩子 if (node.lChild == null) { // 左孩子为空 node.lTag = true; // 将左孩子设置为线索 node.lChild = pre; } if (pre != null && pre.rChild == null) { // 右孩子为空 pre.rTag = true; pre.rChild = node; } pre = node; inThreading(node.rChild); // 线索化右孩子 } } /** * 中序遍历线索二叉树 */ public void inOrderTraverseWithThread(TBTreeNode node) { while(node != null) { while(!node.lTag) { //找到中序遍历的第一个结点 node = node.lChild; } System.out.print(node.element + " "); while(node.rTag && node.rChild != null) { //若rTag为true,则打印后继结点 node = node.rChild; System.out.print(node.element + " "); } node = node.rChild; } } /** * 中序遍历,线索化后不能使用 */ public void inOrderTraverse(TBTreeNode node) { if(node == null) return; inOrderTraverse(node.lChild); System.out.print(node.element + " "); inOrderTraverse(node.rChild); } public TBTreeNode getRoot() { return root;} public static void main(String[] args) { Character[] data = {'A','B','C','D','E','F','G','H','I','J'}; ThreadedBinaryTree<Character> tbt = new ThreadedBinaryTree<>(data); tbt.inOrderTraverse(tbt.getRoot()); System.out.println(); tbt.inThreading(tbt.getRoot()); tbt.inOrderTraverseWithThread(tbt.getRoot()); } }
线索二叉树充分利用了空指针域的空间,提升了遍历二叉树的效率。
具体内容请参考这篇博客 树、森林与二叉树的转换,这里就不写了。
至此树的知识算是基本总结玩完了,这一节开头讲了树的一些基本概念,重点介绍了树的三种不一样的存储方法:双亲表示法、孩子表示法和孩子兄弟表示法。由兄弟表示法引入了一种特殊的树:二叉树,并详细介绍了它的性质、不一样结构的实现方法和遍历方法。最后介绍了线索二叉树的实现方法(感受这个最难理解)。