Linked Structures

Agenda

  1. Motives
  2. Objectives
  3. Mechanisms

1. Motives

In [242]:
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
from timeit import timeit

def time_array_front_insert_delete(n):
    return timeit('lst.insert(0, None) ; del lst[0]',
                  'lst = list(range({}))'.format(n),
                  number=1000)

ns = np.linspace(100, 10000, 50)
plt.plot(ns, [time_array_front_insert_delete(int(n)) for n in ns], 'ro')
plt.show()
In [244]:
# consider:

def concatenate(arr1, arr2):
    """Concatenates the contents of arr1 and arr2 as efficiently (time-wise)
    as possible, so that the resulting structure can be used to index all
    combined elements (arr1's followed by arr2's)."""

    # option 1:
    for x in arr2:
        arr1.append(x)
    return arr1

    # option 2:
    arr1.extend(arr2)
    return arr1

    # option 3:
    return arr1 + arr2
In [245]:
[1, 2, 3] + ['a', 'b', 'c']
Out[245]:
[1, 2, 3, 'a', 'b', 'c']

2. Objectives

We need a new data storage mechanism for constructing data structures that:

  • does not require monolithic, contiguous memory allocation,
  • allows individual elements to be flexibly and efficiently reorganized,
  • and preserves the ability to locate (e.g., via position) and iterate over elements

3. Mechanisms

3.1. Two-Element Lists

In [246]:
# data items
i1 = 'lions'
i2 = 'tigers'
i3 = 'bears'
i4 = 'oh, my'
In [247]:
i1, i2, i3, i4
Out[247]:
('lions', 'tigers', 'bears', 'oh, my')
In [248]:
# creating individual "links"
In [249]:
l1 = [i1, None]
l2 = [i2, None]
l3 = [i3, None]
l4 = [i4, None]
In [250]:
# link-ing them together
In [251]:
l1[0]
Out[251]:
'lions'
In [252]:
l1[1] = l2
l2[1] = l3
l3[1] = l4
In [253]:
l = l1
In [254]:
l[0]
Out[254]:
'lions'
In [255]:
l[1][0]
Out[255]:
'tigers'
In [256]:
l[1][1][0]
Out[256]:
'bears'
In [257]:
l[1][1][1][0]
Out[257]:
'oh, my'
In [258]:
l
Out[258]:
['lions', ['tigers', ['bears', ['oh, my', None]]]]
In [259]:
# iteration
In [260]:
link = l
while link:
    print(link[0])
    link = link[1]
    
lions
tigers
bears
oh, my
In [261]:
# prepending
In [262]:
l0 =['goats', None]
In [263]:
l0[1] = l1
In [264]:
l = l0
In [265]:
link = l
while link:
    print(link[0])
    link = link[1]
goats
lions
tigers
bears
oh, my
In [266]:
# insertion
In [267]:
#between tiger and lion

newlink =['elephant', None]

newlink = ['elephant', l[1][1]]
l[1][1] = newlink
In [268]:
link = l
while link:
    print(link[0])
    link = link[1]
goats
lions
elephant
tigers
bears
oh, my

3.2. "Link" objects

In [269]:
class Link:
    def __init__(self, val, next=None):
        self.val = val
        self.next = next
In [270]:
# manually constructing a list
In [271]:
l1 = Link(i1)
l2 = Link(i2)
l3 = Link(i3)
l4 = Link(i4)
In [272]:
l1.next = l2
l2.next = l3
l3.next = l4
In [273]:
l = l1
In [274]:
l.val
Out[274]:
'lions'
In [275]:
l.next.val
Out[275]:
'tigers'
In [276]:
l.next.next.val
Out[276]:
'bears'
In [277]:
head = Link(i4)
In [278]:
#prepend an element

head = Link(i3, next=head) #next= optional, head = Link(i3, head)
In [279]:
head = Link(i2, next=head)
In [280]:
head = Link(i1, next=head)
In [281]:
head.val
Out[281]:
'lions'
In [282]:
head.next.val
Out[282]:
'tigers'
In [283]:
# iteration
In [284]:
link = head

while link:
    print(link.val)
    link = link.next
    
lions
tigers
bears
oh, my
In [285]:
# prepend an element

head = Link('goat', next = head)
In [286]:
# iteration based on a recursive pattern
In [287]:
class LinkedList:
    def __init__(self):
        self.head = None
        
    def prepend(self, val):
        self.head = Link(val, next=self.head)
        
    def __iter__(self):
        cursor = self.head
        while cursor:
            yield cursor.val
            cursor = cursor.next
            
    def __getitem__(self, idx):
        cursor = self.head
        for _ in range(idx):
            cursor = cursor.next
        return cursor.val
    
    def __repr__(self):
        return '[' + ', '.join(str(x) for x in self) + ']'
In [288]:
l = LinkedList()
In [289]:
l.prepend(0)
In [290]:
l
Out[290]:
[0]
In [291]:
for x in range(1,10):
    l.prepend(x)
In [292]:
l
Out[292]:
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
In [293]:
l[2] #not working 
Out[293]:
7
In [294]:
class LinkedList:
    def __init__(self):
        self.head = None
        
    def prepend(self, val):
        self.head = Link(val, next=self.head)
        
    def __iter__(self):
        cursor = self.head
        while cursor:
            yield cursor.val
            cursor = cursor.next
            
    def __repr__(self):
        return '[' + ', '.join(str(x) for x in self) + ']'
    
    def __getitem__(self, idx):
        cursor = self.head
        for _ in range(idx):
            cursor = cursor.next
        return cursor.val
In [295]:
l = LinkedList()

for x in range(0,10):
    l.prepend(x)
In [296]:
l
Out[296]:
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
In [297]:
l[3]
Out[297]:
6
In [298]:
class BinaryLink:
    def __init__(self, val, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right
In [299]:
# manual construction of a "tree"
In [300]:
t = BinaryLink('hello', left = BinaryLink('world'), right = BinaryLink('universe'))
In [301]:
t.val
Out[301]:
'hello'
In [302]:
t.right.val
Out[302]:
'universe'
In [303]:
t.left.val
Out[303]:
'world'
In [304]:
def tree_iter(root):
    if root:
        yield root.val
        yield from tree_iter(root.left) #self-reading: yield from
        yield from tree_iter(root.right)
In [305]:
for x in tree_iter(t):
    print(x)
hello
world
universe
In [306]:
#self-studying

class NaryLink:
    def __init__(self, val, n=2):
        self.val = val
        self.children = [None] * n
        
    def __getitem__(self, idx):
        return self.children[idx]
    
    def __setitem__(self, idx, val):
        self.children[idx] = val
        
    def __iter__(self):
        for c in self.children:
            yield c
In [307]:
root = NaryLink('Kingdoms', n=5)

root[0] = NaryLink('Animalia', n=35)
root[1] = NaryLink('Plantae', n=12)
root[2] = NaryLink('Fungi', n=7)
root[3] = NaryLink('Protista', n=5)
root[4] = NaryLink('Monera', n=5)

root[2][0] = NaryLink('Chytridiomycota')
root[2][1] = NaryLink('Blastocladiomycota')
root[2][2] = NaryLink('Glomeromycota')
root[2][3] = NaryLink('Ascomycota')
root[2][4] = NaryLink('Basidiomycota')
root[2][5] = NaryLink('Microsporidia')
root[2][6] = NaryLink('Neocallimastigomycota')

def tree_iter(root):
    if root:
        yield root.val
        for c in root:
            yield from tree_iter(c)
In [308]:
for x in tree_iter(root):
    print(x)
Kingdoms
Animalia
Plantae
Fungi
Chytridiomycota
Blastocladiomycota
Glomeromycota
Ascomycota
Basidiomycota
Microsporidia
Neocallimastigomycota
Protista
Monera