我将以高达形态出击(双向带头循环链表讲解)

  个人主页:欢迎大家光临——>沙漠下的胡杨

  各位大帅哥,大漂亮

 如果觉得文章对自己有帮助

 可以一键三连支持博主

 你的每一分关心都是我坚持的动力

 

 ☄: 本期重点:双向带头循环链表的实现

  希望大家每天都心情愉悦的学习工作。 

 ☄: 本期重点:双向带头循环链表的实现

链表的8种结构:

双向带头循环的实现:

头文件的创建和初始化:

源文件:函数接口的实现

创建新的节点:

链表的初始化:

链表的打印:

 

链表的尾插:

 

 链表的头插:

链表的尾删:

链表的头删:

判断链表的是否为空:

链表的长度:

链表的销毁:

如何10分钟之内写出一个链表呢?

在pos位置之前插入x

删除pos位置的结点:

整体代码展示

头文件:

函数实现的源文件:

带头双向循环链表总结


链表的8种结构:

链表的属性上大体分为:单向和双向,带头和不带头,循环和不循环,这3个属性,一共6中,我们排列组合之后就是8种结构。

 其中我们最常用的就是中的两种,一个是之前已经讲过的单链表---->单链表的讲解   

下面我们就看下个链表的另一个常用的结构,也是链表的终极形态啦,叫双向带头循环结构:

 我们可以看到这个链表每个结点上都有4个关系线,比起单链表要麻烦多了,但是虽然结构麻烦,但是使用时很方便哦。

双向带头循环的实现:

头文件的创建和初始化:

首先,我们创建链表时,要先明白这个链表的结构,它有前指针和后指针,和存放数据的区域,所以我们应该如下创建:

typedef int LTDataType;//类型重命名,方便修改数据类型

typedef struct ListNode
{
	struct ListNode *next;//前指针
	struct ListNode *prev;//后指针
	LTDataType data;
}LTNode;//链表重命名

还有要用的头文件

#pragma once//防止头文件重复包含
#define  _CRT_SECURE_NO_WARNINGS//禁止4996报错
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>//使用布尔值引用的头文件
#include <assert.h>//断言头文件

下面是函数的接口

LTNode* BuyListNode(LTDataType x);//创建结点

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);//判断链表是否为空

// 在pos位置之前插入x
void ListInsert(LTNode* pos, LTDataType x);

//  删除pos位置的节点
void ListErase(LTNode* pos);

int ListSize(LTNode* phead);//求链表的长度

void ListDestroy(LTNode* phead);//链表的销毁

源文件:函数接口的实现

我们一个个来看下函数接口的实现:

创建新的节点:

LTNode* BuyListNode(LTDataType x)//创建结点
{
    //开辟新的节点 
	LTNode * newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");//开辟失败结束程序
		exit(-1);
	}

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

	return newnode;//最后返回节点。
}

首先我们使用返回值的方式,不像单链表一样使用二级指针的形式,是因为双向带头循环链表的头结点不会改变,所以没必要使用二级指针。我们单单使用返回值就可以啦。

 

链表的初始化:

LTNode* ListInit()//初始化
{
    //创建头结点,赋值可以随意。
	LTNode *phead = BuyListNode(-1);
	phead->next = phead;//把前后指针都指向自己
	phead->prev = phead;

	return phead;//返回头结点,初始化完成。
}

在链表初始化时,我们其实只是创建了一个头结点,把它的前后指针都指向自己,进而完成后续的操作,这里我们保证链表的整体统一,所以也使用了返回值的形式。

链表的打印:

void ListPrint(LTNode *phead)//打印
{
	assert(phead);//断言节点不为空

    //保存下一个结点,进行遍历
	LTNode *cur = phead->next;
    //从头结点的下一个开始遍历,到头结点结束
	while (cur != phead)
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}

	printf("\n");
}

在链表的打印中,我们要注意的是,这个链表是循环的链表,所以不可以指直接遍历,否则会导致链表死循环,所以我们从链表的头结点的下一个结点进行遍历,到链表的头结点结束,刚好把链表遍历一遍。

 

链表的尾插:

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

上面的链表的尾插,其实就是找到链表的头结点的后指针,进而找到尾节点,在创建一个新节点,然后把他们连接起来就好啦。示意图如下(图画的确实有点丑啦)

 

 链表的头插:

void ListPushFront(LTNode *phead, LTDataType x)//头插
{
	assert(phead);
	
	LTNode* newnode = BuyListNode(x);
	LTNode* tail = phead->next;//找到第一个节点

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

}

头插和尾插大同小异,头插我们要先找出第一个节点,就是头结点的后指针,然后我们进行插入连接就好啦,图示和尾插类似。

 

 

链表的尾删:

void ListPopBack(LTNode *phead)//尾删
{
	assert(phead);
	assert(!ListEmpty(phead));//判断链表不为空

	LTNode* tail = phead->prev;//保存尾节点
	

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

	free(tail);
	tail = NULL;
}

链表的尾删也不难,我们先找到尾节点,接着我们对节点进行修改,把尾节点的前一个的后指针连接到头结点,接着把头结点的前指针,连接到尾节点的前一个,最后free掉尾节点,把尾节点置NULL。

 

链表的头删:

void ListPopFront(LTNode *phead)//头删
{
	assert(phead);
	assert(!ListEmpty(phead));

	LTNode* tail = phead->next;

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

	free(tail);
	tail = NULL;
}

头删和尾删基本一样,就删除的结点不一样,我们只需要找到第一个结点就可以啦。

 

判断链表的是否为空:

bool ListEmpty(LTNode* phead)//判断链表是否为空
{
	assert(phead);

	return phead->next == phead;//判断是否为空
}

我们一般判断链表为空时,我们采用的是 if 语句条件判断,看是否phead->next == phead这样进行判断,我们可以直接用 phead->next == phead 这个表达式的结果进行判断。

 

链表的长度:

int ListSize(LTNode* phead)//求链表的长度
{
	int size = 0;
	LTNode *cur = phead->next;

	while (cur != phead)
	{
		size++;
		cur = cur->next;
	}
	
	return size;
}

这个求链表的长度和打印链表的思路一致,我们不能直接遍历,所以我们保存头结点的下一个,然后进行遍历,一直到链表的头结点处遍历结束,最后我们返回size就可以啦。

 

链表的销毁:

void ListDestroy(LTNode* phead)//链表的销毁
{
	assert(phead);
	LTNode *cur = phead->next;

	while (cur != phead)
	{
        //头删前,要先保存下一个的结点。
		LTNode* next = cur->next;
		ListPopFront(cur);
		cur = next;
	}

	free(phead);
}

链表的销毁中最值得注意的就是,我们在删除链表时必须要把删除前的下一个进行保存,这样不会导致访问不到,删除完成后,我们还要删除链表的头结点

如何10分钟之内写出一个链表呢?

下面就进入最精华的部分,10分钟以内写出一个链表的关键:

// 在pos位置之前插入x
void ListInsert(LTNode* pos, LTDataType x);

//  删除pos位置的节点
void ListErase(LTNode* pos);

关于上面两个函数的实现,就可以帮助我们同时写出 头删头插和尾删尾插 。

 

在pos位置之前插入x

void ListInsert(LTNode* pos, LTDataType x)
{
	assert(pos);//断言pos不为NULL

	LTNode* tail = pos->prev;//找到pos结点的前一个。
	LTNode* newnode = BuyListNode(x);

	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = pos;
	pos->prev = newnode;
}

我们先判断pos不为NULL,然后找到pos结点的前一个,接着创建一个新节点,然后把这三个结点(pos   pos->prev   newnode )进行连接就好啦。

 

删除pos位置的结点:

void ListErase(LTNode* pos)
{
	assert(pos);
	LTNode* tailnext = pos->next;//找到pos的下一个结点
	LTNode* tailprev = pos->prev;//找到pos的前一个结点

	tailprev->next = tailnext;
	tailnext->prev = tailprev;

	free(pos);
	pos = NULL;

}

我们先找到pos的前 后 结点,接着我们进行连接这两个结点(pos->next pos->prev),最后free 掉pos结点,把pos结点处置为NULL。

整体代码展示

头文件:

#pragma once
#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>

typedef int LTDataType;//类型重命名,方便修改数据类型

typedef struct ListNode
{
	struct ListNode *next;//前指针
	struct ListNode *prev;//后指针
	LTDataType data;
}LTNode;//链表重命名


LTNode* BuyListNode(LTDataType x);//创建结点

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);//判断链表是否为空

// 在pos位置之前插入x
void ListInsert(LTNode* pos, LTDataType x);

//  删除pos位置的节点
void ListErase(LTNode* pos);

int ListSize(LTNode* phead);//求链表的长度

void ListDestroy(LTNode* phead);//链表的销毁

函数实现的源文件:

#include "List.h"

LTNode* BuyListNode(LTDataType x)//创建结点
{
	LTNode * newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

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

	return newnode;
}

LTNode* ListInit()//初始化
{
	LTNode *phead = BuyListNode(-1);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

void ListPrint(LTNode *phead)//打印
{
	assert(phead);

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

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;
	ListInsert(phead->prev, x);

}

void ListPushFront(LTNode *phead, LTDataType x)//头插
{
	assert(phead);
	
	//LTNode* newnode = BuyListNode(x);
	//LTNode* tail = phead->next;

	//phead->next = newnode;
	//newnode->prev = phead;
	//newnode->next = tail;
	//tail->prev = newnode;
	ListInsert(phead->next, x);

}

void ListPopBack(LTNode *phead)//尾删
{
	assert(phead);
	assert(!ListEmpty(phead));

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

	//tail->prev->next = phead;
	//phead->prev = tail->prev;
	//free(tail);
	//tail = NULL;
	ListErase(phead->prev);
}

void ListPopFront(LTNode *phead)//头删
{
	assert(phead);
	assert(!ListEmpty(phead));

	//LTNode* tail = phead->next;

	//tail->next->prev = phead;
	//phead->next = tail->next;

	//free(tail);
	//tail = NULL;
	ListErase(phead->next);
}

bool ListEmpty(LTNode* phead)//判断链表是否为空
{
	assert(phead);

	return phead->next == phead;
}
// 在pos位置之前插入x
void ListInsert(LTNode* pos, LTDataType x)
{
	assert(pos);

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

	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = pos;
	pos->prev = newnode;
}

//  删除pos位置的节点
void ListErase(LTNode* pos)
{
	assert(pos);
	LTNode* tailnext = pos->next;
	LTNode* tailprev = pos->prev;

	tailprev->next = tailnext;
	tailnext->prev = tailprev;

	free(pos);
	pos = NULL;

}

int ListSize(LTNode* phead)//求链表的长度
{
	int size = 0;
	LTNode *cur = phead->next;

	while (cur != phead)
	{
		size++;
		cur = cur->next;
	}
	
	return size;
}

void ListDestroy(LTNode* phead)//链表的销毁
{
	assert(phead);
	LTNode *cur = phead->next;

	while (cur != phead)
	{
		LTNode* next = cur->next;
		ListErase(cur);
		cur = next;
	}

	free(phead);
}

带头双向循环链表总结:

总的来说,这个链表的功能已经十分强大了,如果单独使用链表,我们就一般使用这个链表,虽然链表的结构上复杂些,但是我们写好以后的效率方面很高。在函数的接口方面,其实只要把任意位置的删除和插入写好,这个链表就完成了一大半了,所以在10分钟写一个链表是我完全OK的哦。


今天是小胡杨停更20天的首作,未来一个月还是要日更的。

  • 55
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 46
    评论
评论 46
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沙漠下的胡杨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值