【数据结构与算法】第5篇:线性表(一):顺序表(ArrayList)的实现与应用

一、什么是顺序表

顺序表是最简单的一种线性结构。用一段地址连续的存储单元依次存储数据元素。

你可以把它理解为一个可以自动扩容的数组。C语言的原生数组长度是固定的,不够用的时候只能重新申请更大的数组,把数据搬过去。顺序表封装了这个过程,让使用者不用操心容量问题。

顺序表的特点

  • 逻辑上相邻的元素,物理位置上也相邻

  • 可以通过下标直接访问,时间复杂度O(1)

  • 插入和删除操作需要移动元素,时间复杂度O(n)


二、顺序表的结构定义

我们需要用一个结构体来管理顺序表:

c

复制代码
typedef struct {
    int *data;      // 指向动态数组的指针
    int size;       // 当前元素个数
    int capacity;   // 总容量
} SeqList;
  • data:指向一块连续内存的指针,真正存数据的地方

  • size:当前有多少个元素

  • capacity:当前最多能存多少个元素(不一定是内存的实际字节数)


三、基本操作实现

3.1 初始化

c

复制代码
void initSeqList(SeqList *list, int initCapacity) {
    list->data = (int*)malloc(initCapacity * sizeof(int));
    if (list->data == NULL) {
        printf("初始化失败\n");
        exit(1);
    }
    list->size = 0;
    list->capacity = initCapacity;
}

初始化时先申请一块内存,size设为0,capacity就是申请的大小。

3.2 销毁

c

复制代码
void destroySeqList(SeqList *list) {
    if (list->data != NULL) {
        free(list->data);
        list->data = NULL;
    }
    list->size = 0;
    list->capacity = 0;
}

用完一定要释放内存,避免泄漏。

3.3 扩容

扩容是顺序表的核心。当size等于capacity时,再插入新元素就需要扩容。

c

复制代码
void expand(SeqList *list) {
    int newCapacity = list->capacity * 2;  // 翻倍扩容
    int *newData = (int*)realloc(list->data, newCapacity * sizeof(int));
    if (newData == NULL) {
        printf("扩容失败\n");
        return;
    }
    list->data = newData;
    list->capacity = newCapacity;
    printf("扩容到 %d\n", newCapacity);
}

扩容策略:这里用的是翻倍扩容。也可以每次增加固定大小(比如+10)。翻倍扩容的优点是,随着容量变大,扩容次数越来越少,平均时间复杂度更低。

3.4 插入

在指定位置插入元素,这是顺序表最复杂的操作。

c

复制代码
int insert(SeqList *list, int pos, int value) {
    // 检查位置是否合法(可以插在末尾,所以pos可以从0到size)
    if (pos < 0 || pos > list->size) {
        printf("插入位置不合法\n");
        return -1;
    }
    
    // 满了就扩容
    if (list->size == list->capacity) {
        expand(list);
    }
    
    // 移动元素:从最后一个开始往后移,给新元素腾位置
    for (int i = list->size; i > pos; i--) {
        list->data[i] = list->data[i - 1];
    }
    
    // 插入新元素
    list->data[pos] = value;
    list->size++;
    
    return 0;
}

关键点 :移动元素必须从后往前移。如果从前往后移,前面的元素会把后面的覆盖掉。

画个图理解一下,在位置2插入一个元素:

text

复制代码
插入前:[10, 20, 30, 40]  size=4
插入位置2,值25

第一步:从最后一个开始往后移
[10, 20, 30, 40, 40]  i从4移到3
[10, 20, 30, 30, 40]  i移到2时停止

第二步:插入
[10, 20, 25, 30, 40]  size变成5

3.5 删除

删除指定位置的元素,同样需要移动数据。

c

复制代码
int delete(SeqList *list, int pos) {
    // 检查位置是否合法
    if (pos < 0 || pos >= list->size) {
        printf("删除位置不合法\n");
        return -1;
    }
    
    // 保存被删除的值(如果需要的话)
    int value = list->data[pos];
    
    // 移动元素:从pos+1开始往前移
    for (int i = pos; i < list->size - 1; i++) {
        list->data[i] = list->data[i + 1];
    }
    
    list->size--;
    return value;
}

删除比插入简单,移动方向是从前往后

3.6 查找

按值查找,返回第一个匹配的位置。

c

复制代码
int find(SeqList *list, int value) {
    for (int i = 0; i < list->size; i++) {
        if (list->data[i] == value) {
            return i;
        }
    }
    return -1;
}

3.7 打印

c

复制代码
void print(SeqList *list) {
    printf("size=%d, capacity=%d, [", list->size, list->capacity);
    for (int i = 0; i < list->size; i++) {
        printf("%d", list->data[i]);
        if (i < list->size - 1) printf(", ");
    }
    printf("]\n");
}

四、完整代码演示

c

复制代码
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int *data;
    int size;
    int capacity;
} SeqList;

void initSeqList(SeqList *list, int initCapacity) {
    list->data = (int*)malloc(initCapacity * sizeof(int));
    if (list->data == NULL) {
        printf("初始化失败\n");
        exit(1);
    }
    list->size = 0;
    list->capacity = initCapacity;
}

void destroySeqList(SeqList *list) {
    if (list->data != NULL) {
        free(list->data);
        list->data = NULL;
    }
    list->size = 0;
    list->capacity = 0;
}

void expand(SeqList *list) {
    int newCapacity = list->capacity * 2;
    int *newData = (int*)realloc(list->data, newCapacity * sizeof(int));
    if (newData == NULL) {
        printf("扩容失败\n");
        return;
    }
    list->data = newData;
    list->capacity = newCapacity;
    printf("扩容到 %d\n", newCapacity);
}

int insert(SeqList *list, int pos, int value) {
    if (pos < 0 || pos > list->size) {
        printf("插入位置不合法\n");
        return -1;
    }
    
    if (list->size == list->capacity) {
        expand(list);
    }
    
    for (int i = list->size; i > pos; i--) {
        list->data[i] = list->data[i - 1];
    }
    
    list->data[pos] = value;
    list->size++;
    return 0;
}

int delete(SeqList *list, int pos) {
    if (pos < 0 || pos >= list->size) {
        printf("删除位置不合法\n");
        return -1;
    }
    
    int value = list->data[pos];
    
    for (int i = pos; i < list->size - 1; i++) {
        list->data[i] = list->data[i + 1];
    }
    
    list->size--;
    return value;
}

int find(SeqList *list, int value) {
    for (int i = 0; i < list->size; i++) {
        if (list->data[i] == value) {
            return i;
        }
    }
    return -1;
}

void print(SeqList *list) {
    printf("size=%d, capacity=%d, [", list->size, list->capacity);
    for (int i = 0; i < list->size; i++) {
        printf("%d", list->data[i]);
        if (i < list->size - 1) printf(", ");
    }
    printf("]\n");
}

int main() {
    SeqList list;
    initSeqList(&list, 3);
    
    // 插入几个元素,观察扩容
    insert(&list, 0, 10);
    insert(&list, 1, 20);
    insert(&list, 2, 30);
    print(&list);
    
    // 再插一个,触发扩容
    insert(&list, 3, 40);
    print(&list);
    
    // 中间插入
    insert(&list, 2, 25);
    print(&list);
    
    // 删除
    int val = delete(&list, 2);
    printf("删除的值: %d\n", val);
    print(&list);
    
    // 查找
    int pos = find(&list, 30);
    printf("30的位置: %d\n", pos);
    
    destroySeqList(&list);
    return 0;
}

运行结果:

text

复制代码
扩容到 6
size=3, capacity=3, [10, 20, 30]
size=4, capacity=6, [10, 20, 30, 40]
size=5, capacity=6, [10, 20, 25, 30, 40]
删除的值: 25
size=4, capacity=6, [10, 20, 30, 40]
30的位置: 2

五、复杂度分析

操作 时间复杂度 说明
按索引访问 O(1) 直接通过下标计算地址
插入 O(n) 平均移动n/2个元素
删除 O(n) 平均移动n/2个元素
查找(按值) O(n) 最坏情况遍历全部
扩容 均摊O(1) 翻倍扩容,平均每次插入的扩容成本很低

关于扩容的均摊分析:假设初始容量为1,翻倍扩容到n的过程中,总共移动的次数约为2n,平均到n次插入,每次插入的扩容成本是O(1)。


六、顺序表的优缺点

优点

  1. 支持随机访问,按下标取元素是O(1)

  2. 空间连续,CPU缓存友好

  3. 尾插尾删效率高(不需要移动元素)

缺点

  1. 中间插入和删除需要移动大量元素,效率低

  2. 扩容时需要重新申请内存并拷贝数据,有开销

  3. 可能浪费空间(capacity > size的部分)

适用场景

  • 需要频繁随机访问

  • 主要在尾部操作

  • 元素个数大致可预估


七、小结

这一篇我们实现了顺序表,要点总结:

要点 说明
结构 data指针 + size + capacity
扩容 realloc实现,翻倍扩容策略
插入 从后往前移动元素
删除 从前往后移动元素
复杂度 随机访问O(1),插入删除O(n)

下一篇我们会讲单链表,它解决了顺序表插入删除慢的问题,但失去了随机访问的能力。没有完美的数据结构,只有合适的选择。


八、思考题

  1. 如果每次扩容只增加10个位置,而不是翻倍,会有什么问题?

  2. 插入操作中,如果插入位置是末尾,还需要移动元素吗?

  3. 写一个函数,删除顺序表中所有等于某个值的元素(要求时间复杂度O(n))。

  4. 为什么顺序表不适合在头部频繁插入?

欢迎在评论区讨论你的答案。

相关推荐
左左右右左右摇晃2 小时前
数据结构——链表
数据结构·链表
ghie90902 小时前
C# WinForms 条形码生成器(含保存和打印预览功能)
开发语言·c#
仰泳的熊猫2 小时前
题目2584:蓝桥杯2020年第十一届省赛真题-数字三角形
数据结构·c++·算法·蓝桥杯
霑潇雨2 小时前
题解 | 深入分析各款产品年总销售额与竞品的年度对比
大数据·开发语言·数据库
2401_864959282 小时前
C++与Python混合编程实战
开发语言·c++·算法
左左右右左右摇晃2 小时前
Java并发——锁的状态演变
java·开发语言·笔记
2501_945424802 小时前
C++与硬件交互编程
开发语言·c++·算法
2301_818419012 小时前
C++中的表达式模板
开发语言·c++·算法
Roselind_Yi2 小时前
排查Visual C++堆损坏(HEAP CORRUPTION)错误:从报错到解决的完整复盘
java·开发语言·c++·spring·bug·学习方法·远程工作