您的位置:首页 > 其它

几种排序算法总结

2012-10-15 13:12 162 查看
1. Insertion Sort

最简单的排序算法之一

/*
* 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++
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: