三.数据结构之链表

本文详细介绍了链表的概念,包括无头单向非循环链表和带头双向循环链表的实现,涵盖了节点定义、创建、插入、删除、查找等操作,并提供了相应的C语言源码。文章强调了链表在内存管理和操作效率上的优势,以及在不同场景下的适用性。
摘要由CSDN通过智能技术生成

前言:

书接上文,顺序表的问题及思考

  1. 中间或头部的插入删除,时间复杂度为O(N)
  2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
  3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。

为了更好的解决上述问题,本文我们来学习链表:

一.链表的概念及结构

1.何为链表

链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的

如图为单链表的两种结构:
①物理结构
在这里插入图片描述
②逻辑结构
在这里插入图片描述
注意:

  1. 链表的链式结构在逻辑上是连续的,但在物理上不一定连续。
  2. 现实中的节点一般都是从堆上申请出来的。
  3. 从堆上申请的节点,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续

2.链表常见的几种形式

实际中链表的结构非常多样,各种情况组合起来就有8种链表结构:
①单向或者双向
在这里插入图片描述
②带头或者不带头
在这里插入图片描述
③循环或者非循环
在这里插入图片描述
虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:

  1. 无头单向非循环链表
    结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
    在这里插入图片描述

  2. 带头双向循环链表
    结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
    在这里插入图片描述

二.无头单向非循环链表的实现

函数接口:

typedef int SLTDataType;

//定义节点
typedef struct SListNode
{
	SLTDataType data;   //存放数据
 	struct SListNade* next;  //指向下一个节点
}SLTnode;

//申请节点
SLTnode* BuySLTNode(SLTDataType x);
//打印
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);
//在pos之前插入
void SLTInsert(SLTnode** pphead, SLTnode* pos, SLTDataType x);
//删除pos之前的节点
voide SLTErase(SLTnode** pphead, SLTnode* pos);
//在pos之后插入
void SLTInsertAftter(SLTnode* pos, SLTDataType x);
//删除pos之后的节点
void SLTEraseAfter(SLTnode* pos);

1.定义节点

//节点
typedef struct SListNode
{
	SLTDataType data;   //存放数据
 	struct SListNade* next;  //指向下一个节点
}SLTnode;
  1. 链式结构由一个个的节点链接而成,节点中存放当前的数据data和指向下一个节点的指针。
  2. 链表正是有节点串联而成,所以只需记住排在最前面的头节点的位置,就能访问链表中的任意一个节点

注:这里的头节点指的是链表中第一个节点,它本身也会存储数据,而后面讲的带头双向循环链表中的头节点仅是链表起始的标志,并不会存储有效数据

2.创建一个新节点

后面我们要在单链表中进行插入和删除,此时的插入删除是针对节点进行的,这个结点是包括SLTDateType数据以及SLTDateType*的指针,因此,为了方便和减少代码的重复度,我们另写一个函数用来专门创建新结点。

