排序算是算法里最基础、最经典又最常考的问题,今天参加腾讯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); } }
其他的排序算法以后再补充。
关于排序算法的稳定性:
不稳定的排序算法:堆排序、快速排序、希尔排序、直接选择排序
稳定的排序算法:基数排序、冒泡排序、直接插入排序、折半插入排序、归并排序。