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

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

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

一、线性表概述

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

线性表的特点

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

二、顺序存储结构

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缓存、浏览器历史

相关推荐
算法鑫探17 小时前
闰年判断:C语言实战解析
c语言·数据结构·算法·新人首发
WBluuue17 小时前
数据结构与算法:康托展开、约瑟夫环、完美洗牌
c++·算法
木子墨51618 小时前
LeetCode 热题 100 精讲 | 并查集篇:最长连续序列 · 岛屿数量 · 省份数量 · 冗余连接 · 等式方程的可满足性
数据结构·c++·算法·leetcode
2501_9219608519 小时前
双相自指图与弦论边界非对易性的结构同源
数据结构
王老师青少年编程19 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【线性扫描贪心】:均分纸牌
c++·算法·编程·贪心·csp·信奥赛·均分纸牌
EQUINOX119 小时前
2026年码蹄杯 本科院校赛道&青少年挑战赛道提高组初赛(省赛)第一场,个人题解
算法
萝卜小白19 小时前
算法实习Day04-MinerU2.5-pro
人工智能·算法·机器学习
Liangwei Lin19 小时前
洛谷 P3133 [USACO16JAN] Radio Contact G
数据结构·算法
weixin_5134499619 小时前
PCA、SVD 、 ICP 、kd-tree算法的简单整理总结
c++·人工智能·学习·算法·机器人
code_pgf19 小时前
Qwen2.5-VL 算法解析
人工智能·深度学习·算法·transformer