1. 程式人生 > >JS資料結構第五篇 --- 二叉樹和二叉查詢樹

JS資料結構第五篇 --- 二叉樹和二叉查詢樹

一、二叉樹的基本概念

從邏輯結構角度來看,前面說的連結串列、棧、佇列都是線性結構;而今天要了解的“二叉樹”屬於樹形結構。

1.1 多叉樹的基本概念,以上圖中“多叉樹”為例說明

  節點:多叉樹中的每一個點都叫節點;其中最上面的那個節點叫“根節點”;

  父節點:節點1是節點2/3/4/5/6的父節點,然後節點2/3/4/5/6是節點1的子節點;節點2/3/4/5/6又是互為兄弟節點,因為它們有父節點為同一個節點;

  空樹:一個沒有任何節點的樹叫空樹;一棵樹可以只有一個節點,也就是隻有根節點;

  子樹:子節點及子節點的後臺節點形成的一個節點集合叫子樹;對於只有兩個子節點的節點,其左邊的子節點叫左子樹,右邊的叫右子樹;

  葉子節點(leaf):子樹為0的節點;其他子樹不為0的節點叫“非葉子節點”;

  層數(level):根節點在第1層,根節點的子節點在第2層,以此類推(有些說法也從第0層開始結算);

  節點的深度(depth):從根節點到當前節點的唯一路徑上的節點總數;

  節點的高度(height):從當前節點到最遠葉子節點的路徑上的節點總數;

  樹的深度:所有節點深度中的最大值;

  樹的高度:所以節點高度中的最大值;數的深度等於數的高度

  有序樹:樹中任意節點的子節點之間有順序關係;

  無序樹:樹中任意節點的子節點之間沒有順序關係,也叫“自由樹”;

  森林:由n(n >= 0) 顆不相交的樹組成的集合;

1.2 二叉樹的特點

  • 每個節點的度最大為2(最多擁有2顆子樹);
  • 左子樹和右子樹是有順序的。即使某節點只有一顆子樹,也要區分左右子樹;
  • 非空二叉樹的第i層,最多有2^(i-1)個節點(i >= 1)
  • 在高度為h的二叉樹上最多有2^h - 1個節點(h >= 1);
  • 對於任何一顆非空二叉樹,如果葉子節點個數為n0, 度為2的節點個數為n2, 則有n0 = n2 + 1。(這個好理解,假如這個二叉樹除了第一級節點有2個子節點,後面的節點都是隻有一個子節點,則不管這顆二叉樹如何往下延伸,永遠度為2的節點個數是1個,葉子節點為2個;然後如果在這個二叉樹的中間節點,每加一個節點,相當於度為2的節點加一個,葉子節點也加一個,則度為2的節點和葉子節點的增加是同步同數量的,所以對於二叉樹,葉子節點個數 = 度為2的節點個數 + 1 公式是永遠成立的)
  • 假設度為1的節點個數為n1, 那麼二叉樹的節點總數 n = n0 + n1 + n2。則二叉樹的邊數T = n1 + 2 * n2 = n -1 = n0 + n1 + n2 -1  --> n2 + 1 = n0

1.3 真二叉樹/滿二叉樹/完全二叉樹

  • 真二叉樹:所有節點的度都要麼為2,要麼為0;
  • 滿二叉樹:所有節點的度都要麼為2,要麼為0,且所有的葉子節點都在最後一層;
    • 假設滿二叉樹的高度為h (h>=1),那麼第i層的節點數量:2^(i-1), 葉子節點數量:2(^h-1), 總節點數量n = 2^h -1 = 2^0 + 2^1 + 2^2 + ... + 2^(h-1)
    • 等比數列公式:a1=1, 公比q=2, 則an = a1*q^(n-1)=2^(n-1), 前n項之和Sn = a1+a2+...+an = 2^0 + 2^1 + 2^2 + ... + 2^(n-1) = a1(1-q^n)/(1-q)=2^n-1
    • 在同樣高度的二叉樹中,滿二叉樹的葉子節點數量最多、總節點數量最多;
    • 滿二叉樹一定是真二叉樹,真二叉樹不一定是滿二叉樹;
  • 完全二叉樹:葉子節點只會出現在最後兩層,且最後一層的葉子節點都靠左對齊;
    • 完全二叉樹從根節點到倒數第二層是一顆滿二叉樹;滿二叉一定是完全二叉樹,完全二叉樹不一定是滿二叉樹;
    • 度為1的節點只有左子樹;度為1的節點要麼是1個,要麼是0個;
    • 假設完全二叉樹的高度為h(h>=1), 那麼至少有2^(h-1) 個節點(2^0 + 2^1 + 2^2 + ... + 2^(h-2) + 1), 最多有2^h - 1個節點(2^0 + 2^1 + 2^2 + ...+ 2^(h-1), 滿二叉樹);總節點數量為n, 則有2^(h-1) <= n < 2^h  -->  h-1 <= log(2)(n) < h --> h = floor(log(2)(n)) + 1  (floor是向下取整,ceiling是向上取整)
    • 一顆有n個節點的完全二叉樹(n > 0),從上到下,從左到右對節點從1開始進行編號,對任意第i個節點
      • 如果i = 1, 它是根節點
      • 如果i > 1, 它的父節點編號為floor(i/2)
      • 如果2i <= n, 它的左子節點編號為2i
      • 如果2i > n, 它無左子節點
      • 如果2i + 1 <= n, 它的右子節點編號為2i + 1
      • 如果2i + 1 > n , 它無右子節點  

   

面試題:如果一顆完全二叉樹有768個節點,求葉子節點的個數?

分析:假設葉子節點個數為n0,度為1的節點個數為n1,度為2的節點個數為n2。

  則總節點個數n = n0  +  n1 +  n2,而且n0 = n2 + 1 ;

  則n = 2n0 + n1 -1

  根據完全二叉樹的定義我們知道,n1要麼為0,要麼為1:

  當n1為1時, n = 2n0,  n必然為偶數。葉子節點個數n0 = n / 2,非葉子節點個數 n1 + n2 = n / 2 ;

  當n1為0,n = 2n0 - 1,n必然為奇數。葉子節點個數n0 = (n + 1) / 2, 非葉子節點個數 n1 + n2 = (n - 1) / 2

  因此可以判斷出來當這個完全二叉樹有768個節點時,它的葉子節點個數為:384

 

二、二叉查詢樹

二叉查詢樹是一種特殊的二叉樹,較小的值儲存在左節點中,較大的值儲存在右節點中。這一特性使得查詢的效率很高,對於數值型和非數值型的資料,如單詞和字串,都是如此。

2.1 二叉查詢樹的插入邏輯

  2.1.1 設根節點為當前節點

  2.1.2 如果待插入節點儲存的資料小於當前節點,則設新的當前節點為原節點的左節點;反之,執行第2.1.4步

  2.1.3 如果當前節點的左節點為null, 就將新的節點插入這個位置,退出迴圈;反之,繼續執行下一次迴圈

  2.1.4 設新的當前節點為原節點的右節點

  2.1.5 如果當前節點的右節點為null, 就將新的節點插入這個位置,退出迴圈;反之,繼續執行下一次迴圈

//插入元素
    function insertBST(element){
        var node = new Node(element, null, null);

        //根節點判斷
        if (root == null){
            root = node;
        }
        else{ //非根節點
            var current = root;
            while(true){
                if (element < current.element){ //往左節點方向放
                    if (current.left == null){
                        current.left = node;
                        break;
                    }
                    current = current.left;
                }
                else if (element > current.element){ //往右節點方向放
                    if (current.right == null){
                        current.right = node;
                        break;
                    }
                    current = current.right;
                }
                else { //相等,替換
                    current.element = element;
                    return;
                }
            }
        }
        size++;
    }
View Code

2.2 二叉查詢樹的遍歷,遍歷有三種方式:中序、前序、後序

  中序指以升序的方式遍歷所有節點;前序是指先訪問根節點,再以同樣的方式訪問左子樹和右子樹;後序指的是先訪問葉子節點,再從左子樹到右子樹,最後到根節點。

先看個效果圖

遍歷走勢分析圖:

遍歷程式碼:

//二叉樹中序遍歷:以升序方式訪問二叉樹中所有節點
    function inOrder(){
        return inOrderByNode(root);
    }
    function inOrderByNode(node){
        if (node){
            var str = "";
            str += inOrderByNode(node.left);
            str += node.element + ", ";
            str += inOrderByNode(node.right);
            return str;
        }
        return "";
    }


    //前序遍歷:先訪問根節點,再訪問左子樹和右子樹
    function preOrder(){
        return preOrderByNode(root);
    }
    function preOrderByNode(node){
        if (node){
            var str = '';
            str += node.element + ", "; //先訪問根節點
            str += preOrderByNode(node.left); //再訪問左子樹
            str += preOrderByNode(node.right); //再訪問右子樹
            return str;
        }
        return "";
    }


    //後序遍歷:先訪問葉子節點,再左子樹,再右子樹,再到根節點
    function postOrder(){
        return postOrderByNode(root);
    }
    function postOrderByNode(node){
        if (node){
            var str = "";
            str += postOrderByNode(node.left);
            str += postOrderByNode(node.right);
            str += node.element + ", ";
            return str;
        }
        return "";
    }
View Code

 

2.3 查詢二叉查詢樹的最大值、最小值、是否存在某個值

最大值:因為較大的值都是在右子樹上,則最大值一定是在右子樹的最後一個節點上;

最小值:較小的值都是在左子樹上,則最小值一定在左子樹的最後一個節點上;

是否存在某個值,則是遍歷查詢

//查詢最小值:因為較小的值都在左邊,所以最小值一定是左子樹的最後一個節點
    function getMin(){
        var minNode = getMinNode(root);
        if (minNode) {
            return minNode.element;
        }
        return null;
    }
    //查詢最小節點
    function getMinNode(node){
        var current = node;
        while(current){
            if (current.left == null){
                return current;
            }
            current = current.left;
        }
        return null;
    }

    //查詢最大值:因為較大的值都在右邊,所以最大值一定是在右子樹的最後一個節點
    function getMax(){
        var maxNode = getMaxNode(root);
        if (maxNode){
            return maxNode.element;
        }
        return null;
    }
    //查詢最大節點
    function getMaxNode(node){
        var current = node;
        while(current){
            if (current.right == null){
                return current;
            }
            current = current.right;
        }
        return null;
    }

    //查詢指定值,是否存在這個元素
    function isExist(element){
        var current = root;
        while(current){
            if (element < current.element){ //左子樹尋找
                current = current.left;
            }
            else if (element > current.element){ //右子樹尋找
                current = current.right;
            }
            else{ //存在
                return true;
            }
        }
        return false;
    }
View Code

 

2.4 刪除二叉查詢樹中的指定元素

從二叉查詢樹上刪除節點的操作最複雜,其複雜程度取決於刪除哪個節點。如果刪除沒有子節點 的節點,那麼非常簡單。如果節點只有一個子節點,不管是左子節點還是右子節點,就變 得稍微有點複雜了。刪除包含兩個子節點的節點最複雜。

//刪除元素
    function remove(element){
        root = removeNode(root, element);
    }
    function removeNode(node, element){
        if (node == null) {
            return null;
        }

        if (node.element == element){
            size--;
            //node沒有左子樹
            if (node.left == null){
                return node.right;
            }
            else if (node.right == null){ //node沒有右子樹
                return node.left;
            }
            /**
             * node有左子樹和右子樹,這個時候要找出最接近node節點值的節點
             * 1、如果找出比node節點的element稍大的節點,則從node右節點的最小節點
             * 2、如果找出比node節點的element稍小的節點,則從node左節點的最大節點
             */
            //第一種方式,找出比node的element稍微大點的節點
            var minNode = getMinNode(node.right);
            node.element = minNode.element;
            node.right = removeNode(node.right, minNode.element);

            // //第二種方式, 找出比node的element稍微小點的節點
            // var maxNode = getMaxNode(node.left);
            // node.element = maxNode.element;
            // node.left = removeNode(node.left, maxNode.element);

            return node;
        }
        else if(element < node.element){ //往左子樹方向繼續找
            node.left = removeNode(node.left, element);
            return node;
        }
        else{
            //往右子樹方向繼續找
            node.right = removeNode(node.right, element);
            return node;
        }
    }
View Code

 

完整demo見:https://github.com/xiaotanit/Tan_DataStruct