数据结构-链表(详解)

前言:内容包括:链表的分类,无头单向非循环链表的增删查改的实现,带头双向循环链表的增删查改的实现

目录

链表的分类

1. 单向或者双向

​编辑

2. 带头或者不带头 

 3. 循环或者非循环

无头单向非循环链表:

​编辑 带头双向循环链表:

 链表的实现

 1、无头+单向+非循环链表增删查改实现

单链表结构:

动态申请一个节点:

单链表打印:

单链表销毁:

单链表尾插:

单链表的头插:

单链表的尾删:

单链表头删 :

单链表查找:

在某个节点之前插入:

在某个节点之后插入:

删除pos位置的值:

删除pos之后的值:

2、带头+双向+循环链表增删查改实现

双向链表结构:

动态申请一个节点:

双向链表初始化(创建哨兵位的头结点):

双向链表销毁:

双向链表打印 :

双向链表尾插 :

双向链表头插:

双向链表尾删 :

双向链表头删 :

双向链表查找 :

双向链表在pos的前面进行插入:

双向链表删除pos位置的结点 :


链表的分类

组合起来共有8种结构:

1. 单向或者双向

单向:

双向 :

 

2. 带头或者不带头 

不带头:

带头(哨兵位的头结点):

 哨兵位的头结点不存储有效数据,站岗功能

 3. 循环或者非循环

非循环:

 循环:

虽然链表的组合有8种结构,但是最常用还是一下两种结构:

无头单向非循环链表:

一般不会单独用来存数据,更多是作为其他数据结构的子结构,如哈希桶、图的邻接表

 带头双向循环链表:

一般用来单独存数据

 链表的实现

 1、无头+单向+非循环链表增删查改实现

在main函数种,plist指针作为头指针,负责在单链表创建后,指向单链表的第一个节点

SLTNode* plist = NULL;

单链表结构:

typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

动态申请一个节点:

SLTNode* BuySLTNode(SLTDataType x);

malloc一块空间并存入数据后,此函数将会返回这块空间的地址 

SLTNode* BuySLTNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");//若malloc开辟失败,打印错误信息
		return NULL;
	}

	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

单链表打印:

void SLTPrint(SLTNode* phead);
void SLTPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

单链表销毁:

void SLTDestroy(SLTNode** pphead);

当链表的每个节点销毁完成后,指向第一个节点的指针plist需要置空,所以传入plist指针的地址(二级指针接收),plist必须置空,否则成为野指针

void SLTDestroy(SLTNode** pphead)
{
	SLTNode* cur = *pphead;
	while (cur)
	{
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;
}

在释放一个节点之前,需要找到下一个节点,所以先用next指针保存下一个节点的地址,然后才能放心销毁当前节点

单链表尾插:

void SLTPushBack(SLTNode** pphead, SLTDataType x);

尾插分为两种情况:

空链表尾插 和 非空链表尾插

空链表尾插:需要改变头指针plist,使得plist指向插入的第一个节点

所以存在对plist指针进行修改的情况,故而需要传入plist的指针,用二级指针pphead来接收

非空链表尾插: 遍历找到尾节点进行链接

void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);//pphead是头指针plist的地址一定不为空,用assert进行断言,若是pphead为空会报错

	SLTNode* newnode = BuySLNode(x);
	if (*pphead == NULL)//空链表尾插
	{
		*pphead = newnode;//plist需要指向第一个节点
	}
	else//非空链表尾插
	{
		SLTNode* tail = *pphead;
		while (tail->next)//找到尾节点,尾节点的next就是NULL
		{
			tail = tail->next;
		}
		tail->next = newnode;//链接
	}
}

关于assert断言:

pphead一定不能为空,所以需要断言,当pphead为空时,报错

*pphead可以为空,这表明链表为空,空链表是可以插入数据的,打个比方:

银行卡里没钱了,存钱是可以的 

单链表的头插:

void SLTPushFront(SLTNode** pphead, SLTDataType x);

头插也分两种:

空链表头插:需要对plist指针进行修改,使得它指向插入的第一个节点

非空链表头插:1 新节点链接头结点 2 plist指针指向新节点(新节点成为新的头结点)

但是这两种情况下的头插方法都是通用的,空链表头插代码可以与非空链表头插共用一份代码

原因:空链表时,plist==NULL,即*pphead==NULL

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);

	SLTNode* newnode = BuySLNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

单链表的尾删:

void SLTPopBack(SLTNode** pphead);

尾删分两种情况:

链表仅有1个节点:删除此节点后,plist指针需要置空

链表不只1个节点:找到尾节点的前一个结点,当尾结点删除后,它将成为新的尾结点,则其成员next指针需要置空 

void SLTPopBack(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead);//链表为空不能删

	if ((*pphead)->next == NULL)//链表仅有一个结点,它的next就是NULL
	{
		free(*pphead);
		*pphead = NULL;//plist指针置空
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next->next)//找到尾结点的前一个结点,它将会是新的tail,则tail->next就是原来的尾结点,原来的尾结点的next指针就是NULL
		{
			tail = tail->next;
		}
		free(tail->next);//删除尾结点
		tail->next = NULL;//新的尾结点的next要置空
	}
}

单链表头删 :

void SLTPopFront(SLTNode** pphead);

头删也分两种情况:

链表仅有1个节点:删除此节点后,plist指针要置空

 链表不止1个节点:1 保存要删除的节点地址 2 头指针plist指向新的头结点 3 删除节点

但是这两种情况也共用一份代码

