Map 和 Set、搜索树(二叉排序树)、哈希表(哈希桶)(详细)

目录

一、 Map

 1、 Map 的常用方法

 2、 Map.Entry< K, V > 

 3、 TreeMap 和 HashMap 

4、ConcurrentHashMap(多线程 推荐)

 二、 Set

1、 Set 的常用方法

2、 TreeSet 和 HashSet 

三、 搜索树(二叉排序树)

1、 自己实现内部方法

(1)查找

(2)插入

(3)删除

(4)整体代码

2、 性能分析

四、哈希表

1、冲突

2、冲突 —— 避免

(1)哈希函数设计

(2)调节负载因子

3、 冲突 —— 解决

(1)闭散列

(2)开散列(哈希桶)

 4、* 自己实现一个哈希桶 *

(1)key 是可比较的类型(int)

(2)key 是自定义类的对象(person)

五、HashMap 和 HashSet 中的哈希表


   Map 和 set 是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。 

   在搜索的过程中国,一般把搜索的数据称为关键字(Key),和关键字对应的称为值(Value),将其称之为Key-value的键值对,所以模型有两种:

1. 纯 key 模型 (在英文字典中查找英文单词)
2. Key-Value 模型 (统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数:<单词,单词出现的次数>)
而Map中存储的就是key-value的键值对,Set中只存储了Key。

一、 Map

    

    Map是一个接口,并且由上图可以看出来,Map并没有继承自 Collection。

 1、 Map 的常用方法

方法解释
V  get (Object key)返回 key 对应的 value
V  getOrDefault (Object key, V defaultValue)返回 key 对应的 value,若 key 不存在,返回默认值
V  put (K key, V value)设置 key 对应 的 value
V  remove (Obgect key)

删除 key 对应的映射关系

Set <K>  keySet ( )返回所有  key  的不重复集合  (返回值是一个 set)
Collection < V >  values ( )返回所有 value 的可重复集合
Set < Map.Entry < K, V >>  entrySet ( )返回所有的 key-value 映射关系
boolean  containsKey (Object key)判断是否包含 key
boolean  containsValue (Object value)判断是否包含 value 

    在使用时需要注意

(1) Map 是一个接口,因此我们使用这些方法时,要通过 HashMap 或者 TreeMap 来使用。

(2) Map 中存放的键值对的 key 是唯一的,value 是可以重复的。(key相同时,存放value会进行覆盖)。

(3) Map 中的 key 可以全部分离出来,存储在 Set 中;(key 不能重复)

(4) Map 中的 value 可以全部分离出来,存储在 Collection 的任何一个子集合中。(value 可以重复)

(5) Map 中的 key 不能直接修改。如果要进行修改,必须先删除,然后再重新插入。

 2、 Map.Entry< K, V > 

   Map.Entry< K, V > 是 Map 内部实现的用来存放 < key, value >键值对 的 内部类。正因为他存的是 Map 中的键值对,所以我们可以根据其内部的方法来对 key 或 value 进行获取。

方法解释
K  getKey ( )返回 entry 中的 key
V  getValue ( )返回 entry 中的 value
V  setValue (V value)将键值对中的 value 替换为指定的 value

  我们可以这样通俗的认为:每一个 entry 都存储了一个键值对

    而对于我们上面的Map 中的方法 Set < Map.Entry < K, V >>  entrySet ( )   获取映射关系,返回值是一个Set,而 Set 中存储的对象,是一个个键值对。

 3、 TreeMap 和 HashMap 

   TreeMap 和 HashMap 的最本质区别是底层结构的实现: TreeMap是红黑树,而HashMap是哈希桶。

Map 

TreeMap

HashMap
底层结构红黑树哈希桶

插入/删除/查找 

时间复杂度

O( log(N) )

O(1)

是否有序关于 key 有序无序
线程安全不安全不安全
插入/删除/查找  区别需要进行元素比较通过哈希函数计算哈希地址
比较 与 覆写 

key 必须能够比较

否则会抛出ClassCastException异常

自定义类需要 覆写 

equals 方法 和

hashCode 方法

