3.1 算法解释
双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多个数组的多个指针。
若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的区域即为当前的窗口),经常用于区间搜索。
若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是排好序的。
C++ 中的指针:
int x;
int * p1 = &x; // 指针可以被修改,值也可以被修改
const int * p2 = &x; // 指针可以被修改,值不可以被修改(const int)
int * const p3 = &x; // 指针不可以被修改(* const),值可以被修改
const int * const p4 = &x; // 指针不可以被修改,值也不可以被修改
// addition是指针函数,一个返回类型是指针的函数
int* addition(int a, int b) {
int* sum = new int(a + b);
return sum;
}
int subtraction(int a, int b) {
return a - b;
}
int operation(int x, int y, int (*func)(int, int)) {
return (*func)(x,y);
}
// minus是函数指针,指向函数的指针
int (*minus)(int, int) = subtraction;
int* m = addition(1, 2);
int n = operation(3, *m, minus);
对应JS中指针的使用:
- var o1={b:1}实现了在堆内存中创建了一个对象{b:1},o1则存储了该对象在堆内存中的地址,即o1是一个指针,指向{b:1};
- 在JavaScript中,引用类型(对象、数组、正则、Date、函数)的比较,实际上是比较指针是否指向存储器中的同一段地址,只有指向同样的地址才能相等。对于基本类型,只需其值相等,则两个变量就相等。
var o1={b:1};
var o2={b:1};
o1===o2;//false,储存在不同的地址
o1==o2;//false
- 对于引用类型,直接使用’='赋值实际上就是使两者指向同一个对象。
var obj1={b:1};
var obj2=obj1;
obj1===obj2;//true
obj1==obj2;//true
- 对于基本类型,函数add中的形参a、b分别得到变量a1、b1的值的拷贝,是按值传递。在add函数执行环境中对a、b操作不会影响到全局变量a1、b1。
var a1=1,b1=2;
function add(a,b){
a++;
b--;
return a+b;
};
add(a1,b1);//3
a1;//1
b1;//2
- 执行setName(person)时,person指向的内存中的地址便被传入obj,使得obj也指向同样的内存地址,即同一个对象。这里的按值传递,传递的是内存地址。
- 可以通过形参obj修改该对象。
function setName(obj){
obj.name="Nicholas";
obj=new Object();
obj.name="Greg";
}
var person=new Object();
setName(person);
alert(person.name);//"Nicholas"
- 函数指针,从这里可以看出obj对象的m2变量指向了acc这个函数指针,可以直接使用。而m1,指向了fn这个变量,就必须在使用之前声明。即var fn如果挪到obj的下面会出现undefined的报错。
var fn = function()
{
alert('fn method!');
};
var obj =
{
m1:fn,
id:1990,
m2:acc
};
/*
这种声明方式可以解决变量的问题。
*/
function acc()
{
alert('i m acc method!');
};
alert(obj.id);
console.dir(obj);
obj.m1();
3.3 归并两个有序数组
167. 两数之和 II - 输入有序数组
描述:
给定一个已按照 升序排列 的整数数组 numbers ,请你从数组中找出两个数满足相加之和等于目标数 target 。
函数应该以长度为 2 的整数数组的形式返回这两个数的下标值。numbers 的下标 从 1 开始计数 ,所以答案数组应当满足 1 <= answer[0] < answer[1] <= numbers.length 。
你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
算法思路:
- 设计左右指针
- 左指针指向最左边(下标为0),右指针指向最右边(下标为length-1)
- 之和大于目标值,则右指针左移,减小数值
- 之和小于目标值,则左指针右移,增加目标值
var twoSum = function(numbers, target) {
let answer=new Array();
let left=0;
let right=numbers.length-1;
while(numbers[left]+numbers[right] != target){
if(numbers[left]+numbers[right] > target)right--;
if(numbers[left]+numbers[right] < target)left++;
}
answer[0]=left+1;
answer[1]=right+1;
return answer;
};
运行结果:
88. 合并两个有序数组
描述:
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。
请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。
方法一:非指针
算法思路:
- 将nums2里面的内容赋值给nums1末尾n个0
- 用自带排序算法进行排序
var merge = function(nums1, m, nums2, n) {
//赋值
for(let i=m;i<m+n;i++){
nums1[i]=nums2[i-m];
}
//排序
nums1.sort((a,b)=>a-b);
};
运行结果:
方法二:指针
题解:
因为这两个数组已经排好序,我们可以把两个指针分别放在两个数组的末尾,即nums1 的 m - 1 位和 nums2 的 n - 1 位。每次将较大的那个数字复制到 nums1 的后边,然后向前移动一位。因为我们也要定位 nums1 的末尾,所以我们还需要第三个指针,以便复制。
在以下的代码里,我们直接利用 m 和 n 当作两个数组的指针,再额外创立一个 pos 指针,起始位置为 m +n - 1。每次向前移动 m 或 n 的时候,也要向前移动 pos。这里需要注意,如果 nums1的数字已经复制完,不要忘记把 nums2 的数字继续复制;如果 nums2 的数字已经复制完,剩余
nums1 的数字不需要改变,因为它们已经被排好序。
var merge = function(nums1, m, nums2, n) {
let pos = m-- + n-- - 1;
while (m >= 0 && n >= 0) {
nums1[pos--] = nums1[m] > nums2[n]? nums1[m--]: nums2[n--];
}
while (n >= 0) {
nums1[pos--] = nums2[n--];
}
};
运行结果:
3.4 快慢指针
142. 环形链表 II
描述:
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。
说明:不允许修改给定的链表。
进阶:
你是否可以使用 O(1) 空间解决此题?
算法思路:
- 设置快慢指针,快指针一次走两步,慢指针一次走。
- 判断有无环
无环:快指针走到null节点
有环:快慢指针相遇 - 快慢指针相遇时的等式:
slow = D+S1
fast = D+S1+S2+S1 = 2 * slow = 2 * D+2 * S1
===> D=S1 - fast从head出发,一次一步,slow从head出发,一次一步。
- slow和fast在环开始节点相遇
var detectCycle = function(head) {
let slow = head, fast = head;
// 判断是否存在环路
do {
if (!fast || !fast.next) return null;//走到头,没有环路
fast = fast.next.next;
slow = slow.next;
} while (fast != slow);//相遇,有环路
// 如果存在,查找环路节点
fast = head;
while (fast != slow){
slow = slow.next;
fast = fast.next;
}
return fast;
};
运行结果:
复杂度:
时间复杂度:O(n)
空间复杂度:O(1)
3.5 滑动窗口
76. 最小覆盖子串
题目描述
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。
注意:
对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
如果 s 中存在这样的子串,我们保证它是唯一的答案。
算法思路
- 统计t中所含有的字母,以及字母个数
- 设置左右指针,划分s中区间
- 右指针逐步右移,直至区间中包含t中所有字母,保存该区间
- 左指针左移
区间仍包含t中所有字母,更新区间
区间不包含t中所有字母,左指针停止左移 - 右指针继续右移动,寻找下一个符合要求的区间
var minWindow = function(s, t) {
let chars=Array.apply(null,{length:128}).map(()=>0);//统计t中某个字母个数
let flag=Array.apply(null,{length:128}).map(()=>false);//统计t中是否含有某个字母
// 先统计T中的字符情况
for(let i = 0; i < t.length; ++i) {
flag[t[i].charCodeAt()] = true;
++chars[t[i].charCodeAt()];
}
// 移动滑动窗口,不断更改统计数据
let cnt = 0, l = 0, min_l = 0, min_size = s.length + 1;
for (let r = 0; r < s.length; ++r) {
//通过字母个数来判断区间内是否包含t中全部字母
if (flag[s[r].charCodeAt()]) { //t中存在该字母
if (--chars[s[r].charCodeAt()] >= 0) {//记录-1
++cnt;//区间个数+1
}
// 若目前滑动窗口已包含T中全部字符,
// 则尝试将l右移,在不影响结果的情况下获得最短子字符串
while (cnt == t.length) {
if (r - l + 1 < min_size) {//更新区间
min_l = l;
min_size = r - l + 1;
}
if (flag[s[l].charCodeAt()] && ++chars[s[l].charCodeAt()] > 0) {
--cnt;
}
++l;
}
}
}
return min_size > s.length? "": s.substr(min_l, min_size);
};
运行结果
时间复杂度:O(n)
空间复杂度:O(1)
练习
基础难度
633. 平方数之和
给定一个非负整数 c ,你要判断是否存在两个整数 a 和 b,使得 a2 + b2 = c 。
算法思路
- 设置大小指针,max指向大的平方数,min指向小的平方数
- 根据max来确定min。
- 如果maxmax+minmin != c,max–,对应修改min的值。
var judgeSquareSum = function(c) {
let max=Math.floor(Math.sqrt(c));
let min=Math.floor(Math.sqrt(c - max*max));
while(min <= max){
if(min*min + max*max == c){
return true;
}else{
max--;
min=Math.floor(Math.sqrt(c - max*max));
}
}
return false;
};
680. 验证回文字符串 Ⅱ
给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串。
function isPali(head,tail,s){//判断截取字符串是否为回文
while(head < tail){
if(s[head] != s[tail]){
return false;
}
head++;
tail--;
}
return true;
}
var validPalindrome = function(s) {
//设置头尾指针
let head = 0;
let tail=s.length-1;
while(head < tail){
if(s[head] != s[tail]){
return isPali(head + 1, tail,s) || isPali( head , tail-1 , s );
}
head++;
tail--;
}
return true;
};
524. 通过删除字母匹配到字典里最长单词
最初代码
题目描述:
给你一个字符串 s 和一个字符串数组 dictionary 作为字典,找出并返回字典中最长的字符串,该字符串可以通过删除 s 中的某些字符得到。
如果答案不止一个,返回长度最长且字典序最小的字符串。如果答案不存在,则返回空字符串。
解题思路:
- 创建两个指针【SWord, dictionaryWord】用于指向s和字典中的字符串,和最符合要求的字符串【fitIndex】下标
- 逐个获取字典中的字符串,循环判断该字符串可以通过删除 s 中的某些字符得到
- 如果可以得到,并且前面没有符合的字符串(fitIndex == -1) || 字符串长度大于前面符合的字符串(dictionary[fitIndex].length < dictionary[i].length) || 字符串和前面符合的字符串长度相同并且字典序小于前面符合的字符串(dictionary[fitIndex].length == dictionary[i].length && dictionary[fitIndex] > dictionary[i])
代码
var findLongestWord = function(s, dictionary) {
let SWord, dictionaryWord;//创建两指针用于指向s和字典中的字符
let fitIndex=-1;
//选取字典中的字符
for(let i = 0; i < dictionary.length; i++){
SWord = dictionaryWord = 0;
//与字符串s比对
while(SWord<s.length && dictionaryWord<dictionary[i].length){
if(s[SWord] == dictionary[i][dictionaryWord]){
SWord++;
dictionaryWord++;
}else{
SWord++;
}
}
if(dictionaryWord>=dictionary[i].length){
if(fitIndex == -1 || dictionary[fitIndex].length < dictionary[i].length || (dictionary[fitIndex].length == dictionary[i].length && dictionary[fitIndex] > dictionary[i])){
fitIndex = i;
}
}
}
return fitIndex>=0?dictionary[fitIndex]:"";
};
运行结果:
优化代码
优化思路:
- 排除比符合字符串短的字符串
- 排除长度一样,且字典序大的字符串
var findLongestWord = function(s, dictionary) {
let SWord, dictionaryWord;//创建两指针用于指向s和字典中的字符
let fitIndex= "";
//选取字典中的字符
for(let i = 0; i < dictionary.length; i++){
SWord = dictionaryWord = 0;
//排除比符合字符串短和字典序大的
if(dictionary[i].length < fitIndex.length)continue;
if(dictionary[i].length == fitIndex.length && fitIndex < dictionary[i])continue;
//与字符串s比对
while(SWord<s.length && dictionaryWord<dictionary[i].length){
if(s[SWord] == dictionary[i][dictionaryWord]){
SWord++;
dictionaryWord++;
}else{
SWord++;
}
}
if(dictionaryWord>=dictionary[i].length){
if(fitIndex.length < dictionary[i].length || (fitIndex.length == dictionary[i].length && fitIndex > dictionary[i])){
fitIndex = dictionary[i];
}
}
}
return fitIndex;
};
运行结果