当前位置: 首页 > news >正文

【数据结构】C语言实现链表(单链表部分)

目录

前言

链表

链表的分类

1.单向或者双向

 2.带头或者不带头

 3.循环或者非循环

单链表实现

定义节点

接口函数实现

创建节点

打印链表

尾插节点

尾删节点

头插节点

 头删节点

 单链表查找

删除指定位置后的节点

 指定位置后插入节点

删除指定位置

指定位置插入节点

销毁链表

  单链表完整代码


前言

之前我们实现过顺序表,对于顺序表,我们也得思考下面几个问题:

1.中间/头部的插入删除,时间复杂度为O(N)。
2.增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
3.增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
思考:如何解决以上问题呢?下面给出了链表的结构来看看。

链表

概念及结构

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

相对于顺序表,链表在物理结构上并不连续,但是在逻辑结构上是连续的。如图:

 

注意:

1、从上图可以看出,链式结构在逻辑结构上是连续的,但是在物理结构上不一定连续

2、现实中的节点一般都是从堆上申请出来的

3、从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续

链表的分类

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

1.单向或者双向

 2.带头或者不带头

 3.循环或者非循环

 虽然有这么多的链表的结构,但是实际中最常用的还是两种结构:

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

2.带头双向循环链表:结构最复杂,一般用来单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这种链表结构虽然复杂,使用代码实现起来会发现这种结构会有许多优势,实现起来反而更容易。

单链表实现

定义节点

链表的单个节点由数据域跟指针域构成,节点中指针域所存储的指针就是下一个节点的地址。

这里有一点需要注意,next的指针类型是struct SListNode*,不是SLTNode*

typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType x;
	struct SListNode* next;
}SLTNode;

接口函数实现

PS:

        实现增删查改函数之前我们需要知道,单链表并不需要初始化函数,我们只需要创建一个struct SListNode* plist = NULL;由plist指针来管理我们的单链表就可以了。

链表作为数据结构的一种,要实现的功能无非就是增删查改,在实现这些功能之前,我们先来实现一个创建节点的函数。

创建节点

//创建节点
SLTNode* BuySLTNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
    //创建节点失败就退出程序
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	newnode->x = x;
	newnode->next = NULL;
	return newnode;
}

 有了创建节点函数之后,我们在实现一个打印链表信息的函数,这样我们在实现其他函数的时候可以通过打印链表来观察函数功能是否正确。

打印链表

//打印链表
void SLTPrint(SLTNode* phead)
{
	if (phead == NULL)
	{
		printf("NULL\n");
		return;
	}
	SLTNode* cur = phead;
	while (cur)
	{
		printf("%d->", cur->x);
		cur = cur->next;
	}
	printf("NULL\n");
}

如果我们的链表是1 2 3 4 5;打印出来的结果就是1->2->3->4->5->NULL

尾插节点

//尾插
void SLT_PushBack(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuySLTNode(x);
    //如果链表为空,那么此时就是第一次向链表中插入节点,
    //那么只需要将我们创建的节点的指针赋值给*pphead就可以了
	if (*pphead == NULL)
	{
		*pphead = newnode;
		return;
	}
    //如果链表不为空,我们这时需要找到该链表的最后一个节点,
    //然后将我们创建的节点链接在最后一个节点的后面。
	SLTNode* cur = *pphead;
	while (cur->next != NULL)
	{
		cur = cur->next;
	}
	cur->next = newnode;
}

 

 

为什么传入的参数是二级指针?       

       在前面我们讲过,单链表的初始化非常简单,只需要我们创建一个struct SListNode* plist,就可以了。但是,在添加新节点的函数中,我们的参数为什么是一个二级指针呢?那是因为,我们实现的函数中,函数中的参数只是实参的一份拷贝,当我们想要改变他的时候,就必须要传入它的指针(地址)。当我们的链表没有节点的时候,此时plist就是一个NULL,我们向链表中添加节点后,之前的空链表就变成了一个具有一个节点的链表,那么这时就要改变plist。所以我们要传入plist的指针(地址),这样才能改变plist。

       说得简单一点就是,当传入一个空链表并且我们要向这个链表中添加新节点的时候,plist自身会发生改变,所以我们要传入plist的指针。

       后面的尾删、头插、头删函数传入的都是二级指针。都是因为这个原因。

尾删节点

//尾删节点
void SLT_PopBack(SLTNode** pphead)
{
    //如果传入的是空链表,那么就没有删除节点的必要
	assert(*pphead);
    //如果链表只有一个节点,那么
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
		return;
	}
    //在删除最后一个节点的同时,我们需要找到最后一个节点的前一个节点
    //将它的next置成NULL
	SLTNode* cur = *pphead;
	SLTNode* prev = *pphead;
	while (cur->next != NULL)
	{
		prev = cur;
		cur = cur->next;
	}
    //记得将最后一个节点释放,虽然删除后我们也拿不到这个节点的地址
    //但是,这个节点是由我们手动开辟的,一定要置空,防止内存泄漏
	free(cur);
	prev->next = NULL;
}

 

头插节点

//头插节点
void SLT_PushFront(SLTNode** pphead, SLTDataType x)
{    
    //如果链表为空,直接将创建的节点赋值给*pphead
	if (*pphead == NULL)
	{
		*pphead = BuySLTNode(x);
		return;
	}
	SLTNode* newnode = BuySLTNode(x);
	SLTNode* cur = *pphead;
	newnode->next = cur;
	*pphead = newnode;
}

 头删节点

//头删节点
void SLT_PopFront(SLTNode** pphead)
{
    //如果传入的是空链表,就没有必要删除
	assert(*pphead);
	SLTNode* cur = *pphead;
	SLTNode* next = (*pphead)->next;
	free(cur);
    //将第一个节点删除之后,头节点改变。
	*pphead = next;
}

 

 单链表查找

//单链表查找节点
SLTNode* SLT_Find(SLTNode* phead, SLTDataType x)
{
	assert(phead);
	SLTNode* cur = phead;
	while (cur)
	{
        //找到就返回该节点指针
		if (cur->x == x)
		{
			return cur;
		}
		cur = cur->next;
	}
    //找不到就返回空指针
	return NULL;
}

删除指定位置后的节点

//删除指定位置后节点
void SLT_EraseAfter(SLTNode* pos)
{
    //如果pos是最后一个节点,那么就没有必要删除
	assert(pos->next);
	SLTNode* nextnode = pos->next->next;
    //动态开辟的节点,一定要释放
	free(pos->next);
	pos->next = nextnode;
}

 指定位置后插入节点

//指定位置后插入节点
void SLT_InsertAfter(SLTNode* pos, SLTDataType x)
{
    //断言,防止传入空指针
	assert(pos);
	SLTNode* newnode = BuySLTNode(x);
	SLTNode* next = pos->next;
	pos->next = newnode;
	newnode->next = next;
}

 

删除指定位置

//删除指定位置
void SLT_Erase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);
    //如果删除的节点是第一个节点,那么就是头删
	if (*pphead == pos)
	{
		SLTNode* cur = *pphead;
		*pphead = (*pphead)->next;
		free(cur);
		return;
	}
	SLTNode* cur = *pphead;
	SLTNode* prev = NULL;
	while (cur != pos)
	{
		prev = cur;
		cur = cur->next;
	}
	prev->next = cur->next;
	free(cur);
}

 

 

指定位置插入节点

//pos位置插入节点
void SLT_Insert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pos);
    //如果pos是第一个节点,那么就是头插
	if (*pphead == pos)
	{
		SLTNode* newnode = BuySLTNode(x);
		newnode->next = *pphead;
		*pphead = newnode;
		return;
	}
	SLTNode* cur = *pphead;
	SLTNode* prev = NULL;
	while (cur!=pos)
	{
		prev = cur;
		cur = cur->next;
	}
	SLTNode* newnode = BuySLTNode(x);
	prev->next = newnode;
	newnode->next = pos;
}

销毁链表

//销毁链表
void SLT_Destroy(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead);
	SLTNode* cur = *pphead;
	SLTNode* nextnode = *pphead;
	while (cur)
	{
		nextnode = cur->next;
		free(cur);
		cur = nextnode;
	}
    //最后将*pphead置空,防止非法访问
	*pphead = NULL;
}

  单链表完整代码

SList.h

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int SLTDataType;

typedef struct SListNode
{
	SLTDataType x;
	struct SListNode* next;
}SLTNode;

//创建一个节点
SLTNode* BuySLTNode(SLTDataType x);

//打印链表
void SLTPrint(SLTNode* phead);

//尾删尾插
void SLT_PushBack(SLTNode** pphead, SLTDataType x);
void SLT_PopBack(SLTNode** pphead);

//头插头删
void SLT_PushFront(SLTNode** pphead, SLTDataType x);
void SLT_PopFront(SLTNode** pphead);

//单链表查找
SLTNode* SLT_Find(SLTNode* plist, SLTDataType x);

// 单链表在pos位置之后插入x
void SLT_InsertAfter(SLTNode * pos, SLTDataType x);

//在pos位置插入x
void SLT_Insert(SLTNode* phead, SLTNode* pos, SLTDataType x);

// 单链表删除pos位置之后的值
void SLT_EraseAfter(SLTNode* pos);

//删除pos位置
void SLT_Erase(SLTNode** pphead, SLTNode* pos);

//销毁链表
void SLT_Destroy(SLTNode** pphead);

SList.c

#include "SList.h"

SLTNode* BuySLTNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	newnode->x = x;
	newnode->next = NULL;
	return newnode;
}

void SLTPrint(SLTNode* phead)
{
	if (phead == NULL)
	{
		printf("NULL\n");
		return;
	}
	SLTNode* cur = phead;
	while (cur)
	{
		printf("%d->", cur->x);
		cur = cur->next;
	}
	printf("NULL\n");
}

void SLT_PushBack(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuySLTNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
		return;
	}
	SLTNode* cur = *pphead;
	while (cur->next != NULL)
	{
		cur = cur->next;
	}
	cur->next = newnode;
}

void SLT_PopBack(SLTNode** pphead)
{
	assert(*pphead);
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
		return;
	}
	SLTNode* cur = *pphead;
	SLTNode* prev = *pphead;
	while (cur->next != NULL)
	{
		prev = cur;
		cur = cur->next;
	}
	free(cur);
	prev->next = NULL;
}

void SLT_PushFront(SLTNode** pphead, SLTDataType x)
{
	if (*pphead == NULL)
	{
		*pphead = BuySLTNode(x);
		return;
	}
	SLTNode* newnode = BuySLTNode(x);
	SLTNode* cur = *pphead;
	newnode->next = cur;
	*pphead = newnode;
}

void SLT_PopFront(SLTNode** pphead)
{
	assert(*pphead);
	SLTNode* cur = *pphead;
	SLTNode* next = (*pphead)->next;
	free(cur);
	*pphead = next;
}

SLTNode* SLT_Find(SLTNode* phead, SLTDataType x)
{
	assert(phead);
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->x == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return cur;
}

void SLT_InsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySLTNode(x);
	SLTNode* next = pos->next;
	pos->next = newnode;
	newnode->next = next;
}

void SLT_Insert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pos);
	if (*pphead == pos)
	{
		SLTNode* newnode = BuySLTNode(x);
		newnode->next = *pphead;
		*pphead = newnode;
		return;
	}
	SLTNode* cur = *pphead;
	SLTNode* prev = NULL;
	while (cur!=pos)
	{
		prev = cur;
		cur = cur->next;
	}
	SLTNode* newnode = BuySLTNode(x);
	prev->next = newnode;
	newnode->next = pos;
}

void SLT_EraseAfter(SLTNode* pos)
{
	assert(pos->next);
	SLTNode* newnode = pos->next->next;
	free(pos->next);
	pos->next = newnode;
}

void SLT_Erase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);
	if (*pphead == pos)
	{
		SLTNode* cur = *pphead;
		*pphead = (*pphead)->next;
		free(cur);
		return;
	}
	SLTNode* cur = *pphead;
	SLTNode* prev = NULL;
	while (cur != pos)
	{
		prev = cur;
		cur = cur->next;
	}
	prev->next = cur->next;
	free(cur);
}

void SLT_Destroy(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead);

	SLTNode* cur = *pphead;
	SLTNode* nextnode = *pphead;
	while (cur)
	{
		nextnode = cur->next;
		free(cur);
		cur = nextnode;
	}
	*pphead = NULL;
}

以上就是单链表实现的全部内容了,希望能够帮到大家。如果有什么错误的地方还请各位指正。

相关文章:

  • JAVA练习8
  • 聊聊最适合程序员的画图工具
  • JAVA数据结构篇--12理解LinkedHashSetTreeSet
  • DR_CAN基尔霍夫电路题解法【自留用】
  • 21级数据结构考前模拟题
  • 剑指offer----C语言版----第六天
  • Qt音视频开发08-ffmpeg内核优化(极速打开/超时回调/实时响应)
  • 网络安全一哥的奇安信发布了全球高级可持续威胁年度报告 值得学习
  • 13---SpringBoot整合JWT,实现登录和拦截
  • 4366. 上课睡觉
  • vue后台系统管理项目-echarts柱状图实现订单统计
  • 2022年博客之星排行榜 日榜 2023-01-01 博客之星总榜
  • 一名普通Java程序员的2022的总结和2023的展望
  • 【寒假每日一题】洛谷 P1079 [NOIP2012 提高组] Vigenère 密码
  • 【概率论】期末复习笔记:参数估计
  • angular组件开发
  • C++类的相互关联
  • eclipse的离线汉化
  • ES6简单总结(搭配简单的讲解和小案例)
  • exports和module.exports
  • Git初体验
  • Javascript 原型链
  • JavaScript新鲜事·第5期
  • JavaSE小实践1:Java爬取斗图网站的所有表情包
  • Java知识点总结(JavaIO-打印流)
  • nodejs:开发并发布一个nodejs包
  • PAT A1050
  • Rancher如何对接Ceph-RBD块存储
  • RedisSerializer之JdkSerializationRedisSerializer分析
  • vue-cli在webpack的配置文件探究
  • vue从创建到完整的饿了么(18)购物车详细信息的展示与删除
  • 从重复到重用
  • 关于List、List?、ListObject的区别
  • 警报:线上事故之CountDownLatch的威力
  • 浅谈JavaScript的面向对象和它的封装、继承、多态
  • 如何设计一个比特币钱包服务
  • 入门级的git使用指北
  • 跳前端坑前,先看看这个!!
  • 系统认识JavaScript正则表达式
  • 优秀架构师必须掌握的架构思维
  • - 语言经验 - 《c++的高性能内存管理库tcmalloc和jemalloc》
  • 格斗健身潮牌24KiCK获近千万Pre-A轮融资,用户留存高达9个月 ...
  • ​力扣解法汇总1802. 有界数组中指定下标处的最大值
  • ​无人机石油管道巡检方案新亮点:灵活准确又高效
  • # 20155222 2016-2017-2 《Java程序设计》第5周学习总结
  • (02)Hive SQL编译成MapReduce任务的过程
  • (17)Hive ——MR任务的map与reduce个数由什么决定?
  • (33)STM32——485实验笔记
  • (delphi11最新学习资料) Object Pascal 学习笔记---第7章第3节(封装和窗体)
  • (Oracle)SQL优化技巧(一):分页查询
  • (rabbitmq的高级特性)消息可靠性
  • (多级缓存)缓存同步
  • (附源码)spring boot儿童教育管理系统 毕业设计 281442
  • (三)docker:Dockerfile构建容器运行jar包
  • (学习日记)2024.01.19