原因:当链表仅有1个节点,它没有下一个节点,所以它的next指针指向了NULL,

当删除这个节点后,plist会置成NULL

void SLTPopFront(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead);//链表为空不能删除

	SLTNode* del = *pphead;//保存要删除的节点地址
	*pphead = (*pphead)->next;//plist指向新的头结点
	free(del);
}

单链表查找:

SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

在某个节点之前插入:

1 使用单链表查找函数找到某个节点的地址pos

2 在pos之前插入

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);

 使用二级指针pphead:若是pos是第一个结点,则在pos之前插入就是头插逻辑,会对plist指针进行修改,所以需要传plist的地址

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);
	assert(pos);
	if (*pphead == pos)//pos是头结点
	{
		SLTPushFront(pphead,x);//在头结点之前插入即头插
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)//找到pos节点的前一个结点prev
		{
			prev = prev->next;
		}
		SLTNode* newnode = BuySLNode(x);
		prev->next = newnode;//链接
		newnode->next = pos;
	}
}

在某个节点之后插入:

void SLTInsertAfter(SLTNode* pos, SLTDataType x);

不需要传入plist,因为在pos之后插入,可以轻而易举得到pos之后所有结点的地址

传入plist,可以认为需要找到某一个结点的前一个结点才需要plist,因为通过遍历才可以找到某个结点的前一个结点

void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySLNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

删除pos位置的值:

void SLTErase(SLTNode** pphead, SLTNode* pos)

存在删除的pos位置是头结点的情况,需要对plist进行修改,所以需要二级指针 

无需assert(*pphead)

因为assert(pos)已经保证了pos位置的有效,也保证链表不为空,若是pos位置有效(即不为NULL),则说明链表不会为空

void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);
	if (*pphead == pos)//头删
	{
		SLTPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)//找到pos的前一个结点
		{
			prev = prev->next;
		}
		prev->next = pos->next;//前一个结点的next指向pos的下一个结点
		free(pos);
	}
}

删除pos之后的值:

void SLTEraseAfter(SLTNode* pos);
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	assert(pos->next);//不能对NULL进行删除操作,若是pos是尾结点的地址,则pos->next为NULL

	SLTNode* next = pos->next;
	pos->next = next->next;
	free(next);
}

2、带头+双向+循环链表增删查改实现

双向链表结构:

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

动态申请一个节点:

LTNode* BuyLTNode(LTDataType x)
LTNode* BuyLTNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

 

双向链表初始化(创建哨兵位的头结点):

LTNode* LTInit();

开辟一个结点,不存储任何有效数据,作为哨兵位的头结点

当链表为空时,这个哨兵位的头结点需要自己构成一个循环,即自己的next指向自己,自己的prev指向自己 

LTNode* LTInit()
{
	LTNode* phead = BuyLTNode(-1);
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

双向链表销毁:

void LTDestroy(LTNode* phead);

释放完所有的有效节点后才能释放哨兵位的头结点 

void LTDestroy(LTNode* phead)
{
	assert(phead);//phead是指向哨兵位的头结点的指针,一定不能为空

	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;//记录下一个要释放节点的地址
		free(cur);
		cur = next;
	}
	free(phead);//释放哨兵位的头结点
}

双向链表打印 :

void LTPrint(LTNode* phead);
void LTPrint(LTNode* phead)
{
	assert(phead);

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

双向链表尾插 :

void LTPushBack(LTNode* phead, LTDataType x);
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;

	//或者
	//LTInsert(phead,x);
}

1 尾节点的next指向newnode

2 newnode的prev指向尾节点

3 newnode的next指向哨兵位的头结点

4 哨兵位头结点的prev指向newnode

双向链表头插:

void LTPushFront(LTNode* phead, LTDataType x);
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* first = phead->next;//哨兵位的头结点后的真正的头结点
	LTNode* newnode = BuyLTNode(x);

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

	newnode->next = first;
	first->prev = newnode;

	//或者
	//LTInsert(phead->next, x);
}

双向链表尾删 :

双向链表判空:链表若为空,则仅有一个哨兵位的头结点,它既是头,也是尾

bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next == phead;
}
void LTPopBack(LTNode* phead)
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));//链表为空不能删

	LTNode* tail = phead->prev;
	LTNode* tailprev = tail->prev;//找到尾节点的上一个节点,它将成为新的尾节点

	free(tail);
	tailprev->next = phead;//新的尾节点要连接哨兵位的头结点
	phead->prev = tailprev;

	//或者
	//LTErase(phead->prev);
}


双向链表头删 :

void LTPopFront(LTNode* phead)
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));//链表为空不能删

	LTNode* first = phead->next;
	LTNode* second = first->next;//真正头结点的下一个节点second会成为新的真正头结点,链接哨兵位的头结点
	
	phead->next = second;
	second->prev = phead;
	free(first);

	//或者
	//LTErase(phead->next);
}


双向链表查找 :

LTNode* LTFind(LTNode* phead, LTDataType x)
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的前面进行插入:

pos是某个节点的地址,可以通过查找函数得到

void LTInsert(LTNode* pos, LTDataType x)
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);

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

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

双向链表删除pos位置的结点 :

void LTErase(LTNode* pos)
void LTErase(LTNode* pos)
{
	assert(pos);

	LTNode* posPrev = pos->prev;//找到pos的前一个结点
	LTNode* posNext = pos->next;//找到pos的后一个结点

	posPrev->next = posNext;//两个链接
	posNext->prev = posPrev;
	free(pos);
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值