插入排序(希尔排序)

排序就是按关键字的非递减或非递增顺序对一组记录重新进行排列的操作。

由于待排序记录的数量不同、使得排序过程中数据所占用的存储设备会有所不同。根据在排将过程中记录所占用的存储设备可将排序方法分为两大类:一类是内部排序,指的是待排序记全部在放在计算机内存中进行排序的过程;另一类是外部排序,指的是待排序记录的数量很大,以致内存一次不能容纳全部记录,在排序过程中尚需对外在进行访问的排序过程 本章首先介绍各种常用内部排序的方法,最后介绍外部排序的基本过程。

内部排序的方法很多,但就其全面性能而言,很难提出一种被认为是最好的方法,每一种方法都有各自的优缺点,适合在不同的环境(如记录的初始排列状态等)下使用。

内部排序的过程是一个逐步扩大记录的有序序列长度的过程。在排序的过程中,可以将排序章 记录区分为两个区域:有序序列区和无序序列区。

使有序区中记录的数目增加一个或几个的操作称为一趟排序

根据逐步扩大记录有序序列长度的原则不同,可以将内都排序分为以下几类。

(1)插入类:将无序子序列中的一个或几个记录“插入”到有序序列中,从而增加记录的有序子序列的长度。主要包括直接插入排序、折半插入排序和希尔排序。

(2)交换类:通过“交换”无序序列中的记录从而得到其中关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。主要包括冒泡排序和快速排序。

(3)选择类:从记录的无序子序列中“选择”关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。主要包括简单选择排序、树形选择排序和堆排序。

(4)归并类:通过“归并”两个或两个以上的记录有序子序列,逐步增加记录有序序列的长度。2-路归并排序是最为常见的归并排序方法。

(5)分配类:是唯一一类不需要进行关键字之间比较的排序方法,排序时主要利用分配和收集两种基本操作来完成。基数排序是主要的分配类排序方法。

排序算法效率的评价指标

 

前面已指出,就排序方法的全面性能而言,很难提出一种被认为是最好的方法。目前,评排序算法好坏的标准主要有两点。

(1)执行时间

对于排序操作,时间主要消耗在关键字之间的比较和记录的移动上(这里,只考虑以顺序麦方式存储待排序记录),排序算法的时间复杂度由这两个指标决定。因此可以认为,高效的排序算法的比较次数和移动次数都应该尽可能的少。

(2)辅助空间

空间复杂度由排序算法所需的辅助空间决定。辅助空间是除了存放待排序记录占用的空间之外,执行算法所需要的其他存储空间。理想的空间复杂度为O(1),即算法执行期间所需要的辅助空间与待排序的数据量无关。

在以下各节讨论各种排序算法时,将给出有关算法的关键字比较次数和记录的移动次数。有的排序算法其执行时间不仅依赖于待排序的记录个数,还取决于待排序序列的初始状态。因此.对这样的算法,本书还将给出其最好、最坏和平均情况下的3种时间性能评价。

在讨论排序算法的平均执行时间时,均假定待排序记录初始状态是随机分布的,即出现各种排列情况的概率是相等的。同时假定各种排序的结果均是按关键字非递减排序。

插入排序的基本思想是:每一趟将一个待排序的记录,按其关键字的大小插入到已经排好序

的一组记录的适当位置,直到所有待排序记录全部插入为止。

例如,打扑克牌在抓牌时要保证抓过的牌有序排列,则每抓一张牌,就插入合适的位置,直到抓完牌为止,即可得到一个有序序列。

可以选择不同的方法在已排好序的记录中寻找插入位置。根据查找方法的不同,有多种插入排序方法,这里仅介绍3种方法:直接插入排序、折半插入排序和希尔排序。

直接插入排序

插入的主要操作就是移动和对比

直接插入排序基于查找中的顺序查找,可以边查找边移动。直接插入排序主要有三个步骤:寻找位置,移动(得到)位置,插入位置。

void InsertSort(SqList &L)
 {
    int i,j;
   for(i=2;i<=L.length;++i)需要排序的
   {
        if( L.r[i].key<L.r[i-1].key)//将L.r[i]插入有序子表
       { L.r[0]=L.r[i]; // 复制为哨兵  此时的哨兵=我们要为此寻找位置的元素
          L.r[i]=L.r[i-1];空出一个位置  方便移动
          for(j=i-2; L.r[0].key<L.r[j].key;--j)比较时也是从后向前的
          {
              L.r[j+1]=L.r[j]; // 记录后移 移动时是从后面开始移动的(you  know)
          }
         L.r[j+1]=L.r[0]; //插入到正确位置
       }
   }
     
 }

我们进行的操作都是在同一个数组上进行操作的,可以把数组看成两个部分。一部分时排好序的,一部分没有排好序。

