在无序数组中求第K小的数

news2024/11/19 15:32:51

在无序数组中求第 K K K 小的数

改写快排的方法

【思路】在该无序数组中 随机 选择一个数 v v v,拿 v v v 去做整个数组的荷兰国旗问题,即将数组分成三个区域 “小于 v v v | 等于 v v v | 大于 v v v,每个区域都不要求有序,不过等于 v v v 的区域天然就已经有序了,实现这个过程的时间复杂度 O ( N ) O(N) O(N),空间复杂度 O ( 1 ) O(1) O(1)

然后查看 等于 v v v 的区域是否命中了第 K K K 小的数。假设 等于 v v v 的区域下标是从 50 50 50 ~ 60 60 60

  • 如果 K = 55 K = 55 K=55,那么就命中了,第 K K K 小的数就是 v v v
  • 如果 K = 30 K=30 K=30,就是没有命中,而待求的一定就在 小于 v v v 区域,于是在 小于 v v v 区域重复上面的操作,随机选择一个数 v ′ v' v做荷兰国旗问题,分成 “小于 v ′ v' v | 等于 v ′ v' v | 大于 v ′ v' v 三个区域,然后看 v ′ v' v 是否命中了第 K K K 小的数,递归下去;

这就是最优解,时间复杂度为 O ( N ) O(N) O(N)

快排的时间复杂度为 O ( N ∗ l o g N ) O(N * logN) O(NlogN),是 小于 v v v 和 大于 v v v 的区域都要递归进行排序;而本方法中如果 等于 v v v 没有命中,则要么小于 v v v 区域进行递归,要么 大于 v v v 区域进行递归,只走一侧,所以比快排要快。

事实上,如果选择的 v v v 是很差的情况,时间复杂度是 O ( N 2 ) O(N^2) O(N2);如果选择的 v v v 是很好的情况,时间复杂度是 O ( N ) O(N) O(N)。但是因为是随机选择的,所以是个概率事件,最后的期望收敛到 O ( N ) O(N) O(N)

-方法一:大根堆,时间复杂度 O ( N ∗ l o g K ) O(N*logK) O(NlogK)

public class FindMinKth {

	public static class MaxHeapComparator implements Comparator<Integer> {

		@Override
		public int compare(Integer o1, Integer o2) {
			return o2 - o1;
		}

	}

	// 方法一:利用大根堆,时间复杂度O(N*logK)
	public static int minKth1(int[] arr, int k) {
		PriorityQueue<Integer> maxHeap = new PriorityQueue<>(new MaxHeapComparator());
		for (int i = 0; i < k; i++) {
			maxHeap.add(arr[i]);
		}
		for (int i = k; i < arr.length; i++) {
			if (arr[i] < maxHeap.peek()) {
				maxHeap.poll();
				maxHeap.add(arr[i]);
			}
		}
		return maxHeap.peek();
	}
}
  • 方法二:(最优解)改写快排,时间复杂度 O ( N ) O(N) O(N),空间复杂度 O ( 1 ) O(1) O(1)

(1)递归实现:

public class FindMinKth {
	// 方法二:改写快排,时间复杂度O(N)
	public static int minKth2(int[] array, int k) { // k是大于等于1的
		int[] arr = copyArray(array);
		//位于第k-1位置的数就是第k小的数
		return process2(arr, 0, arr.length - 1, k - 1);
	}

	public static int[] copyArray(int[] arr) {
		int[] ans = new int[arr.length];
		for (int i = 0; i != ans.length; i++) {
			ans[i] = arr[i];
		}
		return ans;
	}

	// arr 第k小的数
	// process2(arr, 0, N-1, k-1)
	// arr[L..R]  范围上,如果排序的话(不是真的去排序),找位于index的数
	// index 在 [L..R] 范围
	public static int process2(int[] arr, int L, int R, int index) {
		if (L == R) { //index在L...R范围上,当只有一个数的时候,就是 L == R == index
			return arr[L];
		}
		// 不止一个数  L +  [0, R -L]
		//随机选择一个数
		int pivot = arr[L + (int) (Math.random() * (R - L + 1))];
		//荷兰国旗分区:分成 小于pivot | 等于pivot | 大于pivot 三个区域
		int[] range = partition(arr, L, R, pivot); //返回值range为 等于pivot区域 的左右边界
		//查看是否命中 (因为如果没有命中,只会进入一个情况,所以其中涉及到的递归可以用迭代实现)
		if (index >= range[0] && index <= range[1]) { //index位于等于pivot区域的左右边界中,即命中
			return arr[index];
		} else if (index < range[0]) { //没有命中,index小于 “等于pivot区域” 的左边界,则小于pivot区域递归
			return process2(arr, L, range[0] - 1, index);
		} else { //没有命中,index大于 “等于pivot区域” 的右边界,则大于pivot区域递归
			return process2(arr, range[1] + 1, R, index);
		}
	}
	
	public static int[] partition(int[] arr, int L, int R, int pivot) {
		int less = L - 1;
		int more = R + 1;
		int cur = L;
		while (cur < more) {
			if (arr[cur] < pivot) {
				swap(arr, ++less, cur++);
			} else if (arr[cur] > pivot) {
				swap(arr, cur, --more);
			} else {
				cur++;
			}
		}
		return new int[] { less + 1, more - 1 };
	}

	public static void swap(int[] arr, int i1, int i2) {
		int tmp = arr[i1];
		arr[i1] = arr[i2];
		arr[i2] = tmp;
	}
 }

(2)迭代实现:

public class FindMinKth {
	public static int minKth(int[] arr, int index) {
		int L = 0;
		int R = arr.length - 1;
		int pivot = 0;
		int[] range = null;
		while (L < R) {
			pivot = arr[L + (int) (Math.random() * (R - L + 1))];
			range = partition(arr, L, R, pivot);
			if (index < range[0]) {
				R = range[0] - 1;
			} else if (index > range[1]) {
				L = range[1] + 1;
			} else {
				return pivot;
			}
		}
		return arr[L];
	}
	
	public static int[] partition(int[] arr, int L, int R, int pivot) {
		int less = L - 1;
		int more = R + 1;
		int cur = L;
		while (cur < more) {
			if (arr[cur] < pivot) {
				swap(arr, ++less, cur++);
			} else if (arr[cur] > pivot) {
				swap(arr, cur, --more);
			} else {
				cur++;
			}
		}
		return new int[] { less + 1, more - 1 };
	}
	
	public static void swap(int[] arr, int i, int j) {
		int tmp = arr[i];
		arr[i] = arr[j];
		arr[j] = tmp;
	}
}

bfprt算法

五个人联合发明的算法,用于解决 在无序数组中找到第 K K K 小的数 的问题。

虽然 改写快排 的方法已经是最优解了,但是利用概率来解释时间复杂度是 O ( N ) O(N) O(N),而bfprt算法不用概率来解释也能可以做到 O ( N ) O(N) O(N)

bfprt 算法和改进快排的方法主要区别就在于划分值 v v v 的选择。
请添加图片描述
那么 bfprt 算法是如何讲究地选择划分值 v v v的呢?

  1. 原数组按照五个数分一组,若最后一组不满五个数,则剩几个就几个数为一组。【逻辑概念,时间复杂度 O ( 1 ) O(1) O(1)
数组:[........... ........... ........... ...........]
下标:0 1 2 3 4  5 6 7 8 9 ....
分组:(0~4)  (5~9) ...
  1. 每组数内部排序。【因为每组数只有5个数,所以每组数排好序的时间复杂度 O ( 1 ) O(1) O(1),整体有 N / 5 N/5 N/5 组,所以整体时间复杂度 O ( N ) O(N) O(N)
  2. 取出每组数排好序的中位数组成一组,若最后一组是偶个数,则取出上中位数(如最后一组4个数,排好序后为 a a a b b b c c c d d d,则取出 b b b 即可),该数组长度为 N / 5 N/5 N/5。【时间复杂度 O ( N ) O(N) O(N)
  3. 假设每个小组的中位数组成的数组为 M M M,求 M M M 中的中位数 v v v 即为划分值。【注:假设函数 bfprt(arr, k) 表示求解 arr 数组中的第 k 小的数,那么求 M M M 的中位数时可以借助bfprt 函数,bfprt(M, N/10) 就是求得 M M M 的第 N / 10 N/10 N/10 小的数,也就是 M M M 的中位数】 【T(N/5)规模

举例:
请添加图片描述
好处:精心挑选划分值 v v v 后,那么 < v <v <v 区域和 > v >v >v 区域都有一定的规模。

如何估计 < v < v <v 区域最多有多少个数?

直接估计很难,但是如果能估计出 ≥ v \ge v v 的区域至少有 a a a 个数,那么 < v \lt v <v的部分最多有 N − a N - a Na 个数。

原数组长度为 N N N,每 5 个数为一组,内部排序后选择每组的中位数组成数组 M M M,则 M M M 长度为 N / 5 N/5 N/5

