本篇目标:
学会顺序表的增删查改的代码实现
一.顺序表
1.动静态顺序表
静态顺序表其实就是用有限大小的空间来存储数据,但是这个空间大了容易浪费空间,小了容易造成空间不够的情况。
动态顺序表利用malloc来从堆上申请空间,如果空间小了可以利用顺序表的接口自动扩容,并且空间浪费比较少,例如:
cpp
typedef int SqDataType;
typedef struct SequenceList
{
SqDataType* arr;
int size;
int capacity;
}SqList;
有一个顺序表L=(10,20,30,40,50),当前容量空间为5,空间满了再扩容,则其示意图为:

今天主要是以动态顺序表为主。
2.代码实现
2.1.主体框架
在SqList.h头文件中,我们主要实现对顺序表的接口的声明,如代码所示:
cpp
#pragma once
#include <stdio.h>
#include <stdbool.h>
#include <assert.h>
#include <stdlib.h>
typedef int SqDataType; // 数据元素类型
typedef struct SqList
{
SqDataType* arr;
int size;
int capacity;
}SqList;
// 初始化顺序表
void SqListInit(SqList* ps);
// 销毁顺序表
void SqListDestroy(SqList* ps);
// 返回顺序表中第i个下标位置元素的值
SqDataType GetElem(SqList* ps, int i);
// 返回第⼀个等于x的数据元素的下标,若不存在返回 - 1
int LocateElem(SqList * ps, SqDataType x);
// 在顺序表的第i个位置插⼊元素x
void SqListInsert(SqList* ps, int i, SqDataType x);
// 删除顺序表中第i个元素,并返回删除的值
SqDataType SqListDelete(SqList* ps, int i);
// 打印顺序表中的元素
void SqListPrint(SqList* ps);
// 检测顺序表是否为空,空返回true,否则返回false
bool EmptySqList(SqList* ps);
// 获取顺序表中有效元素个数
int SqListSize(SqList* ps);
// 头插尾插
void SqListPushBack(SqList* ps, SqDataType x);
void SqListPushFront(SqList* ps, SqDataType x);
// 头删尾删
void SqListPopBack(SqList* ps);
void SqListPopFront(SqList* ps);
有人可能会问:为什么要用typedef int****和struct SqList 呢?其实这是为了方便代码维护。如果将来需要存储浮点数,只需修改一处类型定义即可,而tyoedef struct SqList也是为了方便。
2.2.初始化与打印
顺序表的结构体变量创建好后,系统会以随机值对其进行填充,所以在使用前须先进行初始化,步 骤如下:
<1>.先使用malloc申请⼀个默认大小动态数组空间,例如默认大小为4,如果不够,后续可以扩容。
<2>.申请成功后,将有效元素个数初始化为0 ,因为初始化阶段,顺序表中还未存放任何有效元素 <3>.将capacity设置为所申请空间的实际大小
如代码所示:
cpp
void SqListInit(SqList* ps)
{
assert(ps); //防止对方传个NULL
ps->arr = (SqDataType*)malloc(sizeof(SqDataType) * 4);
if (ps->arr == NULL)
{
printf("申请空间失败\n");
exit(1); //相当于return;
}
ps->size = 0;
ps->capacity = 4;
}
然后为了观察顺序表中的元素是否如预期那般存入,还需要一个打印顺序表中元素的接口,如代码:
cpp
void SqListPrint(SqList* ps)
{
printf("顺序表中的元素:");
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->arr[i]);
}
}
2.3.插入与删除
<1>.插入
插入元素之前,我们首先要考虑一个问题,如果ps->size=ps->capacity此时再插入数据不就超过顺
序表的容量了吗?
所以当顺序表中元素存满时,就需要进⾏扩容,否则无法继续插⼊元素。扩容是⼀个前瞻思维,不
仅仅考虑插⼊当前数据没有空间了,还要考虑后⾯数据插⼊也要空间,所以索性⼀次多扩展⼀些,
⼀ 般的做法是2倍左右扩容,当然有些书上是按固定⼤小扩容,比如每次扩容4个空间。
这里我们扩容使用C的库函数 void* realloc (void* ptr, size_t size) 实现,ptr是旧空间的指针,
size是需要的新空间的字节数,realloc函数有以下⼏种情况:
1.原地扩容:

2.异地扩容:

插入的逻辑:

如上图所示,我们有一个顺序表 1, 2, 3, 4, 6。现在需要在数字2之前插入元素6,这里需要考虑的是:应该从后往前移动数据,还是从前往后移动数据?
正确的做法是从后往前移动数据。如果采用从前往后的移动方式,会导致原有数据被覆盖。具体原因可以通过下方图示来理解:

