双指针问题
算法解释
-
双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多个数组的多个指针。
-
若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的区域即为当前的窗口),经常用于区间搜索。
-
若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是排好序的。
两数之和相关问题
167.Sum II - Input array is sorted (Easy)
的整数数组里找到两个数,使它们的和为给定值。已知有且只有一对解
Input: numbers = [2,7,11,15], target = 9
Output: [1,2]
已经知道数组为一个有序数组,分别在首尾处设置指针Lo、hi,不断的计算lo与hi处数值的和,判断是否相等
/**
* 在一个增序的整数数组里找到两个数,使它们的和为给定值。已知有且只有一对解。
* @anthor shkstart
* @create 2020-08-20 20:17
*/
public class Two_Sum {
@Test
public void test1() {
int[] S = {2,3,4};
int target = 6;
System.out.println(Arrays.toString(twoSum1(S,target)));
}
public int[] twoSum(int[] numbers, int target) {
int lo = 0;
int hi = numbers.length - 1;
int sum = 0;
while (lo < hi){
sum = numbers[lo] + numbers[hi];
if (sum == target) break;
if (sum > target){
hi--;
} else{
lo++;
}
}
return new int[]{lo+1,hi+1};
}
/**
* 二分查找
*/
public int[] twoSum1(int[] numbers, int target) {
for (int i = 0; i < numbers.length - 1; ++i) {
int lo = i + 1;
int hi = numbers.length;
int j = find(numbers,target - numbers[i],lo,hi);
if (j != (-1)){
return new int[]{i+1,j+1};
}
}
return new int[]{-1, -1};
}
public int find(int[] S,int e,int lo,int hi){
while (1 < hi - lo){
int mi = (lo + hi) >>1;
//取得两者的中点
if (e < S[mi]){
//mi处值大于e
hi = mi;
//令hi = mi
} else {
//小于e
lo = mi;
//令lo = mi
}
//这里没有考虑相等的情况,把相等放在了右侧区间
}
if(S[lo] == e){
return lo;
} else {
return -1;
}
}
public int find2(int[] S,int e,int lo,int hi){
while (lo < hi){
int n = 0;
while (hi - lo > fib(n) - 1){
//计算合适为恰好的fib(n) - 1 >=
n++;
}
int mi = lo + fib(n-1) - 1;
//前后的子向量的长度为fib(n-1) - 1和fib(n-2) -1
if (e < S[mi]){
hi = mi;
} else if (S[mi] < e){
lo = mi + 1;
} else {
return mi;
}
}
return -1;
}
public int fib(int n){
int f = 0;
//从0开始,
int g = 1;
//斐波那契数列
while(0 < n--){
g = g + f;
//滚动常数
f = g - f;
//先计算后一个数,前一个用相减的方式得到
}
return g;
}
/**
* 指针加上二分查找
*/
public int[] twoSum2(int[] numbers, int target) {
int lo = 0;
int hi = find(numbers,target-numbers[0],0,numbers.length);
int sum = 0;
while (lo < hi){
sum = numbers[lo] + numbers[hi];
if (sum == target) break;
if (sum > target){
hi--;
} else{
lo++;
}
}
return new int[]{lo+1,hi+1};
}
}
平方数之和(简单)167.
题目:给定一个非负整数 c ,你要判断是否存在两个整数 a 和 b,使得 a2 + b2 = c。
一、双指针方法
满足a2+b2 = d2 = c,这是一个中心在原点的圆;在取得a、b时其实关于Y = X是对称的,因此循环的范围可以缩减至根号2分之一;分别在首尾设置指针,判断是否满足条件
/**
* 与之前两数之和类似,不过改成平方
* 时间复杂度O(n)空间复杂度O(1)
* @param c
* @return
*/
public Boolean judgeSquareSum(int c) {
int d = (int) ((Math.sqrt(c)) * Math.sqrt(0.5));
int lo = 0;
int hi = d;
int sum = 0;
while (lo < hi){
sum = lo*lo + hi*hi;
if (sum == c) return true;
if (sum > c){
hi--;
} else{
lo++;
}
}
return false;
}
/**
* 可以尝试二分查找
* i从1到i*i < sum遍历,对每个n =(sum-i*i)进行分析
* 分析方法(a,b,n)
* 每次取中间值,判断其平方是否为n
* 若大于n,则b = 中间值 - 1
* 若小于n,则a = 中间值 + 1
* ==n,则输出true
* 终止于s>e
* 结果时超出了时间限制
*/
public Boolean judgeSquareSum1(int c) {
for (int i = 0;i*i <= c;i++){
int d = c - i*i;
if (find(i,d,d)){
return true;
}
}
return false;
}
public Boolean find(long lo,long hi,int e){
while (1 < hi - lo){
long mi = (lo + hi) >>1;
//取得两者的中点
if (e < mi*mi){
//mi处值大于e
hi = mi;
//令hi = mi
} else {
//小于e
lo = mi;
//令lo = mi
}
//这里没有考虑相等的情况,把相等放在了右侧区间
}
if(lo*lo == e || hi*hi == e){
return true;
} else {
return false;
}
}
/**
* 直接判断存不存在
*/
public Boolean judgeSquareSum2(int c) {
for (long a = 0; a * a <= c; a++) {
double b = Math.sqrt(c - a * a);
if (b == (int) b)
return true;
}
return false;
}
二、费马定理
费马定理:一个非负整数 cc 能够表示为两个整数的平方和,当且仅当 cc 的所有形如4k+34k+3 的质因子的幂次均为偶数;先对数进行因式分解,得到转换为a(n1)+a(n2) + .....然后判断是否含有形为4k + 3 且幂次为奇数的因子
/**
* 费马定理:一个非负整数 cc 能够表示为两个整数的平方和,当且仅当 cc 的所有形如 4k+34k+3 的质因子的幂次均为偶数
*/
public Boolean judgeSquareSum3(int c) {
for (int i = 2; i * i <= c; i++) {
int count = 0;
if (c % i == 0) {
//简单的因式分解
while (c % i == 0) {
count++;
c /= i;
}
/** 到此 c被拆成当前i的n次方 乘 某个无法被当前i整除的数(但是该数进过不断循环总归会变成别的质数的乘积) */
/** 费马平方和定理 */
/** 其他形式的无所谓 只有形为4k + 3 且幂次为奇数的过不了 */
if (i % 4 == 3 && count % 2 != 0)
return false;
}
}
return c % 4 != 3;
}
平方数之和(简单)680.
题目:给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串
一、双指针法
可以把这个题化为数学上的两个蚂蚁在一条轴线上相向运动,在蚂蚁相遇前的见到的每个数字都相等即是。一个回文字符
一旦数字不相等,让两只蚂蚁分别忘掉这个数字,继续前行,当忘掉的次数大于1时,说明这个字符串无法成为一个回文字符串
public Boolean validPalindrome(String s) {
int low = 0, high = s.length() - 1;
while (low < high) {
char c1 = s.charAt(low), c2 = s.charAt(high);
if (c1 == c2) {
low++;
high--;
} else {
Boolean flag1 = true, flag2 = true;
for (int i = low, j = high - 1; i < j; i++, j--) {
char c3 = s.charAt(i), c4 = s.charAt(j);
if (c3 != c4) {
flag1 = false;
break;
}
}
for (int i = low + 1, j = high; i < j; i++, j--) {
char c3 = s.charAt(i), c4 = s.charAt(j);
if (c3 != c4) {
flag2 = false;
break;
}
}
return flag1 || flag2;
}
}
return true;
}
归并两个数组相关问题
合并两个有序数组88.
题目:给定两个有序数组,把两个数组合并为一个。
Input: nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
Output: nums1 = [1,2,2,3,5,6]
一、双指针法
类似于两列蚂蚁排队进洞,其中每一列都按高低排好了,先在要求两队按从低到高依次进洞
两个指针分别指向蚂蚁的两队,比较后将较小的先入洞(即指针向后移动一位),当某一队全部进去后,剩下的蚂蚁高低是有序的直接入洞即可
public int[] merge2(int[] nums1, int m, int[] nums2, int n) {
int t = m-- + n-- - 1;
while (m >= 0 && n >= 0){
nums1[t--] = nums1[m] > nums2[n] ? nums1[m--] : nums2[n--];
}
while (n >= 0){
nums1[t--] = nums2[n--];
}
return nums1;
}
524.通过删除字母匹配到字典里最长单词524.
给定一个字符串和一个字符串字典,找到字典里面最长的字符串,该字符串可以通过删除给定字符串的某些字符来得到。如果答案不止一个,返回长度最长且字典顺序最小的字符串。如果答案不存在,则返回空字符串。
一、双指针法
首先应该将字典中的字符串进行排序,排序的比较方案是:长度长的排前面,当长度相等时字典顺序小的排前面
然后从字典中第一个开始,类似两列蚂蚁,指针分别指向列首,如果相等就向后移动一位,直至任意队列的末尾
不断判断短的一列是否到末尾,若到达就输出true。没有就进行下一个字符串的比较。
public String findLongestWord(String s, List<String> d) {
Collections.sort(d, new Comparator < String > () {
public int compare(String s1, String s2) {
return s2.length() != s1.length() ? s2.length() - s1.length() : s1.compareTo(s2);
}
//这个要多看几遍,还是分不清楚怎么弄
}
);
for (int n = 0;n < d.size();n++) {
String _str = d.get(n);
for (int i = 0,j = 0;i < s.length() && j < _str.length();i++){
if (s.charAt(i) == _str.charAt(j)) j++;
if (j == _str.length()){
return _str;
}
}
}
return "";
}
二、双指针不排序
每次判断时先判断包含与否,再比较长度,时间上可能有点损耗,但空间上不需要额外空间
public Boolean isSubsequence(String x, String y) {
int j = 0;
for (int i = 0; i < y.length() && j < x.length(); i++)
if (x.charAt(j) == y.charAt(i))
j++;
return j == x.length();
}
public String findLongestWord(String s, List < String > d) {
String max_str = "";
for (String str: d) {
if (isSubsequence(str, s)) {
if (str.length() > max_str.length() || (str.length() == max_str.length() && str.compareTo(max_str) < 0))
max_str = str;
}
}
return max_str;
}
快慢指针相关问题
142.环形链表二142.
题目:给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
说明:不允许修改给定的链表
Input: S = "ADOBECODEBANC", T = "ABC"
Output: "BANC"
一、快慢指针法
在数学上,两个点在同一个线路上比赛,如果在线路中有环路的话,跑的快的点最终会追上跑的慢的点,这里便可以判断是否有环路
如果有环路,在两点相遇时,将跑的快的点放回起点,然后两个点以相同的速度运动,再次相遇时的位置就是环路的起点(这里快速每次移动两个单位,慢速每次移动一个单位)
数学上,设直线长度为x,环路长度为y,第一次相遇时,有x+n1y+d = 2(x+d)(1)
将其放回原点后再次移动,相遇时,有x = y - d+n2y(2)
通过化简可知上面两个等式说等价的,即有公式一可得到公式二
public ListNode detectCycle1(ListNode head) {
ListNode slow = head;
ListNode fast = head;
do{
if (fast == null || fast.next == null) return null;
fast = fast.next.next;
slow = slow.next;
}
while (fast != slow);
fast = head;
while (fast != slow){
fast = fast.next;
slow = slow.next;
}
return fast;
}
二、hashset方法
对于数据储存的结构我们知道有collection和map两种体系
collection中的list接口储存有序、可重复的数据,包括Arraylist、linkedlist、vector;set接口,储存无序的不可重复的数据,包括hashset、linkedhashset、treeset。
Map接口用来储存一对(Key-Value)对的数据,有Hashmap、LinkedHashMap、TreeMap、Hashtable、Properties
这里我们选用不可重复的无序的数据储存类型,HashSet,点从原点处开始移动,判断Hashset中是否有相同的数据,没有的话将该点的数据储存进入Hashset中,有的话输出这个节点
public ListNode detectCycle(ListNode head) {
HashSet<ListNode> hashSet = new HashSet<>();
ListNode node = head;
while (node != null){
if (hashSet.contains(node)){
return node;
}
hashSet.add(node);
node = node.next;
}
return null;
}
340.需要会员还没看
滑动窗口相关问题
76覆盖串
给你一个字符串 S、一个字符串 T 。请你设计一种算法,可以在 O(n) 的时间复杂度内,从字符串 S 里面找出:包含 T 所有字符的最小子串。
输入:S = "ADOBECODEBANC", T = "ABC"
输出:"BANC"
一、滑动窗口方法
数学上,要在有序集合S中找到集合T中的元素,最好对T中的几个元素进行重点标记,以显示出与众不同,这里选择用向量的方式作标记,因为会有符合要求的多种情况,不要对集合T本身做改变
类似于在一列蚂蚁中找到比较特殊的几种蚂蚁,并要求蚂蚁的距离最短。在队列的头部设置两个指针lo和hi,找到三个元素前hi向后移动,找到三种蚂蚁后,判断并记录距离,然后进行Lo的移动。类似一个滑动的窗口,直到到达队列的尾部为止。
public String minWindow3(String s, String t) {
int[] chars = new int[128];
Boolean[] flag = new Boolean[128];
for (int i = 0;i < t.length();i++){
flag[t.charAt(i)] = true;
++chars[t.charAt(i)];
}
int l = 0;
int cnt = 0;
int _l = 0;
int len = s.length()+1;
//这里+1是灵魂,以免出现a与aa的情况
for (int r = 0;r < s.length();++r) {
if (flag[s.charAt(r)]) {
if (--chars[s.charAt(r)] >= 0) {
++cnt;
}
while (cnt == t.length()) {
if (r - l + 1 < len) {
_l = l;
len = r - l + 1;
}
if (flag[s.charAt(l)] && ++chars[s.charAt(l)] > 0) {
--cnt;
}
++l;
}
}
}
return len > s.length() ? "" : (String) s.substring(_l,_l+len);
}
HashMap的方法
Map<Character, Integer> ori = new HashMap<Character, Integer>();
Map<Character, Integer> cnt = new HashMap<Character, Integer>();
public String minWindow1(String s, String t) {
int tLen = t.length();
for (int i = 0; i < tLen; i++) {
char c = t.charAt(i);
ori.put(c, ori.getOrDefault(c, 0) + 1);
}
int l = 0, r = -1;
int len = Integer.MAX_VALUE, ansL = -1, ansR = -1;
int sLen = s.length();
while (r < sLen) {
++r;
if (r < sLen && ori.containsKey(s.charAt(r))) {
cnt.put(s.charAt(r), cnt.getOrDefault(s.charAt(r), 0) + 1);
}
while (check() && l <= r) {
if (r - l + 1 < len) {
len = r - l + 1;
ansL = l;
ansR = l + len;
}
if (ori.containsKey(s.charAt(l))) {
cnt.put(s.charAt(l), cnt.getOrDefault(s.charAt(l), 0) - 1);
}
++l;
}
}
return ansL == -1 ? "" : s.substring(ansL, ansR);
}
public Boolean check() {
Iterator iter = ori.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
Character key = (Character) entry.getKey();
Integer val = (Integer) entry.getValue();
if (cnt.getOrDefault(key, 0) < val) {
return false;
}
}
return true;
}