第一个for循环i=2,从第二个数开始。因为这个循环是对数据进行排序,第一个已经算做是排好序的了,所以从第二个开始。for循环内部的操作都是为了对“这个数据”排序。

if( L.r[i].key<L.r[i-1].key)

那么接下来,我们需要判断这个数据是否需要移动寻找位置(if条件的作用),不需要移动位置,不符合if条件。也就是该数据现在的位置很合理。开始进行下一个循环,for循环进行下一次。符合if条件,需要我们寻找位置,移动。if中的for循环的作用就是查找(for循环的条件)和移动(for循环的循环体)。

 L.r[j+1]=L.r[0]; //插入到正确位置

这一段代码就非常关键了,为什么j+1是正确的位置,不是j呢?

首先我们要知道程序是如何才进行到这样一步的,因为if有条件的成立!由此我们知道r[0]处的元素(我们需要寻找位置的元素=i处的元素)大于j处的元素,小于i-1处的元素

(j+1处),在这两处之间。所以我们把这个元素插入在j+1处。此处为正确位置。

折半插入排序

折半插入排序基于折半查找,不同于直接插入排序的是折半插入排序的查找和移动是分开的。不能在一起。这是由于折半查找的前提条件(有序序列)和操作(“折半”不单纯是一个元素而是一整块区间)所以不能直接移动。

void  BInsertSort ( SqList &L )
 { 
    for ( i = 2;  i <= L.length ; ++i )
    {  
       L.r[0] = L.r[i]; low = 1 ; high = i-1 ;
       while ( low <= high )       
        {  
            m = ( low + high ) / 2 ; 
            if  (  L.r[0].key < L.r[m]. key  )  high = m -1 ;
            else  low = m + 1; 
        }
       for ( j=i-1; j>=high+1; - - j ) L.r[j+1] = L.r[j];
       L.r[high+1] = L.r[0];
       
     }
  } 
  

大部分原理都和直接插入排序一样,不同的一点是while循环,当while循环结束的时候才找到符合要求的位置。因为只要循环不结束,就表明一直有区间存在,区间不是一个位置,是多个元素。仍然不符合我们的需要。

希尔排序

希尔排序(Shell's Sort)又称“缩小增量排序”(Diminishing Increment Sort),是插入排序的一种,因D.L.希尔(D.L.Shell)于1959年提出而得名。

当待排序的记录个数较少且待排序序列的关键字基本有序时,直接插入排序效率较高。希尔排序基于以上两点,从“减少记录个数”和“序列基本有序”两个方面对直接插入排序进行了改进。

【算法步骤】

希尔排序实质上是采用分组插入的方法,先将整个待排序记录序列分割成几组,从而减少参与直接插入排序的数据量,对每组分别进行直接插入排序,然后增加每组的数据量,重新分组。这样当经过几次分组排序后,整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。

希尔对记录的分组,不是简单地“逐段分割”,而是将相隔某个“增量”的记录分成一组。

①第一趟取增量d1(d1<n)把全部记录分成d1.个组,所有间隔为d1的记录分在同一组,在各个组中进行直接插入排序。

②第二趟取增量d2(d2<d1),重复上述的分组和排序。

③依次类推,直到所取的增量dt=1,所有记录在同一组中进行直接插入排序为止。

【算法特点】

(1)记录跳跃式地移动导致排序方法是不稳定的。

(2)只能用于顺序结构,不能用于链式结构。

(3)增量序列可以有各种取法,但应该使增量序列中的值没有除1之外的公因子,并且最后一个增量值必须等于1。

(4)记录总的比较次数和移动次数都比直接插入排序的要少,n越大时,效果越明显。所以适合初始记录无序、n较大时的情况。

void   ShellInsert(SqList &L,int dk) {
      
for(i=dk+1;i<=L.length; ++ i)
      if(r[i].key < r[i-dk].key) {         
       r[0]=r[i];
       for(j=i-dk; j>0 &&(r[0].key<r[j].key); j=j-dk;
     r[j+dk]=r[j];
        r[j+dk]=r[0];
       }
}
void   ShellSort(SqList &L,int dlta[ ],int t){
        // 按增量序列dlta[0…t-1]对顺序表L作t趟Shell排序
   for(k=0;k<t;++k)
     ShellInsert(L,dlta[k]);//增量为dlta[k]的一趟插入排序
}  // ShellSort

总结就是:每个dk是一趟(多组dk),每趟可以有很多组(根据dk把数据分成的组),每组进行“直接插入排序”,完成一趟后,进行下一趟。当基本有序时(增量dk=1),对全体记录进行一次直接插入排序。(就相当于有增量的dk的直接插入排序)

每一步操作只不过是多了一个增量dk而已,我们通常的操作是dk=1,你带入思考一下就明白了。操作原理和直接插入排序没什么出入。

有什么不理解的可以给小新留言哦!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值