排序(二)

数据存储结构体,这里默认数据都是从下标1开始存储

const static int MAX = 1024;
// 存储数据的机构体
typedef struct
{
    int r[MAX];
    int length;
}SqList;

交换函数

// 交换两个数
void swap(SqList *L, int i,int j)
{
    int tmp = L->r[i];
    L->r[i] = L->r[j];
    L->r[j] = tmp;
}

1.希尔排序

希尔排序其实就是插入排序的改进,插入排序可以看做每次比较的步长为1,而希尔排序设置一个初始的步长,步长迭代缩减,最后以1结束。希尔排序的复杂度主要和设置这个初始步长有关。当步长选择的比较差时复杂度就接近插入排序的复杂度。

这是插入排序的代码

void InsertSort(SqList *L)
{
    int i,j;
    for(i=2;i<=L->length;i++)// 从第二个数开始依次比较
    {
        if(L->r[i]<L->r[i-1])
        {
            L->[0] = L->r[i];// 将当前数记录在r[0]位置
            // 循环和前边数比较,确定应该插入的位置,并将该位置后边的数都后移一位
            for(j=i-1; L->r[j]>L->r[0];j--)
                L->[j+1] = L->r[j];
            L->r[j+1] = L->r[0];// 将该数插入对应位置
        }
    }
}

希尔排序则需要更改初始的比较步长,并在外部加一个循环,用来迭代步长,使步长最终减小为1.
希尔排序

void ShellSort(SqList *L)
{
    int i,j;
    int step = L->length;
    do{
        step = step/3+1;
        for(i=step+1;i<=L->length;i++)// 从第step+1个数开始依次比较
        {
            if(L->r[i]<L->r[i-step])// 比较当前数和它前step位置的数大小
            {
                L->[0] = L->r[i];// 将当前数记录在r[0]位置
                // 循环和前边相隔step个数比较,确定应该插入的位置
                for(j=i-step; j>0 && L->r[j]>L->r[0];j-=step)
                    L->[j+step] = L->r[j];// 每次找到的比它大的数移动到后边位置
                L->r[j+step] = L->r[0];// 将该数插入对应位置
            }
        }
    }
    while(step>1);
}

步长选取是希尔排序的关键,希尔排序是不稳定的排序算法,而且必须保证最后一次循环步长以1结束。
最好时间复杂度O(n),最坏O(n^2)

2.堆排序

堆是具有下列性质的完全二叉树:

  • 每个节点的值都大于或等于其左右孩子的节点值,称为大顶堆;
  • 每个节点的值都小于等于其左右孩子的节点值,称为小顶堆;

大顶堆和小顶堆使用层序遍历按顺序存储在数组中。
堆排序:利用大顶堆排序,首先将待排序的序列构造成一个大顶堆。此时堆顶元素就为最大的,将堆顶元素和末尾元素交换,然后调整前n-1个数为大顶堆,再将堆顶元素交换到n-1位置,继续将前n-2个数构调整成大顶堆。如此反复执行便能得到一个有序序列。
算法实现时主要需要解决两个问题:

  • 由原始序列构建一个大顶堆
  • 输出堆顶元素后,调整剩余元素为一个大顶堆

堆排序的代码如下

// 堆排序
void HeapSort(SqList *L)
{
    int i = 0;
    // 由初始序列构建大顶堆,从非叶子节点开始,从下到上调整每个非叶子结点
    for (i = L->length / 2; i > 0; i--)
    {
        HeapAdjust(L, i, L->length);
    }
    // 依次取出堆顶元素调整大顶堆
    for (i = L->length; i > 1; i--)
    {
        swap(L, 1, i);      // 将堆顶元素换到末尾
        HeapAdjust(L, 1, i - 1);    // 将堆的元素个数进行-1,并将新交换的根节点重新调整堆为大顶堆
    }
}

这里两个for循环,第一个是由初始给的数组序列构建一个大顶堆,第二个是每次讲大顶堆堆顶元素和末尾元素交换,并调整剩余元素为一个大顶堆。
其中void HeapAdjust(SqList *L, int s,int length)函数是初始假设L中除s节点外都满足大顶堆条件,然后从s节点开始调整为一个合法的大顶堆,将s节点与左右孩子进行比较,如果孩子比节点s值大,则将较大的孩子和s交换位置,继续判断交换后的s节点和他的子节点,代码如下

// 调整大顶堆
void HeapAdjust(SqList *L, int s,int length)
{
    // 初始假设L中除s节点外都满足大顶堆条件
    int i = 0;          
    int tmp = L->r[s];
    for (i = 2 * s; i < length;i*=2)
    {
        // 判断i是否出界而且判断左右孩子哪个比较大,让i指向大的孩子
        if (i < length && L->r[i] < L->r[i + 1])
            i++;
        // 如果子结点的最大值小于需要判断的初始节点值,则表示初始节点放在该位置满足大顶堆条件,直接跳出
        if (L->r[i] < tmp)
            break;
        // 将较大孩子的值赋给父节点
        L->r[s] = L->r[i];
        // 继续判断当前改动的节点对于他的孩子是否满足大顶堆条件,即记录下当前改动的节点
        s = i;
    }
    // 将初始节点值放到对应位置
    L->r[s] = tmp;
}

