C语言<数据结构-双向链表>

实际上,学到双向链表时,对其结构的了解显得额外重要了已经:

我们要知道链表的结构一共分为以下几种:

1.单向或者双向:

 2.带头或者不带头:

像这种有头节点的链表我们称之为带哨兵位的链表,哨兵位的特点是不存储有效数据。 

3.循环或者非循环:

 

 综上所述,我们发现其实链表一共有8种结构:

 但是,虽然有这么多种结构,实际上我们最常用的就只是:

1.无头单向非循环链表

2.带头双向循环链表

接下来我们所介绍的就是第二种,也就是双向链表:

一.基础代码:

typedef int LTDataType;

typedef struct ListNode
{
	struct ListNode* next;
	struct ListNode* prev;

	LTDataType data;
}LTNode;

代码解释:

  1. 数据类型定义

    typedef int LTDataType;
    

     

    这行代码将int类型重命名为LTDataType,这样做的好处是,如果后续需要存储其他类型的数据(如doublechar*等),只需修改这一行代码即可,而不需要改动整个链表的实现。

  2. 双向链表节点结构

    typedef struct ListNode
    {
        struct ListNode* next;
        struct ListNode* prev;
        LTDataType data;
    }LTNode;
    

     

    这个结构体定义了双向链表的节点,包含三个成员:

    • next:指向下一个节点的指针
    • prev:指向前一个节点的指针
    • data:节点存储的数据,类型为LTDataType(这里是int

 与单链表的区别就在于双向链表定义了一个前节点,用于前后链接,不难理解。

二.初始化函数:

由于具有哨兵位节点,所以我们需要初始化一下链表,创建一个哨兵位:
 

LTNode* LTInit()
{
	LTNode* phead = BuyListNode(-1);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

代码解释:

 

  1. 函数功能

    LTNode* LTInit()
    
     

    该函数用于初始化一个双向循环链表,返回指向哨兵节点的指针。

  2. 哨兵节点创建

    LTNode* phead = BuyListNode(-1);
    
     

    调用BuyListNode函数创建一个新节点,并初始化为哨兵节点。参数-1通常作为哨兵节点的数据值,不代表实际数据。

  3. 循环链表的连接

    phead->next = phead;
    phead->prev = phead;
    

     

    将哨兵节点的nextprev指针都指向自身,形成一个空的循环链表。此时链表中只有一个哨兵节点,其前后指针构成一个环。

  4. 返回哨兵节点

    return phead;
    

     

    返回哨兵节点的指针,作为链表的头指针。

三.创建节点函数:

同单链表一样,我们需要写一个创建节点函数: 

LTNode* BuyListNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	node->next = NULL;
	node->prev = NULL;
	node->data = x;

	return node;
}

代码解释:

  1. 内存分配

    LTNode* node = (LTNode*)malloc(sizeof(LTNode));
    

     

    使用malloc分配足够的内存来存储一个LTNode结构体,并将返回的指针转换为LTNode*类型。

  2. 错误处理

    if (node == NULL)
    {
        perror("malloc fail");
        return NULL;
    }
    
     

    检查内存分配是否成功。若失败(返回NULL),使用perror打印错误信息并返回NULL,调用者需检查返回值以处理内存分配失败的情况。

  3. 节点初始化

    node->next = NULL;
    node->prev = NULL;
    node->data = x;
    
     

    将新节点的nextprev指针初始化为NULL,并将数据域设置为传入的参数x

  4. 返回节点指针

    return node;
    
     

    返回指向新节点的指针,供调用者将其插入链表中。

四. 打印函数:

void LTPrint(LTNode* phead)
{
	assert(phead);

	printf("<=head=>");
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d<=>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

前后的打印是为了更好的表示结构;

五.检查函数:

 我们可以预想到,对于删除函数,当链表为空是不能操作的,又因为双向链表具有哨兵位,通过简单的断言无法实现链表的检查,所以我们需要写一个检查函数:

bool LTEmpty(LTNode* phead)
{
	assert(phead);

	return phead->next == phead;
}

代码解释:

  1. 断言检查

    assert(phead);
    
     

    使用assert确保传入的头指针phead不为NULL。若传入NULL,程序会触发断言失败并终止,有助于在开发阶段快速定位错误。

  2. 空链表判断逻辑

    return phead->next == phead;
    
     

    对于带有哨兵节点的双向循环链表,当链表为空时,哨兵节点的next指针会指向自身(初始化时设置)。因此,通过检查phead->next == phead即可判断链表是否为空。

函数作用

该函数用于判断双向循环链表是否为空,时间复杂度为 O (1)。结合之前的LTInit函数,当链表初始化后且未插入任何节点时,LTEmpty会返回true

注意事项

  1. 前置条件

    • 调用此函数前,必须确保phead是通过LTInit初始化的哨兵节点,否则可能导致未定义行为。

六.尾插函数:

 

void LTPushBack(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;
	//LTInsert(phead,x);  
	//虽然是在哨兵位插入,这只是我们画图的一个逻辑,因为遍历是从phead->next开始,所以实际效果就相当于尾插;
}

代码解释:

  1. 断言检查

    assert(phead);
    
     

    确保传入的头指针phead(哨兵节点)不为NULL,防止空指针引用。

  2. 创建新节点

    LTNode* newnode = BuyListNode(x);
    
     

    调用BuyListNode函数创建新节点,并初始化为存储值x

  3. 找到尾节点

    LTNode* tail = phead->prev;
    
     

    在双向循环链表中,哨兵节点的prev指针始终指向尾节点。

  4. 插入新节点

    tail->next = newnode;
    newnode->prev = tail;
    newnode->next = phead;
    phead->prev = newnode;
    
     
    • 将原尾节点的next指向新节点
    • 新节点的prev指向原尾节点
    • 新节点的next指向哨兵节点
    • 哨兵节点的prev指向新节点

七.尾删函数:

void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));

	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;

	tailPrev->next = phead;
	phead->prev = tailPrev;
	free(tail);
	tail = NULL;

	//LTEraser(phead->prev);
}

代码解释:

  1. 断言检查

    assert(phead);
    assert(!LTEmpty(phead));
    
     
    • 第一个断言确保头指针(哨兵节点)不为NULL
    • 第二个断言确保链表不为空(避免删除哨兵节点)
  2. 定位尾节点和前驱节点

    LTNode* tail = phead->prev;
    LTNode* tailPrev = tail->prev;
    
     
    • tail:指向当前尾节点
    • tailPrev:指向尾节点的前一个节点
  3. 调整指针关系

    tailPrev->next = phead;
    phead->prev = tailPrev;
    

     
    • 将前驱节点的next指向哨兵节点
    • 将哨兵节点的prev指向前驱节点
  4. 释放内存

    free(tail);
    tail = NULL;
    

     
    • 释放尾节点的内存
    • 将局部变量tail置为NULL(防止野指针,但不影响原链表结构)

八.头插函数:

void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* newnode = BuyListNode(x);
	LTNode* first = phead->next;
	phead->next = newnode;
	newnode->prev = phead;

	newnode->next = first;
	first->prev = newnode;
	//LTInsert(phead->next,x);
}

 代码解释:

  1. 断言检查

    assert(phead);
    
     

    确保传入的头指针(哨兵节点)不为NULL,防止空指针引用。

  2. 创建新节点

    LTNode* newnode = BuyListNode(x);
    
     

    调用BuyListNode创建新节点,并初始化为存储值x

  3. 定位原头节点

    LTNode* first = phead->next;
    

     

    first指向当前哨兵节点的下一个节点(即原头节点)。

  4. 插入新节点

    phead->next = newnode;
    newnode->prev = phead;
    newnode->next = first;
    first->prev = newnode;
    

     
    • 将哨兵节点的next指向新节点
    • 新节点的prev指向哨兵节点
    • 新节点的next指向原头节点
    • 原头节点的prev指向新节点

九.头删函数:

void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));

	LTNode* begin = phead->next;
	phead->next = begin->next;

	free(begin);
	begin = NULL;
	
	//LTEraser(phead->next);
}

代码解释:

  1. 断言检查

    assert(phead);
    assert(!LTEmpty(phead));
    

     
    • 确保头指针(哨兵节点)不为空
    • 确保链表不为空(防止删除哨兵节点)
  2. 定位待删除节点

    LTNode* begin = phead->next;
    
     
    • begin指向当前链表的第一个数据节点
  3. 调整指针关系

    phead->next = begin->next;
    
     
    • 将哨兵节点的next指针指向待删除节点的下一个节点
  4. 释放内存

    free(begin);
    begin = NULL;
    
     
    • 释放待删除节点的内存
    • 将局部变量begin置为NULL(不影响原链表结构)

 十.任意位置插入函数:

void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* prev = pos->prev;
	LTNode* newnode = BuyListNode(x);

	prev->next = newnode;
	newnode->prev = prev;

	newnode->next = pos;
	pos->prev = newnode;//写完这个函数以后,头插、尾插就可以直接用;
}

代码解释:

  1. 断言检查

    assert(pos);
    

     
    • 确保传入的位置指针pos不为空,防止空指针引用。
  2. 定位前驱节点

    LTNode* prev = pos->prev;
    

     
    • prev指向pos节点的前驱节点。
  3. 创建新节点

    LTNode* newnode = BuyListNode(x);
    
     
    • 创建新节点并初始化为存储值x
  4. 调整指针关系

    prev->next = newnode;
    newnode->prev = prev;
    newnode->next = pos;
    pos->prev = newnode;
    
     
    • 将前驱节点的next指向新节点
    • 新节点的prev指向前驱节点
    • 新节点的next指向pos节点
    • pos节点的prev指向新节点

十一.任意位置删除函数:

void LTEraser(LTNode* pos)
{
	assert(pos);

	LTNode* prev = pos->prev;
	LTNode* next = pos->next;

	prev->next = next;
	next->prev = prev;

	free(pos);
}

不加以赘述;

当写完这两个任意位置函数以后,前面的函数就可以复用了,我在代码最后都有注释;

看到这,相信你对双向链表已经掌握不少了,是不是比单链表简单不少呢,只要你会画图!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值