数据结构(03)——线性表(顺序存储和链式存储)

Hi!探索者们😉,欢迎踏入 408 数据结构的奇妙秘境🌿!​

我是 ankleless📚,和你并肩的寻宝人~ 这是我的探险手札🗺️,里面记着链表森林的岔路陷阱🕸️、栈与队列城堡的机关密码🔐、二叉树山脉的攀登技巧🚶‍♀️,还有哈希表迷宫的快捷密道🌀,盼着能帮你避开数据结构的暗礁险滩🌊。​

愿我们在算法峡谷🌄各自披荆斩棘,在复杂度峰顶🥇击掌相庆,共观数据流转的星河璀璨🌠!冲呀!🚀

1. 线性表

1.1 线性表的定义

线性表是具有相同数据类型 的n(n>=0)个数据元素的有限序列,其中n为表长,当n = 0时,线性表是一个空表。若用L命名线性表,则其一般表示为:

L = (a1,a2,a3,.......an-1,an)

式中,a1是唯一的"第一个"数据元素,又称表头元素;an是唯一的"最后一个"数据元素,又称表尾元素,除第一个元素外,每个元素有且仅有一个直接前驱。除最后一个元素外,每个元素有且仅有一个直接后继("直接前驱"和"前驱"、"直接后继"和"后继"通常被视为同义词)。以上就是线性表的逻辑特性,这种线性有序的逻辑结构正式线性表名字的由来。

总结:

表中元素的个数有限

表中元素具有逻辑上的顺序性,表中元素有其先后次序

表中元素都是数据元素,每个元素都是单个元素;

表中元素的**数据类型都相同,**这意味着每个元素占有相同大小的存储空间;

表中元素具有抽象性,即仅讨论元素间的逻辑关系,而不考虑元素究竟表示什么内容。

:线性表是一种逻辑结构(线性结构) ,表示元素之间一对一的相邻关系 。顺序表和链表是指存储结构,两者属于不同层面的概念,因此不要将其混淆。

1.2 线性表的基本操作

一个数据结构的基本操作是指其最核心、最基本的操作。其他较复杂的操作可通过调用其基本操作来实现。复杂的问题是由一个个小问题堆积而成的,要具有繁化简的思想。

cpp 复制代码
//线性表的基本操作
InitList(&L);//初始化表。构建一个空的线性表
Length(L);//求表长,返回线性表L的长度,即L中数据元素的个数
LocateElem(L, e);//按值查找操作。在表L中查找具有给定关键字值的元素
GetElem(L, i);//按位查找操作,获取表L中位置i上的元素的值
ListInsert(&L, i, e);//插入操作,在表L中的位置i上插入指定元素e
ListDelete(&L, i, &e);//删除操作,删除表L中位置i上的元素e,并返回删除元素的值
PrintList(L);//输出操作,按前后顺序输出线性表L的所有元素值
Empty(L);//判空操作,若L为空表,则返回true,否则返回false
DestroyList(&L);//销毁操作,销毁线性表,并释放线性表L所占用的内存空间
//上述基本操作的命名增强了可读性,这也是变量命名的一些技巧参考

基本操作的实现取决于采用哪种存储结构,存储结构不同,算法的实现也不同;

符号"&"表示C++语言中的引用调用,在C语言中采用指针也可以达到同样的效果。

:基本操作中使用到指针或引用调用的地方,都是需要对线性表内数据进行变化操作(改变L的元素、表长等)

2. 顺序表

顺序表是用一段 连续的存储单元 依次存储数据元素的线性结构,可理解为 "加强版数组",支持随机访问,适合频繁查询、少增删场景。

2.1 顺序表的定义

顺序表本质是结构体,包含 "存储数据的数组 + 记录有效元素个数的长度",C 语言示例:

cpp 复制代码
// 顺序表结构体定义(假设存储 int 类型,可按需改)
#define MAX_SIZE 100  // 顺序表最大容量
typedef struct {
    int data[MAX_SIZE];  // 数组存储数据
    int length;          // 当前有效元素个数
} SeqList;
  • data:连续数组,存数据本体;
  • length:标记已有元素数量,方便操作(比如插入时找位置、判断是否满了)。

2.2 顺序表基本操作的实现

2.2.1 顺序表的创建

"创建" 即定义并初始化一个空顺序表,C 语言里可直接声明变量 + 手动清 0,或写函数封装:

