折半查找(69. x 的平方根、744. 寻找比目标字母大的最小字母、540. 有序数组中的单一元素、278. 第一个错误的版本、153. 寻找旋转排序数组中的最小值、34.查找元素第一和最后位置)

目录

零、二分查找介绍 

0.1 折半查找时间复杂度

0.2 mid 计算 

0.3二分查找的三种形式

一、69. x 的平方根

1.1 题目描述

1.2 代码

1.2.1 二分法

1.2.2 牛顿法

二、744. 寻找比目标字母大的最小字母

2.1 题目描述

2.2 代码

2.2.1 二分法

三、540. 有序数组中的单一元素

3.1 题目描述

3.2 代码

3.2.1 二分法

四、278. 第一个错误的版本

4.1 题目描述

4.2 代码

4.2.2 二分法

五、153. 寻找旋转排序数组中的最小值

5.1 题目描述

5.2 代码

5.2.1 数组遍历

5.2.2 二分法

六、34. 在排序数组中查找元素的第一个和最后一个位置

6.1 题目描述

6.2 代码

6.2.1 二分法


零、二分查找介绍 

0.1 折半查找时间复杂度

二分查找也称为折半查找,每次都能将查找区间减半,这种折半特性的算法时间复杂度为 O(log n)。 

0.2 mid 计算 

有两种计算中值 m 的方式:

  • m = (left + right) / 2
  • m = left + (right - left) / 2

left + right可能出现加法溢出,也就是说加法的结果大于整型能够表示的范围。但是left和 right 都为正数,因此 right - left 不会出现加法溢出问题。所以,最好使用第二种计算法方法。

若有一方赋值条件为 left=mid,或者right=mid,则循环判断条件就为while(left<right),即去掉等于号。  

0.3二分查找的三种形式

 

如果判断条件时while(left<right),则终止状况为left=right

一、69. x 的平方根

1.1 题目描述

实现 int sqrt(int x) 函数。(难度简单)

计算并返回 x 的平方根,其中 x 是非负整数。由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。

示例 1:输入: 4
输出: 2

示例 2:输入: 8
输出: 2
说明: 8 的平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。

1.2 代码

1.2.1 二分法

由于 x 平方根的整数部分ans 是满足 k^2 ≤x 的最大 k 值,因此我们可以对 k 进行二分查找,从而得到答案。

二分查找的下界为 0,上界可以粗略地设定为 x。在二分查找的每一步中,我们只需要比较中间元素mid 的平方与 x 的大小关系,并通过比较的结果调整上下界的范围。由于我们所有的运算都是整数运算,不会存在误差,因此在得到最终的答案ans 后,也就不需要再去尝试ans+1 了。

package BinarySearch;

/**
 * @author : lkw
 * @data : 2021/3/26 10:46
 * @description :69. x 的平方根
 * 二分法
 **/
public class mySqrt_02 {
    public static void main(String[] args) {
        int x=8;
        //int x=1;
        //int x=2147483647;//发生了溢出
       mySqrt(x);

    }
    public static int mySqrt(int x) {
        int left = 0;
        int right = x;
        int ans = -1;//记录<=x的最大mid值
        while (left <=right) {
            int mid = left + (right - left); //推荐使用
            if ((long) mid * mid <= x) {
                ans=mid;
                left = mid + 1;
            } else
                right = mid - 1;
        }
        System.out.println(ans);
        return ans;
    }
}

复杂度分析

  • 时间复杂度:O(logx),即为二分查找需要的次数。

  • 空间复杂度:O(1)。

 

 

1.2.2 牛顿法

牛顿法思路:https://leetcode-cn.com/problems/sqrtx/solution/er-fen-cha-zhao-niu-dun-fa-python-dai-ma-by-liweiw/

package BinarySearch;

/**
 * @author : lkw
 * @data : 2021/3/26 14:00
 * @description :69. x 的平方根
 * 直接用根号下x,发生了溢出
 * 现在使用的牛顿法
 *
 **/
public class mySqrt_01 {

    public static void main(String[] args) {
        //int x=8;
        //int x=1;
        int x=2147483647;//发生了溢出
        System.out.println(mySqrt(x));

    }
    public static int mySqrt(int x) {
                long ans = x;
                while (ans * ans > x) {
                    ans = (ans + x / ans) / 2;
                }
                return (int) ans;
            }
        }

 

二、744. 寻找比目标字母大的最小字母

2.1 题目描述

给你一个排序后的字符列表 letters ,列表中只包含小写英文字母。另给出一个目标字母 target,请你寻找在这一有序列表里比目标字母大的最小字母。(难度简单)

