12 KiB
Представление двоичного дерева с помощью массива
При представлении в виде списка единицей хранения двоичного дерева является узел TreeNode, а узлы соединяются между собой указателями. В предыдущем разделе были рассмотрены основные операции с двоичным деревом, представленным в виде списка.
Можно ли представить двоичное дерево с помощью массива? Ответ положительный.
Представление идеального двоичного дерева
Сначала рассмотрим простой пример. Если дано идеальное двоичное дерево и все его узлы хранятся в массиве в порядке обхода по уровням, то каждому узлу соответствует уникальный индекс массива.
На основе свойств обхода по уровням можно вывести формулу соответствия между индексами родительского и дочерних узлов: если индекс узла равен i, то индекс его левого дочернего узла равен 2i + 1, а правого -- $2i + 2$. На рис. 7.12 показаны отношения соответствия между индексами узлов.
Формула соответствия играет роль, аналогичную ссылкам (указателям) в списке. Имея любой узел в массиве, можно с помощью формулы получить доступ к его левому и правому дочерним узлам.
Представление произвольного двоичного дерева
Идеальное двоичное дерево является частным случаем. Обычно на средних уровнях двоичного дерева присутствует много пустых значений None. Но последовательность обхода по уровням не содержит этих None, поэтому невозможно по этой последовательности определить количество и расположение пустых значений. Это означает, что существует множество структур двоичных деревьев, соответствующих данной последовательности обхода по уровням.
Для такого неидеального двоичного дерева вышеописанный метод представления с помощью массива уже не работает, см. рис. 7.13.
Для решения этой проблемы можно явно записать все значения None в последовательности обхода по уровням. После такой обработки последовательность обхода по уровням уже может однозначно представлять двоичное дерево, как показано на рис. 7.14. Ниже приведен пример кода.
=== "Python"
```python title=""
# Представление двоичного дерева с помощью массива
# Использование None для обозначения пустых мест
tree = [1, 2, 3, 4, None, 6, 7, 8, 9, None, None, 12, None, None, 15]
```
=== "C++"
```cpp title=""
/* Представление двоичного дерева с помощью массива */
// Использование INT_MAX для обозначения пустых мест
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=""
/* Представление двоичного дерева с помощью массива */
// Использование обертки Integer для типа int, чтобы можно было использовать null для обозначения пустых мест
Integer[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 };
```
=== "C#"
```csharp title=""
/* Представление двоичного дерева с помощью массива */
// Использование nullable типа int?, чтобы можно было использовать null для обозначения пустых мест
int?[] tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
```
=== "Go"
```go title=""
/* Представление двоичного дерева с помощью массива */
// Использование типа any для среза, чтобы можно было использовать nil для обозначения пустых мест
tree := []any{1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15}
```
=== "Swift"
```swift title=""
/* Представление двоичного дерева с помощью массива */
// Использование nullable типа Int?, чтобы можно было использовать nil для обозначения пустых мест
let tree: [Int?] = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]
```
=== "JS"
```javascript title=""
/* Представление двоичного дерева с помощью массива */
// Использование null для обозначения пустых мест
let tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
```
=== "TS"
```typescript title=""
/* Представление двоичного дерева с помощью массива */
// Использование null для обозначения пустых мест
let tree: (number | null)[] = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
```
=== "Dart"
```dart title=""
/* Представление двоичного дерева с помощью массива */
// Использование nullable типа int?, чтобы можно было использовать null для обозначения пустых мест
List<int?> tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
```
=== "Rust"
```rust title=""
/* Представление двоичного дерева с помощью массива */
// Использование None для обозначения пустых мест
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=""
/* Представление двоичного дерева с помощью массива */
// Использование максимального значения int для обозначения пустых мест, поэтому значения узлов не могут быть равны 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=""
/* Представление двоичного дерева с помощью массива */
// Использование null для обозначения пустых мест
val tree = arrayOf( 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 )
```
=== "Ruby"
```ruby title=""
### Представление двоичного дерева с помощью массива ###
# Использование nil для обозначения пустых мест
tree = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]
```
Стоит отметить, что совершенное двоичное дерево очень удобно представлять с помощью массива. Вспоминая определение совершенного двоичного дерева, None появляются только на самом нижнем уровне и в правой части, поэтому все значения None обязательно находятся в конце последовательности обхода по уровням.
Это означает, что при использовании массива для представления совершенного двоичного дерева можно опустить хранение всех None, что очень удобно. На рис. 7.15 приведен пример такого представления.
В коде ниже реализуется двоичное дерево, основанное на представлении с помощью массива, включая следующие операции.
- Для заданного узла получение его значения, левого и правого дочернего узла, родительского узла.
- Получение последовательностей обхода в прямом, симметричном, обратном порядке и в порядке обхода по уровням.
[file]{array_binary_tree}-[class]{array_binary_tree}-[func]{}
Преимущества и ограничения
Представление двоичного дерева с помощью массива имеет следующие преимущества:
- Массив хранится в непрерывной области памяти, что хорошо для кеширования. Скорость доступа и обхода достаточно высока.
- Не требуется хранение указателей, что экономит пространство.
- Позволяет выполнять произвольный доступ к узлам.
Однако представление с помощью массива имеет и некоторые ограничения:
- Хранение в массиве требует непрерывной области памяти, поэтому не подходит для хранения деревьев с очень большим объемом данных.
- Добавление и удаление узлов требует выполнения операций вставки и удаления в массиве, которые менее эффективны.
- Когда в двоичном дереве содержится много значений
None, доля данных узлов в массиве низка, что приводит к низкой эффективности использования пространства.



