LeetCode-215. Kth Largest Element in an Array(6种写法(包括BFPRT算法))

LeetCode-215. Kth Largest Element in an Array(6种写法(包括BFPRT算法))

  • 最小堆(非递归调整)
  • 最小堆(递归调整)
  • 最大堆
  • Hash思想
  • 分治解法(利用快排的partition过程)
  • BFPRT算法

题目链接

题意

在未排序的数组中找到第 k 个最大的元素。注意这个和找第K小的数,以及求出最小(大)的k个数是一样的,包括剑指Offer里面的找第K小的数

最小堆(非递归调整)

最小堆的方法就是先用K个数建成一个堆,然后后面的数和堆顶(最小的数)比较,如果大于堆顶,就替换这个堆顶,然后调整堆,最后,堆顶就是答案。这个方法可以达到O(N*logK)的时间复杂度

  /**
     * 使用堆解决  使用一个小根堆(堆顶最小)
     * 非递归调整堆
     */
    public int findKthLargest(int[] nums, int k) {
        if(nums == null || nums.length == 0)return Integer.MAX_VALUE;
        int[] KHeap = new int[k]; //建成一个有K个数的堆
        for(int i = 0; i < k; i++){
            heapInsert(KHeap,nums[i],i);
        }
        //后面的数如果
        for(int i = k ; i < nums.length; i++){
            if(nums[i] > KHeap[0]){
                KHeap[0] = nums[i];
                heapfiy(KHeap,0,k); //调整从0到k
            }
        }
        return KHeap[0];
    }

    //插入数一直往上面调整的过程
    private void heapInsert(int[] KHeap, int num, int index) {
        KHeap[index] = num;
        while(KHeap[index] < KHeap[(index-1)/2]){
            swap(KHeap,index,(index-1)/2);
            index = (index-1)/2;
        }
    }
    //这个函数就是一个数变大了,往下沉的函数,改变的数为index  目前的自己指定的堆的大小为heapSize
    private void heapfiy(int[] kHeap, int index, int heapSize) {
        int left = 2*index + 1;
        while (left < heapSize){
            int minIndex = left + 1 < heapSize && kHeap[left+1] < kHeap[left] ? left + 1 : left;
            minIndex = kHeap[index] < kHeap[minIndex] ? index : minIndex;
            if(minIndex == index)break; //自己就是最大的不用往下面沉
            swap(kHeap,index,minIndex);
            index = minIndex;
            left = 2*index + 1;
        }
    }
    private void swap(int[] arr,int a,int b){
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }

最小堆(递归调整)

这里只是把上面调整堆的过程改成递归写一下。

     /**
     * 最小堆调整的递归的写法
     */
    public int findKthLargest(int[] nums, int k) {
        if(nums == null || nums.length == 0)return Integer.MAX_VALUE;
        int[] KHeap = new int[k]; //建成一个有K个数的堆
        for(int i = 0; i < k; i++){
            heapInsert(KHeap,nums[i],i);
        }
        //后面的数如果
        for(int i = k ; i < nums.length; i++){
            if(nums[i] > KHeap[0]){
                KHeap[0] = nums[i];
                heapfiyRecursion(KHeap,0,k); //调整从0到k
            }
        }
        return KHeap[0];
    }
     //插入数一直往上面调整的过程
    private void heapInsert(int[] KHeap, int num, int index) {
        KHeap[index] = num;
        while(KHeap[index] < KHeap[(index-1)/2]){
            swap(KHeap,index,(index-1)/2);
            index = (index-1)/2;
        }
    }
    public void heapfiyRecursion(int[] data,int i,int size) { //从A[i] 开始往下调整
        int lchild = 2*i+1; //左孩子的下标
        int rchild = 2*i+2;//右孩子的下标
        int mini = i;
        if(lchild < size && data[lchild] < data[mini])mini = lchild;
        if(rchild < size && data[rchild] < data[mini])mini = rchild;
        if(mini != i) {
            swap(data,i,mini);  //把当前结点和它的最大(直接)子节点进行交换
            heapfiyRecursion(data,mini,size);  //继续调整它的孩子
        }
    }

    private void swap(int[] arr,int a,int b){
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }

最大堆

最大堆的做法和最小堆有点不同,一开始将整个数组中的元素构成一个最大堆,这时堆顶元素是最大的,连续将堆顶元素弹出k-1次(每次弹出后都要调整)后堆顶元素就是第k大的数了。

  public int findKthLargest(int[] nums, int k) {
        if(nums == null || nums.length == 0)return Integer.MAX_VALUE;
        int[] KHeap = new int[nums.length]; //建成一个有K个数的堆
        for(int i = 0; i < nums.length; i++){
            heapInsert2(KHeap,nums[i],i);
        }
        int size = nums.length;
        for(int i = 0; i < k-1; i++){ //弹出k-1个
            swap(KHeap,0,size-1);
            size--;
            heapfiy2(KHeap,0,size);
        }
        return KHeap[0];
    }


    //插入数一直往上面调整的过程
    private void heapInsert2(int[] KHeap, int num, int index) {
        KHeap[index] = num;
        while(KHeap[index] > KHeap[(index-1)/2]){
            swap(KHeap,index,(index-1)/2);
            index = (index-1)/2;
        }
    }
    //这个函数就是一个数变大了,往下沉的函数,改变的数为index  目前的自己指定的堆的大小为heapSize
    private void heapfiy2(int[] kHeap, int index, int heapSize) {
        int left = 2*index + 1;
        while (left < heapSize){
            int maxIndex = left + 1 < heapSize && kHeap[left+1] > kHeap[left] ? left + 1 : left;
            maxIndex = kHeap[index] > kHeap[maxIndex] ? index : maxIndex;
            if(maxIndex == index)break; //自己就是最大的不用往下面沉
            swap(kHeap,index,maxIndex);
            index = maxIndex;
            left = 2*index + 1;
        }
    }
    private void swap(int[] arr,int a,int b){
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }

Hash思想

这个是提交的时候,点击了最快的那个解答看了一下,觉得有点厉害。

     /**
     * Hash思想
     */
    public int findKthLargest(int[] nums, int k) {
        if(nums == null || nums.length == 0)return Integer.MAX_VALUE;
        int max = nums[0],min = nums[0];
        for(int i = 0; i < nums.length; i++){
            if(nums[i] > max)max = nums[i];
            if(nums[i] < min)min = nums[i];
        }
        int[] arr = new int[max - min + 1];
        for(int n : nums) arr[max - n]++;
        int sum = 0;
        for(int i = 0; i < arr.length; i++){
            sum += arr[i];
            if(sum >= k){
                return max - i;
            }
        }
        return 0;
    }

分治解法(利用快排的partition过程)

这个方法就是利用快排的partition将数组分成左边部分大于某个数,中间部分等于某个数,右边部分小于某个数(如果是求第k小的就是左边小于某个数,中间等于某个数,右边大于某个数),然后我们每次划分之后,递归的从左边或者从右边去找第K大的数,直到找到在等于部分的。

    /**
     * 分治
     */
    public int findKthLargest(int[] nums, int k) {
        if(nums == null || nums.length == 0)return Integer.MAX_VALUE;
        return process(nums,0,nums.length-1,k-1); //一定要注意这里是k-1
    }

    public int process(int[] arr,int L,int R,int k){
        if(L == R) return arr[L];
        int[] p = partition(arr,L,R,arr[L + (int)(Math.random() * (R-L+1))]); //随机选一个数划分
        if(k >= p[0] && k <= p[1]){
            return arr[k];
        }else if(k < p[0]){
            return process(arr,L,p[0]-1,k);
        }
        else return process(arr,p[1]+1,R,k);
    }

    //划分函数 左边的都比num大,右边的都比num小  用一个数组来记录和num相等的下标的下限和上限
    public int[] partition(int[] arr,int L,int R,int num){
        int less = L-1; //小于部分的最后一个数
        int more = R+1;
        int cur = L;
        while( cur < more){
            if(arr[cur] > num){
                swap(arr,++less,cur++); //把这个比num大的数放到大于区域的下一个,并且把小于区域扩大一个单位
            }else if(arr[cur] < num){
                swap(arr,--more,cur); //把这个比num小的数放到小于去余的下一个,并且把小于区域扩大一个单位
                //同时,因为从小于区域拿过来的数是未知的,所以不能cur++ 还要再次判断一下arr[cur]
            }else{
                cur++;
            }
        }
        return new int[]{less + 1,more - 1};  //返回的是等于区域的两个下标
    }
    private void swap(int[] arr,int a,int b){
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }

BFPRT算法

这个方法其实是在上面的分治方法上面的改进,唯一的不同就是寻找那个划分的数的不同,上面的数是随机生成的数,而BFPRT算法能找到那样一个数,使得划分的时候左右两边相对均匀,看下面的具体的求解过程:
这里写图片描述
看下面的一个例子:
这里写图片描述

    public  int findKthLargest(int[] arr, int K) {
        int[] copyArr = copyArray(arr); //不改变原来的数组
        return select(copyArr, 0, copyArr.length - 1, K - 1);
    }

    public int[] copyArray(int[] arr) {
        int[] res = new int[arr.length];
        for (int i = 0; i != res.length; i++) {
            res[i] = arr[i];
        }
        return res;
    }

    public int select(int[] arr, int L, int R, int k) {
        if (L == R) {
            return arr[L];
        }
        int pivot = medianOfMedians(arr, L, R); //获得这个划分的标准
        int[] p = partition(arr, L, R, pivot);
        if (k >= p[0] && k <= p[1]) {
            return arr[k];
        } else if (k < p[0]) {
            return select(arr, L, p[0] - 1, k);
        } else {
            return select(arr, p[1] + 1, R, k);
        }
    }

    //划分函数 左边的都比num大,右边的都比num小  用一个数组来记录和num相等的下标的下限和上限
    public int[] partition(int[] arr,int L,int R,int num){
        int less = L-1; //小于部分的最后一个数
        int more = R+1;
        int cur = L;
        while( cur < more){
            if(arr[cur] > num){
                swap(arr,++less,cur++); //把这个比num大的数放到大于区域的下一个,并且把小于区域扩大一个单位
            }else if(arr[cur] < num){
                swap(arr,--more,cur); //把这个比num小的数放到小于去余的下一个,并且把小于区域扩大一个单位
                //同时,因为从小于区域拿过来的数是未知的,所以不能cur++ 还要再次判断一下arr[cur]
            }else{
                cur++;
            }
        }
        return new int[]{less + 1,more - 1};  //返回的是等于区域的两个下标
    }

    //划分成5组,取出每一组中的中位数,组成一个中位数组
    public  int medianOfMedians(int[] arr, int L, int R) {
        int num = R - L + 1;
        int offset = num % 5 == 0 ? 0 : 1;
        int[] mArr = new int[num / 5 + offset];
        for (int i = 0; i < mArr.length; i++) {
            int beginI = L + i * 5;
            int endI = beginI + 4;
            mArr[i] = getMedian(arr, beginI, Math.min(R, endI));
        }
        return select(mArr, 0, mArr.length - 1, mArr.length / 2);
    }


    //得到中位数
    public int getMedian(int[] arr, int L, int R) {
        insertionSort(arr, L, R);
        int sum = L + R;
        int mid = (sum / 2) + (sum % 2);
        return arr[mid];
    }

    //插入排序
    public void insertionSort(int[] arr, int L, int R) {
        for (int i = L + 1; i <= R ; i++){
            for(int j = i; j > L && arr[j] < arr[j-1]; j--)swap(arr,j,j-1);
        }
    }

    private void swap(int[] arr,int a,int b){
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }

只要得到了第K(小/大)的数,要得到最小(大)的K个数,就很简单了,再遍历一遍就可以了,如下面的函数,得到最小的K个数:

    public int[] getMinKNums(int[] arr, int k) {
        if (k < 1 || k > arr.length) {
            return arr;
        }
        int minKth = findKthLargest(arr, k);
        int[] res = new int[k];
        int index = 0;
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] < minKth) {
                res[index++] = arr[i];
            }
        }
        for (; index < res.length; index++) {
            res[index] = minKth;
        }
        return res;
    }

猜你喜欢

转载自blog.csdn.net/zxzxzx0119/article/details/81509018