LeetCode刷题攻略:常用数据结构(栈)

为了能够通过技术面试,“刷题”可以说是求职路上避不开的一道坎了。随着像LeetCode这样的刷题网站盛行,面试官也会尽量挑选一些不太热门的题目,或者领域内的题目,仅仅背题肯定是无法通过面试的。需要对大致会出现什么题目、有什么通用的解决方法有所了解,才能够对应题目快速想到最优解。

这里还是必须推荐两本学习算法与数据结构极好的书籍:《算法(第四版)》和《算法导论》。前者更强调“数据结构”的建立,实践性比较强,后者更强调数学上的精确性,分析性比较强。要学好数据结构和算法,笔者觉得这两本书都应该看看,即使是挑着合适自己的章节来看,也总能够有所收获。

这篇文章主要介绍刷题常用的数据结构,并就每一个数据结构给出一道经典例题:

栈是后进先出的数据结构,适用于新处理的数据比旧处理的数据更“重要”的场景。由于自身存储元素,可以在运行过程中“维持”或者“改变”数据的组织方式。需要在循环中用到这种特殊组织方式的算法往往可以使用栈来简化。栈的插入、查找、删除操作都是O(1)时间复杂度,但由于数据结构本身的限制,栈不能随机访问元素(仅能访问栈顶元素),同时只能从栈顶开始遍历,并必然会有一个拆除栈的过程。

经典例题:有效的括号

题目:

给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串,判断字符串是否有效。

有效字符串需满足:

左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。

示例 1:

输入: “()”
输出: true

示例 2:

输入: “()[]{}”
输出: true

示例 3:

输入: “(]”
输出: false

示例 4:

输入: “([)]”
输出: false

示例 5:

输入: “{[]}”
输出: true

原题链接

观察题目可以看到,括号合法的前提是:当出现一个右括号时,往回找最近还未匹配的左括号,该左括号必然能匹配该右括号。所有右括号匹配完后,不能剩下尚未匹配的左括号(相反所有左括号匹配完后,不能剩下尚未匹配的右括号)。根据这个思路可以写出两重循环判断的代码(复杂度O(n ^ 2)):

代码如下:

bool isValid(string s) 
{
    //对于空串返回true
    if (s.empty()) return true;
    //记录最近待匹配的左括号的索引
    int lastLeftBracket = 0;
    //从第二个字符开始遍历字符串
    for (int i = 1; i < static_cast<int> (s.size()); ++i)
    {
        //如果遇到了右括号
        if (s[i] == ')' || s[i] == ']' || s[i] == '}')
        {
            //判断该右括号是否与最近待匹配的左括号匹配
            //若否,直接返回
            if (!checkBracket(s[lastLeftBracket], s[i])) return false;
            //匹配成功,将该左括号修改为另一个字符
            //之所以要修改,是为了标识“匹配成功”
            //避免后续更新待匹配左括号时重复指示该左括号
            s[lastLeftBracket] = '#';
            //遍历该左括号前的所有字符
            for (int j = lastLeftBracket - 1; j >= 0; --j)
            {
                //如果遇到了左括号
                if (s[j] == '(' || s[j] == '[' || s[j] == '{') 
                {
                    //该左括号即为当前需要匹配的左括号
                    lastLeftBracket = j;
                    //结束循环
                    break;
                }
            }
        }
        else
        {
            //如果没有遇到右括号,证明遇到的是左括号
            //更新当前需要匹配的左括号索引
            lastLeftBracket = i;
        }
    }
    //如果括号之间能匹配,程序将运行到这一步
    //如果所有左括号都匹配,最终待匹配的左括号应为特殊字符
    //运行到此处时,所有右括号也必然匹配
    //否则在遇到多余右括号时
    //将会与已经更改为特殊字符的最左端左括号比较
    //该比较必然导致程序返回false
    return s[lastLeftBracket] == '#';
}

//判断两个括号是否匹配
bool checkBracket(char leftBracket, char rightBracket)
{
    //当且仅当两个括号为同一对左右括号时才匹配
    return (leftBracket == '(' && rightBracket == ')') || 
        (leftBracket == '[' && rightBracket == ']') || 
        (leftBracket == '{' && rightBracket == '}');
} 

此代码并不是特别直观,需要稍微解释一下:思想是遍历整个字符串,同时不断更新一个记录最近需要匹配的左括号的索引。当遇到新的右括号时,查找该括号是否与左括号匹配,不匹配返回false,匹配则更新该左括号索引。注意到同一个左括号可能被重复使用多次,故而在一次匹配完成后,需要将该左括号更改为一个特殊字符(比如“#”),避免因重复使用造成歧义。

举个例子:考虑字符串“((({}{}))”,首先初始化待匹配的左括号索引为0(对应“(”),然后从第二个元素开始依次遍历。遇到三个左括号,将待匹配的左括号索引修改为3(对应"{”),然后遇到了一个右括号。“{”与“}”相匹配,测试通过,随后将该“{”修改为"#",使字符串变为“(((#}{}))”,并往回遍历到最近的左括号(索引2,对应“(”)。继续往后遍历,遇到一个左括号,更新待匹配的左括号索引为5(对应“{”)。又遇到了一个右括号,“{”与“}”相匹配,再将该“{”修改为“#”,使字符串变为“(((#}#}))”,并往回遍历到最近的左括号(索引2,对应 “(”)。这时修改“#”的意义就体现出来了——如果不将“{”修改为“#”,在索引5往左遍历时就会遇到索引3的“{”,会误认为这是一个尚待匹配的左括号!最后的比较类似,字符串会变为“(###}#}))”,同时待匹配的左括号索引为0。由于该索引对应的“(”不为“#”,证明该左括号未能正确匹配,返回false作为判断结果。

提交运行一下,貌似成绩还不错:
LeetCode刷题攻略:常用数据结构(栈)_第1张图片
不过这个方法显然比较冗长了,不仅需要修改原字符串,还要内部循环寻找最近未匹配的左括号。之所以会发生这种情况,是因为我们没有清空已经匹配的括号,转而使用修改的方式标记它。如果能够在一对括号匹配后,直接将其删除该有多好?

这时候栈就能够派上用场了——将左括号依次进栈,遇到右括号时,取出栈顶的左括号,比较两者是否匹配,一旦匹配即将该左括号弹出(避免重复使用同一左括号的问题,确保栈顶的左括号就是当前需要匹配的左括号)。即使左括号或右括号过多时也可以解决:若左括号多于右括号,遍历结束后栈内必然剩下左括号;如果右括号多于左括号,必然有右括号不能被左括号匹配。如此更加灵活地将原字符串打散开来作为一个一个字符来考虑,就不再需要标记和内部循环查找最近需要匹配的左括号,将时间复杂度降低到仅剩外部循环的O(n)。

代码如下:

bool isValid(string s) 
{
    //空字符串被认为是有效字符串
    if (s.empty()) return true;
    //储存括号的stack
    stack<char> bracketStack;
    //遍历字符串的每一个字符
    for (int i = 0; i < static_cast<int> (s.size()); ++i) 
    {
        //如果当前字符是任意一种左括号
        if (s[i] == '(' || s[i] == '{' || s[i] == '[') 
        {
            //将该字符加入stack中
            bracketStack.push(s[i]);
        }
        else
        {
            //否则,当前字符是任意一种右括号
            //如果栈中没有元素,证明没有与之匹配的左括号,返回false
            if (bracketStack.empty()) return false;
            //如果栈顶元素与当前右括号不匹配,返回false
            if (!checkBracket(bracketStack.top(), s[i])) return false;
            //左右括号匹配,将该左括号弹出
            bracketStack.pop();
        }
    }
    //如果所有括号是匹配的
    //左括号必然在遇到右括号时被弹出
    //如果栈中还有元素,证明括号不匹配
    //否则匹配
    return bracketStack.empty();
}

//判断两个括号是否匹配
bool checkBracket(char leftBracket, char rightBracket)
{
    //当且仅当两个括号为同一对左右括号时才匹配
    return (leftBracket == '(' && rightBracket == ')') || 
        (leftBracket == '[' && rightBracket == ']') || 
        (leftBracket == '{' && rightBracket == '}');
} 

经过优化之后,执行的速度也有了显著提升!
LeetCode刷题攻略:常用数据结构(栈)_第2张图片

你可能感兴趣的:(LeetCode刷题攻略)