目录
一、🚀 线性表
1.🚢 线性表的
线性表是具有相同特性数据元素的一个有限序列,数据元素有 n 个,即表长为 n。n = 零时,为空表。L 为表名。 被称为表头元素 , 被称为表尾元素 。 表头元素没有直接前驱,但有直接后继 ;表尾元素没有直接后继 , 但有直接前驱。除了表头与表尾元素,其于的都有且只有一个直接前驱和一个直接后继。
L = ( , , , ... , , )
以 为例
直接前驱: 就是它的直接前驱。
直接后继: 就是它的直接后继。
前驱元素: 前面的所有元素都是 的前驱元素,所以 不仅是 的前驱元素,同时也是 的直接前驱。
后继元素: 后面的所有元素都是 的后继元素,所以 不仅是 的后继元素,同时也是 的直接后继。
2.🚢 线性表的基本操作
① 创建一个线性表
② 获取线性表的长度
③ 在线性表中插入一个节点
④ 在线性表的指定位置插入一个节点
⑤ 删除线性表一个节点
⑥ 查询线性表一个节点
⑦ 获取线性表是否为空
3.🚢 线性表的存储结构
线性表的存储结构分为线性存储结构和链式存储结构,线性存储结构称为顺序表,链式存储结构称为链表。链表又分为单链表、双链表、循环链表、静态链表。
二、🚀 顺序表
1.🚔 顺序表的定义
线性表的顺序存储结构称为顺序表,之所以是顺序存储,是因为其逻辑地址上相邻的元素在物理地址上也相邻。由于其物理地址与逻辑地址相对应,所以每个元素上只用存储元素的值,查找元素可以通过索引*数据占用内存空间大小计算得到。
可以用数组来表示。int numList[] = {1, 2, 3};

