前言
本人的技术笔记会优先发布在个人站点longyxblg.cn
顺序表的问题及思考
-
中间\头部的插入和删除,时间复杂度为 O(N)
-
空间扩容时,需要生成新空间,拷贝就空间中的数据,释放就空间。有不小的空间消耗。
-
空间扩容后的空间不一定会全部使用,会造成空间上的浪费
为解决上述问题,可以用到一种新的数据结构------链表
链表(Linked List)
链表也是一种线性表,但是链表的物理结构上是非顺序、非连续的,数据的逻辑顺序是通过链表中的指针链接次序实现的
单链表(Single linked list)
单链表是链表的一种分支,如图所示

可以接住生活中的火车来帮助理解,车厢是链接的

每节车厢都是独立存在的,增加删除不会影响其他车厢。
单链表的每个节点都是独立存在的,增加删除也不会影响其他节点(或结点)。
单链表节点的结构
单链表节点的结构可以通过一个结构体来实现,结构体中存放的是记录的数据和指向下个节点的指针。本文演示的手写的不带头单链表存放的数据类型为 int 。每个节点都是动态开辟的空间。
c
// 重命名方便修改
typedef int SLDataType;
typedef struct Node
{
// 存放的数据
SLDataType data;
// 指向下个节点的指针
struct Node* next;
}Node;
单链表的函数
头文件包含
c
#include<stdio.h> // 标准库函数
#include<stdlib.h> // 主要使用动态申请内存
#include<assert.h> // 主要使用断言
申请节点
每个节点都是动态申请的空间,将这个过程封装为一个函数,动态申请的内存空间存在于堆区。
函数结束后,这块空间不会被系统收回。
c
Node* BuyNode(SLDataType data)
{
//申请一个节点
//动态申请的内存空间存在于堆区。
//函数结束后, 这块空间不会被系统收回
Node* ps = (Node*)malloc(sizeof(Node));
if (!ps)
{
// 判断是否申请成功
perror("malloc error");
exit(1);
}
// 申请成功插入数值
ps->data = data;
ps->next = NULL;
return ps;
}
测试
c
int main()
{
// 测试申请节点
Node* n = BuyNode(1);
return 0;
}

插入节点
单链表的打印
从头节点开始遍历,遍历一个打印一个节点的数据,直到尾节点打印结束
c
void Print_Node(const Node** pphead)
{
// 打印整个链表
// 只是打印,用个 const 关键字
// 判空 传进来的这个 pphead 不能为 NULL
assert(pphead);
if (!(*pphead))
{
printf("当前链表没有数据\n");
return;
}
Node* pcur = *pphead; // 用 pcur 这个指针去遍历
while (pcur)
{
// 用 pcur->next 作为判断条件的话,就不会打印尾节点的数据了
printf("%d ", pcur->data);
pcur = pcur->next;
}
}
单链表头插
插入的节点将成为这个链表的头节点,这个节点指向原链表的头节点

如果这个链表是空的,那么插入的这个节点直接成为该链表的头节点,没有任何其他操作
c
void PushFront(Node** pphead, int data)
{
// 单链表的头插
// pphead 表示原链表的头节点
// 由于希望在函数内的操作影响到实参
// 传入的是 Node* 这个一级指针的地址
// 用 Node** 二级指针来接收
// 调用 BuyNode 函数申请一个节点
Node* newnode = BuyNode(data);
if (!pphead)
{
// pphead 是空说明链表是空的
// 那直接让这个新节点成为这个链表的头节点,不需要做其他操作
pphead = newnode;
return;
}
// 到这儿说明这不是一个空链表
// 插入的节点将成为这个链表的头节点
// 这个节点指向原链表的头节点
// -> 优先级大于 *
newnode->next = *pphead;
// 让这个新节点成为新的头节点
*pphead = newnode;
}
测试
c
int main()
{
// 测试头插节点
Node* n1 = BuyNode(1);
PushFront(&n1,2);
PushFront(&n1,3);
PushFront(&n1,4);
Print_Node(&n1);
return 0;
}

