《圖說演算法-使用C#》要點摘錄 – 「二叉樹」

書名:《圖說演算法-使用C#》

作者:吳燦銘、胡昭民

所讀版本:博碩文化

二叉樹

滿二叉樹(Fully Binary Tree)
  • 節點數 = 2^(高度) – 1,高度>=0

完全二叉樹(Complete Binary Tree)
  • 節點數 < 2^(高度) – 1,但其最底層缺少右邊若干節點

歪斜樹(Skewed Binary Tree)
  • 完全沒有左/右節點

嚴格二叉樹(Strictly Binary Tree)
  • 每個非終端節點均有非空的左右子樹

數組實作二叉樹

  • Left Index = Father Index * 2

  • Right Index = Father Index * 2 + 1

  • 以一維數組建立二叉樹 = 建立一個二叉搜尋樹

    • 可以是空集合,但若不是,則節點上一定要有一個鍵值

    • 左子節點值<根節點;右子節點值>根節點

    • 左右子樹本身也是搜尋樹

    • 樹的每個節點值都不相同

  • static void Main(string[] args)
    {
       int[] data = new int[8] { 6, 3, 5, 9, 7, 8, 4, 2 };
       int[] result = ConstructBinaryTreeByArray(data);
       for (int i = 1; i < result.Length; i++)
      {
           Console.Write(result[i] + " ");
      }            
       Console.WriteLine();    
    }

    public static int[] ConstructBinaryTreeByArray(int[] data)
    {
       int[] tree = new int[data.Length * 2];
       int treeIdx;

       for (int i = 0; i < data.Length; i++)
      {
           for(treeIdx = 1; tree[treeIdx] != 0;)
          {
               if(data[i] > tree[treeIdx])
              {
                   //Bigger than father
                   treeIdx = treeIdx * 2 + 1;
              }
               else
              {
                   //Smaller than father
                   treeIdx = treeIdx * 2;
              }
          }

           tree[treeIdx] = data[i];
      }
       return tree;
    }

鏈表實作二叉樹

  • public class TreeNode
    {
       public int val;
       public TreeNode left;
       public TreeNode right;
       public TreeNode(int val)
      {
           this.val = val;
           left = null;
           right = null;
      }
    }
    public class BinaryTree
    {
       public TreeNode root;        

       public BinaryTree(int root)
      {
           this.root = new TreeNode(root);
      }

       public void Add(int node)
      {
           TreeNode curr = root;
           TreeNode newNode = new TreeNode(node);
           while(true)
          {
               if(newNode.val >= curr.val)
              {
                   if(curr.right == null)
                  {
                       curr.right = newNode;
                       return;
                  }
                   curr = curr.right;
              }
               else
              {
                   if(curr.left == null)
                  {
                       curr.left = newNode;
                       return;
                  }
                   curr = curr.left;                    
              }
          }
      }
    }

二叉樹算法

前序/中序/後序遍歷
  • 前:根 -> 左 -> 右

    • public void ForwardTraverse(TreeNode curr)
      {
         if (curr == null) { return; }
         Console.WriteLine(curr.val);
         ForwardTraverse(curr.left);
         ForwardTraverse(curr.right);
      }
  • 中:左 -> 根 -> 右

    • public void MiddleTraverse(TreeNode curr)
      {
         if (curr == null) { return; }
         MiddleTraverse(curr.left);
         Console.WriteLine(curr.val);
         MiddleTraverse(curr.right);
      }
  • 後:左 -> 右 -> 根

    • public void BackwardTraverse(TreeNode curr)
      {
         if (curr == null) { return; }
         BackwardTraverse(curr.left);
         BackwardTraverse(curr.right);
         Console.WriteLine(curr.val);
      }
二叉搜尋樹
  • 一個二叉樹符合「每一何節點凡資料>左子節點且小於右小節點」,便可稱為二分樹。

    • 二叉搜尋樹和二叉排序樹都是二分樹的一種

    • 二叉搜尋樹和二叉排序樹是一體兩面,沒有分別,可以說是對二分樹的兩種操作方式

  • 可以是空集合,但若不是,則節點上一定要有一個鍵值

  • 左子節點值<根節點;右子節點值>根節點

  • 左右子樹本身也是搜尋樹

  • 樹的每個節點值都不相同

  • bool Finder(TreeNode curr, int target)
    {
       if(curr == null) { return false; }            
       else if(target == curr.val)
      {
           return true;
      }
       else if(target > curr.val)
      {
           return Finder(curr.right, target);
      }
       else
      {
           return Finder(curr.left, target);
      }
    }
二叉樹插入節點
  • if (!tree.Find(10))
    {
       tree.Add(10);
       tree.Traverse(TraversalType.InOrder);
    }
二叉樹刪除節點
  • 如果刪除節點為葉節點,直接將其父樹指向它的引用置空

  • 如果刪除節點只有一棵子樹,則將其父樹指向其子樹

  • 如果刪除節點有兩個子樹,則可選擇令其父樹指向:

    • 其左子樹中最右的葉節點

    • 其右子樹中最左的葉節點

二叉排序樹
  • 第一個輸入的資料作為樹根

  • 之後的資料以遞歸的方式與樹根比較,比根節點小的置於左子樹,比根節點大的置於右子樹

    • 對樹中序遍歷即可得出由小到大的結果

引線二叉樹
  • 把節點中的空鏈結指向樹的其他節點,這些鏈結稱為「引線」

  • 步驟:

    • 中序遍歷二叉樹,將空鏈結改成引線

    • 如果引線指向該節點的左鏈結,將該引線指到中序遍歷順序下的後一個節點

    • 如果引線指向該節點的右鏈結,將該引線指到中序遍歷順序下的後一個節點

    • 指向一個空節點,並將此空節點的右鏈結指向自己,空節點的左子樹是此引線二叉樹

  • 引線二叉樹的結構

    • LBIT:左控制位元

    • LCHILD:左子樹鏈結

    • DATA:節點資料

    • RCHILD:右子樹鏈結

    • RBIT:右控制位元

    • 如果LCHILD為正常指標,LBIT = 1;如果為引線,則=0

    • 如果RCHILD為正常指標,RBIT = 1;如果為引線,則=0

  • 優缺點

    • 優點

      • 中序遍歷不需要用Stack,而一般二叉樹需要

      • 中序遍歷速度較快

      • 避免鏈結閒置

      • 任一節點都容易找出他的中序後繼者(右子樹最小)和中序前行者(左子樹最大)。

    • 缺點

      • 加入/刪除節點比一般二叉樹慢

      • 引線子樹間不能共享

  • public class ThreadedNode
    {
       public int data, lbit, rbit;
       public ThreadedNode lchild, rchild;
       public ThreadedNode(int data)
      {
           this.data = data;
           this.lbit = 0;
           this.rbit = 0;
           this.lchild = null;
           this.rchild = null;
      }
    }

    public class Threaded_Binary_Tree
    {
       //引線二元樹根節點
       public ThreadedNode root;

       public Threaded_Binary_Tree()
      {
           root = null;
      }
       public Threaded_Binary_Tree(int[] data)
      {
           foreach (var i in data)
          {
               AddNode(i);
          }
      }

       public void AddNode(int val)
      {
           ThreadedNode newNode = new ThreadedNode(val);
           ThreadedNode curr;
           ThreadedNode parent;
           ThreadedNode prev = new ThreadedNode(val);
           int pos;
           //設定起始節點
           if(root == null)
          {
               root = newNode;
               root.lchild = root;
               root.rchild = null;
               root.lbit = 0;
               root.rbit = 1;
               return;
          }

           //設定起始節點所指的節點
           curr = root.rchild;
           if(curr == null)
          {
               root.rchild = newNode;
               newNode.lchild = root;
               newNode.rchild = root;
               return;
          }

           parent = root; //父節點是起始節點
           pos = 0; //設定二叉樹的行進方向

           while(curr != null)
          {
               if(curr.data > val)
              {
                   if(pos != 1)
                  {
                       pos = -1;
                       prev = parent;
                  }
                   parent = curr;
                   if(curr.lbit == 1)
                  {
                       curr = curr.lchild;
                  }
                   else
                  {
                       curr = null;
                  }
              }
               else
              {
                   if(pos != 1)
                  {
                       pos = 1;
                       prev = parent;
                  }
                   parent = curr;
                   if(curr.rbit == 1)
                  {
                       curr = curr.rchild;
                  }
                   else
                  {
                       curr = null;
                  }
              }

               if(parent.data > val)
              {
                   parent.lbit = 1;
                   parent.lchild = newNode;
                   newNode.lchild = prev;
                   newNode.rchild = parent;
              }
               else
              {
                   parent.rbit = 1;
                   parent.rchild = newNode;
                   newNode.lchild = parent;
                   newNode.rchild = prev;
              }

               return;
          }
      }
    }
延伸二叉樹
  • 任何一個二叉樹,若具有n個節點,則有n-1個非空鏈結,n+1個空鏈結。

    • 在每一個空鏈結上加一個特定節點,稱為外節點。

    • 其餘節點(不包括根節點)稱為內節點

    • 該樹可被定義為「延伸二叉樹」。

  • 外徑長 = 所有外節點到樹根距離的總和

  • 內徑長 = 所有內節點到樹根距離的總和

  • 如果每個外節點都有加權值(如搜尋機率等),則外徑長必須考慮相關加權值(加權外徑長)

霍夫曼樹
  • 有n個權值(q1, q2 ….. qn),且構成一個有n個節點的二叉樹,每個節點外部節點權值為qi,則加權徑長度最小的稱為「最佳化二叉樹」(霍夫曼樹)

平衡樹(AVL樹)
  • 二叉搜尋樹在加入資料後,有可能產生歪斜樹,使樹的高度增加,導致搜尋效率下降。

  • 因此,需要通過保持二叉樹高度平衡,從而保證搜尋效率。

  • 插入/刪除資料後,對二叉樹進行高度調整。

  • 高度平衡樹條件:

    • 左右子樹也是高度平衡樹

    • 內部節點的左右子樹高度相差小於等於1