在已排序的矩阵中搜索最有效的方法是什么?

8

我有一个任务,需要编写一个算法(不是任何特定语言,只是伪代码),接收一个以特定方式排序的矩阵[大小:M x N],其中所有行都已排序并且所有列也各自排序,并在该矩阵中查找某个特定值。我需要编写最高效的算法。

该矩阵看起来像:

1  3  5
4  6  8
7  9 10

我的想法是从第一行和最后一列开始,简单地检查值。如果它更大,则向下移动;如果它更小,则向左移动,并继续这样做,直到找到该值或索引超出边界(如果该值不存在)。这个算法的时间复杂度为线性O(m+n)。我被告知可以使用对数复杂度来实现。是否可能?如果可能,如何实现?


你能分享一个数据的例子吗?你肯定有收到样本。 - jcolebrand
“所有行都已排序,且所有列都已单独排序”:这是什么意思? - TonyK
@sagar 但这不是教授给出的例子。否则他上面的方法是最快的(先检查行末,然后继续)。此外,首先检查中间行的末尾会更快,有点像二分搜索。 - jcolebrand
啊,好的。没注意到8/7的问题。 - Sagar
你是否已经成功解决了这个问题?还需要帮助吗? - jcolebrand
显示剩余6条评论
9个回答

5
您的矩阵看起来像这样:

a ..... b ..... c
. .     . .     .
.   1   .   2   . 
.     . .     . .
d ..... e ..... f
. .     . .     .
.   3   .   4   .
.     . .     . .
g ..... h ..... i

并具有以下属性:

a,c,g < i  
a,b,d < e
b,c,e < f
d,e,g < h
e,f,h < i

因此,矩阵右下角的值(例如i)始终是整个矩阵中最大的值,并且如果将矩阵分成4个相等的部分,则此属性是递归的。

因此,我们可以尝试使用二进制搜索:

  1. 探测值,
  2. 将其分成若干块,
  3. 选择正确的块(以某种方式),
  4. 使用新的块返回1。

因此,算法可能如下所示:

input: X - value to be searched
until found
 divide matrix into 4 equal pieces
 get e,f,h,i as shown on picture
 if (e or f or h or i) equals X then 
   return found
 if X < e then quarter := 1
 if X < f then quarter := 2
 if X < h then quarter := 3
 if X < i then quarter := 4
 if no quarter assigned then 
    return not_found
 make smaller matrix from chosen quarter 

对我来说,这似乎是一个O(log n)的算法,其中n是矩阵中元素的数量。它有点像二维的二分查找。我不能正式地证明它,但它类似于典型的二分查找。


唯一的问题是最终矩阵的大小为MxN,乍一看似乎这种方法并不那么可扩展。但是,也许你已经找到了一些线索,也许math.stackexchange.com上的专家可以帮助你进一步完善它? - jcolebrand
4
“如果X < f,则quarter := 2”——很遗憾,这并不正确。第3季度可以包含比e大但小于f的值。例如,示例输入中的7。 - Steve Jessop
1
如果整个MxN矩阵都按行排序,且第i行的所有元素均小于第i+1行的所有元素,则其时间复杂度为O(log n),这是在搜索已排序的一维数组时的最优复杂度。但实际情况肯定不会这么理想,因此时间复杂度必然更高。 - Martin G

2
那么这就是样本输入的样子?按对角线排序?确实是一种有趣的排序方式。
由于接下来的行可能会有比该行任何值都要低的值,因此您不能假设给定数据行的任何特定内容。
如果我需要处理大量输入,则会将矩阵读入一个列表结构中,该列表将数据作为元组的一对,将mxn坐标作为元组的一部分,然后对矩阵进行快速排序,然后按值查找它。
或者,如果每个单独位置的值是唯一的,请将MxN数据放入以值为键的字典中,然后根据输入的键(或输入键的哈希值)跳转到MxN的字典条目。
编辑:
请注意,如果您需要多次查看矩阵,则上述答案是有效的。如果您只需要解析一次,则这是您可以执行的最快速度。
for (int i = 0; i<M; i++)
 for (int j=0; j<N; j++)
  if (mat[i][j] == value) return tuple(i,j);

显然,我对这个问题的评论也应该放在这里: |

@sagar 但这不是教授给出的例子。否则他上面的方法是最快的(先检查行末,然后继续)。此外,首先检查中间行的末尾会更快,有点类似于二分搜索。

检查每一行的末尾(并从中间行的末尾开始)以找到内存数组中高于所检查数字的数字是最快的,然后在每个匹配的行上进行二分搜索,直到找到它。


对啊,对角线看起来是有序的,之前没有注意到。但排序不是按对角线进行的,它更简单。每一行都是按照自己的顺序排列的,每一列也是如此。但正如你所说,读取一行并不能提供关于下一行的任何信息。 - Bob
你不能根据下一列来判断当前列,也不能假设查找元素的速度,必须遍历每一个元素。我正在尝试寻找最快的查找值的方法。O(n)似乎是最快的,但如果你只是想解析一次,那么在MxN上使用两个for循环并在找到后提前退出会更快。 - jcolebrand
但是,一旦您需要查找两个值,我在答案中提供的方法比双重循环更快。 - jcolebrand
这也是我的想法。我认为在这种情况下O(n)已经足够好了,我看不出有什么更好的复杂度方式,除非再添加另一个数据结构。而且我也不需要搜索两次,在任务分配中指出,如果该值出现多次,我仍然只需要找到其中一个实例。 - Bob

2

在log M时间内,您可以获得能够包含目标的行范围(对行的第一个值进行二分查找,对行的最后一个值进行二分查找,仅保留那些第一个值小于等于目标且最后一个值大于等于目标的行)。两个二分查找仍然是O(log M)。
然后,在O(log N)时间内,您可以探索这些行中的每一行,同样使用二分查找!

这使得其为O(logM x logN)
tadaaaa


1
你可以进行一次二分查找,只需将较低指针保持在左侧,将较高指针保持在右侧。最终两者之间的所有内容都是要探索的行集。 - Kevin Stricker
2
为了简化数学,让我们假设大小为NxN。现在让我们来看一个糟糕的情况:假设第一行中所有值都小于10,而最后一行中所有值都大于100,我正在寻找目标=50。在这种情况下,我无法消除任何一行,因此仍然需要对n行进行二分搜索,这将给出O(n*logn)。 - Bob
@Bob ~ 是的,我们知道,但这样你就要从最后一行开始向前进行二分查找了。你知道二分查找算法吗?http://en.wikipedia.org/wiki/Binary_search_algorithm - jcolebrand
2
我支持Bob的观点,这个答案是错误的,应该被踩。最坏情况下的复杂度仍然是O(M.Log(N)),第一次搜索很可能会返回整个行集。 - Rabih Kodeih
同样被踩。O(n)符号代表最坏情况下的复杂度,而不是O(logM x logN)。你可以争辩说这个解决方案的期望时间更短,但它仍然保持着O(n*log n)的最坏情况复杂度。 - dcasadevall
是的,算法给出了正确的答案,但最坏情况下的运行时间完全错误。 - Vamsi Nerella

1

这是类似于Michal的回答(我将从中借鉴美丽的图形)的思路。

矩阵:

  min ..... b ..... c
    .       .       .
    .  II   .   I   . 
    .       .       .
    d .... mid .... f
    .       .       .
    .  III  .   IV  .
    .       .       .
    g ..... h ..... max

Min和Max分别是最小值和最大值。"mid"不一定是平均/中位数/任何值。

我们知道mid的值>=象限II中的所有值,并且<=象限IV中的所有值。我们不能对象限I和III做出这样的断言。如果我们递归,我们可以在每个级别消除一个象限。

