假如现在有这么一个问题:
一个序列从1到n依次入栈, 那么可能的出栈序列一共有多少种?
注意: 在任意一个时刻,只要栈不为空, 就可能有元素出栈, 不是说元素全部入栈之后再出栈
这个问题的解其实等同于求n阶的卡特兰数(catalan)
先给出问题的解
设n阶的卡特兰数为k(n), 那么
k(0) = 1, k(1) = 1
k(n) = k(0) * k(n - 1) + k(1) * k(n - 2) + ... + k(n - 2) * k(1) + k(n - 1) * k(0)
或者
k(n) = c(2n, n) / (n + 1)
或者
k(n) = c(2n, n) - c(2n, n-1)
下面是问题的具体分析过程
首先说明为什么问题的解是卡特兰数
卡特兰数指的是在一个n*n的方格中,从左下角走到右上角. 每一步只能往右或者往上, 且在走的过程中不能越过从左下角到右上角的那条对角线.
和入栈出栈问题对比可以发现, 这里的往右走就相当于入栈, 往上走就相当于出栈, 对角线上的点就相当于栈为空的时候, 不能越过对角线就是说在栈为空的时候不能执行弹栈操作
那么卡特兰数如何求得呢, 思路有很多,这里说几种比较容易理解的
1. 用排列组合的方法来求
我们借助栈的思想来理解
n个元素入栈再出栈, 那么就有 2n 次操作, 正常情况下是n次入栈操作, n次出栈操作, 根据排列组合的方法, 共有 c(2n, n) 种可能性
当然了,在这 c(2n, n) 种可能性中, 肯定是有非法操作的, 也就是在在为空或者为负的时候进行出栈操作, 那么非法的情况一共有多少种呢?
我们假设在第一次出现非法操作的时候, 这个时候栈中元素个数为 -1 , 因为在没有元素的时候进行了出栈操作. 由于最终栈中元素数量为 0 , 那么在这一步之后的操作中,入栈次数肯定比出栈次数多 1 . 如果我们对这次操作之后的所有操作进行反转, 那么最终栈中元素的数量就是 -1 减去 1 = -2 了. 这就是非法的情况.
在非法的情况下, 由于最终栈中元素数为-2, 所以在2n次操作中, 肯定有 n - 1 次入栈操作, n + 1 次出栈操作. 这时入栈次数和出栈次数不同是因为我们对部分操作进行了反转, 这一点要明白.
到这里, 答案就很明显了, 非法操作的次数为 c(2n, n -1) = c(2n, n + 1)
所以合法的次数等于所有可能的次数减去非法的次数, 即:
k(n) = c(2n, n) - c(2n, n-1)
2. 折线法
这种方法其实与第一种方法有异曲同工之妙, 只是运用了图形来帮助理解
我们假设一个人在原点,操作1是此人沿右上角45°走一个单位(一个单位设为根号2,这样他第一次进行操作1就刚好走到(1,1)点),操作2是此人沿右下角45°走一个单位。第k次操作2前必须先进行至少k次操作1,就是说明所走出来的折线不能跨越x轴走到y=-1这条线上!在进行n次操作1和n此操作2后,此人必将到到达(2n,0)!若无跨越x轴的限制,折线的种数将为 c(2n,n),即在 2n 次操作中选出n次作为操作1的方法数。
现在只要减去跨越了x轴的情况数。对于任意跨越x轴的情况,必有将与 y = -1相交。找出第一个与 y = -1 相交的点 k,将k点以右的折线根据 y = -1 对称(即操作1与操作2互换了)。可以发现终点最终都会从(2n,0)对称到(2n,-2)。由于对称总是能进行的,且是可逆的。我们可以得出所有跨越了x轴的折线总数是与从(0,0)到(2n,-2)的折线总数。而后者的操作2比操作1要多 0-(-2)=2次。即操作 1 为 n-1 , 操作 2 为 n+1。总数为 c(2n,n-1)。
3. 递归的方法
若用(n,m)表示有n个元素还没有入栈,栈内目前有m个元素时,出栈可能的种数。则问题就是(n,0)。
当目前状态为(n, 0)时只能进行入栈操作,则有(n, 0) = (n - 1, 1)。当m>=1时,可以入栈也可以出栈则有(n, m) = (n - 1, m + 1) + (n, m - 1)。显然(0, m) = 1。这样就有了终止条件。
以上思路可以用递归程序实现
function catalan(n)
{
return condition(n, 0)
}
/*
* 递归函数, 用来求特定的某个时刻对应的可能性种数
*
* @param total - 还没有入栈的元素的个数
*
* @param inStack - 当前栈中元素个数
*
*/
function condition(outStack, inStack)
{
if(outStack === 0)
{
return 1
}
if(inStack === 0)
{
return condition(outStack - 1, 1)
}
return condition(outStack - 1, inStack + 1) + condition(outStack, inStack - 1)
// 注意, 这里第二项inStack减少1然而outStack没有加1是因为这个出栈的元素已经不必再考察了, 我们
// 只需要关注后面的还没有入栈元素即可
}
// 测试
for(let i = 1; i <= 10; i ++)
{
console.log(`${i}阶卡特兰数为: ${catalan(i)}`)
}
// 1阶卡特兰数为: 1
// 2阶卡特兰数为: 2
// 3阶卡特兰数为: 5
// 4阶卡特兰数为: 14
// 5阶卡特兰数为: 42
// 6阶卡特兰数为: 132
// 7阶卡特兰数为: 429
// 8阶卡特兰数为: 1430
// 9阶卡特兰数为: 4862
// 10阶卡特兰数为: 16796