3、内部排序-选择排序

    选择排序

 

    简单选择排序

    简单选择排序的基本思想很简单,以排序从小到大为例,如下:

    1、从第一个元素开始,选出一个最小的元素与第一个元素互换;

    2、继续从第二个元素开始,向后选出最小的元素,与第二个元素互换;

    3、依次循环执行,直到最大的元素放在了最后一个位置上,排序完成。

    可以将第一个元素分别与后面的元素进行比较,遇到更小的,就交换,这样一趟比较下来,第一个元素保存就是最小值,而后再从第二个元素开始,依次与后面的元素比较,遇到更小的,就交换,这样,第二趟比较下来,第二个元素保存的就是第二小的值……依次循环执行,直到完成排序。按照这样的思路,实现代码如下:

 

	public static int[] simpleSelectSort(int array[]) {
		int i, j, len = array.length;
		for (i = 0; i < len; i++) {
			for (j = i + 1; j < len; j++) {
				if (array[i] > array[j]) {
					int temp = array[i];
					array[i] = array[j];
					array[j] = temp;
				}
			}
		}
		return array;
	}

 

    但是在排序中应该尽量避免较多的元素互换操作,而这里每比较一次,如果遇到更小的,就要互换一次。为了减少元素互换操作,我们可以在每次比较后不直接进行交换,而是将较小的元素的位置序号记录下来,这样一趟比较之后,就会得到最小元素的位置,如果最小值的位置发生了改变,再将该位置的元素与第一个元素互换,依次类推……这样每一趟比较完成后最多只需执行一次元素互换操作。改进后的实现代码如下:

 

 

	/*
	 * 第二种形式的选择排序,减少了元素的互换操作
	 * 选择排序后的顺序为从小到大
	 */
	public static int[] simpleSelectSort2(int[] array) {
		int i, j, len = array.length;
		for (i = 0; i < len; i++) {
			int min = i; // 用来记录每一趟比较的最小值的位置
			for (j = i + 1; j < len; j++)
				if (array[min] > array[j])
					min = j; // 仅记录最小值的位置
			// 如果最小值的位置发生了变化,
			// 则最后执行一次元素互换的操作
			if (min != i) {
				int temp = array[i];
				array[i] = array[min];
				array[min] = temp;
			}
		}
		return array;
	}

 

    PS:简单选择排序的平均时间复杂度都为O(n^2),排序元素个数较少时,适合使用,遇到大数据量时,最好选用其他排序算法。

 

 

 

    堆排序

    要弄清楚堆排序,就要先了解下二叉堆这种数据结构。

    二叉堆其实是一棵有着特殊性质的完全二叉树,这里的特殊性质是指:

    1、二叉堆的父节点的值总是大于等于(或小于等于)其左右孩子的值;

    2、每个节点的左右子树都是一棵这样的二叉堆。

    如果一个二叉堆的父节点的值总是大于其左右孩子的值,那么该二叉堆为最大堆,反之为最小堆。我们在排序时,如果要排序后的顺序为从小到大,则需选择最大堆,反之,选择最小堆。

    堆排序的基本思想如下:由二叉堆的定义可知,堆顶元素(即二叉堆的根节点)一定为堆中的最大值或最小值,因此如果我们输出堆顶元素后,将剩余的元素再调整为二叉堆,继而再次输出堆顶元素,再将剩余的元素调整为二叉堆,反复执行该过程,这样便可输出一个有序序列,这个过程就是堆排序。

   实现过程如下:由于我们的输入是一个无序序列,因此要实现堆排序,我们要先后解决如下两个问题:

 

    1、如何将一个无序序列建成一个二叉堆;(建堆

    2、在去掉堆顶元素后,如何将剩余的元素调整为一个二叉堆。(调整

    针对第一个问题,可能很明显会想到用堆的插入操作,一个一个地插入元素,每次插入后调整元素的位置,使新的序列依然为二叉堆。这种操作一般是自底向上的调整操作,即先将待插入元素放在二叉堆后面,而后逐渐向上将其与父节点比较,进而调整位置。但我们完全用不着一个节点一个节点地插入,那我们要怎么做呢?我们需要先来解决第二个问题,解决了第二个问题,第一个问题问题也就迎刃而解了。

    调整二叉堆,要分析第二个问题,我们先给出以下前提:

 

    1、我们排序的目标是从小到大,因此我们用最大堆;

    2、我们将二叉堆中的元素以层序遍历后的顺序保存在一维数组中,根节点在数组中的位置序号为0。

    这样,如果某个节点在数组中的位置序号为i,那么它的左右孩子的位置序号分别为2i+1和2i+2。为了使调整过程更易于理解,我们采用如下二叉堆来分析(注意下面的分析,我们并没有采用额外的数组来存储每次去掉的堆顶数据):

    调整的实现代码如下:

 

	/*
	 * arr[start+1,...,end]满足最大堆的定义, 将arr[start]加入到最大堆arr[start+1,...,end]中,调整arr[start]的位置,使arr[start,...,end]也成为最大堆
	 * 注:由于数组从0开始计算序号,也就是二叉堆的根节点序号为0,因此序号为i的左右子节点的序号分别为2i+1和2i+2
	 */
	public static void heapAdjust(int[] arr, int start, int end) {
		int temp = arr[start]; // 保存当前节点
		int i = 2 * start + 1; // 该节点的左孩子在数组中的位置序号
		while (i <= end) {
			// 找出左右孩子中最大的那个
			if (i + 1 <= end && arr[i + 1] > arr[i])
				i++;
			// 如果符合堆的定义,则不用调整位置
			if (arr[i] <= temp)
				break;
			// 最大的子节点向上移动,替换掉其父节点
			arr[start] = arr[i];
			start = i;
			i = 2 * start + 1;
		}
		arr[start] = temp;
	}

 

    建堆并排序

    按数组顺序建立一棵完全二叉树,但此时需要调整为二叉堆,其实叶子节点可以认为是一个堆(因为堆的定义中并没有对左右孩子间的关系有任何要求,所以可以将这几个叶子节点看做是一个堆),而后需考虑将第一个非叶子节点插入到这个堆中,再次构成一个堆,接着再将第二个非叶子结点插入到新的堆中,再次构成新堆,如此继续,直到该二叉树的根节点也插入到了该堆中,此时构成的堆便是由该数组建成的二叉堆。因此,我们这里同样可以利用到上面所写的heapAdjust(int[],int,int)函数,因此建堆并排序的实现代码可写成如下的形式:

 

	/*
	 * 堆排序后的顺序为从小到大,因此需要建立最大堆
	 */
	public static int[] heapSort(int[] arr) {
		int i, len = arr.length;
		// 把数组建成为最大堆,第一个非叶子节点的位置序号为len/2-1
		for (i = len / 2 - 1; i >= 0; i--)
			heapAdjust(arr, i, len - 1);
		// 进行堆排序
		for (i = len - 1; i > 0; i--) {
			// 堆顶元素和最后一个元素交换位置,这样最后的一个位置保存的是最大的数
			// 每次循环依次将次大的数值在放进其前面一个位置,这样得到的顺序就是从小到大
			int temp = arr[i];
			arr[i] = arr[0];
			arr[0] = temp;
			// 将arr[0,...,i-1]重新调整为最大堆
			heapAdjust(arr, 0, i - 1);
		}
		return arr;
	}

    最后贴出完整源码

 

public class HeapSort {

	/*
	 * arr[start+1,...,end]满足最大堆的定义, 将arr[start]加入到最大堆arr[start+1,...,end]中,调整arr[start]的位置,使arr[start,...,end]也成为最大堆
	 * 注:由于数组从0开始计算序号,也就是二叉堆的根节点序号为0,因此序号为i的左右子节点的序号分别为2i+1和2i+2
	 */
	public static void heapAdjust(int[] arr, int start, int end) {
		int temp = arr[start]; // 保存当前节点
		int i = 2 * start + 1; // 该节点的左孩子在数组中的位置序号
		while (i <= end) {
			// 找出左右孩子中最大的那个
			if (i + 1 <= end && arr[i + 1] > arr[i])
				i++;
			// 如果符合堆的定义,则不用调整位置
			if (arr[i] <= temp)
				break;
			// 最大的子节点向上移动,替换掉其父节点
			arr[start] = arr[i];
			start = i;
			i = 2 * start + 1;
		}
		arr[start] = temp;
	}

	/*
	 * 堆排序后的顺序为从小到大,因此需要建立最大堆
	 */
	public static int[] heapSort(int[] arr) {
		int i, len = arr.length;
		// 把数组建成为最大堆,第一个非叶子节点的位置序号为len/2-1
		for (i = len / 2 - 1; i >= 0; i--)
			heapAdjust(arr, i, len - 1);
		// 进行堆排序
		for (i = len - 1; i > 0; i--) {
			// 堆顶元素和最后一个元素交换位置,这样最后的一个位置保存的是最大的数
			// 每次循环依次将次大的数值在放进其前面一个位置,这样得到的顺序就是从小到大
			int temp = arr[i];
			arr[i] = arr[0];
			arr[0] = temp;
			// 将arr[0,...,i-1]重新调整为最大堆
			heapAdjust(arr, 0, i - 1);
		}
		return arr;
	}

	public static void print(int[] array) {
		for (int i = 0; i < array.length; i++) {
			System.out.print(array[i] + " ");
		}
	}

	public static void main(String args[]) {
		int[] arr = { 3, 1, 5, 4, 9, 8, 15, 18, 2 };
		print(HeapSort.heapSort(arr));
	}

}

 

 

 

总结

 

 

    在每次重新调整堆时,都要将父节点与孩子节点比较,这样,每次重新调整堆的时间复杂度变为O(logn),而堆排序时有n-1次重新调整堆的操作,建堆时有((len-1)/2+1)次重新调整堆的操作,因此堆排序的平均时间复杂度为O(n*logn)。由于我们这里没有借用辅助存储空间,因此空间复杂度为O(1)。

    堆排序在排序元素较少时有点大才小用,待排序列元素较多时,堆排序还是很有效的。另外,堆排序在最坏情况下,时间复杂度也为O(n*logn)。相对于快速排序(平均时间复杂度为O(n*logn),最坏情况下为O(n*n)),这是堆排序的最大优点。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值