一、排序的定义
排序:是按关键字升序(指非递减)或降序(指非递增)的顺序对一组记录重新进行排列的操作。
二、排序的分类
1.内部排序:待排序的记录数不很大,整个排序过程不需要访问外存便能完成。
2.外部排序:待排序的记录数很大,整个排序过程不可能都在内存中完成,需要访问外存。
这里主要介绍适用于在计算机内存对一组记录进行升序排序的标准算法。
三、简单排序方法
特点:都基于对相邻数组的比较,复杂度为O()
1.插入排序
步骤:
①在A[1..i-1]中查找A[i]的插入位置。
②将①中找到的插入位置以后的记录从后往前依次后移一个位置。
③将A[i]插入。
template <typename E, typename Comp>void inssort(E A[], int n)
{
for (int i=1; i<n; i++) //从第2个记录开始插入
for (int j=i; (j>0)&&(Comp::prior(A[j], A[j-1])); j--)
swap(A,j,j-1); //交换当前记录与前一记录
}
分析:每次交换涉及3个基本操作,效率不高。
下面为加入监视哨的插入排序算法:
template < typename E, typename Comp >void inssort(E A[], int n)
{
for (int i=2; i<=n; i++) //记录存储在A[1]~A[n]
{ A[0]=A[i];j=i-1; //设置监视哨A[0]
while (Comp::prior(A[0], A[j])) //把比A[i]大的数据逐一后移
{
A[j+1]=A[j];j--;
}
A[j+1]=A[0];
}
}
分析:与最初的算法相比,减少了交换,但比较的次数不变。
插入排序时间分析:
最好的情况:待排序序列顺序有序,则比较n-1次,移动0次。
最坏的情况:待排序序列逆序有序,则比较次,移动
次。
2.冒泡排序
算法思想:设待排序元素列中元素的个数为n,则从后至前依次将相邻两个记录的排序字段的值进行比较,如果发生逆序(即前一个排序字段的值大于后一个),则将这两个元素交换;这称之为一趟起泡,结果将最小的元素交换到待排序元素序列的第一个位置,其他元素也都向最终排序的方向移,这样最多做n-1趟起泡就能把所有元素排好序。
template < typename E, typename Comp >void bubsort(E A[], int n)
{
for (int i=0; i<n-1; i++) //排n-1趟
for (int j=n-1; j>i; j--) //从后往前
if (Comp::prior(A[j], A[j-1]))
swap(A, j, j-1);
}
分析:如果在第n-1趟之前已经完成排序,则会造成时间上的浪费,因此可以通过添加标志位来改进冒泡排序算法。
以下是改进后的添加标志位的冒泡排序算法:
template < typename E, typename Comp >void bubsort(E A[], int n)
{
int flag;
for (int i=0; i<n-1; i++)
{
flag=FALSE; //加标志位
for (int j=n-1; j>i; j--)
if (Comp::prior(A[j], A[j-1]))
{
swap(A, j, j-1);
flag=TRUE;
}
if(flag==FALSE) return; //发现已经全部有序了
}
}
冒泡排序时间分析:
最好的情况:待排序序列顺序有序,则比较n-1次,移动0次。
最坏的情况:待排序序列逆序有序,则比较次,移动
次。
3.选择排序
算法思想:每一趟从待排序的记录中选出排序字段值最小(最大)的记录,与当前待排序的第一个记录交换位置。选择排序的第i次是“选择”数组中第i小的记录,并将该记录放到数组的第i个位置。直到全部待排序的记录全部排完。
template < typename E, typename Comp >void selsort(E A[], int n)
{
for (int i=0; i<n-1; i++)
{
int lowindex = i;
for (int j=n-1; j>i; j--) // 找到最小值
if (Comp::prior(A[j], A[lowindex]))
lowindex = j; // 将最小值放在合适的位置
swap(A, i, lowindex);
}
}
选择排序时间性能分析:
对n个记录进行简单选择排序,所需进行的关键字间的比较次数总计为。
移动记录的次数,最小值为0, 最大值为n-1。
4.排序低速原因
◼只比较相邻的元素;
◼因此,比较和移动只能一步步进行(除选择排序外)
◼都属于交换排序
后面讨论的排序算法是在不相邻的记录之间进行比较与交换。
四、高效排序方法
1.Shell 排序(又称缩小增量排序法)
与交换排序的不同点:shell排序是在不相邻的记录之间进行比较与交换。
特点:n很小时,或基本有序时排序速度较快。
算法思想:利用插入排序的最佳时间代价特性,先对所有记录按增量进行分组,组内进行插入排序;减少增量重复上面步骤直至增量为1时停止。
例如:
第一趟希尔排序,设增量d=5
第二趟希尔排序,设增量d=3
第三趟希尔排序,设增量d=1
代码如下:
template < typename E, typename Comp >void inssort2(E A[], int n, int incr)
{
for (int i=incr; i<n; i+=incr) //子序列插入排序
for (int j=i;(j>=incr) &&(Comp::prior(A[j], A[j-incr])); j-=incr)
swap(A, j, j-incr);
}
template < typename E, typename Comp >void shellsort(E A[], int n) //希尔排序
{
for (int i=n/2; i>2; i/=2) // 对每个步长d
for (int j=0; j<i; j++)
inssort2<E,Comp>(&A[j], n-j, i);
inssort2<E,Comp>(A, n, 1);
}
该算法为不稳定算法。
2.快速排序
该算法为目前所有内排序算法中在平均情况下最快的一种。
算法思想:找一个记录,以它的关键字作为“枢轴”,凡其关键字小于枢轴的记录均移动至该记录之前,反之,凡关键字大于枢轴的记录均移动至该记录之后。致使一趟排序之后,记录的无序序列将分割成两部分。之后再分别对分割所得两个子序列“递归”进行快速排序。
template < typename E>inline int findpivot(E A[], int i, int j) //取中间为轴
{
return (i+j)/2;
}
template < typename E, typename Comp >inline int partition(E A[], int l, int r,E& pivot)
{
do {
while (Comp::prior(A[++l], pivot));
while ((l<r) && Comp::prior(pivot ,A[--r]));
swap(A, l, r);
} while (l < r);
return l;
}
template < typename E, typename Comp >void qsort(E A[], int i, int j)
{
if (j <= i) return;
int pivotindex = findpivot(A, i, j);
swap(A, pivotindex, j);
int k = partition<E,Comp>(A, i-1, j, A[j]);
swap(A, k, j);
qsort<E,Comp>(A, i, k-1);
qsort<E,Comp>(A, k+1, j);
}
快速排序的时间复杂度为O(nlogn)。
若待排记录的初始状态为按关键字有序时,快速排序将蜕化为起泡排序,其时间复杂度为O()。
3.归并排序
算法思想:将一个序列分成两个等长的子序列,分别对这两个子序列递归地调用归并排序算法,再将两个位置相邻的记录有序子序列。
template < typename E, typename Comp >void mergesort(E A[], E temp[], int left, int right) {
if (left == right) return;
int mid = (left+right)/2;
mergesort<E,Comp>(A, temp, left, mid);
mergesort<E,Comp>(A, temp, mid+1, right);
for (int i=left; i<=right; i++) temp[i] = A[i];
int i1 = left; int i2 = mid + 1;
for (int curr=left; curr<=right; curr++)
{
if (i1 == mid+1) A[curr] = temp[i2++];
else if (i2 > right) A[curr] = temp[i1++];
else if (Comp::prior(temp[i1], temp[i2])) A[curr] = temp[i1++];
else A[curr] = temp[i2++];
}
}
时间复杂度:T(n)=O(nlogn)
空间复杂度:S(n)=O(n)
4.堆排序
算法思想:将无序序列建成一个堆,得到关键字最小(或最大)的记录;输出堆顶的最(大)值后,使剩余的n-1个元素重又建成一个堆,则可得到n个元素的次小值;重复执行,得到一个有序序列。
堆排序需解决的两个问题:
①如何由一个无序序列建成一个堆?
从下往上进行“筛选”,当左右子树都已调整为堆时,最后只要调整根节点使整个二叉树是堆即可。
②如何在输出堆顶元素之后,调整剩余元素,使之成为一个新的堆?
输出堆顶元素之后,以堆中最后一个元素替代之;然后将根结点值与左、右子树的根结点值进行比较,并与其中小者进行交换;重复上述操作,直至叶子结点,将得到新的堆。
template < typename E, typename Comp >void heapsort(E A[], int n)
{ // Heapsort
E maxval;
maxheap<E,Comp> H(A, n, n);
for (int i=0; i<n; i++) // Now sort
maxval=H.removefirst(); // Put max at end
}
堆排序的时间复杂度为O(nlogn)。
5.分配排序
算法思想:根据记录的关键码来确定其排序的最终位置。
template < typename E, class getKey >void binsort(E A[], int n)
{
List<E> B[MaxKeyValue];
for (i=0; i<n; i++)
B[A[i]].append(getKey::key(A[i]));
}
优点:时间复杂度为O(n+MaxKeyValue),当MaxKeyValue很大时,算法的时间代价可能为O()或更差。
缺点:适用范围窄。
6.桶式排序
算法思想:将序列中的元素分配到一组桶中,每个桶再分别排序(使用其他排序方法或递归使用桶式排序),最后依序遍历每个桶,将所有元素有序放回序列。
7.基数排序
算法思想:将关键码看成若干个关键字复合而成,然后对每个关键字进行分配排序依次重复,最终得到一个有序序列。
时间代价分析:对于n个数据的序列,假设基数为r,这个算法需要k趟分配工作。每趟分配的时间为Θ(n+r),因此总的时间开销为Θ(nk+rk)。因为r是基数,它一般是比较小的。可以把它看成是一个常数。变量k与关键码长度有关,它是以r为基数时关键码可能具有的最大位数。在一些应用中我们可以认为k是有限的,因此也可以把它看成是常数。在这种假设下,基数排序的最佳、平均、最差时间代价都是Θ(n),这使得基数排序成为我们所讨论过的具有最好渐近复杂性的排序算法。