应用场景需要 key 有序的场景下key 是否有序不关心,关心是否需要更高的时间性能

   由上面的比较可以发现,HashMap 增删查改 的时间复杂度是 O(1),是非常高效的。因为他在插入元素的过程中是通过哈希函数来进行计算然后插入的。因此在查找元素时,不会对所有元素进行遍历,而是再通过这个哈希函数来找到要查找的元素。

   因此我们通常使用的是 HashMap,并且如果传入的 key-value 中的类型是自定义类型的话,一定要重写 equals 方法和 hashCode 方法。

4、ConcurrentHashMap(多线程 推荐)

   ① ConcurrentHashMap 是线程安全的 HashMap。

   ② ConcurrentHashMap 减少了锁冲突,就让锁加到每个链表的头结点上(锁桶);

   ③ ConcurrentHashMap 只是针对 写操作 加锁了,读操作并没有加锁,而只是使用;

   ④ ConcurrentHashMap 中更广泛的使用CAS,进一步提高效率,(比如维护 size 操作);

   ⑤ ConcurrentHashMap 针对扩容,进行了巧妙的化整为零:

       对于HashTable 来说,只要你这次 put 触发了扩容,就会一口气搬运完,就会导致这次的 put 非常卡顿。

     对于CocurrentHashMap,每次操作只搬运一点点,通过多次操作完成整个搬运过程。同时维护一个新的 HashMap 和一个 旧的HashMap,查找的时候即需要查旧的也要查新的,而插入的时候就只插入到新的HashMap 中。直到全部搬运完在销毁旧的。

 二、 Set

   Set 与 Map 主要的不同之处有两点:① Set 是继承自 Collection 的接口类;② Set 中只存储了 key。

1、 Set 的常用方法

方法解释
boolean  add (E e)

添加元素,但重复的元素不会被添加成功

void  clear ( )

清空集合

boolean  contains (Object o)判断 o 是否在集合中
Iterator < E >  iterato ( )

返回一个迭代器

boolean  remove (Object o)删除集合中的 o
int  size ( )返回 set 中元素的个数
boolean  isEmpty ( )

检测 set 是否为空,

空返回true,否则返回 false

Object []  toArray ( ) 将 set 中的元素转换为 数组 返回
boolean  containsAll (Collection<?> c)

检测 集合 c 中的元素是否在 set 总全部存在,

是返回 true,否则返回 false

boolean  addAll (Collection < ? extrnds E >  c)将集合 c 中的元素添加到 set 中,可以达到去重效果

   在使用时要注意

(1) Set 最大的功能:去重

(2) Set 是继承自 Collection 的一个接口类。使用这些方法需要使用实现这一接口的类:TreeSet HashSet(常用类)。还有一个类 LinkedHashSet,它是在 HashSet的基础上维护了一个双向链表来记录元素的插入顺序的。(HashSet 中 存储时无序的)

(3) Set 的底层是使用 Map 来实现的,其使用 key 与 Object 的一个默认对象,作为键值对插入到 Map 中的。

(4) Set 中 的 key 不能修改,要修改,就得先删除原来的,再重新插入。

(5) Set 中不能插入 null。

2、 TreeSet 和 HashSet 

SetTreeSetHashSet
底层结构红黑树哈希桶

插入/删除/查找

时间复杂度

O( log(N) )O(1)
是否有序关于 key 有序不一定有序
线程安全不安全不安全

插入/删除/查找

区别

按照红黑树的特性来进行插入和删除

1、先计算 key 哈希地址

2、再进行插入和删除

比较 与 覆写

key 必须能比较

否则会抛出 ClassCastException异常

自定义类型需要覆写

equals 方法 和

hashCode 方法

应用场景需要 key 有序的场景下key 不要求有序,关心更高的时间性能的情况

   与 Map 类似,我们更常用的是 HashSet

三、 搜索树(二叉排序树)

   搜索树又叫二叉排序树,它是一棵树,并且具有以下性质:

  • 若它的左子树不为空,则左子树上所有结点的值都小于根结点的值;
  • 若它的右子树不为空,则右子树上所有结点的值都大于根结点的值;
  • 它的左右子树也分别为二叉搜索树。

    如图所示就是一个二叉搜索树。

1、 自己实现内部方法

   首先定义一个结点类:

//结点
class Node {
    public int value;
    public Node left;
    public Node right;

    public Node(int val, Node left, Node right) {
        this.value = val;
        this.left = left;
        this.right = right;
    }

    public Node(int val) {
        this.value = val;
        this.left = left;
        this.right = right;
    }
}

(1)查找

   查找操作较为简单,因为二叉搜索树本质上是一棵树,所以我们只需要遍历树中的每个结点,判断其值是否和目标值相等即可。

    //查找
    public boolean search(int key) {
        Node cur = root;
        while(cur != null) {
            if (cur.value < key) {
                cur = cur.right;
            } else if(cur.value > key) {
                cur = cur.left;
            } else {
                return true;
            }
        }
        return false;
    }

(2)插入

   思路:考虑树是否为 空树,若为空树,直接插入即可;若不是空树,进行接下来的操作。比较 目标元素值 和 当前结点值 的大小,若目标值 小于 当前结点值,当前结点指向当前结点的左孩子进行判断;当目标值 大于 当前结点值,当前结点指向当前结点的右孩子继续进行判断;当目标值 等于 当前结点值时,是不合法的。一直这样循环判断。

   判断到最后当前结点指向空了,此时要插入,没有指向其父母结点的变量。因此我们要定义一个parent 结点,指向当前结点的父母结点,每更新一次指向当前结点的引用值,就更新一个parent的值。

   最后根据目标元素和parent引用指向的结点的大小比较,确定插到左孩子还是右孩子上。

   因此:对于二叉查找树来说,插入的元素一定在叶子结点上。

  • cur 和 parent 来找到 value 需要存储的位置
  • praent.value 和 value 比较大小,确定格式在左边还是在右边进行插入
    //插入
    //对于二叉搜索树来说,插入的数据一定在叶子结点
    public boolean insert(int val) {
        if(root == null) {
            root = new Node(val);
            return true;
        }
        //树不为空
        Node p = root;
        Node cur = root;
        while(cur != null) {
            if (val > cur.value) {
                p = cur;
                cur = cur.right;
            } else if (val < cur.value) {
                p = cur;
                cur = cur.left;
            } else {
                //不能插入相同的数据
                return false;
            }
        }
        Node node = new Node(val);
        if (val > p.value) {
            p.right = node;
        } else {
            p.left = node;
        }
        return true;
    }

(3)删除

   删除的操作较为复杂,需要考虑很多种情况。假设要删除的结点为 cur,其双亲结点为 parent。

① 当 cur.left == null 时,即要删除的结点没有左孩子时:

   a、 cur == root,则 root = cur.right

   b、 cur != root,cur == parent.left,则 parent.left = cur.right

   c、 cur != root,cur == parent.right,则 parent.right = cur.right

② 当 cur.right == null 时,即要删除的结点没有右孩子时:

   a、 cur == root,则 root = cur.left

   b、 cur != root,cur == parent.left,则 parent.left = cur.left

   c、 cur != root,cur == parent.right,则 parent.right = cur.left

③ 当  cur.left != null  && cur.right != null 时,即要删除的结点既有左孩子又有右孩子时:

   使用替换法: 寻找替罪羊,在 cur 的左子树中找最大值 或者 在右子树中找最小值。找到之后将其元素值和 cur 的值进行交换,然后删除这个被交换的元素结点即可。(删除后的一系列调整操作还是上面的这些情况)

   注意:在找最大值(最小的)的过程中,还需要一个 tar 引用以及 tarp引用,tar 指向当前结点,tarp 指向其双亲结点。因此最后还需要判断 tar 是 tarp 的左孩子还是右孩子,然后再进行引用的转换。

    //删除
    //通过两个方法
    //cur 为要删除的结点
    //比较麻烦:三种情况
    public boolean remove(int key) {
        Node cur = root;
        Node p = null;
        while(cur != null) {
            if (cur.value == key) {
                removeNode(cur,p);
                return true;
            } else if(key > cur.value) {
                p = cur;
                cur = cur.right;
            } else {
                p = cur;
                cur = cur.left;
            }
        }
        return false;
    }
    public void removeNode(Node cur, Node p) {
        if (cur.left == null) {
            if (cur == root) {
                root = cur.right;
            } else if (cur == p.left) {
                p.left = cur.right;
            } else {
                //cur == p.right
                p.right = cur.right;
            }
        } else if (cur.right == null) {
            if (cur == root) {
                root = cur.left;
            } else if (cur == p.left) {
                p.left = cur.left;
            } else {
                //cur === p.right
                p.right = cur.left;
            }
        } else {
            //cur 既有左子树又有右子树
            //找替罪羊替换:
            //在左子树中找最大值,或者在右子树中找最小值(肯定是叶子)
            //这里找的是右子树(right)的最大值(left)
            Node tar = cur.right;
            Node tarp = cur;
            while(tar.left != null) {
                tarp = tar;
                tar = tar.left;
            }
            //循环结束,此时 tar 指向目标结点(tar 的左子树为空)
            //替换
            cur.value = tar.value;
            if (tarp.left == tar) {
                //说明 tar 是 tarp 的左孩子
                tarp.left = tar.right;
            } else {
                //tar 是 tarp 的右孩子
                tarp.right = tar.right;
            }
        }
    }

(4)整体代码

//结点
class Node {
    public int value;
    public Node left;
    public Node right;

    public Node(int val, Node left, Node right) {
        this.value = val;
        this.left = left;
        this.right = right;
    }

    public Node(int val) {
        this.value = val;
        this.left = left;
        this.right = right;
    }

    @Override
    public String toString() {
        return "value: " + value;
    }
}

public class BinarySearchTree {
    public Node root = null;
    //查找
    public boolean search(int key) {
        Node cur = root;
        while(cur != null) {
            if (cur.value < key) {
                cur = cur.right;
            } else if(cur.value > key) {
                cur = cur.left;
            } else {
                return true;
            }
        }
        return false;
    }
    //插入
    //对于二叉搜索树来说,插入的数据一定在叶子结点
    public boolean insert(int val) {
        if(root == null) {
            root = new Node(val);
            return true;
        }
        //树不为空
        Node p = root;
        Node cur = root;
        while(cur != null) {
            if (val > cur.value) {
                p = cur;
                cur = cur.right;
            } else if (val < cur.value) {
                p = cur;
                cur = cur.left;
            } else {
                //不能插入相同的数据
                return false;
            }
        }
        Node node = new Node(val);
        if (val > p.value) {
            p.right = node;
        } else {
            p.left = node;
        }
        return true;
    }
    //删除
    //通过两个方法
    //cur 为要删除的结点
    //比较麻烦:三种情况
    public boolean remove(int key) {
        Node cur = root;
        Node p = null;
        while(cur != null) {
            if (cur.value == key) {
                removeNode(cur,p);
                return true;
            } else if(key > cur.value) {
                p = cur;
                cur = cur.right;
            } else {
                p = cur;
                cur = cur.left;
            }
        }
        return false;
    }
    public void removeNode(Node cur, Node p) {
        if (cur.left == null) {
            if (cur == root) {
                root = cur.right;
            } else if (cur == p.left) {
                p.left = cur.right;
            } else {
                //cur == p.right
                p.right = cur.right;
            }
        } else if (cur.right == null) {
            if (cur == root) {
                root = cur.left;
            } else if (cur == p.left) {
                p.left = cur.left;
            } else {
                //cur === p.right
                p.right = cur.left;
            }
        } else {
            //cur 既有左子树又有右子树
            //找替罪羊替换:
            //在左子树中找最大值,或者在右子树中找最小值(肯定是叶子)
            //这里找的是右子树(right)的最大值(left)
            Node tar = cur.right;
            Node tarp = cur;
            while(tar.left != null) {
                tarp = tar;
                tar = tar.left;
            }
            //循环结束,此时 tar 指向目标结点(tar 的左子树为空)
            //替换
            cur.value = tar.value;
            if (tarp.left == tar) {
                //说明 tar 是 tarp 的左孩子
                tarp.left = tar.right;
            } else {
                //tar 是 tarp 的右孩子
                tarp.right = tar.right;
            }
        }
    }
    //中序遍历
    public void inOrder(Node root) {
        if (root == null) {
            return;
        }
        inOrder(root.left);
        System.out.print(root.value + " ");
        inOrder(root.right);
    }


    //测试
    public static void main(String[] args) {
        int[] arr = {23,4,76,9,46,98,1,6};
        BinarySearchTree binarySearchTree = new BinarySearchTree();
        for (int i : arr) {
            binarySearchTree.insert(i);
        }
        binarySearchTree.inOrder(binarySearchTree.root);
        System.out.println("查找:");
        System.out.println(binarySearchTree.search(4));
        System.out.println(binarySearchTree.search(5));
        System.out.println("删除:");
        System.out.println(binarySearchTree.remove(4));
        System.out.println(binarySearchTree.remove(10));
    }
}

2、 性能分析

   因为插入和删除操作都必须先进性查找,因此查找效率就代表的二叉搜索树中各个操作的性能。而对于有 n 个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,比较次数越多。

  •    最优情况下,二叉搜索树为 完全二叉树,其平均次数为:log(N)
  •    最差情况下,二叉搜索树 退化成 单支树,其平均次数为:N/2       (性能大大降低)

   因此,对于 TreeMap 和 TreeSet 来说,实际上是红黑树,而红黑树是一棵近似平衡的二叉搜索树,即在二叉搜索树的基础上+颜色以及红黑树性质

四、哈希表

 引入哈希表的概念:

   之前学习的顺序结构以及平衡树中,元素和其存储位置没有任何对应关系,因此我们在查找某个元素时,就得经过大量的关键字的比较。这样的查找时间复杂度就为 O(N),平衡树中就为树的高度,即 O( log(N) )。可以得出搜索的效率取决于关键字比较的次数

   因此我们想要提高查找的效率,就想可以不经过任何比较,一次性就可以找到我们要找的元素。

   为此,如果构造一种存储结构,通过某种函数将元素和其存储位置之间建立一一对应的联系,根据这样的关系进行插入元素。当我们要查找这个元素时,再通过这个函数就可以不经过任何关键字比较,就可以找到这个元素了。效率将会大大提高。

    这样的方法即为 哈希方法(散列方法),哈希方法中使用的转换函数 为 哈希函数(散列函数),构造出来的结构 为 哈希表(散列表)。 

   注意:这里的哈希函数并不是唯一确定的,而是人为设置的。

1、冲突

   下面用一个例子来解释什么叫冲突。

   假设我们有一个集合:{2,5,7,10,6,15},并且人为设置的哈希函数:hash( key ) = key / capacity  ;capacity 为存储元素底层空间大小(这里为数组长度)。

    冲突:不同关键字通过相同的哈希函数计算出相同的哈希地址,这种现象称为 哈希冲突(哈希碰撞)

2、冲突 —— 避免

   冲突的发生时必然的,我们能做的只有尽量降低冲突率。有两个方面原因引起哈希冲突:

  • 1、哈希函数设计
  • 2、负载因子

(1)哈希函数设计

   引起哈希冲突的一个原因就是:哈希函数的设计不够合理,计算出来的哈希地址重复过多。

哈希函数的设计原则

  • 哈希函数的定义域必须包含需要存储的全部关键码。
  • 哈希函数计算出来的地址能均匀的分布在整个空间。
  • 哈希函数应该比较简单。

常见的哈希函数:

  • 1、直接定值法:直接取关键字任意的某个 线性函数 为散列地址。优点:简单、均匀分布;缺点:需要事先知道关键字的分布场景;使用场景:查找比较小且连续的情况。
  • 2、除留余数法:设散列表中允许的地址数为m,取一个不大于 m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash( key ) = key % p (p <= m),将关键字转换成哈希地址。
  • 3、平方取中法、折叠法、随机数法、数学分析法........ 

(2)调节负载因子

   散列表的负载因子 α :  α = 散列表中的元素个数 / 散列表的长度 。

    因此,我们正常的负载因子大概在 0.75,当负载因子过大时,我们就需要扩容了。

3、 冲突 —— 解决

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

(1)闭散列

   闭散列 也称为 开放地址法:当发生哈希冲突时,若哈希表没有被装满,就将 key 存放到冲突位置的下一个空位置中。有两种找空位置的方法:线性探测、二次探测。

  • 线性探测:从发生冲突的位置开始,一次向后探测,直到找到下一个空位置为止。
  • 二次探测:找下一个空位置的方法为:Hi = (H0 + i²)% m,其中 i = 1,2,3......,H0是通过散列函数 Hash( x ) 对元素的关键码 key进行计算得到的值,m是表的大小。
  • 注意:在搜索时可以不考虑表装满的情况,但是在插入时必须确保表的  装载因子 a 不超过0.5,如果超过必须考虑扩容。

(2)开散列(哈希桶)

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

   类似这种情况:

      开散列中每个桶中放的都是发生哈徐冲突的元素。可以认为是把一个大集合中的搜索问题转化为在小集合中做搜索。

   当冲突还没有办法解决时,我们就需要继续转化:

  • 每个桶背后都是另一个哈希表
  • 每个桶背后都是一棵搜索树(红黑树)...

 4、* 自己实现一个哈希桶 *

   要实现 插入以及查找两个方法,以及考虑负载因子、扩容的情况:

(1)key 是可比较的类型(int)

//自己实现哈希表
//实现 插入和查找两个方法
public class HashBuck {
    //通过内部类来定义结点
    public class Node {
        public int key;
        public int value;
        public Node next;

        public Node(int key, int value) {
            this.key = key;
            this.value = value;
            this.next = null;
        }
    }

    private Node[] array;
    //记录元素个数
    private int usedSize;
    //标准负载因子
    private static final double DEFAULT_LOAD_FACTOR = 0.75;

    //构造方法
    //哈希表的初始容量人为设置为 10
    public HashBuck() {
        this.array = new Node[10];
    }

    //put方法:插入 key-value键值对
    //思路:
    //1.找到 key 的位置
    //2.遍历链表,判断是否已经有 key 了,若有则进行覆盖
    //3.没有 key 值,头插法(或者尾插法),将结点插入
    //4.元素插入成功之后,检查当前散列表的负载因子,若超过就进行扩容
    public void put(int key,int value){
        //1.找位置
        //我们这里自己设置的散列函数
        int index = key % this.array.length;
        Node node = new Node(key, value);
        //2.遍历链表,判断
        Node cur = array[index];
        while(cur != null) {
            if (cur.key == key) {
                cur.value = value;
                return;
            }
            cur = cur.next;
        }
        //3.头插法
        node.next = cur;
        array[index] = node;
        this.usedSize++;
        //4.判断负载因子:
        if(loadFactor() >= DEFAULT_LOAD_FACTOR) {
            resize();
        }
    }

    //求负载因子
    public double loadFactor() {
        return 1.0 * usedSize/array.length;
    }

    //扩容
    //将数组长度翻倍,并且对其中的元素的散列地址重新计算插入
    public void resize() {
        Node[] newArray = new Node[array.length * 2];
        for (int i = 0; i < array.length; i++) {
            Node cur = array[i];
            while(cur != null) {
                int index = cur.key % newArray.length;
                //此时需要将 cur 转移到新数组中:头插/尾插的形式插入到新的数组对应的下标的链表中
                //但是要考虑:如果 cur 的后面还有元素怎么办
                //如果一块转移过去的话,很可能后面这个元素的散列地址和 cur 元素的散列地址不同
                //因此我们需要将 cur.next 再进行存储
                Node curNext = cur.next;
                //头插
                cur.next = newArray[index];
                newArray[index] = cur;
                //头插结束,将 cur 指回原来的 cur.next
                cur = cur.next;
            }
        }
        array = newArray;
    }

    //get方法:通过 key 的值获取 value 值
    //1.找到 key 所在的位置
    //2.遍历该位置下的所有链表,看是否有 key值,若没有则返回 -1
    public int get(int key) {
        int index = key % this.array.length;
        Node cur = this.array[index];
        while(cur != null) {
            if (cur.key == key) {
                return cur.value;
            }
            cur = cur.next;
        }
        return -1;
    }

    //测试
    public static void main(String[] args) {
        HashBuck hashBuck = new HashBuck();
        hashBuck.put(1,6);
        hashBuck.put(2,7);
        hashBuck.put(5,8);
        hashBuck.put(6,9);
        hashBuck.put(9,1067);
        hashBuck.put(7,18);
        hashBuck.put(8,16);
        hashBuck.put(10,109);
        hashBuck.put(12,100);
        hashBuck.put(11,19);
        System.out.println(hashBuck.get(11));
    }

}

(2)key 是自定义类的对象(person)

   大致思路方法和上面类似,最主要的区别在于 put 方法 和 get 方法中,元素的处理方面。因此这里只写了二者不同的地方,负载因子和扩容都是一样的。

put 方法:

  • 1. 找到 key 的存放下标
  • 之前因为 key 是int类型,可以直接使用函数进行计算得到散列地址,但是我们这里的 key 是个泛型。就以自定义类 person 举例,并不能直接通过计算得到散列地址,因此我们要通过在person类中重写 hashCode方法来获得一个 hash 数值,然后再进行散列函数的计算得到散列地址。
  • 2. 遍历该下标下的所有链表结点,判断是否有 key,若有则进行替换,若没有则进行头插。
  • 在这个过程中,我们要不断的进行比较,因此我们的自定义类型也要具备比较能力,因此还要在自定义类中重写 equals方法

get 方法类似

//定义person类
//person 类中只有一个 ID。当 ID相同时,我们认为是同一个人
class Person{
    private String ID;

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return ID == person.ID;
    }

    @Override
    public int hashCode() {
        return Objects.hash(ID);
    }

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

//写一个 泛型 的哈希桶
public class HashBuck2< K, V > {
    //Node类定义为内部类
    static class Node<K, V> {
        public K key;
        public V value;
        public Node<K, V> next;

        public Node(K key, V value) {
            this.key = key;
            this.value = value;
        }
    }

    //开始定义哈希桶的属性
    //数组以及存放元素个数
    public Node<K,V>[] array =(Node<K,V>[]) new Node[10];
    public int uesdSized;

    //put 方法
    //1. 找到 key 的存放下标
    //之前因为 key 是int类型,可以直接使用函数进行计算得到散列地址,但是我们这里的 key 是个泛型
    //就以自定义类 person 举例,并不能直接通过计算得到散列地址,
    // 因此我们要通过在person类中重写 hashCode方法来获得一个 hash数值,然后再进行散列函数的计算得到散列地址。
    //2. 遍历该下标下的所有链表结点,判断是否有 key,若有则进行替换,若没有则进行头插
    //在这个过程中,我们要不断的进行比较,因此我们的自定义类型也要具备比较能力,因此还要在自定义类中重写 equals方法。
    public void put(K key, V value) {
        int hash = key.hashCode();
        int index = hash % array.length;
        Node<K,V> cur = array[index];
        while(cur != null) {
            if (cur.key.equals(key)) {
                cur.value = value;
                return;
            }
            cur = cur.next;
        }
        //循环结束,说明没有 key 这个值,进行头插法
        Node<K,V> node = new Node<>(key, value);
        array[index] = node;
        node.next = array[index];
        //头插成功,存入哈希桶的元素个数+1
        this.uesdSized++;
    }

    //get 方法
    //和 put 方法类似
    //1. 找到key的位置
    //2. 遍历对应位置下的链表,如果有 key 值,就返回,没有 key 值返回 null
    public V get(K key) {
        int hash = key.hashCode();
        int index = hash % array.length;
        Node<K,V> cur = array[index];
        while (cur != null) {
            if (cur.key.equals(key)) {
                return cur.value;
            }
            cur = cur.next;
        }
        return null;
    }

    //测试
    public static void main(String[] args) {
        Person person1 = new Person("123");
        Person person2 = new Person("123");
        HashBuck2<Person,String> hashBuck2 = new HashBuck2<>();
        hashBuck2.put(person1,"我");
        System.out.println(hashBuck2.get(person2));
    }
}

五、HashMap 和 HashSet 中的哈希表

   HashMap 和 HashSet 即是利用哈希桶实现的 Map 和 Set。在 HashMap 的源码内部:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值