前言:
哈喽大家好,我是野生的编程萌新,首先感谢大家的观看。数据结构的学习者大多有这样的想法:数据结构很重要,一定要学好,但数据结构比较抽象,有些算法理解起来很困难,学的很累。我想让大家知道的是:数据结构非常有趣,很多算法是智慧的结晶,我希望大家在学习数据结构的过程是一种愉悦的心情感受。因此我开创了《数据结构》专栏,在这里我将把数据结构内容以有趣易懂的方式展现给大家。
1.线性表的定义
线性表,听名字我们就能感受到,是具有线一样性质的的表。举个鲜明的例子,当我们在上体育课时,一个班的人都排好队,每一排都有一个打头的,一个收尾的,每个人都知道自己前一个是谁,后面一个是谁,这样就如同一根线一样将他们串在了一起,这就可以称为线性表。线性表是数据结构中最基本的一种,它是由n(n>=0)个具有相同类型的元素组成的有限序列。具体来说,线性表由多个元素组成,每个元素都有一个唯一的前驱元素(除第一个元素外)和唯一的后继元素(除最后一个元素外)。这里我们要强调两个点:
- 首先它是一个序列,也就是说,元素之间是有顺序的,若存在多个元素,则第一个元素无前驱,最后一个元素无后继,其他元素都有且仅有一个前驱和后继。如果我们在站队时有同学请假,他的位置将会空出来,那就不能排成一队了(没有补位滴情况)。
- 线性表还强调有限,我们的班级人数都是有限的,元素个数当然也是有限的。事实上,在计算机处理的对象都是有限的,那种无限的数列,只存在于数学的概念中。
线性表逻辑结构图大致如下:
线性表有两种存储方式,一种是顺序存储结构,即将元素依次存储在一块连续的内存中;另一种是链式存储结构,即将元素存储在不连续的内存中,通过指针将它们连接起来。我们前面学过的数组也是顺序表类型,除数组外线性表应用场景还有链表、栈、队列等。线性表是一种常见的数据结构,它具有一对一的关系、存储方式灵活、操作简单高效等特点,广泛应用于各种领域。线性表的优点是操作简单、效率高,但是它的缺点是插入和删除操作需要移动大量的元素,时间复杂度较高。在这一篇博客中我们主要介绍线性表中的顺序存储结构中的顺序表。
2.顺序存储结构——顺序表
顺序表是一种线性数据结构,它通过一组连续的存储单元来存储数据元素,元素之间的关系是一对一的关系。顺序表中的元素在内存中是按照其逻辑顺序依次存放的,可以通过元素在顺序表中的位置(下标)来访问和操作元素。听着怎么感觉顺序表这么像数组呢?顺序表的底层结构就是数组,实现了对数组的封装,实现了常用的增删查改等操作,二者主要区别如下:
- 结构差异:顺序表是通过一段连续的存储空间存储元素的线性表,而数组则是一种普通的数据结构,可以存储不连续的元素。
-
动态性:顺序表的长度可以动态调整,当元素数量超过当前存储空间时,可以进行扩容;而数组的长度是固定的,需要在初始化时指定长度。
-
访问效率:由于顺序表的元素是连续存储的,所以在存取元素时效率较高,可以通过下标直接访问;而数组由于是按照索引存储,需要通过索引值来访问元素。
-
插入和删除操作效率:顺序表的插入和删除操作需要移动元素,所以效率较低;而数组的插入和删除操作由于不需要移动元素,所以效率较高。
-
空间占用:顺序表的存储空间由于需要动态调整,所以可能存在一定的空间浪费;而数组的存储空间是固定的,不会出现空间浪费的情况。
顺序表有两种实现方式:静态顺序表和动态顺序表。静态顺序表是指在程序设计时,为顺序表分配了一段固定大小的存储空间,不允许进行扩容或缩容的操作。由于静态顺序表的大小是固定的,所以需要在定义时预估数据的最大规模,这可能会导致存储空间的浪费或无法满足需求的问题。下面为静态顺序表:
动态顺序表是指在程序运行时,可以根据实际的数据规模进行动态扩容或缩容的操作。动态顺序表可以根据需要动态调整存储空间的大小,有效地利用了内存空间。但是,在扩容或缩容时需要进行数据的搬迁,会带来一定的时间开销。下面为动态顺序表:
顺序表适用于需要频繁访问元素或根据下标查找元素的场景。在元素的插入和删除操作较少,或者已知数据规模的情况下,使用静态顺序表更为合适。在元素的插入和删除操作较多,或者数据规模不确定的情况下,使用动态顺序表更为合适。因为静态顺序表的使用确实不常见,极少情况才会使用静态顺序表,在这里我着重和大家介绍动态顺序表。
3.动态顺序表的的实现
动态顺序表的实现我们需要些一些函数来完成顺序表的初始化、销毁,插入、删除等操作,所以这里我们采用多文件的方式来实现。每一部分的函数都会详细讲解。下面为顺序表的结构代码:
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* a;
int size; //顺序表有效的个数
int capacity; //顺序表的空间大小
}SL;
3.1顺序表的初始化和销毁
我们在主函数通过顺序表的结构来创建一个顺序表,创建完顺序表我们要干什么呢?当然是初始化啦,我们在初始化。在这里我们将顺序表的地址传递给创建的顺序表初始化函数,用一个指针接收顺序表的地址,通过对指针的解引用我们就可以修改顺序表。在顺序表初始化的时候我们要满足:将线性表的长度和容量都设置为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;
}
3.2顺序表的插入和删除
在这一小节主要和大家讲解顺序表的尾插、尾删、头插、头删、任意位置插入、任意位置删除操作。我们在顺序表的插入操作时,我们要先判断顺序表是否已满。如果顺序表已满,则需要进行扩容操作,将顺序表的容量增加一定的大小。所以在介绍顺序表的插入之前我们先写一个函数来为我们判断顺序表是否已满。我们要怎样判断顺序表是否满了呢?只需要判断顺序表中的有效元素个数和空间容量是否相等,相等就是满了呗。这个函数实现如下:
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失败。
3.2.1顺序表的尾插和尾删
顺序表尾插操作是指将一个新元素插入到顺序表的末尾。顺序表的尾插步骤如下:
- 首先,判断顺序表是否已满,如果已满,则无法插入新元素,插入操作失败。
- 如果顺序表未满,则将待插入的新元素放入顺序表最后一个位置。
- 更新顺序表的长度数值,将长度加1。
尾插操作的时间复杂度为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--;
}
3.2.2顺序表的头插和头删
顺序表的头插操作即在顺序表的第一个位置插入一个元素。顺序表的头插步骤如下:
- 判断顺序表是否已满,如果已满则无法进行头插操作。
- 如果顺序表未满,则将顺序表中的所有元素向后移动一位,为要插入的元素腾出位置。从最后一个元素开始,将其移动到下一个位置,一直到第一个元素移动到第二个位置。
- 将要插入的元素放入顺序表的第一个位置。
- 更新顺序表的长度。
我们来实现一下顺序表的头插函数:
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),因为需要将后面的元素往前移动一位,所以操作的时间复杂度与顺序表中已有元素的个数成正比。
3.2.3 顺序表的任意插入和任意删除
顺序表中的任意插入操作是指在任意位置插入一个元素。任意位置插入的具体步骤为下:
- 判断顺序表是否已满,如果已满则无法插入新元素,给出相应提示。
- 判断插入位置是否合法,即在顺序表的范围内,如果不合法则给出相应提示。
- 如果插入位置合法,需要将插入位置以及其后面的元素后移一个位置,为新元素腾出空间。
- 将要插入的元素放入插入位置。
- 更新顺序表的长度。
我们来实现一下顺序表的任意插入函数:
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++;
}
顺序表的任意位置删除操作是指在顺序表中删除指定位置的元素。任意位置删除的具体操作步骤如下:
- 首先判断删除位置的有效性。
-
将指定位置后面的所有元素向前移动一位。从删除位置开始,将删除位置后面的所有元素向前移动一位,覆盖掉删除的元素。可以使用一个循环来实现这个步骤。
-
将顺序表的长度减1,表示顺序表中元素的个数减少了一个。
我们来实现一下任意位置删除函数:
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--;
}
4.多文件实现顺序表
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;
}
我这里简单写一个函数来测试,测试结果为: