c语言实现双向链表

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

我们知道当要删除一个单链表中目标结点的上一个结点时,需要先遍历单链表,然后找到该目标结点,并且标记目标结点的上一个结点,这样才能实现删除目标结点的上一个结点的操作。而还有一种双向链表,该链表的结点不仅有next指针域存储下一个结点的地址,还有一个prev指针域用来存储上一个结点的地址,这样双向链表删除目标结点的上一个结点就不要再遍历链表了,这只是双向链表的一个特点,双向链表还有很多实用的地方。

一、链表的分类

1、 单向或者双向链表

在这里插入图片描述

2、带头结点或者不带头结点的链表

在这里插入图片描述

3、循环或者非循环链表

在这里插入图片描述
上述这些链表经过组合共有8中链表结构
在这里插入图片描述
虽然有很多种链表结构,但是我们最常用的还是两种结构:
不带头单向非循环链表 即为前面我们介绍的单链表。
不带头单向非循环链表结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多,在刷题中关于链表的题,都是使用单链表来考核。
在这里插入图片描述
带头双向循环链表即为我们这次介绍的链表结构
带头双向循环链表结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现增删操作时反而简单了。
在这里插入图片描述

二、带头双向循环链表

1、链表结点的结构体定义

下面代码就为带头双向循环链表的结点的结构体定义,可以看到相比于单链表,双向链表不仅定义了next指针域用来存储下一个结点的地址,还定义了prev指针域用来存储上一个结点的地址。

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

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

2、链表的初始化

带头双向循环链表是带头结点的,所以链表在初始化时就要创建一个头结点。单链表我们使用了二级指针,因为当单链表为空时,此时指向结构体的指针指向NULL,要改变指向结构体的指针,我们就需要得到指向结构体的指针的地址,所以使用了二级指针。但是双向链表的操作我们只改变结构体里面的next指针域和prev指针域,并不会改变指向结构体的指针,所以只需用一级指针即可。
带头双向循环链表的初始化就是创建一个头结点。即代码如下。

LTNode* CreateNode(LTDataType x)
{
	//创建一个新结点
	LTNode* newNode = (LTNode*)malloc(sizeof(LTNode));
	if (newNode == NULL)
	{
		perror("CreateNode_malloc_fail");
		exit(-1);
	}
	//将结点的数据域改为指定值x,
	newNode->data = x;
	newNode->next = NULL;
	newNode->prev = NULL;
	//将该结点返回
	return newNode;  
}

LTNode* ListInit()
{
	//带头双向循环链表的初始化就是创建一个头结点
	//该结点的数据域不需要存有用数据
	LTNode* phead = CreateNode(-1);
	//当链表只有一个头结点时,此时phead的next还是phead
	//phead的prev是phead,这样设置为了使后面的插入删除的代码也适用于只有一个头结点的链表。
	phead->next = phead;
	phead->prev = phead;

	//将头结点返回
	return phead;
}

3、链表的头插和尾插

链表的头插和尾插。


void ListPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	
	//创建一个结点
	LTNode* newNode = CreateNode(x);
	//记录原来链表的尾结点
	LTNode* tailPrev = phead->prev;
	//将新结点插入到原来的尾结点之后。
	tailPrev->next = newNode;
	newNode->prev = tailPrev;
	phead->prev = newNode;
	newNode->next = phead;
	

	/*
	//还可以不记录原来的尾结点,不过这种操作需要注意前后顺序,不然会发生地址丢失的现象
	//创建一个结点
	LTNode* newNode = CreateNode(x);
	//先将尾结点的指针改变
	phead->prev->next = newNode;
	newNode->prev = phead->prev;

	//上面两语句和下面两语句的顺序不能变
	//再将newNode的指针改变
	phead->prev = newNode;
	newNode->next = phead;
	*/
}


void ListPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	//创建一个新结点
	LTNode* newNode = CreateNode(x);
	//记录头结点后面的结点
	LTNode* headNext = phead->next;
	//将新结点插入在头结点之后。
	newNode->next = headNext;
	headNext->prev = newNode;
	phead->next = newNode;
	newNode->prev = phead;

	/*
	//还可以使用这种不创建结点的方法,不过要注意顺序
	//创建一个新结点
	LTNode* newNode = CreateNode(x);
	phead->next->prev = newNode;
	newNode->next = phead->next;

	//语句执行顺序不能变
	phead->next = newNode;
	newNode->prev = phead;
	*/
}

当链表中插入结点后,我们可以打印结点的值。

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

3、链表的头删和尾删

在删除链表中的结点时,要先判定链表是否为空,如果链表为空,则不可以删除。
判断链表是否为空。

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

头删和尾删。

void ListPopBack(LTNode* phead)
{
	assert(phead);
	//如果链表为空,则会断言
	//assert(!ListEmpty(phead));
	
	//也可以这样判断
	assert(phead->next != phead);
	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;
	tailPrev->next = phead;
	phead->prev = tailPrev;
	free(tail);
}


void ListPopFront(LTNode* phead)
{
	assert(phead);
	//如果链表为空,则会断言
	//assert(!ListEmpty(phead));

	//也可以这样判断
	assert(phead->next != phead);

	LTNode* headNext = phead->next;
	LTNode* headNextNext = headNext->next;

	phead->next = headNextNext;
	headNextNext->prev = phead;
	free(headNext);
}

4、链表指定位置结点的插入和删除

void ListInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* newNode = CreateNode(x);
	LTNode* posPrev = pos->prev;
	posPrev->next = newNode;
	newNode->prev = posPrev;
	newNode->next = pos;
	pos->prev = newNode;
}

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

5、链表的头插尾插头删尾删使用ListInsert和ListErase实现

当链表的ListInsert和ListErase函数实现后,则上述实现的头插尾插头删尾删就可以使用这两个函数来实现。
尾插

void ListPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	//在phead结点之前插入新结点就相当于尾插。
	ListInsert(phead, x);
}

头插


void ListPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	//在phead的next结点之前插入一个结点,就相当于头插法
	ListInsert(phead->next, x);
}

尾删

void ListPopBack(LTNode* phead)
{
	assert(phead);
	//如果链表为空,则会断言
	//assert(!ListEmpty(phead));
	
	//也可以这样判断
	assert(phead->next != phead);
	
	//删除phead的上一个结点,就相当于删除尾结点
	ListErase(phead->prev);
}

头删

void ListPopFront(LTNode* phead)
{
	assert(phead);
	//如果链表为空,则会断言
	//assert(!ListEmpty(phead));

	//也可以这样判断
	assert(phead->next != phead);
	
	//删除phead的下一个结点就相当于头删
	ListErase(phead->next);
}

6、链表长度

求链表时需要遍历一遍链表,然后得出链表长度并返回。

int ListSize(LTNode* phead)
{
	assert(phead);
	LTNode* curr = phead;
	int size = 0;
	while (curr != phead)
	{
		++size;
		curr = curr->next;
	}
	return size;
}

虽然有的地方说过可以使用哨兵位的数据域来存储链表的长度,但这只适用于链表结点的数据域为int的类型时才可以,当数据域类型为char或double等其他类型时,就不能存储链表的长度了。

7、链表的销毁

当使用完链表后,就需要对链表的空间进行释放,此时需要将链表的每一个结点都释放。

void ListDestory(LTNode* phead)
{
	assert(phead);
	LTNode* curr = phead->next;
	while (curr != phead)
	{
		LTNode* next = curr->next;
		ListErase(curr);
		curr = next;
	}
	free(phead);  //释放头结点
	//此时的phead为plist的拷贝,所以将phead存的地址置为NULL,但是plist存的地址不会为NULL,此时plist还是指向被释放的头结点。
	//所以想要改变plist存的地址,只能将plist的地址传进来,或者在函数外面该。
	phead = NULL;  
}

在调用ListDestory函数之后,将传入的实参置为NULL,此时plist才不是野指针。因为调用函数时传参传的是一级指针,所以在函数中将phead=NULL,而主函数中plist还是指向那个被释放的头结点的地址,此时plist为野指针,所以要在主函数中将plist置为NULL。
在这里插入图片描述

总结

当我们写出来指定位置插入结点和删除结点的ListInsert和ListErase方法后,头插尾插头删尾删都可以用这两个方法实现,所以当我们需要快速写出来一个双向链表的操作时,可以直接写出来这两个函数,这两个函数实现了,其他的操作都可以调用这两个函数来实现。
顺序表和链表各有各的优缺点,应用的场景也不相同。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值