cpp 复制代码
// 函数方式创建空顺序表
SeqList createSeqList() {
    SeqList L;
    L.length = 0;  // 初始无元素
    // 数组默认可能有脏数据,可循环清 0(简单场景也可跳过,按需处理)
    for (int i = 0; i < MAX_SIZE; i++) {
        L.data[i] = 0;
    }
    return L;
}

// 调用示例
int main() {
    SeqList list = createSeqList();
    printf("创建的顺序表长度:%d\n", list.length); // 输出 0
    return 0;
}

逻辑 :初始化 length 为 0,保证后续增删能正确统计元素数量;清数组是为了避免内存里残留的随机值干扰。

2.2.2 顺序表的初始化

和 "创建" 类似,有时会把初始化单独拆成函数(尤其是需要动态分配内存的场景,不过这里用静态数组简化),核心是重置 length 和数据:

cpp 复制代码
// 初始化顺序表(复用已有的顺序表变量)
void initSeqList(SeqList *L) {  // 用指针修改传入的顺序表
    L->length = 0;
    for (int i = 0; i < MAX_SIZE; i++) {
        L->data[i] = 0;
    }
}

// 调用示例
int main() {
    SeqList list;
    initSeqList(&list); // 传地址,让函数内部能修改
    printf("初始化后长度:%d\n", list.length); // 输出 0
    return 0;
}

为什么用指针 *L ?因为要修改主函数里 list 的值,值传递(直接传 list)只能改副本,指针 / 引用才能真正改原变量 → 这就是开头说的 "需要改数据时用指针 / 引用" 的典型场景!

2.2.3 插入操作

目标 :在顺序表第 pos 个位置(从 1 开始数)插入新元素 value,插入后元素后移,length 加 1。
难点:需要先判断 "顺序表是否满了""插入位置是否合法",再批量移动元素。

cpp 复制代码
// 插入操作:pos 是插入的位置(1~length+1),value 是要插入的值
int insertSeqList(SeqList *L, int pos, int value) {
    // 1. 检查顺序表是否已满
    if (L->length == MAX_SIZE) {
        printf("顺序表满了,无法插入!\n");
        return 0; // 插入失败
    }
    // 2. 检查插入位置是否合法(pos 范围:1 ~ length+1)
    if (pos < 1 || pos > L->length + 1) {
        printf("插入位置 %d 不合法!\n", pos);
        return 0; // 插入失败
    }
    // 3. 元素后移:从最后一个元素开始,往后挪一位,给新元素腾位置
    for (int i = L->length; i >= pos; i--) { 
        L->data[i] = L->data[i - 1]; // 比如 pos=3,data[2]→data[3],依此类推
    }
    // 4. 插入新元素 + 更新长度
    L->data[pos - 1] = value; // 数组下标从 0 开始,所以 pos-1
    L->length++;
    return 1; // 插入成功
}

// 调用示例
int main() {
    SeqList list;
    initSeqList(&list);
    // 插入几个元素试试
    insertSeqList(&list, 1, 10); // 位置 1 插入 10
    insertSeqList(&list, 2, 20); // 位置 2 插入 20
    // 输出看看:应该是 [10,20],长度 2
    for (int i = 0; i < list.length; i++) {
        printf("%d ", list.data[i]); 
    }
    return 0;
}

关键逻辑

  • 后移从 length 开始往前遍历,避免 "先挪前面的,后面的被覆盖";
  • pos 是用户视角的 "第几个位置"(1 起始),所以数组下标要减 1;
  • 插入成功后 length 必须 +1,否则后续操作会错位。

2.2.4 删除操作

目标 :删除第 pos 个位置的元素,后面元素前移,length 减 1

cpp 复制代码
// 删除操作:pos 是要删除的位置(1~length)
int deleteSeqList(SeqList *L, int pos) {
    // 1. 检查顺序表是否为空
    if (L->length == 0) {
        printf("顺序表为空,无法删除!\n");
        return 0;
    }
    // 2. 检查位置是否合法(pos 范围:1 ~ length)
    if (pos < 1 || pos > L->length) {
        printf("删除位置 %d 不合法!\n", pos);
        return 0;
    }
    // 3. 元素前移:从 pos 位置开始,后面的元素往前补
    for (int i = pos; i < L->length; i++) {
        L->data[i - 1] = L->data[i]; // 比如 pos=2,data[2]→data[1]
    }
    // 4. 更新长度(逻辑删除,实际数组可能有残留值,但 length 控制访问)
    L->length--;
    return 1;
}

// 调用示例(基于之前插入的 list)
int main() {
    SeqList list;
    initSeqList(&list);
    insertSeqList(&list, 1, 10);
    insertSeqList(&list, 2, 20);
    // 删除位置 2 的元素(值为 20)
    deleteSeqList(&list, 2); 
    // 输出:应该只剩 [10],长度 1
    for (int i = 0; i < list.length; i++) {
        printf("%d ", list.data[i]); 
    }
    return 0;
}

关键逻辑

  • 前移从 pos 开始往后遍历,把后面的元素往前 "挤",覆盖要删除的位置;
  • length-- 后,后续操作不会访问到已删除的元素(虽然数组里可能还有旧值,但逻辑上被截断了)。

2.2.5 查找操作

目标 :按值查找元素位置,或按位置查找值(这里演示 "按值找下标",按位置找值很简单,直接 data[pos-1] 即可)。

cpp 复制代码
// 按值查找:返回第一个匹配值的下标(从 0 开始),没找到返回 -1
int findByValueSeqList(SeqList L, int value) { 
    for (int i = 0; i < L.length; i++) {
        if (L.data[i] == value) {
            return i; // 找到,返回下标
        }
    }
    return -1; // 没找到
}

// 调用示例
int main() {
    SeqList list;
    initSeqList(&list);
    insertSeqList(&list, 1, 10);
    insertSeqList(&list, 2, 20);
    
    int index = findByValueSeqList(list, 20);
    if (index != -1) {
        printf("值 20 的下标是:%d\n", index); // 输出 1
    } else {
        printf("没找到~");
    }
    return 0;
}

为什么用值传递 SeqList L ?因为查找操作 不修改 顺序表数据,用值传递也能工作;如果要改数据(比如插入、删除),才必须用指针 / 引用!

3. 链表

链表是用 零散的节点 存储数据,每个节点存 "数据 + 指向下一节点的指针",无需连续内存,适合频繁增删场景(不用像顺序表那样批量移动元素)。

3.1 单链表

单链表节点只有一个指针指向下一个节点,结构简单,是链表基础。

3.1.1 单链表的定义

单链表节点 + 链表结构(用头指针标记起点),C 语言示例:

cpp 复制代码
// 单链表节点结构体
typedef struct Node {
    int data;           // 数据域:存具体值
    struct Node *next;  // 指针域:存下一个节点的地址
} Node;

// 单链表:用头指针表示,指向第一个节点(空链表则为 NULL)
typedef struct {
    Node *head; // 头指针
} LinkList;
  • data:存当前节点的值;
  • next:存下一个节点的地址,串联整个链表;
  • head:链表的 "入口",通过它能找到所有节点。
3.1.2 单链表的初始化

目标:创建一个 空链表(头指针指向 NULL,没有节点)。

cpp 复制代码
// 初始化单链表(创建空链表)
void initLinkList(LinkList *L) {
    L->head = NULL; // 头指针置空,链表无节点
}

// 调用示例
int main() {
    LinkList list;
    initLinkList(&list);
    // 此时 list.head == NULL,是空链表
    return 0;
}

为什么用指针 *L ?因为要修改 list.head 的值(让它指向 NULL),必须传地址,否则函数里改的是副本,主函数里 list.head 不会变 → again,需要改数据时用指针 / 引用!

3.1.3 单链表--求表长

目标:遍历链表,统计节点个数(空链表返回 0)。

cpp 复制代码
// 求单链表长度(节点个数)
int getLengthLinkList(LinkList L) { // 不用改数据,值传递也够
    int count = 0;
    Node *p = L.head; // 指针 p 从头节点开始遍历
    while (p != NULL) { // 只要 p 没走到链表末尾(NULL)
        count++;
        p = p->next; // 跳到下一个节点
    }
    return count;
}

// 调用示例(后续插入节点后用,现在先演示逻辑)
int main() {
    LinkList list;
    initLinkList(&list);
    // 此时是空链表,长度 0
    printf("链表长度:%d\n", getLengthLinkList(list)); 
    return 0;
}

遍历逻辑 :用 p 当 "游标",从头节点出发,每次跳 p->next,直到 p 变成 NULL(链表末尾),循环次数就是节点数。

3.1.4 单链表--按序查找

目标:找第 pos 个位置的节点(从 1 开始数),返回节点指针(找不到返回 NULL)。

