Java has a skip list implementation. Uneven distribution of requests

Skip Lists are a data structure that can be used in place of B-Trees. Due to the fact that the balancing algorithm is probabilistic and not strict, insertion and deletion of an element in skip lists is much easier and much faster than in balanced trees.

Skip lists are a probabilistic alternative to balanced trees. They are balanced using a random number generator. Although skip lists have poor worst-case performance, there is no sequence of operations that does this all the time (much like a random pivot quicksort). It is highly unlikely that this data structure will be significantly imbalanced (for example, for a dictionary larger than 250 items, the probability that a search will take three times the expected time is less than one millionth).

Balancing the data structure is probabilistically easier than explicitly balancing it. For many purposes, skip lists are a more natural representation of data than trees. The algorithms are simpler to implement and, in practice, faster than balanced trees. Also, skip lists are very memory efficient. They can be implemented so that one element has an average of about 1.33 pointers (or even less) and does not require storing additional information about balance or priority for each element.

To find an item in a linked list, we have to look at each of its nodes:

If the list is kept sorted and every second of its nodes additionally contains a pointer two nodes ahead, we need to look at no more than ⌈ n/ 2⌉ + 1 nodes (where n- list length):

Similarly, if now every fourth node contains a pointer to four nodes ahead, then you need to look at no more than ⌈ n/ 4⌉ + 2 nodes:

If every 2 i i nodes ahead, then the number of nodes that need to be viewed will be reduced to ⌈log 2 n⌉, and the total number of pointers in the structure will only double:

Such a data structure can be used for fast searches, but inserting and deleting nodes will be slow.

Let's call the node containing k pointers to the forward elements a node level k ... If every 2 i th node contains pointer to 2 i nodes forward, then the levels are distributed as follows: 50% of the nodes - level 1, 25% - level 2, 12.5% ​​- level 3, etc. But what happens if the levels of the nodes are chosen randomly, in the same proportions? For example, like this:

Index number i each node will refer to the next level node i or more, not exactly 2 i-1 knots forward, as it was before. Insertions and deletions will only require local changes; the node level chosen at random when you insert it will never change. Performance can be poor if tiers are poorly assigned, but we will show that such situations are rare. Due to the fact that these data structures are linked lists with additional pointers to skip intermediate nodes, I call them skip lists.

Operations

Let us describe algorithms for finding, inserting, and deleting elements in a dictionary implemented on lists with gaps. Operation search returns the value for the specified key, or signals that the key was not found. Operation inserts associates a key with a new value (and creates a key if it didn't exist before). Operation deleting removes the key. You can also easily add additional operations to this data structure, such as "find the minimum key" or "find the next key".

Each element of the list is a node, the level of which was chosen at random when it was created, and regardless of the number of elements that were already there. Level node i contains i pointers to various elements in front, indexed from 1 to i... We may not store the node level in the node itself. The number of levels is limited by a pre-selected constant MaxLevel... Let's call list level the maximum level of a node in this list (if the list is empty, then the level is 1). Heading the list (in the pictures it is on the left) contains pointers to levels from 1 to MaxLevel... If there are no such level elements yet, then the pointer value is a special NIL element.

Initialization

Let's create a NIL item whose key is greater than any key that might ever appear in the list. The NIL element will terminate all skip lists. The list level is 1, and all pointers from the header point to NIL.

Search for an item

Starting with the highest-level pointer, we move forward along the pointers until they refer to an element that does not exceed the desired one. Then we go down one level below and again move according to the same rule. If we have reached level 1 and cannot go further, then we are just in front of the element we are looking for (if there is one).

Search(list, searchKey)
x: = list -> header
# loop invariant: 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

Inserting and deleting an element

To insert or delete a node, we use the search algorithm to find all elements before the inserted (or deleted) one, then update the corresponding pointers:


In this example, we have inserted a level 2 element.

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

An array is used to store the elements before being inserted (or removed) update... Element update [i] is a pointer to the rightmost node, level i or higher, from those located to the left of the update location.

If the randomly selected level of the inserted node turned out to be greater than the level of the entire list (i.e. if there were no nodes with such a level yet), increase the level of the list and initialize the corresponding elements of the array update pointers to the header. After each deletion, we check if we have deleted the node with the maximum level and, if so, decrease the level of the list.

Level number generation

Earlier, we gave the distribution of the levels of nodes in the case when half of the nodes containing the level indicator i, also contained a pointer to a level node i+1. To get rid of the magic constant 1/2, we denote by p share of level nodes i containing a pointer to the level nodes i+ i. The level number for the new vertex is generated randomly according to the following algorithm:

randomLevel()
lvl: = 1
# random () returns a random number in a half-interval, length: "+ length);))

You can use the below code to make your own basic skipist:

1) Do start and the end before represent start and end of skip list.

