数据结构学习笔记(王道)
PS:本文章部分内容参考自王道考研数据结构笔记
文章目录
一、绪论
1.1. 数据结构
-
数据:是对客观事物的符号表示,在计算机科学中是指所有能输入到计算机中并被计算机程序处理的符号的总称。
-
数据元素:数据的基本单位,一个数据元素可由若干数据项组成。
-
数据项:数据的不可分割的最小单位。
-
数据对象:性质相同的数据元素的集合,是数据的一个子集。
-
数据结构:指互相之间存在着一种或多种特定关系的数据元素的集合,包括逻辑结构,存储结构和对数据的运算。(数据元素都不是孤立存在的)。
-
抽象数据类型(ADT):指一个数学模型以及定义在该模型上的一组操作,只取决于它的一组逻辑特性,用一个三元组表示(D, S, P)。
-
数据类型:是程序设计语言中的一个概念,它是一个值的集合和操作的集合。
-
逻辑结构:是指数据之间关系的描述,与数据的存储结构无关。分为线性结构和非线性结构,通常分为四类结构:
- 集合:结构中的数据元素除了同属于一种类型外,别无其它关系。
- 线性结构:结构中的数据元素之间存在一对一的关系。
- 树型结构:结构中的数据元素之间存在一对多的关系。
- 图状结构(网状结构):结构中的数据元素之间存在多对多的关系。
-
存储结构:是指数据结构在计算机中的表示,又称为数据的物理结构。它包括数据元素的表示和关系的表示,通常由四种基本的存储方法实现:
- 顺序存储方式。数据元素顺序存放,每个存储结点只含一个元素,存储位置反映数据元素间的逻辑关系,存储密度大。有些操作(如插入、删除)效率较差。
- 链式存储方式。每个存储结点除包含数据元素信息外还包含一组(至少一个)指针,指针反映数据元素间的逻辑关系。这种方式不要求存储空间连续,便于动态操作(如插入、删除等),但存储空间开销大(用于指针),且不能折半查找。
- 索引存储方式。除数据元素存储在一组地址连续的内存空间外,还需建立一个索引表,索引表中索引指示存储结点的存储位置(下标)或存储区间端点(下标)。
- 散列存储方式。通过散列函数和解决冲突的方法,将关键字散列在连续的有限的地址空间内,并将散列函数的值解释成关键字所在元素的存储地址。其特点是存取速度快,只能按关键字随机存取,不能顺序存取,也不能折半存取。
1.2. 算法
1.2.1. 算法的基本概念
- 算法:是对特定问题求解步骤的一种描述,是指令的有限序列。其中每一条指令表示一个或多个操作。
- 算法的特性:有穷性、确定性、可行性、输入、输出。
- 算法的设计目标:正确性,可读性,健壮性,高效率与低存储量需求。
算法和程序十分相似,但又有区别。程序不一定具有有穷性,程序中的指令必须是机器可执行的,而算法中的指令则无此限制。算法代表了对问题的解,而程序则是算法在计算机上的特定的实现。一个算法若用程序设计语言来描述,则它就是一个程序。
1.2.2. 算法的时间复杂度
-
如何计算:
- 找到一个基本操作(最深层循环)
- 分析该基本操作的执行次数x与问题规模n的关系 x = f ( n ) x=f(n) x=f(n)
- x的数量级 O ( x ) O(x) O(x)就是算法时间复杂度 T ( n ) T(n) T(n): O ( x ) = T ( n ) O(x)=T(n) O(x)=T(n)
-
常用技巧:
-
加法规则: O ( f ( n ) ) + O ( g ( n ) ) = O ( m a x ( f ( n ) , g ( n ) ) ) O(f(n))+O(g(n))=O(max(f(n), g(n))) O(f(n))+O(g(n))=O(max(f(n),g(n)))
-
乘法规则: O ( f ( n ) ) × O ( g ( n ) ) = O ( f ( n ) × g ( n ) ) O(f(n))×O(g(n))=O(f(n)×g(n)) O(f(n))×O(g(n))=O(f(n)×g(n))
-
“常对幂指阶”
O ( 1 ) < O ( l o g 2 n ) < O ( n ) < O ( n l o g 2 n ) < O ( n 2 ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) < O ( n n ) O(1)<O(log_2n)<O(n)<O(nlog_2n)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n) O(1)<O(log2n)<O(n)<O(nlog2n)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)
-
-
三种复杂度:
- 最坏时间复杂度:考虑输入数据“最坏”的情况。
- 平均时间复杂度:考虑所有输入数据都等概率出现的情况。
- 最好时间复杂度:考虑输入数据“最好”的情况。
算法的性能问题只有在 n 很大时才会暴露出来
1.2.3. 算法的空间复杂度
-
普通程序:
- 找到所占空间大小与问题规模相关的变量
- 分析所占空间 x 与问题规模 n 的关系 x = f ( n ) x=f(n) x=f(n)
- x 的数量级 O ( x ) O(x) O(x) 就是算法空间复杂度 S ( n ) S(n) S(n)
-
递归程序:
- 找到递归调用的深度x与问题规模n的关系 x = f ( n ) x=f(n) x=f(n)
- x的数量级 O ( x ) O(x) O(x) 就是算法空间复杂度 S ( n ) S(n) S(n)
二、线性表
2.1. 线性表的定义和操作
2.1.1. 线性表的基本概念
-
线性表:是具有相同数据类型的 n 个数据元素的有限序列。
-
特点:
-
存在惟一的第一个元素。
-
存在惟一的最后一个元素。
-
除第一个元素之外,每个元素均只有一个直接前驱。
-
除最后一个元素之外,每个元素均只有一个直接后继。
-
-
线性表的存储结构:
- 顺序存储结构:顺序表
- 链式存储结构:链表
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 的长度,即表中元素的个数。 -
PrintList(L)
:打印表。按顺序输出线性表 L 的所有元素值。 -
Empty(L)
:判断是否为空。若 线性表L 为空表,则返回 true,否则返回 false。
操作数据结构的思路:创销、增删改查
2.2. 顺序表
2.2.1. 顺序表的基本概念
-
顺序表:用顺序存储的方式实现线性表。顺序存储,将逻辑上相邻的元素存储在相邻的物理位置上。
-
特点:
- 随机访问,即可以在 O ( 1 ) O(1) O(1) 时间内找到第 i 个元素。
- 存储密度高,每个节点只存储数据元素。
- 拓展容量不方便(即使使用动态分配的方式实现,拓展长度的时间复杂度也比较高,因为需要把数据复制到新的区域)。
- 插入删除操作不方便,需移动大量元素: O ( n ) O(n) O(n)
2.2.2. 顺序表的实现
静态实现:
#define MaxSize 10 // 定义最大长度
typedef struct {
int data[MaxSize]; // 使用静态的数组存放数据元素
int length; // 顺序表的当前长度
}SqList;
// 初始化顺序表
void InitList(SqList &L) {
L.length = 0; // 顺序表初始长度为0
}
int main() {
SqList L; // 声明一个顺序表
InitList(L); // 初始化顺序表
return 0;
}
动态实现:
#define InitSize 10 // 顺序表的初始长度
typedef struct {
int *data; // 声明动态分配数组的指针
int MaxSize; // 顺序表的最大容量
int length; // 顺序表的当前长度
}SeqList;
// 初始化顺序表
void InitList(SqList &L) {
// 用malloc函数申请一片连续的存储空间
L.data = (int *)malloc(InitSize * sizeof(int));
L.length = 0;
L.MaxSize = InitSize;
}
// 增加动态数组的长度
void IncreaseSize(SqList &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; // 顺序表最大长度增加len
free(p); // 释放原来的内存空间
}
int main() {
SeqList L; // 声明一个顺序表
InitList(L); // 初始化顺序表
...
IncreaseSize(L, 5);
return 0;
}
malloc()
函数的作用:会申请一片存储空间,并返回存储空间第一个位置的地址,也就是该位置的指针。
2.2.3. 顺序表的基本操作
插入:
#define MaxSize 10 // 定义最大长度
typedef struct {
int data[MaxSize]; // 用静态的数组存放数据元素
int length; // 顺序表的当前长度
}SqList;
// 在顺序表i位置插入e
bool ListInsert(SqList &L, int i, int e) {
if (i < 1 || i > L.length+1) // 判断i的范围是否有效
return false;
if (L.length >= MaxSize) // 判断存储空间是否已满
return false;
for (int j = L.length; j >= i; j--) // 将第i个元素之后的元素后移
L.data[j] = L.data[j-1];
L.data[i-1] = e; // 在位置i处放入e
L.length++; // 长度+1
return true;
}
int main() {
SqList L;
InitList(L);
ListInsert(L, 3, 3);
return 0;
}
时间复杂度:
- 最好时间复杂度: O ( 1 ) O(1) O(1)
- 最坏时间复杂度: O ( n ) O(n) O(n)
- 平均时间复杂度: O ( n ) O(n) O(n)
删除:
#define MaxSize 10
typedef struct {
int data[MaxSize];
int length;
} SqList;
// 删除顺序表i位置的数据并存入e
bool ListDelete(SqList &L, int i, int &e) {
if (i < 1 || i > L.length) // 判断i的范围是否有效
return false;
e = L.data[i-1]; // 将被删除的元素赋值给e
for (int j = i; j < L.length; j++) //将第i个位置后的元素前移
L.data[j-1] = L.data[j];
L.length--;
return true;
}
int main() {
SqList L;
InitList(L);
int e = -1;
if (ListDelete(L, 3, e))
printf("已删除第3个元素,删除元素值为%d\n", e);
else
printf("位序i不合法,删除失败\n");
return 0;
}
时间复杂度:
- 最好时间复杂度: O ( 1 ) O(1) O(1)
- 最坏时间复杂度: O ( n ) O(n) O(n)
- 平均时间复杂度: O ( n ) O(n) O(n)
按位查找:
// 静态分配的按位查找
#define MaxSize 10
typedef struct {
ElemType data[MaxSize];
int length;
}SqList;
ElemType GetElem(SqList L, int i) {
return L.data[i-1];
}
// 动态分配的按位查找
#define InitSize 10
typedef struct {
ElemType *data;
int MaxSize;
int length;
}SeqList;
ElemType GetElem(SeqList L, int i) {
return L.data[i-1];
}
时间复杂度: O ( 1 ) O(1) O(1)
按值查找:
#define InitSize 10
typedef struct {
ElemType *data;
int MaxSize;
int length;
}SqList;
// 查找第一个元素值为e的元素,并返回其位序
int LocateElem(SqList L, ElemType e) {
for (int i = 0; i < L.length; i++)
if (L.data[i] == e)
return i+1; // 数组下标为i的元素值等于e,返回其位序i+1
return 0; // 没有查找到
}
在《数据结构》考研初试中,手写代码可以直接用“==”,无论 ElemType 是基本数据类型还是结构类型
时间复杂度:
- 最好时间复杂度: O ( 1 ) O(1) O(1)
- 最坏时间复杂度: O ( n ) O(n) O(n)
- 平均时间复杂度: O ( n ) O(n) O(n)
2.3. 链表
2.3.1. 单链表的基本概念
- 单链表:用链式存储实现了线性结构。一个结点存储一个数据元素,各结点间的前后关系用一个指针表示。
- 特点:
- 优点:不要求大片连续空间,改变容量方便。
- 缺点:不可随机存取,要耗费一定空间存放指针。
- 两种实现方式:
- 带头结点,写代码更方便。头结点不存储数据,头结点指向的下一个结点才存放实际数据。
- 不带头结点,麻烦。对第一个数据结点与后续数据结点的处理需要用不同的代码逻辑,对空表和非空表的处理需要用不同的代码逻辑。
2.3.2. 单链表的实现
不带头结点的单链表:
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
//初始化一个空的单链表
bool InitList(LinkList &L){
L = NULL; //空表,暂时还没有任何结点
return true;
}
void test(){
LinkList L; //声明一个指向单链表的头指针
//初始化一个空表
InitList(L);
...
}
//判断单链表是否为空
bool Empty(LinkList L){
return (L==NULL)
}
带头结点的单链表:
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
// 初始化一个单链表(带头结点)
bool InitList(LinkList &L){
L = (LNode *)malloc(sizeof(LNode)); //分配一个头结点
if (L == NULL) //内存不足,分配失败
return false;
L->next = NULL; //头结点之后暂时还没有结点
return true;
}
void test(){
LinkList L; //声明一个指向单链表的头指针
//初始化一个空表
InitList(L);
...
}
//判断单链表是否为空
bool Empty(LinkList L){
if (L->next == NULL)
return true;
else
return false;
}
2.3.3. 单链表的插入
按位序插入(带头结点):
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
//在第i个位置插入元素e
bool ListInsert(LinkList &L, int i, ElemType e){
if(i<1)
return False;
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
p = L; //循环找到第i-1个结点
while(p!=NULL && j<i-1){
//如果i>lengh,p最后会等于NULL
p = p->next;
j++;
}
//p值为NULL说明i值不合法
if (p==NULL)
return false;
//在第i-1个结点后插入新结点
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;
p->next = s;
//将结点s连到p后
return true;
}
时间复杂度:
- 最好时间复杂度: O ( 1 ) O(1) O(1)
- 最坏时间复杂度: O ( n ) O(n) O(n)
- 平均时间复杂度: O ( n ) O(n) O(n)
按位序插入(不带头结点):
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
//在第i个位置插入元素e
bool ListInsert(LinkList &L, int i, ElemType e){
//判断i的合法性
if(i<1)
return false;
//需要判断插入的位置是否是第1个
if(i==1){
LNode *s = (LNode *)malloc(size of(LNode));
s->data =e;
s->next =L;
L=s; //头指针指向新结点
return true;
}
//i>1的情况与带头结点一样,唯一区别是j的初始值为1
LNode *p; //指针p指向当前扫描到的结点
int j=1; //当前p指向的是第几个结点
p = L;
//循环找到第i-1个结点
while(p!=NULL && j<i-1){
//如果i>lengh,p最后会等于NULL
p = p->next;
j++;
}
//p值为NULL说明i值不合法
if (p==NULL)
return false;
//在第i-1个结点后插入新结点
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
时间复杂度:
- 最好时间复杂度: O ( 1 ) O(1) O(1)
- 最坏时间复杂度: O ( n ) O(n) O(n)
- 平均时间复杂度: O ( n ) O(n) O(n)
除非特别声明,否则之后的代码都默认为带头结点!
指定结点的后插操作:
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
// 在结点p后插入元素e
bool 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;
}
// 按位序插入的函数中可以直接调用后插操作
bool ListInsert(LinkList &L, int i, ElemType e){
if(i<1)
return False;
LNode *p;
//指针p指向当前扫描到的结点
int j=0;
//当前p指向的是第几个结点
p = L;
//循环找到第i-1个结点
while(p!=NULL && j<i-1){
//如果i>lengh, p最后会等于NULL
p = p->next;
j++;
}
return InsertNextNode(p, e)
}
时间复杂度: O ( 1 ) O(1) O(1)
指定结点的前插操作:
如果传入头指针,就可以循环整个链表找到指定结点p的前驱结点q,再对q进行后插操作;
如果不传入头指针,可以在指定结点p后插入一个结点s,并交换两个结点所保存的数据,从而变相实现指定结点的前插操作。
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
// 在结点p前插入元素e
bool InsertPriorNode(LNode *p, ElemType e){
if(p==NULL)
return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
// 内存不足分配失败
if(s==NULL)
return false;
// 将s插入结点p之后
s->next = p->next;
p->next = s;
// 交换两个结点中的数据
s->data = p->data;
p->data = e;
return true;
}
时间复杂度: O ( 1 ) O(1) O(1)
2.3.4. 单链表的删除
按位序删除:
typedef struct LNode{
ElemType data;
struct LNode *next;}LNode, *LinkList;
// 删除第i个结点并将其所保存的数据存入e
bool ListDelete(LinkList &L, int i, ElemType &e){
if(i<1)
return false;
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
p = L;
//循环找到第i-1个结点
while(p!=NULL && j<i-1){
//如果i>lengh,p和p的后继结点会等于NULL
p = p->next;
j++;
}
if(p==NULL)
return false;
if(p->next == NULL)
return false;
//令q暂时保存被删除的结点
LNode *q = p->next;
e = q->data;
p->next = q->next;
free(q)
return true;
}
时间复杂度:
- 最好时间复杂度: O ( 1 ) O(1) O(1)
- 最坏时间复杂度: O ( n ) O(n) O(n)
- 平均时间复杂度: O ( n ) O(n) O(n)
删除指定结点:
如果传入头指针,就可以循环整个链表找到指定结点p的前驱结点q,再对p进行删除操作;
如果不传入头指针,可以把指定结点p的后继结点q删除,并使结点p保存结点q存储的数据,从而变相实现删除指定结点的操作。但是如果指定结点p没有后继结点,这么做会报错。
// 删除指定结点p
bool DeleteNode(LNode *p){
if(p==NULL)
return false;
LNode *q = p->next; // 令q指向p的后继结点
// 如果p是最后一个结点,则q指向NULL,继续执行就会报错
p->data = q->data;
p->next = q->next;
free(q);
return true;
}
时间复杂度: O ( 1 ) O(1) O(1)
2.3.5. 单链表的查找
按位查找:
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
// 查找指定位序i的结点并返回
LNode * 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;
}
// 封装后的插入操作,在第i个位置插入元素e,可以调用查询操作和后插操作
bool ListInsert(LinkList &L, int i, ElemType e){
if(i<1)
return False;
// 找到第i-1个元素
LNode *p = GetElem(L, i-1);
// 在p结点后插入元素e
return InsertNextNode(p, e)
}
时间复杂度:
- 最好时间复杂度: O ( 1 ) O(1) O(1)
- 最坏时间复杂度: O ( n ) O(n) O(n)
- 平均时间复杂度: O ( n ) O(n) O(n)
按值查找:
// 查找数据域为e的结点指针,否则返回NULL
LNode * LocateElem(LinkList L, ElemType e){
LNode *P = L->next;
// 从第一个结点开始查找数据域为e的结点
while(p!=NULL && p->data != e){
p = p->next;
}
return p;
}
时间复杂度:
- 最好时间复杂度: O ( 1 ) O(1) O(1)
- 最坏时间复杂度: O ( n ) O(n) O(n)
- 平均时间复杂度: O ( n ) O(n) O(n)
计算单链表长度:
// 计算单链表的长度
int Length(LinkList L){
int len=0; //统计表长
LNode *p = L;
while(p->next != NULL){
p = p->next;
len++;
}
return len;
}
时间复杂度: O ( n ) O(n) O(n)
2.3.6. 单链表的建立
尾插法建立单链表:
// 使用尾插法建立单链表L
LinkList List_TailInsert(LinkList &L){
int x; //设ElemType为整型int
L = (LinkList)malloc(sizeof(LNode)); //建立头结点(初始化空表)
LNode *s, *r = L; //r为表尾指针
scanf("%d", &x); //输入要插入的结点的值
while(x!=9999){
//输入9999表示结束
s = (LNode *)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r = s; //r指针指向新的表尾结点
scanf("%d", &x);
}
r->next = NULL; //尾结点指针置空
return L;
}
时间复杂度: O ( n ) O(n) O(n)
头插法建立单链表:
LinkList 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;
//将新结点插入表中,L为头指针
scanf("%d", &x);
}
return L;
}
头插法实现链表的逆置:
// 将链表L中的数据逆置并返回
LNode *Inverse(LNode *L){
LNode *p, *q;
p = L->next; //p指针指向第一个结点
L->next = NULL; //头结点置空
// 依次判断p结点中的数据并采用头插法插到L链表中
while (p != NULL){
q = p;
p = p->next;
q->next = L->next;
L->next = q;
}
return L;
}
具体解释详见【数据结构】单链表逆置:头插法图解
2.3.7. 双链表
-
**双链表的定义:**双链表也是链表的一种。双链表的每个数据节点中都有两个指针,分别指向前驱节点和后继结点。
-
双链表的实现:
typedef struct DNode{ //定义双链表结点类型 ElemType data; //数据域 struct DNode *prior, *next; //前驱和后继指针 }DNode, *DLinklist;
-
双链表的初始化 (带头结点):
typedef struct DNode{ ElemType data; struct DNode *prior, *next; }DNode, *DLinklist; // 初始化双链表 bool InitDLinkList(Dlinklist &L){ L = (DNode *)malloc(sizeof(DNode)); if(L==NULL) return false; L->prior = NULL; //头结点的prior指针永远指向NULL L->next = NULL; //头结点之后暂时还没有结点,置空 return true; } void testDLinkList(){ DLinklist L; InitDLinkList(L); ... } // 判断双链表是否为空 bool Empty(DLinklist L){ if(L->next == NULL) return true; else return false; }
-
双链表的后插操作:
typedef struct DNode{ ElemType data; struct DNode *prior, *next; }DNode, *DLinklist; // 将结点s插入到结点p之后 bool InsertNextDNode(DNode *p, DNode *s){ if(p==NULL || s==NULL) return false; s->next = p->next; // 判断结点p之后是否有后继结点 if (p->next != NULL) p->next->prior = s; s->prior = p; p->next = s; return true; }
双链表的前插操作、按位序插入操作都可以转换成后插操作
-
双链表的删除操作:
// 删除p结点的后继结点 bool DeletNextDNode(DNode *p){ if(p==NULL) return false; // 找到p的后继结点q 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; } // 销毁一个双链表 bool DestoryList(DLinklist &L){ // 循环释放各个数据结点 while(L->next != NULL){ DeletNextDNode(L); free(L); // 头指针置空 L=NULL; } }
-
双链表的遍历:
// 向后遍历 while(p!=NULL){ // 对结点p做相应处理 p = p->next; } // 向前遍历 while(p!=NULL){ // 对结点p做相应处理 p = p->prior; } // 跳过头结点的遍历 while(p->prior!=NULL){ //对结点p做相应处理 p = p->prior; }
双链表不可随机存取,按位查找、按值查找操作都只能用遍历的方式实现。
2.3.8. 循环链表
-
**循环链表的定义:**循环链表是另一种形式的链式存储结构。它的特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环。
-
循环单链表的实现:
typedef struct LNode{ ElemType data; struct LNode *next; }DNode, *Linklist; // 初始化循环单链表 bool InitList(LinkList &L){ L = (LNode *)malloc(sizeof(LNode)); if(L==NULL) return false; // 最后一个结点的next指针指向头结点 L->next = L; return true; } // 判断循环单链表是否为空 bool Empty(LinkList L){ if(L->next == L) return true; else return false; } // 判断结点p是否为循环单链表的表尾结点 bool isTail(LinkList L, LNode *p){ if(p->next == L) return true; else return false; }
-
循环双链表的实现:
typedef struct DNode{ ElemType data; struct DNode *prior, *next; }DNode, *DLinklist; // 初始循环双链表 bool InitDLinkList(DLinklist &L){ L = (DNode *) malloc(sizeof(DNode)); if(L==NULL) return false; // 头结点的prior指针指向最后一个结点,最后一个结点的next指针指向头结点 L->prior = L; L->next = L; } // 判断循环双链表是否为空 bool Empty(DLinklist L){ if(L->next == L) return true; else return false; } // 判断结点p是否为循环双链表的表尾结点 bool isTail(DLinklist L, DNode *p){ if(p->next == L) return true; else return false; }
-
循环双链表的插入和删除操作:
// 将结点s插入到结点p之后 bool InsertNextDNode(DNode *p, DNode *s){ s->next = p->next; //循环双链表不用担心p结点的下一个结点为空 p->next->prior = s; s->prior = p; p->next = s; } // 删除p结点的后继结点 bool DeletNextDNode(DNode *p){ // 找到p的后继结点q DNode *q =p->next; //循环双链表不用担心q结点的下一个结点为空 p->next = q->next; q->next->prior=p; free(q); return true; }
2.3.9. 静态链表
- 静态链表的定义:用数组的方式实现的链表。分配一整片连续的内存空间,各个结点集中安置,每个结点包括了数据元素和下一个结点的数组下标。
-
特点:
- 优点:增、删操作不需要大量移动元素。
- 缺点:不能随机存取,只能从头结点开始依次往后查找,容量固定不变!
-
静态链表的定义:
#define MaxSize 10 //静态链表的最大长度 struct Node{ //静态链表结构类型的定义 ElemType data; //存储数据元素 int next; //下一个元素的数组下标 }; // 用数组定义多个连续存放的结点 void testSLinkList(){ struct Node a[MaxSize]; //数组a作为静态链表, 每一个数组元素的类型都是struct Node ... }
也可以这么定义:
#define MaxSize 10 //静态链表的最大长度 typedef struct{ //静态链表结构类型的定义 ELemType data; //存储数据元素 int next; //下一个元素的数组下标 }SLinkList[MaxSize]; void testSLinkList(){ SLinkList a; }
第一种是我们更加熟悉的写法,第二种写法则更加侧重于强调 a 是一个静态链表而非数组。
-
静态链表的注意点:
- 初始化静态链表时,需要把
a[0]
的next
设为-1,并将空闲结点的next
设置为某个特殊值,比如-2。 - 按位序查找结点时,从头结点出发挨个往后遍历结点,时间复杂度 O = ( n ) O=(n) O=(n)。
- 按位序插入结点的步骤:①找到一个空的结点,存入数据元素;②从头结点出发找到位序为 i-1 的结点;③修改新结点的
next
为 -1;④修改 i-1 号结点的next
为新结点的下标;
- 初始化静态链表时,需要把
2.3.10. 顺序表和链表的比较
-
逻辑结构:顺序表和链表都属于线性表,都是线性结构。
-
存储结构:
-
顺序表:顺序存储
- 优点:支持随机存取,存储密度高。
- 缺点:大片连续空间分配不方便,改变容量不方便。
-
链表:链式存储
- 优点:离散的小空间分配方便,改变容量方便。
- 缺点:不可随机存取,存储密度低。
-
-
基本操作 - 创建:
-
顺序表:需要预分配大片连续空间。若分配空间过小,则之后不方便拓展容量;若分配空间过大,则浪费内存资源。
- 静态分配:静态数组,容量不可改变。
- 动态分配:动态数组,容量可以改变,但是需要移动大量元素,时间代价高(使用
malloc()
、free()
)。
-
链表:只需要分配一个头结点或者只声明一个头指针。
-
-
基本操作 - 销毁:
-
顺序表:修改
Length
= 0- 静态分配:静态数组——系统自动回收空间。
- 动态分配:动态数组——需要手动
free()
。
-
链表:依次删除各个结点
free()
。
-
-
基本操作 - 增/删:
- 顺序表:插入 / 删除元素要将后续元素后移 / 前移;时间复杂度: O ( n ) O(n) O(n),时间开销主要来自于移动元素。
- 链表:插入 / 删除元素只需要修改指针;时间复杂度: O ( n ) O(n) O(n),时间开销主要来自查找目标元素。
-
基本操作 - 查找:
- 顺序表
- 按位查找: O ( 1 ) O(1) O(1)
- 按值查找: O ( n ) O(n) O(n),若表内元素有序,可在 O ( l o g 2 n ) O(log2n) O(log2n) 时间内找到(二分法)
- 链表:
- 按位查找: O ( n ) O(n) O(n)
- 按值查找: O ( n ) O(n) O(n)
- 顺序表
三、栈与队列
3.1. 栈
3.1.1. 栈的基本概念
- 栈是特殊的线性表:只允许在一端进行插入或删除操作,其逻辑结构与普通线性表相同。
- 栈顶:允许进行插入和删除的一端 (最上面的为栈顶元素)。
- 栈底:不允许进行插入和删除的一端 (最下面的为栈底元素)。
- 空栈:不含任何元素的空表。
- 特点:后进先出(后进栈的元素先出栈)、LIFO(Last In First Out)。
- 缺点:栈的大小不可变,解决方法:共享栈。
3.1.2. 栈的基本操作
InitStack(&S)
:初始化栈。构造一个空栈 S,分配内存空间。DestroyStack(&S)
:销毁栈。销毁并释放栈 S 所占用的内存空间。Push(&S, x)
:进栈。若栈 S 未满,则将 x 加入使其成为新的栈顶元素。Pop(&S, &x)
:出栈。若栈 S 非空,则弹出(删除)栈顶元素,并用 x 返回。GetTop(S, &x)
:读取栈顶元素。若栈 S 非空,则用 x 返回栈顶元素。StackEmpty(S)
:判空。断一个栈 S 是否为空,若 S 为空,则返回 true,否则返回 false。
3.1.3. 栈的顺序存储实现
顺序栈的定义:
#define MaxSize 10 //定义栈中元素的最大个数
typedef struct{
ElemType data[MaxSize]; //静态数组存放栈中元素
int top; //栈顶元素
}SqStack;
void testStack(){
SqStack S; //声明一个顺序栈(分配空间)
}
顺序栈的初始化:
#define MaxSize 10
typedef struct{
ElemType data[MaxSize];
int top;
}SqStack;
// 初始化栈
void InitStack(SqStack &S){
S.top = -1; //初始化栈顶指针
}
// 判断栈是否为空
bool StackEmpty(SqStack S){
if(S.top == -1)
return true;
else
return false;
}
入栈出栈:
// 新元素进栈
bool Push(SqStack &S, ElemType x){
// 判断栈是否已满
if(S.top == MaxSize - 1)
return false;
S.data[++S.top] = x;
return true;
}
// 出栈
bool Pop(SqStack &x, ElemType &x){
// 判断栈是否为空
if(S.top == -1)
return false;
x = S.data[S.top--];
return true;
}
读取栈顶元素:
// 读栈顶元素
bool GetTop(SqStack S, ElemType &x){
if(S.top == -1)
return false;
x = S.data[S.top];
return true;
}
共享栈(两个栈共享同一片空间):
#define MaxSize 10 //定义栈中元素的最大个数
typedef struct{
ElemType data[MaxSize]; //静态数组存放栈中元素
int top0; //0号栈栈顶指针
int top1; //1号栈栈顶指针
}ShStack;
// 初始化栈
void InitSqStack(ShStack &S){
S.top0 = -1;
S.top1 = MaxSize;
}
3.1.4. 栈的链式存储实现
链栈的定义:
typedef struct Linknode{
ElemType data; //数据域
Linknode *next; //指针域
}Linknode,*LiStack;
void testStack(){
LiStack L; //声明一个链栈
}
链栈的初始化:
typedef struct Linknode{
ElemType data;
Linknode *next;
}Linknode,*LiStack;
// 初始化栈
bool InitStack(LiStack &L){
L = (Linknode *)malloc(sizeof(Linknode));
if(L == NULL)
return false;
L->next = NULL;
return true;
}
// 判断栈是否为空
bool isEmpty(LiStack &L){
if(L->next == NULL)
return true;
else
return false;
}
入栈出栈:
// 新元素入栈
bool pushStack(LiStack &L,ElemType x){
Linknode *s = (Linknode *)malloc(sizeof(Linknode));
if(s == NULL)
return false;
s->data = x;
// 头插法
s->next = L->next;
L->next = s;
return true;
}
// 出栈
bool popStack(LiStack &L, int &x){
// 栈空不能出栈
if(L->next == NULL)
return false;
Linknode *s = L->next;
x = s->data;
L->next = s->next;
free(s);
return true;
}
3.2. 队列
3.2.1. 队列的基本概念
- 队列是操作受限的线性表:只允许在一端进行插入 (入队),另一端进行删除 (出队)。
- 队头:允许删除的一端。
- 队尾:允许插入的一端。
- 空队列:不含任何元素的空表。
- 特点:先进先出(先入队的元素先出队)、FIFO(First In First Out)。
3.2.2. 队列的基本操作
InitQueue(&Q)
:初始化队列。构造一个空队列 Q。DestroyQueue(&Q)
:销毁队列。销毁并释放队列 Q 所占用的内存空间。EnQueue(&Q, x)
:入队。若队列 Q 未满,将 x 加入,使之成为新的队尾。DeQueue(&Q, &x)
:出队。若队列 Q 非空,删除队头元素,并用 x 返回。GetHead(Q,&x)
:读队头元素。若队列 Q 非空,则将队头元素赋值给 x。QueueEmpty(Q)
:判空。若队列 Q 为空,则返回 true。
3.2.3. 队列的顺序存储实现
顺序队列的定义:
#define MaxSize 10; //定义队列中元素的最大个数
typedef struct{
ElemType data[MaxSize]; //用静态数组存放队列元素
int front, rear; //队头指针和队尾指针
}SqQueue;
void test{
SqQueue Q; //声明一个队列
}
顺序队列的初始化:
#define MaxSize 10;
typedef struct{
ElemType data[MaxSize];
int front, rear;
}SqQueue;
// 初始化队列
void InitQueue(SqQueue &Q){
// 初始化时,队头、队尾指针指向0
// 队尾指针指向的是即将插入数据的数组下标
// 队头指针指向的是队头元素的数组下标
Q.rear = Q.front = 0;
}
// 判断队列是否为空
bool QueueEmpty(SqQueue Q){
if(Q.rear == Q.front)
return true;
else
return false;
}
入队出队(循环队列):
// 新元素入队
bool EnQueue(SqQueue &Q, ElemType x){
// 如果队列已满直接返回
if((Q.rear+1)%MaxSize == Q.front) //牺牲一个单元区分队空和队满
return false;
Q.data[Q.rear] = x;
Q.rear = (Q.rear+1)%MaxSize;
return true;
}
// 出队
bool DeQueue(SqQueue &Q, ElemType &x){
// 如果队列为空直接返回
if(Q.rear == Q.front)
return false;
x = Q.data[Q.front];
Q.front = (Q.front+1)%MaxSize;
return true;
}
获得队头元素:
// 获取队头元素并存入x
bool GetHead(SqQueue &Q, ElemType &x){
if(Q.rear == Q.front)
return false;
x = Q.data[Q.front];
return true;
}
注意:
-
循环队列不能使用
Q.rear == Q.front
作为判空的条件,因为当队列已满时也符合该条件,会与判空发生冲突! -
解决方法一:牺牲一个单元来区分队空和队满,即将
(Q.rear+1)%MaxSize == Q.front
作为判断队列是否已满的条件。(主流方法) -
解决方法二:设置 size 变量记录队列长度。
#define MaxSize 10; typedef struct{ ElemType data[MaxSize]; int front, rear; int size; }SqQueue; // 初始化队列 void InitQueue(SqQueue &Q){ Q.rear = Q.front = 0; Q.size = 0; } // 判断队列是否为空 bool QueueEmpty(SqQueue 0){ if(Q.size == 0) return true; else return false; } // 新元素入队 bool EnQueue(SqQueue &Q, ElemType x){ if(Q.size == MaxSize) return false; Q.size++; Q.data[Q.rear] = x; Q.rear = (Q.rear+1)%MaxSize; return true; } // 出队 bool DeQueue(SqQueue &Q, ElemType &x){ if(Q.size == 0) return false; Q.size--; x = Q.data[Q.front]; Q.front = (Q.front+1)%MaxSize; return true; }
-
解决方法三:设置 tag 变量记录队列最近的操作。(
tag=0
:最近进行的是删除操作;tag=1
:最近进行的是插入操作)#define MaxSize 10; typedef struct{ ElemType data[MaxSize]; int front, rear; int tag; }SqQueue; // 初始化队列 void InitQueue(SqQueue &Q){ Q.rear = Q.front = 0; Q.tag = 0; } // 判断队列是否为空 bool QueueEmpty(SqQueue 0){ if(Q.front == Q.rear && Q.tag == 0) return true; else return false; } // 新元素入队 bool EnQueue(SqQueue &Q, ElemType x){ if(Q.rear == Q.front && tag == 1) return false; Q.data[Q.rear] = x; Q.rear = (Q.rear+1)%MaxSize; Q.tag = 1; return true; } // 出队 bool DeQueue(SqQueue &Q, ElemType &x){ if(Q.rear == Q.front && tag == 0) return false; x = Q.data[Q.front]; Q.front = (Q.front+1)%MaxSize; Q.tag = 0; return true; }
3.2.4. 队列的链式存储实现
链队列的定义:
// 链式队列结点
typedef struct LinkNode{
ElemType data;
struct LinkNode *next;
}
// 链式队列
typedef struct{
// 头指针和尾指针
LinkNode *front, *rear;
}LinkQueue;
链队列的初始化(带头结点):
typedef struct LinkNode{
ElemType data;
struct LinkNode *next;
}LinkNode;
typedef struct{
LinkNode *front, *rear;
}LinkQueue;
// 初始化队列
void InitQueue(LinkQueue &Q){
// 初始化时,front、rear都指向头结点
Q.front = Q.rear = (LinkNode *)malloc(sizeof(LinkNode));
Q.front -> next = NULL;
}
// 判断队列是否为空
bool IsEmpty(LinkQueue Q){
if(Q.front == Q.rear)
return true;
else
return false;
}
入队出队:
// 新元素入队
void EnQueue(LinkQueue &Q, ElemType x){
LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));
s->data = x;
s->next = NULL;
Q.rear->next = s;
Q.rear = s;
}
// 队头元素出队
bool DeQueue(LinkQueue &Q, ElemType &x){
if(Q.front == Q.rear)
return false;
LinkNode *p = Q.front->next;
x = p->data;
Q.front->next = p->next;
// 如果p是最后一个结点,则将队头指针也指向NULL
if(Q.rear == p)
Q.rear = Q.front;
free(p);
return true;
}
以上是带头结点的链队列,下面是不带头结点的操作:
typedef struct LinkNode{
ElemType data;
struct LinkNode *next;
}LinkNode;
typedef struct{
LinkNode *front, *rear;
}LinkQueue;
// 初始化队列
void InitQueue(LinkQueue &Q){
// 不带头结点的链队列初始化,头指针和尾指针都指向NULL
Q.front = NULL;
Q.rear = NULL;
}
// 判断队列是否为空
bool IsEmpty(LinkQueue Q){
if(Q.front == NULL)
return true;
else
return false;
}
// 新元素入队
void EnQueue(LinkQueue &Q, ElemType x){
LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));
s->data = x;
s->next = NULL;
// 第一个元素入队时需要特别处理
if(Q.front == NULL){
Q.front = s;
Q.rear = s;
}else{
Q.rear->next = s;
Q.rear = s;
}
}
//队头元素出队
bool DeQueue(LinkQueue &Q, ElemType &x){
if(Q.front == NULL)
return false;
LinkNode *s = Q.front;
x = s->data;
if(Q.front == Q.rear){
Q.front = Q.rear = NULL;
}else{
Q.front = Q.front->next;
}
free(s);
return true;
}
3.2.5. 双端队列
-
定义:
- 双端队列是允许从两端插入、两端删除的线性表。
- 如果只使用其中一端的插入、删除操作,则等同于栈。
- 输入受限的双端队列:允许一端插入,两端删除的线性表。
- 输出受限的双端队列:允许两端插入,一端删除的线性表。
-
考点:判断输出序列的合法化
- 例:数据元素输入序列为 1,2,3,4,判断 4! = 24 个输出序列的合法性
栈中合法的序列,双端队列中一定也合法ZS
栈 | 输入受限的双端队列 | 输出受限的双端队列 |
---|---|---|
14个合法 ( 1 n + 1 C 2 n n \frac{1}{n+1}C^{n}_{2n} n+11C2nn) | 只有 4213 和 4231 不合法 | 只有 4132 和 4231 不合法 |
3.3. 栈与队列的应用
3.3.1 栈在括号匹配中的应用
- 用栈实现括号匹配:
- 最后出现的左括号最先被匹配 (栈的特性——LIFO)。
- 遇到左括号就入栈。
- 遇到右括号,就“消耗”一个左括号(出栈)。
- 匹配失败情况:
- 扫描到右括号且栈空,则该右括号单身。
- 扫描完所有括号后,栈非空,则该左括号单身。
- 左右括号不匹配。
文章评论