目录

第11题. Container With Most Water

题目描述(中等难度)

LeetCode 11-15 题 详解 Java版 ( 万字 图文详解 LeetCode 算法题11-15 =====>>> <建议收藏>)_空间复杂度

每个数组代表一个高度,选两个任意的柱子往里边倒水,能最多倒多少水。

解法一 暴力解法

直接遍历任意两根柱子,求出能存水的大小,用一个变量保存最大的。

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)。

解法二

LeetCode 11-15 题 详解 Java版 ( 万字 图文详解 LeetCode 算法题11-15 =====>>> <建议收藏>)_算法_02

我们理一下思路,大小是由长度和高度决定,如果选 0 到 8 就保证了长度最长,此时大小是 0 号柱子的高度 1 乘以长度 8 。我们如果想面积更大怎么做呢,只能减小长度,增加高度。是左边的柱子向右移动变成 1 号柱子呢?还是右边的柱子向左移动变成 7 号柱子呢?当然是哪边的柱子短就改哪边的!只有这样,高度才有可能增加。

例如我们如果把 8 号柱子变成 7 号柱子,此时长度减少了,然而高度还是 0 号柱子没有变化,所以面积就会减少。把 1 号柱子变成 2 号柱子就很好了,因为此时高度就变成了 8 号柱子的高度,面积就有可能会增加。

如果左右两边柱子相等该怎么办呢?随意!

我们假设 1 号 和 8 号 柱子高度是相等的。如果他们之间的柱子只有 1 根比它俩高或者没有比它俩高的,那么最大面积就一定选取是 1 号和 8 号了,所以 1 号接着变大,或者 8 号接着减小都是无所谓的,因为答案已经确定了。

LeetCode 11-15 题 详解 Java版 ( 万字 图文详解 LeetCode 算法题11-15 =====>>> <建议收藏>)_空间复杂度_03

假设 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

题目描述(中等难度)

LeetCode 11-15 题 详解 Java版 ( 万字 图文详解 LeetCode 算法题11-15 =====>>> <建议收藏>)_算法_04

把数字转换成罗马数字,正常情况就是把每个字母相加,并且大字母在前,小字母在后,上边也介绍了像 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

题目描述(简单难度)

LeetCode 11-15 题 详解 Java版 ( 万字 图文详解 LeetCode 算法题11-15 =====>>> <建议收藏>)_空间复杂度_05

和上一道题相反,将罗马数字转换成阿拉伯数字。

解法一

先来一种不优雅的,也就是我开始的想法。就是遍历字符串,然后转换就可以,但同时得考虑 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

题目描述(简单难度)

LeetCode 11-15 题 详解 Java版 ( 万字 图文详解 LeetCode 算法题11-15 =====>>> <建议收藏>)_算法_06

解法一 垂直比较

LeetCode 11-15 题 详解 Java版 ( 万字 图文详解 LeetCode 算法题11-15 =====>>> <建议收藏>)_时间复杂度_07

我们把所有字符串垂直排列,然后一列一列的比较,直到某一个字符串到达结尾或者该列字符不完全相同。

下边看一下我的代码,看起来比较多

//这个函数判断 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),常数个额外空间。

解法二 水平比较

LeetCode 11-15 题 详解 Java版 ( 万字 图文详解 LeetCode 算法题11-15 =====>>> <建议收藏>)_leetcode_08

我们将字符串水平排列,第 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)。

解法三 递归

LeetCode 11-15 题 详解 Java版 ( 万字 图文详解 LeetCode 算法题11-15 =====>>> <建议收藏>)_java_09

我们把原来的数组分成两部分,求出左半部分的最长公共前缀,求出右半部分的最长公共前缀,然后求出的两个结果再求最长公共前缀,就是最后的结果了。

求左半部分的最长公共前缀,我们可以继续把它分成两部分,按照上边的思路接着求。然后一直分成两部分,递归下去。

直到该部分只有 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

题目描述(中等难度)

LeetCode 11-15 题 详解 Java版 ( 万字 图文详解 LeetCode 算法题11-15 =====>>> <建议收藏>)_算法_10

解法一 暴力解法

无脑搜索,三层循环,遍历所有的情况。但需要注意的是,我们需要把重复的情况去除掉,就是 [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,用来保存结果。

对于遍历,这里用到了从两头同时遍历,从而降低了时间复杂度,很妙!