目录
1 链表
- 链表结构是一个数据结构,它包含0个或多个节点。一个节点包含了一个数据项,以及到其它节点的一个或多个链接
1.1 单链表结构和双链表结构
- 单链表结构的节点包含了一个数据项和到下一个节点的一个链接。
- 单链表结构示意图
- 双链表结构的接待你除了一个数据项和到下一个节点的链接外,还有包括了到前一个节点的链接
- 双链表结构示意图
- 与数组对比
- 和数组相同,链表结构表示了项的线性序列
- 但是链表结构无法通过指定索引,立即访问某一项。而是必须从结构的一端开始,沿着链表进行,直到达到想要的位置(或项)
- 分配内存的方式和数组有很大的不同,并且对于插入和删除操作有两个重要的影响:
- 一旦找到一个 插入点或删除点,就可以进行插入和删除,而不需要在内存中移动数据项
- 在每一次插入和删除的过程中,链表的结构会调整大小,并且不需要额外的内存代价,也不需要复制数据项
1.2 非连续性内存和节点
- 数组项必须存储在连续的内存中,即数组中的项的逻辑顺序是和内存中的物理单元序列紧密耦合的。
- 链表结构将结构中的项的逻辑顺序和内存中的顺序解耦了。即只要计算机遵照链表结构中的一个给定项的地址和位置的链接,就能够在内存中找到它的单元在何处。这种内存表示方案,叫作非连续性的内存。
- 链表结构的基本单位表示是节点。单链表的节点包含了如下的部分或字段:
- 一个数据项
- 到结构中的下一个节点的一个链接
- 单链表节点
- 双链表节点
1.3 单链表节点类
节点类很简单。灵活性和易用性很关键。因此,一个节点对象的实例变量通常不会有方法调用,并且在创建节点的时候,构造方法允许用户设置节点的链接。
- 类代码
# -*- encoding:utf-8 -*-
# Author:ZFT
class Node(object):
"""Represnets a singly linked node."""
# 单链表节点只是包含了一个数据项和对下一个节点的引用。
def __init__(self, data, next = None):
"""Instantiates a Node with a default next of None."""
self.data = data
self.next = next
- 测试代码
# -*- encoding:utf-8 -*-
# Author:ZFT
from Data_structure.Chapter4.node import Node
head = None
# Add five nodes to the beginning of the linked structure
for count in range(1, 6):
head = Node(count, head) #头插法
probe = head
print(probe.data)
# Print the contents of the structure
# 遍历
while head != None:
print(probe.data)
probe = probe.next
- 代码分析
- 指针head生成了链表结构。这个指针以这样一种方式操作,最近插入的项总是位于结构的开始处
- 因此,当显示数据的时候,它们按照和插入时相反的顺序出现
- 此外,当显示数据的时候,head指针重新设置为下一个节点,直到head指针变为None。因此,在这个过程的最后,节点实际上从链表结构中删除了。对于程序来说,节点不再可用,并且会在下一次垃圾回收的时候回收
2 单链表结构上的操作
- 几乎数组上的所有操作都是基于索引的,而索引是数组结构中一个不可或缺的部分。
- 在链表结构中,程序员必须通过操作结构中的链接来模拟基于索引的操作
2.1 遍历
- 使用临时指针,将这个变量先初始化为链表结构的head指针,然后控制一个循环
probe = head
while probe != None:
probe = probe.next
- 注意,这个过程结束的时候,probe指针是None,但是,head指针仍然引用第1个节点
- 通常,遍历一个单链表结构会访问每一个节点,并且当遇到一个空链接的时候终止。因此,值None充当负责停止这个过程的哨兵
- 遍历在时间上是线性的,并且不需要额外的内存
2.2 搜索
- 链表必须从第1个节点开始并且沿着链接直到遇到哨兵
- 可能会出现两个哨兵:
- 空链接,表明不再有要检查的数据
- 等于目标项的一个数据项,表明一次成功的搜索
- 如下是搜素一个给定项的代码:
probe = head
while probe != None and targetItem != probe.data:
probe = probe.next
if probe != None:
return probe.data
else:
return False
- 如下是访问第i项的代码:
# Assume 0 <= index < n
probe = head
while index > 0:
probe = probe.next
index -= 1
return probe.data
- 对于单链表结构,顺序搜索是线性的(平均情况下)
- 和数组不同,链表并不支持随机访问
2.3 替换
- 在单链表结构中的替换操作也利用了遍历模式。在这种情况下,我们在链表结构中搜索一个给定项或一个给定位置,并且用新的项替换该项。
- 第一个操作,即替换一个给定的项,并不需要假设目标项在链表结构中。如果目标项不在,那就不会发生替换,并且该操作返回Fasle。如果目标项存在,新的项会替换他,并且该操作返回True。
probe = head
while probe != None and targetItem != probe.data:
probe = probe.next
if probe != None:
probe.data = newItem
else:
return False
- 还有一个替换第i项的操作,它假设0<=i<n。
# Assume 0 <= index < n
probe = head
while index > 0:
probe = probe.next
i -= 1
probe.data = newItem
- 两种替换操作在平均情况下都是线性的
2.4 在开始处插入
- 在结构的开始处插入一项
head = Node(newItem, head)
2.5 在末尾插入
- 在结构的末尾插入需要考虑两种情况:
- head指针为None,此时,将head指针设置为新的节点
- head指针不为None,此时,代码将搜索最后一个节点,并将其next指针指向新的节点
newNode = Node(newItem)
if head is None:
head = newNode
else:
probe = head
while probe.next != None:
probe = probe.next
probe.next = newNode
2.6 在开始处删除
- 在结构开始处删除假设结构中至少有一个节点
# Assume at least one node in the structure
removedItem = head.data
head = head.next
return removedItem
2.7 从末尾删除
- 在结构末尾处删除假设结构中至少有一个节点
- 需要考虑以下两种情况:
- 只有一个节点。head指针设置为None
- 在最后一个节点之前有节点。代码搜素倒数第二个节点并将其next指针设置为None
# Assume at least one node in structure
removedItem = head.data
if head.next is None:
head = None
else:
probe = head
while probe.next.next != None:
probe = probe.next
removedItem = probe.next.data
probe.next = None
return removedItem
2.8 在任何位置插入
- 在一个链表的第i个位置插入一项,必须先找到位置为i-1(i<n)或者n-1(i>=n)的节点。然后考虑如下两种情况。此处还要考虑head为空或者插入位置小于等于0的情况:
- 该节点的next指针为None。这意味着,i>=n,因此,应该将新的项放在链表结构的末尾
- 该节点的next指针不为None。这意味着,0<i<n,因此,必须将新的项放在位于i-1和i的节点之间
- 和搜素第i项相同,插入操作必须计数节点,直到到达了想要的位置。然而,由于目标索引可能会大于或者等于节点的数目,因此在搜索中,必须小心避免超出链表结构的末尾。为了解决这一问题,循环有一个额外的条件,就是测试当前节点的next指针,看看这个点是否是最后一个节点
if head is None or index <= 0:
head = Node(newItem, head)
else:
# Search for node at position index - 1 or the last position
probe = head
while probe.next != None and index > 1:
probe = probe.next
index -= 1
# Insert new node after node at position index - 1 or last position
probe.next = Node(newItem, probe.next)
2.9 从任意位置删除
- 从一个链表结构中删除第i个项,具有以下3种情况:
- i<=0——使用删除第1项的代码
- 0<i<n——搜索位于i-1位置的节点,删除其后面的节点
- i>=n——删除最后一个节点
- 假设链表结构至少有1项。这个模式类似于插入节点所使用的模式,因为你必须保证不会超过链表结构的末尾。也就是说,必须允许probe指针不会超过链表结构的倒数第2个节点
# Assumes that the linked structure has at least one item
if index <= 0 or head.next is None:
removedItem = head.data
head = head.next
return removedItem
else:
# Search for node at position index -1 or the next to last position
probe = head
while index > 1 and probe.next.next != None:
probe = probe.next
index -= 1
removedItem = probe.next.data
probe.next = probe.next.next
return removedItem
3 复杂度权衡:时间、空间和单链表结构
操作 | 运行时间 |
在第i个位置访问 | O(n),平均情况 |
在第i个位置替换 | O(n),平均情况 |
在开始处插入 | O(1),最好情况和最坏情况 |
在开始处删除 | O(1),最好情况和最坏情况 |
在第i个位置插入 | O(n),平均情况 |
在第i个位置删除 | O(n),平均情况 |
- 单链表结构相对于数组来说的主要优点并不是时间性能,而是内存性能。
- 当必须调整数组的大小的时候,其时间和内存都是线性的。当调整链表结构的大小的时候(这在每一次插入或删除的时候都会发生),其时间和内存都是常数的。
- 此外,在链表结构中没有浪费内存的问题。链表结构的物理大小不会超过其逻辑大小。链表结构确实有一个额外的内存代价,因为单链表结构必须要为指针使用n个内存单元格。这个代价在双链表中更是变本加厉,因为你双链表的节点有两个链接。
4 链表的变体
4.1 带有一个哑头节点的循环链表结构
- 循环链表结构包含了从结构中的最后一个节点返回到第一个节点的一个链接。在这个实现中,至少总是有一个节点。这个节点也就是哑头节点(dummy header node),它不包含数据,但是充当了链表结构的开头和结尾的一个标记
- 对第i个节点的搜索,从哑头节点之后的节点开始。空链接的最初结构如下:
head = Node(None, None)
head.next = head
- 如下是在第i个位置插入节点的代码,它使用了这个链表结构的新的表示:
# Search for node at position index - 1 or the last position
probe = head
while index > 0 and probe.next != head:
probe = probe.next
index -= 1
# Insert new node after node at position index - 1 or last position
probe.next = Node(newItem, probe.next)
4.2 双链表结构
- 双链表结构比单链表结构更有优越性。它允许用户做如下的事情:
- 从给定的节点,向左移动到前一个节点
- 直接移动到最后一个节点
- 双链表节点通常有两个指针,称为next和previous。还有一个外部的tail指针,它允许直接访问结构中的最后一个节点
- 双链表结构的节点类的实现:
class Node(object):
"""Represnets a singly linked node."""
# 单链表节点只是包含了一个数据项和对下一个节点的引用。
def __init__(self, data, next = None):
"""Instantiates a Node with a default next of None."""
self.data = data
self.next = next
class TwoWayNode(Node):
def __init__(self, data, previous = None, next = None):
"""Instantiates a TwoWayNode."""
Node.__init__(self, data, next)
self.previous = previous
- 如下是测试程序,它通过在末尾添加项来创建了一个双链表结构。然后,程序从最后一项开始朝着第一项处理,最终显示了整个链表结构的内容:
# -*- encoding:utf-8 -*-
# Author:ZFT
from Data_structure.Chapter4.node import TwoWayNode
# Create a doubly linked structure with one node
head = TwoWayNode(1)
tail = head
# Add four nodes to the end of the doubly linked structure
for data in range(2, 6):
# 在链表结构的末尾插入一个新的项。tail尾针总是指向这个非空链表结构的
# 最后一个节点
tail.next = TwoWayNode(data, tail)
tail = tail.next
# Print the contents of the linked structure in reverse order
probe = tail
while probe != None:
print(probe.data)
probe = probe.previous
- 在链表未尾插入一个新的项的语句
tail.next = TwoWayNode(data, tail)
tail = tail.next
- 新节点的 previous 指针必须指向当前的尾节点。通过将 tail 当作该节点构造方法的第2个参数传递,来实现这一点
- 当前尾节点的 next 指针必须指向新的节点,这通过第一条赋值语句来实现
- tail 指针必须指向新的节点。第二条赋值语句实现这点
- 带有哑头节点的循环双链表
5 总结
- 链表结构是一个数据结构,它包含了0个或多个节点。一个节点包含了以下数据项,以及到其它节点的一个或多个链接
- 单链表结构的节点包含了一个数据项和到下一个节点的一个链接。双链表结构中的节点还包含了到前一个节点的一个链
- 在链表结构中插入和删除操作,不需要移动数据元素。最多只需要创建一个节点。在线性的链表结构中插入、删除和访问等操作,所需要的时间都是线性的
- 在链表结构中,使用头节点可以简化一些操作,例如,添加或删除等操作