1. 程式人生 > >線索二叉樹原理及前序、中序線索化(Java版)

線索二叉樹原理及前序、中序線索化(Java版)

轉載

原文地址:https://blog.csdn.net/UncleMing5371/article/details/54176252

一、線索二叉樹原理

      前面介紹二叉樹原理及特殊二叉樹文章中提到,二叉樹可以使用兩種儲存結構:順序儲存二叉連結串列。在使用二叉連結串列的儲存結構的過程中,會存在大量的空指標域,為了充分利用這些空指標域,引申出了“線索二叉樹”。回顧一下二叉連結串列儲存結構,如下圖: 


這裡寫圖片描述

      通過觀察上面的二叉連結串列,存在著若干個沒有指向的空指標域。對於一個有n個節點的二叉連結串列,每個節點有指向左右節點的2個指標域,整個二叉連結串列存在2n個指標域。而n個節點的二叉連結串列有n-1條分支線,那麼空指標域的個數=2n-(n-1) = n+1個空指標域,從儲存空間的角度來看,這n+1個空指標域浪費了記憶體資源。 

      從另外一個角度來分析,如果我們想知道按中序方式遍歷二叉連結串列時B節點的前驅節點或者後繼節點時,必須要按中序方式遍歷二叉連結串列才能夠知道結果,每次需要結果時都需要進行一次遍歷,是否可以考慮提前儲存這種前驅和後繼的關係來提高時間效率呢? 
      綜合以上兩方面的分析,可以通過充分利用二叉連結串列中的空指標域,存放節點在某種遍歷方式下的前驅和後繼節點的指標。我們把這種指向前驅和後繼的指標成為線索,加上線索的二叉連結串列成為線索連結串列,對應的二叉樹就成為“線索二叉樹(Threaded Binary Tree) 。

二、構建線索二叉樹過程

1、我們對二叉樹進行中序遍歷(不瞭解二叉樹遍歷請參考

二叉樹及特殊二叉樹介紹),將所有的節點右子節點為空的指標域指向它的後繼節點。如下圖: 


中序線索二叉樹

      通過中序遍歷我們知道H的right指標為空,並且H的後繼節點為D(如上圖第1步),I的right指標為空,並且I的後繼節點為B(如上圖第2步),以此類推,知道G的後繼節點為null,則G的right指標指向null。

2、接下來將這顆二叉樹的所有節點左指標域為空的指標域指向它的前驅節點。如下圖: 


這裡寫圖片描述

      如上圖,H的left指標域指向Null(如第1步),I的前驅節點是D,則I的left指標指向D,以此類推。

      通過上面兩步完成了整個二叉樹的線索化,最後結果如下圖: 


這裡寫圖片描述

      通過觀察上圖(藍色虛線代表後繼、綠色虛線代表前驅),可以看出,線索二叉樹,等於是把一棵二叉樹轉變成了一個“
特殊的雙向連結串列“(後面會解釋為什麼叫特殊的雙向連結串列),這樣對於我們的新增、刪除、查詢節點帶來了方便。所以我們對二叉樹以某種次序遍歷使其變為線索二叉樹的過程稱做是線索化。如下圖: 

