接着上次的话题,今天还主要谈谈排序的问题,主要包括希尔排序和归并排序。
希尔排序
希尔排序是一种基于插入排序的快速排序算法,对于大规模乱序数组插入排序很慢,因为它只会交换相邻元素,因此元素只能一点一点从数组的一端移动到另外一端。如果主键最小的那个元素恰好位于数组的尽头,要将它移动到正确位置就需要N-1次移动。希尔排序为了加快速度简单的改进了插入排序,交换不相邻的元素以对数组进行局部排序,并最终使用插入排序将局部有序的数组排序。希尔排序的思想是是使数组中任意间隔为h的的元素都是有序的,这样的数组被称为h有序数组。可以看出希尔排序的效率跟h的取值是有关系的,而关于h值(序列)的选取跟数据规模有很大的关系,而希尔排序也正是复杂在这里,关于序列的选取这里不再赘述,后面我会列举几种应用中的常见序列。这里分析过程中将会使用Vaughan Pratt提出的(3^k-1)/2序列,{1,4,13,40}在实现算法的过程中可以提前将序列保存到一个数组中,也可以实时计算。
下面来举个例子来说明希尔排序是如何工作的。
对于数组[13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10],采用Pratt的序列,
h=13时,将数组可以进行下面的排列:
13 14 94 33 82 25 59 94 65 23 45 27 73
25 39 10
按列排序后:
13 14 10 33 82 25 59 94 65 23 45 27 73
25 39 94
h=4时:
13 14 10 33
82 25 59 94
65 23 45 27
73 25 39 94
排序后:
13 14 10 27
65 23 39 33
73 25 45 94
82 25 59 94
h=1时:
13 14 10 27 65 23 39 33 73 25 45 94 82 25 59 94
最后再将数组 [13 14 10 27 65 23 39 33 73 25 45 94 82 25 59 94]进行一次插入排序,这样就完成整个数组的排序。
关于希尔排序时间复杂度的问题,由于跟序列的选取有着很大的关系,这里不详细说明,但可以确定的是,在最坏的情况下我们所采用的序列运行过程中的比较次数跟N^(3/2) 成正比,突破了平方级别的运行时间。最后说说关于序列的问题,
已知的最好步长序列是由Sedgewick提出的(1, 5, 19, 41, 109,...),该序列的项来自 9*4^i-9*2^i+1和2^(i+2)*(2^(i+2)-3)+1(i=0,1,2,3...)这两个算式,奇数项采用第一个算式的值,偶数项采用第二个算式的值,用这样步长序列的希尔排序比插入排序要快,甚至在小数组中比快速排序和堆排序还快,但是在涉及大量数据时希尔排序还是比快速排序慢。另一个在大数组中表现优异的步长序列是(斐波那契数列除去0和1将剩余的数以黄金分区比的两倍的幂进行运算得到的数列):(1, 9, 34, 182, 836, 4025, 19001, 90358, 428481, 2034035, 9651787, 45806244, 217378076, 1031612713,…)。
归并排序
1.自顶向下的归并排序(递归实现)
归并排序就是将一个数组(递归的)分为两部分,对每部分分别排序然后将两个排好序的子序列合并就可以完成整个数组的排序。将两个子序列合并的过程我们称之为 merge,对于merge方法中一种常用的操作就是利用一个辅助数组,先将原序列的全部元素拷贝到辅助数组中,然后再进行合并的操作,后面我将会放上merge方法的代码,其 中的注释会详细介绍每一步的操作。
原理如下(假设序列共有个元素):
- 将序列每相邻两个数字进行归并操作,形成个序列,排序后每个序列包含两/一个元素
- 若此时序列数不是1个则将上述序列再次归并,形成个序列,每个序列包含四/三个元素
- 重复步骤2,直到所有元素排序完毕,即序列数为1
2.自底向上的归并排序(非递归实现)
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 重复步骤3直到某一指针到达序列尾
- 将另一序列剩下的所有元素直接复制到合并序列尾
template<typename T>
void shell_sort(T array[], int length) {
int h = 1;
while (h < length / 3) {
h = 3 * h + 1;
}
while (h >= 1) {
for (int i = h; i < length; i++) {
for (int j = i; j >= h && array[j] < array[j - h]; j -= h) {
std::swap(array[j], array[j - h]);
}
}
h = h / 3;
}
}
/**
*
* @tparam T
* @param array 待排序数组
* @param aux 辅助数组
* @param lo 最小位置
* @param mid 中间位置
* @param hi 结束为止
*/
template <typename T>
void merge(T array[],T aux[], int lo, int mid, int hi) {
int i = lo;
int j = mid + 1;
for (int k = lo; k <= hi; ++k) { //将array[lo...hi]拷贝到辅助数组aux中
aux[k] = array[k];
}
for (int k = lo; k <= hi; ++k) {
if (i > mid) { //如果左边取尽,就取右边元素
array[k] = aux[j++];
} else if (j > hi) { //如果右边元素取尽,就取左边元素
array[k] = aux[i++];
} else if (aux[j] < aux[i]) { //如果右边元素小于左边元素,取右边元素
array[k] = aux[j++];
} else {
array[k] = aux[i++]; //如果左边元素小于等于右边元素,取左边元素
}
}
}
template <typename T> //主程序
void merge_sort(T array[],int length){
T *aux;
aux=new T[length];
sort_(array,aux,0,length-1);
delete aux;
}
template <typename T> //递归程序
void sort_(T array[],T aux[],int lo,int hi){
if(hi<=lo) return;
int mid=lo+(hi-lo)/2;
sort_(array,aux,lo,mid); //左序列排序
sort_(array,aux,mid+1,hi); //右序列排序
merge(array,aux,lo,mid,hi); //合并
}
template <typename T>
void merge_sort2(T array[],int length){
T *aux;
aux=new T[length];
for (int i = 1; i <length ; i+=i) {
for (int lo = 0; lo <length-i ; lo+=i+i) {
merge(array,aux,lo,lo+i-1,std::min(lo+i+i-1,length-1)); //min方法使用标准库的就行
}
}
delete []aux;
}