什么是双指针算法?
1 双指针算法一般是指有两个指针,可以用来遍历,也可以用来查找,还可以当做滑动窗口,双指针算法的关键是寻找到什么时候更新low指针,什么时候更新high指针,也就是根据什么条件来更新指针,和贪心算法主要找到什么是最贪心差不多。
167. 两数之和 II - 输入有序数组
题目描述
给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。
函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。
说明:
返回的下标值(index1 和 index2)不是从零开始的。<br/>
你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。<br/>
例子
输入: numbers = [2, 7, 11, 15], target = 9
输出: [1,2]
解释: 2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。
思考 1
不管做任何的算法,首先要理解好前提,也就是已知条件,比如这里已经明确表示了这是已经升序的了,所以很容易想到使用双指针,一个指针指向最小,一个指向最大,然后根据是否和targe相等不断的移动指针。
实现1
/**
* @param {number[]} numbers
* @param {number} target
* @return {number[]}
*/
export default (numbers, target) => {
let minPoints = 0;
const len = numbers.length;
let maxPoints = len - 1;
const res = [];
while (minPoints < maxPoints) {
if (numbers[minPoints] + numbers[maxPoints] === target) {
res.push(++minPoints);
res.push(++maxPoints);
} else if (numbers[minPoints] + numbers[maxPoints] > target) {
maxPoints--;
} else {
minPoints++;
}
}
return res;
};
算法时间复杂度 O(n), 空间复杂度 O(1)
88. 合并两个有序数组
题目描述
给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。
说明:
1 初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。
2 你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。
例子
输入:
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
提示:
1 -10^9 <= nums1[i], nums2[i] <= 10^9
2 nums1.length == m + n
3 nums2.length == n
思考 1
1 采用两个指针,一个指向nums1的末尾,一个指向nums2的末尾,然后对比就可以了,不断的插入到num1的末尾,算法比较简单。
2 还有数组很容易就想到了排序,可以先把nums2加入到nums1,然后可以进行排序就可以了。
实现1
/**
* @param {number[]} nums1
* @param {number} m
* @param {number[]} nums2
* @param {number} n
* @return {void} Do not return anything, modify nums1 in-place instead.
*/
// [0];
// (0)[1];
// 1;
export default (nums1, m, nums2, n) => {
let i = m - 1;
let j = n - 1;
let k = m + n - 1;
for (let m1 = m; m1 < m + n; m1++) {
nums1[m1] = 0;
}
while (k >= 0) {
if (nums2[j] >= nums1[i]) {
nums1[k] = nums2[j];
k--;
j--;
} else {
if (i < 0 && j >= 0) {
while (j >= 0) {
nums1[k] = nums2[j];
k--;
j--;
}
} else if (j < 0 && i >= 0) {
while (i >= 0) {
nums1[k] = nums1[i];
k--;
i--;
}
} else if (i >= 0 && j >= 0) {
nums1[k] = nums1[i];
k--;
i--;
} else {
k--;
}
}
}
return nums1;
};
算法时间复杂度 O(m+n), 空间复杂度 O(1)
实现2
/**
* @param {number[]} nums1
* @param {number} m
* @param {number[]} nums2
* @param {number} n
* @return {void} Do not return anything, modify nums1 in-place instead.
*/
export default (nums1, m, nums2, n) => {
for (let i = m; i < m + n; i++) {
nums1[i] = nums2[i - m];
}
nums1.sort((a, b) => a - b);
return nums1;
};
算法时间复杂度 O(m+nlg(m+n)), 空间复杂度 O(1)
142. 环形链表 II
题目描述
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。
说明:
1 不允许修改给定的链表。
进阶:
你是否可以使用 O(1) 空间解决此题?。
例子1
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点,因为链表中有一个环,其尾部连接到第二个节点。
例子2
输入:head = [1,2], pos = 0
输出:链表中有一个环,其尾部连接到第一个节点。。
例子3
输入:输入:head = [1], pos = -1
输出:返回 null,链表中没有环。
提示:
1 链表中节点的数目范围在范围 [0, 104] 内
2 -10^5 <= Node.val <= 10^5
3 pos 的值为 -1 或者链表中的一个有效索引
思考 1
1 因为以前做过一道印象特别深刻的题目,就是判断链表中是否存在环,采用快慢指针,慢指针走一步,快指针走两步,所以这里也准备使用快慢指针,可是这里并不是求链表中是否存在环,而是要找到环的开始节点,后来想到以前还做过一道龟兔赛跑,查找节点的题目,可是还是没有头绪
2 后来看了题解才想起了,这个题目是做过的,但是还没理解题目的真谛
可以发现真正的要点就是a=c,所以要找起始点就很容易了,可以分别从开始出发一个指针,从z节点出发一个指针,当两者相遇的时候就是环的起始点。
这种解法其实就是知道就知道,不知道除非看过,不然很难想出来
这种固定套路的记住就可以了
实现1
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var detectCycle = function(head) {
let temphead = head;
let slow = head;
let fast = head;
do {
slow = slow ? slow.next : null;
fast = fast? fast.next : null;
fast = fast ? fast.next : null;
} while (slow !== fast && fast !== null);
if (slow === fast && fast!==null) {
while (temphead !== fast) {
temphead = temphead.next;
fast = fast.next;
}
}
return fast || null;
};
算法时间复杂度 O(n), 空间复杂度 O(1)
76. 最小覆盖子串
题目描述
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。
说明:
1 如果 s 中存在这样的子串,我们保证它是唯一的答案。
进阶:
你能设计一个在 o(n) 时间内解决此问题的算法吗?
例子1
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
例子2
输入:s = "a", t = "a"
输出:"a"
提示:
1 1 <= s.length, t.length <= 10^5
2 s 和 t 由英文字母组成
思考 1
1 这里首先想到的肯定是双指针,因为这个专题就是双指针, 所以首先想到的肯定是设置两个指针,一个slow,一个fast,
slow的位置肯定是小于或者等于fast指针的位置。
然后遍历字符串,找到包含所有t的子串,然后再不断更新slow指针,那么下一个问题就变成了按照什么规则更新slow指针?
然后很自然就会想到因为我们是找的最短的字符串,所以如果slow指针不断前进的时候,如果发现slow现在所指的字符不在t中,当然可以继续前进,因为slow在这种情况下前进,肯定不会影响最短字符串的是否包含t中的所有字符的。
当发现slow指针指的字符在t中的时候,这个时候因为字符串还没有遍历完,所以我们还是需要继续遍历下去的,否则不能肯定现在找到的是最短的字符串是s中能找到的包含t的最短字符串,这个时候就要更新slow指针,也就是让从我们找到的最短字符串中删除一个在t中的字符且在我们的找到的最短字符串中重复的次数不大于在t中出现的次数的字符。
这个题目有些不是很好理解的地方,可能就是在理解slow指针应该怎么前进?
比如 s="ADOBECODEBANC",t="ABC"的时候,
我们可以很容易的发现slow = 0,fast = 5的时候,也就是"ADOBEC"的时候是包含所有t的字符的,这时候最短字符串就是"ADOBEC"
然后想办法从"ADOBEC" 删除掉一个在t中且在我们找到的最短字符串"ADOBEC"重复次数小于在t中重复的次数,当然这个例子中,t 不包含重复的字符,
可以发现A在t中,所以slow一直前进到1,然后fast前进到10,此时又发现新的包含t的字符串“DOBECODEBA”
因为D和O不在t中,所以slow可以一直前进到4,也就是变成slow=4,fast=10
这个时候重点就来了,slow下一步如何前进,如果此时slow前进到4,可以发现“B”在t中,那么是不是就删除"B",让slow=5,让fast继续寻找“B”呢?很明显不行,因为“ECODEBA”里边有"B",没有必要去寻找"B"
所以slow还得继续前进,一直前进到slow=7,也就是删除“C”,这个时候待查找字符串就变成了“ODEBA”,这样就可以让fast去寻找 “C”了。
如果还是不懂,可以看一下代码,主要看下是如何不断的更新slow的,
关键的一点是记住更新slow的目的是为了在已经查找的最短字符串中删除一个字符,然后让fast再去寻找到一个,形成新的最短字符串
通过这个题可以学习一下如何维持一个滑动窗口的状态,不断的从窗口中删除和不断的添加到滑动窗口
实现1
/**
* @param {string} s
* @param {string} t
* @return {string}
*/
export default (s, t) => {
const tMap = new Map();
for (let i = 0; i < t.length; i++) {
if (!tMap.has(t[i])) {
tMap.set(t[i], 1);
} else {
let count = tMap.get(t[i]) + 1;
tMap.set(t[i], count);
}
}
let cnt = 0,
slow = 0,
resStart = 0,
resLen = s.length + 1;
for (let fast = 0; fast < s.length; fast++) {
if (tMap.has(s[fast])) {
let count = tMap.get(s[fast]) - 1;
tMap.set(s[fast], count);
if (count >= 0) {
cnt++;
}
// 若目前滑动窗口已包含T中全部字符,
// 则尝试将l右移,在不影响结果的情况下获得最短子字符串
while (cnt === t.length) {
if (fast - slow + 1 < resLen) {
resStart = slow;
resLen = fast - slow + 1;
}
let count = tMap.get(s[slow]) + 1;
tMap.set(s[slow], count);
if (tMap.has(s[slow]) && count > 0) {
cnt--;
}
slow++;
}
}
}
return resLen > s.length ? "" : s.substr(resStart, resLen);
};
这里的时间复杂度可以很容易的看出是O(s.length),空间复杂度O(t.length)
633. 平方数之和
题目描述
给定一个非负整数 c ,你要判断是否存在两个整数 a 和 b,使得 a^2 + b^2 = c 。
例子1
输入: 5
输出: True
解释: 1 * 1 + 2 * 2 = 5
例子2
输入:3
输出:false
例子3
输入:4
输出:true
例子4
输入:2
输出:true
例子5
输入:1
输出:true
提示:
1 0 <= c <= 231 - 1
思考 1
1 直接使用两个指针,遍历从1到Math.sqrt(c)就可以了,题目比较简单。
实现1
/**
* @param {number} c
* @return {boolean}
*/
export default (c) => {
let high = Math.ceil(Math.sqrt(c));
let low = 0;
while (low <= high) {
if (Math.pow(low, 2) + Math.pow(high, 2) === c) {
return true;
} else if (Math.pow(low, 2) + Math.pow(high, 2) > c) {
high--;
} else {
low++;
}
}
return false;
};
这里的时间复杂度可以很容易的看出是O(Math.sqrt(c)),空间复杂度O(1)
680. 验证回文字符串 Ⅱ
题目描述
最多删除一个字符后,判断剩余的字符串是否为回文串。
例子1
输入: "aba"
输出: True
例子2
输入:"abca"
输出:True
解释:可以删除c字符就可以了
提示:
1 字符串长度小于50000
思考 1
1 直接使用两个指针,一个指针从开头,一个从结尾,如果发现不一致就看下删除low指向的字符,看下剩下的是否是回文字符串,看下删除high指向的字符,剩下的是否是回文字符串, 最后如果发现删除了超过一个字符,则返回false,否则为true,思路比较简单。
实现1
/**
* @param {string} s
* @return {boolean}
*/
const isPalindrome = (s, low, high, count) => {
if (count > 1) return false;
while (low < high) {
if (s[low] !== s[high]) {
count++;
return isPalindrome(s, low, high - 1, count) || isPalindrome(s, low + 1, high, count);
} else {
low++;
high--;
}
}
return true;
};
const validPalindrome = (s) => {
if (!s) {
return false;
}
if (s.length === 1) {
return true;
}
let low = 0;
let high = s.length - 1;
let count = 0;
return isPalindrome(s, 0, high, count);
};
export default isPalindrome;
这里的时间复杂度可以很容易的看出是O(n)因为我们不管递归多少次,最多也是遍历整个字符串进行对比,空间复杂度O(1)
524. 通过删除字母匹配到字典里最长单词
题目描述
给定一个字符串和一个字符串字典,找到字典里面最长的字符串,该字符串可以通过删除给定字符串的某些字符来得到。如果答案不止一个,返回长度最长且字典顺序最小的字符串。如果答案不存在,则返回空字符串。
例子1
输入: s = "abpcplea", d = ["ale","apple","monkey","plea"]
输出: "apple"
例子2
输入:s = "abpcplea", d = ["a","b","c"]
输出:"a"
提示:
1 所有输入的字符串只包含小写字母。
2 字典的大小不会超过 1000。
3 所有输入的字符串长度不会超过 1000。
思考 1
1 这个应该也很简单,主要是先排序数组,然后遍历数组中每个元素,假设数组中元素为s1,判断下s中是否存在s1中所有字符,且s1中出现的每个字符必须和出现在s中的相对顺序都一样,因为只能在s中删除字符变成s1,如果相对顺序不一致,就无法通过s中删除字符变成s1.
这里需要注意的是js中对于字符串的字典排序使用localeCompare
indexOf函数可以指定第二个参数
实现1
/**
* @param {string} s
* @param {string[]} d
* @return {string}
*/
export default (s, d) => {
// 首先按照长度降序排序,如果字符串长度相同,按照字典序升序排序
d.sort((a, b) => {
if (a.length !== b.length) {
return b.length - a.length;
}
return a.localeCompare(b);
});
for (let i = 0; i < d.length; i++) {
let index = -1;
for (let j = 0; j < d[i].length; j++) {
// 判断在d[i]中的字符是否也在s中,同时相对顺序是一定的
index = s.indexOf(d[i][j], index + 1);
// 如果不存在,就没必要进行下去了
if (index < 0) {
break;
}
}
// 如果找到了,直接跳出
if (index >= 0) {
return d[i];
}
}
return "";
};
这里的时间复杂度可以很容易的看出是Math.max((d.length* d[i].length * s.length),s.length* lg(s.length))
因为可以看到是三层循环和对s进行排序
空间复杂度O(1)
340. 找出至多包含k个不同字符的最长子串
题目描述
至多包含 K 个不同字符的最长子串
给定一个字符串 s ,找出至多包含 k 个不同字符的最长子串 T。
例子1
输入: s = "eceba", k = 2
输出: 3
解释:则 T 为 “ece”,所以长度为 3。
例子2
输入:s = “aa”, k = 1
输出: 2
解释:则 T 为 “aa”,所以长度为 2。
思考 1
1 因为这里是双指针的专题,很自然就想到了双指针,那么双指针的核心是什么呢?
可以思考一下
核心就是找到如何更新两个指针,这里很容易就想到一个low指针指向包含k个不同字符的字符串s1的低位,一个high指向s1高位
剩下的就是考虑如何不断的更新两个指针,什么时候更新low指针,什么时候更新high指针
比较容易想到在这种时候,如果high指针向前移动,直到找到和不能再移动,不能再移动就是指发现了和high移动第一步后不相同的字符的时候,假设此时有a个相同字符,a >= 1, 此时如何更新low呢?
此时最好的情况是low指向的字符在s1中只出现一次,此时可以直接low++(此时low指向的字符肯定不等于high指向的字符,如果此时相等,那么说明我们找到的s1是错误的)
那么剩下的就是当low指向的字符在s1中出现了不止一次,出现了多次,那么该如何更新low呢?或者low更新不了,是不是能更新high呢?
这应该是题目最难的地方,所以这里可以停下里思考一下?
这时候low可以一直删除字符,直到字符在s1中只出现一次,可以停止了,此时有发现了一个新的字符串s2,比较s2和s1的长度就可以了
2 其实这里还可以使用dp,因为很容易就可以看出dp转移方程
假设dp[i] 表示含有k个字符的最长字符串
那么dp[i+1] 很容易想到dp[i+1] = Math.max(dp[i], 以i+1为结尾的最长k个字符串)
实现1
/**
* @param {string} s
* @param {number} k
* @return {number}
*/
export default (s, k) => {
// 处理边界情况
if (s.length === 0 || k === 0) return 0;
// 不同字符的个数
let distinct = 0;
// 记录每个字符出现的次数
const map = new Map();
let low = 0;
let res = 0;
for (let high = 0; high < s.length; high++) {
const highChar = s.charAt(high);
if (!map.has(highChar)) {
map.set(highChar, 1);
distinct++;
} else {
let count = map.get(highChar) + 1;
map.set(highChar, count);
}
// 如果不同的字符数小于k,继续增加high,直到找到包含大于k个字符
if (distinct <= k) {
res = Math.max(res, high - low + 1);
} else {
// 大于k个字符的时候,这时候就要更新low,不断删除字符,直到小于k个不同字符
while (distinct > k) {
const lowChar = s.charAt(low);
let lowCount = map.get(lowChar);
if (lowCount > 1) {
map.set(lowChar, lowCount - 1);
} else {
map.delete(lowChar);
distinct--;
}
low++;
}
}
}
return res;
};
这里的时间复杂度可以很容易的看出是O(n),虽然里边有while循环,但是仍然是O(n)
空间复杂度O(n)
双指针算法总结
一般是涉及到范围的都可以尝试下双指针算法,双指针算法的关键是要找到如何更新两个指针,让我们更加的接近算法的解。换句话说就是要想想如何缩小我们要查找的范围,让我们更加的接近答案。