一、线性表的定义
线性表List:零个或多个数据元素的有限序列。
※强调:
- 线性表示一个序列。也就是元素之间是有顺序的,若元素存在多个,则第一个元素无前驱,最后一个元素无后继,其他每个元素都有且只有一个前驱和后继;
- 线性表强调是有限的,即元素个数是有限的。在计算机中处理的对象都是有限的,那种无限的序列只存在于数学的概念中。
若将线性表记为
a
1
a_1
a1,···,
a
i
−
1
a_{i-1}
ai−1,
a
i
a_{i}
ai,
a
i
+
1
a_{i+1}
ai+1,
a
n
a_{n}
an,则表中
a
i
a_{i}
ai领先于
a
i
a_{i}
ai,
a
i
a_{i}
ai领先于
a
i
+
1
a_{i+1}
ai+1,称
a
i
−
1
a_{i-1}
ai−1是
a
i
a_{i}
ai的直接前驱元素
,
a
i
+
1
a_{i+1}
ai+1是
a
i
a_{i}
ai的直接后继元素
。线性表元素的个数n(n
≥
\geq
≥ 0)定义为线性表的长度
,当n=0时,称为空表
。i为数据元素
a
i
a_{i}
ai在线性表中的位序
。在复杂的线性表中,一个数据元素可以由若干数据项组成。
二、线性表的抽象数据类型
(对于不同的应用,线性表的基本操作是不同的,以下操作是最基本的。)线性表的抽象数据类型定义如下:
ADT 线性表(List)
Data
线性表的数据对象集合为{ a 1 a_1 a1,···, a i − 1 a_{i-1} ai−1, a i a_{i} ai, a i + 1 a_{i+1} ai+1, a n a_{n} an},每个元素的类型均为DataType。其中,除第一个元素 a 1 a_1 a1外,每一个元素有且只有一个直接前驱元素,除了最后一个元素 a n a_{n} an,每一个元素有且只有一个直接后继元素。数据元素之间的关系是一对一的关系。
Operation
InitList(*L):初始化操作,建立一个空的线性表L。
ListEmpty(L):若线性表为空,返回true,否则返回false。
ClearList(*L):将线性表清空。
GetElem(L,i,*e):将线性表L中的第i个位置元素值返回给e。
LocateElem(L,e):在线性表L中查找与给定值e相等的元素,查找成功就返回该元素的序号,否则返回0表示失败。
ListInsert(*L,i,e):在线性表L中第i个位置插入新元素e。
ListDelete(*L,i,*e):删除线性表L中第i个位置元素,并用e返回其值。
ListLength(L):返回线性表L的元素个数。
endADT
三、线性表的顺序存储结构
1.顺序存储定义
线性表的顺序存储结构,指的是用一段地址连续的存储单元一次存储线性表的数据元素。
2.顺序存储方式
线性表的顺序存储结构,说白了就是在内存中找块地儿,通过占位的形式,把一定内存空间给占了,然后把相同数据类型的数据元素一次存放在这块空间中。既然线性表的每个数据元素的类型都相同,所以可以用C语言(其他语言也相同)的一维数组来实现顺序存储结构,代码如下:
#define MAXSIZE 20
typedef int ElemType;
typedef struct
{
ElemType data[MAXSIZE];
int length;
}SqList;
顺序结构需要三个属性:
- 存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置。
- 线性表的最大存储容量:数组长度MAXSIZE。
- 线性表的当前长度:length。
3.数据长度和线性表长度的区别
数组的长度是存放线性表的存储空间的长度,存储分配后这个量一般是不变的,动态分配会带来性能上的损耗。
线性表的长度是线性表中数据元素的个数,随着线性表插入和删除操作的进行,这个量是变化的。。
在任意时刻,线性表的长度应该小于等于数组的长度。
4.地址计算方法
内存中的地址,就像图书馆的座位一样,都是有编号的。存储器中的每个存储单元都有自己的编号,这个编号称为地址
。由于每个数据元素,不管是整型、实型还是字符型,都是需要占用一定的存储空间的。假设占用的是c个存储单元,那么线性表中第i+1个数据元素的存储位置和第i个数据元素的存储位置满足下列关系(LO0C表示获得存储位置的函数):
L O C ( a i + 1 ) = L O C ( a i ) + c LOC(a_{i+1})=LOC(a_i)+c LOC(ai+1)=LOC(ai)+c
所以对于第i个数据元素 a i a_i ai的存储位置可以由 a 1 a_1 a1推算得出
L O C ( a i ) = L O C ( a 1 ) + ( i − 1 ) ∗ c LOC(a_{i})=LOC(a_1)+(i-1)*c LOC(ai)=LOC(a1)+(i−1)∗c
通过这个公式,可以随时计算出线性表中任意位置的地址,对每一个元素都是相同的处理时间,即其存取时间性能为
O
(
1
)
O(1)
O(1),通常那具有这一特点的存储结构称为随机存取结构
。
5.顺序存储结构获取元素操作
- 获得元素的操作:
#define OK 1
#define ERROR 0
/*Status是函数的类型,其值是函数结果状态代码,如OK等*/
typedef int Status;
/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*/
/*操作结果:用e返回L中第i个数据元素的值,注意i是指位置,第一个位置是从0开始*/
Status GetElem(SqList L,int i,ElemType *e)
{
if (L.length == 0 || i<1 || i>L.length)
return ERROR;
*e = L.data[i - 1];
return OK;
}
这里把指针*e的值给修改成L->data[i - 1],这就是真正要返回的数据。
6.顺序存储结构插入操作
- 插入操作
插入算法的思路
(1)如果插入位置不合理,抛出异常;
(2)如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;
(3)从最后一个元素开始向前遍历到第 i 个位置,分别将它们都像后移一个位置;
(4)将要插入元素填入位置 i 处;
(5)表长加1。
/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*/
/*操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1*/
Status ListInsert(SqList* L, int i, ElemType e)
{
int k;
if (L->length == MAXSIZE) /*顺序线性表已经满*/
return ERROR;
if(i<1||i>L->length+1) /*当i比第一位置小或者比最后一位置大时*/
return ERROR;
if (i <= L->length) /*若插入数据位置不在表尾*/
{
for (k = L->length; k >= i + 1; k--) /*将要插入位置后的元素向后移一位*/
{
L->data[k + 1] = L->data[k];
}
}
L->data[i - 1] = e; /*将新元素插入*/
L->length++;
return OK;
}
7.顺序存储结构删除操作
- 删除操作
删除算法的思路:
(1)如果位置不合理,抛出异常;
(2)取出删除元素;
(3)从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置;
(4)表长减1。
/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*/
/*操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1*/
Status ListDelete(SqList* L, int i, ElemType *e)
{
int k;
if (L->length == 0) /*顺序线性表为空*/
return ERROR;
if (i<1 || i>L->length ) /*删除位置不正确*/
return ERROR;
*e = L->data[i - 1];
if (i < L->length) /*若删除数据位置不在表尾*/
{
for (k = i; k < L->length; k++) /*将要删除位置后的元素向前移一位*/
{
L->data[k - 1] = L->data[k];
}
}
L->length--;
return OK;
}
8.小结
线性表的顺序存储结构,在读取数据时,不管哪个位置,时间复杂度都是 O ( 1 ) O(1) O(1);在插入或删除时,时间复杂度都是 O ( n ) O(n) O(n)。
- 优缺点
优点:①无须为表示元素之间的逻辑关系增加额外的存储空间;
②可以快速地存取表中任意位置的元素
缺点:①插入和删除需要移动大量元素;
②当线性表长度变化较大时,难以确定存储空间容量;
③造成存储空间的“碎片”
四、线性表的链式存储结构
1.链式存储结构定义
线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。链式结构中,除了要存储数据元素信息外,还要存储它的后继元素的存储地址,我们把存储数据元素信息的域称为数据域
,把存储直接后继位置的域称为指针域
。指针域中存储的信息称作指针
或链
。这两部分信息组成数据元素
a
i
a_i
ai的存储映像,称为结点(Node)
。
n个结点(
a
i
a_i
ai的存储映像)链结成一个链表,即为线性表(
a
1
a_1
a1,
a
2
a_{2}
a2,···,
a
n
a_n
an)的链式存储结构,
因为此链表的每个结点中只包含一个指针域,所以叫做单链表
。
对于线性表来说,总得有个头有个尾,链表也不例外。把链表中指向第一个结点指针叫做头指针,若链表有头结点,则是指向头结点的指针,无论链表是否为空,头指针均不为空,头指针是链表的必要元素
,那么整个链表的存取就必须是从头指针开始进行了。之后的每一个结点,其实就是上一个的后继指针指向位置。最后一个结点的指针为“空”(通常用NULL或“^”符号表示)
。为了方便对链表的操作,会在单链表的第一个结点前附设一个结点,称为头结点。
头结点的数据域可以不存储任何信息,也可以存储如线性表的长度等附加信息,头结点的指针域存储指向第一个结点的指针。
2.线性表链式存储结构代码描述
若线性表为空表,则头结点的指针域为“NULL”或“^”。
单链表示意图(没有头结点)
单链表示意图(带有头结点)
空链表如下图
单链表中,在C语言可用结构指针来描述:
/*线性表的单链表存储结构*/
typedef struct Node
{
ElemType data;
struct Node* next;
}Node;
typedef struct Node* LinkList; /*定义LinkList*/
假设p是指向线性表第i个元素的指针,则该结点
a
i
a_i
ai的数据域可以用p->data来表示,p->data的值是一个数据元素,结点
a
i
a_i
ai的指针域可以用p->next来表示,p->next的值是一个指针。p->next指向第
i
+
1
i+1
i+1个元素。
3、单链表的存取
在顺序存储结构中,计算任意一个元素的存储位置是很容易的,但是单链表中,由于第 i i i个元素到底在哪没办法一开始就知道,必须从头开始找,在算法上相对麻烦一些。
获得链表第i个数据的算法思路:
(1)声明一个指针p指向链表第一个结点,初始化 j j j从1开始;
(2)当 j < i j<i j<i时,遍历链表,让p的指针向后移动,不断指向下一结点, j j j累加1;
(3)若到链表末尾p为空,则说明第 i i i个结点不存在;
(4)否则查找成功,返回结点p的数据。
/*初始条件:链式线性表L已存在,1≤i≤ListLength(L)*/
/*操作结果:用e返回L中第i个元素的值*/
Status GetElem(LinkList L, int i, ElemType* e)
{
int j;
LinkList p; /*声明一个结点p*/
p = L->next; /*让p指向链表L的第一个结点*/
j = 1; /*j为计数器*/
while (p && j < i) /*p不为空或者计数器j还没有等于i时,循环继续*/
{
p = p->next; /*让p指向下一个结点*/
++j;
}
if (!p || j > i)
return ERROR; /*第i个元素不存在*/
*e = p->data; /*取第i个元素的数据*/
return OK;
}
4、单链表的插入
不用惊动其它结点,只需要让s->next和p->next的指针做一点改变就可。
s->next = p->next; /*将p的后继结点赋值给s的后继*/
p->next = s; /*将s赋值给p的后继*/
上述代码的顺序不可交换,否则会导致数据的丢失。对于单链表的表头和表尾的特殊情况,也是相同的操作。
单链表第i个数据插入结点的算法思路:
(1)声明一个指针p指向链表头结点,初始化 j j j从1开始;
(2)当 j < i j<i j<i时,遍历链表,让p的指针向后移动,不断指向下一结点, j j j累加1;
(3)若到链表末尾p为空,则说明第 i i i个结点不存在;
(4)否则查找成功,在系统中生成一个空结点s;
(5)将数据元素e赋值给s->data;
(6)单链表的插入标准语句s->next = p->next; p->next = s;
(7)返回成功。
/*初始条件:链式线性表L已存在,1≤i≤ListLength(L)*/
/*操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1*/
Status LinkInsert(LinkList *L, int i, ElemType e)
{
int j;
LinkList p,s;
p = *L;
j = 1;
while (p && j < i) /*寻找第i个结点*/
{
p = p->next; /*让p指向下一个结点*/
++j;
}
if (!p || j > i)
return ERROR; /*第i个元素不存在*/
s = (LinkList)malloc(sizeof(Node)); /*生成新结点(c语言标准函数)*/
s->data = e;
s->next = p->next; /*将p的后继结点赋值给s的后继*/
p->next = s; /*将s赋值给p的后继*/
return OK;
}
这段代码中用到了C语言的malloc函数,它的作用时生成新的结点,其数据类型与Node是一样的。
5、单链表的删除
实现将结点q删除的操作,就是将它的前继结点的指针绕过,指向它的后继结点即可。实际上就只有一步p->next = p->next ->next,用q来取代p->next ,即:
q = p->next;
p->next = q->next; /*将q的后继赋值给p的后继*/
单链表第i个数据删除结点的算法思路:
(1)声明一个指针p指向链表头结点,初始化 j j j从1开始;
(2)当 j < i j<i j<i时,遍历链表,让p的指针向后移动,不断指向下一结点, j j j累加1;
(3)若到链表末尾p为空,则说明第 i i i个结点不存在;
(4)否则查找成功,将欲删除的结点p->next赋值给q;
(5)单链表的删除标准语句p->next = q->next;
(6)将q结点中的数值赋值给e,作为返回;
(7)释放q结点;
(8)返回成功。
/*初始条件:链式线性表L已存在,1≤i≤ListLength(L)*/
/*操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1*/
Status LinkDelete(LinkList* L, int i, ElemType *e)
{
int j;
LinkList p, q;
p = *L;
j = 1;
while (p->next && j < i) /*寻找第i个结点*/
{
p = p->next;
++j;
}
if (!(p->next) || j > i)
return ERROR; /*第i个元素不存在*/
q = p->next;
p->next = q->next; /*将q的后继赋值给p的后继*/
*e = q->data; /*将q结点中的数据给e*/
free(q); /*释放删除结点的内存*/
return OK;
}
删除和插入都是由两部分组成:第一部分就是遍历查找第i个结点;第二个部分就是插入和删除。从这个算法来说,它们的时间复杂度都是
O
(
n
)
O(n)
O(n)。在不知道第i个结点的指针位置时,单链表结构比之顺序存储结构没有太大优势,但是如果希望从第i个位置插入10个结点,对于顺序存储结构,每一次插入都需要移动
n
−
i
n-i
n−i个结点,每次都是
O
(
n
)
O(n)
O(n)。而单链表,只需第一次时找到第i个位置的指针,此时为
O
(
n
)
O(n)
O(n),接下来只是简单的通过赋值移动指针,复杂度是
O
(
1
)
O(1)
O(1)。显然,对于插入或删除数据操作越频繁,单链结构的效率优势越明显
。
6、单链表的整表创建
单链表是一种动态结构,它所占用空间的大小和位置是不需要预先分配的,可以根据系统的情况和实际的需求即时完成。所以创建单链表的过程就是一个动态生成链表的过程。即从“空表”的初始状态,依次建立各元素的结点,并逐个插入链表
单链表整表创建的算法思路:
(1)声明一个指针p和计数器变量 i i i;
(2)初始化一空链表L;
(3)让L的头结点的指针指向NULL,即建立一个带头结点的单链表;
(4)循环:
①生成一新结点赋值给p;
②随机生成一个数字赋值给p的数据域p->data;
③将p插入到头结点与前一新结点之间。
/*随机产生n个元素的值,建立代表头结点的单链线性表L(头插法)*/
void CreateListHead(LinkList* L, int n)
{
LinkList p;
int i;
srand(time(0)); /*初始化随机数种子*/
*L = (LinkList)malloc(sizeof(Node));
(*L)->next = NULL; /*先建立一个带头结点的单链表*/
for (i = 0; i < n; i++)
{
p = (LinkList)malloc(sizeof(Node)); /*生成新结点*/
p->data = rand() % 100 + 1; /*随机生成100以内的数字*/
p->next = (*L)->next;
(*L)->next = p; /*插入到表头*/
}
}
上面的代码其实是用插队的方法,始终用新结点在第一的位置。
也可以将新结点都插在终端结点的后面,称之为尾插法。
/*随机产生n个元素的值,建立代表头结点的单链线性表L(尾插法)*/
void CreateListTail(LinkList* L, int n)
{
LinkList p,r;
int i;
srand(time(0)); /*初始化随机数种子*/
*L = (LinkList)malloc(sizeof(Node)); /*先建立一个带头结点的单链表*/
r = *L; /*r为指向尾部的结点*/
for (i = 0; i < n; i++)
{
p = (LinkList)malloc(sizeof(Node)); /*生成新结点*/
p->data = rand() % 100 + 1; /*随机生成100以内的数字*/
r->next = p; /*将表尾部端结点的指针指向新的结点*/
r = p; /*将当前节点定义为表尾结点*/
}
r->next = NULL; /*表示当前链表结束*/
}
注意L与r的关系,L是指这个单链表,而r是指尾结点,r会随着循环不断变化结点,而L则是随着循环增长一个多结点的链表。循环结束后r的指针域置空为NULL。
7、单链表的整表删除
单链表整表删除的算法思路:
(1)声明一指针p和q;
(2)将第一个结点赋值给p;
(3)循环
①将下一结点赋值给q;
②释放p;
③将q赋值给p。
/*初始条件:链式线性表L已存在,*/
/*操作结果:将L重置为空表*/
Status ClearList(LinkList* L)
{
LinkList p, q;
p = (*L)->next; /*p指向第一个结点*/
while (p) /*检测是否到表尾*/
{
q = p->next;
free(p);
p = q;
}
(*L)->next = NULL; /*头结点指针域为空*/
return OK;
}
五、单链表结构与顺序存储结构的优缺点
1、存储分配方式
- 顺序存储结构用一段连续的存储单元一次存储;
- 单链表存储结构用一组任意的存储单元存放。
2、时间性能
- 查找
- 顺序存储结构—— O ( 1 ) O(1) O(1)
- 单链表—— O ( n ) O(n) O(n)
- 插入和删除
- 顺序结构需要平均移动一半表长的元素—— O ( n ) O(n) O(n)
- 单链表找出位置的指针后,插入和删除—— O ( 1 ) O(1) O(1)
3、 空间性能
- 顺序存储结构——需要预分配存储,分大了浪费,分小了溢出
- 单链表——不需要分配存储空间,只要有就可以分配,元素个数不受限
总结:
若线性表需要频繁查找,很少插入和删除操作时,宜用顺序结构;
当线性表中的元素个数变化较大或不知道多大时。最好用单链表结构。
六、静态链表
1、静态链表的定义
早期的编程高级语言没有指针,就用数组代替指针来描述单链表。让数组的元素由两个数据域组成,data和cur。也就是说数组的每一个下标都对应着一个data和一个cur。cur相当于单链表中的next指针,存放该元素的后继在数组中的下标,把cur叫做游标
。把用数组描述的链表叫做静态链表,或游标实现法。
#define MAXSIZE 1000 /*存储空间初始分配量*/
/*线性表的静态链表存储结构*/
typedef struct
{
ElemType data;
int cur; /*游标(Cursor),为0时表示无指向*/
}Component,StaticLinkList[MAXSIZE];
另外对数组的第一个和最后一个元素作为特殊元素处理——不存数据。通常把未被使用的数组元素称为备用链表。下标为0的元素的cur,即数组第一个元素的cur就存放备用链表的第一个结点的下标;而数组的最后一个元素的cur则存放第一个有数值的元素的下标,相当于单链表中头结点的作用,当整个链表为空时,则为0。
上图所示相当于初始化数组状态,见下面代码:
/*将一维数组space中各分量链成一个备用链,space[0].cur为头指针,"0"表示空指针*/
Status InitList(StaticLinkList space)
{
int i;
for (i = 0; i < MAXSIZE-1; i++)
space[i].cur = i + 1;
space[MAXSIZE - 1].cur = 0; /*目前静态链表为空,最后一个元素的cur为0*/
return OK;
}
假设数据存入链表,如下图,“庚”是最后一个有值元素,它的cur设置为0;最后一个元素则因为第一个有值元素是“甲”而它的下标是1,所以设置最后一个元素的cur为1;第一个元素则因为备用链表的第一个下标为7,所以设置其cur为7。
2、静态链表的插入
在动态链表中,结点的申请和释放分别借用malloc()和free()来实现,在静态链表中,操作的是数组,不存在申请和释放的问题,所以需要自己实现这两个函数。
为了辨明数组中哪些分量未被使用,解决的办法是将所有未被使用过的及已被删除的分量用游标链成一个备用链表,每当进行插入时,便可以从备用链表上取第一个结点作为待插入的新结点。
为了获得空闲空间,即得到数组头元素的cur存的值——第一个空闲的下标,并返回其值,有如下实现:
/*若备用空间链表非空,则返回分配的结点下标,否则返回0*/
int Malloc_SSL(StaticLinkList space)
{
int i = space[0].cur; /*当前数组第一个元素的cur存的值
就是要返回的第一个备用空闲的下标*/
if (space[0].cur)
space[0].cur = space[i].cur; /*由于要拿出第一个备用分量来使用
所以就得把它的下一个分量拿来备用*/
return i;
}
不仅返回第一个空闲的下标,还把第一个空闲的cur再赋给头元素的cur,之后就可以继续分配新的空间,实现类似malloc()函数的作用。
现在如果需要在“乙”和“丁”之间插入一个“丙”,如果是顺序结构则需要把后面的元素都向后移一位,静态链可以直接修改元素的游标,如将“丙”放在下标7的位置上,找到“乙”,修改它的cur为7,再回到“丙”,将它的cur改为3,如下图。
代码实现:
Status ListInsert(StaticLinkList L, int i, ElemType e)
{
int j, k, l;
k = MAXSIZE - 1; /*k首先是最后一个元素的下标*/
if (i < 1 || i > ListLength(L) + 1)
return ERROR;
j = Malloc_SSL(L); /*获得空闲分量的下标*/
if (j)
{
L[j].data = e; /*将数据赋值给此空闲分量的data*/
for (l = 1; l <= i - 1; l++) /*找到第i个元素之前的位置*/
k = L[k].cur;
L[j].cur = L[k].cur; /*把第i个元素之前的cur赋值给新元素的cur*/
L[k].cur = j; /*把新元素的下标赋值给第i个元素之前的元素的cur*/
return OK;
}
return ERROR;
}
其中ListLength实现:
/*初始条件:静态链表L已存在。*/
/*操作结果:返回L中数据元素的个数*/
int ListLength(StaticLinkList L)
{
int j = 0;
int i = L[MAXSIZE - 1].cur;
while (i)
{
i = L[i].cur;
j++;
}
return j;
}
3、静态链表的删除
删除元素时,需要自己有实现free()功能的函数。
/*将下标为k的空闲结点回收到备用链表*/
void Free_SSL(StaticLinkList space, int k)
{
space[k].cur = space[0].cur; /*把第一个元素的cur赋值给要删除的元素的cur*/
space[0].cur = k; /*把要删除的分量的下标赋值给第一个元素的cur*/
}
接下来假设要移除“甲”,则“乙”变成了第一个有值元素,需要改变最后一个元素的游标;并且原先“甲”的位置空出来了,变成了第一个空闲分量,需要改变第一个元素的cur:
代码实现:
/*删除在L中第i个数据元素*/
Status ListDelete(StaticLinkList L, int i)
{
int j, k;
if (i<1 || i>ListLength(L) + 1)
return ERROR;
k = MAXSIZE - 1;
for (j = 1; j < i - 1; j++)
k = L[k].cur;
j = L[k].cur;
L[k].cur = L[j].cur;
Free_SSL(L, j);
return OK;
}
七、循环链表
将单链表中终端结点的指针由看指针改为指向头结点,使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表(circular linked list)
。循环链表解决了从一个结点出发访问到链表的全部结点。
为了使空链表和非空链表的处理一直,通常设置一个头结点,当然并不是循环链表一定要头结点。循环链表带有头结点的空链表如下图
对于非空链表,
循环链表和单链表的主要差异在于循环的判断条件,原来是判断p->next是否为空。现在则是p->next不等于头结点,则循环未结束。
在单链表中,在有头结点的时候访问第一个结点只需
O
(
1
)
O(1)
O(1)的时间,但是访问最后一个结点需要
O
(
n
)
O(n)
O(n)的时间。在循环链表中可以不使用头指针,而引入尾指针rear
——指向终端结点的指针,在查找终端结点时,就是rear,查找时间为
O
(
1
)
O(1)
O(1);查找开始结点时,就是rear->next->next,查找时间也是
O
(
1
)
O(1)
O(1)。
有了尾指针,在合并两个循环链表时就很简单了,只需要修改尾指针的指向、终端结点的指针,如下图
p=rearA->next; /*保存A表的头结点*/
rearA->next=rearB->next->next; /*将本来指向B表的第一个结点*/
/*赋值给rearA->next,即②*/
q=rearB->next;
rearB->next=p; /*将原A表的头结点赋值给rearB->next,即③*/
free(q) /**释放q/
八、双向链表
1、定义
在单链表中,查找下一结点的时间是
O
(
1
)
O(1)
O(1),但是查找上一结点,最坏的时间复杂度是
O
(
n
)
O(n)
O(n),为了克服这一缺点,设计出来了双向链表
。双向链表(double linked list)是在单链表的每个结点中在设置一个指向其前驱结点的指针域
。所以在双向链表中有两个指针域,一个指向直接前驱,一个指向直接后继。
/*线性表的双向链表存储结构*/
typedef struct DulNode
{
ElemType data;
struct DulNode* prior; /*直接前驱指针*/
struct DulNode* next; /*直接后继指针*/
}DulNode,*DuLinkList;
既然单链表也可以有循环链表,那么双向链表也可以是循环链表。
双向链表的循环带头结点的空链表如下图:
非空的循环带头结点的双向链表:
2、 双向链表的插入操作
双向链表是单链表中扩展出来的结构,所以它的很多操作是和单链表相同的,比如求长度的ListLength、查找元的GetElem、获取元素位置的LocateElem等,这些操作都只涉及一个方向的指针。
在插入操作时,需要改变两个指针变量,现假设存储元素e的结点为s,将结点s插入到结点p和p->next之间,如下图
s-> prior = p; /*把p赋值给s的前驱*/
s-> next = p-> next; /*把p-> next赋值给s的后继*/
p-> next -> prior = s; /*把s赋值给p-> next的前驱*/
p -> next = s; /*把s赋值给p的后继*/
3、双向链表的删除操作
假设需要删除结点p,删除操作如下图:
p -> prior -> next = p -> next; /*把p->next赋值给p->prior的后继*/
p -> next -> prior = p -> prior; /*把p->prior赋值给p->next的前驱*/
free(p); /*释放p结点*/