线性表(linear list)是n个具有相同特性的数据元素的有限序列。线性表是⼀种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...
线性表在逻辑上是线性结构,也就说是连续的⼀条直线。但是在物理结构上并不⼀定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
顺序表在逻辑上是连续的,并且物理结构(内存)上也是连续的。
顺序表和数组的区别 :顺序表的底层结构是数组,对数组的封装,实现了常用的增删改查等接口(函数)。
顺序表分类
静态顺序表 概念:使用定长数组存储元素,如下图:
静态顺序表缺陷:空间给少了不够用,给多了造成空间浪费 。
动态顺序表:
动态顺序表的实现:
头文件SeqList.h:
// 动态顺序表--通过指针来实现--动态内存开辟
typedef int SLDataType; // 顺序表的数据的存放类型未知或者存放char等类型的数据时只需要修改int为char即可
typedef struct Seqlist
{
SLDataType* arr; // 需要初始化内存大小,不能灵活调整
int size; // 标记顺序表中的数据个数
int capacity; // 标记顺序表的内存大小(最多存放元素的个数)
}SL;
// 顺序表的初始化的函数声明
void SLinitial(SL* ps);
// 顺序表的销毁的函数声明
void SLdestroy(SL* ps);
// 顺序表的增加数据的函数声明 1.从尾部增加数据
void SLpush_back(SL* ps, SLDataType num);
// 顺序表的增加数据的函数声明 2.从投部增加数据
void SLpush_front(SL* ps, SLDataType num);
// 3.从尾部删除数据
void SLdeletion_back(SL* ps);
// 4.从头部删除数据
void SLdeletion_front(SL* ps);
//5.指定位置之前插入数据
void SLinsert(SL* ps, int pos, SLDataType num);
//6.指定位置删除数据
void SLdeletion_in_position(SL* ps, int pos);
// 7.找出指定位置的数据
int SLfind(SL* ps, SLDataType num);
// // 打印顺序表的内容
void SLprint(SL* ps);
顺序表实现文件SeqList.c:
顺序表的初始化和销毁:
注意:参数的传递必须是指针(地址)才能在调用的函数内部改变主函数的变量。否则形参只是实参的一份临时拷贝。
首先初始化,指针置空,size和capacity置为0。销毁的时候若指针不为空(顺序表不为空)先释放空间,再将size和capacity置为0。这里需要使用free释放内存空间,因为动态申请的内存空间是由malloc和realloc申请得到的。arr指针变量记得置空避免成为野指针造成非法访问。打印顺序表的内容,使用for循环打印【0,size)之间的数据。
#define _CRT_SECURE_NO_WARNINGS
#include "Seqlist.h"
// 函数实现功能
// 创建一个函数,调用这个函数实现顺序表的初始化
void SLinitial(SL* ps)
{
ps->arr = NULL;
ps->size = 0;
ps->capacity = 0;
}
// 顺序表的销毁函数
void SLdestroy(SL* ps)
{
if (ps->arr != NULL)
{
free(ps->arr);
}
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
// 打印顺序表的内容
void SLprint(SL* ps)
{
assert(ps);
for (int i = 0; i < ps->size;i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n");
}
头插、尾插:
头插和尾插首先判断传入的指针不能为空,然后要判断顺序表容量够不够,不够的话要增容。这里增容使用的是realloc()函数,第一个参数是要增容的地址,第二个参数是增容的字节个数。需要注意的是realloc增容有两种情况:1:直接在原来的空间的后面增容返回的还是所传递的参数的地址(需要增容的地址);2:原来的空间的后面空间不足,从别的地方新开辟一段空间,将原来的数据挪到新的空间内,此时返回的是新的地址。需要判断在使用。(创建一个临时变量接收realloc返回的地址,判断后再赋值给原来的地址)
增容时选择2倍数增容,参考手机内存发展史。
从尾部插入直接就是将数据放入size下标的位置即可,size需要加1。头部插入时,需要将数据挪动,这里需要从后往前挪动,如果从前往后挪会造成数据的覆盖导致数据丢失。
// 检查内存空间是否够
void SLcheckcapacity(SL* ps)
{
assert(ps);
if (ps->size == ps->capacity)
{
// 2倍数的增容,若ps->capacity为0就是第一次申请内存那么设置初始值为sizeof(SLDataType),便于arr的加减访问元素,若不为0则2倍数增容
int newcapacity = ps->capacity == 0 ? sizeof(SLDataType) : 2 * ps->capacity;
SLDataType* temp = (SLDataType*)realloc(ps->arr, newcapacity * sizeof(SLDataType));
// 申请空间失败
if (temp == NULL)
{
perror("realloc() fail");// 打印错误的原因,若错误则屏幕打印:realloc() fail: 错误内容...
exit(1);//直接退出程序,不在运行
}
// 申请空间成功
ps->arr = temp;
ps->capacity = newcapacity;
}
}
// 1.从尾部增加数据
void SLpush_back(SL* ps, SLDataType num)
{
assert(ps);
// 首先判断是否有足够的内存空间存放新增的数据,无内存或者内存不够时申请和增加内存使用realloc
SLcheckcapacity(ps);
// 增加数据,加入新数据后size要+1
ps->arr[ps->size] = num;
ps->size++;
}
// 2.从头部增加数据
void SLpush_front(SL* ps, SLDataType num)
{
assert(ps);
// 首先判断是否有足够的内存空间存放新增的数据,无内存或者内存不够时申请和增加内存使用realloc
SLcheckcapacity(ps);
// 先将已有的数据从后往前挪到后面,空出第一个位置
for (int i = ps->size;i > 0;i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[0] = num;
ps->size++;
}
尾删和头删及指定位置插入数据。
尾删需要断言指针是否为空和size个数,直接将size减一即可,不需要将size-1位置的数据做其他操作,我们遍历顺序表的时候就不会遍历到这个元素了。
头部删除,也需要断言。然后将后面的数据往前挪覆盖原先的元素,然后size减一即可。
指定位置插入数据需要断言传入的位置是否在size内,若pos==0为头插,pos==size为尾插。插入数据需要判断内存是否足够,然后将pos及其后面的数据从后往前挪动,把pos位置空出来插入新的数据,size要加1。
// 3.从尾部删除数据
void SLdeletion_back(SL* ps)
{
assert(ps);
assert(ps->size); // 一个元素都没有时报错代码结束运行
--ps->size;
}
// 4.从头部删除数据
void SLdeletion_front(SL* ps)
{
assert(ps);
assert(ps->size); // 一个元素都没有时报错代码结束运行
for (int i = 0;i < ps->size - 1;i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
//5.指定位置插入数据
void SLinsert(SL* ps, int pos, SLDataType num)
{
assert(ps);
// 检查pos是否在有效范围内
assert(pos >= 0 && pos <= ps->size);
// 首先判断是否有足够的内存空间存放新增的数据,无内存或者内存不够时申请和增加内存使用realloc
SLcheckcapacity(ps);
// 或者直接这样包含上面4种情况 将pos位置及之后的元素后移一位
for (int i = ps->size; i > pos; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
// 在pos位置插入新元素
ps->arr[pos] = num;
ps->size++;
}
删除指定位置的数据和找出指定某个数据然后返回该数据的位置的下标。
删除指定位置的数据需要断言,将pos后面的数据往前挪一位将pos及其后面的覆盖掉即可。size要减1。
找出某个元素的下标。就是遍历顺序表若找到了则返回数据的下标,没找到则返回-1。
//6.指定位置删除数据
void SLdeletion_in_position(SL* ps, int pos)
{
assert(ps);
assert(ps->size); // 一个元素都没有时报错代码结束运行
assert(pos >= 0 && pos < ps->size); // 检查pos是否在有效范围内
// 将pos位置之后的元素前移一位
for (int i = pos; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
// 元素个数减1
ps->size--;
}
// 7.找出指定位置的数据
int SLfind(SL* ps, SLDataType num)
{
assert(ps);
assert(ps->size); // 一个元素都没有时报错代码结束运行
for (int i = 0;i < ps->size;i++)
{
if (ps->arr[i] == num)
return i; // 找到了
}
// 没有找到
return -1;
}
测试顺序表的接口是否能够实现预想的功能test.c文件:
在指定位置插入数据和删除指定位置的数据需要配合查找函数一起使用。
#define _CRT_SECURE_NO_WARNINGS
#include "Seqlist.h"
// 测试代码
void test()
{
// 创建一个顺序表
SL s;
// 调用函数初始化顺序表
SLinitial(&s);
// 增删改查
// 1.从尾部增加数据
SLpush_back(&s, 1);
SLpush_back(&s, 2);
SLpush_back(&s, 3);
SLpush_back(&s, 4);
SLprint(&s); // 1 2 3 4
// 2.从头部增加数据
//SLpush_front(&s, -1);
//SLpush_front(&s, -2);
//SLprint(&s); // -2 -1 1 2 3 4
// 3.从尾部删除数据
//SLdeletion_back(&s);
//SLprint(&s); // -2 -1 1 2 3
// 4.从头部删除数据
//SLdeletion_front(&s);
//SLdeletion_front(&s);
//SLprint(&s); // 1 2 3
//5.指定位置之前插入数据
//SLinsert(&s, 1, -5);
//SLprint(&s); // -5 1 2 3 4
////6.指定位置删除数据
//SLdeletion_in_position(&s, 4);
//SLprint(&s); // 2 3 4
// 7.找出指定位置的数据
int find = SLfind(&s, 5);
if (find < 0)
printf("没有找到!");
else
printf("找到了,下标为%d", find);
// 调用函数销毁顺序表
SLdestroy(&s);
}
int main()
{
test();
return 0;
}
总结:本文详细介绍了线性表数据结构中的动态顺序表实现。主要内容包括:1) 线性表的基本概念与分类;2) 动态顺序表的结构设计(包含数据指针、当前元素个数和容量);3) 关键功能实现:初始化、销毁、头插/尾插、头删/尾删、指定位置插入/删除、查找等操作;4) 动态扩容机制及内存管理注意事项;5) 测试用例演示。重点阐述了动态顺序表通过指针实现动态内存分配的优势,避免了静态顺序表空间浪费的问题,并提供了完整代码实现方案。