26考研 | 王道 | 数据结构 | 第二章 线性表
第二章 线性表
2.1 线性表的定义和基本操作

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

特点:
1.存在惟一的第一个元素
2.存在惟一的最后一个元素
3.除第一个元素之外,每个元素均只有一个直接前驱
4.除最后一个元素之外,每个元素均只有一个直接后继几个概念:
1.ai是线性表中的“第i个”元素线性表中的位序。
2.a1是表头元素;an是表尾元素。
3.除第一个元素外,每个元素有且仅有一个直接前驱:除最后一个元素外,每个元素有且仅有一个直接后继。存储结构:
1.顺序存储结构:顺序表
2.链式存储结构:链表
2.1.2 线性表的基础操作
- InitList(&L):初始化表。构造一个空的线性表L,分配内存空间。
- DestroyList(&L): 销毁操作。销毁线性表,并释放线性表L所占用的内存空间。
- ListInsert(&L;i,e):插入操作。在表L中的第i个位置上插入指定元素e。
- ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。
- LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元素。
- GetElem(L,i): 按位查找操作。获取表L中第i个位置的元素的值。
- Length(L):求表长。返回线性表L的长度,即L中数据元素的个数。
- PrintList(L):输出操作。按前后顺序输出线性表L的所有元素值。
- Empty(L):判空操作。若L为空表,则返回true,否则返回false。
什么时候要传入参数的引用“&“– 对参数的修改结果需要“带回来”看下面举例:
- 首先是传值调用:
1 | #include<stdio.h> |
- 然后再看传址调用
1 | #include<stdio.h> |
2.2 顺序表

我们看完线性表的逻辑结构和基本运算,现在继续学习物理结构:顺序表

本小节完整代码查看
静态分配的顺序表
1 |
|
动态分配的顺序表
1 |
|
2.2.1 顺序表的概念
- 顺序表:用顺序存储的方式实现线性表顺序存储。把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
- 顺序表的特点:
1.随机访问,即可以在O(1)O(1)时间内找到第 i 个元素。
2.存储密度高,每个节点只存储数据元素。
3.拓展容量不方便(即使使用动态分配的方式实现,拓展长度的时间复杂度也比较高,因为需要把数据复制到新的区域)。
4.插入删除操作不方便,需移动大量元素:O(n)O(n)。

2.2.2. 顺序表的实现 
- 顺序表的静态分配
顺序表的表长刚开始确定后就无法更改(存储空间是静态的)
1 | //顺序表的实现--静态分配 |
- 顺序表的动态分配
1 | //顺序表的实现——动态分配 |
2.2.3 顺序表的基本操作

- 顺序表的插入操作
ListInsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e。
平均时间复杂度 = O(n)
1 | #define MaxSize 10 //定义最大长度 |
- 顺序表的删除操作
ListDelete(&Li,&e): 删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。
平均时间复杂度 = O(n)
1 | #define MaxSize 10 |
- 顺序表的查找

- 顺序表的按位查找
GetElem(L,i):按位查找操作。获取表L中第i个位置的元素的值
平均时间复杂度O(1)
1 | // 静态分配的按位查找 |
1 | // 动态分配的按位查找 |
- 顺序表的按值查找
LocateElem(L,e): 按值查找操作。在表L中查找具有给定关键字值的元素
平均时间复杂度 =O(n)
1 | #define InitSize 10 //定义最大长度 |
2.3 线性表的链式表示
本小节完整代码查看
都是笔者手写的,如果有错的还望包涵
带头结点单链表
1 |
|
双链表
1 |
|
循环单链表
要对虚拟头结点的下一个节点做更多的操作,因为很多操作一开始都是p=q的,但是循环之后只能p=q->next,既要判断q->next是否存在,又要根据i的位置修改单链表时的操作
1 |
|

以上我们看完顺序表的物理存储了,然后我们学习单链表

2.3.1. 单链表的基本概念
- 单链表:用链式存储实现了线性结构。一个结点存储一个数据元素,各结点间的前后关系用一个指针表示。
- 特点:
优点:不要求大片连续空间,改变容量方便。
缺点:不可随机存取,要耗费一定空间存放指针。 - 两种实现方式:
带头结点,写代码更方便。头结点不存储数据,头结点指向的下一个结点才存放实际数据。
不带头结点,麻烦。对第一个数据结点与后续数据结点的处理需要用不同的代码逻辑,对空表和非空表的处理需要用不同的代码逻辑。
1 | typedef struct LNode |
- 强调这是一个单链表–使用 LinkList
- 强调这是一个结点–使用 LNode*
2.3.2. 单链表的实现
- 不带头节点
1 | typedef struct LNode{ |
- 带头节点
1 | typedef struct LNode |
带头结点和不带头结点的比较:
- 不带头结点:写代码麻烦!对第一个数据节点和后续数据节点的处理需要用不同的代码逻辑,对空表和非空表的处理也需要用不同的代码逻辑; 头指针指向的结点用于存放实际数据;
- 带头结点:头指针指向的头结点不存放实际数据,头结点指向的下一个结点才存放实际数据;
2.3.3. 单链表的插入

- 按位序插入(带头结点)
Listlnsert(&Li,e): 插入操作。在表L中的第i个位置上插入指定元素e
找到第i-1个结点(前驱结点),将新结点插入其后;其中头结点可以看作第0个结点,故i=1时也适用。
平均时间复杂度:O(n)
1 | typedef struct LNode |
- 按位序插入(不带头结点)
Listlnsert(&L,i,e): 插入操作。在表L中的第i个位置上插入指定元素e。将新结点插入其后;
因为不带头结点,所以不存在“第0个”结点,因此!i=1 时,需要特殊处理——插入(删除)第1个元素时,需要更改头指针L;
1 | typedef struct LNode |
- 指定结点的后插操作
InsertNextNode(LNode *p, ElemType e);
给定一个结点p,在其之后插入元素e; 根据单链表的链接指针只能往后查找,故给定一个结点p,那么p之后的结点我们都可知,但是p结点之前的结点无法得知
1 | typedef struct LNode |
- 指定结点的前插操作
设待插入结点是*s,将*s插入到*p的前面。我们仍然可以将*s插入到*p的后面。然后将p->data与s->data交换,这样既能满足了逻辑关系,又能是的时间复杂度为O(1)
1 | //前插操作:在p结点之前插入元素e |
2.3.4. 单链表的删除
- 按位序删除节点
ListDelete(&L, i, &e): 删除操作,删除表L中第i个位置的元素,并用e返回删除元素的值;头结点视为“第0个”结点;
思路:找到第i-1个结点,将其指针指向第i+1个结点,并释放第i个结点
1 | typedef struct LNode{ |
- 指定结点的删除
1 | bool DeleteNode(LNode *p){ |
2.3.5. 单链表的查找

- 单链表的按位查找
GetElem(L, i): 按位查找操作,获取表L中第i个位置的元素的值;
平均时间复杂度O(n)
1 | LNode * GetElem(LinkList L, int i){ |
- 单链表的按值查找
LocateElem(L, e):按值查找操作,在表L中查找具有给定关键字值的元素;
平均时间复杂度:O(n)
1 | LNode * LocateElem(LinkList L, ElemType e){ |
- 求单链表的长度
Length(LinkList L):计算单链表中数据结点(不含头结点)的个数,需要从第一个结点看是顺序依次访问表中的每个结点。
算法的时间复杂度为O(n)
1 | int Length(LinkList L){ |
2.3.6. 单链表的建立

- Step 1:初始化一个单链表
- Step 2:每次取一个数据元素,插入到表尾/表头
- 尾插法建立单链表
平均时间复杂度O(n)
思路:每次将新节点插入到当前链表的表尾,所以必须增加一个尾指针r,使其始终指向当前链表的尾结点。好处:生成的链表中结点的次序和输入数据的顺序会一致。
1 | // 使用尾插法建立单链表L |
- 头插法建立单链表
平均时间复杂度O(n)
1 | LinkList List_HeadInsert(LinkList &L){ //逆向建立单链表 |
- 链表的逆置
算法思想:逆置链表初始为空,原表中结点从原链表中依次“删除”,再逐个插入逆置链表的表头(即“头插”到逆置链表中),使它成为逆置链表的“新”的第一个结点,如此循环,直至原链表为空;
1 | LNode *Inverse(LNode *L) |
2.3.7. 双链表

- 双链表中节点类型的描述
1 | typedef struct DNode{ //定义双链表结点类型 |
- 双链表的初始化(带头结点)
1 | typedef struct DNode{ //定义双链表结点类型 |
- 双链表的插入操作
后插操作
InsertNextDNode(p, s): 在p结点后插入s结点
1 | bool InsertNextDNode(DNode *p, DNode *s){ //将结点 *s 插入到结点 *p之后 |
- 双链表的删除操作
删除p节点的后继节点
1 | //删除p结点的后继结点 |
- 双链表的遍历操作
前向遍历
1 | while(p!=NULL){ |
后向遍历
1 | while(p!=NULL){ |
注意:双链表不可随机存取,按位查找和按值查找操作都只能用遍历的方式实现,时间复杂度为O(n)O(n)
2.3.8. 循环链表

- 循环单链表
最后一个结点的指针不是NULL,而是指向头结点
1 | typedef struct LNode{ |
单链表和循环单链表的比较:
★★单链表:从一个结点出发只能找到该结点后续的各个结点;对链表的操作大多都在头部或者尾部;设立头指针,从头结点找到尾部的时间复杂度=O(n),即对表尾进行操作需要O(n)的时间复杂度;
★★循环单链表:从一个结点出发,可以找到其他任何一个结点;设立尾指针,从尾部找到头部的时间复杂度为O(1),即对表头和表尾进行操作都只需要O(1)的时间复杂度;
★★循环单链表优点:从表中任一节点出发均可找到表中其他结点。
- 循环双链表
表头结点的prior指向表尾结点,表尾结点的next指向头结点
1 | typedef struct DNode{ |
- 循环链表的插入
1 | bool InsertNextDNode(DNode *p, DNode *s){ |
- 循环链表的删除
1 | //删除p的后继结点q |
2.3.9. 静态链表

**单链表:**各个结点散落在内存中的各个角落,每个结点有指向下一个节点的指针(下一个结点在内存中的地址);
**静态链表:**用数组的方式来描述线性表的链式存储结构: 分配一整片连续的内存空间,各个结点集中安置,包括了——数据元素and下一个结点的数组下标(游标)
静态链表用代码表示
1 | #define MaxSize 10 //静态链表的最大长度 |
或者是:
1 | #define MaxSize 10 //静态链表的最大长度 |
相当于:
1 | #define MaxSize 10 //静态链表的最大长度 |
2.3.10. 顺序表和链表的比较
【逻辑结构】
- 顺序表和链表都属于线性表,都是线性结构
【存储结构】
- 顺序表:顺序存储
- 优点:支持随机存取,存储密度高
- 缺点:大片连续空间分配不方便,改变容量不方便
- 链表:链式存储
- 优点:离散的小空间分配方便,改变容量方便
- 缺点:不可随机存取,存储密度低
【基本操作 - 创建】
- 顺序表:需要预分配大片连续空间。若分配空间过小,则之后不方便拓展容量;若分配空间过大,则浪费内存资源;
- 静态分配:静态数组,容量不可改变
- 动态分配:动态数组,容量可以改变,但是需要移动大量元素,时间代价高(malloc(),free())
- 链表:只需要分配一个头结点或者只声明一个头指针
【基本操作 - 销毁】
- 静态数组——系统自动回收空间
- 动态分配:动态数组——需要手动free()
【基本操作-增/删】
- 顺序表:插入/删除元素要将后续元素后移/前移;时间复杂度=O(n),时间开销主要来自于移动元素;
- 链表:插入/删除元素只需要修改指针;时间复杂度=O(n),时间开销主要来自查找目标元素
【基本操作-查】
顺序表
- 按位查找:O(1)
- 按值查找:O(n),若表内元素有序,可在O(log2n)时间内找到
链表
- 按位查找:O(n)
- 按值查找:O(n)
顺序、链式、静态、动态四种存储方式的比较
顺序存储的固有特点:
- 逻辑顺序与物理顺序一直,本质上是用数组存储线性表的各个元素(即随机存取);存储密度大,存储空间利用率高。
链式存储的固有特点:
- 元素之间的关系采用这些元素所在的节点的“指针”信息表示(插、删不需要移动节点)。
静态存储的固有特点:
- 在程序运行的过程中不要考虑追加内存的分配问题。
动态存储的固有特点:
- 可动态分配内存;有效的利用内存资源,使程序具有可扩展性。













