第三章 线性表

1.线性表的定义

线性表:零个或多个数据元素的有限序列。

它是一个序列。如果有多个元素,则第一个元素无前驱,最后一个元素无后继,其他的每一个元素只有一个前驱和后继。

将线性表记为 a1,a2,a3,则a1是a2的直接前驱元素,a3是a2的直接后继元素。有且仅仅只有一个直接前驱和一个直接后继。

所以线性表元素的个数n(n≥0)定义为线性表的长度,当n=0,为空表

在非空表中的每个元素都有一个确定的位置,比如a1是第一个,a3是最后一个。a2是第二个,称2为数据元素a2在线性表中的位序

2.线性表的抽象数据类型

当传递一个参数给函数的时候,这个参数会不会在函数内被改动决定了使用什么参数形式

如果需要被改动,则需要传递指向这个参数的指针

如果不用被改动可以直接传递这个参数

总结:需要改动则传递参数的指针,不需要则传递这个参数

3.线性表的顺序存储结构

顺序存储结构

线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。

说白了就是:在内存中,占个地方,通过占位的形式,将一定的内存空间给占,然后把相同数据类型的数据元素一次存放在这里。

使用一维数组来实现顺序存储结构

https://cdn.jsdelivr.net/gh/weixuna/MyPic/24/9/image_07d1e446d8b116cfd15553faabca4bdd.png

数据长度与线性表长度的区别

数组的长度是存放线性表的存储空间长度。

线性表的长度是线性表中数据元素的个数,会变化。

地址计算方法

C语言中的数组是从0开始为第一个下标。所以线性表的第i个元素是要存储在数组下标为i-1的位置。

存储器中的每个存储单元都有自己的编号,这个编号为地址。

假设占用的是c个存储单元,那么线性表中第i+1个数据元素的存储位置和第i个数据元素的存储位置满足以下关系(LOC表示获得存储位置的函数)

https://cdn.jsdelivr.net/gh/weixuna/MyPic/24/9/image_55de4f7918d6dc449ca1f46790bc4d9c.png

例如:如果LOC(1)为第一个元素的地址,每个元素占用4个存储单元,即C=4,那么第二个元素LOC(2),就是LOC(1)+4

所以对于第i个数据元素ai的存储位置可以得出ai:

https://cdn.jsdelivr.net/gh/weixuna/MyPic/24/9/image_a4d51b982d1eb08b5a90df01964d3a29.png

https://cdn.jsdelivr.net/gh/weixuna/MyPic/24/9/image_174ffdb4600e63fb2766a24e75af8551.png

4.顺序存储结构的插入与删除

获得元素操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 定义两个宏,分别表示函数执行成功和失败的状态
#define OK 1
#define ERROR 0

// 定义一个新的类型名Status,它实际上就是int类型
typedef int Status;

// 函数声明:从顺序表L中获取第i个位置的元素,并将其赋值给变量e
// SqList L:顺序表结构体
// int i:表示获取元素的位置,也就是索引
// ElemType *e:这是一个指针,用于存放获取到的元素。
Status GetElem(SqList L, int i, ElemType *e) {
// 检查i是否在合法范围内,即i大于0且小于等于顺序表的长度
if (i < 1 || i > L.length) {
// 如果i不合法,返回错误状态ERROR
return ERROR;
}
// 如果i合法,将第i个位置的元素赋值给e指向的变量
// 注意:由于数组索引是从0开始的,所以第i个元素实际上是L.data[i-1]
*e = L.data[i - 1];
// 返回成功状态OK
return OK;
}

插入操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 函数声明,用于在顺序表L中第i个位置插入元素e,并返回操作状态
Status ListInsert(SqList *L, int i, ElemType e) {
int k;

// 检查顺序表是否已满,如果已满,返回错误状态
if (L->length == MAXSIZE) return ERROR;

// 检查插入位置i是否合法,如果不合法(小于1或大于顺序表长度+1),返回错误状态
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];
}
}

// 将新元素e插入到位置i-1(因为数组索引从0开始)
L->data[i - 1] = e;

// 顺序表长度增加1
L->length++;

// 插入成功,返回成功状态
return OK;
}

删除操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 函数声明,从顺序表L中删除第i个位置的元素,并通过e返回该元素的值,函数返回操作状态
Status ListDelete(SqList *L, int i, ElemType *e){
int k;

// 检查顺序表是否为空,如果为空,返回错误状态
if(L->length == 0) return ERROR;

// 检查删除位置i是否正确,即i是否在1到L->length的范围内,如果不正确,返回错误状态
if(i < 1 || i > L->length) return ERROR;

// 将要删除的元素赋值给*e,即通过指针e返回被删除元素的值
*e = L->data[i-1];

// 如果删除的位置不是最后一个元素,则需要将后续元素前移
if(i < L->length){
for(k = i; k < L->length; k++){
// 将位置k的元素前移到位置k-1
L->data[k-1] = L->data[k];
}
}

// 顺序表的长度减1,因为已经删除了一个元素
L->length--;

// 如果删除操作成功,返回OK
return OK;
}


线性表顺序存储结构的优缺点

https://cdn.jsdelivr.net/gh/weixuna/MyPic/24/9/image_13333133983167bb4cd71cd15bea7d30.png

5.线性表的链式存储结构

顺序存储结构不足的解决办法

缺点:插入和删除需要移动大量元素

线性表链式存储结构定义

特点:用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续,也可以不是连续。

存储数据元素信息的称为数据域,存储直接后继位置的域称为指针域。

指针域存储的信息称为指针或链。这两部分信息组成数据元素ai的存储映像,为结点

n个结点(ai的存储映像)链结成一个链表,即为线性表的链式存储结构,因此链表的每个节点只包含每个指针域,称为单链表。

单链表:通过每个节点的指针将线性表的数据元素按照逻辑次序链接在一起。

https://cdn.jsdelivr.net/gh/weixuna/MyPic/24/9/image_8ca06caf17426ccd753056ee8b9bfee0.png

链表中第一个节点的存储位置叫做头指针,所以整个链表的存储从头指针开始。之后的每一个节点,就是上一个后继指针指向的位置。最后一个,直接后继不存储,所以线性链表的最后一个节点指针为“NULL”。

https://cdn.jsdelivr.net/gh/weixuna/MyPic/24/9/image_c05973f5263e0dceadf26947138c10f6.png

为了方便会在单链表的第一个节点前设一个头节点。

https://cdn.jsdelivr.net/gh/weixuna/MyPic/24/9/image_8542d2fedc1ee0879e88a3da161e296c.png

头指针与头节点的区别

https://cdn.jsdelivr.net/gh/weixuna/MyPic/24/9/image_1228d149ebd3d54fe7118927040fdcd3.png

线性表链式存储结构代码描述

若线性表为空,则头节点的指针域为空

https://cdn.jsdelivr.net/gh/weixuna/MyPic/24/9/image_348a891425f2846276f25d45223f98fb.png

https://cdn.jsdelivr.net/gh/weixuna/MyPic/24/9/image_5e4a480fa5b3f39ebeb1a6c0dc6c6bef.png

带有头节点的单链表,如下

https://cdn.jsdelivr.net/gh/weixuna/MyPic/24/9/image_544182ab322fecd18a3a63c7c0bdab3d.png

空链表如下

https://cdn.jsdelivr.net/gh/weixuna/MyPic/24/9/image_2a20349a12feb1221aba8588734c7b25.png

1
2
3
4
5
6
7
// 定义链表节点结构体
typedef struct None {
int data; // 数据域
struct None *next; // 指向下一个节点的指针
} Node; // 使用 Node 作为别名

typedef struct None *Link; // 定义指向 Node 的指针类型 Link

从代码中来看:

结点由存放数据元素的数据域,存放后继节点地址的指针域组成。

https://cdn.jsdelivr.net/gh/weixuna/MyPic/24/9/image_9554015f67b9e4719bb4916ffbe4ac12.png

单链表的读取

算法思路:

(1)声明一个指针p指向链表的第一个结点,初始化j从1开始。

(2)当j<i时,遍历链表,让p的指针向后移动,不断指向下一结点,j累加i。

(3)若到链表末尾p为空,则说明第i个结点不存。

(4)否则查找成功,返回结点p的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static GetElem(LinkList L, int i, ElemType *e)
{
int j; //声明结点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;
}
// ||:一个为真,则都为真 &&:要两个都为真

单链表的插入与删除

插入算法思路

(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)返回成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static GetElem(LinkList L, int i, ElemType *e)
{
int j;
LinkList p = L->next; // 假设 L 是头结点,p 指向第一个数据结点
j = 1; // 初始化计数器,从第 1 个结点开始
while (p && j < i) // 遍历链表,直到找到第 i 个结点
{
p = p->next;
j++;
}
if (!p || j > i) // 如果链表为空或者 j 超过了 i,返回错误
{
return ERROR;
}

// 插入新结点
LinkList s = (LinkList)malloc(sizeof(Node)); // 为新结点分配内存
s->data = e;
s->next = p->next; // 将p的后继结点赋值给s的后继
p->next = s; // 将s赋值给p的后继

return OK;
}

删除算法思路

(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)返回成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static GetElem(LinkList L, int i, ElemType *e)
{
int j;
p = L->next; // 初始化 p 指针,指向链表的第一个节点
j = 1; // 从第 1 个节点开始计数
while (p && j < i) // 遍历链表,找到第 i 个节点的前一个节点
{
p = p->next;
j++;
}
if (!p || j > i) // 如果链表长度不足 i,或者 j 超过 i,返回错误
{
return ERROR;
}
q = p->next; // 找到要删除的节点 q(即第 i 个节点)
p->next = q->next; // 让 p 的后继指针跳过 q,指向 q 的后继节点
*e = q->data; // 将要删除的节点 q 的数据保存到 e 中
free(q); // 释放被删除节点 q 所占的内存
return OK; // 返回成功
}


单链表的整表创建

(1)声明一个指针p和计算器变量i。

(2)初始化一空链表L。

(3)让L的头结点的指针指向NULL,建立一个带头结点的单链表。

(4)循环。

1、生成一个新节点赋值给p。

2、随机生成一个数字赋值给p的数据域p->data。

3、将p插入到头结点与前一新结点之间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static GetElem(LinkList L, int i, ElemType *e)
{
LinkList p; // 定义一个指针 p 用于遍历链表
int i; // 定义计数器 i(注意:与外部函数参数 i 冲突)
srand(time(0)); // 初始化随机数种子,确保每次运行时生成不同的随机数

*L = (LinkList)malloc(sizeof(LNode)); // 为头节点分配内存
(*L)->next = NULL; // 将头节点的后继指针设为 NULL,表示链表初始化为空

for (i = 0; i < n; i++) // 循环生成 n 个节点
{
p = (LinkList)malloc(sizeof(LNode)); // 为新节点 p 分配内存
p->data = rand() % 100 + 1; // 生成 1 到 100 之间的随机数,并赋给 p->data
p->next = (*L)->next; // 将当前链表的第一个节点赋值为 p 的后继
(*L)->next = p; // 将 p 插入到头节点之后
}
}

循环链表

循环链表和单链表的差异是p->netx是判断是否为空,循环链表的判断是p->next不等于头结点。

https://cdn.jsdelivr.net/gh/weixuna/MyPic/24/9/image_71a70083f1532923691f0a5bc12253a1.png

1
2
3
4
5
6
P = rearA -> next;          //保存A表的头结点,1点
rearA -> next = rearB -> next -> next; //将本是指向B表的第一个结点(不是头结点)

q = rearB -> next;
rearB -> next = p; //赋值给rearA->next,2点
free(q); //释放q

双向链表

双向链表:是在单链表的每个节点中,在设置一个指向前驱结点的指针域。

1
2
3
4
5
6
typedef struct DulNode
{
ElemType data;
struct DuLNode *prior; //直接前驱指针
struct DuLNone *next; //直接后继指针
}DulNode,*DuLinkList;

总结

https://cdn.jsdelivr.net/gh/weixuna/MyPic/24/9/image_a1421df87866f8fe3001afdcd3585367.png

https://cdn.jsdelivr.net/gh/weixuna/MyPic/24/9/image_75d63b25107a5df72426d4e9bc78a72f.png

https://cdn.jsdelivr.net/gh/weixuna/MyPic/24/9/image_f9bc45f80a5d7cda3a0012e9c2f44f9f.png