详解线性表——顺序表【一看完,诶,发现我已经掌握顺序表核心了】

在这里插入图片描述
在这里插入图片描述

今天的你,学习了吗?敲代码了吗?比昨天有哪些进步?


1、线性表

线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…

线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。

2、顺序表

2.1、概念及结构

顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
在这里插入图片描述
顺序表一般可以分为:静态顺序表和动态顺序表

2.2、静态顺序表

静态顺序表:使用定长数组存储元素。

//静态顺序表
//存在问题:开小了,不够用。开大了,存在浪费。
#define N 7   //这里就关乎到开辟多少空间的问题了
typedef int SLDataType;

typedef struct Seqlist
{
    SLDataType a[N];//定长数组
    size_t size;//记录存储多少个数据
};

图解分析:
在这里插入图片描述
❓ 这里是用定长数组来存储元素的,那么这就有一个问题了,定长数组方便我们来使用吗?

💯 对于数组来说,我们必须要考虑的就是数组大小的选取,在上述代码中,我们是利用宏定义来改变数组的大小,那我们到底需要定义多大的数组呢?这里就存在疑问了,开小了,不够用;开大了,存在浪费。这就是静态顺序表存在的局限。

2.3、动态顺序表

动态顺序表:使用动态开辟的数组存储元素。

typedef int SLDataType;

typedef struct Seqlist
{
    SLDataType* a;//指向动态开辟的数组
    size_t size;  //记录存储数据的个数
    size_t capacity; //记录存储数据空间的大小
};

图解分析:
在这里插入图片描述
💯 动态顺序表是对空间进行灵活开辟使用的,空间不够时候,就增容,而且增容不会增太大,避免造成大部分空间浪费现象,所以动态顺序表更加灵活,更加常用。

2.4、动态顺序表各种函数接口实现源码

静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下面我们实现动态顺序表。

2.4.1、动态顺序表初始化函数

//顺序表初始化
//一般有两种初始化形式,一是初始化都为空,而是不为空
void SeqListInit(SeqList* PSl)//这里只有利用传址调用才能完后初始化
{
    	assert(PSl);
    	PSl->a = NULL;   //这里面一定要是对结构体里面的成员进行初始化
    	PSl->size = 0;   //记录数据的个数
    	PSl->capacity = 0;//记录空间的大小
}

💯分析:

动态顺序表初始化一般有两种:一是直接初始化为空,另一种就是初始化不为空。这里我们就写第一种初始化。

图解分析:
在这里插入图片描述

2.4.2、动态顺序表尾插函数

//检查是否扩容函数
void SeqListCheckCapacity(SeqList* Psl)
{
	size_t newCapacity = PSl->capacity == 0 ? 4 : PSl->capacity * 2;
	if (PSl->size == PSl->capacity)//如果数据需要的空间大于原本的空间,此时就需要扩容
	{
		SLDataType* tmp = (SLDataType*)realloc(PSl->a, sizeof(SLDataType) * newCapacity);
		//realloc用法是:旧空间,需新阔的空间大小(这个大小需要计算正确)
		//利用realloc扩容,realloc如果起始空间为0时候,它其实就和malloc效果一样
		//如果起始不为0,而且空间不够realloc开始作用时候,realloc扩容后,还会将原来的空间拷贝下来
		//再加上新扩容的空间
		if (tmp == NULL)//判断一下tmp是否为空,防止为空的时候后面操作出现程序崩溃
		{
			printf("relloc fail\n");
			exit(-1);//这里直接退出程序
		}
		else
		{
			PSl->a = tmp;
			PSl->capacity = newCapacity;
		}
	}
}
//尾插函数实现
void SeqListPushBack(SeqList* PSl, SLDataType x)
{
	assert(PSl);
	SeqListCheckCapacity(Psl);
	PSl->a[PSl->size] = x;
	PSl->size++;
}

💯分析:

在这个尾插函数里面我们需要考虑到:在尾插时候空间是否够用,不够用时候,就利用动态开辟空间进行增容处理,因此这里面就存在一个判断是否扩容的操作。
扩容这里利用 realloc 进行扩容,realloc 在初始空间为空的时候扩容用法和malloc 是一样的,而且这里用 realloc 还因为它既可以原地扩容,也可以异地扩容。其在异地扩容的时候会自己开辟新空间,然后将旧空间里的值拿到新空间再把旧空间自动销毁。

💯扩容图解分析:
在这里插入图片描述
尾插操作图解分析:
在这里插入图片描述

2.4.3、动态顺序表尾删函数

//动态顺序表尾删函数
void SeqListPopBack(SeqList* PSl)
{
	assert(PSl);
	if (PSl->size > 0)
	{
		PSl->size--;
	}
	/*for (int i = 0; i < n; i++)这里是自己尝试一次性删除多个数据,
	//n作为一次需要删除的数据个数当作形参传过来
	//{
	//	if (PSl->size > 0)
	//		PSl->size--;
	//	else
	//		break;
	//}*/
}

💯分析:

尾删函数这里就不需要进行扩容检查了,直接进行删除数据操作就可以。

图解分析:
在这里插入图片描述

2.4.4、动态顺序表头插函数

//头插函数
void SeqListPushFront(SeqList* Psl, SLDataType x)
{
    assert(Psl);
    SeqListCheckCapacity(Psl);//调用检查扩容函数
    int end = Psl->size - 1;
    while (end >= 0)
    {
    	Psl->a[end + 1] = Psl->a[end];
		--end;
	}
	Psl->a[0] = x;//将要头插的数放到头部
	Psl->size++;
}

💯分析:

头插数据,这里因为要插入数据,所以就涉及到检查是否扩容的问题。然后头插就是将数据放到最前面一个位置,这时候就存在将后面的数据依次向后面挪动位置。

图解分析:
在这里插入图片描述

2.4.5、动态顺序表头删函数

//头删函数
void SeqListPopFront(SeqList* Psl)
{
	assert(Psl);
	//int start = 0;
	if (Psl->size > 0)//防止头删到只有一个数字,出现越界,此时一旦出现越界,
		             //可能就会让之后的头插出现无法插入的问题
	{
	  	while (start < Psl->size - 1)
  		{
     		 Psl->a[start] = Psl->a[start + 1];
      		++start;
  		 }
		Psl->size--;
  }
}

💯分析:

这里在操作头删函数的时候,需要注意在删除的时候是否只有一个数字,然后删除后出现越界现象,有时候这里一旦越界是无法报错检查出来的,但是若在后面进行头插或者尾插时候就会出现问题。

图解分析:
在这里插入图片描述
头删函数有两种方法,图解分析一下看看:
在这里插入图片描述

尾插尾删函数的时间复杂度是O(1);头插头删函数的时间复杂度是O(N);所以不到万不得已不要使用头插头删函数,因为时间效率低

2.4.6、实现在pos位置插入x函数

//在pos位置插入x
void SeqListInsert(SeqList* Psl,size_t pos, SLDataType x)
{
	//pos是下标位置
	assert(Psl);//assert是暴力检查
	//温和检查pos是否越界
	if (pos > Psl->size)//这里如果pos==size相当于是尾插
	{
		printf("pos越界\n");
		return;
	}
	//也可以暴力检查
	//assert(pos <= Psl->size);
	SeqListCheckCapacity(Psl);//插入数字需要检查空间是否需要扩容
	for (int i = Psl->size; i > pos; i--)
	{
		Psl->a[i] = Psl->a[i - 1];
	}
	Psl->a[pos] = x;
	Psl->size++;
}

💯分析:

注意这里 pos 指的是下标位置,这时候需要注意传过来的 pos 是否越界,所以需要判断一下:当然可以温柔判断(强烈建议),也可以暴力断言。一提到插入数据,当然就别忘了考虑是否扩容这个问题啦~

图解分析:
在这里插入图片描述

2.4.7、删除pos位置的数据x

//删除pos位置x
void SeqListErase(SeqList* Psl,size_t pos)
{
	//pos是下标位置
	assert(Psl);
	if (pos >= Psl->size)
	{
		printf("pos越界\n");
		return;
	}
	else
	{
		while (pos < Psl->size)
		{
			Psl->a[pos] = Psl->a[pos + 1];
			pos++;
		}
		Psl->size--;
	}
}

💯分析:

这里依然需要注意 pos 是下标位置,然后在删除的时候需要考虑需要删除的 pos 位置是否越界。删除之后将后面的数据依次向前挪动覆盖。

图解分析:
在这里插入图片描述

2.4.8、顺序表查找函数

//顺序表查找函数
int SeqListFind(SeqList* Psl, SLDataType x)
{
	assert(Psl);
	for (int i = 0; i < Psl->size; i++)
	{
		if (x == Psl->a[i])
		{
			return i;
		}
	}
	return -1;
}

💯分析:

顺序表查找函数这里是一个返回值为 int 类型的函数。该函数一般会和删除插入函数配合使用。

图解分析:
在这里插入图片描述

2.4.9、顺序表修改函数

//修改数据函数
void SeqListModify(SeqList* Psl, size_t pos, SLDataType x)
{
	assert(Psl);
	assert(pos < (int)Psl->size);
	Psl->a[pos] = x;
}

💯分析:

这里修改函数比较简单,直接将修改值赋值给被修改值就可以。

图解分析:
在这里插入图片描述

2.4.10、销毁函数和打印函数

//销毁函数
void SeqListDestroy(SeqList* Psl)
{
	assert(Psl);
	free(Psl->a);
	Psl->a = NULL;
	Psl->size = Psl->capacity = 0;
}

//打印函数
void SeqListPrint(SeqList* Psl)
{
	for (size_t i = 0; i < Psl->size; i++)
	{
		printf("%d ", Psl->a[i]);
	}
}

💯分析:

这里利用 free对使用完后的顺序表空间进行释放处理。当顺序表使用完之后,在内存开辟的一整块空间就对其进行释放销毁,防止出现内存泄露。这里要注意不能对同一空间进行多次free释放.

2.5、顺序表头文件源码


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

typedef int SLDataType;

typedef struct SeqList
{
	SLDataType* a;
	size_t size;
	size_t capacity;
}SeqList;

//顺序表初始化函数声明
void SeqListInit(SeqList* Pls);

//尾插尾删函数时间复杂度是:O(1)
void SeqListPushBack(SeqList* Psl, SLDataType x);//顺序表尾插函数声明
void SeqListCheckCapacity(SeqList* Psl);//检查扩容函数
void SeqListPopBack(SeqList* Psl);//顺序表尾删函数声明

//时间复杂度是:O(N)
void SeqListPushFront(SeqList* Psl, SLDataType x);//头插函数
void SeqListPopFront(SeqList* Psl);//头删函数

//在pos位置插入x
void SeqListInsert(SeqList* Psl, size_t pos, SLDataType x);

//删除pos位置的x
void SeqListErase(SeqList* Psl,size_t pos);

//顺序表查找函数
int SeqListFind(SeqList* Psl, SLDataType x);

//顺序表修改函数
void SeqListModify(SeqList* Psl, size_t pos, SLDataType x);

//销毁函数
void SeqListDestroy(SeqList* Psl);
								  
//打印函数
void SeqListPrint(SeqList* Psl);

2.6、拓展一下realloc原地扩容和异地扩容

顺序表里面使用realloc的好处是:realloc在异地扩容的时候会自己开辟新空间,然后将旧空间里的值拿到新空间再把旧空间自动销毁。

原地扩容和异地扩容取决于需要扩容的大小,扩容小了一般会直接原地扩容,需要扩容的空间大了,就要异地扩容。

原地扩容:

#include <stdio.h>
#include <stdlib.h>
int main()
{
    int* p = (int*)malloc(sizeof(int) * 10);
    printf("%p\n", p);

    int* p1 = (int*)realloc(p, sizeof(int) * 15);
	//realloc扩容在这里不是扩大了100个int型,而是扩大后为100个int型,即这里增加了90个int型
	printf("%p\n", p1);
	return 0;
}

运行结果:
在这里插入图片描述
💯分析:

以上是原地扩容,因为这里在int大小上面*15,意思是扩容后整体大小是15个int类型,这里扩容比较小,所以是原地扩容,可以看出打印的地址相同。

异地扩容:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    int* p = (int*)malloc(sizeof(int) * 10);
    printf("%p\n", p);

    int* p1 = (int*)realloc(p, sizeof(int) * 100);
	//realloc扩容在这里不是扩大了100个int型,而是扩大后为100个int型,即这里增加了90个int型
	printf("%p\n", p1);
	return 0;
}

运行结果:
在这里插入图片描述
💯分析:

以上是异地扩容,扩容后是100个int整型,因为需要扩容的空间大,所以就会进行异地扩容,可以看出扩容前和扩容后打印的地址不一样。这里realloc会销毁旧空间,然后将之前就空间的数据移到新空间。

2.7、 顺序表的局限

  1. 中间/头部的插入删除,时间复杂度为O(N)。
  2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
  3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。

总结

顺序表各个环节的实行过程,这里已经详细介绍完毕。当然顺序表有利也有弊:其中尾插尾删函数的时间复杂度是O(1);头插头删函数的时间复杂度是O(N);所以不到万不得已不要使用头插头删函数,因为时间效率低。一般在这里面使用尾插尾删函数,但是顺序表各个环节实现的思维是值得深究学习的,因为有很多题目不会直接考顺序表的实现,而是间接考察其中的一些思想和思维方式。
顺序表是线性表最简单的环节,所以大家应该牢牢打好基础,希望最大家有所帮助。如有不足之处,请批评指正!
谢谢观看!
再见!
以上代码均可运行,所用编译环境为 vs2019 ,运行时注意加上编译头文件#define _CRT_SECURE_NO_WARNINGS 1
在这里插入图片描述

评论 50
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

<vince>

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

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

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

打赏作者

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

抵扣说明:

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

余额充值