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並不能保證什麼。