《劍指offer》之在完全二叉樹中新增子節點

Moonshadow2333發表於2022-04-12

問題描述^1

完全二叉樹是每一層(除最後一層外)都是完全填充(即,節點數達到最大)的,並且所有的節點都儘可能地集中在左側。

例如:圖1 中的 4 棵二叉樹均為完全二叉樹。實現資料結構 CBTInserter 有如下三種方法:

  • CBTInserter(Node root) 使用頭節點為 root 的給定樹初始化該資料結構;

  • CBTInserter->insert(val) 向樹中插入一個新節點,節點型別為 TreeNode,值為 val 。使樹保持完全二叉樹的狀態,並返回插入的新節點的父節點的值;

  • CBTInserter.get_root() 將返回樹的頭節點。

Laravel

圖1

說明:在 (a) 中的完全二叉樹中新增節點 7 得到 (b);在 (b) 中的完全二叉樹中新增節點 8 得到 (c);在 (c) 中的完全二叉樹中新增節點 9 得到 (d)。

什麼是完全二叉樹?

在讀完題之後,寫程式碼之前需要先搞明白什麼是完全二叉樹?

完全二叉樹是由滿二叉樹而引出來的,若設二叉樹的深度為h,除第 h 層外,其它各層 (1~h-1) 的結點數都達到最大個數(即1~h-1層為一個滿二叉樹),第 h 層所有的結點都連續集中在最左邊,這就是完全二叉樹^2

總結:

對於一個深度為 h 的二叉樹必須滿足以下條件才是完全二叉樹:

1. 1~h-1 層是滿二叉樹(國內的滿二叉樹^3)。

2. h 層的所有節點都連續集中在最左邊。

文字描述有點抽象,看幾個例子就明白了。

Laravel

圖2

圖中的(1)(2)(3)都是完全二叉樹,而(4)(5)(6)都不是完全二叉樹。

(1)(2)(3)是完全二叉樹,那肯定符合完全二叉樹的定義。那(4)(5)(6)為什麼不是呢?我們一個一個來分析:

  • (4)是一個深度為 4 的二叉樹,1 ~ 2 層是滿二叉樹,但第三層不是。不滿足 1~4-1 層是一個滿二叉樹的條件。

  • (5)和(6)都是深度為 3 的二叉樹,12 層是一個滿二叉樹,但是 第 3 層的所有節點並非連續集中在最左邊。(5)的第三層最左邊都沒有節點,而(6)的第三層的所有節點不連續。

問題分析

解決這個問題的關鍵在於理解完全二叉樹的特點及在二叉樹新增節點的順序。

完全二叉樹特點:

1. 1~h-1 層是滿二叉樹,h 層最多有 2h-1個節點。

2. h 層的所有節點都連續集中在最左邊。

新增順序:

1. 如果最底層節點的個數小於2h-1個,則從左到右找到該層第一個空缺位置並新增新的節點。

例如,向圖(1)中的 (c) 新增節點:

Laravel

圖3

2. 如果最底層節點的個數為2h-1,此時再向二叉樹新增新的節點會在二叉樹中新增新的一層,而且新的節點是新的一層最左邊的節點,也就是說,新節點的父節點是原來最下面一層的最左邊的節點。

例如,向圖(1)中的 (b) 新增節點:

Laravel

圖4

在完全二叉樹中新增新節點的順序看起來是從上至下,按層從左至右新增的,這就是典型的二叉樹廣度優先搜尋的順序。

解題思路

1. 在完全二叉樹中按照廣度優先搜尋的順序找出第一個左子節點或右子節點還有空缺的節點。

2. 如果該節點沒有左子節點,那麼新的節點就作為該節點的左子節點;如果該節點沒有右子節點,那麼新的節點就作為該節點的右子節點。

完整程式碼:


<?php

class GBTInserter{

    private $queue = [];

    private $root;

    public function __construct($root){

        // 利用廣度優先搜尋找到第一個缺少子節點的節點

        $this->root = $root;

        array_push($this->queue,$root);

        while($this->queuePeek($this->queue)->left != null && $this->queuePeek($this->queue)->right != null){

            $node = array_shift($this->queue);

            array_push($this->queue,$node->left);

            array_push($this->queue,$node->right);

        }

    }

    public function queuePeek($queue){

        // 輔助函式,用於返回佇列中的第一個元素。

        return $queue[0];

    }

    public function insert($val){

        // 新增子節點的邏輯

        $parent = $this->queuePeek($this->queue);

        $node = new Node($val);

        if($parent->left == null){

            $parent->left = $node;

        }else{

            $parent->right = $node;

            array_shift($this->queue);

            array_push($this->queue,$parent->left);

            array_push($this->queue,$parent->right);

        }

        return $parent->data;

    }

    public function getRoot(){

        return $this->root;

    }

}

// 建立二叉樹的類

class Node{

    public $left = NULL;

    public $right = NULL;

    public $data = '';

    public function __construct($data){

        $this->data = $data;

    }

    public function buildTree(Node $lchild = NULL,Node $rchild = NULL){

        if(!is_null($lchild)){

            $this->left = $lchild;

        }

        if(!is_null($rchild)){

            $this->right = $rchild;

        }

    }

}

// 建立完全二叉樹

$a = new Node(1);

$b = new Node(2);

$c = new Node(3);

$d = new Node(4);

$e = new Node(5);

$f = new Node(6);

$a->buildTree($b,$c);

$b->buildTree($d,$e);

$c->buildTree($f);

$GBTInserter = new GBTInserter($a);

// 新增節點

$GBTInserter->insert(7);

$GBTInserter->insert(8);

$GBTInserter->insert(9);

$re = $GBTInserter->insert(10);

模擬過程

以向圖1中的 (b) 新增節點為例。

1. GBTInserter 類的構造方法中的廣度優先搜尋演算法是如何找到第一個缺少子節點的節點的?

Laravel

圖5

①. 圖5 步驟(0),將完全二叉樹的根節點新增到佇列中。

②. 根節點 $a 的左右子節點都不為空,於是 $a 出隊,將 $a 的左右子節點依次入隊,如圖5 步驟(1)所示。相關程式碼如下:


public function __construct($root){

   // 將完全二叉樹的根節點新增到佇列中

   array_push($this->queue,$root);

   // 隊首的節點的左右子節點都不為空時,繼續執行。

   while($this->queuePeek($this->queue)->left != null && $this->queuePeek($this->queue)->right != null){

      $node = array_shift($this->queue);

      array_push($this->queue,$node->left);

      array_push($this->queue,$node->right);

   }

}

③. 接下來就是迴圈處理 $b 、$c 節點。由於 $b 、$c 節點的左右子節點都不為空,所以過程和處理 $a 節點類似,如圖5 步驟(2)、步驟(3)所示。

④. 經過步驟(3)之後,佇列中儲存的全是第 3 層的節點。而隊首節點 $d 沒有子節點,迴圈終止。即第一個缺少子節點的節點就儲存在佇列的隊首。

1. GBTInserter 類的 insert() 方法是如何新增子節點的:

在例項化 GBTInserter 類之後,佇列中儲存的都是缺少子節點的節點:


$queue = [$d,$e,$f,$g]

而隊首的節點就是第一個缺少子節點的節點。而我們正是要向該節點新增子節點。

①. 利用 queuePeek 方法獲取隊首節點。

②. 例項化需要新增的節點。


public function insert($val){

   ...

   $parent = $this->queuePeek($this->queue);

   $node = new Node($val);

   ...

}

③. 先問隊首節點是否有左子節點?如果沒有則新增至左子節點,否則新增到右子節點,並且並且佇列出一入二

  • 出一是指將隊首的節點出隊,該節點在新增右子節點之後已經不再缺少子節點了,而佇列記錄的是缺少子節點的節點,自然需要將其出隊。

  • 入二是指將隊首節點的左右子節點入隊。

④. 返回隊首節點的值。


public function insert($val){

   ...

   if($parent->left == null){

    $parent->left = $node;

   }else{

    $parent->right = $node;

    array_shift($this->queue);

    array_push($this->queue,$parent->left);

    array_push($this->queue,$parent->right);

   }

   return $parent->data;

}

複雜度分析

1. 時間複雜度:

  • GBTInserter 的建構函式從本質上來說是按照廣度優先搜尋的順序找出二叉樹中所有既有左子節點又有又子節點的節點,因此時間複雜度為 O(n)。

  • 呼叫函式 insert 在完全二叉樹中新增一個節點最多隻需要在佇列中刪除一個節點並新增兩個節點。通常,佇列的插入、刪除的時間複雜度都是 O(1)。

2. GBTInserter 類需要一個佇列來實現廣度優先搜尋演算法儲存缺少左子節點或右子節點的節點,空間複雜度為 O(n)。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章