一、概述
给出2-9共八个数字与字母的映射,然后输入一个数字序列,输出所有可能的字母序列。
本题的背景为九宫格输入法,第一眼看到这题有点蛋疼。因为暴力穷举实在是太好想了,但是时间复杂度过于夸张——就算按小了算,每个数字对应三个字母,那一个有10个数字的序列则会有个结果,时间复杂度为,这太大了。
然后开始想有没有什么好方法。
比如说先把两个数字的情况列出来,遇到相同的数字对可以简化操作等。但都没有在本质上减少时间。为什么?因为你n个数字,最后的结果一定是个,那为了得到这些结果,循环一定要这么多次。无论你什么算法,这都是不可避免的。因此找不到更好的方法。
但是我在看到题目的时候没想这么多,就想找到一些好的方法,无果。不得不去看discuss。
主要思路有两个,其一就是暴力求解,其二则是DFS,另外还有回溯剪枝的方法。
偷偷说一句,我当时是想到了DFS的方法的,但是由于太久没刷题,做DFS还在建树,建树的过程把我卡住了。真丢人。
其实DFS不用建树的,迭代就好了。
二、分析
1、暴力求解
我们先来看输入,string digits,是一个字符串。整体思路就是遍历整个字符串,每次拿出一个数字。初始化一个字符串vector作为结果result。拿出的数字会对应另一个字符串Seq。我们要做的工作就是对于结果result中的每个字符串,在它后面加上Seq中的一个元素组成一个新的字符串,重新存在result中。
举例来说,对于digits为234的情况:
首先我们拿出2,2对应的是abc。然后初始化result。此时result为空。然后在abc中取出a,将a放在result中的每个元素的后面。嗯,result中没有元素,所以将a自己存在result中。然后拿出b,同理把b存进去,然后是c。这样数字2对应的字符串就处理完了。
接下来拿出3,3对应的是def。首先取出d,将d放在result中的每个元素后面。首先放在a后面,是ad;然后放在b后面,bd;接下来cd。这样d处理完了。接下来是e。仍然取出a,得到ae;然后是be,ce。同理得到af,bf,cf。
注意一点,不要得到ad、bd、cd就存回result,因为a、b、c接下来还有用呢,要把Seq这个序列都处理完了再都放回result里面。因此在处理Seq的时候,每次先新建一个结果tmp,然后将tmp赋值给result比较好。
代码如下:
class Solution {
public:
string PhoneNum[10]={{""},{""},{"abc"},{"def"},{"ghi"},{"jkl"},{"mno"},{"pqrs"},{"tuv"},{"wxyz"}};
//注意这里,string数组初始化必须用PhoneNum[10]而不能用PhoneNum[],否则报错“无法从类内初始值设定项推导数组界限”
vector<string> letterCombinations(string digits) {
vector<string> result;
if (digits.length()==0)//C++中,string的length和size没有区别,都是返回字节数
return result;
result.push_back("");//这一句必须要有,没有这一句,只声明的话,result的size是0;有这一句就是1
for(int i=0;i<digits.length();i++)
{
int NowNum=digits[i]-'0';
vector<string> tmp;
if(NowNum<2)
continue;
string& Seq=PhoneNum[NowNum];
for(int j=0;j<Seq.length();j++)
for(int k=0;k<result.size();k++)
{
tmp.push_back(result[k]+Seq[j]);
}
result=tmp;
}
return result;
}
};
但是结果有点差强人意。
修改几处可以得到很好的结果:
修改的地方是这里:
所有的i++都变成++i;这样会让时间加速极多。
将赋值操作result=tmp;变为调用函数result.swap(tmp);可以减少空间的使用。原因是赋值操作是把tmp复制然后赋值给result;而swap则是指针的变化。
但我觉得这个时间消耗0ms仍然有点怪怪的。
2、DFS求解
DFS不用建树!!!记住。
这个DFS的方法还是很简单的。法1的总体框架是一层一层的叠上去,最后统一输出所有元素;而法2则是先输出第一个元素,再输出第二个...在这一点上有区别。
对于普通的二叉树DFS我们都很熟悉:
先是确定递归出口,然后循环调用DFS。对于本题也一样。
先确定几个要点:对于输出的字符串数组中的每个字符串元素,其大小与输入的数字字符串大小相同,比如说你输入的是23,那无论输出的是ad还是cf,长度都是2。明确了这一点我们就可以确定递归出口了。
本题中的DFS要维护以下几个变量:tmp、Dig、nowDep、maxDep。其中,tmp是某个输出元素,它在DFS到某个叶节点时就变成输出元素的完全体;Dig就是输入的数字字符串;nowDep是现在tmp处理到第几个字母;maxDep是tmp最大要有几个字母。
在循环调用DFS时,循环结构是Dig中对应位对应的字母的字符串,就像2对应abc,那就对abc分别DFS。循环体就是调用函数。
同样以输入23为例。
首先判断递归出口,nowDep=0,maxDep=2;不退出,然后开始循环:Dig的第nowDep个元素是2,由于2对应的是abc,因此拿出a,把a放入tmp,然后调用DFS。
首先判断递归出口,nowDep=1,maxDep=2;不退出,然后开始循环:Dig的第nowDep个元素是3,由于3对应的是def,因此拿出d,把d放入tmp,然后调用DFS。
首先判断递归出口,nowDep=2,maxDep=2;因此将tmp压入result,退出。从而得到第一个元素ad。
代码如下:
class Solution {
public:
string PhoneNum[10]={{""},{""},{"abc"},{"def"},{"ghi"},{"jkl"},{"mno"},{"pqrs"},{"tuv"},{"wxyz"}}; vector<string> result; //注意这里的result是全局变量
vector<string> letterCombinations(string digits)
{
if (digits.length() == 0)
return result;
int maxDep = digits.size();
string tmp(maxDep, 0);
DFS(tmp, digits, 0, maxDep);
return result;
}
void DFS(string &tmp, string Dig, int nowDep, int maxDep)
{
if (nowDep == maxDep)
result.push_back(tmp);
else
{
for (int i = 0; i<PhoneNum[Dig[nowDep]-'0'].size(); i++)//注意这里要-'0'
{
tmp[nowDep] = PhoneNum[Dig[nowDep]-'0'][i];//注意这里要-'0'
DFS(tmp, Dig, nowDep + 1, maxDep);
}
}
}
};
时间复杂度不错,空间复杂度有点大。
有两个地方要注意:
其一是string的静态初始化,可以用string tmp(a,b)来初始化,这样的效果是string中有a个b元素;
其二是char转换成int一定注意减去'0',这个debug有点难看出来。
三、总结
算是水题,只要明确了时间复杂度没法降下来就好做了。主要是DFS的应用。
几个月没刷题现在手很生,所以做的有点慢。