相关文章:
选择排序(Selection Sort)
选择排序分为三种,直接选择排序、树形选择排序(锦标赛排序)、堆排序(大根堆、小根堆)。直接选择排序和堆排序是不稳定排序,树形选择排序是稳定排序。
直接选择排序
通过设置哨位,将哨位位置的数与哨位之后(包括哨位)的序列中最小的数进行交换,然后哨位递增,直到哨位到达数组中最后一个数为止。
基本思路:
1、设置哨位为i,此时i = 0,,也就是数组的第一个数,然后遍历数组中下标为[i, n – 1](n为数组长度)的数,取最小的数和哨位位置的数互换。
2、哨位递增,遍历数组中下标为[i, n – 1](n为数组长度)的数,取最小的数和哨位位置的数互换。
3、重复第二步,直至i = n – 1为止,此时数组已经有序,排序结束
直接选择排序的java实现:
int pos = 0;
for (int i = 0; i < data.length; i++) {
pos = i;
// 找出从i开始,到数组末尾这段数组中最小的数,pos标志的是这个最小的数在数组中的位置
for (int j = i + 1; j < data.length; j++) {
if (data[j] < data[pos]) {
pos = j;
}
}
swap(i, pos); // 交换两个数的位置
}
直接选择排序的最好时间复杂度和最差时间复杂度都是O(n²),因为即使数组一开始就是正序的,也需要将两重循环进行完,平均时间复杂度也是O(n²)。空间复杂度为O(1),因为不占用多余的空间。直接选择排序是一种原地排序(In-place sort)并且稳定(stable sort)的排序算法,优点是实现简单,占用空间小,缺点是效率低,时间复杂度高,对于大规模的数据耗时长。
树形选择排序(锦标赛排序)
树形选择排序利用满二叉树的性质,将待排序的数放入叶子节点中,然后同属于一个根节点的两个叶子节点相互比较,较小的叶子节点复制到其根节点,然后根节点之间再相互比较,直到整棵树的根节点,此时整棵树的根节点为待排序数组中最小的一个数,在下一次循环中要将这个数置为最大值,然后再开始循环,直到全部的数都被取出,排序完成。因为这种排序类似于比赛中的淘汰赛,所以又称之为锦标赛排序。
基本思路:
1、构造一棵满二叉树,要求可以将待排序数组全部放入叶子节点中
2、将两个叶子节点中较小的数挪入根节点中,全部挪完之后,再将两个根节点中较小的数挪入它们的根节点中,直到整棵树的根节点。
3、取出根节点中的数,将叶子节点中的这个数置为max,重复第二步,直到每个数都被取出过一次。
树形选择排序的java实现:
int depth = 0;
int nodes = 0;
// 计算出装下待排序数组所需的二叉树的深度
for (; Math.pow(2, depth) < data.length; depth++);
// 计算树的总结点数
for (int i = (int) Math.pow(2, depth); i > 0; i = i / 2) {
nodes = nodes + i;
}
// 根据的到的树的总结点数创建数组
int[] tree = new int[nodes];
// 初始化树中结点的值
Arrays.fill(tree, Integer.MAX_VALUE);
// 开始存放待排序数组的树的位置
int beginPos = nodes - (int) Math.pow(2, depth);
// 将待排序数组中的数复制到树的叶子节点中
for (int i = 0, j = beginPos; i < data.length; i++, j++) {
tree[j] = data[i];
}
int loopBeginPos;
int loopBound;
int count;
// 取数的次数为待排序数组的长度
for (int i = 0; i < data.length; i++) {
// loopBeginPos为开始比较的位置,loopBound为结束比较的位置,
// 下一次开始比较的位置为上一次开始比较的位置的父节点,
// 当下一次开始比较的位置为0的时候,说明比较已经到了最顶层,可以结束这一次的比较了
for (loopBeginPos = beginPos, loopBound = tree.length - 1; loopBeginPos > 0;
loopBeginPos = getParentNode(loopBeginPos)) {
// 每两个节点进行一次比较,将较小的节点赋值给他们的父节点
for (count = loopBeginPos; count + 1 <= loopBound; count = count + 2) {
tree[getParentNode(count)] = tree[count] < tree[count + 1] ? tree[count] : tree[count + 1];
}
// 下一次比较的结束位置,为这次比较结束节点的父节点
loopBound = getParentNode(count - 2);
}
// 将这次比较得到的最小的数取出
data[i] = tree[0];
// 将这次比较得到的最小的数原先存放的位置存放的数赋值为max
for (int j = beginPos; j < tree.length; j++) {
if (tree[j] == tree[0]) {
tree[j] = Integer.MAX_VALUE;
break;
}
}
}
/**
* 获取指定节点的父节点
* @param node
* @return
*/
private int getParentNode(int node) {
return (node - 1) / 2;
}
由于每次选出最小值只需要做log2n次比较,所以时间复杂度为O(nlogn)。该算法需要的辅助空间较多,由于必须要构成一棵满二叉树,因此叶子节点数是大于待排序数组长度的最小的2的幂,假设是2x,那么需要的多余空间是2x – n + 2x – 1,空间复杂度为O(n)(自己算的,不确定对不对)。该方法的优点是时间复杂度低,但是实现复杂,占用空间多,与max的比较次数多,浪费时间。
堆排序(Heap Sort)
堆排序是对树形选择排序的改进,分为大根堆和小根堆,也是构造一棵二叉树。大根堆的树的根节点会大于等于两个子节点,小根堆的树的根节点会小于等于两个子节点。
以大根堆为例,在构造好一个大根堆之后,堆顶的数就是整个待排序序列的最大值,把这个数与数组最后一个数进行交换,然后调整堆,使堆重新成为一个大根堆,再将堆顶的数与数组的倒数第二个数进行交换,循环进行至堆顶和要交换的位置重合,结束排序。
大根堆基本思路:
1、将待排序数组构造成一个大根堆
2、设置交换位置pos = n – 1,n为数组长度,然后将堆顶的数和交换位置的数交换,由于交换使堆不是大根堆了,所以调整堆重新成为大根堆,pos递减
3、重复第二步,直到pos = 0;
其中核心部分是如何建堆和如何调整堆。因为建堆其实也是调用调整堆的方法,所以先说如何调整堆。
调整堆的方法接收两个参数,一个是调整的范围,一个是从哪个节点开始调整。开始调整的节点下面的二叉树必须是已经调整好的堆。假如从顶点开始调整,首先获取顶点的左节点,判断如果这个左节点在调整的范围内并且比父节点大,那么标志最大值为左节点。注意,这里只是标志,而不是发生实际的交换,因为还有右节点的情况没有考虑。然后看右节点是否比标志的最大值大并且在调整的范围内,如果是,那么交换父节点和右节点,如果不是,并且左节点比父节点大,那么交换左节点和父节点。如果左右节点都没有比父节点大,说明此时堆满足大根堆的要求,无需再向下调整。如果发生了交换,那么以被交换的子节点为待调整节点,范围不变,进行下一次调整堆的操作。
建堆的时候是自底向上调整堆,从最后一个节点开始调整,范围为待排序数组的长度。之所以是自底向上,是因为调整堆的前提是待调整节点下面的二叉树都是堆,自底向上建堆才能满足这个要求。
从上面的描述可知,大根堆排序得到的序列是升序排列。
大根堆排序java实现:
// 初始化数组,使数组成为大根堆
buildMaxHeapify();
// 将第一个数和最后一个数交换,然后使除最后一个数之外的数组成为大根堆
for (int i = data.length - 1; i > 0; i--) {
swap(0, i);
maxHeapify(i - 1, 0);
}
/**
* 初始化建立大根堆
*/
private void buildMaxHeapify() {
// 获取从后往前第一个父节点
int startIndex = getParentNode(data.length - 1);
for (int i = startIndex; i >= 0; i--) {
maxHeapify(data.length - 1, i);
}
}
/**
* 调整大根堆
* @param size 调整的深度
* @param index 从该节点开始调整
*/
private void maxHeapify(final int size, final int index) {
int left = getLeftNode(index); // 获取左节点
int right = getRightNode(index); // 获取右节点
int parentNode = index;
// 如果左节点在数组范围之内,并且左节点上的值比父节点上的大,那么使父节点为左节点(标志,但未交换)
if (left <= size && data[left] > data[parentNode]) {
parentNode = left;
}
// 如果右节点在数组范围之内,并且右节点上的值比标志节点上的大,那么使父节点为右节点(标志,但未交换)
if (right <= size && data[right] > data[parentNode]) {
parentNode = right;
}
// 如果父节点有改变,那么交换两个值,然后调整以原来的子节点为父节点的堆
if (parentNode != index) {
swap(index, parentNode);
maxHeapify(size, parentNode);
}
}
/**
* 获取指定节点的父节点
* @param node
* @return
*/
private int getParentNode(int node) {
return (node - 1) / 2;
}
/**
* 获取指定节点的左子节点
* @param parentNode
* @return
*/
private int getLeftNode(int parentNode) {
return parentNode * 2 + 1;
}
/**
* 获取指定节点的右子节点
* @param parentNode
* @return
*/
private int getRightNode(int parentNode) {
return parentNode * 2 + 2;
}
堆排序的最好和最差情况时间复杂度都为O(nlogn),平均时间复杂度也为O(nlogn),空间复杂度为O(1),无需使用多余的空间帮助排序。优点是占用空间小,时间复杂度低,达到了基于比较的排序的最低时间复杂度,缺点是实现较为复杂,并且当待排序序列发生改动时,哪怕是小改动,都需要调整整个堆来维护堆的性质,维护开销大。
三种选择排序的总结
直接选择排序是最简单的选择排序,但是时间复杂度高。树形选择排序虽然时间复杂度低,但是实现复杂,辅助空间多,无谓的比较次数多,是一种在实际运用中比较少见的排序算法。堆排序是改进版的锦标赛排序,是一种比较优异的排序算法,时间复杂度达到比较排序算法的最低值,无需额外的空间,但是维护开销大,因此在实际运用中也很少见到它。
本文所使用的java代码已上传至github,为java project:https://github.com/sysukehan/SortAlgorithm.git