程序员修炼之路-(3)排序(上):基本排序
1 基本排序
对于所有排序算法,被排序元素需要满足下列数学性质:
Ø 自反性(reflextive):for all v,v=v
Ø 对称性(antisymmetric):for all v and w,if v<w then w>v and if v=w then w=v
Ø 传递性(transitive):for all v,w and x,if v<=w and w<=x then v<=x
对于包含这样元素的数组,我们才能对其排序。
1.1 选择排序(selection sort)
思路与实现
选择排序的思路很简单:数组的前半部分是排好序的,那么之后不断从后半部分选择出最小元素,交换到后半部分的第一个位置上。
实现思路很清晰,要注意的是要用数组下标记录min的位置,然后才能swap,避免低级错误…
选择排序的特点
1)输入不敏感:在任何输入上的性能总是n平方,不存在最好、平均、最坏情况之分。
2)数据移动最小化:key的swap次数仅为n-1。这也是在众多排序算法中,选择排序的与众不同之处。
1.2 冒泡排序(bubble sort)
思路与实现
冒泡排序不断交换两个元素的位置,将最大元素“冒泡”到数组末尾。
基本排序的思想很简单,所以实现时一般循环体比较好写,难的是循环条件。例如,冒泡排序的inner loop写成j<i-1,没想好是<还是<=,结果造成最后一个元素永远不会被排序。
冒泡排序与选择排序类似,在任何输入上性能都一样。但是swap次数却与输入相关。
1.3 插入排序(insertion sort)
思路与实现
与选择排序很类似,插入排序也将数组看成前后两部分。但是插入排序是将后半部分第一个元素,插入到它应在的位置上。生活中最典型的例子就是玩牌时排序手中的牌,但编程实现中,我们要挪动数组中的元素才能将后面的牌插入到它应在的位置。
实现时想到了两种做法:
1) 第一种是比较后将A[i]向前swap,然后inner loop下一次迭代继续比较A[i]与前一个元素,后面能看到JDK 6中就是采用这种实现方式,但能够证明这种方式inner loop的body中操作更多,整体上比第二种方式要慢(《算法设计与分析基础》插入排序的一道练习题)。
2) 另一种则较为标准,将A[i]与前面元素逐个比较找到其位置,比较过程中将这些大于A[i]的元素后移。注意循环终止条件对后面代码的推导作用。
插入排序的特点
在已经或几乎排好序的数组上,运行的非常快。因为每一轮比较都会立即或很快结束。怎么定义几乎排好序?这里引入逆序对(inversion)的概念,指与最终顺序相反的两个元素。当逆序对少于数组长度的常数倍时就认为数组是部分排序的(partially sorted)。当逆序对很少时,插入排序要快于任何其他排序算法(选择、冒泡、归并、快速排序等)。同时,插入排序对小数组也是个不错的排序算法,这也是为什么JDK 6的Arrays.sort()中对长度小于7的原始类型的小数组用插入排序,而大数组时用快速排序(JDK 7中已升级为DualPivotQuicksort)。
以下是总结的插入排序的几种典型应用情况:
1) 每个元素都离最终位置不远。
2) 只有少数元素不在最终位置。
3) 小数组拼接到一个已排序的大数组后面。
4) 很小的数组。