排序之堆排序

       首先我们看看堆的定义:

       所谓堆是一个满足以下两个条件的二叉树:1、完全二叉树,即除了最后一层右边的元素可能缺位,其他都是满的。2、每个父母节点的键要大于其子女节点的键(父母节点都大于子女节点的键值叫最大堆,都小于叫最小堆)。

       堆排序的第一步就是要将给定序列构造出一个堆。

       我们暂且用数组来实现一个堆。我们以最大堆为例,对于构造一个堆,有两种算法,自底向上以及自顶向下法。下面我们分别来看看这两种算法:


       自底向上法:这一算法的基本思路是将给定序列先全部放到一颗完全二叉树上,然后从最后一个父母节点开始检验,看它是否满足大于子女节点这个条件,若满足则看前一个父母节点,若不满足,则将父节点和较大的子女节点交换位置,再看前一个父母节点,重复这个步骤一直到检查完所有的父母节点。

       这个算法实现起来还是比较容易的,只不过有几个地方要注意一下。

       1、对于一个n元素的堆有下面几个结论:

             a、父母节点位于前 n/2 ,子女节点位于后n/2

             b、若一个父母节点的位置为 i ,则其子女节点分别为 2*i 和 2*i+1,若一个节点堆序号为 i ,则其父母节点的堆序号为 i/2

       注意:以上结论都建立在堆序号是从1 开始的基础上,如果我们用数组来实现堆,那么就要注意数组元素下标和堆序号的关系。

        自底向上法构造堆用伪代码表述如下:

void heapButtonUp(int a[1...n]){
//用自底向上法构造最大堆
//输入:数组a[1...n]
//输出:大堆a[1...n]
for i←n/2 to 1 do      //从1到n/2表示的是父母节点
   k←i;     //记录最后一个父母节点,以便后面改变
   v←a[k];  //
   heap←false;
   while not heap and 2*k<=n
         j←2*k;  //j表示k的一个子女节点
         if j<n     //表明k有两个子女节点
            if a[j]<a[j+1]
                 j←j+1;
          //到此j表示k的较大子女节点的序号
          if v>a[j]{   //满足要求,不要往下掉
              heap←true;
           }else {//不满足要求,往下掉,再次判断
             a[k]←a[j];
             k←j;  //父母节点变为j,再判断
             a[j]←v;
            }  
}

       2、这个算法中,外层循环是要遍历所有的父母节点,要求所有的父母节点满足:大于其子女节点,内层循环的作用是,如果某个父母节点发生了下掉,则要求下掉后,若该节点还是父母节点,也要满足父母节点的要求。这个算法实现起来还是比较容易的,关键是要注意,我们用数组作为堆的存储结构的时候,要注意数组的下标是从0开始,而堆序号要求从1开始。
      
       自顶向下法:该算法的主要思路是将新的键连续的插入预先构造好的堆中,然后将其与父节点进行比较,如果父节点小于新插入的键值,则将父节点和新插入的键进行交换,然后再往上检查,直到满足最大堆为止。(这个算法只描述主要思路)

       堆排序思路:首先将要排序的序列构造出一个最大堆,然后将堆顶元素与最后一个元素交换位置,堆的规模减一,然后再重新构造这个堆,依次进行下去,直到最后一个元素。
       利用自底向上法构造大堆实现堆排序算法如下:
heapSort(int[] a){
//利用自底向上法构造大堆对数组进行堆排序
//输入:可排序数组a[1...n]
//输出:有序数组a[1...n]
for i← 0 to n-1
     heapButtonUp(a[1...n-i];//构造出一个大堆
     a[1]↔a[n-i];  //交换堆顶元素和最后一个元素
     

}

     对于堆排序算法的时间效率,这里先不做详细分析,为O(nlogn),与归并排序的时间效率同属于一类。堆排序还有一个优点就是,它不需要额外的存储空间。在记录数较少的文件并不提倡使用堆排序,但在数据较大的文件还是很有效的。

下面为实现堆排序算法的一个简单程序:
package com.poe.sort;

import java.util.Scanner;

/**
 * 自底向下法构造最大堆
 * 
 ***** 注意点:节点序号不能从0开始,要从1开始,否则第一个父母节点找不到子女节点******** 第k号父母节点的子女节点序号为
 * 2k,2k+1是建立在节点序号从1开始的基础上的,这样要对数组的下标进行处理
 * 
 * @author Sam
 * 
 */
public class HeapButtonUp {
	private static int[] array; // 待构造堆的数组,由命令行输入

	/**
	 * 用自底向下构造最大堆 end表示要构造成堆的数组的终止下标
	 * 
	 * @param a为待构造堆得数组
	 */
	public static void heapBu(int[] a, int end) { // end表示数组的序号
		int heapEnd = end + 1; // 堆序号为1...heapEnd
		for (int i = heapEnd / 2; i > 0; i--) { // 堆序号1...end+1,其中前一半为父母节点,后一半为叶子节点//第一个父母节点为a.length/2,然后要检验这个父母节点是否满足要求
			System.out.println("检查" + i + "号节点");
			int k = i; // k表示父母节点的下标,在后面可能会改变,因此不直接用i
			int v = a[i - 1]; // 保存第一个父母节点的值,堆序号和数组序号相差1
			boolean heap = false; // 是否满足最大堆的标志,为true表示满足,false表示不满足
			while (!heap && 2 * k <= heapEnd) {// 当还不满足最大堆以及子女节点还在范围内时执行此循环
				// 内循环的目的是:1、保证第一次检验时此父母节点满足要求,2、若发生交换,还要求交换后的新父母节点也满足要求,即要判断是否还需要往下掉
				int j = 2 * k; // j代表第k个父母节点的子女节点,而且是较大子女的下标
				if (j < heapEnd) {// 表示第k个节点有两个子女节点
					if (a[j - 1] <= a[j + 1 - 1]) // 找到两个子女节点中的较大值
						j = j + 1;
				}
				// 到此,k号父母节点的较大子女节点为j号
				if (v >= a[j - 1]) { // 若父母节点的值大于较大子女节点,则不需要往下掉,也就不再需要再次内循环,退出内循环
					System.out.println(i + "号节点满足要求");
					heap = true;
				} else { // 父母节点往下掉,还要再次内循环,直到最底层
					System.out.println(i + "号节点不满足要求");
					a[k - 1] = a[j - 1]; // 将父母节点与较大子女节点交换位置,并将掉下来的原父母节点所在的位置设为新的父母节点,进行内循环
					k = j;
					a[j - 1] = v;
				}
			}
		}
	}

	public static void main(String[] args) {
		array = new int[10];
		System.out.println("请输入要排序的元素:");
		Scanner sc = new Scanner(System.in);
		for (int i = 0; i < 10; i++) {
			System.out.print("array[" + i + "]=");
			array[i] = sc.nextInt();
			System.out.println();
		}
		System.out.print("排序前:");
		for (int i = 0; i < array.length; i++)
			System.out.print(array[i] + " ");
		System.out.println();

		for (int i = 0; i < array.length; i++) {
			// 自底向下构造堆
			heapBu(array, array.length - 1 - i);
			int temp = array[0]; // 保存最大元素
			array[0] = array[array.length - 1 - i]; // 将堆顶元素与最后一个元素对换
			array[array.length - 1 - i] = temp;

		}

		System.out.print("排序后:");
		for (int i = 0; i < array.length; i++)
			System.out.print(array[i] + " ");
		System.out.println();
	}

}


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值