mirror of
https://github.com/TheAlgorithms/Java.git
synced 2025-07-09 20:20:56 +08:00
Add Skip List (#3154)
This commit is contained in:

committed by
GitHub

parent
22be348c54
commit
e59568bc5e
@ -0,0 +1,324 @@
|
|||||||
|
package com.thealgorithms.datastructures.lists;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skip list is a data structure that allows {@code O(log n)} search complexity
|
||||||
|
* as well as {@code O(log n)} insertion complexity within an ordered sequence
|
||||||
|
* of {@code n} elements. Thus it can get the best features of a sorted array
|
||||||
|
* (for searching) while maintaining a linked list-like structure that allows
|
||||||
|
* insertion, which is not possible with a static array.
|
||||||
|
* <p>
|
||||||
|
* A skip list is built in layers. The bottom layer is an ordinary ordered
|
||||||
|
* linked list. Each higher layer acts as an "express lane" for the lists
|
||||||
|
* below.
|
||||||
|
* <pre>
|
||||||
|
* [ ] ------> [ ] --> [ ]
|
||||||
|
* [ ] --> [ ] [ ] --> [ ]
|
||||||
|
* [ ] [ ] [ ] [ ] [ ] [ ]
|
||||||
|
* H 0 1 2 3 4
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @param <E> type of elements
|
||||||
|
* @see <a href="https://en.wikipedia.org/wiki/Skip_list">Wiki. Skip list</a>
|
||||||
|
*/
|
||||||
|
public class SkipList<E extends Comparable<E>> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node before first node.
|
||||||
|
*/
|
||||||
|
private final Node<E> head;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum layers count.
|
||||||
|
* Calculated by {@link #heightStrategy}.
|
||||||
|
*/
|
||||||
|
private final int height;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function for determining height of new nodes.
|
||||||
|
* @see HeightStrategy
|
||||||
|
*/
|
||||||
|
private final HeightStrategy heightStrategy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current count of elements in list.
|
||||||
|
*/
|
||||||
|
private int size;
|
||||||
|
|
||||||
|
private static final int DEFAULT_CAPACITY = 100;
|
||||||
|
|
||||||
|
public SkipList() {
|
||||||
|
this(DEFAULT_CAPACITY, new BernoulliHeightStrategy());
|
||||||
|
}
|
||||||
|
|
||||||
|
public SkipList(int expectedCapacity, HeightStrategy heightStrategy) {
|
||||||
|
this.heightStrategy = heightStrategy;
|
||||||
|
this.height = heightStrategy.height(expectedCapacity);
|
||||||
|
this.head = new Node<>(null, this.height);
|
||||||
|
this.size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(E e) {
|
||||||
|
Objects.requireNonNull(e);
|
||||||
|
Node<E> current = head;
|
||||||
|
int layer = height;
|
||||||
|
Node<E>[] toFix = new Node[height + 1];
|
||||||
|
|
||||||
|
while (layer >= 0) {
|
||||||
|
Node<E> next = current.next(layer);
|
||||||
|
if (next == null || next.getValue().compareTo(e) > 0) {
|
||||||
|
toFix[layer] = current;
|
||||||
|
layer--;
|
||||||
|
} else {
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int nodeHeight = heightStrategy.nodeHeight(height);
|
||||||
|
Node<E> node = new Node<>(e, nodeHeight);
|
||||||
|
for (int i = 0; i <= nodeHeight; i++) {
|
||||||
|
if (toFix[i].next(i) != null) {
|
||||||
|
node.setNext(i, toFix[i].next(i));
|
||||||
|
toFix[i].next(i).setPrevious(i, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
toFix[i].setNext(i, node);
|
||||||
|
node.setPrevious(i, toFix[i]);
|
||||||
|
}
|
||||||
|
size++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public E get(int index) {
|
||||||
|
int counter = -1; // head index
|
||||||
|
Node<E> current = head;
|
||||||
|
while (counter != index) {
|
||||||
|
current = current.next(0);
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
return current.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void remove(E e) {
|
||||||
|
Objects.requireNonNull(e);
|
||||||
|
Node<E> current = head;
|
||||||
|
int layer = height;
|
||||||
|
|
||||||
|
while (layer >= 0) {
|
||||||
|
Node<E> next = current.next(layer);
|
||||||
|
if (e.equals(current.getValue())) {
|
||||||
|
break;
|
||||||
|
} else if (next == null || next.getValue().compareTo(e) > 0) {
|
||||||
|
layer--;
|
||||||
|
} else {
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i <= layer; i++) {
|
||||||
|
current.previous(i).setNext(i, current.next(i));
|
||||||
|
current.next(i).setPrevious(i, current.previous(i));
|
||||||
|
}
|
||||||
|
size--;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A search for a target element begins at the head element in the top
|
||||||
|
* list, and proceeds horizontally until the current element is greater
|
||||||
|
* than or equal to the target. If the current element is equal to the
|
||||||
|
* target, it has been found. If the current element is greater than the
|
||||||
|
* target, or the search reaches the end of the linked list, the procedure
|
||||||
|
* is repeated after returning to the previous element and dropping down
|
||||||
|
* vertically to the next lower list.
|
||||||
|
*
|
||||||
|
* @param e element whose presence in this list is to be tested
|
||||||
|
* @return true if this list contains the specified element
|
||||||
|
*/
|
||||||
|
public boolean contains(E e) {
|
||||||
|
Objects.requireNonNull(e);
|
||||||
|
Node<E> current = head;
|
||||||
|
int layer = height;
|
||||||
|
|
||||||
|
while (layer >= 0) {
|
||||||
|
Node<E> next = current.next(layer);
|
||||||
|
if (e.equals(current.getValue())) {
|
||||||
|
return true;
|
||||||
|
} else if (next == null || next.getValue().compareTo(e) > 0) {
|
||||||
|
layer--;
|
||||||
|
} else {
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int size() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print height distribution of the nodes in a manner:
|
||||||
|
* <pre>
|
||||||
|
* [ ] --- --- [ ] --- [ ]
|
||||||
|
* [ ] --- [ ] [ ] --- [ ]
|
||||||
|
* [ ] [ ] [ ] [ ] [ ] [ ]
|
||||||
|
* H 0 1 2 3 4
|
||||||
|
* </pre>
|
||||||
|
* Values of nodes is not presented.
|
||||||
|
*
|
||||||
|
* @return string representation
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
List<boolean[]> layers = new ArrayList<>();
|
||||||
|
int sizeWithHeader = size + 1;
|
||||||
|
for (int i = 0; i <= height; i++) {
|
||||||
|
layers.add(new boolean[sizeWithHeader]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Node<E> current = head;
|
||||||
|
int position = 0;
|
||||||
|
while (current != null) {
|
||||||
|
for (int i = 0; i <= current.height; i++) {
|
||||||
|
layers.get(i)[position] = true;
|
||||||
|
}
|
||||||
|
current = current.next(0);
|
||||||
|
position++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Collections.reverse(layers);
|
||||||
|
String result = layers.stream()
|
||||||
|
.map(layer -> {
|
||||||
|
StringBuilder acc = new StringBuilder();
|
||||||
|
for (boolean b : layer) {
|
||||||
|
if (b) {
|
||||||
|
acc.append("[ ]");
|
||||||
|
} else {
|
||||||
|
acc.append("---");
|
||||||
|
}
|
||||||
|
acc.append(" ");
|
||||||
|
}
|
||||||
|
return acc.toString();
|
||||||
|
})
|
||||||
|
.collect(Collectors.joining("\n"));
|
||||||
|
String positions = IntStream.range(0, sizeWithHeader - 1)
|
||||||
|
.mapToObj(i -> String.format("%3d", i))
|
||||||
|
.collect(Collectors.joining(" "));
|
||||||
|
|
||||||
|
return result + String.format("%n H %s%n", positions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value container.
|
||||||
|
* Each node have pointers to the closest nodes left and right from current
|
||||||
|
* on each layer of nodes height.
|
||||||
|
* @param <E> type of elements
|
||||||
|
*/
|
||||||
|
private static class Node<E> {
|
||||||
|
|
||||||
|
private final E value;
|
||||||
|
private final int height;
|
||||||
|
private final List<Node<E>> forward;
|
||||||
|
private final List<Node<E>> backward;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public Node(E value, int height) {
|
||||||
|
this.value = value;
|
||||||
|
this.height = height;
|
||||||
|
|
||||||
|
// predefined size lists with null values in every cell
|
||||||
|
this.forward = Arrays.asList(new Node[height + 1]);
|
||||||
|
this.backward = Arrays.asList(new Node[height + 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Node<E> next(int layer) {
|
||||||
|
checkLayer(layer);
|
||||||
|
return forward.get(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNext(int layer, Node<E> node) {
|
||||||
|
forward.set(layer, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPrevious(int layer, Node<E> node) {
|
||||||
|
backward.set(layer, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Node<E> previous(int layer) {
|
||||||
|
checkLayer(layer);
|
||||||
|
return backward.get(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public E getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkLayer(int layer) {
|
||||||
|
if (layer < 0 || layer > height) {
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Height strategy is a way of calculating maximum height for skip list
|
||||||
|
* and height for each node.
|
||||||
|
* @see BernoulliHeightStrategy
|
||||||
|
*/
|
||||||
|
public interface HeightStrategy {
|
||||||
|
int height(int expectedSize);
|
||||||
|
int nodeHeight(int heightCap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In most common skip list realisation element in layer {@code i} appears
|
||||||
|
* in layer {@code i+1} with some fixed probability {@code p}.
|
||||||
|
* Two commonly used values for {@code p} are 1/2 and 1/4.
|
||||||
|
* Probability of appearing element in layer {@code i} could be calculated
|
||||||
|
* with <code>P = p<sup>i</sup>(1 - p)</code>
|
||||||
|
* <p>
|
||||||
|
* Maximum height that would give the best search complexity
|
||||||
|
* calculated by <code>log<sub>1/p</sub>n</code>
|
||||||
|
* where {@code n} is an expected count of elements in list.
|
||||||
|
*/
|
||||||
|
public static class BernoulliHeightStrategy implements HeightStrategy {
|
||||||
|
|
||||||
|
private final double probability;
|
||||||
|
|
||||||
|
private static final double DEFAULT_PROBABILITY = 0.5;
|
||||||
|
private static final Random RANDOM = new Random();
|
||||||
|
|
||||||
|
public BernoulliHeightStrategy() {
|
||||||
|
this.probability = DEFAULT_PROBABILITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BernoulliHeightStrategy(double probability) {
|
||||||
|
if (probability <= 0 || probability >= 1) {
|
||||||
|
throw new IllegalArgumentException("Probability should be from 0 to 1. But was: " + probability);
|
||||||
|
}
|
||||||
|
this.probability = probability;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int height(int expectedSize) {
|
||||||
|
long height = Math.round(Math.log10(expectedSize) / Math.log10(1 / probability));
|
||||||
|
if (height > Integer.MAX_VALUE) {
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
return (int) height;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int nodeHeight(int heightCap) {
|
||||||
|
int level = 0;
|
||||||
|
double border = 100 * (1 - probability);
|
||||||
|
while (((RANDOM.nextInt(Integer.MAX_VALUE) % 100) + 1) > border) {
|
||||||
|
if (level + 1 >= heightCap) {
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
level++;
|
||||||
|
}
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
package com.thealgorithms.datastructures.lists;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class SkipListTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void add() {
|
||||||
|
SkipList<String> skipList = new SkipList<>();
|
||||||
|
assertEquals(0, skipList.size());
|
||||||
|
|
||||||
|
skipList.add("value");
|
||||||
|
|
||||||
|
print(skipList);
|
||||||
|
assertEquals(1, skipList.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void get() {
|
||||||
|
SkipList<String> skipList = new SkipList<>();
|
||||||
|
skipList.add("value");
|
||||||
|
|
||||||
|
String actualValue = skipList.get(0);
|
||||||
|
|
||||||
|
print(skipList);
|
||||||
|
assertEquals("value", actualValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void contains() {
|
||||||
|
SkipList<String> skipList = createSkipList();
|
||||||
|
print(skipList);
|
||||||
|
|
||||||
|
boolean contains = skipList.contains("b");
|
||||||
|
|
||||||
|
assertTrue(contains);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void remove() {
|
||||||
|
SkipList<String> skipList = createSkipList();
|
||||||
|
int initialSize = skipList.size();
|
||||||
|
print(skipList);
|
||||||
|
|
||||||
|
skipList.remove("a");
|
||||||
|
|
||||||
|
print(skipList);
|
||||||
|
assertEquals(initialSize - 1, skipList.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void checkSortedOnLowestLayer() {
|
||||||
|
SkipList<String> skipList = new SkipList<>();
|
||||||
|
String[] values = {"d", "b", "a", "c"};
|
||||||
|
Arrays.stream(values).forEach(skipList::add);
|
||||||
|
print(skipList);
|
||||||
|
|
||||||
|
String[] actualOrder = IntStream.range(0, values.length)
|
||||||
|
.mapToObj(skipList::get)
|
||||||
|
.toArray(String[]::new);
|
||||||
|
|
||||||
|
assertArrayEquals(new String[]{"a", "b", "c", "d"}, actualOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SkipList<String> createSkipList() {
|
||||||
|
SkipList<String> skipList = new SkipList<>();
|
||||||
|
String[] values = {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"};
|
||||||
|
Arrays.stream(values).forEach(skipList::add);
|
||||||
|
return skipList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print Skip List representation to console.
|
||||||
|
* Optional method not involved in testing process. Used only for visualisation purposes.
|
||||||
|
* @param skipList to print
|
||||||
|
*/
|
||||||
|
private void print(SkipList<?> skipList) {
|
||||||
|
System.out.println(skipList);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user