数据结构与算法之二分查找部分(一)

        二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。(百度百科)

        接下来我们通过一个例子来近一步了解二分查找

leetCode 35.搜索插入位置

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

输入: nums = [1,3,5,6], target = 5

输出: 2

输入: nums = [1,3,5,6], target = 2

输出: 1

输入: nums = [1,3,5,6], target = 0

输出: 0

        首先我们来分析,该题如何使用到了二分查找,因为数组是单调递增的,所以,我们可以通过mid=(left+right)/2来将数组分为左半区间和右半区间,并通过nums[mid]和target的大小来确定target所属的位置在数组的左半区间还是右半区间,并不断地二分数组,直至找到正确的区间

        按照这个思路,在对比nums[mid]和target的时候,有以下三种情况:

        1.nums[mid] = target

        这种情况下,我们找到了目标值,返回其对应的索引mid即可

        2.nums[mid] > target

        这种情况下,说明target位于左区间,因此我们接下来要在左区间进行查找,因此将right设为mid-1,这样在下一次二分中,我们二分的区间就是0 ~ mid-1即左区间

        3.nums[mid] < target

        这种情况下,说明target位于右区间,因此我们接下来要在右区间进行查找,因此将left设为mid+1,这样在下一次二分中,我们二分的区间就是mid+1 ~ nums.length-1即右区间

        最后一个问题,我们什么时候停止二分呢?这个终止条件其实要看情况进行分析。就本题而言,退出二分的条件应该是left>right,因为左边界超过右边界时,对于这样的数组已经没有意义,也没法二分了。

按照这个思路,代码如下:

public int searchInsert(int[] nums, int target) {
        //数组为空时,target必然不存在
    if(nums==null || nums.length==0){
        return -1;
    }
    int left=0,right=nums.length-1;
    while(left<=right){
        int mid= left+((right-left)>>1);
        //找到target
        if(nums[mid]==target){
            return mid;
        }
        //target位于左半区间
        else if(nums[mid]>target){
            right=mid-1;
        }
        //target位于右半区间
        else if(nums[mid]<target){
            left=mid+1;
        }
    } 
    return left;
}

        还需要额外说明的是,如果原数组中没有目标元素,退出循环前的最后一步,必然有left=right,但target未知,执行的操作可能是left=mid+1也可能是right=mid-1,但无论如何返回的一定是较大的那个。因为对于nums[mid] < target这种情况,target>nums[i] 所以此时,应返回较大的Left,表示插入位置应位于nums[i]后面,对于nums[mid] > target,target<nums[i] 此时right-1,left较大,应返回left,表明插入位置应位于nums[i]处。这就是最后返回的是left和right里面的最大值的原因。

        接下来在此基础上,我们再对二分查找做近一步地拓展。

        leetCode34.查找元素第一个和最后一个位置

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

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

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

        本题的大致思路与35题类似,但是有一个地方不一样,那就是数组虽然是有序的,但元素可以重复出现,也就是说,当target = nums[mid]时,这个时候找到的mid并不一定就是第一个元素或者最后一个元素。

        既然如此,我们需要对这种情况进行处理,对于查找第一个出现的元素的索引的情况来说,我们可以在target = nums[mid]时,将right 设为 mid继续进行二分查找。这样缩小了查找区间,又不会丢掉我们要查找的目标值。同时,应将循环条件改为left=right时退出,这样,我们只需要检查退出循环时的nums[left]是否等于target,即可判断出target是否在nums中出现过。如果出现,必然返回的是第一次出现的索引,因为二分查找缩小的是右区间。对于查找最后一个出现的索引,同样的方法,令left=mid,即缩小左区间,根据返回值判断target是否出现过,若出现过,返回left,没出现,则返回-1

        按照这个思路,具体代码如下:

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


}
//找出元素出现的第一个位置 
public int findFirstPosition(int[] nums, int target){
    int left=0;
    int right=nums.length-1;
    while (left<right){
        int mid=(left +right)/2;
        if(nums[mid]>target){
            right=mid-1;
        }
        else if(nums[mid]<target){
            left=mid+1;
        }
        else {
            right=mid;
        }
    }
    if (nums[left]==target) {
        return left;
    }
    return -1;
}
//找出元素最后一个出现的位置
public int findLastPosition(int[] nums, int target){
    int left=0;
    int right=nums.length-1;
    while (left<right){
        int mid=(left +right+1)/2;
        if(nums[mid]>target){
            right=mid-1;
        }
        else if(nums[mid]<target){
            left=mid+1;
        }
        else {
            left=mid;
        }
    }
    if (nums[left]==target){
        return left;
    }
    return -1;
}

找出第一个大于目标元素的索引 

nums = {1,3,5,5,6,6,8,9,11} target = 7

输出:6

      大体思路仍是判断nums[mid]和target的大小然后进行区间缩减的操作,但这里要对nums[mid]>target的情况进行判断,因为nums[mid-1]若<=target,mid-1 ~ mid才是我们要找的目标区间,无脑让right =mid -1可能会丢掉正确的区间

        同时注意对于等于的情况应放在小于里面进行判断,我们要找的是第一个大于目标元素的索引,等于的情况也依然要舍弃相应区间。

public static int lowBoundnum(int[] nums,int target,int left, int right) {

        while (left <= right) {
            int mid = left + ((right - left) >> 1);
               if (nums[mid] > target) {
                  if (mid == 0 || nums[mid-1] <= target) {
                    return mid;
                }
                else{
                    right = mid -1;
                }

            } else if (nums[mid] <= target){
                left = mid + 1;
            }
        }
         return -1;
    }

第一个小于目标元素的索引

        与前面的找出第一个大于目标值的索引相同,应对target<nums[mid]的情况进行判断,若nums[mid+1]>=target,则此区间为目标区间,不能无脑left=mid+1,可能丢掉目标区间。

        同理,等于的情况放入大于中去,因为要找的是最后一个小于target的索引,等于的情况要舍弃区间。

public static int upperBoundnum(int[] nums,int target,int left, int right) {

        while (left <= right) {

            int mid = left + ((right - left) >> 1);
               if (nums[mid] < target) {
                 if (mid == right || nums[mid+1] >= target) {
                    return mid;
                }
                else{
                    left = mid + 1;
                }

            } else if (nums[mid] >= target){
                right = mid - 1;
            }
        }
             return -1;
    }

        

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值