单链表尾插
遍历链表找到尾节点,让尾节点指向这个新插入的节点,新插入的节点成为尾节点
c
void PushBack(Node** pphead, int data)
{
// 单链表的尾插
// 需要先找到尾节点
// 尾节点的 next 没有指向,为 NULL
// 传进来的 newnode 不能是空
// 调用 BuyNode 函数申请一个节点
Node* newnode = BuyNode(data);
if (!pphead)
{
// pphead 是空说明链表是空的
// 那直接让这个新节点成为这个链表的头节点,不需要做其他操作
pphead = newnode;
return;
}
Node* ptail = *pphead; // 用新指针去便利链表
while (ptail->next)
{
// 继续向后遍历
ptail = ptail->next;
}
// 出循环说明找到了 next 为 NULL 的节点
// 就是尾节点
// 让尾节点的 next 指向这个新节点
ptail->next = newnode;
}
测试
c
int main()
{
// 测试尾插节点
Node* n1 = BuyNode(1);
PushBack(&n1,2);
PushBack(&n1,3);
PushBack(&n1,4);
Print_Node(&n1);
return 0;
}

链表查找
遍历这个链表查找
c
Node* Find(Node** pphead, int data)
{
// 单链表查找
// 判空,链表不为空才能进行查找
assert(pphead);
Node* pcur = *pphead; // 用 pcur 这个指针去遍历链表
while (pcur)
{
// 如果用 pcur->next 就不会查找最后一个节点
if (pcur->data == data)
{
// 返回这个节点的指针
return pcur;
}
}
// 出循环说明遍历完了链表
return NULL;
}
测试
c
int main()
{
// 测试查找函数
Node* n1 = BuyNode(1);
PushFront(&n1, 2);
Node* test1 = Find(&n1,2);
Node* test2 = Find(&n1,3);
return 0;
}

指定位置前插入
找到指定位置,让指定位置前的节点

c
void Push_Pos_Before(Node** pphead, Node* pos, SLDataType data)
{
// 指定位置前插入
// 判空,pos 和 pphead 都不能传空
assert(pos && pphead);
if (*pphead == pos)
{
// 这实际上就是头插
// 调用头插函数
PushFront(pphead, data);
return;
}
Node* pcur = *pphead; // 用 pcur 去遍历链表
Node* prev = *pphead; // 用 prev 来记录 pcur 前的一个节点
while (pcur)
{
if (pcur->data == pos->data)
{
// 找到了指定位置
// 申请一个节点
Node* newnode = BuyNode(data);
// 让指定位置前的这个节点指向这个新节点
prev->next = newnode;
// 让这个新节点指向指定位置节点
newnode->next = pcur;
break;
}
prev = pcur;// 记录 pcur 前的一个节点
pcur = pcur->next;
}
}
测试
c
int main()
{
// 测试指定位置前插入
Node* n1 = BuyNode(1);
PushFront(&n1, 2);
PushFront(&n1, 3);
PushFront(&n1, 4);
Print_Node(&n1);
printf("\n");
Node* test1 = Find(&n1, 2);
Push_Pos_Before(&n1, test1, 5);
Print_Node(&n1);
return 0;
}

指定位置后插入
新节点指向指定位置的节点指向的节点,指定位置节点指向这个新节点

c
void Push_Pos_After(Node* pos, SLDataType data)
{
// 指定位置后插入数据
// 传进来的 pos 不能为空
assert(pos);
// 申请新节点
Node* newnode = BuyNode(data);
// 先让这个节点指向指定位置后的那个节点
newnode->next = pos->next;
// 再让指定位置节点指向这个新节点
pos->next = newnode;
}
测试
c
int main()
{
// 测试指定位置后插入
Node* n1 = BuyNode(1);
Push_Pos_After(n1,2);
Push_Pos_After(n1,3);
Print_Node(&n1);
return 0;
}

删除节点
链表的每个节点都是一块动态申请的内存空间,删除节点要用到 free
头删
让链表的头节点指向的下一个节点成为这个链表的新头节点,释放原头节点
c
void PopFront(Node** pphead)
{
// 单链表的头删
// 判空,链表不为空才能进行删除,传进来的 pphead 也不能为 NULL
assert(pphead && (*pphead));
// 记录原头节点指向的下一个节点
Node* newhead = (*pphead)->next;
// 释放原头节点
free((*pphead));
// 让原头节点指向的下一个节点成为新的头节点
(*pphead) = newhead;
}
测试
c
int main()
{
// 测试头删
Node* n1 = BuyNode(1);
PushBack(&n1, 2);
PushBack(&n1, 3);
PushBack(&n1, 4);
Print_Node(&n1);
printf("\n");
PopFront(&n1);
Print_Node(&n1);
printf("\n");
PopFront(&n1);
Print_Node(&n1);
printf("\n");
PopFront(&n1);
Print_Node(&n1);
printf("\n");
PopFront(&n1);
Print_Node(&n1);
printf("\n");
PopFront(&n1);
Print_Node(&n1);
printf("\n");
return 0;
}

尾删
c
void PopBack(Node** pphead)
{
// 单链表尾删
// 判空,链表不为空才能进行删除,传进来的 pphead 也不能为 NULL
assert(pphead && (*pphead));
//找到当前链表的尾节点
Node* pcur = *pphead; // 用 pcur 去遍历链表
Node* ptail = pcur->next; // 用 ptail 去记录 pcur-next 指向的节点
// 处理只有一个节点的情况
if (!ptail)
{
free(*pphead);
*pphead = NULL;
return;
}
while (ptail->next)
{
pcur = pcur->next;
ptail = ptail->next;
}
// 出来说明 ptail->next = NULL ,这是尾节点
// 释放尾节点
free(ptail);
ptail = NULL;
//指向这个尾节点的节点的 next 置空
pcur->next = NULL;
}
测试
c
int main()
{
//测试尾删
Node* n1 = BuyNode(1);
PushBack(&n1, 2);
PushBack(&n1, 3);
PushBack(&n1, 4);
Print_Node(&n1);
printf("\n");
PopBack(&n1);
Print_Node(&n1);
printf("\n");
PopBack(&n1);
Print_Node(&n1);
printf("\n");
PopBack(&n1);
Print_Node(&n1);
printf("\n");
PopBack(&n1);
Print_Node(&n1);
printf("\n");
return 0;
}

指定位置删除
让指定位置前的节点指向指定位置后的节点,释放指点位置的节点

c
void Pop_Pos(Node** pphead, Node* pos)
{
// 指定位置删除
//判空, pos 和 pphead 都不能传空
assert(pphead && pos);
Node* pcur = *pphead; // 用 pcur 遍历链表
Node* prev = NULL; // 记录 pcur 前的节点
if (*pphead == pos)
{
// 如果删除的是头节点就直接调头删函数
PopFront(pphead);
return;
}
while (pcur)
{
if (pcur == pos)
{
// 让指定位置前的节点指向指定位置后的节点
prev->next = pcur->next;
// 释放指点位置的节点
free(pcur);
return;
}
prev = pcur;
pcur = pcur->next;
}
}
测试
c
int main()
{
Node* n1 = BuyNode(1);
PushBack(&n1, 2);
PushBack(&n1, 3);
PushBack(&n1, 4);
Print_Node(&n1);
printf("\n");
// 测试一直删头部节点的情况
printf("测试一直删头部节点\n");
Pop_Pos(&n1, n1);
Print_Node(&n1);
printf("\n");
Pop_Pos(&n1, n1);
Print_Node(&n1);
printf("\n");
Pop_Pos(&n1, n1);
Print_Node(&n1);
printf("\n");
Node* n2 = BuyNode(1);
PushBack(&n2, 2);
PushBack(&n2, 3);
PushBack(&n2, 4);
Print_Node(&n2);
printf("\n");
// 测试删除中间数据的情况
Node* find = Find(&n2, 2);
printf("测试删除中间节点\n");
Pop_Pos(&n2, find);
Print_Node(&n2);
printf("\n");
// 测试删除尾节点的情况
find = Find(&n2, 4);
printf("测试删除尾节点\n");
Pop_Pos(&n2, find);
Print_Node(&n2);
printf("\n");
return 0;
}
单链表的销毁
使用完成后将每一个动态申请的节点释放
c
void Destory(Node** pphead)
{
// 单链表的销毁
if (!(pphead && *pphead))
{
printf("当前已是空链表!\n");
return;
}
Node* pcur = *pphead; // 用 pcur 去遍历链表
Node* ptail = pcur->next; // 记录 pcur 的下一个节点
while (pcur)
{
// pcur 为空的时候说明尾节点也已经完成释放
// 记录下一节点
ptail = pcur->next;
// 释放当前节点
free(pcur);
pcur = ptail;
}
// 完成释放后将头节点指控
*pphead = NULL;
}
测试
c
int main()
{
Node* n1 = BuyNode(1);
PushBack(&n1, 2);
PushBack(&n1, 3);
PushBack(&n1, 4);
Print_Node(&n1);
printf("\n");
Destory(&n1);
Print_Node(&n1);
Destory(&n1);
return 0;
}

