【数据结构】--- 顺序表

1.概念与结构

概念:顺序表是一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下使用数组存储。
顺序表的底层结构是数组,对数组进行了封装,实现了常用的增删改查等接口。

2.顺序表的分类

2.1静态顺序表

#define N 1000
typedef int SLDataType;
struct SeqList
{
	SLDataType arr[N];
	int size;//数据元素的个数
	int capacity;//顺序表的容量
};

这样我们就定义好了一个静态的顺序表,我们使用typedef给顺序表的元素类型重命名了,这样当我们需要修改数据元素类型的时候,可以减少修改的代码量,当然我们可以使用typedef关键字来给顺序表结构体命名来简化代码。

typedef struct SeqList
{
	SLDataType arr[N];
	int size;//数据元素的个数
	int capacity;//顺序表的容量
}SL;

SL就是重命名的类型名,后续的代码可以直接使用SL来定义一个顺序表的结构体类型,当然我们也可以单独的给定义好的结构体进行类型重命名。

struct SeqList
{
	SLDataType arr[N];
	int size;
	int capacity;
}SL;
typedef struct Seqlist SL;

动态顺序表有一个缺点就是,顺序表的容量是事先定义好的,空间给少了不够用,给多了造成浪费,这在实际的开发的过程中是一个很大的问题,于是我们引出了动态顺序表。

2.2 动态顺序表

typedef int SLDataType;
typedef struct SeqList
{
	SLDataType* arr;
	int size;
	int capacity;
}SL;

这就是动态顺序表的定义。动态顺序表通过动态内存管理可以实现空间的自由的申请和释放,大大避免了静态顺序表的弊端。接下来我们将实现动态顺序表的各种接口。

3.动态顺序表的实现

3.1顺序表的初始化

算法:

  1. 把arr指向NULL
  2. 把size置为0
  3. 把capacity置为0
void SLInit(SL s)
{
	s.arr = NULL;
	s.size = 0;
	s.capacity = 0;
}

这就是初始化的代码,让我们调试一下。

void test()
{
	SL sl;
	SLInit(sl);
}
int main()
{
	test();
}

还没有开始调试直接弹出bug。
在这里插入图片描述
我们的目的就是初始化顺序表的结构体变量,但是在使用初始化函数结构体之前,因为变量使用前必须初始化而报错,这是很搞得的一件事情。但是我们为了测试这个函数,还是强行的随意赋值一下。

void test()
{
	SL sl;
	sl.size = 2;
	SLInit(sl);
}
int main()
{
	test();
}

F10开始调试,调试结果如下。
在这里插入图片描述

这里首先要注意的是,sl是实参的名字,s是形参的命名,调试结果显示:实参sl并没有被初始化函数初始成功,被修改的还是我们为了测试函数作用而给实参成员随意赋的值,这里我们才恍然大悟,形参是实参的一份临时拷贝,这个函数确实初始化了形参变量,但是函数返回后,这个函数栈帧被销毁,等于这个函数什么都没有做,所以我们修改变量的值的时候,要使用传址的方式。

void SLInit(SL* s)
{
	s->arr = NULL;
	s->size = 0;
	s->capacity = 0;
}

void test()
{
	SL sl;
	SLInit(&sl);
}
int main()
{
	test();
}

这是修改后的代码,下面我们调试一下。
在这里插入图片描述
这里实参sl被正确的初始化,说明修改后的代码是正确的。

3.2判定顺序表的空间是否足够

当顺序表的容量capacity和顺序表中有效的数据元素个数size相等的时候说明空间已经满了,这个时候我们就需要对顺序表进行扩容。先给出代码,然后进行解释。

void SLCheckCapacity(SL* ps)
{
	if (ps->size == ps->capacity)
	{
		int NewCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
		SLDataType* tmp = (SLDataType*)realloc(ps->arr,sizeof(SLDataType) * NewCapacity);
		if (tmp == NULL)
		{
			perror("realloc fail!");
			exit(1);
		}
		ps ->arr = tmp;
		ps->capacity = NewCapacity;
	}
}
  • 当顺序表的size和capacity相等的时候,顺序表需要扩容,进入if判断语句
  • 定义新容量的大小的时候,使用了三目操作符,意思是第一次扩容的初始值是4,后续都是2倍扩容,2倍扩容是一种高效的扩容方式。
  • 然后使用realloc函数重新申请了空间并进行了非空判断,当tmp为空的时候,说明申请失败,直接退出程序。当tmp不为空,说明申请成功,把中间变量tmp的值赋给arr实现了扩容。
  • 最后把NewCapacity赋给ps成员capacity实现了容量记录的更新。

这里说明一下,结构体传参的时候尽量按址传递,因为按值传递的时候,形参会拷贝同一份结构体在内存中,而结构体占用内存是相对大的,多次调用按值传递的函数时,会占用大量内存。

3.4遍历打印顺序表

给出一个顺序表
在这里插入图片描述
由上表可知,我们只需要从下标为0打印到下标为size-1即可完成顺序表的遍历打印

void SLPrint(SL* ps)
{
	int i = 0;
	for(i;i<=ps->size-1;i++)
	{
		printf("%d ", ps->arr[i]);
	}
	printf("\n");
}

这就是遍历打印的代码,无需过多的解释。

3.4尾插

尾插就是在顺序表的尾部插入一个数据元素。
在这里插入图片描述
由图可知,尾插元素的下标就是size,我们插入后记得把size++,下面给出代码。

void SLPushBack (SL* ps, SLDataType x)
{
	assert(ps != NULL);
	SLCheckCapacity(ps);
	ps->arr[ps->size] = x;
	ps->size++;
}
  • 想要插入元素首先进行顺序表的容量检查,调用一下SLCheckCapaity函数。
  • assert的参数为假的时候就是中断程序并报错。
  • 尾插的时间复杂度为O(1)

这里使用打印函数测试一下尾插

void test()
{
	SL sl;
	SLInit(&sl);
	SLPushBack(&sl, 1);
	SLPushBack(&sl, 2);
	SLPrint(&sl);
}

在这里插入图片描述

3.5头插

头插顾名思义就是在顺序表的头部插入一个数据元素,这里头部就是下标为0的位置。
下面给出头插的图示和代码
在这里插入图片描述

在这里插入图片描述

void SLPushFront(SL* ps, SLDataType x)
{
	assert(ps);
	SLCheckCapacity(ps);
	int i = 0;
	for (i = ps->size - 1; i >= 0, i--)
	{
		ps->arr[i + 1] = ps->arr[i];
	}
	ps->arr[0] = x;
	ps->size++;
}
  • 头插前同样要判定顺序表的容量
  • 我把第一个需要移动的数据元素的下标赋值给i,i>=0这个条件由图可以看出。
  • 把元素插入到下标为0的位置后别忘了size++。
  • 头插的时间复杂度为O(n)

下面测试一下头插。

void test()
{
	SL sl;
	SLInit(&sl);
	SLPushBack(&sl, 1);
	SLPushBack(&sl, 2);
	SLPushFront(&sl, 0);
	SLPrint(&sl);
}

在这里插入图片描述
头插成功!

3.6尾删

尾删就是删除线性表的最后一个数据。

void SLPopBack(SL* ps)
{
	if (ps == NULL)
	{
		return;
	}
	assert(ps->size > 0);
	ps->size--;
}
  • 删除的条件首先是顺序表中有数据让你删,所以size要大于0。
  • assert(ps)也可以使用if语句,效果相同。
  • 计算机严格来说不存在“删除”这个词,因为存储器中存储的都是1和0,并且存储器的容量是固定的,不会越删越小,我们经常说的删除实际就是放弃对某块内存数据的管理,或者直接覆盖。
  • 这里我们直接size–就可以了,下次进行插入的时候,数据就会被覆盖了。
  • 尾删的时间复杂度为O(1)

测试一下。

void test()
{
	SL sl;
	SLInit(&sl);
	SLPushBack(&sl, 1);
	SLPushBack(&sl, 2);
	SLPopBack(&sl);
	SLPrint(&sl);
}

在这里插入图片描述

测试成功。

3.7头删

头删就是删除下标为0的数据元素。
在这里插入图片描述

在这里插入图片描述

void SLPopFront(SL* ps)
{
	if (ps == NULL)
	{
		return;
	}
	assert(ps->size > 0);
	int i = 0;
	for (i = 1; i <= ps->size - 1; i++)
	{
		ps->arr[i - 1] = ps->arr[i];
	}
	ps->size--;
}
  • 头删同样要判断size是否要大于0。
  • 头的数据是被直接”覆盖“了。
  • 我把要移动的第一个元素的下标赋给i,由图可以看出i<=size-1;
  • 头删的时间复杂度为O(n)

测试一下

void test()
{
	SL sl;
	SLInit(&sl);
	SLPushBack(&sl, 1);
	SLPushBack(&sl, 2);
	SLPopFront(&sl);
	SLPrint(&sl);
}

在这里插入图片描述
测试成功!

3.8在顺序表中查找指定元素

遍历查找就可以啦,这个比较简单。
在这里插入图片描述

int SLFind(SL* ps, SLDataType x)
{
	assert(ps);
	int i = 0;
	for (i = 0; i <= ps->size - 1; i++)
	{
		if (ps->arr[i] == x)
		{
			return i;
		}
	}
	return -1;
}
  • 遍历的下标为0 到size -1;
  • 跳出循环,说明没有找到对应的元素,返回-1是因为和数组的下标区分

测试一下

void test()
{
	SL sl;
	SLInit(&sl);
	SLPushBack(&sl, 1);
	SLPushBack(&sl, 2);
	int find1 = SLFind(&sl, 2);
	int find2 = SLFind(&sl, 99);
	printf("%d %d", find1, find2);
	//SLPrint(&sl);
}

在这里插入图片描述
测试成功!

3.9在指定的位置之前插入数据

在这里插入图片描述

在这里插入图片描述

void SLInsert(SL* ps, int pos, SLDataType x)
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size );
	SLCheckCapacity(ps);
	int i = 0;
	for (i = ps->size - 1; i >= pos; i--)
	{
		ps->arr[i + 1] = ps->arr[i];
	}
	ps->arr[pos] = x;
	ps->size++;
}
  • pos 的位置必须在下标的范围内,当pos等于0的时候相当于头插,当pos等于size的时候相当于尾插。
  • 插入前必要检查顺序表的容量,插入后size++。
  • 我把第一个要移动的元素的下标赋值给i,由图看出,i>=pos。
  • 时间复杂度最坏的情况下为O(n)

测试一下


void test()
{
	SL sl;
	SLInit(&sl);
	SLPushBack(&sl, 1);
	SLPushBack(&sl, 2);
	SLPushBack(&sl, 3);
	SLPushBack(&sl, 4);
	SLInsert(&sl, 4, 99);
	SLPrint(&sl);
}

在这里插入图片描述
测试成功!

3.10 删除指定位置的元素

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

void SLErase(SL* ps, int pos)
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size - 1);
	int i = 0;
	for (int i = pos + 1; i <= ps->size - 1; i++)
	{
		ps->arr[i - 1] = ps->arr[i];
	}
	ps->size--;
}
  • 这个函数的pos的取值和上一个函数的pos不同,这个函数的pos只能取下标范围内的,而上一个可以取到size实现尾插。
  • 我把第一个要移动元素的下标赋值给i,由图看出i<=size-1。

测试一下

void test()
{
	SL sl;
	SLInit(&sl);
	SLPushBack(&sl, 1);
	SLPushBack(&sl, 2);
	SLPushBack(&sl, 3);
	SLPushBack(&sl, 4);
	SLErase(&sl, 2);
	SLPrint(&sl);
}

在这里插入图片描述
测试成功!

3.11销毁顺序表

顺序表实际是数组的封装,销毁顺序表的第一步首先是销毁被封装的数组。

void SLDesTroy(SL* ps)
{
	if (ps->arr)
		free(ps->arr);
	ps -> arr = NULL;
	ps->size = ps->capacity = 0;
}

测试一下

void test()
{
	SL sl;
	SLInit(&sl);
	SLPushBack(&sl, 1);
	SLPushBack(&sl, 2);
	SLPushBack(&sl, 3);
	SLPushBack(&sl, 4);
	SLDesTroy(&sl);
	SLPrint(&sl);
}

在这里插入图片描述
当尾插后的顺序表被摧毁后,什么都没有打印。

这篇文章是我的第一篇关于数据结构的文章,码出来的目的是为了梳理思路加强学习效果并分享给有需要的朋友,本人的水平有限,文章有纰漏的地方,欢迎大家指正和交流。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值