2.3 设计算法

知识

分治模式

  • 分解:原问题分为若干子问题,这些子问题都是原问题的规模较小的实例。
  • 解决:递归求解各子问题。当子问题规模足够小则直接求解。
  • 合并:合并这些子问题的解成原问题的解。

归并排序

  • 分解:分解带排序的n个元素的序列为两个n/2的子序列。
  • 解决:使用归并排序递归地排序两个子序列。
  • 合并:合并两个已排序的子序列产生已排序的答案。

当排序序列长度为1时,递归开始回升。

下面是用RUBY的实现,没有使用哨兵牌。

def merge(arr,p,q,r)
arr1 = arr[p..q]
arr2 = arr[q+1..r]
k = p
  while !(arr1.empty?&&arr2.empty?) do         
    if arr1.empty?
      arr[k]=arr2.shift
    elsif arr2.empty?
      arr[k]=arr1.shift
    elsif arr1[0]<=arr2[0]
      arr[k]=arr1.shift
    else
      arr[k]=arr2.shift
    end  
    k=k+1
  end
end

def merge_sort(arr, p, r)
  if p < r
    q = (p+r)/2
    merge_sort(arr,p,q)
    merge_sort(arr,q+1,r)
    merge(arr,p,q,r)
  end
end

对于合并(merge)过程满足循环不变式:

  • 初始化:当循环开始前,k=p,所以数组arr[p..k-1]为空,满足不变式。
  • 保持:循环中,每一次循环都是把arr1和arr2从队列前端取较小放入arr中,而arr1和arr2是排序好的,所以保证了arr[p..k-1]也是排序好的。
  • 终止:终止时k=r+1, a[p..r] 全部排序完成。

分析分治算法

递归式根据在较小输入规模上的运行时间来描述在规模为n的问题上的总运行时间。

假设这个较小输入规模就是 n<=c, 此时 T(n) = Θ(1).

其他情况:T(n)=aT(n/b)+D(n)+C(n), 其中:

  • a是被分成的子问题的个数
  • n/b是子问题的输入规模(a和b不一定相同)
  • D(n)是分解问题的时间
  • C(n)是合并问题的时间

在归并排序算法中:

  • 分解:简单的计算与规模无关,所以D(n)=Θ(1)
  • 解决:分为2个规模为n/2的子问题,贡献2T(n/2)的运行时间
  • 合并:merge方法可以看出,C(n)=Θ(n)

D(n)相当于常数项,可以并进C(n)中,因为C(n)相当于一个线性函数。

所以:

T(n)=Θ(1)               n=1

T(n)=2T(n/2)+Θ(n)       n>1

使用c代替Θ(1)(为啥Θ(n)变成了cn呢,这是一个假设。假设c为T(1)与C(n)+D(n)中较大的那个,得出的就是运行时间的上界;假设为较小的得到的就是下界,这两个界不影响时间的阶,所以可以规避这个问题。):

T(n)= c                 n=1

T(n)=2T(n/2)+cn         n>1

下面来做一个递归树

T(n) =>     cn          =再展开==>      cn               ...

          /    \                     /    \

     T(n/2)    T(n/2)            cn/2      cn/2

                                /    \     /    \

                           T(n/4)T(n/4) T(n/4)  T(n/4)

最终:


结果出来啦,T(n) = cn*(lgn+1) = cnlgn + cn

所以,时间复杂度是Θ(nlgn)

习题

2.3-1 使用图2-4作为模型,说明归并排序在数组A=<2,41,52,26,38,57,9,49>上的操作

答:

2,9,26,38,41,49,52,57
2,26,41,52
9,38,49,57
2,41
26,52
38,57
9,49
2
41
52
26
38
57
9
49


2.3-2 重写merge,使之不用哨兵。

答:这种写法的效率经过测试慢于上面的实现方法。

def merge(arr,p,q,r)
arr1 = arr[p..q]
arr2 = arr[q+1..r]
k = p
  while  !(arr1.empty?&&arr2.empty?)  do
    if arr1.empty?
      arr[k..r] = arr2.shift(arr2.size)
    elsif arr2.empty?
      arr[k..r] = arr1.shift(arr1.size)
    elsif arr1[0]<=arr2[0]
      arr[k]=arr1.shift
    else
      arr[k]=arr2.shift
    end  
    k=k+1
  end
end</span>

2.3-3 使用数学归纳法证明,当n刚好是2的幂时,以下递归式的解是T(n) = nlgn

T(n) = 2             n=2

