本文参考Weiss的数据结构与算法分析一书,将详细解释常用的以下几种排序算法:
- 插入排序
- 希尔排序
- 堆排序
- 归并排序
- 快速排序
- 桶式排序
首先,我们约定所有的排序算法都接收一个含有元素的数组和一个包含元素个数的整数。函数原型如下:
void Sort(ElementType A[], uint64_t N);
其中A是待排序数组的首地址,N是元素个数。
一、插入排序
插入排序,顾名思义,就是把元素插入到某个合适的位置,插入排序的原理很简单,其实就是从第二个元素开始,把每个元素放到前面数组中合适的位置上。插入排序由N-1趟排序组成,对于 P=1 趟到 P=N-1 趟,插入排序保证从位置0到位置P上的元素为已排序的状态。
我们用以下这个表格来表达排序过程:
初始 | 12 | 81 | 65 | 70 | 10 |
---|---|---|---|---|---|
第一趟排序后 | 12 | 81 | 65 | 70 | 10 |
第二趟排序后 | 12 | 65 | 81 | 70 | 10 |
第三趟排序后 | 12 | 65 | 70 | 81 | 10 |
第四趟排序后 | 10 | 12 | 65 | 70 | 81 |
表中加粗的元素表示被移动的元素。
第一趟:移动第二个元素 81 ,81>12,停止。
第二趟:移动第三个元素 65 ,65<81, 于是往前挪一个位置,65>12,停止。
第三趟:移动第四个元素 70 ,70<81, 于是往前挪一个位置,70>65,停止。
第四趟:移动第五个元素 10 ,10<81,往前挪一个位置,10<70,往前挪,10<65往前挪,10<12,往前挪,此时已经到位置 0 上,停止。
插入排序的实现很简单,以下是代码:
void InsertionSort(ElementType A[], uint64_t N) //选择排序
{
uint64_t j, P;
ElementType Tmp;
for (P = 1; P < N; P++)
{
Tmp = A[P];
for (j = P; (j>0) && (A[j - 1] > Tmp); j--)
{
A[j] = A[j - 1];
}
A[j] = Tmp;
}
}
在上面的描述中我用“往前挪”,而实现的时候没有使用Swap函数交换两个元素,而是首先将A[P]赋值给Tmp,然后如果需要往前挪,就直接用前一个元素将其覆盖掉,最后将Tmp赋值给合适的位置,即 A[j] = Tmp 。这样做的目的是避免显示的交换,从而提高效率
二、希尔排序
希尔排序(Shellsort)的名称源于它的发明者Donald Shell,它通过比较相距一定间隔的元素来工作,各趟比较所用的距离随着算法的进行而缩小,直到只比较相邻元素的最后一趟排序为止。由于这个原因,希尔排序有时候也叫缩小增量排序(diminishing increment sort)。
我们把 h1,h2,...,ht 叫做增量序列(increment sequence)。只要 h1 =1,任何增量序列都是可行的。在使用增量 hk 的一趟排序后,对于每一个i有A[i]<=A[i+{h_k}],即所有相隔 hk 的元素都被排序。此时称数组是 hk 排序的。
有两点需要说明,
-
h1
必须要等于1,假设
h1
=2,那么最后排序的结果是:奇数位置上的排序,偶数位置上的排序,而整体未必排序。
- 一个
hk
排序数组保持它的
hk
排序性,也就是说,后面的排序不能破坏前面的排序结果。
现在,我们以增量序列1,3,5来演示希尔排序:
位置 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
初始 | 81 | 94 | 11 | 96 | 12 | 35 | 17 | 95 | 28 | 58 | 41 | 75 | 15 |
5-排序后 | 35 | 17 | 11 | 28 | 12 | 41 | 75 | 15 | 96 | 58 | 81 | 94 | 95 |
3-排序后 | 28 | 12 | 11 | 35 | 15 | 41 | 58 | 17 | 94 | 75 | 81 | 96 | 95 |
1-排序后 | 11 | 12 | 15 | 17 | 28 | 35 | 41 | 58 | 75 | 81 | 94 | 95 | 96 |
5-排序后,位置0,5,10;1,6,11;2,7,12;3,8;4,9上的元素分别被排序;
3-排序后,位置0,3,6,9,12;1,4,7,10;2,5,8,11上的元素分别被排序;
1-排序后,位置0,1,2,3,4,5,6,7,8,9,10,11,12上的元素被排序。
增量序列的一种流行选择是使用希尔建议的序列: ht=[N/2] 和 hk=hk+1/2 (这不是最好的增量序列)。其中一趟 hk 排序的作用就是对独立的 hk 个子数组进行排序,与插入排序一样,为了性能以下是代码:
void ShellSort(ElementType A[], uint64_t N) //希尔排序
{
uint64_t i, j, Increment;
ElementType Tmp;
for (Increment = N / 2; Increment > 0; Increment /= 2) //此处使用希尔增量,但不是最好的增量序列
{
for (i = Increment; i < N; i++)
{
Tmp = A[i];
for (j = i; j >= Increment; j -= Increment) //每次for就是一次插入排序
{
if (A[j - Increment] > Tmp)
A[j] = A[j - Increment];
else
break;
}
A[j] = Tmp;
}
}
}
三、堆排序
建立N个元素的二叉堆的花费是O(N),然后我们执行N次出队操作(DeleteMin),而堆的一个重要特性堆序性保证了出队元素是堆中最小的,我们按顺序把这些出队的元素拷贝到一个额外的数组,然后在将数组中数据拷贝回来,就能得到N个元素的排序。每个DeleteMin操作花费的时间是O(logN),所以总的运行时间是O(NlogN)。
由于上述算法需要一个额外的数组来存储出队的元素,因此存储需求增加一倍。有种巧妙的办法可以解决这个问题:每次DeleteMin之后堆缩小了1,因此位于堆中最后的单元可以用来存储刚刚出队的元素。比如一个含有六个元素的堆,第一次出队的元素是A1,我们把A1放在位置6上,第二次出队的元素是A2,我们把它放在位置5上。
使用这种策略,在最后一次DeleteMin后,该数组将以递减的顺序包含这些元素,如果我们希望以递增顺序包含这些元素,那我们可以每次都做DeleteMax操作,也就是说,堆的第一个元素不是堆中最小,而是堆中最大的,这在实现时只要在堆的基础上稍作改动即可。
(未完待续。。。)
先把代码附上:
#include "sort.h"
void InsertionSort(ElementType A[], uint64_t N) //选择排序
{
uint64_t j, P;
ElementType Tmp;
for (P = 1; P < N; P++)
{
Tmp = A[P];
for (j = P; (j>0) && (A[j - 1] > Tmp); j--)
{
A[j] = A[j - 1];
}
A[j] = Tmp;
}
}
void ShellSort(ElementType A[], uint64_t N) //希尔排序
{
uint64_t i, j, Increment;
ElementType Tmp;
for (Increment = N / 2; Increment > 0; Increment /= 2) //此处使用希尔增量,但不是最好的增量序列
{
for (i = Increment; i < N; i++)
{
Tmp = A[i];
for (j = i; j >= Increment; j -= Increment) //每次for就是一次插入排序
{
if (A[j - Increment] > Tmp)
A[j] = A[j - Increment];
else
break;
}
A[j] = Tmp;
}
}
}
#define LeftChild(i) (2*(i)+1) //与二叉堆的不同之处在于,二叉堆序号以1开始,而此处以0开始,所以左孩子是2*i+1
static void PerDown(ElementType A[], uint64_t i, uint64_t N) //“下滤”函数,将 堆A 中第i元素下滤
{
uint64_t Child; //该变量相当于堆中“空穴”位置
ElementType Tmp;
//判断条件很重要!!!
for (Tmp = A[i]; LeftChild(i) < N; i = Child) //判断左孩子是否存在
{
Child = LeftChild(i);
//这里的判断条件很重要!!!
if (Child != N - 1 && A[Child + 1] > A[Child]) //判断顺序一定不能反过来,前一个判断确定是否有右孩子
Child++;
if (Tmp < A[Child]) //将较大的值往上挪
A[i] = A[Child];
else
break;
}
A[i] = Tmp;
}
//交换元素
static void Swap(ElementType *Ele1, ElementType *Ele2)
{
ElementType Tmp;
Tmp = *Ele1;
*Ele1 = *Ele2;
*Ele2 = Tmp;
}
void HeapSort(ElementType A[], uint64_t N)
{
for (int64_t i = N / 2; i >= 0; i--) //构建二叉堆
{
PerDown(A, i, N);
}
for (uint64_t i = N - 1; i > 0; i--)
{
Swap(&A[0], &A[i]); //直接利用堆的内存空间,这样做节省内存
PerDown(A, 0, i); //依次将最大值放在位置“0”处
}
}
//和并两数组 这里才是归并排序的核心
static void Merge(ElementType A[], ElementType TmpArr[], uint64_t LeftPos, uint64_t RightPos, uint64_t RightEnd)
{
uint64_t LeftEnd = RightPos - 1;
uint64_t TmpPos = LeftPos;
uint64_t ElementsNum = RightEnd - LeftPos + 1;
while (LeftPos <= LeftEnd && RightPos <= RightEnd)
{
if (A[LeftPos] < A[RightPos])
{
TmpArr[TmpPos++] = A[LeftPos++];
}
else
{
TmpArr[TmpPos++] = A[RightPos++];
}
}
while (LeftPos <= LeftEnd)
{
TmpArr[TmpPos++] = A[LeftPos++];
}
while (RightPos <= RightEnd)
{
TmpArr[TmpPos++] = A[RightPos++];
}
for (uint64_t i = 0; i < ElementsNum; i++)
{
A[RightEnd] = TmpArr[RightEnd];
RightEnd--;
}
}
static void Msort(ElementType A[], ElementType TmpArr[], uint64_t Left, uint64_t Right)
{
uint64_t Center;
if (Left < Right)
{
Center = (Left + Right) / 2;
Msort(A, TmpArr, Left, Center);
Msort(A, TmpArr, Center + 1, Right);
Merge(A, TmpArr, Left, Center + 1, Right);
}
}
void MergeSort(ElementType A[], uint64_t N) //归并排序
{
ElementType *TmpArr;
TmpArr = malloc(N*sizeof(ElementType));
if (NULL == TmpArr)
{
printf("Out of space!\r\n");
return;
}
Msort(A, TmpArr, 0, N - 1);
free(TmpArr);
}
//三值中数法选择枢纽元
//将数组A中 Left位置 Right位置 Center位置上数据的
//中值移到倒数第二个位置,最小的移到最左、最大的移到最右
//并返回中值
static ElementType Median3(ElementType A[], uint64_t Left, uint64_t Right)
{
uint64_t Center = (Left + Right) / 2;
if (A[Left] > A[Center])
Swap(&A[Left], &A[Center]);
if (A[Left] > A[Right])
Swap(&A[Left], &A[Right]);
if (A[Center] > A[Right])
Swap(&A[Center], &A[Right]);
//A[Left] <= A[Center] <= A[Right]
Swap(&A[Center], &A[Right - 1]);
//将中间的中值移到倒数第二的位置上
return A[Right-1];
}
#define Cufoff (3)
static void Qsort(ElementType A[], uint64_t Left, uint64_t Right)
{
uint64_t i, j;
ElementType Pivot;
if (Left + Cufoff <= Right)
{
//枢纽元的选择也可以随机选择,但是不能选择第一个元素!
Pivot = Median3(A, Left, Right);
i = Left;
j = Right - 1;
while (true)
{
while (A[++i] < Pivot);
while (A[--j] > Pivot);
if (i < j)
Swap(&A[i], &A[j]);
else
break;
}
Swap(&A[i], &A[Right - 1]);
Qsort(A, Left, i - 1);
Qsort(A, i + 1, Right);
}
else
InsertionSort(A + Left, Right - Left + 1);
}
void QuickSort(ElementType A[], uint64_t N)
{
Qsort(A, 0, N - 1);
}
//桶式排序 MaxElement太大的话不能用 否则内存中中装不下这么大的桶
void BucketSort(ElementType A[], uint64_t N, ElementType MaxElement)
{
uint16_t *Bucket;
Bucket = malloc(MaxElement*sizeof(uint16_t));
if (NULL == Bucket)
{
printf("Max element is too big,I can't finish!\r\n");
return;
}
memset(Bucket, 0, MaxElement*sizeof(uint16_t));
for (uint64_t i = 0; i < N; i++)
{
Bucket[A[i]]++;
}
uint64_t Pos = 0;
for (uint64_t i = 0; i < MaxElement; i++)
{
while (0 != (Bucket[i]--))
{
A[Pos++] = i;
}
}
}