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


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

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


一、基本概念

樹(tree)是n(n>=0)個結點的有窮集。n=0時稱為空樹。在任意一個非空樹中:(1)每個元素稱為結點(node);(2)僅有一個特定的結點被稱為根結點或樹根(root)。(3)當n>1時,其余結點可分為m(m≥0)個互不相交的集合T1,T2,……Tm,其中每一個集合Ti(1<=i<=m)本身也是一棵樹,被稱作根的子樹(subtree)。

注意:

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

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

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

樹的定義:

二、樹的存儲結構

由於樹中每個結點的孩子可以有多個,所以簡單的順序存儲結構無法滿足樹的實現要求。下面介紹三種常用的表示樹的方法:雙親表示法、孩子表示法和孩子兄弟表示法。

1、雙親表示法

由於樹中每個結點都僅有一個雙親結點(根節點沒有),我們可以使用指向雙親結點的指針來表示樹中結點的關系。這種表示法有點類似於前面介紹的靜態鏈表的表示方法。具體做法是以一組連續空間存儲樹的結點,同時在每個結點中,設一個「游標」指向其雙親結點在數組中的位置。代碼如下:

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)的時間找到其雙親結點,但是只能通過遍歷整棵樹才能找到它的孩子結點。一種解決辦法是在結點結構中增加其孩子結點的域,但若結點的孩子結點很多,結點結構將會變的很復雜。

2、孩子表示法

由於樹中每個結點可能有多個孩子,可以考慮用多重鏈表,即每個結點有多個指針域,每個指針指向一個孩子結點,我們把這種方法叫多重鏈表表示法。它有兩種設計方案:

方案一:指針域的個數等於樹的度。其結點結構可以表示為:

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域即可。

3、孩子兄弟表示法

任意一棵樹,它的結點的第一個孩子如果存在則是唯一的,它的右兄弟如果存在也是唯一的。因此,我們可以使用兩個分別指向該結點的第一個孩子和右兄弟的指針來表示一顆樹。其結點結構為:

class Node() {
    E data;
    Node firstChild;
    Node rightSib;
}

其結構如下:

這個方法,可以方便的查找到某個結點的孩子,只需先通過firstChild找到它的第一個孩子,然后通過rightSib找到它的第二個孩子,接着一直下去,直到找到想要的孩子。若要查找某個結點的雙親和左兄弟,使用這個方法則比較麻煩。

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

三、二叉樹

1、基本概念

二叉樹(Binary Tree)是每個節點最多有兩個子樹的樹結構。通常子樹被稱作“左子樹”(left subtree)和“右子樹”(right subtree)。

二叉樹的特點:

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

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

1)、斜樹

所有的結點都只有左子樹的二叉樹叫左斜樹。所有的結點都只有右子樹的二叉樹叫右斜樹。這兩者統稱為斜樹。

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

2)、滿二叉樹

在一棵二叉樹中,如果所有分支結點都存在左子樹和右子樹,並且所有葉子都在同一層上,這樣的二叉樹稱為滿二叉樹。

滿二叉樹具有如下特點:

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

3)、完全二叉樹

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

完全二叉樹的特點:

  • 葉子結點只能出現在最下兩層
  • 最下層葉子在左部並且連續
  • 同樣結點數的二叉樹,完全二叉樹的深度最小

4)、平衡二叉樹

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

2、二叉樹的性質

  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。

3、二叉樹的實現

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

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);
    }
}

4、二叉樹的遍歷

二叉樹的遍歷(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個空指針域,利用這些空指針域存放在某種遍歷次序下該結點的前驅結點和后繼結點的指針,這些指針被稱為線索,加上線索的二叉樹稱為線索二叉樹。

結點結構如下:

其中:

  • 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());
    }
}

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

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

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

六、總結

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM