[土味]自制HashTable

自定义一个简单的散列表

主要功能

通过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. 散列表可以用来以常数平均时间实现插入和查找操作
  2. 散列函数的理想状态是容器中的元素平均分布在每个单元格内,链表的长度都为1.但实际上测试结果发现总会有20%以上的单元格是空着的,剩余的80%单元格承担着所有元素.这还是优化散列函数的结果,就是上面的getAsc()方法.不同的散列方法结果相差很大,需要调参数.HashSet里面使用的是31,网上说原因是31是质素,且31 = 2<<5-1,计算机计算比较快,我这边实际测试下来,11的分布结果是最好的.测试结果如下表1.
  3. 数组的初始长度影响不是很大,HashSet里面使用的是16,原因不知道,我这边测试了一下,[10-30]哪个数每次累加一半,10万以内,成为质数的次数最多,结果显示11反而是最多的,16少一个,其他的数结果也差不多.测试结果如下表2,代码如下.
  4. 对于现在的计算机来说,数组的扩容完全可以指数增长,而不用担心内存不够,这样可以减少扩容的次数.

表1

数据量链表最大长度(越小越好)未使用单元格数量(越少越好)全部单元格数量
11100K231879118342
31100K441811118342

表2

次数次数
105214
117225
122234
133246
144251
155260
166272
172281
182293
192305
205

表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;
    }

参考资料

<<数据结构与算法分析>>

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值