系列文章目录
文章目录
前言
需要根据不同的数据、不同的需求去构建不同的数据间的联系,也就是我们所说的数据结构。让我们来学习第一种数据结构——顺序表。
一、线性表
线性表(linear list)
是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
二、顺序表
(1) 概念及结构
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。顺序表就是数组
顺序表一般可以分为:
-
静态顺序表:使用定长数组存储元素。
-
动态顺序表:使用动态开辟的数组存储。
(2) 动态顺序表的构建
typedef struct SL {
DataType* a; //对应元素类型的指针
int size; //记录有多少个有效数据
int capacity; //空间容量大小
}SL;
我们所创建的顺序表是可以动态次序存储的,那我们可以用指针来指向在堆区开辟的动态内存。
当需要存储不同类型的数据类型时,只需要将typedef后面改成我们想要的类型就可以了,提高了代码适用范围。
(3) 接口函数的实现
1. 初始化
void SLInit(SL* pc) {
assert(pc);
pc->a = NULL;
pc->size = 0;
pc->capacity = 0;
}
将指针设置为空指针,将元素个数和顺序表容量设置为0。
2. 扩容
void SLCheckCapacity(SL* pc) {
//扩容
if (pc->size == pc->capacity) {
int newCapacity = pc->capacity == 0 ? 4 : pc->capacity * 2;
DataType* temp = (DataType*)realloc(pc->a, sizeof(DataType) * newCapacity);
if (temp == NULL) {
perror("realloc fail");
exit(-1);
}
else {
pc->a = temp;
pc->capacity = newCapacity;
}
}
}
顺序表刚刚创建,那么其容量是0,此时我们就给它开辟4个元素大小的空间,如果容量不是0,当容量已经满的时候,我们就直接将容量翻倍。realloc函数中传入的是一个空指针的时候,此时的realloc函数就相当于一个malloc函数。因此,我们就可以只用realloc函数。
开辟完内存之后不能直接用LS->a,来接收。因为如果realloc函数开辟失败,函数就会返回一个空指针。
3. 销毁
void SLDestroy(SL* pc) {
assert(pc);
free(pc->a);
pc->a = NULL;
pc->size = 0;
pc->capacity = 0;
}
释放动态内存,size和capacity置为0。
4. 尾插
void SLPushBack(SL* pc, DataType x) {
assert(pc);
SLCheckCapacity(pc);
//尾增
pc->a[pc->size] = k;
pc->size++;
}
再插入之前,我们先判断一下容量是否已满,如果满了则立即扩容。当容量足够时,我们在顺序表的尾部插入一个数据,并且让元素个数加一。
5. 尾删
void SLPopBack(SL* pc) {
assert(pc);
//温柔的检查
/*if (pc->size == 0) {
return;
}*/
//暴力的检查
assert(pc->size > 0);
pc->size--;
}
尾删只需要size–,这样做的目的就是在顺序表打印的时候,不让其访问最后一个元素,那么此时从表面上来看就完成了数据的删除。
但是这里有一个问题,size的最小值是0,倘若我们的顺序表本身就是空的,那么是不需要进行size–的,假设我们进行了size–的功能,那么我们利用size表示下标,访问顺序表内容的时候,就会出现严重的数组向前越界访问的问题。因此,在实现尾删的过程中需要进行条件的判断。
6. 头插
void SLPushFront(SL* pc, DataType x) {
assert(pc);
SLCheckCapacity(pc);
//挪动数据:整体后移,再头部插入
int end = pc->size - 1;
for (; end >= 0; end--) {
pc->a[end+1] = pc->a[end];
}
pc->a[0] = x;
pc->size++;
}
首先需要检查一下容量,其次我们需要将现存的数据整体向后挪动一个单位,这样做的目的是把第一个位置让出来,以便我们插入新的数据。最后把元素个数size++。一定是从最后一个元素开始挪动,不然会发生元素覆盖!
7. 头删
void SLPopFront(SL* pc) {
assert(pc);
//越界写可能会被检查出来——抽查
//越界写基本不会被检查出来
//不同编译器查越界方式不同
//往往会在数组的后面定义值,通过值的改变来判断是否越界
//顺序表为空了,不要再头删了
assert(pc->size > 0);
int begin = 1;
while (begin < pc->size) {
pc->a[begin - 1] = pc->a[begin];
begin++;
}
pc->size--;
}
头删和尾删的大体逻辑是相同的,我们首先需要检查一下size是否为0。接下来的操作我们只需要让后面的数据覆盖前面的数据,最终就完成了头删的效果。注意一定是从第二个元素开始向前覆盖,避免元素覆盖!
8. 随机插入
void SLInsert(SL* pc, int pos, DataType x) {
assert(pc);
assert(pos >= 0);
assert(pos <= pc->size);
SLCheckCapacity(pc);
int end = pc->size;
while (end > pos) {
pc->a[end] = pc->a[end-1];
end--;
}
pc->a[end] = x;
pc->size++;
}
我们可以在顺序表的指定位置去插入数据。我们假设插入位置的下标是pos,那么我们只需要将pos位置到最后的所有数据整体向后移动,然后把pos的位置空出来,再在pos的位置插入一个新的元素,最后size++。
我们直到顺序表的一大特性就是连续性,元素与元素之间必须是连续存储的,所以当我们在与末尾元素间隔一段距离的地方插入一个元素,这样就会导致整个顺序表是不连续的,中间出现了空白部分。虽然在逻辑上出现了错误,但是由于我们的容量一般情况下大于元素个数,所以系统不会报错。需要断言一下即可。
9. 随机删除
void SLErase(SL* pc, int pos) {
assert(pc);
assert(pos >= 0);
assert(pos < pc->size);
int begin = pos;
while (begin < pc->size-1) {
pc->a[begin] = pc->a[begin+1];
begin++;
}
pc->size--;
}
涉及到删除,我们首先需要断言一下size是否为0。然后将pos后面的数据整体向前移动,覆盖原来pos位置所对的数据,最终size–。即完成了指定位置删除功能。同时,我们还需要注意到顺序表的连续性,因此我们就能写下下图所示的代码:
10. 查找
int SLFind(SL* pc, DataType x,int begin) {
assert(pc);
for (int i = begin; i < pc->size; i++) {
if (pc->a[i] == x) {
return i;
}
}
return -1;
}
遍历一遍顺序表即可。找到了则返回所查找元素的下标,找不到则返回-1。
11. 打印
void SLPrint(SL* pc) {
assert(pc);
int i = 0;
for (i = 0; i < pc->size; i++) {
printf("%d ", pc->a[i]);
}
printf("\n");
}
同样也是遍历一下顺序表即可。
总结
顺序表在逻辑和物理结构上是紧密连续的,插入或删除数据要注意挪动的方式,小心临界值。
顺序表缺陷:
- 空间不够,需要扩容,但扩容是有一定代价的,还可能会存在一定空间浪费。
- 头部或者中部插入删除,需要挪动数据,效率低下。
顺序表优势: - 可以随机访问数据