Skip to content

Adding skiplist #328

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Feb 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion pydatastructs/linear_data_structures/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
SinglyLinkedList,
DoublyLinkedList,
SinglyCircularLinkedList,
DoublyCircularLinkedList
DoublyCircularLinkedList,
SkipList
)
__all__.extend(linked_lists.__all__)

Expand Down
209 changes: 199 additions & 10 deletions pydatastructs/linear_data_structures/linked_lists.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from pydatastructs.utils.misc_util import _check_type, LinkedListNode
from pydatastructs.utils.misc_util import _check_type, LinkedListNode, SkipNode
import math, random

__all__ = [
'SinglyLinkedList',
'DoublyLinkedList',
'SinglyCircularLinkedList',
'DoublyCircularLinkedList'
'DoublyCircularLinkedList',
'SkipList'
]

class LinkedList(object):
Expand Down Expand Up @@ -219,12 +221,12 @@ class DoublyLinkedList(LinkedList):
>>> dll.append(5)
>>> dll.appendleft(2)
>>> str(dll)
"['2', '6', '5']"
"['(2, None)', '(6, None)', '(5, None)']"
>>> dll[0].key = 7.2
>>> dll.extract(1).key
6
>>> str(dll)
"['7.2', '5']"
"['(7.2, None)', '(5, None)']"

References
==========
Expand Down Expand Up @@ -290,6 +292,11 @@ def insert_at(self, index, key, data=None):
if self.size == 1:
self.head, self.tail = \
new_node, new_node
elif index == self.size - 1:
new_node.prev = self.tail
new_node.next = self.tail.next
self.tail.next = new_node
self.tail = new_node
else:
counter = 0
current_node = self.head
Expand Down Expand Up @@ -354,12 +361,12 @@ class SinglyLinkedList(LinkedList):
>>> sll.append(5)
>>> sll.appendleft(2)
>>> str(sll)
"['2', '6', '5']"
"['(2, None)', '(6, None)', '(5, None)']"
>>> sll[0].key = 7.2
>>> sll.extract(1).key
6
>>> str(sll)
"['7.2', '5']"
"['(7.2, None)', '(5, None)']"

References
==========
Expand Down Expand Up @@ -409,6 +416,10 @@ def insert_at(self, index, key, data=None):
if self.size == 1:
self.head, self.tail = \
new_node, new_node
elif index == self.size - 1:
new_node.next = self.tail.next
self.tail.next = new_node
self.tail = new_node
else:
counter = 0
current_node = self.head
Expand Down Expand Up @@ -469,12 +480,12 @@ class SinglyCircularLinkedList(SinglyLinkedList):
>>> scll.append(5)
>>> scll.appendleft(2)
>>> str(scll)
"['2', '6', '5']"
"['(2, None)', '(6, None)', '(5, None)']"
>>> scll[0].key = 7.2
>>> scll.extract(1).key
6
>>> str(scll)
"['7.2', '5']"
"['(7.2, None)', '(5, None)']"

References
==========
Expand Down Expand Up @@ -528,12 +539,12 @@ class DoublyCircularLinkedList(DoublyLinkedList):
>>> dcll.append(5)
>>> dcll.appendleft(2)
>>> str(dcll)
"['2', '6', '5']"
"['(2, None)', '(6, None)', '(5, None)']"
>>> dcll[0].key = 7.2
>>> dcll.extract(1).key
6
>>> str(dcll)
"['7.2', '5']"
"['(7.2, None)', '(5, None)']"

References
==========
Expand Down Expand Up @@ -581,3 +592,181 @@ def extract(self, index):
elif index == 0:
self.tail.next = self.head
return node

class SkipList(object):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why it doesn't inherit LinkedList?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope skip list having a linkedlist implementation will lead to another class of sorted linked list or sorting the linkedlist each insertion and maintaining the layer will obviously need the help of tree like structure, it's better to implement them in separate node which can act same.

"""
Represents Skip List

Examples
========

>>> from pydatastructs import SkipList
>>> sl = SkipList()
>>> sl.insert(6)
>>> sl.insert(1)
>>> sl.insert(3)
>>> node = sl.extract(1)
>>> str(node)
'(1, None)'
>>> sl.insert(4)
>>> sl.insert(2)
>>> sl.search(4)
True
>>> sl.search(10)
False

"""

