该文为学习笔记,仅作学习参考,如有错误,望指正!
数据结构在学什么?答案:如何用程序代码把现实世界的问题信息化。
本章内容索引:
相关概念:
数据:数据是信息的载体,是描述客观事物属性的数、字符以及所有能输入到计算机中并被计算机程序识别和处理的符号的集合。数据是计算机程序加工的原料。
数据元素(Data Element)、数据项:数据元素是数据的基本单位,又称之为记录(Record),通常作为一个整体进行考虑和处理。一个数据元素是由若干个数据项组成的,数据项是构成数据元素的不可分割的最小单位。
数据对象:是具有相同性质的数据元素的集合,是数据的一个子集。
数据结构:是相互之间存在一种或多种特定关系的数据元素的集合,包含下面三方面的内容。
数据类型:是一个值的集合和定义在此集合上的一组操作的总称。
原子类型:其值不可再分的数据类型。
结构类型:其值可以再分解为若干成分(分量)的数据类型。
数据类型是对数据元素取值范围与运算的限定。
例如在 C 语言中,若说明 int x
;则变量x可以存放一个整数,取值范围一般为:[-32768,+32767],限定的操作为:[+,-,*,/,%(取模)]。
抽象数据类型(Abstract Data Type,ADT):是抽象数据组织及与之相关的操作。用数学化的语言定义数据的逻辑结构、定义运算,与具体的实现无关。
数据的逻辑结构(Logicial Structure):
集合:各个元素同属一个集合,别无其他关系;
逻辑结构分为线性结构 & 非线性结构
线性结构——一个对一个,如线性表、栈、队列;
线性结构:元素之间是一对一的关系,除了第一个元素,所有元素都有唯一前驱;除了最后一个元素,所有元素都有唯一后继;
非线性结构:
树形结构——一个对多个,如树;树形结构:数据元素之间是一对多的关系;
图状结构——多个对多个,如图;图形结构:数据元素之间是多对多的关系;
数据的物理结构(存储结构):
顺序存储:把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
链式存储:逻辑上相邻的元素在物理位置上可以不相邻,借助指示元素存储位置的指针来表示元素之间的逻辑关系。
索引存储:在存储元素信息的同时,还建立附加的索引表。索引表中的每项称为索引项,索引项的一般形式是(关键字,地址)
散列存储:根据元素的关键字直接计算出该元素的存储地址,又称哈希(Hash)存储。
【小结】:
数据的运算:
施加在数据上的运算包括运算的定义和实现。
运算的定义是针对逻辑结构的。
运算的实现是针对存储结构的。
根据逻辑结构来定义,根据存储结构来实现。
程序=数据结构+算法;
算法的特性:
"好"算法的特质:
正确性:算法应能够正确地解决求解问题。
可读性:算法应具有良好的可读性,以帮助人们理解。
注意:算法可以用伪代码描述,甚至用文字描述,重要的是要"无歧义"地描述出解决问题的步骤。
健壮性:输入非法数据时,算法能适当地作出反应或进行处理,而不会产生莫名其妙的输出结果。
高效率与低存储量需求:
解决一个问题可以有多种不同的算法,在算法正确的前提下,评价算法好坏的方法 :
【例一】
void loveyou(int n){
int i=1;
while(i<=n){
i++;
printf("I Love You Than %d.\n", n);
}
printf("I Love You More than %d.\n", n);
}
int main(){
loveyou(3000);
}
时间开销与问题规模 n 的关系: T ( n ) = 3 n + 3 T(n)=3n+3 T(n)=3n+3
可以只考虑阶数高的部分:即 T ( n ) = O ( n ) T(n)=O(n) T(n)=O(n)
【例二】 T 3 ( n ) = n 3 + n 2 l o g 2 n T_3(n)=n^3+n^2log_2n T3(n)=n3+n2log2n
解: T 3 ( n ) = O ( n 3 ) + O ( n 2 l o g 2 n ) = O ( n 3 ) T_3(n)=O(n^3)+O(n^2log_2n)=O(n^3) T3(n)=O(n3)+O(n2log2n)=O(n3)
【总结】:
最坏时间复杂度:最坏情况下算法的时间复杂度;
平均时间复杂度:所有输入示例等概率出现的情况下,算法的期望运行时间;
最好时间复杂度:最好情况下算法的时间复杂度;(参考价值较小)
【例一】
void loveyou(int n){
int i=1;
while(i<=n){
i++;
printf("I Love You Than %d.\n", n);
}
printf("I Love You More than %d.\n", n);
}
无论问题规模怎么变,算法运行所需的内存空间都是固定的常量,算法空间复杂度为 S ( n ) = O ( 1 ) S(n)=O(1) S(n)=O(1),注:S 表示"Space"
只需关注存储空间大小与问题规模相关的变量。
空间复杂度=递归调用的深度。
线性表的定义
线性表(Linear List):是具有相同(每个数据元素所占空间一样大)数据类型的 n ( n ≥ 0 ) n(n\geq0) n(n≥0) 个数据元素的有限序列(有次序、有限),其中 n 为表长,当 n=0 时线性表是一个 空表。若用 L 命名线性表,则其一般表示为: L = ( a 1 , a 2 , . . . , a i , a i + 1 , . . . , a n ) L=(a_1,a_2,...,a_i,a_{i+1},...,a_n) L=(a1,a2,...,ai,ai+1,...,an)
线性表是信息表的一种形式,表中数据元素之间满足线性关系(或线性结构),是一种最基本、最简单的数据结构类型。
几个概念:
a i a_i ai 是线性表中的"第 i 个"元素线性表中的位序;
注意:位序从 1 开始,数组下标从 0 开始;
a 1 a_1 a1 是表头元素; a n a_n an 是表尾元素;
除第一个元素外,每个元素有且仅有一个直接前驱,除最后一个元素外,每个元素有且仅有一个直接后继;
线性表的基本操作
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 个位置的元素的值。
其他常用操作:
Tips:
对数据的操作(记忆思路)——创销、增删改查;
C 语言函数的定义—— <返回值类型> 函数名 (<参数1类型> 参数1,<参数2类型> 参数2,…);
实际开发中,可根据实际需求定义其他的基本操作;
函数名和参数的形式、命名都可改变(Reference:严蔚敏版《数据结构》);
什么时候要传入引用 “&”—— 对参数的修改结果需要 “带回来”;
【示例】C++ 工具进行编译
#include
void test(int x)
{
x=1024;
printf("test函数内部 x=%d\n",x);
}
int main()
{
int x=1;
printf("调用test前 x=%d\n", x);
test(x);
printf("调用test后 x=%d\n", x);
return 0;
}
运行结果:
调用test前 x=1
test函数内部 x=1024
调用test后 x=1
#include
void test(int &x)
{
x=1024;
printf("test函数内部 x=%d\n",x);
}
int main()
{
int x=1;
printf("调用test前 x=%d\n", x);
test(x);
printf("调用test后 x=%d\n", x);
return 0;
}
运行结果:
调用test前 x=1
test函数内部 x=1024
调用test后 x=1024
Q思考:为什么要实现对数据结构的基本操作?
A:
顺序表的定义
顺序表——用顺序存储的方式实现线性表顺序存储。把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
设线性表第一个元素的存放位置是 L O C ( L ) LOC(L) LOC(L), L O C LOC LOC 是 location 的缩写。
内存 | 地址 | |
---|---|---|
← \leftarrow ← | L O C ( L ) LOC(L) LOC(L) | |
a 1 a_1 a1 | ← \leftarrow ← | L O C ( L ) + LOC(L)+ LOC(L)+ 数据元素的大小 |
a 2 a_2 a2 | ← \leftarrow ← | L O C ( L ) + 2 ∗ LOC(L)+2* LOC(L)+2∗ 数据元素的大小 |
a 3 a_3 a3 | ← \leftarrow ← | ⋯ \cdots ⋯ |
a 4 a_4 a4 | ← \leftarrow ← | ⋯ \cdots ⋯ |
Q思考:如何知道一个数据元素大小?
A:sizeof(ElemType)
,ElemType 就是你的顺序表中存放的数据元素类型。
顺序表的特点:
顺序表的实现——静态分配
#define MaxSize 10 // 定义最大长度
typedef struct{
ElemType data[MaxSize]; // 用静态的"数组"存放数据元素
int length; // 顺序表当前长度
}SqList; // 顺序表的类型定义(静态分配)
顺序表的实现——动态分配
#define InitSize 10 // 顺序表的初始长度
typedef ElemType int;
typedef struct{
ElemType *data; // 指示动态分配数组的指针
int MaxSize; // 顺序表的最大容量
int length; // 顺序表当前长度
}SqList; // 顺序表的类型定义(动态分配方式)
typedef 关键字——数据类型重命名。
typedef <数据类型> <别名>;
ex:
typedef int zhengshu;
typedef int *zhengshuzhizhen;
I n i t L i s t ( S q L i s t ∗ L ) InitList(SqList\ *L) InitList(SqList ∗L):初始化顺序表,将数组内元素均赋值为 0;
void InitList(SqList *L){
for(int i = 0; i < MaxSize; i++)
L->data[i] = 0;
L->length = -1; // 顺序表的初始长度为0
}
void InitList(SqList *L){
L->data = (ElemType *)malloc(InitSize * sizeof(ElemType));
L->length = -1;
L->MaxSize = InitSize;
}
key:动态申请和释放内存空间,malloc、free 函数
L.data = (ElemType *)malloc(sizeof(Elemtype)*InitSize);
// malloc申请一整片连续的存储空间,函数返回一个指针,需要强制转型为你定义的数据元素类型指针
#include
#include
#define InitSize 10
typedef int ElemType;
typedef struct{
ElemType *data;
int MaxSize;
int length;
}SqList;
// 初始化顺序表
void InitList(SqList *L){
L->data = (ElemType *)malloc(InitSize * sizeof(ElemType));
L->length = 0;
L->MaxSize = InitSize;
}
// 增加动态数组的长度
void IncreaseSize(SqList *L, int len){
ElemType *p = L->data;
L->data = (ElemType *)malloc((L->MaxSize + len) * sizeof(ElemType));
for(int i = 0; i < L->length; i++){
L->data[i] = p[i]; // 数据复制到新区域,时间开销大
}
L->MaxSize += len;
free(p); // 释放内存空间
}
int main(){
SqList L; // 声明一个顺序表
InitList(&L); // 初始化顺序表
IncreaseSize(&L, 5); // 往顺序表中随便插入5个元素
return 0;
}
【小结】:realloc函数也可以实现,但建议初学者使用malloc和free更能理解背后过程。
说明:在调试 C 语言代码的时候,遇到报错:error: expected ‘;’, ‘,’ or ‘)’ before ‘&’ token,报错的直接意思是在‘&’标识符之前缺少‘;’或‘,’或‘)’,如下图:
并不是因为缺少分号或括号什么的导致的,而是错误地使用了引用传递,因为使用‘&’进行引用传递是C++中的语法习惯,事实上C语言中‘&’一般用作取地址符,不支持引用传递。
因此,针对该问题有两种解决方案:
&
使用*
(指针)替换,函数调用时,用&
传址。.cpp
。如下代码(后续代码同理,本文不再赘述!!!):给各个数据元素分配连续的存储空间,大小为 M a x S i z e ∗ s i z e o f ( E l e m T y p e ) MaxSize * sizeof(ElemType) MaxSize∗sizeof(ElemType)
C++:
#include
#define MaxSize 10 // 定义最大长度
typedef ElemType int;
typedef struct{
ElemType data[MaxSize]; // 用静态的"数组"存放数据元素
int length; // 顺序表的当前长度
}SqList; // 顺序表的类型定义
// 基本操作——初始化一个顺序表(若不初始化,系统内可能存在的脏数据会影响初始值)
void InitList(SqList &L){
for(int i=0; i
C语言:
#include
#define Maxsize 10
typedef ElemType int;
typedef struct{
ElemType data[Maxsize];
int length;
}SqList;
void InitList(Sqlist *L){
for(int i = 0; i < Maxsize; i++)
L->data[i] = 0;
L->length = 0;
}
int main()
{
SqList L;
InitList(&L);
for(int i = 0; i < Maxsize; i++)
printf("%d ", L.data[i]);
return 0;
}
Q思考:如果"数组"存满了怎么办?
A:可以放弃治疗,顺序表的表长刚开始确定后就无法更改(存储空间是静态的)。
L i s t F u l l ( S q L i s t ∗ L ) ListFull(SqList\ *L) ListFull(SqList ∗L):判断数据表是否已满,数据表满函数返回 1,否则返回 0;
int ListFull(SqList *L){ // 判断顺序表满
/*
if(L.length == N-1){
return 1;
}else{
return 0;
}
*/
return L->length == MaxSize - 1 ? 1 : 0;
}
L i s t E m p t y ( S q L i s t ∗ L ) ListEmpty(SqList\ *L) ListEmpty(SqList ∗L):判断数据表是否为空,若表空返回 -1,否则返回 0;
int ListEmpty(SqList *L){
return L.length == -1 ? 1 : 0;
}
L i s t S h o w ( S q L i s t ∗ L ) ListShow(SqList\ *L) ListShow(SqList ∗L):打印数据表内所有数据到终端;
void ListShow(SqList *L){
for(int i = 0; i < L->length; i++){
printf(" %d ", L->data[i]);
}
putchar(10);
return;
}
L i s t I n s e r t ( S q L i s t ∗ L , E l e m T y p e d a t a ) ListInsert(SqList\ *L, ElemType\ data) ListInsert(SqList ∗L,ElemType data):插入操作(头插),在表 L 中的上入指定元素 data,当前顺序表表长 +1。插入成功返回 true;否则,返回 false;
bool ListInsert(SqList *L, ElemType data){
if(ListFull(L)){
printf("顺序表满!\n");
return false;
}
L.length++;
for(int i = L->length; i > 0; i--)
L->data[i] = L->data[i - 1];
L->data[0] = data; // 插入数据到数组下标为sq->flags
return true;
}
L i s t I n s e r t ( S q L i s t ∗ L , E l e m T y p e d a t a ) ListInsert(SqList\ *L, ElemType\ data) ListInsert(SqList ∗L,ElemType data):插入操作(尾插),在表 L 中的上入指定元素 data,当前顺序表表长 +1。插入成功返回 true;否则,返回 false;
int ListInsert(SqList *L, ElemType data){
if(ListFull(L)){
printf("顺序表满!\n");
return -1;
}
L->length++;
L->data[L->length] = data; // 插入数据到数组下标为sq->flags
return 0;
}
L i s t P o s I n s e r t ( S q L i s t ∗ L , i n t p o s , E l e m T y p e d a t a ) ListPosInsert(SqList\ *L, int\ pos, ElemType\ data) ListPosInsert(SqList ∗L,int pos,ElemType data):插入操作,在表 L 中的第 pos个位置(位序)上插入指定元素 data,当前长度+1。插入成功返回 true;否则,返回 false;
用存储位置的相邻来体现数据元素之间的逻辑关系。
bool ListPosInsert(SqList *L, int pos, ElemType data){
if(pos < 0 || pos > L.length){
printf("插入位序无效!\n");
return false;
}
if(seqlist_full(sq)){
printf("当前顺序表已满!\n");
return false;
}
for(int i = L->length + 1; i >= pos; i--){
L->data[i] = L->data[i - 1];
}
L->data[pos] = data;
L->length++;
return true;
}
【小结】:好的算法,应该具有"健壮性",能处理异常情况,并给使用者反馈。
解:关注最深层循环语句的执行次数与问题规模 n 的关系
问题规模: n = L − > l e n g t h n=L->length n=L−>length (表长)
最好情况:新元素插入到表尾,不需要移动元素,循环0次,最好的时间复杂度= O ( 1 ) O(1) O(1)
最坏情况:新元素插入到表头,需要将原有的 n 个元素全部向后移动,循环 n 次,最坏时间复杂度= O ( n ) O(n) O(n)
平均情况:假设新元素插入到任何一个位置的概率相同,即 i = 1 , 2 , 3 , . . . , l e n g t h + 1 i=1,2,3,...,length+1 i=1,2,3,...,length+1 的概率都是 p = 1 p + 1 p=\frac{1}{p+1} p=p+11,平均时间复杂度= O ( n ) O(n) O(n)
L i s t D e l e t e ( S q L i s t ∗ L ) ListDelete(SqList\ *L) ListDelete(SqList ∗L):顺序表按位删除(头删)。删除成功返回已删除数据,否则返回 -1;
ElemType ListDelete(SqList *L){
if(ListEmpty(L)){
printf("表空\n");
return (ElemType)-1;
}
ElemType temp = L->data[0]; // 保存删除数据
for(int i = 1; i < L->length; i++)
L->data[i - 1] = L->data[i];
L->length--; // 数据下标前移
return temp;
}
L i s t D e l e t e ( S q L i s t ∗ L ) ListDelete(SqList\ *L) ListDelete(SqList ∗L):顺序表按位删除(尾删)。删除成功返回已删除数据,否则返回 -1;
ElemType ListDelete(SqList *L){
if(ListEmpty(L)){
printf("表空\n");
return (ElemType)-1;
}
ElemType temp = L->data[L->length]; // 保存删除数据
L->length--; // 数据下标前移
return temp;
}
L i s t P o s D e l ( S q L i s t ∗ L , i n t p o s ) : ListPosDel(SqList\ *L,\ int\ pos): ListPosDel(SqList ∗L, int pos): 删除操作,删除表 L 中第 pos 个位置的元素。删除成功返回删除元素的值;否则,返回 -1;
ElemType ListPosDel(SqList *L, int pos){
if(pos < 0 || pos > L->length){ // 判断 pos 正确性
printf("删除位序错误!\n");
return (ElemType)-1;
}
if(ListEmpty(L)){
return (ElemType)-2;
}
ElemType temp = L->data[pos]; // 保存所删除数据并返回该数据
for(i = pos - 1; i < L->length; i++)
L->data[i] = L->data[i + 1];
L->length--;
return temp;
}
解:关注最深层循环语句的执行次数与问题规模 n 的关系;
问题规模: n = L − > l e n g t h n=L->length n=L−>length (表长);
最好情况:删除表尾元素,不需要移动元素,循环0次,最好的时间复杂度= O ( 1 ) O(1) O(1);
最坏情况:删除表头元素,需要将后续的 n-1 个元素全部向前移动,循环 n-1 次,最坏时间复杂度= O ( n ) O(n) O(n);
平均情况:假设删除任何一个元素的概率相同,即 i = 1 , 2 , 3 , . . . , l e n g t h i=1,2,3,...,length i=1,2,3,...,length 的概率都是 p = 1 n p=\frac{1}{n} p=n1,则平均时间复杂度 = O ( n ) =O(n) =O(n);
G e t E l e m ( S q L i s t ∗ L , i n t i ) GetElem(SqList\ *L,\ int\ i) GetElem(SqList ∗L, int i):按位查找操作,获取表 L 中第 i 个元素的值。
ElemType GetElem(SqList *L,int i){
return L->data[i-1];
}
时间复杂度: O ( 1 ) O(1) O(1),由于顺序表的各个数据元素在内存中连续存放,因此可以根据起始地址和数据元素大小立即找到第 i 个元素——"随机存取"特性。
L o c a t e E l e m ( S q L i s t ∗ L , E l e m T y p e e ) LocateElem(SqList\ *L,\ ElemType\ e) LocateElem(SqList ∗L, ElemType e):按值查找操作。在表 L 中查找具有给定关键字值的元素。查找成功返回该数据所在顺序表中位置;否则,返回 -1;
int LocateElem(SqList *L, ElemType data){
for(int i = 0; i <= L->length; i++){
if(sq->data[i] == data)
return i + 1;
}
printf("数据不存在\n");
return -1;
}
L i s t U p d a t e V a l u e ( S q L i s t ∗ L , E l e m T y p e O l d D a t a , E l e m T y p e N e w D a t a ) ListUpdateValue(SqList\ *L,\ ElemType\ OldData,\ ElemType\ NewData) ListUpdateValue(SqList ∗L, ElemType OldData, ElemType NewData):将顺序表中旧数据替换为新数据,并输出替换数据的个数。
void ListUpdateValue(SqList *L, ElemType OldData, ElemType NewData){
int flags = 0;
for(int i = 0; i <= L->length; i++){
if(L->data[i] == OldData){
L->data[i] = NewData;
flags++; // 标记替换个数
}
}
if(flags != 0){ // 判断是否有数值替换
printf("替换成功,替换了 %d 个\n", flags);
}else{
printf("无数值替换!\n");
}
}
L i s t U p d a t e P o s V a l u e ( S q L i s t ∗ L , i n t p o s , E l e m T y p e d a t a ) ListUpdatePosValue(SqList\ *L,\ int\ pos,\ ElemType\ data) ListUpdatePosValue(SqList ∗L, int pos, ElemType data):将顺序表 pos 位置数据修改为 data。修改成功返回 0;否则,返回 -1;
int ListUpdatePosValue(SqList *L, int pos, ElemType data){
if(pos < 0 || pos > L.length){
printf("给出的位置错误!");
return -1;
}
L->data[pos] = data;
return 0;
}
L i s t R e p e a t V a l u e D e l ( S q L i s t ∗ L ) ListRepeatValueDel(SqList\ *L) ListRepeatValueDel(SqList ∗L):删除顺序表中重复的数据。
void ListRepeatValueDel(SqList *L){
for(int i = 0; i < L->length; i++){
for(int j = i+1; j <= L->length; j++){
if(L->data[i] == L->data[j]){
ListRepeatValueDel(L,j);
j--;
}
}
}
}
L i s t F u n ( S q L i s t ∗ L 1 , S q L i s t ∗ L 2 ) ListFun(SqList\ *L1,\ SqList\ *L2) ListFun(SqList ∗L1, SqList ∗L2):以 L1 表为基础检测 L2 的数据是否有重复,最后将未重复数据插入 L1 表;
void ListFun(SqList &L1, SqList &L2){
for(int i = 0; i <= L2.length; i++){
if(LocateElem(L1,L2data[i]) == -1){ // 以L1表为基础检测L2的数据是否有重复
ListInsert(s1,s2->data[i]); // 未重复数据插入 L1 表
}
}
}
【小结】:《数据结构》考研初试中,手写代码可以直接用 “==”,无论ElemType是基本数据类型还是结构类型。手写代码主要考察学生是否能理解算法思想,不会严格要求代码完全可运行。有的学校考《C语言程序设计》,那么…也许就要语法严格一些。
最好情况:目标元素在表头,循环1次,最好时间复杂度 = O ( 1 ) =O(1) =O(1)
最坏情况:目标元素在表尾,循环 n 次,最坏时间复杂度 = O ( n ) =O(n) =O(n)
平均情况:假设目标元素出现在任何一个位置的概率相同,都是 1 n \frac{1}{n} n1,则平均时间复杂度 = O ( n ) =O(n) =O(n)
fopen()
#include
FILE *fopen(const char *path, const char *mode);
io
操作),失败则返回 NULL
;fgets()
#include
char *fgets(char *restrict s, int n, FILE *restrict stream);
NULL
;[root@localhost test]# cat link_loop.c
1
12
123
1234
12345
123456
1234567
12345678
123456789
#include
int main(int argc, const char *argv[]){
char buf[128] = {};
FILE *fp = fopen("link_loop.c", "r");
while(fgets(buf, 128, fp) != NULL){
printf("%s", buf);
}
return 0;
}
[root@localhost test]# ./a.out
1
12
123
1234
12345
123456
1234567
12345678
123456789
什么是单链表?
顺序表:
优点:可随机存取,存储密度高;
缺点:要求大片连续空间,改变容量不方便;
单链表:
优点:不要求大片连续空间,改变容量方便;
缺点:不可随机存取,要消耗一定空间存放指针;
用代码定义一个单链表
struct LNode{ // 定义单链表结点类型,LNode:结点
ElemType data; // 每个节点存放一个数据元素,data:数据域
struct LNode *next; // 指针指向下一个节点,*next:指针域
}LNode, *LinkList;
// 增加一个新的结点:在内存中申请一个结点所需空间,并用指针 p 指向这个结点
struct LNode *p=(struct LNode *)malloc(sizeof(struct LNode));
// 单链表
typedef struct LNode LNode;
LNode *p=(LNode *)malloc(sizeof(LNode));
注意:
使用 LinkList
——强调这是一个单链表;
使用 LNode *
——强调这是一个结点;
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
// 或
struct LNode{
ElemType data;
struct LNode *next;
}
typedef struct LNode LNode;
typedef struct LNode *LinkList;
要表示一个单链表时,只需声明一个头指针 L,指向单链表的第一个结点。
LNode *L; // 声明一个指向单链表第一个结点的指针
// 或
LinkList L; // 声明一个指向单链表第一个结点的指针,代码可读性更强
I n i t L i s t ( L i n k L i s t L ) InitList(LinkList\ L) InitList(LinkList L):初始化单链表。初始化成功返回 true;否则,失败返回 false;
带头节点的单链表初始化
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 InitList(LinkList L){
L=NULL; // 空表,暂时还没有任何结点,防止脏数据
return true;
}
void test(){
// 声明一个指向单链表的指针,注意:此处仅是在内存中开辟了一块空间,还没有创建结点
LinkList L;
InitList(L);
// ...后续代码...
}
不带头节点 VS 带头节点
头插法、尾插法:核心就是初始化操作,指定结点的后插操作;
头插法的重要应用:链表的逆置;
L i s t H e a d I n s e r t ( L i n k L i s t L , E l e m T y p e d a t a ) ListHeadInsert(LinkList\ L, ElemType\ data) ListHeadInsert(LinkList L,ElemType data):将 data 插入单链表表头,逆向建立单链表。成功返回 true;否则,失败返回 false;
带头节点
bool ListHeadInsert(LinkList L, ElemType e){
LNode *s = (LNode *)malloc(sizeof(LNode));
if(s == NULL)
return false;
s->data = e;
s->next = L->next;
L->next = s;
return true;
}
不带头节点
bool ListHeadInsert1(LinkList L, ElemType data){
LinkList p = (LinkList)malloc(sizeof(LNode));
if(p == NULL){
printf("头插数据时空间分配报错!\n");
return false;
}
p->data = data;
p->next = L;
}
L i s t T a i l I n s e r t ( L i n k L i s t L , E l e m T y p e d a t a ) ListTailInsert(LinkList\ L,\ ElemType\ data) ListTailInsert(LinkList L, ElemType data):将 data 插入单链表表尾。成功返回 true;否则,失败返回 false;
bool ListTailInsert(LinkList L, ElemType e){
LinkList temp = L;
while(temp->next !=NULL){
temp = temp->next;
}
LNode *s=(LNode *)malloc(sizeof(LNode));
if(s == NULL) // 内存分配失败(某些情况下有可能分配失败,如内存不足)
return false;
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
I n s e r t N e x t N o d e ( p , e ) InsertNextNode(p, e) InsertNextNode(p,e):在 p 结点之后插入元素 e;
// 后插操作:在 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;
}
时间复杂度: O ( 1 ) O(1) O(1)
I n s e r t P r i o r N o d e ( p , e ) InsertPriorNode(p,e) InsertPriorNode(p,e):在 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->next = p->next;
p->next = s;
s->data = p->data;
p->data = e;
return true;
}
L i s t I n s e r t ( & L , i , e ) ListInsert(\&L,i,e) ListInsert(&L,i,e):插入操作,在表 L 中的第 i 个位置上插入指定元素 e。
方法:可以找到第 i-1个结点,将新节点插入其后。
最好的时间复杂度: O ( 1 ) O(1) O(1)
平均时间复杂度: O ( n ) O(n) O(n)
bool ListInsert(LinkList L, int i, ElemType e){
if(i < 1)
return false;
LNode *p; // 指针 p 指向当前扫描到的结点
int j = 0; // 当前 p 指向的是第几个结点
P = L; // L 指向头节点,头节点是第 0 个结点(不存数据)
while(p != NULL && j < i - 1){ // 循环找到第 i-1 个结点
p = p->next;
j++;
}
return InsertNextNode(p, e); // 插入成功
}
L i s t I n s e r t ( & L , i , e ) ListInsert(\&L,i,e) ListInsert(&L,i,e):插入操作,在表 L 中的第 i 个位置上插入指定元素 e。
方法:可以找到第 i-2个结点,将新节点插入其后。
bool ListInsert(LinkList L, int i, ElemType e){
if(i<1)
return false;
if(i==1){ // 插入第一个结点的操作与其他结点操作不同,插入或删除第 1 个元素时,需要更改头指针 L
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-2){ // 循环找到第 i-1 个结点
p = p->next;
j++;
}
return InsertNextNode(p, e); // 插入成功
}
【总结】:不带头结点写代码更不方便,推荐用带头结点的。
【注意】:考试中带头、不带头都有可能考察,注意审题。
时间复杂度: O ( 1 ) O(1) O(1)
D e l e t e N o d e ( L N o d e ∗ p ) DeleteNode(LNode *p) DeleteNode(LNode∗p):删除指定节点 p;
// 删除指定结点 p
bool DeleteNode(LNode *p){
if(p == NULL)
return false;
LNode *m = p->next;
p->data = m->data;
p->next = m->next;
free(m);
return true
}
ListDelete(&L, i, &e):删除操作,删除表 L 中第 i 个位置(方法:找到第 i-1 个结点,将其指针指向第 i+1 个结点,并释放第 i 个结点)的元素,并用 e 返回删除元素的值。
最坏、平均时间复杂度: O ( n ) O(n) O(n)
最好时间复杂度: O ( 1 ) O(1) O(1)
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
bool 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;
if(p->next == NULL)
return false;
LNode *q = p->next;
e = q->data;
p->next = q->next;
free(q);
return true;
}
【总结】:
GetElem(L,i):按位查找操作,获取表 L 中第 i 个位置的元素的值。
平均时间复杂度: O ( n ) O(n) O(n)
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;
}
LocateElem(L,e):按值查找操作,在表 L 中查找具有给定关键字值的元素。
时间复杂度: O ( n ) O(n) O(n)
LNode * LocateElem(LinkList L, ElemType e){
LNode *p = L->next;
while(p != NULL && p->data !=e)
p = p->next;
return p; // 找到后返回该结点指针,否则返回NULL
}
时间复杂度: 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;
}
双链表的初始化(带头结点)
DLinklist 等价于 DNode *,双链表不可随机存取,按位查找、按值查找操作都只能用遍历实现。时间复杂度: O ( n ) O(n) O(n)
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;
}
// 判断双链表是否为空
bool Empty(DLinklist L){
if(L->next == NULL)
return true;
else
return false;
}
// 双链表的插入:在 p 结点后插入 s
bool InsertNextDNode(DNode *p, DNode *s){
if(p == NULL || s == NULL)
return false;
s->next = p->next;
if(p->next != NULL)
p->next->prior = s;
s->prior = p;
p->next = s;
}
// 双链表的删除:删除 p 结点的后继结点
bool DeleteNextDNode(DNode *p){
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;
}
// 销毁双链表
void DestoryList(DLinklist &L){
while(L->next != NULL)
DeleteNextDNode(L);
free(L);
L=NULL;
}
void testDLinkList(){
DLinklist L;
InitLinkList(L);
// ...后续代码...
}
双链表的遍历
后向遍历
while(p != NULL){
p = p->next;
}
前向遍历
while(p != NULL){
p = p->prior;
}
前向遍历(跳过头节点)
while(p->prior != NULL){
p = p->prior;
}
循环单链表
typedef struct Node{
ElemType data;
struct Node *next;
}DNode, *Linklist;
// 判断循环单链表是否为空
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;
}
// 初始化循环单链表
bool InitList(LinkList &L){
L = (LNode *)malloc(sizeof(LNode));
if(L == NULL)
return false;
L->next = L;
return true;
}
循环双链表
typedef struct DNode{
ElemType data;
struct DNode *prior, *next;
}DNode, *DLinklist;
// 判断循环双链表是否为空
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;
}
// 双链表的插入
bool InsertNextDNode(DNode *p, DNode *s){
s->next = p->next;
p->next->prior = s;
s->prior = p;
p->next = s;
}
// 初始化空的循环双链表
bool InitDLinkList(DLinkList &L){
L = (DNode *)malloc(sizeof(DNode));
if(L == NULL)
return false;
L->prior = L;
L->next = L;
return true;
}
什么是静态链表
单链表:各个节点在内存中星罗棋布、散落天涯。
静态链表:分配一整片连续的内存空间,各个结点集中安置。
静态链表:用数组的方式实现的链表。
优点:增、删操作不需要大量移动元素。
缺点:不能随机存取,只能从头节点开始依次往后查找;容量固定不变。
适用场景:
用代码定义一个静态链表
#define MaxSize 10 // 静态链表的最大长度
typedef struct{ // 静态链表结构类型的定义
ElemType data; // 存储数据元素
int next; // 下一个元素的数组下标
}SLinkList[MaxSize];
// ↑等价↓
#define MaxSize 10
struct Node{
ElemType data;
int next;
};
typedef struct Node SLinkList[MaxSize];
【举例】
#define MaxSize 10
struct Node{
int data;
int next;
};
typedef struct{
int data;
int next;
}SLinkList[MaxSize];
void testSLinkList(){
struct Node x;
printf("size X=%d\n", sizeof(x));
struct Node a[MaxSize];
printf("size A=%d\n", sizeof(a));
SLinkList b;
printf("size B=%d\n", sizeof(b));
}
// 运行结果:
size X=8
size A=80
size B=80
查找从头节点触发挨个往后遍历结点 : O ( n ) O(n) O(n)
插入位序为 i 的结点:
1. 找到一个空的结点,存入数据元素
1. 从头节点触发找到位序为 i-1 的结点
1. 修改新节点的 next
1. 修改 i-1 号结点的 next
Round1:逻辑结构
顺序表 | 都属于线性表,都是线性结构; | 链表 |
---|
Round2:存储结构
顺序表 | 链表 | |
---|---|---|
顺序存储 | 都属于线性表,都是线性结构; | 链式存储 |
优点:支持随机存取,存储密度高; | 优点:离散的小空间分配方便,改变容量方便; | |
缺点:大片连续空间分配不方便,改变容量不方便; | 缺点:不可随机存取,存储密度低; |
Round3:基本操作
复习回忆思路:创销、增删改查;
顺序表(顺序存储) 静态分配:静态数组(容量不可改变) |
链表(链式存储) 动态分配:动态数组(malloc、 free) |
|
---|---|---|
需要预分配大片连续空间,若分配空间过小,则之 后不方便扩展容量;若分配空间过大,则浪费内存 资源; | 创 | 只需分配一个头节点(也可以不要 头节点,之声明一个头指针),之 后方便扩展 |
L e n g t h = 0 Length=0 Length=0 系统自动回收空间 |
销 | 依次删除各个节点(free) 需要手动 free |
插入/删除元素要将后续元素都后移/前移; 时间复杂度 ,时间开销主要来自移动元素; 若数据元素很大,则移动的时间代价很高; |
增 删 | 插入/删除元素只需修改指针即可; 时间复杂度 ,时间开销主要 来自查找目标元素; 查找元素的时间代价更低; |
按位查找: O ( 1 ) O(1) O(1) 按值查找: O ( n ) O(n) O(n) 若表内元素有序,可在 O ( l o g 2 n ) O(log_{2}n) O(log2n)时间内找到 |
查 | 按位查找: O ( n ) O(n) O(n) 按值查找: O ( n ) O(n) O(n) |
Round4:用顺序表 or 链表
顺序表 | 链表 | |
---|---|---|
弹性(可扩容) | ||
增、删 | ||
查 |
表长难以预估、经常要增加/删除元素——链表
练习1:
#include
#include
typedef int datatype_t;
typedef struct node{
datatype_t data;
struct node *next;
}LINK;
// 创建头节点
LINK *link_creat_head(){
// 开辟空间,创建头节点
LINK *h = (LINK *)malloc(sizeof(LINK));
// 初始化头结点指针指向
h->next = h;
return h;
}
// 创建有数据的节点
LINK *link_creat_node(datatype_t data){
// 开辟空间,创建有数据的节点
LINK *node = (LINK *)malloc(sizeof(LINK));
// 给节点赋值
node->data = data;
// 初始化节点指针域
node->next = NULL;
return node;
}
// 头插法插入链表
void link_insert_head(LINK *h, datatype_t data){
// 创建新的节点
LINK *node = link_creat_node(data);
// 让新的节点的 next 指向头节点的下一节点
node->next = h->next;
// 让头节点的 next 指向新的 node 节点
h->next = node;
}
// 遍历链表打印
void link_show(LINK *h){
LINK *temp = h;
// 判断是否遍历到最后一个节点
while(h->next != temp){
// h 指针后移一个节点
h = h->next;
// 打印该节点数据
printf(" %d ", h->data);
}
putchar(10);
}
// 去头
LINK *link_cut_head(LINK *h){
LINK *temp = h;
// 判断是否遍历到最后一个节点
while(h->next != temp){
h = h->next;
}
h->next = temp->next;
free(temp);
temp = NULL;
return h->next;
}
// 遍历链表打印(去头)
void link_show2(LINK *h){
LINK *temp = h;
while(h->next != temp){
printf(" %d ", h->data);
h = h->next;
}
printf(" %d ", h->data);
putchar(10);
}
int main(int argc, const char *argv[]){
LINK *h = link_creat_head();
link_insert_head(h, 10);
link_insert_head(h, 20);
link_insert_head(h, 30);
link_insert_head(h, 40);
link_insert_head(h, 50);
link_insert_head(h, 60);
// link_show(h);
link_creat_head(h);
link_show2(h);
return 0;
}
[root@localhost c]# ./a.out
60 50 40 30 20 10
0 60 50 40 30 20 10
练习2:约瑟夫问题
#include
#include
typedef int datatype_t;
typedef struct node{
datatype_t data;
struct node *next;
}LINK;
// 创建头节点
LINK *link_creat_head(){
// 开辟空间,创建头节点
LINK *h = (LINK *)malloc(sizeof(LINK));
// 初始化头结点指针指向
h->next = h;
return h;
}
// 创建有数据的节点
LINK *link_creat_node(datatype_t data){
// 开辟空间,创建有数据的节点
LINK *node = (LINK *)malloc(sizeof(LINK));
// 给节点赋值
node->data = data;
// 初始化节点指针域
node->next = NULL;
return node;
}
// 头插法插入链表
void link_insert_head(LINK *h, datatype_t data){
// 创建新的节点
LINK *node = link_creat_node(data);
// 让新的节点的 next 指向头节点的下一节点
node->next = h->next;
// 让头节点的 next 指向新的 node 节点
h->next = node;
}
// 去头
LINK *link_cut_head(LINK *h){
LINK *temp = h;
// 判断是否遍历到最后一个节点
while(h->next != temp){
h = h->next;
}
h->next = temp->next;
free(temp);
temp = NULL;
return h->next;
}
void joseh(int n, int k, int m){
LINK *h = link_creat_head();
LINK *temp = NULL;
int i;
for(i = n; i > 0; i--){
link_insert_head(h, i);
}
h = link_creat_head(h);
if(m == 1){
for(i = 1; i < k - 1; i++){
h = h->next;
}
while(h->next != h){
temp = h->next;
printf(" %d ", temp->data);
h->next = temp->next;
free(temp);
temp = NULL;
}
printf(" %d \n", h->data);
}
else{
for(i = 1; i < k; i++){
h = h->next;
}
while(h->next != h){
for(i = 1; i < m-1; i++){
h = h->next;
}
temp = h->next;
printf(" %d ", temp->data);
h->next = temp->next;
free(temp);
temp = NULL;
}
printf(" %d \n", h->data);
}
}
int main(int argc, const char *argv[]){
joseh(8, 1, 3);
return 0;
}
[root@localhost c]# ./a.out
0
栈(Stack)是只允许在一端进行插入或删除操作的线性表。
重要术语:栈顶、栈底、空栈。
特点:后进先出(LIFO)。
顺序栈:类似顺序表;
链式栈:①头插,头删;②尾插,尾删;
栈的基本操作
InitStack(&S)
:初始化栈,构造一个空栈 S,分配内存空间。
DestroyStack(&L)
:销毁栈,销毁并释放栈 S 所占用的内存空间。
Push(&S,x)
:进栈,若栈 S 未满,则将 x 加入使之成为新的栈顶。
Pop(&S,&x)
:出栈,若栈 S 非空,则弹出栈顶元素,并用 x 返回。
GetTop(S,&x)
:读栈顶元素,若栈 S 非空,则用 x 返回栈顶元素。(查操作,栈的适用场景中大多只访问栈顶元素)。
其他常用操作
StackEmpty(S)
:判断一个栈 S 是否为空,若 S 为空,则返回 true,否则返回 false。
栈的常考题型
n 个不同元素进栈,出栈元素不同排列的个数为 1 n + 1 C 2 n n \frac{1}{n+1}C_{2n}^{n} n+11C2nn。该公式称为卡特兰(Catalan)数,可采用数学归纳法证明(不要求掌握)。
顺序栈的定义
#define MaxSize 10 // 定义栈中元素的最大个数
typedef struct{
ElemType data[MaxSize]; // 静态数组存放栈中元素
int top; // 栈顶指针
}SqStack;
初始化操作
// 初始化栈
void InitStack(SqStack &S){
S.top = -1; // 初始化栈顶指针
}
void testStack(){
SqStack S; // 声明一个顺序栈(分配空间)
InitStack(S);
// ...后续操作...
}
判断空栈
bool StackEmpty(SqStack S){
if(S.top == -1)
return true; // 栈空
else
return false; // 不空
}
判断栈满
注意:需注意栈顶指针的指向,是栈顶元素还是栈顶元素后一个位置。
bool StackEmpty1(SqStack S){
if(S.top == MaxSize - 1) // 栈满的条件:top == MaxSize - 1
return true; // 栈满
else
return false; // 不满
}
// 另一种方式
bool StackEmpty2(SqStack S){
if(S.top == MaxSize) // 栈满的条件:top == MaxSize
return true; // 栈满
else
return false; // 不满
}
进栈操作
bool Push(SqStack &S, ElemType x){
if(S.top == MaxSize - 1) // 栈满,报错
return false;
S.top = S.top + 1; // 指针先加1
S.data[S.top] = x; // 新元素入栈
// S.data[++S.top] = x; // 此方法也可以实现上述两行相同的操作
return true;
}
出栈操作
bool Pop(SqStack &S, ElemType &x){
if(S.top == -1) // 栈空,报错
return false;
x = S.data[S.top]; // 栈顶元素先出栈
S.top = S.top - 1; // 指针再减1
// 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 InitStack(ShStack &S){
S.top0 = -1;
S.top1 = MaxSize;
}
// 栈满条件:top0 + 1 == top1
链栈的定义
typedef struct Linknode{
ElemType data; // 数据域
struct Linknode *next; // 指针域
}*LiStack; // 栈类型定义
通过顺序表实现栈
#include
#include
#define N 32
typedef int datatype_t;
typedef struct seqlist{
datatype_t data[N];
int top;
}seqlist_t;
// 创建空栈
seqlist_t *seqlist_creat(){
seqlist_t *sq = (seqlist_t *)malloc(sizeof(seqlist_t));
sq->top = -1;
return sq;
}
// 判断栈满
int seqlist_full(seqlist_t *sq){
return sq->top == N - 1 ? 1 : 0;
}
// 判断栈空
int seqlist_empty(seqlist_t *sq){
return sq->top == -1 ? 1 : 0;
}
// 入栈
int seqlist_push(seqlist_t *sq, datatype_t data){
if(seqlist_full(sq)){
printf("栈满\n");
return -1;
}
sq->top++;
sq->data[sq->top] = data;
return 0;
}
// 出栈
datatype_t seqlist_outstack(seqlist_t *sq){
if(seqlist_empty(sq)){
printf("栈空\n");
return (datatype_t)-1;
}
datatype_t temp = sq->data[sq->top];
sq->top--;
return temp;
}
int main(int argc, const char *argv[]){
seqlist_t *sq = seqlist_creat();
seqlist_push(sq, 10);
seqlist_push(sq, 20);
seqlist_push(sq, 30);
seqlist_push(sq, 40);
seqlist_push(sq, 50);
seqlist_push(sq, 60);
printf(" %d ", seqlist_outstack(sq));
printf(" %d ", seqlist_outstack(sq));
printf(" %d ", seqlist_outstack(sq));
printf(" %d ", seqlist_outstack(sq));
printf(" %d ", seqlist_outstack(sq));
printf(" %d ", seqlist_outstack(sq));
return 0;
}
[root@localhost c]# ./a.out
60 50 40 30 20 10
通过链表实现栈(先进后出)
#include
#include
typedef int datatype_t;
typedef struct node{
datatype_t data;
struct node *next;
}link_stack;
// 创建头节点
link_stack *link_create_head(){
link_stack *h = (link_stack *)malloc(sizeof(link_stack));
h->next = NULL;
return h;
}
// 创建有数据的节点
link_stack *link_create_node(datatype_t data){
link_stack *node = (link_stack *)malloc(sizeof(link_stack));
node->data = data;
node->next = NULL;
return node;
}
// 入栈(头插)
void link_push_stack(link_stack *h, datatype_t data){
link_stack *node = link_create_node(data);
node->next = h->next;
h->next = node;
}
// 出栈(头删)
datatype_t link_out_stack(link_stack *h){
link_stack *temp = h->next;
datatype_t val = h->next->data;
h->next = temp->next;
free(temp);
// 防止野指针
temp = NULL;
return val;
}
// 入栈(尾插法)
void link_insert_tail(link_stack *h, datatype_t data){
link_stack *node = link_create_node(data);
while(h->next != NULL){
h = h->next;
}
h->next = node;
}
// 出栈(尾删法)
datatype_t link_tail_del(link_stack *h){
datatype_t val;
while(h->next->next != NULL){
h = h->next;
}
val = h->next->data;
free(h->next);
h->next = NULL;
return val;
}
int main(int argc, const char *argv[]){
link_stack *h = link_create_head();
link_push_stack(h, 10);
link_push_stack(h, 20);
link_push_stack(h, 30);
link_push_stack(h, 40);
link_push_stack(h, 50);
link_push_stack(h, 60);
printf(" %d ", link_out_stack(h));
printf(" %d ", link_out_stack(h));
printf(" %d ", link_out_stack(h));
printf(" %d ", link_out_stack(h));
printf(" %d ", link_out_stack(h));
printf(" %d ", link_out_stack(h));
putchar(10);
return 0;
}
[root@localhost c]# ./a.out
60 50 40 30 20 10
队列(Queue)是只允许在一段进行插入,在另一端删除的线性表。
重要术语:队头、队尾、空队列。
队列的特点:先进先出(FIFO)。
顺序队列(循环队列):利用两个下标值front
,rear
。
链式队列:①头插,尾删;②尾插,头删;③通过两个指针操作链表front
,rear
分别指向链表的头和尾。通过(尾删,头插)。
队列的基本操作
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,否则返回 false。
队列的顺序存储定义
#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){
return Q.rear == Q.front ? true : false;
}
判断队列是否为满
方案一:队列已满的条件为,队尾指针的再下一次位置是队头,即 (Q.rear + 1) % MaxSize == Q.front
,因此牺牲一片空间用以判断队满还是队空。
方案二:初始化时 rear = front = 0; size = 0
,插入成功 size++
,删除成功size--
。
#define MaxSize 10
typedef struct{
ElemType data[MaxSize];
int front, rear;
int size; // 用于指示队列当前长度
}SqQueue;
方案三:初始化时 rear = front = 0; tag = 0
,每次删除操作成功时,都令 tag=0
;每次插入操作成功时,都令tag=1
;只有删除操作,才可能导致队空,只有插入操作,才可能导致队满;队空条件为front == rear && tag = 0
,队满条件为front == rear && tag == 1
。
#define MaxSize 10
typedef struct{
ElemType data[MaxSize];
int front, rear;
int tag; // 最近进行的是删除/插入
}SqQueue;
计算队列元素个数
(rear + MaxSize - front) % MaxSize
入队操作
bool EnQueue(SqQueue &Q, ElemType x){
if((Q.rear + 1) % MaxSize == Q.front) // 判断队满
return false; // 队满则报错
Q.data[Q.rear] = x; // 将 x 插入队尾
Q.rear = (Q.rear + 1) % MaxSize; // 队尾指针后移,+1取模
return true;
}
出队操作
删除一个队头元素,并用 x 返回。
bool DeQueue(SqQueue &Q, ElemType &x){
if(QueueEmpty(Q))
return false;
x = Q.data[Q.front];
Q.front = (Q.front + 1) % MaxSize;
return true;
}
查操作
bool GetHead(SqQueue Q, ElemType &x){
if(QueueEmpty(Q))
return false;
x = Q.data[Q.front];
return true;
}
其他出题方法
需要注意初始时,头指针和尾指针的位置。
队列的链式存储定义
typedef struct LinkNode{ // 链式队列节点
ElemType data;
struct LinkNode *next;
}LinkNode;
typedef struct{ // 链式队列
LinkNode *front *rear; // 队列的队头和队尾指针
}LinkQueue;
判断队列是否为空
bool IsEmpty(LinkNode Q){
// return Q.front->next == NULL ? true : false; // 带头节点
return Q.front == Q.rear ? true :false; // 带头节点+不带头节点
}
初始化(带头结点)
void InitQueue(LinkQueue &Q){
Q.front = Q.rear = (LinkNode *)malloc(sizeof(LinkNode));
Q.front->next = NULL;
}
初始化(不带头节点)
void InitQueue(LinkQueue &Q){
Q.front = Q.rear = NULL;
}
入队(带头节点)
void EnQueue(LinkNode &Q, ElemType x){
LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));
s->data = x;
s->next = NULL;
if(Q.front == NULL){ // 在空队列中插入第一个元素
Q.front = Q.rear = s; // 修改队头队尾指针
}else{
Q.rear->next = s; // 新结点插入到rear之后
Q.rear = s; // 修改表尾指针
}
}
出队(带头结点)
bool DeQueue(LinkQueue &Q, ElemType &x){
if(Q.front == Q.rear)
return false;
LinkNode *p = Q.front->next;
x = p->data; // 用变量 x 返回队头元素
Q.front->next = p->next; // 修改头节点的 next 指针
if(Q.rear == p) // 此次是最后一个节点出队
Q.rear = Q.front; // 修改 rear 指针
free(p); // 释放节点空间
return true;
}
出队(不带头节点)
bool DeQueue(LinkQueue &Q, ElemType &x){
if(Q.front == NULL)
return false;
LinkNode *p = Q.front; // p指向此次出队的节点
x = p->data; // 用变量 x 返回队头元素
Q.front = p->next; // 修改 front 指针
if(Q.rear == p){ // 此次是最后一个节点出队
Q.front = NULL;
Q.rear = NULL;
}
free(p);
return true;
}
队列满的条件
顺序存储 —— 预分配的空间耗尽时队满。
链式存储 —— 一般不会队满,除非内存不足。
双端队列是只允许从两端插入、两端删除的线性表。
输入受限的双端队列是只允许从一段插入、两端删除的线性表。
输出受限的双端队列是只允许从两端插入、一端删除的线性表。
考点:判断输出序列合法性
若数据元素输入序列为1,2,3,4,则哪些输出序列是合法的,哪些是不合法的?
共 A 4 4 = 4 ! = 24 A_{4}^{4}=4!=24 A44=4!=24 种可能情况,通过卡特兰数 1 n + 1 C 2 n n = 1 4 + 1 C 8 4 = 14 \frac{1}{n+1}C_{2n}^{n}=\frac{1}{4+1}C_{8}^{4}=14 n+11C2nn=4+11C84=14计算得出有14中合法情况。
栈:
1,2,3,4 | √ | 2,1,3,4 | √ | 3,1,2,4 | × | 4,1,2,3 | × |
---|---|---|---|---|---|---|---|
1,2,4,3 | √ | 2,1,4,3 | √ | 3,1,4,2 | × | 4,1,3,2 | × |
1,3,2,4 | √ | 2,3,1,4 | √ | 3,2,1,4 | √ | 4,2,1,3 | × |
1,3,4,2 | √ | 2,3,4,1, | √ | 3,2,4,1 | √ | 4,2,3,1 | × |
1,4,2,3 | × | 2,4,1,3 | × | 3,4,1,2 | × | 4,3,1,2 | × |
1,4,3,2 | √ | 2,4,3,1 | √ | 3,4,2,1 | √ | 4,3,2,1 | √ |
oop_link_create_node(data);
//假设链表中有数据 ,保存链表中第一个有数据的节点
DLL *hnext=dl->next;
//node根头节点的关系
dl->next=node;
node->prior=dl;
//node节点跟头节点的下一个节点的关系
node->next=hnext;
hnext->prior=node;
}
void double_loop_link_show(DLL *dl)
{
DLL *temp=dl;
#if 0
//从前往后遍历
while(dl->next!=temp)
{
printf(" %d ",dl->next->data);
dl=dl->next;
}
#endif
//从后往前遍历
while(dl->prior!=temp)
{
printf(" %d ",dl->prior->data);
dl=dl->prior;
}
}
int double_loop_link_empty(DLL *dl)
{
return dl->next==dl ? 1:0;
}
//删除第一个节点
datatype_t double_loop_link_del(DLL *dl)
{
if(double_loop_link_empty(dl))
{
printf("链表空\n");
return (datatype_t)-1;
}
DLL *pnode,*temp;
datatype_t val;
//temp保存将要删除的节点地址 pnode保存删除节点的下一个节点地址
temp=dl->next;
val =temp->data;
pnode=temp->next;
//释放删除节点的空间
free(temp);
temp=NULL;
//删除节点的下一节点与头节点的关系
dl->next=pnode;
pnode->prior=dl;
return val;
}
int main(int argc, const char *argv[])
{
DLL *dl=double_loop_link_create();
double_loop_link_insert(dl,10);
double_loop_link_insert(dl,20);
double_loop_link_insert(dl,30);
double_loop_link_insert(dl,40);
double_loop_link_insert(dl,50);
double_loop_link_insert(dl,60);
double_loop_link_show(dl);
putchar(10);
double_loop_link_del(dl);
double_loop_link_show(dl);
putchar(10);
return 0;
}
顺序表实现循环队列
#include //建立头文件
#include
#define N 32
typedef int datatype_t;
typedef struct seq //创建顺序表(结构体)
{
datatype_t data[N];//存储数据
int front;//标记队列的前放数据位置
int rear;//标记队列的最后一个数据
} loop_seq;
loop_seq *loop_creat() //创建空队列使数据入栈
{
loop_seq *sq=(loop_seq *)malloc(sizeof(loop_seq));
sq->front=sq->rear=0;
}
datatype_t loop_full(loop_seq *sq) //判断队列是否满(封装函数同时声明)
{
return (sq->rear+2)%N==sq->front?1:0;
}
void loop_in(loop_seq *sq,datatype_t data) //入队列的同时判断队列是否满
{
if( loop_full(sq))
{
printf("队满\n");
return ;
}
sq->data[sq->rear]=data;
sq->rear=(sq->rear+1)%N;
}
int loop_empty(loop_seq *sq) //判断队列是否为空(封装函数同时声明)
{
return sq->front==sq->rear?1:0;
}
datatype_t loop_out(loop_seq *sq) //出队列的同时判断队列是否为空
{
if(loop_empty(sq))
{
printf("队空\n");
return (datatype_t)-1;
}
datatype_t val=sq->data[sq->front];
sq->front=(sq->front+1)%N;
}
int main(int argc, const char *argv[]) //主函数调用函数
{
loop_seq *sq=loop_creat();
loop_in(sq,12);
loop_in(sq,22);
loop_in(sq,32);
loop_in(sq,42);
loop_in(sq,52);
loop_out(sq);
loop_out(sq);
loop_out(sq);
loop_out(sq);
loop_out(sq);
return 0;
}
链表实现队列
#include
#include //malloc
typedef int datatype_t;
typedef struct node
{
datatype_t data;
struct node *next;
} linknode_t;
typedef struct linkqueue{
linknode_t *front; // 指向链表的头指针
linknode_t *rear; // 指向链表的最后一个节点的指针
}linkqueue_t;
linkqueue_t *linkqueue_create() //创建空队列
{
linknode_t *h=(linknode_t *)malloc(sizeof(linknode_t));
h->next=NULL;
linkqueue_t *lq=(linkqueue_t *)malloc(sizeof(linkqueue_t));
lq->front=lq->rear=h;
return lq;
}
linknode_t *linknode_create(datatype_t data) //创建节点
{
linknode_t *node=(linknode_t *)malloc(sizeof(linknode_t));
node->next=NULL;
node->data=data;
return node;
}
void linkqueue_in(linkqueue_t *lq,datatype_t data) //入队
{
linknode_t *node=linknode_create(data);
lq->rear->next=node; //将新创建的node节点插入到链表最后
lq->rear=lq->rear->next; //将rear指针指向最后一个节点
}
int linkqueue_empty(linkqueue_t *lq) //判断队空
{
return lq->front==lq->rear ? 1:0;
}
datatype_t linkqueue_out(linkqueue_t *lq) //出队
{
if(linkqueue_empty(lq))
{
printf("队空\n");
return (datatype_t)-1;
}
linknode_t *temp=NULL;
datatype_t val;
temp=lq->front->next;
val=temp->data;
lq->front->next=temp->next;
free(temp);
temp=NULL;
if(lq->front->next==NULL)
{
lq->rear=lq->front;
}
return val;
}
int main(int argc, const char *argv[])
{
linkqueue_t *lq=linkqueue_create();
linkqueue_in(lq,10);
linkqueue_in(lq,20);
linkqueue_in(lq,30);
linkqueue_in(lq,40);
linkqueue_in(lq,50);
linkqueue_in(lq,60);
printf(" %d ",linkqueue_out(lq));
printf(" %d ",linkqueue_out(lq));
printf(" %d ",linkqueue_out(lq));
printf(" %d ",linkqueue_out(lq));
printf(" %d ",linkqueue_out(lq));
printf(" %d ",linkqueue_out(lq));
printf(" %d ",linkqueue_out(lq));
return 0;
}
括号匹配问题:最后出现的左括号最先被匹配(LIFO)
算法演示:遇到左括号就入栈,遇到右括号,就"消耗"一个左括号。
#define MaxSize 10
typedef struct{
char data[MaxSize];
int top;
}SqStack;
// 初始化栈
void InitStack(SqStack &S);
// 判断栈是否为空
bool StackEmpty(SqStack &S);
// 新元素入栈
bool Push(SqStack &S, char x);
// 栈顶元素出栈,用 x 返回
bool Pop(SqStack &S, char &x);
bool bracketCheck(char str[], int length){
SqStack S;
InitStack(S); // 初始化一个栈
for(int i = 0; i < length; i++){
if(str[i] == '(' || str[i] == '[' || str[i] == '{'){
Push(S, str[i]); // 扫描到左括号,入栈
}else{
if(StackEmpty(S)) // 扫描到右括号,且当前栈空
return false; // 匹配失败
char topElem;
Pop(S, topElem); // 栈顶元素出栈
if(str[i] == ')' && topElem != '(')
return false;
if(str[i] == ']' && topElem != '[')
return false;
if(str[i] == '}' && topElem != '{')
return false;
}
}
return StackEmpty(S); // 检索完全部括号后,栈空说明匹配成功
}
表达式 | 中缀表达式 | 后缀表达式 | 前缀表达式 |
---|---|---|---|
示例 | a + b | a b + | + a b |
示例 | a + b - c | a b c - + | - + a b c |
中缀表达式转后缀表达式
中缀转后缀的手算方法:
后缀表达式的手算方法:从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应运算,合体为一个操作数。注意:两个操作数的左右顺序。
后缀表达式的计算(机算):用栈实现,适用于基于栈的编程语言(stack-oriented programming language),如:Forth、PostScript
后缀表达式的计算(机算)
初始化一个栈,用于保存暂时还不能确定运算顺序的运算符。从左到右c护理各个元素,知道末尾,可能遇到三种情况:
按上述方法处理完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式。
中缀表达式转前缀表达式
中缀转前缀的手算方法:
前缀表达式的计算:用栈实现
中缀表达式的计算(用栈计算)
用栈实现中缀表达式的计算:中缀转后缀+后缀表达式求值,两个算法的结合
初始化两个栈,操作数栈和运算符栈,若扫描到操作数,压入操作数栈,若扫描到运算符或界限符,则按照"中缀转后缀"相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应运算,运算结果再压回操作数栈)
函数调用的特点:最后被调用的函数最先执行结束(LIFO)
函数调用时,需要用一个"函数调用栈"存储:
适合用"递归"算法解决:可以把原始问题转换为属性相同,但规模较小的问题。
递归调用时,函数调用栈可称为"递归工作栈";每进入一层递归,就将递归调用所需信息压入栈顶,每退出一层递归,就从栈顶弹出相应信息;
缺点:效率低;太多层递归可能会导致栈溢出;可能包含很多重复计算。
解决方法:可以自定义栈将递归算法改造成非递归算法。
Eg1:
#include
int factorial(int n){
if(n == 0 || n == 1)
return 1;
else
return n * factorial(n - 1);
}
int main(){
int x = factorial(10);
printf("奥里给 %d !\n", x);
return 0;
}
[root@localhost c]# ./a.out
奥里给 3628800 !
Eg2:
#include
int Fib(int n){
if(n == 0)
return 0;
else if(n == 1)
return 1;
else
return Fib(n - 1) + Fib(n - 2);
}
int main(){
int x = Fib(4);
printf("奥里给 %d !\n", x);
return 0;
}
[root@localhost c]# ./a.out
奥里给 3 !
树的层次遍历
注:在"树"章节会详细学习。
按层次遍历处理的时候都需要把对应的左右孩子放到队列队尾,而每次遍历处理的是队头元素。
图的广度优先遍历
注:在"图"章节会详细学习。
队列在操作系统中的应用
多个进程争抢着使用有限的系统资源时,FCFS(First Come First Service,先来先服务)是一种常用策略。
一维数组的存储结构
ElemType a[10]
:ElemType 型一维数组(C语言定义一维数组)
内存 | a[0] | a[1] | a[2] | a[3] | a[4] | a[5] | a[6] | a[7] | a[8] | a[9] | |
---|---|---|---|---|---|---|---|---|---|---|---|
起始地址:(LOC)↑ |
各数组元素大小相同,且物理上连续存放。
数组元素 a[i] 的存放地址 = LOC + i * sizeof(ElemType)
( 0 ≤ i < 10 0 \leq i < 10 0≤i<10)
注:除非题目特别说明,否则数组下标默认从 0 开始。
二维数组的存储结构
ElemType b[2][4]
:2行4列的二维数组(C语言定义二维数组)
逻辑视角:
b[0][0] | b[0][1] | b[0][2] | b[0][3] |
---|---|---|---|
b[1][0] | b[1][1] | b[1][2] | b[1][3] |
内存:
行优先存储:
内存 | b[0][0] | b[0][1] | b[0][2] | b[0][3] | b[1][0] | b[1][1] | b[1][2] | b[1][3] | |
---|---|---|---|---|---|---|---|---|---|
起始地址:LOC ↑ |
M行N列的二维数组 b[M][N]
中,若按行优先存储,则 b[i][j] 的存储地址 = LOC + (i * N + j) * sizeof(ElemType)
列优先存储:
内存 | b[0] [0] | b[1] [0] | b[0] [1] | b[1] [1] | b[0] [2] | b[1] [2] | b[0] [3] | b[1] [3] | |
---|---|---|---|---|---|---|---|---|---|
起始地址:LOC ↑ |
M行N列的二维数组 b[M][N]
中,若按列优先存储,则 b[i][j] 的存储地址 = LOC + (j * M + i) * sizeof(ElemType)
普通矩阵的存储
( a 1 , 1 a 1 , 2 a 1 , 3 ⋯ a 1 , n − 1 a 1 , n a 2 , 1 a 2 , 2 a 2 , 3 ⋯ a 2 , n − 1 a 2 , n a 3 , 1 a 3 , 2 a 3 , 3 ⋯ a 3 , n − 1 a 3 , n ⋮ ⋮ ⋮ ⋮ ⋮ a m , 1 a m , 2 a m , 3 ⋯ a m , n − 1 a m , n ) \begin{pmatrix}a_{1,1} & a_{1,2} & a_{1,3} & \cdots & a_{1,n-1} & a_{1,n} \\ a_{2,1} & a_{2,2} & a_{2,3} & \cdots & a_{2,n-1} & a_{2,n} \\ a_{3,1} & a_{3,2} & a_{3,3} & \cdots & a_{3,n-1} & a_{3,n} \\ \vdots & \vdots & \vdots & & \vdots & \vdots \\ a_{m,1} & a_{m,2} & a_{m,3} & \cdots & a_{m,n-1} & a_{m,n}\end{pmatrix} a1,1a2,1a3,1⋮am,1a1,2a2,2a3,2⋮am,2a1,3a2,3a3,3⋮am,3⋯⋯⋯⋯a1,n−1a2,n−1a3,n−1⋮am,n−1a1,na2,na3,n⋮am,n 可用二维数组存储
注意:描述矩阵元素时,行、列号通常从 1 开始;而描述数组时通常下标从 0 开始(具体看题目给的条件,注意审题!)
对称矩阵的压缩存储
( a 1 , 1 a 1 , 2 a 1 , 3 ⋯ a 1 , n − 1 a 1 , n a 2 , 1 a 2 , 2 a 2 , 3 ⋯ a 2 , n − 1 a 2 , n a 3 , 1 a 3 , 2 a 3 , 3 ⋯ a 3 , n − 1 a 3 , n ⋮ ⋮ ⋮ ⋱ ⋮ ⋮ a n − 1 , 1 a n − 1 , 2 a n − 1 , 3 ⋯ a n − 1 , n − 1 a n − 1 , n a n , 1 a n , 2 a n , 3 ⋯ a n , n − 1 a n , n ) \begin{pmatrix} a_{1,1} & a_{1,2} & a_{1,3} & \cdots & a_{1,n-1} & a_{1,n} \\ a_{2,1} & a_{2,2} & a_{2,3} & \cdots & a_{2,n-1} & a_{2,n} \\ a_{3,1} & a_{3,2} & a_{3,3} & \cdots & a_{3,n-1} & a_{3,n} \\ \vdots & \vdots & \vdots & \ddots & \vdots & \vdots \\ a_{n-1,1} & a_{n-1,2} & a_{n-1,3} & \cdots & a_{n-1,n-1} & a_{n-1,n} \\ a_{n,1} & a_{n,2} & a_{n,3} & \cdots & a_{n,n-1} & a_{n,n} \end{pmatrix} a1,1a2,1a3,1⋮an−1,1an,1a1,2a2,2a3,2⋮an−1,2an,2a1,3a2,3a3,3⋮an−1,3an,3⋯⋯⋯⋱⋯⋯a1,n−1a2,n−1a3,n−1⋮an−1,n−1an,n−1a1,na2,na3,n⋮an−1,nan,n
若 n 阶方阵中任意一个元素 a i , j a_{i,j} ai,j 都有 a i , j = a j , i a_{i,j} = a_{j,i} ai,j=aj,i,则该矩阵为对称矩阵。
策略:只存储主对角线+下三角区,按行优先原则将各元素存入一维数组中。
B [ 0 ] B[0] B[0] | B [ 1 ] B[1] B[1] | B [ 2 ] B[2] B[2] | B [ 3 ] B[3] B[3] | ⋯ \cdots ⋯ | B [ n ( n + 1 ) 2 − 1 ] B[\frac{n(n+1)}{2}-1] B[2n(n+1)−1] | |
---|---|---|---|---|---|---|
a 1 , 1 a_{1,1} a1,1 | a 2 , 1 a_{2,1} a2,1 | a 2 , 2 a_{2,2} a2,2 | a 3 , 1 a_{3,1} a3,1 | ⋯ \cdots ⋯ | a n , n − 1 a_{n,n-1} an,n−1 | a n , n a_{n,n} an,n |
思考:
数组大小应该为多少?
( 1 + n ) ∗ n 2 \frac{(1+n)*n}{2} 2(1+n)∗n
站在程序员的角度,对称矩阵压缩存储后怎样才能方便使用?
可以实现一个"映射"函数,矩阵下标——>数组下标, a i , j ( i ≥ j ) a_{i,j}(i \geq j) ai,j(i≥j)——>B[K]
Key:按行优先原则, a i , j a_{i,j} ai,j 是第几个元素?
[ 1 + 2 + ⋯ + ( i − 1 ) + j ] [1+2+ \cdots +(i-1)+j] [1+2+⋯+(i−1)+j]——>第 i ( i − 1 ) 2 + j \frac{i(i-1)}{2}+j 2i(i−1)+j 个元素, k = i ( i − 1 ) 2 + j − 1 k=\frac{i(i-1)}{2}+j-1 k=2i(i−1)+j−1。
策略:只存储主对角线+下三角区,按列优先原则将各元素存入一维数组中。
出题方法:
三角矩阵的压缩存储
( a 1 , 1 c c ⋯ c c a 2 , 1 a 2 , 2 c ⋯ c c a 3 , 1 a 3 , 2 a 3 , 3 ⋯ c c ⋮ ⋮ ⋮ ⋱ ⋮ ⋮ a n − 1 , 1 a n − 1 , 2 a n − 1 , 3 ⋯ a n − 1 , n − 1 c a n , 1 a n , 2 a n , 3 ⋯ a n , n − 1 a n , n ) \begin{pmatrix} a_{1,1} & c & c & \cdots & c & c \\ a_{2,1} & a_{2,2} & c & \cdots & c & c \\ a_{3,1} & a_{3,2} & a_{3,3} & \cdots & c & c \\ \vdots & \vdots & \vdots & \ddots & \vdots & \vdots \\ a_{n-1,1} & a_{n-1,2} & a_{n-1,3} & \cdots & a_{n-1,n-1} & c \\ a_{n,1} & a_{n,2} & a_{n,3} & \cdots & a_{n,n-1} & a_{n,n} \end{pmatrix} a1,1a2,1a3,1⋮an−1,1an,1ca2,2a3,2⋮an−1,2an,2cca3,3⋮an−1,3an,3⋯⋯⋯⋱⋯⋯ccc⋮an−1,n−1an,n−1ccc⋮can,n 下三角矩阵:除了主对角线和下三角区,其余的元素都相同。
压缩存储策略:按行优先原则将主对角线和下三角区元素存入一维数组中,并在最后一个位置存储常量 c。
B [ 0 ] B[0] B[0] | B [ 1 ] B[1] B[1] | B [ 2 ] B[2] B[2] | B [ 3 ] B[3] B[3] | ⋯ \cdots ⋯ | B [ n ( n + 1 ) 2 − 1 ] B[\frac{n(n+1)}{2}-1] B[2n(n+1)−1] | B [ n ( n + 1 ) 2 ] B[\frac{n(n+1)}{2}] B[2n(n+1)] |
---|---|---|---|---|---|---|
a 1 , 1 a_{1,1} a1,1 | a 2 , 1 a_{2,1} a2,1 | a 2 , 2 a_{2,2} a2,2 | a 3 , 1 a_{3,1} a3,1 | ⋯ \cdots ⋯ | a n , n a_{n,n} an,n | c |
Key:按行优先的原则, a i , j a_{i,j} ai,j 是第几个元素?
k = { i ( i − 1 ) 2 + j − 1 i ≥ j ( 下三角区和主对角线元素 ) n ( n + 1 ) 2 i < j ( 上三角区元素 ) k=\begin{cases} \frac{i(i-1)}{2}+j-1 & i \geq j (下三角区和主对角线元素)\\ \frac{n(n+1)}{2} & i < j (上三角区元素) \end{cases} k={2i(i−1)+j−12n(n+1)i≥j(下三角区和主对角线元素)i<j(上三角区元素)
( a 1 , 1 a 1 , 2 a 1 , 3 ⋯ a 1 , n − 1 a 1 , n c a 2 , 2 a 2 , 3 ⋯ a 2 , n − 1 a 2 , n c c a 3 , 3 ⋯ a 3 , n − 1 a 3 , n ⋮ ⋮ ⋮ ⋱ ⋮ ⋮ c c c ⋯ a n − 1 , n − 1 a n − 1 , n c c c ⋯ c a n , n ) \begin{pmatrix} a_{1,1} & a_{1,2} & a_{1,3} & \cdots & a_{1,n-1} & a_{1,n} \\ c & a_{2,2} & a_{2,3} & \cdots & a_{2,n-1} & a_{2,n} \\ c & c & a_{3,3} & \cdots & a_{3,n-1} & a_{3,n} \\ \vdots & \vdots & \vdots & \ddots & \vdots & \vdots \\ c & c & c & \cdots & a_{n-1,n-1} & a_{n-1,n} \\ c & c & c & \cdots & c & a_{n,n} \end{pmatrix} a1,1cc⋮cca1,2a2,2c⋮cca1,3a2,3a3,3⋮cc⋯⋯⋯⋱⋯⋯a1,n−1a2,n−1a3,n−1⋮an−1,n−1ca1,na2,na3,n⋮an−1,nan,n 上三角矩阵:除了主对角线和上三角区,其余的元素都相同。
Key:按行优先的原则, a i , j a_{i,j} ai,j 是第几个元素?
k = { ( i − 1 ) ( 2 n − i + 2 ) 2 + ( j − i ) i ≤ j ( 上三角区和主对角线元素 ) n ( n + 1 ) 2 i < j ( 下三角区元素 ) k=\begin{cases} \frac{(i-1)(2n-i+2)}{2}+(j-i) & i \leq j (上三角区和主对角线元素)\\ \frac{n(n+1)}{2} & i < j (下三角区元素) \end{cases} k={2(i−1)(2n−i+2)+(j−i)2n(n+1)i≤j(上三角区和主对角线元素)i<j(下三角区元素)
三对角矩阵的压缩存储
( a 1 , 1 a 1 , 2 0 ⋯ 0 0 a 2 , 1 a 2 , 2 a 2 , 3 ⋯ 0 0 0 a 3 , 2 a 3 , 3 ⋯ 0 0 ⋮ ⋮ ⋮ ⋱ ⋮ ⋮ 0 0 0 ⋯ a n − 1 , n − 1 a n − 1 , n 0 0 0 ⋯ a n , n − 1 a n , n ) \begin{pmatrix} a_{1,1} & a_{1,2} & 0 & \cdots & 0 & 0 \\ a_{2,1} & a_{2,2} & a_{2,3} & \cdots & 0 & 0 \\ 0 & a_{3,2} & a_{3,3} & \cdots & 0 & 0 \\ \vdots & \vdots & \vdots & \ddots & \vdots & \vdots \\ 0 & 0 & 0 & \cdots & a_{n-1,n-1} & a_{n-1,n} \\ 0 & 0 & 0 & \cdots & a_{n,n-1} & a_{n,n} \end{pmatrix} a1,1a2,10⋮00a1,2a2,2a3,2⋮000a2,3a3,3⋮00⋯⋯⋯⋱⋯⋯000⋮an−1,n−1an,n−1000⋮an−1,nan,n
三对角矩阵,又称带状矩阵:当 ∣ i − j ∣ > 1 |i-j| > 1 ∣i−j∣>1 时,有 a i , j = 0 ( 1 ≤ i , j ≤ n ) a_{i,j} = 0(1 \leq i, j \leq n) ai,j=0(1≤i,j≤n)
压缩存储策略:按行优先(或列优先)原则,只存储带状部分;一维数组长度为 3 n − 2 3n-2 3n−2
B [ 0 ] B[0] B[0] | B [ 1 ] B[1] B[1] | B [ 2 ] B[2] B[2] | B [ 3 ] B[3] B[3] | ⋯ \cdots ⋯ | B [ 3 n − 3 ] B[3n-3] B[3n−3] | |
---|---|---|---|---|---|---|
a 1 , 1 a_{1,1} a1,1 | a 1 , 2 a_{1,2} a1,2 | a 2 , 1 a_{2,1} a2,1 | a 2 , 2 a_{2,2} a2,2 | ⋯ \cdots ⋯ | a n , n − 1 a_{n,n-1} an,n−1 | a n , n a_{n,n} an,n |
Key:按行优先的原则, a i , j a_{i,j} ai,j 是第几个元素?
稀疏矩阵的压缩存储
( 0 0 4 0 0 5 0 3 0 9 0 0 0 0 0 0 7 0 0 2 0 0 0 0 0 0 0 0 0 0 ) \begin{pmatrix} 0 & 0 & 4 & 0 & 0 & 5 \\ 0 & 3 & 0 & 9 & 0 & 0 \\ 0 & 0 & 0 & 0 & 7 & 0 \\ 0 & 2 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 \end{pmatrix} 000000302040000090000070050000 稀疏矩阵:非零元素远远少于矩阵元素的个数。
压缩存储策略:
顺序存储 —— 三元组<行,列,值>
i(行) | j(列) | v(值) |
---|---|---|
1 | 3 | 4 |
1 | 6 | 5 |
2 | 2 | 3 |
2 | 4 | 9 |
3 | 5 | 7 |
4 | 2 | 2 |
链式存储 —— 十字链表法
定义向下域 down,指向第 j 列的第一个元素;
定义向右域 right,指向第 i 行的第一个元素;
非零数据节点说明:
行 | 列 | 值 |
---|---|---|
指向同列的 | 下一个元素 | 指向同行的下一个元素 |
串,即字符串(String)是由零个或多个字符组成的有限序列。一般记为 S=‘ a 1 a 2 ⋯ a n a_{1}a_{2} \cdots a_{n} a1a2⋯an’ ( n ≥ 0 n \geq 0 n≥0),其中,S是串名,单引号括起来的字符序列是串的值; a i a_{i} ai 可以是字母、数字或其他字符;串中字符的个数 n 称为串的长度。n = 0 时的串称为空串(用 ϕ \phi ϕ 表示)。例:S = “HelloWorld!”;T=‘iPhone 11 Pro Max?’;注:有的地方用双引号(如Java、C),有的地方用单引号(如Python)
子串:串中任意个连续的字符组成的子序列。例:‘iPhone’,‘Pro M’ 是串 T 的字串。
主串:包含字串的串。例:T 是字串 ‘iPhone’ 的主串。
字符在主串中的位置:字符在串中的序号。例:‘1’ 在 T 中的位置是 8 (第一次出现)(注意:位置从 1 开始)。
子串在主串中的位置:字串的第一个字符在主串中的位置。例:‘11 Pro’ 在 T 中的位置为 8.
空串 V.S 空格串:
串是一种特殊的线性表,数据元素之间呈线性关系。
串的数据对象限定为字符集(如中文字符、英文字符、数字字符、标点字符等)。
串的基本操作,如增删改查等通常以子串为操作对象。
串的基本操作
假设有串 T = ""
,S = "iPhone 11 Pro Max?"
,W="Pro"
StrAssign(&T, chars)
:赋值操作,把串 T 赋值为 chars。
StrCopy(&T, S)
:复制操作,由串 S 复制得到串 T。
StrEmpty(S)
:判空操作,若 S 为空串,则返回 TRUE,否则返回 FALSE。
StrLength(S)
:求串长,返回串 S 的元素个数。
ClearString(&S)
:清空操作,将 S 清为空串。
DestroyString(&S)
:销毁串,将串 S 销毁(回收存储空间)。
Concat(&T, S1, S2)
:串联接,用 T 返回由 S1 和 S2 联接而成的新串。
SubString(&Sub, S, pos, len)
:求子串,用 Sub 返回串 S 的第 pos 个字符起长度为 len 的子串。
Index(S, T)
:定位操作,若主串 S 中存在与串 T 值相同的子串,则返回它在主串 S 中第一次出现的位置;否则函数值为 0。
任何数据存到计算机中一定是二进制数。需要确定一个字符和二进制数的对应规则就是"编码"。 字符集 基于同一个字符集,可以有多种编码方案,如:UTF-8、UTF-16 注:采用不同的编码方式,每个字符所占空间不同。 y=f(x):字符集可以视为定义域;编码可以视为函数映射规则;y可以视为对应的二进制数。 拓展:乱码问题 原因:软件的解码方式出现问题 串的顺序存储 串的链式存储 基本操作的实现 什么是模式匹配 串的模式匹配:在主串中找到与模式串相同的子串,并返回其所在位置。 朴素模式匹配算法(简单模式匹配算法)思想: 将主串中与模式串长度相同的子串搞出来,挨个与模式串对比,当子串与模式串某个对应字符不匹配时,就立即放弃当前子串,转而检索下一个子串。 朴素模式匹配算法 朴素模式匹配算法性能分析 若模式串长度为 m,主串长度为 n,则 较好的情况:每个子串第一个字符就与模式串不匹配。 最坏时间复杂度: O ( n m ) O(nm) O(nm) 匹配成功的最好时间复杂度: O ( m ) O(m) O(m) 匹配失败的最好时间复杂度: O ( n − m + 1 ) = O ( n − m ) ≈ O ( m ) O(n-m+1)=O(n-m) \approx O(m) O(n−m+1)=O(n−m)≈O(m) 直到匹配成功/匹配失败最多需要 ( n − m + 1 ) ∗ m (n-m+1)*m (n−m+1)∗m 次比较。 KMP算法:当子串和模式串不匹配时,主串指针 KMP 算法代码: KMP 算法存在的问题 - 存在无效对比 KMP 算法优化:当子串和模式串不匹配时 树的基本概念 树是 n ( n ≥ 0 ) n (n \ge 0) n(n≥0)个结点的有限集合, 空树:节点数为0的树。 非空树的特性: 【结点】:树是一种递归定义的数据结构。 (笔记内容补充如下)树的基本概念 (笔记内容补充如下)树的逻辑结构 : 树中任何节点都可以有零个或多个直接后继节点(子节点),但至多只有一个直接前趋节点(父节点),根节点没有前趋节点,叶节点没有后继节点。 结点之间的关系描述 祖先结点,子孙结点,双亲结点(父节点),孩子结点,兄弟结点,堂兄弟结点; 什么是两个结点之间的路径?答案:只能从上往下; 什么是路径长度?答案:经过几条边; 结点、树的属性描述 结点的层次(深度)—— 从上往下数;默认从1开始,也有从1开始的。 结点的高度——从下往上数; 树的高度(深度)——总共多少层; 结点的度——有几个孩子(分支);叶子结点的度 = 0 =0 =0,非叶子结点的度 > 0 >0 >0 树的度——各结点的度的最大值; 有序树 V.S 无序树 有序树——逻辑上看,树中结点的各子树从左往右是有次序的,不能互换。 无序树——逻辑上看,树中结点的各子树从左往右是无次序的,可以互换。 具体看你用树村什么,是否需要用结点的左右位置反映某些逻辑关系。 树 V.S 森林 森林是 m ( m ≥ 0 ) m(m\ge0) m(m≥0)棵互不相交的树的集合。 空森林: m = 0 m=0 m=0。 常见考点1:树的结点数=树的总度数 + 1 +1 +1 树的度——各结点的度的最大值 常见考点2:度为 m m m的树、 m m m叉树的区别。 常见考点3:度为 m m m的树第 i i i层至多有 m i − 1 m^{i-1} mi−1个结点 ( i ≥ 1 ) (i\ge1) (i≥1)。 常见考点4:高度为 h h h的 m m m叉树至多有 m h − 1 m − 1 \frac{m^{h}-1}{m-1} m−1mh−1个结点。 常见考点5:高度为 h h h的 m m m叉树至少有 h h h个结点;高度为 h h h、度为 m m m的树至少有 h + m − 1 h+m-1 h+m−1个结点。 常见考点6:具有 n n n个结点的 m m m叉树的最小高度为 ⌈ l o g m ( n ( m − 1 ) + 1 ) ⌉ \lceil log_{m}^{(n(m-1)+1)}\rceil ⌈logm(n(m−1)+1)⌉。 高度最小的情况——所有结点都有 m m m个孩子。 (笔记内容补充如下):树的有序性 若树中每个节点的各个子树的排列为从左到右,不能交换,即兄弟之间是有序的,则该树称为有序树。一般的树是有序树。 二叉树的基本概念 二叉树是 n ( n ≥ 0 ) n(n\ge0) n(n≥0)个结点的有限集合: 特点:①每个结点至多只有两棵子树;②左右子树不能颠倒(二叉树是有序树)注意区别:度为2的有序树; 几个特殊的二叉树 满二叉树 一颗高度为 h h h,且含有 2 h − 1 2^{h}-1 2h−1个结点的二叉树。 特点: 完全二叉树 当且仅当其每个结点都与高度为 h h h的满二叉树中编号为 1 → n 1 \to n 1→n的结点一一对应时,称为完全二叉树。 特点: 二叉排序树 一颗二叉树或者是空二叉树,或者时具有如下性质的二叉树: 左子树上所有结点的关键字均小于根节点的关键字; 右子树上所有节点的关键字均大于根节点的关键字; 左子树和右子树又各是一颗二叉排序树。 【总结】:二叉排序树可用于元素的排序、搜索。 平衡二叉树 树上任一结点的左子树和右子树的深度之差不超过1。 【总结】:平衡二叉树能有更高的搜索效率。 二叉树的常考性质 (笔记补充内容):二叉树的性质 二叉树第i(i≥1)层上的节点最多为2(i-1)个。 深度为k(k≥1)的二叉树最多有2k-1个节点。 在任意一棵二叉树中,树叶的数目比度数为2的节点的数目多一。 假设n0是度为0的结点总数(即叶子结点数),n1是度为1的结点总数,n2是度为2的结点总数,则 总节点数为各类节点之和:n = n0 + n1 + n2 ① 总节点数为所有子节点数加一:n = n1 + 2*n2 + 1 ② ②-① 故得:n0 = n2 + 1 ; 满二叉树 :深度为k(k≥1)时有2^k-1个节点的二叉树。 完全二叉树 :只有最下面两层有度数小于2的节点,且最下面一层的叶节点集中在最左边的若干位置上。 完全二叉树深度: 具有n个节点的完全二叉树的深度为:「log2n」+1。 完全二叉树的常考性质 常见考点1:具有 n n n个 ( n > 0 ) (n>0) (n>0)结点的完全二叉树的高度 式子1推理思路如下:高为 推理思路如下:高为 h − 1 h-1 h−1的满二叉树共有 2 h − 1 − 1 2^{h-1}-1 2h−1−1个结点,高为 h h h的完全二叉树至少 2 h − 1 2^{h-1} 2h−1个结点,至多 2 h − 1 2^{h}-1 2h−1个结点。 ⇒ 2 h − 1 ≤ n < 2 h \Rightarrow 2^{h-1}\le n <2^{h} ⇒2h−1≤n<2h ⇒ h − 1 ≤ l o g 2 n < h \Rightarrow h-1\leq log_{2}^n 常见考点2:对于完全二叉树,可以由结点数 推理思路:【突破点】完全二叉树最多只有一个度为 1 1 1的结点,即 n 1 = 0 n_1=0 n1=0或 1 1 1。 【总结】:若完全二叉树有 2 k 2k 2k个(偶数)个结点,则必有 n 1 = 1 n_1=1 n1=1, n 0 = k n_0=k n0=k, n 2 = k − 1 n_2=k-1 n2=k−1。 【总结】:若完全二叉树有 2 k − 1 2k-1 2k−1个(奇数)个结点,则必有 n 1 = 0 n_1=0 n1=0, n 0 = k n_0=k n0=k, n 2 = k − 1 n_2=k-1 n2=k−1。 (笔记补充内容如下)二叉树的存储 : 设完全二叉树的节点数为n,某节点编号为i 二叉树的顺序存储 定义一个长度为 几个重要常考的基本操作: 二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来。 最坏情况:高度为 【总结】:二叉树的顺序存储结构知识和存储完全二叉树。 二叉树的链式存储 【总结】: 思考:如何找到指定节点的父节点?答案:三叉链表 什么是遍历 遍历:按照某种次序把所有的结点都访问一遍。 层次遍历:基于树的层次特性确定的次序规则。 先/中/后序遍历:基于树的递归特性确定的次序规则。 二叉树的遍历 二叉树的递归特性: 先序遍历(代码) 先序遍历(PreOrder)的操作过程如下: 该二叉树的时间空间复杂度为: O ( h + 1 ) O(h+1) O(h+1),其中 中序遍历 中序遍历(InOrder)的操作过程如下: 后序遍历 后序遍历(InOrder)的操作过程如下: 算法思想: 【结论】:若只给出一颗二叉树的前/中/后/层序遍历序列中的一种,不能唯一确定一颗二叉树。 【KEY】:找到树的根结点,并根据中序序列划分左右子树,再找到左右子树根结点。 前序+中序遍历序列 后序+中序遍历序列 层序+中序遍历序列 中序线索二叉树 指向前驱、后继的指针称为"线索";前驱线索:由左孩子指针充当;后继线索:由右孩子指针充当; 先序线索二叉树 后序线索二叉树 中序前驱 (笔记补充内容如下)二叉树的遍历: 由于二叉树的递归性质,遍历算法也是递归的。三种基本的遍历算法如下 先序遍历=====》先访问树根,再访问左子树,最后访问右子树; 中序遍历=====》先访问左子树,再访问树根,最后访问右子树; 后序遍历=====》先访问左子树,再访问右子树,最后访问树根; 递归 防止重复定义:#ifndef LQ 同时打开多个文件:vi file1 file2 练习题:StrCompare(S, T)
:比较操作,若 S>T,则返回值 >0;若 S=T,则返回值 =0;若 S
4.1.2 串的存储结构
// 静态数组实现
#define MAXLEN 255 // 预定义最大串长为255
typedef struct{
char ch[MAXLEN]; // 每个分量存储一个字符
int length; // 串的实际长度
}SString;
// 动态数组实现
typedef struct{
char *ch; // 按串长分配存储区,ch指向串的基地址
int length; // 串的长度
}HString;
HString S;
S.ch = (char *)malloc(MAXLEN *sizeof(char)); // 用完需要手动 free
S.length = 0;
typedef struct StringNode{
char ch[4]; // 每个节点存多个字符
struct StringNode *next;
}StringNode, *String;
SubString(&Sub, S, pos, len)
:求子串,用 Sub 返回串 S 的第 pos 个字符起长度为 len 的子串。#define MAXLEN 255
typedef struct{
char ch[MAXLEN];
int length;
}SString;
bool SubString(SString &Sub, SString S, int pos, int len){
// 子串范围越界
if(pos + len - 1 > S.length)
return false;
for(int i = pos; i < pos + len; i++)
Sub.ch[i-pos + 1] = S.ch[i];
Sub.length = len;
return true;
}
StrCompare(S, T)
:比较操作,若 S>T,则返回值 >0;若 S=T,则返回值 =0;若 Sint StrCompare(SString S, SString T){
for(int i = 1, i <= S.length && i <= T.length; i++){
if(S.sh[i] != T.ch[i])
return S.ch[i]-T.ch[i];
}
return S.length - T.length;
}
Index(S, T)
:定位操作,若主串 S 中存在与串 T 值相同的子串,则返回它在主串 S 中第一次出现的位置;否则函数值为 0。int Index(SString S, SString T){
int i = 1, n = StrLength(S), m = StrLength(T);
SString sub; // 用于暂存子串
while(i <= n -m + 1){
SubString(sub, S, i, m);
if(StrCompare(sub, T) != 0)
++i;
else
return i;
}
return 0;
}
4.2. 串的匹配算法
4.2.1 串的朴素模式匹配算法
int Index(SString S, SString T){
int k = 1;
int i = k, j = 1;
while(i <= S.length && j <= T.length){
if(S.ch[i] == T.ch[j]){
++i;
++j;
}else{
k++;
i = k;
j = 1;
}
}
if(j > T.length)
return k;
else
return 0;
}
4.2.2 KMP 算法 - 朴素模式匹配算法的优化
i
不回溯,模式串指针j=next[j]
算法平均时间复杂度: O ( n + m ) O(n+m) O(n+m)next
数组手算方法:当第j
个字符匹配失败,由前 1 ∼ j − 1 1 \sim j-1 1∼j−1个字符组成的穿记为S
,则:next[j]=S
的最长相等前后缀长度 + 1 +1 +1。特别的,next[1]=0
。int Index_KMP(SString S, SString T, int next[]){
int i = 1, j = 1;
while(i <= S.length && j <= T.length){
if(j == 0 || S.ch[i] == T.ch[j]){
++i;
++j; // 继续比较后继字符
}else{
j = next[j]; // 模式串向右移动
}
if(j > T.length)
return i - T.length; // 匹配成功
else
return 0;
}
}
4.2.3 KMP 算法的进一步优化 - nextval 数组
nextval
数组的求法:先算出next
数组,先令nextval[1]=0
for(int j = 2; j <= T.length; j++){
if(T.ch[next[j]] == T.ch[j])
nextval[j] = nextval[next[j]];
else
nextval[j] = next[j];
}
j=nextval[j]
。第五章 树
5.1 树的定义及性质
5.1.1 树的定义和基本术语
n=0
时,称为空树,这是一种特殊情况。在任意一颗非空树中应满足:
①有且仅有一个根节点;
②没有后继节点称为"叶子结点"(或终端结点);
③有后继节点称为"分支节点"(或非终端结点);
5.1.2 树的性质
m
叉树——每个结点最多只能由m
个孩子的树
度为m的树
m叉树
任意结点的度 ≤ m \le m ≤m(最多 m m m个孩子)
任意结点的度 ≤ m \le m ≤m(最多 m m m个孩子)
至少有一个结点度 = m =m =m(有 m m m个孩子)
允许所有节点的度都 < m
一定是非空树,至少有 m + 1 m+1 m+1个结点
可以是空树
5.2 二叉树
5.2.1 二叉树的定义和基本术语
5.2.2 二叉树的性质
h
为 ⌈ l o g 2 n + 1 ⌉ \lceil log_{2}^{n+1}\rceil ⌈log2n+1⌉或 ⌊ l o g 2 n ⌋ + 1 \lfloor log_{2}^{n}\rfloor+1 ⌊log2n⌋+1。h
的满二叉树共有 2 h − 1 2^{h}-1 2h−1个结点,高为h-1
的满二叉树共有 2 h − 1 − 1 2^{h-1}-1 2h−1−1个结点,所以 2 h − 1 − 1 < n ≤ 2 h − 1 2^{h-1}-1n
推出度为 0 0 0、 1 1 1和 2 2 2的结点个数为 n 0 n_0 n0、 n 1 n_1 n1和 n 2 n_2 n25.2.3 二叉树的存储结构
MaxSize
的数组t
,按照从上至下,从左至右的顺序依次存储完全二叉树中的各个结点。#define MaxSize 100
struct TreeNode{
ElemType value; // 结点中的数据元素
bool isEmpty; // 结点是否为空
}
TreeNode t[MaxSize];
for(int i = 0; i < MaxSize; i++){
t[i].isEmpty = true; // 初始化时所有结点标记为空
}
i
的左孩子—— 2 i 2i 2ii
的右孩子—— 2 i + 1 2i+1 2i+1i
的父节点—— ⌊ i / 2 ⌋ \lfloor i/2\rfloor ⌊i/2⌋i
所在的层次—— ⌈ l o g 2 n + 1 ⌉ \lceil log_{2}^{n+1}\rceil ⌈log2n+1⌉或 ⌊ l o g 2 n ⌋ + 1 \lfloor log_{2}^{n}\rfloor+1 ⌊log2n⌋+1h
且只有h
个结点的单支树(所有结点只有右孩子),也至少需要 2 h − 1 2^h-1 2h−1个存储单元。struct ElemType{
int value;
}
typedef struct BiTNode{
ElemType data; // 数据域
struct BiTNode *lchild, *rchild; // 左、右孩子指针
}BiTNode, *BiTree;
// 定义一颗空树
BiTree root = NULL;
// 插入根结点
root = (BiTree)malloc(sizeof(BiTNode));
root->data = {1};
root->lchild = NULL;
root->rchild = NULL;
// 插入新节点
BiTNode *p = {BiTNode *}malloc(sizeof(BiTNode));
p->data = {2};
p->lchild = NULL;
p->rchild = NULL;
root->lchild = p; // 作为根节点的左孩子
n
个结点的二叉链表共有 n + 1 n+1 n+1个空链域。typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild, *rchild;
struct BiTNode *parent;
}BiTNode, *BiTree;
5.3 二叉树的遍历
5.3.1 二叉树的先中后序遍历
typedef struct BitNode{
ElemType data;
struct BitNode *lchild, *rchild;
}BitNode, *BiTree;
void PreOrder(BiTree){
if(T != NULL){
visit(T); // 访问根结点
PreOrder(T->lchild); // 递归遍历左子树
PreOrder(T->rchild); // 递归遍历右子树
}
}
h
为该二叉树的高度;
5.3.2 二叉树的层次遍历
// 二叉树的结点(链式存储)
typedef struct BiTNode{
char data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
// 链式队列结点
typedef struct LinkNode{
BiTNode *data; // 存指针而不是存结点
struct LinkNode *next;
}LinkNode;
typedef struct{
LinkNode *front, *rear; // 队头队尾
}
// 层序遍历
void LevelOrder(BiTree T){
LinkQueue Q;
InitQueue(Q); // 初始化辅助队列
BiTree p;
EnQueue(Q,T); // 将根结点入队
while(!IsEmpty(Q)){ // 队列不空则循环
DeQueue(Q,p); // 队头结点出队
visit(p); // 访问出队结点
if(p->lchild != NULL)
EnQueue(Q,p->lchild); // 左孩子入队
if(p->rchild != NULL)
EnQueue(Q,p->rchild); // 右孩子入队
}
}
5.3.3 由遍历序列构造二叉树
5.3.4 线索二叉树的概念
n
个结点的二叉树,有n+1
个空链域!可用来记录前驱、后继的信息。// 线索二叉树结点
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild, *rchild;
// tag == 0:表示指针指向孩子;tag == 1:表示指针是"线索";
int ltag, rtag; // 左右线索标志
}ThreadNode, *ThreadTree;
5.3.5 二叉树的线索化
BiTNode *p; // p 指向目标结点
BiTNode *pre = NULL; // 指向当前访问结点的前驱
BiTNode *final = NULL; // 用于记录最终结果
void visit(BiTree *q){
if(q == p) // 当前访问结点刚好是结点 p
final = pre; // 找到 p 的前驱
else
pre = q; // pre 指向当前访问的结点
}
void InOder(BiTree T){
if(T != NULL){
InOder(T->lchild); // 递归遍历左子树
visit(T); // 访问根结点
InOder(T->rchild); // 递归遍历右子树
}
}
BTree *create_tree(int n,int 1)
{
BTree *root= (BTree *)malloc(sizeof(BTree));
root->data=1;
if(2*i<=n)
{
BTree *root->lchild=create_tree(n,2); =========>BTree *create_tree(int n,int 2)
} {
else BTree *root= (BTree *)malloc(sizeof(BTree));
{ root->data=2;
root->lchild=NULL; if(2*i<=n)
} {
BTree *root->lchild=create_tree(n,4); =========> BTree *create_tree(int n,int 2)
if(2*i+1<=n) } {
{ else BTree *root= (BTree *)malloc(sizeof(BTree))
BTree *root->rchild=create_tree(n,2*i+1); { root->data=2;
} root->lchild=NULL; if(2*i<=n)
else } {
{ BTree *root->lchild=create_tree(n,2*i);
root->rchild=NULL; if(2*i+1<=n) }
} { else
return root; BTree *root->rchild=create_tree(n,2*i+1); {
} } root->lchild=NULL;
else }
{
root->rchild=NULL; if(2*i+1<=n)
} {
return root; BTree *root->rchild=create_tree(n,2*i+1);
} }
else
{
root->rchild=NULL;
}
return root;
}
#ifdefine LQ
#endif
Vsp file2#include "seqstack.h"
seqstack_t *seqstack_create() //创建一个空的栈
{
seqstack_t *s;
s = (seqstack_t *)malloc(sizeof(seqstack_t));
s->top = -1;
return s;
}
int seqstack_full(seqstack_t *s) //判断栈是否为满
{
return s->top == N - 1 ? 1 : 0;
}
int seqstack_empty(seqstack_t *s) //判断栈是否为空
{
return s->top == -1 ? 1 : 0;
}
int seqstack_push(seqstack_t *s, datatype_t value) //入栈(压栈)
{
if(seqstack_full(s))
{
printf("seqstack is full\n");
return -1;
}
s->top++;
s->data[s->top] = value;
return 0;
}
int seqstack_show(seqstack_t *s) //打印数据
{
int i = 0;
for(i = 0; i <= s->top; i++)
{
printf("%d ", s->data[i]);
}
putchar(10);
return 0;
}
datatype_t seqstack_pop(seqstack_t *s) //出栈(弹栈)
{
datatype_t value;
if(seqstack_empty(s))
{
printf("seqstack is empty\n");
return -1;
}
value = s->data[s->top];
s->top--;
return value;
}
#include "linkqueue.h"
linkqueue_t *linkqueue_create() //创建一个空的队列
{
//把操作队列的这两个指针在堆区开辟空间
linkqueue_t *lq = (linkqueue_t *)malloc(sizeof(linkqueue_t)); //把操作队列的两个指针在堆区开辟空间
lq->front = lq->rear = (linknode_t *)malloc(sizeof(linknode_t)); //申请一个头结点的空间,标识队列为空
lq->front->next = NULL; //初始化结构体
return lq;
}
int linkqueue_empty(linkqueue_t *lq) //判断队列是否为空
{
return lq->front == lq->rear ? 1 : 0;
}
void linkqueue_input(linkqueue_t *lq, datatype value) //入队
{
linknode_t *temp = (linknode_t *)malloc(sizeof(linknode_t));
temp->data = value;
temp->next = NULL;
lq->rear->next = temp; //将新插入的结点插入到rear的后面
lq->rear = temp; //将rear指向最后一个结点(新插入的结点)
return ;
datatype linkqueue_output(linkqueue_t *lq) //出队
{
if(linkqueue_empty(lq))
{
printf("linkqueue is empty\n");
return (datatype)-1;
}
linknode_t *temp = lq->front->next;
lq->front->next = temp->next;
datatype value = temp->data;
free(temp);
temp = NULL;
if(lq->front->next == NULL) //当最后一个有数据的结点删除之后,需要将rear指向头结点,接着可以执行入队操作
{
lq->rear = lq->front;
}
return value;
}
#include "linkqueue.h"
#include "seqstack.h"
int linkqueue_check(linkqueue_t *lq)
{
//指针指向第一个有数据的结点
linknode_t *temp = lq->front->next; //指针指向第一个有数据的结点
while(temp->next != NULL)
{
//1 - 2 2 - 3 3 - 4 4 - 5
if(temp->data < temp->next->data)
{
temp = temp->next;
}
//1 - 4 4 - 2
else
{
return 0;
}
}
return 1;
}
int balltime()
{
int i;
int count = 0;
linkqueue_t *lq = linkqueue_create(); //创建一个队列,三个栈
seqstack_t *s_min = seqstack_create();
seqstack_t *s_five = seqstack_create();
seqstack_t *s_hour = seqstack_create();
for(i = 1; i <= 27; i++) //将27个球入队
{
linkqueue_input(lq, i);
}
while(1) //循环执行出队入栈,栈满了,出栈入队操作
{
i = linkqueue_output(lq); //出队操作
//计数
count++;
if(s_min->top < 3)
{
seqstack_push(s_min, i);
}
else //分钟栈已满
{
while(!seqstack_empty(s_min)) //出栈入队操作
{
linkqueue_input(lq, seqstack_pop(s_min));
}
if(s_five->top < 10)
{
seqstack_push(s_five, i);
}
else
{
while(!seqstack_empty(s_five))
{
linkqueue_input(lq, seqstack_pop(s_five));
}
if(s_hour->top < 10)
{
seqstack_push(s_hour, i);
}
else
{
while(!seqstack_empty(s_hour))
{
linkqueue_input(lq, seqstack_pop(s_hour));
}
linkqueue_input(lq, i); //将最后一个球入队
if(linkqueue_check(lq) == 1)
{
break;
}
}
}
}
}
return count;
}
int main(int argc, const char *argv[])
{
int count = balltime();
printf("%d min --> %d hour --> %d day\n", count, count / 60, count / 60/ 24);
return 0;
}
【10】树的概念及相关基础知识
【11】二叉树:严格区分左子树和右子树
第i层最多为 2~(i-1)个叶子
【12】满二叉树:
【13】完全二叉树:深度<2的节点在最底层,且具有n个节点的完全二叉树的深度为int log2n +1
【14】二叉树的遍历:先序遍历、中序遍历、后序遍历
#include