第二章 线性表
线性表
线性表的定义
- 线性表示具有相同数据类型的 n(n >= 0)个数据元素的有限序列,其中 n 为表长,当 n = 0时线性表是一个空表。若用L命名线性表,则其一般表示为 L = (a1, a2, ai, ai+1, an)。每个数据元素所占空间一样大。
- 有次序。
- 有限(所有整数按递增次序排列不是一个线性表)。
- ai 是线性表中的“第 i 个”元素线性表的位序。
- a1 是表头元素;an 是表尾元素
- 除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继。
线性表的基本操作
InitList(&L)
:初始化表。构造一个空的线性表 L,分配内存空间。Destroy(&L)
:销毁操作。销毁线性表,并释放线性表 L 所占用的内存空间。LinstInsert(&L, i, e)
:插入操作。在表L中的第 i 个位置上插入指定元素e。ListDelete(&L, i, &e)
:删除操作。删除表 L 中的第 i 个位置的元素,并用 e 返回删除元素的值。LocateElem(L, i)
:按值查找操作。在表 L 中查找具有给定关键字值的元素。GetElem(L, i)
:按位查找操作。获取表 L 中第 i 个位置的元素的值。Length(L)
:求表长。返回线性表 L 的长度,即 L 中数据元素的个数。PrintList(L)
:输出操作。按前后顺序输出线性表 L 的所有元素值。Empty(L)
:判空操作。若 L 为空表,则返回 true,否则返回 false。
顺序表
顺序表的定义
- 用顺序存储的方式实现线性表顺序存储。把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
顺序表的实现
静态分配
1
2
3
4
5
typedef struct {
ElemType data[MaxSize];
int length;
}SqList;动态分配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct {
ElemType* data;
int MaxSize;
int lengeh;
}SeqList;
//增加动态数组的长度
void IncreaseSize(SeqList& L, int len) {
int* p = L.data;
L.data = (int*)malloc((L.MaxSize + len) * sizeof(int));
for(int i = 0; i < L.length; i++) {
L.data[i] = p[i];
}
L.MaxSize = L.MaxSize + len;
free(p);
}顺序表的特点
- 随机访问,即可以在 O(1) 时间内找到第i个元素。
- 存储密度高,每个结点只存储数据元素。
- 拓展容量不方便(即便采用动态分配的方式实现,拓展长度的时间复杂度也比较高)。
- 插入、删除操作不方便,需要移动大量元素。
顺序表的插入和删除
插入操作。在表 L 中的第 i 个位置上插入指定元素e。
1
2
3
4
5
6
7
8
9
10
11
12bool LinstInsert(SqList& L, int i, int e) {
if(i < 1 || i > L.length + 1)
return false;
if(L.length >= MaxSize)
return false;
for(int j = L.length; j >= i; j--) {
L.data[j] = L.data[j - 1];
}
L.data[i - 1] = e;
L.length++;
return true;
}最好时间复杂度:O(1)
最坏时间复杂度:O(n)
平均时间复杂度:O(n)
删除操作。删除表 L 第 i 个位置的元素,并用 e 返回删除元素的值。
1
2
3
4
5
6
7
8
9
10bool ListDelete(SqList& L, int i, int& e) {
if(i < 1 || i > L.length)
return false;
e = L.data[i - 1];
for(int j = i; j < L.length; j++) {
L.data[j - 1] = L.data[j];
L.length--;
return true;
}
}最好时间复杂度:O(1)
最坏时间复杂度:O(n)
平均时间复杂度:O(n)
顺序表的查找
按位查找操作。获取表 L 中第 i 个位置的元素的值。
1
2
3ElemType GetElem(SqList L, int i) {
return L.data[i - 1];
}时间复杂度:O(1)
按值查找操作。在表 L 中查找具有给定关键字值的元素。
1
2
3
4
5
6int LocateElem(SeqList L, ElemType e) {
for(int i = 0; i < L.length; i++)
if(L.data[i] == e)
return i + 1;
return 0;
}最好时间复杂度:O(1)
最坏时间复杂度:O(n)
平均时间复杂度:O(n)
单链表
单链表的定义
顺序表:每个结点中只存放数据元素。
- 优点:可随机存取,存储密度高。
- 缺点:要求大片连续空间,改变容量不方便。
单链表:每个结点除了存放数据元素外,还要存储指向下一个结点的指针。
- 优点:不要求大片连续空间,改变容量方便。
- 缺点:不可随机存取,要耗费一定空间存放指针。
结构体定义
1
2
3
4typedef struct LNode {
ElemType data;
struct LNode* next;
}LNode, * LinkList;强调这是一个单链表:使用
LinkList
强调这是一个结点:使用
LNode*
不带头结点的单链表
1
2
3
4bool InitList(LinkList& L) {
L = NULL;// 空表,暂时还没有任何结点(防止脏数据)
return true;
}带头结点的单链表
1
2
3
4
5
6
7bool InitList(LinkList& L) {
L = (LNode*)malloc(sizeof(LNode));
if(L == NULL)
return false;
L->next = NULL;
return true;
}头结点不存储数据。
单链表的插入
插入操作。在表L中的第i个位置上插入指定元素e。
- 带头结点:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18bool ListInsert(LinkList& L, int i, ElemType e) {
if(i < 1)
return false;
LNode* p;
int j = 0;
p = L;
while(p != NULL && j < i-1) {
p = p->next;
j++;
}
if(p == NULL)
return false;
LNode* s = (LNode*)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;//此句和下一句不能颠倒
p->next = s;
return true;
}最好时间复杂度:O(1)
最坏时间复杂度:O(n)
平均时间复杂度:O(n)
- 不带头结点:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25bool ListInsert(LinkList& L, int i, ElemType e) {
if(i < 1)
return false;
if(i == 1) {
LNode* s = (LNode*)malloc(sizeof(LNode));
s->data = e;
s->next = L;
L = s;
return true;
}
LNode* p;
int j = 1;
p = L;
while(p != NULL && j < i-1) {
p = p->next;
j++;
}
if(p == NULL)
return false;
LNode* s = (LNode*)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;
p->next = s;
return true;
}后插操作。在 p 结点之后插入元素。
1
2
3
4
5
6
7
8
9
10
11bool InsertNextNode(LNode* p, ElemType e) {
if(p == NULL)
return false;
LNode* s = (LNode*)malloc(sizeof(LNode));
if(s == NULL)// 某些情况下内存分配失败(如内存不足)
return false;
s->data = e;
s->next = p->next;
p->next = s;
return true;
}前插操作。在 p 结点之前插入元素 e。
1
2
3
4
5
6
7
8
9
10
11
12bool InsertPriorNode(LNode* p, ElemType e) {
if(p == NULL)
return false;
LNode* s = (LNode*)malloc(sizeof(LNode));
if(s == NULL)
return false;
s->next = p->next;
p->next = s;//新节点s连到p之后
s->data = p->data;//将p中元素复制到s中
p->data = e;//p中元素覆盖为e
return true;
}时间复杂度:O(1)
单链表的删除
删除操作。删除表中第 i 个位置的元素,并用 e 返回删除元素的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18bool ListDelete(LinkList& L, int i, ElemType& e) {
if(i < 1)
return false;
LNode* p;
int j = 0;
p = L;
while(p != NULL && j < i-1) {
p = p->next;
j++;
}
if(p == NULL)
return false;
LNode* q = p->next;
e = q->data;
p->next = q->next;
free(q);
return true;
}最好时间复杂度:O(1)
最坏时间复杂度:O(n)
平均时间复杂度:O(n)
删除指定结点p。
1
2
3
4
5
6
7
8
9bool DeleteNode(LNode* p) {
if(p == NULL)
return false;
LNode* q = p->next;
p->data = p->next->data;
p->next = q->next;
free(q);
return true;
}注意:不满足p是最后一个结点的情况。若需要删除最后一个结点,只能从表头依次寻找p的前驱。
单链表的查找
按位查找,返回第i个元素(带头结点)
1
2
3
4
5
6
7
8
9
10
11
12LNode* GetElem(LinkList L, int i) {
if(i < 0)
return NULL;
LNode* p;
int j = 0;
p = L;
while(p != NULL && j < i) {
p = p->next;
j++;
}
return p;
}平均时间复杂度:O(n)
封装(基本操作)的好处:避免重复代码,简洁、易维护。
按值查找,找到
数据域 == e
的结点。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17LNode* LocateElem(LinkList L, ElemType e) {
LNode* p = L->next;
while(p != NULL && p->data != e)
p = p->next;
return p;
}
- 求表的长度。
```c
int Length(LinkList L) {
int len = 0;
while(p->next != NULL) {
p = p->next;
len++;
}
}时间复杂度:O(1)
单链表的建立
尾插法建立单链表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15LinkList List_TailInsert(LinkList& L) {
int x;
L = (LinkList)malloc(sizeof(LNode));
LNode* s,* r = L;
scanf("%d", &x);
while(x != 9999) {//输入9999表示结束
s = (LNode*)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r = s;
scanf("%d", &x);
}
r->next = NULL;
return L;
}注意:设置一个指向表尾结点的指针。
头插法建立单链表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27LinkList List_HeadInsert(LinkList& L) {
LNode *s;
int x;
L = (LinkList)malloc(sizeof(LNode));
L->next = NULL;
scanf("%d", &x);
while(x != 9999) {//输入9999表示结束
s = (LNode *)malloc(sizeof(LNode));
s->data = x;
s->next = L->next;
L->next = s;
scanf("%d", &x);
}
return L;
}
# 双链表
- 单链表:无法逆向检索,有时候不太方便。
- 双链表:可双向检索,存储密度略低于单链表。
- ```c
typedef struct DNode {
ElemType data;
struct DNode* prior, * next;
}DNode, * DLinkList;双链表的初始化(带头结点)
1
2
3
4
5
6
7
8bool InitDLinkList(DLinklist& L) {
L = (DNode*)malloc(sizeof(DNode));
if(L == NULL)//内存不足,分配失败
return false;
L->prior = NULL;
L->next = NULL;
return true;
}双链表的插入
1
2
3
4
5
6
7
8
9bool InsertNextDNode(DNode* p, DNode* s) {
if(p == NULL || s == NULL)
return false;
s->next = p->next;//将s结点插入到p结点之后
if(p->next != NULL)
p->next->prior = s;
s->prior = p;
p->next = s;
}双链表的删除
1
2
3
4
5
6
7
8
9
10
11
12bool DeleteNextDNode(DNode* p) {//删除p结点的后继结点q
if(p == NULL)
return false;
DNode* q = p->next;
if(q == NULL)
return false;
p->next = q->next;
if(q->next != NULL)
q->next->prior = p;
free(q);
return true;
}双链表的遍历
- 后向遍历
1
2
3
4while(p != NULL) {
//对结点p做相应处理
p = p->next;
}- 前向遍历
1
2
3
4while(p != NULL) {
//对结点p做相应处理
p = p->prior;
}- 前向遍历(跳过头结点)
1
2
3
4while(p->prior != NULL) {
//对结点p做相应处理
p = p->prior;
}双链表不可随机存取,按位查找、按值查找操作都只能用遍历方法实现。时间复杂度O(n)
循环链表
单链表:从一个结点出发只能找到后续的各个结点。
循环单链表:从一个结点出发可以找到其他任何一个结点。
初始化循环单链表
1
2
3
4
5
6
7bool InitList(LinkList& L) {
L = (LNode*)malloc(sizeof(LNode));
if(L == NULL)//内存不足,分配失败
return false;
L->next = L;//头结点next指向头结点
return true;
}在循环单链表中如果需要经常对表头/表尾结点操作,可将表头/表尾设为头结点。
初始化循环双链表
1
2
3
4
5
6
7
8bool InitDLinkList(DLinklist& L) {
L = (DNode*)malloc(sizeof(DNode));
if(L == NULL)
return false;
L->prior = L;
L->next = L;
return true;
}循环双链表的插入
1
2
3
4
5
6bool InsertNextDNode(DNode* p, DNode* s) {
s->next = p->next;//将s结点插入到p结点之后
p->next->prior = s;
s->prior = p;
p->next = s;
}循环双链表的删除
1
2
3
4
5
6
7bool DeleteNextDNode(DNode* p) {//删除p结点的后继结点q
DNode* q = p->next;
p->next = q->next;
q->next->prior = p;
free(q);
return true;
}
静态链表
定义:分配一整片连续的内存空间,各个结点集中安置(用数组方式实现的链表)。
- 与数组不同之处:数组各个元素是按照数据下标0至n依次排序的。而在每个结点包含数据元素和下一个结点的数组下标(游标)两个信息,因而静态链表不一定依次排序。
- 每个数据元素4B,每个游标4B(每个结点8B),设起始地址为addr(不是e0地址),则e1的存放地址为addr + 8*2。
结构体定义
1
2
3
4
5
struct Node {
ElemType data;
int next;//下一个数据下标
}1
2
3
4
5
typedef struct {
ElemType data;
int next;
}SLinkList[MaxSize]SLinkList a
可用 SLinkList 定义“一个长度为 MaxSize 的 Node 型数组”
初始化静态链表:
- 把 a[0] 的 next 设为-1。
- 把其他结点的 next 设为一个特殊的值来表示空闲,如-2。
查找:从头结点出发依次往后遍历结点。
插入位序为i的结点:
- 找到一个空的结点,存入数据元素。
- 从头结点出发找到位序为 i-1 的结点。
- 修改新结点的 next。
- 修改 i-1 号结点的 next。
删除某个结点:
- 从头结点出发找到前驱结点。
- 修改前驱结点的游标。
- 被删除结点的 next 设为-2。
静态链表的优点:增删操作不需要大量移动元素
静态链表的缺点:
- 不能随机存取,只能从头结点开始依次往后查找。
- 容量固定不可变。
顺序表和链表的比较
逻辑结构
- 都属于线性表,都是线性结构。
存储结构
- 顺序表:
- 优点:支持随机存取、存储密度高。
- 缺点:大片连续空间分配不方便,改变容量不方便。
- 链表:
- 优点:离散的小空间分配方便,改变容量方便。
- 缺点:不可随机存取,存储密度低。
基本操作
- 初始化:
- 顺序表:需要预分配大片连续空间。若分配内存过小,则之后不方便拓展容量;若分配空间过大,则浪费内存资源。
- 静态分配:静态数组:容量不可改变。
- 动态分配:动态数组(malloc、free):容量可以改变,但需要移动大量元素,时间代价高。
- 链表:只需分配一个头结点(也可以不要头结点,只声明一个头指针),以后方便拓展。
- 顺序表:需要预分配大片连续空间。若分配内存过小,则之后不方便拓展容量;若分配空间过大,则浪费内存资源。
- 销毁:
- 顺序表:修改
length
=0。- 静态分配(静态数组):系统自动回收空间。
- 动态分配(动态数组:malloc、free):需要手动 free。
- 链表:依次删除各个结点(free)。
- 顺序表:修改
- 插入删除:
- 顺序表:需要将后续元素后移/前移。
- 时间复杂度:O(n)。时间开销主要来自于移动元素。若数据元素很大,则移动的时间代价很高。
- 链表:只需要修改指针。
- 时间复杂度:O(n)。时间开销主要来自于查找元素。查找元素的时间代价更低。
- 顺序表:需要将后续元素后移/前移。
- 查找:
- 顺序表:
- 按位查找:O(1)
- 按值查找:O(n)。若表内元素有序,则可在O(log2n)时间内找到。
- 链表:
- 按位查找:O(n)
- 按值查找:O(n)
- 顺序表:
- 表长难以预估、经常要增加/删除元素:链表。
- 表长可预估、查询(搜索)操作较多:顺序表。