C++:分治算法之选择问题的选择第k小元素问题

news2025/1/10 10:51:07

目录

3.2.6 选择问题

分析过程: 

解法一:  

 算法代码:

【单组数据】

【多组数据】 

运行结果:

 解法二

 代码:

运行结果:

解法三:


3.2.6 选择问题

¢ 对于给定的 n 个元素的数组 a[0 n 1] ,要求从中找出第 k 小的元素。
¢ 输入

输入有多组测试例。

对每一个测试例有2行,第一行是整数nk1kn1000),第二行是n个整数。

¢ 输出

k小的元素

输入样

输出样

5 2

3 9 4 1 6

7 3

4 59 7 23 61 55 46

3

23

分析过程: 

¢ 一种简单的解决方法就是对全部数据进行排序,于是得到问题的解。
l 但即使用较好的排序方法,算法的复杂性也为 n log n
¢ 快速排序算法是分治策略的典型应用,不过不是对问题进行等份分解(二分法),而是通过分界数据(支点)将问题分解成独立的子问题。

¢ 首先选 第一个数 作为分界数据,将比它 的数据存储在它的 左边 ,比它 的数据存储在它的 右边 ,它存储在左、右两个子集之间。这样左、右子集就是原问题分解后的独立子问题。
l 再用同样的方法,继续解决这些子问题,直到每个子集只有一个数据,就完成了全部数据的排序工作。

¢ 利用 快速排序算法的思想,来解决选择问题。
l 一趟快速排序后,分解出 左子集 中元素个数为 nleft ,则选择问题可能是以下几种情况之一:
nleft k 1,则分界数据就是选择问题的答案。
nleft k 1 ,则选择问题的答案继续在左子集中找
问题规模变小了。
nleft k 1 ,则选择问题的答案继续在右子集中找
问题 变为选择 k- nleft -1 小的数,问题的 规模变 小了。

输入样

输出样

5 2

3 9 4 1 6

3

1

3

9

4

6

此算法在最坏情况时间复杂度为 O(n 2 ) ,此时 nleft 总是为 0 ,左子集为空,即第 k 小元素总是位于 right 子集中。
平均时间复杂度为 O( n )

解法一:  

 

 算法代码:

【单组数据】

#include<iostream>
#include<algorithm>
using namespace std;
const int NUM = 1001;

int a[NUM];

int select(int left,int right,int k){
	//找到了第k小色元素
	if (left >= right)
		return a[left];
	int i = left;//从左到右的指针
	int j = right;//从右到左的指针
	int pivot = a[left];//把最左边的元素作为分界数据

	while (true)
	{
		//寻找左边大于pivot的数
		do {
			i++;
		} while (a[i]<pivot);
		//寻找右边小于pivot的数
		do {
			j--;
		} while (a[j] > pivot);

		if (i >= j)
			break;//没有发现交换对象
		swap(a[i], a[j]);
	}
	if (j - left + 1 == k)
		return pivot;
	a[left] = a[j];
	a[j] = pivot;
	if (j - left + 1 < k)
		return select(j + 1, right, k - (j - left + 1));
	else
		return select(left, j - 1, k);
}

int main() {
	int n, k;
	cin >> n >> k;
	for (int i = 0; i < n; i++) {
		cin >> a[i];
	}
	cout<<select(0, n - 1, k);
	return 0;
}

【多组数据】 

#include<iostream>
#include<algorithm>
using namespace std;
const int NUM = 1001;

int a[NUM];

int select(int left, int right, int k) {
    //找到了第k小色元素
    if (left >= right)
        return a[left];
    int i = left;//从左到右的指针
    int j = right;//从右到左的指针
    int pivot = a[left];//把最左边的元素作为分界数据

    while (true)
    {
        //寻找左边大于pivot的数
        do {
            i=i+1;
        } while (a[i] < pivot);
        //寻找右边小于pivot的数
        do {
            j=j-1;
        } while (a[j] > pivot);

        if (i >= j)
            break;//没有发现交换对象
        swap(a[i], a[j]);
    }
    if (j - left + 1 == k)
        return pivot;

    //存储pivot
    a[left] = a[j];
    a[j] = pivot;

    if (j - left + 1 < k)
        return select(j + 1, right, k - (j - left + 1));
    else
        return select(left, j - 1, k);
}

int main() {
    int n, k;
    while (cin >> n >> k && n != 0 && k != 0) {
        for (int i = 0; i < n; i++) {
            cin >> a[i];
        }
        cout << select(0, n - 1, k) << endl;
    }
    return 0;
}

            这段代码实现了选择问题中的查找第 k 小元素的算法,该算法通过快速选取法(QuickSelect)实现,基本思路为利用快速排序的部分思路进行枢轴(pivot)的划分缩小问题规模,最终得到第 k 小的元素。

