剑走偏锋-leetcode中的奇技淫巧

来源:互联网 发布:为什么有网络协议 编辑:程序博客网 时间:2024/06/11 19:31

断断续续做leetcode,已然数月有余。
做得多了,一种切身感受油然而生:每个人思考问题的方式和角度都是有道可循的,也就是说,一道题上次你用这样的方式解决,这次让你做,即使你已经忘记了上次做过这道题,最终形成的做法和上次大同小异。暂且称之为“思维固定模式论”。
映射到生活中也是如此,这次遇到同样的问题,你使用解决办法了A,并且成功解决了办法,那么下次遇到同样的问题你多半还会使用A办法。这是好事也是坏事,好事是因为A其实就是我们生活的经验,这些经验能够指导我们更好更便捷的处理事情;坏事是因为,这些经验容易让人不假思索,而错过了一些更好更高效的解释办法。
比如:leetcode的discuss区,当我看着着别人的高票解决问题之道,好似看着一道智慧之光,又能隐隐约约听到解题人狡黠的笑声,并沉头低语道:没想到吧!

1.0[位运算] trick one -制作某num的全“1”mask(例如:5-101-111)

学会使用二进制的思维来解决问题
学会使用Integer.highestOneBit(num)方法
见识到简洁代码的威力了吧

例题:476. Number Complement
Given a positive integer, output its complement number. The complement strategy is to flip the bits of its binary representation.
大意为求一个数的二进制取反的int值,例如(5-101-010-2 即返回2)

my solution:
step one:构造表示二进制的字符串
step two:根据二进制字符串一次取反并求值

源码:

 public int findComplement(int num) {        String binStr=Integer.toBinaryString(num);        char[] binaStr=binStr.toCharArray();        String compNum="";        for(int i=0;i<binStr.length();i++)        {            String temp=String.valueOf(binaStr[i]);            if(temp.equals("1"))            {                compNum+=0;            }            else {                compNum+=1;            }        }        return getIntFromBinString(compNum);    }    public static int getIntFromBinString(String str)    {        char[] temp=str.toCharArray();        int res=0;        int j=0;        for(int i=temp.length-1;i>0;i--)        {            String st=String.valueOf(temp[i]);            if(st.equals("1"))            {                res+=Math.pow(2, j);            }            j++;        }        return res;    }

better solution:
step one:构造mask,找到highestOneBit
step two:对num取反
step three:上述两个数AND
源码:

public int findComplement(int num) {        return ~num & ((Integer.highestOneBit(num) << 1) - 1);    }

Integer.highestOneBit(num)的详细介绍
简言之就是取到这个数二进制的最高位

1.1[位运算] trick one -为重复而生的异或^运算-无视顺序-高效解决
所有偶数个的重复问题都可以通过异或运算求解
百度面试题

例题:136. Single Number
Given an array of integers, every element appears twice except for one. Find that single one.
很简单的一道题,直接的解题步骤:
两重循环全遍历,时间复杂度:O(n)=n^2

better solution:
对所有数值进行异或运算
源码:

public class Solution {    public int singleNumber(int[] nums) {        int result = 0;        for (int i = 0; i<nums.length; i++)result ^=nums[i];        return result;        }    }

原理就不在多说了
first , we have to know the bitwise XOR in java

0 ^ N = N
N ^ N = 0

无关乎顺序,就酱

题外衍生一下,之前遇到的百度面试题也是类似的解法,题目如下
箱子里有100个黑球和100个白球,每次取出(不放回)两个球,如果取出的两个球是同色则放回一个黑球,如果取出的两个球是不同颜色则放回一个白球,问最后剩一个黑球的概率是多少?
类似的,所谓同色放黑,异色放白,也就是一种异或运算。此处取黑色为“0”,白色为“1”
黑^黑=白^白=1^1=0^0
白^黑=黑^白=1^0
故一百个白球和一百个黑球异或也就是:
1^1^1^1^1^1^1^1^1^1^1^……^0^0^0^0^0^0^0^0^0^0^=0^0=0
所以最后剩一个黑球的概率必然是100%

1.2[位运算] trick one -异或^移位<<与&运算实现加法
从就计算机的角度看基本操作-加减乘除都是基本的寄存器中数据到运算器中运算的复杂化,同样的其他的负责操作又可以由加减乘除复杂化得来。把握底层的实现细节是件好事

例题:371. Sum of Two Integers
Calculate the sum of two integers a and b, but you are not allowed to use the operator + and -.
不用+或-实现加减法

better solution:
step one :^实现a和b的各位相加,&和<<运算实现进位
step two:进位后如果进位数不为0,则循环step one

源码:

public class Solution {    public int getSum(int a, int b) {    return b==0? a:getSum(a^b, (a&b)<<1); //be careful about the terminating condition;    }}

以上题为例,
a^b实现各位相加(比如:5+6 a^b=1001^1010=0011)
(a&b)<<1实现进位(比如 :5+6 a&b<<1=10000)
由于b不等于零,然后将b&到前面的a^b中即得到了:10011=11;

1.3 [位运算]trick one -异实现字符的查差
做题时,要有一种将字母和数字,将数字和二进制位统一起来的意识
举个栗子:看到”a’的时候要能够看到它其实是一个八位的ascall码

也就是97,也就是10010111

例题:389. Find the Difference
Given two strings s and t which consist of only lowercase letters.

String t is generated by random shuffling string s and then add one more letter at a random position.

Find the letter that was added in t.
简单来说就是,两个字符串,其中有一个字符串多了一个字符,找到它返回出来。

my solution

public class Solution {    public char findTheDifference(String s, String t) {        char temp=0;        for(int i=0;i<s.length();i++)temp^=s.charAt(i);        for(int i=0;i<t.length();i++)temp^=t.charAt(i);        return temp;    }}

不多解释了,理解了^就ok

1.4[位运算]- trick one-移位运算实现进制转换

题目:一个十进制int转换为十六进制

better solution:

public class Solution {    char[] map = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};    public String toHex(int num) {        if(num == 0) return "0";        String result = "";        while(num != 0){            result = map[(num & 15)] + result;             num = (num >>> 4);        }        return result;    }}

做一个map,用&运算来判定index,>>>移位每次移动4位,也就是一个十六进制位,同理八进制也可以用移动三位来实现,二进制也可以用移动一位来实现,已经用代码验证

1.5[位运算]- trick one-模4判断问题
很多和求模进制转换的问题最终都可以化解到位运算上高效的解决

问题:342. Power of Four
Given an integer (signed 32 bits), write a function to check whether it is a power of 4.

源码:

class Solution {    public boolean isPowerOfFour(int num) {       return num > 0 && (num&(num-1)) == 0 && (num & 0x55555555) != 0;    }}

解释:Good solution without good explanation,it’s easy to find that power of 4 numbers have those 3 common features.First,greater than 0.Second,only have one ‘1’ bit in their binary notation,so we use x&(x-1) to delete the lowest ‘1’,and if then it becomes 0,it prove that there is only one ‘1’ bit.Third,the only ‘1’ bit should be locate at the odd location,for example,16.It’s binary is 00010000.So we can use ‘0x55555555’ to check if the ‘1’ bit is in the right place.With this thought we can code it out easily!
简言之即:4的幂次方的数,必然满足这样三个条件:1.大于0;2.最低位为零(二进制);3.偶数个位置上至少有一个1

2.0[字符串]- trick two-善用StringBuild(包括reverse和apped方法,高效且方便)
不要着眼于局部,应该从整体来把握(这题不该一段一段来处理,同时还要注意边界值,而应该整体的来考虑所有分段字符串)
当时真是太弱鸡,split和reverse方法都不清楚,也难怪源码这么丑;
代码这么长的原因主要是没有调用一些api,导致例如slplit和trim等方法都是手撕完成

例题:557. Reverse Words in a String III
给定一个字符串,将每一个以空格隔开的字符依次进行翻转(例如:Input: “Let’s take LeetCode contest” Output: “s’teL ekat edoCteeL tsetnoc”)
my solution:

public String reverseWords(String s) {            char[] temp=s.toCharArray();            int flag=0;            String res="";            int count=0;            for(int i=0;i<s.length();i++)            {                String _tpChar=String.valueOf(temp[i]);                if(_tpChar.equals(" ")||i==s.length()-1)                {                    if(_tpChar.equals(" "))                        count=1;                    char[] _temp=new char[i-flag];                    int j=i-1;                    if(i==s.length()-1){                        j=i;                        if(count==0)                        {                            _temp=new char[i+1];                        }                    }                    i++;                    for(int k=0;k<_temp.length;k++)                    {                        _temp[k]=temp[j];                        j--;                    }                    flag+=_temp.length;                    res+=String.valueOf(_temp);                    if(_temp.length==i-1&&count==1)                        res+=" ";                    if(i==s.length()-1&&count==1)                        res+=temp[i];                }            }            //String resu=res.substring(0, res.length()-1);            return res;        }

better solution:
step one:get all substring
step two:reverser all substring
step three:connect all substring into a whole part
step four:remove the space at the end

public String reverseWords(String s) {        String[] str = s.split(" ");        for (int i = 0; i < str.length; i++) str[i] = new StringBuilder(str[i]).reverse().toString();        StringBuilder result = new StringBuilder();        for (String st : str) result.append(st + " ");        return result.toString().trim();    } 

3.0[字符串]-trick three-java 8 新特性 steam -为紧凑并行的集合操作而生
stream介绍- 最经典的文章
当你好奇心驱使你去发掘一些新东西时,往往你会得到的更多
对新技术新事物保持热情和开放

例题:500. Keyboard Row
input一个字符串数组,返回其中那些由同一行键盘字符组成的字符串(Input: [“Hello”, “Alaska”, “Dad”, “Peace”] Output: [“Alaska”, “Dad”])
这里写图片描述

better solution:
step one:generate String[] into a stream
step two:filter all the String which match with exp[qwertyuiop/asdfghjkl/zxcvbnm]
step three:change stream into array
源码:

public String[] findWords(String[] words) {         return Stream.of(words).filter(s -> s.toLowerCase().matches("[qwertyuiop]*|[asdfghjkl]*|[zxcvbnm]*")).toArray(String[]::new);    }

题解之简洁优美超乎你我的想象,那就顺便点一下stream:
1.Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)
2.Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator
3.流的操作类型分为两种:Intermediate:一个流可以后面跟随零个或多个 intermediate 操作。包括:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered
Terminal:一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。包括:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator

具体使用参照上面链接。

3.1[字符串]-trick three-[字符串]-java 8 新特性 stream -为紧凑并行的集合操作而生-allMatch() anyMatch() noMatch()
之前已经讨论过了,对于一次使用快速遍历的集合操作,stream是一个最佳的选择

例题:
520. Detect Capital
Given a word, you need to judge whether the usage of capitals in it is right or not.

We define the usage of capitals in a word to be right when one of the following cases holds:

All letters in this word are capitals, like “USA”.
All letters in this word are not capitals, like “leetcode”.
Only the first letter in this word is capital if it has more than one letter, like “Google”.
简单来说就是判断一个字符串符不符合全大写字母或者全小写或者首字母大写这三种规则这一

better solution:

public class Solution {    public boolean detectCapitalUse(String word) {        return word.chars().allMatch(Character::isUpperCase)                         || word.chars().skip(1).allMatch(Character::isLowerCase);    }}

使用stream,首先要熟悉他的所有intermediate和terminal操作,简单概括主要有:前者-过滤,映射,限定范围选择,排序,跳过;后者-组合求解reduce和循环遍历foreach,匹配match,findfirrt等

4.0[二叉树]-.trick four-return 1+function()范式求树高
递归类的问题最好从语义上去理解和code,而不要从语法上,比如这道题:
例题:Given a binary tree, find its maximum depth.

better solution:

public int maxDepth(TreeNode root) {        if(root==null){            return 0;        }        return 1+Math.max(maxDepth(root.left),maxDepth(root.right));    }

对于每一个节点来说(递归的特质)返回左右子树中更长的一条,就是这道题的语义。
二叉树问题对我来说是个难题。

4.1[二叉树]-.trick four-[二叉树]-确定节点操作
二叉树问题和dp有点像,都是先要确定一种在每一个节点(每一步)的范式,然后递归执行,唯一的不同是dp一般要建立一个存储之前步骤造成影响的记录,一般是个一二维数组。

例题:
Invert a binary tree.转置一个二叉树,简单来说就是交换左右子树

Google: 90% of our engineers use the software you wrote (Homebrew), but you can’t invert a binary tree on a whiteboard so fuck off.
据说当初这个homebrew去面试谷歌,没能写出二叉树转置所以gg了。看着这个先是客气一番(我们都用你写的程序),但是(fuck off)还是很搞笑,哈哈哈

my solution
step one:判断节点是否为零
step two:交换左右子树
step three:递归调用

源码:

public class Solution {    public TreeNode invertTree(TreeNode root) {        TreeNode temp;        if(root==null)            return null;        if(root.left==null&&root.right==null)            return root;        TreeNode p=root;        if(p.left!=null||p.right!=null)        {            temp=p.left;            p.left=p.right;            p.right=temp;            invertTree(p.left);            invertTree(p.right);        }        return root;    }}

better solution:
step和我的大同小异,但是可以从语义上来说,我的解法更好理解;

public class Solution {    public TreeNode invertTree(TreeNode root) {        if (root == null) {            return null;        }        final TreeNode left = root.left,                right = root.right;        root.left = invertTree(right);        root.right = invertTree(left);        return root;    }}

5.0.[数理支持]-trick five-逻辑为重,实现为清
想清楚问题后,解法会变得很简单,甚至有时候我会觉得这种题目并不算一道好的编程题,它考查的东西侧重数理逻辑和推理判断,对于编程能力和具体实现没有多少帮助

例题:258. Add Digits
Given a non-negative integer num, repeatedly add all its digits until the result has only one digit.

For example:

Given num = 38, the process is like: 3 + 8 = 11, 1 + 1 = 2. Since 2 has only one digit, return it.
简单来说:把各位数加起来,直到最后只剩一位为止。

my solution:用了loop,不上了
better solution :
源码:

return num==0?0:(num%9==0?9:(num%9));

具体的解释在下面:

Digital roots can be calculated with congruences in modular arithmetic rather than by adding up all the digits, a procedure that can save time in the case of very large numbers.
可以用modular arithmetic 可以比依次把各位加起来高效得多
Digital roots can be used as a sort of checksum, to check that a sum has been performed correctly. If it has, then the digital root of the sum of the given numbers will equal the digital root of the sum of the digital roots of the given numbers. This check, which involves only single-digit calculations, can catch many errors in calculation.
这种运算可以用作checksum**检查盒**(tcp,http等协议中就有类似的检查盒存在)
具体的congruences in modular arithmetic模运算方式

简单来说就是10^k=1相当于10^k%9=1;将各位加起来这个操作,也就是将各位依次对9取模了。
举个栗子:203 = 200%9+3%9=2+3=5;
根据结合律:200%9+3%9=(200+3)%9–>nums%9

5.1.[数理支持]-trick five-逻辑为重,实现为清
一个小把戏:Nim Game:两个人移动石头,每个人每次可以移动1-3块石头,移动最后一块石头的人获胜,问给定n石头,假设两人都是很聪明的人,计算最终谁将获胜,代码很简单,一行解决,关键在于想清楚整个过程

源码和解释就不贴了,需要私聊

5.2.[数理支持]-trick five-求模问题,反向思维
反向思考问题,有时候会有意想不到的奇效

问题:326. Power of Three/two

better solution:找一个int范围内,2(3)最大指数(例如3是1162261467),然后直接模判断是否为零就行

6.0.[分模块解决]trick six–将每一个操作隔离出来,也就是一种小规模化,最后reduce的解法
类似的问题遇到不少,如果按照原始的思路,遇到一个坑填一个坑,往往很复杂;
但是有一种,直接集中填好A类坑,然后又集中填好B类坑的方式,比遇坑填坑的方式,无论时间复杂度还是空间复杂度都要更低

例题:283. Move Zeroes
Given an array nums, write a function to move all 0’s to the end of it while maintaining the relative order of the non-zero elements.

For example, given nums = [0, 1, 0, 3, 12], after calling your function, nums should be [1, 3, 12, 0, 0].

Note:
You must do this in-place without making a copy of the array.
Minimize the total number of operations.
简单来说就是,把一个int数组的所有‘0’移动到最后

my solution:
step one:遍历所有成员,判断是否为零;
step two:检测到成员为零,将所有该成员之后的成员往前移动一格
step three :将最后一个成员置零,将遍历的长度减一
源码:

public class Solution {    public void moveZeroes(int[] nums) {        int len=nums.length;        for(int i=0;i<len;i++){            if(nums[i]==0)            {                for(int j=i;j<len-1;j++)                {                    nums[j]=nums[j+1];                }                nums[len-1]=0;                len--;                i--;            }        }    }}

better solution:
step one:将所有不为零的数字依次填入数组内;
step two :将剩余空间的位置依次填入零;
源码:

public void moveZeroes(int[] nums) {    if (nums == null || nums.length == 0) return;            int insertPos = 0;    for (int num: nums) {        if (num != 0) nums[insertPos++] = num;    }            while (insertPos < nums.length) {        nums[insertPos++] = 0;    }}

对比来看,my solution的时间复杂度最坏情况是NlogN,better solution的时间复杂度稳定为O(n);

7.0.[空数组解决匹配问题]trick six–将操作的结果(一般为英文字符)作为index放入到一个空数组中作为另一个数组操作的pattern
这个问题也可以归结到模块分治;
将内容(或者准确的说 值)作为index的确是一种good trick

例题:383. Ransom Note
Given an arbitrary ransom note string and another string containing letters from all the magazines, write a function that will return true if the ransom note can be constructed from the magazines ; otherwise, it will return false.
大意note的全部字符是否可以由magazines中的字符构成

my solution
step one:新建一个magazine长度相同的字符数组flag数组
step two:遍历note中的所有字符;
step three:用note中的每一个字符去和magazine比较
step four:比较成功且flag数组为零则继续,否则失败

public class Solution {    public static boolean canConstruct(String ransomNote, String magazine)    {        int[] flag=new int[magazine.length()];        for(int i=0;i<ransomNote.length();i++)        {            boolean see=false;            for(int j=0;j<magazine.length();j++)            {                if(magazine.charAt(j)==ransomNote.charAt(i)&&flag[j]==0)                {                    see=true;                    flag[j]=1;                    break;                }            }            if(!see)return false;        }        return true;    }}

better solution:
step one:创建一个大小为26的数组;
step two:遍历note,执行note[i-‘a’]++
step three:遍历magazines,判断magazines[j-‘a’]–,小于零则为失败

public class Solution {    public boolean canConstruct(String ransomNote, String magazine) {        int[] arr = new int[26];        for (int i = 0; i < magazine.length(); i++) {            arr[magazine.charAt(i) - 'a']++;        }        for (int i = 0; i < ransomNote.length(); i++) {            if(--arr[ransomNote.charAt(i)-'a'] < 0) {                return false;            }        }        return true;    }}

比对分析my solution时间复杂度为:O(n^2);空间复杂度为O(n);
better solution时间复杂度为:O(n);空间复杂度为1;

7.1.[空数组解决匹配问题]trick six–将操作的结果(一般为英文字符)作为index放入到一个空数组中作为另一个数组操作的pattern
类似的问题,不在详述

例题:大致的意思是一个String t和一个String s,判断s是不是t的乱序

my solution :两个String调用Arrays.sort();然后逐个比较;

better solution:

public class Solution {    public boolean isAnagram(String s, String t) {        int[] alphabet = new int[26];        for (int i = 0; i < s.length(); i++) alphabet[s.charAt(i) - 'a']++;        for (int i = 0; i < t.length(); i++) alphabet[t.charAt(i) - 'a']--;        for (int i : alphabet) if (i != 0) return false;        return true;    }}

8.0.[dp问题]trick eight–建立一个记忆数组,每次都用新的数据更新记忆,同时每次操作也取决于之前的记忆 9.0.[回溯问题]trick nine留着下一部分的内容

综合来看,对于上述归纳的trick,原因无非那么几种
对java的一些特殊(新)特性不够了解
对位运算不够敏感
对特殊算法技巧理解不够透彻(dp 分治 回溯等)
对树的递归问题不够熟练

后面一篇,看完算4+做够200题再回来接着写

原创粉丝点击