数据结构课程设计--基于知识图谱的中医食疗数据管理与应用

记录课设码调试过程中遇到的问题

目录

第1关:系统函数调用

题目描述

主要问题

体会

代码

第2关:食材信息增加

主要问题:

体会

主要函数

第3关:食材信息删除

第4关:食材信息修改

第5关:基于顺序表的顺序查找

主要函数

时空复杂度和ASL分析

第6关:基于顺序表的折半查找

折半查找

求折半查找的平均查找长度

时空复杂度和ASL

第7关:基于链表的顺序查找

主要函数

第8关:基于二叉排序树的查找

参考资料

主要函数

二叉排序树的插入

二叉排序树的查询

二叉排序树的ASL

时空复杂度和ASL

第9关:基于字典树的查找

参考资料

主要函数

初始化字典树

字典树构建

字典树查询

字典树平均查找长度

注意点

时空复杂度和asl分析

第10关:基于开放地址法的散列查找

参考资料

主要函数

InitHT

Hash计算哈希值

HTInsert往散列表中插入新元素

SearchHash在哈希表中查找

注意点

时空复杂度和asl分析

第11关:基于链地址法的散列查找

参考资料

注意点

主要函数

链表数组初始化

计算哈希值

往散列表中插入元素

查找散列表

时空复杂度和asl分析

第12关:基于KMP算法的食材关键信息查询

主要函数

第13关:直接插入排序

主要函数

第14关:折半插入排序

主要函数

第15关:简单选择排序

主要函数

第16关:冒泡排序

注意点

主要函数

第17关:快速排序

注意点

主要函数

时空复杂度和asl分析

第18关:高位优先字符串排序

gpt对话

主要函数

第19关:基于规则的实体识别

基本思路

主要函数

expandAsterisks

IndexBF

EntityRecognition

第20关:基于规则的关系抽取

基本思路

主要函数

RelationExtraction

indexBF

第21关:基于邻接表的知识图谱构建

第22关深搜

注意点

函数流程图

主要函数

DFS

第23关:基于编辑距离的食材功效矩阵构建

主要函数

第24关:基于 Dijkstra 算法的食材推荐

主要函数

第25关:基于 Floyd 算法的食材推荐

学习资料

主要函数


第1关:系统函数调用

题目描述

一个系统函数,树形结构,输入一段序列表示调用序列,如1230000,表示调用第二层的第一个函数=》调用第三层的第2个函数=》call第四层的第3个函数=》0表示返回上一层调用函数=》返回上一层=》返回上一层=》返回上一层

主要问题

  1. 通过switch-case的方式在每一层中去调用下一层函数,需要设置一个标志位,来表示在树形结构中,是往下调用,还是向上返回(因为反映到代码中都是对函数的调用,而返回到上一层则不需要打印返回地点的信息)
  2. systemCall函数同理,需要控制cin>>s只能在向下调用时启动,所以也需要一个flag特判,否则返回到最终的时候,还需要输入一个回车符,才能够打印“退出系统”

体会

  1. 通过在代码中增加打印的调式字段来追踪程序运行过程,效果要优于设置断点debug

代码

#include<bits/stdc++.h>
using namespace std;
//初始化全局变量,索引和空字符串 
int i = -1;
string s = "";
//设置标志位,用来区分函数调用是向上返回还是向下调用 
int flag=1;
// 以下定义的函数可供其他函数调用,以实现本题功能
// 本题的输入需自己设置,可以使用switch case实现调用对应的函数

//声明函数原型
void InsertFood();
void DeleteFood() ;
void ModifyFood() ;
void SeqSearch() ;
void BinarySearch(); 
void SearchList() ;
void SearchBST() ;
void TrieSearch() ;
void SearchHash();
void SearchHL() ;
void KMP() ;
void InsertSort(); 
void BInsertSort(); 
void SelectSort() ;
void BubbleSort() ;
void QuickSort();
void MSDSort() ;
void EntityRecognition(); 
void RelationExtraction(); 
void CreateUDG();
void QuestionAnswering(); 
void CreateAMG();
void ShortestPathDIJ(); 
void ShortestPathFloyd(); 
void FoodADM() ;
void FoodSearch();
void FoodSort();
void FoodManagement(); 
void CreateKG();
void QAAndRecommendation();
void SystemCall() ;


void InsertFood() {
	flag=0; 
    cout << "食材信息增加" << endl;
    if(s[++i]=='0')
    	FoodADM();
}

void DeleteFood() {
    cout << "食材信息删除" << endl;
    flag=0;
    if(s[++i]=='0')
    	FoodADM();
}

void ModifyFood() {
    cout << "食材信息修改" << endl;
    flag=0;
    if(s[++i]=='0')
    	FoodADM();
}

//
void SeqSearch() {
    cout << "基于顺序表的顺序查找" << endl;
    flag=0;
    if(s[++i]=='0')
    	FoodSearch();
    	
}

void BinarySearch() {
    cout << "基于顺序表的折半查找" << endl;
    flag=0;
    if(s[++i]=='0')
    	FoodSearch();
}

void SearchList() {
//	cout<<"i="<<i<<" ";
    cout << "基于链表的顺序查找" << endl;
    flag=0;
    if(s[++i]=='0'){
//    	cout<<"i="<<i<<" s[i]="<<s[i]<<" 返回上一层函数:FoodSearch"<<endl;
		FoodSearch(); 
	}
}

void SearchBST() {
    cout << "基于二叉排序树的查找" << endl;
    flag=0;
    if(s[++i]=='0')
    	FoodSearch();
}

void TrieSearch() {
	flag=0;
    cout << "基于字典树的查找" << endl;flag=0;
    if(s[++i]=='0')
    	FoodSearch();
}

void SearchHash() {
    cout << "基于开放地址法的散列查找" << endl;
    flag=0;
    if(s[++i]=='0')
    	FoodSearch();
}

void SearchHL() {
    cout << "基于链地址法的散列查找" << endl;
    flag=0;
    if(s[++i]=='0')
    	FoodSearch();
}


void KMP() {
    cout << "基于KMP算法的食材关键信息查询" << endl;
    flag=0;
    if(s[++i]=='0')
    	FoodManagement();
}

void InsertSort() {
    cout << "直接插入排序" << endl;
    flag=0;
    if(s[++i]=='0')
    	FoodSort();
}

void BInsertSort() {
    cout << "折半插入排序" << endl;
    flag=0;
    if(s[++i]=='0')
    	FoodSort();
}

void SelectSort() {
    cout << "简单选择排序" << endl;
    flag=0;
    if(s[++i]=='0')
    	FoodSort();
}

void BubbleSort() {
    cout << "冒泡排序" << endl;
    flag=0;
    if(s[++i]=='0')
    	FoodSort();
}

void QuickSort() {
    cout << "快速排序" << endl;
    flag=0;
    if(s[++i]=='0')
    	FoodSort();
}

void MSDSort() {
    cout << "高位优先字符串排序" << endl;
    flag=0;
    if(s[++i]=='0')
    	FoodSort();
}

void EntityRecognition() {
    cout << "基于规则的实体识别" << endl;
    flag=0;
    if(s[++i]=='0')
    	CreateKG();
}

void RelationExtraction() {
    cout << "基于规则的关系抽取" << endl;
    flag=0;
     if(s[++i]=='0')
    	CreateKG();
}

void CreateUDG() {
    cout << "基于邻接表的知识图谱构建" << endl;
    flag=0;
     if(s[++i]=='0')
    	CreateKG();
}

void QuestionAnswering() {
    cout << "基于路径推理的知识图谱多跳问答" << endl;
    flag=0;
     if(s[++i]=='0')
    	QAAndRecommendation();
}

void CreateAMG() {
    cout << "基于编辑距离的食材功效矩阵构建" << endl;
    flag=0;
     if(s[++i]=='0')
    	QAAndRecommendation();
}

void ShortestPathDIJ() {
    cout << "基于Dijkstra算法的食材推荐" << endl;
    flag=0;
     if(s[++i]=='0')
    	QAAndRecommendation();
}

void ShortestPathFloyd() {
    cout << "基于Floyd算法的食材推荐" << endl;
    flag=0;
     if(s[++i]=='0')
    	QAAndRecommendation();
}

