28 排序算法

冒泡排序

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
typedef int ElemType;
typedef struct{
    ElemType *elem;//存储元素的起始地址
    int TableLen;//元素个数
}SSTable;
void ST_Init(SSTable &ST,int len)
{
    ST.TableLen=len;
    ST.elem=(ElemType *)malloc(sizeof(ElemType)*ST.TableLen);//申请一块堆空间,当数组来使用
    int i;
    srand(time(NULL));//随机数生成,每一次执行代码就会得到随机的 10 个元素
    for(i=0;i<ST.TableLen;i++)
    {
        ST.elem[i]=rand()%100;//生成的是 0-99 之间
    }
}
//打印数组中的元素
void ST_print(SSTable ST)
{
    for(int i=0;i<ST.TableLen;i++)
    {
        printf("%3d",ST.elem[i]);
    }
    printf("\n");
}
//交换两个元素
void swap(ElemType &a,ElemType &b)
{
    ElemType tmp;
    tmp=a;
    a=b;
    b=tmp;
}
// 64 94 95 79 69 84 18 22 12 78
// 12 64 94 95 79 69 84 18 22 78 外层完成一次循环后,最小值到了最前面
void BubbleSort(ElemType A[],int n)
{
    int i,j;
    bool flag;
    for(i=0;i<n-1;i++)//i 最多访问到 8
    {
        flag=false;//元素是否发生交换的标志
        for(j=n-1;j>i;j--)//把最小值就放在最前面
        {
            if(A[j-1]>A[j])
            {
                swap(A[j-1],A[j]);
                flag=true;
            }
        }
        if(false==flag)//如果一趟比较没有发生任何交换,说明有序,提前结束排序
            return;
    }
}
//冒泡排序
int main() {
    SSTable ST;
    ST_Init(ST, 10);//初始化
// ElemType A[10]={ 64, 94, 95, 79, 69, 84, 18, 22, 12 ,78};
//内存 copy 接口,当你 copy 整型数组,或者浮点型时,要用 memcpy,不能用 strcpy,初试考 memcpy概率很低
//memcpy(ST.elem,A,sizeof(A));//这是为了降低调试难度,每次数组数据固定而设计的
    ST_print(ST);//排序前打印
    BubbleSort(ST.elem, 10);
    ST_print(ST);//排序后再次打印
    return 0;
}

时间复杂度其实就是程序实际的运行次数,可以看到内层是 j>i,外层 i 的值是从 0 到 N -1 ,所以程序的总运行次数是1+ 2 + 3+...+ (N-1) ,即从1 一直加到 N -1 ,这是等差数列求和,得到的结果是 N(N -1)/2 ,即总计运行了这么多次,忽略了低阶项和高阶项的首项系数,因为时间复杂度为 O(n^2)。因为未使用额外的空间(额外空间必须与输入元素的个数 N 相关),所以空间复杂为 O(1)。如果数组本身有序,那么就是最好的时间复杂度 O(n)。

快速排序

快速排序的核心是分治思想:假设目标依然是按从小到大的顺序排列,找到数组中的一个分割值,把比分割值小的数都放在数组的左边,把比分割值大的数都放在数组的右边,这样分割值的位置就被确定。数组一分为二,我们只需排前一半数组和后一半数组,复杂度直接减半。采用这种思想,不断地进行递归,最终分割得只剩一个元素时,整个序列自然就是有序的。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
typedef int ElemType;
typedef struct{
    ElemType *elem;//存储元素的起始地址
    int TableLen;//元素个数
}SSTable;
void ST_Init(SSTable &ST,int len)
{
    ST.TableLen=len;
    ST.elem=(ElemType *)malloc(sizeof(ElemType)*ST.TableLen);//申请一块堆空间,当数组来使用
    int i;
    srand(time(NULL));//随机数生成,每一次执行代码就会得到随机的 10 个元素
    for(i=0;i<ST.TableLen;i++)
    {
        ST.elem[i]=rand()%100;//生成的是 0-99 之间
    }
}
//打印数组中的元素
void ST_print(SSTable ST)
{
    for(int i=0;i<ST.TableLen;i++)
    {
        printf("%3d",ST.elem[i]);
    }
    printf("\n");
}
//交换两个元素
void swap(ElemType &a,ElemType &b)
{
    ElemType tmp;
    tmp=a;
    a=b;
    b=tmp;
}
// 64 94 95 79 69 84 18 22 12 78
//比 64 小的放在左边,比 64 大的放在右边
int Partition(ElemType A[],int low,int high)
{
    ElemType pivot=A[low];//首先使用左边变量存储分割值
    while(low<high)
    {
        while(low<high&&A[high]>=pivot)//从后往前遍历,找到一个比分割值小的
            --high;
        A[low]=A[high];//把比分隔值小的那个元素,放到 A[low]
        while(low<high&&A[low]<=pivot)//从前往后遍历,找到一个比分割值大的
            ++low;
        A[high]=A[low];//把比分隔值大的那个元素,放到 A[high],因为刚才 high 位置的元素已经放到了low 位置
    }
    A[low]=pivot;
    return low;//返回分隔值所在的下标
}
//递归实现
void QuickSort(ElemType A[],int low,int high)
{
    if(low<high)
    {
        int pivotpos=Partition(A,low,high);//分割点左边的元素都比分割点要小,右边的比分割点大
        QuickSort(A,low,pivotpos-1);
        QuickSort(A,pivotpos+1,high);
    }
}

