Список с пропусками — различия между версиями
(→Псевдокод) |
|||
Строка 1: | Строка 1: | ||
− | '''Список с пропусками''' (англ. ''skip list'') — поисковая структура данных, | + | '''Список с пропусками''' (англ. ''skip list'') — поисковая структура данных, реализующая интерфейс упорядоченного множества, позволяет производить операции поиска, добавления и удаления элемента в списке за достаточно малое время. |
Поиск элемента в списке производится за <tex>\Theta(\log(n))</tex>; добавление и удаление элемета происходит за то же время, что и поиск, однако эти операции могут замедлить поиск в структуре. | Поиск элемента в списке производится за <tex>\Theta(\log(n))</tex>; добавление и удаление элемета происходит за то же время, что и поиск, однако эти операции могут замедлить поиск в структуре. | ||
Строка 23: | Строка 23: | ||
# Переместиться на один уровень вниз и перейти к пункту 2. Если рассматриваемый элемент находится на нижнем уровне - выйти из поиска | # Переместиться на один уровень вниз и перейти к пункту 2. Если рассматриваемый элемент находится на нижнем уровне - выйти из поиска | ||
− | Пример поиска числа 8 в списке из описания: | + | Пример поиска числа <tex>8</tex> в списке из описания: |
[[Файл:SkipListSearch.png]] | [[Файл:SkipListSearch.png]] | ||
Строка 52: | Строка 52: | ||
Используя монетку с распределением отличным от <tex>\{\dfrac{1}{2}</tex>, <tex>\dfrac{1}{2}\}</tex>, можно влиять на количество элементов на верхних уровнях. Так, например, при использовании монеты с распределением <tex>\{p,q\}</tex>}, математическое ожидание количества элементов на уровне <tex>l</tex> равно <tex dpi=150>n q^l</tex>, каждый уровень будет составлять <tex>q</tex> от предыдущего; время поиска будет равно <tex>O(\dfrac{k}{q} + nq^k)</tex>. Соответственно при честной монетке и <tex>\log(n)</tex> уровней получаем оценку, полученную ранее. | Используя монетку с распределением отличным от <tex>\{\dfrac{1}{2}</tex>, <tex>\dfrac{1}{2}\}</tex>, можно влиять на количество элементов на верхних уровнях. Так, например, при использовании монеты с распределением <tex>\{p,q\}</tex>}, математическое ожидание количества элементов на уровне <tex>l</tex> равно <tex dpi=150>n q^l</tex>, каждый уровень будет составлять <tex>q</tex> от предыдущего; время поиска будет равно <tex>O(\dfrac{k}{q} + nq^k)</tex>. Соответственно при честной монетке и <tex>\log(n)</tex> уровней получаем оценку, полученную ранее. | ||
Для крайних распределений: | Для крайних распределений: | ||
− | * | + | * <tex>\{0, 1\}</tex> {{---}} <tex>O(k+n)</tex> |
− | * | + | * <tex>\{1, 0\}</tex> {{---}} <tex>\infty</tex> (если разрешить добавление новых уровней при проталкивании элемента после броска монетки; иначе <tex>O(n)</tex>) |
===Удаление элемента=== | ===Удаление элемента=== | ||
Строка 71: | Строка 71: | ||
Конструктор | Конструктор | ||
<code> | <code> | ||
− | skip_list (list l): | + | '''list''' skip_list ('''list''' l): |
− | list lvl = build_lvl(l) | + | '''list''' lvl = build_lvl(l) <font color=darkgreen>// Здесь происходит построение первого уровня</font> |
− | '''while''' | + | '''while''' lvl.size() > 2 |
− | lvl = build_lvl (lvl) | + | lvl = build_lvl (lvl) <font color=darkgreen>// Добавление следующих уровней; последний содержит два элемента</font> |
− | '''return''' t | + | '''return''' t |
− | list build_lvl (list lvl) // Отсеивание нечётных элементов | + | '''list''' build_lvl ('''list''' lvl) <font color=darkgreen>// Отсеивание нечётных элементов</font> |
− | list next_lvl | + | '''list''' next_lvl |
− | node i = lvl.head() | + | '''node''' i = lvl.head() <font color=darkgreen>// Перебор всех элементов lvl</font> |
− | while | + | '''while''' (i != ''null'') '''and''' (i != lvl.tail()) |
− | next_lvl.push_back(node(i.key, i)) | + | next_lvl.push_back(node(i.key, i)) <font color=darkgreen>// Добавление чётного элемента;</font> |
− | i = i.next.next | + | i = i.next.next <font color=darkgreen>// он содержит ключ и ссылку на предыдущий уровень</font> |
− | '''return''' next_lvl | + | '''return''' next_lvl |
</code> | </code> | ||
Поиск элемента по ключу: | Поиск элемента по ключу: | ||
<code> | <code> | ||
− | T find (list skip_list, K key) | + | '''T''' find ('''list''' skip_list, '''K''' key) |
− | node res | + | '''node''' res |
− | '''for''' (res = skip_list.head; res.ref != '' | + | '''for''' (res = skip_list.head; res.ref != ''null''; res = res.ref) |
− | // Cпускаемся на шаг вниз, если можем (п. 3) | + | <font color=darkgreen>// Cпускаемся на шаг вниз, если можем (п. 3)</font> |
− | '''while''' | + | '''while''' res.key <= key <font color=darkgreen>// Переходим к следующему элементу (п. 2)</font> |
− | res = res.next() | + | res = res.next() |
− | '''return''' res.data | + | '''return''' res.data |
</code> | </code> | ||
Вставка: | Вставка: | ||
<code> | <code> | ||
− | node insert (node i, K key, T data) | + | '''node''' insert ('''node''' i, '''K''' key, '''T''' data) |
− | '''while''' | + | '''while''' i.key <= key <font color=darkgreen>// Ищем подходящее место</font> |
− | i = i.next() | + | i = i.next() |
− | node tmp = ''' | + | '''node''' tmp = ''null'' <font color=darkgreen>// Для записи в поле down</font> |
− | '''if''' i.ref != '' | + | '''if''' i.ref != ''null'' <font color=darkgreen>// Мы не на нижнем уровне</font> |
− | tmp = insert (i.ref, key) | + | tmp = insert (i.ref, key) <font color=darkgreen>// Рекурсивный вызов на более низком уровне</font> |
− | '''if''' tmp == '' | + | '''if''' tmp == ''null'' <font color=darkgreen>// Проверка броска монетки</font> |
− | '''return''' '' | + | '''return''' ''null'' |
− | i.next = '''new''' node (i.next, tmp, data, key) | + | i.next = '''new''' node (i.next, tmp, data, key) <font color=darkgreen>//Непосредственно вставка</font> |
− | '''if''' random(0,1) > 0.5 // Бросок монетки | + | '''if''' random(0,1) > 0.5 <font color=darkgreen>// Бросок монетки</font> |
− | return i.next | + | return i.next <font color=darkgreen>// Нужно передать новый элемент для вставки выше</font> |
'''else''' | '''else''' | ||
− | return ''' | + | return ''null'' |
− | '''void''' insert (list skip_list, K key, T data) // Обёрточка | + | '''void''' insert ('''list''' skip_list, '''K''' key, '''T''' data) <font color=darkgreen>// Обёрточка</font> |
− | insert(skip_list.head, key, data) | + | insert(skip_list.head, key, data) |
</code> | </code> | ||
Удаление: | Удаление: | ||
<code> | <code> | ||
− | '''void''' erase (node i, K key) | + | '''void''' erase ('''node''' i, '''K''' key) |
− | '''if''' | + | '''if''' i == ''null'' |
− | '''return''' | + | '''return''' |
− | '''while''' | + | '''while''' i.key <= key <font color=darkgreen>// Ищем элемент</font> |
− | i = i.next() | + | i = i.next() |
− | erase(i.ref, key) | + | erase(i.ref, key) <font color=darkgreen>// Удаляем с нижних уровней</font> |
− | '''if''' | + | '''if''' i.key == key <font color=darkgreen>// Если ключ совпадает</font> |
− | '''delete'''(i) | + | '''delete'''(i) <font color=darkgreen>// удаляем и с этого уровня</font> |
− | '''void''' erase (list skip_list, K key) // Обёрточка | + | '''void''' erase ('''list''' skip_list, '''K''' key) <font color=darkgreen>// Обёрточка</font> |
− | erase(skip_list.head, key) | + | erase(skip_list.head, key) |
</code> | </code> | ||
− | + | ==Применение== | |
+ | * Базы данных | ||
+ | * Распределённые вычисления и p2p | ||
+ | * Масштабируемые параллельные приоритетные очереди и словари | ||
+ | В вычислительной геометрии широко применяются структуры на основе списка с пропусками. | ||
==См. также== | ==См. также== | ||
*[[Список]] | *[[Список]] | ||
*[[Рандомизированное бинарное дерево поиска]] | *[[Рандомизированное бинарное дерево поиска]] | ||
*[[Поисковые структуры данных]] | *[[Поисковые структуры данных]] | ||
+ | Структуры на основе списка с пропусками: | ||
+ | *[[Skip quadtree: определение, время работы]] | ||
+ | *[http://en.wikipedia.org/wiki/Skip_graph - Википедия — Skip graph(En)] | ||
==Источники информации== | ==Источники информации== | ||
*[http://habrahabr.ru/post/111913/ Хабрахабр — Списки с пропусками] | *[http://habrahabr.ru/post/111913/ Хабрахабр — Списки с пропусками] |
Версия 13:59, 8 июня 2015
Список с пропусками (англ. skip list) — поисковая структура данных, реализующая интерфейс упорядоченного множества, позволяет производить операции поиска, добавления и удаления элемента в списке за достаточно малое время.
Поиск элемента в списке производится за
; добавление и удаление элемета происходит за то же время, что и поиск, однако эти операции могут замедлить поиск в структуре.Такая производительность достигается за счёт добавления новых уровней. При этом нижним уровнем является исходный список, а каждый следующий уровень - список, содержащий часть элементов предыдущего уровня со ссылками на эти элементы.
Содержание
Построение
Список с пропусками строится на основе существующего односвязного отсортированного списка.
Добавив дополнительные уровни, каждый из которых представляет предыдущий уровень без нечетных элементов, мы получим возможность осуществлять поиск, вставку и удаление элементов подобно операциям с двоичным деревом поиска. Соответственно, асимптотика этих операций будет составлять
.Операции над структурой
Поиск элемента
Допустим, что в нашем списке с пропусками существуют
уровней, при этом первым уровнем ( ) будет исходный список.В таком случае алгоритм поиска в этой структуре будет представлять из себя следующие операции:
- Начинаем поиск элемента в верхнем списке ( ), рассмотрим первый элемент
- Переходить к следующему элементу списка, пока значение в следующей ячейке меньше или равно ключу
- Переместиться на один уровень вниз и перейти к пункту 2. Если рассматриваемый элемент находится на нижнем уровне - выйти из поиска
Пример поиска числа
в списке из описания:Рассмотрим время работы для списка с двумя уровнями. Тогда время работы алгоритма поиска будет зависеть от количества элементов на уровне
. Представим, что на этот уровень у нас случайным образом попало несколько элементов. Следовательно в худшем случае поиска мы получим следующую оценку на время работы:
Минимизируя, мы получаем, что
В итоге время за которое мы найдем элемент в списке с пропусками с двумя уровнями будет равняться:
Также можно убедиться, что список с пропусками, имеющий
уровней, будет лучше всего работать с элементами на уровне; время работы такого списка будет равно . Для уровней же время работы составитВставка элемента
Для вставки элемента в список с пропусками, нам необходимо выполнить следующие шаги:
- Найти с помощью алгоритма поиска позицию, куда нам надо вставить этот элемент
- Вставить наш элемент в нижний уровень списка с пропусками
- «Подбросить монетку» и в зависимости от результата протолкнуть элемент на уровень выше
- Повторять предыдущий шаг до тех пор, пока у нас «подброс монетки» дает положительный результат
Таким образом, если использовать честную монету, то математическое ожидание количества элементов на втором уровне равняется
, на третьем уровне и т.д. На уровне у нас окажется один элемент. Ну и соответственно вероятности попасть элементу на второй уровень — это , на третий и т.д. Вероятность попасть на уровень равна .Используя монетку с распределением отличным от
, , можно влиять на количество элементов на верхних уровнях. Так, например, при использовании монеты с распределением }, математическое ожидание количества элементов на уровне равно , каждый уровень будет составлять от предыдущего; время поиска будет равно . Соответственно при честной монетке и уровней получаем оценку, полученную ранее. Для крайних распределений:- —
- — (если разрешить добавление новых уровней при проталкивании элемента после броска монетки; иначе )
Удаление элемента
Алгоритм удаления достаточно тривиален.
- Найти удаляемый элемент
- Удалить его со всех уровней
Псевдокод
Наглядный, но не очень эффективный по памяти вариант списка с пропусками.
В узлах списка хранятся:
- — следующий узел
- — тот же узел на следующем уровне
- — данные типа T
- — ключ типа K
Конструктор
list skip_list (list l): list lvl = build_lvl(l) // Здесь происходит построение первого уровня while lvl.size() > 2 lvl = build_lvl (lvl) // Добавление следующих уровней; последний содержит два элемента return t
list build_lvl (list lvl) // Отсеивание нечётных элементов list next_lvl node i = lvl.head() // Перебор всех элементов lvl while (i != null) and (i != lvl.tail()) next_lvl.push_back(node(i.key, i)) // Добавление чётного элемента; i = i.next.next // он содержит ключ и ссылку на предыдущий уровень return next_lvl
Поиск элемента по ключу:
T find (list skip_list, K key) node res for (res = skip_list.head; res.ref != null; res = res.ref) // Cпускаемся на шаг вниз, если можем (п. 3) while res.key <= key // Переходим к следующему элементу (п. 2) res = res.next() return res.data
Вставка:
node insert (node i, K key, T data) while i.key <= key // Ищем подходящее место i = i.next() node tmp = null // Для записи в поле down if i.ref != null // Мы не на нижнем уровне tmp = insert (i.ref, key) // Рекурсивный вызов на более низком уровне if tmp == null // Проверка броска монетки return null i.next = new node (i.next, tmp, data, key) //Непосредственно вставка if random(0,1) > 0.5 // Бросок монетки return i.next // Нужно передать новый элемент для вставки выше else return null
void insert (list skip_list, K key, T data) // Обёрточка insert(skip_list.head, key, data)
Удаление:
void erase (node i, K key) if i == null return while i.key <= key // Ищем элемент i = i.next() erase(i.ref, key) // Удаляем с нижних уровней if i.key == key // Если ключ совпадает delete(i) // удаляем и с этого уровня
void erase (list skip_list, K key) // Обёрточка erase(skip_list.head, key)
Применение
- Базы данных
- Распределённые вычисления и p2p
- Масштабируемые параллельные приоритетные очереди и словари
В вычислительной геометрии широко применяются структуры на основе списка с пропусками.
См. также
Структуры на основе списка с пропусками: