欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

关于二分查找的笔记

程序员文章站 2024-03-17 22:30:46
...

简介

做题的过程中,经常要对数组进行二分查找,二分查找主要有两种目的:

  1. 判断数组中是否有某个元素
  2. 判断元素在数组中首次出现的位置

本文列出了上述两种目的的代码,以及一些笔记。

判断存在性

判断存在性的代码比较简单,直接贴出来了:

bool
bin_search(vector<int> nums, int target) {
    int lo = 0;
    int hi = nums.size()-1;

    int mid = (lo+hi)/2;
    while (lo <= hi) {
        if (nums[mid] == target) 
            return true;
        else if (nums[mid] < target) {
            lo = mid+1;
        } else {
            hi = mid-1;
        }

        mid = (lo+hi)/2;
    }

    return false;
}

查找位置

这种类型的二分用于查找:

  1. 如果元素存在,元素的第一次出现位置
  2. 如果元素不存在,元素应该在的位置

看过《算法:第四版》的同学应该知道,这就是书中的rank方法。我之前看过书中的代码,不过“纸上得来终觉浅,绝知此事要躬行”,在我做题的时候,虽然能写出rank的代码来,思路却总是不顺畅,这里错一点那里错一点。今天终于下定决心总结一下问题所在:

  1. 变数太多,例如左端点lo什么时候更新,nums[mid]<target还是nums[mid]<=target?lo怎么更新,是lo=mid还是lo=mid+?最后的结果是lo还是hi,还是要另外判断?这些问题总是缠绕着我本来就不清醒的脑子。
  2. 情况太多

在经过思考之后,我总结出自己的两个解决方法:

  1. 一个问题可能有多种解决方式,二分也有多种实现方法,而且这些方法都是一套一套的,一套方法中的各个小步骤往往是固定搭配着用的。为了解决第一个问题,可以先定下一些不变的量,减少变数。
  2. 首先思考简化情况的方法,如果不行的话,穷尽所有情况。

下面说二分的其他两种实现。

第一种实现

int
find(vector<int> nums, int target) {
    int lo = 0;
    int hi = nums.size()-1;

    // 最终lo会指向这个数应该在的位置
    // 那么lo应该指向的是<target的数,最后mid会等于lo,lo需要越过mid
    int mid = (lo+hi)/2;
    while (lo < hi) {
        if (nums[mid] < target) {
            lo = mid+1;
        } else {
            // nums[mid] >= target
            hi = mid;
        }
        mid = (lo+hi)/2;
    }
    return lo;
}

下面记录一下我的思考过程,不确定是否对别人有帮助。
刚开始时代码模板是:

// 1
int
find(vector<int> nums, int target) {
    int lo = 0;
    int hi = nums.size()-1;
    int mid = (lo+hi)/2;

    while () {
		
        mid = (lo+hi)/2;
    }
    return ?;
}

首先,确定一下变数:最终lo要么指向target出现的第一个位置,要么指向它应该在的位置,于是代码变成了:

// 2
int
find(vector<int> nums, int target) {
    int lo = 0;
    int hi = nums.size()-1;
    int mid = (lo+hi)/2;

    while () {
		
        mid = (lo+hi)/2;
    }
    return lo;
}

现在再确定一下变数:lo指向的数小于target,hi指向的数大于等于target。
这样最终会得到一条界限,于是代码变成:

// 1
int
find(vector<int> nums, int target) {
    int lo = 0;
    int hi = nums.size()-1;
    int mid = (lo+hi)/2;

    while () {
		if (nums[mid] < target) {
			lo = mid; // 由于nums[mid]<target,所以可以保证nums[lo]总是小于target
		} else {
			hi = mid; // 同理,可以保证nums[hi] >= target
		}
        mid = (lo+hi)/2;
    }
    return lo;
}

现在先考虑一下最终的情况:

  1. nums[lo]<target; nums[hi]>=target; lo+1==hi;,这是最希望得到的情况,lo和hi分别在界限两边,此时退出循环对lo+1,然后返回lo即可。
  2. nums[lo]>=target; lo==hi;,有可能整个数组都大于等于target
  3. nums[hi]<target; lo==hi;,整个数组都小于target
    由上述三种情况,可以写出代码:
int
find(vector<int> nums, int target) {
    int lo = 0;
    int hi = nums.size()-1;
    int mid = (lo+hi)/2;

    while (lo != mid) {  // 这个条件可以保证在上面三种情况发生时退出
		if (nums[mid] < target) {
			lo = mid; // 由于nums[mid]<target,所以可以保证nums[lo]总是小于target
		} else {
			hi = mid; // 同理,可以保证nums[hi] >= target
		}
        mid = (lo+hi)/2;
    }
    if (nums[lo]<target) {
    	// 第1,3种情况
    	lo+1;
    }
    return lo; // 隐含第2种情况
}

第二种实现

下面是比较简洁的实现:

int
find(vector<int> nums, int target) {
    int lo = 0;
    int hi = nums.size()-1;

    // 最终lo会指向这个数应该在的位置
    // 那么lo应该指向的是<target的数,lo需要越过mid
    int mid = (lo+hi)/2;
    while (lo < hi) {
        if (nums[mid] < target) {
            lo = mid+1;
        } else {
            // nums[mid] >= target
            hi = mid;
        }
        mid = (lo+hi)/2;
    }
    return lo;
}
相关标签: 刷题