常见排序算法(二)(选择排序)

相关文章:

常见排序算法(零)(各类排序算法总结与比较)

常见排序算法(一)(冒泡排序、插入排序)

常见排序算法(二)(选择排序)

常见排序算法(三)(快速排序、归并排序、计数排序)

常见排序算法(四)(基数排序、桶排序)


选择排序(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



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值