实验8: 链表,树
Lab 8: Linked Lists, Trees
Due by 11:59pm on Tuesday, October 20.
查看英文原文
初始文件
下载 lab08.zip。 在压缩包中,你可以找到本实验问题的初始文件,以及一份 Ok 自动评分器。
主要内容
如果你需要复习本实验的材料,请参考本节。你可以直接跳到问题部分,遇到困难再回到这里。
Linked Lists
We've learned that a Python list is one way to store sequential values. Another type of list is a linked list. A Python list stores all of its elements in a single object, and each element can be accessed by using its index. A linked list, on the other hand, is a recursive object that only stores two things: its first value and a reference to the rest of the list, which is another linked list.
We can implement a class, Link
, that represents a linked list object. Each
instance of Link
has two instance attributes, first
and rest
.
class Link:
"""A linked list.
>>> s = Link(1)
>>> s.first
1
>>> s.rest is Link.empty
True
>>> s = Link(2, Link(3, Link(4)))
>>> s.first = 5
>>> s.rest.first = 6
>>> s.rest.rest = Link.empty
>>> s # Displays the contents of repr(s)
Link(5, Link(6))
>>> s.rest = Link(7, Link(Link(8, Link(9))))
>>> s
Link(5, Link(7, Link(Link(8, Link(9)))))
>>> print(s) # Prints str(s)
<5 7 <8 9>>
"""
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 is not Link.empty:
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) + '>'
A valid linked list can be one of the following:
- An empty linked list (
Link.empty
) - A
Link
object containing the first value of the linked list and a reference to the rest of the linked list
What makes a linked list recursive is that the rest
attribute of a single
Link
instance is another linked list! In the big picture, each Link
instance stores a single value of the list. When multiple Link
s are linked
together through each instance's rest
attribute, an entire sequence is
formed.
Note: This definition means that the
rest
attribute of anyLink
instance must be eitherLink.empty
or anotherLink
instance! This is enforced inLink.__init__
, which raises anAssertionError
if the value passed in forrest
is neither of these things.
To check if a linked list is empty, compare it against the class attribute
Link.empty
. For example, the function below prints out whether or not the
link it is handed is empty:
def test_empty(link):
if link is Link.empty:
print('This linked list is empty!')
else:
print('This linked list is not empty!')
Motivation: Why linked lists
Since you are already familiar with Python's built-in lists, you might be wondering why we are teaching you another list representation. There are historical reasons, along with practical reasons. Later in the course, you'll be programming in Scheme, which is a programming language that uses linked lists for almost everything.
For now, let's compare linked lists and Python lists by looking at two common sequence operations: inserting an item and indexing.
Python's built-in list is like a sequence of containers with indices on them:
Linked lists are a list of items pointing to their neighbors. Notice that there's no explicit index for each item.
Suppose we want to add an item at the head of the list.
- With Python's built-in list, if you want to put an item into the container labeled with index 0, you must move all the items in the list into its neighbor containers to make room for the first item;
- With a linked list, you tell Python that the neighbor of the new item is the old beginning of the list.
Now, let's take a look at indexing. Say we want the item at index 3 from a list.
- In the built-in list, you can use Python list indexing, e.g.
lst[3]
, to easily get the item at index 3. - In the linked list, you need to start at the first item and repeatedly follow
the
rest
attribute, e.g.link.rest.rest.first
. How does this scale if the index you were trying to access was very large?
Can you think of situations where you would want to use one type of list over another? In this class, we aren't too worried about performance. However, in future computer science courses, you'll learn how to make performance tradeoffs in your programs by choosing your data structures carefully.
Trees
Recall that a tree is a recursive abstract data type that has a label
(the
value stored in the root of the tree) and branches
(a list of trees directly
underneath the root).
We saw one way to implement the tree ADT -- using constructor and selector
functions that treat trees as lists. Another, more formal, way to implement the
tree ADT is with a class. Here is part of the class definition for Tree
,
which can be found in lab07.py
:
class Tree:
"""
>>> t = Tree(3, [Tree(2, [Tree(5)]), Tree(4)])
>>> t.label
3
>>> t.branches[0].label
2
>>> t.branches[1].is_leaf()
True
"""
def __init__(self, label, branches=[]):
for b in branches:
assert isinstance(b, Tree)
self.label = label
self.branches = list(branches)
def is_leaf(self):
return not self.branches
Even though this is a new implementation, everything we know about the tree ADT remains true. That means that solving problems involving trees as objects uses the same techniques that we developed when first studying the tree ADT (e.g. we can still use recursion on the branches!). The main difference, aside from syntax, is that tree objects are mutable.
Here is a summary of the differences between the tree ADT implemented using functions and lists vs. implemented using a class:
- | Tree constructor and selector functions | Tree class |
---|---|---|
Constructing a tree | To construct a tree given a label and a list of branches , we call
tree(label, branches)
|
To construct a tree object given a label and a list of branches , we call
Tree(label, branches) (which calls the Tree.__init__ method)
|
Label and branches | To get the label or branches of a tree t , we call label(t) or
branches(t) respectively
|
To get the label or branches of a tree t , we access the instance attributes
t.label or t.branches respectively
|
Mutability | The tree ADT is immutable because we cannot assign values to call expressions | The label and branches attributes of a Tree instance can be
reassigned, mutating the tree |
Checking if a tree is a leaf | To check whether a tree t is a leaf, we call the convenience function
is_leaf(t)
|
To check whether a tree t is a leaf, we call the bound method t.is_leaf() .
This method can only be called on Tree objects. |
必答题
Python 会输出什么?
Q1: WWPD: 链表
阅读 lab08.py
中的 Link
类。确保你理解其中的 doctest。
使用 Ok 测试你对以下 "Python 会输出什么"(WWPD)问题的理解。
python3 ok -q link -u --local
如果你认为答案是
<function ...>
,请输入Function
;如果会报错,输入Error
;如果没有任何输出,输入Nothing
。如果遇到困难,尝试在纸上绘制链表的框图(box-and-pointer diagram),或者使用
python3 -i lab09.py
将Link
类加载到解释器中进行调试。
>>> from lab08 import *
>>> link = Link(1000)
>>> link.first
______1000
>>> link.rest is Link.empty
______True
>>> link = Link(1000, 2000)
______AssertionError
>>> link = Link(1000, Link())
______TypeError
>>> from lab08 import *
>>> link = Link(1, Link(2, Link(3)))
>>> link.first
______1
>>> link.rest.first
______2
>>> link.rest.rest.rest is Link.empty
______True
>>> link.first = 9001
>>> link.first
______9001
>>> link.rest = link.rest.rest
>>> link.rest.first
______3
>>> link = Link(1)
>>> link.rest = link
>>> link.rest.rest.rest.rest.first
______1
>>> link = Link(2, Link(3, Link(4)))
>>> link2 = Link(1, link)
>>> link2.first
______1
>>> link2.rest.first
______2
>>> from lab08 import *
>>> link = Link(5, Link(6, Link(7)))
>>> link # Look at the __repr__ method of Link
______Link(5, Link(6, Link(7)))
>>> print(link) # Look at the __str__ method of Link
______<5 6 7>
链表
Q2: 转换链表
编写一个函数 convert_link
,它接受一个链表作为输入,并返回一个包含相同元素的 Python 列表。你可以假设输入链表是浅层的,即其中的元素都不是另一个链表。
尝试分别使用迭代和递归两种方法来解决这个问题!
def convert_link(link):
"""Takes a linked list and returns a Python list with the same elements.
>>> link = Link(1, Link(2, Link(3, Link(4))))
>>> convert_link(link)
[1, 2, 3, 4]
>>> convert_link(Link.empty)
[]
"""
"*** YOUR CODE HERE ***"
使用 Ok 来测试你的代码:
python3 ok -q convert_link --local
Q3: 间隔删除
实现 every_other
函数,该函数接受一个链表 s
。它会对 s
进行原地修改,使得所有奇数索引(基于 0
开始的索引)的元素都从链表中移除。例如:
>>> s = Link('a', Link('b', Link('c', Link('d'))))
>>> every_other(s)
>>> s.first
'a'
>>> s.rest.first
'c'
>>> s.rest.rest is Link.empty
True
如果 s
的元素少于两个,则保持不变。
不要返回任何值!
every_other
应直接修改原链表。
def every_other(s):
"""Mutates a linked list so that all the odd-indiced elements are removed
(using 0-based indexing).
>>> s = Link(1, Link(2, Link(3, Link(4))))
>>> every_other(s)
>>> s
Link(1, Link(3))
>>> odd_length = Link(5, Link(3, Link(1)))
>>> every_other(odd_length)
>>> odd_length
Link(5, Link(1))
>>> singleton = Link(4)
>>> every_other(singleton)
>>> singleton
Link(4)
"""
"*** YOUR CODE HERE ***"
使用 Ok 来测试你的代码:
python3 ok -q every_other --local
树
Q4: 累积乘积
编写一个函数 cumulative_mul
,该函数会修改树 t
,使得每个节点的标签变为以该节点为根的子树中所有标签的乘积。
def cumulative_mul(t):
"""Mutates t so that each node's label becomes the product of all labels in
the corresponding subtree rooted at t.
>>> t = Tree(1, [Tree(3, [Tree(5)]), Tree(7)])
>>> cumulative_mul(t)
>>> t
Tree(105, [Tree(15, [Tree(5)]), Tree(7)])
"""
"*** YOUR CODE HERE ***"
使用 Ok 来测试你的代码:
python3 ok -q cumulative_mul --local
选做题
Q5: 循环
Link
类可以表示包含循环的链表。也就是说,一个链表可能包含自身作为子链表。
>>> s = Link(1, Link(2, Link(3)))
>>> s.rest.rest.rest = s
>>> s.rest.rest.rest.rest.rest.first
3
实现 has_cycle
函数,判断给定的 Link
实例是否包含循环,并返回相应的布尔值。
提示:遍历链表,并尝试记录已经访问过的
Link
对象。
def has_cycle(link):
"""Return whether link contains a cycle.
>>> s = Link(1, Link(2, Link(3)))
>>> s.rest.rest.rest = s
>>> has_cycle(s)
True
>>> t = Link(1, Link(2, Link(3)))
>>> has_cycle(t)
False
>>> u = Link(2, Link(2, Link(2)))
>>> has_cycle(u)
False
"""
"*** YOUR CODE HERE ***"
使用 Ok 来测试你的代码:
python3 ok -q has_cycle --local
作为额外的挑战,请实现 has_cycle_constant
函数,并且仅使用常数空间复杂度。(如果按照上面的提示实现,空间复杂度将是线性的。)该解法代码较短(少于
20 行),但需要一个巧妙的思路。在向他人请教之前,尝试自己找到解决方案:
def has_cycle_constant(link):
"""Return whether link contains a cycle.
>>> s = Link(1, Link(2, Link(3)))
>>> s.rest.rest.rest = s
>>> has_cycle_constant(s)
True
>>> t = Link(1, Link(2, Link(3)))
>>> has_cycle_constant(t)
False
"""
"*** YOUR CODE HERE ***"
使用 Ok 来测试你的代码:
python3 ok -q has_cycle_constant --local
Q6: 反转奇数层
编写一个函数 reverse_other
,它会修改树,使得每隔一层(即奇数深度)的标签被反转。例如,
Tree(1, [Tree(2, [Tree(4)]), Tree(3)])
会变成:
Tree(1, [Tree(3, [Tree(4)]), Tree(2)])
注意,节点本身不会被交换,只有标签会被反转。
def reverse_other(t):
"""Mutates the tree such that nodes on every other (odd-depth) level
have the labels of their branches all reversed.
>>> t = Tree(1, [Tree(2), Tree(3), Tree(4)])
>>> reverse_other(t)
>>> t
Tree(1, [Tree(4), Tree(3), Tree(2)])
>>> t = Tree(1, [Tree(2, [Tree(3, [Tree(4), Tree(5)]), Tree(6, [Tree(7)])]), Tree(8)])
>>> reverse_other(t)
>>> t
Tree(1, [Tree(8, [Tree(3, [Tree(5), Tree(4)]), Tree(6, [Tree(7)])]), Tree(2)])
"""
"*** YOUR CODE HERE ***"
使用 Ok 来测试你的代码:
python3 ok -q reverse_other --local