一、顺序表
(1)、基本概念
- 概念:顺序存储的线性表(线性关系+顺序存储)
- 图解:

- 说明:就是将数据存储到一片连续的内存中,在C语言的环境下,可以是具名的栈数组,或者是匿名的堆数组(因为可存储的空间大),存储方式不仅仅只是提供数据的存储空间,而是必须要能体现数据之间的逻辑关系,当采用顺序存储的方式来存放数据 时,唯一能够用来表达数据间本身的逻辑关系就是存储位置(简单来说,顺序表是一种用连续的内存空间来存储数据元素的线性数据结构,它的逻辑顺序和物理顺序是完全一致的)。比如队列中的两个人:

(2)、顺序表的核心特点
1、优点:
-
随机访问能力强 :这是它最大的优点。通过下标(索引)可以直接访问任何一个元素,时间复杂度是 O(1),速度非常快。
-
存储密度高:因为它只存储数据元素本身,不需要像链表那样额外存储指针地址,所以空间利用率高。
2、缺点:
-
插入和删除效率低 :这是它最大的缺点。假设要在第
i
个位置插入一个新元素,那么从第i
个位置开始的所有元素都必须向后移动一位 ,腾出空间。删除则需要向前移动一位来填补空位。平均时间复杂度为 O(n)。 -
容量固定,扩容麻烦 :创建时需要预先指定大小。如果空间不够了,就需要申请一个更大的新空间,然后把所有数据逐个复制过去,非常耗时。
(3)、顺序表的设计
cpp
1、顺序表的管理结构体设计
2、初始化顺序表
3、判断顺序表是否满了
4、判断顺序表是否为空了
5、增删查改顺序表
a、删:销毁顺序表、将顺序表指定位置的数据删除
b、增:向顺序表中的表头插入一个数据
c、查:遍历顺序表
d、改:根据位置修改顺序表中的数据
1、顺序表的管理结构体设计
- 说明(结构体里面的数据):
- 顺序表的总容量
- 顺序表的当前最末数据的下标位置
- 指向顺序表内存(堆内存)的指针
- 图解

- 示例代码
cpp
typedef struct sq_list
{
int capacity; // 容量
int index; // 下标
int *data_p; // 指针
}sq_list_t, *sq_list_p;
2、初始化顺序表
- 说明:所谓初始化就是建立一个不包含任何元素的顺序表,设置好管理结构体中的表的总容量、末数据小标、申请好顺序表内存空间等系列准备工作
- 图解:

- 示例代码
cpp
sq_list_p SEQUENTIAL_LIST_Init(unsigned int cap_size)
{
sq_list_p p = malloc(sizeof(sq_list_t));
bzero(p, sizeof(sq_list_t));
if ( p!= NULL)
{
p->capacity = cap_size;
p->index = -1;
p->data_p = malloc(sizeof(int)*cap_size);
if (p->data_p == NULL)
{
free(p);
return NULL;
}
}
else
return NULL;
}
// 3、返回管理结构体的指针
return p;
}
3、销毁顺序表
- 说明:顺序表后面不再使用了,应当要释放其所占用的内存空间,这被称为顺序表的销毁
- 图解:

- 示例代码:
cpp
void SEQUENTIAL_LIST_UnInit(sq_list_p p)
{
if (p == NULL)
return;
free(p->data_p);
free(p);
}
4、判断顺序表是否满了
- 说明:需要先判断顺序表是否满了,这样可以决定是否还可以增加数据。
- 图解:

- 示例代码
cpp
bool SEQUENTIAL_LIST_IfFull(sq_list_p p)
{
return p->index == p->capacity-1;
}
5、判断顺序表是否为空了
- 说明:判断其是否空了,这样可以决定是否还需要删除数据。
- 图解

- 示例代码
cpp
bool SEQUENTIAL_LIST_IfEmpty(sq_list_p p)
{
return p->index == -1;
}
6、增加顺序表数据
- 说明:当我们将数据插入到表头的时候,顺序表后面的数据依次向后退
- 图解

- 示例代码
cpp
int SEQUENTIAL_LIST_InsertData(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++;
return 0;
}
7、删除数据
- 说明:根据顺序表的位置,将其数据删除(将要删除的数据的后面的数据全部往前挪一个位置)
- 图解:

- 示例代码
cpp
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;
for (int i = data_pos; i<=p->index; i++)
{
p->data_p[i] = p->data_p[i+1];
}
p->index--;
return 0;
}
8、修改数据
- 说明:根据位置来修改顺序表中的数据
- 图解:

- 示例代码
cpp
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;
}
9、遍历数据
- 说明:获取顺序表里面现在的存有的数据
- 图解:

- 示例代码
cpp
void SEQUENTIAL_LIST_ShowList(sq_list_p p)
{
printf("==================顺序表里面的数据====================\n\n");
for (int i = 0; i<=p->index; i++)
printf("顺序表里面的内存的数据data_p[%d] == %d\n", i, p->data_p[i]);
}
10、顺序表的使用
cpp
/**
******************************************************************************
* @file main.c
* @author MChine慕青
* @version V0.0.1
* @date 2025.09.04
* @brief 使用顺序表实现数据的增删查改功能
* 环境:ubuntu16.04
* 编译:gcc main.c sequential_list.c
* 执行:./a.out
*
******************************************************************************
* @attention
*
* 本文档只供学习使用,不得商用,违者必究
*
* 有疑问或者建议:3211735057@qq.com
*
******************************************************************************
*/
#include "sequential_list.h"
// 主函数
int main(int argc, char const *argv[])
{
// (1)、栈数组(数据在栈内存中)
int buf[128] = {0};
// (2)、堆数组(数据在堆内存中)
char *buf_p = malloc(128);
// (1)、初始化顺序表
sq_list_p p = SEQUENTIAL_LIST_Init(128);
if ( p == NULL)
{
printf("初始化顺序表失败!\n");
return -1;
}
// (2)、选择功能(增删查改、退出(销毁顺序表))
int select = 0;
int add_new_data = 0;
int del_data_pos = 0;
int change_data_pos = 0;
int change_data = 0;
while (1)
{
//遍历
SEQUENTIAL_LIST_ShowList(p);
printf("请选择一下功能:\n");
printf("1、增加数据\n");
printf("2、删除数据\n");
printf("3、修改数据\n");
printf("4、退出\n");
scanf("%d", &select);
while(getchar()!='\n');
switch (select)
{
case 1:
// 提示
printf("请输入要插入的数据:\n");
scanf("%d", &add_new_data);
while(getchar()!='\n');
// 添加数据到顺序表表头中
SEQUENTIAL_LIST_InsertData(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);
// 提示
printf("系统已退出!\n");
goto exit_sys_label;
break;
}
}
exit_sys_label:
return 0;
}
二、单向链表(单链表)
(1)、基本概念:
单链表是一种线性数据结构 ,它不像数组那样在内存中占据连续的空间,而是通过"指针"将一组零散的内存块串联起来使用。
- 图解

(2)、特点:
-
非连续存储:节点在内存中可以是分散的,靠指针连接。这使得增加和删除节点非常高效,只需要修改指针指向即可,不需要移动大量数据。
-
顺序访问 :你不能 像数组那样直接跳到第5个节点。你必须从头节点(Head) 开始,一个一个地通过
Next
指针往后找(这被称为"顺序访问")。查找效率相对较低。 -
方向性:是"单"向的,只能从头到尾,不能从尾到头。
(3)、单向链表的设计
cpp
1、节点设计
2、初始化空单向链表(初始化头节点)
3、初始化数据节点
4、判断链表是否为空
5、增删查改单向链表
a、增:头插法、尾插法
b、查:遍历链表
c、删:删除链表中的某个数据、销毁整个链表
d、改:修改链表中的某个数据
1、节点设计
- 说明:单向链表的节点只需要包括数据域、指针域
- 图解:

- 示例代码
cpp
typedef struct node
{
// 数据域
int data;
// 指针域
struct node* next_p;
}node_t, *node_p;
2、初始化空单向链表(初始化头节点)
- 概念:首先链表有两种常见的形式,一种是带有所谓的头节点的(推荐),一种是不带头节点的,所谓的头节点是不存放有效数据的节点,仅仅用来方便操作
- 图解

- 说明:
- 由于头节点不存放有效数据的,因此如果空链表中带有头节点,那么头指针(head_node的next_p)将永远不变,这会给以后的链表操作带来些许便捷
- 图解:初始化头节点

- 示例代码:
cpp
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;
}
return p;
}
3、初始化数据节点
- 说明:初始化有数据的节点
- 图解

- 示例代码
cpp
node_p LINK_LIST_InitDataNode(int data)
{
// 1、给数据节点申请一个内存空间(堆内存)
node_p p = malloc(sizeof(node_t));
bzero(p, sizeof(node_t));
if ( p != NULL)
{
// 数据域
p->data = data;
// 指针域
p->next_p = NULL;
}
else
{
return NULL;
}
return p;
}
4、判断链表是否为空
- 说明:头节点的next_p指向的是NULL
- 图解

- 示例代码
cpp
bool LINK_LIST_IfEmpty(node_p head_node)
{
return head_node->next_p == NULL;
}
5、插入数据
a、头插法
- 说明:链表中的头节点的后面插进去
- 图解

