【线性表】顺序表详解(C语言版)

目录

引言

 一、定义

1.静态顺序表

2.动态顺序表

二、初始化

三、销毁

四、插入

1.容量判断

2.头插

 3.尾插

4.中间插入

(1)函数实现

(2)用中间插入实现头插

(3)用中间插入实现尾插

五、删除

1.头删

2.尾删

3.中间删除

(1)函数实现

(2)用中间删除实现头删

(3)用中间删除实现尾删

五、查找

六、修改

结尾


ID:HL_5461

引言

在开始动手写之前,我们先来了解顺序表是什么:

顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构。我们用一张图来具体表示一下,大概是介个样子


 一、定义

在了解了顺序表是什么之后,我们可以开始顺序表的定义了。

1.静态顺序表

正如前面强调的,顺序表的物理地址是连续的,对于一个连续的空间,我们最容易想到的便是数组,而对于一个数组,显然的,我们不太可能用完它全部的空间,这时候,我们需要一个数字size用来表示有效数字个数。

于是我们将数组arr和有效数据个数size封装成一个线性表结构体

图示

typedef int SLDataType;//定义SLDataType为int,方便以后修改存储的数据类型
#define M 10//将M定义为10,方便以后修改定长数组长度

typedef struct SeqList
{
	SLDataType arr[M];//定长数组
	int size;//有效数据个数
}SL;

代码

2.动态顺序表

静态顺序表虽然简明易懂,但平心而论并不是一个好的选择:M过大会造成空间的浪费,M过小又会有数据放不下。所以便有了动态顺序表。

不同于静态顺序表的结构体直接封装一个定长数组,对于动态顺序表,我们需要一个需要存储的数据类型的指针a来指向动态开辟的内存空间,还需要一个数字capicity来表示当前开辟的空间大小,同样的,我们不太可能用完它全部的空间,所以需要一个数字size用来表示有效数字个数。

封装的结构体如下:

图示 

typedef int SLDataType;//定义SLDataType为int,方便以后修改存储的数据类型

typedef struct SeqList
{
	SLDataType* a;//动态开辟的空间指针
	int size;//有效数据个数
	int capicity;//空间大小
}SL;

代码


(接下来的所有操作都基于动态顺序表完成)

二、初始化

首先有必要提一个初学者很容易犯的错误:

//这是一个错误示范
void SLInit(SL s)
{
	s.a = NULL;
	s.size = 0;
	s.capicity = 0;
}

这其实就是实参与形参的问题,这个函数里面,我们需要改变的是创建的结构体本身,所以这里是万万不能传结构体的,我们需要传的是已经创建好的结构体的地址。

正确的代码如下:

void SLInit(SL* ps)//s是一个已经创建的动态顺序表
{
	ps->a = NULL;//指向空间指针置空
	ps->size = 0;//有效数据个数为0
	ps->capicity = 0;//空间大小为0
}

当然,我们不妨对它再改进改进,在初始化时就分配一定量的空间:

void SLInit(SL* ps)
{
	ps->a = (SLDataType*)malloc(sizeof(SLDataType)*4);//开辟4个每个大小为SLDataType大小的空间

	if (ps->a == NULL)//判断空间开辟是否成功
	{
		perror("SLInit");//不成功打印初始化错误
		exit(-1);//程序以非正常形式结束
	}

	ps->size = 0;//有效数据个数为0
	ps->capicity = 4;//空间大小为4
}

三、销毁

个人感觉销毁算是最简单的操作了,so,我们直接上代码:

void SLDestroy(SL* ps)
{
	free(ps->a);//释放指针a所指向的空间
	ps->a = NULL;//将a置为空指针,防止其变为野指针
	ps->size = 0;//有效数据个数置0
	ps->capicity = 0;//空间大小置0
}

这里主要注意不要出现前面示范的实参形参的问题就可以啦~


四、插入

我们实现三种插入:头插、尾插和中间插入。实现完中间插入,我会再将头插和尾插用中间插入实现一遍。

插入之前我们要先判断线性表的容量是否已满,即size是否等于capacity,未满则可以直接插入,反之需要扩容。所以我们先写一个判断空满的函数,而后再实现插入。

1.容量判断

进行容量判断我们直接比较size和capacity的值就好,相等则已满,此时需要用realloc函数进行扩容。realloc函数的具体功能及用法大家可以到cplusplus上面查询,就不在此赘述。但有一点特别需要注意,在运用realloc函数时,我们最好创建一个临时的SL类型的指针变量用于接收realloc函数返回的地址,这是为了防止realloc函数开辟空间失败返回空指针导致原地址丢失的情况。同时,这里因为需要替换原来的地址,所以我们需要传顺序表的地址给函数。

话不多说,直接上代码:

void SLCheckCapacity(SL* ps)
{
	if (ps->size == ps->capicity)//判断容量是否已满
	{
		SLDataType* tmp = (SLDataType*)realloc(ps->a, ps->capicity * sizeof(SLDataType) * 2);
        //将容量扩大为原来的二倍

		if (tmp == NULL)
		{
			perror("CapacityIncrease");//不成功返回增容失败的错误信息
			exit(-1);//程序以非正常形式结束
		}
		ps->a = tmp;//令指针a指向新地址
		tmp = NULL;
        //临时变量置空(这里置不置空其实问题不大,因为tmp出了函数就被销毁了,but,养成好习惯)
		ps->capicity *= sizeof(SLDataType) * 2;//capacity修改为新的容量数
	}

}

2.头插

对于头插,我们采用挨个元素向后覆盖直到把下标为0的位置覆盖到下标为1的位置,最后用想插入的x覆盖下标为0的元素的方法。

这样说不太清楚,我用图片加以说明。

首先定义一个整型变量 end 让它等于 size 。因为 size 代表元素个数,而下标是从 0 开始的,所以此时 end 比最后一个元素的下标数大1,也就说 a[end] 指向数组的第一个空位,此时我们让元素依次往后覆盖,也就说 a[end] = a[end - 1] ,同时end--。将它们放入一个循环中。

 

如图,这是将 a[1] 元素覆盖到 a[2] 元素,并将 end-- 后的样子。注意,我们这里只是做了一个简单的覆盖,并没有进行“删除”或者“移动”操作,所以 a[1] 中的元素不变也不为空,仍然等于原先的1,直到 a[0] 元素将它覆盖。

这是 a[0] 元素覆盖到 a[1] 的样子,此时循环可以终止了,由此我们可以发现,循环终止的条件就是 end == 0 。

最后,我们再在 a[0] 位置覆盖上我们要插入的x。

以上都是数组中有元素的情况,我们再补充一个数组中没有元素的情况。

数组中没有元素,此时 end = size = 0,并没有进循环,直接执行 a[0] = x。

讲解完了我们开始上代码~

void SLPushFront(SL* ps, SLDataType x)//x为要插入元素
{
	SLCheckCapacity(ps);//检查容量,已满扩容

	int end = ps->size;
	while (end)//判断a[end]是否到头
	{
		ps->a[end] = ps->a[end - 1];//没到头则将后一个覆盖到前面
		end--;//同时a[end]往前挪
	}
	ps->a[0] = x;//令x覆盖a[0]
	ps->size++;//有效数据个数加1
}

 3.尾插

相较于头插,尾插方便的多,因为不用移动元素,先调用一下之前写的判断容量的函数,然后 a[size] = x ,size++ 就OK啦~头插你都抗下来了,相信尾插就是个小case,so,也不用图解了,咱直接上代码~

void SLPushBack(SL* ps, SLDataType x)//x为要插入元素
{
	SLCheckCapacity(ps);//检查容量,已满扩容
	ps->a[ps->size] = x;//将x插入到最后
	ps->size++;//有效数据个数加1
}

4.中间插入

(1)函数实现

中间插入其实和头插很相像,也是挨个元素向后覆盖直到把下标为 pos 的位置覆盖到下标为 pos+1 的位置,其中 pos 是要插入位置的下标(这里要特别注意,有些中间插入的pos值是指插入位置处在第几个元素,个人认为下标使用起来更方便,所以这里都是代表下标)。方法很简单,但是有一点需要注意:pos有范围,必须在[0,size]之间,可以取0,0即为头插,可以取size,size即为尾插。

我们照例使用图解。

 首先定义一个整型变量 end 让它等于 size ,我们让元素依次往后覆盖,也就说 a[end] = a[end - 1] ,同时end--。将它们放入一个循环中。

 这是将 a[pos] 元素覆盖到 a[pos+1] 元素,并将 end-- 后的样子,此时循环可以终止了,由此我们可以发现,循环终止的条件就是 end - pos == 0 。

 最后,我们再在 a[pos] 位置覆盖上我们要插入的x。

来看看代码~

void SLInsert(SL* ps, int pos, SLDataType x)
{
	assert(pos >= 0 && pos <= ps->size);//断言pos范围,确保插入在有效位置
	SLCheckCapacity(ps);//检查容量,已满扩容
	int end = ps->size;
	while (end - pos)//判断a[end]是否到达pos位置
	{
		ps->a[end] = ps->a[end - 1];//没到头则将后一个覆盖到前面
		end--;//同时a[end]往前挪
	}
	ps->a[pos] = x;//令x覆盖a[pos]
	ps->size++;//有效数据个数加1
}

(2)用中间插入实现头插

这个没啥可讲的,直接调用写好的SLInsert函数,并将插入位置设成0即可。

void SLPushFront(SL* ps, SLDataType x)//x为要插入元素
{
	SLInsert(ps, 0, x);
}

(3)用中间插入实现尾插

尾插将pos值设为size就好,不再赘述。

void SLPushBack(SL* ps, SLDataType x)//x为要插入元素
{
	SLInsert(ps, ps->size, x);
}

五、删除

1.头删

在头删之前,我们要先判断是否有元素可供我们删除,即size是否大于0。头删的思路是,将后一个不断往前覆盖,如a[0]被a[1]覆盖,a[1]被a[2]覆盖,直到最后一个元素覆盖到前一个,然后size--删去最后一个元素,由于前面的元素都是向前覆盖的,size--也就相当于删去了第一个元素。

