几种排序算法总结

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++

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值