2.3.1 分治法

插入排序使用了增量方法:在排序子数组A[1..j-1]后,将单个元素A[j]插入子数组的适当位置,产生排序好的子数组A[1..j].

分治法,该算法的最坏情况运行时间比插入排序要少得多。分治算法的优点之一是,通过使用第4章介绍的技术往往很容易确定其运行时间。

分治法


许多有用的算法在结构上是递归的:为了解决一个给定的问题,算法一次或多次递归地调用其自身以解决紧密相关的若干子问题。这些算法典型地遵循分治法思想:将原问题分解为几个规模较小但类似于原问题的子问题,递归地求解这些子问题,然后再合并这些子问题的解来建立原问题的解。

分治模式在每层递归时都有三个步骤:

分解原问题为若干子问题,这些子问题是原问题的规模较小的实例。

解决这些子问题,递归求解各子问题。然而,若干子问题的规模足够小,则直接求解。

合并这些子问题的解成原问题的解。

归并排序算法完全遵循分治模式。直观上其操作如下:

  分解:分解待排序的n个元素的序列成各具n/2个元素的两个子序列。

  解决:使用归并排序递归地排序两个子序列

  合并:合并两个已排序的子序列以产生已排序的答案

当待排序的序列长度为1时,递归"开始回升",在这种情况下不要做任何工作,因为长度为1的每个序列都已排好序。

归并排序算法的关键操作是"合并"步骤中两个已排序列的合并。我们通过调用一个辅助过程MERGE(A,p,q,r)来完成合并,其中A是一个数组,p,q和r是数组下标,满足p<=q<r。该过程假设子数组A[p..q]和A[q+1..r]都已排好序。它合并这两个子数组形成单一的已排好序的子数组并代替当前的子数组A[p..r].

过程MERGE需要O(n)的时间,其中n=r-p+1是待合并元素的总数。它按以下方式工作。回到我们玩扑克的例子,假设桌面上有两堆牌面朝上的牌,每堆都已排序,最小的牌在顶上。我们希望把这两堆牌合并成单一的排好序的输出堆,牌面朝下地放在桌上。我们的基本步骤包括在牌面朝上的两堆牌中选取较小的一张,将该牌从其堆中移开(该堆的顶上将显露一张新牌)并牌面朝下地将该牌放置到输出堆。因为我们只是比较顶上的两张牌,所以计算上每个基本步骤需要常量时间。因为我们最多执行n个基本步骤,所以合并需要O(n)的时间。

下面的伪代码实现了上面的思想,但有一个额外的变化,以避免在每个基本步骤必须检查是否有堆为空,在每个堆的底部放置一张哨兵牌,它包含一个特殊的值,用于简化代码。这里我们使用∞作为哨兵值,结果每当显露一张值为∞的牌,它不可能为较小的牌,除非两个堆都已显露出其哨兵牌。但是,一旦发生这种情况,所有非哨兵牌都已被放置到输出堆。因为我们事先知道刚好r-p+1张牌将被放置到输出堆,所以一旦已执行r-p+1个基本步骤,算法就可以停止。

MERGE(A,p,q,r)

1   n1 = q-p+1

2   n2 = r-q

3   let L[1..n1+1] and R[1..n2+1] be new arrays

4   for i = 1 to n1

5      L[i] = A[p+i-1]

6   for j = 1 to n2

7      R[j] = A[q+j]

8   L[n1+1] = ∞

9   R[n2+1] = ∞

10  i = 1

11  j = 1

12  for k = p to r

13     if L[i]<=R[i]

14       A[k] = L[i]

15       i = i + 1

16     else A[k] = R[j]

17       j = j + 1

过程MERGE的详细过程如下:第1行计算子数组A[p..q]的长度n1,第2行计算子数组A[q+1..r]的长度n2.在第3行,我们创建长度分别为n1+1和n2+1的数组L和R("左"和"右"),每个数组中额外的位置将保存哨兵。第4-5行的for循环将子数组A[p..q]复制到L[1..n1],第6-7行的for循环将子数组A[q+1..r]复制到R[1..n2].第8-9行将哨兵放在数组L和R的末尾。第10-17行,通过维持以下循环不变式,执行r-p+1个基本步骤:

在开始第12-17行for循环的每次迭代时,子数组A[p..k-1]按从小到大的顺序包含L[1..n1+1]和R[1..n2+1]中的k-p个最小元素。进而,L[i]和R[i]是各自所在数组中未被复制回数组A的最小元素。

我们必须证明第12-17行for循环的第一次迭代之前该循环不变式成立,该循环的每次迭代保持该不变式,并且循环终止时,该不变式提供了一种有用的性质来证明正确性。

初始化:循环的第一次迭代之前,有k=p,所以子数组A[p..k-1]为空。这个空的子数组包含L和R的  k-p=0个最小元素。又因为i=j=1,所以L[i]和R[j]都是各自所在数组中未被复制回数组A的最小元素。

保持:为了理解每次迭代都维持循环不变式,首先假设L[i]<=R[j].这时,L[i]是未被复制回数组A的最小元素。因为A[p..k-1]包含k-p个最小元素,所以在第14行将L[i]复制到A[k]之后,子数组A[p..k]将包含k-p+1个最小元素。增加k的值(在for循环中更新)和i的值(在第15行中)后,为下次迭代重新建立了该循环不变式。反之,若L[i]>R[i],则第16-17行执行适当的操作来维持该循环不变式。

终止:终止时k=r+1.根据循环不变式,子数组A[p..k-1]就是A[p..r]且按从小到大顺序包含L[1..n1+1]和R[1..n2+1]中的k-p=r-p+1个最小元素。数组L和R一起包含n1+n2+2=r-p+3个元素。除两个最大的元素以外,其它所有元素都已被复制回数组A,这两个最大元素就是哨兵。

时间: 2024-08-15 03:49:52

2.3.1 分治法的相关文章

专题:分治法

分治法(Divide and Conquer) 作为五大算法之一的分治法,可算是最早接触的一种算法.分治法,与其说是一种算法,不如将其称为策略来的更贴切一些.算法的思想就是将大问题分成小问题,并解决小问题之后合并起来生成大问题的解. 分治法的精髓: 分--将问题分解为规模更小的子问题: 治--将这些规模更小的子问题逐个击破: 合--将已解决的子问题合并,最终得出“母”问题的解: 分治法的作用,自然是让程序更加快速地处理问题.比如一个n的问题分解成两个n/2的问题,并由两个人来完成,效率就会快一些

分治法(一)

这篇文章将讨论: 1) 分治策略的思想和理论 2) 几个分治策略的例子:合并排序,快速排序,折半查找,二叉遍历树及其相关特性. 说明:这几个例子在前面都写过了,这里又拿出来,从算法设计的策略的角度把它们放在一起来比较,看看分治是如何实现滴.由于内容太多,我将再花一篇文章来写4个之前没有写过的分治算法:1,大整数乘法   2,矩阵乘法的分治策略   3,最近点对  4,凸包问题,请见下一篇. 好了,切入正题. --------------------------------------------

分治法

分治法的基本思想是将一个规模为n的问题分解为k个规模较小的子问题,这些子问题相互独立且与原问题相同.递归的解这些子问题,然后将各子问题的解合并得到原问题的解. 分治法所能解决的问题一般具有以下几个特征: 1) 该问题的规模缩小到一定的程度就可以容易地解决 2) 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质. 3) 利用该问题分解出的子问题的解可以合并为该问题的解: 4) 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题. 分治法的基本步骤:分治法在

算法实验:分治法合并排序(C++)

这篇文章分两部分来写,第一部分写代码的实现过程,第二部分把实验报告从头到尾呈现出来. 我习惯调试使用的编译器是DEV C++,不是vs系列的,可能头文件上有点区别.但是下面的报告是我放到vs里面测试过的,可以直接用,不影响. 第一部分:(解析) 题目:随机产生一个整型数组,然后用合并排序将该数组做升序排列,要求输出排序前和排序后的数组. 题目分析: 需要随机产生一个整数数组: 采用的算法是合并排序,也就是用归并排序: 输出排序后的数组. 随机产生一个整数数组:这个问题首先想到的是用rand()函

分治法与递归编程步骤

分治法是一种很强大的算法设计方法.基本思想是:将原问题分解为几个规模小但类似于原问题的子问题,递归的求解这些子问题,然后再合并这些子问题的解来建立原问题的解. 在分治策略中,递归地求解一个问题,在每层递归中应用如下三个步骤: (1)分解(Divide):将原问题分解为一些子问题,子问题的形式与原问题一样,只是规模更小. (2)解决(Conquer):递归地解出子问题.如果子问题规模足够小,则停止递归,直接求解. (3)合并(Combine):将子问题的解组合成原问题的解. 分治思想体现在编码上,

分治法 求 逆序对数 的个数 时间复杂度为O(n*logn)

思路: 分治法 归并排序的过程中,有一步是从左右两个数组中,每次都取出小的那个元素放到tmp[]数组中 右边的数组其实就是原数组中位于右侧的元素.当不取左侧的元素而取右侧的元素时,说明左侧剩下的元素均比右侧的第一个元素大,即均能构成一个逆序对.假设现在左侧剩余n个元素,则逆序对数+n. 另外,如果当所有右侧的元素都取完,但是左侧仍然有元素剩余时,左侧剩余的元素已经在之前的运算中加到了逆序对中,不需要再添加一次 下面给出 归并排序 和 求逆序对数 两份代码: code1: 归并排序 #includ

《github一天一道算法题》:分治法求数组最大连续子序列和

看书.思考.写代码! /*************************************** * [email protected] * blog: http://blog.csdn.net/hustyangju * 题目:分治法求数组最大连续子序列和 * 思路:分解成子问题+合并答案 * 时间复杂度:O(n lgn) * 空间复杂度:O(1) ***************************************/ #include <iostream> using nam

分治法-汉诺塔问题

一 基本概念 分治法,顾名思义分而治之的意思,就是把一个复杂的问题分成两个或很多其它的同样或相似的子问题,再把子问题分成更小的子问题--直到最后子问题能够简单的直接求解,原问题的解即子问题的解的合并. 二基本思想及策略 分治法的设计思想是:将一个难以直接解决的大问题,切割成一些规模较小的同样问题,以便各个击破,分而治之. 分治策略是:对于一个规模为n的问题,若该问题能够easy地解决(比方说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式同样,递归地解

分治法-最近距离问题Java实现

分治算法,有很多典型的问题,如最近点问题.线性选择问题.整数划分问题.大整数成绩问题.棋盘覆盖问题.循环赛日程表.二分搜索.Strassen矩阵乘法.汉诺塔等.准备花些时间逐个解决这些问题,并用Java实现,从最近点问题开始.网上找到一些代码,标题如"java 用蛮力法和分治法求解最近对有关问题",虽然体现了分治,但划分不够彻底,因此我重新对其进行了实现. 一.基本思想及策略: 首先,说说分治的思想.分治, "分而治之",就是把一个复杂的问题分成两个或更多的相同或相

分治法--二分查找、乘方、斐波那契数

1.二分查找 常见错误: 死循环:循环体外的初始化条件,与循环体内的迭代步骤, 都必须遵守一致的区间规则,也就是说,如果循环体初始化时,是以左闭右开区间为边界的,那么循环体内部的迭代也应该如此.如果两者不一致,会造成程序的错误. 溢出:middle = left + (right - left) / 2 终止条件:一般来说,如果左闭右闭,则left<=right: 如果一开一闭,则left<right: 关键看left能不能等于right,而且要考虑实际情况,有时不能这样简单终结,会出现死循环