2.1 线性表的类型定义
2.2 线性表的顺序表示和实现
2.3 线性表的链式表示和实现
2.3.1 线性链表
2.3.2 循环链表
2.3.3 双向链表
2.1 线性表的类型定义
一、线性表(Liner_list)的基本概念
线性表的定义:
由n(n≥0)个类型相同的数据元素组成的有限序列。
通常表示成:
L=( a1, a2,…,ai-1,ai,ai+1,…,an) 其中:
ØL:线性表名称,习惯用大写;
Øai :组成线性表的数据元素 (1<=i<=n),习惯用小写;
Øn:表中数据元素个数,称为线性表的长度
Ø当n=0时,线性表为空,称为空线性表。
下角标i表示该元素在线性表中的位置或序号.
注意:
ai是一个抽象符号,不同情况下有不同的含义。
La=(34,89,765,12,90,-34) // 数据元素类型为int。
Ls=("Hello","World", "China") //数据元素类型为string。
Lb=(book1,book2,...,book100)//数据元素类型为如下的结构型:
struct bookinfo
{ int No; //图书编号
char *name; //图书名称
char *auther; //作者名称
...; };
二、线性表的逻辑结构特征:
Øai-1领先于ai,ai领先于ai+1。称ai-1是ai的直接前驱;ai+1是ai的直接后继。
Ø非空的线性表中,有且仅有一个没有直接前趋的开始结点 a1 ;有且仅有一个没有直接后继的终端结点 an ;
Ø内部结点ai(2≤i≤n-1)都有且仅有一个直接前趋ai-1和一个直接后继ai+1。
Ø同一个线性表中的元素具有相同特性,即数据对象。
三、线性表的ADT定义:
ADT List{
数据对象:D = {ai | ai属于Elemtype, (i=1,2,…,n, n≥0)}
数据关系:R = {< ai-1, ai >| ai-1, ai属于D,(i=2,3,…,n)}
基本操作:
InitList(L); //初始化线性表
DestroyList(L); //销毁线性表
ListInsert(L,i,e); //插入元素
ListDelete(L,i,e);//删除元素
……
} ADT List
线性表的基本操作:
Ø初始化线性表L InitList(L)
Ø求线性表L的长度 Getlen(L)
Ø判断线性表L是否为空 ListEmpty(L)
Ø元素定位操作 Locate(L,x)
Ø取元素内容操作 GetElem(L,i,e)
Ø插入数据元素操作 ListInsert(L,i,e)
Ø删除线性表L中第i个数据元素 ListDelete(L,i,e)
Ø输出操作 List(L)
说明:
Ø 某数据结构上的基本运算,不是它的全部运算,只是一些常用的基本的运算。
Ø每一个基本运算在实现时可根据不同的存储结构派生出一系列相关的运算来,不必定义出所有的运算,掌握基本运算后,其它的运算可以通过基本运算来实现。
Ø上面各操作中定义的线性表L,仅仅是一个抽象在逻辑结构层次的线性表,尚未涉及到它的存储结构。
Ø每个操作在逻辑结构层次上不能用某种语言写出具体的算法,算法的实现只有在存储结构确立之后。
2.2 线性表的顺序表示和实现
2.2.1顺序表的定义
Ø 顺序表:采用顺序存储结构存储的线性表,即在内存中用地址连续的一组存储空间顺序存放线性表的各元素。
Ø用物理上的相邻实现数据元素之间的逻辑相邻关系。如图 2.1 所示。
Ø既简单,又自然。
2.2.2顺序表上的基本操作实现
//1. 初始化顺序表
int initlist(sqlist *L)
{L->data=(ElemType*) malloc(INITSIZE*sizeof(ElemType));
//分配空间
if (L->data==NULL)
return 0; //若分配空间不成功,返回0
L->length=0; //将当前线性表长度置0
L->listsize=INITSIZE; //当前顺序表的容量为初始量
return 1; //成功返回1
}
//2. 销毁顺序表
void destroylist(sqlist *L)
{
if (L->data)
free(L->data);
//释放线性表占据的所有存储空间
}
//3. 清空顺序表
void clearlist(sqlist *L)
{
L->length=0; //将线性表的长度置为0
}
//4. 求顺序表的长度
int getlen(sqlist &L)
{
return (L.length);
}
//5. 判断顺序表是否为空
int listempty(sqlist &L)
{ if (L.length==0)
return 1;
else
return 0;
}
//6. 取顺序表中第i个数据元素的内容
int getelem(sqlist &L, int i, ElemType *e)
{ if (i<1||i>L.length)
return 0;
//判断i值是否合理,若不合理,返回ERROR
*e=L.data[i-1];
/*数组中第i-1的单元存储着线性表中第i个数据元 素的内容*/
return 1;
}
//7. 元素定位操作
int locate(sqlist &L, ElemType e)
//在顺序表中检索第一个值为e的元素位序
{
for (i=0; i< L.length; i++)
if (L.data[i]==e) return i+1;
return 0; //未找到,返回0
}
//8、顺序表的插入
//例:(35,12,24,42),在i=2的位置上插入33
//什么时候不能插入?->注意边界条件
//表满:length>=MaxSize
//合理的插入位置:1≤i≤length+1(i指的是元素的序号)
在顺序表L中第i个位序上插入数据元素X算法步骤:
1)对输入参数的安全性进行检查,插入位置i应在表长+1范围内,即1≤i≤L.length+1
2)存储空间的处理:若原表的空间已满,应追加存储空间的分配
3)数据块的移动:将表中从i到L.length位置上的所有元素往后移动一个位置。
4)在第i个位序上插入x, 表长加1: ++L.length
int insert_sq(sqlist *L,int i,ElemType e)
{ if (i<1||i>L->length+1) return 0;
//检查i值是否合理,不合理返回0
if (L->length==L->listsize)
{ //空间不够,需增加存储空间
newbase=(ElemType *)realloc(L->data,
(L->listsize+LISTINCREMENT)*sizeof(ElemType));
if (!newbase) exit(0) ;//空间分配失败
L->data=newbase; // 新基址
L.lisesize+=LISTINCREMENT;} // 增加存储容量
for ( j=L->length-1 ; j>=i-1 ; j-- )
//将线性表第i个元素之后的所有元素向后移动
L->data[j+1]=L->data[j];
L->data[i-1]=e;
//将新元素的内容放入线性表的第i个位置
L->length++; //表长增1
return 1;}
注意:在第i (1≤i≤n)个元素之前插入一个元素时,需将第n至第i (共n-i+1)个元素向后移动一个位置。
顺序表插入算法分析
设表长为n,执行该算法的时间主要花费在结点后移语句上,该语句的执行次数(移动结点的次数)是n-i+1。
Ø最好情况:i=n+1,即在表尾插入元素,循环变量的终值大于表长,不进行结点后移,其时间复杂度O(1);
Ø最坏情况:i=1,即在表头插入元素,结点后移语句循环执行n次,需移动表中所有结点,时间复杂度为O(n)。
结论:线性表的插入操作中,所需移动结点的次数不仅依赖于表的长度,还与插入位置有关。
Ø若假定在n+1个位置上插入元素的可能性均等,则平均移动元素的个数为:
在顺序表上做插入运算,平均要移动表上一半结点。当表长 n较大时,算法效率相当低。 虽然Eis(n)中n的系数较小,但就数量级而言,仍然是线性阶的。
因此顺序表插入算法的平均时间复杂度为O(n)。
//9、顺序表的删除
//例:(35, 33, 12, 24, 42),删除i=2的数据元素。
//需要考虑的异常情况:
//(1).是否表空?
//(2).删除位置是否合适?(正常位置:1≤i≤n)
删除顺序表中第 i个数据元素算法步骤:
1)对输入参数的安全性进行检查,插入位置i应在表长范围内,即1≤i≤L.length
2)取出位序为i的元素值赋给e
3)数据块的移动:将表中从i+1到L.length位置上的所有元素往前移动一个位置
4)表长减1:–L.length
int delete_sq(sqlist *L,int i,ElemType *e)
{
if (listempty(L))
return 0; //检测线性表是否为空
if (i<1||i>L->length)
return 0; //检查i值是否合理
*e=L->data[i-1]; //将欲删除的数据元素内容
//保留在e所指示的存储单元中
for (j=i;j<L->length;j++)
//将线性表第i+1个元素之后的所有元素向前移动
L->data[j-1]=L->data[ j ];
L->length--; //表长减1
return 1;
}
注意:删除第i (1≤i≤n)个元素时需将第i+1至第n (共n-i)个元素向前移动一个位置。
顺序表删除算法的分析
该算法的时间分析与插入算法相似,结点的移动次数也是由表长n和位置i决定。
Ø最好情况:i=n,删除最后一个元素,循环变量的初值大于终值,前移语句将不执行,无需移动结点;
Ø最坏情况:i=1,删除第一个元素,前移语句将循环执行n-1次,需移动表中除开始结点外的所有结点。
两种情况下,时间复杂度分别为O(1)和O(n)。
在进行删除操作时,若假定删除每个元素的可能性均等,则平均移动元素的个数为: X
即在顺序表上做删除运算,平均要移动表中约一半的结点,平均时间复杂度也是O(n)。
//10、顺序表的实现——按值查找
//例:在(35, 33, 12, 24, 42) 中查找值为12的元素,返回在表中的序号。
//注意序号和下标之间的关系
分析结论
顺序存储结构表示的线性表,在做插入或删除操作时,平均需要移动大约一半的数据元素。当线性表的数据元素量较大,并且经常要对其做插入或删除操作时,这一点需要值得考虑。
例2-1 顺序表LA和LB分别表示两个集合A和B,现要求一个新的集合A=A∪B。
算法思想:① 依次从LB中取出一个数据元素;
② 判在LA中是否存在;
③ 若不存在,则插入到LA中。
例2-2 顺序表LA和LB中的数据元素按值递增有序排列,长度分别为la.len和lb.len ,要求将LA和LB归并为一个新的线性表LC,且仍按值递增排列。
算法思想:
① 初始化:置LC为空表,i,j分别指向LA和LB的第一个数据元素且初值为1 ,k表示LC的长度且初值为0。
② 当i<=la.len &&j<=lb.len时: ai<=bj :将ai插入在LC的k+1前,i++,k++;否则,将bj插入 在LC的k+1前,j++,k++。
③ 重复②直到某个表的元素插入完毕。
④ 将未插入完的表的余下的元素,依次插入在LC后。
思考题
利用顺序表的操作,实现以下的函数
(1) 从顺序表L中删除具有最小值的元素并由函数返回被删元素的值。空出的位置由最后一个元素填补,若顺序表为空则显示出错信息并退出运行。
(2)删除顺序表L中从第i个元素起的k个元素。
(3)从顺序表中删除具有给定值x的所有元素。
(4)从顺序表中删除所有其值重复的元素,使表中所有元素的值均不相同。
小 结
v1、线性表的定义
v2、线性表的特征
v3、线性表的顺序存储结构
(1)地址关系:
LOC(ai+1)=LOC(ai)+L (2 ≤ i≤n)
LOC(ai)=LOC(a1)+(i-1)*L (1 ≤i ≤n)
(2)插入、删除算法
A、算法描述
B、时间复杂度
线性表顺序存储结构的特点:
线性表的数据元素依次存放在连续的存储单元中
利用数据元素的存储顺序表示相应的逻辑顺序,这种存储方式属于静态存储形式。
是一种简单、方便的存储方式。
暴露的问题
在做插入或删除元素的操作时,会产生大量的数据元素移动;
对于长度变化较大的线性表,要一次性地分配足够的存储空间,但这些空间常常又得不到充分的利用;
2.3 线性表的链式表示和实现
存储思想:用一组任意的存储单元存放线性表的元素
单链表:线性表的链接存储结构。
2.3.1 线性链表
单链表
例:(a1, a2 ,a3, a4)的存储示意图
![1557561562277](G:\笔记\Typora\数据结构\02-线性表\images\02-单链表存储示意图 .png)
存储特点:
1.逻辑次序和物理次序
不一定相同。
2.元素之间的逻辑关系
用指针表示。
单链表
是由若干结点构成的;单链表的结点只有一个指针域。
单链表的结点结构:
data:存储数据元素
next:存储指向后继结点的地址
2、单链表的简化图形描述形式,如下图所示:
head:头指针,指向单链表中的第一个结点,是单链表操作的入口点。
最后一个结点:没有直接后继结点,通常其指针域放入一个特殊的值NULL,图示中常用(^)符号表示。
*判断单链表结束的标志:指针域为NULL
带头结点的单链表
空表
非空表
空表和非空表不统一,缺点?
如何将空表与非空表统一?
3、带头结点的单链表
在链表的第一个结点之前附加一个结点,称为头结点。这样可以免去对链表第一个结点的特殊处理。如下图所示:
头结点的数据域为NULL或为特定的值,指针指向第一个结点。
何谓头指针、头结点和首元结点?
头指针是指向链表中第一个结点(或为头结点或为首元结点)的指针。
单链表可由一个头指针唯一确定。
头结点是在链表的首元结点之前附设的一个结点;数据域内只放空表标志和表长等信息;
首元结点是指链表中存储线性表第一个数据元素a1的结点。
4、链式存储结构的特点
v数据元素的存储单元可以是连续的,也可以不连续。
v元素在存储单元中的存放顺序与逻辑顺序不一定一致;
v对线性表操作时,只能通过头指针进入链表,并通过每个结点的指针域向后扫描,具有这种特点的存取方式被称为顺序存取方式。
5、单链表上基本操作的实现
//用C语言描述的单链表存储结构如下:
# include “stdio.h”
typedef int ElemType;
typedef struct node//定义链表结点类型
{ ElemType data; //数据域
struct node *next; //指针域
}LNode, slink,*LinkList; ; //单链表类型名
如何申请一个结点?
1、利用系统函数 malloc( )
2、利用程序中已经声明数组的元素产生结点.
如何引用数据元素?
(*s).data ; s=(LinkList)malloc(sizeof(LNode) ;
|
s->data ;
如何引用指针域?
s->next;
指针操作基本示例
指针指向节点 p=q
指针指向后继 p=q->next
指针移动 p=p->next
链指针改接 p->next=q
链指针改接后继 p->next=q->next
(1)建立单链表
方法:头插法、尾插法
头插法:
尾插法:
a、头插法建立单链表
™从一个空表开始,重复读入数据,生成新结点。
™将读入数据存放到新结点的数据域中。
™将新结点插入到当前链表的表头上。
™直到读入n个元素为止。
LNode *creatslink(int n)
{LNode *head,*p;
int i;
if(n<1) return NULL;
head=NULL ;
for(i=1;i<=n;i++) //建立n个结点的单链表
{ p=(LNode *)malloc(sizeof(LNode));
scanf(“%d”,&p->data);
p->next=head;
head=p; }
p=(slink*)malloc(sizeof(slink));
p->next=head;
head=p; //头结点
return (head); }
指针变量和结点变量是两个不同的概念。
Øp为指针变量(其值为结点地址),*p为结点变量。
Øp通过标准函数生成,即:
p=(LNode *)malloc(sizeof(LNode));
函数malloc分配了一个类型为LNode的结点变量的空间,并将其首地址放入指针变量p中。
不再需要p所指的结点变量时,通过函数free§释放P结点的变量空间
b、尾插法建立单链表
头插法建立链表虽然算法简单,但生成的链表中结点的次序和输入的顺序相反。
若希望二者次序一致,可采用尾插法建表。
™将新结点插入到当前链表的表尾上,为此必须增加一个尾指针r始终指向当前链表的尾结点。
尾插建表法思想:
-
读入一个数据元素
-
生成一个新结点
-
数据元素送入结点数据域,指针域置为空(NULL) 4) 如果是空表:头指针指向该结点。
否则:结点插到表尾。
- 重复步骤1) ~ 4),直到表创建完。
LNode *creatslink(int n)
{LNode *head,*p,*r;
int i;
if(n<1) return NULL;
r=head=(LNode *)malloc(sizeof(LNode)); //头结点
for(i=1;i<=n;i++)
{ p=(LNode *)malloc(sizeof(LNode));
scanf(“%d”,&p->data);
r->next=p; r=p; }
r->next=NULL; //处理尾结点
return(head); }
链表开始结点之前附加一个头结点的优点:
a、对第一个结点的操作不再特殊。第一个结点的位置被存放在头结点的指针域中,所以在链表第一个位置上的操作就和在表的其它位置上的操作一致,无需进行特殊处理;
b、空表和非空表的处理也就统一。无论链表是否为空,其头指针是指向头结点非空指针(空表中头结点的指针域为空) 。
上述算法里动态申请新结点空间时未加错误处理,可作下列处理:
p=(LNode *) malloc(sizeof(LNode)));
if(p==NULL)
{ printf(“No space for node can be obtained”);
return 0;
}
以上两种建立单链表算法的时间复杂度均为O(n)。
(2) 定位操作(或称查找操作)
a、按序号查找
在链表中,不能象顺序表中那样直接按序号i访问结点,只能从链表的头指针出发,顺链域next逐个结点往下搜索,直到搜索到第i个结点为止。因此,链表不是随机存取结构。
设单链表的长度为n,要查找表中第i个结点,仅当1≤i≤n时,i的值是合法的。但有时需要找头结点的位置,故我们将头结点看做是第0个结点,其算法如下:
LNode *locate(LNode *head,int i) //返回第i个结点的指针
{
int j=1;
LNode *p;
p=head->next;
while(p->next && j<i) //下一结点不为空,且没到i
{
p=p->next;
j++;
}
if (i==j)
return p;
else
return NULL;
}
b、按值查找
按值查找是在链表中,查找是否有结点值等于给定值x的结点,若有的话,则返回首次找到的其值为x的结点的存储位置;否则返回NULL。
查找过程从头结点出发,顺着链表逐个将结点的值和给定值x作比较。其算法如下:
LNode *locate (LNode *head,ElemType x)
{
LNode *p;
p=head->next; //让p指向第一个结点
while( p && p–>data!=x)
p=p–>next;
if(p==NULL) //找到最后一个结点的指针域为空
return 0;
return p;
}
算法的执行时间与x的取值有关,其平均时间复杂度为O(n)。
(3) 插入运算
将值为x的新结点插入到表的第i个结点的位置上,即插入到ai-1与ai之间。因此
步骤:
™首先找到ai-1的存储位置p。
™生成一个数据域为x的新结点*q。
™令结点*p的指针域指向新结点,新结点的指针域指向结点ai,实现三个结点ai-1,x和ai之间的逻辑关系变化
单链表的插入:在第i-1个结点后插入一个新结点
思想:
-
从第1个结点出发,找到第i-1个结点;
-
将新结点插入其后
-
返回操作结果信息(成功与否)
找第i-1个结点:
p=head
p=p->next : 是个循环过程
循环条件:没到表尾且没到第i-1个结点。
p&&j<i-1 (j的初始为0)
插入结点:
q->next=p->next
p->next=q
算法:
int insert(LNode *head ,int i, ElemType x)
{//在第i个结点之前插入值为x的新结点
LNode *p,*q;
int j=0;
if(i<1) return 0;
p=head;
while( p && j<i-1 ) //找第i-1个结点
{ p=p->next; j++; }
if(p==NULL) return 0; //i值超过表长+1
q=(LNode *)malloc(sizeof(LNode));
q->data=x;//创建新节点
q->next=p–>next;
p->next=q;//插入结点语句,其前后顺序不能交换
return 1;
}
设链表的长度为n,合法的插入位置是1≤i≤n+1。注意当i=1时,while循环找到的是头结点,当 i=n+1时,while循环找到的是结点an。
算法的时间复杂度为:O(n)。
(4) 删除运算
删除运算是将表的第i个结点删去。
因为在单链表中结点ai的存储地址是在其直接前趋结点a i-1的指针域next中,所以
步骤:
™首先找到a i-1的存储位置p。
™然后令p–>next指向ai的直接后继结点,即把ai从链上摘下。
™最后释放结点ai的空间,将其归还给“存储池”。
此过程为:删除第i个结点
思想:
-
从第1个结点出发,找到第i-1个结点;
-
删除第i个结点
-
返回操作结果信息(成功与否)
算法:
int delete(slink *head, int i, ElemType *e)
{//删除单链表中第i个结点
slink *p,*q;
int j;
p=head;j=0;
while(p->next && j<i-1) { p=p->next; j++; }
if(p–>next==NULL || j>i-1) return 0;//当i>n或i<1时,删除位置不合理
q=p->next; //q指向被删除结点
p->next=q->next; *e=q->data;
free( q ) ;
return 1;}
设单链表的长度为n,则删去第i个结点仅当1≤i≤n时是合法的。注意,当i=n+1时,虽然被删结点不存在,但其前趋结点却存在,它是终端结点。因此被删结点的直接前趋*p存在并不意味着被删结点就一定存在。
仅当p存在(即p!=NULL)且p不是终端结点(即p–>next!=NULL)时,才能确定被删结点存在。显然此算法的时间复杂度也是O(n)。设单链表的长度为n,则删去第i个结点仅当1≤i≤n时是合法的。注意,当i=n+1时,虽然被删结点不存在,但其前趋结点却存在,它是终端结点。因此被删结点的直接前趋*p存在并不意味着被删结点就一定存在。
仅当p存在(即p!=NULL)且p不是终端结点(即p–>next!=NULL)时,才能确定被删结点存在。显然此算法的时间复杂度也是O(n)。
通过上面的基本操作我们得知:
(1) 在单链表上插入、删除一个结点,必须知道其前驱结点。
(2) 单链表不具有按序号随机访问的特点,只能从头指针开始一个个顺序进行。
2.3.2 循环链表
从单链表中某结点p出发如何找到 ?
将单链表的首尾相接,将终端结点的指针域由空指针改为指向头结点,构成单循环链表,简称循环链表.
1、单向循环链表
循环链表是一种头尾相接的链表。其特点是无须增加存储量,仅对表的链接方式稍作改变,即可使得表处理更加方便灵活。
单循环链表:在单链表中,将终端结点的指针域NULL改为指向表头结点的或开始结点,就得到了单链形式的循环链表,并简单称为单循环链表。
为了使空表和非空表的处理一致,循环链表中也可设置一个头结点。这样,空循环链表仅有一个自成循环的头结点表示。如下图所示:
在用头指针表示的单循环链表中,找开始结点a1的时间是O(1),然而要找到终端结点an,则需从头指针开始遍历整个链表,其时间是O(n)。
在很多实际问题中,表的操作常常是在表的首尾位置上进行,此时头指针表示的单循环链表就显得不够方便。
如果改用尾指针rear来表示单循环链表,则查找开始结点a1和终端结点an都很方便,它们的存储位置分别是 (rear->next)->next和rear,显然,查找时间都是O(1)。
因此,实际中多采用尾指针表示单循环链表。
p= R1–>next; /*保存R1 的头结点指针*/
R1->next=R2->next->next; /*头尾连接*/
free(R2->next); /*释放第二个表的头结点*/
R2->next=p; /*组成循环链表*/
2.3.3 双向链表
如何求结点p的直接前驱,时间性能?
为什么可以快速求得结点p的后继?
如何快速求得结点p的前驱?
双链表:
在单链表的每个结点中再设置一个指向其前驱结点的指针域。
结点结构:
data:数据域,存储数据元素;
prior:指针域,存储该结点的前趋结点地址;
next:指针域,存储该结点的后继结点地址。
//定义结点结构:
# include “stdio.h”
typedef int ElemType;
typedef struct node //双链表结点类型
{ ElemType data;
struct node *next; //指向直接后继结点
struct node *prior; //指向直接前驱结点
}dlink; //双链表类型
和单链表类似,双链表一般也是由头指针唯一确定的,增加头结点也能使双链表上的某些运算变得方便。
设指针p指向某一结点,则双向链表结构的对称性可用下式描述:
(p->prior)->next=p=(p->next)->prior
即结点p的存储位置既存放在其前趋结点(p->prior)的直接后继指针域中,也存放在它的后继结点*(p->next)的直接前趋指针域中。
双链表的插入
q->prior=p;
q->next=p->next;
p->next->prior=q;
p->next=q;
//注意指针修改的相对顺序
//(1) 插入操作
int insert(dlink *head, int i, ElemType x)
{ //在双链表中第i个结点前插入一个值为x的新结点
dlink *p,*q; int j;
if(i<1) return 0;
p=head; j=0;
while( p && j<i-1) { p=p->next; j++; }
if(p==NULL) return 0;
q=(dlink *)malloc(sizeof(dlink));
q->data=x;
q->next=p->next; q->prior=p;
if(p->next!==NULL)p->next->prior = q;
p->next=q;
return 1;}
//(2) 删除操作
int delete(dlink *head, int i, ElemType *e)
{ //删除双链表中第i个结点
dlink *p; int j;
if(i<1) return 0;
p=head->next; j=1;
while( p&& j<i) { p=p->next; j++; }
if(p==NULL) return 0; // 第i个元素不存在
p->prior->next=p->next;
p->next->prior=p->prior;
*e= p->data; free(p); return 1;}
注意:
与单链表的插入和删除操作不同的是,在双链表中插入和删除必须同时修改两个方向上的指针。上述两个算是法的时间复杂度均为O(n)。
2、双向循环链表
将双链表的最后一个结点的后继指针域的值由空改为指向头结点,头结点中的前驱指针域的值由空改为指向尾结点,即构成双向循环链表。双向循环链表的结点类型同双链表。
例1、已知单链表H,写一算法将其倒置。即实现如图2.12的操作。(a)为倒置前,(b)为倒置后。
算法思路:
依次取原链表中的每个结点,将其作为第一个结点插入到新链表中头结点前,指针p用来指向当前结点,p为空时结束。
该算法只是对链表中顺序扫描一边即完成了倒置,所以时间性能为O(n)。
例2、 已知单链表L,写一算法,删除其重复结点,即实现如图2-13的操作。(a)为删除前,(b)为删除后。
算法思路:
用指针p指向第一个数据结点,从它的后继结点开始到表的结束,找与其值相同的结点并删除之;p指向下一个;依此类推,p指向最后结点时算法结束。
例3、假定采用带头节点的单链表保存单词,当两个单词有相同的后缀时,则可共享相同的后缀存储空间,例如,“loading”和“being”,如下图所示。
设str1和str2分别指向两个单词所在单链表的头节点,链表节点结构为:
请设计一个时间上尽可能高效的算法,找出由str1和str2所指向两个链表共同后缀的起始位置(如图中字符i所在节点的位置p)。
要求:
(1)给出算法的基本设计思想。
(2)根据设计思想,采用C或C++或JAVA语言描述算法,关键为之处给出注释。
(3)说明你所设计算法的时间复杂度。
typedef struct Node
{ char data;
struct Node *next;
} SNODE;
SNODE * Findlist(SNODE *str1,SNODE *str2)
{ int m,n;
SNODE *p,*q;
m=Listlen(str1); //求单链表str1的长度m
n=Listlen(str2); //求单链表str2的长度n
for (p=str1;m>n;m--) //若m大,则str1后移m-n+1个节点
p=p->next;
for (q=str2;m<n;n--) //若n大,则str1后移n-m+1个节点
q=q->next;
while (p->next!=NULL && p->next!=q->next)
{ p=p->next; //p、q两步后移找第一个指针值相等的节点
q=q->next;
}
return p->next;
}
顺序表和链表的比较
在本章介绍了线性表的逻辑结构及它的两种存储结构:顺序表和链表。
通过对它们的讨论可知它们各有优缺点,
顺序存储有三个优点:
(1)方法简单,各种高级语言中都有数组,容易实现。
(2)不用为表示结点间的逻辑关系而增加额外的存储开销。
(3) 顺序表具有按元素序号随机访问的特点。
顺序存储有两个缺点:
(1) 在顺序表中做插入删除操作时,平均移动大约表中一半的元素,因此对n较大的顺序表效率低。
(2) 需要预先分配足够大的存储空间,估计过大,可能会导致顺序表后部大量闲置;预先分配过小,又会造成溢出。
链表的优缺点恰好与顺序表相反。
在实际中怎样选取存储结构呢?
通常有以下几点考虑: