c语言数据结构——单向不带头不循环链表的实现

文章目录

单向不带头不循环链表

今天这篇文章将介绍一个新的数据结构类型------链表。

链表有八种结构,以单向/双向,带头/不带头,循环/不循环进行分类。

其中有两种最常用的:
1.单向不带头不循环链表
2.双向不带头循环链表

这篇文章对第一种形式进行实现

链表与顺序表的区别

在此之前,已经了解过了顺序表的实现了。顺序表的底层是使用数组进行实现的。数组的优点就是可以随机访问,但是由于空间容量的倍增,很容易造成空间的浪费。

在这里先介绍链表的优点,具体会在后面来说:

链表是由一系列节点组成的,每个节点存储着下一个节点的地址。这样子就可以通过地址的索引找到下一个节点。链表每开辟一个新的节点,就向内存申请一块空间。这样子不会造成空间的浪费。

多文件管理

链表的文件管理仍是采取和顺序表一样的方式:

SingleLinkedList.h 负责头文件的包含、变量的定义、函数的定义
SingleLinkedList.c 对头文件中定义的函数进行实现
test.c 测试与调试对应功能的是否可行正确

链表的定义结构

首先我们来看看什么是链表:

来举一个生动形象的例子:比如火车。火车是由一节一节的车厢组成的,每一节车厢里面坐着乘客,车厢与车厢之间是连接的。这就很像链接起来的。

而链表的组成就是一系列的节点通过链接而来。每个节点内存放着想要的数据。而节点与节点之间的连接是通过指针实现的。每个节点都存放着下一个节点的地址,若没有下一个节点,那么这个地址置空即可。

所以我们知道了定义链表,也就是定义链表的节点:

c 复制代码
typedef int SListDataType;

typedef struct SListNode {//每个节点的定义
	SListDataType x;
	struct SListNode* next;
}SListNode;

还是一样,需要把数据类型进行重命名,方便修改想要存储的数据。

这里有两种方式对链表进行管理。
1.再创建一个结构体

就像顺序表一样,顺序表是通过SL结构体进行管理顺序表的。这个结构体内部含有一个指向顺序表首元素地址的指针。所以在测试流程中需要专门写一个函数对这个变量进行初始化。

同样的道理:我们可以专门创建一个结构体来管理链表的后续空间:

c 复制代码
typedef struct SingleLinkedList {
	SListNode* head;//指向第一个节点的指针
	int num;//节点个数
}SList;

这样子操作,就需要专门写一个函数对链表进行初始化了。

2.不需要创建结构体

当然,也可以直接在main函数中创建一个指向的节点的指针:SListNode* head=NULL;

后续插入元素的时候就把这个头指针作为参数传入函数中插入即可。因为已经完成了对头指针的初始化,所以并不需要再专门写一个函数。

在本篇文章中,使用第二种方法进行管理。

获得链表节点个数

c 复制代码
int GetSListDataQuantity(SListDataType** pphead) {
	SListNode* pmove = *pphead;
	int count = 0;
	while (pmove) {
		pmove = pmove->next;
		count++;
	}
	return count;
}

这个操作十分的简单。我们知道链表是由一个一个节点组成的。只要某节点的下一个节点为空,就说明该节点就是最后一个节点。所以只需要从头开始遍历即可,直到pmove为空指针时退出循环。

链表增加元素

链表的尾插及创建节点函数

链表的尾插,顾名思义,就是在链表的结尾插入一个新的节点。

假设有这么个链表,我们应该如何进行尾插呢?

1.得开辟一个新的节点,并且将新的节点中的元素赋值完毕

这一步操作就需要使用malloc函数,开辟一个大小为sizeof(SListNode)的空间,并把这个新空间的内容赋值完毕。
2.需要找到链表的尾部,然后将尾部节点的next指针指向这个新的节点

但是这里要注意的是,如果链表有节点存在,尾巴就是最后一个节点。但是如果此时链表为空呢?