在比较时,字母是依序循环出现的。举个例子:如果目标字母 target = 'z' 并且字符列表为 letters = ['a', 'b'],则答案返回 'a'

示例:

输入:letters = ["c", "f", "j"]
        target = "a"
输出: "c"

输入:letters = ["c", "f", "j"]
        target = "c"
输出: "f"

输入:letters = ["c", "f", "j"]
        target = "d"
输出: "f"

输入:letters = ["c", "f", "j"]
        target = "g"
输出: "j"

输入:letters = ["c", "f", "j"]
        target = "j"
输出: "c"

输入:letters = ["c", "f", "j"]
        target = "k"
输出: "c"

提示:letters长度范围在[2, 10000]区间内。
         letters 仅由小写字母组成,最少包含两个不同的字母。
         目标字母target 是一个小写字母。

 

2.2 代码

2.2.1 二分法

package BinarySearch;

/**
 * @author : lkw
 * @data : 2021/3/27 9:15
 * @description :744. 寻找比目标字母大的最小字母(难度简单)
 * //字母是依次循环出现的
 **/
public class nextGreatestLetter_01 {
    public static void main(String[] args) {
        char[] letters = {'c','f','j'};
        char target = 'z';
        nextGreatestLetter(letters,target);
    }

    public static char nextGreatestLetter(char[] letters, char target) {
        int left= 0;int n=letters.length;
        int right =n-1;
        while (left<=right){
            int mid = left+(right-left)/2;
            if (letters[mid]<target){
                left=mid+1;
            }
            else  right=mid-1;
        }
       
        return letters[left%letters.length];
        //return left < n ? letters[left] : letters[0];


    }
}

复杂度分析

  • 时间复杂度:O(log n)。n 指的是 letters 的长度,我们只查看数组中的logn 个元素。
  • 空间复杂度:O(1)。只使用了常数个指针。

 

三、540. 有序数组中的单一元素

3.1 题目描述

给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。(难度中等)

示例 1:输入: [1,1,2,3,3,4,4,8,8]
输出: 2

示例 2:输入: [3,3,7,7,10,11,11]
输出: 10
注意: 您的方案应该在 O(log n)时间复杂度和 O(1)空间复杂度中运行。

 

3.2 代码

3.2.1 二分法

1.只查偶数位,若偶数位的后方还为自己,则说明单个元素在mid之后,否则,单个元素在mid之前,保证mid为偶数。

2.若mid为奇数,则需要将mid变为偶数,于是mid--,若改为mid++,则nums[mid] == nums[mid + 1]中mid+1可能会越界。

3. 如果 nums[mid] == nums[mid + 1],那么 index 所在的数组位置为 [mid + 2, right],此时令 left = mid + 2;如果 nums[mid] != nums[mid + 1],那么 index 所在的数组位置为 [left, mid],此时令 right = mid。(例如[1,1,2,3,3]的mid为2,此位置也可能是单个元素的位置)

4.因为 right 的赋值表达式为 right = mid,那么循环条件也就只能使用 left < right 这种形式。

解题思路:https://leetcode-cn.com/problems/single-element-in-a-sorted-array/solution/you-xu-shu-zu-zhong-de-dan-yi-yuan-su-by-leetcode/ 

package BinarySearch;

/**
 * @author : lkw
 * @data : 2021/3/27 10:05
 * @description :540. 有序数组中的单一元素(难度中等)
 *  只查偶数位,若偶数位的后方还为自己,则说明单个元素在mid之后,否则,单个元素在mid之前,保证mid为偶数
 *
 * 如果 nums[mid] == nums[mid + 1],那么 index 所在的数组位置为 [mid + 2, right],
 * 此时令 left = mid + 2;如果 nums[mid] != nums[mid + 1],那么 index 所在的数组位置为 [left, mid],此时令 right= mid。

 * 因为 right 的赋值表达式为 right= mid,那么循环条件也就只能使用 left < right这种形式。
 **/
public class singleNonDuplicate_01 {
    public static void main(String[] args) {
    //int nums[]={1,1,2,3,3,4,4,8,8};
        int nums[]={1,1,2,3,3};
    singleNonDuplicate(nums);
    }

    public static int singleNonDuplicate(int[] nums) {
int left=0;
int right= nums.length-1;
while(left<right){
    //只查偶数位,若偶数位的后方还为自己,则说明单个元素在mid之后,否则,单个元素在mid之前,保证mid为偶数
    int mid = left+(right-left)/2;
    if (mid%2==1) mid--;        //++有可能越界
    if (nums[mid]==nums[mid+1]){
        left =mid+2;
    }
    else right=mid;
}

return nums[left];
    }
}

复杂度分析

  • 时间复杂度:O(log n)。我们仅对元素的一半进行二分搜索。
  • 空间复杂度:O(1),仅用了常数的空间。

 

四、278. 第一个错误的版本

4.1 题目描述

你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。(难度简单)

假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。

你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。

示例:  给定 n = 5,并且 version = 4 是第一个错误的版本。

调用 isBadVersion(3) -> false
调用 isBadVersion(5) -> true
调用 isBadVersion(4) -> true

所以,4 是第一个错误的版本。 

 

4.2 代码

4.2.2 二分法

注意:isBadVersion(mid)==ture 说明为错误版本,返回第一个为true的版本

若mid版出错,则说明出错的在[left,mid],令right=mid。

若mid版正确,则说明出错的在[mid+1,right],令left=md+1。

因为 right 的赋值表达式为right=mid,因此循环条件为 left < right。

/**
 * @author : lkw
 * @data : 2021/3/29 8:26
 * @description :278. 第一个错误的版本(简单)
 **/
/* The isBadVersion API is defined in the parent class VersionControl.
      boolean isBadVersion(int version); */

public class Solution extends VersionControl {
    public int firstBadVersion(int n) {
         int left=1;
        int right=n;
        int mid;
        //若mid版出错,则说明出错的在[l,mid]
        //若mid版正确,则说明出错的在[mid+right]
        while(left<right){
            mid = left+(right-left)/2;
            //isBadVersion(mid)==ture 说明为错误版本
            if (!isBadVersion(mid)){
                //如果是正确版本,说明错版本在后面
                left=mid+1;
            }
            else right=mid;
        }
        return left;
    }
}
  • 时间复杂度:O(logn)。搜索空间每次减少一半,因此时间复杂度为O(logn)。
  • 空间复杂度:O(1)。

 

五、153. 寻找旋转排序数组中的最小值

5.1 题目描述

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

请找出其中最小的元素。

示例 1:输入:nums = [3,4,5,1,2]
输出:1


示例 2:输入:nums = [4,5,6,7,0,1,2]
输出:0

示例 3:输入:nums = [1]
输出:1

5.2 代码

5.2.1 数组遍历

package BinarySearch;
/**
 * @author : lkw
 * @data : 2021/3/29 8:58
 * @description :153. 寻找旋转排序数组中的最小值(难度中等)
 **/
public class findMin_01 {
    public static void main(String[] args) {
        int nums[]={3,4,5,1,2};
        findMin(nums);
    }

    public static int findMin(int[] nums) {
        int len= nums.length;
        int m=nums[0];
        for (int i = 0; i <len-1 ; i++) {
            if (nums[i]>nums[i+1]){
                m=nums[i+1];
            }
        }
        return m;
    }
}

 

  • 时间复杂度:O(n),n为数组长度
  • 空间复杂度:O(1)

 

5.2.2 二分法

若mid的值小于等于右边界值,则旋转最小数字应在mid左侧区域,并且包含mid,[left,mid],因此right=mid;

若mid的值大于右边界值,则旋转最小数字在mid右侧区域,并且肯定不是mid(因为mid的值大,不是最小数组),因此区间调整为[mid+1,right],因此left=mid+1;

因为 right 的赋值表达式为right=mid,因此循环条件为 left < right。

package BinarySearch;

/**
 * @author : lkw
 * @data : 2021/3/29 9:26
 * @description :153. 寻找旋转排序数组中的最小值(难度中等)
 * 二分法
 **/
public class findMin_02 {
    public static void main(String[] args) {
        int nums[]={3,4,5,1,2};
        System.out.println(findMin(nums));
    }

    public static int findMin(int[] nums) {
        int len= nums.length;
        int left=0;
        int right=len-1;
        while(left<right){
            int mid = left+(right-left)/2;
            //mid的值小于等于右边界值,在其左侧,并且包含mid,[left,mid]
        if (nums[mid]<=nums[right]){
            right=mid;
        }//mid的值大于右边界值,则旋转最小数字在mid右侧区域,并且肯定不是mid(因为mid大,不是最小数组),因此区间调整为[mid+1,right]
        else left=mid+1;
        }
        return nums[left];
    }

}
  • 时间复杂度:O(logn)
  • 空间复杂度:O(1)

 

六、34. 在排序数组中查找元素的第一个和最后一个位置

6.1 题目描述

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。(难度中等)

