剑指offer面试题39(java版):数组中出现次数超过一半的数字

welcome to my blog

剑指offer面试题39(java版):数组中出现次数超过一半的数字

题目描述

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。

第四次做; 位运算的思想: 如果一个数字的出现次数超过了数组长度的一半, 那么这个数字二进制的各个bit的出现次数同样超过了数组长度的一半

class Solution {
    public int majorityElement(int[] nums) {
        int[] bit = new int[32];
        int n = nums.length;
        for(int a : nums){
            for(int i=0; i<32; i++){
                //无符号右移; 负数的无符号右移移动的是补码还是原码?
                if(((a>>>i) & 1) == 1)
                    bit[i]++;
            }
        }
        
        int res=0;
        for(int i=0; i<32; i++){
            if(bit[i]>n/2)
                res = res | (1<<i);
        }
        return res;
    }
}

第四次做; 分治思想; 本题中使用分治的方法得到的不是众数, 那得到的是什么? 这么说吧, 如果某个元素出现的次数超过了长度的一半, 那么使用分治的思想一定可以得到这个数; 反例: {2,2,3,3,2,2,5,5}, 使用下面的算法返回的是5; 虽然众数是2, 但是2的出现次数没有大于数组长度的一半,所以算法失效了

/*
分治算法; 归并排序就属于分治算法
本题中使用分治的方法得到的不是众数, 那得到的是什么?
这么说吧, 如果某个元素出现的次数超过了长度的一半, 那么使用分治的思想一定可以得到这个数
再次强调: 某个元素出现的次数超过了长度的一半, 该算法才有效

反例:
{2,2,3,3,2,2,5,5}, 使用下面的算法返回的是5; 虽然众数是2, 但是2的出现次数没有大于数组长度的一半,所以算法失效了
*/
class Solution {
    public int majorityElement(int[] nums) {
        return core(nums, 0, nums.length-1);
    }
    private int core(int[] nums, int left, int right){
        //base case
        if(left==right)
            return nums[left];
        //
        int mid = left + ((right-left)>>1);
        int leftRes = core(nums, left, mid);
        int rightRes = core(nums, mid+1, right);
        if(leftRes == rightRes)
            return leftRes;
        int leftCount = count(nums, left, mid, leftRes);
        int rightCount = count(nums, mid+1, right, rightRes);
        return leftCount>rightCount? leftRes:rightRes;
    }
    private int count(int[] arr, int left, int right, int target){
        int count = 0;
        for(int i=left; i<=right; i++){
            if(arr[i] == target)
                count++;
        }
        return count;
    }
}

思路1

  • 出发点:数组中出现次数超过数组长度一半的元素, 出现的次数比其他元素加起来都多(这是个充分必要条件)
  • 充要条件没法直接用
  • 可以做到扫描一遍数组找到出现次数最多的元素。出现次数最多是必要不充分条件。

笔记

  • 掌握基础:扫描一遍数组找到出现次数最多的元素

第三次做,核心:分解问题 ? ; 原问题:找出出现的次数超过数组长度的一半的元素; 分解为两个问题:1)找出出现次数最多的元素;2)检查这个元素的出现次数是否超过数组长度的一半

  • 代码中的for循环找出的元素有可能是出现次数超过数组长度的一半的元素, 也有可能是出现次数最多的元素, 还有可能是最后一个元素, 例子1,2,2,3,4,5 找出的是5
/*
核心:分解问题
原问题:找出出现的次数超过数组长度的一半的元素
分解为两个问题:1)找出出现次数最多的元素;2)检查这个元素的出现次数是否超过数组长度的一半
*/
public class Solution {
    public int MoreThanHalfNum_Solution(int [] array) {
        if(array==null || array.length==0)
            return 0;
        if(array.length==1)
            return array[0];
        //1)找出出现次数最多的元素
        // 这个方法并不能一定找出出现次数最多的元素, 找出的元素有可能是出现次数超过一半的元素, 需要验证
        int curr=array[0], count=1;
        for(int i=1; i<array.length; i++){
            if(array[i] != curr){
                count--;
                if(count==0){
                    curr=array[i];
                    count=1;
                }
            }
            else
                count++;
        }
        //2)检查这个元素的出现次数是否超过数组长度的一半
        count=0;
        for(int i=0; i<array.length; i++){
            if(array[i]==curr)
                count++;
        }
        return count > array.length/2 ? curr : 0;
    }
}

第二遍做(我写的是错的, 我只进行了一次partition, 正确的做法是对数组完整的排序),使用的必要条件:满足要求的元素中一定有一个元素位于数组中间

  • 为什么这个代码是错的呢? 例如2,2,1,2,3. 如果划分值是3, 那么partition之后的结果是2,2,1,2,3. 中间的元素不是出现次数最多的元素!
  • 数组中间的元素索引为(N-1)/2. N为奇数时正好指向中间元素,N为偶数时指向中间两个元素中靠左的那个元素
public class Solution {
    public int MoreThanHalfNum_Solution(int [] array) {
        /*
        之前写的方案是使用了必要条件之一: 满足要求的元素是数组中出现次数最多的元素
        现在用一用另一个必要条件:满足要求的元素一定出现在arr[N/2]处, 配合partition使用
        */
        if(array==null || array.length < 1 )
            return 0;
        //
        partition(array, 0, array.length-1);
        int curr = array[(array.length-1)/2], count = 0;
        for(int i=0; i<array.length; i++){
            if(array[i] == curr)
                count++;
        }
        return count > array.length/2 ? curr : 0;
    }
    public void partition(int[] arr, int left, int right){
        int random = (int)(Math.random()*(right-left+1)+left);
        swap(arr,random, right);
        int small=left-1;//小于区
        int big=right;//大于区
        while(left<big){
            if(arr[left] < arr[right])
                swap(arr, left++, ++small);
            else if(arr[left] > arr[right])
                swap(arr, left, --big);
            else
                left++;
        }
        swap(arr, big, right);//将划分值放到大于区的边界
        //return new int[]{small+1, big};//没用到快排,不用返回等于区边界
    }
    public void swap(int[] arr, int i, int j){
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

第二遍做,使用的必要条件:满足要求的元素一定是数组中出现次数最多的元素

public class Solution {
    public int MoreThanHalfNum_Solution(int [] array) {
        if(array==null || array.length<1)
            return 0;
        int curr=array[0], count=1;
        for(int i=1; i<array.length; i++){
            if(array[i] == curr)
                count++;
            else{
                count--;
                if(count==0){
                    curr = array[i];
                    count = 1; //第一次忘记更新次数。 更新curr必须更新count
                }
            }
        }
        //count>0,说明对应的curr是数组中出现次数最多的元素,需要进一步检查是否满足出现次数大于数组长度一半的条件
        if(count>0){
            count=0;//记录出现次数最多的元素的出现次数
            for(int i=0; i<array.length;i++)
                if(array[i]==curr)
                    count++;
        }
        return count > array.length/2 ? curr : 0;
    }
}
public class Solution {
    public int MoreThanHalfNum_Solution(int [] array) {
        //input check
        if(array.length<1)
            return 0;
        //execute
        /*
        出发点:数组中出现次数超过数组长度一半的元素, 出现的次数比其他元素加起来都多(实际上这只是必要条件,并不是充分条件! 反例见下面的分析), 所以可以这样做
        用curr记录当前的元素, count表示当前元素出现的次数.
        如果下一个元素和curr相等, 则count++
        如果下一个元素和curr不等,则count--
            如果count==0, 那么令curr等于下一个元素, count=1
        循环结束后,如果,count>0, 那么curr就是结果. 不对. 因为如果
        (有没有可能, count>0, curr不是正确的结果? 这就需要证明最开始的出发点是不是正确的. 看起来出发点不够完备, 反例:[1,2,3,2,4,2,5,2,3])
        */
        int curr = array[0];
        int count = 1;
        for(int i=1; i<array.length; i++){
            if(array[i] == curr)
                count++;
            else{
                count--;
                if(count==0){
                    curr = array[i];
                    count = 1;
                }
            }
        }
        //check result 检查curr是不是出现次数超过数组长度的一半
        count = 0;
        for(int i=0; i<array.length; i++){
            if(array[i]==curr)
                count++;
        }
        if(count > array.length/2)
            return curr;
        return 0;
    }
}

笔记

  • 务必掌握快速排序, 由其弄清楚partition的写法, 比如处理输入长度为1的数组{6}

思路2 快速排序的partition

  • 出发点:对于出现次数超过数组长度一半的元素,对该数组进行排序,排序后索引为N/2的元素就是结果
  • 上面的出发点仍然是个必要条件.
  • 可以想象一个长度大于N/2的滑窗在数组中滑动,无论怎么滑动都会包含索引为N/2的元素. 如果相同的元素挨在一起,也就是对元素进行排序的话,出现次数超过N/2的元素组成的滑窗中一定包含索引为N/2的元素
  • 相当于找一组数中的中位数,可以采用快速排序中的partition想法
public class Solution {
    public int MoreThanHalfNum_Solution(int [] array) {
        //input check
        if(array.length < 1)
            return 0;
        //execute
        int index = partition(array, 0, array.length-1);
        while(true){
            if(index ==  array.length/2)
                break;
            else if(index < array.length/2)
                index = partition(array, index+1, array.length-1);
            else
                index = partition(array, 0, index-1);
        }
        int curr = array[index];
        int count = 0;
        for(int i=0; i<array.length; i++)
            if(array[i]==curr)
                count++;
        if(count > array.length/2)
            return curr;
        return 0;
    }
    public int partition(int[] array, int lo, int hi){
        int pivot = array[lo];
        int i=lo, j=hi+1;
        while(true){
            while(++i < array.length && array[i] < pivot)
                if(i==hi)
                    break;
            while(--j >= 0 && array[j] >= pivot) //这里一定要用>=,不能只用>
                if(j==lo)
                    break;
            if(i>=j)
                break;
            swap(array, i, j);
        }
        swap(array, lo, j);
        return j;
    }
    public void swap(int[] array, int i, int j){
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
}

稍微简洁些

  • index就是第K个数字, 所以index等于K-1时即可
import java.util.ArrayList;
public class Solution {
    public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
        // 基于partition的方法
        //input check
        ArrayList<Integer> al = new ArrayList<Integer>();
        if(input.length < k || k<=0)
            return al;
        //execute
        int index = partition(input, 0, input.length-1);
        while(index != k-1){
            if(index > k-1)
                index = partition(input, 0, index-1);
            if(index < k-1)
                index = partition(input, index+1, input.length-1);
        }
        for(int i=0; i<=index; i++)
            al.add(input[i]);
        return al;
    }
    public int partition(int[] input, int lo, int hi){
        //注意input只有一个元素的情况, 此时要注意数组索引是否越界
        int i=lo, j=hi+1, pivot=input[lo];
        while(true){
            while(++i < input.length && input[i] < pivot)
                if(i==hi)
                    break;
            while(--j >= 0 && input[j] >= pivot)
                if(j==lo)
                    break;
            if(i>=j)
                break;
            swap(input, i, j);
        }
        swap(input, lo, j);
        return j;
    }
    public void swap(int[] input, int i, int j){
        int temp = input[i];
        input[i] = input[j];
        input[j] = temp;
    }
}

你可能感兴趣的:(剑指offer,剑指offer)