@myecho
2018-03-29T13:36:39.000000Z
字数 6281
阅读 819
数据结构
首先,排序算法的稳定性大家应该都知道,通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。
选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法,而冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法
针对昨天面试中出现的问题,今天又把所有的排序算法重写了一遍,加深印象。
#include <iostream>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
//冒泡排序 稳定的
void BubbleSort(int * a, int length) {
bool change = true;
for (int i = length - 1; i >= 0 && change; --i) {
change = false;
for (int j = 0; j < i; ++j) {
if (a[j+1] < a[j]) {
change = true;
int tmp = a[j+1];
a[j+1] = a[j];
a[j] = tmp;
}
}
}
}
最好情况比较N-1次,交换0次(有序的情况),最差情况比较N^2/2次,交换N^2/2次(逆序)
//插入排序 稳定的
void InsertSort(int * a, int length) {
for (int i = 1; i < length; ++i) {
for (int j = i; j > 0 && a[j] < a[j-1]; j--)
exch(a, j, j-1);
//可二分查找代替,但是虽然比较和查找时间复杂度降低,但是每次需要移动的元素个数与顺序比较和查找是相同的。
}
}
平均是N^2/4次比较以及N^2/4次交换,最坏情况下需要N^2/2次比较和N^2/2次交换,最好情况下需要N-1次比较和0次交换(指的是每次比较如果小则交换,而不是将要插入的值右移)
void shellSort(int * a, int length) {
int N = length;
int h = 1;
while (h < N/3) h= 3*h + 1; //1,4,13,40,121,364...
while (h >= 1) {
for (int i = h; i < N; i++) {
//将a[i]插入到a[i-h],a[i-2*H],a[i-3*h]等
for(int j = i; j >= h && less(a[j],a[j-h]); j-=h)
exch(a,j,j-h);
}
}
}
一个有趣的结论是使用如上所示的递增序列时,算法的最坏运行时间不会超过N^(3/2),也属于是不稳定排序算法
//选择排序 不稳定
void SelectSort(int * a, int length) {
for (int i = length - 1; i >= 0; --i) {
int k = i;
for(int j = 0; j < i; ++ j) {
if (a[k] < a[j]) k = j;
}
if (k != i) {
int tmp = a[i];
a[i] = a[k];
a[k] = tmp;
}
}
}
1. 运行时间与输入无关,都有N次交换以及N^2/2次比较
2. 数据的移动是最少的,只需要N次交换
//堆排序 不稳定
void HeapAdujst(int * a, int s, int length) { 下沉操作 //调用的必要条件是左右均为最小堆了
int tmp = a[s];
int j = 2*s + 1; //1~length时和0~length-1时是不同的
while (j < length) {
if ((j < length - 1) && a[j+1] < a[j]) j++;
if (tmp < a[j]) break;
else {
a[s] = a[j];
s = j;
j = 2*s + 1;
}
}
a[s] = tmp;
}
private void swim(int * a, int s) { //上浮操作
while (s > 0 && a[(s-1)/2] > a[s]) {
exch((s-1)/2, s);
s = (s-1)/2;
}
}
void HeapSort(int * a, int length) {
for (int i = length/2; i >= 0; --i) HeapAdujst(a, i, length); //只有这样才满足调用的必要条件 建堆的复杂度是O(N) 证明在:https://www.zhihu.com/question/20729324
for (int i = length - 1; i >= 0; --i) {
int tmp = a[i];
a[i] = a[0];
a[0] = tmp;
HeapAdujst(a, 0, i); //最后长度是i
}
}
堆排总的时间复杂度为O(N*logN)
补充:建堆的过程可以上升也可以下沉,下沉时必须保证子树已经是堆的形式,上升时则必须保证目前的上边的树已经是堆的状态了。上升建堆的复杂度是nlogn
//快速排序 不稳定
int partition(int * a, int low, int high) { //从大到小
int pivotkey = a[low];
while (low < high) {
while(low < high && a[high] < pivotkey) high--;
a[low] = a[high];
while(low < high && a[low] > pivotkey) low++;
a[high] = a[low];
} //上式中应该尽量把=当做停下扫描的条件,尽管这样可能会不必要的进行一些等值元素的交换,避免在数组包含大量重复值得情况下将左右数组分的不均匀,从而导致运行时间变成平方级别
a[low] = pivotkey;
return low;
}
void QuickSort(int * a, int low, int high) {
if (low < high) {
int pivotloc = partition(a, low, high);
QuickSort(a, low, pivotloc - 1);
//do something on privot
QuickSort(a, pivotloc + 1, high);
}
}
关于快速排序的优化:
1. 切换到插入排序,将if (low < high)修改为if (high <= low + M) 进行插入排序,5~15之间的任意值在大多数情况下可以令人满意
2. 三取样切分,取样大小为3,取中间的数作为中枢
3. 三向切分,适用于存在大量重复元素的数组 http://www.cnblogs.com/kirov/p/5041075.html
快排非递归版本:http://www.cnblogs.com/zhangchaoyang/articles/2234815.html
快速排序的稳定化:开额外O(n)空间.每轮partition都直接扫一遍l..r, 小于pivot的放一个数组,大于pivot的放另外一个数组. 然后再合并到原数组当中.这样就是稳定的了
//归并排序 稳定
//将有二个有序数列a[first...mid]和a[mid...last]合并
void Merge(int a[], int first, int mid, int last, int c[]) {
int i = first, j = mid + 1;
for (int k = first; k <= last; ++k)
if (i > mid) c[k] = a[j++]; //左边用尽
else if (j > last) c[k] = a[i++]; //右边用尽
else if (a[i] <= a[j]) c[k] = a[i++]; //相等时优先放置前边的,保持稳定性
else c[k] = a[j++]; //求逆序对时只需要加上一句cnt += mid - i + 1; 即可统计得到逆序对的数目(必须以a[j]为准,不能以a[i]为准会产生重复)
for (int k = first; k <= last; ++k) {
a[k] = c[k];
}
}
void MergeSort(int a[], int low, int high, int temp[]) {
if (low < high) {
int mid = low + (high - low) / 2; //避免溢出
MergeSort(a, low, mid, temp);//[low, mid]
MergeSort(a, mid+1, high, temp); //[mid+1, high]
Merge(a, low, mid, high, temp);
}
}
以上所示为自顶向下的归并排序,也可以使用自底向上的归并排序(如下所示),子数组的大小分别为1,2,4,8等。
算法复杂度为N*logN,但是需要O(N)的额外的时间消耗
public static void sort(Comparable[] a) {
int N = a.length;
aux = new Comparable[N];
for (int sz = 1; sz < N; sz = sz + sz) //sz子数组的大小,整体数组的一半
for (int lo = 0; lo < N - sz; lo += sz + sz) //lo子数组索引
merge(a, lo, lo + sz -1, Math.min(lo + sz + sz - 1, N - 1));
}
自底向上的归并排序适合用链表组织的数据,这种方法只需要重新组织链表就能原地排序,不需要创建任何新的链表结构
归并排序的优化:
1. 对小规模子数组使用插入排序
2. 测试数组是否已经排序 如果a[mid] <= a[mid+1],我们就认为已经有序,这样可以直接跳过Merge过程
3. 不将元素复制到辅助数组 需要在递归调用时不断交换输入数组和辅助数组的角色
http://algs4.cs.princeton.edu/22mergesort/MergeX.java.html
private static void sort(Comparable[] src, Comparable[] dst, int lo, int hi) {
// if (hi <= lo) return;
if (hi <= lo + CUTOFF) {
insertionSort(dst, lo, hi);
return;
}
int mid = lo + (hi - lo) / 2;
sort(dst, src, lo, mid);
sort(dst, src, mid+1, hi);
// if (!less(src[mid+1], src[mid])) {
// for (int i = lo; i <= hi; i++) dst[i] = src[i];
// return;
// }
// using System.arraycopy() is a bit faster than the above loop
if (!less(src[mid+1], src[mid])) {
System.arraycopy(src, lo, dst, lo, hi - lo + 1);
return;
}
merge(src, dst, lo, mid, hi); //merge中仅需要将src中合并到有序到dst中,不需要拷贝的动作了
}
非递归的归并排序:
http://www.cnblogs.com/xing901022/p/3671771.html
//桶排序
void BucketSort(int a[], int length, int max) {
int * bucket = new int [max+1];
memset(a, 0, sizeof(bucket));
for (int i = 0; i < length; ++i) bucket[a[i]]++; //计数
for (int i = 0, j = 0; i < max; i++) {
while (bucket[i]-- > 0)
a[j++] = i;
}
}
//基数排序
原理类似桶排序,这里总是需要10个桶,多次使用
每次循环将一位上的数入桶并进行排序
void RadixSort(int a[], int n, int maxd) {
int r = 1;
int * cnt = new int[10];
int * tmp = new int[10];
//maxd为最大的位数,比如3000时 maxd为4
for (int i = 0; i < maxd; ++i) {
for (int j = 0; j < 10; ++j) cnt[j] = 0;
for (int j = 0; j < n; ++j) {
int k = a[j] / r;
int q = k % 10;
cnt[q] ++;
}
for (int j = 1; j < 10; ++j) {
cnt[j] += cnt[j - 1]; //计算累积和,为了计算排名
}
for (int j = n - 1; j >= 0; j--) {
int p = a[j] / r;
int s = p % 10;
tmp[cnt[s] - 1] = a[j];
cnt[s] --;
}
for (int j = 0; j < n; ++j) {
a[j] = tmp[j]; //重排序
}
r *= 10;
}
}
//计数排序
当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。
public static void countsort(int[] input, int[] output, int k) {
// input为输入数组,output为输出数组,k表示有所输入数字都介于0到k之间
int[] c = new int[k];
int len = c.length;
for (int i = 0; i < len; i++) {
c[i] = 0;
}
for (int i = 0; i < input.length; i++) {
c[input[i]]++;
}
for (int i = 1; i < len; i++) {
c[i] = c[i] + c[i - 1];
}
// 把输入数组中的元素放在输出数组中对应的位置上
for (int i = input.length - 1; i >= 0; i--) {
output[c[input[i]] - 1] = input[i];
c[input[i]]--;
}
}
补充证明:基于比较的排序算法的下界
实际上应该是lgN!~nlogN 只不过可以将lgN!近似为nlogN
http://www.cnblogs.com/sanghai/p/6378072.html
(比较)排序算法时间的下界:比较排序算法的复杂度下界是 O(nlog(n))
证明
对于n个待排序元素,在未比较时,可能的正确结果有n!种。
在经过一次比较后,其中两个元素的顺序被确定,所以可能的正确结果剩余n!/2种。
依次类推,直到经过m次比较,剩余可能性n!/(2^m)种。
直到n!/(2^m)<=1时,结果只剩余一种。此时的比较次数m为o(nlogn)次。
所以基于排序的比较算法,最优情况下,复杂度是o(nlogn)的。
因为log(n!)的增长速度与 nlogn 相同,即 log(n!)=Θ(nlogn)
关于快速排序的实现:
1.
2.
我们大多数的实现都是基于第二种改动来的,因为在第2种实现中,我们可以发现其并没有将pivot放入正确的位置,因此出现了递归时左端需要处理p位置的边界情况
两种实现方式的对比:
1. Hoare’s scheme is more efficient than Lomuto’s partition scheme because it does three times fewer swaps on average, and it creates efficient partitions even when all values are equal.
2. Like Lomuto’s partition scheme, Hoare partitioning also causes Quicksort to degrade to O(n^2) when the input array is already sorted, it also doesn’t produce a stable sort.
3. Note that in this scheme, the pivot’s final location is not necessarily at the index that was returned, and the next two segments that the main algorithm recurs on are (lo..p) and (p+1..hi) as opposed to (lo..p-1) and (p+1..hi) as in Lomuto’s scheme.