数据结构【第十八天】:排序(希尔排序、堆排序、归并排序、快速排序)

目录

希尔排序

堆排序

堆排序算法

复杂度分析

归并排序

递归实现

复杂度分析

非递归实现

快速排序

代码实现

复杂度分析

快速排序优化


希尔排序

基本思想:采用跳跃分割的方式,将相距某个增量的记录组成一个子序列,这样才能保证子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。随着增量减少,最终实现整个序列有序。

//对顺序表L做希尔排序
void ShellSort(SqList *L)
{
    int i,j;
    int increment = L->length;
    do 
    {
        increment = increment/3+1;    //增量序列,即间隔,不断变小直至1
        for(i = increment+1; i<L->length; i++)
        {
            if(L->r[i]<L->r[i-increment])    //需要将r[i]插入到间隔为increment的子序列中
            {
                L->r[0] = L->r[i];    //暂存L->r[0]
                for(j = i-increment; j>0&&L->r[j]>L->r[0];j-=increment)
                    L->r[j+increment] = L->r[j];    //记录后移,找到插入的位置
                L->r[j+increment] = L->r[0];    //插入
            }
        }
    }
    while(increment>1);
}

可见希尔排序主要是利用间隔量将序列分割成几个子序列,然后进行直接插入排序,然后减小增量再次排序,直至最后增量为1时再次排序(需要交换的数据变少),实现排序效率的提高。

增量的选取十分关键,研究表明选取可以获得不错的效率。其时间复杂度为O(n^(3/2)),要好于直接排序的O(n^2)。注意增量序列的最后一个增量必须是1才行,同时由于序列记录是跳跃式的移动,因此希尔排序不是稳定排序算法。

堆排序

堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆 (左图所示) ; 或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆(右图所示)

如果按照层序遍历的方式给结点从1开始编号,则结点之间满足如下关系

同样,将大顶堆和小顶堆用层序遍历存入数组(0保留,从1开始),也一定满足上述关系。

堆排序算法

基本思想(假设大顶堆):将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根结点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值) 。然后将剩余的 n - 1 个序列重新构造成一个堆,这样就找到n个元素中的次小值。如此反复执行,便能得到一个有序序列了。

整个排序过程中有两个重要点:

1.将无序序列构建成一个堆

2.在输出堆顶元素后,调整剩余元素成为一个新的堆

//对顺序表L进行堆排序
void HeapSort(SqList *L)
{
    int i;
    for(i = L->length/2; i>0; i--)    //将L->r构成一个大顶堆 
                                      //i从最后一个非叶子节点开始构建
       HeapAdjust(L,i,L->length);
    for(i = L->length; i>1; i--)
    {
        swap(L,1,i);    //将堆顶记录和和未经排序子序列最后一个记录交换
        HeapAdjust(L,1,i-1);    //将L->[1,...,i-1]重新调整为大顶堆
    }
}


//已知L->r[s...m]中记录的关键字除L->r[s]之外均满足堆的定义
//调整L->r[s]的关键字,使L->r[s...m]成为一个大顶堆
void HeapAdjust(SqList *L,int s,int m)
{
    int temp,j;
    temp = L -> r[s];    //保存待调整值
    for(j=2*s;j<=m;j*=2)    //沿着关键字较大的孩子节点向下筛选
    {
        if(j<m && L->r[j]<L->r[j+1])    //j<m说明j不是最后一个结点,且左孩子小于右孩子
            ++j;            //j为关键字中较大的记录的下标
        if(temp >= L->r[j])  //r[s]比它两个孩子都大,则应该插在s位置上
            break;
        L->r[s] = L->r[j];   //s位置应插入原本较大的孩子
        s = j;               //更新s的下标,挪至其原本较大的孩子处
    }
    L->r[s] = temp;        //s位置插入原数据
}

复杂度分析

其运行时间均消耗在初始构建堆和重建堆石的反复筛选上,整个堆的构造时间复杂度为O(n),重建堆的时间复杂度为O(nlogn),所以堆排序的总体时间复杂度为O(nlogn),且对原始记录的排序状态不敏感。

空间复杂度上,它只需要一个用来交换的暂存单元,但是是一种不稳定的排序方法,且不适合待排序序列个数较少的情况

归并排序

基本思想:假设初始序列含有 n 个记录 , 则可以看成是 n 个有序的子序列,每个子序列的长度为1 ,然后两两归并,得到 [n/2]([x]表示不小于 x 的最小整数)个长度为2或1的有序子序列;再两两归并,……,如此重复,直至得到一个长度为 n 的有序序列为止 ,这种排序方法称为 2 路归并排序。

递归实现

//封装成与之前一样
void MergeSort(SqList *L)
{
    MSort(L->r,L->r,1,L->length);
}

//将SR[s...t]归并排序为TR1[s...t]
void MSort(int SR[],int TR1[],int s,int t)
{
    int m;
    int TR2[MAXSIZE+1];
    if(s==t)
        TR1[s] = SR[s];
    else
    {
        m = (s+t)/2;    //将SR[s...t]评分为SR[s...m]和SR[m+1...t]
        MSort(SR,TR2,s,m);    //递归将SR[s...m]归并为有序TR2[s...m]
        MSort(SR,TR@,m+1.t);    //递归将SR[m+1...t]归并为有序TR2[m+1...t]
        Merge(TR2,TR1,s,m,t);    //将TR2[s...m]和TR2[m+1...t]归并到TR1[s...t]    
    }
}

//将有序的SR[i...m]和SR[m+1...n]归并为有序的TR[i...n]
void Merge(int SR[],int TR[],int i,int m,int n) //i\m\n分别为头、中间、尾
{
    int j,k,l;
    for(j=m+1,k=i; i<=m && j<=n; k++)    //将SR中的记录由小到大归并如TR
    {
        if(SR[i]<SR[j])        //前半段的较小,存入TR[k],下标++
            TR[k] = SR[i++];
        else
            TR[k] = SR[j++];   //后半段较小,存入TR[k],下标++
    }
    if(i<=m)        //前半段有剩余
    {
        for(l=0;l<+m+1;l++)
            TR[k+1] = SR[i+1];    //将剩余的SR[i...m]复制到TR
    }
    if(j<=n)       //后半段有剩余
    {
        for(l=0;l<=n-j;l++)
            TR[k+1] = SR[j+1];    //将剩余的SR[j...n]复制到TR
    }
}

Msort函数的数据交换示意图如下:

复杂度分析

其空间复杂度需要与原始记录序列同样数量的存储空间存放归并结果以及递归时的栈空间。空间复杂度为O(n+logn)。但是其为两两排序,故归并排序是一种比较占用内存,但却效率高且稳定的算法。

非递归实现

void MergeSort2(SqList *L)
{
    int* TR = (int*)malloc(L->length * sizeof(int));    //申请等量长度的空间
    int k = 1;
    while(k<L->length)
    {
        MergePass(L->r,TR,k,L->length);
        k = 2*k;    //子序列长度加倍
        MergePass(TR,L->r,k,L->length);
        k = 2*k;
    }
}

//将SR[]中相邻长度为s的子序列两两归并到TR[]
//s为子序列长度 n为序列总长度
void MergePass(int SR[], int TR[], int s,int n)
{
    int i = 1;
    int j;
    while(i<=n-2*s+1)    //子序列长度不到总长度的一半,仍然可以归并
    {
        Merge(SR,TR,i,i+s-1,i+2*s-1);    //两两归并
        i = i + 2*s;    
    }
    if(i<n-s+1)        //归并最后两个序列赋值给TR 即有一个单出来了
        Merge(SR,TR,i,i+s-1,n);
    else            //恰好排序完毕,将有序数据赋值给TR
        for(j=i;j<=n;j++)
            TR[j] = SR[j];
}

非递归的方法,避免了递归时的栈空间损耗,只用到了归并时的临时数组,空间复杂度为O(n)。可以说,归并排序时,尽量使用非递归的方法。

快速排序

基本思想:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。

代码实现

//快速排序,统一格式
void QuickSort(SqList *L)
{
    QSort(L,1,L->length);
}

//对顺序表L中的子序列L->r[low...high]作快速排序
void QSort(SqList *L,int low,int high)
{
    int pivot;
    if(low<high)
    {
        pivot = Partition(L,low,high);    //将L->r[low...high]一分为二,计算枢轴值pivot
        QSort(L,low,pivot-1);        //对低子表进行递归排序
        QSort(L,pivot-1,high);       //对高子表进行递归排序
    }
}

其中Partition函数要做的就是选取一个关键字,将其放在一个位置,使得其左边的值都比它小,而右边的值都比它大。这样的关键字称为枢轴。

//交换顺序表之中子表的记录,使枢轴记录到位,并返回其所在位置
//此时该关键字在它之后的记录均大于它,在它之前的记录均小于它
//下述的枢轴选取的是第一个元素
int Partition(SqList *L,int low,iny high)
{
    int pivotkey;
    pivotkey = L->r[low];
    while(low<high)
    {
        while(low<high && L->r[high]>=pivotkey)
            high--;
        swap(L,low,high);    //将比枢轴记录小的记录交换到低端
        while(low<high && L->r[low]<=pivotkey)
            low--;
        swap(L,low,high);    //将比枢轴记录大的记录交换到高端
    }
    return low;        //返回枢轴所在的位置
}

复杂度分析

时间复杂度最好为O(nlogn),最坏的情况为O(n^2),平均情况为O(nlogn)

空间复杂度最好为O(logn),最坏的情况为O(n),平均情况为O(logn)

快速排序优化

1.优化选取枢轴

上述代码的枢轴选取的是第一个元素,如果这个数值的选取不合适(过大或者过小)就会导致性能的高低。于是提出了以下方法:

(1)随机选取:在r[low]和r[high]之间随机抽取一个值

(2)三数取中法:取三个关键字先进行排序,将中间数作为枢轴, 一般是取左端、右端和中间三个数,也可以随机抽取三个数

(3)九数取中法:与(2)类似

2.优化不必要的交换

关键字的位置没必要一直变化,只要最后更新即可,于是代码优化如下

//交换顺序表之中子表的记录,使枢轴记录到位,并返回其所在位置
//此时该关键字在它之后的记录均大于它,在它之前的记录均小于它
//下述的枢轴选取的是第一个元素
int Partition(SqList *L,int low,iny high)
{
    int pivotkey;
    pivotkey = L->r[low];
    L->r[0] = pivotkey;    //枢轴关键字备份到r[0] 
    while(low<high)
    {
        while(low<high && L->r[high]>=pivotkey)
            high--;
        L->r[low] = L->r[high];    //将原本的交换改为替换操作
        while(low<high && L->r[low]<=pivotkey)
            low--;
        L->r[high] = L->r[low]; 
    }
    L->r[low] = L->r[0];    //将枢轴数值替换回L.r[low]
    return low;        //返回枢轴所在的位置
}

3.优化小数组时的排序方案

当数据量较小的时候,使用简单的插入排序方式反而更加有效率,于是代码改进如下:

#define MAX_SORT_FLAG 7    //数组长度阈值
void QSort(SqList *L,int low,int high)
{
    int pivot;
    if((high-low)>MAX_SORT_FLAG)    //大于阈值,快速排序
    {
        pivot = Partition(L,low,high);    //将L->r[low...high]一分为二,计算枢轴值pivot
        QSort(L,low,pivot-1);        //对低子表进行递归排序
        QSort(L,pivot-1,high);       //对高子表进行递归排序
    }
    else
        InsertSort(L);    //插入排序    
}

4.优化递归操作

使用尾递归的方式减小递归对空间和时间的损耗

#define MAX_SORT_FLAG 7    //数组长度阈值
void QSort1(SqList *L,int low,int high)
{
    int pivot;
    if((high-low)>MAX_SORT_FLAG)    //大于阈值,快速排序
    {
        while(low<high)
        {
            pivot = Partition(L,low,high);    //将L->r[low...high]一分为二,计算枢轴值pivot
            QSort(L,low,pivot-1);       //对低子表进行递归排序
            low = pivot + 1;            //尾递归
        }
    }
    else
        InsertSort(L);    //插入排序    
}

总结

从综合各项指标来说,经过优化的快逮排序是性能最好的排序算法,但是不同的场合,我们也应该考虑使用不同的算法来应对它 。
 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值