Leetcode第一題:兩數之和(3種語言)

ssswill發表於2018-12-08

Leetcode第一題:兩數之和

給定一個整數陣列 nums 和一個目標值 target,請你在該陣列中找出和為目標值的 兩個 整數。
你可以假設每種輸入只會對應一個答案。但是,你不能重複利用這個陣列中同樣的元素。
示例:
給定 nums = [2, 7, 11, 15], target = 9
因為 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]

標註:僅在Python解法做詳細分析,c++與java如無特別需要注意的地方不做分析。

一、Python解法

解法2,3參考了linfeng886的部落格

解法1:暴力搜尋

class Solution:
    def twoSum(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: List[int]
        """
        #對nums每個元素迴圈
        for i in range(len(nums)):
        #從nums[i]的下一個開始找
            for j in range(i+1,len(nums)):
            #如果找到了就返回值
                if nums[i]+nums[j]==target:
                    return i,j

分析:程式碼十分簡單,就是首先用i在陣列裡迴圈一輪,在每個i迴圈下,去從剩下的元素找target-nums[i]的值。如果找到了,就return i,j兩個數。(程式假設一定可以找的到)
來看看執行結果:
在這裡插入圖片描述
在這裡插入圖片描述
可以看到效率是十分低的,主要原因就是用了兩個for迴圈,時間複雜度是O(n2)。

解法2:一次for迴圈

一開始犯了一個小錯誤的程式碼
class Solution:
    def twoSum(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: List[int]
        """
        #直接在i一個一個迴圈的時候,直接判斷target-nums[i]在列表裡嗎,在的話用list.index()獲取索引,用了一次for迴圈。很棒。
        for i in range(len(nums)):
            if target-nums[i] in nums:
                return i,nums.index(target-nums[i])

此程式碼的執行問題在於:
在這裡插入圖片描述
我們可以看到,忘記了一個元素只能用一次的規矩,因此做一個判斷即可。
修改版:

class Solution:
    def twoSum(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: List[int]
        """
        for i in range(len(nums)):
            if target-nums[i] in nums:
            #增加了返回的兩個數下表不能相同的判斷
                if i!=nums.index(target-nums[i]):
                    return i,nums.index(target-nums[i])

在這裡插入圖片描述
可以看到:速度上升了好幾倍。效果還不錯。但是通過檢視官方解答參考了linfeng886的部落格知道還有一種更快的方法----基於hash table的解決方法。

解法3:基於Python字典查詢

關於hash table(雜湊表),簡單來說就是存有鍵值對key,value的一種資料結構,對於熟悉Python的人來說,常見的字典就是一種hash table。它的查詢速度是很快的,可以理解為O(1)。所以這裡相當於在解法2的基礎上做了一個改進。解法2 是在空間不變的前提下,在i迴圈時,直接在列表裡查詢是否含有target-nums[0]的元素,而列表的查詢速度是遠不如hash table。所以解法三的關鍵就在於,查詢的任務在字典中進行而不在list中進行(有待商榷)
程式碼:

class Solution:
    def twoSum(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: List[int]
        """
        #遍歷一個i,就把nums[i]放在字典裡。之後i不斷相加,總會碰到
        #target - nums[i]在字典裡的情況。碰到的時候return即可。這裡注意到先return的是dict[a],再是當前的i,原因就是dict裡存的value都是以前遍歷過的i,所以它比當前的i小,按從小到大的順序所以這樣排。
        dict = {}
        for i in range(len(nums)):
            a = target - nums[i]
            if a in dict:
                return dict[a],i
            else:
                dict[nums[i]] = i

執行結果:
在這裡插入圖片描述
可以從程式碼中看到,解法3就是把需要查詢的target-nums[i]從解法2中list變成了dict而已,但結果提高的十分可觀。但要注意的是,這裡用空間與實踐做了一個tradeoff,也就說解法3雖然快了很多,但是需要的空間增加了一個新的dict。
這也給我們了一個提示:以後如遇到類似的需要遍歷查詢的元素時,不妨參照本例,利用hash table查詢。

二、Java解法

解法2,3參照了官方解法twosum
以及菜鳥教程java陣列篇
Pythonliast與java陣列區別 https://blog.csdn.net/wu1226419614/article/details/80870120
關於hashmap資料型別
https://www.cnblogs.com/hello-yz/p/3712610.html

解法1:暴力搜尋

程式碼:

class Solution {
    public int[] twoSum(int[] nums, int target) {
        int[] a = new int[2];
        for(int i=0;i<nums.length;i++){
            for(int j=i+1;j<nums.length;j++){
                if(nums[i]+nums[j]==target){
                    a[0] = i;
                    a[1] = j;
                 //return new int[] {i,j};   
                }
            }
        }
        return a;
    }
}

在這裡插入圖片描述
這裡不作特別說明。只想提及的是關於return new int[] {i,j}的些許解釋。這種寫法是官方解讀給出的。參考菜鳥教程關於陣列的解釋,可以知道這是函式輸入引數或者充當返回值的一種方式。
同時,官方解法在類的最後會throw一個異常,若將其刪除會報錯,因為throw也是return的一種替代形式。(就是說即使這個類在開頭就說了不是void的,要返回一個int[]或者其他的東西,但是在最後丟擲一個異常語法上是符合的。)對於本例,執行著就會從if下的return離開程式,所以不會丟擲異常的。

解法2:兩次for迴圈(兩遍hashmap)

在這裡和Python的3種解法做一個比較。可以看到兩種語言的解法1是完全相同的。但是解法2上,會有一些區別。之後解法3又是完全相同的。為什麼解法2會和Python解法2有區別呢?
先回顧下Python解法2:通過i迴圈列表,直接判斷target - nums[i]是否在列表裡,在的話,就直接返回i,與list.index(target-nums[I])。這裡我們用了Python內建函式index。可以方便的獲取到索引,而對於java的陣列,並沒有那麼方便獲取陣列元素索引的函式。這裡有一個很好的比較,從中可以知道java對於陣列有一個binarySearch的查詢方法,而它本身就是用二分法查詢實現的,所以只適用於有序陣列。同時若再用一次for迴圈獲取索引,得不償失。那不如多用一次for迴圈把索引與數值一一對應起來,用類似Python字典的方式,這樣查詢的更快-----hashmap
程式碼:

class Solution {
    public int[] twoSum(int[] nums, int target) {
        int[] b = new int[2];
        HashMap<Integer,Integer> map = new HashMap<Integer,Integer>(); 
        for(int i=0;i<nums.length;i++){
            map.put(nums[i],i);
        }
        for(int j=0;j<nums.length;j++){
            int a = target - nums[j];
            if(map.containsKey(a) && j!=map.get(a)){
                b[0] = j;
                b[1] = map.get(a);
                    return b;
                 //return new int[] {j,map.get(a)};
            }
        }
    return b;
}
}

在這裡插入圖片描述
這裡需要注意幾點:
1.HashMap<Integer,Integer> = new HshMap<Integer,Integer>()這裡的Integer不能用基本資料型別int。可以參看這裡
2.hashmap宣告定義格式,和一般類的例項化一樣。
class1 xx = new class1()
3.hashmap與hashtable。hashmap基本上可以等同於hashtable。而且可以看作為其升級版。HashMap幾乎可以等價於Hashtable,除了HashMap是非synchronized的,並可以接受null(HashMap可以接受為null的鍵值(key)和值(value),而Hashtable則不行)。詳情可檢視 http://www.importnew.com/7010.html
4.hashmap的put,get分別為存與取。以及containsKey在程式碼裡都有體現,很容易理解。
5.注意if判斷裡的&&(短路運算子)不能被&代替,可以通過LeetCode的簡單試例,但是提交時會報錯。分析:&即使前面為false,運算子後面的邏輯式還是會被判斷。而後面的為j != map.get(a)。很可能a是不存在的,故會報錯。將map.get(a)列印出來是一個null指標。進一步解釋,執行用&的程式碼會報錯:
Exception in thread “main” java.lang.NullPointerException
這也就是空指標異常。解釋是"程式遇上了空指標"。簡單地說就是呼叫了未經初始化的物件或者是不存在的物件,這個錯誤經常出現在建立圖片,呼叫陣列這些操作中,比如圖片未經初始化,或者圖片建立時的路徑錯誤等等。對陣列操作中出現空指標。陣列的初始化是對陣列分配需要的空間,而初始化後的陣列,其中的元素並沒有例項化,依然是空的,所以還需要對每個元素都進行初始化(如果要呼叫的話)。參見 https://zhidao.baidu.com/question/494551043.html
但是,這一切在python中是允許的。

解法3:一次for迴圈(一次hashmap)

此解法思想與Python解法3如出一轍,是一模一樣的,唯一區別在於Python使用字典做查詢,Java使用HashMap做查詢。因此本解法不做過多說明。
程式碼:

class Solution {
    public int[] twoSum(int[] nums, int target) {
        HashMap<Integer,Integer> map = new HashMap<Integer,Integer>();
        for(int i=0;i<nums.length;i++){
            if(map.containsKey(nums[i])){
                return new int[] {map.get(nums[i]),i};
            }
            map.put(target-nums[i],i);
        }
        return new int[]{1,1,1};
    }
}

在這裡插入圖片描述
可以看到,與解法2相比速度提升有限。
這裡需要注意的是:1.程式碼最後一行無論return的是什麼(必須是陣列)無所謂的,因為不會走到這一步的,但是最優解還是像官方解答一樣丟擲一個異常比較好。因為如果真走到這一步了,說明程式肯定出現了異常。
2.我這裡寫的和官方解法略有不同,其實是一樣的。我把put進去的值換成了target-nums[i]。然後判斷nums[i]是否在map中,具體就不說了,略顯繁瑣。

三、C++解法

有了上述兩種語言的解答做鋪墊,我覺得C++解法思路就會清晰很多了。

解法1:暴力搜尋

直接看程式碼:

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        //vector型別為長度可變的陣列,更靈活。需要#include<vector>
        vector<int> a;
        //int a[2];這裡指定返回的是verctor型別,故這裡不能用普通陣列array
        for(int i=0;i<nums.size();i++){
            for(int j =i+1;j<nums.size();j++){
                if(nums[i]+nums[j]==target){
                    //在a的最後新增元素
                    a.push_back(i);
                    a.push_back(j);
                    //a[0] = i;
                    //a[1] =j;
                    return a;
                }
            }
        }
    //注意這裡和java不一樣,不需要一定要返回一個vector了。雖然該類要求有一個返回值
    }
};


