转:C#数据结构和算法学习系列十二----散列HashTable类
散列是一种常见的存储数据的技术,按照这种方式可以非常迅速地插入和取回数据。散列所采用的数据结构被称为是散列表。尽管散列表提供了快速地插入、 删除、以及取回数据的操作,但是诸如查找最大值或最小值这样的查找操作,散列表却无法执行地非常快。对于这类操作,其他数据结构会更适合。.NET 框架库提供了一种非常有用的处理散列表的类,即Hashtable 类。
A.散列。散列表数据结构是围绕数组设计的。虽然可以稍后根据需要增加数组的大小,但是数组是由第0 号元素一直到一些预定义尺寸的元素组成的。存储在数组内的每一个数据项都是基于一些数据块的,这被称为是关键字。为了把一个元素存储到散列表内,利用所谓 的散列函数把关键字映射到一个范围从0 到散列表大小的数上。散列函数的理想目标是把自身单元内的每一个关键字都存储到数组内。然而,由于可能的关键字是不限制数量的,而数组的大小又是有限的, 所以散列函数比较现实的目标是把关键字尽可能平均地分布到数组的单元内。
1.选择散列函数。选择散列函数是依据所用关键字的数据类型。如果所用的关键字是整数,那么最简单的函数是返回关键字对数组大小取模的结果。但是有 些情况不建议使用这种方法,比如关键字都是以0 结束,且数组的大小为10 的情况。这就是数组的大小必须始终为素数的原因之一。此外,如果关键字是随机整数,那么散列函数应该更均匀地分布关键字。下面的程序举例说明了此散列函数 的工作原理:
- using System;
- class chapter12
- {
- static void Main()
- {
- string[] names = new string[99];
- string name;
- string[] someNames = new string[]{"David","Jennifer", "Donnie", "Mayo",
- "Raymond",
- "Bernica", "Mike", "Clayton", "Beata", "Michael"};
- int hashVal;
- for (int i = 0; i < 10; i++)
- {
- name = someNames[i];
- hashVal = SimpleHash(name, names);
- names[hashVal] = name;
- }
- ShowDistrib(names);
- }
- static int SimpleHash(string s, string[] arr)
- {
- int tot = 0;
- char[] cname;
- cname = s.ToCharArray();
- for (int i = 0; i <= cname.GetUpperBound(0); i++)
- tot += (int)cname[i];
- return tot % arr.GetUpperBound(0);
- }
- static void ShowDistrib(string[] arr)
- {
- for (int i = 0; i <= arr.GetUpperBound(0); i++)
- if (arr[i] != null)
- Console.WriteLine(i + " " + arr[i]);
- }
- }
2.查找散列表中数据。为了在散列表中查找数据,需要计算关键字的散列值,然后访问数组中的对应元素。就是这样简单。下面是函数:
- static bool InHash(string s, string[] arr)
- {
- int hval = BetterHash(s, arr);
- if (arr[hval] == s)
- return true;
- else
- return false;
- }
3.解决冲突。在处理散列表的时候,不可避免地会遇到这种情况,即计算出的关键字的散列值已经存储了另外一个关键字。这就是所谓的冲突。在发生冲突的时候可以使用几种技术。这些技术包括桶式散列法、开放定址法、和双重散列法。
(1).桶式散列法。桶是一种存储在散列表元素内的简单数据结构,它可以存储多个数据项。在大多数实现中,这种数据结构就是一个数组,但是在这里的 实现中将会使用arraylist,它会允许不考虑运行超出范围而且允许分配更多的空间。最后,这种方数据结构会使实现更加高效。为了插入一个数据项,首 先用散列函数来确定哪一个arraylist 用来存储数据项。然后查看此数据项是否已经在arraylist 内。如果存在,就什么也不做。如果不存在,就调用Add 方法把此数据项添加到arraylist 内。为了从散列表中移除一个数据项,还是先确定要移除的数据项的散列值,并且转到对应的arraylist。然后查看来确信该数据项在arraylist 内。如果存在,就把它移除掉。如下:
- public class BucketHash
- {
- private const int SIZE = 101;
- ArrayList[] data;
- 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 - 1; i++)
- tot += 37 * tot + (int)charray[i];
- tot = tot % data.GetUpperBound(0);
- if (tot < 0)
- tot += data.GetUpperBound(0);
- return (int)tot;
- }
- public void Insert(string item)
- {
- int hash_value;
- hash_value = Hash(itemvalue);
- if (data[hash_value].Contains(item))
- 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);
- }
- }
当使用桶式散列法的时候,能做的最重要的事情就是保持所用的arraylist 元素的数量尽可能地少。在向散列表添加数据项或从散列表移除数据项的时候,这样会最小化所需做的额外工作。在前面的代码中,通过在构造器调用中设置每个 arraylist 的初始容量就可以最小化arraylist 的大小。一旦有了冲突,arraylist 的容量会变为2,然后每次arraylist 满时容量就会扩充两倍。虽然用一个好的散列函数,arraylist 也不应该变得太大。散列表中元素数量与表大小的比率被称为是负载系数。研究表明在负载系数为1.0 的时候,或者在表的大小恰好等于元素数量的时候,散列表的性能最佳。
(2).开放定址法。开放定址函数会在散列表数组内寻找空单元来放置数据项。如果尝试的第一个单元是满的,那么就尝试下一个空单元,如此反复直到最终找到 一个空单元为止。大家在本小节内会看到两种不同的开放定址策略:即线性探查和平方探查。线性探查法采用线性函数来确定试图插入的数组单元。这就意味着会顺 次尝试单元直到找到
一个空单元为止。线性探查的问题是数组内相邻单元中的数据元素会趋近成聚类,从而使得后续空单元的探查变得更长久且效率更低。平方探查法解决了聚类问题。平方函数用来确定要尝试哪个单元。
(3).双重散列法。双重散列法是一种有趣的冲突解决策略,但是实际上已经说明了平方探查法通常会获得更好的性能。
B.HashTable类。Hashtable 类是Dictionary 对象的一种特殊类型,它存储了键值对,其中的数值都是在源于关键字的散列代码的基础上进行存储的。这里可以为关键字的数据类型指定散列函数或者使用内置的 函数(稍后将讨论它)。Hashtable 类是非常有效率的,而且应该把它用于任何可能自定义实现的地方。
1.Hashtable使用。Hashtable 类是System.Collections 命名空间的一部分内容,所以必须在程序开始部分导入System.Collections。Hashtable 对象可实例化如下:
- Hashtable symbols = new Hashtable();
- HashtTable symbols = new Hashtable(50);
- HashTtable symbols = new Hashtable(25, 3.0F);
利用Add 方法就可以把键值对添加到散列表内。这个方法会取走两个参数:即关键字和与关键字相关联的数值。在计算完关键字的散列值之后,会把这个关键字添加到散列表内。如下:
- Hashtable symbols = new Hashtable(25);
- symbols.Add(" salary", 100000);
- symbols.Add(" name", "David Durr");
- symbols.Add(" age", 43);
- symbols.Add(" dept", "Information Technology");
还可以用索引来给散列表添加元素,为了做到这样,要编写一条赋值语句来把数值赋值给指定的关键字作为索引(这非常像数组的索引)。如果这个关键字已 经不存在了,那么就把一个新的散列元素添加到散列表内。如果这个关键字已经存在,那么就用新的数值来覆盖这个存在的数值。如下:
- Symbols["sex"] = "Male";
- Symbols["age"];
Hashtable 类有两个非常有用的方法用来从散列表中取回关键字和数值:即Keys 和Values。这些方法创建了一个Enumerator 对象,它允许使用For Each 循环或者其他一些技术来检查关键字和数值。
- using System;
- using System.Collections;
- class chapter12
- {
- static void Main()
- {
- Hashtable symbols = new Hashtable(25);
- symbols.Add(" salary", 100000);
- symbols.Add(" name", "David Durr");
- symbols.Add(" age", 45);
- symbols.Add(" dept", "Information Technology");
- symbols["sex"] = "Male";
- Console.WriteLine("The keys are: ");
- foreach (Object key in symbols.Keys)
- Console.WriteLine(key);
- Console.WriteLine();
- Console.WriteLine("The values are: ");
- foreach (Object value in symbols.Values)
- Console.WriteLine(value);
- }
- }
2.Hashtable 类的实用方法。
(1).Count 属性存储着散列表内元素的数量,它会返回一个整数。
(2).Clear 方法可以立刻从散列表中移除所有元素。
(3).Remove 方法会取走关键字,而且该方法会把指定关键字和相关联的数值都移除。
(4).ContainsKey 方法查看该元素或者数值是否在散列表内。
3.Hashtable 的应用程序。程序首先从一个文本文件中读入一系列术语和定义。这个过程是在子程序BuildGlossary 中编码实现的。文本文件的结构是:单词,定义,用逗号在单词及其定义之间进行分隔。这个术语表中的每一个单词都是单独一个词,但是术语表也可以很容易地替 换处理短语。这就是用逗号而不用空格作分隔符的原因。此外,这种结构允许使用单词作为关键字,这是构造这个散列表的正确方法。另一个子程序 DisplayWords 把单词显示在一个列表框内,所以用户可以选取一个单词来获得它的定义。既然单词就是关键字,所以能使用Keys 方法从散列表中正好返回单词。然后,用户就可以看到有定义的单词了。用户可以简单地点击列表框中的单词来获取其定义。用Item 方法就可以取回定义,并且把它显示在文本框内。
- using System;
- using System.Collections.Generic;
- using System.ComponentModel;
- using System.Data;
- using System.Drawing;
- using System.Text;
- using System.Windows.Forms;
- using System.Collections;
- using System.IO ;
- namespace WindowsApplication3
- {
- public partial class Form1 : Form
- {
- private Hashtable glossary = new Hashtable();
- public Form1()
- {
- InitializeComponent();
- }
- private void Form1_Load(object sender, EventArgs e)
- {
- BuildGlossary(glossary);
- DisplayWords(glossary);
- }
- private void BuildGlossary(Hashtable g)
- {
- StreamReader inFile;
- string line;
- string[] words;
- inFile = File.OpenText(@"c:\words.txt ");
- char[] delimiter = new char[] { ',' };
- while (inFile.Peek() != -1)
- {
- line = inFile.ReadLine();
- words = line.Split(delimiter);
- g.Add(words[0], words[1]);
- }
- inFile.Close();
- }
- private void DisplayWords(Hashtable g)
- {
- Object[] words = new Object[100];
- g.Keys.CopyTo(words, 0);
- for (int i = 0; i <= words.GetUpperBound(0); i++)
- if (!(words[i] == null))
- lstWords.Items.Add((words[i]));
- }
- private void lstWords_SelectedIndexChanged(object sender, EventArgs e)
- {
- Object word;
- word = lstWords.SelectedItem;
- txtDefinition.Text = glossary[word].ToString();
- }
- }
- }