双向带头链表实现

双向带头链表

带头双向链表是一种数据结构,它是双向链表的一个变体。在双向链表中,每个节点都有两个指针,一个指向前一个节点,另一个指向后一个节点。这种结构允许在两个方向上遍历链表

带头双向链表的特点是它有一个额外的“头节点”(或称为“哨兵节点”),这个头节点不包含实际的数据,而是作为链表的起始点。头节点的存在简化了链表操作,因为不需要特别处理链表为空的情况,或是对链表的第一个和最后一个节点进行特殊操作

带头双向链表具有以下特点:

  1. 头节点:这是一个特殊的节点,通常不存储任何数据(或者存储一些代表链表状态的元信息),它的存在是为了简化链表操作。

  2. 双向链接:链表中的每个节点都有两个链接:一个指向前一个节点,另一个指向后一个节点。头节点也遵循这个规则,它的前一个节点链接到链表的最后一个节点,后一个节点链接到链表的第一个节点

  3. 插入和删除操作:由于链表是双向的,所以可以从两个方向插入和删除节点。头节点的存在使得在链表头部插入和删除节点变得更加方便

  4. 遍历:可以从头节点开始,向前或向后遍历整个链表

  5. 灵活性和效率:相比于单向链表,双向链表(尤其是带头节点的)在进行某些操作时更加灵活和高效,尤其是在需要频繁地插入和删除节点时

双向带头链表实现的功能介绍

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int ListDataType;

typedef struct ListNode
{
	ListDataType _data;
	struct ListNode* _prev;
	struct ListNode* _next;
}ListNode;

ListNode* ListCreate(ListDataType x);

//双向链表的初始化
ListNode* ListInit();


//双向链表的删除
void ListDestory(ListNode* pHead);

//双向链表的打印
void ListPrint(ListNode* pHead);

//双向链表的尾插
void ListPushBack(ListNode* pHead, ListDataType x);

//双向链表的头插
void ListPushFront (ListNode* pHead, ListDataType x);

//双向链表的尾删
void ListPopBack(ListNode* pHead);

//双向链表的头删
void ListPopFront(ListNode* pHead);

//双向链表的查找
ListNode* ListNodeFinde(ListNode* pHead, ListDataType x);

//双向链表的在pos位置前删除
void ListInsert(ListNode* pos, ListDataType x);

//双向链表删除pos点
void ListErase(ListNode* pos);

下面我们将一步一步的介绍每个功能:

链表的初始化和新结点的创建

ListNode* ListCreate(ListDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}
	newnode->_next = NULL;
	newnode->_prev = NULL;
	newnode->_data = x;
	return newnode;
}

这个是一个结点的简单样子

使用Malloc创建一个ListNode 的结构体。ListNode 是一个代表双向链表节点的结构体,包含至少三个成员:_next(指向下一个节点的指针),_prev(指向前一个节点的指针),和 _data(存储数据的变量)

ListNode* ListInit()
{
	ListNode* phead = ListCreate(-1);
	phead->_next = phead;
	phead->_prev = phead;
	
	return phead;
}

这是一个哨兵位,在初始化的时候它的头尾都指向自己

首先调用 ListCreate(-1) 来创建一个新的链表节点。这里的 -1 是用于初始化头节点的数据值。在许多实现中,头节点的数据部分通常不用于存储有效数据,而是作为链表操作的辅助

接着,将新创建的节点(phead)的 _next_prev 指针都指向它自己。这种做法是典型的空双向链表(带头节点)的初始化方式,其中头节点的前一个和后一个节点都是它自己,表示链表为空

链表的打印

void ListPrint(ListNode* phead)
{
	assert(phead);
	ListNode* cur = phead->_next;
	printf("head <-> ");
	while (cur != phead)
	{
		printf("%d <-> ", cur->_data);
		cur = cur->_next; 
	}
	printf("\n");

}
  1. 初始化遍历指针:定义一个 ListNode* 类型的指针 cur,并将其初始化为指向头节点的下一个节点(phead->_next)。这是遍历链表的起点

  2. 打印头节点标记:首先打印出 "head <-> ",表示链表的起始

  3. 遍历链表:使用一个 while 循环遍历链表。只要 cur 不等于 phead(即没有回到头节点,这意味着链表尚未遍历完),循环就继续。在每次循环中,打印当前节点的数据(cur->_data),然后将 cur 更新为下一个节点(cur->_next

链表的头插和头删

头插

void ListPushFront(ListNode* phead, ListDataType x)
{
	assert(phead);
	ListNode* newnode = ListCreate(x);
	ListNode* tmp = phead->_next;
	phead->_next = newnode;
	newnode->_prev = phead;
	newnode->_next = tmp;
	//ListInsert(phead->_next, x);
}

 头插的例子

  1. 创建新节点:调用 ListCreate(x) 创建一个新的节点,将传入的数据 x 存储在该节点中

  2. 保存当前第一个节点:将链表的当前第一个节点(即头节点的下一个节点,phead->_next)保存到一个临时变量 tmp

  3. 将新节点插入到链表前端

    • 将头节点的 _next 指针指向新节点,即 phead->_next = newnode
    • 将新节点的 _prev 指针指向头节点,即 newnode->_prev = phead
    • 将新节点的 _next 指针指向原先的第一个节点(即 tmp),即 newnode->_next = tmp

头删

void ListPopFront(ListNode* phead)
{
	assert(phead);
	assert(phead->_next != phead);
	ListNode* first = phead->_next;
	ListNode* second = first->_next;

	phead->_next = second;
	second->_prev = phead;
	free(first);
	first = NULL;
	//ListErase(phead->_next);
}

 

  1. 定位第一个和第二个节点

    • first 指向链表的第一个节点,即头节点的下一个节点(phead->_next
    • second 指向链表的第二个节点,即 first 节点的下一个节点(first->_next
  2. 移除第一个节点

    • 将头节点的 _next 指针指向 second,即 phead->_next = second
    • second 节点的 _prev 指针指向头节点,即 second->_prev = phead
    • 释放(freefirst 节点所占用的内存
    • first 设置为 NULL 以避免悬挂指针

 链表的尾插和尾删

尾插

void ListPushBack(ListNode* phead, ListDataType x)
{
	assert(phead);
	ListNode* tail = phead->_prev;
	ListNode* newnode = ListCreate(x);
	tail->_next = newnode;
	newnode->_prev = tail;
	phead->_prev = newnode;
	newnode->_next = phead;
	//ListInsert(phead, x);


}

头插和尾插类似

  1. 找到尾节点:通过 phead->_prev 获取链表的尾节点,存储在 tail 变量中。在带头节点的双向链表中,头节点的 _prev 成员通常指向链表的最后一个节点

  2. 创建新节点:调用 ListCreate(x) 创建一个新的节点,其数据字段被设置为 x

  3. 将新节点链接到链表

    • 将当前尾节点的 _next 指针指向新节点:tail->_next = newnode
    • 将新节点的 _prev 指针指向当前尾节点:newnode->_prev = tail
  4. 更新头节点和新节点的指针

    • 将头节点的 _prev 指针指向新节点,使新节点成为链表的新尾节点:phead->_prev = newnode
    • 将新节点的 _next 指针指向头节点,维持链表的循环结构:newnode->_next = phead

尾删

void ListPopBack(ListNode* phead)
{
	assert(phead);
	assert(phead->_next != phead);

	ListNode* tail = phead->_prev;
	ListNode* cur = tail->_prev;
	cur->_next = phead;
	phead->_prev = cur;
	free(tail);
	tail = NULL;
	//ListErase(phead->_prev);

}
  1. 定位尾节点和倒数第二个节点

    • tail 指向链表的尾节点,即头节点的 _prev 成员
    • cur 指向倒数第二个节点,即 tail_prev 成员
  2. 移除尾节点

    • 将倒数第二个节点的 _next 指针指向头节点,即 cur->_next = phead
    • 将头节点的 _prev 指针指向倒数第二个节点,即 phead->_prev = cur
    • 释放(freetail 节点所占用的内存
    • tail 设置为 NULL 以避免悬挂指针

链表的查找

//双向链表的查找
ListNode* ListNodeFinde(ListNode* phead, ListDataType x)
{
	assert(phead);
	ListNode* cur = phead->_next;
	while (cur != phead)
	{
		if (cur->_data == x)
		{
			return cur;
		}
		cur = cur->_next;
	}
	return NULL;
}

 

  1. 初始化遍历指针:定义一个 ListNode* 类型的指针 cur,并将其初始化为指向头节点的下一个节点(phead->_next)。这是遍历链表的起点

  2. 遍历链表:使用一个 while 循环遍历链表。只要 cur 不等于 phead(即没有回到头节点,这意味着链表尚未遍历完),循环就继续

  3. 查找匹配节点

    • 在每次循环中,检查当前节点的数据(cur->_data)是否等于查找的值 x
    • 如果找到匹配的节点,则返回该节点的指针
    • 如果没有找到,将 cur 更新为下一个节点(cur->_next
  4. 未找到匹配节点:如果遍历完整个链表都没有找到匹配的节点,最后函数返回 NULL

链表的删除pos点内容和pos点前插入

pos点前插入

//双向链表的在pos位置前插入
void ListInsert(ListNode* pos, ListDataType x)
{
	assert(pos);
	ListNode* newnode = ListCreate(x);
	ListNode* prev = pos->_prev;
	prev->_next = newnode;
	newnode->_prev = prev;
	newnode->_next = pos;
	pos->_prev = newnode;
	
}

 pos点插入

 

  1. 创建新节点:调用 ListCreate(x) 创建一个新的节点,其数据字段被设置为 x

  2. 定位 pos 的前一个节点:通过 pos->_prev 获取 pos 节点的前一个节点,存储在 prev 变量中

  3. 将新节点插入到链表中

    • prev 节点的 _next 指针指向新节点:prev->_next = newnode
    • 将新节点的 _prev 指针指向 prevnewnode->_prev = prev
    • 将新节点的 _next 指针指向 posnewnode->_next = pos
    • pos 节点的 _prev 指针指向新节点:pos->_prev = newnode

pos点删除

//双向链表删除pos点
void ListErase(ListNode* pos)
{
	assert(pos);
	ListNode* prev = pos->_prev;
	ListNode* next = pos->_next;
	prev->_next = next;
	next->_prev = prev;

	free(pos);
	pos = NULL;

}

  1. 定位前后节点

    • prev 指向 pos 节点的前一个节点,即 pos->_prev
    • next 指向 pos 节点的下一个节点,即 pos->_next
  2. 从链表中移除 pos 节点

    • prev 节点的 _next 指针指向 next,即 prev->_next = next
    • next 节点的 _prev 指针指向 prev,即 next->_prev = prev
  3. 释放内存并清除指针

    • 使用 free(pos) 释放 pos 节点所占用的内存
    • pos 设置为 NULL 以避免悬挂指针

链表和顺序表的优势和缺陷

链表(双向)优势:

  1. 任意位置插入删除都是O(1)
  2. 按需申请释放,合理利用空间、不存在浪费

问题:

  1. 下标随机访问不方便,O(N)

顺序表问题:

  1. 头部或者中间插入删除效率低,要挪动数据。O(N)
  2. 空间不够需要扩容,扩容一定的消耗,且可能存在一定的空间浪费
  3. 只适合尾插尾删

优势(物理内存延续)

  1. 支持下标的随机访问O(N)
  • 30
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值