考研数据结构笔记(2)线性表

本文详细介绍了线性表的定义、特点,包括顺序表示和链式表示两种实现方式。顺序表示中,线性表的查找、插入和删除操作的平均时间复杂度为O(n)。链式表示中,单链表和循环链表的特性被讨论,插入和删除操作的时间复杂度也为O(n)。文章还对比了顺序表和链表的优缺点,并探讨了线性表在合并操作中的应用。
摘要由CSDN通过智能技术生成


线性结构的基本特点是除第一个元素无直接后继之外,其他每个数据元素都有一个前驱和后继

一、线性表的定义和特点

1.定义

存在有序序列例如a,b,c…,z这样由n个数据特效相同的元素构成的有限序列称为线性表(n=0时称为空表)

2.特点

存在唯一的首末元素,除第一个元素以外每个数据元素均只有一个前驱,除最后一个数据元素以外,结构中的每个数据元素均只有一个后继。

二、线性表的顺序表示和实现

线性表的顺序表示指的是用一组地址连续的存储单元依次存储线性表的数据元素,也称作线性表的顺序存储结构。其特点是,逻辑上相邻的数据元素,其物理次序也是相邻的。(这一点与单链表常做比较)

1. 基本操作的实现

首先说明下顺序表的存储结构:

#define MAXSIZE 100    //定义顺序表可能达到的最大长度
typedef struct
{
 ElemType *elem;        //存储空间的基地址
 int length;              //顺序表长度
}SqList;                //表明结构类型

a. 查找

查找操作是:
根据指定的元素值e, 查找顺序表中第1个与e相等的元素。若查找成功,则返 回该元素在表中的位置序号;若查找失败,则返回0。
课本代码范例:

int LocateELem(SqList L,ElemType e)//在顺序表1中查找值为e的数据元素, 返回其序号 
	for(i=O;i< L.length;i++) 
	If(L.elem[i)==e) 
	return i+l;     //查找成功, 返回序号i+l
	return O;           //查找失败, 返回 0
}

算法时间复杂度分析:
当在顺序表中查找一个数据元素时,其时间主要耗费在数据的比较上,而比较的次数取决千 被查元素在线性表中的位置。这里取算法的平均时间复杂度,查找n个元素的查询次数和为n*(n+1)/2,故最后得出平均值为(n+1)/2,所以查找算法的平均时间复杂度为O(n)。

b. 插入

步骤

  1. 判断插入位置i是否合法,若不合法(也就是i超过了顺便表的范围)则返回 ERROR。
  2. 判断顺序表的存储空间是否已满,若满见肤返回 ERROR。
  3. 将第n个至第l个位置的元素依次向后移动一个位置,空出第l个位置(i=n+1时无需移动)。
  4. 将要插入的新元素e放入第i个位置。5)表长加1。
    课本的代码范例:
Status Listinsert(SqList &L,int i ,ElemType e) {
//在顺序表L 中第 l.个位置之前插入新的元素e, i值的合法范围是 1<=i<=L.length+l 
	if((i<l) 11 (i>L.length+l)) 
	return ERROR;         //i值不合法
	if(L.length==MAXSIZE) 
	return ERROR;         //当前存储空间已满
	for (j=L. length-1; j>=i-1; j--) 
	L.elem[j+l]=L.elem[j];   //插入位置及之后的元素后移
	L.elem[i-l)=e;              ///将新元素e放入第l个位置
	++L.length;    //表长加1
	return OK;
}

算法时间复杂度分析
当在顺序表中某个位置上插入一个数据元素时, 其时间主要耗费在移动元素上, 而移动元素的个数取决于插入元素的位置。
在表头插入一个元素需要移动n个元素,在第一个元素后插入一个元素,需要移动n-1个元素,依次类推,直到在表尾插入一个元素,需要移动0个元素。整个过程中需要移动元素个数之和为n(n+1)/2,插入次数为n+1次,故最后得平均需要移动元素个数为二者相除,即n/2次,因此,得插入算法平均时间复杂度为O(n)。

c. 删除

线性表的删除操作是指:
线性表的删除操作是指:将表的第i个元素删去,将长度为n的线性表变成长度为n-1 的线性表,数据元素a-1、 a和 a+1之间的逻辑关系发生了变化,为了在存储结构上反映这个变化,同样需要移动元素。也就是将删除元素的后面元素依次向前移动一个位置。
步骤

  1. 判断插入位置i是否合法,若不合法(也就是i超过了顺便表的范围)则返回 ERROR。
  2. 将第i个至第n个的元素依次向前移动一个位置 (i= n时无需移动)。
  3. 表长减 1。
    课本代码范例
Status ListDelete(SqList &L,int i) {
//在顺序表L中删除第i个元素,i值的合法范围是1<=i<=L.length 
	if((i<l) 11 (i>L.length)) 
	return ERROR;    //i值不合法
	for (j=i; j <=L. length-1; j ++) 
	L.elem[j-1)=L.elem[j);  //被删除元素之后的元素前移
	--L.length;    //表长减1
	return OK; 
}

算法时间复杂度分析:
当在顺序表中某个位置上删除一个数据元素时,其时间主要耗费在移动元素上,而移动元素 的个数取决于删除元素的位置。删除第一个元素时需要移动n-1个元素,删除第二个元素时需要移动n-2个元素,依次类推,直到要删除元素为最后一个元素,需要移动0个元素。整个过程中需要移动元素个数之和为n(n-1)/2,删除次数为n次,故最后得平均需要移动元素个数为二者相除,即(n-1)/2次,因此,得插入算法平均时间复杂度为O(n)。

2. 线性表的缺点

在做插入或删除操作时,需移动大量元素。另外由千数组有长度相对固定的静态特性,当表中数据元素个数较多且变化较大时, 操作过程相对复杂,必然导致存储空间的浪费。

三、线性表的链式表示和实现

1. 单链表的定义、表示以及存储结构

a. 定义

线性表链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的,因为链表关心的只是数据元素之间的逻辑顺序,而不是每个数据元素在存储器中的实际位置。)。单链表的结点包括两个域:其中存储数据元素信息的域称为数据域;存储直接后继存储位置的域称 为指针域。指针域中存储的信息称作指针或链。由于此链表的每个结点中只包含一个指针域,故又称线性链表或单链表。根据链表结点所含指针个数、指针指向和指针连接方式,可将链表分为单链表、循环链表、 双向链表、二叉链表、十字链表、邻接表、邻接多重表等。其中单链表、循环链表和双向链表用于实现线性表的链式存储结构,其他形式多用于实现树和图等非线性结构。

b. 表示

通常将链表画成用箭头相链接的结点的序列,结点之间的箭头表示链域中的指针。一般情况下,为了处理方便,在单链表的第一个结点之前附设一个结点,称之为头结点。

c. 存储结构

typedef struct LNode
{
 ELemType data;              //结点的数据域
 Struct LNode *next;           //结点的指针域
}LNode,*LinkList;               //LinkList为指向结构体LNode的指针类型

d.几个容易混淆的概念

1.首元结点:是指链表中存储第一个数据元素a1的结点。
2.头结点:是在首元结点之前附设的一个结点,其指针域指向首元结点。头结点的数据域可以不存储任何信息,也可存储与数据元素类型相同的其他附加信息。
3.头指针:是指向链表中第一个结点的指针。若链表设有头结点,则头指针所 指结点为线性表的头结点;若链表不设头结点,则头指针所指结点为该线性表的首元结点。

2. 单链表基本操作的实现

a. 初始化(构造空表)

步骤

1.生成新结点作为头结点,用头指针L 指向头结点。
2.头结点的指针域置空。

代码
L=new LNode;       //生成新结点作为头结点,用头指针L指向头结点
L->next=NULL;      //头结点的指针域置空

b. 查找

步骤

