• leetcode -- 二分查找


    之前在数据结构搜索那章说过,折半(二分)一般适用于有序列表的查找,但是在写的时候需要注意算法的细节。我在leetcode上总共写了八道应用了二分算法的题目,从中总结一下写二分算法需要注意什么样的细节

    一般二分查找

    leetcode,第704题,binary search

    Given a sorted (in ascending order) integer array nums of n elements and a target value, write a function to search target in nums. If target exists, then return its index, otherwise return -1.
    Example 1:
    Input: nums = [-1,0,3,5,9,12], target = 9
    Output: 4
    Explanation: 9 exists in nums and its index is 4

    这道题就是最简单的二分查找算法,我当时的解法也是二分法,

       public int search(int[] nums, int target) {
            int start = 0, end = nums.length - 1;
            while(start <= end) {
            	int middle = (end + start) / 2;
            	if(target > nums[middle]) {
            		start = middle + 1;
            	}else if(target < nums[middle]) {
            		end = middle - 1;
            	}else if(target == nums[middle]){
            		return middle;
            	}
            }
            return -1;
        }
    

    对于二分算法的写法,这是其中一种,还有其他写法,一共有三种模板写法
    Alt text

    这上面的三种写法中,第三种是使用最多的,因为很多时候mid最好还是不要跳过,还需要继续使用。第一种写法写法更加简洁,不过适用于没有重复元素或者不需要寻找第一个、最后一个位置。我们可以发现模板写法和我们的写法有一点不同,这点就是mid的求法,模板中mid=left + (right - left) / 2,在我们的写法中middle = (end + start) / 2,我们的写法更容易出现问题,如果end和start这个时候都非常的大,超出了int的范围(-2147483648, 2147483647),那么值就会变成0,但是模板中mid = left + (right - left) / 2就不会出现这样的问题。

    leetcode,第278题,First Bad Version

    You are a product manager and currently leading a team to develop a new product. Unfortunately, the latest version of your product fails the quality check. Since each version is developed based on the previous version, all the versions after a bad version are also bad.
    Suppose you have n versions [1, 2, ..., n] and you want to find out the first bad one, which causes all the following ones to be bad.
    You are given an API bool isBadVersion(version) which will return whether version is bad. Implement a function to find the first bad version. You should minimize the number of calls to the API.
    Example:
    Given n = 5, and version = 4 is the first bad version.
    call isBadVersion(3) -> false
    call isBadVersion(5) -> true
    call isBadVersion(4) -> true
    Then 4 is the first bad version.

    这道题也是查找位置,不过比较特殊,第一个版本错了,那么后面就会一直错,这个对错就是它的顺序,判断条件就是它对还是错。

        public int firstBadVersion(int n) {
        	if(n == 1) return n;
            int start = 1;
            int end = n;
            int bad_version = 1;
            while(start <= end){
            	// 那种写法不对,middle = (start + end) / 2,
            	// 这种写法在小数据量下没关系,但是数据量大,就是相加错误
                int middle = start + (end - start) / 2;
                if(isBadVersion(middle)){
                    bad_version = middle;
                    end = middle - 1;
                }else{
                    start = middle + 1;
                }
            }
            return bad_version;
        }
    

    当然对于二分查找,也可以使用回溯法来实现它。

        // 使用回溯法
        public int firstBadVersion(int n) {
        	return helper(1,n);
        }
        public int helper(int start, int end) {
    		if(start >= end) return start;
    		int middle = start + (end - start) / 2;
    		// 这里和上面不同。这里并没有记录下来,并且middle可能就是的
    		if(isBadVersion(middle)) return helper(start, middle);
    		else return helper(middle + 1, end);
        }
    

    注意查找位置

    这种题目一般都是因为查找的元素比较特殊,比如插入的位置,并且因为循环判定条件<=的原因,要格外注意位置在哪里。

    leetcode,第35题,Search Insert Position

    Given a sorted array and a target value, return the index if the target is found. If not, return the index where it would be if it were inserted in order.You may assume no duplicates in the array.
    Example 1:
    Input: [1,3,5,6], 2
    Output: 1

    这道题是一个插入题,其中应该注意的是返回值该是什么,下面的代码使用的是start来作为返回值,如果使用middle作为返回值,在数组也有相同元素的情况下没什么问题,一旦数组中没有这个元素,那么插入的位置就会小一格,而start是正好在位置上。

        // 二分查找位置
        public int searchInsert(int[] nums, int target) {
        	if(nums.length == 0) return 0;
        	int start = 0;
        	int end = nums.length - 1;
        	while(start <= end) {
        		int middle = (start + end) / 2;
        		if(target == nums[middle]) {
        			return middle;
        		}else if(target > nums[middle]) {
        			start = middle + 1;
        		}else if(target < nums[middle]) {
        			end = middle - 1;
        		}
        	}
            // 要注意返回的位置,因为它可能会比插入位置的值要小
        	return start;
        }
    

    leetcode,第74题,Search a 2D Matrix,

    Write an efficient algorithm that searches for a value in an m x n matrix. This matrix has the following properties:
    &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp Integers in each row are sorted from left to right.
    &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp The first integer of each row is greater than the last integer of the previous row.
    Example 1:
    Input:
    matrix = [
    [1, 3, 5, 7],
    [10, 11, 16, 20],
    [23, 30, 34, 50]
    ]
    target = 3
    Output: true

    这道题我是用的是俩次二分查找,因为它在行列上是有顺序的,我们可以先在行上二分,看目标数在哪一行中,找出特定行,但是要注意这个数在这个行中哪个位置。下面的代码用的是end来表示,这里如果用start,因为判断条件是<=,那么end最终会多出一格,因为执行了end++。

        // 这里是用了俩次二分查找
        // 也可以将二维数组并成一维数组
        public boolean searchMatrix(int[][] matrix, int target) {
        	if(matrix.length == 0 || matrix[0].length == 0) return false;
            int start = 0,end = matrix.length - 1;
            int matrix_line = 0;
            while(start <= end) {
            	int middle = (start + end) / 2;
        		if(target == matrix[middle][0]) {
        			return true;
        		}else if(target > matrix[middle][0]) {
        			start = middle + 1;
        		}else if(target < matrix[middle][0]) {
        			end = middle - 1;
        		}	
            }
            // 注意查找的位置。这里不是插入,不应该用start
            matrix_line = Math.max(end, 0);
            start = 0;
            end = matrix[matrix_line].length - 1;
            while(start <= end) {
            	int middle = (start + end) / 2;
        		if(target == matrix[matrix_line][middle]) {
        			return true;
        		}else if(target > matrix[matrix_line][middle]) {
        			start = middle + 1;
        		}else if(target < matrix[matrix_line][middle]) {
        			end = middle - 1;
        		}	
            }
            return false;
        }
    

    半有序

    这种题目都是将数组旋转为前提,数组的全部元素并不是有序的,但是以某个元素为分界,俩边都是有序,当然也可以完全有序。
    leetcode,第153题,Find Minimum in Rotated Sorted Array

    Suppose an array sorted in ascending order is rotated at some pivot unknown to you beforehand.
    (i.e., [0,1,2,4,5,6,7] might become [4,5,6,7,0,1,2]).
    Find the minimum element.
    You may assume no duplicate exists in the array.
    Example 1:
    Input: [3,4,5,1,2]
    Output: 1

    上面就是以0为界限,前面有序,后面也有序。开始的时候,我的想法以第一个元素为标杆,这里就是4,然后二分,这里就是7与4进行比较,如果middle大于标杆元素,那么说明还在一个序列中,start = middle+1,直到找到了另一个有序,但是这种方法在面对序列中的元素都有序的时候就出错了,比如[1,2,3,4,5,6],那么它肯定找不到了,因为都比标杆元素大。

    之后想法是先找分界线,那么以最后一个元素为标杆比较好,因为如果一个序列中,那么最后元素都会比前面的大,如果中间元素middle大于最后元素end,说明俩者不在一个序列中,这个时候就可以将start往后移动,如果小于,就说明在一个序列中,可以将end往前移动。最后不确定start和end谁最小的最好方法就是比较一下。这里的判定条件已经发生了改变,变成了starrt + 1 < end,这是因为下面的不用再+1和-1,直接用了middle,如果还是以前的start <= end,整个程序就会没有发生变化,一直循环处理下去。

        // 使用二分的思想,不至于全部遍历一下
        // 速度很快
        public int findMin(int[] nums) {
        	if(nums.length == 1) return nums[0]; 
            int start = 0;
            int end = nums.length - 1;
            int target = nums[end];
            while(start + 1 < end) {
            	int middle = start + (end - start)/2;
            	if(nums[middle] > target) {
            		start = middle;
            	}else if(nums[middle] < target){
            		end = middle;
            		target = nums[end];
            	}
            }
            return Math.min(nums[start], nums[end]);
    

    leetcode,第154题,Find Minimum in Rotated Sorted Array II

    Suppose an array sorted in ascending order is rotated at some pivot unknown to you beforehand.
    (i.e., [0,1,2,4,5,6,7] might become [4,5,6,7,0,1,2]).
    Find the minimum element.
    The array may contain duplicates.
    Example 2:
    Input: [2,2,2,0,1]
    Output: 0

    这道题最大的不同就是有了重复元素,但是其实解法都是一样的,只不过需要将重复元素进行判定消去,如果周边有相同的元素,那么就是移动一格。

        // 大概的思路与之前的一样,重复元素,就简单的办法就是直接去除重复元素
        // 但是时间太久,速度太慢,但是其他的思想都差不多。
        public int findMin(int[] nums) {
        	if(nums.length == 1) return nums[0]; 
            int start = 0;
            int end = nums.length - 1;
            int target = nums[end];
            while(start + 1 < end) {
            	while(start < end && nums[end] == nums[end - 1]) {
            		end--;
            	}
            	while(start < end && nums[start] == nums[start + 1]) {
            		start++;
            	}
            	int middle = start + (end - start)/2;
            	if(nums[middle] > target) {
            		start = middle;
            	}else if(nums[middle] < target){
            		end = middle;
            		target = nums[end];
            	}
            }
            return Math.min(nums[start], nums[end]);
        }
    

    leetcode,第33题,Search in Rotated Sorted Array

    Suppose an array sorted in ascending order is rotated at some pivot unknown to you beforehand.
    (i.e., [0,1,2,4,5,6,7] might become [4,5,6,7,0,1,2]).
    You are given a target value to search. If found in the array return its index, otherwise return -1.
    You may assume no duplicate exists in the array.
    Your algorithm's runtime complexity must be in the order of O(log n).
    Example 1:
    Input: nums = [4,5,6,7,0,1,2], target = 0
    Output: 4

    这道题与上面俩道题差不多,只不过上面都是查找最值,这道题是查找相应的元素,一开始想法是双指针法,前后俩个序列,然后依次进行比较,因为这俩个序列也是有序,因而知道结束条件是什么。但是这种写法速度非常的慢。

            // 将之变成俩个序列,分别使用顺序查找,速度有点慢
    	public int search(int[] nums, int target) {
    		if(nums.length == 0) return -1;
    		int one_point = 0;
    		int two_point = nums.length - 1;
    		boolean is_one_search = true, is_two_search = true;
    		while(one_point < nums.length && two_point >= 0 && (is_one_search || is_two_search)) {
    			if(target == nums[one_point]) {
    				return one_point;
    			}else if(target > nums[one_point]) {
    				one_point++;
    			}else if(target < nums[one_point]) {
    				is_one_search = false;
    			}
    			
    			if(target == nums[two_point]) {
    				return two_point;
    			}else if(target < nums[two_point]) {
    				two_point--;
    			}else if(target > nums[two_point]) {
    				is_two_search = false;
    			}	
    		}
    		return -1;
        }
    

    之后看了别人的想法,发现二分也是可以解决这道题,不过二分需要判断四种情况:

    • middle元素大于start元素,说明前面元素都是有序的。
    1. start < target < middle,那么该元素就在前面的序列中,这时end = middle,来缩小范围。
    2. 如果不在, 就得缩小到后面序列中,start = middle。
    • middle元素小于end元素,说明后面元素都是有序的。
    1. middle < target < end,那么说明元素在后面的序列中,这时需要start = middle,来缩小范围。
    2. 如果不在,就得缩小到前面序列中,end = middle.
            // 使用二分法,分四种情况进行讨论,速度可以,内存消耗大
    	public int search(int[] nums, int target) {
    		if(nums.length == 0) return -1;
    		int start = 0;
    		int end = nums.length - 1;	
    		// 这种start = middle写法中,判断条件不能是start < end,这样会导致它不变
    		while(start + 1 < end) {
    			int middle = start + (end - start)/2;
    			if(target == nums[middle]) return middle;
    			if(nums[start] < nums[middle]) {
    				if(target <= nums[middle] && target >= nums[start]) {
    					end = middle;
    				}else {
    					start = middle;
    				}
    				
    			}else if(nums[end] > nums[middle]){
    				if(target >= nums[middle] && target <= nums[end]) {
    					start = middle;
    				}else {
    					end = middle;
    				}
    			}
    		}
    		// 这样判断最好,分清楚
    		if(nums[start] == target) return start;
    		if(nums[end] == target) return end;
    		return -1;
    	}
    

    leetcode,第81题,Search in Rotated Sorted Array II

    Suppose an array sorted in ascending order is rotated at some pivot unknown to you beforehand.
    (i.e., [0,0,1,2,2,5,6] might become [2,5,6,0,0,1,2]).
    You are given a target value to search. If found in the array return true, otherwise return false.
    Example 1:
    Input: nums = [2,5,6,0,0,1,2], target = 0
    Output: true

    这道题和上面一样,只不过这道题多了重复元素,和之前一样的思路,先去除重复元素,再使用二分法来进行判断。

        public boolean search(int[] nums, int target) {
        	if(nums.length == 0) return false;
    		int start = 0;
    		int end = nums.length - 1;
    		
    		// 这种start = middle写法中,判断条件不能是start < end,这样会导致它不变
    		while(start + 1 < end) {
    			if(start < end && nums[start] == nums[start + 1]) start++;
    			if(start < end && nums[end] == nums[end - 1]) end--;
    			int middle = start + (end - start)/2;
    			if(target == nums[middle]) return true;
    			
    			
    			if(nums[start] < nums[middle]) {
    				if(target <= nums[middle] && target >= nums[start]) {
    					end = middle;
    				}else {
    					start = middle;
    				}
    			// 一定要加上这句话。不能直接写else,不然对于{3,1,1}这种无法判断
    			}else if(nums[end] > nums[middle]){
    				if(target >= nums[middle] && target <= nums[end]) {
    					start = middle;
    				}else {
    					end = middle;
    				}
    			}
    		}
    		if(nums[start] == target || nums[end] == target) return true;
    		return false;
        }
    

    总结

    二分法(折半)思路较为简单,并且可以用在元素有序的情形下,但是二分法需要注意细节,停止条件,查找位置,判定条件,还有中间位置的计算。如果可以的话,先演示一下,要特别注意那些特殊情况下的写法。本文中全部的实现过程都已经上传到我的github

  • 相关阅读:
    题解——栈(卡特兰递归数的应用)
    题解——主的赦免(递归的进一步理解)
    sql开窗函数 row_number () over(order by id )
    c# resources
    Fillder 转载
    用javascript实现控制打开网页窗口的大小 和HTML如何关闭窗口的技巧大全
    datatable 与dataview的区别
    Asp.net中DataBinder.Eval用法的总结
    FrameSet左右收缩编码
    ajax get post
  • 原文地址:https://www.cnblogs.com/mary1/p/13235639.html
Copyright © 2020-2023  润新知