数据结构——顺序表【2】

⭐顺序表功能

💎顺序表尾插

顾名思义,就是在顺序表的最后一个位置插入和存储数据,根据顺序表只能连续存储数据的性质,只能通过size指引的下标位置,依次一个一个由低位下标到高位下标存放数据而不发生跳位或错位的过程。

//函数声明
void SLPushBack(SL* seq, SeqLElemType x);					//尾插

//函数定义
void SLPushBack(SL* seq, SeqLElemType x)		
{
	assert(seq);
	SLCapacityCheck(seq);
	seq->arr[seq->size] = x;
	seq->size++;
}

//尾插数据
SLPushBack(sq, 1);
SLPushBack(sq, 2);
SLPushBack(sq, 3);
SLPushBack(sq, 4);

//打印
SLPrint(sq);

说明:

  1. 任何数据在插入顺序表前,都要进行顺序表扩容,防止顺序表为空或满时存不下数据的情况。由此在断言判断结构体指针不为空且顺序表容量可用情况下再进行数据的尾部插入,插入的原理如下图所展示:

    在这里插入图片描述
    🍕图中可以看出,数据每次插入的位置正好在size所对应的下标位置上,待插入完毕后,size自增,即表明了顺序表中有效数据增加了一个。

  2. 根据上述代码,依次尾插了4个数据,结果打印如下:

   当前顺序表结构:1 2 3 4
  1. 可以看出数据已经成功尾插到顺序表,并通过Print这个遍历和打印函数将顺序表中的每个数据打印了出来。
  2. 值得注意的是🥵,此时因为顺序表扩容时仅扩容了4个int空间,而此时已经将4个空间,也就是4*4 == 16个字节的空间全部占满了,此时如上图,size由于自增所指向的数组下标为4,如果后续插入数据时不进行扩容,则会造成顺序表存储的越界,引发错误,所以每次插入前的扩容检查是非常必要的。
💎顺序表尾删

与尾插相对应的是尾删功能,也就是逆序上面的步骤,将存在size约束的下标位置前,最后一个个数据依次移出顺序表。

//函数声明
void SLPopBack(SL* seq);							//尾删

//函数定义
void SLPopBack(SL* seq)							
{
	assert(seq);
	if (!SLEmpty(seq))
	{
		seq->size--;
	}
}

//尾删数据
SLPopBack(sq);
SLPopBack(sq);

//打印
SLPrint(sq);

说明:

  1. 此处调用了判空函数,若顺序表为空,则返回一个true,但是通过取反操作符将true转换为false,就不会进入到即使顺序表为空仍然使有效数据个数size自减的逻辑了。

  2. 相反,如果顺序表不为空,表示其中有数据,此时可以通过让size的自减达到“移除”元素的作用,因为虽然此种方式不会真的将数据移出顺序表中,但是顺序表之后的增删查改都不会访问到size位置上的数据,相当于被“移除”了:

    在这里插入图片描述
    🍕图中看起来数据被移出顺序表了,但真实情况是通过size的减少使计算机和我们能访问到的数据减少了,而真正的数值仍然留在里面。

    调试观察也可发现这样的逻辑关系:>在这里插入图片描述

🍕逐过程调试观察可发现
在这里插入图片描述

🍕最终运行可以发现
在这里插入图片描述

✨即使内存中对应下标的值仍然存在,在我们观察和计算机访问时再也不能访问到被尾删的几个数据了,如果此时再次尾删将会失败,因为会被判空逻辑所阻碍,阻止计算机继续将size减少到-1。

  1. 其实删除的逻辑就是视而不见,或者覆盖,如果我们看不到,计算机也看不到,就相当于被删除了,数字像凭空消失了一般,当后续再次进行尾插时,新插入的数据将会覆盖旧的在相同下标位置上的数据,这样尾删和尾插的逻辑就顺其自然了。
💎顺序表头插

头插就是在一个顺序表下标为0的位置插入一个数据,若这个顺序表其他值都不存在,头插也就相当于尾插的效果。但是当一个顺序表中存在其他值时,这时候要小心了,因为头部数据的插入可能会覆盖原有的数据,使顺序表失效。

//函数声明
void SLPushFront(SL* seq, SeqLElemType x);					//头插

//函数定义
void SLPushFront(SL* seq, SeqLElemType x)				
{
	assert(seq);
	SLCapacityCheck(seq);
	if (SLEmpty(seq))
	{
		SLPushBack(seq, x);
		return;
	}
	int Tail = seq->size - 1;
	while (Tail >= 0)
	{
		seq->arr[Tail + 1] = seq->arr[Tail];
		Tail--;
	}
	seq->arr[0] = x;
	seq->size++;
}

//数据插入
SLPushFront(sq, 4);
SLPushFront(sq, 3);
SLPushFront(sq, 2);
SLPushFront(sq, 1);
SLPushFront(sq, 0);

//打印
SLPrint(sq);

说明:

  1. 头插需要传入两个参数,一个结构体指针,一个待插入类型的值。进行基本的断言防空指针和扩容检查后,需要进行多个判断:

    ✨如果顺序表为空,则此时可调用尾插函数或头插函数,即只需将数值插入首位置即可,无需在意顺序。

    ✨如果顺序表不为空,为了避免将原有的头部数据覆盖,需要将数据进行整体后挪,原理如下图:

    在这里插入图片描述

    ✨由此可知,整体的挪动是非常必要的,如果数据不后挪,当原本存在数据时将会覆盖第一个数据。

  2. 为了实现这样的功能,观察代码,我们定义了一个整型Tail,用于充当指针的作用来帮助元素的挪动提供下标参考,初始化其值为size的前一个下标位置,如本例中size == 3,表示顺序表中有3个有效数据,则Tail指向2号下标,即数值3所在位置。

  3. 当Tail指向该值时,将该值赋值到Tail + 1的位置上,即完成了元素的赋值拷贝,也就相当于了数值在表中的移动,一直循环此步骤直至Tail指向首元素时,进行最后一次挪动,将首元素0所在位置挪到下标为1的位置上,此时虽然0号位置仍存有原有数据,但将进行新值存入覆盖,即完成了头插。

  4. 值得注意的是🥵, 头插在插入过程中也要考虑扩容问题,因为每次头插前我们加入了SLCapacityCheck函数,所以不必担心一次性多次调用头插函数而存在顺序表越界问题。

最终头插打印结果
在这里插入图片描述

💎顺序表头删

头删原理与头插有很多相似之处,最大的共同点在于若原顺序表存在元素,则对所有元素都要进行整体挪动,以覆盖首元素的目的达到删除的效果。

//函数声明
void SLPopFront(SL* seq);						//头删

//函数定义
void SLPopFront(SL* seq)						//头删
{
	assert(seq);
	if (SLEmpty(seq))
	{
		return;
	}
	else
	{
		int move = 1;
		while (move < seq->size)
		{
			seq->arr[move - 1] = seq->arr[move];
			move++;
		}
		seq->size--;
	}
}

//头删数据
SLPopFront(sq);
SLPopFront(sq);

//打印
SLPrint(sq);

说明:

  1. 仍以刚才的例子,对原先头插入的顺序表0 1 2 3 4进行头删,则需要将数据进行整体左挪,以覆盖原有数据并使有效数据个数size减少,原理如下:

    在这里插入图片描述
    本例中,定义的move下标初始为1,用下标1的元素对0号下标的元素进行覆盖赋值,即等同于进行了删除操作。之后move每移动一次,将move所在下标位置值覆盖到move - 1的位置上,直到move与size值相同时停止覆盖,并将size自减。

  2. 除了一般情况,还要注意顺序表为空时的处理,观察代码可知,当使用判空函数,返回顺序表为空的true时,进入内部直接跳出函数,即不执行后续的挪动覆盖操作,可使顺序表为空时不执行头删功能。

  3. 观察头删结果
    在这里插入图片描述

💎顺序表中间插入

顺序表的中间插入和头插一样,是一个最大程度上可以区分顺序表和普通数组之间功能和性质的函数,中间插入对于顺序表而言,功能是非常全面的,它不仅可以作为对非空数据表的中间某个下标位置任意插入,还能完美还原和实现头插和尾插的功能。

