前言
本文以Node类数据结构(链表结构,singky linked node/doubly linked node)为例,围绕如何利用Python实现包接口以及抽象集合的开发展开介绍。本文面向于对OOP编程有一定基础的朋友,建议您在阅读本文章前参考学习文章 <面向对象编程>,如果您对【接口函数、模块、包、库 】等概念仍模糊,文章 Python数据结构之基础概念讲解(一)已给出了具象化的详细解释。
注:本系列文章参考自书籍 Fundamentals of Python Data Structure ,博客内容上如有错误,还请大家指出🙏
引入
链表结构包括单链表结构(singky linked node)和双链表结构(doubly linked node),相较于数组中数据项的连续存储方式,链表结构中数据项的逻辑顺序和内存中的顺序解耦,也就是说,要访问链表结构中的数据项只需通过一个给定项的地址和位置的链接,从堆栈空间内申请的指针即可寻址找到该数据项。这种内存的映射关系,我们称之为非连续性的内存(noncontiguous memory)。
链表结构的基本单元是节点(node)👇
- 单链表的节点包括了一个数据项(
node.data
)和到结构中的下一个节点的一个链接(node.next
),单链表结构的用户通过沿着一个外部的头链接(head link)来访问第一个项。 - 双链表的节点包括了==一个数据项(
node.data
)、到结构中的下一个节点的一个链接(node.next
)、到结构中前一个节点的一个链接(node.previous
),双链表结构的用户同样可以通过沿着一个外部的头链接(head link)来访问第一个项。 - 头节点初始化为空,即未存储任何数据项;在链表结构中的最后一个节点没有下一个的指向,因而被设置为空链接(empty link)
👉一览自顶向下的设计流程:
👉层级目录如下:
>DATA_STRUCTURES
>arrays.py
>node.py
>node_test.py
>testnode.py
>linkedbag.py
>baginterface.py
>abstractcollection.py
>testset.py
硬件资源/配置环境: MacBookPro12,1/VS Code
一、Array 类的准备
请确保在您的Python数据结构学习目录下已创建好arrays.py,因为在完成Node类的设计过程中需要用到Array类实例。👇
"""
File: arrays.py
An array is like a list, but the client can use only [], len, iter and str
To instantiate, use <variable> = Array(<capacity>, <optional fill value>)
The fill value is None by default
"""
class Array(object):
""" Represents an array """
def __init__(self, capacity, fillValue = None):
""" Capacity is the static size of the array.
fillValue is placed at each position """
self._items = list()
for count in range(capacity):
self._items.append(fillValue)
def __len__(self):
""" -> The capacity of the array """
return len(self._items)
def __str__(self):
""" -> The string representation of the array """
return str(self._items)
def __iter__(self):
""" Support traversal with a for loop """
return iter(self._items)
def __getitem__(self, index):
""" Subscript operator for access at index """
return self._items[index]
def __setitem__(self, index, newItem):
""" Subscript operator for replacement at index """
self._items[index] = newItem
使用命令 python <文件名>
,确保该文件在IDE中正常运行👇
二、单链表节点类的实现
接下来考虑我们可以用什么样的数据类型将链表结构表示出来:C++和C语言中有指针的定义、允许用户在不连续的内存映射关系中找到链表结构中的数据项,FORTRAN语言虽然没有指针的概念,但其可以通过两个数组分别存储数据项及其在内存中的物理地址以达到目的;而在Python中我们可以使用类的概念定义一个链表结构。
根据单链表结构特点,在node.py文件中定义一个单链表节点类如下,并创建三个节点(头节点、含有数据项A的节点、含有数据项B的节点):
"""
File: node.py
Node classes for one-way linked structures linked structures.
"""
class Node(object):
def __init__(self, data, next = None):
"""Instantiates a Node with default next of None"""
self.data = data
self.next = next
# Just an empty link
node1 = None
# A node containing data and an empty link
node2 = Node("A", None)
# A node containing data and a link to node2
node3 = Node("B", node2)
创建一个node_test.py文件以测试👇
"""
File: node_test.py
使用 Node 类创建一个单链表结构,并输出其内容
"""
from node import Node
from arrays import Array
head = None
# Add five nodes to the beginning of the linked structure
for count in range(1, 6):
head = Node(count, head)
# Print the contents of the structure
while head != None:
print(head.data)
head = head.next # 当显示数据时,按照与插入相反的顺序输出直至head指向None,最后节点被回收
""" 将一个填满的数组中的项转移到一个单链表结构中,这个操作保证输出时保持这些项的顺序不变,这与上述的靠近头节点插入相反 """
a = Array(5)
print("{0:=^20}".format("原来的数组"))
for i in range(len(a)):
a[i] = i + 1
print("%d " % a[i], end="")
print("\n")
print("{0:=^20}".format("转移后的数组"))
head = None
i = 0
while i < len(a):
head = Node(a[i], head)
i += 1
print(head.data, end=" ")
输出为:
现在来实现基于单链表数据结构的常见操作:
- 取长度
- 遍历搜索数据项
- 插入(头插法、尾插法、在任何位置插入)
- 删除(从任意位置删除)
- 以字符串的形式输出
在testnode.py中实现上述功能 👇
"""
File: testnode.py
Add some operation to test the Node class.
"""
from node import Node
def length(head):
"""Returns the number of items in the linked structure
referred to by head."""
probe = head
count = 0
while probe != None:
count += 1
probe = probe.next
return count
def search(head, targetItem):
"""Search for the item in the linked structure.
If found, print(pos), otherwise not found"""
probe = head
count = 0
while probe != None and targetItem != probe.data:
probe = probe.next
count += 1
if probe != None:
if probe.data == targetItem:
print("Find it! At position【%d】 of the linked structure\n" % count)
else:
print("【%d】is not in the linked structure\n" % targetItem)
def insert(index, newItem, head):
"""Inserts newItem at position is the linked structure
referred to by head. Returns a reference to the new
structure."""
if index <= 0:
# newItem goes at the head
head = Node(newItem, head)
else:
# Search for node at position index - 1 or the last position
probe = head
while index > 1 and probe.next != None:
probe = probe.next
index -= 1
# Insert new node after node at position index - 1
# or last position
probe.next = Node(newItem, probe.next)
return head
def pop(index, head):
"""Removes the item at index from the linked structure
referred to by head and returns the tuple (head, item)
Precondition: 0 <= index < length(head)"""
if index < 0 or index >= length(head):
raise IndexError("Index out of bounds")
# Assumes that the linked structure has at least one item
if index == 0:
removedItem = head.data
head = head.next
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 (head, removedItem)
def printStructure(head):
"""Prints the items in the structure referred to by head."""
probe = head
while probe != None:
print(probe.data, end = " ")
probe = probe.next
print()
def main():
"""Tests modifications."""
head = None
head = insert(0, "1", head)
print("After inserting a node at pos【0】:", end = " ")
printStructure(head)
(head, item) = pop(0, head)
print("A node.data at pos【0】:", item, end = " ")
printStructure(head)
# Add five nodes to the beginning of the linked structure
for val in range(1, 6):
head = Node(val, head)
printStructure(head)
print(length(head))
search(head, 8)
(head, item) = pop(0, head)
print("【5 4 3 2 1 1】->", item, end = " ")
printStructure(head)
(head, item) = pop(length(head) - 1, head)
print("Expect 【1 4 3 2】:", item, end = " ")
printStructure(head)
(head, item) = pop(1, head)
print("Expect 【3 4 2】:", item, end = " ")
printStructure(head)
pop(4, head) # raise ERROR
if __name__ == "__main__":
main()
输出为:
三、双链表节点类的实现
根据双链表结构特点,在node.py文件中定义一个双链表节点类如下:
"""
File: node.py
Node classes for one-way linked structures and two-way
linked structures.
"""
class Node(object):
def __init__(self, data, next = None):
"""Instantiates a Node with default next of None"""
self.data = data
self.next = next
class TwoWayNode(Node):
def __init__(self, data, previous = None, next = None):
Node.__init__(self, data, next)
self.previous = previous
# Just an empty link
node1 = None
# A node containing data and an empty link
node2 = Node("A", None)
# A node containing data and a link to node2
node3 = Node("B", node2)
因为我们在testnode.py中定义的方法统一适用于链表结构,所以只需在其中添加一个创建双链表结构的方法即可套用之前设计的功能函数👇
"""
File: testnode.py
Add a makeTwoWay function.
Tests the Node class.
"""
from node import Node, TwoWayNode
def length(head):
"""Returns the number of items in the linked structure
referred to by head."""
probe = head
count = 0
while probe != None:
count += 1
probe = probe.next
return count
def search(head, targetItem):
"""Search for the item in the linked structure.
If found, print(pos), otherwise not found"""
probe = head
count = 0
while probe != None and targetItem != probe.data:
probe = probe.next
count += 1
if probe != None:
if probe.data == targetItem:
print("Find it! At position【%d】 of the linked structure\n" % count)
else:
print("【%d】is not in the linked structure\n" % targetItem)
def insert(index, newItem, head):
"""Inserts newItem at position is the linked structure
referred to by head. Returns a reference to the new
structure."""
if index <= 0:
# newItem goes at the head
head = Node(newItem, head)
else:
# Search for node at position index - 1 or the last position
probe = head
while index > 1 and probe.next != None:
probe = probe.next
index -= 1
# Insert new node after node at position index - 1
# or last position
probe.next = Node(newItem, probe.next)
return head
def pop(index, head):
"""Removes the item at index from the linked structure
referred to by head and returns the tuple (head, item)
Precondition: 0 <= index < length(head)"""
if index < 0 or index >= length(head):
raise IndexError("Index out of bounds")
# Assumes that the linked structure has at least one item
if index == 0:
removedItem = head.data
head = head.next
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 (head, removedItem)
def makeTwoWay(head):
"""Creates and returns a doubly linked structure that
contains the items in the structure referred to by head."""
if head is None:
# Empty structure
return None
else:
# Set the first node
twoWayHead = TwoWayNode(head.data)
twoWayProbe = twoWayHead
probe = head
# Set remaining nodes, if any
while probe.next != None:
newNode = TwoWayNode(probe.next.data, twoWayProbe)
twoWayProbe.next = newNode
twoWayProbe = newNode
probe = probe.next
return twoWayHead
def printStructure(head):
"""Prints the items in the structure referred to by head."""
probe = head
while probe != None:
print(probe.data, end = " ")
probe = probe.next
print()
def main():
"""Tests modifications."""
head = None
# Add five nodes to the beginning of the linked structure
for count in range(1, 6):
head = Node(count, head)
print("5 4 3 2 1:", end = " ")
printStructure(head)
print("5 4 3 2 1:", end = " ")
twoWayHead = makeTwoWay(head)
printStructure(twoWayHead)
if __name__ == "__main__":
main()
四、LinkedBag 类的实现
对照ArrayBag类的定义,将基于Node类的链表结构基本操作打包至文件linkedbag.py中。如下所示:
"""
File: linkedbag.py
Add clone method to linkedbag.(compare to the arraybag)
"""
from node import Node
class LinkedBag(object):
"""A link-based bag implementation."""
# Constructor
def __init__(self, sourceCollection = None):
"""Sets the initial state of self, which includes the
contents of sourceCollection, if it's present."""
self.items = None
self.size = 0
if sourceCollection:
for item in sourceCollection:
self.add(item)
# Accessor methods
def isEmpty(self):
"""Returns True if len(self) == 0, or False otherwise."""
return len(self) == 0
def __len__(self):
"""-Returns the number of items in self."""
return self.size
def __str__(self):
"""Returns the string representation of self."""
return "{" + ", ".join(map(str, self)) + "}"
def __iter__(self):
"""Supports iteration over a view of self."""
cursor = self.items
while not cursor is None:
yield cursor.data
cursor = cursor.next
def __add__(self, other):
"""Returns a new bag containing the contents
of self and other."""
result = LinkedBag(self)
for item in other:
result.add(item)
return result
def clone(self):
"""Returns a copy of self."""
return LinkedBag(self)
def __eq__(self, other):
"""Returns True if self equals other,
or False otherwise."""
if self is other: return True
if type(self) != type(other) or \
len(self) != len(other):
return False
for item in self:
if not item in other:
return False
return True
# Mutator methods
def clear(self):
"""Makes self become empty."""
self.size = 0
self.items = None
def add(self, item):
"""Adds item to self."""
self.items = Node(item, self.items)
self.size += 1
def remove(self, item):
"""Precondition: item is in self.
Raises: KeyError if item in not in self.
Postcondition: item is removed from self."""
# Check precondition and raise if necessary
if not item in self:
raise KeyError(str(item) + " not in bag")
# Search for the node containing the target item
# probe will point to the target node, and trailer
# will point to the one before it, if it exists
probe = self.items
trailer = None
for targetItem in self:
if targetItem == item:
break
trailer = probe
probe = probe.next
# Unhook the node to be deleted, either the first one or one
# thereafter
if probe == self.items:
self.items = self.items.next
else:
trailer.next = probe.next
# Decrement logical size
self.size -= 1
👉现在我们在文件linkedbag_test.py中对LinkedBag类进行测试,如下:
注:我为了省事,就将arraybag_test里的代码搬了过来,改成了LinkedBag类进行测试,测试的类方法不全、仅供参考。读者可自行编写代码测试
"""
File: linkedbag_test.py
"""
from linkedbag import LinkedBag
a = LinkedBag([1, 2, 3, 4])
b = a # 实际上是深拷贝,即b是a在的视图,对a的修改都会映射到b中
c = LinkedBag([1, 2, 3, 4]) # Python在堆中另外申请了一个内存空间以存储对象c
print("Setting items when class being created: ", a)
a.__init__([22, 33, 44]) # 初始化赋值后会覆盖之前在创建对象时传入的列表,即恢复了出厂设置
print("\nAfter Initialing: ", a)
a.__add__(b) # 该方法不会直接将b中的值添加到a中,该类方法只起到验证可以将b对象添加到a对象的作用
print("\n\'b __Add__ a?\' Now a: ", a)
print("\nCan b Add into a? Now a: ", a.__add__(b))
print("\na Equals To b: ", a.__eq__(b))
print("\na Equals To c: ", a.__eq__(c))
a.add(4)
print("\nAfter adding【4】to a: ", a)
print("\nRemove an item not existing: \n", c.remove(5))
输出为:
五、抽象集合类的实现
Ok,至此,暂停一下关于链表数据结构的包开发,结合先前在文章 基础篇(一)Array类数据结构的包接口开发 中实现的arraybag.py和抽象出来适用于所有包的baginterface.py,我们来总结出一个适用于所有数据类型的抽象集合及其操作,在文件abstractcollection.py中完成 AbstractCollection类 的创建:
"""
File: abstractcollection.py
"""
class AbstractCollection(object):
"""An abstract collection implementation."""
# Constructor
def __init__(self, sourceCollection = None):
"""Sets the initial state of self, which includes the
contents of sourceCollection, if it's present."""
self.size = 0
if sourceCollection:
for item in sourceCollection:
self.add(item)
# Accessor methods
def isEmpty(self):
"""Returns True if len(self) == 0, or False otherwise."""
return len(self) == 0
def __len__(self):
"""Returns the number of items in self."""
return self.size
def __str__(self):
"""Returns the string representation of self."""
return "[" + ", ".join(map(str, self)) + "]"
def __add__(self, other):
"""Returns a new bag containing the contents
of self and other."""
result = type(self)(self)
for item in other:
result.add(item)
return result
def __eq__(self, other):
"""Returns True if self equals other,
or False otherwise."""
if self is other: return True
if type(self) != type(other) or \
len(self) != len(other):
return False
otherIter = iter(other)
for item in self:
if item != next(otherIter):
return False
return True
def count(self, item):
"""Returns the number of instances of item in self."""
total = 0
for nextItem in self:
if nextItem == item:
total += 1
return total
六、制作测试包
在文件testset.py中添加如下所示的代码:
"""
File: testset.py
A tester program for set implementations.
"""
from arrayset import ArraySet
from linkedbag import LinkedBag
def test(setType):
"""Expects a set type as an argument and runs some tests
on objects of that type."""
lyst = [2013, 61, 1973]
print("The list of items added is:", lyst)
s1 = setType(lyst)
print("Expect 3:", len(s1))
print("Expect the set's string:", s1)
print("Expect True:", 2013 in s1)
print("Expect False:", 2012 in s1)
print("Expect the items on separate lines:")
for item in s1:
print(item)
s1.clear()
print("Expect {}:", s1)
s1.add(25)
s1.remove(25)
print("Expect {}:", s1)
s1 = setType(lyst)
s2 = setType(s1)
print("Expect True:", s1 == s2)
print("Expect False:", s1 is s2)
print("Expect one of each item:", s1 + s2)
for item in lyst:
s1.remove(item)
print("Expect {}:", s1)
# test(ArraySet)
test(LinkedBag)
输出为: