新手向超好理解的带头双向循环链表

发布时间:2022-07-04 发布网站:脚本宝典
脚本宝典收集整理的这篇文章主要介绍了新手向超好理解的带头双向循环链表脚本宝典觉得挺不错的,现在分享给大家,也给大家做个参考。

目录

    • 一、易错接口详解
      • 1.1 链表初始化
      • 1.2 链表的销毁
      • 1.3 尾删
      • 1.4 在任意节点前插入
    • 二、简单接口实现
      • 2.1 打印函数
      • 2.2 尾插
      • 2.3 头插
      • 2.4 尾删
      • 2.5 计算链表有效节点个数
      • 2.6 通过值查找节点
      • 2.7 对任意节点(头节点除外)删除
    • 三、头文件和相关定义

一、易错接口详解

1.1 链表初始化

初始化链表,即需要开辟一个头节点,这个头节点中的值没有意义,这个头节点的两个指针nextprev要指向自己。详情内容请跳转至第三大部分,即可查看结构体的定义。

第一步需要开辟相应的空间,于是封装一个创建新节点的函数BuyListNode

static LTNode* BuyListNode(LTDataTyPE x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		PRintf(";malloc failn");
		exIT(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

加上static修饰,只会在该文件内被调用。

之后就调整这个新节点的nextprev指向头节点自己。

LTNode* ListInit()
{
	LTNode* phead = BuyListNode(0);
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

调用BuyListNode函数时参数为0,因为头节点中的值没有意义,所以可以随意赋值。

1.2 链表的销毁

调用这个接口通常是在程序结束前将内存释放,还给操作系统,避免内存泄漏

因为是循环链表,所以可以无限制地走下去,所以控制何时停止释放是关键。

要考虑特殊情况,参数为空指针就直接断言报错。

若只有一个头节点(不含有效节点),那么就直接free掉头节点。

void ListDestroy(LTNode* phead)
{
	assert(phead);
    if (phead->next == NULL)
	{
		free(phead);
		phead = NULL;
        return;
	}
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = NULL;
		cur = next;
	}
	free(cur);
	cur = NULL;
}

但是需要注意的是,调用该接口时,一般直接传入的是链表头节点地址(接口中对应的形参只是一份临时拷贝),而不是二级指针,这样会导致调用完这个接口后,该头节点指针变成野指针,即指向的空间已被释放,所以还需要调用者自己手动释放。这种情况可以类比free函数的调用。ListErase接口同理。

使用一级指针,外面用了以后,存在野指针的问题,但是他保持了接口的一致性。 用二级指针,解决了野指针问题。但从接口设计的角度,有点混乱。

1.3 尾删

尾删的调用必须保证链表中有不包括头节点的有效节点,同时头节点也不能为空。

判断链表是否为空的这个功能可以封装为一个函数。

尾删算法F1a;尾节点的前一个节点的next指向头节点,头节点的prev指向尾节点的前一个节点,再free掉尾节点即可。

bool ListEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next == phead;
}

void ListPopBack(LTNode* phead)
{
	assert(phead);
	assert(!ListEmpty(phead));
	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;
	free(tail);
	tailPrev->next = phead;
	phead->prev = tailPrev;
}

1.4 在任意节点前插入

单向链表在任意节点后插入的效率更高,因为不用通过首节点找尾,但是缺点是不能头插(这里指的是无头的链表)。

而带头双向循环就可避免这个缺点,因为不管怎么插效率都高,所以采用在任意节点前插入。

算法:保留指定节点的前一个节点,记作prev,创建一个新节点newnode,更改指定节点posprevnewnode的指针指向即可。

void ListInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* newnode = BuyListNode(x);
	LTNode* prev = pos->prev;
	prev->next = newnode;
	newnode->prev = prev;
	newnode->next = pos;
	pos->prev = newnode;
}

二、简单接口实现

2.1 打印函数

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

2.2 尾插

void ListPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = BuyListNode(x);
	phead->prev->next = newnode;
	newnode->prev = phead->prev;
	phead->prev = newnode;
	newnode->next = phead;
}

2.3 头插

void ListPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = BuyListNode(x);
	LTNode* next = phead->next;
	phead->next = newnode;
	newnode->prev = phead;
	next->prev = newnode;
	newnode->next = next;
}

2.4 尾删

void ListPopFront(LTNode* phead)
{
	assert(phead);
	assert(!ListEmpty(phead));
	LTNode* fNode = phead->next;
	LTNode* next = fNode->next;
	free(fNode);
	fNode = NULL;
	phead->next = next;
	next->prev = phead;
}

2.5 计算链表有效节点个数

size_t ListSize(LTNode* phead)
{
	assert(phead);
	size_t count = 0;
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		count++;
		cur = cur->next;
	}
	return count;
}

2.6 通过值查找节点

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;
}

2.7 对任意节点(头节点除外)删除

void ListErase(LTNode* pos)
{
	assert(pos);
	LTNode* prev = pos->prev;
	LTNode* next = pos->next;
	free(pos);
	pos = NULL;
	prev->next = next;
	next->prev = prev;
}

三、头文件和相关定义

#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
typedef int LTDataType;

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

LTNode* ListInit();
void ListDestroy(LTNode* phead);
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);

脚本宝典总结

以上是脚本宝典为你收集整理的新手向超好理解的带头双向循环链表全部内容,希望文章能够帮你解决新手向超好理解的带头双向循环链表所遇到的问题。

如果觉得脚本宝典网站内容还不错,欢迎将脚本宝典推荐好友。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。