双指针法

双指针法常用于求和排序之类的场景,是一种时间换空间的算法。
学习算法只看理论知识是远远不够的,需要从各种各样的题中去细细体悟算法的魅力,从而真真的把知识刻在脑子里,成为自己的一部分;
所以此篇文章基本都是算法题,让我们从实际的代码角度来揭开双指针的神秘面纱吧!
下面的题都是从某一方面的知识写的例子,大家如果有其他想法,代码敲起来~

1. 给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个整数,并返回他们的数组下标。

这道题没有用到双指针,单纯的用来练练手,从第二题开始为双指针算法题;
示例: 给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9 所以返回 [0, 1]

求和问题基本可以反向思考为 差值问题,比如说a + b = target,则可以想成target - a = b, 这样就能避免对数组进行两次循环,再对循环的值相加;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

// 方法一
let nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
function demo (nums, target) {
// 这里我用对象来模拟 map 的能力
const diffs = {}
// 缓存数组长度
const len = nums.length
// 遍历数组
for (var i = 0; i < len; i++) {
// 判断当前值对应的 target 差值是否存在(是否已遍历过)
if (diffs[target - nums[i]] !== undefined) {
// 若有对应差值,那么答案get!
return [diffs[target - nums[i]], i]
}
// 若没有对应差值,则记录当前值
diffs[nums[i]] = i
}
};

const result = demo(nums, 16);
console.log(result); // [ 6, 8 ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

// 方法二
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
function demo(nums, target) {

// 缓存数组长度
const len = nums.length;

for (var i = 0; i < len; i++) {
let num = target - nums[i];
let index = nums.indexOf(num);
if (index !== -1) {
return [i, index];
}
}
}

const result2 = demo(nums, 16);
console.log(result2); // [ 5, 9 ]

2. 给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。

说明: 初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。 你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* @param {number[]} nums1
* @param {number} m
* @param {number[]} nums2
* @param {number} n
* @return {void} Do not return anything, modify nums1 in-place instead.
*/
function demo(nums1, m, nums2, n) {
// 初始化两个指针的指向,初始化 nums1 尾部索引k
let i = m - 1, j = n - 1, k = m + n - 1
// 当两个数组都没遍历完时,指针同步移动
while(i >= 0 && j >= 0) {
// 取较大的值,从末尾往前填补
if(nums1[i] >= nums2[j]) {
nums1[k] = nums1[i]
i--
k--
} else {
nums1[k] = nums2[j]
j--
k--
}
}

// nums2 留下的情况,特殊处理一下
while(j>=0) {
nums1[k] = nums2[j]
k--
j--
}
};

投机写法

1
2
3
4
5
function demo(nums1, m, nums2, n) {
nums1.splice(m);
nums2.splice(n);
return [...nums1, ...nums2].sort((a, b) => a - b);
}

3. 给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

输入: [ 0, 1, 0, 3, 12 ], 输出: [ 1, 3, 12, 0, 0 ], 只能在当前数组上操作,不能使用新建数组、合并等方法;

常规方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let arr = [0, 1, 0, 3, 12];

function demo(arr) {
let num = arr.length;
if(!num) return;
for (var i = 0; i <= num; i++) {
if (arr[i] === 0) {
arr.splice(i, 1);
arr.push(0);
i--; // 当前项删除后,下一个元素的下标前移了一位,所以要遍历到下一个元素,就需要保持原位
num--; // 删除了一位,数组需要的循环次数就应该减去一位,移到最后的0是不需要遍历的
}
}

}

demo(arr);

console.log(arr); // [1,3,12,0,0]

双指针方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let arr = [0, 1, 0, 3, 12];

function demo(nums) {
let n = nums.length;
if (!n) return;
let i = 0;
// 把非零的项依次移到前面,i为指针
for (var j = 0; j < n; j++) {
if (nums[j] !== 0) {
nums[i] = nums[j];
i++;
}
}
// k为指针2,指向非零项处理后第一个为0的元素
for (var k = i; k < n; k++) {
nums[k] = 0
}
return nums;
};

console.log(demo(arr)); // [ 1, 3, 12, 0, 0 ]

4. 给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。

示例:给定数组 nums = [-1, 0, 1, 2, -1, -4], 满足要求的三元组集合为: [ [-1, 0, 1], [-1, -1, 2] ]
注意:答案中不可以包含重复的三元组。

解析:
1.在数组求和的算法中,我们要习惯性在脑子里显现求和就是求差值指针 这两思路!
2.尽量从求差值的角度去减少循环次数,从而降低时间复杂度;
3.指针要求数组是可序的,所以当数组是无序时,要将数组排序,创造可以使用指针的条件;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// 对撞指针
let nums = [-3, 3, 1, -1, -2, 0, -1, 1 - 2, 2];
let m = 0;

/**
* 满足数组中三个数相加等于 m 的数组对象
* @param {number[]} arr 目标数组
* @param {number} m 三个数相加的和
* @return {array[]}
*/
function demo(arr, m) {

if (!arr || !arr.length) return;
// 保存目标数组
let result = [];
// 双指针法需要数组有序,有序才好指针移动
arr = arr.sort((a, b) => a - b);

// console.log(arr); // [-3, -2, -1, -1, -1, 0, 1, 2, 3]

let len = arr.length;


for (var i = 0; i < len; i++) {
// 对撞指针
let j = i + 1; // 指针1
let k = len - 1; // 尾指针

// 处理重复
if (arr[i] === arr[i - 1]) {
continue;
}

while (j < k) {
if ((arr[i] + arr[j] + arr[k]) > m) {
// 则尾指针向前移动
k--;
// 尾指针如果重复则跳过
if (arr[k] === arr[k + 1]) {
k--;
}

}
if ((arr[i] + arr[j] + arr[k]) < m) {
// 前指针向后移动一位
j++;
// 前指针如果重复则跳过
if (arr[j] === arr[j - 1]) {
j++;
}
}
if ((arr[i] + arr[j] + arr[k]) === m) {
result.push([arr[i], arr[j], arr[k]]);
// 两指针一同前进
k--;
j++;
// 重复值跳过,是一种优化,让代码少干点没有意义的活
if (arr[k] === arr[k + 1]) {
k--;
}
if (arr[j] === arr[j - 1]) {
j++;
}
}

}

}
return result;

}

console.log(demo(nums, m));