文章目录
动静态顺序表的选择
顺序表根据存储元素的数组的长度 “可变” 和 “不可变” 分为 “静态顺序表” 和 “动态顺序表” 两种类型,那么我们写实现顺序表时该如何选择呢?
动静态顺序表的区别如下:
- 静态顺序表:使用定长数组存储元素。
- 动态顺序表:使用动态开辟的数组存储元素。
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。但是计划赶不上变化,现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以我们选择实现动态顺序表。
顺序表的接口实现
接口清单
一般对顺序表进行增删查改要通过它提供的接口来进行,下面为大家罗列一些顺序表应该实现的基本接口:
// 基本增删查改接口
// 顺序表初始化
void SeqListInit(SeqList* psl);
// 检查空间,如果满了,进行增容
void CheckCapacity(SeqList* psl);
// 顺序表尾插
void SeqListPushBack(SeqList* psl, SLDataType x);
// 顺序表尾删
void SeqListPopBack(SeqList* psl);
// 顺序表头插
void SeqListPushFront(SeqList* psl, SLDataType x);
// 顺序表头删
void SeqListPopFront(SeqList* psl);
// 顺序表查找
int SeqListFind(SeqList* psl, SLDataType x);
// 顺序表在pos位置插入x
void SeqListInsert(SeqList* psl, size_t pos, SLDataType x);
// 顺序表删除pos位置的值
void SeqListErase(SeqList* psl, size_t pos);
// 顺序表销毁
void SeqListDestory(SeqList* psl);
// 顺序表打印
void SeqListPrint(SeqList* psl);
项目创建
顺序表的实现采用多文件的项目管理模式
SeqList.h
文件用于包含库的头文件、函数声明、符号常量定义等。
SeqList.c
文件用于实现接口。
test.c
文件用于阶段性测试接口实现是否正常。
顺序表初始化、尾插、头插、打印接口的实现及测试
- 顺序表的初始化
// 顺序表初始化
void SeqListInit(SeqList* psl)
{
assert(psl); // 结构体的地址不能为空
psl->data = NULL;
psl->size = psl->capacity = 0;
}
如果不初始化,结构体的成员size、capacity都是编译器默认设置的初始值(VS下是0xcccccccc),而成员data更是不知道存放哪块空间的地址,因此,为了更好的使用顺序表,应该在创建之后立即及逆行初始化。
- 顺序表的尾插
我们把数组下标为0的位置定义为顺序表的 “头”,最右边的元素为 “尾”。
// 顺序表尾插
void SeqListPushBack(SeqList* psl, SLDataType x)
{
assert(psl); // 结构体地址不能为空
CheckCapacity(psl); // 检测容量,需要的情况下扩容
psl->data[psl->size++] = x; // 尾插之后size增加1
}
- 顺序表的头插
而所谓头插就是在下标为0的位置插入一个数据。
实现的思路就是把当前顺序表的数据整体从后往前挪动一个单位,最后用新的数据将旧的数据覆盖。
// 顺序表头插
void SeqListPushFront(SeqList* psl, SLDataType x)
{
assert(psl); // 结构体地址不能为空
CheckCapacity(psl); // 检测容量,需要的情况下扩容
int i = 0;
for (i = psl->size; i > 0; i--) // 挪动数据
psl->data[i] = psl->data[i - 1];
psl->data[i] = x;
psl->size++; // 尾插之后size增加1
}
而无论是尾插还是头插,都涉及到一个问题,就是当记录长度的size
>= 数组当前最大容量capacity
时,都无法进行该操作,因此需要在插入之前对数组进行一个检测,判断是否需要扩容。
// 检查空间,如果满了,进行增容
static void CheckCapacity(SeqList* psl)
{
if (psl->capacity <= psl->size)
{
int NewCapacity = psl->capacity == 0 ? INIT_NUM : psl->capacity * 2;
SLDataType* tmp = realloc(psl->data, NewCapacity * sizeof(SLDataType));
if (!tmp)
{
printf("realloc failed!\n");
exit(-1);
}
psl->data = tmp;
psl->capacity = NewCapacity;
}
}
这里使用
realloc
的原因是因为初始化时默认capacity
和size
都是0,并且当第一个参数为NULL时,它的作用等同于malloc
扩容的时候,一般二倍扩容比较合适。
- 顺序表数据打印
打印数据到控制台上能够直观体现写的尾插头插是否正确,原理什么好讲的,单纯的就是一个for
循环。
// 顺序表打印
void SeqListPrint(SeqList* psl)
{
int i = 0;
for (i = 0; i < psl->size; i++)
{
printf("%d ", psl->data[i]);
}
printf("\n");
}
- 第一阶段测试
不知道你是否有这种经历,顺序表的接口实现到这里时,自我感觉超级良好,感觉顺序表so easy,于是接着往下一顿输出,然后你把顺序表所有的接口写完了,当你满怀欣喜的编译链接运行,然后十几个红色的报错信息教你做人,然后你顿时傻眼了,没办法你只能硬着头皮去改。
如果你的经验不错,很快就改好了,那恭喜你;但是如果你的基础不扎实或者说第一次接触顺序表,改了两个小时都没有改好,那大概率心态是要崩的。
所以,代码写一部分,测一部分,是一个好的编程习惯,它能够保证我们之前写的代码没问题,并且找出现在写的代码中的bug,好的程序员不是写代码有多快,而是做到尽可能少产bug的情况下把代码写得又快又好。
接下来就演示一下个人写代码过程中是如何测试代码的
void testInsert()
{
SeqList sl;
SeqListInit(&sl);
SeqListPushBack(&sl, 1); // 测试点1:尾插第一个数据,raalloc充当malloc
SeqListPrint(&sl);
SeqListPushBack(&sl, 2);
SeqListPushBack(&sl, 3);
SeqListPushBack(&sl, 4); // 测试点2:尾插满INIT_NUM个数据
SeqListPrint(&sl);
SeqListPushBack(&sl, 5); // 测试点3:二倍扩容
SeqListPrint(&sl);
// 头插同理,如果嫌弃上面的结果影响测试,可以先注释掉
SeqListPushFront(&sl, 1); // 测试点1:头插第一个数据,raalloc充当malloc
SeqListPrint(&sl);
SeqListPushFront(&sl, 2);
SeqListPushFront(&sl, 3);
SeqListPushFront(&sl, 4); // 测试点2:尾插满INIT_NUM个数据
SeqListPrint(&sl);
SeqListPushFront(&sl, 5); // 测试点3:二倍扩容
SeqListPrint(&sl);
}
int main(void)
{
testInit();
return 0;
}
- 测试结果
当看到测试结果和预期一致是否有一种满足感,这种满足感能大大提高我们写代码的积极性。
顺序表的头删、尾删接口的实现及测试
- 顺序表的尾删
尾删就是将表中最右边的元素删掉,乍一看你或许很疑惑,怎么删?
其实吧,操作非常简单,所谓的删除操作的本质就是看不到。
// 顺序表尾删
void SeqListPopBack(SeqList* psl)
{
assert(psl); // 结构体地址不能为空
assert(psl->size > 0); // 删除的前提是要保证有数据可以删
psl->size--;
}
其实下标为2的位置中放到数据依然是3,但是,只要我访问不到,数据就等于不存在。
如果此时在尾插一个新的数据,那么新的数据就会将原本的3覆盖掉。
怎么样,是不是非常简单。
- 顺序表的头删
头删和头插一样,都要进行数据的挪动,不过尾插是数据向后整体挪动一位,而头删是后面的数据向前整体挪动一位。
// 顺序表头删
void SeqListPopFront(SeqList* psl)
{
assert(psl); // 结构体地址不能为空
assert(psl->size > 0); // 删除的前提是要保证有数据可以删
int i = 0;
for (i = 0; i < psl->size - 1; i++)
psl->data[i] = psl->data[i + 1];
psl->size--;
}
- 第二阶段,删除相关接口的测试
void testDelete()//——ok
{
SeqList sl;
SeqListInit(&sl);
SeqListPushFront(&sl, 1);
SeqListPushFront(&sl, 2);
SeqListPushFront(&sl, 3);
SeqListPushFront(&sl, 4);
SeqListPushFront(&sl, 5);
SeqListPrint(&sl);
SeqListPopFront(&sl);
SeqListPopFront(&sl);
SeqListPrint(&sl); // 头删两个
SeqListPopFront(&sl);
SeqListPopFront(&sl);
SeqListPrint(&sl); //头删四个
SeqListPopFront(&sl);
SeqListPrint(&sl);
SeqListPopFront(&sl); // 删完继续删
SeqListPrint(&sl);
//尾删同理,将头删函数替换成尾删函数即可,这里就不放出来了
}
- 测试结果
我们看到测试结果的第5行出现了一行提示信息,它明确告知我们哪个文件,哪一行出现了错误,经过查看后我们发现这是我们对空表进行删除操作,这很明显是不合理,而这就是断言的好处,它可以及时对不合理操作进行处理,并告知我们。
顺序表的查找、任意位置删除和插入的实现和测试
- 顺序表元素的查找
查找接口的思想:从左到右依次比较目标,返回第一次找到的位置的下标,如果找不到,返回-1。
// 顺序表查找
int SeqListFind(SeqList* psl, SLDataType x)
{
assert(psl);
int i = 0;
for (i = 0; i < psl->size; i++)
{
if (psl->data[i] == x)
return i;
}
return -1;
}
- 往顺序表的任意位置插入元素
前面我们介绍了头插和尾插,虽然这两种插入可以满足我们往顺序表里面添加数据的需求,但是能不能实现任意位置的插入呢?
答案是可以的,并且任意位置的插入接口实现起来和头插是差不多的。
头插是将所有的数据整体往后挪动一位,然后将第一个位置用新的数据覆盖;任意位置插入就是将下标为i
开始的所有数据整体往后挪动一位。
// 顺序表删除pos位置的值
void SeqListErase(SeqList* psl, size_t pos)
{
assert(psl);
assert(0 <= pos && pos < psl->size); // 可删除的下标区间:[0,psl->size-1]
int i = 0;
for (i = pos; i < psl->size - 1; i++)
psl->data[i] = psl->data[i + 1];
psl->size--;
}
- 第三阶段测试:任意的插入、删除和查找接口
void testFind()//——ok
{
SeqList sl;
SeqListInit(&sl);
SeqListPushBack(&sl, 1);
SeqListPushBack(&sl, 2);
SeqListPushBack(&sl, 3);
SeqListPushBack(&sl, 4);
SeqListPushBack(&sl, 5);
SeqListPrint(&sl);
int index = SeqListFind(&sl, 5); //测试点1:查找的返回值
if (index != -1)
printf("找到了,下标是%d\n", index);
else
printf("找不到\n");
SeqListInsert(&sl, index, 3); // 测试点2:插入
SeqListPrint(&sl);
SeqListErase(&sl, sl.size-1); // 测试点3:删除
SeqListPrint(&sl);
SeqListDestroy(&sl);
}
int main(void)
{
testFind();
return 0;
}
- 测试结果
顺序表的销毁接口以及内存泄漏
- 顺序表的销毁
// 顺序表销毁
void SeqListDestroy(SeqList* psl)
{
assert(psl);
free(psl->data);
psl->capacity = psl->size = 0;
}
程序关闭后内存会自动释放,为什么这里还要多此一举的去释放空间呢?
因为,数组空间是动态开辟在内存的堆区上开辟的,它只有两种释放方式,分别是手动释放和关闭程序,如果在顺序表使用完毕后不进行释放,那么会造成操作内存泄漏。
这里出现了一个概念叫 “内存泄漏”,一台计算机的内存容量是固定的,如果用户申请空间并使用完毕后却没有将这部分内存归还给操作系统,这就导致了这块内存空间既无人使用,操作系统又找不到。如果建立大量的顺序表,有可能导致操作系统因内存不够而崩溃。
因此,对于不再使用的动态开辟的空间进行及时的释放是一个好的编程习惯!
源代码
以下为动态顺序表的源代码。
SeqList.h
#pragma once
//头文件包含
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
///* 静态顺序表 */
//#define MAX_SIZE 1000 //顺序表最大容量
//typedef int SLDateType; //数据类型重定义
//typedef struct SeqList //顺序表结点结构体定义及类型重定义
//{
// SLDateType data[MAX_SIZE]; //数组
// int size; //记录当前数据个数
//}SeqList;
/* 动态顺序表 */
#define INIT_NUM 4 //初始化顺序表,默认开辟INIT_NUM个空间
typedef int SLDataType; //数据类型重定义
typedef struct SeqList //顺序表结点结构体定义及类型重定义
{
SLDataType* data;
int size;
int capacity;
}SeqList;
//顺序表基本接口
void SeqListInit(SeqList* psl); // 顺序表初始化
void CheckCapacity(SeqList* psl); // 检查空间,如果满了,进行增容
void SeqListPushBack(SeqList* psl, SLDataType x); // 顺序表尾插
void SeqListPopBack(SeqList* psl); // 顺序表尾删
void SeqListPushFront(SeqList* psl, SLDataType x); // 顺序表头插
void SeqListPopFront(SeqList* psl); // 顺序表头删
int SeqListFind(SeqList* psl, SLDataType x); // 顺序表查找
void SeqListInsert(SeqList* psl, size_t pos, SLDataType x);// 顺序表在pos位置插入x
void SeqListErase(SeqList* psl, size_t pos); // 顺序表删除pos位置的值
void SeqListDestroy(SeqList* psl); // 顺序表销毁
void SeqListPrint(SeqList* psl); // 顺序表打印
SeqList.c
#include "SeqList.h"
// 顺序表初始化
void SeqListInit(SeqList* psl)
{
assert(psl); // 结构体的地址不能为空
psl->data = NULL;
psl->size = psl->capacity = 0;
}
// 顺序表销毁
void SeqListDestroy(SeqList* psl)
{
assert(psl);
free(psl->data);
psl->capacity = psl->size = 0;
}
// 检查空间,如果满了,进行增容
static void CheckCapacity(SeqList* psl)
{
if (psl->capacity <= psl->size)
{
int NewCapacity = psl->capacity == 0 ? INIT_NUM : psl->capacity * 2;
SLDataType* tmp = realloc(psl->data, NewCapacity * sizeof(SLDataType));
if (!tmp)
{
printf("realloc failed!\n");
exit(-1);
}
psl->data = tmp;
psl->capacity = NewCapacity;
}
}
// 顺序表尾插
void SeqListPushBack(SeqList* psl, SLDataType x)
{
assert(psl); // 结构体地址不能为空
CheckCapacity(psl); // 检测容量,需要的情况下扩容
psl->data[psl->size++] = x;
}
// 顺序表头插
void SeqListPushFront(SeqList* psl, SLDataType x)
{
assert(psl); // 结构体地址不能为空
CheckCapacity(psl); // 检测容量,需要的情况下扩容
int i = 0;
for (i = psl->size; i > 0; i--) // 挪动数据
psl->data[i] = psl->data[i - 1];
psl->data[i] = x;
psl->size++;
}
// 顺序表打印
void SeqListPrint(SeqList* psl)
{
int i = 0;
for (i = 0; i < psl->size; i++)
{
printf("%d ", psl->data[i]);
}
printf("\n");
}
// 顺序表尾删
void SeqListPopBack(SeqList* psl)
{
assert(psl); // 结构体地址不能为空
assert(psl->size > 0); // 删除的前提是要保证有数据可以删
psl->size--;
}
// 顺序表头删
void SeqListPopFront(SeqList* psl)
{
assert(psl); // 结构体地址不能为空
assert(psl->size > 0); // 删除的前提是要保证有数据可以删
int i = 0;
for (i = 0; i < psl->size - 1; i++)
psl->data[i] = psl->data[i + 1];
psl->size--;
}
// 顺序表查找
int SeqListFind(SeqList* psl, SLDataType x)
{
assert(psl);
int i = 0;
for (i = 0; i < psl->size; i++)
{
if (psl->data[i] == x)
return i;
}
return -1;
}
// 顺序表在pos位置插入x
void SeqListInsert(SeqList* psl, size_t pos, SLDataType x)
{
assert(psl);
assert(0 <= pos && pos <= psl->size); // 确保插入位置符合规范,0等价于头插,psl->size等价于尾插
CheckCapacity(psl); // 检测容量,需要的情况下扩容
int i = 0;
for (i = psl->size; i > pos; i--) // 挪动数据
psl->data[i] = psl->data[i - 1];
psl->data[i] = x; // 退出循环时,i就是pos
psl->size++;
}
// 顺序表删除pos位置的值
void SeqListErase(SeqList* psl, size_t pos)
{
assert(psl);
assert(0 <= pos && pos < psl->size); // 可删除的下标区间:[0,psl->size-1]
int i = 0;
for (i = pos; i < psl->size - 1; i++)
psl->data[i] = psl->data[i + 1];
psl->size--;
}