leetCode進階算法題+解析(八十九)

不知不覺這個系列已經第八十九篇筆記了。我還記得當時第一篇開章是19年末的時候。因爲是進階算法,所以平時遇到的簡單難度的題目一般都不記在這裏。刷題這麼久也算是從一個小白變成了一個老白。很多時候堅持讓我刷下去的不是種種優點好處,畢竟工作中非算法崗的算法雞肋的很。主要還是一羣一起學習的小夥伴和熱愛吧。初步打算這個系列99篇就結束了。以後還是會刷題的,但是怎麼記筆記再定。

使數組唯一的最小增量

題目:給定整數數組 A,每次 move 操作將會選擇任意 A[i],並將其遞增 1。返回使 A 中的每個值都是唯一的最少操作次數。

示例 1:
輸入:[1,2,2]
輸出:1
解釋:經過一次 move 操作,數組將變爲 [1, 2, 3]。
示例 2:
輸入:[3,2,1,2,1,7]
輸出:6
解釋:經過 6 次 move 操作,數組將變爲 [3, 4, 1, 2, 5, 7]。
可以看出 5 次或 5 次以下的 move 操作是不能讓數組的每個值唯一的。
提示:
0 <= A.length <= 40000
0 <= A[i] < 40000

思路:這個題其實應該比較簡單,而且解題方式也應該多樣的。我暫時id想法是用一個8w大小的布爾數組作爲當前元素是否存在的記錄(因爲最壞的情況4萬元素,每一個都是4w,那麼最大要累加到7999)然後一個一個元素去判斷就行了。我去實現試試。
狗比題目,說是數據範圍四萬以下。結果快十萬的數據都出來了。所以果斷下標越界了,不過我思路應該沒問題。63個測試案例過了57個,並且也是辛苦寫出來了,所以還是貼出來吧:

class Solution {
    public int minIncrementForUnique(int[] nums) {
        if(nums.length == 0) return 0;
        Arrays.sort(nums);
        //因爲只能增加不能減少,所以當前值的能變成的起點應該是上一次填充喝當前值較大的那個
        int start = nums[0];
        int ans = 0;
        boolean[] flags = new boolean[80000];
        for(int i = 0;i<nums.length;i++){
            if(flags[nums[i]] == false){
                flags[nums[i]] = true;//當前值未出現過,可以用
            }else {
                for(int j = Math.max(start,nums[i])+1;j<flags.length;j++){
                    if(flags[j] == false){
                        flags[j] = true;
                        ans += j-nums[i];
                        start = j;
                        break;
                    }
                }
            }
        }
        return ans;
    }
}

然後這個題的解法也是根據上面的代碼有的,挺簡單的邏輯:

class Solution {
    public int minIncrementForUnique(int[] nums) {
        if(nums.length == 0) return 0;
        Arrays.sort(nums);
        //從第二小的開始判斷。所以最小的不用管。
        int start = nums[0]+1;
        int ans = 0;
        for(int i = 1;i<nums.length;i++){
           if(nums[i] < start){//如果當前值比可以使用的下一個值小,那麼要加到可以使用的下一個值。並且可使用的下一個值+1
               ans += start-nums[i];
               start++;
           }else{//當前值大於等於可使用的下一個值,那麼當前值不變,可使用的下一個值變成當前值+1
               start = nums[i]+1;
           }
        }
        return ans;
    }
}

我覺得註釋寫的挺清楚的了。總而言之這個題的重點是可使用值和當前值的比較。不過我的代碼性能不是很好,我去看看性能第一的代碼:
剛去看了性能 前三的代碼,都用到了4w這個極端值。同樣的代碼放到現在去提交一提交一個報錯。呵。所以這個題我就不多看了,直接下一題吧。

驗證棧序列

題目:給定 pushed 和 popped 兩個序列,每個序列中的 值都不重複,只有當它們可能是在最初空棧上進行的推入 push 和彈出 pop 操作序列的結果時,返回 true;否則,返回 false 。

示例 1:
輸入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1]
輸出:true
解釋:我們可以按以下順序執行:
push(1), push(2), push(3), push(4), pop() -> 4,
push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1
示例 2:
輸入:pushed = [1,2,3,4,5], popped = [4,3,5,1,2]
輸出:false
解釋:1 不能在 2 之前彈出。
提示:
0 <= pushed.length == popped.length <= 1000
0 <= pushed[i], popped[i] < 1000
pushed 是 popped 的排列。

思路:這個題我覺得應該是先push。push到pop的點就pop。如果無可push還不能全部pop就false。都能push都能pop就true。感覺應該不難,我去代碼實現試試。
第一版代碼:

class Solution {
    public boolean validateStackSequences(int[] pushed, int[] popped) {
        if(pushed.length == 0) return true;
        //因爲push是追加。pop是從頭彈出。所以我們可以一邊push一邊pop。發現和實際不符合就false
        Stack<Integer> stack = new Stack<>();
        stack.push(pushed[0]);
        int push = 1;
        int pop = 0;
        while(push < pushed.length){
            while (!stack.isEmpty() && stack.peek() == popped[pop]){
                stack.pop();
                pop++;
            }
            stack.push(pushed[push++]);
        }
        //到這裏說明都push進來了,不會存在空的現象。所以只要判斷pop的長度和隊首值就可以了。
        while (pop<popped.length && stack.peek() == popped[pop]){
            stack.pop();
            pop++;
        }
        if(stack.isEmpty()){
            return true;
        }else {
            return false;
        }
    }
}

這個題怎麼說呢,跟之前想的差不多,肯定push在pop前。然後因爲值是唯一的。所以遇到能pop就pop。最後只要看能不能把全部的棧中元素pop出去,能就說明true,不能就false。我這個代碼新能也還算是不錯。我去看看性能第一的代碼:

class Solution {
    public boolean validateStackSequences(int[] pushed, int[] popped) {
        int[] stack=new int[pushed.length];
        int point=0;
        int Point=0;
        for(int i=0;i<pushed.length;i++){
            stack[point]=pushed[i];
            for(int j=point;j>=0;j--){
                if(stack[point]==popped[Point]){
                    stack[point]=0;
                    point--;
                    Point++;
                }
                else
                    break;  
            }
            point++;
        }
        if(Point==popped.length)
            return true;
        else
            return false;
    }
}

我用了棧這種數據結構來很方便的peek,push,pop。不過人家用的數組,估計性能是好在這裏吧。其實本質上最後也是看能退出(pop)的數量是不是全部的元素。大同小異,不多說了。下一題。

令牌放置

題目:你的初始 能量 爲 P,初始 分數 爲 0,只有一包令牌 tokens 。其中 tokens[i] 是第 i 個令牌的值(下標從 0 開始)。令牌可能的兩種使用方法如下:
如果你至少有 token[i] 點 能量 ,可以將令牌 i 置爲正面朝上,失去 token[i] 點 能量 ,並得到 1 分 。
如果我們至少有 1 分 ,可以將令牌 i 置爲反面朝上,獲得 token[i] 點 能量 ,並失去 1 分 。
每個令牌 最多 只能使用一次,使用 順序不限 ,不需 使用所有令牌。在使用任意數量的令牌後,返回我們可以得到的最大 分數 。

示例 1:
輸入:tokens = [100], P = 50
輸出:0
解釋:無法使用唯一的令牌,因爲能量和分數都太少了。
示例 2:
輸入:tokens = [100,200], P = 150
輸出:1
解釋:令牌 0 正面朝上,能量變爲 50,分數變爲 1 。不必使用令牌 1 ,因爲你無法使用它來提高分數。
示例 3:
輸入:tokens = [100,200,300,400], P = 200
輸出:2
解釋:按下面順序使用令牌可以得到 2 分:

  1. 令牌 0 正面朝上,能量變爲 100 ,分數變爲 1
  2. 令牌 3 正面朝下,能量變爲 500 ,分數變爲 0
  3. 令牌 1 正面朝上,能量變爲 300 ,分數變爲 1
  4. 令牌 2 正面朝上,能量變爲 0 ,分數變爲 2
    提示:
    0 <= tokens.length <= 1000
    0 <= tokens[i], P < 104

思路:這個題看似很複雜,其實只要遵守兩個原則:用最少的能量換分。用分換最大的能量。這兩個原則來用貪心算最多能多少有分就行了。感覺應該不復雜,我去實現試試。
第一版代碼:

class Solution {
    public int bagOfTokensScore(int[] tokens, int power) {
        Arrays.sort(tokens);
        //積分從小到大去換。能量從大到小去換。而且應該以積分爲主。
        int l = 0;
        int r = tokens.length-1;
        int ans = 0;
        while(l<=r){
            if(power>=tokens[l]){
                power -= tokens[l];
                ans++;//積分+1
                l++;
            }else{
                if(ans<1 || l == r) return  ans;
                ans--;//用一積分去換能量了。所以積分-1
                power += tokens[r];
                r--;
            }
        }
        return ans;
    }
}

這個題還是挺簡單的,主要是思路清晰就可以了。重點在於上面說的:積分用最少的能量換。積分換能量要換最多的能量。還有就是如果遇到最便宜的能量都買不起了就可以直接return了。因爲我這個代碼性能超過了百分百,所以就不看別人的代碼了,直接下一題。

按照遞增順序顯示卡牌

題目:牌組中的每張卡牌都對應有一個唯一的整數。你可以按你想要的順序對這套卡片進行排序。最初,這些卡牌在牌組裏是正面朝下的(即,未顯示狀態)。現在,重複執行以下步驟,直到顯示所有卡牌爲止:
從牌組頂部抽一張牌,顯示它,然後將其從牌組中移出。
如果牌組中仍有牌,則將下一張處於牌組頂部的牌放在牌組的底部。
如果仍有未顯示的牌,那麼返回步驟 1。否則,停止行動。
返回能以遞增順序顯示卡牌的牌組順序。答案中的第一張牌被認爲處於牌堆頂部。

示例:
輸入:[17,13,11,2,3,5,7]
輸出:[2,13,3,11,5,17,7]
解釋:
我們得到的牌組順序爲 [17,13,11,2,3,5,7](這個順序不重要),然後將其重新排序。
重新排序後,牌組以 [2,13,3,11,5,17,7] 開始,其中 2 位於牌組的頂部。
我們顯示 2,然後將 13 移到底部。牌組現在是 [3,11,5,17,7,13]。
我們顯示 3,並將 11 移到底部。牌組現在是 [5,17,7,13,11]。
我們顯示 5,然後將 17 移到底部。牌組現在是 [7,13,11,17]。
我們顯示 7,並將 13 移到底部。牌組現在是 [11,17,13]。
我們顯示 11,然後將 17 移到底部。牌組現在是 [13,17]。
我們展示 13,然後將 17 移到底部。牌組現在是 [17]。
我們顯示 17。
由於所有卡片都是按遞增順序排列顯示的,所以答案是正確的。
提示:
1 <= A.length <= 1000
1 <= A[i] <= 10^6
對於所有的 i != j,A[i] != A[j]

思路:這個題想了好久,什麼奇偶排序,遞歸之類的。後來越想越覺得沒必要。其實這個題我們完全可以用模擬的方式來實現。每次拿出一個下一張牌放在牌組的最後面。我們完全可以用一個鏈表來模擬,取第一個元素,然後把下一個元素放到鏈表的最後。用這種方式來把所有的牌填充完。思路比較清晰,我去試試代碼。
第一版代碼:

class Solution {
    public int[] deckRevealedIncreasing(int[] deck) {
        LinkedList<Integer> list = new LinkedList<>();
        for(int i = 0;i<deck.length;i++) list.add(i);
        Arrays.sort(deck);
        int[] ans = new int[deck.length];
        int i = 0;
        while(list.size()>0){
            ans[list.removeFirst()] = deck[i++];
            if(list.size()>0) {
                list.add(list.removeFirst());
            }
        }
        return ans;
    }
}

總而言之思路和上面的差不多,代碼的話就是用鏈表模擬卡牌牌組就好了。然後從小到大的順序其實就是牌本身的順序。感覺這個題的難度不是代碼而是思路。這個題的性能還不錯,我去看看性能第一的代碼:
emmm...這個性能第一的代碼有點類似於我最開始的思路。只不過我越想越複雜就沒去實現,換了思路,結果人家實現了。附上代碼:

class Solution {
    public int[] sortedDeckRevealedIncreasing(int[] deck) {
        if (deck.length == 1)
            return deck;
        int[] right = new int[deck.length / 2];
        for (int i = 0; i < right.length; i++)
            right[i] = deck[deck.length - right.length + i];
        right = sortedDeckRevealedIncreasing(right);
        int[] newDeck = new int[deck.length];
        for (int i = 0; i < deck.length; i += 2)
            newDeck[i] = deck[i / 2];
        if ((deck.length & 1) == 1) {
            newDeck[1] = right[right.length - 1];
            for (int i = 0; i < right.length - 1; i++)
                newDeck[i * 2 + 3] = right[i];
        }
        else
            for (int i = 0; i < deck.length; i += 2)
                newDeck[i + 1] = right[i / 2];
        return newDeck;
    }

    public int[] deckRevealedIncreasing(int[] deck) {
        Arrays.sort(deck);
        return sortedDeckRevealedIncreasing(deck);
    }
}

首先是分奇偶,然後隔一個填充一次。並且用的遞歸實現。但是這個代碼的複雜性比我之前寫的複雜多了,這個代碼仔細讀一遍是能讀懂的,但是寫不出來,哈哈,下一題了。

翻轉等價二叉樹

題目:我們可以爲二叉樹 T 定義一個翻轉操作,如下所示:選擇任意節點,然後交換它的左子樹和右子樹。只要經過一定次數的翻轉操作後,能使 X 等於 Y,我們就稱二叉樹 X 翻轉等價於二叉樹 Y。編寫一個判斷兩個二叉樹是否是翻轉等價的函數。這些樹由根節點 root1 和 root2 給出。

示例:
輸入:root1 = [1,2,3,4,5,6,null,null,null,7,8], root2 = [1,3,2,null,6,4,5,null,null,null,null,8,7]
輸出:true
解釋:我們翻轉值爲 1,3 以及 5 的三個節點。



提示:
每棵樹最多有 100 個節點。
每棵樹中的每個值都是唯一的、在 [0, 99] 範圍內的整數。

思路:這個題怎麼說呢,我覺得挺簡單的,首先從最頂層開始判斷,每個元素只要左右節點相等說明就可以換。遇到父/左/右三點不相等說明false。理論比較好理解,下面我去代碼試試。
只能說樹的題目,遞歸yyds,直接上第一版代碼:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public boolean flipEquiv(TreeNode root1, TreeNode root2) {
        if(root1 == root2) return true;
        if (root1 != null && root2 != null && root1.val == root2.val){
            return (flipEquiv(root1.left, root2.left) && flipEquiv(root1.right, root2.right))||(flipEquiv(root1.left,root2.right)&&flipEquiv(root1.right,root2.left));
        }
        return false;
    }
}

我本來還以爲性能不見得特別好,沒想到直接超過百分百。總而言之就是遞歸。因爲可以交換,所以滿足的條件是左右相等或者說左右互等。代碼比較簡單,就不多說了,剛剛看了眼題解,感覺大多數思路都這樣,所以這個題就過了。
本篇筆記就記到這了,如果稍微幫到你了記得點個喜歡點個關注,也祝大家工作順順利利,感覺羣裏一句話挺好的:今天不學習,明天變垃圾。所以一起加油吧!

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