单链表专题(C语言)
文章目录
- 前言
- 一、链表的概念和结构
- 链表的特点
- 链表的结构示意
- 二、实现单链表
- 三、链表的分类
- 总结
前言
链表是一种最基本、最常用的数据结构之一。相较于数组,链表在动态内存分配、插入、删除等操作上具有天然的优势。C 语言作为系统级编程语言,常常需要直接管理内存,通过链表来动态存储和操作数据也成为了不少工程项目中的必备技能。本文将通过详细讲解和完整代码示例,帮助读者理解和实现一个简单的单链表。
一、链表的概念和结构
链表(Linked List)是一种由节点构成的数据结构,每个节点包含存储数据以及指向下一个节点的指针(或引用)。与数组连续的内存区域不同,链表的节点分散存储,通过指针将它们串联起来,形成一个线性结构。
链表的特点
- 动态内存分配:链表在程序运行时动态创建,无需提前确定大小。
- 高效的插入和删除:在已知节点位置的前提下,插入或删除操作只涉及指针调整,不需要移动大量数据。
- 非随机访问:链表不能像数组那样通过下标直接访问某个元素,必须从头节点开始依次查找。
链表的结构示意
假设有三个节点组成的链表,结构图如下:
[数据 | 指针] -> [数据 | 指针] -> [数据 | 指针] -> NULL
- 每个节点中的 数据 用于存储具体信息,指针 指向下一个节点。
- 最后一个节点的指针指向
NULL
,表示链表的结束。
二、实现单链表
sequentialtable.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//定义顺序表结构
//#define N 1000
静态顺序表
//struct sqllist
//{
// int arr[N];
// int size;
//};
//动态顺序表
typedef int type;
typedef struct sqlList
{
type* arr;
int size;
int capacity;
}SL;
//初始化顺序表
void SLInit(SL* p);
//销毁顺序表
void DeleteSL(SL* p);
//检查是否有还有空间
void CapacityCheak(SL* p);
//尾插
void PushBack(SL* p, type x);
//头插
void PushFront(SL* p, type x);
//头删
void PopFront(SL* p);
//尾删
void PopBack(SL* p);
//在指定位置之前插入数据
void SLInsert(SL* p, int pos, type x);
//删除指定位置的内容
void SLErase(SL* p, int pos);
//查询顺序表
int SLFind(SL* p, type x);
//打印
void Print(SL* p);
sequentialtable.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "sequentialtable.h" // 包含顺序表数据结构及相关类型的头文件
/*
* 函数:SLInit
* 作用:初始化顺序表,将数组指针置为空,同时将大小和容量都设置为 0
* 参数:SL* p —— 指向顺序表结构的指针
*/
void SLInit(SL* p)
{
p->arr = NULL; // 初始化数组为空
p->size = p->capacity = 0; // 初始化大小和容量为 0
}
/*
* 函数:DeleteSL
* 作用:销毁顺序表,释放数组内存,并重置顺序表的大小与容量
* 参数:SL* p —— 指向顺序表结构的指针
*/
void DeleteSL(SL* p)
{
if (p->arr) // 如果数组不为空,则释放内存
{
free(p->arr);
}
p->arr = NULL; // 重置数组指针为空
p->size = p->capacity = 0; // 重置大小和容量为 0
}
/*
* 函数:CapacityCheak
* 作用:检查顺序表是否有足够空间存储新元素,如果空间不足则扩容
* 参数:SL* p —— 指向顺序表结构的指针
* 说明:
* - 若 size 等于 capacity,说明数组已满,需要重新分配内存
* - 新容量:如果原容量为 0,则设为4;否则扩为原来的2倍
*/
void CapacityCheak(SL* p)
{
// 判断当前存储元素个数是否已达到容量
if (p->size == p->capacity)
{
int newCapacity = p->capacity == 0 ? 4 : 2 * p->capacity;
// 重新分配内存
type* tmp = (type*)realloc(p->arr, newCapacity * sizeof(type));
if (tmp == NULL)
{
// 内存申请失败,输出错误信息并退出程序
perror("realloc");
exit(1);
}
// 内存申请成功,更新数组指针与容量
p->arr = tmp;
p->capacity = newCapacity;
}
}
/*
* 函数:PushBack
* 作用:在顺序表尾部追加一个新元素
* 参数:
* SL* p —— 指向顺序表结构的指针
* type x —— 要插入的元素
* 说明:
* - 首先检查顺序表是否有足够空间,否则扩容
* - 将元素存入数组最后位置,再更新元素个数
*/
void PushBack(SL* p, type x)
{
assert(p); // 确保传入的顺序表指针不为 NULL
CapacityCheak(p); // 检查是否需要扩容
p->arr[p->size++] = x; // 插入新元素,并更新顺序表的 size
}
/*
* 函数:PushFront
* 作用:在顺序表前端插入一个新元素
* 参数:
* SL* p —— 指向顺序表结构的指针
* type x —— 要插入的元素
* 说明:
* - 插入新元素前,需要将原有元素全部向后移动1位
* - 插入后,顺序表 size 加 1
*/
void PushFront(SL* p, type x)
{
assert(p); // 确保传入的顺序表指针不为 NULL
CapacityCheak(p); // 检查是否需要扩容
// 将所有元素向后移动一位,以腾出位置给新元素
for (int i = p->size; i > 0; i--)
{
p->arr[i] = p->arr[i - 1];
}
p->arr[0] = x; // 在数组第一个位置插入新元素
++p->size; // 更新顺序表的 size
}
/*
* 函数:PopFront
* 作用:删除顺序表前端的元素
* 参数:SL* p —— 指向顺序表结构的指针
* 说明:
* - 删除操作将所有后续元素依次前移一位
* - 顺序表 size 减 1
*/
void PopFront(SL* p)
{
assert(p); // 确保顺序表指针不为 NULL
assert(p->arr); // 确保数组已经被分配
// 将每个元素向前移动一位,覆盖掉第一个元素
for (int i = 0; i < p->size - 1; i++)
{
p->arr[i] = p->arr[i + 1];
}
--p->size; // 更新顺序表的 size
}
/*
* 函数:PopBack
* 作用:删除顺序表尾部的元素
* 参数:SL* p —— 指向顺序表结构的指针
* 说明:
* - 直接将 size 减 1,不需要移动元素
*/
void PopBack(SL* p)
{
assert(p); // 确保顺序表指针不为 NULL
assert(p->arr); // 确保数组已经被分配
--p->size; // 更新顺序表的 size,尾部元素自动舍弃
}
/*
* 函数:SLInsert
* 作用:在顺序表的指定位置 pos 插入新元素 x
* 参数:
* SL* p —— 指向顺序表结构的指针
* int pos —— 插入位置(索引),范围 0 到 size 均有效
* type x —— 要插入的元素
* 说明:
* - 插入前需将 pos 及之后的元素向后移动一个位置
*/
void SLInsert(SL* p, int pos, type x)
{
assert(p); // 确保顺序表指针不为 NULL
assert(pos >= 0 && pos <= p->size); // 检查 pos 是否在合法范围内
// 将 pos 后面的所有元素向后移动一位
for (int i = p->size; i > pos; i--)
{
p->arr[i] = p->arr[i - 1];
}
p->arr[pos] = x; // 将新元素存入 pos 位置
++p->size; // 更新顺序表的 size
}
/*
* 函数:SLErase
* 作用:删除顺序表中指定位置 pos 的元素
* 参数:
* SL* p —— 指向顺序表结构的指针
* int pos —— 要删除的元素位置(索引)
* 说明:
* - 删除后,将 pos 后面的元素向前移动一位
*/
void SLErase(SL* p, int pos)
{
assert(p); // 确保顺序表指针不为 NULL
assert(pos >= 0 && pos <= p->size); // 检查 pos 是否在合法范围内
// 从 pos 开始,将后面的元素前移一位覆盖 pos 元素
for (int i = pos; i < p->size - 1; i++)
{
p->arr[i] = p->arr[i + 1];
}
--p->size; // 更新顺序表的 size
}
/*
* 函数:SLFind
* 作用:在顺序表中查找元素 x
* 参数:
* SL* p —— 指向顺序表结构的指针
* type x —— 要查找的元素
* 返回值:
* 查找到则返回 x(也可以返回索引更好),否则返回 -1 表示没有找到
* 说明:
* - 此处返回 x 值不是很直观,通常建议返回元素的索引或指针
*/
int SLFind(SL* p, type x)
{
assert(p); // 确保顺序表指针不为 NULL
for (int i = 0; i < p->size; i++)
{
if (p->arr[i] == x)
{
return x; // 如果找到匹配元素,返回 x(建议返回 i 或者 p->arr[i] 的地址)
}
}
return -1; // 没有找到返回 -1
}
/*
* 函数:Print
* 作用:打印顺序表中所有元素
* 参数:SL* p —— 指向顺序表结构的指针
* 说明:
* - 遍历数组并依次打印每个元素,用空格分隔,最后换行
*/
void Print(SL* p)
{
for (int i = 0; i < p->size; i++)
{
printf("%d ", p->arr[i]); // 打印数组中每个元素
}
printf("\n"); // 输出换行,结束打印
}
三、链表的分类
在数据结构中,除了单链表外,还有其他几种链表,每种在不同场景下有其优势:
- 单链表:每个节点仅包含一个指针,指向下一个节点。结构简单,适用于大多数基本应用,但只能单向遍历。
- 双向链表:每个节点包含两个指针,一个指向前一个节点,一个指向后一个节点。能实现双向遍历,但占用更多内存。
- 循环链表:链表的尾节点指针指向头节点,形成一个闭环,适用于实现循环调度或需要循环遍历的场景。
总结
本文通过使用 C 语言详细介绍了单链表这一基本数据结构的概念、结构及其实现方法。主要内容包括:
- 链表的基本概念:了解链表如何通过节点和指针构成灵活的数据结构。
- C 语言实现:包括节点的定义、头插法和尾插法插入、删除节点及遍历链表的常用操作,配合完整代码示例帮助实践。
- 链表的分类:简单介绍了单链表、双向链表与循环链表的区别和应用场景。