嵌入式开发——数据结构

该文为学习笔记,仅作学习参考,如有错误,望指正!

第一章. 绪论

数据结构在学什么?答案:如何用程序代码把现实世界的问题信息化

本章内容索引:

在这里插入图片描述

1. 数据结构的基本概念

相关概念:

数据:数据是信息的载体,是描述客观事物属性的数、字符以及所有能输入到计算机中并被计算机程序识别和处理的符号的集合。数据是计算机程序加工的原料。

数据元素(Data Element)、数据项:数据元素是数据的基本单位,又称之为记录(Record),通常作为一个整体进行考虑和处理。一个数据元素是由若干个数据项组成的,数据项是构成数据元素的不可分割的最小单位。

数据对象:是具有相同性质的数据元素的集合,是数据的一个子集。

数据结构:是相互之间存在一种或多种特定关系的数据元素的集合,包含下面三方面的内容。

  1. 逻辑结构:表示数据运算之间的抽象关系(如邻接关系、从属关系等),按每个元素可能具有的直接前趋数和直接后继数将逻辑结构分为“线性结构”和“非线性结构”两大类。
  2. 存储结构:逻辑结构在计算机中的具体实现方法,分为顺序存储方法、链接存储方法、索引存储方法、散列存储方法。
  3. 数据结构(DS)可用形式化语言描述,即DS是一个二元组:DS =(D,R),其中,D为数据元素的集合,R为D上关系的集合。数据运算:对数据进行的操作,如插入、删除、查找、排序等 。

数据类型:是一个值的集合和定义在此集合上的一组操作的总称。

  1. 原子类型:其值不可再分的数据类型。

  2. 结构类型:其值可以再分解为若干成分(分量)的数据类型。

  3. 数据类型是对数据元素取值范围与运算的限定。

  4. 例如在 C 语言中,若说明 int x;则变量x可以存放一个整数,取值范围一般为:[-32768,+32767],限定的操作为:[+,-,*,/,%(取模)]。

抽象数据类型(Abstract Data Type,ADT):是抽象数据组织及与之相关的操作。用数学化的语言定义数据的逻辑结构、定义运算,与具体的实现无关。

数据的逻辑结构(Logicial Structure):

集合:各个元素同属一个集合,别无其他关系;

逻辑结构分为线性结构 & 非线性结构

  1. 线性结构——一个对一个,如线性表、栈、队列;

    线性结构:元素之间是一对一的关系,除了第一个元素,所有元素都有唯一前驱;除了最后一个元素,所有元素都有唯一后继;

  2. 非线性结构:

    树形结构——一个对多个,如树;树形结构:数据元素之间是一对多的关系;

    图状结构——多个对多个,如图;图形结构:数据元素之间是多对多的关系;

数据的物理结构(存储结构):

顺序存储:把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。

链式存储:逻辑上相邻的元素在物理位置上可以不相邻,借助指示元素存储位置的指针来表示元素之间的逻辑关系。

索引存储:在存储元素信息的同时,还建立附加的索引表。索引表中的每项称为索引项,索引项的一般形式是(关键字,地址)

散列存储:根据元素的关键字直接计算出该元素的存储地址,又称哈希(Hash)存储

【小结】

  1. 若采用顺序存储,则各个数据元素之间在物理上必须是连续的,若采用非顺序存储,则各个数据元素在物理上可以是离散的
  2. 数据的存储结构影响存储空间分配的方便程度

数据的运算:

施加在数据上的运算包括运算的定义和实现。

运算的定义针对逻辑结构的。

运算的实现针对存储结构的。

根据逻辑结构来定义,根据存储结构来实现

2. 算法(Algorithm)的基本概念

程序=数据结构+算法;

  1. 数据结构:是要处理的信息;
  2. 算法:是处理信息的步骤;

算法的特性:

  1. 有穷性:有穷时间内能执行完成。算法是有穷的,程序可以是无穷的
  2. 确定性:相同的输入只会产生相同的输出,计算步骤无二义性。
  3. 可行性:可以用已有的基本操作实现算法。
  4. 输入:一个算法有 0 个或多个输入,即丢给算法处理的数据。
  5. 输出:一个算法有 1 个或多个输出,即算法处理的结果。

"好"算法的特质:

  1. 正确性:算法应能够正确地解决求解问题。

  2. 可读性:算法应具有良好的可读性,以帮助人们理解。

    注意:算法可以用伪代码描述,甚至用文字描述,重要的是要"无歧义"地描述出解决问题的步骤。

  3. 健壮性:输入非法数据时,算法能适当地作出反应或进行处理,而不会产生莫名其妙的输出结果。

  4. 高效率与低存储量需求:

    1. 高效率:执行速度快,时间复杂度低;
    2. 低存储量:不费内存,空间复杂度低;

解决一个问题可以有多种不同的算法,在算法正确的前提下,评价算法好坏的方法 :

  1. 消耗时间的多少 :
  2. 消耗存储空间的多少 :
  3. 容易理解、容易编程和调试、容易维护。时间复杂度的概念介绍
  4. 问题的规模 :输入数据量的大小,用n来表示。
  5. 算法的时间复杂度 :算法消耗时间,它是问题规模的函数 T(n)。

3. 算法的时间复杂度

  1. 语句的频度(Frequency Count)为可执行语句在算法(或程序)中重复执行的次数。若某语句执行一次的时间为 t,执行次数为 f,则该语句所耗时间的估计为 t ∗ f t*f tf
  2. n → ∞ n \to \infty n l i m ( T ( n ) n 3 ) = 2 lim(\frac{T(n)}{n^3})=2 lim(n3T(n))=2,故 T ( n ) T(n) T(n) n 3 n^3 n3 为同阶无穷大,或说 T ( n ) T(n) T(n) n 3 n^3 n3 成正比, T ( n ) T(n) T(n) 的量级为 n 3 n^3 n3,记为: T ( n ) = O ( n 3 ) T(n)=O(n^3) T(n)=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);
}
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)

  1. 加法规则: T ( n ) = T 1 ( n ) + T 2 ( n ) = O ( f ( n ) ) + O ( g ( n ) ) = O ( m a x ( f ( n ) , g ( n ) ) ) T(n)=T_1(n)+T_2(n)=O(f(n))+O(g(n))=O(max(f(n),g(n))) T(n)=T1(n)+T2(n)=O(f(n))+O(g(n))=O(max(f(n),g(n)))
  2. 乘法规则: T ( n ) = T 1 ( n ) ⋅ T 2 ( n ) = O ( f ( n ) ) ⋅ O ( g ( n ) ) = O ( f ( n ) ⋅ g ( n ) ) T(n)=T_1(n) \cdot T_2(n)=O(f(n)) \cdot O(g(n))=O(f(n) \cdot g(n)) T(n)=T1(n)T2(n)=O(f(n))O(g(n))=O(f(n)g(n))
  3. 规律小结: 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)

【例二】 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)

【总结】

  1. 顺序执行的代码只会影响常数项,可以忽略;
  2. 只需挑循环中的一个基本操作分析它的执行次数与 n 的关系即可;
  3. 如果有多层嵌套循环,只需关注最深层循环循环了几次;

最坏时间复杂度:最坏情况下算法的时间复杂度;

平均时间复杂度:所有输入示例等概率出现的情况下,算法的期望运行时间;

最好时间复杂度:最好情况下算法的时间复杂度;(参考价值较小)

4. 算法的空间复杂度

【例一】

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"

只需关注存储空间大小与问题规模相关的变量

空间复杂度=递归调用的深度

第二章. 线性表

1. 线性表的定义和基本操作

线性表的定义

线性表(Linear List):是具有相同(每个数据元素所占空间一样大)数据类型的 n ( n ≥ 0 ) n(n\geq0) nn0数据元素有限序列(有次序、有限),其中 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)

线性表是信息表的一种形式,表中数据元素之间满足线性关系(或线性结构),是一种最基本、最简单的数据结构类型。

几个概念:

  1. a i a_i ai 是线性表中的"第 i 个"元素线性表中的位序

    注意:位序从 1 开始,数组下标从 0 开始;

  2. a 1 a_1 a1表头元素 a n a_n an表尾元素

  3. 除第一个元素外,每个元素有且仅有一个直接前驱,除最后一个元素外,每个元素有且仅有一个直接后继

线性表的基本操作

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 个位置的元素的值。

其他常用操作:

  1. Length(L):求表长。返回线性表 L 的长度,即 L 中数据元素的个数。
  2. PrintList(L):输出操作。按前后顺序输出线性表 L 的所有元素值。
  3. Empty(L):判空操作。若 L 为空表,则返回true,否则返回false。

Tips:

  1. 对数据的操作(记忆思路)——创销、增删改查;

  2. C 语言函数的定义—— <返回值类型> 函数名 (<参数1类型> 参数1,<参数2类型> 参数2,…);

  3. 实际开发中,可根据实际需求定义其他的基本操作;

  4. 函数名和参数的形式、命名都可改变(Reference:严蔚敏版《数据结构》);

  5. 什么时候要传入引用 “&”—— 对参数的修改结果需要 “带回来”;

    【示例】C++ 工具进行编译

    #include <stdio.h>
    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 <stdio.h>
    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:

  1. 团队合作编程,你定义的数据结构要让别人能够很方便的使用(封装)
  2. 将常用的操作 / 运算封装成函数,避免重复工作,降低出错风险

2. 顺序表

2.1 顺序表的定义

顺序表的定义

顺序表——顺序存储的方式实现线性表顺序存储。把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。

设线性表第一个元素的存放位置是 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 就是你的顺序表中存放的数据元素类型。

顺序表的特点:

  1. 随机访问,即可以在 O ( 1 ) O(1) O(1) 时间内找到第 i i i 个元素。
  2. 存储密度高,每个节点只存储数据元素。
  3. 拓展容量不方便(即便采用动态分配的方式实现,扩展长度的时间复杂度也比较高)。
  4. 插入、删除操作不方便,需要移动大量元素。

顺序表的实现——静态分配

#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;

2.2 顺序表的初始化

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 <stdio.h>
#include <stdlib.h>
#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语言中‘&’一般用作取地址符,不支持引用传递。

因此,针对该问题有两种解决方案:

  1. 方法1:如果坚持要使用C语言,可以考虑把&使用*(指针)替换,函数调用时,用&传址。
  2. 方法2:不想修改代码,则可以改用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) MaxSizesizeof(ElemType)

  1. C++:

    #include <stdio.h>
    #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=-1;		// 顺序表的初始长度为0
    }
    int main(){
        SqList L;		// 声明一个顺序表
        InitList(L);		// 初始化顺序表
        for(int i=0; i<MaxSize; i++)		// 这种访问方式不够友好,更好的做法是使用基本操作来访问各个数据元素
            printf("data[%d]=%d\n",i,L.data[i]);
        return 0;
    }
    
  2. C语言:

    #include <stdio.h>
    #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:可以放弃治疗,顺序表的表长刚开始确定后就无法更改(存储空间是静态的)。

2.3 顺序表判空/判满

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;
}

2.4 打印顺序表

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;
}

2.5 顺序表的插入操作

2.5.1 顺序表的插入

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;
}
2.5.2 顺序表按位插入

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)

2.6 顺序表的删除操作

2.6.1 顺序表的删除

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;
}
2.6.2 顺序表按位删除

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)

2.7 顺序表的查找

2.7.1 顺序表的按位查找

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 个元素——"随机存取"特性。

2.7.2 顺序表的按值查找

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;
}

2.8 将顺序表中旧数据修改为新数据

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");
	}
}

2.9 按照位置修改顺序表中数据

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;
}

2.10 删除顺序表中重复数据

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--;
			}
		}
	}
}

2.11 合并顺序表

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)

3. 链表

  1. fopen()

    #include <stdio.h>
    FILE *fopen(const char *path, const char *mode);
    
    1. 功能:打开一个文件;
    2. 参数:
      1. path:要打开的文件名字;
      2. mode:r;
    3. 返回值:如果成功返回文件流指针(通过文件流指针进行对文件的 io 操作),失败则返回 NULL
  2. fgets()

    #include <stdio.h>
    char *fgets(char *restrict s, int n, FILE *restrict stream);
    
    1. 功能:读取一行数据 stream;
    2. 参数:
      1. s:存储读取一行的数据的缓冲区;
      2. n:读取数据大小;
      3. stream:文件流指针;
    3. 返回值:成功,返回读取到字符串的首地址;失败,返回 NULL
    [root@localhost test]# cat link_loop.c 
    1
    12
    123
    1234
    12345
    123456
    1234567
    12345678
    123456789
    
    #include <stdio.h>
    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
    

1. 单链表

什么是单链表?

在这里插入图片描述

  1. 顺序表:

    优点:可随机存取,存储密度高;

    缺点:要求大片连续空间,改变容量不方便;

  2. 单链表:

    优点:不要求大片连续空间,改变容量方便;

    缺点:不可随机存取,要消耗一定空间存放指针;

用代码定义一个单链表

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;		// 声明一个指向单链表第一个结点的指针,代码可读性更强
1.1 单链表的初始化

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 带头节点

在这里插入图片描述

1.2 头插法建立单链表

头插法、尾插法:核心就是初始化操作,指定结点的后插操作;

头插法的重要应用:链表的逆置

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;
}
1.3 尾插法建立单链表

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;
}
1.4 指定结点的后插操作

在这里插入图片描述

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;
}
1.5 指定结点的前插操作

在这里插入图片描述

时间复杂度: 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;
}
1.6. 带头节点的单链表按位序插入

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);	// 插入成功
}
1.7 不带头节点的单链表按位序插入

在这里插入图片描述

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);	// 插入成功
}

【总结】:不带头结点写代码更不方便,推荐用带头结点的。

【注意】:考试中带头、不带头都有可能考察,注意审题。

1.8 单链表指定结点的删除

时间复杂度: 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(LNodep):删除指定节点 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
}
1.9 带头节点的单链表按位序删除

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;
}

【总结】

  1. 这些代码都要会写,都重要;
  2. 打牢基础,慢慢加速;
  3. 体会带头结点、不带头节点的代码的区别;
  4. 体会 “封装” 的好处;
1.10 单链表的按位查找

在这里插入图片描述

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;
}
1.11 单链表的按值查找

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
}
1.12 求单链表的长度

时间复杂度: 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;
}

2.3.2. 双链表

在这里插入图片描述

双链表的初始化(带头结点)

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);
    // ...后续代码...
}

双链表的遍历

  1. 后向遍历

    while(p != NULL){
        p = p->next;
    }
    
  2. 前向遍历

    while(p != NULL){
        p = p->prior;
    }
    
  3. 前向遍历(跳过头节点)

    while(p->prior != NULL){
        p = p->prior;
    }
    

2.3.3. 循环链表

循环单链表

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;
}

2.3.4. 静态链表

什么是静态链表

单链表:各个节点在内存中星罗棋布、散落天涯。

静态链表:分配一整片连续的内存空间,各个结点集中安置。

静态链表:用数组的方式实现的链表。

优点:增、删操作不需要大量移动元素。

缺点:不能随机存取,只能从头节点开始依次往后查找;容量固定不变

适用场景:

  1. 不支持指针的低级语言;
  2. 数据元素数量不变的场景(如操作系统的文件分配表 FAT)

用代码定义一个静态链表

#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

2.4. 顺序表和链表的比较

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 链表

顺序表链表
弹性(可扩容)😭😀
增、删😭😀
😀😭

表长难以预估、经常要增加/删除元素——链表

2.5. 链表练习

  1. 练习1:

    #include <stdio.h>
    #include <stdlib.h>
    
    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. 练习2:约瑟夫问题

    #include <stdio.h>
    #include <stdlib.h>
    
    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
    

第三章. 栈和队列

3.1. 栈

3.1.1 栈的基本概念

在这里插入图片描述

栈(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)数,可采用数学归纳法证明(不要求掌握)。

3.1.2 栈的顺序存储实现

在这里插入图片描述

顺序栈的定义

#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

3.1.3 栈的链式存储实现

在这里插入图片描述

链栈的定义

typedef struct Linknode{
        ElemType data;		// 数据域
        struct Linknode *next;		// 指针域
}*LiStack;		// 栈类型定义

3.1.4 顺序栈 & 链栈的练习

  1. 通过顺序表实现栈

    #include <stdio.h>
    #include <stdlib.h>
    
    #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
    
  2. 通过链表实现栈(先进后出)

    1. 方法1:利用链表的头插头删法实现栈;
    2. 方法2:利用链表的尾插尾删实现栈;
    #include <stdio.h>
    #include <stdlib.h>
    
    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
    

3.2. 队列

3.2.1 队列的基本概念

在这里插入图片描述

队列(Queue)是只允许在一段进行插入,在另一端删除线性表

重要术语:队头、队尾、空队列。

队列的特点:先进先出(FIFO)。

顺序队列(循环队列):利用两个下标值frontrear

链式队列:①头插,尾删;②尾插,头删;③通过两个指针操作链表frontrear分别指向链表的头和尾。通过(尾删,头插)。

队列的基本操作

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。

3.2.2 队列的顺序存储实现

在这里插入图片描述

队列的顺序存储定义

#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;
}

其他出题方法

需要注意初始时,头指针和尾指针的位置。

3.2.3 队列的链式存储实现

队列的链式存储定义

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;
}

队列满的条件

顺序存储 —— 预分配的空间耗尽时队满。

链式存储 —— 一般不会队满,除非内存不足。

3.2.4 双端队列

在这里插入图片描述

双端队列是只允许从两端插入、两端删除的线性表。

输入受限的双端队列是只允许从一段插入、两端删除的线性表。

输出受限的双端队列是只允许从两端插入、一端删除的线性表。

考点:判断输出序列合法性

若数据元素输入序列为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,42,1,3,43,1,2,4×4,1,2,3×
1,2,4,32,1,4,33,1,4,2×4,1,3,2×
1,3,2,42,3,1,43,2,1,44,2,1,3×
1,3,4,22,3,4,1,3,2,4,14,2,3,1×
1,4,2,3×2,4,1,3×3,4,1,2×4,3,1,2×
1,4,3,22,4,3,13,4,2,14,3,2,1

3.2.5 双向循环链表

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;
}

3.2.6 顺序队列 & 链式队列的练习

  1. 顺序表实现循环队列

    #include <stdio.h>         //建立头文件
    #include <stdlib.h>
    #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;
    }
    
  2. 链表实现队列

    #include <stdio.h>
    #include <stdlib.h>  //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;
    }
    

3.3. 栈的应用

3.3.1 栈在括号匹配中的应用

括号匹配问题:最后出现的左括号最先被匹配(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);		// 检索完全部括号后,栈空说明匹配成功
}

3.3.2 栈在表达式求值中的应用

在这里插入图片描述

表达式中缀表达式后缀表达式前缀表达式
示例a + ba b ++ a b
示例a + b - ca b c - +- + a b c

中缀表达式转后缀表达式

中缀转后缀的手算方法:

  1. 确定中缀表达式中各个运算符的运算顺序(运算顺序不唯一,因此对应的后缀表达式也不唯一)。
  2. 选择下一个运算符,按照【左操作数 右操作数 运算符】的方式组合成一个新的操作数。
  3. 如果还有运算符没被处理,就继续 2。
  4. “左优先” 原则:只要左边的运算符能先计算,就优先算左边的(可保证运算顺序唯一)。

后缀表达式的手算方法:从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应运算,合体为一个操作数。注意:两个操作数的左右顺序。

后缀表达式的计算(机算):用栈实现,适用于基于栈的编程语言(stack-oriented programming language),如:Forth、PostScript

  1. 从左往右扫描下一个元素,直到处理完所有元素。
  2. 若扫描到操作数则压入栈,并回到 1,否则执行 3。
  3. 若扫描到运算符,则弹出两个栈顶元素,执行相应运算(注意:先出栈的是"右操作数"),运算结果压回栈顶,回到 1。

后缀表达式的计算(机算)

初始化一个栈,用于保存暂时还不能确定运算顺序的运算符。从左到右c护理各个元素,知道末尾,可能遇到三种情况:

  1. 遇到操作数,直接加入后缀表达式。
  2. 遇到界限符,遇到 “(” 直接入栈,遇到 “)” 则依次弹出栈内运算符并加入到后缀表达式,直到弹出 “(” 为止。注意:“(” 不加入后缀表达式。
  3. 遇到运算符,依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到 “(” 或栈空则停止。之后再把当前运算符入栈。

按上述方法处理完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式。

中缀表达式转前缀表达式

中缀转前缀的手算方法:

  1. 确定中缀表达式中各个运算符的运算顺序
  2. 选择下一个运算符,按照【运算符 左操作数 右操作数】的方式组合成一个新的操作数。
  3. 如果还有运算符没被处理,就继续 2。
  4. “右优先” 原则:只要右边的运算符能先计算,就优先算右边的。

前缀表达式的计算:用栈实现

  1. 从右往左扫描下一个元素,直到处理完所有元素。
  2. 若扫描到操作数则压入栈,并回到 1,否则执行 3。
  3. 若扫描到运算符,则弹出两个栈顶元素,执行相应运算(注意:先出栈的是"右操作数"),运算结果压回栈顶,回到 1。

中缀表达式的计算(用栈计算)

用栈实现中缀表达式的计算:中缀转后缀+后缀表达式求值,两个算法的结合

初始化两个栈,操作数栈和运算符栈,若扫描到操作数,压入操作数栈,若扫描到运算符或界限符,则按照"中缀转后缀"相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应运算,运算结果再压回操作数栈)

3.3.3 栈在递归中的调用

函数调用的特点:最后被调用的函数最先执行结束(LIFO)

函数调用时,需要用一个"函数调用栈"存储:

  1. 调用返回地址
  2. 实参
  3. 局部变量

适合用"递归"算法解决:可以把原始问题转换为属性相同,但规模较小的问题。

递归调用时,函数调用栈可称为"递归工作栈";每进入一层递归,就将递归调用所需信息压入栈顶,每退出一层递归,就从栈顶弹出相应信息;

缺点:效率低;太多层递归可能会导致栈溢出;可能包含很多重复计算。

解决方法:可以自定义栈将递归算法改造成非递归算法。

  1. Eg1:

    #include <stdio.h>
    
    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 !
    
  2. Eg2:

    #include <stdio.h>
    
    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 !
    

3.4. 队列的应用

树的层次遍历

注:在"树"章节会详细学习。

按层次遍历处理的时候都需要把对应的左右孩子放到队列队尾,而每次遍历处理的是队头元素。

图的广度优先遍历

注:在"图"章节会详细学习。

队列在操作系统中的应用

多个进程争抢着使用有限的系统资源时,FCFS(First Come First Service,先来先服务)是一种常用策略。

  1. Eg1:CPU资源的分配
  2. Eg2:打印数据缓冲区

3.5. 特殊矩阵的压缩存储

在这里插入图片描述

一维数组的存储结构

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 0i<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,1am,1a1,2a2,2a3,2am,2a1,3a2,3a3,3am,3a1,n1a2,n1a3,n1am,n1a1,na2,na3,nam,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,1an1,1an,1a1,2a2,2a3,2an1,2an,2a1,3a2,3a3,3an1,3an,3a1,n1a2,n1a3,n1an1,n1an,n1a1,na2,na3,nan1,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,n1 a n , n a_{n,n} an,n

思考:

  1. 数组大小应该为多少?

    ( 1 + n ) ∗ n 2 \frac{(1+n)*n}{2} 2(1+n)n

  2. 站在程序员的角度,对称矩阵压缩存储后怎样才能方便使用?

    可以实现一个"映射"函数,矩阵下标——>数组下标, a i , j ( i ≥ j ) a_{i,j}(i \geq j) ai,jij——>B[K]

    Key:按行优先原则, a i , j a_{i,j} ai,j 是第几个元素?

    [ 1 + 2 + ⋯ + ( i − 1 ) + j ] [1+2+ \cdots +(i-1)+j] [1+2++(i1)+j]——>第 i ( i − 1 ) 2 + j \frac{i(i-1)}{2}+j 2i(i1)+j 个元素, k = i ( i − 1 ) 2 + j − 1 k=\frac{i(i-1)}{2}+j-1 k=2i(i1)+j1

策略:只存储主对角线+下三角区,按列优先原则将各元素存入一维数组中。

出题方法:

  1. 存储上三角?下三角?
  2. 行优先?列优先?
  3. 矩阵元素的下标是从0?1?开始
  4. 数组下标从0?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,1an1,1an,1ca2,2a3,2an1,2an,2cca3,3an1,3an,3cccan1,n1an,n1ccccan,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,nc

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(i1)+j12n(n+1)ij(下三角区和主对角线元素)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,1cccca1,2a2,2ccca1,3a2,3a3,3cca1,n1a2,n1a3,n1an1,n1ca1,na2,na3,nan1,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(i1)(2ni+2)+(ji)2n(n+1)ij(上三角区和主对角线元素)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,1000a1,2a2,2a3,2000a2,3a3,300000an1,n1an,n1000an1,nan,n

三对角矩阵,又称带状矩阵:当 ∣ i − j ∣ > 1 |i-j| > 1 ij>1 时,有 a i , j = 0 ( 1 ≤ i , j ≤ n ) a_{i,j} = 0(1 \leq i, j \leq n) ai,j=01i,jn

压缩存储策略:按行优先(或列优先)原则,只存储带状部分;一维数组长度为 3 n − 2 3n-2 3n2

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[3n3]
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,n1 a n , n a_{n,n} an,n

Key:按行优先的原则, a i , j a_{i,j} ai,j 是第几个元素?

  1. 前 i-1 行共 3(i-1)-1 个元素
  2. a i , j a_{i,j} ai,j 是 i 行第 j-i+2 个元素
  3. a i , j a_{i,j} ai,j 是第 2i+j-2 个元素
  4. 数组下标从 0 开始, k = 2 i + j − 3 k=2i+j-3 k=2i+j3
  5. i = ⌈ ( k + 2 ) / 3 ⌉ i = \lceil (k+2)/3\rceil i=⌈(k+2)/3向上取整即可满足"刚好"大于等于

稀疏矩阵的压缩存储

( 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 稀疏矩阵:非零元素远远少于矩阵元素的个数。

压缩存储策略:

  1. 顺序存储 —— 三元组<行,列,值>

    i(行)j(列)v(值)
    134
    165
    223
    249
    357
    422
  2. 链式存储 —— 十字链表法

    定义向下域 down,指向第 j 列的第一个元素;

    定义向右域 right,指向第 i 行的第一个元素;

    非零数据节点说明:

    指向同列的下一个元素指向同行的下一个元素

第四章. 串

4.1. 串的定义和基本操作以及存储结构

4.1.1 串的定义和基本操作

在这里插入图片描述

,即字符串(String)是由零个或多个字符组成的有限序列。一般记为 S=‘ a 1 a 2 ⋯ a n a_{1}a_{2} \cdots a_{n} a1a2an’ ( n ≥ 0 n \geq 0 n0),其中,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 空格串:

  1. M = ‘’:M是空串;
  2. N = ’ ':N是由三个空格字符组成的空格串,每个空格字符占 1B;

串是一种特殊的线性表,数据元素之间呈线性关系。

串的数据对象限定为字符集(如中文字符、英文字符、数字字符、标点字符等)。

串的基本操作,如增删改查等通常以子串为操作对象

串的基本操作

假设有串 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。

StrCompare(S, T):比较操作,若 S>T,则返回值 >0;若 S=T,则返回值 =0;若 S<T,则返回值 <0。

任何数据存到计算机中一定是二进制数。需要确定一个字符和二进制数的对应规则就是"编码"。

字符集

  1. 英文字符 —— ASCII字符集;
  2. 中英文 —— Unicode 字符集;

基于同一个字符集,可以有多种编码方案,如:UTF-8、UTF-16

注:采用不同的编码方式,每个字符所占空间不同。

y=f(x):字符集可以视为定义域;编码可以视为函数映射规则;y可以视为对应的二进制数。

拓展:乱码问题

原因:软件的解码方式出现问题

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;若 S<T,则返回值 <0。

int 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;
}

朴素模式匹配算法性能分析

若模式串长度为 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(nm+1)=O(nm)O(m)

直到匹配成功/匹配失败最多需要 ( n − m + 1 ) ∗ m (n-m+1)*m (nm+1)m 次比较。

4.2.2 KMP 算法 - 朴素模式匹配算法的优化

KMP算法:当子串和模式串不匹配时,主串指针i不回溯,模式串指针j=next[j]算法平均时间复杂度: O ( n + m ) O(n+m) O(n+m)

next数组手算方法:当第j个字符匹配失败,由前 1 ∼ j − 1 1 \sim j-1 1j1个字符组成的穿记为S,则:next[j]=S的最长相等前后缀长度 + 1 +1 +1。特别的,next[1]=0

KMP 算法代码:

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 数组

KMP 算法存在的问题 - 存在无效对比

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];
}

KMP 算法优化:当子串和模式串不匹配时j=nextval[j]

第五章 树

5.1 树的定义及性质

5.1.1 树的定义和基本术语

在这里插入图片描述

树的基本概念

树是 n ( n ≥ 0 ) n (n \ge 0) n(n0)个结点的有限集合,n=0时,称为空树,这是一种特殊情况。在任意一颗非空树中应满足:

  1. 有且仅有一个特定的称为根的结点。
  2. n > 1 n>1 n>1时,其余结点可分为 m ( m ≥ 0 ) m(m\ge0) m(m0)个互不相交的有限集合 T 1 , T 2 , . . . , T m T_1,T_2,...,T_m T1T2...Tm,其中每个集合本身又是一棵树,并且称为根节点的子树。

空树:节点数为0的树。

非空树的特性:
①有且仅有一个根节点;
②没有后继节点称为"叶子结点"(或终端结点);
③有后继节点称为"分支节点"(或非终端结点);

【结点】:树是一种递归定义的数据结构。

(笔记内容补充如下)树的基本概念

  1. 节点度数:一个节点的子树的个数称为该节点的度数,
  2. 一棵树度数:一棵树的度数是指该树中节点的最大度数。
  3. 叶子节点(终端节点):度数为零的节点称为树叶或终端节点,
  4. 分之节点:度数不为零的节点称为分支节点,除根节点外的分支节点称为内部节点。
  5. 一个节点的子树之根节点称为该节点的子节点,该节点称为它们的父节点,
  6. 同一节点的各个子节点之间称为兄弟节点。一棵树的根节点没有父节点,叶节点没有子节点。
  7. 节点的层数等于父节点的层数加一,根节点的层数定义为一。树中节点层数的最大值称为该树的高度或深度。

(笔记内容补充如下)树的逻辑结构 :

树中任何节点都可以有零个或多个直接后继节点(子节点),但至多只有一个直接前趋节点(父节点),根节点没有前趋节点,叶节点没有后继节点。

结点之间的关系描述

祖先结点,子孙结点,双亲结点(父节点),孩子结点,兄弟结点,堂兄弟结点;

什么是两个结点之间的路径?答案:只能从上往下;

什么是路径长度?答案:经过几条边;

结点、树的属性描述

结点的层次(深度)—— 从上往下数;默认从1开始,也有从1开始的。

结点的高度——从下往上数;

树的高度(深度)——总共多少层;

结点的度——有几个孩子(分支);叶子结点的度 = 0 =0 =0,非叶子结点的度 > 0 >0 >0

树的度——各结点的度的最大值;

有序树 V.S 无序树

有序树——逻辑上看,树中结点的各子树从左往右是有次序的,不能互换。

无序树——逻辑上看,树中结点的各子树从左往右是无次序的,可以互换。

具体看你用树村什么,是否需要用结点的左右位置反映某些逻辑关系。

树 V.S 森林

森林是 m ( m ≥ 0 ) m(m\ge0) m(m0)棵互不相交的树的集合。

空森林: m = 0 m=0 m=0

5.1.2 树的性质

在这里插入图片描述

常见考点1:树的结点数=树的总度数 + 1 +1 +1

树的度——各结点的度的最大值

m叉树——每个结点最多只能由m个孩子的树

常见考点2:度为 m m m的树、 m 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 <m
一定是非空树,至少有 m + 1 m+1 m+1个结点可以是空树

常见考点3:度为 m m m的树第 i i i层至多有 m i − 1 m^{i-1} mi1个结点 ( i ≥ 1 ) (i\ge1) (i1)

常见考点4:高度为 h h h m m m叉树至多有 m h − 1 m − 1 \frac{m^{h}-1}{m-1} m1mh1个结点。

常见考点5:高度为 h h h m m m叉树至少有 h h h个结点;高度为 h h h、度为 m m m的树至少有 h + m − 1 h+m-1 h+m1个结点。

常见考点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(m1)+1)

高度最小的情况——所有结点都有 m m m个孩子。

(笔记内容补充如下):树的有序性

若树中每个节点的各个子树的排列为从左到右,不能交换,即兄弟之间是有序的,则该树称为有序树。一般的树是有序树。

5.2 二叉树

5.2.1 二叉树的定义和基本术语

在这里插入图片描述

二叉树的基本概念

二叉树是 n ( n ≥ 0 ) n(n\ge0) n(n0)个结点的有限集合:

  1. 或者为空二叉树,即 n = 0 n=0 n=0
  2. 或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分别是一棵二叉树。

特点:①每个结点至多只有两棵子树;②左右子树不能颠倒(二叉树是有序树)注意区别:度为2的有序树

在这里插入图片描述

几个特殊的二叉树

满二叉树

一颗高度为 h h h,且含有 2 h − 1 2^{h}-1 2h1个结点的二叉树。

特点:

  1. 只有最后一层有叶子节点。
  2. 不存在度为1的结点。
  3. 按层序从1开始编号,结点 i i i的左孩子为 2 i 2i 2i,右孩子为 2 i + 1 2i+1 2i+1;结点 i i i的父节点为 ⌊ i 2 ⌋ \lfloor\frac{i}{2}\rfloor 2i(如果有的话)。

完全二叉树

当且仅当其每个结点都与高度为 h h h的满二叉树中编号为 1 → n 1 \to n 1n的结点一一对应时,称为完全二叉树。

特点:

  1. 只有最后两层可能有叶子节点。
  2. 最多只有一个度为1的结点。
  3. 同满二叉树的③。
  4. i ≤ ⌊ n 2 ⌋ i \le \lfloor\frac{n}{2}\rfloor i2n为分支结点, i > ⌊ n 2 ⌋ i > \lfloor\frac{n}{2}\rfloor i>2n为叶子节点。

二叉排序树

在这里插入图片描述

一颗二叉树或者是空二叉树,或者时具有如下性质的二叉树:

左子树上所有结点的关键字均小于根节点的关键字;

右子树上所有节点的关键字均大于根节点的关键字;

左子树和右子树又各是一颗二叉排序树。

【总结】:二叉排序树可用于元素的排序、搜索。

平衡二叉树

树上任一结点的左子树和右子树的深度之差不超过1。

【总结】:平衡二叉树能有更高的搜索效率。

5.2.2 二叉树的性质

二叉树的常考性质

  1. 常见考点1:设非空二叉树中度为0、1和2的结点个数分别为 n 0 n_0 n0 n 1 n_1 n1 n 2 n_2 n2,则 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1(叶子节点比二分支节点多一个)假设树中接地点总数为 n n n,则
    1. n = n 0 + n 1 + n 2 n=n_0+n_1+n_2 n=n0+n1+n2
    2. n = n 1 + 2 n 2 + 1 n=n_1+2n_2+1 n=n1+2n2+1
    3. → n 0 = n 2 + 1 \to n_0=n_2+1 n0=n2+1
  2. 常见考点2:二叉树第 i i i层至多有 2 i − 1 2^{i-1} 2i1个结点 ( i ≥ 1 ) (i\ge1) (i1) m m m叉树第 i i i层至多有 m i − 1 m^{i-1} mi1个结点 ( i ≥ 1 ) (i\ge1) (i1)
  3. 常见考点3:高度为 h h h的二叉树至多有 2 h − 1 2^{h}-1 2h1个结点(满二叉树);高度为 h h h m m m叉树至多有 m h − 1 m − 1 \frac{m^{h}-1}{m-1} m1mh1个结点。

(笔记补充内容):二叉树的性质

  1. 二叉树第i(i≥1)层上的节点最多为2(i-1)个。

  2. 深度为k(k≥1)的二叉树最多有2k-1个节点。

  3. 在任意一棵二叉树中,树叶的数目比度数为2的节点的数目多一。

    假设n0是度为0的结点总数(即叶子结点数),n1是度为1的结点总数,n2是度为2的结点总数,则

    总节点数为各类节点之和:n = n0 + n1 + n2 ①

    总节点数为所有子节点数加一:n = n1 + 2*n2 + 1 ②

    ②-①

    故得:n0 = n2 + 1 ;

  4. 满二叉树 :深度为k(k≥1)时有2^k-1个节点的二叉树。

  5. 完全二叉树 :只有最下面两层有度数小于2的节点,且最下面一层的叶节点集中在最左边的若干位置上。

    完全二叉树深度: 具有n个节点的完全二叉树的深度为:「log2n」+1。

完全二叉树的常考性质

  1. 常见考点1:具有 n n n ( n > 0 ) (n>0) (n>0)结点的完全二叉树的高度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

    式子1推理思路如下:高为h的满二叉树共有 2 h − 1 2^{h}-1 2h1个结点,高为h-1的满二叉树共有 2 h − 1 − 1 2^{h-1}-1 2h11个结点,所以 2 h − 1 − 1 < n ≤ 2 h − 1 2^{h-1}-1<n\le2^{h}-1 2h11<n2h1 ⇒ 2 h − 1 < n + 1 ≤ 2 h \Rightarrow 2^{h-1}<n+1\le 2^{h} 2h1<n+12h ⇒ h − 1 < l o g 2 n + 1 ≤ h \Rightarrow h-1<log_{2}^{n+1}\le h h1<log2n+1h,向上取整后 h = ⌈ l o g 2 n + 1 ⌉ h=\lceil log_{2}^{n+1}\rceil h=log2n+1

    推理思路如下:高为 h − 1 h-1 h1的满二叉树共有 2 h − 1 − 1 2^{h-1}-1 2h11个结点,高为 h h h的完全二叉树至少 2 h − 1 2^{h-1} 2h1个结点,至多 2 h − 1 2^{h}-1 2h1个结点。 ⇒ 2 h − 1 ≤ n < 2 h \Rightarrow 2^{h-1}\le n <2^{h} 2h1n<2h ⇒ h − 1 ≤ l o g 2 n < h \Rightarrow h-1\leq log_{2}^n <h h1log2n<h ⇒ h = ⌊ l o g 2 n + 1 ⌋ \Rightarrow h=\lfloor log_{2}^{n}+1\rfloor h=log2n+1

  2. 常见考点2:对于完全二叉树,可以由结点数n推出度为 0 0 0 1 1 1 2 2 2的结点个数为 n 0 n_0 n0 n 1 n_1 n1 n 2 n_2 n2

    推理思路:【突破点】完全二叉树最多只有一个度为 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=k1

    【总结】:若完全二叉树有 2 k − 1 2k-1 2k1个(奇数)个结点,则必有 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=k1

5.2.3 二叉树的存储结构

二叉树的存储结构
顺序存储
链式存储

(笔记补充内容如下)二叉树的存储 :

设完全二叉树的节点数为n,某节点编号为i

  1. 当i>1(不是根节点)时,有父节点,其编号为i/2;
  2. 当2i≤n时,有左孩子,其编号为2i ,否则没有左孩子,本身是叶节点;
  3. 当2i+1≤n时,有右孩子,其编号为2i+1 ,否则没有右孩子;

二叉树的顺序存储

在这里插入图片描述

定义一个长度为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;		// 初始化时所有结点标记为空
}

几个重要常考的基本操作:

  1. i的左孩子—— 2 i 2i 2i
  2. i的右孩子—— 2 i + 1 2i+1 2i+1
  3. i的父节点—— ⌊ i / 2 ⌋ \lfloor i/2\rfloor i/2
  4. 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+1

二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来

最坏情况:高度为h且只有h个结点的单支树(所有结点只有右孩子),也至少需要 2 h − 1 2^h-1 2h1个存储单元。

【总结】:二叉树的顺序存储结构知识和存储完全二叉树。

二叉树的链式存储

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 二叉树的先中后序遍历

在这里插入图片描述

什么是遍历

遍历:按照某种次序把所有的结点都访问一遍。

层次遍历:基于树的层次特性确定的次序规则。

先/中/后序遍历:基于树的递归特性确定的次序规则。

二叉树的遍历

二叉树的递归特性:

  1. 要么是各空二叉树;
  2. 要么就是由"根结点+左子树+右子树"组成的二叉树。
    1. 先序遍历:根左右(NLR)
    2. 中序遍历:左根右(LNR)(需要加界限符)
    3. 后序遍历:左右根(LRN)

先序遍历(代码)

先序遍历(PreOrder)的操作过程如下:

  1. 若二叉树为空,则什么也不做;
  2. 若二叉树非空:
    1. 访问根结点;
    2. 先序遍历左子树;
    3. 先序遍历右子树;
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);		// 递归遍历右子树
        }      
}

该二叉树的时间空间复杂度为: O ( h + 1 ) O(h+1) O(h+1),其中h为该二叉树的高度;

中序遍历

中序遍历(InOrder)的操作过程如下:

  1. 若二叉树为空,则什么也不做;
  2. 若二叉树非空:
    1. 先序遍历左子树;
    2. 访问根结点;
    3. 先序遍历右子树;

后序遍历

后序遍历(InOrder)的操作过程如下:

  1. 若二叉树为空,则什么也不做;
  2. 若二叉树非空:
    1. 先序遍历左子树;
    2. 先序遍历右子树;
    3. 访问根结点;

5.3.2 二叉树的层次遍历

算法思想:

  1. 初始化一个辅助队列;
  2. 根节点入队;
  3. 若队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾(如果有的话);
  4. 重复③直至队列为空。
// 二叉树的结点(链式存储)
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 由遍历序列构造二叉树

【结论】:若只给出一颗二叉树的前/中/后/层序遍历序列中的一种,不能唯一确定一颗二叉树。

由二叉树的遍历序列构造二叉树
前序+中序遍历序列
后序+中序遍历序列
层序+中序遍历序列

【KEY】:找到树的根结点,并根据中序序列划分左右子树,再找到左右子树根结点。

前序+中序遍历序列

在这里插入图片描述

后序+中序遍历序列

在这里插入图片描述

层序+中序遍历序列

在这里插入图片描述

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;
                                                                                          }  

防止重复定义:#ifndef LQ
#ifdefine LQ
#endif

同时打开多个文件:vi file1 file2
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 +114】二叉树的遍历:先序遍历、中序遍历、后序遍历
#include <stdio.h>
#include <stdlib.h>
typedef int data_t;
typedef struct Bitree{
                   data_t data;
                   struct Bitree *lchild;
                   struct Bitree *rchild;
}BTree;
BTree *create_tree(int n,int i)           //创建二叉树
{
         BTree *root= (BTree *)malloc(sizeof(BTree));
         int num;
         scanf("%d",&num);
         root->data=num;
         if(2*i<=n)
         {
          root->lchild=create_tree(n,2*i);
         }
         else
         {
                   root->lchild=NULL;
         }
         if(2*i+1<=n)
         {
                   root->rchild=create_tree(n,2*i+1);
         }
         else
         {
                   root->rchild=NULL;
         }
return root;
}
void tree_first(BTree *root)         //先序列遍历 (根左右)
{
         printf("  %d   ",root->data);
         if(root->lchild!=NULL)
         {
                   tree_first(root->lchild);        
         }
         if(root->rchild!=NULL)
         {
                   tree_first(root->rchild);
         }
return ;
}
void tree_in(BTree *root)          //中序列遍历 (根左右)
{
         if(root->lchild!=NULL)
         {
                   tree_in(root->lchild);  
         }
         printf("  %d   ",root->data);
         if(root->rchild!=NULL)
         {
                   tree_in(root->rchild);
         }
return ;
}
void tree_after(BTree *root)         //后序列遍历 (左右根if)
{
         if(root->lchild!=NULL)
         {
                   tree_after(root->lchild);       
         }
         if(root->rchild!=NULL)
         {
                   tree_after(root->rchild);
         }
         printf("  %d   ",root->data);
return ;
}
int main(int argc, const char *argv[])
{
         BTree *root=create_tree(8,1);      
         tree_first(root);
         putchar(10);
         tree_in(root);
         putchar(10);
         tree_after(root);
         putchar(10);
         return 0;
}15】二叉树的终端输入
#include <stdio.h>
#include <stdlib.h>
typedef int data_t;
typedef struct Bitree{
                   data_t data;
                   struct Bitree *lchild;
                   struct Bitree *rchild;
}BTree;
BTree *create_tree(int n,int i)       //创建二叉树
{
         BTree *root= (BTree *)malloc(sizeof(BTree));
         int num;
         scanf("%d",&num);
         root->data=num;
         if(2*i<=n)
         {
          root->lchild=create_tree(n,2*i);
         }
         else
         {
                   root->lchild=NULL;
         }
         if(2*i+1<=n)
         {
                   root->rchild=create_tree(n,2*i+1);
         }
         else
         {
                   root->rchild=NULL;
         }
return root;
}
void tree_first(BTree *root)       //先序列遍历 (根左右)
{
         printf("  %d   ",root->data);
         if(root->lchild!=NULL)
         {
                   tree_first(root->lchild);        
         }
         if(root->rchild!=NULL)
         {
                   tree_first(root->rchild);
         }
return ;
}
void tree_in(BTree *root)       //中序列遍历 (根左右)
{
         if(root->lchild!=NULL)
         {
                   tree_in(root->lchild);  
         }
         printf("  %d   ",root->data);
         if(root->rchild!=NULL)
         {
                   tree_in(root->rchild);
         }
return ;
}
void tree_after(BTree *root)        //后序列遍历 (根左右)
{
         if(root->lchild!=NULL)
         {
                   tree_after(root->lchild);       
         }
         if(root->rchild!=NULL)
         {
                   tree_after(root->rchild);
         }
         printf("  %d   ",root->data);
return ;
}
int main(int argc, const char *argv[])
{
         BTree *root=create_tree(8,1);      
         tree_first(root);
         putchar(10);
         tree_in(root);
         putchar(10);
         tree_after(root);
         putchar(10);
         return 0;
}16】不完全二叉树
#include <stdio.h>
#include <stdlib.h>
typedef char data_t;
typedef struct Bitree{
                   data_t data;
                   struct Bitree *lchild;
                   struct Bitree *rchild;
}BTree;
BTree *create_tree()          //创建二叉树
{
         BTree *root= (BTree *)malloc(sizeof(BTree));
         char  num;
         scanf("%c",&num);
         getchar();         //回收垃圾字符
         if(num=='#')
         {
         return NULL;
         }
         root->data=num;
         root->lchild=create_tree();
         root->rchild=create_tree();
return root;
}
void tree_first(BTree *root)       //先序列遍历 (根左右)
{
         printf("  %c   ",root->data);
         if(root->lchild!=NULL)
         {
                   tree_first(root->lchild);        
         }
         if(root->rchild!=NULL)
         {
                   tree_first(root->rchild);
         }
return ;
}
void tree_in(BTree *root)         //中序列遍历 (根左右)
{
         if(root->lchild!=NULL)
         {
                   tree_in(root->lchild);  
         }
         printf("  %c   ",root->data);
         if(root->rchild!=NULL)
         {
                   tree_in(root->rchild);
         }
return ;
}
void tree_after(BTree *root)         //后序列遍历 (根左右)
{
         if(root->lchild!=NULL)
         {
                   tree_after(root->lchild);       
         }
         if(root->rchild!=NULL)
         {
                   tree_after(root->rchild);
         }
         printf("  %c   ",root->data);
return ;
}
int main(int argc, const char *argv[])
{
         BTree *root=create_tree(8,1);      
         tree_first(root);
         putchar(10);
         tree_in(root);
         putchar(10);
         tree_after(root);
         putchar(10);
         return 0;
}17】二叉树的层遍历
#include "1_Bitree.h"        //自定义函数库
BTree *create_tree(int n,int i)
{
         BTree *root= (BTree *)malloc(sizeof(BTree));
         int num;
         scanf("%d",&num);
         root->data=num;
         if(2*i<=n)
         {
          root->lchild=create_tree(n,2*i);
         }
         else
         {
                   root->lchild=NULL;
         }
         if(2*i+1<=n)
         {
                   root->rchild=create_tree(n,2*i+1);
         }
         else
         {
                   root->rchild=NULL;
         }
return root;
}
void tree_first(BTree *root)          //先序列遍历 (根左右)
{
         printf("  %d   ",root->data);
         if(root->lchild!=NULL)
         {
                   tree_first(root->lchild);        
         }
         if(root->rchild!=NULL)
         {
                   tree_first(root->rchild);
         }
return ;
}
void tree_in(BTree *root)        //中序列遍历 (根左右)
{
         if(root->lchild!=NULL)
         {
                   tree_in(root->lchild);  
         }
         printf("  %d   ",root->data);
         if(root->rchild!=NULL)
         {
                   tree_in(root->rchild);
         }
return ;
}
void tree_after(BTree *root)          //后序列遍历 (根左右)
{
         if(root->lchild!=NULL)
         {
                   tree_after(root->lchild);       
         }
         if(root->rchild!=NULL)
         {
                   tree_after(root->rchild);
         }
         printf("  %d   ",root->data);
return ;
}
#include "link_queue.h"           //自定义创建空队列
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;
}
#include "/home/linux/18061/day_4/tree_level/link_queue.h"       //主函数
#include "/home/linux/18061/day_4/tree_level/1_Bitree.h"
void tree_order_level(BTree *root)
{
         data_t val;
         linkqueue_t         *lq=linkqueue_create();        //创建一个队列
         linkqueue_in(lq,root);
         while(!linkqueue_empty(lq))
         {
                   root=linkqueue_out(lq);
                   printf("  %d  ",root->data);
                   if(root->lchild!=NULL)
                   {
                            linkqueue_in(lq,root->lchild);        
                   }
                   if(root->rchild!=NULL)
                   {
                            linkqueue_in(lq,root->rchild);
                   }
         }
}
int main(int argc, const char *argv[])
{
         BTree *root=create_tree(9,1);
         tree_order_level(root);
         return 0;
}              

第六章 图

1、图的存储:二维数组

创建图,打印数据及关系

#include <stdio.h>
#include <stdio.h>
#include <string.h> //memset
#define N 5
typedef int data_t;
typedef struct graph{
	data_t	data[N];
	int  matrix[N][N];
}graph_t;
graph_t *graph_create()          //创建空的图
{
	graph_t *g=malloc(sizeof(graph_t));	//开辟空间
	memset(g,0,sizeof(graph_t));      //清空g指向的起始地址到偏移量为sizeof(graph_t)大小 为0(按字节清空);
        return g; 
}
void  graph_insert(graph_t *g)          //插入顶点数据
{
	int i;
	for(i=0; i<N; i++)
	{
		printf("v%d>>",i);
		scanf("%d",&(g->data[i]));
	}
	getchar();
    return ;
}
void graph_input(graph_t *g)           //输入定点关系
{
	int i,j;
	while(scanf("(v%d,v%d)",&i,&j)==2) 	//输入关系
	{
		getchar();		//无向图对称
		g->matrix[i][j]=g->matrix[j][i]=1;
	}
}
void graph_show(graph_t *g)       //打印关系
{
	int i,j;
	printf("  v0 v1 v2 v3 v4 \n");
	for(i=0;i
	{
		printf("v%d",i);
		for(j=0;j
		{
		printf(" %d ",g->matrix[i][j]);
		}
		putchar(10);
	}
}
int main(int argc, const char *argv[])
{
	graph_t *g=graph_create();
	graph_insert(g);
	graph_input(g);
	graph_show(g);
	return 0;
}

图的深度搜索

图的广度搜索

#include "BFS.h"
#include "link_queue.h"
//创建空的图
graph_t *graph_create()
{
    //开辟空间
	graph_t *g=(graph_t *)malloc(sizeof(graph_t));
	//清空g指向的起始地址到偏移量为sizeof(graph_t)大小 为0(按字节清空);
	memset(g,0,sizeof(graph_t));
    return g;
}
//插入顶点数据
void  graph_insert(graph_t *g)
{
	int i;
	for(i=0;i<N; i++)
	{
		printf("v%d>>",i);
		scanf("%d",&(g->data[i]));
	}
	getchar();
	return ;
}
//输入定点关系
void graph_input(graph_t *g)
{
	int i,j;
	//输入关系
	while(scanf("%d,%d",&i,&j)==2)
	{	
		//无向图对称
		getchar();
		g->matrix[i][j]=g->matrix[j][i]=1;
	}
}
//打印关系
void graph_show(graph_t *g)
{
	int i,j;
	printf("  v0 v1 v2 v3 v4 \n");
	for(i=0;i
	{
		printf("v%d",i);
		for(j=0;j
		{
		printf(" %d ",g->matrix[i][j]);
		}
		putchar(10);
	}
}
//查找下一个邻接点
//下标值为[0]-[N-1]
//int v为当前节点
//j为下一个邻接点
int graph_find_next(graph_t *g,int v)
{
	int j;
	for(j=0;j
	{
		if(g->matrix[v][j]==1&&visited[j]==0)
		{
			return j;
		}
	}
	return -1;
}
//广度优先搜索
void  graph_BFS(graph_t *g,int v)//起始顶点的下标
{
	linkqueue_t *lq=linkqueue_create();
	int val,i;
	linkqueue_in(lq,v);
    visited[v]=1;
	while(linkqueue_empty(lq)!=1)
	{
		val=linkqueue_out(lq);
		printf(" v%d ",val);
		for(i=0;i
		{
			if(g->matrix[val][i]==1&&visited[i]==0)
			{
				linkqueue_in(lq,i);	
				visited[i]=1;  //标记下标为v的顶点已经被访问
			}
		}
	}
}
int main(int argc, const char *argv[])
{
	graph_t *g=graph_create();
	graph_insert(g);
	graph_input(g);
//	graph_DFS(g,0);
	putchar(10);
//	graph_show(g);
	graph_BFS(g,0);	 
	return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值