因此,如果目标值小于mid,则必须搜索象限I、II和III。如果目标值大于mid,则必须搜索象限I、III和IV。

每步空间减少了3/4:

n * (3/4)x = 1

n = (4/3)x

x = log4/3(n)

对数只相差一个常数因子,因此这是O(log(n))。

find(min, max, target)
    if min is max
        if target == min
            return min
        else
            return not found
    else if target < min or target > max
        return not found
    else
        set mid to average of min and max 
        if target == mid
            return mid
        else
            find(b, f, target), return if found
            find(d, h, target), return if found

            if target < mid
                return find(min, mid, target)
            else
                return find(mid, max, target)

0
JavaScript 解决方案:
//start from the top right corner
//if value = el, element is found
//if value < el, move to the next row, element can't be in that row since row is sorted
//if value > el, move to the previous column, element can't be in that column since column is sorted

function find(matrix, el) {

  //some error checking
  if (!matrix[0] || !matrix[0].length){
    return false;
  }
  if (!el || isNaN(el)){
    return false;
  }

  var row = 0; //first row
  var col = matrix[0].length - 1; //last column

  while (row < matrix.length && col >= 0) {
    if (matrix[row][col] === el) { //element is found
      return true;
    } else if (matrix[row][col] < el) {
      row++; //move to the next row
    } else {
      col--; //move to the previous column
    }
  }

  return false;

}

0
public static boolean find(int a[][],int rows,int cols,int x){
    int m=0;
    int n=cols-1;
while(m<rows&&n>=0){
    if(a[m][n]==x)
        return1;
    else if(a[m][n]>x)
        n--;
    else m++;
}

}

0

这是错误的答案

我真的不确定任何一个答案是否是最优答案。我正在尝试。

  1. 二分查找第一行和第一列,找出“x”可能在哪一行和哪一列。你会得到0,j和i,0。如果在这一步中没有找到x,则x将在i行或j列上。
  2. 在步骤1中找到的行i和列j上进行二分查找。

我认为时间复杂度是2 *(log m + log n)。

如果输入数组是正方形(n * n),则可以通过沿对角线进行二分搜索来减少常数。


1
抱歉,这是错误的。想象一个4X4矩阵,其中第一行和第一列都看起来像1 2 3 10,你被要求查找7。你会说它在第3行第3列,但事实并非如此。7可能在(2, 2),而其余所有单元格都可以是999... - ihadanny

0

关于获取对角线,然后在对角线上进行二分查找,从右下角开始检查是否在对角线上方,如果是,则将对角线数组位置作为所在列的列号,否则检查是否在下方。每次在列上运行二分查找一旦在对角线上有命中(使用对角线的数组位置作为列索引)。我认为这就是@user942640所说的。

您可以获得上述运行时间,并在需要时(某个时刻)将算法交换为在初始对角线数组上进行二分查找(考虑到其n * n元素并且获取x或y长度为O(1),因为x.length = y.length。即使在百万*百万的二分查找中,如果对角线小于半步,则向上回退对角线,如果不小于,则向回二分查找到您所在的位置(这是在沿对角线进行二分查找时稍微更改的算法)。我认为对角线比沿行进行二分查找更好,只是我现在太累了,无法看数学 :)

顺便说一句,我认为运行时间与分析略有不同,你可以用最好/最坏/平均情况、时间与内存大小等方面来描述。因此,问题最好陈述为“在最坏情况分析中最好的运行时间是什么”,因为在最好情况下,你可以进行一次暴力线性扫描,并且该项可能位于第一个位置,这将比二分搜索更好的“运行时间”...

0

这里是 n 的下限。从长度为 n 的未排序数组 A 开始。根据以下规则构建新矩阵 M:次对角线包含数组 A,其上方的元素均为负无穷,其下方的元素均为正无穷。行和列都已排序,在 M 中查找条目与在 A 中查找条目相同。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接