T(n) = 2T(n/2)+n     n=2^k,k>1

答:

第一数学归纳法
一般地,证明一个与自然数n有关的命题P(n),有如下步骤:
(1)证明当n取第一个值n0时命题成立。n0对于一般数列取值为0或1,但也有特殊情况;
(2)假设当n=k(k≥n0,k为自然数)时命题成立,证明当n=k+1时命题也成立。
综合(1)(2),对一切自然数n(≥n0),命题P(n)都成立。

按照数学归纳法,

首先 n = 2时,T(n) = 2*lg2 = 2,成立。

假设,当n = 2^k时,有T(2^k) = 2^k*lg(2^k) = k*2^k。

只需要证明当n=2^(k+1)的时候,T(2^(k+1))=(k+1)*2^(k+1)成立即可。

T(2^(k+1)) = 2T(2^(k+1)/2)+2^(k+1) = 2T(2^k)+2^(k+1) = 2k*2^k + 2^(k+1) = k*2^(k+1)+2^(k+1) = (k+1)2^(k+1)成立。

所以题干得证。


2.3-4 我们可以把插入排序表示为如下的一个递归过程。为了排序A[1..n],我们递归排序A[1..n-1],然后把A[n]插入已排序的数组A[1..n-1]。

      为插入排序的这个递归版本的罪化情况运行时间写一个递归式。

答:

  • 分解:A[1..n]的数组,分解成A[1..n-1]和A[n]两个部分,A[1..n-1]成为新的待排序数组
  • 解决:将A[1..n-1]的部分继续递归地进行排序
  • 合并:将A[n]插入已排序的A[1..n-1]中

也就是说知道遇到n=2时,递归开始回升。

合并的过程就是插入排序的一次循环的内容。

T(n) = Θ(1)                     n<=c

T(n) = aT(n/b)+D(n)+C(n)        Other

根据本题,C(n)的复杂度是Θ(n)。D(n)的复杂度是Θ(1). 由于每次都只有一个字数组进入迭代,所以a = 1, 而下一个迭代规模是 n-1,所以:

T(n) = T(n-1)+Θ(n)        Other

用c代入得到递归式:

T(n) = c                     n=1

T(n) = T(n-1)+ cn            Other



2.3-5回顾查找问题(参见练习2.1-3),注意到,如果序列A已排好序,就可以将序列的中点与v比较。根据比较结果,原序列中就有一半不用再做考虑了。

二分查找算法重复这个过程,每次都将序列的剩余部分规模减半。为二分查找写出迭代或递归的伪代码。证明:二分查找的最坏情况运行时间为Θ(lgn)

答:

递归版本:

  • 分解:把A[1..n]从中间分为两部分,并取其一作为子问题的输入
  • 解决:通过比较中间值取其一作为子问题的输入,进入递归继续查找
  • 合并:不需要合并结果集,用尾递归直接返回值

伪代码:

search(A,l,r,v)
    if l < r
        m = (l+r)/2
        if A[m] < v
            return search(A,m+1,r,v)
        else if A[m] > v
            return search(A,l,m-1,v)
        else
            return m
    else
        if A[l] == v
            return l
        else 
            return nil
迭代版本:
search (A,v)
    start = 1
    end = A.size
    result = nil
    while start <= end 
        m = (start +end)/2
        if A[m]>v
            end = m-1
        else if A[m] < v
            start = m+1
        else
            result = m
            break


递归式:

T(n) = c    n = 1

T(n) = T(n/2)+c    n >1

按照上面做成递归树,共lgn+1层,所以是Θ(lgn)


2.3-6 注意到2.1节中的过程INSERTION-SORT的第5-7行的while循环采用一种线性查找来(反向)扫描已排好序的数组A[1..j-1]。我们可以使用二分查找来把插入排序的最坏情况总运行时间改进到Θ(nlgn)嘛?

答:

不可以,因为不仅仅是查找就可以,还需要把元素移动的过程,这是无法避免的。


2.3-7 描述一个运行时间为Θ(nlgn)的算法,给定n个整数的集合S和另一个整数x,该算法能确定S中是否存在两个其和刚好为x的元素。

答:

这个算法第一步是作差,第二步是在之后的集合中使用二分查找找到这个差。

外层循环需要进行(n-1)次,第二步复杂度Θ(lgn),所以(n-1)lgn取高次项为nlgn,所以是Θ(nlgn).

猜你喜欢

转载自blog.csdn.net/lokira518/article/details/47273757
2.3
今日推荐