HashMap学习笔记

原理

hash表是一种数据结构,它使用hash函数组织数据,以支持快速插入和搜索

其关键思想是使用hash函数将键映射到存储桶

  1. 当我们插入一个新的键是,hash函数将决定键分配到哪一个桶中,并将该键存储仔相应的桶中。
  2. 当我们搜索一个键时,hash表使用相同的hash函数来查找对应的桶,并只在特定的桶中进行搜索。

设计hash表的关键

hash函数

hash函数是hash表中最重要的组件,该hash表的用于将键映射到特定的桶。简单举例,我们使用 y= x % 5 作为散列函数,其中x是键值,y是分配的桶的索引。
散列函数将取决与键值的范围和桶的数量。

如何设计hash函数是一个开放的问题,思想时尽可能地将键分配到桶中,理想情况瞎,完美的hash函数是键和桶之间是一对一映射,然而大多数情况瞎,hash函数并不完美,需要在桶地数量和桶的容量之间进行权衡。

冲突解决

冲突解决算法应该解决以下几个问题:

  1. 如何组织在一个桶中的值?
  2. 如果同一个桶中分配了太多的值,怎么办?
  3. 如何在特定的桶中搜索目标值?

这些问题与桶的容量和可能映射到同一个桶的键的数目有关。

假设存储最大键数的桶有N个键,如果N是常数且很小,我们可以简单地使用一个数组将键存在同一个桶中。如果N是可变的或者很大,我们可能需要使用高度平衡的二叉树来代替。

训练

插入搜索是hash表中的两个基本操作,此外还有基于这两个操作的操作,当我们删除元素时,要先搜索元素,然后在元素存在的情况下从相应位置移除元素。

设计Hash集合

这里使用LinkedList数组来实现HashSet,并记录一个size属性。index是key%size后的索引,在单个LinkedList中,将key作为值存入,实现多个键存在一个桶里。相同的key当然是相同的值,不同的key在index一样的时候可以存进同一个桶,并且根据key区分,以实现一个桶多个键的效果。

class MyHashSet {
    private LinkedList[] lists;
    private final int size = 10000;

    /**
     * Initialize your data structure here.
     */
    public MyHashSet() {
        lists = new LinkedList[size];
    }

    public void add(int key) {
        int index = key % size;
        if (lists[index] == null) {
            lists[index] = new LinkedList();
        }
        if (!contains(key)) {
            lists[index].addFirst(key);
        }
    }

    public void remove(int key) {
        int index = key % size;
        if (lists[index] != null) {
            lists[index].remove((Integer) key);
        }
    }

    /**
     * Returns true if this set contains the specified element
     */
    public boolean contains(int key) {
        int index = key % size;
        return lists[index] != null && lists[index].contains(key);
    }
}

/**
 * Your MyHashSet object will be instantiated and called as such:
 * MyHashSet obj = new MyHashSet();
 * obj.add(key);
 * obj.remove(key);
 * boolean param_3 = obj.contains(key);
 */
设计HashMap

记录了Node数组、容量、当前大小以及负载因子。当size>=capacity * THERESHOD时扩容为原来的两倍。
为了方便理解代码,这里hash函数只是简单返回了自身,要了解更多可以查看HashMap源码的Hash方法。
这里的桶都是为了存储键,值是和键是一一对应的,只要考虑键和桶的关系就行。

class MyHashMap {
    Node[] arr;
    int capacity;
    int size;
    private static final double THERESHOD = 0.75;

    /**
     * Initialize your data structure here.
     */
    public MyHashMap() {
        capacity = 200000;
        arr = new Node[capacity];
        size = 0;
    }

    /**
     * value will always be non-negative.
     */
    public void put(int key, int value) {
        put(arr, key, value);
    }

    private void put(Node[] arr, int key, int value) {
        if (size > capacity * THERESHOD) {
            // 二倍扩容
            growCapacity();
        }
        int idx = hash(key) % capacity;
        // 使用二次hash 解决碰撞
        while (arr[idx] != null && arr[idx].key != key) {
            if (arr[idx].value == -1) {
                // 说明这个元素已经被remove了
                break;
            }
            idx = hash(idx) % capacity;
        }
        arr[idx] = new Node(key, value);
        size++;
    }

    private void growCapacity() {
        // 倍增后reHash放入即可
        capacity *= 2;
        Node[] newArr = new Node[capacity];
        reHash(newArr, arr);
        arr = newArr;
    }

    private void reHash(Node[] newArr, Node[] arr) {
        for (Node node : arr) {
            // 被删掉的应该被清除
            if (node != null && node.value != -1) {
                put(newArr, node.key, node.value);
            }
        }
    }

    /**
     * Returns the value to which the specified key is mapped, or -1 if this map contains no mapping for the key
     */
    public int get(int key) {
        int idx = getIdxByKey(key);
        return idx == -1 ? -1 : arr[idx].value;
    }

    private int getIdxByKey(int key) {
        int idx = hash(key) % capacity;
        while (arr[idx] != null && arr[idx].key != key) {
            idx = hash(idx) % capacity;
        }
        if (arr[idx] == null || arr[idx].value == -1) {
            return -1;
        }
        return idx;
    }

    private int hash(int key) {
        return Integer.hashCode(key);
    }

    /**
     * Removes the mapping of the specified value key if this map contains a mapping for the key
     */
    public void remove(int key) {
        int idx = getIdxByKey(key);
        if (idx != -1) {
            arr[idx].value = -1;
            size--;
        }
    }
}

class Node {
    int key;
    int value;

    public Node(int key, int value) {
        this.key = key;
        this.value = value;
    }
}
复杂度分析-hash表

如果有M个键,那么在使用Hash表时,很同意就达到O(M)的空间复杂度。
但是,Hash表的时间复杂度和设计有很强的联系。我没可能使用数组来将值存在同一个桶中,理想情况下,桶的大小足够小时,可以看作是一个常数。插入和搜索的时间复杂度都是O(1)。
但在最坏的情况瞎,桶大小的最大值将为N。插入时间复杂度为O(1),搜索时为O(N)。

内置hash表的原理
内置hash表的典型设计是:

  1. 键值可以是任何 可hash化 的类型。并且属于可hash类型的值将具有hash码。此hash码将用于映射函数以获取存储区索引。
  2. 每个桶包含一个数组,用于在初始时将所有值存储在同一个桶中。
  3. 如果在同一个桶中有太多的值,这些值将被保留在一个高度平衡的二叉搜索树中。

插入和搜索的平均时间复杂度仍为O(1)。最坏情况下的插入和搜索的时间复杂度是O(logN),使用高度平衡的BST。这是在插入和搜索之间的一种平衡。

实际使用

使用hash集合查重

简单地迭代每个值并将值插入集合中。 如果值已经在哈希集中,则存在重复。

boolean findDuplicates(List<Type>& keys) {
    // Replace Type with actual type of your key
    Set<Type> hashset = new HashSet<>();
    for (Type key : keys) {
        if (hashset.contains(key)) {
            return true;
        }
        hashset.insert(key);
    }
    return false;
}
HashMap查询出现次数

目标元素作为键,出现次数作为值,每遍历一次更新值

提供更多信息

在这个例子中,如果我们只想在有解决方案时返回 true,我们可以使用哈希集合来存储迭代数组时的所有值,并检查 target - current_value 是否在哈希集合中。但是,我们被要求返回更多信息,这意味着我们不仅关心值,还关心索引。我们不仅需要存储数字作为键,还需要存储索引作为值。因此,我们应该使用哈希映射而不是哈希集合。

ReturnType aggregateByKey_hashmap(List<Type>& keys) {
    // Replace Type and InfoType with actual type of your key and value
    Map<Type, InfoType> hashmap = new HashMap<>();
    for (Type key : keys) {
        if (hashmap.containsKey(key)) {
            if (hashmap.get(key) satisfies the requirement) {
                return needed_information;
            }
        }
        // Value can be any information you needed (e.g. index)
        hashmap.put(key, value);    
    }
    return needed_information;
}
按键聚合

示例 :给定一个字符串,找到它重的第一个非重复字符并返回它的索引。如果它不存在,则返回-1

解决此问题的一种简单方法是首先计算每个字符的出现次数。然后通过结果找出第一个与众不同的角色。因此,我们可以维护一个哈希映射,其键是字符,而值是相应字符的计数器。每次迭代一个字符时,我们只需将相应的值加 1。

解决此类问题的关键是在遇到现有键时确定策略。在上面的示例中,我们的策略是计算事件的数量。有时,我们可能会将所有值加起来。有时,我们可能会用最新的值替换原始值。策略取决于问题,实践将帮助您做出正确的决定。

ReturnType aggregateByKey_hashmap(List<Type>& keys) {
    // Replace Type and InfoType with actual type of your key and value
    Map<Type, InfoType> hashmap = new HashMap<>();
    for (Type key : keys) {
        if (hashmap.containsKey(key)) {
            hashmap.put(key, updated_information);
        }
        // Value can be any information you needed (e.g. index)
        hashmap.put(key, value);    
    }
    return needed_information;
}

设计键

  1. 当字符串 / 数组中每个元素的顺序不重要时,可以使用排序后的字符串 / 数组作为键。

  2. 如果只关心每个值得偏移量,通常事第一个值得偏移量,则可以使用偏移量作为键。

  3. 在树中,有时会希望使用TreeNode作为键,但在大多数情况下,采用子树得序列化(值+路径的递归路径)表述可能会更好。

  4. 在矩阵中,可以使用行索引或者列索引作为键。
    ddd

  5. 在数独中,可以讲行索引和列索引组合来标识此元素属于哪个块。

  6. 有时在矩阵中,希望将值聚合在同一对角线中

致谢 —— leecode

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值