215. 数组中的第K个最大元素(快速排序、堆排序)

根据这道题总结一下快速排序和堆排序,再根据这两种方法写这道题。

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

示例 1:

输入: [3,2,1,5,6,4], k = 2
输出: 5

示例 2:

输入: [3,2,3,1,2,4,5,5,6], k = 4
输出: 4

提示:

  • 1 <= k <= nums.length <= 105
  • -104 <= nums[i] <= 104

我们首先给出快速排序的代码,快速排序的思路是先选取一个基准值,然后把小于基准值的放到基准值左边,把大于基准值的放到基准值右边,这样就会变成三部分(基准值左边部分、基准值、基准值右边部分),对基准值左右再递归进行这个步骤。代码分三部分:快速排序辅助分区部分、排序部分和主函数,分区部分就是把比基准值小的放左边,比基准值大的放右边,然后把基准值放中间,排序部分就是递归排序。

#include <iostream>
#include <vector>
#include <utility> // for std::swap// 快速排序的辅助函数,进行分区
int partition(std::vector<int> &nums, int low, int high) {// 选择最左侧的元素作为基准值(pivot)int pivot = nums[low];int i = low + 1; // i指针用来记录比基准值小的区域的最后一个元素的位置int j = high; // j指针用来记录比基准值大的区域的第一个元素的位置// 循环进行分区操作while(true) {// 从左向右找,找到大于等于基准值的元素while (nums[i] < pivot) {i++;}// 从右向左找,找到小于等于基准值的元素while (nums[j] > pivot) {j--;}if (i < j) {std::swap(nums[i], nums[j]);} else {// 完成分区,左边全是小于等于基准值,右边全是大于等于基准值break;}}// 交换基准值到分区的中间std::swap(nums[low], nums[j]);// 返回基准值的最终位置return i;
}// 快速排序的递归函数
void quickSort(std::vector<int> &nums, int low, int high) {if (low < high) {// 分区操作int pivotIndex = partition(nums, low, high);// 对基准值左边的子序列进行快速排序quickSort(nums, low, pivotIndex - 1);// 对基准值右边的子序列进行快速排序quickSort(nums, pivotIndex + 1, high);}
}int main() {std::vector<int> nums = {10, 7, 8, 9, 1, 5};int n = nums.size();quickSort(nums, 0, n - 1);for (int num : nums) {std::cout << num << " ";}return 0;
}

运行结果(每一步分区的过程)为:

6 7 8 9 1 5 3 3 6 1 10 
6 1 3 3 1 5 6 9 8 7 10 
5 1 3 3 1 6 6 9 8 7 10 
1 1 3 3 5 6 6 9 8 7 10 
1 1 3 3 5 6 6 9 8 7 10 
1 1 3 3 5 6 6 9 8 7 10 
1 1 3 3 5 6 6 7 8 9 10 
1 1 3 3 5 6 6 7 8 9 10 
1 1 3 3 5 6 6 7 8 9 10 

快速排序的时间复杂度是O(nlogn)

基于快速排序可以写出做这道题的快速选择方法的代码,与快速排序一样,需要先分区,之后确定了基准值最终所在的位置,然后不需要进行排序操作,只需要知道第k大的元素是在基准值左边还是右边,然后在那个分区找就可以了,也是递归来查找,这个就是在快排的过程中直接找到了,所以不需要进行完整的快排,因此复杂度变低:

#include <iostream>
#include <vector>
#include <utility> // for std::swap// 快速排序的辅助函数,进行分区
int partition(std::vector<int> &nums, int low, int high) {// 选择最左侧的元素作为基准值(pivot)int pivot = nums[low];int i = low + 1; // i指针用来记录比基准值小的区域的最后一个元素的位置int j = high; // j指针用来记录比基准值大的区域的第一个元素的位置// 循环进行分区操作while(true) {// 从左向右找,找到大于等于基准值的元素while (nums[i] < pivot) {i++;}// 从右向左找,找到小于等于基准值的元素while (nums[j] > pivot) {j--;}if (i < j) {std::swap(nums[i], nums[j]);} else {// 完成分区,左边全是小于等于基准值,右边全是大于等于基准值break;}}// 交换基准值到分区的中间std::swap(nums[low], nums[j]);// 返回基准值的最终位置return j;
}// 快速排序的递归函数
int quickSelect(std::vector<int> &nums, int low, int high, int kIndex) {if (low == high) {// 当子数组只有一个元素时,返回该元素return nums[low];}int pivotIndex = partition(nums, low, high);if (kIndex <= pivotIndex) {// 第k大的元素索引在左侧子数组中return quickSelect(nums, low, pivotIndex, kIndex);} else {// 第k大的元素索引在右侧子数组中return quickSelect(nums, pivotIndex + 1, high, kIndex);}
}int main() {std::vector<int> nums = {10, 5, 3, 2, 1, 6, 8, 7};int n = nums.size();int k = 3;// 第k大的元素的索引是k-1int kIndex = k - 1;int ans = quickSelect(nums, 0, n - 1, n - 1 - kIndex);std::cout << "The ans is " << ans << std::endl;return 0;
}

注意,当求第k大的元素时,传入的是索引k-1,当求第k小的元素(第n-k+1大)时,传入索引n-k(即n-1-kIndex)。这个方法时间复杂度是O(n)

下面来总结一下堆排序和这道题,我们给出堆排序的代码:

#include <iostream>
#include <vector>
#include <algorithm> // for std::swap// 自上向下调整堆,保证堆的性质
void heapify(std::vector<int> &nums, int n, int i) {int largest = i; // 初始时假设当前节点为最大值int left = 2 * i + 1;  // 左子节点int right = 2 * i + 2; // 右子节点// 如果左子节点存在且大于当前节点,更新最大值节点if (left < n && nums[left] > nums[largest]) {largest = left;}// 如果右子节点存在且大于当前节点,更新最大值节点if (right < n && nums[right] > nums[largest]) {largest = right;}// 如果最大值节点发生了变化,交换当前节点和最大值节点的值,并继续调整if (largest != i) {std::swap(nums[i], nums[largest]);heapify(nums, n, largest);}
}// 堆排序
void heapSort(std::vector<int> &nums) {int n = nums.size();// 从最后一个非叶子节点开始建堆,即从 (n/2 - 1) 节点开始for (int i = n / 2 - 1; i >= 0; i--) {heapify(nums, n, i);}// 从最后一个元素开始,交换元素并进行调整堆操作for (int i = n - 1; i > 0; i--) {std::swap(nums[0], nums[i]); // 将当前堆的最大值放到数组末尾heapify(nums, i, 0); // 调整堆,新的堆大小为 i}
}int main() {std::vector<int> nums = {16, 10, 8, 7, 2, 3, 4, 1, 9, 14};heapSort(nums);std::cout << "Sorted array: ";for (int num : nums) {std::cout << num << " ";}return 0;
}

堆排序有三个重要部分:维护堆的性质,建堆,排序。以大根堆为例,这是一颗完全二叉树,父节点的值大于子节点的值,下标为i的节点的父节点下标是(i - 1) / 2(整数除法),下标为i的节点的左孩子下标是i * 2 + 1,右孩子下标是i * 2 + 2,因此,假如有n个元素,那么堆的最后一个非叶子节点的下标是n / 2 - 1

  • 维护堆的性质,即为保证父节点值大于子节点值,从上而下调整,比如当前i节点不满足这个性质,那么交换i节点和它的左右孩子中最大的那个,然后再判断子节点那里是否满足堆的性质(之所以需要这样是因为如果进行了交换,那么子节点那里可能会发生变化,比如3 6 5 2 4这个情况,首先36进行了交换,变成了6 3 5 2 4,那么3 2 4那个部分(之前是6 2 4)就需要再次进行交换)。
  • 建堆,即从最后一个非叶子节点开始,自下而上维护堆的性质,直到根节点。
  • 堆排序,将当前堆的最大值放到数组末尾,然后把它排除出去,再从根向下进行堆的维护,新的堆的大小为n-1,重复这个过程,直到只剩一个元素。
    运行结果为:
create heap
16 14 8 9 10 3 4 1 7 2
sort heap for 9 nums 14 10 8 9 2 3 4 1 7 16
sort heap for 8 nums 10 9 8 7 2 3 4 1 14 16
sort heap for 7 nums 9 7 8 1 2 3 4 10 14 16
sort heap for 6 nums 8 7 4 1 2 3 9 10 14 16
sort heap for 5 nums 7 3 4 1 2 8 9 10 14 16
sort heap for 4 nums 4 3 2 1 7 8 9 10 14 16
sort heap for 3 nums 3 1 2 4 7 8 9 10 14 16
sort heap for 2 nums 2 1 3 4 7 8 9 10 14 16
sort heap for 1 nums 1 2 3 4 7 8 9 10 14 16
sorted heap
1 2 3 4 7 8 9 10 14 16

可以看到16, 10, 8, 7, 2, 3, 4, 1, 9, 14经过建堆过程(自最后一个非叶子节点向上维护堆),变成了16 14 8 9 10 3 4 1 7 2,然后需要进行堆排序,将1614交换,然后不管16了,这个时候它是最后一个元素,再从根向下维护堆,得到14 10 8 9 2 3 4 1 7 16,然后再将147交换,进行相同的步骤,最后排序成功。

有了堆排序的基础,我们利用堆排序解决数组中的第K个最大元素的问题,事实上,在堆排序取最大值的过程中,已经体现出来了,在第一次取16,这就是第1大的元素,第二次取14就是第2大的元素,那么我们想得到第k大元素的值,只需要设置堆排序的停止条件为i > n - k,然后这时候的nums[0](即根节点值)为第k大的元素。如果我们想得到第’k’小的元素,那么就取第n-k+1大的元素。

详细代码如下:

#include <iostream>
#include <vector>
#include <algorithm> // for std::swap// 自上向下调整堆,保证堆的性质
void heapify(std::vector<int> &nums, int n, int i) {int largest = i; // 初始时假设当前节点为最大值int left = 2 * i + 1;  // 左子节点int right = 2 * i + 2; // 右子节点// 如果左子节点存在且大于当前节点,更新最大值节点if (left < n && nums[left] > nums[largest]) {largest = left;}// 如果右子节点存在且大于当前节点,更新最大值节点if (right < n && nums[right] > nums[largest]) {largest = right;}// 如果最大值节点发生了变化,交换当前节点和最大值节点的值,并继续调整if (largest != i) {std::swap(nums[i], nums[largest]);heapify(nums, n, largest);}
}// 堆排序取数
int heapSelect(std::vector<int> &nums, int k) {int n = nums.size();// 从最后一个非叶子节点开始建堆,即从 (n/2 - 1) 节点开始for (int i = n / 2 - 1; i >= 0; i--) {heapify(nums, n, i);}std::cout << "create heap" << std::endl;for (int num : nums) {std::cout << num << " ";}std::cout << "\n";// 从最后一个元素开始,交换元素并进行调整堆操作for (int i = n - 1; i > n - k; i--) {std::swap(nums[0], nums[i]); // 将当前堆的最大值放到数组末尾heapify(nums, i, 0); // 调整堆,新的堆大小为 istd::cout << "sort heap for " << i << " nums" << " ";for (int num : nums) {std::cout << num << " ";}std::cout << "\n";}std::cout << "sorted heap" << std::endl;  for (int num : nums) {std::cout << num << " ";}return nums[0];
}int main() {std::vector<int> nums = {16, 10, 8, 7, 2, 3, 4, 1, 9, 14};int n = nums.size();int k = 4;int ans = heapSelect(nums, n - k + 1);std::cout << "ans=" << ans << std::endl;return 0;
}

运行结果:

create heap
16 14 8 9 10 3 4 1 7 2
sort heap for 9 nums 14 10 8 9 2 3 4 1 7 16
sort heap for 8 nums 10 9 8 7 2 3 4 1 14 16
sort heap for 7 nums 9 7 8 1 2 3 4 10 14 16
sort heap for 6 nums 8 7 4 1 2 3 9 10 14 16
sort heap for 5 nums 7 3 4 1 2 8 9 10 14 16
sort heap for 4 nums 4 3 2 1 7 8 9 10 14 16
sorted heap
4 3 2 1 7 8 9 10 14 16 ans=4

时间复杂度是O(nlogn),建堆的复杂度是O(n),删除堆顶元素的复杂度是O(klogn),所以总共的时间复杂度是O(n+klogn)=O(nlogn)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/15698.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

qmt量化交易策略小白学习笔记第6期【qmt如何获取股票历史涨跌停价格】

qmt如何获取股票历史涨跌停价格 qmt更加详细的教程方法&#xff0c;会持续慢慢梳理。 也可找寻博主的历史文章&#xff0c;搜索关键词查看解决方案 &#xff01; 感谢关注&#xff0c;需免费开通量化回测与咨询实盘权限&#xff0c;可以和博主联系&#xff01; 获取股票历史…

[数据结构] -- 单链表

&#x1f308; 个人主页&#xff1a;白子寰 &#x1f525; 分类专栏&#xff1a;C打怪之路&#xff0c;python从入门到精通&#xff0c;数据结构&#xff0c;C语言&#xff0c;C语言题集&#x1f448; 希望得到您的订阅和支持~ &#x1f4a1; 坚持创作博文(平均质量分82)&#…

c++编程14——STL(3)list

欢迎来到博主的专栏&#xff1a;c编程 博主ID&#xff1a;代码小豪 文章目录 list成员类型构造、析构、与赋值iterator元素访问修改元素list的操作 list list的数据结构是一个链表&#xff0c;准确的说应该是一个双向链表。这是一个双向链表的节点结构&#xff1a; list的使用…

Vue学习笔记3——事件处理

事件处理 1、事件处理器&#xff08;1&#xff09;内联事件处理器&#xff08;2&#xff09;方法事件处理器 2、事件参数3、事件修饰符 1、事件处理器 我们可以使用v-on 指令(简写为)来监听DOM事件&#xff0c;并在事件触发时执行对应的JavaScript。 用法: v-on:click"me…

JVM学习-执行引擎

执行引擎 执行引擎是Java虚拟机核心组成部分之一虚拟机是一个相对于物理机的概念&#xff0c;这两种机器都有代码执行能力&#xff0c;其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的&#xff0c;而虚拟机的执行引擎是由软件自行实现的&#xf…

【算法】递归、搜索与回溯——简介

简介&#xff1a;递归、搜索与回溯&#xff0c;本节博客主要是简单记录一下关于“递归、搜索与回溯”的相关简单概念&#xff0c;为后续算法做铺垫。 目录 1.递归1.1递归概念2.2递归意义2.3学习递归2.4写递归代码步骤 2.搜索3.回溯与剪枝 递归、搜索、回溯的关系&#xff1a; …

ICML2024 定义新隐私保护升级:DP-BITFIT新型微调技术让AI模型学习更安全

DeepVisionary 每日深度学习前沿科技推送&顶会论文分享&#xff0c;与你一起了解前沿深度学习信息&#xff01; 引言&#xff1a;差分隐私在大模型微调中的重要性和挑战 在当今的深度学习领域&#xff0c;大型预训练模型的微调已成为提高各种任务性能的关键技术。然而&am…

推特热帖:大语言模型自荐能够替代的20种人类工作!快来看你是否需要转行!

最近推特上有一个例子引起了广泛的讨论&#xff0c;事情的起因是这样的&#xff1a;网友让 GPT-4o 预测一下自己未来将会替代人类哪些工作&#xff1f; 这听起来很有趣&#xff01;GPT-4o会给出什么样的预测呢&#xff1f; 3.5研究测试&#xff1a;hujiaoai.cn 4研究测试&…

02-Linux【基础篇】

一、Linux的目录结构 1.基本介绍 Linux的文件系统采用层级式的树状目录结构&#xff0c;在此结构中的最上层是根目录"/"&#xff0c;然后在此目录下再创建其他的目录 深刻理解Linux树状文件目录是非常重要的 记住一句经典的话&#xff1a;在Linux世界里&#xff…

如何在 DigitalOcean Droplet 云主机上创建 Ubuntu 服务器

在本文中&#xff0c;你将通过 DigitalOcean 的管理面板创建一个 Ubuntu 服务器&#xff0c;并将其配置为使用你的 SSH 密钥。设置好服务器后&#xff0c;你可以在其上部署应用程序和网站。 本教程是DigitalOcean云课程简介的一部分&#xff0c;它指导用户完成将应用程序安全地…

win10右键没有默认打开方式的选项的处理方法

问题描述 搞了几个PDF书籍学习一下&#xff0c;不过我不想用默认的WPS打开&#xff0c;因为WPS太恶心人了&#xff0c;占用资源又高。我下载了个Sumatra PDF&#xff0c;这时候我像更改pdf文件默认的打开程序&#xff0c;发现右击没有这个选项。 问题解决 右击文件–属性–…

汽车以太网发展现状及挑战

一、汽车以太网技术联盟 目前推动汽车以太网技术应用与发展的组织包括&#xff1a;OPEN Alliance&#xff08;One-Pair Ether-Net Alliance SIG&#xff09;联盟&#xff0c;主要致力于汽车以太网推广与使用&#xff0c;该联盟通过推进 BroadR- Reach 单对非屏蔽双绞线以太网传…

设计新境界:大数据赋能UI的创新美学

设计新境界&#xff1a;大数据赋能UI的创新美学 引言 随着大数据技术的蓬勃发展&#xff0c;它已成为推动UI设计创新的重要力量。大数据不仅为界面设计提供了丰富的数据资源&#xff0c;还赋予了设计师以全新的视角和工具来探索美学的新境界。本文将探讨大数据如何赋能UI设计…

面试八股之JVM篇3.5——垃圾回收——G1垃圾回收器

&#x1f308;hello&#xff0c;你好鸭&#xff0c;我是Ethan&#xff0c;一名不断学习的码农&#xff0c;很高兴你能来阅读。 ✔️目前博客主要更新Java系列、项目案例、计算机必学四件套等。 &#x1f3c3;人生之义&#xff0c;在于追求&#xff0c;不在成败&#xff0c;勤通…

1688. 比赛中的配对次数

题目&#xff1a; 给你一个整数 n &#xff0c;表示比赛中的队伍数。比赛遵循一种独特的赛制&#xff1a; 如果当前队伍数是 偶数 &#xff0c;那么每支队伍都会与另一支队伍配对。总共进行 n / 2 场比赛&#xff0c;且产生 n / 2 支队伍进入下一轮。 如果当前队伍数为 奇数 …

python梯度下降法求解三元线性回归系数,并绘制结果

import numpy as np import matplotlib.pyplot as plt # 生成随机数据 np.random.seed(0) X1 2 * np.random.rand(100, 1) X2 3 * np.random.rand(100, 1) X3 4 * np.random.rand(100, 1) y 4 3 * X1 5 * X2 2 * X3 np.random.randn(100, 1) # 合并特征 X_b np.hsta…

Vue中组件之间的通信有哪些方法

在Vue中&#xff0c;组件之间的通信有多种方法&#xff0c;以下是一些常见的方法&#xff1a; Props和$emit&#xff1a; 父组件通过props向子组件传递数据。子组件通过$emit触发事件&#xff0c;将数据传递给父组件。 provide和inject&#xff1a; 在Vue 2.2.0版本中引入的选…

云计算-特殊机制(Specialsed Mechanisms)

自动扩展监听器 (Automated Scaling Listener) 自动扩展监听器是一种特定类型的服务代理。它运行在云提供商的网络中&#xff0c;监控云消费者和云服务之间的网络流量。通过分析消费者和服务之间的消息量和类型&#xff0c;它可以测量云服务的负载。 自动扩展监听器对变化的负载…

常见 JVM 面试题补充

原文地址 : 26 福利&#xff1a;常见 JVM 面试题补充 (lianglianglee.com) CMS 是老年代垃圾回收器&#xff1f; 初步印象是&#xff0c;但实际上不是。根据 CMS 的各个收集过程&#xff0c;它其实是一个涉及年轻代和老年代的综合性垃圾回收器。在很多文章和书籍的划分中&…

SpringCloud Alibaba的相关组件的简介及其使用

Spring Cloud Alibaba是阿里巴巴为开发者提供的一套微服务解决方案&#xff0c;它基于Spring Cloud项目&#xff0c;提供了一系列功能强大的组件&#xff0c;包括服务注册与发现、配置中心、熔断与限流、消息队列等。 本文将对Spring Cloud Alibaba的相关组件进行简介&#xff…