自定义一个简单的散列表
主要功能
通过java代码实现一个简单的HashTable数据结构,可以进行增删查,并实现迭代器,以及自动扩容.
代码实现
package com.example.springboot01.util;
import org.junit.Test;
import java.util.HashSet;
import java.util.Iterator;
/**
* 自制散列表,使用数组+链表实现
*/
public class MyHashTable<T> {
// 容器中元素的个数
private int size = 0;
// 散列表长度
private static int tableSize = 120000;
// 不指定泛型,需要自己控制输入的类型
private MyLinkedList<T>[] array = new MyLinkedList[tableSize];
public MyHashTable() {
// 初始化链表
for(int i=0; i<tableSize; i++) {
MyLinkedList<T> list = new MyLinkedList<>();
array[i] = list;
}
}
/**
* 插入
* @param t T
*/
public void add(T t) {
if(t instanceof Integer) {
}
else if(t instanceof String) {
int index = getAsc((String)t);
// 获取到MyLinkedList对象
MyLinkedList<T> list = array[index];
// 如果已存在,则跳过
if(list.size() > 0 && list.get(0) != null) {
// 依次比较
MyLinkedList<T>.Itr<T> itr = list.iterator();
while(itr.hasNext()) {
if(t.equals(itr.next())) {
return;
}
}
}
// 不存在,执行插入
list.add(t);
// 容器中元素个数加一
size++;
}
}
/**
* 删除指定元素
* @param t 元素
*/
public void remove(T t) {
if(t instanceof Integer) {
}
else if(t instanceof String) {
int index = getAsc((String)t);
MyLinkedList<T> list = array[index];
if(list.get(0) != null) {
// 依次比较
MyLinkedList<T>.Itr<T> itr = list.iterator();
while(itr.hasNext()) {
// 比较两个字符串是否相等
if(t.equals(itr.next())) {
// 删除元素
itr.remove();
// 容器中元素的个数减一
size--;
}
}
}
}
}
/**
* 判断容器中是否包含某个元素
* @param t 元素
* @return boolean
*/
public boolean contains(T t) {
if(t instanceof Integer) {
}
else if(t instanceof String) {
int index = getAsc((String)t);
MyLinkedList<T> list = array[index];
if(list.size() > 0 && list.get(0) != null) {
// 依次比较
MyLinkedList<T>.Itr<T> itr = list.iterator();
while(itr.hasNext()) {
if(t.equals(itr.next())) {
return true;
}
}
}
}
return false;
}
/**
* 迭代器类,这并不是真正的迭代器,只是为了迭代而迭代,用不着定义泛型.
* 按先数组后链表的顺序依次遍历
*/
public class Itr {
// 遍历过程中当前数组下标
private int currentIndex = 0;
// 遍历过程中当前链表索引
private int currentNode = -1;
// 遍历过程中当前游标
private int cursor = -1;
/**
* 是否存在下一个元素
* @return
*/
public boolean hasNext() {
return cursor+1 < size();
}
public T next() {
// 游标加一
cursor++;
// 当前节点加一
currentNode++;
// 如果当前节点超出了当前链表的长度,则移动到下一个链表的第一个节点
if(array[currentIndex].size() < (currentNode+1)) {
currentIndex++;
currentNode = 0;
// 如果下一个链表为空,则移动到再下一个链表
while(array[currentIndex].size() == 0) {
currentIndex++;
}
}
return array[currentIndex].get(currentNode);
}
/**
* 删除元素
*/
public void remove() {
// 直接调用MyLinkedList里面的remove方法删除当前节点
array[currentIndex].remove(currentNode);
// 游标减一
cursor--;
// 容器中元素的个数减一
size--;
// 当前链表索引减一
currentNode--;
}
}
/**
* 返回迭代器对象
* @return Itr
*/
public Itr iterator() {
return new Itr();
}
/**
* 返回容器中元素的个数
* @return
*/
public int size() {
return size;
}
/**
* 计算字符串的key,用于确定对应到散列表的哪个单元格
* @param str 元素
* @return key
*/
public int getAsc(String str) {
int hashVal = 0;
for(char c: str.toCharArray()) {
hashVal = 31 * hashVal + c;
//System.out.println(hashVal);
}
hashVal %= tableSize;
if(hashVal < 0) {
hashVal += tableSize;
}
return hashVal;
}
@Test
public void testMyHashTable() {
long startTime = System.currentTimeMillis();
MyHashTable<String> myHashTable = new MyHashTable<>();
for(int i=0; i<100000; i++) {
myHashTable.add("Hello" + i);
}
MyHashTable<String>.Itr itr2 = myHashTable.iterator();
while(itr2.hasNext()) {
String content = itr2.next();
}
long middleTime = System.currentTimeMillis();
System.out.println("totalTime: " + (System.currentTimeMillis() - startTime) + "ms");
System.out.println("contains Hello100? : " + myHashTable.contains("Hello100"));
System.out.println("contains: Hell100? : " + myHashTable.contains("Hell100"));
System.out.println("totalTime: " + (System.currentTimeMillis() - middleTime) + "ms");
}
}
性能测试
可以看到,对于10万之内的数据,速度都很快.
代码优化
增加了自动扩容部分
package com.example.springboot01.util;
import org.junit.Test;
/**
* 自制散列表,使用数组+链表实现
*/
public class MyHashTable<T> {
// 容器中元素的个数
private int size = 0;
// 散列表长度
private static int tableSize = 16;
// 加载因子,容器中所有元素的个数与容器容量的比值
private int loadFactor = 1;
// 测试用,记录所有链表中最长是多长
public int deepth = 0;
// 测试用,记录数组空白单元格
public int blankCellNum = 0;
// 测试用,记录数组所有单元格
public int allCellNum = tableSize;
// 不指定泛型,需要自己控制输入的类型
private MyLinkedList<T>[] array = new MyLinkedList[tableSize];
public MyHashTable() {
// 初始化链表
for(int i=0; i<tableSize; i++) {
MyLinkedList<T> list = new MyLinkedList<>();
array[i] = list;
}
}
/**
* 插入
* @param t T
*/
public void add(T t) {
if((size+1) > tableSize * loadFactor) {
// 扩容
expandArray();
}
if(t instanceof Integer) {
}
else if(t instanceof String) {
int index = getAsc((String)t);
// 获取到MyLinkedList对象
MyLinkedList<T> list = array[index];
// 如果已存在,则跳过
if(list.size() > 0 && list.get(0) != null) {
// 依次比较
MyLinkedList<T>.Itr<T> itr = list.iterator();
while(itr.hasNext()) {
if(t.equals(itr.next())) {
return;
}
}
}
// 不存在,执行插入
list.add(t);
// 容器中元素个数加一
size++;
}
}
/**
* 删除指定元素
* @param t 元素
*/
public void remove(T t) {
if(t instanceof Integer) {
}
else if(t instanceof String) {
int index = getAsc((String)t);
MyLinkedList<T> list = array[index];
if(list.get(0) != null) {
// 依次比较
MyLinkedList<T>.Itr<T> itr = list.iterator();
while(itr.hasNext()) {
// 比较两个字符串是否相等
if(t.equals(itr.next())) {
// 删除元素
itr.remove();
// 容器中元素的个数减一
size--;
}
}
}
}
}
/**
* 判断容器中是否包含某个元素
* @param t 元素
* @return boolean
*/
public boolean contains(T t) {
if(t instanceof Integer) {
}
else if(t instanceof String) {
int index = getAsc((String)t);
MyLinkedList<T> list = array[index];
if(list.size() > 0 && list.get(0) != null) {
// 依次比较
MyLinkedList<T>.Itr<T> itr = list.iterator();
while(itr.hasNext()) {
if(t.equals(itr.next())) {
return true;
}
}
}
}
return false;
}
/**
* 扩容
*/
private void expandArray() {
//System.out.println("===========================================expand start==========================================");
tableSize = tableSize + (tableSize >> 1);
allCellNum = tableSize;
// 新建一个数组
MyLinkedList<T>[] newArray = new MyLinkedList[tableSize];
// 初始化链表
for(int i=0; i<tableSize; i++) {
MyLinkedList<T> list = new MyLinkedList<>();
newArray[i] = list;
}
// 将原先的元素复制到新数组中
Itr itr = iterator();
while(itr.hasNext()) {
T t = itr.next();
if(t instanceof Integer) {
}
else if(t instanceof String) {
int index = getAsc((String)t);
// 获取到MyLinkedList对象
MyLinkedList<T> list = newArray[index];
// 执行插入
list.add(t);
}
}
array = newArray;
//System.out.println("===========================================expand end==========================================");
}
/**
* 迭代器类,这并不是真正的迭代器,只是为了迭代而迭代,用不着定义泛型.
* 按先数组后链表的顺序依次遍历
*/
public class Itr {
// 遍历过程中当前数组下标
private int currentIndex = 0;
// 遍历过程中当前链表索引
private int currentNode = -1;
// 遍历过程中当前游标
private int cursor = -1;
/**
* 是否存在下一个元素
* @return
*/
public boolean hasNext() {
return cursor+1 < size();
}
public T next() {
// 游标加一
cursor++;
// 当前节点加一
currentNode++;
// 如果当前节点超出了当前链表的长度,则移动到下一个链表的第一个节点
if(array[currentIndex].size() < (currentNode+1)) {
currentIndex++;
//System.out.println("第" + currentIndex + "个链表");
currentNode = 0;
// 如果下一个链表为空,则移动到再下一个链表
while(array[currentIndex].size() == 0) {
currentIndex++;
//System.out.println("第" + currentIndex + "个链表");
blankCellNum++;
}
}
if(deepth < (currentNode + 1)) {
deepth = currentNode + 1;
}
return array[currentIndex].get(currentNode);
}
/**
* 删除元素
*/
public void remove() {
// 直接调用MyLinkedList里面的remove方法删除当前节点
array[currentIndex].remove(currentNode);
// 游标减一
cursor--;
// 容器中元素的个数减一
size--;
// 当前链表索引减一
currentNode--;
}
}
/**
* 返回迭代器对象
* @return Itr
*/
public Itr iterator() {
return new Itr();
}
/**
* 返回容器中元素的个数
* @return
*/
public int size() {
return size;
}
/**
* 计算字符串的key,用于确定对应到散列表的哪个单元格
* hashCode有可能为负,因为超出了int类型的最大值
* @param str 元素
* @return key
*/
public int getAsc(String str) {
int hashVal = 0;
for(char c: str.toCharArray()) {
// 对于我这个例子,10万条数据11效果是最好的,String里面的散列函数使用的是31,可能原因是31=2<<8-1,计算比较快,又是质数
hashVal = 11 * hashVal + c;
}
hashVal %= tableSize;
if(hashVal < 0) {
hashVal += tableSize;
}
return hashVal;
}
@Test
public void testMyHashTable() {
long startTime = System.currentTimeMillis();
MyHashTable<String> myHashTable = new MyHashTable<>();
for(int i=0; i<100000; i++) {
myHashTable.add("Hello" + i);
}
long middleTime = System.currentTimeMillis();
System.out.println("insertCost: " + (System.currentTimeMillis() - startTime) + "ms");
MyHashTable<String>.Itr itr2 = myHashTable.iterator();
myHashTable.deepth = 0;
myHashTable.blankCellNum = 0;
while(itr2.hasNext()) {
String content = itr2.next();
//System.out.println(content);
}
System.out.println("readCost: " + (System.currentTimeMillis() - middleTime) + "ms");
System.out.println("maxDeepth: " + myHashTable.deepth);
System.out.println("blankCellNum: " + myHashTable.blankCellNum);
System.out.println("allCellNum: " + myHashTable.allCellNum);
}
}
总结
- 散列表可以用来以常数平均时间实现插入和查找操作
- 散列函数的理想状态是容器中的元素平均分布在每个单元格内,链表的长度都为1.但实际上测试结果发现总会有20%以上的单元格是空着的,剩余的80%单元格承担着所有元素.这还是优化散列函数的结果,就是上面的getAsc()方法.不同的散列方法结果相差很大,需要调参数.HashSet里面使用的是31,网上说原因是31是质素,且31 = 2<<5-1,计算机计算比较快,我这边实际测试下来,11的分布结果是最好的.测试结果如下表1.
- 数组的初始长度影响不是很大,HashSet里面使用的是16,原因不知道,我这边测试了一下,[10-30]哪个数每次累加一半,10万以内,成为质数的次数最多,结果显示11反而是最多的,16少一个,其他的数结果也差不多.测试结果如下表2,代码如下.
- 对于现在的计算机来说,数组的扩容完全可以指数增长,而不用担心内存不够,这样可以减少扩容的次数.
表1
数据量 | 链表最大长度(越小越好) | 未使用单元格数量(越少越好) | 全部单元格数量 | |
---|---|---|---|---|
11 | 100K | 2 | 31879 | 118342 |
31 | 100K | 4 | 41811 | 118342 |
表2
次数 | 次数 | ||
---|---|---|---|
10 | 5 | 21 | 4 |
11 | 7 | 22 | 5 |
12 | 2 | 23 | 4 |
13 | 3 | 24 | 6 |
14 | 4 | 25 | 1 |
15 | 5 | 26 | 0 |
16 | 6 | 27 | 2 |
17 | 2 | 28 | 1 |
18 | 2 | 29 | 3 |
19 | 2 | 30 | 5 |
20 | 5 |
表2测试代码:
/**
* 测试[10-30]哪个数每次累加一半,10万以内,成为质数的次数最多
*/
@Test
public void testNum() {
for(int i=10; i<31; i++) {
int count2 = 0;
int k = i;
while(k < 1000000) {
if(isPrimeNumber(k)) {
count2++;
}
k = k + (k >> 1);
}
System.out.println(i + ": " + count2);
}
}
/**
* 判断是否是质素
* @param num
* @return
*/
public boolean isPrimeNumber(int num) {
for(int i=2; i<num; i++) {
if(num % i == 0) {
return false;
}
}
return true;
}