归并排序算法

文章介绍了归并排序这种基于分治法的排序算法,通过递归地将表分割成两半,然后合并已排序的子表,以实现高效排序。相比选择排序,归并排序在大数据量时表现更优。文章详细讲解了合并和分割的过程,并提供了使用链表表示的C语言实现示例。
摘要由CSDN通过智能技术生成

目录

        合并

        分割

        排序

        完整代码


        现在要考虑一种名为归并排序的与选择排序有着天壤之别的排序算法。递归的方式能最好地描述归并排序,而归并排序展示了分治法的强大,在这种排序方法中,我们通过将问题“分为”大小减半的两个相似问题来为表(a1,a2,a3,…,an),从原则上讲,可以首先将原表分为两个元素任选的大小相等的表,不过在我们开发的程序中,将会将其分为一个含有奇数编号元素的表(a1,a3,a5,…),以及一个含有偶数编号元素的表(a2,a4,a6…),接着单独为大小减半的两个表排序。,随着待排序表长度n的增加,归并排序所需时间的增长速度要远慢于选择排序所需时间的增长速度。因此,即便递归调用会额外耗费些时间,当n很大时,还是应该优先使用归并排序而不是选择排序。


合并

  “合并”是指用两个已排序表生成一个只包含这两个表中所有元素的已排序表。请注意,对未排序的表谈“合并”是没有意义的。

  有一种合并两个表的简单方式,就是从表开头开始分析它们。在每一步中,我们找出两个表当前开头位置的两个元素中较小的那个,选择该元素作为合并后的表的下一个元素,并将该元素从它原来所在的表中删除,使该表具有一个新的“首位”元素。虽然我们在两个表开头的元素相同时会选取第一个表开头的元素,但是持平关系的打破是具有任意性的。

  例:考虑合并以下两个表。

  L1 (1, 2,7,7,9) 和 L2  (2,4,7,8)

  两个表的第一个元素分别为1和2 。因为1比较小,所以将其选作合并后的表M的第一个元素,并将1从 L1中删除,因此新的 L1就是 (2,7,7,9) 。现在,L1和 L2 的第一个元素都是2。可以任选其一。假设采取持平情况下总是从 L1中选取元素的策略,那么合并后的表M就变为(1,2),表L1变为 (7,7,9) ,而 L2 仍为 (2,4,7,8) 。

   我们将会发现,如果把表表示为链表,设计归并算法的工作会更简单。接着,要假设表的元素都为整数。因此,每个元素都能表示为一个“单元”,或者说是struct CELL类型的结构体,而表则表示为指向CELL的LIST类型的指针。这些定义都是由DefCell(int, CELL, LIST)宏来定义的。这种对DefCell宏的使用会扩展为:

typedef struct CELL *LIST;
struct CELL
{
    int element;
    LIST next;
};

  每个单元的element字段都含有一个整数,而next字段则含有指向表中下一单元的指针。如果当前的元素是表中最后一个元素,next字段就含有表示空指针的值NULL。然后整列整数就会用指向表第一个单元的指针(即一个LIST类型的变量)来表示。而空表会用值为NULL的变量(而不是指向第一个元素的指针)来表示。

 LIST merge(LIST list1, LIST list2)
 {
     if (list1 == NULL)return list2;
     else if (list2 == NULL)return list1;
     else if (list1->element <= list2->element)
     {
         /*  在这里,两个表都不为空,
             而且第一个表的首个元素更小。
             得到的结果就是第一个表的第一个元素,
             后面跟上其余元素的合并。*/
         list1->next = merge(list1->next, list2);
         return list1;
     }
     else
     {
         /* list2 的首个元素更小 */
         list2->next = merge(list1, list2->next);
         return list2;
     }
 }

  merge函数接受两个表作为参数,并返回合并后的表。也就是说,形式参数list1和list2是指向两个给定表的指针,而返回值是指向合并后的表的指针。递归算法可描述为如下形式。 

  依据。如果任一表为空,那么另一个表就是所需的结果。这条规则是通过图2-24中的第(1)行和第(2)行实现的。请注意,如果两个表都为空,就将返回list2。不过这是正确的,因为这里list2的值是NULL,而两个空表的结合还是空表。

   归纳。如果两个表都不为空,那么每个表都有第一个元素。我们可以将两个表的第一个元素分别称为list1->element和list2->element,即分别由list1和list2指向的单元的element字段。返回的表从含有最小元素的单元开始。该返回表其余的部分由两个表中除这个最小元素之外的所有元素组成。


分割

  归并排序的另一项重要任务是将一个表均分为两个表,或者,如果原表的长度为奇数,就分为长度只相差1的两个表。要完成这一工作,一种方式是数出表中元素的数目,然后除以2,并在表的中点将其拆分。我们将给出一个简单的递归函数split,将这些元素“处理”进两个表,其中一个表由第1个、第3个、第5个等元素组成,而另一个表则由偶数位置的元素组成。更确切地说,split函数会将偶数编号的元素从作为参数给出的表中删除,并返回一个由这些偶数编号元素组成的新表。

 LIST split(LIST list)
 {
     LIST pSecondCell;
 
     if (list == NULL)return NULL;
     else if (list->next == NULL)return NULL;
     else
     {
         /* 有至少两个表 */
         pSecondCell = list->next;
         list->next = pSecondCell->next;
         pSecondCell->next = split(pSecondCell->next);
         return pSecondCell;
     }
 }

  它的参数是LIST类型的表,这样定义是和merge函数有关的。请注意,局部变量pSecondCell被定义为LIST类型。这里是将pSecondCell用作指向表第二个单元(而不是指向表本身)的指针,不过其实LIST类型当然是指向单元的指针。

  split是个具有副作用的函数。它会从作为参数给出的表中删除偶数位置的单元,而且它会将这些单元组合成一个作为该函数返回值的新表。我们能以如下形式,用归纳的方式描述该分割算法。它对表的长度进行了归纳,这段归纳具有多个依据情况。

  依据。如果表的长度为0或1,那么我们什么都不用做。这就是说,空表会被“分割成”两个空表,而只有一个元素的表,在分割时会将唯一的元素留在给定的表中,并返回一个空的偶数编号元素表(因为原表没有偶数编号的元素,所以这个表中没有元素)。

  归纳。归纳步骤适用于list中至少存在两个元素的情况。具体如下图


排序

 LIST MergeSort(LIST list)
 {
     LIST Secondlist;
 
     if (list == NULL)return NULL;
     else if (list->next == NULL)return list;
     else
     {
         /* 表中至少有两个元素 */
         Secondlist split(list);
         /* 请注意,这样做的副作用是有一半元素会从表中删除 */
 
         return merge(MergeSort(list), MergeSort(Secondlist));
     }
 }

  依据。如果待排序的表为空或长度为1,那么只要返回该表即可,因为它是已排序的。

  归纳。如果待排序表的长度至少为2,那么在第(10)行使用split函数,从list中删除偶数编号的元素,并使用这些被删除的元素组成另一个表,该表是由局部变量SecondList指向的。第(13)行会递归地为大小减半的表排序,并返回这两个表的归并结果。

  值得注意的是,分割和合并操作是交替进行的,而不是在完成所有分割工作后再进行合并。

完整代码:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值