顺序表完全指南:从原理到实现

引言

在数据结构的学习中,线性表是最基础也是最重要的数据结构之一。线性表是n个数据元素的有限序列,这些元素具有相同的特性。

线性表从存储结构上分为两种:

  • 顺序表:物理地址连续(数组)

  • 链表:物理地址不连续,逻辑上连续(通过指针连接)

顺序表采用数组存储数据,元素在内存中物理地址连续,可以通过下标直接访问任意元素,时间复杂度为O(1)。今天,我们将从零开始实现一个完整的顺序表,涵盖初始化、增删改查、反转、合并等核心操作。


第一部分:顺序表的基本概念

一、什么是顺序表?

顺序表是用一段连续的物理内存来存储数据元素的线性结构。在C语言中,通常使用数组来实现。

二、顺序表结构体设计

cpp 复制代码
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>

#define MAX_SIZE 100

// 顺序表结构体
struct SeqList {
    int arr[MAX_SIZE];    // 存储数据的数组(固定大小)
    int seqsize;          // 当前元素个数
    int capacity;         // 最大容量
};

结构体成员说明:

成员 类型 作用
arr int数组 存储实际数据
seqsize int 记录当前有多少个有效元素
capacity int 记录数组的最大容量

第二部分:顺序表的基础操作

一、初始化

原理: 将顺序表的元素个数设为0,容量设为最大值。

cpp 复制代码
void InitSeqList(struct SeqList* ps) {
    assert(ps != NULL);           // 断言:确保指针不为空
    ps->capacity = MAX_SIZE;      // 设置容量
    ps->seqsize = 0;              // 初始没有元素
}

二、判空与判满

判空: 用于判断顺序表是否为空,在删除操作前需要检查。

cpp 复制代码
bool IsEmpty(struct SeqList* ps) {
    assert(ps != NULL);
    return ps->seqsize == 0;
}

判满: 用于判断顺序表是否已满,在插入操作前需要检查。

cpp 复制代码
bool IsFull(struct SeqList* ps) {
    assert(ps != NULL);
    return ps->seqsize == ps->capacity;
}

三、打印顺序表

cpp 复制代码
void Print(struct SeqList* ps) {
    assert(ps != NULL);
    for (int i = 0; i < ps->seqsize; i++) {
        printf("%d ", ps->arr[i]);
    }
    printf("\n");
}

第三部分:插入操作

一、尾插(在末尾插入)

原理: 直接在seqsize位置放入新元素(因为数组下标从0开始,seqsize正好是下一个空闲位置)。

cpp 复制代码
// 时间复杂度:O(1)
bool Insert_Back(struct SeqList* ps, int val) {
    assert(ps != NULL);
    if (IsFull(ps)) return false;
    
    ps->arr[ps->seqsize] = val;
    ps->seqsize++;
    return true;
}

示意图:

二、头插(在头部插入)

原理: 将所有元素向后移动一位,然后将新元素放在第一个位置。

cpp 复制代码
// 时间复杂度:O(n)
bool Insert_Front(struct SeqList* ps, int val) {
    assert(ps != NULL);
    if (IsFull(ps)) return false;
    
    // 将arr[0]到arr[seqsize-1]整体向后移动1位
    memmove(ps->arr + 1, ps->arr, sizeof(ps->arr[0]) * ps->seqsize);
    ps->arr[0] = val;
    ps->seqsize++;
    
    return true;
}

示意图:

三、按下标插入

原理:index位置(含)之后的元素向后移动一位,然后在index位置放入新元素。

cpp 复制代码
// 时间复杂度:O(n)
bool Insert_Index(struct SeqList* ps, int val, int index) {
    assert(ps != NULL);
    if (IsFull(ps)) return false;
    if (index > ps->seqsize || index < 0) return false;
    
    // 将index及之后的元素向后移动
    memmove(ps->arr + index + 1, ps->arr + index, 
            sizeof(ps->arr[0]) * (ps->seqsize - index));
    ps->arr[index] = val;
    ps->seqsize++;
    
    return true;
}

四、按位置插入(位置从1开始)

原理: 与按下标插入类似,但位置从1开始计数(更符合人的习惯)。位置pos对应的下标是pos-1

cpp 复制代码
// 时间复杂度:O(n)
bool Insert_Pos(struct SeqList* ps, int val, int pos) {
    assert(ps != NULL);
    if (IsFull(ps)) return false;
    if (pos < 1 || pos > ps->seqsize + 1) return false;
    
    // pos-1 是实际的下标
    memmove(ps->arr + pos, ps->arr + pos - 1, 
            sizeof(ps->arr[0]) * (ps->seqsize - pos + 1));
    ps->arr[pos - 1] = val;
    ps->seqsize++;
    
    return true;
}

第四部分:删除操作

一、尾删(在末尾删除)

原理: 只需将seqsize减1即可,原来最后一个元素变为"无效"(下次插入会覆盖)。

cpp 复制代码
// 时间复杂度:O(1)
bool Pop_Back(struct SeqList* ps) {
    assert(ps != NULL);
    if (IsEmpty(ps)) return false;
    
    ps->seqsize--;
    return true;
}

二、头删(删除头部)

原理: 将第二个元素开始的所有元素向前移动一位,覆盖第一个元素。

cpp 复制代码
// 时间复杂度:O(n)
bool Pop_Front(struct SeqList* ps) {
    assert(ps != NULL);
    if (IsEmpty(ps)) return false;
    
    memmove(ps->arr, ps->arr + 1, (--ps->seqsize) * sizeof(int));
    return true;
}

示意图:

三、按下标删除

cpp 复制代码
// 时间复杂度:O(n)
bool Pop_Index(struct SeqList* ps, int index) {
    assert(ps != NULL);
    if (IsEmpty(ps)) return false;
    if (index < 0 || index >= ps->seqsize) return false;
    
    memmove(ps->arr + index, ps->arr + index + 1, 
            sizeof(int) * (ps->seqsize - index - 1));
    ps->seqsize--;
    
    return true;
}

四、按位置删除

cpp 复制代码
// 时间复杂度:O(n)
bool Pop_Pos(struct SeqList* ps, int pos) {
    assert(ps != NULL);
    if (IsEmpty(ps)) return false;
    if (pos < 1 || pos > ps->seqsize) return false;
    
    memmove(ps->arr + pos - 1, ps->arr + pos, 
            sizeof(int) * (ps->seqsize - pos));
    ps->seqsize--;
    
    return true;
}

五、按值删除(删除所有匹配的元素)

原理: 使用双指针法,将所有不等于val的元素保留下来,覆盖到数组前面。

cpp 复制代码
// 方法1:遍历删除(边删边移动)
bool Pop_Val1(struct SeqList* ps, int val) {
    assert(ps != NULL);
    if (IsEmpty(ps)) return false;
    
    bool found = false;
    for (int i = 0; i < ps->seqsize; i++) {
        if (val == ps->arr[i]) {
            // 删除找到的元素,后面的元素前移
            memmove(ps->arr + i, ps->arr + i + 1, 
                    sizeof(int) * (--ps->seqsize - i));
            i--;  // 继续检查当前位置(原i+1已移到i)
            found = true;
        }
    }
    return found;
}

// 方法2:双指针法(更高效,一次遍历完成)
bool Pop_Val2(struct SeqList* ps, int val) {
    assert(ps != NULL);
    if (IsEmpty(ps)) return false;
    
    int j = 0;  // 新数组的写入位置
    for (int i = 0; i < ps->seqsize; i++) {
        if (ps->arr[i] != val) {
            ps->arr[j++] = ps->arr[i];
        }
    }
    
    // 如果所有元素都被保留,说明没有找到val
    if (j == ps->seqsize) return false;
    
    ps->seqsize = j;
    return true;
}

双指针法示意图:

第五部分:顺序表的高级操作

一、交换函数(辅助函数)

cpp 复制代码
void swap(int* a, int* b) {
    // 不使用临时变量的交换(加减法)
    *a = *a + *b;
    *b = *a - *b;
    *a = *a - *b;
    // 注意:此方法可能导致溢出,生产环境中使用临时变量更安全
}

// 更安全的版本
void swap_safe(int* a, int* b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

二、反转顺序表

原理: 双指针法,从两端向中间交换元素。

cpp 复制代码
// 时间复杂度:O(n)
void Reverse_SeqList(struct SeqList* ps) {
    assert(ps != NULL);
    if (IsEmpty(ps)) return;
    
    int left = 0;
    int right = ps->seqsize - 1;
    
    while (left < right) {
        swap(&ps->arr[left], &ps->arr[right]);
        left++;
        right--;
    }
}

示意图:

三、顺序表相加(大数加法)

原理: 将两个顺序表当作数字(每个元素是一位数字),相加得到新的顺序表。

cpp 复制代码
// 类似大数加法的思路
void Add_SeqList1_SeqList2(struct SeqList* ps1, struct SeqList* ps2, 
                           struct SeqList* result) {
    assert(ps1 != NULL && ps2 != NULL && result != NULL);
    
    // 如果两个都为空,直接返回
    if (IsEmpty(ps1) && IsEmpty(ps2)) return;
    
    // 如果其中一个为空,直接复制另一个
    if (IsEmpty(ps1)) {
        memmove(result->arr, ps2->arr, ps2->seqsize * sizeof(ps2->arr[0]));
        result->seqsize = ps2->seqsize;
        return;
    }
    if (IsEmpty(ps2)) {
        memmove(result->arr, ps1->arr, ps1->seqsize * sizeof(ps1->arr[0]));
        result->seqsize = ps1->seqsize;
        return;
    }
    
    int carry = 0;  // 进位
    int i = ps1->seqsize - 1;
    int j = ps2->seqsize - 1;
    
    // 从最低位(数组末尾)开始相加
    while (i >= 0 || j >= 0 || carry) {
        if (i >= 0) carry += ps1->arr[i--];
        if (j >= 0) carry += ps2->arr[j--];
        
        // 头插结果(保证顺序正确)
        Insert_Front(result, carry % 10);
        carry /= 10;
    }
}

示例:

cpp 复制代码
ps1: [1, 2, 3]  → 数字 123
ps2: [9, 8, 7]  → 数字 987
相加:123 + 987 = 1110
结果:[1, 1, 1, 0]

第六部分:综合测试

cpp 复制代码
int main() {
    struct SeqList s1, s2, result;
    
    // 初始化顺序表1
    InitSeqList(&s1);
    Insert_Front(&s1, 1);
    Insert_Front(&s1, 2);
    Insert_Front(&s1, 3);
    printf("顺序表1: ");
    Print(&s1);  // 3 2 1
    
    // 初始化顺序表2
    InitSeqList(&s2);
    Insert_Front(&s2, 9);
    Insert_Front(&s2, 8);
    Insert_Front(&s2, 7);
    printf("顺序表2: ");
    Print(&s2);  // 7 8 9
    
    // 顺序表相加
    InitSeqList(&result);
    Add_SeqList1_SeqList2(&s1, &s2, &result);
    printf("相加结果: ");
    Print(&result);  // 1 1 1 0
    
    // 反转测试
    Reverse_SeqList(&result);
    printf("反转后: ");
    Print(&result);
    
    return 0;
}

第七部分:顺序表操作复杂度总结

操作 时间复杂度 说明
尾插 O(1) 直接写入
头插 O(n) 需要移动所有元素
按位置插入 O(n) 需要移动插入位置后的元素
尾删 O(1) 直接减小size
头删 O(n) 需要移动所有元素
按位置删除 O(n) 需要移动删除位置后的元素
按值删除 O(n) 需要遍历查找
反转 O(n) 双指针交换
按值查找 O(n) 遍历查找
按下标访问 O(1) 数组直接索引

总结

一、顺序表核心要点

概念 说明
物理存储 连续内存(数组)
逻辑结构 线性结构
访问方式 下标访问 O(1)
插入/删除 尾部O(1),中间/头部O(n)
容量限制 固定大小(静态)或可扩容(动态)

二、常用操作速查表

操作 函数名 关键点
初始化 InitSeqList 设置seqsize=0
判空 IsEmpty seqsize == 0
判满 IsFull seqsize == capacity
尾插 Insert_Back 直接放入seqsize位置
头插 Insert_Front 整体后移,再放头部
尾删 Pop_Back 直接seqsize--
头删 Pop_Front 整体前移
按值删除 Pop_Val 双指针法高效
反转 Reverse_SeqList 双指针交换

三、memmove vs memcpy

函数 特点 适用场景
memmove 支持内存重叠 元素移动(源和目标重叠)
memcpy 不支持内存重叠 不重叠的拷贝

本文详细介绍了顺序表的实现,涵盖了:

  • 初始化、判空、判满

  • 各种插入和删除操作(头、尾、按位置、按值)

  • 反转和加法等高级操作

  • 每个操作的时间复杂度分析

顺序表是数据结构学习的起点,理解其原理对于后续学习链表、栈、队列等数据结构至关重要。

学习建议:

  1. 自己动手实现一遍所有函数

  2. 理解每个操作的时间复杂度

  3. 注意边界条件的处理

  4. 学会使用断言(assert)进行参数检查

相关推荐
澈2071 小时前
C++ list容器完全指南
数据结构·c++·链表
承渊政道3 小时前
【动态规划算法】(完全背包问题从状态定义到空间优化)
数据结构·c++·学习·算法·leetcode·动态规划·哈希算法
爱写代码的倒霉蛋3 小时前
2023年天梯赛L1-8
数据结构·算法
上弦月-编程4 小时前
C语言指针超详细教程——从入门到精通(面向初学者)
java·数据结构·算法
莫等闲-4 小时前
代码随想录一刷记录Day44——leetcode1143.最长公共子序列 53. 最大子序和
数据结构·c++·算法·leetcode·动态规划
承渊政道4 小时前
【动态规划算法】(背包问题经典模型与解题套路)
数据结构·c++·学习·算法·leetcode·动态规划·哈希算法
jieyucx4 小时前
Go语言切片:动态灵活的数据序列
算法·golang·指针·顺序表·数组·结构体·切片
我头发多我先学4 小时前
C++ 红黑树:从规则到实现,手把手带你写一棵红黑树
数据结构·c++·算法
努力努力再努力wz5 小时前
【MySQL进阶系列】拒绝冗余SQL:带你透彻理解视图的底层逻辑
android·c语言·数据结构·数据库·c++·sql·mysql