7 ответов
Поскольку вы упомянули Список, который является как индексируемым (я предполагаю, что вы хотите скорейшего извлечения), так и для того, чтобы разрешать дубликаты, я бы посоветовал вам использовать пользовательский набор с LinkedList или ArrayList.
Вам нужно иметь базовый набор, например, HashSet и продолжать добавлять к нему значения. Если вы сталкиваетесь с дубликатом, значение этого набора должно указывать на список. Итак, у вас будет и быстрый поиск, и, конечно же, вы сохраните свои объекты в виде коллекции psuedo.
Это должно дать вам хорошую эффективность для извлечения. В идеале, если ваши Ключи не дублируются, вы достигнете O (1) в качестве скорости поиска.
Этот ответ задерживается на 3 года, но я надеюсь, что он будет полезен тем, кто хочет получить список пропусков Java с этого момента:)
Это решение позволяет дублировать, как вы просили. Я следую примерно руководству здесь http://igoro.com/archive/skip-lists-are-fascinating , поэтому сложности похожи на то, что удалить стоит O (nlogn) - как я это делал "Не пытайтесь использовать двусвязные узлы, я предполагаю, что это приведет к тому, что удалить до O (logn).
Код содержит: интерфейс, список пропуска, реализующий интерфейс, и класс node. Он также является общим.
Вы можете настроить параметр LEVELS для производительности, но помните компромисс между пространством и временем.
Вы можете использовать приведенное ниже код, чтобы сделать свой собственный базовый скипист :
1) Сделайте start и конец до represent start and end of skip list .
2) Add the nodes и assign pointers до следующего based on
If(node is even) then ,assign a fast lane pointer with next pointer else assign only pointer to next node
Java-код для базового списка пропуска (вы можете добавить дополнительные функции):
Public class MyClass { public static void main(String args) { Skiplist skiplist=new Skiplist(); Node n1=new Node(); Node n2=new Node(); Node n3=new Node(); Node n4=new Node(); Node n5=new Node(); Node n6=new Node(); n1.setData(1); n2.setData(2); n3.setData(3); n4.setData(4); n5.setData(5); n6.setData(6); skiplist.insert(n1); skiplist.insert(n2); skiplist.insert(n3); skiplist.insert(n4); skiplist.insert(n5); skiplist.insert(n6); /*print all nodes*/ skiplist.display(); System.out.println(); /* print only fast lane node*/ skiplist.displayFast(); } } class Node{ private int data; private Node one_next; //contain pointer to next node private Node two_next; //pointer to node after the very next node public int getData() { return data; } public void setData(int data) { this.data = data; } public Node getOne_next() { return one_next; } public void setOne_next(Node one_next) { this.one_next = one_next; } public Node getTwo_next() { return two_next; } public void setTwo_next(Node two_next) { this.two_next = two_next; } } class Skiplist{ Node start; //start pointer to skip list Node head; Node temp_next; //pointer to store last used fast lane node Node end; //end of skip list int length; public Skiplist(){ start=new Node(); end=new Node(); length=0; temp_next=start; } public void insert(Node node){ /*if skip list is empty */ if(length==0){ start.setOne_next(node); node.setOne_next(end); temp_next.setTwo_next(end); head=start; length++; } else{ length++; Node temp=start.getOne_next(); Node prev=start; while(temp != end){ prev=temp; temp=temp.getOne_next(); } /*add a fast lane pointer for even no of nodes*/ if(length%2==0){ prev.setOne_next(node); node.setOne_next(end); temp_next.setTwo_next(node); temp_next=node; node.setTwo_next(end); } /*odd no of node will not contain fast lane pointer*/ else{ prev.setOne_next(node); node.setOne_next(end); } } } public void display(){ System.out.println("--Simple Traversal--"); Node temp=start.getOne_next(); while(temp != end){ System.out.print(temp.getData()+"=>"); temp=temp.getOne_next(); } } public void displayFast(){ System.out.println("--Fast Lane Traversal--"); Node temp=start.getTwo_next(); while(temp !=end){ System.out.print(temp.getData()+"==>"); temp=temp.getTwo_next(); } } }
Вывод:
Простой обход -
1 = > 2 = > 3 = > 4 = > 5 = > 6 = >
Быстрая перемотка дорожки -
2 == > 4 == > 6 == >
Когда вы создаете ConcurrentSkipListSet , вы передаете компаратор в конструктор.
New ConcurrentSkipListSet<>(new ExampleComparator());
public class ExampleComparator implements Comparator
Вы можете создать компаратор, который сделает ваш SkipListSet вести себя как обычный список.
Я не утверждаю, что это моя собственная реализация. Я просто не могу вспомнить, где я его нашел. Если вы знаете, дайте мне знать, и я обновлю. Это работает для меня хорошо:
Public class SkipList
Исправлена ошибка в реализации, предоставляемой @PoweredByRice. Он выбросил NPE для случаев, когда удаленный node был первым node. Другие обновления включают переименованные имена переменных и обратную печать порядка списка пропусков.
Import java.util.Random;
interface SkippableList
Представьте, что ваш коллега-нытик пришел рассказать о своей непростой задаче - ему нужно не просто упорядочить по возрастанию набор целых чисел, а выдать все элементы упорядоченного набора с L-го по R-й включительно!
Вы заявили, что это элементарная задача и, чтобы написать решение на языке C#, вам нужно десять минут. Ну, или час. Или два. Или шоколадка «Алёнка»
К оценке решения есть несколько комментариев:
Ваш код будут оценивать и тестировать три программиста:Решение может быть очень интересным, поэтому я посчитал нужным его описать.
- Билл будет запускать ваше решение на тестах размером не больше 10Кб.
- В тестах Стивена количество запросов будет не больше 10^5, при этом количество запросов на добавление будет не больше 100.
- В тестах Марка количество запросов будет не больше 10^5.
Обозначим Add(e) - добавление элемента в хранилище, а Range(l, r) - взятие среза с l по r элемент.
Тривиальный вариант хранилища может быть таким:
C Range(l, r) - взятие среза можно оценить, как O(r - l).
C Add(e) - вставка в худшем случае будет работать за O(n), где n - количество элементов. При n ~ 10^6, вставка - узкое место. Ниже в статье будет предложен улучшенный вариант хранилища.
Пример исходного кода можно посмотреть .
Эта структура данных не получила широкой известности (кстати, и на хабре достаточно обзорно о ней написано), хотя у нее хорошие асимптотические оценки. Любопытства ради захотелось ее реализовать, тем более имелась подходящая задача.
Пусть у нас есть отсортированный односвязный список:
В худшем случае поиск выполняется за O(n). Как его можно ускорить?
В одной из видео-лекций, которые я пересматривал, когда занимался задачкой, приводился замечательный пример про экспресс-линии в Нью-Йорке:
На примере изображен идеальный SkipList, в реальности всё выглядит подобным образом, но немного не так:)
Решать это предлагается так: при вставке мы добавляем новый элемент в самый нижний уровень и начинаем подкидывать монетку, пока она выпадает, мы проталкиваем элемент на следующий вышележащий уровень.
Попробуем вставить элемент - 67. Сначала найдем, куда в нижележащем списке его нужно вставить:
Предположил, что монетка выпала два раза подряд. Значит нужно протолкнуть элемент на два уровня вверх:
После того, как мы получаем доступ к i-му элементу (кстати, получаем мы его за O(ln n)), сделать срез не представляется трудным.
Пусть необходимо найти Range(5, 7). Сначала получим элемент по индексу пять:
А теперь Range(5, 7):
SkipListNode {Собственно, так и сделано .
int Element;
SkipListNode Next;
int Width;
}
Но в C# в массиве хранится еще и его длина, а хотелось сделать это за как можно меньшее количество памяти (как оказалось, в условиях задачи все оценивалось не так строго). При этом хотелось сделать так, чтобы реализация SkipList и расширенного RB Tree занимала примерно одинаковое количество памяти.
Ответ, как уменьшить потребление памяти, неожиданно был найден при ближайшем рассмотрении из пакета java.util.concurrent.
ListNode {
int Element;
ListNode Next;
}Lane {
Lane Right;
Lane Down;
ListNode Node;
}
int randomLevel() {Подробнее о генерации псевдослучайных чисел с помощью XOR можно почитать в этой статьe . Особого увеличения скорости я не заметил, поэтому, не использовал его.
int x = randomSeed;
x ^= x << 13;
x ^= x >>> 17;
randomSeed = x ^= x << 5;
if ((x & 0x80000001) != 0)
return 0;
int level = 1;
while (((x >>>= 1) & 1) != 0) ++level;
return level;
}
Исходник получившегося хранилища можно глянуть .
Все вместе можно забрать с googlecode.com (проект Pagination).
Array | RBTree | SkipList | |
---|---|---|---|
Random | 127033 ms | 1020 ms | 1737 ms |
Ordered | 108 ms | 457 ms | 536 ms |
Ordered by descending | 256337 ms | 358 ms | 407 ms |
Также были проведены замеры: сколько каждое хранилище занимает в памяти при вставленных в него 10^6 элементах. Использовался студийный профайлер, для простоты запускался такой код:
var storage = ...Результаты:
for (var i = 0; i < 1000000; ++i)
storage.Add(i);
Array | RBTree | SkipList | |
---|---|---|---|
Total bytes allocated | 8,389,066 bytes | 24,000,060 bytes | 23,985,598 bytes |
P.S.: я был на стажировке в компании СКБ Контур. Это чтобы не отвечать на одинаковые вопросы =)
Списки с пропусками - это структура данных, которая может применяться вместо сбалансированных деревьев. Благодаря тому, что алгоритм балансировки вероятностный, а не строгий, вставка и удаление элемента в списках с пропусками реализуется намного проще и значительно быстрее, чем в сбалансированных деревьях.
Списки с пропусками - это вероятностная альтернатива сбалансированным деревьям. Они балансируются с использованием генератора случайных чисел. Несмотря на то, что у списков с пропусками плохая производительность в худшем случае, не существует такой последовательности операций, при которой бы это происходило постоянно (примерно как в алгоритме быстрой сортировки со случайным выбором опорного элемента). Очень маловероятно, что эта структура данных значительно разбалансируется (например, для словаря размером более 250 элементов вероятность того, что поиск займёт в три раза больше ожидаемого времени, меньше одной миллионной).
Балансировать структуру данных вероятностно проще, чем явно обеспечивать баланс. Для многих задач списки пропуска это более естественное представление данных по сравнению с деревьями. Алгоритмы получаются более простыми для реализации и, на практике, более быстрыми по сравнению со сбалансированными деревьями. Кроме того, списки с пропусками очень эффективно используют память. Они могут быть реализованы так, чтобы на один элемент приходился в среднем примерно 1.33 указатель (или даже меньше) и не требуют хранения для каждого элемента дополнительной информации о балансе или приоритете.
Для поиска элемента в связном списке мы должны просмотреть каждый его узел:
Если список хранится отсортированным и каждый второй его узел дополнительно содержит указатель на два узла вперед, нам нужно просмотреть не более, чем ⌈n
/2⌉ + 1 узлов(где n
- длина списка):
Аналогично, если теперь каждый четвёртый узел содержит указатель на четыре узла вперёд, то потребуется просмотреть не более чем ⌈n
/4⌉ + 2 узла:
Если каждый 2 i
i
узлов вперёд, то количество узлов, которые необходимо просмотреть, сократится до ⌈log 2 n
⌉, а общее количество указателей в структуре лишь удвоится:
Такая структура данных может использоваться для быстрого поиска, но вставка и удаление узлов будут медленными.
Назовём узел, содержащий k указателей на впередистоящие элементы, узлом уровня k
. Если каждый 2 i
-ый узел содержит указатель на 2 i
узлов вперёд, то уровни распределены так: 50% узлов - уровня 1, 25% - уровня 2, 12.5% - уровня 3 и т.д. Но что произойдёт, если уровни узлов будут выбираться случайно, в тех же самых пропорциях? Например, так:
Указатель номер i каждого узла будет ссылаться на следующий узел уровня i или больше, а не на ровно 2 i -1 узлов вперёд, как было до этого. Вставки и удаления потребуют только локальных изменений; уровень узла, выбранный случайно при его вставке, никогда не будет меняться. При неудачном назначении уровней производительность может оказаться низкой, но мы покажем, что такие ситуации редки. Из-за того, что эти структуры данных представляют из себя связные списки с дополнительными указателями для пропуска промежуточных узлов, я называю их списками с пропусками .
Каждый элемент списка представляет из себя узел, уровень которого был выбран случайно при его создании, причём независимо от числа элементов, которые уже находились там. Узел уровня i содержит i указателей на различные элементы впереди, проиндексированные от 1 до i . Мы можем не хранить уровень узла в самом узле. Количество уровней ограничено заранее выбранной константой MaxLevel . Назовём уровнем списка максимальный уровень узла в этом списке (если список пуст, то уровень равен 1). Заголовок списка (на картинках он слева) содержит указатели на уровни с 1 по MaxLevel . Если элементов такого уровня ещё нет, то значение указателя - специальный элемент NIL.
Search
(list, searchKey)
x:= list→header
# инвариант цикла: x→key < searchKey
for
i:= list→level downto
1 do
while
x→forward[i]→key < searchKey do
x:= x→forward[i]
# x→key < searchKey ≤ x→forward→key
x:= x→forward
if
x→key = searchKey then
return
x→value
else
return
failure
Insert
(list, searchKey, newValue)
local
update
x:= list→header
for
i:= list→level downto
1 do
while
x→forward[i]→key < searchKey do
x:= x→forward[i]
# x→key < searchKey ≤ x→forward[i]→key
update[i] := x
x:= x→forward
if
x→key = searchKey then
x→value:= newValue
else
lvl:= randomLevel()
if
lvl > list→level then
for
i:= list→level + 1 to
lvl do
update[i] := list→header
list→level:= lvl
x:= makeNode(lvl, searchKey, value)
for
i:= 1 to
level do
x→forward[i] := update[i]→forward[i]
update[i]→forward[i] := x
Delete
(list, searchKey)
local
update
x:= list→header
for
i:= list→level downto
1 do
while
x→forward[i]→key < searchKey do
x:= x→forward[i]
update[i] := x
x:= x→forward
if
x→key = searchKey then
for
i:= 1 to
list→level do
if
update[i]→forward[i] ≠ x then
break
update[i]→forward[i] := x→forward[i]
free(x)
while
list→level > 1 and
list→header→forward = NIL do
list→level:= list→level – 1
Для запоминания элементов перед вставляемым(или удаляемым) используется массив update . Элемент update[i] - это указатель на самый правый узел, уровня i или выше, из числа находящихся слева от места обновления.
Если случайно выбранный уровень вставляемого узла оказался больше, чем уровень всего списка (т.е. если узлов с таким уровнем ещё не было), увеличиваем уровень списка и инициализируем соответствующие элементы массива update указателями на заголовок. После каждого удаления проверяем, удалили ли мы узел с максимальным уровнем и, если это так, уменьшаем уровень списка.
randomLevel
()
lvl:= 1
# random() возвращает случайное число в полуинтервале }