目录
问题描述
编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:
实验任务
采用二分搜索策略实现问题的求解程序,验证输入输出结果,并对以下三种设计算法的时间复杂度进行对比分析:
方法一:蛮力法,对于每一行可以像搜索未排序的一维数组——通过检查每个元素来判断是否有目标值。
方法二:二分法搜索策略,矩阵已经排过序,可以采用二分查找加快搜索效率(通过行列切片)。
方法三:搜索空间缩减策略,可以将已排序的二维矩阵划分为四个子矩阵,其中两个可能包含目标,其中两个肯定不包含,进一步提升问题的求解效率。
方法四:采用类搜索树的方式进行求解。
解决过程及结果
输入输出约定
输入:
第一行输入两个正整数m,n表示metrix矩阵行数和列数; 接下来的m行每行输入n个整数,即输入metrix矩阵; 最后一行输入一个整数表示目标值。
输出:
目标值存在,输出true; 目标值不存在,输出false。
蛮力法求解
算法思路比较easy,在此不表。
程序代码编写:
#include <iostream>
using namespace std;
int matrix[1000][1000];
int m, n, target;
bool find_num1( ); //函数:寻找矩阵中是否存在target数据
int main()
{
bool in;
cin>>m>>n;
for(int i=0; i<m; i++)
for(int j=0; j<n; j++)
cin>>matrix[i][j];
cin>>target; //以上为数据读入模块
in = find_num1( );
if(in == true) cout<<"true"<<endl;
else cout<<"false"<<endl;
return 0;
}
bool find_num1( )
{
for(int i=0; i<n; i++)
for(int j=0; j<n; j++)
if(matrix[i][j]==target) return true;
return false;
}
运行结果:
时间复杂度分析:
查找算法由代码段中的find_num1函数实现,以下分析都是对这一函数的分析。
a).现在我们先分析查找成功的情况:假设我们想要查找的数字存在于给定m*n矩阵的第i行第j列。那么在蛮力算法中,我们需要对第0~i-1行进行完整的查找,对每一行的查找的时间复杂度为O(n),总体来说,有: 。
因此,蛮力法解决该问题中查找成功的平均时间复杂度为:
当m与n均趋近于无穷时, ~(1)。
b).现在我们分析查找失败的情况,该情况下,我们需要遍历矩阵的每一个元素,因此时间复杂度为(2)。
c).综合以上分析,蛮力法求解该问题的时间复杂度的数量级为。
分治法求解
求解思路:
由于矩阵有--每行的元素从左到右升序排列,每列的元素从上到下升序排列--的特征。我们可以考虑使用分治法进行解题。
我们可以首先将矩阵进行行切割,对每一行进行二分查找。若找到就结束查找并返回提示信息,找不到就对下一行进行查找。找完所有行都没有找到则结束,返回提示信息。
同时由于列有序的特点分析可知,假设我们现在对第i-1行完成了查找,发现目标值在第t-1个数和第t个数之间,即a[i-1][t-1]<target<a[i][t],由于任取k>i-1, j>=t,有a[k][j]>a[i][t],因此也有target>a[i][t],因此接下来的第i行~第m行的第t列及以后的数字都不可能为target,也就没有必要进行查找了。因此第i行开始,二分的范围应该是a[i][0]~a[i][t-1]。
程序代码编写:
//主函数与蛮力法一致,仅仅将find_num1的调用换成find_num2即可
bool find_num2( )
{
int i, t=n-1, s, middle;
while(i<m&&t>=0)
{
s = 0;
while(s <= t)
{
middle = (s+t)/2;
if(matrix[i][middle] == target) return true;
if(matrix[i][middle] > target) t = middle-1;
else s = middle+1;
if(matrix[i][middle] > target) t = midddle-1;
else t = middle;
}
i++;
}
return false;
}
时间复杂度分析:
查找算法由4.1.2.1中的find_num2实现,以下分析都是对这一函数的分析。
a).现在我们先分析查找成功的情况:假设我们想要查找的数字存在于给定m*n矩阵的第i行第j列。那么在二分搜索算法中,我们需要对第0~i-1行进行完整的查找,对每一行的查找的时间复杂度为与二分查找一致,为O(log n),而在第i行的查找中,最坏情况的查询次数为log n。总体来说,有: 。因此,蛮力法解决该问题中查找成功的平均时间复杂度为:
因此,有~。
b).现在我们分析查找失败的情况,该情况下,我们需要遍历矩阵的每一个元素,因此时间复杂度为。
c).综合以上分析,二分法求解该问题的时间复杂度数量级为:。
空间缩减策略求解
求解思路:
在上述--行列切片,二分求解--思路的基础上,更加充分的利用该矩阵行列有序的特点可以得到空间缩减策略。首先对当前求解矩阵的中间行进行二分查找,假设查找到target则将flag置为1,同时返回,否则我们可以得到target在当前行的第i到i+1个数之间。利用i和当前行信息,我们可以将矩阵划分为右上和左下两个子矩阵进行查找。(原因:a[now][i]<target<a[now][i+1],则必有a[now][i]所在行列划分得到的左上矩阵所有元素都小于target,且a[now][i+1]所在行列划分得到的右下矩阵所有元素都大于target)。
样例求解过程分析:
下面将以在以下矩阵中查找5为例进行样例执行分析来辅助理解:
1 | 4 | 7 | 11 |
2 | 5 | 8 | 12 |
3 | 6 | 9 | 16 |
10 | 13 | 14 | 19 |
18 | 21 | 23 | 26 |
首先对中间行(0+4)/2=2行进行查找,发现5在3和6之间,因此将搜索矩阵转化为以下由X分割的两个矩阵(记为右上1(下表左)和左下1(下表右))的搜索。
4 | 7 | 11 | X | 10 |
5 | 8 | 12 | X | 18 |
首先搜索右上矩阵,该矩阵中间行为第0行,在该行中5在4和7之间,于是,搜索矩阵又转换为下图所示:
5 |
该列表只有一行,查找到target,因此flag置为1,查找完成且已找到,逐层返回。
代码编写:
#include <iostream>
using namespace std;
int matrix[1000][1000];
int m, n, target;
void find_num3(int s, int t, int l, int r, int &flag);
int main()
{
int flag;
cin>>m>>n;
for(int i=0; i<m; i++)
for(int j=0; j<n; j++)
cin>>matrix[i][j];
cin>>target; //读入数据
find_num3(0, m-1, 0, n-1, flag);
if(flag == 1) cout<<"true"<<endl;
else cout<<"false"<<endl;
return 0;
}
//s:当前搜索矩阵的第一行
//t:当前搜索矩阵的最后一行
//l:当前搜索矩阵的最左侧位置
//r:当前搜索矩阵的最右侧位置
//flag:判断是否已经找到的标志
void find_num3(int s, int t , int l, int r, int &flag)
{
if(flag==1) return;
if(s>t||l>r) return; //
int f = l, b = r, now = (s+t)/2, middle; //对当前矩阵的中间行进行查找
while(f<=b) //二分查找
{
middle = (f+b)/2;
if(matrix[now][middle] == target)
{
flag=1;
return;
}
if(matrix[now][middle] > target) b=middle-1;
else f=middle+1;
}
//当前行没找到进行划分,查找左下和右上矩阵
if(matrix[now][middle] > target){
find_num3(0, now-1, middle, r, flag);
find_num3(now+1, t, l, middle-1, flag);
}
else{
find_num3(0, now, middle+1, r, flag);
find_num3(now+1, t, l, middle, flag);
}
}
时间复杂度分析:
查找算法由4.1.3.1中的find_num3实现,以下分析都是对这一函数的分析。对于本实验所采用的空间缩减策略代码来讲,平均时间复杂度的分析较为困难,因此,我们直接分析最坏情况,以此为依据分析时间复杂度。对于这一递归算法来说,每次进行一个不会直接退出递归的执行过程,都会将问题的复杂度在行的维度上缩小至原来的一半(将所剩的左下部分与右上部分拼接分析易得此结论)。因此,有以下递推式:
由此,我们可以得出结论,该问题的空间缩减策略的时间复杂度的数量级为:。
类搜索树法求解
求解思路:
我们可以将该有序矩阵看作一棵以左下角位置数字为根的二叉搜索树进行求解。 例如下面表格所示的矩阵可转化为其右侧所示的搜索路径
代码编写:
#include <iostream>
using namespace std;
int matrix[1000][1000];
int m, n, target;
bool find_num4();
int main()
{
bool flag;
cin>>m>>n;
for(int i=0; i<m; i++)
for(int j=0; j<n; j++)
cin>>matrix[i][j];
cin>>target;
flag=find_num4();
if(flag==1) cout<<"true"<<endl;
else cout<<"false"<<endl;
return 0;
}
bool find_num4()
{
int s=0, i;
for(i=m-1; i>=0; i--)
{
while(s<n&&matrix[i][s]<target) s++;
if(s>=n||matrix[i][s]==target) break;
}
if(i<0||s>=n) return false;
else return true;
}
时间复杂度分析:
该算法的最长搜索路径长度为m+n,因此时间复杂度为。