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;
    }
}

我本来还以为性能不见得特别好,没想到直接超过百分百。总而言之就是递归。因为可以交换,所以满足的条件是左右相等或者说左右互等。代码比较简单,就不多说了,刚刚看了眼题解,感觉大多数思路都这样,所以这个题就过了。
本篇笔记就记到这了,如果稍微帮到你了记得点个喜欢点个关注,也祝大家工作顺顺利利,感觉群里一句话挺好的:今天不学习,明天变垃圾。所以一起加油吧!

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