{"id":6196,"date":"2025-01-05T21:13:14","date_gmt":"2025-01-05T21:13:14","guid":{"rendered":"https:\/\/algocademy.com\/blog\/how-to-handle-tree-serialization-a-comprehensive-guide\/"},"modified":"2026-10-15T12:51:16","modified_gmt":"2026-10-15T12:51:16","slug":"how-to-handle-tree-serialization-a-comprehensive-guide","status":"publish","type":"post","link":"https:\/\/algocademy.com\/blog\/how-to-handle-tree-serialization-a-comprehensive-guide\/","title":{"rendered":"Tree Serialization and Deserialization: Preorder and Level Order"},"content":{"rendered":"<p>To serialize a binary tree, walk it in a fixed order and write a marker such as <code>null<\/code> for every missing child. To deserialize, read the tokens back in the same order. The markers are what make the result unambiguous: without them, a list of values cannot tell you where the branches are. This is the problem behind LeetCode 297, &#8220;Serialize and Deserialize Binary Tree&#8221;, and the two standard answers are preorder (depth-first) and level order (breadth-first). Both are below, with code that has been run.<\/p>\n<h2>The setup<\/h2>\n<p>All the code uses this node class. The functions must work for any tree, including an empty tree, a single node, a skewed tree and negative values.<\/p>\n<pre><code class=\"language-python\">from collections import deque\n\nclass TreeNode:\n    def __init__(self, val=0, left=None, right=None):\n        self.val = val\n        self.left = left\n        self.right = right<\/code><\/pre>\n<p>The example tree used throughout:<\/p>\n<pre><code class=\"language-python\">#      1\n#     \/ \\\n#    2   3\n#       \/ \\\n#      4   5\nroot = TreeNode(1, TreeNode(2), TreeNode(3, TreeNode(4), TreeNode(5)))<\/code><\/pre>\n<h2>Why you need null markers<\/h2>\n<p>The preorder values of this tree are <code>1,2,3,4,5<\/code>. So are the preorder values of a right-leaning chain <code>1 -> 2 -> 3 -> 4 -> 5<\/code>. One traversal of values alone does not identify a tree. (For a binary search tree it does, because the ordering rule tells you where each value goes. That special case is covered near the end.) For a general binary tree, record the empty children too, and the traversal becomes reversible.<\/p>\n<h2>Approach 1: preorder with null markers<\/h2>\n<p>Visit the node, then the left subtree, then the right subtree. Emit <code>null<\/code> whenever you hit an empty child.<\/p>\n<pre><code class=\"language-python\">def serialize_preorder(root):\n    out = []\n    def dfs(node):\n        if node is None:\n            out.append(\"null\")\n            return\n        out.append(str(node.val))\n        dfs(node.left)\n        dfs(node.right)\n    dfs(root)\n    return \",\".join(out)\n\ndef deserialize_preorder(data):\n    tokens = iter(data.split(\",\"))\n    def build():\n        tok = next(tokens)\n        if tok == \"null\":\n            return None\n        node = TreeNode(int(tok))\n        node.left = build()\n        node.right = build()\n        return node\n    return build()<\/code><\/pre>\n<p>For the example tree this prints:<\/p>\n<pre><code class=\"language-text\">1,2,null,null,3,4,null,null,5,null,null<\/code><\/pre>\n<h3>How deserialization reads it back<\/h3>\n<p>The shared iterator is the whole trick. Each call to <code>build()<\/code> consumes exactly the tokens of one subtree, then returns, so the next call starts at the right place.<\/p>\n<ol>\n<li><code>1<\/code>: create node 1, then build its left subtree.<\/li>\n<li><code>2<\/code>: create node 2, build its left subtree. The next token is <code>null<\/code>, so left is empty. The token after is <code>null<\/code> too, so right is empty. Node 2 is a leaf; return it.<\/li>\n<li>Back at node 1, build the right subtree. <code>3<\/code> becomes a node; its left reads <code>4<\/code>, <code>null<\/code>, <code>null<\/code> (a leaf), its right reads <code>5<\/code>, <code>null<\/code>, <code>null<\/code>.<\/li>\n<li>All tokens are used and the tree is rebuilt.<\/li>\n<\/ol>\n<p>Serializing the rebuilt tree gives the same string back, which confirms the round trip.<\/p>\n<h2>Approach 2: level order (BFS) with null markers<\/h2>\n<p>This is the format LeetCode uses to display trees, so it is the one you see in examples like <code>[1,2,3,null,null,4,5]<\/code>. Use a queue, and push both children of every real node, including missing ones.<\/p>\n<pre><code class=\"language-python\">def serialize_level(root):\n    if root is None:\n        return \"\"\n    out, queue = [], deque([root])\n    while queue:\n        node = queue.popleft()\n        if node is None:\n            out.append(\"null\")\n        else:\n            out.append(str(node.val))\n            queue.append(node.left)\n            queue.append(node.right)\n    while out[-1] == \"null\":   # trailing nulls carry no information\n        out.pop()\n    return \",\".join(out)\n\ndef deserialize_level(data):\n    if not data:\n        return None\n    tokens = data.split(\",\")\n    root = TreeNode(int(tokens[0]))\n    queue, i = deque([root]), 1\n    while queue and i &lt; len(tokens):\n        node = queue.popleft()\n        if i &lt; len(tokens) and tokens[i] != \"null\":\n            node.left = TreeNode(int(tokens[i]))\n            queue.append(node.left)\n        i += 1\n        if i &lt; len(tokens) and tokens[i] != \"null\":\n            node.right = TreeNode(int(tokens[i]))\n            queue.append(node.right)\n        i += 1\n    return root<\/code><\/pre>\n<p>For the example tree this prints:<\/p>\n<pre><code class=\"language-text\">1,2,3,null,null,4,5<\/code><\/pre>\n<h3>How deserialization reads it back<\/h3>\n<p>Tokens are consumed two at a time, as the left and right child of the next node in the queue. Only real nodes enter the queue, so each node in it is waiting for its children.<\/p>\n<ol>\n<li>Root is 1. Queue: [1]. Read <code>2<\/code>, <code>3<\/code>: they become the children of 1. Queue: [2, 3].<\/li>\n<li>Pop 2. Read <code>null<\/code>, <code>null<\/code>: no children. Queue: [3].<\/li>\n<li>Pop 3. Read <code>4<\/code>, <code>5<\/code>: its children. Queue: [4, 5].<\/li>\n<li>Pop 4 and 5: tokens are used up, the loop ends.<\/li>\n<\/ol>\n<p>Note that the output omits the nulls under 4 and 5. Trailing nulls are redundant because the reader stops when the tokens run out. Without trimming, the string would be <code>1,2,3,null,null,4,5,null,null,null,null<\/code>.<\/p>\n<h2>Which one to use<\/h2>\n<table>\n<thead>\n<tr>\n<th scope=\"col\"><\/th>\n<th scope=\"col\">Preorder<\/th>\n<th scope=\"col\">Level order<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Time<\/td>\n<td>O(n)<\/td>\n<td>O(n)<\/td>\n<\/tr>\n<tr>\n<td>Extra space<\/td>\n<td>O(h) call stack, plus the output<\/td>\n<td>O(w) queue, where w is the widest level, plus the output<\/td>\n<\/tr>\n<tr>\n<td>Code<\/td>\n<td>Shortest; recursion mirrors the tree<\/td>\n<td>Iterative; no recursion limit<\/td>\n<\/tr>\n<tr>\n<td>Output size<\/td>\n<td>n values and n + 1 nulls<\/td>\n<td>Same, minus the trailing nulls<\/td>\n<\/tr>\n<tr>\n<td>Matches LeetCode&#8217;s display format<\/td>\n<td>No<\/td>\n<td>Yes<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Here n is the number of nodes and h is the height. In the worst case h = n (a skewed tree). In an interview, preorder is usually the faster one to write correctly. Choose level order when the format matters or the tree can be very deep. Both visit every node once, so time is linear either way.<\/p>\n<h2>Edge cases to test<\/h2>\n<p>These cases were all run against both pairs of functions:<\/p>\n<ul>\n<li><strong>Empty tree.<\/strong> Preorder gives <code>null<\/code>, which reads back as <code>None<\/code>. The level-order version above returns an empty string, which also reads back as <code>None<\/code>.<\/li>\n<li><strong>Single node.<\/strong> <code>TreeNode(-7)<\/code> gives <code>-7,null,null<\/code> in preorder. Negative values work because the separator is a comma and values are parsed with <code>int()<\/code>.<\/li>\n<li><strong>Left-skewed tree<\/strong> (1 with left child 2 with left child 3). Preorder: <code>1,2,3,null,null,null,null<\/code>. Level order: <code>1,2,null,3<\/code>.<\/li>\n<li><strong>Duplicate values.<\/strong> No problem, because structure comes from the null markers, not the values.<\/li>\n<li><strong>Deep trees.<\/strong> A left chain of 5,000 nodes made the recursive preorder serializer raise <code>RecursionError<\/code> in CPython, whose default recursion limit is 1,000. The level-order version handled it. If the input can be that deep, use an iterative traversal with an explicit stack or the BFS format.<\/li>\n<\/ul>\n<h2>Common mistakes<\/h2>\n<ul>\n<li><strong>Omitting the nulls.<\/strong> Values alone are ambiguous, as shown above.<\/li>\n<li><strong>Ambiguous separators.<\/strong> If you join values with no delimiter, <code>1,23<\/code> and <code>12,3<\/code> become the same string. Use a delimiter and parse tokens, not characters.<\/li>\n<li><strong>Using an empty-string token for null<\/strong> and then splitting on commas. It works, but <code>\"1,,,\"<\/code> is easy to get wrong when trimming. An explicit <code>null<\/code> token (or <code>#<\/code>) is clearer.<\/li>\n<li><strong>Forgetting that deserialization must consume tokens in the same order<\/strong> as serialization wrote them. Mixing a preorder writer with a level-order reader gives wrong trees without any error.<\/li>\n<li><strong>Appending to a string in a loop<\/strong> in languages where that copies. Collect tokens in a list and join once.<\/li>\n<\/ul>\n<h2>Follow-up questions an interviewer asks<\/h2>\n<h3>Serialize a binary search tree compactly<\/h3>\n<p>This is LeetCode 449. Because of the BST ordering rule, the preorder values alone are enough: no null markers. To rebuild, insert values in order, or use the bounds trick: recursively take the next value if it lies between (low, high). That gives O(n) time and a shorter string.<\/p>\n<h3>Serialize an N-ary tree<\/h3>\n<p>There are no left and right slots, so write each node&#8217;s value followed by its child count, in preorder. The tree with root 1, children 3, 2, 4, and node 3 having children 5, 6, becomes <code>1 3 3 2 5 0 6 0 2 0 4 0<\/code>. Reading it back: take a value, take a count k, then build k children recursively. The round trip matched when run.<\/p>\n<pre><code class=\"language-python\">class Node:\n    def __init__(self, val, children=None):\n        self.val = val\n        self.children = children or []\n\ndef serialize_nary(n):\n    return f\"{n.val} {len(n.children)}\" + \"\".join(\" \" + serialize_nary(c) for c in n.children)\n\ndef deserialize_nary(s):\n    tokens = iter(s.split())\n    def build():\n        val, k = int(next(tokens)), int(next(tokens))\n        return Node(val, [build() for _ in range(k)])\n    return build()<\/code><\/pre>\n<h3>What if node values are strings that may contain commas?<\/h3>\n<p>Then the delimiter is no longer safe. Either escape the delimiter, prefix each value with its length (<code>3:abc<\/code>), or use a real format such as JSON.<\/p>\n<h3>What about a tree with cycles, or a general graph?<\/h3>\n<p>Preorder would loop forever. Give each node an id, keep a map of nodes already written, and emit a reference to the id instead of re-visiting. This is how most real serializers handle shared or cyclic object graphs.<\/p>\n<h3>Can you do it in less space?<\/h3>\n<p>The output must hold n values, so O(n) is the floor for the string. You can shrink the nulls with a bitmap for structure plus a separate list of values, or compress the text, but the interview answer is usually just to explain the O(n) bound and the trade-off.<\/p>\n<h2>Where this is used outside interviews<\/h2>\n<p>The same idea, flattening a hierarchy to a sequence that can be rebuilt, appears when you save a document tree or a scene graph, send a nested structure over the network, or store a decision tree from a machine learning library. In practice you rarely hand-roll the format: you use JSON, Protocol Buffers or the library&#8217;s own save function, and your job is choosing which fields to keep. The interview version is practice for reasoning about traversal order and reversibility.<\/p>\n<h2>Practice<\/h2>\n<p>If tree traversals still feel shaky, rehearse them first, since serialization is just a traversal with markers. The <a href=\"https:\/\/algocademy.com\/link\/?lang=py&amp;problem=level-order-traversal&amp;solution=1\">Level Order Traversal<\/a> and <a href=\"https:\/\/algocademy.com\/link\/?lang=py&amp;problem=post-order-tree-traversal&amp;solution=1\">Post Order Tree Traversal<\/a> lessons cover the two orders used here, and <a href=\"https:\/\/algocademy.com\/blog\/trees-and-their-applications-in-computer-science\/\">this overview of trees<\/a> covers the basics. Then solve LeetCode 297 yourself without looking, using both formats, and test the edge cases above. For more guided problems with step-by-step help, see <a href=\"https:\/\/app.algocademy.com\/\">AlgoCademy<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Serialize and deserialize a binary tree (LeetCode 297) with preorder and level-order null markers. Runnable Python, traces, complexity and follow-ups.<\/p>\n","protected":false},"author":1,"featured_media":6195,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[23],"tags":[],"class_list":["post-6196","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-problem-solving"],"_links":{"self":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts\/6196"}],"collection":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/comments?post=6196"}],"version-history":[{"count":2,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts\/6196\/revisions"}],"predecessor-version":[{"id":8934,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts\/6196\/revisions\/8934"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/media\/6195"}],"wp:attachment":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/media?parent=6196"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/categories?post=6196"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/tags?post=6196"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}