BUAA(2021春)——单词查找+查找算法Hash和Trie的拓展(可能和大作业有关)

48 篇文章 154 订阅
本文介绍了数据结构中查找算法的应用,包括顺序查找、折半查找、索引查找和Hash表查找,特别是针对Hash表的拉链法处理冲突及其优化。此外,还探讨了Trie树(字典树)在单词查找中的效率,包括链式Trie和数组Trie的实现。文章强调了不同查找方式的时间复杂度,并提供了实际测试数据和执行时间对比,以展示不同算法的性能差异。
摘要由CSDN通过智能技术生成

看前须知

要点介绍和简要声明.

第六次上机题汇总

单词查找+查找算法Hash和Trie的拓展(可能和大作业有关).

排座位(简)a——挺有技巧的.

整数排序——多个排序算法最好熟练掌握.

题目内容

问题描述

从标准输入中读入一个英文单词及查找方式,在一个给定的英文常用单词字典文件dictionary3000.txt中查找该单词,返回查找结果(查找到返回1,否则返回0)和查找过程中单词的比较次数。查找前,先将所有字典中单词读入至一个单词表(数组)中,然后按要求进行查找。字典中单词总数不超过3500,单词中的字符都是英文小写字母,并已按字典序排好序(可从课件下载区下载该字典文件 点此处下载:提取码:1234.)。字典中的单词和待查找单词的字符个数不超过20

查找方式说明:查找方式以1~4数字表示,每个数字含义如下:

  1. 在单词表中以顺序查找方式查找,因为单词表已排好序,遇到相同的或第一个比待查找的单词大的单词,就要终止查找

  2. 在单词表中以折半查找方式查找;

  3. 在单词表中通过索引表来获取单词查找范围,并在该查找范围中以折半方式查找。索引表构建方式为:以26个英文字母为头字母的单词在字典中的起始位置和单词个数来构建索引表,如:
    在这里插入图片描述
    该索引表表明以字母a开头的单词在单词表中的开始下标位置为0,单词个数为248。

4:按下面给定的hash函数为字典中单词构造一个hash表,hash冲突时按字典序依次存放单词。hash查找遇到冲突时,采用链地址法处理,在冲突链表中找到或未找到(遇到第一个比待查找的单词大的单词或链表结束)便结束查找。

/* compute hash value for string */

#define NHASH  3001

#define MULT  37

unsigned int hash(char *str)

{

   unsigned int h=0;

   char *p;



   for(p=str; *p!='\0'; p++)

          h = MULT*h + *p;

   return h % NHASH;

}

提示:hash表可以构建成指针数组,hash冲突的单词形成一有序链表。

输入形式

单词字典文件dictionary3000.txt存放在当前目录下,待查找单词和查找方式从标准输入读取。待查找单词只包含英文小写字母,与表示查找方式的整数之间以一个空格分隔。

输出形式

将查找结果和单词比较次数输出到标准输出上,两整数之间以一个空格分隔。

样例

单词字典文件dictionary3000.txt与课件下载中提供的相同,下面两列中,左侧为待查找单词与查找方式,右侧为对应的输出结果:

wins 1                              0 3314

wins 2                              0 12

wins 3                              0 7

wins 4                              0 2

yes 1                               1 3357

yes 2                               1 10

yes 3                               1 4

yes 4 1 1

样例说明

wins在单词字典中不存在,4种查找方式都输出结果0,顺序查找、折半查找、索引查找和hash查找的单词比较次数分别为:3314、12、7和2次(wins的hash位置与字典中physics和suggest相同)。

yes在单词字典中存在,4种查找方式都输出结果1,顺序查找、折半查找、索引查找和hash查找的单词比较次数分别为:3357、10、4和1。

题解

思考和详解

这道题中规中矩,除了第三种查找方式比较另类之外,其余的三种都是比较正常的,但是那三种查找方式的重要程度可以用下面的不等式来说明:Hash > Bsearch > 朴素顺序查找 , 因此掌握Hash查找是十分必要的。

但是这道题写的Hash查找是很明显是不符合正常查找的要求的,因为这个字典中每个单词仅仅只有一个,那么在统计词频的时候我们应该怎么办呢?如何在拉链法的情况下正确完成Hash表的构建呢?以及面对超大容量单词时我们应该怎么去构建获取关键值的函数使得哈希冲突不那么明显(就是怎么少构建拉链)?这些都是我们要去思考的问题。想要一探究竟可以移步拓展版块。

本题的难点在于如何去完成第三中查找,其实只需要构建一个二维数组即可,数组横坐标表示字母,纵坐标表示起止单词位置,然后套用第二中二分查找就行了,没什么难度。

参考代码

#include<stdio.h>
#include<string.h>
#include<math.h>
#include<stdlib.h>
#include<ctype.h>
#include<stdbool.h>
#define NHASH 3001
#define MULT 37
typedef struct dictionary{  	//字典结构体 
	char word[2000];			//单词存储 
}Word,*Wordp;
typedef struct HashNode{
	char word[2000];			//Hash结构体 
	struct HashNode *next;
}Hash,*Hashp;
Hashp table[NHASH];				//Hash表 
unsigned int hash(char *str);  //获得键值 
void HashInsert(Hashp *table,char *s);		//Hash表插入
int SearchHash(Hashp *table,char *s);	 	//Hash表查找
int order_search(char *val);				//顺序查找 
int b_search(char *val,int low,int high);	//二分查找 
int hash_b_search(char *val);				//索引 + 二分查找 

int i=0,j,flag,op,cnt,ValNum,hash_b_key[3000][2];	//hash_b_key是 索引 + 二分查找 的数组,横坐标表示字母,纵坐标表示起止单词位置 
Word s[5000];					//字典 
char val[2000],alpha=0;
int main()
{	
	hash_b_key[alpha][0]=0;		// 初始化字母 a 的第一个单词位置 
	FILE *fp = fopen("dictionary3000.txt", "r");
	while ((fscanf(fp, "%s", s[i].word) != EOF))
    {
    	if(s[i].word[0]-'a'>alpha) 	// 如果到下一个单词 
    	{
			hash_b_key[alpha][1]=i; //记录结束位置 
    		alpha=s[i].word[0]-'a';	//找到字母行数 
    		hash_b_key[alpha][0]=i; //记录起始位置 
		}
    	HashInsert(table,s[i].word); //插入哈希表 
        i++;
    }
	ValNum=i;	//单词总个数 
	hash_b_key[alpha][1]=ValNum; //记录字母 z 的最后一个单词位置 
    fclose(fp);
	while(~scanf("%s %d",val,&op))
	{
		cnt=0;
		switch(op)
		{
			case 1 :
				printf("%d ",order_search(val));	//顺序查找 
				printf("%d\n",cnt);
				break;
			case 2 :
				printf("%d ",b_search(val,0,ValNum-1));	//二分查找 
				printf("%d\n",cnt);	
				break;
			case 3 :
				printf("%d ",hash_b_search(val));	//索引 + 二分查找 
				printf("%d\n",cnt);
				break;
			case 4 :
				printf("%d ",SearchHash(table,val));	//Hash表查找
				printf("%d\n",cnt);
				break;
		}
	}
	return 0;
}
int order_search(char *val)
{
	flag=0;
	for(i=0;i<ValNum;i++)
	{
		if(strcmp(val,s[i].word)==0) //匹配 
		{
			cnt++;
			flag=1;	 		//查找成功
			break;
		}
		else if(strcmp(val,s[i].word)<0)
		{
			cnt++;
			break;
		}
		else if(strcmp(val,s[i].word)>0)
			cnt++;
	}
	if(flag==1)	return 1;
	else	return 0;
}
int b_search(char *val,int low,int high)
{
	int mid;
    while (low<=high){             // 查找结束的条件   
        mid=(low+high)/2;
        cnt++;
        if(strcmp(val,s[mid].word)==0)
        {
        	return 1; 		/*  查找成功  */
		}  
        if(strcmp(val,s[mid].word)>0)
        {
        	low=mid+1;		/*  准备查找后半部分 */
		}       
        else 
        {
			high=mid-1;	/* 准备查找前半部分 */
		}        
    }
    return 0;               /*   查找失败  */
}
int hash_b_search(char *val)
{
	return b_search(val,hash_b_key[val[0]-'a'][0],hash_b_key[val[0]-'a'][1]-1);//利用记录的索引位置进行二分查找 
}
void HashInsert(Hashp *table,char *s)	//Hash表插入
{
	int key=hash(s);    //获得键值 
	Hashp p=table[key];
	if(p==NULL)
	{
		p= (Hashp)malloc(sizeof(Hash)); //构造拉链 
		p->next=NULL;
		strcpy(p->word,s);
		table[key]=p;
	}
	else
	{
		while(p->next!=	NULL)	//移动到拉链最后一段 
			p=p->next;
		p->next= (Hashp)malloc(sizeof(Hash)); //构造拉链
		p=p->next;
		strcpy(p->word,s);	
		p->next=NULL;
	}
}
int SearchHash(Hashp *table,char *s)  //Hash表查找
{
	int key=hash(s);	//获得键值 
	Hashp p=table[key];
	if(p!=NULL)
	{
		while (p!=NULL)
		{
			cnt++;
			if(strcmp(p->word, s)<0)		//小于则向下查找 
			{
				p = p->next;
			}
			else if(strcmp(p->word,s)==0)  //查找成功 
				return 1;
			else 
				return 0;
    	}	
	}
	if(p==NULL)	return 0;
}
unsigned int hash(char *str)  //获得键值 
{
	unsigned int h=0;
    char *p;
	for(p=str; *p!='\0'; p++)
		h = MULT*h + *p;
   return h % NHASH;
}

