Python数据结构之双向链表详解

目录
  • 0. 学习目标
  • 1. 双向链表简介
    • 1.1 双向链表介绍
    • 1.2 双向链表结点类
    • 1.3 双向链表优缺点
  • 2. 双向链表实现
    • 2.1 双向链表的初始化
    • 2.2 获取双向链表长度
    • 2.3 读取指定位置元素
    • 2.4 查找指定元素
    • 2.5 在指定位置插入新元素
    • 2.6 删除指定位置元素
    • 2.7 其它一些有用的操作
  • 3. 双向链表应用
    • 3.1 双向链表应用示例
    • 3.2 利用双向链表基本操作实现复杂操作

0. 学习目标

单链表只有一个指向直接后继的指针来表示结点间的逻辑关系,因此可以方便的从任一结点开始查找其后继结点,但要找前驱结点则比较困难,双向链表是为了解决这一问题,其使用两个指针表示结点间的逻辑关系。在上一节中我们已经讨论了单链表及其相关操作的实现,本节中我们将重点讨论双向链表及其相关操作的实现。

通过本节学习,应掌握以下内容:

双向链表的基本概念及其优缺点

双向链表基本操作的实现

利用双向链表的基本操作实现复杂算法

1. 双向链表简介

1.1 双向链表介绍

双向链表 (doubly linked list) 与单链表相似,同样使用结点和指针的相关概念,属于顺序表的链式存储结构,单链表和双向链表的唯一区别在于双向链表是用两个指针表示结点间的逻辑关系,增加了一个指向其直接前驱的指针域,这样形成的链表有两条不同方向的链——前驱链和后继链,因此称为双向链表,或称为双链表。

1.2 双向链表结点类

在双向链表中,根据已知结点查找其直接前驱结点可以与查找其直接后继结点一样方便。与单链表相同,双向链表同样可以分为带有头结点和不带头结点两类,本节仅讨论带头结点的双向链表。双向链表的结点示意图如下所示,每个结点都有两个指针——指向直接后继的指针 next 和指向直接前驱的指针 previous:

用 Python 实现双向链表结点类如下:

class Node:
    def __init__(self, data=None):
        self.data = data
        self.next = None
        self.previous = None

    def __str__(self):
        return str(self.data)

previous 变量指向直接前驱结点,而 next 变量保留对直接后继结点的引用,而 data 变量用于存储数据,重载 __str__ 方法用于便于打印结点对象。

1.3 双向链表优缺点

双向链表的优点在于给定双向链表中的一个节点,我们可以双向遍历,直接访问它的前驱结点,这样在需要查找前驱的操作中,就不必再从头开始遍历整个链表,极大的方便了诸如删除结点等操作。

而双向链表的主要缺点如下:

  • 每个结点需要一个额外的前驱指针,需要更多的空间;
  • 结点的插入或删除需要更多的指针修改操作。

2. 双向链表实现

类似于单链表,接下来让我们实现一个带有头结点的双链表类,并用头指针标识链表的开头,如果你还不了解单链表,可以参考《单链表及其操作实现》相关介绍。

2.1 双向链表的初始化

双向链表的初始化建立一个空的带头结点的单链表,其表长 length 初始化为 0,此时链表中没有元素结点,只有一个头结点:

class DoublyLinkedList:
    def __init__(self, data=None):
        self.length = 0
        # 初始化头结点
        head_node = Node()
        self.head = head_node

创建双向链表 DoublyLinkedList 对象的时间复杂度为O(1)。

NOTE:如果你还记得继承机制的话,我们也可以令 DoublyLinkedList 继承自在《单链表及其操作实现》中实现的 SinglyLinkedList,可以极大的化简双向链表的实现。

2.2 获取双向链表长度

求取双向链表长度只需要重载 __len__ 从对象返回 length 的值,因此时间复杂度为O(1):

    def __len__(self):
        return self.length

2.3 读取指定位置元素

双向链表中读取指定位置元素的算法与单链表完全相同,只需要使用后继链访问每一个结点即可,因此操作的复杂度同样为O(n),接下来我们将重载 __getitem__ 操作实现读取链表指定位置元素的操作;同时,我们希望确保索引在可接受的索引范围内,否则将引发 IndexError 异常:

    def __getitem__(self, index):
        if index > self.length - 1 or index < 0:
            raise IndexError("DoublyLinkedList assignment index out of range")
        else:
            count = -1
            current = self.head
            while count < index:
                current = current.next
                count += 1
            return current.data

