数据结构之链表

本文介绍了数据结构中的链表概念,特别是C语言中单链表和带头双向循环链表的实现。文章详细阐述了结构体成员、节点申请、插入、删除、查找和销毁等操作,并提供了源码实现。单链表适用于邻接表等数据结构,而双向循环链表由于其快速的尾插和定位特性,常用于STL中的list。
摘要由CSDN通过智能技术生成

🎉welcome🎉
✒️博主介绍:博主大一智能制造在读,热爱C/C++,会不定期更新系统、语法、算法、硬件的相关博客,浅浅期待下一次更新吧!
✈️算法专栏:算法与数据结构
😘博客制作不易,👍点赞+⭐收藏+➕关注

前言

在上一篇中,我们了解到了数据结构中的线性表中的顺序表,但是顺序表是存在很多缺点的,比如顺序表的增容,会出现更换空间,然后导致效率降低,同时,因为我们为了减少开辟空间的次数,每次扩容都是两倍,导致了,如果只是插入很少的几个数,但是扩容了一个很大的空间,造成了不必要的浪费,那这些有没有什么方法去解决呢?那便有了这篇文章要介绍的数据结构——链表。

链表

概念

链表,故名思意,和链子一样,这也是它在我们要画一个链表表示的形式,如同一个链子一样:

在这里插入图片描述
在这里插入图片描述

上面这两个图是这篇博客要介绍的两种最常用的链表——无头单链表和双向循环链表,既然这两种是常用的,那相比较就会有不常用的,不常用的有六种,也就是链表总共有八种结构,这八种结构都是由三种结构组合而成的:

  1. 单项或者双向
    在这里插入图片描述
  1. 带头或者不带头

在这里插入图片描述

  1. 循环或者非循环

在这里插入图片描述

八种是有上面三种组合起来的,本文将讲解C语言实现单链表和带头双向循环链表。

单链表

单链表为第一种中的单向链表,单链表本身缺陷很多,通常不是单独出现使用,而是为了后期的一些数据结构,如邻接表就是一堆单链表组成的,所以对于单链表的增删查同样也是很重要的,只有掌握单链表的增删查改才可以更好掌握后期的使用单链表的数据结构。

单链表的结构体成员

对于链表而言,因为在物理空间上不是连续的,每个节点都存在与地址空间的任意位置,则每个成员除了要保存本身的数据,还需要知道下一个节点的信息,如同侦察游戏一样,在上一条线索中会有东西指向下一条线索,那节点本身是存在与地址空间当中,则本身的地址就是一个编号,那上一个节点只需要一个指针指向下一个节点的地址空间即可,则结构体成员需要两个,一个是存储数据的data,一个就是指向下一个节点的指针next。

typedef int SLTDateType;

typedef struct SListNode
{
	SLTDateType data;
	struct SListNode* next;
}SListNode;

单链表的节点申请

链表的地址空间是不连续,则对于链表而言,每次的插入都是需要去申请空间创建新的节点, 那节点的申请就会变的频繁,这时就可以单独将节点申请封装成函数,提高代码复用性。

SListNode* BuySListNode(SLTDateType x)
{
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

单链表的插入

pos位置插入

单链表的插入对比顺序表是很简单,只需要知道位置,申请一个新的节点,改变两个位置的指针即可。

void SListInsertAfter(SListNode* pos, SLTDateType x)
{
	assert(pos);
	SListNode* newnode = BuySListNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}
头插和尾插

头插尾插和pos位置插入并无差别,只是pos位置变成了特定的位置,但是尾插是需要去找尾的,只有找到尾部还能进行插入。

// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x)
{
	assert(pplist);
	SListNode* newnode = BuySListNode(x);
	newnode->next = *pplist;
	*pplist = newnode;
}
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x)
{
	assert(pplist);
	SListNode* newnode = BuySListNode(x);
	if (*pplist== NULL)
	{
		*pplist = newnode;
	}
	//找尾
	else
	{
		SListNode* tail = *pplist;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

对于这三种插入而言,头插和pos位置插入是很快的,但是尾插的效率相比较前两种而言,是很慢的,因为单链表的遍历对比顺序表而言是很慢的。

单链表的删除

pos位置删除

对于删除而言,只需要将指向它的指针指向它指向的节点即可,即它的上一个节点指向它的下一个节点,然后将它的空间释放后即可。

void SListEraseAfter(SListNode* pos)
{
	assert(pos);
	assert(pos->next);
	SListNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}
头删和尾删

头删尾删和插入时一样的,尾删同样是需要去找尾的。

// 单链表头删
void SListPopFront(SListNode** pplist)
{
	assert(pplist);
	assert(*pplist);

	SListNode* f = *pplist;
	(*pplist)->next = f->next;
	free(f);
	f = NULL;
}
// 单链表的尾删
void SListPopBack(SListNode** pplist)
{
	assert(pplist);
	assert(*pplist);
	if ((*pplist)->next == NULL)
	{
		free(*pplist);
		*pplist = NULL;
	}
	else
	{
		SListNode* tail = *pplist;
		while (tail->next->next != NULL)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
		
	}
}

单链表的查找

单链表的查找本质和顺序表的查找并无区别,只是遍历时需要用指针去遍历,每次将指针更新成下一个节点的指针即可。

SListNode* SListFind(SListNode* plist, SLTDateType x)
{
	SListNode* cur = plist;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur=cur->next;
	}
	return NULL;
}

单链表的销毁

销毁其实就是在遍历的基础上,每次将都将节点释放掉即可。

void SListDestroy(SListNode* plist)
{
	assert(plist);
	SListNode* p, *q;
	p = plist;
	while (p)
	{
		q = p->next;
		free(p);
		p = q;
	}
	plist = NULL;
}

带头双向循环链表

双向循环链表相比较单链表而言强大了许多,在stl中,list的底层就是双向循环链表,它可以很快的去尾插,任意位置的插入也可以很快的定位到。

带头双向循环链表的结构体成员

双向循环链表对比单链表多了一个指针,一个是指向下一个节点位置,一个指向前一个节点位置。

typedef int LTDataType;
typedef struct ListNode
{
	LTDataType _data;
	struct ListNode* _next;
	struct ListNode* _prev;
}ListNode;

带头双向循环链表的创建节点

与单链表无差别。

带头双向循环链表的插入

pos位置的插入

双向循环链表相比较单链表而言结构体成员多了个指向前一个成员的指针,而插入的时候因为双向的特性,则不仅仅需要让插入节点前一个节点指向插入节点,还需要插入节点的下一个节点的指向后指针的指针指向插入节点,同时,对于改变指针指向的顺序是需要思考的,当前将前一个的指针改变指向插入节点时,就找不下下一个节点,但是先将下一个节点指针改变就找不到前一个节点,这里可以先将要插入的节点指针先指向这两个节点,在去改变这两个节点的指针。

void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	
	ListNode* newn = newln(x);

	newn->_next = pos;
	newn->_prev = pos->_prev;
	pos->_prev->_next = newn;
	pos->_prev = newn;
}
头插和尾插

头插和尾插可以直接使用pos位置的插入,复用代码。只需要穿入指向头和尾的指针即可。

//尾插
void ListPushBack(ListNode* pHead, LTDataType x)
{
	assert(pHead);

	ListInsert(pHead, x);
}
//头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead);

	ListInsert(pHead->_next, x);
}

带头双向循环链表的删除

pos位置的删除

带头循环双向链表的删除也是改变指针,使其脱离链表,然后释放该节点。

void ListErase(ListNode* pos)
{
	assert(pos);

	pos->_prev->_next = pos->_next;
	pos->_next->_prev = pos->_prev;

	free(pos);
	pos = nullptr;
}

头删和尾删

与插入一样,采用复用代码的形式。

//尾删
void ListPopBack(ListNode* pHead)
{
	assert(pHead);

	ListErase(pHead->_prev);
}
//头删
void ListPopFront(ListNode* pHead)
{
	assert(pHead);

	ListErase(pHead->_next);
}

查找和销毁

与单链表相同。

单链表源码

.h

#pragma once

#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int SLTDateType;

typedef struct SListNode
{
	SLTDateType data;
	struct SListNode* next;
}SListNode;

// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x);
// 单链表打印
void SListPrint(SListNode* plist);
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x);
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x);
// 单链表的尾删
void SListPopBack(SListNode** pplist);
// 单链表头删
void SListPopFront(SListNode** pplist);
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x);
// 单链表在pos位置之后插入x
// 分析思考为什么不在pos位置之前插入?
void SListInsertAfter(SListNode* pos, SLTDateType x);
// 单链表删除pos位置之后的值
// 分析思考为什么不删除pos位置?
void SListEraseAfter(SListNode* pos);
// 单链表的销毁
void SListDestroy(SListNode* plist);

