当前位置: 首页 > news >正文

【数据结构】线性表--链表(二)

【数据结构】线性表--链表(二)

  • 一.前情回顾
  • 二.知识补充
    • 1.单向链表,双向链表
    • 2.带头结点,不带头结点
    • 3.循环链表,不循环链表
  • 三.带头双向循环链表的实现
    • 1.申请新结点:
    • 2.链表初始化:
    • 3.尾插函数:
    • 4.头插函数:
    • 5.头删函数:
    • 6.尾删函数:
    • 7.查找函数:
    • 8.在pos结点前插入函数:
    • 9.删除pos结点的值函数:
    • 10.判空函数:
    • 11.销毁函数:
  • 四.总结
    • 1.头文件(声明链表的结构,操作等,起到目录作用):
    • 2.源文件(具体实现各种操作):
    • 3.测试文件(对各个函数功能进行测试):

一.前情回顾

上篇文章主要讲述了单链表的特点及各种增删查改等各种操作,我们会发现单链表在某些操作中需要寻找前驱结点,然而单链表中并没有直接指向前驱结点的指针,这样使得有些操作会有点麻烦,因此可以增加一个指向前驱结点的指针域,成为双链表。

二.知识补充

同时我们可以在链表的第一个结点前增加一个哨兵位的头结点,为了操作的统一和方便而设立。它的数据域并不保存实际的有效数据,指针域指向链表的第一个结点。
除此之外,链表还可分为循环链表和不循环链表。循环链表的最后一个结点又指向头结点,不循环链表的尾结点指向空。
因此链表总共可分为三类:

1.单向链表,双向链表

在这里插入图片描述

2.带头结点,不带头结点

在这里插入图片描述

3.循环链表,不循环链表

在这里插入图片描述
所以以上三类可以任意结合出来八种链表,上篇文章讲述的单链表具体是不带头单向不循环链表。
本篇文章则要讲述带头双向循环链表。

三.带头双向循环链表的实现

tips:涉及增删查改的各种操作时,可以自己画图看看如何修改各个指针的指向,便于理解。

1.申请新结点:

申请新结点即先用malloc函数申请一块结点大小的空间,然后将数值赋给数值域,指针都置为空,然后将新结点返回。
malloc函数不清楚的可以去看我之前的C语言动态内存管理那篇文章: https://blog.csdn.net/2401_85032912/article/details/142744593?spm=1001.2014.3001.5501)

//申请新结点
ListNode* BuyListNode(LTDataType x)
{ListNode* NewNode = (ListNode*)malloc(sizeof(ListNode));NewNode->data = x;NewNode->prev = NULL;NewNode->next = NULL;return NewNode;
}

2.链表初始化:

链表初始化需要先申请一个头结点,然后将头结点的prev指针和next指针均指向自己(因为是双向链表),数值域可以给一个不具有实际意义的值。
在这里插入图片描述

初始化方式有两种,第一种是通过传二级指针修改头结点;第二种是直接申请新结点,修改完之后作为返回值返回。

//初始化函数
ListNode* ListInit()
{//初始链表为空时,需将头结点的prev指针和next指针都指向自己ListNode* phead = BuyListNode(-1);phead->next = phead->prev = phead;return phead;
}
//或:
/*
void ListInit(ListNode** pphead)
{//初始链表为空时,需将头结点的prev指针和next指针都指向自己*pphead = BuyListNode(-1);(*pphead)->next = (*pphead)->prev = *pphead;
}
*/

3.尾插函数:

因为可以通过头结点找到第一个有效结点,所以不需要传二级指针(后续操作同理)。
双向链表尾插时不需要从头遍历到尾结点,因为头结点的prev指针即指向最后一个结点。
如图:
在这里插入图片描述
因此在尾插时,只需要通过头结点找到最后一个结点,将新结点的prev指针指向最后一个结点;next指针指向头结点;让最后一个结点的next指针指向新的结点;将头结点的prev指针指向新的结点。

//尾插函数
void ListPushBack(ListNode* phead, LTDataType x)
{assert(phead); ListNode* newnode = BuyListNode(x);ListNode* tail = phead->prev;//找到最后一个结点newnode->prev = tail;//新结点的prev指向最后一个结点newnode->next = phead;//新结点的next指向头结点tail->next = newnode;//最后一个结点的next指向新结点phead->prev = newnode;//头结点的prev指向新结点//此时新结点成为当前链表的最后一个结点
}

此时新结点就变成最后一个结点。

4.头插函数:

头插时先找到第一个结点,将新结点的next指针指向第一个结点,prev指针指向头结点,再第一个结点的next指针指向修改为新结点,最后修改头结点的next指针指向的结点。

//头插函数
void ListPushFront(ListNode* phead, LTDataType x)
{assert(phead);ListNode* newnode = BuyListNode(x);ListNode* first = phead->next;//找到第一个结点newnode->next = first;newnode->prev = phead;first->prev = newnode;phead->next = newnode;
}

因为将第一个结点找出来了,所以不需要考虑修改指针的先后顺序,否则需要考虑顺序问题。

5.头删函数:

头删时注意链表为空时不能删除。不为空时,找出第一个结点和第二个结点,将头结点的next指针指向第二个结点,将第二个结点的prev指针指向头结点,然后释放第一个结点指向的空间,指针置为空。

//头删函数
void ListPopFront(ListNode* phead)
{assert(phead);//链表为空时不能删除(或者使用断言)if (phead->next == phead)return;else{ListNode* first = phead->next;//找到第一个结点ListNode* second = first->next;//找到第二个结点phead->next = second;second->prev = phead;free(first);first = NULL;}
}

6.尾删函数:

链表为空时同样不能删除。不为空时找出最后一个结点和倒数第二个结点,将头结点的prev指针指向倒数第二个结点,倒数第二个结点的next指针指向头结点,释放最后一个结点,并置为空。

//尾删函数
void ListPopBack(ListNode* phead)
{assert(phead);//链表为空时不能删除(或者使用断言)if (phead->next == phead)return;else{ListNode* tail = phead->prev;//找到最后一个结点ListNode* prev = tail->prev;//找到倒数第二个结点phead->prev = prev;prev->next = phead;free(tail);tail = NULL;}
}

7.查找函数:

链表为空时直接返回NULL,链表不为空时,从第一个结点开始遍历到最后一个结点,若结点的值与查找的值相等,返回该结点,否则返回NULL。

ListNode* ListFind(ListNode* phead, LTDataType x)
{assert(phead);//链表为空时,返回NULLif (phead->next == phead)return NULL;else{ListNode* cur = phead->next;while (cur != phead){if (cur->data == x)return cur;elsecur = cur->next;}return NULL;}
}

8.在pos结点前插入函数:

首先申请一个新结点,然后找到pos结点的前一个结点,将前一个结点的next指针指向新结点,pos结点的prev指针也指向新结点,新结点的prev指针指向前一个结点,next指针指向pos结点。

//在pos位置前插入结点
void ListInsert(ListNode* pos, LTDataType x)
{assert(pos);ListNode* newnode = BuyListNode(x);ListNode* prev = pos->prev;//找到前一个结点prev->next = newnode;pos->prev = newnode;newnode->prev = prev;newnode->next = pos;
}

实现完该函数之后,头插、尾插函数可以直接复用该函数。
头插即在头结点的next位置插入,可以直接写成ListInsert(phead->next, x);
尾插即在头结点前插入,可以直接写成ListInsert(phead, x);

9.删除pos结点的值函数:

删除pos结点时,如果该链表仅剩最后一个头结点,不能删除。
其余情况先找到pos结点的前一个结点和后一个结点,将前一个结点的next指针指向后一个结点,后一个结点的prev指针指向前一个结点,然后释放pos结点,并置为空。

//删除pos位置的结点
void ListErase(ListNode* pos)
{assert(pos);//如果该链表仅剩最后一个头结点,不能删除if (pos->next == pos)return;ListNode* prev = pos->prev;ListNode* next = pos->next;prev->next = next;next->prev = prev;free(pos);pos = NULL;
}

实现完该函数后,头删和尾删函数即可复用该函数。
头删即把头结点的next指针指向的结点删除可以直接写成 ListErase(phead->next);
尾删即把头结点的prev指针指向的结点删除,可以直接写成ListErase(phead->prev);

10.判空函数:

判空函数很简单,如果头结点的next指针指向的结点还是自己即为空,返回true,否则不为空,返回false。

//判空函数
bool ListEmpty(ListNode* phead)
{if (phead->next == phead)return true;return false;
}

11.销毁函数:

销毁函数比较简单,找到第一个结点之后,进入循环,找到下一个结点,然后释放当前结点,直到结点==头结点退出循环,最后释放头结点,并置为空。

//销毁函数
void ListDestory(ListNode* phead)
{assert(phead);ListNode* cur = phead->next;while (cur != phead){ListNode* tmp = cur;cur = cur->next;free(tmp);tmp = NULL;}free(phead);phead = NULL;
}

四.总结

带头双向循环链表可能结构稍微复杂,但是优点是在任意位置插入删除结点的时间复杂度都是O(1), 缺点是以结点为存储单位,不支持随机访问。
以下是全部源码实现:

1.头文件(声明链表的结构,操作等,起到目录作用):

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>typedef int LTDataType;
typedef struct ListNode
{LTDataType data;//存放的数据元素struct ListNode* prev;//前驱指针struct ListNode* next;//后继指针
}ListNode;
//C++中双向链表叫List,所以起名也叫List//申请新结点
ListNode* BuyListNode(LTDataType x);//打印函数(方便调试观看)
void PrintList(ListNode* phead);//初始化函数
ListNode* ListInit();
//或者:void ListInit(ListNode** pphead);//销毁函数(通过头结点可以链表第一个有效节点进行修改,因此不需要传二级指针)
void ListDestory(ListNode* phead);//尾插函数
void ListPushBack(ListNode* phead, LTDataType x);//头插函数
void ListPushFront(ListNode* phead, LTDataType x);//头删函数
void ListPopFront(ListNode* phead);//尾删函数
void ListPopBack(ListNode* phead);//查找函数
ListNode* ListFind(ListNode* phead, LTDataType x);//在pos位置前插入结点
void ListInsert(ListNode* pos, LTDataType x);//删除pos位置的结点
void ListErase(ListNode* pos);//判空函数
bool ListEmpty(ListNode* phead);

2.源文件(具体实现各种操作):

#include"List.h"//申请新结点
ListNode* BuyListNode(LTDataType x)
{ListNode* NewNode = (ListNode*)malloc(sizeof(ListNode));NewNode->data = x;NewNode->prev = NULL;NewNode->next = NULL;return NewNode;
}//打印函数(方便调试观看)
void PrintList(ListNode* phead)
{ListNode* cur = phead->next;while (cur != phead){printf("%d ", cur->data);cur = cur->next;}printf("NULL\n");
}//初始化函数
ListNode* ListInit()
{//初始链表为空时,需将头结点的prev指针和next指针都指向自己ListNode* phead = BuyListNode(-1);phead->next = phead->prev = phead;return phead;
}
//或:
/*void ListInit(ListNode** pphead)
{//初始链表为空时,需将头结点的prev指针和next指针都指向自己*pphead = BuyListNode(-1);(*pphead)->next = (*pphead)->prev = *pphead;
}*///销毁函数
void ListDestory(ListNode* phead)
{assert(phead);ListNode* cur = phead->next;while (cur != phead){ListNode* tmp = cur;cur = cur->next;free(tmp);tmp = NULL;}free(phead);phead = NULL;
}//尾插函数
void ListPushBack(ListNode* phead, LTDataType x)
{assert(phead); //ListNode* newnode = BuyListNode(x);//ListNode* tail = phead->prev;//找到最后一个结点//newnode->prev = tail;//新结点的prev指向最后一个结点//newnode->next = phead;//新结点的next指向头结点//tail->next = newnode;//最后一个结点的next指向新结点//phead->prev = newnode;//头结点的prev指向新结点//或者ListInsert(phead, x);
}//头插函数
void ListPushFront(ListNode* phead, LTDataType x)
{assert(phead);//ListNode* newnode = BuyListNode(x);//ListNode* first = phead->next;//找到第一个结点//newnode->next = first;//newnode->prev = phead;//first->prev = newnode;//phead->next = newnode;//或者ListInsert(phead->next, x);
}//头删函数
void ListPopFront(ListNode* phead)
{assert(phead);链表为空时不能删除(或者使用断言)//if (phead->next == phead)//	return;//else//{//	ListNode* first = phead->next;//找到第一个结点//	ListNode* second = first->next;//找到第二个结点//	phead->next = second;//	second->prev = phead;//	free(first);//	first = NULL;//}//或者ListErase(phead->next);
}//尾删函数
void ListPopBack(ListNode* phead)
{assert(phead);链表为空时不能删除(或者使用断言)//if (phead->next == phead)//	return;//else//{//	ListNode* tail = phead->prev;//找到最后一个结点//	ListNode* prev = tail->prev;//找到倒数第二个结点//	phead->prev = prev;//	prev->next = phead;//	free(tail);//	tail = NULL;//}//或者ListErase(phead->prev);
}//查找函数
ListNode* ListFind(ListNode* phead, LTDataType x)
{assert(phead);//链表为空时,返回NULLif (phead->next == phead)return NULL;else{ListNode* cur = phead->next;while (cur != phead){if (cur->data == x)return cur;elsecur = cur->next;}return NULL;}
}//在pos位置前插入结点
void ListInsert(ListNode* pos, LTDataType x)
{assert(pos);ListNode* newnode = BuyListNode(x);ListNode* prev = pos->prev;//找到前一个结点prev->next = newnode;pos->prev = newnode;newnode->prev = prev;newnode->next = pos;
}//删除pos位置的结点
void ListErase(ListNode* pos)
{assert(pos);//如果该链表仅剩最后一个头结点,不能删除if (pos->next == pos)return;ListNode* prev = pos->prev;ListNode* next = pos->next;prev->next = next;next->prev = prev;free(pos);pos = NULL;
}//判空函数
bool ListEmpty(ListNode* phead)
{if (phead->next == phead)return true;return false;
}

3.测试文件(对各个函数功能进行测试):

#include"List.h"//测试初始化函数
void test01()
{ListNode* phead = ListInit();PrintList(phead);ListDestory(phead);
}//测试尾插函数
void test02()
{ListNode* phead = ListInit();ListPushBack(phead, 1);ListPushBack(phead, 2);ListPushBack(phead, 3);ListPushBack(phead, 4);PrintList(phead);ListDestory(phead);
}//测试头插函数
void test03()
{ListNode* phead = ListInit();ListPushFront(phead, 1);ListPushFront(phead, 2);ListPushFront(phead, 3);ListPushFront(phead, 4);PrintList(phead);ListDestory(phead);
}//测试头删函数
void test04()
{ListNode* phead = ListInit();ListPushBack(phead, 1);ListPushBack(phead, 2);ListPushBack(phead, 3);ListPushBack(phead, 4);PrintList(phead);ListPopFront(phead);PrintList(phead);ListPopFront(phead);PrintList(phead);ListPopFront(phead);PrintList(phead);ListPopFront(phead);PrintList(phead);ListPopFront(phead);PrintList(phead);ListDestory(phead);
}//测试尾删函数
void test05()
{ListNode* phead = ListInit();ListPushBack(phead, 1);ListPushBack(phead, 2);ListPushBack(phead, 3);ListPushBack(phead, 4);PrintList(phead);ListPopBack(phead);PrintList(phead);ListPopBack(phead);PrintList(phead);ListPopBack(phead);PrintList(phead);ListPopBack(phead);PrintList(phead);ListPopBack(phead);PrintList(phead);ListDestory(phead);
}//测试查找函数
void test06()
{ListNode* phead = ListInit();ListPushBack(phead, 1);ListPushBack(phead, 2);ListPushBack(phead, 3);ListPushBack(phead, 4);PrintList(phead);//查找是否有值为1的结点if (ListFind(phead,1) != NULL){printf("存在值为1的结点\n");}else{printf("不存在值为1的结点\n");}//查找是否有值为57的结点if (ListFind(phead, 57) != NULL){printf("存在值为57的结点\n");}else{printf("不存在值为57的结点\n");}ListDestory(phead);
}//测试在结点前插入函数
void test07()
{ListNode* phead = ListInit();ListPushBack(phead, 1);ListPushBack(phead, 2);ListPushBack(phead, 3);ListPushBack(phead, 4);PrintList(phead);//在第三个结点前插入300(首先找到第三个结点)ListNode* pos1 = ListFind(phead, 3);ListInsert(pos1, 300);PrintList(phead);//在第1个结点前插入100(首先找到第1个结点)ListNode* pos2 = ListFind(phead, 1);ListInsert(pos2, 100);PrintList(phead);//在最后一个结点前插入400(首先找到最后一个结点)ListNode* pos3 = ListFind(phead, 4);ListInsert(pos3, 400);PrintList(phead); ListDestory(phead);
}//测试删除pos结点
void test08()
{ListNode* phead = ListInit();ListPushBack(phead, 1);ListPushBack(phead, 2);ListPushBack(phead, 3);ListPushBack(phead, 4);PrintList(phead);删除值为1的结点(首先先找到值为1的结点)ListNode* pos1 = ListFind(phead, 1);ListErase(pos1);PrintList(phead);删除值为3的结点(首先先找到值为3的结点)ListNode* pos2 = ListFind(phead, 3);ListErase(pos2);PrintList(phead);//删除值为4的结点(首先先找到值为4的结点)ListNode* pos3 = ListFind(phead, 4);ListErase(pos3);PrintList(phead);ListDestory(phead);
}//测试判空函数
void test09()
{ListNode* phead = ListInit();ListPushBack(phead, 1);ListPushBack(phead, 2);ListPushBack(phead, 3);ListPushBack(phead, 4);PrintList(phead);if (ListEmpty(phead))printf("链表为空\n");elseprintf("链表不为空\n");ListNode* phead2 = ListInit();if (ListEmpty(phead2))printf("链表为空\n");elseprintf("链表不为空\n");ListDestory(phead);ListDestory(phead2);
}int main()
{//test01();test02();//test03();//test04();//test05();//test06();//test07();//test08();//test09();return 0;
}

感谢阅读
在这里插入图片描述

相关文章:

  • 【软件测试】软件缺陷(Bug)的详细描述
  • Oracle 执行计划中的 ACCESS 和 FILTER 详解
  • 【软件设计师:体系结构】15.计算机体系结构概论
  • PIC18F45K80 ECAN模块使用
  • 第J7周:对于ResNeXt-50算法的思考
  • Java学习手册:微服务设计原则
  • Dify之八添加各种在线大模型
  • 为特定领域微调嵌入模型:打造专属的自然语言处理利器
  • 学习黑客5 分钟读懂什么是 CVE?
  • html object标签介绍(用于嵌入外部资源通用标签)(已不推荐使用deprecated,建议使用img、video、audio标签)
  • 前端使用腾讯地图api实现定位功能
  • 损失函数(平方损失MSE、绝对值损失MAE、负对数似然损失NLL、交叉熵损失CEL和二元交叉熵损失BCE)原理、公式调库实现与手动实现
  • IC解析之TPS92682-Q1(汽车LED灯控制IC)
  • Dp通用套路(闫式)
  • 39-算法打卡-二叉树-基础知识-第三十九天
  • 从零开始的python学习(六)P86+P87+P88
  • 有关SOA和SpringCloud的区别
  • MySQL 8.0 OCP 英文题库解析(二)
  • C++ stl中的stack和queue的相关函数用法
  • 23盘古石决赛
  • 上海第四批土拍成交额97亿元:杨浦宅地成交楼板单价半年涨近7000元
  • 夜读丨喜马拉雅山的背夫
  • 理财经理泄露客户信息案进展:湖南省检受理申诉,证监会交由地方监管局办理
  • 北约年度报告渲染所谓“中国核威胁”,国防部回应
  • “三德子”赵亮直播间卖“德子土鸡”,外包装商标实为“德子土”
  • 印巴局势快速升级,外交部:呼吁印巴以和平稳定的大局为重