【算法】快速选择算法

news/2024/5/19 23:07:32 标签: 快速选择算法, 排序算法, 快速排序

目录

  • 1.概述
  • 2.代码实现
    • 2.1.基于简单交换排序
    • 2.2.基于堆排序
    • 2.3.基于快速排序
  • 3.应用

更多数据结构与算法的相关知识可以查看数据结构与算法这一专栏。

1.概述

(1)快速选择算法 (Quick Select Algorithm) 是一种用于在无序数组中寻找第 k 小(或第 k 大)元素的高效算法,它是快速排序算法的一个变种。快速选择算法的基本思想是通过每次选取一个枢纽元素(通常是随机选择或者选择第一个或最后一个元素),将数组划分为两个部分,并将枢纽元素放置在其最终的排序位置上。然后,根据枢纽元素的位置,判断第 k 小(或第 k 大)元素在数组的左半部分还是右半部分,并且递归地在相应的部分中查找。

(2)与快速排序不同的是,快速选择算法只需要递归处理数组的一半。这样,在平均情况下,算法的时间复杂度为 O(n),其中 n 是数组的大小。然而,在最坏情况下,快速选择算法的时间复杂度可能达到 O(n2),但这种情况的概率很低。

2.代码实现

实现快速选择算法有许多方式,下面将逐一介绍。需要注意的是,在下面的实现方式中,只有基于快速排序快速选择算法的时间复杂度为 O(n)。此外,有关排序算法的具体知识可以参考【算法】内部排序这篇文章。

注意:下面的代码实现都是找出数组中的第 k 个最大元素。

2.1.基于简单交换排序

这里考虑利用简单交换排序的特点,即每完成一趟排序,就至少会有一个元素的位置确定,那么只需要对数组 nums 进行 k 躺降序排序,此时的 nums[k - 1] 必为数组 nums 中第 k 个最大的元素,将其返回即可。该算法的时间复杂度为 O(k * n),具体实现代码如下:

class Solution {
    public int findKthLargest(int[] nums, int k) {
    	//进行 k 趟排序
        for (int i = 0; i < k; i++) {
            for (int j = i + 1; j < nums.length; j++) {
                if (nums[i] < nums[j]) {
                	//交换 nums[i] 和 nums[j]
                    int tmp = nums[i];
                    nums[i] = nums[j];
                    nums[j] = tmp;
                }
            }
        }
        return nums[k - 1];
    }
}

2.2.基于堆排序

我们可以使用堆排序来找出数组中的第 k 个最大元素,即建立一个大根堆,做 k − 1 次删除操作后堆顶元素就是我们要找的答案。在 Java 中,我们可以直接使用优先级队列来完成这一操作。当然,如果想更加了解推排序,我们也可以手动实现。该算法的时间复杂度为 O(nlogk),具体代码如下:

//使用优先级队列
class Solution {
    public int findKthLargest(int[] nums, int k) {
        //创建一个大根堆
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);
        //将数组中的元素加入堆
        for (int num : nums) {
            maxHeap.offer(num);
        }
        //进行 k - 1 次删除操作
        for (int i = 0; i < k - 1; i++) {
            maxHeap.poll();
        }
        //堆顶元素就是第 k 个最大元素
        return maxHeap.peek();
    }
}
//手动实现堆排序
class Solution {
    public int findKthLargest(int[] nums, int k) {
        int length = nums.length;
        //循环建立初始堆,调用 sift 算法 ⌊n / 2⌋ 次
        for (int i = (length - 1) / 2; i >= 0; i--) {
            sift(nums, i, length - 1);
        }
        for (int i = length - 1; i >= length - k; i--) {
            //将 nums[i] 与根 nums[0] 交换
            int tmp = nums[0];
            nums[0] = nums[i];
            nums[i] = tmp;
            //对 nums[0...i - 1] 进行筛选,得到 i 个节点的堆
            sift(nums, 0, i - 1);
        }
        return nums[length - k];
    }

    //对 nums[low...high] 进行筛选,使得以 nums[low] 为根节点的左子树和右子树均为大根堆
    public void sift(int[] nums, int low, int high) {
        // nums[j] 是 nums[i] 的左孩子
        int i = low;
        int j = (i == 0) ? 1 : 2 * i;
        int tmp = nums[i];
        while (j <= high) {
            //如果右孩子更大,则将 j 指向右孩子
            if (j < high && nums[j] < nums[j + 1]) {
                j++;
            }
            //根节点小于最大孩子节点
            if (tmp < nums[j]) {
                nums[i] = nums[j];
                nums[j] = tmp;
                i = j;
                j = 2 * i;
            } else {
                //如果跟节点大于等于最大孩子关键字,筛选结束
                break;
            }
        }
    }
}

2.3.基于快速排序

具体实现思路如下:

  • 首先定义一个函数 findKthLargest,该函数接收一个数组 nums 和一个整数 k,返回该数组中第 k 大的元素。
  • 在 findKthLargest 函数中,调用 quickSelect 函数进行快速选择,在数组 nums 的下标范围 [left, right] 中查找第 index 大的元素。
  • 在 quickSelect 函数中,首先调用 randomPartition 函数进行随机划分,得到基准元素的下标 q:
    • 判断 q 是否等于 index,如果相等则说明已经找到了第 k 大的元素,直接返回 nums[q]。
    • 如果 q 不等于 index,根据 q 在 [left, right] 中与 index 的大小关系,递归调用 quickSelect 函数查找第 k 大的元素。如果 q < index,则第 k 大的元素在右区间,递归调用 quickSelect(nums, q + 1, right, index);否则第 k 大的元素在左区间,递归调用 quickSelect(nums, left, q - 1, index)。
  • randomPartition 函数采用随机选择基准元素的方法,将一个下标 i 转换成基准元素,使得 [left,i-1] 中的所有元素都小于等于基准元素,[i+1, right] 中的所有元素都大于等于基准元素,并返回基准元素的下标。
  • partition 函数是快速排序中划分数组的核心操作,它以 nums[left] 为基准,将数组 nums[left…right] 划分为两个子数组 nums[left…i-1] 和 nums[i+1…right],并返回最终元素 nums[left] 所在的下标 i。此处实现的是双指针的快速排序实现方式。
class Solution {
    Random random = new Random();

    public int findKthLargest(int[] nums, int k) {
        //第 K 个最大元素其实就是升序排序后的元素 nums[nums.length - k]
        return quickSelect(nums, 0, nums.length - 1, nums.length - k);
    }
    
    public int quickSelect(int[] nums, int left, int right, int index) {
        int q = randomPartition(nums, left, right);
        if (q == index) {
            //当前划分的下标 q 等于 index,则说明这次划分操作后已经找到了第 k 个最大元素,其下标为 q,直接返回 nums[q] 即可
            return nums[q];
        } else {
            /*
                当前划分的下标 q 不等于 index: 
                (1) 如果 q < index,那么则说明第 k 个最大元素在右区间,此时递归右区间
                (2) 如果 q > index,那么则说明第 k 个最大元素在左区间,此时递归左区间
            */
            if (q < index) {
                return quickSelect(nums, q + 1, right, index)
            } else {
                return quickSelect(nums, left, q - 1, index);
            }
        }
    }
    
    public int randomPartition(int[] nums, int left, int right) {
        // random.nextInt(int num): 随机返回一个 [0, num) 内的整数
        int i = random.nextInt(right - left + 1) + left;
        int tmp = nums[i];
        nums[i] = nums[left];
        nums[left] = tmp;
        return partition(nums, left, right);
    }
    
    //以 nums[left] 为基准,进行一趟划分并返回最终元素 nums[left] 所在的下标
    private int partition(int[] nums, int left, int right) {
        int i = left, j = right;
        //以 nums[left] 为基准
        int benchmark = nums[left];
        while (i < j) {
            //从右向左扫描,找到一个小于 benchmark 的元素 nums[j]
            while (i < j && benchmark <= nums[j]) {
                j--;
            }
            nums[i] = nums[j];
            //从左向右扫描,找到一个大于 benchmark 的元素 nums[i]
            while (i < j && nums[i] <= benchmark) {
                i++;
            }
            nums[j] = nums[i];
        }
        nums[i] = benchmark;
        return i;
    }
}

3.应用

(1)快速选择算法在处理数组中第 k 大/小元素的问题上具有广泛的应用场景。以下是一些应用场景的示例:

  • 数组中的第 k 大/小元素快速选择算法可以在线性时间复杂度内找到数组中的第 k 大/小元素。这在需要找到排名靠前/靠后的元素时非常有用,例如找到数组中的中位数或前 k 个最大的元素。
  • 排序问题:在快速排序算法中,使用快速选择算法找到基准元素的位置,然后通过递归地对基准元素左右两部分进行排序,最终实现对整个数组的排序。
  • 基于中位数的分割问题:在一些分割问题中,我们可以使用快速选择算法找到数组的中位数,并将数组分割成两个部分。这在一些分治算法中非常常见,例如快速排序、快速选择等。
  • 统计问题快速选择算法可用于解决一些统计问题,例如找到数组中出现次数最多的元素、找到更大/更小的元素等。

总的来说,快速选择算法适用于需要在数组中查找第 k 大/小元素的问题,并且相比完全排序整个数组,快速选择算法具有更高的效率。它在平均情况下的时间复杂度为O(n),在最坏情况下的时间复杂度为O(n2),但通过一些优化技巧,可以将最坏情况下的时间复杂度降低到O(n)。

(2)例如,在 462.最小操作次数使数组元素相等 II 这题中,我们就可以使用快速选择算法来快速找出数组中的中位数,并计算使所有数组元素相等需要的最小操作数。


http://www.niftyadmin.cn/n/5219882.html

相关文章

Python实现艺术设计?提取图片中颜色并绘制成可视化图表,从大师作品中提取配色方案

文章目录 导入模块并加载图片提取颜色并整合成表格绘制图表实战环节关于Python技术储备一、Python所有方向的学习路线二、Python基础学习视频三、精品Python学习书籍四、Python工具包项目源码合集①Python工具包②Python实战案例③Python小游戏源码五、面试资料六、Python兼职渠…

Excel如何比较两列数据的不同

当遇到exel有两个列表的数据&#xff0c;需要比较得到他们的不同的部分&#xff0c;并且得到一个不同的值的列表。示例如下&#xff1a; 目的是&#xff1a;通过比较&#xff0c;知道Column2的哪些值不在在Column1里。 WPS直接提供了这一个功能&#xff0c;如下图&#xff1a;…

大数据Doris(三十):删除数据(Delete)

文章目录 删除数据(Delete) 一、​​​​​​​DELETE FROM Statement(条件删除)

python读取PDF文件中的指定页码的范围并存储到指定的文件名

读取PDF文件中的指定页码的范围并存储到指定的文件名 # -*- coding: utf-8 -*- """ Created on Mon Nov 27 21:36:12 2023author: cnliu pip install pypdf2 #安装pypdf2 --3.o版 """ from PyPDF2 import PdfWriter, PdfReader import os#pat…

详解ClickHouse的ReplaceMergeTree

区别于MergeTree表引擎&#xff0c;ReplacingMergeTree删除重复数据时是通过相同的分区值&#xff08;ORDER BY的值&#xff09; 数据去重发生在后台合并数据时&#xff0c;后台合并数据是随机的&#xff0c;所以有时会有一些没处理的数据&#xff0c;可以通过OPTIMIZI来手动合…

每日一题--寻找重复数

蝶恋花-王国维 阅尽天涯离别苦&#xff0c; 不道归来&#xff0c;零落花如许。 花底相看无一语&#xff0c;绿窗春与天俱莫。 待把相思灯下诉&#xff0c; 一缕新欢&#xff0c;旧恨千千缕。 最是人间留不住&#xff0c;朱颜辞镜花辞树。 目录 题目描述&#xff1a; 思路分析…

洗地机应该怎么选?希亦、必胜、米博、添可、小米洗地机实测推荐

作为一个常年测评智能家居的博主&#xff0c;关于洗地机的测评使用这些年也积累了不少的体验感受。以至于常被周边的朋友问到&#xff0c;洗地机到底是不是真的好用&#xff1f;洗地机有什么优点吗&#xff1f;选购的时候应该怎么选呢&#xff1f;洗地机什么牌子比较好呢&#…

FilterChain攻击解析及利用

文章目录 BASE64解码和编码原理浅析EncodingDecoding Filterchain构造&#xff08;原理阐述&#xff09;回顾死亡代码特性一&#xff08;双重去杂&#xff09;特性二&#xff08;粘合性&#xff09; 任意字符构造工具一工具二 实战例题[NSSRound#7 Team]brokenFilterChain&…