CS331 - Datastructures and Algorithms

Version 1

Course webpage for CS331

Linked Lists

Agenda

  1. The LinkedList and Node classes
  2. Implementing append
  3. Implementing deletion
  4. Bidirectional links
  5. Run-time analysis
  6. 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]
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)\)
Last updated on Monday, March 8, 2021
Published on Wednesday, March 3, 2021
 Edit on GitHub