从leetcode上一道"Two Sum"题目谈下HashMap数据结构和算法复杂度分析

题目来自https://leetcode.com/problems/two-sum/description/

题目描述

Given an array of integers, return indices of the two numbers such that they add up to a specific target.
You may assume that each input would have exactly one solution, and you may not use the same element twice.//不能用同一个元素两次
Example:
Given nums = [2, 7, 11, 15], target = 9,
Because nums[0] + nums[1] = 2 + 7 = 9,
return [0, 1].

给定一个整数数组和一个目标整数,返回使数组中的两个数的和为目标整数的两个数的下标。

解答一

最直观的做法,暴力破解。

class Solution {
    public int[] twoSum(int[] nums, int target) {
        for(int i=0; i<nums.length-1; i++)
            for(int j=i+1; j<nums.length; j++){
                if( i != j && target == nums[i] + nums[j]) //题目要求不能用同一个元素两次,即i,j不能相等
                return new int[]{i,j};
            }
        return null;
    }
}

解答二

利用HashMap的查询效率,缩减时间复杂度。

class Solution {
    public int[] twoSum(int[] nums, int target) {
        Map<Integer, Integer> map = new HashMap();
        for(int i=0;i<nums.length;i++)
            map.put(nums[i],i);
        for(int i=0;i<nums.length;i++){
            if(map.containsKey(target-nums[i]) && i != map.get(target-nums[i]))
                return new int[]{i,map.get(target-nums[i])};
        }
        return null;
    }
}

结果被评了个Wrong Answer。TestCases如下:

Input:
[3,3]
6
Output:
null
Expected:
[0,1]

意思是不能用同一个元素两次,但是原本数组中不同元素可以相等。这样的话不能把数组元素作为key,元素下标作为value。因为同一个key用put操作,会用新的value覆盖旧的value,旧元素下标会丢失。但是官方solution也是将数组元素作为key(如下),此处很疑惑。

官方Solution

Approach 1: Brute Force

public int[] twoSum(int[] nums, int target) {
    for (int i = 0; i < nums.length; i++) {
        for (int j = i + 1; j < nums.length; j++) {
            if (nums[j] == target - nums[i]) {
                return new int[] { i, j };
            }
        }
    }
    throw new IllegalArgumentException("No two sum solution");
}

Time complexity : O(n2)
Space complexity: O(1). (由于不需要另行开辟空间,只是常数的变量申请,时间复杂度为O(1))

Approach 2: Two-pass Hash Table

What is the best way to maintain a mapping of each element in the array to its index? A hash table.
第一次循环把元素值作为key,元素下标作为value。

public int[] twoSum(int[] nums, int target) {
    Map<Integer, Integer> map = new HashMap<>();
    for (int i = 0; i < nums.length; i++) {
        map.put(nums[i], i);
    }
    for (int i = 0; i < nums.length; i++) {
        int complement = target - nums[i];
        if (map.containsKey(complement) && map.get(complement) != i) {
            return new int[] { i, map.get(complement) };
        }
    }
    throw new IllegalArgumentException("No two sum solution");
}

用空间换时间
Time complexity : O(n).(如果hash函数完全无冲突,则查询时间复杂度为O(1),n次循环时间复杂度为O(n))
Space complexity: O(n).

Approach 3: One-pass Hash Table

仅用一次循环就解决问题,在循环的同时将元素放入map。我们只需要在新元素进map之前在map内查找元素的complement,如果没有再放进map。

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");
}

Time complexity : O(n).
Space complexity: O(n).

HashMap操作的的时间复杂度

put操作

可以看到如果当前散列位置无元素,即散列完全有效,则时间复杂度为O(1)

/*@return the previous value associated with key, null if there was no mapping for key. (A null return can also indicate that the map previously associated null with key.)*/
//如之前无此key则返回null;如之前有此key,则覆盖旧value,返回旧value
public V put(K key, V value) {
	return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;//map为空时resize()复杂度为O(1)
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);//如果当前key的散列位置为空,则新建结点。
        else {//如果当前key的散列位置已有元素
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))//如果当前key的散列位置不为空且key相同
                e = p;//e指向当前散列位置p
            else if (p instanceof TreeNode)//如果当前散列位置p为TreeNode
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {//当前key的散列位置不为空且key不相同,并且当前散列位置为链表存储
                for (int binCount = 0; ; ++binCount) {//循环直到找到当前散列位置为key的结点
                    if ((e = p.next) == null) {//当前散列位置的后继结点为空
                        p.next = newNode(hash, key, value, null);//新建结点在当前位置之后
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);//大于7就树化
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))//如果找到了当前结点的key跟参数的key相同,即指向了已有的key结点
                        break;
                    p = e;//当前结点指像e,即下一个结点,直到为空,或者找到key结点,必要时树化
                }
            }
            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操作

通过key来取value,可以看到如果当前散列位置无元素,即散列完全有效,则时间复杂度为O(1)

public V get(Object key) {
	Node<K,V> e;
	return (e = getNode(hash(key), key)) == null ? null : e.value;
}
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;
    }

containsKey操作

可以看到如果当前散列位置无元素,即散列完全有效,则时间复杂度为O(1)

public boolean containsKey(Object key) {
	return getNode(hash(key), key) != null;
}
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;
    }

比较的经典语句
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))

containsValue操作

直接用for循环扫描table,时间复杂度为O(n)。
如果散列的不好,即一个结点有若干个元素,则还要更长时间。

public boolean containsValue(Object value) {
        Node<K,V>[] tab; V v;
        if ((tab = table) != null && size > 0) {
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                    if ((v = e.value) == value ||
                        (value != null && value.equals(v)))
                        return true;
                }
            }
        }
        return false;
    }

总结

利用java包含的数据结构提高算法效率

如HashSet、HashMap

熟悉各种数据结构的各种操作的执行效率

如排序算法

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值