Python lists are implemented as a "dynamic array", which isn't optimal for all use cases.
😠Inserting an element is slow, especially near front of list:
😠Plus inserting too many elements can require re-creating the entire list in memory, if it exceeds the pre-allocated memory.
A linked list is a chain of objects where each object holds a value and a reference to the next link. The list ends when the final reference is empty.
Linked lists require more space but provide faster insertion.
timeit.timeit(lambda: f(range(10,000)), number=10)
Operation | Linked List | list | list advantage |
Insert at head | 0.0249 | 0.1235 | 0.23 |
Insert at mid | 18.985 | 0.0630 | 296. |
Insert at end | 24.429 | 0.0025 | 10187. |
Search mid | 0.0251 | 0.0015 | 16. |
Index mid | 0.02556 | 0.00113 | 21. |
Length | 0.02968 | 0.00116 | 23. |
Sum | 0.03016 | 0.00184 | 16. |
Linked lists require more space and provide slower insertion and transversal.
class Link:
empty = ()
def __init__(self, first, rest=empty):
self.first = first
self.rest = rest
How would we use that?
ll = Link("A", Link("B", Link("C")))
class Link:
"""A linked list."""
empty = ()
def __init__(self, first, rest=empty):
assert rest is Link.empty or isinstance(rest, Link)
self.first = first
self.rest = rest
def __repr__(self):
if self.rest:
rest_repr = ', ' + repr(self.rest)
else:
rest_repr = ''
return 'Link(' + repr(self.first) + rest_repr + ')'
def __str__(self):
string = '<'
while self.rest is not Link.empty:
string += str(self.first) + ' '
self = self.rest
return string + str(self.first) + '>'
Similar to [x for x in range(3, 6)]
def range_link(start, end):
"""Return a Link containing consecutive integers
from start to end, not including end.
>>> range_link(3, 6)
Link(3, Link(4, Link(5)))
"""
if start >= end:
return Link.empty
return Link(start, range_link(start + 1, end))
Similar to [f(x) for x in lst]
def map_link(f, ll):
"""Return a Link that contains f(x) for each x in Link LL.
>>> square = lambda x: x * x
>>> map_link(square, range_link(3, 6))
Link(9, Link(16, Link(25)))
"""
Similar to [f(x) for x in lst]
def map_link(f, ll):
"""Return a Link that contains f(x) for each x in Link LL.
>>> square = lambda x: x * x
>>> map_link(square, range_link(3, 6))
Link(9, Link(16, Link(25)))
"""
if ll is Link.empty:
return Link.empty
return Link(f(ll.first), map_link(f, ll.rest))
Similar to [x for x in lst if f(x)]
def filter_link(f, ll):
"""Return a Link that contains only the elements x of Link LL
for which f(x) is a true value.
>>> is_odd = lambda x: x % 2 == 1
>>> filter_link(is_odd, range_link(3, 6))
Link(3, Link(5))
"""
Similar to [x for x in lst if f(x)]
def filter_link(f, ll):
"""Return a Link that contains only the elements x of Link LL
for which f(x) is a true value.
>>> is_odd = lambda x: x % 2 == 1
>>> filter_link(is_odd, range_link(3, 6))
Link(3, Link(5))
"""
if ll is Link.empty:
return Link.empty
elif f(ll.first):
return Link(ll.first, filter_link(f, ll.rest))
return filter_link(f, ll.rest)
Attribute assignments can change first
and
rest
attributes of a Link
.
s = Link("A", Link("B", Link("C")))
s.first = "Hi"
s.rest.first = "Hola"
s.rest.rest.first = "Oi"
The rest of a linked list can contain the linked list as a sub-list.
s = Link("A", Link("B", Link("C")))
t = s.rest
t.rest = s
s.first # 'A'
s.rest.rest.rest.rest.rest.first # 'B'
def insert_front(linked_list, new_val):
"""Inserts new_val in front of linked_list,
returning new linked list.
>>> ll = Link(1, Link(3, Link(5)))
>>> insert_front(ll, 0)
Link(0, Link(1, Link(3, Link(5))))
"""
def insert_front(linked_list, new_val):
"""Inserts new_val in front of linked_list,
returning new linked list.
>>> ll = Link(1, Link(3, Link(5)))
>>> insert_front(ll, 0)
Link(0, Link(1, Link(3, Link(5))))
"""
return Link(new_val, linked_list)
def add(ordered_list, new_val):
"""Add new_val to ordered_list, returning modified ordered_list.
>>> s = Link(1, Link(3, Link(5)))
>>> add(s, 0)
Link(0, Link(1, Link(3, Link(5))))
>>> add(s, 3)
Link(0, Link(1, Link(3, Link(5))))
>>> add(s, 4)
Link(0, Link(1, Link(3, Link(4, Link(5)))))
>>> add(s, 6)
Link(0, Link(1, Link(3, Link(4, Link(5, Link(6))))))
"""
if new_val < ordered_list.first:
elif new_val > ordered_list.first and ordered_list.rest is Link.empty:
elif new_val > ordered_list.first:
return ordered_list
def add(ordered_list, new_val):
"""Add new_val to ordered_list, returning modified ordered_list.
>>> s = Link(1, Link(3, Link(5)))
>>> add(s, 0)
Link(0, Link(1, Link(3, Link(5))))
>>> add(s, 3)
Link(0, Link(1, Link(3, Link(5))))
>>> add(s, 4)
Link(0, Link(1, Link(3, Link(4, Link(5)))))
>>> add(s, 6)
Link(0, Link(1, Link(3, Link(4, Link(5, Link(6))))))
"""
if new_val < ordered_list.first:
original_first = ordered_list.first
ordered_list.first = new_val
ordered_list.rest = Link(original_first, ordered_list.rest)
elif new_val > ordered_list.first and ordered_list.rest is Link.empty:
ordered_list.rest = Link(new_val)
elif new_val > ordered_list.first:
add(ordered_list.rest, new_val)
return ordered_list
The challenge:
Version | 10,000 runs | 100,000 runs |
---|---|---|
Python list | 2.6 seconds | 37 seconds |
Link | 0.01 seconds | 0.1 |
Try it yourself on your local machine (Legit Python!): warandpeace.py
But, see our preformance comparison with other operators.
This is what we've been using:
tree(label, branches) |
Returns a tree with given LABEL at its root, whose branches are BRANCHES |
label(tree) |
Returns the label of root node of TREE |
branches(tree) |
Returns the branches of TREE (each a tree). |
is_leaf(tree) |
Returns true if TREE is a leaf node. |
Using an implementation like this:
def tree(label, branches=[]):
return [label] + list(branches)
def label(tree):
return tree[0]
def branches(tree):
return tree[1:]
def is_leaf(tree):
return not branches(tree)
🤔 How could we represent trees as a Python class?
class Tree:
def __init__(self, label, branches=[]):
self.label = label
self.branches = list(branches)
def is_leaf(self):
return not self.branches
🤔 What's different? What's the same?
tree | Tree |
---|---|
t = tree(label, branches=[]) |
t = Tree(label, branches=[]) |
branches(t) |
t.branches
|
label(t) |
t.label
|
is_leaf(t) |
t.is_leaf() |
def fib_tree(n):
if n == 0 or n == 1:
return tree(n)
else:
left = fib_tree(n - 2)
right = fib_tree(n - 1)
fib_n = label(left) + label(right)
return tree(fib_n, [left, right])
def fib_tree(n):
if n == 0 or n == 1:
return Tree(n)
else:
left = fib_tree(n - 2)
right = fib_tree(n - 1)
fib_n = left.label + right.label
return Tree(fib_n, [left, right])
This is what assignments actually use:
class Tree:
def __init__(self, label, branches=[]):
self.label = label
for branch in branches:
assert isinstance(branch, Tree)
self.branches = list(branches)
def is_leaf(self):
return not self.branches
def __repr__(self):
if self.branches:
branch_str = ', ' + repr(self.branches)
else:
branch_str = ''
return 'Tree({0}{1})'.format(self.label, branch_str)
def __str__(self):
return '\n'.join(self.indented())
def indented(self):
lines = []
for b in self.branches:
for line in b.indented():
lines.append(' ' + line)
return [str(self.label)] + lines
def double(t):
"""Doubles every label in t, mutating t.
>>> t = Tree(1, [Tree(3, [Tree(5)]), Tree(7)])
>>> double(t)
>>> t
Tree(2, [Tree(6, [Tree(10)]), Tree(14)])
"""
t.label = t.label * 2
for b in t.branches:
double(b)
Removing subtrees from a tree is called pruning.
Always prune branches before recursive processing.
def prune(t, n):
"""Prune all sub-trees whose label is n.
>>> t = Tree(3, [Tree(1, [Tree(0), Tree(1)]), Tree(2, [Tree(1), Tree(1, [Tree(0), Tree(1)])])])
>>> prune(t, 1)
>>> t
Tree(3, [Tree(2)])
"""
t.branches = [___ for b in t.branches if ___]
for b in t.branches:
prune(___, ___)
Removing subtrees from a tree is called pruning.
Always prune branches before recursive processing.
def prune(t, n):
"""Prune all sub-trees whose label is n.
>>> t = Tree(3, [Tree(1, [Tree(0), Tree(1)]), Tree(2, [Tree(1), Tree(1, [Tree(0), Tree(1)])])])
>>> prune(t, 1)
>>> t
Tree(3, [Tree(2)])
"""
t.branches = [b for b in t.branches if b.label !=n]
for b in t.branches:
prune(b, n)
Why are Tree
and Link
considered recursive
objects?
Each type of object contains references to the same type of object.
Tree
can contain additional instances of
Tree
, in the branches
variable.
Link
can contain an additional instance of
Link
, in the rest
variable.
Both classes lend themselves to recursive algorithms. Generally:
Tree
: The base case is when is_leaf()
is
true;branches
.
Link
: The base case is when the rest is
empty
;rest
.