代码如图所示:
cpp
// 在顺序表的第i个位置插⼊元素x
void SqListInsert(SqList* ps, int i, SqDataType x)
{
assert(ps);
assert(i >= 0 && i <= ps->size); //插入的位置应当满足顺序表的数据个数
if (ps->size == ps->capacity)
{
SqDataType* tmp = (SqDataType*)realloc(ps->arr, sizeof(SqDataType)*ps->capacity * 2);
if (tmp == NULL)
{
printf("扩容失败\n");
exit(1);
}
ps->arr = tmp;
ps->capacity = 2 * ps->capacity;
}
for (int j = ps->size; j > i; j--)
{
ps->arr[j] = ps->arr[j-1];
}
ps->arr[i] = x;
ps->size++;
}
插入是我们应该用assert来判断插入的位置是否符合要求。
有了在i位置的插入之后,我们就可以复用这个代码,完成头插和尾插
cpp
// 尾插
void SqListPushBack(SqList* ps, SqDataType x)
{
SqListInsert(ps, ps->size, x);
}
//头插
void SqListPushFront(SqList* ps, SqDataType x)
{
SqListInsert(ps, 0, x);
}
<2>.删除

如上图,我们有个L=(1,5,8,5,3)的顺序表,我要删除下表为1的元素,这里需要考虑的是:应该从
后往前移动数据,还是从前往后移动数据?
从前向后遍历可以避免数据覆盖的问题;如果从后向前遍历,则可能导致数据被覆盖。具体原因可
以通过下图来理解:

代码如图所示:
cpp
SqDataType SqListDelete(SqList* ps, int i)
{
assert(ps);
assert(i >= 0 && i < ps->size);
int tmp = ps->arr[i]; //先保存要删除的值
for(int j=i;j<ps->size-1;j++)
{
ps->arr[j] = ps->arr[j + 1];
}
ps->size--;
return tmp;
}
有了在下标为i的删除后,就可以利用这个代码完成对头删/尾删的操作了,如代码所示:
cpp
// 尾删
void SqListPopBack(SqList* ps)
{
assert(ps);
SqListDelete(ps, ps->size-1);
}
//头删
void SqListPopFront(SqList* ps)
{
SqListDelete(ps, 0);
}
2.4.查找
顺序表有两种查找操作,位序查找和按值查找。
位序查找函数原型: SqDataType GetElem(SqList* ps, int i) 第i个位置元素随机访问,在i满足 0
<= i < s.size 时(不满足则报错),直接返回顺序表第i个元素即可,如代码所示:
cpp
//返回顺序表中第i个下标位置元素的值
SqDataType GetElem(SqList* ps, int i)
{
assert(ps);
assert(i >= 0 && i < ps->size);
return ps->arr[i];
}
按值查找函数原型: int LocateElem(SqList* ps, SqDataType x) 从前往后逐个查 找,找到第⼀个
相等的就返回其下标,否则返回-1。
cpp
int LocateElem(SqList* ps, SqDataType x)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
{
return i;
}
}
return -1;
}
2.5.其他接口
当我们使用顺序表后,理应销毁它,防止内存泄漏,比较简单,如代码所示:
cpp
void SqListDestroy(SqList* ps)
{
assert(ps);
free(ps->arr);
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
然后可以判断一下顺序表是否为空和数据个数,如代码所示:
cpp
// 检测顺序表是否为空,空返回true,否则返回false
bool EmptySqList(SqList* ps)
{
assert(ps);
return ps->size == 0;
}
// 获取顺序表中有效元素个数
int SqListSize(SqList* ps)
{
assert(ps);
return ps->size;
}
2.6.测试代码
如图所示:
cpp
#include "SqList.h"
void TestSqList1()
{
SqList L;
SqListInit(&L);
SqListInsert(&L, 0, 9);
SqListInsert(&L, 1, 10);
SqListInsert(&L, 2, 20);
SqListInsert(&L, 3, 30);
SqListInsert(&L, 4, 40);
SqListInsert(&L, 5, 50); // 扩容
SqListPrint(&L);
// 头插
SqListInsert(&L, 0, 0);
SqListPrint(&L);
// 尾插
SqListInsert(&L, SqListSize(&L), 60);
SqListPrint(&L);
// 中间插入
SqListInsert(&L, 2, 2);
SqListPrint(&L);
}
void TestSqList2()
{
SqList L;
SqListInit(&L);
SqListInsert(&L, 0, 9);
SqListInsert(&L, 1, 10);
SqListInsert(&L, 2, 20);
SqListInsert(&L, 3, 30);
SqListInsert(&L, 4, 40);
SqListInsert(&L, 5, 50); // 扩容
SqListPrint(&L);
// 删除顺序表第1个位置上的元素
printf("顺序表中有效元素个数为:%d \n", SqListSize(&L));
// 删除第一个数据
printf("删除的元素是:%d \n", SqListDelete(&L, 0));
// 删除末尾的数据
printf("删除的元素是:%d \n", SqListDelete(&L, SqListSize(&L) - 1));
// 删除中间的数据
printf("删除的元素是:%d \n", SqListDelete(&L, 2));
}
int main()
{
//TestSqList1();
TestSqList2();
return 0;
}