diff --git a/src/main/java/com/thealgorithms/datastructures/lists/SkipList.java b/src/main/java/com/thealgorithms/datastructures/lists/SkipList.java new file mode 100644 index 000000000..5cd14f9dd --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/lists/SkipList.java @@ -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. + *

+ * 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. + *

+ * [ ] ------> [ ] --> [ ]
+ * [ ] --> [ ] [ ] --> [ ]
+ * [ ] [ ] [ ] [ ] [ ] [ ]
+ *  H   0   1   2   3   4
+ * 
+ * + * @param type of elements + * @see Wiki. Skip list + */ +public class SkipList> { + + /** + * Node before first node. + */ + private final Node 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 current = head; + int layer = height; + Node[] toFix = new Node[height + 1]; + + while (layer >= 0) { + Node 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 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 current = head; + while (counter != index) { + current = current.next(0); + counter++; + } + return current.value; + } + + public void remove(E e) { + Objects.requireNonNull(e); + Node current = head; + int layer = height; + + while (layer >= 0) { + Node 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 current = head; + int layer = height; + + while (layer >= 0) { + Node 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: + *
+     * [ ] --- --- [ ] --- [ ]
+     * [ ] --- [ ] [ ] --- [ ]
+     * [ ] [ ] [ ] [ ] [ ] [ ]
+     *  H   0   1   2   3   4
+     * 
+ * Values of nodes is not presented. + * + * @return string representation + */ + @Override + public String toString() { + List layers = new ArrayList<>(); + int sizeWithHeader = size + 1; + for (int i = 0; i <= height; i++) { + layers.add(new boolean[sizeWithHeader]); + } + + Node 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 type of elements + */ + private static class Node { + + private final E value; + private final int height; + private final List> forward; + private final List> 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 next(int layer) { + checkLayer(layer); + return forward.get(layer); + } + + public void setNext(int layer, Node node) { + forward.set(layer, node); + } + + public void setPrevious(int layer, Node node) { + backward.set(layer, node); + } + + public Node 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 P = pi(1 - p) + *

+ * Maximum height that would give the best search complexity + * calculated by log1/pn + * 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; + } + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/lists/SkipListTest.java b/src/test/java/com/thealgorithms/datastructures/lists/SkipListTest.java new file mode 100644 index 000000000..b26de0c55 --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/lists/SkipListTest.java @@ -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 skipList = new SkipList<>(); + assertEquals(0, skipList.size()); + + skipList.add("value"); + + print(skipList); + assertEquals(1, skipList.size()); + } + + @Test + void get() { + SkipList skipList = new SkipList<>(); + skipList.add("value"); + + String actualValue = skipList.get(0); + + print(skipList); + assertEquals("value", actualValue); + } + + @Test + void contains() { + SkipList skipList = createSkipList(); + print(skipList); + + boolean contains = skipList.contains("b"); + + assertTrue(contains); + } + + @Test + void remove() { + SkipList skipList = createSkipList(); + int initialSize = skipList.size(); + print(skipList); + + skipList.remove("a"); + + print(skipList); + assertEquals(initialSize - 1, skipList.size()); + } + + @Test + void checkSortedOnLowestLayer() { + SkipList 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 createSkipList() { + SkipList 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); + } +}