> 文档中心 > 数据结构03 - 链表纸老虎 - 听起来吓人,实现起来很轻松的带头双向循环链表(6000字纯C语言带你理解链表)

数据结构03 - 链表纸老虎 - 听起来吓人,实现起来很轻松的带头双向循环链表(6000字纯C语言带你理解链表)


数据结构与算法

Lesson03 - 链表C语言实现(没有伪代码)


文章目录

  • 数据结构与算法
  • 前言
  • 1 什么是链表
    • 1.1 基本概念
    • 1.2 链表的类型
  • 二、无头单链表的实现(纯C语言)
    • 2.1 创建单个结点
    • 2.2 打印
    • 2.3 尾插
    • 2.4 尾删
    • 2.5 头插
    • 2.6 头删
    • 2.7 按值查找
    • 2.8 在pos位置之后插入 x
    • 2.9 在pos位置之前插入x
    • 2.10 删除pos位置之后的元素
  • 3 带头双向循环链表的实现(纯C语言)
    • 3.1 创建单个结点
    • 3.2 初始化链表,并返回一个头结点的指针
    • 3.3 打印链表
    • 3.4 尾插
    • 3.5 头插
    • 3.6 尾删
    • 3.7 头删
    • 3.8 按值查找 - 返回结点的位置
    • 3.9 在pos位置之前插入x
    • 3.10 删除pos位置的元素
  • //后记

前言

💘 Lesson03 主要讲解数据结构中的 - 链表,以及链表的C语言实现。

💘 本篇文章没有伪代码,帮助大家在理解顺序表的前提下,自己动手用C语言实现链表

1 什么是链表

1.1 基本概念

🏃 线性表的链式存储又称为链表
🏃 它是指通过一组任意的存储单元来存储线性表的数据结构
🏃 由于是任意结点,还得符合线性表的逻辑结构特点,因此每一个节点除了放自身的数据元素,还必须放一个指向其后继的指针

💘 类似于下图:

在这里插入图片描述

🏃 虽然在内存中它们是不连续的,但在逻辑上是相连的

1.2 链表的类型

💘相比较与顺序表,链表的类型就丰富多彩了

🏃 一个结点可以存很多信息,除了指向后继的指针,我们可以添加一个指向前驱的指针,于是可分成单向双向
🏃 如果我们添加一个哨兵位的头结点,可以将插入和删除等操作变得容易一点,以此可分为带头结点不带头结点
🏃 如果将尾结点的 next 指针指向头结点,这样就形成了一个循环结构,能够很好地找到头尾,因此可分为循环非循环

通过排列组合,可以组合出8中不同的链表,它们各有优缺点,本篇文章重点介绍单链表带头双向循环链表

💘 单链表

typedef int SListDataType;typedef struct SListNode{//存储结点数据SListDataType val;//指向下个结点的指针struct SListNode* next;}ListNode;

数据结构03 - 链表纸老虎 - 听起来吓人,实现起来很轻松的带头双向循环链表(6000字纯C语言带你理解链表)

💘 带头双向循环链表

typedef int SListDataType;typedef struct SListNode{SListDataType val;struct SListNode* next;struct SListNode* prev;}ListNode;

数据结构03 - 链表纸老虎 - 听起来吓人,实现起来很轻松的带头双向循环链表(6000字纯C语言带你理解链表)

二、无头单链表的实现(纯C语言)

2.1 创建单个结点

🏃 此接口为了方便增加新数据元素时,需要不断动态申请空间的问题

//创建单个结点ListNode* BuyListNode(SListDataType x){ListNode* tmp = (ListNode*)malloc(sizeof(ListNode));if (tmp == NULL){perror("BuyListNode::malloc");}tmp->next = NULL;tmp->val = x;}

2.2 打印

🏃 遍历整个链表

void ListPrint(ListNode* pl){ListNode* i = pl;//遍历整个链表,直到遇到NULLwhile (i){printf("%d ", pl->val);i = i->next;}printf("\n");}

2.3 尾插

🏃 先找到链表的最后一个结点,然后将新建的结点放在他的后面

//尾插void ListPushBack(ListNode** ppl, SListDataType x){ListNode* tmp = BuyListNode(x);//如果传过来的是NULL指针,说明是第一个结点if ((*ppl) == NULL){(*ppl) = tmp;return;}ListNode* tail= (*ppl);//寻找尾结点while (tail->next){tail= tail->next;}tail->next = tmp;}

2.4 尾删

🏃 找到最后一个结点,将其 free 掉
🏃 但是不能直接 free ,因为我们要将倒数第二个结点的 next 置成 NULL,因此我们需要多加一个结点,用来保存尾结点的前一个结点

void ListPopBack(ListNode** ppl){//如果是NULL指针,不必删除if ((*ppl) == NULL){return;}//如果只有一个结点,直接freeif ((*ppl)->next == NULL){free((*ppl));(*ppl) = NULL;return;}//定义一个prev记录前一个结点ListNode* prev = NULL;ListNode* tail = *ppl;while (tail->next){prev = tail;tail = tail->next;}free(tail);prev->next = NULL;}

2.5 头插

🏃 创建一个新结点,连在原来链表的前面即可

//从头部插入一个节点void ListPushFront(SListNode** pplist, SListDateType x){//即使传的NULL指针也没关系SListNode* newnode = BuySListNode(x);newnode->next = *pplist;//修改指针,使其指向新的第一个结点(*pplist) = newnode;newnode = NULL;}

2.6 头删

🏃 free 第一个结点,修改指针
🏃 处理一个NULL指针情况即可

//从头部删除一个节点void SListPopFront(SListNode** pplist){if (*pplist == NULL) return;//修改第一个结点的指针SListNode* tmp = *pplist;(*pplist) = tmp->next;free(tmp);tmp = NULL;}

2.7 按值查找

🏃 遍历整个链表,找到返回结点指针,没有找到返回 NULL

//按值查找,返回找到后的地址SListNode* SListFind(SListNode* plist, SListDateType x){while (plist){if (plist->date == x){return plist;}plist = plist->next;}return NULL;}

2.8 在pos位置之后插入 x

🏃 一定要先修改 newnode 的指针,如果先修改 pos 的指针,那么整个链表就会断开

//在pos位置之后插入xvoid SListInsertAfter(SListNode* pos, SListDateType x){assert(pos);SListNode* newnode = BuySListNode(x);//先修改newnode的指针newnode->next = pos->next;pos->next = newnode;}

2.9 在pos位置之前插入x

🏃

void SListInsert(SListNode** pplist, SListNode* pos, SListDateType x){assert(pos);SListNode* newnode = BuySListNode(x);//如果是第一个结点,相当于头插if ((*pplist) == pos){newnode->next = *pplist;*pplist = newnode;}else{//先找到pos位置的前一个结点SListNode* prev = *pplist;while (prev->next != pos){prev = prev->next;}//找到后修改指针newnode->next = pos;prev->next = newnode;}

2.10 删除pos位置之后的元素

🏃 注意讨论 pos 为NULL,和 pos 为唯一结点的情况

void SListEraseAfter(SListNode* pos){if (pos == NULL && pos->next == NULL){return;}else{//记录后一个结点SListNode* tmp = pos->next;//让pos结点指向后一个结点的后一个结点pos->next = pos->next->next;free(tmp);tmp = NULL;}}

3 带头双向循环链表的实现(纯C语言)

3.1 创建单个结点

🏃 此接口为了方便增加新数据元素时,需要不断动态申请空间的问题

//创建一个新结点ListNode* BuyListNode(LTDataType x){ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));if (newnode == NULL){perror("BuyListNode::malloc");}newnode->next = NULL;newnode->prev = NULL;newnode->data = x;return newnode;}

3.2 初始化链表,并返回一个头结点的指针

🏃 创建一个头结点

ListNode* ListInit(){ListNode* head = BuyListNode(0);//由于是循环链表,当只有头结点时,prev和next都指向自己head->next = head;head->prev = head;return head;}

3.3 打印链表

🏃 遍历整个链表

void ListPrint(ListNode* phead){ListNode* cur = phead->next;//循环链表判断结尾的标志是最后一个结点的next指向headwhile (cur != phead){printf("%d->", cur->data);cur = cur->next;}printf("NULL\n");}

3.4 尾插

🏃 找到最后一个结点,然后插入新结点

void ListPushBack(ListNode* phead, LTDataType x){assert(phead);ListNode* newnode = BuyListNode(x);//不必遍历整个链表去找尾结点,头结点的prev即使尾结点ListNode* tail = phead->prev;//修改指针,使其还是一个循环链表tail->next = newnode;newnode->prev = tail;newnode->next = phead;phead->prev = newnode;}

3.5 头插

🏃 创建一个新结点,连在原来链表的前面即可

//从头部插入一个结点xvoid ListPushFront(ListNode* phead, LTDataType x){assert(phead);ListNode* newnode = BuyListNode(x);//相当于在头结点和第二个结点之间插入新结点newnode->prev = phead;newnode->next = phead->next;phead->next->prev = newnode;phead->next = newnode;}

3.6 尾删

🏃 head 的 prev 即是尾

//从尾部删除一个结点void ListPopBack(ListNode* phead){assert(phead);//如果只有头结点,不必删除if (phead->next == phead) return;//记录尾结点以及尾结点的前驱ListNode* tail = phead->prev;ListNode* p_tail = tail->prev;free(tail);tail = NULL;//修改指针使其还是循环链表phead->prev = p_tail;p_tail->next = phead;}

3.7 头删

🏃 记录第一个结点,以及第二个结点,方便修改指针

void ListPopFront(ListNode* phead){assert(phead);if (phead->next == phead) return;//记录第一个结点,以及第二个结点,方便修改指针ListNode* first = phead->next;ListNode* n_first = first->next;free(first);first = NULL;phead->next = n_first;n_first->prev = phead;}

3.8 按值查找 - 返回结点的位置

🏃 遍历链表,寻找 x

ListNode* ListFind(ListNode* phead, LTDataType x){assert(phead);ListNode* cur = phead->next;//注意尾结点的条件while (cur != phead){if (cur->data == x){return cur;}cur = cur->next;}return NULL;}

3.9 在pos位置之前插入x

🏃 注意修改指针的顺序

void ListInsert(ListNode* pos, LTDataType x){assert(pos);ListNode* prev = pos->prev;ListNode* newnode = BuyListNode(x);prev->next = newnode;newnode->prev = prev;newnode->next = pos;pos->prev = newnode;}

3.10 删除pos位置的元素

🏃 删除结点一定要注意不能让链表断开

void ListErase(ListNode* pos){assert(pos);ListNode* prev = pos->prev;ListNode* next = pos->next;prev->next = next;next->prev = prev;free(pos);}

💘 综上就是关于链表最核心的知识,对您有帮助的老铁,还望关注、点赞、收藏一键三连哦~~~

//后记

🏃 以后每周会更新一到两篇学习数据结构的笔记,用来记录自己的学习历程,复习巩固所学的知识。
🏃 文中概念或者代码有任何错误的地方,希望大佬们在评论区指出,本人会虚心接受并且改正。