這裡做一些筆記:
1.求陣列長度。
Python:len(list);
java:nums.length;
c++:nums.size()
2.java與c++的基本陣列型別長度都是不可變的,要求靈活的使用用vector代替。關於陣列與vector在c++與Java中的使用
c++相關參見菜鳥教程
Java的vector參見zheting的部落格
重要的一點貼出來了:
java使用需要import java.util.Vector;
插入功能:
public final synchronized void adddElement(Object obj)
將obj插入向量的尾部。obj可以是任何型別的物件。對同一個向量物件,亦可以在其中插入不同類的物件。但插入的應是物件而不是數值,所以插入數值時要注意將陣列轉換成相應的物件。
例如:要插入整數1時,不要直接呼叫v1.addElement(1),正確的方法為:

Vector v1 = new Vector(); 
Integer integer1 = new Integer(1); 
v1.addElement(integer1); 

3.關於陣列作為函式的形參。
本例中是這樣寫的:

vector<int> twoSum(vector<int>& nums, int target) {
//insert your code
}

或者這樣寫:

vector<int> twoSum(vector<int> &nums, int target) {
//insert your code
}

或者:

vector<int> twoSum(vector<int> nums, int target) {
//insert your code
}

具體可檢視菜鳥教程關於陣列做形參的講解

