前言
一般的基本排序方法有冒泡排序,插入排序,选择排序等等,但是这些方法需要的时间复杂度最低也是O(n^2)级别,在我们需要对大数量级的数据处理时,这样的时间复杂度显然不能满足我们对时间上的需求,这里我们简单介绍两种时间复杂度较低的排序方法:归并排序和快速排序。
一、归并排序
归并排序(合并排序):基于分治思想实现对n个元素排序的算法。
基本思想:将n个元素分成两个大小大致相同的两个集合,分别对两个集合排序(这个过程在递归中进行了多次,最后每个子集合的元素个数都为1),然后将排序好的两个子集合合并成一个要求排序集合(最后合并的集合就是按要求排序的集合)。
伪代码如下:
void MergeSort(int a[], int left, int right) {
if (left < right) { // 至少有两个元素
int mid = (left + right) / 2; // 取中点
MergeSort(a, left, mid);
MergeSort(a, mid + 1, right); // 分别对子集合排序
Merge(a, b, left, mid, right);// 合并到数组b
copy(a, b, left, right); // 复制回数组a
}
}
通过分析我们可以发现将数据暂存如b数组后者在复制回数组a的时间复杂度都为O(n),通过递归的时间复杂度为O(logn),所以总时间复杂度为O(nlogn),是一个渐进最优算法。
对于MergeSort算法我们还可以从多方面对他进行改进。例如,我们可以从分治策略的机制入手,消除算法之中的递归:把递归变成a中的相邻的元素两两配对(在上述过程为递归的最底层),分别排序后合并在b数组中,在复制回a数组,然后是四个为一个子集合,然后八个……直到完成排序。
下面给出代码:
void MergeSort(int a[], int n) {
int s = 1;
while (s < n) {
MergePass(a, b, s, n);//合并到数组b
s += s;
MergePass(b, a, s, n);//合并到数组a
s += s;
}
}
void MergePass(int x[], int y[], int s, int n) { // 合并大小为s的相邻子数组
int i = 0;
while (i <= n - s * 2) {
Merge(x, y, i, i + s - 1, i + s * 2 - 1);
i += s * 2;
}
if (i + s < n)// 剩下的元素小于2*s分为两种情况
Merge(x, y, i, i + s - 1, n - 1);
else
for (int j = i;j <= n - 1;j++)
y[j] = x[j];
}
void Merge(int c[], int d[], int l, int m, int r) {//合并c[l:m] 和 c[m+1:r] 到 d[l:r]
int i = l, j = m + 1, k = l;
while (i <= m && j <= r) {
if (c[i] <= c[j])
d[k++] = c[i++];
else
d[k++] = c[j++];
}
if (i > m)
for (int q = j;q <= r;q++)
d[k++] = c[q];
else
for (int q = i;q <= m;q++)
d[k++] = c[q];
}
这里默认的是将数组从小到大排序。
二、快速排序
快速排序是基于分治思想的另一种排序算法。其基本思想分为三步:
1:选定基准(也就是一个元素)a[p]:该元素将数组a[p:r]分为三段:a[p:q-1],a[q],a[q+1:r]。
其中a[p:q-1]之中的任何一个元素小于等于a[q],a[q+1:r]之中的任何一个元素大于等于a[q]。q再划分过程中确定。
2:递归求解:分别对a[p:q-1] , a[q+1,r] 递归求解。
3:合并:由于上述递归排序过程是就地进行的所以不需要任何计算a[p:r]就已经排好序了。
下面给出代码:
void quicksort(int a[], int p, int r) {
if (p < r) {
int q = Partation(a, p, r);
quicksort(a, p, q - 1);//左半段排序
quicksort(a, q + 1, r);//右半段排序
}
}
int Partation(int a[], int p, int r) {
int i = p,j = r + 1;
int x = a[p];//大于x的数交换到右边,小于x的数交换到左边
while (1) {
while (a[++i] < x && i < r);//找到大于x的数
while (a[--j] > x);//找到小于x的数
if (i >= j) break;//这种情况表示已经排序,则退出循环
swap(a[i], a[j]);
}
a[p] = a[j];
a[j] = x;
return j;
}
Parition对a[p:r]进行划分时,以元素r[p]作为划分的基准,分别从左,右两端井始,扩展两个区域a[p:i]和a[j:r]小,使a[p:i]中元素小于或等于x,而a[j:r]中元素大于或等于t。初始时,i = p,且j= r + 1.
在while 循环体中,下标j逐渐减小,i逐渐增大,直到a[i] >= x >= a[j]。如果这两个不等式是严格的,则a[i]不会是左边区域的元素,而a[j]不会是右边区域的元素。此时若i < j,就应该交按a[j]与a[j]的位置,扩展左右两个区域。
while循环重复至i >= j时结束。这时a[p:r]已被划分成a[p:q-1]. a[q]和a[q+1:r]且满足a[p:q-1]中元素不大于a[q+1:r]中元素。在Partition结束时,返回划分点q = j。
事实上,函数Parition(的主要功能是将小于x的元素放在原数组的左半部分,将大于x的元素放在原数组的右半部分。其中有些细节需要注意,例如,算法中的下标i和j不会超出a[p:r]的下标界。另外,在快速排序算法中选取a[p]作为基准可以保证算法正常结束。如果选择a[r]作为划分的基准,且a[r]又是 a[p:r]中的最大元素,则Partition 算法返回的值为q=r,这会使QuickSort陷入死循环。
综合上述分析我们可以发现快速排序的时间复杂度和数据的具体情况相关:即与每次的数据划分是否对称有关:
其中最坏情况每次划分为1和n-1此时的时间复杂度为O(n2)。
最好的情况每次划分的部分为n/2,此时的时间复杂度最低为O(nlogn)。
事实证明快速排序在平均情况下的时间复杂度也为O(logn),所以在排序算法中也算是快的。
总结
尽管很多编译器,库函数由快速排序的实现,但是学习并理解这两种排序算法的原理更有助于我们在以后的使用和解决问题,同时还有助于我们增长算法的思维活跃度。