初识数据结构之入门必懂——链表(双链表)及顺序表与链表的对比

发布于:2022-12-16 ⋅ 阅读:(910) ⋅ 点赞:(0)


前言

对于线性表中最基础的2种数据结构,顺序表与单链表,我们在此进行对比。
随后,我们继续深入学习链表的第二种形式——循环带头双链表


一、顺序表与单链表对比

存储结构 顺序表 (单)链表
物理存储 连续 非连续
是否支持随机访问 支持 不支持
增加数据方法 适合尾部操作 适合头部操作
动态内存分配 可能浪费空间 不会浪费空间
增容 需要申请新空间,释放旧空间,耗时 仅需开辟新节点
对数据操作难易程度 较简单 较困难
缓存利用率

若需频繁对内部数据进行操作,一般选用顺序表存储数据
若需频繁增加,删除数据,而对数据本身无过多操作,则选用链表

二、对于双链表函数的定义以及实现

1.对于双链表节点的定义

代码如下(示例):

typedef int LTDataType;
typedef struct ListNode
{
	struct ListNode* next;//下一节点
	struct ListNode* prev;//上一节点
	LTDataType data;
}LTNode;

2.对于双链表函数的定义与实现

代码如下(示例):

LTNode* ListInit();
void ListDestory(LTNode* phead);
LTNode* BuyListNode(LTDataType x);
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);
// pos位置之前插入
void ListInsert(LTNode* pos, LTDataType x);
// 删除pos位置
void ListErase(LTNode* pos);

此处双链表接口函数,如果需要传输地址,全部只传一级指针。
这是因为,双链表带头结点,无论有无节点,都不需要改变头结点地址。这同时也是单链表不传二级指针的第二种方法,只需要给它加个头节点即可。

接下来,我们对上述函数一一实现。

LTNode* ListInit()
{
	LTNode* head=(LTNode*)malloc(sizeof(LTNode));
	assert(head);//申请空间失败直接中止
	head->next=head->prev=head;//如果置空,则不满足循环
	return head;
	//表头不存数据,所以不初始化头节点的data
}

此函数就使用返回值LTNode*,从而避免传递二级指针,目的在于保证接口函数的一致性

void ListDestory(LTNode* phead)
{
	assert(phead);//若phead为NULL,则中止函数
	LTNode* cur=phead->next,*del=NULL;
	while(cur!=phead)
	{
		del=cur;
		free(del);
		cur=cur->next;
		del=NULL;//好习惯不能丢,其实丢了也行
	}
	free(phead);
}

需要和单链表区别的点在于while循环的终止条件,由于它循环,所以不能是指空

LTNode* BuyListNode(LTDataType x)
{
	LTNode* newnode=(LTNode*)malloc(sizeof(LTNode));
	assert(newnode);//检查一下是否成功
	newnode->next=newnode->prev=NULL;
	newnode->data=x;
	return newnode;
}

创造理由:简化其他函数

void ListPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);//若phead为NULL,则中止函数
	LTNode* newnode=BuyListNode(x);
	phead->prev->next=newnode;
	newnode->prev=phead->prev;
	newnode->next=phead;
	phead->prev=newnode;
}

相比单链表的找尾,是多么的简单友好啊!!

void ListPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);//若phead为NULL,则中止函数
	LTNode* newnode=BuyListNode(x);
	phead->next->prev=newnode;
	newnode->next=phead->next;
	newnode->prev=phead;
	phead->next=newnode;
}

相比单链表的头插要传二级指针,更改首节点,是多么的友好啊!!!

void ListPopBack(LTNode* phead)
{
	assert(phead);//若phead为NULL,则中止函数
	if(phead->prev==phead)
	return ;
	//assert(!ListEmpty(phead));暴力检查
	else
	{
		LTNode* del=phead->prev;
		phead->prev->prev->next=phead;
		phead->prev=phead->prev->prev;//这么写,纯属好玩
		free(del);
		del=NULL;//好习惯
	}
}
void ListPopFront(LTNode* phead)
{
	assert(phead);//若phead为NULL,则中止函数
	if(phead->next==phead)
	return ;
	//assert(!ListEmpty(phead));暴力检查
	else
	{
		LTNode* del=phead->next;
		phead->next->next->prev=phead;
		phead->next=phead->next->next;//这么写,纯属好玩
		free(del);
		del=NULL;//好习惯
	}
}

其实这里函数的实现非常简单,每次看这些函数,都让人直呼想哭。
唯一需要注意的就是改变节点的prev和next时要注意顺序,但这和单链表比起来,无论是难易程度还是效率,双链表完胜

bool ListEmpty(LTNode* phead)
{
	assert(phead);//若phead为NULL,则中止函数
	return phead->next==phead;
}
size_t ListSize(LTNode* phead)
{
	assert(phead);//若phead为NULL,则中止函数
	size_t i=0;
	LTNode* cur=phead->next;
	while(cur!=phead)
	{
		cur=cur->next;	
		i++;
	}
	return i;
}
LTNode* ListFind(LTNode* phead, LTDataType x)
{
	assert(phead);//若phead为NULL,则中止函数
	LTNode* cur=phead->next;
	while(cur!=phead)
	{
		if(cur->data==x)
		return cur;
		cur=cur->next;	
	}
	printf("未找到值为X的节点\n");//可有可无
	return NULL;
}
// pos位置之前插入
void ListInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* newnode=BuyListNode(x);
	pos->prev->next=newnode;
	newnode->prev=pos->prev;
	newnode->next=pos;
	pos->prev=newnode;
}

注意交换顺序即可

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

总结

综上,我们已经完全掌握了线性表最基础的两种,并且对比了它们的优缺点,知道了应该在什么情况下使用哪一种。

本文含有隐藏内容,请 开通VIP 后查看