《算法笔记》(一)——日期差值问题、散列(哈希查找)

学习内容均来自由胡凡、曾磊主编的《算法笔记》,对其中的内容进行总结整理方便自己的复习和省察


日期处理问题

日期处理问题一般分为两种,一种是给定起始日期和天数,要求计算指定天数后的日期是什么;另一种是给定头尾两个日期,计算它们之间的天数。这类问题主要要考虑到的有:平闰年问题和大小月问题,因此有些细节问题会比较复杂。其他的类似于制作一张万年历表,都是基于上述两种问题得来的。

思路

可以用模拟的方法进行求解。就像我们日常生活中的计数模式一样,把天数一天天地加上去,如果遇到需要进位的地方就月份加一,日数置一或年份加一,月份置一。为了方便读取每个月的天数,直接给定二维数组用来存放每个月的天数(二维是为了表示闰年),规定第二维为0时是平年,1时是闰年。
为提升速度,可以将年份先加到与第二个日期的年份相差1为止(如果同年容易错误计算跨年情况,如18年1月与16年4月)。
为了数组的一维数的下标与实际的月份对应,共设立13个月,将第0个月置空。

//问题描述:
//有两个日期,求两个日期之间的天数,如果两个日期是连续的,
//则规定它们之间的天数为两天
//输入:20130101
//输入:20130105
//输出:5
#include
using namespace std;
int month[13][2]={
     {
     0,0},{
     31,31},{
     28,29},{
     31,31},{
     30,30},{
     31,31},{
     30,30},
				{
     31,31},{
     31,31},{
     30,30},{
     31,31},{
     30,30},{
     31,31}	};
bool isleap(int year){
     
	return((year%4==0&&year%100!=0) || (year%400==0));
}
int main(){
     
	
	int time1,y1,m1,d1;
	int time2,y2,m2,d2;
	while(cin>>time1>>time2){
     
		if(time1>time2){
     
			int temp = time1;
			time1 = time2;
			time2 = temp;
		}
		y1=time1/10000,m1=time1%10000/100,d1=time1%100;
		y2=time2/10000,m2=time2%10000/100,d2=time2%100;
		int ans=1;
		while(y1<y2||m1<m2||d1<d2){
     
			d1++;
			if(d1==month[m1][isleap(y1)]+1){
     
				m1++;
				d1=1;
			}
			if(m1==13){
     
				y1++;
				m1=1;
			}
			ans++;
		}
		cout<<ans<<endl; 
	}	
	return 0;
}

散列

散列(Hash)是一种常用的用空间换时间的做法。个人的理解是在两两列表之间产生某种对应的映射关系。比如数组就是其中的一种,它把存入的整数或字符映射成了数组的下标,供你进行快速地访问和判断。

比如以下为一道例题的求解过程。

给出N个正整数,再给出M个正整数。问这M个正整数是否分别在N个正整数中出现过。如N个正整数{2,3,4,5}和M个正整数{1,2},其中只有2在里面出现过。显然,它可以用循环遍历的方法求解,但是其时间复杂度会达到O(NM),因此散列将会是不错的办法。即另开一个数组记录N集合中的数字是否出现过,出现过的数字(2,3,4,5)作为新开数组的下标,并将其作标记,在M集合中的数字出现后,便可以通过M中的数字对应到新开数组下标所对应的标记,看看是否出现过,完成本题。
代码如下。

#include
using namespace std;
const int maxn = 10010;
bool hashTable[maxn] = {
     false};
int main(){
     
	
	int n,m,x;
	cin>>n>>m;
	for(int i=0;i<n;i++){
     
		cin>>x;
		hashTable[x] = true;
	} 
	
	for(int i=0;i<m;i++){
     
		cin>>x;
		if(hashTable[x]){
     
			cout<<"YES"<<endl;
		}else{
     
			cout<<"NO"<<endl;
		}
	}
	
	return 0;
} 

通过新建一个简单的hashTable就完成了这道题,巧妙地利用空间提升了速度。但是,在这道题中,输入的是不大的数字,因此正好可以利用数组的坐标进行标记与判断。一旦输入的数字过大,或者是输入的为一段文字,此时的方法就不够“万能”。因此,我们可以通过一种方法,将输入的东西转换成数组的下标或是其他我们想标记的元素,这种方法往往被定义成一种函数,即为散列函数H。
将元素通过一个函数转换为整数,使得该整数可以尽量唯一地代表这个元素
常用的方法有

  1. 直接取坐标法(如上文的例题)
  2. 线性变换法 H(key)=a*key+b
  3. 平方取中法
  4. 除留余数法 H(key)=key%mod <-质数
  5. 随机数法 H(key)=random(key)
  6. 折叠法 将关键字分割成位数相等的几部分,取这几部分的叠加和(舍去位的进位)作为哈希地址。包括移位叠加(每部分最后一位对齐再叠加)和边界叠加(从一端向另一端沿边界逐次折叠,然后对齐相加)
  7. 数字分析法

可以想见,常用的这几种方法都有很多不足之处如空间上的浪费。但最重要的是很难确保映射的唯一性。如最常用的除留余数法,产生的数最多是[0,mod)中的数,如果需要更多的对应关系则难以招架,这样的情况叫做“冲突”。因此,我们用其他的方法来解决这些冲突。

  1. 开放地址法
  2. 再哈希法
  3. 链地址法
  4. 公共溢出区

开放地址法 即为为产生冲突的地址求得一个地址序列(依照一定的次序探测判断)如:
线性探测再散列:原有的H(key)被占用了以后,通过不断地+1或者是-1或者是其他的常数+c/-c寻找没有被占用的H(key)
平方探测再散列:原有的H(key)被占用了以后,通过不断地+/-c^2寻找没有被占用的H(key)
随机探测再散列:同上,在寻找新地址的时候采用一个随机数
再哈希法 顾名思义,再使用一次哈希法,会增加计算的时间
链地址法 不计算新的hash值,而是把所有H(key)相同的Key值用链表连接成一条单链表,最后再按指示串联起来。
公共溢出区法 开辟一个溢出表,发生冲突的所有记录都填入溢出表,最后再进行计算

上述方法也是哈希查找的做法,将待查找的元素通过哈希函数映射成正整数,在查找的时候就可以十分方便地找到自己想要的结果。不过在一般情况下,可以使用STL中的map来直接使用hash的功能更为方便,不用自己写哈希表。

以书中的问题结尾:
给出N个字符串,再给出M个查询字符串,问每个查询字符串在N个字符串中出现的次数。

#include
const int maxn = 100;
char S[maxn][5],temp[5];
int hashTable[26*26*26+10];
int hashFunc(char s[],int len){
     
	int id=0;
	for(int i=0;i<len;i++){
     
		id=id*26+(s[i]-'A');
	}
	return id;
}
int main(){
     
	int n,m;
	cin>>n>>m;
	for(int i=0;i<n;i++){
     
		cin>>s[i];
		int id=hashFunc(s[i],3);   //将字符串转换为整数 
		cout<<"id="<<id<<endl;
		hashTblechar[id]++;  //该字符串的出现次数+1 
	}
	for(int i=0;i<m;i++){
     
		cin>>temp;
		int id=hashFunc(temp,3);  //将字符串temp转换为整数 
		cout<<hashTblechar[id]<<endl;  //输出该字符串出现的次数 
	}
	
	return 0;
}

你可能感兴趣的:(数据结构与算法,哈希查找,哈希,C++,数据结构与算法)