算法中的一个模式:栈式遍历

说到算法,我们都知道,它是一个能够有效解决问题的指令序列。
说到模式,我们都会想到design pattern,它是在软件设计中不断出现的可重用的解决方案。

那么,算法中有没有模式呢?答案是yes。
为了和design pattern区分,我把算法中的模式定义为,在各种算法中不断出现的类似的解决问题方式。

这里我想讲一个在很多算法中都出现过的过程,我把它命名为“栈式遍历”。
我们先定义一下这个过程:
首先,有一个数组a[i] (0<=i<n)。
其次,我们有一个栈,指针top指向栈顶元素。
再次,我们有一个布尔型的判别函数f(top-k+1,top-k+2,..., top, i),表示栈顶的k个元素和a[i]是否相容。
遍历过程是这样的:


1:  for i=0 to n-1
2:    while(栈里面至少有k个元素 and
3:          f(top-k+1,top-k+2,..., top, i) = false)
4:      令栈顶元素退栈
5:    end while
6:    令a[i]进栈
7:  end for

这个过程很简单,通俗的讲就是,如果遍历过程中的a[i]和栈顶的若干数不相容,就一直退栈,直到它们相容了或者栈顶元素不够了。然后把a[i]放进栈里。在这个过程中,每个数进栈一次,至多出栈一次,所以时间复杂度是O(n)。

下面,我们就看看它是如何在具体问题中发挥作用的。
第一个例子是一个很常见的面试问题:
给定一个n*n的数组a[i,j],数组的每个元素不是0就是1。我们要求的是,由1组成的最大矩形面积。比如下面的数组:
                        0000
                        1110
                        0110
                        0111
最大矩形面积是6。
对于这个问题,这里只讨论如何在O(n^2)的时间复杂度下求出该面积。为了解决问题,我们先解决一个稍微容易一些的子问题,有了这个子问题的结论,原问题就不费吹灰之力了。
考虑下列问题:
假设平地有n个高高低低的木棍排成一排,其高度用a[0..n-1]表示,其宽度为1。我是否能在O(n)的时间内,求出这些木棍所覆盖的最大矩形面积?比如我有高度为1,3,2,4,1的5根木棍,如下图所示
                               |
                             | |
                             |||
                            |||||
其覆盖的最大矩形区域由下图中的*号表示,面积为6。
                                |
                             |  |
                            ***
                           |***|

如果能够在O(n)的时间解决这个子问题,原问题一定可以在O(n^2)的时间内解决。这是因为,如果把a[i,j]数组的每一行切开向上看去,连续的1就是子问题中的木棍!

下面我们看看栈式遍历是如何来解决这个子问题的。
经观察我们发现,对于每一根木棍,总是有一个由它的高度所决定的最大矩形,这个矩形有一个左边界,一个右边界,在这两个边界里面,所有的其它木棍都不会比这根木棍矮(上例中由*号覆盖的区域就是由高度为2的木棍决定的)。而问题的解就是每个木棍所决定的最大矩形中的最大者。为了寻找由某个木棍高度决定的最大矩形,我们必须找到这两个边界。显然,如果朴素地对每个木棍分别向两边循环查找,时间复杂度一定高O(n)。那么有什么不朴素的方法呢?这个时候,栈式遍历闪亮出场了!

我们定义判别函数为f(top, i) = true   if a[i]>=栈顶元素
                                       = false  if a[i]<栈顶元素
(这里只需要跟栈顶元素比较,所以k为1)
然后我们按照上面所讲的栈式遍历,对数组a[i]从左到右进行遍历。如果我们把0..n-1看做时间的话,那么对于每个a[i],我们发现,它出栈的那个时刻,即是它所决定的那个最大矩形的右边界!!对于最后没有出栈的木棍,我们把n作为右边界。也就是说,在一次栈式遍历的过程中,我们可以同时找到所有木棍所决定的最大矩形的右边界。同理,我们再从右往左遍历一次,就可以找到它们的左边界。因此,在O(n)的时间内,我们把问题解决了。再回到原问题,我们利用栈式遍历得到了一个O(n^2)的算法。

第二个例子,我想讲讲计算几何中求凸壳的graham scan算法。这个经典算法,就是用栈式遍历得到的。
对于平面中的一堆点,我们要求它的凸壳。graham scan算法可以描述为:
1:  将所有点p(x,y)按照y从小到大排序,如果y相同则比较x
2:  将p0和p1入栈
3:  for i=2 to n-1
4:    while(栈中至少有2个元素 and
5:             S(p_top-1, p_top, pi)<=0) (S函数用来计算3个点所组成的有向面积)
6:      将栈顶元素退栈
7:    end while
8:    将pi入栈
9:  end for
我们看到了,这个就是栈式遍历!这里的k为2,即对每个点pi要根据栈顶的2个点才能判别是否相容。
事实上,上述算法只找到了凸包中右半边的点。我们令i从大到小再做一遍,即可得到凸包左半边的点。
对于graham scan算法的更多的信息,可以参考wiki:http://en.wikipedia.org/wiki/Graham_scan。

最后总结一下,这篇文章里讲述了一个利用栈对数据进行处理的方式。这个方式很简单,但是通过对判别函数的定义,我们可以用它发掘出数据中潜在的相互关系,并通过一些遍历过程中的附加信息(比如进栈和出栈的时间)解决问题。我们可以把这个算法的模式看做一种解决问题的思路,当面对新问题的时候,尝试一下也许能有意想不到的效果。

  • 2
    点赞
  • 0
    收藏
  • 打赏
    打赏
  • 0
    评论

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论

打赏作者

gnefuil

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值