前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >第1题号使用散列表的解题思路 | LeetCode动画

第1题号使用散列表的解题思路 | LeetCode动画

作者头像
我脱下短袖
发布2020-02-14 16:42:38
3900
发布2020-02-14 16:42:38
举报
文章被收录于专栏:算法无遗策算法无遗策

今天分享一个关于散列表的LeetCode题,题号是1,标题是:两数之和。解题思路会着重介绍HashMap散列表的动态空间变化,以及分析HashMap.containsKey(value)的时间复杂度。

题目描述

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。

你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。

示例:

代码语言:javascript
复制
给定 nums = [2, 7, 11, 15], target = 9

因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]

解题

学习过散列表之后解决这道题会很简单,当然,直接寻址表也可以解决这道题,但直接寻址表是采用空间换取时间的方法,不适用单个值数据很大的场景,例如输入数组[1000001,1000003,100011,100013,100015],就要创建数组长度为100015的直接寻址表,而实际存储的数据只有五个。

所以,散列表可以利用合适的散列函数,将关键字散列到合适的槽中(散列表数组中的一个位置)。直接寻址表查找一个数,时间复杂度直接就是O(1),查找非常快。如果散列表也要求时间复杂度和直接选指表对齐,也可以做到。

HashMap类也是散列表的实现,我们翻看一下HashMap的部分源码:

代码语言:javascript
复制
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

HashMap创建的数组长度初始为16,默认的加载因子是0.75,加载因子是实际存储元素的个数 / 数组长度。算一下16 * 0.75等于12,意味着,实际存储元素的个数超过12,就创建一个目前数组长度*2的新数组,将原数组里面的节点依次通过新的散列函数(因为数组长度也要变化)散列到新的数组中(新的散列表)。

动画:散列表加载因子超过默认加载因子

http://mpvideo.qpic.cn/0b78eaaaaaaauqaondrh7vpfaigdaaqaaaaa.f10002.mp4?dis_k=6b117e84aecf6a3be298ea68195d9adf&dis_t=1581669642

通过上面的动画可以看到,散列表至少有25%的空间是空的,如果存储的元素是比较均匀分布的话,HashMap查找效率高,时间复杂度也能期望是O(1)。

如果散列函数不合适,将实际存储的元素散列到一个槽中,相当于要维护一个线性结构。但在jdk8版本中,如果这个线性结构的节点数超过8个,则将散列表中每一个槽的线性结构都转换为红黑树。

散列表table数组里面存放着每一个Entry对象,每一个Entry本质上是一个单向链表。如果table其中有一个Entry对象节点数查过8个阈值,会将table每一个Entry对象都转换为红黑树的树形结构,或者应该说一个Entry至少有两个节点的都可以转化为红黑树。

代码语言:javascript
复制
/**
* The bin count threshold for using a tree rather than list for a
* bin.  Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;

如果有兴趣看一下链表转换为红黑树的代码,更多内容可以打开HashMap源码看一下:

代码语言:javascript
复制
// 单向链表转换为红黑树
/**
* Replaces all linked nodes in bin at index for given hash unless
* table is too small, in which case resizes instead.
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}
动画:散列表中链表向红黑树的变化

http://mpvideo.qpic.cn/0bf25yaacaaaiaaoogbhy5pfb3wdahxaaaia.f10002.mp4?dis_k=117f0b80aff5a67dfc3f35fcd951138d&dis_t=1581669642

Code:LeetCode题号为1的合适答案
代码语言:javascript
复制
class Solution {
    public int[] twoSum(int[] nums, int target) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            int complement = target - nums[i];
            if (map.containsKey(complement)) {
                return new int[] { map.get(complement), i };
            }
            map.put(nums[i], i);
        }
        throw new IllegalArgumentException("No two sum solution");
    }
}
HashMap.containsKey(value)时间复杂度分析

HashMap.containsKey(value)是先使用指针指向数组引用,时间复杂度为O(1),未命中时,才回去遍历红黑树,时间复杂度为O(n)。

但是时间复杂度为什么会出现O(1)呢?这个需要看源码就能看清楚了。

代码语言:javascript
复制
/**
* Returns <tt>true</tt> if this map contains a mapping for the
* specified key.
*
* @param   key   The key whose presence in this map is to be tested
* @return <tt>true</tt> if this map contains a mapping for the specified
* key.
*/
public boolean containsKey(Object key) {
    return getNode(hash(key), key) != null;
}

调用了getNode(hash(key), key)方法,参数分别为key的hash值和key值,再来看一下这个方法的实现。

代码语言:javascript
复制
/**
 * Implements Map.get and related methods
 *
 * @param hash hash for key
 * @param key the key
 * @return the node, or null if none
 */
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 && // always check first node
            ((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;
}

Node<K, V> first是一个单向链表节点,包含了key、value、hash和指向下个节点的指针;Node<K, V>[] tab是一个数组,也是散列表。

看上面算法中可能影响时间复杂度的便是first = tab[(n - 1) & hash]) != null,tab[(n - 1) & hash执行了如下操作:

1.指针first指向那一行数组的引用(那一行数组是通过table下标范围n-1和key的hash值计算出来的),若命中,则通过下标访问数组,时间复杂度为O(1);

2.如果没有直接命中(key进行hash时,产生相同的位运算值),存储方式变为红黑树,那么遍历树的时间复杂度为O(n)。

综上,HashMap.containsKey(value)的最好情况是O(1),最坏情况是O(n)。

-----完结-----

喜欢本文的朋友,欢迎关注「算法无遗策」,和我们一起学数据结构、刷算法题。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-01-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 算法无遗策 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 题目描述
  • 解题
    • 动画:散列表加载因子超过默认加载因子
      • 动画:散列表中链表向红黑树的变化
        • Code:LeetCode题号为1的合适答案
          • HashMap.containsKey(value)时间复杂度分析
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档