如果面试官再考在无序数组中找第K小的值,就用这篇文章跟他唠!

在无序数组中找第K小的值,还只会用堆解决?是时候学会O(N)的bfprt算法了

相信大家对这个问题并不陌生,在无序数组中找第K小的值,一般在笔试或者机试的时候,大家能比较快速的写出利用大根堆来解决的算法,其实在Coding 的时候也确实推荐大家用,因为当K比较小时,这个算法的时间复杂度为O(n*logK)级别,已经很不错了,但是当面试官问你这个问题的时,这个答案只能算是中规中矩,首先先分享出用大根堆解决问题的代码

//先构造自己的大根堆比较器
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<Integer>(11,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();
	}

利用容量为K的大根堆,不断更新第K小的值,相信大家都能看懂,时间复杂度O(n*logK),空间自然是额外用了一个堆。(当然也可以顺便因为他的正确性,可以把它当作对数器函数!如果还不了解对数器的同学可以参考我的第一篇文章 学会对数器,自己测试代码!

那么为了更详细的介绍bfprt算法,我们先来看第二种:改写快排的算法。因为bfprt算法只是在这上面做了一个地方的改动。我们直接来看改写快排方法的代码:

// 改写快排,时间复杂度O(N)
	public static int minKth2(int[] array, int k) {
		int[] arr = copyArray(array);
		return process2(arr, 0, arr.length - 1, k - 1);
	}//参数意义:模仿快速排序但其实不排序,下标从0到n-1;找下标为k-1的数
	//也就是第k小的数
	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) { // L = =R ==INDEX
			return arr[L];
		}
		// 不止一个数  L +  [0, R -L]
		int pivot = arr[L + (int) (Math.random() * (R - L + 1))];
		//pivot是随机打在数组某个位置
		// range[0] range[1]
		//  L   ..... R     pivot 
		//  0         1000     70...800
		int[] range = partition(arr, L, R, pivot);//这里的partition函数	
		//是将数组分为——左边小于第k小的值,
		//中间等于,右边大于的结构,不同于快排的是,如果中间区域不中,
		//那么只会去左边小于区或右边大于区一侧进行递归,
		//而快排是无论怎样都是两边都要进行递归
		if (index >= range[0] && index <= range[1]) {
			return arr[index];
		} else if (index < range[0]) {
			return process2(arr, L, range[0] - 1, index);
		} else {
			return process2(arr, range[1] + 1, R, index);
		}
	}

👆这里体现出了随机数pivot的重要性,如果随机的恰好靠近中间,那么时间复杂度接近O(N),如果出现在边上,那可能会退化为O(N^2),所以这个算法的时间复杂度其实是依概率收敛于O(N)。后文的bfprt算法正是改进了pivot的取值而使算法完全收敛于O(N)

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 };
	}

👆partition分区比较简单,不做过多解释

重点来了👇,bfprt算法,改变在于精确的给出了pivot的值,直接看代码

// 利用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];
		}
		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);
		}
	}

👆除了pivot = medianOfMedians(arr, L, R);别的框架没变

// 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];
		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));
		}
		// marr中,找到中位数
		// marr(0, marr.len - 1,  mArr.length / 2 )
		return bfprt(mArr, 0, mArr.length - 1, mArr.length / 2);
	}

👆这里进行解释,首先

1,把数组分成长度为5的小组,最后不足5的为1组,分为了n/5组
2,每组让其内部有序,时间复杂度O(N)
3,取每组中位数,让他们组成数组mArr
4,最后找到mArr数组中的中位数,作为pivot返回给我们需要的基准值

这里可能比较难理解的是,父函数bfprt调用了子函数medianOfMedians,而子函数的最后一行
return bfprt(mArr, 0, mArr.length - 1, mArr.length / 2);
又调用了父函数,父慈子孝哈哈哈哈哈哈哈哈哈哈哈

结合注释多看几遍代码,应该还是能理解的,他的精髓就在于每次递归都能够多甩掉一部分数据,让查找更加集中。最后让我举个例子说明:

假设一共25个数,那么n = 25 ,5个一组,一共五组,n/5。
找出5个排序后的中位数,假设为1,2,3,4,5,他们五个组成mArr
那mArr的中位数就是3,长度为n/5,那4,5是在3之后,所以组内大于3的占n/5的一半也就是n/10;不要忘记这个4,5也只是分别各自组内的中位数,是一个代表,所以大于3的分别还有两个n/10。
至此,对于pivot = 3,至少3n/10,比他大,那么立即推:有7n/10个数比它小。所以每次递归都能够甩掉3n/10的数,时间复杂度:
N + 7N/10 + …一定收敛于O(N)。

这个算法大家学会了吗?当然笔试机试还是推荐用大根堆来解决。讲得不好的地方欢迎大家批评指正!有问题也可以留言私信!持续分享编程计算机相关文章话题,欢迎关注,评论区交个朋友?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值