void FoodADM() {
// 食材基本信息的增加、删除与修改模块,调用对应的功能函数实现
//	cout<<"i="<<i<<" ";
    if(flag==1)
		cout << "食材基本信息的增加、删除与修改" << endl;
	switch(s[++i]){
			case '1':
				flag=1;
				InsertFood();
				break;
			case '2':
				flag=1;
				DeleteFood();
				break;
			case '3':
				flag=1;
				ModifyFood();
				break;
			case '0':
				flag=0;
				FoodManagement();
				break;	
		} 
}

void FoodSearch() {
// 基于多种查找策略的食材信息查找模块,调用对应的功能函数实现
//	cout<<"i="<<i<<" "<<"s[i]="<<s[i];
    if(flag==1)cout << "基于多种查找策略的食材信息查找" << endl;
	switch(s[++i]){
			case '1':
				flag=1;
				SeqSearch();
				break;
			case '2':
				flag=1;
				BinarySearch();
				break;
			case '3':
				flag=1;
//				cout<<"i="<<i<<" s[i]="<<s[i]<<" ";
				SearchList();
				break;
			case '4':
				flag=1;
				SearchBST();
				break;
			case '5':
				flag=1;
				TrieSearch();
				break;
			case '6':
				flag=1;
				SearchHash();
				break;
			case '7':
				flag=1;
				SearchHL();
				break;
			case '0':
				flag=0;
//				cout<<"i="<<i<<" s[i]="<<s[i]<<" 返回上一层函数:FoodManagement"<<endl;
				FoodManagement();
				break;	
		} 
}

void FoodSort() {
// 基于多种排序策略的食材信息排序模块,调用对应的功能函数实现
    if(flag)
		cout << "基于多种排序策略的食材信息排序" << endl;
	switch(s[++i]){
			case '1':
				flag=1;
				InsertSort();
				break;
			case '2':
				flag=1;
				BInsertSort();
				break;
			case '3':
				flag=1;
				SelectSort();
				break;
			case '4':
				flag=1;
				BubbleSort();
				break;
			case '5':
				flag=1;
				QuickSort();
				break;
			case '6':
				flag=1;
				MSDSort();
				break;
			case '0':
				flag=0;
				FoodManagement();
				break;	
		} 
}

void FoodManagement() {
// 食材基本信息管理模块
// 调用FoodADM()、FoodSearch()、KMP()、FoodSort()函数实现
//	cout<<"i="<<i<<" ";
    if(flag)
		cout << "食材基本信息管理" << endl;
		switch(s[++i]){
			case '1':
				flag=1;
				FoodADM();
				break;
			case '2':
				flag=1;
//				cout<<"i="<<i<<" s[i]="<<s[i]<<" ";
				FoodSearch();
				break;
			case '3':
				flag=1;
				KMP();
				break;
			case '4':
				flag=1;
				FoodSort();
				break;
			case '0':
				flag=0;
//				cout<<"s[i]="<<s[i]<<" ";
//				cout<<"i="<<i<<" s[i]="<<s[i]<<" 返回上一层函数:SystemCall"<<endl;
				SystemCall();
				break;	
		} 
}

void CreateKG() {
// 中医食疗知识图谱构建模块,调用对应的功能函数实现

    if(flag)
		cout << "中医食疗知识图谱构建" << endl;
		switch(s[++i]){
			case '1':
				flag=1;
				EntityRecognition();
				break;
			case '2':
				flag=1;
				RelationExtraction();
				break;
			case '3':
				flag=1;
				CreateUDG();
				break;
			case '0':
				flag=0;
				SystemCall();
				break;	
		} 
}

void QAAndRecommendation() {
// 中医食疗问答与推荐模块,调用对应的功能函数实现
//    cout<<"i="<<i<<" s[i]="<<s[i]<<" ";
	if(flag)
		cout << "中医食疗问答与推荐" << endl;
//    if(s[++i]=='\0')return;
		switch(s[++i]){
			case '1':
				flag=1;
				QuestionAnswering();
				break;
			case '2':
				flag=1;
				CreateAMG();
				break;
			case '3':
				flag=1;
				ShortestPathDIJ();
				break;
			case '4':
				flag=1;
				ShortestPathFloyd();
				break;
			case '0':
				flag=0;
				SystemCall();
				break;	
		} 
}


void SystemCall() {
// 基于知识图谱的中医食疗数据管理与应用系统功能调用
// 调用FoodManagement()、CreateKG()、QAAndRecommendation()函数实现
		if(flag)cin>>s;
//		cout<<"s="<<s<<endl;
//		cout<<"i="<<i<<endl;
//		cout<<"i="<<i<<" s[i]="<<s[i]<<endl;
		switch(s[++i]){
			case '1':
//				cout<<"yes"<<endl;
				flag=1; 
//				cout<<"i="<<i<<" s[i]="<<s[i]<<" ";
				FoodManagement();
				break;
			case '2':
				flag=1;
				CreateKG();
				break;
			case '3':
				flag=1;
//				cout<<"i="<<i<<" s[i]="<<s[i]<<"调用QAAndRecommendation()"<<endl;
				QAAndRecommendation();
				break;
			case '0':
//				cout<<"i="<<i<<" s[i]="<<s[i]<<endl;
				cout<<"退出系统"<<endl;
				break;
		}
}

第2关:食材信息增加

主要问题:

  1. food.txt的编码方式在本地上要先改为ANSI,否则本地测试就会乱码
  2. 读文件,一个汉字在dev上占2个字节,在头歌上占3个字节,我们只需要把它当作字符串处理就可以了,然后使用字符串的substr()和c_str()函数实现截取和转换为char型数组
  3. InsertFood()函数中在读食谱数量和验方数量之后要getline换个行

体会

  1. 熟悉了substr的用法,对编码、字节有了更多的了解
  2. 通过增加打印字段来调试极为方便

主要函数

#include <bits/stdc++.h>
#define MAXSIZE 10000
using namespace std;
typedef struct{
	char name[100];		        // 中文名称
	char sname[100];	        // 英文名称
	char health[10000];	        // 养生功效
	char nutrition[10000];      // 营养与功效
	char expert[10000];	        // 专家提醒
	char link[10000];	        // 相关链接
	string recipe[30];	        // 养生保健食谱
	int recipe_size = 0;        // 食谱数量
	string therapy[30];	        // 食疗验方
	int therapy_size = 0;       // 验方数量
} Food;
typedef struct{
	Food *elem;                 // 指向数组的指针
	int length;                 // 数组的长度
} SqList;

void InitList(SqList &L){
// 使用动态内存分配new进行初始化
	L.elem = new Food[MAXSIZE];
	L.length = 0;
}

void FreeList(SqList &L){
// 释放内存
	delete[] L.elem;
}

/*
1.一个汉字在c++上占2个字节,头歌上占3个字节 
2.从第99个酱油开始只到相关链接字段 
*/
void ReadFile(SqList &L, string filename){
// 从文件中读取食材信息,将其按顺序存入L.elem指向的数组中
	//定义输入流 
	ifstream fin(filename);
	if(!fin.is_open()){
		exit(1);
	}
	int i=0;
	//循环读入每种食材的信息 
	while(!fin.eof()){
		//(1)中文名称
//		cout<<"i="<<i<<" ";
		string line;
		getline(fin, line);
//		cout<<"line="<<line<<" ";
		if(line.substr(0,10)=="中文名称:"){
			strcpy(L.elem[i].name,line.substr(10).c_str());
			getline(fin, line);
		}
		//(2)英文名称 
//		cout<<"name="<<L.elem[i].name<<endl; 
		if(line.substr(0,10)=="英文名称:"){
			strcpy(L.elem[i].sname,line.substr(10).c_str());
			getline(fin, line);
		}
		if(line=="#"){
			i++;
			continue;
		}		
		//(3)养生功效
		if(line.substr(0,10)=="养生功效:"){
			strcpy(L.elem[i].health,line.substr(10).c_str());
			getline(fin, line);
		}
		if(line=="#"){
			i++;
			continue;
		}		
		//(4)营养与功效
		if(line.substr(0,12)=="营养与功效:"){
//			cout<<"yes"<<endl;
			strcpy(L.elem[i].nutrition,line.substr(12).c_str());
			getline(fin, line);
		}	
		if(line=="#"){
			i++;
			continue;
		}		
		//(5)专家提醒 
		if(line.substr(0,10)=="专家提醒:"){
			strcpy(L.elem[i].expert,line.substr(10).c_str());
			getline(fin, line);
		}
		if(line=="#"){
			i++;
			continue;
		}		
		//(7)相关链接
		if(line.substr(0,10)=="相关链接:"){
			strcpy(L.elem[i].link,line.substr(10).c_str());
			getline(fin,line);
		}
		if(line=="#"){
			i++;
			continue;
		}		
		//(8)养生保健食谱
		int k=0;
		if(line.substr(0,14)=="养生保健食谱:"){
			string targetPrefix = "食疗验方:";
		 	while(getline(fin, line)&&line.compare(0, 10, targetPrefix) != 0){
		 	L.elem[i].recipe[k] = line;
		 	k++;
		 	}
		}
		L.elem[i].recipe_size = k;	
		 //(9)食疗验方 
		 k=0;
		 while(getline(fin, line)&&line!="#"){
		 	L.elem[i].therapy[k] = line;
		 	k++;
		 }
		 L.elem[i].therapy_size = k;
		 i++;
	}
	L.length = i;
//	cout<<"L.length="<<L.length<<endl;
//	cout<<"读文件成功"<<endl;
}

void SaveFile(SqList &L, string filename){
// 保存食材信息到文件
	ofstream fout(filename);
	for(int i=0; i<L.length; i++){
		fout<<"中文名称:"<<L.elem[i].name<<endl;
		fout<<"英文名称:"<<L.elem[i].sname<<endl;
		fout<<"养生功效:"<<L.elem[i].health<<endl;
		fout<<"营养与功效:"<<L.elem[i].nutrition<<endl;
		fout<<"专家提醒:"<<L.elem[i].expert<<endl;
		fout<<"相关链接:"<<L.elem[i].link<<endl;
		if(L.elem[i].recipe_size)fout<<"养生保健食谱:"<<endl;
		for(int j=0; j<L.elem[i].recipe_size; j++){
			fout<<L.elem[i].recipe[j]<<endl;
		}
		if(L.elem[i].therapy_size)fout<<"食疗验方:"<<endl;
		for(int j=0; j<L.elem[i].therapy_size; j++){
			fout<<L.elem[i].therapy[j]<<endl;
		}
		if(i!=L.length-1)
			fout<<"#"<<endl;
	} 
//	cout<<"保存文件成功"<<endl;
}

/*
1.读食疗验方和养生保健食谱的时候在cin>>k后面再读一个换行符才能正常输入 ?? 
 
*/
bool InsertFood(SqList &L){
// 插入食材信息,输入食材中文名称、英文名称、养生功效、营养与功效、养生保健食谱和食疗验方信息
// 如果插入成功,返回true,否则,返回false
	//(1)读中文名 
	string line;
	getline(cin, line);
	for(int i=0;i<L.length; i++){
		if(L.elem[i].name==line.c_str()){
			return false;
		}
	}
	if(L.length!=MAXSIZE){
		int i = L.length;
//		cout<<"line="<<line<<" ";
		strcpy(L.elem[i].name, line.c_str());
//		cout<<"name="<<L.elem[i].name<<endl;
		//(2)英文名 
		getline(cin, line);
		strcpy(L.elem[i].sname, line.c_str());
		//(3)养生功效
		getline(cin, line);
		strcpy(L.elem[i].health, line.c_str())	;
		//(4)营养与功效
		getline(cin, line);
		strcpy(L.elem[i].nutrition, line.c_str());
		//(5)专家提醒 
		getline(cin, line);
		strcpy(L.elem[i].expert, line.c_str())	;
		//(6)相关链接
		getline(cin, line);
		strcpy(L.elem[i].link, line.c_str());
		//(7)养生保健食谱
		int k;
		cin>>k;
		L.elem[i].recipe_size = k;
//		cout<<"receipe.size="<<L.elem[i].recipe_size<<endl;
		//-----------------------------------------换行??? 
		getline(cin, line);
		for(int j=0; j<k; j++){
			getline(cin, line);
			L.elem[i].recipe[j] = line;
//			cout<<"j="<<j<<" L.elem[i].recipe[j]="<<L.elem[i].recipe[j]<<endl;
		} 	
		 //(8)食疗验方 
		cin >> k;
		L.elem[i].therapy_size = k;
//		cout<<"therapy.size="<<L.elem[i].therapy_size<<endl;
		getline(cin, line);
		 for(int j=0; j<k; j++){
			getline(cin, line);
			L.elem[i].therapy[j] = line;
//			cout<<"j="<<j<<" L.elem[i].therapy[]="<<L.elem[i].therapy[j]<<endl;
		}
		 L.length++;
//		 cout<<"插入成功"<<endl;
		 return true;
	}else{
		return false;
	}
}

void Print(SqList &L){
// 输出食材信息
		int i = L.length-1;
//		cout<<"yes"<<endl;
		cout<<"中文名称:"<<L.elem[i].name<<endl;
		cout<<"英文名称:"<<L.elem[i].sname<<endl;
		cout<<"养生功效:"<<L.elem[i].health<<endl;
		cout<<"营养与功效:"<<L.elem[i].nutrition<<endl;
		cout<<"专家提醒:"<<L.elem[i].expert<<endl;
		cout<<"相关链接:"<<L.elem[i].link<<endl;
		if(L.elem[i].recipe_size)
			cout<<"养生保健食谱:"<<endl;
//		cout<<"receipe.size"<<L.elem[i].recipe_size<<endl;
		for(int j=0; j<L.elem[i].recipe_size; j++){
			cout<<L.elem[i].recipe[j]<<endl;
		}
		if(L.elem[i].therapy_size)
			cout<<"食疗验方:"<<endl;
		for(int j=0; j<L.elem[i].therapy_size; j++){
			cout<<L.elem[i].therapy[j]<<endl;
		}
}


int main(){
	SqList L;
	InitList(L);
	string originFilename = "food.txt";
	string newFilename = "new_food.txt";
	ReadFile(L, originFilename);
	if (InsertFood(L)){
		SaveFile(L, newFilename);
		ReadFile(L, newFilename);
		Print(L);
	}
	else{
		cout << "增加失败" << endl;
	}
	FreeList(L);
	return 0;
}

第3关:食材信息删除

Food *DeleteFood(SqList &L, char *name){
// 根据中文名称删除指定食材信息
// 如果删除成功,返回该食材的信息,否则,返回NULL
    	//(1)检查是否存在 
	int i;
	Food *food; 
	for (i = 0; i < L.length; i++){
		if (strcmp(name, L.elem[i].name) == 0){
//			cout<<"Yes"<<endl;
			food = new Food; 
			strcpy(food->name, L.elem[i].name);
			strcpy(food->sname, L.elem[i].sname);
			strcpy(food->health, L.elem[i].health);
			strcpy(food->nutrition, L.elem[i].nutrition);
			strcpy(food->expert, L.elem[i].expert);
			strcpy(food->link, L.elem[i].link);
			food->recipe_size = L.elem[i].recipe_size;
			food->therapy_size = L.elem[i].therapy_size;
			for(int m=0; m<food->recipe_size; m++){
				food->recipe[m] = L.elem[i].recipe[m];
			}
			for(int m=0; m<food->therapy_size; m++){
				food->therapy[m] = L.elem[i].therapy[m];
			}
			break;
		}
	}
	if(i==L.length){
		return NULL;
	}
	//(2)删除
	int pos = i;//待删除位置的索引 
	for( i=pos+1; i<L.length; i++){
		L.elem[i-1] = L.elem[i];
	}
	L.length--;
//	cout<<"删除成功--------------------------------------------"<<endl; 
	return food; 
}

第4关:食材信息修改

bool ModifyFood(SqList &L, int type, char *name, string lines[], int n){
// 食材信息修改,如果type是0,则修改养生保健食谱,否则修改食疗验方信息
// 如果修改成功,返回true,否则,返回false
		//(1)检查该食材是否存在 
	int i;
//	cout<<"待修改的name="<<name<<endl; 
//	cout<<"L.length="<<L.length<<endl;
	for ( i = 0; i < L.length; i++){
//		cout<<"i="<<i<<" L.elem[i]name="<<L.elem[i].name<<endl;
		if (strcmp(name, L.elem[i].name) == 0){
//			cout<<"yes"<<endl;
			break;
		}
	}
	if(i==L.length)
		return false;
//	cout<<"look"<<endl;
	//(2)修改,此时i位置上的即为要修改的食材 
	if(type==0){
		L.elem[i].recipe_size = n;
		for(int k=0; k<n; k++){
			L.elem[i].recipe[k] = lines[k];
		}
	}else{
		L.elem[i].therapy_size = n;
		for(int k=0; k<n; k++){
			L.elem[i].therapy[k] = lines[k];
		}
	}	
	return true;
}

第5关:基于顺序表的顺序查找

主要函数

int SeqSearch(SqList &L, char *sname){
// 在顺序表L中顺序查找食材英文名称等于sname的数据元素
// 若找到,则返回该元素在表中的下标,否则返回-1
	int i;
	int pos = -1;
	for ( i = 0; i < L.length; i++){
		if (strcmp(sname, L.elem[i].sname) == 0){
			pos = i;
			break;
		}
	}
	return pos;
}

double GetASL(SqList &L){
// 返回基于顺序表的顺序查找的ASL
	double a = (L.length+1)/2;
	return a;
}

时空复杂度和ASL分析

  1. 时间复杂度:O(n)
  2. 空间复杂度:O(1)
  3. ASL:(n+1)/2

第6关:基于顺序表的折半查找

折半查找

  1. 类似二叉搜索树BST,有序的,二叉树搜索树的形态仅与有序表的元素个数有关
  2. 某一元素的查找长度就是它的比较次数
  3. 对英文名排序,我们选择strcmp(sname1, sname2),这里使用冒泡排序

求折半查找的平均查找长度

//比较次数 
int cmp ;
int BinarySearch(SqList &L, char *sname){
// 在顺序表L中折半查找食材英文名称等于sname的数据元素
// 若找到,则返回该元素在表中的下标,否则返回-1
	//(1)置查找区间初值 
	int low = 0, high = L.length-1;
	int mid;
	while(low<=high){
		mid = (low+high)/2;
		cmp++; 
		if(strcmp(sname, L.elem[mid].sname)==0) 
			return mid;
		else if(strcmp(sname, L.elem[mid].sname)<0) 
			high = mid-1; //查找前一子表 
		else 
			low = mid+1; //查找后一子表 
	}
}

double GetASL(SqList &L){
// 返回基于顺序表的折半查找的ASL 
	int n = L.length;
	double asl = 0;
	for(int i=0; i<n; i++){
		cmp = 0;
		BinarySearch(L, L.elem[i].sname);
		asl+=cmp;
	}
	asl/=n;
	return asl;
}

时空复杂度和ASL

  1. 时间复杂度:O(n),空间复杂度:O(1)
  2. 平均查找长度ASL:有序表对应的BST二叉排序树的所有结点的深度,与结点总数的商,就是有序表折半查找的ASL

第7关:基于链表的顺序查找

主要函数

LNode *SearchList(LinkList &L, char *sname){
// 在带头结点的单链表L中查找食材英文名称为sname的元素
	LNode *p = L->next;
	while(p){
		if(strcmp(sname, p->data.sname)==0){
			return p;
		}else{
            p=p->next;
        }
	}
	return NULL;
}

double GetASL(LinkList &L){
// 返回基于链表的顺序查找的ASL
	LNode *p = L->next;
	int i=0;
	double asl =0;
	while(p){
		i++;
		p = p->next;
	}
	asl = (i+1)/2;
	return asl;
}

第8关:基于二叉排序树的查找

参考资料

关于二叉排序树可以参考这篇资料图解二叉排序树

主要函数

二叉排序树的插入

二叉排序中的插入就是遍历

void InsertBST(BSTree &T, Food e){ 
// 当二叉排序树T中不存在关键字等于e.sname的数据元素时,则插入该元素
	if(T==NULL){
		T = new BSTNode;
		T->data = e;
		return;
	}
	if(strcmp(e.sname, T->data.sname)==0){
		return;
	}else if(strcmp(e.sname, T->data.sname)>0){
		InsertBST(T->rchild, e);
	}else{
		InsertBST(T->lchild, e);
	}
}

二叉排序树的查询

二叉排序树的查询也是遍历

BSTNode *SearchBST(BSTree &T, char *sname){
// 查找对应食材
	if(T==NULL)
		return NULL;
	if(strcmp(T->data.sname, sname)==0)
		return T;
	if(strcmp(sname, T->data.sname)>0)
		return SearchBST(T->rchild, sname);
	else
		return SearchBST(T->lchild, sname);
}

二叉排序树的ASL

假设每个元素的查找概率相等,每个元素的查找长度就是该元素在二叉排序树中的深度,ASL就等于所有节点的深度之和除以节点总数

int GetSumCmp(BSTree T, int sumCmp){
// 统计查找成功时的总比较次数
	//就是统计每个节点的深度的和
	if(T==NULL)return 0;
	return GetSumCmp(T->lchild, sumCmp+1) + GetSumCmp(T->rchild, sumCmp+1) + sumCmp;
}

double GetASL(BSTree &T, int count){
// 返回基于二叉排序树查找的ASL
	//每个结点的深度的总和除以总结点数
	if(T==NULL)return 0;
	 double asl = 0;
	 int sumCmp = 1;
	 sumCmp = GetSumCmp(T, sumCmp);
	asl = sumCmp;
	asl/= count;
	return asl;
}

时空复杂度和ASL

  1. 时间复杂度:插入、查找、删除本质上都是二叉排序树的遍历,O(logn)
  2. 空间复杂度:O(n)
  3. ASL:所有结点的深度之和除以结点总数

第9关:基于字典树的查找

参考资料

GitHub字典树 英雄哪里出来之字典树

主要函数

初始化字典树

TNode *InitTNode(){
// 初始化Trie树结点
	TNode* T = new TNode;
	for(int i=0;i<53; i++){
		T->child[i] = NULL;
	}
	T->foodPtr = NULL;
	return T;
}

字典树构建

TNode *BuildTree(SqList &L){
// 构建基于链式存储的Trie树
// 构建成功后返回指向根节点的指针
	TNode *root = InitTNode();
	TNode *p; 
	for(int i=0; i<L.length; i++){
		//(1)存储当前第i个食材
		p = root; 
//		cout<<"yes"<<endl;
//		cout<<"当前来到第"<<i<<"个食材"<<endl;
		for(int j=0; L.elem[i].sname[j]!='\0'; j++){
			//(2)获取当前字符的索引 
			int idx;
			char c = L.elem[i].sname[j];
//			cout<<"当前读到的字符为:"<<c<<endl;
			if(c>='a'&&c<='z'){
				idx = c-'a';
			}else if(c>='A' && c<='Z'){
				idx = c-'A'+26;
			}else{
				//空格 
				idx = 52;
			}
			if(p->child[idx]!=NULL){
				p = p->child[idx];
			}else{
//				cout<<"新建结点"<<endl; 
				p->child[idx] = InitTNode();
				p = p->child[idx];
			}
		}
		//(3)来到字符串结尾,叶子节点存放当前第i个食材 
		Food *food = new Food;
		strcpy(food->name, L.elem[i].name);
		strcpy(food->sname, L.elem[i].sname);
		strcpy(food->health, L.elem[i].health);
		strcpy(food->nutrition, L.elem[i].nutrition);
		strcpy(food->expert, L.elem[i].expert);
		strcpy(food->link, L.elem[i].link);
		food->recipe_size = L.elem[i].recipe_size;
		food->therapy_size = L.elem[i].therapy_size;
		for(int m=0; m<food->recipe_size; m++){
			food->recipe[m] = L.elem[i].recipe[m];
		}
		for(int m=0; m<food->therapy_size; m++){
			food->therapy[m] = L.elem[i].therapy[m];
		}
		p->foodPtr = food; 
	}
//	cout<<"建trie树成功"<<endl;
	return root; 
}

字典树查询

Food *TrieSearch(TNode *root, char *sname){
// 基于字典树的查找
// 如果查找成功则返回指向该食材的指针,如果查找失败则返回NULL
	TNode *p = root;
	for(int i=0; sname[i]!='\0'; i++){
		int idx;
		char c = sname[i];
		if(c>='a'&&c<='z'){
			idx = c-'a';
		}else if(c>='A' && c<='Z'){
			idx = c-'A'+26;
		}else{
			//空格 
			idx = 52;
		}
		if(p->child[idx]!=NULL){
			p = p->child[idx];
		}else{
			return NULL;
		}
	}
	return p->foodPtr;
}

字典树平均查找长度

double GetASL(SqList &L){
// 计算查找成功时的平均查找长度ASL
	double asl=0;
	for(int i=0; i<L.length; i++){
		asl+=strlen(L.elem[i].sname);
	}
	asl/=L.length;
	return asl;
}

注意点

  • 字典树结点初始化的时候要把孩子节点的指针置空

时空复杂度和asl分析

  1. 时间复杂度:插入、查找,假设字符串长度为L,时间复杂度就是O(L),总体就是O(n*L),空间复杂度同
  2. asl:对应的字典树的所有叶子节点的深度,与叶子节点总数的商

第10关:基于开放地址法的散列查找

参考资料

主要函数

InitHT

void InitHT(HashTable &HT){
// 散列表初始化
    //(1)分配地址空间
    HT.key = new Food[m];
    HT.length = 0;
    //(2)初始化地址存放的内容为空 
    for(int i=0; i<m; i++){
        HT.key[i].sname[0] = '\0';
    } 
}

Hash计算哈希值

int Hash(char *sname){
// 实现散列函数:字符串sname中各字符的下标(从0开始)的平方乘以字符对应的ASCII码值,相加后与199取余
	int sum = 0;
	for (int i = 0; i < strlen(sname); i++)
	{
		sum += ((i) * (i) * int(sname[i]));
	}
	return sum % 199;
}

HTInsert往散列表中插入新元素

void HTInsert(HashTable &HT, Food f, int &sumCmp){
// 往散列表中插入新的食材f
// 在插入的过程中统计总的比较次数sumCmp
	//(1)计算哈希值,寻找可以存放的地址 
	int cur = Hash(f.sname);
	if(HT.key[cur].sname[0]=='\0'){
		//(2)如果当前哈希值对应的地址为空,存放 
		HT.key[cur] = f;
		sumCmp++;
	}else{
		//(3)不为空,线性探测寻找下一个存储位置 
		for(int i=1; i<m; i++){
			sumCmp++;
			cur = (cur+i)%HT.length;
			if(HT.key[cur].sname[0]=='\0'){
					break;
			}
		}
		HT.key[cur] = f;
	}
}

SearchHash在哈希表中查找

int SearchHash(HashTable HT, char *key){
// 在散列表HT中查找食材英文名称等于key的元素
// 若找到,则返回散列表的单元标号,否则返回-1
	//(1)计算查找元素的哈希值 
	int cur = Hash(key);
	//(2)如果哈希值对应的位置为空,查找失败 
	if(HT.key[cur].sname=='\0')return -1;
	//(3)检查哈希值对应的位置是否为待查找元素 
	if(strcmp(HT.key[cur].sname, key)==0){
		return cur;
	}else{
		//(4)按照线性探测法向后寻找待查找元素 
		for(int i=1; i<m; i++){
			cur = (cur+i)%HT.length;
			if(HT.key[cur].sname=='\0') return -1;
			if(strcmp(HT.key[cur].sname, key)==0) return cur; 
		}
	}
	return -1;
}

注意点

  1. 开放地址法中按照线性探测法计算下一个散列地址 cur = (cur+i)%HT.length
  2. 什么是散列表的长度

时空复杂度和asl分析

开放地址法散列表的时空复杂度:

  1. 插入操作的时间复杂度: 在开放地址法中,插入元素时,会尝试在散列表中的不同位置寻找一个空槽位。平均情况下,插入的时间复杂度是常数时间(O(1)),但在极端情况下,可能需要线性时间(O(n)),其中 n 是散列表的大小。

  2. 查找操作的时间复杂度: 在平均情况下,查找操作的时间复杂度也是常数时间(O(1))。在最坏情况下,可能需要线性时间(O(n)),这取决于散列表的大小和冲突的处理方式。

  3. 删除操作的时间复杂度: 与查找操作相似,平均情况下删除的时间复杂度是常数时间(O(1)),在最坏情况下可能是线性时间(O(n))。

  4. 空间复杂度: 空间复杂度主要取决于散列表的大小和存储的元素数量。在平均情况下,空间复杂度是 O(n),其中 n 是散列表的大小。

开放地址法散列表的平均查找长度 (ASL):

  1. 平均查找长度的计算: 平均查找长度 (ASL) 表示查找一个元素所需的平均探测次数。在开放地址法中,探测的次数取决于冲突的处理方式。常见的开放地址法探测方式包括线性探测、二次探测和双重散列等。

  2. 线性探测的平均查找长度: 在线性探测中,如果一个槽位被占用,就向后查找直到找到一个空槽位。平均查找长度取决于槽位的占用情况和散列表的大小。

  3. 二次探测和双重散列的平均查找长度: 这两种探测方式也有相应的平均查找长度公式,与槽位的占用情况、散列表的大小和其他参数有关。

总体来说,开放地址法的平均查找长度与散列表的装载因子、冲突解决策略等因素密切相关。在合理选择这些因素的情况下,平均查找长度可以维持在一个较小的常数水平,使得查找效率较高。

第11关:基于链地址法的散列查找

参考资料

阿里云 - 数据结构和算法】散列表的查找算法(开放地址法,链地址法)

注意点

  1. 指针数组放的是头节点,元素从首元结点开始放
  2. 头歌上使用的是尾插法

主要函数

链表数组初始化

typedef struct LNode{
	Food data;			        // 食材信息
	struct LNode *next;         // 指向下一级结点
} LNode, *LinkList;

void InitList(LinkList *H){
// 链表初始化
	for(int i=0; i<m; i++){
		H[i] = new LNode;
		H[i]->next = NULL; 
	}
}

计算哈希值

int Hash(char *sname){
// 实现散列函数:字符串sname中各字符的下标(从0开始)的平方乘以字符对应的ASCII码值,相加后与199取余
	int sum = 0;
	for (int i = 0; i < strlen(sname); i++){
		sum += ((i) * (i) * int(sname[i]));
	}
	return sum % 199;
}

往散列表中插入元素

void ListInsert(LinkList *H, Food f, int &sumCmp){
// 往散列表中插入新的食材f
// 在插入的过程中统计总的比较次数sumCmp
    int cur = Hash(f.sname);
	// if(H[cur]->next==NULL){
	// 	sumCmp++;//插入一次,比较一次 
	// 	H[cur]->next = new LNode;
	// 	H[cur]->next->data = f;
	// }else{
    //可以简化为第二种情况,直接判断p->next是否为空 
        //(1)设置标记位,判断待插入元素是否已经存在   
		bool flag = true;      
        //(2)获取插入位置的前驱节点=》欲加入的单链表的尾结点
		LNode *p = H[cur];
		while(p->next){
			sumCmp++; //遍历链表,比较一次 
			if(strcmp(p->data.sname, f.sname)==0) flag=false;
			p = p->next;
		}
		if(flag){
            //插入一次,比较一次
			sumCmp++;
			//尾插法 
			LNode *r = p; 
			p = new LNode;
			p->data = f;
			p->next =  r->next;
			r->next = p;
		}
	// }
		
}

查找散列表

int SearchHL(LinkList *H, char *key){
// 在散列表HT中查找食材英文名称等于key的元素
// 若找到,则返回散列表的单元标号,否则返回-1
	int cur = Hash(key);
	int ret = -1;
	LNode *p = H[cur]->next;
	while(p){
		if(strcmp(key, p->data.sname)==0){
			ret = cur;
		}
		p = p->next;
	}
	// cout<<"查找成功"<<endl;
	return ret;
}

时空复杂度和asl分析

链地址法散列表的时空复杂度:

  1. 插入操作的时间复杂度: 在链地址法中,插入元素时,将元素插入到相应的链表中。插入的时间复杂度是常数时间(O(1)),因为只需在链表头部插入。

  2. 查找操作的时间复杂度: 在链地址法中,查找操作需要遍历相应的链表,平均情况下查找的时间复杂度是链表的平均长度。假设散列表中有 �N 个槽位,每个槽位的链表平均长度为 �L,则查找的时间复杂度是 �(�)O(L)。在理想情况下,当 �L 较小时,查找接近常数时间。在最坏情况下,如果所有元素都散列到同一个槽位形成一条链表,查找的时间复杂度是 �(�)O(N)。

  3. 删除操作的时间复杂度: 删除操作也需要遍历相应的链表,平均情况下删除的时间复杂度是链表的平均长度,即 �(�)O(L)。

  4. 空间复杂度: 空间复杂度主要取决于散列表的大小 �N 和存储的元素数量。在平均情况下,空间复杂度是 �(�+�)O(N+M),其中 �N 是槽位的数量,�M 是元素的数量。

链地址法散列表的平均查找长度 (ASL):

  1. 平均查找长度的计算: 平均查找长度 (ASL) 表示查找一个元素所需的平均步数。在链地址法中,它与链表的平均长度有关。

  2. 理想情况下: 如果散列函数能够均匀地将元素散列到每个槽位,且每个槽位上的链表长度趋于均匀分布,那么平均查找长度可以近似为链表的平均长度,即 ���≈�/2ASL≈L/2。

  3. 最坏情况下: 在最坏情况下,如果所有元素都散列到同一个槽位形成一条链表,平均查找长度就等于链表的长度,即 ���=�ASL=L。

第12关:基于KMP算法的食材关键信息查询

没有需要特别注意的地方,背熟kmp板子即可

主要函数

void GetNext(const char *T, int next[]){
// 计算子串T的next数组
	int len = strlen(T);
	int i=0, j=-1;
	next[0] = -1;
	while(i<len-1){
		if(j==-1|| T[i]==T[j]){
			i++;
			j++;
			next[i] = j;
		}else{
			j = next[j];
		}
	}
}

bool KMP(const char *S, const char *T, int next[]){
// 利用模式串T的next数组求子串T在主串S中是否存在
// 如果查找成功则返回true,如果查找失败则返回false
	int i=0, j=-1;
	GetNext(T, next);
	int lenS = strlen(S);
	int lenT = strlen(T); 
	if(lenT>lenS)
		return false;
	while(i<lenS && j<lenT){
		if(j==-1|| S[i]==T[j]){
			i++;
			j++;
		}else{
			j = next[j];
		}
	}
	if(j>=lenT)
		return true;
	else
		return false;
}

bool SearchList(SqList &L, char *keyword, int next[]){
// 遍历顺序表食材的养生功效和营养与功效信息,调用KMP算法
// 如果在食材中查找成功则返回true,如果查找失败则返回false
	bool ret = false;
	for(int i=0; i<L.length; i++){
		if( KMP(L.elem[i].health, keyword, next) || KMP(L.elem[i].nutrition, keyword, next)){
			cout<<L.elem[i].name<<endl;
			ret = true;
		}		
	}
	return ret;
}

第13关:直接插入排序

主要函数

void InsertSort(SqList &L, int &kcn, int &rmn){
// 对顺序表L做直接插入排序,从后向前顺序比较
// 注:L.elem[0]用做哨兵单元
// 输出排序后的食材英文名称、KCN和RMN
	for(int i=2; i<=L.length; i++){
        
 //(1)小于号,需要将elem[i]插入前面的有序子表
		if( ++kcn && strcmp(L.elem[i].sname, L.elem[i-1].sname)<0){
            //(2)将待插入的记录暂存到哨兵结点
			L.elem[0] = L.elem[i];
            //(3)待插入记录的前一元素后移
			L.elem[i] = L.elem[i-1];
			rmn+=2;
			int j;
            //(4)从后向前寻找插入位置
			for( j=i-2; ++kcn&&strcmp(L.elem[0].sname, L.elem[j].sname)<0 ; j--){
				L.elem[j+1] = L.elem[j];
				rmn++;
			}
			//退出的时候j来到<=L.elem[0].sanme的位置,j+1为大于L.elem[0]的待插入位置 
			L.elem[j+1] = L.elem[0];
			rmn++;
		}
	}
	for(int i=1; i<=L.length; i++){
		cout<<L.elem[i].sname<<endl;
	}
	cout<<"总的关键字比较次数KCN为:"<<kcn<<endl;
	cout<<"总的记录移动次数RMN为:"<<rmn<<endl;
}

第14关:折半插入排序

将直接插入排序中的顺序查找插入位置改成了折半查找

主要函数

void BInsertSort(SqList &L, int &kcn, int &rmn){
// 对顺序表做折半插入排序
// 注:L.elem[0]用做哨兵单元
// 输出排序后的食材英文名称、KCN和RMN
	for(int i=2; i<=L.length; i++){
		//(1)将待插入记录存放到哨兵结点 
		L.elem[0] = L.elem[i];
		rmn++;
		//(2)查找区间初始值
		int low = 1, high = i-1;
		while(low<=high){
			int mid = (low+high)/2;
			if( ++kcn && strcmp(L.elem[0].sname, L.elem[mid].sname)<0){
				//(3)区间中值大于待插入记录,向前一子表查询 
				high = mid-1;
			}else{
				//(4)区间中值小于等于待插入记录,向后一子表查询
				//保证了折半排序的稳定性 
				low = mid+1;
			}
		}
		//(5)退出时查找区间变成了单点,记录后移腾位子 
		//low=high
		//=> low = high = mid,  [mid] > key, high-1, high<low=mid
		//=> low = high = mid,  [mid] <=key, low+1, high=mid<low
		//也就是循环退出时 [high+1]>key, [high]<key,需要放到high+1的位子上,从i-1到high+1的元素依次后移即可 
		for(int j=i-1; j>=high+1; j--){
			L.elem[j+1] = L.elem[j];
			rmn++;
		}
		L.elem[high+1] = L.elem[0];
		rmn++;
	}
	for(int i=1; i<=L.length; i++){
		cout<<L.elem[i].sname<<endl;
	}
	cout<<"总的关键字比较次数KCN为:"<<kcn<<endl;
	cout<<"总的记录移动次数RMN为:"<<rmn<<endl;
}

第15关:简单选择排序

主要函数

void SelectSort(SqList &L, int &kcn, int &rmn){
// 对顺序表做简单选择排序
// 注:L.elem[0]用做哨兵单元
// 输出排序后的食材英文名称、KCN和RMN
	for(int i=1; i<L.length; i++){
		//(1)从i到第n个,选择最小的,和第i个交换
		int k = i;
		for(int j = i+1; j<=L.length; j++){
			if(++kcn && strcmp(L.elem[j].sname, L.elem[k].sname)<0){
				k = j;
			}
		}
		//(2)检查最小的是否为第i个,即是否需要交换 
		if(k!=i){
			L.elem[0] = L.elem[k];
			L.elem[k] = L.elem[i];
			L.elem[i] = L.elem[0];
			rmn+=3;
		}
	}
	for(int i=1; i<=L.length; i++){
		cout<<L.elem[i].sname<<endl;
	}
	cout<<"总的关键字比较次数KCN为:"<<kcn<<endl;
	cout<<"总的记录移动次数RMN为:"<<rmn<<endl;
}

第16关:冒泡排序

注意点

  1. 相比于普通的冒泡排序,这里增加了一个标志位,用来表示该趟是否发生交换
  2. 如果发生了交换,则不需要进行剩余的趟数

主要函数

void BubbleSort(SqList &L, int &kcn, int &rmn){
// 对顺序表L做冒泡排序
// 注:elem[0]闲置
// 输出排序后的食材英文名称、KCN和RMN
	//(1)设置一个标记位,来表示当前这趟排序是否发生了交换
	//如果没有发生交换,则不需要进行剩下的趟数 
	int flag;
	for(int i=1; i<=L.length-1; i++){
		flag=0;
		for(int j=1; j<=L.length-i; j++){
			//(2)增加kcn和rmn 
			if( ++kcn && strcmp(L.elem[j].sname, L.elem[j+1].sname)>0){
//				cout<<"yes"<<endl;
				L.elem[0] = L.elem[j+1];
				L.elem[j+1] = L.elem[j];
				L.elem[j] = L.elem[0];
				rmn+=3;
				flag=1;
			}
		}
		if(flag==0)
			break;
	}
	for(int i=1; i<=L.length; i++){
		cout<<L.elem[i].sname<<endl;
	}
	cout<<"总的关键字比较次数KCN为:"<<kcn<<endl;
	cout<<"总的记录移动次数RMN为:"<<rmn<<endl;
}

第17关:快速排序

注意点

  1. Partition函数实现了分组,即把比枢轴大的放到右边,比枢轴小的放到左边
  2. 然后递归处理左子表和右子表

主要函数

int Partition(SqList &L, int low, int high, int &kcn, int &rmn){
// 对顺序表中的子表elem[low..high]进行一趟排序,返回枢轴位置
// 用子表的第一个记录做枢轴记录
// 注:L.elem[0]用来存枢轴记录
	//(1)哨兵储存一号位(枢轴)并后面作为枢轴的值参与比较 
 	L.elem[0] = L.elem[low];
 	rmn++;
	while(low<high){
		//(2)从右往左扫,寻找比枢轴大的记录,即循环结束时,枢轴所指记录小于枢轴 
		while(low<high && ++kcn && strcmp(L.elem[high].sname, L.elem[0].sname)>=0) --high;
		//(3)将比枢轴大的记录放到低端 
		L.elem[low] = L.elem[high];
		rmn++;
		//(4)从左往右扫,寻找比枢轴小的记录,即循环结束时low所指记录大于枢轴 
		while(low<high && ++kcn && strcmp(L.elem[low].sname, L.elem[0].sname)<=0) ++low;
		//(5)将比枢轴大的记录放到高端 
		L.elem[high] = L.elem[low];
		rmn++;
	}
	//(6)将枢轴放到low结束时所在的中间位置 
	L.elem[low] = L.elem[0];
	rmn++;
	//(7)返回中间位置,即循环结束时low所在的位置 
	return  low;   
}


void QSort(SqList &L, int low, int high, int &kcn, int &rmn){
// 调用前置初值:low=1; high=L.length;
// 对顺序表L中的子序列L.elem[low..high]做快速排序
	if(low<high){
		//(1)排序,返回中间位置 
		int pos = Partition(L, low ,high, kcn, rmn);
		//(2)递归处理左子表,从low到pos-1 
		QSort(L, low, pos-1, kcn, rmn);
		//(3)递归处理右子表,从pos+1到high 
		QSort(L, pos+1, high, kcn, rmn); 
	}
}


void QuickSort(SqList &L){
// 对顺序表做快速排序
// 输出排序后的食材英文名称、KCN和RMN
	int kcn =0, rmn=0;
	QSort(L, 1, L.length, kcn, rmn);
	for(int i=1; i<=L.length; i++){
		cout<<L.elem[i].sname<<endl;
	}
	cout<<"总的关键字比较次数KCN为:"<<kcn<<endl;
	cout<<"总的记录移动次数RMN为:"<<rmn<<endl;
}

时空复杂度和asl分析

  1. 时间复杂度: 在平均情况下,快速排序的时间复杂度为 �(�log⁡�)O(nlogn),其中 �n 是数组的长度。在最坏情况下,即每次划分都不平衡,时间复杂度为 �(�2)O(n2)。然而,通过随机选择基准元素或其他优化策略,可以减少最坏情况的概率。

  2. 空间复杂度: 快速排序是一种原地排序算法,它不需要额外的空间存储除了递归调用所需的栈空间外的其他数据。因此,空间复杂度是 �(log⁡�)O(logn)。

  3. 在快速排序中,最坏划分(worst-case partitioning)通常发生在每次选择的基准元素都是当前子数组中的最小或最大元素的情况。这导致每次划分都只能将数组中的一个元素移到正确的位置上,而其他元素都留在了原来的一侧,使得递归的深度达到 �n,其中 �n 是数组的长度。

    以下是一个最坏划分的例子:

    考虑数组 [5,4,3,2,1][5,4,3,2,1],并以第一个元素 55 作为基准元素。

    1. 第一次划分:55 是最大的,所有其他元素都在左侧,变为 [4,3,2,1,5][4,3,2,1,5]。
    2. 第二次划分:44 是最大的,所有其他元素都在左侧,变为 [3,2,1,4,5][3,2,1,4,5]。
    3. 在这个例子中,每次划分都只减少了一个元素的有序性,因此需要 �n 次划分才能完成整个排序过程。这导致了最坏情况下的时间复杂度为 �(�2)O(n2)。为了避免这种情况,通常采用随机选择基准元素或者采用其他优化策略,以提高快速排序在实际应用中的性能。

    4. 以此类推,每次划分只能将一个元素移到正确的位置上。

第18关:高位优先字符串排序

gpt对话

gpt解答

主要函数

// 获取字符串s在下标d处的字符,如果d超出字符串长度,则返回-1
char getChar(Food &s, int d) {
   if(d<strlen(s.sname)){
   		return s.sname[d];
   }else{
   		return -1;
   } 
}

void MSDSort(SqList &L, SqList &L2, int l, int r, int d){
//L为原始的顺序表,L2为辅助排序开辟的顺序表,l是排序的下界,r是排序的上界
//d为排序的字母的下标位置,从sname中下标为0的位置开始,往后递归处理
	//(1)当组内只有一个元素时递归结束
	if(l>=r)return; 

	//(2)ASC||码范围,一个字节,0-255,定义基数R和计数数组
	int R = 256;
	int arr[R+2] = {0}; 
	
    //(3)统计各字符频率,arr数组从2开始存数 
    for(int i=l; i<=r; i++){
    	arr[getChar(L.elem[i], d)+2]++;
	}

    //(3)将频率转化为索引,计算新的每一组开始的索引位置
	for(int i=0; i<R+1; i++){
		arr[i+1] += arr[i]; 
	} 

    //(4)数据分组,arr[i]存放的是第i-1组的起始存放位置,利用辅助数组暂存 
    for (int i = l; i <= r; i++) {
        L2.elem[arr[getChar(L.elem[i], d) + 1]++] = L.elem[i];
    }

    //(5)数据回写 
    for (int i = l; i <= r; i++) {
        L.elem[i] = L2.elem[i - l];
    }

    //(6)递归排序每个小组,此时arr[i]存放的是第i组的开始索引,即第i-1组的结束索引+1,arr[0]=0 
    for (int i = 0; i < R; i++) {
        MSDSort(L, L2, l + arr[i], l + arr[i + 1] - 1, d + 1);
    }
}

第19关:基于规则的实体识别

基本思路

  1. 由于所占字符不同,我们先将规则中的*扩展为两个或三个,然后正常匹配即可
  2. 将BF函数的主串起始检查位置设置为实参,遍历主串即可

主要函数

expandAsterisks

// 将规则中的每个*扩大为两个*
string expandAsterisks(const char* rule) {
    string expandedRule;
    for (int i = 0; rule[i] != '\0'; i++) {
        if (rule[i] == '*') {
            expandedRule += "**";
        } else {
            expandedRule += rule[i];
        }
    }
    return expandedRule;
}

IndexBF

int IndexBF(string S, string T, int &i)
{//简单模式匹配算法,S为主串(目标串),T为子串(模式串)。
//匹配成功返回主串中所含子串第一次出现的位置,否则返回-1。
//	cout<<"主串为:"<<S.ch<<endl;
//	cout<<"字串为:"<<T.ch<<endl;
	 int j=0;
	 int len1 = S.length();
	 int len2 = T.length();
	 while(i<len1 && j<len2){
	 	if(S[i]==T[j] || T[j]=='*'){
	 		i++;
	 		j++;
		 }else{
		 	i = i-j+1;
		 	j = 0;
		 }
	 }
//	 cout<<"i="<<i<<" j="<<j<<endl; 
	 if(j>=len2) return i-len2;
	 else
	 	return -1;
}

EntityRecognition

bool EntityRecognition(const char* S, const char* T){
// S为非结构化文本,T为规则
// 如果匹配成功返回true,否则返回false
// 输出所有匹配到的实体

	//(1)将keyword中的*变成若干个*号以适应中文字符占若干个字节 
    string text(S);
    string keyword = expandAsterisks(T);
    //(2)cnt记录*长度,j记录*在规则中首次出现的索引 
    int cnt=0,j=0;
    for(int i=0; keyword[i]!='\0'; i++){
    	if(keyword[i]=='*'){
    		cnt++;
		}
	}
	cnt=cnt/2;
	for(int i=0; keyword[i]!='\0'; i++){
    	if(keyword[i]=='*'){
    		j=i;
    		break;
		}
	}
//	cout<<"j="<<j<<endl;
//	cout<<"cnt="<<cnt<<endl;
//    cout<<"keyword.length="<<keyword.length()<<endl;
    
    //(3)pos记录主串中的开始匹配位置 
    int pos=0;
    bool ret = false;
    while(pos<text.length()){
    	int index = IndexBF(text, keyword, pos);
	    if (index != -1) {
	    	ret = true;
//	        cout << "匹配成功,实体位置:" << index <<",实体内容:" << text.substr(index+j,cnt*2) << endl;
	        //(4)cnt*x以适应一个中文字符占若干个字节 
			cout << text.substr(index+j,cnt*2) << endl;
	    }
	}
	return ret;
}

第20关:基于规则的关系抽取

基本思路

遍历规则,将规则中的AB替换为实体得到字符串replaceRule,将replaceRule在text中BF匹配即可

主要函数

RelationExtraction

