跳到主要内容
数据结构:顺序表的概念、实现与操作 | 极客日志
C 算法
数据结构:顺序表的概念、实现与操作 综述由AI生成 顺序表是线性表的顺序存储结构,采用连续内存空间存储数据元素。文章介绍了顺序表的概念、动态数组实现、初始化、销毁、插入(头插/尾插)、删除(头删/尾删)、查找等核心操作,并提供了完整的 C 语言代码示例。此外,通过移除元素、删除有序数组重复项及合并两个有序数组三个经典练习题,展示了快慢指针等优化技巧,最后分析了顺序表在空间效率、随机访问方面的优势以及插入删除效率低、扩容开销大的局限性。
路由之心 发布于 2026/2/5 更新于 2026/5/29 2.5K 浏览数据结构:顺序表
前言
在计算机科学中,数据结构是组织和存储数据的基石,而线性表作为最基本的结构之一,广泛应用于各类算法和程序中。顺序表作为线性表的一种实现方式,以其连续的存储结构和高效的随机访问特性,成为许多场景下的首选。
本文将深入探讨顺序表的核心概念、实现方法及其常见操作,包括初始化、插入、删除、查找等关键功能。同时,通过实际代码示例和典型练习(如移除元素、合并有序数组等),帮助读者掌握顺序表的应用技巧。此外,还将分析顺序表的优势与局限性,以便在实际开发中合理选择数据结构。
1. 线性表
线性表(linear list)是 n 个具有相同特性的数据元素的有限序列。线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑上 是线性结构,也就说是连续的一条直线。但是在物理结构 上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
线性表中元素的个数 n(n≥0)定义称为线性表的长度,当 n = 0 时称之为空表 。
对于非空的线性表,每个数据元素都有一个确定的位置,可表示为 L= (a1, a2, …, ai - 1, ai, ai +1, …, an)。其中,L 是表名,a1 是第一个数据元素,也称表头元素;an 是最后一个数据元素,也称表尾元素;ai - 1 处在 ai 的前边,称为 ai 的直接前驱;ai + 1 处在 ai 的后边,称为 ai 的直接后继;ai 是表中的第 i 个数据元素,也称为结点;i 是数据元素 ai 在线性表中的位序。
对于非空的线性表或线性结构,其特点是:
顺序性(序列) :元素具有线性顺序,除第一个数据元素无前驱、最后一个数据元素无后继之外,其他每个数据元素均有一个前驱和一个后继;
有限性(有限) :元素个数有限,在计算机中处理的对象都是有限的;
相同性(相同特性) :所有数据元素的类型相同,即数据元素来自于同一数据对象;
抽象性(元素类型不确定) :数据元素的类型需要根据实际的具体问题而确定,在定义中是不具体的,而是抽象的。
由抽象数据类型定义的线性表,可以根据实际所采用的存储结构形式,进行具体的表示和实现,这里采用顺序表 的形式进行实现。
2. 顺序表
2.1 概念及其结构
顺序表是用一段物理地址 连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表的底层结构是数组,对数组的封装,实现了常用的增删查改等接口。
注意: 因为顺序表的实现采用的是数组,所以有的书、文章等可能直接写的是数组而不是线性表,但两者的本质是一样的。
由于数组存放在连续内存空间上的相同类型数据的集合,因为数组在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。
顺序表一般可以分为:
静态顺序表 :使用定长数组存储元素。
动态顺序表 :使用动态开辟的数组存储。
2.2 定义顺序表结构 创建三个文件,分别是:SeqList.h、SeqList.c、test.c
SeqList.h:顺序表结构声明顺序表的方法
SeqList.c:实现顺序表的方法
test.c:测试文件
#define N 100
struct SeqList {
int arr[N];
int size;
};
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致 N 定大了,空间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下面我们实现动态顺序表。
typedef int SLDataType;
typedef struct SeqList {
SLDataType* arr;
int size;
int capacity;
} SL;
typedef int SLDataType; 之所以这样重命名定义是为了后期有大量代码时,此时输入的不再是 int 类型,可能是 char、double 类型,方便修改。
typedef struct SeqList { }SL; 这样是为了更方便的进行调用。
2.3 顺序表的初始化 #include <stdio.h>
#include <stdlib.h>
#include <assert.h>
SeqList.c 文件写出实现文件:全部初始化为 0
void SLInit (SL* ps)
{
ps->arr = NULL ;
ps->size = ps->capacity = 0 ;
}
void SLInit(SL s) 传值调用会报错:使用未初始化的局部变量
void SLTest01 () {
SL sl;
SLInit(sl);
}
这是因为传实参让形参来实现,传值的本质是值的拷贝 ,而 sl 没有初始化,没有值,故要对其初始化就必须传地址。
void SLTest01 () {
SL sl;
SLInit(&sl);
}
2.4 顺序表的销毁
void SLDestory (SL* ps) {
if (ps->arr)
{
free (ps->arr);
}
ps->arr = NULL ;
ps->size = ps->capacity = 0 ;
}
2.5 顺序表的插入 这里讲解两种插入:头部插入 和尾部插入 。也就是分别是从第一个节点插入和最后一个结点插入。
void SLPushBack (SL* ps, SLDataType x) ;
void SLPushFront (SL* ps, SLDataType x) ;
SL* ps:要往哪插
SLDataType x:要插入的值
2.5.1 尾插 因为我们知道 size 指向的是尾节点的下一个位置,而尾插就是往尾节点的下一个位置进行插入,所以我们可以这样写
void SLPushBack (SL* ps, SLDataType x) {
ps->arr[ps->size] = x;
++ps->size;
}
不过这样直接尾插是有错误的,因为我们在创建时让空间的值为 0,没法进行插入,故我们需要先判断插入的空间够不够 ,如果不够就要申请空间 。
增容通常是成倍数的增加,一般是 2 或 3 倍,这是数学推理出来的。
if (ps->capacity == ps->size) {
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
if (tmp == NULL ) {
perror("realloc fail!" );
exit (1 );
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
判断空间是否充足 在头插时我们也需要用到,所以我们直接封装成函数进行调用:
void SLCheckCapacity (SL* ps) {
if (ps->capacity == ps->size) {
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLDataType* tmp = (SLDataType*)realloc (ps->arr, newCapacity * sizeof (SLDataType));
if (tmp == NULL ) {
perror("realloc fail!" );
exit (1 );
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
}
之后因为可能会有传空指针的情况,所以我们需要进行判断。
void SLPushBack (SL* ps, SLDataType x) {
assert(ps);
SLCheckCapacity(ps);
ps->arr[ps->size++] = x;
}
2.5.2 头插 因为我们知道数组的元素是不能删的,只能覆盖 ,所以为了防止覆盖要从后往前移动,将头结点空出来,然后数据个数要增加。
void SLPushFront (SL* ps, SLDataType x) {
assert(ps);
SLCheckCapacity(ps);
for (int i = ps->size; i > 0 ; i--) {
ps->arr[i] = ps->arr[i - 1 ];
}
ps->arr[0 ] = x;
ps->size++;
}
2.6 打印 我们对顺序表进行头插 和尾插 后,我们就可以对这个插入的新链表进行打印测试是否正确。因为我们不需要修改顺序表,所以传值就行。
void SLPrint (SL s) {
for (int i = 0 ; i < s.size; i++) {
printf ("%d " , s.arr[i]);
}
printf ("\n" );
}
void SLTest01 () {
SL s1;
SLInit(&s1);
SLPushBack(&s1, 1 );
SLPushBack(&s1, 2 );
SLPushBack(&s1, 3 );
SLPushBack(&s1, 4 );
SLPrint(s1);
SLPushFront(&s1, 5 );
SLPushFront(&s1, 6 );
SLPrint(s1);
}
int main () {
SLTest01();
return 0 ;
}
2.7 顺序表的删除
void SLPopBack (SL* ps) ;
void SLPopFront (SL* ps) ;
2.7.1 尾删 非常简单,只需要判断插入的不为空,且删除的顺序表不能为空,然后让数据个数减少。
void SLPopBack (SL* ps) {
assert(ps);
assert(ps->size);
--ps->size;
}
2.7.2 头删 由于数组的元素是不能删的,只能覆盖 ,所以我们让数据从前往后覆盖,然后让数据个数减少。
void SLPopFront (SL* ps) {
assert(ps);
assert(ps->size);
for (int i = 0 ; i < ps->size - 1 ; i++) {
ps->arr[i] = ps->arr[i + 1 ];
}
ps->size--;
}
2.8 指定位置之前插入/指定位置删除数据 void SLInsert (SL* ps, int pos, SLDataType x) ;
void SLErase (SL* ps, int pos) ;
SL* ps:要往哪插入/删除
int pos:要插入/删除的位置
SLDataType x:要插入的值
2.8.1 指定位置之前插入 我们要注意插入的位置要大于 0 且不大于数据个数对应的下标。其他的和头插差不多,pos 之前的数据不要动,之后的数据重复头插的操作。然后在插入时要判断空间大小够不够。
void SLInsert (SL* ps, int pos, SLDataType x) {
assert(ps);
assert(pos >= 0 && pos <= ps->size);
SLCheckCapacity(ps);
for (int i = ps->size; i > pos; i--) {
ps->arr[i] = ps->arr[i - 1 ];
}
ps->arr[pos] = x;
ps->size++;
}
2.8.2 指定位置删除 我们要注意插入的位置要大于 0 且小于数据个数对应的下标。
void SLErase (SL* ps, int pos) {
assert(ps);
assert(pos >= 0 && pos < ps->size);
for (int i = pos; i < ps->size - 1 ; i++) {
ps->arr[i] = ps->arr[i + 1 ];
}
ps->size--;
}
2.9 查找 查找也很简单,只需要保证查找的值是等于我们想要的值即可。
int SLFind (SL* ps, SLDataType x) {
assert(ps);
for (int i = 0 ; i < ps->size; i++) {
if (ps->arr[i] == x) {
return i;
}
}
return -1 ;
}
2.10 所有代码 #pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLDataType;
typedef struct SeqList {
SLDataType* arr;
int size;
int capacity;
} SL;
void SLInit (SL* ps) ;
void SLDestory (SL* ps) ;
void SLPrint (SL s) ;
void SLPushBack (SL* ps, SLDataType x) ;
void SLPopBack (SL* ps) ;
void SLPushFront (SL* ps, SLDataType x) ;
void SLPopFront (SL* ps) ;
void SLInsert (SL* ps, int pos, SLDataType x) ;
void SLErase (SL* ps, int pos) ;
int SLFind (SL* ps, SLDataType x) ;
#include "SeqList.h"
void SLInit (SL* ps)
{
ps->arr = NULL ;
ps->size = ps->capacity = 0 ;
}
void SLCheckCapacity (SL* ps) {
if (ps->capacity == ps->size) {
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLDataType* tmp = (SLDataType*)realloc (ps->arr, newCapacity * sizeof (SLDataType));
if (tmp == NULL ) {
perror("realloc fail!" );
exit (1 );
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
}
void SLPushBack (SL* ps, SLDataType x) {
assert(ps);
SLCheckCapacity(ps);
ps->arr[ps->size++] = x;
}
void SLPushFront (SL* ps, SLDataType x) {
assert(ps);
SLCheckCapacity(ps);
for (int i = ps->size; i > 0 ; i--) {
ps->arr[i] = ps->arr[i - 1 ];
}
ps->arr[0 ] = x;
ps->size++;
}
void SLPopBack (SL* ps) {
assert(ps);
assert(ps->size);
--ps->size;
}
void SLPopFront (SL* ps) {
assert(ps);
assert(ps->size);
for (int i = 0 ; i < ps->size - 1 ; i++) {
ps->arr[i] = ps->arr[i + 1 ];
}
ps->size--;
}
void SLInsert (SL* ps, int pos, SLDataType x) {
assert(ps);
assert(pos >= 0 && pos <= ps->size);
SLCheckCapacity(ps);
for (int i = ps->size; i > pos; i--) {
ps->arr[i] = ps->arr[i - 1 ];
}
ps->arr[pos] = x;
ps->size++;
}
void SLErase (SL* ps, int pos) {
assert(ps);
assert(pos >= 0 && pos < ps->size);
for (int i = pos; i < ps->size - 1 ; i++) {
ps->arr[i] = ps->arr[i + 1 ];
}
ps->size--;
}
int SLFind (SL* ps, SLDataType x) {
assert(ps);
for (int i = 0 ; i < ps->size; i++) {
if (ps->arr[i] == x) {
return i;
}
}
return -1 ;
}
void SLDestory (SL* ps) {
if (ps->arr)
{
free (ps->arr);
}
ps->arr = NULL ;
ps->size = ps->capacity = 0 ;
}
void SLPrint (SL s) {
for (int i = 0 ; i < s.size; i++) {
printf ("%d " , s.arr[i]);
}
printf ("\n" );
}
#define _CRT_SECURE_NO_WARNINGS
#include "SeqList.h"
void SLTest01 () {
SL s1;
SLInit(&s1);
SLPushBack(&s1, 1 );
SLPushBack(&s1, 2 );
SLPushBack(&s1, 3 );
SLPushBack(&s1, 4 );
SLPrint(s1);
SLPushFront(&s1, 5 );
SLPushFront(&s1, 6 );
SLPrint(s1);
SLPopBack(&s1);
SLPrint(s1);
SLPopFront(&s1);
SLPrint(s1);
SLInsert(&s1, 0 , 99 );
SLPrint(s1);
SLInsert(&s1, s1.size, 88 );
SLPrint(s1);
SLErase(&s1, 0 );
SLPrint(s1);
SLErase(&s1, s1.size - 1 );
SLPrint(s1);
int find = SLFind(&s1, 3 );
if (find < 0 ) {
printf ("没有找到\n" );
} else {
printf ("找到了,下标为%d\n" , find);
}
SLDestory(&s1);
}
int main () {
SLTest01();
return 0 ;
}
3. 练习
3.1 移除元素 思路一: 两层 for 循环,一个 for 循环遍历数组元素,第二个 for 循环更新数组
int removeElement (int * nums, int numsSize, int val) {
for (int i = 0 ; i < numsSize; i++) {
if (nums[i] == val) {
for (int j = i + 1 ; j < numsSize; j++) {
nums[j - 1 ] = nums[j];
}
i--;
numsSize--;
}
}
return numsSize;
}
思路二: 创建新的数组,遍历原数组,将非 val 的值放在新数组中,原数组删去,之后再看有多少
int removeElement (int * nums, int numsSize, int val) {
int * temp = (int *)malloc (numsSize * sizeof (int ));
int k = 0 ;
for (int i = 0 ; i < numsSize; i++) {
if (nums[i] != val) {
temp[k] = nums[i];
k++;
}
}
for (int i = 0 ; i < k; i++) {
nums[i] = temp[i];
}
free (temp);
return k;
}
快慢指针法:通过一个快指针和慢指针在一个循环下完成两个循环的工作。
快指针:寻找新数组的元素,新数组就是不含有目标元素的数组
慢指针:指向更新新数组下标的位置
创建两个变量 src, dst
(1)若 src 指向的值为 val,则 src++
(2)若 src 指向的值不是 val,则 nums[dst] = nums[src],src++,dest++
int removeElement (int * nums, int numsSize, int val) {
int src, dst; src = dst = 0 ;
while (src < numsSize) {
if (nums[src] == val) {
src++;
} else {
nums[dst++] = nums[src++];
}
}
return dst;
}
int removeElement (int * nums, int numsSize, int val) {
int slow = 0 ;
for (int fast = 0 ; fast < numsSize; fast++) {
if (nums[fast] != val) {
nums[slow++] = nums[fast];
}
}
return slow;
}
3.2 删除有序数组的重复项 思路一: 开创额外的数组,将不同的数组放在新数组中
int removeDuplicates (int * nums, int numsSize) {
if (numsSize == 0 ) {
return 0 ;
}
int slow = 1 ;
for (int fast = 1 ; fast < numsSize; fast++) {
if (nums[fast] != nums[fast - 1 ]) {
nums[slow] = nums[fast];
++slow;
}
}
return slow;
}
3.3 合并两个有序数组 思路一: 将 num2 中所有数据依次放到 num1 数组后面,用排序算法对 num1 进行排序(借助低下的排序算法会影响到整体的运行效率)
思路二: 从后往前比大小 — 比谁大,谁大谁放后面
要复制的数组有 l1,l3 两个指针,被复制的数组有 l2 一个指针。
l2 先出循环
l1 先出循环,num2 中还有数据未放到 num1 中
void merge (int * nums1, int nums1Size, int m, int * nums2, int nums2Size, int n) {
int l1 = m - 1 ;
int l2 = n - 1 ;
int l3 = m + n - 1 ;
while (l1 >= 0 && l2 >= 0 ) {
if (nums1[l1] < nums2[l2]) {
nums1[l3--] = nums2[l2--];
} else {
nums1[l3--] = nums1[l1--];
}
}
while (l2 >= 0 ) {
nums1[l3--] = nums2[l2--];
}
}
时间复杂度:O(m + n)
空间复杂度:O(1)
4. 顺序表的优点与局限性 数组存储在连续的内存空间内,且元素类型相同。这种做法包含丰富的先验信息,系统可以利用这些信息来优化数据结构的操作效率。
空间效率高 :数组为数据分配了连续的内存块,无须额外的结构开销。
支持随机访问 :数组允许在 O(1) 时间内访问任何元素。
缓存局部性 :当访问数组元素时,计算机不仅会加载它,还会缓存其周围的其他数据,从而借助高速缓存来提升后续操作的执行速度。
插入与删除效率低 :当数组中元素较多时,插入与删除操作需要移动大量的元素。
长度不可变 :数组在初始化后长度就固定了,扩容数组需要将所有数据复制到新数组,开销很大。
空间浪费 :如果数组分配的大小超过实际所需,那么多余的空间就被浪费了。
结语 顺序表作为线性表的一种基础实现方式,以其内存连续存储的特性在数据访问效率上展现出显著优势。通过顺序存储结构,可以高效完成随机访问、尾插尾删等操作,适合静态或低频动态数据场景。然而,其插入删除的平均时间复杂度较高,且扩容可能带来性能损耗,因此在需要频繁修改数据的场景中需权衡选择。
通过移除元素、删除有序数组重复项及合并有序数组等经典题目,能够深入理解顺序表的操作逻辑与边界条件。这些练习不仅巩固了顺序表的核心操作,也为后续学习更复杂的数据结构奠定基础。
顺序表的优缺点启示我们:在程序设计时,需根据具体需求选择数据结构。若数据规模固定且注重访问速度,顺序表是理想选择;若需频繁增删,可能需要考虑链式结构或其他动态方案。理解其特性,方能灵活运用于实际问题。
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online