解法2:兩次map(非雜湊表)

這裡注意的是用的是c++的map實現的key,value配對。而c++中還有hash_map,即hash table。二者的區別是:
hash_map採用hash表儲存,map一般採用紅黑樹(RB Tree)實現。因此其記憶體資料結構是不一樣的
二者區別具體可檢視
zhenyusoso的部落格Miles-的部落格
先來看map實現的程式碼:

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        vector<int> a;
        map<int,int> map;
        //hash_map<int,int> hp;
        for(int i=0;i<nums.size();i++){
           map[nums[i]] = i;
            
        }
        for(int j=0;j<nums.size();j++){
            if(map.count(target-nums[j])==1 && map[target-nums[j]]!=j){
                a.push_back(j);
                a.push_back(map[target-nums[j]]);
                return a;
            }
        }
    return a;
    }
};

在這裡插入圖片描述
做幾點分析:
1.此方法和java的解法2版本是十分接近的。主要不同之處在於java的hashmap呼叫的方法有:put(key,value),get(key),containsKey()
而c++的map查詢鍵值是否在為count。關於map的count與find是很常用的方法,二者用途一樣。具體見Andy Niu的部落格
2.這裡用的是map,為什麼不用hash_map類來實現呢?我在xcode中實現了一遍,macos系統關於hash_map的宣告比較特殊:

#if defined __GNUC__ || defined __APPLE__
#include <ext/hash_map>
#else
#include <hash_map>
#endif
int main()
{
        using namespace __gnu_cxx;

        hash_map<int, int> map;
}

同時用find方法判別hash_map是否含有想要的key。

hash_map<int,int> hp;
if(hp.find(target-nums[j])!=hp.end() && hp[target-nums[j]]!=j){
//程式碼區
}

解法3:1次map(非雜湊表)

這個就沒有什麼要分析的了,和上面兩種語言的解法三一模一樣。

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        vector<int> a;
        map<int,int> map;
        for(int i=0;i<nums.size();i++){
            if(map.find(target-nums[i])!=map.end()){
                a.push_back(map[target-nums[i]]);
                a.push_back(i);
                return a;
            }
            else{
                map[nums[i]] = i;
            }
        }
         return a;
        }
    

};

在這裡插入圖片描述
這裡程式碼用了map的find而不是count來判斷map是否含有指定key。

ps:後面的刷題筆記應該會3種語言分開記錄,這樣一篇文章太長,容易產生厭倦感。

相關文章