数据结构之无头单向不循环链表

目录

前言

一、无头单向不循环链表的结构

二、各接口函数的实现

(1)插入函数

①头部插入函数(SListPushFront)

②尾部插入函数(SListPushBack)

 ③任意位置后面插入(SListInsertAfter)

(2)删除函数

①头部删除函数(SLitsPopFront)

 ②、尾部删除函数(SListPopBack)

③、任意位置后删除(SListEraseAfter)

(3)打印函数、查找函数、销毁函数及新结点的申请函数

①打印函数(SListPrint)

②查找函数(SListFind)

③购买新结点(BuySListNode)

④销毁函数(SListDestroy)

小结


前言

链表是数据结构中不可或缺的一部分,其指的是物理存储单元上非连续、非顺序的存储结构,其链接关系通过指针来实现,形状如链子,故称为链表,这种数据结构在很多地方都有运用。

链表根据其特性有是否带头、是否循环、单向双向三种,分为8种类型,本文介绍的链表为无头单向不循环链表,是链表类型中结构最为简单的。但结构简单并不代表实现起来也是简单的,无头单向不循环链表这种结构常出现在OJ题中,或作为更复杂结构的子结构,如哈希桶等。

下面介绍其结构和各接口函数的实现思路及代码。

一、无头单向不循环链表的结构

链表的基本结构如下图:

分析一个数据结构,一般我们都是通过研究其单一单元来进行分析,如上图所示,链表的单一单元包含了两大部分,分别为存储有效数据的数据域和存储下一地址的指针域,单一单元如下:

理解了链表的基本结构,下面我们来了解一下链表的三大特性:是否带头、是否循环、单向双向。

①是否带头问题:

如图,带哨兵位的头结点,其数据域不存储有效数据;不带哨兵位的头结点,其本质就是一个指针,关于是否带头问题,此处不展开介绍。

②是否循环问题:

若一个链表是循环链表,其链表的tail将指向自身的head,可以无限循环下去;

若一个链表不是循环链表,则其tail将指向NULL。

③单向双向问题:

单向链表:只有一个指针域,指向下一个数据的地址;

双向链表:有两个指针域,一个指针指向上一个数据的地址,另一个指针指向下一个数据的地址。

 介绍完三大特性之后,我们来进入本文正题:无头单向不循环链表的结构设计!

代码如下:

typedef struct SListNode//链表单个个体结构
{
	SLTDateType data;//数据域
	struct SListNode* next;//指针域
}SLTNode;

二、各接口函数的实现

研究数据结构时一般需要实现初始化Init函数、销毁Destroy函数、插入数据函数(头部插入、尾部插入、任意位置插入)、删除数据函数(头部删除、尾部删除、任意位置删除)、查找函数、打印函数等等。下面对各函数的实现及代码进行简单介绍。

(1)插入函数

①头部插入函数(SListPushFront)

实现思路:

 代码实现:

void SListPushFront(SLTNode** pphead, SLTDateType x)
{
	SLTNode* newnode = BuySListNode(x);//新结点
	newnode->next = *pphead;
	*pphead = newnode;
}

②尾部插入函数(SListPushBack)

实现思路:尾部插入需分情况讨论,当链表没有结点时,将新结点直接当成头结点插入即可;当链表有一个及以上结点时,则需要找到链表的尾结点,将新结点插入到尾结点后面成为新的尾。

 代码实现:

void SListPushBack(SLTNode** pphead, SLTDateType x)
{
	SLTNode* newnode = BuySListNode(x);//获得新结点
	if (*pphead == NULL)//链表没有结点时,将新结点当成头结点
	{
		*pphead = newnode;
	}
	else//链表有一个及以上结点时,找到尾结点,在尾结点后插入新结点
	{
		//找尾
		SLTNode* tail = *pphead;
		while (tail->next != NULL)//遍历找尾
		{
			tail = tail->next;
		}
        //插入结点
		tail->next = newnode;
	}
}

 ③任意位置后面插入(SListInsertAfter)

实现思路:该函数根据提供的参数,在pos指针后插入数据,实现较简单,只需注意链接的先后顺序即可。

 代码实现:

void SListInsertAfter(SLTNode* pos, SLTDateType x)
{
	assert(pos);
	SLTNode* newnode = BuySListNode(x);//新结点
	newnode->next = pos->next;//新结点的next指向pos的next
	pos->next = newnode;//pos的next指向新结点
	
}

(2)删除函数

①头部删除函数(SLitsPopFront)

实现思路:

如插入函数需要申请新结点一般,删除函数都需要考虑的是是否有结点可以删除,所以删除函数分两种情况,第一种情况是有结点删除,第二种情况是没有结点删除。

代码实现:

void SListPopFront(SLTNode** pphead)
{
	assert(*pphead != NULL);//断言;没有结点删除直接退出
	SLTNode* next = (*pphead)->next;//保存头结点的下一个结点地址,防止释放头结点后无法找到
	free(*pphead);//释放头结点
	*pphead = next;//将下一个结点设置为头结点
}

 ②、尾部删除函数(SListPopBack)

实现思路:同头部删除类似,也需分情况解决,但尾部删除不仅需要考虑没有结点的情况,也需要考虑只有一个结点的情况和两个及两个以上结点的情况。

代码实现:

void SListPopBack(SLTNode** pphead)
{
	assert(*pphead != NULL);//断言;没有结点删除直接退出
	SLTNode* tail = *pphead;//用于找到尾
	if ((*pphead)->next == NULL)//只有一个结点
	{
		free(*pphead);
		*pphead = NULL;
	}
	else//多个结点
	{
		while (tail->next->next != NULL)//找尾的前一个结点
		{
			tail = tail->next;
		}
		free(tail->next);//释放尾结点
		tail->next = NULL;//让尾结点的前一个结点指向空,成为新的尾
	}
}

③、任意位置后删除(SListEraseAfter)

实现思路:在指定地址后面删除结点,改变链接关系即可。

代码实现:

void SListEraseAfter(SLTNode* pos)
{
	SLTNode* next = pos->next;//保存将要删除的结点,避免修改pos的指向后无法找到此结点
	pos->next = next->next;//让pos指向将要删除的结点的下一个结点
	free(next);//释放结点
	next = NULL;
}

(3)打印函数、查找函数、销毁函数及新结点的申请函数

①打印函数(SListPrint)

由于实现思路简单,只需控制指针遍历链表,打印数据域的数据即可,直接上代码。

void SListPrint(SLTNode* phead)
{
	SLTNode* cur = phead;//一般不改变phead,所以用cur来控制遍历
	while (cur)//cur为空时循环结束
	{
		printf("%d->", cur->data);//打印
		cur = cur->next;//迭代
	}
	printf("NULL\n");
}

②查找函数(SListFind)

与打印函数类似,整体思路都是遍历链表,若找到数据与需查找的数据一致,则返回该结点的地址,否则返回NULL。

SLTNode* SListFind(SLTNode* phead, SLTDateType x)
{
	SLTNode* cur = phead;//控制遍历
	while (cur)
	{
		if (cur->data == x)//判断是否是查找的数据
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}
	return NULL;
}

③购买新结点(BuySListNode)

SLTNode* BuySListNode(SLTDateType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//申请结点
	if (newnode == NULL)//判断申请是否成功
	{
		printf("malloc fail\n");
		exit(-1);
	}
	newnode->data = x;//设置数据
	newnode->next = NULL;
	return newnode;
}

④销毁函数(SListDestroy)

利用双指针迭代遍历,从头到尾释放每一个结点。

void SListDestroy(SLTNode** pphead)
{
	SLTNode* next = (*pphead)->next;//保存下一个结点
	while (next)
	{
		free(*pphead);//释放头结点
		*pphead = next;//保存的结点成为新的头
		next = next->next;//保存下一个结点
	}
	free(*pphead);//最后释放指针
	*pphead = NULL;
}

小结

本文介绍的链表结构虽然存在许多缺陷,如无法随机访问,已知一个结点无法快速得到其上一个结点的地址等等,这些都是无头单向不循环链表不实用的原因,但是其却是许多面试题中常考察的结构,其还是大部分高级结构的子结构,所以了解并熟悉它十分重要。

本次数据结构的介绍到此就告一段落了,希望能对大家有所帮助。

附整体代码。

SList.h

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

typedef int SLTDateType;//链表存储数据的类型

typedef struct SListNode//链表单个个体结构
{
	SLTDateType data;//数据域
	struct SListNode* next;//指针域
}SLTNode;

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

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

// 尾插 头插 尾删 头删
void SListPushBack(SLTNode** pphead,SLTDateType x);
void SListPushFront(SLTNode** pphead, SLTDateType x);
void SListPopBack(SLTNode** pphead);
void SListPopFront(SLTNode** pphead);

//查找
SLTNode* SListFind(SLTNode* phead,SLTDateType x);

//在pos位置之后插入
void SListInsertAfter(SLTNode* pos, SLTDateType x);

//在pos位置之后删除
void SListEraseAfter(SLTNode* pos);

//购买新结点
SLTNode* BuySListNode(SLTDateType x);

SList.c

#include"SList.h"

SLTNode* BuySListNode(SLTDateType 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;//一般不改变phead,所以用cur来控制遍历
	while (cur)//cur为空时循环结束
	{
		printf("%d->", cur->data);//打印
		cur = cur->next;//迭代
	}
	printf("NULL\n");
}

void SListPushBack(SLTNode** pphead, SLTDateType x)
{
	SLTNode* newnode = BuySListNode(x);//获得新结点
	if (*pphead == NULL)//链表没有结点时,将新结点当成头结点
	{
		*pphead = newnode;
	}
	else//链表有一个及以上结点时,找到尾结点,在尾结点后插入新结点
	{
		//找尾
		SLTNode* tail = *pphead;
		while (tail->next != NULL)//遍历找尾
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

void SListPushFront(SLTNode** pphead, SLTDateType x)
{
	SLTNode* newnode = BuySListNode(x);//新结点
	newnode->next = *pphead;
	*pphead = newnode;
}

void SListPopBack(SLTNode** pphead)
{
	assert(*pphead != NULL);//断言;没有结点删除直接退出
	SLTNode* tail = *pphead;//用于找到尾
	if ((*pphead)->next == NULL)//只有一个结点
	{
		free(*pphead);
		*pphead = NULL;
	}
	else//多个结点
	{
		while (tail->next->next != NULL)//找尾的前一个结点
		{
			tail = tail->next;
		}
		free(tail->next);//释放尾结点
		tail->next = NULL;//让尾结点的前一个结点指向空,成为新的尾
	}
}

void SListPopFront(SLTNode** pphead)
{
	assert(*pphead != NULL);//断言;没有结点删除直接退出
	SLTNode* next = (*pphead)->next;//保存头结点的下一个结点地址,防止释放头结点后无法找到
	free(*pphead);//释放头结点
	*pphead = next;//将下一个结点设置为头结点
}

SLTNode* SListFind(SLTNode* phead, SLTDateType x)
{
	SLTNode* cur = phead;//控制遍历
	while (cur)
	{
		if (cur->data == x)//判断是否是查找的数据
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}
	return NULL;
}

void SListInsertAfter(SLTNode* pos, SLTDateType x)
{
	assert(pos);
	SLTNode* newnode = BuySListNode(x);//新结点
	newnode->next = pos->next;//新结点的next指向pos的next
	pos->next = newnode;//pos的next指向新结点
	
}

void SListEraseAfter(SLTNode* pos)
{
	SLTNode* next = pos->next;//保存将要删除的结点,避免修改pos的指向后无法找到此结点
	pos->next = next->next;//让pos指向将要删除的结点的下一个结点
	free(next);//释放结点
	next = NULL;
}

void SListDestroy(SLTNode** pphead)
{
	SLTNode* next = (*pphead)->next;//保存下一个结点
	while (next)
	{
		free(*pphead);//释放头结点
		*pphead = next;//保存的结点成为新的头
		next = next->next;//保存下一个结点
	}
	free(*pphead);//最后释放指针
	*pphead = NULL;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值