LintCode双指针/滑动窗口/Two Sum类型题总结

双指针题算是数组类型题目的一个子模块了。

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数向前尽可能的压缩,最后把没压缩的那部分全置0就行了。比如103040,先压缩成134,剩余的3为全置为0。过程中需要一个指针记录压缩到的位置。

从前往后遍历数组,遇到一个非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;
    }

383. Container With Most Water

给定一个代表容器边界高度的数组,求最大的装水量。

容器装水量的多少取决于最短的那块木板的高度。这道题用双指针法,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;
    }


386. Longest Substring with At Most K Distinct Characters
给定一个字符串,求它的最长子串,这个子串最多只能有K个不同的字符。跟上题类似,只不过要把HashSet换成HashMap了。

    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;
    }

32. Minimum Window Substring

给定一个源数组和一个目标数组,都是由字母组成的字符串数组。要求在源数组中找到一个最短的窗口,使得窗口内的子串包含所有目标数组中的字母。这也是一道滑动窗口题,首先把目标数组里的字母都存到一个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;
    }
}

以下为Two Sum类型题目:

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;
    }

608. Two Sum - Input array is sorted

跟上题类似,区别就在于数组是有序的了。这样就可以用双指针法来解决了。左右双指针往中间扫描,如果比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;
    }

443. Two Sum - Greater than target

给一个数组和一个数字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;
    }

609. Two Sum - Less than or equal to target

跟上题类似,只是把条件从大于换成了小于等于。

    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;
    }

382. Triangle Count

给定一个数组,从中找到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;
    }

587. Two Sum - Unique pairs

给定一个数组(可能有重复元素),以及一个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;
    }

610. Two Sum - Difference equals to target

跟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;
    }

607. Two Sum - Data structure design

要求设计一个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;
    }
}



你可能感兴趣的:(Algorithm)