2020-11-28

1、散列表的概念

散列表Hash Table:又称为哈希表/Hash表,是根据键key直接访问在内存中数据的一种数据结构。它由数组演化而来,利用了数组按下标进行随机访问的特性。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数存放记录的数组叫做散列表

散列函数(哈希函数)

散列函数要满足的基本要求:

1、hash(key)必须大于等于0,因为hash值作为数组下标。
2、如果key1 == key2 ,则hash(key1)==hash(key2)。
3、如果key1 != key2 ,则hash(key1)!=hash(key2)。

好的散列函数的特点:

(1)、散列函数不能太复杂,否则会耗费大量时间在计算哈希值上,间接影响散列表性能。
(2)、散列函数计算的哈希值尽可能随机均匀分布,减少冲突

散列函数的设计方法:

(1)、直接寻址法:取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key) = a·key + b,其中a和b为常数。
(2)、除留取余法:最常用的构造散列函数的方法
对于散列表长为m的散列函数,有H(key) = key % P(P<=m,一般取P为小于等于m的最大质数)。该方法的关键在于P的选择
例如采用除留取余法时,令P=12,则对一系列关键字key有如下图所示,不过还是存在冲突,比如18与30就有冲突。
在这里插入图片描述
令P=11,则有如下图所示:虽然也有冲突,但是冲突次数大大减少。
在这里插入图片描述
(3)、平方取中法:也是一种常用的散列函数构造方法,它 取关键字平方后的中间几位为哈希地址,即公式为H(key) = key^2后中间几位数字。例如H(1234)=227,因为1234的平方为1522756!!!

散列冲突

当key1 != key2 ,但是hash(key1)==hash(key2),此时称为Hash冲突。根据鸽巢原理可得,哈希表的重复问题(冲突)是不可避免的,因为键的数目总是比索引的数目多,不管是多么高明的算法都不可能解决这个问题。就算键的数目比索引的数目少,必有一个输出串对应多个输入串,冲突还是会发生。

解决散列冲突的思路:

(1)、开放寻址法:一旦发生散列冲突,重新去寻找一个空的散地址。
1)、线性探测:从当前已被占用的位置开始依次向后查找,如果到达尾部,则绕到头部继续寻址。每次探测步长为1
2)、二次探测:解决线性探测存在的问题,以H+1^2、
H+2^2、。。。探测可用位置

3)、双重散列(双重哈希):两个哈希函数h1(key)和h2(key),当某个键值key经过h1(key)之后发现冲突,就使用第二个散列函数。

(2)、链表法(拉链法)数组的每个下标位置称为槽slot,每个槽会对应一条链表,所有冲突的元素放到相同槽对应的链表中。ps.槽slot是不是很熟悉,参考我的InnoDB数据页内多条记录如何存储?
在这里插入图片描述
拉链法结合了数组查询快,链表增删快的特点,比较适合存储大对象、大数据量的散列表,而且比起开放寻址法更加灵活,支持更多的优化策略,比如用红黑树替代链表!!!

2、散列表的应用

一个企业级的散列表应该具有的特点:

支持快速查询、插入、删除操作;
内存占用合理,不会浪费过多内存空间;
性能稳定。

散列表的设计思路:

构造一个合适的散列函数;
定义装填因子阈值,最好设计动态扩容策略;
设计合适的散列冲突解决策略。
装填因子a=n/m 其中n 为关键字个数,m为表长。加载因子越大,填满的元素越多,好处是,空间利用率高了,但:冲突的机会加大了.反之,加载因子越小,填满的元素越少,好处是:冲突的机会减小了,但:空间浪费多了!

Java中的HashMap和HashTable:

(1)、JDK1.7中:数组+链表(拉链法),底层维护一个Entry数组。
在这里插入图片描述
Entry 就是数组中的元素,每个 Entry 其实就是一个 key-value 对,它持有一个指向下一个元素的引用,这就构成了链表。

put(K,V)方法存值时:首先获取K的hash值,然后构造一个Entry对象,最后路由算法((table.length-1) & K的hash值)找到对应table数组下标。之后如果put元素的K计算的hash值相同,找到一样的table下标,就会进入链表中。

(2)、JDK1.8中:数组+链表+红黑树,底层维护一个Node数组。
在这里插入图片描述

JDK8 HashMap源码:
public class HashMap<K,V> extends AbstractMap<K,V>
       implements Map<K,V>, Cloneable, Serializable{
       
    // 默认table数组大小16
     static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
     
    //table数组最大容量
     static final int MAXIMUM_CAPACITY = 1 << 30;
     
    //负载因子
     static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
    //树化阈值,当table数组某个下标index出链表长度大于8时有可能转为红黑树
     static final int TREEIFY_THRESHOLD = 8;
     
    //树降级为链表的阈值,删除元素后容量小于6有可能红黑树转为链表
     static final int UNTREEIFY_THRESHOLD = 6;
     
    //树化的另一个参数,只有当哈希表所有元素个数大于64,才允许树化。和TREEIFY_THRESHOLD联合使用
     static final int MIN_TREEIFY_CAPACITY = 64;
     
     
    transient Node<K,V>[] table;
    transient int size;
    transient int modCount;//散列表修改次数(替换不算入次数)
    int threshold;//扩容阈值
    final float loadFactor;
    
    
    /--------四个构造方法-----------/
     * public HashMap(int initialCapacity, float loadFactor) {
     		//3个if其实是对输入参数的校验
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
            
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
                                               
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);	//table数组长度必须是2^n
        
	        1)、static final int tableSizeFor(int cap) {
		        int n = cap - 1;
		        n |= n >>> 1;
		        n |= n >>> 2;
		        n |= n >>> 4;
		        n |= n >>> 8;
		        n |= n >>> 16;
		        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
		    }
    }
    
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
     

    /---------------put方法--------------------/
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
        
        	1)、让key的hash值的高16位也参与路由算法
        	static final int hash(Object key) {
		        int h;
		        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
		    }
		    
		    2)、
		    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
                   
                // tab:表示散列表 ;p:表示当前元素; n数组长度 ; i表示key经过路由算法后的数组下标
                 
	        Node<K,V>[] tab; Node<K,V> p; int n, i;
	        
	        	//延迟初始化逻辑,只有第一次插入时才初始化避免内存浪费
	        if ((tab = table) == null || (n = tab.length) == 0)
	            n = (tab = resize()).length;
	            
	            //没有散列冲突,将K-V封装成Node对象插入数组中
	        if ((p = tab[i = (n - 1) & hash]) == null)
	            tab[i] = newNode(hash, key, value, null);
	            
	          //下面是散列冲突三种情况:替换、红黑树、链表
	        else {
	            Node<K,V> e; K k;
	            
	            //新插入元素key的hash和数组中相同,发生替换
	            if (p.hash == hash &&
	                ((k = p.key) == key || (key != null && key.equals(k))))
	                e = p;
	                
	             //红黑树情况
	            else if (p instanceof TreeNode)
	                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
	                
	             //链表情况
	            else {
	                for (int binCount = 0; ; ++binCount) {
	                	//遍历整个链表没有和当前key的hash相同的,插入链表尾部
	                    if ((e = p.next) == null) {
	                        p.next = newNode(hash, key, value, null);
	                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
	                            treeifyBin(tab, hash);
	                        break;
	                    }
	                    //遍历整个链表有和当前key的hash相同的,发生替换
	                    if (e.hash == hash &&
	                        ((k = e.key) == key || (key != null && key.equals(k))))
	                        break;
	                    p = e;
	                }
	            }
	            
	            if (e != null) { // existing mapping for key
	                V oldValue = e.value;
	                if (!onlyIfAbsent || oldValue == null)
	                    e.value = value;
	                afterNodeAccess(e);
	                return oldValue;
	            }
	        }
	        ++modCount;
	        if (++size > threshold)
	            resize();//触发扩容
	        afterNodeInsertion(evict);
	        return null;
	    }
    }
 
/---------get方法-----------/
 	public V get(Object key) {
        Node<K,V> e;
        //put插入元素时hash(key)了,所以取元素时也要这么做
        return (e = getNode(hash(key), key)) == null ? null : e.value;
        
        	1)、 final Node<K,V> getNode(int hash, Object key) {
        	
			        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
			        
			        if ((tab = table) != null && (n = tab.length) > 0 &&
			            (first = tab[(n - 1) & hash]) != null) {
			            
			            //查询数据就在数组中,不用查询链表或者红黑树
			            if (first.hash == hash && 
			                ((k = first.key) == key || (key != null && key.equals(k))))
			                return first;
			                
			            if ((e = first.next) != null) {
			            	
			                if (first instanceof TreeNode)
			                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
			                do {
			                    if (e.hash == hash &&
			                        ((k = e.key) == key || (key != null && key.equals(k))))
			                        return e;
			                } while ((e = e.next) != null);
			            }
			        }
			        return null;
			    }
	}		   
}

3、哈希算法(摘要算法)

将任意数据通过一个函数转换为长度固定的数据串。使用摘要算法,通过函数f()对任意长度的数据data计算出固定长度的摘要digest,目的是为了发现原始数据是否被篡改。一个好的哈希算法要满足:
1、将任何一条不论长短的信息计算出唯一的哈希值。
2、摘要长度固定,散列冲突概率小(对于不同的原始数据,哈希值相同的概率极小)。
3、摘要不可能被反向破译,即哈希算法是单向的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这是一个 SQL 语句,用于向借阅表中插入数据。该表包含以下字段:借阅编号、读者编号、书籍编号、借阅日期、归还日期、借阅状态。每条数据表示一次借阅记录。其中借阅编号、读者编号、书籍编号、借阅日期和借阅状态是必填项,归还日期为可选项,如果借阅状态为“已还”则必须填写归还日期。 具体插入的数据如下: - 借阅编号:100001,读者编号:123413,书籍编号:0001,借阅日期:2020-11-05,归还日期:NULL,借阅状态:借阅 - 借阅编号:100002,读者编号:223411,书籍编号:0002,借阅日期:2020-9-28,归还日期:2020-10-13,借阅状态:已还 - 借阅编号:100003,读者编号:321123,书籍编号:1001,借阅日期:2020-7-01,归还日期:NULL,借阅状态:过期 - 借阅编号:100004,读者编号:321124,书籍编号:2001,借阅日期:2020-10-09,归还日期:2020-10-14,借阅状态:已还 - 借阅编号:100005,读者编号:321124,书籍编号:0001,借阅日期:2020-10-15,归还日期:NULL,借阅状态:借阅 - 借阅编号:100006,读者编号:223411,书籍编号:2001,借阅日期:2020-10-16,归还日期:NULL,借阅状态:借阅 - 借阅编号:100007,读者编号:411111,书籍编号:1002,借阅日期:2020-9-01,归还日期:2020-9-24,借阅状态:已还 - 借阅编号:100008,读者编号:411111,书籍编号:0001,借阅日期:2020-9-25,归还日期:NULL,借阅状态:借阅 - 借阅编号:100009,读者编号:411111,书籍编号:1001,借阅日期:2020-10-08,归还日期:NULL,借阅状态:借阅

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值