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\] 区间内的元素代表"还需要继续判断的范围"
 
参考文档
代码实现
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 在不同段,就能直接确定相对位置
 - 如果在同一段,就按普通二分处理
 
 
参考文档
代码实现
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;
}