1. 程式人生 > >C#算法系列(2)——線索二叉樹

C#算法系列(2)——線索二叉樹

       首先在這裡宣告一下,本篇部落格參考另外一位大神的部落格,部落格連結如下:http://blog.csdn.net/UncleMing5371/article/details/54176252。由於寫的很好理解,所以就拿來借鑑一下,主要目的也是出於學習,讓自己的演算法能力有所提高。如果有版權問題,歡迎私信我,我收到後立馬刪除。好了,看下今天的主題——線索二叉樹。二叉樹的其它知識可以參考以上我上篇文章,C#算法系列(1)——二叉樹,在這裡就不再贅述。我們先來看下線索二叉樹的構建過程。

一、線索二叉樹的由來

       線索二叉樹的本質:主要是針對在構建二叉樹時存在空指標域的問題進行優化。具體來講:對於一個有n個結點二叉連結串列,每個結點有指向左右孩子的兩個指標域,所以一共是2n個指標域。而n個結點的二叉樹一共有n-1條分支數,也就是說,其實存在2n-(n-1) = n+1個空指標域。優化的目的是將空指標域給利用起來,將空的指標域指向其前驅或後繼。可是這樣一來就會存在一個問題,如何區分指標域指向的是孩子結點還是前驅或後繼,因此,還需要額外增加一個bool型別的標識位,進行區分。當標識位為0時,則表示的是其孩子結點,若為1時,則指向的是其前驅或後繼結點。

