由LeetCode_633题引发的对算法时间复杂度的思考以及ArrayList和HashSet底层实现的巩固

今天在LeetCode刷题时碰到一个“怪题”,说难也不难,说不难我又花了很长时间,并且一直被困扰其中。百度参考别人意见,到最后解决,这个过程思考了很多,现在记下来。

原题:633. 平方数之和
描述:

给定一个非负整数 c ,你要判断是否存在两个整数 a 和 b,使得 a2 + b2 = c。
示例1:

输入: 5
输出: True
解释: 1 * 1 + 2 * 2 = 5

示例2:
输入: 3
输出: False

题目不难,相信稍微思考一下就能写出下面的代码:

class Solution {
   public boolean judgeSquareSum(int c) {
       if(c < 0){
           return false;
       }
    // 考虑到其实x y最大也就是c的开平方了
       int middle = (int)Math.sqrt(c);
       for(int x=0;x<=middle;x++){
        for(int y=middle;y>=0;y--){
            if(x*x + y*y == c){// 遍历找到满足条件的x y
                return true;
            }
        }
    }
       return false;
   }
}

上面这段代码简单易于理解,在输入值较小时可以“正常”工作,但是随着输入值的增大,效率会下降很快,因为两层循环嵌套导致完全遍历耗费大量的时间(这个也跟电脑CPU性能有关系),结果是我在本题运行这段代码正常,但是提交时运行大量单元测试时不通过,提示:超过时间限制。此时入参为Integer.MAX_VALUE也就是21亿多。我花了很长时间思考,尝试过另外几种方式,不过本质都是基于基础数组遍历,最后发现这条路根本走不通,总是会提示超出时间限制。

最后经过搜索,在他人思路的提示下,使用如下代码:

class Solution {
    public boolean judgeSquareSum(int c) {
        if(c < 0){
            return false;
        }
        // 考虑到其实x y最大也就是c的开平方了
        int middle = (int)Math.sqrt(c);
        // 最关键的就是引入HashSet
        Set<Integer> set = new HashSet<>();
        for(int x=0;x<=middle;x++){
            set.add(x*x);// 将所有x y可能的值加入set
        }
        Iterator<Integer> ite = set.iterator();
        while(ite.hasNext()){
            int next = ite.next();
            if(set.contains(c-next)){// 结果减去x平方的可能值就是y的平方值
                return true;
            }
        }
        return false;
    }
}

上面这段代码可以完美提交通过,到了这里,相信你已经在思考了。如果你有兴趣,还可以把上面的HashSet换成List的ArrayList来实现同样的效果,结果当然也会提交不通过。

很明显,这道题不但在考你的基础算法实现,还涉及到算法效率优化问题。也就是必须要关注算法的时间复杂度

既然如此,就趁这个机会加深一下ArrayList与HashSet元素查找的时间复杂度区别,实际上就是底层的实现区别。

上述第一套代码使用双层数组遍历的时间复杂度为O(n²),ArrayList本质就是通过数组实现的,所以如果上述第二套代码如果使用ArrayList实现也是同样的时间复杂度。

ArrayList判断是否包含某个元素的实现:

    public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }
    public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)// 通过遍历数组查找元素
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

而使用HashSet实现为什么可以呢?答案还是在它的内部实现,HashSet是通过HashMap的KeySet来实现的,判断是否包含某个元素的实现:

    // HashSet
    public boolean contains(Object o) {
        return map.containsKey(o);// 通过HashMap实现
    }
    // HashMap
    public boolean containsKey(Object key) {
        return getNode(hash(key), key) != null;
    }
    // HashMap获取指定节点
    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) {// 直接通过hash定位到数组元素,而不是遍历获取
            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;
    }

看上面代码我添加第一行注释的位置,可以看到HashSet的元素定位(实际上是HashMap的key寻址)是一步到位(为什么可以这样实现?因为put元素的时候也是同样的方式实现,这里不讨论),也就是说时间复杂度为O(1)(取决于Hash算法是否均匀分布,可能存在需要遍历链表的情况,如上述第二部分的注释,通过每次插入操作后的rehash,实际上平均情况下HashMap能达到O(1))。

这就是为什么通过HashSet的元素检索实现不会出现效率问题,主要还是通过HashMap实现的键hash寻址实现的功劳。

因为这个问题,又找了点资料,顺便贴一下常见算法的时间复杂度:

O(1)<O(logn)<O(n)<O(nlogn)<O(n^2)<O(n^3)<O(2^n)<O(n!)

Ο(1)表示基本语句的执行次数是一个常数,一般来说,只要算法中不存在循环语句,其时间复杂度就是Ο(1)。其中Ο(log2n)、Ο(n)、 Ο(nlog2n)、Ο(n^2)和Ο(n^3)称为多项式时间,而Ο(2^n)和Ο(n!)称为指数时间。计算机科学家普遍认为前者(即多项式时间复杂度的算法)是有效算法,把这类问题称为P(Polynomial,多项式)类问题,而把后者(即指数时间复杂度的算法)称为NP(Non-Deterministic Polynomial, 非确定多项式)问题

就到这里,还是感触颇深,顺便又巩固一下基础,加深一下源码实现知识。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值