Java & Leetcode


记录leetcode关于二分查找的题目

二分法的基本思想都是 减而治之

一、常规二分查找

1. 查找法

public int IterativeBS(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 right = mid-1;
        }

        return left;
    }

使用查找法最直观的一点就是最后得到的是一个空区间,比较适合用于待查元素可能不在有序数组中的查找。

2. 排除法

public int IterativeBSExclude(int[] nums, int target){
        int left=0, right=nums.length-1;

        // 对于出现在最后位置的情况,该算法没有考虑进去,因此需要单独判断或者将右指针包括该位置
        // if(nums[right] < target) return right+1;

        // 最后会得到一个单一的值
        while(left<right){
            int mid = left+(right-left)/2;
            if(nums[mid] < target){
                left = mid+1;
            }
            // 否则还是有可能存在于右侧中,不能直接进行剔除
            else right = mid;
        }

        return left;
    }

使用排除法在一些复杂情况中处理起来更为方便,适合于待查元素存在于有序数组中的查找。(上面代码中如果需要查询任意一个数字的插入位置时,需要将注释代码还原,或者将mid向上取整)

3. 递归版本的查找法

public int RecursiveBS(int[] nums, int target){
        return RBS(nums, 0, nums.length-1, target);
    }

private int RBS(int[] nums, int left, int right, int target){
    if(left > right) return left;
    int mid = left+(right-left)/2;
    if(nums[mid] < target) return RBS(nums, mid+1, right, target);
    else return RBS(nums, left, mid-1, target);
}

PS:如果数组中包含重复元素,则上述皆为左查询,即返回的是满足条件的最左边的值。

二、寻找旋转排序数组的最小值

1、无重复元素

public int findMin(int[] nums) {
		  // 该实现为查找版本,需要额外考虑很多情况
//        int left=0, right = nums.length-1;
//        if(right<0) return -1;
//        while(left <= right){
//            int mid = left+(right-left)/2;
//
//            // 出现中间位时的必要判断
//            if(mid >0 && nums[mid]<nums[mid-1])
//                return nums[mid];
//            if(nums[mid] < nums[right]){
//                right = mid-1;
//            }else left = mid+1;
//        }
//
//        // 设置判断条件,当位置不在初始位上时,找到的位置位于下一位
//        if(left>0) return nums[left-1];
//        return nums[left];

        // 使用排除法的更简洁版本
        int left=0, right=nums.length-1;
        if(right<0) return -1;
        while(left < right){
            int mid = left+(right-left)/2;
            if(nums[mid] < nums[right]){
                // 核心点:当前的位置也可能为最终结果,也是此处没有想好
                right=mid;
            }else left=mid+1;
        }

        return nums[left];
    }

不包含重复元素时,可以通过测试发现右侧有序时,待查结果一定会位于左侧,利用这个不变量进行排除。

2、 可能包含重复元素

public int findMinRt(int[] nums) {
        int left=0, right=nums.length-1;
        if(right<0) return -1;
        while(left < right){
            int mid = left+(right-left)/2;
            if(nums[mid] < nums[right]){
                right=mid;
            }else if(nums[mid] > nums[right]) left=mid+1;
            // 由于使用的是右边判断,因此出现相同情况时应该优先缩小右区间
            // 错误案例:1,3,3
//            else ++left;
            else --right;
        }

        return nums[left];
    }

对于包含重复元素的情况,此时需要考虑一种特殊情况,即出现中间与右边相等时(0,2,2,2),此时如果还是按照之前的判断进行右移,则会出现直接跳过结果的情况,因此需要单独进行判断。

三、搜索旋转排序数组

1、无重复元素