二、線索二叉樹的構建過程

       (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,以此類推。
       (3) 通過上面兩步完成了整個二叉樹的線索化,最後結果如下圖:
這裡寫圖片描述

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

       仔細分析上面的雙向連結串列,與線索化之後的二叉樹相比,比如節點D與後繼節點I,在完成線索化之後,並沒有直接線索指標,而是存在父子節點的指標;節點A與節點F,線上索化完成之後,節點A並沒有直接指向後繼節點F的線索指標,而是通過父子節點遍歷可以找到最終的節點F,前驅節點也存在同樣的問題,正因為很多節點之間不存在直接的線索,所以我將此雙向連結串列稱做“特殊的雙向連結串列
”,再使用過程中根據指標是線索指標還是子節點指標來分別處理,所以在每個節點需要標明當前的左右指標是線索指標還是子節點指標,這就需要修改節點的資料結構。修改後的資料結構如下:
class TreeNode<T>
    {
        private T data; //資料域
        public TreeNode<T> lChild; //左孩子
        public TreeNode<T> rChild; //右孩子
        private bool ltag; //true表線索, false結點
        private bool rtag;

        public Boolean Ltag
        {
            get { return ltag; }
            set { ltag = value; }
        }

        public Boolean Rtag
        {
            get { return rtag; }
            set { rtag = value; }
        }

        public T Data
        {
            get { return data; }
            set { data = value; }
        }

        public TreeNode(T val)
        {
            this.data = val;
            this.lChild = null;
            this.rChild = null;
        }
    }

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

這裡寫圖片描述

二、線索二叉樹的程式碼(C#版)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace 二叉樹_線索化
{
    class BinaryTree<T>
    {
        private TreeNode<T> pre; //指向剛剛訪問過的結點

        //通過陣列構建二叉樹
        public TreeNode<T> CreatBinaryTree(T[] vals, int index)
        {
            TreeNode<T> root = null;

            if (index < vals.Length)
            {
                root = new TreeNode<T>(vals[index]);
                root.lChild = CreatBinaryTree(vals, index * 2 + 1);
                root.rChild = CreatBinaryTree(vals, index * 2 + 2);
            }
            return root;
        }


        //中序遍歷線索化 
        // 如果當前結點無左孩子,則剛剛訪問的pre結點為當前結點的前驅
        // 如果剛剛訪問的pre結點無右孩子,則當前結點為pre結點的後繼
        public void InTreading(TreeNode<T> root)
        {
            if (root == null)
            {
                return;
            }
            //處理左子樹
            InTreading(root.lChild);

            //左指標為空,將左指標指向前驅節點
            if (root.lChild == null)
            {
                root.lChild = pre;
                root.Ltag = true;
            }
            //前一個節點的後繼節點指向當前節點
            if (pre != null && pre.rChild == null)
            {
                pre.rChild = root;
                pre.Rtag = true;
            }
            pre = root;
            //處理右子樹
            InTreading(root.rChild);
        }

        //中序遍歷線索二叉樹,按照後繼方式進行遍歷
        public void InOrderTraversal(TreeNode<T> root)
        {
            //1、找中序遍歷方式最開始的節點
            while (root != null && !root.Ltag)
            {
                root = root.lChild;
            }

            while (root != null)
            {
                Console.Write(root.Data + " ");
                //如果右指標是線索
                if (root.Rtag)
                {
                    root = root.rChild;
                }
                //如果右指標不是線索,找到右子樹開始的節點(即右子樹最左邊的結點)
                else
                {
                    root = root.rChild;
                    while (root != null && !root.Ltag)
                    {
                        root = root.lChild;
                    }
                }
            }
        }

        //中序遍歷線索二叉樹,按照前驅方式遍歷(思路:找到最右子節點開始倒序遍歷)
        public void InPreOrderTraversal(TreeNode<T> root)
        {
            //找最後一個結點
            while (root.rChild != null && !root.Rtag)
            {
                root = root.rChild;
            }

            while (root != null)
            {
                Console.Write(root.Data + " ");
                //如果左指標是線索
                if (root.Ltag)
                {
                    root = root.lChild;
                }
                //如果左指標不是線索,找到左子樹開始的節點
                //(因為是倒序,所以最開始的結點為左子樹最右邊的結點)
                else
                {
                    root = root.lChild;
                    while (root.rChild != null && !root.Rtag)
                    {
                        root = root.rChild;
                    }
                }
            }
        }

        //前序線索化二叉樹
        public void PreThreading(TreeNode<T> root)
        {
            if (root == null)
            {
                return;
            }
            //左指標為空,將左指標指向前驅節點
            if (root.lChild == null)
            {
                root.lChild = pre;
                root.Ltag = true;
            }
            //前一個節點的後繼節點指向當前節點
            if (pre != null && pre.rChild == null)
            {
                pre.rChild = root;
                pre.Rtag = true;
            }
            pre = root;
            //如果左指標不是索引,處理左子樹
            if (!root.Ltag)
            {
                PreThreading(root.lChild);
            }
            //如果右指標不是索引,處理右子樹
            if (!root.Rtag)
            {
                PreThreading(root.rChild);
            }
        }

        //前序遍歷線索二叉樹(按照後繼線索遍歷)
        public void PreInTraversal(TreeNode<T> root)
        {
            while (root != null)
            {
                while (!root.Ltag)
                {
                    Console.Write(root.Data + " ");
                    root = root.lChild;
                }
                Console.Write(root.Data + " ");
                root = root.rChild;
            }
        }
    }
}

以及主程式測試類Program.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace 二叉樹_線索化
{
    class Program
    {
        static void Main(string[] args)
        {
            char[] data = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J' };
            BinaryTree<char> tree = new BinaryTree<char>();
            TreeNode<char> root = tree.CreatBinaryTree(data,0);
            Console.WriteLine("二叉樹中序線索化...");
            tree.InTreading(root);
            Console.Write("中序按後繼節點遍歷線索二叉樹結果:");
            tree.InOrderTraversal(root);
            Console.WriteLine();
            Console.Write("中序按前驅節點遍歷線索二叉樹結果:");
            tree.InPreOrderTraversal(root);
            Console.WriteLine();
            TreeNode<char> root2 = tree.CreatBinaryTree(data,0);
            Console.WriteLine("二叉樹先序線索化...");
            tree.PreThreading(root2);
            Console.Write("先序按後繼節點遍歷線索二叉樹結果:");
            tree.PreInTraversal(root2);
            Console.WriteLine();
            Console.ReadKey();
        }
    }
}

實驗結果截圖如下:

這裡寫圖片描述

       以上若有不對或有疑問的地方歡迎私信我,謝謝!!!

參考資料: