hello-algo/en/docs/chapter_tree/array_representation_of_tree.md
2024-05-06 05:27:10 +08:00

14 KiB

comments
true

7.3   Array representation of binary trees

Under the linked list representation, the storage unit of a binary tree is a node TreeNode, with nodes connected by pointers. The basic operations of binary trees under the linked list representation were introduced in the previous section.

So, can we use an array to represent a binary tree? The answer is yes.

7.3.1   Representing perfect binary trees

Let's analyze a simple case first. Given a perfect binary tree, we store all nodes in an array according to the order of level-order traversal, where each node corresponds to a unique array index.

Based on the characteristics of level-order traversal, we can deduce a "mapping formula" between the index of a parent node and its children: If a node's index is i, then the index of its left child is 2i + 1 and the right child is $2i + 2$. Figure 7-12 shows the mapping relationship between the indices of various nodes.

Array representation of a perfect binary tree{ class="animation-figure" }

Figure 7-12   Array representation of a perfect binary tree

The mapping formula plays a role similar to the node references (pointers) in linked lists. Given any node in the array, we can access its left (right) child node using the mapping formula.

7.3.2   Representing any binary tree

Perfect binary trees are a special case; there are often many None values in the middle levels of a binary tree. Since the sequence of level-order traversal does not include these None values, we cannot solely rely on this sequence to deduce the number and distribution of None values. This means that multiple binary tree structures can match the same level-order traversal sequence.

As shown in Figure 7-13, given a non-perfect binary tree, the above method of array representation fails.

Level-order traversal sequence corresponds to multiple binary tree possibilities{ class="animation-figure" }

Figure 7-13   Level-order traversal sequence corresponds to multiple binary tree possibilities

To solve this problem, we can consider explicitly writing out all None values in the level-order traversal sequence. As shown in Figure 7-14, after this treatment, the level-order traversal sequence can uniquely represent a binary tree. Example code is as follows:

=== "Python"

```python title=""
# Array representation of a binary tree
# Using None to represent empty slots
tree = [1, 2, 3, 4, None, 6, 7, 8, 9, None, None, 12, None, None, 15]
```

=== "C++"

```cpp title=""
/* Array representation of a binary tree */
// Using the maximum integer value INT_MAX to mark empty slots
vector<int> tree = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};
```

=== "Java"

```java title=""
/* Array representation of a binary tree */
// Using the Integer wrapper class allows for using null to mark empty slots
Integer[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 };
```

=== "C#"

```csharp title=""
/* Array representation of a binary tree */
// Using nullable int (int?) allows for using null to mark empty slots
int?[] tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
```

=== "Go"

```go title=""
/* Array representation of a binary tree */
// Using an any type slice, allowing for nil to mark empty slots
tree := []any{1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15}
```

=== "Swift"

```swift title=""
/* Array representation of a binary tree */
// Using optional Int (Int?) allows for using nil to mark empty slots
let tree: [Int?] = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]
```

=== "JS"

```javascript title=""
/* Array representation of a binary tree */
// Using null to represent empty slots
let tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
```

=== "TS"

```typescript title=""
/* Array representation of a binary tree */
// Using null to represent empty slots
let tree: (number | null)[] = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
```

=== "Dart"

```dart title=""
/* Array representation of a binary tree */
// Using nullable int (int?) allows for using null to mark empty slots
List<int?> tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
```

=== "Rust"

```rust title=""
/* Array representation of a binary tree */
// Using None to mark empty slots
let tree = [Some(1), Some(2), Some(3), Some(4), None, Some(6), Some(7), Some(8), Some(9), None, None, Some(12), None, None, Some(15)];
```

=== "C"

```c title=""
/* Array representation of a binary tree */
// Using the maximum int value to mark empty slots, therefore, node values must not be INT_MAX
int tree[] = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};
```

=== "Kotlin"

```kotlin title=""
/* Array representation of a binary tree */
// Using null to represent empty slots
val tree = mutableListOf( 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 )
```

=== "Ruby"

```ruby title=""

```

=== "Zig"

```zig title=""

```

Array representation of any type of binary tree{ class="animation-figure" }

Figure 7-14   Array representation of any type of binary tree

It's worth noting that complete binary trees are very suitable for array representation. Recalling the definition of a complete binary tree, None appears only at the bottom level and towards the right, meaning all None values definitely appear at the end of the level-order traversal sequence.

This means that when using an array to represent a complete binary tree, it's possible to omit storing all None values, which is very convenient. Figure 7-15 gives an example.

Array representation of a complete binary tree{ class="animation-figure" }

Figure 7-15   Array representation of a complete binary tree

The following code implements a binary tree based on array representation, including the following operations:

  • Given a node, obtain its value, left (right) child node, and parent node.
  • Obtain the preorder, inorder, postorder, and level-order traversal sequences.

=== "Python"

```python title="array_binary_tree.py"
class ArrayBinaryTree:
    """Array-based binary tree class"""

    def __init__(self, arr: list[int | None]):
        """Constructor"""
        self._tree = list(arr)

    def size(self):
        """List capacity"""
        return len(self._tree)

    def val(self, i: int) -> int | None:
        """Get the value of the node at index i"""
        # If the index is out of bounds, return None, representing a vacancy
        if i < 0 or i >= self.size():
            return None
        return self._tree[i]

    def left(self, i: int) -> int | None:
        """Get the index of the left child of the node at index i"""
        return 2 * i + 1

    def right(self, i: int) -> int | None:
        """Get the index of the right child of the node at index i"""
        return 2 * i + 2

    def parent(self, i: int) -> int | None:
        """Get the index of the parent of the node at index i"""
        return (i - 1) // 2

    def level_order(self) -> list[int]:
        """Level-order traversal"""
        self.res = []
        # Traverse array
        for i in range(self.size()):
            if self.val(i) is not None:
                self.res.append(self.val(i))
        return self.res

    def dfs(self, i: int, order: str):
        """Depth-first traversal"""
        if self.val(i) is None:
            return
        # Pre-order traversal
        if order == "pre":
            self.res.append(self.val(i))
        self.dfs(self.left(i), order)
        # In-order traversal
        if order == "in":
            self.res.append(self.val(i))
        self.dfs(self.right(i), order)
        # Post-order traversal
        if order == "post":
            self.res.append(self.val(i))

    def pre_order(self) -> list[int]:
        """Pre-order traversal"""
        self.res = []
        self.dfs(0, order="pre")
        return self.res

    def in_order(self) -> list[int]:
        """In-order traversal"""
        self.res = []
        self.dfs(0, order="in")
        return self.res

    def post_order(self) -> list[int]:
        """Post-order traversal"""
        self.res = []
        self.dfs(0, order="post")
        return self.res
```

=== "C++"

```cpp title="array_binary_tree.cpp"
[class]{ArrayBinaryTree}-[func]{}
```

=== "Java"

```java title="array_binary_tree.java"
/* Array-based binary tree class */
class ArrayBinaryTree {
    private List<Integer> tree;

    /* Constructor */
    public ArrayBinaryTree(List<Integer> arr) {
        tree = new ArrayList<>(arr);
    }

    /* List capacity */
    public int size() {
        return tree.size();
    }

    /* Get the value of the node at index i */
    public Integer val(int i) {
        // If the index is out of bounds, return null, representing an empty spot
        if (i < 0 || i >= size())
            return null;
        return tree.get(i);
    }

    /* Get the index of the left child of the node at index i */
    public Integer left(int i) {
        return 2 * i + 1;
    }

    /* Get the index of the right child of the node at index i */
    public Integer right(int i) {
        return 2 * i + 2;
    }

    /* Get the index of the parent of the node at index i */
    public Integer parent(int i) {
        return (i - 1) / 2;
    }

    /* Level-order traversal */
    public List<Integer> levelOrder() {
        List<Integer> res = new ArrayList<>();
        // Traverse array
        for (int i = 0; i < size(); i++) {
            if (val(i) != null)
                res.add(val(i));
        }
        return res;
    }

    /* Depth-first traversal */
    private void dfs(Integer i, String order, List<Integer> res) {
        // If it is an empty spot, return
        if (val(i) == null)
            return;
        // Pre-order traversal
        if ("pre".equals(order))
            res.add(val(i));
        dfs(left(i), order, res);
        // In-order traversal
        if ("in".equals(order))
            res.add(val(i));
        dfs(right(i), order, res);
        // Post-order traversal
        if ("post".equals(order))
            res.add(val(i));
    }

    /* Pre-order traversal */
    public List<Integer> preOrder() {
        List<Integer> res = new ArrayList<>();
        dfs(0, "pre", res);
        return res;
    }

    /* In-order traversal */
    public List<Integer> inOrder() {
        List<Integer> res = new ArrayList<>();
        dfs(0, "in", res);
        return res;
    }

    /* Post-order traversal */
    public List<Integer> postOrder() {
        List<Integer> res = new ArrayList<>();
        dfs(0, "post", res);
        return res;
    }
}
```

=== "C#"

```csharp title="array_binary_tree.cs"
[class]{ArrayBinaryTree}-[func]{}
```

=== "Go"

```go title="array_binary_tree.go"
[class]{arrayBinaryTree}-[func]{}
```

=== "Swift"

```swift title="array_binary_tree.swift"
[class]{ArrayBinaryTree}-[func]{}
```

=== "JS"

```javascript title="array_binary_tree.js"
[class]{ArrayBinaryTree}-[func]{}
```

=== "TS"

```typescript title="array_binary_tree.ts"
[class]{ArrayBinaryTree}-[func]{}
```

=== "Dart"

```dart title="array_binary_tree.dart"
[class]{ArrayBinaryTree}-[func]{}
```

=== "Rust"

```rust title="array_binary_tree.rs"
[class]{ArrayBinaryTree}-[func]{}
```

=== "C"

```c title="array_binary_tree.c"
[class]{ArrayBinaryTree}-[func]{}
```

=== "Kotlin"

```kotlin title="array_binary_tree.kt"
[class]{ArrayBinaryTree}-[func]{}
```

=== "Ruby"

```ruby title="array_binary_tree.rb"
[class]{ArrayBinaryTree}-[func]{}
```

=== "Zig"

```zig title="array_binary_tree.zig"
[class]{ArrayBinaryTree}-[func]{}
```

7.3.3   Advantages and limitations

The array representation of binary trees has the following advantages:

  • Arrays are stored in contiguous memory spaces, which is cache-friendly and allows for faster access and traversal.
  • It does not require storing pointers, which saves space.
  • It allows random access to nodes.

However, the array representation also has some limitations:

  • Array storage requires contiguous memory space, so it is not suitable for storing trees with a large amount of data.
  • Adding or deleting nodes requires array insertion and deletion operations, which are less efficient.
  • When there are many None values in the binary tree, the proportion of node data contained in the array is low, leading to lower space utilization.