二分查找【学习笔记】Java

本文详细介绍了二分查找的三种版本:基础版、改动版和平衡版,并提供了Java实现。此外,还探讨了如何在有序数组中寻找重复元素的最左和最右侧索引,以及在LeetCode相关题目中的应用。文章强调了不同版本二分查找的优化点和适用场景。
摘要由CSDN通过智能技术生成

  • 若文章内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系博主删除。
  • 本文仅做学习用途,无他用。(记录学习时刻,方便个人在线阅览)

【黑马程序员】数据结构与算法(2023 版)



基础版


需求:在 有序数组 A A A 内,查找值 target

  • 如果找到了目标值,返回索引
  • 如果找不到目标值,返回 − 1 -1 1

算法描述
前提给定一个内含 n n n 个元素的有序数组 A A A,满足 A 0 ≤ A 1 ≤ A 2 ≤ ⋯ ≤ A n − 1 A_{0}\leq A_{1}\leq A_{2}\leq \cdots \leq A_{n-1} A0A1A2An1,一个待查值 t a r g e t target target
1设置 l o w = 0 low=0 low=0 h i g h = n − 1 high=n-1 high=n1
2如果 l o w > h i g h low \gt high low>high,结束查找。表示没找到目标值
3设置 m i d d l e = f l o o r ( l o w + h i g h 2 ) middle = floor(\frac {low+high}{2}) middle=floor(2low+high) m i d d l e middle middle 为中间索引, f l o o r floor floor 是向下取整( ≤ l o w + h i g h 2 \leq \frac {low+high}{2} 2low+high 的最小整数)
4如果 t a r g e t < A m i d d l e target < A_{middle} target<Amiddle 设置 h i g h = m i d d l e − 1 high = middle - 1 high=middle1,跳到第 2 步
5如果 A m i d d l e < t a r g e t A_{middle} < target Amiddle<target 设置 l o w = m i d d l e + 1 low = middle + 1 low=middle+1,跳到第 2 步
6如果 A m i d d l e = t a r g e t A_{middle} = target Amiddle=target,结束查找,找到了

  • 强调设置 m i d d l e = f l o o r ( l o w + h i g h 2 ) middle = floor(\frac {low + high}{2}) middle=floor(2low+high)middle 为中间索引, f l o o r floor floor 是向下取整即小于等于 l o w + h i g h 2 \frac {low + high}{2} 2low+high 的最小整数

/**
 * 二分查找_基础版
 * <p></p>
 * * low、high、middle 指针都可能是查找目标
 * *  (low > high) 时表示区域内没有要找的了
 * * 每次改变 low, high 边界时, middle 已经比较过了(确定其不是目标值),
 * * 因此 low 是 (middle + 1) ,high 是 (middle - 1)
 * * 向左查找, 比较次数少
 * * 向右查找, 比较次数多
 *
 * @param a      : 	待查找的升序数组
 * @param target : 	待查找的目标值
 * @return 			找到目标值返回索引,找不到则返回 -1
 */
public static int binarySearchBasic(int[] a, int target) {
    int low = 0;
    int high = a.length - 1;

    while (low <= high) { // 证明范围里有内容
        int middle = (low + high) >>> 1;
        
        if (target < a[middle]) {
            high = middle - 1;
        } else if (a[middle] < target) {
            low = middle + 1;
        } else {
            return middle; // 找到目标值了,返回索引
        }
    }

    return -1;
}

  • l o w low low h i g h high high 对应着搜索区间 [ 0 , a . l e n g t h − 1 ] [0, a.length-1] [0,a.length1]注意此处是闭合的区间
  • l o w < = h i g h low<=high low<=high 意味着搜索区间内还有未比较的元素, l o w low low h i g h high high 指向的元素也可能是比较的目标
    • 思考:如果不加 l o w = = h i g h low==high low==high 行不行?
    • 回答:不行,因为这意味着 l o w low low h i g h high high 指向的元素会漏过比较
  • m i d d l e middle middle 对应着中间位置,中间位置左边和右边的元素可能不相等(差一个),不会影响结果
  • 如果某次未找到,那么缩小后的区间内不包含 m i d d l e middle middle

改动版(右边界不在查找范围内)


/**
 * 二分查找_改动版(右边界不在查找范围内)
 * <p></p>
 * * low、middle 指针可能是查找目标
 * * high 指针不可能是查找目标
 * * 因为 (low >= high) 时表示区域内没有要找的了
 * * 改变 low 边界时, middle 已经比较过了(确定其不是目标值), 因此需要 (low = middle + 1)
 * * 改变 high 边界时, middle 已经比较过了(不是目标), 同时因为 high 指针不可能是查找目标,所以 (high = middle)
 *
 * @param a      : 	待查找的升序数组
 * @param target : 	待查找的目标值
 * @return 			找到目标值返回索引,找不到则返回 -1
 */
public static int binarySearchAlternative(int[] a, int target) {
    int low = 0;
    int high = a.length; // 此时 high 指针指向的一定不是 target

    while (low < high) {
        int middle = (low + high) >>> 1;
        if (target < a[middle]) {
            high = middle;
        } else if (a[middle] < target) {
            low = middle + 1;
        } else {
            return middle;
        }
    }

    return -1;
}

  • l o w low low h i g h high high 对应着搜索区间 [ 0 , a . l e n g t h ) [0, a.length) [0,a.length)注意此处是左闭右开的区间
  • l o w < h i g h low<high low<high 意味着搜索区间内还有未比较的元素, h i g h high high 指向的 一定不是 查找目标
    • 思考:为啥这次不加 l o w = = h i g h low==high low==high 的条件了?
    • 回答:这回 h i g h high high 指向的不是查找目标。
      如果还加 l o w = = h i g h low==high low==high 条件,就意味着 h i g h high high 指向的还会再次比较,找不到目标值时,程序会陷入死循环
  • 如果某次要缩小右边界,那么 h i g h = m i d d l e high=middle high=middle,因为此时的 m i d d l e middle middle 已经 不是 查找目标了

平衡版(循环内缩小范围,循环外比较)


/**
 * 二分查找_平衡版
 * <p></p>
 * * 不奢望循环内通过 middle 找出目标, 缩小区间直至剩 1 个, 剩下的这个可能就是要找的(通过 low 指针)
 * * low 指针可能是查找目标
 * * high 指针不可能是查找目标
 * * 因为上面的设置. 当区域内还剩一个元素时, 我们可以用 (high - middle == 1) 来表示这种情况
 * * 改变 low 边界时, middle 可能就是目标, 同时因为 low 指针有可能是查找目标. 所以有 (low = middle)
 * * 改变 high 边界时, middle 已经比较过,其并不是目标值, 同时因为 high 指针不可能是目标值. 所以有 (high = middle)
 * <p></p>
 * * 优点:三分支改为二分支, 循环内比较次数减少
 *
 * @param a      : 	待查找的升序数组
 * @param target : 	待查找的目标值
 * @return 			找到目标值返回索引,找不到则返回 -1
 */
public static int binarySearchBalance(int[] a, int target) {
    int low = 0;
    int high = a.length;

    while (1 < high - low) { // 范围内待查找的元素个数 > 1 时
        int middle = (low + high) >>> 1;
        if (target < a[middle]) { // 目标在左边
            high = middle;
        } else { // 目标在 middle 或右边
            low = middle;
        }
    }

    return (target == a[low]) ? low : -1;
}

思想:

  • 左闭右开的区间, l o w low low 指向的可能是目标,而 h i g h high high 指向的不是目标
  • 不奢望循环内通过 m i d d l e middle middle 找出目标,缩小区间直至剩 1 个,剩下的这个可能就是要找的目标值(通过 h i g h high high
    • h i g h − l o w > 1 high - low > 1 highlow>1 的含义是: 在范围内的待比较的元素个数 > 1 在范围内的待比较的元素个数 > 1 在范围内的待比较的元素个数>1
  • 改变 l o w low low 边界时,它指向的可能是目标,因此不能 m i d d l e + 1 middle + 1 middle+1
  • 优点:循环内的平均比较次数减少了
    • 时间复杂度: Θ ( l o g ( n ) ) \Theta(log(n)) Θ(log(n))

Arrays.binarySearch()


Java 版的二分查找


  • java/util/Arrays.java
private static int binarySearch0(int[] a, int fromIndex, int toIndex, int key) {
    int low = fromIndex;
    int high = toIndex - 1;

    while (low <= high) {
        int mid = (low + high) >>> 1;
        int midVal = a[mid];

        if (midVal < key)
            low = mid + 1;
        else if (midVal > key)
            high = mid - 1;
        else
            return mid; // key found
    }
    
    return -(low + 1);  // key not found.
}

  • 例: [ 1 , 3 , 5 , 6 ] [1, 3, 5, 6] [1,3,5,6] 要插入 2 2 2,那么就是找到一个位置进行插入操作(这个位置左侧元素都比它小)
    • 等循环结束,还是没找到目标值。又因为 l o w low low 左侧元素肯定都比 t a r g e t target target 小,故 l o w low low 即插入点
  • 插入点取负是为了与找到情况区分
  • − 1 -1 1 是为了把索引 0 0 0 位置的插入点与找到的情况进行区分

  • 测试代码
public void test4() {
    int[] a = {2, 5, 8};
    int target = 4;

    int i = Arrays.binarySearch(a, target);

    System.out.println("-(插入点 + 1) = " + i); // -2 = -(插入点 + 1)

    if (i < 0) {
        int insertIndex = Math.abs(i + 1); // 真正的插入点索引
        System.out.println("插入点索引:" + insertIndex);
 
        int[] b = new int[a.length + 1];
        System.arraycopy(a, 0, b, 0, insertIndex);
        System.out.println(Arrays.toString(b));
        
        b[insertIndex] = target;
        System.arraycopy(a, insertIndex, b, insertIndex + 1, a.length - insertIndex);
        
        System.out.println(Arrays.toString(b));
    }
}
  • 控制台输出结果
-(插入点 + 1) = -2
插入点索引:1
[2, 0, 0, 0]
[2, 4, 5, 8]

LeftMost 和 RightMost


LeftMost 初版


有时我们希望返回的是最左侧的重复元素,如果用基础版的二分查找的话

  • 对于数组 [ 1 , 2 , 3 , 4 , 4 , 5 , 6 , 7 ] [1, 2, 3, 4, 4, 5, 6, 7] [1,2,3,4,4,5,6,7],查找元素 4 4 4,结果是索引 3 3 3
  • 对于数组 [ 1 , 2 , 4 , 4 , 4 , 5 , 6 , 7 ] [1, 2, 4, 4, 4, 5, 6, 7] [1,2,4,4,4,5,6,7],查找元素 4 4 4,结果也是索引 3 3 3,但这并不是最左侧的元素

/**
 * 二分查找 LeftMost_1
 * 在数组中有多个元素和目标元素相同的情况下,返回最左边的重复元素的索引
 *
 * @param a      : 	待查找的升序数组
 * @param target : 	待查找的目标值
 * @return 			找到目标值,返回重复元素的最左索引
 * 					找不目标值,到则返回 -1
 */
public static int binarySearchLeftMost_1(int[] a, int target) {
    int low = 0;
    int high = a.length - 1;
    int candidate = -1;

    while (low <= high) {
        int middle = (low + high) >>> 1;

        if (target < a[middle]) {
            high = middle - 1;
        } else if (target > a[middle]) {
            low = middle + 1;
        } else {
            // 记录候选位置
            candidate = middle;
            high = middle - 1;
        }
    }

    return candidate;
}

RightMost 初版


有时我们希望返回的是数组最左侧的重复元素


/**
 * 二分查找 RightMost_1
 * 在数组中有多个元素和目标元素相同的情况下,返回最右边的重复元素的索引
 *
 * @param a      : 	待查找的升序数组
 * @param target : 	待查找的目标值
 * @return 			找到目标值,返回重复元素的最右索引
 * 					找不目标值,到则返回 -1
 */
public static int binarySearchRightMost_1(int[] a, int target) {
    int low = 0;
    int high = a.length - 1;
    int candidate = -1;

    while (low <= high) {
        int middle = (low + high) >>> 1;

        if (target > a[middle]) {
            low = middle + 1;
        } else if (target < a[middle]) {
            high = middle - 1;
        } else {
            // 记录候选位置
            candidate = middle;
            low = middle + 1;
        }
    }
    return candidate;
}

LeftMost 改版


下面的代码只是改版之一罢了(在应用那一小节才是更加全面)

/**
 * 二分查找 LeftMost_2
 *
 * @param a      	待查找的升序数组
 * @param target 	待查找的目标值
 * @return 			返回 (>= target) 的重复元素的最左索引
 * 						找到目标值,返回与目标值相等的,最靠左的索引位置
 * 						没找到目标值,返回大于目标值,最靠左的元素的索引位置
 */
public static int binarySearchLeftMost_2(int[] a, int target) {
    int low = 0, high = a.length - 1;
    while (low <= high) {
        int middle = (low + high) >>> 1;
        if (target <= a[middle]) {
            high = middle - 1;
        } else {
            low = middle + 1;
        }
    }
    return low;
}

RightMost 改版


下面的代码只是改版之一罢了(在应用那一小节才是更加全面)

/**
 * 二分查找 RightMost_2
 *
 * @param a      : 	待查找的升序数组
 * @param target : 	待查找的目标值
 * @return 			返回 (<= target) 的重复元素的最右索引
 * 						找到了目标值,返回等于目标值的元素的最右索引
 * 						没有找到目标值的时候,它返回的是小于目标的最靠右的元素的索引
 */
public static int binarySearchRightMost_2(int[] a, int target) {
    int low = 0;
    int high = a.length - 1;

    while (low <= high) {
        int middle = (low + high) >>> 1;

        if (target >= a[middle]) {
            low = middle + 1;
        } else if (target < a[middle]) {
            high = middle - 1;
        }
    }
    return low - 1;
}

应用


在这里插入图片描述


范围查询

  • 查询 x < 4 x \lt 4 x<4 0.. l e f t m o s t ( 4 ) − 1 0 .. leftmost(4) - 1 0..leftmost(4)1
  • 查询 x ≤ 4 x \leq 4 x4 0.. r i g h t m o s t ( 4 ) 0 .. rightmost(4) 0..rightmost(4)
  • 查询 4 < x 4 \lt x 4<x r i g h t m o s t ( 4 ) + 1.. ∞ rightmost(4) + 1 .. \infty rightmost(4)+1..∞
  • 查询 4 ≤ x 4 \leq x 4x l e f t m o s t ( 4 ) . . ∞ leftmost(4) .. \infty leftmost(4)..∞
  • 查询 4 ≤ x ≤ 7 4 \leq x \leq 7 4x7 l e f t m o s t ( 4 ) . . r i g h t m o s t ( 7 ) leftmost(4) .. rightmost(7) leftmost(4)..rightmost(7)
  • 查询 4 < x < 7 4 \lt x \lt 7 4<x<7 r i g h t m o s t ( 4 ) + 1.. l e f t m o s t ( 7 ) − 1 rightmost(4) + 1 .. leftmost(7) - 1 rightmost(4)+1..leftmost(7)1

求排名 l e f t m o s t ( t a r g e t ) + 1 leftmost(target) + 1 leftmost(target)+1

  • t a r g e t target target 可以不存在。如: l e f t m o s t ( 5 ) + 1 = 6 leftmost(5)+1 = 6 leftmost(5)+1=6
  • t a r g e t target target 也可以存在。如: l e f t m o s t ( 4 ) + 1 = 3 leftmost(4)+1 = 3 leftmost(4)+1=3

求前任(predecessor) l e f t m o s t ( t a r g e t ) − 1 leftmost(target) - 1 leftmost(target)1

  • l e f t m o s t ( 3 ) − 1 = 1 leftmost(3) - 1 = 1 leftmost(3)1=1,前任 a 1 = 2 a_1 = 2 a1=2
  • l e f t m o s t ( 4 ) − 1 = 1 leftmost(4) - 1 = 1 leftmost(4)1=1,前任 a 1 = 2 a_1 = 2 a1=2

求后任(successor) r i g h t m o s t ( t a r g e t ) + 1 rightmost(target)+1 rightmost(target)+1

  • r i g h t m o s t ( 5 ) + 1 = 5 rightmost(5) + 1 = 5 rightmost(5)+1=5,后任 a 5 = 7 a_5 = 7 a5=7
  • r i g h t m o s t ( 4 ) + 1 = 5 rightmost(4) + 1 = 5 rightmost(4)+1=5,后任 a 5 = 7 a_5 = 7 a5=7

求最近邻居

  • 前任和后任距离更近者

LeetCode 部分题目


704 题:二分查找


链接https://leetcode.cn/problems/binary-search/

给定一个 n n n 个元素有序的(升序)整型数组 nums 和一个目标值 target

写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 − 1 -1 1


解法①(基础版)


public class LeetCode704_1 {
    public int search(int[] nums, int target) {
        int low = 0, high = nums.length - 1;

        while (low <= high) {
            int middle = (low + high) >>> 1;
            if (target < nums[middle]) {
                high = middle - 1;
            } else if (target > nums[middle]) {
                low = middle + 1;
            } else {
                return middle;
            }
        }

        return -1;
    }
}

解法②(改动版)


public class LeetCode704_2 {
    public int search(int[] nums, int target) {
        int low = 0, high = nums.length;

        while (low < high) {
            int middle = (low + high) >>> 1;
            if (target < nums[middle]) {
                high = middle;
            } else if (target > nums[middle]) {
                low = middle + 1;
            } else {
                return middle;
            }
        }

        return -1;
    }
}

解法③(平衡版)


public class LeetCode704_3 {
    public int search(int[] nums, int target) {
        int low = 0, high = nums.length;

        while (high - low > 1) {
            int middle = (low + high) >>> 1;
            if (target < nums[middle]) {
                high = middle;
            } else {
                low = middle;
            }
        }

        return (nums[low] == target) ? low : -1;
    }
}

35 题·:搜索插入位置


链接:https://leetcode.cn/problems/search-insert-position/

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。

如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

请必须使用时间复杂度为 O ( l o g n ) O(log n) O(logn) 的算法。

提示:数组 nums 为 升序 排列数组


数组中不存在重复元素


这里的代码和 java/util/Arrays.java 中的 binarySearch0() 方法很像

public class LeetCode35_1 {
    public int searchInsert(int[] nums, int target) {
        int low = 0, high = nums.length - 1;

        while (low <= high) {
            int middle = (low + high) >>> 1;
            long middleValue = nums[middle];
            if (middleValue < target) {
                low = middle + 1;
            } else if (middleValue > target) {
                high = middle - 1;
            } else {
                return middle; // key found
            }
        }

        return low; // key not found
    }
}

数组中存在重复元素


当有多个元素的值和目标值一致时,返回这些重复元素中的最左索引

若数组中不存在与目标值一致的元素,则返回插入点

public int searchInsert(int[] nums, int target) {
    int low = 0, high = nums.length - 1;

    while (low <= high) {
        int middle = (low + high) >>> 1;

        if (nums[middle] < target) {
            low = middle + 1;
        } else {
            high = middle - 1;
        }
    }

    return low; 
}

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


链接https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/

给你一个按照非递减顺序排列的整数数组 n u m s nums nums,和一个目标值 t a r g e t target target

请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 t a r g e t target target,返回 [ − 1 , − 1 ] [-1, -1] [1,1]

你必须设计并实现时间复杂度为 O ( l o g n ) O(log n) O(logn) 的算法解决此问题。

提示: n u m s nums nums 是一个非递减数组,即该数组是一个存在重复元素的升序数组


public class LeetCode34 {
    public int[] searchRange(int[] nums, int target) {
        int left = leftMost(nums, target);
        int right = rightMost(nums, target);

        return (left == -1) ? new int[]{-1, -1} : new int[]{left, right};
    }

    public int leftMost(int[] nums, int target) {
        int low = 0, high = nums.length - 1;
        int candidate = -1;

        while (low <= high) {
            int middle = (low + high) >>> 1;
            if (target < nums[middle]) {
                high = middle - 1;
            } else if (target > nums[middle]) {
                low = middle + 1;
            } else {
                candidate = middle;
                high = middle - 1;
            }
        }

        return candidate;
    }

    public int rightMost(int[] nums, int target) {
        int low = 0, high = nums.length - 1;
        int candidate = -1;

        while (low <= high) {
            int middle = (low + high) >>> 1;
            if (target < nums[middle]) {
                high = middle - 1;
            } else if (target > nums[middle]) {
                low = middle + 1;
            } else {
                candidate = middle;
                low = middle + 1;
            }
        }

        return candidate;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值