2) Add the nodes and assign pointers to the next based on

If (node ​​is even) then assign a fast lane pointer with next pointer else assign only pointer to next node

Java code for a basic skip list (you can add more functionality):

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 (tem p! = 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 ();)))

Conclusion:

Simple workaround -

1 = > 2 = > 3 = > 4 = > 5 = > 6 = >

Fast forwarding a track -

2 == > 4 == > 6 == >

When you create the ConcurrentSkipListSet, you are passing a comparator to the constructor.

New ConcurrentSkipListSet<>(new ExampleComparator ()); public class ExampleComparator implements Comparator (// your impl)

You can create a comparator that will make your SkipListSet behave like a regular list.

I am not claiming that this is my own implementation. I just can't remember where I found it. If you know, let me know and I will update. This works well for me:

Public class SkipList > implements Iterable (Node _head = new Node<>(null, 33); private final Random rand = new Random (); private int _levels = 1; private AtomicInteger size = new AtomicInteger (0); ///

/// Inserts a value into the skip list. /// public void insert (T value) (// Determine the level of the new node. Generate a random number R. The // number of // 1-bits before we encounter the first 0-bit is the level of the node. / / Since R is // 32-bit, the level can be at most 32. int level = 0; size.incrementAndGet (); for (int R = rand.nextInt (); (R & 1) == 1; R >> = 1) (level ++; if (level == _levels) (_levels ++; break;)) // Insert this node into the skip list Node newNode = new Node<>(value, level + 1); Node cur = _head; for (int i = _levels - 1; i> = 0; i--) (for (; cur.next [i]! = null; cur = cur.next [i]) (if (cur.next [i] .getValue (). compareTo (value)> 0) break;) if (i<= level) { newNode.next[i] = cur.next[i]; cur.next[i] = newNode; } } } /// /// Returns whether a particular value already exists in the skip list /// public boolean contains (T value) (Node cur = _head; for (int i = _levels - 1; i> = 0; i--) (for (; cur.next [i]! = null; cur = cur.next [i]) (if (cur.next [i] .getValue (). compareTo (value)> 0) break; if (cur.next [i] .getValue (). compareTo (value) == 0) return true;)) return false; ) /// /// Attempts to remove one occurence of a particular value from the skip /// list. Returns /// whether the value was found in the skip list. /// public boolean remove (T value) (Node cur = _head; boolean found = false; for (int i = _levels - 1; i> = 0; i--) (for (; cur.next [i]! = null; cur = cur.next [i]) (if (cur.next [i] .getValue (). compareTo (value) == 0) (found = true; cur.next [i] = cur.next [i] .next [i]; break;) if (cur.next [i] .getValue () .compareTo (value)> 0) break;)) if (found) size.decrementAndGet (); return found; ) @SuppressWarnings (("rawtypes", "unchecked")) @Override public Iterator iterator () (return new SkipListIterator (this, 0);) public int size () (return size.get ();) public Double toArray () (Double a = new Double; int i = 0; for (T t: this) (a [i] = (Double) t; i ++;) return a;)) class Node > (public Node next; public N value; @SuppressWarnings ("unchecked") public Node (N value, int level) (this.value = value; next = new Node;) public N getValue () (return value;) public Node getNext () (return next;) public Node getNext (int level) (return next;) public void setNext (Node next) (this.next = next;)) class SkipListIterator > implements Iterator (SkipList list; Node current; int level; public SkipListIterator (SkipList list, int level) (this.list = list; this.current = list._head; this.level = level;) public boolean hasNext () (return current.getNext (level)! = null;) public E next () (current = current.getNext (level); return current.getValue ();) public void remove () throws UnsupportedOperationException (throw new UnsupportedOperationException ();))

Fixed bug in implementation provided by @PoweredByRice. He threw out NPE for cases where the remote node was the first node. Other updates include renamed variable names and reverse printing of the skip list order.

Import java.util.Random; interface SkippableList > (int LEVELS = 5; boolean delete (T target); void print (); void insert (T data); SkipNode search (T data); ) class SkipNode > (N data; @SuppressWarnings ("unchecked") SkipNode next = (SkipNode ) new SkipNode; SkipNode (N data) (this.data = data;) void refreshAfterDelete (int level) (SkipNode current = this; while (current! = null && current.getNext (level)! = null) (if (current.getNext (level) .data == null) (SkipNode successor = current.getNext (level) .getNext (level); current.setNext (successor, level); return; ) current = current.getNext (level); )) void setNext (SkipNode next, int level) (this.next = next;) SkipNode getNext (int level) (return this.next;) SkipNode search (N data, int level, boolean print) (if (print) (System.out.print ("Searching for:" + data + "at"); print (level);) SkipNode result = null; SkipNode current = this.getNext (level); while (current! = null && current.data.compareTo (data)< 1) { if (current.data.equals(data)) { result = current; break; } current = current.getNext(level); } return result; } void insert(SkipNodeskipNode, int level) (SkipNode current = this.getNext (level); if (current == null) (this.setNext (skipNode, level); return;) if (skipNode.data.compareTo (current.data)< 1) { this.setNext(skipNode, level); skipNode.setNext(current, level); return; } while (current.getNext(level) != null && current.data.compareTo(skipNode.data) < 1 && current.getNext(level).data.compareTo(skipNode.data) < 1) { current = current.getNext(level); } SkipNodesuccessor = current.getNext (level); current.setNext (skipNode, level); skipNode.setNext (successor, level); ) void print (int level) (System.out.print ("level" + level + ": ["); int length = 0; SkipNode current = this.getNext (level); while (current! = null) (length ++; System.out.print (current.data + ""); current = current.getNext (level);) System.out.println ("], length:" + length); )) public class SkipList > implements SkippableList (private final SkipNode head = new SkipNode<>(null); private final Random rand = new Random (); @Override public void insert (T data) (SkipNode skipNode = new SkipNode<>(data); for (int i = 0; i< LEVELS; i++) { if (rand.nextInt((int) Math.pow(2, i)) == 0) { //insert with prob = 1/(2^i) insert(skipNode, i); } } } @Override public boolean delete(T target) { System.out.println("Deleting " + target); SkipNodevictim = search (target, true); if (victim == null) return false; victim.data = null; for (int i = 0; i< LEVELS; i++) { head.refreshAfterDelete(i); } System.out.println("deleted..."); return true; } @Override public SkipNodesearch (T data) (return search (data, true);) @Override public void print () (for (int i = LEVELS-1; i> = 0; i--) (head.print (i);) System.out.println ();) private void insert (SkipNode SkipNode, int level) (head.insert (SkipNode, level);) private SkipNode search (T data, boolean print) (SkipNode result = null; for (int i = LEVELS-1; i> = 0; i--) (if ((result = head.search (data, i, print))! = null) (if (print) (System.out.println ("Found" + data.toString () + "at level" + i + ", so stopped"); System.out.println ();) break;)) return result; ) public static void main (String args) (SkipList sl = new SkipList<>(); int data = (4,2,7,0,9,1,3,7,3,4,5,6,0,2,8); for (int i: data) (sl.insert (i);) sl.print (); sl.search (4); sl.delete (4); System.out.println ("Inserting 10"); sl.insert (10); sl.print (); sl.search (10); ))

An interesting type of data structure for an efficient implementation of an ordered dictionary ADT is skip ^ cnucoK (skip list). This data structure organizes objects in random order like this

in such a way that the search and update are, on average, performed in 0 (log n) time, where n is the number of dictionary objects. Interestingly, the notion of averaged time complexity used here is independent of the possible distribution of keys upon input. On the contrary, it is dictated by the use of random number generators in the implementation of the input process to facilitate the process of deciding where to insert a new object. The execution time in this case will be equal to the average execution time of the input of any random numbers used as input objects.

Random number generation techniques are embedded in most modern computers because they are widely used in computer games, cryptography, and computer simulations. Some methods, called pseudorandom number generators, generate numbers pseudo-randomly according to some law, starting with a so-called seed. Other methods use hardware to extract "truly" random numbers. In any case, the computer has numbers that meet the requirements for random numbers in the given analysis.

The main advantage of using random numbers in the data structure and when creating an algorithm is the simplicity and reliability of the resultant structures and methods. In this way, you can create a simple randomized data structure called a skip-list that provides a logarithmic cost of search time, similar to the cost of using a binary search algorithm. However, the estimated time required for a skip list will be larger than for a binary lookup in a lookup table. On the other hand, with a dictionary update, a skip list is much faster than lookup tables.

Let a skip-list S for a dictionary D consist of a series of lists (iSo, S \, Si, S / j). Each list S \ stores a set of dictionary objects D by keys in a non-decreasing sequence plus objects with two special keys, written as "-oo" and "+ oo", where "-oo" means less, and "+ oo" - more than any a possible key that can be in D. In addition, the lists in S meet the following requirements:

List S 0 contains each dictionary object D (plus special objects with keys "-oo" and "+ oo");

For / = 1, ..., h - 1, the list Si contains (in addition to "-oo" and "+ oo") a randomly generated set of objects from the list S t _ b

The list S h contains only "-oo" and "+ oo".

An example of such a skip list is shown in Fig. 8.9, graphically representing a list S with a list at the bottom and lists S \, ..., S ^ above it. The height (height) of the list S is denoted as h.

Rice. 8.9. Example skip list

The lists are intuitively organized this way; so that? / + / contains at least every second object 5 /. As it will be shown when considering the input method, objects in St + \ are chosen arbitrarily from objects in Si so that each selected object 5 / is included in 5 / + \ with probability 1/2. Figuratively speaking, we determine whether or not to place an object from into Si + 1 depending on which side the coin tossed will fall - heads or tails. Thus, we can assume that S \ contains about π / 2 objects, S2 - π / 4, and, accordingly, Sj - about n / 2 ′ objects. In other words, the height h of the list S can be about logn. However, halving the number of objects from one list to another is not a mandatory requirement in the form of an explicit property of a skip-list. On the contrary, random selection is more important.

Using positional abstraction for lists and trees, you can think of a skip list as a two-dimensional collection of positions organized horizontally into levels and vertically into towers. Each level is a list S ^ and each tower is a set of positions that store the same object and are located on top of each other in lists. Skip list positions can be traversed using the following operations:

after (/?): returns the position following the next level; before (/?): returns the position preceding p at the same level; below (/?): returns the position below p in the same tower; above (/?): Returns the position above p in the same tower.

Let's set that the above operations should return null if the requested position does not exist. Without going into details, note that a skip-list is easily implemented using a coherent structure in such a way that the traversal methods take 0 (1) time (if there is position p in the list). Such a connected structure is a collection of h doubly linked lists, which are double linked lists, which in turn are doubly linked lists.

The skip-list structure allows simple search algorithms for dictionaries. In fact, all skip-list search algorithms rely on the fairly elegant SkipSearch method, which takes the key k and finds the object in the skip-list S with the largest key (which may be "-oo") less than or equal to k. Suppose we have the desired key k The SkipSearch method sets the position of p to the top-left position in the skip-list S. That is, p is set to the position of the special object with the "-oo" key in S ^. Then the following is done:

1) if S.below (/?) Is null, then the search ends - one level below found the largest object in S with a key less than or equal to the desired key k. Otherwise, we go down one level in this tower, setting p S.be \ ow (p);

2) from position p we move to the right (forward) until we find ourselves in the extreme right position of the current level, where keu (/?)< к. Такой шаг называется сканированием вперед (scan forward). Указанная позиция существует всегда, поскольку каждый уровень имеет специальные ключи «+оо» и «-оо». И на самом деле, после шага вправо по текущему уровню р может остаться на исходном месте. В любом случае после этого снова повторяется предыдущее действие.

Rice. 8.10. An example of a skip-list search. The 50 positions examined during the search for the key are highlighted with a bar code

Code snippet 8.4 describes the skip-list search algorithm. With such a method, it's easy to implement the findElement (/ :) operation. The operation p ^ - SkipSearch (A;) is performed and the equality key (p) ~ k is checked. If ^ is equal, /?.element is returned. Otherwise, the signaling message NO_SUCH_KEY is returned.

SkipSearch Algorithm (/ :):

Input: search key?

Output: the position of p in S whose object has the largest key less than or equal to k.

Lets say p is the top-left position in S (consisting of at least two levels), while below (p) * null do

p below (p) (scan down) while key (after (p))< к do

Let p after (p) (scan ahead) return p

Code snippet 8.4. The search algorithm in the skip-list S

It turns out that the estimated execution time of the SkipSearch algorithm is 0 (log n). Before justifying this, here is the implementation of the methods for updating the skip-list.

SKIP Is a system for monitoring the execution of orders based on MS SharePoint, which allows you to fully automate the process of working with orders. This is a well-thought-out, out-of-the-box solution for organizing work with orders. It is suitable both for work in large and geographically distributed companies, and for medium-sized companies due to the ability to implement the most flexible configuration of all modules.

The SKIP system is based on the Microsoft SharePoint platform, which automatically means that it can be integrated with Microsoft products, including Microsoft Office.

System functionality

The SKIP system is a "boxed" solution and in this version contains a basic set of functionalities necessary to automate the work with orders:

  • Appointment, execution, control of the order;
  • Tracking the status of order execution;
  • The ability to create nested ("child") orders.

Color coded order list

At the same time, the presented functionality is implemented in such a way as to provide the user with the widest possible possibilities for working with the system:

  • Cataloging of orders (one order can be located in different folders);
  • Filtering lists of orders;
  • Export of lists of orders to MS Excel;
  • Performing discipline reports;
  • Color indication of orders depending on the execution time and order status;
  • The ability to attach an arbitrary number of files of any format to the Order Card;
  • Integration with Outlook calendars;
  • Customizable notifications about the appointment and progress of work with the order;
  • The system of replacing employees for the period of vacation or business trip;
  • Creation of periodic assignments (or assignments with a schedule) for events that have a specific period (meetings, meetings, etc.);
  • Displaying the deadlines for the execution of orders on the Gantt chart;
  • etc

Task List with Gantt Chart