數據結構與算法(四),樹

轉載請註明出處:http://www.cnblogs.com/wangyingli/p/5933257.htmlhtml

前面講到的順序表、棧和隊列都是一對一的線性結構,這節講一對多的線性結構——樹。「一對多」就是指一個元素只能有一個前驅,但能夠有多個後繼。java


1、基本概念

樹(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

注意:算法

  • n>0時,根節點是惟一的。
  • m>0時,子樹的個數沒有限制,但它們必定是互不相交的。

結點擁有的子樹數被稱爲結點的(Degree)。度爲0的結點稱爲葉節點(Leaf)或終端結點,度不爲0的結點稱爲分支結點。除根結點外,分支結點也被稱爲內部結點。結點的子樹的根稱爲該結點的孩子(Child),該結點稱爲孩子的雙親父結點。同一個雙親的孩子之間互稱爲兄弟樹的度是樹中各個結點度的最大值。數組

結點的層次(Level)從根開始定義起,根爲第一層,根的孩子爲第二層。雙親在同一層的結點互爲堂兄弟。樹中結點的最大層次稱爲樹的深度(Depth)或高度。若是將樹中結點的各個子樹當作從左到右是有次序的,不能互換的,則稱該樹爲有序樹,不然稱爲無序樹森林是m(m>=0)棵互不相交的樹的集合。post

樹的定義:this

2、樹的存儲結構

因爲樹中每一個結點的孩子能夠有多個,因此簡單的順序存儲結構沒法知足樹的實現要求。下面介紹三種經常使用的表示樹的方法:雙親表示法、孩子表示法和孩子兄弟表示法。設計

一、雙親表示法

因爲樹中每一個結點都僅有一個雙親結點(根節點沒有),咱們可使用指向雙親結點的指針來表示樹中結點的關係。這種表示法有點相似於前面介紹的靜態鏈表的表示方法。具體作法是以一組連續空間存儲樹的結點,同時在每一個結點中,設一個「遊標」指向其雙親結點在數組中的位置。代碼以下: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找到它的第二個孩子,接着一直下去,直到找到想要的孩子。若要查找某個結點的雙親和左兄弟,使用這個方法則比較麻煩。

這個方法最大的好處是將一顆複雜的樹變成了一顆二叉樹。這樣就可使用二叉樹的一些特性和算法了。

3、二叉樹

一、基本概念

二叉樹(Binary Tree)是每一個節點最多有兩個子樹的樹結構。一般子樹被稱做「左子樹」(left subtree)和「右子樹」(right subtree)。

二叉樹的特色:

  • 二叉樹不存在度大於2的結點。
  • 二叉樹的子樹有左右之分,次序不能顛倒。

以下圖中,樹1和樹2是同一棵樹,但它們是不一樣的二叉樹。

1)、斜樹

全部的結點都只有左子樹的二叉樹叫左斜樹。全部的結點都只有右子樹的二叉樹叫右斜樹。這二者統稱爲斜樹。

斜樹每一層只有一個結點,結點的個數與二叉樹的深度相同。其實斜樹就是線性表結構。

2)、滿二叉樹

在一棵二叉樹中,若是全部分支結點都存在左子樹和右子樹,而且全部葉子都在同一層上,這樣的二叉樹稱爲滿二叉樹。

滿二叉樹具備以下特色:

  • 葉子只能出如今最下一層
  • 非葉子結點的度必定是2
  • 一樣深度的二叉樹中,滿二叉樹的結點個數最多,葉子數最多。

3)、徹底二叉樹

若設二叉樹的高度爲h,除第 h 層外,其它各層 (1~h-1) 的結點數都達到最大個數,第h層有葉子結點,而且葉子結點都是從左到右依次排布,這就是徹底二叉樹。

徹底二叉樹的特色:

  • 葉子結點只能出如今最下兩層
  • 最下層葉子在左部而且連續
  • 一樣結點數的二叉樹,徹底二叉樹的深度最小

4)、平衡二叉樹

平衡二叉樹又被稱爲AVL樹(區別於AVL算法),它是一棵二叉排序樹,且具備如下性質:它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,而且左右兩個子樹都是一棵平衡二叉樹

二、二叉樹的性質

  1. 在二叉樹的第i層上至多有2^i-1^個結點(i>=1)。

  2. 深度爲k的二叉樹至多有2^k^-1個結點(k>=1)。

  3. 對任何一棵二叉樹T,若是其終端結點個數爲n~0~,度爲2的結點數爲n~2~,則n~0~ = n~2~ + 1。

  4. 具備n個結點的徹底二叉樹的深度爲「log~2~n」+ 1(「x」表示不大於x的最大整數)。

  5. 若是對一棵有n個結點的徹底二叉樹的結點按層序編號(從第一層到第「log~2~n」+ 1層,每層從左到右),對任一結點i(1≤i≤n)有:
    • 若i=1,則結點i是二叉樹的根,無雙親;如i>1,則其雙親是結點「i/2」。
    • 如2i>n,則結點i無左孩子(結點i爲葉子結點);不然其左孩子是結點2i。
    • 若2i+1>n,則結點i無右孩子;不然其右孩子是結點2i+1。

三、二叉樹的實現

二叉樹是一種特殊的樹,它的存儲結構相對於前面談到的通常樹的存儲結構要簡單一些。

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的二叉樹有:

4、線索二叉樹

對於n個結點的二叉樹,在二叉鏈存儲結構中有n+1個空指針域,利用這些空指針域存放在某種遍歷次序下該結點的前驅結點和後繼結點的指針,這些指針被稱爲線索,加上線索的二叉樹稱爲線索二叉樹。

結點結構以下:

其中:

  • lTag爲0時,lChild指向該結點的左孩子,爲1時指向該結點的前驅
  • rTag爲0時,rChild指向該結點的右孩子,爲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());
    }
}

線索二叉樹充分利用了空指針域的空間,提升了遍歷二叉樹的效率。

5、樹、森林與二叉樹的轉換

具體內容請參考這篇博客 樹、森林與二叉樹的轉換,這裏就不寫了。

6、總結

至此樹的知識算是基本總結玩完了,這一節開頭講了樹的一些基本概念,重點介紹了樹的三種不一樣的存儲方法:雙親表示法、孩子表示法和孩子兄弟表示法。由兄弟表示法引入了一種特殊的樹:二叉樹,並詳細介紹了它的性質、不一樣結構的實現方法和遍歷方法。最後介紹了線索二叉樹的實現方法(感受這個最難理解)。

相關文章
相關標籤/搜索