劍指Offer對答如流系列 - 重建二叉樹

来源:https://www.cnblogs.com/JefferyChenXiao/archive/2020/01/31/12246258.html
-Advertisement-
Play Games

面試題6:重建二叉樹 題目描述: 輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建出該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重覆的數字。例如輸入前序遍歷序列{1,2,4,7,3,5,6,8}和中序遍歷序列{4,7,2,1,5,3,8,6},則重建出圖2.6所示的二叉樹並輸出它的頭結點。二叉 ...


面試題6:重建二叉樹

題目描述:

輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建出該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重覆的數字。例如輸入前序遍歷序列{1,2,4,7,3,5,6,8}和中序遍歷序列{4,7,2,1,5,3,8,6},則重建出圖2.6所示的二叉樹並輸出它的頭結點。二叉樹結點的定義如下:

class Node{
    int e;
    Node left;
    Node right;
    Node(int x) { e = x; }
}

在這裡插入圖片描述

這道題主要測試你對二叉樹性質的瞭解,非常有代表性,不過在這之前,我們先把二叉樹的基本性質捋一遍。

二叉樹的基本性質

(1)關於樹

樹是一種數據結構,它是由n(n>=1)個有限結點組成一個具有層次關係的集合。

在這裡插入圖片描述

樹的基本術語有:

  • 每個結點有零個或多個子結點
  • 沒有父節點的結點稱為根節點
  • 每一個非根結點有且只有一個父節點
  • 除了根結點外,每個子結點可以分為多個不相交的子樹。
  • 若一個結點有子樹,那麼該結點稱為子樹根的“雙親”,子樹的根稱為該結點的“孩子”。有相同雙親的結點互為“兄弟”
  • 一個結點的所有子樹上的任何結點都是該結點的後裔。從根結點到某個結點的路徑上的所有結點都是該結點的祖先。
  • 結點的度:結點擁有的子樹的數目
  • 葉子結點:度為0的結點
  • 分支結點:度不為0的結點
  • 樹的度:樹中結點的最大的度
  • 層次:根結點的層次為1,其餘結點的層次等於該結點的雙親結點的層次加1
  • 樹的高度:樹中結點的最大層次
  • 森林:0個或多個不相交的樹組成。對森林加上一個根,森林即成為樹;刪去根,樹即成為森林。

“樹”是電腦科學中非常重要的一部分內容,它的變形和應用非常之多。比如說,在做通訊錄的時候,Android 工程師往往喜歡用字典樹來存取數據,對於Java後端,有的時候我們處理的數據的時候也需要進行區間的查詢,比如說去年你的博客在什麼時間段關註你的人增長最快啊,一天中自己的博文閱讀量最高的時間段啊,可以採用線段樹來實現。在JDK1.8的HashMap的結構中,當元素超過8個的時候,會轉為紅黑樹等等。

不過對於Java開發而言,二叉樹是基礎也是接觸較多的一種結構。

(2)關於二叉樹

二叉樹是每個結點最多有兩個子樹的樹結構。它有五種基本形態:

1)空樹;

2)只有根的樹,即單結點;

3)有根且有一個左子樹;

4)有根且有一個右子樹;

5)有根且有一個左子樹,有一個右子樹。

二叉樹的性質有:

  • 二叉樹第i層上的結點數目最多為2i-1(i>=1)
  • 深度為k的二叉樹至多有2k-1個結點(k>=1)
  • 包含n個結點的二叉樹的高度至少為(log2n)+1
  • 在任意一棵二叉樹中,若葉子結點的個數為n0,度為2的結點數為n2,則n0=n2+1

    第四條的證明: 因為二叉樹中所有結點的度數均不大於2,設n0表示度為0的結點個數,n1表示度為1的結點個數,n2表示度為2的結點個數。
    三類結點加起來為總結點個數,於是便可得到:n=n0+n1+n2 (公式1)
    由度之間的關係可得第二個等式:n=n0*0+n1*1+n2*2+1 即n=n1+2n2+1 (公式2)
    將(公式1)(公式2)組合在一起可得到n0=n2+1

瞭解這第四條性質就差不多了。如果想進一步學習二叉樹的性質,不妨去找本《離散數學》?

對二叉樹遍歷的理解

以這棵樹為例:

在這裡插入圖片描述

前序遍歷:根結點 —> 左子樹 —> 右子樹(先遍歷根節點,然後左右)

    //前序遍歷以node為根
    public void preOrder(Node node) {
        //終止條件
        if(node == null) {
            return;
        }
        System.out.println(node.e);
        preOrder(node.left);
        preOrder(node.right);
    }

這棵樹的前序遍歷為:FCADBEHGM

中序遍歷:左子樹—> 根結點 —> 右子樹(在中間遍歷根節點)

    //中序以node為根的
    public void inOrder(Node node) {
        //終止條件
        if(node == null) {
            return;
        }
        inOrder(node.left);
        System.out.println(node.e);
        inOrder(node.right);
    }

