数据结构-排序

插入排序

  • 直接插入排序
算法思想:在插入第i(i>01个元素时,前面i-1个元素已经排好序;
解决的问题:
(1)如何构造初始的有序序列:将第1个元素视作初始的有序序列,依次将第2个直至最后一个插入有序表
(2)如何查找待插入记录的插入位置;
void InsertSort(int r[],int n)
{  
   int j;  //i:指向无序序列的第1个元素;j:指向有序序列最后1个元素;
   for(int i=2;i<=n;++i)  //将第1个元素视作有序序列,逐个从2-n无序序列中,拿1个元素插入到有序序列
    { r[0]=r[i];  //r[0]暂存作用,存储待插入有序序列的元素
      j=i-1;    //j指向待插入有序序列元素的前一个位置,j用于将元素移动方便待插入元素插入合适位置
      while(r[0]<r[j])  //逐个后移元素,直至待插入元素>=有序序列中某一元素,将其插入后一位置
       { r[j+1]=r[j];
         j--;
       }
      r[j+1]=r[0];
    }
}
适用于待排序序列基本有序或者待排序元素数量较少的情况
  • 希尔排序:基于直接插入排序,使得待排序序列基本有序,如果待排序序列数量n较小,则排序效率将大大提高,这便是希尔排序
算法思想:将整个待排序序列分割成若干个子序列,在子序列内部进行直接插入排序,待整个序列基本有序时,对全体记录进行直接插入排序;
(1)按照相隔某个增量的记录组成一个子序列;增量:d1=n/2,d(i+1)=di/2;
(2)在子序列内进行直接插入排序,无论前进还是后移,每次变量是d,取代之前的1;
void ShellSort(int a[],int n)
{ for(d=n/2;d>=1;d=d/2)  //区别1:外循环计算增量d的值,首次:d=n/2;之后,d为上一轮整理的1/2;
    { for(int i=d+1;i<=n;++i)  //区别2:初始位置是i=d+1;
       { a[0]=a[i];
         j=i-d;        //区别3:所有+1或者-1都替换为+d或-d;
         while(j>0 && a[0]<a[j]){  //区别4:因为d一般大于1,所以存在j<0的风险,所以:保护一下
          a[j+d]=a[j];
          j=j-d;}
         a[j+d]=a[0];
        }
     }
}
对自己的建议:在熟练掌握直接插入排序的基础上,掌握以上4个区别,直接改,便是希尔排序;
希尔排序采用分治法思想,其时间复杂度取决于:增量的函数 

交换排序

主要操作是:交换;算法思想:从待排序序列中选取两个记录,对他们的关键码进行比较;如果反序,调整他们的存储位置;

  • 冒泡排序
冒泡排序:
算法思想:两两比较相邻记录的关键码,如果反序则交换,直到没有反序的记录为止
解决的问题(相比较于经典的冒泡排序):
(1)在一趟冒泡排序中,如果有多个记录位于最终位置,应该如何记录?
方法:定义exchange变量,记录交换的位置,在整体交换结束后,exchange记录的是最后一次交换的位置,那么exchange之前为无序区,之后为有序区(不再参与交换)
(2)如何确定冒泡排序的有序区和无序区,使得已经位于最终位置的记录不再参与下一次排序?
方法:定义bound变量,bound=exchange,记录无序区的最后一个记录的位置,则下一轮冒泡排序的排序区域是:r[0]-r[bound]
(3)如何判别冒泡排序的结束?
方法:每趟排序开始的时候,定义exchange=0,如果存在排序,则exchange>0;若exchange=0,则冒泡排序结束

void BubbleSort(int r[],int n)
{ exchange=n-1;  //1.初始化exchange
  while(exchange)     //2.在exchange=0的情况下结束的前提条件下,进行以下内容
   { bound=exchange;  //3.确定本轮冒泡排序的无序区范围
     exchange=0;      //4.定义exchange=0检验本轮是否存在排序的情况并呼应第2步
     for(int i=0;i<bound;++i)   //5.对无序区进行冒泡排序(0~bound)
      { if(r[i]>r[i+1])
          { p=r[i];r[i]=r[i+1];r[i+1]=p;}
        exchange=i;   //6.利用exchange记录本轮最后一次交换的位置
      }
   }
}
  • 快速排序:基于冒泡排序,增大记录的比较和移动距离
算法思想:(递归本质)首先选择一个轴值-比较的基准,一轮排序后,轴值左侧的都是小于等于轴值的记录,右侧都是大于等于轴值的记录,不断循环以上内容 
解决的问题:
(1)如何选取轴值:很多种选择方法,默认:以第一记录为轴值;【第1步】
(2)如何分割:i=first,j=end;首先,j从后往前扫描,一旦r[j]<r[i],违背规定的前小后大,所以交换r[i]和r[j]的值,此时:i++;接着,i从前往后扫描,一旦r[j]<r[i],违背规定的前小后大,所以交换r[i]和r[j]的值,此时:j--;最终 产生的结果是,轴值所在位置左边<=它,右边>=它;【第2步】
(3)如何处理分割后的2个待排序子序列:对【第2步】执行结束后产生的2个子序列递归地执行快速排序
(4)如何判别快速排序的结束:当first不再<end
int partition(int r[],int first,int end)  //解决第一二个问题,第1步&第2步
{  int i=first,j=end,k;
   while(i<j){
    while(i<j && r[i].key<=r[j].key) j--;
     if(i<j){
      k=r[i].key;r[i].key=r[j].key;r[j].key=k;i++;}
    while(i<j && r[i].key<=r[j].key) i++;
     if(i<j){
      k=r[i].key;r[i].key=r[j].key;r[j].key=k;j--;}
   }
   return i;
}
void QuickSort(int r[],int first,int end)  //解决第三个问题,第3步
{  if(first<end){         //解决第四个问题,第4步【递归函数的出口】
    pos=partition(r,first,end);  //确定轴值所在位置,轴值将序列划分为2个子序列
    QuickSort(r,first,pos-1);  //递归处理前半序列
    QuickSort(r,pos+1,end);    //递归处理后半序列
   }
}
快速排序的时间复杂度取决于递归的深度,递归的深度取决于轴值选取的好坏;最好的情况:O(nlog2^n)每次划分后,左右序列长度相同;最坏情况:O(n^2);平均情况:O(nlog2^n)
一般时间复杂度最坏的情况,是因为轴值选择不当的原因;
快速排序不适合序列基本有序或者序列很短的情况,一般序列基本有序或者序列较短,直接插入排序和冒泡排序很适合;
快速排序一定程度上牺牲了空间复杂度,换取了时间上的高效,因为快速排序使用了递归,占用了一定的系统栈;
冒泡排序是对相邻记录进行比较和交换,每次只能改变一对逆序记录;快速排序是从待排序序列两端开始,逐渐向中间靠拢,每经过一次交换,有可能改变几对逆序序列,从而加快了快速排序.

选择排序

  • 简单选择排序
主要操作是选择;
算法思想:每次从待排序序列中选出关键码最小的记录,添加到有序序列中.
解决问题:
(1)如何在待排序序列中选出关键码最小的记录:设置min记录1轮遍历中关键码最小的记录位置;
(2)如何确定待排序序列中关键码最小的记录在有序序列中的位置:


void SelSort(int r[],int n)
{
  for(int i=0;i<n-1;i++){
  min=i;  //i记录每轮遍历无序区的起点位置,并将i值赋给min用于标记本轮遍历中关键码最小的记录位置
  for(int j=i+1;j<n;j++){  //从min后一位开始遍历无序区,用min记录本轮循环最小关键码的位置
   if(r[j]<r[min])
    min=j;}
  if(min!=i){  //如果1轮循环遍历后的min的位置,不在初始位置,交换
   int p=r[min];
   r[min]=r[i];
   r[i]=p;}
  }
}
时间复杂度:O(n^2)-二重循环
  • 堆排序(天才算法)
算法思想:每次构造一个堆,将堆的根节点(最大值||最小值)添加到有序序列中.将剩余序列继续调整为一个堆,循环 以上内容直至堆中只有一个记录.

堆的定义:堆是具有下列性质的完全二叉树;每个结点的值都小于或等于左右孩子的值的堆叫小根堆;每个结点的值都大于或等于左右孩子的堆叫大根堆;
小根堆:
1.小根堆的根结点是所有结点的最小者;2.较小结点靠近根结点但不绝对; 
大根堆:
1.大根堆的根结点是所有结点的最大者;2.较大结点靠近根结点但不绝对;

解决问题:
(1)如何由一个无序序列建成初始堆:
(2)如何处理堆顶记录:第k次处理堆顶记录是将堆顶记录r[1]与序列中第n-k+1个记录r[n-k+1]进行交换
(3)如何调整剩余记录成一个新的堆:第k次调整剩余记录,此时,剩余记录有n-k个,调整根结点至第n-k个记录.

前提条件:【完全二叉,左右是堆,先上后下】在完全二叉树的前提条件下,从下[i=n/2]往上[++i]开始,保证左右子树是堆
堆调整:在一棵完全二叉树中,根结点左右子树均是堆,如何调整根结点使得整个完全二叉树成为堆.

建成一个大根堆,解决了第一个问题:
void shift(int r[],int k,int end)  //k作为起点,end作为终点;k又同时作为根结点,保证围绕k的根结点的左右子树成为堆
{ i=k;j=2*i;  //i:根结点,也就是起始结点;j:根结点左子树
  while(j<=end){  
   if(j<end && r[j]<r[j+1])  //j<end成立:k作为根结点,拥有左右子树;r[j]<r[j+1]:左子树小于右子树,将j放置于左右子树较大位置;
    j++;
   if(r[i]<r[j]){ //比较根节点与左右子树较大者,将较大者调整为根结点
    p=r[i];r[i]=r[j];r[j]=p;}   
   i=j;  //因为上述的结点的调整可能造成下边的堆被破坏,所以:继续对下边的堆进行调整
   j=2*j;  //此处的函数关系是:完全二叉树的性质关系【好好记忆理解一下】
   }
}
for (int k=n/2;k>=1;k--)  //从下至上地遍历每一个根结点,使得围绕每一个根结点左右子树成为堆
    shift(r,k,n);

void HeapSort(int r[],int n)    //堆排序主程序
{  for(int k=n/2;k>=1;--k){  //1.初建堆
    shift(r,k,n);}
   for(k=1;k<n;++k){   //2.【解决了第2个问题】k作为根结点从第1位开始遍历到第n-1位,每一次将堆顶记录与无序序列的最右端记录进行交换;此时会破坏原本的堆平衡,所以:
    p=r[1];r[1]=r[n-k+1];r[n-k+1]=p;  
    shift(r,1,n-k);}  //3.【解决了第三个问题】重新调整堆:以堆的第1位记录作为开始,以n-k位作为结束
}
时间复杂度-最好,最坏,平均:O(nlog2^n);空间复杂度:需要一个辅助空间r[0];时间复杂度,空间复杂度越低的算法,是个好算法;本算法本尊便是!

归并排序

分配排序

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值