算法 (三)快速排序、堆排序、堆结构应用:获取最小的K个数、获取中位数

本文详细介绍了快速排序的实现原理、复杂度分析,包括三项切分的随机快排,以及堆排序的堆相关概念和实现过程。同时探讨了堆结构在获取最小的K个数和中位数问题上的应用,分析了不同情况下使用大根堆和小根堆的优劣。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、快速排序

时间复杂度O(N*logN),额外空间复杂度O(logN)

1.1 简单分析

这里只讲快排的完善版本:三项切分的随机快排

将一个数组分为小于一个数,等于一个数,大于一个数的三部分(主要是下面的partition函数来实现),为了节省变量,默认取最右边的数(这里用随机交换处理了一下,防止复杂度对数据有依赖),然后遍历数组,递归。

1.2快排的复杂度分析:

常数项很少,在时间复杂度都为O(nlogn)(因为涉及到了递归和master公式,详见上一篇博文)的时候,拼常数项,快排(随机快排)的额外空间复杂度是O(logN),空间浪费在要记住断点,patition,相当于二分查找,最差情况是O(N)(每次断电都在最右或者最左,所以这里用数组中随机的一个数和最右边的数交换,防止这种情况出现
在工程上,快排不是递归的版本(工程上不会存在递归行为)

注意,这里代码有个坑:
在交换数组中两个数的时候,本来想装高级用一下异或交换,结果出来全是0,焦头烂额,因为这个异或是有坑的(敲黑板!!!),一个数异或自己,就是0,在这个快排中,当【swap(arr, ++less, lo++);】的时候,会出现自己和自己交换,这个时候用异或就会导致这个数变成0,所以啊,还是老老实实用temp交换吧
(参考文章:用异或进行两个数交换的陷阱

具体代码如下:

package cn.nupt.sort;

import java.util.Arrays;

/**
 * @Description: 快速排序(随机快排)
 *
 * @author PizAn
 * @date 2019年1月23日 下午8:30:34
 * 
 */
public class QuickSort {
	public static void quickSort(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		quickSort(arr, 0, arr.length - 1);

	}

	private static void quickSort(int[] arr, int lo, int hi) {

			if(lo >= hi){
				return;
				}
		
			swap(arr, lo + (int) ((hi - lo + 1) * Math.random()), hi); // 将hi换成是[lo,hi]里面的随机数,这就是随机快排的关键,这里的duble转int,比如10.9转了就变成10了
			int[] p = partition(arr, lo, hi); // 将lo到hi分成三份,小于hi,等于hi和大于hi
												// ,快排的第二个关键
			quickSort(arr, lo, p[0] - 1);// 递归调用
			quickSort(arr, p[1] + 1, hi);

	

	}

	private static int[] partition(int[] arr, int lo, int hi) {
		int less = lo - 1;
		int more = hi;
		while (lo < more) {
			if (arr[lo] < arr[hi]) { // 如果指到的是比hi小的数,和less后面一个数交换,然后less往后走一个囊括,lo也指下一个数
				swap(arr, ++less, lo++); // less永远指向左边部分最后一个
			} else if (arr[lo] > arr[hi]) { // 如果指到的是比hi大的数,和more指针换一下位置,然后more指针往前走一格,将交换后的比hi大的数囊括进去
				swap(arr, --more, lo); // more永远指向右边部分的第一个数(注意:这个地方lo没有++,因为从--more换来的数还不知道是不是比arr[hi]小,还需要再来一次循环比较!!!!!)
			} else {
				lo++;
			}

		}
		swap(arr, more, hi);// 因为我们是以arr[hi]为指标的,这个时候和more交换一下,more就知道了中间的一部分的最后一个数

		return new int[] { less + 1, more };

	}

	private static void swap(int[] arr, int i, int j) {
		int temp = arr[i];
		arr[i] = arr[j];
		arr[j] = temp;

	}


2、堆排序

时间复杂度O(N*logN),额外空间复杂度O(1)

2.1 堆的相关概念(优先级队列结构,就是堆结构)

完全二叉树:满二叉树,或者从左至右补齐的二叉树,堆就是完全二叉树

堆又分为大根堆和小根堆:

  • 大根堆:在这颗树中,任何一颗子树,最大值就是头部
  • 小根堆:在这颗树中,任何一颗子树,最小值就是头部
2.2 堆排序

堆之所以可以对数组进行排序,是因为数组可以表示成完全二叉树(即从左至右补齐的二叉树)(注:这是为了方便描述,脑补的,实际上没有生成),可以通过下面三个公式找到其左右孩子和父节点(0的父节点是自己)

  • 对于数组中第i个数的左节点:2 * i + 1
  • 对于数组中第i个数的右节点:2 * i + 2
  • 对于数组中第i个数的父节点:( i - 1 ) / 2

堆的两个主要方法:

  1. heapInsert:一个节点如果比它的头节点大,上浮(一般用来将数组构建为大根堆)
    建立一个大根堆的时间复杂度为O(N)
  2. heapify:下沉,一个头节点如果变小了,那么就重新调整这个堆,将小的数下沉,一般是用来堆排序的时候,伴随着堆size的减小和排序的进行
    时间复杂度为O(NlogN),所以堆排的时间复杂度为O(N + NlogN) = O( NlogN)

堆排序的主要步骤是:

  1. 将数组变成大根堆(heapInsert),这个时候0指针处就是数组中最大的数
  2. 0和数组最后一个数交换,堆的size减小一,就找到了数组最大的数
  3. 剩下的heapify,将0指针处的数下沉,重新构建大根堆
  4. 如此往复,得到从小到大的排序数组

在下面的代码中,注意,在比较index的左右两个子节点大小并把largest的名号赋予最大的一个时,下面两个代码是有区别的!!!!下面的是正确的,因为在右节点在超出size的时候,largest就是左节点了

int largest = left + 1 < size && arr[left] > arr[left + 1] ? left : left + 1;
int largest = left + 1 < size && arr[left + 1] > arr[left] ? left + 1 : left;//左右孩子哪个大,谁的下标就是largest

完整代码和注释如下(这里加上了对数器,作测试用,具体看前面的博文):

package cn.nupt.sort;

import java.util.Arrays;

/**
 * @Description: 堆排序
 *
 * @author PizAn
 * @date 2019年1月24日 下午2:25:57
 * 
 */
public class HeapSort {

	public static void heapSort(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}

		for (int i = 0; i < arr.length; i++) {
			heapInsert(arr, i);
		}

		int size = arr.length;
		swap(arr, 0, --size);
		while (size > 0) {
			heapify(arr, 0, size);
			swap(arr, 0, --size);
		}

	}

	// 将index上浮到最大,將数组变成大根堆形式
	private static void heapInsert(int[] arr, int index) {
		while (arr[index] > arr[(index - 1) / 2]) {
			swap(arr, index, (index - 1) / 2);
			index = (index - 1) / 2;
		}

	}

	// 将index下沉到最小,在交换了0和最后一个元素后,重新调整,变成 大根堆
	private static void heapify(int[] arr, int index, int size) {
		int left = 2 * index + 1;
		//在index、index的左右子节点中找到最大的那个
		while (left < size) {

			// 在index的左右节点比出大小
			int largest = left + 1 < size && arr[left + 1] > arr[left] ? left + 1 : left;// 左右孩子哪个大,谁的下标就是largest

			// 左右子节点的胜者再和index比较,如果还是index大,那么就不要下沉,直接退出,如果比index大,那么下沉,
			// 在这个堆排序中,0与最后一个元素交换后,index初始化为0,将0处的元素下沉
			largest = arr[index] > arr[largest] ? index : largest;

			if (largest == index) {
				break;
			}

			// 将index和largest交换,也就是将小的下沉了
			swap(arr, index, largest);
			// index指向最大的,这里肯定是之前的index的子节点了
			index = largest;

			left = 2 * index + 1;
		}

	}

	private static void swap(int[] arr, int i, int j) {
		int temp = arr[i];
		arr[i] = arr[j];
		arr[j] = temp;

==========================for test======================

	public static void comparator(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		Arrays.sort(arr);
	}

	public static int[] generateRandonArray(int maxSize, int maxValue) {
		int[] arr = new int[(int) ((maxSize + 1) * Math.random())];

		for (int i = 0; i < arr.length; i++) {
			arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) ((maxValue) * Math.random());
		}

		return arr;

	}

	public static boolean isEqual(int[] arr1, int[] arr2) {
		if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
			return false;
		}

		if (arr1 == null && arr2 == null) {
			return true;
		}

		if (arr1.length != arr2.length) {
			return false;
		}

		for (int i = 0; i < arr2.length; i++) {
			if (arr1[i] != arr2[i]) {
				return false;
			}
		}

		return true;

	}

	public static void printArray(int[] arr) {
		if (arr == null) {
			return;
		}
		for (int i = 0; i < arr.length; i++) {
			System.out.print(arr[i] + " ");
		}
		
		System.out.println();

	}
	
	public static int[] copyArray(int[] arr) {
		int[] arrSup = new int[arr.length];
		for (int i = 0; i < arrSup.length; i++) {
			arrSup[i] = arr[i];
		}
		return arrSup;
		
	}
	
	
	public static void main(String[] args) {
		int loopTimes = 10000;
		int maxSize = 5000;
		int maxValue = 5000;
		boolean flag = true;
		for (int i = 0; i < loopTimes; i++) {
			
			int[] arr1 = generateRandonArray(maxSize, maxValue);
			int[] arr2 = copyArray(arr1);
			
			heapSort(arr1);
			comparator(arr2);
			if(!isEqual(arr1, arr2)){
				flag = false;
				printArray(arr1);
				printArray(arr2);
				break;
			}
			
		}
		
		System.out.println(flag ? "good job!" : "oh no");
		
	}

}

3、堆结构应用:获取最小的K个数、获取中位数

思路:两种堆:大根堆,存k个数;用小根堆,弹k次 (这里取最小的K个数,创建大根堆, 取最大的K个数,创建小根堆
最小堆和最小堆的时间复杂度都是n*logn,但是最小堆在k个数之后添加的话需要调整堆, 但是最大堆只要每次和顶端的比较就可以。另外最小堆不能解决数字动态增加的情况。

下面用大根堆来获取一个数组中最小的k个数(这里重复的即相同的是否算一个数可以在后面的判断语句中修改)

package cn.nupt;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.PriorityQueue;

/**
 * @Description: 输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。
 *
 * @author PizAn
 * @Email pizan@foxmail.com
 * @date 2019年3月11日 下午1:17:03
 * 
 */
public class Q_pp_GetLeastNumbers {

	public ArrayList<Integer> GetLeastNumbers_Solution(int[] input, int k) {

		// 解析:两种方法:大根堆,存k个数 小根堆,弹k次 (这里 取最小的K个数,创建大根堆, 取最大的K个数,创建小根堆)
		// 最小堆和最小堆的时间复杂度都是n*logn,但是最小堆在k个数之后添加的话需要调整堆,
		// 但是最大堆只要每次和顶端的比较就可以。另外最小堆不能解决数字动态增加的情况。

		if (input == null) {
			return null;
		}

		ArrayList<Integer> list = new ArrayList<Integer>();
		if (k == 0 || k > input.length) {
			return list; // 这里就是返回""
		}

		// 取最小的K个数,建立一个大根堆(这里重写了比较器,默认是创建小根堆)
		PriorityQueue<Integer> maxHeep = new PriorityQueue<Integer>(new Comparator<Integer>() {

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

		// 遍历数组,往堆里放东西

		for (int i = 0; i < input.length; i++) {
			if (maxHeep.size() < k) { // 如果堆里面的数没有k个,入堆
				maxHeep.offer(input[i]);
			} else {
				if (maxHeep.peek() > input[i]) { // 如果堆顶元素大于arr[i],弹出堆顶元素,将arr[i]入堆
					maxHeep.poll();
					maxHeep.offer(input[i]);
				}
			}

		}

		//对于堆的遍历,这里可以用迭代器,也可以再开辟一个ArrayList,并将maxHeep放到ArrayList的构造方法里
		
		//方法一:ArrayList构造函数
		/*ArrayList<Integer> list2 = new ArrayList<Integer>(maxHeep);
		return list2;*/
		
		//方法二:迭代器
	/*	
	  Iterator<Integer> iter = maxHeep.iterator();
		while (iter.hasNext()) {
			list.add(iter.next());
			
			
		}

		return list;*/
		
		
		//方法三,直接增强for循环
		for(Integer inte : maxHeep){
			list.add(inte);
		}
		
		return list;

		

	}
}

堆还有很多用处,举个例子,不断获得随机数,在某个时间段我想知道我已经获得的数的中间值

  1. 如果按照之前的思路,我们要先找个集合存放这些数,如数组,然后用O(NlogN)复杂度的排序算法来排序,再计算中间值
  2. 如果用堆,建立一个大根堆,建立一个小根堆,然后偶数个放在大根堆里,奇数个放在小根堆里(不是直接放,而是先放到小/大根堆里,然后弹出一个放到大/小根堆里,这样就能保证小根堆里全是大数,大根堆里全是小数了),始终保持大根堆的占N/2的较小数据,小根堆的占N/2大的数据,随时可以知道中间数,其加入数和弹出数的时间复杂度只跟树的层数有关,就是log(N),即弹出表头,然后重新下沉建立大的时间,试想,如果我现在获得了40多亿数据,用堆,算下来就是log(40亿)= 32
    下面是具体代码:
package cn.nupt;

import java.util.Comparator;
import java.util.PriorityQueue;

/**
 * @Description: 获取数据流中的中位数
 *
 * @author PizAn
 * @Email pizan@foxmail.com
 * @date 2019年3月11日 下午3:48:54
 * 
 */
public class Q_pp_GetMedian {

	private PriorityQueue<Integer> minHeep;
	private PriorityQueue<Integer> maxHeep;
	int count = 0;

	public Q_pp_GetMedian() {
		minHeep = new PriorityQueue<Integer>();
		maxHeep = new PriorityQueue<Integer>(new Comparator<Integer>() {

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

	public void Insert(Integer num) {
		//这个地方是1,然后下面就先把数放在了max中,所以如果总数是奇数(这个跟count放在前后没关系,反正是要++),那肯定是man中多了一个数
		count++; 

		if (count % 2 == 0) { //偶数个放到 min
			maxHeep.offer(num);
			minHeep.offer(maxHeep.poll());
		} else {  //奇数个放到 max
			minHeep.offer(num);
			maxHeep.offer(minHeep.poll());
		}

	}

	public Double GetMedian() {
		if (count % 2 == 1) {// 入堆是奇数,不用想,肯定是大根堆里面的数多一个
			// return (double) maxHeep.peek();
			return new Double(maxHeep.peek());

		} else {
			return ((double) (maxHeep.peek() + (double) minHeep.peek()) / 2);
			// return new Double((maxHeep.peek() + minHeep.peek()) / 2);
		}

	}

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值