- 示例代码:
cpp
void LINK_LIST_InsertHeadDataNode(node_p head_node, node_p new_node)
{
// 1、先让new_node里面的next_p指向data_node
new_node->next_p = head_node->next_p;
// 2、再让head_node里面的next_p指向new_node
head_node->next_p = new_node;
}
b、尾插法
- 说明:链表中的最后一个节点后面插入进去
- 图解

- 示例代码
cpp
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;
}
6、删除数据
说明:根据数据来删除链表中的节点
图解:

- 示例代码
cpp
int LINK_LIST_DelDataNode(node_p head_node, int del_data)
{
// 1、判断链表是否为空,是的话,返回-1
if (LINK_LIST_IfEmpty(head_node))
return -1;
node_p tmp_p = NULL;
node_p last_node = NULL;
node_p del_node = NULL;
node_p next_node = NULL;
for (tmp_p = head_node; tmp_p->next_p!=NULL; tmp_p=tmp_p->next_p)
{
// 判断要删除的数据
if ( (tmp_p->next_p->data) == del_data)
{
last_node = tmp_p;
del_node = tmp_p->next_p;
next_node = del_node->next_p;
break;
}
}
// 绕过原链表要删除的节点
last_node->next_p = next_node;
del_node->next_p = NULL;
free(del_node);
return 0;
}
7、修改数据
- 说明:通过数据值,找到链表中的节点,并修改里面的数据
- 图解:

- 示例代码
cpp
int LINK_LIST_ChangeNodeData(node_p head_node, int data, int change_data)
{
// 如果链表为空,那么直接释放头节点空间即可
if (LINK_LIST_IfEmpty(head_node))
return -1;
node_p tmp_p = NULL;
// 遍历所有可能
for (tmp_p = head_node->next_p; tmp_p!=NULL; tmp_p=tmp_p->next_p)
{
// 找到要修改的数据
if ((tmp_p->data) == data)
{
tmp_p->data = change_data;
break;
}
}
return 0;
}
8、遍历数据
- 说明:遍历整个链表,并逐个打印里面的数据
- 图解:

- 示例代码:
cpp
int LINK_LIST_ShowListData(node_p head_node)
{
// 判断链表是否为空
if (LINK_LIST_IfEmpty(head_node))
return -1;
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++;
}
return 0;
}
9、销毁整个链表
- 说明
- 图解:

- 示例代码
cpp
void LINK_LIST_UnInit(node_p head_node)
{
// 若是链表为空,就直接释放头节点空间
if (LINK_LIST_IfEmpty(head_node))
{
free(head_node);
return;
}
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);
}
free(head_node);
}
(4)、单向链表的使用
cpp
/**
******************************************************************************
* @file main.c
* @author MChine慕青
* @version V0.0.1
* @date 2025.09.05
* @brief 使用单向链表实现数据的增删查改功能
* 环境:ubuntu22.04
* 编译:gcc main.c link_list.c
* 执行:./a.out
*
******************************************************************************
* @attention
*
* 本文档只供学习使用,不得商用,违者必究
*
* 有疑问或者建议:3211735057@qq.com
*
******************************************************************************
*/
#include "link_list.h"
int main(int argc, char const *argv[])
{
// 初始化头节点
node_p head_node = LINK_LIST_InitHeadNode();
if (head_node == NULL)
{
printf("初始化空链表(头节点)失败!\n");
return -1;
}
// 选择功能
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)
{
// 遍历
LINK_LIST_ShowListData(head_node);
printf("请选择以下功能:\n");
printf("1、插入数据(头插法)\n");
printf("2、插入数据(尾插法)\n");
printf("3、删除数据(根据数据值)\n");
printf("4、修改数据(根据数据值)\n");
printf("5、退出(销毁链表)\n");
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");
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;
}
三、顺序表与单链表的比较总结
顺序表和单链表是数据结构中最基础的两种线性表实现方式,各自具有独特的优缺点和适用场景。
顺序表通过连续内存存储数据,支持随机访问且缓存命中率高,适合频繁查询或元素数量固定的场景。单链表通过节点动态分配内存,插入删除效率高且无需预分配空间,适合动态数据操作或内存受限的环境。
实际开发中,需根据具体需求选择合适的数据结构。若注重查询性能且数据规模稳定,顺序表更优;若需要频繁增删或数据规模变化大,单链表更合适。理解二者的核心原理和性能差异,能帮助开发者优化算法设计,提升程序效率。
掌握这两种结构的实现与应用,是进一步学习栈、队列、树等高级数据结构的重要基础。