哈希函数与哈希表的实现

289 阅读5分钟

哈希函数

哈希函数(y = f(x))的性质:

  1. 输入域无穷尽,如字符串可以无限长,输出域有穷尽
  2. 相同的输入,相同的输出,没有随机的成分
  3. 不同的输入也可能有相同的输出,称作哈希碰撞,但是概率极低,可以等于雷劈的概率
  4. 哈希函数的输出结果具有均匀性,离散型,比如即使输入有规律,哈希函数也可以使输出是离散的,不规律的,且输出的结果均匀的填充在输出域上。如果你将输出域的各个值%m,那么%后结果也会在0~m-1上均匀分布

相关应用-存数:

假设你只有1G的内存,要判断出0~2^32 -1个数,大约40亿个,其中出现最多次数的数是哪一个

想法一

用哈希表(HashMap),记录下各个数的值和对应出现的次数

  • 分析,每一个key(int)和对应的value(int)至少是4 * 2 = 8字节(没有算索引等空间),那么在最差情况需要3.2G的空间,所以不符合要求。
  • 1千兆字节(G)=1073741824 字节(B)---约等于10亿字节
想法二
  • 利用哈希函数可以得到40亿个哈希值,然后对每一个哈希值%100,那么由于哈希函数的性质,在0~99号文件中含有不同数的数量是差不多一样多的,这样每一个小文件的内存是0.32G,每一个文件取出来最大的那个值后释放,最后比较在这100个中谁最大就OK。
  • 流程如下:
    1. 循环每一个数,依次带入哈希函数得到哈希值
    2. 先将值%100后值为0的数存入哈希表中,key是这个数字,value是出现的次数
    3. 循环完成,得到0号文件的最大次数的值,释放0号文件的空间
    4. 回到第一步重新开始,执行到第2步的时候,这时将%100后的值为 0 + 1 = 1的存入哈希表中。。。(依次进行下去)

哈希表的实现

image.png

  • 哈希表的增删改查都是O(1),但是不小,理论上是单次是O(logN):因为我们以最差每个链上只可以串2个来看,串完两个就要扩容成两个链,依旧一个链只能串两个,这是所有的N都需要重新计算哈希值,所以一共需要O(N* logN)时间复杂度,其中N是数的个数,logN是扩容的次数。而对于所有的N来说,单个操作时间复杂度是O(logN),因为实际中一个链会有更长的串,所以是O(log以k长为底N)接近O(1),对于java还可以在不用的在线时间的时候自己进行扩容,更加节省时间。
  • 存入链的过程是,一个数通过哈希函数计算哈希值,哈希值%(初始数组的长度)即知道放的位置。同样找一个值是否存在的时候也是一样的操作。所以很类似与寻址的过程,时间复杂度主要取决于链的长度和扩容次数。
  • 首先要简单介绍一下HashMap的内部存储。我们知道,Map是用来存储key-value类型数据的,一个对在Map的接口定义中被定义为Entry,HashMap内部实现了Entry接口。HashMap内部维护一个Entry数组。当put一个新元素的时候,根据key的hash值计算出对应的数组下标。数组的每个元素是一个链表的头指针,用来存储具有相同下标的Entry。

image.png

哈希表的相关操作

package class01;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;

public class Code01_HashMap {

	public static void main(String[] args) {
		HashMap<String, String> map = new HashMap<>();
		map.put("zuo", "31");

		System.out.println(map.containsKey("zuo"));//true
		System.out.println(map.containsKey("chengyun"));//false
		System.out.println("=========================");

		System.out.println(map.get("zuo"));//31
		System.out.println(map.get("chengyun"));//null
		System.out.println("=========================");

		System.out.println(map.isEmpty());//false
		System.out.println(map.size());//1
		System.out.println("=========================");

		System.out.println(map.remove("zuo"));//这个挺有意思,返回的是对应的值31
		System.out.println(map.containsKey("zuo"));//false
		System.out.println(map.get("zuo"));//null
		System.out.println(map.isEmpty());//true
		System.out.println(map.size());//0
		System.out.println("=========================");

		map.put("zuo", "31");
		System.out.println(map.get("zuo"));//31
		map.put("zuo", "32");
		System.out.println(map.get("zuo"));//32
		System.out.println("=========================");

		map.put("zuo", "31");
		map.put("cheng", "32");
		map.put("yun", "33");
		map.put("yun2", "34");
		map.put("yun3", "35");
		for (String key : map.keySet()) {//得到key的集合
			System.out.println(key);//yun zuo yun2 yun3 cheng无顺序
		}
		System.out.println("=========================");

		for (String values : map.values()) {//得到值的集合
			System.out.println(values);//33 31 34 35 32和上面对应,原因和它的原理有关,
			// 看似没顺序,其实是通过哈希函数算出了哈希值存储的
		}
		System.out.println("=========================");

		map.clear();
		map.put("zuo", "31");
		map.put("cheng", "32");
		map.put("yun", "33");
		map.put("yun2", "34");
		map.put("yun3", "35");
		//Entry是Map中用来保存一个键值对的,而Map实际上就是多个Entry的集合。
		// Entry<key,value>和Map<key,value>一样的理解方式就OK了,只能在哈希表中用
		for (Entry<String, String> entry : map.entrySet()) {//得到entry数组的集合
			String key = entry.getKey();
			String value = entry.getValue();
			System.out.println(key + "," + value);
			// yun,33 zuo,31 yun2,34 yun3,35 cheng,32无序

		}
		System.out.println("=========================");
		map.clear();
		map.put("A", "1");
		map.put("B", "2");
		map.put("C", "1");
		map.put("D", "3");
		map.put("E", "1");
		map.put("F", "4");
		map.put("G", "2");
		//Entry是Map中用来保存一个键值对的,而Map实际上就是多个Entry的集合。
		// Entry<key,value>和Map<key,value>一样的理解方式就OK了,只能在哈希表中用
		for (Entry<String, String> entry : map.entrySet()) {//得到entry数组的集合
			String key = entry.getKey();
			String value = entry.getValue();
			System.out.println(key + "," + value);
			// A,1 B,2 C,1 D,3 E,1 F,4 G,2 还挺有意思是顺序出来的,但是对比上一段代码,觉得这是巧合,只是和哈希函数有关系

		}
		System.out.println("=========================");
		//不能在使用迭代器的同时移除数据
		// you can not remove item in map when you use the iterator of map
//		 for(Entry<String,String> entry : map.entrySet()){
//			 if(!entry.getValue().equals("1")){
//				 map.remove(entry.getKey());
//			 }
//		 }
         //解决办法
		// if you want to remove items, collect them first, then remove them by
		// this way.
		List<String> removeKeys = new ArrayList<String>();
		for (Entry<String, String> entry : map.entrySet()) {
			if (!entry.getValue().equals("1")) {
				removeKeys.add(entry.getKey());
			}
		}
		for (String removeKey : removeKeys) {
			map.remove(removeKey);
		}
		for (Entry<String, String> entry : map.entrySet()) {
			String key = entry.getKey();
			String value = entry.getValue();
			System.out.println(key + "," + value);
			//A,1 C,1 E,1
		}
		System.out.println("=========================");

	}

}

相关题目-实现结构

【题目】 设计一种结构,在该结构中有如下三个功能: insert(key):将某个key加入到该结构,做到不重复加入 delete(key):将原本在结构中的某个key移除 getRandom(): 等概率随机返回结构中的任何一个key。

【要求】 Insert、delete和getRandom方法的时间复杂度都是O(1)

public class test1{
        public static class Pool<K>{
            //之所以要建立两个Map是因为,删除操作给的参数是对应的要删除的数,不是索引
            private HashMap<K, Integer> keyIndexMap;
            private HashMap<Integer, K> indexKeyMap;
            private int size;

            public Pool(){
                this.keyIndexMap = new HashMap<K, Integer>();
                this.indexKeyMap = new HashMap<Integer, K>();
                this.size = 0;
            }

            public void insert(K key){
                if (!this.keyIndexMap.containsKey(key)){
                    this.keyIndexMap.put(key, this.size);
                    this.indexKeyMap.put(this.size++, key);
                }
            }

            public void delete(K key){
                if (this.keyIndexMap.containsKey(key));
                //得到要删除键的索引数
                int deleteIndex = this.keyIndexMap.get(key);
                int lastIndex = --this.size;
                //得到最后一个位置的索引数对应的键
                K lastKey = this.indexKeyMap.get(lastIndex);
                //修改操作,使最后一个位置的键和新的索引数建立对应
                this.keyIndexMap.put(lastKey, deleteIndex);
                this.indexKeyMap.put(deleteIndex, lastKey);
                //移除删除的键
                this.keyIndexMap.remove(key);
                //在另一个Map中移除最后的索引键值对
                this.indexKeyMap.remove(lastIndex);
            }

            public K getRandom(){
                if (this.size == 0){
                    return null;
                }
                int randomIndex = (int) (Math.random() * this.size);
                return this.indexKeyMap.get(randomIndex);
            }

        }

        public static void main(String[] args){
            Pool<String> pool = new Pool<>();
            pool.insert("zuo");
            pool.insert("cheng");
            pool.insert("yun");
            System.out.println(pool.getRandom());
            System.out.println(pool.getRandom());
            System.out.println(pool.getRandom());
            System.out.println(pool.getRandom());
        }
}