cpp 复制代码
// 按序查找:pos 是位置(1~length),返回节点指针
Node* findByPosLinkList(LinkList L, int pos) {
    // 1. 处理非法位置(pos 必须 ≥1,且不超过链表长度)
    if (pos < 1) {
        printf("位置 %d 不合法!\n", pos);
        return NULL;
    }
    // 2. 遍历找位置
    Node *p = L.head;
    int count = 1; // 当前遍历到第几个节点(从 1 开始)
    while (p != NULL && count < pos) { 
        p = p->next;
        count++;
    }
    // 3. 判断是否找到(p 为 NULL 说明没到 pos 就走完了)
    if (p == NULL) {
        printf("位置 %d 超出链表长度!\n", pos);
        return NULL;
    }
    return p; // 找到,返回节点指针
}

// 调用示例(后续插入节点后用,先理解逻辑)
// 假设链表有 3 个节点:10→20→30,找 pos=2 → 应返回存 20 的节点

关键逻辑

  • count 记录当前遍历到第几个节点,走到 count == pos 时停下;
  • 如果 p 提前变成 NULL,说明链表长度不够,返回 NULL。
3.1.5 单链表--按值查找

目标:找第一个值等于 value 的节点,返回指针(找不到返回 NULL)。

cpp 复制代码
// 按值查找:返回第一个值匹配的节点指针
Node* findByValueLinkList(LinkList L, int value) {
    Node *p = L.head;
    while (p != NULL) {
        if (p->data == value) {
            return p; // 找到,返回节点
        }
        p = p->next; // 没找到,继续下一个
    }
    return NULL; // 遍历完没找到
}

// 调用示例(后续插入节点后用)
// 假设链表节点值是 10→20→30,找 value=20 → 返回存 20 的节点

逻辑 :和顺序表的按值查找类似,遍历每个节点对比 data,找到就返回,否则继续。

3.1.6 单链表--插入结点

目标 :在第 pos 个位置插入新节点(分 "头插""中间 / 尾插",逻辑统一处理)。
步骤

  1. 找到第 pos-1 个节点(插入位置的前驱节点);

  2. 新节点的 next 指向原 pos 节点;

  3. 前驱节点的 next 指向新节点。

    cpp 复制代码
    // 插入节点:pos 是位置(1~length+1),value 是新节点的值
    int insertNodeLinkList(LinkList *L, int pos, int value) {
        // 1. 处理 pos=1 的特殊情况(头插)
        if (pos == 1) {
            Node *newNode = (Node*)malloc(sizeof(Node)); // 分配新节点内存
            if (newNode == NULL) {
                printf("内存分配失败!\n");
                return 0;
            }
            newNode->data = value;
            newNode->next = L->head; // 新节点 next 指向原头节点
            L->head = newNode;       // 头指针指向新节点
            return 1;
        }
        
        // 2. 找 pos-1 位置的前驱节点
        Node *pre = findByPosLinkList(*L, pos - 1); // 注意传 *L(值传递)
        if (pre == NULL) {
            return 0; // 找不到前驱,插入失败
        }
        
        // 3. 分配新节点并插入
        Node *newNode = (Node*)malloc(sizeof(Node));
        if (newNode == NULL) {
            printf("内存分配失败!\n");
            return 0;
        }
        newNode->data = value;
        newNode->next = pre->next; // 新节点 next 指向原 pos 节点
        pre->next = newNode;       // 前驱节点 next 指向新节点
        return 1;
    }
    
    // 调用示例
    int main() {
        LinkList list;
        initLinkList(&list);
        // 插入几个节点:
        insertNodeLinkList(&list, 1, 10); //
3.1.7 单链表--删除操作

目标 :删除第 pos 个位置的节点,需处理 "删头节点""删中间 / 尾节点" 两种情况,核心是 找到前驱节点,跳过待删节点并释放内存。

cpp 复制代码
// 单链表节点结构体(复用之前的定义)
typedef struct Node {
    int data;           // 数据域
    struct Node *next;  // 指针域:指向下一个节点
} Node;

// 单链表结构体(复用之前的定义)
typedef struct {
    Node *head; // 头指针,指向第一个节点
} LinkList;

// 删除操作:pos 是要删除的位置(1 ~ 链表长度)
int deleteNodeLinkList(LinkList *L, int pos) {
    // 1. 处理空链表
    if (L->head == NULL) {
        printf("链表为空,无法删除!\n");
        return 0;
    }

    Node *p = NULL; // 用来存待删除的节点
    // 2. 处理删除头节点(pos = 1)
    if (pos == 1) {
        p = L->head;             // 标记头节点
        L->head = L->head->next; // 头指针指向下一个节点
    } else {
        // 3. 找 pos-1 位置的前驱节点
        Node *pre = findByPosLinkList(*L, pos - 1); // 调用之前实现的按序查找
        if (pre == NULL || pre->next == NULL) {
            printf("位置 %d 不合法,删除失败!\n", pos);
            return 0;
        }
        p = pre->next;           // 标记待删除节点
        pre->next = p->next;     // 前驱节点跳过待删节点
    }

    // 4. 释放待删除节点的内存(避免内存泄漏)
    free(p);
    p = NULL;
    return 1;
}

// 辅助函数:按位置查找节点(复用之前的 findByPosLinkList)
Node* findByPosLinkList(LinkList L, int pos) {
    if (pos < 1) {
        printf("位置 %d 不合法!\n", pos);
        return NULL;
    }
    Node *p = L.head;
    int count = 1;
    while (p != NULL && count < pos) {
        p = p->next;
        count++;
    }
    if (p == NULL) {
        printf("位置 %d 超出链表长度!\n", pos);
    }
    return p;
}
3.1.8 头插法建立单链表

目标 :每次把新节点插在 链表头部 (头指针位置),适合需要 逆序输入 的场景(比如输入 1 2 3,链表是 3 → 2 → 1)。

cpp 复制代码
// 头插法建表:输入 n 个数据,返回创建好的链表
LinkList createListByHeadInsert() {
    LinkList L;
    L.head = NULL; // 初始化空链表

    int n, value;
    printf("请输入链表长度 n:");
    scanf("%d", &n);

    for (int i = 0; i < n; i++) {
        printf("请输入第 %d 个数据:", i + 1);
        scanf("%d", &value);

        // 1. 分配新节点
        Node *newNode = (Node*)malloc(sizeof(Node));
        newNode->data = value;

        // 2. 头插逻辑:新节点 next 指向原头节点
        newNode->next = L.head;
        // 3. 头指针指向新节点(新节点成为新的头)
        L.head = newNode;
    }

    return L;
}
3.1.9 尾插法建立单链表

目标 :每次把新节点插在 链表尾部 ,适合需要 顺序输入 的场景(输入 1 2 3,链表就是 1 → 2 → 3)。

cpp 复制代码
// 尾插法建表:输入 n 个数据,返回创建好的链表
LinkList createListByTailInsert() {
    LinkList L;
    L.head = NULL; // 初始化空链表
    Node *tail = NULL; // 尾指针,始终指向最后一个节点

    int n, value;
    printf("请输入链表长度 n:");
    scanf("%d", &n);

    for (int i = 0; i < n; i++) {
        printf("请输入第 %d 个数据:", i + 1);
        scanf("%d", &value);

        // 1. 分配新节点
        Node *newNode = (Node*)malloc(sizeof(Node));
        newNode->data = value;
        newNode->next = NULL; // 新节点是尾部,next 置空

        // 2. 尾插逻辑
        if (L.head == NULL) {
            // 链表为空时,头指针和尾指针都指向新节点
            L.head = newNode;
            tail = newNode;
        } else {
            // 链表非空时,尾节点 next 指向新节点,更新尾指针
            tail->next = newNode;
            tail = newNode;
        }
    }

    return L;
}

持续更新修正内容中~

相关推荐
山顶风景独好2 分钟前
【Leetcode】随笔
数据结构·算法·leetcode
科大饭桶1 小时前
C++入门自学Day11-- String, Vector, List 复习
c语言·开发语言·数据结构·c++·容器
Cx330❀3 小时前
【数据结构初阶】--排序(四):归并排序
c语言·开发语言·数据结构·算法·排序算法
艾莉丝努力练剑4 小时前
【C语言16天强化训练】从基础入门到进阶:Day 1
c语言·开发语言·数据结构·学习
番薯大佬4 小时前
编程算法实例-冒泡排序
数据结构·算法·排序算法
KarrySmile4 小时前
Day8--滑动窗口与双指针--1004. 最大连续1的个数 III,1658. 将 x 减到 0 的最小操作数,3641. 最长半重复子数组
数据结构·算法·双指针·滑动窗口·不定长滑动窗口·最大连续1的个数·最长子数组
岑梓铭13 小时前
考研408《计算机组成原理》复习笔记,第五章(2)——CPU指令执行过程
笔记·考研·408·计算机组成原理·计组
大阳12317 小时前
线程(基本概念和相关命令)
开发语言·数据结构·经验分享·算法·线程·学习经验
FPGA19 小时前
探讨4B/5B编码、8B/10B编码区别以及FPGA实现
数据结构