public int search(int[] nums, int target) {
        // 该类型不适合采用排除法,因为有可能不存在,需要额外的判断
        int left=0, right=nums.length-1;
        while(left <= right){
            int mid = left + (right-left) / 2;
            if(nums[mid] == target) return mid;
            if(nums[mid] >= nums[left]){
                if(nums[mid] > target && nums[left] <= target){
                    right = mid-1;
                }else left = mid+1;
            }else {
                if(nums[mid] < target && nums[right] >= target){
                    left = mid+1;
                }else right=mid-1;
            }
        }

        return -1;
    }

此处选择查找法,对于元素不一定存在与数组中的情况,查找法更为合适。注意到这里事先进行的是右侧判断,原因是因为右边有序比左边有序的情况更简单(可以通过实例来找到这个规律)

2、可能包含重复元素

public boolean search_repeat(int[] nums, int target) {
        int left = 0, right = nums.length-1;
        if(right<0) return false;

        while(left <= right){
            int mid = left + (right-left) / 2;

            if(nums[mid] == target) return true;
            // 由题意,可知待查询结果不一定存在于数组中,因此采用查找法比较合适
            if(nums[mid] > nums[left]){
                // 左有序
                if(target >= nums[left] && target < nums[mid]){
                    right = mid-1;
                }else left = mid+1;
            }else if(nums[mid] < nums[left]){
                // 右有序
                if(target <= nums[right] && target > nums[mid]){
                    left = mid+1;
                }else right = mid-1;
			// 两边相等时退化为逐步查询
            }else left++;
        }
        return false;
    }

出现了重复元素,则需要将相等时候的情况进行单独考虑,缩小判断区间,算法会退化为O(n)时间复杂度。

四、山脉数组

1、山脉数组的峰顶索引

public int peakIndexInMountainArray(int[] arr) {
        // 由题意,一定会存在一个满足该条件的索引,因此选择排除法
        int left =0, right = arr.length-1;
        // 甚至能够省略判断条件,右边会收缩到最终位
//        if(arr[0]>arr[1]) return 0;
        while(left<right){
            // 因为先对右边进行收缩,因此采用向上取整
            int mid = left + (right-left+1) / 2;
            if(mid>0 && arr[mid-1]>arr[mid]){
                right = mid-1;
            }else left = mid;
        }

        return left;
    }

排除法+向上取整(排除法先对右边进行收缩,所以采用向上取整)。

2、山脉数组中查找目标值

public int findInMountainArray(int target, MountainArray mountainArr) {
		// 先寻找峰值(必存在,排除法)
        int left=0 ,right = mountainArr.length()-1, mid= 0;
        while(left < right){
            mid = left + (right-left+1) /2;
            if(mid>0 && mountainArr.get(mid-1)>mountainArr.get(mid)){
                right = mid-1;
            }else left= mid;
        }

        int peak_index = left, pos=-1;
        if(mountainArr.get(peak_index) < target) return pos;
		
		// 再寻找左边符合的下标(可能不存在,查找法)
        left = 0;
        right = peak_index;
        while(left<= right){
            mid = left+ (right-left)/2;
            if(mountainArr.get(mid) < target){
                left = mid+1;
            }else right = mid-1;
        }

        if(mountainArr.get(left) == target) return left;
		
		// 如果没找到,再找右边的
        left = peak_index+1;
        right = mountainArr.length()-1;
        while(left <= right){
            mid = left + (right-left)/2;
            if(mountainArr.get(mid) < target){
                right = mid-1;
            }else left = mid+1;
        }
		
		// 注意右侧与左边会刚好相反,因此最后的结果也需要处理
        if(mountainArr.get(left-1) == target) return left-1;
        return pos;
    }

这里主要注意山脉数组的定义,并不会出现层峦叠嶂的现象!!!

五、寻找两个正序数组的中位数

暴力法(逐项过滤)

public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        // 朴素法,先合并再利用中位数性质求取
        int len_1 = nums1.length, len_2 = nums2.length;
        int center = (len_1+len_2) / 2, even = 0;
        if((len_1+len_2)%2 == 0) even = 1;
        int[] res = new int[]{0,0};

        int i_1 = 0, i_2 = 0, count=0;
        while(i_1<len_1 || i_2<len_2){
        	// 设置尾部判断条件,防止越界错误
            int cur_val_1 = 1000000+1, cur_val_2 = 1000000+1;
            if(i_1 < len_1) cur_val_1 = nums1[i_1];
            if(i_2 < len_2) cur_val_2 = nums2[i_2];

            if(cur_val_1 < cur_val_2){
                i_1 ++;
            }else i_2++;
			// 先不管奇偶性,都找到后再进行判断
            if(count == center-1) res[0] = Math.min(cur_val_1, cur_val_2);
            if(count == center){
                res[1] = Math.min(cur_val_1, cur_val_2);
                break;
            }

            count++;
        }

        if(even == 0) return res[1];
        return (double)(res[0]+res[1])/2;
        }

自己写的暴力法,不需要额外的空间开销,但是还是有点冗余,可以写得更加优美。

public double findMedianSortedArrays(int[] nums1, int[] nums2) {
		int len_1 = nums1.length, len_2 = nums2.length;
        int i_1=0, i_2 = 0;
        int len = len_1+len_2, pre_value=0, cur_value=0;

        for(int j=0;j<=len/2;++j){
            pre_value = cur_value;
            // 当前一个数组还存在元素,并且比后一个数组元素小或者后一个数组没元素时,从前一个数组中选取
            if(i_1<len_1 && (i_2>=len_2 || nums1[i_1]<nums2[i_2])){
                cur_value = nums1[i_1++];
            }else cur_value = nums2[i_2++];

        }

        if(len%2==1) return cur_value;
        return (double)(cur_value+pre_value)/2;
    }

优化的部分:将奇偶判断合并,取消了尾部判断,直接换成条件赋值。时间复杂度为O(n+m),空间复杂度为O(1)。

寻找第k小数(二分优化)

public double findMedianSortedArrays_Recursive(int[] nums1, int[] nums2){
        int len_1 = nums1.length, len_2 = nums2.length;
        // 由于k是从1开始的,因此需要寻找的是上界与下一个值
        int pre = (len_1+len_2+1) / 2, cur=(len_1+len_2+2) / 2;
        return (getKthValue(nums1, 0, len_1-1, nums2, 0, len_2-1, pre)+getKthValue(nums1,0,len_1-1, nums2,0, len_2-1, cur))*0.5;
    }

    // 寻找第k小数
    private int getKthValue(int[] nums1, int st_1, int ed_1, int[] nums2, int st_2, int ed_2, int k){
        int len1 = ed_1-st_1+1;
        int len2 = ed_2-st_2+1;

        // 始终将短数组作为 nums1,便于后续的判断
        if(len1 > len2) return getKthValue(nums2, st_2, ed_2, nums1, st_1, ed_1, k);
        // 当第一个数组中已经被排除完后,答案必定在另一个数组中,直接返回即可
        if(len1 == 0) return nums2[st_2+k-1];

        // 当只剩下最后一个时,选择其中的小值作为最终答案
        if(k == 1) return Math.min(nums1[st_1], nums2[st_2]);

        // 注意截取的长度不能超过当前数组的最大长度
        int i = st_1+ Math.min(k/2, len1)-1;
        int j = st_2+ Math.min(k/2, len2)-1;

        // 删除两者中的小值,注意此处删除的值在最终合并数组中不一定都是连续的,但是必定是在第k小数的左边
        if(nums1[i] < nums2[j]){
            return getKthValue(nums1, i+1, ed_1, nums2, st_2, ed_2, k-(i-st_1+1));
        }else
            return getKthValue(nums1, st_1, ed_1, nums2, j+1, ed_2, k-(j-st_2+1));
    }

将求取中位数转化为寻找第k小数问题,需要注意的是这个k是从1开始的,所以在使用时需要-1(边界条件害死人)。时间复杂度为O(log(m+n)),空间复杂度为O(1)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值