Note: The prerequisite for this article is an understanding of how binary trees work.
Trees are complex structures. Unlike linear collections such as arrays or linked lists, trees are non-linear and each element in a tree has positional information such as the parent-child relationship between nodes. When you want to send a tree structure to your backend, you need to send the data of each node, and a way to represent the parent-child relationship for each node.
Your strategy in how you choose to represent this information is called your encoding strategy. The opposite of that - changing your encoded data back to its original form - is your decoding strategy.
There are many ways to encode a tree and decode a tree. The important thing to keep in mind is that encoding and decoding strategies are closely related. The way you choose to encode a tree directly affects how you might decode a tree.
Encoding and decoding are synonyms to serializing and deserializing trees.
As a reference, the following code represents the typical Node
type of a binary tree:
class BinaryNode<Element: Comparable> {
var data: Element
var leftChild: BinaryNode?
var rightChild: BinaryNode?
// ... (rest of the implementation)
}
As mentioned before, there are different ways to do encoding. For no particular reason, you'll opt for the following rules:
- The result of the encoding will be a
String
object. - You'll encode using pre-order traversal.
Here's an example of this operation in code:
extension BinaryNode {
// 1
fileprivate var separator: String { return "," }
fileprivate var nilNode: String { return "X" }
// 2
var encodedString: String {
var str = ""
preOrderTraversal { data in
if let data = data {
let string = String(describing: data)
str.append(string)
} else {
str.append(nilNode)
}
str.append(separator)
}
return str
}
// 3
func preOrderTraversal(visit: (Element?) throws -> ()) rethrows {
try visit(data)
if let leftChild = leftChild {
try leftChild.preOrderTraversal(visit: visit)
} else {
try visit(nil)
}
if let rightChild = rightChild {
try rightChild.preOrderTraversal(visit: visit)
} else {
try visit(nil)
}
}
}
Here's a high level overview of the above code:
-
separator
is a way to distinguish the nodes in a string. To illustrate its importance, consider the following encoded string "banana". How did the tree structure look like before encoding? Without theseparator
, you can't tell. -
encodedString
is the result of the encoding process. Returns a string representation of the tree. For example: "ba,nana,nil" represents a tree with two nodes - "ba" and "nana" - in pre-order format. -
It is interesting to note that this pre-order traversal implementation also emits
nil
values in place of absent children.
Your decoding strategy is the exact opposite of your encoding strategy. You'll take an encoded string, and turn it back into your binary tree.
Your encoding strategy followed the following rules:
- The result of the encoding will be a
String
object. - You'll encode using pre-order traversal.
The implementation also added a few important details:
- node values are separated by
,
nil
children are denoted by thenil
string
These details will shape your decode
operation. Here's a possible implementation:
extension BinaryNode {
// 1
public static func decode(from str: String) -> AVLNode<String>? {
let components = encoded.lazy.split(separator: separator).reversed().map(String.init)
return decode(from: components)
}
public static func decode(from array: inout [String])
-> AVLNode<String>? {
guard !array.isEmpty else { return nil }
let value = array.removeLast()
guard value != "\(nilNode)" else { return nil }
let node = AVLNode<String>(value: value)
node.leftChild = decode(from: &array)
node.rightChild = decode(from: &array)
return node
}
}
Here's a high level overview of the above code:
-
Takes a
String
, and usessplit
to partition the contents ofstring
into an array based on theseparator
defined in the encoding step. The result is firstreversed
, and then mapped to aString
. Thereverse
step is an optimization for the next function, allowing us to usearray.removeLast()
instead ofarray.removeFirst()
. -
Using an array as a stack, you recursively decode each node. The array keeps track of sequence of nodes and progress.