类似的,我们也可以实现修改指定位置元素的操作,只需要重载 __setitem__ 操作,其复杂度同样为O(n):

  def __setitem__(self, index, value):
        if index > self.length - 1 or index < 0:
            raise IndexError("DoublyLinkedList assignment index out of range")
        else:
            count = -1
            current = self.head
            while count < index:
                current = current.next
                count += 1

            current.data = value

2.4 查找指定元素

与单链表相同,当查找指定元素时,需要设置一个跟踪链表结点的指针 current,令其顺着 next 域依次指向每个结点,每指向一个结点就判断其值是否等于指定值 value,若是则返回该结点索引;否则继续往后搜索,如果链表中无此元素,则引发 ValueError 异常,其时间复杂度为O(n):

    def locate(self, value):
        count = -1
        current = self.head
        while current != None and current.data != value:
            count += 1
            current = current.next
        if current and current.data == value:
            return count
        else:
            raise ValueError("{} is not in sequential list".format(value))

2.5 在指定位置插入新元素

在指定位置插入新元素有两种不同的方法,一种是找到待插入位置的结点 current,然后将待插结点插入 current 之前;另一种方法是找到待插入位置结点的前驱结点 prev,然后待插结点插入 prev 之后,两种方法的操作略有不同,这里以第二种方法的操作为例,第一种方法的具体操作留给大家进行推导。

由于 prev 指向待插入位置的后继结点,因此如果插入位置为列表末尾,由于 prev.next = None,无法使用 prev.next.previous,而在链表中间部位 prev.next.previous = prev,所以显然插入链表中间位置和链表末尾的操作有所不同。

(1) 在双向链表的末尾插入一个结点步骤如下:

  • 遍历列表直到最后一个结点,创建新结点;
  • 将新节点的 previous 指针指向链表的最后一个结点;
  • 更新原链表最后一个结点的 next 指针指向新结点。

(2) 在双链表中间插入结点与单链表类似,但是需要更多的步骤用于修改指针:

  • 首先遍历链表到插入位置的前驱结点 prev,创建新结点;
  • 新结点的 next 指针指向要插入新结点位置的下一个节点,新结点的 previous 指针指向 prev;
  • 插入位置后继节点的 previous 指向新节点,prev 结点的 next 指针指向新节点。

算法实现如下所示:

    def insert(self, index, data):
        count = 0
        prev = self.head
        # 判断插入位置的合法性
        if index > self.length or index < 0:
            raise IndexError("DoublyLinkedList assignment index out of range")
        else:
            new_node = Node(data)
            while count < index:
                prev = prev.next
                count += 1
            new_node.previous = prev
            self.length += 1
            if prev.next:
                # 链表中间插入结点
                new_node.next = prev.next
                prev.next.previous = new_node
                prev.next = new_node
            else:
                # 链尾插入结点
                prev.next = new_node

2.6 删除指定位置元素

删除指定位置元素,只需要找到相应位置结点 current,修改指针后,删除结点即可。需要注意的是,除了需要将 current 的前驱结点的 next 指针指向 current 的后继节点外,如果删除的并非链尾元素,还需要将 current 的后继节点的 previous 指针指向 current 的前驱结点:

算法实现如下所示:

    def get_node(self, index):
        """辅助函数,用于根据位置返回结点"""
        if index > self.length - 1 or index < 0:
            raise IndexError("SinglyLinkedList assignment index out of range")
        count = -1
        current = self.head
        while count < index:
            current = current.next
            count += 1
        return current
    def __delitem__(self, index):
        """删除指定位置元素"""
        if index > self.length - 1 or index < 0:
            raise IndexError("SinglyLinkedList assignment index out of range")
        else:
            current = self.get_node(index)
            if current:
                current.previous.next = current.next
            # 如果删除的并非最后一个结点
            if current.next:
                current.next.previous = current.previous
            self.length -= 1
            del current

在插入和删除操作中,都是先确定操作位置,然后再进行插入和删除操作,所以其时间复杂度均为O(n)。

2.7 其它一些有用的操作

2.7.1 链表元素输出操作

将双向链表转换为字符串以便进行打印,使用 str 函数调用对象上的 __str__ 方法可以创建适合打印的字符串表示:

    def __str__(self):
        s = "["
        current = self.head.next
        count = 0
        while current != None:
            count += 1
            s += str(current)
            current = current.next
            if count < self.length:
                s += '<-->'
        s += "]"
        return s

2.7.2 删除指定元素