這棵樹的中序遍歷為:ACBDFHEMG

後序遍歷:左子樹 —> 右子樹 —> 根結點(最後遍歷根節點)

        //中序以node為根
        public void postOrder(Node node) {
            //終止條件
            if(node == null) {
                return;
            }
    
            postOrder(node.left);
            postOrder(node.right);
            System.out.println(node.e);
        }

這棵樹的後序遍歷為:ABDCHMGEF

層序遍歷:

    //層序遍歷
    public void levelOrder() {
        Queue<Node> q = new LinkedList<>();
        q.add(root);
        while( !q.isEmpty() ) {
            Node cur = q.remove();
            System.out.println(cur.e);
            
            if(cur.left != null)
                q.add(cur.left);
            if(cur.right != null)
                q.add(cur.right);  //後出
        }
    }

這棵樹層序遍歷為:FCEADHGBM

所謂的前序、中序、後序,就是對根節點而言的,左右的遍歷順序不變,前序就是根節點最先遍歷,然後左右;中序就是把根節點放在中間遍歷;後序則是把根節點放在最後遍歷。

中序遍歷能夠幫助我們很好地確定根節點的位置,這個就有點可怕了,實際面試的時候,不單單會有給出前序遍歷和中序遍歷的結果讓你重建二叉樹,還有給出後序遍歷和中序遍歷結果或者 層序遍歷和中序遍歷的結果重建二叉樹、

其他的遍歷組合均不能還原出二叉樹的形狀,因為無法確認其左右孩子。例如,前序為AB,後序為AB,則無法確認出,B節點是A節點的左孩子還是右孩子,因此無法還原。

題解

思路:通過前序遍歷獲得根節點的位置,利用根節點將中序序列分為左子樹和右子樹,然後不斷的遞歸劃分即可。

代碼中有解釋。

  private Node buildTree(int[] pre, int preBegin, int preEnd, int[] mid, int midBegin, int midEnd) {
        // 前序遍歷確第一個元素為根節點
        Node root = new Node(pre[preBegin]);
        // 用於標記中序遍歷結果中根節點的位置
        int midRootLocation= 0;
        for (int i = midBegin; i <= midEnd; i++) {
            if (mid[i] == pre[preBegin]) {  
                midRootLocation= i;
                break;
            }
        }

        if ( midRootLocation - midBegin >= 1 ) {
            // 遞歸得到左子樹
            
            // 中序遍歷:左子樹—> 根結點 —> 右子樹(在中間遍歷根節點)
            // midRootLocation標記了中序遍歷結果中根節點的位置,這個位置兩端對應根節點左子樹和右子樹
            // midRootLocation- midBegin 表示該根節點左邊的節點的數量
            
            // 前序遍歷:根結點 —> 左子樹 —> 右子樹(先遍歷根節點,然後左右)
            // preBegin 標記了前序遍歷結果中根節點的位置
            // preBegin + 1 表示該根節點左子樹起始位置
            // preBegin + (midRootLocation- midBegin) 表示給根節點左子樹結束的位置
            Node left = buildTree(pre, preBegin + 1, preBegin + (midRootLocation- midBegin),
                    mid, midBegin, midRootLocation - 1);
            root.left = left;
        }


        if ( midEnd - midRootLocation >= 1 ) {
            // 遞歸得到右子樹
            // 原理和上面相同
            Node right = buildTree(pre, preEnd - (midEnd - midRootLocation) + 1, preEnd,
                    mid, midRootLocation+ 1, midEnd);
            root.right = right;
        }

        return root;
    }

舉一反三

(1)給出後序遍歷和中序遍歷的結果重建二叉樹

思路:通過後序獲取根節點的位置,然後在中序中劃分左子樹和右子樹,然後遞歸劃分即可。

形式與上面 給出 前序遍歷和中序遍歷的結果重建二叉樹相同

    private Node buildTree(int[] mid, int midBegin, int midEnd, int[] end, int endBegin, int endEnd) {
        
        Node root = new Node(end[endEnd]);
        int midRootLocation = 0;
        for (int i = midEnd; i >= midBegin; i--) {
            if (mid[i] == end[endEnd]) {
                midRootLocation = i;
                break;
            }
        }
        
        //還原左子樹
        if (midRootLocation - midBegin >= 1 ) {
            Node left = buildTree(mid, midBegin, midRootLocation - 1, end, endBegin, endBegin + (midRootLocation - midBegin) - 1);
            root.left = left;
        }

        //還原右子樹
        if (midEnd - midRootLocation >=  1 ) {
            Node right = buildTree(mid, midRootLocation + 1, midEnd, end, endEnd - (midEnd - midRootLocation), endEnd - 1);
            root.right = right;
        }
        
        return root;
    }

(2)給出層序遍歷和中序遍歷結果重建二叉樹

思路:

(1)根據層序遍歷獲取根節點的位置

(2)根據(1)將中序劃分為左子樹和右子樹

(3)根據(2)劃分出的左子樹和右子樹分別在層序遍歷中獲取其對應的層序順序

(4)然後遞歸調用劃分。

  private Node buildTree(int[] mid, int[] level, int midBegin, int midEnd) {

        // 層序遍歷的第一個結果是 根節點
        Node root = new Node(level[0]);
        // 用於標記中序遍歷的根節點
        int midLocation = -1;
        for (int i = midBegin; i <= midEnd; i++) {
            if (mid[i] == level[0]) {
                midLocation = i;
                break;
            }
        }

        if (level.length >= 2) {
            if (isLeft(mid, level[0], level[1])) {
                Node left = buildTree(mid, getLevelArray(mid, midBegin, midLocation - 1, level), midBegin, midLocation - 1);
                root.left = left;
                if (level.length >= 3 && !isLeft(mid, level[0], level[2])) {
                    Node right = buildTree(mid, getLevelArray(mid, midLocation + 1, midEnd, level), midLocation + 1, midEnd);
                    root.right = right;
                }
            } else {
                Node right = buildTree(mid, getLevelArray(mid, midLocation + 1, midEnd, level), midLocation + 1, midEnd);
                root.right = right;
            }
        }
        return root;
    }


    // 功能 : 判斷元素是根節點的左子樹節點還是右子樹節點
    // 參數 : target為根節點 isLeft在中序遍歷結果中判斷children是根節點的左子樹還是右子樹
    // 返回值 : 如果為左子樹節點則為true 否則為false
    private boolean isLeft(int[] array, int target, int children) {
        boolean findC = false;

        for (int temp : array) {
            if (temp == children) {
                findC = true;
            } else if (temp == target) {
                return findC;
            }
        }
        return false;
    }


    // 功能: 將中序序列中midBegin與midEnd的元素依次從level中提取出來,保持level中的元素順序不變
    private int[] getLevelArray(int[] mid, int midBegin, int midEnd, int[] level) {
        int[] result = new int[midEnd - midBegin + 1];
        int curLocation = 0;
        for (int i = 0; i < level.length; i++) {
            if (contains(mid, level[i], midBegin, midEnd)) {
                result[curLocation++] = level[i];
            }
        }
        return result;
    }

    // 如果array的begin和end位置之間(包括begin和end)含有target,則返回true。
    private boolean contains(int[] array, int target, int begin, int end) {
        for (int i = begin; i <= end; i++) {
            if (array[i] == target) {
                return true;
            }
        }
        return false;
    }

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 第一次用maven創建項目的時候,因為本地倉庫中沒有jar包,需要從中央倉庫下載,所以會比較慢,因為從中央倉庫下載預設使用的國外的鏡像下載,速度比較慢,我們可以把鏡像修改為從阿裡雲下載,這樣比較快,方法,打開maven在本地的位置,找到conf文件夾下的setting,xml打開,在mirrors標... ...
  • 學完maven後,可以創建maven的javaweb工程,在創建完成後還需要一些配置,下麵來說下具體步驟,在這裡我創建的是一個模塊,創建web項目的方式和創建模塊一樣 ...
  • MyBatis Plus是一個 MyBatis的增強工具,在 MyBatis 的基礎上只做增強不做改變,使用MyBatis Plus時,不會影響原來Mybatis方式的使用。 SpringBoot+MyBatis Plus環境搭建 SQL腳本: CREATE TABLE ( int(11) NOT ...
  • 二叉樹創建 先序線索、中序線索,通過線索進行的 先序遍歷、中序遍歷。 main.cpp: #include <iostream> #include <queue> #include "ThreadedBinaryTree.h" using namespace std; int main() { qu ...
  • Java 虛擬機中定義的 Class 文件格式。每一個 Class 文件都對應著唯一一個類 或介面的定義信息,但是相對地,類或介面並不一定都得定義在文件里(譬如類或介面也可以通過 類載入器直接生成)。本章中,我們只是通俗地將任意一個有效的類或介面所應當滿足的格式稱為 “Class 文件格式”,即使它 ...
  • 整型 簡介 # 是否可變類型: 不可變類型 # 作用:記錄年齡、手機號 # 定義: age = 18 # --> 內部操作 age = int(18) # int('sada') # 報錯 int(1.1) # int('1.1') # int() 只能轉純數字的字元串,小數點都不行 a = 111 ...
  • 學習完maven後,用maven創建了一個web項目,然後在這個web項目中創建了一個java文件夾並標記這個目錄為源碼根目錄,當我準備創建一個Servlet的時候發現沒有 ...
  • Python中集合的相關操作 集合是一個無序的,不重覆的數據組合 它的主要作用如下: 去重,把一個列表變成集合,就自動去重了 關係測試,測試兩組數據之前的交集、差集、並集等關係 list_1 =set( [1, 3, 5, 4, 3, 7, 8, 1, 9]) #創建一個集合 list_2 = se ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...