目录
- [1 链表的成因](#1 链表的成因)
- [2 链表的概念](#2 链表的概念)
- [3 单链表的结构和操作实现](#3 单链表的结构和操作实现)
-
- [3.1 单链表的结构体定义](#3.1 单链表的结构体定义)
- [3.2 生成新结点](#3.2 生成新结点)
- [3.3 单链表的尾插操作](#3.3 单链表的尾插操作)
- [3.4 单链表的头插操作](#3.4 单链表的头插操作)
- [3.5 单链表的尾删操作](#3.5 单链表的尾删操作)
- [3.6 链表的头删操作](#3.6 链表的头删操作)
- [3.7 链表的输出操作](#3.7 链表的输出操作)
- [3.8 在链表指定位置之前插入](#3.8 在链表指定位置之前插入)
- [3.9 指定删除链表的某一个结点](#3.9 指定删除链表的某一个结点)
- [3.10 在链表的指定位置之后插入结点](#3.10 在链表的指定位置之后插入结点)
- [3.11 删除指定位置的下一个结点](#3.11 删除指定位置的下一个结点)
- [3.12 销毁链表](#3.12 销毁链表)
- [4 单链表整体实现](#4 单链表整体实现)
1 链表的成因
在之前的文章中,我介绍过了顺序表,可以通过下面的链接查看
对于顺序表这种结构而言,它具有一定的缺点
- 在顺序表的头部或中间位置插入,删除元素时,时间复杂度较高
- 顺序表对内存空间较为浪费,比如说,只需要存储一个元素,但是却扩充100个空间的情况,剩下的99个空间直接被浪费了
- 每次扩充空间,释放空间时,都需要额外的时间开销,效率低
为了解决这些顺序表的缺点,就出现了链表这种结构
2 链表的概念
链表是线性表的一种,它在逻辑结构上是连续的,但是在物理结构上是不一定连续的
链表有多种分类,在多种分类中,主要关心的是单链表 和双链表
在这篇文章中,先来介绍一下单链表
3 单链表的结构和操作实现
3.1 单链表的结构体定义
单链表一般指 不带虚拟头结点的单向不循环链表 ,它每个结点都保存了数据和下一个结点的地址
比如下图就是一个单链表:
对应的结构体定义如下:
c
typedef int SListDataType;
typedef struct SListNode
{
SListDataType data; //数据
struct SListNode* next; //指向下一个结点的指针
}SLTNode;
3.2 生成新结点
要生成新结点,只需要通过malloc动态开辟空间,将值保存进新空间内并设置next指针为空即可
c
SLTNode* SLTCreateNode(SListDataType x)
{
SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
assert(node);
node->data = x;
node->next = NULL;
return node;
}
3.3 单链表的尾插操作
尾插操作时,需要处理空链表和非空链表两种情况
空链表
空链表的情况下,先创建新结点,再使头指针head指向新结点即可
非空链表
非空链表在进行尾插时,需要先通过指针 tail 来寻找到链表尾结点,再创建新结点并插入到尾部
注:此处保存1的结点的next指针中,保存的应为0x13ff40
代码实现
c
void SLTPushBack(SLTNode** pHead, SListDataType x)
{
assert(pHead);
SLTNode* newNode = SLTCreateNode(x); //创建新结点
if (*pHead == NULL) //是空链表
{
*pHead = newNode;
}
else //不是空链表
{
SLTNode* tail = *pHead;
while (tail->next) //寻找尾结点
{
tail = tail->next;
}
tail->next = newNode; //插入新结点
}
}
3.4 单链表的头插操作
头插操作不需要分情况讨论,直接创建新结点,使新结点的 next 指针指向下一个结点,使 head 指针指向新结点即可
代码实现
c
void SLTPushFront(SLTNode** pHead, SListDataType x)
{
assert(pHead);
SLTNode* newNode = SLTCreateNode(x);
newNode->next = *pHead;
*pHead = newNode;
}
3.5 单链表的尾删操作
单链表在进行尾删时,需要考虑一个结点,多个结点两种情况
一个结点时
当只有一个结点时,只需要释放head所指向的空间并将head置为空即可
多个结点时
首先遍历链表,使用 prev 记录倒数第二个结点,使用 rear 来记录最后一个结点,更改 prev 指向的节点的 next 指针,释放 rear 指向的结点即可
代码实现
c
void SLTPopBack(SLTNode** pHead)
{
assert(pHead && *pHead);
if ((*pHead)->next == NULL) //只有一个结点
{
free(*pHead);
*pHead = NULL;
}
else //不止一个节点
{
SLTNode* prev = *pHead;
SLTNode* rear = prev->next;
while (rear->next) //寻找最后一个结点和倒数第二个结点
{
prev = prev->next;
rear = rear->next;
}
free(rear);
rear = NULL;
prev->next = NULL;
}
}
3.6 链表的头删操作
在进行头删时,先用指针 cur 记录下要删除的结点,再修改 head 指针的指向,释放要删除的结点,最后将 cur 置为空,防止野指针问题
代码实现
c
void SLTPopFront(SLTNode** pHead)
{
assert(pHead && *pHead); //防止空指针解引用
SLTNode* cur = *pHead;
*pHead = cur->next;
free(cur);
cur = NULL;
}
3.7 链表的输出操作
要将链表每个结点保存的值进行输出,只需要使用一个指针来遍历链表并访问每一个结点保存的值就可以了
代码实现
c
void SLTPrint(SLTNode* head)
{
SLTNode* cur = head;
while (cur != NULL) //使用cur寻找每一个节点
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("\n");
}
3.8 在链表指定位置之前插入
如果想在链表的某一个结点之前插入新的结点,就需要分在第一个结点前,不在第一个结点前的两类情况,pos 是指定的位置
在第一个结点前
在第一个结点之前插入新的结点,实际上就是头插,因此调用头插的函数即可
不在第一个结点前
这个时候就需要 prev 指针来寻找 pos 结点的前一个结点,找到后,创建新结点,使新结点的 next 指针指向 pos 结点,prev 的 next 指针指向新结点
代码实现
c
void SLTInsert(SLTNode** pHead, SLTNode* pos, SListDataType x)
{
assert(pHead);
if (pos == *pHead) //插入的位置在第一个结点前
{
SLTPushFront(pHead, x);
}
else //插入的位置不在第一个结点前
{
SLTNode* prev = *pHead;
while (prev->next != pos) //寻找pos的前一个结点
{
prev = prev->next;
}
SLTNode* node = SLTCreateNode(x); //插入新结点
node->next = pos;
prev->next = node;
}
}
3.9 指定删除链表的某一个结点
在指定删除链表的某一结点时,要区分第一个结点和其他结点两个情况
pos 指向了要删除的结点
删除第一个结点
删除第一个结点时,实际上就是在进行头删的操作,直接调用头删的函数即可
删除其他结点
删除其他结点时,需要使用 prev 指针找到 pos 的前一个结点,更改 prev 指针指向结点的 next 指针,使其指向后一个结点,再释放 pos 结点
代码实现
c
void SLTErase(SLTNode** pHead, SLTNode* pos)
{
assert(pHead && *pHead);
assert(pos);
if (pos == *pHead) //删除第一个结点
{
SLTPopFront(pHead);
}
else //删除其它结点
{
SLTNode* prev = *pHead;
while (prev->next != pos) //找pos的前一个结点
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
3.10 在链表的指定位置之后插入结点
pos 为指定的位置,先创建新结点,更改新结点的 next 指针,使其指向 pos 的下一个结点,再使 pos 的 next 指针指向新结点即可
代码实现
c
void SLTInsertAfter(SLTNode* pos, SListDataType x)
{
assert(pos);
SLTNode* node = SLTCreateNode(x);
node->next = pos->next;
pos->next = node;
}
3.11 删除指定位置的下一个结点
pos 为指定的位置,先记录 pos 的下一个结点为 cur,使 pos 指向结点的 next 指针指向后一个结点,再释放 cur 指向的结点即可
代码实现
c
void SLTEraseAfter(SLTNode* pos)
{
assert(pos && pos->next);
SLTNode* cur = pos->next;
pos->next = cur->next;
free(cur);
cur = NULL;
}
3.12 销毁链表
销毁链表时,需要使用到两个指针 prev 和 rear,prev用来指向要销毁的结点,rear 用来记录下一个结点,防止销毁结点后找不到下一个结点的情况发生,然后遍历链表,释放每一个结点即可
代码实现
c
void SListDesTroy(SLTNode** pHead)
{
assert(pHead && *pHead);
SLTNode* prev = *pHead;
while (prev) //销毁每一个节点
{
SLTNode* rear = prev->next;
free(prev);
prev = rear;
}
*pHead = NULL; //头指针置为空
}
4 单链表整体实现
SList.c
c
//函数的实现
#include "SList.h"
//打印链表节点
void SLTPrint(SLTNode* head)
{
SLTNode* cur = head;
while (cur != NULL) //使用cur寻找每一个节点
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("\n");
}
//产生新结点
SLTNode* SLTCreateNode(SListDataType x)
{
SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
assert(node);
node->data = x;
node->next = NULL;
return node;
}
//尾插
void SLTPushBack(SLTNode** pHead, SListDataType x)
{
assert(pHead);
SLTNode* newNode = SLTCreateNode(x); //创建新结点
if (*pHead == NULL) //是空链表
{
*pHead = newNode;
}
else //不是空链表
{
SLTNode* tail = *pHead;
while (tail->next) //寻找尾结点
{
tail = tail->next;
}
tail->next = newNode; //插入新结点
}
}
//头插
void SLTPushFront(SLTNode** pHead, SListDataType x)
{
assert(pHead);
SLTNode* newNode = SLTCreateNode(x);
newNode->next = *pHead;
*pHead = newNode;
}
//尾删
void SLTPopBack(SLTNode** pHead)
{
assert(pHead && *pHead);
if ((*pHead)->next == NULL) //只有一个结点
{
free(*pHead);
*pHead = NULL;
}
else //不止一个节点
{
SLTNode* prev = *pHead;
SLTNode* rear = prev->next;
while (rear->next) //寻找最后一个结点和倒数第二个结点
{
prev = prev->next;
rear = rear->next;
}
free(rear);
rear = NULL;
prev->next = NULL;
}
}
//头删
void SLTPopFront(SLTNode** pHead)
{
assert(pHead && *pHead); //防止空指针解引用
SLTNode* cur = *pHead;
*pHead = cur->next;
free(cur);
cur = NULL;
}
//查找
SLTNode* SLTFind(SLTNode* head, SListDataType x)
{
SLTNode* cur = head;
while (cur)
{
if (cur->data == x) //cur指向的节点保存了x
return cur;
cur = cur->next;
}
return NULL;
}
//指定位置之前插入
void SLTInsert(SLTNode** pHead, SLTNode* pos, SListDataType x)
{
assert(pHead);
if (pos == *pHead) //插入的位置在第一个结点前
{
SLTPushFront(pHead, x);
}
else //插入的位置不在第一个结点前
{
SLTNode* prev = *pHead;
while (prev->next != pos) //寻找pos的前一个结点
{
prev = prev->next;
}
SLTNode* node = SLTCreateNode(x); //插入新结点
node->next = pos;
prev->next = node;
}
}
//指定位置删除
void SLTErase(SLTNode** pHead, SLTNode* pos)
{
assert(pHead && *pHead);
assert(pos);
if (pos == *pHead) //删除第一个结点
{
SLTPopFront(pHead);
}
else //删除其它结点
{
SLTNode* prev = *pHead;
while (prev->next != pos) //找pos的前一个结点
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
//指定位置之后插入
void SLTInsertAfter(SLTNode* pos, SListDataType x)
{
assert(pos);
SLTNode* node = SLTCreateNode(x);
node->next = pos->next;
pos->next = node;
}
//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos)
{
assert(pos && pos->next);
SLTNode* cur = pos->next;
pos->next = cur->next;
free(cur);
cur = NULL;
}
//销毁链表
void SListDesTroy(SLTNode** pHead)
{
assert(pHead && *pHead);
SLTNode* prev = *pHead;
while (prev) //销毁每一个节点
{
SLTNode* rear = prev->next;
free(prev);
prev = rear;
}
*pHead = NULL; //头指针置为空
}
SList.h
c
//结构体定义,函数声明
#pragma once
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
//单链表结构体定义
typedef int SListDataType;
typedef struct SListNode
{
SListDataType data;
struct SListNode* next;
}SLTNode;
//打印链表节点
void SLTPrint(SLTNode* head);
//尾插
void SLTPushBack(SLTNode** pHead, SListDataType x);
//头插
void SLTPushFront(SLTNode** pHead, SListDataType x);
//尾删
void SLTPopBack(SLTNode** pHead);
//头删
void SLTPopFront(SLTNode** pHead);
//查找
SLTNode* SLTFind(SLTNode* head, SListDataType x);
//指定位置前插入
void SLTInsert(SLTNode** pHead, SLTNode* pos, SListDataType x);
//指定位置删除
void SLTErase(SLTNode** pHead, SLTNode* pos);
//指定位置后插入
void SLTInsertAfter(SLTNode* pos, SListDataType x);
//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SListDesTroy(SLTNode** pHead);