天天看点

《算法导论》代码搬运工--堆排序

最近买了本中文版的《算法导论》,打算系统的看一下基础的算法,并尽量的实现一下书上的代码例子。书很厚,内容很多,有简单的也有难懂的,所以打算开一个新的系列,长期不定时的加入新的内容,就叫《算法导论》代码搬运工系列。代码基本都是用java实现的,实现语言并不重要,java和C的语法差别也并不大。

这一篇来搬运一个基础的排序算法:堆排序。

堆排序:顾名思义,就是用堆来实现排序的功能,那么在堆排序之前就先要知道什么是堆,怎么建堆。

《算法导论》中用数组来表示一个堆(二叉堆),如下图,那么堆中元素和元素之间的关系就需要使用数组下标来维护了,对一个二叉堆,用层序来依次存储到一维数组中,那么对于一个下标为 i 的元素a[i],它的parent(i)节点可以表示为a[i/2],当然i/2是取floor,左孩子left(i)可以表示为a[i*2],右孩子right(i)可以表示为a[i*2+1](注意下标不越界)。其他堆的性质就很简单了,例如最大堆,那么父亲节点的值一定是大于等于两个孩子节点的值的。

《算法导论》代码搬运工--堆排序

对于堆排序,有三种基本的操作:max-heapify, build-max-heap, heapsort,依次是最大化一个堆(只能针对当前堆,对于当前堆影响不到的其他子堆,算是一次原子操作--我自己的理解),建立一个完整的最大堆(对于一个堆的很多次max-heapify操作,使得整个堆成为最大/最小堆),以及最后的利用堆来排序。

下面是三种操作的java代码:

max-heapify:

public void max_heapify(int a[], int index, int heap_size) {
		int left_index = index * 2;
		int right_index = index * 2 + 1;
		int largest = index;
		if (left_index < heap_size && a[left_index] > a[index])
			largest = left_index;
		if (right_index < heap_size && a[right_index] > a[largest])
			largest = right_index;
		if (largest != index) {
			int temp = a[index];
			a[index] = a[largest];
			a[largest] = temp;
			max_heapify(a, largest, heap_size);
		}
	}
           

过程就是,找出当前节点和它左右孩子中最大的那个,如果最大的不是自己,那么和自己交换,并且对被交换那个下标递归执行当前步骤(之前有过一个困惑,我以为应该对下标为index的做递归,其实要注意到这里的largest下标是固定的,而其中的元素被改变了,所以针对的递归对象应该是以下标为准)。

build-max-heap:

int a[] = { Integer.MIN_VALUE, 16, 4, 10, 14, 7, 9, 3, 2, 8, 1 };
		Heap_sort hs = new Heap_sort();
		int heap_size = a.length;
		// 因为下标n/2+1后面的元素都是叶节点,所以只要从非叶节点开始往下遍历就可以,叶子节点没有必要遍历
		for (int i = heap_size / 2; i >= 1; i--) {
			hs.max_heapify(a, i, a.length);
		}
           

因为0下标需要额外判断,不如让有效内容从下标1开始,然后对非叶子节点进行遍历执行max-heapify操作,因为叶子节点没有孩子,所以没有必要去遍历,又由于二叉堆是完全二叉树,所以下标n/2后面的节点都是叶子节点。

heapsort:

public void heap_sort(int a[]) {
		int heap_size = a.length;
		for (int i = a.length - 1; i > 1; i--) {
			int temp = a[i];
			a[i] = a[1];
			a[1] = temp;
			heap_size--;
			max_heapify(a, 1, heap_size);
		}
	}
           

注意到前面的max-heapify有一个参数是heap_size,如果不牵涉到排序,那么这个参数就一直是数组的length,但是如果牵涉到排序,这个有效length是会变的。因为堆排序的过程是这样的:根据大顶堆的性质,堆顶元素,也就是根节点的值一定是最大的,所以每一次都把堆顶元素和最后一个叶子节点交换,再把这个堆顶元素从堆里面踢出去放在这个数组的末尾,然后把原来的堆最大化,那么下一个最大的元素又冒出来了,再做一样的动作直到堆中元素都被按照顺序排列到数组中,这个数组就是这一堆数字的升序排列(降序只需要做一个小顶堆即可)。

再提一下每一个排序算法都很关注的时间复杂度,堆排序的时间复杂度是O(nlgn),因为并不需要额外的空间,所以空间复杂度一直都是常数级,是原址排序(就地排序)。

在练习题中,有两道证明题,一个是证明在最坏情况下,堆排序的时间复杂度是Ω(nlgn),另一个是证明在所有元素都不同的情况下,堆排序的时间复杂度是Ω(nlgn),不去深究如何证明,但是可以知道堆排序的时间复杂度的数量级一直是nlgn的。对于O, Ω和Θ的定义可以去翻一下数学书,对于一个算法的时间复杂度的界的定义,但是不去特别深究也问题不大。