1. 归并排序
归并排序是利用递归与分治技术将数据序列分成为越来越小的半子表,再对半子表排序,最后再用递归方法将排好序的半子表合并成为越来越大的有序序列。
归并排序算法的原理:
对于给定的一组记录(假设共有 n 个记录),首先将每两个相邻的长度为 1 的子序列进行归并,得到 n/2 (向上取整)个长度为 2 或 1 的有序子序列,再将其两两归并,反复执行此过程,知道得到一个有序序列。
所以,归并排序的关键是两步:第一步,划分半子表;第二步,合并半子表。以数组 {49,38,65,97,76,13,27} 为例,归并排序的具体步骤如下:
程序示例如下:
public class SortMerge {
public static void Merge(int[] array, int p, int q, int r) {
int i,j,k,n1,n2;
n1 = q - p + 1;
n2 = r-q;
int[] L = new int[n1];
int[] R = new int[n2];
for(i = 0, k = p; i < n1; i++, k++)
L[i] = array[k];
for(i = 0, k = q + 1; i < n2; i++, k++)
R[i] = array[k];
for(k = p, i = 0, j = 0; i < n1 && j < n2; k++) {
if(L[i] < R[j]) {
array[k] = L[i];
i++;
}
else {
array[k] = R[j];
j++;
}
}
if(i < n1) {
for(j = i; j < n1; j++, k++)
array[k] = L[j];
}
if(j < n2) {
for(i = j; i < n2; i++, k++)
array[k] = R[i];
}
}
public static void MergeSort(int[] array, int p, int r) {
if(p < r) {
int q = (p + r)/2;
MergeSort(array, p, q);
MergeSort(array, q+1, r);
Merge(array, p, q, r);
}
}
public static void main(String[] args) {
int i = 0;
int a[] = {5,4,9,8,7,6,0,1,3,2};
int len = a.length;
MergeSort(a, 0, len-1);
for(i = 0; i < len; i++)
System.out.print(a[i] + " ");
}
}
二路归并排序的过程需要进行 logn 趟。每一趟就是讲两个有序子序列进行归并,而每一对有序子序列归并时,记录的比较次数等于记录的移动次数,记录移动的次数均等于文件中记录的个数 n,即每一趟归并的时间复杂度为 O(n)。因此,二路归并排序的时间复杂度为 O(nlogn)。
2. 快速排序
快速排序是一种非常高效的排序算法,它采用“分而治之”的思想,把大的拆分为小的,小的在拆分为更小的。
其原理如下: 对于一字给定的记录,通过一趟排序后,将原序列分为两部分,其中前一部分的所有记录均比后一部分的所有记录小,然后再一次对前后两部分的记录进行快速排序,递归该过程,指导序列中所有记录均有序为止。
具体而言,算法步骤如下:
1) 分解。将输入的序列 array[m … n] 划成两个非孔子序列 array[m … k] 和 array[k+1 … n],使 array[m … k] 中任一元素的值不大于 array[k + 1 … n]中任一元素的值。
2)递归求解。通过递归调用快速排序算法分别对 array[m … k] 和 array[k+1 … n]进行排序。
3)合并。由于对分解出的两个子序列的排序是就地进行的,所以在 array[m … k] 和 array[k+1 … n] 都排好序后不需要执行任何计算 array[m … n]就已排好序。
以数组 {49,38,65,97,76,13,27,49}为例,整个排序过程如下所示:
程序示例如下:
public class SortQuick {
public static void sort(int[] array, int low, int high) {
int i, j;
int index;
if(low >= high)
return;
i = low;
j = high;
index = array[i];
while(i < j) {
while(i < j && array[j] >= index)
j--;
if(i < j)
array[i++] = array[j];
while(i < j && array[i] < index)
i++;
if(i < j)
array[j--] = array[i];
}
array[i] = index;
sort(array, low, i-1);
sort(array, i+1, high);
}
public static void quickSort(int[] array) {
sort(array,0,array.length-1);
}
public static void main(String[] args) {
int i = 0;
int[] a= {49,38,65,97,76,13,27,49};
quickSort(a);
for(i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
}
} /* Output:
13 27 38 49 49 65 76 97
*///~
当初始的序列整体或局部有序,快速排序的性能就会下降,此时,快速排序将退化为冒泡排序。
快速排序的相关特点如下:
1) 最坏时间复杂度。最坏情况是指每次区间划分的结果都是基准关键字的左边(右边)序列为空,而另一边区间中的记录项仅比排序前少了一项,即选择的基准关键字是待排序的所有记录中最小或最大的,例如,如果选取第一个记录为基准关键字,当初始序列按递增顺序排列时,每次选择的基准关键字都是所有记录中的最小者,这时记录与基准关键字的比较次数会增多。因此,在这种情况下,需要进行(n-1)次区间划分。对于第 k( 0<k<n )次区间划分,划分前的序列长度为(n-k+1),需要进行(n-k)次记录的比较。因此,当 k 从 1 到 (n-1)时,进行的比较次数总共为 n(n-1)/2,所以,在最坏情况下快速排序的时间复杂度为 O(n^2)。
2) 最好时间复杂度。最好情况是指每次区间划分结果都是基准关键字左右两边的序列长度相等或相差为 1,即选择的基准关键字为待排序的记录中的中间值。此时,进行比较的次数总共为 nlogn,所以,在最好情况下快速排序的时间复杂度为 O(nlogn)。
3) 平均时间复杂度。快速排序的平均时间复杂度为 O(nlogn)。虽然快速排序在最坏情况下的时间复杂度为 O(n^2),但是在所有平均时间复杂度为 O(nlogn)的算法中,快速排序的平均性能是最好的。
4) 空间复杂度。快速排序的过程中需要一个栈空间来实现递归。当每次对区间的划分都比较均匀时(即最好情况),递归树的最大深度为 ⌈logn⌉+1 (logn为向上取整);当每次区间划分都使得有一边的序列长度为 0 时(即最坏情况),递归树的最大深度为 n。在每轮排序结束后比较基准关键字左右的记录个数,对记录多的一边先进行排序,此时,栈的最大深度可降为 logn。因此,快速排序的平均空间复杂度为 O(logn)。
5) 基准关键字的选取。基准关键字的选取是决定快速排序算法性能的关键。常用基准关键字的选取方式如下:
(1)三者取中。三者取中是指在当前序列中,将其首、尾和中间位置上的记录进行比较,选择三者的中值作为基准关键字,在划分开始前交换序列中的第一个记录与基准关键字的位置。
(2)取随机数。取 left(左边)和 right(右边)之间的一个随机数 m( left⩽m⩽right ),用 n[m] 作为基准关键字。这种方法使得 n[ left ] 和 n[ right ] 之间的记录是随机分布的,采用此方法得到的快速排序一般称为随机的快速排序。
3. 归并排序和快速排序的区别
快速排序和归并排序的原理都是基于“分而治之”思想,即首先把待排序的元素分成两组,然后分别对这两组排序,最后把两组结果合并起来。
它们的区别在于,进行的分组策略不同,后面的合并策略也不同。
归并排序的分组策略是假设待排序的元素存放在数组中,那么其把数组前面一半元素作为一组,后面一半作为另一组。
快速排序是根据元素的值来分组,即大于某个值的元素放在一组,而小于的放在另外一组,该值称为基准。所以,对整个排序过程而言,基准值的挑选非常重要,如果选择不合适,太大或太小,那么所有元素都分在一组了。
总的来说,快速和归并排序,如果分组策略越简单,后面的合并策略就越复杂,因为快速排序在分组时,已经根据元素大小来分组了,而合并时,只需要把两个分组合并起来就行了,归并排序则需要对两个有序的数组根据大小合并。