• 【数据结构】双向链表



    一.带头双向循环链表

    在这里插入图片描述

    1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结
      构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
    2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都
      是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带
      来很多优势,实现反而简单了,后面我们代码实现了就知道了。

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

    1.定义结构体

    #define _CRT_SECURE_NO_WARNINGS 1
    #pragma once
    
    //头文件
    #include
    #include
    #include
    //对数据类型进行重命名,这样可以对多种数据类型进行存储,有利于控制链表
    typedef int LTDataType;
    
    //结点(结构体)
    typedef struct ListNode
    {
    	struct ListNode* prev;
    	struct ListNode* next;
    	LTDataType data;
    }LTNode;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    2.函数接口

    不用二级指针的几种解决方案
    1.返回值
    2.建立哨兵位

    //初始化
    LTNode* ListInit();
    //打印
    void ListPrint(LTNode* phead);
    //尾插
    void ListPushBack(LTNode* phead, LTDataType x);
    //头插
    void ListPushFront(LTNode* phead, LTDataType x);
    //尾删
    void ListPopBack(LTNode* phead);
    //头删
    void ListPopFront(LTNode* phead);
    //删除链表结点时,用于判断链表是否为空,为空返回真,不为空返回假
    bool ListEmpty(LTNode* phead);
    //计算链表大小
    size_t ListSize(LTNode* phead);
    //链表查找
    LTNode* ListFind(LTNode* phead, LTDataType x);
    //链表插入
    void ListInsert(LTNode* pos, LTDataType x);
    //链表删除
    void ListErase(LTNode* pos);
    //链表销毁
    void ListDestroy(LTNode* phead);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    3.初始化双向链表

    #define _CRT_SECURE_NO_WARNINGS 1
    #include"List.h"
    
    //初始化
    LTNode* ListInit()
    {
    	//建立带哨兵位的头结点
    	LTNode* guard = (LTNode*)malloc(sizeof(LTNode));
    	if (guard == NULL)
    	{
    		perror("malloc fail");
    		exit(-1);
    	}
    	//带头双向循环链表的初始化
    	//就是先建立一个头结点
    	//头结点的两个指针域都指向头结点,形成自环
    	guard->next = guard;
    	guard->prev = guard;
    
    	return guard;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    4.创建新结点

    //创建新结点
    LTNode* BuyListNode(LTDataType x)
    {
    	//建立哨兵位
    	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
    	if (node == NULL)
    	{
    		perror("malloc fail");
    		exit(-1);
    	}
    	node->next = NULL;
    	node->prev = NULL;
    	node->data = x;
    
    	return node;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    5.尾插

    先创建一个新结点,再尾插。

    void ListPushBack(LTNode* phead, LTDataType x)
    {
    	//不可能是空,因为有哨兵位
    	assert(phead);
    	LTNode* newnode = BuyListNode(x);
    
    	LTNode* tail = phead->prev;
    
    	tail->next = newnode;
    	newnode->prev = tail;
    	newnode->next = phead;
    	phead->prev = newnode;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    6.打印

    void ListPrint(LTNode* phead)
    {
    	assert(phead);
    	printf("phead<=>");
    	LTNode* cur = phead->next;//带哨兵卫的头结点没有数据
    
    	//因为是循环链表,所以以cur!=phead作为判断条件,否则就会死循环
    	while (cur != phead)
    	{
    		printf("%d<=>", cur->data);
    		//迭代
    		cur = cur->next;
    	}
    	printf("\n");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    测试结果:
    在这里插入图片描述

    7.头插

    void ListPushFront(LTNode* phead, LTDataType x)
    {
    	assert(phead);
    
    	//先创建新结点
    	LTNode* newnode = BuyListNode(x);
    	//不关心顺序
    	LTNode* first = phead->next;//保留哨兵卫头结点的下一个结点
    	newnode->next = first;
    	first->prev = newnode;
    	phead->next = newnode;
    	newnode->prev = phead;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    测试用例:
    在这里插入图片描述

    8.判断链表是否为空

    链表删除之前需要判断链表是否为空(只有带哨兵卫的头结点),为空返回true,不为空返回false。采用bool类型

    //删除链表结点时,用于判断链表是否为空,为空返回真,不为空返回假
    bool ListEmpty(LTNode* phead)
    {
    	assert(phead);
    	if (phead->next == phead)//出了头结点,没有其他新增结点
    	{
    		return true;
    	}
    	else
    	{
    		return false;
    	}
    	//return phead->next==phead;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    9.尾删

    在这里插入图片描述

    void ListPopBack(LTNode* phead)
    {
    	assert(phead);
    	//尾删还要保证,链表不为空
    	assert(!ListEmpty(phead));
    
    	//tail指向最后一个要尾删的结点
    	LTNode* tail = phead->prev;
    	//prev指向tail前面一个结点(即将变成尾结点)
    	LTNode* prev = tail->prev;
    
    	phead->prev = prev;
    	prev->next = phead;
    	free(tail);
    	tail = NULL;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    10.头删

    void ListPopFront(LTNode* phead)
    {
    	assert(phead);
    	assert(!ListEmpty(phead));
    
    	LTNode* first = phead->next;
    	LTNode* second = first->next;
    	phead->next = second;
    	second->prev = phead;
    
    	free(first);
    	first = NULL;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    测试用例:
    在这里插入图片描述

    11.链表大小

    size_t ListSize(LTNode* phead)
    {
    	assert(phead);
    
    	size_t n = 0;
    	LTNode* cur = phead->next;
    	while (cur != phead)
    	{
    		n++;
    		//迭代
    		cur = cur->next;
    	}
    	printf("\n");
    
    	return n;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    12.查找

    LTNode* ListFind(LTNode* phead, LTDataType x)
    {
    	assert(phead);
    	LTNode* cur = phead->next;
    	while (cur != phead)
    	{
    		if (cur->data == x)
    		{
    			return cur;
    		}
    		//迭代
    		cur = cur->next;
    	}
    	return NULL;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    13.插入

    链表的插入一般都是在pos前进行插入

    //在pos之前插入
    void ListInsert(LTNode* pos, LTDataType x)
    {
    	//有可能查找不到pos,则会返回空,如果你输入的数据查找不到,则报错,说明输入错误
    	assert(pos);
    
    	LTNode* prev = pos->prev;
    	LTNode* newnode = BuyListNode(x);
    
    	//prev newnode pos
    	newnode->next = pos;
    	pos->prev = newnode;
    	newnode->prev = prev;
    	prev->next = newnode;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    实现了链表的插入后,我们可以将之前的头插和尾插做一个改造。
    尾插相当于在头结点前面插入一个新结点
    ListInsert(phead, x);
    头插,需要记录头结点后面的结点,再插入
    LTNode* first = phead->next;
    ListInsert(first, x);

    14.链表删除

    //链表删除
    void ListErase(LTNode* pos)
    {
    	assert(pos);
    	//prev pos next
    	LTNode* prev = pos->prev;
    	LTNode* next = pos->next;//记录pos前后的位置
    
    	prev->next = next;
    	next->prev = prev;
    
    	free(pos);
    	pos = NULL;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    15.链表销毁

    void ListDestroy(LTNode* phead)
    {
    	assert(phead);
    	//边遍历,边释放
    	LTNode* cur = phead->next;
    	while (cur != phead)
    	{
    		LTNode* next = cur->next;
    		free(cur);
    		cur = next;
    	}
    	free(phead);
    	phead = NULL;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    三.顺序表与链表的区别

    顺序表的优点:
    1.尾插尾删效率很高
    2.随机访问(利用下标访问)
    3.相比链表结构,顺序表CPU高速缓存命中率更高
    顺序表的缺点:
    1.头部和中部插入删除效率低。(O(N))
    2.扩容:性能消耗,空间浪费

    链表的优点:
    1.任意位置插入删除效率很高。O(1)
    2.按需申请释放
    链表的缺点:
    1.不支持随机访问

  • 相关阅读:
    MBR15200FAC-ASEMI插件肖特基二极管MBR15200FAC
    7、ByteBuffer(方法演示1(切换读写模式,读写))
    PostgreSql 数据库,根据库名称,查询这个库下面的所有表名称和这个表的注释
    【RocketMQ】发送事务消息
    支持JDK19虚拟线程的web框架,之五(终篇):兴风作浪的ThreadLocal
    Sqli-labs靶场第16关详解[Sqli-labs-less-16]自动化注入-SQLmap工具注入
    python 解析xml
    Git - 导出(archive)、忽略(gitignore)、隐藏(Stash)、合并冲突(merge)的解决方法
    Advanced .Net Debugging 2:CLR基础
    卧式铣床主传动系统设计建模及运动仿真
  • 原文地址:https://blog.csdn.net/weixin_63449996/article/details/126492635