算法——二分查找

算法——二分查找

  • 一、二分查找的框架
    • 1.基本二分查找使用条件:
    • 2.递归方式
    • 3.迭代方式
  • 二、基本二分查找的两种写法
    • 1.定义target在闭区间[left,right]
    • 2.定义target在左闭右开区间[left,right)
  • 三、寻找边界的二分查找
    • 1.寻找左侧边界的二分查找
      • 写法1
      • 写法2
    • 2.寻找右侧边界的二分查找
      • 写法1
      • 写法2
  • 四、二分查找问题的泛化
  • 五、用二分查找优化算法效率

一、二分查找的框架

1.基本二分查找使用条件:

  1. 数组为有序数组
  2. 数组中无重复元素(一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,需要使用寻找边界的二分查找)

二分查找的时间复杂度:O(logn)
假设如果数组有n个元素,切分的次数为k,每次都切一半,也就是 n / (2^k) = 1,转换公式为 2^k = n,那么k就是log2N,所以时间复杂度为O(logn)。

2.递归方式

public int search(int nums[], int start, int end, int target) {
    if(end >= start) {
        int mid = start + (end - start) / 2;
        if(nums[mid] == target) {
            return mid;
        } else if (target < nums[mid]) {
            return search(nums, start, mid - 1, target);
        } 
        return search(nums, mid + 1, end, target);
    }
    return -1;
}

3.迭代方式

int binarySearch(int[] nums, int target) {
    int left = 0, right = ...;

    while(...) {
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
            ...
        } else if (nums[mid] < target) {
            left = ...
        } else if (nums[mid] > target) {
            right = ...
        }
    }
    return ...;
}

分析二分查找技巧:尽量不要出现 else,把所有情况用 else if 写清楚,这样可以清楚地展现所有细节。“……”的地方是要注意的细节

int mid = left + (right - left) / 2(left + right) / 2相同,但可防止left与right相加太大导致溢出

二、基本二分查找的两种写法

1.定义target在闭区间[left,right]

  • right赋值为nums.length - 1,区间[left,right]与数组长度相等
  • while (left <= right)要使用 <= ,因为left == right是有意义的,循环的终止条件是left == right + 1所以使用 <=
  • if (nums[middle] > target) right要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1
int binarySearch(int[] nums, int target) {
    int left = 0; 
    int right = nums.length - 1; // 定义target在左闭右闭的区间里,[left, right]

    while(left <= right) {
        int mid = left + (right - left) / 2;
        if(nums[mid] == target)
            return mid;  // 数组中找到目标值,直接返回下标
        else if (nums[mid] < target)
            left = mid + 1; // target 在右区间,所以[middle + 1, right]
        else if (nums[mid] > target)
            right = mid - 1; // target 在左区间,所以[left, middle - 1]
    }
     // 未找到目标值,返回-1
    return -1;
}

2.定义target在左闭右开区间[left,right)

  • right赋值为nums.length,区间[left,right)与右端已超出数组长度,所以是开区间
  • while (left < right)要使用 < ,因为left == right在区间[left, right)是没有意义的,循环的终止条件就是left == right所以使用 <
  • if (nums[middle] > target) right要赋值为 middle,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle](nums[middle]处于开区间边界)
int binarySearch(int[] nums, int target) {
    int left = 0; 
    int right = nums.length; // 定义target在左闭右开的区间里,[left, right)

    while(left < right) {
        int mid = left + (right - left) / 2;
        if(nums[mid] == target)
            return mid;  // 数组中找到目标值,直接返回下标
        else if (nums[mid] < target)
            left = mid + 1; // target 在右区间,所以[middle + 1, right]
        else if (nums[mid] > target)
            right = mid; // target 在左区间,在[left, middle)中
    }
     // 未找到目标值,返回-1
    return -1;
}

三、寻找边界的二分查找

当有重复元素时,如果有多个目标元素,找到目标元素的边界位置(返回最大/最小的索引)

1.寻找左侧边界的二分查找

  • 找边界关键在于对于 nums[mid] == target 这种情况的处理:找到 target 时不要立即返回,而是缩小「搜索区间」的上界 right,在区间 [left, mid) 中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。

  • 由于 while 的退出条件是 left == right + 1,所以当 target 比 nums 中所有元素都大时,会存在left > nums.length,因此,最后返回结果的代码应该检查越界情况

写法1

int left_bound(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    // 搜索区间为 [left, right]
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            // 搜索区间变为 [mid+1, right]
            left = mid + 1;
        } else if (nums[mid] > target) {
            // 搜索区间变为 [left, mid-1]
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 收缩右侧边界
            right = mid - 1;
        }
    }
    // 检查出界情况、未找到情况,返回-1
    if (left >= nums.length || nums[left] != target)
        return -1;
    //查找到结果,最后返回输出
    return left;  //或返回right+1
}

写法2

int left_bound(int[] nums, int target) {
    if (nums.length == 0) return -1;
    int left = 0;
    int right = nums.length; 
    
    while (left < right) { 
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
            right = mid;
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid; // 注意
        }
    }
    // 检查出界,
	if (left >= nums.length) return -1;
	//
	return nums[left] == target ? left : -1;
}

2.寻找右侧边界的二分查找

缩小左侧边界,检查right越界

写法1

int right_bound(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 这里改成收缩左侧边界即可
            left = mid + 1;
        }
    }
    // 这里改为检查 right 越界的情况
    if (right < 0 || nums[right] != target)
        return -1;
    return right;
}

写法2

int right_bound(int[] nums, int target) {
    if (nums.length == 0) return -1;
    int left = 0, right = nums.length;
    
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
            left = mid + 1; // 注意
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid;
        }
    }
    //检查出界,left == 0即right<0(由循环退出条件得)
    if (left == 0)  return -1;
    //找到返回left-1,未找到返回-1
    return nums[left-1] == target ? (left-1) : -1;
}

四、二分查找问题的泛化

需要从题目中抽象出一个自变量 x(x的取值范围即为二分查找的搜索区间),一个关于 x 的函数 f(x),以及一个目标值 target。x, f(x), target 满足以下条件:

  1. f(x) 必须是在 x 上的单调函数(单调增单调减都可以)。

  2. 题目要求计算满足约束条件 f(x) == target 时的 x 的值

eg:有一个升序排列的有序数组 nums 以及一个目标元素 target,请你计算 target 在数组中的索引位置,如果有多个目标元素,返回最小的索引。

x ——数组元素的索引下标
f(x) ——升序排列的有序数组,即f(x)单调递增
target ——目标值
要求计算:满足 f(x) == target 的 x 的最小值是多少

可以使用二分查找

// 函数 f 是关于自变量 x 的单调函数
int f(int x) {
    // ...
}

// 主函数,在 f(x) == target 的约束下求 x 的最值
int solution(int[] nums, int target) {
    if (nums.length == 0) return -1;
    // 自变量 x 的最小值是多少?
    int left = ...;
    // 自变量 x 的最大值是多少?
    int right = ... + 1;
    
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (f(mid) == target) {
            // 题目是求左边界还是右边界?
            // ...
        } else if (f(mid) < target) {
            // 怎么让 f(mid) 大一点?
            // ...
        } else if (f(mid) > target) {
            // 怎么让 f(mid) 小一点?
            // ...
        }
    }
    return ...; //左边界left、右边界left - 1
}

五、用二分查找优化算法效率

题目一般都会要求算法的时间复杂度,如果是 O ( n l o g n ) O(nlogn) O(nlogn)这样存在对数的复杂度,一般都要往二分查找的方向上靠
想用二分查找技巧优化算法,首先要把 for 循环形式的暴力算法写出来

// func(i) 是 i 的单调函数(递增递减都可以)
int func(int i);
int target;
// 形如这种 for 循环可以用二分查找技巧优化效率
for (int i = 0; i < n; i++) {
    if (func(i) == target)
        return i;
}

注意观察 for 循环形式,不一定是 func(i) == target 作为终止条件,可能是 <= 或者 >= 的关系,这个可以根据具体的题目意思来推断,使用查找左侧/右侧边界的变体

你可能感兴趣的:(笔记,数据结构与算法,后端,经验分享,学习,算法,java)