bool RelationExtraction(char *text, char *entity1, char *entity2, Relation *r){
// 如果实体之间存在关系返回true,否则返回false
// 输出所有存在的三元组
	string t(text);
	string e1(entity1);
	string e2(entity2);
	bool ret = false;
	//(1)遍历所有规则关系
	for(int i=0 ;i<4; i++){
//		cout<<"i="<<i<<endl;
		for(int j=0; r[i].rule[j]!=""; j++){
			//(2)将A、B替换为实体
			string replaceRule = r[i].rule[j];
			int posA = replaceRule.find("A");
			if(posA != string::npos){
    			replaceRule.replace(posA, 1, entity1);
			} 
			int posB = replaceRule.find("B");
			if(posB!=string::npos){
				replaceRule.replace(posB, 1, entity2);
			}
//			cout<<"replaceRule="<<replaceRule<<endl;
			//(3)在文本中查找关系
			int pos = 0;
		    while(pos<t.length()){
//		    	cout<<"pos="<<pos<<endl;
		    	int index = IndexBF(t, replaceRule, pos);
			    if (index != -1) {
			    	ret = true;
			    	cout<<entity1<<"-"<<r[i].relation<<"-"<<entity2<<endl;
			    }
			}
		}
	} 
	return ret;
}

indexBF

int IndexBF(string S, string T, int &i)
{//简单模式匹配算法,S为主串(目标串),T为子串(模式串)。
//匹配成功返回主串中所含子串第一次出现的位置,否则返回-1。
//	cout<<"主串为:"<<S.ch<<endl;
//	cout<<"字串为:"<<T.ch<<endl;
	 int j=0;
	 int len1 = S.length();
	 int len2 = T.length();
	 while(i<len1 && j<len2){
	 	if(S[i]==T[j] || T[j]=='*'){
	 		i++;
	 		j++;
		 }else{
		 	i = i-j+1;
		 	j = 0;
		 }
	 }
//	 cout<<"i="<<i<<" j="<<j<<endl; 
	 if(j>=len2) return i-len2;
	 else
	 	return -1;
}

第21关:基于邻接表的知识图谱构建

第22关深搜

注意点

  1. 为下一次深搜做准备??

函数流程图

主要函数

DFS

void DFS(ALGraph& G, int i, string path){
	//(1)将当前结点标记为已访问 
	visited[i] = true;

    //(2)如果当前结点是食谱,输出路径并返回 
    if(G.vertices[i].entity == 3){
    	path+="->"+G.vertices[i].info;
        cout << path << endl;
        return;
    }
	//如果当前结点是食材且没有食谱 
	if(G.vertices[i].entity==0){
		bool flag=false;
		ArcNode* p = G.vertices[i].firstarc;
		while(p){
			if(G.vertices[p->adjvex].entity==3){
				flag=true;
				break;
			}
			p = p->nextarc;
		}
		if(flag==false){
			path+="->"+G.vertices[i].info;
			cout<<path<<endl;
			return;
		}	
	}
    //(3)当前结点是证、疾病、食材
	if(G.vertices[i].entity!=4)  
    	path+="->"+G.vertices[i].info;
    ArcNode* p = G.vertices[i].firstarc;
    while (p) {
        //(4)当前结点是证机概要且当前边是“有证机概要” 
        if(G.vertices[i].entity==4 && p->relationship==3){
        	if(!visited[p->adjvex]){
        		DFS(G, p->adjvex, path);
			}
		}
		//(5)疾病 推荐食材 
		if(G.vertices[i].entity==1 && p->relationship==2){
        	if(!visited[p->adjvex]){
        		DFS(G, p->adjvex, path);
			}
		}
		//(6)食材 推荐食谱 
		if(G.vertices[i].entity==0 && p->relationship==1){
        	if(!visited[p->adjvex]){
        		DFS(G, p->adjvex, path);
			}
		}
		visited[p->adjvex] = false;
		p = p->nextarc;
    }
}

void QuestionAnswering(ALGraph& G, string symptom){
    // 初始化visited 数组
    for (int i = 0; i < G.vexnum; i++) {
        visited[i] = false;
    }
    string path;
    // 找到 symptom 在图中的位置
    int startVex = LocateVex(G, symptom);
    path += G.vertices[startVex].info;
    DFS(G, startVex, path);
}

第23关:基于编辑距离的食材功效矩阵构建

主要函数

typedef struct {
    int vexs[100];            // 顶点表
    double arcs[100][100];    // 邻接矩阵
    int vexnum, arcnum;       // 图的当前顶点数和边数
} AMGraph;

void InitAMGraph(AMGraph& G) {
    // 初始化邻接矩阵
   G.vexnum = 0 ;
   for(int i=0; i<100; i++){
   	for(int j=0; j<100; j++){
   			G.arcs[i][j] = INF;
	   }
   }
}

void CreateAMG(AMGraph& GM, ALGraph& G) {
    // 调用编辑距离算法计算相似度,构建食材之间的邻接矩阵
    //(1)初始化邻接矩阵的顶点表 
	for(int i=0, j=0; i<100; j++){
    	if(G.vertices[j].entity==0){
    		GM.vexs[i++] = j;
		}
	}
//	for(int i=0; i<100; i++){
//		int k = GM.vexs[i];
//		cout<<G.vertices[k].info<<endl;
//	} 
	//(2)填充邻接矩阵 
	for(int i=0; i<100; i++){
		for(int j=0; j<100; j++){
			//(3)计算当前两个食材的功效字符串
			int v1 = GM.vexs[i];
			int v2 = GM.vexs[j];
			
			//(4)遍历v1的边,寻找功效边对应的功效
			string str1="";
			ArcNode *p1 = G.vertices[v1].firstarc;
			while(p1){
				if(p1->relationship==0){
					if(str1!="")
						str1=str1+"#"+G.vertices[p1->adjvex].info;
					else
						str1= G.vertices[p1->adjvex].info; 
				}
				p1 = p1->nextarc;
			}
			cout<<G.vertices[v1].info<<" str1="<<str1<<endl;
			//(5)遍历v2的边,寻找功效边对应的功效
			string str2 = "";
			ArcNode *p2 = G.vertices[v2].firstarc;
			while(p2){
				if(p2->relationship==0){
					if(str2!="")
						str2=str2+"#"+G.vertices[p2->adjvex].info;
					else
						str2= G.vertices[p2->adjvex].info;
				}
				p2 = p2->nextarc;
			}
			cout<<G.vertices[v2].info<<" str2="<<str2<<endl;
			
			//(6)计算文本相似度并填充到矩阵中 
			GM.arcs[i][j] = 1-TextSimilarity(str1, str2); 	
		}
	}   
}

第24关:基于 Dijkstra 算法的食材推荐

主要函数

void ShortestPathDIJ(AMGraph& G, bool S[], double D[], int v0) {
// 使用迪杰斯特拉算法求最短路径,S表示是否已被确定最短路径长度,D表示最短路径长度,v0表示顶点下标
	//(1)初始化v0到其他点的初始距离、标记、每个点的前驱 	
	for(int i=0; i<100; i++){
		S[i] = false;
		D[i] = G.arcs[v0][i];
	} 
	S[v0] = true;
	D[v0] = 0;
	
	//(2)初始化结束,开始主循环,每次求得v0到某个顶点的最小距离更新到D中
	//对剩下99个顶点进行计算,循环99次 
	for(int i=1; i<100; i++){
		int min = INF;
		int vmin;
		
		//(3)寻找未确定最短路径的终点中最短的 
		for(int w=0; w<100; w++){
			if(!S[w] && D[w]<min){
				min = D[w];
				vmin = w;
			}
		}
		//(4)将当前路径最短的点加入S集合中 
		S[vmin] = true;
		//(5)更新从v0出发到其余未确定顶点的最短路径
		for(int w=0; w<100; w++){
			//如果未确定最短路径的顶点w,距离大于 经由v0->vmin->w,则更新最短路径 
			if(!S[w] && D[w] > (D[vmin]+G.arcs[vmin][w])) {
				D[w] = D[vmin]+G.arcs[vmin][w];
			} 
		}	 
	} 
}

第25关:基于 Floyd 算法的食材推荐

学习资料

GitHub Pages - 图最短路径算法之弗洛伊德算法(Floyd)

主要函数

void ShortestPathFloyd(AMGraph& G, double D[][100]) {
// 使用弗洛伊德算法求最短路径,D表示最短路径长度
	//(1)初始化D数组
	for(int i=0; i<100; i++){
		for(int j=0; j<100; j++){
			D[i][j] = G.arcs[i][j];
		}
	} 
	
	
	//(2)动态规划更新 
    for(int k=0; k<100; k++){
    	for(int i=0; i<100; i++){
    		for(int j=0; j<100; j++){
    			D[i][j] = min(D[i][j], D[i][k]+D[k][j]);
			}
		}
	}
}

  • 25
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值