为什么学习数据结构
每个编程语言都有自己擅长的领域和使用范围,但每个语言真正相通的点在于数据结构和算法。
数据结构和算法是脱离编程语言而存在的,不同的语言有不同的版本,但内在的逻辑却不会变化,编程思想不会变化。
什么是栈
栈是一种线性表,仅能够在栈顶操作,有着后进先出(Last In First Out)的特性。
对于栈,必须牢记一点:
- 栈的操作,只能在栈顶操作
栈示意图
生活中,有一个贴切的例子,羽毛球筒就是很形象的栈结构。每次去羽毛球,你只能从顶部取,最底下的羽毛球是娶不到的,用完羽毛球后,只能从顶部放回去。
还有一个栈的典型例子就是,我们在编辑文档时候,经常使用的control + z 撤回的操作,这就可以用栈来实现,把每一步操作都放到栈中,当你想回退的时候,就使用pop方法,把栈顶元素弹出,于是就得到了上一步的操作。
数据存储
从数据的存储角度来看,实现栈有两种方式,
- 以数组作为基础
- 以链表为基础
数组是最简单的实现方式,数组也是大家平时使用最频繁的,最熟悉的数据类型。
先定义一个简单的栈。
function Stack() {
let items = [] // 使用数组来存储数据
}
栈方法:
- push 添加元素到栈顶(放一个羽毛球)
- pop 弹出栈顶元素(从羽毛球筒拿出一个羽毛球)
- top 返回栈顶元素,不是弹出,这点和pop有点区别,(看一眼羽毛球筒中最顶端的羽毛球,但是不拿)
- isEmpty 栈是否为空(羽毛球是否用完了)
- size 栈里元素的个数(一共有多少羽毛球)
- clear 清空栈(把羽毛球筒里的球都倒出来)
下面来实现一个栈
function Stack() {
let items = [] // 使用数组来存储数据
//从栈顶添加元素,也叫压栈
this.push = function(elements) {
items.push(elements)
}
//弹出栈顶元素,
this.pop = function() {
return items.pop()
}
// 返回栈顶元素,
this.top = function() {
return items[items.length - 1]
}
// 判断栈是否为空
this.isEmpty = function() {
return items.length === 0
}
// 返回栈的个数
this.size = function() {
return items.length
}
// 清空栈
this.clear = function() {
items = []
}
}
上面的代码就实现了一个很简单的栈。
栈的应用
1.判断合法括号(成对出现)
- (1)(2)(3(4)) 合法,成对出现
- (1)(2)(3(4) 不合法,没有成对出现,少了一个 ')'
从栈的角度去考虑这个问题,就会很简单。
- 我们遍历每个字符,
- 遇到左括号,就压入栈中
- 遇到右括号,
- 判断栈是否为空,为空说明没有左括号对应,就是不合法,
- 不为空就把栈顶元素弹出,就抵消了一对括号。
- 当遍历结束后,如果栈是空的,就说明所有的左右括号都抵消了,如果栈里还有元素,就说明缺少右括号,不合法。
function isLegalBrackets(str) {
let stack = new Stack()
for(let i = 0; i < str.length; i++) {
let item = str[i]
if(item === '(') {
// 遇到左括号,压入栈中,
stack.push(item)
} else if(item === ')') {
// 判断栈是否为空,如果为空,说明没有左括号与之抵消,不合法
if(stack.isEmpty()) {
return false
} else {
// 栈顶元素弹出
stack.pop()
}
}
}
return stack.isEmpty
}
2.计算后缀表达式
什么是后缀表达式?
['1', '2', '3', '+', '*']
1 + 1 中缀表达式
1 1 + 后缀表达式
数字在前,运算符在后。这样的就是后缀表达式。
现在要计算表达式 ['4', '13', '5', '/', '+']的运算结果
这个问题站在栈的角度上去考虑,就很好解决,
- 遍历数组
- 如果元素是数字,就压入栈中,
- 如果元素是运算符中的其中一个,择从栈中连续弹出两个元素,并对这两个元素计算,将计算结果压入栈中,
- 遍历结束后,栈中只有一个元素,这个元素就是表达式的结果。
// 计算后缀表达式
function calcExp(exp) {
let stack = new Stack()
for(let i = 0; i < exp.length; i++) {
let item = exp[i]
if(['+','*','-','/'].indexOf(item) >= 0) {
let valueFirst = stack.pop()
let valueSecond = stack.pop()
// 第一个出栈的结果在表达式的左边, 第二个出栈的结果在表达式的右边
let expStr = valueSecond + item + valueFirst // 表达式的字符串
let res = parseInt(eval(expStr))
stack.push(res)
}else {
stack.push(item)
}
}
// 表达式如果是正确的,最终栈里只有一个元素,为表达式的最终结果
return stack.pop()
}
3.实现一个具有min方法的栈,返回栈里的最小元素,并且时间复杂度为o(1).
分析过程,非常重要 !要多阅读几遍
- 1.我们要实现的这个栈,除了有常规的方法外,还要有min方法,所以我们要有两个栈,一个栈是为常规方法存在的,一个栈是为min方法存在的。
- 2.编程思想有一个很重要的思想叫分而治之,就是分开想,分开处理。我们现在考虑常规栈dataStack,它就是一个常规栈,就是常规方法pop ,push哪些,正常实现就好了。 这个时候我们再去考虑最小栈 minStack ,这个时候,你就不要再去考虑常规栈(dataStack)的情况了,只关心最小栈(minStack), 最小栈就是用来存储栈里的最小值,我们先考虑边界情况,如果minStack为空,这个时候如果push进来一个元素,那这个元素一定是最小的,所以此时,直接放入minStack即可。如果minStack不为空,那么minStack栈的栈顶不就是他的最小元素吗,如果push 进来的元素比minStack的栈顶元素还小,那就直接放入minStack就好了,这样minStack始终存放的都是最小值。
function MinStack() {
let dataStack = new Stack() // 用来存放数据
let minStack = new Stack() // 用来存放最小值
this.push = function(item) {
dataStack.push(item) // 数据栈,是常规栈,常规操作即可
// 如果minStack为空,或者item < minStack的栈顶元素,就放入minStack中
if (minStack.isEmpty() || item < minStack.top()) {
minStack.push(item)
} else {
// 如果item > 栈顶元素,把minStack的栈顶元素再放入一次
// 是为了 minStack 和 dataStack 的元素个数一致
minStack.push(minStack.top())
}
}
// pop 的时候,两个栈都pop
this.pop = () => {
dataStack.pop()
minStack.pop()
}
// min方法就是取minStack的栈顶元素
this.min = function () {
return minStack.top()
}
}
let min_stack = new MinStack()
min_stack.push(1)
min_stack.push(2)
min_stack.push(3)
console.log(min_stack.min())