Leetcode刷题总结——二分法

451 阅读4分钟

关于二分法,推荐去看这篇文章,解释的非常清楚了。
这里只是用自己的方法做一个总结,记录下我个人的理解。

1. 二分法求值

这个是最简单的情况了,在数组source中寻找是否存在目标值target。如果存在返回索引,如果不存在返回-1。
解决思路就是,在source数组的索引 [0, source.length() - 1] 中,使用中间索引mid对应的值和target进行比较;

  • 如果区间的长度为0,则返回-1;
  • 如果mid对应的值等于target,则直接返回索引mid;
  • 如果mid对应的值比target大,则在左边的区间 [0, mid - 1] 中继续使用二分法;
  • 如果mid对应的值比target小,则在右边的区间 [mid + 1, source.length() - 1] 中继续使用二分法;

事实上,可以采用递归迭代两种方式来实现二分法,但一般采用迭代的方式来实现,因为其效率更高。

/**
 * 二分法求值
 */
public int findTarget(int[] source, int target) {
    // 如果区间的长度为0,则返回-1;
    if (source.length == 0) {
        return -1;
    }
    // 搜索区间为左闭右闭区间[left, right]
    int left = 0;
    int right = source.length - 1;
    
    // 这里循环终止的条件是left > right,即区间的长度为0
    while (left <= right) {
        int mid = (right - left) / 2 + left;
        if (source[mid] > target) {
            // 搜索区间变为[left, mid - 1]
            right = mid - 1;
        } else if (source[mid] < target) {
            // 搜索区间变为[mid + 1, right]
            left = mid + 1;
        } else {
            // 找到了,返回索引
            return mid;
        }
    }
    // 没找到,返回-1
    return -1;
}

2. 二分法求边界

不同的场景中,对边界的定义可能不同。为了方便记忆,我们先定义一下本文中边界的概念:

  • 左边界:数组source中小于target的最大索引的后一位,即第一个大于等于target的索引
  • 右边界:数组source中大于target的最小索引的前一位,即最后一个小于等于target的索引

例如,当source = {1, 2, 2, 3, 4, 6, 7, 9}; target = 2的时候:
左边界left = 1,即第一个大于等于2的索引;
右边界right = 2,即最后一个小于等于2的索引;

当source = {1, 2, 2, 3, 4, 6, 7, 9}; target = 5的时候:
左边界left = 5,即第一个大于等于5的索引;
右边界right = 4,即最后一个小于等于5的索引;
(当然left > right说明target不存在)

这样定义边界有两个好处:
一是方便记忆代码,这点在后边会有总结;
二是索引在 [left, right] 中的值都是target,区间的长度就是source中target的个数。

(1) 求左边界

第一种方法,用左闭右闭区间求解:

/**
 * 左闭右闭区间求左边界
 */
int left_bound1(int[] source, int target) {
    // 如果区间的长度为0,则返回-1;
    if (source.length == 0) {
        return -1;
    }
    // 搜索区间为左闭右闭区间[left, right]
    int left = 0;
    int right = source.length - 1;

    // 这里循环终止的条件是left = right + 1,即区间的长度为0
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (source[mid] == target) {
            // 如果索引mid处的值等于target,那么mid要么是左边界,要么在左边界的右边
            // 如果mid是在左边界的右边,可以在左区间[left, mid - 1]中寻找左边界
            // 如果mid刚好就是左边界,这样不是把区间移过了? 
            // 注意到其实循环终止的条件是left = right + 1,最后只要返回left的值即可保证正确性,如果返回right就错了
            right = mid - 1;
        } else if (source[mid] < target) {
            // 如果索引mid处的值比target小,说明需要在右区间[mid + 1, right]中寻找左边界
            left = mid + 1;
        } else if (source[mid] > target) {
            // 如果索引mid处的值比target大,说明需要在左区间[left, mid - 1]中寻找左边界
            right = mid - 1;
        }
    }
    return left;
}

第二种方法,用左闭右开区间求解:

/**
 * 左闭右开区间求左边界
 */
int left_bound(int[] source, int target) {
    // 如果区间的长度为0,则返回-1;
    if (source.length == 0) {
        return -1;
    }
    // 搜索区间为左闭右开区间[left, right)
    int left = 0;
    int right = source.length;

    // 这里循环终止的条件是left == right,即区间的长度为0
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (source[mid] == target) {
            // 如果索引mid处的值等于target,那么mid要么是左边界,要么在左边界的右边
            // 如果mid是在左边界的右边,可以在左区间[left, mid)中寻找左边界
            // 如果mid刚好就是左边界,这样不是把区间移过了?
            // 事实上,由于区间右侧为开,所以看似移过了,其实right的值还是在左边界上
            // 注意到其实循环终止的条件是left = right,最后无论返回left还是right都行
            right = mid;
        } else if (source[mid] < target) {
            // 如果索引mid处的值比target小,说明需要在右区间[mid + 1, right)中寻找左边界
            left = mid + 1;
        } else if (source[mid] > target) {
            // 如果索引mid处的值比target大,说明需要在左区间[left, mid)中寻找左边界
            right = mid;
        }
    }
    return left;
}

(2) 求右边界

第一种方法,用左闭右闭区间求解:

/**
 * 左闭右闭区间求右边界
 */
int right_bound(int[] source, int target) {
    // 如果区间的长度为0,则返回-1;
    if (source.length == 0) {
        return -1;
    }
    // 搜索区间为左闭右闭区间[left, right]
    int left = 0;
    int right = source.length - 1;

    // 这里循环终止的条件是left = right + 1,即区间的长度为0
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (source[mid] == target) {
            // 如果索引mid处的值等于target,那么mid要么是右边界,要么在右边界的左边
            // 如果mid是在右边界的左边,可以在右区间[mid + 1, right]中寻找右边界
            // 如果mid刚好就是右边界,这样不是把区间移过了?
            // 注意到其实循环终止的条件是right = left - 1,最后只要返回right的值即可保证正确性,如果返回left就错了
            left = mid + 1;
        } else if (source[mid] < target) {
            // 如果索引mid处的值比target小,说明需要在右区间[mid + 1, right]中寻找右边界
            left = mid + 1;
        } else if (source[mid] > target) {
            // 如果索引mid处的值比target大,说明需要在左区间[left, mid - 1]中寻找右边界
            right = mid - 1;
        }
    }
    return right;
}

第二种方法,用左闭右开区间求解:

/**
 * 左闭右开区间求右边界
 */
int right_bound2(int[] source, int target) {
    // 如果区间的长度为0,则返回-1;
    if (source.length == 0) {
        return -1;
    }
    // 搜索区间为左闭右开区间[left, right]
    int left = 0;
    int right = source.length;

    // 这里循环终止的条件是left = right,即区间的长度为0
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (source[mid] == target) {
            // 如果索引mid处的值等于target,那么mid要么是右边界,要么在右边界的左边
            // 如果mid是在右边界的左边,可以在右区间[mid + 1, right)中寻找右边界
            // 如果mid刚好就是右边界,这样不是把区间移过了?
            // 是的,所以最后的结果应该-1
            // 注意到其实循环终止的条件是left = right,最后无论返回left-1还是right-1都行
            left = mid + 1;
        } else if (source[mid] < target) {
            // 如果索引mid处的值比target小,说明需要在右区间[mid + 1, right)中寻找右边界
            left = mid + 1;
        } else if (source[mid] > target) {
            // 如果索引mid处的值比target大,说明需要在左区间[left, mid)中寻找右边界
            right = mid;
        }
    }
    return left - 1;
}

总结一下:

  1. 左闭右闭区间法中二分的结果是:[left, mid - 1], [mid + 1, right]
    左闭右开区间法中二分的结果是:[left, mid), [mid + 1, right)
  2. 左闭右闭区间法中求左边界返回left,求右边界返回right
    左闭右开区间法中求左边界返回left或right都行,求右边界时返回left-1或right-1;

个人更倾向于左闭右闭区间法,这样可以和二分求值统一起来,移动左边界就mid+1,移动右边界就mid-1,求左区间就返回left,求右区间就返回right。

参考引用

二分法:github.com/sanbaideng/…