嵌入式 - 数据结构与算法:(1-1)数据结构 - 顺序表(Sequential List)

下一篇

目 录


线性表包括:顺序表、链表


顺序表(Sequential List)

顺序表的本质就是一个可以动态增长的数组

存储方式是:连续内存 (底层是数组),元素紧挨着存放,支持随机访问

在 C 中,数组一旦定义,大小通常是固定的。但实际使用中,我们不知道要存多少个元素,所以需要动态分配内存 ,并在不够用时扩容

这就引出了顺序表的设计目标:模拟一个"长度可变"的数组,同时记录当前用了多少空间、总共能用多少空间。

1)核心结构

我们需要三个信息:① 指向数据的指针 (代替固定数组)、② 当前有多少个有效元素 (比如插入了 5 个数)、③ 当前最多能存多少个元素(避免越界)。

那么 结构体 就是一个很好的容器,例如:

c 复制代码
/* 定义顺序表 */
typedef struct {
    int *data;      // 指向一块连续内存的首地址(相当于动态数组)
    int length;     // 当前已经存了多少个元素(从 0 开始计数),后续如果要遍历顺序表的话,是遍历此变量,这个变量很重要!!!!!
    int capacity;   // 这块内存总共能存多少个元素
} SeqList;

一个顺序表的完整内存空间应包含 数据区+其他变量 。

2)关键操作(封装成函数)

2.1)初始化

  • malloc 申请一块初始内存(比如能存 10 个整数)
  • 设置 length = 0(还没放任何数据)
  • capacity = 10

例如:

c 复制代码
/* 初始化顺序表 */ 
bool InitSeqList(SeqList *list) 
{
    list->data = (int *)malloc(INIT_CAPACITY * sizeof(int));
    if (!list->data) return false;
    list->length = 0;
    list->capacity = INIT_CAPACITY;
    return true;
}

2.2)插入元素(重点理解)

顺序表插入新元素只能在 当前已有/有效元素的末尾之后、或已有元素之间的合法位置 插入,不能乱插的原因在下面第三小点阐述。

① 在尾部插入元素

尾部插入其实就是"在 length 位置插入",所以尾部就是 index = length

例如:

c 复制代码
/* 尾部插入元素 elem(独立实现) */
bool PushBack(SeqList *list, int elem) {
    // 如果满了,先扩容
    if (list->length >= list->capacity) {
        if (!Resize(list)) {
            printf("Error: Failed to resize in PushBack!\n");
            return false;
        }
    }
    // 直接放到末尾
    list->data[list->length] = elem;
    list->length++;
    return true;
}

如果想简化代码的,可以使用 Insert 函数:

c 复制代码
// 尾部插入元素 elem
bool PushBack(SeqList *list, int elem) {
    return Insert(list, list->length, elem);
}
② 在第 i 个位置插入,可以是表首
  • 先检查 i 是否合法(0 ≤ i ≤ length)
  • 如果 length == capacity,说明满了,需要用 realloc 扩容(比如增加 5 个空间)
  • 把从第 i 位开始的所有元素往后挪一位
  • 把新元素放进第 i
  • length++
  • ⚠️ 注意:因为内存是连续的,插入中间必须"腾地方",所以效率低。

例如:

c 复制代码
/* 在 index 位置插入元素 elem */ 
bool Insert(SeqList *list, int index, int elem) 
{
    if (index < 0 || index > list->length) {
        printf("Error: Insert index out of range!\n");
        return false;
    }
    // 如果满了,扩容
    if (list->length >= list->capacity) {
        if (!Resize(list)) {
            printf("Error: Failed to resize!\n");
            return false;
        }
    }
    // 后移元素, 这里 --i和i--等同,后续也是
    for (int i = list->length; i > index; --i) {
        list->data[i] = list->data[i - 1];
    }
    list->data[index] = elem;
    list->length++;
    return true;
}
③ 为什么不能在最大容量内的任意地址插入?

首先在技术上,是能直接在最大容量内的任意地址写入的,内存是我们自己申请的,可以随便在哪读写。

根本原因是:

由于顺序表在定义的时候,就定义 length 表示有效数据个数,一旦你绕过 length 直接写入非连续位置,就破坏了顺序表"逻辑连续"的契约,导致所有基于 length 的操作(遍历、查找、插入、删除)都无法正确感知或管理你写入的数据,从而造成逻辑混乱或数据丢失。

展开来说就是:

顺序表在研究出来的时候,就规定了一个顺序表的结构体包含三个信息:① 指向一篇连续内存(存放数据)的指针② 当前有多少个有效元素 length③ 当前最多能存多少个元素(申请的内存大小限制) ,它的很多操作都是基于 length 的。它对外承诺的行为包括:

  1. 元素是连续存储的(逻辑上)
  2. 长度 = 有效元素个数
  3. 遍历时只遍历前 length 个元素
  4. 插入/删除保持连续性

一旦跳过 length 随意插入,那么:

  • 后果 1:遍历会漏掉你的数据
  • 后果 2:后续插入会覆盖或错乱
  • 后果 3:查找、删除等操作全部失效

你可以在申请的内存中的任意地址插入数据,只要你记得,但是这样的话,你就要始终记得那个位置有数据,并且要承担因为后续插入数据引起的数据移动、数据存储混乱问题。而且违背了顺序表设计的初衷。后面想解决这个问题,就可以使用链表。

2.3)删除元素

  • 检查位置是否合法
  • 记下要删的值
  • 把它后面的所有有效元素往前挪一位
  • length--
  • ⚠️ 注意:所有元素往前挪一位的本质是覆盖前一位,全部移完之后,原来的最后一位并没有数据覆盖,但一般都不管,length-1 之后,那位数据就已经不包含在有效数据之内的,下次插入数据会被覆盖掉。

例如:

c 复制代码
/* 删除 index 位置的元素,并通过 *elem 返回被删元素 */
bool Delete(SeqList *list, int index, int *elem) {
    if (index < 0 || index >= list->length) {
        printf("Error: Delete index out of range!\n");
        return false;
    }
    *elem = list->data[index];
    // 前移元素
    for (int i = index; i < list->length - 1; ++i) {
        list->data[i] = list->data[i + 1];
    }
    list->length--;
    return true;
}

2.4)随机访问

  • 直接 list.data[i] 就能拿到第 i 个元素(O(1) 时间)

2.5)遍历/打印顺序表

顺序表的有效数据长度为 length ,所有只需要遍历前 length 个元素即可。

例如:

c 复制代码
/* 打印顺序表 */
void PrintSeqList(const SeqList *list) {
    printf("SeqList: [");
    for (int i = 0; i < list->length; ++i) {
        printf("%d", list->data[i]);
        if (i < list->length - 1) printf(", ");
    }
    printf("] (length=%d, capacity=%d)\n", list->length, list->capacity);
}

2.6)查找(某个元素第一次出现的位置)

c 复制代码
/* 查找 elem 第一次出现的位置,未找到返回 -1 */
int Find(const SeqList *list, int elem) {
    for (int i = 0; i < list->length; ++i) {
        if (list->data[i] == elem) {
            return i;  // return i+1 也行,看用途
        }
    }
    return -1;
}

3)完整代码示例

C 语言手撕顺序表完整代码(初始化、销毁、插入元素、删除元素等操作):

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

#define INIT_CAPACITY 10   // 初始容量
#define INCREMENT     5    // 扩容增量

// 顺序表结构体
typedef struct {
    int *data;        // 指向数据区的指针
    int length;       // 当前元素个数
    int capacity;     // 当前最大容量
} SeqList;

// 初始化顺序表
bool InitSeqList(SeqList *list) {
    list->data = (int *)malloc(INIT_CAPACITY * sizeof(int));
    if (!list->data) return false;
    list->length = 0;
    list->capacity = INIT_CAPACITY;
    return true;
}

// 销毁顺序表
void DestroySeqList(SeqList *list) {
    free(list->data);
    list->data = NULL;
    list->length = 0;
    list->capacity = 0;
}

// 打印顺序表
void PrintSeqList(const SeqList *list) {
    printf("SeqList: [");
    for (int i = 0; i < list->length; ++i) {
        printf("%d", list->data[i]);
        if (i < list->length - 1) printf(", ");
    }
    printf("] (length=%d, capacity=%d)\n", list->length, list->capacity);
}

// 获取当前长度
int GetLength(const SeqList *list) {
    return list->length;
}

// 扩容函数(内部使用)
static bool Resize(SeqList *list) {
    int new_capacity = list->capacity + INCREMENT;
    int *new_data = (int *)realloc(list->data, new_capacity * sizeof(int));
    if (!new_data) return false;
    list->data = new_data;
    list->capacity = new_capacity;
    return true;
}

// 在 index 位置插入元素 elem
bool Insert(SeqList *list, int index, int elem) {
    if (index < 0 || index > list->length) {
        printf("Error: Insert index out of range!\n");
        return false;
    }
    // 如果满了,扩容
    if (list->length >= list->capacity) {
        if (!Resize(list)) {
            printf("Error: Failed to resize!\n");
            return false;
        }
    }
    // 后移元素
    for (int i = list->length; i > index; --i) {
        list->data[i] = list->data[i - 1];
    }
    list->data[index] = elem;
    list->length++;
    return true;
}

// 删除 index 位置的元素,并通过 *elem 返回被删元素
bool Delete(SeqList *list, int index, int *elem) {
    if (index < 0 || index >= list->length) {
        printf("Error: Delete index out of range!\n");
        return false;
    }
    *elem = list->data[index];
    // 前移元素
    for (int i = index; i < list->length - 1; ++i) {
        list->data[i] = list->data[i + 1];
    }
    list->length--;
    return true;
}

// 查找 elem 第一次出现的位置,未找到返回 -1
int Find(const SeqList *list, int elem) {
    for (int i = 0; i < list->length; ++i) {
        if (list->data[i] == elem) {
            return i;
        }
    }
    return -1;
}

// 获取 index 位置的元素,通过 *elem 返回
bool GetElem(const SeqList *list, int index, int *elem) {
    if (index < 0 || index >= list->length) {
        return false;
    }
    *elem = list->data[index];
    return true;
}

// 测试主函数
int main() {
    
    SeqList list;
    /* SeqList *list = (SeqList*) malloc (sizeof (SeqList)); // 也可以用动态内存分配方法定义这个顺序表结构体 */
    
    if (!InitSeqList(&list)) {
        printf("Failed to initialize list!\n");
        return -1;
    }

    // 插入测试
    Insert(&list, 0, 10);
    Insert(&list, 1, 20);
    Insert(&list, 1, 15);  // 插入到中间
    PrintSeqList(&list);   // [10, 15, 20]

    // 查找
    int pos = Find(&list, 15);
    printf("Find 15 at index: %d\n", pos);

    // 删除
    int deleted;
    if (Delete(&list, 1, &deleted)) {
        printf("Deleted element: %d\n", deleted);
        PrintSeqList(&list);  // [10, 20]
    }

    // 获取元素
    int val;
    if (GetElem(&list, 1, &val)) {
        printf("Element at index 1: %d\n", val);
    }

    DestroySeqList(&list);
    return 0;
}

运行结果如下:

c 复制代码
SeqList: [10, 15, 20] (length=3, capacity=10)
Find 15 at index: 1
Deleted element: 15
SeqList: [10, 20] (length=2, capacity=10)
Element at index 1: 20

相关推荐
水龙吟啸1 小时前
数据结构与算法随机复习–Day1
数据结构·c++·算法
生成论实验室2 小时前
《事件关系阴阳博弈动力学:识势应势之道》第八篇:认知与反思关系——探索、定位与延续
人工智能·算法·架构·知识图谱·创业创新
YaraMemo2 小时前
一文带你区分全局最优解和帕累托最优解
算法·5g·信息与通信·信号处理
白夜11172 小时前
C++(标签派发 Tag Dispatching)
开发语言·c++·笔记·算法
YaraMemo3 小时前
数学优化问题中的三大转化:多目标转化为单目标,多变量转化为单变量,有约束转化为无约束
人工智能·算法·5g·信息与通信·信号处理
Ailan_Anjuxi3 小时前
【附Python源码】使用minGPT训练自己的小型GPT语言模型
算法
QuZero3 小时前
StampedLock Mechanism
java·算法
云泽8083 小时前
二叉树高阶笔试算法题精讲(二):非递归遍历与序列构造全解析
c++·算法·面试
无限进步_4 小时前
C++ 多态机制完全解析:从虚函数重写到动态绑定原理
java·c语言·jvm·数据结构·c++·windows·后端