希尔排序是直接插入排序的改进,通过引入递减增量,对待排序列分组,并对分组做直接插入排序,重复上述步骤,并逐步减小增量为1,最终使待排序列有序。
相对于直接插入排序O(N^2)的平均时间复杂度,希尔排序的平均时间复杂度为O(nlog2n)。另外希尔排序的时间复杂度和选择增量序列的方式有关,通常有N/2,Hibbard,Sedgewick等方法。
N/2,最坏情况T=Θ(N^2),性能不高的原因是增量序列不互质
Hibbard Dk=2^k-1, Tworst:T=Θ(N^3/2), 猜想:Tavg=O(N^5/4)
2^k-1,k=0,1,2,3,4, 常见序列如{15,7,3,1,0}
Sedgewick,由9*4i-92^i+1 或 4i-3*2i+1获得
最坏:Tworst=O(N^4/3), Tavg=O(N^7/6),常见序列如{929, 505, 209, 109, 41, 19, 5, 1, 0}
本文通过实际测试,直观了解希尔排序设置不同递减增量时排序性能的变化。
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define ElementType int
typedef struct LNode *List;
struct LNode
{
int Data;
List Next;
};
ElementType *initA(int N);
void showA(ElementType A[], int N);
void ShellSort(ElementType A[], int N);
void ShellSortWithSedgewick(ElementType A[], int N);
void ShellSortWithHibbard(ElementType A[], int N);
List FindKth(List L, int K);
List Insert(List L, int K, ElementType X);
void showList(List L);
List initList(int N);
double run(int N, int num, int flag);
int main()
{
srand(time(0));
int N = 10000;
int num = 100;
int flag = 0;
// ElementType *A = initA(N);
// showA(A, N);
// // ShellSort(A, N);
// // ShellSortWithSedgewick(A, N);
// ShellSortWithHibbard(A, N);
// showA(A, N);
printf("%6.2e", run(N, num, flag));
return 0;
}
ElementType *initA(int N)
{
ElementType *A = (ElementType *)malloc(N * sizeof(ElementType));
// ElementType B[] = {5, 4, 3, 2, 1};
for (int i = 0; i < N; i++)
{
if (N < 100)
{
A[i] = rand() % 100;
// A[i] = B[i];
}
else
{
A[i] = rand() % N; // 随机
// A[i] = 1; // 全1
// A[i] = i;//顺序
// A[i] = N - 1 - i; // 逆序
}
}
return A;
}
void showA(ElementType A[], int N)
{
for (int i = 0; i < N; i++)
{
printf("%d ", A[i]);
}
printf("\n");
}
void ShellSort(ElementType A[], int N)
{ // 原始希尔排序,增量使用N/2
int D, P, i;
ElementType tmp;
for (D = N / 2; D > 0; D /= 2)
{
for (P = D; P < N; P++)
{
tmp = A[P];
for (i = P; i >= D && A[i - D] > tmp; i -= D)
{
A[i] = A[i - D];
}
A[i] = tmp;
}
}
}
void ShellSortWithSedgewick(ElementType A[], int N)
{ /* 希尔排序 - 用Sedgewick增量序列 */
int Si, D, P, i;
ElementType tmp;
/* 这里只列出一小部分增量 */
int Sedgewick[] = {929, 505, 209, 109, 41, 19, 5, 1, 0};
for (Si = 0; Sedgewick[Si] >= N; Si++)
; /* 初始的增量Sedgewick[Si]不能超过待排序列长度 */
for (D = Sedgewick[Si]; D > 0; D = Sedgewick[++Si])
for (P = D; P < N; P++)
{ /* 插入排序*/
tmp = A[P];
for (i = P; i >= D && A[i - D] > tmp; i -= D)
A[i] = A[i - D];
A[i] = tmp;
}
}
void ShellSortWithHibbard(ElementType A[], int N)
{
int Hi, D, P, i;
ElementType tmp;
int Hibbard[] = {15, 7, 3, 1, 0};
for (Hi = 0; Hibbard[Hi] >= N; Hi++)
;
for (D = Hibbard[Hi]; D > 0; D = Hibbard[++Hi])
{
for (P = D; P < N; P++)
{
tmp = A[P];
for (i = P; i >= D && A[i - D] > tmp; i -= D)
{
A[i] = A[i - D];
}
A[i] = tmp;
}
}
}
List FindKth(List L, int K)
{
int loc = 1;
List S = L;
for (; S; S = S->Next)
{
if (K == loc)
{
break;
}
else
{
loc++;
}
}
return S;
}
List Insert(List L, int K, ElementType X)
{
List S, P;
if (K == 1)
{
S = (List)malloc(sizeof(struct LNode));
S->Next = L;
S->Data = X;
return S;
}
P = FindKth(L, K - 1);
if (P != NULL)
{
S = (List)malloc(sizeof(struct LNode));
S->Data = X;
S->Next = P->Next;
P->Next = S;
return L;
}
else
{
printf("Error:Index %d out of range.\n", K);
return NULL;
}
}
void showList(List L)
{
List S = L->Next;
if (S)
{
for (; S; S = S->Next)
{
printf("%d ", S->Data);
}
printf("\n");
}
}
List initList(int N)
{
int i, K = 1;
List L = NULL;
for (i = 0; i < N; i++)
{
if (N < 100)
{
L = Insert(L, K, rand() % 100);
}
else
{
L = Insert(L, K, rand() % N);
// L = Insert(L, K, 1);
// L = Insert(L, K, i);
// L = Insert(L, K, N-1-i);
}
}
List H = (List)malloc(sizeof(struct LNode));
H->Next = L;
return H;
}
double run(int N, int num, int flag)
{
clock_t start, stop;
double duration, sum = 0.0;
for (int i = 0; i < num; i++)
{
if (flag == 0)
{
int *A = initA(N);
start = clock();
// ShellSort(A, N);
// ShellSortWithSedgewick(A, N);
ShellSortWithHibbard(A, N);
stop = clock();
}
else
{
List L = initList(N);
start = clock();
//
stop = clock();
}
duration = ((double)(stop - start)) / CLK_TCK;
sum += duration;
}
return sum / num;
}
测试逻辑:
对3种算法按设定数据规模,重复次数,数据样本执行,取单次运行的花费的时间。
实测结果:
/*
性能实测:
10000,100,随机数
数组:5.23e-01[bubble],1.56e-01[插入],3.40e-03[shell]
链表:1.65e+00[bubble],3.05e-01[插入]
N/2
100000,100,随机数 4.91e-02
100000,100,全1 1.25e-02
100000,100,顺序 1.16e-02
100000,100,逆序 1.58e-02
Sedgewick
100000,100,随机数 2.86e-03
100000,100,全1 6.10e-04
100000,100,顺序 5.60e-04
100000,100,逆序 1.70e-03
Hibbard
100000,100,随机数 1.23e-02
100000,100,全1 3.10e-04
100000,100,顺序 3.40e-04
100000,100,逆序 2.17e-02
*/
测试结果分析:
首先用希尔排序和冒泡,插入排序性能做了一个横向比较,测试样本为
10000随机数,运行100次,取平均单次运行时间
冒泡和插入平均时间复杂度是O(N^2),从测试结果可见,排序性能比希尔低2个数量级。
10000,100,随机数
数组:5.23e-01[bubble],1.56e-01[插入],3.40e-03[shell]
链表:1.65e+00[bubble],3.05e-01[插入]
其次是希尔不同增量序列选取的排序性能对比:
分别是N/2,Sedgewick,Hibbard。数据规模100,000[10万],重复运行100次,数据特征包括随机数,全1,顺序,逆序4种。
从测试结果看,选取不同的增量序列,对排序性能影响巨大,以N/2和Sedgewick随机数测试结果为例,N/2是【4.91e-02】
Sedgewick是【2.86e-03】,差距在一个数量级。另外,不同数据特征也对排序性能有巨大影响。同样是Sedgewick,全1【6.10e-04】和随机数【2.86e-03】相比,也有一个数量级的差别。
N/2
100000,100,随机数 4.91e-02
100000,100,全1 1.25e-02
100000,100,顺序 1.16e-02
100000,100,逆序 1.58e-02
Sedgewick
100000,100,随机数 2.86e-03
100000,100,全1 6.10e-04
100000,100,顺序 5.60e-04
100000,100,逆序 1.70e-03
Hibbard
100000,100,随机数 1.23e-02
100000,100,全1 3.10e-04
100000,100,顺序 3.40e-04
100000,100,逆序 2.17e-02
小结:
从测试结果看,希尔排序选取Sedgewick增量序列的性能最好,实现复杂度也没有提高,实际应用中希尔排序,应采用Sedgewick增量序列。