一、顺序表
(1)基本概念
概念:顺序存储的线性表
图解:
说明:
在C语言的环境下,将数据存储到一片连续的内存钟,在C语言的环境下,可以是具名的栈数组,或者是匿名的对数组(因为可存储的空间大),存储方式不仅仅只是提供数据的存储空间,二十必须要能体现数据之间的逻辑关系,当采用顺序存储的方式来存放数据是,唯一能够用来表达数据间本身的逻辑关系,当采用顺序存储的方式来存放数据时,唯一能够用来表达数据间本身逻辑关系就是存储位置,比如队列中的两个人:
(2)顺序表的设计
1、顺序表的管理结构体设计
2、初始化顺序表
3、增删查改顺序表
// 前提:判断顺序表是否满了或者空了
a、删:销毁顺序表、将顺序表指定位置的数据删除
b、增:向顺序表中的表头插入一个数据
c、查:遍历顺序表 d、改:根据位置修改顺序表中的数据
1、管理结构体设计
说明:(结构体里面的数据)
顺序表的总容量
顺序表的当前最末数据的下标位置
指向顺序表内存(堆内存)的指针
图解:
示例代码:
typedef struct sq_list
{
int capacity; // 顺序表的容量
int index; // 顺序表的数据下标(最末尾数据的下标)
int *data_p; // 指向顺序表内存的指针
}sq_list_t, *sq_list_p;
2、初始化顺序表
说明:所谓初始化就是建立一个不包含任何元素的顺序表,设置好管理结构体中的表的总容量、末数据下标、申请号顺序表内存空间等准备工作
图解:
示例代码:
/**
* @brief 初始化顺序表
* @note None
* @param cap_size:顺序表的容量
* @retval 成功:返回顺序表的管理结构体的指针
* 失败:返回NULL
*/
sq_list_p SEQUENTIAL_LIST_Init(unsigned int cap_size)
{
//1、给顺序表管理结构体申请一个堆内存空间
sq_list_p p = malloc(sizeof(sq_list_t));
bzero(p, sizeof(sq_list_t));
//2、给申请的空间进行赋值操作
if (p != NULL)
{
p->capacity = cap_size; //顺序表的内存空间的容量(有多少个数据)
p->index = -1; //顺序表的数据下表(最末尾数据的下表) --- 一开时没有数据存入,所以是-1(0的话是第一个数据)
p->data_p = malloc(sizeof(int)*cap_size); //指向顺序表内存的指针(申请一个堆空间,并让指针指向它)
if (p->data_p == NULL) //判断p->data_p是否申请堆空间失败,如果失败,就将管理结构体空间释放并返回NULL
{
free(p);
return NULL;
}
}
else //判断p是否申请堆空间失败,如果失败,就返回NULL
{
return NULL;
}
//3、返回管理结构体的指针
return p;
}
3、销毁顺序表
说明:一个顺序表后面如果不再使用,应当要释放其所占用的内存空间,这被成为顺序表的销毁。
图解:
示例代码:
/**
* @brief 销毁顺序表
* @note None
* @param p:顺序表管理结构体的指针(通过它来将整个顺序表全部销毁)
* @retval None
*/
void SEQUENTIAL_LIST_UnInit(sq_list_p p)
{
//1、如果顺序表本身就为NULL,就不必继续下面的内容
if (p == NULL)
{
return;
}
//2、释放堆内存空间(由内到外)
free(p->data_p);
free(p);
}
4、判断顺序表是否为满或者空
说明:需要先判断顺序表是否满了,这样可以决定是否可以增加数据,判断其是否为空,这样可以决定是否还需要删除数据
图解:
示例代码:
/**
* @brief 判断顺序表数据是否满了
* @note None
* @param p:顺序表管理结构体的指针
* @retval 顺序表数据满了:返回true
* 顺序表数据未满:返回false
*/
bool SEQUENTIAL_LIST_IfFull(sq_list_p p)
{
return p->index == p->capacity-1;
}
/**
* @brief 判断顺序表数据是否空了
* @note None
* @param p:顺序表管理结构体的指针
* @retval 顺序表数据为空:返回true
* 顺序表数据为空:返回false
*/
bool SEQUENTIAL_LIST_IfEmpty(sq_list_p p)
{
return p->index == -1;
}
5、插入数据
说明: 当我们将数据插入到表头的时候,顺序表后面的数据依次向后退
图解:
示例代码:
/**
* @brief 在顺序表的表头插入一个数据(头插法)
* @note None
* @param p: 顺序表管理结构体的指针
* new _data:要插入的数据
* @retval 成功:返回0
* 失败:返回-1
*/
int SEQUENTIAL_LIST_InserData(sq_list_p p, int new_data)
{
//1、判断顺序表数据是否满了,满了返回-1
if (SEQUENTIAL_LIST_IfFull(p))
{
return -1;
}
//2、将原有的数据全部往后挪一个位置,再将数据插入到表头中
for (int i = p->index; i >= 0; i--)
{
p->data_p[i+1] = p->data_p[i];
}
//3、将数据插入到表头
p->data_p[0] = new_data;
//4、顺序表额数据下表index+1
p->index++;
//5、成功返回0
return 0;
}
6、删除数据
说明:根据顺序表的位置,将其数据删除(将要删除的数据的后面的一个数据全部往前移动一个位置)
图解:
示例代码:
/**
* @brief 将顺序表指定的位置的数据删除
* @note None
* @param p: 顺序表管理结构体的指针
* data_pos:要删除的顺序表数据的位置
* @retval 成功:返回0
* 失败:返回-1
*/
int SEQUENTIAL_LIST_DelPosData(sq_list_p p, unsigned int data_pos)
{
//1、如果顺序表里面没有数据,并且没有这个位置的数据,就返回-1
if ((SEQUENTIAL_LIST_IfEmpty(p)) || (data_pos > (p->index)))
{
return -1;
}
//2、根据要删除的数据的位置,将其后面的数据全部往前挪动一个位置即可
for (int i = data_pos; i <= p->index; i++)
{
p->data_p[i] = p->data_p[i+1];
}
//3、将下标index-1
p->index--;
//4、成功删除数据,返回0
return 0;
}
7、遍历数据
说明:获取顺序表里面现在的存有的数据
图解:
示例代码:
/**
* @brief 查看顺序表
* @note None
* @param p: 顺序表管理结构体的指针
* @retval None
*/
void SEQUENTIAL_LIST_ShowList(sq_list_p p)
{
printf("====================顺序里面的数据===================\n");
for (int i = 0; i <= p->index; i++)
{
printf("顺序表里面的内存的数据data_p[%d] == %d\n", i, p->data_p[i]);
}
printf("====================================================\n");
}
8、修改数据
说明:根据位置来修改顺序中的数据
图解:
示例代码:
/**
* @brief 修改顺序表中的数据
* @note 根据数据的位置来修改数据
* @param p: 顺序表管理结构体的指针
* data_pos: 要修改的顺序表数据的位置
* change_data:修改后的数据
* @retval 成功:返回0
* 失败:返回-1
*/
int SEQUENTIAL_LIST_ChangeData(sq_list_p p, int data_pos, int change_data)
{
//1、判断顺序表是否为空
if ((SEQUENTIAL_LIST_IfEmpty(p)) || (data_pos > (p->index)))
{
return -1;
}
//2、根据位置,修改数据
p->data_p[data_pos] = change_data;
//3、成功返回0
return 0;
}
(3)顺序表的使用
/**
******************************************************************************
* @file main.c
* @author Yi_An忆安
* @version V0.0.1
* @date 2025.09.1
* @brief 使用顺序表实现数据的增删查改功能
* 环境:ubuntu22.04
* 编译:make
* 执行:make run
* 说明:make help
*
******************************************************************************
* @attention
*
* 本文档只供学习使用,不得商用,违者必究
*
* 微信公众号: 梦入长安
* 有疑问或者建议:15778632984@163.com
*
******************************************************************************
**/
#include "../Include/sequential_list.h"
int main(int argc, char const *argv[])
{
//1、定义一个栈数组buf(数据再堆内存中)
int buf[128] = {0};
//2、定义一个指针buf_p指向堆空间的内存
char *buf_p = malloc(128);
//3、初始化顺序表
sq_list_p p = SEQUENTIAL_LIST_Init(128);
if (p == NULL)
{
printf("初始化顺序表失败!\n");
return -1;
}
//4、选择功能(增、删、查、改)
int select = 0;
int add_new_data = 0;
int del_data_pos = 0;
int change_data_pos = 0;
int change_data = 0;
while (1)
{
//(1)显示整个顺序表的数据
SEQUENTIAL_LIST_ShowList(p);
//(2)、显示功能选项
printf("请选择需要使用的功能:\n");
printf("1、增加数据\n");
printf("2、删除数据\n");
printf("3、修改数据\n");
printf("4、退出\n");
//(3)选择需要的功能
scanf("%d", &select);
while(getchar()!='\n');
switch (select)
{
case 1:
printf("请输入要插入的数据");
scanf("%d", &add_new_data);
while (getchar() != '\n');
//添加数据到顺序表表头中
SEQUENTIAL_LIST_InserData(p,add_new_data);
break;
case 2:
printf("请输入要删除的数据的位置:\n");
scanf("%d", &del_data_pos);
while (getchar() != '\n');
//根据位置删除顺序表中的数据
SEQUENTIAL_LIST_DelPosData(p, del_data_pos);
break;
case 3:
printf("请输入需要修改的位置和数据\n");
scanf("%d,%d", &change_data_pos, &change_data);
while (getchar() !='\n');
//修改顺序表中的数据
SEQUENTIAL_LIST_ChangeData(p, change_data_pos, change_data);
break;
case 4:
//销毁顺序表
SEQUENTIAL_LIST_UnInit(p);C
printf("系统已退出!\n");
goto exit_sys_label;
break;
}
}
exit_sys_label:
return 0;
}
注意:本代码的编译和执行需要处在以下工程文件当中,否则只需要正常使用gcc命令执行即可
二、单向链表(单链表)
(1)基本概念
概念:链式存储的线性表,简称链表(线性关系+链式存储)
图解:
说明:既然顺序存储中的数据因为挤在一起而导致需要成片移动,那种很容易想到的解决方案是将数据离散的存储在不同的内存块中,然后用指针将它们串起来,这种朴素的思路所形成的链表式线性表,就是所谓链表(单向链表(单向循环链表)、双向链表(双向循环链表))
(2)单向链表的设计
1、节点设计
2、初始化空单向链表(初始化头节点)
3、初始化数据节点
4、增删查改单向链表 // 前提:判断链表是否为空
a、增:头插法、尾插法
b、查:遍历链表
c、删:删除链表中的某个数据、销毁整个链表
d、改:修改链表中的某个数据
1、节点设计
说明:单向链表的节点非常简单,节点除了要保存的用户数据(数据域)之外(这里以整型数据为例),只需要增加一个指向本类节点的指针(指针域)即可
示例代码:
typedef struct node
{
// 数据域
int data; // 数据可以是任意数据
// 指针域
struct node* next_p; // 指向相邻的下一个节点的指针
}node_t, *node_p;
2、初始化空单向链表(初始化头节点)
概念:首先,链表有两种常见的形式,一种是带有头节点的,一种是不带头节点的,所谓的头节点是不存放有效数据的节点,仅仅用来方便我们操作
图解1:
注意:
头节点的head_node是可选的,为了方便某些操作,建议大家有头节点好一点
头节点的指针:head_node的next_p
说明:
由于头节点不存放有效数据,因此如果空链表中带有头节点,那么头指针(hear_node)的
next_p将永远不会变,这会给以后的链表操作带来些许便捷
图解2:
示例代码:
/**
* @brief 初始化空单向链表(初始化头节点)
* @note None
* @param None
* @retval 成功:返回指向这个头节点的指针
* 失败:返回NULL
*/
node_p LINK_LIST_InitHeadNode(void)
{
// 1、给头节点申请一个内存空间(堆内存)
node_p p = malloc(sizeof(node_t));
bzero(p, sizeof(node_t));
// 2、将都节点的next_p指针指向NULL
if (p != NULL)
{
//数据域
//指针域
p->next_p =NULL; //防止指针乱指
}
else
{
return NULL;
}
// 3、成功返回头节点
return p;
}
3、初始化数据节点
说明:这是一个有数据的节点
图解:
示例代码:
/**
* @brief 初始化数据节点
* @note None
* @param data:数据节点中的数据
* @retval 成功:返回指向这个数据节点的指针
* 失败:返回NULL
*/
node_p LINK_LIST_InitDateNode(int data)
{
// 1、给数据节点申请一个内存空间(堆内存)
node_p p = malloc(sizeof(node_t));
bezero(p, sizeof(node_t));
// 2、将数据节点的next_p指针指向NULL,并且将传进来的数据对此节点的数据进行赋值
if (p != NULL)
{
// 数据域
p->data = data; //数据赋值
// 指针域
p->next_p = NULL; //防止指针乱指C
}
else
{
return NULL;
}
// 3、成功返回数据节点
return p;
}
4、插入数据
说明:将数据节点插入到链表中,一种头插法(链表中的头节点的后面插进去),一种尾插法(链表中的最后一个节点后面插入进去)
头插法图解:
头插法示例代码:
/**
* @brief 插入数据(头插法)
* @note None
* @param head_node:头节点
* new_node: 要插入的数据节点
* @retval None
*/
void LINK_LIST_HeadInsertDataNode(node_p head_node, node_p new_node)
{
//1、先让new_node里面的next_p指向data_node(head_node->next_p)
new_node->next_p = head_node->next_p;
//2、再让head_node里面的next_p指向new_node
head_node->next_p =new_node;
}
尾插法图解:
尾插法示例代码:
/**
* @brief 插入数据(尾插法)
* @note None
* @param head_node:头节点
* new_node: 要插入的数据节点
* @retval None
*/
void LINK_LIST_TailInsertDataNode(node_p head_node, node_p new_node)
{
//1、设置一个中间指针、将指针指向单向链表的末尾节点
node_p tmp_p = NULL;
for (tmp_p = head_node; tmp_p->next_p != NULL; tmp_p = tmp_p->next_p)
{
//2、让tmp_p的next_p指向新节点
tmp_p->next_p = new_node;
//3、再让new_node的next_p指向NULL
new_node->next_p = NULL;
}
}
5、判断链表是否为空
说明:如果头节点的next_p指向NULL,那么就可以证明这个链表是空的
图解:
示例代码:
/**
* @brief 判断链表是否为空
* @note None
* @param head_node:头节点
* @retval 如果链表为空:返回true
* 如果链表非空:返回false
*/
bool LINK_LIST_IfEmpty(node_p head_node)
{
return head_node->next_p == NULL;
}
6、遍历链表
说明:遍历整个链表,并逐个打印里面的数据
图解:
示例代码:
/**
* @brief 遍历整个链表并打印出里面的数据
* @note None
* @param head_node:头节点
* @retval 成功:返回0
* 失败:返回-1
*/
int LINK_LIST_ShowListData(node_p head_node)
{
// 1、判断链表是否为空,是的话,返回-1
if (LINK_LIST_IfEmpty(head_node))
return -1;
// 2、遍历整个链表,并逐个打印里面的数据
node_p tmp_p = NULL;
int i = 0;
printf("======================链表中的数据==========================\n");
for ( tmp_p = head_node->next_p; tmp_p!=NULL; tmp_p=tmp_p->next_p)
{
printf("链表中的第%d的节点, 数据为: %d\n", i, tmp_p->data);
i++;
}
printf("===========================================================\n");
// 3、成功返回0
return 0;
}
7、删除数据
说明:根据数据来删除链表中的节点
图解:
示例代码:
/**
* @brief 删除数据
* @note 根据数据的值找到链表中的节点,并将其删除
* @param head_node:头节点
* del_data: 要删除的数据
* @retval 成功:返回0
* 失败:返回-1
*/
int LINK_LIST_DelDataNode(node_p head_node, int del_data)
{
// 1、判断链表是否为空,是的话,返回-1
if (LINK_LIST_IfEmpty(head_node))
return -1;
// 2、移动到要删除的节点数据那里去
node_p tmp_p = NULL;
node_p last_node = NULL;
node_p del_node = NULL;
node_p next_node = NULL;
// a、从头到尾遍历一遍,找到要删除的数据
for (tmp_p = head_node; tmp_p->next_p!=NULL; tmp_p=tmp_p->next_p)
{
// b、判断要删除的数据
if ( (tmp_p->next_p->data) == del_data)
{
// c、保存删除数据的上一个节点和下一个节点、及删除节点
last_node = tmp_p;
del_node = tmp_p->next_p;
next_node = del_node->next_p;
break;
}
}
// 3、绕过原链表要删除的节点
last_node->next_p = next_node;
del_node->next_p = NULL;
// 4、释放要删除节点的资源
free(del_node);
// 5、成功返回0
return 0;
}
8、销毁整个链表
说明:从头结点开始,将其后面的数据节点一个个删除,最后再删除头节点本身
图解:
示例代码:
/**
* @brief 销毁链表
* @note None
* @param head_node:头节点
* @retval None
*/
void LINK_LIST_UnInit(node_p head_node)
{
// 1、如果链表为空,那么直接释放头节点空间即可
if (LINK_LIST_IfEmpty(head_node))
{
free(head_node);
return;
}
// 2、
node_p tmp_p = NULL; // 遍历
node_p last_node = head_node;
node_p del_node = head_node->next_p;
node_p next_node = del_node->next_p;
int num = 0;
// 如果链表只有一个数据节点
if (next_node == NULL)
{
last_node->next_p = next_node;
free(del_node);
}
// 否则
else
{
for (tmp_p = head_node; tmp_p->next_p!=NULL; tmp_p=tmp_p->next_p)
{
// a、删除节点并释放其内存
last_node->next_p = next_node;
free(del_node);
printf("num == %d\n", num);
num++;
// b、轮回继续
del_node = next_node;
next_node = next_node->next_p;
}
// 少删除了一个节点数据
last_node->next_p = next_node;
free(del_node);
}
// 3、释放头节点
free(head_node);
}
9、销毁链表
说明:通过数据值,找到链表中的节点,并修改里面的数据
图解:
示例代码:
/**
* @brief 修改数据
* @note 根据数据的值来找到链表中的节点,对里面的数据进行修改
* @param head_node: 头节点
* data: 要修改的数据
* change_data:修改的数据
* @retval 成功:返回0
* 失败:返回-1
*/
int LINK_LIST_ChangeNodeData(node_p head_node, int data, int change_data)
{
// 1、如果链表为空,那么直接释放头节点空间即可
if (LINK_LIST_IfEmpty(head_node))
return -1;
// 2、遍历整个链表,找到数据的节点,并修该节点的相应的数据
node_p tmp_p = NULL;
// a、遍历所有可能
for (tmp_p = head_node->next_p; tmp_p!=NULL; tmp_p=tmp_p->next_p)
{
// b、找到要修改的数据
if ((tmp_p->data) == data) // 根据学号
{
tmp_p->data = change_data; // 修改这个学生的其它信息
break;
}
}
// 3、成功返回0
return 0;
}
(3)单向链表的使用
/**
******************************************************************************
* @file main.c
* @author Yi_An忆安
* @version V0.0.1
* @date 2025.09.5
* @brief 使用单向链表实现数据的增删查改功能
* 环境:ubuntu22.04
* 编译:make
* 执行:make run
* 说明:make help
*
******************************************************************************
* @attention
*
* 本文档只供学习使用,不得商用,违者必究
*
* 微信公众号: 梦入长安
* 有疑问或者建议:15778632984@163.com
*
******************************************************************************
**/
#include "link_list.h"
int main(int argc, char const *argv[])
{
// (1)、初始化头节点
node_p head_node = LINK_LIST_InitHeadNode();
if (head_node == NULL)
{
printf("初始化空链表(头节点)失败!\n");
return -1;
}
// (2)、选择功能(增删改查、退出(销毁链表))
node_p new_node = NULL;
int select = 0;
int new_data = 0;
int del_data = 0;
int find_data = 0; // 相当于学号
int change_data = 0; // 学生的其它信息(电话、成绩等)
while (1)
{
// 1、显示整个链表的数据
LINK_LIST_ShowListData(head_node);
// 2、显示功能选项
printf("请选择以下功能:\n");
printf("1、插入数据(头插法)\n");
printf("2、插入数据(尾插法)\n");
printf("3、删除数据(根据数据值)\n");
printf("4、修改数据(根据数据值)\n");
printf("5、退出(销毁链表)\n");
// 3、选择要做的功能
scanf("%d", &select);
while(getchar()!='\n');
switch (select)
{
case 1:
// 提示
printf("请输入要添加的数据(整数)(头插法)\n");
// 输入要插入的数据
scanf("%d", &new_data);
while(getchar()!='\n');
// 生成一个数据节点
new_node = LINK_LIST_InitDataNode(new_data);
// 将新生成的数据节点添加到链表中(头插法)
LINK_LIST_HeadInsertDataNode(head_node, new_node);
break;
case 2:
// 提示
printf("请输入要添加的数据(整数)(尾插法)\n");
// 输入要插入的数据
scanf("%d", &new_data);
while(getchar()!='\n');
// 生成一个数据节点
new_node = LINK_LIST_InitDataNode(new_data);
// 将新生成的数据节点添加到链表中(头插法)
LINK_LIST_TailInsertDataNode(head_node, new_node);
break;
case 3:
// 提示
printf("请输入要删除的数据(整数)(根据数据值)\n");
// 输入要删除的数据
scanf("%d", &del_data);
while(getchar()!='\n');
// 删除链表中的数据(从头到尾删除一个)
LINK_LIST_DelDataNode(head_node, del_data);
break;
case 4:
// 提示
printf("请输入要修改的数据(整数)(根据数据值)\n");
// 通过学号(find_data),修改学生的其它信息(changge_data)
scanf("%d,%d", &find_data, &change_data);
while(getchar()!='\n');
// 找到链表中的数据了,并修改其数据节点的相应的数据
LINK_LIST_ChangeNodeData(head_node, find_data, change_data);
break;
case 5:
LINK_LIST_UnInit(head_node);
printf("系统已退出!\n");
goto exit_sys_label;
break;
}
}
exit_sys_label:
return 0;
}
注意:本代码的编译和执行需要处在以下工程文件当中,否则只需要使用gcc命令执行即可