堆结构与堆排序

145 阅读7分钟

堆的相关概念

1.堆结构就是用数组实现的完全二叉树结构,逻辑上是一棵完全二叉树,但物理上是保存在数组中。

2.完全二叉树中如果每棵子树的最大值都在顶部就是大根堆

3.完全二叉树中如果每棵子树的最小值都在顶部就是小根堆

顺序存储中父子节点的关系

数组模拟,从0位置开始,i为下标

父节点:(i-1)/2

左子节点:2*i+1

右子节点:2*i+2

通过i下标,和公式,就可以得出当前下标节点的父节点,与左右子节点的位置。

数组模拟,从1位置开始,i为下标

左:2*i (i << 1)

右:2*i+1 (i << 1 |1)

父:i/2 (i >> 1)

数组模拟堆,第一个是从0位置开始,第二个是从1位置开始。

因为开始位置不同,所以计算的公式也不同。但第从1位置开始,可以优化成位运算。

Java中的堆实现

public static class MyMaxHeap {
		private int[] heap;
		private final int limit;
		private int heapSize;

		public MyMaxHeap(int limit) {
			heap = new int[limit];
			this.limit = limit;
			heapSize = 0;
		}

		public boolean isEmpty() {
			return heapSize == 0;
		}

		public boolean isFull() {
			return heapSize == limit;
		}

		public void push(int value) {
			if (heapSize == limit) {
				throw new RuntimeException("heap is full");
			}
			heap[heapSize] = value;
			heapInsert(heap, heapSize++);
		}

		public int pop() {
			int ans = heap[0];
			swap(heap, 0, --heapSize);
			heapify(heap, 0, heapSize);
			return ans;
		}

		private void heapInsert(int[] arr, int index) {
		
			while (arr[index] > arr[(index - 1) / 2]) {
				swap(arr, index, (index - 1) / 2);
				index = (index - 1) / 2;
			}
		}
		
		private void heapify(int[] arr, int index, int heapSize) {
			int left = index * 2 + 1;
			while (left < heapSize) { 
				
				int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
				largest = arr[largest] > arr[index] ? largest : index;
				if (largest == index) {
					break;
				}
				swap(arr, largest, index);
				index = largest;
				left = index * 2 + 1;
			}
		}

		private void swap(int[] arr, int i, int j) {
			int tmp = arr[i];
			arr[i] = arr[j];
			arr[j] = tmp;
		}

	}

堆结构两个重要方法,heapInsert与heapify

heapInsert用于构建堆

push添加操作就主要调用的heapInsert方法

public void push(int value) {
			if (heapSize == limit) {
				throw new RuntimeException("heap is full");
			}
			//既是size,也可以用作新来元素存放的位置
			heap[heapSize] = value;
			heapInsert(heap, heapSize++);
		}

因为用的数组实现堆,当数组添加了一个元素时,就会执行heapInsert操作,调整元素位置,以构建成逻辑上的堆。

heapInsert方法

private void heapInsert(int[] arr, int index) {
			// (i-1)/2 为父节点位置
			// 循环交换,直到不比父大
			// index=0
			while (arr[index] > arr[(index - 1) / 2]) {
				swap(arr, index, (index - 1) / 2);
				index = (index - 1) / 2;
			}
		}

新加进来的数,现停在了index位置。然后进行通过公式,与当前父节点循环比较,如果当前index大于此时它的父节点,就与父节点进行交换。不仅值交换了,当然index也要进行更新,变成它之前的父节点的index值。然后继续进行比较交换。

添加一个数,数在堆的逻辑中,位于完全二叉树的最底层。然后通过比较值的大小,不断往上移动,移动到正确的位置,以构成最大堆。

heapify也用于维护堆结构

pop取出数据主要调用heapify方法

public int pop() {
			int ans = heap[0];
			swap(heap, 0, --heapSize);
			heapify(heap, 0, heapSize);
			return ans;
		}

pop取出大根堆中最大的值,即数组中的第一个数,相当于完全二叉树中的根节点。

pop数据,我们需要维护堆的结构。

所以上面代码大致思路为

记录heap[0]的值,然后将根节点与最后的节点进行交换,堆的heapSize减1。

这样根节点的值不是删除,而是使用最后的根节点进行补充,而原来的根节点因为交换到了最后,而heapSize也减1,所以相当于,将其排除出了堆中。

但因为交换后根节点的值,不满足大根堆的结构,所以我们需要进行heapify,维护堆结构。

heapify方法

因为根节点的值为之前最小节点的值,所以我们可以将其,与子节点进行循环比较。

往下看,不断的下沉,循环完成,则为一个正常的堆结构。

private void heapify(int[] arr, int index, int heapSize) {
			//通过index根节点,计算出左子节点位置
			int left = index * 2 + 1;
			//有左子节点,就进入循环,进行比较。
			// 如果有左孩子,可能有或没有右孩子。所以具体的在循环内部操作
			while (left < heapSize) { 
				// 把较大孩子的下标,给largest
				// 左右子节点的比较,如果存在右子节点left + 1 < heapSize,
				// 并且右子节点大于左子节点,largest就为右子节点,否则是左
				int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
				
				//最大值节点与父节点比较,最大为largest
				largest = arr[largest] > arr[index] ? largest : index;
				//largest等于index,即在上一行比较中,子节点没干过父节点index,index为最大值,堆结构正常,直接break
				if (largest == index) {
					break;
				}
				//子节点大于父节点index,index和较大子节点,要互换
				swap(arr, largest, index);
				//更新index与left,继续比较。
				index = largest;
				left = index * 2 + 1;
			}
		}

