目录
第11题. Container With Most Water
题目描述(中等难度)
解法一 暴力解法
解法二
总
第12题. Integer to Roman
题目描述(中等难度)
解法一
解法二
解法三
总
第13题: Roman to Integer
题目描述(简单难度)
解法一
解法二
解法三
总
第14题: Longest Common Prefix
解法一 垂直比较
解法二 水平比较
解法三 递归
总
第15题 : 3Sum
题目描述(中等难度)
解法一 暴力解法
解法二
总
喜欢 请点个 + 关注
第11题. Container With Most Water
题目描述(中等难度)
每个数组代表一个高度,选两个任意的柱子往里边倒水,能最多倒多少水。
解法一 暴力解法
直接遍历任意两根柱子,求出能存水的大小,用一个变量保存最大的。
public int maxArea(int[] height) { int max = 0; for (int i = 0; i < height.length; i++) { for (int j = i + 1; j < height.length; j++) { int h = Math.min(height[i], height[j]); if (h * (j - i) > max) { max = h * (j - i); } } } return max; }
时间复杂度:O(n²)。
空间复杂度:O(1)。
解法二
我们理一下思路,大小是由长度和高度决定,如果选 0 到 8 就保证了长度最长,此时大小是 0 号柱子的高度 1 乘以长度 8 。我们如果想面积更大怎么做呢,只能减小长度,增加高度。是左边的柱子向右移动变成 1 号柱子呢?还是右边的柱子向左移动变成 7 号柱子呢?当然是哪边的柱子短就改哪边的!只有这样,高度才有可能增加。
例如我们如果把 8 号柱子变成 7 号柱子,此时长度减少了,然而高度还是 0 号柱子没有变化,所以面积就会减少。把 1 号柱子变成 2 号柱子就很好了,因为此时高度就变成了 8 号柱子的高度,面积就有可能会增加。
如果左右两边柱子相等该怎么办呢?随意!
我们假设 1 号 和 8 号 柱子高度是相等的。如果他们之间的柱子只有 1 根比它俩高或者没有比它俩高的,那么最大面积就一定选取是 1 号和 8 号了,所以 1 号接着变大,或者 8 号接着减小都是无所谓的,因为答案已经确定了。
4
假设 1 号 和 8 号之间有 2 根或以上的柱子比它俩高,假设是 4 号和 6 号比它俩高。1 号会变到 2 号、3 号,最终为 4 号,8 号会变到 7 号, 6 号,而在这个过程中产生的面积一定不会比 1 号和 8 号产生的面积大,因为过程中的柱子都比 1 号和 8 号低。所以是先变 1 号还是先变 8 号是无所谓的,无非是谁先到达更长的柱子而已。
看一下下边的算法,会更加清楚一些。
public int maxArea2(int[] height) { int maxarea = 0, l = 0, r = height.length - 1; while (l < r) { maxarea = Math.max(maxarea, Math.min(height[l], height[r]) * (r - l)); if (height[l] < height[r]) l++; else r--; } return maxarea; }
时间复杂度:O(n)。
空间复杂度:O(1)。
总
为了减少暴力解法的时间复杂度,只能去深层次的理解题意,从而找出突破点。
第12题. Integer to Roman
题目描述(中等难度)
把数字转换成罗马数字,正常情况就是把每个字母相加,并且大字母在前,小字母在后,上边也介绍了像 4 和 9 那些特殊情况。
解法一
这个是自己的解法,主要思想就是每次取出一位,然后得到相应的罗马数字,然后合起来就行
public String getRoman(int num,int count){ //count 表示当前的位数,个位,十位... char[]ten={'I','X','C','M'}; //1,10,100,1000 char[]five={'V','L','D'};//5,50,500 String r=""; if(num<=3){ while(num!=0){ r+=ten[count]; num--; } } if(num==4){ r=(ten[count]+"")+(five[count]+""); } if(num==5){ r=five[count]+""; } if(num>5&&num<9){ r=five[count]+""; num-=5; while(num!=0){ r+=ten[count]; num--; } } if(num==9){ r = (ten[count] + "") + (ten[count + 1] + ""); } return r; } public String intToRoman(int num) { String r=""; int count=0; while(num!=0){ int pop=num%10; r=getRoman(pop,count)+r; count++; num/=10; } return r; }
时间复杂度:num 的位数 log10(num)+1log_{10}(num)+1log10(num)+1所以时间复杂度是 O(log(n))。
空间复杂度:常数个变量,O(1)。
下边在分享一些 LeetCode 讨论里的一些解法
解法二
https://leetcode.com/problems/integer-to-roman/discuss/6310/My-java-solution-easy-to-understand
public String intToRoman(int num) { int[] values = {1000,900,500,400,100,90,50,40,10,9,5,4,1}; String[] strs = {"M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"}; StringBuilder sb = new StringBuilder(); for(int i=0;i<values.length;i++) { while(num >= values[i]) { num -= values[i]; sb.append(strs[i]); } } return sb.toString(); }
相当简洁了,主要就是把所有的组合列出来,因为罗马数字表示的大小就是把所有字母相加,所以每次 append 那个,再把对应的值减去就行了。
时间复杂度:不是很清楚,也许是 O(1)?因为似乎和问题规模没什么关系了。
空间复杂度:O(1).
解法三
https://leetcode.com/problems/integer-to-roman/discuss/6376/Simple-JAVA-solution
public String intToRoman(int num) { String M[] = {"", "M", "MM", "MMM"};//0,1000,2000,3000 String C[] = {"", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"};//0,100,200,300... String X[] = {"", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"};//0,10,20,30... String I[] = {"", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"};//0,1,2,3... return M[num/1000] + C[(num%1000)/100]+ X[(num%100)/10] + I[num%10]; }
这就更加暴力了,把每位的情况都列出来然后直接返回,但思路清晰明了呀。
时间复杂度:O(1)或者说是 num 的位数,不是很确定。
空间复杂度:O(1)。
总
这道题感觉难度应该是 easy ,没有那么难,就是理清楚题意,然后就可以往出列举就行了。
第13题: Roman to Integer
题目描述(简单难度)
6
和上一道题相反,将罗马数字转换成阿拉伯数字。
解法一
先来一种不优雅的,也就是我开始的想法。就是遍历字符串,然后转换就可以,但同时得考虑 IV,IX 那些特殊情况。
public int getInt(char r) { int ans = 0; switch (r) { case 'I': ans = 1; break; case 'V': ans = 5; break; case 'X': ans = 10; break; case 'L': ans = 50; break; case 'C': ans = 100; break; case 'D': ans = 500; break; case 'M': ans = 1000; } return ans; } public int getInt(char r, char r_after) { int ans = 0; switch (r) { case 'I': ans = 1; break; case 'V': ans = 5; break; case 'X': ans = 10; break; case 'L': ans = 50; break; case 'C': ans = 100; break; case 'D': ans = 500; break; case 'M': ans = 1000; break; } if (r == 'I') { switch (r_after) { case 'V': ans = 4; break; case 'X': ans = 9; } } if (r == 'X') { switch (r_after) { case 'L': ans = 40; break; case 'C': ans = 90; } } if (r == 'C') { switch (r_after) { case 'D': ans = 400; break; case 'M': ans = 900; } } return ans; } public boolean isGetTwoInt(char r, char r_after) { if (r == 'I') { switch (r_after) { case 'V': return true; case 'X': return true; } } if (r == 'X') { switch (r_after) { case 'L': return true; case 'C': return true; } } if (r == 'C') { switch (r_after) { case 'D': return true; case 'M': return true; } } return false; } public int romanToInt(String s) { int ans = 0; for (int i = 0; i < s.length() - 1; i++) { ans += getInt(s.charAt(i), s.charAt(i + 1)); //判断是否是两个字符的特殊情况 if (isGetTwoInt(s.charAt(i), s.charAt(i + 1))) { i++; } } //将最后一个字符单独判断,如果放到上边的循环会越界 if (!(s.length() >= 2 && isGetTwoInt(s.charAt(s.length() - 2), s.charAt(s.length() - 1)))) { ans += getInt(s.charAt(s.length() - 1)); } return ans; }
时间复杂度:O(n),n 是字符串的长度。
空间复杂度:O(1)。
下边分享一些优雅的。
解法二
https://leetcode.com/problems/roman-to-integer/description/
public int romanToInt(String s) { int sum=0; if(s.indexOf("IV")!=-1){sum-=2;} if(s.indexOf("IX")!=-1){sum-=2;} if(s.indexOf("XL")!=-1){sum-=20;} if(s.indexOf("XC")!=-1){sum-=20;} if(s.indexOf("CD")!=-1){sum-=200;} if(s.indexOf("CM")!=-1){sum-=200;} char c[]=s.toCharArray(); int count=0; for(;count<=s.length()-1;count++){ if(c[count]=='M') sum+=1000; if(c[count]=='D') sum+=500; if(c[count]=='C') sum+=100; if(c[count]=='L') sum+=50; if(c[count]=='X') sum+=10; if(c[count]=='V') sum+=5; if(c[count]=='I') sum+=1; } return sum; }
把出现的特殊情况,提前减了就可以。
时间复杂度:O(1)。
空间复杂度:O(1
解法三
https://leetcode.com/problems/roman-to-integer/discuss/6509/7ms-solution-in-Java.-easy-to-understand
利用到罗马数字的规则,一般情况是表示数字大的字母在前,数字小的字母在后,如果不是这样,就说明出现了特殊情况,此时应该做减法。
private int getVal(char c){ switch (c){ case 'M': return 1000; case 'D': return 500; case 'C': return 100; case 'L': return 50; case 'X' : return 10; case 'V': return 5; case 'I': return 1; } throw new IllegalArgumentException("unsupported character"); } public int romanToInt(String s) { int res = 0; if(s.length() == 0) return res; for (int i = 0; i < s.length() - 1; i++) { int cur = getVal(s.charAt(i)); int nex = getVal(s.charAt(i+1)); if(cur < nex){ res -= cur; }else{ res += cur; } } return res + getVal(s.charAt(s.length()-1)); }
时间复杂度:O(1)。
空间复杂度:O(1)
总
这道题也不难,自己一开始没有充分利用罗马数字的特点,而是用一些 if,switch 语句判断是否是特殊情况,看起来就很繁琐了。
第14题: Longest Common Prefix
解法一 垂直比较
我们把所有字符串垂直排列,然后一列一列的比较,直到某一个字符串到达结尾或者该列字符不完全相同。
下边看一下我的代码,看起来比较多
//这个函数判断 index 列的字符是否完全相同 public boolean isSameAtIndex(String[] strs, int index) { int i = 0; while (i < strs.length - 1) { if (strs[i].charAt(index) == strs[i + 1].charAt(index)) { i++; } else { return false; } } return true; } public String longestCommonPrefix(String[] strs) { if (strs.length == 0) return ""; //得到最短的字符串的长度 int minLength = Integer.MAX_VALUE; for (int i = 0; i < strs.length; i++) { if (strs[i].length() < minLength) { minLength = strs[i].length(); } } int j = 0; //遍历所有列 for (; j < minLength; j++) { //如果当前列字符不完全相同,则结束循环 if (!isSameAtIndex(strs, j)) { break; } } return strs[0].substring(0, j); }
下边看一下,官方的代码
public String longestCommonPrefix(String[] strs) { if (strs == null || strs.length == 0) return ""; //遍历所有列 for (int i = 0; i < strs[0].length() ; i++){ char c = strs[0].charAt(i); // 保存 i 列第 0 行的字符便于后续比较 //比较第 1,2,3... 行的字符和第 0 行是否相等 for (int j = 1; j < strs.length; j ++) { /** * i == strs[j].length() 表明当前行已经到达末尾 * strs[j].charAt(i) != c 当前行和第 0 行字符不相等 * 上边两种情况返回结果 */ if (i == strs[j].length() || strs[j].charAt(i) != c) return strs[0].substring(0, i); } } return strs[0]; }
时间复杂度:最坏的情况就是 n 个 长度为 m 的完全一样的字符串,假设 S 是所有字符的和,那么 S = m * n,时间复杂度就是 O(S)。当然正常情况下并不需要比较所有字符串,最多比较 n * minLen 个字符就可以了。
空间复杂度:O(1),常数个额外空间。
解法二 水平比较
我们将字符串水平排列,第 0 个和第 1 个字符串找最长子串,结果为 leet,再把结果和第 2 个字符串比较,结果为 leet,再把结果和第 3 个字符串比较,结果为 lee,即为最终结果。
public String longestCommonPrefix3(String[] strs) { if (strs.length == 0) return ""; String prefix = strs[0]; // 保存结果 // 遍历每一个字符串 for (int i = 1; i < strs.length; i++) { // 找到上次得到的结果 prefix 和当前字符串的最长子串 int minLen = Math.min(prefix.length(), strs[i].length()); int j = 0; for (; j < minLen; j++) { if (prefix.charAt(j) != strs[i].charAt(j)) { break; } } prefix = prefix.substring(0, j); } return prefix; }
时间复杂度:最坏情况和解法一是一样,n 个长度为 m 的完全相同的字符,就要比较所有的字符 S,S = n * m 。但对于正常情况,处于最短字符串前的字符串依旧要比较所有字符,而不是最短字符串个字符,相对于解法一较差。
空间复杂度:O(1)。
解法三 递归
我们把原来的数组分成两部分,求出左半部分的最长公共前缀,求出右半部分的最长公共前缀,然后求出的两个结果再求最长公共前缀,就是最后的结果了。
求左半部分的最长公共前缀,我们可以继续把它分成两部分,按照上边的思路接着求。然后一直分成两部分,递归下去。
直到该部分只有 1 个字符串,那么最长公共子串就是它本身了,直接返回就可以了。
public String longestCommonPrefix(String[] strs) { if (strs == null || strs.length == 0) return ""; return longestCommonPrefix(strs, 0 , strs.length - 1); } //递归不断分成两部分 private String longestCommonPrefix(String[] strs, int l, int r) { if (l == r) { return strs[l]; } else { int mid = (l + r)/2; String lcpLeft = longestCommonPrefix(strs, l , mid); String lcpRight = longestCommonPrefix(strs, mid + 1,r); return commonPrefix(lcpLeft, lcpRight); } } //求两个结果的最长公共前缀 String commonPrefix(String left,String right) { int min = Math.min(left.length(), right.length()); for (int i = 0; i < min; i++) { if ( left.charAt(i) != right.charAt(i) ) return left.substring(0, i); } return left.substring(0, min); }
时间复杂度:
空间复杂度:
每次遇到递归的情况,总是有些理不清楚,先空着吧。
总
进行了垂直比较和水平比较,又用到了递归,solution 里还介绍了二分查找,感觉这里用二分查找有些太僵硬了,反而使得时间复杂度变高了。还介绍了前缀树,这里后边遇到再总结吧。
第15题 : 3Sum
题目描述(中等难度)
解法一 暴力解法
无脑搜索,三层循环,遍历所有的情况。但需要注意的是,我们需要把重复的情况去除掉,就是 [1, -1 ,0] 和 [0, -1, 1] 是属于同一种情况的。
public List<List<Integer>> threeSum(int[] nums) { List<List<Integer>> res = new ArrayList<List<Integer>>(); for (int i = 0; i < nums.length; i++) { for (int j = i + 1; j < nums.length; j++) for (int k = j + 1; k < nums.length; k++) { if (nums[i] + nums[j] + nums[k] == 0) { List<Integer> temp = new ArrayList<Integer>(); temp.add(nums[i]); temp.add(nums[j]); temp.add(nums[k]); //判断结果中是否已经有 temp 。 if (isInList(res, temp)) { continue; } res.add(temp); } } } return res; } public boolean isInList(List<List<Integer>> l, List<Integer> a) { for (int i = 0; i < l.size(); i++) { //判断两个 List 是否相同 if (isSame(l.get(i), a)) { return true; } } return false; } public boolean isSame(List<Integer> a, List<Integer> b) { int count = 0; Collections.sort(a); Collections.sort(b); //排序后判断每个元素是否对应相等 for (int i = 0; i < a.size(); i++) { if (a.get(i) != b.get(i)) { return false; } } return true; }
时间复杂度:n 表示 num 的个数,三个循环 O(n³),而 isInList 也需要 O(n),总共就是 O(n4)O(n^4)O(n4),leetCode 复杂度到了 O(n3)O(n^3)O(n3) 一般就报超时错误了,所以算法还得优化。
空间复杂度:最坏情况,即 O(N), N 是指 n 个元素的排列组合个数,即 N=Cn3N=C^3_nN=Cn3,用来保存结果。
解法二
参考了这里
主要思想是,遍历数组,用 0 减去当前的数,作为 sum ,然后再找两个数使得和为 sum。
这样看来遍历需要 O(n),再找两个数需要 O(n²)的复杂度,还是需要 O(n³)。
巧妙之处在于怎么找另外两个数。
最最优美的地方就是,首先将给定的 num 排序。
这样我们就可以用两个指针,一个指向头,一个指向尾,去找这两个数字,这样的话,找另外两个数时间复杂度就会从 O(n²),降到 O(n)。
而怎么保证不加入重复的 list 呢?
要记得我们的 nums 已经有序了,所以只需要找到一组之后,当前指针要移到和当前元素不同的地方。其次在遍历数组的时候,如果和上个数字相同,也要继续后移。文字表述比较困难,可以先看下代码。
public List<List<Integer>> threeSum(int[] num) { Arrays.sort(num); //排序 List<List<Integer>> res = new LinkedList<>(); for (int i = 0; i < num.length-2; i++) { //为了保证不加入重复的 list,因为是有序的,所以如果和前一个元素相同,只需要继续后移就可以 if (i == 0 || (i > 0 && num[i] != num[i-1])) { //两个指针,并且头指针从i + 1开始,防止加入重复的元素 int lo = i+1, hi = num.length-1, sum = 0 - num[i]; while (lo < hi) { if (num[lo] + num[hi] == sum) { res.add(Arrays.asList(num[i], num[lo], num[hi])); //元素相同要后移,防止加入重复的 list while (lo < hi && num[lo] == num[lo+1]) lo++; while (lo < hi && num[hi] == num[hi-1]) hi--; lo++; hi--; } else if (num[lo] + num[hi] < sum) lo++; else hi--; } } } return res; }
时间复杂度:O(n²),n 指的是 num
空间复杂度:O(N),最坏情况,即 N 是指 n 个元素的排列组合个数,即 N=Cn3N=C^3_nN=Cn3,用来保存结果。
总
对于遍历,这里用到了从两头同时遍历,从而降低了时间复杂度,很妙!
今天我们一起学习了LeetCode 10-15 题的算法分析,感谢大家阅读,觉得不错记得收藏哦!