基础知识
链表是各对象按线性顺序排列的数据结构。与数组相比,数组的线性顺序是由数组下标决定的,而链表的顺序则是由各个对象里的指针(这里是广义上的指针,并不是C语言中的概念)决定的。链表为动态集合提供了一种简单而灵活的表示方法。
链表的形式一般以一个双向链表描述,对于这种链表而言,它的每一个元素都是一个对象,包含若干信息和两个指针,其中一个是指向前一个元素的指针
p
r
e
v
prev
prev,另一个是指向下一个元素的指针
n
e
x
t
next
next。设
x
x
x为链表的一个元素,如果
x
.
p
r
e
v
=
None
x.prev=\text{None}
x.prev=None,则元素
x
x
x没有前驱,因此是链表的第一个元素,即链表的头;如果
x
.
n
e
x
t
=
None
x.next=\text{None}
x.next=None,则元素
x
x
x没有后继,因此是链表的最后一个元素,即链表的尾。当我们构建一个链表时,我们会用一个头节点
h
e
a
d
head
head的
n
e
x
t
next
next指向链表的第一个元素,而这个头节点不存储其他信息。
链表可以有多种形式。如果我们省略每个元素的
p
r
e
v
prev
prev指针,则这个链表就成了单链表;如果将各元素存储的信息按一定的顺序排列在链表中,则这个链表就成了排序链表;如果将尾节点的
n
e
x
t
next
next指针指向头节点,则这个链表就成了循环链表。
代码实现
下面我们将利用Python面向对象的有关知识来逐步实现一个单链表的数据结构。
首先,我们先定义节点类,包含一个存储信息的包data
和指针next
:
class Node:
def __init__(self,*data):
self.data=data
self.next=None
然后,我们再写一个链表的类,包含头节点head
:
class LinkedList:
def __init__(self):
self.head=Node()
可以看到,这里的头节点不存储信息,next
指针也指向空。
下面我们向链表类中添加功能。对于数据结构而言,无非就是四种操作:增、删、改、查。
首先我们先写“增”的操作。我们希望这个操作可以从任意位置向链表插入元素,因此,我们可以先重载一下链表类的__getitem__
方法,便于我们用X[i]
的形式对链表序号为i
的元素进行访问:
def __getitem__(self,idx):
if idx<-1:
return None
cur=self.head
while idx>-1 and cur!=None:
cur=cur.next
idx-=1
return cur
由于我们有时需要访问头节点,所以我们人为地定义头节点的序号为 − 1 -1 −1 ,头节点的后继(即链表存储信息的第一个元素)序号为 0 0 0 。
好了,现在我们可以编写插入操作了:
def insert(self,idx,*data):
newnode=Node(*data)
pre=self[idx-1]
newnode.next=pre.next
pre.next=newnode
根据代码不难理解,如果我们要将元素 x x x 插入到序号为 i i i 的位置,我们可以先定位到 i − 1 i-1 i−1 ,然后将 i − 1 i-1 i−1 指向 x x x ,并将 x x x 指向原本 i − 1 i-1 i−1 的后继。
同样的道理,我们可以编写删除操作:
def remove(self,idx=0):
pre=self[idx-1]
pre.next=pre.next.next
要删除序号为 i i i 的元素,我们只需将序号为 i − 1 i-1 i−1 的元素指向 i i i 的后继即可(这样一来原本的 i i i 仍然是存在的,不过我们找不到它了)。
修改则更加简单了,重载__setitem__
即可,此处略。
至于查找,只要像重载__getitem__
那样遍历即可。不过,我们希望可以用多个关键词查找,因此我们将data
和key
打包成一个二元元组,并以包的形式传入:
def _search(self,*data_key):
cur=self.head.next
while cur!=None:
flag=True
for i in data_key:
if cur.data[i[-1]]!=i[0]:
flag=False
break
if flag:
return cur
else:
cur=cur.next
return None
对于一般的查找,即以一个关键字且存储信息只有一个的情况,我们再写一个如下的方法:
def search(self,data,key=0):
return self._search((data,key))
至此,我们已经基本完成了链表类的底层编写。事实上我们已经可以用这个类做很多事情了,不过在开展这些工作之前,我们先给它再加上一些功能,便于我们使用。
首先,我们可能会想要知道链表存储了多少个节点,因此我们可以重载一下链表类的__len__
方法:
def __len__(self):
cnt=0
cur=self.head.next
while cur!=None:
cur=cur.next
cnt+=1
return cnt
其次,我们一般插入节点都习惯于插入到链表尾部,因此我们可以借助__len__
写一个append
方法:
def append(self,*data):
self.insert(len(self),*data)
然后,我们可能还想将链表中的信息都打印出来,因此我们可以重载链表类的__str__
方法:
def __str__(self):
cur=self.head.next
li=[]
while cur!=None:
l=list(cur.data)
if len(l)==1:
li.append(l[0])
else:
li.append(list(cur.data))
cur=cur.next
return li.__str__()
可以看到,这里我们做了一个小妥协,并不会将所有的信息都以列表形式打印,毕竟当我们一个节点只放一条数据的时候,仍然打印一对括号是不够美观的。
此外,我们可能还希望对链表实现求最值的功能,因此需要重载节点类的__lt__
方法和链表类的__iter__
和__next__
方法:
# in Node
def __lt__(self,oth,key=0):
return self.data[key]<oth.data[key]
# in LinkedList
def __iter__(self):
self.cur=self.head
return self
def __next__(self):
self.cur=self.cur.next
if self.cur!=None:
return self.cur
else:
raise StopIteration
有些时候,我们可能还想将链表反转一下,所以我们可以写一个递归的reverse
方法:
def reverse(self,pre=None):
if self.head.next==None:
self.head.next=pre
else:
buf=self.head.next
self.head.next=self.head.next.next
buf.next=pre
self.reverse(buf)
至此,我们所有的代码都已经敲完了,整理下来就是这样的:
class Node:
def __init__(self,*data):
self.data=data
self.next=None
def __lt__(self,oth,key=0):
return self.data[key]<oth.data[key]
class LinkedList:
def __init__(self):
self.head=Node()
def __getitem__(self,idx):
if idx<-1:
return None
cur=self.head
while idx>-1 and cur!=None:
cur=cur.next
idx-=1
return cur
def __str__(self):
cur=self.head.next
li=[]
while cur!=None:
l=list(cur.data)
if len(l)==1:
li.append(l[0])
else:
li.append(l)
cur=cur.next
return li.__str__()
def __len__(self):
cnt=0
cur=self.head.next
while cur!=None:
cur=cur.next
cnt+=1
return cnt
def __iter__(self):
self.cur=self.head
return self
def __next__(self):
self.cur=self.cur.next
if self.cur!=None:
return self.cur
else:
raise StopIteration
def insert(self,idx,*data):
newnode=Node(*data)
pre=self[idx-1]
newnode.next=pre.next
pre.next=newnode
def remove(self,idx=0):
pre=self[idx-1]
pre.next=pre.next.next
def _search(self,*data_key):
cur=self.head.next
while cur!=None:
flag=True
for i in data_key:
if cur.data[i[-1]]!=i[0]:
flag=False
break
if flag:
return cur
else:
cur=cur.next
return None
def search(self,data,key=0):
return self._search((data,key))
def append(self,*data):
self.insert(len(self),*data)
def reverse(self,pre=None):
if self.head.next==None:
self.head.next=pre
else:
buf=self.head.next
self.head.next=self.head.next.next
buf.next=pre
self.reverse(buf)
编写以下测试代码:
x=LinkedList()
x.append(4)
x.append(7,'abc')
x.append(6,123)
x.append('')
x.append(-6,-5)
我们得到以下输出:
[4, [7, 'abc'], [6, 123], '', [-6, -5]]
实战演练
好的,应用这份代码,我们来编写一个实用程序。
在这个程序内,我们希望实现日常支出的简单管理,其功能有:
1.输入
n
n
n项支出项目,并依次输出所有支出项目。
2.求出这
n
n
n个支出项目中的最小、最大和平均消费。
3.按照日期找出某一天的所有花费。
4.按照项目找出该项目的所有花费。
5.按照日期和项目找出所有花费 。
首先我们贴出代码:
class Node:
def __init__(self,*data):
self.data=data
self.next=None
def __lt__(self,oth):
return self.data[-1]<oth.data[-1]
def __repr__(self):
return [*self.data].__repr__()
class Bill:
def __init__(self):
self.head=Node()
def __getitem__(self,idx):
if idx<-1:
return None
cur=self.head
while idx>-1 and cur!=None:
cur=cur.next
idx-=1
return cur
def __str__(self):
cur=self.head.next
li=[]
while cur!=None:
l=list(cur.data)
if len(l)==1:
li.append(l[0])
else:
li.append(l)
cur=cur.next
return li.__str__()
def __len__(self):
cnt=0
cur=self.head.next
while cur!=None:
cur=cur.next
cnt+=1
return cnt
def __iter__(self):
self.cur=self.head
return self
def __next__(self):
self.cur=self.cur.next
if self.cur!=None:
return self.cur
else:
raise StopIteration
def insert(self,idx,*data):
newnode=Node(*data)
pre=self[idx-1]
newnode.next=pre.next
pre.next=newnode
def remove(self,idx=0):
pre=self[idx-1]
pre.next=pre.next.next
def _findall(self,*data_key):
cur=self.head.next
li=[]
while cur!=None:
flag=True
for i in data_key:
if cur.data[i[-1]]!=i[0]:
flag=False
break
if flag:
li.append(cur)
cur=cur.next
return li
def findall(self,data,key=0):
return self._findall((data,key))
def append(self,*data):
self.insert(len(self),*data)
def average(self):
return sum([i.data[-1] for i in self])/len(self)
if __name__=="__main__":
b=Bill()
## n=int(input())
## for i in range(n):
## date,project,value=input('日期:'),input('项目:'),int(input('支出:'))
## b.append(date,project,value)
b.append('11.08','买零食',139)
b.append('11.08','租服装',1900)
b.append('11.20','买零食',200)
print(b)
print(min(b).data[-1])
print(max(b).data[-1])
print(b.average())
## date=input('查询日期:')
## b.findall(date,0)
## proj=input('查询项目:')
## b.findall(proj,1)
## date,proj=input('查询日期:'),input('查询项目:')
## b._finall((date,0),(proj,1))
print(b.findall('11.20',0))
print(b.findall('买零食',1))
print(b._findall(('11.08',0),('租服装',1)))
##输出:
##[['11.08', '买零食', 139], ['11.08', '租服装', 1900], ['11.20', '买零食', 200]]
##139
##1900
##746.3333333333334
##[['11.20', '买零食', 200]]
##[['11.08', '买零食', 139], ['11.20', '买零食', 200]]
##[['11.08', '租服装', 1900]]
然后,我们一点点来看我们对刚才的代码做了什么改动。
首先,我们看一下数据的存储方式,三项数据(日期、项目、支出)按0、1、2的序号放入data
包中,并用它们的序号来完成访问。
其次,我们看节点类,我们将__lt__
类直接重载为比较data[-1]
的大小,即直接比较支出项的大小,因为本题没有要求按日期或项目比较;此外我们还重载了节点类的__repr__
方法,因为我们待会需要print最小和最大节点。
然后,就是链表类了,其他地方基本没动(甚至remove
方法和reverse
方法都可以删掉),动的地方就是search
变成了findall
,因为我们要找到所有符合要求的支出。与search
相比,findall
将原本return
的操作改成了append
,即,将所有满足要求的支出放在列表里返回。另外就是average
方法了,得益于重载了__iter__
,我们可以直接用列表推导和sum
函数来完成平均值的求取。
随着这一章的结束,相信我们对于Python的面向对象的有关知识已经有了更深一层的了解。