【leetcode easy】twosum_两数之和_1

开始刷leetcode了,算法小渣渣只先从简单地刷起。leetcode到目前为止共有944道。因为基础比较薄弱,打算在easy阶段,每天至少刷3道题,三个月完成~

 第一道题就是十分经典的两数之和的题,虽然代码量很少,但是需要注意的点还是有很多的。

 

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].

 

方法1:

class Solution {
    public int[] twoSum(int[] nums, int target) {
        for (int i = 0; i < nums.length; i++) {           //数组的值的长度:array_name.length
            for (int j = i + 1; j < nums.length; j++) {
                if (nums[i] + nums[j] == target) {        //❗️==和=不要混淆
                    return new int[] { i, j };            //数组的初始化赋值,其中有一种是int a[] = new int[]{9,7,21}; 不需要定义数组的大小,数组会根据赋值大小分配空间
                }
            }
        }
        throw new IllegalArgumentException("No two sum solution");  //如果不写这行会报错:missing return statement,原因下面给出   //是throw,不是return
    }
}

为什么如果不在函数体内抛出throw就会报错呢,因为虽然if/else里面已经有了return,但该逻辑语句在程序执行的过程中不一定会执行到,例如抛出异常等问题的出现,所以必须在try-catch外加一个return确保无论发生什么情况都会return.

 

复杂度分析:

由于这种算法是暴力算法,所以花费很多时间。

  • 时间复杂度:O(N2)
  • 空间复杂度:O(1)

 

方法二:算法改进,遍历两遍哈希表

key---hash--->f(key)

【经验】在需要输出数组的下标,或者要对下标进行操作时,用哈希表可以事半功倍,大幅度提高时间性能。

 为了对运行时间复杂度进行优化,我们需要一种更有效的方法来检查数组中是否存在目标元素。如果存在,我们需要找出它的索引。保持数组中的每个元素与其索引相互对应的最好方法是什么?哈希表。

通过以空间换取速度的方式,我们可以将查找时间从 O(n)降低到 O(1)。哈希表正是为此目的而构建的,它支持以 近似 恒定的时间进行快速查找。我用“近似”来描述,是因为一旦出现冲突,查找用时可能会退化到 O(n)。但只要你仔细地挑选哈希函数,在哈希表中进行查找的用时应当被摊销为 O(1)

一个简单的实现使用了两次迭代。在第一次迭代中,我们将每个元素的值和它的索引添加到表中。然后,在第二次迭代中,我们将检查每个元素所对应的目标元素(target - nums[i]targetnums[i])是否存在于表中。注意,该目标元素不能是 nums[i]nums[i] 本身!

public int[] twoSum(int[] nums, int target) {
    Map<Integer, Integer> map = new HashMap<>();    //初始化哈希表 Map<type,type> name = new HashMap<>(); 此时type不是int,而是Integer
    for (int i = 0; i < nums.length; i++) {
        map.put(nums[i], i);     //哈希表赋值: name.put(key,f(key));   因为我们需要输出的是index,所以把f(key)当做索引
    }
    for (int i = 0; i < nums.length; i++) {
        int complement = target - nums[i];
//containsKey判断Map集合对象中是否包含指定的键名。包含就返回true,否则返回false。
     //map.get(key): 输出的值是f(key)
    
if (map.containsKey(complement) && map.get(complement) != i) { //能找到和是target的数,且不是它本身 return new int[] { i, map.get(complement) }; } } throw new IllegalArgumentException("No two sum solution"); }

 

复杂度分析:

  • 时间复杂度:O(n), 我们把包含有 n 个元素的列表遍历两次。由于哈希表将查找时间缩短到 O(1) ,所以时间复杂度为 O(n)。
  • 空间复杂度:O(n), 所需的额外空间取决于哈希表中存储的元素数量,该表中存储了 n 个元素。 因为新建了哈希表,所以占用了额外的空间。

 

先说一下哈希表。参考博文

1.哈希表的定义

  这里先说一下哈希表的定义:哈希表是一种根据关键码去寻找值的数据映射结构,该结构通过把关键码映射的位置去寻找存放值的地方,说起来可能感觉有点复杂,我想我举个例子你就会明白了,最典型的的例子就是字典,大家估计小学的时候也用过不少新华字典吧,如果我想要获取“按”字详细信息,我肯定会去根据拼音an去查找 拼音索引(当然也可以是偏旁索引),我们首先去查an在字典的位置,查了一下得到“安”,结果如下。这过程就是键码映射,在公式里面,就是通过key去查找f(key)。其中,按就是关键字(key),f()就是字典索引,也就是哈希函数,查到的页码4就是哈希值。

          通过字典查询数据

2.哈希冲突

  但是问题又来了,我们要查的是“按”,而不是“安,但是他们的拼音都是一样的。也就是通过关键字按和关键字安可以映射到一样的字典页码4的位置,这就是哈希冲突(也叫哈希碰撞),在公式上表达就是key1≠key2,但f(key1)=f(key2)。冲突会给查找带来麻烦,你想想,你本来查找的是“按”,但是却找到“安”字,你又得向后翻一两页,在计算机里面也是一样道理的。

  但哈希冲突是无可避免的,为什么这么说呢,因为你如果要完全避开这种情况,你只能每个字典去新开一个页,然后每个字在索引里面都有对应的页码,这就可以避免冲突。但是会导致空间增大(每个字都有一页)。

  既然无法避免,就只能尽量减少冲突带来的损失,而一个好的哈希函数需要有以下特点:

  1.尽量使关键字对应的记录均匀分配在哈希表里面(比如说某厂商卖30栋房子,均匀划分ABC3个区域,如果你划分A区域1个房子,B区域1个房子,C区域28个房子,有人来查找C区域的某个房子最坏的情况就是要找28次)。

  2.关键字极小的变化可以引起哈希值极大的变化。

  比较好的哈希函数是time33算法。现在几乎所有流行的HashMap都采用了DJB Hash Function,俗称“Time33”算法,Times33实现起来非诚简单,不断的与33相乘:nHash = nHash*33 + *key++

  核心的算法就是如下:

复制代码
unsigned long hash(const char* key){
    unsigned long hash=0;
    for(int i=0;i<strlen(key);i++){
        hash = hash*33+str[i];
    }  
    return hash;
}
复制代码

3.关于哈希表的性能

  由于哈希表高效的特性,查找或者插入的情况在大多数情况下可以达到O(1),时间主要花在计算hash上,当然也有最坏的情况就是hash值全都映射到同一个地址上,这样哈希表就会退化成链表,查找的时间复杂度变成O(n),但是这种情况比较少,只要不要把hash计算的公式外漏出去并且有人故意攻击(用兴趣的人可以搜一下基于哈希冲突的拒绝服务攻击),一般也不会出现这种情况。

哈希冲突攻击导致退化成链表

 

方法三:遍历一遍哈希表

事实证明,我们可以一次完成。在进行迭代并将元素插入到表中的同时,我们还会回过头来检查表中是否已经存在当前元素所对应的目标元素。如果它存在,那我们已经找到了对应解,并立即将其返回。

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

复杂度分析:

  • 时间复杂度:O(n),我们只遍历了包含有 n个元素的列表一次。在表中进行的每次查找只花费 O(1) 的时间。
  • 空间复杂度:O(n),所需的额外空间取决于哈希表中存储的元素数量,该表最多需要存储 n 个元素。 
     

 

参考:哈希表:http://www.cnblogs.com/s-b-b/

posted @ 2018-11-26 19:04  白白要变成厉害的程序猿  阅读(225)  评论(0编辑  收藏  举报