数据结构:哈希表(线性结构)

在这里插入图片描述
(1)哈希表将我们关心的内容转换成索引,直接用一个数组来存储相应的内容,数组本身支持随机访问,可使用O(1)的复杂度来完成各项操作
(2)哈希表中可以存储各种数据类型,对于每种数据类型,都需要一种方法将其转换成索引,将该数据类型转换成索引的函数就是哈希函数
(3)复杂情况下,如键是指身份证号,不能直接用来当作数组的索引;键是字符串,需要设计一个哈希函数将字符串转换成索引;键是浮点数,符合类型等,需要设计一个合理的哈希函数
(4)很难保证每一个键通过哈希函数的转换对应不同的索引,可能产生哈希冲突

在这里插入图片描述
关键问题:1设计哈希函数 2解决哈希冲突
流程:键->值----> 索引->值(映射) 利用数组随机访问特性进行操作

在这里插入图片描述
空间换时间:多存储一些东西,预处理或缓存,实际执行算法任务时快很多

哈希函数的设计:键通过哈希函数得到的索引分布越均匀越好

整型:模一个素数

在这里插入图片描述
在这里插入图片描述缺点:分布不均匀(后六位永远不可能比320000大) 没有利用所有信息

模一个素数解决分布不均匀且利用所有信息

在这里插入图片描述

浮点数:按照32/64为整型数据处理(浮点数本身是32/64为二进制表示,只不过计算机解析成了浮点数

在这里插入图片描述

字符串:可以把一个字符串看成一个大的B进制的整型,每一个字符对应不同的数字,计算出字符串对应的整型数字,再进行整型处理

(1)浮点型相比,浮点型依然只占32或64位空间,但字符串可由若干字符组合,所占空间数量不固定,但依然可以转换成整型处理
(2)可以将一个整数看成一个字符串,每一个字符就是一个数字;可将一个字符串看成一个26进制的整数,a对应0,z对应25,z加上1需要进位,code对应的整数为c对应的数字乘以263+o对应的数字乘以262+…
在这里插入图片描述

存在的问题:计算低效且可能造成整型溢出

在这里插入图片描述
hash的最后结果需要取模,对于字符串来说,如果字符串特别长,B又特别大,算出的大整型可能产生整型的溢出,可将取模的过程移到每个括号的里面,保证每次都计算出一个比M更小的数再乘以B再加一个数字,整个过程就不会整型溢出(求余的性质)
在这里插入图片描述

复合类型:复合类型的处理类似字符串的处理,字符串可理解成由多个字符组成的复合类型,对于自己设计的复合类型,只不过每一部分不一定是一个字符而已

在这里插入图片描述

哈希函数设计原则

在这里插入图片描述

Java中的hashCode()只返回数据类型对应的一个整型数(int类型),不是前文提到的索引,因为取模M(哈希表的大小)未知

(1)对于标准库中的数据类型,不需要自己去实现hash函数,可直接调用hashCode方法直接获得当前数据所对应的hash函数值;对于自定义类型,可以覆盖Object父类中就有的函数hashCode,定义自己的hashCode
(2)让int类型的a作为键的话,先将其转换成一个对象(包装类)
(3)Java封装的hashCode的返回值是一个int值,由于int是有符号的,所以返回的有正有负,要把hashCode再转成一个索引,需要在自己的哈希表的类中完成

        int a=42;
        System.out.println(((Integer)a).hashCode());
        int b=-42;
        System.out.println(((Integer)b).hashCode());
        String s="asfdf";
        System.out.println(s.hashCode());

(4)Java中封装的hashCode和之前讲过的哈希函数有一些不同,hashCode()返回的是一个int值
(5)之前讲的将一个整数转换成索引的方式,是对一个素数进行取模运算,这个素数的值其实也是哈希表的大小,如果没有这个哈希表的话,也取不出这个素数,所以在定义类时,不能直接将它转成一个索引,因为我们不知道索引的最大值。hashCode()只是将每一个数据类型和一个整型对应了起来,整型可正可负,整型如何再更数组中的索引对应,由哈希表内部的逻辑来完成

自定义类型的hashCode

public class Student {
    int grade;
    int cls;
    String firstName;
    String lastName;

    Student(int grade,int cls,String firstName,String lastName){
        this.grade=grade;
        this.cls=cls;
        this.firstName=firstName;
        this.lastName=lastName;
    }
    将Student类作为哈希表中的键来进行存储,相应的这个类必须可以转换成哈希函数
    直接覆盖Object的hashCode方法
    @Override
    public int hashCode() {
        设计了一个复合类型有四个作用域
        直接把四个部分看成四个数字
        将其看成由四个数字组成的B进制的数
        任取B
        int B=31;
        计算hash值
        实际上是字符串转整型中的每一个字符都要进行相应的操作,只不过在设计hashCode
        的时候哈希表的大小未知,即M的值未知,所以还无法对M进行求余
        int hash=0;
        hash=hash*B+grade;
        grade本身就是整型,直接加上grade
        hash=hash*B+cls;
        firstName本身是一个字符串,直接用字符串的hashCode方法将其转换成一个整数
        
        hash=hash*B+firstName.hashCode();
        hash=hash*B+lastName.hashCode();
        
        累加过程可能产生整型溢出,已经是最大的整型值,再加上1,循环回来变成最小的负整型值
        即使产生了整型的溢出,程序不会中断,hash值依然是在一个整型的范围里进行运算,只不过运算的结果
        不一定是真正的数学运算的结果,在产生整型溢出后,对溢出的处理依然可以得到一个整型的计算结果
        虽然数学的结果不对,但它同样满足函数的语意,函数的语意是将函数中的所有成员变量综合起来求一个整数
        最终把这个整数值求出来返回回去就好了,这个过程虽然可能产生溢出,也是没有问题的
        return hash;
        复合类型求哈希值,依次求出每一部分对应的整型值,进行累加后得到复合类型对应的整型值

    }

Student类有hashCode后,可以使用java提供的和哈希表相关的数据结构

        HashSet以哈希表作为底层实现的集合
        HashSet<Student> set=new HashSet<>();
        存进去会自动调用student的hashCode().计算出相应的索引值,存储到相应的位置中
        set.add(student);
        student->索引->存储学生信息
        哈希函数 Student类定义的哈希函数,将Student类型的数据转换成索引的方法
        HashMap<Student,Integer> map=new HashMap<>();
        
        map.put(student,100);
        student->索引->存储(学生信息,分数信息)

在HashSet中添加重复对象时过程类似

没有重写hashCode()时,使用Object类的默认hashCode实现,地址相同的类对象哈希函数值才相同)Java标准库中的类已经重写了hashCode(),只要对应的属性相同,哈希函数值就相同,类似的需要重写自定义类的hashCode()

(1)当在Student类中没有重写hashCode时,依然正常运行,对java来说,每一个Object类默认有一个hashCode的实现,hashCode是根据创建的每一个Object的地址相应地转换成整型,地址相同的Student类的对象,hashCode的值一样,而内容相容,地址不同的Student类的对象,hashCode的值不一样

返回的哈希函数值相同不一定是同一个对象,需要进一步使用equals判断,地址相同或类型相同且属性相同才是同一个对象,在集合中添加时,才视为重复元素

(2)重写hashCode后,内容相容上的两个Student类的对象,hashCode的值一样,需要进一步区分,两个Student类的对象是否是仅仅属性相同,地址不同,还是属性相同,地址相同,两个对象其实是同一个对象;自己写的hashCode函数只是用于帮助我们计算哈希函数的值,但是在产生哈希冲突的时候,同样是要比较两个不同的对象它们之间是否是相等的,也就是对应的哈希函数值虽然相等,此时产生了冲突,为了辨别两个类的不同,要看这两个类是否是真正相等的,如果一个类要作为哈希表的键的话,只覆盖一个hashCode是不够的,需要覆盖equals,判断两个对象是否相等

判断两个对象是否相等(地址相同或类型相同且值相同)如在集合中添加重复的元素
    @Override
    public boolean equals(Object o){
        if(this==o)
            return true;
        if(o==null)
            return false;
        if(getClass()!=o.getClass())
            return false;
        Student another=(Student)o;
        return this.grade==another.grade&&
                this.cls==another.cls&&
                this.firstName.equals(another.firstName)&&
                this.lastName.equals(another.lastName);
    }
 在产生哈希冲突时,虽然对应同样的哈希值,也可以equals方法区分出两个类对象的不同

在这里插入图片描述
如图,产生哈希冲突,放进同一个桶里的数据,可使用equals区分是否是重复数据

哈希冲突的处理:Seperate Chaining

哈希表本质在一片数组空间中,数组的每一个位置都存储一个查找表,查找表的底层实现是任意可实现查找表的数据结构(LinkedListMap,TreeMap,LinkedListSet, TreeSet)

在这里插入图片描述
hashCode(k1)可能为负,需要去掉负号,hashCode(k1)&0x7fffffff 去掉符号(0x7fffffff表示31个1的二进制数,哈希函数值进行按位与,int型为32位,最高位为符号位,0与最高位(1为负,0为正)按位与结果为0,1与剩余位按位与,还是剩余位,最后的到哈希函数值的绝对值

在这里插入图片描述

在哈希表索引为4的地方存储k1,k1和k3产生哈希冲突时,依然把k3放到索引为1的位置,由于存在哈希冲突,哈希表中每个索引对应的位置存储的实际是一个LinkedListMap或TreeMap(查找表,使用链表或平衡树作为底层实现)

添加的元素找到索引,添加进索引存储的TreeMap中

在这里插入图片描述
在这里插入图片描述
(1)HashMap存储的是映射形式,HashSet存储的是集合形式,可以简单的对映射的值传空实现集合,HashSet也可以复用HashMap或使用TreeMap的数组来实现

(2)哈希冲突达到一定程度(平均来讲,每一个位置所存储的元素数多于某一个程度)红黑树的操作虽然每一步从时间复杂度来说都比链表要低,但是当数据规模比较小的时候,其实链表是更坏的,当哈希冲突很少(冲突只在1.2.3。。。),链表进行删除等操作是很快的,但对于这么小的数据规模使用红黑树的话,可能要使用各种旋转操作来保证满足红黑树的性质,操作反而慢一些(哈希冲突少,使用LinkedList;哈希冲突多,使用Tree

用HashSet检测循环

使用 HashSet 而不是向量、列表或数组的原因是因为我们反复检查其中是否存在某数字。检查数字是否在哈希集中需要 O(1)的时间,而对于其他数据结构,则需要 O(n)的时间。

在这里插入图片描述

实现哈希表

之前实现的树都是二分搜索树,要求键必须具有可比较性,在**HashTable中键不需要具有可比较性(即使查找表是二分搜索树),**但必须具有hashCode 方法(固有的,有必要可以覆盖hashCde)

哈希表有M个地址,向哈希表中放入N个元素,平均来看每一个地址有N/M个元素,如果每一个地址挂接链表,平均复杂度是O(N/M),每一个操作的主要时间消耗在链表的相应操作上,而对于寻找到地址所对应的链表,由于数组支持随机访问,用O(1)的时间就能访问;当每一个地址挂接一棵平衡树,平均时间复杂度是O(logN/M)

在这里插入图片描述

使哈希表时间复杂度为O(1):M是一个常数,N无限变大时,N/M也趋向无穷,底层实现是静态数组时,时间复杂度不可能是O(1)级别;底层实现为动态数组,M随着N的改变自适应变化

在这里插入图片描述
(1)与普通数组扩容不同,普通数组当元素装满时就扩容,在哈希表链地址法中没有元素装满地址空间的概念,每一个地址是一个哈希值对应的位置,当M个空间开辟出来时,相当于M个空间已经被占据了,每一个位置是哈希值对应的查找表

(2)扩容缩容的大概原理是一致的,新开辟一片空间,把现在哈希表的内容装进新的空间:将哈希表中的每个键值数据拿出来重新计算索引放到新的哈希表中

import java.util.TreeMap;

public class HashTable<K,V>{
    //哈希表可以看成TreeMap的数组,底层是红黑树
    //封装了TreeMap,实现哈希表简单
    private TreeMap<K,V>[] hashtable;
   //哈希表的大小(合适的素数)
    private int M;
    //哈希表中已经存储的元素
    private int size;
    private static final int upperTol=10;
    private static final int lowerTol=2;
    private static final int initCapacity=7;

    public HashTable(int M) {

        this.M = M;
        //初始化数组长度,有M个TreeMap
        hashtable = new TreeMap[M];
        size = 0;
        //初始化数组中的每个TreeMap
        for(int i=0;i<M;i++)
            hashtable[i]=new TreeMap<>();

    }

    public HashTable(){
        this(initCapacity);
    }

   //辅助函数,真正的哈希函数hashCode%M;将键值转换成哈希表中对应的索引值
    private int hash(K key){
        //任何key都在hashtable中有对应的索引,因为%M后会得到小于M的索引值
        return (key.hashCode()&0x7fffffff)%M;
    }

    private void resize(int newM){
        TreeMap<K,V>[] newHashTable=new TreeMap[newM];
        //对newM个TreeMap进行初始化
        for(int i=0;i<newM;i++)
            newHashTable[i]=new TreeMap<>();
        //修改hash()中使用的M
        int oldM=M;
         this.M=M;
        //将hashtable中的每个键值对取出来放到新的数组中对应的新位置,对应的索引变化了
        for(int i=0;i<oldM;i++) {
            TreeMap<K, V> map = hashtable[i];
            for (K key : map.keySet())
                newHashTable[hash(key)].put(key, map.get(key));
        }
        this.hashtable=newHashTable;
    }


    public void add(K key, V value) {
        //取出hashtable中相应的TreeMap,看当前位置的TreeMap中
        //是否已经包含了key,包含了就修改,不包含就添加,并维护size
        TreeMap<K,V> map=hashtable[hash(key)];
        if(map.containsKey(key))
             map.put(key,value);
        else {
            map.put(key, value);
            size++;

            //避免整型向浮点型转换,除法转换成乘法
            if (size >= upperTol * M)
                resize(M * 2);
        }
    }


    public V remove(K key) {

        TreeMap<K,V> map=hashtable[hash(key)];
        V ret=null;
        if(map.containsKey(key)) {
            ret=map.remove(key);
            size--;
            if (size <lowerTol * M&&M/2>=initCapacity)
                resize(M / 2);

        }
            return ret;

    }


    public boolean contains(K key) {
        return hashtable[hash(key)].containsKey(key);
    }


    public V get(K key) {
        return hashtable[hash(key)].get(key);
    }


    public void set(K key, V value) {
        TreeMap<K,V> map=hashtable[hash(key)];
        if(map.containsKey(key))
            map.put(key,value);
        else{
            throw new IllegalArgumentException("wrong");
        }

    }


    public int getSize() {
        return size;
    }


    public boolean isEmpty() {
        return size==0;
    }
}

时间复杂度

均摊复杂度(平摊到每个地址空间的操作,当作平摊到每个地址空间的数据规模),元素从N增加到upperTol*N加上触发地址空间加倍(O(N)级别)的时间复杂度,均摊到之前(upperTol-1)*N的每一步元素添加操作中;每个操作在O(lowerTol)-O(upperTol),维护的哈希表对每一个地址平均来讲,哈希碰撞的元素(在每一个地址中相应的查找表存储的元素个数)是在lowerTol-upperTol之间,复杂度是O(1)级别

在这里插入图片描述

在这里插入图片描述

开放地址法避免哈希冲突:这种哈希表就是一个数组

每一个地址都对所有元素是开放的
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

集合与映射的底层实现可以是链表、树、哈希表

有序集合和映射:在保存数据的同时,维持了有序性,TreeMap和TreeSet

无序集合和映射:HashSet、HashMap

在这里插入图片描述

抽象数据结构:一种抽象的表达

线性表:所有数据是线性排列,可以使用动态数组或链表来实现,如栈和队列;集合和映射:底层实现可以是链表、树、哈希表

在这里插入图片描述

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值