本文介绍线性结构中的顺序表,这种连续排序的方式在需要频繁访问特定位置元素的应用场景(如矩阵运算、数组查询)中非常高效。
初阶数据结构:顺序表
本文讲解线性表中的顺序表结构,涵盖静态与动态定义、初始化、销毁、容量检查及增删查改接口实现。详细说明了尾插优于头插的时间复杂度,并指出增容时的空间浪费问题。提供了完整的 C 语言代码示例,包含头文件声明与源文件实现,适合初学者理解顺序表的底层逻辑与内存管理。

本文讲解线性表中的顺序表结构,涵盖静态与动态定义、初始化、销毁、容量检查及增删查改接口实现。详细说明了尾插优于头插的时间复杂度,并指出增容时的空间浪费问题。提供了完整的 C 语言代码示例,包含头文件声明与源文件实现,适合初学者理解顺序表的底层逻辑与内存管理。

本文介绍线性结构中的顺序表,这种连续排序的方式在需要频繁访问特定位置元素的应用场景(如矩阵运算、数组查询)中非常高效。
线性表是 n 个具有相同特性的数据元素的有限序列。线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑上是线性结构,也就是说是一条连续的直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
顺序表是线性表的一种存储方式,它是用一组连续的存储单元依次存储线性表的数据元素。简单来说,就像是把线性表中的元素一个挨着一个地放在数组中进行增删查改工作,就像在一排连续的小格子里存放东西一样。

typedef int SLDataType; // 静态开辟(不推荐)
#define N 7
typedef struct SeqList {
SLDataType array[N]; // 指向动态开辟的数组
int size; // 数据个数
} SL;
静态顺序表就是创建一个普通的数组,通常数组的长度大小是固定的,所以这种存储方式是不灵活的。静态顺序表的定长数组导致 N 定大了空间开多了浪费,开少了不够用,只适用于确定知道需要存多少数据的场景。
typedef int SLDataType;
#define INIT_CAPACITY 4
typedef struct SeqList {
SLDataType* a; // 指向动态开辟的数组
int size; // 数据个数
int capacity; // 空间容量
} SL;
动态顺序表是用的最多的顺序表,符合大多数场景下的使用,可以根据场景的需要调整数组的大小,比静态数组更加灵活。
void SLPrint(SL* ps) {
assert(ps);
for (int i = 0; i < ps->size; ++i) {
printf("%d ", ps->a[i]);
}
printf("\n");
}
因为要访问 ps 指针,所以基本功能的实现都要用断言来预防非法访问,顺序表的打印和数组的打印基本思路一致,这里不过多赘述。
void SeqInit(SL* ps) {
assert(ps);
ps->a = (SLDataType*)malloc(sizeof(SLDataType) * INIT_CAPACITY);
if (ps->a == NULL) {
perror("malloc fail");
return;
}
ps->size = 0;
ps->capacity = INIT_CAPACITY;
}
对顺序表初始化,这里有个头文件中的宏定义 #define INIT_CAPACITY 4,目的是为了方便修改初始化时的大小,不然每次修改要改多处,定义之后就只需要修改一个地方即可。刚开始 capacity 也要给一定的容量,而不是 0。
void SLDestroy(SL* ps) {
assert(ps);
free(ps->a);
ps->a = NULL;
ps->size = ps->capacity = 0;
}
这里唯一要注意的点就是,释放完动态数组 a 之后要记得将指针置为空,不然会导致野指针的出现。
void SLCheckCapacity(SL* ps) {
assert(ps);
if (ps->size == ps->capacity) {
SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity * 2);
if (tmp == NULL) {
perror("realloc fail");
return;
}
ps->a = tmp;
}
ps->capacity *= 2;
}
size 表示的是当前顺序表存储了多少数据,capacity 表示的是当前顺序表能够存储多少数据,所以当数据存满了之后就需要进行扩容操作,通常我们每次扩容都只扩容两倍,以免空间的浪费。
值得注意的是:realloc 开辟的空间大小不是 ps->capacity * 2,而是 sizeof(SLDataType) * ps->capacity * 2,前者是只考虑了扩大数组元素个数,但是没考虑到每个元素的字节大小,这是需要重点注意的。
void SLPushBack(SL* ps, SLDataType x) {
assert(ps);
SLCheckCapacity(ps);
ps->a[ps->size++] = x;
}
// O(n)
尾插就是在顺序表最后面插入数据,先检查容量是否足够插入数据,然后 ps->a[ps->size++] = x 可以拆分为 ps->a[ps->size] = x 和 ps->size++ 理解,先将下一个数据加入顺序表,然后修改 size 大小。
尾插只需要把 n 个数据依次放到末尾,所以时间复杂度为 O(n)。
void SLPushFront(SL* ps, SLDataType x) {
assert(ps);
SLCheckCapacity(ps);
int end = ps->size - 1;
while (end >= 0) {
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[0] = x;
ps->size++;
}
// O(n^2)
还是一样,先检查容量是否足够插入数据,然后用覆盖的方式,将前一个数据覆盖到后一个数据上,即整体数据向右移动,也可以使用 memmove 函数,最后记得修改 size 大小,然后在开头插入数据。
值得注意的是:头插每次插入 n 个数据之前都需要挪动数据,因此时间复杂度为 O(n²),所以得出结论尾插的效率是高于头插的。
void SLPopBack(SL* ps) {
assert(ps->size > 0);
ps->a[ps->size - 1] = 0;
ps->size--;
}
最后一个数据的索引为 size-1,所以将该位置的数据置为 0 即可,但又有人疑问了,需不需要把删除的空间回收了?答案是不需要也没必要,因为通常空间的回收都是整体回收而不是一部分,而且多出来的空间也有可能被使用。
值得注意的是:要考虑顺序表没有数据的情况,如果没有数据了还删除肯定是会造成访问错误的,所以要加断言 assert(ps->size > 0),头删也是如此。
void SLPopFront(SL* ps) {
assert(ps);
assert(ps->size > 0);
int begin = 1;
while (begin < ps->size) {
ps->a[begin - 1] = ps->a[begin];
++begin;
}
ps->size--;
}
先检查容量是否足够插入数据,然后用覆盖的方式,将后一个数据覆盖到前一个数据上,即整体数据向左移动,也可以使用 memmove 函数,最后记得修改 size 大小。
void SLInsert(SL* ps, int pos, SLDataType x) {
assert(ps);
assert(pos <= ps->size && pos >= 0);
SLCheckCapacity(ps);
int end = ps->size - 1;
while (end >= pos) {
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[pos] = x;
ps->size++;
}
在 pos 位置之后将所有数据向左移,然后在 pos 位置插入数据,注意要断言 pos <= ps->size && pos >= 0,避免传入的 pos 地址是个无效地址。
void SLErase(SL* ps, int pos, SLDataType x) {
assert(ps);
assert(pos <= ps->size && pos >= 0);
int begin = pos + 1;
while (begin < ps->size) {
ps->a[begin - 1] = ps->a[begin];
++begin;
}
ps->size--;
}
定义变量 begin 从要删除元素的下一个位置开始,使用 while 循环将从 pos + 1 位置开始的元素依次向左移动一个位置,覆盖要删除的元素。
值得注意的是:最后一个元素不需要特殊处理。因为顺序表的元素个数是由 size 控制的,当 size 减 1 后,无论原来最后一个元素的值是什么,它都不在有效的元素列表中了,所以不需要对其进行特殊处理,如将其置为某个默认值或进行其他操作。
int SLFind(SL* ps, SLDataType x) {
assert(ps);
for (int i = 0; i < ps->size; i++) {
if (ps->a[i] == x) {
return i;
}
}
return -1;
}
遍历数组,符合则返回索引值,不符合返回 -1。
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLDataType;
// 静态开辟(不推荐)
//#define N 7
//typedef struct SeqList
//{
// SLDataType array[N];
// int size;
//} SL;
// 动态开辟
#define INIT_CAPACITY 4
typedef struct SeqList {
SLDataType* a;
int size;
int capacity;
} SL;
void SeqInit(SL* s);
void SeqDestory(SL* s);
void SLCheckCapacity(SL* ps);
void SLPushBack(SL* ps, SLDataType x);
void SLPushFront(SL* ps, SLDataType x);
void SLPopBack(SL* ps);
void SLPopFront(SL* ps);
void SLInsert(SL* ps, int pos, SLDataType x);
void SLErase(SL* ps, int pos, SLDataType x);
int SLFind(SL* ps, SLDataType x);
#define _CRT_SECURE_NO_WARNINGS
#include "SeqList.h"
// 顺序表打印
void SLPrint(SL* ps) {
assert(ps);
for (int i = 0; i < ps->size; ++i) {
printf("%d ", ps->a[i]);
}
printf("\n");
}
// 顺序表初始化
void SeqInit(SL* ps) {
assert(ps);
ps->a = (SLDataType*)malloc(sizeof(SLDataType) * INIT_CAPACITY);
if (ps->a == NULL) {
perror("malloc fail");
return;
}
ps->size = 0;
ps->capacity = INIT_CAPACITY;
}
// 顺序表销毁
void SLDestroy(SL* ps) {
assert(ps);
free(ps->a);
ps->a = NULL;
ps->size = ps->capacity = 0;
}
// 检查空间,如果满了,进行增容
void SLCheckCapacity(SL* ps) {
assert(ps);
if (ps->size == ps->capacity) {
SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity * 2);
if (tmp == NULL) {
perror("realloc fail");
return;
}
ps->a = tmp;
}
ps->capacity *= 2;
}
// 顺序表尾插
void SLPushBack(SL* ps, SLDataType x) {
assert(ps);
SLCheckCapacity(ps);
ps->a[ps->size++] = x;
}
// 顺序表头插
void SLPushFront(SL* ps, SLDataType x) {
assert(ps);
SLCheckCapacity(ps);
int end = ps->size - 1;
while (end >= 0) {
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[0] = x;
ps->size++;
}
// 顺序表头删
void SLPopFront(SL* ps) {
assert(ps);
assert(ps->size > 0);
int begin = 1;
while (begin < ps->size) {
ps->a[begin - 1] = ps->a[begin];
++begin;
}
ps->size--;
}
// 顺序表尾删
void SLPopBack(SL* ps) {
assert(ps->size > 0);
ps->a[ps->size - 1] = 0;
ps->size--;
}
// 顺序表在 pos 位置插入 x
void SLInsert(SL* ps, int pos, SLDataType x) {
assert(ps);
assert(pos <= ps->size && pos >= 0);
SLCheckCapacity(ps);
int end = ps->size - 1;
while (end >= 0) {
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[pos] = x;
ps->size++;
}
// 顺序表删除 pos 位置的值
void SLErase(SL* ps, int pos, SLDataType x) {
assert(ps);
assert(pos <= ps->size && pos >= 0);
int begin = pos + 1;
while (begin < ps->size) {
ps->a[begin - 1] = ps->a[begin];
++begin;
}
ps->size--;
}
// 顺序表查找
int SLFind(SL* ps, SLDataType x) {
assert(ps);
for (int i = 0; i < ps->size; i++) {
if (ps->a[i] == x) {
return i;
}
}
return -1;
}


微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online