Перейти к содержанию

K-d-дерево

Материал из Мегавики — свободной энциклопедии
K-мерное дерево
Тип Многомерное дерево Двоичное дерево поиска
Год изобретения 1975
Автор Йон Бентли
Сложность в О-символике
В среднем В худшем случае
Расход памяти O(n) O(n)
Поиск O(log n) O(n)
Вставка O(log n) O(n)
Удаление O(log n) O(n)
Трёхмерное дерево.

k-d-дерево (англ. k-d tree, сокращение от k-мерное дерево) — это структура данных с разбиением пространства для упорядочивания точек в k-мерном пространстве. k-d-деревья используются для некоторых приложений, таких как поиск в многомерном пространстве ключей (поиск диапазона и поиск ближайшего соседа). k-d-деревья — особый вид двоичных деревьев поиска.

Математическое описание[править]

K-мерное дерево — это несбалансированное дерево поиска для хранения точек из k. Оно предлагает похожую на R-дерево возможность поиска в заданном диапазоне ключей. В ущерб простоте запросов, требования к памяти O(kn) вместо O((log(n))k1).

Существуют однородные и неоднородные k-d-деревья. У однородных k-d-деревьев каждый узел хранит запись. При неоднородном варианте внутренние узлы содержат только ключи, листья содержат ссылки на записи.

В неоднородном k-d-дереве Hi(t)=(x1,x2,,xi1,t,xi+1,,xk) при 1ik параллельно оси (k1)-мерной гиперплоскости в точке t. Для корня нужно разделить точки через гиперплоскость H1(t) на два по возможности одинаково больших множества точек и записать t в корень, слева от этого сохраняются все точки у которых x1<t, справа те у которых x1>t. Для левого поддерева нужно разделить точки опять на новую «разделенную плоскость» H2(t), а t сохраняется во внутреннем узле. Слева от этого сохраняются все точки у которых x2<t. Это продолжается рекурсивно над всеми пространствами. Потом всё начинается снова с первого пространства до того, пока каждую точку можно будет ясно идентифицировать через гиперплоскость.

k-d-дерево можно построить за O(n(k+log(n))). Поиск диапазона может быть выполнен за O(n11k+a), при этом a обозначает размер ответа. Требование к памяти для самого дерева ограничено O(kn).

Операции с k-d-деревьями[править]

Структура[править]

Структура дерева, описанная на языке C++:

constexpr int N=10; // количество пространств ключей

struct Item {   // структура элемента
  int key[N];   // массив ключей определяющих элемент
  char *info;   // информация элемента
};

struct Node {   // структура узла дерева
  Item i;       // элемент
  Node *left;   // левое  поддерево
  Node *right;  // правое поддерево
}

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

Анализ поиска элемента

Очевидно, что минимальное количество просмотренных элементов равно 1, а максимальное количество просмотренных элементов — O(h), где h — это высота дерева. Остаётся посчитать среднее количество просмотренных элементов An.

[x0,x1,x2,...,xn] — заданный элемент.

Рассмотрим случай h=3. Найденными элементами могут быть:

find(t1):[(x0=t1)];A=1.
find(t2):[(x0<t1)(x0=t2)];A=2.
find(t3):[(x0>t1)(x0=t3)];A=2.
find(t4):[(x0<t1)(x0<t2)(x0=t4)];A=3.
find(t5):[(x0<X1)(x0>t2)(x0=t5)];A=3.
find(t6):[(x0<t1)(x0<t3)(x0=t6)];A=3.
find(t7):[(x0<t1)(x0>t3)(x0=t7)];A=3.

и так для каждого пространства ключей. При этом средняя длина поиска в одном пространстве составляет:

A=1+2+2+3+3+3+37=1772,4.

Средняя величина считается по формуле: An=k=1nkpn,k

Остаётся найти вероятность pn,k. Она равна pn,k=pA,kpn, где pA,k — число случаев, когда A=k и pn — общее число случаев. Не сложно догадаться, что pn,k=2k12n1.

Подставляем это в формулу для средней величины:

An=k=1nkpn,k=k=1nk2k12n1=12n1k=1nk2k1=
=12n1k+1=1n(k+1)2k=12n1(k+1=1nk2k+k+1=1n2k)=
=12n1(k=1nk2k+k=1n2k2nn2n)=
=12n1(n2n+2(n+1)2n+1+22n+231n2n)=2n(n1)+12n1,

то есть Ah=2h(h1)+12h1, где h — высота дерева.

Если перейти от высоты дерева к количеству элементов, то:

An=O(2h(h1)+12h1)=O(h2h2h11)=O(log(nN+1)2log(nN+1)2log(nN+1)11)=O(log(nN+1)n+Nn1)=

=O(log(nN+1)n+Nn1), где N — количество элементов в узле.

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

Добавление элементов[править]

Добавление элементов происходит точно так же, как и в обычном двоичном дереве поиска, с той лишь разницей, что каждый уровень дерева будет определяться ещё и пространством к которому он относится.

Алгоритм продвижения по дереву:

for (int i = 0; tree; i++) // i - это номер пространства
    if (tree->x[i] < tree->t)  // t - медиана
        tree = tree->left;     // переходим в левое поддерево
    else
        tree = tree->right;    // переходим в правое поддерево

Добавление выполняется за O(h), где h — высота дерева.

Удаление элементов[править]

При удалении элементов дерева может возникнуть несколько ситуаций:

  • Удаление листа дерева — довольно простое удаление, когда удаляется один узел и указатель узла-предка просто обнуляется.
  • Удаление узла дерева (не листа) — очень сложная процедура, при которой приходится перестраивать всё поддерево для данного узла.

