剑指offer面试题53(java版):数字在排序数组中出现的次数

welcome to my blog

剑指offer面试题53(java版):数字在排序数组中出现的次数

题目描述

统计一个数字在排序数组中出现的次数。

 

示例 1:

输入: nums = [5,7,7,8,8,10], target = 8
输出: 2
示例 2:

输入: nums = [5,7,7,8,8,10], target = 6
输出: 0
第四次做; 这道题真的好! 二分法; 核心: 1)先找左边界再找右边界, left和right的目标都是target; 2)while(left<right)这个条件无法处理数组中只有一个元素的情况, 需要改成while(left<=right) 3)注意, 数组中只有一个元素时, mid才能等于n-1; 数组中有两个及以上的元素时, mid不会等于n-1的! 最大等于n-2
class Solution {
    public int search(int[] nums, int target) {
        int n = nums.length;
        if(n==0){
            return 0;
        }
        int left=0, right = n-1, mid;
        int border=-1;
        while(left<=right){
            mid = left + (right - left)/2;
            if(nums[mid] > target){
                right = mid - 1;
            }else if(nums[mid] < target){
                left = mid + 1;
            }else{
                if(mid==n-1 || nums[mid+1]>target){
                    border = mid;
                    break;
                }
                //nums[mid+1] == target
                left = mid+1;
            }
        }
        if(border==-1){
            return 0;
        }
        int R = border;
        border = -1;
        left = 0;
        right = n-1;
        while(left<=right){
            mid = left + (right - left)/2;
            if(nums[mid] > target){
                right = mid - 1;
            }else if(nums[mid] < target){
                left = mid + 1;
            }else{
                if(mid==0 || nums[mid-1]<target){
                    border = mid;
                    break;
                }
                right = mid - 1;
            }
        }
        int L = border;
        return R - L + 1;
    }
}
/*
双指针? 时间复杂度O(N)
使用二分法应该是O(logN)的时间复杂度
*/
class Solution {
    public int search(int[] nums, int target) {
        //input check
        if(nums==null || nums.length==0 || target<nums[0] || target>nums[nums.length-1])
            return 0;
        int n = nums.length;
        int left=0, right=n-1, mid;
        int leftBorder=-1, rightBorder=n;
        //舍弃的思路: left指向左边界, right指向target; left和right的目标不同的话, while的循环条件就要变; 以前的二分法, left和right的目标是一样的
        //采用的思路: left和right都指向target
        //找左边界
        //下面的循环条件不适用于数组中只有一个元素的, 如果数组中只有一个元素则不会进入下面的循环
        //while(left<right)
        while(left<=right){
            mid = left + ((right-left)>>1);
            if(nums[mid] > target)
                right = mid-1;
            else if(nums[mid] < target)
                left = mid + 1;
            else{
                if(mid==0){
                    leftBorder=0;
                    break;
                }
                if(nums[mid-1]!=target){
                    leftBorder = mid;
                    break;
                }
                right = mid - 1;
            }
                
        }
        //数组中没有target
        if(leftBorder==-1)
            return 0;
        
        //再找右边界
        left=0;
        right=n-1;
        while(left<=right){
            mid = left + ((right-left)>>1);
            if(nums[mid] > target)
                right = mid - 1;
            else if(nums[mid] < target)
                left = mid + 1;
            else{
                //注意, 数组中只有一个元素时, mid才能等于n-1
                if(mid==n-1){
                    rightBorder = mid;
                    break;
                }
                //注意, 数组中有两个及以上的元素时, mid不会等于n-1的! 最大等于n-2
                if(nums[mid+1]!=target){
                    rightBorder=mid;
                    break;
                }
                left = mid + 1;
            } 
                
        }
        return rightBorder - leftBorder + 1;
        
    }
}

思路

  • 找到第一个k的索引和最后一个k的索引就知道有序数组中k出现的次数了
笔记
  • 二分法中,最好先判断arr[mid]>k, 接着arr[mid]<k, 剩下的便是arr[mid]==k, arr[mid]==k这个条件就不用显式地写出了, 见下面的循环写法
  • 注意下面程序中getFirst和getLast的递归终止条件的不同
  • 看到有序数组要联想二分法
  • int mid = (lo+hi)>>1;有溢出的风险, 最好写成int mid = lo+(hi-lo)>>1;
  • getFirst的递归终止条件是lo>hi
  • getLast的递归终止条件是lo==hi
  • getFirst会比getLast多进行一次递归,也就是lo==hi之后再多进行一次
  • 递归execute中的条件可以写成if((mid>lo && array[mid-1]!=k) || (mid==lo && array[mid]==k))也可以写成if((mid>0 && array[mid-1]!=k) || (mid==0 && array[mid]==k)),感觉写成前面那种更舒服些?
第三次做, 二分法,O(logN); 二分到死会有left>right, 本题中left>right说明数组中没有该元素; 其实更好的做法是找完左边界接着判断leftBorder是否等于-1 或者判断left是否大于right, 是的话说明数组中没有该元素, 直接返回0即可, 就不用再寻找右边界了; 最开始想着一次二分直接找出左右边界, 发现行不通, 还是得一个边界一个边界地找; 二分流程比之前的循环版本写得好一点, 尤其是else的处理

public class Solution {
    public int GetNumberOfK(int [] array , int k) {
       if(array==null || array.length==0)
           return 0;
        //找出左边界
        int left=0,right=array.length-1, mid, leftBorder=-1, rightBorder=-1;
        while(left<=right){
            mid = left + ((right-left)>>1);
            if(array[mid] < k)
                left = mid+1;
            else if(array[mid] > k)
                right = mid-1;
            else{
                if(mid==0){
                    leftBorder = mid;
                    break;
                }
                if(array[mid-1]!=k){
                    leftBorder = mid;
                    break;
                }
                right = mid - 1;
            }
        }
        //找出右边界
        left=0; 
        right=array.length-1;
        while(left<=right){
            mid = left + ((right - left) >> 1);
            if(array[mid] < k)
                left = mid + 1;
            else if(array[mid] > k)
                right = mid - 1;
            else{
                if(mid==array.length-1){
                    rightBorder = mid;
                    break;
                }
                if(array[mid+1]!=k){
                    rightBorder = mid;
                    break;
                }
                left = mid + 1;
            }
        }
        return left > right ? 0 : rightBorder - leftBorder + 1;
    }
}
第三次做, 双向遍历; while循环中更新数组索引后, 需要检查索引是否越界; O(N)
public class Solution {
    public int GetNumberOfK(int [] array , int k) {
       if(array==null || array.length==0)
           return 0;
        int left=0, right = array.length-1;
        while(left<array.length && array[left] != k)
            left++;
        while(right>=0 && array[right]!=k)
            right--;
        return left <= right ? right - left + 1 : 0;
    }
}
第二次做,二分法,循环版; 还是要明确:两个元素的mid指向靠左的元素; base case怎么处理? 可以写在while循环的第一个if里, 也就是第一个if是base case,else if, else是普遍情况; 这个二分法写的很不简洁,逻辑倒是还清晰; mid可以在while循环的最后更新,见最后一个写法
//再写个循环版本的二分法吧; while代替了调用递归; base case怎么解决?
public class Solution {
    public int GetNumberOfK(int [] array , int k) {
       if(array==null || array.length==0)
           return 0;
        int left = 0, right = array.length-1, mid;
        int leftBorder=-1, rightBorder=-1;
        //先找左边界
        while(left<=right){
            mid = left + ((right - left)>>1);
            if(array[mid] > k)
                right = mid - 1;
            else if(array[mid] < k)
                left = mid + 1;
            else{ // here, array[mid] == k
                if(mid!=0 && array[mid-1]==k)
                    right = mid - 1;
                else if(mid!=0 && array[mid-1]!=k){
                    leftBorder = mid;
                    break;
                }
                else{
                    leftBorder = mid;
                    break;
                }
            }
        }
        //如果数组中没有k,也就没必要找右边界了, 直接返回0即可
        if(leftBorder == -1)
            return 0;
        //寻找右边界
        left=0;
        right=array.length-1;
        while(left<=right){
            mid = left + ((right - left) >> 1);
            if(array[mid] > k)
                right = mid - 1;
            else if(array[mid] < k)
                left = mid + 1;
            else{//here, array[mid] == k
                if(mid==array.length-1){
                    rightBorder = mid;
                    break;
                }
                else if(array[mid+1]==k)
                    left = mid+1;
                else{
                    rightBorder=mid;
                    break;
                }
            }
        }
        return rightBorder - leftBorder + 1;
    }
}
第二次做,二分法;主要是要记住:两个元素的mid指向靠左的元素; 递归版
public class Solution {
    public int GetNumberOfK(int [] array , int k) {
       /*
       一次二分法中能找出左右边界吗?
       如果不能的话时间复杂度也是O(N)了,不如直接遍历一遍数组
       */
        if(array==null || array.length==0)
            return 0;
        //
        int left = CoreLeft(k, array, 0, array.length-1);
        int right = CoreRight(k, array, 0, array.length-1);
        if(left==-1)
            return 0;
        return right - left + 1;
    }
    //二分法找最左边的k,递归版
    public int CoreLeft(int k, int[] arr, int left, int right){
        //base case
        if(left==right && arr[left]==k)
            return left;
        if(left==right && arr[left]!=k)
            return -1;
        //
        int mid = left + ((right-left)>>1);
        int index;
        if(arr[mid] > k)
            index = CoreLeft(k, arr, left, mid-1);
        else if(arr[mid] < k)
            index = CoreLeft(k, arr, mid+1, right);
        else{
            //mid-1可能越界 毕竟只剩两个的话mid是左边那个,mid-1就越界了
            //比如:[3,3,3,3,4,5],3就会越界, 具体地mid=0,mid-1=-1越界
            //这个例子提醒我:要时刻记住,两个元素的mid是靠左的元素
            if(mid != 0 && arr[mid-1]==k)
                index = CoreLeft(k, arr, left, mid-1);
            else if(mid!=0 && arr[mid-1]!=k)
                index = mid;
            else
                index = mid; //要么这里决定最终的返回值,要么base case决定最终的返回值
        }
        return index;
    }
    //二分法找数组中最右边的k,递归版
    public int CoreRight(int k, int[] arr, int left, int right){
        //base case
        if(left==right && arr[left]==k)
            return left;
        //数组中没有k
        if(left==right && arr[left]!=k)
            return -1;
        //
        int mid = left + ((right - left )>>1);
        int index;
        if(arr[mid] > k)
            index = CoreRight(k, arr, left, mid-1);
        else if(arr[mid] < k)
            index = CoreRight(k, arr, mid+1, right);
        else{
            if(arr[mid+1]==k)
                index = CoreRight(k, arr, mid+1, right);
            else
                index = mid;//最终的返回值要么由这句决定,要么由base case决定
        }
        return index;
    }
}
第二次做,一遍循环非常方便
  • 题外话:因为数组是有序的,所以可以考虑二分法
public class Solution {
    public int GetNumberOfK(int [] array , int k) {
       int count=0;
        for(int i=0; i<array.length; i++)
            if(array[i] == k)
                //因为数组是排序的,所以不担心k出现在多段中
                count++;
        return count;
    }
}
第二次做,双向遍历写起来比较麻烦
public class Solution {
    public int GetNumberOfK(int [] array , int k) {
        if(array==null || array.length == 0)
            return 0;
        //双向遍历
        int left = 0, right = array.length-1;
        for(; left<array.length; left++)
            if(array[left]==k)
                break;
        for(; right>=0; right--)
            if(array[right]==k)
                break;
        if(left==array.length && array[0]==k && array[array.length-1]==k)
            return array.length;
        else if(left==array.length)
            return 0;
        else
            return right - left + 1;
    }
}
递归写法
public class Solution {
    public int GetNumberOfK(int [] array , int k) {
        //input check
        if(array.length<1)
            return 0;
        //execute
        int first = getFirst(array,0, array.length-1, k);
        int last = getLast(array,0, array.length-1, k);
        if(first==-1)
            return 0;
        return last-first+1;
    }
    //采用二分查找法查找k
    public int getFirst(int[] array, int lo, int hi, int k){
        //recursion finish
        if(lo>hi) // 满足这个条件说明array中没有k这个元素
            return -1;
        //execute
        int mid = (lo+hi)>>1;
        int index = -1;
        if(array[mid] == k){
            if((mid>0 && array[mid-1]!=k) || (mid==0 && array[mid]==k)) //这种if条件过于复杂
                index = mid;
            else 
                index = getFirst(array,lo, mid-1, k);
        }
        else if(array[mid]>k)
            index = getFirst(array, lo, mid-1, k);
        else if(array[mid]<k)
            index = getFirst(array, mid+1, hi, k);
        return index;
    }
    public int getLast(int[] array, int lo, int hi, int k){
        //resursion finish
        if(lo==hi)
            return array[lo]==k?lo:0;
        //execute
        int mid = (lo+hi)>>1;
        int index = -1;
        if(array[mid] == k){
            if((mid<array.length-1 && array[mid+1]!=k) || (mid==hi && array[mid]==k)) //这种if条件过于复杂
                index = mid;
            else 
                index = getLast(array,mid+1, hi, k);
        }
        else if(array[mid]>k)
            index = getLast(array, lo, mid-1, k);
        else if(array[mid]<k)
            index = getLast(array, mid+1, hi, k);
        return index;
    }
}
二分查找法的循环写法也务必熟悉,getLast是循环写法(自己的太笨重…)
public class Solution {
    public int GetNumberOfK(int [] array , int k) {
        //input check
        if(array.length<1)
            return 0;
        //execute
        int first = getFirst(array,0, array.length-1, k);
        int last = getLast(array,0, array.length-1, k);
        if(first==-1)
            return 0;
        return last-first+1;
    }
    //采用二分查找法查找k
    // 递归写法
    public int getFirst(int[] array, int lo, int hi, int k){
        //recursion finish
        if(lo>hi) // 满足这个条件说明array中没有k这个元素
            return -1;
        //execute
        int mid = (lo+hi)>>1;
        int index = -1;
        if(array[mid] == k){
            if((mid>lo && array[mid-1]!=k) || (mid==lo && array[mid]==k)) //这种if条件过于复杂
                index = mid;
            else 
                index = getFirst(array,lo, mid-1, k);
        }
        else if(array[mid]>k)
            index = getFirst(array, lo, mid-1, k);
        else if(array[mid]<k)
            index = getFirst(array, mid+1, hi, k);
        return index;
    }
    // 循环写法
    public int getLast(int[] array, int lo, int hi, int k){
        int mid=-1,index=-1;
        while(lo <= hi){
            mid = (lo+hi)>>1;
            // 应该先判断大于,小于的情况,剩下的便是等于的情况! 见别人的好答案
            if(array[mid]==k){
                if(mid+1>hi){
                    index = hi;
                    break;
                }
                else if(array[mid+1]==k) // mid+1 <= hi
                    lo = mid+1;
                else{
                    index = mid;
                    break;
                }
            }
            else if(array[mid] > k)
                hi = mid-1;
            else
                lo = mid+1;
        }
        return index;
    }
}
循环版本的二分法
public class Solution {
    public int GetNumberOfK(int [] array , int k) {
        int length = array.length;
        if(length == 0){
            return 0;
        }
        int firstK = getFirstK(array, k, 0, length-1);
        int lastK = getLastK(array, k, 0, length-1);
        if(firstK != -1 && lastK != -1){
             return lastK - firstK + 1;
        }
        return 0;
    }
    //递归写法
    private int getFirstK(int [] array , int k, int start, int end){
        if(start > end){
            return -1;
        }
        int mid = (start + end) >> 1;
        if(array[mid] > k){
            return getFirstK(array, k, start, mid-1);
        }else if (array[mid] < k){
            return getFirstK(array, k, mid+1, end);
        }else if(mid-1 >=0 && array[mid-1] == k){
            return getFirstK(array, k, start, mid-1);
        }else{
            return mid;
        }
    }
    //循环写法
    private int getLastK(int [] array , int k, int start, int end){
        int length = array.length;
        int mid = (start + end) >> 1;
        while(start <= end){
            if(array[mid] > k){
                end = mid-1;
            }else if(array[mid] < k){
                start = mid+1;
            //执行到这里要清晰地认识到: 此时满足条件array[mid]==k
            }else if(mid+1 < length && array[mid+1] == k){ 
                start = mid+1;
            }else{
                return mid;
            }
            mid = (start + end) >> 1;
        }
        return -1;
    }
}

题目二 0~n-1中缺失的数字

一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。
在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。

 

示例 1:

输入: [0,1,3]
输出: 2
示例 2:

输入: [0,1,2,3,4,5,6,7,9]
输出: 8。
第一次做; 有序数组; 二分法, 时间复杂度O(logN); 核心: 1)找到第一个nums[i]!=i的i, 此i就是缺失的数字 2)二分法的while循环条件中如果有等于, 那么必须用mid+1或者mid-1更新left或者right, 不能用mid更新left或者right, 会产生死循环; 如果while循环条件中没有等于, 那么可以用mid更新left或者right
/*
最简单的方法, 从头开始遍历数组, 第一个arr[i]!=i的i就是缺失的元素, 时间复杂度O(N)

找到最左边的那个arr[i]!=i的数字
相当于找左边界, 时间复杂度O(logN)
*/
class Solution {
    public int missingNumber(int[] nums) {
        int n = nums.length;
        int left=0, right=n-1, mid;
        /*
        大规律: 如果while循环条件中有等于, 那么更新left和right时不能和mid相等, 必须是mid+1或者mid-1, 否则会出现死循环
        */
        while(left<right){
            mid = left + ((right-left)>>1);
            if(nums[mid] != mid)
                right = mid;
            else
                left = mid+1;
        }
        
        return nums[left]==left? left+1 : left;
    }
}
力扣题解; 这种写法中, low指向nums[mid]!=mid的mid, 而high的指向不确定, 都有可能, 所以最后用low判断
class Solution {
    public int missingNumber(int[] nums) {
        int low = 0;
        int high = nums.length - 1;
        while(low < high){
            int mid = low + ((high - low) >> 1);
            if(nums[mid] != mid){
                high = mid - 1;
            } else {
                low = mid + 1;
            }
        }
        return nums[low] == low ? nums[low]+1 : nums[low]-1;
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值