【数据结构】第二章——线性表(3)

顺序表基本操作的实现

  • 导言
  • 一、顺序表的创建
  • 二、插入元素
    • 2.1 插入操作的基本格式
    • 2.2 插入操作的实现
    • 2.3 插入操作的时间复杂度
  • 三、修改表长
  • 四、删除元素
    • 2.1 删除操作的基本格式
    • 2.2 删除操作的实现
    • 2.3 删除操作的时间复杂度
  • 五、查找元素
    • 5.1 按位查找
    • 5.2 按值查找
    • 5.3 查找操作的时间复杂度
  • 六、修改元素
  • 结语

导言

大家好,很高兴又和大家见面了!!!
在上一篇中,咱们介绍了顺序表的基本概念,以及通过C语言实现顺序表的创建和对表长的修改。今天咱们将详细介绍一下使用C语言实现顺序表的增删改查。接下来,跟我一起来看看今天的内容吧!!!

一、顺序表的创建

我们先来回顾一下上一篇的内容,在上一篇中,我们介绍了顺序表的两种创建方式——静态分配与动态分配。今天我们通过动态分配的方式来创建一个顺序表,如下所示:

//顺序表的创建——动态分配
#define InitSize 5//定义顺序表初始表长
typedef struct
{
	int* date;//定义指向顺序表的指针
	int MaxSize, length;//定义顺序表的最大表长以及当前表长
}Sqlist;//将顺序表重命名为Sqlist
int main()
{
	Sqlist L;//创建顺序表
	return 0;
}

在定义完顺序表的初始表长、指向顺序表的指针、顺序表的最大表长以及顺序表的当前表长后,我们通过重命名的顺序表名字定义了一个顺序表L,此时的顺序表在内存空间中还未申请空间,所以我们需要通过对顺序表进行初始化,来给顺序表申请一块空间,如下所示:

//顺序表的初始化
void InitList(Sqlist* L)
{
	//通过calloc在内存中申请长度为初始表长,大小为int的一块连续空间
	L->date = (int*)calloc(InitSize, sizeof(int));
	L->MaxSize = InitSize;//将此时的最大表长初始化为初始表长
	L->length = 0;//目前顺序表中并未有任何元素,所以给当前表长初始化为0
}
int main()
{
	Sqlist L;//创建顺序表
	InitList(&L);//通过传址传参对L进行初始化
	return 0;
}

现在我们已经完成了顺序表的创建与初始化,现在我们就可以对顺序表进行一些基本操作了;

二、插入元素

插入操作ListInsert(&L,I.e):在表中的第i个位置上插入指定元素e。

这里的i指的是元素的位序,那对应的数组下标就是i-1

现在我们可以想象一个情景:目前我们的顺序表中有了一系列的元素,我们想要在其中两个元素中间插入一个新的元素,如下所示:

【数据结构】第二章——线性表(3)_第1张图片
我们可以看到,如果想将这个新元素e插入到顺序表中,我们就需要将其余的元素往后移,将e要插入的位置空出来才行,如下所示:

【数据结构】第二章——线性表(3)_第2张图片
在移动完元素后,我们才能顺利的将新的元素插入进行,这时就会出现以下几种情况:

  1. 顺序表为空表;
  2. 顺序表当前表长小于顺序表最大表长;
  3. 顺序表当前表长等于顺序表最大表长;

接下来我们来一一分析这几种情况;

  1. 顺序表为空表

在顺序表为空表时,我们想要给顺序表插入元素,只能够从首元素开始插入,如果顺序表为空表,而我们想要从位序3的位置插入一个元素,这是不合理的,此时不能正常插入元素。

  1. 顺序表当前表长小于顺序表最大表长

在这个情况下,又会有几种插入方式:

  • 如果我们想要在已有元素的位序上进行插入操作,可以通过将该位序以及后面的元素往后移动,然后再插入元素;
  • 如果我们想要在顺序表的最后插入元素,此时我们可以直接将元素插入到顺序表中;
  • 如果我们想要插入的元素位序不符合当前表长,那我们也不能正常插入元素;
  1. 顺序表当前表长等于顺序表最大表长

在这个情况下,我们也不能正常插入元素;

综上所述,我们可以的出结论:

  1. 顺序表进行插入操作时,需要判断插入元素的位序的合理性以及当前表长的合理性
  2. 顺序表进行插入操作时,需要对当前位置后面的元素进行移动,最好的情况是为空表时直接在第一个位序上进行插入,以及在表的最后面的空位上进行插入
  3. 顺序表在完成插入操作后,需要给用户进行一个反馈,是插入成功还是插入失败

下面我们来看一下插入操作的基本格式;

2.1 插入操作的基本格式

