二分查找(前提是数组、有序、不包含重复元素)

简单二分

数组中不包含重复元素,重点关注每次判边的逻辑。

框架 左闭右闭
class Solution {
public int binarySearch(int[] nums, int target) {
    int left = 0; 
    int right = nums.length; // 注意  

    while(left <right) {
    //这个mid每次都要更新,写在循环内部
        int mid = left + ((right - left)>>1);
        if(nums[mid] == target)
            return mid; 
        else if (nums[mid] < target)
            left = mid + 1; // 注意
        else if (nums[mid] > target)
            right = mid; // 注意
    }
    return -1;
}
}

分析二分查找的一个技巧是:不要出现 else,而是把所有情况用 else if 写清楚,这样可以清楚地展现所有细节。

统一使用左闭右闭 right 的赋值是 nums.length - 1 while(left <= right) left = mid + 1, right = mid - 1

左闭右开
class Solution {
public int binarySearch(int[] nums, int target) {
    int left = 0; 
    int right = nums.length - 1; // 注意  

    while(left <= right) {
    //这个mid每次都要更新,写在循环内部
        int mid = left + ((right - left)>>1);
        if(nums[mid] == target)
            return mid; 
        else if (nums[mid] < target)
            left = mid + 1; // 注意
        else if (nums[mid] > target)
            right = mid - 1; // 注意
    }
    return -1;
}
}
寻找左侧边界的二分搜索

找到 target 时不要立即返回,而是缩小「搜索区间」的上界 right,在区间 [left, mid) 中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。

int left_bound(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 别返回,锁定左侧边界
            right = mid - 1;
        }
    }
    // 判断 target 是否存在于 nums 中
    if (left < 0 || left >= nums.length) {
        return -1;
    }
    // 判断一下 nums[left] 是不是 target
    return nums[left] == target ? left : -1;
}
寻找右侧边界的二分查找
int right_bound(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 这里改成收缩左侧边界即可
            left = mid + 1;  //mid = left - 1
        }
    }
    // 最后改成返回 left - 1
    if (right < 0 || right >= nums.length) {
        return -1;
    }
    return nums[right] == target ? right : -1;
}
34 在排序数组中查找元素的第一个和最后一个位置 (存在重复元素)

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

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

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

class Solution {
    public int[] searchRange(int[] nums, int target) {
        return new int[]{left_bound(nums, target), right_bound(nums, target)};
    }

    int left_bound(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        // 搜索区间为 [left, right]
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] < target) {
                // 搜索区间变为 [mid+1, right]
                left = mid + 1;
            } else if (nums[mid] > target) {
                // 搜索区间变为 [left, mid-1]
                right = mid - 1;
            } else if (nums[mid] == target) {
                // 收缩右侧边界
                right = mid - 1;
            }
        }
        // 检查出界情况
        if (left >= nums.length || nums[left] != target) {
            return -1;
        }
        return left;
    }

    int right_bound(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] < target) {
                left = mid + 1;
            } else if (nums[mid] > target) {
                right = mid - 1;
            } else if (nums[mid] == target) {
                // 这里改成收缩左侧边界即可
                left = mid + 1;
            }
        }
        // 这里改为检查 right 越界的情况,见下图
        if (right < 0 || nums[right] != target) {
            return -1;
        }
        return right;
    }
}
35 搜索插入位置

使用左闭右开的方式

class Solution {
    public int searchInsert(int[] nums, int target) {
        return left_bound(nums, target);
    }

    int left_bound(int[] nums, int target) {
        if (nums.length == 0)
            return -1;
        int left = 0;
        int right = nums.length;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                //right = mid; //当存在重复元素,找最左侧插入点
                return mid;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else if (nums[mid] > target) {
                right = mid;
            }
        }
        return left;
    }
}
74 搜索二维矩阵

给你一个满足下述两条属性的 m x n 整数矩阵:

  • 每行中的整数从左到右按非严格递增顺序排列。
  • 每行的第一个整数大于前一行的最后一个整数。

给你一个整数 target ,如果 target 在矩阵中,返回 true ;否则,返回 false 。


只要知道二维数组的的行数 m 和列数 n,二维数组的坐标 (i, j) 可以映射成一维的 index = i * n + j;反过来也可以通过一维 index 反解出二维坐标 i = index / n, j = index % n。

m3,n=4 mid=6 i=1;j=2; 对应第七个数 0-6 第七个数

class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        int m = matrix.length, n = matrix[0].length;
        // 把二维数组映射到一维
        int left = 0, right = m * n - 1;
        // 前文讲的标准的二分搜索框架
        while(left <= right) {
            int mid = left + (right - left) / 2;
            if(get(matrix, mid) == target)
                return true;
            else if (get(matrix, mid) < target)
                left = mid + 1;
            else if (get(matrix, mid) > target)
                right = mid - 1;
        }
        return false;
    }

    // 通过一维坐标访问二维数组中的元素  需要mid作为整体坐标
    int get(int[][] matrix, int index) {
        int m = matrix.length, n = matrix[0].length;
        // 计算二维中的横纵坐标
        int i = index / n, j = index % n; // 4*4  13 是3行一列
        return matrix[i][j];
    }
}

简单二分 + 循环数组

循环数组:4, 5, 6, 7, 8, 0, 1, 2 ⇒ target = 5, target = 1

特点:一分为二后,一侧是有序数组,另一侧是循环数组。

根据这个特点,先判断有序数组、循环数组分别在哪一侧,再判断 target 在有序数组 or 循环数组,判断方法是让 target 和有序数组的首尾做比较,看是否在有序数组中。

循环数组:旋转前升序排列 4, 5, 6, 7, 8, 0, 1, 2 4, 5, 6, 7, 8, 0, 1, 2 复制两份可以看出循环(末尾数小于开头数)


33 搜索旋转排序数组 (左闭右闭)

整数数组 nums 按升序排列,数组中的值 互不相同

在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。


由于题目说数字无重复,举个例子:

1 2 3 4 5 6 7 可以大致分为两类,

第一类 2 3 4 5 6 7 1 这种,也就是 nums[start] <= nums[mid]。此例子中就是 2 <= 5。

这种情况下,前半部分有序。因此如果 nums[start] <=target<nums[mid],则在前半部分找,否则去后半部分找。

第二类 6 7 1 2 3 4 5 这种,也就是 nums[start] >= nums[mid]。此例子中就是 6 > 2。

这种情况下,后半部分有序。因此如果 nums[mid] <target<=nums[end],则在后半部分找,否则去前半部分找。

class Solution {
    public int search(int[] nums, int target) {
        if (nums == null || nums.length == 0) {
            return -1;
        }
        int start = 0;
        int end = nums.length - 1;
        int mid;
        while (start <= end) {
            mid = start + (end - start) / 2;
            if (nums[mid] == target) {
                return mid;
            }
            //前半部分有序,注意此处用小于等于
            if (nums[start] <= nums[mid]) {
                if (target >= nums[start] && target < nums[mid]) {
                    end = mid - 1; //在前半部分找
                } else {
                    start = mid + 1;
                }
            } else {
                 //后半部分有序,注意此处用小于等于
                if (target <= nums[end] && target > nums[mid]) {
                    start = mid + 1;
                } else {
                    end = mid - 1;
                }
            }
        }
        return -1;
    }
}
153 寻找旋转排序数组中的最小值

已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:

  • 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
  • 若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]

注意,数组 [a[0], a[1], a[2], ..., a[n-1]]旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。

给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。


这段代码通过二分查找的方式来寻找最小值。关键点在于判断最小值是在左侧还是右侧: mid时数组下标

如果nums[mid] > nums[right],说明最小值在mid的右侧,因为数组的右侧是无序的,而左侧是有序的。

如果nums[mid] <= nums[right],则需要判断mid是否是最小值。如果mid是数组的第一个元素,或者nums[mid]比它前面的元素小,那么mid就是最小值。

否则,最小值在mid的左侧。

通过这种方式,我们可以在对数时间复杂度内找到数组中的最小值。

public class Solution {
    public int findMin(int[] nums) {
        int left = 0, right = nums.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2; // 防止溢出,同时找到中间位置
            
            // 如果中间元素大于最右边的元素,说明最小值在右侧
            if (nums[mid] > nums[right]) {
                left = mid + 1;
            } else {
                // 如果中间元素是最小值(比前一个元素小或者是数组的第一个元素)
                if (mid == 0 || nums[mid] < nums[mid - 1]) {
                    return nums[mid];
                } else {
                    // 如果中间元素不是最小值,说明最小值在左侧
                    right = mid - 1;
                }
            }
        }
        return -1; // 如果数组为空,返回-1
    }
}
面试题 10.09. 排序矩阵查找

给定M×N矩阵,每一行、每一列都按升序排列,请编写代码找出某元素。


class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        // 检查矩阵是否为空
        if (matrix.length == 0 || matrix[0].length == 0) {
            return false;
        }
        
        // 遍历矩阵的每一行   在每一行中应用二分查找
        for (int[] row : matrix) {
            // 在当前行中搜索目标值
            int index = search(row, target);
            if (index >= 0) {
                // 如果目标值存在于当前行,则返回 true
                return true;
            }
        }
       
        // 目标值不存在于矩阵中
        return false;
    }

    // 二分查找
    int search(int[] nums, int target) {
        int l = 0; // 左边界
        int r = nums.length - 1; // 右边界
        
        while (l <= r) {
            int mid = l + (r - l) / 2; // 计算中间位置
            int num = nums[mid]; // 获取中间位置的值
            
            if (num == target) {
                // 如果中间位置的值等于目标值,返回中间位置
                return mid;
            } else if (num > target) {
                // 如果中间位置的值大于目标值,更新右边界
                r = mid - 1;
            } else {
                // 如果中间位置的值小于目标值,更新左边界
                l = mid + 1;
            }
        }
        
        // 目标值不存在于数组中
        return -1;
    }
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值