#每日n题# 最短无序连续子数组

158 阅读2分钟

原创🌶🐤解法

对数组排序,记录每个值【应该在】的位置;维护一个左右边界,遍历原数组,遇到逆序的元素,就用 [有序数组中的位置, 原数组中的位置] 更新边界值。

var findUnsortedSubarray = function (nums) {
  if (nums.length === 1) {
    return 0;
  }
  // 这里注意 sort 会更改原数组,所以复制一个出来
  let arr = [...nums];
  let sortedNums = arr.sort((a, b) => a - b);
  let posMap = new Map();
  // 用来保存每个值的位置,因为有重复元素,value是一个数组
  for (let i in sortedNums) {
    if (!posMap.get(sortedNums[i])) {
      posMap.set(sortedNums[i], [i]);
    }else{
      posMap.get(sortedNums[i]).push(i)
    }
  }
  let left = nums.length - 1;
  let right = 0;
  let max = Number.MIN_SAFE_INTEGER;
  // 更新左右边界
  for (let i in nums) {
    if (nums[i] < max) {
      // 左边取最小值
      // 对于重复元素,我们优先考虑大的下标作为无序区间的start
      // 比如 [2, 6, 8, 2] ,实际上是 1 的位置,而不是 0 
      // [6, 8, 2, 2],用了 1 之后,再用 0
      left = Math.min(left, posMap.get(nums[i]).shift());
      right = i;
    }
    max = Math.max(max, nums[i]);
  }
  return right + left + 1 === nums.length ? 0 : right - left + 1;
};

大佬的O(n)

从左开始找升序列,从右找降序列;获得无序子序列的边界;找这个无序序列的 min 和 max,将 min 和 max 归位,归位的位置构成的区间就是结果。

var findUnsortedSubarray = function (nums) {
  let len = nums.length;
  if (len === 1) {
    return 0;
  }

  let left = 0;
  let right = nums.length - 1;
  // 寻找有序区间的边界
  for (let i = 0; i < len; i++) {
    if (nums[i] >= nums[left]) {
      left = i;
    } else {
      break;
    }
  }
  for (let j = right; j >= left; j--) {
    if (nums[j] <= nums[right]) {
      right = j;
    } else {
      break;
    }
  }
  // 数组本身有序
  if (left === right) {
    return 0;
  }

  // 找无序区间的 min 和 max
  let min = nums[left];
  let max = nums[right];
  for (let m = left; m <= right; m++) {
    min = Math.min(min, nums[m]);
    max = Math.max(max, nums[m]);
  }
  
  // 归位
  let leftFlag = 0;
  let rightFlag = 0;
  
  // 移动指针,直到最小值 min 大于等于数组中的某个值 
  // 边界为 -1 是考虑到 left = 0 的情况
  for (let i = left; i >= -1; i--) {
    if (min < nums[i]) {
      leftFlag = i;
    }
  }
  
  // 移动指针,直到最大值 max 小于等于数组中的某个值 
  for (let j = right; j <= nums.length; j++) {
    if (max > nums[j]) {
      rightFlag = j;
    }
  }
  return rightFlag - leftFlag + 1;
};

大佬最终版

再来思考一下【逆序】为何物:
逆序,说明下标为 n 的当前值需要插入到前面的序列[0, ..., n - 1]中才能让整个序列有序。那么,如果小于前面序列任意一值,就称之为【逆序】。由于阈值越大,当前值越容易小于它,所以我们应该用最大值 max 作为判断是否逆序的条件。
image.png 这样就能找到最右边的【逆序数】,再往右,数组就有序了。那么该如何获得区间长度呢?

直接算这个数到归位之间的距离?不行,因为它之前还可能有其它需要归位的数。
这个时候其实我们需要的是一个最左边的【逆序数】,再往左,就没有逆序数了。
同理,我们从右边出发,用最小值 min 来判断是否逆序。 image.png

var findUnsortedSubarray = function (nums) {
  let p2 = 0;
  let p1 = 0;
  let max = nums[0];
  let min = nums[nums.length - 1];
  for (let i = 0; i < nums.length; i++) {
    max = Math.max(max, nums[i]);
    // 发现小于 max 的时候就会赋值给 p1
    if (nums[i] < max) { 
      p1 = i;
    }
  }
  for (let i = nums.length - 1; i >= 0; i--) {
    min = Math.min(min, nums[i]);
    // 发现大于 min 的时候就会赋值给 p2
    if (nums[i] > min) {
      p2 = i;
    }
  }
  const diff = p1 - p2;
  return diff > 0 ? diff + 1 : diff;
};

咦?这怎么和【解法2】中从前后找有序序列的操作这么相似??在这个用例里面直接right - left + 1就可以啊。
但如果是 [2, 3, 4, 2, 5],这两种边界就不会重合。一个是[3, 3],一个是[1, 3]
本质还是一个问题:【解法2】中,无法知道中间的乱序序列会如何影响到两边的序列。而【解法3】中,p1 和 p2 边界是和两边序列的 maxmin 比较过的,所以能决定边界。