• 【算法】二分查找


    一、算法理解

    二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法,前提是数据结构必须先排好序,可以在数据规模的对数时间复杂度内完成查找。
    二分查找要求线性表具有随机访问的特点(例如数组),也要求线性表能够根据中间元素的特点推测它两侧元素的性质,以达到缩减问题规模的效果。

    二、适用场景

    1. 有序序列查找。
    2. 元素支持随机访问(指定索引访问)。

    三、注意事项

    关键点:

    1. 二分查找的关键变量是 left,mid,right三个元素,根据目标值和mid所在索引的数值和target目标值进行比对,根据[mid]中间值的大小重新确定查找区间。
    2. 用途:二分法的基础是分治策略的基础,分治用的最多的是二分拆解计算。
    3. 可以使用 循环查找方式、或递归查找方式。

    如:递归查找方式典型模板:(以递增顺序序列为例)

    • right指向是查找范围下一个元素(无效元素)
    public int search(int[] nums, int target) {
        return medianSearchByIndex(nums, 0, nums.length, target);   //首次在left[0],right[length]范围查找
    }
    
    public int medianSearchByIndex(int[] nums,int left,int right,int target) {
        if(left < right) {    //注意, right是查询终点后一个元素,所以 < 
            int mid = (left + right) / 2;  // 注意
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] < target) {
                return searchByIndex(nums, mid+1, right, target);
            } else {
                return searchByIndex(nums, left, mid, target);   //注意, right是查询终点后一个元素,所以是mid
            }
        }
        return 0;
    }
    
    
    • right是查找范围最后一个有效元素
    public int search(int[] nums, int target) {
        return medianSearchByIndex(nums, 0, nums.length, target);   //首次在left[0],right[length]范围查找
    }
    
    public int medianSearchByIndex(int[] nums,int left,int right,int target) {
        if(left <= right) {    //注意, right是查询终点最后一个有效元素,最终二分到只有一个元素left==right
            int mid = (left + right) / 2;  // 注意
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] < target) {
                return searchByIndex(nums, mid+1, right, target);
            } else {
                return searchByIndex(nums, left, mid-1, target);   //注意, right是查询终点最后一个有效元素,所以是mid-1,应为mid已排除,不用重新查。
            }
        }
        return 0;
    }
    
    

    四、案例

    1)【困难】寻找2个正序数组中位数

    力扣:https://leetcode-cn.com/problems/median-of-two-sorted-arrays/
    给定两个大小为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。
    请你找出这两个正序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。
    你可以假设 nums1 和 nums2 不会同时为空。

    示例 1:
    nums1 = [1, 3]
    nums2 = [2]
    则中位数是 2.0

    示例 2:
    nums1 = [1, 2]
    nums2 = [3, 4]
    则中位数是 (2 + 3)/2 = 2.5

    示例3:
    输入:nums1 = [], nums2 = [1]
    输出:1.00000

    说明:
    提示:
    nums1.length == m
    nums2.length == n
    0 <= m <= 1000
    0 <= n <= 1000
    1 <= m + n <= 2000
    -106 <= nums1[i], nums2[i] <= 106

    【思路】:

    • 常规的思路是:num1[]和num2[]合并、排序后,变成一个长度为m+n的有序大序列,找其中位数。m+n是基数,则返回索引[(m+n)/2]的值;m+n是偶数,则返回索引[(m+n)/2]、[((m+n)/2)+1]的均值。

    • 如上可知,本质上:对于总长m+n的两个序列,只要找到索引[(m+n)/2]、[((m+n)/2)+1]的值即可。那么考虑用二分查找 从两个序列中找 第k个数 方式来实现呢?
      image

    代码参考:

    • 采用循环查找方式
        public double findMedianSortedArrays(int[] nums1, int[] nums2) {
            int totalLength = nums1.length + nums2.length;
            if (totalLength == 0) {
              return 0;
            }
            
            if (totalLength % 2 == 1) {            
                // 第totalLength / 2 + 1个数值
                double median = getKthElement(nums1, nums2, totalLength / 2 + 1); 
                return median;
                
            } else {
                // 第totalLength / 2个数值
                double median1 = getKthElement(nums1, nums2, totalLength / 2);
                // 第totalLength / 2 + 1个数值
                double median2 = getKthElement(nums1, nums2, totalLength / 2 + 1);            
                return (median1 + median2) / 2;
            }
        }
    
        public int getKthElement(int[] nums1, int[] nums2, int k) {
            int length1 = nums1.length, length2 = nums2.length;
            int index1 = 0, index2 = 0;
            int kthElement = 0;
    
            while (true) {
                // 边界情况
                if (index1 == length1) {
                    return nums2[index2 + k - 1];
                }
    
                if (index2 == length2) {
                    return nums1[index1 + k - 1];
                }
    
                if (k == 1) {
                    return Math.min(nums1[index1], nums2[index2]);
                }
    
                // 正常情况,第k/2个元素,对应索引k/2-1
                int half = k/2;
                int newIndex1 = Math.min(index1 + half - 1, length1 - 1);
                int newIndex2 = Math.min(index2 + half - 1, length2 - 1);
    
                if (nums1[newIndex1] <= nums2[newIndex2]) {
                    k -= (newIndex1 - index1 + 1);   //排除掉用nums1中0~newIndex1元素,计算个数
                    index1 = newIndex1 + 1;    //刷新排除后的索引
                } else {
                    k -= (newIndex2 - index2 + 1);
                    index2 = newIndex2 + 1;
                }
            }
    

    代码参考:

    • 采用递归查找方式
        public double findMedianSortedArrays(int[] nums1, int[] nums2) {
            int totalLength = nums1.length + nums2.length;
            if (totalLength == 0) {
                return 0;
            }
    
            if (totalLength % 2 == 1) {
                // 第totalLength / 2 + 1个数值
                double median = getKthElementLoop(nums1, 0,nums2, 0,totalLength / 2 + 1);
                return median;
    
            } else {
                // 第totalLength / 2个数值
                double median1 = getKthElementLoop(nums1, 0,nums2, 0, totalLength / 2);
                // 第totalLength / 2 + 1个数值
                double median2 = getKthElementLoop(nums1, 0,nums2, 0, totalLength / 2 + 1);
                return (median1 + median2) / 2;
            }
    
        }
        
        public int getKthElementLoop(int[] nums1, int startIndex1, int[] nums2, int startIndex2, int k) {
            if (startIndex1 < 0 || startIndex1 >= nums1.length) {
                return nums2[startIndex2 + k - 1];
            }
    
            if(startIndex2 <0 || startIndex2 >= nums2.length) {
                return nums1[startIndex1 + k - 1];
            }
    
            if (k == 1) {
                return  Math.min(nums1[startIndex1], nums2[startIndex2]);
            }
    
            int half = k/2;
            int newIndnex1 = Math.min(startIndex1 + half - 1, nums1.length - 1);
            int newIndnex2 = Math.min(startIndex2 + half - 1, nums2.length - 1);
    
            if (nums1[newIndnex1] <= nums2[newIndnex2]) {
                return getKthElementLoop(nums1, newIndnex1 + 1, nums2, startIndex2, k-(newIndnex1 - startIndex1 + 1));
            }else {
                return getKthElementLoop(nums1, startIndex1, nums2, newIndnex2 + 1, k-(newIndnex2 - startIndex2 + 1));
            }
        }
    

    3)搜索旋转排序数组

    升序整数数组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 。

    示例 1:
    输入:nums = [4,5,6,7,0,1,2], target = 0
    输出:4

    示例 2:
    输入:nums = [4,5,6,7,0,1,2], target = 3
    输出:-1

    示例 3:
    输入:nums = [1], target = 0
    输出:-1

    提示:
    1 <= nums.length <= 5000
    -10^4 <= nums[i] <= 10^4
    nums 中的每个值都 独一无二
    nums 肯定会在某个点上旋转

    【思路】:

    1. 遍历,找到旋转点。则:旋转点前的值大,旋转点后的值小。
    2. 比较 旋转点 和 target大小,分别在 旋转点左侧子序列 或 旋转点右侧子序列 二分查找target。

    【微码逻辑】:
    1)方式一:如果right指向比较边界外位置(right为无效索引)

    • 比较序列后端,right=length,为最后一个元素的下一位置。
    • 此时要注意:递归、或while循环下一轮应该是:[left,mid)【mid已比较过了,下轮作为范围外索引】、[mid+1,right)。
    • 每轮递归是条件是left < right。如:
      • 最后2个位置[3,4),则:mid=3,否则进入下一轮:
        • [3,3),3上轮已比较过了,这轮的3是区间外位置。需要跳出。
        • 或[4,4),4是区间外位置,需要跳出。

    2)方式二:right指向边界最后一个位置(right为有效索引)

    • 比较序列后端,right = length-1,为最后一个有效元素。
    • 此时要注意:递归、或while循环下一轮应该是:[left,mid-1]【mid已比较过了,下轮有效索引结尾是mid-1】、[mid+1,right]。
    • 每轮递归是条件是left <= right。如:
      • 循环最后2个位置Index是[x , x+1],则:mid=x,x位置值不相等,则:下一轮进入:
        • [x, x-1],跳出。
        • 或 [x+1 , x+1],则:mid = x+1,x+1位置值不相等,则下一轮进入:
          • [x+1, x]、或[x+2, x+1],跳出
      • 如果最后2个位置是Index[0, 1],则:mid=0,0位置不相等,下轮是[0,-1],[1, 1]

    一、代码参考1:递归 + right为无效元素

       public static int search(int[] nums, int target) {
            if (nums.length == 0) {
                return -1;
            }
    
            int index = -1;
            for (int i = 0; i < nums.length - 1; i++) {
                if (nums[i] > nums[i + 1]) {
                    index = i + 1;
                }
            }
    
            int ret = 0;
            if (index == -1) {
                ret = searchFor(nums, 0, nums.length, target);
            } else if (target >= nums[0]) {
                ret = searchFor(nums, 0, index, target);
            } else {
                ret = searchFor(nums, index, nums.length, target);
            }
            return ret;
        }
    
        public static int searchFor(int[] nums, int left, int right, int target) {
            if(left < right) {
                int mid = (left + right) / 2;
    
                if (nums[mid] == target) {
                    return mid;
                } else if (target < nums[mid]) {
                    return searchFor(nums, left, mid, target);
                } else {
                    return searchFor(nums, mid + 1, right, target);
                }
            }
            return -1;
        }
    

    二、代码参考2:递归 + right为有效元素

        public int search(int[] nums, int target) {
            if (nums.length == 0) {
                return -1;
            }
    
            int index = -1;
            for (int i = 0; i < nums.length - 1; i++) {
                if (nums[i] > nums[i + 1]) {
                    index = i;
                }
            }
    
            int ret = 0;
            if (index == -1) {
                ret = searchFor(nums, 0, nums.length -1, target);
            } else if (target >= nums[0]) {
                ret = searchFor(nums, 0, index , target);
            } else {
                ret = searchFor(nums, index + 1, nums.length -1, target);
            }
            return ret;
        }
    
        public static int searchFor(int[] nums, int left, int right, int target) {
            if(left <= right) {
                int mid = (left + right) / 2;
    
                if (nums[mid] == target) {
                    return mid;
                } else if (target < nums[mid]) {
                    return searchFor(nums, left, mid-1, target);
                } else {
                    return searchFor(nums, mid + 1, right, target);
                }
            }
            return -1;
        }
    

    三、代码参考3:while循环 + right为无效元素

    public static int search(int[] nums, int target) {
            if (nums.length == 0) {
                return -1;
            }
    
            int index = -1;
            for (int i = 0; i < nums.length - 1; i++) {
                if (nums[i] > nums[i + 1]) {
                    index = i + 1;
                }
            }
    
            int ret = 0;
            if (index == -1) {
                ret = searchFor(nums, 0, nums.length, target);
            } else if (target >= nums[0]) {
                ret = searchFor(nums, 0, index , target);
            } else {
                ret = searchFor(nums, index, nums.length, target);
            }
            return ret;
        }
    
        public static int searchFor(int[] nums, int left, int right, int target) {
            while(left < right) {
                int mid = (left + right) / 2;
    
                if (nums[mid] == target) {
                    return mid;
                }else if (target < nums[mid]) {
                    right = mid;
                } else {
                    left = mid + 1;
                }
            }
            return -1;
        }
    

    四、代码参考4:while循环 + right有效元素

        public static int search(int[] nums, int target) {
            if (nums.length == 0) {
                return -1;
            }
    
            int index = -1;
            for (int i = 0; i < nums.length - 1; i++) {
                if (nums[i] > nums[i + 1]) {
                    index = i;
                }
            }
    
            int ret = 0;
            if (index == -1) {
                ret = searchFor(nums, 0, nums.length -1, target);
            } else if (target >= nums[0]) {
                ret = searchFor(nums, 0, index , target);
            } else {
                ret = searchFor(nums, index + 1, nums.length -1, target);
            }
            return ret;
        }
    
        public static int searchFor(int[] nums, int left, int right, int target) {
            while(left <= right) {
                int mid = (left + right) / 2;
    
                if (nums[mid] == target) {
                    return mid;
                } else if (target < nums[mid]) {
                    right = mid -1;
                } else {
                    left = mid + 1;
                }
            }
            return -1;
        }
    

    4)小张刷题计划

    力扣:https://leetcode-cn.com/problems/xiao-zhang-shua-ti-ji-hua/submissions/

    为了提高自己的代码能力,小张制定了 LeetCode 刷题计划,他选中了 LeetCode 题库中的 n 道题,编号从 0 到 n-1,并计划在 m 天内按照题目编号顺序刷完所有的题目(注意,小张不能用多天完成同一题)。

    在小张刷题计划中,小张需要用 time[i] 的时间完成编号 i 的题目。此外,小张还可以使用场外求助功能,通过询问他的好朋友小杨题目的解法,可以省去该题的做题时间。为了防止“小张刷题计划”变成“小杨刷题计划”,小张每天最多使用一次求助。

    我们定义 m 天中做题时间最多的一天耗时为 T(小杨完成的题目不计入做题总时间)。请你帮小张求出最小的 T是多少。

    示例 1:
    输入:time = [1,2,3,3], m = 2
    输出:3
    解释:第一天小张完成前三题,其中第三题找小杨帮忙;第二天完成第四题,并且找小杨帮忙。这样做题时间最多的一天花费了 3 的时间,并且这个值是最小的。

    示例 2:
    输入:time = [999,999,999], m = 4
    输出:0
    解释:在前三天中,小张每天求助小杨一次,这样他可以在三天内完成所有的题目并不花任何时间。

    限制:
    1 <= time.length <= 10^5
    1 <= time[i] <= 10000
    1 <= m <= 1000

    【题解思路】
    此题最终结果每天最多耗费x时间刚好能读完所有小册、每天花费x-1时间又读不完所有小册。本质上是寻找x值。x取值范围1~10000。
    1)思路一:采用x从小到大遍历的方式,知道第一个能读完所有测试的x,即为结果。
    2)思路二:采用思路一的方式效率低。可以考虑采用二分法找x,要求x最终满足如上条件。

        public int readBookTimes(int[] time, int m) {
            int left = 0;
            int right = 10000 * time.length;
            while (left < right) {
                int mid = (left + right)/2;
                if (canReadAll(time, m, mid)) {
                    if (mid == 0) {
                        return 0;
                    }
    
                    if (!canReadAll(time, m, mid - 1)) {
                        return mid;
                    } else {
                        right = mid;
                    }
                } else {
                    left = mid + 1;
                }
            }
    
            return -1;
        }
    
        public boolean canReadAll(int[] times, int day, int maxTimes) {
            int tmpDay = 1;
            int tmpTime = 0;
            int tmpMaxTime = 0;
    
            for (int i = 0; i < times.length; i++) {
                // 计算当前天读取第i本书的时间
                if (times[i] > tmpMaxTime) {  // 剔除第i本书,前面临时记录的剔除书时间记入总耗时
                    tmpTime += tmpMaxTime;
                    tmpMaxTime = times[i];
                } else {
                    tmpTime += times[i];
                }
    
                // 如果加上第i本书,超时每天最大时间,则第i本书下一天在读。注这里:i要--回退。
                if (tmpTime > maxTimes) {
                    i--;
                    // 清空数据,用于下一天计算
                    tmpTime = 0;
                    tmpMaxTime = 0;
    
                    // 读书天数++
                    tmpDay++;
                    if (tmpDay > day) {
                        return false;
                    }
                }
            }
    
            return true;
        }
    
  • 相关阅读:
    第一阶段冲刺第七天
    第一次冲刺第六天
    第十一周学习进度条
    第一阶段冲刺第五天
    《我们应该怎样做需求分析》阅读笔记
    个人总结
    第二阶段个人总结十
    第二阶段个人总结九
    第二阶段个人总结八
    第二阶段个人总结七
  • 原文地址:https://www.cnblogs.com/yickel/p/14922627.html
Copyright © 2020-2023  润新知