单链表|附完整源码—c语言版

 我们都知道数组在内存中是连续存储的,顺序表就是由链表存储的,它有一个其他数据结构都没有的优点——支持随机访问(就是利用下标访问元素),这可以让其支持一些排序算法的操作。

但是顺序表也有一些如下缺点:

1. 中间/头部的插入删除,时间复杂度为O(N)
2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容

到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间

结合着顺序表的缺点,链表也就诞生了

链表的概念及结构


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

 总的来说,链表在内存中存储不一定连续,大多由内存碎片彼此连接而成。那么它们又是靠什么连接在一起的呢?那当然少不了指针的功劳。

链表本质上就是一个结构体,分为存储数据的val以及指向下一个节点的指针next,当然如果是双向循环的话就会有两个指针了,我们这里都以单链表为例。

链表的类型按方向,是否带头,是否循环各分两类,所以组合起来一共就有八种类型。不过请大家不必担心,其中最常用的就是结构最简单的不带头单向非循环链表以及结构最复杂但是实施起来非常简单的带头双向循环链表

本篇主要介绍不带头单向非循环链表

 

 

不带头单向非循环链表的实现

学习一种数据结构,就是要学会它的增删查改,再进一步学会它的应用。我们就来看看不带头单向非循环链表是如何实现的吧!

不带头单向非循环链表虽然是结构最简单的,但其实它是实现起来最复杂的,不信的小伙伴们可以在学完不带头单向非循环链表后去实现其他的链表,会发现都不在话下。

结点结构的定义

typedef int SLDateType;

typedef struct SLTNode
{
	SLDateType data;
	SLTNode* next;
}SLTNode;

如上文所讲,一个结点包含的内容有数据data和保存下一个结点的地址的指针。

链表正是由这样一个个的结构体由指针连起来而形成的,所以我们只需要知道它的头结点的位置,就能访问链表的所有数据了。

函数接口的实现

SLTNode* SListBuildNode(SLDateType x);//创建一个结点
void SListPrint(SLTNode* phead);//将链表打印出来,主要用来检验接口是否实现正确
void SListPushBack(SLTNode** pphead, SLDateType x);//尾插
void SListPopBack(SLTNode** phead);//尾删
void SListPushFront(SLTNode** pphead, SLDateType x);//头插
void SListPopFront(SLTNode** phead);//头删
SLTNode* SListFind(SLTNode* phead, SLDateType x);//在链表中查找一个数
void SListDestory(SLTNode** pphead);//销毁整个单链表
void SListInsert(SLTNode* phead, SLTNode* pos, SLDateType x);//在链表的中间插入一个结点
void SListErase(SLTNode** pphead, SLTNode* pos);//删除任意位置的数据

由于要实现的接口很多,代码量较大,我们这里选择较难的尾插和尾删来实现,只要这两个会了,想必其他的大家都能够斩于马下。完整源码将会放到文章末尾处。

尾删

思路:

  1. 把最后一个结点删除,即把最后一个结点free掉,然后把倒数第二个结点的next指向NULL。
  2. 上面提到倒数第二个,那如果链表只有一个结点呢?
  3. 更进一步,如果链表删着删着为空,没有一个结点了呢?

结合上文,我们应该分为三种情况,对于链表为空的非法情况,我们直接assert即可。

有如下代码

void SListPopBack(SLTNode** pphead)
{
	assert(*pphead);
	SLTNode* cur = *pphead;
	
	//链表只有一个数据
	if ((*pphead)->next == NULL)
	{
		*pphead = (*pphead)->next;
		free(cur);
		cur = NULL;
	}
	else
	{
		while (cur->next->next)
		{
			cur = cur->next;
		}
		free(cur->next);
		cur->next = NULL;
	}
}

尾插

思路:

首先将cur指向头结点,通过变化tail遍历链表找到最后一个尾结点,然后将其的next指向新结点newNode,newNode的next置空即可完成。

注意:

  1. 我们要找的是最后一个结点,而尾删找的是倒数第二个结点,这里有细微区别。
  2. 因为插入结点的时候都要创造一个结点,为了代码的复用我们将其定义为一个函数SLTNode* SListBuildNode(SLDateType x);

 有了思路,代码实现起来就很简单了

void SListPushBack(SLTNode** pphead, SLDateType x)
{
	SLTNode* newNode = SListBuildNode(x);
	//链表为空
	if (*pphead == NULL)
	{
		*pphead = newNode;
	}
	else
	{
		SLTNode* cur = *pphead;
		while (cur->next)
		{
			cur = cur->next;
		}
		cur->next = newNode;
	}
}

注意:
为什么有的接口要用到二级指针呢?

我们首先要明确一个概念,要改变谁,就要通过谁的指针解引用进行操作。

改变int,就要用到int*;改变int*,就要用到int**。

以头插为例子,我们将newNode的next指向头结点phead后,phead是不是应该要往前移动指向newNode,使得newNode成为新的头结点呢?所以这里就要传二级指针啦!

 

——————————————————————————————————————————

完整源码如下

SList.h

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


typedef int SLDateType;

typedef struct SLTNode
{
	SLDateType data;
	SLTNode* next;
}SLTNode;

SLTNode* SListBuildNode(SLDateType x);
void SListPrint(SLTNode* phead);
void SListPushBack(SLTNode** pphead, SLDateType x);
void SListPopBack(SLTNode** phead);
void SListPushFront(SLTNode** pphead, SLDateType x);
void SListPopFront(SLTNode** phead);
SLTNode* SListFind(SLTNode* phead, SLDateType x);
void SListDestory(SLTNode** pphead);//销毁整个单链表a
void SListInsert(SLTNode* phead, SLTNode* pos, SLDateType x);
void SListErase(SLTNode** pphead, SLTNode* pos);//删除任意位置的数据

 SList.c

SLTNode* SListBuildNode(SLDateType x)
{
	SLTNode* newNode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newNode == NULL)
	{
		perror("malloc");
		exit(-1);
	}
	newNode->next = NULL;
	newNode->data = x;
	return newNode;
}

void SListPrint(SLTNode* phead)
{
		SLTNode* cur = phead;
		while (cur != NULL)
		{
			printf("%d->", cur->data);
			cur = cur->next;
		}
		printf("NULL\n");
}

void SListPushBack(SLTNode** pphead, SLDateType x)
{
	SLTNode* newNode = SListBuildNode(x);
	//链表为空
	if (*pphead == NULL)
	{
		*pphead = newNode;
	}
	else
	{
		SLTNode* cur = *pphead;
		while (cur->next)
		{
			cur = cur->next;
		}
		cur->next = newNode;
	}
}

void SListPopBack(SLTNode** pphead)
{
	assert(*pphead);
	SLTNode* cur = *pphead;
	
	//链表只有一个数据
	if ((*pphead)->next == NULL)
	{
		*pphead = (*pphead)->next;
		free(cur);
		cur = NULL;
	}
	else
	{
		while (cur->next->next)
		{
			cur = cur->next;
		}
		free(cur->next);
		cur->next = NULL;
	}
}
void SListPushFront(SLTNode** pphead, SLDateType x)
{
	SLTNode* newNode = SListBuildNode(x);

	newNode->next = *pphead;
	*pphead = newNode;
}
void SListPopFront(SLTNode** pphead)
{
	assert(*pphead);
	SLTNode* cur = (*pphead)->next;
	free(*pphead);
	*pphead = cur;
}
SLTNode* SListFind(SLTNode* phead, SLDateType x)
{
	SLTNode* cur = phead;
	while (cur->data != x)
	{
		cur = cur->next;
		if (cur == NULL)
		{
			printf("链表中没有该值\n");
			return NULL;
		}
	}
	return cur;
}

void SListDestory(SLTNode** pphead)//销毁整个单链表
{
	assert(*pphead);

	while (*pphead)
	{
		SLTNode* last = (*pphead)->next;
		free(*pphead);
		*pphead = last;
	}

}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值