线性表(Linear List)是其组成元素间具有线性关系的一种线性结构,是由n个具有相同数据类型的数据元素a0、a1、…、an-1构成的有限序列,一般表示为:
(a0,ai,…ai,ai+1,…,an-1)
其中,数据元素ai可以是字母、整数、浮点数、对象或其他更复杂的信息,i代表数据元素在线性表中的位序号(0≤i<n),n是线性表的元素个数,称为线性表的长度,当n=0时线性表为空表。
线性表中的数据元素具有线性的“一对一”的逻辑关系,是与位置有关的,即第i个元素ai处于第i-1个元素ai-1的后面和第i+1个元素ai+1的前面。这种位置上的有序性就是一种线性关系,可以用二元组表示为L=(D,R),其中有以下关系:
D={ai|0≤i<n}
R={r}
r={<ai,ai-1>|0≤i<n-1}
对应的逻辑结构如图2.1所示。
线性表的逻辑结构图
在线性表(a0,a1,…,an-1)中,a0为开始结点,没有前驱元素,an-1为终端结点,没有后继元素。除开始结点和终端结点外,每个数据元素ai都有且仅有一个前驱元素和后继元素。
抽象描述
from abc import ABCMeta,abstractmethod,abstractproperty
class IList(metaclass=ABCMeta):
@abstractmethod
def clear(self):
pass
@abstractmethod
def isEmpty(self):
pass
@abstractmethod
def length(self):
pass
@abstractmethod
def get(self,i):
pass
@abstractmethod
def insert(self,x):
pass
@abstractmethod
def remove(self,i):
pass
@abstractmethod
def indexOf(self,x):
pass
@abstractmethod
def display(self):
pass
from abc import ABCMeta, abstractmethod, abstractproperty
class IList(metaclass=ABCMeta):
@abstractmethod
def clear(self):
"""清空线性表中的所有元素。"""
pass
@abstractmethod
def isEmpty(self):
"""检查线性表是否为空。
返回:
bool: 如果线性表为空返回True,否则返回False。
"""
pass
@abstractmethod
def length(self):
"""返回线性表中元素的个数。
返回:
int: 线性表中元素的个数。
"""
pass
@abstractmethod
def get(self, i):
"""获取线性表中第i个位置的元素值。
参数:
i (int): 元素的位置,从0开始计数。
返回:
Any: 返回第i个位置的元素值。
异常:
IndexError: 如果i超出了线性表的范围,则抛出IndexError异常。
"""
pass
@abstractmethod
def insert(self, x):
"""向线性表中插入元素x。
参数:
x (Any): 待插入的元素值。
"""
pass
@abstractmethod
def remove(self, i):
"""删除线性表中第i个位置的元素,并返回被删除的元素值。
参数:
i (int): 待删除元素的位置,从0开始计数。
返回:
Any: 被删除的元素值。
异常:
IndexError: 如果i超出了线性表的范围,则抛出IndexError异常。
"""
pass
@abstractmethod
def indexOf(self, x):
"""返回线性表中元素x的第一次出现的位置。
参数:
x (Any): 待查找的元素值。
返回:
int: 元素x第一次出现的位置,如果未找到则返回-1。
"""
pass
@abstractmethod
def display(self):
"""显示线性表中的所有元素。"""
pass
顺序表
- 定义
线性表的顺序存储结构是把线性表中的所有元素按照其逻辑顺序依次存储到计算机的内存单元中指定存储位置开始的一块连续的存储空间中,称为顺序表。顺序表用一组连续的内存单元依次存放数据元素,元素在内存中的物理存储次序和它们在线性表中的逻辑次序一致,即元素ai与其前驱元素ai-1和后继元素ai+1的存储位置相邻,如图2.2所示。
又因为数据表中的所有数据元素具有相同的数据类型,所以只要知道顺序表的基地址和数据元素所占存储空间的大小即可计算出第i个数据元素的地址,可表示为:
Loc(ai)=Loc(a0)+i×c,其中0≤i≤n-1
其中,Loc(ai)是数据元素ai的存储地址,Loc(a0)是数据元素a0的存储地址,即顺序表的基地址,i为元素位置,c为一个数据元素占用的存储单元。
可以看出,计算一个元素地址所需的时间为常量,与顺序表的长度n无关; 存储地址是数据元素位序号i的线性函数。因此,存取任何一个数据元素的时间复杂度为O(1),顺序表是按照数据元素的位序号随机存取的结构。 - 特点
(1) 在线性表中逻辑上相邻的元素在物理存储位置上也同样相邻。
(2) 可按照数据元素的位序号进行随机存取。
(3) 进行插入、删除操作需要移动大量的数据元素。
(4) 需要进行存储空间的预先分配,可能会造成空间浪费,但存储密度较高。 - 描述
可以使用数组来描述线性表的顺序存储结构。在程序设计语言中数组是一种构造数据类型。数组存储具有相同数据类型的元素集合,数组的存储单元个数称为数组长度,每个存储单元的地址是连续的,每个元素连续存储。数组通过下标识别元素,元素的下标是其存储单元序号,表示元素在数组中的位置。一维数组使用一个下标唯一确定一个元素,二维数组使用两个下标唯一确定一个元素。
线性表的顺序存储
class SqList(IList):
def __init__(self, maxsize):
"""初始化顺序表。
Args:
maxsize (int): 顺序表的最大长度。
"""
self.curlen = 0 # 顺序表的当前长度
self.maxSize = maxsize # 顺序表的最大长度
self.listItem = [None] * self.maxSize # 顺序表存储空间
def clear(self):
"""清空顺序表中的所有元素。"""
self.curlen = 0
def isEmpty(self):
"""检查顺序表是否为空。
Returns:
bool: 如果顺序表为空返回True,否则返回False。
"""
return self.curlen == 0
def length(self):
"""返回顺序表的元素个数。
Returns:
int: 顺序表的元素个数。
"""
return self.curlen
def get(self, i):
"""获取顺序表中第i个位置的元素值。
Args:
i (int): 元素的位置,从0开始计数。
Returns:
Any: 返回第i个位置的元素值。
Raises:
IndexError: 如果i超出了顺序表的范围,则抛出IndexError异常。
"""
if 0 <= i < self.curlen:
return self.listItem[i]
else:
raise IndexError("Index out of range")
def insert(self, i, x):
"""在顺序表中第i个位置插入元素x。
Args:
i (int): 插入位置,从0开始计数。
x (Any): 待插入的元素值。
Raises:
IndexError: 如果i超出了顺序表的范围,则抛出IndexError异常。
ValueError: 如果顺序表已满,则抛出ValueError异常。
"""
if self.curlen == self.maxSize:
raise ValueError("List is full")
if i < 0 or i > self.curlen:
raise IndexError("Index out of range")
for j in range(self.curlen, i, -1):
self.listItem[j] = self.listItem[j - 1]
self.listItem[i] = x
self.curlen += 1
def remove(self, i):
"""删除顺序表中第i个位置的元素,并返回被删除的元素值。
Args:
i (int): 待删除元素的位置,从0开始计数。
Returns:
Any: 被删除的元素值。
Raises:
IndexError: 如果i超出了顺序表的范围,则抛出IndexError异常。
"""
if 0 <= i < self.curlen:
value = self.listItem[i]
for j in range(i, self.curlen - 1):
self.listItem[j] = self.listItem[j + 1]
self.listItem[self.curlen - 1] = None
self.curlen -= 1
return value
else:
raise IndexError("Index out of range")
def indexOf(self, x):
"""返回顺序表中元素x第一次出现的位置。
Args:
x (Any): 待查找的元素值。
Returns:
int: 元素x第一次出现的位置,如果未找到则返回-1。
"""
for i in range(self.curlen):
if self.listItem[i] == x:
return i
return -1
def display(self):
"""显示顺序表中的所有元素。"""
print(self.listItem[:self.curlen])
# 示例用法
if __name__ == "__main__":
# 创建一个顺序表对象,最大长度为5
sq_list = SqList(5)
# 插入元素
sq_list.insert(0, 10)
sq_list.insert(1, 20)
sq_list.insert(2, 30)
sq_list.insert(3, 40)
sq_list.insert(4, 50)
print("当前顺序表:", end=" ")
sq_list.display() # [10, 20, 30, 40, 50]
# 删除元素
deleted_value = sq_list.remove(1)
print("删除的元素值:", deleted_value) # 20
print("当前顺序表:", end=" ")
sq_list.display() # [10, 30, 40, 50]
# 获取元素
element = sq_list.get(2)
print("获取的元素值:", element) # 40
# 检查顺序表是否为空
print("顺序表是否为空:", sq_list.isEmpty()) # False
# 获取顺序表长度
print("顺序表长度:", sq_list.length()) # 4
插入insert
- 插入操作
插入操作insert(i,x)是在长度为n的顺序表的第i个数据元素之前插入值为x的数据元素,其中0≤i≤n,当i=0时在表头插入,当i=n时在表尾插入,在插入操作完成后表长加1,顺序表的逻辑结构由(a0,a1,…,ai-1,ai,…,an-1)变成了(a0,a1,…,ai-1,x,ai,…,an-1),如图2.3所示。
其主要步骤如下。
(1) 判断顺序表的存储空间是否已满,若已满则抛出异常。
(2) 判断参数i的值是否满足0≤i≤curLen,若不满足则抛出异常。
(3) 将插入位置及其之后的所有数据元素后移一个存储位置。
(4) 在位置i处插入新的数据元素x。
(5) 表长加1。
删除remove
删除操作
删除操作remove(i,x)是将长度为n的顺序表的第i个数据元素删除,其中0≤i≤n-1,删除操作完成后表长减1,顺序表的逻辑结构由(a0,a1,…,ai-1,ai,…,an-1)变成了(a0,a1,…,ai-1,ai+1,…,an-1)
其主要步骤如下。
(1) 判断参数i是否满足0≤i≤curLen-1,若不满足则抛出异常。
(2) 将第i个数据元素之后的数据元素都向前移动一个存储单元。
(3) 表长减1。
查找indexOf
查找操作indexOf(x)是在长度为n的顺序表中寻找初次出现的数据元素值为x的数据元素的位置。
其主要步骤为将x与顺序表中的每一个数据元素的值进行比较,若相等,则返回该数据元素的位置; 若比较结束未找到等值的数据元素,返回-1。
建立一个由a~z的26个字母组成的字母顺序表,求每个字母的直接前驱和直接后继,编程实现。
L=SqList(26)
for i in range(26):
L.insert(i,chr(ord('a')+i))
while True:
i=input("请输入需要查询元素的位序号:\n")
综上所述,顺序表具有较好的静态特性、较差的动态特性。
(1) 顺序表利用元素的物理存储次序反映线性表元素的逻辑关系,不需要额外的存储空间进行元素间关系的表达。顺序表是随机存储结构,存取元素ai的时间复杂度为O(1),并且实现了线性表抽象数据类型所要求的基本操作。
(2) 插入和删除操作的效率很低,每插入或删除一个数据元素,元素的移动次数较多,平均移动顺序表中数据元素个数的一半; 并且数组容量不可更改,存在因容量小造成数据溢出或者因容量过大造成内存资源浪费的问题。
线性表的链式存储
采用链式存储方式存储的线性表称为链表,链表是用若干地址分散的存储单元存储数据元素,逻辑上相邻的数据元素在物理位置上不一定相邻,必须采用附加信息表示数据元素之间的逻辑关系,因此链表的每一个结点不仅包含元素本身的信息数据域,而且包含元素之间逻辑关系的信息,即逻辑上相邻结点地址的指针域。
单链表
单链表是指结点中只包含一个指针域的链表,指针域中储存着指向后继结点的指针。单链表的头指针是线性表的起始地址,是线性表中第一个数据元素的存储地址,可作为单链表的唯一标识。单链表的尾结点没有后继结点,所以其指针域值为None。
为了使操作简便,在第一个结点之前增加头结点,单链表的头指针指向头结点,头结点的数据域不存放任何数据,指针域存放指向第一个结点的指针。空单链表的头指针head为None。图2.5为不带头结点的单链表的存储示意图,图2.6为带头结点的单链表的存储示意图。
单链表的结点的存储空间是在插入和删除过程中动态申请和释放的,不需要预先分配,从而避免了顺序表因存储空间不足需要扩充空间和复制元素的过程,避免了顺序表因容量过大造成内存资源浪费的问题,提高了运行效率和存储空间的利用率。
结点类描述
单链表类描述
class LinkList(IList):
def __init__(self):
self.head=Node() #构造函数初始化头结点
def create(self,l,order):
if order:
self.create_tail(l)
else:
self.create_head(l)
查找
查找操作
(1) 位序查找get(i)是返回长度为n的单链表中第i个结点的数据域的值,其中0≤i≤n-1。由于单链表的存储空间不连续,因此必须从头结点开始沿着后继结点依次进行查找。
查找操作indexOf(x)是在长度为n的单链表中寻找初次出现的数据域值为x的数据元素的位置。
其主要步骤为将x与单链表中的每一个数据元素的数据域进行比较,若相等,则返回该数据元素在单链表中的位置; 若比较结束未找到等值的数据元素,返回-1。
插入
插入操作insert(i,x)是在长度为n的单链表的第i个结点之前插入数据域值为x的新结点,其中0≤i≤n,当i=0时,在表头插入,当i=n时在表尾插入。
与顺序表相比,单链表不需要移动一批数据元素,而只需要改变结点的指针域,改变有序对,即可实现数据元素的插入,即<ai-1,ai>转变为<ai-1,x>和<x,ai>,如图2.7所示。
插入操作的主要步骤如下:
(1) 查找到插入位置的前驱结点,即第i-1个结点。
(2) 创建数据域值为x的新结点。
(3) 修改前驱结点的指针域为指向新结点的指针,新结点的指针域为指向原第i个结点的指针。
带头结点的单链表的插入操作
不带头结点的单链表的插入操作
分析以上代码可以发现,由于链式存储采用的是动态存储分配空间,所以在进行插入操作之前不需要判断存储空间是否已满。
在带头结点的单链表上进行插入操作时,无论插入位置是表头、表尾还是表中,操作语句都是一致的; 但是在不带头结点的单链表上进行插入操作时,在表头插入和在其他位置插入新结点的语句是不同的,需要分两种情况进行处理。
删除
删除操作remove(i,x)是将长度为n的单链表的第i个结点删除,其中0≤i≤n-1。
与顺序表相比,单链表不需要移动一批数据元素,而只需要改变结点的指针域,实现有序对的改变,即可删除结点,即<ai-1,ai>和<ai,ai+1>转变为<ai-1,ai+1>,如图2.8所示。
其主要步骤如下。
(1) 判断单链表是否为空。
(2) 查找待删除结点的前驱结点。
(3) 修改前驱结点的指针域为待删除结点的指针域。
单链表的建立
头插法
将新结点插入到单链表的表头,读入的数据顺序与结点顺序相反。
尾插法
将新结点插入到单链表的表尾,读入的数据顺序与结点顺序相同。
其他链表
循环链表
循环链表与单链表的结构相似,只是将链表的首尾相连,即尾结点的指针域为指向头结点的指针,从而形成了一个环状的链表。
循环链表与单链表的操作算法基本一致,判定循环链表中的某个结点是否为尾结点的条件不是它的后继结点为空,而是它的后继结点是否为头结点。
在实现循环链表时可用头指针或尾指针或二者同时使用来标识循环链表,通常使用尾指针来进行标识,可简化某些操作。
双向链表
双向链表的结点具有两个指针域,一个指针指向前驱结点,一个指针指向后继结点。使得查找某个结点的前驱结点不需要从表头开始顺着链表依次进行查找,减小时间复杂度。
结点类描述
双向链表的基本操作实现
其与单链表的不同之处主要在于进行插入和删除操作时每个结点需要修改两个指针域。
插入操作
删除操作
顺序表和链表的比较
(1) 线性表是其组成元素间具有线性关系的一种线性结构,其实现方式主要为基于顺序存储的实现和基于链式存储的实现。
(2) 线性表的顺序存储结构称为顺序表,可用数组实现,可对数据元素进行随机存取,时间复杂度为O(1),在插入或删除数据元素时时间复杂度为O(n)。
(3) 线性表的链式存储结构称为链表,不能直接访问给定位置上的数据元素,必须从头结点开始沿着后继结点进行访问,时间复杂度为O(n)。在插入或删除数据元素时不需要移动任何数据元素,只需要更改结点的指针域即可,时间复杂度为O(1)。
(4) 循环链表将链表的首尾相连,即尾结点的指针域为指向头结点的指针,从而形成了一个环状的链表。
(5) 双向链表的结点具有两个指针域,一个指针指向前驱结点,一个指针指向后继结点,使得查找某个结点的前驱结点不需要从表头开始顺着链表依次进行查找,减小时间复杂度。