数据结构之顺序表

当当!欢迎进入数据结构的学习!

谈到数据结构,相信大家应该已经早就听过这个名词,但是什么是数据结构,大家应该还比较陌生

数据结构是由“数据”和“结构”两词组合⽽来。

什么是数据?常⻅的数值1、2、3、4…、学生系统⾥保存的⽤⼾信息(姓名、性别、年龄、学历等等)、⽹⻚⾥⾁眼可以看到的信息(⽂字、图⽚、视频等等),这些都是数据

那什么是结构呢?
当我们想要使⽤⼤量使⽤同⼀类型的数据时,通过⼿动定义⼤量的独⽴的变量对于程序来说,可读性⾮常差,我们可以借助数组这样的数据结构将⼤量的数据组织在⼀起,结构也可以理解为组织数据的⽅式。

举个例子,当我们想要在学生系统中找到你自己的名字,那实在很难,因为学生的人数太多了,你一个一个向下翻,可能要找很久;但如果你直接搜索你的学号,那一下子便会找到

总结一下:数据结构是计算机存储、组织数据的⽅式。数据结构是指相互之间存在⼀种或多种特定关系的数据元素的集合。数据结构反映数据的内部构成,即数据由那部分构成,以什么⽅式构成,以及数据元素之间呈现的结构。
通过数据结构,能够有效将数据组织和管理在⼀起。按照我们的⽅式任意对数据进⾏增删改查等操作。

最基础的数据结构——数组

数组我们应该都很熟悉,在C语言的学习当中,我们经常使用数组来存储同类型的数据,比如我们想存储1~100的数据,就会创建一个可以容纳100个int类型大小的一维数组。
乍一看很好用,其实在很多地方都受到了限制。比如数组无法自由改变空间大小,一旦创建就无法更改,而实际的应用中,数据的大小都是在实时发生变化的;再者,数组只可以存储一组同类型的数据,而现实生活中,一个人的信息往往要分成很多类别。而这些就导致了数组提供的接口不足以支撑复杂场景的数据处理,只会在简单的一些程序中使用到。

顺序表

  • 顺序表是线性表的一种,而线性表是具有相同特性的一类数据结构的统称
  • 线性表在逻辑结构上一定是线性的,但是在物理结构上(内存存储)不一定线性
  • 而线性又是什么呢:
    线性是指事物或对象按照一条直线或线段的方式排列或展开。在数学和计算机科学中,线性也可以指线性结构,即数据元素按照一对一的关系依次排列。线性结构中的每个元素都有一个前驱元素和一个后继元素,除了第一个元素没有前驱,最后一个元素没有后继。

    我们之前使用过数组,知道数组的排列方式,举个例子:
int a[3]={123};
//数组 a 是一个包含 3 个整型数据的整型数组。
//数组a的内存布局:a[0]-->1,a[1]-->2,a[2]-->3
//每个整型数据通常占用 4 个字节的空间,因此每个数组元素占用 4 个字节的空间。
//相邻的数组元素在内存中是连续存储的,也就是说,它们的存储空间是相连的。

经过分析,我们可以发现,数组不仅仅在逻辑上是线性的,而且在物理结构上也是线性的,而顺序表的底层结构就是数组,也就是说,顺序表也拥有数组的这一特性。

顺序表又分为两种:静态顺序表和动态顺序表

typedef int SLDataType;

//静态顺序表
typedef struct SeqList
{
	SLDataType a[10];//定长数组
	int size;//数据的有效个数
}SL;

//动态顺序表
typedef struct SeqList
{
	SLDataType* a;
	int size;//数据有效个数
	int capacity;//空间容量
}SL;

讲过数组,大家应该知道动态顺序表更好用,因为静态顺序表跟数组拥有一样的缺点,那就是空间给多了用不着,而给少了又不够用
而上面我们定义了一个变量叫SLDataType,大家应该能够发现它的用处,就是在使用顺序表时,我们可能会使用多种类型的数据,而这样使用,只需要在使用某种类型的时候将int改成别的数据类型,而不需要在程序中一个一个修改,更加简洁方便

顺序表实操

在这里插入图片描述
首先,我们需要创建一个头文件和两个源文件,头文件相当于目录,写结构的实现和声明以及函数的声明,而一个源文件写函数的具体实现,另外一个则是用来测试
如图:
在这里插入图片描述
而接下来我们就可以进行尝试:

//SeqList.h
typedef int SLDataType;

typedef struct SeqList{
	SLDataType* a;
	int size;//数据有效个数
	int capacity;//空间容量
}SL;

void SLInit(SL* s);//函数声明

//SeqList.c
#include<stdio.h>
#include"SeqList.h"

//初始化顺序表
void SLInit(SL s){
	s.a = NULL;//置为空指针
	s.size = s.capacity = 0;
}

//test.c
#include<stdio.h>
#include"SeqList.h"

void SLTest(){
	SL sl;
	SLInit(sl);
}

int main()
{
	SLTest();
	return 0;
}

我们发现,运行的时候会出错,而出错的原因就是使用了sl这个未初始化的变量。那我们就会很奇怪啊,我们明明初始化了呀,这就犯了一个经典错误:误把形参当实参,形参的改变量是无法作用到实参上的,所以要想真正达到初始化的效果,就必须传址调用而不是传值调用,接下来我们进行修改:

//SeqList.c
#include<stdio.h>
#include"SeqList.h"

//初始化顺序表
void SLInit(SL* s){
	s->a = NULL;
	s->size = s->capacity = 0;
}

//test.c
#include<stdio.h>
#include"SeqList.h"

void SLTest(){
	SL sl;
	SLInit(&sl);
}

int main()
{
	SLTest();
	return 0;
}

而此时,才是真正正确的运用

//销毁顺序表
void SLDestroy(SL* s) {
	if (s->a != NULL)//s->a要确实有动态开辟内存时,我们才能free
		free(s->a);

	s->a = NULL;//free之后要置为空指针,不free也要
	s->size = s->capacity = 0;//有效个数和空间容量都变为0
}
另外还有四个需要实现的功能:增加数据、删除数据、修改数据,查找数据

1. 增加数据
增加数据分为尾插和头插,画一个图便于理解:
在这里插入图片描述

我们先讲尾插

  • 首先,不管头插尾插,我们都需要确定一个问题,那就是内存空间的大小允不允许我们插入一个新的数据;如果空间不足,那么我们又需要扩容多少大小的空间来容纳新的数据呢?一般来说,都是扩容原来空间大小的两倍或者1.5倍
  • 有些人可能会说,为什么不能插入一个扩容一块空间?首先,如果我们要插入50个数据,那么就需要扩容50次,频繁扩容会降低程序的性能,并且如果本来扩容的那一块内存不够,又会找到另一块空间进行扩容,拷贝原来的空间到这块略大一点的空间并删除原来空间储存的内容,虽然不会导致野指针的问题,但是程序运行的时候会较为麻烦,效率低下

而讲到扩容,我们就会想到动态内存管理使用的另一个函数:realloc,它可以自由改变指针所指向的那片空间的大小。于是,我们就可以根据顺序写出头插与尾插的函数了

//判断空间是否足够,不够就扩
void SLCheckCapacity(SL* s){
	//空间不足以容纳其他数据,扩容
	if (s->size == s->capacity)
	{
		int newCapacity = s->capacity == 0 ? 4 : 2 * s->capacity;//防止初始化空间为0的情况,如果是0就给4个空间,如果不是就原来空间×2赋给新值
		//这个问题也可以在初始化的时候解决
		SLDataType* tmp = (SLDataType*)realloc(s->a, newCapacity * sizeof(SLDataType));
		//防止扩容失败将原来的指针指向的内容清空,用tmp先来
		if (tmp == NULL)
		{
			perror("realloc");
			return 1;
		}

		s->a = tmp;//扩容好再把指针指向tmp
		s->capacity = newCapacity;//没问题就把原空间乘以2的值赋给capacity
	}
}

//尾插
void SLPushBack(SL* s, SLDataType x) {

	//如果一开始s指针就为空的话
	if (s == NULL)
		return;

	SLCheckCapacity(s);
	s->a[s->size++] = x;//s—>size的值就是指针指向的后一个位置,也就是扩容内存的第一个位置,++是因为有效数据+1
}

//头插
void SLPushFront(SL* s, SLDataType x) {

	assert(s);
	//扩容
	SLCheckCapacity(s);

	//所有原有内容后移一位
	for (size_t i = s->size; i > 0; i--)
	{
		s->a[i] = s->a[i - 1];
	}
	s->a[0] = x;
	s->size++;
}

可以看到,扩容的方法十分巧妙,不仅需要防止一开始初始化数据空间为0的情况,也需要防备realloc扩容失败而导致的后果,而并不是简单的让原空间*2就可以了
而头插和尾插,也因为插入的方式不同,一个是在原本a[s->size]的位置处插入,而另一个则是所有原有内容后移之后,直接空出第一个位置给插入。不过都不能忘记的地方就是size要++,毕竟有效数据+1了

2.减少数据
减少数据也分为尾删和头删,同样画个图给大家便于理解
在这里插入图片描述

删除元素比起添加元素少了一个问题,那就是空间大小是否足够,但另一个问题就是需要注意空间里是否还有足够的元素删除,这是删除元素的问题
尾删很简单,只需要让有效数据的个数-1,就可以达到删除数据的效果,而头删则需要移动数据的位置达到效果

bool SLIsEmpty(SL* ps)//判断是否还有有效数据
{
	assert(ps);
	return ps->size == 0;
}

void SLPopBack(SL* ps)
{
	assert(ps);
	assert(!SLIsEmpty(ps)); // 断言检查是否为空

	ps->size--; // 减少 size,相当于删除最后一个元素
}

void SLPopFront(SL* ps)
{
	assert(ps);
	assert(!SLIsEmpty(ps)); // 断言检查是否为空

	// 将后面的元素向前移动一个位置
	for (size_t i = 0; i < ps->size-1; i++)
	{
		ps->a[i] = ps->a[i + 1];
	}

	ps->size--; // 减少 size,相当于删除第一个元素
}

3.指定位置之前插入/删除数据
刚才我们写的都是头插尾插,头删尾删,位置上是无法自己选择的,但在一些数据结构的使用中,我们需要在指定位置插入或删除数据,这时候之前我们使用的函数效率就比较低下了,这时候,我们就需要研究如何在指定位置之前插入/删除数据

void SLInsert(SL* ps, int pos, SLDataType x)//指定位置之前插入
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size);//0和ps->size就是头插和尾插

	SLCheckCapacity(ps);//扩容

	for (size_t i = ps->size-1; i > pos-1; i--)
	{
		ps->a[i + 1] = ps->a[i];//从后往前,逐个后移
	}

	ps->a[pos] = x;//赋值
	ps->size++;//有效数据+1
}

void SLErase(SL* ps, int pos)//指定位置之前删除
{
	assert(ps); // 断言检查是否为NULL
	assert(!SLIsEmpty(ps)); // 断言检查是否为空
	assert(pos >= 0 && pos < ps->size);

	for (size_t i = pos; i < ps->size - 1; i++)
	{
		ps->a[i] = ps->a[i + 1]; // 将后面的元素向前移动一个位置
	}

	ps->size--; // 减少 size,相当于删除指定位置的元素
}

4.查找指定元素
最后一个函数就是用来在顺序表中查找指定元素,并返回一个布尔值表示是否找到该元素

bool SLFind(SL* ps, SLDataType x)
{
	assert(ps);
	for (size_t i = 0; i < ps->size; i++)
	{
		if (ps->a[i] == x)//找到了
			return true;
	}

	return false;//没找到
}

以上就是顺序表的一个简单介绍,并且在集齐这些函数之后,顺序表的功能也已经基本完备,可以正常使用,而之后的文章中,我将会给大家展示顺序表的一些简单运用,希望大家在看完文章之后也能够理解!

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值