這裡寫圖片描述 

      仔細分析上面的雙向連結串列,與線索化之後的二叉樹相比,比如節點D與後繼節點I,在完成線索化之後,並沒有直接線索指標,而是存在父子節點的指標;節點A與節點F,線上索化完成之後,節點A並沒有直接指向後繼節點F的線索指標,而是通過父子節點遍歷可以找到最終的節點F,前驅節點也存在同樣的問題,正因為很多節點之間不存在直接的線索,所以我將此雙向連結串列稱做“特殊的雙向連結串列”,再使用過程中根據指標是線索指標還是子節點指標來分別處理,所以在每個節點需要標明當前的左右指標是線索指標還是子節點指標,這就需要修改節點的資料結構。修改後的資料結構如下:
   class Node {
        String data;    //資料域
        Node left;      //左指標域
        Node right;     //右指標域
        byte leftType;  //左指標域型別 0:指向子節點、1:前驅或後繼線索
        byte rightType; //右指標域型別 0:指向子節點、1:前驅或後繼線索
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

最終的二叉連結串列修改為如下圖的樣子: 


這裡寫圖片描述

三、線索二叉樹的程式碼(Java版)

下面是中序線索化二叉樹的實現程式碼:

/**
 * @Title: 二叉樹相關操作
 * @Description:
 * @Author: Uncle Ming
 * @Date:2017年1月6日 下午2:49:14
 * @Version V1.0
 */
public class ThreadBinaryTree {

    private Node preNode;   //線索化時記錄前一個節點

    //節點儲存結構
    static class Node {
        String data;        //資料域
        Node left;          //左指標域
        Node right;         //右指標域
        boolean isLeftThread = false;   //左指標域型別  false:指向子節點、true:前驅或後繼線索
        boolean isRightThread = false;  //右指標域型別  false:指向子節點、true:前驅或後繼線索

        Node(String data) {
            this.data = data;
        }
    }

    /**
     * 通過陣列構造一個二叉樹(完全二叉樹)
     * @param array
     * @param index
     * @return
     */
    static Node createBinaryTree(String[] array, int index) {
        Node node = null;

        if(index < array.length) {
            node = new Node(array[index]);
            node.left = createBinaryTree(array, index * 2 + 1);
            node.right = createBinaryTree(array, index * 2 + 2);
        }

        return node;
    }

    /**
     * 中序線索化二叉樹
     * @param node  節點
     */
    void inThreadOrder(Node node) {
        if(node == null) {
            return;
        }

        //處理左子樹
        inThreadOrder(node.left);

        //左指標為空,將左指標指向前驅節點
        if(node.left == null) {
            node.left = preNode;
            node.isLeftThread = true;
        }

        //前一個節點的後繼節點指向當前節點
        if(preNode != null && preNode.right == null) {
            preNode.right = node;
            preNode.isRightThread = true;
        }
        preNode = node;

        //處理右子樹
        inThreadOrder(node.right);
    }

    /**
     * 中序遍歷線索二叉樹,按照後繼方式遍歷(思路:找到最左子節點開始)
     * @param node
     */
    void inThreadList(Node node) {
        //1、找中序遍歷方式開始的節點
        while(node != null && !node.isLeftThread) {
            node = node.left;
        }

        while(node != null) {
            System.out.print(node.data + ", ");

            //如果右指標是線索
            if(node.isRightThread) {
                node = node.right;

            } else {    //如果右指標不是線索,找到右子樹開始的節點
                node = node.right;
                while(node != null && !node.isLeftThread) {
                    node = node.left;
                }
            }
        }
    }

    /**
     * 中序遍歷線索二叉樹,按照前驅方式遍歷(思路:找到最右子節點開始倒序遍歷)
     * @param node
     */
    void inPreThreadList(Node node) {
        //1、找最後一個節點
        while(node.right != null && !node.isRightThread) {
            node = node.right;
        }

        while(node != null) {
            System.out.print(node.data + ", ");

            //如果左指標是線索
            if(node.isLeftThread) {
                node = node.left;

            } else {    //如果左指標不是線索,找到左子樹開始的節點
                node = node.left;
                while(node.right != null && !node.isRightThread) {
                    node = node.right;
                }
            }
        }
    }

    /**
     * 前序線索化二叉樹
     * @param node
     */
    void preThreadOrder(Node node) {
        if(node == null) {
            return;
        }

        //左指標為空,將左指標指向前驅節點
        if(node.left == null) {
            node.left = preNode;
            node.isLeftThread = true;
        }

        //前一個節點的後繼節點指向當前節點
        if(preNode != null && preNode.right == null) {
            preNode.right = node;
            preNode.isRightThread = true;
        }

        preNode = node;

        //處理左子樹
        if(!node.isLeftThread) {
            preThreadOrder(node.left);
        }

        //處理右子樹
        if(!node.isRightThread) {
            preThreadOrder(node.right);
        }
    }

    /**
     * 前序遍歷線索二叉樹(按照後繼線索遍歷)
     * @param node
     */
    void preThreadList(Node node) {
        while(node != null) {

            while(!node.isLeftThread) {
                System.out.print(node.data + ", ");
                node = node.left;
            }

            System.out.print(node.data + ", ");
            node = node.right;
        }
    }

    public static void main(String[] args) {
        String[] array = {"A", "B", "C", "D", "E", "F", "G", "H"};
        Node root = createBinaryTree(array, 0);

        ThreadBinaryTree tree = new ThreadBinaryTree();
        tree.inThreadOrder(root);
        System.out.println("中序按後繼節點遍歷線索二叉樹結果:");
        tree.inThreadList(root);
        System.out.println("\n中序按後繼節點遍歷線索二叉樹結果:");
        tree.inPreThreadList(root);

        Node root2 = createBinaryTree(array, 0);
        ThreadBinaryTree tree2 = new ThreadBinaryTree();
        tree2.preThreadOrder(root2);
        tree2.preNode = null;
        System.out.println("\n前序按後繼節點遍歷線索二叉樹結果:");
        tree.preThreadList(root2);
    }
}

四、小結

  1. 線索化的實質就是將二叉連結串列中的空指標改為指向前驅節點或後繼節點的線索;
  2. 線索化的過程就是修改二叉連結串列中空指標的過程,可以按照前序、中序、後序的方式進行遍歷,分別生成不同的線索二叉樹;
  3. 有了線索二叉樹之後,我們再次遍歷時,就相當於操作一個雙向連結串列。
  4. 使用場景:如果我們在使用二叉樹過程中經常需要遍歷二叉樹或者查詢節點的前驅節點和後繼節點,可以考慮採用線索二叉樹儲存結構。