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

Материал из Викиконспекты
Перейти к: навигация, поиск
(Отмена правки 47987 участника 188.227.78.59 (обсуждение))
Строка 1: Строка 1:
'''Список с пропусками''' (''skip list'') — поисковая структура данных, позволяющая производить операции поиска, добавления и удаления элемента в списке за достаточно малое время.
+
'''Список с пропусками''' (''skip-list'') — вероятностная структура данных, основанная на нескольких отсортированных односвязных списках.
  
Поиск элемента в списке производится за <tex>\Theta(\log(n))</tex>; добавление и удаление элемета происходит за то же время, что и поиск, однако эти операции могут замедлить поиск в структуре.
+
Отсортированный связный список является простейшей структурой с временем поиска <tex>\Theta(n)</tex>. Добавление дополнительных уровней, обеспечивающих быстрый доступ через несколько элементов, поможет улучшить асимптотику до <tex>\Theta(\log(n))</tex>.
  
Такая производительность достигается за счёт добавления новых уровней. При этом нижним уровнем является исходный список, а каждый следующий уровень - список, содержащий часть элементов предыдущего уровня со ссылками на эти элементы.
+
==Описание==
 
 
==Построение==
 
 
Список с пропусками строится на основе существующего односвязного отсортированного списка.
 
Список с пропусками строится на основе существующего односвязного отсортированного списка.
  
 
[[Файл:SimpleList.png]]
 
[[Файл:SimpleList.png]]
  
Добавив дополнительные уровни, каждый из которых представляет предыдущий уровень без нечетных элементов, мы получим возможность осуществлять поиск, вставку и удаление элементов подобно операциям с двоичным деревом поиска. Соответственно, асимптотика этих операций будет составлять <tex>\Theta(\log(n))</tex>.
+
Добавив дополнительные уровни, каждый из которых представляет предыдущий уровень без нечетных элементов, мы получим возможность осуществлять поиск, вставку и удаление элементов подобно операциям с двоичным деревом поиска. Одинаковые элементы связаны между уровнями. Соответственно, асимптотика этих операций будет составлять <tex>\Theta(\log(n))</tex>.
  
 
[[Файл:SkipList.png]]
 
[[Файл:SkipList.png]]
Строка 16: Строка 14:
 
==Операции над структурой==
 
==Операции над структурой==
 
===Поиск элемента===
 
===Поиск элемента===
Допустим, что в нашем списке с пропусками существуют <tex>k</tex> уровней, при этом первым уровнем (<tex>L_1</tex>) будет исходный список.
+
Допустим, что в нашем списке с пропусками существуют два уровня: <tex>L_1</tex>, в котором содержатся все элементы и <tex>L_2</tex>, в котором присутствует только часть из них. Между одинаковыми элементами этих двух списков существуют ссылки.
  
 
В таком случае алгоритм поиска в этой структуре будет представлять из себя следующие операции:
 
В таком случае алгоритм поиска в этой структуре будет представлять из себя следующие операции:
# Начинаем поиск элемента в верхнем списке (<tex>L_k), рассмотрим первый элемент
+
# Начинаем поиск элемента в верхнем левом углу
# Переходить к следующему элементу списка, пока значение в следующей ячейке меньше или равно ключу
+
# Передвигаться будем по списку <tex>L_2</tex>, пока значение в следующей ячейке меньше или равно ключу
# Переместиться на один уровень вниз и перейти к пункту 2. Если рассматриваемый элемент находится на нижнем уровне - выйти из поиска
+
# Переместиться в нижний уровень и продолжить аналогичный метод поиска по списку <tex>L_1</tex> 
  
 
Пример поиска числа 8 в списке из описания:
 
Пример поиска числа 8 в списке из описания:
Строка 27: Строка 25:
 
[[Файл:SkipListSearch.png]]
 
[[Файл:SkipListSearch.png]]
  
Рассмотрим время работы для списка с двумя уровнями.
 
 
Тогда время работы алгоритма поиска будет зависеть от количества элементов на уровне <tex>L_2</tex>. Представим, что на этот уровень у нас случайным образом попало несколько элементов. Следовательно в худшем случае поиска мы получим следующую оценку на время работы:
 
Тогда время работы алгоритма поиска будет зависеть от количества элементов на уровне <tex>L_2</tex>. Представим, что на этот уровень у нас случайным образом попало несколько элементов. Следовательно в худшем случае поиска мы получим следующую оценку на время работы:
  
Строка 38: Строка 35:
 
<tex> \sqrt{n}  + \dfrac{n}{\sqrt{n}} = 2 \sqrt{n}</tex>
 
<tex> \sqrt{n}  + \dfrac{n}{\sqrt{n}} = 2 \sqrt{n}</tex>
  
Так же можно убедиться, что список с пропусками, имеющий <tex>k<tex> уровней, будет лучше всего работать с <tex>\sqrt[k]{n} </tex> элементами на уровне; время работы такого списка будет равно <tex>k \sqrt[k]{n} </tex>.
+
Делая аналогичные подсчеты для списков с пропусками, в которых содержится больше уровней, получаем:
Для <tex>\log_2{n}</tex> уровней же время работы составит <tex> \log_2{n} \times \sqrt[\log{n}]{n} = n ^ {\frac{1}{log_2{n}}} \times \log{n} = n ^ {\log_n{2}} \times \log_2{n} = 2\log_2{n}</tex>
+
* Для трех уровней: <tex> 3 \sqrt[3]{n}</tex>
 
+
* Для четырех уровней: <tex> 4 \sqrt[4]{n}</tex>
Пример поиска:
+
* Для пяти уровней: <tex> 5 \sqrt[5]{n}</tex>
<code>
+
* Для <tex>\log{n}</tex> уровней: <tex> \log{n} \times \sqrt[\log{n}]{n} = n ^ {\frac{1}{log{n}}} \log{n} = n ^ {\log_n{2}} \log{n} = 2\log{n}</tex>
    T* find (list <node> skip_list, K key) {       // Возвращает собственно элемент
+
        node* pos = _find (skip_list, key);
+
В списках с пропусками, в которых содержится <tex>\log{n}</tex> уровней будет себя вести очень похоже на сбалансированные бинарные деревья поиска. В идеальной данной структуре соотношение между соседними уровнями будет равняться двум. Поиск в <tex>\log{n}</tex> списке с пропусками будет осуществляться за асимптотическое время <tex>O(\log{n})</tex>.
        return pos->key == key ? pos : NULL;        // Вернёт элемент, если он есть, иначе NULL
 
    }
 
  
    node* _find (list <node> skip_list, K key) {    // Возвращает место элемента
 
        node * res;
 
        for (res = skip_list.begin(); res->ref != NULL; res = res->ref) {
 
            while (res->key <= key)                // Переходим к следующему элементу (п. 2)
 
                res = res->next();
 
        }                                          // Спускаемся на шаг вниз, если можем (п. 3)
 
        return res;                                // Возвращаем позицию элемента, либо (в случае отсутствия)
 
    }                                              // позицию, после которой его следует вставить
 
</code>
 
 
===Вставка элемента===
 
===Вставка элемента===
 
Для вставки элемента в список с пропусками, нам необходимо выполнить следующие шаги:
 
Для вставки элемента в список с пропусками, нам необходимо выполнить следующие шаги:
Строка 66: Строка 52:
 
Таким образом, если использовать честную монету, то математическое ожидание количества элементов на втором уровне равняется <tex>\dfrac{n}{2}</tex>, на третьем уровне <tex>\dfrac{n}{4}</tex> и т.д. На уровне <tex>\log{n}</tex> у нас окажется один элемент. Ну и соответственно вероятности попасть элементу на второй уровень — это <tex>\dfrac{1}{2}</tex>, на третий <tex>\dfrac{1}{4}</tex> и т.д. Вероятность попасть на уровень <tex>\log{n}</tex> равна <tex>\dfrac{1}{n}</tex>.
 
Таким образом, если использовать честную монету, то математическое ожидание количества элементов на втором уровне равняется <tex>\dfrac{n}{2}</tex>, на третьем уровне <tex>\dfrac{n}{4}</tex> и т.д. На уровне <tex>\log{n}</tex> у нас окажется один элемент. Ну и соответственно вероятности попасть элементу на второй уровень — это <tex>\dfrac{1}{2}</tex>, на третий <tex>\dfrac{1}{4}</tex> и т.д. Вероятность попасть на уровень <tex>\log{n}</tex> равна <tex>\dfrac{1}{n}</tex>.
  
Используя монетку с распределением отличным от {<tex>\dfrac{1}{2}</tex>, <tex>\dfrac{1}{2}</tex>}, можно влиять на количество элементов на верхних уровнях. Так, например, при использовании монеты с распределением {<tex>p</tex>, <tex>q</tex>}, математическое ожидание количества элементов на уровне <tex>l</tex> равно <tex>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>}, можно влиять на количество элементов на верхних уровнях (и соответственно, на количество уровней). Однако как при большем количестве проталкиваний элементов на уровень выше, так и при меньшем, количество шагов при поиске элемента возрастает. При распределении {0, 1} структура превращается в обыкновенный список, при {1, 0} {{---}} в <tex>n</tex> параллельных списков. В обоих случаях
Для крайних распределений:
 
* {<tex>0, 1</tex>} - <tex>O(k+n)</tex>
 
* {<tex>1, 0</tex>} - <tex>\infty</tex> (если разрешить добавление новых уровней при проталкивании элемента после броска монетки; иначе <tex>O(n)</tex>)
 
  
 
===Удаление элемента===
 
===Удаление элемента===
Строка 77: Строка 60:
  
 
==Псевдокод==
 
==Псевдокод==
Наглядный, но не очень эффективный по памяти вариант списка с пропусками.
 
  
В узлах списка хранятся:
 
* <tex>next</tex> {{---}} следующий узел
 
* <tex>down</tex> {{---}} тот же узел на следующем уровне
 
* <tex>data</tex> {{---}} данные типа T
 
* <tex>key</tex>  {{---}} ключ типа K
 
 
Конструктор
 
 
<code>
 
<code>
skip_list (list l):
+
  const float P = 0.5
    list lvl = build_lvl(l);                // Здесь происходит построение первого уровня
+
 
     while (lvl.size() > 2)  
+
  int random_level()
        lvl = build_lvl (lvl);             // Добавление следующих уровней; последний содержит два элемента
+
      int lvl = (int)(log(frand())/log(1.-P))
    return t;
+
      if lvl < MAX_LEVEL
 +
          return lvl
 +
      return MAX_LEVEL 
 +
 
 +
  boolean Find (int key)
 +
      SkipNode x = header
 +
      for i = level to 0
 +
          while x.forward[i] != NULL and x.forward[i].value < key
 +
              x = x.forward[i]
 +
      x = x.forward[0]
 +
      return x != NULL && x.value == key
 +
   
 +
   
 +
   
 +
 
 +
  void Insert(int value)
 +
      SkipNode x = header
 +
      SkipNode update
 +
      update.assign(MAX_LEVEL + 1, 0)
 +
      
 +
      for i = level to 0
 +
          while x.forward[i] != NULL and x.forward[i].value < value
 +
              x = x.forward[i]
 +
          update[i] = x
 +
      x = x.forward[0]
 +
      if x == NULL or x.value != value       
 +
          int lvl = random_level()
 +
          if lvl > level
 +
              for i = level + 1 to lvl
 +
                  update[i] = header
 +
              level = lvl
 +
          x = new SkipNode(lvl, value)
 +
          for i = 0 to lvl
 +
              x.forward[i] = update[i].forward[i]
 +
              update[i].forward[i] = x
 +
           
 +
           
 +
  void Erase(int value)
 +
      SkipNode x = header
 +
      SkipNode update
 +
      update.assign(MAX_LEVEL + 1, 0)
 +
      for i = level to 0
 +
          while x.forward[i] != NULL and x.forward[i].value < value
 +
              x = x.forward[i]
 +
          update[i] = x
 +
      x = x.forward[0]
 +
      if x.value == value
 +
          for i = 0 to level
 +
              if update[i].forward[i] != x
 +
                  break
 +
              update[i].forward[i] = x.forward[i];
 +
          delete x
 +
          while level > 0 or header.forward[level] == NULL
 +
              level--
  
list build_lvl (list lvl)                  // Отсеивание нечётных элементов
 
    list next_lvl;
 
    node i = lvl.head();                    // Перебор всех элементов lvl
 
    while ((i != NULL) && (i != lvl.tail()))
 
        next_lvl.push_back(node(i.key, i)); // Добавление чётного элемента;
 
        i = i.next.next;                    // он содержит ключ и ссылку на предыдущий уровень
 
    return next_lvl;
 
</code>
 
Поиск элемента по ключу:
 
<code>
 
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;
 
</code>
 
Вставка:
 
<code>
 
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);
 
 
</code>
 
</code>
Удаление:
 
<code>
 
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);
 
</code>
 
==См. также==
 
*[[Список]]
 
*[[Рандомизированное бинарное дерево поиска]]
 
*[[Поисковые структуры]]
 
 
==Ссылки==
 
==Ссылки==
 
*[http://habrahabr.ru/post/111913/ Хабрахабр — Списки с пропусками]
 
*[http://habrahabr.ru/post/111913/ Хабрахабр — Списки с пропусками]

Версия 18:40, 7 июня 2015

Список с пропусками (skip-list) — вероятностная структура данных, основанная на нескольких отсортированных односвязных списках.

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

Описание

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

SimpleList.png

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

SkipList.png

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

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

Допустим, что в нашем списке с пропусками существуют два уровня: [math]L_1[/math], в котором содержатся все элементы и [math]L_2[/math], в котором присутствует только часть из них. Между одинаковыми элементами этих двух списков существуют ссылки.

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

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

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

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] 3 \sqrt[3]{n}[/math]
  • Для четырех уровней: [math] 4 \sqrt[4]{n}[/math]
  • Для пяти уровней: [math] 5 \sqrt[5]{n}[/math]
  • Для [math]\log{n}[/math] уровней: [math] \log{n} \times \sqrt[\log{n}]{n} = n ^ {\frac{1}{log{n}}} \log{n} = n ^ {\log_n{2}} \log{n} = 2\log{n}[/math]

В списках с пропусками, в которых содержится [math]\log{n}[/math] уровней будет себя вести очень похоже на сбалансированные бинарные деревья поиска. В идеальной данной структуре соотношение между соседними уровнями будет равняться двум. Поиск в [math]\log{n}[/math] списке с пропусками будет осуществляться за асимптотическое время [math]O(\log{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]}, можно влиять на количество элементов на верхних уровнях (и соответственно, на количество уровней). Однако как при большем количестве проталкиваний элементов на уровень выше, так и при меньшем, количество шагов при поиске элемента возрастает. При распределении {0, 1} структура превращается в обыкновенный список, при {1, 0} — в [math]n[/math] параллельных списков. В обоих случаях

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

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

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

Псевдокод

 const float P = 0.5
 
 int random_level()
     int lvl = (int)(log(frand())/log(1.-P))
     if lvl < MAX_LEVEL
         return lvl
     return MAX_LEVEL  
 
 boolean Find (int key)
     SkipNode x = header
     for i = level to 0
         while x.forward[i] != NULL and x.forward[i].value < key
             x = x.forward[i]
     x = x.forward[0]
     return x != NULL && x.value == key
   
   
   
 
 void Insert(int value) 
     SkipNode x = header
     SkipNode update
     update.assign(MAX_LEVEL + 1, 0)
   
     for i = level to 0
         while x.forward[i] != NULL and x.forward[i].value < value
             x = x.forward[i]
         update[i] = x
     x = x.forward[0]
     if x == NULL or x.value != value        
         int lvl = random_level()
         if lvl > level
             for i = level + 1 to lvl
                 update[i] = header
             level = lvl
         x = new SkipNode(lvl, value)
         for i = 0 to lvl
             x.forward[i] = update[i].forward[i]
             update[i].forward[i] = x
           
           
 void Erase(int value)
     SkipNode x = header
     SkipNode update
     update.assign(MAX_LEVEL + 1, 0)
     for i = level to 0
         while x.forward[i] != NULL and x.forward[i].value < value
             x = x.forward[i]
         update[i] = x
     x = x.forward[0]
     if x.value == value
         for i = 0 to level
             if update[i].forward[i] != x
                 break
             update[i].forward[i] = x.forward[i];
         delete x
         while level > 0 or header.forward[level] == NULL
             level--


Ссылки