数据结构与算法之Python实现——循环链表、双向循环链表

在前面我们学习了单链表,本期将介绍循环链与双链表以及它们的相关操作,在最后会给具体案例来实现双链表的应用

🍁 循环链表

在单链表中,如果我们要遍历链表中的最后一个元素,我们就得从头结点开始一个一个地遍历,但当我们遍历到最后一个元素,这时我们想继续遍历前面的结点,就又要手动从头开始。

为了避免这个麻烦,也就是保持遍历的“不间断”,我们希望遍历到最后一个结点后,下一个结点就是头结点,然后就可以一直不间断地遍历下去。于是循环链表就这样出现了。(em…虽然这样解释有点牵强)

那么循环链表如何实现呢?首先我们需要解决如何将链表初始化的问题,也就是使单链表构成一个循环
在这里插入图片描述

🍃 循环链表的初始化

表的结构和结点的结构还是可以和单链表一样,不用做啥改动

class LNode:
    def __init__(self,elem):
        self.elem = elem
        self.next_ = None

class LList:
    def __init__(self):
        self.head = None

然后就是所谓的初始化

    # 尾插法添加元素
    def insert_tail(self,elem):
        # 生成一个新结点
        node = LNode(elem)
        # 若头结点为空,则直接将元素赋给头结点
        if self.head is None:
            self.head = node
            # 头结点指向头结点形成循环
            node.next_ = self.head
        # 若头结点不为空
        else:
            cur = self.head
            # 找到链表中最后一个结点
            while cur.next_ != self.head:
                cur = cur.next_
            # 新结点指向头结点
            node.next_ = cur.next_
            # 最后一个结点指向新结点
            cur.next_ = node

🍃 获取循环链表的长度

    def get_length(self):
        # 若头结点为空
        if self.head is None:
            return 0
        cur = self.head
        # 若链表中只有一个结点
        if cur.next_ == self.head:
            return 1
        # 计数器
        count = 0
        # 若链表中元素超过一个,找到最后一个元素,此时并未算上最后一个元素
        while cur.next_ != self.head:
            count += 1
            cur = cur.next_
        # 算上最后一个元素
        count += 1
        return count

🍃 按下标删除循环链表中的元素

    def delete_sub(self,pos):
        # 若表为空
        if self.head is None:
            print("The list is empty!")
            return
        # 若表中只有一个数据
        if self.head.next_ == self.head:
            self.head = None
        # 若表有5个数据,想删除第3个,输入2、输入7、输入12,最后pos都为2
        pos = pos % self.get_length()
        cur = self.head
        # 找到被删除元素的前一个元素
        for i in range(self.get_length() + pos - 1):
            cur = cur.next_
        # 使被删除元素的其前一个元素指向被删除元素的下一个元素
        cur.next_ = cur.next_.next_
        # 注意!若删除的是头结点,那么此时头结点就没了,若再次遍历就会出错,所以这时需要重设头结点
        if pos == 0:
            self.head = cur.next_
  • 关于pos = pos % self.get_length()

其实自己再草稿本上算一算也可以得到答案的,这里举个例。若链表中有7个元素,我们输入的是15,则 p o s = 15 % 7 = 1 pos = 15\%7=1 pos=15%7=1,那么就会删除下标为1的元素。

为什么要这样做?若输入下标在长度范围内,就不用设这一步,因为是循环链表,所以我们要考虑一下输入下标超出长度的情况,这一步也是为了方便后面找到被删除元素的前一个元素,同样,也是为了将删除头结点的情况结合到一起(若不这样,删除头结点需单独地操作)

  • 关于self.head = cur.next_

在这里插入图片描述
可以看到,head完全脱离了链表,如果此时想遍历再执行cur = l.head操作,就不能得到我们想要的结果。所以要使head指向cur.next_,这样head就又重新“回到”了链表中

🍃 按下标查找循环链表中元素的值

    def search_sub(self,pos):
        pos = pos % self.get_length()
        cur = self.head
        for i in range(pos):
            cur = cur.next_
        return cur.elem

🍃 验证循环链表的操作

❗️注意:在添加、删除操作后,都要重新执行一遍cur = l.head(因为前面也有对cur的操作,所以需要再初始化一遍,因为自己被这个卡了一会0.0)。

data = list(map(int,input("Please input a series of datas and split them by spaces:").split()))
l = LList()
# 初始化
print('Initiate')
for i in range(len(data)):
    l.insert_tail(data[i])

cur = l.head
for i in range(l.get_length()):
    print(cur.elem)
    cur = cur.next_
print('--------------------------------')
# 添加元素
print('Add new elem')
l.insert_tail(6)
cur = l.head
for i in range(l.get_length()):
    print(cur.elem)
    cur = cur.next_
print('--------------------------------')
# 删除元素
print('Delete elem')
l.delete_sub(0)
cur = l.head
for i in range(l.get_length()):
    print(cur.elem)
    cur = cur.next_
print('--------------------------------')
# 查找元素
print('Search elem')
print('The value of No.5 is: %d' % l.search_sub(4))

执行结果如下:
在这里插入图片描述

🍃 说明

对于循环链表的操作有很多种,就添加元素来说,我这里用的是尾插法,你还可以用前插法,一般插法;就删除元素来说,不仅可以按下标删除,也可以按值删除,还可以删除多个值相同的元素;就查找元素来说,可以按下标查找一个元素的值,也可以按值查找一个元素的下标…

我想说明的是,对于一个循环链表,不仅仅是循环链表,操作是有很多种的,需要对具体情况设计具体的操作,我们学习这个的目的就是了解这种结构,并且锻炼自己写代码的思维和能力。

不要局限于客观你所看到的,要发散自己的思维,要举一反三,将理论与实际相结合,才是王道👍。

🍁 双向链表

有了循环链表后,在某些方便的遍历会方便许多,但有时候还是很麻烦,比如我在想循环链表的删除操作时,我就只能遍历到要被删除元素的前一个元素,这就很伤脑筋的说(因为一般的遍历肯定是直接遍历到要操作的结点嘛)。

如果遍历到被删除元素时,我们能够在掉过头来遍历前一个元素的话,那么就更方便了,于是双向链表就这样出现了~

双向嘛,讲究的就是一个双向奔赴,你奔向我,我也奔向你~咳咳,直接一点呢就是在两个结点间有两个箭头,前一个指向后一个,后一个也指向前一个。如下图:
在这里插入图片描述

这里呢,原本双向链表是没有循环的,我在作图的时候突发奇想想把这个两个结合起来试试。那么这个标题看来就应该是双向循环链表了!

🍃 双向循环链表的初始化

对于结点类和表类,可以在循环链表上继承,也可以重新写。

class DLNode:
    def __init__(self,elem):
        self.elem = elem
        self.next_ = None
        self.last_ = None

class DLList:
    def __init__(self):
        self.head = None

这里新加了一个指针域last_指向前一个结点。

    def insert_tail(self,elem):
        # 生成一个新结点
        node = DLNode(elem)
        # 若头结点为空
        if self.head is None:
            self.head = node
            # node指向头结点
            node.next_ = self.head
            # 头结点反过来指向node,这两句很重要,不然此时头结点的next_和last_可能为空,不方便后续的添加元素
            self.head.last_ = node
        # 若头结点不为空
        else:
            cur = self.head
            # 找到头结点的前一个结点
            while cur.next_ != self.head:
                cur = cur.next_
            # cur指向node
            cur.next_ = node
            # node反过来指向cur
            node.last_ = cur
            # node又指向头结点构成单循环
            node.next_ = self.head
            # 头结点又指向node构成双循环
            self.head.last_ = node

这里依旧采用的尾插法添加元素

🍃 获取双向循环链表的长度

这个跟循环链表那个操作差不多,当然也可以从其它方面来计算链表的长度。

    def get_length(self):
        count = 0
        # 若头结点为空,即链表中元素个数为0
        if self.head is None:
            return count
        cur = self.head
        # 找到头结点的前一个结点
        while cur.next_ != self.head:
            count += 1
            cur = cur.next_
        # 因为循环的条件,头结点前一个结点并未记上,所以这里要加一
        count += 1
        # 注意要返回,之前调试的时候又是这里错了~_~
        return count

🍃 按下标删除双向循环链表种的元素

    def delete_sub(self,pos):
        cur = self.head
        pos = pos % self.get_length()
        # 若链表为空
        if self.head is None:
            raise Exception('The linked list is none!')
        # 若链表中只有一个元素
        elif self.get_length() == 1:
            self.head = None
        # 若链表中有两个元素
        elif self.get_length() == 2:
            # 找到被删除结点
            for i in range(pos):
                cur = cur.next_
            # 使另一个结点成为头结点
            self.head = cur.next_
            # 将两个指针域指向自身
            self.head.next_ = self.head
            self.head.last_ = self.head
        else:
            # 找到被删除结点
            for i in range(pos):
                cur = cur.next_
            # 使被删除结点的上一个结点指向被删除结点的下一个结点
            cur.last_.next_ = cur.next_
            # 使被删除结点的下一个结点反过来指向被删除结点的上一个结点
            cur.next_.last_ = cur.last_
            # 若删除的是头结点,则使头结点的下一个结点成为头结点
            if pos == 0:
                self.head = cur.next_

🍃 验证双向循环链表的操作

data = list(map(int,input('Please input a series of datas by spaces:').split()))
dl = DLList()
# 初始化
for i in range(len(data)):
    dl.insert_tail(data[i])
cur = dl.head
# 打印链表中的元素
print('The linked list is:')
for i in range(dl.get_length()):
    print(cur.elem,end=' ')
    cur = cur.next_
print('\n')
print('------------------------------------')
# 添加元素
dl.insert_tail(6)
cur = dl.head
print('The new linked list is:')
for i in range(dl.get_length()):
    print(cur.elem,end=' ')
    cur = cur.next_
print('\n')
print('------------------------------------')
# 清空链表
print('Start to clear up')
for i in range(dl.get_length()):
    dl.delete_sub(0)
    cur = dl.head
    for j in range(dl.get_length()):
        print(cur.elem,end=' ')
        cur = cur.next_
    print('\n')
print('The length of the linked list is:%d' % dl.get_length())

执行结果如下:
在这里插入图片描述

