面試題 04.02. 最小高度樹-用遊標將數組切割成虛擬數組。

做題目前隨便說點

  • 樹是一種抽象數據類型,一種具有樹結構形式的數據集合。

  • 節點個數確定,有層次關係。

  • 有根節點。

  • 除了根,每個節點有且只有一個父節點。

  • 沒有環路。

  • 所有數據結構都可以用鏈表表示或者用數組表示,樹也一樣。

面試題04.02. 最小高度樹

給定一個有序整數數組,元素各不相同且按升序排列,編寫一個算法,創建一棵高度最小的二叉搜索樹。

示例:
給定有序數組: [-10,-3,0,5,9],

一個可能的答案是:[0,-3,9,-10,null,5],它可以表示下面這個高度平衡二叉搜索樹:

     0 
    / \ 
  -3   9 
  /   / 
-10  5 

來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/minimum-height-tree-lcci
著作權歸領釦網絡所有。商業轉載請聯繫官方授權,非商業轉載請註明出處。

  • 給定一個數組,也就是給定一個支持下標訪問的數據結構。而且有順序。

  • 元素各不同,也就是不存在元素相等的情況。

  • 升序,也就意味着數組是有順序的,方便我們查找和搜索等定位工作。

  • 返回值是一個二叉樹,而且具搜索功能,換句話說,就是用根節點把數組的數字分成兩堆,左右兩邊的數組也按照這個順序建立樹。這就是二叉搜索樹了,相比普通的二叉樹,二叉搜索樹轉化爲中序遍歷數組具有順序(降序或者升序)。

  • 二叉搜索樹具有順序性質的性質,或者說每個節點值都介於左右子節點之間,也具有二叉樹的二叉性質(2個分叉的性質)。

  • 高度最小的樹,就是說從根節點到葉子的最長距越小越好,極端的做法就是所有節點都放在根節點的左端,所有節點都沒有右節點。當然這肯定本題目最不想看到的答案。那麼高度最小要怎麼理解麼?難道全部放在右邊?肯定也不是。正確的做法應該是平均分攤到兩邊。

  • 我們知道二叉搜索樹的中序遍歷是有順序的,那麼反過來說有順序的數組也可以二叉搜索樹的中序遍歷。好巧。

  • 所以我們可以採用中序遍歷的方式一顆樹,從根節點開始,然後把數組的值按照中序遍歷的規則訪問下標,讀取值,然後附值給二叉樹的對應節點。爲什麼可以這樣做呢?因爲我們剛剛見過了,二叉搜索樹和有序數組可以按照中序遍歷的形式相互一一映射,也就是說相互對應起來。我們可以對二叉搜索樹進行中序遍歷讀取值,並附值給一位數組。那麼用同樣的代碼框架,我們也可以把讀取對應下表的數組值附值給二叉搜索樹的對應節點。

  • 思路確定了,那麼下一個問題,我們知道,已知一種遍歷方式是無法確定二叉樹的結構的(二叉樹有3種遍歷方式,前序,中序,後續遍歷。)。所以已知中序遍歷我們無法確定二叉樹的結構,因爲一箇中序遍歷可以映射出多種二叉樹結構。根據我們選擇根節點的方式不同,那麼構造出來的樹結構也就不一樣了。怎麼選呢?我們上面也分析了,應該從中間開始選。也就是對於每個節點的附值都儘量選數組的中間值來附值。每個節點都按照這個策略。那麼構造出來的樹一定是左右兩邊一樣長(如果數組的元素是奇數個的話)。如果是偶數個,可以選擇把多出來的一個留給左子樹也可以留給右子樹。

  • 還有一個問題就是存儲的問題,我們知道遞歸算法的一個弊病就是容易造成很大的內存開銷。

  • 所以我們用遞歸算法遍歷二叉樹一定要考慮傳值問題,最無腦的方法就是不停的申請局部變量進行傳值,遞歸算法的遞推階段不停的開闢局部變量,那麼這個將是要命的。

  • 我們知道遞歸算法在遞推過程開闢函數棧空間已經很要命了,如果還要申請局部變量那就更要命了,前者我們沒辦法(因爲大部分遞歸都不是尾遞歸,所以無法轉化爲迭代算法),後者我們可以選擇不用局部變量,而選擇全局變量的方案。

  • 一旦以全局變量的方案執行,我們就可以利用變量的全局性,實現資源共享,相比每個函數一個變量,這種多個函數共享一個或者n個變量的做法就顯得節約很多了。其次我們還可以利用“共享”的特點做到函數間的“0拷貝”傳值。比如正常情況下,我們傳遞一個參數給另一個函數,是需要申請參數變量的內存空間。而共享內存傳值就不需要,頂多只需要傳一個索引變量用以告訴其它函數全局空間的變量座標位置。

  • 不管如何這種分時共享全局變量的方案是一種很聰明的節省空間的做法。當前各種什麼雲虛擬機技術,也就是利用這個思想來做的,從而節省了物理機的購買開銷。(但是虛擬機用戶的費用開銷不一定會減少,手動滑稽。)

  • 好了,所有問題都解決了,接下來就開始寫框架和算法了。

解題:

審題:

代碼框架如下:

class Solution {
    
    TreeNode recursive(數組) {
        // 邊界判斷
        if(...) {
            return node;
        }
        
        // 前序遍歷,訪問節點,讀取或者修改屬性值。
        // 遞歸訪問左子樹
        recursive(左堆的數組);
        // 中序遍歷
        中間值數組附值給根節點的val。
        // 遞歸訪問左子樹
        recursive(右堆的數組);
        // 後續遍歷
        // 也可以在這裏讀取或者修改屬性值,這種是回溯思路。
        // 返回值根節點。
        return node;
    }
}
  • 如果先遞歸左右子樹叫回溯法,因爲編譯成計算機指令程序的話,會先修改葉子的值再修改根的值。然後一步一步return。他是自下而上的update每個節點的屬性。
  • 如果先訪問修改節點信息,然後才遞歸左右子樹,就是普通的非線性遞歸了。

(這裏不要扯複雜先。專心學習框架。暴力解題。)

開始解題:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    
    class RefArray {
     public int[] source;
     public int length;
     public int baseStart;
     public RefArray(int[] arr, int length, int baseStart) {
        this.source = arr;
        this.length = length;
        this.baseStart = baseStart;
     } 
    }
     
     private boolean isEmptyJavaArray(RefArray refArr) {
        boolean result = (refArr == null || refArr.length == 0);
        return result;
     }

     private RefArray getLeftArray(RefArray refArr) {
        // 除2表示如果是偶數,多處的一個元素就分配給左邊的樹。這是一個細節問題,細品就明白了。
        int leftArrayLength = refArr.length/2;
        RefArray leftArray = new RefArray(refArr.source, leftArrayLength, refArr.baseStart);
        return leftArray;
        
     }
     private int getMiddleNum(RefArray refArr) {
        int middleIndex = refArr.length/2;
        return refArr.source[refArr.baseStart + middleIndex];
     }
     
     private RefArray getRightArray(RefArray refArr) {
        // 除2表示如果是偶數,多處的一個元素就分配給左邊的樹。這是一個細節問題,細品就明白了。數學公式來的,自己推導。
        int rightArrayLength = refArr.length - (refArr.length/2 + 1);
        // 算座標值一定要記得要用物理座標refArr.baseStart。
        int rightStartIndex = refArr.baseStart + ( refArr.length - rightArrayLength );
        RefArray rightArray = new RefArray(refArr.source, rightArrayLength, rightStartIndex);
        return rightArray;
        
     }
    
     public TreeNode recursive(RefArray refArr) {
        TreeNode root = null;
        // 邊界判斷
        if(isEmptyJavaArray(refArr)) {
            return root;
        } else {
            // 初始化
            root = new TreeNode(0);
        }
        
        // 遞歸訪問左子樹
        TreeNode leftSonRoot = recursive(getLeftArray(refArr));  
        root.left = leftSonRoot;
        
        // 中序遍歷
        // explain: 中間值數組附值給根節點的val。
        root.val = getMiddleNum(refArr);
        
        // 遞歸訪問右子樹
        TreeNode rightSonRoot = recursive(getRightArray(refArr));
        root.right = rightSonRoot;
        
        // 返回值根節點。
        return root;
    }

    public TreeNode sortedArrayToBST(int[] nums) {
        if( nums == null ) {
            return null;
        }
        return recursive(new RefArray(nums, nums.length, 0));
    }
}

總結:

  • 該題目有一個難點,就是處理數組的切割,我們創建了一個引用類型的虛擬數組來切割物理數組,所以物理座標到虛擬座標之間的換算是個難點。考察計算機內存基準遍歷的基礎知識。

  • 這道題目代碼結構上經過封裝,將數學運算座標的細節封裝在模塊方法裏。而遞歸的部分儘量做到模仿二叉樹的中序遍歷思路是對了,畢竟“二叉搜索樹的中序遍歷得到的數組是有序的數組,那麼有序數組一定可以遵守中序遍歷下標訪問規則並延續中序遍歷的遞歸路徑重建二叉搜索樹。”怎麼出來就怎麼回去。

  • 這個題目我們運用了共享全局變量來節省內存空間的思想,以及運用面向對象的封裝思想,設計了一個虛擬數組類RefArray.class(Reference Array)。從而讓遞歸框架專心處理遞歸構建二叉搜索樹的工作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章