线性表
线性表(List)
零个或多个数据元素的有限序列。
定义
若将线性表记为\((a_1, …,a_{i-1},a_{i}, a_{i+1}, …, a_n)\),则表中
\(a_{i-1}\)领先于\(a_i\),\(a_i\)领先于\(a_{i+1}\),称\(a_{i-1}\)是\(a_i\)的直接前驱元素,\(a_{i+1}\)是\(a_i\)的直接后继元素。
当i=1,2,…,n-1时,\(a_i\)有且仅有一个直接后继,
当i=2,3,…, n时,\(a_i\)有且仅有一个直接前驱。

所以线性表元素的个数 n(n≥0) 定义为线性表的长度,当n=0时,称为空表。
线性表的顺序存储结构
线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。
线性表\((a_1,a_2,……,a_n)\)的顺序存储示意图如下:

C语言实现顺序存储的结构
#define MAXSIZE 20 /*存储空间初始分配量*/
typedef int ElemType; /*ElemType类型根据实际情况而定,这里假设为int*/
typedef struct
{
ElemType data [MAXSIZE]; /*数组存储数据元素,最大值为MAXSIZE*/
int length; /*线性表当前长度*/
}SqList;
这里,我们描述顺序存储结构需要三个属性:
- 存储空间的起始位置:数组 data,它的存储位置就是存储空间的存储位置。
- 线性表的最大存储容量:数组长度 MaxSize。
- 线性表的当前长度:length。
数组的长度是存放线性表的存储空间的长度,存储分配后这个量是一般是不变
线性表的长度是线性表中数据元素的个数,随着线性表插入和删除操作的进行,这个量是变化的
在任意时刻,线性表的长度应该小于等于数组的长度。
获取元素操作
#define OK 1
#define ERROR O
#define TRUE 1
#define FALSE 0
typedef int Status;
/*Status是函数的类型,其值是函数结果状态代码,如OK等*/
/*初始条件:顺序线性表 L 已存在,1≤i≤ListLength(L)*/
/*操作结果:用e返回 L 中第i个数据元素的值*/
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;
}
插入元素操作
举个例子,本来我们在春运时去买火车票,大家都排队排的好好的。这时来了一个美女,对着队伍中排在第三位的你说,“大哥,求求你帮帮忙,我家母亲有病,我得急着回去看她,这队伍这么长,你可否让我排在你的前面?”你心一软,就同意了。这时,你必须得退后一步,否则她是没法进到队伍来的。这可不得了,后面的人像恪虫一样,全部都得退一步。骂起四声。但后面的人也不清楚这加塞是怎么回事,没什么办法。

插入算法的思路:
- 如果插入位置不合理,抛出异常;
- 如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;
- 从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置;
- 将要插入元素填入位置i处;
- 表长加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) /*当主不在范围内时*/
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;
}
删除元素操作
接着刚才的例子。此时后面排队的人群意见都很大,都说怎么可以这样,不管什么原因,插队就是不行,有本事,找火车站开后门去。就在这时,远处跑来一胖子,对着这美女喊,可找到你了,你这骗子,还我钱。只见这女子二话不说,突然就冲出了队伍,胖子追在其后,消失在人群中。哦,原来她是倒卖火车票的黄牛,刚才还装可怜。于是排队的人群,又像肽虫一样,均向前移动了一步,骂声渐息,队伍又恢复了平静。

/*初始条件:顺序线性表 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;
}
总结应用
线性表顺序存储结构的优缺点:
- 优点
无须为表示表中元素之间的逻辑关系而增加额外的存储空间
可以快速地存取表中任一位置的元素 - 缺点
插入和删除操作需要移动大量元素
当线性表长度变化较大时,难以确定存储空间的容量
造成存储空间的“碎片”
线性表的顺序存储结构,在存、读数据时,不管是哪个位置,时间复杂度都是O(1);
而插入或删除时,时间复杂度都是O(n)。
这就说明,它比较适合元素个数不太变化,而更多是存取数据的应用。
线性表的链式存储结构
线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。
这就意味着,这些数据元素可以存在内存未被占用的任意位置。

为了表示每个数据元素 \(a_i\) 与其直接后继数据元素 \(a_{i+1}\) 之间的逻辑关系,对数据元素\(a_i\)来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称做指针或链。这两部分信息组成数据元素 \(a_i\) 的存储映像,称为结点(Node)。

对于线性表来说,总得有个头有个尾,链表也不例外。我们把链表中第一个结点的存储位置叫头指针。
那么整个链表的存取就必须是从头指针开始进行了。
- 头指针
头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针
头指针具有标识作用,所以常用头指针冠以链表的名字
无论链表是否为空,头指针均不为空。头指针是链表的必要元素

- 头结点
头结点是为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义(也可存放链表的长度)
有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其它结点的操作就统一了
头结点不一定是链表必须要素

C语言实现链式存储的结构
/*线性表的单链表存储结构*/
typedef struct Node
{
ElemType data;
struct Node *next;
}Node;
typedef struct Node *LinkList;/*定义 LinkList*/
结点 \(a_i\) 的数据域我们可以用 p->data 来表示,p->data 的值是一个数据元素,
结点 \(a_i\) 的指针域可以用 p->next 来表示,p->next 的值是一个指针。p->next 指向谁呢?当然是指向第i+1个元素,即指向 \(a_{i+1}\) 的指针。

获取元素操作
在线性表的顺序存储结构中,我们要计算任意一个元素的存储位置是很容易的。
但在单链表中,由于第i个元素到底在哪?没办法一开始就知道,必须得从头开始找。
获得链表第i个数据的算法思路:
1.声明一个结点 p 指向链表第一个结点,初始化j从1开始;
2.当j < i时,就遍历链表,让 p 的指针向后移动,不断指向下一结点,j 累加 1;
3.若到链表末尾 p 为空,则说明第 i 个元素不存在;
4.否则查找成功,返回结点 p 的数据。
实现代码算法如下:
/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*/
/*操作结果:用e返回L中第i个数据元素的值*/
Status GetElem( LinkList L, int i, ElemType *e)
{
int j;
LinkList p; /*声明一结点p*1*/
p = L->next; /*让p指向链表工的第一个结点*/
j = 1; /*为计数器*/
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;
}
其主要核心思想就是“工作指针后移”,最坏情况的时间复杂度是O(n)。
插入元素操作

只需要 s->next = p->next; p->next=s;
解读这两句代码,也就是说先让 p 的后继结点改成 s 的后继结点(赋值给 s 的指针域),再把结点 s 变成 p 的后继结点,注意顺序不能换!

想象下猴子捞月,如果猴子不够用,需要把你加进去,那是不是得先抓住下面的猴子,然后再叫原来抓住这只猴子的猴子抓住你。
如果那只猴子一开始就去抓你,而你又没有一开始抓住下面的猴子,那不完犊子了吗?

单链表第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.返回成功。
实现代码算法如下:
/*初始条件:顺序线性表 L 已存在,1<i≤ListLength(L),*/
/*操作结果:在 L 中第 i 个位置之前插入新的数据元素e,L 的长度加1*/
Status ListInsert (LinkList *L, int i, ElemType e)
{
int j;
LinkList p, s;
p = *L;
j = 1;
while (p && j<i) /*寻找第主个结点*/
{
p = p->next;
++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;
}
删除元素操作
现在我们再来看单链表的删除。设存储元素 \(a_i\) 的结点为q,要实现将结点q删除单链表的操作,其实就是将它的前继结点的指针绕过,指向它的后继结点即可

我们所要做的,实际上就是一步,p->next = p->next->next,
或者 q = p->next; p->next = q->next;
单链表第i个数据删除结点的算法思路:
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.返回成功。
实现代码算法如下:
/*初始条件:顺序线性表 L 已存在,1≤i≤ListLength(L)*/
/*操作结果:删除 L 的第 i 个数据元素,并用 e 返回其值,L 的长度减1*/
Status ListDelete (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; /*第i个元素不存在*/
q = p->next;
p->next = q->next; /*将q的后继赋值给p的后继*/
*e = q->data; /*将q结点中的数据给e*/
free(a); /*让系统回收此结点,释放内存*/
return OK;
}

浙公网安备 33010602011771号