第1章、概述
1.1数据结构
1.1.1例一、数据组织
如何在书架上摆放书本?
方法一:随便放
- 操作1:新书怎么插入?
- 哪里有空位就插哪里,一步到位
- 操作2:怎么找指定的书?
- 累死
方法二:拼音放
按照书名的配音字母顺序摆放
-
操作1:新书怎么插入?
- 需要把空位移开再插入,比较累
-
操作2:怎么查找指定的书?
- 二分查找法!
方法三:划分区域
将书架划分为几个区域,每个区域摆放特定类别的书,在每个类别里面,按照书名的拼音字母顺排放
- 操作1:
- 先定类别,二分查找确定位置,再移出空位
- 操作2:
- 先定类别,再二分查找
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BnTioRJV-1666602897695)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220915142303070.png)]
1.1.2例二:空间使用
写程序实现一个函数PrintN,使得传入一个正整数N的参数以后,可以顺序打印1到N的全部正整数。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mZYSW75K-1666602897697)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220915141024208.png)]
-
在10,100,1000时,循环实现的代码和递归实现的代码都可以跑出来,但是在10000时,递归代码空间占用过大,非正常中止————报错
-
#include <iostream> #include <string> using namespace std; void Print1(int N) { for (int i = 0; i < N; i++) { cout << "N = " << i + 1 << " "; } } void Print2(int N) { if (N) { Print2(N - 1); cout << "N = " << N << " "; } } int main() { cout << "请输入您要打印的数据:"; int num; cin >> num; int select; cout << "请输入您选择的函数:(1/循环 2/递归)"; cin >> select; switch (select){ case 1: Print1(num); break; case 2: Print2(num); break; default: cout << "输入有误!" << endl; break; } system("pause"); return 0; }
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p1Ln0nGz-1666602897699)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220915142353757.png)]
1.1.3例三:算法效率
写程序计算给定多选择在给定点x处的值
方法一:直接表示法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SS4GHXZK-1666602897700)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220915143417000.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jwtw2Ue7-1666602897701)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220915143720387.png)]
#include <iostream>
#include <string>
#include <time.h>
clock_t start, stop;
#define MAX 10
#define MAXK 1e7
using namespace std;
double f1(int n, double a[], double x) {
int i = 0;
double p = a[0];
for (int i = 1; i <= n; i++) {
p += (a[i] * pow(x, i));
}
return p;
}
double f2(int n, double a[], double x) {
double p = a[n];
for (int i = n; i > 0; i++) {
p = a[i - 1] + p * x;
return p;
}
}
int main() {
double time;
double a[MAX];
for (int i = 0; i < MAX; i++) a[i] = (double)i;
start = clock();
for (int i = 0; i < MAXK; i++) {
f1(MAX - 1, a, 1.1);
}
stop = clock();
time = ((double)(stop - start)) / CLK_TCK / MAXK;
cout << "f1:\nticks = " << ((double)(stop - start)) << "\t time = " << time << endl;
start = clock();
for (int i = 0; i < MAXK; i++) {
f2(MAX - 1, a, 1.1);
}
stop = clock();
time = ((double)(stop - start)) / CLK_TCK / MAXK;
cout << "f2:\nticks = " << ((double)(stop - start)) << "\t time = " << time << endl;
system("pause");
return 0;
}
方法二:巧用结合律
思路:从里面向外面实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sUKhv2M7-1666602897703)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220915143810133.png)]
补充:clock()
-
clock():捕捉从程序开始运行到clock()被调用时所耗费的时间,这个时间单位是clock tick,即"时间打点"
-
常数CLK_TCK:机器时钟每秒所走的时钟打点数。
-
#include <iostream> #include <string> #include <time.h> using namespace std; clock_t start, stop; //clock_t 是clock()函数返回变量的类型 void Myfunction() {} int main() { double duration; //记录被测函数运行时间 //不在测试范围内的准备工作,写在之前 start = clock(); Myfunction(); stop = clock(); duration = ((double)(stop - start)); system("pause"); return 0; }
1.1.4 抽象数据类型
1.4.1说明
- 数据类型
- 数据对象集
- 数据集合相关联的操作集
- 抽象:描述数据类型的方法不依赖于具体实现
- 与存放数据的机器无关
- 与数据存储的物理结构无关
- 与实现操作的算法和编程语言无关
只描述数据对象集和相关操作集**“是什么”,并不涉及"如何做到"**的问题。
1.4.2例四:矩阵抽象数据类型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZYHPNesG-1666602897704)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220915152446229.png)]
1.2算法
1.2.1算法的定义
算法(Algorithm)
例一:选择排序算法
void SelectionSort(int List[], int N){
//将N给整数List[0]……List[N-1]进行非递减排序
for(i = 0; i < N; i ++){
Minpostition = ScanForMin(List, i ,N-1);
//在List[i]到List[i-1]中寻找最小元,将其位置赋值给MinPosition
Swap(List[i], List[Minpostition]);
//将未排序部分的最小元放入有序部分的最后位置
}
}
1.2.2什么是好算法
空间复杂度
时间复杂度
1.2.3复杂度的渐近表示法
1.2.3.1概念说明
- 上界
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ER73ZsXD-1666602897705)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220915155551741.png)]
- 下界
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RsYhYEBy-1666602897706)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220915155606281.png)]
- 既是上界又是下界
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-75URa6wa-1666602897707)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220915155619688.png)]
1.2.3.2复杂度分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5l1qBSyi-1666602897708)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220915160135133.png)]
1.3应用示例———最大子列和
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BwH9XIuX-1666602897709)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220915160415627.png)]
算法一
#include <iostream>
#include <string>
#include <time.h>
clock_t start, stop;
#define N 1000
#define NUM 1
using namespace std;
int MaxSub1(int a[], int n) {
int sum, MaxNum = 0;
int i, j, k;
for ( i = 0; i < n; i++) {
for ( j = i; j < n; j++) {
sum = 0;
for (k = i; k <= j; k++) {
sum += a[k];
if (sum > MaxNum)
MaxNum = sum;
}
}
}
return MaxNum;
}
int main() {
int a[N] = { -1,2,5,6,-13,5,-2,9,13,8,10 ,-1,-8,3,-6};
double time;
start = clock();
for (int y = 0; y < NUM; y++) {
MaxSub1(a, N);
}
//int MaxNum = MaxSub1(a, N);
stop = clock();
//cout << "MaxSub1 = " << MaxNum << endl;
cout << "time = " << ((double)(stop - start) / CLK_TCK / NUM) << endl;
system("pause");
return 0;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lnFyl4ZA-1666602897710)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220915212805675.png)]
算法二
#include <iostream>
#include <string>
#include <time.h>
clock_t start, stop;
#define N 10000
#define NUM 100
using namespace std;
int MaxSub1(int a[], int n) {
int sum, MaxNum = 0;
int i, j;
for (i = 0; i < n; i++) {
sum = 0;
for (j = i; j < n; j++) {
sum += a[j];
if (sum > MaxNum) {
MaxNum = sum;
}
}
}
return MaxNum;
}
int main() {
int a[N] = { -1,2,5,6,-13,5,-2,9,13,8,10 ,-1,-8,3,-6 };
double time;
start = clock();
for (int y = 0; y < NUM; y++) {
MaxSub1(a, N);
}
//int MaxNum = MaxSub1(a, N);
stop = clock();
//cout << "MaxSub1 = " << MaxNum << endl;
cout << "time = " << ((double)(stop - start) / CLK_TCK / NUM) << endl;
system("pause");
return 0;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cPBLZkDT-1666602897711)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220915221203570.png)]
算法三:分而治之
#include <iostream>
#include <string>
#include <time.h>
using namespace std;
#define N 10000
#define M 10000
clock_t start, stop;
int Max(int A, int B, int C) {
return A > B ? A > C ? A : C : B > C ? B : C;
}
int SumDivid(int List[], int Left, int Right) {
if (Left == Right) {
if (List[Left] < 0) {
return 0;
}
else {
return List[Left];;
}
}
int Center = (Left + Right) / 2;
int LeftMax, RightMax;
LeftMax = SumDivid(List, Left, Center);
//cout << "LeftMax = " << LeftMax << endl;
RightMax = SumDivid(List, Center + 1, Right);
//cout << "RightMax = " << RightMax << endl;
//定义左右边界
int LeftBorderSum, RightBorderSum;
int LeftBorderMax, RightBorderMax;
//左边界扫描
LeftBorderMax = LeftBorderSum = 0;
for (int i = Center; i > Left; i--) {
LeftBorderSum += List[i];
if (LeftBorderSum > LeftBorderMax) {
LeftBorderMax = LeftBorderSum;
}
}
//右边界扫描
RightBorderMax = RightBorderSum = 0;
//注意!这里并不是左边界的CV,Center需要+1
for (int i = Center + 1; i < Right; i++) {
RightBorderSum += List[i];
if (RightBorderSum > RightBorderMax) {
RightBorderMax = RightBorderSum;
}
}
return Max(LeftMax, RightMax, LeftBorderMax + RightBorderMax);
}
int main() {
int list[N] = { -1,2,5,6,-13,5,-2,9,13,8,10,-1,-8,3,-6,9,-12,2,5,7,2,-6 };
int MaxSum = SumDivid(list, 0, N);
cout << "已知int类型的数组list{-1,2,5,6,-13,5,-2,9,13,8,10,-1,-8,3,-6,9,-12,2,5,7,2,-6…………}\n\t最大子列和为:" << MaxSum << endl;
start = clock();
for (int i = 0; i < M; i++) {
SumDivid(list, 0, N);
}
stop = clock();
double ALLtime = (double)(stop - start) / CLK_TCK;
double time = (double)(stop - start) / CLK_TCK / M;
cout << "本次共运行" << M << "次数据,每个数据包含" << N << "个数字!" << endl;
cout << "总运行时间:" << ALLtime << "(秒)\t单次运行时间:" << time << "(秒)" << endl;
system("pause");
return 0;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V0ihsuu9-1666602897713)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220916154450964.png)]
算法四:在线处理
#include <iostream>
#include <string>
#include <time.h>
clock_t start, stop;
#define N 1000
#define NUM 1000000
using namespace std;
int MaxSub1(int a[], int n) {
int sum, MaxNum;
int i;
sum = MaxNum = 0;
for (i = 0; i < n; i++) {
sum += a[i];
if (sum > MaxNum) {
MaxNum = sum;
}
else if (sum < 0) {
sum = 0;
}
}
return MaxNum;
}
int main() {
int a[N] = { -1,2,5,6,-13,5,-2,9,13,8,10 ,-1,-8,3,-6 };
double time;
start = clock();
for (int y = 0; y < NUM; y++) {
MaxSub1(a, N);
}
//int MaxNum = MaxSub1(a, N);
stop = clock();
//cout << "MaxSub1 = " << MaxNum << endl;
cout << "time = " << ((double)(stop - start) / CLK_TCK / NUM) << endl;
system("pause");
return 0;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ncNIyF8P-1666602897714)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220915220722330.png)]
第2章、线性结构
2.1 引子:多项式表示
2.1.1、多项式的说明
1、【例】多项式及运算
-
一元多项式
-
主要运算:多项式的加减乘除等
2、【分析】表示多项式
- 多项式的关键数据
- 多项式的项数:
- 各项系数: 指数:
2.1.2、表示方法
(1)顺序存储结构直接表示
(2)顺序存储结构表示非零项
- 按照指数的大小有序存储!
(3)链表结构存储非零项
2.2线性表及顺序存储
2.2.1线性表的定义
- 线性表:是由同类型数据元素构成的,有序序列的线性结构
- 表中元素的个数称为线性表的长度
- 线性表没有元素时,称为空表
- 线性表起始位置叫表头,表结束的位置叫表尾
2.2.2 线性表的抽象数据类型
2.2.3 线性表的顺序存储
2.3顺序表的定义与typedef
2.3.1、静态和动态顺序表
- 静态的顺序表(采用定长数组的形式进行存储),动态的顺序表(使用动态开辟的数组进行存储)
- struct A 和 typedef struct A - 英雄与侠义的化身 - 博客园 (cnblogs.com)
//静态
#define MaxSize 10
//typedef————数据类型重命名
typedef struct {
string data[MaxSize];
int length;
}SqList;
void InitList(SqList &L) {
/*for (int i = 0; i < MaxSize; i++) {
L.data[i] = "";
}*/
L.length = 0;
}
#define INITSIZE 10
typedef struct {
string *data;
int MaxSize;
int Lengh;
}SeqList;
void InitList(SeqList &L) {
L.data = (string *)malloc(INITSIZE * sizeof(string));
L.Lengh = 0;
L.MaxSize = INITSIZE;
}
2.3.2、动态申请和释放内存
void* calloc(size_t num, size_t size);
int* p = (int *)calloc(10, sizeof(int));
-
函数的功能是为num个大小为size的元素开辟一款空间,并且把空间的每个字节初始化为0。
-
与函数malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为全0。
2.3.3、顺序表的特定
2.3.4、顺序线性表的主要操作的实现
//静态分配顺序表
#include <iostream>
#include <string>
using namespace std;
#define MaxSize 10
typedef struct {
string data[MaxSize];
int length;
}SqList;
//初始化
void InitList(SqList &L) {
/*for (int i = 0; i < MaxSize; i++) {
L.data[i] = "";
}*/
L.length = 0;
}
//打印
void PrintList(SqList &L) {
if (L.length <= 0) {
cout << "此顺序表不存在!" << endl;
return;
}
for (int i = 0; i < L.length; i++) {
cout << L.data[i] << "---";
}
cout << endl;
}
//任意位置插入
void InsertList(SqList &L, int num, string elem) {
if (L.length >= MaxSize) {
cout << "该顺序表已经超过最大长度!" << endl;
return;
}
if (num<1 || num>L.length + 1) {
cout << "插入位置有误,应该在0到" << L.length + 1 << "之间进行插入" << endl;
}
for (int j = L.length; j >= num; j--) {
L.data[j] = L.data[j - 1];
}
L.data[num - 1] = elem;
L.length++;
}
//修改
void ChangeList(SqList &L) {
if (L.length == 0) {
cout << "该顺序表为空,无法进行修改" << endl;
return;
}
int pos;
string ele;
cout << "请输入您要修改的位置:";
cin >> pos;
cout << "请输入您要修改的元素:";
cin >> ele;
if (pos<1 || pos>L.length) {
cout << "修改位置超出顺序表范围,请重新输入" << endl;
return;
}
L.data[pos - 1] = ele;
}
//删除
void DeleteList(SqList &L,string &ele) {
int pos;
cout << "请输入您要删除元素的位置:";
cin >> pos;
ele = L.data[pos - 1];
if (pos<1 || pos>L.length) {
cout << "删除位置有误,无法删除!" << endl;
return;
}
for (int i = pos - 1; i < L.length; i++) {
L.data[i] = L.data[i + 1];
}
L.length--;
}
//查找——查找第一个元素为elem的元素并且返回其次序
int FindList(SqList &L, string elem) {
if (L.length <= 0) {
cout << "此表为空表!" << endl;
}
for (int i = 0; i < L.length; i++) {
if (L.data[i] == elem) return i + 1;
}
return 0;
}
//销毁
void DestoryList(SqList &L) {
if (L.length <= 0) {
cout << "此顺序表为空,无法销毁!" << endl;
return;
}
L.length = 0;
//L.data = NULL;
cout << "此顺序表已经被销毁" << endl;
free(L.data);
}
int main() {
SqList L;
InitList(L);
int Num;
cout << "请选择您要插入数据的次数:";
cin >> Num;
for (int i = 0; i < Num; i++) {
string ele;
cout << "请输入您要插入的数据:";
cin >> ele;
int pos;
cout << "请输入您要插入的位置:";
cin >> pos;
InsertList(L, pos, ele);
}
PrintList(L);
ChangeList(L);
PrintList(L);
string ele = " ";
DeleteList(L, ele);
cout << "删除的元素ele为:" << ele << endl;
PrintList(L);
cout << "请输入您要查找的元素:";
string element;
cin >> element;
int pos = FindList(L, element);
cout << "元素" << element << "在顺序表的位置为" << pos << endl;
DestoryList(L);
//PrintList(L);
system("pause");
return 0;
}
//动态分配顺序表
#include <iostream>
#include <string>
using namespace std;
#define INITSIZE 4 //顺序表初始长度
typedef char ElemType;
typedef struct {
ElemType *data;
int Lengh;
int MaxSize;
}SqList;
//辅助函数
bool Is_Full(SqList *L) {
if (L->Lengh >= L->MaxSize) {
return true;
}
return false;
}
bool Is_Empty(SqList *L) {
if (L->Lengh <= 0) {
return true;
}
return false;
}
void Print_List(SqList *L) {
if (Is_Empty(L)) {
cout << "该表为空,无法打印!" << endl;
return;
}
for (int i = 0; i < L->Lengh; i++) {
cout << "data[" << i << "] = " << L->data[i] << " ";
}
cout << endl;
}
int Find_List(SqList *L, ElemType elem) {
if (Is_Empty(L)) {
cout << "此顺序表为空,无法查找元素" << endl;
return -1;
}
for (int i = 0; i < L->Lengh; i++) {
if (L->data[i] == elem) {
return i + 1;
}
}
return -1;
}
//操作函数
void Init_List(SqList *L) {
L->data = (ElemType *)malloc(sizeof(ElemType)*INITSIZE);
if (!L->data) {
cout << "L->data 内存分配失败!" << endl;
return;
}
L->MaxSize = INITSIZE;
L->Lengh = 0;
/*cout << "最大容量为:" << L->MaxSize << endl;
cout << "顺序表长为:" << L->Lengh << endl;*/
}
void Insert_List(SqList *L, ElemType ele, int pos) {
if (pos<1 || pos>L->Lengh + 1) {
cout << "插入位置有误!" << endl;
return;
}
if (Is_Full(L)) {
/*cout << "最大容量为:" << L->MaxSize << endl;
cout << "顺序表长为:" << L->Lengh << endl;*/
L->data = (ElemType *)realloc(L->data, sizeof(ElemType)*(L->MaxSize + L->MaxSize));
cout << "扩容中……" << endl;
system("pause");
L->MaxSize += L->MaxSize;
/*cout << "最大容量为:" << L->MaxSize << endl;
cout << "顺序表长为:" << L->Lengh << endl;*/
}
for (int i = L->Lengh; i > pos - 1; i--) {
L->data[i] = L->data[i - 1];
}
L->data[pos - 1] = ele;
L->Lengh++;
}
void Delete_List(SqList *L,ElemType *ele, int pos) {
if (Is_Empty(L)) {
cout << "该顺序表为空,无法删除元素!" << endl;
}
if (pos<1 || pos>L->Lengh + 1) {
cout << "删除位置有误!" << endl;
return;
}
*ele = L->data[pos - 1];
for (int i = pos - 1; i < L->Lengh; i++) {
L->data[i] = L->data[i + 1];
}
L->Lengh--;
}
void Change_List(SqList *L, ElemType ele, int pos) {
if (Is_Empty(L)) {
cout << "此顺序表为空,无法进行修改" << endl;
return;
}
if (pos<1 || pos>L->Lengh + 1) {
cout << "修改位置有误!" << endl;
return;
}
L->data[pos - 1] = ele;
}
void Print_Pos(SqList *L, ElemType elem) {
int pos = 0;
pos = Find_List(L, elem);
if (pos >= 1) {
cout << "元素" << elem << "在顺序表中的位置为:" << pos << endl;
return;
}
cout << "找不到元素" << elem << endl;
}
void Destory_List(SqList *L) {
free(L->data);
L->Lengh = 0;
L->MaxSize = 0;
}
int main() {
SqList L;
Init_List(&L);
cout << " - - - - - - 元素插入 - - - - - - " << endl;
Insert_List(&L, 'G', 4);
Insert_List(&L, 'F', 1);
Insert_List(&L, 'L', 2);
Insert_List(&L, 'I', 2);
Insert_List(&L, 'D', 1);
Print_List(&L);
cout << endl;
cout << " - - - - - - 元素删除 - - - - - - " << endl;
ElemType *ele = new ElemType();
Delete_List(&L, ele, 3);
cout << "被删除的元素是:" << *ele << endl;
Print_List(&L);
cout << endl;
cout << " - - - - - - 元素插入 - - - - - - " << endl;
Insert_List(&L, 'O', 2);
Insert_List(&L, 'X', 3);
Insert_List(&L, 'Z', 3);
Print_List(&L);
cout << endl;
cout << " - - - - - - 元素修改 - - - - - - " << endl;
Change_List(&L, 'M', 5);
Change_List(&L, 'M', 3);
Change_List(&L, 'M', 1);
Print_List(&L);
cout << endl;
cout << " - - - - - - 元素查找 - - - - - - " << endl;
int pos;
Print_Pos(&L, 'Y');
Print_Pos(&L, 'M');
Print_Pos(&L, 'L');
cout << endl;
cout << " - - - - - - 元素销毁 - - - - - - " << endl;
Destory_List(&L);
Print_List(&L);
system("pause");
return 0;
}
2.4线性表的链式存储
- 不要求逻辑上相邻的元素物理上也相邻,通过"链建立起数据元素之间的逻辑关系
- 插入、删除不需要移动数据元素,只需要修改"链"。
2.4.1 单链表
2.4.1.1 单链表的定义
typedef struct LNode {//LNode 结点
ElemType Data;//数据域
struct LNode *next;//指针指向下一个结点
}LNode,*LinkList;
//重命名前
struct LNode *p = (struct LNode *) malloc(sizeof(struct LNode));
//重命名后
typedef————数据类型重命名
typedef struct LNode LNode;
LNode *p = (LNode *) malloc(sizeof(LNode));
2.4.1.2 注意事项
- 使用LinkList强调这个是一个单链表
- 使用LNode*强调这是一个结点
2.4.1.3 两种单链表
//不带头节点单链表
typedef struct LNode {
int Data;
LNode* Next;
}LNdoe, *LinkList;
void Init_List(LinkList &L) {//如果不用&表示修改的是复制体
L = NULL;//空表,暂时没有结点,防止有脏数据
}
bool Is_Empty(LinkList &L) {
return (L == NULL);
}
//带头节点单链表
typedef struct LNode {
int *Data;
LNode *next;
}LNode, *LinkList;
void Init_List(LinkList &L) {
L = (LNode *)malloc(sizeof(LNode));//分配一个头节点
if (L == NULL) {
cout << "分配失败" << endl;
return;
}
L->next = NULL;//头节点之后暂时没有其他结点
}
//判断带头节点单链表是否为空
bool Is_Empty(LinkList &L) {
return (L->next == NULL);
}
2.4.1.4 单链表的其他操作
#include <iostream>
#include <string>
using namespace std;
typedef char ElemType;
typedef struct LNode {
ElemType data;
LNode *next;
}LNode, *LinkList;
void Init_List(LinkList &L) {
L = (LNode *)malloc(sizeof(LNode));
if (L == NULL) {
cout << "头节点分配失败" << endl;
}
L->next = NULL;
}
bool Is_Empty(LinkList &L) {
return (L->next == NULL);
}
//在指定结点后插入数据
bool Push_Back_LNode_List(LNode *p, ElemType ele) {
if (p == NULL)
return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
if (s == NULL){//内存分配失败
cout << "内存分配失败" << endl;
return false;
}
s->data = ele;
s->next = p->next;
p->next = s;
return true;
}
//在指定位置后插入数据
bool Push_Back_List(LinkList &L, ElemType ele, int pos) {
if (pos < 1)
return false;
LNode * p;//定义一个结点,表示当前结点的位置
int j = 0;
p = L;//当前结点位于头节点
while (p != NULL && j < pos - 1) {
p = p->next;
j++;
}
return Push_Back_LNode_List(p, ele);
}
//在指定结点前插入数据
bool Push_Hand_LNode_List(LNode *p, ElemType ele) {
if (p == NULL)
return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
if (s == NULL) {
cout << "内存分配失败" << endl;
return false;
}//内存分配失败
s->next = p->next;
p->next = s;
s->data = p->data;
p->data = ele;
return true;
}
//在指定位置前插入数据
bool Push_Hand_List(LinkList &L, ElemType ele, int pos) {
if (pos < 1)
return false;
LNode * p;//定义一个结点,表示当前结点的位置
int j = 0;
p = L;//当前结点位于头节点
if (pos == 1 ) {
return Push_Back_LNode_List(p, ele);
}
while (p != NULL && j < pos - 1) {
p = p->next;
j++;
}
return Push_Hand_LNode_List(p, ele);
}
//打印链表
void Print_List(LinkList &L) {
LNode *p;
p = L;
while (p->next != NULL) {
p = p->next;
cout << "--" << p->data << "--";
}
cout << endl;
}
//删除指定位置的数据
bool Delete_List(LinkList &L, int pos) {
if (pos < 1) {
cout << "删除位置有误!" << endl;
return false;
}
int j = 0;
LNode *p;
p = L;
while (p != NULL && j < pos - 1) {
j++;
p = p->next;
}
if (p == NULL) {
//cout << "删除失败" << endl;
return false;
}
if (p->next == NULL) {
//cout << "删除失败" << endl;
return false;
}
LNode *q = p->next;
p->next = q->next;
free(q);
return true;
}
//打印删除的数据
void Print_Delete_List(LinkList &L, int pos) {
//Delete_List(L, pos);
int jude = Delete_List(L, pos);
if (jude >= 1) {
cout << "删除成功:" << endl;
Print_List(L);
return;
}
cout << "删除失败:" << endl;
Print_List(L);
}
//查找数据
LNode * Find_List(LinkList &L, ElemType ele) {
LNode *p;
p = L;
while (p->next != NULL) {
p = p->next;
if (p->data == ele) {
return p;
}
}
return NULL;
}
//修改数据
bool Change_List(LinkList &L, ElemType ele1, ElemType ele2) {
LNode *p;
p = L;
while (p->next != NULL) {
p = p->next;
if (p->data == ele1) {
p->data = ele2;
return true;
}
}
return false;
}
void Print_Change_List(LinkList &L, ElemType ele1, ElemType ele2) {
bool a = Change_List(L, ele1, ele2);
if (a >= 1){
Print_List(L);
}
else{
cout << "修改失败" << endl;
}
}
LNode* Reverse_List(LinkList &L) {
LinkList temp;
Init_List(temp);
LNode *p;
p = L;
while (p->next != NULL) {
p = p->next;
Push_Hand_List(temp, p->data, 1);
}
return temp;
}
void Creat_List(LinkList L) {
char c = ' ';
cout << "请输入您要插入的元素(!退出程序):" << endl;
cin >> c;
while (c != '!') {
Push_Hand_List(L, c, 1);
cout << "请再次输入您创建的元素:" << endl;
cin >> c;
}
cout << "创建成功!" << endl;
system("pause");
system("cls");
}
int main() {
LinkList L;
Init_List(L);
cout << " - - - - - - - 创建操作 - - - - - - - " << endl;
Creat_List(L);
Print_List(L);
cout << endl;
cout << " - - - - - - - 插入操作 - - - - - - - " << endl;
Push_Hand_List(L, 'c', 1);
Push_Hand_List(L, 'b', 1);
Push_Hand_List(L, 'F', 2);
Push_Hand_List(L, 'J', 1);
Push_Hand_List(L, 'O', 1);
Push_Hand_List(L, 'P', 1);
Push_Hand_List(L, 'K', 1);
Print_List(L);
cout << endl;
cout << " - - - - - - - 删除操作 - - - - - - - " << endl;
Print_Delete_List(L, 3);
Print_Delete_List(L, 8);
Print_Delete_List(L, 4);
cout << endl;
cout << " - - - - - - - 查找操作 - - - - - - - " << endl;
if (Find_List(L, 'K') == NULL) {
cout << "未找到元素" << endl;
}
else{
cout << "找到元素:" << Find_List(L, 'K')->data << endl;
}
cout << endl;
cout << " - - - - - - - 修改操作 - - - - - - - " << endl;
Print_Change_List(L, 'J', 'Q');
cout << endl;
cout << " - - - - - - - 逆转操作 - - - - - - - " << endl;
LinkList temp = Reverse_List(L);
Print_List(temp);
system("pause");
return 0;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GbdTWqIU-1666602897715)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220923113746935.png)]
2.4.2 双链表
2.4.2.1双链表与单链表的比较
- 单链表:无法逆向进行检索,有时候处理起来不太方便
- 双链表:可以顺序检索也可以逆向检索,存储密度低,操作方便
2.4.2.2双链表的定义
typedef char ElemType;
typedef struct DNode {
ElemType data;
struct DNode *piror, *next;
}DNode, *DLinkList;
2.4.2.3双链表的初始化
//初始化
bool Init_DNode(DLinkList &L) {
//分配一个头节点
L = (DNode *)malloc(sizeof(DNode));
if (L == NULL) {
cout << "内存不足,分配空间失败" << endl;
}
//头节点的next域暂时没有其他结点
L->next = NULL;
//头节点的piror域永远指向NULL
L->piror = NULL;
cout << "双向链表初始化成功!" << endl;
return true;
}
//判断是否为空
bool Is_Empty(DLinkList L) {
if (L->next == NULL)
return true;
else
return false;
}
2.4.2.4双链表的其他操作
#include <iostream>
#include <string>
using namespace std;
typedef char ElemType;
//链表的定义
typedef struct DNode{
ElemType data;
DNode *piror, *next;
}DNode, *DLinkList;
//链表的初始化
bool Init_DList(DLinkList &L) {
L = (DNode *)malloc(sizeof(DNode));
//判断有没有初始化成功
if (L == NULL) {
cout << "分配空间失败,双向链表初始化失败!" << endl;
return false;
}
L->piror = NULL;
L->next = NULL;
cout << "双向链表初始化成功!" << endl;
return true;
}
//后插法
bool Push_Back_DList(DLinkList &L, ElemType ele, int pos) {
DNode *p, *s;
p = L;
s = (DNode *)malloc(sizeof(DNode));
if (s == NULL) {
cout << "分配内存失败" << endl;
return false;
}
int i = 0;
while (p != NULL && i < pos) {
p = p->next;
i++;
}
if (p == NULL) {
cout << "插入失败!" << endl;
return false;
}
if (p->next != NULL)
s->next = p->next;
else
s->next = NULL;
s->piror = p;
p->next = s;
s->data = ele;
cout << "插入成功" << endl;
return true;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hOOZ0wTy-1666602897717)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220923132250497.png)]
//删除
void Delete_DList(DLinkList &L, int pos) {
DNode *p, *q;
p = L;
int i = 0;
while (p != NULL && i < pos) {
p = p->next;
i++;
}
q = p->next;
if (p == NULL)
cout << "删除失败!" << endl;
if (p->next == NULL)
p->piror->next = NULL;
else
p->piror->next = p->next;
p->next->piror = p->piror;
cout << "删除成功" << endl;
}
//遍历
void Print_DList(DLinkList &L) {
DNode *p = L;
while (p->next!= NULL) {
cout << "---" << p->next->data << "---";
p = p->next;
}
cout << endl;
}
//main函数
int main() {
DLinkList L;
Init_DList(L);
Push_Back_DList(L, 'H', 0);
Push_Back_DList(L, 'J', 3);
Push_Back_DList(L, 'G', 1);
Push_Back_DList(L, 'O', 1);
Push_Back_DList(L, 'U', 3);
Push_Back_DList(L, 'K', 2);
Print_DList(L);
Delete_DList(L, 3);
Print_DList(L);
system("pause");
return 0;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uPbQI787-1666602897718)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220923131713866.png)]
2.5循环链表
2.4.3.1循环单链表的定义
- 单链表:表尾结点的next指针指向NULL
- 从一个结点出发只能找到后继结点不能找到前驱结点
- 循环单链表:表尾结点的next指针指向头节点
- 从一个结点出发可以找到其他任何一个结点
2.4.3.2循环单链表的初始化
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-47iBKOwD-1666602897719)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220923135751136.png)]
2.4.3.3 循环双链表的定义
- 双链表:表尾结点的next域指向NULL,表头结点的prior域指向NULL。
- 循环双链表:表尾结点的next域指向头节点,表头结点的piror域指向表尾结点。
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kcFjtpHf-1666602897720)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220923140324591.png)]
2.4.3.4循环双链表的初始化
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J0n9zrur-1666602897721)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220923140429231.png)]
2.4.3.5关于循环链表的插入和删除问题
- 因为循环链表的尾部结点都会指向循环链表的头节点而不是指向空,所以在循环链表中对数据进行插入和删除时,不需要将尾部的next域置为空!
2.6静态链表
2.7顺序表和链表的比较
2.7.1 逻辑结构
- 顺序表和链表都是线性结构,同属于线性表。
2.7.2存储结构
- 顺序表
- 支持随机存取,存储密度高
- 大片连续空间分配不方便,改变容量较为困难
- 链表
- 离散的小口径分配方便,改变容量容易
- 不可以进行数据存储,存储密度低
2.7.3 基本操作
- 线性表的创建
- 顺序表:需要预分大片的连续空间,如果分配的空间国小,不方便以后的扩展容量,若分配空间过大则会造成内存资源的浪费。
- 链表:只需要分配一个头节点或者声明一个指针,之后方便扩展
- 线性表的销毁
- 顺序表:修改Length=0
- 静态分配:系统自动回收空间
- 动态分配:需要手动进行free
- 链表:依次删除各个结点free
- 顺序表:修改Length=0
- 线性表的增删
- 顺序表:插入/删除数据都需要将后续元素后移/前移
- 链表:只需要查找目标元素,修改相关指针即可
- 线性表的查找
- 顺序表:按位查找O(1),按值查找O(N),若表内元素有序则位O(logN)
- 链表:按位查找O(N),按值查找O(N)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KMrw5BUM-1666602897722)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220923144246519.png)]
2.8广义表与多重链表
第3章、栈和队列
3.1栈的初识
3.1.1栈的基本概念
3.1.1.1栈的定义
- 栈(Stack)是一种只允许在一端进行插入或者删除操作的线性表。
- 栈的逻辑结构域普通的线性表无异,但是数据的插入和删除与普通线性表略有区别。
3.1.1.2栈的特点
- 后进先出 Last In First Out (LIFO)
3.1.1.3栈的基本操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2dHDWWx6-1666602897723)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220924172143289.png)]
3.1.2栈的顺序存储
#include <iostream>
#include <string>
using namespace std;
#define MaxSize 10
typedef char ElemType;
typedef struct {
ElemType data[MaxSize];
int top;
}Stack;
void Init_Stack(Stack &s) {
s.top = -1;
cout << "栈的初始化成功!" << endl;
}
void Push_Stack(Stack &s) {
ElemType ele;
cout << "请输入您要入栈的元素(!表示停止入栈):";
cin >> ele;
while (ele != '!'){
if (s.top >= MaxSize - 1) {
cout << "栈内元素已满,无法入栈!" << endl;
return;
}
s.data[++s.top] = ele;
cout << "入栈成功" << endl;
cout << "请输入您要入栈的元素(!表示停止入栈):";
cin >> ele;
}
return;
}
void Pop_Stack(Stack &s) {
ElemType ele;
cout << "按任意字符继续出栈,按!停止出栈:";
cin >> ele;
while (ele != '!') {
if (s.top < 0) {
cout << "当前已经为空栈,无法出栈!" << endl;
return;
}
s.top--;
cout << "出栈成功,请继续" << endl;
cout << "按任意字符继续出栈,按!停止出栈:";
cin >> ele;
}
}
void Print_Stack(Stack &s) {
if (s.top < 0) {
cout << "此栈为空!" << endl;
return;
}
cout << "栈顶元素为:" << s.data[s.top] << endl;
}
int main() {
Stack s;
Init_Stack(s);
Print_Stack(s);
Push_Stack(s);
Print_Stack(s);
Pop_Stack(s);
Print_Stack(s);
system("pause");
return 0;
}
3.1.3栈的链式存储
3.1.3.1带头结点的初始化
bool Init_Stack(LiStack &S) {
S = (LinkNode *)malloc(sizeof(LinkNode));
if (S == NULL) {
cout << "链栈初始化失败!" << endl;
return false;
}
S->next = NULL;
}
3.1.3.2不带头结点的初始化及相关操作
#include <iostream>
#include <string>
using namespace std;
typedef char ElemType;
typedef struct LinkNode{
ElemType data;
LinkNode *next;
}LinkNode, *LiStack;
void Init_Stack(LiStack &S) {
S = NULL;
}
bool Push_Stack(LiStack &S) {
ElemType ele;
cout << "请输入您要插入的数据(!停止插入):";
cin >> ele;
while (ele != '!'){
LinkNode *p;
p = S;
S = (LinkNode *)malloc(sizeof(LinkNode));
if (S == NULL) {
cout << "False" << endl;
return false;
}
S->next = p;
S->data = ele;
cout<<"插入成功,输入您要插入的数据(!停止插入):";
cin >> ele;
}
return true;
}
void Pop_Stack(LiStack &S) {
ElemType ele;
LinkNode *p = new LinkNode();
cout << "输入任意字符删除数据(!停止删除):";
cin >> ele;
while (ele != '!') {
p = S;
if (p == NULL) {
cout << "Stack is Empty!" << endl;
return;
}
S = p->next;
//free(p);
cout << "输入任意字符删除数据(!停止删除):";
cin >> ele;
}
}
void Print_Stack(LiStack &S) {
if (S == NULL) {
cout << "Stack is NULL!" << endl;
return;
}
cout << "The Top Of Stack Is:" << S->data << endl;
}
int main() {
LiStack S;
Init_Stack(S);
Print_Stack(S);
Push_Stack(S);
Print_Stack(S);
Pop_Stack(S);
Print_Stack(S);
system("pause");
return 0;
}
3.1.4 共享栈
3.2队列的初识
3.2.1队列的基本概念
3.2.1.1队列的定义
- 队列(Queue)是一种允许在一端进行插入,在另外一端进行删除的线性表
3.2.1.2队列的特点
- 队列的特点是:先进先出 Fist in Fist Out (FIFO)
3.2.1.3队列的基本操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IKOSZyRu-1666602897724)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220924195112401.png)]
3.2.2队列的顺序实现
#include <iostream>
#include <string>
using namespace std;
#define MaxSize 10
typedef char ElemType;
typedef struct{
ElemType data[MaxSize];
int front;//队头
int rear;//队尾
}QList;
void Init_Queue(QList &Q) {
Q.front = 0;
Q.rear = 0;
}
void Push_Queue(QList &Q, ElemType ele) {
if (Q.rear - Q.front >= MaxSize) {
cout << "队列已满无法入队!" << endl;
return;
}
Q.rear++;
Q.data[Q.rear%MaxSize] = ele;
}
void Pop_Queue(QList &Q) {
if (Q.front == Q.rear) {
cout << "队列为空,无法出队!" << endl;
return;
}
Q.front++;
}
void Print_Queue(QList &Q) {
if (Q.front == Q.rear) {
cout << "队列为空,无法输出" << endl;
return;
}
cout << Q.data[Q.front%MaxSize + 1] << endl;
}
int main() {
QList Q;
Init_Queue(Q);
Print_Queue(Q);
Push_Queue(Q, 'J');
Push_Queue(Q, 'A');
Push_Queue(Q, 'K');
Push_Queue(Q, 'E');
Push_Queue(Q, 'R');
Print_Queue(Q);
Pop_Queue(Q);
Print_Queue(Q);
Pop_Queue(Q);
Print_Queue(Q);
Pop_Queue(Q);
Print_Queue(Q);
Pop_Queue(Q);
Print_Queue(Q);
system("pause");
return 0;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yy2qHDdm-1666602897725)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220924201853906.png)]
3.2.3循环队列
//一般的队列
//进队
Q.rear++;
Q.data[Q.rear%MaxSize] = ele;
//循环队列
//进队
Q.data[Q.rear] = ele;
Q.rear = (Q.rear + 1)%MaxSize;
//出队
Q.front =(Q.front + 1) %MaxSize;
//队满
1、(Q.rear + 1)%MaxSize == Q.front;
//队空
Q.rear == Q.front;
3.2.4队列的链式实现
3.2.4.1链队的声明
//声明及初始化
typedef char ElemType;
typedef struct LNode {
ElemType data;
LNode * next;
}LNode;
typedef struct {
LNode *front;
LNode *rear;
}LinkQueue;
//带头结点链队的初始化
void Init_Queue(LinkQueue &Q) {
//申请一片空间,将front和rear都指向头结点
Q.rear = Q.front = (LNode *)malloc(sizeof(LNode));
Q.front->next = NULL;
}
//不带头结点链队的初始化
void Init_Queue(LinkQueue &Q) {
Q.front = NULL;
Q.rear = NULL;
}
3.2.4.2链队及其相关操作
#include <iostream>
#include <string>
using namespace std;
typedef char ElemType;
typedef struct LNode {
ElemType data;
LNode * next;
}LNode;
typedef struct {
LNode *front;
LNode *rear;
}LinkQueue;
void Init_Queue(LinkQueue &Q) {
//申请一片空间,将front和rear都指向头结点
Q.rear = Q.front = (LNode *)malloc(sizeof(LNode));
Q.front->next = NULL;
}
bool Is_Empty_QLink(LinkQueue &Q) {
if ((int)Q.front == (int)Q.rear)
//cout << "Y" << endl;
return true;
//cout << "N" << endl;
return false;
}
//入队
void Push_QLink(LinkQueue &Q) {
ElemType ele;
cout << "请输入入队的元素(!退出入队操作):";
cin >> ele;
LNode *s;
while (ele != '!') {
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = ele;
s->next = NULL;
Q.rear->next = s;//将新的结点插入到rear之后
Q.rear = s;//修改rear的指针
cout << "请输入入队的元素(!退出入队操作):";
cin >> ele;
}
}
void Pop_QLink(LinkQueue &Q) {
ElemType ele;
cout << "按任意元素出队(!退出出队操作)";
cin >> ele;
while (ele != '!') {
if (Is_Empty_QLink(Q)){
/*cout << "Q.rear = " << (int)Q.rear << endl;
cout << "Q.front = " << (int)Q.front << endl;*/
cout << "Is Empty" << endl;
return;
}
LNode *p;
//LNode * p = (LNode *)malloc(sizeof(LNode));
p = Q.front->next;
Q.front->next = p->next;
if (Q.rear == p)
Q.rear = Q.front;
free(p);
cout << "按任意元素出队(!退出出队操作)";
cin >> ele;
}
}
void Print_QLink(LinkQueue &Q) {
if (Is_Empty_QLink(Q)) {
cout << "Queue is empty can't Print" << endl;
return;
}
cout << "队列元素是:" << Q.front->next->data<< endl;
}
int main() {
LinkQueue Q;
Init_Queue(Q);
Push_QLink(Q);
Print_QLink(Q);
Pop_QLink(Q);
Print_QLink(Q);
system("pause");
return 0;
}
3.2.5双端队列
3.2.5.1定义
- 双端队列是允许从两端插入,两端删除的线性表。
3.2.5.2双端队列的其他变种
3.2.5.3例一、输出顺序的合法性
【问题】若数据元素输入序列1、2、3、4,则哪些输出序列是合法的,哪些是非法的
3.3栈和队列的应用
3.3.1栈在括号匹配中的问题
3.3.1.1说明
3.3.1.2具体过程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OL666zhv-1666602897727)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220925113337205.png)]
3.3.1.3代码实现
3.3.2栈在表达式求值的应用
-
前缀表达式————波兰表达式
-
后缀表达式————逆波兰表达式
3.3.2.1中缀、后缀、前缀
3.3.2.2中缀转后缀
3.3.2.2.1中缀转后缀手算
- 中缀表达式转后缀的手算方法
- ①确定中缀表达式
- ②选择下一个运算符,按照的方式组合成一个新的操作数
- ③如果还有运算符没有被处理,继续第二步
- 可以保证运算顺序的唯一
-
15 7 1 1 + - \ 3 * 2 1 1 + + - //5
3.3.2.2.2中缀转后缀计算
- 后缀表达式的计算:
- ①从左往右扫描下一个元素,直到处理完所有元素
- ②若扫描到操作上则压入栈,并且回到①;否则执行③
- 若扫描到运算符,则弹出两个栈顶元素,执行相关操作,运算结果返回栈顶,回到①
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dYW8r6vX-1666602897728)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220925143714630.png)]
3.3.2.3中缀转前缀
3.3.2.3.1中缀转前缀手算
- :
- ①确定中缀表达式中
- ②选择下一个运算符,按照的方法组合成一个新的操作数
- ③如果还有运算符没有被处理就继续②
- .
3.3.2.3.2中缀转前缀机算
- ①扫描下一个元素,直到处理完所有元素
- 若扫描到操作上则压入栈,并且回到步骤①,否则执行步骤③
- 若扫描到运算符,则弹出两个栈顶元素,执行相关操作,运算结果压回栈顶,回到①
3.3.2.4中缀的计算(用栈实现)
- 中缀表达式的计算:
- 初始化两个栈,和
- 若扫描到操作上,压入操作上栈
- 若扫描到运算符或者界限符,则按照"中缀转后缀"相同的逻辑压入运算符栈(期间也会运算符,)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ryjOEvX0-1666602897729)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20220925144445892.png)]
//main.cpp
#include <iostream>
#include <ctype.h>
#include <math.h>
using namespace std;
#include "Calculator.h"
int main()
{
Calculator dstk(20); //声明一个对象,此时进行初始化.构造了容量为20的栈s
dstk.Push('#'); //初始时将'#'压入栈中
dstk.InfixToPostfix(dstk); //将中缀表达式转换为后缀表达式
dstk.Pop(); //删除遗留在栈s中的#号,使栈s用于计算后缀表达式用
dstk.Eval(dstk); //计算(后缀)达式的值
dstk.OutPut(dstk); //输出表达式结果
return 0;
}
//Calculator.h
enum ResultCode{Underflow,Overflow,MissingOperand,DivideByZero};
//计算器类
class Calculator
{
public:
Calculator(int mSize); //构造函数
bool IsEmpty(){return (top==-1);}
bool IsFull(){return (top==maxSize);}
int isp(char c); //栈内优先级函数
int icp(char c); //栈外优先级函数
void Push(const char &x); //向栈中添加非数值(如运算符等)元素函数,中缀表达式转后缀时
void Pop(); //删除栈顶元素
int Top(); //返回栈顶元素
void InfixToPostfix(Calculator &dstk); //将中缀表达式转换为后缀表达式函数
void Eval(Calculator &dstk); //运行计算器函数
void Clear(Calculator &dstk); //清除计算器
void OutPut(Calculator &dstk); //输出计算结果
private:
int top,maxSize;
int *s; //栈S
void PushOperand(char x); //后缀表达式中,使操作数x进栈
void PushCalcuRes(int result); //使两个操作数的运算结果进栈
bool GetOperands(int &x,int &y,Calculator &dstk); //取出栈顶两个操作数x和y
void DoOperator(char op,Calculator &dstk); //取出栈顶两个操作数x和y,执行运算y<op>x
};
Calculator::Calculator(int mSize)
{ //构造函数的实现
maxSize=mSize;
s=new int[maxSize];
top=-1;
}
int Calculator::isp(char c)
{
//计算运算符c的栈内优先级
int priority;
switch(c)
{
case '(':priority=0;break;
case '+':
case '-':priority=5;break;
case '*':
case '/':priority=6;break;
case '#':priority=0;break;
}
return priority;
}
int Calculator::icp(char c)
{
//计算运算符c的栈内优先级
int priority;
switch(c)
{
case '(':priority=8;break;
case '+':
case '-':priority=5;break;
case '*':
case '/':priority=6;break;
case '#':priority=0;break;
}
return priority;
}
void Calculator::Push(const char &x)
{ //使运算符进栈
if(IsFull())
throw Overflow;
s[++top]=x; //新元素进栈,一定要注意前缀加与后缀加的区别
}
void Calculator::Pop()
{
//删除栈顶元素
if(IsEmpty())
throw Underflow;
top--;
}
int Calculator::Top()
{
//返回栈顶元素
if(IsEmpty())
throw Underflow;
return s[top];
}
void Calculator::InfixToPostfix(Calculator &dstk)
{
//将中缀表达式转换为后缀表达式
char ch,y;
cout<<"请输入你要计算的表达式,以#号结束:";
while(cin>>ch,ch!='#')
{
if(isdigit(ch)||isalpha(ch))
cout<<ch;
else if(ch==')')
for(y=dstk.Top(),dstk.Pop();y!='(';y=dstk.Top(),dstk.Pop())
cout<<y;
else
{
for(y=dstk.Top();icp(ch)<=isp(dstk.Top());dstk.Pop())
cout<<y;
dstk.Push(ch); //当前运算符进栈
}
}
while ((dstk.IsEmpty()==false)&&(dstk.Top()!='#'))
{
y=dstk.Top();
dstk.Pop();
cout<<y;
}
cout<<endl;
}
void Calculator::Eval(Calculator &dstk)
{
//运行计算器函数
char m; //注意,这里是字符类型的,同样可以运算,只是是转换为对应的ASCII参与运算的(我在这里犯了错误)
cout<<"请输入上面计算下来的后缀表达式:"<<endl;
while (cin>>m,m!='#')
{
switch(m)
{
case '+':
case '-':
case '*':
case '/':
case '^':
DoOperator(m,dstk);
break;
default:
//cout<<"需要进栈的元素为:"<<m<<endl; //为了测试需要进栈的元素而加。纯属调试之用
dstk.PushOperand(m); //使操作数m进栈
//cout<<"而实际进栈的元素为:"<<s[top]<<endl; //为了测试实际进栈的元素而加。纯属调试之用
}
}
}
void Calculator::Clear(Calculator &dstk)
{
//清除计算器
dstk.Clear(dstk);
}
void Calculator::OutPut(Calculator &dstk)
{
int result;
result=dstk.Top();
cout<<"所计算的表达式的值为:"<<result<<endl;
}
void Calculator::PushOperand(char m)
{
//操作数m进栈,注意,此时m是个字符,m的值是它对应的ASCII码的值(在机器内的16进制表示)
int t=m;//此时计算时用的是m的ASCII码的值,不过在机器内是用16进制表示的,这里是将其转换为10进制表示,并赋给t
if(IsFull())
throw Overflow;
t=m-48; //将其转换为对应的整型数(描述可能有点不妥),然后进栈
s[++top]=t;
}
void Calculator::PushCalcuRes(int result)
{
if(IsFull())
throw Overflow;
s[++top]=result;
}
bool Calculator::GetOperands(int & x,int &y,Calculator &dstk)
{
//取出栈顶两个操作数x和y,执行运算y<op>x
if(dstk.IsEmpty())
throw MissingOperand; //抛出下溢异常
else
{
x=dstk.Top(); //返回栈顶元素并将其转换为其对应的ASCII码值的16进制表示,类型的强制转换
dstk.Pop();
}
if(dstk.IsEmpty())
throw MissingOperand;
else
{
y=dstk.Top(); //返回栈顶元素并将其转换为其对应的ASCII码值的16进制表示,将栈内的整型数进行
dstk.Pop();
}
return true;
}
void Calculator::DoOperator(char op,Calculator &dstk)
{
//计算(后缀)表达式的值
bool result;
int x,y;
result=GetOperands(x,y,dstk); //获取两个操作数
if(result) //判断是否获取成功
switch(op)
{
case '+':dstk.PushCalcuRes(y+x);break;
case '-':dstk.PushCalcuRes(y-x);break;
case '*':dstk.PushCalcuRes(y*x);break;
case '/':
if(x==0)
throw DivideByZero;
else
dstk.PushCalcuRes(y/x);
break;
case '^':dstk.PushCalcuRes(pow(y,x));break;
}
else
Clear(dstk);
}
#include <iostream>
#include <string>
//判断是不是数字的一个函数库
#include <ctype.h>
using namespace std;
typedef double ElemType1;
typedef char ElemType2;
//创建数栈
typedef struct NumNode {
ElemType1 data;
NumNode* next;
}NumNode, *NLinkStack;
//创建符栈
typedef struct CharNode {
ElemType2 data;
CharNode* next;
}CharNode, *CLinkStack;
//带头结点的数栈初始化
void Init_NumStack(NLinkStack &N) {
N = (NumNode *)malloc(sizeof(NumNode));
if (N == NULL) {
cout << "数栈分配内存失败!" << endl;
return;
}
N->data = NULL;
N->next = NULL;
cout << "数栈初始化成功" << endl;
}
//带头结点的符栈初始化
void Init_CharStack(CLinkStack &C) {
C = (CharNode *)malloc(sizeof(CharNode));
if (C == NULL) {
cout << "符栈分配内存失败!" << endl;
return;
}
C->data = NULL;
C->next = NULL;
cout << "符栈初始化成功" << endl;
}
//元素优先级
int Priority_Char(char ele) {
int pri;
switch (ele){
case '+': {
pri = 5;
break;
}
case '-': {
pri = 5;
break;
}
case '*': {
pri = 6;
break;
}
case '/': {
pri = 6;
break;
}
case '=': {
pri = 0;
break;
}
}
return pri;
}
//算术运算
int MyOperator(double n1, double n2, char c) {
switch (c){
case'+':
return n2 + n1;
break;
case'-':
return n2 - n1;
break;
case'*':
return n2 * n1;
break;
case'/':
if (n2 > (n2 / n1)*n1) {
return (n2 / n1) + 1;
}
return n2 / n1;
break;
}
}
//判断数栈是否为空
bool Is_Num_Empty(NLinkStack &N) {
if (N->next == NULL)
return true;
return false;
}
//判断符栈是否为空
bool Is_Char_Empty(CLinkStack &C) {
if (C->next == NULL)
return true;
return false;
}
//数栈栈顶元素
int Top_NumStack(NLinkStack &N) {
NumNode *p;
p = N;
if (Is_Num_Empty(N)) {
cout << "数栈栈顶元素为空!" << endl;
return 0;
}
//cout << "数栈栈顶元素为:" << p->next->data << endl;
return p->next->data;
}
//符栈栈顶元素
char Top_CharStack(CLinkStack &C) {
CharNode *p;
p = C;
if (Is_Char_Empty(C)) {
cout << "符栈栈顶元素为空!" << endl;
return ' ';
}
//cout << C->next->data << endl;
return p->next->data;
}
//将数字压入数栈
void Push_NumStack(NLinkStack &N, ElemType1 ele) {
NumNode *p1;
p1 = N;
p1 = (NumNode *)malloc(sizeof(NumNode));
if (p1 == NULL) {
cout << "p1的内存分配失败" << endl;
return;
}
p1->data = ele;
p1->next = N->next;
N->next = p1;
}
//将运算符压入符栈
void Push_CharStack(CLinkStack &C, ElemType2 ele) {
CharNode *p2;
p2 = C;
p2 = (CharNode *)malloc(sizeof(CharNode));
if (p2 == NULL) {
cout << "p2的内存分配失败" << endl;
return;
}
p2->data = ele;
p2->next = C->next;
C->next = p2;
cout << "元素" << ele << "入栈成功!" << endl;
}
//符栈出栈
char Pop_CharStack(CLinkStack &C) {
if (Is_Char_Empty(C)) {
cout << "空栈" << endl;
return ' ';
}
CharNode * p;
p = C->next;
char ch;
ch = p->data;
C->next = p->next;
free(p);
return ch;
}
//数栈出栈
int Pop_NumStack(NLinkStack &N) {
if (Is_Num_Empty(N)) {
cout << "空栈" << endl;
return 0;
}
NumNode * p;
p = N->next;
int num;
num = p->data;
N->next = p->next;
free(p);
return num;
}
//入栈元素为数字
void Push_Number(NLinkStack &N, int ele) {
Push_NumStack(N, ele);
cout << "元素" << ele << "入栈成功!" << endl;
}
//运算
void Operator_Num_Char(CLinkStack &C, NLinkStack &N, ElemType2 ele) {
ElemType1 num1, num2;
ElemType2 ch;
num1 = Pop_NumStack(N);
num2 = Pop_NumStack(N);
ch = Pop_CharStack(C);
cout << "表达式为:" << num2 << ch << num1 << endl;
//将运算结果压入数据栈
Push_Number(N, MyOperator(num1, num2, ch));
cout << "运算结果为:" << Top_NumStack(N) << endl;
cout << endl;
}
//将输入的数据分别压入数栈和符栈
void Push_Stack(NLinkStack &N, CLinkStack &C) {
cout << "请输入您要计算的表达式(以=!结尾):" << endl;
ElemType2 ele;
while (cin >> ele, ele != '!') {
if (isdigit(ele)) {
//入栈元素为数字
Push_Number(N, ele - '0');
}
else {
//判断即将入栈元素和栈内元素优先级
if (!Is_Char_Empty(C)) {
if (ele != '=') {//若表达式未输完
while (!Is_Char_Empty(C) && Priority_Char(ele) <= Priority_Char(Top_CharStack(C))) {
Operator_Num_Char(C, N, ele);
}
Push_CharStack(C, ele);
}
else {//若表达式输完,且符栈不为空
while (!Is_Char_Empty(C)) {
Operator_Num_Char(C, N, ele);
}
}
}
else{
Push_CharStack(C, ele);
}
}
}
//输出最终结果
cout << Top_NumStack(N) << endl;
return;
}
int main() {
NLinkStack N;
CLinkStack C;
//初始化
Init_NumStack(N);
Init_CharStack(C);
Push_Stack(N, C);
/*Top_CharStack(C);
Top_NumStack(N);*/
system("pause");
return 0;
}
3.3.3栈在递归中的应用
1)函数调用背后的过程
2)例一
- 太多次递归可能会导致栈溢出!
3.3.4队列的应用—树的层次遍历
【旧版】3.3.4_队列的应用__树的层次遍历_————哔哩哔哩_bilibili
第4章、串
4.1初识字符串
4.1.1串的定义和基本操作
4.1.1.1串的定义
定义
-
串,即,是由零个或者多个字符组成的有序序列,一般记为
-
其中,S是,单引号括起来的字符序列是串的值,ai可以是字母,数字或者其他字符;串中字符的个数n称为。n=0时的串为(用表示)。
专业术语
- 子串:串中任意个连续的字符组成的子序列
- 主串:包含子串的串
- 字符在主串的位置:字符在串中的序号
- 子串在主串的位置:子串的第一个字符在主串的位置
注意事项
-
空串和空格串不是一个东西
M = ''; //M为空串 N = ' ' //N是由三个空格字符组成的字符串,占3B
-
在字符串中,位序是从1开始的而不是从0开始
4.1.1.2串和线性表
- 串是一种特殊的线性表,数据元素之间呈线性关系;
- 串的数据对象限定为字符集,而线性表可以是各种数据类;
- 串的基本操作,如增删改查。
4.1.1.3串的基本操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hzsrAAv9-1666602897731)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221001160657316.png)]
4.1.2串的存储结构
4.1.2.1串的顺序存储
//定长的顺序存储
#define MAXLEN 255//预定义最大串长为255
typedef struct {
char ch[MAXLEN];//每个分量存储一个字符
int length;//串的实际长度
}SString;
//动态
typedef struct {
char *ch;
int length;
}HString;
HString S;
S.ch = (char *)malloc(sizeof(char));//需要手动free
S.length = 0;
4.1.2.2串的链式存储
//一般方法
typedef struct StringNode {
char ch;
StringNode *next;
}StringNode, *String;
//串的存储密度低,每个字符1B,而每个指针4B
//一般方法
typedef struct StringNode {
char ch[4];
StringNode *next;
}StringNode, *String;
//存储密度提高
4.1.2.3串的相关操作
//串的定义
typedef struct {
char ch[MAXLEN];
int lengh;
}SString;
//求子串,返回串S第pos起字符串长度为len的子串
void SubString(SString &Sub, SString S, int pos, int len) {
//子串范围越界
if (pos + len - 1 > S.lengh) {
cout << "子串范围越界" << endl;
return;
}
for (int i = pos; i < pos + len; i++)
Sub.ch[i - pos + 1] = S.ch[i];
Sub.lengh = len;
}
//比较两个字符串:T1>T2,返回值>0否则<=0
int StrCompare(SString T1, SString T2) {
for(int i = 0; i <= T1.lengh&&i <= T2.lengh; i++) {
if (T1.ch[i] != T2.ch[i])
return T1.ch[i] - T2.ch[i];
}
//若扫描过的所有字符都相同,那么长度更大的串更大
return T1.lengh - T2.lengh;
}
//定位操作,若主串S存在与串T相同的子串,则返回在主串中第一次出现的位置,否则返回0
int Index(SString S, SString T) {
int i = 1, n = S.lengh, m = T.lengh;
//用于暂存子串
SString Sub;
while (i <= n - m + 1) {
SubString(Sub, S, i, m);
if (StrCompare(Sub, T) != 0)++i;
else return i;
}
return 0;
}
//生成一个值为串常量chars的串T
bool StrAssign(SString &T, char *chars){
int i;
if(!T.ch)//若存在脏数据,释放内存
free(T.ch);
for(i = 0; !*chars; ++i, ++chars){
T.ch = (char *)malloc(i*sizeof(char));
if(T.ch==NULL)
return false;
}
for(int i = 0; j<i ;i++)
T.ch[i] = chars[i];
T.lengh = i;
return true;
}
4.2串的相关算法
4.2.1 串的朴素模式匹配算法
4.2.1.1模式匹配算法
-
在
主串
中找到与模式串
相同的子串
,并且返回其所在位置 -
主要思想:
- 将主串中与模式串长度相同的子串分离出来,挨个与模式串进行比较,如果子串与模式串某个对应字符不匹配时,立即放弃当前子串,转而检索下一个子串
-
若模式串长m,主串长n,直到匹配成功/失败最多需要匹配⭐️
(n-m+1)*m
次比较🔴
最坏的时间复杂度:O(nm)
-
缺点:
-
朴素模式匹配算法当子串与模式串仅部分匹配时👿会经常回溯,导致时间开销增加。
-
4.2.1.2朴素模式匹配
int Find_Sub_Pos(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;
return 0;
}
4.2.2 KMP算法
4.2.2.1说明
- 具体思路:主串指针步回溯,
只有模式串的指针才会回溯!
4.2.2.2代码说明
int 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;//匹配成功
return 0;
}
}
4.2.2.3求模式串的next数组
-
next数组:当模式串的第j个字符匹配失败时,令
模式串跳到next[j]
再继续匹配 -
串的**
前缀
**:包含第一个字符,且不包含最后一个字符的子串。 -
串的**
后缀
**:包含最后一个字符,且不包含第一个字符的子串。 -
当第j个字符匹配失败,由前
1~j-1
个字符组成的字符串记为S,那么**next[j] = S的前缀与后缀最长相等的长度+1
,特别地❗️next[1]=0
❗️**
4.2.2.3.4 next数组练习题
① 模式串:ababaa
序号J | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
模式串 | a | b | a | b | a | a |
next[j] | 0 | 1 | 1 | 2 | 3 | 4 |
②模式串:aaaab
序号j | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
模式串 | a | a | a | a | b |
next[j] | 0 | 1 | 2 | 3 | 4 |
4.2.2.3.5KMP算法性能分析
❗️KMP算法平均时间复杂度为:O(n+m)
❗️
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zP8cp4sP-1666602897733)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221001201336638.png)]
4.2.3 KMP算法的优化—nextval数组
4.2.3.1 KMP算法存在的问题
- 当出现
不匹配的位置
时,若该位置的元素
和字符串头部的元素
相同,就会出现一次没有意义的对比。
4.2.3.2 解决方法
-
当出现上述问题时,❗️
直接将该位置的元素的值设置为字符串头元素值
。 -
因为根据KMP算法的原理:
-
if (next[j]==0){
i++;
j++;
}
-
- 从左往右依次确定nextval的值
- 默认next val[1]=0
- 原本next[2]=1,但是序号为2的字符和序号1一样,所以,next val[2]=next val[1]
- 同理next val[3]=next val[2]
- next val[4]=next val[3]
- 第五个字符和第四个字符不相等,所以我们让next val[5]=next[5]
4.2.3.3next val数组求法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o67egR4F-1666602897734)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221001203937720.png)]
第5章、树与二叉树
5.1树的初识
5.1.1树的定义
5.1.1.1树的基本概念
- 空树:结点数为0的树
- 非空树的特点❗️
- 🌲有且仅有一个根结点
- 🌲没有后继的结点称为“叶子结点”(或终端结点)
- 🌲有后继的结点称为“分支结点”(或非终端结点)
- 🌲
除了根结点没有前驱外,其他结点
,有且仅有一个前驱
❗️
5.1.1.2结点、树的属性描述
- 结点的属性
- 结点的层次——从上往下数,从根开始定义,根为第一层,根的孩子为第二层。
- 树的高度/深度——树中结点的最大层次
- //结点的高度——从下往上数
结点的度
——该结点有几个孩子(分支)
- 数的属性
- 树的高度(深度)——一个多少层
树的度
——各结点的度的最大值
5.1.1.3有序树和无序树
- 有序树:从逻辑上看树中结点的各子树从左往右是有次序的,不能互换。
- 有序树:从逻辑上看树中结点的各子树从左往右是无次序的,不能互换。
5.1.1.4树和森林
- 森林:森林是由m(m>=0)棵互不相交的树的集合。
5.1.2树的性质
-
考点1:
-
考点2:度为m的树、m叉树的区别
-
考点3
-
考点4
-
考点5
-
考点6
等比数列求和公式:
5.2二叉树
5.2.1二叉树的定义和基本概念
5.2.1.1二叉树的基本概念
-
二叉树是n (n>=0) 个结点的有限集合
①或者为空二叉树,即n = 0
②或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成,左子树和右子树分别是一颗二叉树。
-
特点
①每个结点至多有两颗子树
②左右子树不能颠倒(二叉树是有序树)
③
度为2的有序树和二叉树不是一个东西
🌲二叉树是递归定义的数据结构
5.2.1.2二叉树的五种状态
5.2.1.3几个特殊的二叉树
满二叉树
。一颗高度为h,且含有个结点的二叉树
特点
- 只有最后一层存在叶子结点
- 不存在度为1的结点
- 按层序从1开始编号,结点为i的左孩子为
2i
,右孩子为2 i+1
,结点i的符结点为[i/2]
(向下取整)
完全二叉树
。当且仅当其每个结点都与高度为h的满二叉树中编号为1~n的结点一一对应时,称为完全二叉树
特点
- 只有最后两层可能有叶子结点
- ❗️
最多只有一个度为1的结点
❗️ - 按层序从1开始编号,结点为i的左孩子为
2i
,右孩子为2 i+1
,结点i的符结点为[i/2]
(向下取整) - i<=[n/2]为分支结点,i>[n/2]为叶子结点
- 如果完全二叉树的某一个结点有孩子的话,那一定是左孩子
二叉排序树
。一颗二叉树或者空二叉树,或者是具有如下性质的二叉树
- **
左子树
上的所有结点的关键字均小于根结点
**的关键字 - **
右子树
上的所有结点的关键字均大于根结点
**的关键字 - 左子树和右子树又分别是一颗二叉排序树
平衡二叉树
。树上的任意结点的左子树和右子树的深度之差不超过1。平衡二叉树有更高的搜索效率。
5.2.2常见的性质&考点
5.2.2.1 二叉树的考点
- 考点1:在二叉树中,叶子结点比二分支结点多一个。即
- 假设树中所有的结点总数为N,度为0的结点n0,为1的结点n1,为2的结点n2
- 那么N的数量则为所有的结点相加,即N=n0+n1+n2
- 除了根结点以外,所有结点都有一个入度,设B为入度总数则,N=B+1
- 又因为这些入度都是有出度为1、2的结点得来的,即B=n1+2*n2
- 即N=n1+2*n2+1且N=n0+n1+n2===>n0=n2+1
-
考点2:二叉树第i层至多有个结点; m叉树第i层最多有个结点
-
考点3:高度为h的二叉树最多有个结点(满二叉树)
5.2.2.2 完全二叉树的考点
-
考点1:具有n个(n>0)结点的
完全二叉树的高度为
-
考点2:对于
完全二叉树
,可以由结点数n推出度为0、1、2的结点个数n0、n1、n2 -
若2i>n,结点i无左孩子,否则结点i左孩子为2i;若2i+1>n,结点i无左孩子,否则结点i左孩子为2i+1
5.2.3二叉树的存储结构
5.2.3.1二叉树的顺序存储
定义一个长度为Max_Size的数组t,按照从上至下,从左往右的顺序依次存储**完全二叉树
**的各个结点
#define Max_Size 100
typedef char ElemType;
struct TreeNode {
//这里我们用二维数组实现,其实也可以用一维数组实现,就应该value,value==0表示结点为空
ElemType value;//结点中的数据元素
bool IsEmpty;//结点是否为空
};
TreeNode t[Max_Size];
void Init_Tree(TreeNode *t) {
//初始化时将所有结点标记为空
for (int i = 0; i < Max_Size; i++) {
t[i].IsEmpty= true;
}
}
- 二叉树的顺序存储结构中,一定要把二叉树的结点编号和完全二叉树对应起来,否则不容易找到某个结点的左、右孩子,父节点等信息。
- 二叉树的顺序存储会浪费大量空间,因此实际应用很少用顺序存储存储二叉树
- 二叉树的顺序存储只适合存储完全二叉树
5.2.3.2二叉树的链式存储
🌟n个结点的二叉链表共有n+1个空链域,可以将之用于构造线索二叉树
。
typedef int ElemType;
typedef struct BiTNode {
ElemType data;//数据域
BiTNode * LChild, *RChile;//左、右孩子指针
BiTNode * Parent;//有父节点指针叫三叉链表,否则叫二叉链表
}BiTNode, *BiTree;
//定义空树
void Init_Tree(BiTree &root) {
root = NULL;
}
//插入根结点
void Insert_root(BiTree &root) {
root = (BiTree)malloc(sizeof(BiTNode));
root->data = 1;
root->LChild = NULL;
root->RChile = NULL;
}
//插入新的结点
void Insert_Node(BiTree &root) {
BiTNode *p = (BiTNode *)malloc(sizeof(BiTNode));
if (p == NULL) {
cout << "结点分配失败" << endl;
return;
}
p->data = 2;
p->LChild = NULL;
p->RChile = NULL;
root->LChild = p;
}
//三叉链表————方便寻找父结点
typedef struct BiTNode {
ElemType data;//数据域
BiTNode * LChild, *RChile;//左、右孩子指针
BiTNode * Father;//父节点指针
}BiTNode, *BiTree;
5.3二叉树的遍历
5.3.1二叉树的先中后序遍历
5.3.1.1三种遍历方法
- 先序遍历(先根遍历):根左右(
N
LR) - 中序遍历(中根遍历):左根右(L
N
R) - 后序遍历(后根遍历):左右根(LR
N
)
5.3.1.2练习题
先序遍历: | A-B-D-E-C-F-G |
---|---|
中序遍历: | D-B-E-A-F-C-G |
后序遍历: | D-E-B-F-G-C-A |
先序遍历 | A-B-D-G-E-C-F |
---|---|
中序遍历 | D-G-B-E-A-F-C |
后序遍历 | G-D-E-B-F-C-A |
三种遍历方法 | 表达式 |
---|---|
先序遍历 | -+a*b-cd/ef |
中序遍历(需要加界限符) | a+b*c-d-e/f |
后序遍历 | abcd-*+ef/- |
5.3.1.3代码实现
//准备条件
#include <iostream>
#include <string>
using namespace std;
typedef char ElemType;
typedef struct BiTNode{
ElemType data;
BiTNode * LChild, *RChile;
}BiTNode, *BiTree;
//访问根结点的函数
void Visit(BiTree T){
cout<<T->data<<"--";
}
//先序遍历
void PreOrder(BiTree T) {
if (T != NULL) {
//先访问根结点
Visit(T);
//递归遍历左子树
PreOrder(T->LChild);
//递归遍历右子树
PreOrder(T->RChile);
}
}
//中序遍历
void MidOrder(BiTree T) {
if (T != NULL) {
//递归遍历左子树
PreOrder(T->LChild);
//访问根结点
Visit(T);
//递归遍历右子树
PreOrder(T->RChile);
}
}
//后序遍历
void MidOrder(BiTree T) {
if (T != NULL) {
//递归遍历左子树
PreOrder(T->LChild);
//递归遍历右子树
PreOrder(T->RChile);
//访问根结点
Visit(T);
}
}
5.3.1.4先序遍历递归图
5.3.1.5求树的深度
//求树的深度
int TreeDepth(BiTree T) {
if (T == NULL)
return 0;
else {
int L = TreeDepth(T->LChild);
int R = TreeDepth(T->RChile);
return L > R ? L + 1 : R + 1;
}
}
5.3.2二叉树的层序遍历
5.3.2.1基本思想
- 初始化一个辅助队列
- 根结点入队
- 判断队列是否为空,若非空则将队头元素出队,并且访问该结点,将其左、右孩子入队
- 重复步骤2和3直到队列为空
5.3.2.2代码
void LevelOrder(BiTree T){
//创建一个队列
LinkQueue Q;
Init_Queue(Q);//初始化队列
BiTree p;
Push_Queue(Q,T);//根结点入队
while(!Is_Empty_Queue(Q)){//判断队列是否为空,若非空则入队
Pop_Queue(Q, p);//队头元素出队
Visit(p); //访问该结点
if(p->LChild!=NULL)
Push_Queue(Q,p->LChlid);//左孩子入队
if(p->RChild!=NULL)
Push_Queue(Q,p->LChild);//右孩子入队
}
}
5.3.3由遍历序列构造二叉树
一个遍历序列可能对应多种二叉树的形态,那么给定一个遍历序列怎么确定它的二叉树呢?这就是本节要研究的问题。
5.3.3.1三种方法
说明
- 若仅给定一种遍历序列我们无法确定一颗二叉树,但是如果给定两种遍历序列我们就很好确认了
5.3.3.2前序+中序遍历序列
前序序列
:根结点—> 左子树的前序遍历序列 —>右子树的前序遍历序列
中序序列
:左子树的中序遍历序列 —> 根结点 —> 右子树的中序遍历序列
例子
-
前序序列先根结点,所可道D是根节点,由中序序列可知EAF是左子树,HCBGI为右子树
-
先看左子树,由前序可知左树的根结点为A,又中序可知,E为左,F为右
-
再看右边,由前序可知,B为根,由中序可知HC为左,GI为右
-
由前序可知C为根,且前序和中序中CH位置不一样,所以H为左
-
由前序知G为根,前前序中序GI位置一致,所以I为右
5.3.3.3后序+中序
后序序列
:左子树的前序遍历序列 —>右子树的前序遍历序列 —> 根结点
中序序列
:左子树的中序遍历序列 —> 根结点 —> 右子树的中序遍历序列
例子
5.3.3.4层序+中序
层序序列
:根结点 —>左子树的根 —> 右子树的根 ……
中序序列
:左子树的中序遍历序列 —> 根结点 —> 右子树的中序遍历序列
例子
5.3.4线索二叉树的概念
如何找到在指定结点p在中序遍历序列中的前驱?
思路
- 从根结点出发,重新进行一次中序遍历,指针q记录当前访问的结点,指针pre记录上一个被访问的结点
- 当q==p时,pre为p前驱
void InOrder(BiTree T){
if(T!=NULL){
InOrder(T->lchild);
visit(T);
InOrder(T->rchild);
}
}
//访问结点q
void Visit(BiTNode *q){
if(q==p)
final=pre;//若访问结点为p,则找到前驱
else
pre=q;//否则,pre指向当前访问的结点
}
5.3.4.1中序线索二叉树
5.3.4.2线索二叉树存储结构
//二叉树的结点
typedef int ElemType;
typedef struct BiTNode {
ElemType data;//数据域
BiTNode * LChild, *RChile;//左、右孩子指针
}BiTNode, *BiTree;
//线索二叉树结点
typedef int ElemType;
typedef struct ThreadNode {
ElemType data;//数据域
ThreadNode * LChild, *RChile;//左、右孩子指针
int Ltag,Rtag;//左右线索标志
}ThreadNode, *ThreadNode;
- tag==0时,标明指针指向的是孩子
- tag==1时,标明指针指向的是"线索"
- Ltag表示指向前驱
- Rtag表示指向后继
5.3.4.3先序线索二叉树
5.3.4.4后序线索二叉树
5.3.5二叉树的线索化
5.3.5.1中序线索化
typedef int ElemType;
//线索二叉树结点
typedef struct ThreadNode{
ElemType data;
ThreadNode* LChild, *RChile;
int Ltag, Rtag;//左右线索的标志
}ThreadNode, *ThreadTree;
//全局变量,指向当前访问结点的前驱
ThreadNode *pre = NULL;
void Visit(ThreadNode *q) {
if (q->LChild == NULL) {//左子树为空,建立前驱线索
q->LChild = pre;
q->Ltag = 1;//修改标志
}
if (pre != NULL && pre->RChile == NULL) {
pre->RChile = q;//建立前驱结点的后继线索
pre->Rtag = 1;
}
pre = q;
}
//中序遍历二叉树,一边遍历一边线索化
void InThread(ThreadTree T){
if (T != NULL) {
InThread(T->LChild);//中序遍历左子树
Visit(T);//访问根结点
InThread(T->RChile);//中序遍历右子树
}
}
//中序线索化二叉树
void CreateInThread(ThreadTree T) {
pre = NULL;//pre初始值为空
if (T != NULL) {//非空二叉树才能线索化
InThread(T);
if (pre->RChile == NULL)
pre->Rtag = 1;//处理遍历的最后一个结点
}
}
5.3.5.2先序线索化
//先序遍历二叉树,一边遍历一边线索化
void PreThread(ThreadTree T){
if (T != NULL) {
Visit(T);//访问根结点
//先序遍历会将左孩子指针指向前驱,而后面又需要访问左孩子指针,所以需要加一个判断
if(T->Ltag==0)
PreThread(T->LChild);//先序遍历左子树
PreThread(T->RChile);//先序遍历右子树
}
}
5.3.5.3后序线索化
//后序遍历二叉树,一边遍历一边线索化
void PostThread(ThreadTree T){
if (T != NULL) {
PostThread(T->LChild);//后序遍历左子树
PostThread(T->RChile);//后序遍历右子树
Visit(T);//访问根结点
}
}
5.3.6线索二叉树中找前驱后继
5.3.6.1中序线索二叉树
中序线索二叉树找后继
在中序线索二叉树中找到指定结点*p的中序后继 next
- 若p->Rtag==1,则next = p->RChild
- 若Rtag==0
代码实现
//找到以p为根的子树中,第一个被中序遍历的结点
ThreadNode *FirstNode(ThreadNode *p){
//循环找到最左下结点(不一定是叶子结点)
while(p->Ltag==0) p=p->LChild;
return p;
}
//在中序线索二叉树中找到结点p的后继结点
ThreadNode *NextNode(ThreadNode *p){
//右子树左下角
if(Rtag==0)
return FirstNode(p->RChild);
return p->Rchild;//Rtag==1直接返回后继线索
}
//对中序线索二叉树进行中序遍历
void InOrder(ThreadNode *T){
for(ThreadNode *p = FirstNode(T);p!=NULL;p=NextNode(p))
visit(p);
}
中序线索二叉树找前驱
在中序线索二叉树中找到指定结点*p的中序前驱 pre
- 若p->Ltag==1,则pre = p->LChild
- 若p->Ltag==0
//找到以结点p为根的子树中,最后一个被中序遍历的结点
ThreadNode *LastNode(ThreadNode *p){
//若右子树为空,循环继续
while(p->Rtag==0)
p=p->Rchild;
return p;
}
//在中序线索二叉树中找到结点p的前驱结点
ThreadNode *PreNode(ThreadNode *p){
if(P->Ltag==0)
return LastNode(p->Lchild);
return p->Lchild;
}
//对中序线索二叉树进行逆向中序遍历
void RevInorder(ThreadNode *p){
for(ThreadNode *p=LastNode(T);p!=NULL;p=PreNode(p))
visit(p);
5.3.6.2先序线索二叉树
先序找后继
先序找前驱
5.3.6.3后序线索二叉树
后序找后继
后序找前驱
5.4树和森林
5.4.1树的存储结构
5.4.1.1双亲表示法(顺序存储)
data | A | B | C | D | E | F | G | H | I | J | K |
---|---|---|---|---|---|---|---|---|---|---|---|
parent | -1 | 0 | 0 | 0 | 1 | 1 | 2 | 3 | 3 | 3 | 4 |
#define MAX_TREE_SIZE 100
typedef char ElemType;
typedef struct {//树的结点定义
ElemType data;//数据元素
int parent;//双亲位置域
}PTNode;
typedef struct {//树的类型定义
PTNode nodes[MAX_TREE_SIZE];
int n;//结点数
}PTree;
5.4.1.2孩子表示法
#define MaxSize 100
typedef char ElemType;
//先声明孩子结点的数据类型:保存下标
typedef struct {
int Child;//孩子在数组中的下标
SNode* next;//下一个孩子
}SNode;
//再声明数组指向孩子:实际数据从存储
typedef struct {
ElemType data;
SNode* firstChild;
}PNode;
//最后声明数组
typedef struct {
PNode nodes[MaxSize];
int n, r;//根结点数和根的位置
};
5.4.1.3孩子兄弟表示法
5.4.1.4森林和二叉树的转换
5.4.2树和森林的遍历
5.4.2.1树的遍历
先根遍历
:若树非空、先访问根结点,再依次对每棵树进行先根遍历。
对树的**
先根遍历
序列和与这棵树相对应的二叉树的先序序列
**相同A B C D
A (B E F) (C G) (D H I J)
A (B (E K) F) (C G) (D H I J)
后根遍历
:若树非空,先依次对每棵子树进行后根遍历,最后访问根结点。
对树的**
后根遍历
序列和与这棵树相对应的二叉树的中序序列
**相同K E F B G CH I J D A
层次遍历
:用队列实现
- 若树非空、则根节点入队
- 若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队
- 重复2直到队列为空
5.4.2.2森林的遍历
森林
:森林是m(m>=0)棵互不相交的树的集合,每颗树去掉根结点后,其各个子树组成森林。
先序遍历森林
- 若森林为非空,则按照如下规则进行遍历
- 访问森林中第一棵树的根结点
- 先序遍历第一棵树根结点的子树森林
- 先序遍历除去第一棵树之后剩余的树构成的森林。
以上效果等同于:依次对各个树进行先根遍历
B E K L F C G D H M I J
中序遍历森林
- 若森林为非空,则按照如下规则进行遍历
- 中序遍历第一棵树根结点的子树森林,访问第一棵树的根节点
- 中序遍历除去第一棵树之后剩余的树构成的森林。
以上效果等同于:
- 依次对各个树进行后根遍历
- 将之转换为二叉树,对二叉树进行中序遍历
K L E F B G C M H I J D
5.5二叉树的应用
5.5.1二叉排序树
5.5.1.1二叉排序树的定义
二叉排序树
:又叫二叉查找树BST,一棵二叉树或者空二叉树,或者是具有如下性质的二叉树
- 左子树上的结点关键字均小于根结点的关键字
- 右子树上的结点关键字均大于根结点的关键字
- 左子树和右子树又各是一颗二叉排序树
左子树结点值 < 根结点值 < 右子树结点值
5.5.1.2二叉排序树的查找
左子树结点值 < 根结点值 < 右子树结点值
- 若树非空,目标值与根结点值进行比较:
- 若相等,查找成功
- 若小于根结点值,则在左子树查找,否则右子树查找
- 查找成功,返回结点指针,失败,返回NULL
//二叉排序树结点
typedef struct BSTNode {
int key;
BSTNode* LChild, *RChild;
}BSTNode, *BSTree;
//非递归
//在二叉排序树中寻找值为key的结点
BSTNode * BST_Search(BSTree T, int ele) {
//传入二叉树和要查找的值,返回结点指针
while (T != NULL && T->key == ele) {
if (ele < T->key)
//小,在左子树上寻找
T = T->LChild;
else
//大在右子树上寻找
T = T->RChild;
}
return T;
}
//在二叉排序中查找值为key的结点(递归实现)
BSTNode *BST_Find(BSTree T, int ele) {
if (T == NULL)
return NULL;
if (ele == T->key)
return T;
if (ele < T->key)
BST_Find(T->LChild, ele);
else
BST_Find(T->RChild, ele);
}
5.5.1.3二叉排序树的插入
//递归算法
bool Insert_BST(BSTree T, int ele) {
if (T == NULL) {
T = (BSTree)malloc(sizeof(BSTNode));
T->key = ele;
T->LChild = T->RChild = NULL;
return true;
}
else if (ele == T->key)//存在相同关键字的结点,插入失败
return false;
else if (ele < T->key)
//若ele小于key,递归调用插入函数,直到树的R/L为空
return Insert_BST(T->LChild, ele);
else if (ele > T->key)
return Insert_BST(T->RChild, ele);
}
//非递归算法
bool BST_Inster(BSTree T, int ele) {
if (T == NULL) {
T = (BSTree)malloc(sizeof(BSTNode));
T->key = ele;
T->LChild = T->RChild = NULL;
return true;
}
else if (ele == T->key)//存在相同关键字的结点,插入失败
return false;
while (T != NULL) {
if (ele < T->key)
T = T->LChild;
else
T = T->RChild;
}
}
//二叉树的构造
void Creat_BST(BSTree &T, int str[], int n){
T = NULL;//初始时,T为空树
int i = 0;
while(i<n){
BST_Insert(T,str[i]);
i++;
}
}
5.5.1.4二叉排序树的删除
先搜索找到目标结点:
- 若被删除的结点z是叶子结点,直接删除,不会破坏二叉排序树的性质。
- 若被删除的结点z只有一颗左子树或者右子树,则让z的子树成为z父节点的子树,代替z的位置
- 若被删除的结点z有左、右两棵子树,则令z的直接后继(或直接前驱)代替z,然后从二叉排序树中删除这个直接后继(直接前驱),这样就成了第一或第二种情况
- Z的前驱:Z的右子树中最左下的结点,该结点一定没有左子树
- Z的后继:Z的左子树中最右下的结点,该结点一定没有右子树
5.5.1.5查找效率分
查找长度
:在查找运算中,需要对比关键字的次数称为查找长度,反映了查找操作时间复杂度
- 若树高h,找到最下层的一个结点需要对比h次
- 最好情况:n个结点的二叉树最小高度为。平均查找长度=
- 最坏情况:每个结点只有一个分支,树高h=结点树n。平均查找长度=O(n)
5.5.2平衡二叉树
5.5.2.1平衡二叉树的定义
平衡二叉树
:简称平衡树(AVL树),树上任意一结点的左子树和右子树的高度之差不能超过1
结点的平衡因子
:左子树高 - 右子树高
- 平衡二叉树的平衡因子只可能是1、0、-1
- 只要有任一个结点的平衡因子绝对值大于1,那么他就不是平衡二叉树
//平衡二叉树的结点
typedef struct {
int key;
int balabce;
AVLNode * Lchild, Rchild;
}AVLNode, *AVLTree;
5.5.2.2平衡二叉树的插入
在二叉排序树中插入新的结点以后,如何保持平衡
-
插入一个结点以后,查找路径上的所有结点都可能受到影响
-
所有我们需要从插入点往回找第一个不平衡结点,跳转以该结点为根的子树。
-
注意!每次调整的对象都是**
最小不平衡子树
**在插入操作中,只需要将最小不平衡子树调整平衡,其他祖先结点都会恢复平衡
5.5.2.3调整最小不平衡子树
LL不平衡子树
-
二叉排序树的特性:左子树结点值<根节点值<右子树结点值
-
BL<B<BR<A<AR
-
解决方法:
-
将A的左孩子B向右上放旋转代替A成为根节点
-
将A结点向右下旋转成为B的右子树的根结点
-
B的原右子树成为A的左子树
//设上图A结点的指针为f,B结点的指针为p,A结点的父亲结点为gf
f->Lchild = p->Rchild;
p->Rchild = f;
gf->Lchild/Rchild = p;
RR不平衡子树
-
二叉排序树的特性:左子树结点值<根结点值<右子树结点值
-
AL<A<BL<B<BR
-
解决方法
-
将B结点向左上旋转代替A成为根结点
-
将A结点向左下选择成为B的左子树的根结点
-
B的原子树作为A的右子树
//设上图A结点的指针为f,B结点的指针为p,A结点的父亲结点为gf
f->Rchild = p->Lchild;
p->Lchild = f;
gf->Lchild/Rchild = p;
LR不平衡子树
-
解决方法
-
先让C向左上选择提升到B结点的位置
-
再把C结点向右上旋转提升到A结点的位置
-
注意!无论是在CL中插入位置,还是在CR中插入位置,解决步骤都不会改变
RL不平衡子树
- 解决方法
- 先右旋再左旋
- 先将A结点的右孩子B的左子树的根节点C向右上提升到B结点的位置
- 再把C结点向左上旋转提升到A结点的位置
5.5.2.4查找效率
- 若树高为h,在最坏情况下,查找一个关键字最多需要对比n次,即查找操作的时间复杂度不可能超过O(h)
5.5.2.5习题
练习1
练习2
5.5.3红黑树
5.5.3.1红黑树的定义和性质
红黑树和平衡二叉树
平衡二叉树(AVL):
- 插入\删除 很容易破坏平衡特性,需要频繁调整树的形态。如:插入操作导致不平衡,则需要**先计算平衡因子,找到最小不平衡子树(时间开销大)**,再进行LL/RR/LR/RL的调整
- 适合以查找为主,插入删除较少的场景
红黑树:
- 插入\删除 很多时候并不会破坏红黑的特性,无需频繁调整树的形态,即便调整一般都可以**在常数级时间内完成**。
- 适合频繁插入删除的场景
红黑树的定义
- 红黑树是二叉排序树===> 左子树<=根节点<=右子树
- 红黑树的每个结点或是**红色或是黑色**
- 红黑树的根节点和叶节点(外部结点、NULL结点、失败结点)均为黑色
- 不存在两个相邻的红结点(即红结点的父结点和孩子结点均为黑色)
- 对每个结点,从该结点到任意一叶结点的简单路径上,所含黑结点的数目相同
左根右、根叶红、不红红、黑路同
struct RBnode {//红黑树结点的定义
int key;//关键字的值
RBnode * parent;
RBnode * lChild;
RBnode * rChild;
int color;//结点颜色可以用0、1表示黑、红
};
黑高
结点的黑高bh——从某个结点出发(不含该节点)到达任意一空叶结点的路径上黑结点的总数
思考:根结点黑高为h的红黑树,内部结点数(关键字)至少有多少个?
回答:内部结点点数最少的情况——总共h层黑结点的满树形态,
因为如果有一颗不为黑结点就不是最少,
不是满树,就不会满足黑路同的特性
结论:(2^h)-1
红黑树的性质
- 从根结点到叶子结点的最长路径不大于最短路径的两倍
- 有n个内部结点的红黑树高度
- 若总高为h,那么由于不红红,根结点的黑高至少为h/2
- 若根结点黑高为h/2根据黑高的结论—>内部结点数
->红黑树的**查找操作时间复杂度 =**
5.5.3.2红黑树的插入
插入步骤
- 先查找,确定插入位置(原理同二叉排序树),插入新结点
- 新节点 是根——染为**黑色**
- 新结点 非根——染为**红色**
- 若插入新结点后依然满足红黑树的定义则结束插入
- 若插入新结点后不满足红黑树的定义,需要调整**(根据新结点叔叔的颜色而定)**
- 黑叔:
旋转+染色(颜色取反)
- LL型:右单旋,父换爷+染色
- RR型:左单旋,父换爷+染色
- LR型:左、右双旋,儿换爷+染色
- RL型:右、左双旋,儿换爷+染色
- 红叔:
染色(颜色取反)+变新
- 叔叔、父亲、爷爷结点染色
- 将爷爷视为新插入的结点
- 黑叔:
红黑树的插入
5.5.3.3红黑树的删除
重要考点
- 红黑树删除操作的时间复杂度 = [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xAR9ZYLM-1666602897736)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221021203646942.png)]
- 在红黑树中删除结点的方式和**“二叉排序树的删除”**一样
- 按照2删除结点后,可能破坏红黑树特性,此时需要**调整结点颜色、位置**,使其满足红黑树特性
5.5.4哈夫曼树
5.5.4.1带权路径长度
结点的****:具有某种现实含义的数值(如,表示结点的重要性等)
:从树的根结点到该结点的路径长度(经过的边数)与该节点上权值的乘积
:树中所有的的带权路径长度之和(WLP,Weighted Path Length)
5.5.4.2哈夫曼树的定义
在含有n个带权叶结点的二叉树中,其中带权路径长度WPL最小的二叉树称为哈夫曼树,也叫最优二叉树
5.5.4.3哈夫曼树的构造
给定n个权值分别为_W 1_,W 2,W 3……,_W n_的结点,构造哈夫曼树的算法如下:
- 将这n个结点分别作为n棵仅含一个结点的二叉树,构成森林F
- 构造一个新的结点,从F中取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根节点的权值之和。
- 从F中删除刚才选出的两棵树,同时将新树加入F中
- 重复2,3步骤,直到F从森林变成一棵树
- 每个初始结点最终都会成为叶结点,且权值越小的结点到根结点的路径长度就越大
- 哈夫曼树的结点总数为2n-1
- 哈夫曼树中不存在度为1的结点
- 哈夫曼树并不唯一,单WPL必然相同且最优
- WPL = 1 * 7+2 * 3 + 3 * 2 + 4 * 1 + 4 * 2 = 32
5.5.4.4哈夫曼编码
-
固定长度编码——每个字符用相等长度的二进制位表示
-
可变长度编码
——允许对不同字符用不等长的二进制位表示 -
若没有一个编码是另外一个编码的前缀,那么这样的编码我们就叫它**
前缀编码
**。 -
由哈夫曼树得到
哈夫曼编码
——字符集中的字符作为一个叶子结点,各个字符出现的频度作为结点的权值。 -
哈夫曼树不唯一,哈夫曼的编码也不唯一
-
哈夫曼编码可以用于数据的压缩
- 左边,前缀编码:无歧义
- 右边,非前缀编码:有歧义
5.5.5树与等价问题
定义
离散数学中,等价(类)关系定义是:如果集合S中的关系R是自反的、对称的和传递的,则成它为一个等价关系
- 自反:对于每个x∈X,都有(x,x)∈R;
- 对称:对于任意的x,y∈X,若当(x,y)∈R时,有(y,x)∈R;
- 传递:对于任意的x,y,z∈X,当(x,y)∈R且(y,z)∈R时,有(x,z)∈R。
具体操作
划分等价类需要对集合进行的操作有3个:
- 构造只含有单个成员的集合,
- 判断某个单元所在子集,
- 归并两个互不相交的集合为一个集合
并查集
- 令S中每个元素各自形成一个只含单个成员的子集,记作S1,S2,…,Sn。
- 重复读入m个偶对,对每个读入的偶对(x,y),判断x和y所属子集。不失一般性,假设x∈Si,y∈Sj,若**Si != Sj**,则将Si并入Sj并置Si为空(或将Sj并入Si并置Sj为空)。则当m个偶对都处理过后,S1,S2,…,Sn中所有非空子集即为S的R等价类。
一、构造只含单个成员的集合;
二、判定某个单元素所在的子集;
三、归并两个互不相交的集合为一个集合。
代码实现
5.6回溯法与树的遍历
在程序设计中,有一些或一组解或全部解或最优解问题,不是根据某种特定的计算法则,而是利用试探与回溯的搜索技术求解。
5.5.1 求含n个元素的集合的幂集
题目描述
集合A的幂集是由集合A的所有子集组成的集合。如A={1,2,3},则A的幂集
ρ(A) = {{1,2,3},{1,2},{1,3},{1},{2,3},{2},{3},}
解题思路
将,幂集ρ(A)的元素的过程看成是依次对集合中的元素进行“取”或“舍弃”的过程。
5.5.2四皇后问题
题目描述
-
一棵四叉树,每个结点表示一个局部布局或者完整的布局。根结点表示棋盘的初始状态:棋盘上无子,每个皇后棋子都有4个可以选择的位置,但是在任何情况下,棋盘的合法布局都应该满足以下条件
-
任意两个棋子都不占据棋盘上的同一行、同一列、或同一对角线。
void Trial (int i, int n){
//进入本函数时,在n*n棋盘前i-1已经放置了互不攻击的i-1个棋子
//现从第i行起继续为后序棋子选择合适位置
//当i>n时,得到了合适的棋局输出
if(i>n)输出棋盘当前布局;//n为4时即为四皇后问题
else if(j=i;j<n;++j){//从计算机底层来讲++j比j+更加高效
在第i行第j列放置一个棋子;
if(当前布局合法)Trial(i+1, n);
移走i行j列棋子;
}
}
#include <iostream>
#include <cmath>
#define SIZE 5
using namespace std;
int a[SIZE][SIZE];
int b[SIZE];//存放皇后的列位置,下标为皇后的行位置
int cnt = 1;
void queue(int n) {
if (n == SIZE - 1) {
cout << "No. " << cnt++ << endl;
for (int i = 1; i <= SIZE - 1; i++) {
for (int j = 1; j <= SIZE - 1; j++) {
cout << a[i][j] << " ";
}
cout << endl;
}
return;
}
int i, j;
for (i = 1; i <= SIZE - 1; i++) {//列
for (j = 1; j < n; j++) {//前n-1个皇后
if (i == b[j] || abs(i - b[j]) == abs(n - j)) {
break;
}
}
if (j == n) {
a[n][i] = 1;
b[n] = i;
queue(n + 1);
a[n][i] = 0;
}
}
return;
}
int main(int argc, char** argv) {
queue(1);
return 0;
}
补充内容——堆(Heap)
1、堆的概述
1.1什么是堆
1.1.1优先队列
定义
- 优先队列(Priority Queue):特殊的队列,取出元素的顺序是按照元素的**优先权(关键字)**大小,而不是元素进入队列的先后顺序。
实现
数组
:
- 插入——元素总是插入尾部O(1)
- 删除——查找最大(或最小)关键字O(n)
- 从数组中删去需要移动的元素O(n)
链表
:
- 插入——元素总是插入链表的头部O(1)
- 删除——查找最大(或最小)关键字O(n)
- 删去结点O(1)
有序数组
:
- 插入——找到合适的元素O(n)或
- 移动元素并插入O(n)
- 删除——删去最后一个元素O(1)
有序链表
:
- 插入——找到合适的元素O(n)
- 插入元素O(1)
- 删除——首元素或者最后元素O(1)
1.1.2二叉树的存储
二叉查找树
- 若用二叉查找树,插入结点Log N,删除最大、最小结点,在最左边或者最右边,时间效率高!
- 但是,一直进行删除最大/最小结点,整个树容易歪掉,树的高度不再是Log N
完全二叉树
结构性:
用数组表示的完全二叉树
有序性:
任何一个结点的关键字是其子树所有结点的最大或者最小值
🔳最大堆,也叫大顶堆:最大值
🔳最小堆,也叫小顶堆:最小值
1.1.3例题
【最大堆和最小堆】
【不是堆】
1.2堆的插入
1.2.1最大堆的创建
typedef int ElemType;
typedef struct{
ElemType *elements;//存放堆元素的数组
int size;//堆的当前元素个数
int Capacity;//堆的最大容量
}HeapStruct,*MaxHeap;
MaxHeap Greate(int MaxSize) {
//创建容量为MaxSize的空的最大堆
MaxHeap H = (MaxHeap)malloc(sizeof(HeapStruct));
H->elements = (ElemType *)malloc((MaxSize + 1) * sizeof(ElemType));
H->size = 0;
H->Capacity = MaxSize;
H->elements[0] = MaxData;
//定义哨兵,为大于堆中所有可能元素的值,便于更快操作。
return H;
}
1.2.2最大堆的插入
//将新增结点插入
void Insert(MaxHeap H, ElemType item) {
int i;
if (IsFull(H)) {
cout << "最大堆已满!" << endl;
return;
}
i = ++H->size;
//i指向插入后堆的最后一个元素的位置
for (; H->elements[i / 2] < item; i /= 2)
//i=i/2;一直到父节点值大于当前值
H->elements[i] = H->elements[i / 2];
H->elements[i] = item;
}
1.3堆的删除
取出根结点(最大值)元素,同时删除堆的一个结点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-834stLoh-1666602897738)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221011202216777.png)]
- 把31移至根
- 找出31较大的孩子
- 重复步骤二,直到31没有较大的孩子
ElemType Delete(MaxHeap H) {
//从最大堆H中取出键值为最大的元素,并且删除一个元素
int Parent, Child;
ElemType MaxItem, temp;
if (IsEmpty(H)) {
cout << "最大堆为空" << endl;
return;
}
MaxItem = H->elements[1];
//取出最大根结点
temp = H->elements[H->size--];
//相当于temp=H->elements[H->size]; H->size--;
//从最大堆中最后一个元素从根结点开始向上过滤下层结点
for (Parent = 1; Parent * 2 <= H->size; Parent = Child) {
Child = 2 * Parent;
if ((Child != H->size) && (H->elements[Child] < H->elements[Child + 1]))
Child++;
if (temp >= H->elements[Child])
//判断temp是否比左右儿子大的那一个大,若大,跳出循环
break;
else
H->elements[Parent] = H->elements[Child];
//不大,Parent=child
}
//给temp找一个位置
H->elements[Parent] = temp;
return MaxItem;
}
1.4堆的建立
建立最大堆
:将已经存在的N个元素,按照最大堆的要求存放再一个一维数组中。
- 方法1:通过插入操作,将N个元素一个个相继插入到一个初始为空的堆中去,时间代价为O(Nlog N)。
- 方法2:在线性时间复杂度下建立最大堆
- 将N个元素按输入顺序存入,先满足完全二叉树的结构特性
- 调整各个结点位置,以满足最大堆的有序特性。
- 思路:从最小的堆开始,从下往上,依次建堆
2、集合
2.1集合的表示和查找
2.1.1集合的表示
🔲集合的运算:交、并、补、差、判定一个元素是否属于某一个集合
🔲并查集:集合并、查某元素属于什么集合
-
【例】
- 有10台电脑{1,2,3,4,5,6,7,8,9,10},已知下列电脑之间已经实现了连接:1和2、2和4、3和5、4和7、5和8、6和9、6和10。那么在2和7之间,5和9之间是否连通?
-
【思路分析】
-
将10台电脑看成10个集合
-
已知一种连接X和Y,那么就将X和Y对应的集合合并
-
查询X和Y是否连接即判断二者是否在同一个集合
-
🔲并查集问题中的集合存储如何实现
- 用树的结构表示集合,树的每个结点表示集合的元素
- 采用数组存储形式
#define MaxSize 100
typedef char ElemType;
typedef struct {
ElemType data;
int Parent;
}SetType[MaxSize];
//查找某个元素所在的集合
int Find_Set(SetType s[], ElemType ele) {
int i;
for (i = 0; i < MaxSize&& s[i]->data != ele; i++)
if (i >= MaxSize) {
//未找到返回-1
cout << "Error" << endl;
return -1;
}
//循环找到树根结点所在的下标
for (; s[i]->Parent >= 0; i = s[i]->Parent)
return i;
}
2.2集合的并运算
🔲具体思路
- 分别找到X1和X2两个元素所在集合树的根节点
- 如果他们不同根,则将其中一个根结点的父节点设置为另外一个根节点的数组下标。
//集合的并运算,注意这里是用数组保存的所以形参只需要3个
void Union_Set(SetType s[], ElemType ele1, ElemType ele2) {
int root1, root2;
root1 = Find_Set(s, ele1);
root2 = Find_Set(s, ele2);
if (root1 != root2)
s[root1]->Parent = root2;
}
🔲存在的问题
- 由于Union_Set操作是将一个树并的另外一个树之后,那么就会存在一个问题,使得树越来越高,这对于Find_Set操作非常不友好
- 为了改善以后的查找性能,我们可以采用小的集合合并到相对较大的集合中。(修改Union函数)
解决方法
- 通过给根结点赋-3、-7这样的值,巧妙的表示集合的元素个数,其中-代表这是一个根结点,7代表结点的元素个数。
3、小白专场
3.1堆中的路径
#include <iostream>
#include <string>
using namespace std;
#define MaxN 1000
#define MinH -10001
int Size, H[MaxN];
void Create() {
Size = 0;
H[0] = MinH;
}
void Insert(int ele) {
int i;
for (i = ++Size; H[i / 2] >= ele; i /= 2)
H[i] = H[i / 2];
H[i] = ele;
}
int main() {
int n, m, x, i, j;
cout << "请输入插入元素个数:";
cin >> n;
cout << "请输入查询结点个数:";
cin >> m;
Create();
cout << "请输入插入元素:";
for (i = 0; i < n; i++) {
cin >> x;
Insert(x);
}
cout << "请输入您要查询的结点:";
for (i = 0; i < m; i++) {
cin >> j;
cout << H[j] << " ";
while (j > 1) {
j = j / 2;
cout << H[j] << " ";
}
cout << endl;
}
system("pause");
return 0;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yUJjJkkh-1666602897739)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221012115604161.png)]
第6章、图
6.1图的逻辑结构
6.1.1基本概念
6.1.1.1图的定义
- 注意:线性表可以是空表,树可以是空树,但是图不能是空图,即一个**
图的顶点集一定是非空集
**,但是它的边可以是空集。
6.1.1.2无向图和有向图
6.1.1.3简单图和多重图
简单图
:1、不存在重复的边 2、不存在顶点到自身的边
多重图
:1、图G中某两个结点之间的边数多于一条,又允许顶点通过同一条边和自己关联,则G位多重图
6.1.1.4专用术语
对于**无向图
**
- 顶点v的度是指依附于该顶点的边的条数,记为TD(v)
对于**有向图
**
- 入度是以顶点v为终点的有向边的数目,记为ID(v)
- 出度是以顶点v为起点的有向边的数目,记为OD(v)
- 顶点v的度 = 入度 + 出度,即 TD(v) = ID(v) + OD(v)
顶点-顶点的关系
-
路径——顶点到顶点之间的一条路径是指顶点序列,。
-
回路——第一个顶点和最后一个顶点相同的路径称为回路或环
-
简单路径——在路径序列中,顶点不重复出现的路径就叫简单路径
-
简单回路——除了第一个顶点和最后一个顶点以外,其余顶点不重复出现的回路称为简单回路。
-
路径长度——路径上边的数目
-
点到点的距离——从顶点u出发到顶点v的最短路径若存在,则路径的长度称为从u到v的距离。若从u到v根本不存在路径,则记该距离为∞。
-
在无向图中,若两个顶点之间有路径存在,那么认为是连通的。
-
在有向图,若两个顶点既有正向的路径也有逆向的路径,那么这两个顶点就叫强连通。
-
连通图
-
强联通图
6.1.1.5子图
6.1.1.6连通分量
连通分量
无向图中的极大连通子图(子图必须连通,且包含尽可能多的顶点和边)称为连通分量。
强连通分量
有向图中的极大连通子图(子图必须强连通,同时保留尽可能多的边)称为有向图的强连通分量。
6.1.1.7生成树和生成森林
生成树
-
连通图的生成树是包含图中全部顶点的一个极小连通图。(边要尽可能少,但是得保持连通)
-
若图中的顶点数为n,那么它的生成树含有n-1条边,对于生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边,则会形成回路。
生成森林
- 在非连通图中,连通分量的生成树,构成了非连通图的生成森林。
6.1.1.8边的权、带权图
6.1.2几种特殊的图
无向完全图
- 无向图中任意两个顶点之间都存在边。
- 若无向图的顶点数| V | = n,则| E |∈[0, ] = [0, n(n-1)/2]
有向完全图
- 有向图中,任意两个顶点之间都存在方向相反的两条弧。
- 若有向图的顶点数| V | = n,则| E |∈[0, ] = [0, n(n-1)]
稀疏图
- 边数很少的图称为稀疏图
稠密图
- 边数很多的图称为稀疏图
- 稀疏图和稠密图是一个相对的概念
数和森林
6.2图的存储结构
6.2.1邻接矩阵
6.2.1.1不带权邻接图
#define MaxVertexNum 100
typedef struct {
char Vec[MaxVertexNum];//顶点表
bool Edge[MaxVertexNum][MaxVertexNum];//邻接矩阵、边表
int vexnum, arcnum;//图当前的顶点数和边数
}MGraph;
6.2.1.2邻接矩阵带权图
//带权
#define MaxVertexNum 100
#define INFINITY 35535//最大的int值用于定义常量"无穷"
typedef char VertexType;
typedef int EdgeType;
typedef struct {
VertexType Vex[MaxVertexNum];
EdgeType Edge[MaxVertexNum][MaxVertexNum];//边的权
int vexnum, arcnum;//顶点数和弧数
}MGraph;
- 有一些代码中用0表示自己指向自己
- 说明,在带权图中,一个元素的值为0或者∞,那么这两种状态表示与之对应两个点之间不存在边。
6.2.1.3邻接矩阵性能分析
6.2.1.4邻接矩阵的性质
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PMbENNoI-1666602897741)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221006194805007.png)]
6.2.2邻接表
#define MaxVertexNum 100
//边
typedef struct {
int adjvex; //边、弧指向哪个结点
ArcNode * next; //指向下一条弧的指针
//InfoType info; //边权值
}ArcNode;
//顶点
typedef struct VNode {
int data;//顶点信息
ArcNode* first;//第一条边
}VNode, AdjList[MaxVertexNum];
typedef struct {
AdjList vertices;
int vexnum, arcnum;//多少结点多少边
};
6.2.3十字链表
6.2.3.1十字链表存储有向图
绿色的结点是指向其他结点的(出度),橙色结点的是其他结点指向来的(入度)
#define MAXSIZE 20
typedef struct ArcBox {
int tailvex, headvex;//弧头编号和弧尾编号
ArcBox *hLink, *tLink;//弧头相同的下条弧和弧尾相同的下条弧
//InfoType info;//权值
}ArcBox;
typedef struct VexNode {
int data;//数据域
ArcBox * firstin, *firstout;//该点作为弧头/尾的第一条弧
}VexNode;
typedef struct {
VexNode List[MAXSIZE];
int vexnum, arcnum;//顶点数和弧数
}OLGraph;
6.2.3.2十字链表性能分析
-
空间复杂度:O(|V| + |E|)
-
如何找到指定顶点的出度——顺着first out
-
如何找到指定顶点的入度——顺着first in
-
注意!十字链表只能用于存储有向图
6.2.4邻接多重表
6.2.4.1邻接多重表存储无向图
typedef struct ArcBox {
int i, j;//边的两个顶点编号
ArcBox *iLink, *jLink;//依附于顶点i和j的两条边
//InfoType info;//权值
}ArcBox;
typedef struct VexNode {
int data;//数据域
ArcBox * firstedge;//与该顶点相连的第一条边
}VexNode;
typedef struct {
VexNode List[MAXSIZE];
int vexnum, arcnum;//顶点数和弧数
}OLGraph;
6.2.4.2邻接多重表的性能分析
-
空间复杂度:O(|V| + |E|),每条边只对应一份数据
-
删除边、删除结点等操作很方便
-
注意!十字链表只能用于存储无向图
6.2.4.3十字链表和邻接多重表
6.3图的基本操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l4FxZE1j-1666602897742)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221006202017116.png)]
Adjacent(G,x,y)
:判断图G是否存在边<x,y>或(x,y)
Neighbors(G,x)
:列出图G中与结点x邻接的边
InsertVertex(G,x)
:列出图G中插入顶点x
DeleteVertex(G,x)
:列出图G中删除顶点x
AddEdge(x,y)
:若无向边(x,y)或有向边<x,y>不存在,则向G中添加边
❗️FirstNeighbor(G,x)
:求图中顶点x的第一个邻接点,若有返回顶点号,若没有或不存在返回-1
- 邻接矩阵O(1)~O(|V|)
- 邻接表O(1)
❗️NextNeighbor(G,x,y)
:给定一个顶点y,该顶点是x的一个邻接点,返回除y以外的顶点x的下一个邻接点的顶点号,若y是x的最后一个邻近点,返回-1。
6.4图的遍历算法
6.4.1图的广度优先遍历
6.4.1.1树的广度优先遍历(层序遍历)
树の具体思想
:从根节点出发,逐层的依次向下进行遍历。
图の具体思想
:从要访问的结点出发,比如结点2,依次访问与之相邻的结点1、6,再访问与1、6相邻的结点。
树 vs 图的广度优先
:
-
树
- 树找与之相邻的其他结点,找到该结点的孩子结点
- 树不存在回路,搜索相邻的结点时,不可能搜索到已经访问过的结点
-
图
- 图利用找邻结点的基本操作实现
- 搜索相邻的顶点时,可能搜索到已经访问过的结点。即给各个结点一个标记tag,标记为0表示没有访问,为1表示已经访问。
6.4.1.2代码实现
树的层序遍历
-
①若树非空,根结点入队
-
②若队列非空,队头元素依次出队,并且访问该结点,将该结点的孩子结点依次入队
-
③重复②的操作,直到队列为空
图的广度优先遍历
- 找到与一个顶点相邻的所有顶点
- 标记哪些顶点被访问过
- 需要一个辅助队列
-
❗️
NextNeighbor(G,x,y)
:给定一个顶点y,该顶点是x的一个邻接点,返回除y以外的顶点x的下一个邻接点的顶点号,若y是x的最后一个邻近点,返回-1。 -
❗️
FirstNeighbor(G,x)
:求图中顶点x的第一个邻接点,若有返回顶点号,若没有或不存在返回-1 -
bool visited [MAX_ERTEX_NUM]:访问标记的数组
bool visited[Max_size];//visited默认初始值为false,且默认数组从1开始
void BFS_Traverse(Graph G){//对图进行广度优先遍历
for(int i = 0;i<G.vecnum;i++){
visited[i] = false;//访问标记的数组初始化
}
Init_Queue(Q);//初始化辅助队列
for(int i = 1;i<=G.vecnum;++i){//从0开始遍历序列
if(!visited[i])//对每个分量执行依次BFS
BFS(G,i);
}
}
//广度优先遍历
void BFS(Graph G, int v){//从顶点v出发,广度优先遍历图G
visit(v);//访问初始结点v
visited[v] = true;
Push_Queue(Q,v);//将元素v入队
while(!Is_Empty(G)){
Pop_Queue(Q,v);//将顶点v出队
for(w=FirstNeighbor(G,v), w>=0; w=NextNeighbor(G,v,w)){
//检测v的所有邻接点
if(!visited[w]){//若结点w未被访问过
visit[w];
visited[w]=true;//修改标记
Push_Queue(Q,w);//元素w入队
}//if
}//for
}//while
}
6.4.1.3遍历序列的可变性
- 同一个图的邻接矩阵表示方法唯一,因此广度优先遍历序列唯一。
- 同一个图的邻接表表示方法不唯一,因此广度优先遍历序列不唯一。
6.4.1.4复杂度分析
-
点+边
6.4.1.5广度优先生成树
6.4.1.6广度优先生成森林
6.4.2图的深度优先遍历
6.4.2.1树的深度优先遍历
//树的先根遍历
void PreOrrder(TreeNode *p){
if(P!=NULL){
visit(R);
while(R还有下一个子树T)
PreOrder(T);//遍历下一棵子树
}
}
-
注意!新找到的结点一定是没有访问过的!
6.4.2.2图的深度优先遍历
步骤
- 假设初始状态所有结点都未被访问visited[i]=true
- 深度优先搜索可以从图的某个顶点v出发,访问此顶点
- 依次从v的未访问的邻接点出发深度优先遍历图,
- 遍历visited数组,若全为false,说明遍历完了,若没有
- 再依次分为visited[i]==true的结点
-
❗️
NextNeighbor(G,x,y)
:给定一个顶点y,该顶点是x的一个邻接点,返回除y以外的顶点x的下一个邻接点的顶点号,若y是x的最后一个邻近点,返回-1。 -
❗️
FirstNeighbor(G,x)
:求图中顶点x的第一个邻接点,若有返回顶点号,若没有或不存在返回-1 -
bool visited [MAX_ERTEX_NUM]:访问标记的数组
bool visited[Max_Size];//标记访问数组,数组从1开始,且初始值为false
void DFSTraverse(Graph G){//对图G进行深度优先遍历
for(int i=0;i<G.vexnum;i++)
visited[Max_Size]=false;//初始化数据
for(int v=1;v<=G.vexnum;v++){
if(!visited[v])
DFS(G,v);
}
}
void DFS(Graph G,int v){//从顶点v出发,深度遍历图
visit(v);
visited[v]=true;//修改标记
//前面的相当于链表的node,后面的相当于next指针
for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w)){
if(!visited[w])
DFS(G,w);
}//for
}
6.4.2.3复杂度分析
空间复杂度
空间复杂度:来自函数调用栈,最坏情况,递归的深度为O(|V|),最好情况下递归的深度为O(1)
时间复杂度
时间复杂度 = 访问各个结点所需要的时间 + 探索各条边所需要的时间
-
邻接矩阵
-
访问|V|个顶点所需要的时间O(|V|)
-
查找每个顶点都需要O(|V|)的时间,而总共有|V|个顶点
-
时间复杂度 = [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g5xRK9Cm-1666602897743)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221008142958132.png)]
-
邻接表
-
访问|V|个顶点所需要的时间O(|V|)
-
查找各个顶点的邻接点总共需要的时间O(|E|)
-
时间复杂度 = [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a7gUFRjs-1666602897744)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221008143145415.png)]
6.4.2.4深度优先遍历序列
- 同一个图的邻接矩阵表示方法唯一,因此深度优先遍历序列唯一
- 同一个图的邻接表表示方法不唯一,因此深度优先遍历序列不唯一
从2出发的深度优先遍历序列:2-1-5-6-3-4-7-8
从3出发的深度优先遍历序列:3-4-7-6-2-1-5-8
从1出发的深度优先遍历序列:1-2-6-3-4-7-8-5
从2出发的深度优先遍历序列:2-6-7-8-4-3-1-5
从3出发的深度优先遍历序列:3-6-2-1-5-7-8-4
从1出发的深度优先遍历序列:1-2-6-7-8-4-3-5
6.4.2.5深度优先生成树
- 同一个图的邻接矩阵表示方法唯一,因此深度优先遍历序列唯一,深度优先生成树也唯一
- 同一个图的邻接表表示方法不唯一,因此深度优先遍历序列不唯一,深度优先生成树不唯一
6.4.2.6深度优先生成森林
6.4.2.7图的遍历与连通性
无向图
- 对于无向图而言,进行BFS/DFS遍历,调用函数次数=连通分量
- 对于连通图,只需要调用1次 BFS/DFS
有向图
-
对于有向图进行BFS/DFS遍历,调用BFS/DFS函数的次数需要具体问题具体分析
-
若起始顶点到其他各顶点都有路径,则只需要调用1次BFS/DFS函数
-
对于强连通图,从任一结点出发都只需要调用1次BFS/DFS
6.5图的应用
6.5.1最小生成树
6.5.1.1最小生成树的概念
最小生成树概念
-
对于一个G=(V,E),生成树不同,每棵树的权(树中所有边上的权值之和)也可能不同,设R为G的所有生成树的集合,若T为R中树,则T称为G的。
-
最小生成树可能有多个,但是边的权值之和总唯一且最小
-
最小生成树的边数=顶点数-1。砍掉一条则不连通,多一条则有回路
-
若一个连通图本身就是一棵树,那么它的最小生成树就是自身
-
只有连通图才能有生成树,非连通图只有生成森林
6.5.1.2Prim算法
-
从某一个顶点开始构建生成树
-
每次将最小代价的新顶点纳入生成树
-
重复以上步骤,直到所有顶点都纳入。
-
时间复杂度:适合边稠密图
6.5.1.2Kruskal算法
-
克鲁斯卡尔Kruskal
-
每次选择一条权值最小的边,使这两条边的两头连通(原本已连通的就不选)
-
直到所有结点连通
-
时间复杂度:适合边稀疏
6.5.1.3实现思想
Prim算法
-
假设从顶点V0开始,并且创建两个数组
-
①循环遍历所有结点,找到LowCost最低的,并且还没有加入树的结点
-
②再次循环遍历,更新还没有加入各个顶点的LowCost值
-
③重复①、②
-
从V0开始,总共需要n-1轮处理,每轮处理需要遍历两个数组,一个数组n两个就是2n
-
综上所述,O(n^2)
Kruskal算法
- 检查第1条边的两个顶点是否连通(是否属于同一个集合)
- 并查集:刚开始将所有结点看成不同的集合,
- 第一条V0和V3刚开始属于不同的集合,即二者不连通,将之连接,并且加入一个集合
- 检查第2条边的两个顶点是否连通(是否属于同一个集合)
- ……
- 总共执行e轮,每轮判断两个顶点是否属于同一个集合,需要[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sZohIZCS-1666602897746)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221008170501677.png)]
- 总时间复杂度=[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pmzsfrgt-1666602897747)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221008170517027.png)]
6.5.2最短路径问题
6.5.2.1 BFS算法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZTbeo8hE-1666602897748)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221008171616770.png)]
- 以结点2为例,不难发现,想要求出最短路径,只需要对图进行广度优先遍历
代码实现
//求顶点 u 到其他顶点的最短路径
void BFS_MIN(Graph G, int u){
//d[i]表示从u到i的最短路径
for(int i=1; i<=G.vexnum; i++){
d[i]=∞;//初始化路径长度为无穷大
path[i]=-1;//最短路径从哪个顶点过来
}
du[u]=0;
visited[u]=true;
Push_Queue(Q,u);
while(!IS_Empty(Q)){//BFS算法主过程
Pop_Queue(Q);//队头元素出队
for(w=FirstNeighbor(G,u),w>=0;w=NextNeighbor(G,u,w)){
if(!visited(G,w)){
d[w]=d[u]+1;//路径长度+1
path[w]=u;//最短路径应该是从u到w
visited[w]=true;
Push_Queue(Q,w);//顶点入队
}//if
}//for
}//while
}
-
就是对BFS算法进行小修改,再visit一个顶点时,修改其最短路径长度d[ ]并且在path[ ]记录其前驱结点
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iCDl1rXN-1666602897749)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221008173049772.png)]
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TAEdMgxz-1666602897750)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221008173328688.png)]
6.5.2.2Dijstra算法
- 第一轮
-
- 循环遍历所有结点,找到还没有确定最短路径,且dist最小的顶点V4,令final[4]=true
- 检查所有和V4相邻的顶点,若其final为false则更新dist和path
- 更新dist[v1]=8 path[v1]=4
- 更新dist[v2]=14 path[v2]=4
- 更新dist[v3]=7 path[v3]=4
- 第二轮
-
- 循环遍历所有结点,找到还没有确定最短路径,且dist最小的顶点V3,令final[3]=true
- 检查所有和V3相邻的顶点V0和V2,若其final为false则更新dist和path
- 更新dist[v2]=13 path[v2]=3
- 第三轮
-
- 循环遍历所有结点,找到还没有确定最短路径,且dist最小的顶点V1,令final[1]=true
- 检查所有和V3相邻的顶点V0、V4和V2,若其final为false则更新dist和path
- 更新dist[v2]=9 path[v2]=1
- 第四轮
-
- 循环遍历所有结点,找到还没有确定最短路径,且dist最小的顶点V2,令final[2]=true
- 算法结束
V0到V2的最短带权路径长度为:dist[2]=9
通过path[ ]可知,V0到V2的最短路径为V2<---->V1<---->V4<---->V0
时间复杂度分析
用于带负权值图
- 事实上V0到V2的最短带权路径长度为5
- 所有,Dijkstra算法不适合用于有负权值的带权图
6.5.2.3Floyd算法
动态规划具体思想
- Floyd算法:求出每一对顶点之间的最短路径
- 使用动态规划思想,将问题的求解分为多个阶段
- 对于n个顶点的图G,求任意一对顶点Vi—>Vj之间的最短路径可以分解为以下几个阶段
**#初始:**不允许在其他顶点中转,最短路径是?
- 若允许在V0中转,最短路径是?
- 若允许在V0、V1中转,最短路径是?
- 若允许在V0、V1、V2中转,最短路径是?
……
**#n-1:**若允许在V0、V1、V2……Vn-1中转,最短路径是?
Floyd算法
按图索骥
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aJUVeUAI-1666602897751)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221008184601347.png)]
代码实现
//准备工作,根据图的信息初始化矩阵a和path
for(int k=0;k<n;k++){//考虑以Vk为中转点
for(int i=0;i<n;i++){//遍历整个矩阵,i为行、j为列
for(int j=0;j<n;j++){
if(A[i][j]>A[i][k]+A[k][j]){//以Vk为中转路径更短
A[i][j]=A[i][k]+A[k][j];//更新中转长度
path[i][j]=k;//更新中转点
}
}
}
}
时间复杂度:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4L7UxfPN-1666602897753)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221008185144752.png)]
空间复杂度:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a2WR8XnO-1666602897754)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221008185155280.png)]
Floyd算法实例
【旧版】_王道考研数据结构_6.4_4_最短路径问题_Floyd算法_实例:__12分40秒_哔哩哔哩_bilibili
三种算法的对比
6.5.2.4有向无环图(DAG)
有向无环图
:若一个有向图中不存在环,那么就称为无环图,简称DAG图(Directed Acyclic Graph)
DAG描述表达式
解题方法
练习题1
- B
练习题2
6.5.2.5拓扑排序
AVO网
拓扑排序的实现
- 从AOV网中选择一个没有前驱的顶点(入度为0)并输入
- 从网中删除该顶点和所有以他为起点的有向边
- 重复步骤1、2直到当前的AOV网为空或者网中不存在无前驱的顶点为止
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T6ny0F4o-1666602897755)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221008195022005.png)]
代码实现
#define Max_Size 100
typedef struct ArcNode{//边表结点
int adjvex;//该弧所指向的结点的位置
ArcNode * nextarc;//指向下条弧的指针
//InfoType info; //网的边权值
}ArcNode;
typedef struct VNode{//顶点表结点
VertexType data;//顶点信息
ArcNode *firstNode;//指向第一条依附于该结点的弧的指针
}VNode, AdjList[Max_Size];
typedef struct{
AdjList vertices;//邻接表
int vexnum,arcnum;//图的顶点数和弧数
}Graph;//以邻接表存储的图类型
bool TopoLogicalSort(Graph G){
InitStack(S);//初始化栈,存储度为0的顶点
for(int i=0;i<G.vexnum;i++){
if(indegree[i]==0)
//indegree记录结点当前的入度
push(S,i);//将度为0的顶点入栈
}
int count=0;//记录当前已经输出的顶点数
while(!Is_Empty(S)){//栈不为空,存在入度为0的元素
Pop(S,i);//栈顶元素出栈
Print[count++]=i;//输出出栈的元素 print[]记录得到的拓扑排序序列
for(p=G.vertices[i].fristNode;p; p=p->nextarc){
//将所有i指向顶点的入度减1,并且将度为0的入栈
v=p->adjvex;
if(!=(--indegree[v]))
Push(S,v);//入度为0出度
}
}//while
if(count<G.vexnum)//比较count和顶点个数的值
return false;//排序失败,有向图有回路
return true;//成功
}
逆拓扑排序
- 从AOV网中选择一个没有后继的顶点(出度为0)并输入
- 从网中删除该顶点和所有以他为起点的有向边
- 重复步骤1、2直到当前的AOV网为空或者网中不存在无前驱的顶点为止
逆拓扑排序的实现
6.5.2.6关键路径
AOE网
在带权的有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如完成活动所需的时间),称之为用边表示活动的网络,简称AOE网(Activity On Edge NetWork)。
性质
- 只有在某个顶点所代表的事件发生以后,从该点出发的各有向边所代表的活动才能开始
- 只有在进入某顶点的各有向边所代表的活动都已经结束时,该顶点所代表的事件才能开始,且有一些活动考研并行执行。
专业术语
- 在AOE网中,仅有一个入度为0的顶点,称为开始顶点(源点),表示整个工程的开始,也仅有一个出度为0的顶点,称为结束顶点(汇点),表示整个工程结束。
- 从源点到汇点的有向路径可能有多条,所有路径中,具有最大路径长度的路径称为关键路径,而把关键路径上的活动叫关键活动。
- 完成整个工程的最短时间就是关键路径的长度,若关键活动不能按时完成,就会影响整个工程的完成时间。
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JScVHSfX-1666602897756)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221008204455249.png)]
求关键路径的步骤
特性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T1NRX0xZ-1666602897757)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221008205329652.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HbLDB2Xm-1666602897758)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221008205347519.png)]
第7章、查找算法
7.1 查找的基本概念
7.1.1基本概念
查找——在数据集合中寻找满足某种条件的数据元素的过程称为查找
查找表(查找结构)——用于查找的数据集合称为查找表,它由同一类型的元素(或记录)组成
关键字——数据元素中唯一标识该元素某个数据项的值,使用基于关键字的查找,查找结构应该是唯一的。
7.1.2对查找表的常见操作
- 查找符合条件的数据元素
- 插入、删除某个数据元素
7.1.3查找算法评价指标
查找长度——在查找运算中,需要比对关键字的次数,称为查找长度
平均查找长度——所有查找过程中进行关键字的比较次数平均值
7.2 几种查找算法
7.2.1顺序查找
顺序查找
,也叫线性查找,通常用于线性表。(顺序存储、链式存储)
算法思想
:从头到尾或者从尾到头挨个查找
7.2.1.1顺序查找的实现
typedef char ElemType;
typedef struct {
ElemType *elem;//动态数据基地址
int TableLen;//表长
}SSTable;
//顺序查找
int Search(SSTable ST, ElemType key) {
int i;
for (i = 0; i < ST.TableLen && ST.elem[i] != key; i++)
return i == ST.TableLen ? -1 : i;
}
//哨兵方法:不需要判断是否越界,执行效率更高,
//严蔚敏数据结构说>1000的可以提升百分数50但是本人测试数据10万差距并没有那么多大根提升百分之15左右
int Search_2(SSTable ST, ElemType key) {
ST.elem[0] = key;//将哨兵插入0号元素
int i;
for (i = ST.TableLen; ST.elem[i] != key; --i)
return i;//查找成功返回下标元素,查找失败返回0
}
7.2.1.2查找效率分析
7.2.1.3顺序查找的优化
对有序表
被查找概率不等
7.2.2折半查找
7.2.2.1算法思想
折半查找,也叫"二分查找",仅适用于**有序的顺序表**。
7.2.2.2实现代码
typedef int ElemType;
typedef struct {
ElemType *elem;
int Tablen;//表长
}SSTable;
//折半查找
int Binary_search(SSTable ST, ElemType key) {
int Low = 0, high = ST.Tablen - 1, mid;
while (Low <= high) {
//取中间位置
mid = (Low + high) / 2;
if (ST.elem[mid] < key)
//从后半部分继续查找
Low = mid + 1;
else if (ST.elem[mid] > key)
//从前半部分继续查找
high = mid - 1;
else
//若查找成功则返回
return mid;
}
//查找失败,返回-1
return -1;
}
7.2.2.3查找判断树
如果当前low和high有奇数个元素,则mid分隔以后,左右两部分元素个数相等。
如果当前low和high之间有偶数个元素,mid分隔以后,左半部分比右边部分元素少一个。
折半查找判定树中,若[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1GGvTItz-1666602897759)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221016162517947.png)],那么对于任何一个结点,必有:
右子树结点树-左子树结点树=0或1
折半查找判定树一定是一个平衡二叉树!
在折半查找判定树中,只有最下面一层不满,因此元素个数为n时[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jkd321M5-1666602897761)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221016162952723.png)]
判定树结点关键字:左<中<右,满足二叉排序树的定义,而且还是一个平衡二叉排序树
失败结点:n+1个(等于成功结点空链域数量)
7.2.2.4折半查找效率
ASL成功 = (1 * 1 + 2 * 2 + 3 * 4 + 4 * 4) / 11 = 3
ASL失败 = (3 * 4 + 4 * 8)/12 = 11/3
折半查找**时间复杂度 =** [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yCb3c0tj-1666602897762)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221016163342040.png)]
7.2.3分块查找
7.2.3.1分块查找算法思想
索引表中保存的是每个分块的最大关键字和分块的存储区间。
分块查找,也叫**索引顺序查找**,算法如下:
- 在索引表中确定带查询记录所属块(可顺序、可折半)
- 在块内顺序查找
特点:块内无序,块间有序
//索引表
typedef struct{
ElemType maxValue;
int low, high;
}Index;
//顺序表存储实际元素
ElemType List[100]
7.2.3.2用折半查找查索引
若索引表不包含目标关键字,则折半查找索引最终停留在low>high的位置,要**在low所指分块查找**。
原因:最终low左边一定小于目标关键字,high右边一定大于目标关键字,而分块存储的索引表中保存的是各个分块的最大关键字。
7.2.3.3查找效率分析
顺序查找:
Li=(b+1)/2 Ls=(s+1)/2
b=n/s===> ASL = Li+Ls= ,当 s = √n 时, ASL最小 = √n + 1
折半查找:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-syWS4IsG-1666602897763)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221016171423425.png)]
7.3 B树
7.3.1 五叉查找树
7.3.1.1 声明
struct Node {
ElemType key[4];//最多4个关键字
Node * child[5];//最多5个孩子
int num;//结点中有几个关键字
};
7.3.1.2查找效率
若每个结点内关键字过少,导致树变高,需要查找更多结点,查找效率变低
策略
:m叉查找树中,规定除了根节点以外,任何一个结点至少有个分叉,即至少有个关键字
eg.对于五叉树而言,规定任何结点都至少有3个分叉,2个关键字。
策略
:m叉查找树中,规定对于任何结点,其子树的高度都要相同。
7.3.2 B树
7.3.2.1 B树的定义
定义
- B树,又称**多路平衡查找树**,B树中的所有结点的孩子个数的最大值称为B树的阶,通常用m表示。一颗m阶B树或为空树,或为满足如下特性的m叉树。
特性
- 树中的每一个结点至多有m棵子树,即至多含m-1个关键字
- 若根结点不是终端结点,则至少有两棵树
- 除根结点以外的所有非叶子结点至少有棵子树,即至少含有个关键字
- 所有非叶子结点的结构如下:
- 所有的**叶子结点**都出现再同一层次,并且不带信息。
m阶B树的核心特性
-
根节点的子树数目∈[2,m],关键字数目∈[1,m-1]
其他结点的子树数目∈;关键字数目∈
-
任一结点,其所有子树高度相同
-
关键字的值:子树0<关键字1<子树1<关键字2<子树2<……(类别二叉查找树,左子树关键字<中<右)
7.3.2.2 B树的高度
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fWmHQoYx-1666602897765)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221018185337071.png)]
最小高度
- 让每个结点尽可能的满,有m-1个关键字,m个分叉,则因此
最大高度
-
让各层分叉尽可能少,即根结点2个分叉,其他结点个分叉,各层结点至少有:
第一层 1、第二层 2、第三层……第h层
第h+1层共有叶子结点(失败结点)
n个关键字的B树必有n+1个叶子结点,
7.3.2.3 B树的相关操作
五阶B树——结点关键字个数即:2<=n<=4**(注:此处省略失败结点)**
B树的插入
- 在插入key后,若导致原结点关键字字数超过上限,则从中间位置(),将其中的**关键字分为两部分**,左部分包含的关键字放在原结点中,右部分包含的关键字放在新结点中,中间位置()的结点插入原结点的父结点
- 若此时导致**父节点的关键字个数也超过了上限**,则继续进行这种分裂操作,直到这个过程传到根节点为止,进而导致B树高度增1.
- 新元素一定是插入最底层“终端结点”,用“查找”来确定插入位置
B树的删除
-
若删除的关键字在**终端结点**,关键字不低于下限则直接删除该关键字
-
若被删除关键字在**非终端结点**,则用直接前驱或直接后继来代替被删除的关键字
- 直接前驱:当前关键字**左侧指针所指子树"最右下"的元素**
- 直接后继:当前关键字**右侧指针所指子树"最左下"的元素**
-
若删除的元素在**终端结点,但是该结点的关键字低于下限**
兄弟够借
-
当右兄弟很宽裕时,用当前结点的后继,后继的后继来填补空缺
-
-
当左兄弟很宽裕时,用当前结点的前驱,前驱的前驱来填补空缺
-
兄弟不够借
- 若被删除关键字所在结点删除前的关键字个数低于下限,且此时该结点相连的左、右兄弟结点的关键字个数均=,则将关键字删除后与**左(或右)兄弟结点及双亲结点的关键字进行合并**。
- 若双亲结点不是根结点,且关键是个数减少到,则将之与自己的兄弟结点合并,重复上述操作,直到符合B树的要求。
-
7.3.3 B+树
7.3.3.1 B+树的定义
一棵**m阶的B+树**需满足以下条件:
- 每个分支结点最多有m棵子树(孩子结点)
- **根结点不是叶子**结点时至少有两颗子树,其他每个分支结点至少有棵树。
- ❗️结点的子树个数和关键字个数相等❗️B树有m个关键字有m+1棵子树
- 所有的**叶子结点包含全部关键字及指向相应记录的指针,叶子结点中将关键字按大小顺序排列,并且相邻叶子结点按照大小顺序互相连接起来**。(B+树支持顺序查找)
- 所有分支结点(蓝色部分)中仅包含它各个结点中关键字的最大值及指向其子结点的指针。
7.3.3.2 B+树的查找
在B+树中一共有两种查找方式:
1、从根节点开始查找
2、从结点p开始顺序查找
在B+树中,无论查找成功与否,最终都要走到最下面一层结点
在B树中,可能停留在查找的某一层。
7.3.3.3 B+树 VS B树
m阶B树
-
n个关键字对应n+1棵子树
-
根结点关键字数n∈[1,m-1]
其他结点关键字数n∈[,m-1]
-
在B树中各个结点包含的关键字不重复
-
B树的结点中包含有关键字对应的记录的存储地址
m阶B+树
-
结点中的n个关键字对应n棵子树
-
根结点关键字数n∈[1,m]
其他结点关键字数n∈[,m]
-
在B+树中,叶子结点包含全部关键字,非叶子结点出现过的关键字也会出现在叶子节点中
-
在B+树中,叶子节点包含信息,非叶子结点仅起到索引作用,非叶子结点中的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址
7.4 散列查找
7.4.1 拉链法
7.4.1.1散列表的定义
散列表
(Hash Table),又称**哈希表,是一种数据结构,其特点是:数据元素的关键字与其存储地址直接相关**。
如何建立"关键字"和"存储地址"的联系?——————通过"散列函数(哈希函数)"
例子
:有一堆数据元素,关键字分别为{19,14,23,1,68,20,84,27,55,11,10,79},散列函数 H(key)=key%13
若不同关键字通过一个散列函数映射到了同一个值,那么就叫它们"同义词",通过散列函数确定的位置已经存放了其他元素,则称这种情况叫“冲突”。
7.4.1.2解决散列表冲突
用拉链法(也叫链接法、链地址法)处理“冲突”:把所有“同义词”存储在一个链表中。
查找目标:27
- 通过散列函数计算目标元素存储地址:Addr = H(Key)
- 27 % 13 = 1
- 27的查找长度 = 3
查找目标21
- 通过散列函数计算目标元素存储地址:Addr = H(Key)
- 21 % 13 = 8
- 21的查找长度 = 0 (大多数学校将空指针不示为查找,有一些学校也会将空指针的判定作一次比较)
查找长度 —— 在查找运算中,需要对比关键字的次数称为查找长度
ASL成功 = 1* 6 + 2* 4 + 3 + 4 /12 =1.75
ASL失败 = 0+ 4+ 0 + 2+ 0+ 0+ 2+ 1+ 0+ 0+ 2+ 1+ 0/13 = 0.92
在理想情况下:散列查找实际复杂度可以达到O(1)
7.4.1.3常见的散列函数
除留余数法
H(Key) = Key % p
散列表表长为m,取一个不大于m但是最接近或等于m的**质数**p
直接定址法
H(Key) = Key 或 H(Key) = a*Key + b
其中,a和b是常数。这种方法计算最简单,不会产生冲突,它适合**关键字分布基本连续的情况**,若关键字分布不连续,空位较多,则容易造成存储空间浪费。
数字分析法
选取数码分布较为均匀的若干位作为散列地址
- 设关键字是r进制(如十进制),而**r个数码在各位上出现的频率不一定相同,可能在某些位上分布均匀一些**,每种数码出现的机会均等
- 而在某些位分布不均匀,只有某几种数码经常出现,此时可以选取数码较为均匀的若干散列地址。这种方法适合已知关键字集合,若更换了关键字,则需要重新构造新的散列函数。
平方取中法
取关键字的平方值的中间几位作为散列地址
具体取多少位视实际情况而定,这种方法得到的散列地址与关键字的每位都有关系,因此使得散列地址分布较为均匀,适用于关键字的每位取值都不够均匀或均小于散列地址所需的位数。
7.4.2 开放地址法
7.4.2.1 定义
所谓**开放定址法**,是指可存放新表项的空闲地址既向它的同义词表项开放,又向它的非同义词表项开放,其数学递推式为:
i = 0,1,2,……,k (k<=m-1),m表示散列表表长;di为增量序列,i可以理解为"第i次发生冲突"
7.4.2.2线性探测法
1、定义
线性探测法
——di = 0,1,2,3,……,m-1;即发生粗体时,每次往后探测相邻的下一个单元是否为空。
- H(key) = 1%13 = 1 H0=(1+d0)%16=1 发生冲突 H1=(1+d1)%16=2
- H(key) = 84%13 = 6 H0=(6+d0)%16=6 发生冲突 H1=(6+1)%16=7 发生冲突 H2=(6+2)%16=8
- H(key) = 27%13 = 1 发生冲突 H1=2 发生冲突 H2=3 发生冲突 H3=4
- ……
若加入一个关键字为25
则 H(25)=25%13=12 哈希函数值域[0,12]
H1=(H(Key)+1)%16=13 冲突处理函数值域[0,15]
2、查找操作
- **查找27:**H(Key)=27%13=1 冲突 H1=2 冲突 H2=3 冲突 H3=4 27的查找长度为4
- **查找11:**H(Key)=11%13=11 11的查找长度为1
- **查找21:**H(Key)=21%13=8 冲突 H1=9 冲突 H2=10 冲突 H3=11 冲突 H4=12 冲突 H5=13 21查找长度6
3、删除操作
采用"开放地址法"时,删除结点不能简单地将被删除的结点空间置空,否则会将截断在它之后填入散列表的同义词结点的查找路径,可以做一个"删除标记font>",进行逻辑删除。
4、查找效率分析
线性探测法很容易造成同义词、非同义词的"聚集(堆积)现象",严重影响查找效率
产生原因——冲突后再探测一定是放在某个连续的位置
7.4.2.3平方探测法
平方探测法
:
d0=0 d1=1 d2=-1 d3=4 d4=-4 d5=9 d6=-9 ……
平方探测法比起线性探测法更不容易产生"聚集(堆积)"问题
非重点小坑:散列表长度m必须是一共可以表示成4j+3的质数,才能探测所有位置
7.4.2.4伪随机序列法
di是一个伪随机序列,如di=0,4,65,2,8,22……
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GknqOJ4x-1666602897767)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221019201108482.png)]
7.4.2.5再散列法
再散列法
(再哈希法):除了元素的散列函数H(Key)之外,还可以多准备几个散列函数,当散列函数冲突时,用下一个散列函数计算一个新地址,直到不冲突为止。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WEbaMbi7-1666602897769)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221019201320607.png)]
第8章、排序算法
8.1排序的基本概念
8.1.1定义
8.1.2排序算法的评价指标
8.1.3排序算法的分类
8.2 插入和希尔排序
8.2.1 插入排序
8.2.1.1 基本概念
算法思想
:每次将一个待排序的记录按其关键字大小插入到已经排好序的子序列中,直到全部记录插入完成。
8.2.1.2 代码实现
//直接插入排序
void InsertSort(int A[]) {
int i, j, temp;
for (i = 0; i < Lengh; i++)
if (A[i] < A[i - 1]) {
temp = A[i];
for (j = i - 1; j >= 0 && A[j] > temp; j--)
A[j + 1] = A[j];
A[j + 1] = temp;
}
}
//直接插入排序,带哨兵
void InsertSort2(int B[]) {
int i, j;
//注意这里是Lengh!!!
for (i = 2; i <= Lengh; i++)
if (B[i] < B[i - 1]) {
B[0] = B[i];
for (j = i - 1; B[0] < B[j]; --j)
B[j + 1] = B[j];
B[j + 1] = B[0];
}
}
8.2.1.3 算法效率分析
空间复杂度:O(1)
时间复杂度:主要来自对比关键字、移动元素若有n个元素,则需要n-1趟处理
**最好**情况:原本就有序——O(n)
**最坏**情况:原本逆序——O(n*n)
8.2.1.4优化——折半插入排序
思路:先用折半查找找到应该插入的位置,再移动元素。
当A[mid]==A[0]时,为了保证算法的"稳定性",应该继续在mid所指位置右边寻找插入位置。
当low>high时折半查找停止,将[low,i-1]内元素全部右移,并且将A[0]复制到low所指位置
//折半法插入
void InsertSort3(int C[]) {
int i, j, high, low, mid;
for (int i = 2; i <= Lengh; i++) {
C[0] = C[i];
high = i - 1, low = 1;
while (low <= high) {
mid = (low + high) / 2;
if (C[mid] < C[0])
low = mid + 1;
else
high = mid - 1;
}
for (int j = i - 1; j >= low; j--)
C[j + 1] = C[j];
C[low] = C[0];
}
}
8.2.2 希尔排序
8.2.2.1基本概念
具体思想
:先追求表中元素部分有序,再逐渐逼近全局有序。
8.2.2.2算法实现
#include <iostream>
#include <string>
using namespace std;
#define Lengh 9
void Print(int A[]) {
for (int i = 1; i < Lengh; i++)
cout << "A[" << i << "] = " << A[i] << "\t";
cout << endl;
}
void ShellSort(int A[]) {
int d, i, j;
for (d = Lengh / 2; d >= 1; d /= 2) {
for (i = d + 1; i < Lengh; ++i)
if (A[i] < A[i - d]) {
A[0] = A[i];
for (j = i - d; i > 0 && A[j] > A[0]; j -= d)
A[j + d] = A[j];
A[j + d] = A[0];
}
cout << "d = " << d << ":" << endl;
Print(A);
}
}
void ShellSort2(int B[]) {
int d, i, j, k;
for (d = Lengh / 2; d >= 1; d /= 2) {
for (i = 1; i <= d; ++i) {
for (j = d + i; j < Lengh; j += d)
if (B[j] < B[j - d]) {
B[0] = B[j];
for (k = j - d; k > 0 && B[0] < B[k]; k -= d)
B[k + d] = B[k];
B[k + d] = B[0];
}
}
cout << "d = " << d << ":" << endl;
Print(B);
}
}
int main() {
int A[Lengh] = { 0,49,38,65,97,76,13,27,49 };
ShellSort(A);
system("pause");
return 0;
}
8.2.2.3算法性能分析
希尔排序是不稳定的,并且仅适用于顺序表不适合链表。
8.3 交换排序
基于交换的排序:根据序列中两个关键字的比较结构来对换这两个记录在序列中的位置
8.3.1冒泡排序
定义
- 从后往前(或从前往后)两两比较相邻元素的值,若为逆序(A[i-1]>A[i]),则交换他们,直到序列比较完,称这样的过程为"一趟"冒泡排序。
代码实现
void BubbleSort(int A[]) {
for (int i = 0; i < lengh; i++) {
bool flag = false;//表示本趟冒泡是否发生交换
for (int j = lengh - 1; j > i; j--)
if (A[j - 1] > A[j]) {//若为逆序
int temp = A[j - 1];
A[j - 1] = A[j];
A[j] = temp;
flag=true;
}
if(flag==false)
return;//本趟遍历后未发生交换
}
}
算法性能分析
- 空间复杂度:O(1)
- 最好情况(有序)
- 比较次数n-1;交换次数=0
- 最好时间复杂度=O(n)
- 最坏情况(逆序)
- 比较次数(n-1)+(n-2)+……+1=交换次数
- 最坏时间复杂度=O(n^3)
- 平均时间复杂度=O(n^2)
注意!
交换次数指的是调用了多少次swap
而移动元素指的是swap里面的移动元素。每次交换都需要移动元素3次
如果某一趟排序过程中未发生交换,算法可以提前结束
8.3.2快速排序
算法思想
- 在待排序表L[1……n]中任取一个元素pivot作为枢轴(或基准,通常取首元素)
- 通过一趟排序将待排序列表划分为独立的两个部分L[1……k-1]和L[k+1……n],使得L[1……k-1]中所有元素都小于pivot,L[k+1……n]中所有元素大于等于pivot
- 将pivot放在其最终位置L(k)上
- 递归的对两个子表重复上述过程,直至每个部分只有一个元素或为空
代码实现
//- 划分函数
//1. 创建一个枢轴
//2. 用low和high指针搜索枢轴最终位置
//3. 返回枢轴位置
int Partition(int A[], int Low, int high) {
int pivot = A[Low];
while (Low < high) {
while (Low < high && A[high] >= pivot)
high--;
A[Low] = A[high];
while (Low < high && A[Low] <= pivot)
Low++;
A[high] = A[Low];
}
//while循环结束,此时high和low指向同一个位置,需要将空缺用枢轴补上
A[Low] = pivot;
return Low;//返回最终枢轴的位置
}
//- 快排函数
//1. 判断low和high指针的位置
//2. 进行划分
//3. 递归调用快排
void Quick_Sort(int A[], int Low, int high) {
if (Low < high) {
int pos = Partition(A, Low, high);
Quick_Sort(A, Low, pos - 1);
Quick_Sort(A, pos + 1, high);
}
}
算法效率分析
-
时间复杂度
- 初始序列待排序元素(0~7) low++,high-- O(n)
- 第一层Quick_Sort待排序元素(02,47) low++,high-- O(n)
- 第二层Quick_Sort待排序元素(0,2,4~5,7) low++,high-- O(n)
- ……
综上所述:每一层Quick_Sort只需要处理剩余的排序元素,且时间复杂度不超过O(n)
时间复杂度 = O(n * 递归层数)
由二叉树性质可知:
最好时间复杂度 = [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PsJADJru-1666602897770)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221023141254582.png)]
最坏时间复杂度 = [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k5SahxQ8-1666602897771)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221023141322086.png)]
实际应用复杂度 = [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xaQniNSJ-1666602897772)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221023141254582.png)]
-
空间复杂度
空间复杂度 = O(递归层数)
最好空间复杂度 = [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yYnfAVQj-1666602897773)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221023141356620.png)]
最坏空间复杂度 = [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tMxQ0J65-1666602897774)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221023141405713.png)]
- 把n个元素组织成二叉树,二叉树的层数就是递归调用的深度
- n个结点的二叉树 最小高度 = [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KBnOFewx-1666602897775)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221023141057511.png)] 最大高度 = n
快速排序的弊端
若每一次选中的**“枢轴”将待排序序列划分为均匀的两个部分,则递归深度最小,算法效率最高**。
反而言之,若初始序列**有序或逆序,则快速排序性能最差**,因为每次选择的都是最靠边的元素。
快速排序算法优化思路
- 选头、中、尾三个位置的元素,取中间值作为枢轴元素
- 随机选取一个元素作为枢轴元素
8.4选择排序
每一趟在待排序元素中选取关键字最小(或最大)的元素加入有序子序列
8.4.1 简单选择排序
代码实现
//简单选择排序
void Select_Sort(int A[]) {
for (int i = 0; i < Lengh - 1; i++) {
int min = i;//记录最小元素位置
for (int j = i + 1; j < Lengh; j++) {
if (A[j] < A[min])
min = j;
}
if (min != i) {
int temp = A[i];
A[i] = A[min];
A[min] = temp;
}
}
}
复杂度分析
- 空间复杂度 O(1)
- 时间复杂度 O(n^2)
- 无论是有序、逆序、还是乱序,都需要经过n-1次处理
- 需要对比(n-1)+(n-1)+……+1次
8.4.2 堆排序
8.4.2.1什么是堆
优先队列
- 优先队列(Priority Queue):特殊的队列,取出元素的顺序是按照元素的**优先权(关键字)**大小,而不是元素进入队列的先后顺序。
实现
数组
:
- 插入——元素总是插入尾部O(1)
- 删除——查找最大(或最小)关键字O(n)
- 从数组中删去需要移动的元素O(n)
链表
:
- 插入——元素总是插入链表的头部O(1)
- 删除——查找最大(或最小)关键字O(n)
- 删去结点O(1)
有序数组
:
- 插入——找到合适的元素O(n)或
- 移动元素并插入O(n)
- 删除——删去最后一个元素O(1)
有序链表
:
- 插入——找到合适的元素O(n)
- 插入元素O(1)
- 删除——首元素或者最后元素O(1)
8.4.2.2二叉树的存储
二叉查找树
- 若用二叉查找树,插入结点Log N,删除最大、最小结点,在最左边或者最右边,时间效率高!
- 但是,一直进行删除最大/最小结点,整个树容易歪掉,树的高度不再是Log N
完全二叉树
结构性:
用数组表示的完全二叉树
有序性:
任何一个结点的关键字是其子树所有结点的最大或者最小值
🔳最大堆,也叫大顶堆:最大值
🔳最小堆,也叫小顶堆:最小值
8.4.2.3例题
【最大堆和最小堆】
【不是堆】
8.4.2.4堆的插入
最大堆的创建
- 思路:把所有非终端结点都检查一遍,是否满足最大堆的要求,若不满足进行调整
- 检查当前结点是否**根>左>右**,若不满足将当前结点与更大的孩子交换
- 若元素互换破坏了下一级的堆,则采用相同方法继续调整
//王道代码
void HeadAdjust(int A[],int k,int len) {
//A[0]暂存子树的根结点
A[0] = A[k];
for (int i = 2 * k; i <= len; i *= 2) {
//判断i和len的作用是看该i 有没有右兄弟
if (i < len && A[i] < A[i + 1])
//判断左右子树的值哪个更大
i++;
//判断根结点和左右子树的大小
if (A[0] >= A[i])
break;
else {
A[k] = A[i];
//***修改k的值以便于继续向下筛选***
k = i;
}
}
//被筛选的结点的值放入最终位置
A[k] = A[0];
}
//建立大根堆
void BuildMaxHeap(int A[],int len) {
//遍历非终端结点
for (int i = len / 2; i > 0; i--)
HeadAdjust(A, i, len);
}
//浙大代码
typedef int ElemType;
typedef struct{
ElemType *elements;//存放堆元素的数组
int size;//堆的当前元素个数
int Capacity;//堆的最大容量
}HeapStruct,*MaxHeap;
MaxHeap Greate(int MaxSize) {
//创建容量为MaxSize的空的最大堆
MaxHeap H = (MaxHeap)malloc(sizeof(HeapStruct));
H->elements = (ElemType *)malloc((MaxSize + 1) * sizeof(ElemType));
H->size = 0;
H->Capacity = MaxSize;
H->elements[0] = MaxData;
//定义哨兵,为大于堆中所有可能元素的值,便于更快操作。
return H;
}
最大堆的插入
//将新增结点插入
void Insert(MaxHeap H, ElemType item) {
int i;
if (IsFull(H)) {
cout << "最大堆已满!" << endl;
return;
}
i = ++H->size;
//i指向插入后堆的最后一个元素的位置
for (; H->elements[i / 2] < item; i /= 2)
//i=i/2;一直到父节点值大于当前值
H->elements[i] = H->elements[i / 2];
H->elements[i] = item;
}
8.4.2.5堆的删除
取出根结点(最大值)元素,同时删除堆的一个结点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8RdVBBzL-1666602897777)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221011202216777.png)]
- 把31移至根
- 找出31较大的孩子
- 重复步骤二,直到31没有较大的孩子
ElemType Delete(MaxHeap H) {
//从最大堆H中取出键值为最大的元素,并且删除一个元素
int Parent, Child;
ElemType MaxItem, temp;
if (IsEmpty(H)) {
cout << "最大堆为空" << endl;
return;
}
MaxItem = H->elements[1];
//取出最大根结点
temp = H->elements[H->size--];
//相当于temp=H->elements[H->size]; H->size--;
//从最大堆中最后一个元素从根结点开始向上过滤下层结点
for (Parent = 1; Parent * 2 <= H->size; Parent = Child) {
Child = 2 * Parent;
if ((Child != H->size) && (H->elements[Child] < H->elements[Child + 1]))
Child++;
if (temp >= H->elements[Child])
//判断temp是否比左右儿子大的那一个大,若大,跳出循环
break;
else
H->elements[Parent] = H->elements[Child];
//不大,Parent=child
}
//给temp找一个位置
H->elements[Parent] = temp;
return MaxItem;
}
8.4.2.6堆的建立
建立最大堆
:将已经存在的N个元素,按照最大堆的要求存放再一个一维数组中。
- 方法1:通过插入操作,将N个元素一个个相继插入到一个初始为空的堆中去,时间代价为O(Nlog N)。
- 方法2:在线性时间复杂度下建立最大堆
- 将N个元素按输入顺序存入,先满足完全二叉树的结构特性
- 调整各个结点位置,以满足最大堆的有序特性。
- 思路:从最小的堆开始,从下往上,依次建堆
8.4.2.7 基于大根堆进行堆排序
- 选择排序:每一趟在待排序元素中选取关键字最大的元素加入有序子序列
- 堆排序
- 每一趟将堆顶元素加入有序子序列,与待排序序列中的最后一个元素交换
- 将待排序元素序列再调整为大根堆(小元素不断下坠)
//建立大根堆
void BuildMaxHeap(int A[], int len);
//将以k为根的子树调整为大根堆
void HeapAdjust(int A[], int k, int len);
//堆排序的完整逻辑
//堆排序的逻辑
void HeapSort(int A[], int len) {
//初始建堆
BuildMaxHeap(A, len);
for (int i = len; i > 1; i--) {
A[0] = A[i];
A[i] = A[1];
A[1] = A[0];
//将剩余的待排序元素整理为堆
HeadAdjust(A, i, i - 1);
}
}
8.4.2.8算法效率分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nczycsfv-1666602897778)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221023165318530.png)]
8.5 归并和基数排序
8.5.1归并排序
术语
-
归并:将两个或者多个已经有序的序列合并成一个
-
二路归并:即将两个有序序列合成一个,每选出一个小元素只需对比关键字1次。
-
四路归并:将四个有序序列合成一个,每选出一个小元素只需对比关键字3次。
归并排序
- 一个初始序列,将序列中的每个单独元素看成一个独立的,排好序的部分,将相邻的部分进行二路归并
代码实现
- 创建一个辅助数组,将原数组的元素移动到辅助数组中
- 将辅助数组的元素归并到原数组中,这里需要用到两个元素i,j
- 将剩余元素放入原数组
int B[MaxSize];
//1. 创建一个辅助数组,将原数组的元素移动到辅助数组中
//2. 将辅助数组的元素归并到原数组中,这里需要用到两个元素i,j
//3. 将剩余元素放入原数组
void Merge(int low, int mid, int high, int A[]) {
int i, j, k;
for (k = low; k <= high; k++)
B[k] = A[k];
for (i = low, j = mid + 1, k = i; i <= mid && j <= high; k++) {
if (B[i] <= B[j])
A[k] = B[i++];
else
A[k] = B[j++];
}
while (i <= mid) A[k++] = B[i++];
while (j <= high) A[k++] = B[j++];
}
void MergeSort(int A[], int low, int high) {
if (low < high) {
int mid = (low + high) / 2;
MergeSort(A, low, mid);
MergeSort(A, mid + 1, high);
Merge(low, mid, high, A);
}
}
算法效率分析
- 二路归并树——形态上是一颗倒立的二叉树
- 二叉树的第h层最多有[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QC7KVWIh-1666602897779)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221024140225482.png)]个结点,若树高为h,则满足[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HA8x5wGG-1666602897780)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221024140252228.png)]即[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4arg5Oqt-1666602897781)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221024140305494.png)]
- 稳定的算法
结论:
n个元素进行2路归并排序,归并的趟数就是二叉树的树高-1 =[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TmaWv5K7-1666602897782)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221024140408127.png)]
每趟归并的时间复杂度**O(n)** 算法的时间复杂度[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9WkP2bYF-1666602897783)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221024140441568.png)]
空间复杂度 = O(n),来自辅助数组B 递归工作栈[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KphQrgu4-1666602897784)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221024140408127.png)]总体上复杂度还是O(n)的数量级
8.5.2基数排序
实现步骤
定义说明
假设长度为n的线性表中每个结点aj的关键字由d元组组成。
其中,,r称为**“基数”**。
基数排序
基数排序得到的**递减**序列过程如下
- 初始化:设置**r个空队列**,
- 按照各个关键字位 权重递增的次序(个、十、百),对关键字分别进行分配和收集
- 分配:顺序扫描各个元素,若当前处理的关键字位=x,则将元素插入Qx队尾
- 收集:把各个队列中的结点依次出队并且链接
算法效率分析
//基数排序通常基于链式存储实现
typedef struct LinkNode{
ElemType data;
LinkNode * next;
}LinkNode,*LinkList;
//链队
typedef struct{
LinkNode * front,*rear;
}LinkQueue;
//收集代码
p->next = Q[n].front;
- 空间复杂度
- 需要r个辅助队列,O®
- 时间复杂度
- 一趟分配O(n),一趟收集O®,一共d趟分配+收集
- O(d(n+r))
- 稳定性
基数排序的应用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yvUqcNPb-1666602897785)(C:\Users\Adam\AppData\Roaming\Typora\typora-user-images\image-20221024145003505.png)]
8.6 外部排序
8.6.1 外部排序相关概念
8.6.1.1外存和内存之间的数据交换
8.6.1.2外部排序原理
构造初始归并段
第一趟归并
注意!当一个输入缓冲区空了以后需要立即将下一个归并段读入缓冲区
第二趟归并
第三趟归并
8.6.1.3影响外部排序的因素
8.6.1.4优化思路
使用多路归并可以优化外部排序,减少时间开销
K路平衡归并
- 最多只能有k个归并段并为1个
- 每趟归并中,若有m个归并段参与归并,则经过这趟处理得到[m/k]个新归并段(向上取整)
8.6.2 败者树
败者树
败者树——可以视为一颗完全二叉树(多路一个头)。k个叶结点分别是当前参与比较的元素,非叶子结点用于记忆左右子树的失败者,而让胜者往上进行比较,直到根结点。
败者树在多路平衡并归的应用
8.6.3 置换-选择排序
具体步骤
- 从FI输入w个记录到工作区WA
- 从WA中选取其中关键字取最小的记录,记为MINIMAX记录
- 将MINIMAX记录输出到FO
- 若FI不为空,从FI输入下一个记录到WA
- 从WA中所有关键字选出补MINIMAX记录大的记录中选取最小的关键字作为新的MINIMAX记录
- 重复步骤3~5,直到WA选不出新的MINIMAX记录为止,输出一个归并段的结束标志到FO
- 重复2~6,直到WA为空
8.6.4 最佳归并树
归并树的神秘性质
- 每个初始归并段看作一个叶子结点,归并段的长度作为结点权值
- 则上面这颗归并树的带权路径长度 WPL = 度磁盘次数 = 写磁盘次数
- 即要让磁盘I/O次数最少,就一定要让归并树的WPL最小——哈夫曼树
构造二路归并的最佳归并树
多路归并最佳归并树
注意!对于k叉归并,若初始归并段的数量无法构成严格的k叉归并树,则需要补充几个长度为0的虚段,再进行k叉哈夫曼树的构造。
添加虚段的数量
k叉的最佳归并树一定是一颗严格的k叉树,即树只有度为k和度为0的结点。
设度为k的结点有nk个,度为0的有n0个,归并树总结点树=n则:
初始归并段数量+虚段数量=n0
- 若(初始归并段-1)%(k-1) = 0 ,说明构成严格k叉树
- 若(初始归并段-1)%(k-1) = u != 0,则需要补充(k-1)-u个虚段
v-1666602897778)]
8.5 归并和基数排序
8.5.1归并排序
术语
-
归并:将两个或者多个已经有序的序列合并成一个
-
二路归并:即将两个有序序列合成一个,每选出一个小元素只需对比关键字1次。
-
四路归并:将四个有序序列合成一个,每选出一个小元素只需对比关键字3次。
归并排序
- 一个初始序列,将序列中的每个单独元素看成一个独立的,排好序的部分,将相邻的部分进行二路归并
代码实现
- 创建一个辅助数组,将原数组的元素移动到辅助数组中
- 将辅助数组的元素归并到原数组中,这里需要用到两个元素i,j
- 将剩余元素放入原数组
int B[MaxSize];
//1. 创建一个辅助数组,将原数组的元素移动到辅助数组中
//2. 将辅助数组的元素归并到原数组中,这里需要用到两个元素i,j
//3. 将剩余元素放入原数组
void Merge(int low, int mid, int high, int A[]) {
int i, j, k;
for (k = low; k <= high; k++)
B[k] = A[k];
for (i = low, j = mid + 1, k = i; i <= mid && j <= high; k++) {
if (B[i] <= B[j])
A[k] = B[i++];
else
A[k] = B[j++];
}
while (i <= mid) A[k++] = B[i++];
while (j <= high) A[k++] = B[j++];
}
void MergeSort(int A[], int low, int high) {
if (low < high) {
int mid = (low + high) / 2;
MergeSort(A, low, mid);
MergeSort(A, mid + 1, high);
Merge(low, mid, high, A);
}
}
算法效率分析
- 二路归并树——形态上是一颗倒立的二叉树
- 二叉树的第h层最多有[外链图片转存中…(img-QC7KVWIh-1666602897779)]个结点,若树高为h,则满足[外链图片转存中…(img-HA8x5wGG-1666602897780)]即[外链图片转存中…(img-4arg5Oqt-1666602897781)]
- 稳定的算法
结论:
n个元素进行2路归并排序,归并的趟数就是二叉树的树高-1 =[外链图片转存中…(img-TmaWv5K7-1666602897782)]
每趟归并的时间复杂度**O(n)** 算法的时间复杂度[外链图片转存中…(img-9WkP2bYF-1666602897783)]
空间复杂度 = O(n),来自辅助数组B 递归工作栈[外链图片转存中…(img-KphQrgu4-1666602897784)]总体上复杂度还是O(n)的数量级
8.5.2基数排序
实现步骤
定义说明
假设长度为n的线性表中每个结点aj的关键字由d元组组成。
其中,,r称为**“基数”**。
基数排序
基数排序得到的**递减**序列过程如下
- 初始化:设置**r个空队列**,
- 按照各个关键字位 权重递增的次序(个、十、百),对关键字分别进行分配和收集
- 分配:顺序扫描各个元素,若当前处理的关键字位=x,则将元素插入Qx队尾
- 收集:把各个队列中的结点依次出队并且链接
算法效率分析
//基数排序通常基于链式存储实现
typedef struct LinkNode{
ElemType data;
LinkNode * next;
}LinkNode,*LinkList;
//链队
typedef struct{
LinkNode * front,*rear;
}LinkQueue;
//收集代码
p->next = Q[n].front;
- 空间复杂度
- 需要r个辅助队列,O®
- 时间复杂度
- 一趟分配O(n),一趟收集O®,一共d趟分配+收集
- O(d(n+r))
- 稳定性
基数排序的应用
[外链图片转存中…(img-yvUqcNPb-1666602897785)]
8.6 外部排序
8.6.1 外部排序相关概念
8.6.1.1外存和内存之间的数据交换
8.6.1.2外部排序原理
构造初始归并段
第一趟归并
注意!当一个输入缓冲区空了以后需要立即将下一个归并段读入缓冲区
第二趟归并
第三趟归并
8.6.1.3影响外部排序的因素
8.6.1.4优化思路
使用多路归并可以优化外部排序,减少时间开销
K路平衡归并
- 最多只能有k个归并段并为1个
- 每趟归并中,若有m个归并段参与归并,则经过这趟处理得到[m/k]个新归并段(向上取整)
8.6.2 败者树
败者树
败者树——可以视为一颗完全二叉树(多路一个头)。k个叶结点分别是当前参与比较的元素,非叶子结点用于记忆左右子树的失败者,而让胜者往上进行比较,直到根结点。
败者树在多路平衡并归的应用
8.6.3 置换-选择排序
具体步骤
- 从FI输入w个记录到工作区WA
- 从WA中选取其中关键字取最小的记录,记为MINIMAX记录
- 将MINIMAX记录输出到FO
- 若FI不为空,从FI输入下一个记录到WA
- 从WA中所有关键字选出补MINIMAX记录大的记录中选取最小的关键字作为新的MINIMAX记录
- 重复步骤3~5,直到WA选不出新的MINIMAX记录为止,输出一个归并段的结束标志到FO
- 重复2~6,直到WA为空
8.6.4 最佳归并树
归并树的神秘性质
- 每个初始归并段看作一个叶子结点,归并段的长度作为结点权值
- 则上面这颗归并树的带权路径长度 WPL = 度磁盘次数 = 写磁盘次数
- 即要让磁盘I/O次数最少,就一定要让归并树的WPL最小——哈夫曼树
构造二路归并的最佳归并树
多路归并最佳归并树
注意!对于k叉归并,若初始归并段的数量无法构成严格的k叉归并树,则需要补充几个长度为0的虚段,再进行k叉哈夫曼树的构造。
添加虚段的数量
k叉的最佳归并树一定是一颗严格的k叉树,即树只有度为k和度为0的结点。
设度为k的结点有nk个,度为0的有n0个,归并树总结点树=n则:
初始归并段数量+虚段数量=n0
- 若(初始归并段-1)%(k-1) = 0 ,说明构成严格k叉树
- 若(初始归并段-1)%(k-1) = u != 0,则需要补充(k-1)-u个虚段