LeetCode介绍java
LeetCode是算法练习、交流等多功能网站,感兴趣的同窗能够关注下(老司机请超车)。页面顶部的Problems菜单对应算法题库,附带历史经过滤、难易程度等信息。node
将来计划算法
打算用Kotlin语言,按照从易到难的顺序有选择地实现LeetCode库中的算法题,考虑到Kotlin的学习与巩固、算法的思考与优化,争取一星期完成一篇文章(每篇只总结一题,可能偷偷作了后面的好几题^_^)。 数组
固然,除了单纯地用kotlin实现外,还会指出一些容易忽略的坑,并对结果进行更深一层的分析。学习
编码测试测试
点击标题Two Sum(难度Easy)就会进入具体的题目界面,包括描述、编码区、运行/提交按钮、参考方案、讨论等。优化
此次就先选择第一题:给定一个整数型的数组nums和一个目标值target,要求编码实现计算出两个数组下标index1和index2,使得两个下标对应的元素和等于目标值,即nums[index1]+nums[index2]=target。网站
因为描述中有提到,能够假设每一个输入都会有一个靠谱的答案,且同一个元素不能用两次(即不容许出现[2, 2]这样的结果),因此实现的时候能够不用太担忧有没有答案或什么异常之类的状况,以后的编码中只会象征性地给出没有结果时的异常处理。编码
方案1,两层for循环spa
1 class Solution { 2 fun twoSum(nums: IntArray, target: Int): IntArray { 3 for (i in 0..nums.size - 2) { 4 for (j in i + 1..nums.size - 1) { 5 if (nums[j] == target - nums[i]) { 6 return kotlin.intArrayOf(i, j) 7 } 8 } 9 } 10 throw IllegalArgumentException("No two sum solution") 11 } 12 }
上述代码中for循环用了..,是会包含最后一个元素的,即范围取[start, end]。和..效果相同的有rangeTo,相似的还有until(差异在于范围取[start, end),具体用法感兴趣的同窗尝试并作比较)。
在LeetCode上运行会提示正确性与耗时等信息,本文只给出本地电脑上IntelliJ IDEA的运行状况(不存在LeetCode运行时可能有网速等外在因素的干扰)。
测试案例(下同):
1 fun main(args: Array<String>) { 2 var start = System.currentTimeMillis() 3 println("" + Solution().twoSum(intArrayOf(230, 863, 916, 585, 981, 404, 316, 785, 88, 12, 70, 435, 384, 778, 887, 755, 740, 337, 86, 92, 325, 422, 815, 650, 920, 125, 277, 336, 221, 847, 168, 23, 677, 61, 400, 136, 874, 363, 394, 199, 863, 997, 794, 587, 124, 321, 212, 957, 764, 173, 314, 422, 927, 783, 930, 282, 306, 506, 44, 926, 691, 568, 68, 730, 933, 737, 531, 180, 414, 751, 28, 546, 60, 371, 493, 370, 527, 387, 43, 541, 13, 457, 328, 227, 652, 365, 430, 803, 59, 858, 538, 427, 583, 368, 375, 173, 809, 896, 370, 789 4 ), 542).asList()) 5 var end = System.currentTimeMillis() 6 println(end - start) 7 }
关于耗时,建议采用屡次运行后再取平均,这里留给你们发挥想象。最好在一个稳定的环境下测试,且耗时是相对的(相同环境下对不一样算法的结果进行对比,环境变化可比性就意义不大了)。
输出:
运行屡次,发现耗时31ms居多,有时会是47ms,偶尔会是67ms等。
LeetCode提交详情
19次测试总耗时539ms,平均每次大概28.3ms,与31ms仍是很接近的。
方案2,Map初始添加
1 class Solution { 2 fun twoSum(nums: IntArray, target: Int): IntArray { 3 val mapA = mutableMapOf<Int, Int>() 4 for (i in 0..nums.size - 1) { 5 mapA.put(nums[i], i) 6 } 7 for (i in 0..nums.size - 1) { 8 var value = target - nums[i] 9 if (mapA.containsKey(value) && mapA.get(value) != i ) { 10 return kotlin.intArrayOf(i, mapA.get(value)!!) 11 } 12 } 13 throw IllegalArgumentException("No two sum solution") 14 } 15 }
消除了两层循环,多用了一个数组大小的空间,本意是打算用空间换时间。
方案3,Map过程添加
1 class Solution { 2 fun twoSum(nums: IntArray, target: Int): IntArray { 3 val mapA = mutableMapOf<Int, Int>() 4 for (i in 0..nums.size - 1) { 5 var value = target - nums[i] 6 if (mapA.containsKey(value)) { 7 return kotlin.intArrayOf(mapA.get(value)!!, i) 8 } else { 9 mapA.put(nums[i], i) 10 } 11 } 12 throw IllegalArgumentException("No two sum solution") 13 } 14 }
针对mapA的元素添加过程作了优化,不是像方案2中那样一开始就将数组元素所有进行映射,而是边查找边添加。
结果分析
注意点1,耗时状况
后面两种方案没有给出输出结果,缘由是对于耗时来讲,三种方案是差很少的。这就有疑问了,后两种利用了Map映射机制,可能在空间上确实增长了,可是循环才是耗时主要因素,为何时间并无减小呢?
遇到这种状况,就不建议百度或者谷歌了,不为别的,就由于源码最靠谱。
代码中是经过mutableMapOf创建mapA变量的,找下去,在Maps.kt中:
1 public inline fun <K, V> mutableMapOf(): MutableMap<K, V> = LinkedHashMap()
线索LinkedHashMap,找下去,在TypeAliases.kt中:
1 @SinceKotlin("1.1") public typealias LinkedHashMap<K, V> = java.util.LinkedHashMap<K, V>
用到了类型别名。正如Kotlin的自我介绍,其和Java及JVM是很亲密的。线索java.util.LinkedHashMap,找下去,在LinkedHashMap.java中:
1 public boolean containsKey(Object key) { 2 return getNode(hash(key), key) != null; 3 }
能够看到Kotlin中containsKey最终调用了Java中的getNode,真相就在下面:
1 final Node<K,V> getNode(int hash, Object key) { 2 Node<K,V>[] tab; Node<K,V> first, e; int n; K k; 3 if ((tab = table) != null && (n = tab.length) > 0 && 4 (first = tab[(n - 1) & hash]) != null) { 5 if (first.hash == hash && // always check first node 6 ((k = first.key) == key || (key != null && key.equals(k)))) 7 return first; 8 if ((e = first.next) != null) { 9 if (first instanceof TreeNode) 10 return ((TreeNode<K,V>)first).getTreeNode(hash, key); 11 do { 12 if (e.hash == hash && 13 ((k = e.key) == key || (key != null && key.equals(k)))) 14 return e; 15 } while ((e = e.next) != null); 16 } 17 } 18 return null; 19 }
代码第11-15行,其实仍是用到了遍历。问题的答案就有解了,Map+while耗时和for+for差异不大,前者代码更简洁,后者不需额外空间。
那么,有没有更好的方案呢?欢迎同窗们提出,你们一块儿讨论、学习。
注意点2,Map映射的坑
LeetCode或者其余平台的测试案例也是随机的,有时候并不会发现代码中的潜在问题。
好比上述案例目标值是542,三种方案结果都是一致的[28, 45]。若是目标值改成1093,即数组的第1、二个元素下标[0, 1]是指望结果,可是第二种方案倒是[0, 40],而其余两种方案正常。
问题就出在其全部元素值是初始添加的,来看其中这一段代码:
1 for (i in 0..nums.size - 1) { 2 mapA.put(nums[i], i) 3 }
对于Map映射,put操做当key不存在时进行添加,不然进行再赋值。因此当数组元素存在相同的值时,最后求出的下标值就会是最后一个,而不是第一个。
改进方案是在put操做前进行key的存在判断:
1 for (i in 0..nums.size - 1) { 2 if (!mapA.containsKey(nums[i])) { 3 mapA.put(nums[i], i) 4 } 5 }
因此,须要对本身写的代码多测试和思考,不断发现问题并优化,运行succeed或提交accepted并不能保证什么。