.c

#include"slist.h"

// 单链表打印
void SListPrint(SListNode* plist)
{
	SListNode* cur = plist;
	while (cur)
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}
//申请一个新节点
SListNode* BuySListNode(SLTDateType x)
{
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x)
{
	assert(pplist);
	SListNode* newnode = BuySListNode(x);
	if (*pplist== NULL)
	{
		*pplist = newnode;
	}
	//找尾
	else
	{
		SListNode* tail = *pplist;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x)
{
	assert(pplist);
	SListNode* newnode = BuySListNode(x);
	newnode->next = *pplist;
	*pplist = newnode;
}
// 单链表的尾删
void SListPopBack(SListNode** pplist)
{
	assert(pplist);
	assert(*pplist);
	if ((*pplist)->next == NULL)
	{
		free(*pplist);
		*pplist = NULL;
	}
	else
	{
		SListNode* tail = *pplist;
		while (tail->next->next != NULL)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
		
	}
}
// 单链表头删
void SListPopFront(SListNode** pplist)
{
	assert(pplist);
	assert(*pplist);

	SListNode* f = *pplist;
	(*pplist)->next = f->next;
	free(f);
	f = NULL;
}
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x)
{
	SListNode* cur = plist;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur=cur->next;
	}
	return NULL;
}
//pos后面插入
void SListInsertAfter(SListNode* pos, SLTDateType x)
{
	assert(pos);
	SListNode* newnode = BuySListNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}
//pos后面删除
void SListEraseAfter(SListNode* pos)
{
	assert(pos);
	assert(pos->next);
	SListNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}
// 单链表的销毁
void SListDestroy(SListNode* plist)
{
	assert(plist);
	SListNode* p, *q;
	p = plist;
	while (p)
	{
		q = p->next;
		free(p);
		p = q;
	}
	plist = NULL;
}

带头双向循环链表

.h

#pragma once

#include<cassert>
#include<cstdlib>
#include<cstdio>

// 带头+双向+循环链表增删查改实现
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType _data;
	struct ListNode* _next;
	struct ListNode* _prev;
}ListNode;

// 双向链表销毁
void ListDestory(ListNode* pHead);
// 双向链表打印
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);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);

.c

#include"dlist.h"

//销毁链表
void ListDestory(ListNode* pHead)
{
	assert(pHead);

	ListNode* cur = pHead->_next;

	while (cur)
	{
		ListNode* n = cur->_next;
		free(cur);
		cur = n;
	}
	free(pHead);
}
void ListPrint(ListNode* pHead)
{
	assert(pHead);
	
	printf("<-head->");

	ListNode* cur = pHead->_next;

	while (cur)
	{
		printf("%d->", cur->_data);
		cur = cur->_next;
	}
	
	printf("\n");
}
//创建新节点
ListNode* newln(LTDataType x)
{
	ListNode* nhead = new ListNode;
	if (!nhead)
	{
		perror("new nhead");
	}
	
	nhead->_data = x;
	return nhead;
}
//插入
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	
	ListNode* newn = newln(x);

	newn->_next = pos;
	newn->_prev = pos->_prev;
	pos->_prev->_next = newn;
	pos->_prev = newn;
}
//尾插
void ListPushBack(ListNode* pHead, LTDataType x)
{
	assert(pHead);

	ListInsert(pHead, x);
}
//头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead);

	ListInsert(pHead->_next, x);
}
//删除
void ListErase(ListNode* pos)
{
	assert(pos);

	pos->_prev->_next = pos->_next;
	pos->_next->_prev = pos->_prev;

	free(pos);
	pos = nullptr;
}
//尾删
void ListPopBack(ListNode* pHead)
{
	assert(pHead);

	ListErase(pHead->_prev);
}
//头删
void ListPopFront(ListNode* pHead)
{
	assert(pHead);

	ListErase(pHead->_next);
}
//查找
ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	assert(pHead);

	ListNode* cur = pHead;
	while (cur)
	{
		if (cur->_data == x)
		{
			return cur;
		}
		cur = cur->_next;
	}
	return nullptr;
}

🚀专栏:算法与数据结构
🙉都看到这里了,留下你们的👍点赞+⭐收藏+📋评论吧🙉

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

封心锁爱的前夫哥

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

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

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

打赏作者

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

抵扣说明:

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

余额充值