__slots__ = ['head', 'tail', '_levels', '_num_nodes', 'seed']

def __new__(cls):
obj = object.__new__(cls)
obj.head, obj.tail = None, None
obj._num_nodes = 0
obj._levels = 0
obj._add_level()
return obj

@classmethod
def methods(cls):
return ['__new__', 'levels', 'search',
'extract', '__str__', 'size']

def _add_level(self):
self.tail = SkipNode(math.inf, next=None, down=self.tail)
self.head = SkipNode(-math.inf, next=self.tail, down=self.head)
self._levels += 1

@property
def levels(self):
"""
Returns the number of levels in the
current skip list.
"""
return self._levels

def _search(self, key) -> list:
path = []
node = self.head
while node:
if node.next.key >= key:
path.append(node)
node = node.down
else:
node = node.next
return path

def search(self, key) -> bool:
return self._search(key)[-1].next.key == key

def insert(self, key, data=None):
"""
Inserts a new node to the skip list.

Parameters
==========

key
Any valid identifier to uniquely
identify the node in the linked list.

data
Any valid data to be stored in the node.
"""
path = self._search(key)
tip = path[-1]
below = SkipNode(key=key, data=data, next=tip.next)
tip.next = below
total_level = self._levels
level = 1
while random.getrandbits(1) % 2 == 0 and level <= total_level:
if level == total_level:
self._add_level()
prev = self.head
else:
prev = path[total_level - 1 - level]
below = SkipNode(key=key, data=None, next=prev.next, down=below)
prev.next = below
level += 1
self._num_nodes += 1

@property
def size(self):
return self._num_nodes

def extract(self, key):
"""
Extracts the node with the given key in the skip list.

Parameters
==========

key
The key of the node under consideration.

Returns
=======

return_node: SkipNode
The node with given key.
"""
path = self._search(key)
tip = path[-1]
if tip.next.key != key:
raise KeyError('Node with key %s is not there in %s'%(key, self))
return_node = SkipNode(tip.next.key, tip.next.data)
total_level = self._levels
level = total_level - 1
while level >= 0 and path[level].next.key == key:
path[level].next = path[level].next.next
level -= 1
walk = self.head
while walk is not None:
if walk.next is self.tail:
self._levels -= 1
self.head = walk.down
self.tail = self.tail.down
walk = walk.down
else:
break
self._num_nodes -= 1
if self._levels == 0:
self._add_level()
return return_node

def __str__(self):
node2row = {}
node2col = {}
walk = self.head
curr_level = self._levels - 1
while walk is not None:
curr_node = walk
col = 0
while curr_node is not None:
if curr_node.key != math.inf and curr_node.key != -math.inf:
node2row[curr_node] = curr_level
if walk.down is None:
node2col[curr_node.key] = col
col += 1
curr_node = curr_node.next
walk = walk.down
curr_level -= 1
print(self._num_nodes, self._levels)
sl_mat = [[str(None) for _ in range(self._num_nodes)] for _ in range(self._levels)]
walk = self.head
while walk is not None:
curr_node = walk
while curr_node is not None:
if curr_node in node2row:
row = node2row[curr_node]
col = node2col[curr_node.key]
sl_mat[row][col] = str(curr_node)
curr_node = curr_node.next
walk = walk.down
sl_str = ""
for level_list in sl_mat[::-1]:
for node_str in level_list:
sl_str += node_str + " "
if len(sl_str) > 0:
sl_str += "\n"
return sl_str
53 changes: 48 additions & 5 deletions pydatastructs/linear_data_structures/tests/test_linked_lists.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pydatastructs.linear_data_structures import DoublyLinkedList, SinglyLinkedList, SinglyCircularLinkedList, DoublyCircularLinkedList
from pydatastructs.linear_data_structures import DoublyLinkedList, SinglyLinkedList, SinglyCircularLinkedList, DoublyCircularLinkedList, SkipList
from pydatastructs.utils.raises_util import raises
import copy, random

Expand All @@ -22,7 +22,9 @@ def test_DoublyLinkedList():
assert dll.search(3) == dll[-2]
assert dll.search(-1) is None
dll[-2].key = 0
assert str(dll) == "['7', '5', '1', '6', '1.1', '0', '9']"
assert str(dll) == ("['(7, None)', '(5, None)', '(1, None)', "
"'(6, None)', '(1.1, None)', '(0, None)', "
"'(9, None)']")
assert len(dll) == 7
assert raises(IndexError, lambda: dll.insert_at(8, None))
assert raises(IndexError, lambda: dll.extract(20))
Expand Down Expand Up @@ -57,7 +59,8 @@ def test_SinglyLinkedList():
assert sll.popleft().key == 2
assert sll.popright().key == 6
sll[-2].key = 0
assert str(sll) == "['2', '4', '1', '0', '9']"
assert str(sll) == ("['(2, None)', '(4, None)', '(1, None)', "
"'(0, None)', '(9, None)']")
assert len(sll) == 5
assert raises(IndexError, lambda: sll.insert_at(6, None))
assert raises(IndexError, lambda: sll.extract(20))
Expand Down Expand Up @@ -93,7 +96,8 @@ def test_SinglyCircularLinkedList():
assert scll.popright().key == 6
assert scll.search(-1) is None
scll[-2].key = 0
assert str(scll) == "['2', '4', '1', '0', '9']"
assert str(scll) == ("['(2, None)', '(4, None)', '(1, None)', "
"'(0, None)', '(9, None)']")
assert len(scll) == 5
assert raises(IndexError, lambda: scll.insert_at(6, None))
assert raises(IndexError, lambda: scll.extract(20))
Expand Down Expand Up @@ -130,7 +134,9 @@ def test_DoublyCircularLinkedList():
assert dcll.popleft().key == 2
assert dcll.popright().key == 4
dcll[-2].key = 0
assert str(dcll) == "['7', '5', '1', '6', '1', '0', '9']"
assert str(dcll) == ("['(7, None)', '(5, None)', '(1, None)', "
"'(6, None)', '(1, None)', '(0, None)', "
"'(9, None)']")
assert len(dcll) == 7
assert raises(IndexError, lambda: dcll.insert_at(8, None))
assert raises(IndexError, lambda: dcll.extract(20))
Expand All @@ -148,3 +154,40 @@ def test_DoublyCircularLinkedList():
dcll_copy.extract(index)
assert str(dcll_copy) == "[]"
assert raises(ValueError, lambda: dcll_copy.extract(1))

def test_SkipList():
random.seed(0)
sl = SkipList()
sl.insert(2)
sl.insert(10)
sl.insert(92)
sl.insert(1)
sl.insert(4)
sl.insert(27)
sl.extract(10)
assert str(sl) == ("(1, None) None None None None \n"
"(1, None) None None None None \n"
"(1, None) (2, None) (4, None) (27, None) (92, None) \n")
assert raises(KeyError, lambda: sl.extract(15))
assert sl.search(1) is True
assert sl.search(47) is False

sl = SkipList()

for a in range(0, 20, 2):
sl.insert(a)
assert sl.search(16) is True
for a in range(4, 20, 4):
sl.extract(a)
assert sl.search(10) is True
for a in range(4, 20, 4):
sl.insert(a)
for a in range(0, 20, 2):
sl.extract(a)
assert sl.search(3) is False

li = SkipList()
li.insert(1)
li.insert(2)
assert li.levels == 1
assert li.size == 2
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ def test_LinkedListQueue():
q1 = Queue(implementation='linked_list', items = [0, 1])
q1.append(2)
q1.append(3)
assert str(q1) == "['0', '1', '2', '3']"
assert str(q1) == ("['(0, None)', '(1, None)', "
"'(2, None)', '(3, None)']")
assert len(q1) == 4
assert q1.popleft().key == 0
assert q1.popleft().key == 1
Expand Down Expand Up @@ -83,7 +84,7 @@ def test_LinkedListQueue():
q1 = Queue(implementation='linked_list', items = [0, 1], double_ended=True)
q1.appendleft(2)
q1.append(3)
assert str(q1) == "['2', '0', '1', '3']"
assert str(q1) == "['(2, None)', '(0, None)', '(1, None)', '(3, None)']"
assert len(q1) == 4
assert q1.popleft().key == 2
assert q1.pop().key == 3
Expand Down
Loading