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


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

    🍁 循环链表

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

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

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

    🍃 循环链表的初始化

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

    class LNode:
        def __init__(self,elem):
            self.elem = elem
            self.next_ = None
    
    class LList:
        def __init__(self):
            self.head = None
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    然后就是所谓的初始化

        # 尾插法添加元素
        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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    🍃 获取循环链表的长度

        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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

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

        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_
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 关于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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    🍃 验证循环链表的操作

    ❗️注意:在添加、删除操作后,都要重新执行一遍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))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

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

    🍃 说明

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

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

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

    🍁 双向链表

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

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

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

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

    🍃 双向循环链表的初始化

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

    class DLNode:
        def __init__(self,elem):
            self.elem = elem
            self.next_ = None
            self.last_ = None
    
    class DLList:
        def __init__(self):
            self.head = None
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这里新加了一个指针域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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

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

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

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

        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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

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

        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_
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

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

    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())
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

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

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

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

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

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

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

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

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

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

    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!')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

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

        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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

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

    # 检测表的初始化
    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.') # 操作结束
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69

    咱们来看一看结果:

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

    🍁 总结

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

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

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

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

  • 相关阅读:
    PP-ChatOCRv2、PP-TSv2、大模型半监督学习工具...PaddleX新特性等你来pick!
    设计模式之适配器模式C++实现
    Mysql_14 存储引擎
    论文相关知识:扁平化设计
    我与足球以及世界杯的过往
    第一行代码 Android 第九章9.1-9.2(WebView的用法,使用HttpURLConnect发送HTTP请求,使用OkHttp发送HTTP请求)
    bootstrap导航窗格响应式二级菜单
    CTF 代码审计之绕过过滤的空白字符
    Intel 高性能库之IPP信号处理简介及下载(版本5.1、6.1,含32位和64位及注册)
    Opencv_3_图像对象的创建与赋值
  • 原文地址:https://blog.csdn.net/weixin_62917800/article/details/127640512