上期回顾
之前已经实现了这些函数:
# 创建节点
create_node(data, next)
# 为 cur_node 设置下一节点为 next_node
set_next_node(cur_node, next_node)
# 获取当前节点的下一节点
get_next_node(cur_node)
如何表示链表?
链表是一个一个节点构成的,不使用面向对象技术,如何表示链表呢?
可以想象有一根铁链,我们怎么把它拎起来呢?如果我们将一个中间节点作为抓手,可以么?
如果将单向链表的中间节点作为链表入口,我们知道单向链表的节点不允许向前查找节点,也就是链表中 head 节点左侧的所有数据都将丢失。
显然我们不能允许这种情况发生,所以,对于单向链表,需要一个没有左侧节点的 Node 作为链表的头部,也就是链表的第一个节点。
由于没有比第一个节点更早的数据,所以这样识别一个单向链表就不会丢失数据。
对于空的链表,它没有任何节点,为了可以表示这种链表,我们将 head 也作为一个链表节点,这样即使是空链表,也会占有一个空间。head 节点的数据是 None,因为它不会被用来保存数据,只是用来表示这个单向链表。以后我们就用一个单向链表的 head 节点,来指代整个链表,我将 head 节点直接命名为 chain_entry,意思就是这里是链表的入口。
创建链表
下面我们可以开始编写链表相关的函数了。
第一个,创建链表,上面已经提到,我们会直接用一个特殊的节点(数据为 None)来表示链表的入口。所以创建链表实际上是创建一个空链表,这个链表里只有 chain_entry 节点。
def create_chain():
entry = create_node() # 默认 data 是 None,所以不填参数也可以
# 为了方便,我们为 entry 增加一个 length 字段,保存链表的长度,刚开始是 0
entry['length'] = 0
return entry
这样我们就创建了一个 chain_entry,我们还为它增加了记录长度的字段 “length”。
chain_entry 被用来指代整个链表,它刚被创建的时候,看起来是这样的:
插入操作
由于我们对链表的访问,都是从 chain_entry 开始,依次访问第一个、第二个··· 节点,单向链表操作最快的,就是对第一个节点的增删,以为从 chain_entry 进来,第一个看到的就是它。
我们使用链表的时候,关注的是 “数据”:
- 我们将一个数据插入了链表,
- 而不会考虑先创建一个包含数据的节点,再把节点插入到链表头部,
第 1 种说法简短、清晰,是第 2 种说法的抽象。第 2 种说法是第 1 种的细节。
“抽象” 对于构建大型系统是有益的,这种抽象是代码分层和设计逻辑分层的目标。
继续探讨向链表插入数据的问题,第 2 种表述就是插入节点的基本操作:
- 创建一个包含数据的节点。
- 将节点插入链表头部。
创建包含数据的节点,我们会使用 create_node 创建,包含新数据的节点已经准备好!
如何向链表头部插入数据呢?
- 将第一个节点设置为新节点的下一个节点。
- 将新节点设置为 entry 的下一个节点。
一定要注意步骤,如果颠倒过来,就需要使用额外的变量保存第一个节点的引用,否则新节点设置为 entry 的下一个节点时,第一个节点的引用就被覆盖丢失了。
上代码,顺便实现了一些有用的功能:
# 获取链表长度
def get_chain_length(entry):
return entry['length']
# 链表是否为空
def is_chain_empty(entry):
return get_chain_length(entry) == 0
# 获取节点数据
def get_node_data(node):
return node['data']
def push_chain(entry, data):
new_node = create_node(data)
# 为了清晰,我们单独把第一个节点保存在 first_node
first_node = get_next_node(entry)
set_next_node(new_node, first_node)
set_next_node(entry, new_node)
# 别忘了增加链表节点计数
entry['length'] = entry['length'] + 1
# 遍历链表,便于我们查看链表内容
def chain_all(entry):
node = entry
while get_next_node(node):
node = get_next_node(node)
print(get_node_data(node))
可以看到,整个链表的数据都是由 push_chain 插入的,这个函数还维护了链表长度信息,所以链表的数据增删一定要使用指定函数,不要直接访问原始链表数据增删,这会造成 entry 的信息错误。
痛苦的领悟 没有面向对象技术,数据访问权限问题解决起来不是很方便。只能靠使用者自觉了。 这个问题在后面面向对象版本中会得到解决。
简单测试一下
chain = create_chain()
push_chain(chain, "hello")
push_chain(chain, "world")
push_chain(chain, "!")
print("length: {}".format(get_chain_length(chain)))
print("all chain nodes data:")
chain_all(chain)
输出是这样的:
$ python test3.py
length: 3
all chain nodes data:
!
world
hello
怎么输出顺序跟插入的时候不一样呢?
因为我们每次插入的数据,都是从头部插入的,读取的时候会先读到最后插入的数据,最早插入的数据最后读到,所以就成倒序了。
下节预告
目前已经实现了链表的构造和遍历,并且将连接节点的操作隐含在链表的有关接口里,是不是很棒!
下一节我们将会继续完善链表的操作函数,比如删除节点,那将会是“不使用面向对象,实现链表” 的最后一节。
有了这一系列的基础,之后我们将迅速的实现一个面向对象的版本。