堆排序

     以下内容主要参考了严蔚敏版的数据结构教材。
     对于n个元素的序列 { k 0 , k 1 , k 2 , . . . k n − 1 } \{k_0,k_1,k_2,...k_{n-1}\} {k0,k1,k2,...kn1},如果 k i < = k 2 i + 1 k_i<=k_{2i+1} ki<=k2i+1 k i < = k 2 i + 2 k_i<=k_{2i+2} ki<=k2i+2(小堆)或者 k i > = k 2 i + 1 k_i>=k_{2i+1} ki>=k2i+1 k i > = k 2 i + 2 k_i>=k_{2i+2} ki>=k2i+2(大堆),其中 i = 0 , 1 , 2 , . . . , ⌊ n 2 ⌋ − 1 i=0,1,2,...,\lfloor \frac{n}{2}\rfloor-1 i=0,1,2,...,2n1,则该序列称为堆。当以一维数组存储堆时索引i也可以看成是数组中元素的索引。如果将某堆的一维数组存储结构看成是某颗二叉树的顺序存储结构,则该二叉树上所有非叶子节点的值都不大于其左右孩子的值(小堆)或者该二叉树上所有非叶子节点的值都不小于其左右孩子的值(大堆)。二叉树的根节点是所有n个节点的值中最小的(小堆)或者二叉树的根节点是所有n个节点的值中最大的(大堆)。以二叉树作为图示的两个堆的实例如图1所示。

 
图1.

     如果在得到堆的最小或最大元素之后,将剩下的 n − 1 n-1 n1个元素重新调整又重建一个新的堆就可以得到n个元素中的次小或次大值。重复以上过程便可以得到一个有序序列,这个过程称之为堆排序。
     堆排序需要解决两个问题:

  1. 如何由一个无序序列建成一个堆。
  2. 如何在输出堆顶元素之后,调整剩余的元素成为一个新的堆。

     对于第二个问题,在输出堆顶元素之后可以用堆中的最后一个元素替代堆顶元素,然后比较当前堆顶元素和其左右孩子中较大的那个(大堆)或较小的那个(小堆),如果堆顶元素大于等于较大的那个则调整结束否则堆顶元素和较大的那个互换并从互换的那个孩子开始递归的调整(大堆),如果堆顶元素小于等于较小的那个则调整结束否则堆顶元素和较小的那个互换并从互换的那个孩子开始递归的调整(小堆)。调整直到当前要调整的节点为叶子节点或当前调整过程没有出现互换时停止。对于图1所示的大堆的排序过程如图2所示。

 
图2.

     对于第一个问题,可以从第 ⌊ n 2 ⌋ − 1 \lfloor \frac{n}{2}\rfloor-1 2n1个元素(最后一个非终端节点)开始向下递归调整,接着是倒数第二个非终端节点,最后是根节点。从一个无序序列建大堆的过程如图3所示。

 
图3.
// To heapify a subtree rooted with node i which is an index in arr[]. n is size of heap 
void heapify(int arr[], int n, int i)
{
	int largest = i; // Initialize largest as root 
	int l = 2 * i + 1; // left = 2*i + 1 
	int r = 2 * i + 2; // right = 2*i + 2 

	// If left child is larger than root 
	if (l < n && arr[l] > arr[largest])
		largest = l;

	// If right child is larger than largest so far 
	if (r < n && arr[r] > arr[largest])
		largest = r;

	// If largest is not root 
	if (largest != i)
	{
		swap(arr[i], arr[largest]);

		// Recursively heapify the affected sub-tree 
		heapify(arr, n, largest);
	}
	return;
}

// main function to do heap sort 
void heapSort(int arr[], int n)
{
	// Build heap (rearrange array) 
	for (int i = n / 2 - 1; i >= 0; i--)
		heapify(arr, n, i);

	// One by one extract an element from heap 
	for (int i = n - 1; i > 0; i--)
	{
		// Move current root to end 
		swap(arr[0], arr[i]);

		// call max heapify on the reduced heap 
		heapify(arr, i, 0);
	}
}

/* A utility function to print array of size n */
void printArray(int arr[], int n)
{
	for (int i = 0; i < n; ++i)
		cout << arr[i] << " ";
	cout << "\n";
}
// Driver program 
int main()
{
	int arr[] = { 12, 11, 13, 5, 6, 7 };
	int n = sizeof(arr) / sizeof(arr[0]);

	heapSort(arr, n);

	cout << "Sorted array is \n";
	printArray(arr, n);
}

     源代码来源于这里。堆排序对记录数较少的文件并不值得提倡,但对记录数较大的文件还是比较值得的。其主要时间花费主要是在初始建堆和后续调整建新堆的过程中。对于深度为k的堆(其实就是其对应的完全二叉树的高度),向下调整的过程中最多进行 2 ( k − 1 ) 2(k-1) 2(k1)次关键字的比较。对于有n个元素高度为h的初始序列,初始建堆的过程中关键字比较的次数不会超过 4 n 4n 4n。推导如下:因为第i层 ( i = 1 , 2 , . . . , h ) (i=1,2,...,h) (i=1,2,...,h)的节点数最多为 2 i − 1 2^{i-1} 2i1,以它们为根的二叉树的深度为 h − i + 1 h-i+1 hi+1,则第i层中的所有节点在初始建堆的过程中关键字的比较的次数之和为

  • 2 i − 1 ∗ 2 ( h − i ) = 2 i ∗ ( h − i ) 2^{i-1}*2(h-i)=2^i*(h-i) 2i12(hi)=2i(hi)

则整个初始建堆过程中关键字的比较次数为:

  • ∑ i = h − 1 1 2 i ∗ ( h − i ) \sum\limits_{i=h-1}^{1}2^i*(h-i) i=h112i(hi),令 i = h − j i=h-j i=hj则有:
  • ∑ i = h − 1 1 2 i ∗ ( h − i ) \sum\limits_{i=h-1}^{1}2^i*(h-i) i=h112i(hi)= ∑ j = 1 h − 1 2 h − j ∗ j \sum\limits_{j=1}^{h-1}2^{h-j}*j j=1h12hjj= ∑ j = 1 h − 1 2 h j 2 j \sum\limits_{j=1}^{h-1}2^h\frac{j}{2^j} j=1h12h2jj= 2 h ∑ j = 1 h − 1 j 2 j 2^h\sum\limits_{j=1}^{h-1}\frac{j}{2^j} 2hj=1h12jj<= 2 n ∑ j = 1 h − 1 j 2 j 2n\sum\limits_{j=1}^{h-1}\frac{j}{2^j} 2nj=1h12jj
  • 又因为 ∑ j = 1 h − 1 j 2 j = 1 2 + 2 4 + 3 8 + 4 16 + . . . + h − 1 2 h − 1 \sum\limits_{j=1}^{h-1}\frac{j}{2^j}=\frac{1}{2}+\frac{2}{4}+\frac{3}{8}+\frac{4}{16}+...+\frac{h-1}{2^{h-1}} j=1h12jj=21+42+83+164+...+2h1h1,2 ∑ j = 1 h − 1 j 2 j = 1 + 2 2 + 3 4 + 4 8 + . . . + h − 1 2 h − 2 \sum\limits_{j=1}^{h-1}\frac{j}{2^j}=1+\frac{2}{2}+\frac{3}{4}+\frac{4}{8}+...+\frac{h-1}{2^{h-2}} j=1h12jj=1+22+43+84+...+2h2h1,所以:
  • 2 ∑ j = 1 h − 1 j 2 j − ∑ j = 1 h − 1 j 2 j 2\sum\limits_{j=1}^{h-1}\frac{j}{2^j}-\sum\limits_{j=1}^{h-1}\frac{j}{2^j} 2j=1h12jjj=1h12jj= ∑ j = 1 h − 1 j 2 j \sum\limits_{j=1}^{h-1}\frac{j}{2^j} j=1h12jj= 1 + 1 2 + 1 4 + 1 8 + . . . + 1 2 h − 2 − h − 1 2 h − 1 1+\frac{1}{2}+\frac{1}{4}+\frac{1}{8}+...+\frac{1}{2^{h-2}}-\frac{h-1}{2^{h-1}} 1+21+41+81+...+2h212h1h1
  • 又因为 1 + 1 2 + 1 4 + 1 8 + . . . + 1 2 h − 2 1+\frac{1}{2}+\frac{1}{4}+\frac{1}{8}+...+\frac{1}{2^{h-2}} 1+21+41+81+...+2h21= 2 − 1 2 h − 2 2-\frac{1}{2^{h-2}} 22h21(等比数列求和),所以:
  • ∑ j = 1 h − 1 j 2 j \sum\limits_{j=1}^{h-1}\frac{j}{2^j} j=1h12jj= 2 − 1 2 h − 2 − h − 1 2 h − 1 2-\frac{1}{2^{h-2}}-\frac{h-1}{2^{h-1}} 22h212h1h1= 2 − h + 1 2 h − 1 2-\frac{h+1}{2^{h-1}} 22h1h+1<=2.最后因此:
  • ∑ i = h − 1 1 2 i ∗ ( h − i ) \sum\limits_{i=h-1}^{1}2^i*(h-i) i=h112i(hi)<=4n

     初始堆建立完毕后还要调用 v o i d h e a p i f y ( i n t a r r [ ] , i n t n , i n t i ) void\quad heapify(int\quad arr[], int \quad n, int \quad i) voidheapify(intarr[],intn,inti)一共 n − 1 n-1 n1次来得到最后的有序序列,这个过程的关键字比较次数为(n个节点的完全二叉树的高度为 ⌊ log ⁡ 2 n ⌋ + 1 \lfloor \log_2^n \rfloor+1 log2n+1):

  • 2 ∗ ( ⌊ log ⁡ 2 n − 1 ⌋ + ⌊ log ⁡ 2 n − 2 ⌋ + . . . + ⌊ log ⁡ 2 2 ⌋ ) 2*(\lfloor \log_2^{n-1} \rfloor+\lfloor \log_2^{n-2} \rfloor+...+\lfloor \log_2^2 \rfloor) 2(log2n1+log2n2+...+log22)<= 2 ∗ n ⌊ log ⁡ 2 n ⌋ 2*n\lfloor \log_2^n\rfloor 2nlog2n

因此堆排序在最坏情况下的时间复杂度为 O ( 4 n + 2 n log ⁡ 2 n ) O(4n+2n\log_2^n) O(4n+2nlog2n)= O ( n log ⁡ 2 n ) O(n\log_2^n) O(nlog2n)
     在面试中经常遇到要求从很大量的数据(m个)中(比如100万)找到最大的几个(n个)(比如10)树。这时我们可以先从海量数据中选取少部分的数据(k个)(比如100个)并构建一个小堆,在迭代剩下的数据,如果当前迭代的数据小于等于堆顶的元素则继续迭代下一个数据,如果当前迭代的数据大于堆顶的元素,则用当前的迭代元素代替堆顶元素并重新调整成堆。迭代完之后再在这k的数据中寻找最大的那m个。在内存不太够的情况下,这种方法也是可行的,可以只是将堆放在内存中,然后每次从硬盘中读取剩下的下一个需要迭代的数据。
     也可以先将这大量的数据划分成许多块,然后每一块分别进行排序(如果有多台机器的话可以真真的并行执行)。最后从这许多块排好序的数据块中挑出所需要的最大的几个数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qqssss121dfd

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值