【数据结构】单链表(超详细讲解)c代码实现

文章目录

    • 1. 链表的相关概念
      • 1.1 为什么要引进链表
      • 1.2 链表的概念
      • 1.3 链表的组成
      • 1.4 链表的物理结构
      • 1.5 链表的分类
    • 2. 无头单向不循环链表的实现
      • 2.1 单链表的声明
      • 2.2 打印单链表
      • 2.3 动态申请一个节点
      • 2.4 释放所有节点
      • 2.5 单链表尾插
      • 2.6 单链表头插
      • 2.7 单链表尾删
      • 2.8 单链表头删
      • 2.9 查找指定值的节点,并返回节点地址
      • 2.10 在单链表的指定位置后插入指定值
      • 2.11 删除指定位置pos的元素
    • 3. 完整代码展示

1. 链表的相关概念

1.1 为什么要引进链表

我们已经掌握过了一种存储的数据结构了——顺序表。为什么我们还要引进链表呢?其实,顺序表存在了很多的问题,为了解决这些问题,我们就引进了链表的概念。

顺序表存在的问题:

  • 顺序表的头插、头删或者从中间插入、删除,都需要挪动数据,时间复杂度为O(n),效率较低。
  • 顺序表增加数据一直需要判断空间是否不足,不足需要增容,最后还需要释放空间,消耗很大。
  • 增容我们是按照2倍这样的方式增容的,当后面数据很大时,可能我们只需要几个空间了,然而我们突然开辟出2倍原来的空间,会造成严重的浪费空间的问题。

1.2 链表的概念

我们上一节学习了顺序表,顺序表是用一段物理地址连续的存储单元来存储数据的线性结构。而链表是采用一段物理地址不连续的存储结构,链表的基本操作是通过指针指向的方式实现的,通过指针的指向,可以使链表更加灵活

1.3 链表的组成

链表是由众多节点组成的,链表中的每个元素称为节点。

节点的组成:

  1. 数据域:存储数据元素
  2. 指针域:存储下一个节点(元素)的地址

1.4 链表的物理结构

所有节点的地址不是连续的,链表在物理结构上不一定是线性的,但在逻辑结构上是线性的。

在这里插入图片描述

1.5 链表的分类

链表的形式非常多样化

  • 单向、多向
    【数据结构】单链表(超详细讲解)c代码实现_第1张图片

【数据结构】单链表(超详细讲解)c代码实现_第2张图片

  • 带头节点、不带头节点(含哨兵位的头结点,不存储数据,只是一个结构,指向)

【数据结构】单链表(超详细讲解)c代码实现_第3张图片

在这里插入图片描述

  • 循环、非循环

【数据结构】单链表(超详细讲解)c代码实现_第4张图片

在这里插入图片描述

  • 链表中常见的两种结构

【数据结构】单链表(超详细讲解)c代码实现_第5张图片

2. 无头单向不循环链表的实现

这节我们讲无头单向不循环链表,下一节我们讲带头双向循环链表

首先我们先创建一个工程:
【数据结构】单链表(超详细讲解)c代码实现_第6张图片

  • test.c—主函数、测试整个链表的流程
  • SList.h—单链表的声明、接口函数的声明、引用的头文件
  • SList.c—单链表接口函数的具体实现

2.1 单链表的声明

//定义单链表的节点
typedef struct SListNode
{
	SLDataType data;//数据域
	struct SListNode* next;//指针域
}SLTNode;

2.2 打印单链表

//打印单链表
//不会改变链表的头指针,传一级指针
void SListPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->data);//循环遍历单链表
		cur = cur->next;//拿到当前节点的指针域,就可以找到下一个节点
	}
	printf("NULL\n");
}

2.3 动态申请一个节点

//动态申请一个节点,并返回节点的地址
SLTNode* BuySLTNode(SLDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	//检查是否开辟成功
	if (newnode == NULL)
	{
		printf("malloc fail\n");
		return;
	}
	//赋值操作
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

2.4 释放所有节点

//释放所有节点
void SListDestory(SLTNode** pphead)
{
	assert(pphead != NULL);//断言
	SLTNode* cur = *pphead;//找到第一个节点
	while (cur != NULL)//遍历链表
	{
		SLTNode* next = cur->next;//先存储前一个节点的next值,防止把它释放了,找不到下一个节点了
		free(cur);//释放节点
		cur = next;
	}
	*pphead = NULL;//置空
}

2.5 单链表尾插

【数据结构】单链表(超详细讲解)c代码实现_第7张图片

//尾插
void SListPushBack(SLTNode** pphead, SLDataType x)
{
	SLTNode* newnode = BuySLTNode(x);//先动态申请一个节点
	if (*pphead == NULL)
	{
		*pphead = newnode;//当单链表中没有元素时, 这个节点给pphead当成单链表的第一个元素
	}
	else//单链表中已经有元素了
	{
		//需要找到尾结点的指针
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail->next;
		}
		tail->next = newnode;//找到最后一个节点,并把newnode赋给最后一个节点的next值
	}
}

测试一下尾插:
【数据结构】单链表(超详细讲解)c代码实现_第8张图片
注意1:

我这这里切记要记住,我们传plist的时候,一定要传pilist的指针。我们要传址而不是传值,传值只是创建一个临时变量phead,只会改变phead的值,并不会改变我们想要的plist的值。当我们传plist的值时,因为当链表为空时,我们要创建第一个节点,我们要改变plist的指向,让他指向第一个节点。但是初始的plist和phead都是指向NULL,调用函数后,phead指向了新节点,而plist还是指向了NULL。

注意2:

我们在进行尾插时,一定要分两步,当链表为空时,当链表不为空时。当链表为空时,plist直接指向新节点;当链表不为空时,先要找到链表的最后一个节点,然后将尾结点的next指向新节点。

2.6 单链表头插

【数据结构】单链表(超详细讲解)c代码实现_第9张图片

//头插
void SListPushFront(SLTNode** pphead, SLDataType x)
{
	assert(pphead != NULL);//断言
	SLTNode* newnode = BuySLTNode(x);//动态申请一个节点
	//注意顺序不能搞反了
	newnode->next = *pphead;//新节点指向单链表的第一个节点,即新节点的next指针指向plist
	*pphead = newnode;//plist指向头插的新节点
}

测试一下头插:

【数据结构】单链表(超详细讲解)c代码实现_第10张图片
注意:单链表的头插我们不需要考虑单链表是否为空的情况,不管单链表是否为空,这样写都是正确的

2.7 单链表尾删

单链表的尾删一共有两种思路
思路一:找到单链表尾结点的上一个节点,通过上一个节点的next指针,找到最后一个节点
思路二:利用双指针的思路,分别找到单链表的尾结点和他的上一个节点

【数据结构】单链表(超详细讲解)c代码实现_第11张图片

//尾删
void SListPopBack(SLTNode** pphead)
{
	assert(pphead != NULL);//断言,检查是否传参错误
	assert(*pphead);//断言,单链表不能为空
	//分三种情况
	//1.空,已经用断言排除过了
	//2.一个节点
	//3.一个节点以上
	if ((*pphead)->next == NULL)//单链表只有一个节点
	{
		free(*pphead);//释放节点
		*pphead = NULL;
	}
	//思路一:找到尾结点的上一个节点
	//else//单链表有多个节点
	//{
	//	SLTNode* tail = *pphead;
	//	while (tail->next->next != NULL)//找到尾结点的上一个节点
	//	{
	//		tail = tail->next;
	//	}
	//	free(tail->next);//释放尾结点
	//	tail->next = NULL;//置空
	//}
	//思路二:双指针
	else//单链表有多个节点
	{
		SLTNode* prev = NULL;//尾结点的上一个节点
		SLTNode* tail = *pphead;//尾结点
		while (tail->next != NULL)
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);//删除尾结点
		prev->next = NULL;//置空
	}
}

测试一下尾删:
【数据结构】单链表(超详细讲解)c代码实现_第12张图片

2.8 单链表头删

【数据结构】单链表(超详细讲解)c代码实现_第13张图片

//头删
void SListPopFront(SLTNode** pphead)
{
	assert(pphead != NULL);//断言,检查是否传参错误
	assert(*pphead);//断言,单链表不能为空
	SLTNode* next = (*pphead)->next;//plist指向头结点的下一个节点,直接跳过第一个节点
	free(*pphead);//释放头结点
	*pphead = next;//plist指向原来的第二个节点
}

测试一下头删:
【数据结构】单链表(超详细讲解)c代码实现_第14张图片

2.9 查找指定值的节点,并返回节点地址

//查找指定数据
SLTNode* SListFind(SLTNode* phead, SLDataType x)
{
	SLTNode* cur = phead;
	while (cur != NULL)//遍历单链表
	{
		if (cur->data == x)
		{
			return cur;
		}
	}
	return NULL;//未找到,返回NULL
}

2.10 在单链表的指定位置后插入指定值

【数据结构】单链表(超详细讲解)c代码实现_第15张图片

为什么不在pos位置前插入指定值?

  • 单链表更适合在指定位置后插入指定值,如果在指定位置前插入,需要遍历链表找到指定位置的前一个节点。而在指定位置后插入数据,只需要找到pos位置就行了。C++官方库的链表给的也是在之后插入
//在pos的后面插入x
void SListInsert(SLTNode* pos, SLDataType x)
{
	assert(pos);//断言
	SLTNode* newnode = BuySLTNode(x);//动态申请一个节点
	newnode->next = pos->next;//新节点的next指向pos位置的后一个节点
	pos->next = newnode;//pos的next指向新节点的位置
}

2.11 删除指定位置pos的元素

【数据结构】单链表(超详细讲解)c代码实现_第16张图片

//删除pos位置的节点
void SListErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);
	assert(*pphead);
	//当pos正好为第一个节点时,相当于头删
	if (*pphead == pos)
	{
		SListPopFront(pphead);
	}
	else//pos在链表的中间位置
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;//pos前一个节点指向pos的后一个节点
		free(pos);//释放pos
		pos = NULL;//置空
	}
}

3. 完整代码展示

  • SList.h
#pragma once

#include 
#include 
#include 

typedef int SLDataType;

