数据结构第二章:线性表

一、线性表的基本概念

✅ 定义

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

记作:L = (a₁, a₂, ..., aₙ)

  • n = 0:空表
  • aᵢ 的前驱:aᵢ₋₁(i > 1)
  • aᵢ 的后继:aᵢ₊₁(i < n)

✅ 基本特性

  • 元素具有相同数据类型
  • 元素之间存在一对一的线性关系
  • 支持位置访问(第 i 个元素)

二、线性表的两种存储结构

线性表可通过两种方式在计算机中实现:

复制代码
线性表
├── 顺序存储 → 顺序表(SqList)
└── 链式存储 → 链表(LinkList)
    ├── 单链表(最常用)
    ├── 双链表
    ├── 循环链表
    └── 静态链表(数组模拟指针)

三、基本操作列表(通用接口)

无论采用哪种存储方式,线性表通常支持以下9种基本操作:

操作 功能
InitList(&L) 初始化空表
DestroyList(&L) 销毁表,释放内存
ListEmpty(L) 判断是否为空
ListLength(L) 返回表长
GetElem(L, i, &e) 获取第 i 个元素值
LocateElem(L, e) 查找元素 e 的位置
ListInsert(&L, i, e) 在第 i 位插入 e
ListDelete(&L, i, &e) 删除第 i 位元素,并返回其值
PrintList(L) 打印所有元素

⚠️ 注意:i 从 1 开始计数(逻辑序号),不是数组下标!


四、顺序表(SqList)实现

1. 结构定义

(1)静态分配
cpp 复制代码
#define MaxSize 50 // 定义线性表的最大长度

// 静态顺序表结构
typedef struct {
    ElemType data[MaxSize]; // 顺序表的元素
    int length; // 顺序表的当前长度
} SqList; // 顺序表的类型定义
(2)动态分配
cpp 复制代码
#define InitSize 100 // 表长度的初始定义

// 动态顺序表结构
typedef struct {
    ElemType *data; // 顺序表的元素
    int MaxSize; // 顺序表的最大长度
    int length; // 顺序表的当前长度
} SeqList; // 顺序表的类型定义
 
L.data = new ElemType[InitSize]; // 初始动态分配

2. 操作实现(C++语言)

(1)初始化

静态分配初始化

cpp 复制代码
#define MaxSize 50 // 定义线性表的的最大长度

// 静态顺序表结构 
typedef struct {
    ElemType data[MaxSize]; // 顺序表的元素
    int length; // 顺序表的当前长度
} SqList; // 顺序表的类型定义

// 顺序表静态初始化
void initSqList(SqList &L) {
    L.length = 0;
}

int main() 
{ 
   SqList L; //声明一个顺序表
   initSqList(L); //顺序表初始长度为0
}

动态分配初始化

cpp 复制代码
#define InitSize 100 // 表长度的初始定义

// 动态顺序表结构
typedef struct {
    ElemType *data; // 顺序表的元素
    int MaxSize; // 顺序表的最大长度
    int length; // 顺序表的当前长度
} SeqList; // 顺序表的类型定义

// 顺序表动态初始化 
void initSeqList(SeqList &L) {
    L.data = new ElemType[InitSize]; // 分配存储空间
    L.MaxSize = InitSize; // 初始化存储容量
    L.length = 0; // 初始化顺序表的当前长度
} 

int main() 
{ 
   SeqList L; //声明一个顺序表
   initSeqList(L); //纯徐表动态初始化
}
(2)插入操作
cpp 复制代码
#define MaxSize 50 // 定义线性表的的最大长度

// 静态顺序表结构 
typedef struct {
    ElemType data[MaxSize]; // 顺序表的元素
    int length; // 顺序表的当前长度
} SqList; // 顺序表的类型定义

// 在位置i插入指定元素 L:顺序表 i:顺序表位序(非数组下标!!!) e: 插入元素
bool listInsert(SqList &L, int i, ElemType e) {
    // 1. 判断i的范围是否有效
    if(i < 1 || i > L.length + 1) {
        return false;
    }
    // 2. 判断当前存储空间是否已满
    if(L.length >= MaxSize) {
        return false;
    }
    // 3. 将第i个之后的元素全部往后移动一位(包括第i个元素)
    for(int j = L.length; j >= i; j--) {
        L.data[j] = L.data[j - 1];
    }
    // 4. 在位置i插入指定元素
    L.data[i - 1] = e;
    // 5. 线性表长度加1
    L.length++;
    return true;
}
(3)删除操作
cpp 复制代码
#define MaxSize 50 // 定义线性表的的最大长度

// 静态顺序表结构 
typedef struct {
    ElemType data[MaxSize]; // 顺序表的元素
    int length; // 顺序表的当前长度
} SqList; // 顺序表的类型定义

// 删除并返回指定元素 L: 顺序表 i: 顺序表位序(非数组下标!!!) e: 删除并返回的指定元素
bool ListDelete(SqList &L, int i, ElemType &e) {
    // 1. 判断i的范围是否有效
    if(i < 1 || i > L.length) {
        return false;
    }
    // 2. 返回删除的指定元素
    e = L.data[i -1];
    // 3. 将第i个之后的元素全部往前移动一位
    for(int j = i; j < L.length; j++) {
        L.data[j - 1] = L.data[j];
    }
    // 4. 线性表长度减1
    L.length--;
    return true;
}
(4)按值查找
cpp 复制代码
#define MaxSize 50 // 定义线性表的的最大长度

// 静态顺序表结构 
typedef struct {
    ElemType data[MaxSize]; // 顺序表的元素
    int length; // 顺序表的当前长度
} SqList; // 顺序表的类型定义

// 按值查找 L: 顺序表 e: 指定元素
int LocateElem(SqList L, ElemType e) {
    // 遍历数组
    for(int i = 0; i < L.length; i++) {
        // 下标为i的值等于e,返回位序++i(非下标!!!)
        if(L.data[i] == e) {
            return i + 1;
        }
    }
    return 0;  // 未找到
}

3. 顺序表特点总结

项目 说明
随机访问 ✅ O(1)
插入/删除 ❌ O(n),需移动元素
内存连续 ✅ 是
空间预分配 ✅ 需提前设定 MAXSIZE
适用场景 频繁查找、较少修改

五、单链表(LinkList)实现(带头节点)

1. 结构定义

cpp 复制代码
 // 单链表结构
typedef struct LNode{ //定义单链表结点类型
    ElemType data; // 数据域
    struct LNode *next; // 指针域
} LNode, *LinkList;

2. 操作实现(C++语言)

(1)初始化

带头节点

cpp 复制代码
 // 单链表结构
typedef struct LNode{ //定义单链表结点类型
    ElemType data; // 数据域
    struct LNode *next; // 指针域
} LNode, *LinkList;

// 带头节点的单链表的初始化
bool InitList(LinkList &L) {
    L = new LNode(); // 创建头节点
    L->next = NULL; // 头节点之后暂时还没有元素节点
    return true;
}

不带头节点

cpp 复制代码
 // 单链表结构
typedef struct LNode{ //定义单链表结点类型
    ElemType data; // 数据域
    struct LNode *next; // 指针域
} LNode, *LinkList;

// 不带头节点的单链表的初始化
bool InitList(LinkList &L) {
    L = NULL; //不带头结点的单链表的初始化
    return true;
}
(2)求表长操作
cpp 复制代码
 // 单链表结构
typedef struct LNode{ //定义单链表结点类型
    ElemType data; // 数据域
    struct LNode *next; // 指针域
} LNode, *LinkList;

// 求表长 L: 单链表指针(指向表头)
int LLength(LinkList L) {
    int len = 0;
    LNode *p = L;
    while(p -> next != NULL) {
        p = p -> next;
        len++;
    }
    return len;
}
(3)按序号查找表结点
cpp 复制代码
 // 单链表结构
typedef struct LNode{ //定义单链表结点类型
    ElemType data; // 数据域
    struct LNode *next; // 指针域
} LNode, *LinkList;

// 按序号查找结点 L: 单链表指针 i: 序号
LNode* GetElem(LinkList L, int i) {
    LNode *p = L; //指针p指向当前扫描到的结点
    int j = 0; //记录当前结点的位序,头结点是第0个结点
    // 注:这里用p != NULL而不是 p -> next != NULL是因为查找时第i个序号可能查找失败(即链表内没有第i个结点),如果使用p -> next != NULL,遍历完链表,没有找到元素,p最终指向的是最后一个非空结点
    while(p != NULL && j < i) { //循环找到第i个结点
        p = p -> next;
        j++;
    }
    return p; //返回第i个结点的指针或NULL
}
(4)按值查找表结点
cpp 复制代码
 // 单链表结构
typedef struct LNode{ //定义单链表结点类型
    ElemType data; // 数据域
    struct LNode *next; // 指针域
} LNode, *LinkList;

// 按值查找表结点 L: 单链表 e: 指定数据
LNode* LocateElem(LinkList L, ElemType e) {
    LNode *p = L -> next;
    // 注:这里用p != NULL而不是 p -> next != NULL是因为查找指定结点时可能查找失败(即链表内没有等值的结点),如果使用p -> next != NULL,遍历完链表,没有找到元素,p最终指向的是最后一个非空结点
    while(p != NULL && p -> data != e) { // 从头节点的下一个节点开始查找数据域为e的结点
        p = p -> next;
    }
    return p; // 返回该结点指针
}
(5)插入结点操作(第 i 位)

后插法

cpp 复制代码
 // 单链表结构
typedef struct LNode{ //定义单链表结点类型
    ElemType data; // 数据域
    struct LNode *next; // 指针域
} LNode, *LinkList;

// 单链表插入
bool listInsert(LinkList &L, int i, ElemType e) {
    //指针p指向当前扫描到的结点
    LNode *p = L;
    // 查找插入位置的前一个结点(第i -1 个结点)
    int j = 0;
    while(p != NULL && j < i - 1) {
        p = p -> next;
        j++;
    }
    // 结点不存在或为空返回false
    if(p == NULL) {
        return false;
    }
    // 新建待插入结点s
    LNode *s = new LNode();
    s -> data = e; // 设置数据域
    // s -> next指针指向p的下一个结点
    s -> next = p -> next;
    // p -> next指针指向s
    p -> next = s;
    return true;
}

前插法

cpp 复制代码
 // 单链表结构
typedef struct LNode{ //定义单链表结点类型
    ElemType data; // 数据域
    struct LNode *next; // 指针域
} LNode, *LinkList;

// 单链表前插法 L: 单链表 i: 插入位置 e: 插入数据
bool listBeforeInsert(LinkList &L, int i, ElemType e) {
    //指针p指向当前扫描到的结点
    LNode *p = L;
    // 查找插入位置的结点(第i个结点)
    int j = 0;
    while(p != NULL && j < i) {
        p = p -> next;
    }
    // 结点不存在或为空返回false
    if(p == NULL) {
        return false;
    }
    // 新建待插入结点s
    LNode *s = new LNode();
    // s -> next 指向p结点当前的下一个结点
    s -> next = p -> next;
    // p -> next 指向s
    p -> next = s;
    // 结点s和结点p之间的数据值互换
    s -> data = p -> data;
    p -> data = e;
    return true;
}
(6)删除结点(第 i 位)

方法一:

cpp 复制代码
 // 单链表结构
typedef struct LNode{ //定义单链表结点类型
    ElemType data; // 数据域
    struct LNode *next; // 指针域
} LNode, *LinkList;

// 删除结点 L: 单链表 i: 删除序号 e: 返回的删除数据值 
bool listDelete(LinkList &L, int i, ElemType &e) {
    //指针p指向当前扫描到的结点
    LNode *p = L;
    // 查找第 i - 1个结点
    int j = 0;
    while(p != NULL && j < i - 1) {
        p = p -> next;
        j++;
    }
    // 当前结点或下一结点不存在或为空返回false
    if(p == NULL || p->next == NULL) {
        return false;
    }
    // 新建指针q,指向p结点当前的下一个结点
    LNode *q = p -> next;
    // 返回删除数据值
    e = q -> data;
    // p -> next 指向q结点的下一个结点(即当前p结点后面第二个结点)
    p -> next = q -> next;
    // 释放q结点
    free(q);
    return true;
} 

方法二:

cpp 复制代码
 // 单链表结构
typedef struct LNode{ //定义单链表结点类型
    ElemType data; // 数据域
    struct LNode *next; // 指针域
} LNode, *LinkList;

// 删除结点 L: 单链表 i: 删除序号 e: 返回的删除数据值 
bool listDelete(LinkList &L, int i, ElemType &e) {
    //指针p指向当前扫描到的结点
    LNode *p = L;
    // 查找第 i个结点
    int j = 0;
    while(p != NULL && j < i) {
        p = p -> next;
        j++;
    }
    // 当前结点或下一结点不存在或为空返回false
    if(p == NULL || p->next == NULL) {
        return false;
    }
    // 新建指针q,指向p结点当前的下一个结点
    LNode *q = p -> next;
    // 返回删除数据值
    e = p -> data;
    // 将q的数据值赋值到p中
    p -> data = q -> data;
    // p -> next 指向q结点的下一个结点(即当前p结点后面第二个结点)
    p -> next = q -> next;
    // 释放q结点
    free(q);
    return true;
} 
(7)采用头插法建立单链表
cpp 复制代码
 // 单链表结构
typedef struct LNode{ //定义单链表结点类型
    ElemType data; // 数据域
    struct LNode *next; // 指针域
} LNode, *LinkList;

// 建立链表前插法 L:单链表
void List_HeadInsert(LinkList &L) {
    // 创建单链表头节点
    L = new LNode();
    L -> next = NULL;
    // 输入插入结点的数据值
    int x;
    scanf("%d", &x);
    // 循环前插结点
    while(x != 9999) {
        // 创建插入结点
        LNode *s = new LNode();
        // 设置数据值
        s -> data = x;
        // 插入操作
        s -> next = L -> next;
        L -> next = s;
        // 输入下一个待插入结点的数据值
        scanf("%d", &x);
    }
}
(8)采用尾插法建立单链表
cpp 复制代码
 // 单链表结构
typedef struct LNode{ //定义单链表结点类型
    ElemType data; // 数据域
    struct LNode *next; // 指针域
} LNode, *LinkList;

// 建立链表尾插法 L:单链表
void List_TailInsert(LinkList &L) {
    // 创建单链表头结点
    L = new LNode();
    // p指针指向头结点
    LNode *p = L;
    // 输入插入结点的数据值
    int x;
    scanf("%d", &x);
    // 循环尾插结点
    while(x != 9999) {
        // 创建待插入结点s
        LNode *s = new LNode();
        // 设置数据值
        s -> data = x;
        // p -> next指向插入结点
        p -> next = s;
        // p指针指向下一个结点(即刚刚插入的结点)
        p = s;
        scanf("%d", &x);
    }
    p -> next = NULL;
}

踩坑:

  • 遍历链表结点时,循环条件的选择依据操作目的有所不同:

  • while(p->next != NULL)

    适用于需要访问链表中每个有效结点的情况,此时仅需遍历无需查找条件

  • while(p != NULL && j < i)

    适用于按序号或值查找特定结点的情况,带有定位条件,需区分查找成功与失败状态

六、双链表(Doubly Linked List)

1. 结构定义

cpp 复制代码
// 双链表结构
typedef struct DNode { // 定义双链表结点类型
    ElemType data; // 数据域
    struct DNode *prior, *next; // 前驱和后继指针
} DNode, *DLinkList;

2. 插入操作(在 p 节点后插入 s)

cpp 复制代码
// 双链表结构
typedef struct DNode { // 定义双链表结点类型
    ElemType data; // 数据域
    struct DNode *prior, *next; // 前驱和后继指针
} DNode, *DLinkList;

 // 插入操作(在 p 节点后插入 s)
void DListInsert(DNode *p, DNode *s) {
    // 待插入的结点s -> next 指向p结点当前的下一个结点
    s -> next = p -> next;
    // 待插入的结点s -> prior指向p结点
    s -> prior = p;
    // p -> next指向插入结点s
    p -> next = s;
    if(s -> next != NULL){ // ④ 加边界判断,规避崩溃
        s -> next -> prior = s; 
    }
} 

3. 删除操作(删除 p 节点)

cpp 复制代码
// 双链表结构
typedef struct DNode { // 定义双链表结点类型
    ElemType data; // 数据域
    struct DNode *prior, *next; // 前驱和后继指针
} DNode, *DLinkList;

// 删除操作(删除 p 节点)
void DListDelete(DNode *p) {
    // 获取p结点的前一个结点q
    DNode *q = p -> prior;
    // q -> next 指向p结点当前的下一个结点
    q -> next = p -> next;
    // p结点的下一个结点的prior指针指向q
    if(p -> next != NULL) {
        p -> next -> prior = q;
    }
    // 释放q结点
    free(p);
}

4. 特点与考点

项目 说明
空间开销 每个节点多一个指针
访问方向 ✅ 可前向/后向遍历
插入/删除 无需查找前驱,效率更高
典型应用 浏览器"前进/后退"、LRU缓存

高频考点

"双链表中删除一个非头尾节点需修改几个指针?" → 2个


七、循环链表(Circular Linked List)

1. 逻辑特点

  • 尾节点的 next 指向头节点(或头指针)
  • 可为单循环或双循环

2. 判空条件(带头节点)

循环单链表

cpp 复制代码
L->next == L  // 表示空表

循环双链表

cpp 复制代码
L -> next = L;
L -> prior = L;

3. 遍历示例

循环单链表

cpp 复制代码
 // 单链表结构
typedef struct LNode{ //定义单链表结点类型
    ElemType data; // 数据域
    struct LNode *next; // 指针域
} LNode, *LinkList;

// 单链表遍历示例
void PrintCircularList(LinkList L) {
    LNode *p = L;
    while(p -> next != L) {
        p = p -> next;
        printf("%d", p -> data);
    }
}

循环双链表与单链表一致

4. 特点与考点

优势 应用场景
从任意节点出发可遍历全表 约瑟夫问题(Josephus)
无需额外指针记录尾部 操作系统进程调度轮转

高频考点

"带头节点的循环单链表为空的条件?" → L->next == L


八、静态链表(Static Linked List)

1. 设计思想

  • 数组模拟链表
  • 下标(cursor)代替指针

2. 结构定义

cpp 复制代码
#define MaxSize 50 //静态链表的最大长度
typedef struct { //静态链表结构类型的定义
    ElemType data; //存储数据元素
    int next; //下一个元素的数组下标
} SLinkList[MaxSize];

3. 关键机制

  • 0 号单元:通常作为头节点
  • 空闲链表 :未用分量通过 cursor 链接
  • Malloc/Free:从空闲链表取/还节点

4. 特点与考点

项目 说明
无指针 适合不支持指针的语言
逻辑关系 由游标(下标)实现
插入/删除 不需移动元素
考研地位 了解即可,可能出选择题

典型考题

"静态链表中,游标的作用是什么?" → 存储下一个元素的数组下标


九、顺序表 vs 链表 对比(必背表格)

比较项 顺序表 单链表 双链表 循环链表 静态链表
存储方式 连续内存 非连续+指针 非连续+双指针 首尾相连 数组+游标
随机访问 ✅ O(1) ❌ O(n) ❌ O(n) ❌ O(n) ❌ O(n)
插入/删除 ❌ O(n) ✅ 定位后 O(1) ✅ 更灵活 ✅ 同单/双 ✅ 同链表
空间开销 最小 同单/双 小(无指针)
适用场景 查找多 修改多 双向遍历 轮询问题 无指针环境

十、高频考点与真题提示

必考内容

  • 顺序表插入/删除时移动元素个数(如:插入第 i 位,移动 n-i+1 个)
  • 链表操作的指针变化图(画图题)
  • 时间复杂度分析(顺序表 vs 链表)
  • "带头节点" vs "不带头节点"的区别(考研默认带头)

典型真题

【2020年408】在一个长度为 n 的顺序表中删除第 i 个元素,平均移动次数为?
答案:(n-1)/2
【某校自命题】静态链表的逻辑关系由什么实现?
答案:游标(下标)
【2022年模拟题】双链表删除一个非头尾节点需修改几个指针?
答案:2 个

相关推荐
技术狂人1681 天前
(六)大模型算法与优化 15 题!量化 / 剪枝 / 幻觉缓解,面试说清性能提升逻辑(深度篇)
人工智能·深度学习·算法·面试·职场和发展
tobias.b1 天前
408真题解析-2009-8-数据结构-B树-定义及性质
数据结构·b树·计算机考研·408考研·408真题
CoovallyAIHub1 天前
为你的 2026 年计算机视觉应用选择合适的边缘 AI 硬件
深度学习·算法·计算机视觉
汉克老师1 天前
GESP2025年12月认证C++六级真题与解析(单选题8-15)
c++·算法·二叉树·动态规划·哈夫曼编码·gesp6级·gesp六级
刘立军1 天前
程序员应该熟悉的概念(8)嵌入和语义检索
人工智能·算法
hk11241 天前
【Architecture/Refactoring】2026年度企业级遗留系统重构与高并发架构基准索引 (Grandmaster Edition)
数据结构·微服务·系统架构·数据集·devops
im_AMBER1 天前
Leetcode 95 分割链表
数据结构·c++·笔记·学习·算法·leetcode·链表
Boilermaker19921 天前
[算法基础] FooldFill(DFS、BFS)
算法·深度优先·宽度优先
leiming61 天前
c++ find 算法
算法