与删除指定位置元素略有不同,删除指定元素需要在链表中删除第一个具有与给定值相同数据元素的结点,但修改指针的操作是类似的,其时间复杂度同样为O(n):

    def del_value(self, value):
        current = self.head
        while current:
            if current.data == value:
                current.previous.next = current.next
                if current.next:
                    current.next.previous = current.previous
                self.length -= 1
                del current
                return
            else:
                current = current.next
        raise ValueError("The value provided is not present!")

2.7.3 在链表尾部追加新元素

为了方便的在链表尾部追加新元素,可以实现函数 append:

    def append(self, data):
        new_node = Node(data)
        current = self.head
        while current.next:
            current = current.next
        current.next = new_node
        new_node.previous = current
        self.length += 1

此算法的时间复杂度为O(n),如果需要经常在链表尾部追加新元素,可以使用增加尾指针 tail 用于追踪链表的最后一个元素,利用尾指针在链表尾部追加新元素时间复杂度可以降至O(1)。

3. 双向链表应用

接下来,我们首先测试上述实现的双向链表,以验证操作的有效性,然后利用实现的基本操作来实现更复杂的算法。

3.1 双向链表应用示例

首先初始化一个链表 dllist,并在其中追加若干元素:

dllist = DoublyLinkedList()
# 在链表末尾追加元素
dllist.append('apple')
dllist.append('banana')
dllist.append('orange')
# 在指定位置插入元素
dllist.insert(0, 'grape')
dllist.insert(4, 'lemon')

我们可以直接打印链表中的数据元素、链表长度等信息:

print('双向链表 sllist 为:', dllist)
print('双向链表 sllist 长度为:', len(dllist))
print('双向链表 sllist 第0个元素为:', dllist[0])
# 修改数据元素
dllist[0] = 'pear'
del(dllist[3])
print('双向修改链表 sllist 数据后:', dllist)

以上代码输出如下:

双向链表 dllist 为: [grape<-->apple<-->banana<-->orange<-->lemon]
双向链表 dllist 长度为: 5
双向链表 dllist 第0个元素为: grape
修改双向链表 dllist 数据后: [pear<-->apple<-->banana<-->lemon]

接下来,我们将演示在指定位置添加/删除元素、以及如何查找指定元素等:

# 修改数据元素
dllist[0] = 'pear'
print('修改双向链表 dllist 数据后:', dllist)
dllist.insert(0, 'watermelon')
print('在位置 0 添加 watermelon 后双向链表链表 ddlist 数据:', dllist)
del(dllist[3])
print('删除位置 3 处元素后双向链表 ddlist 数据:', dllist)
dllist.append('lemon')
print('在尾部追加元素 lemon 后双向链表 ddlist 数据:', dllist)
dllist.del_value('lemon')
print('删除 lemon 后双向链表 dllist 数据:', dllist)
print('watermelon 在双向链表 dllist 中的索引为:', dllist.locate('orange'))

以上代码输出如下:

修改双向链表 dllist 数据后: [pear<-->apple<-->banana<-->orange<-->lemon]
在位置 0 添加 watermelon 后双向链表链表 ddlist 数据: [watermelon<-->pear<-->apple<-->banana<-->orange<-->lemon]
删除位置 3 后双向链表 ddlist 数据: [watermelon<-->pear<-->apple<-->orange<-->lemon]
在尾部追加元素 lemon 后双向链表 ddlist 数据: [watermelon<-->pear<-->apple<-->orange<-->lemon<-->lemon]
删除 lemon 后双向链表 dllist 数据: [watermelon<-->pear<-->apple<-->orange<-->lemon]
watermelon 在双向链表 dllist 中的索引为: 3

3.2 利用双向链表基本操作实现复杂操作

[1] 利用双向链表的基本操作,合并两个双向链表:

def merge(dllist1, dllist2):
    current = dllist1.head
    while current.next:
        current = current.next
    if dllist2.head.next:
        tmp = dllist2.head.next
        current.next = tmp
        tmp.previous = current
    dllist1.length += len(dllist2)
    return dllist1
# 算法测试
dllist1 = DoublyLinkedList()
dllist2 = DoublyLinkedList()
for i in range(5):
    dllist1.append(i)
    dllist2.append((i+1)*5)
print('双向链表 dllist1:', dllist1)
print('双向链表 dllist2:', dllist2)
dllist = merge(dllist1, dllist2)
print('链表合并结果:', dllist)

程序输出结果如下:

双向链表 dllist1: [0<-->1<-->2<-->3<-->4]
双向链表 dllist2: [5<-->10<-->15<-->20<-->25]
链表合并结果: [0<-->1<-->2<-->3<-->4<-->5<-->10<-->15<-->20<-->25]

到此这篇关于Python数据结构之双向链表详解的文章就介绍到这了,更多相关Python双向链表内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • python实现数据结构中双向循环链表操作的示例

    看此博客之前建议先看看B站的视频python数据结构与算法系列课程,该课程中未实现双向循环链表的操作,所以我按照该视频的链表思路实现了双向循环链表的操作,欢迎大家阅读与交流,如有侵权,请联系博主! 下面附上代码: class Node: def __init__(self, elem): self.elem = elem self.prev = None self.next = None class DoubleCycleLinkList: def __init__(self, node=Non

  • Python双向循环链表实现方法分析

    本文实例讲述了Python双向循环链表实现方法.分享给大家供大家参考,具体如下: 最近身边的朋友在研究用python来实现数据结构.遇到一个问题就是双向循环链表的实现,改指向的时候总是发蒙. 我自己尝实现了一个python的双向循环链表.附上代码,希望对大家有帮助. 如果不懂什么是双向循环链表的伙伴,需要补习一下数据结构的基础之后哦~~~ 在python当中 用一个类Node 来实现链表的节点,节点数据有三个变量: prev:前驱指针: 用于指向当前节点前一个节点 next: 后继指针  用于指

  • python双向链表实现实例代码

    示意图: python双向链表实现代码: 复制代码 代码如下: #!/usr/bin/python# -*- coding: utf-8 -*- class Node(object):    def __init__(self,val,p=0):        self.data = val        self.next = p        self.prev = p class LinkList(object):    def __init__(self):        self.he

  • Python单向链表和双向链表原理与用法实例详解

    本文实例讲述了Python单向链表和双向链表原理与用法.分享给大家供大家参考,具体如下: 链表是一种数据结构,链表在循环遍历的时候效率不高,但是在插入和删除时优势比较大. 链表由一个个节点组成. 单向链表的节点分为两个部分:存储的对象和对下一个节点的引用.注意是指向下一个节点. 而双向链表区别于单向链表的是它是由三个部分组成:存储的对象.对下一个节点的引用.对上一个节点的引用,可以实现双向遍历. 单向列表的结构如下图: head是头节点,tail是尾节点,每个节点由Data存储对象和Next对下

  • Python数据结构之双向链表的定义与使用方法示例

    本文实例讲述了Python数据结构之双向链表的定义与使用方法.分享给大家供大家参考,具体如下: 和单链表类似,只不过是增加了一个指向前面一个元素的指针而已. 示意图: python 实现代码: #!/usr/bin/python # -*- coding: utf-8 -*- class Node(object): def __init__(self,val,p=0): self.data = val self.next = p self.prev = p class LinkList(obje

  • python双向链表原理与实现方法详解

    本文实例讲述了python双向链表原理与实现方法.分享给大家供大家参考,具体如下: 双向链表 一种更复杂的链表是"双向链表"或"双面链表".每个节点有两个链接:一个指向前一个节点,当此节点为第一个节点时,指向空值:而另一个指向下一个节点,当此节点为最后一个节点时,指向空值. 操作 is_empty() 链表是否为空 length() 链表长度 travel() 遍历链表 add(item) 链表头部添加 append(item) 链表尾部添加 insert(pos,

  • Python数据结构之双向链表详解

    目录 0. 学习目标 1. 双向链表简介 1.1 双向链表介绍 1.2 双向链表结点类 1.3 双向链表优缺点 2. 双向链表实现 2.1 双向链表的初始化 2.2 获取双向链表长度 2.3 读取指定位置元素 2.4 查找指定元素 2.5 在指定位置插入新元素 2.6 删除指定位置元素 2.7 其它一些有用的操作 3. 双向链表应用 3.1 双向链表应用示例 3.2 利用双向链表基本操作实现复杂操作 0. 学习目标 单链表只有一个指向直接后继的指针来表示结点间的逻辑关系,因此可以方便的从任一结点

  • Python数据结构之循环链表详解

    目录 0. 学习目标 1. 循环链表简介 2. 循环单链表实现 2.1 循环单链表的基本操作 2.2 简单的实现方法 2.3 循环单链表应用示例 2.4 利用循环单链表基本操作实现复杂操作 3. 循环双链表实现 3.1 循环双链表的基本操作 3.2 循环双链表应用示例 0. 学习目标 循环链表 (Circular Linked List) 是链式存储结构的另一种形式,它将链表中最后一个结点的指针指向链表的头结点,使整个链表头尾相接形成一个环形,使链表的操作更加方便灵活.我们已经介绍了单链表和双向

  • Python数据结构之栈详解

    目录 0.学习目标 1.栈的基本概念 1.1栈的基本概念 1.2栈抽象数据类型 1.3栈的应用场景 2.栈的实现 2.1顺序栈的实现 2.1.1栈的初始化 2.2链栈的实现 2.3栈的不同实现对比 3.栈应用 3.1顺序栈的应用 3.2链栈的应用 3.3利用栈基本操作实现复杂算法 0. 学习目标 栈和队列是在程序设计中常见的数据类型,从数据结构的角度来讲,栈和队列也是线性表,是操作受限的线性表,它们的基本操作是线性表操作的子集,但从数据类型的角度来讲,它们与线性表又有着巨大的不同.本节将首先介绍

  • Python数据结构之队列详解

    目录 0. 学习目标 1. 队列的基本概念 1.1 队列的基本概念 1.2 队列抽象数据类型 1.3 队列的应用场景 2. 队列的实现 2.1 顺序队列的实现 2.2 链队列的实现 2.3 队列的不同实现对比 3. 队列应用 3.1 顺序队列的应用 3.2 链队列的应用 3.3 利用队列基本操作实现复杂算法 0. 学习目标 栈和队列是在程序设计中常见的数据类型,从数据结构的角度来讲,栈和队列也是线性表,是操作受限的线性表,它们的基本操作是线性表操作的子集,但从数据类型的角度来讲,它们与线性表又有

  • Python数据结构之递归方法详解

    目录 1.学习目标 2.递归 2.1递归的基本概念 2.2递归的重要性 2.3递归三原则 2.4递归的应用 3.递归示例 3.1列表求和 3.2汉诺塔(Towers of Hanoi)问题 1.学习目标 递归函数是直接调用自己或通过一系列语句间接调用自己的函数.递归在程序设计有着举足轻重的作用,在很多情况下,借助递归可以优雅的解决问题.本节主要介绍递归的基本概念以及如何构建递归程序. 通过本节学习,应掌握以下内容: 理解递归的基本概念,了解递归背后蕴含的编程思想 掌握构建递归程序的方法 2.递归

  • python数据结构之链表详解

    数据结构是计算机科学必须掌握的一门学问,之前很多的教材都是用C语言实现链表,因为c有指针,可以很方便的控制内存,很方便就实现链表,其他的语言,则没那么方便,有很多都是用模拟链表,不过这次,我不是用模拟链表来实现,因为python是动态语言,可以直接把对象赋值给新的变量. 好了,在说我用python实现前,先简单说说链表吧.在我们存储一大波数据时,我们很多时候是使用数组,但是当我们执行插入操作的时候就是非常麻烦,看下面的例子,有一堆数据1,2,3,5,6,7我们要在3和5之间插入4,如果用数组,我

  • C++调用Python基础功能实例详解

    c++调用Python首先安装Python,以win7为例,Python路径为:c:\Python35\,通过mingw编译c++代码. 编写makefile文件,首先要添加包含路径: inc_path += c:/Python35/include 然后添加链接参数: ld_flag += c:/Python35/libs/libpython35.a 在源文件中添加头文件引用: #include "Python.h" Python解释器需要进行初始化,完成任务后需要终止: void s

  • python实现单向链表详解

    本文研究的主要是Python中实现单向链表的相关内容,具体如下. 什么是链表 链表顾名思义就是-链 链表是一种动态数据结构,他的特点是用一组任意的存储单元存放数据元素.链表中每一个元素成为"结点",每一个结点都是由数据域和指针域组成的.跟数组不同链表不用预先定义大小,而且硬件支持的话可以无限扩展. 链表与数组的不同点: 数组需要预先定义大小,无法适应数据动态地增减,数据小于定义的长度会浪费内存,数据超过预定义的长度无法插入.而链表是动态增删数据,可以随意增加. 数组适用于获取元素的操作

  • redis中的数据结构和编码详解

    redis中的数据结构和编码:     背景: 1>redis在内部使用redisObject结构体来定义存储的值对象. 2>每种类型都有至少两种内部编码,Redis会根据当前值的类型和长度来决定使用哪种编码实现. 3>编码类型转换在Redis写入数据时自动完成,这个转换过程是不可逆的,转换规则只能从小内存编码向大内存编码转换.     源码: 值对象redisObject: typedef struct redisObject {                 unsigned ty

随机推荐