精华内容
下载资源
问答
  • 什么是哈希表底层是怎么实现的?
    2022-04-21 11:00:41

            一、哈希表介绍

    哈希表(也叫散列表,HashTable)是能够通过给定的关键字的值直接访问到具体对应的值的一个数据结构。哈希表使用某种算法操作(散列函数)将键转化为数组的索引来访问数组中的数据,这样可以通过Key-value的方式来访问数据,达到常数级别的存取效率。

            二、哈希表的底层实现

    哈希表的本质是一个数组。实现哈希表一般有两种方法

    • 数组+链表(同时也是解决哈希冲突的办法)
    • 数组+二叉树(当冲突过多时链表会转换为树结构)—Java中的HashMap

            三、哈希函数

    哈希函数指将哈希表中元素的关键键值映射为元素存储位置的函数。

            四、哈希冲突

    一个元素经过哈希函数的计算得到的存储位置已经被占用,这个情况称为哈希冲突

            五、如何解决哈希冲突

    • 开放寻址法
    • 拉链法

    更多相关内容
  • 哈希表底层实现

    2021-01-20 13:46:03
    数据结构的物理存储结构只有两种:顺序存储结构和链式存储结构。 哈希表使用数组作为主干,实现查找,插入,删除元素的时间复杂度为O(1)。 哈希表(key,value) 就是把Key通过一个固定的算法函数既哈希函数转换成一个...
  • 底层实现数据结构哈希表

    千次阅读 2019-04-18 18:35:24
    哈希表时要解决两个问题,索引如何设计以及解决哈希冲突(即由键转换的索引与之前的相同)

    目录



    什么是哈希表


    哈希表,也称散列表,是实现字典操作的一种有效的数据结构。

    尽管在最坏的情况下,散列表查找一个元素的时间复杂度与链表中查找的时间相同,达到了O(n),然而实际应用中,散列表查找的性能是极好的,在一些合理的假设下,在散列表中可以查找一个元素的平均时间复杂度是O(1)。

    哈希表的例子:
    比如我们要存26个字母,开了一个 freq 数组:
    在这里插入图片描述
    但是并不是所有时候,都能实现键和索引一一对应的。
    若是存身份证号码,总不可能开18位数的空间去存每个身份证号码,所以我们想到的解决方式是通过一种人为约定的规则去把每个键放到对应的索引里,这样每个索引里放多个键,可以少开一点空间。

    这里设计一个人为约定的规则就是设计一个哈希函数,
    把每个键放到对应的索引里,我们怎么在同一个索引里找到相应的键,这就叫产生了哈希冲突 ,

    我们要做的事情就是解决两个问题,索引如何设计(哈希函数的设计)以及解决哈希冲突(即由键转换的索引与之前的相同)。
    在这里插入图片描述

    -------------------------------------------------------------------------------- 回到目录

    哈希函数的设计


    也就是设计索引的范围

    整型

    在这里插入图片描述
    在这里插入图片描述
    数组空间并不是开越大越好的,像上图一样,开4位就可以了,若是开6位,因为这是身份证,开6位的话前2位是生日的日期,只有可能是01-31,那么32-99都被浪费掉了,这就导致分布不均匀,所以我们要具体问题具体分析。

    那我们要怎么取模?
    如果随便模一个数的话容易导致分布不均匀。在这里我们可以模一个素数解决分布不均匀的情况(已有数学证明)。

    举个例子:
    在这里插入图片描述
    若是模合数,那只出现0和2,导致了分布不均匀;若是模素数,则分布变均匀了。

    那这个素数怎么取?
    对数据规模的不同取不同素数。
    已有人做出了一个表,在以下网址可查阅到。
    在这里插入图片描述
    该表的使用:给出一个数据的上界和下界,比如若数据在2的5次方到2的6次方之间的话,那么模的素数就可以选择53。

    浮点型数和字符串数

    浮点型数和字符串数都可以转换成整型处理:

    在这里插入图片描述
    在这里插入图片描述

    • 可以把字符串看作是26进制的数,然后来计算它的hash值;
    • B为抽象写法,表示B进制。

    在这里插入图片描述

    • 在运算的过程中,为了防止高次方的运算,可以利用多项式的拆解来处理提高运算效率;
    • 为了防止大整数的溢出,取模的时候我们每次运算一次就进行取模,和最后取模的效果是一样的;

    hash(code) 代码的表示:
    在这里插入图片描述

    复合类型

    和字符串处理一样,对于字符串来说,可理解为多个字符组成的复合类型。
    在这里插入图片描述

    哈希函数设计原则:

    在这里插入图片描述

    -------------------------------------------------------------------------------- 回到目录

    重写 hashCode() 和 equals() 方法


    我们可以使用 java 自带的 hashCode() ,但要通过重写 hashCode() 来计算我们的 hash 值。

    因为如果没有重写 hashCode() 的话,默认是根据每个对象的地址把它映射成整型。
    所以虽然是同样的数据,但是两次 new 会产生两个对象,指向不一样的地址。
    如果重写了,只要数据相同,不管怎么 new 都是同一个值。

    由于不能仅仅只按照 hashCode 来比较两个对象是否相同,所以就要重写 equals 方法,自己写的 hashCode 只是计算 hash 函数的值,但是产生 hash 冲突的时候(虽然 hash 函数值相等),还是要比较两个不同的对象是否相等 。(equals 在产生哈希冲突的时候,用来区分两个类对象的不同)

    例如下面的 Student 类,我们计算 hash 的值的方法如下:

    public class Student {
    
        private int grade;//年级
        private int cls; //班级
        private String firstName;
        private String lastName;
    
        public Student(int grade, int cls, String firstName, String lastName) {
            this.grade = grade;
            this.cls = cls;
            this.firstName = firstName;
            this.lastName = lastName;
        }
    
    
        //复合类型重写 Object类中的hashCode()方法
        // Object类中已经写了,是通过地址比较的 
        @Override
        public int hashCode() {
            int hash = 0;
            int B = 31; //这里B进制为31进制
    
            hash = hash*B + grade;
            hash = hash*B + cls;
            hash = hash*B + firstName.toLowerCase().hashCode();
            hash = hash*B + lastName.toLowerCase().hashCode();
    
            return hash;
        }
    
        @Override
        public boolean equals(Object obj) {
            if(this == obj){
                return true;
            }
            if(obj == null){
                return false;
            }
            if(getClass() != obj.getClass()){
                return false;
            }
    
            Student another = (Student)obj;
            return this.grade == another.grade &&
                    this.cls == another.cls &&
                    this.firstName.toLowerCase().equals(another.firstName.toLowerCase()) &&
                    this.lastName.toLowerCase().equals(another.lastName.toLowerCase());
        }
    }
    

    相关测试:

    public class HashCodeTest {
    
        public static void main(String[] args) {
    
            /**
             * 测试各个类型的hashCode() 都是使用一个整数映射
             */
            int a = 42;
            System.out.println(((Integer)a).hashCode());	//42
    
            int b = -42;
            System.out.println(((Integer)b).hashCode());	//-42
    
            double c = 3.1415926;
            System.out.println(((Double)c).hashCode());		//219937201
    
    
            System.out.println(Integer.MAX_VALUE + 1);	//-2147483648
    
            Student student = new Student(3, 2, "xinxin", "zheng");
            System.out.println(student.hashCode());	//-1941420881
    
            Student student2 = new Student(3, 2, "xinxin", "zheng");
            System.out.println(student2.hashCode());	//-1941420881
    
            System.out.println(student.hashCode() == student2.hashCode()); //true
            System.out.println(student == student2); //false
    
        }
    }
    

    -------------------------------------------------------------------------------- 回到目录

    使用数组+红黑树实现 HashMap


    • 数组的里面是红黑树实现。
    • 因为JDK中的红黑树使用的 TreeMap 实现,所以这里直接使用 TreeMap 当做红黑树使用。
      在这里插入图片描述
    public class MyHashMap<K extends Comparable<K>,V> {
    
    	//素数表
        private final int[] capacity = {
                53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593,
                49157, 98317, 196613, 393241, 786433, 1572869, 3145739, 6291469,
                12582917, 25165843, 50331653, 100663319, 201326611, 402653189, 805306457, 1610612741
        };
    
        private static final int upperTol = 10; /**每一个TreeMap内部超过这个就要扩容 --> size >= upperTol * M */
        private static final int lowerTol = 2; /** 每一个TreeMap内部小于这个就要缩容 --> size < lowerTol * M */
        private int capacityIndex = 0; /**这个是容量数组的下标,一开始是capacity[0]的容量*/
    
        private TreeMap<K,V>[] hashtable;/** hash数组,每一个数组对应的都是一棵红黑树 */
        private int size; /**总的元素个数*/
        private int M; /**数组大小*/
    
        public MyHashMap(){
            this.M = capacity[capacityIndex];//一开始大小为53
            size = 0;
            hashtable = new TreeMap[M];
            for(int i = 0; i < M; i++) 
            	hashtable[i] = new TreeMap<>();
        }
    
        public int size(){
            return size;
        }
    
        /** 计算hash值(也就是对应数组的索引)  使用hashCode % M 的方法  注意hashCode()要取绝对值*/
        private int hash(K key){
            return (key.hashCode() & 0x7fffffff) % M;	//取绝对值的写法
        }
    
        /**  add */
        public void put(K key,V value){
            TreeMap<K,V>map = hashtable[hash(key)]; //找到对应的数组index
            if(map.containsKey(key)){
                map.put(key,value);
            }else {
                map.put(key,value);
                size++;
    
                /**判断是否要扩容 */
                if(size >= upperTol * M && capacityIndex + 1 < capacity.length) {//需要扩容且可以扩容
                    capacityIndex++;
                    resize(capacity[capacityIndex]); //扩容到容量数组的下一个值
                }
            }
        }
    
        public V remove(K key){
            V ret = null;
            TreeMap<K,V>map = hashtable[hash(key)];
    
            if(map.containsKey(key)){
                ret = map.remove(key);
                size--;
    
                if(size < lowerTol * M && capacityIndex - 1 >= 0){
                    capacityIndex--;
                    resize(capacity[capacityIndex]);
                }
            }
            return ret;
        }
    
        private void resize(int newM) {
            TreeMap<K,V>[] newHashtable = new TreeMap[newM];
            for(int i = 0; i < newM; i++)
                newHashtable[i] = new TreeMap<>();
            int oldM = this.M;
            this.M = newM;
            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;
        }
    
        // 相当于put
        public void set(K key,V value){
            TreeMap<K,V>map = hashtable[hash(key)];
            if(!map.containsKey(key))
                throw new IllegalArgumentException(key + "doesn't exist!");
            map.put(key,value);
        }
        
        public boolean contains(K key){
            return hashtable[hash(key)].containsKey(key);
        }
        public V get(K key){
            return hashtable[hash(key)].get(key);
        }
    }
    

    注意:

    • capacity 数组是用来 resize(扩容,缩容) 的时候使用的数组,因为我们上面说过,M要设计成素数会更好的均匀分布;
    • upperTol 和 lowerTol 表示平均 TreeMap 数组内的容量达到这两个容量的时候就进行扩容或者缩容;
    • java 中的 hashCode 取出来的结果有可能是负值,而数组从0开始,对应不了负值,所以要去符号。
      • 去符号方法:
        16进制法中,每一位表示4个bit,所以这个数有31位1,而最高位是符号位。
        对31位的1进行按位与,这样前面的符号和0与完后就变成0。
        (key.hashCode() & 0x7fffffff) % M; 其实就是Math.abs(key.hashCode()) % M;
    • resize()函数中的 int oldM = this.M; this.M = newM; 使用oldM来保存之前的M的做法是为了在下面求hash(key)求的是新的hash函数的值,不是旧的hash的值,这点很容易忽视;

    -------------------------------------------------------------------------------- 回到目录

    使用数组+链表实现 HashMap


    import java.util.LinkedList;
    /**
     * 自定义map的升级版,查询效率较高
     * map底层实现 : 数组+链表
     */
    public class LinkHashMap<K,V> {
    
        private class Node{
            public K key;
            public V value;
    
            public Node(K key, V value) {
                this.key = key;
                this.value = value;
            }
        }
    
        private final int[] capacity
                = {53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593,
                49157, 98317, 196613, 393241, 786433, 1572869, 3145739, 6291469,
                12582917, 25165843, 50331653, 100663319, 201326611, 402653189, 805306457, 1610612741};	//素数表,可以保持每次resize后M都是素数
    
        private static final int upperTol = 10;
        private static final int lowerTol = 2;
        private int capacityIndex = 0;
    
        private LinkedList<Node>[] linkedLists;
        private int size;
        private int M;
    
        public int size() {
            return size;
        }
    
    
        public LinkHashMap() {
            this.M = capacity[capacityIndex];
            size = 0;
            linkedLists = new LinkedList[M];
            for(int i = 0; i < M; i++)
                linkedLists[i] = new LinkedList<>();
        }
    
        private int hash(K key){
            return (key.hashCode() & 0x7fffffff) % M;
        }
    
    
        public void put(K key, V value) {
            Node node = new Node(key, value);
            int hash = hash(key);
            LinkedList<Node>list = linkedLists[hash];
            if (list == null) {
                list = new LinkedList<>();
                linkedLists[hash] = list;
                list.add(node);
            } else {
                Node node2 = null;
                for (int i = 0; i < list.size(); i++) {
                    node2 = list.get(i);
                    if (node2.key.equals(key)) {
                        node2.value = value;
                        return;
                    }
                }
                linkedLists[hash].add(node);
            }
            size++;
            if(size >= upperTol * M && capacityIndex + 1 < capacity.length){
                capacityIndex ++;
                resize(capacity[capacityIndex]);
            }
        }
    
        public V remove(K key) {
            int hash = hash(key);
            V ret = null;
            LinkedList<Node>list = linkedLists[hash];
    
            if(list != null){
                Node node2 = null;
                for(int i = 0; i < list.size(); i++){
                    node2 = list.get(i);
                    if(node2.key.equals(key)){
                        ret = node2.value;
                        list.remove(i);// list.remove(node2);
                        size--;
                        //resize
                        if(size < lowerTol * M && capacityIndex - 1 >= 0){
                            capacityIndex --;
                            resize(capacity[capacityIndex]);
                        }
                        return ret;
                    }
                }
            }
            return null;
        }
    
        private void resize(int newM) {
            LinkedList<Node>[]newLinkedLists = new LinkedList[newM];
            for(int i = 0; i < newM; i++)
                newLinkedLists[i] = new LinkedList<>();
            int oldM = this.M;
            this.M = newM;	//写这句的目的是,下面的newLinkedLists[hash(node.key)].add(node)中的hash函数,里面是对M取模的,如果不赋值要出大事
            Node node = null;
            for(int i = 0; i < oldM; i++){
                LinkedList<Node>list = linkedLists[i];
                for(int j = 0; j < list.size(); j++){
                    node = list.get(j);
                    newLinkedLists[hash(node.key)].add(node);
                }
            }
            this.linkedLists = newLinkedLists;
        }
    
    
        public boolean contains(K key){
            int hash = hash(key);
            for(int i = 0; i < linkedLists[hash].size(); i++){
                if(linkedLists[hash].get(i).key.equals(key))
                    return true;
            }
            return false;
        }
        public V get(K key){
            int hash = hash(key);
            Node node = null;
            for(int i = 0; i < linkedLists[hash].size(); i++){
                node = linkedLists[hash].get(i);
                if(node.key.equals(key))
                    return node.value;
            }
            return null;
        }
        public void set(K key,V value){
            int hash = hash(key);
            LinkedList<Node>list = linkedLists[hash];
            if(list == null)
                throw new IllegalArgumentException(key + " doesn't exist!");
            Node node = null;
            for(int i = 0; i < list.size(); i++){
                node = list.get(i);
                if(node.key.equals(key)){
                    node.value = value;
                    return;
                }
            }
            throw new IllegalArgumentException(key + " doesn't exist!");
        }
    }
    
    

    现在问题来了,
    在这里插入图片描述
    因为M是固定的,而N是一直增大的,所以就不是O(1)。为了让M也能随之增大,我们让它动态扩容,才有了O(1)。
    在这里插入图片描述
    需要注意的是,哈希表的均摊复杂度为O(1),牺牲了有序性

    这也说明了若某些结构性能更优,就说明它牺牲了某些性质(比如缺失顺序性,没有寻址最大值、前驱等操作)或多了空间

    -------------------------------------------------------------------------------- 回到目录

    更多处理冲突的方法


    开放地址法

    • 线性探测: 遇到哈希冲突+1;
    • 平方探测: + 1 ,+ 4 ,+9,+16;
    • 二次hash:hash2(key);
      在这里插入图片描述
    展开全文
  • 底层结构2.1哈希概念2.2哈希函数2.3常见哈希函数2.4 哈希冲突3.闭散列(开放定址法)3.1线性探测3.2闭散列扩容-载荷因子3.3二次探测4.开散列4.1开散列增容 1.unordered_map/unordered_set unordered_map是存储<...

    1.unordered_map/unordered_set

    1. unordered_map是存储<key, value>键值对的关联式容器,其允许通过keys快速的索引到与其对应的value。
    2. 在unordered_map中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此键关联。键和映射值的类型可能不同。
    3. 在内部,unordered_map没有对<kye, value>按照任何特定的顺序排序, 为了能在常数范围内找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
    4. unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭代方面效率较低。

    unordered_map的元素访问:
    在这里插入图片描述

    2. 底层结构

    unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构

    2.1哈希概念

    哈希结构:可以不经过任何比较,一次直接从表中得到要搜索的元素。通过哈希函数(hashFunc) 使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

    当向该结构中插入元素:根据待插入元素的关键码,以 函数(hashFunc) 计算出该元素的存储位置并按此位置进行存放。

    搜索元素: 对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。

    该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)

    在这里插入图片描述

    2.3常见哈希函数

    1.直接定址法–(常用):

    • 取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B ;
      优点:简单、高效 ;
      缺点:需要事先知道关键字的分布情况 ;

    使用场景:适合查找比较小且连续的情况,比如:第一次出现的字符:构建一个数组hash[ch-‘a’] 来映射位置;
    不适用场景:数据分散,数据元素不连续,很容易造成空间浪费,比如 :一组数据最小的为1,最大的到了99999999;

    2.除留余数法(常用)

    设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数i作为除数,按照哈希函数:Hash(key) = key% i(p<=m), 将关键码转换成哈希地址;

    注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

    2.4 哈希冲突

    • 不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞

    解决哈希冲突两种常见的方法是:闭散列和开散列

    3.闭散列(开放定址法)

    当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。寻找下一个空位置的方法有线性探测法和二次探测法

    3.1线性探测

    • 从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止;

    • 插入:通过哈希函数获取待插入元素在哈希表中的位置
      在这里插入图片描述

    删除:

    采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素;
    在这里插入图片描述

    线性探测的缺点:空间踩踏

    3.2闭散列扩容-载荷因子

    在这里插入图片描述

    3.3二次探测

    在这里插入图片描述
    在这里插入图片描述
    因此:闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷;

    4.开散列

    开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

    4.1开散列增容

    开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容;

    • 载荷因子为1进行扩容;

    优点:
    - 不同位置冲突时,不再互相干扰,载荷因子一般控制在1

    缺点:
    - 迭代器遍历输出的时候,不是有序输出的;

    在这里插入图片描述

    如果所有的数据都冲突到一个桶下面了,怎么办?

    1.桶下面挂红黑树:极限也是logN的时间复杂度,但是也只是这一会而已,当增容的时候,这种现象就会缓解;
    2.多阶哈希

    展开全文
  • 哈希表 结构定义 typedef struct dictht{ //哈希表数组 dictEntry **table; //哈希表大小 unsigned long size; //哈希表大小掩码,用于计算索引值 unsigned long sizemask; //该哈希表已有节点的数量 ...

    哈希表

    结构定义

    typedef struct dictht{
    	//哈希表数组
        dictEntry **table;
        //哈希表大小
        unsigned long size;
        //哈希表大小掩码,用于计算索引值
        unsigned long sizemask;
        //该哈希表已有节点的数量
       unsigned long used ;
    }dictht 

    table 属性是一个数组 ,数组中的每个元素都是一个指向dict.h/dictEntry结构的指针,每个dictEntry结构都保存键值对。

    dictEntry数据结构

    typedef struct dictEntry {
    	//键
        void * key;
        //值
        union{
        void  * val;
        uint64_t u64;
        int64_t s64;
        }
        //指向下一个哈希表节点,形成链表,用来解决哈希冲突
        struct dictEntry *next;
    }dictEntry;

    * 键值对中的值可以是一个指向实际值的指针,或者是一个无符号的64位整数或有符号的64位整数或者double类的值,这样做的好处是可以节省内存空间,因为当值是整数或者浮点数时,可以将值的数据内嵌在dictEntry结构里面,无需在用一个指针指向实际的值,从而节省了内存空间。

    字典的数据结构

    typedef struct dict{
    	//类型特定函数
        dictType * type;
        //私有数据
        void * privdata;
        //哈希表,用来解决rehash
        dicth ht[2];
        //rehash索引,当rehash不在进行时,值为-1
        int trehashind ;      
    }

    type属性和privdata属性是针对不同类型的键值对,为了创建多态字典而设置的:

    • type属性指向dictType结构的指针,每个dictType结构保存了一簇用与操作特定类型键值对的函数,Redis会为了通途不同的字典设置不同的类型特定函数。
    • privdata属性则保存了需要传给那些类型特定函数的可选参数。
    typedf struct dictType{
    	//计算哈希值的函数
        unsigned int (*hashFunction)(const void *key);
        //复制键的函数
        void *(*keyDup)(void * privdata,const void *key);
        //复制值的函数
        void *(*valDup)(void * privdata,const void *obj);
        //对比键的函数
        int (*keyCompare)(void * privdata,const void * key1,const void * key2);
        //销毁键的函数
        void (*keyDestructor)(void *privdata,void *key);
        //销毁值的函数
        void (*valDestructor)(void *privdata,void *obj);  
    }dictType;
    • ht属性是一个包含两个项的数组,数组中的每个项都是一个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用。
    • rehashidx用于记录rehash目前的进度,如果目前没有进行rehash,那么值为-1、

    解决哈希冲突

    Reids哈希表使用链地址法来解决哈希冲。

    因为dictEntry节点组成的链表没有没有指向链表表尾的指针,所以为了速度考虑,程序总是将新节点添加到链表的表头位置,排在其他已有节点的前面

    REHASH

    为了让哈希表的负载因子维持在一个合理的范围内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行相应的扩展或者收缩。扩展和收缩哈希表的工作可以通过执行rehash(重新散列)操作来完成,Redis对字典的哈希表执行rehash的步骤

    1. 为字典的ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及ht[0]当前包含的键值对数量(可以通过ht[0].used属性的值得到)
      1. 如果执行的是扩展操作,那么ht[1]的大小为第一个大于等于ht[0].used*的2的n次方。
      2. 如果执行的是收缩操作,那么ht[1]的大小为第一个大于等于ht[0].used的2的n次方。
    1. 将保存在ht[0]中的所有键值对rehash到ht[1]上面:rehash指的是重新计算键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上。
    2. 当ht[0]包含的键值对都迁移到了ht[1]之后(ht[0]变成了空表),释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表,为下一次rehash做准备。

    哈希表的扩展与收缩

    当满足以下任意一个条件的时候,程序会自动开始对哈希表执行扩展操作:

    • 服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1.
    • 服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于5。

    根据BGSAVE命令或者BGREWRITEAOF命令是否正在执行,服务器执行扩展操作所需的负载因子并不相同,这是因为在执行BGSAVE命令或BGREWRITEAOF命令的过程中,Redis需要创建当前服务器的子进程,而大多数操作系统都采用写时复制技术来优化子进程的使用效率,所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,从而尽可能地避免在子进程存在期间进行哈希表扩展操作,这可以避免不必要的内存写入操作,最大限度地节约内存。

    渐进式rehash

    当哈希表里面存在的键值对过多的时候,要一次性将这些键值对全部rehash到ht[1]的话,庞大的计算量可能会导致服务器在一段时间内停止服务。因此为了避免rehash对服务器性能造成影响,服务器不是一次性将ht【0】里面的所有键值对全部rehash到ht【1】,而是分多次、渐进式地将ht【0】里面的键值对慢慢地rehash到ht【1】。

    1. 为ht【1】分配空间,让字典同时持有ht【0】和ht【1】两个哈希表。
    2. 在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehah工作正式开始。
    1. 在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashindx索引上的所有键值对rehahs到ht[1],当rehash工作完成之后,程序将rehashidx属性的值增一。
    2. 随着字典操作的不断进行,最终在某个时间点上,ht[0]的所有键值对都会全部被rehash至ht[1],这时程序将rehashidx属性的值设置为-1,表示rehash操作完成。

    渐进式rehash执行期间的哈希表操作

    在渐进式rehash进行期间,字典的删除、查找、更新等操作会在两个哈希表上进行。(程序会优先在ht[0]上进行操作,如果目标不存在ht[0]的话,就会继续到ht[1]里面进行查找)。

    在渐进式rehash执行期间,新添加到字典的键值对一律会被保存到ht[1]里面。而ht[0]则不在进行任何添加操作。

    展开全文
  • HashMap底层数据结构

    千次阅读 2020-12-23 19:22:14
    HashMap集合: 底层哈希表/散列表的数据结构 HashMap集合: 1、HashMap集合底层哈希表/散列表的数据结构。 2、哈希表是一个怎样的数据结构呢? 哈希表是一个数组和单向链表的结合体。 数组:在查询方面...
  • 哈希表 详解
  • Java数据结构-哈希表的实现(hash)

    千次阅读 多人点赞 2022-03-26 16:10:18
    我想给大家介绍的是哈希表,当我们学集合的时候一定会接触到hashtable,我们却不明白为什么要同时重写equals方法和hashCode方法,今天,我就带大家了解哈希表底层,让大家彻底弄懂哈希表.哈希表的概述 哈希表的创建思路 ...
  • 哈希表存储数据结构原理

    千次阅读 2018-03-31 20:59:48
    哈希表底层使用的也是数组机制,数组中也存放对象,而这些对象往数组中存放时的位置比较特殊,当需要把这些对象给数组中存放时,那么会根据这些对象的特有数据结合相应的算法,计算出这个对象在数组中的位置,然后把...
  • Python中常用的数据结构哈希表(字典) 常用的数据结构有数组、链表(一对一)、栈和队列、哈希表、树(一对多)、图(多对多)等结构。 在本目录下我们将讲解,通过python语言实现常用的数据结构。 4.哈希表 哈希表...
  • 从解决一个问题入手:大量的数据要存储查询,构造哈希表来解决 初步想法 借鉴数组下标访问的思路来做,只需知道起始位置和下标值, 不管数组中有多少个元素,都可以一次访问到, 将元素和元素位置建立一种一一...
  • 哈希表查找成功与查找失败的求法4.1 查找成功的求法4.2 查找失败的求法二、用泛型实现开散列与开散列在源码上的底层实现1.用泛型实现开散列2.开散列在源码上的底层实现2.1 HashMap底层面试题 一、哈希表 1.哈希表...
  • 【Java数据结构哈希表详解

    千次阅读 2021-12-30 09:27:43
    哈希表详解
  • 数据结构——HashMap底层实现

    千次阅读 2019-06-24 14:31:34
    如果没有研究过单列集合和双列集合底层实现的同学们,你万万没有想到HashMap的父亲是Map,但是在单列集合HashSet中竟然还有HashMap的身影。 HashSet是Collection的子类,但是点开HashSet底层我们可以看到...
  • 红黑树与哈希表的比较(数据结构!)

    千次阅读 2020-04-11 10:22:31
    而相对于哈希表这个数据结构来讲,哈希表的插入删除操作的理论上时间复杂度是常数时间的,这有个前提就是哈希表不发生数据碰撞。在发生碰撞的最坏的情况下,哈希表的插入和删除时间复杂度最坏能达到O(n)。而在一般...
  • 散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组...
  • 哈希表的实现

    2018-12-21 10:31:25
    hash的简单实现 数据结构初学 对于这里希望留下一些资源 期待大家批评指正
  • 【算法与数据结构 10】哈希表——高效查找的利器

    千次阅读 多人点赞 2020-09-24 17:03:21
    大家都在看的高效《数据结构与算法》!
  • 哈希表的扩容实现机制导语哈希表什么是哈希表装载因子 Hashcode哈 希冲突扩容方案 Java 中的实现 Redis 中的实现结束语 哈希表是实际开发中非常常用的数据结构也很重要了解其底层实 现细节也是非常重要的 我这篇...
  • 简单查找:O(n),相当于数据都是无序的,需要一个一个对比,看当前的数据是否是目标数据 二分查找:O(log2n),数据有序排列,每次将数据分为两半,提高查找效率 散列表:O(1),对于要查找的数据,用散列函数直接映射...
  • HashSet:底层数据结构哈希表

    千次阅读 2015-10-08 23:27:35
     * HashSet:底层数据结构哈希表  * HashSet是如何保证元素的唯一性呢?(ArrayList只依赖equals)  * 是通过元素的两个方法,hashCode和equeals来完成  * 如果元素的HashCode值相同,才会判断equals是否为...
  • Redis 数据结构哈希表 超实战追 - 女生技术 抠: .x. Redis 的字典底层使用哈希表实现说到哈希表大家应该能联想到 HashMap 或者是 Hashtable 也应该能联想到 key value 的存储形式 以及哈希表扩容哈希算法等知识点...
  • Redis 数据结构哈希表 超实战追 - 女生技术 抠: .x. Redis 的字典底层使用哈希表实现说到哈希表大家应该能联想到 HashMap 或者是 Hashtable 也应该能联想到 key value 的存储形式 以及哈希表扩容哈希算法等知识点...
  • Redis 数据结构哈希表

    千次阅读 2019-03-15 20:12:53
    Redis 的字典底层使用哈希表实现,说到哈希表大家应该能联想到 HashMap 或者是 Hashtable,也应该能联想到 key、value 的存储形式,以及哈希表扩容,哈希算法等知识点。那么 Redis 字典是否也是通过这种形式实现的呢...
  • Redis底层数据结构之Hash

    千次阅读 2020-12-08 21:17:23
    数据结构定义2.1字典dict2.2 哈希表ditcht2.3 真正的存储结构dictEntry3. 扩容和缩容4. rehash5. hash相关指令5.1hset/hsetnx5.2 hget5.3 exists5.4 hdel5.5 hlen5.6 hstrlen5.6 hincrby/hincrbyfloat5.7 hmset5.8 ...
  • 数据结构-哈希表理解与实现

    千次阅读 2018-01-23 18:07:59
    1、在我们C++11中的unordered_set/unordered_map以及unordered_multiset/unordered_multimap底层最主要的实现就是哈希表;而哈希表则是通过关键字(key)按照一定的规律映射到表中的位置。 2、最常用构造哈希表的...
  • 哈希表底层使用的也是数组机制,数组中也存放对象,而这些对象往数组中存放时的位置比较特殊,当需要把这些对象给数组中存放时,那么会根据这些对象的特有数据结合相应的算法,计算出这个对象在数组中的位置,然后把...
  • HashMap底层数据结构原理精讲

    千次阅读 2019-07-01 08:42:13
    HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。HashMap储存的是键值对,HashMap很快。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 HashMap实际...
  • 数据结构哈希表

    千次阅读 多人点赞 2021-04-28 08:09:45
    目录第一章 哈希表介绍第二章 哈希冲突第三章 哈希函数第四章 哈希表实现 项目地址:https://gitee.com/caochenlei/data-structures 第一章 哈希表介绍 设计一个写字楼通讯录,存放该写字楼所有公司的通讯信息,...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 87,415
精华内容 34,966
关键字:

哈希表的底层数据结构

数据结构 订阅