具体来说, select 函数的流程如下:

  1. 确定枢轴,默认选取序列中的第一个元素 a[left] 作为枢轴。
  2. 初始化 i = left 和 j = right 两个指针,从两端向中间扫描,直至 i >= j 停止扫描。
  3. 在扫描过程中,不断移动指针 i 和 j,使 a[i] < pivot 且 a[j] > pivot,然后交换 a[i] 和 a[j] 的值,直到 i >= j 停止。
  4. 将枢轴和 a[j] 位置的元素互换,将 j - left + 1 的长度作为枢轴左边元素个数计算。
  5. 如果 k 等于枢轴左边元素个数,则返回枢轴的值。
  6. 如果 k 小于枢轴左边元素个数,则递归地在左半边序列里选择第 k 小元素。
  7. 如果 k 大于枢轴左边元素个数,则递归地在右半边序列里选择第 k - (j - left + 1) 小元素。

例如,对于序列 {3, 5, 1, 6, 4, 2},查找第 3 小的元素,在 select(0, 5, 3) 中,首先选择 a[0] = 3 作为枢轴 pivot,然后从两端开始移动指针,扫描过程如下:

[3] 5 1 6 4 2   // i = 1, j = 5
 3 [2] 1 6 4 5   // i = 2, j = 4
 3 [2 1] 6 4 5   // i = 3, j = 4
 3 [2 1] 4 6 5   // i = 3, j = 3
 3 [2 1] 4 5 6   // i = 3, j = 2
 3 2 [1 4] 5 6   // i = 4, j = 1
 3 2 [1 4] 5 6   // i = 4, j = 1, 结束扫描

从上表可以看出,最终将枢轴和 a[j] 位置的元素互换,得到序列 {1, 2, 3, 6, 4, 5},此时第 3 小的元素 为 3,返回结果。


	a[left] = a[j];
	a[j] = pivot;

        在这段代码中, a[left] = a[j] 的作用是将枢轴的值(即最左边的元素)与当前分界点 j 所在位置的元素交换。而 a[j] = pivot 则是将原来的枢轴值(即最左边的元素)赋给当前分界点 j 所在位置,保证了左右两个子序列仍然是以枢轴值为分界的。

        这样做的原因是,选择算法在进行一轮快速排序后会得到分界点 j 的位置,也就是说,在分界点左边的所有元素都小于枢轴值,而在右边的所有元素都大于它。但是此时枢轴已经不在最左边,而是位于分界点处,因此需要将枢轴的位置与分界点所在位置进行交换,以便在递归处理子区间时能够正确地处理左右子序列。

        总之,a[left] = a[j]a[j] = pivot 这两行代码可以帮助我们维护所需的元素分界点,确保选择算法能够正确地返回第 k 小的元素。

这段代码是一个经典的算法——快速选择,用于在一个无序数组中找到第 k 小的元素。它的改进之处可能包括以下几个方面:

  1. 随机选择枢轴 - 快速选择算法实际上是一种快速排序的变体,它通过将数组分为两个部分来寻找第 k 小的元素。在选择枢轴时,可以考虑随机选择一个位置而不是一直使用最左边的元素。这样能够减少最坏情况的出现概率,提高算法的效率。

  2. 三数中值分割 - 在选择枢轴时,可以考虑多取几个数并找到这些数的中位数作为枢轴值。这样能够更好地均衡左右子序列的大小,减少递归次数。

  3. 循环选择 - 如果数组很大,递归选择可能会耗费大量的时间和空间。因此,可以考虑使用循环选择算法来代替递归算法,从而提高算法的效率。

  4. 利用线性时间选择算法 - 在某些情况下,我们需要在无序数组中查找第 k 小的元素,并且这个元素的规模非常大,超出了基于比较排序的算法的时间复杂度下界。这时可以使用线性时间选择算法来解决这个问题,它的时间复杂度为 O(n)。

需要指出的是,以上提到的改进并不是一定都要实现,具体要看具体情况,找到适合自己问题的最优解即可。

运行结果:

 


 解法二

为了解决输入数据不随机排列以及比较元素大小时出现的问题,可以对快速选择算法进行以下改进:

  1. 在每次递归之前,随机选取一个元素作为枢轴点,确保元素的选择是随机的。
  2. 比较元素大小时使用 <= 和 >=,以处理数组中有重复元素的情况。
  3. 使用插入排序等排序算法来处理小数据集,避免 QuickSelect 算法退化成 O(n^2)。

下面是改进后的代码实现:

  •  代码:

#include <iostream>
#include <algorithm>

using namespace std;
const int NUM = 1001;
int a[NUM];
// 插入排序,用于处理小数据集
void insertionSort(int a[], int start, int end) {
    for (int i = start + 1; i <= end; i++) {
        int key = a[i];
        int j = i - 1;
        while (j >= start && a[j] > key) {
            a[j + 1] = a[j];
            j--;
        }
        a[j + 1] = key;
    }
}

int quickSelect(int a[], int left, int right, int k) {
    if (right - left + 1 <= 5) {
        insertionSort(a, left, right);
        return a[left + k - 1];
    }
    // 随机选择枢轴点
    int pivotIndex = rand() % (right - left + 1) + left;
    int pivot = a[pivotIndex];
    swap(a[pivotIndex], a[right]);
    int i = left - 1;
    for (int j = left; j < right; j++) {
        if (a[j] <= pivot) {
            i++;
            swap(a[i], a[j]);
        }
    }
    swap(a[i + 1], a[right]);
    int index = i + 1 - left + 1;
    if (index == k) {
        return a[i + 1];
    }
    else if (index > k) {
        return quickSelect(a, left, i, k);
    }
    else {
        return quickSelect(a, i + 2, right, k - index);
    }
}

int main() {
    int n, k;
    while (cin >> n >> k && n != 0 && k != 0) {
        for (int i = 0; i < n; i++) {
            cin >> a[i];
        }
        cout << quickSelect(a, 0, n - 1, k) << endl;
    }
    return 0;
}

这段代码实现了快速选择算法,用于查找数组中第 k 小的元素。具体过程如下:

  1. 读入数据,包括数组大小 n 和要查找的第 k 小的元素。
  2. 读入 n 个整数,表示要查找的数组。
  3. 调用 quickSelect 函数查找数组中第 k 小的元素,并打印结果。
  4. 重复步骤 1~3,直到读入的 n 和 k 均为 0 为止。

        在 quickSelect 函数中,首先判断当前处理的数据集是否已经很小(比如小于等于 5),如果是,则使用插入排序来处理。这是因为插入排序对于小数据集的效率更高。

        如果数据集很大,则随机选取一个枢轴点,将比枢轴点小的元素放在数组左边,比枢轴点大的元素放在数组右边。此时,枢轴点就是整个数组中的第 index 小元素(index 是枢轴点的位置)。通过比较 index 和 k 的大小关系,可以判断第 k 小元素在数组的哪一侧,并递归处理该侧的数据集,直到找到第 k 小的元素。

        值得注意的是,为了避免算法退化成 O(n^2),每次递归前都需要随机选择枢轴点,保证算法的随机性。另外,在比较元素大小时,使用了 <= 和 >=,以处理数组中有重复元素的情况。

  • 运行结果:


解法三:

C++:分治算法之选择问题的选择第k小元素问题

用 C++ 设计分治算法解决选择问题中的选择第 k 小元素问题。

       首先,对于一个无序数组来说,如果想要找到其中第 k 小的元素,可以使用快速选择算法(QuickSelect)进行处理。快速选择算法的基本思路是选取数组中的一个主元,将数组划分为两个部分,并根据主元在数组中的位置关系,决定继续处理数组的哪一部分。通过每次划分将待查找的第 k 小元素所在的区间不断缩小,最终得到结果。

       然而,在实际应用中,数据量很大时,快速选择算法的效率可能会受到影响。这时可以使用分治算法来解决问题。分治算法是一种将问题分成若干个子问题处理的思想,通过将原问题分解为多个规模较小的子问题并逐个解决,最终将子问题的解组合起来得到原问题的解。

        具体来说,对于选择第 k 小元素的问题,可以利用分治算法的思想将数组按照主元划分为两个部分,并根据主元在数组中的位置关系,决定继续处理数组的哪一部分。每次处理只需要考虑一个子问题,这样就不需要对整个数组进行排序,从而提高了算法的效率。可以使用递归的方式实现分治算法解决选择问题中的选择第 k 小元素问题。

          下面是 C++ 代码示例:

int quickSelect(vector<int>& nums, int left, int right, int k) {
    if (left == right) {// 只有一个元素
        return nums[left];
    }

    // 选取一个主元
    int pivotIndex = rand() % (right - left + 1) + left;
    int pivot = nums[pivotIndex];

    // 将数组按照主元划分为两部分
    swap(nums[pivotIndex], nums[right]);
    int i = left - 1;// i 表示左数组的右边界位置
    for (int j = left; j <= right; j++) {
        if (nums[j] < pivot) {
            i++;
            swap(nums[i], nums[j]);
        }
    }
    swap(nums[i+1], nums[right]);
    i++;

    // 根据主元在数组中的位置关系决定继续处理左/右子问题
    if (k == i - left + 1) {
        return nums[i];
    } else if (k < i - left + 1) {
        return quickSelect(nums, left, i-1, k);
    } else {
        return quickSelect(nums, i+1, right, k - (i - left + 1));
    }
}

int kthSmallest(vector<int>& nums, int k) {
    return quickSelect(nums, 0, nums.size()-1, k);
}

 

以上代码中的 quickSelect 函数为快速选择算法的实现,kthSmallest 函数则是使用分治算法解决选择问题中的选择第 k 小元素问题的实现。函数的参数含义如下:

  • nums:待查找第 k 小元素的数组;
  • left:当前处理区间的左边界;
  • right:当前处理区间的右边界;
  • k:目标元素的排名。

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

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

相关文章

DAY 53 Haproxy负载均衡集群

常见的Web集群调度器 目前常见的Web集群调度器分为软件和硬件&#xff1a; 软件通常使用开源的LVS、Haproxy、 Nginx LVS性能最好&#xff0c;但是搭建相对复杂&#xff1b;Nginx 的upstream模块支持群集功能&#xff0c;但是对群集节点健康检查功能不强&#xff0c;高并发性能…

第一章 Linux是什么

Linux是一套操作系统&#xff0c;如同下图所示&#xff0c;Linux就是核心与系统调用接口那两层。至于应用程序不算Linux。 1.1 Linux当前应用的角色 由于Linux kernel实在是非常的小巧精致&#xff0c;可以在很多强调省电以及较低硬件资源的环境下面执行&#xff1b; 此外&…

【Elasticsearch】NLP简单应用

文章目录 NLP简介ES中的自然语言处理(NLP)NLP演示将opennlp插件放在ESplugins路径中下载NER模型配置opennlp重启ES、验证 NLP简介 NLP代表自然语言处理&#xff0c;是计算机科学和人工智能领域的一个分支。它涉及使用计算机来处理、分析和生成自然语言&#xff0c;例如英语、中…

企业对网络安全的重视度开始降低

近日&#xff0c;英国科学技术部发布了《2023年企业网络安全合规调查报告》&#xff08; Cyber Security Breaches Survey &#xff09;&#xff0c;对英国所有企业和社会性组织目前的网络威胁态势和合规建设进行研究&#xff0c;同时也就如何提升新一代网络应用的合规性给出专…

02-管理员登录与维护 尚筹网

一、管理员登陆 需要做的&#xff1a; 对存入数据库的密码进行MD5加密在登录界面登录失败时的处理抽取后台页面的公共部分检查登录状态&#xff0c;防止未登录时访问受保护资源的情况 具体操作如下&#xff1a; 1&#xff09;、MD5加密 ​ 使用到的CrowdConstant类中的一些…

人的全面发展评价指标体系—基于相关-主成分分析构建

本文先从经济、社会、生活质量和人口素质四个方面海选了众多人的全面发展评价指标&#xff0c;然后根据可观测性原则剔除无法获得的指标进行了初步筛选&#xff0c;再利用相关性分析删除相关系数大的指标&#xff0c;以及通过主成分分析删除因子负载小的指标&#xff0c;完成了…

CCD视觉检测设备如何选择光源

CCD视觉检测设备的机器视觉系统对光源的要求很高&#xff0c;光源是决定图像质量的一个重要因素。那么&#xff0c;我们就来看看CCD图像加网设备和机器视觉系统光源的选择点——CCD图像加网设备。 CCD视觉检测设备机器视觉系统光源选择要点&#xff1a; 1. 对比度&#xff1a;…

最新VUE面试题

前言 本文以前端面试官的角度出发&#xff0c;对 Vue 框架中一些重要的特性、框架的原理以问题的形式进行整理汇总&#xff0c;意在帮助作者及读者自测下 Vue 掌握的程度。 本文章节结构以从易到难进行组织&#xff0c;建议读者按章节顺序进行阅读&#xff0c;当然大佬级别的…

P1915 [NOI2010] 成长快乐

此题为世纪难题 题目提供者 洛谷 难度 NOI/NOI/CTSC 输入输出样例 输入 #1 5 1 6 0 0 1 5 2 2 0 0 输出 #1 1 5 5 2 2 1 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~此题非常难&#xff0c;小白就不用想着独自完成了 题解&#xff1a; #…

如何在 Windows 11 启用 Hyper-V

准备在本机玩一下k8s&#xff0c;需要先启用 Hyper-V&#xff0c;谁知道这一打开&#xff0c;没有 Hyper-V选项&#xff1a; 1、查看功能截图&#xff1a; 2、以下文件保存记事本&#xff0c;然后重命名为*.bat pushd "%~dp0" dir /b %SystemRoot%\servicing\Packa…

常用的MySQL 优化方法

数据库优化一方面是找出系统的瓶颈&#xff0c;提高MySQL数据库的整体性能&#xff0c;而另一方面需要合理的结构设计和参数调整&#xff0c;以提高用户的相应速度&#xff0c;同时还要尽可能的节约系统资源&#xff0c;以便让系统提供更大的负荷。   本文我们来谈谈项目中常用…

maven中的 type ,scope的作用

dependency为什么会有type为pom&#xff0c;默认的值是什么&#xff1f; dependency中type默认为jar即引入一个特定的jar包。那么为什么还会有type为pom呢?当我们需要引入很多jar包的时候会导致pom.xml过大&#xff0c;我们可以想到的一种解决方…

Linux指令-2

文章目录 一、 m a n man man [选项] 命令1、功能&#xff1a;2、常用选项&#xff1a;3、运用实例 二、 c p cp cp [选项] 源文件/目录 目标文件/目录1、功能&#xff1a;2、常用选项&#xff1a;3、运用实例 三、 m v mv mv [选项] 源文件/目录 目标文件/目录1、功能…

PySide6/PyQT多线程之 编程入门指南:基础概念和最佳实践

前言 本篇文章介绍 PySide6/PyQT多线程编程的基本概念&#xff0c;用到的知识点&#xff0c;以及PySide6/PyQT多线程的基本使用。 看多线程介绍&#xff0c;就看 知识点&#x1f4d6;&#x1f4d6; &#xff1b; 看多线程代码&#xff0c;就看 实现 。 知识点&#x1f4d6;&…

《手腕光电容积图智能手表对房颤检测的录制长度和其他心律失常的影响》阅读笔记

目录 一、论文摘要 二、论文十问 三、论文亮点与不足之处 四、与其他研究的比较 五、实际应用与影响 六、个人思考与启示 参考文献 一、论文摘要 本研究旨在评估手腕光电容积图&#xff08;PPG&#xff09;的定量分析是否能检测到房颤&#xff08;AF&#xff09;。使用心…

项目管理-计算专题(挣值分析)

挣值分析法 是对项目进行跟踪与预测的方法&#xff1b;项目有良好的任务细分以及合理的日程安排&#xff1b;不牵涉到复杂的数学计算&#xff1b;在软件项目管理中&#xff0c;一般以一周为单位定期进行。 项目案例 有一个砌墙项目&#xff0c;需要完成一堵长度为100米的围墙…

第二十四章 纹理贴图

通常情况下&#xff0c;3D网格模型只能展示游戏对象的几何形状&#xff0c;而表面的细节则纹理贴图提供。纹理贴图通过UV坐标“贴附”在模型的表面。当然&#xff0c;这个过程不需要我们在Unity中完成&#xff0c;而是在建模软件中完成的。通常情况下&#xff0c;我们通过3ds m…

基于matlab使用合成雷达和无线通信信号训练的语义分割神经网络执行频谱检测

一、前言 此示例展示了如何使用使用合成雷达和无线通信信号训练的语义分割神经网络执行频谱检测。经过训练的神经网络可以识别出现在相同接收频谱中的雷达和无线通信信号。此外&#xff0c;网络可以识别接收信号的占用带宽。 二、介绍 由于对更高速度和更大覆盖范围的需求不断增…

多元时间序列 | BP神经网络多变量时间序列预测(Matlab完整程序)

多元时间序列 | BP神经网络多变量时间序列预测(Matlab完整程序) 目录 多元时间序列 | BP神经网络多变量时间序列预测(Matlab完整程序)预测结果评价指标基本介绍程序设计参考资料预测结果 评价指标 训练集数据的R2为:0.99805 测试集数据的R2为:0.98351 训练集数据的MAE为:…

小黑子—Java从入门到入土过程:第八章

Java零基础入门8.0 Java系列第八章1. 双列集合 Map1.1 Map 集合中常见的API1.2 Map 集合的遍历方式1.2 - I 第一种遍历方式&#xff1a;键找值KeySet 方法1.2 - II 第二种遍历方式&#xff1a;键值对 entrySet 方法1.2 - III 第三种遍历方式&#xff1a;lambda表达式 1.3 HashM…