此时head==NULL,我们直接在head的后面插入新节点就可以了。所以找尾插入是需要进行分类讨论的。

还需要注意一点的是:插入节点的方法有很多种,比如尾插,头插,任意节点后面插入。都是需要使用malloc函数开辟新的节点。

正常插入操作如果在每一个方法内都写,非常冗杂,且重复片段。这个时候我们就想,能不能创建一个函数,专门生成节点的并且把地址返回呢?这样不就很方便插入了吗?

当然是可以的:

c 复制代码
SListNode* SListBuyNode(SListDataType x) {
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
	if (!newnode) {
		perror("malloc failed");
		exit(-1);
	}
	newnode->x = x;
	newnode->next = NULL;
	return newnode;
}

定义一个SListBuyNode函数,返回值为SListNode*,这样子,开辟新节点的事情就是由这个函数进行完成的。这个函数会返回这个节点的地址。再插入的时候,只需要将链表的尾部的next指针指向这个新节点的地址就ok了。然后要注意的是如果节点开辟不成功程序不能再继续进行,需要马上退出。

尾插函数的实现:

c 复制代码
void SListPushBack(SListNode** pphead, SListDataType x) {
	if (!*pphead) *pphead = SListBuyNode(x); 
	else {
		SListNode* ptail = *pphead;
		while (ptail->next) {
			ptail = ptail->next;
		}
		ptail->next = SListBuyNode(x);
	}
}

再来解释一下为什么要使用二级指针作为形参

我们知道,要想通过函数修改一个变量的内容,是需要传址调用的。因为形参是实参的一份临时拷贝。只有接收到地址时候,才能根据这个地址修改到实参的内容。

而刚刚讲到,在main函数中声名了一个结构体指针SListNode* head = NULL;虽然head本身就是一个地址,但是传址调用是相对的。也就是说,形参是指向形参的地址。此时head是一个指针变量,存放地址。但head终归是一个变量,也会有地址。所以需要传入head变量的地址也就是&head作为实参。一级指针变量的地址是二级指针类型。所以形参需要用二级指针进行接收。

当不用修改链表的内容的时候,只需要传入一级指针就可以了。但是为了方便,我在定义全部函数的时候都选择使用二级指针接收 (因为可以直接cv参数部分)。

链表的头插

前面是在链表的尾部进行插入,那能不能在链表的头部插入呢?答案是可以的。有了前面尾插的介绍,头插就十分简单了。

先来看代码:

c 复制代码
void SListPushFront(SListNode** pphead, SListDataType x) {
	if (!*pphead) *pphead = SListBuyNode(x);
	else {
		SListNode* NewNode = SListBuyNode(x);
		SListNode* OriginHead = *pphead;
		(*pphead) = NewNode;
		NewNode->next = OriginHead;
	}
}

还是需要进行分析:

如果链表为空,那么头插其实就是在链表的头指针后插入。

如果链表不为空,就开辟一个新节点,然后使用标记一下头插前链表的第一个节点的地址OriginHead

*pphead永远指向的是新链表的头,所以让 *pphead指向newnode,然后让newnode的next指针指向OriginHead,这样,头插的连接操作就完成了。

任意位置节点后插入

讲完头插和尾插后,我们再来实现一个在给定位置节点后插入:

c 复制代码
void SListInsertPos(SListNode** pphead, SListDataType x,int pos) {
	if (pos<0 || pos>GetSListDataQuantity(pphead)) {
		printf("The Pos %d Is Not Legal! Cannot Delete!\n",pos);
		return;
	}
	else {
		if (!pos) {
			SListPushFront(pphead, x);
		}
		else {
			pos--;
			SListNode* p = *pphead;
			while (pos--) {
				p = p->next;
			}
			//p指向的是要第pos个节点
			SListNode* pnext = p->next;
			SListNode* new = SListBuyNode(x);
			new->next = p->next;
			p->next = new;
		}
	}
}

