前面已经介绍了基础的插入排序、选择排序和冒泡排序,以及高效的希尔排序,看这里。下面介绍剩余的部分。
五、高效的排序方法之堆排序
堆排序使用了选择排序固有的方法,采用找最大元素的方法,从数组末尾开始放置:将数组建堆,堆的根节点数值最大,将其放在数组末尾,然后排除掉这个已在最合适位置的元素,重新建堆,根节点代表的最大元素放到数组的倒数第二个位置,以此类推。代码实现:
template<class T>
void heapsort(T data[],int size){
//Robert Floyd自底向上将数组转化为堆
for(int i=size/2-1;i>=0;--i) //size/2-1是最后一个非叶节点
moveDown (data,i,size-1); //data[0]将放置数组最大值
for(i=size-1;i>=1;--i){
swap(data[0],data[i]); //最大值放到右边合适位置,从右往左有序
moveDown(data,0,i-1); //恢复树data[0],...,data[i-1]的堆属性,不需要重新转化
}
}
其中,moveDown函数,将根元素沿树向下移动直至找到合适的位置,满足(最大)堆的定义:1)每个节点的值大于等于其每个子节点的值;2)该树完全平衡,最后一层的叶字都处于最左侧的位置。实现代码:
template<class T>
void moveDown(T data[], int first, int last){
int largest=2*first+1; //父节点序号为first,它的两个子节点必为2*first+1和2*first+2
//若数组元素个数是奇数,则最后会单独一个叶子节点
while(largest<=last){
//找子节点中最大的那个
if(largest<last && data[largest]<data[largest+1])
largest++;
//若父节点小于子节点,则父节点跟大的子节点换位置
if(data[first]<data[largest]]){
swap(data[first],data[largest]);
first=largest;
largest=2*first+1;
}
else largest=last+1; //跳出循环
}
}
时间复杂度:最好O(n log(n)),最差O(n log(n)),平均O(n log(n))
六、高效的排序方法之快速排序
快速排序的原则很类似于希尔排序,都是将原始问题划分成更易解的子问题。具体而言,快速排序将原始数组通过设置边界值的方式划分出两个子数组(大于此边界值的归为一个子数组,不大于的归为另一个),在两个子数组中再设置边界值划分,...,反复进行,直到得到仅包含一个元素的数组,在“准备排序的过程中”实现了“排序”。选择边界值的方法很多,通常选第一个元素,更通常选择中间位置的元素,实现代码:
template<class T>
//递归地划分数组
void quicksort(T data[],int first,int last){
int lower=first+1,upper=last;
swap(data[first],data[(first+last)/2]);
T bound=data[first];
while(lower<=upper){
//左边统一为小于边界值的
while(data[lower]<bound)
lower++;
//右边统一为大于边界值的
while(bound<data[upper])
upper--;
//位置不合适则调换位置
if(lower<upper)
swap(data[lower++],data[upper--]);
//遍历结束,满足条件,退出第n次迭代
else lower++;
}
//边界值换到合适的位置上,不再变动
swap(data[upper],data[first])
//左边数组继续划分
if(first<upper-1)
quicksort(data,first,upper-1);
//右边数组继续划分
if(upper+1<last)
quicksort(data,upper+1,last);
}
template<class T>
void quicksort(T data[],int n){
//最大值预处理,防止lower的值超过数组末端
int i,max;
if(n<2)
return;
for(i=1,max=0;i<n;i++)
if(data[max]<data[i])
maxi=i;
swap(data[n-1],data[max]);
quicksort(data,0,n-2);
}
时间复杂度:最好O(n log(n)),最差O(n^2),平均O(n log(n))
七、高效的排序方法之归并排序
归并排序是基于分治的排序算法,将复杂问题分解成简单的子问题,跟快速排序的思路一致,但差别在快速排序很难控制划分的过程,而归并排序是使划分尽可能简单,着重于合并两个已排好序的数组。具体而言,归并排序就是不断地把原数组划分成大小相等的两个子数组,直至划分的子数组仅包含一个元素,然后将划分的子数组不断做有序的组合并成大数组。代码实现:
template<class T>
void mergesort(T data[],int start,int end,T result[]){
if(1==end-start) //数组只两个元素进行排序后返回
{ if(data[start]>data[end]){
int temp=data[start];
data[start]=data[end];
data[end]=temp;
}
return;
}
else if(0==end-start) //数组只一个元素不需要排序
return;
else
{ //继续划分,分别对左右子数组操作
mergesort(data,start,(end-start+1)/2+start,result);
mergesort(data,(end-start+1)/2+start+1,end,result);
//合并
merge(data,start,end,result);
//排好序的临时数组覆盖回原始数组中去
for(int i=start;i<=end;i++)
data[i]=result[i];
}
}
其中的merge函数实现:
templat<class T>
void merge(T data[],int start,int end,T result[]){
int left_length=(end-start+1)/2+1;
int left_index=start;
int right_index=start+left_length;
int result_index=start;
while(left_index<right_index && right_index<end+1) //当左右子数组都有元素时比较后放入
{
//对排好序的左右子数组合并
if(data[left_index]<=data[right_index])
result[result_index++]=data[left_index];
else
result[result_index++]=data[right_index];
}
//将剩下的元素放进去
while(left_index<right_index)
result[result_index++]=data[left_index++];
while(right_index<end+1)
result[result_index++]=data[right_index++];
}
时间复杂度:最好O(n log(n)),最差O(n log(n)),平均O(n log(n))
八、高效的排序方法之基数排序
生活中应用基数排序的案例:图书馆卡片排序,根据字母表中的字母将卡片分为很多堆,每一堆包含了姓名以相同字母开头的作者,然后在每堆中根据作者姓名的第二个字母划分堆,这个过程一直进行下去直到划分出的堆数等于最长的作者姓名的字母个数为止。对整数排序时可以是从右往左的顺序进行,创建0-9共10个堆,刚开始时所有整数都按照自己最右边的数放入相应的堆里面,比如18放在8堆里,然后合并堆,再看所有整数的倒数第二个数字,18放在1堆里,直到最长整数的最左边数位处理完,注意上述的过程需要保持整数的相对位置(使用队列),不然前面的过程实现按低位放置将毫无意义。代码实现:
void radixsort(long data[],int n){
register int d,j,k,factor;
const int radix=10;
const int digits=10; //数组中整数的最大位数
Queue<long> queues[radix];
//从右往左按数位存储
for(d=0,factor=1;d<digits;factor*=radix,d++){
for(j=0;j<n;j++)
//除以factor舍弃在当前排序过程中数位d后面的所有数位
//除以radix并取模舍弃数位d之前的所有数位,实现将元素按当前数位分类存储到队列
queques[(data[j]/factor)%radix].enqueue(data[j]);
//将划分的几个队列合并起来,重组成新数组
for(j=k=0;j<radix;j++)
while(!queues[j].empty())
data[k++]=queues[j].dequeue();
}
}
时间复杂度:最好O(n k),最差O(n k),平均O(n k)
至此,常见的几个常规的和高效的排序算法介绍完毕,而STL里的<algorithm>提供了常用的几个排序实现,比如sort()函数实现了快速排序,stable_sort()实现了归并排序。