Java的手写简单的哈希表

①TreeMap,底层为一个搜索树,但是,不是一个普通 的搜索树,而是一棵红黑树。存储数据的形式为键值对的形式,但是,键的值不可以重复,并且所有的键都必须是可以比较大小的。

在TreeMap当中,键不可以为空,否则插入时候就会报nullPointerException。

存放的键一定是按照有序的顺序存储的,因为二叉搜索树也是有序的。

public static void main(String[] args) {
        //往TreeMap当中存放元素的时候,key一定需要可以比较,否则会报错。
        Map<String,Integer> map=new TreeMap<>();
        //存放的顺序为键的值的比较
        //谁小就谁排在前面
        map.put("hi",2);
        map.put("hello",3);
        map.put("11",null);
        //不可以存放null
        map.put(null,1);
        System.out.println(map);
        //获取键的值的集合
        Set<String> set=map.keySet();
        System.out.println(set);
        //Map.entrySet():获取key,value:提供遍历map的一种方法
        Set<Map.Entry<String, Integer>> set1=map.entrySet();
        //treeMap当中不可以存储key为NULL的元素
        for (Map.Entry<String, Integer> set2:set1){
            System.out.println("key:"+set2.getKey()+"value:"+set2.getValue());
        }

②TreeSet

底层也是一棵搜索树,存储数据的特点为有序的,不可以重复的,所有存放的元素都是可以比较,不可重复的。因此,插入,删除,查找的时间复杂度都是log(n)

TreeSet其实底层就是TreeMap的实现,键为存入的值,当无参构造时候,会实例化一个TreeMap.

 当添加键的时候,会把对应的键插入集合当中,值为一个默认的Object类型的数据。

有关HashMap的问题的延申:

一、哈希冲突:两个不同的key,通过相同的哈希函数,被分配到了同一个位置。

由于哈希表底层数组的容量往往是小于实际需要存储的key的容量的,因此哈希冲突

只能减少,不可以避免。

  哈希函数的设计规则:当定义域为k时候,值域最好为[0,K-1]

  因此,有一种方法,叫做除留余数法,可以设定哈希函数:

 

 假设散列表当中允许的地址数为m,此时m=8,取一个不大于m,但是接近或者等于m的质数作为除数(p),那么,此时:Hash(key)=key%p;

此时,如果填入的key为2,再次填入为为10的key,那么这两个key就发生了哈希冲突。

二、调节负载因子

何为负载因子?

负载因子k=填入表中的元素个数/散列表的长度

此图为负载因子与冲突率之间的一个关系。当负载因子越大的时候,冲突率越高

因此为了降低负载因子,只能增加散列表的长度,

但是,一般情况下面,哈希表是有一个负载因子的规定常数的,大约是0.75。

当超过这个大小的时候,需要进行原来数组的扩容

三、闭散列,开散列,用于处理哈希冲突的办法:

闭散列:又称为开放地址法

缺点:容易出现堆积(聚集)现象

影响平均查找长度

如何删除元素?

把待删除元素的key置为0,即:做一个标记。不能直接置为空,否则无法查找发生哈希冲突的另外的元素。

①、闭散列,使用线性探测法

说明一下上图:当存放4这个key的时候,由于散列的长度为10,那么存放的位置就是4%10=4,此时,欲再次存入44,那么需要用44%10,得到的也是4,此时发生了哈希冲突,那么存放的地址就需要一直沿着散列往下走,找到下一个为空的地址存入,上图中,即为下标5的位置。

但是,如果后面的数组位置都不为空的情况下面,就会从0下标开始,一直往后走,直到找到为空的位置存放。

缺点:会把冲突发生的元素都聚集在一起,不易进行删除操作

②、闭散列,使用二次探测法

Hi=(H0+i²)%m:

H0为第一次存放位置的下标,i为第i次发生的冲突,m为散列表的长度。Hi为第i次发生冲突时候Hi存放的位置。

③开散列(哈希桶)

开散列,就是Java的HashMap来处理的(重点掌握)

把冲突的元素都放到数组对应的位置,但是该位置存储的是链表,而不是普普通通的元素,

如图所示,4,14,44冲突,但是他们都仍然在同一个位置按照链式存储,这种操作的方式就成为开散列。

jdk1.7以及以前采用的是头插法插入,jdk1.8开始,采用尾插法。

在合适的情况下面,链表会变成红黑树,

当数组的长度超过64并且链表的长度超过8的时候,链表会变为红黑树

总结以上两种方法:采用开放地址法处理哈希冲突的时候,其平均查找长度应当大于链地址法

性能分析:通常情况下,HashMap的插入,删除,查找的时间复杂度都为O(1)

下面是一个简单的哈希表代码实现,后面还会实现一个泛型的类型的

/**
 * @author 25043
 */
public class TestHashBunk {
    static class Node{
        public int key;
        private int val;
        public Node next;
        public Node(int key,int val){
            this.key=key;
            this.val=val;
        }
    }
    public int usedSize;

    public Node[] array;

    public TestHashBunk(){
        array=new Node[8];
    }

    /**
     * 插入表当中
     * 键@param key
     * 值@param val
     */
    public void put(int key,int val){
        int keyIndex=key%array.length;
        //通过散列函数找到对应的节点
        Node cur=array[keyIndex];
        //检验是否有重复插入的值
        while (cur!=null){
            if(cur.key==key){
                //更新值
                cur.val=val;
                return;
            }
            cur=cur.next;
        }
        //头插法插入元素
        Node node=new Node(key, val);
        node.next=array[keyIndex];
        array[keyIndex]=node;
        usedSize++;
        //当达到负载因子的时候,要进行扩容
        if(get(usedSize)>=0.75){
            resize();
        }
    }

    /**
     * 对原来的哈希表进行扩容
     * 扩容的思路:首先新建一个数组,大小为原来数组的2倍
     * 遍历原来的数组,对其中的每一个存储的链表进行遍历,把对应的节点重新
     * 使用散列函数再排一遍,因为,下标需要重新计算
     * 
     * 举个例子:比如原来的数组有8个元素
     * 
     * 后面如果需要扩容到16个的时候,假设原来有key为9的元素,存储到了索引为1的位置,
     * 那么如果扩容之后,9%16=9,需要改变位置,因此不可以直接使用
     * Arrays.copyOf(array,2*array.length)进行扩容
     */
    private void resize(){
        Node[] newArray=new Node[array.length*2];
        for (Node node : array) {
            //获取每个数组的头节点,重新排列到新的数组当中
            Node cur = node;
            while (cur != null) {
                //记录待检查节点的下一个节点
                Node curNext = cur.next;
                //获取到新的位置
                int newKeyIndex = cur.key % (array.length * 2);
                //cur的next区域指向新的头节点,完成头插法
                cur.next = newArray[newKeyIndex];
                newArray[newKeyIndex] = cur;
                //cur继续往下走
                cur = curNext;
            }
        }
        //array指向新的数组
        array=newArray;
    }

    /**
     * 获取负载因子
     * 有效的大小@param usedSize
     * 负载因子@return
     */
    public float get(int usedSize){
        return usedSize*1.0f/array.length;
    }

    public void disPlay(){
        for(int i=0;i<array.length;i++){
            Node cur=array[i];
            System.out.println("这是第"+i+"行遍历");
            while (cur!=null){
                System.out.println("当前节点的key为:"+cur.key+"当前节点的值为:"+cur.val);
                cur=cur.next;
            }
        }
    }

    public int getValue(int key){
        int hashKey=key%array.length;
        Node cur=array[hashKey];
        while (cur!=null){
            if(cur.key==key){
                return cur.val;
            }
            cur=cur.next;
        }
        System.out.println("对应的key不存在");
        return -1;
    }

    public static void main(String[] args) {
        TestHashBunk testHashBunk=new TestHashBunk();
        testHashBunk.put(1,3);
        testHashBunk.put(2,6);
        testHashBunk.put(9,3);
        testHashBunk.disPlay();
        int val= testHashBunk.getValue(9);
        System.out.println(val);
    }
}

泛型的,重写了HashCode方法,让相同对象的hashCode相同

class Person{

    public String id;

    public Person(String id){
        this.id=id;
    }

    @Override
    public String toString() {
        return "Person{" +
                "id='" + id + '\'' +
                '}';
    }

   
    /**
     * 重写hashCode,让equals的对象的哈希码的值相同
     * 哈希码@return
     */
    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}
/**
 * @author 25043
 */
public class HashBunk2<K,V> {
    static class Node<K,V>{
        public K k;
        public V v;
        public Node<K,V> next;
        public Node(K k,V v){
            this.k=k;
            this.v=v;
        }
    }

    public Node<K,V>[] array= new Node[10];
    public int usedSize;


    /**
     * 此时,方法当中的hash相当于之前直接key的值
     * 因此,如果两个对象的hashCode一样,不一定equals一样
     * 反之,equals一样,hashCode一定一样
     *
     * hashCode一样,说明一定会分配到同一个下标的位置
     *
     * @param k
     * @param v
     */
    public void put(K k,V v){
        //不可以直接除,因为类型不一样
        //int keyIndex=k%array.length;
        //返回hashKey
        int hash=k.hashCode();
        int keyIndex=hash%array.length;
        Node<K,V> node=new Node(k,v);
        Node<K,V> cur=array[keyIndex];
        while (cur!=null){
            if(cur.k.equals(k)){
                cur.v=v;
                return;
            }
            cur=cur.next;
        }
        node.next=array[keyIndex];
        array[keyIndex]=node;
        usedSize++;
        if(usedSize*1.0/array.length>0.75){
            resize();
        }
    }

    private void resize() {
        Node<K,V>[] newNodeArray=new Node[2*array.length];
        for (Node<K, V> kvNode : array) {
            Node<K, V> cur = kvNode;
            while (cur != null) {
                Node<K, V> curNext = cur.next;
                int newKeyIndex = cur.k.hashCode() % newNodeArray.length;
                cur.next = newNodeArray[newKeyIndex];
                newNodeArray[newKeyIndex] = cur;
                cur = curNext;
            }
        }
        array=newNodeArray;
    }

    public static void main(String[] args) {
        Person person1=new Person("123");
        Person person2=new Person("123");
        System.out.println(person1.hashCode());
        System.out.println(person2.hashCode());
    }
}

面试题:

HashCode如果相同,那么是否对象相同,如果对象相同,是否HashCode一定相同

首先要明白的是,HashCode一定要再哈希表当中才会发挥出它的意义的,当一个类重写了HashCode方法时候,一般情况下面,会根据把指定需要比较的属性进行比较,调用Objec.hash()方法,获取到根据对应的属性产生的哈希值,比如如下操作:

比如此Person类重写了HashCode方法,传入的参数是 id,也就意味着,如果把此Preson放入到HashMap当中,id相同的对象会分配到同一个hashCode,也就是说,hashCode相同,会被分配到HashMap当中数组的同一个索引的位置。

而equals一样,则一定代表是同一个对象,那么它的对应的HashCode的值也一定是相同的

 常见哈希冲突处理:闭散列(线性探测、二次探测)、开散列(链地址法)、多次散列

已知某个哈希表的n个关键字具有相同的哈希值,如果使用二次探测再散列法将这n个关键字存入哈希表,至少要进行()次探测。'

元素1:探测1次

  元素2:探测2次

  元素3:探测3次

  。。。

  元素n:探测n次

  故要将n个元素存入哈希表中,总共需要探测:1+2+3+...+n = n*(n+1)/2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值