算法题解之数组

Best Time to Buy and Sell Stock

只能买1次的股票问题

思路:遍历数组的同时,记录到当前天为止的历史最低价格,那么在当前天卖出的最大收益就是当前价格减去历史最低价格,同时更新历史最大收益。

 1 public class Solution {
 2     public int maxProfit(int[] prices) {
 3         if (prices == null || prices.length == 0) {
 4             return 0;
 5         }
 6         int min_price = Integer.MAX_VALUE;
 7         int max_profile = 0;
 8         
 9         for (int i : prices) {
10             min_price = Math.min(min_price, i);
11             max_profile = Math.max(max_profile, i - min_price);
12         }
13         
14         return max_profile;
15     }
16 }
View Code

Best Time to Buy and Sell Stock II

买卖任意次的股票问题

思路:状态dp[i]表示最后一次卖出是在i或i之前的最大收益。如果不在最后一天卖出,那么相当于dp[i-1];如果在最后一天卖出,那么要找到dp[j-1]-prices[j]在j从0~i的最大值,这个值可以一边遍历数组一边记录历史最优。dp[i]是以上两种情况的最大值。

 1 class Solution {
 2     public int maxProfit(int[] prices) {
 3         if (prices == null || prices.length == 0) {
 4            return 0; 
 5         }
 6         int[] dp = new int[prices.length];
 7         dp[0] = 0;
 8         int max_diff = -prices[0]; // 0~i中dp[i-1]-prices[i]的最大值
 9         int res = 0;
10         for (int i = 1; i < dp.length; i++) {
11             max_diff = Math.max(max_diff, dp[i-1] - prices[i]);
12             dp[i] = Math.max(dp[i-1], max_diff + prices[i]);
13             res = Math.max(res, dp[i]);
14         }
15         return res;
16         
17     }
18 }
View Code

 Best Time to Buy and Sell Stock III          

买卖至多两次的股票问题

思路:以第一次交易的结束点来划分该问题。profit[i]表示第一次交易在i卖出的最大收益。之后从后往前遍历数组,得到对应在每个第一次交易结束点的情况下,第二次交易所能达到的最大收益,第一次收益与第二次收益相加得到总收益,然后更新历史最大收益。

 1 class Solution {
 2     public int maxProfit(int[] prices) {
 3         if(prices == null || prices.length == 0) {
 4             return 0;
 5         }
 6         int[] profit = new int[prices.length];
 7         int min_price = Integer.MAX_VALUE;
 8         for (int i = 0; i < prices.length; i++) {
 9             min_price = Math.min(min_price, prices[i]);
10             profit[i] = prices[i] - min_price;
11         }
12         
13         int res = 0;
14         int max_price = Integer.MIN_VALUE;
15         int max_profit = 0;
16         for (int i = prices.length - 1; i > 0; i--) {
17             max_price = Math.max(max_price, prices[i]);
18             max_profit = Math.max(max_profit, max_price - prices[i]);
19             res = Math.max(res, profit[i-1] + max_profit);
20         }
21         return Math.max(res, prices[prices.length-1] - prices[0]);
22     }
23 }
View Code

Best Time to Buy and Sell Stock IV

买卖至多K次的股票问题

思路:

          

Best Time to Buy and Sell Stock with Cooldown

有cooldown的股票问题

思路:跟任意次的股票问题差不多,不同的地方是每次记录的max是dp[j-2]-prices[j]而不是dp[j-1]-prices[j],因为要cooldown一下。时间复杂度O(n)。

 1 public class Solution {
 2     public int maxProfit(int[] prices) {
 3         if (prices == null || prices.length <= 1) {
 4             return 0;
 5         }
 6         int[] dp = new int[prices.length];
 7         int max = Math.max(-prices[0], -prices[1]);
 8         dp[1] = Math.max(0, prices[1] - prices[0]);
 9         
10         for (int i = 2; i < dp.length; i++) {
11             max = Math.max(dp[i - 2] - prices[i], max);
12             dp[i] = Math.max(dp[i - 1], max + prices[i]);
13         }
14         return dp[dp.length - 1];
15     }
16 }
View Code

 

Gas Station

加油站

思路1:很容易想到的暴力解法,时间复杂度为O(n^2)。

思路2:可以将该问题转化为求循环数组的最大子数组。将gas[i] - cost[i]作为新的循环数组,则最大循环数组代表了能累积的最大油量,该数组的起点就是解。

思路3:(思想很重要)

这道题最直观的思路,是逐个尝试每一个站点,从站 i 点出发,看看是否能走完全程。如果不行,就接着试着从站点 i+1出发。

假设从站点 i 出发,到达站点 k 之前,依然能保证油箱里油没见底儿,从k 出发后,见底儿了。那么就说明 diff[i] + diff[i+1] + ... + diff[k] < 0,而除掉diff[k]以外,从diff[i]开始的累加都是 >= 0的。也就是说diff[i] 也是 >= 0的,这个时候我们还有必要从站点 i + 1 尝试吗?仔细一想就知道:车要是从站点 i+1出发,到达站点k后,甚至还没到站点k,油箱就见底儿了,因为少加了站点 i 的油。。。

因此,当我们发现到达k 站点邮箱见底儿后,i 到 k 这些站点都不用作为出发点来试验了,肯定不满足条件,只需要从k+1站点尝试即可!因此解法时间复杂度从O(n2)降到了 O(2n)。之所以是O(2n),是因为将k+1站作为始发站,车得绕圈开回k,来验证k+1是否满足。

等等,真的需要这样吗?

我们模拟一下过程:

a. 最开始,站点0是始发站,假设车开出站点p后,油箱空了,假设sum1 = diff[0] +diff[1] + ... + diff[p],可知sum1 < 0;

b. 根据上面的论述,我们将p+1作为始发站,开出q站后,油箱又空了,设sum2 = diff[p+1] +diff[p+2] + ... + diff[q],可知sum2 < 0。

c. 将q+1作为始发站,假设一直开到了未循环的最末站,油箱没见底儿,设sum3 = diff[q+1] +diff[q+2] + ... + diff[size-1],可知sum3 >= 0。

要想知道车能否开回 q 站,其实就是在sum3 的基础上,依次加上 diff[0] 到 diff[q],看看sum3在这个过程中是否会小于0。但是我们之前已经知道 diff[0] 到 diff[p-1] 这段路,油箱能一直保持非负,因此我们只要算算sum3 + sum1是否 <0,就知道能不能开到 p+1站了。如果能从p+1站开出,只要算算sum3 + sum1 + sum2 是否 < 0,就知都能不能开回q站了。

因为 sum1, sum2 都 < 0,因此如果 sum3 + sum1 + sum2 >=0 那么 sum3 + sum1 必然 >= 0,也就是说,只要sum3 + sum1 + sum2 >=0,车必然能开回q站。而sum3 + sum1 + sum2 其实就是 diff数组的总和 Total,遍历完所有元素已经算出来了。因此 Total 能否 >= 0,就是是否存在这样的站点的 充分必要条件。

这样时间复杂度进一步从O(2n)降到了 O(n)。
View Code
 1 public class Solution {
 2     public int canCompleteCircuit(int[] gas, int[] cost) {
 3         int start = 0;
 4         int total = 0;
 5         int sum = 0;
 6         for (int i = 0; i < gas.length; i++) {
 7             total += gas[i] - cost[i];
 8             sum += gas[i] - cost[i];
 9             if (sum < 0) {
10                 start = i + 1;
11                 sum = 0;
12             }
13         }
14         return total >= 0 ? start : -1;
15     }
16 }
View Code

Game of Life

细胞游戏

思路:inplace要求一个细胞更新之后还要保证之后的细胞能知道它更新之前的状态,方法就是,用四个状态分别表示:

0:原来是死的,现在是死的

-1:原来是死的,现在是活的

1:原来是活的,现在是活的

2:原来是活的,现在是死的。

最后再将所有-1替换为1,所有2替换为0即可。

 1 public class Solution {
 2     public void gameOfLife(int[][] board) {
 3         if (board == null || board.length == 0 || board[0] == null) {
 4             return;
 5         }
 6         int m = board.length;
 7         int n = board[0].length;
 8         for (int i = 0; i < m; i++) {
 9             for (int j = 0; j < n; j++) {
10                 set(board, i, j, m, n);
11             }
12         }
13         for (int i = 0; i < m; i++) {
14             for (int j = 0; j < n; j++) {
15                 update(board, i, j);
16             }
17         }
18     }
19     
20     public void set(int[][] board, int i, int j, int m, int n) {
21         int lives = 0;
22         for (int x = Math.max(i - 1, 0); x <= Math.min(i + 1, m - 1); x++) {
23             for (int y = Math.max(j - 1, 0); y <= Math.min(j + 1, n - 1); y++) {
24                 lives += board[x][y] >= 1 ? 1 : 0;
25             }
26         }
27         lives -= board[i][j];
28         if (board[i][j] == 1) {
29             if (lives < 2 || lives > 3) {
30                 board[i][j] = 2;
31             } else {
32                 board[i][j] = 1;
33             }
34         } else {
35             if (lives == 3) {
36                 board[i][j] = -1;
37             } else {
38                 board[i][j] = 0;
39             }
40         }
41     }
42     
43     public void update(int[][] board, int i, int j) {
44         if (board[i][j] == -1) {
45             board[i][j] = 1;
46         } else if (board[i][j] == 2) {
47             board[i][j] = 0;
48         }
49     }
50 }
View Code

H-IndexI

计算科学家的h指数I

思路1:先排一遍序,遍历数组,h从N依次递减下来,找到第一个满足h <= citations[i]的地方。时间复杂度O(nlgn),空间操作是in-place。

 1 public class Solution {
 2     public int hIndex(int[] citations) {
 3         if (citations == null || citations.length == 0) {
 4             return 0;
 5         }
 6         Arrays.sort(citations);
 7         int h = citations.length;
 8         for (int i = 0; i < citations.length; i++) {
 9             if (citations[i] >= h) {
10                 return h;
11             }
12             h--;
13         }
14         return h;
15     }
16 }
View Code

思路2:使用hash(这里用数组)。paperNum[i]代表引用数为i的文章数(大于N的记为N)。时间复杂度O(n),但是要使用额外空间。

 1 public class Solution {
 2     public int hIndex(int[] citations) {
 3         int N = citations.length;
 4         int[] paperNum = new int[N + 1];
 5         for (int citation : citations) {
 6            citation = citation > N ? N : citation;
 7            paperNum[citation]++;
 8         }
 9         
10         int paperCount = 0;
11         for (int h = N; h > 0; h--) {
12             paperCount += paperNum[h];
13             if (paperCount >= h) {
14                 return h;
15             }
16         }
17         return 0;
18     }
19 }
View Code

Majority Element

查找多数元素

思路1:我自己的思路。用一个hash表记次数,最后返回次数最多的数。时间复杂度O(n),最坏情况空间复杂度O(n/2);

 1 public class Solution {
 2     public int majorityElement(int[] nums) {
 3         Map<Integer, Integer> map = new HashMap<Integer, Integer>();
 4         int res = 0;
 5         int maxCount = 0;
 6         for (int num : nums) {
 7             if (map.containsKey(num)) {
 8                 map.put(num, map.get(num) + 1);
 9             } else {
10                 map.put(num, 1);
11             }
12             if (map.get(num) > maxCount) {
13                 maxCount = map.get(num);
14                 res = num;
15             }
16         }
17         return res;
18         
19     }
20 }
View Code

思路2:Moore voting algorithm:先选取第一个元素作为多数元素,记一个count作为它的得票数,如果后面的元素与它相等则count++,如果不相等则count--,当count为0时,说明前面的数中不存在重复次数超过一半的数,原问题可以转化为从剩下的数组中找出多数元素。时间复杂度O(n),空间复杂度O(1)。

 1 public class Solution {
 2     public int majorityElement(int[] nums) {
 3 
 4         int major = 0; 
 5         int count = 0;
 6         for (int num : nums) {
 7             if (count == 0) {
 8                 count++;
 9                 major = num;
10             } else if (major == num) {
11                 count++;
12             } else {
13                 count--;
14             }
15         }
16         return major;
17     }
18 }
View Code

Minimum Size Subarray Sum

满足条件的最小子数组的长度

思路:这道题跟gas station很像。分析过程都是:先考虑它的暴力解法,时间复杂度为O(n^2),排除用动态规划。通常这种情况就分析暴力方法做了什么重复性的工作。

   暴力方法是:定义两个指针,p1是子数组的头,p2是子数组的尾。固定住p1,p2遍历p1之后的每个元素,结束后p1加1,p2再遍历p1之后每一个元素......

   容易知道,如果i~j这段子数组小于s,那么i~j中的任意一段子数组都比s小,就可以不考虑;反之,如果i~j这一段子数组大于s,那么任何包含这段子数组的数组肯定大于s,也可以不考虑。因此当p2移动到第一个使子数组大于s的地方时,之后的所有元素就可以不遍历,并且p1加一后,p2的开始位置也就是当前位置(因为它们之间的任意子数组都是小于s的)。用这种策略的话,避免了重复性工作,时间复杂度为O(n)。

 1 public class Solution {
 2     public int minSubArrayLen(int s, int[] nums) {
 3         int[] prefix = new int[nums.length + 1];
 4         for (int i = 1; i < prefix.length; i++) {
 5             prefix[i] = prefix[i - 1] + nums[i - 1];
 6         }
 7         
 8         int p1 = 0;
 9         int p2 = 1;
10         int min_len = Integer.MAX_VALUE;
11         while (p1 <= prefix.length - 1 && p2 <= prefix.length - 1) {
12             while (p2 <= prefix.length - 1 && prefix[p2] - prefix[p1] < s) {
13                 p2++;
14             }
15             if (p2 <= prefix.length - 1) {
16                 min_len = Math.min(p2 - p1, min_len);
17             }
18             p1++;
19         }
20         return (min_len == Integer.MAX_VALUE ? 0 : min_len);
21     }
22 }
View Code

Missing Number

寻找丢失的数

思路1:我自己的思路,比较麻烦。遍历数组,把当前数对应的数组的位置置为-1。这种方法虽然时间复杂度为O(n),空间复杂度为O(1),但是修改了原始数据。。不是个好方法。

 1 public class Solution {
 2     public int missingNumber(int[] nums) {
 3         int n = nums.length;
 4         for (int i = 0; i < n; i++) {
 5             int cur = nums[i];
 6             if (cur == n || cur == -1) {
 7                 continue;
 8             } else if (cur <= i) {
 9                 nums[cur] = -1;
10             } else {
11                 while (cur > i && cur < n) {
12                     int tmp = nums[cur];
13                     nums[cur] = -1;
14                     cur = tmp;
15                 }
16                 if (cur < n) {
17                     nums[cur] = -1;
18                 }
19             }
20         }
21         
22         for (int i = 0; i < n; i++) {
23             if (nums[i] != -1) {
24                 return i;
25             }
26         }
27         return n;
28     }
29 }
View Code

思路2:sum。可以算出0~n的和,遍历数组在sum上减去当前数,最后剩下的就是Miss number。

1 public int missingNumber(int[] nums) { //sum
2     int len = nums.length;
3     int sum = (0+len)*(len+1)/2;
4     for(int i=0; i<len; i++)
5         sum-=nums[i];
6     return sum;
7 }
View Code

思路3:异或。将数组中所有数异或,再异或0~n,最后结果一定是miss number(因为这里面只有miss number是单个的,其他数字都是成对的,异或时两两抵消)。

1 public int missingNumber(int[] nums) { //xor
2     int res = nums.length;
3     for(int i=0; i<nums.length; i++){
4         res ^= i;
5         res ^= nums[i];
6     }
7     return res;
8 }
View Code

思路4:如果该数组是排序的,用二分比较好,这样时间复杂度就是O(lgn)了。

Move Zeroes

移动0元素

思路:用两个指针。一个指向第一个0元素,另一个往后扫描,扫到0直接过,扫到非0数就与第一个0元素交换。

 1 public class Solution {
 2     public void moveZeroes(int[] nums) {
 3         if (nums == null) {
 4             return;
 5         }
 6         int zeroHead = -1;
 7         while (++zeroHead < nums.length && nums[zeroHead] != 0) {}
 8         for (int i = zeroHead + 1; i < nums.length; i++) {
 9             if (nums[i] != 0) {
10                 int tmp = nums[i];
11                 nums[i] = 0;
12                 nums[zeroHead++] = tmp;
13             }
14         }
15     }
16 }
View Code

Product of Array Except Self

除去自身的数组乘积

思路:要求不能用除法。不能有额外空间消耗(除了输出的output数组外),要在O(n)的时间完成。首先发现每个output值可以分解为左半边的乘积乘以右半边的乘积,因此第一遍扫描先在output记录左边的前缀积,第二遍从后往前扫描,用右边乘积乘以左边记录好的乘积即可。

 1 public class Solution {
 2     public int[] productExceptSelf(int[] nums) {
 3         if (nums == null || nums.length == 0) {
 4             return null;
 5         }
 6         int[] output = new int[nums.length];
 7         
 8         output[0] = nums[0];
 9         for (int i = 1; i < nums.length; i++) {
10             output[i] = output[i - 1] * nums[i];
11         }
12         
13         int right_mult = 1;
14         for (int i = nums.length - 1; i > 0; i--) {
15             output[i] = right_mult * output[i - 1];
16             right_mult *= nums[i];
17         }
18         output[0] = right_mult;
19         
20         return output;
21     }
22 }
View Code

Repeated Substring Pattern

重复子串模式

思路1:当子串的长度能整除str的长度时,把子串重复多次后,与Str比较。

 1 public class Solution {
 2     public boolean repeatedSubstringPattern(String str) {
 3         
 4         for (int l = 1; l <= str.length() / 2; l++) {
 5             if (str.length() % l == 0) {
 6                 int times = str.length() / l;
 7                 String sub = str.substring(0, l);
 8                 StringBuilder sb = new StringBuilder();
 9                 for (int i = 1; i <= times; i++) {
10                     sb.append(sub);
11                 }
12                 if (sb.toString().equals(str)) {
13                     return true;
14                 }
15             }
16         }
17         return false;
18     }
19 }
View Code

思路2:kmp。还没写。

Reverse String

翻转字符串

思路:将字符串转化为charArray,然后用two pointers翻转这个charArray。

 1 public class Solution {
 2     public String reverseString(String s) {
 3         if (s == null) {
 4             return null;
 5         }
 6         char[] cs = s.toCharArray();
 7         int start = 0;
 8         int end = s.length() - 1;
 9         while (start < end) {
10             char tmp = cs[start];
11             cs[start] = cs[end];
12             cs[end] = tmp;
13             start++;
14             end--;
15         }
16         return String.valueOf(cs);
17     }
18 }
View Code

Reverse Vowels of a String

翻转元音

思路:跟翻转字符串一样,只不过两根指针一直滑到元音停住再交换。

 1 public class Solution {
 2     public String reverseVowels(String s) {
 3         if (s == null) {
 4             return null;
 5         }
 6         char[] cs = s.toCharArray();
 7         int start = 0;
 8         int end = s.length() - 1;
 9         while (start < end) {
10             while (start < s.length() && !isVowel(cs[start])) {
11                 start++;
12             }
13             while (end >= 0 && !isVowel(cs[end])) {
14                 end--;
15             }
16             if (start < end) {
17                 char tmp = cs[start];
18                 cs[start] = cs[end];
19                 cs[end] = tmp;
20                 start++;
21                 end--;
22             }
23         }
24         return String.valueOf(cs);
25     }
26     
27     public boolean isVowel(char c) {
28         if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' || c == 'A' || c == 'E' || c == 'I' || c == 'O' || c == 'U') {
29             return true;
30         }
31         return false;
32     }
33 }
View Code

   

Rotate Array

旋转数组

虽然是一道easy,但是要求用多种方法,这里写出两种in-place并且时间复杂度是O(n)的解法。

思路1:三步翻转法:使用reverse,先把整个数组翻转,再把前k个翻转,再把后 n-k 个翻转。(这种方法现在在leetcode已经超时了。。leetcode时间要求卡得太死了)

 1 public class Solution {
 2     public void rotate(int[] nums, int k) {
 3         k %= nums.length;
 4         reverse(nums, 0, nums.length - 1);
 5         reverse(nums, 0, k - 1);
 6         reverse(nums, k, nums.length - 1);
 7     }
 8     public void reverse(int[] nums, int start, int end) {
 9         while (start < end) {
10             int temp = nums[start];
11             nums[start] = nums[end];
12             nums[end] = temp;
13             start++;
14             end--;
15         }
16     }
17 }
View Code

思路2:使用Cyclic Replacements:具体的做法是,选定一个起点,把它的值放到它最终的位置上(放之前要把那个位置上的数存到prev),然后把prev里的数放到它最终的位置上.......直到回到起点。如果这时已经放置了n个数,则结束,否则起点的位置加一继续。代码中,prev是当前要放置的数,next是该数要放的位置,cur是prev原来的位置。

 1 public class Solution {
 2     public void rotate(int[] nums, int k) {
 3         k = k % nums.length;
 4         int count = 0;
 5         for (int start = 0; count < nums.length; start++) {
 6             int current = start;
 7             int prev = nums[start];
 8             do {
 9                 int next = (current + k) % nums.length;
10                 int temp = nums[next];
11                 nums[next] = prev;
12                 prev = temp;
13                 current = next;
14                 count++;
15             } while (start != current);
16         }
17     }
18 }
View Code

Summary Ranges

范围合并

思路:记录范围的head,end。如果当前数比end大1,则end更新为当前数;否则,将head到end加到结果中,并将head和end更新为当前数。

 1 public class Solution {
 2     public List<String> summaryRanges(int[] nums) {
 3         List<String> res = new ArrayList<String>();
 4         if (nums == null || nums.length == 0) {
 5             return res;
 6         }
 7         
 8         int head = nums[0];
 9         int end = head;
10         for (int i = 1; i < nums.length; i++) {
11             if (nums[i] == end + 1) {
12                 end = nums[i];
13             } else {
14                 add(res, head, end);
15                 head = nums[i];
16                 end = head;
17             }
18         }
19         add(res, head, end);
20         return res;
21     }
22     
23     public void add(List<String> res, int head, int end) {
24         if (head == end) {
25             res.add(String.valueOf(head));
26         } else {
27             res.add(String.valueOf(head) + "->" + String.valueOf(end));
28         }
29     }
30 }
View Code

   

Trapping Rain Water

接雨水

思路:这道题一开始想复杂了,觉得要用单调栈,寻找每个柱形左边第一个比它大和右边第一个比它大,然后就想了好久没做出来。后来才发现只需要寻找每个柱形左边(包括该柱形)的最大值和右边(包括该柱形)的最大值,两边最大值的最小值就是接雨水之后所能达到的高度。想清楚这点后就很简单了。因此这道题难点在于想到把问题转化为求每个柱形左边最大值和右边最大值。

 1 public class Solution {
 2     public int trap(int[] height) {
 3         if (height == null || height.length == 0) {
 4             return 0;
 5         }
 6         int len = height.length;
 7         int[] leftMax = new int[len];
 8         int[] rightMax = new int[len];
 9         leftMax[0] = height[0];
10         for (int i = 1; i < len; i++) {
11             leftMax[i] = Math.max(height[i], leftMax[i - 1]);
12         }
13         rightMax[len - 1] = height[len - 1];
14         for (int j = len - 2; j >= 0; j--) {
15             rightMax[j] = Math.max(height[j], rightMax[j + 1]);
16         }
17         int water = 0;
18         for (int i = 0; i < len; i++) {
19             water += Math.min(leftMax[i], rightMax[i]) - height[i];
20         }
21         return water; 
22     }
23 }
View Code
原文地址:https://www.cnblogs.com/coldyan/p/6039814.html