您的位置:首页 > 理论基础 > 数据结构算法

数据结构之排序算法之O(nlogn)

2013-08-20 22:28 351 查看
上一次写了冒泡,选择,和插入排序。

这一次写一下堆排序、归并、快速排序。

堆排序

堆排序,主要是利用完全二叉树在数组中的存储方式(层序遍历),i 位置的节点的儿子是 2i 和 2i+1 。

最大堆的定义是,每个结点的值都比他的儿子大的一颗完全二叉树。

那么我们排序的过程就是:

首先将数组中的元素构建成一个最大堆
然后将堆顶(根节点)的值拿出来,和最后一个元素交换,这样最大的数就在最后了。
然后将交换到堆顶的节点进行下沉操作,找到其在当前最大堆中的位置。那么除了最后一个元素,前面的又是一个最大堆了。
重复上面的两步,直到只剩最大堆中只剩下一个元素。

那么如何将数组中的元素构建成最大堆呢

首先实现除根节点外,两个儿子树都已经是最大堆的情况,将根节点的值和儿子比较,然后和儿子中大的交换,交换后又是一个只有除根节点外,儿子树是最大堆的情况,继续交换,直到不需要交换(比两个儿子大或者没有儿子)

代码如下:

void HeapAdjust(double *a,int begin,int end)
{
if (!a||end<begin)
return;

double temp=a[begin];
int i;
for (i=2*begin ; i<=end ; i*=2)
{
if (i<end && a[i]<a[i+1])//找到根节点儿子中的较大值
i++;
if (temp>=a[i])//根节点已经比儿子大,不需要交换
break;
else            //交换,并更新现在根节点的位置。
{
a[begin]=a[i];
begin=i;
}
}
a[begin]=temp;
}


剩下的就好办了,首先从二叉树最后一个节点的父节点开始,将其和和儿子比较,挑选父节点和两个儿子节点3个中的最大值,放在父节点上。
然后处理最后一个父节点之前的父节点,这样能够保证每个处理的父节点而根节点的堆都符合第一条的要求。循环即可。

下面是堆排序代码:

void HeapSort(double *a,int begin,int end)
{
if (!a||end<begin)
return;

int i;
for (i=end/2 ; i >=begin ; i--)  //生成最大堆
HeapAdjust(a,i,end);

for (i=end;i>begin;i--)
{
swap(&a[begin],&a[i]);       //交换堆顶元素和最有一个元素
HeapAdjust(a,begin,i-1);     //更新交换堆顶后的最大堆
}

}


堆排序分析,HeapAdjust函数的时间复杂度为 O(logN),因为完全二叉树的高度就是 logN,每次堆顶元素下降最多下降
logN 次。

综合起来,HeapAdjust一共执行了 O(2N)
次,那么复杂度就是 O(2NlogN)=O(NlogN)。

归并排序

归并排序的思想非常简单,先是 1对1 排序,然后 2对2 ,然后
4对4 ,直到N,共执行了 logN 次,每次执行 O(N)
次,那么复杂度就是 O(NlogN)。

下面是非递归的代码,比较恶心,在写的时候经常在划分长度上出问题。

void Merge(double* a,double* temp,int begin,int mid,int end) //将a[begin]到a[end]中的数据归并到temp[begin]到temp[end]中,mid是中间值
{
if (!a || !temp || mid<begin || end<mid)
{
return;
}
int i,j,k;
for (i=k=begin,j=mid+1 ; i<=mid&&j<=end ; )
{
if (a[i]<a[j])
temp[k++]=a[i++];
else
temp[k++]=a[j++];
}
while (i<=mid)
{
temp[k++]=a[i++];
}
while (j<=end)
{
temp[k++]=a[j++];
}
}

void MergePass(double* a,double* temp,int begin,int end,int sigleLength)
//将a[begin]到a[end]中的数据分段归并到temp[begin]到temp[end]中,sigleLength是每一段的长度,执行完成后,每段内的数据是有序的
{
int len=end-begin+1;
int i=begin,j;
while (i+2*sigleLength-1 <= end)
{
Merge(a,temp,i,i+sigleLength-1,i+2*sigleLength-1);
i+=2*sigleLength;
}
if (i+sigleLength-1 < end)
Merge(a,temp,i,i+sigleLength-1,end);
else
for (j=i;j<=end;j++)
temp[j]=a[j];
}

void MergeSort_xunhuan(double* a,int begin,int end)
{
if (!a || end<begin)
{
return;
}
int i;
int len=end-begin+1;
double *temp=(double*)malloc(sizeof(double)*len);
for (i=0;i<len;i++)
temp[i]=0;
i=1;
while (i<=len)
{
MergePass(a,temp,begin,end,i);
i*=2;
MergePass(temp,a,begin,end,i);
i*=2;
}
}


快速排序

首先在数组中找到一个值,然后对数组操作,保证这个值左边的值都比它小,右边的都比它大。
然后以这个值所在的位置分开,数组变成两个小数组,然后继续执行上一步,直到所有的数组长度变成1,那就不需要比较了,所有的数字都已经排序完毕。

代码:

int FindPos(double *p,int low,int high)
{
double val = p[low];
while (low<high)
{
while(low<high&&p[high]>=val)
high--;
p[low]=p[high];
while(low<high&&p[low]<val)
low++;
p[high]=p[low];
}
p[low]=val;
return low;
}


上面的函数实现了第一步的过程,还需要一个递归函数来实现第二步。

代码:

void QuickSort(double *a,int low,int high)
{
if (!a || high<=low)
return;

if (low<high)
{
int pos=FindPos(a,low,high);
QuickSort(a,low,pos-1);
QuickSort(a,pos+1,high);
}
}


到这里还有优化的余地,即第一步中选取中间值,是从数组第一个元素开始的,这个值在数组中排序与靠近中间越好,但是第一个值是中间值的概率很小,可以先找出 第一位、中间位、最后位 这3个值的中间值,放在数组首位,然后在执行函数,实际测试中,这个优化会大大提生快速排序的性能,优化前,数组很大的时候,由于重复递归,很快就会栈溢出,但是优化后栈溢出就很难出现了。

还有一个,当数组个数较少时,采用插入排序比快速排序更好,我们可以加个判断,当数组长度小于128(或者其他的数),退出递归,这样数组中就是分段有序的,然后再调用一遍插入排序,就可以了(在数组分段有序的情况下,插入排序的复杂度接近
O(N) )。当然也可以在长度小于128时,直接调用插入排序对这个小数组进行插入排序。经测试,这个优化会缩短一半的时间。

下面是优化后递归代码。

void QuickSort(double *a,int low,int high)
{
if (!a || high-low<128)
return;

int mid=(low+high)/2;
if (a[low]>a[high])
swap(&a[high],&a[low]);
if (a[mid]>a[high])
swap(&a[high],&a[mid]);
if (a[low]<a[mid])
swap(&a[mid],&a[low]);

if (low<high)
{
int pos=FindPos(a,low,high);
QuickSort(a,low,pos-1);
QuickSort(a,pos+1,high);
}
}

void QuickSort_2(double *a, int low, int high)   //快速排序
{
QuickSort(a, low, high);
InsertSort(a, low, high);          //最后用插入排序对整个数组排序
}


3个排序分析,由于堆排序进行两次 logN 操作,所以时间是归并排序的2倍,而快速排序由于是递归的原因,是比不上非递归的归并的,下面的测试结果也说明这一点。等以后写了非递归的快速排序再测试一遍。

测试数组大小为5000万,浮点

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: