复习——线性表

线性表(Linear List)完整学习笔记

一、基本概念

1.1 定义

线性表 是具有相同数据类型的 n(n≥0)个数据元素的有限序列

1.2 数学表示

复制代码
L = (a₁, a₂, ..., aᵢ, ..., aₙ)
  • n:表长(n=0时为空表)

  • aᵢ:数据元素,i为位序(从1开始)

1.3 基本特征

  1. 有限性:元素个数有限

  2. 有序性:逻辑上顺序排列

  3. 唯一性:每个元素有且仅有一个直接前驱和直接后继(首尾元素除外)

  4. 同质性:所有元素数据类型相同

二、顺序表(Sequence List)

2.1 存储结构

复制代码
// 静态分配
#define MAXSIZE 100
typedef struct {
    ElemType data[MAXSIZE];  // 存储空间
    int length;              // 当前长度
} SqList;

// 动态分配
typedef struct {
    ElemType *data;          // 动态数组指针
    int length;              // 当前长度
    int capacity;            // 总容量
} SeqList;

2.2 核心特点

  • 物理结构:内存连续存储

  • 地址计算:LOC(aᵢ) = LOC(a₁) + (i-1) × sizeof(ElemType)

  • 随机访问:O(1)时间复杂度

2.3 基本操作

2.3.1 初始化
复制代码
Status InitList(SqList *L) {
    L->length = 0;          // 空表长度为0
    return OK;
}
2.3.2 插入操作
复制代码
// 在第i个位置插入元素e
Status ListInsert(SqList *L, int i, ElemType e) {
    // 1. 参数检查
    if (i < 1 || i > L->length + 1) 
        return ERROR;        // i的范围不合法
    if (L->length >= MAXSIZE) 
        return ERROR;        // 存储空间已满
    
    // 2. 元素后移(从最后一个开始)
    for (int j = L->length; j >= i; j--) {
        L->data[j] = L->data[j - 1];
    }
    
    // 3. 插入新元素
    L->data[i - 1] = e;
    L->length++;
    
    return OK;
}

时间复杂度:O(n),平均移动 n/2 个元素

2.3.3 删除操作
复制代码
// 删除第i个位置的元素,用e返回其值
Status ListDelete(SqList *L, int i, ElemType *e) {
    // 1. 参数检查
    if (i < 1 || i > L->length)
        return ERROR;
    
    // 2. 保存被删除元素
    *e = L->data[i - 1];
    
    // 3. 元素前移
    for (int j = i; j < L->length; j++) {
        L->data[j - 1] = L->data[j];
    }
    
    L->length--;
    return OK;
}

时间复杂度:O(n),平均移动 (n-1)/2 个元素

2.3.4 按值查找
复制代码
int LocateElem(SqList L, ElemType e) {
    for (int i = 0; i < L.length; i++) {
        if (L.data[i] == e)
            return i + 1;    // 返回位序(从1开始)
    }
    return 0;                // 查找失败
}

时间复杂度:O(n),平均比较 (n+1)/2 次

2.4 优缺点总结

优点 缺点
1. 随机访问,支持下标操作 1. 插入删除需要移动大量元素
2. 存储密度高(无指针开销) 2. 容量固定(静态分配)
3. 缓存友好(空间局部性) 3. 扩容成本高(动态分配)

三、单链表(Singly Linked List)

3.1 存储结构

复制代码
// 节点定义
typedef struct LNode {
    ElemType data;           // 数据域
    struct LNode *next;      // 指针域
} LNode, *LinkList;

3.2 链表形态

3.2.1 不带头节点
复制代码
head → [a₁|→] → [a₂|→] → ... → [aₙ|NULL]
3.2.2 带头节点(推荐)
复制代码
head → [头节点|→] → [a₁|→] → [a₂|→] → ... → [aₙ|NULL]
        ↑
       头指针

优点:统一操作,简化代码(空表和非空表操作一致)

3.3 基本操作

3.3.1 头插法建立链表
复制代码
// 逆向建立链表(输入顺序与链表顺序相反)
LinkList CreateList_HeadInsert(int n) {
    LinkList L = (LinkList)malloc(sizeof(LNode));  // 创建头节点
    L->next = NULL;                                // 初始为空链表
    
    for (int i = 0; i < n; i++) {
        LNode *s = (LNode*)malloc(sizeof(LNode));
        scanf("%d", &s->data);
        
        s->next = L->next;    // 新节点指向原第一个节点
        L->next = s;          // 头节点指向新节点
    }
    
    return L;
}
3.3.2 尾插法建立链表
复制代码
// 正向建立链表(保持输入顺序)
LinkList CreateList_TailInsert(int n) {
    LinkList L = (LinkList)malloc(sizeof(LNode));
    LNode *r = L;                    // r始终指向尾节点
    
    for (int i = 0; i < n; i++) {
        LNode *s = (LNode*)malloc(sizeof(LNode));
        scanf("%d", &s->data);
        
        r->next = s;                 // 尾节点指向新节点
        r = s;                       // r指向新的尾节点
    }
    
    r->next = NULL;                  // 尾节点指针置空
    return L;
}
3.3.3 按序号查找
复制代码
LNode *GetElem(LinkList L, int i) {
    if (i < 0) return NULL;          // i不合法
    
    LNode *p = L;                    // p指向头节点
    int j = 0;                       // 当前p指向的节点序号
    
    while (p != NULL && j < i) {
        p = p->next;
        j++;
    }
    
    return p;                        // 返回第i个节点指针
}

时间复杂度:O(n)

3.3.4 按值查找
复制代码
LNode *LocateElem(LinkList L, ElemType e) {
    LNode *p = L->next;              // 从第一个节点开始
    
    while (p != NULL && p->data != e) {
        p = p->next;
    }
    
    return p;                        // 找到返回指针,否则返回NULL
}

时间复杂度:O(n)

3.3.5 插入节点
复制代码
// 在第i个位置插入元素e
Status ListInsert(LinkList L, int i, ElemType e) {
    // 1. 找到第i-1个节点
    LNode *p = GetElem(L, i - 1);
    if (p == NULL) return ERROR;     // i值不合法
    
    // 2. 创建新节点
    LNode *s = (LNode*)malloc(sizeof(LNode));
    s->data = e;
    
    // 3. 插入操作
    s->next = p->next;
    p->next = s;
    
    return OK;
}

时间复杂度:O(n)(主要花费在查找上,插入本身O(1))

3.3.6 删除节点
复制代码
// 删除第i个位置的节点
Status ListDelete(LinkList L, int i, ElemType *e) {
    // 1. 找到第i-1个节点
    LNode *p = GetElem(L, i - 1);
    if (p == NULL || p->next == NULL) 
        return ERROR;                // i值不合法
    
    // 2. 删除操作
    LNode *q = p->next;              // q指向要删除的节点
    *e = q->data;                    // 保存被删除元素的值
    
    p->next = q->next;               // 修改指针
    free(q);                         // 释放节点空间
    
    return OK;
}

3.4 优缺点总结

优点 缺点
1. 动态分配,无需预先确定容量 1. 不支持随机访问
2. 插入删除只需修改指针 2. 存储密度低(有指针开销)
3. 物理上可以不连续存储 3. 查找需要遍历,效率较低

四、特殊链表

4.1 循环链表(Circular Linked List)

复制代码
// 初始化循环链表(带头节点)
Status InitCircularList(LinkList *L) {
    *L = (LinkList)malloc(sizeof(LNode));
    if (*L == NULL) return ERROR;
    
    (*L)->next = *L;      // 头节点指向自身
    return OK;
}

// 判断循环链表是否为空
int IsEmpty(LinkList L) {
    return (L->next == L);
}

特点

  • 尾节点指向头节点(带头节点)或首节点(不带头节点)

  • 从任意节点出发都可访问所有节点

  • 约瑟夫环是其典型应用

4.2 双向链表(Doubly Linked List)

复制代码
typedef struct DuLNode {
    ElemType data;                // 数据域
    struct DuLNode *prior;        // 前驱指针
    struct DuLNode *next;         // 后继指针
} DuLNode, *DuLinkList;
4.2.1 插入操作
复制代码
// 在节点p之后插入节点s
Status InsertAfterNode(DuLNode *p, DuLNode *s) {
    if (!p || !s) return ERROR;
    
    s->prior = p;
    s->next = p->next;
    
    if (p->next != NULL) {
        p->next->prior = s;
    }
    
    p->next = s;
    return OK;
}
4.2.2 删除操作
复制代码
// 删除节点p
Status DeleteNode(DuLNode *p) {
    if (!p) return ERROR;
    
    if (p->prior != NULL) {
        p->prior->next = p->next;
    }
    
    if (p->next != NULL) {
        p->next->prior = p->prior;
    }
    
    free(p);
    return OK;
}

五、性能对比总结

操作/特性 顺序表 单链表 双向链表 循环链表
存储方式 连续存储 离散存储 离散存储 离散存储
随机访问 O(1) O(n) O(n) O(n)
头部插入 O(n) O(1) O(1) O(1)
尾部插入 O(1) O(n) O(1)* O(1)*
中间插入 O(n) O(n) O(n) O(n)
查找元素 O(n) O(n) O(n) O(n)
空间开销 小(无指针) 中(1指针) 大(2指针) 中(1指针)
缓存友好
扩容难度 困难 容易 容易 容易

注:如果维护尾指针,尾部操作可达O(1)

六、应用场景选择指南

6.1 选择顺序表的情况:

  • 数据量稳定或可预估

  • 需要频繁随机访问元素

  • 插入删除操作较少

  • 对内存空间要求严格

  • 追求最高访问速度

6.2 选择链表的情况:

  • 数据量变化大,无法预估

  • 频繁在任意位置插入删除

  • 内存碎片化严重

  • 需要频繁动态调整结构

  • 不要求随机访问

6.3 具体应用示例:

数据结构 典型应用
顺序表 数组、矩阵、查找表、静态数据库
单链表 栈、队列、多项式表示、稀疏矩阵
循环链表 约瑟夫环、轮询调度、循环缓冲区
双向链表 浏览器历史记录、文本编辑器、LRU缓存

七、重要考点与常见问题

7.1 易错点

  1. 边界处理:头节点、尾节点、空表

  2. 指针更新顺序:链表操作中指针修改的顺序很重要

  3. 内存泄漏:malloc后必须free

  4. 野指针:释放后要及时置NULL

7.2 经典算法

  1. 链表逆置

  2. 链表合并

  3. 链表判环

  4. 求链表中间节点

  5. 删除重复节点

7.3 代码模板

复制代码
// 链表遍历模板
void TraverseList(LinkList L) {
    LNode *p = L->next;        // 从第一个节点开始
    while (p != NULL) {
        // 处理p->data
        p = p->next;
    }
}
相关推荐
DeeplyMind1 小时前
第5章:并发与竞态条件-12:Locking Traps
linux·驱动开发·ldd
烛衔溟1 小时前
C语言图论:无向图基础
c语言·数据结构·图论·无向图
dragoooon341 小时前
[Linux网络基础——Lesson11.「NAT & 代理服务 & 内网穿透」]
linux·网络·智能路由器
秋深枫叶红1 小时前
嵌入式第二十九篇——数据结构——树
数据结构·学习·算法·深度优先
能源系统预测和优化研究1 小时前
【原创代码改进】基于贝叶斯优化的PatchTST综合能源负荷多变量时间序列预测
算法·回归·transformer·能源
渡我白衣1 小时前
计算机组成原理(3):计算机软件
java·c语言·开发语言·jvm·c++·人工智能·python
小龙报1 小时前
【C语言初阶】动态内存分配实战指南:C 语言 4 大函数使用 + 经典笔试题 + 柔性数组优势与内存区域
android·c语言·开发语言·数据结构·c++·算法·visual studio
dragoooon341 小时前
[Linux网络基础——Lesson10.「数据链路层 & ARP 具体过程 & ARP 欺骗」]
linux·网络·网络协议
小龙报1 小时前
【算法通关指南:算法基础篇(三)】一维差分专题:1.【模板】差分 2.海底高铁
android·c语言·数据结构·c++·算法·leetcode·visual studio