线性表——顺序表

77 阅读8分钟

 一、什么是线性表

        目前,市面上几乎所有的数据结构教材都将线性表作为开篇第一章讲解,这引发了一个思考:线性表在数据结构中究竟处于什么地位?

        在我看来,线性表如同数据结构体系中的"基础构件" 。后续学习的栈、队列、树、图等复杂数据结构,大多是在顺序表的逻辑结构基础上,通过施加不同的操作约束扩展存储关系而形成的。比如:

  • 是操作受限(LIFO)的线性表
  • 队列是操作受限(FIFO)的线性表
  • 多维数组可视为线性表的扩展
  • 矩阵是带有特殊关系的线性表

        因此,掌握线性表是理解更复杂数据结构的基石。本篇将首先讲解物理结构最简单的线性表——顺序表,从三个关键维度展开:

  1. 逻辑结构:数据元素之间的线性关系
  2. 存储结构:数据在内存中的物理存储方式
  3. 基本操作:对数据的主要运算实现

二、顺序表的逻辑结构

顺序表的逻辑结构体现了最简单、最直观的数据组织方式——线性关系。

2.1. 逻辑结构的核心特征

1.元素间的"一对一"关系

  • 每个元素有且仅有一个直接前驱(首元素除外)
  • 每个元素有且仅有一个直接后继(尾元素除外)

2. 确定的先后次序

  • 每个元素有且仅有一个直接前驱(首元素除外)
  • 每个元素有且仅有一个直接后继(尾元素除外)
  1. 有限的元素集合

4.元素都是数据元素,每个元素都是单个元素

        上述所提及的逻辑结构特点并不只是顺序表特有的的,链表也同样具有。简言之,这是线性结构所具有的特点。

三、顺序表的物理结构

        线性表的两种基本实现——顺序表和链表,其根本区别在于物理存储结构的不同。这种存储差异直接决定了它们各自的操作特性和算法适用性。顺序表的连续存储特性支持随机访问,使其适合快速排序、折半查找等高效算法;而链表的离散存储特性虽然牺牲了随机访问能力,但在动态插入删除方面具有天然优势。

        顺序表的存储方式是内存开辟一整块连续的空间进行存储,其特点是存储密度高,支持随机存取。但同时一次性开辟一整块连续空间灵活性交较差、分配空间不方便、改变容量不方便。

四、顺序表的基本操作

        顺序表的操作主要就创建、销毁、增、删、改、查。根据创建选择的分配方式不同,又可以分为静态分配和动态分配。

4.1 静态分配创建顺序表

静态分配的核心特点:

  • 固定大小MAX_SIZE在编译时确定
  • 连续存储:元素在内存中连续存放
  • 空间预分配:无论是否使用,都占用固定内存

适用场景

  • 数据量固定变化不大的情况
  • 频繁查找,较少插入删除的场景
  • 内存充足,对存储效率要求高的应用
#include <stdio.h>
#include <stdbool.h>

#define MAX_SIZE 100  // 定义顺序表的最大容量

// 定义顺序表结构体
typedef struct {
    int data[MAX_SIZE];  // 静态数组存储数据元素
    int length;          // 当前顺序表的长度
} SqList;

// 初始化顺序表
void InitList(SqList *L) {
    L->length = 0;  // 初始长度为0
    printf("顺序表初始化成功!\n");
}

// 判断顺序表是否为空
bool ListEmpty(SqList L) {
    return L.length == 0;
}

// 判断顺序表是否已满
bool ListFull(SqList L) {
    return L.length >= MAX_SIZE;
}

// 获取顺序表长度
int ListLength(SqList L) {
    return L.length;
}

// 按位置插入元素
bool ListInsert(SqList *L, int i, int e) {
    // 判断插入位置是否合法
    if (i < 1 || i > L->length + 1) {
        printf("插入位置不合法!\n");
        return false;
    }
    
    // 判断顺序表是否已满
    if (ListFull(*L)) {
        printf("顺序表已满,无法插入!\n");
        return false;
    }
    
    // 将第i个位置及之后的元素后移
    for (int j = L->length; j >= i; j--) {
        L->data[j] = L->data[j - 1];
    }
    
    // 插入新元素
    L->data[i - 1] = e;  // 注意:数组下标从0开始,位序从1开始
    L->length++;
    
    printf("元素 %d 插入成功!\n", e);
    return true;
}

// 按位置删除元素
bool ListDelete(SqList *L, int i, int *e) {
    // 判断删除位置是否合法
    if (i < 1 || i > L->length) {
        printf("删除位置不合法!\n");
        return false;
    }
    
    // 判断顺序表是否为空
    if (ListEmpty(*L)) {
        printf("顺序表为空,无法删除!\n");
        return false;
    }
    
    // 保存被删除的元素
    *e = L->data[i - 1];
    
    // 将第i个位置之后的元素前移
    for (int j = i; j < L->length; j++) {
        L->data[j - 1] = L->data[j];
    }
    
    L->length--;
    printf("元素 %d 删除成功!\n", *e);
    return true;
}

// 按位置查找元素
bool GetElem(SqList L, int i, int *e) {
    if (i < 1 || i > L.length) {
        printf("查找位置不合法!\n");
        return false;
    }
    
    *e = L.data[i - 1];
    printf("第 %d 个元素是:%d\n", i, *e);
    return true;
}

// 按值查找元素位置
int LocateElem(SqList L, int e) {
    for (int i = 0; i < L.length; i++) {
        if (L.data[i] == e) {
            printf("元素 %d 位于第 %d 个位置\n", e, i + 1);
            return i + 1;
        }
    }
    
    printf("未找到元素 %d\n", e);
    return -1;
}

// 遍历输出顺序表
void PrintList(SqList L) {
    if (ListEmpty(L)) {
        printf("顺序表为空!\n");
        return;
    }
    
    printf("顺序表元素:");
    for (int i = 0; i < L.length; i++) {
        printf("%d ", L.data[i]);
    }
    printf("\n");
}

// 清空顺序表
void ClearList(SqList *L) {
    L->length = 0;
    printf("顺序表已清空!\n");
}

int main() {
    SqList L;  // 声明顺序表变量
    int deletedElem;
    
    // 初始化顺序表
    InitList(&L);
    
    // 插入元素测试
    ListInsert(&L, 1, 10);  // 在第1个位置插入10
    ListInsert(&L, 2, 20);  // 在第2个位置插入20
    ListInsert(&L, 3, 30);  // 在第3个位置插入30
    ListInsert(&L, 2, 15);  // 在第2个位置插入15
    
    // 遍历输出
    PrintList(L);
    
    // 查找操作测试
    GetElem(L, 2, &deletedElem);  // 查找第2个元素
    LocateElem(L, 20);            // 查找值为20的元素位置
    
    // 删除操作测试
    ListDelete(&L, 2, &deletedElem);  // 删除第2个元素
    PrintList(L);
    
    // 获取长度
    printf("当前顺序表长度:%d\n", ListLength(L));
    
    return 0;
}

4.2 动态分配创建顺序表

相较于静态分配,动态分配具有以下优势:

  • 灵活性:根据实际数据量调整内存
  • 内存效率:避免预分配过多内存的浪费
  • 适应性:适合数据量变化大的场景
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

#define INIT_SIZE 10  // 初始容量

// 定义动态顺序表结构体
typedef struct {
    int *data;        // 动态数组指针
    int length;       // 当前长度
    int maxSize;      // 当前最大容量
} SeqList;

// 初始化顺序表
void InitList(SeqList *L) {
    L->data = (int*)malloc(INIT_SIZE * sizeof(int));
    if (L->data == NULL) {
        printf("内存分配失败!\n");
        exit(1);
    }
    L->length = 0;
    L->maxSize = INIT_SIZE;
    printf("顺序表初始化成功!初始容量:%d\n", INIT_SIZE);
}

// 扩展顺序表容量
void ExpandList(SeqList *L, int newSize) {
    int *newData = (int*)realloc(L->data, newSize * sizeof(int));
    if (newData == NULL) {
        printf("内存扩展失败!\n");
        return;
    }
    L->data = newData;
    L->maxSize = newSize;
    printf("顺序表容量扩展为:%d\n", newSize);
}

// 自动扩容(当空间不足时)
void AutoExpand(SeqList *L) {
    if (L->length >= L->maxSize) {
        int newSize = L->maxSize * 2;  // 容量翻倍
        ExpandList(L, newSize);
    }
}

// 判断顺序表是否为空
bool ListEmpty(SeqList L) {
    return L.length == 0;
}

// 获取顺序表长度
int ListLength(SeqList L) {
    return L.length;
}

// 获取当前容量
int GetCapacity(SeqList L) {
    return L.maxSize;
}

// 按位置插入元素
bool ListInsert(SeqList *L, int i, int e) {
    // 检查插入位置合法性
    if (i < 1 || i > L->length + 1) {
        printf("插入位置不合法!当前长度:%d\n", L->length);
        return false;
    }
    
    // 自动扩容检查
    AutoExpand(L);
    
    // 移动元素
    for (int j = L->length; j >= i; j--) {
        L->data[j] = L->data[j - 1];
    }
    
    // 插入新元素
    L->data[i - 1] = e;
    L->length++;
    
    printf("元素 %d 插入成功!当前长度:%d,容量:%d\n", e, L->length, L->maxSize);
    return true;
}

// 按位置删除元素
bool ListDelete(SeqList *L, int i, int *e) {
    if (i < 1 || i > L->length) {
        printf("删除位置不合法!\n");
        return false;
    }
    
    if (ListEmpty(*L)) {
        printf("顺序表为空,无法删除!\n");
        return false;
    }
    
    // 保存被删除元素
    *e = L->data[i - 1];
    
    // 移动元素
    for (int j = i; j < L->length; j++) {
        L->data[j - 1] = L->data[j];
    }
    
    L->length--;
    printf("元素 %d 删除成功!当前长度:%d\n", *e, L->length);
    
    // 缩容检查(可选:当长度远小于容量时)
    if (L->length < L->maxSize / 4 && L->maxSize > INIT_SIZE) {
        int newSize = L->maxSize / 2;
        ExpandList(L, newSize);
    }
    
    return true;
}

// 按位置查找元素
bool GetElem(SeqList L, int i, int *e) {
    if (i < 1 || i > L.length) {
        printf("查找位置不合法!\n");
        return false;
    }
    
    *e = L.data[i - 1];
    printf("第 %d 个元素是:%d\n", i, *e);
    return true;
}

// 按值查找元素位置
int LocateElem(SeqList L, int e) {
    for (int i = 0; i < L.length; i++) {
        if (L.data[i] == e) {
            printf("元素 %d 位于第 %d 个位置\n", e, i + 1);
            return i + 1;
        }
    }
    
    printf("未找到元素 %d\n", e);
    return -1;
}

// 遍历输出顺序表
void PrintList(SeqList L) {
    if (ListEmpty(L)) {
        printf("顺序表为空!\n");
        return;
    }
    
    printf("顺序表元素[长度=%d, 容量=%d]:", L.length, L.maxSize);
    for (int i = 0; i < L.length; i++) {
        printf("%d ", L.data[i]);
    }
    printf("\n");
}

// 销毁顺序表(释放内存)
void DestroyList(SeqList *L) {
    free(L->data);
    L->data = NULL;
    L->length = 0;
    L->maxSize = 0;
    printf("顺序表已销毁,内存已释放!\n");
}

// 清空顺序表(保留内存)
void ClearList(SeqList *L) {
    L->length = 0;
    printf("顺序表已清空!当前容量:%d\n", L->maxSize);
}

int main() {
    SeqList L;
    int deletedElem;
    
    // 初始化顺序表
    InitList(&L);
    
    // 测试插入操作(触发自动扩容)
    printf("\n=== 测试插入操作 ===\n");
    for (int i = 1; i <= 15; i++) {
        ListInsert(&L, i, i * 10);
    }
    PrintList(L);
    
    // 测试查找操作
    printf("\n=== 测试查找操作 ===\n");
    GetElem(L, 5, &deletedElem);
    LocateElem(L, 80);
    
    // 测试删除操作
    printf("\n=== 测试删除操作 ===\n");
    ListDelete(&L, 3, &deletedElem);
    ListDelete(&L, 7, &deletedElem);
    PrintList(L);
    
    // 测试手动扩容
    printf("\n=== 测试手动扩容 ===\n");
    ExpandList(&L, 30);
    PrintList(L);
    
    // 测试清空和销毁
    printf("\n=== 测试清空操作 ===\n");
    ClearList(&L);
    PrintList(L);
    
    // 重新插入测试
    ListInsert(&L, 1, 999);
    ListInsert(&L, 2, 888);
    PrintList(L);
    
    // 最终销毁
    DestroyList(&L);
    
    return 0;
}

      

五、总结

        相较链表来说,顺序表是非常容易能实现的。对这部分内容的学习还是要动手写代码才能理解的更深刻。