【数据结构与算法】排序算法

排序算是算法里最基础、最经典又最常考的问题,今天参加腾讯PC客户端一面,不出所料又考了排序问题,今天打算在这里好好总结并实现几个常见排序。


首先是插入排序,插入排序由 N - 1 趟排序组成,对于第 P 趟排序后,保证下标从 0 ~ P 的元素是有序的。插入排序最优情况下(已经排好序)运行时间为 O(N),平均情形下 θ(N^2)。代码实现如下:

template<typename T>
void InsertionSort(T *data, int N)
{
	int i, p;
	T tmp;
	for(p = 1; p < N; ++p)
	{
		tmp = data[p];
		for(i = p; i > 0 && data[i - 1] > tmp; --i )
			data[i] = data[i - 1];
		data[i] = tmp;
	}
}



然后是快速排序,快排是在实践中最后快的已知排序法,它的平均运行时间是 O(NlogN),其最坏情形为 O(N^2)。快排是一种分治的递归排序,参考《数据结构与算法》,我们得到一下代码:

template <typename T>
T mid3(T *data, int left, int right)
{
		int mid = (left + right) / 2;
		if(data[left] > data[mid])
			swap(data[left], data[mid]);
		if(data[left] > data[right])
			swap(data[left], data[right]);
		if(data[mid] > data[right])
			swap(data[mid], data[right]);

		swap(data[mid], data[right - 1]);
		return data[right - 1];
}

template <typename T>
void QuickSort(T *data, int left, int right)
{
	int i, j;
	T pivot;
	if(right - left > 3)
	{
		pivot = mid3(data, left, right);
		i = left + 1; 
		j = right - 2;
		
		while(1)
		{
			while(data[i] < pivot)
				++i;
			while(data[j] > pivot)
				--j;
			if(i < j)
				swap(data[i], data[j]);
			else
				break;
		}
		swap(data[i], data[right - 1]);
		QuickSort(data, left, i - 1);
		QuickSort(data, i + 1, right);
	}
	else
		InsertionSort(data + left, right - left + 1);
}
需要注意的是该版本的快排在元素数量小于5个时,调用了插入排序。


再来一个使用第一个元素作为枢纽的代码:

void Qsort(int a[], int left, int right)
{
    if(left >= right)
        return;

    int i = left, j = right;
    int pivot = a[i];                            //用字表的第一个记录作为枢轴
 
    while(i < j)
    {
        while(i < j && a[j] >= pivot)            //注意两个while的顺序与循环外的a[i] = pivot 是对应起来的
            --j;
        a[i] = a[j];                             //将比第一个小的移到低端
 
        while(i < j && a[i] <= pivot)
            ++i;
        a[j] = a[i];                              //将比第一个大的移到高端
    }
    a[i] = pivot;                                 //枢轴记录到位
    Qsort(a, left, i - 1);
    Qsort(a, i + 1, high);
}






接着是归并排序,它是外部排序的基石,它的时间复杂度为 O(NlogN),但是它一般不用于内部排序,主要在于其所需空间为 2 倍的数组大小,而且还有额外的数组搬移,这会严重放慢排序的速度。归并排序的核心思想也是分治。代码如下:

template <typename T>
void Merge(T *data, T *tmp, int lbegin, int rbegin, int rend)
{
	int lend = rbegin - 1;
	int num = rend - lbegin + 1;
	int i = lbegin;

	while(rbegin <= rend && lbegin <= lend)
	{
		if(data[rbegin] < data[lbegin])
			tmp[i++] = data[rbegin++];
		else
			tmp[i++] = data[lbegin++];
	}

	while(rbegin <= rend)
		tmp[i++] = data[rbegin++];

	while(lbegin <= lend)
		tmp[i++] = data[lbegin++];
	
	while(num--)                    //注意开始我的代码是这样的。。。 data[rend--] = tmp[rend--];找了半天错。。
	{
		data[rend] = tmp[rend];
		rend--;
	}
}

template <typename T>
void MergeSort(T *data, T *tmp, int left, int right)
{
	if(left < right)
	{
		int mid = (left + right) >> 1;
		MergeSort(data, tmp, left, mid);
		MergeSort(data, tmp, mid + 1, right);
		Merge(data, tmp, left, mid + 1, right);
	}
	else
		return;
}





再者是堆排序。堆排序使用了一种叫做二叉堆(binary heap)的数据结构,主要操作是 BuildHeap 和 DeleteMin(max) 两个。其中建立堆的时间为 O(N),每一次的 DeleteMin 操作花费时间 O(logN),因此堆排序的运行时间为 O(N + NlogN) = O(NlogN)。

此处的堆排序设计极为精巧,节省了时间与空间。它将根节点与尾节点交换后再将树的大小减一,然后再下滤一次,这样的结果是经过 N - 1 次 DeleteMin 后,数组正好是从大到小排列。

对排序的代码实现如下:

//最小堆,使用二叉堆结构。对于有N个元素的二叉堆,有效下标从 1 ~ N,下标 0 处用来保护,不存储数据。
void PercDown(int *data, int i, int N)
{
	int tmp, child;
	child = i << 1;
	for(tmp = data[i]; child <= N;)
	{
		if(child + 1 <= N && data[child + 1] < data[child] )
			child++;
		if(data[child] >= tmp)                         //妈蛋,这里又错了一次,一定注意是和 tmp 比,而不是 data[i]
			break;
		
		data[i] = data[child];
		i = child;
		child = i << 1;
	}
	data[i] = tmp;
}

void HeapSort(int *data, int N)
{
	//BuildHeap
	int i;
	for(i = N / 2; i > 0; --i)
		PercDown(data, i, N);

	//DeleteMin
	for(i = N; i > 1;)
	{
		swap(data[i], data[1]);
		PercDown(data, 1, --i);
	}
}


其他的排序算法以后再补充。



关于排序算法的稳定性:

不稳定的排序算法:堆排序快速排序希尔排序、直接选择排序

稳定的排序算法:基数排序、冒泡排序、直接插入排序、折半插入排序、归并排序

你可能感兴趣的:(快速排序,归并排序,插入排序,堆排序,排序算法)