最近开始学习王争老师的《数据结构与算法之美》,通过总结再加上自己的思考的形式记录这门课程,文章主要作为学习历程的记录。
栈可以看作是一种“操作受限”的线性表,当某个数据只涉及在一端插入和删除数据,并满足后进先出,先进后出的特性,应首选“栈”这种数据结构。如下图:
用数组实现的栈,叫做顺序栈,用链表实现的栈,叫做链式栈。不管是顺序栈还是链式栈,存储数据只需要一个大小为n的数组。在入栈和出栈过程中,只需要一两个临时变量存储空间,因此空间复杂度为O(1)。但这不意味空间复杂度为O(n)。因为这n个空间是必须的,无法省掉。因此说空间复杂度的时间,除了原本的数据存储空间,算法运行还需要额外的存储空间。
支持动态扩容的顺序栈
动态过程如上,当数组空间不够时,我们就重新申请一块更大的内存,将原来的数组中的数据统统拷贝过去。
对于出栈操作来说,我们不会涉及内存的重新申请和数据搬移,所以出栈的时间复杂度仍是O(1).但当空间不够时,就需要重新申请内存和数据搬移,时间复杂度即为O(n)。因此最好情况时间复杂度是O(1),最坏情况时间复杂度是O(n)。平均情况下的时间复杂度可以用摊还分析法。如下图,因此可以得到入栈操作的均摊时间复杂度为O(1)。
栈在函数调用中的应用
操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构,用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量作为一个栈帧入栈。当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈。
int main() {
int a = 1;
int ret = 0;
int res = 0;
ret = add(3, 5);
res = a + ret;
printf("%d", res);
return 0;
}
int add(int x, int y) {
int sum = 0;
sum = x + y;
return sum;
}
函数调用栈情况如下:
栈在表达式求值中的应用
编译器采用两个栈来实现。其中一个保存操作数的栈,另一个是保存运算符的栈。从左到右遍历表达式,若遇到数字,则进行压栈。当遇到运算符,与运算符栈的栈顶元素进行比较。如果比运算符栈顶元素优先级高,就将当前运算符压入栈。反之,从操作数栈中取2个操作数,从运算符栈中取栈顶运算符。计算后,再把计算结果压入操作数栈,继续比较。以(3+5*8-6为例)
栈在括号匹配中的应用
除了用栈来实现表达式求值,我们还可以借助栈来检查表达式中的括号是否匹配。以LeetCode上第20题(有效的括号)为例:给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。
以下是自己敲的代码,思路是当字符串长度为0时,返回True,当字符串长度为单数时,返回False,当字符串长度为双数时,遇到’(’,’[’,’{‘时压栈,遇到‘)’,‘]’,‘}’则将’(’,’[’,’{'删除。
class Solution(object):
def isValid(self, s):
"""
:type s: str
:rtype: bool
"""
l = []
if len(s)==0:
return True
if len(s)%2!=0:
return False
for j in range(len(s)):
c = s[j]
if c=='(':
l.append(c)
elif c=='[':
l.append(c)
elif c=='{':
l.append(c)
elif c==')' and ('(' in l):
if l[-1]=='(':
del l[-1]
else:
return False
elif c==']' and ('[' in l):
if l[-1]=='[':
del l[-1]
else:
return False
elif c=='}' and ('{' in l):
if l[-1]=='{':
del l[-1]
else:
return False
else:
return False
if j==len(s)-1:
if len(l)==0:
return True
else:
return False
参考资料:王争《数据结构与算法之美》
LeetCode题库