算法专题01 - 双指针

"two point sulution"

Posted by Lin on October 1, 2020

本文罗列下双指针技巧 及常规的题目:

  • 快慢指针: 判断链表是否有环的问题(141题),查找重复数字(287题目);
  • 左右指针: 二分查找;
  • 滑动窗口算法;
  • k-sum 问题

题目实战

快慢指针相关

  1. 判断链表中是否有环

用两个指针,一个跑得快,一个跑得慢。如果不含有环,跑得快的那个指针最终会遇到 null,说明链表不含环;如果含有环,快指针最终会超慢指针一圈,和慢指针相遇,说明链表含有环。

class Solution {
    boolean hasCycle(ListNode head) {
        ListNode fast, slow;
        fast = slow = head;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;

            if (fast == slow) return true;
        }
        return false;
    }
}
  1. 已知链表中含有环,返回这个环的起始位置

当快慢指针相遇时,让其中任一个指针指向头节点,然后让它俩以相同速度前进,再次相遇时所在的节点位置就是环开始的位置。

为什么? 第一次相遇时,假设慢指针 slow 走了 k 步,那么快指针 fast 一定走了 2k 步,也就是说比 slow 多走了 k 步(也就是环的长度)。设相遇点距环的起点的距离为 m,那么环的起点距头结点 head 的距离为 k - m,也就是说如果从 head 前进 k - m 步就能到达环起点。就是说 如果从相遇点继续前进 k - m 步,也恰好到达环起点。

class Solution {
    ListNode detectCycle(ListNode head) {
        ListNode fast, slow;
        fast = slow = head;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
            if (fast == slow) break;
        }
        // 上面的代码类似 hasCycle 函数
        slow = head;
        while (slow != fast) {
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }
}
  1. 寻找链表的中点

让快指针一次前进两步,慢指针一次前进一步,当快指针到达链表尽头时,慢指针就处于链表的中间位置。

class Solution {
    ListNode main(ListNode head) {
        ListNode fast, slow;
        fast = slow = head;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        // slow 就在中间位置
        return slow;
    }
}
  1. 寻找链表的倒数第 k 个元素

使用快慢指针,让快指针先走 k 步,然后快慢指针开始同速前进。这样当快指针走到链表末尾 null 时,慢指针所在的位置就是倒数第 k 个链表节点。

class Solution {
    ListNode main(ListNode head) {
        ListNode slow, fast;
        slow = fast = head;
        while (k-- > 0)
            fast = fast.next;

        while (fast != null) {
            slow = slow.next;
            fast = fast.next;
        }
        return slow;
    }
}

滑动窗口

这个算法技巧的思路非常简单,就是维护一个窗口,不断滑动,然后更新答案么。

大致逻辑是这样:

int left = 0, right = 0;

while (right < s.size()) {
    // 增大窗口
    window.add(s[right]);
    right++;

    while (window needs shrink) {
        // 缩小窗口
        window.remove(s[left]);
        left++;
    }
}

代码模板:

/* 滑动窗口算法框架 */
class tmp {
    void slidingWindow(string s, string t) {
        unordered_map<char, int> need, window;
        for (char c : t) need[c]++;

        int left = 0, right = 0;
        int valid = 0;
        while (right < s.size()) {
            // c 是将移入窗口的字符
            char c = s[right];
            // 右移窗口
            right++;
            // 进行窗口内数据的一系列更新
            //...

            /*** debug 输出的位置 ***/
            printf("window: [%d, %d)\n", left, right);
            /********************/

            // 判断左侧窗口是否要收缩
            while (window needs shrink) {
                // d 是将移出窗口的字符
                char d = s[left];
                // 左移窗口
                left++;
                // 进行窗口内数据的一系列更新
                //...
            }
        }
    }
}
  1. 无重复字符的最长子串(leetcode#3)

右指针前进,当发现窗口内有重复字符时,左指针前进,直到无重复字符;当窗口内没有重复字符时,更新结果;

public class Solution {
    public int lengthOfLongestSubstring(String s) {
        int res, left, right;
        res = left = right = 0;
        Set<Character> set = new HashSet<>();
        while (right < s.length()) {
            Character ch = s.charAt(right);
            while (set.contains(ch)) {
                set.remove(s.charAt(left));
                left++;
            }
            set.add(ch);
            res = Math.max(res, right - left + 1);
            right++;
        }
        return res;
    }
}

左右指针

  1. 盛最多水的容器(leetcode#11)

初始化左右指针为 左指针指向0,右指针指向末尾,逐渐向中间的高度大于右指针时,移动右指针;反之,移动左指针。同时更新结果。

public class Solution {
    public int maxArea(int[] height) {
        int maxArea = 0;
        for (int i = 0, j = height.length - 1; i < j;) {
            int tmpArea = (j - i) * Math.min(height[i], height[j]);
            if (tmpArea > maxArea) {
                maxArea = tmpArea;
            }
            if (height[i] > height[j]) {
                j--;
            } else {
                i++;
            }
        }
        return maxArea;
    }
}

K-SUM问题

问题描述: Given an array of integers, return all unique combinations of k numbers such that they add up to a specific target. (Assuming k < number of elements in the given integer array)

这是面试常见题目类型,首先先将数据排序: Arrays.sort(nums);

方法定义为: List<List<Integer>> kSum(int[] nums, int k, int target)

先来看看 k=1k=2的场景:

k=1

if (k == 1) {
  List<List<Integer>> res = new ArrayList<>();
  if (Arrays.binarySearch(nums, target) >= 0) {
    res.add(Collections.singletonList(target));
    return res;
  }
}

k=2

使用左右指针技巧,定义 left指针指向头 right指针指向尾,并不断往中间靠拢,计算 nums[left] + nums[j]target:

  • nums[left] + nums[right] == target,恭喜 找到一个组合,加入到结果集后,同时移动两个指针 ++left; --right;
  • nums[left] + nums[right] > target,移动 --right(需 找一个更小的和);
  • nums[left] + nums[right] < target,移动 ++left
  • 直至 left >= right, 结束。

伪代码:

List<List<Integer>> res = new ArrayList<>();
int left = 0, right = nums.length - 1;
while (left < right) {
  int sum = nums[left] + nums[right];
  if (sum == target) {
    res.add(new ArrayList<>(Arrays.asList(nums[left], nums[right])));
    # de-dup
    while (left < right && nums[left] == nums[left + 1]) ++left;
    while (left < right && nums[right] == nums[right - 1]) --right;
    ++left;
    --right;
  } else if (sum < target) {
    ++left;
  } else {
    --right;
  }
}
return res;

k>=3

k=3时,我们可以降级到 k=2问题: 我们可以遍历数组,来求得 nums[left] + nums[right] = target - nums[index]

所以,K-SUM问题可以通过递归(通过求(K-1)问题)求解。

k-1函数定义: private List<List<Integer>> kSumHelper(int[] nums, int k, int target, int beginIdx)

那么,3-sum 的伪代码为:

List<List<Integer>> res = new ArrayList<>();
for (int i = 0; i <= nums.length - 3; i++) {
  // Get 2-Sum result for each element
  List<List<Integer>> subResults = kSumHelper(nums, 2, target - nums[i], i + 1);
  for (List<Integer> list : sub) {
    list.add(0, nums[i]);
  }
res.addAll(sub);
return res;

最后,完整的代码为:

/**
 * @author lin
 * @version v 0.1 2020/11/27
 **/
public class Ksum {
    /**
     * Find all k elements groups that adding up to given target.
     *
     * @param nums   input array
     * @param k      k
     * @param target target value
     * @return all groups
     */
    public List<List<Integer>> kSum(int[] nums, int k, int target) {
        Arrays.sort(nums);
        if (k <= 1) {
            List<List<Integer>> res = new ArrayList<>();
            if (k == 1 && Arrays.binarySearch(nums, target) >= 0) {
                res.add(Collections.singletonList(target));
            }
            return res;
        }
        return kSum(nums, k, target, 0);
    }

    /**
     * Helper function for K-sum.
     *
     * @param nums     input array
     * @param k        k
     * @param target   target value
     * @param beginIdx begin index
     * @return all groups of k elements from beginIdx that adding up to target.
     */
    private List<List<Integer>> kSum(int[] nums, int k, int target, int beginIdx) {
        int len = nums.length;
        if (k == 2) {
            List<List<Integer>> res = new ArrayList<>();
            int left = beginIdx, right = len - 1;
            while (left < right) {
                int sum = nums[left] + nums[right];
                if (sum == target) {
                    res.add(new ArrayList<>(Arrays.asList(nums[left], nums[right])));
                    while (left < right && nums[left] == nums[left + 1]) {
                        ++left;
                    }
                    while (left < right && nums[right] == nums[right - 1]) {
                        --right;
                    }
                    ++left;
                    --right;
                } else if (sum < target) {
                    ++left;
                } else {
                    --right;
                }
            }
            return res;
        }
        List<List<Integer>> res = new ArrayList<>();
        for (int i = beginIdx; i <= len - k; i++) {
            if (i > beginIdx && nums[i] == nums[i - 1] || nums[i] + (k - 1) * nums[len - 1] < target) {
                continue;
            }
            if (nums[i] + (k - 1) * nums[i + 1] > target) {
                break;
            }
            List<List<Integer>> sub = kSum(nums, k - 1, target - nums[i], i + 1);
            for (List<Integer> list : sub) {
                list.add(0, nums[i]);
            }
            res.addAll(sub);
        }
        return res;
    }
}

leetcode 的双指针专题

参考资料: