Linked Lists
Agenda
- The
LinkedList
andNode
classes - Implementing
append
- Implementing deletion
- Bidirectional links
- Run-time analysis
- Closing remarks
1. The LinkedList
and Node
classes
class LinkedList:
class Node:
def __init__(self, val, next=None):
self.val = val
self.next = next
def __init__(self):
self.head = None
self.len = 0
def __len__(self): # O(n)
return self.len
def normalize_index(self,i):
assert(i >= -len(self) and i < len(self))
if i < 0: # -i to accessing from back of list
i = len(self) + i
return i
def find_link(self, pos): # O(n)
assert(pos >= 0 and pos < len(self))
cur = self.head
print(pos)
for i in range(0,pos):
cur = cur.next
if not cur:
raise IndexError()
return cur
def __getitem__(self, index):
nindex = self.normalize_index(index)
# print(nindex)
return self.find_link(nindex).val
def __setitem__(self, index, val):
nindex = self.normalize_index(index)
cur = self.find_link(nindex)
cur.val = val
def prepend(self, val):
self.head = self.Node(val,self.head)
self.len += 1
def append(self, val):
self.insert(len(self),val)
def insert(self, pos, val): # O(n)
if pos != len(self):
npos = self.normalize_index(pos)
else:
npos = len(self)
assert(npos >= 0 and npos <= len(self))
if npos == 0:
self.prepend(val)
else:
link = self.find_link(npos - 1) # call to find_link is O(n)
newcell = self.Node(val, link.next)
link.next = newcell
self.len += 1
def __delitem__(self, pos): # O(n)
npos = self.normalize_index(pos)
assert(npos >= 0 and npos < len(self))
if npos == 0:
self.head = self.head.next
else:
cur = self.find_link(npos - 1) # call to find_link is O(n)
cur.next = cur.next.next
self.len += -1
def __iter__(self):
cur = self.head
while cur:
yield cur.val
cur = cur.next
def concat(self,other):
# if len(self) == 0:
# self.head = other.head
# else:
# self.tail = other.tail
for el in other: # n
self.insert(self.len,el) # n * n = O(n^2)
def reserve(self): # for example [1,2,3] -> [3,2,1] O(n)
pass # return reversed list
def __repr__(self):
return '[' + ', '.join(str(x) for x in self) + ']'
lst = LinkedList()
for i in range(3):
lst.prepend(i)
lst
[2, 1, 0]
2. Implementing append
- actual implementations are above
Option 1 (only append)
lst = LinkedList()
for i in range(10):
lst.append(i)
lst
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Option 2 (append and prepend)
lst = LinkedList()
for i in range(2):
lst.append(i)
lst
[0, 1]
3. Implementing deletion
Deleting the head
class LinkedList (LinkedList):
def del_head(self):
assert(len(self) > 0)
del self[0]
lst = LinkedList()
for i in range(10):
lst.append(i)
lst.del_head()
lst.del_head()
lst
[2, 3, 4, 5, 6, 7, 8, 9]
Deleting the tail
class LinkedList (LinkedList):
def del_tail(self):
assert(len(self) > 0)
del self[len(self) - 1]
lst = LinkedList()
for i in range(10):
lst.append(i)
lst.del_tail()
lst.del_tail()
lst
[0, 1, 2, 3, 4, 5, 6, 7]
4. Bidirectional links (Doubly-linked list) & Sentinel head
class LinkedList:
class Node:
def __init__(self, val, prior=None, next=None):
self.val = val
self.prior = prior
self.next = next
def __init__(self):
self.count = 0
self.head = self.Node(None)
self.head.next = self.head
self.head.prior = self.head
def prepend(self, value): # O(1)
self.count += 1
newn = self.Node(value, prior = self.head, next = self.head.next)
self.head.next.prior = newn
self.head.next = newn
def append(self, value): # O(1)
self.count += 1
newn = self.Node(value, prior = self.head.prior, next = self.head)
self.head.prior.next = newn
self.head.prior = newn
def __getitem__(self, idx): # n = O(n), but we can do it in n/2
# Write n/2 (first half access though next, second half access through prior)
assert(idx >= 0 and idx < len(self))
n = self.head.next
for i in range(0,idx):
n = n.next
return n.val
def __len__(self):
return self.count
def __iter__(self):
n = self.head.next
while n is not self.head:
yield n.val
n = n.next
def __repr__(self):
return '[' + ', '.join(str(x) for x in self) + ']'
lst = LinkedList()
for i in range(10):
lst.prepend(i)
for i in range(10):
lst.append(i)
lst
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
5. Incorporating a “cursor”
class LinkedList:
class Node:
def __init__(self, val, prior=None, next=None):
self.val = val
self.prior = prior
self.next = next
def __init__(self):
self.count = 0
self.head = self.Node(None)
self.head.next = self.head
self.head.prior = self.head
self.cursor = None
def insert_after(self, n, x):
if not n:
raise Exception("Need a cell to insert after!")
self.count += 1
newn = self.Node(x, prior = n, next = n.next)
n.next.prior = newn
n.next = newn
def append(self, value): # O(1)
self.insert_after(self.head.prior, value)
def get_cell(self, idx):
assert(idx >= 0 and idx < len(self))
n = self.head.next
for i in range(0,idx):
n = n.next
return n
def __getitem__(self, idx): # n = O(n), but we can do it in n/2
# Write n/2 (first half access though next, second half access through prior)
n = self.get_cell(idx)
return n.val
def cursor_set(self, idx):
self.cursor = self.get_cell(idx)
def cursor_get(self):
if not self.cursor:
raise Exception("Cursor has not been set yet!")
return self.cursor.val
def cursor_move_backwards(self, n):
if not self.cursor and self.count > 0:
raise Exception("Cursor has not been set yet!")
for i in range(0,n):
self.cursor = self.cursor.prior
if self.cursor == self.head:
self.cursor = self.cursor.prior
def cursor_move_forwards(self, n):
if not self.cursor and self.count > 0:
raise Exception("Cursor has not been set yet!")
for i in range(0,n):
self.cursor = self.cursor.next
if self.cursor == self.head:
self.cursor = self.cursor.next
def cursor_insert(self, x):
if not self.cursor:
raise Exception("Cursor has not been set yet!")
self.insert_after(self.cursor, x)
def delete_cell(self, n):
if not n and not (n == self.head):
raise Exception("Need a cell to insert after!")
n.prior.next = n.next
n.next.prior = n.prior
self.count += -1
def cursor_delete(self):
self.delete_cell(self.cursor)
self.cursor = self.cursor.next
if self.cursor == self.head:
self.cursor = self.head.next
if self.head.next == self.head:
self.cursor == None
def __len__(self):
return self.count
def __iter__(self):
n = self.head.next
while n is not self.head:
yield n.val
n = n.next
def __repr__(self):
return '[' + ', '.join(str(x) for x in self) + ']'
lst = LinkedList()
for i in range(10):
lst.append(i)
lst
for x in lst:
print(x)
lst.cursor_set(4)
for x in 'abcd':
lst.cursor_insert(x)
lst
[0, 1, 2, 3, 4, d, c, b, a, 5, 6, 7, 8, 9]
lst.cursor_set(8)
for _ in range(4):
lst.cursor_delete()
lst
[0, 1, 2, 3, 4, d, c, b, 8, 9]
6. Run-time analysis
Run-time complexities for circular, doubly-linked list of \(N\) elements:
- indexing (position-based access) = \(O(n)\)
- search (unsorted) = \(O(n)\)
- search (sorted) = \(O(n)\) — binary search isn’t possible!
- prepend = \(O(1)\)
- append = \(O(1)\) with single linked list \(O(n)\)
- insertion at arbitrary position: indexing = \(O(n)\) + insertion = \(O(1)\)
- deletion of arbitrary element: indexing = \(O(n)\) + deletion = \(O(1)\)