线性表详解:顺序与链式存储

📋 线性表详解:顺序与链式存储

学习链表最好的学习方法就是画图,不懂就画图!

一、线性表概述

线性表 是最基本、最简单、也是最常用的一种数据结构。线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的。

线性表的特点

特点 说明
有穷性 元素个数有限
有序性 元素之间有先后顺序
相同性 所有元素类型相同
唯一性 每个元素有唯一的前驱和后继(首尾除外)

二、顺序存储结构

2.1 结构设计

线性表的顺序存储使用的是数组,将节点的地址都存在这个数组中以此来形成线性表。

c 复制代码
// 顺序线性表结构定义
typedef struct {
    unsigned int** node;  // 存储节点地址的数组
    int capacity;         // 容量
    int length;           // 当前长度
} SeqList;

开辟空间时要记得为这个数组开辟空间,其大小为 (unsigned int*) * capacity

2.2 操作实现

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

#define CAPACITY 10

typedef void* SeqListNode;

typedef struct {
    SeqListNode* node;  // 节点指针数组
    int capacity;       // 容量
    int length;         // 当前长度
} SeqList;

// 创建顺序线性表
SeqList* SeqList_Create(int capacity) {
    SeqList* list = (SeqList*)malloc(sizeof(SeqList));
    list->node = (SeqListNode*)malloc(sizeof(SeqListNode) * capacity);
    list->capacity = capacity;
    list->length = 0;
    return list;
}

// 销毁顺序线性表
void SeqList_Destroy(SeqList* list) {
    if (list != NULL) {
        if (list->node != NULL) {
            free(list->node);
        }
        free(list);
    }
}

// 清空顺序线性表
void SeqList_Clear(SeqList* list) {
    if (list != NULL) {
        list->length = 0;
    }
}

// 获取长度
int SeqList_Length(SeqList* list) {
    return (list != NULL) ? list->length : 0;
}

// 插入元素
int SeqList_Insert(SeqList* list, SeqListNode node, int pos) {
    if (list == NULL || pos < 0 || pos > list->length) {
        return -1;
    }
    
    // 从后往前移动元素
    for (int i = list->length; i > pos; i--) {
        list->node[i] = list->node[i - 1];
    }
    
    list->node[pos] = node;
    list->length++;
    return 0;
}

// 获取元素
SeqListNode SeqList_Get(SeqList* list, int pos) {
    if (list == NULL || pos < 0 || pos >= list->length) {
        return NULL;
    }
    return list->node[pos];
}

// 删除元素
SeqListNode SeqList_Delete(SeqList* list, int pos) {
    if (list == NULL || pos < 0 || pos >= list->length) {
        return NULL;
    }
    
    SeqListNode ret = list->node[pos];
    
    // 从前往后移动元素
    for (int i = pos; i < list->length - 1; i++) {
        list->node[i] = list->node[i + 1];
    }
    
    list->length--;
    return ret;
}

2.3 时间复杂度分析

操作 时间复杂度 说明
获取元素 O(1) 直接通过下标访问
插入元素 O(n) 需要移动后续元素
删除元素 O(n) 需要移动后续元素

三、链式存储结构

3.1 链表技术推演

线性表的链式存储有一个头域 ,每个节点都包含这个头域就可以将各个节点串起来,形成线性表。头域保存的是下一个节点的地址

链表的核心思想

  • 不需要连续的内存空间
  • 每个节点包含数据和指向下一个节点的指针
  • 通过指针将节点串联起来

3.2 结构设计

c 复制代码
// 链表节点头域
typedef struct _tag_LinkListNode {
    struct _tag_LinkListNode* next;
} LinkListNode;

// 链表结构
typedef void LinkList;

// 链表头节点(内部实现)
typedef struct _tag_LinkList {
    LinkListNode header;  // 头节点
    int length;           // 链表长度
} TLinkList;

💡 难点说明typedef void LinkList; 表示 LinkList* 相当于一个指向链表头节点的指针。这种设计使用户不需要知道内部实现细节,直接使用 LinkList 来创建并使用链表即可。

3.3 插入操作

c 复制代码
// 在链式线性表list的pos位置插入节点node
int LinkList_Insert(LinkList* list, LinkListNode* node, int pos) {
    TLinkList* sList = (TLinkList*)list;
    
    if (sList == NULL || node == NULL || pos < 0) {
        return -1;
    }
    
    // 从头节点开始遍历
    LinkListNode* current = (LinkListNode*)sList;
    
    // 移动pos次后,current指向pos-1位置
    for (int i = 0; (i < pos) && (current->next != NULL); i++) {
        current = current->next;
    }
    
    // 插入节点
    node->next = current->next;
    current->next = node;
    
    sList->length++;
    return 0;
}

图解插入过程

复制代码
插入前:A → B → C → D
在位置2插入节点X:

步骤1:移动到位置1(pos-1)
current → B

步骤2:X的next指向C
X → C

步骤3:B的next指向X
B → X

插入后:A → B → X → C → D

3.4 删除操作

c 复制代码
// 删除链式线性表list的pos位置的节点
LinkListNode* LinkList_Delete(LinkList* list, int pos) {
    TLinkList* sList = (TLinkList*)list;
    
    if (sList == NULL || pos < 0) {
        return NULL;
    }
    
    // 从头节点开始遍历
    LinkListNode* current = (LinkListNode*)sList;
    
    // 移动到要删除节点的前一个位置
    for (int i = 0; (i < pos) && (current->next != NULL); i++) {
        current = current->next;
    }
    
    // 删除节点
    LinkListNode* ret = current->next;
    current->next = ret->next;
    
    sList->length--;
    return ret;
}

3.5 完整实现

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

typedef void LinkList;

typedef struct _tag_LinkListNode {
    struct _tag_LinkListNode* next;
} LinkListNode;

typedef struct _tag_LinkList {
    LinkListNode header;
    int length;
} TLinkList;

// 创建链式线性表
LinkList* LinkList_Create() {
    TLinkList* list = (TLinkList*)malloc(sizeof(TLinkList));
    if (list != NULL) {
        list->header.next = NULL;
        list->length = 0;
    }
    return list;
}

// 销毁链式线性表
void LinkList_Destroy(LinkList* list) {
    free(list);
}

// 清空链式线性表
void LinkList_Clear(LinkList* list) {
    TLinkList* sList = (TLinkList*)list;
    if (sList != NULL) {
        sList->header.next = NULL;
        sList->length = 0;
    }
}

// 获取链式线性表的长度
int LinkList_Length(LinkList* list) {
    TLinkList* sList = (TLinkList*)list;
    return (sList != NULL) ? sList->length : -1;
}

// 在链式线性表list的pos位置插入节点node
int LinkList_Insert(LinkList* list, LinkListNode* node, int pos) {
    TLinkList* sList = (TLinkList*)list;
    
    if (sList == NULL || node == NULL || pos < 0) {
        return -1;
    }
    
    LinkListNode* current = (LinkListNode*)sList;
    
    for (int i = 0; (i < pos) && (current->next != NULL); i++) {
        current = current->next;
    }
    
    node->next = current->next;
    current->next = node;
    
    sList->length++;
    return 0;
}

// 获取链式线性表list的pos位置的节点
LinkListNode* LinkList_Get(LinkList* list, int pos) {
    TLinkList* sList = (TLinkList*)list;
    
    if (sList == NULL || pos < 0) {
        return NULL;
    }
    
    LinkListNode* current = (LinkListNode*)sList;
    
    for (int i = 0; (i < pos) && (current->next != NULL); i++) {
        current = current->next;
    }
    
    return current->next;
}

// 删除链式线性表list的pos位置的节点
LinkListNode* LinkList_Delete(LinkList* list, int pos) {
    TLinkList* sList = (TLinkList*)list;
    
    if (sList == NULL || pos < 0) {
        return NULL;
    }
    
    LinkListNode* current = (LinkListNode*)sList;
    
    for (int i = 0; (i < pos) && (current->next != NULL); i++) {
        current = current->next;
    }
    
    LinkListNode* ret = current->next;
    if (ret != NULL) {
        current->next = ret->next;
        sList->length--;
    }
    
    return ret;
}

3.6 使用示例

c 复制代码
// 定义业务节点结构
typedef struct {
    LinkListNode header;  // 必须放在第一个位置
    int data;
} MyNode;

int main() {
    // 创建链表
    LinkList* list = LinkList_Create();
    
    // 创建并插入节点
    MyNode n1 = {{NULL}, 10};
    MyNode n2 = {{NULL}, 20};
    MyNode n3 = {{NULL}, 30};
    
    LinkList_Insert(list, (LinkListNode*)&n1, 0);
    LinkList_Insert(list, (LinkListNode*)&n2, 1);
    LinkList_Insert(list, (LinkListNode*)&n3, 2);
    
    // 遍历链表
    for (int i = 0; i < LinkList_Length(list); i++) {
        MyNode* node = (MyNode*)LinkList_Get(list, i);
        printf("data[%d] = %d\n", i, node->data);
    }
    
    // 销毁链表
    LinkList_Destroy(list);
    return 0;
}

📌 关键点 :带头结点,位置从0开始的单链表,返回链表中第pos个位置处元素的值时,需要返回 current->next

四、循环链表

4.1 循环链表的插入

循环链表的最后一个节点的next指针指向头节点,形成一个环。

c 复制代码
// 循环链表插入算法
int CircleList_Insert(LinkList* list, LinkListNode* node, int pos) {
    // ... 与单链表类似
    // 区别:插入时需要考虑循环特性
}

4.2 循环链表的删除

c 复制代码
// 循环链表删除算法
LinkListNode* CircleList_Delete(LinkList* list, int pos) {
    // 删除时需要更新尾节点的next指针
    // 保持循环特性
}

五、双向链表

双向链表的每个节点有两个指针域,分别指向前驱和后继。

c 复制代码
typedef struct _tag_DLinkListNode {
    struct _tag_DLinkListNode* prev;  // 前驱指针
    struct _tag_DLinkListNode* next;  // 后继指针
} DLinkListNode;

双向链表的优点

  • 可以双向遍历
  • 删除操作更简单(不需要找前驱节点)
  • 查找效率更高

双向链表的缺点

  • 每个节点需要额外的空间存储prev指针
  • 插入和删除操作需要修改更多的指针

六、顺序存储 vs 链式存储对比

比较项 顺序存储 链式存储
存储方式 连续内存空间 分散内存空间
空间利用率 需要预分配,可能浪费 按需分配,无浪费
随机访问 ✅ O(1) ❌ O(n)
插入删除 ❌ O(n),需要移动元素 ✅ O(1),只需修改指针
空间开销 只存数据 额外存储指针
内存连续性 连续,缓存友好 分散,缓存不友好

七、实际应用场景

顺序存储适用场景

  1. 需要频繁随机访问:如数组访问、矩阵运算
  2. 数据量固定或变化不大:如静态配置表
  3. 内存资源充足:不需要频繁插入删除

链式存储适用场景

  1. 频繁插入删除:如消息队列、任务调度
  2. 数据量不确定:动态增长
  3. 内存碎片化:无法分配连续大块内存

八、总结

数据结构 优点 缺点 适用场景
顺序表 随机访问快、存储密度高 插入删除慢、需要预分配 查询多、修改少
单链表 插入删除快、动态分配 不能随机访问、额外指针空间 修改多、查询少
循环链表 可从任意节点遍历全表 实现稍复杂 轮询调度
双向链表 双向遍历、删除简单 空间开销大 LRU缓存、浏览器历史

相关推荐
复杂网络2 小时前
论最小 Agent 计算机的形态
算法
kisshyshy18 小时前
🍦 雪糕、食堂、火车厢:三幅漫画吃透栈、队列与链表
javascript·算法
猿人谷1 天前
不只是 CPU 阈值:STAR 如何用 GAT + Transformer 做容器级自动扩缩容?
人工智能·算法
复杂网络1 天前
Stable Diffusion 视觉大模型微调技术深度调研
算法
复杂网络1 天前
基于 Stable Diffusion 架构的视觉大模型代表性工作与原理深度解析
算法
MrZhao4001 天前
Agent Loop 如何用 Hook 扩展:权限、日志与工具拦截
算法
MrZhao4001 天前
Agent 为什么需要 Skills:别把所有知识都塞进 system prompt
算法
JieE2123 天前
LeetCode 101. 对称二叉树|JS 递归 + 迭代双解法,彻底搞懂镜像判断
javascript·算法
JieE2123 天前
LeetCode 56. 合并区间|超清晰 JS 图解思路,面试高频区间题
javascript·算法·面试
Jack204 天前
HarmonyOS开发中错误处理策略:网络异常统一处理
算法