本文目录
一、什么是链表?
1. 概念
链表是一种基础的数据结构,采用了链式存储的方式。它由一系列节点组成,每个节点包含两部分:数据域和指针域。数据域存储实际数据;指针域指向下一个节点,形成链式结构。
typedef int SLTDataType; // 使用typedef对数据域的类型进行定义别名,增加后续代码的可维护性
typedef struct SListNode // 链表节点
{
//
SLTDataType val; // 数据域,存储具体的数据
struct SListNode* next; // 指针域,指向下一个节点
}SLTNode; // 对链表节点进行定义别名,使代码更加简洁
2. 优缺点分析
- 优点:
- 动态扩展:无需预分配内存,可根据需求动态增加或删除节点
- 高效增删:仅需修改指针指向节点,无需移动数据
- 缺点:
- 访问低效:无法随机访问,需顺序遍历节点
- 空间开销:每个节点需要额外存储指针
- 内存碎片:在堆中频繁的
malloc
和free
会使内存分散,导致堆中存在大量不连续的小块空闲区域,形成外碎片
二、单链表的实现
虚拟头节点
如果没有虚拟头节点,那么在插入和删除时我们就需要提前判断一下链表是否为空,而且在释放头节点时会需要用到二级指针,有点麻烦。这里增加一个虚拟头节点可以使每一个节点都具有前驱节点,操作更加统一方便。
因为后续多个插入函数都需要申请节点,所以把申请节点写成函数,方便后续操作
SLTNode* BuyNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL) // 判断申请是否成功
{
perror("malloc fail\n");
exit(1);
}
newnode->val = x;
newnode->next = NULL;
return newnode;
}
创建虚拟头节点
SLTNode* InitLink() // 返回类型是链表节点的指针,调用时记得用一个变量去接收该函数返回的指针
{
// 创建虚拟头节点
SLTNode* phead = BuyNode(0);
return phead;
}
1. 尾插
尾插是让链表的最后一个节点指向新创建的节点
操作步骤:
- 创建新节点
- 找到链表的尾节点
- 将尾节点的后继指针修改成新节点
void SLTPushBack(SLTNode* phead, SLTDataType x)
{
// 创建新节点
SLTNode* newnode = BuyNode(x);
// 找尾节点
SLTNode* tail = phead;
while (tail->next)
tail = tail->next;
tail->next = newnode;
}
打印链表
那么我们怎么来验证插入是否成功呢?通过IDE去看变量的值吗?
这么操作并不明显,也不直观,我的建议是通过打印法去调试。这么做我们能直观得看到链表到底是什么样的
void SLTPrint(SLTNode* phead)
{
SLTNode* cur = phead->next;
// 遍历链表
while (cur)
{
printf("%d->", cur->val);
cur = cur->next;
}
printf("NULL\n"); // 当节点为空时补上NULL
}
2. 头插
头插是将新节点插入到链表的第一个位置
当然,因为我们前面创建了虚拟头节点,所以操作尤其简单:
- 创建新节点
- 将newnode的后继指针指向原本的第一个节点
- 将phead的后继指针指向newnode
有的同学可能想问,操作2和操作3能反过来吗? 答案是显而易见的,不可以!!!
如果我们先把phead的后继节点指向了newnode,那么就没有指针指向原本的第一个节点了,那么我们该如何去查找原本的头节点呢?
当然,你也可以选择使用cur去储存原本的第一个节点,再去执行3和2,但这样明显更麻烦了,有简单的操作为什么不用呢?
void SLTPushFront(SLTNode* phead, SLTDataType x)
{
SLTNode* newnode = BuyNode(x);
newnode->next = phead->next;
phead->next = newnode;
}
3. 尾删
尾删是将链表的最后一个节点删除
操作步骤:
- 先找到链表的倒数第二个节点
- 释放链表的最后一个节点
- 将倒数第二个节点的后继指针置为空
void SLTPopBack(SLTNode* phead)
{
// 这个尾指的是尾删之后的新尾
SLTNode* tail = phead;
if (tail->next == NULL) // 判断链表是否为空
{
perror("SList is empty!\n");
return 0;
}
while (tail->next->next) // 寻找新尾
tail = tail->next;
free(tail->next); // 释放旧尾
tail->next = NULL; // 将新尾的后继指针置为NULL
}
有的同学可能会问为什么循环里的条件是tail->next->next
其实这也很简单,当当前节点的后继的后继节点仍为非空的时候,那么从当前节点开始算,至少还存在三个节点。我们要寻找的新尾节点是倒数第二个节点,如果从当前节点开始算,只剩两个节点的时候,那么我们就找到了新尾,循环也就此停止。
4. 头删
头删是删除第一个节点
其实头删只需要将虚拟头节点的下一个节点置为原头节点的后继节点即可,但是因为需要删除原头节点,所以这里使用一个head去记录原本的头节点,再执行前面的操作,最后释放原头节点
void SLTPopFront(SLTNode* phead)
{
if (phead->next == NULL) // 仍然是先判断链表是否为空
{
perror("SList is empty");
return 0;
}
SLTNode* head = phead->next;
phead->next = head->next;
free(head);
}
5. 查找
查找只需要遍历链表,对比每个节点的数据域,如果数据域相同返回该节点的指针;如果遍历完毕仍未找到则返回空指针
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
// 因为带了个虚拟头节点,所以从虚拟头节点的下一个开始
SLTNode* cur = phead->next;
while (cur)
{
if (cur->val == x)
return cur;
cur = cur->next;
}
printf("Not Found!\n");
return NULL;
}
6. 在指定位置之前插入
首先要找到pos节点的前驱节点,用cur记录,再将newnode的后继指向pos节点,最后将cur的后继指向newnode。至于为什么是这个操作顺序,参考前面的头插,要是实在不理解的话可以画图看看不同顺序是否会使链表断连,这里便不做过多叙述
void SLTInsert(SLTNode* phead, SLTNode* pos, SLTDataType x)
{
SLTNode* cur = phead->next;
SLTNode* newnode = BuyNode(x);
while (cur->next != pos && cur)
{
cur = cur->next;
}
newnode->next = pos;
cur->next = newnode;
}
7. 删除pos节点
找到pos节点的前驱,将前驱节点的后继置为pos节点的后继节点,删除pos节点。
void SLTErase(SLTNode* phead, SLTNode* pos)
{
SLTNode* cur = phead->next;
while (cur->next != pos)
{
cur = cur->next;
}
if (cur == NULL) // 防止有人传了个莫名其妙的node指针,还是确认一下吧
{
perror("The node to be deleted does not exist!\n");
exit(1);
}
cur->next = pos->next;
free(pos);
}
8. 在指定位置之后插入数据
- 创建新节点
- 新节点的后继为pos节点的后继
- pos节点的后继为新节点
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
SLTNode* newnode = BuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
9. 删除pos之后的节点
- 用cur记录pos的后继节点
- 判断cur是否为空
- 将pos节点的后继设为cur的后继节点
- 释放cur
void SLTEraseAfter(SLTNode* pos)
{
SLTNode* cur = pos->next;
if (cur == NULL)
{
perror("The next node to be deleted does not exist!\n");
exit(1);
}
pos->next = cur->next;
free(cur);
}
10. 销毁链表
从虚拟头节点开始释放每个节点
// 这里因为传的是一级指针,所以只能释放掉链表中的内容,并不能对头节点置空
// 所以在上层调用函数中,还需手动将虚拟头节点置空
// 当然,也可以使用二级指针直接将所有操作都放在销毁函数中执行
void SLTDesTroy(SLTNode* phead)
{
SLTNode* cur = phead;
SLTNode* prev = phead;
while(cur)
{
prev = cur;
cur = cur->next;
free(prev);
}
}
三、源码
头文件和源文件分离,加上一个测试文件
SList.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
typedef int SLTDataType;
typedef struct SListNode
{
//
SLTDataType val;
struct SListNode* next;
}SLTNode;
// 初始化一个带虚拟头节点的单链表
SLTNode* InitLink();
// 打印单链表
void SLTPrint(SLTNode* phead);
// 尾插
void SLTPushBack(SLTNode* phead, SLTDataType x);
// 头插
void SLTPushFront(SLTNode* phead, SLTDataType x);
// 尾删
void SLTPopBack(SLTNode* phead);
// 头删
void SLTPopFront(SLTNode* phead);
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//在指定位置之前插入数据
void SLTInsert(SLTNode* phead, SLTNode* pos, SLTDataType x);
//删除pos节点
void SLTErase(SLTNode* phead, SLTNode* pos);
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SLTDesTroy(SLTNode* phead);
SList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
SLTNode* BuyNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail\n");
exit(1);
}
newnode->val = x;
newnode->next = NULL;
return newnode;
}
SLTNode* InitLink()
{
// 创建虚拟头节点
SLTNode* phead = BuyNode(0);
return phead;
}
void SLTPrint(SLTNode* phead)
{
SLTNode* cur = phead->next;
while (cur)
{
printf("%d->", cur->val);
cur = cur->next;
}
printf("NULL\n");
}
void SLTPushBack(SLTNode* phead, SLTDataType x)
{
SLTNode* newnode = BuyNode(x);
// 找尾节点
SLTNode* tail = phead;
while (tail->next)
tail = tail->next;
tail->next = newnode;
}
void SLTPushFront(SLTNode* phead, SLTDataType x)
{
SLTNode* newnode = BuyNode(x);
newnode->next = phead->next;
phead->next = newnode;
}
void SLTPopBack(SLTNode* phead)
{
// 这个尾指的是尾删之后的新尾
SLTNode* tail = phead;
if (tail->next == NULL)
{
perror("SList is empty!\n");
return 0;
}
while (tail->next->next)
tail = tail->next;
free(tail->next);
tail->next = NULL;
}
void SLTPopFront(SLTNode* phead)
{
if (phead->next == NULL)
{
perror("SList is empty");
return 0;
}
SLTNode* head = phead->next;
phead->next = head->next;
free(head);
}
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
// 因为带了个虚拟头节点,所以从虚拟头节点的下一个开始
SLTNode* cur = phead->next;
while (cur)
{
if (cur->val == x)
return cur;
cur = cur->next;
}
printf("Not Found!\n");
return NULL;
}
void SLTInsert(SLTNode* phead, SLTNode* pos, SLTDataType x)
{
SLTNode* cur = phead->next;
SLTNode* newnode = BuyNode(x);
while (cur->next != pos && cur)
{
cur = cur->next;
}
newnode->next = pos;
cur->next = newnode;
}
void SLTErase(SLTNode* phead, SLTNode* pos)
{
SLTNode* cur = phead->next;
while (cur->next != pos)
{
cur = cur->next;
}
if (cur == NULL)
{
perror("The node to be deleted does not exist!\n");
exit(1);
}
cur->next = pos->next;
free(pos);
}
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
SLTNode* newnode = BuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
void SLTEraseAfter(SLTNode* pos)
{
SLTNode* cur = pos->next;
if (cur == NULL)
{
perror("The next node to be deleted does not exist!\n");
exit(1);
}
pos->next = cur->next;
free(cur);
}
// 这里因为传的是一级指针,所以只能释放掉链表中的内容,并不能对头节点置空
// 所以在上层调用函数中,还需手动将虚拟头节点置空
// 当然,也可以使用二级指针直接将所有操作都放在销毁函数中执行
void SLTDesTroy(SLTNode* phead)
{
SLTNode* cur = phead;
SLTNode* prev = phead;
while(cur)
{
prev = cur;
cur = cur->next;
free(prev);
}
}
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include "SList.h"
void test_SLTPushBack()
{
SLTNode* phead = InitLink();
SLTPushBack(phead, 1);
SLTPushBack(phead, 2);
SLTPushBack(phead, 3);
SLTPushBack(phead, 4);
SLTPrint(phead);
}
void test_SLTPushFront()
{
SLTNode* phead = InitLink();
SLTPushFront(phead, 1);
SLTPushFront(phead, 2);
SLTPushFront(phead, 3);
SLTPushFront(phead, 4);
SLTPrint(phead);
}
void test_SLTPopBack()
{
SLTNode* phead = InitLink();
SLTPushBack(phead, 1);
SLTPushBack(phead, 2);
SLTPushBack(phead, 3);
SLTPushBack(phead, 4);
SLTPrint(phead);
SLTPopBack(phead);
SLTPrint(phead);
SLTPopBack(phead);
SLTPopBack(phead);
SLTPopBack(phead);
SLTPrint(phead);
}
void test_SLTPopFront()
{
SLTNode* phead = InitLink();
SLTPushBack(phead, 1);
SLTPushBack(phead, 2);
SLTPushBack(phead, 3);
SLTPushBack(phead, 4);
SLTPrint(phead);
SLTPopFront(phead);
SLTPrint(phead);
SLTPopFront(phead);
SLTPopFront(phead);
SLTPopFront(phead);
SLTPrint(phead);
}
void test_SLTFind()
{
SLTNode* phead = InitLink();
SLTPushBack(phead, 1);
SLTPushBack(phead, 2);
SLTPushBack(phead, 3);
SLTPushBack(phead, 4);
SLTPrint(phead);
SLTNode* cur1 = SLTFind(phead, 3);
SLTPrint(cur1);
SLTNode* cur2 = SLTFind(phead, 5);
SLTPrint(cur2);
}
void test_SLTInsert()
{
SLTNode* phead = InitLink();
SLTPushBack(phead, 1);
SLTPushBack(phead, 2);
SLTPushBack(phead, 3);
SLTPushBack(phead, 4);
SLTPrint(phead);
SLTNode* cur = SLTFind(phead, 3);
SLTInsert(phead, cur, 6);
SLTPrint(phead);
}
void test_SLTErase()
{
SLTNode* phead = InitLink();
SLTPushBack(phead, 1);
SLTPushBack(phead, 2);
SLTPushBack(phead, 3);
SLTPushBack(phead, 4);
SLTPrint(phead);
SLTNode* cur = SLTFind(phead, 3);
SLTErase(phead, cur);
SLTPrint(phead);
}
void test_SLTInsertAfter()
{
SLTNode* phead = InitLink();
SLTPushBack(phead, 1);
SLTPushBack(phead, 2);
SLTPushBack(phead, 3);
SLTPushBack(phead, 4);
SLTPrint(phead);
SLTNode* cur = SLTFind(phead, 3);
SLTInsertAfter(cur, 6);
SLTPrint(phead);
}
void test_SLTDesTroy()
{
SLTNode* phead = InitLink();
SLTPushBack(phead, 1);
SLTPushBack(phead, 2);
SLTPushBack(phead, 3);
SLTPushBack(phead, 4);
SLTPrint(phead);
SLTDesTroy(phead);
phead = NULL;
SLTPrint(phead);
}
int main()
{
//test_SLTPushBack();
//test_SLTPushFront();
//test_SLTPopBack();
//test_SLTPopFront();
//test_SLTFind();
//test_SLTInsert();
//test_SLTErase();
//test_SLTInsertAfter();
test_SLTDesTroy();
return 0;
}