目录
[1 · 线性表](#1 · 线性表)
[2 · 顺序表](#2 · 顺序表)
[2 - 1 · 顺序表与数组的区别](#2 - 1 · 顺序表与数组的区别)
[2 - 2 · 静态顺序表](#2 - 2 · 静态顺序表)
[2 - 3 · 动态顺序表](#2 - 3 · 动态顺序表)
[3 · 接口及实现](#3 · 接口及实现)
[3 - 1 · 接口总览](#3 - 1 · 接口总览)
[3 - 2 · 初始化,检查容量,销毁](#3 - 2 · 初始化,检查容量,销毁)
[3 - 2 - 1 · 初始化](#3 - 2 - 1 · 初始化)
[3 - 2 - 2 · 检查容量](#3 - 2 - 2 · 检查容量)
[3 - 2 - 3 · 销毁](#3 - 2 - 3 · 销毁)
[3 - 2 - 4 · 测试](#3 - 2 - 4 · 测试)
[3 - 3 · 头插,头删,打印](#3 - 3 · 头插,头删,打印)
[3 - 3 - 1 · 头插](#3 - 3 - 1 · 头插)
[3 - 3 - 2 · 头删](#3 - 3 - 2 · 头删)
[3 - 3 - 3 · 打印](#3 - 3 - 3 · 打印)
[3 - 3 - 4 · 测试](#3 - 3 - 4 · 测试)
[3 - 4 · 尾插,尾删](#3 - 4 · 尾插,尾删)
[3 - 4 - 1 · 尾插](#3 - 4 - 1 · 尾插)
[3 - 4 - 2 · 尾删](#3 - 4 - 2 · 尾删)
[3 - 4 - 3 · 测试](#3 - 4 - 3 · 测试)
[3 - 5 · 查找,指定位置插入,指定位置删除](#3 - 5 · 查找,指定位置插入,指定位置删除)
[3 - 5 - 1 · 查找](#3 - 5 - 1 · 查找)
[3 - 5 - 2 · 指定位置插入](#3 - 5 - 2 · 指定位置插入)
[3 - 5 - 3 · 指定位置删除](#3 - 5 - 3 · 指定位置删除)
[3 - 5 - 4 · 测试](#3 - 5 - 4 · 测试)
[4 · 顺序表的缺点](#4 · 顺序表的缺点)
1 · 线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表有:顺序表、链表、栈、队列、字符串...
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
数组相信大伙都不陌生,链式结构如下图:

可以看出,链式结构在物理结构上并不是连续的,而我们熟知的数组在物理结构上是连续的,这两种结构各有优劣。简单来说,数组可以随机访问,而链式结构方便增加与删除,后续会详细介绍。
2 · 顺序表
2 - 1 · 顺序表与数组的区别
简单来说,顺序表的底层结构是数组,顺序表在此基础上进行封装,实现了增删查改多种功能。
2 - 2 · 静态顺序表
静态顺序表是用定长数组来进行存储:
如下:
#define MAX 100
typedef int SLDataType;
typedef struct SeqList
{
SLDataType a[MAX];
int size;
}SeqList;
静态顺序表有个很明显的缺陷,就是数组的大小要给多少,给少了可能不够用,给多了可能又会浪费。
因此,本篇将着重介绍动态顺序表。
2 - 3 · 动态顺序表
动态顺序表就是按需申请,不够用的时候就进行扩容:
typedef int SLDataType;
//动态顺序表
typedef struct SeqList
{
SLDataType* a;
int size;//有效数据总个数
int capacity;//总容量
}SeqList;
这里用到了 typedef ,最上面的是方便进行存储类型的修改,在代码实现中用 SLDataType,到时候如果想要修改存储的类型,只需要改这里一处即可。
下面在结构体这里的 typedef 是方便后续使用,可以少写 struct。
3 · 接口及实现
3 - 1 · 接口总览
如下:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLDataType;
//动态顺序表
typedef struct SeqList
{
SLDataType* a;
int size;//有效数据总个数
int capacity;//总容量
}SeqList;
//初始化
void SeqListInit(SeqList* p);
//检查容量,不够就扩容
void CheakCapacity(SeqList* p);
//头插
void SeqListPushFront(SeqList* p, SLDataType x);
//头删
void SeqListPopFront(SeqList* p);
// 尾插
void SeqListPushBack(SeqList* p, SLDataType x);
// 尾删
void SeqListPopBack(SeqList* p);
//查找
int SeqListFind(SeqList* p, SLDataType x);
//指定位置插入
void SeqListInsert(SeqList* p, int pos, SLDataType x);
//指定位置删除
void SeqListErase(SeqList* p, int pos);
//销毁
void SeqListDestroy(SeqList* p);
//打印
void SeqListPrint(SeqList* p);
下面我们一个个介绍:
3 - 2 · 初始化,检查容量,销毁
3 - 2 - 1 · 初始化
代码如下:
void SeqListInit(SeqList* p)
{
assert(p);
p->a = NULL;
p->size = p->capacity = 0;
}
先进行assert断言,防止接收的参数为空指针,保证代码的健壮性。
随后正常初始化,这里可以初始化的时候就给一个初始的容量,上面写的是初始不给容量的,这两种写法关系到检查容量的写法。
3 - 2 - 2 · 检查容量
代码如下:
void CheakCapacity(SeqList* p)
{
assert(p);
int newCapacity = 0;
if (p->size == p->capacity)
{
newCapacity = p->capacity == 0 ? 4 : p->capacity * 2;
SLDataType* ptr = (SLDataType*)realloc(p->a,sizeof(SLDataType) * newCapacity * 2);
if (ptr == NULL)
{
perror("realloc");
exit(1);
}
p->a = ptr;
p->capacity = newCapacity;
}
}
如果容量不够就需要扩容,那么就产生了一个问题,一次扩容应该扩多少?
我们扩容需要用到realloc,如果频繁使用,会产生较大的消耗。
因此我们决定扩容一次将原有容量翻倍。
那么触发扩容的条件是表满,即 size == capacity
如果初始化的时候我们没有给初始容量的话,此时的capacity 是0,而0*2 仍会等于0,所以我们用一个三目操作符来确定新的容量,再进行扩容,最后更新容量。
如果初始化的时候给了初始容量,此时就没有capacity 为0的情况了。
3 - 2 - 3 · 销毁
代码如下:
void SeqListDestroy(SeqList* p)
{
if(p->a)
{
free(p->a);
}
p->a = NULL;
p->size = p->capacity = 0;
}
将开辟的空间释放,然后指针置空,size 与 capacity 置0
3 - 2 - 4 · 测试
我们对上面的功能测试一下:
#include "SeqList.h"
void Test1()
{
SeqList s;
SeqListInit(&s);
CheakCapacity(&s);
SeqListDestroy(&s);
}
int main()
{
Test1();
return 0;
}
通过调试窗口:
初始化之后:

检查容量之后:

销毁:

3 - 3 · 头插,头删,打印
3 - 3 - 1 · 头插
代码如下:
void SeqListPushFront(SeqList* p, SLDataType x)
{
assert(p);
CheakCapacity(p);
//如果空表,直接给
if (p->size == 0)
{
p->a[0] = x;
p->size++;
}
else
{
//非空,集体后移再给
int i = 0;
for (i = p->size; i > 0; i--)
{
p->a[i] = p->a[i - 1];
}
p->a[0] = x;
p->size++;
}
}
头插就是将数据插入到表头的位置。
如果表满,显然是不能插入的,因此我们先检查容量,如果表是空的,那么就可以直接插入数据,如果非空,为了保持顺序与原有数据,就需要先将原有元素向后移动1位,再在表头插入数据,最后,当插入数据成功,更新size的值,也就是+1。
3 - 3 - 2 · 头删
代码如下:
void SeqListPopFront(SeqList* p)
{
assert(p);
//空表不能删
assert(p->size);
//集体前移,size--
int i = 0;
for (i = 0; i < p->size - 1; i++)
{
p->a[i] = p->a[i + 1];
}
p->size--;
}
头删就是删除表头位置的数据。
空表显然是不能进行删除的,因此我们用assert断言,size不能为0。
我们只需要让除了表头位置的数据向前移动,覆盖原有值,最后更新size的值,也就是-1即可。
至于最后一个多出来的数据,可处理也可不处理,我们更新了size 也就更新了有效数据的总个数,原size 处的值(-1之前)对我们是无影响的。
3 - 3 - 3 · 打印
代码如下:
void SeqListPrint(SeqList* p)
{
assert(p);
int i = 0;
for (i = 0; i < p->size; i++)
{
printf("%d ", p->a[i]);
}
printf("\n");
}
打印就是将顺序表中的元素打印出来。
注意:这里的形参可以不用使用指针,因为无需修改我们顺序表的成员。
但是还是建议使用指针,为了保持接口一致性,我们的接口当需要访问顺序表的时候统一使用的是指针,如果有的接口使用指针,有的接口不使用指针,就会造成混乱,不知道什么时候传参传什么,增加了使用时的记忆成本,因此,将参数统一使用指针,就是保持了接口一致,方便使用。
3 - 3 - 4 · 测试
我们对上面的功能测试一下:
void Test2()
{
SeqList s;
SeqListInit(&s);
SeqListPushFront(&s, 1);
SeqListPrint(&s);
SeqListPushFront(&s, 2);
SeqListPrint(&s);
SeqListPushFront(&s, 3);
SeqListPrint(&s);
SeqListPushFront(&s, 4);
SeqListPrint(&s);
SeqListPopFront(&s);
SeqListPrint(&s);
SeqListPopFront(&s);
SeqListPrint(&s);
SeqListPopFront(&s);
SeqListPrint(&s);
SeqListPopFront(&s);
SeqListPrint(&s);
/*SeqListPopFront(&s);
SeqListPrint(&s);*/
SeqListDestroy(&s);
}
int main()
{
//Test1();
Test2();
return 0;
}
我们进行了4次头插,每次头插完进行一次打印,再进行4次头删,每次删完进行一次打印。
运行一下:

在此基础上,如果我们再次进行头删,就会触发我们的assert断言了。
将最后一个头删的注释取消后再运行:

3 - 4 · 尾插,尾删
3 - 4 - 1 · 尾插
代码如下:
void SeqListPushBack(SeqList* p, SLDataType x)
{
assert(p);
CheakCapacity(p);
p->a[p->size] = x;
p->size++;
}
先检查容量,然后在顺序表尾插入数据,最后更新size 即可。
3 - 4 - 2 · 尾删
代码如下:
void SeqListPopBack(SeqList* p)
{
assert(p);
//空表不能删
assert(p->size);
p->size--;
}
空表显然不能删,断言之后直接让size-1即可。
至于被删除的位置的值,可修改也可不修改,对我们是无影响的。
3 - 4 - 3 · 测试
我们测试一下上面的功能:
void Test3()
{
SeqList s;
SeqListInit(&s);
SeqListPushBack(&s, 1);
SeqListPrint(&s);
SeqListPushBack(&s, 2);
SeqListPrint(&s);
SeqListPushBack(&s, 3);
SeqListPrint(&s);
SeqListPopBack(&s);
SeqListPrint(&s);
SeqListPopBack(&s);
SeqListPrint(&s);
SeqListPopBack(&s);
SeqListPrint(&s);
/*SeqListPopBack(&s);
SeqListPrint(&s);*/
SeqListDestroy(&s);
}
int main()
{
//Test1();
//Test2();
Test3();
return 0;
}
我们进行三次尾插再三次尾删,每次执行完就打印一次
运行一下:

在此基础上,如果再次进行一次尾删,就会触发我们的assert断言了。
3 - 5 · 查找,指定位置插入,指定位置删除
3 - 5 - 1 · 查找
代码如下:
int SeqListFind(SeqList* p, SLDataType x)
{
assert(p);
int i = 0;
for (i = 0; i < p->size; i++)
{
if (p->a[i] == x)
{
return i;
}
}
//没找到就返回一个无效的下标
return -1;
}
查找就是找到一个值在顺序表中的位置,我们这里找的是下标
遍历顺序表并比较,找到了就返回,如果没找到就返回一个无效的下标。
3 - 5 - 2 · 指定位置插入
代码如下:
void SeqListInsert(SeqList* p, int pos, SLDataType x)
{
assert(p);
//位置要在表内
assert(pos >= 0 && pos <= p->size);
CheakCapacity(p);
int i = 0;
for (i = p->size - 1; i >= pos; i--)
{
p->a[i + 1] = p->a[i];
}
p->a[pos] = x;
p->size++;
}
由于顺序表的底层结构是数组,数据是连续存放的,因此可以做到随机访问。
这里指定的位置是下标位置。
首先要确保指定的位置在顺序表内,所以使用assert断言。pos是可以等于size的,虽然下标为size的位置是不在顺序表实际范围中的,但是是可以插入的,相当于尾插。
随后,既然是插入数据,必然是要先检查容量的,之后就是pos及pos后的元素先向后移动1位,再将数据插入到pos位置,最后更新size。
当然,如果指定位置为表头,那就等同于头插,指定位置为表尾,等同于尾插。
3 - 5 - 3 · 指定位置删除
代码如下:
void SeqListErase(SeqList* p, int pos)
{
assert(p);
//空表不能删
assert(p->size);
//位置要在表内
assert(pos < p->size);
int i = 0;
for (i = pos; i < p->size - 1; i++)
{
p->a[i] = p->a[i + 1];
}
p->size--;
}
这里的指定位置是下标位置。
空表显然不能删,所以用了assert断言。
确保指定位置要在表内,所以用了assert断言,与指定位置插入不同的是:pos不能等于size,因为下标为size的位置是不在顺序表中的。
只需让pos位置后的元素向前覆盖1位,再更新size即可。
当然,如果指定位置为表头,那就等同于头删,指定位置为表尾,等同于尾删。
3 - 5 - 4 · 测试
我们对上面的功能测试一下:
void Test4()
{
SeqList s;
SeqListInit(&s);
SeqListPushBack(&s, 1);
SeqListPushBack(&s, 2);
SeqListPushBack(&s, 3);
SeqListPushBack(&s, 4);
SeqListPrint(&s);
SeqListInsert(&s, 0, 5);
SeqListPrint(&s);
SeqListInsert(&s, 5, 6);
SeqListPrint(&s);
SeqListErase(&s, 0);
SeqListPrint(&s);
SeqListErase(&s, 4);
SeqListPrint(&s);
int find = SeqListFind(&s, 4);
printf("find == %d\n", find);
find = SeqListFind(&s, 1);
printf("find == %d\n", find);
find = SeqListFind(&s, 6);
printf("find == %d\n", find);
SeqListDestroy(&s);
}
int main()
{
//Test1();
//Test2();
//Test3();
Test4();
return 0;
}
运行一下:

4 · 顺序表的缺点
- 中间/头部的插入删除,时间复杂度为O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
总结
以上简单介绍了顺序表有关内容,关于数据结构其余内容,请期待后续更新
以上内容如有错误或不准确之处,欢迎指出,或者你有更好的想法,也欢迎交流。