也许你会问为啥有关双向循环链表的操作这么少,其实吧,我也只是提供一些思路,想到哪些写哪些~(小声一点说就是懒了( ´◔ ‸◔`))。

下面就是具体案例的实现了,让我们一步一步地来设计!

🍁 案例实现——核酸检测登记表

首先,要用循环双链表实现核酸检测登记表(某一天的信息)的话,我们需要先确定我们需要录入的信息:姓名,性别,年龄,手机号,做核酸的时间,其中姓名、性别、手机号、做核酸时间都用字符串型。那么结点的结构如下:

class PerNode:
    def __init__(self,name,gender,age,telenum,date):
        self.name = name
        self.gender = gender
        self.age = age
        self.telenum = telenum		# 电话号码
        self.date = date
        self.next_ = None
        self.last_ = None

然后就是链表的结构和功能的设计:

  • 录入信息,也就是添加元素
  • 删除信息,也就是删除元素
  • 查询信息,也就是查找元素

基本的功能就是上面三个,如果还需要其它信息,再作改进。而添加和删除功能直接将上面代码套过来就可以了,不用做太大的改动。如下👇:

class PerList:
    def __init__(self):
        self.head = None

    def get_length(self):
        count = 0
        if self.head is None:
            return count
        cur = self.head
        while cur.next_ != self.head:
            count += 1
            cur = cur.next_
        count += 1
        return count

    def add(self,id,name,gender,age,telenum,date):
        node = PerNode(id,name,gender,age,telenum,date)
        if self.head is None:
            self.head = node
            self.head.next_ = self.head
            self.head.last_ = self.head
        else:
            cur = self.head
            while cur.next_ != self.head:
                cur = cur.next_
            cur.next_ = node
            node.last_ = cur
            node.next_ = self.head
            self.head.last_ = node

    def delete(self,pos):
        cur = self.head
        pos = pos % self.get_length()
        if self.head is None:
            print('The list is empty!!')
            return

        elif self.get_length() == 1:
            self.head = None

        elif self.get_length() == 2:
            for i in range(pos):
                cur = cur.next_
            self.head = cur.next_
            self.head.next_ = self.head
            self.head.last_ = self.head

        else:
            for i in range(pos):
                cur = cur.next_
            cur.last_.next_ = cur.next_
            cur.next_.last_ = cur.last_
            if pos == 0:
                self.head = cur.next_

    def search(self,id):
        cur = self.head
        for i in range(self.get_length()):
            if cur.id == id:
                return cur
            cur = cur.next_
        print('Searching fails.The data does not exist!')

在调试的时候发现一个问题,如果每个人的数据有一个序号的话,删除一个数据后其它数据的序号是没有变的,这样在查找时只有按照删除前的序号进行查询,这样是十分麻烦的,所以我们还需写一个删除后重新排列序号的函数,如下:

    def rearrange(self,pos):
        pos = pos % self.get_length()
        cur = self.head
        # 若删除后只剩一个结点,需判断它的id是否为1,若不为1,也就是为2,则需要减1
        # 若它的id是1,则不用减
        if self.get_length() == 1:
            if cur.id != 1:
                cur.id -= 1
            return
        # 删除一个结点后剩余结点大于两个的情况
        else:
            # 找到被删除结点的位置,这个位置可能会被其它结点顶替也可能不存在
            for i in range(pos):
                cur = cur.next_
            # 因为删除结点后是后面的结点往前补上来,所以后面的结点都要减1(注意这里就理解为单链表就行了)
            cur.id -= 1
            # 对该操作不作循环,所以按照单链表的方式遍历即可
            while cur.next_ != self.head:
                cur = cur.next_
                cur.id -= 1

然后就是具体操作了,请看👇:

# 检测表的初始化
tt = PerList() # Test table,检测表
print('------------------------------Start typing information--------------------------------') # 开始录入信息
flag = 1 # 用来作为循环的条件
count = 1 # 自动改变每个人员的id,例如增加一个人后它的id就自动加1
while flag == 1:
    print('No.%d' % count)
    name = input('Please input the name:') # 输入人员的姓名
    gender = input('Please input the gender:') # 输入人员的性别,male为男性,female为女性
    age = int(input('Please input the age:')) # 输入人员的年龄
    telenum = input('Please input the phone number:') # 输入人员的电话号码
    date = input('Please input the typing date:') # 输入核酸检测的时间
    tt.add(count,name,gender,age,telenum,date)
    print('If you want to continue ,please input 1,else input 0:----',end=' ') # 如果你想继续录入,请输入1,不想则输入0
    flag = int(input())
    if flag == 1:
        count += 1
        print('\n')
print('------------------------------The end of the entry------------------------------------') # 录入结束

# 输出人员信息表
print('\n')
print('------------------------------The list personnal information sheet--------------------') # 人员信息表
cur = tt.head
print('%-20s%-20s%-20s%-20s%-20s%-20s' % ('id','name','gender','age','telenum','date'))
for i in range(tt.get_length()):
    print('%-20d%-20s%-20s%-20d%-20s%-20s' % (cur.id,cur.name,cur.gender,cur.age,cur.telenum,cur.date))
    cur = cur.next_
print('\n')

# 删除人员表
print('------------------------------Delete the information------------------------------------------') # 删除信息
print('Do you want to delete the information? If so input 1,else input 0:',end=' ') # 你是否想删除信息,是请输入1,不是输入0
flag = int(input())
while flag == 1:
    print('Please input the number of the data that you want to delete:',end=' ') # 请输入你想删除人员的id
    pos = int(input())
    tt.delete(pos - 1)
    print('Now the length of the list is: %d,and the list is displayed as below:' % tt.get_length()) # 输入删改后表的长度和表的内容
    cur = tt.head
    print('%-20s%-20s%-20s%-20s%-20s%-20s' % ('id', 'name', 'gender', 'age', 'telenum', 'date'))
    for i in range(tt.get_length()):
        print('%-20d%-20s%-20s%-20d%-20s%-20s' % (cur.id, cur.name, cur.gender, cur.age, cur.telenum, cur.date))
        cur = cur.next_
    print('Do you want to end up deleting?Just input 0,or input 1 to continue:',end=' ') # 你是否想结束删除操作,是输入0,继续输入1
    flag = int(input())
cur = tt.head
print('%-20s%-20s%-20s%-20s%-20s%-20s' % ('id','name','gender','age','telenum','date'))
for i in range(tt.get_length()):
    print('%-20d%-20s%-20s%-20d%-20s%-20s' % (cur.id,cur.name,cur.gender,cur.age,cur.telenum,cur.date))
    cur = cur.next_
print('-----------------------------The end of deleting---------------------------------------') # 删除操作结束
print('\n')

# 查找数据
print('-----------------------------Search the date-------------------------------------------') # 查询表
print('Do you want to search the data?If so input 1,else input 0:',end=' ') # 你是否想查询表,是输入1,不是则输入0
flag = int(input())
while flag == 1:
    print('Please input the value of the id that you want to search:',end=' ') # 请输入你想查找人员的id
    pos = int(input())
    p = tt.search(pos)
    if p == None:
        break
    print('%-20s%-20s%-20s%-20s%-20s%-20s' % ('id', 'name', 'gender', 'age', 'telenum', 'date'))
    print('%-20d%-20s%-20s%-20d%-20s%-20s' % (p.id, p.name, p.gender, p.age, p.telenum, p.date))
    print('Do you want to end up searching? Just input 0,or input 1 to continue:',end=' ') # 你是否想结束查找,是输入0,继续输入1
    flag = int(input())
print('Operation ends.') # 操作结束

咱们来看一看结果:

首先是输入数据:
在这里插入图片描述
在这里插入图片描述
然后我们发现Alice的核酸时间明显有错,因为在一个时间段做核酸时间肯定是连起来的嘛,所以我们需要将她的信息删除,如下:
在这里插入图片描述
最后就是查询信息了在这里插入图片描述
这个操作就结束了。

🍁 总结

本篇呢,主要就是讲的循环链表这样一个知识以及对它的应用,也许你看到这也会有跟我一样的一个疑问:貌似就介绍的时候用了一些“循环”吧,其它循环体现在哪里呢?

后来我又仔细想了想,当然是体现在对链表的操作上了!本来循环链表就是为了某些操作更方便而实现的,且应用的时候具体实现细节别人也看不到呀!所以不用太过纠结这个问题~

这个呢,也是我第一次将代码的应用写得这么详细,写代码不难,难就难在调试啊找bug啊,这个真是有苦说不出呀
在这里插入图片描述
好在功夫不负有心人,咱终于就是给它搞定了。不过仍有很多不足的地方,像最后的循环链表的应用,对于查找,我们可以按照id查找,也可以按照姓名,手机号等等查找,且不仅可以用顺序查找,还可以用折半查找、分块查找等等!

所以说,路还很长,任重而道远呐

  • 5
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值