09月11, 2019

不用面向对象,实现单向链表(3)

上期回顾

之前已经实现了这些函数:

# 创建节点
create_node(data, next)

# 为 cur_node 设置下一节点为 next_node
set_next_node(cur_node, next_node)

# 获取当前节点的下一节点
get_next_node(cur_node)

如何表示链表?

链表是一个一个节点构成的,不使用面向对象技术,如何表示链表呢?

可以想象有一根铁链,我们怎么把它拎起来呢?如果我们将一个中间节点作为抓手,可以么?

title

如果将单向链表的中间节点作为链表入口,我们知道单向链表的节点不允许向前查找节点,也就是链表中 head 节点左侧的所有数据都将丢失。

显然我们不能允许这种情况发生,所以,对于单向链表,需要一个没有左侧节点的 Node 作为链表的头部,也就是链表的第一个节点。

title

由于没有比第一个节点更早的数据,所以这样识别一个单向链表就不会丢失数据。

对于空的链表,它没有任何节点,为了可以表示这种链表,我们将 head 也作为一个链表节点,这样即使是空链表,也会占有一个空间。head 节点的数据是 None,因为它不会被用来保存数据,只是用来表示这个单向链表。以后我们就用一个单向链表的 head 节点,来指代整个链表,我将 head 节点直接命名为 chain_entry,意思就是这里是链表的入口。

title

创建链表

下面我们可以开始编写链表相关的函数了。

第一个,创建链表,上面已经提到,我们会直接用一个特殊的节点(数据为 None)来表示链表的入口。所以创建链表实际上是创建一个空链表,这个链表里只有 chain_entry 节点。

def create_chain():
    entry = create_node() # 默认 data 是 None,所以不填参数也可以
    # 为了方便,我们为 entry 增加一个 length 字段,保存链表的长度,刚开始是 0
    entry['length'] = 0
    return entry

这样我们就创建了一个 chain_entry,我们还为它增加了记录长度的字段 “length”。

chain_entry 被用来指代整个链表,它刚被创建的时候,看起来是这样的:

title

插入操作

由于我们对链表的访问,都是从 chain_entry 开始,依次访问第一个、第二个··· 节点,单向链表操作最快的,就是对第一个节点的增删,以为从 chain_entry 进来,第一个看到的就是它。

我们使用链表的时候,关注的是 “数据”:

  1. 我们将一个数据插入了链表,
  2. 而不会考虑先创建一个包含数据的节点,再把节点插入到链表头部,

第 1 种说法简短、清晰,是第 2 种说法的抽象。第 2 种说法是第 1 种的细节。

“抽象” 对于构建大型系统是有益的,这种抽象是代码分层和设计逻辑分层的目标。

继续探讨向链表插入数据的问题,第 2 种表述就是插入节点的基本操作:

  1. 创建一个包含数据的节点。
  2. 将节点插入链表头部。

创建包含数据的节点,我们会使用 create_node 创建,包含新数据的节点已经准备好!

title

如何向链表头部插入数据呢?

title

  1. 将第一个节点设置为新节点的下一个节点。
  2. 将新节点设置为 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

怎么输出顺序跟插入的时候不一样呢?

因为我们每次插入的数据,都是从头部插入的,读取的时候会先读到最后插入的数据,最早插入的数据最后读到,所以就成倒序了。

下节预告

目前已经实现了链表的构造和遍历,并且将连接节点的操作隐含在链表的有关接口里,是不是很棒!

下一节我们将会继续完善链表的操作函数,比如删除节点,那将会是“不使用面向对象,实现链表” 的最后一节。

有了这一系列的基础,之后我们将迅速的实现一个面向对象的版本。

本文链接:http://www.thinkinpython.com/post/singly_link_list_3.html

-- EOF --