0
点赞
收藏
分享

微信扫一扫

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

前端王祖蓝 2021-09-30 阅读 107

现在平均一周一篇笔记,我是真的着急又无奈。日常crud,扣需求改需求吵架,加班加点联调,以前刚刚来这个公司的时候看公司项目,一个表中两三个字段字段名字不同,但是存的是一样的东西,十分不理解甚至还觉得是谁写的代码这么low,各种鄙视。到现在一天两版需求,刚做完的功能就要大改,甚至一个需求昨天要求,今天加上了,明天又说不要了。。。我只能觉得是在修身养性。
哪怕偶尔闲着了也想着放过自己一下吧,看个搞笑视频,听听爱听的歌,反正刷题是被落下了,尤其是现在有时候一道题卡住,要大量的时间去思考,思考思考着就忘了,所以现在都是一周一遍笔记,自己也不满意这个学习速度,但是又挺无奈的,走一步算一步吧。
吐槽完毕,开始刷题!

最少一动次数使数组元素相等2

题目:给定一个非空整数数组,找到使所有数组元素相等所需的最小移动数,其中每次移动可将选定的一个元素加1或减1。 您可以假设数组的长度最多为10000。

思路:这个题我觉得应该很简单啊,不就是大了往小了减,小了往大了加,直到所有的数字都一样嘛。至于最终结果会是多少,我隐隐的有想法。肯定是要有一个标准的数值,然后每个元素往这个元素上变。但是这个标准元素是多少呢?大了小了都不合适,我个人觉得应该是中间那个数,如果是偶数个,中间两位都是可以的。我去试试代码。
我本来只想测试一下这个想法是不是正确的,结果ac了,性能还挺好,哈哈,直接贴代码:

class Solution {
    public int minMoves2(int[] nums) {
        if(nums.length<2) return 0;
        Arrays.sort(nums);
        //奇数取中间的,偶数随便取中间的两个中的一个
        int num = nums[nums.length/2];
        int res = 0;
        for(int i : nums) {
            res += Math.abs(num-i);
        }
        return res;

    }
}

虽然一次ac,但是我觉得排序这块我可以试试手写排序,能让性能更好一点。正好好久没写过了,我去试试用排序能不能性能更好一点。
关于这个问题,现实给了我一个很大的耳光,用uitl里的sort3ms通过,我手写快排后5ms通过。。。我也不知道我在折腾什么。但是写都写了,先附上快排的代码:

class Solution {
    public int minMoves2(int[] nums) {
        if(nums.length<2) return 0;
        quickSort(nums,0,nums.length-1);
        //奇数取中间的,偶数随便取中间的两个中的一个
        int num = nums[nums.length/2];
        int res = 0;
        for(int i : nums) {
            res += Math.abs(num-i);
        }
        return res;

    }

    public void quickSort(int[] nums,int left,int right) {
        if(left>=right) return; 
        int mid = (right-left)/2+left;
        quickSort(nums, left, mid);
        quickSort(nums, mid+1, right);
        int[] temp = new int[right-left+1];
        int l = left;
        int r = mid+1;
        int i = 0;
        while(r<=right && l<=mid ) {
            if(nums[l]>nums[r]) {
                temp[i++] = nums[r++];
            }else {
                temp[i++] = nums[l++]; 
            }
        }
        while(i<right-left+1) {
            if(r != right+1) {
                temp[i++] = nums[r++];
            }else {
                temp[i++] = nums[l++];
            }
        }
        for(int j = left;j<=right;j++) {
            nums[j] = temp[j-left];
        }        
    }
}

然后说说为什么用Arrays.sort会性能比快排好,刚刚去看了sort的源码:



因为比较多我就不复制粘贴了,只能说人家的排序是根据数组的长度动态选择排序方法的,什么快排插排等,选择的都是最优的,所以性能比我单纯写个快排好已经能理解了,说实话以前都没注意过简单的一个sort方法居然这么复杂,至于这个题,我已经想不出优化方法了,直接去看性能排行第一的代码:

class Solution {
    public int minMoves2(int[] nums) {
        if (nums == null || nums.length <= 1) {
            return 0;
        }
        Arrays.sort(nums);
        int i = 0;
        int j = nums.length-1;
        int ans = 0;
        while (i < j) {
            ans += (nums[j] - nums[i]);
            i++;
            j--;
        }
        return ans;

    }
}

