排序算法
在说排序之前,先介绍一些简单的概念。
- 排序(sort): 设有n个记录$R_1, R_2,…,R_n$,存放于内存或者外存中。排序的任务是找到$(1,2,…,n)$的一个排列$(p_1,p_2,…,p_n)$,使得$\textup{Key}(R_{p_1}) \le \textup{Key}(R_{p_2}) \le … \le \textup{Key}(R_{p_n})$,其中key(R)表示R的关键字。
- 直接排序: 将原待排序的n个记录重新整理为$R_{p_1}, R_{p_2},…,R_{p_n}$,记录被重新移动整理。
- 间接排序: 待排序的n个记录位置不变,而是用额外的的空间存储排列,排列结果是(p1,p2,…pn)。
- 稳定性: 如果排序算法保持排列记录中关键字等价的记录的相对位置,称排序算法是稳定的。
介绍好了这些概念,我们接下来看看一些排序算法。
插入排序
插入排序的基本思想是将待续的区间逐渐扩大。如果想要对区间[first, last)排序,最初,只有一个元素的区间[first, first+1)是已经排序的,如果区间[first, i)已排序,需要将*i元素插入到合适的位置,使区间[first, i]仍是排序的,那么排序区间就增大了。
下面是简单的代码:
// 假设区间[first, i)已排序,
// 将*i元素插入到合适的位置使区间[first, i]仍是排序的
void guarded_linenar_insert(int * first, int * i)
{
const int value = *i;
int * prev = i;
for (--prev; i != first && value < *prev; --prev)
{
*i = *prev;
i = prev;
}
*i = value;
}
// 对区间[first, last)排序
void guarded_linear_sort(int * first, int * last)
{
if (first >= last)
{
return;
}
for (int * i = first; i != last; ++i)
{
guarded_linenar_insert(first, i);
}
}
选择排序
选择排序有两种,一种是最小值选择排序,另一种是最大值选择排序。以最小值选择为例讲解。
最小值选择排序的基本思想是在待排序区间[first, last)中找到最小值,将其交换到first位置,然后对[first+1,last)做同样的处理。
下面是简单的代码:
//假设待排序元素存放于[first,last)中
//函数找出[first,last)中的最小元素,把它交换到first位置上
void min_first(int * first, int * last)
{
int * min_pos = first;
int temp;
for (int * i = first + 1; i < last; ++i)
{
if (*i < *min_pos)
{
min_pos = i;
}
}
temp = *min_pos;
*min_pos = *first;
*first = temp;
}
//使用最小值选择排序,对区间[first, last)排序
void select_sort(int * first, int * last)
{
for (0; last – first > 1; ++first)
{
min_first(first, last);
}
}
Shell排序
Shell排序是对插入排序的改进。增量为h的插入排序,是对数组中下标为a[0],a[h],a[2h],…a[kh]的元素排序。也就是相邻的元素的间隔是h。共有h组。对每一组使用简单插入排序。
Shell排序算法取单调增的增量序列(h0, h1,…,hk),其中hk<n。先对数组做hk排序,再做hk-1排序,依次类推。
实验中较好的增量序列为:
- $h_i=2^{i+1}-1;$
- $h_i=9(4^i-2^i)+1$
下面代码是使用增量为$h_{i+1}=3h_i+1$的增量序列。
//假设待排序元素存放于[first,last)中
//函数安装增量h找出[first,last)中的最小元素,把它交换到first位置上
void min_first_h(int * first, int * last, int h)
{
int * min_pos = first;
int temp;
for (int * i = first + h; i < last; i += h)
{
if (*i < *min_pos)
{
min_pos = i;
}
}
temp = *min_pos;
*min_pos = *first;
*first = temp;
}
//假设待排序元素存放于[first,last)中
//函数安装增量h对子序列[first,first+h,first+2h,...,last)排序
void h_sort(int * first, int * last, int h)
{
for (0; last - first > h; first += h)
{
min_first_h(first, last, h);
}
}
//取增量序列为hi+1 = 3hi + 1,
//对区间[first, last)进行Shell排序
void shell_sort(int * first, int * last)
{
const int n = last - first;
int h = 1;
for (0; h < n; h = 3 * h + 1)
{
;
}
for (h = (h - 1) / 3; h > 0; h = (h - 1) / 3)
{
for (int i = 0; i < h; ++i)
{
h_sort(first + i, last, h);
}
}
}
堆排序
堆排序可以看做是优化后的选择排序。原因是堆排序使用大根堆辅助排序。大根堆满足的性质是任意节点的键值都不小于其左子树所有节点的键值最大值,同时也不小于其右子树所有节点的键值最大值。显然大根堆的堆顶即是堆中所有元素的最大值,可以快速找到最大值。然后就可以用简单的快速排序原理进行排序:
简单代码如下:
//假设(i, last)满足大根堆性质,函数加入元素a[i],
//使得[i, last)满足大根堆
void shift_down(int * a, int i, int last)
{
int max_c = 0;
int temp = 0;
while (true)
{
max_c = 2 * i + 1;
if (max_c >= last)
{
return;
}
if (max_c + 1 < last && a[max_c] < a[max_c + 1])
{
++max_c;
}
if (a[max_c] <= a[i])
{
return;
}
else
{
temp = a[i];
a[i] = a[max_c];
a[max_c] = temp;
i = max_c;
}
}
}
//假设待排序元素存放于a[0, size),函数调整使得a[0,size)满足大根堆性质
void make_heap(int *a, int size)
{
for (int half = (size - 2) / 2; half >= 0; --half)
{
shift_down(a, half, size);
}
}
//假设待排序元素存放于a[0, size),并且已经满足大根堆性质
//函数把堆顶元素a[0]移动到a[size-1],并重新调整,使得
//a[0, size-1)满足大根堆性质
void pop_heap(int *a, int size){
int temp = a[0];
a[0] = a[size - 1];
a[size - 1] = temp;
shift_down(a, 0, size - 1);
}
//假设待排序元素存放于a[0, size),函数使用堆排序对区间[0, size)排序
void heap_sort(int * a, int size)
{
make_heap(a, size);
for (0; size >= 1; --size) {
pop_heap(a, size);
}
}
快速排序
快速排序的基本思想是分治法。快速排序每次选定一个标准,称为枢轴(pivot)p,将待排序对象分为两个部分,[first, mid)和[mid, last),使得[first, mid)中的元素小于p,[mid, last)中的元素大于等于p。通常选取的p是待排序对象的某个元素,这样可以把待排序对象分为三个部分[first, mid), mid和[mid+1, last),其中[first, mid)中的元素小于p,[mid, last)中的元素大于等于p,mid位置的元素等于p。只要递归执行这种划分操作就可以排序了。
下面是简单代码:
//选定first上的元素作为枢轴pivot,将[first, last)划分为3个部分
//[frist,mid)中元素小于pivot,mid位置元素等于于pivot,[mid+1,last)中元素大于等于pivot
//返回mid指针
int * partition(int * first, int * last)
{
if (last - first < 2)
{
return first;
}
const int pivot = *first;
while (true)
{
for (--last; last != first && *last >= pivot; --last)
{
;
}
if (last == first)
{
*first = pivot;
return first;
}
else
{
*first = *last;
}
for (++first; first != last && *first < pivot; ++first)
{
;
}
if (first == last)
{
*last = pivot;
return last;
}
else
{
*last = *first;
}
}
}
//使用快速排序,对区间[first, last)排序
void quick_sort(int * first, int * last)
{
if (last - first < 2)
{
return;
}
int * mid = partition(first, last);
quick_sort(first, mid);
quick_sort(mid + 1, last);
}
归并排序
归并排序的基本操作是将两个已经排好序的子段归并为一个大的排序段。对于数组来说,可以先把数组划分为左右两个部分,分别对两个子段排序后,再归并。这样可以用递归的思想来完成。
下面是归并排序的简单代码:
//将区间[first, last)拷贝到[dest,dest+last-first)
void copy(const int * first, const int * last, int * dest)
{
for (0; first != last; ++first, ++dest)
{
*dest = *first;
}
}
//假设区间[first1, last1)和[first2, last2)都已经排序
//函数将区间[first1, last1)和[first2, last2),合并为一个更大的排序区间
//结果拷贝到[result,result+last1-first1+last2-first2)
void merge(int * result, const int * first1, const int * last1
, const int * first2, const int * last2)
{
for (0; first1 != last1 && first2 != last2; ++result)
{
*result = *first1 < *first2 ? *first1++ : *first2++;
}
if (first1 != last1)
{
copy(first1, last1, result);
}
if (first2 != last2)
{
copy(first2, last2, result);
}
}
//使用归并排序,对区间[first,last)排序。
//区间[temp, temp+last-first)作为辅助空间
//结果保存在区间[first,last)
void merge_sort(int * first, int * last, int *temp)
{
int * mid = first + (last - first) / 2;
if (first >= mid)
{
return;
}
merge_sort(first, mid, temp);
merge_sort(mid, last, temp);
merge(temp, first, mid, mid, last);
copy(temp, temp + (last - first), first);
}
基数排序
基数排序适用于数据为整型或者可以转换为有整型键值的待排序元素。其基本思想是先对待排序区间安照最高位排序,再按次高位排序,一次类推,直到对最末位排序为止。代码略。
各类排序算法性能比较
这才是真正的干货 😺。
排序算法 | 最佳情形 | 最差情形 | 平均 | 空间 | 稳定性 |
---|---|---|---|---|---|
插入排序 | n-1 | n (n-1) / 2 | n (n-1) / 4 | 原地 | 稳定 |
选择排序 | n (n-1) / 2 | n (n-1) / 2 | n (n-1) / 2 | 原地 | 稳定 |
希尔排序 | - | - | O(n5/4) | 原地 | 不稳定 |
堆排序 | O(n)+nlog2n | O(n)+nlog2n | O(n)+nlog2n | 原地 | 不稳定 |
快速排序 | nlog2n | n(n+1)/2 | nlog2n | 原地 | 不稳定 |
归并排序 | nlog2n | nlog2n | nlog2n | O(n) | 稳定 |
基数排序 | O(d(n+radix)) | O(d(n+radix)) | O(d(n+radix)) | O(n) | 稳定 |