哈喽大家好,我是野生的编程萌新,首先感谢大家的观看。数据结构的学习者大多有这样的想法:数据结构很重要,一定要学好,但数据结构比较抽象,有些算法理解起来很困难,学的很累。我想让大家知道的是:数据结构非常有趣,很多算法是智慧的结晶,我希望大家在学习数据结构的过程是一种愉悦的心情感受。因此我开创了《数据结构》专栏,在这里我将把数据结构内容以有趣易懂的方式展现给大家。
线性表,听名字我们就能感受到,是具有线一样性质的的表。举个鲜明的例子,当我们在上体育课时,一个班的人都排好队,每一排都有一个打头的,一个收尾的,每个人都知道自己前一个是谁,后面一个是谁,这样就如同一根线一样将他们串在了一起,这就可以称为线性表。线性表是数据结构中最基本的一种,它是由n(n>=0)个具有相同类型的元素组成的有限序列。具体来说,线性表由多个元素组成,每个元素都有一个唯一的前驱元素(除第一个元素外)和唯一的后继元素(除最后一个元素外)。这里我们要强调两个点:
线性表逻辑结构图大致如下:

线性表有两种存储方式,一种是顺序存储结构,即将元素依次存储在一块连续的内存中;另一种是链式存储结构,即将元素存储在不连续的内存中,通过指针将它们连接起来。我们前面学过的数组也是顺序表类型,除数组外线性表应用场景还有链表、栈、队列等。线性表是一种常见的数据结构,它具有一对一的关系、存储方式灵活、操作简单高效等特点,广泛应用于各种领域。线性表的优点是操作简单、效率高,但是它的缺点是插入和删除操作需要移动大量的元素,时间复杂度较高。在这一篇博客中我们主要介绍线性表中的顺序存储结构中的顺序表。
顺序表是一种线性数据结构,它通过一组连续的存储单元来存储数据元素,元素之间的关系是一对一的关系。顺序表中的元素在内存中是按照其逻辑顺序依次存放的,可以通过元素在顺序表中的位置(下标)来访问和操作元素。听着怎么感觉顺序表这么像数组呢?顺序表的底层结构就是数组,实现了对数组的封装,实现了常用的增删查改等操作,二者主要区别如下:
顺序表有两种实现方式:静态顺序表和动态顺序表。静态顺序表是指在程序设计时,为顺序表分配了一段固定大小的存储空间,不允许进行扩容或缩容的操作。由于静态顺序表的大小是固定的,所以需要在定义时预估数据的最大规模,这可能会导致存储空间的浪费或无法满足需求的问题。下面为静态顺序表:

动态顺序表是指在程序运行时,可以根据实际的数据规模进行动态扩容或缩容的操作。动态顺序表可以根据需要动态调整存储空间的大小,有效地利用了内存空间。但是,在扩容或缩容时需要进行数据的搬迁,会带来一定的时间开销。下面为动态顺序表:

顺序表适用于需要频繁访问元素或根据下标查找元素的场景。在元素的插入和删除操作较少,或者已知数据规模的情况下,使用静态顺序表更为合适。在元素的插入和删除操作较多,或者数据规模不确定的情况下,使用动态顺序表更为合适。因为静态顺序表的使用确实不常见,极少情况才会使用静态顺序表,在这里我着重和大家介绍动态顺序表。
动态顺序表的实现我们需要些一些函数来完成顺序表的初始化、销毁,插入、删除等操作,所以这里我们采用多文件的方式来实现。每一部分的函数都会详细讲解。下面为顺序表的结构代码:
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* a;
int size; //顺序表有效的个数
int capacity; //顺序表的空间大小
}SL;我们在主函数通过顺序表的结构来创建一个顺序表,创建完顺序表我们要干什么呢?当然是初始化啦,我们在初始化。在这里我们将顺序表的地址传递给创建的顺序表初始化函数,用一个指针接收顺序表的地址,通过对指针的解引用我们就可以修改顺序表。在顺序表初始化的时候我们要满足:将线性表的长度和容量都设置为0,表示线性表当前为空,将分配的内存空间的起始地址也置空处理。顺序表的初始化函数代码如下:
void SLInit(SL* ps)
{
ps->a = NULL;
ps->size = 0;
ps->capacity = 0;
}顺序表的销毁是指将顺序表中的数据元素清空,并释放顺序表占用的存储空间,使之成为一个空表。具体步骤为下:首先,释放顺序表中存储数据元素的内存空间,然后,清空顺序表中的数据元素。注意,顺序表的销毁操作可能会导致数据丢失,所以在执行销毁操作之前,应该确保不再需要使用该顺序表中的数据。顺序表的销毁函数代码如下:
void SLDestroy(SL* ps)
{
if (ps->a)
free(ps->a);
ps->size =0;
ps->capacity = 0;
}在这一小节主要和大家讲解顺序表的尾插、尾删、头插、头删、任意位置插入、任意位置删除操作。我们在顺序表的插入操作时,我们要先判断顺序表是否已满。如果顺序表已满,则需要进行扩容操作,将顺序表的容量增加一定的大小。所以在介绍顺序表的插入之前我们先写一个函数来为我们判断顺序表是否已满。我们要怎样判断顺序表是否满了呢?只需要判断顺序表中的有效元素个数和空间容量是否相等,相等就是满了呗。这个函数实现如下:
void SLCheckcapacity(SL* ps)
{
if (ps->size == ps->capacity)//空间已经不足以插入数据
{
int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLDataType* tmp = (SLDataType*)realloc(ps->a, newcapacity * sizeof(SLDataType));
if (tmp == NULL)
{
perror("realloc");
return 1;
}
ps->a = tmp;
ps->capacity = newcapacity;
}
}在上面的代码中我们使用了一个三目运算符来判断顺序表的空间容量是否为0,如果为0,那么就将它的空间容量修改为4,若不是0,那么就将顺序表的空间容量进行2倍扩容(因为在C++的库中就存在顺序表扩容函数,它扩容就是扩大2倍,这里为了对标那个函数,所以就进行2倍扩容)。扩容不一定百分百成功,所以下面对扩容结果进行判断,如果扩容失败就返回realloc失败。
顺序表尾插操作是指将一个新元素插入到顺序表的末尾。顺序表的尾插步骤如下:
尾插操作的时间复杂度为O(1),即常数时间复杂度,因为无论顺序表的长度是多少,尾插操作都只需要进行一次操作即可完成。顺序表的尾插函数实现代码如下:
void SLpushBack(SL* ps,SLDataType x)
{
assert(ps != NULL);//判断
//1.空间足够用直接尾插
//2.空间不够现申请空间,再进行尾插
SLCheckcapacity(ps);
ps->a[ps->size++] = x;//插入数据
}顺序表的尾删操作指的是删除顺序表中的最后一个元素。顺序表的删除基本可以分为两个步骤:
为什么我会说基本可以分为两个步骤呢?当然是因为还有能偷懒的写法(●'◡'●),我们直接将顺序表的有效长度减一不直接就能实现了吗。 尾删时我们还要考虑一个因素顺序表是不是空的,所以我们写一个函数来判断一下:
bool SLisEmpty(SL* ps)
{
assert(ps);
return ps->size == 0;
}首先老样子判断只想顺序表的指针是否指向空值,下面如果ps指向顺序表中的size等于0,那么返回true,否则返回false。我们来实现一下顺序表尾删函数:
void SLpopBack(SL* ps)
{
assert(ps != NULL);
assert(!SLisEmpty(ps));
ps->size--;
}顺序表的头插操作即在顺序表的第一个位置插入一个元素。顺序表的头插步骤如下:
我们来实现一下顺序表的头插函数:
void SLpushFront(SL* ps, SLDataType x)
{
assert(ps != NULL);//判断
//插入情况和尾插相似
SLCheckcapacity(ps);
for (SLDataType i = ps->size; i > 0; i--)
{
ps->a[i] = ps->a[i - 1];
}
ps->a[0] = x;
ps->size++;
}头插操作的时间复杂度为O(n),其中n为顺序表中已有元素的个数。因为需要将已有元素后移一位,所以操作的时间复杂度与顺序表中已有元素的个数成正比。
顺序表的头删操作是指删除顺序表中第一个元素的操作。顺序表的头删的步骤如下:
我们来实现一下顺序表的头删函数:
void SLpopFront(SL* ps)
{
assert(ps != NULL);
for (SLDataType i = 0; i < ps->size - 1; i++)
{
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}头删操作的时间复杂度也为O(n),因为需要将后面的元素往前移动一位,所以操作的时间复杂度与顺序表中已有元素的个数成正比。
顺序表中的任意插入操作是指在任意位置插入一个元素。任意位置插入的具体步骤为下:
我们来实现一下顺序表的任意插入函数:
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
SLCheckcapacity(ps);
int end = ps->size;
while (end >= pos)
{
ps->a[end] = ps->a[end-1];
--end;
}
ps->a[pos-1] = x;
ps->size++;
}当然有的朋友喜欢任意位置之后插入,其实现代码如下:
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
SLCheckcapacity(ps);
int end = ps->size-1;
while (end >= pos)
{
ps->a[end+1] = ps->a[end];
--end;
}
ps->a[pos] = x;
ps->size++;
}顺序表的任意位置删除操作是指在顺序表中删除指定位置的元素。任意位置删除的具体操作步骤如下:
我们来实现一下任意位置删除函数:
void SLErase(SL* ps, int pos)
{
assert(ps != NULL);
assert(pos >= 0 && pos < ps->size);
for (SLDataType i = pos; i < ps->size - 1; i++)
{
ps->a[i-1] = ps->a[i];
}
ps->size--;
}任意位置之后删除代码实现:
void SLErase(SL* ps, int pos)
{
assert(ps != NULL);
assert(pos >= 0 && pos < ps->size);
for (SLDataType i = pos; i < ps->size - 1; i++)
{
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}SeqList.h文件:
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* a;
int size; //顺序表有效的个数
int capacity; //顺序表的空间大小
}SL;
void SLInit(SL* ps);//顺序表初始化
void SLDestroy(SL* ps);//顺序表的删除
void SLpushBack(SL* ps,SLDataType x);//尾插数据
void SLpushFront(SL* ps, SLDataType x);//头插数据
void SLpopBack(SL* ps);//尾删数据
void SLpopFront(SL* ps);//头删数据
void SLInsert(SL* ps, int pos, SLDataType x);//任意位置插入数据
void SLErase(SL* ps, int pos);//任意位置删除数据SeqList.c文件:
#include"SeqList.h"
void SLInit(SL* ps)
{
ps->a = NULL;
ps->size = ps->capacity = 0;
}
void SLDestroy(SL* ps)
{
if (ps->a)
free(ps->a);
ps->size = ps->capacity = 0;
}
void SLCheckcapacity(SL* ps)
{
if (ps->size == ps->capacity)//空间已经不足以插入数据
{
int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLDataType* tmp = (SLDataType*)realloc(ps->a, newcapacity * sizeof(SLDataType));
if (tmp == NULL)
{
perror("realloc");
return 1;
}
ps->a = tmp;
ps->capacity = newcapacity;
}
}
void SLpushBack(SL* ps,SLDataType x)
{
assert(ps != NULL);//判断
//1.空间足够用直接尾插
//2.空间不够现申请空间,再进行尾插
SLCheckcapacity(ps);
ps->a[ps->size++] = x;//插入数据
}
void SLpushFront(SL* ps, SLDataType x)
{
assert(ps != NULL);//判断
//插入情况和尾插相似
SLCheckcapacity(ps);
for (SLDataType i = ps->size; i > 0; i--)
{
ps->a[i] = ps->a[i - 1];
}
ps->a[0] = x;
ps->size++;
}
bool SLisEmpty(SL* ps)
{
assert(ps);
return ps->size == 0;
}
void SLpopBack(SL* ps)
{
assert(ps != NULL);
assert(!SLisEmpty(ps));
ps->size--;
}
void SLpopFront(SL* ps)
{
assert(ps != NULL);
for (SLDataType i = 0; i < ps->size - 1; i++)
{
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}
void print(SL* ps)
{
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
SLCheckcapacity(ps);
int end = ps->size-1;
while (end >= pos)
{
ps->a[end+1] = ps->a[end];
--end;
}
ps->a[pos] = x;
ps->size++;
}
void SLErase(SL* ps, int pos)
{
assert(ps != NULL);
assert(pos >= 0 && pos < ps->size);
for (SLDataType i = pos; i < ps->size - 1; i++)
{
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}test.c文件:
#include"SeqList.h"
void Test()
{
SL sl;
SLInit(&sl);
SLpushBack(&sl, 1);
SLpushBack(&sl, 2);
SLpushBack(&sl, 3);
SLpushBack(&sl, 4);
SLpushBack(&sl, 5);
print(&sl);
SLErase(&sl, 2);
print(&sl);
SLInsert(&sl, 2, 6);
print(&sl);
}
int main()
{
Test();
return 0;
}我这里简单写一个函数来测试,测试结果为:
