作者:几冬雪来
时间:2023年2月26日
内容:数据结构顺序表讲解第二部分
目录
前言:
顺序表余下部分讲解:
1.头插:
头插和尾插的区别:
2.头删:
头删和尾删的差距:
3.中间插入或者删除数据:
4.中间插入数据:
5.中间删除数据:
6.查找数据:
7.添加菜单:
8.代码:
结尾:
在上一篇博客中我们对顺序表的初始化,尾增尾删等操作都进行了讲解。但是顺序表的内容却不只有这一些,像上一篇博客有说明到的顺序表的插入和删除的操作,在这个操作里面我们还有将其细分,分为尾插尾删和头插头删,因此我们今天继续对顺序表进行进一步的讲解。
以日常使用的QQ和微信为例,如果顺序表只有初始化,尾插尾删和释放空间在几种功能的话,那它一定不是一个合格的顺序表。因为功能太少了,要是想让它成为一个较正规的顺序表的话,我们应该对其功能进行开发。
在上一篇博客中,我们说过顺序表如果要插入或者删除数据的话有两种方法,一种就是已经讲解过的尾插尾删,那么今天讲解就先来讲解一下头插和头插跟尾插的区别。
这里在书写头插代码的时候,最好在一开始就给代码进行断言操作,关于断言的好处可以讲解一下。
断言:如果断言的内容为假,会在断言处报错,并显示其行数。
接下来我们就来画图来看看头插操作的原理是什么。
首先头部插入,头插的操作并不会随随便便多出一个空间来插入数据,因此如果进行头插的话,我们就要对空间的数据进行移动。在这里,我们给空间最后一个数据一个指针指向,每次移动我们的数据后end指针就进行--操作。直到end的值小于0的时候就停止移动,到这里空间最前面就刚刚好友一块空间可以插入数据。
将上面的一系列写为代码就是这个样子,这里有人可能不理解这个代码在表示什么意思,我们依旧通过画图来辅助讲解。
通过代码可以看出在输入5个值之后,空间进行了一次扩容,size指向了最后一个数据的下一位也就是这个空间没有值,而且size-1则是指向我们空间的最后一个数。在后面,我们又将我们下标为end的值赋值到下标为end+1的空间,形成一个函数的移动,直到我们的end的值小于0截止。
既然我们全部的数据都进行了一次移动,那么接下来就是在我们空间开始的位置插入我们想要的值。
这里因为进行了一次头插行为,因此空间中的数据又增加了一个,所以这里我们的size需要进行一次++的操作,x就是想要插入的值。
但是,代码这样写的话,在某些特殊情况下就可能发生报错。那么是哪一种特殊情况呢?就说一个可能性,就例如我们空间已经放满的情况,在这种情况下要是我们对数据进行移动的话,就会发生越界的行为。
因此在我们进行头插之前,我们需要对这块空间进行扩容操作。而我们这里扩容操作的函数和当初尾插扩容的原理是一样的,所以我们可以直接去调用尾插扩容部分的代码。
接下来就是试验的阶段了,试着插入一些值,然后运行代码来看看输出的结果如何,是否符合结果和预期。
而试验结果则和预想的一致,在头部依次插入1,2,3,4这4个值。到这里我们头插所需要的代码就写好了。 那么我们头插和尾插这两种插入方法有什么区别吗?
既然插入方法分为头插和尾插,那它们之间肯定有不同之处。这里的就用数据结构学到的新知识——头插和尾插的时间复杂度有什么不同。
由图我们可以得出一个结论,那就是写代码的时候要避免头插的出现和书写。如果没有系统认真学习数据结构的内容,这里我们可能会让我头插和尾插的差别不大,但是介绍之后才发现两者其实有很大的差别。
说明完了头插的具体操作方法之后,接下来就是对头删的介绍。那么头删的原理和操作过程是怎么样的呢?
其实我们的头删的操作过程就是挪动覆盖数据。但是这里要注意,在进行头删操作的时候,我们挪动数据的顺序和我们头插的挪动数据的顺序是不一样的,进行头删操作的时候,我们不能从后往前走。为什么?
就像这张图的内容一样,我们的数据从后往前走,先将4的位置进行移动覆盖。但是如果数据从后往前走,我们空间中的3值直接被覆盖掉了。如果一直进行这样的操作,最后我们的空间就全部存放的是4这个值了。因此,头删我们应该从前往后进行操作。
类似这种形式,我们把begin定为首元素后面的下一个元素,接下来把begin的值赋值给begin-1,再执行begin的++操作,代码全部覆盖好后,这里因为少了一个数据,所以size的值要进行-1。思路有了,我们就可以开始写代码了。
但是我们的这个代码是一个不完善的代码,为什么说不完善,如果这里的size的值为0,begin的值为1,size小于begin条件不成立循环不执行,可是在最后我们还是会对size进行--的操作。
因此在执行代码之前需要对size进行断言。
处理完了这个问题,接下来再来看看一个地方,还是size。
在最后代码全部覆盖完毕后,最后一个值重复的4要不要删掉?其实不用,这里的代码是通过size来控制的,这里将代码打印出来看看。
到这里,我们的头删的代码也就写完了,那么头插尾插我们对比过差距,头删和尾删之间有没有差距呢?其实是有的。
这里如果单单进行一次删除,那么我们头删和尾删的时间复杂度都是O(N),但是如果要持续的删除数据在这种情况下两者再进行对比就不一样了。
因此,在写代码的时候,如果没有什么特殊情况的话,尽量不要使用头删和头插的操作。
如果在顺序表中,我们刚刚好想删除中间的某个数据,但是在后来觉得自己不应该删除这样数据又想将这个数据再添加回来,那么在这里我们两种添加和删除的方式都不好解决,这里就需要一个新的编程帮我们解决这个问题了。
首先无论是头插尾插,还是在中间插入数据我们都应该为代码进行断言的操作,不过比起头插和尾插,这里中间插入数据要两个断言的操作。
那为什么要对pos进行断言呢?这是因为如果要在中间插入数据,那么pos的值最多是大于等于0(头插)或者小于等于size(尾插)的。
假如我们要在pos处插入一个值,我们的办法就是将pos后面的值往后移动直到有一个空间可以进行插入的操作。 下面是代码的书写。
首先还是老样子,判断这块空间的大小是否需要扩容。接下来当pos小于end的时候就将end前面的代码依次往后进行挪动,在最后下标为pos处将想添加的值添加进去,并对size进行++就行了。
在上面我们有对这个代码说明过,这个代码不单单可以用来中间插入数据,也可以通过它来实现头插和尾插。那么头插和尾插要靠这个代码实现的话应该怎么样修改?
这个样子我们就可以将头插和尾插的代码进行替换了。
既然可以在中间添加数据,那么相对于的也可以删除数据。那么怎么将我们空间中间的某一个不想要的数据进行删除呢?
一开始又是断言,只不过这个断言和中间插入数据的断言有一丝丝不太一样的地方, 这里的pos断言不能等于size,因为如果是插入数据的话,等于size相当于是尾插操作,但是这里不能是因为如果是删除数据我们这里等于size的话是没有数据能被删除的。
那么我们这个代码是怎么样运行,运行的原理是什么?我们画一张图来看看。
这里的begin是我们pos的下一位,当我们的begin小于size的时候,我们就将下标为begin的值赋值给下标为begin-1那里去,然后begin进行++,理解了原理下来就是简单的写代码环节。
那在这里需不需要检查size为0的情况?其实是不用的,因为在断言处我们就已经间接的检查过一遍了,后面就自己动手来插入一下看看。
这里就是通过顺序表来查找我们里面想要的值,没有什么特殊的方法,只有暴力求解那就是一个一个值看一遍直到找到我们的值, 关于查找数据没什么可以讲的,这里我们就直接上代码。
这就是我们查找数据的代码了。
既然我们写的是顺序表,顺序表可以作为我们的通讯录,那么作为通讯录,我们的菜单是必不可少的。
这样代码就添加好了。
SeqLish.c文件
#include "SeqLish.h"
void SLInit(SL *ps)
{
assert(ps);
ps->a = (SLDataType*)malloc(sizeof(SLDataType)*INIT_CAPACITY);
if (ps->a == NULL)
{
perror("malloc fail");
return;
}
ps->size = 0;
ps->capacity = INIT_CAPACITY;
}
void SLDestroy(SL* ps)
{
free(ps->a);
ps->a = NULL;
ps->size = ps->capacity = 0;
}
void SLPrint(SL* ps)
{
for (int i = 0; i < ps->size; ++i)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
void SLPopBack(SL* ps)
{
assert(ps);
//断言
/*assert(ps->size > 0);*/
//判断
if (ps->size == 0)
{
return;
}
/*ps->a[ps->size - 1] = 0;*/
ps->size--;
}
void SLPushBack(SL* ps, SLDataType x)
{
/*if (ps->size == ps->capacity)
{
SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType)*ps->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->a = tmp;
ps->capacity *= 2;
}
ps->a[ps->size] = x;
ps->size++;*/
SLInsert(ps, ps->size, x);
}
void SLCheckCapacity(SL* ps)
{
assert(ps);
if (ps->size == ps->capacity)
{
SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->a = tmp;
ps->capacity *= 2;
}
}
void SLPushFront(SL* ps, SLDataType x)
{
/*assert(ps);
SLCheckCapacity(ps);
int end = ps->size - 1;
while (end >= 0)
{
ps->a[end + 1] = ps->a[end];
--end;
}
ps->size++;
ps->a[0] = x;*/
SLInsert(ps, 0, x);
}
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size > 0);
int begin = 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
++begin;
}
ps->size--;
}
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
SLCheckCapacity(ps);
int end = ps->size - 1;
while (end >= pos)
{
ps->a[end + 1] = ps->a[end];
--end;
}
ps->a[pos] = x;
ps->size++;
}
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
int begin = pos + 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
++begin;
}
ps->size--;
}
int SLFind(SL* ps, SLDataType x)
{
assert(ps);
for (int i = 0; i < ps->size; ++i)
{
if (ps->a[i] == x)
{
return i;
}
}
return -1;
}
SeqLish.h文件
#pragma once
#include
#include
#include
typedef int SLDataType;
#define N 10
#define INIT_CAPACITY 4
//定义动态顺序表
typedef struct SeqLish
{
SLDataType *a;//指针指向一块空间(堆区)
int size;//有效数据的个数
int capacity;//空间的容量
}SL;
//增删查改
void SLInit(SL *ps);
void SLDestroy(SL *ps);
void SLPrint(SL* ps);
void SLCheckCapacity(SL* ps);
void SLPushBack(SL* ps, SLDataType x);
void SLPopBack(SL* ps);
void SLPushFront(SL* ps, SLDataType x);
void SLPopFront(SL* ps);
void SLInsert(SL* ps, int pos, SLDataType x);
void SLErase(SL* ps, int pos);
int SLFind(SL* ps, SLDataType x);
test.c文件
#define _CRT_SECURE_NO_WARNINGS 1
#include "SeqLish.h"
//void TestSeqList1()
//{
// SL s;
// SLInit(&s);
// SLPushBack(&s, 1);
// SLPushBack(&s, 2);
// SLPushBack(&s, 3);
// SLPushBack(&s, 4);
// SLPushBack(&s, 5);
// SLPrint(&s);
//
// SLPopBack(&s);
// SLPopBack(&s);
// SLPopBack(&s);
// SLPopBack(&s);
// SLPopBack(&s);
// SLPopBack(&s);
// SLPrint(&s);
//
// SLPushBack(&s, 10);
// SLPushBack(&s, 20);
// SLPrint(&s);
//
// SLDestroy(&s);
//}
//void TestSeqList2()
//{
// SL s;
// SLInit(&s);
// SLPushFront(&s, 1);
// SLPushFront(&s, 2);
// SLPushFront(&s, 3);
// SLPushFront(&s, 4);
// SLPrint(&s);
//
// SLPopFront(&s);
// SLPrint(&s);
//
// SLPopFront(&s);
// SLPrint(&s);
//
// SLPopFront(&s);
// SLPrint(&s);
//
// SLPopFront(&s);
// SLPrint(&s);
//}
void TestSeqList3()
{
SL s;
SLInit(&s);
SLPushBack(&s, 1);
SLPushBack(&s, 2);
SLPushBack(&s, 3);
SLPushBack(&s, 4);
SLPushBack(&s, 5);
SLPushBack(&s, 6);
SLPushBack(&s, 7);
SLPushBack(&s, 8);
SLPushBack(&s, 9 );
SLPrint(&s);
SLInsert(&s, 4, 40);
SLPrint(&s);
SLInsert(&s, 0, 40);
SLPrint(&s);
SLErase(&s, 4);
SLPrint(&s);
SLErase(&s, 3);
SLPrint(&s);
}
void menu()
{
printf("**********************************\n");
printf("1.尾插数据 2.尾删数据\n");
printf("7.打印数据 -1.退出\n");
printf("**********************************\n");
}
int main()
{
SL s;
SLInit(&s);
int option = 0;
while (option != -1)
{
menu();
scanf("%d", &option);
if (option == 1)
{
printf("请依次输入要尾插的数据,以-1结束:");
int x = 0;
while (x != -1)
{
scanf("%d", &x);
SLPushBack(&s, x);
}
}
else if (option == 7)
{
SLPrint(&s);
}
}
/*TestSeqList1();*/
/*TestSeqList2()*//*;*/
//TestSeqList3();
return 0;
}
这就是我们顺序表至今为止的全部代码了。
好不容易将我们的顺序表写完了,这里有人可能会问:我们顺序表的增删查改怎么没有改啊?其实在我们找的过程中就可以实现我们改的操作。到这里我们的顺序表就正式的结束了,这里顺序表的菜单并没有写得那么详细,大家可以以后对其进行修改。最后希望这篇博客能为各位提供帮助。