开始刷leetcode了,算法小渣渣只先从简单地刷起。leetcode到目前为止共有944道。由于基础比较薄弱,打算在easy阶段,天天至少刷3道题,三个月完成~html
第一道题就是十分经典的两数之和的题,虽然代码量不多,可是须要注意的点仍是有不少的。算法
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.app
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; 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.性能
复杂度分析:优化
因为这种算法是暴力算法,因此花费不少时间。spa
key---hash--->f(key)code
【经验】在须要输出数组的下标,或者要对下标进行操做时,用哈希表能够事半功倍,大幅度提升时间性能。htm
为了对运行时间复杂度进行优化,咱们须要一种更有效的方法来检查数组中是否存在目标元素。若是存在,咱们须要找出它的索引。保持数组中的每一个元素与其索引相互对应的最好方法是什么?哈希表。
经过以空间换取速度的方式,咱们能够将查找时间从 O(n)下降到 O(1)。哈希表正是为此目的而构建的,它支持以 近似 恒定的时间进行快速查找。我用“近似”来描述,是由于一旦出现冲突,查找用时可能会退化到 O(n)。但只要你仔细地挑选哈希函数,在哈希表中进行查找的用时应当被摊销为 O(1)。
一个简单的实现使用了两次迭代。在第一次迭代中,咱们将每一个元素的值和它的索引添加到表中。而后,在第二次迭代中,咱们将检查每一个元素所对应的目标元素(target - nums[i]target−nums[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"); }
复杂度分析:
先说一下哈希表。参考博文
这里先说一下哈希表的定义:哈希表是一种根据关键码去寻找值的数据映射结构,该结构经过把关键码映射的位置去寻找存放值的地方,提及来可能感受有点复杂,我想我举个例子你就会明白了,最典型的的例子就是字典,你们估计小学的时候也用过很多新华字典吧,若是我想要获取“按”字详细信息,我确定会去根据拼音an去查找 拼音索引(固然也能够是偏旁索引),咱们首先去查an在字典的位置,查了一下获得“安”,结果以下。这过程就是键码映射,在公式里面,就是经过key去查找f(key)。其中,按就是关键字(key),f()就是字典索引,也就是哈希函数,查到的页码4就是哈希值。
经过字典查询数据
可是问题又来了,咱们要查的是“按”,而不是“安,可是他们的拼音都是同样的。也就是经过关键字按和关键字安能够映射到同样的字典页码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; }
因为哈希表高效的特性,查找或者插入的状况在大多数状况下能够达到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"); }
复杂度分析: