选择排序
简单选择排序
简单选择排序的基本思想很简单,以排序从小到大为例,如下:
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)),这是堆排序的最大优点。