虽然是任意位置插入,但不是真的任意。

当传入的pos<0或者pos>此时链表节点个数时,不能插入,并且需要提示。

当pos=0时,规定为头插。

其余的情况都是找到第pos个位置的节点,在这个节点的"尾插"即可。

判断链表是否为空

在讲到删除操作前,首先的i先定义一个函数SListEmpty,判断链表是否为空。

为什么呢?

因为如果是一个空链表,那么就没有执行删除操作的必要了,直接提示即可。

c 复制代码
bool SListEmpty(SListDataType** pphead) {
	if (!*pphead) return true; 
	else return false; 
}

使用bool类型作为返回值。只需要判断一下*pphead是否为NULL即可。

链表删除元素

链表的尾删

对应链表尾插,我们可以定义函数尾删。

我们先来分析一下:

如果链表为空,给予提示后直接返回,不执行删除操作。

如果不为空,需要找到最后一个节点,并且将该节点的前一个节点的next指针指向该节点的下一个节点的地址。

就像这样,需要把plast->next=p->next;

但是,当链表只有一个节点的时候,是没有所谓的前面一个节点的。所以当链表节点个数为1时候,直接删除唯一的一个节点就可以,然后让*pphead=NULL。其余情况按照上述操作。

c 复制代码
void SListDeleteBack(SListNode** pphead) {
	if (SListEmpty(pphead)) {
		printf("Already No Element! Cannot Delete\n");
		return;
	}
	else {
		if (GetSListDataQuantity(pphead) == 1) {
			free(*pphead);
			*pphead = NULL; 
		}
		else {
			SListNode* pdelete = *pphead;
			while (pdelete->next->next) {
				pdelete = pdelete->next;
			}
			SListNode* ppdelete = pdelete->next;
			pdelete->next = NULL;
			free(ppdelete);
			ppdelete = NULL;
		}
		return;
	}
}

当然,在这里我进行了一些操作,走到的是倒数第二个节点,然后再找到最后一个节点。这样子十分方便。因为单链表是没办法往前走的,所以当先走到尾节点的时候,需要再进行一次遍历找到尾节点的前一个,非常麻烦。

删除节点,其实也就是释放节点内存,使用函数free即可,删除完后及时置空,避免野指针出现。

链表的头删

对应头插 也可以进行头删

c 复制代码
void SLitsDeleteFront(SListNode** pphead) {
	if (SListEmpty(pphead)) { 
		printf("Already No Element! Cannot Delete\n");
		return;
	}
	else{
		SListNode* OriginHead = *pphead;
		*pphead = OriginHead->next;
		free(OriginHead);
		OriginHead = NULL;
	}
}

还是一样,若链表为空,不能删。

如果不为空:因为头删就是删除*pphead指向的节点。所以标记一下,让OriginHead指向原来的头,然后真正的头往后移动一个节点。删除OriginHead指向的节点即可。

任意位置删除

当想要任意位置删除时:

c 复制代码
void SListDeletePos(SListNode** pphead,int pos) {
	if (SListEmpty(pphead)) {
		printf("Already No Element! Cannot Delete\n");
		return;
	}
	else {
		if (pos <= 0 || pos > GetSListDataQuantity(pphead)) {
			printf("The Pos %d Is Not Legal!\n", pos);
			return;
		}
		else {
			if (pos == 1) SLitsDeleteFront(pphead);
			else {
				pos = pos - 2;
				SListNode* p = *pphead;
				while (pos--) p = p->next;
				//此时p指向的是第pos个节点的上一个
				SListNode* pnext = p->next, * ppnext = pnext->next;
				p->next = ppnext;
				free(pnext);
				pnext = NULL;
			}
		}
	}
}

链表为空:无法删除,提示并返回

链表不为空:还是需要分类。

