散列表又叫哈希表,参考了众多博文,一个通俗易懂的解释就是:一个数据类型M,如果它有个函数f(key),对任意一个输入的key通过该函数都能转化为一个地址,存储在M中,那么M就是一个哈希表,这个函数就叫哈希函数。
再说说为什么需要哈希函数,我们都知道在数据最重要的就是增删改查,然而数组这种数据结构改查快,但是增删慢(因为要移位),另一种数据结构链表,它增删快,但是改查慢,那么有没有更好的方法?哈希表就是一个非常好的解决方案,对任意一个输入,根据哈希函数就能找到其对应的位置,时间复杂度近似O(1)。
进入正题,C#中我们可以自定义散列表也可以使用Hashtable类,下面分别来介绍。
一、自定义散列表
首先,既然要自定义散列表,那么我们需要为散列表设定一种数据结构,并且给定散列表的长度,这里用数组Array作为例子,存储我们的键值对数据。
其次,要有散列表那么一定要有散列函数或者叫哈希函数,如何设置这个函数呢?大家很容易想到的问题是假如两个键值传进散列函数后得到同样的结果该怎么办,这个问题在哈希表里有个专门的名字叫冲突,所以散列函数的设置有很多技巧,包括散列表的容量也有要求,后面再说明。
最后,上代码,看注释:
class Program
{
static void Main(string[] args)
{
//散列表的容量,需要为素数(因为常与其做取模运算)
string[] namesHash = GetHashTable(97);
foreach (var item in namesHash)
{
if (item != null)
{
WriteLine(CaculateHashVal(item,namesHash.Length)+ " "+ item);
}
}
}
private static string[] GetHashTable(int Capacity)
{
//返回一个哈希表,根据里面存储的数据类型,这是一个string类型的哈希表
string[] names = new string[Capacity];
string[] somenames = { "xiaoyao", "zhaohongyu", "hahahah", "xiaofang", "liweiwie", "ruanxuemei", "zhaojiangshan", "happy" };
for (int i = 0; i < somenames.Length; i++)
{
int hashVal = CaculateHashVal(somenames[i], names.Length);
names[hashVal] = somenames[i];
}
return names;
}
private static int CaculateHashVal(string v,int hashtableLength)
{
//返回一个存储string数据的地址
//一个简单的哈希函数,根据各个元素字符的ASCII码的和取模运算
int temp = 0;
foreach (char item in v)
{
temp += item;
}
return temp%hashtableLength;
}
private static bool FindInHash(string str,string[] hashtable)
{
//在哈希表中查找,效率非常高
int hashval = CaculateHashVal(str,hashtable.Length);
if(hashtable[hashval]== str)
{
return true;
}
else
{
return false;
}
}
}
看看结果:
这里采用的哈希函数为将每个字符串的所有字符的ASCII码加起来再对哈希表的容量取模,为了避免冲突,哈希表的容量一般为素数。但是我们可以看到这里的数值分布不够均匀,这样的散列表效率不高,那么提高效率的关键就在于设置一个好的哈希函数,这里其实还可以使用霍纳(Horner)法则让数值分布更均匀。
二、如何解决冲突
解决哈希表的冲突一般有三种方法,分别叫桶式散列法、开放定址法和双重散列法。
桶式散列法是指将每一个散列表中的键值对都扩充为一个数组,这样如果产生了同样的键值,我们就将该键值对放入键值所在的数组中,这就类似于一个二维数组,从而解决冲突问题。
下面给出一个桶式散列法的例子:
class BucketHash
{
//桶式散列法
private const int SIZE = 101;
ArrayList[] data;
//构造一个ArrayList的二维列表
//并且每个一维元素是一个容量为4的ArrayList
public BucketHash()
{
data = new ArrayList[SIZE];
for (int i = 0; i <= SIZE-1; i++)
{
data[i] = new ArrayList(4);
}
}
public int Hash(string s)
{
long tot = 0;
char[] charray;
charray = s.ToCharArray();
//这里采用了霍纳法则来计算哈希函数
for (int i = 0; i < s.Length; i++)
{
tot += 37 * tot + (int)charray[i];
}
tot = tot % data.Length;
if (tot < 0)
{
tot += data.Length;
}
return (int)tot;
}
public void Insert(string item)
{
int hash_value;
hash_value = Hash(item);
//如果对应散列结果已经有了,那加入到对应的桶中
if (data[hash_value].Count!= 0)
{
data[hash_value].Add(item);
}
else
{
data[hash_value].Add(item);
}
}
public void Remove(string item)
{
int hash_value;
hash_value = Hash(item);
if (data[hash_value].Contains(item))
{
data[hash_value].Remove(item);
}
}
static void Main(string[] args)
{
}
}
开放定址法是指如果某个地址被占据了,那么我就可以在其附近寻找空的地址,然后将键值对放入即可,那么寻找空地址也有一些方法,包括线性探查发和平方探查法等等。
双重散列法也很好理解,当我们发现某个键已经被占据了,那么我们就再将它哈希一下,就是用哈希函数对其进行二次计算,当然要考虑无限循环等问题。
三、内置的哈希函数
C#中内置的哈希函数十分好用,各个功能类似于ArrayList一样,下面给出例子。注意引入命名空间System.Collection
class Program
{
static void Main(string[] args)
{
Hashtable symblos = new Hashtable(25);
symblos.Add("salary", 100000);
symblos.Add("name", "Ravid Durr");
symblos.Add("age", 28);
symblos.Add("dept", "Information Technology");
//如果没有键sex则增加键值对
symblos["sex"] = "Male";
//如果有键age则修改为45
symblos["age"] = 45;
foreach (var item in symblos.Keys)
{
WriteLine(item);
}
WriteLine();
foreach (var item in symblos.Values)
{
WriteLine(item);
}
WriteLine();
WriteLine(symblos.Count);
symblos.Remove("salary");
WriteLine(symblos.ContainsValue(100000));
}
}