【面试必刷101】系列blog目的在于总结面试必刷101中有意思、可能在面试中会被考到的习题。总结通用性的解题方法,针对特殊的习题总结思路。既是写给自己复习使用,也希望与大家交流。
【面试必刷101】递归/回溯算法总结I
【面试必刷101】递归/回溯算法总结II
【面试必刷101】链表
【面试必刷101】二叉树
【面试必刷101】二分查找
【面试必刷101】栈和队列
【面试必刷101】哈希
双指针比较灵活,可以大大降低时间复杂度,可用在数组,单链表等数据结构中。
逻辑比较简单,但是需要注意这一句Collections.sort(intervals, (a, b) -> a.start - b.start);
就是按照起始位置进行排序,这个非常重要。
import java.util.*;
/**
* Definition for an interval.
* public class Interval {
* int start;
* int end;
* Interval() { start = 0; end = 0; }
* Interval(int s, int e) { start = s; end = e; }
* }
*/
public class Solution {
public ArrayList<Interval> merge(ArrayList<Interval> intervals) {
ArrayList<Interval> res = new ArrayList<Interval>();
if (intervals.size() == 0) return res;
// 这个排序非常重要,需要学会自己写。
Collections.sort(intervals, (a, b) -> a.start - b.start);
int min = intervals.get(0).start, max = intervals.get(0).end;
for (Interval item : intervals) {
if (item.start > max) {
Interval tmp = new Interval(min, max);
res.add(tmp);
min = item.start;
max = item.end;
} else {
max = Math.max(max, item.end);
}
}
res.add(new Interval(min, max));
return res;
}
}
area面积计算方式为:area = (j - i) * Math.min(height[i], height[j])
有啥规律呢?当j-i
减少时,当且仅当Math.min(height[i], height[j])
变大。所以可以由此进行判别指针的变化情况,如果height[i]
i++
找到height[i]
增大的进行判断;相同,如果height[i]>height[j]
那就j--
找到height[j]
增大的进行判断。
import java.util.*;
public class Solution {
public int maxArea (int[] height) {
if (height.length < 2) return 0;
int i = 0, j = height.length - 1;
int res = -1;
while (i < j) {
int area = (j - i) * Math.min(height[i], height[j]);
res = Math.max(area, res);
if (height[i] < height[j]) {
int t = height[i];
while (i < j && height[i] <= t) {
i++;
}
} else {
int t = height[j];
while (i < j && height[j] <= t) {
j--;
}
}
}
return res;
}
}
为啥单调栈版本不香嘞,明明很简单也很好理解。但结果而言,属实有点拉胯
import java.util.*;
public class Solution {
public long maxWater (int[] arr) {
Deque<Integer> dq = new LinkedList<>();
int res = 0;
for (int i = 0; i < arr.length; i++) {
while (!dq.isEmpty() && arr[dq.peekLast()] < arr[i]) {
int pre1 = dq.pollLast();
if (dq.isEmpty()) break;
int pre2 = dq.peekLast();
res += (i - pre2 - 1) * (Math.min(arr[i], arr[pre2]) - arr[pre1]);
}
dq.addLast(i);
}
return res;
}
}
双指针思路:
我们都知道水桶的短板问题,控制水桶水量的是最短的一条板子。这道题也是类似,我们可以将整个图看成一个水桶,两边就是水桶的板,中间比较低的部分就是水桶的底,由较短的边控制水桶的最高水量。但是水桶中可能出现更高的边,比如上图第四列,它比水桶边还要高,那这种情况下它是不是将一个水桶分割成了两个水桶,而中间的那条边就是两个水桶的边。
有了这个思想,解决这道题就容易了,因为我们这里的水桶有两个边,因此可以考虑使用对撞双指针往中间靠。
具体做法:
step 1:检查数组是否为空的特殊情况
step 2:准备双指针,分别指向数组首尾元素,代表最初的两个边界
step 3:指针往中间遍历,遇到更低柱子就是底,用较短的边界减去底就是这一列的接水量,遇到更高的柱子就是新的边界,更新边界大小。
双指针也不太行,此时需要再改进。
import java.util.*;
public class Solution {
public long maxWater (int[] arr) {
if (arr.length < 3) return 0;
int i = 0, j = arr.length - 1, res = 0;
int lmax = 0, rmax = 0;
while (i < j) {
lmax = Math.max(lmax, arr[i]);
rmax = Math.max(rmax, arr[j]);
if (lmax < rmax) {
res += lmax - arr[i++];
} else {
res += rmax - arr[j--];
}
}
return res;
}
}
三指针:
这题让求柱子中间能盛多少水,首先可以肯定两边的两个柱子是不能盛水的,只有两边之间的柱子有可能会盛水。最简单的一种方式就是使用3个指针,先找到最高的柱子,用一个指针top指向最高柱子,然后最高柱子左边用两个指针,一个left,一个right(这里的left和right指向柱子的高度)。
如果left
大于right
,那么肯定是能盛水的,因为left
是小于等于最高柱子top的,并且right
指向的柱子是在left
和最高柱子top之间,根据木桶原理盛水量由最矮的柱子决定,所以盛水是left
-right
。
如果left
不大于right
,是不能盛水的,这时候我们要让left
等于right
。因为right
是不能超过最高柱子的,我们增加left的高度,有利于后面计算的时候盛更多的水。
import java.util.*;
public class Solution {
public long maxWater (int[] arr) {
if (arr.length < 3) return 0;
int mid = 0, max = -1, res = 0;
for (int i = 0; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
mid = i;
}
}
int i = 0, j = 1;
while (i < j && j < mid) {
if (arr[i] > arr[j]) {
res += arr[i] - arr[j];
j ++;
} else {
i = j;
j ++;
}
}
j = arr.length - 1; i = j - 1;
while (i < j && i > mid) {
if (arr[j] > arr[i]) {
res += arr[j] - arr[i];
i --;
} else {
j = i;
i --;
}
}
return res;
}
}
这题的思路很简单:用end往后扩展,找到满足条件的值,再使用begin往后扩展,找到满足条件最短的值。
难点在于:怎么判断条件满足了?答案是引入了一个cnt计数,计算未被满足的T中字符的数量。
该怎么理解呢?map中存储了T中元素的个数。当S中end遍历到一个元素时,如果该元素的map值大于0,说明T中的元素还没有被耗尽,所以cnt–。反之,如果已经小于0了,cnt就不必要变化了,因为对应元素已经耗尽了。无论该值大于0还是小于0,如果这个元素在T中出现过,此时value都需要-1。当S中begin遍历一个元素也是如此,如果当前value值为0,说明该元素恰好被耗尽,如果begin往后走,cnt就要+1,表示该元素在T中出现过,但是在S中没有出现过。但是如果value大于0,或者小于0,含义分别是亏欠了该元素和盈余该元素,此时cnt不变(应该不会出现亏欠,因为对应元素都是小于等于0的,否则也不会出现cnt==0这个条件)。
如果使用int[] 做map速度快点,但是map更好理解一点,下面贴出两者的代码。
import java.util.*;
public class Solution {
public String minWindow (String S, String T) {
int[] map = new int[128];
char[] s = S.toCharArray();
char[] t = T.toCharArray();
for (int i = 0; i < t.length; i++) {
map[t[i]] ++;
}
int begin = 0, end = 0, cnt = t.length, size = Integer.MAX_VALUE, head = 0;
while (end < s.length) {
if (map[s[end]] > 0) {
cnt--;
}
map[s[end]]--;
end++;
while (cnt == 0) {
if (size > end - begin) {
size = end - begin;
head = begin;
}
if (map[s[begin]] == 0) {
cnt++;
}
map[s[begin]] ++;
begin++;
}
}
return size == Integer.MAX_VALUE ? "" : S.substring(head, head + size);
}
}
import java.util.*;
public class Solution {
public String minWindow (String S, String T) {
Map<Character, Integer> map = new HashMap<>();
char[] s = S.toCharArray();
char[] t = T.toCharArray();
for (int i = 0; i < t.length; i++) {
map.put(t[i], map.getOrDefault(t[i], 0) + 1);
}
int begin = 0, end = 0, size = Integer.MAX_VALUE, head = 0;
int cnt = t.length;
for (int i = 0; i < s.length; i++) {
if (map.containsKey(s[end])) {
map.put(s[end], map.get(s[end]) - 1);
if (map.get(s[end]) >= 0) {
cnt--;
}
}
end ++;
while (cnt == 0) {
if (size > end - begin) {
size = end - begin;
head = begin;
}
if (map.containsKey(s[begin])) {
if (map.get(s[begin]) == 0) {
cnt++;
}
map.put(s[begin], map.get(s[begin]) + 1);
}
begin++;
}
}
//System.out.println(size + " ; " + head);
return size == Integer.MAX_VALUE ? "" : S.substring(head, head + size);
}
}
import java.util.*;
public class Solution {
public int maxLength (int[] arr) {
int[] hash = new int[100010];
int res = 0;
for (int i = 0, j = 0; i < arr.length; i++) {
hash[arr[i]] ++;
while (hash[arr[i]] > 1) {
hash[arr[j++]] --;
}
res = Math.max(res, i - j + 1);
}
return res;
}
}
双指针感觉是最有可能在面试中考的题型了,难度适中,代码少,知识点综合度比较高。
需要多做几遍,双指针值得好好学。
今日无总结,事情很多。