🌳

固定長データを持つ全二分木のシリアライズとデシリアライズ

2022/10/22に公開

概要

全二分木 (全てのノードが葉であるか, 2つの子を持つ二分木) をビット列にシリアライズする方法についてまとめる. なお, 全二分木のデータは固定長であるとする.

前提

本記事では, 二分木を次のデータ構造で表す.

structure Node
   Node left,
   Node right,
   BitArray data,
end structure

ここで, ノードが葉である場合, leftrightには null が入る.

シリアライズ

二分木を後置順で深さ優先探索し, その順にノードをビット列として書き込んでいく. ビット列への変換は次のルールで行う.

  • 葉ノードの場合, 1 とそれに続けてデータを書き込む.
  • 枝ノードの場合, 0 とそれに続けてデータを書き込む.
  • 最後には, 木の終端を示す 0 を書き込む.

この処理は次の疑似コードで表される.

function serialize(Node node, BitArray bits): BitArray
   if node.left is not null and node.right is not null then
      bits <- serialize(node.left, bits)
      bits <- serialize(node.right, bits)
      bits.push(1)
   else
      bits.push(0)
   end if
   Concatenate node.data to the end of bits.
   return bits
end function

例として, 各ノードが8ビット固定長のデータを持つ, 次のような二分木を考える.

Binary Tree

これを後置順で探索した結果は

d -> e -> b -> f -> g -> c -> a

であるから, 上記のルールで変換すれば

1 00100011 1 11001111 0 01000101 1 10001000 1 00001100 0 00011010 0 00000001 0

となる.

デシリアライズ

ビット列の先頭から見ていき, 1 ならばデータを基に葉ノードを生成し, スタックに追加する. 0 ならばスタックから2つノードを取り出し, それとデータを基に枝ノードを生成, 同様にスタックに追加する. そして 0 を見つけたときに, スタックの長さが1ならば, 木の終端に達したとして終了となる.

この処理は次の疑似コードで表される.

function deserialize(BitArray bits): Node
   stack <- empty stack
   for i = 0, 9, ..., bits_length - 1 do
      if bits[i] = 1 then
         data <- bits[i+1..i+8]
         node <- Node { left: null, right: null, data }
      else
         if stack_length = 1 then
            return stack.pop()
         end if
         left <- stack.pop()
         right <- stack.pop()
         data <- bits[i+1..i+8]
         node <- Node { left, right, data }
      end if
      stack.push(node)
   end for
end function

シリアライズと同様の例を考える. シリアライズされたビット列

1 00100011 1 11001111 0 01000101 1 10001000 1 00001100 0 00011010 0 00000001 0

を先頭から x ビット目まで読んだときのスタックの中身は, 次のように変化する. 以下, 葉ノードをLeaf(data), 枝ノードをBranch(data, left, right)と表す.

x : stack
0 : []
18: [ Leaf(0x23), Leaf(0xCF) ]
27: [ Branch(0x45, Leaf(0x23), Leaf(0xCF)) ]
45: [
       Branch(0x45, Leaf(0x23), Leaf(0xCF)),
       Leaf(0x88),
       Leaf(0x0C)
    ]
54: [
       Branch(0x45, Leaf(0x23), Leaf(0xCF)),
       Branch(0x1A, Leaf(0x88), Leaf(0x0C))
    ]
63: [
       Branch(
          0x01,
          Branch(0x45, Leaf(0x23), Leaf(0xCF)),
          Branch(0x1A, Leaf(0x88), Leaf(0x0C))
       )
    ]

よって, 0 である64ビット目を読む時のスタックの長さは1であるから, 処理が終了される. そして, このときのスタックの中身が元の木構造になっている.

GitHubで編集を提案

Discussion