几种排序算法总结
2012-10-15 13:12
162 查看
1. Insertion Sort
最简单的排序算法之一
2. Shell Sort
Shell排序是冲破二次时间屏障的第一批算法之一,证明它的亚二次时间界较难。以下采用Shell建议的(流行,但是不好)增量序列:
编程的简单使得Shell排序成为对适度的大量输入数据经常选用的算法,任何排序任务开始时都可以用Shell排序,若证明效率不高,可以再改换快排。
3. Heep Sort
复杂度为O(NlogN)。最坏情况下比较2NlogN-O(N)次,最少比较NlogN-O(N),平均比较2NlogN-O(Nlog logN)。
二叉堆是一棵完全二叉树,由于其规律性,可以用一个数组来实现。
上述算法避免使用额外的附加数组空间,将每次pop出来的Max元素放到原始数组的末端,并将数组大小减1。
采用数组实现堆排序的问题是,我们每次总是将大的移除,然后将最末的元素放到堆顶让其自我调整,这样一来,有很多比较将是被浪费的,因为最后一个元素能留在堆顶的可能性微乎其微。所以,如果当我们移除堆顶元素之后,如果能直接比较其兄弟节点,然后提一个上来,效率肯定要好一些。但是这样又会面临另外的问题,因为这种算法不能利用数组,否则将进行大量的数据移动,而如果利用指针,又会面临空间分配的问题,空间分配一样非常耗费时间。除非利用某种方式优化内存分配,比如预留等等。(摘)
4. Merge Sort
采用递归分治法实现,最坏情况下复杂度为O(NlogN),另外还需要O(N)的空间。
虽然运行时间为O(NlogN),但是归并排序很难用于主存排序,不仅是因为要开辟额外的空间,还需要花费时间将数据复制到临时数组再复制回来。当然情况也不尽然,由于在所有流行算法中归并排序的比较次数最少,当对象比较的耗时大于对象移动的耗时(例如Java),归并排序的价值便体现出来;否则的话,使用归并排序的代价很大(如C++编译器在处理函数模板时具有强大的在线优化能力,对象比较速度很快)。
如果能够使用很少的数据移动,那么即使使用稍微多一些的比较算法也是合理的。Quick Sort较好的平衡了这两者,也是C++库中普遍使用的排序算法。
5. Quick Sort
快排是在实践中最快的已知排序算法,平均运行时间为O(NlogN),最坏为O(N*N),通过优化枢纽元的选择,可以较容易的避免最坏情形。
保证快排平均时间复杂度的关键是枢纽元的选取,最理想的是选择待排序序列中值,即,使每次递归都将原问题分成两个大小相等的子问题。但准确选择中值需要大量的操作,需要采用一些近似方法,枢纽元的选择通常来说有三种方式:
1) 选取第一个元素。naive,在预排序输入下(很常见)是平方级的复杂度;
2) 随机选取元素。基本保证对半分,但是生成随机数的代价较大;
3) 三数中值法。去左端、右端、中心位置上的三个元素的中值,消除预排序不好的情形。
快排的另一个问题是对重复元素的处理。当遇到与枢纽元相等元素时,如果i和j都不停止,则有可能引发平方级复杂度(想一下所有元素都相等);若二者都停止递增(减)并进行交换,虽然看起来交换次数多了,但保证了建立两个几乎相等的子数组,总时间逼近O(NlogN)。
快排的第三个问题是,对于很小的数组(N<=20)使用快排,递归带来的开销将掩盖性能,通常对于小数组不递归的使用快排,而代之以诸如插排这样对小数组有效的算法,通常N选取10。
6. 间接排序
快排中使用了大量的元素移动操作,如果Type对象很大的话,效率会很低。可以使用中间置换算法来解决这个问题。
按照以下算法,重新排列长为L的循环需要L+1次Type复制,所以总体来看,Type元素复制的次数为M=N-C1+(C2+C3+...+Cn),其中CL是长度为L时的移动次数。最好的情况是没有Type复制,此时有N个长度为1的循环;最坏的情况有N/2个长度为2的循环,此时需要3N/2次Type操作。
7. 外部排序
在处理海量数据时,内存通常放不下所有待排数据,这时候就要借助磁盘进行排序。访问磁盘上的一个元素需要把磁带转动到正确位置,要花费大量时间,因此磁盘排序的目的就是尽量避免不连续的磁盘访问。磁盘排序通常需要多个磁带驱动器进行辅助,如果只有一个磁带驱动器可用,那么任何排序算法都需要O(N*N)次磁盘访问。
1)简单算法。(两路的情形)设有四盘磁带,两盘输入,两盘输出,假设每次内存只能装下M条记录,则需要log(N/M)(上取整数)趟工作外加一次初始的顺串构造。
2)多路合并。(k路的情形)2k盘磁带,需要logk(N/M)上取整趟磁盘访问,多路最小元的比较可以借助优先队列实现。
3)多相合并。 只需要k+1盘磁带来完成原本2k盘磁带的工作,要求初始顺串的个数时斐波那契数列(非斐波那契数列通过一些哑顺串来填补)Fn,将其分裂成两个斐波那契数列Fn-1(大的),Fn-2(小的),然后迭代合并。合并趟数为Fn-1在斐波那契数列中的位置。
4)替换选择。假设内存中使用优先队列排序,考虑到只要内存中的一个元素输出到磁带上,它所占用的内存就可以被另外的记录y使用,比较y与刚才输出元素x的关系,若y>x,则将y放入到优先队列中,y属于当前顺串;否则,y不属于当前顺串,将其放到优先队列的死区(dead space),属于下一个顺串。直到有限队列的大小为0。这种方法产生平均长度为2M的顺串,可以进一步减少合并次数。
Ref: Data Structures and Alogrithm Analysis in C++
最简单的排序算法之一
/* * Insertion Sort * 2012.10.15, by jfk * Time Complexity: O(n*n) */ template <typename Type> void insertion_sort(Vector<type>, &a) { int j = 0; for(int p = 0; p < a.size(); p++) { Type tmp = a[p]; for (j = p; j > 0 && tmp < a[j-1]; j--) a[j] = a[j-1]; a[j] = tmp; } }
2. Shell Sort
Shell排序是冲破二次时间屏障的第一批算法之一,证明它的亚二次时间界较难。以下采用Shell建议的(流行,但是不好)增量序列:
/* * Shell Sort * 2012.10.15, by jfk * Not optimal, rethink "gap". * When using "Shell gap", the Worst Time Complexity is O(n*n) */ template <typename Type> void shell_sort(Vector<Type> &a) { for(int gap = a.size()/2; gap > 0; gap = gap / 2) { for(int i = gap; i < a.size(); i++) { Type tmp = a[i]; int j = i; for( ; j >= gap && tmp < a[j - gap]; j -= gap) a[j] = a[j - gap]; a[j] = tmp; } } }Shell增量的问题在于,这些增量未必互素,因此较小的增量可能影响很小,Hibbard提出的增量形式为1,3,7,..., 2的k次方-1,效果好一些。Shell排序的运行时间依赖于增量序列的选择,证明相当复杂:使用Shell增量时最坏运行时间为O(n2);使用Hibbard增量的最坏运行时间为O(n3/2)。
编程的简单使得Shell排序成为对适度的大量输入数据经常选用的算法,任何排序任务开始时都可以用Shell排序,若证明效率不高,可以再改换快排。
3. Heep Sort
复杂度为O(NlogN)。最坏情况下比较2NlogN-O(N)次,最少比较NlogN-O(N),平均比较2NlogN-O(Nlog logN)。
二叉堆是一棵完全二叉树,由于其规律性,可以用一个数组来实现。
inline int leftChild(int i) { return 2*i; } template <typename Type> void percolateDown(vector<Type> &a, int loca, int size) { int child; Type tmp; for(tmp = a[loca]; leftChild(loca) < size; loca = child) { child = leftChild(loca); //最大堆,不是a[child + 1] < a[child] if(child != size -1 && a[child] < a[child + 1]) child ++; if(tmp < a[child]) //最大堆 a[loca] = a[child]; else break; } a[loca] = tmp; } template <typename Type> void heepSort(vector<Type> &a) { //创建堆 for(int i = a.size()/2; i >= 0; i--) percolateDown(a, i, a.size()); //排序 for(int i = a.size() - 1; i >= 0; i--) { swap(a[0], a[i]); percolateDown(a, 0, i); } }
上述算法避免使用额外的附加数组空间,将每次pop出来的Max元素放到原始数组的末端,并将数组大小减1。
采用数组实现堆排序的问题是,我们每次总是将大的移除,然后将最末的元素放到堆顶让其自我调整,这样一来,有很多比较将是被浪费的,因为最后一个元素能留在堆顶的可能性微乎其微。所以,如果当我们移除堆顶元素之后,如果能直接比较其兄弟节点,然后提一个上来,效率肯定要好一些。但是这样又会面临另外的问题,因为这种算法不能利用数组,否则将进行大量的数据移动,而如果利用指针,又会面临空间分配的问题,空间分配一样非常耗费时间。除非利用某种方式优化内存分配,比如预留等等。(摘)
4. Merge Sort
采用递归分治法实现,最坏情况下复杂度为O(NlogN),另外还需要O(N)的空间。
template<typename Type> void mergeSort(vector<Type> &a) { vector<Type> temp(a.size()); mergeSort(a, temp, 0, a.size()-1); } template<typename Type> void mergeSort(vector<Type> &a, vector<Type> &temp_a, int left, int right) { int middle = (left + right) / 2; if(left < right) { mergeSort(a, temp_a, left, middle); mergeSort(a, temp_a, middle + 1, right); merge(a, temp_a, left, middle + 1, right); } } template<typename Type> void merge(vector<Type> &a, vector<Type> &temp_a, int leftPos, int rightPos, int rightEnd) { int leftEnd = rightPos - 1; int tmpPos = leftPos; //temp_a中的数据指针 int numElements = rightEnd - leftPos + 1; while(leftPos <= leftEnd && rightPos <= rightEnd) if(a[leftPos] < a[rightPos]) temp_a[tmpPos ++] = a[leftPos ++]; else temp_a[tmpPos ++] = a[rightPos ++]; while(leftPos <= leftEnd) temp_a[tmpPos ++] = a[leftPos ++]; while(rightPos <= rightEnd) temp_a[tmpPos ++] = a[rightPos ++]; //只有rightEnd没有发生变化,只能用它 for(int i = 0; i < numElements; i++, rightEnd--) a[rightEnd] = temp_a[rightEnd]; }
虽然运行时间为O(NlogN),但是归并排序很难用于主存排序,不仅是因为要开辟额外的空间,还需要花费时间将数据复制到临时数组再复制回来。当然情况也不尽然,由于在所有流行算法中归并排序的比较次数最少,当对象比较的耗时大于对象移动的耗时(例如Java),归并排序的价值便体现出来;否则的话,使用归并排序的代价很大(如C++编译器在处理函数模板时具有强大的在线优化能力,对象比较速度很快)。
如果能够使用很少的数据移动,那么即使使用稍微多一些的比较算法也是合理的。Quick Sort较好的平衡了这两者,也是C++库中普遍使用的排序算法。
5. Quick Sort
快排是在实践中最快的已知排序算法,平均运行时间为O(NlogN),最坏为O(N*N),通过优化枢纽元的选择,可以较容易的避免最坏情形。
保证快排平均时间复杂度的关键是枢纽元的选取,最理想的是选择待排序序列中值,即,使每次递归都将原问题分成两个大小相等的子问题。但准确选择中值需要大量的操作,需要采用一些近似方法,枢纽元的选择通常来说有三种方式:
1) 选取第一个元素。naive,在预排序输入下(很常见)是平方级的复杂度;
2) 随机选取元素。基本保证对半分,但是生成随机数的代价较大;
3) 三数中值法。去左端、右端、中心位置上的三个元素的中值,消除预排序不好的情形。
快排的另一个问题是对重复元素的处理。当遇到与枢纽元相等元素时,如果i和j都不停止,则有可能引发平方级复杂度(想一下所有元素都相等);若二者都停止递增(减)并进行交换,虽然看起来交换次数多了,但保证了建立两个几乎相等的子数组,总时间逼近O(NlogN)。
快排的第三个问题是,对于很小的数组(N<=20)使用快排,递归带来的开销将掩盖性能,通常对于小数组不递归的使用快排,而代之以诸如插排这样对小数组有效的算法,通常N选取10。
template<typename Type> void quickSort(vector<Type> &a) { quickSort(a, 0, a.size()-1); } template<typename Type> const Type& median3(vector<Type> &a, int left, int right) { int middle = (left + right)/2; if (a[right] < a[left]) swap(a[right], a[left]); if(a[middle] < a[left]) swap(a[middle], a[left]); if(a[right] < a[middle]) swap(a[right], a[middle]); //将枢纽元交换到right-1处 //此时顺序为:[left][... ...][pivot][right] swap(a[middle], a[right - 1]); return a[right - 1]; } template<typename Type> void quickSort(vector<Type> &a, int left, int right) { //在元素数目小于10时采用快排,否则采用插排 //防止过度递归带来的性能下降 if(left + 10 <= right) { Type pivot = median3(a, left, right); //比较时从left+1和right-2开始 //因为left, right, pivot之间的大小关系已经得到维持 //而且,a[left]和a[right]分别为j和i指针提供了警戒标记,防止越界 int i = left, j = right -1; for(; ;) { while(a[++i] < pivot){ } while(a[--j] > pivot){ } if(i < j) swap(a[i], a[j]); else break; } //还原pivot元素的位置 swap(a[i], a[right - 1]); //递归比较中值左边和右边 quickSort(a, left, i-1); quickSort(a, i+1, right); } else insertionSort(a, left, right); } template<typename Type> void insertionSort(vector<Type> &a, int left, int right) { int j; Type temp; for(int i = left + 1; i <= right; i++) { for(j = i, temp = a[i]; j > 0 && a[j -1] > temp; j--) a[j] = a[j - 1]; a[j] = temp; } }最后,快排之所以快的原因是,算法内部循环仅由一个增1/减1运算(运算很快)、一个测试以及一个转移组成。
6. 间接排序
快排中使用了大量的元素移动操作,如果Type对象很大的话,效率会很低。可以使用中间置换算法来解决这个问题。
按照以下算法,重新排列长为L的循环需要L+1次Type复制,所以总体来看,Type元素复制的次数为M=N-C1+(C2+C3+...+Cn),其中CL是长度为L时的移动次数。最好的情况是没有Type复制,此时有N个长度为1的循环;最坏的情况有N/2个长度为2的循环,此时需要3N/2次Type操作。
template<typename Type> class Pointer { public: Pointer(Type *rhs = NULL): pointee(rhs){ } //重载<操作符,为保证能正确对Pointer采用快排, //必须将快排中的Type元素比较符号均重写为< bool operator < (const Pointer &rhs) const { return *pointee < *rhs.pointee; } //类型转换函数,从Pointer<Type>转换到Type* operator Type*() const { return pointee; } private: Type* pointee; } template<typename Type> void largeObjectSort(vector<Type> &a) { //> >之间必须有空格 vector<Pointer<Type> > p(a.size()); int i, j, nextj; for(i = 0; i < a.size(); i++) p[i] = &a[i]; quickSort(p); for(i = 0; i < a.size(); i++) if(p[i] != &a[i]) { Type tmp = a[i]; for(j = i; p[j] != &a[i]; j = nextj) { nextj = p[j] - &a[0]; a[j] = *p[j];//移动中间元素 p[j] = &a[j];//复位指针 } a[j] = tmp;//移动最后元素 p[j] = &a[j];//复位指针 } }
7. 外部排序
在处理海量数据时,内存通常放不下所有待排数据,这时候就要借助磁盘进行排序。访问磁盘上的一个元素需要把磁带转动到正确位置,要花费大量时间,因此磁盘排序的目的就是尽量避免不连续的磁盘访问。磁盘排序通常需要多个磁带驱动器进行辅助,如果只有一个磁带驱动器可用,那么任何排序算法都需要O(N*N)次磁盘访问。
1)简单算法。(两路的情形)设有四盘磁带,两盘输入,两盘输出,假设每次内存只能装下M条记录,则需要log(N/M)(上取整数)趟工作外加一次初始的顺串构造。
2)多路合并。(k路的情形)2k盘磁带,需要logk(N/M)上取整趟磁盘访问,多路最小元的比较可以借助优先队列实现。
3)多相合并。 只需要k+1盘磁带来完成原本2k盘磁带的工作,要求初始顺串的个数时斐波那契数列(非斐波那契数列通过一些哑顺串来填补)Fn,将其分裂成两个斐波那契数列Fn-1(大的),Fn-2(小的),然后迭代合并。合并趟数为Fn-1在斐波那契数列中的位置。
4)替换选择。假设内存中使用优先队列排序,考虑到只要内存中的一个元素输出到磁带上,它所占用的内存就可以被另外的记录y使用,比较y与刚才输出元素x的关系,若y>x,则将y放入到优先队列中,y属于当前顺串;否则,y不属于当前顺串,将其放到优先队列的死区(dead space),属于下一个顺串。直到有限队列的大小为0。这种方法产生平均长度为2M的顺串,可以进一步减少合并次数。
Ref: Data Structures and Alogrithm Analysis in C++
相关文章推荐
- 几种排序算法的简单总结
- 几种排序算法总结
- (二)几种排序算法的学习总结(归并排序)
- c++常见的几种排序算法总结
- 几种常用排序算法总结(转载)
- 几种排序算法性能比较总结
- 总结几种排序算法的Java实现
- 几种排序算法的总结
- 总结几种常用的排序算法(含代码)
- 总结几种排序算法(二)---选择排序
- 基础的几种排序算法的总结 及其 解释排序的超棒效果图
- 几种排序算法总结
- 数据结构几种常用排序算法总结
- 数据结构几种排序算法的时间和空间复杂度总结
- (三)几种排序算法的学习总结(快速排序)
- Java基础-几种常见排序算法总结
- 几种排序算法的总结
- 几种常见排序算法总结(java版)
- 几种常用排序算法总结
- 几种排序算法总结(冒泡、选择、插入、快速)