选择排序:直接选择排序&堆排序
上一篇中, 介绍了交换排序中的冒泡排序和快速排序, 那么这一篇就来介绍一下 选择排序和堆排序, 以及他们与快速排序的比较.
一、直接选择排序
1. 思想
在描述直接选择排序思想之前, 先来一个假设吧.(先不管这个假设是什么思想的排序啊)
假设我有两个集合, 一个是待排序集合, 一个是空集合. 现在通过这个空的集合来完成待排序集合的排序.
第一步: 我可以遍历listA集合, 找到其中最大的数num, 然后把这个num插入到listB中.
listB.Insert(num);
listA.Remove(num);
第二步: 重复第一步的操作, 一直到消耗完 listA 集合中的数.
这样的话, 数据就逐渐从listA有序的流向了listB. 最后来看listB, 就是一个有序的集合.
代码如下:
static List<int> InsertSort(List<int> listA) { if (listA == null) return null; List<int> listB = new List<int>(); int maxNum = 0; int ptr = 0; int i = 0; while (listA.Count > 0) { for (i = 0, ptr = 0, maxNum = listA[0]; i < listA.Count; i++) { if (listA[i] < maxNum) { maxNum = listA[i]; ptr = i; } } listB.Add(maxNum); listA.RemoveAt(ptr); }
listA = listB; return listA; }
直接选择排序的思想, 与这里的思想是一样的, 只不过, 不需要使用中间集合来排序, 直接就在自己集合里面做交换就可以了.
接下来, 就来简化上面的过程, 可以得到直接选择排序版本:
static List<int> SelectionSort(List<int> list) { int count = list.Count; //要遍历的次数 for (int i = 0; i < count - 1; i++) { //假设tempIndex的下标的值最小 int tempIndex = i; for (int j = i + 1; j < count; j++) { //如果tempIndex下标的值大于j下标的值,则记录较小值下标j if (list[tempIndex] > list[j]) tempIndex = j; } //最后将假想最小值跟真的最小值进行交换 var tempData = list[tempIndex]; list[tempIndex] = list[i]; list[i] = tempData; } return list; }
2. 复杂度
选择排序的时间复杂度是O(n2), 乍一看, 怎么和冒泡排序的复杂度是一样的?
从上面的代码来看, 选择排序的循环次数是这样的:
(n-1) + (n-2) + (n-3) + ... + 2 + 1
结果为 : n2/2
所以, 选择排序的时间复杂度也是O(n2)
不过, 由于选择排序需要进行的交换操作很少, 最多的时候, 只会发生n-1次交换,
冒泡排序则是两两交换, 最多的时候, 会发生 n2/2次交换. 所以, 选择排序的性能还是要由于冒泡排序的.
这也是上一篇的, 我会问, 为什么每次都要交换, 而不能找到最大/最小的值, 才进行交换操作.
二、堆排序
1. 思想
1). 前奏
相信很多人都喜欢看篮球比赛, 尤其是nba季候赛, 那么先来看一下, 季后赛是怎么晋级的.
nba季后赛就是这种逐级递进的方式, 来决定最后的胜者. 在排序中, 我能否借鉴这种形式来排序呢?
1. 看左下角的三个方块, 有两个是4级的, 一个是3级的, 假设每个方块里面都放有一个数, 大小各不同, 那么我是否可以比较这三个数, 然后把其中最大的数, 放到第3级中呢?
2. 如果每三个方块都可以这样比较, 那么是否会决出一个最大的值, 放入第1级的这个唯一的一个方块中呢?
这里面还有一个问题, 如果只有两个方块怎么办呢? 那就只能一个在上, 一个在下, 而不能是两个在下, 因为需要有一个晋级.
2) 二叉堆
以上的这张图, 看起来是不是有点熟悉, 感觉有点像二叉树结构. 在这里, 是二叉堆.
既然要把一组数据映射成为一个二叉堆, 那么就要明白, 这组数据是怎么存储和映射的.
从上到下, 从左到右的一个顺序来映射. 从图上看, 每一级对应的下标范围为 : 2n-1 ~ 2(2n-1)
接下来就对他进行一轮堆排序: (只需要对父节点进行判断就可以了)
这里的比较顺序: 父节点的比较顺序是从下往上, 从有右往左. 且与父节点交换的这个节点还需要与后代节点比较. 以确定他的位置
Demo:
一个父节点的比较, 代码如下:
static void HeapAdjust(List<int> list, int parent, int length) { //temp保存当前父节点 int temp = list[parent]; //得到左孩子的下标(这可是二叉树的定义,大家看图也可知道) int child = 2 * parent + 1; while (child < length) { //如果parent有右孩子,则要判断左孩子是否小于右孩子 if (child + 1 < length && list[child] < list[child + 1]) child++; //此时的child指向右孩子 //父亲节点大于子节点,就不用做交换 if (temp >= list[child]) //这里能这么写, 就是因为temp节点下的所有节点, 都已经拍过序了, 保证了父节点是子节点中最大的 break; //将较大子节点的值赋给父亲节点 //这里相当于向前覆盖, 因为temp已经取出来了, 所以相当于有一个坑可以填数 list[parent] = list[child]; //下面就是为下一个循环做准备 //也就是说, 让parent指向temp的其中一个大的子节点 //然后将该子节点做为下一个父节点 parent = child; //找到该新父节点的左孩子节点 child = 2 * parent + 1; } //最后将temp值赋给较大的子节点,以形成两值交换 list[parent] = temp; }
接下来就是堆排序的代码了
///<summary> /// 堆排序 ///</summary> ///<param name="list"></param> public static void HeapSort(List<int> list) { //list.Count/2-1 就是最后一个父节点的下标, 所有的父节点都是往前放的 for (int i = list.Count / 2 - 1; i >= 0; i--) { HeapAdjust(list, i, list.Count); } //最后输出堆元素 for (int i = list.Count - 1; i > 0; i--) { //堆顶与当前堆的最后一个元素进行值对调 int temp = list[0]; list[0] = list[i]; list[i] = temp; //重新塑造堆, 因为堆的最后一个元素会被剔除出去 //剔除出去的数据, 就是有序的数据 HeapAdjust(list, 0, i); } }
这里的思想, 就是不断地构建堆, 得到最大的值, list[0], 然后将最大的值和堆的最后一个值交换, 剔除这个最大值, 重新塑造堆, 此时, 堆已经少了一个值了.
也就是说, 刚开始有10个数进行堆排序, 一轮之后, 最大的值剔除出去, 再进行堆排序的时候, 就只有9个值了.
2. 复杂度
堆排序的时间复杂度为O(nlog2n), 从复杂度上来看, 好像和快速排序是一样的, 那么事实上是不是这样呢.
其证明过程见下面参考链接
3. 堆排序 vs 快速排序
从上面来看, 在2000数据量下, 堆排序还是要慢与快速排序的, 难怪微软会使用快速排序呢.
那么堆排序是否一无是处呢?
快速排序并不能在排序没有完成的情况下, 取出其中最大/最小的一些数, 但是选择排序和堆排序擅长这个, 可以再排序还没有完成的情况下, 取出其中最大/最小的一些数据.
参考: