Список с пропусками — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
(Псевдокод)
Строка 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>\{0, 1\}</tex> {{---}} <tex>O(k+n)</tex>
* {<tex>1, 0</tex>} - <tex>\infty</tex> (если разрешить добавление новых уровней при проталкивании элемента после броска монетки; иначе <tex>O(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''' (lvl.size() > 2)
+
         '''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();                    // Перебор всех элементов lvl
+
         '''node''' i = lvl.head()                     <font color=darkgreen>// Перебор всех элементов lvl</font>
         while ((i != NULL) '''and''' (i != lvl.tail()))
+
         '''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 != '''NULL'''; res = res.ref)
+
         '''for''' (res = skip_list.head; res.ref != ''null''; res = res.ref)
                                             // Cпускаемся на шаг вниз, если можем (п. 3)
+
                                             <font color=darkgreen>// Cпускаемся на шаг вниз, если можем (п. 3)</font>
             '''while''' (res.key <= key)          // Переходим к следующему элементу (п. 2)
+
             '''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''' (i.key <= key)                // Ищем подходящее место
+
         '''while''' i.key <= key                   <font color=darkgreen>// Ищем подходящее место</font>
             i = i.next();
+
             i = i.next()  
         node tmp = '''NULL''';                    // Для записи в поле down
+
         '''node''' tmp = ''null''                     <font color=darkgreen>// Для записи в поле down</font>
         '''if''' i.ref != '''NULL'''                    // Мы не на нижнем уровне
+
         '''if''' i.ref != ''null''                    <font color=darkgreen>// Мы не на нижнем уровне</font>
             tmp = insert (i.ref, key);      // Рекурсивный вызов на более низком уровне
+
             tmp = insert (i.ref, key)       <font color=darkgreen>// Рекурсивный вызов на более низком уровне</font>
             '''if''' tmp == '''NULL'''                  // Проверка броска монетки
+
             '''if''' tmp == ''null''                  <font color=darkgreen>// Проверка броска монетки</font>
                 '''return''' '''NULL''';
+
                 '''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 '''NULL''';
+
             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''' (i == '''NULL''')
+
         '''if''' i == ''null''
             '''return''';
+
             '''return'''  
         '''while''' (i.key <= key)                // Ищем элемент
+
         '''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''' (i.key == key)                    // Если ключ совпадает
+
         '''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) — поисковая структура данных, реализующая интерфейс упорядоченного множества, позволяет производить операции поиска, добавления и удаления элемента в списке за достаточно малое время.

Поиск элемента в списке производится за [math]\Theta(\log(n))[/math]; добавление и удаление элемета происходит за то же время, что и поиск, однако эти операции могут замедлить поиск в структуре.

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

Построение

Список с пропусками строится на основе существующего односвязного отсортированного списка.

SimpleList.png

Добавив дополнительные уровни, каждый из которых представляет предыдущий уровень без нечетных элементов, мы получим возможность осуществлять поиск, вставку и удаление элементов подобно операциям с двоичным деревом поиска. Соответственно, асимптотика этих операций будет составлять [math]\Theta(\log(n))[/math].

SkipList.png

Операции над структурой

Поиск элемента

Допустим, что в нашем списке с пропусками существуют [math]k[/math] уровней, при этом первым уровнем ([math]L_1[/math]) будет исходный список.

В таком случае алгоритм поиска в этой структуре будет представлять из себя следующие операции:

  1. Начинаем поиск элемента в верхнем списке ([math]L_k[/math]), рассмотрим первый элемент
  2. Переходить к следующему элементу списка, пока значение в следующей ячейке меньше или равно ключу
  3. Переместиться на один уровень вниз и перейти к пункту 2. Если рассматриваемый элемент находится на нижнем уровне - выйти из поиска

Пример поиска числа [math]8[/math] в списке из описания:

SkipListSearch.png

Рассмотрим время работы для списка с двумя уровнями. Тогда время работы алгоритма поиска будет зависеть от количества элементов на уровне [math]L_2[/math]. Представим, что на этот уровень у нас случайным образом попало несколько элементов. Следовательно в худшем случае поиска мы получим следующую оценку на время работы:

[math] \approx \vert L_2\vert + \dfrac{\vert L_1\vert }{\vert L_2\vert } = \vert L_2\vert + \dfrac{n}{\vert L_2\vert }[/math]

Минимизируя, мы получаем, что [math]\vert L_2 \vert ^ 2 = n[/math]

В итоге время за которое мы найдем элемент в списке с пропусками с двумя уровнями будет равняться:

[math] \sqrt{n} + \dfrac{n}{\sqrt{n}} = 2 \sqrt{n}[/math]

Также можно убедиться, что список с пропусками, имеющий [math]k[/math] уровней, будет лучше всего работать с [math]\sqrt[k]{n} [/math] элементами на уровне; время работы такого списка будет равно [math]k \sqrt[k]{n} [/math]. Для [math]\log_2{n}[/math] уровней же время работы составит [math] \log_2{n} \cdot \sqrt[\log_2{n}]{n} = n ^ {\frac{1}{log_2{n}}} \cdot \log_2{n} = n ^ {\log_n{2}} \cdot \log_2{n} = 2\log_2{n}[/math]

Вставка элемента

Для вставки элемента в список с пропусками, нам необходимо выполнить следующие шаги:

  1. Найти с помощью алгоритма поиска позицию, куда нам надо вставить этот элемент
  2. Вставить наш элемент в нижний уровень списка с пропусками
  3. «Подбросить монетку» и в зависимости от результата протолкнуть элемент на уровень выше
  4. Повторять предыдущий шаг до тех пор, пока у нас «подброс монетки» дает положительный результат

Таким образом, если использовать честную монету, то математическое ожидание количества элементов на втором уровне равняется [math]\dfrac{n}{2}[/math], на третьем уровне [math]\dfrac{n}{4}[/math] и т.д. На уровне [math]\log{n}[/math] у нас окажется один элемент. Ну и соответственно вероятности попасть элементу на второй уровень — это [math]\dfrac{1}{2}[/math], на третий [math]\dfrac{1}{4}[/math] и т.д. Вероятность попасть на уровень [math]\log{n}[/math] равна [math]\dfrac{1}{n}[/math].

Используя монетку с распределением отличным от [math]\{\dfrac{1}{2}[/math], [math]\dfrac{1}{2}\}[/math], можно влиять на количество элементов на верхних уровнях. Так, например, при использовании монеты с распределением [math]\{p,q\}[/math]}, математическое ожидание количества элементов на уровне [math]l[/math] равно [math]n q^l[/math], каждый уровень будет составлять [math]q[/math] от предыдущего; время поиска будет равно [math]O(\dfrac{k}{q} + nq^k)[/math]. Соответственно при честной монетке и [math]\log(n)[/math] уровней получаем оценку, полученную ранее. Для крайних распределений:

  • [math]\{0, 1\}[/math][math]O(k+n)[/math]
  • [math]\{1, 0\}[/math][math]\infty[/math] (если разрешить добавление новых уровней при проталкивании элемента после броска монетки; иначе [math]O(n)[/math])

Удаление элемента

Алгоритм удаления достаточно тривиален.

  1. Найти удаляемый элемент
  2. Удалить его со всех уровней

Псевдокод

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

В узлах списка хранятся:

  • [math]next[/math] — следующий узел
  • [math]down[/math] — тот же узел на следующем уровне
  • [math]data[/math] — данные типа T
  • [math]key[/math] — ключ типа 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
  • Масштабируемые параллельные приоритетные очереди и словари

В вычислительной геометрии широко применяются структуры на основе списка с пропусками.

См. также

Структуры на основе списка с пропусками:

Источники информации