如果数组中不存在目标值 target,返回 [-1, -1]。

进阶:

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

示例 1:输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

示例 2:输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]

示例 3:输入:nums = [], target = 0
输出:[-1,-1]

提示:

0 <= nums.length <= 105
-109 <= nums[i] <= 109
nums 是一个非递减数组
-109 <= target <= 109

 

6.2 代码

6.2.1 二分法

二分法的三种写法:

https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/solution/da-jia-bu-yao-kan-labuladong-de-jie-fa-fei-chang-2/

1. 第一次出现的位置和最后出现的位置由两个函数书写:

两个函数的思路https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/solution/da-jia-bu-yao-kan-labuladong-de-jie-fa-fei-chang-2/

import java.util.Arrays;

public class Solution {

    public int[] searchRange(int[] nums, int target) {
        if (nums.length == 0) {
            return new int[]{-1, -1};
        }
        int firstPosition = findFirstPosition(nums, target);
        int lastPosition = findLastPosition(nums, target);
        return new int[]{firstPosition, lastPosition};
    }


    private int findFirstPosition(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;

            if (nums[mid] == target) {
                // ① 不可以直接返回,应该继续向左边找,即 [left, mid - 1] 区间里找
                right = mid - 1;
            } else if (nums[mid] < target) {
                // 应该继续向右边找,即 [mid + 1, right] 区间里找
                left = mid + 1;
            } else {
                // 此时 nums[mid] > target,应该继续向左边找,即 [left, mid - 1] 区间里找
                right = mid - 1;
            }
        }

        // 此时 left 和 right 的位置关系是 [right, left],注意上面的 ①,此时 left 才是第 1 次元素出现的位置
        // 因此还需要特别做一次判断
        if (left != nums.length && nums[left] == target) {
            return left;
        }
        return -1;
    }


    private int findLastPosition(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;

        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                // 只有这里不一样:不可以直接返回,应该继续向右边找,即 [mid + 1, right] 区间里找
                left = mid + 1;
            } else if (nums[mid] < target) {
                // 应该继续向右边找,即 [mid + 1, right] 区间里找
                left = mid + 1;
            } else {
                // 此时 nums[mid] > target,应该继续向左边找,即 [left, mid - 1] 区间里找
                right = mid - 1;
            }
        }

        if (right != -1 && nums[right] == target) {
            return right;
        }
        return -1;
    }
}

找第一个位置和最后一个位置的区别仅仅在于nums[mid] == target:

找第一个位置

if (nums[mid] == target) {
            // ① 不可以直接返回,应该继续向左边找,即 [left..mid - 1] 区间里找
            right = mid - 1;

最后一个位置:

if (nums[mid] == target) {
            // 只有这里不一样:不可以直接返回,应该继续向右边找,即 [mid + 1, right] 区间里找
            left = mid + 1;

因此可以将两个函数进行合并。

 

2.合并两个函数: 

合并思路:https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/solution/zai-pai-xu-shu-zu-zhong-cha-zhao-yuan-su-de-di-3-4/

package BinarySearch;

/**
 * @author : lkw
 * @data : 2021/3/29 9:53
 * @description :34. 在排序数组中查找元素的第一个和最后一个位置(难度中等)
 **/
public class searchRange_01 {
    public static void main(String[] args) {
        int[] nums = {5,7,7,8,8,10};
        int tatget= 8;
        System.out.println(searchRange(nums, tatget));
    }

        public static int[] searchRange(int[] nums, int target) {
            int leftIdx = binarySearch(nums, target, true);//找第一个出现的
            int rightIdx = binarySearch(nums, target, false) - 1;//找最后一位
            if (leftIdx <= rightIdx && rightIdx < nums.length && nums[leftIdx] == target && nums[rightIdx] == target) {
                //判断找的的是否在数组内,找到的是否是目标元素
                return new int[]{leftIdx, rightIdx};
            }
            return new int[]{-1, -1};
        }

        public static int binarySearch(int[] nums, int target, boolean lower) {
            int left = 0, right = nums.length - 1;
            int ans = nums.length;//??
            while (left <= right) {
              int mid = left+(right-left ) / 2;
                if (nums[mid] > target || (lower && nums[mid]== target)) {
                    //nums[mid] > target或者lower==true且nums[mid]= target进入循环(即找左边界)
                    right = mid - 1;
                    ans = mid;
                } else {
                    //nums[mid] < target的情况,或者找lower==false且nums[mid]= target(即找右边界)
                    left = mid + 1;
                }
            }
            return ans;
        }
    }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值