《数据结构》复习之排序算法

本文详细介绍了八种常见的排序算法:直接插入排序、希尔排序、冒泡排序、快速排序、简单选择排序、堆排序、二路归并排序和基数排序。分析了每种算法的思想、代码实现、时间复杂度和空间复杂度。总结了排序算法的稳定性,并对各种排序算法的平均和最坏时间复杂度进行了对比,同时强调了部分排序算法在有序序列和无序序列上的表现差异。
摘要由CSDN通过智能技术生成

1.排序算法

1.1直接插入排序

  算法思想:
  每次将一个待排序的数据元素,插入到前面已经排好序的数列中的适当位置,使数列 依然有序;直到待排序数据元素全部插入完为止。
  算法代码:

void insertSort(int *A,int n)//数组下标从0开始
{
    for(i=1;i<n;i++)
    {
        temp=A[i];   //将待插入的元素存在temp中
        j=i-1;
        while(j>=0&&temp<A[j])
        {
            A[j+1]=A[j];
            j--;
        }
        j++;
        A[j]=temp;
    }
} 

  时间复杂度:
  最坏的情况整个序列均为逆序,则时间复杂度为O(n2)。
  最好的情况即整个序列有序,则时间复杂度为O(n)。
  综上所述,插入排序的时间复杂度为O(n2
  空间复杂度:
  额外辅助空间只需要一个temp,因此空间复杂度为O(1)。

1.2希尔排序

  算法思想:
  希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最 大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小,插入排序对于 有序的序列效率很高。
  算法代码:

int dk[]={5,3,1};
void shellSort(int *&A,int dk,int n)
{
    int i,j,temp;
    for(i=dk;i<n;i++)
    {
        temp=A[i];
        j=i-dk;
        while(j>=0&&temp<A[j])
        {
            A[j+dk]=A[j];
            j=j-dk;
        }
        j=j+dk;
        A[j]=temp;
    }
}

  时间复杂度:
  希尔排序的平均情况为O(nlog2n)。
  空间复杂度:
  希尔排序的空间复杂度与插入排序一样,为O(1)。

1.3冒泡排序

  算法思想:
  冒泡排序通过一系列的“交换”动作完成。首先第一个记录和第二个记录比较,如果第一个大,则交换,否则不交换。然后第二个记录和第三个记录比较,如果第二个大,则两者交换,否则不交换…….一直按这种方式进行下去,最终最大的那个记录被交换到最后,一趟冒泡排序完成。因此它通过一趟又一趟 地比较数组中的每一个元素,使较大的数据下沉,较小的数据上升来完成算法。要注意的是,冒泡排序算法结束的条件是一趟排序过程中没有发生元素交换。
  算法代码:

void bubbleSort(int *A, int n)
{
    int i, j,flag,temp;
    for (i = n ; i > 1;i--)
    {
        flag = 0;
        for (j = 0; j <i-1 ;j++)//边界情况要注意,把边界带入
        {
            if (A[j]>A[j+1])
            {
                temp = A[j];
                A[j] = A[j + 1];
                A[j + 1] = temp;
                flag = 1;
            }
        }
        if (flag==0)
        {
            return;
        }
    }
}

  时间复杂度:
  最坏情况,待排序序列逆序,此时时间复杂度为O(n2)。
  最好的情况即整个序列有序,则时间复杂度为O(n)。
  综上所述,插入排序的时间复杂度为O(n2
  空间复杂度:
  额外辅助空间只需要一个temp,因此空间复杂度为O(1)。

1.4快速排序

  算法思想:
  快速排序选取第一个数 A,从两边开始交换,直至将序列分为以 A 为基准的左边和右 边,左边的数均小于 A,右边的数均大于 A,然后两边递归。
  算法代码:

void quickSort(int *&A,int l,int r)
{
    int temp;
    int i=l,j=r;
    if(l<r)
    {
        temp=A[i];
        while(i<j)
        {
            while(i<j&&A[j]>temp)  //一定要有i<j 
                j--;
            if(i<j)
            {
                A[i]=A[j];
                i++;
            }
            while(i<j&&A[i]<temp)
                i++;
            if(i<j)
            {
                A[j]=A[i];
                j--;
            }
        }
        A[i]=temp;
        qsort(A,l,i-1);
        qsort(A,i+1,r);
    }
} 

  时间复杂度:
  快速排序最好的情况下的时间复杂度为O(nlog2n),待排序列越接近无序,本算法效率越高。最坏情况下的时间复杂度为O(n2),待排序列越接近有序,本算法效率越低。平均时间复杂度为O(nlog2n)。就平均时间而言,快速排序是所有排序算法中最好的。快速排序的排序趟数与初始序列有关。
  空间复杂度:
  空间复杂度为O(log2n)。快速排序是递归进行的,递归需要栈的帮助。

1.5简单选择排序

  算法思想:
  从头到尾顺序扫描序列,找出一个最小的数字,和第一个交换,接着从剩下的记录中 继续这种选择和交换,最终使序列有序。
  算法代码:

void simpleSelectSort(int *A, int n)
{
    int i, j,k,min,temp;
    for (i = 0; i < n;i++)
    {
        k = i;
        for (j = i + 1;j<n; j++)
        {
            if (A[j]<A[k])
            {
                k = j;
            }
        }
        temp = A[k];
        A[k] = A[i];
        A[i] = temp;
    }
}

  时间复杂度:
  时间复杂度为O(n2)。
  空间复杂度:
  额外辅助空间只需要一个temp,因此空间复杂度为O(1)。

1.6堆排序

  算法思想:
  堆是一种数据结构,可以把堆看成一棵完全二叉树,这棵完全二叉树满足:任何一个非叶子结点的值都不大于(或不小于)其左右孩子的结点的值。若父亲大孩子小,则这样的堆叫作大顶堆;若父亲小孩子大,则这样的堆叫作小顶堆。
  堆排序执行过程描述(大顶堆)如下:
  1. 从无序序列所确定的完全二叉树的第一个非叶子结点开始, 从右至左,从上到下,对每个结点进行调整,最终将得到一个大顶堆。
  对结点的调整方法:将当前结点(假设为a)的值与其孩子结点进行较,如果存在大于a值的孩子结点,则从中选出最大的一个与a交换。当a来到下一层的时候重复上述过程,直到a的孩子结点值小于a的值为止。
  2. 将当前无序序列中的第一个元素,反映在树中时根节点(假设为a)与无序序列中最后一个元素交换(假设为b)。a进入有序序列,到达最终位置,无序序列中元素减少1个,有序序列中元素增加1个。此时只有结点b可能不满足堆的定义,对其进行调整。
  3.重复2中的过程,直到无序序列中的元素剩下1个时排序结束。
  算法代码:

#include<iostream>
using namespace std;

int *A;

/*本函数完成对数组A[low]到A[high]范围内对在位置low上的节点的调整*///只是调整一个结点的函数
void HeapAdjust(int *&A,int low,int high)  //由于A[]是一棵完全二叉树,所以存储的元素必须从1开始
{
    int i=low;
    int j=i*2;
    int temp=A[i];
    while(j<=high)   //终止条件
    {
        if(j<high&&A[j+1]>A[j])   //若右孩子比较大,则把j指向右孩子,j<high这句话不能忘
        {
            j++;
        }
        if(temp<A[j])
        {
            A[i]=A[j];  //将A[j]调整到双亲位置上
            i=j;     //修改i和j的值以便能继续调整
            j=i*2;
        }
        else    //调整结束
        {
            break;   
        }   
    }
    A[i]=temp;
} 


int main()
{
    int i,n,temp;
    cin>>n;
    A=new int[n+1];
    for(i=1;i<=n;i++)
    {
        cin>>A[i];
    }

    for(i=n/2;i>=1;i--)    //建立初始堆
    {
        HeapAdjust(A,i,n); 
    }

    for(i=n;i>=2;i--)
    {
        temp=A[1];
        A[1]=A[i];
        A[i]=temp;
        HeapAdjust(A,1,i-1); 
    } 

    for(i=1;i<=n;i++)
    {
        cout<<A[i]<<" ";
    } 
    cout<<endl; 
    return 0;
} 

  时间复杂度:
  堆排序的时间复杂度为O(nlog2n)。堆排序在最坏情况下的时间复杂度也是O(nlog2n),这是它相对于快速排序的最大优点。堆排序适合的场景是记录数很多的情况,典型的例子是从10000个记录中选出前10个最小的。这种情况用堆排序最好。如果记录数较少,则不提倡使用堆排序。
  空间复杂度:
  本算法的额外空间只有一个temp,因此空间复杂度为O(1)。

1.7二路归并排序

  算法思想:
  归并排序先分解要排序的序列,从 1 分成 2,2 分成 4,依次分解,当分解到只有 1 个 一组的时候,就可以排序这些分组,然后依次合并回原来的序列中,这样就可以排序所有数据。
  算法代码:

void Msort(int *&A,int l,int h)
{
    if(l<h)
    { 

        int m=(l+h)/2;
        Msort(A,l,m);
        Msort(A,m+1,h);
        Merge(A,l,m,h);
    } 
}

void Merge(int *&A,int l,int m,int h)  //归并操作
{
    int *temp=new int[h-l+1];
    memset(temp,0,sizeof(temp));
    int i,j,k=0;
    i=l;
    j=m+1;
    while(i<=m&&j<=h)
    {
        if(A[i]<A[j])
        {
            temp[k]=A[i];
            k++;
            i++;
        }
        else
        {
            temp[k]=A[j];
            k++;
            j++;
        }
    }
    while(i<=m)
    {
        temp[k]=A[i];
        k++;
        i++;
    }
    while(j<=h)
    {
        temp[k]=A[j];
        k++;
        j++;
    }
    j=0;
    for(i=l;i<=h;i++)
    {
        A[i]=temp[j];
        j++;
    }
}

  非递归形式的代码:

void MergeSort()
{
    //writein();
    int size=1,low,mid,high;  
    while(size<=Maxsize-1)  
    {  
        low=0;  
        while(low+size<=Maxsize)  
        {  
            mid=low+size-1;  
            high=mid+size;  
            if(high>Maxsize-1)//第二个序列个数不足size   
                high=Maxsize;         
            Merge(low,mid,high);//调用归并子函数   
            low=high+1;//下一次归并时这一次的下界   
        }  
        size*=2;//范围扩大一倍   
    }  
    //display();
}

  时间复杂度:
  归并排序的时间复杂度为O(nlog2n)。
  空间复杂度:
  因为归并排序需要转存整个待排序序列,因此空间复杂度为O(n)。

1.8基数排序

  算法思想:
  基数排序的是“多关键字排序,采用最低位优先,不必通过比较,而是通过“分 配”和“收集”。如一个三位数,可以分成十个桶,然后将个位 0-9 分别放入桶中,再收集, 接着依次十位,百位。程序中用一个 count 数组来计数每个数的该放在整个排序的数组中的位 置。从个位开始计数,放置,一共进行 5 次循环(位数最大为 5)。
  算法代码:略。
  时间复杂度:
  时间复杂度为 O(d *(r+n))。d 为关键字的个数(循环次数),n 为序列的元素数(一次循环的分配次数),r 为关键字的取值范围(一次循环的收集次数)。
  空间复杂度:O(r)。
  

2.排序算法总结

2.1排序算法的稳定性

  • 什么是稳定性?
      所谓稳定性是指待排序的序列中有两个或两个以上相同项,排序前和排序后,看这些相同的项的相对位置有没有发生变化,如果没有发生变化,就是稳定的。
  • 稳定性的意义
      稳定意思是说原本键值一样的元素排序后相对位置不变。学习的时候,可能编的程序里面要排序的元素都是简单类型,实际上真正使用的时候,可能是对一个复杂类型的数组排序,而排序的键实际上只是这个元素中的一个属性,对于一个简单类型,数字值就是其全部意义,即使交换了也看不出什么不同。但是对于复杂的类型,交换的话可能就会使原本不应该交换的元素交换了。
      比如,一个“学生”数组,按照年龄排序,“学生”这个对象不仅含有“年龄”,还有其他很多属性,稳定的排序会保证比较时,如果两个学生年龄相同,一定不交换。(转自百度知道)

2.2复杂度总结

  • 时间复杂度
    平均情况下,快速排序,希尔排序,归并排序和堆排序的时间复杂度均为O(nlog2n),其他都是O(n2)。一个特殊的排序是基数排序,时间复杂度为 O(d *(r+n))。
    最坏情况下,快速排序的时间复杂度为O(n2),其他与平均情况相同。
    助记方法:教官说:“快些以nlog2n的速度归队”。其中,“快”指快速排序,“些”指希尔排序(发音相似),“队”指堆排序(谐音),这四种排序的平均复杂度都是O(nlog2n)。
  • 空间复杂度
    记住几个特殊的就好,快排为O(log2n),归并排序为O(n),基数排序为O(r),其他都是O(1)。
  • 其他
    直接插入排序和冒泡排序更适合有序序列,而快排越无序效率越高。
    (以上复杂度总结转自天勤考研书)

2.3稳定性总结

  一句话记忆:心情不稳定快些选好友来聊天吧。
  这里,“快”指快速排序,“些”指希尔排序,“选”指简单选择排序,“堆”指堆排序。这四种排序是不稳定的,其他都是稳定的。
  (以上稳定性总结转自天勤考研书)

2.4其他

  经过一趟排序,能保证一个元素达到最终位置的有冒泡,快速,简单选择和堆排序。(根据排序原理来记忆)。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值