一 背景
遇到的一道算法题:已知矩阵内的元素,每行 从左到右递增;每列 从上到下递增; 给定一个数字t,要求判断矩阵中是否存在这个元素。
要求:时间复杂度尽可能低
二 概念
这样的矩阵也叫做杨氏矩阵,通常可以用二维数组来表示。
杨氏矩阵示例(1):
这里有一个需要注意的地方,每行的递增和每列的递增,并不能保证跨行情况下的右边数字一定大于左边数字。我们只能知道 左上一定小于右下。
之所以描述这么多,是因为这道查找题目的解答一定要建立在对杨氏矩阵的理解之上。
三 解法和思考
3.1 数组遍历
m行n列数组,逐个数字遍历,最差的时间复杂度为 O(mxn);
3.2 遍历优化-1
3.1的解法没有利用任何已知信息。考虑到一行数字,从左到右递增,那么我们可以在3.1的基础上,把每行内的查找改为使用二分查找的方式,时间复杂度为O(m logn)
如果m!=n,那么还可以降为O(min(mxlogn,nlogm))
3.3 遍历查找优化-2
杨氏矩阵查值的优化:由于杨氏矩阵从左到右从上到下都是逐渐递增的,假如找11这个数,先从第一行从左到右,如果找到大于11的第一个值,此时表明这一行没有值,这时向下找,看下面的值如果大于11向左找,如果找到小于11的第一个值,此时说明这一行也没有要找的值,这时向下继续找,如果下面的值小于要找的值就向右找,如此反复就可以找到目标值,相比于遍历查找少了很多的比较,但是实现过程也比较复杂
3.4 递归解法
所有元素都扫描一遍用递归解法。由杨氏矩阵的特点我们可以每次查找矩阵中当前元素的下边和右边直到要查找的数key小于当前元素那就说明没有这个数不存在返回false,就这样每次改变要查找元素的坐标并递归调用该方法,直到元素的坐标大于这个二维数组的长度时返回false即可。
3.5 分治法查找
在元素中取第一个元素的对角线,由于其特点对角线上的元素也是递增的,如果有就在对角线上,如果没有就找和这个目标值相邻的两个数再通过这两个数找到两个可能存在的子矩阵。之后继续每个矩阵取第一个元素这样就能找到了。这个相邻的子矩阵具体找法是:
对于小的那个值取其右边和下边构成的矩阵。这个矩阵中的值大于它。对于大的那个值取其左边和上边构成的矩阵,该矩阵中的值小于它。这样反复的找对角线,找矩形。就可以找到这个值了。
3.6 定位查找法
从右边开始比较元素,如果比目标元素大就往左查找比较,如果比目标元素小就往下然后继续往左找,这个方法相比3.3,好在不用向右查找,因为右边的上面一定大于要查找的值那么它的右边也一定大于要查找的值,这是由杨氏矩阵的特性决定的。
为了简化步骤,最好是从矩阵的右上角(即 第一行 第n-1列) 或 左下角(第m行第0列)开始查找,这样是为了最好地利用矩阵属性。以右上角开始查找为例,这里使用示例矩阵举例,待查找元素为10:
1、右上角元素为8,小于10;而8是本行最大数值,所以只能向下查找,8所在的第一行元素都被排除;
2、9依然小于10,所以继续向下,查到11>10,因此在本行向左查找,(11所在的这列元素都可以排除,因为上面的8、9前两轮已排除,而11以下的元素都大于11,所以自然也都大于10)
3、9<10,因为右侧元素已经都排除,所以只剩下了同列下一行(元素10)这唯一一个选择
4、10正好是要查找的元素,所以返回成功。由此也容易推断,最差的情况是继续在最后一行,向左遍历完剩余的两个元素。
那么这种方法的时间复杂度最差情况为O(m+n)
基于上述的分析和示例所示的推导过程,可以写出如下代码【java版本】:
public class YoungSearch {
public static int findNum(int[][] arr, int row, int col, int target){
int i=0;
int j=col-1;
while(i<row&& j>=0){
if(arr[i][j] < target){
++i;
}
else if(arr[i][j]>target){
--j;
}else{
return 1;
}
}
return 0;
}
public static void main(String[] args){
int a[][] = {
{ 1, 3, 5 }, { 3, 5, 7 }, { 5, 7, 9 }};
int result = findNum(a, 3,3, 3);
System.out.println(result);
}
}