(数据结构)链表

目录

1.链表的概念和结构

2.链表的分类

3.链表的实现

3.1无头+单向+非循环 链表

3.1.1 头文件部分

3.1.2接口部分(部分实现)

3.2带头+双向+循环 链表

3.2.1头文件部分

3.2.2 接口部分(部分实现)

4.链表和顺序表的区别


1.链表的概念和结构

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

结构:

2.链表的分类

实际中链表的结构是非常多样的,以下情况组合起来就有8种链表结构:

1.单向或者双向

2.不带头或者带头

3.循环或者非循环

3.链表的实现

3.1无头+单向+非循环 链表

3.1.1 头文件部分

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.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
void SListInsertAfter(SListNode* pos, SLTDateType x);
//单链表在pos位置之前插入x
void SListInsertFront(SListNode** pplist,SListNode* pos, SLTDateType x);
// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos);
// 删除pos位置
void SListErase(SListNode**pplist,SListNode* pos);
// 单链表的销毁
void SListDestroy(SListNode**pplist);

3.1.2接口部分(部分实现)

逻辑图:(无头单向非循环链表)

动态申请节点+单链表查找:

SListNode* BuySListNode(SLTDateType x)
{
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));//realloc是调整空间,malloc是申请空间
	assert(newnode);
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}
SListNode* SListFind(SListNode* plist, SLTDateType x)
{
	assert(plist);//防止链表为空,链表为空则查找的无意义
	SListNode* cur = plist;//一般不希望指向头结点的指针plist被改变,想让plist不动始终指向头结点
	while (cur)//保证不漏掉任何一个结点
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

单链表的尾插+单链表的头插:

单链表的尾插:

 先创建一个新的结点(newnode),然后在创建一个新的指针变量tail,让tail一直遍历,一直遍历到尾结点,然后令尾结点的tail->next = newnode:,然后令newnode->next = NULL;

void SListPushBack(SListNode** pplist, SLTDateType x)
{
	SListNode* newnode = BuySListNode(x);//先申请一个结点
	if (*(pplist) == NULL)//链表为空时尾插
	{
		*pplist = newnode;
	}
	else//链表不为空时
	{
		SListNode* tail = *pplist;
		while (tail->next != NULL)//令tail找到最后一个结点
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

 单链表的头插:

 先malloc一个结点(newnode),先令newnode->next = *pplist;,然后令*pplist = newnode;,

为什么传的是2级指针呢?

因为是要改变指针的内容,所以传的是指针的指针,然后对其解引用,即可访问指针的内容

void SListPushFront(SListNode** pplist, SLTDateType x)//注意:这里传的是2级指针,改变指针则传指针的地址
{
	SListNode* newnode = BuySListNode(x);
	if (*pplist == NULL)
	{
		*pplist = newnode;
	}
	else
	{
		newnode->next = *pplist;
		*pplist = newnode;
	}
}

单链表的尾删+单链表的头删:

单链表的尾删:

 拿一个指针tail指向尾结点,在拿一个指针tailPrev指向尾结点的前一个结点,然后令tailPrev->next = NULL;,假如没拿一个指针tail记录尾结点的地址,当tailPrev->next = NULL;时那就意味着尾结点的地址丢了,此时会造成内存泄漏,因此拿一个指针tail记录尾结点的地址,最后free(tail);

void SListPopBack(SListNode** pplist)
{
	assert(*pplist);//没有结点时尾删
	//只有一个结点是尾删
	if ((*pplist)->next == NULL)
	{
		free(*pplist);
		*pplist = NULL;
	}
	//不止一个结点时尾删
	SListNode* tail = *pplist;
	SListNode* tailPrev = NULL;
	while (tail->next != NULL)
	{
		tailPrev = tail;
		tail = tail->next;
	}
	tailPrev->next = NULL;
	free(tail);
}

 单链表的头删:

 

 拿一个指针(next)记录首结点的下一个结点,然后free(*pplist);,随后在把plist指向新的首结点(*pplist = next:)

void SListPopFront(SListNode** pplist)
{
	assert(*pplist);
	if ((*pplist)->next == NULL)
	{
		free(*pplist);
		*pplist = NULL;
	}
	SListNode* next = (*pplist)->next;
	free(*pplist);
	*pplist = next;
}

单链表在pos位置之后插入x+单链表在pos位置之前插入x:

单链表在pos位置之后插入x:

 首先要拿一个next指针记录pos结点之后的一个结点,然后链接pos结点和newnode结点(pos->next = newnode;),随后在链接newnode和next结点(newnode->next = next;),假如不拿一个指针next记录pos的下一个结点,那此时要注意插入的结点和前后结点的链接顺序,假如不注意链接顺序可能会丢掉pos之后结点的地址,导致pos之后的结点无法与新节点newnode链接到一起

void SListInsertAfter(SListNode* pos, SLTDateType x)
{
	assert(pos);

	SListNode* newnode = BuySListNode(x);
	SListNode* next = pos->next;
	pos->next = newnode;
	newnode->next = next;
}

 单链表在pos位置之前插入x:

拿一个指针(prev)记录pos之前的一个结点,然后在malloc一个新的结点(newnode),然后令newnode中的next存下pos的地址,在令prev(原来pos之前的一个结点)和newnode链接到一起prev->next = newnode;

void SListInsertFront(SListNode**pplist,SListNode* pos, SLTDateType x)
{
	assert(pos);
	assert(*pplist);

	if (pos == *pplist)
	{
		SListPushFront(pplist, x);
	}
	else
	{
		SListNode* prev = *pplist;
		while (prev->next != pos)//找到了pos结点之前结点的地址
		{
			prev = prev->next;
		}
		SListNode* newnode = BuySListNode(x);
		newnode->next = pos;
		prev->next = newnode;
	}
}

单链表删除pos位置之后的值+删除pos位置:

单链表删除pos位置之后的值:

void SListEraseAfter(SListNode* pos)
{
	assert(pos);

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

 删除pos位置:

 先拿一个指针(prev)记录下pos之前的一个结点,在拿一个指针(next)记录下pos之后位置的结点,然后在把prev指向的结点和next指向的结点链接到一起prev->next = next;,然后在free(pos);

void SListErase(SListNode** pplist, SListNode* pos)
{
	assert(pos);
	assert(*pplist);
	//头删
	if (pos == *pplist)
	{
		SListPopFront(pplist);
	}
	else
	{
		SListNode* prev = *pplist;
		while (prev->next != pos)//找到pos之前的一个结点
		{
			prev = prev->next;
		}
		SListNode* next = pos->next;
		free(pos);
		prev->next = next;
	}
}

单链表的销毁:

建立一个循环,在建立一个指针(prev)去指向首结点(*pplist),然后先把指向首结点的指针(*pplist)去移动指向下一个结点,此时prev相当于记录了原来的首结点,然后在去free(prev);,以此去不断循环,最后当*pplist指向空时循环结束,同时也意味着链表销毁

void SListDestroy(SListNode** pplist)
{
	assert(*pplist);//销毁链表一定是链表不为空才去销毁

	while (*pplist)
	{
		SListNode* prev = *pplist;
		*pplist = (*pplist)->next;
		free(prev);
	}
}

3.2带头+双向+循环 链表

3.2.1头文件部分

#pragma once
// 带头+双向+循环链表增删查改实现
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

typedef int LTDataType;
typedef struct ListNode
{
	LTDataType _data;
	struct ListNode* _next;// _next 这个结点的下一个结点
	struct ListNode* _prev;// _prev 这个结点的前一个结点
}ListNode;

//哨兵位初始化
ListNode* ListInit();
//创建结点的函数
ListNode* BuyListNode(LTDataType x);
// 双向链表销毁
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);
//计算有几个结点
int ListSize(ListNode* pHead);
//双向链表判空
bool ListEmpty(ListNode* pHead);

3.2.2 接口部分(部分实现)

逻辑图:(带头双向循环链表)

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

 以一个结点为参照,这个结点其中的_next存下一个结点的地址,_prev存前一个结点的地址,_data存的是这个结点中保存的数据

先创建一个结点(newnode),并且建立一个指向pos结点之前的一个指针(prev),然后令其中的_next指向pos结点,pos结点中的_prev存下newnode的地址,此时相当于把pos结点和newnode结点链接到了一起,然后令newnode中的_prev记录下prev结点地址,在把prev中的_next放入newnode的地址,此时相当于把prev结点和newnode结点链接到了一起,此时pos结点才算真正的插入了进去

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

	ListNode* newnode = BuyListNode(x);
	ListNode* prev = pos->_prev;

	newnode->_next = pos;
	pos->_prev = newnode;
	prev->_next = newnode;
	newnode->_prev = prev;
}

双向链表头删 和 尾删 和 判空:

双向链表头删:

 拿一个指针(pHeadnext)记录头结点(pHead)之后的一个结点的地址,然后在拿一个指针(pHeadnextnext)去记录pHeadnext下一个结点的地址,首先把pHead中的_next存下pHeadnextnext保存的地址,然后在把pHeadnextnext中的_prev存下pHead中保存的地址,然后在free(pHeadnext);,假如不用一个指针(pHeadnext)记录pHead的下一个结点的地址,当pHead结点与pHeadnextnext结点链接到一起时相当于把pHeadnext保存的那个结点给丢失了,此时会造成内存泄漏

void ListPopFront(ListNode* pHead)
{
	assert(pHead);
	assert(!ListEmpty(pHead));

	ListNode* pHeadNext = pHead->_next;
	ListNode* pHeadNextNext = pHeadNext->_next;

	pHead->_next = pHeadNextNext;
	pHeadNextNext->_prev = pHead;
	free(pHeadNext);
}

 尾删:

void ListPopBack(ListNode* pHead)
{
	
	assert(pHead);
	assert(!ListEmpty(pHead));

	ListNode* tail = pHead->_prev;
	ListNode* tailPrev = tail->_prev;

	tailPrev->_next = pHead;
	pHead->_prev = tailPrev;

	free(tail);
}

 判空:

bool ListEmpty(ListNode* pHead)
{
	assert(pHead);

	return pHead->_next == NULL;
}

双向链表头插 和  双向链表尾插:

双向链表头插:

void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead);

	ListNode* newnode = BuyListNode(x);
	ListNode* next = pHead->_next;

	pHead->_next = newnode;
	newnode->_prev = pHead;
	newnode->_next = next;
	next->_prev = newnode;
}

 双向链表尾插:

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;
}

创建结点 和 初始化哨兵位结点:

ListNode* BuyListNode(LTDataType x)
{
	ListNode* node = (ListNode*)malloc(sizeof(ListNode));
	if (node == NULL)
	{
		printf("malloc fail");
		exit(-1);
	}
	node->_data = x;
	node->_next = NULL;
	node->_prev = NULL;
	return node;
}
ListNode* ListInit()
{
	ListNode* pHead = BuyListNode(-1);
	pHead->_next = pHead;
	pHead->_prev = pHead;

	return pHead;
}

4.链表和顺序表的区别

不同点顺序表链表
存储空间上物理上一定连续逻辑上连续,但物理上不一定连续
随机访问支持 -> O(1)不支持 -> O(N)
任意位置插入或者删除元素可能需要搬移元素,效率低O(N)只需修改指针指向
插入动态顺序表,空间不够时需要扩容没有容量的概念
应用场景元素高效率存储+频繁访问任意位置插入和删除频繁
缓存利用率

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值