数据结构------------线性表之链表(详细讲解)

系列文章目录

第一章:数据结构-----顺序表

第二章:数据结构-----二叉树

前言

  上期我们讲到了顺序表,那么顺序表有没有什么劣势呢?有小伙伴可能会说,顺序表的查找比较不方便,的确,不过线性表的查找都不是他们的长处,我们对于查找会在后期使用更高效的数据结构来实现,例如:平衡搜索树,哈希表来实现,这里我们不去谈线性表的搜索,链表的头插和中间插入是O(N)的时间复杂度,需要将后面所有元素都后移一遍,空间连续存储,而链表的空间不连续,插入则更为方便,下面我们将以写接口的方式和小伙伴们讨论链表的优缺点......

一、链表是什么

            链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。

下面我们用几种更容易理解的图片解释链表的物理结构和逻辑结构

tips:链表可分为8种

这里给出分法,自由组合即可

1.带头(哨兵位)链表/不带头链表

    注:头节点不存放任何数据,有些人可能说可以存放数据的数量阿,那么可以思考一个问题,如果我的链表存放数据是double,阁下又将如何应对。如果存放数据超过int的最大存储个数,阁下又当如何应对。

2.双向链表/单向链表

3.循环链表/非循环链表 

本文将实现 单向不带头链表 和 双向循环带头链表(最常用)

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

2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都
是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带
来很多优势,实现反而简单了,后面我们代码实现了就知道了。(所以我们一定不要被复杂的数据结构吓到了,学就完了)

二、链表的接口代码实现

一.单向不带头链表

1.头文件的引入

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

2. 链表元素的创建

typedef int SLTDataType;
struct SListNode
{
	int data;
	struct  SListNode* next;//指向下一个节点
};

3.开辟空间

由于头插尾插等操作多次使用,我们决定封装成一个函数来达到复用的效果。

SLTNode* BuySListNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}
typedef struct SListNode SLTNode;

4.各种接口的声明及实现 

void SListPushBack(SLTNode** pphead, SLTDataType x);//尾插
void SListPushFront(SLTNode** pphead, SLTDataType x);//头插
void SListPrint(SLTNode* phead);
void SListPopFront(SLTNode** pphead);//头删
void SListPopBack(SLTNode** pphead);//尾删
SLTNode* SListFind(SLTNode* phead, SLTDataType x);
void SListInsert(SLTNode** pphead,SLTNode* pos,SLTDataType x);//在pos的前面插入
void SListErase(SLTNode** pphead,SLTNode* pos);//删除pos位置的值
//在i前面插入x
// void SListInsert(SLTNode** head ,int i,SLTDataType x);
//删除i 位置的值
//  void SListErase(SLTNode** head ,int i);
//上述i表示下标
//
void SListPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while(cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}
SLTNode* BuySListNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}
void SListPushBack(SLTNode** pphead, SLTDataType x)//也可以用引用来解决,*&,引用的意思是别名
{
	SLTNode* newnode = BuySListNode(x);
	
	if (*pphead == NULL)
	{
		*pphead = newnode;//形参是实参的临时拷贝,形参的改变不会影响实参
	}
	else
	{
		//找尾节点的指针
		SLTNode* tail = *pphead;
		while (tail->next != NULL)//phead是空指针,给tail也是空指针,再访问就会崩溃
		{
			tail = tail->next;
		}
		//尾节点链接新节点
		tail->next = newnode;
	}
}
//不会改变链表的头指针,传一级指针
//会改变链表的头指针,传二级指针
void SListPushFront(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuySListNode(x);
	if (*pphead == NULL)//考虑头插如果只有一个节点
	{
		*pphead = newnode;
	}
	else 
	{
		newnode->next = *pphead;
		*pphead = newnode;
	}
}
void SListPopFront(SLTNode** pphead)//头删
{
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	//如果直接一free直接找不到这个链表了
	*pphead = next;
}
void SListPopBack(SLTNode** pphead)
{
	//空
	//一个节点
	//一般情况
	if (*pphead == NULL)
	{
		return;
	}
	else if ((*pphead)->next==NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		//找尾节点前一个的指针,再定义一个prev
		SLTNode* prev = NULL;
		SLTNode* tail = *pphead;
		while (tail->next != NULL)//phead是空指针,给tail也是空指针,再访问就会崩溃
		{
			prev = tail;//把自己的位置给到prev,自己在往下走
			tail = tail->next;
		}
		free(tail);
		prev->next = NULL;
	}
}
//寻找pos
SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* cur = phead;
	//while (cur != NULL)
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}

		cur = cur->next;
	}

	return NULL;
}
//在pos的前面插入
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	if (pos == *pphead)
	{
		SListPushFront(pphead, x);
	}
	SLTNode* newnode = BuySListNode(x);
	SLTNode* prev = pphead;
	
	while(prev->next!=pos)
	{
		prev = prev->next;
	}
	prev->next = newnode;
	newnode->next = pos;

}
void SListErase(SLTNode** pphead,SLTNode* pos)
{
	if (pos == *pphead)
	{
		SListPopFront(pphead);
	}
	SLTNode* prev = pphead;

	while (prev->next != pos)
	{
		prev = prev->next;
	}
	prev->next = pos->next;
	free(pos);
}
//删除pos位置的值

这里注意一点,形参只是实参的临时拷贝,我们有两种选择方式,第一种,让函数的返回值为结构体指针,不然如果是无返回值的函数则无法真正的改变实参本身,所以我们传指针的地址来修改实参。 

聪明的小伙伴可能又发现了,我们erase和insert就可以实现头插尾插的全部内容,如果要快速实现完整的链表,可千万不要一个一个实现奥,我们可以利用函数的复用实现更精简的代码效果。

一.双向带头循环链表 

这里直接给出完整的代码

List.h

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

typedef int LTDataType;
typedef struct ListNode
{
	struct ListNode* next;
	struct ListNode* prev;//前驱指针
	int data;
}ListNode;


ListNode* ListInit();
void ListDestory(ListNode* phead);
void ListPrint(ListNode* phead);
void ListPushBack(ListNode* phead,LTDataType x);
void ListPushFront(ListNode* phead, LTDataType x);
void ListPopBack(ListNode* phead);
void ListPopFront(ListNode* phead);
ListNode* ListFind(ListNode* phead, LTDataType x);
void ListInsert(ListNode* pos, LTDataType x);
void ListErase(ListNode* pos, LTDataType x);
bool ListEmpty(ListNode* phead);
int ListSize(ListNode* phead);

List.c

void ListPrint(ListNode* phead)
{
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}
ListNode* BuyListNode(LTDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}
ListNode* ListInit()//这个时候仍然存在形参改变不了实参的问题,所以我们使用返回结构体指针来解决这个问题
{
	ListNode* phead = BuyListNode(0);
	phead->next = phead;
	phead->prev = phead;
	return phead;
}
void ListDestory(ListNode* phead)
{
	assert(phead);
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		ListNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
	phead = NULL;
}
void ListPushBack(ListNode* phead,LTDataType x)
{
	assert(phead);
	ListNode* tail=phead->prev;
	ListNode* newnode = BuyListNode(x);
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
}

void ListPushFront(ListNode* phead, LTDataType x)
{
	assert(phead);
	ListNode* first = phead->next;
	ListNode* newnode = BuyListNode(x);
	//接下来就是三个节点的链接
	phead->next = newnode;
	newnode->next = first;
	newnode->prev = phead;
	first->prev = newnode;


}
void ListPopFront(ListNode* phead)
{
	assert(phead);
	assert(phead->next != phead);//防止只剩一个哨兵节点也删掉
	ListNode* first = phead->next;
	ListNode* second = first->next;
	phead->next = second;
	second->prev = phead;
	free(first);
	first = NULL;


}
void ListPopBack(ListNode* phead)
{
	assert(phead);
	assert(phead->next != phead);//防止只剩一个哨兵节点也删掉
	ListNode* tail = phead->prev;
	ListNode* prev = tail->prev;
	prev->next = phead;
	phead->prev = prev;
	free(tail);
	tail = NULL;


}
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;
}
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	ListNode* prev = pos->prev;
	ListNode* newnode = BuyListNode(x);
	prev->next = newnode;
	newnode->next = pos;
	pos->prev = newnode;
	newnode->prev = prev;
}
void ListErase(ListNode* pos, LTDataType x)
{
	assert(pos);
	ListNode* prev = pos->prev;
	ListNode* next = pos->next;
	prev->next = next;
	next->prev = prev;
	free(pos);
	pos = NULL;
}

有人可能问了,这里为啥头插尾插可以只传一级指针呢?因为这里有哨兵位置,就不必修改传过来的指针了,通过哨兵位去修改即可。

附上小题几道,将在下期讲解。

1.反转链表

2.链表的中间节点

 3.环形链表

4.合并两个有序链表

 总结

以上就是今天要讲的内容,本文仅仅介绍了链表的两种,剩下的链表结构小伙伴们可以自由探索,最后附上的力扣真题兄弟们可以尝试尝试,如果本篇文章对你的数据结构有帮助,能不能点个免费的三连呢,这对博主的创作能起到极大的助力,最后愿诸君都能学有所成。

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值