python3实现双向链表图文详解

在上一节《python3实现单向链表图文详解》中我们实现了单向链表,但是单向链表受其只能从前往后进行遍历的限制,在有的时候效率并不是很高,例如需要从特定结点往前进行查询时。这一节我们就将其改进版本双向链表也来实现一下。当然单向链表的改进并不只有双向链表这一种,在以后的文章中我们再实现下单向循环链表甚至双向循环链表。

双向链表

和单向链表的结构比起来,双向链表的每个节点除了包含后继结点的地址,还包含前驱结点的地址

1-dll.png

这样在进行类似从某结点往前查找的时候就不用再从头遍历了,但是每个节点占用的内存就多了,属于是典型的空间换时间

下面直接看看代码实现。

python3实现

同样和单向链表一样,先搞定结点再搞定链表类。

结点类

只是比单向链表多个一个前驱的地址,加上一个实例属性即可

class Node(object):
    def __init__(self, value):
        self.value = value
        self.next = None
        self.prev = None

这里的prev就是前驱的地址,同样默认为None

链表类

构造函数没啥区别,同样是一个头地址即可,依然只供内部访问

属性说明类型
__head指向第一个结点的地址对象属性,仅内部使用

最后还是和单向链表一样实现下面这些方法

方法说明类型
isEmpty链表长度为0返回True,否则返回False对象方法
length返回链表数据的长度对象方法
travel遍历整个链表,依次打印每个结点的值对象方法
append在链表尾端添加结点对象方法
shift在链表头部添加结点对象方法
insert在指定下标添加结点对象方法
remove删除第一次出现的某个值对象方法
exist检查某个值是否在链表中对象方法

仔细想一想大家应该就能发现,这上面的isEmptylengthtravelexist这四个方法的实现逻辑并没有用到前驱结点的地址,所以和单向链表的实现方法是一模一样,完全不用改。

既然是面向对象,那我们完全可以import前面实现单向链表的类,继承下来实现双向链表,这样这四个方法就不用重写了,只需要关心另外的四个即可。

下面来逐一看看剩下的四个方法。

shift

从头插入结点,如下图所示

2-shift.png

还是秉承着先建立后断开的原则,第一步先将新结点的next指向原先的第一个结点,然后将self.__head指向新结点,最后将原先第一个结点的prev也指向新结点即可。

当然还是要考虑原先是空链表的特殊情况,这时候只需要将self.__head指向新结点即可。

def shift(self, value):
	node = Node(value)
	if self.isEmpty() == True:
		self.__head = node
	else:
		# link before break
		node.next = self.__head
		self.__head.prev = node
		self.__head = node
append

尾部插入就简单得多,同样也是游标移动到最后一个结点的时候,将原先最后节点的next指向新结点,不过这里要多的一步就是将新结点的prev也指向原先的最后结点。

同样也要考虑原先是空链表的特殊情况,也是直接将新结点赋值给self.__head即可

def append(self, value):
    node = Node(value)
    if self.isEmpty():
        self.__head = node
    else:
        cur = self.__head
        while cur.next != None:
            cur = cur.next
        cur.next = node
        node.prev = cur

只有最后一句代码是新加的。

insert

任意位置插入结点就稍微复杂一点,因为涉及到一共四根线的断开和连接

3-insert.png

同样秉承先建立后断开的原则,先将新结点的prevnext指向前后两个结点,然后再修改原先前驱结点的next以及后继结点的prev,指向新结点。

值得一提的就是,在单向链表中因为要操作前驱结点,游标只能提留在前驱结点上。而在双向链表中,则没有这个限制,游标可以在后继结点上。体现在代码上就是循环的退出条件

def insert(self, pos, value):
    """insert a node at specific position"""
    if pos <= 0:
        self.shift(value)
    elif pos > (self.length() - 1):
        self.append(value)
    else:
        node = Node(value)
        cur = self.__head
        count = 0
        while count < pos:
            cur = cur.next
            count += 1
        node.next = cur
        node.prev = cur.prev
        cur.prev.next = node
        cur.prev = node

注意这里的循环退出从pos-1改为了pos使得游标停留在后继结点上。

同时最后这四句代码,如果换一种顺序也可以写作

node.next = cur
node.prev = cur.prev
cur.prev = node
node.prev.next = node

因为先修改了后继结点的prev指向,所以只能通过新结点去找到前驱结点。方法不止一种,主要是理解思路。

remove

删除一个结点

4-remove.png

比之前的单向链表要多删除一个后继结点的prev引用即可。

补充一下python的内存清理机制,是某内存的值被别的变量引用的个数减为0的时候就被清理。所以当被删除的结点被前驱结点的next和后继结点的prev引用都删除之后,其引用个数减为0,地址就会被回收。而它的pre和next无论指向的是不是None都无所谓

同时因为涉及到删除后继结点的prev,当后继结点为None的时候也要考虑进去,也就是只有一个结点的时候

