JS數據結構第五篇 — 二叉樹和二叉查找樹

  • 2019 年 10 月 3 日
  • 筆記

一、二叉樹的基本概念

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

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

四則運算的表達式可以分為三種:

  •  前綴表達式(prefix  expression),又稱波蘭表達式
  • 中綴表達式(infix  expression)
  • 後綴表達式(postfix  expression),又稱為逆波蘭式表達式

如果將表達式的操作作為葉子節點,運算符作為父節點(假設只有四則運算),這些節點剛好可以組成一顆二叉樹。

比如表達式:A / B + C * D – E  ,如果對這顆二叉樹進行遍歷

  • 前序遍歷:- + / A B * C D E ,剛好就是前綴表達式(波蘭表達式)
  • 中序遍歷:A / B + C * D – E,剛好就是中綴表達式
  • 後序遍歷:A B / C D * + E -,剛好就是後綴表達式(逆波蘭表達式)

 

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