SLTnode* BuySLTNode(SLTDataType x)
{
	SLTnode* newnode = (SLTnode*)malloc(sizeof(SLTnode));
	if (newnode == NULL)
	{
		perror("malloc");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

3.单链表打印

void SLTPrint(SLTnode* phead)
{
	SLTnode* cur = phead;  //cur指向phead存放的地址
	while (cur != NULL)    //不为空
	{
		printf("%d->", cur->data);  //输出当前节点存的值
		cur=cur->next;    //该节点指向下一个节点
	}
	printf("NULL\n");
}

cur=cur->next; 这句话的含义是:
在这里插入图片描述

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. 从代码中可以看出,我们使用了二级指针,这是因为我们要在原链表上做出修改,所以要传原链表头节点的地址,也就是一级指针的地址,要用二级指针来接收
  2. 然后是找尾过程:
    在这里插入图片描述

5.单链表尾删

//尾删
void SLTPopBack(SLTnode** pphead)
{
	assert(pphead);
	//1.检查是否为空
	assert(*pphead);
	//2.只有一个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTnode* tail = *pphead;
		while (tail->next->next != NULL)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}

注意:

  1. 链表为空时,没东西可删,要特判一下
  2. 链表只有一个节点时,删完就空了,也特别处理一下
  3. tail->next->next != NULL这句循环条件的含义是:
    在这里插入图片描述

6.单链表头插

//头插
void SLTPushFront(SLTnode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTnode* newnode = BuySLTNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

如下图所示:
在这里插入图片描述

7.单链表头删

//头删
void SLTPopFront(SLTnode** pphead)
{
	assert(pphead);
	//1.检查是否为空
	assert(*pphead);
	SLTnode* first = *pphead;
	*pphead = first->next;
	free(first);
	first = NULL;
}

先检查是否为空,如果不为空,则:
在这里插入图片描述
直接覆盖掉第一个节点以达到头删的效果。

8.单链表查找

//查找
SLTnode* SLTFind(SLTnode* phead, SLTDataType)
{
	SLTnode* cur = phead;
	while (cur)
	{
		if (cur->data == x)  //找到了
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL; //没找到
}

9.在pos之前插入

//在pos之前插入
void SLTInsert(SLTnode** pphead, SLTnode* pos, SLTDataType x)
{
	assert(pos);  //pos这个节点存在
	assert(pphead);  //正确传值,不能为空
	if (pos == *pphead)  //pos在第一个节点处
	{
		SLTPushFront(pphead, x); //复用头插函数
	}
	else
	{
		//找到pos的前一个位置
		SLTnode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		SLTnode* newnode = BuySLTNode(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}

在这里插入图片描述

10.在pos之前删除

//删除pos之前的节点
voide SLTErase(SLTnode** pphead, SLTnode* pos)
{
	assert(pphead);
	assert(pos);
	if (*pphead == pos)
	{
		SLTPopFront(pphead);
	}
	else
	{
		//找到pos的前一个位置
		SLTnode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;  //覆盖掉pos
		free(pos);
	}
}

11.在pos之后插入

//在pos之后插入
void SLTInsertAftter(SLTnode* pos, SLTDataType x)
{
	assert(pos);
	SLTnode* newnode = BuySLTNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

在这里插入图片描述

12.在pos之后删除

//删除pos之后的节点
void SLTEraseAfter(SLTnode* pos)
{
	assert(pos);
	assert(pos->next);  //保证pos之后有节点
	SLTnode* del = pos->next;
	pos->next = del->next;  //覆盖以删除
	free(del);
	del = NULL;
}

13.assert( )断言的使用

在上面代码中我们会发现,为什么有的代码需要断言指针,有的就不需要,有的要断言一级指针,有的要断言二级指针,也有的什么都不用断言,这是为什么呢?

  1. 首先,assert函数在调试的时候非常好用,一旦()内为假,会立刻报错,而且会帮我们提示报错在哪个文件和在哪行代码。
  2. 一级指针也就是phead,当链表为空的时候,phead就是为NULL,此时需要根据函数要实现的实际功能来确定是否断言。比如:打印函数SLTPrint(),当传来的指针为空时,说明链表中没有元素,但是没有元素也是可以打印的,如果断言就会报错。
  3. 二级指针永远指向phead,phead的地址是永远存在的,那么pphead就一定不可能为空,所以有**pphead的地方就需要断言pphead,这样可以及时发现在传参时传成空指针的错误。

三.带头双向循环链表的实现

1.函数接口:

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

//创建节点
listnode* Buylistnode(LTDataType x);
//双链表初始化
listnode* init();
//双向链表的打印
void listprint(listnode* phead);
//双向链表的尾插
void listpushback(listnode* phead, LTDataType x);
//双向链表的尾删
void listpopback(listnode* phead);
//双向链表的头插
void listpushfront(listnode* phead, LTDataType x);
//双向链表的头删
void listpopfront(listnode* phead);
//双向链表查找
listnode* listfind(listnode* phead, LTDataType x);
//销毁双向链表
void listdestroy(ListNode* phead);
//双向链表pos位置前面插入
void listinsert(listnode* pos, LTDataType x);
//双向链表删除pos处的节点
void listera

故名思意,带头双向循环链表新增加了哨兵位的头节点,并且链表首尾是循环链接的,严格来说并没有实际的头,所以我们将哨兵位的头节点当作头,方便函数接口的实现。

2.节点结构的定义

既然是双向循环链表,那就得要求每个结点既保存下一个结点的地址,又要保存上一个结点的地址。结构中,prev指向前一个结点,next指向后一个结点。

typedef int LTDataType;
 
typedef struct ListNode
{
	LTDataType data;
	struct ListNode* prev;//指向前一个结点
	struct ListNode* next;//指向后一个结点
}LTNode;

四.源码

1.单链表

(1)SList.h

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

typedef int SLTDataType;

//定义节点
typedef struct SListNode
{
	SLTDataType data;   //存放数据
 	struct SListNade* next;  //指向下一个节点
}SLTnode;

//申请节点
SLTnode* BuySLTNode(SLTDataType x);
//打印
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);
//在pos之前插入
void SLTInsert(SLTnode** pphead, SLTnode* pos, SLTDataType x);
//删除pos之前的节点
voide SLTErase(SLTnode** pphead, SLTnode* pos);
//在pos之后插入
void SLTInsertAftter(SLTnode* pos, SLTDataType x);
//删除pos之后的节点
void SLTEraseAfter(SLTnode* pos);

(2)SList.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"

//申请一个新节点
SLTnode* BuySLTNode(SLTDataType x)
{
	SLTnode* newnode = (SLTnode*)malloc(sizeof(SLTnode));
	if (newnode == NULL)
	{
		perror("malloc");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}
//打印
void SLTPrint(SLTnode* phead)
{
	SLTnode* cur = phead;  //临时指针
	while (cur != NULL)   //不为空
	{
		printf("%d->", cur->data);  //输出当前节点存的值
		cur=cur->next;    //该节点指向下一个节点
	}
	printf("NULL\n");
}
//尾插
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;
	}
}
//尾删
void SLTPopBack(SLTnode** pphead)
{
	assert(pphead);
	//1.检查是否为空
	assert(*pphead);
	//2.只有一个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTnode* tail = *pphead;
		while (tail->next->next != NULL)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}
//头插
void SLTPushFront(SLTnode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTnode* newnode = BuySLTNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}
//头删
void SLTPopFront(SLTnode** pphead)
{
	assert(pphead);
	//1.检查是否为空
	assert(*pphead);
	SLTnode* first = *pphead;
	*pphead = first->next;
	free(first);
	first = NULL;
}
//查找
SLTnode* SLTFind(SLTnode* phead, SLTDataType)
{
	SLTnode* cur = phead;
	while (cur)
	{
		if (cur->data == x)  //找到了
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL; //没找到
}
//在pos之前插入
void SLTInsert(SLTnode** pphead, SLTnode* pos, SLTDataType x)
{
	assert(pos);  //pos这个节点存在
	assert(pphead);  //正确传值,不能为空
	if (pos == *pphead)  //pos在第一个节点处
	{
		SLTPushFront(pphead, x); //复用头插函数
	}
	else
	{
		//找到pos的前一个位置
		SLTnode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		SLTnode* newnode = BuySLTNode(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}
//删除pos之前的节点
voide SLTErase(SLTnode** pphead, SLTnode* pos)
{
	assert(pphead);
	assert(pos);
	if (*pphead == pos)
	{
		SLTPopFront(pphead);
	}
	else
	{
		//找到pos的前一个位置
		SLTnode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;  //覆盖掉pos
		free(pos);
	}
}
//在pos之后插入
void SLTInsertAftter(SLTnode* pos, SLTDataType x)
{
	assert(pos);
	SLTnode* newnode = BuySLTNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}
//删除pos之后的节点
void SLTEraseAfter(SLTnode* pos)
{
	assert(pos);
	assert(pos->next);  //保证pos之后有节点
	SLTnode* del = pos->next;
	pos->next = del->next;  //覆盖以删除
	free(del);
	del = NULL;
}

2.双链表

(1)list.h

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

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

//创建节点
listnode* Buylistnode(LTDataType x);
//双链表初始化
listnode* init();
//双向链表的打印
void listprint(listnode* phead);
//双向链表的尾插
void listpushback(listnode* phead, LTDataType x);
//双向链表的尾删
void listpopback(listnode* phead);
//双向链表的头插
void listpushfront(listnode* phead, LTDataType x);
//双向链表的头删
void listpopfront(listnode* phead);
//双向链表查找
listnode* listfind(listnode* phead, LTDataType x);
//销毁双向链表
void listdestroy(ListNode* phead);
//双向链表pos位置前面插入
void listinsert(listnode* pos, LTDataType x);
//双向链表删除pos处的节点
void listera

(2)list.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "list.h"

//创建节点
listnode* Buylistnode(LTDataType x)
{
	listnode* node = (listnode*)malloc(sizeof(listnode));

	if ((node == NULL))
	{
		perror("malloc");
		return NULL;
	}
	node->next = NULL;
	node->prev = NULL;
	node->data = x;
	return node;
}

//双链表初始化
listnode* init()
{
	listnode* phead = Buylistnode(-1);
	//自己指向自己
	phead->next = phead;
	phead->prev = phead;
	return phead;
}
//双向链表的打印
void listprint(listnode* phead)
{
	//phead不能为空,至少有哨兵卫的头节点
	assert(phead);
	printf("<=head=>");
	//从哨兵卫头节点下一个节点开始打印
	listnode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d<=>",cur->next);
		cur = cur->next;
	}
	printf("\n");
}
//双向链表的尾插
void listpushback(listnode* phead, LTDataType x)
{
	assert(phead);
	头节点指向的上一个节点就是尾节点
	//listnode* newnode = Buylistnode(x);
	//listnode* tail = phead->prev;

	//tail->next = newnode;
	//newnode->prev = tail;

	//newnode->next = phead;
	//phead->prev = newnode;
	listinsert(phead, x);
}
//判断是否只有一个头节点
bool ltempty(listnode* phead)
{
	assert(phead);
	return phead->next == phead;
}
//双向链表的尾删
void listpopback(listnode* phead)
{
	/*assert(phead);
	assert(!ltempty(phead));

	listnode* tail = phead->prev;
	listnode* tailprev = tail->prev;

	tailprev->next = phead;
	phead->prev = tailprev;

	free(tail);
	tail = NULL;*/
	listerase(phead->prev);
}
//双向链表的头插
void listpushfront(listnode* phead, LTDataType x)
{
	assert(phead);

	/*listnode* newnode = Buylistnode(x);
	listnode* first = phead->next;

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

	newnode->next = first;
	first->prev = newnode;*/
	listinsert(phead->next, x);
}
//双向链表的头删
void listpopfront(listnode* phead)
{
	/*assert(phead);
	assert(!ltempty(phead));

	listnode* first = phead->next;
	listnode* firstnext = first->next;

	firstnext->prev = phead;
	phead->next = firstnext;

	free(first);
	first = NULL;*/
	listerase(phead->next);
}
//双向链表查找
listnode* listfind(listnode* phead, LTDataType x)
{
	assert(phead);
	listnode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

//双向链表pos位置前面插入
void listinsert(listnode* pos, LTDataType x)
{
	assert(pos);
	listnode* prev = pos->prev;
	listnode* newnode = Buylistnode(x);

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

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

//双向链表删除pos处的节点
void listerase(listnode* pos)
{
	assert(pos);
	listnode* prev = pos->prev;
	listnode* next = pos->next;

	prev->next = next;
	next->prev = prev;

	free(pos);
	pos = NULL;
}
//销毁双向链表
void listdestroy(ListNode* phead)
{
	assert(phead);
	ListNode* cur = phead->next;
	while (cur!=phead)
	{
		listnode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
	phead = NULL;
}

本篇到此结束,码文不易,还请多多支持哦!!

  • 22
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 31
    评论
评论 31
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

殿下p

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值