目录
线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是⼀种在实际中⼴泛使⽤的数据结构,常⻅的线性表:顺序表、链表、栈、队列、字符串...
线性表在逻辑上是线性结构,也就说是连续的⼀条直线。但是在物理结构上并不⼀定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
顺序表
1.概念与结构
概念:顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。
等等!那这玩意儿到底和数组有啥区别?
问得好!其实顺序表就是基于数组的概念发展而来,有着比数组更强大的功能。具体区别如下:
- 功能封装:数组简单存储,顺序表封装更多操作函数。
- 动态性:数组大小固定,顺序表可动态改变容量。
- 内存管理:数组需手动管理内存,部分顺序表可自动处理。
- 应用场景:数组适用于元素数量固定、操作简单的情况;顺序表适用于元素数量可变、操作灵活的场景。
2.对顺序表的详解
抽象!太抽象了!
不瞒你说,其实我也觉得抽象。但别急,这么爱举例子的我,肯定早就准备好代码了,下面我们来看一组例子:
(1)创建顺序表结构体
为了方便实现后期增删查改等功能,我们还需要统计出顺序表空间大小和存放的有效数据个数。“typedef int SLDatatype”的作用是将SLDatatype类型改为int类型,以防后期更改顺序表存放的数据类型。
typedef int SLDatatype;//SLDatatype类型即为int类型,以防后期更改顺序表存放的数据类型
typedef struct SeqList
{
SLDatatype* arr;
int capacity; //空间大小
int size; //有效数据个数
}SL;
(2)顺序表的初始化
这里需要注意的是,我们传入的参数是顺序表的指针。这样才能改变实参,而非仅仅形参的改变。
//初始化
void SLInit(SL* ps)
{
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
(3)判断顺序表空间足够与否并扩容
整体来说不难理解,看一下注释应该不成问题。唯一的难点可能是有些同学不懂realloc的用法,博主将它的用法放在了疑难解答,大家可以自行学习。
void SLCheckCapacity(SL* ps)
{
//判断空间是否充足
if (ps->size == ps->capacity)//如果有效数据个数等于顺序表空间大小,即扩容
{
//若capacity为0,给默认值(4),否则×2
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLDatatype* tmp = (SLDatatype*)realloc(ps->arr, newCapacity * sizeof(SLDatatype));
if (tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
}
(4)插入数据
插入数据一般分为头插、尾插和特定位置插入,他们都要向数据表中增添数据,因此不要忘了先判断是否需要增容再进行插入操作,下面我们一一介绍。
①尾插
顾名思义,就是在顺序表的末尾插入数据。也是三种插入方法中最简单的一个。
参数含义:
ps
:是指向顺序表结构体的指针,用来操作顺序表的相关数据。x
:要添加到顺序表末尾的数据。
代码如下:
void SLPushBack(SL* ps, SLDatatype x)
{
assert(ps);//assert(ps != NULL)判断ps是否为空
SLCheckCapacity(ps);//判断空间是否足够
ps->arr[ps->size++] = x;
}
②头插
在头部插入,我们要将该顺序表中所有数据向后移动一位。此时在顺序表的第一个位置存入数据。
参数含义和尾插完全一致,这里不再复述。
代码如下:
//头插
void SLPushFront(SL* ps, SLDatatype x)
{
assert(ps);
//判断空间是否足够
SLCheckCapacity(ps);
//数据整体后移一位
for (int i = ps->size; i > 0; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
//下标为0的位置空出来
ps->arr[0] = x;
ps->size++;
}
③特定位置插入
这个也不难,就是将指定位置及其以后的数据全部向后移动一位。将该位置放入我们的数据即可。
参数含义:
- ps:是指向顺序表结构体的指针,用来操作顺序表的相关数据。
- x:要添加到顺序表末尾的数据。
- pos:在顺序表中指定的位置
代码如下:
//在指定位置之前插入数据(空间足够才能直接插入数据)
void SLInsert(SL* ps, SLDatatype x, int pos)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);//判断位置是否是有效范围
SLCheckCapacity(ps);//判断空间是否充足
//pos及之后的数据整体向后移动一位
for (int i = ps->size; i > pos; i--)
{
ps->arr[i] = ps->arr[i - 1]; //pos+1 -> pos
}
ps->arr[pos] = x;//将数据插入指定位置
ps->size++;//有效数据个数加1
}
(5)输出数据
利用循环,将顺序表中所有内容顺序输出。
void SLPrint(SL* ps)//输出顺序表中的内容
{
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n");
}
(6)删除数据
①尾删
这里直接将顺序表的size减1。实际上该内存没有被释放掉,但是由于size减1,输出时会少输出1位,实现逻辑上的删除。
//尾删
void SLPopBack(SL* ps)
{
assert(ps);
assert(ps->size);//空顺序表不能继续删除
ps->size--;
}
②头插
除首元素外的所有元素向前移动一位,再删除最后一个元素即可。
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size);
//数据整体向前挪动一位
for (int i = 0; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];//i = size-2
}
ps->size--;
}
③特定位置插入
特定位置之后的元素向前移动一位,删除最后一个元素即可。
//删除指定位置的数据
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
for (int i = pos; i < ps->size-1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
(7)查找数据
不多解释,上代码:
int SLFind(SL* ps, SLDatatype x)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
{
return i;
}
}
//没有找到
return -1;
}
(8)销毁顺序表
当不需要顺序表的时候呢,将内存释放,重新初始化即可。
//销毁
void SLDestroy(SL* ps)
{
if (ps->arr)//相当于ps->arr != NULL
{
free(ps->arr);
}
//回归初始状态
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
疑难解答
1.关于线性表物理结构不一定连续
“在物理结构上并不一定是连续的”,这是说在计算机内存中实际存储这些元素时,它们的存储位置不一定是相邻的。
当以数组形式存储线性表时,数组在内存中通常是一块连续的存储空间,通过下标可以直接快速地访问到相应的元素。例如一个整数数组 int arr[5] = {1, 2, 3, 4, 5}
,这些元素在内存中是连续存放的。
而当以链式结构(比如链表)存储线性表时,每个元素(节点)在内存中的位置是随机的,并不一定相邻。节点通过指针来链接,形成逻辑上的线性顺序。例如一个链表节点可能存放在内存的一处,它的下一个节点可能存放在相隔较远的另一处,通过节点中的指针来找到下一个节点的位置。
总之,线性表的逻辑结构强调元素之间的顺序关系,而物理存储结构则考虑如何在计算机内存中有效地实现这种逻辑关系,不一定非要连续存储。
2.realloc的用法
realloc
函数用于重新分配内存块的大小。函数原型:
void *realloc(void *ptr, size_t size);
参数含义:
ptr
:指向之前通过malloc
、calloc
或realloc
分配的内存块的指针。size
:新的所需内存大小。用法及作用:
- 如果
ptr
为NULL
,则realloc
的功能类似于malloc
,分配指定大小的内存。- 如果
size
为0
且ptr
不为NULL
,则相当于释放ptr
指向的内存,类似于free
函数。- 否则,
realloc
尝试改变之前分配的内存块的大小。如果有足够的连续空间可扩展现有内存块,会在原地进行扩展并返回原指针。如果没有足够的连续空间,会分配新的内存块,将原有数据复制到新位置,并释放旧的内存块,然后返回新的指针。
例如:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *ptr = (int *)malloc(sizeof(int) * 5); // 分配 5 个整数的内存
// 填充一些数据
for (int i = 0; i < 5; i++)
{
ptr[i] = i;
}
int *newPtr = (int *)realloc(ptr, sizeof(int) * 10); // 重新分配为 10 个整数的内存
if (newPtr!= NULL)
{
ptr = newPtr; // 更新指针
// 继续使用扩展后的内存
for (int i = 5; i < 10; i++)
{
ptr[i] = i;
}
// 输出数据
for (int i = 0; i < 10; i++)
{
printf("%d ", ptr[i]);
}
printf("\n");
free(ptr); // 释放内存
}
else
{
printf("Reallocation failed!\n");//创建失败提示信息
}
return 0;
}