复杂度分析
运行时间主要消耗在初始简历大顶堆和后边调整大顶堆上,总体的时间复杂度为O(nlogn),最坏O(nlogn),最好O(nlogn)。
稳定性:由于记录的比较与交换是跳跃式的,所以堆排序是一种不稳定的排序算法。
由于初始时建堆时需要的比较次数较多,所以不适合待排序列个数较少的情况。

3.归并排序

时间复杂度:平均:O(nlogn),最好:O(nlogn),最坏:O(nlogn)
稳定性:稳定
归并排序原理:假设有n个数的序列需要排序,首先进行两两比较排序,然后将相邻的两两子序列进行归并排序,一直归并到n/2子列,然后对1-n/2子列和n/2+1到length子列进行归并,得到有序数列n,这种排序方法称为2路归并排序。
归并排序的递归实现

// 合并TR2中两个子序列s-m和m+1-t到数组TR1中
void Merge(int TR2[], int TR1[], int s, int m, int t)
{
    int i, j, k;
    for (i = s, j = m + 1,k=s; i <= m&&j <= t;k++)
    {
        if (TR2[i]>TR2[j])
        {
            TR1[k] = TR2[j];
            j++;
        }
        else
        {
            TR1[k] = TR2[i];
            i++;
        }
    }
    if (i <= m)
    {
        for (int l = i; l <= m; l++,k++)
        {
            TR1[k] = TR2[l];            //将剩余的i-m复制到TR1中
        }
    }
    if (j <= t)
    {
        for (int l = j; l <= t; l++, k++)
        {
            TR1[k] = TR2[l];            // 将剩余的m+1 -- t复制到TR1中
        }
    }
}
//SR数组归并排序为TR1数组
void Msort(int SR[], int TR1[], int s, int t)
{
    int TR2[MAX];
    if (s == t)
        TR1[s] = SR[s];
    else
    {
        int m = (s + t) / 2;        // 将SR[s...t]平分为SR[s...m]和SR[m+1...t]
        Msort(SR, TR2, s, m);       // 递归对SR[s...m]进行归并排序为TR2
        Msort(SR, TR2, m + 1, t);   // 递归对SR[m+1...t]进行归并排序为TR2
        Merge(TR2, TR1, s, m, t);   // 实际的归并排序 将TR2中s到m和m+1到t归并排序为TR1
    }
}
// 归并排序
void MergeSort(SqList *L)
{
    Msort(L->r, L->r, 1, L->length);
}

非递归实现

// 将数组SR中长度为k的两个相邻子列进行归并放入TR数组中
void MergePass(int SR[], int TR[], int k, int length)
{
    int i = 1;
    while (i <= length - (2 * k)+1)
    {
        Merge(SR, TR, i, i + k - 1, i + 2 * k - 1);
        i = i + 2 * k;
    }
    // 归并最后两个序列
    if (i < length - k + 1)
    {
        Merge(SR, TR, i, i + k - 1, length);
    }
    else
    {
        // 剩下单个子序列
        for (int j = i; j <= length; j++)
        {
            TR[j] = SR[j];
        }
    }
}

// 归并排序非递归实现
void MergeSort2(SqList *L)
{
    int *TR = (int *)malloc(sizeof(int)*(L->length + 1)); // 申请一个辅助空间
    int k = 1;
    while (k < L->length)
    {
        MergePass(L->r, TR, k, L->length);
        k = k * 2;
        MergePass(TR, L->r, k, L->length);
        k = k * 2;
    }

}

4.快速排序

思想:快速排序通过一趟排序将数列划分为两个部分,前边的都比选中的关键字小,后边的都比选中的关键字大,然后继续对前后两部分分别进行排序,以达到整个序列有序。
递归实现代码如下

// 将L-r中low到high划分为两部分,并返回划分点的位置
int Partition(SqList *L, int low, int high)
{
    int pivotKey = L->r[low];           // 用子序列的第一个作为划分点
    // 进行划分:这里使用两端交替向中间划分
    while (low < high)
    {
        while (low < high && pivotKey <= L->r[high])
            high--;
        swap(L, low, high);
        while (low < high && pivotKey >= L->r[low])
            low++;
        swap(L, low, high);
    }

    return high;
}

// 对表中序列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]一分为二,前边都比L->r[pivot]小,后边都比这个大 
        Qsort(L, low, pivot - 1);           // 对low - pivot-1这部分继续进行递归划分
        Qsort(L, pivot + 1, high);          // 进行递归划分
    }
}
// 快速排序
void QuickSort(SqList *L)
{
    Qsort(L, 1, L->length);
}

最坏情况下复杂度为O(n^2),平均时间复杂度为O(nlogn),最好O(nlogn)
稳定性:不稳定

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值