【必须学好】字典、哈希与Map

http://blog.csdn.net/cownew/article/details/6478993


HashMap、Map等是很多公司面试、笔试的时候常考的题目,也是实际开发中经常用到的数据结构,必须好好掌握。因此我从《J2EE开发全程实录》中摘取了下面的片段,希望对同学们有帮助。学习时请对照着《数据结构》这门课中“散列”相关的章节复习。

在实际问题中,按照给定的值进行数据查询是经常遇到的,比如,在电话号码簿中查询某个人的电话号码;在图书馆中按照ISBN编号查找某本书的位置;在地图中按照坐标查找某个地点的地名等等。为此,人们创造了一种能够根据记录的关键码(也就是用以标识数据在记录中的存放位置的数据项)方便的检索到对应的记录信息的数据结构,这就是字典(Dictionary)

2.2.1字典的定义

我们都使用过字典,如英汉字典、成语字典,图书的检索目录、电话簿等也可以看作广义上的字典。在计算机科学中,把字典也当成一种数据结构。

我们把字典定义为“键-值对”(Key-Value Pair)的集合。根据不同的问题,我们为名字和值赋予不同的含义,比如,在英汉字典中,英文单词是名字,此单词的中文解释条目是值;在电话簿中,人名是名字,此人名对应的电话号码是值。

字典最基本的操作包括:find(查找)add(插入)remove(删除),分别用来从字典中检索数据、插入数据和删除数据。在实际存储中,我们将“键-值对”存储于记录中,通过键(也就是“键-值对”中的名字)来标识该“键-值对”。“键-值对”的存放位置和其键之间的对应关系用一个二元组表示:(,值的位置)

从字典中查找“键-值对”的最简单方法就是使用数组存储,然后在查找的时候遍历此数组,当遍历到和被查找的“键-值对”的名字相同项的时候,这个“键-值对”就被找到了。这种最朴实的方式肯定是不能满足实际要求的,因此人们发明了一种检索效率非常高的组织字典数据的方法,即哈希表结构。

2.2.2哈希表与哈希方法

哈希方法在“键-值对”的存储位置与它的键之间建立一个确定的对应函数关系hash(),使得每一个键与结构中的一个唯一的存储位置相对应:

存储位置=hash()

在搜索时,首先对键进行hash运算,把求得的值当做“键-值对”的存储位置,在结构中按照此位置取“键-值对”进行比较,若键相等,则表示搜索成功。在存储“键-值对”的时候,依照相同的hash函数计算存储位置,并按此位置存放,这种方法就叫做哈希方法,也叫做散列方法。在哈希方法中使用的转换函数hash被称作哈希函数(或者散列函数)。按照此中算法构造出来的表叫做哈希表(或者散列表)

哈希函数建立了从“键-值对”到哈希表地址集合的一个映射,有了哈希函数,我们就可以根据键来确定“键-值对”在哈希表中的位置的地址。使用这种方法由于不必进行多次键的比较,所以其搜索速度非常快,很多系统都使用这种方法进行数据的组织和检索。

举一个例子,有一组“键值对”:<5,tom><8,Jane><12,Bit><17,Lily><20,sunny>,我们按照如下哈希函数对键进行计算:hash(x)=x%17+3,得出如下结果:hash(5)=8hash(8)=11hash(12)=15hash(17)=3hash(20)=6。我们把<5,tom><8,Jane><12,Bit><17,Lily><20,sunny>分别放到地址为8111536的位置上。当要检索17对应的值的时候,只要首先计算17的哈希值为3,然后到地址为3的地方去取数据就可以找到17对应的数据是“Lily”了,可见检索速度是非常快的。

2.2.3冲突与冲突的解决

通常键的取值范围比哈希表地址集合大很多,因此有可能经过同一哈希函数的计算,把不同的键映射到了同一个地址上面,这就叫冲突。比如,有一组“键-值对”,其键分别为123617251330930976,采用的哈希函数是:

public static int hash(int key)

{

return key%73+13420;

}

则将会得到hash(12361)=hash(7251)=hash(3309)=hash(30976)=13444,即不同的键通过哈希函数对应到了同一个地址,我们称这种哈希计算结果相同的不同键为同义词。

如果“键-值对”在加入哈希表的时候产生了冲突,就必须找另外一个地方来存放它,冲突太多会降低数据插入和搜索的效率,因此希望能找到一个不容易产生冲突的函数,即构造一个地址分布比较均匀的哈希函数。常用的哈希函数包括:直接定址法、数字分析法、除留余数法、乘留余数法、平方取中法、折叠法等。应该根据实际工作中关键码的特点选用适当的方法。

虽然采用合适的哈希方法能够降低冲突的概率,但是冲突仍然是不可避免的,处理冲突的最常用方法就是“桶”算法:假设哈希表有m个地址,就将其改为m个“桶”,其桶号与哈希地址一一对应,每个桶都用来存放互为同义词的键,也就是如果两个不同的键用哈希函数计算得到了同一个哈希地址,就将它们放到同一个桶中,检索的时候在桶内进行顺序检索。

2.2.4Java中的Map接口

字典数据结构如此重要,以至于实际开发中经常需要使用它们。JDK中提供了相关的类供我们使用,从而避免了自己开发字典类的麻烦。

在以前版本的JDK中,最常使用的字典类就是Dictionary抽象类及其实现类Hashtable,不过在新版本的JDK中不推荐读者使用Dictionary抽象类而是使用Map接口,并且由于Dictionary的实现类Hashtable也实现了Map接口,所以我们没有理由不使用Map接口。

Map接口有很多实现类,比如HashMapTreeMapHashtableSortedMap等,在第三方开源包中也有提供了更多功能的实现类,比如Apache-Commons项目中的LRUMap。最常用的就是HashMapHashtable,它们最大的区别就是Hashtable是线程安全的,而HashMap则不是线程安全的,在使用的时候必须进行同步。由于JDK中的工具类java.util.Collections提供了一个synchronizedMap方法,可以将非线程安全的Map接口变量采用装饰者模式改造成线程安全的,因此使用HashMap的场合更多一些,后边的论述也将以HashMap为主。

2.3HashMap

HashMapMap接口的实现类中最常用的一个,熟练的掌握这个类的使用将会提高解决问题的速度

HashMap的主要方法

int size():得到Map中“键-值对”的数量

boolean isEmpty():Map是否是空的,也就是是否不含有任何“键-值对”

boolean containsKey(Object key):Map中是否含有以key为键的“键-值对”

boolean containsValue(Object value):Map中是否含有以value为值的“键-值对”

Object get(Object key):从Map中得到以key为键的值,如果Map中不含有以key为键的“键-值对”则返回null

Object put(Object key, Object value):向Map中存储以key为键、value为值的“键-值对”

Object remove(Object key):从Map中移除以key为键的“键-值对”

void putAll(Map t):将另一个Map中的所有“键-值对”导入到此Map中

void clear():清除所有“键-值对”

Set keySet():得到所有的键

Collection values():得到所有的值

Set entrySet():得到所有的“键-值对”,Set中的类型是Map.Entry

2.3.1应用举例

【实例2-2】工号查询

Tom的工号是1155669Jim的工号是1155689Jane的工号是1255669Kevin的工号是1165669Bit的工号是1155660Gavin的工号是1155639。请编写一个程序,输入工号后显示此工号对应的人名。

代码 2.5 工号查询

public class Map01

{

public static void main(String[] args)

{

NameSearcher ns = new NameSearcher();

System.out.println(ns.searchByNum("1155669"));

}

}

class NameSearcher

{

private Map map;

public NameSearcher()

{

super();

map = new HashMap();

map.put("1155669", "Tom");

map.put("1155689", "Jim");

map.put("1255669", "Jane");

map.put("1165669", "Kevin");

map.put("1155660", "Bit");

map.put("1155639", "Gavin");

}

public String searchByNum(String num)

{

return (String) map.get(num);

}

}

运行结果

Tom

【实例2-3】枚举工号

Tom的工号是1155669Jim的工号是1155689Jane的工号是1255669Kevin的工号是1165669Bit的工号是1155660Gavin的工号是1155639。请编写一个程序,显示所有工号。

代码 2.6 枚举工号

Map map = new HashMap();

map.put("1155669", "Tom");

map.put("1155689", "Jim");

map.put("1255669", "Jane");

map.put("1165669", "Kevin");

map.put("1155660", "Bit");

map.put("1155639", "Gavin");

Set keySet = map.keySet();

Iterator iterator = keySet.iterator();

while(iterator.hasNext())

{

String key = (String) iterator.next();

System.out.println(key);

}

