LeetCode算法訓練:初級算法(持續更新...)

簡介

LeetCode初級算法簡介

本來想重初中級和企業面試算法開始的,但是最後還是選擇從基礎的開始,因爲我們並不是爲了刷題而刷題,而是在刷題過程中鍛鍊一種算法思維,在大量的訓練之後形成一種對算法的獨特見解,培養那種對算法的的敏感度,看到題目,大腦中可以浮現一個解題藍圖,而且從初級開始慢慢建立信心,而且這也是在爲後邊複雜算法的解題思路打基礎。

如果你也想訓練自己的算法思維,也可以加入我,從初級算法開始,開啓你的算法之旅:初級算法

自己的一些思考:不要在看完題目後直接就看答案,然後去背題,這樣行成的算法記憶是不牢固的,一定要有自己的思考;而且不要一開始就在IDEA上邊去寫,一定試着自己在leetCode提供的白板上邊寫一遍最後在放到IDEA上邊去執行看有什麼問題,以便鞏固你的基礎API的使用和熟練度;還有一點就是大膽些,不是面試我們試錯成本低,儘可能把我們的想法融入到代碼中

因篇幅問題,博客中只列出示例和自己的解題答案,詳細可以直接點擊題目查看。

刪除排序數組中的重複項

題目 給定一個排序數組,你需要在 原地 刪除重複出現的元素,使得每個元素只出現一次,返回移除後數組的新長度。
不要使用額外的數組空間,你必須在 原地 修改輸入數組 並在使用 O(1) 額外空間的條件下完成。
示例 1:
給定數組 nums = [1,1,2],
函數應該返回新的長度 2, 並且原數組 nums 的前兩個元素被修改爲 1, 2。
你不需要考慮數組中超出新長度後面的元素。
示例 2:
給定 nums = [0,0,1,1,1,2,2,3,3,4],
函數應該返回新的長度 5, 並且原數組 nums 的前五個元素被修改爲 0, 1, 2, 3, 4。
你不需要考慮數組中超出新長度後面的元素。

我的答案

class Solution {
     public int removeDuplicates(int[] nums) {
        if (nums == null || nums.length == 0) return 0;
        if (nums.length == 1) return 1;

        int elem = nums[0];
        int markIndex = 1;
        //給定 nums = [0,0,1,1,1,2,2,3,3,4]
        for (int i = 1; i < nums.length; i++) {
            if (elem != nums[i]) {
                nums[markIndex] = nums[i];
                markIndex++;
                elem = nums[i];
            }
        }

        return markIndex;
    }
}

執行用時:1 ms
內存消耗:41.5 MB

買賣股票的最佳時機 II

給定一個數組,它的第 i 個元素是一支給定股票第 i 天的價格。
設計一個算法來計算你所能獲取的最大利潤。你可以儘可能地完成更多的交易(多次買賣一支股票)。
注意:你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。

示例 1:
輸入: [7,1,5,3,6,4]
輸出: 7
解釋: 在第 2 天(股票價格 = 1)的時候買入,在第 3 天(股票價格 = 5)的時候賣出, 這筆交易所能獲得利潤 = 5-1 = 4 。
隨後,在第 4 天(股票價格 = 3)的時候買入,在第 5 天(股票價格 = 6)的時候賣出, 這筆交易所能獲得利潤 = 6-3 = 3 。

示例 2:
輸入: [1,2,3,4,5]
輸出: 4
解釋: 在第 1 天(股票價格 = 1)的時候買入,在第 5 天 (股票價格 = 5)的時候賣出, 這筆交易所能獲得利潤 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接連購買股票,之後再將它們賣出。
因爲這樣屬於同時參與了多筆交易,你必須在再次購買前出售掉之前的股票。

示例 3:
輸入: [7,6,4,3,1]
輸出: 0
解釋: 在這種情況下, 沒有交易完成, 所以最大利潤爲 0。

public static int maxProfit(int[] prices) {
        if (prices == null || prices.length == 0) return 0;
     
        int maxProfit = 0;
        int buyPrice = -1;
        int sellPrice = -1;
        //[7,1,5,3,6,4]
        for (int i = 1; i < prices.length; i++) {
            //漲
            if (prices[i] >= prices[i - 1]) {
                if (buyPrice == -1) {
                    buyPrice = prices[i - 1];

                   // System.out.println("buyPrice: " + buyPrice);
                }
            } else {
                if (sellPrice == -1 && buyPrice != -1) {
                    sellPrice = prices[i - 1];
                   // System.out.println("sellPrice: " + sellPrice);
                }
            }

            if (i == prices.length - 1 && sellPrice == -1 && buyPrice != -1) {
                sellPrice = prices[prices.length - 1];
                //System.out.println("sellPrice: " + sellPrice);
            }

            if (buyPrice != -1 && sellPrice != -1) {
                maxProfit += (sellPrice - buyPrice);
                buyPrice = -1;
                sellPrice = -1;
            }
        }
        return maxProfit;
    }

執行用時:23 ms
內存消耗:39.7 MB

運行完之後,和統計的執行時間相比較發現我的可能邏輯想複雜了,我是把剛要上漲的時候記下買入價格,剛要跌的時候記下賣出價格,計算一次,然後一次類推,然後如果是一直上漲那麼就把最後一個價格作爲賣出價格,最後把每次的最大收益疊加。
然後這個時候我點開別人1ms執行時間的代碼,寫的非常簡單。我就思考哪裏會有問題,然後嘗試着吧System.output.println()去掉後就變成了1ms。很開心。
修改後:
執行用時:1 ms
內存消耗:39.1 MB

旋轉數組

給定一個數組,將數組中的元素向右移動 k 個位置,其中 k 是非負數。

示例 1:
輸入: [1,2,3,4,5,6,7] 和 k = 3
輸出: [5,6,7,1,2,3,4]
解釋:
向右旋轉 1 步: [7,1,2,3,4,5,6]
向右旋轉 2 步: [6,7,1,2,3,4,5]
向右旋轉 3 步: [5,6,7,1,2,3,4]

示例 2:
輸入: [-1,-100,3,99] 和 k = 2
輸出: [3,99,-1,-100]
解釋:
向右旋轉 1 步: [99,-1,-100,3]
向右旋轉 2 步: [3,99,-1,-100]
第一次自己寫沒有好的方法只能暴力。但是提交執行時間過長,無法通過。
儘管做了左旋還是右旋的的判斷,但是k和數組特別大的時候就會非常耗時。

class Solution {
  public static void rotate(int[] nums, int k) {
        if (nums == null || nums.length == 0) return;
        
        int realK = k % nums.length;
        boolean right = true;

        if (realK > (nums.length / 2)) {
            right = false;
            realK = nums.length - realK;
        }

        for (int i = 0; i < realK; i++) {
            if (right) {
                for (int j = 0; j < nums.length; j++) {
                    int t = nums[(j + 1) % nums.length];
                    nums[(j + 1) % nums.length] = nums[0];
                    nums[0] = t;
                }
            } else {
                for (int j = nums.length - 1; j >= 1; j--) {
                    int t = nums[(j - 1) % nums.length];
                    nums[(j - 1) % nums.length] = nums[nums.length - 1];
                    nums[nums.length - 1] = t;
                }
            }
        }

    }
}

第二次查看看到官方解題思路時,發現旋轉法是最好理解的,然後自己寫了一遍,先全部翻轉一次,然後把數組分爲0 - k, k - length兩部分別翻轉,記得k要做取餘否則會超出數組大小,之後才通過。

class Solution {
    public void rotate(int[] nums, int k) {
        if (nums == null || nums.length == 0) return;
        reverse(nums, 0, nums.length);
        reverse(nums, 0, k % nums.length);
        reverse(nums, k % nums.length, nums.length);
    }

    public void reverse(int[] nums, int start, int end) {
        int needReverse = (end - start) / 2;
        for (int i = 0; i < needReverse; i++) {
            int temp = nums[start + i];
            nums[start + i] = nums[end - i - 1];
            nums[end - i - 1] = temp;
        }
    }
}

執行用時:0 ms
內存消耗:40.5 MB

還有一個環狀替換法,還有一種是犧牲空間的使用額外數組的方式,大家可以去看下。
這裏介紹一下環狀替換拓寬下視野:
環狀替換
爲防止數組越界先把k做取餘處理。
從零開始輪詢數組,然後把第一個數緩存在prev,用臨時變量temp緩存要替換位置current + k的數;
用prev替換到要替換位置current + k的數,然後把temp賦值給prev更新prev的值,更新當前位置爲current + k,count數加1。
因爲我們要把對應的數字調整到對應的位置只需要length次就OK了,也就是每個數調整一次位置。
當current等於start的時候說明交換到最後又回到了start的位置上,相當於這一條交換路線我們已經走過一次了,我們需要停止當前交換從下個位置開始繼續交換,否則就會一直跑無限循環。

一般這種情況可以用下邊的方法預計他們的重新開始交換的次數:
當 length 和 k 的最大公約數 等於 1 的時候:1 次遍歷就可以完成交換;
比如 n = 5, k = 3
當 length 和 k 的最大公約數 不等於 1 的時候:1 次遍歷是無法完成的所有元素歸位,需要 m (最大公約數) 次

public static void rotate2(int[] nums, int k) {
        k = k % nums.length;
        int count = 0;
        for (int i : nums) {
            System.out.print(i + ",");
        }

        System.out.print("\n");
        System.out.println("length: " + nums.length);
        System.out.println("k: " + k);
        for (int start = 0; count < nums.length; start++) {
            System.out.println("-------------------");

            int current = start;
            int prev = nums[start];
            System.out.println("緩存下來的數據 prev: " + prev);
            do {
                int next = (current + k) % nums.length;
                System.out.print("當前位置: " + current + " 替換 ");
                System.out.println(next + " 位置內容");

                System.out.print("nums[" + next + "]:" + nums[next] + "-->");

                int temp = nums[next];
                nums[next] = prev;
                prev = temp;
                current = next;
                count++;

                System.out.println("nums[" + next + "]:" + nums[next]);

                System.out.println("移動次數: " + count);

                for (int i : nums) {
                    System.out.print(i + ",");
                }
                System.out.print("\n");
                System.out.println("-------------------");
                System.out.println("緩存下來的數據 prev: " + prev);
            } while (start != current);
            System.out.println("countend: " + count);
            System.out.println("start: " + start);
        }
    }
存在重複元素

給定一個整數數組,判斷是否存在重複元素。
如果任意一值在數組中出現至少兩次,函數返回 true 。如果數組中每個元素都不相同,則返回 false 。
我的思路是先排序一下然後在找出是否有相同的數。
這個還有其他的方式,比如利用HashSet。

public boolean containsDuplicate(int[] nums) {
        if(nums == null || nums.length == 0 || nums.length == 1) return false;
        Arrays.sort(nums);
        int k = nums[0];
        for(int i = 1 ; i< nums.length ; i++) {
            if(k != nums[i]) {
                k = nums[i];
            } else{
                return true;
            }
        }
        return false;
    }

執行用時:4 ms
內存消耗:43.5 MB

只出現一次的數字

給定一個非空整數數組,除了某個元素只出現一次以外,其餘每個元素均出現兩次。找出那個只出現了一次的元素。
說明:
你的算法應該具有線性時間複雜度。 你可以不使用額外空間來實現嗎?
受上一個題的啓發仍然是先排序,排序後肯定都是成對成對的數,然後循環一次,因爲是成對出現,從第一個書開始,如果第一個和第二個不一致記錄標記repeat爲false,則直接的出結果爲nums[i];若果是中間的則是第一個第二個相同則跳到下一對 i= i+1 後邊再 i++,如果這個時候後邊一對不相同則返回nums[i];如果前邊的重複這個時候正好是數組的最後一對判斷,i經過i+1之後,i爲倒數第二個位置,則把最後一個數返回。

public int singleNumber(int[] nums) {
       if (nums.length == 1) return nums[0];
        Arrays.sort(nums);
        boolean repeat = false;
        for (int i = 0; i < nums.length - 1; i++) {
            if (nums[i] == nums[i + 1]) {
                repeat = true;
                i = i + 1;
            }
            if (repeat && i == nums.length - 2) {
                return nums[i + 1];
            }
            if (!repeat) {
                return nums[i];
            }
            if (repeat) {
                repeat = false;
            }
        }
        throw new IllegalArgumentException("Not found single elem");
    }

執行用時:4 ms
內存消耗:41 MB
這個地方感覺有些巧合正好湊了這個出來,看了官方解答之後發現他們有更巧妙地解法,就是用異或。

  • 任何數和 0 做異或運算,結果仍然是原來的數,即 a ⊕ 0=a。
  • 任何數和其自身做異或運算,結果是 0,即a⊕a=0。
  • 異或運算滿足交換律和結合律,即 a⊕b⊕a=b⊕a⊕a=b⊕(a⊕a)=b⊕0=b。
class Solution {
	public int singleNumber(int[] nums) {
		if(nums.length == 1) return nums[0];
		int sum = 0;
		for(int i : nums) {
			sum ^=i;
		}
		return sum;
	}
}

執行用時:1 ms
內存消耗:40.4 MB
執行時間大幅減少。

兩個數組的交集 II

給定兩個數組,編寫一個函數來計算它們的交集。
示例 1:
輸入: nums1 = [1,2,2,1], nums2 = [2,2]
輸出: [2,2]

示例 2:
輸入: nums1 = [4,9,5], nums2 = [9,4,9,8,4]
輸出: [4,9]

說明:
輸出結果中每個元素出現的次數,應與元素在兩個數組中出現的次數一致。
我們可以不考慮輸出結果的順序。

public static int[] intersect2(int[] nums1, int[] nums2) {
        Arrays.sort(nums1);
        Arrays.sort(nums2);
        int i = 0;
        int j = 0;
        int k = 0;
        while(i < nums1.length && j < nums2.length){
            if(nums1[i] == nums2[j]) {
                nums1[k++] = nums1[i];
                i++;
                j++;
            } else if(nums1[i] < nums2[j]) {
                i++;
            } else if(nums1[i] > nums2[j]) {
                j++;
            }
        }
        return Arrays.copyOfRange(nums1,0,k);
    }

執行用時:2 ms
內存消耗:39.7 MB

在答案上邊還有一個是用HashMap的也是可以的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章