注:优先级队列结构,就是堆结构

堆排序

堆排序时间复杂度

一个数组,进行堆排序。

数组所有元素,循环heapinsert构建堆。时间复杂度O(N*logN)

因为一共N个数,想象中的树高度为logN ,所以一次操作O(logN),循环了N次。即O(N * logN)

然后不断swap,heapSize减一,heapify。

相当于将最大数放到数组末尾,heapSize减一将其排除出堆结构,heapify维护堆结构

		int heapSize = arr.length;
		swap(arr, 0, --heapSize);
		// O(N*logN)
		while (heapSize > 0) { // O(N)
			heapify(arr, 0, heapSize); // O(logN)
			swap(arr, 0, --heapSize); // O(1)
		}

此操作也是,O(N * logN)

所有整个sort操作有2次O(N * logN),所以堆排序的时间复杂度为O(N * logN)

堆排序的优化

一个需求,只需要将数组形成堆结构,不需要排序,可以有O(N)的时间复杂度。

上面构建堆,是循环heapInsert,时间复杂度O(N*logN)

而我们可以从数组尾部倒序开始,每个节点heapify。

此操作只能用于一次得到所有数据,然后构建堆。如果是一个一个得到数据,则不适用

 for (int i = arr.length - 1; i >= 0; i--) {
		 	heapify(arr, i, arr.length);
		 }

==为什么是O(N)复杂度?==

可能有人会疑惑,为什么堆排序中,我的一次heapify是 O(logN)。循环N次O(N * logN)

但构建堆时,同样的heapify,进行N次,怎么变成O(N)了。

因为堆排序中,每次的heapify都是完整都走完树的高度,logN。循环N次,即O(N * logN)

但我构建堆时

从最后一层(N/2个节点)开始heapify,但无法往下沉,进行一次操作,复杂度N/2*1

倒数第二层(N/4个节点),heapify,最多往下沉1次,所以2次操作,复杂度N/4*2

倒数第三层(N/8个节点),heapify,最大往下沉2次,所以3次操作,复杂度N/8*3

依次类推

可以参考知乎的回答

堆排序中建堆过程时间复杂度O(n)怎么来的? - TwoFrogs的回答 - 知乎 www.zhihu.com/question/20…

堆排序代码与对数器

public class Code03_HeapSort {

	// 堆排序额外空间复杂度O(1)
	public static void heapSort(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		// O(N*logN)
		for (int i = 0; i < arr.length; i++) { // O(N)
			heapInsert(arr, i); // O(logN)
		}
		// O(N)

		// for (int i = arr.length - 1; i >= 0; i--) {
		// 	heapify(arr, i, arr.length);
		// }
		int heapSize = arr.length;
		swap(arr, 0, --heapSize);
		// O(N*logN)
		while (heapSize > 0) { // O(N)
			heapify(arr, 0, heapSize); // O(logN)
			swap(arr, 0, --heapSize); // O(1)
		}
	}

	// arr[index]刚来的数,往上
	public static void heapInsert(int[] arr, int index) {
		while (arr[index] > arr[(index - 1) / 2]) {
			swap(arr, index, (index - 1) / 2);
			index = (index - 1) / 2;
		}
	}

	// arr[index]位置的数,能否往下移动
	public static void heapify(int[] arr, int index, int heapSize) {
		int left = index * 2 + 1; // 左孩子的下标
		while (left < heapSize) { // 下方还有孩子的时候
			// 两个孩子中,谁的值大,把下标给largest
			// 1)只有左孩子,left -> largest
			// 2) 同时有左孩子和右孩子,右孩子的值<= 左孩子的值,left -> largest
			// 3) 同时有左孩子和右孩子并且右孩子的值> 左孩子的值, right -> largest
			int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
			// 父和较大的孩子之间,谁的值大,把下标给largest
			largest = arr[largest] > arr[index] ? largest : index;
			if (largest == index) {
				break;
			}
			swap(arr, largest, index);
			index = largest;
			left = index * 2 + 1;
		}
	}

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

	// for test
	public static void comparator(int[] arr) {
		Arrays.sort(arr);
	}

	// for test
	public static int[] generateRandomArray(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;
	}

	// for test
	public static int[] copyArray(int[] arr) {
		if (arr == null) {
			return null;
		}
		int[] res = new int[arr.length];
		for (int i = 0; i < arr.length; i++) {
			res[i] = arr[i];
		}
		return res;
	}

	// for test
	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 < arr1.length; i++) {
			if (arr1[i] != arr2[i]) {
				return false;
			}
		}
		return true;
	}

	// for test
	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();
	}

	// for test
	public static void main(String[] args) {

		// 默认小根堆
		PriorityQueue<Integer> heap = new PriorityQueue<>();
		heap.add(6);
		heap.add(8);
		heap.add(0);
		heap.add(2);
		heap.add(9);
		heap.add(1);

		while (!heap.isEmpty()) {
			System.out.println(heap.poll());
		}

		int testTime = 500000;
		int maxSize = 100;
		int maxValue = 100;
		boolean succeed = true;
		for (int i = 0; i < testTime; i++) {
			int[] arr1 = generateRandomArray(maxSize, maxValue);
			int[] arr2 = copyArray(arr1);
			heapSort(arr1);
			comparator(arr2);
			if (!isEqual(arr1, arr2)) {
				succeed = false;
				break;
			}
		}
		System.out.println(succeed ? "Nice!" : "Fucking fucked!");

		int[] arr = generateRandomArray(maxSize, maxValue);
		printArray(arr);
		heapSort(arr);
		printArray(arr);
	}

}

本文由博客一文多发平台 OpenWrite 发布!