//快速排序
int main()
{
    SSTable ST;
    ST_Init(ST,10);//初始化
    ElemType A[10]={ 64, 94, 95, 79, 69, 84, 18, 22, 12 ,78};
//内存 copy 接口,当你 copy 整型数组,或者浮点型时,要用 memcpy,不能用 strcpy,初试考 memcpy概率很低
            memcpy(ST.elem,A,sizeof(A));//这是为了降低调试难度,每次数组数据固定而设计的
    ST_print(ST);
    QuickSort(ST.elem,0,9);//注意这个位置是 n-1,也就是 9,因为函数里取了 high 位置的值
    ST_print(ST);
    return 0;
}

假如每次快速排序数组都被平均地一分为二,那么可以得出 QuickSort 递归的次数是 log2n,第一次 partition 遍历次数为 n,分成两个数组后,每个数组遍历 n/2 次,加起来还是 n,因此时间复杂度是 O(nlog2n),因为计算机是二进制的,所以在复试面试回答复杂度或与人交流时,提到复杂度时一般直接讲 O(nlogn),而不带下标 2。快速排序最差的时间复杂度为什么是 n2呢?因为数组本身从小到大有序时,如果每次我们仍然用最左边的数作为分割值,那么每次数组都不会二分,导致递归 n 次,所以快速排序最坏时间复杂度为 n 的平方。当然,为了避免这种情况,有时会首先随机选择一个下标,先将对应下标的值与最左边的元素交换,再进行 partition 操作,从而极大地降低出现最坏时间复杂度的概率,但是仍然不能完全避免。

因此快排最好和平均时间复杂度是 O(nlog2n),最差是 O(n2)。快排的空间复杂度是 O(log2n),因为递归的次数是 log2n,而每次递归的形参都是需要占用空间的。

插入排序

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
typedef int ElemType;
typedef struct{
    ElemType *elem;//整型指针
    int TableLen;
}SSTable;
void ST_Init(SSTable &ST,int len)
{
    ST.TableLen=len;//申请 10 个元素的空间
    ST.elem=(ElemType *)malloc(sizeof(ElemType)*ST.TableLen);
    int i;
    srand(time(NULL));
    for(i=0;i<ST.TableLen;i++)
    {
        ST.elem[i]=rand()%100;//随机了 10 个数
    }
}
void ST_print(SSTable ST)
{
    for(int i=0;i<ST.TableLen;i++)
    {
        printf("%3d",ST.elem[i]);
    }
    printf("\n");
}
//王道书的插入排序用了哨兵,但是考研插入排序不会考哨兵,用了哨兵只是省略了 j>=0 这一句代码
void InsertSort(ElemType* arr,int n)
{
    int i, j, insertVal;
    for (i = 1; i < n; i++) //控制要插入的数
    {
        insertVal = arr[i];//先保存要插入的值
//内层控制比较,j 要大于等于 0,同时 arr[j]大于 insertval 时,arr[j]位置元素往后覆盖
        for (j = i - 1; j >= 0 && arr[j] > insertVal; j -= 1)
        {
            arr[j + 1] = arr[j];
        }
        arr[j + 1] = insertVal;
    }
}
int main()
{
    SSTable ST;
    ST_Init(ST,10);//申请 10 个元素空间
    ST_print(ST);//排序前打印
    InsertSort(ST.elem,10);
    ST_print(ST);//排序后再次打印
    return 0;
}

随着有序序列的不断增加,插入排序比较的次数也会增加,插入排序的执行次数也是从 1 加到N-1,总运行次数为 N(N -1)/2 ,时间复杂度依然为 O(n^2)。因为未使用额外的空间(额外空间必须与输入元素的个数 N 相关),所以空间复杂为 O(1)。

如果数组本身有序,那么就是最好的时间复杂度 O(n)。

选择排序

#include <cstdio>
#include <cstdlib>
#include <time.h>
#include <string.h>
typedef int ElemType;
typedef struct{
    ElemType *elem;
    int TableLen;
}SSTable;
void ST_Init(SSTable &ST,int len)//申请空间,并进行随机数生成
{
    ST.TableLen=len;
    ST.elem=(ElemType *)malloc(sizeof(ElemType)*ST.TableLen);
    int i;
    srand(time(NULL));
    for(i=0;i<ST.TableLen;i++)
    {
        ST.elem[i]=rand()%100;
    }
}
void ST_print(SSTable ST)
{
    for(int i=0;i<ST.TableLen;i++)
    {
        printf("%3d",ST.elem[i]);
    }
    printf("\n");
}
void swap(ElemType &a,ElemType &b)
{
    ElemType tmp;
    tmp=a;
    a=b;
    b=tmp;
}
void SelectSort(ElemType A[],int n)
{
    int i,j,min;//min 记录最小的元素的下标
    for(i=0;i<n-1;i++)//最多可以为 8
    {
        min=i;//认为第 i 个位置最小
        for(j=i+1;j<n;j++)//j 最多可以为 9
        {
            if(A[j]<A[min])
                min=j;
        }
        if(min!=i)
        {
//遍历完毕找到最小值的位置后,与 A[i]交换,这样最小值被放到了最前面
            swap(A[i],A[min]);
        }
    }
}
//选择排序
int main()
{
    SSTable ST;
    ST_Init(ST,10);//初始化
//下面两行为了降低调试难度,每次数组数据固定而设计的
//ElemType A[10] = { 64, 94, 95, 79, 69, 84, 18, 22, 12 ,99 };
//memcpy(ST.elem,A,sizeof(A));
    ST_print(ST);
    SelectSort(ST.elem,10);
    ST_print(ST);
    return 0;
}

选择排序虽然减少了交换次数,但是循环比较的次数依然和冒泡排序的数量是一样的,都是从 1加到 N -1,总运行次数为 N(N -1) / 2 。我们忽略循环内语句的数量,因为我们在计算时间复杂度时,主要考虑与 N 有关的循环,如果循环内交换得多,例如有 5 条语句,那么最终得到的无非是5n^2;循环内交换得少,例如有 2 条语句,那么得到的就是 2n^2,但是时间复杂度计算是忽略首项系数的,因此最终还是 O(n2)。因此,选择排序的时间复杂度依然为 O(n^2)。因为未使用额外的空间(额外空间必须与输入元素的个数 N 相关),所以空间复杂为 O(1)。另外考研初试问时间复杂度,直接写最终结果即可,不用分析过程,除非清晰说明需要给出计算过程,或者分析过程(但是目前一直都没有这个要求)。

堆排序

如果父结点的下标是dad,那么父结点对应的左子结点的下标值是2*dad+1。接着,依次将每棵子树都调整为父结点最大,最终将整棵树变为一个大根堆。

堆排序的步骤:随机生成十个元素,首先把堆调整为大根堆,然后交换根部元素也就是A[0],和最后一个元素,这样最大的元素就放到了数组最后,接着将剩余 9 个元素继续调整为大根堆,然后交换 A[0]和 9 个元素的最后一个,循环往复,直到有序。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
typedef int ElemType;
typedef struct{
    ElemType *elem;
    int TableLen;
}SSTable;
void ST_Init(SSTable &ST,int len)//申请空间,并进行随机数生成
{
    ST.TableLen=len;
    ST.elem=(ElemType *)malloc(sizeof(ElemType)*ST.TableLen);
    int i;
    srand(time(NULL));
    for(i=0;i<ST.TableLen;i++)
    {
        ST.elem[i]=rand()%100;
    }
}
void ST_print(SSTable ST)
{
    for(int i=0;i<ST.TableLen;i++)
    {
        printf("%3d",ST.elem[i]);
    }
    printf("\n");
}
void swap(ElemType &a,ElemType &b)
{
    ElemType tmp;
    tmp=a;
    a=b;
    b=tmp;
}
//调整某个父亲节点,王道书上的
void AdjustDown(ElemType A[],int k,int len)
{
    int i;
    A[0]=A[k];
    for(i=2*k;i<=len;i*=2)
    {
        if(i<len&&A[i]<A[i+1])//左子节点与右子节点比较大小
            i++;
        if(A[0]>=A[i])
            break;
        else{
            A[k]=A[i];
            k=i;
        }
    }
    A[k]=A[0];
}
//用数组去表示树 类似层次建树 王道书上的
void BuildMaxHeap(ElemType A[],int len)
{
    for(int i=len/2;i>0;i--)
    {
        AdjustDown(A,i,len);
    }
}
//王道书上的
void HeapSort(ElemType A[],int len)
{
    int i;
    BuildMaxHeap(A,len);//建立大顶堆
    for(i=len;i>1;i--)
    {
        swap(A[i],A[1]);
        AdjustDown(A,1,i-1);
    }
}
//调整子树
void AdjustDown1(ElemType A[], int k, int len)
{
    int dad = k;
    int son = 2 * dad + 1; //左孩子下标
    while (son<=len)
    {
        if (son + 1 <= len && A[son] < A[son + 1])//看下有没有右孩子,比较左右孩子选大的
        {
            son++;
        }
        if (A[son] > A[dad])//比较孩子和父亲,如果孩子大于父亲,那么进行交换
        {
            swap(A[son], A[dad]);
            dad = son;//孩子重新作为父亲,判断下一颗子树是否符合大根堆
            son = 2 * dad + 1;
        }
        else {
            break;
        }
    }
}
void HeapSort1(ElemType A[], int len)
{
    int i;
//建立大根堆
    for (i = len / 2; i >= 0; i--)
    {
        AdjustDown1(A, i, len);
    }
    swap(A[0], A[len]);//交换顶部和数组最后一个元素
//下面的策略是,不断调整剩余元素为大根堆,因为根部最大,所以再次与 A[i]交换(相当于放到数组后面),循环往复
    for (i = len - 1; i > 0; i--)
    {
        AdjustDown1(A, 0, i);//剩下元素调整为大根堆
        swap(A[0], A[i]);
    }
}
//堆排序
int main()
{
    SSTable ST;
    ST_Init(ST,10);//初始化
    ElemType A[10] = { 3, 87, 2, 93, 78, 56, 61, 38, 12, 40 };
    memcpy(ST.elem,A,sizeof(A));
    ST_print(ST);
//HeapSort(ST.elem, 9);//王道书零号元素不参与排序,考研考的都是零号元素要参与排序
    HeapSort1(ST.elem,9);//所有元素参与排序
    ST_print(ST);
    return 0;
}

AdjustDown1 函数的循环次数是 log2n,HeapSort1 函数的第一个 for 循环了 n/2 次,第二个 for 循环了 n 次,总计次数是 3/2nlog2n 次,因此时间复杂度是 O(nlog2n)。堆排最好、最坏、平均时间复杂度都是 O(nlog2n)。

堆排的空间复杂度是 O(1),因为没有使用与 n 相关的额外空间。

归并排序

归并排序的代码是采用递归思想实现的。首先,最小下标值和最大下标值相加并除以 2,得到中间下标值 mid,用 MergeSort对 low 到 mid 排序,然后用MergeSort 对 mid+1 到 high 排序。当数组的前半部分和后半部分都排好序后,使用 Merge 函数。Merge 函数的作用是合并两个有序数组。为了提高合并有序数组的效率,在 Merge 函数内定义了 B[N]。首先,我们通过循环把数组 A 中从 low 到 high 的元素全部复制到 B 中,这时游标 i(遍历的变量称为游标)从 low 开始,游标 j 从 mid+1 开始,谁小就将谁先放入数组 A,对其游标加 1,并在每轮循环时对数组 A 的计数游标 k 加 1。

#include <stdio.h>
#include <stdlib.h>
#define N 7
typedef int ElemType;
//49,38,65,97,76,13,27
void Merge(ElemType A[],int low,int mid,int high)
{
    static ElemType B[N];//加 static 的目的是无论递归调用多少次,都只有一个 B[N]
    int i,j,k;
    for(k=low;k<=high;k++)//复制元素到 B 中
        B[k]=A[k];
    for(i=low,j=mid+1,k=i;i<=mid&&j<=high;k++)//合并两个有序数组
    {
        if(B[i]<=B[j])
            A[k]=B[i++];
        else
            A[k]=B[j++];
    }
    while(i<=mid)//如果有剩余元素,接着放入即可
        A[k++]=B[i++];//前一半的有剩余的放入
    while(j<=high)
        A[k++]=B[j++];//后一半的有剩余的放入
}
//归并排序不限制是两两归并,还是多个归并,考研都是考两两归并
void MergeSort(ElemType A[],int low,int high)//递归分割
{
    if(low<high)
    {
        int mid=(low+high)/2;
        MergeSort(A,low,mid);//排序好前一半
        MergeSort(A,mid+1,high);//排序好后一半
        Merge(A,low,mid,high);//讲两个排序好的数组合并
    }
}
void print(int* a)
{
    for(int i=0;i<N;i++)
    {
        printf("%3d",a[i]);
    }
    printf("\n");
}
// 归并排序
int main()
{
    int A[7]={49,38,65,97,76,13,27};//数组,7 个元素
    MergeSort(A,0,6);
    print(A);
    return 0;
}

MergeSort 函数的递归次数是 log2n,Merge 函数的循环了 n 次,因此时间复杂度是 O(nlog2n)。归并排序最好、最坏、平均时间复杂度都是 O(nlog2n)。

归并排序的空间复杂度是 O(n),因为使用了数组 B,它的大小与 A 一样,占用 n 个元素的空间。

所有排序算法时间和空间复杂度汇总

稳定性是指排序前后,相等的元素位置是否会被交换。复杂性是指代码编写的难度。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值