Skip to content

Latest commit

 

History

History
391 lines (339 loc) · 12.1 KB

二分搜索.md

File metadata and controls

391 lines (339 loc) · 12.1 KB

二分搜索

二分搜索模板

给一个有序数组和目标值,找第一次/最后一次/任何一次出现的索引,如果没有出现返回 -1

模板四点要素

  • 1、初始化:start=0、end=len-1
  • 2、循环条件:start <= end
  • 3、比较中点和目标值:A[mid] ==、 <、> target
  • 4、判断最后两个元素是否符合:A[start]、A[end] ? target

时间复杂度 O(logn),使用场景一般是有序数组的查找。

// 二分搜索最常用模板
var search = function(nums, target) {
    let left = 0, mid = 0, right = nums.length-1;
    while(left + 1 < right){
        mid = left + ((right-left) >> 1);  // 考虑到 start + end 数据溢出的情况
        if(nums[mid] === target){
            return mid;
        }
        if(nums[mid] > target){
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
 	// 最后剩下两个元素,手动判断
    if (nums[left] === target) {
        return left;
    }
    if (nums[right] === target) {
        return right;
    }
    return -1;
};

大部分二分查找类的题目都可以用这个模板,然后做一点特殊逻辑即可。

模板 #1 (left <= right)

二分查找的最基础和最基本的形式。 查找条件可以在不与元素的两侧进行比较的情况下确定(或使用它周围的特定元素)。 不需要后处理,因为每一步中,你都在检查是否找到了元素。如果到达末尾,则知道未找到该元素。

模板 #2 (left < right)

一种实现二分查找的高级方法。 查找条件需要访问元素的直接右邻居。 使用元素的右邻居来确定是否满足条件,并决定是向左还是向右。 保证查找空间在每一步中至少有 2 个元素。 需要进行后处理。 当你剩下 1 个元素时,循环 / 递归结束。 需要评估剩余元素是否符合条件。

模板 #3 (left + 1 < right)

实现二分查找的另一种方法。 搜索条件需要访问元素的直接左右邻居。 使用元素的邻居来确定它是向右还是向左。 保证查找空间在每个步骤中至少有 3 个元素。 需要进行后处理。 当剩下 2 个元素时,循环 / 递归结束。 需要评估其余元素是否符合条件。

如果是最简单的二分搜索,不需要找第一个、最后一个位置、或者是没有重复元素,可以使用模板#1,代码更简洁:

var search = function(nums, target) {
    let start = 0, mid = 0, end = nums.length-1;
    while(start <= end){
        mid = start + ((end-start)>>1);
        if(nums[mid] === target){
            return mid;
        }
        if(nums[mid] > target){
            end = mid - 1;
        } else {
            start = mid + 1;
        }
    }
    return -1;
};

JS 用 indexOf 也可以:

var search = function(nums, target) {
    return nums.indexOf(target);
};

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值 target,返回 [-1, -1]。

进阶:

  • 你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?

输入:nums = [5,7,7,8,8,10], 输出:[3,4]

var searchRange = function(nums, target) {
    if(target<nums[0] || target>nums[nums.length-1]){
        return [-1,-1];
    }
    let res = [-1,-1];
    let left = 0, mid = 0, right = nums.length-1;
    while(left <= right){
        mid = left + ((right-left) >> 1);
        if(nums[mid] >= target){
            if(mid === 0 || (nums[mid] === target && nums[mid-1]<target)){
                res[0] = mid;
                break;
            }else{
                right = mid - 1;
            }
        }else{
            left = mid + 1;
        }
    }
    if(res[0]!==-1 && nums[res[0]]===target){
        if(res[0]===nums.length-1){
            res[1] = res[0];
        } else {
            for(let i=res[0]+1;i<nums.length;i++){
                if(nums[i] !== target){
                    res[1] = i-1;
                    break;
                }
                if(i===nums.length-1 && nums[i]===target){
                    res[1] = i;
                }
            }
        }
    }
    return res;
};
var searchRange = function(nums, target) {
    return [nums.indexOf(target),nums.lastIndexOf(target)]; //使用内置方法,运行更快
};

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

var searchInsert = function (nums, target) {
    let left = 0, mid = 0, right = nums.length - 1
    let res = 0
    while (left + 1 < right) {
        mid = left + ((right - left) >> 1)
        if (nums[mid] < target) {
            left = mid
        } else {
            right = mid
        }
    }
    if (target <= nums[left]) {
        return left
    } else if (target <= nums[right]) {
        return right
    } else {
        return right + 1
    }
};

编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。

var searchMatrix = function (matrix, target) {
    let row = matrix.length
    let col = matrix[0].length
    let start = 0
    let end = row * col - 1
    let mid = 0
    while (start + 1 < end) {
        mid = start + ((end - start) >> 1)
        val = matrix[parseInt(mid / col)][mid % col]
        if (val > target) {
            end = mid
        } else if (val < target) {
            start = mid
        } else {
            return true
        }
    }
    if (matrix[parseInt(start / col)][start % col] === target || matrix[parseInt(end / col)][end % col] === target) {
        return true
    }
    return false
};

假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。 你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。

var solution = function (isBadVersion) {
    /**
     * @param {integer} n Total versions
     * @return {integer} The first bad version
     */
    return function (n) {
        let start = 1
        let end = n
        let mid = 1
        while (start + 1 < end) {
            mid = start + ((end - start) >> 1)
            if(isBadVersion(mid)){
                end = mid
            } else {
                start = mid
            }
        }
        if(isBadVersion(start)){
            return start
        }
        return end
    };
};

假设按照升序排序的数组在预先未知的某个点上进行了旋转( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。 请找出其中最小的元素。

var findMin = function (nums) {
    let start = 0
    let end = nums.length - 1
    let mid = 0
    while (start + 1 < end) {
        let mid = start + ((end - start) >> 1)
        if (nums[mid] > nums[end]) { // 以最后的为target
            start = mid
        } else {
            end = mid
        }
    }
    if (nums[start] > nums[end]) {
        return nums[end]
    }
    return nums[start]
};

给你一个可能存在 重复 元素值的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。

var findMin = function (nums) {
   let start = 0
   let end = nums.length - 1
   let mid = 0
   while (start + 1 < end) {
       while (start < end && nums[start] === nums[start + 1]) {
           start++
       }
       while (start < end && nums[end] === nums[end - 1]) {
           end--
       }
       let mid = start + ((end - start) >> 1)
       if (nums[mid] >= nums[end]) {
           start = mid
       } else {
           end = mid
       }
   }
   if(nums[start] > nums[end]){
       return nums[end]
   }
   return nums[start]
};

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。

var search = function (nums, target) {
    let start = 0
    let end = nums.length - 1
    let mid = 0
    while (start + 1 < end) {
        let mid = start + ((end - start) >> 1)
        if (nums[mid] === target) {
            return mid
        }
        if (nums[start] < nums[mid]) {
            if (nums[start] <= target && target <= nums[mid]) {
                end = mid
            } else {
                start = mid
            }
        }
        if (nums[end] > nums[mid]) {
            if (nums[end] >= target && nums[mid] <= target) {
                start = mid
            } else {
                end = mid
            }
        }
    }
    if (nums[start] === target) {
        return start
    } else if (nums[end] === target) {
        return end
    }
    return -1
};

同上,数字可重复。

var search = function (nums, target) {
    let start = 0
    let end = nums.length - 1
    let mid = 0
    while (start + 1 < end) {
        let mid = start + ((end - start) >> 1)
        if (nums[mid] === target) {
            return true
        }
        while (start < end && nums[start] === nums[start + 1]) {
            start++
        }
        while (start < end && nums[end] === nums[end - 1]) {
            end--
        }
        if (nums[start] < nums[mid]) {
            if (nums[start] <= target && target <= nums[mid]) {
                end = mid
            } else {
                start = mid
            }
        }
        if (nums[end] > nums[mid]) {
            if (nums[end] >= target && nums[mid] <= target) {
                start = mid
            } else {
                end = mid
            }
        }
    }
    if (nums[start] === target || nums[end] === target) {
        return true
    }
    return false
};

总结

二分搜索核心四点要素(必背&理解)

  • 1、初始化:start=0、end=len-1
  • 2、循环退出条件:start + 1 < end
  • 3、比较中点和目标值:A[mid] ==、 <、> target
  • 4、判断最后两个元素是否符合:A[start]、A[end] ? target

练习题