括号匹配

来源:互联网 发布:软件技术职称 编辑:程序博客网 时间:2024/05/17 08:41

源自2017.3.17参加网易游戏实习面试时的面试题,题目为:给定N对括号”()”,有多少种合法的匹配方式?

简单递归

当时的思路:递归,设定两个初始变量leftL,leftR,分别表示,当前还剩下的左右括号数目,如果左右括号数均为0,那么表示匹配完毕;否则,当前选择左括号,leftL - 1,进入下一层递归,当前选择右括号,leftR - 1,进入下一层递归,两者的匹配数目相加即可!
边界情况:leftL或者leftR < 0,以及leftL > leftR时,此时匹配已经不合法,返回,相当于剪枝。
代码如下:

int totalCount = 0;void matchNum(int leftL, int leftR){    //剪枝(剩余的左括号数必须小于右括号数)    if (leftL > leftR || leftL < 0 || leftR < 0)    {        return;    }    if (leftL == 0 && leftR == 0)    {        totalCount++;        return;    }    matchNum(leftL - 1, leftR);    matchNum(leftL, leftR - 1);}

由于存在重复计算,算法复杂度为O(2^N),当N超过15时,在PC上(I7-4790)运行时间超过10S;

带备忘录的递归

上述的递归中,涉及大量的重复计算,如下图(以N=4为例):
这里写图片描述
可以通过建立一个备忘录,记录已经计算的结点,递归到某一层时,先查询备忘录是否已经计算,如果是,直接返回备忘录中的值,否则进行递归计算。这里,使用std::map存储结点,代码如下:

std::map<std::pair<int, int>, long long> umap;long long matchNum1(int leftL, int leftR){    //剪枝(剩余的左括号数必须小于右括号数)    if (leftL > leftR || leftL < 0 || leftR < 0)    {        return 0;    }    if (leftL == 0 && leftR == 0)    {        //totalCount++;        return 1;    }    long long result = 0;    if (umap.find(std::pair<int, int>(leftL - 1, leftR)) != umap.end())    {        result += umap[std::pair<int, int>(leftL - 1, leftR)];    }    else    {        long long  tmp = matchNum1(leftL - 1, leftR);        umap[std::pair<int, int>(leftL - 1, leftR)] = tmp;        result += tmp;    }    if (umap.find(std::pair<int, int>(leftL, leftR - 1)) != umap.end())    {        result += umap[std::pair<int, int>(leftL, leftR - 1)];    }    else    {        long long tmp = matchNum1(leftL, leftR - 1);        umap[std::pair<int, int>(leftL, leftR - 1)] = tmp;        result += tmp;    }    return result;}

这里每次查询时间为O(logN),计算的结点数为O(N^2),总的时间复杂度为O(LogN*N^2);如果想要在O(1)时间内完成查询,需要使用std::unordered_map,但如果使用std::unordered_map,必须要对此处的std::pair< int, int >创建自定义的hash函数和等价准则,对此可以参考博客,
代码如下:

#include <functional>template<typename T>inline void hash_combine(std::size_t& seed, const T& val){    seed ^= std::hash<T>()(val)+0x9e3779b9 + (seed << 6) + (seed >> 2);}template<typename T>inline void hash_val(std::size_t& seed, const T& val){    hash_combine(seed, val);}template<typename T, typename... Types>inline void hash_val(std::size_t& seed, const T& val, const Types&... args){    hash_combine(seed, val);    hash_val(seed, args...);}template<typename... Types>inline std::size_t hash_val(const Types& ...args){    std::size_t seed = 0;    hash_val(seed, args...);    return seed;}std::size_t hash(const std::pair<int, int>& c){    return  hash_val(c.first, c.second);};bool eq(const std::pair<int, int>& c1, const std::pair<int, int>& c2){    return c1.first == c2.first && c1.second == c2.second;}; std::unordered_map<std::pair<int, int>, long long, decltype(&hash), decltype(&eq)>     umap(100, hash, eq);long long matchNum1(int leftL, int leftR){    //剪枝(剩余的左括号数必须小于右括号数)    if (leftL > leftR || leftL < 0 || leftR < 0)    {        return 0;    }    if (leftL == 0 && leftR == 0)    {        //totalCount++;        return 1;    }    long long result = 0;    if (umap.find(std::pair<int, int>(leftL - 1, leftR)) != umap.end())    {        result += umap[std::pair<int, int>(leftL - 1, leftR)];    }    else    {        long long  tmp = matchNum1(leftL - 1, leftR);        umap[std::pair<int, int>(leftL - 1, leftR)] = tmp;        result += tmp;    }    if (umap.find(std::pair<int, int>(leftL, leftR - 1)) != umap.end())    {        result += umap[std::pair<int, int>(leftL, leftR - 1)];    }    else    {        long long tmp = matchNum1(leftL, leftR - 1);        umap[std::pair<int, int>(leftL, leftR - 1)] = tmp;        result += tmp;    }    return result;}

动态规划方式

令剩余的左括号数为L,剩余的右括号数为R,那么可得递推式:F(L,R) = F(L - 1,R) + F(L, R - 1)。
完整的递归式为:
(1)F(L,R) = 0,L < 0 or R < 0 or L > R
(2)F(L,R) = 1,L = 0 and R = 0
(3)F(L,R) = F(L - 1,R) + F(L, R - 1), others

当N = 4时,动规的表格如下(行为L,列为R):
这里写图片描述
可以使用(N+1)^2的空间用于记录计算过的值,但是这里使用(N+1)就够了,代码如下:

long long matchNum2(int num){    if (num == 0)    {        return 1;    }    std::vector<long long> dp(num + 1, 1);    for (int i = 1; i <= num; ++i)    {        for (int k = i + 1; k <= num; ++k)        {            dp[k] += dp[k - 1];        }    }    return dp[num];}

时间对比(Release模式)

N = 18时;
这里写图片描述
当N > 20时,普通递推已经无法短时间计算出来,故不参加以下对比:
N = 5998时,
这里写图片描述
(计算结果为负数,是因为溢出了!)
可以发现,动态规划计算速度比递归快了很多;
此外,当N大于某个值时,递归会因栈空间不足而崩溃,因此,使用动态规划的方式不仅在时间,而且在代码的强健性上也强于递归!

卡特兰数

在网上搜索关于括号匹配的信息时,看到这个问题可以用卡特兰数解决,关于卡特兰数,可以转至链接.

注:以下内容参考博客

上面的方法,是基于剩余的左右括号数量来得到递归式的,那么有没有另外的思考方式呢?

现在,如果我们仅仅考虑左括号,而不考虑右括号,通过下面的示意图可以看出,我们可以将整个问题,拆分成一个个子问题,然后将子问题合并就可以解决整个问题。
这里写图片描述
这里a[i]的含义是:第i个左括号左(右)边的括号的匹配数,a[i] * a[N - 1 - i]表示当前左括号在位置i+1时,可以得到的匹配数(不知道这样表述清不清楚,看图应该能更加明白)。
递推式为:
F(N) = F(i) * F(N-1-i),0 <= i <= N - 1;
当i = 0或者1时,F(i) = 1;
对应的代码如下:

long long matchNum3(int num){    if (num == 0 || num == 1)    {        return 1;    }    std::vector<long long> dp(num + 1, 1);    for (int i = 2; i <= num; ++i)    {        long long tmp = 0;        for (int k = 0; k < i; ++k)        {            tmp += dp[k] * dp[i - 1 - k];        }        dp[i] = tmp;    }    return dp[num];}

两种动态规划方法的时间对比:

这里写图片描述
N = 115998时,
计算结果由于溢出,可以忽略;前者是自己写的动态规划方法计算时间,后者是卡特兰数的动态规划方法的计算时间,可以发现前者约是后者时间的 1/2,在实验了其它的N值后,基本都是在1/2左右,为什么会这样暂时不去分析了。