数据结构与算法之二分查找算法

175 阅读1分钟

二分查找针对的是一个有序的数据集合。

二分查找非常高效,时间复杂度是 O( logn )。

假设数据大小是 n,每次查找后数据都会缩小为原来的一半,也就是会除以 2。最坏情况下,直到查找区间被缩小为空,才停止。


二分查找模板:

function search(nums, target) {
  let left = 0;
  let right = nums.length - 1;
  while (left <= right) {
    let mid = left + ((right - left) >> 1);
    if (nums[mid] === target) { // 刚好是二分中点
      return mid; 
    } else { // 否则缩小查找范围
      if (nums[mid] > target) {
        right = mid - 1;
      } else {
        left = mid + 1;
      }
    }
  }
  return -1;
}

注意以下三点:

  1. 求中点的写法应是: mid = left + ((right - left) >> 1)

    • mid = ( left + right )/ 2 写法的弊端:溢出,如果 left 和 right 比较大的话,两者之和就有可能会溢出。

    • 用位运算代替除法运算,性能更好。

  2. 循环退出的条件,是 left <= right,而不是 left < right

  3. left 和 right 的更新,left = mid + 1, right = mid - 1

    • 注意这里的 +1 和 -1,如果直接写成 left = mid 或者 high = mid,就可能会发生死循环。比如,当 left = 3,right = 3 时,如果 nums[3] 不等于 target,就会导致一直循环不退出。

纯粹的二分查找很简单,相对较难的是二分查找的变形问题

如果数组中出现多个目标值,要求找出第一个或者最后一个。

如有序数组[1, 2, 3, 3, 3, 3, 7, 8],请找出第一个等于 3 的数组元素下标。

  • 求第一个的模板

    找到 target 时检查边界情况,是否为第一个元素?前一个元素是否为 target?

    对于符合边界判断的结果则返回,否则继续缩小搜索范围。

    while (left <= right) {
      let mid = left + ((right - left) >> 1);
      if (nums[mid] === target) {
        /* 找到 target 时,需要检查边界情况 */
        if (mid === 0 || nums[mid - 1] !== target) return mid;
        else right = mid - 1;
      } else if (nums[mid] < target) {
        left = mid + 1;
      } else {
        right = mid - 1;
      }
    }
    
  • 求最后一个的模板

    找到 target 时检查边界情况,是否为最后一个元素?后一个元素是否为 target?

    对于符合边界判断的结果则返回,否则继续缩小搜索范围。

    while (left <= right) {
      let mid = left + ((right - left) >> 1);
      if (nums[mid] === target) {
        /* 找到 target 时,需要检查边界情况 */
        if (mid === nums.length - 1 || nums[mid + 1] !== target) return mid;
        else left = mid + 1;
      } else if (nums[mid] < target) {
        left = mid + 1;
      } else {
        right = mid - 1;
      }
    }
    

两道 leetcode 题目练习巩固:

  • 33. 搜索旋转排序数组

    const search = function(nums, target) {
      let left = 0;
      let right = nums.length - 1;
      while (left <= right) {
        let mid = left + ((right - left) >> 1);
        if (nums[middle] === target) return middle;
        // 否则缩小查找范围
        if (nums[left] <= nums[middle]) { // 左边有序
          if (nums[left] <= target && nums[middle] >= target) { // 在左
            right = middle;
          } else { // 在右
            left = middle + 1;
          }
        } else { // 右边有序
          if (nums[middle + 1] <= target && nums[right] >= target) {
            left = middle + 1;
          } else {
            right = middle;
          }
        }
      }
      return -1;
    };
    
  • 34. 在排序数组中查找元素的第一个和最后一个位置

    const searchRange = function(nums, target) {
      return [searchFrist(nums, target), searchLast(nums, target)];
    };
    ​
    const searchFrist = function(nums, target) {
      let left = 0;
      let right = nums.length - 1;
      while (left <= right) {
        let mid = left + ((right - left) >> 1);
        if (nums[mid] === target) {
          // 找到 target 时,需要检查边界情况
          if (mid === 0 || nums[mid - 1] !== target) return mid;
          else right = mid - 1;
        } else if (nums[mid] < target) {
          left = mid + 1;
        } else {
          right = mid - 1;
        }
      }
      return -1;
    }
    ​
    const searchLast = function(nums, target) {
      let left = 0;
      let right = nums.length - 1;
      while (left <= right) {
        let mid = left + ((right - left) >> 1);
        if (nums[mid] === target) {
          if (mid === nums.length - 1 || nums[mid + 1] !== target) return mid;
          else left = mid + 1;
        } else if (nums[mid] < target) {
          left = mid + 1;
        } else {
          right = mid - 1;
        }
      }
      return -1;
    }