美文网首页java学习之路
leetCode进阶算法题+解析(五十一)

leetCode进阶算法题+解析(五十一)

作者: 唯有努力不欺人丶 | 来源:发表于2020-09-15 15:08 被阅读0次

    根据字符出现频率排序

    题目:给定一个字符串,请将字符串里的字符按照出现的频率降序排列。

    示例 1:
    输入:
    "tree"
    输出:
    "eert"
    解释:
    'e'出现两次,'r'和't'都只出现一次。
    因此'e'必须出现在'r'和't'之前。此外,"eetr"也是一个有效的答案。
    示例 2:
    输入:
    "cccaaa"
    输出:
    "cccaaa"
    解释:
    'c'和'a'都出现三次。此外,"aaaccc"也是有效的答案。
    注意"cacaca"是不正确的,因为相同的字母必须放在一起。
    示例 3:
    输入:
    "Aabb"
    输出:
    "bbAa"
    解释:
    此外,"bbaA"也是一个有效的答案,但"Aabb"是不正确的。
    注意'A'和'a'被认为是两种不同的字符。

    思路:这个题,简直就是送分。我现在的想法就是把字符串转化成数组,下标代表字符,数值代表出现的次数。因为没有任何时间空间要求,我不知道这个字符串是不是只有大小写字母,先暂定52位数组。去 实现下试试。
    我踏马果然太天真,看题目中没说只包含大小写字母就不应该试这个运气,不过问题不大,仍然很简单,简单的改造一下就行。附上我第一版本的代码截图,毕竟辛苦写的:

    只包含字母大小写答案
    好了,正确答案出来了:
    class Solution {
        public String frequencySort(String s) {
            StringBuffer[] d = new StringBuffer[134];
            for (int i = 0;i<d.length;i++) {
                d[i] = new StringBuffer();
            }
            for(char c : s.toCharArray()) {
                d[c].append(c);
            }
            Arrays.sort(d, new Comparator<StringBuffer>() {
                @Override
                public int compare(StringBuffer o1, StringBuffer o2) {
                    if(o1.length()<o2.length()) return 1;
                    if(o1.length()>o2.length()) return -1;
                    return 0;
                }
            });
            StringBuffer sb = new StringBuffer();
            for(StringBuffer ss:d){
               sb.append(ss);
            }
            return sb.toString();
            
        }
    }
    

    其实就是把字典下标改成了134,因为char最大133.别的没啥改动。我个人感觉优化的空间就是如果是空StringBuffer就不要参与排序浪费性能了,这里加一个集合,所有非空的才放进去会好一点吧,不过因为比较麻烦,所以我就不写了,直接去看性能排行第一的代码了。

    class Solution {
        public String frequencySort(String s) {
            int[] letters = new int[128];
            char[] a = s.toCharArray();
            for (char c : a) {
                letters[c]++;
            }
    
            int curr = 0;
            int len = s.length();
            while(curr < len){
                int max = 0;
                char c = 0;
                // 遍历找出最大的次数
                for(int i = 0 ; i < 128 ; ++i) {
                    if(letters[i] > max) {
                        max = letters[i];
                        c = (char) i;
                    }
                }
                
                while(letters[c] > 0){
                    a[curr] = c;
                    letters[c]--;
                    curr++;
                }
            }
     
            return new String(a);
        }
    }
    

    处于能看懂,灵机一动也可能想到的范围,因为这个题比较简单所以就不多说了,下一题。

    用最少数量的箭引爆气球

    题目:在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以y坐标并不重要,因此只要知道开始和结束的x坐标就足够了。开始坐标总是小于结束坐标。平面内最多存在104个气球。
    一支弓箭可以沿着x轴从不同点完全垂直地射出。在坐标x处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。

    Example:
    输入:
    [[10,16], [2,8], [1,6], [7,12]]
    输出:
    2
    解释:
    对于该样例,我们可以在x = 6(射爆[2,8],[1,6]两个气球)和 x = 11(射爆另外两个气球)。

    思路:这个题真的看了n遍才看懂,然後觉得题意还算是好理解,但是这个叙述真的一眼难尽。读懂题目后有隐隐约约的思路。首先第一步就是排序,从开始最小排序,然後再取最小值范围内end最小的,当下一个开始值大于这个end说明到这里要重新 定点了,暂时这个是很模糊的想法,我去代码试试。
    只能说这个题比我想得要简单的多,说真的我觉得题目描述吓到我了,直接贴代码,一次通过的:

    class Solution {
        public int findMinArrowShots(int[][] points) {
            if(points == null || points.length == 0) return 0;
            Arrays.sort(points, new Comparator<int[]>() {
                @Override
                public int compare(int[] o1, int[] o2) {
                    if(o1[0]>o2[0]) return 1;
                    if(o1[0]<o2[0]) return -1;
                    return 0;               
                }
            });
            int res = 1;
            int start = points[0][0];
            int end = points[0][1];
            for(int[] i:points){
                if(i[0]<=end){//开始不大于最小的结束,有共同区间,不用单独写
                    end = Math.min(end,i[1]);
                }else{//开始大于最小的结束,要从新开始定点
                    start = i[0];
                    end = i[1];
                    res++;
                }
            }
            return res;
        }
    }
    

    其实我之前的思路是对的,就是判断有没有共同区间,然後判断要不要重新定点。这个性能超过百分之八十多,我去看看性能排行第一的代码:

    class Solution {
        public int findMinArrowShots(int[][] points) {
            if (points == null || points.length == 0) return 0;
            //这种写法排序比用lambda快
            Arrays.sort(points, new Comparator<int[]>() {
                @Override
                public int compare(int[] o1, int[] o2) {
                    return o1[1] - o2[1];
                }
            });
            int count = 1, end = points[0][1];
            for (int i = 1; i < points.length; i++) {
                if (points[i][0] <= end) {
                    continue;
                }
                count++;
                end = points[i][1];
            }
            return count;
        }
    }
    

    思路差不多,只不过处理的更加优雅,而且看了人家的代码才发现我这个start确实是没用到。不过我改了下比较大小的步骤提交性能立刻就上来了,附个图下一题。


    性能超过百分之九十七

    四数相加2

    题目:给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0。
    为了使问题简单化,所有的 A, B, C, D 具有相同的长度 N,且 0 ≤ N ≤ 500 。所有整数的范围在 -228 到 228 - 1 之间,最终结果不会超过 231 - 1 。

    例如:
    输入:
    A = [ 1, 2]
    B = [-2,-1]
    C = [-1, 2]
    D = [ 0, 2]
    输出:
    2
    解释:
    两个元组如下:

    1. (0, 0, 0, 1) -> A[0] + B[0] + C[0] + D[1] = 1 + (-2) + (-1) + 2 = 0
    2. (1, 1, 0, 0) -> A[1] + B[1] + C[0] + D[0] = 2 + (-1) + (-1) + 0 = 0

    思路:题目居然还说为了使问题简单化?我忍不住笑出了声。反正继续说这道题吧。我个人的感觉就是笛卡尔积。其实就是a中和b中的每个计算,再和c中的每个计算,再和d中的每个计算。如果结果是0则答案+1.这个题目中说整数是有范围的,所以还可以剪个枝。我不知道会不会超时,但是我觉得单纯的实现绝对是可以的。我直接去写代码了。
    我这个嘴简直是开光了,没啥说的,反正就是超时,附个截图:

    超时截图
    好吧,我看的答案,四层循环拆开两个,然後直接贴代码(我没想到能有重复的数字):
    class Solution {
        public int fourSumCount(int[] A, int[] B, int[] C, int[] D) {
            Map<Integer, Integer> map = new HashMap<>();
            for(int a : A){
                for(int b : B){
                    map.put(a + b , map.getOrDefault(a + b, 0) + 1);
                }
            }
            int sum = 0;
            for(int a : C){
                for(int b : D){
                    if(map.containsKey(0 - a - b)){
                        sum += map.get(0 -a - b);
                    }
                }
            }
            return sum;
        }
    }
    

    怎么说呢,我觉得这个题的考点,就是性能优化?不多解释了,没啥好说的,下一题了。

    132模式

    题目:给定一个整数序列:a1, a2, ..., an,一个132模式的子序列 ai, aj, ak 被定义为:当 i < j < k 时,ai < ak < aj。设计一个算法,当给定有 n 个数字的序列时,验证这个序列中是否含有132模式的子序列。

    注意:n 的值小于15000。
    示例1:
    输入: [1, 2, 3, 4]
    输出: False
    解释: 序列中不存在132模式的子序列。
    示例 2:
    输入: [3, 1, 4, 2]
    输出: True
    解释: 序列中有 1 个132模式的子序列: [1, 4, 2].
    示例 3:
    输入: [-1, 3, 2, 0]
    输出: True
    解释: 序列中有 3 个132模式的的子序列: [-1, 3, 2], [-1, 3, 0] 和 [-1, 2, 0].

    思路:这个题我不知道是不是我想的太简单了,两次遍历,从前往后最小值,从后往前最小值。然後遍历每一个元素,判断前后小于当前值则true,不然继续往下遍历。我去实现了。
    额,实现了是实现了,但是性能一言难尽,没超时我就觉得挺神奇了,先贴代码:

    class Solution {
        public boolean find132pattern(int[] nums) {
            int len = nums.length;
            int[] front = new int[len];
            front[0] = nums[0];
            for(int i = 1;i<len;i++){
                front[i] = Math.min(front[i-1],nums[i]); 
            }
            for(int i = 1;i<len-1;i++){
                //接下来判断是不是1.2模式
                if(nums[i]>front[i-1] && nums[i]>nums[i+1]) {
                    for(int j = i+1;j<len;j++) {
                        if(nums[j]<nums[i] && nums[j]>front[i-1]) return true;
                    }
                }
            }
            return false;
        }
    }
    

    在做的时候其实我大大小小调了很多次,才有现在的代码。之前想的是前后做两个数组都存最小值,但是后来才明白这个132的意思是1要小于2,反正现在是对付实现了虽然性能不行。我 甚至觉得好像不用front这个数组也可以实现了。我再去试试。

    class Solution {
        public boolean find132pattern(int[] nums) {
            int len = nums.length;
            if(len<3) return false;
            int min = nums[0];
            for(int i = 1;i<len-1;i++){
                min = Math.min(min,nums[i-1]);
                //接下来判断是不是1.2模式
                if(nums[i]>min && nums[i]>nums[i+1]) {
                    for(int j = i+1;j<len;j++) {
                        if(nums[j]<nums[i] && nums[j]>min) return true;
                    }
                }
            }
            return false;
        }
    }
    

    又优化了一次,还是不行,我还是直接看性能排行第一的代码吧。
    我直接贴代码:

    public class Solution {
        public boolean find132pattern(int[] nums) {
            if (nums.length < 3)
                return false;
            int[] min = new int[nums.length];
            min[0] = nums[0];
            for (int i = 1; i < nums.length; i++)
                min[i] = Math.min(min[i - 1], nums[i]);
            for (int j = nums.length - 1, k = nums.length; j >= 0; j--) {
                if (nums[j] > min[j]) {
                    while (k < nums.length && nums[k] <= min[j])
                        k++;
                    if (k < nums.length && nums[k] < nums[j])
                        return true;
                    nums[--k] = nums[j];
                }
            }
            return false;
        }
    }
    

    我总觉得和我最开始的思路差不多,但是性能差的就有点多了,反正不多说了,下一题。

    环形数组循环

    题目:给定一个含有正整数和负整数的环形数组 nums。 如果某个索引中的数 k 为正数,则向前移动 k 个索引。相反,如果是负数 (-k),则向后移动 k 个索引。因为数组是环形的,所以可以假设最后一个元素的下一个元素是第一个元素,而第一个元素的前一个元素是最后一个元素。
    确定 nums 中是否存在循环(或周期)。循环必须在相同的索引处开始和结束并且循环长度 > 1。此外,一个循环中的所有运动都必须沿着同一方向进行。换句话说,一个循环中不能同时包括向前的运动和向后的运动。

    示例 1:
    输入:[2,-1,1,2,2]
    输出:true
    解释:存在循环,按索引 0 -> 2 -> 3 -> 0 。循环长度为 3 。
    示例 2:
    输入:[-1,2]
    输出:false
    解释:按索引 1 -> 1 -> 1 ... 的运动无法构成循环,因为循环的长度为 1 。根据定义,循环的长度必须大于 1 。
    示例 3:
    输入:[-2,1,-1,-2,-2]
    输出:false
    解释:按索引 1 -> 2 -> 1 -> ... 的运动无法构成循环,因为按索引 1 -> 2 的运动是向前的运动,而按索引 2 -> 1 的运动是向后的运动。一个循环中的所有运动都必须沿着同一方向进行。

    思路:这个题怎么说呢,首先读了好几遍题目,然後第一反应就是从第一个元素开始往下顺着走,至于判断是不是环是要用标记的办法。如果走到重复的点说明到了循环,不过题目说一个不是环,还有环中方向要相同。所以还是要标记一下每个元素的正负。反正是有思路,我去代码实现下试试。
    本来以为挺简单的,屁颠屁颠写完了才发现题意都没理解。我本来以为一定是从第0个元素开始,然后测试案例给我一个重击:3,1,2成立,就说明是可以从任何元素开始的。因为理解错误所以最开始的思路要从头开始写了,我再理理思路。
    死乞白赖把答案写出来了,性能一言难尽、但是起码ac了:

    class Solution {
        public boolean circularArrayLoop(int[] nums) {
            if(nums==null || nums.length<2) return false;
            for(int i = 0;i<nums.length;i++) {
                Set<Integer> set = new HashSet<Integer>();
                int v = nums[i]>0?1:-1;//1是往前, -1 是往后
                int idx = i;
                while(!set.contains(idx)) {
                    set.add(idx);
                    int n = nums[idx];
                    //nums中只包含正整数和负整数,不存在0
                    if(n>0) {
                        //当前元素是正数,上一个是负数,所以此路不通
                        if(v == -1) break;
                        idx = (idx+n)%nums.length;
                    }else {
                        //当前元素是负数,上一个是整数,所以此路不通
                        if(v == 1) break;
                        idx = idx+n>=0?idx+n:(idx+n)%nums.length+nums.length; 
                    }
                    if(set.size()>1 && idx == i) return true;
                }
            }       
            return false;
        }
    }
    

    然后性能不好我也能理解,毕竟我一开始的思路只是从头开始,这个题其实我觉得我的代码中还有可进步的,但是我有点懒得想了,直接看性能排行第一的代码吧:

    class Solution {
        int max, len;
        public boolean circularArrayLoop(int[] nums) {        
            this.len = nums.length;
            this.max = 1000 * len;
            for (int i = 0; i < len; i++) {
                int slow = i, fast = i;
                int slowLast, fastLast;
                while (true) {
                    slowLast = slow;
                    slow = getNextIndex(nums, slow);
                    if (nums[slow] == 0 || nums[slow] * nums[slowLast] < 0 || slow == slowLast) {
                        setZero(nums, i);
                        break;
                    }
                    fastLast = fast;
                    fast = getNextIndex(nums, fast);
                    if (nums[fast] == 0 || nums[fast] * nums[fastLast] < 0 || fast == fastLast) {
                        setZero(nums, i);
                        break;
                    }
                    fastLast = fast;
                    fast = getNextIndex(nums, fast);
                    if (nums[fast] == 0 || nums[fast] * nums[fastLast] < 0 || fast == fastLast) {
                        setZero(nums, i);
                        break;
                    }
                    if (slow == fast) {
                        return true;
                    }
                }            
            }
            return false;
        }
    
        public void setZero(int[] nums, int i) {
            int j;
            while (true) {
                j = getNextIndex(nums, i);
                if (nums[j] == 0 || nums[i] * nums[j] < 0) {
                    nums[i] = 0;
                    break;
                }
                nums[i] = 0;
                i = j;
            }
        }
    
        public int getNextIndex(int[] nums, int fast) {
            return (nums[fast] + fast + max) % len;
        }
    }
    

    啧啧,人家这个代码量,我简单看了下,用的快慢指针,之前做链表的时候经常用,能理解,但是我自己反正是没想到。而且这个还涉及到了正负数不能一起算,反正是挺复杂的,有兴趣的自己仔细看看吧。
    这篇笔记就到这里了,如果稍微帮到你了记得点个喜欢点个关注,也祝大家工作顺顺利利!说个题外话,一直以来一起刷题的微信群里,一个小姐姐今天收到了微软的offer,她在群里说了一句,付出总会有回报的,我觉得挺好的。也希望大家都能这样!java技术交流群130031711,欢迎各位踊跃加入!

    相关文章

      网友评论

        本文标题:leetCode进阶算法题+解析(五十一)

        本文链接:https://www.haomeiwen.com/subject/wtvzsktx.html