查找系列合集-散列表
一、散列表
【问题】之前我们的用红黑树实现了O(logN)的查找算法,那么理论上有没有O(1)的查找算法呢?
【分析】除非我们能够单凭键值key就能确定该元素在集合中的位置,直接将其取出
【解决方法】不妨采取映射的方法,将键值k1 k2 ......kn映射到 0 1 2 3 ......n-1,也就是数组下标的位置,那么如何完成这种映射呢?
【哈希】
- 哈希方法也就是对给定的某一个值,采用一定的换算方法,将其映射为一个整数
- Java内部为每一个内置数据类型都设计了hash函数,能够返回一个32bit整数
二、 散列函数
1. 散列函数即哈希函数
2. 可以自己为某种数据类型设计一种哈希函数代替内置的hash函数
- 如一种对象包含3个域 x1 x2 x3 ,则可以设计其hash函数为 int hash = (x1 * r + x2) * r + x3
- 为了不使数据溢出,可以把常数r设置得比较小,并在每一步运算时加上对M取模
3. 基于内置hash函数定义
- 可以对任意key先返回其系统定义的hash值,然后再对M取模,在基础上将其范围限定到0 ~ M-1,防止数组越界
4. 碰撞
- 当两个不同的key所得出的hash值相同,则认为发生了碰撞,因为他们都想要占据这个位置
- 当发生碰撞时,后来者只能再寻求其他位置,解决方法有线性探测(即往后继续查找空位,见缝插针),二次探测再散列(加减一个值的平方后再看看有没有空位)
- 具体解决碰撞的方案
三、基于拉链法的散列表
1. 核心思想是避开了解决hash碰撞的需要,对于每一个hash值(0~M-1),其都拉出了一个链表,把所有hash值都等于它的键存在了这个链表之中
2. 当要寻找一个key时,只要先求出hash值,然后在该hash值所对应的链表里顺序查找
【实现】
![](/assets/blank.gif)
![](/assets/blank.gif)
package search;public class SeparateChainingHashST<Key , Value> {private int N; //键值对总数private int M;//散列表的大小private SequentialSearchST<Key, Value>[] st;public SeparateChainingHashST() {// TODO Auto-generated constructor stubthis(997);}//初始化,为每一个hash值都创建一个链表 M个public SeparateChainingHashST(int M) {// TODO Auto-generated constructor stubthis.M = M;st = (SequentialSearchST<Key, Value>[]) new SequentialSearchST[M];for(int i=0; i<M; i++) {st[i] = new SequentialSearchST();}}private int hash(Key key) {return (key.hashCode() & 0x7fffffff) % M;}public Value get(Key key) {return (Value)st[hash(key)].get(key);}public void put(Key key, Value val) {st[hash(key)].put(key, val);}public static void main(String[] args) {// TODO Auto-generated method stub }}
基于拉链法的散列表
![](/assets/blank.gif)
![](/assets/blank.gif)
package search;//链表,内部类 public class SequentialSearchST<Key, Value>{public SequentialSearchST() {super();// TODO Auto-generated constructor stub }private Node head;//链表头指针//结点类 内部类private class Node{Key key;Value val;Node next;//下一个结点指针public Node() {}public Node(Key key, Value val, Node next) {this.key = key;this.val = val;this.next = next;}}//顺序查找public Value get(Key key) {Node x = head;while(x != null) {if(x.key.equals(key)) {return x.val;}x = x.next;}return null;}//尾插法public void put(Key key, Value val) {//先考虑key存在的情况Node x = head;while(true) {if(x.key.equals(key)) {x.val = val;return;}//如果下一个结点不为空,则继续向下迭代。为空,则直接把新节点插入即可if(x.next != null)x = x.next;else {x.next = new Node(key, val, null);}}}//头插法public void putt(Key key, Value val) {Node x = head;//先搜索一遍防止键 存在while(x != null) {if(x.key.equals(key)) {x.val = val;return;}x = x.next;}//如果 不存在则将该新节点指向旧的头指针, 新头指针指向 它head = new Node(key, val, head);}}
辅助对象链表
四、基于线性探测法的散列表
1.主要思想: 当前位置发生了碰撞便找下一个位置,以此类推,直到找到空位置
2. ADT
public class LinearProbingHashST<Key , Value> {private int N = 0;//当前使用量private int M = 16;//默认容量大小为M//并行数组private Key[] keys;private Value[] vals; }
3. hash函数
private int hash(Key key) {return (key.hashCode() & 0x7fffffff) % M;}
4. 插入操作
//插入操作 冲突 循环后移再探测public void put(Key key, Value val) {//保证使用量不超过额定容量的一半if(N > M/2) {resize(2*M);}int index = hash(key);for(; keys[index] != null; index = (index+1) % M) {if(keys[index].equals(key)) {vals[index] = val;return ;//如果该键已经存在则改值后 return }}keys[index] = key;vals[index] = val;N++; }
5. 扩容操作
- 当散列表大部分被填充之后,所造成的碰撞概率会大大增加,而且所带来的查找 删除操作成本也会很高
- 为什么不直接拷贝数组而选择重新put呢?因为在新容量下的散列表这些key的hash值已经发生了变化。
- 每当使用容量N到达总容量M的一半时,扩容一倍,这样即使对大规模数据的插入也不会很多次调用resize
private void resize(int cap) {LinearProbingHashST<Key , Value> t;t = new LinearProbingHashST<Key , Value>(cap);for(int i=0; i<M; i++) {if(keys[i] != null)t.put(keys[i], vals[i]);}this.keys = t.keys;this.vals = t.vals;this.M = t.M;}
6. 查找操作
- 引入“长键”的概念,长键简单来说就是连续的键,或者叫键簇,散列表可以看成是多个长键组成的,每个长键之间间隔若干个空值(首尾长键算一个)
- 由于采用的是线性探测法,可以有如下定理:hash值相等的键必然处于同一个长键簇之中
public Value get(Key key) {int index = hash(key);//首先算出hash值得到预期位置,如果正好这个位置的键等于key,那么命中,直接返回对应的val//如果发现key不相等,则说明发生冲突,由插入算法可知,要查找的键必然是和冲突键位于同一组长键之中//继续查找直到遇到空为止 遇到空说明该键不存在for(int i = index; keys[i] != null; i = (i+1) % M) {if(keys[i].equals(key)) {return vals[i];}}return null;}
7. 删除操作
- 根据插入查询定理 目标键与hash值所在的键处于同一个长键之中,中间不能有缝隙
- 如果因为删除一个键后导致中间断裂,会导致查询失效
- 示例 G H KLOMN U 其中 M、L hash值相等 K、N hash值不等 长键簇是KLOMN,删除O后会导致M值无法被查询到
- 所以删除O(置空)后,如果O右边的键和O左边的键有hash值相等的情况时,右边的键必然查询不到
//只能将该【长键中】所删除的键置为空然后将其右边的所有键重新插入public void delete(Key key) {int index = hash(key);int pos = index;boolean flag = false;for(; keys[index] != null; index = (index + 1) % M) {if(keys[index].equals(key)) {//找到这个键了 把位置记录下来一会用,index继续增加得到边界flag = true;pos = index;keys[index] = null;vals[index] = null;N--;}}//没找到需要删除的键 就算了if(!flag)return ;//从pos+1的位置 到 index-1的位置所有键需要重新弄插入 注意位置序号需要取模pos = pos + 1;pos %= M;while(pos != index) {Key k = keys[pos];Value v = vals[pos];keys[pos] = null;vals[pos] = null;put(k , v);//重新插入pos = (pos + 1) % M;//后移pos位置 }}
![](/assets/blank.gif)
![](/assets/blank.gif)
package search;import java.util.Random;public class LinearProbingHashST<Key , Value> {private int N = 0;//当前使用量private int M = 16;//默认容量大小为M//并行数组private Key[] keys;private Value[] vals;public LinearProbingHashST() {// TODO Auto-generated constructor stub alloc();}public LinearProbingHashST(int cap) {M = cap;alloc();}private void alloc() {keys = (Key[])new Object[M];vals = (Value[]) new Object[M];}private int hash(Key key) {return (key.hashCode() & 0x7fffffff) % M;}//插入 查询 定理: 目标键与hash值所在的键 位于同一条长键之中,中间不可能有空隙public void show() {for(int i=0; i<M; i++) {if(keys[i] != null)System.out.println(keys[i].toString() + ":" +vals[i].toString());}}//插入操作 冲突 循环后移再探测public void put(Key key, Value val) {//保证使用量不超过额定容量的一半//System.out.println("put :" + N + " " + M/2);if(N > M/2) {resize(2*M); // System.out.println("*************************resize*************************" + M); // show(); // System.out.println("**************************************************"); }int index = hash(key);for(; keys[index] != null; index = (index+1) % M) {if(keys[index].equals(key)) {vals[index] = val;return ;//如果该键已经存在则改值后 return }}keys[index] = key;vals[index] = val;N++;}//这种赋值方式不行 思考一下为什么 从hash值的模出发private void resizes(int cap) {// TODO Auto-generated method stubKey[] ks = (Key[]) new Object[2*cap];Value[] vs = (Value[]) new Object[2*cap];for(int i=0; i<M; i++) {ks[i] = keys[i] ;vs[i] = vals[i] ;}keys = ks;vals = vs;M = cap;}private void resize(int cap) {LinearProbingHashST<Key , Value> t;t = new LinearProbingHashST<Key , Value>(cap);for(int i=0; i<M; i++) {if(keys[i] != null)t.put(keys[i], vals[i]);}this.keys = t.keys;this.vals = t.vals;this.M = t.M;}public Value get(Key key) {int index = hash(key);//首先算出hash值得到预期位置,如果正好这个位置的键等于key,那么命中,直接返回对应的val//如果发现key不相等,则说明发生冲突,由插入算法可知,要查找的键必然是和冲突键位于同一组长 键之中//继续查找直到遇到空为止 遇到空说明该键不存在for(int i = index; keys[i] != null; i = (i+1) % M) {if(keys[i].equals(key)) {return vals[i];}}return null;}//根据插入查询定理 目标键与hash值所在的键处于同一个长键之中,中间不能有缝隙//如果因为删除一个键后导致中间断裂,会导致查询失效//G H KLOMN U ML hash值相等 KN hash值不等//只能将该【长键中】所删除的键置为空然后将其右边的所有键重新插入public void delete(Key key) {int index = hash(key);int pos = index;boolean flag = false;for(; keys[index] != null; index = (index + 1) % M) {if(keys[index].equals(key)) {//找到这个键了 把位置记录下来一会用,index继续增加得到边界flag = true;pos = index;keys[index] = null;vals[index] = null;N--;}}//没找到需要删除的键 就算了if(!flag)return ;//从pos+1的位置 到 index-1的位置所有键需要重新弄插入 注意位置序号需要取模pos = pos + 1;pos %= M;while(pos != index) {Key k = keys[pos];Value v = vals[pos];keys[pos] = null;vals[pos] = null;put(k , v);//重新插入pos = (pos + 1) % M;//后移pos位置 }}public static void main(String[] args) {// TODO Auto-generated method stubLinearProbingHashST<Integer , Integer> hashST;hashST = new LinearProbingHashST<Integer, Integer>();Random r = new Random();for(int i=0; i<1500000; i++) {Integer key = new Integer(r.nextInt(10000000));Integer value = new Integer(r.nextInt(10000000));//System.out.println("key = " + key + " value = " + value); hashST.put(key, value);//System.out.println("N = " + hashST.N + " M = " + hashST.M); }//System.out.println("put 操作完毕");hashST.put(88888, 88888);hashST.put(88888, 99999);//System.out.println(hashST.get(88888));System.out.println("删除前*****************");//hashST.show();//hashST.delete(88888); System.out.println("删除后*****************");//hashST.show();System.out.println(hashST.get(88888));System.out.println("OK");}}
线性探测再散列
五、 关于散列表的均匀散列假设
- 我们使用的散列函数能够均匀并且独立地将所有的键散布于0到M-1之间
- 在一张大小为M且含有N = α M个键的基于线性探测的散列表中,基于上述假设,查找所需要的探测次数为
- 查找命中时:½( 1+1/(1-α) )
- 查找未命中时: ½( 1+1/(1-α)2)
- 因此一般要使α小一些,通常使它一直小于二分之一
转载于:https://www.cnblogs.com/czsharecode/p/10605724.html
查找系列合集-散列表相关推荐
- 查找系列合集-二分查找
一.二分查找 [引入]一个综艺节目是给定一件价格为未知整数的商品,默认最高价格为1个亿, 你每次猜其价格时主持人会告诉你该价格比实际价格高还是低或者相等,现在让你在尽可能少的次数下猜出其价格,请问你的 ...
- Oracle 每日一题系列合集
作者 | JiekeXu 来源 | JiekeXu之路(ID: JiekeXu_IT) 转载请联系授权 | (微信ID:xxq1426321293) 大家好,我是 JiekeXu,很高兴又和大家见面了 ...
- 【Java】 大话数据结构(13) 查找算法(4) (散列表(哈希表))
本文根据<大话数据结构>一书,实现了Java版的一个简单的散列表(哈希表). 基本概念 对关键字key,将其值存放在f(key)的存储位置上.由此,在查找时不需比较,只需计算出f(key) ...
- 易错丨Oracle 每日一题系列合集
在墨天轮平台有个[数据库每日一题]栏目:www.modb.pro/test(复制到浏览器或者点击"阅读原文"可直达,每日一题),均是由数据库行业的专家亲自出题,墨天轮审核后发布的一 ...
- 009.查找手机电话簿【散列表】
1. 散列表 顺序存储的结构类型需要一个一个地按顺序访问元素,当总量很大且我们所要访问的元素比较靠后时,顺序存储的结构类型性能就比较低.而散列表是一种空间换时间的存储结构,也是提升效率的一种比较常用的 ...
- 【《Real-Time Rendering 3rd》提炼总结】完结篇:系列合集电子书PDF下载实时渲染知识网络图谱新系列预告
本文由@浅墨_毛星云 出品,首发于知乎专栏,转载请注明出处. 文章链接: https://zhuanlan.zhihu.com/p/34207965 按照专栏之前的计划,[<Real-Tim ...
- Linux下的查找命令合集(which/whereis/locate/find)
Linux 下的查找命令有很多,常用的有which.whereis.locate.find.那么这4个命令之间各自有什么特点,又有什么区别,什么时候该用哪个才最合适呢?方便我们在开发和学习中能更加有效 ...
- Linux搜索查找命令合集
目录 find locate grep 和 | find 作用: 将指定目录向下递归地遍历其各个子目录,将满足条件的文件或目录显示在终端. 基本语法: find [搜索范围] [参数] 常用参数: - ...
- 使用Intel编译器系列合集
好的帖子: http://topic.csdn.net/u/20080327/16/071b45df-3795-4bf1-9c4d-da4eb5aaa739.html 参考手册: http://sof ...
最新文章
- nodejs pm2使用
- 程序中保留一个Dump
- join orcl的left_Oracle关联查询关于left/right join的那点事
- python教程timeit模块的使用教程
- winfrom水晶报表的创建
- Java面向对象编程篇1——类与对象
- CentOS7.1下targetcli的使用
- java8 metaspacesize_java-8 – Java8 MetaspaceSize标志不起作用
- 飞行器比赛制作过程中的资料搜集(2018.5~6月)
- ESP8266与ESP8285开发时有什么区别
- 由于没有安装音量控制程序,WINDOWS无法在任务栏上显示音量控制,怎么解决?
- 格式化报错a bad sector is being found while format this partition
- K-means算法实现及分析
- 平安人寿优+计划广纳英才,平安代理人实现职涯发展
- 如何给服务器IIS配置文件夹配置everyone权限
- 什么是微信SCRM客服系统
- java entropy_java面试
- 手机信号强度大小的意义
- SMP、NUMA、MMP的简介
- oracle中文问号乱码,Oracle 中文记录 及 乱码 判断 说明 .(转)