定个小目标之刷LeetCode热题(8)

这道题属于一道中等题,我记得我面试中有面试官曾让我写过这道题,当时觉得直接用快速排序排好序就可以了,然后面试官提示了一句“是否需要把数组中所有数字都排好序呢?”,我这才想到直接排序肯定不是最优解,于是就想到了堆排序的思想,当时是没有写出来,今天正好把它搞懂,直接看题解

有两种解题方法,第一种是利用快速排序思想,直接上代码,代码注释有详细讲解每一行代码

class Solution {

    private int quickSelect(List<Integer> nums, int k) {
        // 随机选择基准数
        Random rand = new Random();
        int pivot = nums.get(rand.nextInt(nums.size()));
        // 将大于、小于、等于 pivot 的元素划分至 big, small, equal 中
        List<Integer> big = new ArrayList<>();
        List<Integer> equal = new ArrayList<>();
        List<Integer> small = new ArrayList<>();
        for (int num : nums) {
            if (num > pivot) {
                big.add(num);
            } else if (num < pivot) {
                small.add(num);
            } else {
                equal.add(num);
            }
        }
        // 示例:
        // nums:1 2 5 3 6 9 7 2
        // 下标: 0 1 2 3 4 5 6 7
        // k = 3
        // pivot = 3
        // big: 5 6 9 7
        // small: 1 2 2
        // equal: 3
        // 我们可以按从大到小排列为 big > equal > small
        // k表示第几大,如果k <= big.size(),说明这个第k大的数一定在big中,继续对big进行划分
        if (k <= big.size()) {
            return quickSelect(big, k);
        }
        // 同理,如果k > big.size() + equal.size(),说明这个第k大的数只能在small中,继续对small进行划分
        if (big.size() + equal.size() < k) {
            // k的原本含义是在整个数组中第k大的数,big和equal已经排除在外,此时是在small中,数组长度变小了,自然k也要变小
            return quickSelect(small, k - (big.size() + equal.size()));
        }
        // 第k大的数在equal中,直接返回 pivot
        return pivot;
    }

    public int findKthLargest(int[] nums, int k) {
        List<Integer> list = new ArrayList<>();
        for (int num : nums) {
            list.add(num);
        }
        return quickSelect(list, k);
    }

}

第二种是堆排序

是一种特殊的 完全二叉树,其存储结构类似于完全二叉树,可以用数组实现,如下图所示

数组中的数字并未按照升序降序排列,但其实这棵树是已经有序的状态了,至于为什么?这就与大根堆、小根堆有关了

大根堆: 父结点的值 大于或等于 其子结点的值

小根堆: 父结点的值 小于或等于 其子结点的值

它能以 O(logN)的时间复杂度完成插入、删除和查找操作,通过调整数组中元素的顺序,维护堆的结构,下面我们以 大根堆 为例,对堆的一个重要操作: 下调 ( heapfiy )  进行说明

这个公式是比较重要的,若结点数组下标为 i ,则:

父结点数组下标:( i - 1 ) / 2;

左孩子数组下标:2 * i + 1 ;

右孩子数组下标:2 * i + 2 ;

下调 heapfiy

还是上面图示里那个数组,调整成为一个大根堆,从 最后一个元素开始 向前遍历,比较自己与左右孩子结点的大小,如果小于孩子结点就交换(即:下调 ),下调之后继续与新的左右孩子结点进行比较,如果小于孩子结点就交换,直到不能下调为止。向前继续移动,直到所有结点均遍历一遍,当所有父结点均大于其孩子结点时,那么这颗二叉树就是一棵大根堆

第一步,从后往前遍历,8,9,6节点都是叶子节点,不需要进行下调,接着遍历到7这个节点,我们会发现 7 < 8,8节点是7节点的孩子,那么需要进行交换,交换之后因为7节点是叶子节点了,所以无需继续下调,如下图所示

64n4jjoXbl4AAAAASUVORK5CYII=

第二步,接着遍历到了4这个节点,我们会发现4节点比其左右孩子节点都小,我们需要与其中更大的那个孩子节点进行交换,即4节点与9节点交换,交换之后因为4节点是叶子节点了,所以无需继续下调,如下图所示

H+142RrIAFgQQAAAABJRU5ErkJggg==

第三步,接着遍历到1这个节点,我们又发现1节点比其左右孩子节点都小,同上,我们需要与其中更大的那个孩子节点进行交换,即1节点与9节点交换,交换之后因为1节点是非叶子节点,所以需要判断是否继续下调,如下图所示

lBaM2S1E0TapYaW6kcokC2DYIgmLb3qZWMwrFGUFlZjg0gCIJInf8HXlgQbPgTTl8AAAAASUVORK5CYII=

第四步,上面由于1节点的下调需要继续判断是否符合下调条件,我们会发现1节点比其左右孩子都小,所以需要进行下调,即6节点与1节点交换,交换之后因为6节点是叶子节点,所以无需继续下调,如下图所示

p3TQAAAABJRU5ErkJggg==

至此,整个遍历结束,将一个无序数组调整成为了大根堆,具体代码实现如下

class Solution {
    // 根据已知数组调整为大根堆
    public void heapify(int[] arr, int index, int heapSize) {
        // 利用公式找到当前节点的左孩子索引
        int left = index * 2 + 1;
        // 如果left >= heapSize说明是叶子节点,则不需要进行处理
        while (left < heapSize) {
            // 记录当前节点的左右孩子value最大的下标
            int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
            // 如果孩子的value小于等于父节点则不用进行交换
            if (arr[largest] <= arr[index]) {
                break;
            } else {
                // 交换
                swap(arr, largest, index);
                // 当前节点索引往下调
                index = largest;
                // 更新当前节点的左孩子索引
                left = index * 2 + 1;
            }
        }
    }
}

然后回到本题,采用建大根堆的方式进行寻找第k大的元素,代码如下,代码注释有详细讲解每一行代码

class Solution {
    public int findKthLargest(int[] nums, int k) {
        if (nums == null || (nums != null && nums.length == 0)) {
            return -1;
        }
        // 从后往前遍历调整为大根堆
        for (int i = nums.length - 1; i >= 0; i--) {
            heapify(nums, i, nums.length);
        }
        int heapSize = nums.length;
        // 寻找第k大的元素,比如数组长度为6,k为3,则需要删除大根堆根节点两次
        for (int i = nums.length - 1; i >= nums.length - k + 1; i--) {
            // 把根节点与最后一个节点交换
            swap(nums, 0, i);
            // 堆大小减一,相当于把最后一个节点排除在堆排序之外了
            heapSize--;
            // 再次调整为大根堆
            heapify(nums, 0, heapSize);
        }
        // 经过k - 1次根节点的删除和k次大根堆的调整,当前根节点就是第k大的元素
        return nums[0];
    }

    // 交换
    public void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    // 根据已知数组调整为大根堆
    public void heapify(int[] arr, int index, int heapSize) {
        // 利用公式找到当前节点的左孩子索引
        int left = index * 2 + 1;
        // 如果left >= heapSize说明是叶子节点,则不需要进行处理
        while (left < heapSize) {
            // 记录当前节点的左右孩子value最大的下标
            int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
            // 如果孩子的value小于等于父节点则不用进行交换
            if (arr[largest] <= arr[index]) {
                break;
            } else {
                // 交换
                swap(arr, largest, index);
                // 当前节点索引往下调
                index = largest;
                // 更新当前节点的左孩子索引
                left = index * 2 + 1;
            }
        }
    }
}

 题目链接:题单 - 力扣(LeetCode)全球极客挚爱的技术成长平台

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值