顺序表详解

目录

前言

1.顺序表的概念及结构

​编辑

1.1线性表的概念

1.2顺序表与数组的区别

1.3接口函数

2.顺序表的分类

2.1静态顺序表

2.2动态顺序表

3.实现动态顺序表

3.1 SLCheckCapacity

3.2 SLPushBack

3.3 SLPopBack

3.4 SLPushFront

3.5 SLPopFront

3.6 SLInsert

3.7 SLErase

3.8 SLFind


前言

顺序表是咱学习数据结构时接触到的第一种数据结构。

在学习顺序表之前,我们需要先弄明白一个问题:何为数据结构?

数据结构,单从字面上来讲就是数据的结构


设想一下,内存中有一大堆数据(假设这些数据是数字1、2、3...等等)

然后我们想在这堆数据中找到并拿出其中一个数据 “3” ,要怎么找呢?

这时候就需要用到数据结构的知识——将数据有序的存储起来(排序),

接着就只需要访问对应的序列就能找到该数据了。


总结一下上面这个例子,我们可以得知数据结构有两个特性:

1.能够储存数据

2.储存的数据方便查找(删除、或者修改)

说的通俗易懂一点,数据结构就是一种方便存储数据和访问数据的代码结构。

1.顺序表的概念及结构

数据结构有很多种类,线性表就是一种数据结构,而今天我要讲的顺序表就是一种顺序表。

1.1线性表的概念

什么是线性表?

简单的说,线性表就是逻辑上(物理上不一定)呈线性关系(连续)的数据结构。

举个例子:

我们平时用的数组,逻辑上就是成线性关系的(数组的物理上也呈线性关系)。

而接下来要讲解的顺序表的底层逻辑就是数组。

你或许会有点怀疑:真的有物理上不呈线性关系的线性表吗?

有!这种线性表被称为链表,但是我今天不讲这个。

链表的抽象概念大抵如下图:

1.2顺序表与数组的区别

顺序表的底层结构是数组,对数组的封装,实现了常用的增删改查等接⼝。

说白了就是更灵活,更多功能,更牛b的数组。

但是这里又提到了一个接口的概念,我就顺便讲一下吧。

1.3接口函数

所谓接口函数,就是将函数封装成一个个接口,实现函数功能模块化,每种函数实现一种功能,需要实现这种功能的时候就调用这个函数。

举个例子:

我们平时用的计算器有加减乘除等等功能。

如果用代码写一个计算器出来,那么就需要封装各种功能函数。

比如:实现加法的函数Add,实现减法功能的函数Sub...等等。

而我们将这种被封装为实现特定功能的函数称为接口函数。

2.顺序表的分类

顺序表可以分为两大类:静态顺序表和动态顺序表。

2.1静态顺序表

概念:使用定长数组储存元素(不可扩容)。

缺陷:空间给少了不够用,空间给多了会浪费内存。

例如:

2.2动态顺序表

静态顺序表使用的是定长数组,动态顺序表使用的则是柔性数组,(可扩容)

动态顺序表的特性弥补了静态顺序表的缺陷。

需要注意的是,图中申请空间时使用的函数是realloc,考虑到可能有些人不了解,接下来我就

简单讲解一下这个函数。

void* realloc (void* ptr, size_t size);

realloc - C++ Reference (cplusplus.com)

该函数共有两个参数:①指针ptr        ②无符号整数 size

重要内容大概就是:

重新申请一块size字节大小的空间,并将指针ptr指向空间中的数据拷贝到新申请的空间。

最后返回这个新申请空间的地址。(原来的那个旧的内存块会被free掉)

因此,就需要一个temp指针来保存返回来的新空间的地址,再将temp的值赋给指针a。

3.实现动态顺序表

一个完整的动态顺序表,需要具备有增删查改等功能,因此咱需要写各种接口函数来完善其功能。

需要的接口函数如下:

// 动态顺序表 -- 按需申请
#define INIT_CAPACITY 4
typedef struct SeqList
{
    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);//查找某个数据的下标

共11个接口函数。

对于以上11个接口函数,我着重讲解后面8个接口函数。

前三个接口函数的代码我直接给出来,大伙自己看看就好。

//初始化和销毁
void SLInit(SL* ps)
{
	ps->a = NULL;
	ps->capacity = 0;
	ps->size = 0;
}

void SLDestroy(SL* ps)
{
	free(ps->a);
	ps->size = 0;
	ps->capacity;
}

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

问:为什么函数形参要用指针?

答:假设创建一个顺序表:SL seqlist;

       若要实际改变顺序表的结构,就必须往函数中传入顺序表的指针

       例如:SLPushBack(&seqlist,1);

问:既然要改变顺序表的时候必须要传指针,那么为什么打印顺序表也传指针?

答:当传入的是结构体而不是指针时,函数内会临时生成一个结构体变量并将实参的数值

        赋值给该临时变量,此时就会消耗多余的内存。而如果使用的是指针,指针的大小                永远为4/8,不用担心内存问题。


3.1 SLCheckCapacity

该函数为扩容函数,其功能顾名思义:当数组空间存满时将空间扩大到原来的两倍。

代码实现:

//扩容
void SLCheckCapacity(SL* ps)
{
	if (ps->size == ps->capacity)//当空间存满时
	{
        //若capacity等于0,就将其值初始化为4(宏定义),若不等于0,则扩容为之前的两倍
		int newcapacity = ps->capacity == 0 ? INIT_CAPACITY : 2 * ps->capacity;
		SLDataType* temp = realloc(ps->a, newcapacity * sizeof(SLDataType));
		ps->a = temp;
		ps->capacity = newcapacity;
	}
}

3.2 SLPushBack

接下来的这些插入/删除接口函数才是这篇博客的重头戏!

尾插的概念:往数组末尾插入数据。

画图分析:

代码实现:

//尾插
void SLPushBack(SL* ps, SLDataType x)
{
	assert(ps);//当传入的指针ps为空指针时断言报错
	SLCheckCapacity(ps);//检查数组空间有没有满
	ps->a[ps->size] = x;
	ps->size++;
}

3.3 SLPopBack

该函数的功能就是删除数组末尾的第一个数据。

其实这个函数的实现是非常简单的,只需要将size--即可。

将size--后,数组就无法访问到这个数据,当尾插新数据时,这个数据就会被新数据覆盖掉。

假设对上图顺序表进行尾删,尾删一次size就会减少1。

如此一来,该顺序表只能访问到1、2、3三个数据,而访问不到数据4。

当尾插新数据时,数据4就会被新数据覆盖。

最后再来看看代码实现,可谓是极其简单!

//尾删
void SLPopBack(SL* ps)
{
	assert(ps);//当指针ps为空是断言报错
	assert(ps->size);//当顺序表中没有数据时断言报错
	ps->size--;
}

3.4 SLPushFront

函数功能:在数组的最开头处插入数据。

例如:

可以看到,头插函数的执行可以分为两步:

将数组内所有有效数据全部往后挪动一位。

在数组最开头处插入数据。

代码实现:

//头插
void SLPushFront(SL* ps, SLDataType x)
{
	SLCheckCapacity(ps);
    //将所有数据往后移一位
	for (int i = ps->size; i > 0; i--)
	{
		ps->a[i] = ps->a[i - 1];
	}
    //将数组的第一个数据覆盖为新数据x
	ps->a[0] = x;
	ps->size++;//别忘了size++
}

3.5 SLPopFront

我之前说过尾删的接口函数十分简单,那么头删呢?

头删会比尾删更复杂一点点,因为头删比尾删多了一步:将数组内所有数据往前挪一位。

然后再size--

代码逻辑如下图:

代码实现:

void SLPopFront(SL* ps)
{
	assert(ps);//当传入的指针为空时断言报错
	assert(ps->size);//当顺序表内没有数据时断言报错
    //将数据全体往后挪动一位
	for (int cur = 0; cur < ps->size - 1; cur++)
	{
		ps->a[cur] = ps->a[cur + 1];
	}
	ps->size--;
}

3.6 SLInsert

该函数的功能为:在指定的下标处插入一个数据

相较于头插尾插函数,该函数更具灵活性,代价是需要传入要插入位置的下标pos。

示例:

当需要在下标为1的位置处插入数据时,就需要传入pos=1

即调用函数SLInsert(&seqlist,1,100);

需要注意的是,在pos位置插入数据时,需要将pos处及后面的数据往后挪一格。

因此需要编写一个循环程序来执行后移动作。

实际代码如下:

//指定位置插入数据
void SLInsert(SL* ps, int pos, SLDataType x)
{
	assert(ps);//断言
	assert(pos >= 0 && pos<=ps->size);//插入的下标必须大于等于0,且小于等于size
	SLCheckCapacity(ps);//检查是否需要扩容
    //将pos及后面的数据往后挪一格
	for (int i = ps->size; i > pos; i--)
	{
		ps->a[i] = ps->a[i - 1];
	}
	ps->a[pos] = x;
	ps->size++;
}

3.7 SLErase

该函数的功能与SLInsert相反,是在指定位置删除一个数据。

示例:

同样的,编写SLErase函数时需要注意,要把pos后面的数据全体往前移动一格。

重点注意移动数据循环结构!!

//指定位置删除数据
void SLErase(SL* ps, int pos)
{
	assert(ps);
	assert(pos<ps->size && pos >= 0);//pos的位置要规范
	assert(ps->size);//当顺序表数据为空时删除会报错
    //pos往后的数据全体往前挪一格
	for (int i = pos; i < ps->size; i++)
	{
		ps->a[i] = ps->a[i + 1];
	}
	ps->size--;
}

3.8 SLFind

该函数的功能为查找指定数据在数组中的位置,并返回其下标。

代码思路:

for循环遍历数组,当数组元素等于目标数据x时,返回该下标。

若没有找到,则返回-1。

//查找
int SLFind(SL* ps, SLDataType x)
{
	for (int i = 0; i < ps->size; i++)
	{
		if (ps->a[i] == x)
			return i;
	}
	return EOF;
}

使用SLFind查找下标,再配合SLInsert或者SLErase接口函数,就可以灵活增删数据。

SLInsert和SLErase甚至可以直接替代掉头插头删、尾插尾删的接口函数。

例如:

//用SLInsert/SLErase实现版本
//头部插入删除 / 尾部插入删除

void SLPushBack(SL* ps, SLDataType x)
{
	SLInsert(ps, ps->size, x);
}

void SLPopBack(SL* ps)
{
	SLErase(ps, ps->size-1);
}

void SLPushFront(SL* ps, SLDataType x)
{
	SLInsert(ps, 0, x);
}

void SLPopFront(SL* ps)
{
	SLErase(ps, 0);
}

看完了觉得有用的姥爷们求求您们点个免费的赞和关注,这对我真的很重要!非常感谢大家!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值