双指针题算是数组类型题目的一个子模块了。
373. Partition Array by Odd and Even
把一个数组划分为奇数在前偶数在后的状态,要求in place。很简单,就用双指针法,让两个指针从两头往中间扫描,当左边是偶数右边是奇数时就交换,直到左右指针相遇为止。
public void partitionArray(int[] nums) {
int left = 0, right = nums.length - 1;
while (left < right) {
// 左边是奇数的话就自增,直到找到前面的偶数
while (nums[left] % 2 == 1) {
left++;
}
// 右边是偶数的话就自减,直到找到后面的奇数
while (nums[right] % 2 == 0) {
right--;
}
if (left < right && nums[left] % 2 == 0 && nums[right] % 2 == 1) {
int tmp = nums[left];
nums[left] = nums[right];
nums[right] = tmp;
}
}
return;
}
539. Move Zeroes
把一个数组的所有0移动到末尾,且不改变原数组非零元素之间的相对位置。要求in place进行。
第一种方法是双指针移动法,left和right指针一开始都置为起始元素,然后让right去遍历寻找非0元素。找到了非0元素后,就把两个指针的元素互换,使得后面的right指向的非0元素可以把left指向的0给替换掉。
交换完后,此时的left应该指向下一个0元素,所以left要自增。同时外层循环让right自增,继续去寻找下一个非0元素:
public void moveZeroes(int[] nums) {
int left = 0, right = 0;
while (right < nums.length) {
// right指针找到一个非0的数,就与left指针交换
if (nums[right] != 0) {
int tmp = nums[left];
nums[left] = nums[right];
nums[right] = tmp;
// 交换后,left自增
left++;
}
right++;
}
return;
}
从前往后遍历数组,遇到一个非0数组,就往pos那个位置给填上,填完后pos就自增。然后外层循环的index也要自增。最后把pos之后的所有非0元素置为0:
public void moveZeroes(int[] nums) {
int pos = 0, index = 0;
while (index < nums.length) {
if (nums[index] != 0) {
nums[pos++] = nums[index];
}
index++;
}
while (pos < nums.length) {
nums[pos] = 0;
pos++;
}
}
172. Remove Element
给定一个数组和一个整数elem,要求吧数组中所有等于elem的元素都删除,并返回新数组的长度。比如:Given an array [0,4,4,0,0,2,4,4], value=4 return 4 and front four elements of the array is [0,0,0,2]
这道题的解法和上道题的方法二是一模一样的。双指针压缩法,把所有不等于elem的元素往前压缩:
public int removeElement(int[] A, int elem) {
int pos = 0;
for (int i = 0; i < A.length; i++) {
if (A[i] != elem) {
A[pos++] = A[i];
}
}
return pos;
}
100. Remove Duplicates from Sorted Array
给定一个有序数组,要求去除其中重复的元素。与其说是删除重复元素,倒不如说是把unique的元素全部放到前面。
这道题跟上面两道题有异曲同工之妙,基本思路是一致的。双指针压缩法。我把所有unique的元素尽量往前面放。
左指针pos用于放置元素,右指针i用于从前往后扫描。当扫到一个跟pos不同的元素时,我就可以把它放到pos后面。直到扫描完最后一个元素,这个时候数组的前pos+1个元素就是不包含重复元素的原数组的压缩版了:
public int removeDuplicates(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int pos = 0;
for (int i = 0; i < nums.length; i++) {
if (nums[i] != nums[pos]) {
pos++;
nums[pos] = nums[i];
}
}
return pos + 1;
}
101. Remove Duplicates from Sorted Array II
跟上道题不同之处在于允许2次重复的出现。但是重复超过2次就不行了。基本思路还是一样的。
pre指针用于记录位置值以及放置unique元素,cur指针用于遍历,搜索与pre不同的元素。由于允许出现2次重复,所以需要一个变量count来统计重复次数。
因为数组是有序的,所以可以从前往后遍历的同时记录count。
如果cur和pre不同,则结束计数器(即count置为1),同时把后面那个跟pre不同的元素(即cur)移动到pre的后面。然后把pre指向下一个元素。
如果cur和pre相同,并且此时还可以继续计数,则统计重复次数(即count++)。并且把cur移动到pre的后面。然后把pre指向下一个元素。
外层循环则继续移动cur指针来遍历
public int removeDuplicates(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int pre = 0, count = 1;
for (int cur = 1; cur < nums.length; cur++) {
// 前后两元素不相等,
if (nums[cur] != nums[pre]) {
// 重复计数器重置为1,同时把cur移动到pre的后面,并同时更新pre
count = 1;
nums[++pre] = nums[cur];
} else {
// 前后两元素相等,并且此时还可以继续统计重复数
if (count < 2) {
// 统计重复次数,同时把cur移动到pre的后面,并同时更新pre
count++;
nums[++pre] = nums[cur];
}
}
}
return pre + 1;
}
387. The Smallest Difference
给定两个数组,要求在这两个数组里找到两个数的差,使得两数之差最小。最直观的方法是两层循环逐个去遍历,时间复杂度是O(N*N)。但是我们可以通过排序+双指针把时间复杂度降到O(N*logN)。先把两个数组排好序。然后同时从两个数组的起始位置往后扫描记录最小差。哪个数字小,就先移动那个数组的遍历指针,以减少两数之差的距离。
public int smallestDifference(int[] A, int[] B) {
if (A == null || B == null || A.length == 0 || B.length == 0) {
return 0;
}
Arrays.sort(A);
Arrays.sort(B);
int min = Integer.MAX_VALUE;
int i = 0, j = 0;
while (i < A.length && j < B.length) {
min = Math.min(min, Math.abs(A[i] - B[j]));
if (A[i] < B[j]) {
i++;
} else {
j++;
}
}
return min;
}
给定一个代表容器边界高度的数组,求最大的装水量。
容器装水量的多少取决于最短的那块木板的高度。这道题用双指针法,left和right指针从两边往中间遍历,left和right中较矮的那个就是当前木桶的高度,计算当前木桶容量。
然后比较left和right谁高些,就把矮的那个指针往高指针的方向挪动一位,直到两个指针相遇为止。中途用max逐个比较记录最大木桶容量。
public int maxArea(int[] heights) {
int left = 0, right = heights.length - 1;
int max = 0;
while (left < right) {
int area = (right - left) * Math.min(heights[left], heights[right]);
max = Math.max(area, max);
if (heights[left] < heights[right]) {
left++;
} else {
right--;
}
}
return max;
}
363. Trapping Rain Water
我们来看一种只需要遍历一次即可的解法,这个算法需要left和right两个指针分别指向数组的首尾位置,从两边向中间扫描,在当前两指针确定的范围内,先比较两头找出较小值,如果较小值是left指向的值,则从左向右扫描。如果较小值是right指向的值,则从右向左扫描。
若遇到的值比较小值小,则将差值存入结果,如遇到的值大,则重新确定新的窗口范围,以此类推直至left和right指针重合:
public int trapRainWater(int[] arr) {
int left = 0, right = arr.length - 1, res = 0;
while (left < right) {
// 先从两边指针中较小的那个开始
int height = Math.min(arr[left], arr[right]);
if (arr[left] == height) {
left++;
while (left < right && arr[left] < height) {
res += (height - arr[left]);
left++;
}
} else {
right--;
while (right > left && arr[right] < height) {
res += (height - arr[right]);
right--;
}
}
}
return res;
}
以下为滑动窗口类型题目:
406. Minimum Size Subarray Sum
这道题给定了我们一个数字,让我们求子数组之和大于等于给定值的最小长度。
我们需要定义两个指针left和right,分别记录子数组的左右的边界位置,然后我们让right向右移,直到子数组和大于等于给定值或者right达到数组末尾。
然后我们更新最短距离,并且将left像右移一位,然后把left左边的那个元素从sum中减去。
然后重复上面的步骤,直到right到达末尾。
public int minimumSize(int[] nums, int s) {
if (nums == null || nums.length == 0) {
return -1;
}
int left = 0, right = 0;
int minLength = Integer.MAX_VALUE;
int sum = 0;
for (left = 0; left < nums.length; left++) {
while (right < nums.length && sum < s) {
sum += nums[right];
right++;
}
if (sum >= s) {
minLength = Math.min(minLength, right - left);
}
sum -= nums[left];
}
return (minLength != Integer.MAX_VALUE) ? minLength : -1;
}
384. Longest Substring Without Repeating Characters
给定一个字符串,求它的最长的不包含重复字符的子串的长度。也是一样的套模板。用HashSet记录是否有重复的字符,从左往右扫描,不断更新最长长度。然后如果出现了重复,就把滑动窗口最左边那个字符从Set中去除,然后继续把滑动窗口往右移位。
public int lengthOfLongestSubstring(String s) {
int left = 0, right = 0;
int maxLen = 0;
HashSet set = new HashSet();
for (left = 0; left < s.length(); left++) {
while (right < s.length() && !set.contains(s.charAt(right))) {
set.add(s.charAt(right));
maxLen = Math.max(right - left + 1, maxLen);
right++;
}
set.remove(s.charAt(left));
}
return maxLen;
}
public int lengthOfLongestSubstringKDistinct(String s, int k) {
int left = 0, right = 0;
HashMap map = new HashMap();
int maxLen = 0;
if (k == 0) {
return 0;
}
for (left = 0; left < s.length(); left++) {
while (right < s.length() && map.size() <= k) {
char c = s.charAt(right);
if (map.containsKey(c)) {
map.put(c, map.get(c) + 1);
} else {
if (map.size() == k) {
break;
} else {
map.put(c, 1);
}
}
maxLen = Math.max(right - left + 1, maxLen);
right++;
}
if (map.size() == k) {
char c = s.charAt(left);
if (map.get(c) > 1) {
map.put(c, map.get(c) - 1);
} else {
map.remove(c);
}
}
}
return maxLen;
}
604. Window Sum
求滑动窗口中每个窗口的和,窗口长度为K,从数组的开始滑动到数组的末尾。标记left和right指针指向窗口的开始和结束。先求出第一个滑动窗口的和,然后模拟操作,减去left对应的值,然后把窗口往右移动一位,计算下一个和:
public int[] winSum(int[] nums, int k) {
if (nums == null || nums.length == 0 || k > nums.length) {
return new int[0];
}
int listLength = nums.length - k + 1;
int[] res = new int[listLength];
int index = 0;
// Calculate the first K sum
int sum = 0;
int left = 0, right = k - 1;
for (int i = left; i <= right; i++) {
sum += nums[i];
}
res[index++] = sum;
// Calculate the subsequent sum by reusing existing sum
while (index < listLength) {
if (right + 1 < nums.length) {
sum -= nums[left];
sum += nums[right + 1];
left++;
right++;
res[index++] = sum;
}
}
return res;
}
给定一个源数组和一个目标数组,都是由字母组成的字符串数组。要求在源数组中找到一个最短的窗口,使得窗口内的子串包含所有目标数组中的字母。这也是一道滑动窗口题,首先把目标数组里的字母都存到一个hashmap里。然后标记left和right数组来从左往右扫描源数组,扫描的过程中不断更新符合要求的最短长度和最短窗口子串。这样,扫描结束后,得到的就是符合要求的最短窗口子串了。
public class Solution {
/**
* @param source: A string
* @param target: A string
* @return: A string denote the minimum window
* Return "" if there is no such a string
*/
public boolean isValid(int[] sourceMap, int[] targetMap) {
for (int i = 0; i < sourceMap.length; i++) {
if (targetMap[i] > sourceMap[i]) {
return false;
}
}
return true;
}
public void initTarget(int[] targetMap, String target) {
for (int i = 0; i < target.length(); i++) {
targetMap[target.charAt(i)]++;
}
}
public String minWindow(String source, String target) {
int minLength = source.length();
String minWindow = "";
int[] targetMap = new int[256];
int[] sourceMap = new int[256];
initTarget(targetMap, target);
int left = 0, right = 0;
for (left = 0; left < source.length(); left++) {
while (!isValid(sourceMap, targetMap) && right < source.length()) {
sourceMap[source.charAt(right)]++;
right++;
}
if (isValid(sourceMap, targetMap)) {
if (right - left <= minLength) {
minLength = right - left;
minWindow = source.substring(left, right);
}
}
sourceMap[source.charAt(left)]--;
}
return minWindow;
}
}
56. Two Sum
给定一个目标数字target,要求在数组中找到2个数,使得2数之和等于target。brute force的方法是暴力2层循环O(n^2)。稍微更好的解法是先排序使得数组有序O(nlogn),然后再用左右两个指针从两边往中间扫描,当扫描到的2数之和等于target就返回。更好一点的方法是利用hashmap。目标是要找到a+b=target中的a和b,从左往右扫描数组,每次都把target-a加进map中,同时每次都在map中寻找看target-a存不存在。HashMap的key是target-a, value是a的下标。这样如果下一个数字在map中的key集合里存在的话,我就可以直接把map中的value拿出来,并且把当前指针所在的下标拿出来返回了。
public int[] bruteforce(int[] nums, int target) {
int[] res = new int[]{-1, -1};
for (int i = 0; i < nums.length - 1; i++) {
res[0] = i + 1;
int nextTarget = target - nums[i];
for (int j = i + 1; j < nums.length; j++) {
if (nums[j] == nextTarget) {
res[1] = j + 1;
return res;
}
}
}
return res;
}
public int[] twoSum(int[] nums, int target) {
HashMap map = new HashMap();
int[] res = new int[]{-1, -1};
for (int i = 0; i < nums.length; i++) {
if (map.containsKey(nums[i])) {
res[0] = map.get(nums[i]);
res[1] = i + 1;
break;
} else {
map.put(target - nums[i], i + 1);
}
}
return res;
}
跟上题类似,区别就在于数组是有序的了。这样就可以用双指针法来解决了。左右双指针往中间扫描,如果比target小,左指针就往右移动。若比target大,右指针就往左移动。
public int[] twoSum(int[] nums, int target) {
int[] res = new int[]{-1, -1};
int left = 0, right = nums.length - 1;
while (left < right) {
if (nums[left] + nums[right] == target) {
res[0] = left + 1;
res[1] = right + 1;
break;
} else if (nums[left] + nums[right] > target) {
right--;
} else {
left++;
}
}
return res;
}
给一个数组和一个数字target,要求出数组中有多少对pair的和是大于target。也可以用双指针来解决。先把数组排序好。然后当前两数之和比target小,就继续往右移动左指针。如果当前两数之和比target大,就说明满足条件了,就可以统计一下当前区间有多少对,然后把右指针往左挪一位。然后继续循环:
public int twoSum2(int[] nums, int target) {
Arrays.sort(nums);
int left = 0, right = nums.length - 1;
int res = 0;
while (left < right) {
if (nums[left] + nums[right] <= target) {
left++;
} else {
res += right - left;
right--;
}
}
return res;
}
跟上题类似,只是把条件从大于换成了小于等于。
public int twoSum5(int[] nums, int target) {
Arrays.sort(nums);
int left = 0, right = nums.length - 1;
int res = 0;
while (left < right) {
if (nums[left] + nums[right] > target) {
right--;
} else {
res += right - left;
left++;
}
}
return res;
}
给定一个数组,从中找到3个数使之能组成三角形的三边,问能找到多少组不同的这样的三边。暴力解法就是三层循环O(n^3)。其实这道题跟上面的几道题也是类似的,可以通过先排序后双指针来解决。排序好后,数组的第N个数就是target,前N-1个数构成了一个新的数组。这样就把它转换成了2 sum问题了。
public int triangleCount(int S[]) {
int res = 0;
Arrays.sort(S);
for (int i = S.length - 1; i >= 2; i--) {
int left = 0, right = i - 1;
int target = S[i];
while (left < right) {
if (S[left] + S[right] <= target) {
left++;
} else {
res += right - left;
right--;
}
}
}
return res;
}
给定一个数组(可能有重复元素),以及一个target数字。问有多少组独特的两个数之和等于target。这道题也是可以用先排序后双指针法来解决的,只不过这里在找到了一个符合条件的2个数之后,需要去重:
public int twoSum6(int[] nums, int target) {
int res = 0;
int left = 0, right = nums.length - 1;
Arrays.sort(nums);
while (left < right) {
if (nums[left] + nums[right] == target) {
res++;
while (left + 1 < right && nums[left + 1] == nums[left]) {
left++;
}
while (left < right - 1 && nums[right - 1] == nums[right]) {
right--;
}
left++;
right--;
} else if (nums[left] + nums[right] < target) {
left++;
} else {
right--;
}
}
return res;
}
跟56那道题特别像,只不过把加法换成了减法,减法的话,就需要2个HashMap了:
public int[] twoSum7(int[] nums, int target) {
HashMap map1 = new HashMap();
HashMap map2 = new HashMap();
int[] res = new int[]{-1, -1};
for (int i = 0; i < nums.length; i++) {
if (map1.containsKey(nums[i])) {
res[0] = map1.get(nums[i]);
res[1] = i + 1;
return res;
} else {
map1.put(nums[i] - target, i + 1);
}
if (map2.containsKey(nums[i])) {
res[0] = map2.get(nums[i]);
res[1] = i + 1;
return res;
} else {
map2.put(nums[i] + target, i + 1);
}
}
return res;
}
要求设计一个two sum的数据结构,要求支持add和find操作,每次add都可以往数据结构里面添加一个数字,然后find操作就是找数据结构里有没有2个数的和等于那个target。下面的这个设计是利用HashMap,这样的话,add操作就是O(1),find操作就是O(n)。但是注意要用LinkedHashMap,因为它比HashMap遍历的速度要更快。如果要支持add操作O(N),find操作O(1)的话,那就要利用2个HashMap了,一个用于存储加进来的数据,一个用于把每次加进来的数据都求一遍和,做一个sum的集合。然后find的时候就看那个target是否在sum集合中存在就可以了。
public class TwoSum {
private LinkedHashMap map;
public TwoSum() {
map = new LinkedHashMap();
}
// Add the number to an internal data structure.
public void add(int number) {
if (map.containsKey(number)) {
map.put(number, map.get(number) + 1);
} else {
map.put(number, 1);
}
}
// Find if there exists any pair of numbers which sum is equal to the value.
public boolean find(int value) {
for (Integer num: map.keySet()) {
int a = num, b = value - num;
if (a == b && map.get(a) >= 2) {
return true;
}
if (a != b && map.containsKey(b)) {
return true;
}
}
return false;
}
}