竟然优化点在算res的遍历中,确实是没想到。这道题其实比较简单,只要明白原理就可以做出来了,下一题。

我能赢么?

题目:在 "100 game" 这个游戏中,两名玩家轮流选择从 1 到 10 的任意整数,累计整数和,先使得累计整数和达到或超过 100 的玩家,即为胜者。如果我们将游戏规则改为 “玩家不能重复使用整数” 呢?例如,两个玩家可以轮流从公共整数池中抽取从 1 到 15 的整数(不放回),直到累计整数和 >= 100。给定一个整数 maxChoosableInteger (整数池中可选择的最大数)和另一个整数 desiredTotal(累计和),判断先出手的玩家是否能稳赢(假设两位玩家游戏时都表现最佳)?你可以假设 maxChoosableInteger 不会大于 20, desiredTotal 不会大于 300。

思路:这种题我做过,我记得上一道题是那种非算法,而是规律的题,就是每次数1-3个数,说到某个数肯定赢的那种。这个题我觉得也可能是这种题。我去慢慢分析分析。
我是真没想到这道题能卡住两天,在我坚持不看题解的原则下,最好的结果也不过是超时,所以。。。我屈服了。
看了题解才发现这个题远没有我想的那么简单,之前我已经想过用回溯了,代码写了一大堆然后超时了。看了题解发现这题有两点:

  1. 用dp,这个是我的问题,因为最近没怎么做dp的题,所以压根没往那边想。
  2. 位操作。这个是从未接触过的一个骚操作。毕竟用位来表示某个数字是否被用过真的贼方便而且新奇。

甚至这个单纯的位操作我都看了很久才看明白。大概的原理就是一个数组最开始是0.因为这个题说了最多20个数,所以一个32位的数字完全可以满足这个操作。
接下来骚操作来了:


继续说这个题,只要知道了dp并且知道了用二进制表示用过的数字,整个题虽然理解起来还是不直观,但是起码能看懂了。首先两点:如果给定值小于要求和,那么第一个人可以直接赢了,还有就是所有和加起来没给定值大,那么直接false。
剩下的就要一点点判断。第一个人去拿每个数然后往下递归走,如果某个数能直接赢则是true。还有如果拿完某个数对方都不能赢那么也可以true。不然就六false。我直接去贴代码:

class Solution {
    public boolean canIWin(int maxChoosableInteger, int desiredTotal) {
        //第一个说就超过了
        if(desiredTotal<=maxChoosableInteger) return true;
        //所有数加起来都不够,则赢不了
        if((maxChoosableInteger+1)*maxChoosableInteger/2<desiredTotal) return false;
        return dfs(maxChoosableInteger,desiredTotal,new Boolean[1<<maxChoosableInteger],0);
    }
    public boolean dfs(int maxChoosableInteger,int desiredTotal,Boolean [] b,int d){
        //说明到这步选择的情景已经发生了,所以结果已经知道了,不用重复算了
        if(b[d] != null) return b[d];
        for(int i = 1;i<=maxChoosableInteger;i++){
            int tmp = (1 << (i - 1));
            //这个数没用过
            if((tmp & d)==0){
                //当前数字大于给定值,说明直接赢了。或者说下一个人所有选择都必输,那么也是当前这个人赢
                if(i>=desiredTotal || !dfs(maxChoosableInteger,desiredTotal-i,b,tmp|d)){
                    b[d] = true;
                    return true;
                }
            }
        }
        //所有数字都走了一遍,也不能稳赢所以false
        b[d]=false;
        return false;
    }
}

说真的,我觉得这个代码格外的少,但是每一步都好难。反正我个人觉得是挺难的。毕竟各种因为细节卡住。我觉得我注解写的很详细了,事先也说了好多思路,所以这个题就这样了,下一题。

环绕字符串中唯一的子字符串

题目:把字符串 s 看作是“abcdefghijklmnopqrstuvwxyz”的无限环绕字符串,所以 s 看起来是这样的:"...zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd....".
现在我们有了另一个字符串 p 。你需要的是找出 s 中有多少个唯一的 p 的非空子串,尤其是当你的输入是字符串 p ,你需要输出字符串 s 中 p 的不同的非空子串的数目。 注意: p 仅由小写的英文字母组成,p 的大小可能超过 10000。

思路:这个题我读了好几遍题才算是读懂题意,原来这个s是已经固定了的,就是a-z的无限循环,给定的字符串是p。s中a和c不可能挨着,所以第二个demo中子串只能是c,a。而z和a,a和b,zab都会挨着,所以最后一个是那样的。题意读懂了剩下的就是这个题的解答了,首先我的想法是遍历p。从第一个开始能一直挨着的知道不能挨着。然後这个连续长度是有算法的,1是1,2是3,3是6,4是10.这个好像是叫做乘阶吧,反正这个是能算出来的。然後继续遍历。而且其实这个字串是有上限的,比如a出现一次,再出现也不能算了。我先去实现试试吧,感觉这个题不是很难。
好了,这个题是用一种取巧的方法做出来的,每个单词作为结尾的字串。其实长度越长,说明这个子串越多,比如如果abcde存在,那么bcde,cde,de,e也肯定存在。同理 abcd,bcd,cd,d也存在,abc,bc,c也存在,bc,c也存在。看懂了么?只要判断这个词作为作为结尾有多少种可能,所有的字母加起来,就能知道全部的了,接下来贴代码:

class Solution {
    public int findSubstringInWraproundString(String p) {
        char[] c = p.toCharArray();
        int n = 0;
        int res = 0;
        int[] arr = new int[26];
        for(int i = 0;i<c.length;i++){
            if(i==0 || c[i-1]==c[i]-1 || (c[i]=='a'&& c[i-1]=='z')){
                n++;
            }else{
                n = 1;
            }
            int temp = arr[c[i]-'a'];
            if(temp<n){
                res += n-temp;
                arr[c[i]-'a'] = n;
            }
        }
        return res;
    }
}

下面附上一个性能排行第一的代码:

class Solution {
        public int findSubstringInWraproundString(String p) {
        int len = p.length();
        if (len == 0) return 0;
        int[] map = new int[26];
        int dp = 0;
        int sum = 0;
        char[] arr = p.toCharArray();
        for (int i=0; i<len; i++) {
            char c = arr[i];
            if (i == 0 || (c-arr[i-1] -1)%26 == 0) {
                dp++;
            } else dp = 1;
            int cnt = map[c-'a'];
            if (dp > cnt) {
                sum += dp - cnt;
                map[c-'a'] = dp;
            }
        }
        return sum;
    }
}

这个代码其实很简单,也没啥复杂的操作,只要思路能想到就很容易。这个题就这样了。

验证ip地址

题目:编写一个函数来验证输入的字符串是否是有效的 IPv4 或 IPv6 地址。
如果是有效的 IPv4 地址,返回 "IPv4" ;
如果是有效的 IPv6 地址,返回 "IPv6" ;
如果不是上述类型的 IP 地址,返回 "Neither" 。
IPv4 地址由十进制数和点来表示,每个地址包含 4 个十进制数,其范围为 0 - 255, 用(".")分割。比如,172.16.254.1;同时,IPv4 地址内的数不会以 0 开头。比如,地址 172.16.254.01 是不合法的。
IPv6 地址由 8 组 16 进制的数字来表示,每组表示 16 比特。这些组数字通过 (":")分割。比如, 2001:0db8:85a3:0000:0000:8a2e:0370:7334 是一个有效的地址。而且,我们可以加入一些以 0 开头的数字,字母可以使用大写,也可以是小写。所以, 2001:db8:85a3:0:0:8A2E:0370:7334 也是一个有效的 IPv6 address地址 (即,忽略 0 开头,忽略大小写)。
然而,我们不能因为某个组的值为 0,而使用一个空的组,以至于出现 (::) 的情况。 比如, 2001:0db8:85a3::8A2E:0370:7334 是无效的 IPv6 地址。同时,在 IPv6 地址中,多余的 0 也是不被允许的。比如, 02001:0db8:85a3:0000:0000:8a2e:0370:7334 是无效的。

思路:我总觉得这个题是挺简单的,甚至超出想象的简单。首先判断里面有没有. 如果有的则判断是不是ip4,没有判断有没有:。如果有判断是不是ip6.如果两个都没有直接返回Neither。暂时不太知道这道题的坑在哪里,我先去实现下看看。
做出来了,性能也还行,但是做的我一脸懵B,我都不知道我经历了什么。反正面向测试案例编程,一遍一遍完善判断。我先直接贴代码:

class Solution {
    public String validIPAddress(String IP) {
        //两种符号都包含或者两种符号都不包含直接报错
        if ((IP.contains(".") && IP.contains(":"))||(!IP.contains(".")&& !IP.contains(":")) || IP.startsWith(":") || IP.startsWith(".") || IP.endsWith(":") || IP.endsWith(".")) return "Neither";
        //说明可能是ip4或者都不是
        if(IP.contains(".")) {
            String arr[] = IP.split("\\.");
            if(arr.length!=4) return "Neither";
            for(String s:arr) {
                if("".equals(s) ||s.length()>3 ||(s.length()>1 && s.startsWith("0"))) return "Neither";
                int n = 0;
                for(char c:s.toCharArray()) {
                    if(!Character.isDigit(c)) return "Neither";
                    n = n*10+(c-'0');
                }
                if(n>255) return "Neither";
            }
            return "IPv4";           
        }else {//可能是ip6或者都不是
            String arr[] = IP.split(":");
            if(arr.length!=8) return "Neither";
            for(String s:arr) {
                if("".equals(s) || s.length()>4) return "Neither";
                for(char c:s.toCharArray()) {
                    if(!check(c)) return "Neither";
                }
            }
            return "IPv6";
        }
    }
    public boolean check(char c){
        //c 是 0,1,2,3,4,5,6,7,8,9
        if(c>47 && c<58) return true;
        //c 是A,B,C,D,E,F
        if(c>64 && c<71) return true;
        //c 是 a,b,c,d,e
        if(c>96 && c<103) return true;
        return false;
    }
}

说实话我自己都承认我这个题做的没有一点水平。反正就是各种字符串判断。写的也很墨迹,但是性能出奇的好。我贴一下这个题提交的心路历程:



我去瞅瞅性能排行第一的代码:

class Solution {
    public String validIPAddress(String IP) {
        char[] arr = IP.toCharArray();
        int len = arr.length;
        if(len < 7 || arr[0] == '.' || arr[0] == ':'){
            return "Neither";
        }
        int j = 0;
        while(j < len && arr[j] != '.' && arr[j] != ':'){
            j++;
        }
        if(j == len){
            return "Neither";
        }
        if(arr[j] == ':'){
            // 判断是否为IPV6,否则neither
            int count = 0;
            int temp = 0;
            for(int i = 0; i < len; i++){
                if(arr[i] == ':'){
                    if(temp >= 1 && temp <= 4){
                        count++;
                        temp = 0;
                    }else{
                        return "Neither";
                    }
                }else if((arr[i] >= '0' && arr[i] <= '9') || (arr[i] >= 'a' && arr[i] <= 'f')
                || (arr[i] >= 'A' && arr[i] <= 'F')){
                    temp++;
                }else{
                    return "Neither";
                }
            }
            if(count == 7 && temp >= 1 && temp <= 4){
                return "IPv6";
            }else{
                return "Neither";
            }
        }else{
            // 判断是否为IPV4,否则neither
            int i = 0;
            int num = 0;
            int count = 0;
            int temp = 0;
            while(true){
                if(i == len || (arr[i] == '0' && arr[i + 1] != '.' )|| arr[i] == '.'){
                    return "Neither";
                }
                while(i < len && arr[i] != '.'){
                    if(!Character.isDigit(arr[i])){
                        return "Neither";
                    }
                    num = num * 10 + (arr[i] - '0');
                    i++;
                    temp++;
                    if(temp > 3){
                        return "Neither";
                    }
                }
                if(i == len){
                    if(num <= 255){
                        count++;
                        break;
                    }else{
                        return "Neither";
                    }
                }
                if(num <= 255){
                    count++;
                    i++;
                    num = 0;
                    temp = 0;
                }else{
                    return "Neither";
                }
            }
            if(count == 4){
                return "IPv4";
            }else{
                return "Neither";
            }
        }
    }
}

写的比我还墨迹,但是也就是生判断,这个题简单的难度我觉得都不够,啧啧,下一题。

用Rand7()实现Rand10()

题目:已有方法 rand7 可生成 1 到 7 范围内的均匀随机整数,试写一个方法 rand10 生成 1 到 10 范围内的均匀随机整数。不要使用系统的 Math.random() 方法。

思路:确确实实心中自己一分钟,第一遍审题没看明白,只以为是不用系统的Rand方法实现Rand10().后来才看明白这个rand7()是可以用的。但是整体来说暂时还是没啥思路。我去尝试着提交代码看看,刚刚又审了下题目,我觉得有点点思路了。建立个三维数组,第一维7个。选择rand7()的时候每个被选中的几率是一样的。第二维5个数字。其实rand7()中1-5被选中的几率也是一样的。第三维两个数字。rand7()中1,2被选中的几率也是一样的。之所以这么麻烦是因为确保这个三维数组每一个数字被选中几率一样。然後7乘5乘2一共七十个元素,1-10一个数字出现七次。这样每个数字被选中几率就是一样的了。我感觉这个思路可以,我去代码试试。
确实这个思路可以,该说不说反正是实现了,至于性能也就这么可怜了,我先贴代码:

/**
 * The rand7() API is already defined in the parent class SolBase.
 * public int rand7();
 * @return a random integer in the range 1 to 7
 */
class Solution extends SolBase {
    int[][][] d = new int[7][5][2];
    public Solution(){
        int n = 1;
        int num = 0;
        for(int i = 0;i<7;i++ ) {
            for(int j = 0;j<5;j++) {
                for(int k = 0;k<2;k++) {
                    d[i][j][k] = n;
                    num++;
                    if(num == 7) {
                        n++;
                        num = 0;
                    }
                }
            }
        }
    }
    public int rand10() {
        int i = rand7();
        int j = 10;
        while(j>5){
            j = rand7();
        }
        int k = 10;
        while(k>2){
            k = rand7();
        }
        return d[i-1][j-1][k-1];
    }
}

起码说明这个几率确实是可以了,剩下的优化我也不想了,直接去看题解吧。
额,我只能说我还年轻。这个题我写的也不算很复杂,但是简单方法多的很。我觉得很好的一个思路就是第一个rand7根据奇数偶数判断是1-5还是6-10.
然後第二个rand判断具体是哪个数。这样整体的几率是一样的,而且方便的多,我去实现下。

/**
 * The rand7() API is already defined in the parent class SolBase.
 * public int rand7();
 * @return a random integer in the range 1 to 7
 */
class Solution extends SolBase {
    public int rand10() {
        int i = rand7();
        while(i==7){
            i = rand7();
        }
        int b = rand7();
        while(b>5){
            b = rand7();
        }
        return (i&1)==1?b:b+5;
    }
}

怎么说呢,代码是精简了,但是性能还是不行。我直接去看性能排行第一的代码吧。
我刚刚看了两个性能好的代码,都是伪随机,甚至还有直接用random的。。一言难尽,既然这样我也不把代码贴出来了,直接下一题。

火柴拼正方形

题目:还记得童话《卖火柴的小女孩》吗?现在,你知道小女孩有多少根火柴,请找出一种能使用所有火柴拼成一个正方形的方法。不能折断火柴,可以把火柴连接起来,并且每根火柴都要用到。输入为小女孩拥有火柴的数目,每根火柴用其长度表示。输出即为是否能用所有的火柴拼成正方形。

思路:这个题怎么说呢,火柴数组的长度不超过15?我莫名觉得这个题有点简单啊。大概思路是有了。所有的相加/4.除不开直接false。除开了的话从第一个元素开始判断能不能凑出边长。当然如果有某一根直接长于边长也false。反正我去代码实现下吧。
第一版本果断超时,174个测试案例第170超时了,我个人感觉现在思路没问题,我去试试剪枝什么的能不能好一点。
简单的加了个判断就过了,虽然性能还是很可怜,我先贴代码:

class Solution {
    boolean[] flag;
    int len;
    public boolean makesquare(int[] nums) {
        //小于四个元素直接false
        if(nums == null || nums.length<4) return false;
        int sum = 0;
        for(int i : nums){
            sum += i;
        }
        //不能被4整除直接false
        if(sum%4 != 0) return false;
        Arrays.sort(nums);
        //最长火柴大于边长,一定false
        if(nums[nums.length-1]>sum/4) return false;
        //判断能不能组成四个边
        flag = new boolean[nums.length];
        len = sum/4;
        return dfs(nums,1,0);

    }
    //n是第几条,当凑出四条边则true,temp是当前的和
    public boolean dfs(int[] nums,int n,int temp){
        if (n == 4) return true;
        //当前这条边凑完了,继续往下凑
        if (temp == len) return dfs(nums,n+1,0);
        //走到这说明当前的没凑完,继续凑吧
        for(int i = nums.length-1;i>=0;i--){
            //这个数字没用过。而且可以尝试凑一凑
            if(!flag[i] && temp+nums[i]<=len){
                flag[i] = true;
                //如果这个dfs能是true,说明凑成了一条边,则继续往下凑就行了
                if(dfs(nums,n,temp+nums[i])){
                    return true;
                }
                //走到这说明这个元素不能用,回复false
                flag[i] = false;
                //相同数值就不用重复计算了。这个一定卸载if里面
                while(i>0 && nums[i] == nums[i-1]) i--;
            }

        }
        return false;
    }
}

这个if里面有个while,就是相同的值不重复计算这个,一开始没这句话就超时,现在加上这句话就不超时了。我觉得其实还可以剪枝,我去试试。
这个凭我自己优化有点费劲了,我直接看了题解。上一种回溯+剪枝1ms的代码:

class Solution {
    boolean[] flag;
    public boolean makesquare(int[] nums) {
        // 计算每条边的边长
        int sum = 0;
        for(int num : nums){
            sum = sum + num;
        }

        if(sum == 0 || sum % 4 != 0){
            return  false;
        }
        int len = sum / 4;
        flag = new boolean[nums.length];
        // 降序排列,大的先上,这样剪枝可以剪多点
        // 升序降序都是1ms
        Arrays.sort(nums);
        int L = 0, R = nums.length - 1;
        while(L <= R){
            int t = nums[L];
            nums[L] = nums[R];
            nums[R] = t;
            L++;
            R--;
        }
        return dfs(0, len, 0, 0, nums);
    }

    // edge 表示当前是第几条边,总共四条边
    // len 表示每条边应该的长度
    // u 表示当前边已经到多少长度了
    // start 认为规定一个遍历的顺序,防止重复
    // nums[] 木棍的数组
    boolean dfs(int edge, int len, int u, int start, int[] nums){
        if(edge == 4){
            return true;
        }
        // u 到达len,就可以换条边摆了
        if(u == len){
            return dfs(edge + 1, len, 0, 0, nums);
        }

        for(int i = start; i < nums.length; i++){
            if(!flag[i] && u + nums[i] <= len){
                flag[i] = true;
                if(dfs(edge, len, u + nums[i], i + 1, nums)){
                    return true;
                }
                flag[i] = false;

                // 能够走到这一步,说明这根火柴不行,否则已经return了
                // 相等的火柴也不行
                while(i + 1 < nums.length && nums[i + 1] == nums[i]){
                    i++;
                }

                // 如果这条火柴是边的第一条,那说明在这条边不行,直接false
                if(u == 0){
                    return false;
                }
            }
        }
        return false;
    }    
}

其实主要最重要的就是这句,如果u==0.还没true,直接false。这个思路是很叼的,因为这样就说明某个元素是无论如何也凑不出来边长的,就没必要浪费时间了,直接false。
其实这个题我一直觉得可以用dp实现,不过因为回溯已经实现了我就不想再动脑子了,我去看下性能第一的代码:
好的吧,性能第一的代码就是上面这个回溯的,我也不纠结dp了,这道题就这样吧。
这篇笔记就记到这里,如果稍微帮到你了记得点个喜欢点个关注,也祝大家工作顺顺利利,生活健健康康!java技术交流群130031711,欢迎各位踊跃加入!

举报

相关推荐

0 条评论