当pos<0或者pos>节点个数,这是无法删除的,给予提示并返回

反之:因为删除某节点是需要将被删除节点的前一个节点的next指针指向被删除节点的后一个节点。当pos=1,即删除第一个节点的时候,没有前面节点,但是其实就是头删方式。

其余情况则先找到第pos个节点的前一个,然后找到pos节点和pos节点的下一个,然后执行删除pos节点操作即可。

如图:就是让p指向pos前一个节点,pnext指向pos节点,ppnext指向pos下一个节点。

然后连接p与ppnext指向的节点,断开pnext和ppnext指向节点的连接,删除pnext指向节点即可。

链表查找元素

可以通过坐标来找到坐标节点指向元素,也可以输入数据来寻找该数据是否存在。这些都十分简单,只重点讲述一下通过输入数据寻找。

代码实现

c 复制代码
void SListDataFind(SListNode** pphead, SListDataType x) {
	if (SListEmpty(pphead)) printf("No Elements!\n");
	else {
		int flag = 0;
		SListNode* pfind = *pphead;
		int pos = 1;
		while (pfind) {
			if (pfind->x == x) {
				printf("The Data %d Is In No.%d Node!\n", x, pos);
				flag = 1;
			}
			pfind = pfind->next;
			pos++;
		}
		if (!flag) printf("The Data %d Is Not Exist!\n", x);
	}
}

链表为空 提示链表无元素后返回。

反之:让指针pfind从头到尾找,并提示坐标。不使用返回值形式是因为链表中可能存在一样的数据。

定义的flag是一个标志。因为无论如何链表一定会被pfind遍历完,只要找到过一次元素,就让标志flag=1,出循环后,如果没有找到想要的元素,flag一定为0。

链表修改元素

在这里简单介绍一下通过坐标进行修改对应元素:

c 复制代码
void SListChangeDataPos(SListNode** pphead, SListDataType x, int pos) {
	if (pos <= 0 || pos > GetSListDataQuantity(pphead)) {
		printf("The Pos %d Is Not Legal!\n",pos);
		return;
	}
	SListNode* p = *pphead;
	pos--;
	while (pos--) {
		p = p->next;
	}
	p->x = x;
}

还是一样,需要判断坐标是否合理。

然后找到第pos个节点修改即可。

单向链表的遍历

c 复制代码
void SListTraverse(SListNode** pphead) {
	SListNode* pmove = *pphead;
	while (pmove) {
		printf("%d -> ", pmove->x);
		pmove = pmove->next;
	}
	printf("NULL\n");
}

遍历十分简单,让pmove从头开始走,直到为空。依次将pmove指向节点的数据进行打印。

如果链表为空,不会进入循环,直接打印NULL。

链表销毁

因为在对上开辟了空间,向内存申请空间,当不需要的时候,就需要马上释放。

但是这里释放过程和顺序表那不一样。因为顺序表是的指针a是指向一块连续的空间,所以使用函数free的时候,会将这一块连续的空间一起释放。

但是链表的每个节点的地址大概率不是连续的。因为每开辟一个节点,就申请一个空间。这个空间在堆上申请的,malloc函数会随机选取一个未被使用的空间。所以虽然链表也是连续的,只不过它只是在逻辑上连续了,而不是物理上连续。

所以销毁链表的时候,需要从头到尾,依次删除节点:

c 复制代码
void SListDestroy(SListNode** pphead) {
	if (SListEmpty(pphead)) {
		printf("The Single LinkedList has been Destroy!\n");
		return;
	}
	while (*pphead) {
		SListNode* pfree = *pphead;
		*pphead = (*pphead)->next;
		free(pfree);
		pfree = NULL;
	}
}

链表为空时,不需要再执行销毁操作。

链表不为空时,只要*pphead不为空,就让pfree指向此时 *pphead指向的节点,再让 *pphead往后移动一次。删除pfree指向的节点。

直到*pphead指向为NULL时候,就会退出循环。销毁完毕。

相关代码

c 复制代码
SingleLinkedList.h
c 复制代码
#define _CRT_SECURE_NO_WARNINGS
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<errno.h>
#include<stdbool.h>

typedef int SListDataType;

typedef struct SListNode {//每个节点的定义
	SListDataType x;
	struct SListNode* next;
}SListNode;

typedef struct SingleLinkedList {
	SListNode* head;//指向第一个节点的指针
	int num;//节点个数
}SList;

SListNode* SListBuyNode(SListDataType x);//创建新的节点

void SListPushBack(SListNode** pphead, SListDataType x);//链表尾插

void SListPushFront(SListNode** pphead, SListDataType x);//链表头插

void SListInsertPos(SListNode** pphead, SListDataType x,int pos);//在第几个节点后面插入 
//第0个节点后面也可以插入 头插即可

int GetSListDataQuantity(SListDataType** pphead);//链表中元素个数/节点个数

bool SListEmpty(SListDataType** pphead);//判断链表是否为空 为空返回真 反之为假

void SListDeleteBack(SListNode** pphead);//链表元素尾删

void SLitsDeleteFront(SListNode** pphead);//链表元素头删

void SListDeletePos(SListNode** pphead,int pos);//删除第pos个位置的节点

void SListTraverse(SListNode** pphead);//链表遍历

void SListDataFind(SListNode** pphead, SListDataType x);//寻找元素是否存在 并且告知在第几个节点

void SListChangeDataPos(SListNode** pphead, SListDataType x, int pos);//把第pos个节点的元素修改为x

void SListDestroy(SListNode** pphead);//销毁链表
c 复制代码
SingleLinkedList.c
c 复制代码
#include"SingleLinkedList.h"

SListNode* SListBuyNode(SListDataType x) {
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
	if (!newnode) {
		perror("malloc failed");
		exit(-1);
	}
	newnode->x = x;
	newnode->next = NULL;
	return newnode;
}

void SListPushBack(SListNode** pphead, SListDataType x) {
	if (!*pphead) *pphead = SListBuyNode(x); 
	else {
		SListNode* ptail = *pphead;
		while (ptail->next) {
			ptail = ptail->next;
		}
		ptail->next = SListBuyNode(x);
	}
}

void SListPushFront(SListNode** pphead, SListDataType x) {
	if (!*pphead) *pphead = SListBuyNode(x);
	else {
		SListNode* NewNode = SListBuyNode(x);
		SListNode* OriginHead = *pphead;
		(*pphead) = NewNode;
		NewNode->next = OriginHead;
	}
}

void SListInsertPos(SListNode** pphead, SListDataType x,int pos) {
	if (pos<0 || pos>GetSListDataQuantity(pphead)) {
		printf("The Pos %d Is Not Legal! Cannot Delete!\n",pos);
		return;
	}
	else {
		if (!pos) {
			SListPushFront(pphead, x);
		}
		else {
			pos--;
			SListNode* p = *pphead;
			while (pos--) {
				p = p->next;
			}
			//p指向的是要第pos个节点
			SListNode* pnext = p->next;
			SListNode* new = SListBuyNode(x);
			new->next = p->next;
			p->next = new;
		}
	}
}

int GetSListDataQuantity(SListDataType** pphead) {
	SListNode* pmove = *pphead;
	int count = 0;
	while (pmove) {
		pmove = pmove->next;
		count++;
	}
	return count;
}

bool SListEmpty(SListDataType** pphead) {
	if (!*pphead) return true; 
	else return false; 
}

void SListDeleteBack(SListNode** pphead) {
	if (SListEmpty(pphead)) {
		printf("Already No Element! Cannot Delete\n");
		return;
	}
	else {
		if (GetSListDataQuantity(pphead) == 1) {
			free(*pphead);
			*pphead = NULL; 
		}
		else {
			SListNode* pdelete = *pphead;
			while (pdelete->next->next) {
				pdelete = pdelete->next;
			}
			SListNode* ppdelete = pdelete->next;
			pdelete->next = NULL;
			free(ppdelete);
			ppdelete = NULL;
		}
		return;
	}
}

void SLitsDeleteFront(SListNode** pphead) {
	if (SListEmpty(pphead)) { 
		printf("Already No Element! Cannot Delete\n");
		return;
	}
	else{
		SListNode* OriginHead = *pphead;
		*pphead = OriginHead->next;
		free(OriginHead);
		OriginHead = NULL;
	}
}

void SListDeletePos(SListNode** pphead,int pos) {
	if (SListEmpty(pphead)) {
		printf("Already No Element! Cannot Delete\n");
		return;
	}
	else {
		if (pos <= 0 || pos > GetSListDataQuantity(pphead)) {
			printf("The Pos %d Is Not Legal!\n", pos);
			return;
		}
		else {
			if (pos == 1) SLitsDeleteFront(pphead);
			else {
				pos = pos - 2;
				SListNode* p = *pphead;
				while (pos--) p = p->next;
				//此时p指向的是第pos个节点的上一个
				SListNode* pnext = p->next, * ppnext = pnext->next;
				p->next = ppnext;
				free(pnext);
				pnext = NULL;
			}
		}
	}
}

void SListTraverse(SListNode** pphead) {
	SListNode* pmove = *pphead;
	while (pmove) {
		printf("%d -> ", pmove->x);
		pmove = pmove->next;
	}
	printf("NULL\n");
}

void SListDataFind(SListNode** pphead, SListDataType x) {
	if (SListEmpty(pphead)) printf("No Elements!\n");
	else {
		int flag = 0;
		SListNode* pfind = *pphead;
		int pos = 1;
		while (pfind) {
			if (pfind->x == x) {
				printf("The Data %d Is In No.%d Node!\n", x, pos);
				flag = 1;
			}
			pfind = pfind->next;
			pos++;
		}
		if (!flag) printf("The Data %d Is Not Exist!\n", x);
	}
}

void SListChangeDataPos(SListNode** pphead, SListDataType x, int pos) {
	if (pos <= 0 || pos > GetSListDataQuantity(pphead)) {
		printf("The Pos %d Is Not Legal!\n",pos);
		return;
	}
	SListNode* p = *pphead;
	pos--;
	while (pos--) {
		p = p->next;
	}
	p->x = x;
}

void SListDestroy(SListNode** pphead) {
	if (SListEmpty(pphead)) {
		printf("The Single LinkedList has been Destroy!\n");
		return;
	}
	while (*pphead) {
		SListNode* pfree = *pphead;
		*pphead = (*pphead)->next;
		free(pfree);
		pfree = NULL;
	}
}

test.c内的实现看个人,在这不进行展示。

想要代码的也可以去作者的gitee账户中获取:gitee账户

相关推荐
就很对6 分钟前
7种数据结构
数据结构·windows
f狐0狸x27 分钟前
【蓝桥杯每日一题】3.17
c语言·c++·算法·蓝桥杯·二进制枚举
爱康代码1 小时前
【c语言数组精选代码题】
c语言·开发语言·数据结构
Elnaij2 小时前
从C语言开始的C++编程生活(1)
c语言·c++
执沐3 小时前
C程序设计(第五版)及其参考解答,附pdf
c语言·开发语言
共享家95273 小时前
顺序表的C语言实现与解析
数据结构·算法
打不了嗝 ᥬ᭄4 小时前
平衡树的模拟实现
数据结构·c++
泽02024 小时前
数据结构之双向链表
数据结构
ChoSeitaku4 小时前
NO.42十六届蓝桥杯备战|数据结构|算法|时间复杂度|空间复杂度|STL(C++)
数据结构·算法·蓝桥杯