再选择 M M M 的中位数 v v v,那么数组 M M M ≥ v \ge v v 的至少有 N / 10 N/10 N/10 个数;

而每个数对应回原小组中比该数大的又会多2个,所以整个数组至少有 3 N / 10 3N/10 3N/10 个数是 ≥ v \ge v v 的;

所以 < v \lt v <v 的最多有 7 N / 10 7N/10 7N/10 个;同理, > v \gt v >v 的最多有 7 N / 10 7N/10 7N/10 个。

这就是 bfprt 算法的精髓:可以保证每次递归至少淘汰掉 3 N / 10 3N/10 3N/10 的数据

选出 v v v 后的步骤:

  1. 将原数组分成 < v \lt v <v | = v =v =v | > v \gt v >v 三个区域;【时间复杂度 O ( N ) O(N) O(N)
  2. 如果命中,直接返回;如果没命中,选择 < v <v <v > v >v >v 进行递归。【规模为 T ( 7 N / 10 ) T(7N/10) T(7N/10)

所以总体的时间复杂度为 T ( N ) = T ( 7 N / 10 ) + T ( N / 5 ) + O ( N ) T(N) = T(7N/10) + T(N/5) + O(N) T(N)=T(7N/10)+T(N/5)+O(N),算法导论中证明了该等式最后的时间复杂度为 O ( N ) O(N) O(N)。不能用 master 公式计算时间复杂度,因为子过程的规模不一样。

对于该算法的常见问题:

【 为什么要 5 个数一组?】

事实上3个数一组或7个数一组都能使得时间复杂度收敛到 O ( N ) O(N) O(N),最终之所以选择 5 个数一组,是因为该算法是 5 个人联合发明的,所以人家选择了 5 个数一组,人家喜欢 5 这个数。

实现:

  • 方法三:bfprt算法,时间复杂度 O ( N ) O(N) O(N)
public class FindMinKth {

	// 利用bfprt算法,时间复杂度O(N)
	public static int minKth3(int[] array, int k) {
		int[] arr = copyArray(array);
		return bfprt(arr, 0, arr.length - 1, k - 1);
	}

	// arr[L..R]  如果排序的话,位于index位置的数,是什么,返回
	public static int bfprt(int[] arr, int L, int R, int index) {
		if (L == R) {
			return arr[L];
		}
		// L~R范围上 每五个数一组
		// 每一个小组内部排好序
		// 小组的中位数组成新数组
		// 这个新数组的中位数返回
		int pivot = medianOfMedians(arr, L, R);
		//进行荷兰国旗分区
		int[] range = partition(arr, L, R, pivot);
		//查看是否命中
		if (index >= range[0] && index <= range[1]) {
			return arr[index];
		} else if (index < range[0]) {
			return bfprt(arr, L, range[0] - 1, index);
		} else {
			return bfprt(arr, range[1] + 1, R, index);
		}
	}

	// arr[L...R]  五个数一组
	// 每个小组内部排序
	// 每个小组中位数领出来,组成marr
	// marr中的中位数,返回
	public static int medianOfMedians(int[] arr, int L, int R) {
		int size = R - L + 1;
		int offset = size % 5 == 0 ? 0 : 1;
		//存放每组中位数的数组
		int[] mArr = new int[size / 5 + offset];
		//选出每组中的中位数,放到mArr数组中
		for (int team = 0; team < mArr.length; team++) {
			int teamFirst = L + team * 5;
			// L ... L + 4
			// L +5 ... L +9
			// L +10....L+14
			mArr[team] = getMedian(arr, teamFirst, Math.min(R, teamFirst + 4));
		}
		
		// 子过程调用bfprt方法找到mArr数组的中位数
		// 在mArr的0damarr.len - 1,  mArr.length / 2 )
		return bfprt(mArr, 0, mArr.length - 1, mArr.length / 2);
	}

	public static int getMedian(int[] arr, int L, int R) {
		// 因为每个小组只有5个数,所以选择常数时间复杂度较好的排序算法即可,此处也可以用其他排序算法
		// 没必要用时间复杂度特别优秀的排序算法
		insertionSort(arr, L, R);
		return arr[(L + R) / 2];
	}

	public static void insertionSort(int[] arr, int L, int R) {
		for (int i = L + 1; i <= R; i++) {
			for (int j = i - 1; j >= L && arr[j] > arr[j + 1]; j--) {
				swap(arr, j, j + 1);
			}
		}
	}
}

验证测试:

public class FindMinKth {

	// for test
	public static int[] generateRandomArray(int maxSize, int maxValue) {
		int[] arr = new int[(int) (Math.random() * maxSize) + 1];
		for (int i = 0; i < arr.length; i++) {
			arr[i] = (int) (Math.random() * (maxValue + 1));
		}
		return arr;
	}

	public static void main(String[] args) {
		int testTime = 1000000;
		int maxSize = 100;
		int maxValue = 100;
		System.out.println("test begin");
		for (int i = 0; i < testTime; i++) {
			int[] arr = generateRandomArray(maxSize, maxValue);
			int k = (int) (Math.random() * arr.length) + 1;
			int ans1 = minKth1(arr, k);
			int ans2 = minKth2(arr, k);
			int ans3 = minKth3(arr, k);
			if (ans1 != ans2 || ans2 != ans3) {
				System.out.println("Oops!");
			}
		}
		System.out.println("test finish");
	}
}

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

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

相关文章

学习记录-mybatis+vue+elementUi实现分页查询(后端部分)

这一部分的实现确实让我学到不少东西。不管是后端还是前端部分的实现。 首先需要明确的是&#xff0c;实现分页查询&#xff0c;我们需要从前端获取到几个参数&#xff1f;第一个是当前在第几页&#xff0c;第二个是每一页有多少个值。分别叫做&#xff1a;currentPage和pageSi…

Redis集群之AKF架构原理

当我们搭建集群之前&#xff0c;先要想明白需要解决哪些问题&#xff0c;搞清楚这个之前先回想一下单节点、单实例、单机有哪些问题&#xff1f; 单点故障&#xff1a;只有一台Redis的话&#xff0c;如果出现故障&#xff0c;那么整个服务都不可用缓存容量&#xff1a;单台Red…

【Django项目开发】用户注册模型类、序列化器类、视图类设计(三)

文章目录一、模型类设计1、Django认证系统提供了用户模型类User&#xff0c;为什么还要定义User模型类?2、AbstractUser3、自定义用户模型类的字段有4、User模型类编写好了就可以了吗?二、序列化器类设计1、注意2、单字段进行校验3、用户认证的时候为什么不用create,而用crea…

C++构造函数和析构函数

&#xff08;一&#xff09;构造函数 要点 定义&#xff1a;构造函数 &#xff0c;是一种特殊的方法。主要用来在创建对象时初始化对象&#xff0c; 即为对象成员变量赋初始值&#xff0c;总与new运算符一起使用在创建对象的语句中。特别的一个类可以有多个构造函数 &#xff0…

网络协议知识串讲-第38讲-用双十一的故事串起碎片的网络协议(中)

上一节我们讲到,手机App经过了一个复杂的过程,终于拿到了电商网站的SLB的IP地址,是不是该下单了? 别忙,俗话说的好,买东西要货比三家。大部分客户在购物之前要看很多商品图片,比来比去,最后好不容易才下决心,点了下单按钮。下单按钮一按,就要开始建立连接。建立连接…

Spring Cache(边路缓存)

一、Spring Cache介绍 Spring Cache 是Spring - context-xxx.jar中提供的功能&#xff0c;可以结合EHCache&#xff0c;Redis等缓存工具使用。给用户提供非常方便的缓存处理&#xff0c;缓存基本判断等操作,可以直接使用注解实现。 ​ 在包含了Spring - context-xxx.jar的Spri…

07---vue前端实现增删改查

前端VUE通过请求后端实现增删改查&#xff0c;文末会有前端完整代码 1、实现查询功能 一、实现三个条件一起查询 后台需要实现这三个条件的模糊查询 UserController.java //分页查询GetMapping("/page")public IPage<User> findPage(RequestParam Integer p…

【Jenkins】学习笔记

学习笔记一、Jenkins1.1、Jenkins的作用二、下载安装2.1、安装环境2.2、安装GitLab2.3、安装Jenkins三、Jenkins Git Maven 部署配置3.1、安装maven插件3.2、新建项目3.3、自动发布到测试服务器四、publish over ssh 配置4.1、超时机制4.2、shell的日志输出4.3、运行前清理五…

网络地址转换NAT

目录 IP 地址空间即将面临耗尽的危险 NAT 缓解 IP 地址空间耗尽的问题 NAT 的基本方法 VPN 的要点 IP 地址空间即将面临耗尽的危险 互联网采用了无分类编址方式、动态分配IP地址等措施来减缓IP地址空间耗尽的速度 但由于互联网用户数目的激增&#xff0c;特别是大量小型办公…

Linux搭建DHCP服务

DHCP(Dynamic Host Confifuration Protocol,动态主机配置协议)它可以为客户自动分配IP地址、以及缺省网关、DNS服务器的IP地址等TCP/IP参数。 简单说,就是在DHCP服务器上有一个存放着IP地址、网关、DNS等参数。当客户端请求使用时,服务器则负责将相应的参数分配给客户端,…

win10环境下基于face_recognition搭建自己的人脸识别软件

在win10环境下安装face_recognition&#xff0c;了解face_recognition中api的使用&#xff0c;如人脸截取、人脸矫正、人脸特征提取、人脸关键点提取、人脸mask获取、人脸特征比对等功能。最后构建自己的人脸数据库&#xff0c;使用knn实现人脸识别软件。 1、安装face_recogni…

【圣诞特辑】码一个漂漂亮亮的圣诞树(Single Dog版)

目录 前言 一、C语言版圣诞树 1.代码实现 2.效果图 二、python版圣诞树 1.代码实现 2.效果图​ 三、html5版圣诞树 1.代码实现 2.效果图 总结 前言 圣诞节即将来临&#xff0c;圣诞树也是必不可少的装饰之一。圣诞树是一棵绿叶繁茂的树&#xff0c;上面挂满了彩色的灯…

Nginx学习笔记2【尚硅谷】

host文件修改时&#xff0c;可以更改用户组权限或者复制到某个有权限的位置修改完再复制替换之前的文件。 在server{}中&#xff0c;listenserver_name两个加一起是唯一的。 代理服务器就是一个网关。 配置Nginx反向代理&#xff1a; 注意&#xff1a;在写proxy_pass时&#xf…

java+selenium环境搭建

目录 1.写在前面的话: 2.下载谷歌驱动 3.添加Selenium依赖(我这里添加的是4.0.0版本的) 4.在操作过程中可能出现的问题&解决办法 目录 1.写在前面的话: 2.下载谷歌驱动 3.添加Selenium依赖(我这里添加的是4.0.0版本的) 1.写在前面的话: (1)java版本最低要求为8,Chro…

SpringBoot整合Activemq

目录 一、Pom.xml 二、Spring配置文件 三、队列 四、主题 一、Pom.xml <dependencies><!-- activemq核心依赖包 --><dependency><groupId>org.apache.activemq</groupId><artifactId>activemq-all</artifactId><version>…

终生学习——读书有什么坏处

一般爱读书者往往受到赞扬&#xff0c;但仍然需要谨记一些读书的原则 目录 一、读书的整体观点 二、为什么需要知道读书的坏处 三、何时会出现读书的坏处 四、读书有什么坏处 1、100%全部相信书中的观点&#xff0c;进而实践了错误观点 2、不实践 五、如何杜绝读书的害处…

程序员为了少加班想了这几招

&#x1f4e3;&#x1f4e3;&#x1f4e3;&#x1f4e3;&#x1f4e3;&#x1f4e3;&#x1f4e3; &#x1f38d;大家好&#xff0c;我是慕枫 &#x1f38d;前阿里巴巴高级工程师&#xff0c;InfoQ签约作者、阿里云专家博主&#xff0c;一直致力于用大白话讲解技术知识 &#x…

一个简单的Linux内核字符驱动程序编写

一、背景 为了了解设备驱动程序的框架&#xff0c;在此编写一个简单的字符驱动程序&#xff0c;以此来对驱动程序的框架进行一个简单的了解。 二、设备驱动程序 所谓设备驱动程序&#xff0c;其实就是计算机硬件与外部设备进行通信的接口。由于硬件设备各式各样&#xff0c;…

【Ctfer训练计划】——(四)

作者名&#xff1a;Demo不是emo 主页面链接&#xff1a;主页传送门创作初心&#xff1a;舞台再大&#xff0c;你不上台&#xff0c;永远是观众&#xff0c;没人会关心你努不努力&#xff0c;摔的痛不痛&#xff0c;他们只会看你最后站在什么位置&#xff0c;然后羡慕或鄙夷座右…

【kafka】学习笔记(三)

学习笔记七、Kafka-Eagle 监控7.1 环境准备7.2 Eagle 安装7.3、修改配置文件7.4、添加环境变量7.5、启动Eagle八、Kafka-Kraft 模式8.1、Kafka-Kraft 集群部署8.2、初始化集群数据目录8.3、启动 kafka 集群8.4、测试8.5、集群启动脚本九、SpringBoot集成Kafka七、Kafka-Eagle 监…