1.用指针p指向首元结点。
2.从首元结点开始依次顺着链域next向下查找,只要指向当前结点的指针p不为空,并且 p所指结点的数据域不等于给定值e, 则循环执行以下操作:p指向下一个结点。
3. 返回p。若查找成功,p此时即为结点的地址值,若查找失败,p的值即为NULL。

代码
p->L->next;      //初始化,p指向首元结点
while(p&&p->data!=e)
    p=p->next;    //顺链域向后扫描,直到p为空或p所指结点的数据域等于e
return p;          //查找成功返回
算法时间复杂度

按值查找过程与顺序表类似,执行时间与待查找值相关,因此平均时间复杂度为O(n)。

c. 插入

步骤
  1. 查找要插入结点的位置a,并由指针p指向该位置结点
  2. 生成一个新结点*s
  3. 将*s的数据域设置为x
  4. 将*s的指针域指向b
  5. 将结点p的指针域指向s
代码
P=L;j=0;
while(p&&(j<i-1)){
  P=p->next;
  ++j;             //查找第i-1个结点,p指向该结点
}
if(!p||j>i-1)
   return ERROR;   //i>n+1或者i<1
s=new LNode;     //生成新结点s
s->data=x;        //将*s的数据域设置为x
s->next=p->next;     //将*s的指针域指向b
p->next=s;        //将结点*p的指针域指向*s
算法时间复杂度分析

单链表的插入操作虽然不需要像顺序表的插入操作那样需要移动元素,但平均时间复杂度仍 为O(n)。这是因为,为了在第i个结点之前插入一个新结点,必须首先找到第i-1 个结点, 其时间复杂度与查找算法相同为O(n)。

d. 删除

在这里插入图片描述

步骤
  1. 查找结点ai-1并由指针p指向该结点。
  2. 临时保存待删除结点ai的地址在q中,以备释放。
  3. 将结点*p的指针域指向ai的直接后继结点。
  4. 释放结点ai的空间。
代码
P=L;j=0;
While((p->next)&&(j<i-1)){
   p=p->next;
   ++j;
}
if(!(p->next)||j>i-1)
   return ERROR;   //i>n或者i<1时,删除位置不合理
q=p->next;         //临时保存被删结点的地址以备释放
p-next=q->next;    //改变删除结点前驱结点的指针域
delete q;           //释放删除结点的空间
算法时间复杂度分析

类似插入算法,删除算法时间复杂度亦为O(n)。

e. 创建单链表

前插法

前插法是通过将新结点逐个插入链表的头部(头结点之后)来创建链表,每次申请一个新结 点,读入相应的数据元素值,然后将新结点插入到头结点之后。

步骤
  1. 创建一个只有头结点的空链表。
  2. 根据待创建链表包括的元素个数n, 循环n次执行以下操作:生成一个新结点p; 输入元素值赋给新结点p的数据域;将新结点*p插入到头结点之后。
    如图为了创建线性表a,b,c,d,e 输入时应该逆序输入数据,依次输入e,d,c,b,a。

在这里插入图片描述

代码
L=new LNode; 
L->next=NULL; 
for(i=O;i<n;++i)
{
	 p=new LNode;           //生成新结点*p
	 cin>>p->data;           //输入元素值赋给新结点*p的数据域
	 p->next=L->next;         //将新结点*p插人到头结点之后
	 L->next=p; 
}
算法时间复杂度分析

显然插入n个结点时间复杂度为O(n)。

后插法

后插法是通过将新结点逐个插入到链表的尾部来创建链表。 同前插法一样, 每次申请一个新结点,读入相应的数据元素值。不同的是,为了使新结点能够插入到表尾,需要增加一个尾指针 r指向链表的尾结点。

步骤
  1. 创建一个只有头结点的空链表。
  2. 尾指针r初始化,指向头结点。
  3. 根据创建链表包括的元素个数n, 循环n次执行以下操作:生成一个新结点p; 输入元素值赋给新结点p的数据域;将新结点p 插入到尾结点r之后;尾指针r指向新的尾结点*p。
    如图为线性表 (a,b,c,d,e) 后插法的创建过程,读入数据的顺序和线性表中的逻辑顺序是相同的。
    在这里插入图片描述
