目 录
前 言
线性表(linear list)是n个具有相同特性的数据元素的有限序列。线性表是一种在实际中广泛使用的数据结构,常见的线性表有:顺序表、链表、栈、队列、字符串等
线性表在逻辑上是线性结构(即连续的一条直线)。但在物理结构上并不一定是连续的。线性表在物理上存储时,通常以数组或链式结构的形式存储。
本文将要介绍的即是线性表中的顺序表。
1 顺序表的概念和结构
顺序表是用一段物理地址连续的存储单元依此存储数据元素的线性结构。一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表一般可以分为:
● 静态顺序表:使用定长数组存储元素
● 动态顺序表:使用动态开辟的数组存储元素
可以发现,静态顺序表只适用于确定知道需要存多少数据的场景,使用限制较多,如果静态顺序表中的N定大了,容易造成空间浪费,定小了又会造成空间不足。因此在实际中多使用的是动态顺序表,根据需要动态的分配空间大小。本文中也将主要介绍动态顺序表的实现。
2 动态顺序表的接口实现
2.1 顺序表的初始化、打印和销毁
如上所说,动态顺序表是通过动态开辟的数组来存储数据的,但为了方便了解顺序表的存储情况,比如:当前表中有多少个数据,当前表的容量是多少,我们还需要定义两个变量,一个用来记录当前表中的有效元素个数,一个用来记录当前表的容量。基于此,我们考虑用一个结构体来表示顺序表,结构体中包含指向动态开辟的数组的指针及额外的两个变量(size、capacity)。
🍚如下是顺序表结构体的声明:
typedef int SLDataType; //为了适应多种类型数据的存储,方便进行类型修改,首先对数据类型进行重命名
typedef struct SeqList {
SLDataType* data; //存储数据
int size; //记录表中数据个数
int capacity; // 记录线性表容量大小
}SeqList;
🍚在使用顺序表之前,首先要做的自然是顺序表的初始化了,如下所示是顺序表初始化接口的实现:
//顺序表初始化
void SeqListInit(SeqList* sl) {
assert(sl); //确保传入的不是空指针,以便及时发现空指针访问错误
sl->data = NULL; //初始化时将指向动态数组的指针先置空
sl->size = 0; //初始时表中无数据
sl->capacity = 0; //初始时可以先不开辟空间,等需要存数据时在开
}
考虑到形参是实参的一份临时拷贝,如果直接以结构体作为函数参数,若结构体过大,拷贝传参时无疑会产生较大的消耗,因此在实现顺序表的各个接口时,均采用结构体指针传参的形式,如此,调用函数时,只需要拷贝一份结构体的指针,就可以通过指针访问结构体的各个成员了。
🍚为了直观的看到顺序表中的存储情况,我们可以设计一个接口打印出顺序表中的存储内容,以下是接口的实现:
//顺序表数据打印
void SeqListPrint(SeqList* sl) {
assert(sl);
int i = 0;
//逐个打印顺序表中元素
while (i < sl->size) {
printf("%d ", sl->data[i]);
i++;
}
printf("\n");
}
🍚当我们不需要再使用顺序表时,可以将顺序表进行销毁,因此我们也设计一个接口来实现:
//顺序表销毁
void SeqListDestroy(SeqList* sl) {
assert(sl);
//如果指向动态数组的指针不为空,则通过指针释放动态数组,并将指针置空,以防之后被误用
if (!(sl->data)) {
free(sl->data);
sl->data = NULL;
}
sl->size = 0; //清空表中数据,即表中有效数据个数归零
sl->capacity = 0; //动态数组释放后,表的容量自然也归零了
}
2.2 顺序表扩容
🍚在开头我们也说到了,对于动态顺序表,当当前表容量不够时是可以进行扩容的,那如何来实现呢?在C语言中有一个动态内存开辟函数(realloc)可以用来扩容,以下我们就来具体实现一下顺序表的扩容接口:
//顺序表容量检查
void SLCheckCapacity(SeqList* sl) {
assert(sl);
//当表中有效元素个数与当前表容量相等时,表示当前表已存满数据,即需要扩容
if (sl->size == sl->capacity) {
//每次扩容的大小可根据自己需求指定,这里设置的是每次扩容后,新的容量为原容量的两倍。
//如上我们设置顺序表初始化时是没有开辟空间的,因此这里还需判断一开始容量为0的情况,此时直接先开辟四个数据类型大小的空间
//realloc函数在参数指针为空时相当于malloc函数
int newCapacity = sl->capacity == 0 ? 4 : 2 * (sl->capacity);
SLDataType* temp = (SLDataType*)realloc(sl->data, newCapacity * sizeof(SLDataType));
//注意空间开辟失败的情况,如果开辟失败将返回空指针,因此这里先用临时指针接收指向返回空间的指针,
//对空指针进行判断,确保不会造成空指针访问时,在将指针赋值给表中指针,同时更新容量
assert(temp);
sl->data = temp;
sl->capacity = newCapacity;
}
}
2.3 顺序表尾插
🍚所谓顺序表尾插,即是在顺序表末尾插入新元素,此时只需要确保表中还有足够的空间可以插入数据。尾插不会影响之前的数据,因此插入新元素后也不需要改变之前元素的位置,以下是接口的实现:
//顺序表尾插
void SeqListPushBack(SeqList* sl, SLDataType data) {
assert(sl);
//要想往表中插入数据,首先要确定表中还有足够的空间,否则在非开辟空间中访问会造成数组越界问题,如果空间不够则可以扩容
SLCheckCapacity(sl);
//经过容量检查后可以确保表中还有空间可以插入数据,则尾插只需要将数据插入到表中现有所有效元素最后一位的后一个位置
//基于表中数据下标是从0开始,所以表中有效元素个数即为要进行尾插的位置(下标)
sl->data[sl->size] = data;
sl->size++; //将数据插入后更新表中有效元素的个数
}
2.4 顺序表头插
🍚同样的,只要是插入数据,都必须先确保有足够的空间,所以都需要检查空间容量。但与尾插不同的是,要从顺序表头部插入数据,则将影响之前已有的所有数据,即需要将之前的数据整体向后挪一位,再将数据放入第一个位置,同时要记得将有效元素的个数加一。如图所示为顺序表头插示意图,以下是对应接口的实现:
//顺序表头插
void SeqListFront(SeqList* sl, SLDataType data) {
assert(sl);
SLCheckCapacity(sl);
int end = sl->size ;
while (end > 0) {
sl->data[end] = sl->data[end - 1];
end--;
}
sl->data[0] = data;
sl->size++;
}
2.5 顺序表尾删
🍚顺序表尾插与尾删的实现都相对其它接口要简单些,顺序表尾删即意味着不再需要访问当前所有有效元素的最后一个元素,那只需要将有效元素的个数减一即可,但这里还需要注意的一点是:只有当表中有数据时才可以删除数据,如果表中都没有数据了(size = 0),此时再将有效元素的个数减一(size - 1 = -1),那size将变成负数,而计数是不可能为负值的,所以这里还需要进行空表判断,如果表空则不需要再删除数据了。以下是接口的实现:
//顺序表尾删
void SeqListPopBack(SeqList* sl) {
assert(sl);
assert(sl->size > 0); //确保表中还有数据
sl->size--; //删除数据后,表中有效元素个数减一
}
2.6 顺序表头删
🍚如图所示,顺序表头删时无需另外删除头部数据,只需将除头部数据外的剩余数据逐个向前挪动覆盖原数据即可,同时更新有效元素个数。以下是接口实现:
//顺序表头删
void SeqListPopFront(SeqList* sl) {
assert(sl);
assert(sl->size > 0); //确保表非空
int begin = 1;
//从下标为1的位置开始逐个向前挪动覆盖原数据
while (begin < sl->size) {
sl->data[begin - 1] = sl->data[begin];
begin++;
}
sl->size--;
}
2.7 顺序表查找
🍚顺序表查找即是通过遍历表中有效元素,对比查找表中是否有目标元素,一旦找到则返回对应下标,找不到则返回-1。如果不想从头开始查找的话,也可以指定查找起始位置,以下是对应接口的实现:
//顺序表查找数据
//begin:开始查找的位置
int SeqListFind(SeqList* sl, SLDataType data, int begin) {
assert(sl);
int i = begin;
while (i < sl->size) {
if (sl->data[i] == data) {
return i; //找到即返回对应下标
}
i++;
}
return -1; //未找到则返回-1
}
2.8 顺序表指定位置插入
🍚在顺序表指定位置插入数据,不会影响指定位置之前的数据,但会影响从指定位置开始之后的数据,需要先将会被影响的这部分数据整体向后挪动一位,再在指定位置上插入数据。需要注意的是,除了容量的检查,所输入的指定位置也应当是有效的,即指定下标应当大于等于0,并小于等于size(有效元素个数)。可以发现当指定下标为0时,此时的操作相当于头插;当指定下标为size时,此时的操作相当于尾插。以下是对应接口的实现:
//在指定位置插入数据
void SeqListInsert(SeqList* sl, int position, SLDataType data) {
assert(sl);
assert(position >= 0);
assert(position <= sl->size);
SLCheckCapacity(sl);
int end = sl->size; //从后往前逐个将数据向后挪动一位
while (end > position) {
sl->data[end] = sl->data[end - 1];
end--;
}
sl->data[end] = data;
sl->size++; //更新有效元素个数
}
2.9 顺序表指定位置删除
🍚与头删相似,删除指定位置的数据,只需要将指定位置之后的数据整体向前挪动一位即可,也不需要另外将所要删除的数据置0或做什么别的修改,只要后面的数据覆盖了要删除的数据,目标数据自然就消失了。事实上,当指定下标为0时,该删除操作即为头删,当指定下标为最后一位有效元素的下标(size - 1)时,该删除操作即为尾删。以下是对应接口的实现:
//从指定位置删除数据
void SeqListErase(SeqList* sl, int position) {
assert(sl);
assert(position >= 0);
assert(position < sl->size);
int begin = position + 1; //从前往后逐个将数据向前挪动一位
while (begin < sl->size) {
sl->data[begin - 1] = sl->data[begin];
begin++;
}
sl->size--; //更新有效元素个数
}
以上就是动态顺序表的主要接口的具体实现,通过这些接口我们即可以实现对顺序表的增删查改。可以发现,其实我们只需要实现顺序表在指定位置上的数据插入接口与数据删除接口就可以完成在任意位置数据的插入和删除了,因此如果需要实现顺序表的头插、尾插、头删、尾删功能,也可以使用这两个接口。
3 动态顺序表的整体实现
在上一节中,我们实现了动态顺序表的主要接口,接下来我们可以将这些接口整合起来:将各接口及结构体的声明统一编写在一个头文件(这里是SeqList.h)中,并在该头文件中包含实现接口时所需要用到的库文件;接着将各接口的具体实现编写在与头文件对应的.c
文件中,并在该.c
文件中包含对应的头文件(SeqList.h)。
SeqList.h:
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLDataType;
typedef struct SeqList {
SLDataType* data; //存储数据
int size; //记录表中数据个数
int capacity; // 记录顺序表容量大小
}SeqList;
//顺序表初始化
void SeqListInit(SeqList* sl);
//顺序表数据打印
void SeqListPrint(SeqList* sl);
//顺序表销毁
void SeqListDestroy(SeqList* sl);
//顺序表容量检查
void SLCheckCapacity(SeqList* sl);
顺序表尾插
//void SeqListPushBack(SeqList* sl, SLDataType data);
顺序表头插
//void SeqListFront(SeqList* sl, SLDataType data);
顺序表尾删
//void SeqListPopBack(SeqList* sl);
顺序表头删
//void SeqListPopFront(SeqList* sl);
//在指定位置插入数据
void SeqListInsert(SeqList* sl, int position, SLDataType data);
//从指定位置删除数据
void SeqListErase(SeqList* sl, int position);
//顺序表修改数据
void SeqListModify(SeqList* sl, int position, SLDataType data);
//顺序表查找数据
int SeqListFind(SeqList* sl, SLDataType data, int begin);
SeqList.c:
#define _CRT_SECURE_NO_WARNINGS 1
#include "SeqList.h"
//顺序表初始化
void SeqListInit(SeqList* sl) {
assert(sl);
sl->data = NULL;
sl->size = 0;
sl->capacity = 0;
}
//顺序表数据打印
void SeqListPrint(SeqList* sl) {
assert(sl);
int i = 0;
while (i < sl->size) {
printf("%d ", sl->data[i]);
i++;
}
printf("\n");
}
//顺序表销毁
void SeqListDestroy(SeqList* sl) {
assert(sl);
if (!(sl->data)) {
free(sl->data);
sl->data = NULL;
}
sl->size = 0;
sl->capacity = 0;
}
//顺序表容量检查
void SLCheckCapacity(SeqList* sl) {
assert(sl);
if (sl->size == sl->capacity) {
int newCapacity = sl->capacity == 0 ? 4 : 2 * (sl->capacity);
SLDataType* temp = (SLDataType*)realloc(sl->data, newCapacity * sizeof(SLDataType));
assert(temp);
sl->data = temp;
sl->capacity = newCapacity;
}
}
顺序表尾插
//void SeqListPushBack(SeqList* sl, SLDataType data) {
// assert(sl);
// SLCheckCapacity(sl);
// sl->data[sl->size] = data;
// sl->size++;
//}
顺序表头插
//void SeqListFront(SeqList* sl, SLDataType data) {
// assert(sl);
// SLCheckCapacity(sl);
// int end = sl->size ;
// while (end > 0) {
// sl->data[end] = sl->data[end - 1];
// end--;
// }
// sl->data[0] = data;
// sl->size++;
//}
顺序表尾删
//void SeqListPopBack(SeqList* sl) {
// assert(sl);
// assert(sl->size > 0);
// sl->size--;
//}
顺序表头删
//void SeqListPopFront(SeqList* sl) {
// assert(sl);
// assert(sl->size > 0);
// int begin = 1;
// while (begin < sl->size) {
// sl->data[begin - 1] = sl->data[begin];
// begin++;
// }
// sl->size--;
//}
//在指定位置插入数据
void SeqListInsert(SeqList* sl, int position, SLDataType data) {
assert(sl);
assert(position >= 0);
assert(position <= sl->size);
SLCheckCapacity(sl);
int end = sl->size;
while (end > position) {
sl->data[end] = sl->data[end - 1];
end--;
}
sl->data[end] = data;
sl->size++;
}
//从指定位置删除数据
void SeqListErase(SeqList* sl, int position) {
assert(sl);
assert(position >= 0);
assert(position < sl->size);
int begin = position + 1;
while (begin < sl->size) {
sl->data[begin - 1] = sl->data[begin];
begin++;
}
sl->size--;
}
//顺序表修改数据
void SeqListModify(SeqList* sl, int position, SLDataType data) {
assert(sl);
assert(position >= 0);
assert(position < sl->size);
sl->data[position] = data;
}
//顺序表查找数据
//begin:开始查找的位置
int SeqListFind(SeqList* sl, SLDataType data, int begin) {
assert(sl);
int i = begin;
while (i < sl->size) {
if (sl->data[i] == data) {
return i;
}
i++;
}
return -1;
}
以上是我对数据结构中顺序表的一些学习记录总结,如有错误,希望大家帮忙指正,也欢迎大家给予建议和讨论,谢谢!