Список с пропусками — различия между версиями
Gaporf (обсуждение | вклад) (→Построение) |
Gaporf (обсуждение | вклад) (→Поиск элемента) |
||
Строка 45: | Строка 45: | ||
==Операции над структурой== | ==Операции над структурой== | ||
===Поиск элемента=== | ===Поиск элемента=== | ||
+ | |||
+ | [[Файл:SkipListSearch.png|thumb|600px|Пример поиска числа <tex>8</tex>]] | ||
Допустим, что в нашем списке с пропусками существуют <tex>k</tex> уровней, при этом первым уровнем будет исходный список. | Допустим, что в нашем списке с пропусками существуют <tex>k</tex> уровней, при этом первым уровнем будет исходный список. | ||
Строка 50: | Строка 52: | ||
В таком случае алгоритм поиска в этой структуре будет представлять из себя следующие операции: | В таком случае алгоритм поиска в этой структуре будет представлять из себя следующие операции: | ||
− | # Начинаем поиск элемента в верхнем | + | # Начинаем поиск элемента в самом верхнем (<tex>k</tex>-ом) уровне |
− | # Переходим к следующему элементу списка, пока значение в следующей ячейке меньше | + | # Переходим к следующему элементу списка, пока значение в следующей ячейке меньше |
# Переместиться на один уровень вниз и перейти к шагу <tex>2</tex>. Если мы уже были на первом уровне <tex>-</tex> прекратить поиск. | # Переместиться на один уровень вниз и перейти к шагу <tex>2</tex>. Если мы уже были на первом уровне <tex>-</tex> прекратить поиск. | ||
− | + | В конце алгоритма функция вернёт элемент, значение которого не меньше ключа <tex>key</tex> или ссылку на хвост списка на первом уровне. | |
+ | Если в качестве случайного источника мы будем использовать честную монету, то в среднем случае будет <tex>\log{n}</tex> уровне. На самом верхнем уровне будет не более двух элементов. Тогда на каждом уровне в среднем нужно проверить не более двух элементов (в противном случае могли бы вместо двух нижних элементов проверить ещё один уровнем выше). Уровней всего <tex>\log{n}</tex>, откуда вытекает оценка времени поиска элемента в <tex>O(\log{n})</tex>. | ||
− | + | ====Псевдокод==== | |
+ | Функция <tex>\mathtt{find}</tex> возвращает ссылку на элемент, значение которого не меньше <tex>\mathtt{key}</tex>. В случае, если все элементы в списке с пропусками меньше <tex>\mathtt{key}</tex>, то возвращается ссылка на конец списка с пропусками. | ||
+ | |||
+ | '''T''' find ('''node''' res, '''K''' key) | ||
+ | '''while''' res.key < key <font color=darkgreen>// Пока значение вершины меньше ключа</font> | ||
+ | res = res.next() <font color=darkgreen>// Перейдём к следующей вершине в текущем уровне</font> | ||
+ | '''if''' res.down = ''null'' <font color=darkgreen>// Если мы находимся на первом уровне</font> | ||
+ | '''return''' res <font color=darkgreen>// Мы нашли искомый элемент</font> | ||
+ | '''return''' find(res.down, key) <font color=darkgreen>// Иначе спустимся на один уровень ниже</font> | ||
− | + | Для того, чтобы найти элемент с ключом <tex>\mathtt{key}</tex> в списке с пропусками <tex>\mathtt{list}</tex> необходимо запустить <tex>\mathtt{find}</tex> следующим образом | |
− | + | find(list.head(), key) | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
===Вставка элемента=== | ===Вставка элемента=== |
Версия 20:11, 22 марта 2019
Список с пропусками (англ. skip list) — вероятностная структура данных, позволяющая в среднем за
времени выполнять операции добавления, удаления и поиска элементов.Список с пропусками состоит из нескольких уровней, на каждом из которых находится отсортированный связный список. На самом нижнем (первом) уровне располагаются все элементы. Дальше около половины элементов таким же образом располагаются на втором, почти четверть
на третьем и так далее, но при этом известно, что если элемент расположен на каком-то уровне, то он также расположен на всех нижележащих уровнях.Содержание
Построение
Допустим, что нам задан односвязный отсортированный список и мы хотим построить на его основе список с пропусками, позволяющий в среднем за времени выполнять операции добавления, удаления и поиска элементов.
На самом нижнем уровне списка с пропусками мы расположим исходный список. На втором уровне
всё элементы с чётными номерами, причём каждый элемент будет ссылаться на соответствующий ему элемент на нижнем уровне. Таким же образом построим и третий уровень, куда будем добавлять только те элемента, номера которых кратны четырём. Аналогично построим и последующие уровни.Псевдокод
Каждый уровень списка с пропусками содержит отсортированный односвязный список, у которое есть начало
и конец . Для выполнения операций на списке с пропусками необходимо передавать в качестве аргумента ссылку на начало односвязного списка, расположенного на самом верхнем уровне.Элементы односвязного списка состоят из вершин
, у которых есть поля:- ссылка на следующий элемент списка
- ключ, который хранится в данной вершине
- ссылка на соответственный элемент, лежащий уровнем ниже
Функция возвращает новый уровень списка с пропусками на основе предыдущего построенного уровня.
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)) // Конструктор node(key, down) возвращает новую вершину с ключом key и ссылкой down на соответствующую вершину предыдущего уровеня i = i.next.next // Переход к следующему чётному элементу return next_lvl
Функция
принимает в качестве аргумента односвязный отсортированный список и возвращает новый список с пропусками, построенный на его основе.list skip_list (list l): list lvl = build_lvl(l) // Построение первого уровня while lvl.size() > 2 // Добавление следующих уровней; последний содержит не более двух элементов lvl = build_lvl (lvl) return lvl // Возвращает ссылку на начало верхнего уровня
Операции над структурой
Поиск элемента
Допустим, что в нашем списке с пропусками существуют
уровней, при этом первым уровнем будет исходный список.В таком случае алгоритм поиска в этой структуре будет представлять из себя следующие операции:
- Начинаем поиск элемента в самом верхнем ( -ом) уровне
- Переходим к следующему элементу списка, пока значение в следующей ячейке меньше
- Переместиться на один уровень вниз и перейти к шагу . Если мы уже были на первом уровне прекратить поиск.
В конце алгоритма функция вернёт элемент, значение которого не меньше ключа
или ссылку на хвост списка на первом уровне.Если в качестве случайного источника мы будем использовать честную монету, то в среднем случае будет
уровне. На самом верхнем уровне будет не более двух элементов. Тогда на каждом уровне в среднем нужно проверить не более двух элементов (в противном случае могли бы вместо двух нижних элементов проверить ещё один уровнем выше). Уровней всего , откуда вытекает оценка времени поиска элемента в .Псевдокод
Функция
возвращает ссылку на элемент, значение которого не меньше . В случае, если все элементы в списке с пропусками меньше , то возвращается ссылка на конец списка с пропусками.T find (node res, K key) while res.key < key // Пока значение вершины меньше ключа res = res.next() // Перейдём к следующей вершине в текущем уровне if res.down = null // Если мы находимся на первом уровне return res // Мы нашли искомый элемент return find(res.down, key) // Иначе спустимся на один уровень ниже
Для того, чтобы найти элемент с ключом
в списке с пропусками необходимо запустить следующим образомfind(list.head(), key)
Вставка элемента
Для вставки элемента в список с пропусками, нам необходимо выполнить следующие шаги:
- Найти с помощью алгоритма поиска позицию, где мог бы находиться элемент.
- Вставить наш элемент в текущий уровень списка с пропусками
- Подбросить монету.
- Если выпал «Орёл» то перейти на уровень выше и вернуться к шагу , иначе закончить операцию вставки элемента
Таким образом, если использовать честную монету, то математическое ожидание количества элементов на втором уровне равняется
, на третьем уровне и т.д. На уровне у нас окажется один элемент. Cоответственно вероятности попасть элементу на второй уровень — это , на третий и т.д. Вероятность попасть на уровень равна .Используя монетку с распределением отличным от
, можно влиять на количество элементов на верхних уровнях. Так, например, при использовании монеты с распределением (с вероятностью выпадает «Орёл») математическое ожидание количества элементов на уровне равно . Таким образом, время поиска будет равно . Для крайних распределений:- —
- —
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) // удаляем и с этого уровня
Для того, чтобы удалить элемент с ключом
из списка с пропусками достаточно вызвать функцию следующим образом:erase(list.head, key)
Применение
Список с пропусками применяется во многих приложениях, поскольку имеет ряд преимуществ.
- Быстрая вставка элемента, поскольку не требуется каким-либо образом изменять другие элементы (только предыдущий элемент)
- Проще реализовать, чем сбалансированные деревья или хеш-таблицы
- Следующий элемент достаётся за
- Легко модифицировать под различные задачи
См. также
- Список
- Рандомизированное бинарное дерево поиска
- Поисковые структуры данных
- Skip quadtree
- Skip graph — структура данных, основанная на списке с пропусками