目标:学会应用哈希表以统计的思维对字符串进行处理!学会快速分析出算法的时间和空间复杂度!
目录:
1.哈希表基础知识复习
2.get第一次只出现一次的字符
3.从第一个字符串中删除所有在第二个字符串中出现过的字符
4.删除字符串中所有重复出现的字符(结果串不含重复字符)
5.判断两个英文单词是否为变位词
6.get字符流中第一个中出现一次的字符
链地址法缺点:1.时间复杂度较其他的高(因为可能要遍历链表)2.有的关键字得到的哈希地址频次交高,则该节点对于的链表就非常长?怎么改进?(再哈希法!)
首先你要知道哈希表是干嘛的啊?!
哈希表是一种查找表!哈希函数、地址冲突、平均查找长度、装填因子等概念!
一旦哈希表建立好了,一来一个关键字,我用哈希函数一算就可与算出其在表中的地址,就可与直接去该地址处看这个关键字对应的元素存在吗?如果存在就找到了,如果不存在则查找失败!
哈希函数表的定义:
根据设定的哈希函数H(key)和一种冲突处理方法将一组关键字映像到一个有限连续的地址集(区间)上,并以关键字在地址集中的“像”作为记录在表中的存储位置,这种表便是哈希表,这一映像过程称为哈希造表或散列,所得存储位置成为哈希地址或散列地址!
选择哈希函数要考虑的因素:
2.哈希表是一个压缩映像,所有会出现冲突。
假设哈希表的地址集为0~(n-1),冲突是指由关键字得到的哈希地址为j(0<=k<=n-1)的位置上已经存在有记录,则“处理冲突“就是为该关键字的记录找到另外一个”空“的哈希地址。在处理冲突的过程中可能得到一个地址序列Hi,i=1,2,...,k,(Hi属于[0,n-1]),依次类推,直到Hk不发生冲突为止,则Hk为记录在表中的地址。
处理冲突的方法:
1)开发定址法
Hi=(H(key)+di)MODm i=1,2,...,k(k<=m-1)
H()为哈希函数,k为哈希表长度,di为增量序列:
di=1,2,3,...,m-1,成为线性探测再散列法
di=12,-12,22,...,+-k2(k<=m/2)称为二次探测再散列法
di=伪随机数序列,则称为随机探测再散列法。
线性探测再散列法容易引起“二次聚集“现象。
2)再哈希法
Hi=RHi(key) i=1,2,...,k
RHi均是不同的哈希函数。
3)链地址法
将所有关键字为同义词的记录(具有相同函数值的关键字对该哈希函数来说称为同义词synonym)存储在同一线性链表中。假设某个哈希函数产生的哈希地址在区间[0,m-1]上,则设立一个指针型向量
Chain ChainHash[m];
其每个分量的初始状态都为空指针。凡哈希地址为i的记录都插入到头指针为ChainHash[i]的链表中,在链表中插入的位置要保证同义词在同一线性列表中按关键字有序(以便进行二分查找)
4)建立公共溢出区
HashTable[0..m-1]为基本表
OverTable[0...v]为溢出表
参考
[1] 严蔚敏, 吴伟民. 数据结构(C语言版)[J]. 计算机教育, 2012, No.168(12):62-62.
[2]STL的map、hash_map的使用https://blog.csdn.net/q_l_s/article/details/52416583
例如:每本书都有一个ISBN编号!但我们的小图书馆只有10000本书,就可以用哈希函数对其做一压缩映像
2.哈希表应该的一个例子:《剑指offer》第5章优化时间和空间效率
面试题50:求一字符串中只出现一次的字符
/*
《剑指offer》求一个字符串中第一个只出现一次的字符:
如输入"abaccdeff"则应该输出:'b'
*/
//法2:哈希表映射法:时间复杂度O(n),空间复杂度O(1)
//字符出现了多少次?:统计的思维(很优雅)
/*
我们定义哈希表的键是字符的ASCII值,值是该字符出现的次数。
只需扫描字符串两次:
第一次是统计每个字符出现的次数。
第二次是将第一个只出现一次的字符输出。
处理特殊情况:没有只出现一次的字符,每个字符都出现了两次或两次以上。
*/
char getFirstNotRepeatingChar(string str)
{
if (str.size() == 0)
return '\0';
//ASCII码总共有256个
const int hashTableSiez = 256;
unsigned int* hashTable = new unsigned int[hashTableSiez]();//数组元素全初始化为0
for (int i = 0; i < str.size(); i++)
{
hashTable[str[i]]++;
}
for (int i = 0; i < str.size(); i++)
{
if (hashTable[str[i]] == 1)
return str[i];
}
//处理特殊情况:没有只出现一次的字符,每个字符都出现了两次或两次以上。
return '\0';
}
另外附上暴力搜索法:苦逼的上下而求索啊!(思路简单,但难写更难看!)
/*
最直观的暴力扫描法:对于每一个字符,分别在其前、其后的子串中查找该字符,如都没有出现,则为只出现一次的字符,找到这样的第一个字串!
*/
char getFirstNotRepeatingCharBruteForce(string str)
{
if (str.size() == 0)
return '\0';
string::size_type pos_after;
string::size_type pos_before;
string substr = str;
string substr_after;
string substr_before;
for (int i = 0; i < str.size(); i++)
{
/*
size_type find(_Elem _Ch, size_type _Off = 0) const
{ // look for _Ch at or after _Off
return (find((const _Elem *)&_Ch, _Off, 1));
}
注意是at or after _Off!!
*/
if (i == 0)
{
substr = str.substr(i + 1);
pos_after = substr.find(str[i]);
if (string::npos == pos_after)
{//没找到就输出该字符
return str[i];
}
else
{
continue;
}
}
else if (i > 0 && i < str.size() - 1)
{
/*
_Myt substr(size_type _Off = 0, size_type _Count = npos) const
{ // return [_Off, _Off + _Count) as new string
return (_Myt(*this, _Off, _Count, get_allocator()));
}
*/
//在str[i]之前找
substr_before = str.substr(0, i);
pos_before = substr_before.find(str[i]);
//在str[i]之后找
substr_after = str.substr(i + 1, string::npos);
pos_after = substr_after.find(str[i]);
if (pos_before == string::npos&&pos_after == string::npos)
{
return str[i];
}
else
{
continue;
}
}
else//对于最后一个字符:
{
substr_before = str.substr(0, i);
pos_before = substr_before.find(str[i]);
if (pos_before == string::npos)
{
return str[i];
}
else
{
return '\0';
}
}
}
}
下面是几个测试例子:
#include "stdafx.h"
#include
#include
#include
#include
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
string str = "abaccdeff";
string str2 = "aabbccdde";
string str3 = "";
string str4 = "aabbcc";
char firstNotRepeatChat = getFirstNotRepeatingCharBruteForce(str4);
cout << "First Not Repeating Char in " << str4 << " is " << firstNotRepeatChat << endl;
system("pause");
return 0;
}
3.从第一个字符串中删除所有在第二个字符串中出现过的字符
/*《剑指offer》50题附加1:P246定义一个函数,
输入两个字符串,从第一个字符串中删除在第二个字符串中
出现过的所有字符。
建立第二个字符串的哈希表,扫描第一个字符串:
能在O(1)的时间内判断出该字符是否在第二个字符串中出现过!
*/
/*《剑指offer》50题附加1:P246定义一个函数,
输入两个字符串,从第一个字符串中删除在第二个字符串中
出现过的所有字符。
建立第二个字符串的哈希表,扫描第一个字符串:
能在O(1)的时间内判断出该字符是否在第二个字符串中出现过!
*/
string deleteAwhichInB(string A, string B)
{
if (A.size() == 0)
return A;
if (B.size() == 0)
return A;
//ASCII码总共有256个
const int hashTableSiez = 256;
unsigned int* hashTable = new unsigned int[hashTableSiez]();//数组元素全初始化为0
string tempString = "";
//用哈希表来统计B中出现过的字符
for (int i = 0; i < B.size(); i++)
{
hashTable[B[i]]++;
}
for (int i = 0; i < A.size(); i++)
{
if (hashTable[A[i]] == 0)
{
//用原位删除的话改变了循环边界,有bug!
//A=A.erase(i,1);//删除A中下标为i的字符,[i,i+1),你一删,A.size()就变了!
//还是另设一个空字符串,将没有重复出现过的字符append上去!
/*
STL对字符串的操作要么用size_type类型参数的函数,要么用迭代器类型的参数!
_Myt& erase(size_type _Off, size_type _Count)
{// erase elements [_Off, _Off + _Count)
}
iterator erase(const_iterator _Where)
{ // erase element at _Where
}
*/
tempString = tempString.append(1, A[i]);
}
else
{
continue;
}
}
delete[] hashTable;
return tempString;
}
4.删除字符串中所有重复出现的字符(结果串不含重复字符)
/*
《剑指offer》50题附加2:P246
定义一个函数:删除字符串中重复出现的字符。(即结果串无重复字符)
如输入"google",则输出"gole"
思路:扫描每一个字符,如果它在前面出现过,就将其删除!
*/
string deleteRepeatedChar(string str)
{
if (str.size() == 0)
return str;
//ASCII码总共有256个
const int hashTableSiez = 256;
unsigned int* hashTable = new unsigned int[hashTableSiez]();//数组元素全初始化为0
string strTemp = "";
//用哈希表来统计B中出现过的字符
for (int i = 0; i < str.size(); i++)
{
if (0 == hashTable[str[i]])
{//将不重复的字符追加在strTemp后面!然后设置哈希表对应的值为1
strTemp.append(1,str[i]);
/*
_Myt& append(size_type _Count, _Elem _Ch)
{ // append _Count * _Ch
}
*/
hashTable[str[i]] = 1;
}
else if (1 == hashTable[str[i]])
{//用erase的话原地删除A中下标为i的字符,[i,i+1),你这里一删除了字符,
/*str.size()大小就变!循环就有问题啊!哥!*/
continue;
}
}
delete[] hashTable;
return strTemp;
}
5.判断两个英文单词是否为变位词
/*
《剑指offer》50题附加3:P246
写一个函数判断两个单词是否为变位词(Anagram)?
变位词(Anagram):
silent与listen
evil 与 live
思路:
用数组实现一个哈希表:
扫描第一个字符串,统计串中字符出现的次数。
再扫描第二个字符串,将出现的字符对应的哈希表的位置-1;
最后扫描哈希表,如果哈希表的所有元素都为0,怎这两个单词为变位词。
*/
bool isAnagram(string str1, string str2)
{
if (str1.size() == 0||str2.size()==0)
return false;
const int hashTableSiez = 256;
unsigned int* hashTable = new unsigned int[hashTableSiez]();//数组元素全初始化为0
for (int i = 0; i < str1.size(); i++)
{
hashTable[str1[i]]++;
}
for (int i = 0; i < str2.size(); i++)
{
hashTable[str2[i]]--;
}
for (int i = 0; i < 256; i++)
{
if (hashTable[i] != 0)
{
return false;
}
else
{
continue;
}
}
return true;
}
6.get字符流中第一个中出现一次的字符
/*
offer50_2:求字符流中第一个只出现一次的字符。
当从字符流中只读出"go"两个字符时,第一个只出现一次的字符是g
当从字符流中读入前6个字符"google"时,第一个只出现一次的字符是l
定义一个哈希表的数据容器来保存字符在字符流中的位置:
当第一个字符第一次从字符流中读入时,把它在字符流中的位置保存到数据容器里。
当以后再次读入该字符时,更新数据容器里的值(设为特殊值:-2),表面它在前面出现过。
*/
class CharStaistics{
private:
/*
occurrence[i]=-1;//这个字符目前没有出现
occurrence[i]=-2;//这个字符出现了多次
occurrence[i]=index;//这个字符只出现了一次,且存了其出现时的流下标
*/
int occurrence[256];
int index;
std::string str;
public:
CharStaistics() :index(0)
{
str = "";
for (int i = 0; i < 256; i++)
{
occurrence[i] = -1;
}
}
void Append(char ch)
{
if (occurrence[ch] == -1)
{
occurrence[ch] = index;
}
else if (occurrence[ch] >= 0)
{
occurrence[ch] = -2;
}
str.append(1,ch);//把ch字符接到流str尾部
index++;
}
char getFirstAppearingOnceCahr()
{
char ch = '\0';
//找到哈希表中>=0的最下值对应的键的ASCII即该字符
int minIndex = std::numeric_limits::max();//返回编译器允许的最大int值。
for (int i = 0; i < 256; i++)
{
if(occurrence[i] >= 0 && occurrence[i] < minIndex)
{
ch = (char)i;
minIndex = occurrence[i];
}
}
return ch;
}
};
以上3~6题的测试例子:
#include
#include
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
string str1 = "We are students";
string str2 = "aeiou";
string ans = deleteAwhichInB(str1, str2);
cout << "delete "<
哈希表 哈希表应用 剑指offer