补充测试的数据

【样例输入和输出】

you 1			1 3360	
you 2			1 12
you 3			1 3
you 4			1 3

查找算法Hash和Trie的拓展

本人大作业0.1s,下面有关大作业方面的内容都是自己亲自优化体验过的,不是胡编乱造的。

首先先申明**排序都是快排**

  1. 如果大作业用顺序查找,那么我只能跟你说你可 真优秀~(阴阳怪气哈哈哈),下图是执行时间,单位s。
    在这里插入图片描述
  2. 如果大作业用BST,那么我只能跟你说你有点优秀(认真的),下图是执行时间,单位s。
    在这里插入图片描述
  3. 如果大作业用链式Trie,那么我只能跟你说你优秀(认真的),下图是执行时间,单位s。
    在这里插入图片描述
  4. 如果大作业用Hash,那么我只能跟你说你很优秀(认真的),下图是执行时间,单位s。
    在这里插入图片描述
  5. 如果大作业用数组Trie,那么我只能跟你说你超级优秀(认真的),下图是执行时间,单位s。
    fputchar

Hash拓展

现在我们来解决一下之前提到的问题(那么在统计词频的时候我们应该怎么办呢?如何在拉链法的情况下正确完成Hash表的构建呢?以及面对超大容量单词时我们应该怎么去构建获取关键值的函数使得哈希冲突不那么明显(就是怎么少构建拉链)?)

  1. 对于第一个问题很简单解决,只需要在结构体中加一个参数——出现次数就行
typedef struct HashNode{       //哈希表结构体 
	char word[200];
	int cnt;
	struct HashNode *next;
}Hash,*Hashp;
  1. 对于第二个问题,确实有一点难度,尤其是真正构造拉链时需要小心的有很多,我曾经在这里吃过亏,所以强烈建议大家自己写一下,实在想不出来再看看我附上的代码,自己可以理解体会学习一下。

Hash表的插入

void HashInsert(Hashp *table,char *s)
{
	int key=hashKey(s);    //获得键值 
	Hashp p=table[key];
	if(p==NULL)
	{
		p= (Hashp)malloc(sizeof(Hash));
		p->next=NULL;
		p->cnt=1;
		strcpy(p->word,s);
		table[key]=p;
	}
	else
	{	
		while(strcmp(s,p->word)!=0 && p->next!=NULL)
		{
			p=p->next;
		}
		if(strcmp(s,p->word)==0)	p->cnt++;
		else if(p->next==NULL)
		{
			p->next= (Hashp)malloc(sizeof(Hash)); 
			p=p->next;
			p->next=NULL;
			p->cnt=1;
			strcpy(p->word,s);
		}
	}	
}

Hash表的查找

int SearchHash(Hashp *table,char *s)
{
	int key=hashKey(s);
	Hashp p=table[key];
	if(p!=NULL)
	{
		while (p!=NULL)
		{
			if(strcmp(p->word, s)!=0)
			{
				p = p->next;
			}
        	else return p->cnt;
    	}	
	}
	if(p==NULL)	return 0;
}
  1. 对于怎么获得简直,其实就是利用好质数,利用质数取模来实现均匀的哈希表分配
#define MOD 87965756
unsigned long long hashKey(char *st) {
    unsigned long long key = 0;
    while (*st) {
        key = key * 31 + (unsigned long long)(*st++);
    }
    return key % MOD;
}

Trie拓展

首先是链式Trie

Trie是什么,其实就是字典树(前缀树),那么链式字典树是什么,其实就是26叉树,只不过每一个路径是一个字母罢了。
如图
在这里插入图片描述
可以发现,这棵字典树用边来代表字母,而从根结点到树上某一结点的路径就代表了一个字符串。举个例子1 -> 4-> 8 -> 12, 表示的就是字符串 caa。

有时需要标记插入进 trie 的是哪些字符串,每次插入完成时在这个字符串所代表的节点处打上标记即可。

如果想继续了解可以自己阅览资料

下面提供参考实现插入和查找算法

#include <stdio.h>
#include <stdlib.h>
#define  MAX    26

typedef struct TrieNode
{
	int nCount;  // 该节点前缀 出现的次数
	struct TrieNode *next[MAX]; //该节点的后续节点
} TrieNode;

TrieNode Memory[1000000]; 
int allocp = 0;

//初始化一个节点。
TrieNode * createTrieNode()
{
	TrieNode * tmp = &Memory[allocp++];
	tmp->nCount = 1;
	for (int i = 0; i < MAX; i++)
		tmp->next[i] = NULL;
	return tmp;
}

void insertTrie(TrieNode * * pRoot, char * str)
{
	TrieNode * tmp = *pRoot;
	int i = 0, k;
	while (str[i])
	{
		k = str[i] - 'a'; 
		if (tmp->next[k])
		{
			tmp->next[k]->nCount++;
		}
		else
		{
			tmp->next[k] = createTrieNode();
		}

		tmp = tmp->next[k];
		i++; 
	}

}

int searchTrie(TrieNode * root, char * str)
{
	if (root == NULL)
		return 0;
	TrieNode * tmp = root;
	int i = 0, k;
	while (str[i])
	{
		k = str[i] - 'a';
		if (tmp->next[k])
		{
			tmp = tmp->next[k];
		}
		else
			return 0;
		i++;
	}
	return tmp->nCount; 
}

接下来就是数组形式的Trie,但是有一个很大的缺点就是空间开销超级大,但是呢,查找超级快

下面提供参考实现插入和查找算法

int trie[1000010][26],num[1000010]={0};

void InsertTrie(char *str){
	int p=0;
	for(int i = 0;str[i];i++){
		int n=str[i]-'a';
		if(trie[p][n]==0)
			trie[p][n]=++pos;
		p=trie[p][n];	
	}
	num[p]++;
}

int FindTrie(char *str){
	int p=0;
	for(int i=0;str[i];i++){
		int n=str[i]-'a';
		if(trie[p][n]==0)
			return 0;
		p=trie[p][n];
	}
	return num[p];
}

最后再次提醒:不要复制粘贴抄袭!!(上次查重差点查到我头上,我的天!!)

  • 18
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 16
    评论
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值