Python数据结构之链表

基础知识

链表是各对象按线性顺序排列的数据结构。与数组相比,数组的线性顺序是由数组下标决定的,而链表的顺序则是由各个对象里的指针(这里是广义上的指针,并不是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 i1 ,然后将 i − 1 i-1 i1 指向 x x x ,并将 x x x 指向原本 i − 1 i-1 i1 的后继。

同样的道理,我们可以编写删除操作:

def remove(self,idx=0):
    pre=self[idx-1]
    pre.next=pre.next.next

要删除序号为 i i i 的元素,我们只需将序号为 i − 1 i-1 i1 的元素指向 i i i 的后继即可(这样一来原本的 i i i 仍然是存在的,不过我们找不到它了)。

修改则更加简单了,重载__setitem__即可,此处略。

至于查找,只要像重载__getitem__那样遍历即可。不过,我们希望可以用多个关键词查找,因此我们将datakey打包成一个二元元组,并以包的形式传入:

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的面向对象的有关知识已经有了更深一层的了解。

你可能感兴趣的:(Python笔记)