• 【数据结构】【版本1.2】【线性时代】——双向链表




    快乐的流畅:个人主页


    个人专栏:《算法神殿》《数据结构世界》《进击的C++》

    远方有一堆篝火,在为久候之人燃烧!

    引言

    数据结构世界——双向链表(Double Linked List)

    一、链表的分类


    虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:单链表和双向带头循环链表

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

    二、双向链表的结构

    • 带头链表里的头节点,实际为哨兵位,哨兵位节点不存储任何有效元素,只是站在这里“放哨的”
    • 哨兵位存在的意义: 遍历循环链表避免死循环

    ps:这里的“带头”跟前面我们说的“头节点”是两个概念,实际前面的在单链表阶段称呼不严谨,但是为了更好的理解就直接称为单链表的头节点。

    三、双向链表的模拟实现

    3.1 定义

    typedef int DLDataType;
    typedef struct DListNode
    {
    	struct DListNode* prev;
    	struct DListNode* next;
    	DLDataType data;
    }DLNode;
    
    • 与单链表不同,一个节点里有两个指针,分别指向前节点和后节点,实现双向

    3.2 创建新节点

    DLNode* BuyDLNode(DLDataType x)
    {
    	DLNode* newnode = (DLNode*)malloc(sizeof(DLNode));
    	if (newnode == NULL)
    	{
    		perror("malloc fail");
    		return NULL;
    	}
    	newnode->data = x;
    	newnode->prev = NULL;
    	newnode->next = NULL;
    	return newnode;
    }
    
    • 创建新节点与单链表大致相同,抽离成函数方便创建节点

    3.3 初始化

    DLNode* DLInit()
    {
    	DLNode* phead = BuyDLNode(-1);
    	phead->next = phead;
    	phead->prev = phead;
    	return phead;
    }
    
    • 双向链表与单链表很大区别,就是在于初始化,创建哨兵位,而哨兵位不存储有效数据,所以这里存入-1,并让prev和next指针都指向自身

    3.4 打印

    void DLPrint(DLNode* phead)
    {
    	assert(phead);
    	DLNode* cur = phead;
    
    	printf("Guard");
    	while (cur->next != phead)
    	{
    		cur = cur->next;
    		printf("<==>%d", cur->data);
    	}
    	printf("\n");
    }
    
    • 首先assert断言,因为phead指向哨兵位,一定不为空
    • 其次因为双向循环链表里没有NULL,所有停止条件就看哨兵位,当cur指针的下一个为哨兵位时,就代表走到尾部

    3.5 尾插

    void DLPushBack(DLNode* phead, DLDataType x)
    {
    	assert(phead);
    	DLNode* newnode = BuyDLNode(x);
    	DLNode* tail = phead->prev;
    	
    	tail->next = newnode;
    	newnode->prev = tail;
    	phead->prev = newnode;
    	newnode->next = phead;
    }
    
    • 因为prev指针的存在,使得找尾十分方便,不用遍历链表,直接访问哨兵位的上一个节点即可

    如果是单链表,是不是还要讨论空链表的特殊情况啊?但是,在双向循环链表中,上述代码就可包含所有情况。因为哨兵位的存在,使得链表一定不为空

    在这里插入图片描述

    同时,因为哨兵位的存在,我们不用传二级指针了,只用对结构体进行操作。怎么样,有没有发现双向链表的巨大优势

    3.6 头插

    void DLPushFront(DLNode* phead, DLDataType x)
    {
    	assert(phead);
    	DLNode* newnode = BuyDLNode(x);
    	DLNode* first = phead->next;
    
    	newnode->next = first;
    	first->prev = newnode;
    	phead->next = newnode;
    	newnode->prev = phead;
    }
    

    ps:先链接后一个节点,再链接前一个节点

    3.7 判空

    删除时,不能删除哨兵位,要不然后续都无法对链表进行操作,所以专门写个函数来判断除了哨兵位链表是否为空。

    bool DLEmpty(DLNode* phead)
    {
    	assert(phead);
    	return phead == phead->next;
    }
    
    • 如果phead和phead->next相等,则为空,返回1
    • 如果phead和phead->next不相等,则不为空,返回0

    3.8 尾删

    void DLPopBack(DLNode* phead)
    {
    	assert(phead);
    	assert(!DLEmpty(phead));
    	DLNode* tail = phead->prev;
    	DLNode* tailPrev = tail->prev;
    	
    	free(tail);
    	tailPrev->next = phead;
    	phead->prev = tailPrev;
    }
    
    • 找尾tail直接往phead的上一位,同时创建tailPrev,在释放尾部节点后,双向链接哨兵位

    3.9 头删

    void DLPopFront(DLNode* phead)
    {
    	assert(phead);
    	assert(!DLEmpty(phead));
    	DLNode* first = phead->next;
    	DLNode* second = first->next;
    
    	second->prev = phead;
    	phead->next = second;
    	free(first);
    }
    
    • 找头first往phead的下一位,再创建second,在释放头部空间后,双向链接哨兵位

    3.10 查找与修改

    DLNode* DLFind(DLNode* phead, DLDataType x)
    {
    	assert(phead);
    	DLNode* cur = phead->next;
    
    	while (cur != phead)
    	{
    		if (cur->data == x)
    		{
    			return cur;
    		}
    		cur = cur->next;
    	}
    	return NULL;
    }
    
    • 从哨兵位的下一位开始找,因为哨兵位是不存储有效数据的,直到重新找到哨兵位
    • 找到返回地址,找不到返回NULL

    3.11 指定插入

    在pos前插入

    void DLInsert(DLNode* pos, DLDataType x)
    {
    	assert(pos);
    	DLNode* newnode = BuyDLNode(x);
    	DLNode* prev = pos->prev;
    
    	prev->next = newnode;
    	newnode->prev = prev;
    	newnode->next = pos;
    	pos->prev = newnode;
    }
    
    • 找到pos的上一个节点的地址prev,再进行链接

    在这里可以复用指定插入函数快速实现头插和尾插

    头插

    void DLPushFront(DLNode* phead, DLDataType x)
    {
    	assert(phead);
    	DLInsert(phead->next, x);
    }
    

    尾插

    void DLPushBack(DLNode* phead, DLDataType x)
    {
    	assert(phead);
    	DLInsert(phead, x);
    }
    

    3.12 指定删除

    在pos删除

    void DLErase(DLNode* pos)
    {
    	assert(pos);
    	DLNode* posPrev = pos->prev;
    	DLNode* posNext = pos->next;
    
    	posPrev->next = posNext;
    	posNext->prev = posPrev;
    	free(pos);
    }
    
    • 创建posPrev和posNext两个指针,释放掉pos的节点空间,再相互链接

    在这里也可以复用指定删除函数快速实现头删和尾删

    头删

    void DLPopFront(DLNode* phead)
    {
    	assert(phead);
    	assert(!DLEmpty(phead));
    	DLErase(phead->next);
    }
    

    尾删

    void DLPopBack(DLNode* phead)
    {
    	assert(phead);
    	assert(!DLEmpty(phead));
    	DLErase(phead->prev);
    }
    

    大家有没有发现,因为双向链表存在哨兵位,链表不为空,省去了很多关于空链表和空指针的讨论,一路下来浑身舒畅!

    3.13 销毁

    void DLDestroy(DLNode* phead)
    {
    	assert(phead);
    	DLNode* cur = phead->next;
    
    	while (cur != phead)
    	{
    		DLNode* next = cur->next;
    		free(cur);
    		cur = next;
    	}
    	free(phead);
    }
    
    • 因为为了保持用一级指针的连贯性,所以我们选择最后手动在外部将链表指针置为NULL,实现半自动(和free函数的逻辑一致)

    四、顺序表与双向链表的对比

    不同点顺序表链表
    存储空间上物理上一定连续逻辑上连续,但物理上不一定连续
    随机访问支持,O(1)不支持,O(N)
    任意位置插入或者删除元素可能需要搬移元素,效率低,O(N)只需修改指针指向
    插入动态顺序表,空间不够时需要扩容没有容量的概念
    应用场景元素高效存储+频繁访问任意位置插入和删除频繁

    真诚点赞,手有余香

  • 相关阅读:
    python判断语句
    【韩顺平 零基础30天学会Java】面向对象编程(中级)
    在互联网上少了这一步,你就别想着赚钱?
    第五十五章 本地化和基于标签的开发
    教你几招,快速实现Word转PDF操作
    掌握Go的运行时:从编译到执行
    微服务实战声明式服务调用OpenFeign实践
    当下流行的编程语言
    卷积架构的进一步挖掘:ConvNeXt,RepLKNet,HRNet
    alpine linux如何指定软件包安装源
  • 原文地址:https://blog.csdn.net/2301_79188764/article/details/139569674