题目1384:二维数组中的查找
-
题目描述:
-
在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
-
输入:
-
输入可能包含多个测试样例,对于每个测试案例,
输入的第一行为两个整数m和n(1<=m,n<=1000):代表将要输入的矩阵的行数和列数。
输入的第二行包括一个整数t(1<=t<=1000000):代表要查找的数字。
-
接下来的m行,每行有n个数,代表题目所给出的m行n列的矩阵(矩阵如题目描述所示,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。
-
输出:
-
对应每个测试案例,
输出”Yes”代表在二维数组中找到了数字t。
输出”No”代表在二维数组中没有找到数字t。
-
3 3 5 1 2 3 4 5 6 7 8 9 3 3 1 2 3 4 5 6 7 8 9 10 3 3 12 2 3 4 5 6 7 8 9 10
-
Yes No No
-
样例输入:
-
样例输出:
这道题简单概述,就是怎样以最快的速度,查找二维数组中是否含有某个数。 在此,拙见,提供给大家三种解题思路,供大家参考,也欢迎大家指正。 当然,如果有更好的算法,也希望不吝赐教。 大家共同进步!
先描述三种算法的思路。然后对其中两种,附上代码。呵呵呵,因为另一种,我没有具体的写。 最后对于新学到的知识StreamTokenizer,给出链接供大家参考。
在正式开始前,先说下这道题的一个漏洞。 这道题,虽然是查找二维数组中是否含有某个数,但是却是要求要自己输入数组的。 导致,有人发现,可以在边输入的过程中,边判断是否等于查询的数。 这种方法被多人应用。 在此不加以评判。
图中数据,是为了说明问题随意而写,不具有代表性。 只为了说明问题。 举例中,均以查找数据80为目的。 按题意我的m=7, n=9, t=80. 结果输出应该为No.
方法一,称之为: 多次二分查找 直接上图。
红色表示所查数据可能出现的地方。
步骤描述: 1.按题意,矩阵数组中,每行数据均从小到大,每列数据也是从小到大。所以对于某些行,如果判定右侧的值小于目标值t,则整行值均比t小。但是右侧的值大于t,并不能说明这行不会含有数据t。 所以这一步的目的是缩减所要查询的行数,我们通过比对每行最右侧的值,找到一个最大的小于t的值,计此行的行标为row,则有行标小于等于row的所有行的值全部小于t.(第一次二分查找) 2.对于符合要求的行,进行逐行查找(多次二分查找),实现题目的要求。
没有对此方法实现,有兴趣的同学可以自己实现。
! 这只是个人的初步想法。 朋友们可以去实践。
此方法的优缺点在于: 优点:(1)比较容易理解,(2)对于查找矩阵上层的数据,查询时间小。(3)参数传递简单。 缺点: (1)如果查找的值位于左下侧,则查询时间较长。(2)查询时间不稳定。
方法二, 渐进查找
思想来自《剑指offer》,方法思想差不多同方法一,但是没有用到二分查找。 只是逐次改变行和列的下标,每次都缩减要查找的数据范围,使其更加逼近所要查找的数。 仍从右上角开始,如果此数较所查找数小,则行+1,如果比所查找数大,则列-1.
思想是一步步缩小查找范围,但是没有涉及到二分查找。
不画图了,见谅。 上代码。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
public class Main {
private boolean flag = false;
private int[][] matric;
private void inputOutput() throws IOException{
StreamTokenizer st = new StreamTokenizer(new BufferedReader(
new InputStreamReader(System.in)));
while(st.nextToken() != StreamTokenizer.TT_EOF){
flag = false;
int row = (int)st.nval;
st.nextToken();
int column = (int)st.nval;
st.nextToken();
int cand = (int)st.nval;
if(!(0<row && row<=1000 && 0<column && column<=1000 && 0<cand && cand<=1000000)){
continue;
}
matric = new int[row][column];
for(int i=0; i<row; i++)
for(int j=0; j<column; j++){
st.nextToken();
matric[i][j] = (int)st.nval;
}
findNumber(row, column, cand);
if(flag == false)
System.out.println("No");
else
System.out.println("Yes");
}
}
private void findNumber(int row, int column, int cand) {
if(row<1 || column<1)
return;
int i = 0;
int j = column-1;
while(i<row && j>=0){
if(matric[i][j] == cand){
flag = true;
return;
}
else if(matric[i][j] >cand){
j--;
}
else
i++;
}
return;
}
public static void main(String[] args) throws IOException {
Main main = new Main();
main.inputOutput();
}
}
方法三,称之为 分块查找
算法的主题思想是用一个矩阵的对角线去确定所查数据的可能范围。 1.对于一个不是方正的矩阵,先找到它的最大方正,其余部分则使用迭代用相同的方法处理。 图中,则用细蓝线划分,左侧为方针,右侧为剩余部分。剩余部分中包含多个方正。 2.对于方正而言,先判断t值是不是小于方正的最小值(最左上角的值,图中为1),或者大于方正的最大值(最右下角的值,图中为95)。如果满足其一,则此方正中不可能存在我们所要找的值。 这样做的目的是为了减少一些多余的运算。 3.对于可能存在要查找值的方正,则沿对角线查找,如果找到则返回,无的话则找到界值。(绿线) 例子中,我们要找80,则有33<80<81。 然后根据此两界值,则重新去迭代判断红色框中的数据(可能存在80的区域)。 4. 以上过程多次迭代。则最终会得到结果。
此方法的优缺点在于: 优点:对于大数据量,不论所要查找的数据大小,都可以快速定位至较小的范围。 缺点:(1)多次迭代,(2)参数传递麻烦
以下为第二种方法的代码,已通过。AC
import java.io.*;
public class Main {
private boolean flag = false;
private int[][] matric;
void inputOutput() throws IOException{
StreamTokenizer st = new StreamTokenizer(new BufferedReader(
new InputStreamReader(System.in)));
while(st.nextToken() != StreamTokenizer.TT_EOF){
flag = false;
int row = (int)st.nval;
st.nextToken();
int column = (int)st.nval;
st.nextToken();
int cand = (int)st.nval;
if(!(0<row && row<=1000 && 0<column && column<=1000 && 0<cand && cand<=1000000)){
continue;
}
matric = new int[row][column];
for(int i=0; i<row; i++)
for(int j=0; j<column; j++){
st.nextToken();
matric[i][j] = (int)st.nval;
}
findNumber(0, row-1, 0, column-1 ,cand);
if(flag == false)
System.out.println("No");
else
System.out.println("Yes");
}
}
private void findNumber(int srow, int erow, int scol, int ecol, int cand) {
if(matric[srow][scol]>cand || matric[erow][ecol]<cand)
return;
int mrow = srow;
int ncol = scol;
boolean flagb = (erow-srow > ecol-scol) ? true : false;
int temp = (flagb==true)?(ecol-scol):(erow-srow);
int mhigh = srow + temp;
int nhigh = scol + temp;
// Do the excess part
if(flagb==true){
if(mhigh+1<=erow){
findNumber(mhigh+1,erow,scol,ecol,cand);
}
}
else{
if(nhigh+1<=ecol){
findNumber(srow,erow,nhigh+1,ecol,cand);
}
}
// Do the rectangle part
for(; mrow<=mhigh && ncol<=nhigh; mrow++, ncol++){
if(matric[mrow][ncol]==cand){
flag = true;
return;
}
if(mrow<mhigh && ncol<nhigh){
if(matric[mrow][ncol]<cand && matric[mrow+1][ncol+1]>cand)
break;
}
}
if(mrow<mhigh && ncol<nhigh){
findNumber(srow, mrow, ncol+1, nhigh, cand);
findNumber(mrow+1, mhigh, scol, ncol, cand);
}
return;
}
public static void main(String[] args) throws IOException {
Main con = new Main();
con.inputOutput();
}
}
部分参数说明:srow, erow, mhigh分别为行起始点,行结束点,行当前分割位置;cand为t。
这题代码不知道写了多少个版本,我存了的至少有四个。 遇到的问题有: (1)参数传递。刚开始写的时候总是把小的方正重新复制,然后把方正作为参数传递。后面觉得,直接复制传递方正会用掉很多的时间。所以便全部改写成了坐标传递。具体的时间影响未经考证。如果有谁考证过,悉心听教。 (2)写了很多个版本的输入都用的是,Scanner,但是都没有通过。前文已说,在wzqwsrf的提醒下改用了StreamTokenizer。便得到了AC。最终得证,StreamTokenizer的速度比Scanner要快很多。估计一倍至少是有的。但是好像apache在新版本的java中却不推荐使用Tokenizer。是这样吗???
本来准备贴个原先版本的代码,但是因为事隔太久,竟有点搞不通参数传递的问题。便作罢。 不过原先的代码,写的很乱,也没有什么可借鉴之处。
对于方法二和三的时间效率,实际运行结果二为1820ms,三为1840ms.空间也均在31600kb左右。 结果原因未曾考究。 但方法三种的多次迭代可能会造成时间上的消耗。
学习StreamTokenizer的两个链接: http://sulifeng.iteye.com/blog/577000
http://www.wangchao.net.cn/bbsdetail_14840.html