代码
L=new LNode; 
L->next=NULL; 
r=L;                     //尾指针r指向头结点
for(i=O;i<n;++i)
{
	 p=new LNode;           //生成新结点*p
	 cin>>p->data;           //输入元素值赋给新结点*p的数据域
	 p->next=NULL;          
	 r->next=p;              //将新结点*p插人尾结点*r之后
	 r=p;                    //r指向新的尾结点*p
}
算法时间复杂度分析

显然插入n个结点时间复杂度为O(n)。

f. 循环链表

循环链表是另一种形式的链式存储结构。其特点是表中最后一个结点 的指针域指向头结点,整个链表形成一个环。由此,从表中任一结点出发均可找到表中其他结点。
a. 与单链表的区别:
循环单链表的操作和单链表基本一致,差别仅在于:当链表遍历时,判别当前指针p是否指向表尾结点的终止条件不同。在单链表中,判别条件为p!=NULL或p->next!=NULL,而循环 单链表的判别条件为p!=L或p->next!=L
b. 在某些情况下,
若在循环链表中设立尾指针而不设头指针), 可使一些操 作简化。例如,将两个线性表合并成一个表时,仅需将第一个表的尾指针指向第二个表的第一个结点,第二个表的尾指针指向第一个表的头结点,然后释放第二个表的头结点:

p=B->next->next;   //获取B的头指针
B->next=A->next;  //B的尾指针指向A的头指针
A->next=p;      //A的尾指针指向B的头指针

在这里插入图片描述

g. 双向链表

在单链表中,查找直接后继结点的执行时间为0(1), 而查找直接前驱的执行时间为O(n)。 为克服单链表这种单向性的缺点,可利用双向链表。顾名思义,在双向链表的结点中有两个指针域,一个指向直接后继,另一个指向直接前驱。

存储结构
typedef struct DuLNode
{
	ElemType data;        //数据域
	struct DuLNode *prior;  //直接前驱   
	struct DuLNode *next;    //直接后继 
} DuLNode,*DuLinkList;

双向链表在插入、删除时有很大的不同,在双向链表中需同时修改两个方向上的指针,在插入结点时需要修改四个指针,在删除结点时需要修改两个指针。两者的时间复杂度均为 O(n)。

插入
if(!(p=GetElem_Dul(L,i)))  //在L中确定第i个元素的位置指针p
   return ERROR;      //p为NULL时,第i个元素不存在
s=new DuLNode;      //生成新结点*s
s->data=e;           //将*s的数据域置为e
s->prior=p->prior;    //将*s指向第i个元素的直接前驱元素
p->prior->next=s;    //将第i个元素的直接前驱元素的后驱指针指向s
s->next=p;           //将*s指向第i个元素
p->prior=s;          //第i个元素的前驱指针指向*s

在这里插入图片描述

删除
if(!(p=GetElem_Dul(L,i)))       //在L中确定第i个元素的位置指针p
   return ERROR;            //p为NULL时,第i个元素不存在
p->prior->next=p->next;     //修改被删结点的前驱结点的后继指针为指向p的后继结点
p->next->prior=p->prior;    //修改被删结点的后继结点的前驱指针为指向p的前驱检点
delete p;                   //释放被删结点的空间

在这里插入图片描述

四、顺序表和链表的比较

  1. 存储空间的分配:顺序表的存储空间必须预先分配,元素个数扩充受一定限制,易造成存储空间浪费或空间溢 出现象;而链表不需要为其预先分配空间,只要内存空间允许,链表中的元素个数就没有限制。 基于此,当线性表的长度变化较大,难以预估存储规模时,宜采用链表作为存储结构。
  2. 存储密度的大小:链表的每个结点除了设置数据域用来存储数据元素外,还要额外设置指针域,用来存储指示元素之间逻辑关系的指针,从存储密度上来讲,这是不经济的。
    基于此当线性表的长度变化不大,易千事先确定其大小时,为了节约存储空间,宜采用顺 序表作为存储结构。
  3. 存取元素的效率:顺序表是由数组实现的,它是一种随机存取结构,指定任意一个位置序号i都可以在O(1)时间内直接存取该位置上的元素,即取值操作的效率高;而链表是一种顺序存取结构,按位置访问链表中第i个元素时,只能从表头开始依次向后遍历链表,直到找到第i个位置上的元素,时间复杂度为O(n), 即取值操作的效率低。 基于此,若线性表的主要操作是和元素位置紧密相关的这类取值操作,很少做插入或删除时, 宜采用顺序表作为存储结构。
  4. 插入和删除操作的效率:对于链表,在确定插入或删除的位置后,插入或删除操作无需移动数据,只需要修改指针, 时间复杂度为O(1)。而对千顺序表,进行插入或删除时,平均要移动表中近一半的结点,时间复杂度为O(n)。尤其是当每个结点的信息量较大时,移动结点的时间开销就相当可观。 基千此,对于频繁进行插入或删除操作的线性表,宜采用链表作为存储结构。

五、线性表的应用

a. 线性表的合并

步骤
  1. 分别获取LA表长m和LB表长n。
  2. 从LB中第1个数据元素开始,循环n次执行以下操作:从LB中查找第i (1<=i<=n) 个数据元素赋给e; 在 LA中查找元素e, 如果不存在,则将e插在表LA的最后。
代码
m=ListLength(LA); 
n=ListLength(LB);             //求线性表的长度 
for(i=1;i<=n;i++) 
{
	GetElem(LB,i,e);            //取LB中第i个数据元素赋给e
if (! LocateElem (LA, e))     //LA中不存在和e相同的数据元素
	Listinsert(LA,++m,e);     //将 e插在LA的最后
}
算法的时间复杂度

上述算法的时间复杂度取决于抽象数据类型 List定义中基本操作的执行时间,假设 LA和LB 的表长分别为m和n, 循环执行n次,则GetElem 和 Listlnsert 这两个操作的执行时间 和表长无关,LocateElem的执行时间和表长m成正比,因此,算法的时间复杂度为 O(m x n)。

b. 顺序有序表的合并

步骤

在这里插入图片描述

代码

在这里插入图片描述

算法时间复杂度

此算法在归并时,需要开辟新的辅助空间,所以空间复杂度也为O(m+ n), 空间复杂度较高。 利用链表来实现上述归并时,不需要开辟新的存储空间,可以使空间复杂度达到最低。

c.链式有序表的合并

需设立3个指针 pa、 pb和 pc, 其中pa和 pb 分别指向LA和 LB中当前待比较插入的结点,而pc指向LC中当前最后一个结点(LC的表头结点设为LA的表 头结点)。

步骤
  1. 指针pa和pb初始化,分别指向LA和LB的第一个结点。
  2. LC的结点取值为LA的头结点。
  3. 指针pc初始化,指向LC的头结点。
  4. 当指针 pa 和 pb 均未到达相应表尾时,则依次比较 pa 和 pb 所指向的元素值,从 LA 或 LB 中取元素值较小的结点插入到LC的最后。
  5. 将非空表的剩余段插入到pc所指结点之后。
  6. 释放 LB的头结点。
代码

在这里插入图片描述

算法的时间复杂度

在归并两个链表为一个链表时,不需要另建新表的结点空间,而只需将原来两个链表中结点之间的关系解除,重新按元素值非递减的关系将所有结点链接成一个链表即可,所以空间复杂度为0(1)。

六、总结

在这一部分属于数据结构中重要的基础知识之一,其中比较重要的知识点是单链表的创建、插入、删除、算法时间复杂度,循环链表的创建、插入、删除、算法时间复杂度,双向链表的创建、插入、删除、算法时间复杂度,以及线性表与单链表的比较,考题中常以选择和大题考查。以后有时间回顾时我会更进一步的进行完善,OK本篇博文到此结束。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值