数组和链表的区别python_Pyth数组和链表结构(python)_2

本文内容目录如下,会分拆为两篇笔记,另一则笔记是 "数组和链表结构(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 objectnode1 = None# A node containing data and an empty linknode2 = Node("A", None)# A node containing data and a link to node2node3 = Node("B", node2)

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

定义但节点类.jpg

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

head = Node(count, head)while head is not None:

print(head)

head = head.next

3.4 单链表结构的操作

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

a. 遍历

遍历(traversal)在时间上是线性的,并且不需要额外的内存。head = Nonefor count in range(3, 0, -1):

head = Node(count, head)

probe = headwhile probe is not None:    # 

print(probe)

probe = probe.next

遍历过程如下图所示:

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

b. 搜索和访问

顺序搜索和遍历类似,平均情况下对单链表结构的顺序搜索也是线性的。head = Nonefor count in range(3, 0, -1):

head = Node(count, head)# targetItem是被查找的目标项probe = headwhile probe is not None and targetItem != probe.data:

probe = probe.nextif probe is not None:    # else:    # 

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

probe = probe.next

index -= 1return probe.data

c. 替换

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

在单链表结构中,替换给定项的操作需要利用搜索操作:head = Nonefor count in range(3, 0, -1):

head = Node(count, head)# targetItem是被替换的目标项probe = headwhile probe is not None and targetItem != probe.data:

probe = probe.nextif probe is not None:

probe.data = newItem # 替换目标项的数据

return Trueelse:    return False

替换给索引位置的操作需要利用访问操作:# 假定 0 <= index  0:

probe = probe.next

index -= 1probe.data = newItem # 替换目标索引位置的数据

d. 在开始处插入

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

操作单链表结构_在开始处插入.jpg第一种情况下,head 的初始状态就是 None,将 head 设置为新节点即可。

第二种情况下,不需要像数组那样复制并移动数据,也不需要额外的内存。

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

e. 在末尾处插入

在一个数组的末尾插入一项时,需要的时间和内存都是常数,除非必须调整数组的大小。 在单链表结构的末尾插入时,需考虑以下两种情况:第一种情况,head 的初始状态就是 None,将 head 设置为新节点即可。

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

head = newNodeelse:

probe = head    while probe.next != None:

probe = probe.next

probe.next = newNode

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

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

f. 从开始处删除

该操作的使用的时间和内存都是常数,和数组上的相同操作有明显区别。# Assumes at least one node in the structureremovedItem = head.data

head = head.nextreturn removedItem

下图记录了删除过程:

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

g. 从末尾处删除

从一个数组的末尾删除一项时,需要的时间和内存都是常数,除非必须调整数组的大小。 假设单链表中至少存在一个节点,从链表中删除最后一个节点时需要考虑以下两种情况:第一种情况,只有一个节点,直接将 head 指针设置为 None 即可

第二种情况,至少存在两个节点时,需要先找到倒数第二个节点,并将其 next 指针设置为 None 。# Assumes at least one node in structureremovedItem = head.dataif head.next is None:

head = Noneelse:

probe = head    while probe.next.next != None:# 找到倒数第二项

probe = probe.next

removedItem = probe.next.data

probe.next = Nonereturn removedItem

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

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

h. 在任何位置插入

在数组的第 i 个索引位置插入一项时,需要先将从索引 i 到 n-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 positionprobe = head

j = 0while j+1 <= index and probe.next != head:

probe = probe.next

j += 1# Insert new node after node at position index - 1 or# last positionprobe.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 = nextclass 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 nodehead = TwoWayNode(1)

tail = head# Add four nodes to the end of the doubly linked structurefor data in range(2, 6):    # 通过尾链接添加节点,同时会修改尾连接的引用。

# 前提是链表非空,并且tail始终引用链表的最后一个节点。

tail.next = TwoWayNode(data, tail)

tail = tail.next# Print the contents of the linked structure in reverse orderprobe = tailwhile probe != None:

print(probe.data)

probe = probe.previous

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

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

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

作者:Orca_J35

链接:https://www.jianshu.com/p/7bff3b36fb00

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值