JS/TS算法---双指针(包含滑动窗口环形链表)
什么是双指针(对撞指针、快慢指针)
双指针,指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个相同方向(快慢指针)或者相反方向(对撞指针)的指针进行扫描,从而达到相应的目的。
换言之,双指针法充分使用了数组有序这一特征,从而在某些情况下能够简化一些运算。
用法
对撞指针(首尾指针,左右指针)
对撞指针是指在有序数组中,将指向最左侧的索引定义为左指针(left),最右侧的定义为右指针(right),然后从两头向中间进行数组遍历。
对撞数组适用于有序数组,也就是说当你遇到题目给定有序数组时,应该第一时间想到用对撞指针解题。
- 求和
function sum(arr,target){
let left = 0,right = arr.length-1
while(left<right){
if(arr[left]+arr[right]>target){
right--
}else if(arr[left]+arr[right]<target){
left++
}else if(arr[left]+arr[right]==target){
return [left,right]
}
}
}
- 数组反转
function reverse(arr){
let left = 0, right = arr.length-1
while(left < right){
[arr[left],arr[right]] = [arr[right],arr[left]]
left++
right--
}
return arr
}
快慢指针
快慢指针也是双指针,但是两个指针从同一侧开始遍历数组,将这两个指针分别定义为快指针(fast)和慢指针(slow),两个指针以不同的策略移动,直到两个指针的值相等(或其他特殊条件)为止,如fast每次增长两个,slow每次增长一个。
1.字符串压缩
function compressString(S){
let newS = '', i = 0, j = 0
while(j < S.length - 1){
if(S[j]!==S[j+1]){
newS += S[i]+(j-i+1)
i = j+1
}
j++
}
newS += S[i]+(j-i+1)
return newS.length<S.length?S
}
leecode题详解
左右指针
几数之和
[1] 两数之和---排序+双指针
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6
输出:[0,1]
提示: 只会存在一个有效答案
进阶:你可以想出一个时间复杂度小于 O(n^2) 的算法吗?
先排序 O(NlogN),并记录原来的位置,题目说了确定有唯一答案,所以用左右指针缩小搜索范围 O(N)
function twoSum(nums: number[], target: number): number[] {
const nextNums = nums.map((val, idx) => ({
val,
idx,
}));
nextNums.sort((a, b) => {
return a.val - b.val;
});
const n = nums.length;
let [left, right] = [0, n - 1];
while (left < right) {
const tmp = nextNums[left].val + nextNums[right].val;
if (tmp > target) {
right--;
} else if (tmp < target) {
left++;
} else {
break;
}
}
return [nextNums[left].idx, nextNums[right].idx];
}
[15] 三数之和
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
示例 2:
输入:nums = []
输出:[]
示例 3:
输入:nums = [0]
输出:[]
这道题是1.Two Sum的升级版,需要三个数的和为 0。那么我们可以想到,这三个数中的最小数必定为负数,并且另两个数的和等于这个数的相反数。
因此我们需要对数组从小到大进行排序,之后遍历一遍数组,每次固定住最小的那个数字nums[i],将它的相反数作为 target。
之后的解法就与Two Sum的解法完全一致了,使用首尾指针,由于另外两个数一定比最小数大,因此首次循环首尾指针范围在当前位置i+1到数组尾。
根据以上推导的结论,若这个nums[i]>0,或者尾指针指向的数字<0,则可以直接结束循环了。
跟Two Sum稍有不同的是,当找到 target 的一组解后不能立即结束循环,因为有可能存在多组和为 target 的解。并且数字组成完全相同的解不能放入结果中,需要做好去重操作。
function threeSum(nums: number[]): number[][] {
//数组排序 a-b升序 b-a降序
nums = nums.sort((a, b) => a - b);
const res: number[][] = [];
//length-2即可
for (let i = 0; i < nums.length - 2; i++) {
const min = nums[i];
// 如果数组的最小值都>0,则一定不存在 a + b + c = 0
if (min > 0) break;
// 去掉重复情况
if (i > 0 && min === nums[i - 1]) continue;
const target = 0 - min;
// 设置双指针
let left = i + 1;
let right = nums.length - 1;
while (left < right) {
if (nums[left] + nums[right] === target) {
res.push([min, nums[left], nums[right]]);
// 去除重复情况
while (left < right && nums[left + 1] === nums[left]) left += 1;
while (left < right && nums[right - 1] === nums[right]) right -= 1;
// 指针移动到下一组情况
left += 1;
right -= 1;
} else if (nums[left] + nums[right] > target) {
right -= 1;
} else {
left += 1;
}
}
}
return res;
}
[19] 四数之和
给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a、b、c 和 d 互不相同
nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
示例 1:
输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例 2:
输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]
提示:
1 <= nums.length <= 200
-10^9 <= nums[i] <= 10^9
-10^9 <= target <= 10^9作为 [1] 两数之和 与 [15] 三数之和 的再一次升级版,这一次我们可以整理出这一类问题的通用套路了。
实际上 nSum 问题的通用解法为:先通过遍历数组选定 N 元组中最小的那个数,之后再通过遍历选定第二小的数……当然这一过程可以用递归实现。
直到剩余 2 个数未确定,此时调用 2Sum 方法,通过空间换时间,将 O(n^2) 的时间复杂度缩小为 O(n)。
在进入下一级递归之前,我们需要做好一定的剪枝来提升性能。例如,最小的 4 个数都小于 target,直接退出;或者最大的 4 个数都大于 target, 直接跳过。再例如,当发现当前值与下一个值相同时,说明有重复元素,同样跳过。
最终,nSum 的时间复杂度为 O(n^(N-1))。所以随着 N 的增大,这一算法的优越度也变得越来越低了……因为 N=5 以上以后,一个 O(n^4) 的算法已经足以让很多用例直接超时了。
因此,掌握常规算法,并了解其衍生的题型改造就已经足够了,出 5Sum,6Sum 的题并无必要。
function fourSum(nums: number[], target: number): number[][] {
nums = nums.sort((a, b) => a - b);
const len = nums.length;
const res: number[][] = [];
for (let i = 0; i < len - 3; i++) {
// 最小的 4 个数都小于 target,直接退出;或者最大的 4 个数都大于 target, 直接跳过
if (nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target) break;
if (nums[i] + nums[len - 1] + nums[len - 2] + nums[len - 3] < target) continue;
// 去掉重复情况
if (i > 0 && nums[i - 1] === nums[i]) continue;
// 接下来就是 3Sum 问题了
threeSumCase(i, nums[i], target, nums, res);
}
return res;
function threeSumCase(start: number, first: number, target: number, nums: number[], res: number[][]) {
const len = nums.length;
for (let i = start + 1; i < len - 2; i++) {
const second = nums[i];
// 最小的 4 个数都小于 target,直接退出;或者最大的 4 个数都大于 target, 直接跳过
if (first + second + nums[i + 1] + nums[i + 2] > target) break;
if (first + second + nums[len - 1] + nums[len - 2] < target) continue;
// 去掉重复情况
if (i > start + 1 && nums[i - 1] === nums[i]) continue;
// 接下来就是 2Sum 问题了
let left = i + 1;
let right = nums.length - 1;
while (left < right) {
const sum = first + second + nums[left] + nums[right];
if (sum === target) {
res.push([first, second, nums[left], nums[right]]);
// 去除重复情况
while (left < right && nums[left + 1] === nums[left]) left += 1;
while (left < right && nums[right - 1] === nums[right]) right -= 1;
// 指针移动到下一组情况
left += 1;
right -= 1;
} else if (sum > target) {
right -= 1;
} else {
left += 1;
}
}
}
}
}
滑动窗口
在力扣上刷题时经常可以看到这样的题,求XXX的子串、子数组、子序列等等,这类题一般使用滑动窗口来解决。
情况一:寻找最长的
①初始化左右指针left和right,左右指针之间的内容就是窗口,定义一个变量result记录当前的滑动窗口的结果,定义一个变量bestResult记录当前滑动窗口下的最优结果
②right要向右逐位滑动循环
③每次滑动后,记录当前滑动的结果。如果当前的结果符合条件,则更新最优的结果,然后right要继续向右滑动;如果当前的结果不符合条件,那么要让left逐步收缩
④当right到达结尾时停止滑动
初始化left,right,result,bestResult
while (右指针没有到结尾) {
窗口扩大,加入right对应元素,更新当前result
while (result不满足要求) {
窗口缩小,移除left对应元素,left右移
}
更新最优结果bestResult
right++;
}
返回bestResult
情况二:寻找最短的
①初始化左右指针left和right,左右指针之间的内容就是窗口,定义一个变量result记录当前的滑动窗口的结果,定义一个变量bestResult记录当前滑动窗口下的最优结果
②right要向右逐位滑动循环
③每次滑动后,记录当前滑动的结果。如果当前的结果符合条件,则更新最优的结果,然后right要继续向右滑动;如果当前的结果不符合条件,那么要让left逐步收缩
④当right到达结尾时停止滑动
初始化left,right,result,bestResult
while (右指针没有到结尾) {
窗口扩大,加入right对应元素,更新当前result
while (result不满足要求) {
更新最优结果bestResult
窗口缩小,移除left对应元素,left右移
}
right++;
}
返回bestResult
[3] 无重复字符的最长子串
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
这是一个简化版的滑动窗口题型。解题思路可以参考专题中关于双指针滑动窗口的相关介绍。
按照专题中的思路,这里考虑两点:
- 扩充右边界后,何时能使滑动窗口内的元素满足要求。根据题意,当滑动窗口的hash map中出现字符个数大于1的情况时,说明窗口中字串有重复字符,此时考虑开始缩小左边界。
- 何时更新返回结果。在本题中,当滑动窗口中所有元素个数都为1,则可以认为当前子串为无重复字符串,此时可以比对并更新结果。
function lengthOfLongestSubstring(s: string): number {
const window: Record<string, number> = {};
let res = 0;
let left = 0;
let right = 0;
while (right < s.length) {
// 扩大右边界
const ch = s[right];
right++;
// 更新滑动窗口元素
window[ch] = window[ch] ? window[ch] + 1 : 1;
// 当滑动窗口中该字符个数大于1,此时字串不合法,需要缩小左边界直到使该字符唯一
while (window[ch] > 1) {
// 缩左边界
const dropCh = s[left];
left++;
// 更新滑动窗口元素
window[dropCh] -= 1;
}
// 更新合法情况的结果
res = Math.max(res, right - left);
}
return res;
}
[209] 长度最小的子数组-------寻找最短的
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr],并返回其长度。如果不存在符合条件的子数组,返回 0 。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:
输入:target = 4, nums = [1,4,4]
输出:1
示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
提示:
进阶:
如果你已经实现 O(n) 时间复杂度的解法, 请尝试设计一个 O(n log(n)) 时间复杂度的解法。
这道题是求满足条件的子数组最小长度。对于求子串、子数组的最优解问题,我们首先想到是否能用滑动窗口或是动态规划来解。这道题要求子数组连续,那么其实用滑动窗口就够了。
那么就来到了经典的问题填空环节:
- 扩充右边界后,何时能使滑动窗口内的元素满足要求。根据题意,当滑动窗口内元素总和 >= target 时,考虑开始缩小左边界。
- 何时更新返回结果。在本题中,当滑动窗口内元素总和 >= target 时,比对记录值与当前sum结果,保存最小值。
这样一套操作下来,整个问题就没有任何难点可言了。
function minSubArrayLen(target: number, nums: number[]): number {
let res: number = nums.length + 1;
let left = 0;
let right = 0;
let sum = 0;
while (right < nums.length) {
sum += nums[right];
right += 1;
while (sum >= target) {
res = Math.min(res, right - left);
sum -= nums[left];
left++;
}
}
// 不存在时返回0
return res === nums.length + 1 ? 0 : res;
}
环形链表
[141] 环形链表
给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true 。 否则,返回 false 。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。示例 2:
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。示例 3:
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。
快慢指针
function hasCycle(head: ListNode | null): boolean {
if (head === null || head.next === null) return false;
// 快慢指针
let slow = head;
let fast = head;
while (fast !== null) {
// 慢指针每次移动一位
slow = slow.next;
// 如果满足条件,说明 fast 为尾部结点,不存在环
if (fast.next === null) return false;
// 快指针每次移动两位
fast = fast.next.next;
// slow 和 fast 相等,说明内存地址相同,有环
if (slow === fast) return true;
}
return false;
};
[142] 环形链表 II
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。
判断链表是否有环
可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。
为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢
首先第一点:fast指针一定先进入环中,如果fast指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。
那么来看一下,为什么fast指针和slow指针一定会相遇呢?
可以画一个环,然后让 fast指针在任意一个节点开始追赶slow指针。
会发现最终都是这种情况, 如下图:
fast和slow各自再走一步, fast和slow就相遇了
这是因为fast是走两步,slow是走一步,其实相对于slow来说,fast是一个节点一个节点的靠近slow的,所以fast一定可以和slow重合。
动画如下:
如果有环,如何找到这个环的入口
此时已经可以判断链表是否有环了,那么接下来要找这个环的入口了。
假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。 如图所示:
那么相遇时: slow指针走过的节点数为: x + y
, fast指针走过的节点数:x + y + n (y + z)
,n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数A。
因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2:
(x + y) * 2 = x + y + n (y + z)
两边消掉一个(x+y): x + y = n (y + z)
因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。
所以要求x ,将x单独放在左面:x = n (y + z) - y
,
再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z
注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。
这个公式说明什么呢?
先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。
当 n为1的时候,公式就化解为 x = z
,
这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。
也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。
让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。
动画如下:
那么 n如果大于1是什么情况呢,就是fast指针在环形转n圈之后才遇到 slow指针。
其实这种情况和n为1的时候 效果是一样的,一样可以通过这个方法找到 环形的入口节点,只不过,index1 指针在环里 多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。
function detectCycle(head: ListNode | null): ListNode | null {
if(head===null||head.next===null) return null;
//快慢指针
let slow = head;
let fast = head;
while(fast!==null&&fast.next!==null){
//慢指针移动一次
slow = slow.next;
fast = fast.next.next;
//进入环内
if(slow===fast) {
slow = head;
while(slow!==fast){
slow = slow.next;
fast = fast.next;
}
return slow
}
}
return null;
};
补充
在推理过程中,大家可能有一个疑问就是:为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y 呢?
即文章链表:环找到了,那入口呢? (opens new window)中如下的地方:
首先slow进环的时候,fast一定是先进环来了。
如果slow进环入口,fast也在环入口,那么把这个环展开成直线,就是如下图的样子:
可以看出如果slow 和 fast同时在环入口开始走,一定会在环入口3相遇,slow走了一圈,fast走了两圈。
重点来了,slow进环的时候,fast一定是在环的任意一个位置,如图:
那么fast指针走到环入口3的时候,已经走了k + n 个节点,slow相应的应该走了(k + n) / 2 个节点。
因为k是小于n的(图中可以看出),所以(k + n) / 2 一定小于n。
也就是说slow一定没有走到环入口3,而fast已经到环入口3了。
这说明什么呢?
在slow开始走的那一环已经和fast相遇了。
那有同学又说了,为什么fast不能跳过去呢? 在刚刚已经说过一次了,fast相对于slow是一次移动一个节点,所以不可能跳过去。
好了,这次把为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y ,用数学推理了一下,算是对链表:环找到了,那入口呢? (opens new window)的补充。
快慢指针
剑指 Offer 22. 链表中倒数第k个节点
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。
示例:
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.
function getKthFromEnd(head: ListNode | null, k: number): ListNode | null {
let slow = head;
let fast = head;
//快指针先走k步
while (k-- > 0) fast = fast.next;
//快指针走到头停止
while (fast !== null) {
fast = fast.next;
slow = slow.next;
}
//输出慢指针
return slow;
};
讲解:原地算法(In-Place Algorithm)
原地算法:在计算机科学中,一个原地算法(in-place algorithm)是一种使用小的,固定数量的额外之空间来转换资料的算法。当算法执行时,输入的资料通常会被要输出的部分覆盖掉。不是原地算法有时候称为非原地(not-in-place)或不得其所(out-of-place)。
通俗的说法:就是一个算法,除了可以运用输入数据本身已开辟的空间外,就只可以用极小的辅助空间来进行运算了,一般 额外空间复杂度为 O(1),也就是一个变量。(特殊情况除外)
[283] 移动零
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
示例 2:输入: nums = [0]
输出: [0]
两次遍历
var moveZeroes = function (nums) {
let j = 0;
for (let i = 0; i < nums.length; i++) {
if (nums[i] !== 0) {//遇到非0元素,让nums[j] = nums[i],然后j++
nums[j] = nums[i];
j++;
}
}
for (let i = j; i < nums.length; i++) {//剩下的元素全是0
nums[i] = 0;
}
return nums;
};
双指针一次遍历
思路:定义left、right指针,right从左往右移动,遇上非0元素,交换left和right对应的元素,交换之后left++
复杂度:时间复杂度O(n),空间复杂度O(1)
var moveZeroes = function(nums) {
let left=0,right=0
while(right<nums.length){
if(nums[right]!==0){//遇上非0元素,交换left和right对应的元素
swap(nums,left,right)
left++//交换之后left++
}
right++
}
};
function swap(nums,l,r){
let temp=nums[r]
nums[r]=nums[l]
nums[l]=temp
}
[26] 删除有序数组中的重复项
给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
示例 1:
给定数组 nums = [1,1,2],
函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。
你不需要考虑数组中超出新长度后面的元素。
示例 2:
给定 nums = [0,0,1,1,1,2,2,3,3,4],
函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。
你不需要考虑数组中超出新长度后面的元素。
本来只是一个基本的数组去重动作,但是题目要求我们使用原地算法(空间复杂度O(1)),并且不需要考虑超出长度后面的元素,也就是说我们只需要保证前K个数是有序去重的即可。
我们就能想到双指针,解题流程如下:
创建一个慢指针 i,指向数组第2位数字,再创建一个快指针 j,指向数组第2位。
遍历数组,从数组第二位开始
若 nums[j] 和 nums[j-1] 不等,把 nums[i] 改为 nums[j],再i++。
图解如下:
function removeDuplicates(nums: number[]):number {
let i = 1;
for(let j=1; i<nums.length; j++){
if(nums[j]!=nums[j-1]){
nums[i] = nums[j]
i++;
}
}
return i
};
两个数组指针
[88] 合并两个有序数组
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。
请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。
示例 1:
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
解释:需要合并 [1,2,3] 和 [2,5,6] 。
合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。
示例 2:输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
解释:需要合并 [1] 和 [] 。
合并结果是 [1] 。
示例 3:输入:nums1 = [0], m = 0, nums2 = [1], n = 1
输出:[1]
解释:需要合并的数组是 [] 和 [1] 。
合并结果是 [1] 。
注意,因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。提示:
nums1.length == m + n
nums2.length == n
0 <= m, n <= 200
1 <= m + n <= 200
-109 <= nums1[i], nums2[j] <= 109进阶:你可以设计实现一个时间复杂度为 O(m + n) 的算法解决此问题吗?
function merge(nums1: number[], m: number, nums2: number[], n: number) {
let e = nums1.length - 1;//指向nums1末尾
let mi = m - 1;//指向nums1最后
let ni = n - 1;//指向nums2最后
while (mi >= 0 && ni >= 0) {
if (nums1[mi] > nums2[ni]) {
//最大值移动到末尾
nums1[e] = nums1[mi]
mi--
} else {
nums1[e] = nums2[ni]
ni--
}
//末尾指针左移
e--
}
//nums2为空
while (mi >= 0) {
nums1[e] = nums1[mi]
mi--
e--
}
//nums1为空(均为0)
while (ni >= 0) {
nums1[e] = nums2[ni]
ni--
e--
}
return nums1
};
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南