来看图解:

定义整型begin为0,将a[begin+1]的值赋给a[begin]然后begin++。将它们放在一个循环里面。

这是完成一次以上操作后的样子。

 以上是重复循环直到最后一个元素的样子,此时循环可以终止了。不难发现循环终止的条件是begin+1==size。

最后我们将size--。此时a[8]元素虽然仍然存在,但由于size比原来小1,我们读不到a[8]元素,所以认为它已被删除。

OK,接下来是代码: 

void SLPopFront(SL* ps)
{
	assert(ps->size > 0);//必须有元素可删
	int begin = 0;
	while (begin + 1 < ps->size)//判断a[begin]是否指向最后一个元素
	{
		ps->a[begin] = ps->a[begin + 1];//不是最后一个则将后一个往前一个覆盖
		begin++;//同时a[begin]向后挪
	}
	ps->size--;//有效数字个数减1
}

2.尾删

尾删和尾插一样简单,只需要size--,由于size比原来小1,我们读不到最后一个元素,所以认为它已被删除。但注意,别忘了首先判断size是否大于0。

void SLPopBack(SL* ps)
{
	assert(ps->size > 0);//必须有元素可删
	ps->size--;//删去最后一个元素
}

3.中间删除

(1)函数实现

依旧类似于头删,定义整型begin等于pos,挨个元素向前覆盖直到最后一个元素。和中间插入一样,这里的pos也有范围,是[0,size)注意这里不同意中间插入pos在这里取不到size,因为a[size]是最后一个元素后面的空位,并无元素可删。另外要记得判断size是否等于0,即是否有元素可删。

先过一遍图解再实现代码:

令begin=pos,将begin+1的元素覆盖到begin上,同时begin++,将它们放在一个循环中。

这是完成一次循环后的样子。

这是将最后一个元素覆盖到前一个元素上同时begin++后的样子,此时循环结束,循环结束的条件和头删一样,是begin+1==size 。而后size--删去最后一个元素。

代码如下:

void SLErase(SL* ps, int pos)
{
	assert(ps->size > 0);//必须有元素可删
	assert(pos >= 0 && pos < ps->size);//pos必须在有效下标内
	int begin = pos;
	while (begin + 1 < ps->size)//判断a[begin]是否指向最后一个元素
	{
		ps->a[begin] = ps->a[begin + 1];//不是最后一个则将后一个往前一个覆盖
		begin++;//同时a[begin]向后挪
	}
	ps->size--;//有效数字个数减1
}

(2)用中间删除实现头删

头删直接引用SLErase函数并令pos为0即可。

void SLPopFront(SL* ps)
{
	SLErase(ps, 0);
}

(3)用中间删除实现尾删

引用SLErase函数并令pos=size-1即可。注意这里要减1,因为size比最后一个元素的下标大1。

void SLPopBack(SL* ps)
{
	SLErase(ps, ps->size - 1);
}

五、查找

我们通过遍历的方式挨个查看元素是否等于要找的元素x,是的话返回元素下标,遍历结束没有找到返回-1(也可以返回其它值,但这个值不可以是下标)。循环结束的条件是遍历完或者找到了。

int SLFind(SL* ps, SLDataType x)
{
	for (int i = 0; i < ps->size; i++)
	{
		if (ps->a[i] == x)//a[i]==x说明找到了
		{
			return i;//返回元素所在下标
		}
	}
	return -1;//遍历结束仍没返回任何元素下标证明没有这个元素,返回-1
}

六、修改

输入下标pos直接将a[pos]的值修改为想改成的x就可以了。唯一要注意的是pos是一个[0,size)的值原因参考中间插入。

void SLModify(SL* ps, int pos, SLDataType x)
{
	assert(pos >= 0 && pos < ps->size);//pos必须在有效下标内
	ps->a[pos] = x;//修改a[pos]为x
}

当然我们不妨与SLFind函数相结合,将一个确定值的元素修改为我们想要的值。

void SLModify2(SL* ps, SLDataType x, SLDataType to)
{
	int pos = SLFind(ps, x);//查找值为x的元素并返回下标存放在pos
	if (pos != -1)//如果元素存在
	{
		SLModify(ps, pos, to);//将该元素修改为to
	}
	else//元素不存在
	{
		printf("没有%d这个元素", x);
	}
}

结尾

当然各位不妨根据自己的喜好加一些菜单什么的,在此就不多说啦~由于这篇文章已经写的够长了,所以整体的代码我放在我的码云里了,欢迎大家前来串门:

class_c: 课上要认真鸭~ - Gitee.com

当然我也会再开一篇文章,那里面是完整的代码没有讲解,方便大家取用(比如我学数据结构时曾迫切需要一份完整的不需要修改的代码来应付我的作业……),这里是链接:

【线性表】顺序表实现(C语言仅代码版)_是兰兰呀~的博客-CSDN博客

码云那份含有我这篇文章所有的的代码但稍显混乱,文章里的我删了一部分并添加了菜单,大家自己取舍。

最后的最后,若有错误,欢迎大家批评斧正!

  • 13
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是兰兰呀~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值