MapkeySet方法返回的是所有的键,如果要显示所有人名,只要使用values方法即可。注意values方法的返回值是Collection 接口,与keySet方法的Set类型不同,原因很简单,因为值不像键一样,值是存在重复的情况的。

运行结果:

1155689

1155660

1155639

1165669

1155669

1255669

【实2-3枚举工号名字

Tom的工号是1155669Jim的工号是1155689Jane的工号是1255669Kevin的工号是1165669Bit的工号是1155660Gavin的工号是1155639。请编写一个程序,显示所有工号-名字“键-值对”。

代码 2.7 枚举工号名字

Map map = new HashMap();

map.put("1155669", "Tom");

map.put("1155689", "Jim");

map.put("1255669", "Jane");

map.put("1165669", "Kevin");

map.put("1155660", "Bit");

map.put("1155639", "Gavin");

Set entrySet = map.entrySet();

Iterator iterator = entrySet.iterator();

while (iterator.hasNext())

{

Map.Entry keyValue = (Map.Entry) iterator.next();

Object key = keyValue.getKey();

Object value = keyValue.getValue();

System.out.println("工号:" + key + ";姓名:" + value);

}

运行结果

工号:1155689;姓名:Jim

工号:1155660;姓名:Bit

工号:1155639;姓名:Gavin

工号:1165669;姓名:Kevin

工号:1155669;姓名:Tom

工号:1255669;姓名:Jane

【实2-4嵌套哈希

某公司分为多个部门,各部门的名称不同,而且公司不设立统一的工号,而是每个部门内部自己指定工号,各个部门之间的工号有可能相同。请编写一个程序,用户输入部门名称和工号,检索出对应人的人名。

代码 2.8是嵌套HashMap的实现方式。

代码 2.8 嵌套哈希

public class Map04

{

public static void main(String[] args)

{

PersonSearch04 ps = new PersonSearch04();

ps.add("开发一部", "001", "Tom");

ps.add("开发一部", "002", "Jane");

ps.add("开发一部", "003", "Popo");

ps.add("开发二部", "002", "Ruby");

ps.add("开发二部", "003", "Jay");

ps.add("开发二部", "005", "Cheris");

System.out.println(ps.get("开发二部", "002"));

}

}

class PersonSearch04

{

private Map departMap;

public PersonSearch04()

{

super();

departMap = new HashMap();

}

public void add(String departName, String number, String personName)

{

//首先取得部门人员的哈希表

Map personMap = (Map) departMap.get(departName);

//由于当key不存在的时候get方法会返回null,因此我们只要判断get方法

//是否为空就可以知道“键-值对”是否存在,不用调用containsKey方法

//去判断,这样少了一步计算hashCode的过程,能提高一定的效率

if (personMap == null)

{

//如果不存在部门人员哈希表,则新建一个部门人员哈希表,

//并以部门名称为key,部门人员哈希表为value加入部门哈希表

personMap = new HashMap();

departMap.put(departName, personMap);

}

//将人按照numberkey,人名为value加入部门人员哈希表

personMap.put(number, personName);

}

public String get(String departName, String number)

{

//首先取得部门人员的哈希表

Map personMap = (Map) departMap.get(departName);

if (personMap == null)

{

return null;

}

//从部门人员哈希表中按照工号取出编码

return (String) personMap.get(number);

}

}

运行结果:

Ruby

由于部门名称、工号、姓名没有一个能做为主键,所以可以以部门内的人员做为一个Map,并将这个Map做为Value放入另一个Map中,也就是“Map中的Map”,这就解决了数据检索的问题。这个实现算法有点烦琐,并且有点难以理解,并且如果存在嵌套三层甚至更多层的情况就更难理解,这里采用另一种“复合主键”来实现另一种算法。

代码 2.9 是复合主键的实现方式。

代码 2.9 复合主键的实现方式

public class Map05

{

public static void main(String[] args)

{

PersonSearch05 ps = new PersonSearch05();

ps.add("开发一部", "001", "Tom");

ps.add("开发一部", "002", "Jane");

ps.add("开发一部", "003", "Popo");

ps.add("开发二部", "002", "Ruby");

ps.add("开发二部", "003", "Jay");

ps.add("开发二部", "005", "Cheris");

System.out.println(ps.get("开发二部", "002"));

}

}

class PersonSearch05

{

private Map map;

public PersonSearch05()

{

super();

map = new HashMap();

}

public void add(String departName, String number, String personName)

{

map.put(departName + number, personName);

}

public String get(String departName, String number)

{

return (String) map.get(departName + number);

}

}

部门名称不相同,所以部门名称加工号就可以做为键,这样就可以简化操作,算法实现起来也清晰多了。这种“复合主键”在实际应用中非常广泛,需要注意的是一定要保证这个“复合主键”不会重复,否则就会导致数据混乱。案例系统中的SQLTranslator中就是使用数据库类型加SQL语句做为“复合主键”的。

2.3.2 MapHashCode

开发过程中一般都是使用StringInteger等类型做主键的,这些类型都有已经有实现好的哈希函数算法,我们无需为其实现哈希函数算法,但是也会碰到以自定义类型做主键的情况

【实例2-5】以自定义类型做主键

某公司工号的编号方式为“部门编号+出生年月日”,如果重复则在后边再加顺序号。比如编号为“dev002”的部门的出生年月日为“1979108日”的员工的工号为“dev00219791008”,如果已经存在此工号,则工号为“dev002197910081”。系统中有一个工号信息类NumberInfo

代码 2.8 嵌套哈希

class NumberInfo

{

private String departNum;

private Date birthDay;

private int seqNumber;

public NumberInfo(String departNum, Date birthDay, int seqNumber)

{

super();

this.departNum = departNum;

this.birthDay = birthDay;

this.seqNumber = seqNumber;

}

public NumberInfo(String departNum, Date birthDay)

{

super();

this.departNum = departNum;

this.birthDay = birthDay;

this.seqNumber = -1;

}

public Date getBirthDay()

{

return birthDay;

}

public String getDepartNum()

{

return departNum;

}

public int getSeqNumber()

{

return seqNumber;

}

}

接着实现一个检索程序以NumberInfo类型做为主键:

public class Map06

{

public static void main(String[] args)

{

NumberInfo num1 = new NumberInfo("dev001",new Date(1979,1,1));

NumberInfo num2 = new NumberInfo("dev001",new Date(1979,1,1),1);

NumberInfo num3 = new NumberInfo("dev002",new Date(1980,6,1));

Map map = new HashMap();

map.put(num1,"Tom");

map.put(num2,"Peter");

map.put(num3,"Bill");

NumberInfo numToFind = new NumberInfo("dev001",new Date(1979,1,1));

System.out.println(map.get(numToFind));

}

}

运行结果:

null

有点出乎我们的意料,因为numToFind 和我们想要查找的num1 3个域值都相等,为什么根据numToFind 为键却查不到以num1 为键的值呢?

由于NumberInfo直接从Object继承,而且没有重写Object类的方法,所有的行为都是和Object一致的,hashCode方法也继承自Object,而Object中的hashCode方法返回的hashCode对应于当前的地址,也就是说对于不同的对象,即使它们的内容完全相同,用hashCode()返回的值也会不同,这上违背了我们的意图。因为在使用Map时,希望利用相同内容的对象索引得到相同的目标对象,这就需要hashCode()在此时能够返回相同的值。我们期望numToFindnum1是相等的。所以要重写hashCode方法和equals方法,保证对象内容相同的NumberInfo类实例有相同的hashCode。重写如下:

public boolean equals(Object obj)

{

if (!(obj instanceof NumberInfo))

{

return false;

}

NumberInfo info = (NumberInfo) obj;

return info.getDepartNum().equals(getDepartNum())

&& info.getBirthDay().equals(getBirthDay())

&& info.getSeqNumber() == getSeqNumber();

}

public int hashCode()

{

String s = getDepartNum() + getBirthDay() + getSeqNumber();

return s.hashCode();

}

运行结果:

Tom

根据前边所讲的哈希表的原理,不难得出如下结论:

做为键的对象其hashCodeequals方法必须满足下面的条件:如果两个对象相等,那么它们的hashCode必须相等。

如果要使用自定义类做为键的话,一定要覆盖hashCodeequals方法,不能只覆盖一个而不覆盖另一个。计算hashCode最好的方式就是找出此类的“组合主键”(比如上例中的getDepartNum() + getBirthDay() + getSeqNumber()),然后直接返回这个“组合主键”,除非自己有十足的把握,否则不要自己实现哈希算法,因为那样很容易造成频繁的冲突。

由于使用自定义类做为键有很多问题需要处理,而且处理不当很很容易造成性能问题甚至数据混乱,所以在实际开发中尽量避免用自定义类做键。







  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值