排序是为了能够提高其他算法的效率,比如查找算法。
准确的说来,排序应该属于算法一类的,但是他的实现却是基于各种数据结构的。本文主要针对这些实现排序的数据结构进行介绍。
1. 经典排序算法
从元素的存储设备上看,有内排序和外排序,按照对关键词的操作可以将其分为基于关键词比较的排序和分布排序方法,如果按照时间代价分类,我们又分平方阶排序算法和线性对数阶算法。
但无论是哪一种排序,其基本操作都是比较。
1.1 插入排序
插入排序是最简单的一种排序算法,但是数据增多之后,插入排序的算法效率就变得非常低下。
按照升序,从第二个元素(称当前元素)开始,与其之前的所有元素作比较,在比较过程中发现存在一个元素A小于当前元素B,则将当前元素B插入到A的后一个位置。
这种排序称为直接插入排序,其时间代价为O(n^2),很高,所以有人对插入排序进行了一些改进,使得其时间代价为2的对数阶O(nlogn),这种排序被称为shell排序。
shell排序主要是对排序的元素进行分组(即增量,增量值为多少就),在组内进行一次排序,直至增量值为0结束。
假设有数组A[16],增量值为8时,我们将A[0],A[8];A[1],A[9];A[2],A[10]……;A[7],A[15];分为一组,即从第一个元素开始,同组元素满足前一个元素序号+增量值,且需保证结果序号所对应的元素在排序范围内。
增量值是自己设定的,且shell算法的性能与所选长度序列有很大的关系,上述是最简单的一种分组序列n/2i,最坏情况下的时间代价与直接插入排序相同。
1.2 交换排序
交换排序中我们最应该知道的是冒泡排序,不追求性能,冒泡排序是所有排序中最容易实现的。
按照升序,某一指针从一个元素开始,向后不断的移动,移动过程中比较前后两个元素的大小,如果前一元素>后一元素,则交换,否则指针继续向后移动,直至最后一个元素,这是内层循环。外层循环则是将内层循环实现n次。
在这里我们实现另一种改进的冒泡排序:
void Bubble(int[] A, int n) //A为排序数组,n为数组元素个数
{
int bound = n; //边界值
while (bound != 0)
{
int last = 0; //记录最后一次排序的位置
for (int j = 1; j < bound - 1; j++)
{
if (A[j] > A[j + 1])
{
Swap(A[j], A[j + 1]); //假设实现了交换函数
last = j;
}
bound = last; //更改边界值
}
}
}
上述的实现函数应该才是最常见的冒泡排序实现,该实现是基于冒泡排序性质——在一趟冒泡排序后,最大的元素会上升到数组最后一个位置(升序情况)。
其实在很多的语言类库中(如Java),默认采用的是快速排序模型。想要深入了解快速排序模型的思想,可以参考《算法导论》,这里就不多加叙述了。
1.3 选择排序
选择排序和插入排序有些类似,其基本思想是对n个排序元素进行n-1此选择,将第i小的元素放在第i个位置上。
直接选择排序就是基本思想的实现,接下来我们要讲的是基于堆结构的选择排序。
1.3.1 堆排序
如果对直接选择排序进行实现,我们下意识的就会说,建立一个内外嵌套循环,内层用来找最小元素,外层用来控制主体循环次数,然后稍微懂点儿脑子,我们会说,找最小元素的事情可以单独封装成一个函数,然后建立一个循环用来进行位置的排列。
再深入一想,我们觉得这样还是很麻烦,找最小元素的函数如果只是for循环查找,那太耗时了,可以再进行简化,于是我们想到了一种比赛机制——淘汰赛,两两比赛,输家淘汰,这样一来查找最小元素的时间代价可以至少减少一半。
我们继续思考,这种机制是很好,但是我们该怎么实现它?于是我们可以想到,进行一次元素组的分划,像shell排序那样用最简单的增量值序列来进行分划,可以通过递归自动的将一个数组拆分成一个一个的小块儿,递归结束后就能得到元素的最大值,之后将其存入另一个数组,从当前数组中删除该元素。
这样一看,貌似是可行的,事实也确实如此,但是我们仔细思考后发现,时间代价确实比原来缩短了不少,但是依旧很大(深度递归一次可以得到一个最大值,但是排序需要多次深度递归),但是现在这种实现已经达到了饱和,不能继续压缩,于是我们又回到原点——淘汰赛机制。
淘汰赛机制很像是一种二叉树,但实际上又有些不同,原因是淘汰树是自底向上构造,而且有重复元素……好像不能用二叉树来实现,但是换个思考方式,我们如果直接对元素进行二叉树的构造,那也许可以得到另一种结果——初始化堆之前构造完全二叉树。
按照根结点最大,右兄弟次之,左孩子最小的顺序进行完全二叉树的构造,我们发现可以构造出来,但是他元素的值按照左孩子最小的方式摆放,似乎与我们一般的二叉树遍历顺序不一致,于是我们按照左孩子大于右兄弟的方式构造。
但是这问题又出现了,我们依旧是只能得到最大的值,最多可直接得到前三个的正确序列,貌似并没有什么改进之处啊?
继续想想,既然不能直接得到完整的序列,那么我们可以逐次的摘取树的根结点放到我们所构造的完全二叉树的末尾处(每一次摘取后末尾位置向前移动)。每一次摘取根结点之后就重新将二叉树排列,保持根结点始终是最大元素(不包括已被移动到二叉树末尾的结点元素)——此为重建堆。
堆排序的算法步骤为:
- 初始化建堆。
- 排序(循环n-1次)
- 摘取根结点到完全二叉树末尾
- 重建堆
堆排序的时间代价为线性阶。
这里不给出实现,仅给出过程图示。
2. 总结
关于排序的算法有很多,以上只是其中的三种类型,还有很多其他的排序方式,如分布排序,外排序,内排序,基于关键词的比较排序等。
其中分布排序我们俗称“桶排序”,即将需要排序的元素按照不同的方式(如遵循最低位排序,中间位排序,最高位分布)装入已划分好的不同的桶中,然后在桶内按照关键词等内容进行排序。不过由于这些内容更贴近的是算法,所以在这里不对其赘述。
桶排序中,桶可以被视为一种数据结构,用来盛装最低位相同(或是中间位相同,或是最高位相同)的数据。