一文帶你看懂二叉樹的序列化

咱們先來看下什麼是序列化,如下定義來自維基百科:java

序列化(serialization)在計算機科學的數據處理中,是指將數據結構或對象狀態轉換成可取用格式(例如存成文件,存於緩衝,或經由網絡中發送),以留待後續在相同或另外一臺計算機環境中,能恢復原先狀態的過程。依照序列化格式從新獲取字節的結果時,能夠利用它來產生與原始對象相同語義的副本。對於許多對象,像是使用大量引用的複雜對象,這種序列化重建的過程並不容易。面向對象中的對象序列化,並不歸納以前原始對象所關係的函數。這種過程也稱爲對象編組(marshalling)。從一系列字節提取數據結構的反向操做,是反序列化(也稱爲解編組、deserialization、unmarshalling)。

可見,序列化和反序列化在計算機科學中的應用仍是很是普遍的。就拿 LeetCode 平臺來講,其容許用戶輸入形如:node

[1,2,3,null,null,4,5]

這樣的數據結構來描述一顆樹:git

([1,2,3,null,null,4,5] 對應的二叉樹)github

其實序列化和反序列化只是一個概念,不是一種具體的算法,而是不少的算法。而且針對不一樣的數據結構,算法也會不同。本文主要講述的是二叉樹的序列化和反序列化。看完本文以後,你就能夠放心大膽地去 AC 如下兩道題:算法

<!-- more -->數組

前置知識

閱讀本文以前,須要你對樹的遍歷以及 BFS 和 DFS 比較熟悉。若是你還不熟悉,推薦閱讀一下相關文章以後再來看。或者我這邊也寫了一個總結性的文章二叉樹的遍歷,你也能夠看看。網絡

前言

咱們知道:二叉樹的深度優先遍歷,根據訪問根節點的順序不一樣,能夠將其分爲前序遍歷中序遍歷, 後序遍歷。即若是先訪問根節點就是前序遍歷,最後訪問根節點就是後續遍歷,其它則是中序遍歷。而左右節點的相對順序是不會變的,必定是先左後右。數據結構

固然也能夠設定爲先右後左。

而且知道了三種遍歷結果中的任意兩種便可還原出原有的樹結構。這不就是序列化和反序列化麼?若是對這個比較陌生的同窗建議看看我以前寫的《構造二叉樹系列》app

有了這樣一個前提以後算法就天然而然了。即先對二叉樹進行兩次不一樣的遍歷,不妨假設按照前序和中序進行兩次遍歷。而後將兩次遍歷結果序列化,好比將兩次遍歷結果以逗號「,」 join 成一個字符串。 以後將字符串反序列便可,好比將其以逗號「,」 split 成一個數組。函數

序列化:

class Solution:
    def preorder(self, root: TreeNode):
        if not root: return []
        return [str(root.val)] +self. preorder(root.left) + self.preorder(root.right)
    def inorder(self, root: TreeNode):
        if not root: return []
        return  self.inorder(root.left) + [str(root.val)] + self.inorder(root.right)
    def serialize(self, root):
        ans = ''
        ans += ','.join(self.preorder(root))
        ans += '$'
        ans += ','.join(self.inorder(root))

        return ans

反序列化:

這裏我直接用了力扣 105. 從前序與中序遍歷序列構造二叉樹 的解法,一行代碼都不改。

class Solution:
    def deserialize(self, data: str):
        preorder, inorder = data.split('$')
        if not preorder: return None
        return self.buildTree(preorder.split(','), inorder.split(','))

    def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
        # 實際上inorder 和 preorder 必定是同時爲空的,所以你不管判斷哪一個都行
        if not preorder:
            return None
        root = TreeNode(preorder[0])

        i = inorder.index(root.val)
        root.left = self.buildTree(preorder[1:i + 1], inorder[:i])
        root.right = self.buildTree(preorder[i + 1:], inorder[i+1:])

        return root

實際上這個算法是不必定成立的,緣由在於樹的節點可能存在重複元素。也就是說我前面說的知道了三種遍歷結果中的任意兩種便可還原出原有的樹結構是不對的,嚴格來講應該是若是樹中不存在重複的元素,那麼知道了三種遍歷結果中的任意兩種便可還原出原有的樹結構

聰明的你應該發現了,上面個人代碼用了 i = inorder.index(root.val),若是存在重複元素,那麼獲得的索引 i 就可能不是準確的。可是,若是題目限定了沒有重複元素則能夠用這種算法。可是現實中不出現重複元素不太現實,所以須要考慮其餘方法。那到底是什麼樣的方法呢? 接下來進入正題。

DFS

序列化

咱們來模仿一下力扣的記法。 好比:[1,2,3,null,null,4,5](本質上是 BFS 層次遍歷),對應的樹以下:

選擇這種記法,而不是 DFS 的記法的緣由是看起來比較直觀

序列化的代碼很是簡單, 咱們只須要在普通的遍歷基礎上,增長對空節點的輸出便可(普通的遍歷是不處理空節點的)。

好比咱們都樹進行一次前序遍歷的同時增長空節點的處理。選擇前序遍歷的緣由是容易知道根節點的位置,而且代碼好寫,不信你能夠試試。

所以序列化就僅僅是普通的 DFS 而已,直接給你們看看代碼。

Python 代碼:

class Codec:
    def serialize_dfs(self, root, ans):
        # 空節點也須要序列化,不然沒法惟一肯定一棵樹,後不贅述。
        if not root: return ans + '#,'
        # 節點之間經過逗號(,)分割
        ans += str(root.val) + ','
        ans = self.serialize_dfs(root.left, ans)
        ans = self.serialize_dfs(root.right, ans)
        return ans
    def serialize(self, root):
        # 因爲最後會添加一個額外的逗號,所以須要去除最後一個字符,後不贅述。
        return self.serialize_dfs(root, '')[:-1]

Java 代碼:

public class Codec {
    public String serialize_dfs(TreeNode root, String str) {
        if (root == null) {
            str += "None,";
        } else {
            str += str.valueOf(root.val) + ",";
            str = serialize_dfs(root.left, str);
            str = serialize_dfs(root.right, str);
        }
        return str;
    }

    public String serialize(TreeNode root) {
        return serialize_dfs(root, "");
    }
}

[1,2,3,null,null,4,5] 會被處理爲1,2,#,#,3,4,#,#,5,#,#

咱們先看一個短視頻:

(動畫來自力扣)

反序列化

反序列化的第一步就是將其展開。以上面的例子來講,則會變成數組:[1,2,#,#,3,4,#,#,5,#,#],而後咱們一樣執行一次前序遍歷,每次處理一個元素,重建便可。因爲咱們採用的前序遍歷,所以第一個是根元素,下一個是其左子節點,下下一個是其右子節點。

Python 代碼:

def deserialize_dfs(self, nodes):
        if nodes:
            if nodes[0] == '#':
                nodes.pop(0)
                return None
            root = TreeNode(nodes.pop(0))
            root.left = self.deserialize_dfs(nodes)
            root.right = self.deserialize_dfs(nodes)
            return root
        return None

    def deserialize(self, data: str):
        nodes = data.split(',')
        return self.deserialize_dfs(nodes)

Java 代碼:

public TreeNode deserialize_dfs(List<String> l) {
        if (l.get(0).equals("None")) {
            l.remove(0);
            return null;
        }

        TreeNode root = new TreeNode(Integer.valueOf(l.get(0)));
        l.remove(0);
        root.left = deserialize_dfs(l);
        root.right = deserialize_dfs(l);

        return root;
    }

    public TreeNode deserialize(String data) {
        String[] data_array = data.split(",");
        List<String> data_list = new LinkedList<String>(Arrays.asList(data_array));
        return deserialize_dfs(data_list);
    }

複雜度分析

  • 時間複雜度:每一個節點都會被處理一次,所以時間複雜度爲 $O(N)$,其中 $N$ 爲節點的總數。
  • 空間複雜度:空間複雜度取決於棧深度,所以空間複雜度爲 $O(h)$,其中 $h$ 爲樹的深度。

BFS

序列化

實際上咱們也可使用 BFS 的方式來表示一棵樹。在這一點上其實就和力扣的記法是一致的了。

咱們知道層次遍歷的時候其實是有層次的。只不過有的題目須要你記錄每個節點的層次信息,有些則不須要。

這其實就是一個樸實無華的 BFS,惟一不一樣則是增長了空節點。

Python 代碼:

class Codec:
    def serialize(self, root):
        ans = ''
        queue = [root]
        while queue:
            node = queue.pop(0)
            if node:
                ans += str(node.val) + ','
                queue.append(node.left)
                queue.append(node.right)
            else:
                ans += '#,'
        return ans[:-1]

反序列化

如圖有這樣一棵樹:

那麼其層次遍歷爲 [1,2,3,#,#, 4, 5]。咱們根據此層次遍歷的結果來看下如何還原二叉樹,以下是我畫的一個示意圖:

容易看出:

  • level x 的節點必定指向 level x + 1 的節點,如何找到 level + 1 呢? 這很容易經過層次遍從來作到。
  • 對於給的的 level x,從左到右依次對應 level x + 1 的節點,即第 1 個節點的左右子節點對應下一層的第 1 個和第 2 個節點,第 2 個節點的左右子節點對應下一層的第 3 個和第 4 個節點。。。
  • 接上,其實若是你仔細觀察的話,實際上 level x 和 level x + 1 的判斷是無需特別判斷的。咱們能夠把思路逆轉過來:即第 1 個節點的左右子節點對應第 1 個和第 2 個節點,第 2 個節點的左右子節點對應第 3 個和第 4 個節點。。。(注意,沒了下一層三個字)

所以咱們的思路也是一樣的 BFS,並依次鏈接左右節點。

Python 代碼:

def deserialize(self, data: str):
        if data == '#': return None
        # 數據準備
        nodes = data.split(',')
        if not nodes: return None
        # BFS
        root = TreeNode(nodes[0])
        queue = [root]
        # 已經有 root 了,所以從 1 開始
        i = 1

        while i < len(nodes) - 1:
            node = queue.pop(0)
            #
            lv = nodes[i]
            rv = nodes[i + 1]
            i += 2
            # 對於給的的 level x,從左到右依次對應 level x + 1 的節點
            # node 是 level x 的節點,l 和 r 則是 level x + 1 的節點
            if lv != '#':
                l = TreeNode(lv)
                node.left = l
                queue.append(l)

            if rv != '#':
                r = TreeNode(rv)
                node.right = r
                queue.append(r)
        return root

複雜度分析

  • 時間複雜度:每一個節點都會被處理一次,所以時間複雜度爲 $O(N)$,其中 $N$ 爲節點的總數。
  • 空間複雜度:$O(N)$,其中 $N$ 爲節點的總數。

總結

除了這種方法還有不少方案, 好比括號表示法。 關於這個能夠參考力扣606. 根據二叉樹建立字符串,這裏就再也不贅述了。

本文從 BFS 和 DFS 角度來思考如何序列化和反序列化一棵樹。 若是用 BFS 來序列化,那麼相應地也須要 BFS 來反序列化。若是用 DFS 來序列化,那麼就須要用 DFS 來反序列化。

咱們從馬後炮的角度來講,實際上對於序列化來講,BFS 和 DFS 都比較常規。對於反序列化,你們能夠像我這樣舉個例子,畫一個圖。能夠先在紙上,電腦上,若是你熟悉了以後,也能夠畫在腦子裏。

(Like This)

更多題解能夠訪問個人 LeetCode 題解倉庫:https://github.com/azl3979858... 。 目前已經 30K star 啦。

關注公衆號力扣加加,努力用清晰直白的語言還原解題思路,而且有大量圖解,手把手教你識別套路,高效刷題。

相關文章
相關標籤/搜索