哈希表链表Java代码 链表 Java

JAVA创建一个哈希表储存数据并输出,要完整代码

我就不写了,给个提示吧:

成都创新互联于2013年创立,是专业互联网技术服务公司,拥有项目成都做网站、成都网站设计网站策划,项目实施与项目整合能力。我们以让每一个梦想脱颖而出为使命,1280元科尔沁左翼做网站,已为上家服务,为科尔沁左翼各地企业和个人服务,联系电话:18982081108

建一个类,名字就叫员工,它有三个属性,分别是你要的三个数据,名字、工龄、工号。然后,每次put的时候这样:put('1234',员工1);以员工工号为key,类员工为value

Java编程中,HashMap是一种什么样的Map?

java数据结构-HashMap

一直以来似乎都有一个错觉,认为map跟其他的集合类一样继承自Collection,其实不然,Map和Collection在结构层次上是没有任何关系的,通过查看源码可以发现map所有操作都是基于key-value对,而不是单独的元素。

下面以HashMap为例子,深入对Map的实现机制进行了解,在这个过程中,请打开jdk源码。

Hash算法

HashMap使用Hash算法,所以在解剖HashMap之间,需要先简单的了解Hash算法,Hash算法一般也成为散列算法,通过散列算法将任意的值转化成固定的长度输出,该输出就是散列值,这是一种压缩映射,也就是,散列值的空间远远小于输入的值空间。

简单的说,hash算法的意义在于提供了一种快速存取数据的方法,它用一种算法建立键值与真实值之间的对应关系,(每一个真实值只能有一个键值,但是一个键值可以对应多个真实值),这样可以快速在数组等里面存取数据。

下面我们建立一个HashMap,然后往里面放入12对key-value,这个HashMap的默认数组长度为16,我们的key分别存放在该数组的格子中,每个格子下面存放的元素又是以链表的方式存放元素。

public static void main(String[] args) {

Map map = new HashMap();

map.put("What", "chenyz");

map.put("You", "chenyz");

map.put("Don't", "chenyz");

map.put("Know", "chenyz");

map.put("About", "chenyz");

map.put("Geo", "chenyz");

map.put("APIs", "chenyz");

map.put("Can't", "chenyz");

map.put("Hurt", "chenyz");

map.put("you", "chenyz");

map.put("google", "chenyz");

map.put("map", "chenyz");

map.put("hello", "chenyz");

}

当我们新添加一个元素时,首先我们通过Hash算法计算出这个元素的Hash值的hashcode,通过这个hashcode的值,我们就可以计算出这个新元素应该存放在这个hash表的哪个格子里面,如果这个格子中已经存在元素,那么就把新的元素加入到已经存在格子元素的链表中。

运行上面的程序,我们对HashMap源码进行一点修改,打印出每个key对象的hash值

What--hash值:8

You--hash值:3

Don't--hash值:7

Know--hash值:13

About--hash值:11

Geo--hash值:12

APIs--hash值:1

Can't--hash值:7

Hurt--hash值:1

you--hash值:10

google--hash值:3

map--hash值:8

hello--hash值:0

计算出来的Hash值分别代表该key应该存放在Hash表中对应数字的格子中,如果该格子已经有元素存在,那么该key就以链表的方式依次放入格子中

从上表可以看出,Hash表是线性表和链表的综合所得,根据数据结构的定义,可以得出粗劣的结论,Hash算法的存取速度要比数组差一些,但是比起单纯的链表,在查找和存取方面却要好多。

如果要查找一个元素时,同样的方式,通过Hash函数计算出这个元素的Hash值hashcode,然后通过这个hashcode值,直接找到跟这个hash值相对应的线性格子,进如该格子后,对这个格子存放的链表元素逐个进行比较,直到找到对应的hash值。

在简单了解完Hash算法后,我们打开HashMap源码

初始化HashMap

下面我们看看Map map = new HashMap();这段代码究竟做了什么,发生了什么数据结构的变化。

HashMap中几个重要的属性

transient Entry[] table;

用来保存key-value的对象Entry数组,也就是Hash表

transient int size;

返回HashMap的键值对个数

final float loadFactor;

负载因子,用来决定Entry数组是否扩容的因子,HashMap默认是0.75f

int threshold;

重构因子,(capacity * load factor)负载因子与Entry[]数组容积的乘值

public class HashMapK,V

extends AbstractMapK,V

implements MapK,V, Cloneable, Serializable

{

int threshold;

final float loadFactor;

transient Entry[] table;

static final float DEFAULT_LOAD_FACTOR = 0.75f;

static final int DEFAULT_INITIAL_CAPACITY = 16;

public HashMap(int initialCapacity, float loadFactor) {

if (initialCapacity 0)

throw new IllegalArgumentException("Illegal initial capacity: " +

initialCapacity);

if (initialCapacity MAXIMUM_CAPACITY)

initialCapacity = MAXIMUM_CAPACITY;

if (loadFactor = 0 || Float.isNaN(loadFactor))

throw new IllegalArgumentException("Illegal load factor: " +

loadFactor);

// Find a power of 2 = initialCapacity

int capacity = 1;

while (capacity initialCapacity)

capacity = 1;

this.loadFactor = loadFactor;

threshold = (int)(capacity * loadFactor);

table = new Entry[capacity];

init();

}

以public HashMap(int initialCapacity, float loadFactor)构造函数为例,另外两个构造函数实际上也是以同种方式来构建HashMap.

首先是要确定hashMap的初始化的长度,这里使用的策略是循环查出一个大于initialCapacity的2的次方的数,例如initialCapacity的值是10,那么大于10的数是2的4次方,也就是16,capacity的值被赋予了16,那么实际上table数组的长度是16,之所以采用这样的策略来构建Hash表的长度,是因为2的次方运算对于计算机来说是有相当的效率。

loadFactor,被称为负载因子,HashMap的默认负载因子是0.75f

threshold,接下来是重构因子,由负载因子和容量的乘机组成,它表示当HashMap元素被存放了多少个之后,需要对HashMap进行重构。

通过这一系列的计算和定义后,初始化Entry[] table;

put(key,value)

接下来看一对key-value是如何被存放到HashMap中:put(key,value)

public V put(K key, V value) {

if (key == null)

return putForNullKey(value);

int hash = hash(key.hashCode());

int i = indexFor(hash, table.length);

System.out.println(key+"--hash值:"+i);//这就是刚才程序打印出来的key对应hash值

for (EntryK,V e = table[i]; e != null; e = e.next) {

Object k;

if (e.hash == hash ((k = e.key) == key || key.equals(k))) {

V oldValue = e.value;

e.value = value;

e.recordAccess(this);

return oldValue;

}

}

modCount++;

addEntry(hash, key, value, i);

return null;

}

static int hash(int h) {

h ^= (h 20) ^ (h 12);

return h ^ (h 7) ^ (h 4);

}

static int indexFor(int h, int length) {

return h (length-1);

}

这里是整个hash的关键,请打开源码查看一步一步查看。

hash(key.hashCode()) 计算出key的hash码 //对于hash()的算法,这里有一篇分析很透彻的文章HashMap hash方法分析

indexFor(hash, table.length) 通过一个与算法计算出来,该key应在存放在Hash表的哪个格子中。

for (EntryK,V e = table[i]; e != null; e = e.next) 然后再遍历table[i]格中的链表,判断是否已经存在一样的key,如果存在一样的key值,那么就用新的value覆盖旧的value,并把旧的value值返回。

addEntry(hash, key, value, i) 如果经过遍历链表没有发现同样的key,那么进行addEntry函数的操作,增加当前key到hash表中的第i个格子中的链表中

void addEntry(int hash, K key, V value, int bucketIndex) {

EntryK,V e = table[bucketIndex];

table[bucketIndex] = new EntryK,V(hash, key, value, e);

if (size++ = threshold)

resize(2 * table.length);

}

EntryK,V e = table[bucketIndex]; 创建一个Entry对象来存放键值(ps:Entry对象是一个链表对象)

table[bucketIndex] = new EntryK,V(hash, key, value, e); 将Entry对象添加到链表中

if (size++ = threshold) resize(2 * table.length); 最后将size进行自增,判断size值是否大于重构因子,如果大于那么就是用resize进行扩容重构。

void resize(int newCapacity) {

Entry[] oldTable = table;

int oldCapacity = oldTable.length;

if (oldCapacity == MAXIMUM_CAPACITY) {

threshold = Integer.MAX_VALUE;

return;

}

Entry[] newTable = new Entry[newCapacity];

transfer(newTable);

table = newTable;

threshold = (int)(newCapacity * loadFactor);

}

这里为什么是否需要扩容重构,其实是涉及到负载因子的性能问题

loadFactor负载因子

上面说过loadFactor是一个hashMap的决定性属性,HashSet和HashMap的默认负载因子都是0.75,它表示,如果哈希表的容量超过3/4时,将自动成倍的增加哈希表的容量,这个值是权衡了时间和空间的成本,如果负载因子较高,虽然会减少对内存空间的需求,但也会增加查找数据的时间开销,无论是put()和get()都涉及到对数据进行查找的动作,所以负载因子是不适宜设置过高

get(key)

接下来看看get(key)做了什么

public V get(Object key) {

if (key == null)

return getForNullKey();

int hash = hash(key.hashCode());

for (EntryK,V e = table[indexFor(hash, table.length)];

e != null;

e = e.next) {

Object k;

if (e.hash == hash ((k = e.key) == key || key.equals(k)))

return e.value;

}

return null;

}

这些动作似乎是跟put(key,value)相识,通过hash算法获取key的hash码,再通过indexFor定位出该key存在于table的哪一个下表,获取该下标然后对下标中的链表进行遍历比对,如果有符合就直接返回该key的value值。

keySet()

这里还涉及另一个问题,上面说了HashMap是跟set没有任何亲属关系,但map也一样实现了keySet接口,下面谱析一下keySet在hashMap中是如何实现的,这里给出部分代码,请结合源码查看

public K next() {

return nextEntry().getKey();

}

final EntryK,V nextEntry() {

if (modCount != expectedModCount)

throw new ConcurrentModificationException();

EntryK,V e = next;

if (e == null)

throw new NoSuchElementException();

if ((next = e.next) == null) {

Entry[] t = table;

while (index t.length (next = t[index++]) == null)

;

}

current = e;

return e;

}

代码很简单,就是对每个格子里面的链表进行遍历,也正是这个原因,当我们依次将key值put进hashMap中,但在使用map.entrySet().iterator()进行遍历时候却不是put时候的顺序。

扩容

在前面说到put函数的时候,已经提过了扩容的问题

if (size++ = threshold)

resize(2 * table.length);

这里一个是否扩容的判断,当数据达到了threshold所谓的重构因子,而不是HashMap的最大容量,就进行扩容。

void resize(int newCapacity) {

Entry[] oldTable = table;

int oldCapacity = oldTable.length;

if (oldCapacity == MAXIMUM_CAPACITY) {

threshold = Integer.MAX_VALUE;

return;

}

Entry[] newTable = new Entry[newCapacity];

transfer(newTable);

table = newTable;

threshold = (int)(newCapacity * loadFactor);

}

void transfer(Entry[] newTable) {

Entry[] src = table;

int newCapacity = newTable.length;

for (int j = 0; j src.length; j++) {

EntryK,V e = src[j];

if (e != null) {

src[j] = null;

do {

EntryK,V next = e.next;

int i = indexFor(e.hash, newCapacity);

e.next = newTable[i];

newTable[i] = e;

e = next;

} while (e != null);

}

}

}

transfer方法实际上是将所有的元素重新进行一些hash,这是因为容量变化了,每个元素相对应的hash值也会不一样。

使用HashMap

1.不要再高并发中使用HashMap,HashMap是线程不安全,如果被多个线程共享之后,将可能发生不可预知的问题。

2.如果数据大小事固定的,最好在初始化的时候就给HashMap一个合理的容量值,如果使用new HashMap()默认构造函数,重构因子的值是16*0.75=12,当HashMap的容量超过了12后,就会进行一系列的扩容运算,重建一个原来成倍的数组,并且对原来存在的元素进行重新的hash运算,如果你的数据是有成千上万的,那么你的成千上万的数据也要跟这你的扩容不断的hash,这将产生高额的内存和cpu的大量开销。

当然啦,HashMap的函数还有很多,不过都是基于table的链表进行操作,当然也就是hash算法,Map hashMap在平时我们的应用非常多,最重要的是我们要对每句代码中每块数据结构变化心中有数。

上面主要是参考了jdk源码,数据结构和一些相关资料本着好记性不如烂博客的精神记录下来,希望朋友们如果发觉哪里不对请指出来,虚心请教

我想用JAVA实现一个链表的插入~删除等操作~ (要再界面上操作~~~ 就是我按下一个键能进行插入删除的操作的

编辑词条JAVA容器

解释一:

容器(Container)

Spring 提供容器功能,容器可以管理对象的生命周期、对象与对象之间的依赖关系,您可以使用一个配置文件(通常是XML),在上面定义好对象的名称、如何产生(Prototype 方式或Singleton 方式)、哪个对象产生之后必须设定成为某个对象的属性等,在启动容器之后,所有的对象都可以直接取用,不用编写任何一行程序代码来产生对象,或是建立对象与对象之间的依赖关系。

换个更直白点的说明方式:容器是一个Java 所编写的程序,原先必须自行编写程序以管理对象关系,现在容器都会自动帮您作好。

常用容器:WebSphere,WebLogic,Resin,Tomcat

----------------------------------

解释二:

容器类

Java容器类包含List、ArrayList、Vector及map、HashTable、HashMap

ArrayList和HashMap是异步的,Vector和HashTable是同步的,所以Vector和HashTable是线程安全的,而 ArrayList和HashMap并不是线程安全的。因为同步需要花费机器时间,所以Vector和HashTable的执行效率要低于 ArrayList和HashMap。

Collection

├List 接口

│├LinkedList 链表

│├ArrayList 顺序结构动态数组类

│└Vector 向量

│ └Stack 栈

└Set

Map

├Hashtable

├HashMap

└WeakHashMap List接口

List是有序的Collection,使用此接口能够精确的控制每个元素插入的位置。用户能够使用索引(元素在List中的位置,类似于数组下标)来访问List中的元素,这类似于Java的数组。

和下面要提到的Set不同,List允许有相同的元素。

除了具有Collection接口必备的iterator()方法外,List还提供一个listIterator()方法,返回一个 ListIterator接口,和标准的Iterator接口相比,ListIterator多了一些add()之类的方法,允许添加,删除,设定元素, 还能向前或向后遍历。

实现List接口的常用类有LinkedList,ArrayList,Vector和Stack。

ArrayList类

ArrayList实现了可变大小的数组。它允许所有元素,包括null。ArrayList没有同步。

size,isEmpty,get,set方法运行时间为常数。但是add方法开销为分摊的常数,添加n个元素需要O(n)的时间。其他的方法运行时间为线性。

每个ArrayList实例都有一个容量(Capacity),即用于存储元素的数组的大小。这个容量可随着不断添加新元素而自动增加,但是增长算法 并没有定义。当需要插入大量元素时,在插入前可以调用ensureCapacity方法来增加ArrayList的容量以提高插入效率。

和LinkedList一样,ArrayList也是非同步的(unsynchronized)。

Map接口

请注意,Map没有继承Collection接口,Map提供key到value的映射。一个Map中不能包含相同的key,每个key只能映射一个 value。Map接口提供3种集合的视图,Map的内容可以被当作一组key集合,一组value集合,或者一组key-value映射。

HashMap类

HashMap和Hashtable类似,不同之处在于HashMap是非同步的,并且允许null,即null value和null key。,但是将HashMap视为Collection时(values()方法可返回Collection),其迭代子操作时间开销和HashMap 的容量成比例。因此,如果迭代操作的性能相当重要的话,不要将HashMap的初始化容量设得过高,或者load factor过低。

Collection接口

Collection是最基本的集合接口,一个Collection代表一组Object,即Collection的元素(Elements)。一些Collection允许相同的元素而另一些不行。一些能排序而另一些不行。Java SDK不提供直接继承自Collection的类,Java SDK提供的类都是继承自Collection的“子接口”如List和Set。

所有实现Collection接口的类都必须提供两个标准的构造函数:无参数的构造函数用于创建一个空的Collection,有一个Collection参数的构造函数用于创建一个新的Collection,这个新的Collection与传入的Collection有相同的元素。后一个构造函数允许用户复制一个Collection。

如何遍历Collection中的每一个元素?不论Collection的实际类型如何,它都支持一个iterator()的方法,该方法返回一个迭代子,使用该迭代子即可逐一访问Collection中每一个元素。典型的用法如下:

Iterator it = collection.iterator(); // 获得一个迭代子

while(it.hasNext()) {

Object obj = it.next(); // 得到下一个元素

}

由Collection接口派生的两个接口是List和Set。

Hashtable类

Hashtable继承Map接口,实现一个key-value映射的哈希表。任何非空(non-null)的对象都可作为key或者value。

添加数据使用put(key, value),取出数据使用get(key),这两个基本操作的时间开销为常数。

Hashtable通过initial capacity和load factor两个参数调整性能。通常缺省的load factor 0.75较好地实现了时间和空间的均衡。增大load factor可以节省空间但相应的查找时间将增大,这会影响像get和put这样的操作。

使用Hashtable的简单示例如下,将1,2,3放到Hashtable中,他们的key分别是”one”,”two”,”three”:

Hashtable numbers = new Hashtable();

numbers.put(“one”, new Integer(1));

numbers.put(“two”, new Integer(2));

numbers.put(“three”, new Integer(3));

要取出一个数,比如2,用相应的key:

Integer n = (Integer)numbers.get(“two”);

System.out.println(“two = ” + n);

由于作为key的对象将通过计算其散列函数来确定与之对应的value的位置,因此任何作为key的对象都必须实现hashCode和equals方法。hashCode和equals方法继承自根类Object,如果你用自定义的类当作key的话,要相当小心,按照散列函数的定义,如果两个对象相同,即obj1.equals(obj2)=true,则它们的hashCode必须相同,但如果两个对象不同,则它们的hashCode不一定不同,如果两个不同对象的hashCode相同,这种现象称为冲突,冲突会导致操作哈希表的时间开销增大,所以尽量定义好的hashCode()方法,能加快哈希表的操作。

如果相同的对象有不同的hashCode,对哈希表的操作会出现意想不到的结果(期待的get方法返回null),要避免这种问题,只需要牢记一条:要同时复写equals方法和hashCode方法,而不要只写其中一个。

Hashtable是同步的。

HashMap类

HashMap和Hashtable类似,不同之处在于HashMap是非同步的,并且允许null,即null value和null key。,但是将HashMap视为Collection时(values()方法可返回Collection),其迭代子操作时间开销和HashMap的容量成比例。因此,如果迭代操作的性能相当重要的话,不要将HashMap的初始化容量设得过高,或者load factor过低。

WeakHashMap类

WeakHashMap是一种改进的HashMap,它对key实行“弱引用”,如果一个key不再被外部所引用,那么该key可以被GC回收。

总结

如果涉及到堆栈,队列等操作,应该考虑用List,对于需要快速插入,删除元素,应该使用LinkedList,如果需要快速随机访问元素,应该使用ArrayList。

如果程序在单线程环境中,或者访问仅仅在一个线程中进行,考虑非同步的类,其效率较高,如果多个线程可能同时操作一个类,应该使用同步的类。

要特别注意对哈希表的操作,作为key的对象要正确复写equals和hashCode方法。

尽量返回接口而非实际的类型,如返回List而非ArrayList,这样如果以后需要将ArrayList换成LinkedList时,客户端代码不用改变。这就是针对抽象编程。

同步性

Vector是同步的。这个类中的一些方法保证了Vector中的对象是线程安全的。而ArrayList则是异步的,因此ArrayList中的对象并不是线程安全的。因为同步的要求会影响执行的效率,所以如果你不需要线程安全的集合那么使用ArrayList是一个很好的选择,这样可以避免由于同步带来的不必要的性能开销。

数据增长

从内部实现机制来讲ArrayList和Vector都是使用数组(Array)来控制集合中的对象。当你向这两种类型中增加元素的时候,如果元素的数目超出了内部数组目前的长度它们都需要扩展内部数组的长度,Vector缺省情况下自动增长原来一倍的数组长度,ArrayList是原来的50%,所以最后你获得的这个集合所占的空间总是比你实际需要的要大。所以如果你要在集合中保存大量的数据那么使用Vector有一些优势,因为你可以通过设置集合的初始化大小来避免不必要的资源开销。

使用模式

在ArrayList和Vector中,从一个指定的位置(通过索引)查找数据或是在集合的末尾增加、移除一个元素所花费的时间是一样的,这个时间我们用O(1)表示。但是,如果在集合的其他位置增加或移除元素那么花费的时间会呈线形增长:O(n-i),其中n代表集合中元素的个数,i代表元素增加或移除元素的索引位置。为什么会这样呢?以为在进行上述操作的时候集合中第i和第i个元素之后的所有元素都要执行位移的操作。这一切意味着什么呢?

这意味着,你只是查找特定位置的元素或只在集合的末端增加、移除元素,那么使用Vector或ArrayList都可以。如果是其他操作,你最好选择其他的集合操作类。比如,LinkList集合类在增加或移除集合中任何位置的元素所花费的时间都是一样的?O(1),但它在索引一个元素的使用缺比较慢-O(i),其中i是索引的位置.使用ArrayList也很容易,因为你可以简单的使用索引来代替创建iterator对象的操作。LinkList也会为每个插入的元素创建对象,所有你要明白它也会带来额外的开销。

最后,在《Practical Java》一书中Peter Haggar建议使用一个简单的数组(Array)来代替Vector或ArrayList。尤其是对于执行效率要求高的程序更应如此。因为使用数组(Array)避免了同步、额外的方法调用和不必要的重新分配空间的操作。

理解集合类

集合类存放于java.util包中。

集合类存放的都是对象的引用,而非对象本身,出于表达上的便利,我们称集合中的对象就是指集合中对象的引用(reference)。

集合类型主要有3种:set(集)、list(列表)和map(映射)。

(1)集

集(set)是最简单的一种集合,它的对象不按特定方式排序,只是简单的把对象加入集合中,就像往口袋里放东西。

对集中成员的访问和操作是通过集中对象的引用进行的,所以集中不能有重复对象。

集也有多种变体,可以实现排序等功能,如TreeSet,它把对象添加到集中的操作将变为按照某种比较规则将其插入到有序的对象序列中。它实现的是SortedSet接口,也就是加入了对象比较的方法。通过对集中的对象迭代,我们可以得到一个升序的对象集合。

(2)列表

列表的主要特征是其对象以线性方式存储,没有特定顺序,只有一个开头和一个结尾,当然,它与根本没有顺序的集是不同的。

列表在数据结构中分别表现为:数组和向量、链表、堆栈、队列。

关于实现列表的集合类,是我们日常工作中经常用到的,将在后边的笔记详细介绍。

(3)映射

映射与集或列表有明显区别,映射中每个项都是成对的。映射中存储的每个对象都有一个相关的关键字(Key)对象,关键字决定了对象在映射中的存储位置,检索对象时必须提供相应的关键字,就像在字典中查单词一样。关键字应该是唯一的。

关键字本身并不能决定对象的存储位置,它需要对过一种散列(hashing)技术来处理,产生一个被称作散列码(hash code)的整数值,散列码通常用作一个偏置量,该偏置量是相对于分配给映射的内存区域起始位置的,由此确定关键字/对象对的存储位置。理想情况下,散列处理应该产生给定范围内均匀分布的值,而且每个关键字应得到不同的散列码。

集合类简介

java.util中共有13个类可用于管理集合对象,它们支持集、列表或映射等集合,以下是这些类的简单介绍

集:

HashSet: 使用HashMap的一个集的实现。虽然集定义成无序,但必须存在某种方法能相当高效地找到一个对象。使用一个HashMap对象实现集的存储和检索操作是在固定时间内实现的.

TreeSet: 在集中以升序对对象排序的集的实现。这意味着从一个TreeSet对象获得第一个迭代器将按升序提供对象。TreeSet类使用了一个TreeMap.

列表:

Vector: 实现一个类似数组一样的表,自动增加容量来容纳你所需的元素。使用下标存储和检索对象就象在一个标准的数组中一样。你也可以用一个迭代器从一个Vector中检索对象。Vector是唯一的同步容器类??当两个或多个线程同时访问时也是性能良好的。

Stack: 这个类从Vector派生而来,并且增加了方法实现栈??一种后进先出的存储结构。

LinkedList: 实现一个链表。由这个类定义的链表也可以像栈或队列一样被使用。

ArrayList: 实现一个数组,它的规模可变并且能像链表一样被访问。它提供的功能类似Vector类但不同步。

映射:

HashTable: 实现一个映象,所有的键必须非空。为了能高效的工作,定义键的类必须实现hashcode()方法和equal()方法。这个类是前面java实现的一个继承,并且通常能在实现映象的其他类中更好的使用。

HashMap: 实现一个映象,允许存储空对象,而且允许键是空(由于键必须是唯一的,当然只能有一个)。

WeakHashMap: 实现这样一个映象:通常如果一个键对一个对象而言不再被引用,键/对象对将被舍弃。这与HashMap形成对照,映象中的键维持键/对象对的生命周期,尽管使用映象的程序不再有对键的引用,并且因此不能检索对象。

TreeMap: 实现这样一个映象,对象是按键升序排列的。

Set和List都是由公共接口Collection扩展而来,所以它们都可以使用一个类型为Collection的变量来引用。这就意味着任何列表或集构成的集合都可以用这种方式引用,只有映射类除外(但也不是完全排除在外,因为可以从映射获得一个列表。)所以说,把一个列表或集传递给方法的标准途径是使用Collection类型的参数。

另外,站长团上有产品团购,便宜有保证

数据结构与算法-基础(十八)哈希表

上期使用 红黑树 实现映射结构,这样的结构满足 Key 必须具备可比性,元素有顺序地分布 这两个特点。在实际的应用场景中,存在结构中的 元素是不需要有序的,并且 Key 也不具备可比较性 ,哈希表完全满足这样的应用场景。

比如设计一个公司的通讯录,存放所有员工的通讯信息,就可以拿手机号作为 index,员工的名称、职位等作为 value。用哈希表的方式可以将添加、删除和搜索的时间复杂度控制在 O(1)。

这时创建一个数组,手机号作为 index,然后存放 value。这样能将复杂度控制在 O(1),但是这种 空间换时间 的方式也造成了一些其他问题,比如空间复杂度大(需要更多的空间),空间使用率极其低,非常浪费内存空间。

哈希表 就是空间换时间的处理方式,但是做了优化,在空间和时间两个纬度中达到适当的平衡。

哈希表也叫做散列表,整体结构就是一个数组 ,哈希表会将 key 用哈希函数处理之后返回 hash(哈希值),hash 就是哈希表中的 index这样的处理方式就可以满足搜索时间是 O(1),这样的处理方式就可以满足搜索时间是 O(1)。因为哈希表中的 key 可能不具备可比较性,所以要做哈希处理。

在执行哈希函数之后返回的 hash,可能会出现相同的情况 ,这样的情况就是 哈希冲突 。解决哈希冲突常见的方法有这三种:

JDK1.8 解决哈希冲突的方式就是使用链地址法,其中的链表就是通过链表+红黑树的组合来实现 。比如当哈希表中的容量大于等于 64,并且单向链表的节点数大于 8 时,转换为红黑树,不满足这个条件时就使用单向链表。

哈希函数 是生成哈希值的实现方法,哈希函数的实现步骤大致分为两步:

hash_code 是生成哈希值的函数,也可以直接用 JAVA 中的标准函数 hashCode() 。

这里可以用 位运算替换 % 运算,来提高效率。因为 位运算是二进制运算,所以在设计数组的时候,需要将数组的长度设计为 2 的幂次方。

一个良好的哈希函数,可以让生成的哈希值分布更加均匀,减少哈希冲突的次数,最终可以提升哈希表的性能。

Key 的常见类型可能有证书、浮点数、字符串或者自定义对象,不同的类型生成哈希值的方式也会不一样,但是目标是一致的,就是 尽量让每个 Key 的哈希值唯一,尽量让 Key 中的所有信息参与运算 。

比如在 Java 中, Long 的哈希值实现如下代码:

这里的 和 ^ 就是将高 32 bit 和低 32 bit 混合计算出 32 bit 的哈希值。

在计算字符串的哈希值时,可以将字符串拆解成若干个字符,比如 jack,将它拆解成 j、a、c、k(字符的本质就是一个整数,所以 jack 的哈希值可以表示为 j * n3 + a * n2 + c * n1 + k * n0,表达式也可以写成 [(j * n + a) * n + c] * n + k,代码实现如下:

看上面代码时,可以发现,表达式中的 n 使用的是 31 这个数字,那么为什么用 31 呢?

因为 31 不仅符合 22 - 1 , 而且它还是个奇素数(既是技术,又是素数,还是质数),素数和其他数相乘的结果比其他方式更容易产生唯一性,减少哈希冲突。

JDK 中,乘数 n 也是用 31,31 也是经过观测分布结果后的选择,关于 31 的变体可以有以下几种:

31 * i = (25 - 1) * i = i * 25 - i = (i 5) - i

哈希表设计的用Java代码

#include stdio.h

#include string.h

#include stdlib.h

//#include

#define HASH_LEN 50 //哈希表的长度

#define M 47

#define NAME_NO 30 //人名的个数

typedef struct NAME

{

char *py; //名字的拼音

int k; //拼音所对应的整数

}NAME;

NAME NameList[HASH_LEN];

typedef struct hterm //哈希表

{

char *py; //名字的拼音

int k; //拼音所对应的整数

int si; //查找长度

}HASH;

HASH HashList[HASH_LEN];

/*-----------------------姓名(结构体数组)初始化---------------------------------*/

void InitNameList()

{

NameList[0].py="chenghongxiu";

NameList[1].py="yuanhao";

NameList[2].py="yangyang";

NameList[3].py="zhanghen";

NameList[4].py="chenghongxiu";

NameList[5].py="xiaokai";

NameList[6].py="liupeng";

NameList[7].py="shenyonghai";

NameList[8].py="chengdaoquan";

NameList[9].py="ludaoqing";

NameList[10].py="gongyunxiang";

NameList[11].py="sunzhenxing";

NameList[12].py="sunrongfei";

NameList[13].py="sunminglong";

NameList[14].py="zhanghao";

NameList[15].py="tianmiao";

NameList[16].py="yaojianzhong";

NameList[17].py="yaojianqing";

NameList[18].py="yaojianhua";

NameList[19].py="yaohaifeng";

NameList[20].py="chengyanhao";

NameList[21].py="yaoqiufeng";

NameList[22].py="qianpengcheng";

NameList[23].py="yaohaifeng";

NameList[24].py="bianyan";

NameList[25].py="linglei";

NameList[26].py="fuzhonghui";

NameList[27].py="huanhaiyan";

NameList[28].py="liudianqin";

NameList[29].py="wangbinnian";

char *f;

int r,s0;

for (int i=0;iNAME_NO;i++)

{

s0=0;

f=NameList[i].py;

for (r=0;*(f+r) != NULL;r++) //方法:将字符串的各个字符所对应的ASCII码相加,所得的整数做为哈希表的关键字

s0=*(f+r)+s0;

NameList[i].k=s0;

}

}

/*-----------------------建立哈希表---------------------------------*/

void CreateHashList()

{

for (int i=0; iNAME_NO; i ++)

{

HashList[i].py="";

HashList[i].k=0;

HashList[i].si=0;

}

for (i=0; i NAME_NO ; i++)

{

int sum=0;

int adr=(NameList[i].k) % M; //哈希函数

int d=adr;

if(HashList[adr].si==0) //如果不冲突

{

HashList[adr].k=NameList[i].k;

HashList[adr].py=NameList[i].py;

HashList[adr].si=1;

}

else //冲突

{

do{

d=(d+((NameList[i].k))%10+1)%M; //伪散列

sum=sum+1; //查找次数加1

}while (HashList[d].k!=0);

HashList[d].k=NameList[i].k;

HashList[d].py=NameList[i].py;

HashList[d].si=sum+1;

}

}

}

/*-------------------------------------查找------------------------------------*/

void FindList()

{

printf("\n\n请输入姓名的拼音: "); //输入姓名

char name[20]={0};

scanf("%s",name);

int s0=0;

for (int r=0;r20;r++) //求出姓名的拼音所对应的整数(关键字)

s0+=name[r];

int sum=1;

int adr=s0 % M; //使用哈希函数

int d=adr;

if(HashList[adr].k==s0) //分3种情况进行判断

printf("\n姓名:%s 关键字:%d 查找长度为: 1",HashList[d].py,s0);

else if (HashList[adr].k==0)

printf("无该记录!");

else

{

int g=0;

do

{

d=(d+s0%10+1)%M; //伪散列

sum=sum+1;

if (HashList[d].k==0)

{

printf("无记录! ");

g=1;

}

if (HashList[d].k==s0)

{

printf("\n姓名:%s 关键字:%d 查找长度为:%d",HashList[d].py,s0,sum);

g=1;

}

}while(g==0);

}

}

/*--------------------------------显示哈希表----------------------------*/

void Display()

{

printf("\n\n地址\t关键字\t\t搜索长度\tH(key)\t\t拼音 \n"); //显示的格式

for(int i=0; i15; i++)

{

printf("%d ",i);

printf("\t%d ",HashList[i].k);

printf("\t\t%d ",HashList[i].si);

printf("\t\t%d ",(HashList[i].k)%M);

printf("\t %s ",HashList[i].py);

printf("\n");

}

printf("按任意键继续显示...\n"); //由于数据比较多,所以分屏显示(以便在Win9x/DOS下能看到所有的数据)

getchar();

for( i=15; i30; i++)

{

printf("%d ",i);

printf("\t%d ",HashList[i].k);

printf("\t\t%d ",HashList[i].si);

printf("\t\t%d ",(HashList[i].k)%M);

printf("\t %s ",HashList[i].py);

printf("\n");

}

printf("按任意键继续显示...\n");

getchar();

for( i=30; i40; i++)

{

printf("%d ",i);

printf("\t%d ",HashList[i].k);

printf("\t\t%d ",HashList[i].si);

printf("\t\t%d ",(HashList[i].k)%M);

printf("\t %s ",HashList[i].py);

printf("\n");

}

printf("按任意键继续显示...\n");

getchar();

for( i=40; i50; i++)

{

printf("%d ",i);

printf("\t%d ",HashList[i].k);

printf("\t\t%d ",HashList[i].si);

printf("\t\t%d ",(HashList[i].k)%M);

printf("\t %s ",HashList[i].py);

printf("\n");

}

float average=0;

for (i=0;i NAME_NO;i ++)

average+=HashList[i].si;

average/=NAME_NO;

printf("\n\n平均查找长度:ASL(%d)=%f \n\n",NAME_NO,average);

}

/*--------------------------------主函数----------------------------*/

void main()

{

/* ::SetConsoleTitle("哈希表操作"); //Windows API函数,设置控制台窗口的标题

HANDLE hCon = ::GetStdHandle(STD_OUTPUT_HANDLE); //获得标准输出设备的句柄

::SetConsoleTextAttribute(hCon, 10|0); //设置文本颜色

*/

printf("\n------------------------哈希表的建立和查找----------------------");

InitNameList();

CreateHashList ();

while(1)

{

printf("\n\n");

printf(" 1. 显示哈希表\n");

printf(" 2. 查找\n");

printf(" 3. 退出\n");

err:

char ch1=getchar();

if (ch1='1')

Display();

else if (ch1='2')

FindList();

else if (ch1='3')

return;

else

{

printf("\n请输入正确的选择!");

goto err;

}

}

}

hashmap 中 hash 函数怎么是是实现的?还有哪些 hash 的实现方式

HashMap是对数据结构中哈希表(Hash

Table)的实现,Hash表又叫散列表。Hash表是根据关键码Key来访问其对应的值Value的数据结构,它通过一个映射函数把关键码映射到表中一个位置来访问该位置的值,从而加快查找的速度。这个映射函数叫做Hash函数,存放记录的数组叫做Hash表。

在Java中,HashMap的内部实现结合了链表和数组的优势,链接节点的数据结构是Entry

,每个Entry对象的内部又含有指向下一个Entry类型对象的引用,如以下代码所示:

static

class

Entry

implements

Map.Entry

{

final

K

key;

V

value;

Entry

next;

//Entry类型内部有一个自己类型的引用,指向下一个Entry

final

int

hash;

...

}

在HashMap的构造函数中可以看到,Entry表被申明为了数组,如以下代码所示:

public

HashMap()

{

this.loadFactor

=

DEFAULT_LOAD_FACTOR;

threshold

=

(int)(DEFAULT_INITIAL_CAPACITY

*

DEFAULT_LOAD_FACTOR);

table

=

new

Entry[DEFAULT_INITIAL_CAPACITY];

init();

}

在以上构造函数中,默认的DEFAULT_INITIAL_CAPACITY值为16,DEFAULT_LOAD_FACTOR的值为0.75。

当put一个元素到HashMap中去时,其内部实现如下:

public

V

put(K

key,

V

value)

{

if

(key

==

null)

return

putForNullKey(value);

int

hash

=

hash(key.hashCode());

int

i

=

indexFor(hash,

table.length);

...

}


名称栏目:哈希表链表Java代码 链表 Java
本文来源:http://azwzsj.com/article/hhjsoi.html