哈希算法

来源:互联网 发布:网络吃鸡是什么意思啊 编辑:程序博客网 时间:2024/06/08 17:37

1 哈希表的原理和用途

哈希表是基于数组来实现,它提供了快速的插入操作和查找操作。在初始阶段插入的数据量较少时,插入、删除和查找只需接近常量时间,即时间复杂度为0(1)。如果不需要有序遍历数据,并且可以提前预测数据量的大小,那么哈希表在的性价比是最佳的。缺点是基于数组,后续拓展比较困难,某些哈希表快填满时候,性能下降的非常严重。


2 哈希化

2.1 常用的哈希化方法

将关键字通过哈希函数转换为数组下标,这个过程称为哈希化。




2.2 三种方法的实现代码

public class HashTable {/*** 哈希表的底层是数组*/private Employee[] arr;/*** 默认的构造方法*/public HashTable() {arr = new Employee[100];}/*** * @param maxsize*/public HashTable(int maxsize) {arr = new Employee[maxsize];}/*** 插入数据* @param emp*/public void insert(Employee emp){arr[hashCode(emp.getKey())] = emp;}/*** 查找数据* @param key* @return*/public Employee find(String key){return arr[hashCode(key)];}/*** 获取哈希值* 方法1:数字相加* SCII码从0到255,可以容纳字母和标点符号等字符,其中0是48,依次递增,9是57;* @param key* @return*//*public int hashCode(String key){int hashVal = 0;for(int i = key.length() - 1; i >=0; i--){int letter = key.charAt(i) - 48;hashVal += letter;}return hashVal;}*//*** 获取哈希值* 方法2:幂的连乘* @param key* @return*//*public int hashCode(String key){int hashVal = 0;int pow10 = 1;for(int i = key.length() - 1; i >=0; i--){int letter = key.charAt(i) - 48;hashVal += letter * pow10;pow10 *= 10;}return hashVal;}*//*** 获取哈希值* 方法3:去余操作* @param key* @return*/public int hashCode(String key){//考虑数组太大时溢出的情况/*BigInteger hashVal = new BigInteger("0") ;BigInteger pow10 = new BigInteger("1") ;for(int i = key.length() - 1; i >=0; i--){int letter = key.charAt(i) - 48;BigInteger letterB = new BigInteger(String.valueOf(letter));hashVal = hashVal.add(letterB.multiply(pow10));pow10 = pow10.multiply(new BigInteger(String.valueOf(10)));}//取余数return hashVal.mod(new BigInteger(String.valueOf(arr.length))).intValue();*/int hashVal = 0;int pow10 = 1;for(int i = key.length() - 1; i >=0; i--){int letter = key.charAt(i) - 48;hashVal += letter * pow10;pow10 *= 10;}//取余return hashVal % arr.length;}}


3 开发放地址法

3.1原理

当冲突发生时,通过查找数组的一个空位,并将数据填入,而不再用哈希函数得到的数组下标。根据查找空位时使用的方法可细分为线性探测法,二次探测法和再哈希法三种。

3.2线性探测法

3.2.1原理

如果要插入的1000位置已满,则查找1001,若还是满的,则查找1002,依次类推直到找到空位为止。缺点:当增加越来越多的数据项时,填充序列(一串连续的已填充单元)变的越来越长,性能下降,即发生原始聚集。


3.2.2完整代码

public class Student {/*** 学号*/private String stuNo;/*** 有参构造函数* @param stuNo*/public Student(String stuNo) {this.stuNo = stuNo;}/*** 获取关键字* @return*/public String getKey(){return stuNo;}}package openaddressing;/** * @author JayLai * @date 2017年8月30 * @descriptions:开放地址法,线性探测 * @version 1.0 */public class HashTable {/*** 哈希表的底层是数组*/Student[] hashArray;/*** 初始化数组的大小*/int arraySize = 101;/*** 设定初始化哈希表的大小 无参构造函数*/public HashTable() {hashArray = new Student[arraySize];}/*** 指定哈希表的大小 有参构造函数*/public HashTable(int arraySize) {this.arraySize = arraySize;hashArray = new Student[arraySize];}/*** 获取哈希值* * @param key* @return*/public int hashCode(String key) {int hashVal = 0;int power10 = 1;for (int i = key.length() - 1; i >= 0; i--) {hashVal = hashVal + (key.charAt(i) - 48) * power10; // 10的幂的连乘power10 = power10 * 10;}return hashVal % hashArray.length; // 取模}/*** 添加数据* * @param student*/public void add(Student student) {int hashVal = hashCode(student.getKey()); // student若为空,则报空指针异常for (int i = 0; i < arraySize; i++) {if (hashArray[hashVal] == null) {hashArray[hashVal] = student;return;}hashVal++; // 寻找下一个空白位置hashVal %= hashArray.length; // 取摸,避免数组越界}}/*** 删除数据并返回* * @param student* @return*/public Student delete(Student student) {int hashVal = hashCode(student.getKey()); // student若为空,则报空指针异常for (int i = 0; i < arraySize; i++) {if (hashArray[hashVal] == student) {hashArray[hashVal] = null;return student;}hashVal++; // 寻找下一个空白位置hashVal %= hashArray.length; // 取摸,避免数组越界}return null; // 不存在该信息}/*** 查找并返回* * @param student* @return*/public Student find(Student student) {int hashVal = hashCode(student.getKey()); // student若为空,则报空指针异常for (int i = 0; i < arraySize; i++) {if (hashArray[hashVal] == student) {return hashArray[hashVal];}hashVal++; // 寻找下一个空白位置hashVal %= hashArray.length; // 取摸,避免数组越界}return null; // 不存在该信息}/*** 显示数据*/public void display() {StringBuilder str = new StringBuilder(); // 字符串拼接str.append("[");for (int i = 0; i < arraySize; i++) {if (hashArray[i] != null) {str.append(hashArray[i].getKey() + ", ");}}str.replace(str.length() - 2, str.length() - 1, "]"); // 不包含起始值System.out.println(str.toString());}/*** 测试* * @param args*/public static void main(String[] args) {HashTable hashTable = new HashTable();// 添加Student s1 = new Student("1");Student s2 = new Student("2");Student s3 = new Student("3");Student s4 = new Student("4");Student s5 = new Student("5");Student s6 = new Student("2");hashTable.add(s1);hashTable.add(s2);hashTable.add(s3);hashTable.add(s4);hashTable.add(s5);hashTable.add(s6);// 显示System.out.println("显示全部元素:");hashTable.display();// 删除System.out.println("删除对象s6后,剩余元素为:");hashTable.delete(s6);hashTable.display();// 查找if(hashTable.find(s6) == null)System.out.println("不存在s6该信息");}}


3.2.3 测试结果

 






3.3二次探测法

3.3.1 原理

如果哈希函数计算的原始下班为1000,线性探测查找空白就是1001,1002,1003,依此类推。而在二次探测法中,探测的过程是1000+12, 1000+22, 1000+32, 等等。即使跨度变大,依然会产生聚集,称为二次聚集。由于代码和线性探测类似,这里不再演示。


3.4 再哈希法

3.4.1 原理

再哈希法使用一种依赖关键字的探测序列,消除原始聚集和二次聚集等问题。核心步骤是将关键字再做一次哈希化,用这个结果作为步长,从而保证不同关键字探测序列不同。


3.4.2 完整代码

public class HashTable {/*** 哈希表的底层是数组*/Student[] hashArray;/*** 初始化数组的大小*/int arraySize = 101;/*** 设定初始化哈希表的大小 无参构造函数*/public HashTable() {hashArray = new Student[arraySize];}/*** 指定哈希表的大小 有参构造函数*/public HashTable(int arraySize) {this.arraySize = arraySize;hashArray = new Student[arraySize];}/*** 第1个哈希函数* 用于找到原始位置* @param key* @return*/public int hashCode(String key) {int hashVal = 0;int power10 = 1;for (int i = key.length() - 1; i >= 0; i--) {hashVal = hashVal + (key.charAt(i) - 48) * power10; // 10的幂的连乘power10 = power10 * 10;}return hashVal % hashArray.length; // 取模}/*** 第2个哈希函数* 生成步长* @param key* @return*/public int hashFun(String key){int hashVal = 0;int power10 = 1;for (int i = key.length() - 1; i >= 0; i--) {hashVal = hashVal + (key.charAt(i) - 48) * power10; // 10的幂的连乘power10 = power10 * 10;}return 5 - (hashVal % 5); // 1)和第一1哈希函数不同 2)不能输出为0 }/*** 添加数据* * @param student*/public void add(Student student) {int hashVal = hashCode(student.getKey()); // student若为空,则报空指针异常// 步长stepSize = constant - (key % constant)//  constant是小于数组容量的质数,如5,3等int setpSize = hashFun(student.getKey()); for (int i = 0; i < arraySize; i++) {if (hashArray[hashVal] == null) {hashArray[hashVal] = student;return;}hashVal += setpSize; // 寻找下一个空白位置hashVal %= arraySize; // 取摸,避免数组越界}}/*** 删除数据并返回* * @param student* @return*/public Student delete(Student student) {int hashVal = hashCode(student.getKey()); // student若为空,则报空指针异常int setpSize = hashFun(student.getKey()); // 步长for (int i = 0; i < arraySize; i++) {if (hashArray[hashVal] == student) {hashArray[hashVal] = null;return student;}hashVal += setpSize; // 寻找下一个空白位置hashVal %= arraySize; // 取摸,避免数组越界}return null; // 不存在该信息}/*** 查找并返回* * @param student* @return*/public Student find(Student student) {int hashVal = hashCode(student.getKey()); // student若为空,则报空指针异常int setpSize = hashFun(student.getKey()); // 步长for (int i = 0; i < arraySize; i++) {if (hashArray[hashVal] == student) {return hashArray[hashVal];}hashVal += setpSize; // 寻找下一个空白位置hashVal %= arraySize; // 取摸,避免数组越界}return null; // 不存在该信息}/*** 显示数据*/public void display() {StringBuilder str = new StringBuilder(); // 字符串拼接str.append("[");for (int i = 0; i < arraySize; i++) {if (hashArray[i] != null) {str.append(hashArray[i].getKey() + ", ");}}str.replace(str.length() - 2, str.length() - 1, "]"); // 不包含起始值System.out.println(str.toString());}/*** 测试* * @param args*/public static void main(String[] args) {HashTable hashTable = new HashTable();// 添加Student s1 = new Student("1");Student s2 = new Student("2");Student s3 = new Student("3");Student s4 = new Student("4");Student s5 = new Student("5");Student s6 = new Student("2");hashTable.add(s1);hashTable.add(s2);hashTable.add(s3);hashTable.add(s4);hashTable.add(s5);hashTable.add(s6);// 显示System.out.println("显示全部元素:");hashTable.display();// 删除System.out.println("删除对象s6后,剩余元素为:");hashTable.delete(s6);hashTable.display();// 查找if(hashTable.find(s6) == null)System.out.println("不存在s6信息");}}

3.4.3 测试结果









4 链地址法

4.1 原理

在哈希表每个单元中设置链表。某个数据项的关键字还是像通常一样映射到哈希表的单元中,而数据项本身插入到单元的链表中。


4.2 完整代码

public class Node { // 数据域public Student student; // 上一个节点public Node previous; /*** 有参构造函数*/public Node(Student student) {super();this.student = student;} /*** 显示当前节点*/public void showCurrentNode(){System.out.println("stuNo is " + student.getKey());}} public class Link { /*** 尾结点*/private Node last; /*** 无参构造函数*/public Link() {last = null;} /*** 在尾部插入节点* @param student*/public void insertLast(Student student) {Node node = new Node(student);node.previous = last;last = node;}; /*** 查找* @param student* @return*/public Student find(Student student) {Node current = last; // 临时变量while (current != null) { // 循环查找if (current.student == student) {return student;}current = current.previous;}System.out.println("该学生不存在!");return null;} /*** 删除最后一个节点并返回* @return*/public Node deleteLast() {Node current = last;last = last.previous;return current;} /*** 删除节点并返回* @param student* @return*/public Student delete(Student student) {if(last != null && last.student == student){ //判断是否尾节点last = last.previous;return student;} else{ //非尾节点Node current = last.previous;//表示当前节点Node currentNext = last; //表示当前节点的下一个节点while (current != null) {if (current.student == student) {currentNext.previous = current.previous; //删除当前节点return current.student;}currentNext = current;current = current.previous;}}System.out.println("所要删除的节点不存在!");return null;} /*** 显示所有节点信息*/public void display() {Node current = last;while (current != null) {current.showCurrentNode();current = current.previous;}// 换行System.out.println();}} public class HashTable {/*** 哈希表的底层是数组*/Link[] hashArray; /*** 设定初始化哈希表的大小 无参构造函数*/public HashTable() {hashArray = new Link[101];} /*** 指定哈希表的大小 有参构造函数*/public HashTable(int arraySize) {hashArray = new Link[arraySize];} /*** 获取哈希值* * @param key* @return*/public int hashCode(String key) {int hashVal = 0;int power10 = 1;for (int i = key.length() - 1; i >= 0; i--) {hashVal = hashVal + (key.charAt(i) - 48) * power10; // 10的幂的连乘power10 = power10 * 10;}return hashVal % hashArray.length; // 取模} /*** 添加数据* * @param student*/public void add(Student student) {int hashVal = hashCode(student.getKey()); // student若为空,则报空指针异常if (hashArray[hashVal] == null) { // 判断是否为空hashArray[hashVal] = new Link(); // 将链表添加到哈希表中}hashArray[hashVal].insertLast(student); // 在链表尾部插入数据} /*** 删除数据并返回* * @param student* @return*/public Student delete(Student student) {int hashVal = hashCode(student.getKey()); // student若为空,则报空指针异常return hashArray[hashVal].delete(student);} /*** 查找并返回* * @param student* @return*/public Student find(Student student) {int hashVal = hashCode(student.getKey()); // student若为空,则报空指针异常if (hashArray[hashVal].find(student) != null) {return hashArray[hashVal].find(student);}return null;} /*** 显示数据*/public void display() {for (int i = 0; i < hashArray.length; i++) {if (hashArray[i] != null) {hashArray[i].display();}}} /*** 测试* * @param args*/public static void main(String[] args) {HashTable hashTable = new HashTable(); // 添加Student s1 = new Student("1");Student s2 = new Student("2");Student s3 = new Student("3");Student s4 = new Student("4");Student s5 = new Student("5");hashTable.add(s1);hashTable.add(s2);hashTable.add(s2);hashTable.add(s3);hashTable.add(s4);hashTable.add(s5); // 显示System.out.println("显示全部元素:");hashTable.display(); //删除System.out.println("删除对象s2后,剩余元素为:");hashTable.delete(s2);hashTable.display();} }


4.3 测试结果

 


5 开放地址法和链地址法的对比

在开发地址法中,当装填因子(插入元素和哈希表底层数组大小的比值)超过1/2或2/3后,性能下降得很快。在链地址发中填充因子即使达到1以上,对性能都影响不大。因此,链地址法是更健壮的机制,特别是哈希表的存储容量不清楚的前提下。


6 参考文献

[1] Robert, Lafore., Java数据结构和算法.第2版 版. 2004: 中国电力出版社.
[2] Sedgewick Robert与Wayne  Kevin, 算法. 2012: 人民邮电出版社.