由于 int 类型一般情况下占用内存为 4 字节,在内存地址中它们的分配空间是相临的,所以上面的 3 个物理地址(16 进制)每个相差为 4。
#include <stdio.h>
int main(void) {
int numList[] = {1, 2, 3};
// sizeof 是一个运算符,它用于计算其操作数的大小(以字节为单位)
//%zu是size_t类型的正确格式说明符,它用于sizeof运算符的结果
printf("%zu\n", sizeof(int)); // 4
//%p格式说明符用于打印指针的值,也就是内存地址
// printf函数期望对应的参数是void *类型。这是因为void
// *是一个特殊的指针类型,可以指向任何数据类型的对象,而不会丢失信息或引入类型强制
printf("%p", (void *)&numList[0]); // 0x7ff7b71d132c
printf("%p", (void *)&numList[1]); // 0x7ff7b71d1330
printf("%p", (void *)&numList[2]); // 0x7ff7b71d1334
return 0;
}
结构体类型会将其中的成员占用内存相加
typedef struct {
int age;
char *name;
} Person;
printf("Person=%zu\n", sizeof(Person)); // Person=8
2.🚔 顺序表的基本实现——静态分配
静态分配的顺序表只适用于知道数据量的情况下,且数据量不会变的情况下。否则,如果数据量变少,则多余存储空间浪费,数量量变多,则空间不够用,会导致程序崩溃。
#define SIZE 40 // 宏定义 最大长度
typedef struct {
int data[SIZE]; // 整型数组
int Length; // 列表的长度,用于跟踪列表的成员数量
} List;
初始化静态分配的顺序表
// 初始化静态分配的顺序表
void InitList(List *list)
{
// 不需要为 data 分配内存,因为它是 List 结构体的一部分
list->Length = 0;
}
int main(void)
{
List myList;
InitList(&myList);
return 0;
}
3.🚔 顺序表的基本实现——动态分配
动态分配是在程序运行过程中,通过动态存储分配语句分配的。当数据存储空间被占满时,将重新分配一个更大的空间,用于替换原来的空间(注意一下:这里是替换,而不是在原有基础上扩充)
初始化动态分配的顺序表
#include <stdlib.h>
#define INITSIZE 10
typedef struct {
int *data; // 动态数据
int Length; // 顺序表的长度(数据量)
int MaxSize; // 顺序表的最大容量
} List;
// 初始化顺序表
void InitList(List *list) {
// 将分配的内存地址赋值给 list->data
list->data = (int *)malloc(INITSIZE * sizeof(int));
// 检查内存分配是否成功
if (list->data == NULL) {
exit(EXIT_FAILURE);
}
list->Length = 0; // 初始化顺序表的长度
list->MaxSize = INITSIZE; // 安化顺序表的最大容量
}
4.🚔 顺序表的操作
注意:以下操作均以 int 值的顺序表为例,如果为 结构体类型,则需要比对里面的成员
4.1💨 插入操作
在顺序表的第 i 个位置插入元素,要保存 i 为顺序表的有效位置 。将 i 的后继元素整体向后移动一个位置,在空出来的位置插入新元素。随后将 Length 加 1 。操作成功返回 true,否则返回 false。
时间复杂度:
#include <stdbool.h>
#include <stdlib.h>
#define INITSIZE 10
typedef struct {
int *data; // 动态数据
int Length; // 顺序表的长度(数据量)
int MaxSize; // 顺序表的最大容量
} List;
/** 插入操作 */
bool InsertList(List *list, int i, int element) {
// 如果 i 的值 不在顺序表的范围内,则操作失败
if (i < 0 || i > list->Length) return false;
// 如果长度不小于最大容量,则插入一条数据后,必然溢出,操作失败
if (list->Length >= list->MaxSize) return false;
// 逆向循环,直到i的位置
for (int j = list->Length; j >= i; j--) {
// 依次将数据向后移动一格
list->data[j] = list->data[j - 1];
}
// 插入位置是索引,所以要-1
list->data[i - 1] = element;
// 长度+1
list->Length++;
return true;
}
4.2💨 删除操作
将下标为 i 索引位置删除,并将其后继元素向前移动一个索引。操作成功返回 true,否则返回 false。成功后需要将被删除的元素赋值给 element 。
时间复杂度:
/** 删除操作 */
bool DeleteList(List *list, int i, int *element) {
// 如果 i 的值 不在顺序表的范围内,则操作失败
if (i < 0 || i >= list->Length) return false;
// 将被删除的元素赋值给 element
element = list->data[i - 1];
// 从i的位置开始,将后继元素向前移动一格
for (int j = i - 1; j < list->Length; j++) {
list->data[j] = list->data[j + 1];
}
// 长度-1
list->Length--;
return true;
}
4.3💨 查找操作
由于按位查找,只需直接读就可以,就不上代码了,以下是按值查找。
上顺序表中按给定值查找对应元素,找到返回其位序,否则返回-1。
时间复杂度:
#define INITSIZE 8 // 初始化顺序表容量
typedef struct {
int *data; // 动态数据
int MaxSize; // 最大容量
int Length; // 长度(数据量)
} List;
/** 查找操作 */
int FindList(List *list, int element) {
for (int i = 0; i < list->Length; i++) {
// 找到元素,返回其位序
if (element == list->data[i]) {
return i + 1;
}
}
// 查找失败,返回-1
return -1;
}
4.4💨 逆置操作
将顺序表的的元素逆序排列。
分两种情况:顺序表元素有奇数个、顺序表元素有偶数个.。
操作情况:将第一个元素和倒数第一个元素调换位置、将第二个元素和倒数第二个元素调换位置...
依次进行。奇数个元素,中间的不需要调换,偶数个元素,两两调换即可。
奇数个(中间的不需要换)

偶数个(两两调换)

#define INITSIZE 10
typedef struct {
int *data; // 动态数据
int Length; // 顺序表的长度(数据量)
int MaxSize; // 顺序表的最大容量
} List;
/** 逆置操作 */
void ReverseList(List *list) {
int val;
// 只需循环一半
for (int i = 0; i < list->Length / 2; i++) {
// 保存当前索引的值
val = list->data[i];
// lastIdx 为 与 i 对应的索引
int lastIdx = list->Length - i - 1;
// 将 lastIdx 位置的值 赋值到 i 的位置上
list->data[i] = list->data[lastIdx];
// 将 i 位置的值 赋值到 lastIdx 的位置上
list->data[lastIdx] = val;
}
}
三、🚀 链表
1.🚔 单链表定义
线性表的链式存储又称单链表,和顺序表不同的是其逻辑地址相邻的元素在物理地址上并不相邻。由于顺序表总要占用一块连续的内存空间,导致很多零散的内存无法被使用。链表中的元素都是单独存储的,存储在内存中也是分散的。这就需要在每个结点上多存储一个信息,用来存放下一个节点的物理地址。
单链表的查找方式比较费时,它不能通过下标直接定位到对应的元素,而是需要从表头依次对比,直到找到对应的元素。

单链表还分为带头节点和不带头节点
相同点:头指针始终指向链表的第一个节点,头指针为 NULL 时,表示是一个空链表。
不同点:不带头节点的第一个节点为真实节点,其中存储着需要的数据。带头节点的头节点一般只存储指向下一个节点的指针,而不存储数据。
2.🚔 单链表的操作
// 定义单链表节点结构体类型
typedef struct LNode {
int data; //数据域
struct LNode* next; //指针域(指向下一个节点的指针)
}* LinkList;
2.1💨 按位序插入操作——带头结节
判断插入的位置是否合理。
创建链表指针和指针在链表所在的位置。
位置指针,并判断其位置是否是要插入的位置,如果是则进入下一步,否则移动指针至下一个节点。
创建新节点,为其赋值,并将指针指向原位置的下一个节点(a2)。
将原位置(a1)的指针指向新节点。

// 定义单链表节点结构体类型
typedef struct LNode {
int data; // 数据域
struct LNode* next; // 指针域(指向下一个节点的指针)
} LNode, *LinkList;
/** 按位序插入 */
bool InsertList(LinkList list, int i, int e) {
// 插入位置超过最小,返回false
if (i < 1) {
return false;
}
LNode* p = list; // 链表的指针,移动指向每个节点
int j = 0; // 当前指针所在链表的位序
// 指向的节点的位序不是 i,则进入循环
while (p != NULL && j < i - 1) {
p = p->next; // 指针移向下一位
j++;
}
// 插入位置超过最大,值为空,则返回false
if (p == NULL) {
return false;
}
// 创建新节点并分配内存
LNode* t = (LNode*)malloc(sizeof(LNode));
t->data = e; // 为新节点赋值
t->next = p->next; // 将新节点的指针指向原位序的下一位
p->next = t; // 将新节点插入该位序
return true;
}
2.2💨 按位序插入操作——不带头结点
1、 判断插入的位置是否合理。
2、判断是否是在第一个节点插入,是则进行 5、6 步骤。
3、创建链表指针和指针在链表所在的位置。
4、位置指针,并判断其位置是否是要插入的位置,如果是则进入下一步,否则移动指针至下一个节点。
5、创建新节点,为其赋值,并将指针指向原位置的下一个节点(a2)。
6、将原位置(a1)的指针指向新节点。

// 定义单链表节点结构体类型
typedef struct LNode {
int data; // 数据域
struct LNode* next; // 指针域(指向下一个节点的指针)
} LNode, *LinkList;
/** 按位序插入(不带头节点) */
bool InsertListNoHead(LinkList list, int i, int e) {
// 插入位置超过最小,返回false
if (i < 1) {
return false;
}
// 在第一个位置插入
if (i == 1) {
// 创建新节点并分配内存
LNode* t = (LNode*)malloc(sizeof(LNode));
t->data = e;
t->next = list;
list = t; // 头指针指向新节点
return true;
}
LNode* p =
list; // 链表的指针,移动指向每个节点,此时p指向的是第一个节点,不是头节点
int j = 1; // 当前指针所在链表的位序
// 指向的节点的位序不是 i,则进入循环
while (p != NULL && j < i - 1) {
p = p->next; // 指针移向下一位
j++;
}
// 插入位置超过最大,值为空,则返回false
if (p == NULL) {
return false;
}
// 创建新节点并分配内存
LNode* t = (LNode*)malloc(sizeof(LNode));
t->data = e; // 为新节点赋值
t->next = p->next; // 将新节点的指针指向原位序的下一位
p->next = t; // 将新节点插入该位序
return true;
}
2.3💨 指定位置插入操作——头插
1、创建一个节点 s ,将 s 的指针指向 链表开始节点的物理地址。
2、将链表的开始节点的指针指向 新节点 s。
3、将开始节点的数据 赋值给 节点 s。
4、将 要插入的元素 赋值给 链表的开始节点。
此时,开始节点存储的是新数据,而原先开始节点的数据已经到第二个位置了。
时间复杂度:
// 定义单链表节点结构体类型
typedef struct LNode {
int data; // 数据域
struct LNode* next; // 指针域(指向下一个节点的指针)
} LNode, *LinkList;
/** 插入操作(头插法) */
bool HeadInsertList(LinkList list, int e) {
if (list == NULL) {
return false;
}
// 创建节点,并分配内存
LNode* s = (LNode*)malloc(sizeof(LNode));
if (s == NULL) {
// 内存分配失败
return false;
}
// 将新数据 s 的指针,指向第一个节点的指针,也就是第二个节点的物理地址
s->next = list->next;
// 将第一个节点的指针指向 s ,s为第二个节点,s指针将指向第三个节点
list->next = s;
// 将 原第一个节点 list 的数据 保存到 s 节点上
s->data = list->data;
// 将 e 赋值给开始结点,已将 e 节点插入完成,
// list为第一个节点,存储着数据 e,而s节点为第二个节点,存储着原 list 的数据
list->data = e;
return true;
}
2.4💨 指定位置插入操作——尾插
1、创建新节点。
2、创建链表指针,并移动到最后一个节点。
3、将新节点赋值为新的数据。
4、将链表指针指向新节点。
5、此时新节点为表尾元素。
// 定义单链表节点结构体类型
typedef struct LNode {
int data; // 数据域
struct LNode* next; // 指针域(指向下一个节点的指针)
} LNode, *LinkList;
/** 插入操作(尾插法) */
bool TailInsertList(LinkList list, int e) {
// 创建节点,并分配内存
LNode* r = (LNode*)malloc(sizeof(LNode));
if (r == NULL) {
// 内存分配失败
return false;
}
LNode* p = list->next;
// 移动指针至最后一个节点
while (p->next != NULL) p = p->next;
// 此时p为最后一个节点
// 为新节点赋值
r->data = e;
r->next = NULL;
// r为最后一个节点,p为倒数第二个
p->next = r;
return true;
}
2.5💨 查找操作(按值)
先创建一个指针,向链表后移动指针,对比指向的节点是否为要查找的节点,是则返回,否则继续移动指针。如果没有找到则返回 NULL。
时间复杂度:
// 定义单链表节点结构体类型
typedef struct LNode {
int data; // 数据域
struct LNode* next; // 指针域(指向下一个节点的指针)
} LNode, *LinkList;
// 查找操作
LNode* FindList(LinkList list, int element) {
// 创建节点指针
LNode* p = list->next;
// 若不是查找的节点,则移动指针到下一个节点
while (p != NULL && p->data != element) {
// 如果最后没有找到该节点,则下面的语句依然会再执行一遍
// 此时p->next 为 NULL,则赋值后,p为NULL
p = p->next;
}
// 找到了,则p为该节点,否则p为NULL
return p;
}
2.6💨 查找操作(按位序)
// 定义单链表节点结构体类型
typedef struct LNode {
int data; // 数据域
struct LNode* next; // 指针域(指向下一个节点的指针)
} LNode, *LinkList;
/** 查找操作(按位序) */
LNode* FindIdxList(LinkList list, int i) {
if (i <= 0) return NULL; // 查找的位序不合理,返回NULL
LNode* p = list->next; // 创建指针
int j = 1; // j 为位序
while (j < i && p != NULL) {
p = p->next; // 向后移动指针
j++; // 移动 j,使其与 i 相等
}
return p; // 退出循环,要么找到节点,返回节点。要么没找到 p 为 NULL
}
2.7💨 删除操作(按位序)
1、通过位序找到节点 p。
2、创建新节点,并将其指向 p 的下一个节点(用于临时保存数据)。
3、将 p 的下一个节点的信息(数据和指针)赋值到新节点。
4、释放新节点内存。
// 定义单链表节点结构体类型
typedef struct LNode {
int data; // 数据域
struct LNode* next; // 指针域(指向下一个节点的指针)
} LNode, *LinkList;
/** 删除操作 */
bool DeleteList(LinkList list, int i) {
// 找到该位序的节点
LNode* p = FindIdxList(list, i);
// 节点不存在,返回false
if (p == NULL) return false;
// 创建新节点,为要删除节点的下一个节点
LNode* s = p->next;
// 将要删除节点的下一个节点数据 赋值给p,相当于覆盖了p的数据(删除)
p->data = s->data;
p->next = s->next;
// 释放内存
free(s);
return true;
}
3.⚽ 双链表
3.1 💨 单链表与双链表的区别
指针方面:
单链表:只有指向直接后继的指针。
双链表:有指向直接后继、和直接前驱的指针。
存储空间方面:
单链表:需要的空间更少,需要存储数据和指向直接后继的指针。
双链表:需要的空间更多,需要存储数据和指向直接后继和直接前驱的指针。
适用情况:
单链表:适合元素的增加与删除。
双链表:适合元素的查询。
注意:双链表的插入和删除操作,同时要注意其直接前驱和直接后继的指针修改,要不断链!
4.⚽ 循环链表
4.1💨 单链表的循环链表
在其表尾元素的 next 上不为 NULL,而是存储头结点的指针。
4.2💨 双链表的循环链表
在其表尾元素的 next 上不为 NULL,而是存储头结点的指针。而在头节点也存储一个指向表尾元素的指针。 其尾部操作的时间复杂度要低于 非循环链表。
5.⚽ 静态链表
静态链表与静态顺序表比较相似,其需要预先分配一段连续的内存空间。在表中,每个元素不仅存储着数据,还存储着其逻辑地址的下一个元素的指针,其指针内容为相对于静态链表的表头偏移量。
6.⚽ 顺序表与链表的相同与不同点
存取方式:
顺序表:通过索引(与表头的偏移量)存取,时间复杂度为 。
链表:循环链表到要存取的位置,时间复杂度为 。
逻辑地址与物理地址对应的方式:
顺序表:在内存中占用一段连续空间,其在逻辑地址相邻的元素,在物理地址上也相邻。
链表:在内存中占用分散的空间,其在逻辑地址上相邻的元素,在物理地址上并不相邻。由于这个特性,想要知道当前元素的下一个元素是什么,就要在当前元素上多存储一个用于指向下一个元素的指针。
空间分配方式:
顺序表:静态顺序表需要先分配内存,但分配大了则浪费,分配小了则内存会溢出,进而导致程序崩溃。动态顺序表可以解决这个问题,但每当其内存满时,则要在内存中开辟一块更大的空间,并将原表中的数据全部移过去,操作效率不高。
链表:使用内存中的零散空间存储,有效的解决了内存中空间浪费的问题。