一.初识哈希表
哈希表就是一种以 键-值(key-indexed) 存储数据的结构,我们只要输入待查找的值即key,即可查找到其对应的值。
哈希的思路很简单,如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。
使用哈希查找有两个步骤:
- 使用哈希函数将被查找的键转换为数组的索引。在理想的情况下,不同的键会被转换为不同的索引值,但是在有些情况下我们需要处理多个键被哈希到同一个索引值的情况。所以哈希查找的第二个步骤就是处理冲突
- 处理哈希碰撞冲突。有很多处理哈希碰撞冲突的方法,如拉链法和线性探测法。
哈希表是一个在时间和空间上做出权衡的经典例子。如果没有内存限制,那么可以直接将键作为数组的索引。那么所有的查找时间复杂度为O(1);如果没有时间限制,那么我们可以使用无序数组并进行顺序查找,这样只需要很少的内存。哈希表使用了适度的时间和空间来在这两个极端之间找到了平衡。只需要调整哈希函数算法即可在时间和空间上做出取舍。
二.哈希使用
没有对比就没有伤害,常用的数据结构:
数组
使用场景
数组在以下三个情形下很有用:
1)数据量较小。
2)数据规模已知。
3)随机访问,修改元素值。
如果插入速度很重要,选择无序数组。如果查找速度很重要,选择有序数组,并使用二分查找。
缺点
1)需要预先知道数据规模
2)插入效率低,因为需要移动大量元素。
链表
解决的问题
链表的出现解决了数组的两个问题:
1)需要预先知道数据规模
2)插入效率低
使用场景
1)数据量较小
2)不需要预先知道数据规模
3)适应于频繁的插入操作
缺点
1)有序数组可以通过二分查找方法具有很高的查找效率(O(log n)),而链表只能使用顺序查找,效率低下(O(n))。
二叉查找树
解决的问题
1)有序数组具有较高的查找效率(O(log n)),而链表具有较高的插入效率(头插法,O(1)),结合这两种数据结构,创建一种貌似完美的数据结构,也就是二叉查找树。
使用场景
1)数据是随机分布的
2)数据量较大
3)频繁的查找和插入操作(可以提供O(log n)级的查找、插入和删除操作)
缺点
1)如果处理的数据是有序的(升序/降序),那么构造的二叉查找树就会只有左子树(或右子树),也就是退化为链表,查找效率低下(O(log n))。
平衡树
解决的问题
1)针对二叉查找树可能会退化为链表的情况,提出了平衡树,平衡树要求任意节点的左右两个子树的高度差不超过1,避免退化为链表的情况。
使用场景
1)无论数据分布是否随机都可以提供O(log n)级别的查找、插入和删除效率
2)数据量较大
缺点
1)平衡树的实现过于复杂。
哈希表
解决的问题
同平衡树一样,哈希表也不要求数据分布是否随机,不过哈希表的实现比平衡树要简单得多。
使用场景
1)不需要对最大最小值存取。
2)无论数据分布是否随机,理想情况下(无冲突)可以提供O(1)级别的插入、查找和删除效率。
3)数据量较大
缺点
1)由于是基于数组的,数组(哈希表)创建后难以扩展,使用开放地址法的哈希表在基本被填满时,性能下降的非常严重。
2)不能对最大最小值存取。
专用数据结构
栈
顺序栈
优点
1)在输入数据量可预知的情形下,可以使用数组实现栈,并且数组实现的栈效率更高,出栈和入栈操作都在数组末尾完成。
缺点
1)如果对数组大小创建不当,可能会产生栈溢出的情况
链栈
优点
1)不会发生栈溢出的情况
2)输入数据量未知时,使用链栈。通过头插法实现入栈操作,头删法实现出栈操作。出栈和入栈均是O(1)。
缺点
1)由于入栈时,首先要创建插入的节点,要向操作系统申请内存,所以链栈没有顺序栈效率高。
队列
如果数据量已知就使用数组实现队列,未知的话就使用链表实现队列。出队和入队均是O(1)。
| 数据结构 | 优点 | 缺点 | |
| 数组 | 插入快 | 查找慢、删除慢、大小固定 | |
| 有序数组 | 查找快 | 插入慢、删除慢、大小固定 | |
| 栈 | 后进先出 | 存取其他项很慢 | |
| 队列 | 先进先出 | 存取其他项很慢 | |
| 链表 | 插入、删除快 | 查找慢 | |
| 二叉树 | 查找、插入、删除快 | 算法复杂(删除算法) | |
| 红黑树 | 查找、插入、删除快 | 算法复杂 | |
| hash表 | 存取极快(已知关键字)、插入快 | 删除慢、不知关键字时存取很慢、对存储空间使用不充分 | |
| 堆 | 插入快、删除快、对大数据项存取快 | 对其他数据项存取慢 | |
| 图 | 依据现实世界建模 | 算法有些复杂 | |
| AVL树 | 查找、插入、删除快 | 算法复杂 |
3.哈希函数构造
就是映射函数构造,看某个元素具体属于哪一个类别。
除余法: 选择一个适当的正整数 p ,令 h(k ) = k mod p ,这里, p 如果选取的是比较大的素数,效果比较好。而且此法非常容易实现,因此是最常用的方法。最直观的一种,上图使用的就是这种散列法,公式:
index = value % 16
学过汇编的都知道,求模数其实是通过一个除法运算得到的,所以叫“除法散列法”。
平方散列法
求index是非常频繁的操作,而乘法的运算要比除法来得省时(对现在的CPU来说,估计我们感觉不出来),所以我们考虑把除法换成乘法和一个位移操作。公式:
index = (value * value) >> 28 ( 右移,除以2^28。记法:左移变大,是乘。右移变小,是除)
数字选择法: 如果关键字的位数比较多,超过长整型范围而无法直接运算,可以选择其中数字分布比较均匀的若干位,所组成的新的值作为关键字或者直接作为函数值。
斐波那契(Fibonacci)散列法:平方散列法的缺点是显而易见的,所以我们能不能找出一个理想的乘数,而不是拿value本身当作乘数呢?答案是肯定的。
1,对于16位整数而言,这个乘数是40503
2,对于32位整数而言,这个乘数是2654435769
3,对于64位整数而言,这个乘数是11400714819323198485
这几个“理想乘数”是如何得出来的呢?这跟一个法则有关,叫黄金分割法则,而描述黄金分割法则的最经典表达式无疑就是著名的斐波那契数列,即如此形式的序列:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,377, 610, 987, 1597, 2584, 4181, 6765, 10946,…。另外,斐波那契数列的值和太阳系八大行星的轨道半径的比例出奇吻合。
对我们常见的32位整数而言,公式:
index = (value * 2654435769) >> 28
4.冲突处理
线性重新散列技术易于实现且可以较好的达到目的。令数组元素个数为 S ,则当 h(k) 已经存储了元素的时候,依次探查 (h(k)+i) mod S , i=1,2,3…… ,直到找到空的存储单元为止(或者从头到尾扫描一圈仍未发现空单元,这就是哈希表已经满了,发生了错误。当然这是可以通过扩大数组范围避免的)。
5.hash索引跟B树索引的区别。
Hash 索引结构的特殊性,其检索效率非常高,索引的检索可以一次定位,不像B-Tree 索引需要从根节点到枝节点,最后才能访问到页节点这样多次的IO访问,所以 Hash 索引的查询效率要远高于 B-Tree 索引。
(1)Hash 索引仅仅能满足”=”,”IN”和”<=>”查询,不能使用范围查询。
由于 Hash 索引比较的是进行 Hash 运算之后的 Hash 值,所以它只能用于等值的过滤,不能用于基于范围的过滤,因为经过相应的 Hash 算法处理之后的 Hash 值的大小关系,并不能保证和Hash运算前完全一样。
(2)Hash 索引无法被用来避免数据的排序操作。
由于 Hash 索引中存放的是经过 Hash 计算之后的 Hash 值,而且Hash值的大小关系并不一定和 Hash 运算前的键值完全一样,所以数据库无法利用索引的数据来避免任何排序运算;
(3)Hash 索引不能利用部分索引键查询。
对于组合索引,Hash 索引在计算 Hash 值的时候是组合索引键合并后再一起计算 Hash 值,而不是单独计算 Hash 值,所以通过组合索引的前面一个或几个索引键进行查询的时候,Hash 索引也无法被利用。
(4)Hash 索引在任何时候都不能避免表扫描。
前面已经知道,Hash 索引是将索引键通过 Hash 运算之后,将 Hash运算结果的 Hash 值和所对应的行指针信息存放于一个 Hash 表中,由于不同索引键存在相同 Hash 值,所以即使取满足某个 Hash 键值的数据的记录条数,也无法从 Hash 索引中直接完成查询,还是要通过访问表中的实际数据进行相应的比较,并得到相应的结果。
(5)Hash 索引遇到大量Hash值相等的情况后性能并不一定就会比B-Tree索引高。
对于选择性比较低的索引键,如果创建 Hash 索引,那么将会存在大量记录指针信息存于同一个 Hash 值相关联。这样要定位某一条记录时就会非常麻烦,会浪费多次表数据的访问,而造成整体性能低下。
6.
实现
问题描述:设计哈希表实现电话号码查询系统,实现下列功能:
(1) 假定每个记录有下列数据项:电话号码、用户名、地址。
(2) 一是从数据文件old.txt(自己现行建好)中读入各项记录,二是由系统随机产生各记录,并且把记录保存到new.txt文件中以及显示到屏幕上,记录条数不要少于30,然后分别以电话号码和用户名为关键字建立哈希表。
(3) 分别采用伪随机探测再散列法和再哈希法解决冲突。
(4) 查找并显示给定电话号码的记录;查找并显示给定用户名的记录。
(5) 将没有查找的结果保存到结果文件Out.txt中,显示查找结果前,要有提示语句。
// MyHashTable.cpp : 定义控制台应用程序的入口点。
////设计哈希表实现电话号码查询系统
//说明:一是从文件old.txt中读取的数据自己在程序运行前建立,
// 二是由系统随机生成数据,在程序运行由随机数产生器生成,并且将产生的记录保存到 new.txt文件。
//存在的问题:使用随机产生的文件,在显示时出现乱码
#include "stdafx.h"
#include<fstream>//文件流
#include<iostream>
#include <string>
using namespace std;
const int D[] = {3,5,8,11,13,14,19,21};//预定再随机数
const int HASH_MAXSIZE = 50;//哈希表长度
//记录信息类型
class DataInfo
{
public:
DataInfo();//默认构造函数
friend ostream& operator<<(ostream& out, const DataInfo& dataInfo); //重载输出操作符
//friend class HashTable;
//private:
string name;//姓名
string phone;//电话号码
string address;//地址
char sign;//冲突的标志位,'1'表示冲突,'0'表示无冲突
};
DataInfo::DataInfo():name(""), phone(""), address(""), sign('0')
{
}
ostream& operator<<(ostream& out, const DataInfo& dataInfo) //重载输出操作符
{
cout << "姓名:" << dataInfo.name << " 电话:" << dataInfo.phone
<< " 地址:" << dataInfo.address << endl;
return out;
}
//存放记录的哈希表类型
class HashTable
{
public:
HashTable();//默认构造函数
~HashTable();//析构函数
int Random(int key, int i);// 伪随机数探测再散列法处理冲突
void Hashname(DataInfo *dataInfo);//以名字为关键字建立哈希表
int Rehash(int key, string str);// 再哈希法处理冲突 注意处理冲突还有链地址法等
void Hashphone(DataInfo *dataInfo);//以电话为关键字建立哈希表
void Hash(char *fname, int n);// 建立哈希表
//fname 是数据储存的文件的名称,用于输入数据,n是用户选择的查找方式
int Findname(string name);// 根据姓名查找哈希表中的记录对应的关键码
int Findphone(string phone);// 根据电话查找哈希表中的记录对应的关键码
void Outhash(int key);// 输出哈希表中关键字码对应的一条记录
void Outfile(string name, int key);// 在没有找到时输出未找到的记录
void Rafile();// 随机生成文件,并将文件保存在 new.txt文档中
void WriteToOldTxt();//在运行前先写入数据
//private:
DataInfo *value[HASH_MAXSIZE];
int length;//哈希表长度
};
HashTable::HashTable():length(0)//默认构造函数
{
//memset(value, NULL, HASH_MAXSIZE*sizeof(DataInfo*));
for (int i=0; i<HASH_MAXSIZE; i++)
{
value[i] = new DataInfo();
}
}
HashTable::~HashTable()//析构函数
{
delete[] *value;
}
void HashTable::WriteToOldTxt()
{
ofstream openfile("old.txt");
if (openfile.fail())
{
cout << "文件打开错误!" << endl;
exit(1);
}
string oldname;
string oldphone;
string oldaddress;
for (int i=0; i<30; i++)
{
cout << "请输入第" << i+1 << "条记录:" << endl;
cin >> oldname ;
cin >> oldphone;
cin >> oldaddress;
openfile << oldname << " " << oldphone << " " << oldaddress << "," << endl;
}
openfile.close();
}
int HashTable::Random(int key, int i)// 伪随机数探测再散列法处理冲突
{//key是冲突时的哈希表关键码,i是冲突的次数,N是哈希表长度
//成功处理冲突返回新的关键码,未进行冲突处理则返回-1
int h;
if(value[key]->sign == '1')//有冲突
{
h = (key + D[i]) % HASH_MAXSIZE;
return h;
}
return -1;
}
void HashTable::Hashname(DataInfo *dataInfo)//以名字为关键字建立哈希表
{//利用除留取余法建立以名字为关键字建立的哈希函数,在发生冲突时调用Random函数处理冲突
int i = 0;
int key = 0;
for (int t=0; dataInfo->name[t]!='\0'; t++)
{
key = key + dataInfo->name[t];
}
key = key % 42;
while(value[key]->sign == '1')//有冲突
{
key = Random(key, i++);//处理冲突
}
if(key == -1) exit(1);//无冲突
length++;//当前数据个数加
value[key]->name = dataInfo->name;
value[key]->address = dataInfo->address;
value[key]->phone = dataInfo->phone;
value[key]->sign = '1';//表示该位置有值
//cout << value[key]->name << " " << value[key]->phone << " " << value[key]->address << endl;
}
int HashTable::Rehash(int key, string str)// 再哈希法处理冲突
{//再哈希时使用的是折叠法建立哈希函数
int h;
int num1 = (str[0] - '0') * 1000 + (str[1] - '0') * 100 + (str[2] - '0') * 10 + (str[3] - '0');
int num2 = (str[4] - '0') * 1000 + (str[5] - '0') * 100 + (str[6] - '0') * 10 + (str[7] - '0');
int num3 = (str[8] - '0') * 100 + (str[9] - '0') * 10 + (str[10] - '0');
h = num1 + num2 + num3;
h = (h + key) % HASH_MAXSIZE;
return h;
}
void HashTable::Hashphone(DataInfo *dataInfo)//以电话为关键字建立哈希表
{//利用除留取余法建立以电话为关键字建立的哈希函数,在发生冲突时调用Rehash函数处理冲突
int key = 0;
int t;
for(t=0; dataInfo->phone[t] != '\0'; t++)
{
key = key + dataInfo->phone[t];
}
key = key % 42;
while(value[key]->sign == '1')//有冲突
{
key = Rehash(key, dataInfo->phone);
}
length++;//当前数据个数加
value[key]->name = dataInfo->name;
value[key]->address = dataInfo->address;
value[key]->phone = dataInfo->phone;
value[key]->sign = '1';//表示该位置有值
}
void HashTable::Outfile(string name, int key)//在没有找到时输出未找到的记录
{
ofstream fout;
if((key == -1)||(value[key]->sign == '0'))//判断哈希表中没有记录
{
fout.open("out.txt",ios::app);//打开文件
if(fout.fail())
{
cout << "文件打开失败!" << endl;
exit(1);
}
fout << name << endl;//将名字写入文件,有个问题,每次写入的时候总是将原来的内容替换了
fout.close();
}
}
void HashTable::Outhash(int key)//输出哈希表中关键字码对应的记录
{
if((key==-1)||(value[key]->sign=='0'))
cout << "没有找到这条记录!" << endl;
else
{
for(unsigned int i=0; value[key]->name[i]!='\0'; i++)
{
cout << value[key]->name[i];
}
for(unsigned int i=0; i<10; i++)
{
cout << " ";
}
cout << value[key]->phone;
for(int i=0; i<10; i++)
{
cout << " ";
}
cout << value[key]->address << endl;
}
}
void HashTable::Rafile()//随机生成文件,并将文件保存在new.txt文档中
{
ofstream fout;
fout.open("new.txt");//打开文件,等待写入
if(fout.fail())
{
cout << "文件打开失败!" << endl;
exit(1);
}
for(int j=0; j<30; j++)
{
string name = "";
for(int i=0; i<20; i++)//随机生成长个字的名字
{
name += rand() % 26 + 'a';//名字是由个字母组成
}
fout << name << " ";//将名字写入文件
string phone = "";
for(int i=0; i<11; i++)//随机生成长位的电话号码
{
phone += rand() % 10 + '0';//电话号码是纯数字
}
fout << phone << " ";//将电话号码写入文件
string address = "";
for(int i=0; i<29; i++)//随机生成长个字的名字
{
address += rand() % 26 + 'a';//地址是由个字母组成
}
address += ',';
fout << address << endl;//将地址写入文件
}
fout.close();
}
void HashTable::Hash(char *fname, int n)//建立哈希表
//fname是数据储存的文件的名称,用于输入数据,n是用户选择的查找方式
//函数输入数据,并根据选择调用Hashname或Hashphone函数进行哈希表的建立
{
ifstream fin;
int i;
fin.open(fname);//读文件流对象
if(fin.fail())
{
cout << "文件打开失败!" << endl;
exit(1);
}
while(!fin.eof())//按行读入数据
{
DataInfo *dataInfo = new DataInfo();
char* str = new char[100];
fin.getline(str, 100, '\n');//读取一行数据
if(str[0] == '*')//判断数据结束
{
break;
}
i = 0;//记录字符串数组的下标
//a-z:97-122 A-Z:65-90
//本程序的姓名和地址都使用小写字母
while((str[i] < 97) || (str[i] > 122))//读入名字
{
i++;
}
for(; str[i]!=' '; i++)
{
dataInfo->name += str[i];
}
while(str[i] == ' ')
{
i++;
}
for(int j=0; str[i]!=' '; j++,i++)//读入电话号码
{
dataInfo->phone += str[i];
}
while(str[i] == ' ')
{
i++;
}
for(int j=0; str[i]!=','; j++,i++)//读入地址
{
dataInfo->address += str[i];
}
if(n == 1)
{
Hashname(dataInfo);
}
else
{
Hashphone(dataInfo);//以电话为关键字
}
delete []str;
delete dataInfo;
}
fin.close();
}
int HashTable::Findname(string name)//根据姓名查找哈希表中的记录对应的关键码
{
int i = 0;
int j = 1;
int t;
int key = 0;
for(key=0, t=0; name[t] != '\0'; t++)
{
key = key + name[t];
}
key = key % 42;
while((value[key]->sign == '1') && (value[key]->name != name))
{
key = Random(key, i++);
j++;
if(j >= length) return -1;
}
return key;
}
int HashTable::Findphone(string phone)//根据电话查找哈希表中的记录对应的关键码
{
int key = 0;
int t;
for(t=0; phone[t] != '\0' ; t++)
{
key = key + phone[t];
}
key = key % 42;
int j = 1;
while((value[key]->sign == '1') && (value[key]->phone != phone))
{
key = Rehash(key, phone);
j++;
if(j >= length)
{
return -1;
}
}
return key;
}
void main()
{
//WriteToOldTxt();
int k;
int ch;
char *Fname;
HashTable *ht = new HashTable;
while(1)
{
system("cls");//cls命令清除屏幕上所有的文字
cout << "欢迎使用本系统!" << endl << endl;
cout << "请选择数据" << endl;
cout << "1.使用已有数据文件" << endl;
cout << "2.随机生成数据文件" << endl;
cout << "0.结束" << endl;
cout << "输入相应序号选择功能:";
cin >> k;
switch(k)
{
case 0:
return;
case 1:
Fname = "old.txt";//从数据文件old.txt(自己现行建好)中读入各项记录
break;
case 2:
ht->Rafile();
Fname = "new.txt";//由系统随机产生各记录,并且把记录保存到new.txt文件中
break;
default:
cout << "输入序号有误,退出程序。" << endl;
return;
}
do
{
system("cls");
cout << " 请选择查找方式" << endl;
cout << "1.通过姓名查找" << endl;
cout << "2.通过电话查找" << endl;
cout << "输入相应序号选择功能:";
cin >> ch;
if((ch != 1) && (ch != 2))
cout << "输入序号有误!" << endl;
}while((ch != 1) && (ch != 2));
ht->Hash(Fname, ch);
while(ch == 1)
{
int choice;
cout << endl << "请选择功能" << endl;
cout << "1.输入姓名查找数据" << endl;
cout << "2.显示哈希表" << endl;
cout << "0.退出"<<endl;
cout << "输入相应序号选择功能:";
cin >> choice;
switch(choice)
{
case 1:
{//注意此处应该加上大括号
int key1;
string name;
cout << "请输入姓名:";
cin >> name;
key1 = ht->Findname(name);
ht->Outfile(name, key1);
ht->Outhash(key1);
}
break;
case 2:
{
for(int i=0; i<HASH_MAXSIZE; i++)
{
if(ht->value[i]->sign!='0')
{
ht->Outhash(i);
}
}
}
break;
default:
cout << endl << "您的输入有误!" << endl;
}
if(choice == 0)
{
return;
}
}
while(ch == 2)
{
int choice;
cout << endl << "请选择功能" << endl;
cout << "1.输入电话查找数据" << endl;
cout << "2.显示哈希表"<<endl;
cout << "0.退出"<<endl;
cout << "输入相应序号选择功能:";
cin >> choice;
switch(choice)
{
case 1:
{
int key2;
string phone;
cout << "请输入11位的电话号码:";
do
{
cin >> phone;
if(phone.length() != 11)
{
cout << "电话号码应为11位!\n请重新输入:";
}
}while(phone.length() != 11);
key2 = ht->Findphone(phone);
ht->Outfile(phone, key2);
ht->Outhash(key2);
}
break;
case 2:
{
for(int i=0; i<HASH_MAXSIZE; i++)
{
if(ht->value[i]->sign != '0')
{
ht->Outhash(i);
}
}
}
break;
default:
cout << endl << "您的输入有误!" << endl;
}
if(choice == 0)
{
return;
}
}
while((ch != 1) && (ch != 2))
{
cout << "您的输入有误!请输入相应需要选择功能:";
}
}
system("pause");
}