We
can implement a stack using a linked list. Our
class LinkedStack will
have an attribute
'head' that points to
the head of the list:
The stack operations are fairly straightforward:
class LinkedStack:
def __init__(self):
self.head = None
def is_empty(self):
return self.head == None
def push(self, x):
n = Node(x, self.head)
self.head = n
def pop(self):
assert self.head != None, 'stack is empty'
x = self.head.val
self.head = self.head.next
return xNotice that the push() method allocates a node n that points to the previous head, then records n as the new list head. In other words, it prepends a new node to the list. This is similar to the activity in the list_1_to_n() function above.
If
we implement a stack using an array (i.e. a Python list), the push
operation will take
O(1) on average but O(N) in the worst case, where N is the current
stack size. Our linked list-based implementation performs
differently: push always runs in O(1) (assuming that
object allocations run in constant time).
Last week we learned about the queue abstract data type, and saw that implementing a queue with an array is inefficient.
We can implement a queue more efficiently using a linked list. To do so, we will keep pointers to both the head (first node) and tail (last node) in the list, which will allow us to enqueue or dequeue elements in O(1):
Here is an implementation:
class LinkedQueue:
def __init__(self):
self.head = self.tail = None
def isEmpty(self):
return self.head == None
def enqueue(self, x):
n = Node(x, None)
if self.tail == None:
self.head = self.tail = n
else:
self.tail.next = n
self.tail = n
def dequeue(self):
assert self.head != None, 'queue is empty'
x = self.head.val
self.head = self.head.next
if self.head == None:
self.tail = None
return xWe have already seen stacks and queues, two abstract data types. We've seen that it's possible to implement these abstract types using various concrete data structures. For example, we can implement a stack or queue either using an array or a linked list.
Let's now introduce another abstract data type. A set provides the following operations:
A set cannot contain the same value twice: every value is either present in the set or it is not.
This type should be familiar, because it has a default implementation in Python that we have seen and used.
In this algorithms course, we'll study various ways to implement sets. We'd like to understand how we could build efficient sets in Python if they were not already provided in the standard library.
A binary tree consists of a set of nodes. Each node contains a single value and may have 0, 1, or 2 children.
Here is a picture of a binary tree of integers. (Note that this is not a binary search tree, which is a special kind of binary tree that we will discuss later.)
In this tree, node 10 is the root node. 14 is the parent of 12 and 6. 12 is the left child of 14, and 6 is the right child of 14. 14 is an ancestor of 22, and 22 is a descendant of 14.
A node may have 0, 1, or 2 children. In this tree, node 15 has a right child but no left child.
The subtree rooted at 14 is the left subtree of node 10. The subtree rooted at 1 is the right subtree of node 10.
The nodes 12, 5, 22, 4, and 3 are leaves: they have no children. Nodes 10, 14, 1, 6, and 15 are internal nodes, which are nodes that have at least one child.
The depth of a node is its distance from the root. The root has depth 0. In this tree, node 15 has depth 2 and node 4 has depth 3. The height of a tree is the greatest depth of any node. This tree has height 3.
The tree with no nodes is called the empty tree.
Note that a binary tree may be asymmetric: the right side might not look at all like the left. In fact a binary tree can have any structure at all, as long as each node has 0, 1, or 2 children.
A binary tree is complete iff every internal node has 2 children and all leaves have the same height. Here is a complete binary tree of height 3:
A complete binary tree of height h has 2h leaves, and has 20 + 21 + … + 2h-1 = 2h – 1 interior nodes. So it has a total of 2h + 2h – 1 = 2h + 1 – 1 nodes. In this tree there are 23 = 8 leaves and 23 – 1 = 7 interior nodes, a total of 24 – 1 = 15 nodes.
Conversely if a complete binary tree has N nodes, then N = 2h + 1 – 1, where h is the height of the tree. And so h = log2(N + 1) – 1 ≈ log2(N) – 1 = O(log N).
We can represent a binary tree in Python using an object for each node, similarly to how we represent linked lists. Here is a Node class we can use for binary trees:
class Node:
def __init__(self, val, left, right):
self.val = val
self.left = left # left child, or None if absent
self.right = right # right child, or None if absent
We will generally refer to a tree using a pointer
to its root node. We'll use None
to represent the empty tree, just as we used None
for the empty linked list. In all leaf nodes, left and
right will be None.
Here is a small binary tree with just 3 nodes:
We can build this tree in Python as follows:
l = Node(7, None, None) r = Node(5, None, None) root = Node(4, l, r)
Here's a recursive function that counts the number of nodes in a binary tree:
def size(n):
if n == None: # empty tree
return 0
return 1 + size(n.left) + size(n.right)Let's try it on the tree we just built:
>>> size(root) 3
Recursion is a natural fit for trees, since the pattern of recursive calls in a function like this one can mirror the tree structure.
Here's a recursive function that computes the height of a binary tree:
def height(node):
if node == None:
return -1
return max(height(node.left), height(node.right)) + 1Let's try it:
>>> height(root) 1
The constant -1 in this function may seem surprising. Recall that the height of a binary tree is defined as the greatest depth of any node. If a binary tree has only a single node, then the depth of that node is 0, so the tree's height is 0. And so if a tree is empty, meaning that it has no nodes at all, then for consistency it must have a height of one less than that, or -1.
As another example, let's write a function that generates a complete binary tree of a given height h. Each node of the tree will contain a random value from 0 .. 99. Once more we will use recursion:
def complete(h):
if h == -1:
return None
left = complete(h - 1) # generate left subtree
right = complete(h - 1) # generate right subtree
return Node(randrange(100), left, right)Now we can easily generate a large tree:
>>> t = complete(10) >>> size(t) 2047 >>> height(t) 10
A binary search tree is a binary tree in which the values are ordered in a particular way that makes searching easy: for any node N with value v,
all values in N's left subtree are less than v
all values in N's right subtree are greater than v
For example, here is a binary search tree of integers:
We can use a binary search tree to store a set. To do this, we'll write a TreeSet class that holds the current root of a binary tree:
class TreeSet:
def __init__(self):
self.root = None
...It is not difficult to find whether a binary tree
contains a given value x. We begin
at the root. If the root's value is x, then we are done. Otherwise,
we compare x to the root's value v. If x < v, we move to the left
child; if x > v, we move to the right child. We proceed in this
way until we have found x or until we hit None,
in which case x is not in the tree.
Here's how we
can implement this in the TreeSet
class:
def contains(self, x):
n = self.root
while n != None:
if x == n.val:
return True
if x < n.val:
n = n.left
else:
n = n.right
return FalseInserting a value into a binary search tree is
also pretty straightforward. Beginning at the root, we look for an
insertion position, proceeding down the tree just as in the above
algorithm for contains. When we reach an empty left or
right child, we place the new node there. In the TreeSet
class:
# add a value, or do nothing if already present
def add(self, x):
n = Node(x, None, None) # new node to add
p = self.root
if p == None:
self.root = n
return
while True:
if x == p.val:
return # already present
elif x < p.val:
if p.left == None:
p.left = n
return
else:
p = p.left
else: # x > p.val
if p.right == None:
p.right = n
return
else:
p = p.right