数据结构之排序算法之O(nlogn)
2013-08-20 22:28
351 查看
上一次写了冒泡,选择,和插入排序。
这一次写一下堆排序、归并、快速排序。
最大堆的定义是,每个结点的值都比他的儿子大的一颗完全二叉树。
那么我们排序的过程就是:
首先将数组中的元素构建成一个最大堆
然后将堆顶(根节点)的值拿出来,和最后一个元素交换,这样最大的数就在最后了。
然后将交换到堆顶的节点进行下沉操作,找到其在当前最大堆中的位置。那么除了最后一个元素,前面的又是一个最大堆了。
重复上面的两步,直到只剩最大堆中只剩下一个元素。
那么如何将数组中的元素构建成最大堆呢
首先实现除根节点外,两个儿子树都已经是最大堆的情况,将根节点的值和儿子比较,然后和儿子中大的交换,交换后又是一个只有除根节点外,儿子树是最大堆的情况,继续交换,直到不需要交换(比两个儿子大或者没有儿子)
代码如下:
剩下的就好办了,首先从二叉树最后一个节点的父节点开始,将其和和儿子比较,挑选父节点和两个儿子节点3个中的最大值,放在父节点上。
然后处理最后一个父节点之前的父节点,这样能够保证每个处理的父节点而根节点的堆都符合第一条的要求。循环即可。
下面是堆排序代码:
堆排序分析,HeapAdjust函数的时间复杂度为 O(logN),因为完全二叉树的高度就是 logN,每次堆顶元素下降最多下降
logN 次。
综合起来,HeapAdjust一共执行了 O(2N)
次,那么复杂度就是 O(2NlogN)=O(NlogN)。
归并排序
归并排序的思想非常简单,先是 1对1 排序,然后 2对2 ,然后
4对4 ,直到N,共执行了 logN 次,每次执行 O(N)
次,那么复杂度就是 O(NlogN)。
下面是非递归的代码,比较恶心,在写的时候经常在划分长度上出问题。
快速排序
首先在数组中找到一个值,然后对数组操作,保证这个值左边的值都比它小,右边的都比它大。
然后以这个值所在的位置分开,数组变成两个小数组,然后继续执行上一步,直到所有的数组长度变成1,那就不需要比较了,所有的数字都已经排序完毕。
代码:
上面的函数实现了第一步的过程,还需要一个递归函数来实现第二步。
代码:
到这里还有优化的余地,即第一步中选取中间值,是从数组第一个元素开始的,这个值在数组中排序与靠近中间越好,但是第一个值是中间值的概率很小,可以先找出 第一位、中间位、最后位 这3个值的中间值,放在数组首位,然后在执行函数,实际测试中,这个优化会大大提生快速排序的性能,优化前,数组很大的时候,由于重复递归,很快就会栈溢出,但是优化后栈溢出就很难出现了。
还有一个,当数组个数较少时,采用插入排序比快速排序更好,我们可以加个判断,当数组长度小于128(或者其他的数),退出递归,这样数组中就是分段有序的,然后再调用一遍插入排序,就可以了(在数组分段有序的情况下,插入排序的复杂度接近
O(N) )。当然也可以在长度小于128时,直接调用插入排序对这个小数组进行插入排序。经测试,这个优化会缩短一半的时间。
下面是优化后递归代码。
3个排序分析,由于堆排序进行两次 logN 操作,所以时间是归并排序的2倍,而快速排序由于是递归的原因,是比不上非递归的归并的,下面的测试结果也说明这一点。等以后写了非递归的快速排序再测试一遍。
测试数组大小为5000万,浮点
这一次写一下堆排序、归并、快速排序。
堆排序
堆排序,主要是利用完全二叉树在数组中的存储方式(层序遍历),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万,浮点
相关文章推荐
- <三>java数据结构与算法 选择排序
- 数据结构与算法:排序
- 【数据结构与算法】内部排序之一:插入排序和希尔排序的N中实现(不断优化,附完整源码)
- Java数据结构与算法:排序
- 数据结构与算法——字符串排序
- 【数据结构与算法】内部排序之三:堆排序(含完整源码)
- 数据结构与算法学习:选择排序
- 数据结构算法之快速排序
- 【数据结构与算法】内部排序之三:堆排序(含完整源码)
- 数据结构与算法之排序
- Python 数据结构与算法 —— 从分治的角度看快速排序、归并排序
- 【数据结构与算法】直接插入排序
- 排序_算法_数据结构
- 【数据结构与算法】treemap应用 排序
- 【数据结构与算法】【排序】基本概念
- 【数据结构与算法】【排序】简单选择排序的代码实现
- 数据结构及简单算法的总结----之【排序】
- 【数据结构与算法】【排序】总结
- 【数据结构与算法】内部排序之二:冒泡排序和选择排序(改进优化,附完整源码)
- 【练习】数据结构与算法练习题之高效排序