Q55- code34- 在排序数组中查找元素的第一个和最后一个位置 + Q56- code33- 搜索旋转排序数组

Q55- code34- 在排序数组中查找元素的第一个和最后一个位置

实现思路

1 方法1:二分查找

1 实现思路

1.1 第1个位置:>=t的最小值 (minGt); 最后1个位置: <=t的最大值 (maxLt)

1.2 所有大小关系的整数查找,都可以转化为 minGt

  • maxLt,可以转化为 >= (t + 1)的最小值 - 1,即 minGt(t + 1) - 1
  • t的 最小值,可以转化为 >= (t + 1)的最小值,即 minGt(t + 1)

  • < t的 最大值,可以转化为 >= t的最小值 - 1,即 minGt(t) - 1

2.1 二分法里的区间,其含义是

  • 区间内的,表示的是 还未处理的的元素
  • 区间外的,表示的是 已经确定处理过的元素
  • 所以开闭区间,表示的就是【当前还未处理的元素】,是否 不包括/包括 这个idx的值

2.2 区间维护的本质

  • 二分查找的核心就是不断缩小"待查找区间"

  • left, right\] 区间内的元素代表"还需要继续判断的范围"

参考文档

01- 方法1参考文档

代码实现

1.1 方法1.1- 双开区间

  • 时间复杂度:O(logn)
  • 空间复杂度:O(1)
ts 复制代码
function searchRange(nums: number[], target: number): number[] {
  const lt = minGt(nums, target);
  // 易错点1: 如果lt 超出范围,或者lt 不等于target,说明不存在
  if (lt === nums.length || nums[lt] !== target) return [-1, -1];
  // <=t的最大值 (maxLt)相当于 minGt(t+1) - 1
  const gt = minGt(nums, target + 1) - 1;
  return [lt, gt];
}

// 获取 >=t的 最小值
function minGt(nums: number[], target: number): number {
  // 左右都是开区间,从而保证整个内部区间都是未处理过的
  // 开区间说明了未处理的元素,不包括-1 和 len
  // 即 [-Infinity, -1]都 < t, [len, Infinity]都 >= t
  let l = -1, r = nums.length;
  // 如果l + 1 === r,说明此时l和r都被处理过了,此时区间内没有待处理元素
  while (l + 1 < r) {
    const mid = l + ((r - l) >> 1);
    if (nums[mid] < target) {
      l = mid;
    } else {
      r = mid;
    }
  }
  // 到此时,l必然是 < t的最后一个元素,r必然是 >= t的第1个元素
  return r;
}

1.2 方法1.2- 左闭右开区间

  • 时间复杂度:O(logn)
  • 空间复杂度:O(1)
ts 复制代码
function searchRange(nums: number[], target: number): number[] {
  const lt = minGt(nums, target);
  // 易错点1: 如果lt 超出范围,或者lt 不等于target,说明不存在
  if (lt === nums.length || nums[lt] !== target) return [-1, -1];
  // <=t的最大值 (maxLt)相当于 minGt(t+1) - 1
  const gt = minGt(nums, target + 1) - 1;
  return [lt, gt];
}

// minGt: >=t的最小值- 左闭右开区间实现
function minGt(nums: number[], target: number): number {
  // 表示 未处理的元素- 包括l; 不包括r
  let l = 0, r = nums.length;
  // 包括l,不包括r, 所有l === r时(如 [4,4)时),说明没有元素需要处理了
  // 所以循环条件是 l < r
  while (l < r) {
    const mid = (l + r) >> 1;
    if (nums[mid] < target) {
      l = mid + 1;
    } else {
      r = mid;
    }
  }
  // 任意返回l或者r都可,因为循环结束时 l === r
  return r;
}

Q56- code33- 搜索旋转排序数组

实现思路

1 方法1:开区间折纸法

1.1 原始数组的特点:

  • 一开始是完全升序的数组,比如 [1,2,3,4,5,6,7]
  • 数组中的元素互不相同(题目保证)

1.2 旋转的本质:

  • 旋转操作实际上 就是把数组从某个点切开
  • 然后把左边的部分移到右边
  • 比如在4处切开:[1,2,3,4|5,6,7] → [5,6,7|1,2,3,4]
  • 关键是:只切了一刀!

1.3 为什么一定有一半有序:

  • 旋转点(那一刀)只可能 存在于一边
  • 没有旋转点的那边,保持了原来的升序性质
  • 有旋转点的那边,被切断了升序性质
  • 所以任意时刻,把数组从中间分开:
    • 旋转点要么在左边
    • 要么在右边
    • 不可能同时在两边(因为只切了一刀)

1.4 这就导致了一个重要推论:

  • 对于任意一个二分点:
    • 如果旋转点在左边 → 右边一定有序
    • 如果旋转点在右边 → 左边一定有序

这就像是折一张纸:

  • 原始数组就像一张平整的纸(完全有序)
  • 旋转就像在某处折一下
  • 无论你在哪里横着切开这张折纸
  • 切口的一边一定是平整的(有序的)
  • 另一边可能包含折痕(旋转点)

2.1 关键思想

  • 发现了"一半必有序"的性质
  • 更妙的是发现:无序的那半边其实是一个小号的原问题
  • 所以同样的解法可以一直用下去

方法2:last比较法

1.1 本质上是把"位置关系"转化为"值关系":

  • 不需要找到分割点
  • 只需要知道 x 和 target 分别在哪一段 (相对位置关系)
  • l1段 必然都大于 l2段
    • 如果x 和 target 在不同段,就能直接确定相对位置
    • 如果在同一段,就按普通二分处理

参考文档

01- 方法2参考文档

代码实现

1 方法1- 开区间折纸法

  • 时间复杂度:O(logn)
  • 空间复杂度:O(1)
ts 复制代码
function search(nums: number[], target: number): number {
  let l = -1, r = nums.length, len = nums.length;
  while (l + 1 < r) {
    const mid = (l + r) >> 1;
    const x = nums[mid];
    if (x === target) return mid;
    // [6,7,8,9, 2,3,4]
    //        m          t = 5

    // Vmid < Vl,说明此时旋转点在左边,右侧是升序的
    // 易错点1:这里需要用nums[0]作为固定参考点 判断旋转情况
    if (x < nums[0]) {
      // 若满足条件,说明 target 一定属于右侧升序区间内
      // 否则 target 一定属于左侧区间
      target > x && target <= nums[len - 1] ? (l = mid) : (r = mid);
    } else {
      // 说明此时旋转点在右边,左侧是升序的
      // 若满足条件,说明 target 一定属于左侧升序区间内
      // 否则 target 一定属于右侧区间
      target < x && target >= nums[0] ? (r = mid) : (l = mid);
    }
  }
  return -1;
}

2.1 方法2: last比较法

  • 时间复杂度:O(logn)
  • 空间复杂度:O(1)
ts 复制代码
function search(nums: number[], target: number): number {
  // last是天然的l1 和 l2 的 分割点
  // l1的每个值 > l2的每个值
  const last = nums.at(-1);
  let l = -1, r = nums.length;
  while (l + 1 < r) {
    const mid = l + ((r - l) >> 1);
    const x = nums[mid];
    if (x === target) return mid;
    // 易错点1:需要判断等于last的情况,不然当x或者t正好为last时,会误判断所属区间
    // x属于l2; t属于l1
    if (x <= last && target > last) {
      r = mid;
    } else if (x > last && target <= last) {
      // x属于l1; t属于l2
      l = mid;
    } else {
      // x 和 taget 属于同一段,比较这2个值进行二分即可
      x < target ? (l = mid) : (r = mid);
    }
  }
  return -1;
}
相关推荐
用户66982061129825 分钟前
js今日理解 blob和arrayBuffer 二进制数据
前端·javascript
想想肿子会怎么做8 分钟前
Flutter 环境安装
前端·flutter
断竿散人9 分钟前
Node 版本管理工具全指南
前端·node.js
转转技术团队10 分钟前
「快递包裹」视角详解OSI七层模型
前端·面试
1024小神15 分钟前
Ant Design这个日期选择组件最大值最小值的坑
前端·javascript
卸任16 分钟前
Electron自制翻译工具:自动更新
前端·react.js·electron
安禅不必须山水17 分钟前
Express+Vercel+Github部署自己的Mock服务
前端
哈撒Ki20 分钟前
快速入门zod4
前端·node.js
再睡一夏就好41 分钟前
【排序算法】④堆排序
c语言·数据结构·c++·笔记·算法·排序算法
再睡一夏就好44 分钟前
【排序算法】⑥快速排序:Hoare、挖坑法、前后指针法
c语言·数据结构·经验分享·学习·算法·排序算法·学习笔记