Kotlin實現LeetCode算法題之Two Sum

 

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

相關文章
相關標籤/搜索