Иногда процесс удаления узла решается модификациями k-d-дерева. К примеру, если у нас в узле содержится массив элементов, то при удалении всего массива узел дерева остаётся, но новые элементы туда больше не записываются.

Поиск диапазона элементов[править]

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

Алгоритм
Z – узел дерева
[(x_0_min, x_1_min, x_2_min,..., x_n_min),(x_0_max, x_1_max, x_2_max,..., x_n_max)] - заданный диапазон

Функция Array(Node *&Z){
If ([x_0_min, x_1_min, x_2_min,..., x_n_min]<Z){
		Z=Z->left; // левое поддерево
}
else
If ([x_0_max, x_1_max, x_2_max,..., x_n_max]>Z){
			Z=Z->right; // правое поддерево
}
		Else{ // просмотреть оба поддерева
			Array(Z->right); // запустить функцию для правого поддерева
			Z=Z->left; // просмотреть левое поддерево
		}
}
Анализ

Очевидно, что минимальное количество просмотренных элементов это O(h), где h — высота дерева. Так же очевидно, что максимальное количество просмотренных элементов это O(2h1), то есть просмотр всех элементов дерева. Остаётся посчитать среднее количество просмотренных элементов An.

[(x0min,x1min,x2min,...,xnmin),(x0max,x1max,x2max,...,xnmax)] — заданный диапазон.

Оригинальная статья про k-d-деревья даёт такую характеристику: An=O(hlog(h)) для фиксированного диапазона.

Если перейти от высоты дерева к количеству элементов, то это будет: An=O(log(log(n1))log(n1))

Поиск ближайшего соседа[править]

Поиск ближайшего элемента разделяется на две подзадачи: определение возможного ближайшего элемента и поиск ближайших элементов в заданном диапазоне.

Дано дерево tree. Мы спускаемся по дереву к его листьям по условию treex[i](<,>=)treet и определяем вероятный ближайший элемент по условию lmin=((x0x[i]0)2+(x1x[i]1)2+...+(xnx[i]n)2). После этого от корня дерева запускается алгоритм поиска ближайшего элемента в заданном диапазоне, который определяется радиусом R=lmin=((x0x[i]0)2+(x1x[i]1)2+...+(xnx[i]n)2).

Радиус поиска корректируется при нахождении более близкого элемента.

Алгоритм
Z – корень дерева
List – список для найденных ближайших элементов
[x_0,x_1,x_2...,x_n] - координаты всех измерений нашего элемента, для которого и ищутся ближайшие
Len – минимальная длина
CHILDREN - максимальное число детей у каждого элемента

Функция Maybe_Near(Node *&Z) { // поиск ближайшего возможного элемента
	while (Z) {
        for (i=0;i<N;i++) { // проверка элементов в узле
            len_cur = sqrt((x_0-x[i]_0)^2 + (x_1-x[i]_1)^2 + ... + (x_n-x[i]_n)^2); // длина текущего элемента
            if (Len > длины текущего элемента) {
	             Len = len_cur; // установление новой длины
	             Delete(List); // очистка списка
	             Add(List); // добавить новый элемент в список
            } else if(длины равны) {
					Add(List); // добавить новый элемент в список
            }
			if ((x_0 == x[i]_0) && (x_1 == x[i]_1) && ... && (x_n == x[i]_n)) {
				return 1;
            }
		}

		if ([x_0,x_1,x_2...,x_n] < Z)
			Z = Z->left; // левое поддерево
        if ([x_0,x_1,x_2...,x_n] > Z)
			Z = Z->right; // правое поддерево			
    }
}

Функция Near(Node *&Z) { // рекурсивный поиск ближайшего элемента в заданном диапазоне
    if (!Z) {
        return List;
    }
    
    len_cur = sqrt((x_0-x[i]_0)^2 + (x_1-x[i]_1)^2 + ... + (x_n-x[i]_n)^2); // расстояние от нашей точки до текущей
    if (len_cur < Len) { // нашли длину меньше минимальной
        Len = len_cur; // установление новой минимальной длины
        Delete(List); // очистка списка - ведь все найденные до этого элементы находятся дальше, чем текущий
        Add(List, Z); // добавить текущий элемент в список
    } else if (len_cur == Len) { // длина равна минимальной
        Add(List, Z); // просто добавить новый элемент в список
    }
    
    for (i = 0; i < CHILDREN; i++) { // для всех дочерних элементов выполним то же самое
        Near(Z->children[i]); // просмотреть все поддеревья
    }
}
Анализ

Очевидно, что минимальное количество просмотренных элементов это O(h), где h — это высота дерева. Так же очевидно, что максимальное количество просмотренных элементов это O(2h1), то есть просмотр всех узлов. Остаётся посчитать среднее количество просмотренных элементов.

[(x0,x1,x2,...,xn)] — заданный элемент, относительно которого нужно найти ближайший. Эта задача разделяется на две подзадачи: нахождение ближайшего элемента в узле и нахождение ближайшего элемента в заданном диапазоне. Для решения первой подзадачи потребуется один спуск по дереву, то есть O(h).

Для второй подзадачи, как мы уже вычислили, поиск элементов в заданном диапазоне выполняется за O(hlog(h)). Чтобы узнать среднее, достаточно просто сложить эти две величины:

=O(h)+O(hlog(h))=O(h)(O(log(h))+1)).

См. также[править]

Примечания[править]

Ссылки[править]