二维数组的二分法查找

二维数组内的二分查找

发表于:2012年04月18日  12:07 0

【题目】
已知一个二维整数数组(或者说,一个整数矩阵),每行从左到右递增,每列从上往下递增。要求定义一个函数,输入矩阵和某个整数k,判断k是否在这个矩阵内。

【思路】
像四叉树那样递归查找,这是一种思路,不过我不是很喜欢。我希望一个循环就能搞定,不希望碰到类似于回溯之类的逻辑。

如果一个循环就搞定,那么最好是能像切西瓜一样,把不要的部分一点点切掉,同时剩下的部分仍旧是规整的一块。

观察以下矩阵(为方便行文,矩阵内全是正整数,大于0小于10的数在十位上补0)。
01,02,08,09
02,04,09,12
04,07,10,13
06,08,11,15

易得,该矩阵的最小值m在左上角,最大值M在右下角。易证得,这不是个案,是个普遍的规律;并且,从这样的一个矩阵中任意圈出一个子矩阵,也符合这个规律。一个整数k,如果小于m或者大于M,则必然不在这个矩阵内。

继续探索这种矩阵的性质,看是否有线索。

将该矩阵分成上下两部分:
01,02,08,09
02,04,09,12
 ------------
04,07,10,13
06,08,11,15

如果一个整数k,大于上半部分右下角的值(这里是12),则k必然不在上半部分。

如果一个整数k,小于上半部分左上角的值(这里是04),则k必然不在下半部分。

将该矩阵分成左右两部分:
01,02 | 08,09
02,04 | 09,12
04,07 | 10,13
06,08 | 11,15

如果一个整数k,大于左半部分右下角的值(这里是08),则k必然不在左半部分。

如果一个整数k,小于右半部分左上角的值(这里是08),则k必然不在右半部分。

有以上推论做基础,我们就可以设计“切蛋糕”式的算法了。

【方法1】

先说个思路,不太严格:

01:在最上面的一条边上,通过二分法查找k,找到就退出。否则,找到比k大的最小整数,将其所在及右边的列统统“切掉”。

02:在最下面的一条边上,通过二分法查找k,找到就退出。否则,找到比k小的最大整数,将其所在及左边的列统统“切掉”。

03:在最左面的一条边上,通过二分法查找k,找到就退出。否则,找到比k大的最小整数,将其所在及下边的行统统“切掉”。

04:在最右面的一条边上,通过二分法查找k,找到就退出。否则,找到比k小的最大整数,将其所在及上边的行统统“切掉”。

05:以上每一步中,“切”之前的矩阵大小如果为0,则失败退出;以上四步结束后,如果还没找到,则返回01,继续切。

举例如下。在以下矩阵中寻找07。
01,02,08,09
02,04,09,12
04,07,10,13
06,08,11,15

过程如下:
01:最上面一行,找不到07,而比07大的最小整数是08。切一刀,得到以下矩阵
    01,02
    02,04
    04,07
    06,08
02:最下面一行,找不到07,而比07小的最小整数是06。切一刀,得到以下矩阵
    02
    04
    07
    08
03:最左面的一列……好吧,找07应该很容易了。顺利退出。

以上所描述的算法,有两个问题没解决:第一,是否已经最优;第二,怎样从数学上证明,“算法找到目标数k”的充分条件就是“矩阵含有目标数k”。后者不难证,前者要考虑。

【方法2】
仍旧使用这个例子:在以下矩阵中寻找07。
01,02,08,09
02,04,09,12
04,07,10,13
06,08,11,15

考察方法1切割矩阵的过程,我们可以发现,第二刀似乎白切了,只需要第一刀和第三刀。问题来了:能否把方法1中的第二步忽略掉?而第三步和第四步,能否也只保留一个?

先将这个算法写出来,再探讨它是否有效。
01:在最上面的一条边上,通过二分法查找k,找到就退出。否则,找到比k大的最小整数,将其所在及右边的列统统“切掉”。

02:在最右面的一条边上,通过二分法查找k,找到就退出。否则,找到比k小的最大整数,将其所在及上边的行统统“切掉”。

03:以上每一步中,“切”之前的矩阵大小如果为0,则失败退出;以上两步结束后,如果还没找到,则返回01,继续切。

需要证明有效性:
> 当k确实存在于矩阵中的时候,以上算法中的第一步和第二步都是可以执行的;
> 当算法结束的时候,如果没找到k,那么矩阵中肯定没有k

讨论如下:
* 令矩阵左上角的数为m,右下角的数为M。当k确实存在于矩阵中的时候,它必然满足m<=k<=M。
* 如果我们以矩阵右上角为轴,将矩阵最右边一列逆时针旋转90度,与矩阵最上面一行拼起来,必然形成一个头为m尾为M的递增数列S。
* 如果m<=k<=M,那么要么能在S中找到k,要么能找到两个相邻的数p和q,满足p * p和q要么同在最上面一行,要么同在最右边一列。因此,第一步和第二步,至少有一个能产生一个有效的“切割”,排除掉至少一行或者一列。
* 完成一个有效切割之后,k必然仍旧在余下的矩阵内,令其左上角的数为m'右下角的数为M'则必然满足m'=k<=M'
* 因此,“k确实存在于矩阵中”是“方法2能找到k”的充分条件。

经过以上的讨论,方法2可以改良一下:
01:令矩阵左上角的数为m,右下角的数为M。判断目标数k是否满足m<=k<=M。不满足,则失败退出。
02:以矩阵右上角为轴,将矩阵最右边一列逆时针旋转90度,与矩阵最上面一行拼起来,形成一个头为m尾为M的递增数列S。
03:在S中查找k。找到,则成功退出。否则,应该能找到两个相邻的数p和q,满足p 04:如果p和q同在最上面一行,则将q所在列及其右边的列“切掉”,返回01;
05:如果p和q同在最右边一列,则将p所在行及其上边的行“切掉”,返回01

该方法的时间复杂度,应该是O((h+w)*log(h+w)),其中h是矩阵高,w是矩阵宽。

应该还可以再优化。

你可能感兴趣的:(实践总结经验)