提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
数据结构是学习计算机编程中十分重要的一个方面,今天我们来介绍一下,其中比较基础的顺序表的内容。
一、数据结构是什么?
数据结构定义
数据结构(data structure)是带有结构特性的数据元素的集合,它研究的是数据的逻辑结构和数据的物理结构以及它们之间的相互关系,并对这种结构定义相适应的运算,设计出相应的算法,并确保经过这些运算以后所得到的新结构仍保持原来的结构类型。简而言之,数据结构是相互之间存在一种或多种特定关系的数据元素的集合,即带“结构”的数据元素的集合。“结构”就是指数据元素之间存在的关系,分为逻辑结构和存储结构。
为什么要使用数据结构
数据结构的作用主要包括组织和存储数据、提高算法效率、解决复杂问题、提供抽象和模型以及支持数据共享和交换。
也就是说数据结构可以帮助我们优化自己的代码,使之更加高效,更能解决复杂问题。
二、顺序表
1.顺序表分类
顺序表可以分为两类,一个是静态顺序表,一个是动态顺序表。顾名思义,静态顺序表是静止的,一旦确定后,其大小是无法改变的。而动态顺序表是可以根据需要动态调整的。两种顺序表在逻辑结构和物理结构上都是连续的,即他们的内存空间是连续的。
静态顺序表
静态顺序表是一种线性表的顺序存储结构,它使用定长数组来存储元素。在初始化时,静态顺序表的空间大小就已经确定,之后不能随意增加或缩小。这种数据结构适合在预先知道所需存储数据量的情况下使用,因为如果初始分配的空间太小,可能会导致存储空间不足;而如果分配的空间太大,则可能会造成空间浪费。
typedef int SlDataType//将需要存储的数据类型重命名,方便以后替换为其他数据类型
typedef struct SeqList
{
SlDataType arr[100];//起始状态大小确定
int size;//数组成员个数
int capacity;//数组大小
}
在我们知晓需要存储成员个数时可以使用静态顺序表,但很多时候我们是不知道的,所以静态顺序表是不方便的。我们接下来介绍更加灵活的动态顺序表。这两者的使用是差不多的。
动态顺序表
动态顺序表是一种线性数据结构,它使用动态分配的数组来存储元素。这意味着在程序执行过程中,根据需要动态地分配或释放存储空间。与静态顺序表相比,动态顺序表的主要优点是它可以根据实际需要灵活地调整存储空间的大小,避免了空间浪费或不足的问题。动态顺序表的实现包括初始化、销毁、容量检查、尾插、尾删、头插、头删、查找、指定下标插入和删除等接口。当动态顺序表的容量耗尽时,它会自动进行扩容,以适应更多数据的存储。
typedef int SlDataType//将需要存储的数据类型重命名,方便以后替换为其他数据类型
typedef struct SeqList
{
SlDataType* arr;//起始状态大小不确定,所以使用指针,方便动态动态内存开辟
int size;//数组成员个数
int capacity;//数组大小
}
下面我们来展示动态顺序表的初始化,增删查等功能。主要涉及到的知识点是结构体,动态内存分配,指针。
2.动态顺序表相关函数
1.顺序表初始化
void SlInit(Sl*ps)
{
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
初始化顺序表实际上就是初始化创建的这个结构体的成员,结构体有三个成员,第一个指针在不知道的指向时,先置空指针,规避野指针的出现,因为数组的大小没有创建,所以成员大小和数组大小都先设置为零,(我们所使用的结构体都是上面动态顺序表创建的结构体)。这样初始化部分就结束了。
2.顺序表销毁
void SlDestroy(Sl* ps)
{
if (ps->arr)
{
free(ps->arr);
}
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
因为我们的数组是动态开辟出来的,所以我们要通过free函数将其释放。首先判断arr指针是否创建,若已创建,则将其释放,再把它置为空指针,最后把size和capacity设为0,就完成了销毁,这里最重要的是释放空间,防止内存泄漏。
3.顺序表检验大小
void SlCheckCapacity(Sl* ps)
{
if (ps->capacity == ps->size)
{
int newCapacity = ps->capacity==0?4:2*ps->capacity;//判断原顺序表是否为空
SlDataType* tmp=(SlDataType*)realloc(ps->arr, newCapacity * sizeof(SlDataType));
//申请空间,一般申请原大小的2到3倍
if (tmp == NULL)
{
perror("realloc");
exit(1);
}//判断是否申请成功
ps->arr = tmp;
ps->capacity = newCapacity;
tmp = NULL;
}
}
这个函数并不是我们直接使用的函数,因为在后面的其他函数现实时,很多情况下需要检验顺序表大小是否够用判断是否需要开辟空间,所以我们提前写好,方便使用。
首先,判断顺序表空间大小(capacity)是否等于数组成员数量(size),若等于则数组已满,需开辟空间,并且判断capacity是否为0,即判断顺序表是否建立。如果没建立,先开辟4字节空间,如果已建立,则扩容为原来两倍。(这里也可以预先给数组几个字节的空间,这里就不用再判断了)。接着我们使用realloc函数申请空间,因为realloc申请空间不一定在原地址,所以我们用一个临时指针tmp接受,并判断是否开辟成功,再将指针赋给arr,最后将capacity改为新大小即可。
4.顺序表打印
//顺序表打印
void SlPrint(Sl s)
{
int i = 0;
for (i = 0; i < s.size; i++)
{
printf("%d ", s.arr[i]);
}
printf("\n");
}
顺序表打印是为了让我们更直观看到现象,打印方法就是正常的遍历数组循环打印,这里不再赘述。
5.尾插
void SlPushBack(Sl* ps, SlDataType x)
{
assert(ps);
SlCheckCapacity(ps);
ps->arr[ps->size++] = x;//赋值,并size++
}
顾名思义就是在顺序表尾部插入数据,首先要判断指针是否为空,这里我们使用assert断言(也可以使用if判断)。然后检查顺序表大小是否够用,最后在顺序表末尾(即size处)插入数据即可。插入后size++。上面的代码是后置++,先使用后++。
6.头插
void SlPushFront(Sl* ps, SlDataType x)
{
assert(ps);
SlCheckCapacity(ps);
int i = 0;
for (i = ps->size; i>0; i--)
{
ps->arr[i] = ps->arr[i-1];//从后向前,将数组每一位向后移一位
}
ps->arr[0] = x;
ps->size++;
}
头插相对麻烦,需要将数据向后移动再插入。首先判断指针有效性和顺序表空间,之后再将数组元素后移,因为后移牵扯覆盖的问题所以我们从后向前移,最后在下标为0处插入数据,size++即可。
7.尾删
void SlPopBack(Sl* ps)
{
assert(ps);
assert(ps->size);
ps->size--;
}
尾删就比较简单,但是我们需要理解,虽说我们说删除了它,但是这个数据依然存在,只是我们不去访问它,如同他被删除了一样。所以这里我们进行指针和空间判断后,直接将size–,不再访问最后一个数据,就达到尾删的效果。
8.头删
void SlPopFront(Sl* ps)
{
assert(ps);
assert(ps->size);
int i = 0;
for (i = 0; i<ps->size-1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
头删和头插有相同之处,我们将数据前移,将第二个数据覆盖第一个数据即可达到删除第一个数据的效果,后面的数据依次前移,进行覆盖,达到头删目的。
9.在指定位置前插入数据
void SlInsert(Sl* ps, int pos, SlDataType x)
{
assert(ps);//判断不为空
assert(pos >= 0 && pos <= ps->size);//判断插入位置合法
SlChechCapacity(ps);
int i = 0;
for (i = ps->size; i > pos; i--)
{
ps->arr[i] = ps->arr[i - 1];//向后移
}
ps->arr[pos] = x;
ps->size++;
}
形参中ps是结构体指针,pos是插入位置,x是插入数据。首先判断指针有效和空间大小,这里还需判断插入位置是否有效,不能小于零,不能大于size,之后将pos后的数据全部后移,留出位置,插入数据即可。最后size++。
10.指定位置删除
void SlErase(Sl* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
int i = 0;
for (i = pos; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
指定位置删除和头删相似,都是使用数据覆盖,这里先确定数据的位置然后将后面的数据向前覆盖达到删除原来数据的目的,最后size–即可。因为size改变所以最后的数据等于删除了。(如下图最后的4).
11.查找数据
int SlFind(Sl* ps, SlDataType x)
{
assert(ps);
int i = 0;
for (i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
{
return i;//找到了,返回下标
}
}
return -1;//没找到
}
这里的查找数据就很简单了,只要遍历数组进行比较就行,如果找到了就返回下标,如果没找到就返回-1,之后我们就可以根据返回值判断是否找到。
头文件
在我们写好函数时,把他们放在头文件中,在包含进主文件中就可以使用了,以上就是动态顺序表的底层函数。
总结
以上就是今天要讲的内容,本文简单介绍了顺序表的实现,关于静态顺序表的内容和动态的类似,可以根据上文研究,希望这篇文章能帮到你们。