//插入操作的基本格式
bool ListInsert(Sqlist* L, int i, ElemType e)
{
	//判断插入位序的合理性
	if (i<1 || i>L->length + 1)
		return false;
	//判断表长的合理性
	if (L->length >= L->MaxSize)
		return false;
	//进行元素移动
	for (int j = L->length; j >= i; j--)
		L->date[j] = L->date[j - 1];
	//插入元素
	L->date[i - 1] = e;
	//更新当前表长
	L->length++;
	return true;
}
  • 在判断位序的合理性时,之所以是i<1,这是因为顺序表中的位序是从1开始,对应的数组下标是0,当小于1时,对应的数组下标就为负数,此时无法放入数组中;
  • i=L->length+1,表示的是在顺序表的表尾进行插入,如果i>L->length+1,表示的是此时的元素是插入到其他位置,这个位置与表中的最后一个元素没有做到物理位置上相邻这个要求,所以也是不能正常插入顺序表的;
  • L->length==L->MaxSize,表示的是此时顺序表中元素已经放满了,无法继续插入新的元素;
  • 我们在移动元素时,之所以是j>=i,这是因为,我们需要从顺序表的最后一个元素进行移动,直到将需要插入的位序上的元素全部移动完,才能正常插入。所以对于位序为i上的元素也要进行移动,位序i对应的下标i-1这个位置就空出来了,此时我们再进行插入的话就是插入到下标为i-1的位置,对应的位序则是i

2.2 插入操作的实现

接下来我们来尝试着插入相应的元素,并将插入完的顺序表打印出来,如下所示:

//插入操作
bool ListInsert(Sqlist* L, int i, int e)
{
	//判断插入位序的合理性
	if (i<1 || i>L->length + 1)
		return false;
	//判断表长的合理性
	if (L->length >= L->MaxSize)
		return false;
	//进行元素移动
	for (int j = L->length; j >= i; j--)
		L->date[j] = L->date[j - 1];
	//插入元素
	L->date[i - 1] = e;
	//更新当前表长
	L->length++;
	return true;
}
//打印顺序表
void PrintList(Sqlist L)
{
	printf("打印顺序表L:>");
	for (int i = 0; i < L.length; i++)
		printf("%d ", L.date[i]);
	printf("\n");
}
int main()
{
	Sqlist L;//创建顺序表
	InitList(&L);//通过传址传参对L进行初始化
	if (ListInsert(&L, 1, 1))
		PrintList(L);
	else
		printf("插入失败\n");
	if (ListInsert(&L, 2, 2))
		PrintList(L);
	else
		printf("插入失败\n");
	if (ListInsert(&L, 4, 4))
		PrintList(L);
	else
		printf("插入失败\n");
	if (ListInsert(&L, 3, 4))
		PrintList(L);
	else
		printf("插入失败\n");
	if (ListInsert(&L, 4, 5))
		PrintList(L);
	else
		printf("插入失败\n");
	if (ListInsert(&L, 3, 3))
		PrintList(L);
	else
		printf("插入失败\n");
	return 0;
}

我们在进行插入操作后,对返回值通过if语句来判断是否插入成功,成功,则打印插入后的顺序表,不成功则提示插入失败,测试结果如下所示:

【数据结构】第二章——线性表(3)_第3张图片
可以看到,当我们第三次想要在位序为4的位置插入元素4时,结果是插入失败;当我们将元素4和5都放入顺序表后,再插入元素3,此时元素4和5有往后进行移动。现在我们就很好的实现了插入操作。

2.3 插入操作的时间复杂度

对于插入操作来说,我们需要关注的是插入操作中最深层的代码的时间复杂度,即

	for (int j = L->length; j >= i; j--)
		L->date[j] = L->date[j - 1];
  • 这里最好的情况就是没有移动任何元素,也就是此时的代码是顺序执行,时间复杂度为O(1)
  • 最坏的情况是从表头插入元素,此时所有的元素都需要进行移动,对应的时间复杂度为O(n);
  • 正常情况下,假设有一个表长为n的顺序表,我们可以在表中插入的位置是从1~n+1,即从表头到表尾的位置进行插入,那每个位置插入的概率都为 p = 1 / ( n + 1 ) p=1/(n+1) p=1/(n+1)
    当我从表头插入的话,那此时的循环次数为n次,从第二个位置插入则是n-1次,以此类推,当在表尾插入时,循环指向次数为0次,所以我们很容易的得到平均时间复杂度为:
    Σ i = 1 n + 1 p i ( n − i + 1 ) = n p + ( n − 1 ) p + … … + p \Sigma_{i=1}^{n+1}p_i(n-i+1)=np+(n-1)p+……+p Σi=1n+1pi(ni+1)=np+(n1)p+……+p
    Σ i = 1 n + 1 p i ( n − i + 1 ) = [ ( n + 1 ) n / 2 ] ∗ [ 1 / ( n + 1 ) ] = n / 2 \Sigma_{i=1}^{n+1}p_i(n-i+1)=[(n+1)n/2]*[1/(n+1)]=n/2 Σi=1n+1pi(ni+1)=[(n+1)n/2][1/(n+1)]=n/2
    即平均时间复杂度为O(n)

三、修改表长

现在我们的顺序表已经放满了,此时如果我们想修改表长的话,上一篇也介绍过,可以通过malloccalloc进行申请新的空间,也可以通过realloc直接对顺序表进行修改。

这里我们先看看通过realloc如何进行修改,如下所示:

【数据结构】第二章——线性表(3)_第4张图片
这里初始化空间是为了让大家看的更清楚,实际操作的过程中是不需要的,我们如果想插入新的元素可以直接在表尾进行插入即可,下面我们来看一下通过malloccalloc应该如何进行修改:

【数据结构】第二章——线性表(3)_第5张图片
可以看到,此时能够达到同样的效果,但是要注意的是:

  1. malloc申请的空间,不会主动初始化为0,而calloc申请的空间会主动将空间内的元素初始化为0;
  2. 由realloc增加的新空间,不会主动初始化为0,但是它会在申请空间时将原空间的数据主动复制到新的空间中去;

所以,大家在修改表长时对函数的选择可以根据自己的喜好来决定,对应的代码如下:

//修改表长
void IncreaseSize(Sqlist* L, int len)
{
	//通过realloc进行修改
	L->date = (int*)realloc(L->date, (L->MaxSize + len) * sizeof(int));
	//L-date——修改空间的起始点,必须是通过malloc或者calloc申请的空间
	//(L->MaxSize + len) * sizeof(int)——修改后的空间大小
	L->MaxSize += len;//修改顺序表的最大表长
------------------------------------------------------------------------------------------
	//通过calloc进行修改
	int* p = L->date;//通过临时指针指向原先的空间
	L->date = (int*)calloc(L->MaxSize + len, sizeof(int));//申请新的空间
	//复制元素到新的空间
	for (int i = 0; i < L->length; i++)
		L->date[i] = p[i];
	L->MaxSize += len;//修改顺序表的最大表长
	free(p);//释放原先的空间
	p = NULL;//将临时指针p变成空指针可以不需要
}

四、删除元素

删除操作ListDelete(&L,i,&e):删除表L中第i个位置的元素,并用e返回删除元素的值;

删除元素与插入元素原理相同的,当我需要删除一个元素时,我只需要将这个元素后面的元素往前移动就可以完成删除操作了,如下所示:

【数据结构】第二章——线性表(3)_第6张图片
在删除时,我们同样需要对删除对象的合理性进行判断,接下来我们就来看看删除的格式;

2.1 删除操作的基本格式

//删除操作的基本格式
bool ListDelete(Sqlist* L, int i, ElemType* e)
{
	//判断删除对象位序的合理性
	if (i<1 || i>L->length)
		return false;
	//将删除的对象赋值给e
	*e = L->date[i - 1];//通过同类型的变量e来记录被删除的元素
	//移动元素
	for (int j = i; j < L->length; j++)
		L->date[j - 1] = L->date[j];
	//修改表长
	L->length--;
	return true;
}
  • 我们需要删除的元素肯定是在顺序表中已经存放的元素,也就是当前表长中的全部元素。当我们需要删除的元素不在这个范围内时,我们是无法进行删除操作的;
  • 我们在通过变量e记录被删除的元素后,此时就位序为i的位置就空出来了,我们就需要将此位置后面的元素依次前移,移动完所有元素后,此时的表长需要减1;

2.2 删除操作的实现

接下来我们来尝试着删除两个元素:

//删除操作
bool ListDelete(Sqlist* L, int i, int* e)
{
	//判断删除对象位序的合理性
	if (i<1 || i>L->length)
		return false;
	//将删除的对象赋值给e
	*e = L->date[i - 1];//通过同类型的变量e来记录被删除的元素
	//移动元素
	for (int j = i; j < L->length; j++)
		L->date[j - 1] = L->date[j];
	//修改表长
	L->length--;
	return true;
}
int main()
{
	Sqlist L;//创建顺序表
	InitList(&L);//通过传址传参对L进行初始化
	//插入元素
	……
	//修改表长
	IncreaseSize(&L, 5);//将表长增加5
	L.length = L.MaxSize;
	//打印顺序表
	PrintList(L);
	int e = -1;//定义同类型的变量e来记录删除的元素
	if (ListDelete(&L, 3, &e))
	{
		printf("位序%d上的元素%d已经成功删除\n", 3, e);
		PrintList(L);
	}
	else
	{
		printf("要删除的位序不合理,删除失败\n");
	}
	if (ListDelete(&L, 11, &e))
	{
		printf("位序%d上的元素%d已经成功删除\n", 3, e);
		PrintList(L);
	}
	else
	{
		printf("要删除的位序不合理,删除失败\n");
	}
	return 0;
}

与插入操作相同,我们也是通过if语句来进行判断是否成功删除元素,下面我们来看一下测试结果:

【数据结构】第二章——线性表(3)_第7张图片
可以看到位序3上的元素3已经成功删除,但是位序11上的元素并未被删除,因为我们此时的表长时10,没有11这个位序,所以系统提示删除失败。现在我们就很好的实现了删除操作。

2.3 删除操作的时间复杂度

对于删除操作而言,我们可以看到它其实就是插入操作的反操作,插入操作是在位序i上插入一个新元素,需要将该位序即后面的元素往后移动,而删除操作则是将位序i后面的元素往前移动,因此他们的时间复杂度相同:

  • 最好时间复杂度为O(1);
  • 最坏时间复杂度为O(n);
  • 平均时间复杂度为O(n);

五、查找元素

我们查找顺序表中的元素有两种方式:按值查找与按位查找。

  • 按值查找操作LocateElem(L,e):在表L中查找具有给定关键字值的元素;
  • 按位查找操作GetElem(L,i):获取表L中第i个位置的元素的值;

简单的理解就是一个通过给定值,来查找顺序表中有没有该元素,如果有返回元素的下标;而按位查找则是通过位序i找到表中对应位序的元素的值。下面我们就来一一介绍一下这两种查找方式;

5.1 按位查找

按位查找的话我们需要在查找前先判断一下该位序是否合法,如果合法我们就可以将该位序上的元素的值返回给函数,按位查找的基本格式如下所示:

//按位查找的基本格式
ElemType GetElem(Sqlist L, int i)
{
	//判断位序的合理性
	if (i<1 || i>L.length)
		return -1;//不合理则返回-1
	else
		return L.date[i - 1];//合理则返回对应位序上的元素的值
}

大家在定义按位查找时一定要注意函数的返回类型与元素的类型是一致的。

5.2 按值查找

按值查找的话就是通过在顺序表中查找有无对应元素的值,如果找到了就将元素的位序返回给函数,按值查找的基本格式如下所示:

//按值查找的基本格式
int LocateElem(Sqlist L, ElemType e)
{
	//进行顺序查找
	for (int i = 0; i < L.length; i++)
	{
		//判断对应的值是否是我们需要查找的值
		if (L.date[i] == e)
			return i + 1;//返回对应的位序
	}
	return 0;//没有找到则返回0
}

按值查找的查找方式这里我是一顺序查找举例,在顺序表中,因为元素在逻辑上也是相邻的,也就是说,顺序表的逻辑是有序的,我们在查找时也可以通过折半查找法进行查找,当然后面会陆续介绍更多的查找方法。

5.3 查找操作的时间复杂度

如果我们是通过按位查找的话,时间复杂度就是O(1);
当我们通过按值查找的话,根据查找方式的不同,它会有不同的时间复杂度,就拿这里的顺序查找来说,只要是从头开始往后找,那它就会有三种情况:

  1. 最好情况:查找的元素为首元素,此时只需要查找一次就行,对应的时间复杂度为O(1)
  2. 最坏情况:查找的元素在表尾,此时需要查找n次,对应的时间复杂度为O(n)
  3. 平均情况:因为查找各元素的概率都相同,此时就好比插入元素一样,在插入之前我们也是需要进行查找,所以此时的时间复杂度为O(n)

六、修改元素

我们在查找完元素后,可以通过返回值对该元素进行修改,如下所示:
【数据结构】第二章——线性表(3)_第8张图片
如果我们是通过按值查找来查找对应元素的话,那我们在修改元素时就是通过元素的位序进行修改;

【数据结构】第二章——线性表(3)_第9张图片
如果我们通过按位查找来查找对应元素的话,那我们在修改元素时,就可以通过查找到的值来进行修改。

当然对于修改元素来说,不管是通过值来修改,还是通过位序来修改,它都需要在修改前找到对应的元素才行。对于这些函数的定义,可以根据自己的需求来进行,但是前提是定义的函数需要有健壮性。

结语

到这里,咱们对顺序表的介绍就全部完成了,希望大家看完这篇内容,能够掌握对顺序表的基本操作。接下来我们将开始介绍线性表的第二种表示方式——链式表示,大家记得关注哦!

最后感谢各位的翻阅,咱们下一篇再见!!!

你可能感兴趣的:(数据结构,保姆级教学,数据结构,算法,c语言,改行学it,学习,经验分享)