def remove(self, value):
    cur = self.__head
    # remove the first node
    if cur.value == value:
        if cur.next is None:
            self.__head = None
        else:
            self.__head = cur.next
            cur.next.prev = None
        return value
    elif self.__head is None:
        return
    else:
        while cur.next != None:
            if cur.value == value:
                cur.next.prev = cur.prev
                cur.prev.next = cur.next
                return value
            else:
                cur = cur.next
        if cur.value == value:
            cur.prev.next = None
            return value
        return

好,还是和单向链表一样的代码去验证

if __name__ == "__main__":
    dll = DoubleleLinkedList()
    
    print(dll.exist(10))  # False
    print(dll.length())  # 0
    print(dll.isEmpty()) # True

    dll.append(1)
    print(dll.length())  # 1
    print(dll.isEmpty())  # False

    dll.append(2)
    dll.append(3)
    dll.append(4)
    dll.append(5)
    dll.append(6)
    dll.shift(8)  # 8123456
    dll.insert(2, 9)  # 81923456
    dll.insert(-2, 10)  # 10 81923456
    dll.insert(100, 11)  # 10 81923456 11   
    dll.travel()  # 10 8 1 9 2 3 4 5 6 11
    print(dll.length())  # 10
    print(dll.isEmpty())  # False
    print(dll.exist(100))  # False
    print(dll.exist(6))  # True

    dll.remove(6)
    dll.remove(10)
    dll.remove(11)
    print(dll.exist(6))  # False
    
    dll.travel()  # 8 1 9 2 3 4 5

最后的结果也是没问题

False
0
True
1
False
10 8 1 9 2 3 4 5 6 11 
10
False
False
True
False
8 1 9 2 3 4 5 

和单向链表对比

目前似乎还看不太出来和单向链表对比时间复杂度上的提升,这是因为我们的链表类只保留了__head这一个头部地址。如果再加一个__tail保留尾部地址的话,当进行从后往前遍历的时候就容易多了,例如进行类似dll[-1]的操作。当然这也同样是用空间换时间了。

完整代码

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time: 2020-Jul-09
# @Author: xiaofu

class Node(object):
    def __init__(self, value):
        self.value = value
        self.next = None
        self.prev = None


class DoubleLinkedList(object):
    def __init__(self, node=None):
        self.__head = node

    def isEmpty(self):
        """check if the list is empty"""
        return self.__head is None

    def length(self):
        """length of the list"""
        # cur is current position
        cur = self.__head
        count = 0
        while cur != None:
            cur = cur.next
            count += 1
        return count

    def travel(self):
        """traverse through the list"""
        # cur is current position
        cur = self.__head
        while cur != None:
            print(cur.value, end=" ")
            cur = cur.next
        print('')

    def shift(self, value):
        """add a node to the start"""
        node = Node(value)
        if self.isEmpty() == True:
            self.__head = node
        else:
            # link before break
            node.next = self.__head
            self.__head.prev = node
            self.__head = node

    def append(self, value):
        """add a node to the end"""
        node = Node(value)
        if self.isEmpty():
            self.__head = node
        else:
            cur = self.__head
            while cur.next != None:
                cur = cur.next
            cur.next = node
            node.prev = cur

    def insert(self, pos, value):
        """insert a node at specific position"""
        if pos <= 0:
            self.shift(value)
        elif pos > (self.length() - 1):
            self.append(value)
        else:
            node = Node(value)
            cur = self.__head
            count = 0
            while count < pos:
                cur = cur.next
                count += 1
            node.next = cur
            node.prev = cur.prev
            cur.prev.next = node
            cur.prev = node

    def remove(self, value):
        """remove a node from list when a value first appears"""
        cur = self.__head
        # remove the first node
        if cur.value == value:
            if cur.next is None:
                self.__head = None
            else:
                self.__head = cur.next
                cur.next.prev = None
            return value
        # elif self.length() == 0:
        elif self.__head is None:
            return
        else:
            while cur.next != None:
                if cur.value == value:
                    cur.next.prev = cur.prev
                    cur.prev.next = cur.next
                    return value
                else:
                    cur = cur.next
            if cur.value == value:
                cur.prev.next = None
                return value
            return

    def exist(self, value):
        """whether a node exists in list"""
        cur = self.__head
        while cur != None:
            if cur.value == value:
                return True
            else:
                cur = cur.next
        return False


if __name__ == "__main__":
    dll = DoubleLinkedList()

    print(dll.exist(10))
    print(dll.length())
    print(dll.isEmpty())

    dll.append(1)
    print(dll.length())
    print(dll.isEmpty())

    dll.append(2)
    dll.append(3)
    dll.append(4)
    dll.append(5)
    dll.append(6)
    dll.shift(8)
    dll.insert(2, 9)
    dll.insert(-2, 10)
    dll.insert(100, 11)
    dll.travel()
    print(dll.length())
    print(dll.isEmpty())
    print(dll.exist(100))
    print(dll.exist(6))

    dll.remove(6)
    dll.remove(10)
    dll.remove(11)
    print(dll.exist(6))

    dll.travel()

我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值