数组和链表结构(python)_2

作者: import_hello | 来源:发表于2018-07-28 09:46 被阅读36次

    本文内容目录如下,会分拆为两篇笔记,另一则笔记是 "数组和链表结构(python)_1"。

    目录.jpg

    3. 链表结构

    Linked Structures

    在数组之后,链表结构可能使程序中最常用的数据结构。

    3.1 单链表结构和双链表结构

    单链表结构(Singly Linked Structure)和双链表结构(Doubly Linked Structure)是两种最简单的链表结构,其结构如下图所示:

    两种类型的链表结构.jpg

    单链表结构的用户通过外部的头链接(head link)可访问链表的第一个节点,然后根据该节点提供的信息,便可访问其后继项,并以此类推。在单链表结构中,很容易获取当前节点的后继项,但很难获取当前节点的前驱项。

    双链表包含两个方向的链接,因此用户很容易获取当前项的前驱项和后继项。双链表还提供了名为尾链接(tail link)的外部链接,以便用户直接访问链表的最后一项。

    两种链表的最后一项都不包含指向下一项的链接,这种情况被称为空链接(empty link)。双链表的第一项和最后一项都包含空链接。

    链表结构无法通过索引直接访问其中的某项,必须从链表的第一项起沿着链表找寻目标索引。

    3.2 非连续性内存和节点

    数组使用的是连续内存,而链表是用的是非连续性内存(Noncontiguous Memory) 。

    链表结构的基本单位是节点(node),它包含了一个数据项和一个对后继项的引用。双链表的节点中还包含了一个对前驱项的引用。

    注意,不同于 C++ 使用指针指向后继项,Python 直接引用后继项即可。在 Python 中变量可以引用任何内容,包括 None(表示空链接)。

    3.3 定义单链表的节点类

    Defining a Singly Linked Node Class Node

    节点类很简单,其构造方法允许用户设置节点连接,不过节点对象通常不包含方法调用。

    class Node(object):
    
        def __init__(self, data, next=None):
            """Instantiates a Node with default next of None"""
            self.data = data
            self.next = next
    
        def __str__(self):
            return str(self.data)
    
    # Just an None object
    node1 = None
    
    # A node containing data and an empty link
    node2 = Node("A", None)
    
    # A node containing data and a link to node2
    node3 = Node("B", node2)
    

    执行上述代码后,三个变量的状态如下图所示:

    定义但节点类.jpg

    下面的代码展示了使用循环方式来创建一个链表结构,并访问其中的每一个节点。其中最新插入的项总位于链表前端。另外,当打印数据时,head 会重新指向下一个节点,直到 head 为 None 为止。因此,完成输出后,实际上会从链表中删除所有节点。对于程序来说,这些被删除的节点会在下一次垃圾回收的时候被回收。

    head = None
    for count in range(1, 6):
        head = Node(count, head)
    
    while head is not None:
        print(head)
        head = head.next
    

    3.4 单链表结构的操作

    几乎数组上的所有操作都是基于索引的,并且索引是数组结构中不可或缺的部分。但是,在链表结构中,程序员必须通过操作结构中的链表来模拟基于索引的操作。

    a. 遍历

    遍历(traversal)在时间上是线性的,并且不需要额外的内存。

    head = None
    for count in range(3, 0, -1):
        head = Node(count, head)
    
    probe = head
    while probe is not None:
        # <use or modify probe.data >
        print(probe)
        probe = probe.next
    

    遍历过程如下图所示:

    操作单链表结构_遍历.jpg

    b. 搜索和访问

    顺序搜索和遍历类似,平均情况下对单链表结构的顺序搜索也是线性的。

    head = None
    for count in range(3, 0, -1):
        head = Node(count, head)
    
    # targetItem是被查找的目标项
    probe = head
    while probe is not None and targetItem != probe.data:
        probe = probe.next
    if probe is not None:
        # <targetItem has been found>
    else:
        # <targetItem is not in the linked structure>
    

    和数组不同,链表结构不支持随机访问。因此,访问链表结构中的某一项,也是一次顺序搜索操作,如下是访问第 i 项的代码:

    # 假定 0 <= index < n, index是目标索引项
    probe = head
    while index > 0:
        probe = probe.next
        index -= 1
    return probe.data
    

    c. 替换

    以下两种替换操作在平均情况下都是线性的。

    在单链表结构中,替换给定项的操作需要利用搜索操作:

    head = None
    for count in range(3, 0, -1):
        head = Node(count, head)
    
    # targetItem是被替换的目标项
    probe = head
    while probe is not None and targetItem != probe.data:
        probe = probe.next
    if probe is not None:
        probe.data = newItem # 替换目标项的数据
        return True
    else:
        return False
    

    替换给索引位置的操作需要利用访问操作:

    # 假定 0 <= index < n, index是目标索引项
    probe = head
    while index > 0:
        probe = probe.next
        index -= 1
    probe.data = newItem # 替换目标索引位置的数据
    

    d. 在开始处插入

    在一个链表结构的开始处插入项时,可分为以下两种情况:

    操作单链表结构_在开始处插入.jpg
    • 第一种情况下,head 的初始状态就是 None,将 head 设置为新节点即可。
    • 第二种情况下,不需要像数组那样复制并移动数据,也不需要额外的内存。

    所以,在一个链表结构的开始处插入数据时,时间和内存都是常数,这与数组的操作过程截然不同。

    e. 在末尾处插入

    在一个数组的末尾插入一项时,需要的时间和内存都是常数,除非必须调整数组的大小。 在单链表结构的末尾插入时,需考虑以下两种情况:

    • 第一种情况,head 的初始状态就是 None,将 head 设置为新节点即可。
    • 第二种情况,当 head 初始状态不为 None 时,需要先遍历链表以获取链表的最后一个节点,然后再进行插入。该操作在时间上是线性的,在空间上是常数。
    newNode = Node(newItem)
    if head is None:
        head = newNode
    else:
        probe = head
        while probe.next != None:
            probe = probe.next
        probe.next = newNode
    

    下图展示了在拥有 3 项元素的一个链表末尾插入新项的过程:

    操作单链表结构_在末尾处插入.jpg

    f. 从开始处删除

    该操作的使用的时间和内存都是常数,和数组上的相同操作有明显区别。

    # Assumes at least one node in the structure
    removedItem = head.data
    head = head.next
    return removedItem
    

    下图记录了删除过程:

    操作单链表结构_从开始处删除.jpg

    g. 从末尾处删除

    从一个数组的末尾删除一项时,需要的时间和内存都是常数,除非必须调整数组的大小。 假设单链表中至少存在一个节点,从链表中删除最后一个节点时需要考虑以下两种情况:

    • 第一种情况,只有一个节点,直接将 head 指针设置为 None 即可
    • 第二种情况,至少存在两个节点时,需要先找到倒数第二个节点,并将其 next 指针设置为 None
    # Assumes at least one node in structure
    removedItem = head.data
    if head.next is None:
        head = None
    else:
        probe = head
        while probe.next.next != None:# 找到倒数第二项
            probe = probe.next
        removedItem = probe.next.data
        probe.next = None
    return removedItem
    

    下图展示了从拥有 3 个项的链表结构中删除最后一项的过程:

    操作单链表结构_从末尾处删除.jpg

    h. 在任何位置插入

    在数组的第 i 个索引位置插入一项时,需要先将从索引 in-1 的项都向后移动。 在链表的第 i 个索引位置插入项时,需要先找到第 i-1 (如果 i) 或 n-1 (如果 i>=n)的索引位置,然后插入一个节点。和遍历操作一样,该操作的性能也是线性的,但使用的内存数是常数。

    if head is None or index <= 0:
        head = Node(newItem, head)
    else:
        # Search for node at position index - 1 or the last position
        probe = head
        j = 0
        while j <= index-2 and probe.next != None:
            probe = probe.next
            j += 1
        # Insert new node after node at position index - 1
        # or last position
        probe.next = Node(newItem, probe.next)
    

    下图展示了在包含 3 个项的链表结构中,在第 2 个索引位置插入一项的过程:

    操作单链表结构_在任何位置插入.jpg

    在目标项之前插入一项:

    if head is not None:
        if head.next is None and\
                targetItem == head.data:
            # 链表中仅有一个元素,且该元素等于目标项
            head = Node(newItem, head)
        else:
            probe = head
            while probe.next is not None and\
                    targetItem != probe.next.data:
                probe = probe.next
            if probe.next is not None:
                probe.next = Node(newItem, probe.next)
    else:
        # 不存在目标项时的操作
    

    i. 从任意位置删除

    从一个链表结构中删除索引值是 i 的项,存在以下三种情况:

    • i<=0 ,直接删除索引为 0 的节点
    • 0 ,找到索引为 i-1 的节点,然后删除其后的节点
    • i>=n ,直接删除最后一个节点
    # 假设链表结构中至少包含一个节点
    if index <= 0 or head.next is None
        removedItem = head.data
        head = head.next
        return removedItem # 返回被删除的节点
    else:
        # Search for node at position index - 1 or
        # the next to last position
        probe = head
        while index > 1 and probe.next.next != None:
            probe = probe.next
            index -= 1
        removedItem = probe.next.data
        probe.next = probe.next.next
        return removedIte
    

    下图展示了从包含 4 个节点链表结构中删除索引为 2 的节点的过程:

    操作单链表结构_从任意位置删除.jpg

    3.5 复杂度分析

    下表给出了单链表各种操作的运行时间:

    操作 运行时间
    访问第 i 个位置的元素 O(n),平均情况
    替换第 i 个位置的元素 O(n),平均情况
    在开始处插入 O(1),最好情况和最坏情况
    从开始处删除 O(1),最好情况和最坏情况
    在第 i 个位置插入 O(n),平均情况
    从第 i 个位置删除 O(n),平均情况

    单链表只有两个操作在时间上不是线性的。单链表结构相较数组的主要优点并不是时间性能,而是内存性能。当必须调整数组的尺寸时,其时间和内存都是线性的。当调整链表结构的尺寸时(插入或删除操作都会伴随链表尺寸的改变),其时间和内存都是常数。因为链表结构的物理尺寸和逻辑尺寸相等,所有在链表结构中没有浪费内存的问题。链表结构有一个额外的内存代价,因为单链表结构必须要为指针使用 n 个内存单元。这种代价在双链表结构中还会翻倍,因为双链表的节点有两个链接。

    3.6 链表的变体

    a. 包含哑头结点的循环链表结构

    对于单链表结构,在其开始处插入(删除)节点的操作,其实是在任意位置插入(删除)节点的特殊情况——特殊之处在于 head 引用需要被改变。如果使用带哑头节点(Dummy Header Node)的循环链表结构,便可避免这种特殊性,从而在任何情况下都无需改变 head 引用的对象。

    首先,循环链表结构的最后一个节点的链接总会引用第一个节点;其次,这个实现中第一个节点始终是哑头节点——哑头节点不包含数据,用于充当链表结构中开头和结尾的一个标志——并且 head 始终会引用哑头节点。在该实现的空链表结构中,哑头节点的链接会引用哑头节点自身。空链表结构如下:

    head = Node(None, None)
    head.next = head
    

    示意图如下:

    哑头节点_01.jpg

    本实现的数据节点位于哑头节点之后,并且最后一个数据节点的链接需要引用哑头节点。另外,数据节点的索引依然从 0 开始。下图展示了包含一个数据节点的实现:

    哑头节点_02.jpg

    执行在任意索引位置插入节点的操作时,本实现的代码如下:

    # Search for node at position index - 1 or the last position
    probe = head
    j = 0
    while j+1 <= index and probe.next != head:
        probe = probe.next
        j += 1
    # Insert new node after node at position index - 1 or
    # last position
    probe.next = Node(newItem, probe.next)
    

    可见,本实现的优点在于插入(删除)操作都只需要考虑一种情况。

    b. 双链表结构

    由于双链表包含两个方向的链接,因此可获取当前项的前驱项和后继项。由于尾链接(tail link)的存在,使得用户可以直接访问最后一个节点。

    双链表结构.jpg

    通过扩展之前的 Node 类,便可实现双链表结构的节点类:

    class Node(object):
        def __init__(self, data, next = None):
            """Instantiates a Node with default next of None"""
            self.data = data
            self.next = next
    class TwoWayNode(Node):
        def __init__(self, data, previous = None, next = None):
            """Instantiates a TwoWayNode."""
            Node.__init__(self, data, next)
            self.previous = previous
    

    构建双链表的代码如下:

    """File: testtwowaynode.py
    Tests the TwoWayNode class.
    """
    from node import TwoWayNode
    # Create a doubly linked structure with one node
    head = TwoWayNode(1)
    tail = head
    # Add four nodes to the end of the doubly linked structure
    for data in range(2, 6):
        # 通过尾链接添加节点,同时会修改尾连接的引用。
        # 前提是链表非空,并且tail始终引用链表的最后一个节点。
        tail.next = TwoWayNode(data, tail)
        tail = tail.next
    # Print the contents of the linked structure in reverse order
    probe = tail
    while probe != None:
        print(probe.data)
        probe = probe.previous
    

    下图展示了在双链表结构的末尾插入一个新节点的过程:

    双链表结构_插入过程.jpg

    双链表结构较为通用的插入和删除操作也存在两种情况,这点与单链表的操作相同,可以通过借助带有哑头节点的循环链表来简化插入和删除操作。

    除了在尾部插入和删除的操作,双链表结构其余操作的时间复杂度与单链表的对应操作相同。不过,双链表结构中额外的指针,需要一个额外的。线性的内存使用量。

    相关文章

      网友评论

        本文标题:数组和链表结构(python)_2

        本文链接:https://www.haomeiwen.com/subject/abmwmftx.html