【LeetCode笔记】215. 数组中的第K个最大元素(Java、快排、堆排、并发快排)

题目描述

  • 大名鼎鼎的TOP K,主要考察排序
  • 快排 & 堆排
    在这里插入图片描述

思路 & 代码

快排

  • 没啥好说的,就是快排结束后,返回倒数第K个数字即可。
  • 重点就在于快排的实现了,好久没敲了= =:
  1. 原理:使用标志位进行分治的排序,每趟能让标志位的左边都不大于标志位,右边都不小于标志位。
  2. 注意点:边界问题、起始位置等,详见代码注释
  3. 留个坑:随机化mark,防止退化到 O( n 2 n^2 n2)
class Solution {
    public int findKthLargest(int[] nums, int k) {
        myQuickSort(nums, 0, nums.length - 1);
        return nums[nums.length - k];
    }
    void myQuickSort(int[] nums, int left, int right){
        if(left >= right){
            return;
        }
        int mark = nums[left];
        int i = left, j = right;
        // 总共进行四次 i < j 的判断!
        while(i < j){
            // 从右边开始找,避免标志位本就应在最左边的情况导致出错。
            while(i < j && nums[j] >= mark){
                j--;
            }
            // 注意 ‘=’ 噢~
            while(i < j && nums[i] <= mark){
                i++;
            }
            if(i < j){
                int temp = nums[i];
                nums[i] = nums[j];
                nums[j] = temp;
            }
        }
        // 结束后 i == j,此时nums[i] 一定不大于 nums[left](可证)
        // 把标志位放到正确的位置
        nums[left] = nums[i];
        nums[i] = mark;
        myQuickSort(nums, left, i - 1);
        myQuickSort(nums, i + 1, right);
    }
}
基于 Fork / Join 的并发快排
  • 效率直接从 34s 提高到 8s,太狠了
  • fork / join简直是快排的指定好兄弟,又好用效率又高。
class Solution {
    public int findKthLargest(int[] nums, int k) {
        // 开池,放任务
        ForkJoinPool forkJoinPool = new ForkJoinPool(Runtime.getRuntime().availableProcessors());
        forkJoinPool.invoke(new SortTask(nums, 0, nums.length - 1));
        
        return nums[nums.length - k];
    }

    class SortTask extends RecursiveAction {
        int[] arr;
        int left;
        int right;

        public SortTask(int[] arr, int left, int right) {
            this.arr = arr;
            this.left = left;
            this.right = right;
        }

        @Override
        protected void compute() {
            // 1、任务结束
            if(left >= right) return;

            // 2、当前任务执行
            int mark = arr[left];
            int i = left, j = right;
            while(i < j) {
                while(i < j && arr[j] >= mark) j--;
                while(i < j && arr[i] <= mark) i++;
                if(i < j) {
                    int temp = arr[i];
                    arr[i] = arr[j];
                    arr[j] = temp;
                }
            }
            arr[left] = arr[i];
            arr[i] = mark;

            // 3、任务拆分、执行
            SortTask leftTask = new SortTask(arr, left, i - 1);
            SortTask rightTask = new SortTask(arr, i + 1, right);
            leftTask.fork();
            rightTask.fork();
            leftTask.join();
            rightTask.join();
        }
    }
}
针对 topK 的快排优化
  • 能达到 Fork / Join 的效率,并且不需要额外线程资源
  • 关键在于局部有序,对于 topK 问题来说,我们只关心第K个最大元素。
  • 因此实际上并不需要做到全局有序,可以在每次递归时都只选择一个区间进行递归
  • 时间复杂度:等待有缘人补充
class Solution {
    public int findKthLargest(int[] nums, int k) {
        sort(nums, 0, nums.length - 1, k);
        return nums[nums.length - k];
    }

    void sort(int[] nums, int left, int right, int k) {
        if(left >= right) return;
        int mark = nums[left];
        int i = left, j = right;
        while(i < j) {
            while(i < j && nums[j] >= mark) j--;
            while(i < j && nums[i] <= mark) i++;
            if(i < j) {
                int temp = nums[i];
                nums[i] = nums[j];
                nums[j] = temp;
            }
        }
        nums[left] = nums[i];
        nums[i] = mark;
        int rightNums = right - i + 1;
        if(rightNums < k) sort(nums, left, i - 1, k - rightNums); // 走左,右边全部拿走
        else sort(nums, i + 1, right, k); // 走右,不变
    }
}

堆排

  • 可以直接用堆排,也可以针对题目变形一下(会快很多)。这里都贴一下
基本堆排
  • 三个主要函数:heapify、buildTree 与 myHeapSort
  • 逻辑上是完全二叉树,通过数组实现
  • Parent = (son - 1) / 2
  • Son1 = Parent * 2 + 1。 Son2 = Son1 + 1。
  • heapify:递归向下
  • buildTree:从下(第一个Parent)往上遍历进行heapify
  • myHeapSort:每次都取最值,与结尾交换。然后缩小范围继续。
class Solution {
    public int findKthLargest(int[] nums, int k) {
        // 堆排序,类似快排做法
        myHeapSort(nums);
        return nums[nums.length - k];
    }

    void swap(int[] nums, int one, int two){
        int temp = nums[one];
        nums[one] = nums[two];
        nums[two] = temp;
    }

	// 自顶向下;n 不一定等于 nums.length 噢~
    void heapify(int[] nums, int n, int now){
        // 递归结束条件
        if(now >= n){
            return;
        }
        // 选出最大值对应下标:两次对比
        int maxIndex = now;
        int son1 = 2 * now + 1;
        int son2 = son1 + 1;
        // 判断是否超出边界。
        if(son1 < n && nums[maxIndex] < nums[son1]){
            maxIndex = son1;
        }
        if(son2 < n && nums[maxIndex] < nums[son2]){
            maxIndex = son2;
        }
        // 需要继续的情况
        if(maxIndex != now){
            swap(nums, now, maxIndex);
            heapify(nums, n, maxIndex);
        }
    }

	// 自底向上
    void buildTree(int[] nums){
        int lastNode = nums.length - 1;
        int lastParent = (lastNode - 1) / 2;
        // 自下而上,自右而左进行 heapify
        for(; lastNode >= 0; lastNode--){
            heapify(nums, nums.length, lastNode);
        }
    }

    void myHeapSort(int[] nums){
        buildTree(nums);
        int lastNode = nums.length - 1;
        for(int i = lastNode; i >= 0; i--){
            swap(nums, 0, i);
            heapify(nums, i, 0);
        }
    }
}
结合题目的堆排
  • 找到第K大元素的时候直接返回值。
class Solution {
    public int findKthLargest(int[] nums, int k) {
        // 堆排序,结合题目的改进
        return myHeapSort(nums, k);
    }

    void swap(int[] nums, int one, int two){
        int temp = nums[one];
        nums[one] = nums[two];
        nums[two] = temp;
    }

    void heapify(int[] nums, int n, int now){
        // 递归结束条件
        if(now >= n){
            return;
        }
        // 选出最大值对应下标:两次对比
        int maxIndex = now;
        int son1 = 2 * now + 1;
        int son2 = son1 + 1;
        // 判断是否超出边界。
        if(son1 < n && nums[maxIndex] < nums[son1]){
            maxIndex = son1;
        }
        if(son2 < n && nums[maxIndex] < nums[son2]){
            maxIndex = son2;
        }
        // 需要继续的情况
        if(maxIndex != now){
            swap(nums, now, maxIndex);
            heapify(nums, n, maxIndex);
        }
    }

    void buildTree(int[] nums){
        int lastNode = nums.length - 1;
        int lastParent = (lastNode - 1) / 2;
        // 自下而上,自右而左进行 heapify
        for(; lastNode >= 0; lastNode--){
            heapify(nums, nums.length, lastNode);
        }
    }

    // 排到第k个的时候直接结束。
    int myHeapSort(int[] nums, int k){
        buildTree(nums);
        int lastNode = nums.length - 1;
        for(int i = lastNode; i >= 0; i--, k--){
            if(k == 1){
                return nums[0];
            }
            swap(nums, 0, i);
            heapify(nums, i, 0);
        }
        return -1;
    }
}

二刷

  • 快排:右边开始、等于号,还是这两个注意点。。
while(i < j){
	// 从右边开始找,避免标志位本就应在最左边的情况导致出错。
	while(i < j && nums[j] >= mark) j--;
	// 没有 '=' 会造成死循环
	while(i < j && nums[i] <= mark) i++;
  • 堆排:三个主要函数。一个自顶向上,一个自底向上。
class Solution {
    public int findKthLargest(int[] nums, int k) {
        heapSort(nums);
        return nums[nums.length - k];
    }

    // heapSort:先建树,然后从后往前,逐个交换、维护堆。
    void heapSort(int[] nums) {
        buildTree(nums);
        for(int i = nums.length - 1; i >= 0; i--) {
            swap(nums, i, 0);
            heapify(nums, i, 0);
        }
    }

    // buildTree:自底向上进行建树
    void buildTree(int[] nums) {
        for(int son = nums.length - 1; son >= 0; son--) {
            heapify(nums, nums.length, son);
        }
    }

    // heapify:自顶向下进行堆维护,注意边界判断
    void heapify(int[] nums, int length, int nowIndex) {
        if(nowIndex >= length) {
            return;
        }
        int son1 = nowIndex * 2 + 1;
        int son2 = son1 + 1;
        int maxIndex = nowIndex;
        if(son1 < length && nums[son1] > nums[maxIndex]) {
            maxIndex = son1;
        }
        if(son2 < length && nums[son2] > nums[maxIndex]) {
            maxIndex = son2;
        }
        if(maxIndex != nowIndex) {
            swap(nums, maxIndex, nowIndex);
            heapify(nums, length, maxIndex);
        }
    }

    void swap(int[] nums, int left, int right) {
        int temp = nums[left];
        nums[left] = nums[right];
        nums[right] = temp;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值