leetCode进阶算法题+解析(七十八)

数组中的最长山脉

题目:我们把数组 A 中符合下列属性的任意连续子数组 B 称为 “山脉”:B.length >= 3
存在 0 < i < B.length - 1 使得 B[0] < B[1] < ... B[i-1] < B[i] > B[i+1] > ... > B[B.length - 1]
(注意:B 可以是 A 的任意子数组,包括整个数组 A。)给出一个整数数组 A,返回最长 “山脉” 的长度。如果不含有 “山脉” 则返回 0。

示例 1:
输入:[2,1,4,7,3,2,5]
输出:5
解释:最长的 “山脉” 是 [1,4,7,3,2],长度为 5。
示例 2:
输入:[2,2,2]
输出:0
解释:不含 “山脉”。
提示:
0 <= A.length <= 10000
0 <= A[i] <= 10000

思路:类似的题好像挺多的。简而言之其实没有山脉的情况就是数组是持平/递增/递减的。否则每一个峰值都能形成一个山脉。然后这个题的标签是双指针。然后我个人的想法是这个题的答案除了最后一个元素不会有重合部分。比如山 1 3 2,和下一个山(如果有下一个山的话)只有2有可能是重合的。这里也只是有可能。1,3绝对不可能用到。因为3,2的往下走的。下一个山一定开始是往上。说这么多我只是想说明这个题可能一次遍历就能解决,我去试试这个思路。

class Solution {
    public int longestMountain(int[] arr) {
        int max = 0;
        int len = arr.length;
        for(int i = 1;iarr[i-1] && arr[i]>arr[i+1]) {
                int left = i-1;
                int right = i+1;
                while(left>0 && arr[left]>arr[left-1]) left--;
                while(rightarr[right+1]) right++;
                max = Math.max(max, right-left+1);
            }
        }
        return max;
    }
}

事实证明想那么多没啥用。这个题的解法就是单纯的找到峰顶然后判断山长。取最长的就行。本质上看似是双层循环。一层for一层while,但是本质上像我上面说的其实几乎不会有重复遍历的字符。也就是不至于n方的时间复杂度的。上面代码性能超过百分百了,所以我就不浪费时间了,直接下一题。

一手顺子

题目:爱丽丝有一手(hand)由整数数组给定的牌。 现在她想把牌重新排列成组,使得每个组的大小都是 W,且由 W 张连续的牌组成。如果她可以完成分组就返回 true,否则返回 false。

示例 1:
输入:hand = [1,2,3,6,2,3,4,7,8], W = 3
输出:true
解释:爱丽丝的手牌可以被重新排列为 [1,2,3],[2,3,4],[6,7,8]。
示例 2:
输入:hand = [1,2,3,4,5], W = 4
输出:false
解释:爱丽丝的手牌无法被重新排列成几个大小为 4 的组。
提示:
1 <= hand.length <= 10000
0 <= hand[i] <= 10^9
1 <= W <= hand.length

思路:这个题怎么说呢,因为数据范围是10的九次方。所以用数组下标代替值值代表个数的方法就不行了。不过我觉得这个题其实不至于这么复杂。应该可以直接排序存储。然后获取的时候是顺序往下看能不能续上上一次。如果上一个不缺了的话则开启新的。如果上一个缺但是还续不上则直接false。总而言之思路就是这样。我去试试代码。
第一版本代码:

class Solution {
    public boolean isNStraightHand(int[] hand, int W) {
        if(W == 1) return true;
        Arrays.sort(hand);
        List> list = new ArrayList>(); 
        for(int i = 0;i=0;j--) {
                List cur = list.get(j);
                //因为是递增的,所以这个不会存在下一个了,直接false
                if(cur.get(cur.size()-1)+1 cur = new ArrayList();
                cur.add(temp);
                list.add(cur);
            }
        }
        return list.size() == 0;
    }
}

不仅ac,而且还性能不错。超过了百分之八十六的用户。而且其实实际上比我想的还要简单,总而言之这个题目暴力法还是比较容易做出来的。但是我这个性能不是最好的,而且总有模糊的感觉这个题应该有什么取巧的办法,我去看看性能第一的代码:

    class Solution {
        public boolean isNStraightHand(int[] hand, int W) {

            int len = hand.length;
            if (len % W != 0) return false;
            Arrays.sort(hand);
            boolean[] taken = new boolean[len];
            for (int i = 0; i < len; i++) {
                if (taken[i]) continue;
                taken[i] = true;
                int cur = hand[i] + 1;
                int end = hand[i] + W - 1;
                // 直接冲下一个位置开始遍历,因为用的hand那么hand[i]必定存在值
                int j = i + 1;
                while (cur <= end && j < len) {
                    if (hand[j] == cur && !taken[j]) {
                        cur++;
                        taken[j] = true;
                    }
                    j++;
                }
                if (cur != end + 1) return false;
            }
            return true;
        }
    }

这个代码的处理只用了一个布尔数组做记忆化,没什么额外的空间。所以性能比较好。然后也比较好理解,就是一时间没想到要这么做。因为不难所以就不多说了,下一题。

字母位移

题目:有一个由小写字母组成的字符串 S,和一个整数数组 shifts。我们将字母表中的下一个字母称为原字母的 移位(由于字母表是环绕的, 'z' 将会变成 'a')。例如·,shift('a') = 'b', shift('t') = 'u',, 以及 shift('z') = 'a'。对于每个 shifts[i] = x , 我们会将 S 中的前 i+1 个字母移位 x 次。返回将所有这些移位都应用到 S 后最终得到的字符串。

示例:
输入:S = "abc", shifts = [3,5,9]
输出:"rpl"
解释:
我们以 "abc" 开始。
将 S 中的第 1 个字母移位 3 次后,我们得到 "dbc"。
再将 S 中的前 2 个字母移位 5 次后,我们得到 "igc"。
最后将 S 中的这 3 个字母移位 9 次后,我们得到答案 "rpl"。
提示:
1 <= S.length = shifts.length <= 20000
0 <= shifts[i] <= 10 ^ 9

思路:这个题怎么说呢,首先这个位移数十的九次方。因为只有26个字母,所以这个绝对可以用取余的方式化简。然后剩下的我不确定暴力法能不能ac。而且其实我们可以用一个数组计算出每一位字母的最终位移数。再化简。这样只位移一次就行了。我感觉是可以,我去代码实现下试试。
第一版代码:

class Solution {
    public String shiftingLetters(String S, int[] shifts) {
        char[] dir = new char[] {'a','b','c','d','e','f','g','h','i','j',
                'k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'};
        int len = shifts.length;
        long[] d = new long[len];
        for(int i = 0;i

总而言之,超时一次,低空略过一次。性能差的一批,能ac是运气了。上面的代码思路和我之前想的差不多。第一次是用的int作为计算量。但是每次都要取模26.所以超时了。第二次果断用long,起码是过了。然后优化的点应该还是在于我这边的双层for循环,其实这个只要算一次就行了。有了大概的思路。我去实现下试试。
第二版本代码:

class Solution {
    public String shiftingLetters(String S, int[] shifts) {
        char[] dir = new char[] {'a','b','c','d','e','f','g','h','i','j',
                'k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'};
        int len = shifts.length;
        long[] d = new long[len];
        d[len-1] = shifts[len-1]; 
        for(int i = len-2;i>=0;i--) d[i] = d[i+1]+shifts[i];
        for(int i = 0;i

这个性能起码不是低空过了,虽然也不是很好,下面我去看看性能第一的代码吧:

class Solution {
    public String shiftingLetters(String S, int[] shifts) {

        for (int i = 0; i < shifts.length; i++) {
            shifts[i] = shifts[i] % 26;
        }

        for (int i = shifts.length-2; i >= 0 ; i--) {
            shifts[i] += shifts[i+1];
        }

        char[] SChr = S.toCharArray();
        for (int i = 0; i < shifts.length; i++) {
            SChr[i] = (char)((SChr[i] - 'a' + shifts[i]) % 26 + 'a');
        }

        return String.valueOf(SChr);
    }
}

感觉思路差不多,还是细节处理上不同。我用了stringbuffer来追加。然后这个代码是用char数组的。总而言之思路比较容易想,细节处理上有的练。下一题了。

喧嚣和富有

题目:在一组 N 个人(编号为 0, 1, 2, ..., N-1)中,每个人都有不同数目的钱,以及不同程度的安静(quietness)。为了方便起见,我们将编号为 x 的人简称为 "person x "。如果能够肯定 person x 比 person y 更有钱的话,我们会说 richer[i] = [x, y] 。注意 richer 可能只是有效观察的一个子集。另外,如果 person x 的安静程度为 q ,我们会说 quiet[x] = q 。现在,返回答案 answer ,其中 answer[x] = y 的前提是,在所有拥有的钱不少于 person x 的人中,person y 是最安静的人(也就是安静值 quiet[y] 最小的人)。

示例:
输入:richer = [[1,0],[2,1],[3,1],[3,7],[4,3],[5,3],[6,3]], quiet = [3,2,5,4,6,1,7,0]
输出:[5,5,2,5,4,5,6,7]
解释:
answer[0] = 5,
person 5 比 person 3 有更多的钱,person 3 比 person 1 有更多的钱,person 1 比 person 0 有更多的钱。
唯一较为安静(有较低的安静值 quiet[x])的人是 person 7,
但是目前还不清楚他是否比 person 0 更有钱。
answer[7] = 7,
在所有拥有的钱肯定不少于 person 7 的人中(这可能包括 person 3,4,5,6 以及 7),
最安静(有较低安静值 quiet[x])的人是 person 7。
其他的答案也可以用类似的推理来解释。
提示:
1 <= quiet.length = N <= 500
0 <= quiet[i] < N,所有 quiet[i] 都不相同。
0 <= richer.length <= N * (N-1) / 2
0 <= richer[i][j] < N
richer[i][0] != richer[i][1]
richer[i] 都是不同的。
对 richer 的观察在逻辑上是一致的。

思路:这个题一看就比较复杂。入参一个数组型数组。而且这个题的关系比较乱。当然了这种关系比较容易想到的一种数据类型:拓扑图。而且本来也是可以类比的:A比B大。C比B小。我们可以得出结果A比C大。同理每一个元素都是可以这样获取关系。因为题目是不小于。所以哪怕一个元素没有任何比较,我们无法得知他的任何关系,还有他自己在那保底。总而言之我的思路:第一步,构图。第二步:遍历每一个的结果集获取最安静的。我去试着写代码。
第一版代码:

class Solution {
    ArrayList[] list;
    int[] quiet;
    public int[] loudAndRich(int[][] richer, int[] quiet) {
        list = new ArrayList[quiet.length];
        this.quiet = quiet;
        //构图
        for(int i = 0;i(); 
        for(int[] i : richer) list[i[1]].add(i[0]);//i[0]比i[1]富有。所以i[0]进入i[1]的圈子
        int[] ans = new int[quiet.length];
        Arrays.fill(ans,-1);//初始值设置为-1.因为有的人可能结果是0,那样我们不知道是判断过没有
        for(int i = 0;iquiet[temp]) res = temp;
        }
        return res;
    }
}

这个题让我说的话:题目不难,就是贼绕。
最大的复杂的点不是构图,是求的是人。比较的是安静程度。所以这个dfs里我各种尝试好几次才实现了这么简单的几行代码。
总而言之这个题一定一定一定思路要清晰,不然做着做着就容易懵。其次构图算是一点吧,一开始我用新循环的形式赋初值,结果发现不行。。非要用list[i]这种才可以。这个也算是我新知道的一个小技巧。剩下的也就没啥了。上面代码性能就挺好的,我去看看性能第一的代码:

class Solution {
    ArrayList[] graph;
    int[] answer;
    int[] quiet;

    public int[] loudAndRich(int[][] richer, int[] quiet) {
        int N = quiet.length;
        graph = new ArrayList[N];
        answer = new int[N];
        this.quiet = quiet;

        for (int node = 0; node < N; ++node)
            graph[node] = new ArrayList();

        for (int[] edge: richer)
            graph[edge[1]].add(edge[0]);

        Arrays.fill(answer, -1);

        for (int node = 0; node < N; ++node)
            dfs(node);
        return answer;
    }

    public int dfs(int node) {
        if (answer[node] == -1) {
            answer[node] = node;
            for (int child: graph[node]) {
                int cand = dfs(child);
                if (quiet[cand] < quiet[answer[node]])
                    answer[node] = cand;
            }
        }
        return answer[node];
    }
}

和我的思路大同小异,细节的处理上我是返回值,人家是直接在dfs里赋值了,也就这点区别。这个题过了。

车队

题目:N 辆车沿着一条车道驶向位于 target 英里之外的共同目的地。每辆车 i 以恒定的速度 speed[i] (英里/小时),从初始位置 position[i] (英里) 沿车道驶向目的地。一辆车永远不会超过前面的另一辆车,但它可以追上去,并与前车以相同的速度紧接着行驶。此时,我们会忽略这两辆车之间的距离,也就是说,它们被假定处于相同的位置。车队 是一些由行驶在相同位置、具有相同速度的车组成的非空集合。注意,一辆车也可以是一个车队。即便一辆车在目的地才赶上了一个车队,它们仍然会被视作是同一个车队。会有多少车队到达目的地?

示例:
输入:target = 12, position = [10,8,0,5,3], speed = [2,4,1,1,3]
输出:3
解释:
从 10 和 8 开始的车会组成一个车队,它们在 12 处相遇。
从 0 处开始的车无法追上其它车,所以它自己就是一个车队。
从 5 和 3 开始的车会组成一个车队,它们在 6 处相遇。
请注意,在到达目的地之前没有其它车会遇到这些车队,所以答案是 3。
提示:
0 <= N <= 10 ^ 4
0 < target <= 10 ^ 6
0 < speed[i] <= 10 ^ 6
0 <= position[i] < target
所有车的初始位置各不相同。

思路:首先分析这个题:1.每一个车到达需要的时间。2.车辆起始位置(因为后面的车不能超车)。3.我的想法是后面的车时间小于前面的车。则统一一批到达。大概思路比较清楚,但是不知道怎么用文字表达,我直接去试试代码
第一版代码:

class Solution {
    public int carFleet(int target, int[] position, int[] speed) {
        int len = position.length;
        double[][] time = new double[len][2];
        for(int i = 0;i() {
            @Override
            public int compare(double[] o1, double[] o2) {
                return (int)(o2[0]-o1[0]);
            }
        });
        double pre = -1;
        int ans = 0;
        for(int i = 0;ipre){//当前车达到时间大于前面那个车。所以单独是一队
                pre = time[i][1];
                ans++;
            }//不进入if说明当前车比之前车早到达。但是因为不能超车,所以同时到达,是一批
        }
        return ans;
    }
}

ac是ac了,但是这个代码性能不咋地。总而言之这个题单纯的做出来比较简单,就是数据处理稍微复杂点。首先要判断速度。其次是其实位置的倒叙(因为起始位置越大其实是越靠前的)。然后就好像我上面说的:思路清楚了其实这些细节都是可以一点点扣的。然后性能不太好我觉得优化的点不太确定。毕竟思路不太会变。剩下的就是数据结构的变化了。不知道用treeMap会不会好。因为每个车起始位置不一样所以到不用担心key重复。可是完全不觉得map会比数据性能好。我再寻思寻思:


越改性能越差系列

总而言之改成map了,性能更差了,虽然代码好像是简洁了,附上代码:

class Solution {
    public int carFleet(int target, int[] position, int[] speed) {
        int len = position.length;
        Map map = new TreeMap<>();
        for(int i = 0;ipre){//当前车达到时间大于前面那个车。所以单独是一队
                pre = d;
                ans++;
            }//不进入if说明当前车比之前车早到达。但是因为不能超车,所以同时到达,是一批
        }
        return ans;
    }
}

然后对于优化我暂时没啥思路了,我去看看性能第一的代码:

class Solution {
    public int carFleet(int target, int[] position, int[] speed) {
        int n = position.length;
        if (n < 2) {
            return n;
        }
        long[] aux = new long[n];
        for (int i = 0; i < n; i++) {
            aux[i] = (long) (target - position[i]) * 10000000 + speed[i];
        }
        Arrays.sort(aux);
        int ans = 0;
        double max = 0.0;
        for (int i = 0; i < n; i++) {
            double time = (double) (aux[i] / 10000000) / (aux[i] % 10000000);
            if (time > max) {
                max = time;
                ans++;
            }
        }
        return ans;
    }
}

说实话,我是真的不服,这个代码是用数值表示两个意思。(因为最大范围10的6次方。所以速度的余数。距离是整数前面的那个。)也就是数值即表示了速度也表示了路程。但是为什么性能就好了这么多?就是排序的时候吧。。。emmmm,,,总而言之思路是很新鲜。主要是这样会让性能好是我第一次知道。别的思路差不多,这个题就这样了。下一题。

考场就坐

题目:在考场里,一排有 N 个座位,分别编号为 0, 1, 2, ..., N-1 。当学生进入考场后,他必须坐在能够使他与离他最近的人之间的距离达到最大化的座位上。如果有多个这样的座位,他会坐在编号最小的座位上。(另外,如果考场里没有人,那么学生就坐在 0 号座位上。)
返回 ExamRoom(int N) 类,它有两个公开的函数:其中,函数 ExamRoom.seat() 会返回一个 int (整型数据),代表学生坐的位置;函数 ExamRoom.leave(int p) 代表坐在座位 p 上的学生现在离开了考场。每次调用 ExamRoom.leave(p) 时都保证有学生坐在座位 p 上。

示例:
输入:["ExamRoom","seat","seat","seat","seat","leave","seat"], [[10],[],[],[],[],[4],[]]
输出:[null,0,9,4,2,null,5]
解释:
ExamRoom(10) -> null
seat() -> 0,没有人在考场里,那么学生坐在 0 号座位上。
seat() -> 9,学生最后坐在 9 号座位上。
seat() -> 4,学生最后坐在 4 号座位上。
seat() -> 2,学生最后坐在 2 号座位上。
leave(4) -> null
seat() -> 5,学生最后坐在 5 号座位上。
提示:
1 <= N <= 10^9
在所有的测试样例中 ExamRoom.seat() 和 ExamRoom.leave() 最多被调用 10^4 次。
保证在调用 ExamRoom.leave(p) 时有学生正坐在座位 p 上。

思路:这个题的第一个肯定是填充0.但是因为N的范围比较大。所以用枚举肯定有点不现实。我的想法就是两端的值有个范围。其次往里插入一个算一个元素。然后每次插入都要从头往后算两个元素的中间值距离大小。
第一版ac代码:

class ExamRoom {
    int len;
    Set set;//用Treeset的自带排序

    public ExamRoom(int N) {
         this.len = N-1;
         set = new TreeSet();
    }
    
    public int seat() {
        if(set.size() == 0) {
            set.add(0);
            return 0;
        }
        int ans = -1;//结果的位置
        int temp = -1;//最近的同学
        int pre = Integer.MIN_VALUE;//上一个位置的同学
        for(int i : set) {
            if(pre == Integer.MIN_VALUE && i>0) {
                temp = i;
                ans = 0;
                pre = i;
                continue;
            }
            int j = i-pre;//两个同学之间的距离
            if(j/2>temp) {//当前同学的最小距离大于结果的
                temp = j/2;
                ans = pre+j/2;
            }
            pre = i;
        }
        if(len-pre>temp) {
            ans = len;
        }
        set.add(ans);
        return ans;
    }
    
    public void leave(int p) {
        set.remove(p);
    }
}

/**
 * Your ExamRoom object will be instantiated and called as such:
 * ExamRoom obj = new ExamRoom(N);
 * int param_1 = obj.seat();
 * obj.leave(p);
 */

这个题思路上和一开始的类似,没有什么特别难的,然后处理上一开始我打算用list,后来发现需要自带排序,所以改成了treeSet。然后我这个代码虽然ac了,但是性能不太好。我觉得应该可能还是有更好的方式来处理这个差距。不知道优先队列能不能性能好一点。但是我确实是懒得去重写了,直接看性能第一的代码了:
性能第一的代码:

class Node{
    int val;
    Node pre;
    Node next;
    Node(int val){
        this.val = val;
    }
    Node(int val, Node pre, Node next){
        this.val = val;
        this.pre = pre;
        this.next = next;
    }
}

class ExamRoom {
    int cap;
    Node root;

    public ExamRoom(int N) {
        cap = N;
        root = new Node(0);

    }
    
    public int seat() {
        if(root.next == null){
            root.next = new Node(0, root, null);
            return 0;
        }
        Node cur = root.next;
        int mySeat = 0;
        int len = cur.val - mySeat;
        Node charu = root;
        while(cur.next != null){
            if(((cur.next.val - cur.val) / 2) > len){
                mySeat = cur.val + ((cur.next.val - cur.val) / 2);
                len = (cur.next.val - cur.val) / 2;
                charu = cur;
            }
            cur = cur.next;
        }
        if((cap - 1 - cur.val) > len){
            mySeat = cap - 1;
            charu = cur;
        }
        Node newStudent = new Node(mySeat, charu, charu.next);
        if(charu.next != null){
            charu.next.pre = newStudent;
        }
        charu.next = newStudent;
        return mySeat;

    }
    
    public void leave(int p) {
        Node cur = root.next;
        while(cur != null){
            if(cur.val == p){
                cur.pre.next = cur.next;
                if(cur.next != null){
                    cur.next.pre = cur.pre;
                }
                break;
            }
            cur = cur.next;
        }
    }
}

/**
 * Your ExamRoom object will be instantiated and called as such:
 * ExamRoom obj = new ExamRoom(N);
 * int param_1 = obj.seat();
 * obj.leave(p);
 */

用了一个链表来存储的。虽然代码更复杂但是确实性能好了很多。然后我刚刚又扫了性能第二的代码,用的就是优先队列+map边界。也顺便附上代码:

import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Queue;

public class ExamRoom {
    Queue pq = new PriorityQueue<>(new Comparator() {
        @Override
        public int compare(Integer[] interval1, Integer[] interval2) {
            int alen = distance(interval1);
            int blen = distance(interval2);

            if(alen == blen)
                return interval1[0] - interval2[0];
            else
                return blen - alen;
        }
    });

    int N;
    Map leftMap, rightMap;

    public ExamRoom(int N) {

        this.N = N;
        this.leftMap = new HashMap<>();
        this.rightMap = new HashMap<>();
        Integer[] dummy = new Integer[]{-1, N};
        this.pq.add(dummy);
        this.leftMap.put(-1, dummy);
        this.rightMap.put(N, dummy);
        
    }
    
    public int seat() {
        Integer[] itv = pq.poll();
        int left = itv[0];
        int right = itv[1];
        int where;
        if(left == -1){ // 0
            where = 0;
        }
        else if(right == N){ // N-1
            where = N-1;
        }
        else{ // (left + right) / 2
            where = (left + right) / 2;
        }
        // System.out.println(where);
        place(left, right, where);
        return where;

    }
    
    public void leave(int p) {
        // System.out.println(p);
        if(pq.isEmpty())
            return;
        Integer[] leftPart = rightMap.get(p);
        Integer[] rightPart = leftMap.get(p);
        rightMap.remove(p);
        leftMap.remove(p);
        pq.remove(leftPart);
        pq.remove(rightPart);
        Integer left = leftPart[0];
        Integer right = rightPart[1];
        Integer[] newOne = new Integer[]{left, right};
        leftMap.put(left, newOne);
        rightMap.put(right, newOne);
        pq.add(newOne);
    }

    public int distance(Integer[] a){
        if(a[0] == -1 || a[1] == N){
            return a[1] - a[0] - 2;
        }
        else
            return (a[1] - a[0]) / 2 - 1;
    }

    public void place(int left, int right, int where){

        Integer[] new1 = new Integer[]{left, where};
        Integer[] new2 = new Integer[]{where, right};
        pq.add(new1);
        pq.add(new2);
        leftMap.put(left, new1);
        leftMap.put(where, new2);
        rightMap.put(right, new2);
        rightMap.put(where, new1);
    }
}

/**
 * Your ExamRoom object will be instantiated and called as such:
 * ExamRoom obj = new ExamRoom(N);
 * int param_1 = obj.seat();
 * obj.leave(p);
 */

这种题目怎么说呢,因为是开放式的所以其实解法挺多的,主要是思路。其次是优化的角度(比如说用链表我就完全没想到)。只能多熟悉熟悉估计会好一些吧。这个题就这么过了。
本篇笔记就记到这里,如果稍微帮到你了记得点个喜欢点个关注。也祝大家工作顺顺利利,生活健健康康。愿所有的努力不被辜负。

你可能感兴趣的:(leetCode进阶算法题+解析(七十八))