JS/TS算法---双指针(包含滑动窗口环形链表)

什么是双指针(对撞指针、快慢指针)

双指针,指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个相同方向(快慢指针)或者相反方向(对撞指针)的指针进行扫描,从而达到相应的目的。
换言之,双指针法充分使用了数组有序这一特征,从而在某些情况下能够简化一些运算。

用法

对撞指针(首尾指针,左右指针)

对撞指针是指在有序数组中,将指向最左侧的索引定义为左指针(left),最右侧的定义为右指针(right),然后从两头向中间进行数组遍历。
对撞数组适用于有序数组,也就是说当你遇到题目给定有序数组时,应该第一时间想到用对撞指针解题。

  1. 求和
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]
    } 
  }
}
  1. 数组反转
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到达结尾时停止滑动

初始化leftright,result,bestResult
while (右指针没有到结尾) {
    窗口扩大,加入right对应元素,更新当前result
    while (result不满足要求) {
        窗口缩小,移除left对应元素,left右移
    }
    更新最优结果bestResult
    right++;
}
返回bestResult

情况二:寻找最短的

①初始化左右指针left和right,左右指针之间的内容就是窗口,定义一个变量result记录当前的滑动窗口的结果,定义一个变量bestResult记录当前滑动窗口下的最优结果
②right要向右逐位滑动循环
③每次滑动后,记录当前滑动的结果。如果当前的结果符合条件,则更新最优的结果,然后right要继续向右滑动;如果当前的结果不符合条件,那么要让left逐步收缩
④当right到达结尾时停止滑动

初始化leftright,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" 是一个子序列,不是子串。

这是一个简化版的滑动窗口题型。解题思路可以参考专题中关于双指针滑动窗口的相关介绍。

按照专题中的思路,这里考虑两点:

  1. 扩充右边界后,何时能使滑动窗口内的元素满足要求。根据题意,当滑动窗口的hash map中出现字符个数大于1的情况时,说明窗口中字串有重复字符,此时考虑开始缩小左边界。
  2. 何时更新返回结果。在本题中,当滑动窗口中所有元素个数都为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)) 时间复杂度的解法。

这道题是求满足条件的子数组最小长度。对于求子串、子数组的最优解问题,我们首先想到是否能用滑动窗口或是动态规划来解。这道题要求子数组连续,那么其实用滑动窗口就够了。

那么就来到了经典的问题填空环节:

  1. 扩充右边界后,何时能使滑动窗口内的元素满足要求。根据题意,当滑动窗口内元素总和 >= target 时,考虑开始缩小左边界。
  2. 何时更新返回结果。在本题中,当滑动窗口内元素总和 >= 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:

img

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

img

输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

img

输入: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:

img

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:

img

输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:

img

输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。

判断链表是否有环

可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。

为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢

首先第一点:fast指针一定先进入环中,如果fast指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。

那么来看一下,为什么fast指针和slow指针一定会相遇呢?

可以画一个环,然后让 fast指针在任意一个节点开始追赶slow指针。

会发现最终都是这种情况, 如下图:

142环形链表1

fast和slow各自再走一步, fast和slow就相遇了

这是因为fast是走两步,slow是走一步,其实相对于slow来说,fast是一个节点一个节点的靠近slow的,所以fast一定可以和slow重合。

动画如下:

141.环形链表

如果有环,如何找到这个环的入口

此时已经可以判断链表是否有环了,那么接下来要找这个环的入口了。

假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。 如图所示:

142环形链表2

那么相遇时: 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同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。

动画如下:

142.环形链表II(求入口)

那么 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)中如下的地方:

142环形链表5

首先slow进环的时候,fast一定是先进环来了。

如果slow进环入口,fast也在环入口,那么把这个环展开成直线,就是如下图的样子:

142环形链表3

可以看出如果slow 和 fast同时在环入口开始走,一定会在环入口3相遇,slow走了一圈,fast走了两圈。

重点来了,slow进环的时候,fast一定是在环的任意一个位置,如图:

142环形链表4

那么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

};

posted @   青川薄  阅读(1082)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
点击右上角即可分享
微信分享提示