//函数声明
void SLInsert(SL* seq, int pos, SeqLElemType x);	//中间插入

//函数定义
void SLInsert(SL* seq, int pos, SeqLElemType x)		//中间插入
{
	assert(seq);
	SLCapacityCheck(seq);
	if (SLEmpty(seq) || pos > seq->size || pos < 0)
	{
		if (pos != 0 || pos > seq->size || pos < 0)
		{
			printf("插入位置失败\n");
		}
		else
		{
			SLPushFront(seq, x);
		}
	}
	else if (pos == seq->size)
	{
		SLPushBack(seq, x);
	}
	else
	{
		int Tail = seq->size - 1;
		while (Tail >= pos)
		{
			seq->arr[Tail + 1] = seq->arr[Tail];
			Tail--;
		}
		seq->arr[pos] = x;
		seq->size++;
	}
}

//中间插入数据
SLInsert(sq, 8, 5);
SLInsert(sq, 0, -1);
SLInsert(sq, 3, 8);
SLInsert(sq, -1, 10);
SLPrint(sq);

//打印
SLPrint(sq);

说明:

  1. 中间插入因为功能强大,使用范围广,所以在定义时有很多点需要注意:

    🍕顺序表为空时

    🍕顺序表不为空,但插入位置非法时

    🍕顺序表不为空,插入末位置或首位置时

  2. 先看第一条,若所插入的顺序表是空表,则此时插入的位置必须为下标0所在位置,即输入的pos也必须是0。插入的方式同头插空表时一致,无论头插或尾插的实现方式都可以,如果空表输入了其他位置值,可以打印提示插入失败。

  3. 其次,如果一个顺序表不为空,但是中间插入的位置在有效数据个数范围外时,此时也需要通过pos > size或pos < 0判断插入的位置不合法,从而输出插入失败。

  4. 如果插入的位置为首部或尾部,则可直接调用头插或尾插函数来实现,在中间插入的原理解释为:

    在这里插入图片描述
    整体移动原理与头插类似,不同的地方在于pos可以放置在有效范围内的任意位置,甚至可以替代头插和尾插函数使用,且都是使用了Tail来标识当前需要挪动的数据和目的地下标。

  5. 如果仍以上例作为有元素顺序表,使用该例数据进行中间插入得到如下结果:
    在这里插入图片描述

    可以看出,与上述原理图所得结果相同,当插入位置pos下标超出有效范围时,提示插入失败。而当在合法位置插入,图中蓝色框即代表插入成功。

💎顺序表中间删除

大体原理与头删尾删类似,多引入了Tail下标,需要整体左移数据,代码如下:

//函数声明
void SLErase(SL* seq, int pos);						//中间删除

//函数定义
void SLErase(SL* seq, int pos)						//中间删除
{
	assert(seq);
	if (SLEmpty(seq))
	{
		printf("顺序表为空,无法删除\n");
	}
	else if (pos > seq->size || pos < 0)
	{
		printf("删除失败\n");
	}
	else
	{
		int Tail = pos + 1;
		while (Tail < seq->size)
		{
			seq->arr[Tail - 1] = seq->arr[Tail];
			Tail++;
		}
		seq->size--;
	}
}

//中间删除
SLErase(sq, 3);
SLErase(sq, 1);
SLErase(sq, 8);
SLErase(sq, -1);

//打印
SLPrint(sq);

说明:

  1. 基本与头删尾删类似,如果进行尾删则无需移动数据,而头删需要将非空顺序表整体左移元素下标位置。

  2. 如果只删除在pos下标位置上的数据,则开始时需要将移动标识变量Tail置于pos+1的位置上,就相当于将pos看作了首元素,后续操作以该pos元素所在下标进行头删覆盖即可,原理图:

    在这里插入图片描述

  3. 需要注意的是🥵,当待删除位置pos非法,或删了只剩空表继续删除时,定义了两个打印函数提示删除失败,其他情况则正常挪动和覆盖删除。

  4. 链表空删得到如下结果:
    在这里插入图片描述

  5. 链表正常和异常删除:
    在这里插入图片描述

💎查找和修改函数

查找下标对应元素值和查找元素值对应下标的功能是可以和其他函数进行联动的,比如查找一个值,取其下标并删除或查找一个元素下标并返回其值,或得到下标并对该下标所对元素进行修改等,是一个完整的生态圈。

✨查找下标对应元素
//函数声明
SeqLElemType SLFind(SL* seq, int pos);				//查找下标对应元素

//函数定义
SeqLElemType SLFind(SL* seq, int pos)				//查找下标对应元素
{
	assert(seq);
	if (SLEmpty(seq))
	{
		printf("顺序表为空\n");
	}
	else if (pos >= seq->size)
	{
		printf("访问越界,非法访问\n");
	}
	else
	{
		return seq->arr[pos];
	}
	return -1;
}

//查找值
printf("查找3号下标的元素为:%d\n", SLFind(sq, 3));
printf("查找0号下标的元素为:%d\n", SLFind(sq, 0));
printf("查找6号下标的元素为:%d\n\n", SLFind(sq, 6));
✨查找元素对应下标
//函数声明
int SLFindPos(SL* seq, SeqLElemType x);				//查找元素对应下标

//函数定义
int SLFindPos(SL* seq, SeqLElemType x)				//查找元素对应下标
{
	assert(seq);
	if (SLEmpty(seq))
	{
		printf("顺序表为空\n");
	}
	else
	{
		int find = 0;
		while (find < seq->size)
		{
			if (seq->arr[find] == x)
			{
				return find;
			}
			find++;
		}
	}
	printf("找不到\n");
	return -1;
}

//查找下标
printf("查找元素5所在下标为:%d\n", SLFindPos(sq, 5));
printf("查找元素2所在下标为:%d\n", SLFindPos(sq, 2));
printf("查找元素3所在下标为:%d\n\n", SLFindPos(sq, 3));
✨修改下标对应元素
//函数声明
void SLModify(SL* seq, int pos, SeqLElemType x);	//修改下标对应元素

//函数定义
void SLModify(SL* seq, int pos, SeqLElemType x)		//修改下标对应元素
{
	assert(seq);
	if (SLEmpty(seq))
	{
		printf("顺序表为空\n");
	}
	SeqLElemType y = SLFind(seq, pos);
	if (y == -1)
	{
		printf("修改失败\n");
	}
	else
	{
		seq->arr[SLFindPos(seq, y)] = x;
	}
}

//修改值
SLModify(sq, 0, -8);
SLModify(sq, 3, 99);
SLModify(sq, 1, 10);
SLModify(sq, 10, 10);

//打印
SLPrint(sq);

🍕以尾插的0 1 2 3 4 5为,模板三者打印结果如下:
在这里插入图片描述

💎销毁顺序表

对所创建顺序表的扩容数组进行销毁,如果开辟了存放顺序表信息的结构体指针空间,也同样需要进行销毁。

//函数声明
void SLDestroy(SL* seq);							//顺序表销毁

//函数定义
void SLDestroy(SL* seq)								//顺序表销毁
{
	assert(seq);
	free(seq->arr);
	seq->arr = NULL;
	seq->capacity = seq->size = 0;
	free(seq);
}
  1. 注意🥵,释放顺序是有讲究的,因为如果先释放了存储顺序表信息的结构体指针SL* seq,那么将无法释放其中所包含的已扩容数组seq->arr,只能先释放结构体中动态扩容的数组arr,并将所有数据置空或置0,再释放该结构体地址才是正确的顺序和步骤。
  2. 每次创建完成并扩容,使用完成后,顺序表和结构体都要及时释放,虽然不释放,所有在栈和堆上的数据都会随着程序的结束自动释放,但作为合格的程序员要能自己开辟使用内存并自己释放,才是好的代码风格。

🌟连续阅读更加流畅,顺序表预热章节:数据结构——顺序表【1】


⭐后话

  1. 博客项目代码开源,获取地址请点击本链接
  2. 若阅读中存在疑问或不同看法欢迎在博客下方或码云中留下评论。
  3. 欢迎访问我的CSDN博客主页Gitee码云主页,更多学习资料记得随时关注和三连点赞,您的支持是我分享的动力~
  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值