Статьи

Алгоритм недели: балансировка бинарного дерева поиска

Вступление

Бинарное дерево поиска является очень полезной структурой данных, где поиск может быть значительно быстрее , чем при поиске в связанном списке. Однако в некоторых случаях поиск в двоичном дереве может быть таким же медленным, как поиск в связанном списке, и это в основном зависит от входной последовательности. В самом деле, в случае сортировки входных данных двоичное дерево будет очень похоже на связанный список, и поиск будет медленным.

Вставка в двоичное дерево поиска

Бинарное дерево поиска может сильно походить на связанные списки, если вход почти отсортирован!

Чтобы преодолеть это, мы должны немного изменить структуру данных, чтобы оставаться хорошо сбалансированными. Интуитивно понятно, что процесс поиска будет лучше, если дерево хорошо разветвлено. Это когда поиск предмета станет быстрее с минимальными усилиями.

Сбалансированное дерево

Поиск в сбалансированном дереве значительно быстрее, чем поиск в несбалансированном дереве!

Поскольку мы знаем, как построить двоичное дерево поиска, остается только сохранить его сбалансированным. Очевидно, нам нужно будет перебалансировать дерево при каждой вставке и удалении, что усложнит поддержание этой структуры данных по сравнению с несбалансированными деревьями поиска, но поиск в ней будет значительно быстрее.

обзор

Чтобы сбалансировать дерево, мы можем использовать очень простой и интуитивно понятный подход. Сначала давайте рассмотрим одно несбалансированное дерево.

Сбалансированный против Несбалансированный

По сравнению с сбалансированным деревом справа от изображения выше с теми же элементами, мы видим, что корень приблизительно равен его среднему элементу. Т.е. 4 является средним элементом последовательности [1,2,3,4,5,6,7]!

Если мы посмотрим на последовательность [2 3 4], ясно, что при построении бинарного дерева он будет выглядеть как связанный список. Однако, если мы выберем средний элемент для корня — мы легко построим сбалансированное дерево. Так что единственное, что нужно сделать, это вытащить средний элемент из списка.

Теперь мы видим, что создание сбалансированного двоичного дерева из отсортированного связанного списка не так уж сложно. С другой стороны, как я уже говорил выше, на каждой вставке мы должны балансировать дерево. Вы можете думать о дереве из значений [1,2,3,4,5] и о том же дереве после вставки [44,45,46,47,48]. Очевидно, что корень полученного дерева больше не будет 3.

Таким образом, нам необходимо осуществить ребалансировку в трех основных операциях. Сначала нам нужно создать связанный список из сбалансированного двоичного дерева. На втором месте нам нужно будет найти средний элемент, а на третьем — снова построить сбалансированное дерево поиска.

Надеемся, что первые две задачи легко реализовать, потому что составить отсортированный список из двоичного дерева поиска очень просто. Нам нужно просто пройти сквозь дерево слева направо и направо рекурсивно. Поскольку более мелкие элементы находятся в левом поддереве, а более крупные элементы находятся справа, мы уверены, что результирующий список будет отсортирован. Тогда найти средний элемент так же просто, как найти средний индекс массива, который знает его длину.

Балансировка Оптимизация

Конечно, главная проблема перебалансировки дерева при каждой вставке / удалении состоит в том, что эти операции будут медленными, и рано или поздно у нас будут проблемы. Это может произойти, если мы часто меняем структуру данных. Вот почему мы должны подумать об оптимизации.

Обычно мы вставляем и перебалансируем каждый шаг, что медленно. С другой стороны, мы можем сделать массовую вставку, на время забыв о перебалансировке. Только после того, как вставки сделаны, мы можем перебалансировать все дерево.

Массовая вставка только с одним балансом

Выполнение массовой вставки / удаления и только одна балансировка сделают структуру данных быстрее!

Тот же подход мы можем использовать с массовым удалением. Мы можем просто установить в NIL элементы, которые хотим удалить, но мы можем хранить их в памяти некоторое время. Таким образом, поиск останется относительно быстрым без изменения баланса дерева. Однако этот подход можно использовать осторожно, потому что мы будем хранить некоторые данные в памяти, фактически не используя их.

Массовое удаление

Мы можем обнулять элементы без фактического удаления указателей (ссылок) и структуры дерева!

Реализация

Реализация сбалансированных двоичных деревьев сложнее, чем просто реализация двоичных деревьев поиска. Вот пример в PHP .

class Node
{
	protected   $_parent = null;
	protected   $_left = null;
	protected   $_right = null;
	protected   $_key;
    protected   $_data = null;
 
    /**
     * @param int $key
     * @param mixed $data 
     */
	public function __construct($key, $data)
	{
		$this->_key = $key;
        $this->_data = $data;
	}
 
    /**
     * Empty the node by keeping up the key, but
     * setting up the data to NULL 
     */
    public function doEmpty() 
    {
        $this->_data = null;
    }
 
    /**
     * Print the key
     * 
     * @return string
     */
	public function __toString()
	{
		return 'First name: ' . $this->_data['f_name']
                . '<br />'
                . 'Last name: ' . $this->_data['l_name']
                . '<br />' 
                . 'Birthday: ' . $this->_data['b_day'];
	}
 
    public function &getParent() { return $this->_parent; }
    public function setParent($parent) { $this->_parent = $parent; }
 
    public function &getLeft() { return $this->_left; }
    public function setLeft($left) { $this->_left = $left; }
 
    public function &getRight() { return $this->_right; }
    public function setRight($right) { $this->_right = $right; }
 
    public function &getKey() { return $this->_key; }
    public function setKey($key) { $this->_key = $key; }
 
    public function &getData() { return $this->_data; }
    public function setData($data) { $this->_data = $data; }
}
 
class BalancedBinaryTree
{
    /**
     * Reference to the root tree
     * 
     * @var Node 
     */
	protected $_root = null;
 
    /**
     * @param type $new
     * @param type $node
     * @return type 
     */
	protected function _insert($new, &$root)
	{
        // in case the tree is empty
        // make the new node the root of
        // the tree
		if ($root == null) {
			$root = $new;
			return;
		}
 
		if ($new->getKey() <= $root->getKey()) {
			if ($root->getLeft() == null) {
				$root->setLeft($new);
				$new->setParent($root);
			} else {
				$this->_insert($new, $root->getLeft());
			}
		} else {
			if ($root->getRight() == null) {
				$root->setRight($new);
				$new->setParent($root);
			} else {
				$this->_insert($new, $root->getRight());
			}
		}		
	}
 
    /**
     * FALSE on not found
     * 
     * @param string $firstName
     * @param BalancedBinaryTree $tree
     * @return boolean 
     */
	protected function _search($firstName, &$tree)
	{
        if ($tree == null) {
            return FALSE;
        }
 
        $data = $tree->getData();
 
        if ($firstName == $data['f_name']) {
			return $tree;
		}
 
        // search the left sub-tree
        return $this->_search($firstName, $tree->getLeft())
                . $this->_search($firstName, $tree->getRight());
	}
 
    /**
     *
     * @param int $key
     * @param Node $tree
     * @return FALSE or Node 
     */
    protected function _searchByKey($key, &$tree)
    {
        if ($tree == null) {
            return FALSE;
        }
 
        if ($tree->getKey() == $key) {
            return $tree;
        } else if ($tree->getKey() > $key) {
            return $this->_searchByKey($key, $tree->getLeft());
        } else {
            return $this->_searchByKey($key, $tree->getRight());
        }
    }
 
    /**
     * Returns a list out of the tree by emptying the tree. 
     * In other way the tree and the list will allocate memory
     * 
     * @param BalancedBinaryTree $tree 
     */
    protected function _leftRootRight($tree)
    {
        if ($tree == null) {
            return array();
        }
 
        return array_merge(
                $this->_leftRootRight($tree->getLeft()),
                array(array('key' => $tree->getKey(), 'data' => $tree->getData())),
                $this->_leftRootRight($tree->getRight()));
    }
 
    public function _balance($list)
    {
        if (empty($list)) {
            return;
        }
 
        // split the list
        $chunks = array_chunk($list, ceil(count($list) / 2));
        $mid = array_pop($chunks[0]);
 
        $node = new Node($mid['key'], $mid['data']);
        $this->insert($node);
 
        $this->_balance($chunks[0]);
        if (isset($chunks[1]))
            $this->_balance($chunks[1]);
    }
 
    /**
     * Balance a binary search tree 
     */
    public function balance()
    {
        $list = array();
        // make a list out of the tree
        $list = $this->_leftRootRight($this->_root);
 
        // find the medium! Because the list is ordered
        // we can find the middle element in various ways
        $chunks = array_chunk($list, ceil(count($list) / 2));
        $mid = array_pop($chunks[0]);
 
        // empty the tree
        $this->_root = null;
 
        // inser the root
        $node = new Node($mid['key'], $mid['data']);
        $this->insert($node);
 
        $this->_balance($chunks[0]);
        $this->_balance($chunks[1]);
    }
 
    /**
     * Insert a new item into the tree
     * 
     * @param type $node 
     */
	public function insert($newNode)
	{
		$this->_insert($newNode, $this->_root);
	}
 
    /**
     * Search by item key
     * 
     * @param int $key
     * @return Node or FALSE
     */
    public function searchByKey($key)
    {
        return $this->_searchByKey($key, $this->_root);
    }
 
    /**
     * @param BalancedBinary $tree
     * @return string 
     */
    protected function _print($tree)
    {
        if ($tree == null) { return ''; }
 
        return $this->_print($tree->getLeft()) . ' ' 
                . $tree->getKey() . ' ' 
                . $this->_print($tree->getRight());
    }
 
    /**
     * Print the tree from left through the root and the right 
     */
    public function __toString()
    {
        if ($this->_root == null) {
            return 'The tree is empty!';
        }
 
        return $this->_print($this->_root->getLeft()) . ' '
                . $this->_root->getKey() . ' '
                . $this->_print($this->_root->getRight());
    }
}
 
$a = new Node(90, array(
    'f_name' => 'W.A.',
    'l_name' => 'Mozart',
    'b_day' => '1756-01-27',
));
 
$b = new Node(100, array(
    'f_name' => 'John',
    'l_name' => 'Smith',
    'b_day' => '23.05.2039',
));
 
$c = new Node(80, array(
    'f_name' => 'Sarah',
    'l_name' => 'Johnnes',
    'b_day' => 'tomorrow',
));
 
$d = new Node(60, array(
    'f_name' => 'Ludwig Van',
    'l_name' => 'Beethoven',
    'b_day' => '1770-12-17',
));
 
$e = new Node(70, array(
    'f_name' => 'Barbara',
    'l_name' => 'Stefanel',
    'b_day' => 'today',
));
 
$t = new BalancedBinaryTree();
 
$t->insert($a);
$t->insert($b);
$t->insert($c);
$t->insert($d);
$t->insert($e);
 
echo $t;
 
echo $t->searchByKey(70);
 
$t->balance();
 
echo $t->searchByKey(70);

Сложность поиска

По сравнению с несбалансированными деревьями двоичного поиска мы уверены, что поиск в сбалансированных деревьях достаточно быстрый. Максимальная высота дерева — log (n), поэтому поиск в худшем случае — O (log (n)) .

BST Chart

По сравнению с поиском в связанных списках за O (n) время поиска в сбалансированном двоичном дереве равно O (log (n)) в худшем случае!

заявка

Поиск в сбалансированном бинарном дереве быстрый. Что еще более важно, мы уверены, что в худшем случае поиск будет O (log (n)). Единственная проблема заключается в том, что поддержание сбалансированного дерева — это медленная операция, которая потребляет слишком много ресурсов и должна выполняться аккуратно.

Похожие сообщения:

  1. Компьютерные алгоритмы: двоичное дерево поиска
  2. Построить отсортированный PHP связанный список
  3. Компьютерные алгоритмы: бинарный поиск