数据结构:链表

一.引入

上一篇的顺序表我们提到了,顺序表是存在它的局限性的,而且从更多角度上来说,顺序表使用起来并不是那么便捷,所以这一篇我们来讲解一下链表是如何使用的。

二.单链表概念及结构

1.概念
概念:链表是一种 物理存储结构上非连续 、非顺序的存储结构,数据元素的 逻辑顺序 是通过链表中的 指针链 次序实现的
2.结构小复习

这里的next是一个struct SListNode类型的指针,不是结构体

上面//struct SListNode是类型名,下面的需要在}后SLTNode

3.结构详解
void SLTPrint(SLTNode * phead)
{
	SLTNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

 cur指向的是地址而不是数据,cur先指向地址,如果不是空地址,则打印出数据,再指向下一个地址,就是先有地址,再有数据。

下面将顺序表与链表对比:

(1).

顺序表需要断言,而链表不需要断言:

因为链表即便是NULL,也可以打印,无非是空链表,但是顺序表必须断言,因为ps指针指向的不是结构体数据,而是a,size,capacity,也就是说即使没有数据,a可以空,但是ps指向的这块空间不能为空,况且顺序表取决于size的大小而并非a中的数据。

(2).

 这里不可以cur++,因为链表不是顺序表,不是连续的,顺序表++是下一个位置但是链表不是。

 (3).

 这里不可以蓝框这么写,因为从结构示意图可以知道,在cur->next指向最后一个数据下一位的时候才是NULL,这样会导致最后一个数据无法被打印出。

 

 三.单链表接口实现

1.头文件
#pragma once

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

typedef int SLTDataType;

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

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

void SLTPrint(SLTNode* phead);
void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPushFront(SLTNode** pphead, SLTDataType x);

void SLTPopBack(SLTNode** pphead);
void SLTPopFront(SLTNode** pphead);


// 
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
// pos֮ǰ
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
// posλɾ
void SLTErase(SLTNode** pphead, SLTNode* pos);

// pos
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
// posλúɾ
void SLTEraseAfter(SLTNode* pos);
2.打印链表
void SLTPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}
3.扩容操作
SLTNode* BuySLTNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;
}
4.尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
    assert(pphead);
	SLTNode* newnode = BuySLTNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}
细节解说

1.链表和顺序表最大的不同就是链表必须是逻辑连接的,也就是必须有xxxx->next这样的写法连接起来,链表之间不是挨着的,必须逻辑上->next才能找到下一个目标,实现连接。

2.二级指针使用的目的:

下面是使用一级指针对应的效果

 可以发现一级指针对单链表没有任何作用,这是因为我们只是传了形参,而没有把地址传过去,所以实际的内容没有发生改变。

我们大可以想象为:数据和地址存储在一个指针里,而这些指针存储在大的单链表里,因此改变数据要用一级指针,但是改变链表中的指针(如上图,删除链表中的最后一块数据,包含数据和地址)这就是改变整个链表,就必须要用二级指针了。

5.头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
    assert(pphead);
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}
6.尾删
void SLTPopBack(SLTNode** pphead)
{
    assert(pphead);
	//暴力检查
	assert(*pphead!=NULL);
	//温柔检查
	if ((*pphead) == NULL)
		return;
	//考虑单节点的情况
	if ((*pphead)->next = NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* prev = NULL;
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		tail = NULL;
	}
}
找尾细节解说
1.错误写法以及链表与顺序表的区别

 可以发现,这个写法会出现bug,也就是说没有成功完成尾删,关键就在于红框内的内容tail=NULL,这个写法是有问题的,因为我们知道,链表不是连续的,如果要找到链表中节点的位置就必须使用xxxx->next查找,没有连接的话是无法进行操作的。

顺序表是一整块连续的空间,即使异地扩容,也是一整块的,连续的;链表是多个不连续的空间通过指针连接成的一块联合区域,物理上不连续,逻辑上连续。

2.正确写法

 上面两个图原理上是一样的,都是正确写法。

7.头删
void SLTPopFront(SLTNode** pphead)
{
    assert(pphead);
	assert(*pphead);
	SLTNode* front = *pphead;
	*pphead = front->next;
	free(front);
	front = NULL;
}
8.查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}
9.插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pos);
	assert(pphead);
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		SLTNode* newnode = BuySLTNode(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}

插入原理:

pphead的断言

对每一个我们进行增删的,我们都要对pphead进行断言,因为pphead是plist的地址,我们的pphead使用的是二级指针,plist(链表)可以为空,但是pphead(地址)不能为空,*pphead是plist,所以对于引入链表,必须对pphead进行断言,而*pphead(plist)则在进行删除操作的时候必须断言。

10.删除
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);
	assert(*pphead);
	if (pos = *pphead)
	{
		SLTPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
	}
}

这个是最正常的一个思路,还有一个别样的方法:

交换法

这个只要不是尾结点就可以,不用头结点也可以。上图是插入,下图是删除,都是通过交换数据,改变链表链接完成的。

11.pos后面插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

12.pos后面删除
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	assert(pos->next);
	SLTNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}

四.链表的分类

链表一共有8种

1. 无头单向非循环链表: 结构简单 ,一般不会单独用来存数据。实际中更多是作为 其他数据结构的子结 ,如哈希桶、图的邻接表等等。另外这种结构在 笔试面试 中出现很多。
2. 带头双向循环链表: 结构最复杂 ,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单。

五.双向循环链表接口实现

1.头文件
#include<stdio.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* LTInit();
void LTDestroy(LTNode* phead);
void LTPrint(LTNode* phead);
bool LTEmpty(LTNode* phead);

void LTPushBack(LTNode* phead, LTDataType x);
void LTPopBack(LTNode* phead);

void LTPushFront(LTNode* phead, LTDataType x);
void LTPopFront(LTNode* phead);

// posλ????
void LTInsert(LTNode* pos, LTDataType x);
void LTErase(LTNode* pos);
 2.扩容操作
LTNode* BuyListNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	node->prev = NULL;
	node->next = NULL;
	node->data = x;

	return node;
}
3.初始化链表
LTNode* LTInit()
{
	LTNode* phead = BuyListNode(-1);
	phead->next = phead;//尾的next指向哨兵位的头
	phead->prev = phead;//哨兵位的头得prev指向链表的尾
	return phead;
}
4.打印双向循环链表
void LTPrint(LTNode* phead)
{
	assert(phead);
	printf("<=haed=>");
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d=>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}
5.判断是否为空链表并返回bool值
bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next == phead;
}
6.双向循环链表的尾插

 

7.双向循环链表的尾删
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));

	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;
	tailPrev->next = phead;
	phead->prev = tailPrev;
	free(tail);
	tail = NULL;
}
8.双向循环链表的头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* newnode = BuyListNode(x);
	LTNode* first = phead->next;
	phead->next = newnode;
	newnode->prev = phead;
	newnode->next = first;
	first->prev = newnode;
}
9.双向循环链表的头删
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));

	LTNode* first = phead->next;
	LTNode* firstPrev = first->prev;
	LTNode* firstNext = first->next;
	firstPrev->next = firstNext;
	firstNext->prev = firstPrev;
	free(first);
}
10.双向循环链表的插入
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* Prev = pos->prev;
	LTNode* newnode = BuyListNode(x);

	Prev->next = newnode;
	newnode->prev = Prev;
	newnode->next = pos;
	pos->prev = newnode;
}
11.双向循环链表的删除
void LTErase(LTNode* pos)
{
	assert(pos);
	LTNode* Prev = pos->prev;
	LTNode* Next = pos->next;

	Prev->next = Next;
	Next->prev = Prev;
	free(pos);
}
12.双向循环链表操作规律

很明显,双向循环链表的操作关键是记录节点。插入节点就是把前一个节点和当前节点记录下来,删除节点就是把上一个节点和下一个节点记录下来,然后free掉对应的参数。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值