//定义单链表的节点
typedef struct SListNode
{
	SLDataType data;//数据域
	struct SListNode* next;//指针域
}SLTNode;

//不会改变链表的头指针,传一级指针
void SListPrint(SLTNode* phead);//打印单链表

//可能会改变链表的头指针,传二级指针
void SListPushBack(SLTNode** pphead, SLDataType x);//尾插
void SListPushFront(SLTNode** pphead, SLDataType x);//头插
void SListPopBack(SLTNode** pphead);//尾删
void SListPopFront(SLTNode** pphead);//头删

SLTNode* SListFind(SLTNode* phead, SLDataType x);//查找指定数据
void SListInsert(SLTNode* pos, SLDataType x);//在pos的后面插入x
void SListErase(SLTNode** pphead, SLTNode* pos);//删除pos位置的节点
  • SList.c
#include "SList.h"

//动态申请一个节点,并返回节点的地址
SLTNode* BuySLTNode(SLDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	//检查是否开辟成功
	if (newnode == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	//赋值操作
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

//释放所有节点


//打印单链表
//不会改变链表的头指针,传一级指针
void SListPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->data);//循环遍历单链表
		cur = cur->next;//拿到当前节点的指针域,就可以找到下一个节点
	}
	printf("NULL\n");
}

//释放所有节点
//void SListDestory(SLTNode** pphead)
//{
//	assert(pphead != NULL);//断言
//	SLTNode* cur = *pphead;//找到第一个节点
//	while (cur != NULL)//遍历链表
//	{
//		SLTNode* next = cur->next;//先存储前一个节点的next值,防止把它释放了,找不到下一个节点了
//		free(cur);//释放节点
//		cur = next;
//	}
//	*pphead = NULL;//置空
//}

//尾插
void SListPushBack(SLTNode** pphead, SLDataType x)
{
	assert(pphead != NULL);
	SLTNode* newnode = BuySLTNode(x);//先动态申请一个节点
	if (*pphead == NULL)
	{
		*pphead = newnode;//当单链表中没有元素时, 这个节点给pphead当成单链表的第一个元素
	}
	else//单链表中已经有元素了
	{
		//需要找到尾结点的指针
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;//找到最后一个节点,并把newnode赋给最后一个节点的next值
	}
}

//头插
void SListPushFront(SLTNode** pphead, SLDataType x)
{
	assert(pphead != NULL);//断言
	SLTNode* newnode = BuySLTNode(x);//动态申请一个节点
	newnode->next = *pphead;//新节点指向单链表的第一个节点,即新节点的next指针指向plist
	*pphead = newnode;//plist指向头插的新节点
}

//尾删
void SListPopBack(SLTNode** pphead)
{
	assert(pphead != NULL);//断言,检查是否传参错误
	assert(*pphead);//断言,单链表不能为空
	//分三种情况
	//1.空,已经用断言排除过了
	//2.一个节点
	//3.一个节点以上
	if ((*pphead)->next == NULL)//单链表只有一个节点
	{
		free(*pphead);//释放节点
		*pphead = NULL;
	}
	//思路一:找到尾结点的上一个节点
	//else//单链表有多个节点
	//{
	//	SLTNode* tail = *pphead;
	//	while (tail->next->next != NULL)//找到尾结点的上一个节点
	//	{
	//		tail = tail->next;
	//	}
	//	free(tail->next);//释放尾结点
	//	tail->next = NULL;//置空
	//}
	//思路二:双指针
	else//单链表有多个节点
	{
		SLTNode* prev = NULL;//尾结点的上一个节点
		SLTNode* tail = *pphead;//尾结点
		while (tail->next != NULL)
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);//删除尾结点
		prev->next = NULL;//置空
	}
}

//头删
void SListPopFront(SLTNode** pphead)
{
	assert(pphead != NULL);//断言,检查是否传参错误
	assert(*pphead);//断言,单链表不能为空
	SLTNode* next = (*pphead)->next;//plist指向头结点的下一个节点,直接跳过第一个节点
	free(*pphead);//释放头结点
	*pphead = next;//置空
}

//查找指定数据
SLTNode* SListFind(SLTNode* phead, SLDataType x)
{
	SLTNode* cur = phead;
	while (cur != NULL)//遍历单链表
	{
		if (cur->data == x)
		{
			return cur;
		}
	}
	return NULL;//未找到,返回NULL
}

//在pos的后面插入x
void SListInsert(SLTNode* pos, SLDataType x)
{
	assert(pos);//断言
	SLTNode* newnode = BuySLTNode(x);//动态申请一个节点
	newnode->next = pos->next;//新节点的next指向pos位置的后一个节点
	pos->next = newnode;//pos的next指向新节点的位置
}

//删除pos位置的节点
void SListErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);
	assert(*pphead);
	//当pos正好为第一个节点时,相当于头删
	if (*pphead == pos)
	{
		SListPopFront(pphead);
	}
	else//pos在链表的中间位置
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;//pos前一个节点指向pos的后一个节点
		free(pos);//释放pos
		pos = NULL;//置空
	}
}

你可能感兴趣的:(数据结构,数据结构,c语言,链表)