1.1线性表的定义
线性表:零个或多个数据元素的有限序列。
注:
(1)它是一个序列。元素之间是有顺序的,若元素存在多个,则第一个元素无前驱,最后一个元素无后继,其他元素有且只有一个前驱和后继。
(2)有限。元素个数是有限的。
将线性表记为
由图可以看到,是的直接前驱元素,是的直接后继元素。
线性表元素的个数n(n>=0)定义为线性表的长度,当n=0时,称为空表。
若 是第一个元素,是最后一个,第i个元素是,i叫做线性表中的位序。
在较复杂的线性表中,一个数据元素可以由若干个数据项组成。
1.2线性表的抽象数据类型
线性表的操作:创建和初始化、重置为空表、根据位序得到数据元素、查找、表长、插入删除数据。
抽象数据类型定义:
ADT 线性表(List)
Data
线性表的数据集合为,每个元素类型均为DataType。其中,除了,其他元素都有且仅有一个直接前驱,除了,其他元素都有且仅有一个直接后继。数据元素之间的关系是一对一的关系。
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
**例:**实现线性表集合A和B的并集操作。
思路:把存在在B集合但不存在在A集合的元素插入到A中,循环集合B,判断元素是否存在在A集合中,不存在则插入。
cpp
void union(SqList *La, SqList Lb)
{
int La_len, Lb_len, i;
ElemType e; /*声明与La和Lb相同的数据元素e*/
La_len = ListLength(*La); /*求线性表的长度*/
Lb_len = ListLength(Lb);
for(i = 1; i <= Lb_len; i++)
{
GetElem(Lb, i &e); /*取Lb中第i个数据元素赋给e*/
if(!LocateElem(*La, e)) /*La中不存在和e相同的数据元素*/
ListInsert(La, ++La_len, e); /*插入*/
}
}
注:
++当传递一个参数给函数时,这个参数会不会在函数内被改动决定了使用什么参数形式。++
++如果需要被改动,则需要传递指向这个参数的指针。++
++如果不需要被改动,可以直接传递这个参数。++
1.3线性表的顺序存储结构
1.3.1顺序存储定义
线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。
顺序存储示意图:
1.3.2顺序存储方式
顺序存储结构将一片连续空间占了,将相同数据类型的数据元素依次存放在这块空间。使用一维数组来实现顺序存储结构,即第一个元素存放在下标为0的位置(起始位置),然后依次存放。
这个数组的长度就是顺序存储的最大容量,线性表长度不能超过最大存储容量。
顺序存储结构代码:
cpp
#define MAXSIZE 20 /*存储空间初始分配量*/
typedef int ElemType /*ElemType类型清空而定*/
typedef struct
{
ElemType data[MAXSIZE]; /*数组,存储数据元素*/
int length; /*线性表当前长度*/
}SqList;
描述顺序存储结构需要三个属性:
(1)存储空间起始位置:数组data,它的存储位置就是存储空间的存储位置。
(2)线性表的最大容量:数组长度MAXSIZE。
(3)线性表的当前长度:length。
1.3.3数组长度和线性表长度的区别
数组长度是指存放线性表的存储空间的长度,存储分配后这个量一般是不变的。
线性表长度是指线性表中数据元素的个数,会随着线性表的删除和插入操作变化。
在任意时刻,线性表的长度都应该小于等于数组的长度。
1.3.4地址计算方法
线性表从1开始,数组从0开始,即:线性表的第i个元素存储在数组下标为i-1的位置。
存储器中的每个存储单元都有自己的编号,这个编号叫做地址。
假设每个数据元素占用的是c个存储单元,那么线性表中的第i+1和第i个数据元素的存储位置满足:
时间性能为O(1)这一特点的存储结构称为随机存储结构。
1.4顺序存储结构的插入与删除
1.4.1获得元素操作
将线性表L中的第i个位置元素返回。即:将i的数值在数组下标范围内,把数组第i-1下标位置的值返回。
cpp
#define OK 1
#define ERROR 0
/*Status是函数的类型,其值是函数结果状态代码,如OK等*/
typedef int Status;
/*操作结果:用e返回L中第i个数据元素*/
Status GetElem(SqList L, int i, ElemType *e)
{
if(L.length == 0 || i < i || i > L.length)
return ERROR;
*e = L.data[i - 1];
return OK;
}
1.4.2插入操作
ListInsert(*L,i,e),即在线性表L中的第i个位置插入新元素e。
思路:(1)如果插入位置不合理,抛出异常。
(2)线性表长度大于等于数组长度,抛出异常或动态增加容量。
(3)从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置。
(4)将要插入元素填入位置i处。
(5)表长加1。
cpp
/*初始条件:顺序线性表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 || L -> length + 1) /*i比第1个位置小或比最后一个位置大*/
return ERROR;
if(i <= L -> length)
{
for(k = L -> length - 1; k >= i - 1; k--)
L -> data[k + 1] = L -> data[k];
}
L -> data[i - 1] = e;
L -> length++;
return OK;
}
1.4.3删除操作
思路:(1)如果删除位置不合理,抛出异常
(2)取出删除元素
(3)从删除元素位置开始遍历最后一个元素位置,分别将它们都向前移动一个位置。
(4)表长减1。
cpp
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;
}
插入删除操作时间复杂度:
(1)插入删除操作位于最后一个位置,时间复杂度为O(1),因为不需要移动元素。
(2)对第一个位置元素进行插入删除操作,时间复杂度为O(n),因为要移动所有的元素。
(3)平均情况,,元素插入到第i个位置,删除第i个元素,需要移动n-i个元素。最终平均移动次数和最中间的元素的移动次数相等,为(n-1)/2。时间复杂度为O(n)。
综上:线性表的顺序存储结构在读取数据时,时间复杂度是O(1),插入和删除操作时,时间复杂度是O(n)。
1.4.4线性表顺序存储结构的优缺点
1.5线性表的链式存储结构
1.5.1定义
特点:用一组任意的存储单元线性表的数据元素,存储单元是任意的,可以连续也可以不连续,这些数据元素可以存在内存中未被占用的任意位置。
为了表示数据元素和直接后继数据元素之间的逻辑关系,对于,在存储数据元素本身以外,还需要存储一个指示其直接后继的存储位置。存储数据元素信息的域称为数据域,存储直接后继位置的域叫做指针域。指针域中存储的信息叫做指针或链。这两部分信息组成数据元素存储映像,称为结点。
n个结点链结成一个链表,即为线性表的链式存储结构,每个结点中只包含一个指针域,所以叫单链表。单链表通过每个结点指针域将数据元素按逻辑次序链接在一起。
链表中的第一个结点的存储位置叫做头指针,整个链表的存取就必须从头指针开始进行。链表的最后一个结点的指针为空(NULL或 ^ )。
为了方便操作,在单链表的第一个结点前设一个头节点。数据域可以不存储任何信息,也可以存线性表长度等公共数据,头节点的指针域存储指向第一个结点的指针。
1.5.2头指针与头节点
头指 针
1.是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针。
2.具有标志作用,所以常用头指针冠以链表名字。
3.无论链表是否为空,头指针均不为空。头指针是链表的必要条件。
头结点
1.是为了操作的统一和方便设立,放在第一元素的结点之前,其数据域一般无意义。
2.有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其他结点的操作统一。
3.不是链表必要元素。
1.5.3代码描述
若线性表为"空",则头结点的指针域为空。
cpp
/*线性表的单链表存储结构*/
typedef struct Node
{
ElemType data;
struct Node *next;
}Node;
typedef struct Node *LinkList
结点由存放数据元素的数据域和存放后继结点地址的指针域组成。设p是指向线性表第i个元素的指针,则使用p->data的值表示结点的数据域,p->next表示指针域,p->next指向。如果p -->data=,那么p->next->data=。
1.
1.6单链表的读取
获取第i个元素的数据操作
思路:
(1)声明一个指针p指向链表的第一个结点,初始化j从1开始;
(2)当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
(3)若到链表末尾p为空,则说明第i个结点不存在;
(4)否则查找成功,返回结点p的数据。
cpp
/*操作结果:用e返回L中第i个数据元素的值*/
Status GetElem(LinkList L, int i, ElemType *e)
{
int j;
LinkList p; //声明一个指针p
p = L -> next; //让p指向L的第1个结点
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;
}
1.7单链表的插入和删除
1.7.1单链表的插入
假设存储元素e的结点为s,将s插入到p和p->next中间
将p的后继结点赋值给s的后继,然后将s的值赋给p的后继。
cpp
s -> next = p -> next;
p -> next = s;
二者位置不能互换位置。单链表表头表尾操作相同。
单链表第i个数据插入结点算法思路:
(1)声明一指针p指向链表头结点,初始化j从1开始;
(2)当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
(3)若到链表末尾p为空,则说明第i个结点不存在;
(4)否则查找成功,在系统中生成一个空结点s;
(5)将数据元素e赋值给s->data;
(6)单链表的插入标准语句s->next=p->next; p->next=s;
(7)返回成功。
cpp
//在L中第i个位置之前插入新的数据元素e,L元素加1
Status(LinkList *L, int i, ElemType e)
{
int j;
LinkList p, s;
p = *L;
j = 1;
while(p && j < i) //寻找第i个结点
{
p = p -> next;
++j;
}
if(!p || j > i)
return ERROR; //第i个元素不存在
s = (LinkList)malloc(sizeof(Node)); //生成新结点
s -> data = e;
s -> data = p -> next; //将p的后继结点赋值给s的后继结点
p -> next = s; //将s的值赋值给p的后继
return 0;
}
1.7.2单链表的删除
p -> next = p -> next -> next,用q取代p -> next,即:
cpp
q = p ->next;
p -> next = q -> next;
思路:
(1)声明一指针p指向表头结点,初始化j从1开始;
(2)当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
(3)若到链表末尾p为空,则说明第i个结点不存在;
(4)否则查找成功,将欲删除的结点p->next赋值给q;
(5)单链表删除标准语句p->next=q->next;
(6)将q结点中的数据赋值给e,作为返回;
(7)释放q结点;
(8)返回成功。
cpp
Status(LinkList *L, int i, ElemType e)
{
int j;
LinkList p, q;
p = *L;
j = 1;
while(p ->next && j < i)
{
p = p -> next;
++j;
}
if(!(p -> next) || j > i)
return ERROR;
q = p ->next;
p -> next = q -> next;
*e = q -> data;
free(q);
return OK;
}
对于插入删除数据频繁的操作,单链表的效率优势就越明显。
1.8单链表的整表创建
单链表是一种动态结构,它所占用空间大小和位置是不需要预先分配划分,可以根据系统的情况和实际需求即使生成。
创建单链表的过程就是动态生成链表的过程。即从"空表"的初始状态起,依次建立各元素结点,并逐个插入链表。
思路:
(1)声明一指针p和计数器变量i;
(2)初始化空链表L;
(3)让L的头结点的指针指向NULL,即建立一个带头结点的单链表。
(4)循环:
1.生成一新结点赋值给p
2.随机生成一数字赋值给p的数据域p->data
3.将p插入到头结点与前一新结点之间。
头插法:
cpp
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; //插入到表头
}
}
采用的是插队的方法,始终让新结点在第一的位置。
尾插法:
cpp
void CreateListHead(LinkList *L, int n)
{
LinkList p;
int i;
srand(time(0)); //随机数种子
*L = (LinkList)malloc(sizeof(Node));
r = *L; //r为指向尾部的结点
for(i = 0; i < n; i++)
{
p = (Node*)malloc(sizeof(Node)); //生成新结点
p -> data = rand () % 100 + 1; //随机生成100以内的数字
r -> next = p; //将表尾终端结点的指针指向新结点
r = p; //将当前的新结点定义为表尾终端结点
}
r -> next = NULL; //链表结束
}
将新结点放在最后。L是指整个单链表,r是指向尾结点的变量,r会随循环不断变化的结点,L随着循环增长为一个多结点的链表。
r->next=p的意思是将刚才的表尾终端结点r的指针指向新结点p。
r=p是让r重新称为尾结点。
此时r为尾结点,那么循环结束后,让这个结点的指针域置空,即:r ->next=NULL。
1.9单链表的整表删除
单链表的销毁,思路:
(1)声明一个指针p和q;
(2)将第一个结点赋值给p;
(3)循环:
1.将下一结点赋值给q;
2.释放p;
3.将q赋值给p;
cpp
Status(LinkList *L)
{
LinkList p, q;
int i;
p = (*L) -> next; //p指向第一个结点
while(p) //没到表尾
{
q = p -> next;
free(p);
p = q;
}
(*L) -> next = NULL; //头结点指针域为空
return OK;
}
1.10单链表结构与顺序存储结构的优缺点
分配方式
顺序存储结构用一段连续的存储单元依次存储线性表的数据元素。
单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素。
时间性能
查找
顺序存储:O(1)
单链表:O(n)
插入和删除
顺序存储结构需要平均移动表长的一半,O(n)
单链表找到指针位置后,插入删除时间复杂度仅为O(1)
空间性能
顺序存储结构需要预先分配空间,大了浪费,小了溢出。
单链表不预先分配存储空间,有空就分配,元素个数不受限。
即:若线性表需要频繁查找,不怎么进行插入删除操作,宜使用顺序存储结构;若要频繁进行插入删除操作,不频繁查找,更适合使用单链表。
1.11静态链表
让数组的元素都由两个数据域组成,data和cur。data用来存储数据元素,cur相当于单链表中的next指针。用数组描述的链表叫做静态链表。
cpp
#define MAXSIZE 1000
typedef struct
{
ElemType data;
int cur; //为0时无指向
}Component,StaticList[MAXSIZE]
对数组第一个和最后一个元素作为元素处理,不存数据。把未被使用的数组元素称为备用链表。
cpp
//将一维数组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 0;
}
假设已经将数据存入静态数组,如下图所示:
1.11.1静态链表的插入操作
主要要解决如何用静态模拟动态链表结构的存储空间分配,需要时申请,无用时释放。
动态链表使用malloc()和free(),但是静态链表不存在,需要我们自己实现这两个函数才能插入和删除。
cpp
//若备用空间链表非空,则返回分配的结点下标,否则返回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存的第一个空闲的下标。
实现插入操作:
cpp
Status ListInsert(StaticLinkList L, int i, ElemType e)
{
int j, k, l;
k = MAXSIZE - 1; //k首先是最后一个元素下标
if(i < 1 || i > ListLenght(L) + 1)
return ERROR;
j = Malloc_SSL(L); //获得空闲分量的下标
if(j)
{
L[j].data = e; //将数据赋值给此分量的data
for(l = 1; l <= i -1; l++) //将数据赋值给data
k = L[k].cur;
L[j].cur = L[k].cur; //把第i个元素之间的cur赋值给新的cur
L[k].cur = j; //把新元素的下标赋值给第i个元素之前元素的cur
return OK;
}
return ERROR;
}
1.11.2静态链表的删除操作
cpp
//删除L中的第i个数据元素
Status ListDelete(StaticLinkList L, int i)
{
int j, k;
if(i < 1 || i > ListLength(L))
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;
}
cpp
//将下标为k的空间结点回收到备用链表
void Free_SSL(StaticLinkList space, int k)
{
space[k].cur = space[0].cur; //把第一个元素cur值赋给要删除的分量cur
space[0].cur = k; //把要删除的分量下标赋值给第一个元素的cur
}
这个意思是A走了,然后这个位置空了出来,如果有新的元素进来,最优先考虑这个位置,所以原来的第一个空位分量就是下标为8的分量,让要删除的这个位置成为第一个优先空位。
1.11.3优缺点
优点:在插入和删除时只需要改变游标不需要移动位置,改进了需要移动大量元素的缺点。
缺点:失去随机存储的特性,没有解决表长问题。
1.12循环链表
头尾相接的单链表称为循环链表。
循环链表解决了如何从当中一个结点出发,访问到链表的全部结点。
为使空链表与非空链表处理方式一致,设一个头结点。
空链表:
非空循环链表:
p->next不等于头结点,则循环未结束。
使用指向终端结点的尾指针表示循环链表可以很方便的查找到开始和终端结点。
终端结点用尾指针rear表示,开始结点:rear->next->next
将两个循环链表合并成一个表:
cpp
p = rearA -> next; //保存A表的头结点
rearA -> next = rearB -> next -> next; //将本指向B表的第一个结点赋值给rearA->next
q = rearB -> next;
rearB -> next = p; //将原A表的头结点赋值给rearB -> next
free(q);
1.13双向链表
双向链表是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。所以在双向链表中的结点都有两个指针域,一个指向直接前驱,一个指向直接后继。
cpp
//线性表的双向链表存储结构
typedef struct DulNode
{
ElemType data;
struct DulNode *prior; //直接前驱指针
struct DulNode *next; //直接后继指针
}DulNode, *DuLinkList;
空链表:
非空双向循环链表:
双向链表某个结点p的后继的前驱是它自己。
cpp
p -> next -> prior = p = p -> prior -> next
插入和删除时需要改变两个指针变量
存储元素e的结点s,插入到p和p ->next之间
cpp
s -> prior = p; //把p赋值给s的直接前驱
s -> next = p -> next; //把p->next赋值给s的直接后继
p -> next -> prior = s; //把s赋值给p->next的前驱
p -> next = s; //把s的值赋给p的后继
删除结点p
cpp
p -> prior -> next = p -> next; //把p->next赋给p->prior的后继
p -> next -> prior = p -> prior; //把p->prior赋值给p->next的前驱
free(p);