【数据结构】双链表

大家好!今天我们来学习数据结构中的双链表。(我们这里讲解的是带头(哨兵位)双向循环链表哦~)

目录

1. 双链表的概念

2. 双链表的逻辑结构

3. 双链表的定义

4. 双链表的接口实现

4.1 动态申请一个新结点

4.2 双链表的初始化

4.3 打印双链表

4.4 尾插数据

4.5 尾删数据

4.6 头插数据

4.7 头删数据

4.8 获得双链表的长度

4.9 查找指定数据

4.10 在指定位置之前插入数据

4.11 删除指定位置

4.12 销毁双链表

5. 双链表的完整代码

5.1 List.h

5.2 List.c

5.3 Test.c

6. 顺序表和链表的区别

7. 总结


1. 双链表的概念

双链表也叫双向链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双链表中的任意一个结点开始,都可以很方便地访问它的前驱结点后继结点

2. 双链表的逻辑结构

我们这里以带头双向循环链表为例,它的逻辑结构如下:

我们从中也可以发现,双链表相比单链表的优势是方便找某一个结点的前一个结点

3. 双链表的定义

typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;  //存储的数据
	struct ListNode* prev;  //存放前一个结点的地址
	struct ListNode* next;  //存放后一个结点的地址
}LTNode;

使用结构体创建一个双链表

SLTDataType替换int,方便对不同类型的数据进行修改。

SLTNode替换struct SListNode,方便简洁。

data是结点的数据域*prev用来存放前一个结点的地址(前驱)*next用来存放后一个结点的地址(后继)

4. 双链表的接口实现

双链表的所有接口函数一览:

//动态申请一个新结点
LTNode* BuyLTNode(LTDataType x);
//双链表的初始化
LTNode* LTInit();
//打印双链表
void LTPrint(LTNode* phead);
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//尾删
void LTPopBack(LTNode* phead);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//头删
void LTPopFront(LTNode* phead);
//获得双链表的长度
int LTSize(LTNode* phead);
//查找
LTNode* LTFind(LTNode* phead, LTDataType x);
//在pos位置之前插入x
void LTInsert(LTNode* pos,LTDataType x);
//删除pos位置
void LTErase(LTNode* pos);
//销毁双链表
void LTDestroy(LTNode* phead);

这些接口函数主要实现了单链表的增删改查等功能,接下来我们一一实现这些函数!

4.1 动态申请一个新结点

我们每次给链表插入数据时,都需要动态开辟空间申请结点。所以我们将这个过程封装成函数,方便后续使用。

我们使用malloc()函数动态开辟一块空间表示新结点newnode,malloc函数返回一个void*类型的指针,指向分配的内存块的起始位置。如果内存分配失败,则返回一个空指针NULL。

所以我们要判断newnode是否为空指针NULL如果newnode是空指针,则用perror()函数打印相关错误,并用exit(-1)退出程序

如果newnode不为空,我们就用newnodedata赋值。又因为这是新开辟的结点,我们暂时将newnodeprevnewnodenext指向空

//动态申请一个新结点
LTNode* BuyLTNode(LTDataType x)
{
	LTNode* newnode = (LTNode *)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

4.2 双链表的初始化

双链表的初始化,就是给双链表创建一个头结点。因为头结点(哨兵位)不存储有效数据,所以我们将头结点的data赋值为-1,同时让头结点的prev和next都指向自己,最后返回头结点的地址。

//双链表的初始化
LTNode* LTInit()
{
	LTNode* phead = BuyLTNode(-1);
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

4.3 打印双链表

遍历双链表,依次打印双链表的元素。

我们定义一个结构体类型的指针cur,让cur一开始指向头结点的下一个结点(也就是哨兵位后面的一个结点)。当cur不为空时,输出cur指向的结点的值(cur->data),然后让cur指向下一个结点(cur=cur->next),依次进行,直到cur为头结点时停止(因为最后一个结点的next指针指向头结点)

//打印双链表
void LTPrint(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	printf("phead<=>");
	while (cur != phead)
	{
		printf("%d<=>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

4.4 尾插数据

尾插,就是创建一个新结点newnode,然后将newnode插入到尾结点tail的后面,让tail的next指向newnode,让newnode的prev指向tail;让newnode的next指向头结点phead头结点phead的prev指向newnode。建立这样的连接后,尾插就完成了

//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* tail = phead->prev;
	LTNode* newnode = BuyLTNode(x);

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

4.5 尾删数据

尾删,我们需要定义一个tailPrev存储尾结点tail的前一个结点(也就是tail->prev),再free掉tail,让tailPrev的next指向头结点phead,让头结点phead的prev指向tailPrev

这里要注意的是,如果链表为空(phead->next==phead),我们就不能进行尾删,所以我们要用assert()进行断言。

//尾删
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(phead->next!= phead); //链表为空的情况
	
	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;
	free(tail);
	tailPrev->next = phead;
	phead->prev = tailPrev;
}

4.6 头插数据

所谓头插,就是在双链表的头结点(哨兵位)后面的一个结点前插入数据。我们调用BuyLTNode()函数创建一个新结点newnode,让newnode的next指向头结点phead的next头结点phead的next的prev指向newnode;让头结点phead的next指向newnodenewnode的prev指向phead

//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = BuyLTNode(x);

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

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

4.7 头删数据

头删,就是将头结点(哨兵位)后面的那一个结点删除。这里我们可以用first存储头结点(哨兵位)后面的第一个结点,用second存储哨兵位后面的第二个结点。然后free掉first。将头结点phead的next指向second而second的prev指向头结点

这里也要注意,如果链表为空(phead->next==phead),我们就不能进行头删,所以我们要用assert()进行断言。

​ 

//头删
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);  //链表为空的情况
	LTNode* first = phead->next;
	LTNode* second = phead->next->next;

	free(first);

	phead->next = second;
	second->prev = phead;
}

4.8 获得双链表的长度

要获得双链表的长度,我们就使用cur从头结点(哨兵位)的后一个结点开始遍历,直到cur等于头结点phead时停止

//获得双链表的长度
int LTSize(LTNode* phead)
{
	assert(phead);
	int size = 0;
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		++size;
		cur = cur->next;
	}
	return size;
}

4.9 查找指定数据

定义一个结构体指针cur,让cur首先指向头结点(哨兵位)的下一个结点,然后遍历双链表,如果找到了指定数据cur->data==x),就直接返回cur。否则让cur指向cur->next,直到cur为头结点时停止。如果没有提前退出,完整完成了整个循环(也就是没有找到指定数据),就返回空指针NULL

//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
			return cur;

		cur = cur->next;
	}
	return NULL;
}

4.10 在指定位置之前插入数据

我们调用BuyLTNode()函数创建一个新结点newnode,定义一个结构体指针posPrev用来保存pos位置的前一个位置,让posPrev的next指向newnodenewnode的prev指向posPrev;让newnode的next指向pospos的prev指向newnode

既然我们要在指定位置之前插入数据,那么这个指定位置必须是存在的,所以我们要使用assert()断言。

//在pos位置之前插入x
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* posPrev = pos->prev;
	LTNode* newnode = BuyLTNode(x);

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

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

4.11 删除指定位置

我们要删除指定位置,可以定义一个结构体指针posPrev保存要删除位置的前一个位置,定义一个结构体指针posNext保存要删除位置的后一个位置。然后free掉posposPrev的next指向posNextposNext的prev指向posPrev

既然要删除指定位置,那么这个指定位置也必须是存在的,这里也同样要用assert()断言。

//删除pos位置
void LTErase(LTNode* pos)
{
	assert(pos);
	LTNode* posPrev = pos->prev;
	LTNode* posNext = pos->next;
	free(pos);
	posPrev->next = posNext;
	posNext->prev = posPrev;
}

4.12 销毁双链表

我们先让cur指向头结点(哨兵位)的下一个结点,然后遍历双链表,定义一个结构体指针next用来保存遍历时每一个结点的后面一个结点,依次free每个结点然后让cur指向next,直到cur指向头结点时停止

最后将头结点(哨兵位)phead释放

//销毁双链表
void LTDestroy(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
}

5. 双链表的完整代码

5.1 List.h

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

typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;  //存储的数据
	struct ListNode* prev;  //指向前一个结点的指针
	struct ListNode* next;  //指向后一个结点的指针
}LTNode;

//动态申请一个新结点
LTNode* BuyLTNode(LTDataType x);
//双链表的初始化
LTNode* LTInit();
//打印双链表
void LTPrint(LTNode* phead);
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//尾删
void LTPopBack(LTNode* phead);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//头删
void LTPopFront(LTNode* phead);
//获得双链表的长度
int LTSize(LTNode* phead);
//查找
LTNode* LTFind(LTNode* phead, LTDataType x);
//在pos位置之前插入x
void LTInsert(LTNode* pos,LTDataType x);
//删除pos位置
void LTErase(LTNode* pos);
//销毁双链表
void LTDestroy(LTNode* phead);

5.2 List.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"

//动态申请一个新结点
LTNode* BuyLTNode(LTDataType x)
{
	LTNode* newnode = (LTNode *)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

//双链表的初始化
LTNode* LTInit()
{
	LTNode* phead = BuyLTNode(-1);
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

//打印双链表
void LTPrint(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	printf("phead<=>");
	while (cur != phead)
	{
		printf("%d<=>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* tail = phead->prev;
	LTNode* newnode = BuyLTNode(x);

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

//尾删
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(phead->next!= phead); //链表为空的情况
	
	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;
	free(tail);
	tailPrev->next = phead;
	phead->prev = tailPrev;
}

//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = BuyLTNode(x);

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

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

//头删
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);  //链表为空的情况
	LTNode* first = phead->next;
	LTNode* second = phead->next->next;

	free(first);

	phead->next = second;
	second->prev = phead;
}

//获得双链表的长度
int LTSize(LTNode* phead)
{
	assert(phead);
	int size = 0;
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		++size;
		cur = cur->next;
	}
	return size;
}

//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
			return cur;

		cur = cur->next;
	}
	return NULL;
}

//在pos位置之前插入x
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* posPrev = pos->prev;
	LTNode* newnode = BuyLTNode(x);

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

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

//删除pos位置
void LTErase(LTNode* pos)
{
	assert(pos);
	LTNode* posPrev = pos->prev;
	LTNode* posNext = pos->next;
	free(pos);
	posPrev->next = posNext;
	posNext->prev = posPrev;
}

//销毁双链表
void LTDestroy(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
}

5.3 Test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"

void TestList()
{
	LTNode* plist = LTInit();
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPushBack(plist, 5);
	LTPrint(plist);

	LTPushFront(plist, 10);
	LTPushBack(plist, 10);
	LTPrint(plist);

	LTPopBack(plist);
	LTPopFront(plist);
	LTPrint(plist);

	LTPushFront(plist, 10);
	LTPushFront(plist, 20);
	LTPushFront(plist, 30);
	LTPushFront(plist, 40);
	LTPrint(plist);

	LTPopFront(plist);
	LTPrint(plist);

	LTPopBack(plist);
	LTPrint(plist);

	LTDestroy(plist);
	plist = NULL;
}

6. 顺序表和链表的区别

存储器层次结构:

顺序表

优点:下标随机访问,cpu高速缓存命中率高

缺点:头部和中间插入删除效率低,扩容有一定程度性能消耗,可能存在一定程度的空间浪费。

链表

优点:可以任意位置插入删除,复杂度O(1),能够按需申请释放。

缺点:不支持下标随机访问。

7. 总结

到这里,我们就用C语言实现了数据结构中的双链表。有什么问题欢迎在评论区讨论。如果觉得文章有什么不足之处,可以在评论区留言。如果喜欢我的文章,可以点赞收藏哦!

  • 23
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 16
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值