leetcode 215. Kth Largest Element in an Array

目录

一、问题分析

二、代码实现

1、排序

采用内置排序

采用冒泡排序

基数排序

2、堆

采用大顶堆

采用小顶堆

3、快速选择

递归版本

迭代版本

将输入数组随机化

采用中间下标的元素作为参照元素

采用BFPTR算法对参照元素的选取进行优化


 

https://leetcode.com/problems/kth-largest-element-in-an-array/

返回数组中第k大的元素

 

一、问题分析

Input: [3,2,3,1,2,4,5,5,6] and k = 4Output: 4

输出的是第k大的元素,注意不是第k大的不同元素。另外,k总是大于等于1。

 

二、代码实现

1、排序

采用内置排序

采用Arrays.sort(),然后返回nums[nums.length-k]即可,时间为O(nlogn)。

import java.util.Arrays;
import java.util.Collections;
class Solution {
    public int findKthLargest1(int[] nums, int k) {
        
        //排序
        Arrays.sort(nums);
        return nums[nums.length-k]; 

        //Arrays.sort(nums, Collections.reverseOrder()); 
        //return nums[k - 1];
    }
}

采用冒泡排序

采用用冒泡的方式,进行k轮冒泡。

class Solution {
    
    public int findKthLargest1(int[] nums, int k) {
        
        //冒泡
        for (int i=0; i<k; i++) {   //控制冒泡的轮数为k
            for (int j=0; j<nums.length-1; j++) {
                if (nums[j] > nums[j+1]) {
                    int temp = nums[j];
                    nums[j] = nums[j+1];
                    nums[j+1] = temp;
                }
            }
        }        
        return nums[nums.length-k];
    }
}

基数排序

class Solution {
    
    //LSD radix sort
    public int findKthLargest9(int[] nums, int k) {
        final int R = (1 << 8);
        final int bitmask = R - 1;
        int[] aux = new int[nums.length];
        for (int i = 0; i < 4; i++) {
            int[] count = new int[R + 1];
            for (int num : nums) {
                int c = (num >>> (i * 8)) & bitmask;
                count[c + 1]++;
            }
            for (int r = 0; r < R; r++) count[r + 1] += count[r];
            if (i == 3) {
                int shift1 = count[R] - count[R/2];
                int shift2 = count[R/2];
                for (int r = 0; r < R/2; r++)
                    count[r] += shift1;
                for (int r = R/2; r < R; r++)
                    count[r] -= shift2;
            }
            for (int num : nums) {
                int c = (num >>> (i * 8)) & bitmask;
                aux[count[c]++] = num;
            }
            System.arraycopy(aux, 0, nums, 0, nums.length);
        }
        return nums[nums.length - k];
    }
    
}

2、堆

采用大顶堆

采用大顶堆装入所有元素,然后删除k-1个元素,剩下的堆顶元素即为所求,时间为O(n+klogn)。

import java.util.PriorityQueue;
import java.util.Collections;
class Solution {

    public int findKthLargest3(int[] nums, int k) {
        
        //大顶堆
        //PriorityQueue heap = new PriorityQueue(Collections.reverseOrder()); //error
        PriorityQueue<Integer> heap = new PriorityQueue<>(Collections.reverseOrder()); 
        //
        /*
        Queue<Integer> PQ= new PriorityQueue<>(nums.length, new Comparator<Integer>() {
			@Override
			public int compare(Integer o1, Integer o2) {
				if(o1>o2) return -1;
				else if(o1<o2) return 1;
				else return 0;
			}
    	} );
        */
        for (int i=0; i<nums.length; i++) {
            heap.add(nums[i]);
        }
        for (int i=0; i<k-1; i++) {
            heap.poll();
        }
        
        return heap.peek(); //error: incompatible types: Object cannot be converted to int
    }
    
}

采用小顶堆

采用小顶堆装入所有元素,同时控制元素个数在k个,当数组遍历完成之后,堆顶元素即为所求,时间为O(n*logk)。

import java.util.PriorityQueue;
import java.util.Collections;
class Solution {
    
    public int findKthLargest4(int[] nums, int k) {
        //小顶堆
        PriorityQueue<Integer> heap = new PriorityQueue<Integer>(k + 1);
        /*
        PriorityQueue<Integer> heap =
            new PriorityQueue<Integer>((n1, n2) -> n1 - n2);
        */

        for(int e : nums) {
            heap.add(e);
            
            if (heap.size() > k) {
                heap.poll();
            }
        }

        return heap.poll();
    }
    
}

3、快速选择

采用快排的划分方法,平均时间复杂度为O(n),最坏时间复杂度为O(n^2)。

递归版本

主要思路是不断划分数组,如果参照元素后面的元素个数count(包括参照元素)等于k,直接返回参照元素;如果小于k,则令k = k - count,在左边继续找;如果大于k,则k不变,在右边继续找。

class Solution {
    
    public int findKthLargest5(int[] nums, int k) {
        //快速选择
        return quickSelect(nums, 0, nums.length-1, k);
    }
    private int quickSelect(int[] nums, int low, int high, int k) {
        int pivot = low;
        
        for (int i=low; i<high; i++) {
            if (nums[i] <= nums[high]) {
                swap(nums, i, pivot++);
            }
        }
        swap(nums, high, pivot);
        
        int count = high - pivot + 1;    //大于等于nums[pivot]的元素个数
        if (count == k) {
            return nums[pivot];
        }
        if (count > k) {
            return quickSelect(nums, pivot+1, high, k);    //返回右边部分
        }
        return quickSelect(nums, low, pivot-1, k-count);  
    }
    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
    
}

迭代版本

所求元素在数组排序后的下标为index = nums.length-k(找第k大的元素等价于找第n-k+1小的元素),因此主要思路是不断划分数组,直到找到一个下标等于index的参照元素,将其返回。

class Solution { 
    
     public int findKthLargest6(int[] nums, int k) {
        k = nums.length - k;   //第k大的元素在排序后应该所在的位置
         
        int l = 0, r = nums.length - 1;
        while (l <= r) {
            int i = l; // partition [l,r] by A[l]: [l,i]<A[l], [i+1,j)>=A[l]
            for (int j = l + 1; j <= r; j++) {
                if (nums[j] < nums[l]) {
                    swap(nums, j, ++i);
                }
            }
            swap(nums, l, i);   //i-1为最后一个小于参照元素的元素的下标,i为参照元素

            if (k < i) r = i - 1;
            else if (k > i) l = i + 1;
            else return nums[i];    //找到一个下标等于所求下标的参照元素
        }
         
        return -1; //k非法的情况
    }
    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
    
}

将输入数组随机化

由于输入数组可能基本有序以及选取参照元素方式的单一,导致时间可能为O(n^2)。这里先对输入数组打乱所有元素的顺序,当然,也可以在选取参照元素的时候,采用随机元素或者下标为中间的元素。

class Solution {
    
    public int findKthLargest(int[] nums, int k) {
        shuffle(nums);
        return findK(nums, nums.length - k, 0, nums.length - 1);
    }
    private int findK(int[] nums, int k, int l, int r) {
        int pivotIndex = partition(nums, l, r);

        if (k < pivotIndex)
            return findK(nums, k, l, pivotIndex - 1);
        else if (k > pivotIndex)
            return findK(nums, k, pivotIndex + 1, r);
        else // k == pivotIndex
            return nums[k];
    }
    private int partition(int[] a, int lo, int hi) {
        int pivotIdx = lo; // bad practice
        int pivotVal = a[pivotIdx];

        // put pivot last
        swap(a, pivotIdx, hi);
        int storeIdx = lo;

        for (int i = lo; i < hi; i++) {
            if (a[i] <= pivotVal) {
                swap(a, i, storeIdx);
                storeIdx++;
            }
        }
        swap(a, storeIdx, hi);  //storeIdx+1是第一个大于参照元素的元素,storeIdx为参照元素
        return storeIdx;
    }
    private void shuffle(int[] a) {
        for (int i = a.length - 1; i >= 0; i--) {
            int j = (int) (Math.random() * (i + 1)); // j=0..i
            swap(a, i, j);
        }
    }
    private void shuffle2(int[] a) {
        final Random random = new Random();

        for(int ind = 1; ind < a.length; ind++) {
            final int r = random.nextInt(ind + 1);
            exch(a, ind, r);
        }
    }
    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
    
}

采用中间下标的元素作为参照元素

在区间[left, right]中选取参照元素时,选取的参照元素nums[mid]满足nums[left] <= nums[mid] <= nums[right],可以有效避免最坏情况的出现(参照元素总是选中区间最大值或最小值)。此种方式比前面几种快得多,运行时间可以超过98%的Java提交。

class Solution {
    
    public int findKthLargest8(int[] nums, int k) {
        return select(nums, k-1);
    }
    // Quick select
    private int select(int[] nums, int k) {
        int left = 0, right = nums.length-1;
        while(true) {
            if(left == right)
                return nums[left];
            int pivotIndex = medianOf3(nums, left, right);
            pivotIndex = partition(nums, left, right, pivotIndex);
            if(pivotIndex == k)
                return nums[k];
            else if(pivotIndex > k)
                right = pivotIndex-1;
            else
                left = pivotIndex+1;
        }
    }
    //Use median-of-three strategy to choose pivot,很强大的选取参照元素操作的优化方式
    private int medianOf3(int[] nums, int left, int right) {    
        //让nums[right]<=nums[mid]<=nums[left] 
        int mid = left + (right - left) / 2;
        if(nums[right] > nums[left])
            swap(nums, left, right);
        if(nums[right] > nums[mid])
            swap(nums, right, mid);
        if(nums[mid] > nums[left])
            swap(nums,left, mid);
        
        //返回mid
        return mid;
    }
    private int partition(int[] nums, int left, int right, int pivotIndex) {
        int pivotValue = nums[pivotIndex];
        swap(nums, pivotIndex, right);
        
        int index = left;
        for(int i = left; i < right; ++i) {
            if(nums[i] > pivotValue) {
                swap(nums, index, i);
                ++index;
            }
        }
        swap(nums, right, index);
        
        return index;
    }
    
}

采用BFPTR算法对参照元素的选取进行优化

通过选取中位数的中位数作为参照元素,来避免单一地选取最低/最高下标的元素所导致的最坏情况(O(n^2))的发生。具体来说,把元素分为元素个数为5的小组(最后一组元素个数可能为n mod 5),分别进行插入排序。将各个小组的中位数移到数组前面,然后重复以上操作,直到只剩一个元素,将其作为参照元素。

class Solution {
    
    public int findKthLargest(int[] nums, int k) {
        return bfprt(nums, 0, nums.length-1, k);
    }
    private int bfprt(int[] a, int l, int r, int k) {
        int p = findMid(a, l, r);   //寻找中位数的中位数
        int i = partition111(a, l, r, p);
        
        //int m = i - l + 1;
        int m = r - i + 1;
        if (m == k) {
            return a[i];
        }
        if (m > k) {
            //return bfprt(a, l, i-1, k);
            return bfprt(a, i+1, r, k);
        }
        //return bfprt(a, i+1, r, k-m);
        return bfprt(a, l, i-1, k-m);
    }
    private int findMid(int[] a, int l, int r) {
        if (l == r) {
            return l;
        }
        
        //处理前几组元素
        int i = 0, n = 0;
        for (i=l; i<r-5; i+=5) {
            insertionSort(a, i, i+4);
            n = i - l;
            swap(a, l+n/5, i+2);
        }
        //处理最后一组元素(剩余元素)
        int num = r - i + 1;
        if (num > 0) {
            insertionSort(a, i, i+num-1);
            n = i - l;
            swap(a, l+n/5, i+num/2);
        }
        
        n /= 5;
        if (n == 0) {
            return l;
        }
        return findMid(a, l, l + n);
        
    }
    private void insertionSort(int[] a, int l, int r) {
        for (int i=l+1; i<=r; i++) {
            if (a[i-1] > a[i]) {
                int temp = a[i];
                int j = i;
                while (i>i && a[j-1]>temp) {
                    a[j] = a[j-1];
                    j--;
                }
                a[j] = temp;
            }
        }
    }
    private int partition(int[] a, int l, int r, int p) {
        swap(a, l, p);
        int pivot = a[l];
        
        int i = l, j = r;
        while (i < j) {
            while (a[j]>pivot && i<j) {
                j--;
            }
            a[i] = a[j];
            while (a[i]<=pivot && i<j) {
                i++;
            }
            a[j] = a[i];
        }
        a[i] = pivot;
        return i;
    }
    
}

 

参考:

https://zhuanlan.zhihu.com/p/31498036

https://blog.csdn.net/laojiu_/article/details/54986553

https://www.jianshu.com/p/495e5019669c

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值