双指针算法是一种高效处理线性结构问题的经典算法策略。本文从一道例题开始,详细分析双指针算法的设计与实现,给出双指针算法的一些特点,从而帮助读者识别使用双指针算法的问题,并顺利使用双指针算法解决问题。
问题描述
小U有一条彩带,每一厘米都被涂上了一种颜色。她需要从彩带上截取一段,使得这段彩带中的颜色种类不超过K种。为了满足任务要求,小U希望截取的彩带段尽可能长。现在你需要帮助小U计算出满足条件的最长彩带段的长度。
例如:彩带的长度为8,每个颜色的编号是 1, 2, 3, 2, 1, 4, 5, 1,小U最多允许3种不同的颜色。此时,最长的满足条件的彩带段是 1, 2, 3, 2, 1,长度为 5。
测试样例
样例1:
输入:
N = 8, K = 3, a = [1, 2, 3, 2, 1, 4, 5, 1]
输出:5
样例2:
输入:
N = 7, K = 2, a = [1, 2, 2, 2, 1, 1, 3]
输出:6
样例3:
输入:
N = 6, K = 4, a = [4, 5, 4, 6, 7, 8]
输出:5
问题分析
首先我们发现,问题中提到“截取一段彩带”,也就是说这代表着一段连续的区间。从而可以得出,我们需要从彩带的左端到右端寻找满足条件的最长连续区间。我们通常可以使用两个端点表示一段连续区间,用两个变量 l(左端点)和 r(右端点)来跟踪区间范围。
对于每一个可能的区间(用两个嵌套循环暴力枚举),统计区间内的颜色种类是否不超过K种。如果满足条件,则更新最长长度。但显然这样做的时间复杂度为O(N^2),只能解决N在1000内的问题。有没有什么方法能在线性时间内解决问题呢?
很明显的,我们每次枚举区间端点时,每个元素都重复判断了很多次。有没有什么办法能够让每个元素只被判断一次(或常数次)?而双指针算法正是用于解决这样的问题的。
解法设计
我们设左右端点为两个可以移动的指针,初值均设为0(彩带开头)。每次枚举,我们把右指针 r 向右移动一格,并将新颜色加入窗口。如果当前区间内的颜色种类不超过K,则当前区间合法,计入答案。当加入新颜色后,窗口的颜色种类超过 K,时,我们让左指针 l 向右移动(删除区间内的颜色),直到窗口合法。我们可以使用一个哈希表去记录窗口内每种颜色的出现频次并通过哈希表的键值对数量来跟踪当前窗口内的颜色种类。显然,这种方法每个颜色只会进入和离开区间各一次,故该解法的时间复杂度是O(N)的,也就是线性。
解法实现
以下是基于上述解法设计的 Java 代码实现:
public static int solution(int N, int K, List<Integer> a) {
// write code here
int l = 0, r = 0;
int cnt = 1;
int ans = 1;
HashMap<Integer, Integer> mp = new HashMap<>();
mp.put(a.get(0), 1);
while (r < N - 1) {
r++;
if (!mp.containsKey(a.get(r))) {
mp.put(a.get(r), 1);
cnt++;
} else {
mp.put(a.get(r), mp.get(a.get(r)) + 1);
}
// System.out.println(cnt);
while (cnt > K) {
mp.put(a.get(l), mp.get(a.get(l)) - 1);
if (mp.get(a.get(l)) == 0) {
mp.remove(a.get(l));
cnt--;
}
l++;
}
ans = Math.max(ans, r - l + 1);
}
return ans;
}
}
算法特点
从本题的分析与实现中,我们可以总结双指针算法的几个共性特点:
-
使用两个指针控制窗口范围:双指针一般维护一个区间,区间的左边界和右边界分别由两个指针控制。通过移动指针,可以动态调整区间的范围以满足问题的约束。
-
对区间内的状态进行实时统计:使用哈希表、计数器或其它数据结构辅助记录窗口内的状态。例如,本题中通过哈希表记录颜色频次并维护当前颜色种类数。
-
区间移动操作对应区间状态维护:右指针向右移动扩展窗口时,增加元素的状态;左指针向右移动收缩窗口时,移除元素的状态。