栈合集一

108 阅读13分钟

题目来源:books.halfrost.com/leetcode/Ch…

1700.无法吃午餐的学生

// 这道题,还是挺有意思的,有两个考虑的角度,一个是从人的角度考虑,一个是从三明治的角度考虑
// 这里,我们从三明治的角度考虑,将栈顶的三明治分配给,一名学生,当这个三明治没人要的时候,结束循环  ;
func countStudents(students []int, sandwiches []int) int {
    mp := make(map[int]int)
    for _, s := range students {
        mp[s]++
    }

    // 记录分了多少三明治
    cnt := 0 
    for _, s := range sandwiches {
        if mp[s] <= 0  {
            return len(sandwiches) - cnt
        }

        mp[s]--
        cnt++
    }

    return 0 
}

1673.找出最具竞争力的子序列

什么时候才适合用单调递归栈呢?当我们需要维持元素之间相对顺序的时候,可以考虑使用单调栈;单调栈需要考虑:

  • 为了保证栈的特性,什么时候出栈?
// 这道题能想到的一种解法,是找出所有的长度为k的子序列,然后在一个个比较,哪个更具有竞争力
// 单调栈的解法,什么时候,我们才会释放栈中元素?
// 当,[i, len(nums)) + 当前栈中元素,大于 k个元素,并且,当前元素小于栈顶元素的时候,需要释放栈尾元素
// 当我们希望,保持的是一个单调递增栈(栈中元素要保证递增且当前元素一定是要入栈的)
// 那么,我们就可以通过比较栈顶元素与当前处理元素的关系来,确定是否需要出栈
func mostCompetitive(nums []int, k int) []int {
    sk := []int{}

    for i := 0; i < len(nums); i++ {
        // 为了保证栈中元素递增,需要校验出栈条件
        for len(sk) + len(nums) - i > k && len(sk) > 0 && nums[i] < sk[len(sk)-1] {
            sk = sk[:len(sk)-1]
        }

        sk = append(sk, nums[i])
    }

    // [0, k), 左闭右开区间,这里只有k个元素
    return sk[:k]
}

1653.使字符串平衡的最少删除次数

// 1. 动态规划求解
  // 如果当前元素为b,保留b,就是前面元素的解;
  // 如果当前元素为a,保留a,删除前面的b,或者前面pre + 1; 
// 2. 模拟分解法求解
// dp[i]表示区间[0, i)之间需要删除多少次
// 当有时需要考虑越界问题的时候,可以考虑左闭右开区间;
// 这道题,重点知道状态转移方程如何写
func minimumDeletions(s string) int {
    dp := make([]int, len(s) + 1)

    bCount := 0 
    // 以字符串为标杆,来计算dp数组的内容
    for i := 0; i< len(s); i++ {
        if s[i] == 'a' {
            dp[i+1] = min(dp[i] + 1, bCount)
        } else if s[i] == 'b' {
            dp[i+1] = dp[i]
            bCount++
        }
    }

    return dp[len(s)]
}

1614.括号的最大嵌套深度

// 这道题,定义cnt,左括号++,右括号--,然后统计最大结果值
// 这种小题也挺有意思的,这里的++,--,是不是可以考虑成入栈与出栈的意思呢
func maxDepth(s string) int {
    cnt, res := 0, 0 
    for i := 0;i < len(s); i++ {
        if s[i] == '(' {
            cnt++
            res = max(res, cnt)
        } else if s[i] == ')'{
            cnt--
        }
    }

    return res 
}

1249.移除无效的括号

为了达到平衡,标准是余量>=0, 并且到整个字符串末尾余量一定为0,当余量大于0时,需要从右到左,删除最先遇到的左括号;

// 括号合法,余量始终需要大于等于0,且最后余量必须为0;
// 余量 = 左括号数 - 右括号数
// 那么,可以使用cnt记录余量,当计算过程中为0表示,需要删除右括号;
// 当遍历完整个字符串后,cnt>0,那么需要从头在遍历一会,删除左括号
// 重点在余量的计算上
func minRemoveToMakeValid(s string) string {
  cnt := 0 
  res := []byte{}

  for i := 0; i < len(s); i++ {
    if s[i] == '(' {
      cnt++
    } else if s[i] == ')' {
      if cnt > 0 {
        cnt--
      } else {
        continue
      }
    }

    res = append(res, s[i])
  }

  // 多于的左括号一定是在右边,结论确实是这样的,那又改怎么理解呢?这里我还是还是想不明白啊
  for i := len(res)-1; i >= 0; i-- {
    if res[i] == '(' && cnt > 0 {
      // 这里有个习惯,应该是执行完操作以后,在减去标记数(因为业务开发过程中,标记数修改出现的问题,远小于业务代码)
      res = append(res[:i], res[i+1:]...)
      cnt--
    }
  }
    
  return string(res)
}

1209.删除字符串中的所有相邻重复项 II

暴力法:

// 这道题的暴力解法,也有必要学习一下
// 题意:删除原有字符串中部分字符,重新生成新的字符,对于这种,如果没有极限的空间复杂度的要求
// 可以将数组移动到另外一个地方,在进行相应的逻辑处理;
// 为什么需要摞个地,因为这样更加方便操作
// 我们不应该看了一遍答案以后,靠记忆力来重写这类题,而是应该考逻辑来解题;
// 当遍历处理每个节点的时候,如果该节前与前面节点凑成k个,就把这些节点删除,直到遍历完真个字符串
func removeDuplicates(s string, k int) string {
    arr := []byte{}

    for i := 0; i < len(s); i++ {
        arr = append(arr, s[i])

        for len(arr) > 0 {
            tmp := arr[len(arr)-1]
            cnt := 0 
            for i := len(arr) - 1; i >= 0; i-- {
                if arr[i] == tmp {
                    cnt++
                } else {
                    break
                }
            }

            if cnt == k {
                arr = arr[:len(arr) - k]
            } else {
                break
            }
        }
    }

    return string(arr)
}

暴力解法,在于每次遇到新的字符串的时候,都需要重新检查字符串前面的值,如果我们使用栈,记录该字符串出现的次数,那么就可以消除重复计算了;

栈解法:

func removeDuplicates(s string, k int) string {
    type Node struct {
        cnt int    // 字符出现的次数
        char byte   // 每个字符
    }

    sk := []Node{}

    for i := 0; i < len(s); i++ {
        // 如果等于栈顶元素
        if len(sk) > 0 && sk[len(sk)-1].char == s[i] {
            sk[len(sk)-1].cnt++

            if sk[len(sk)-1].cnt == k {
                sk = sk[:len(sk)-1]
            }
        } else {
        	// 如果不等于栈顶元素
            sk = append(sk, Node{1, s[i]})
        }
    }

    // 将栈里面的内容,合成一个新的字符串
    by := []byte{}
    for i := 0; i < len(sk); i++ {
        for j := 0; j < sk[i].cnt; j++ {
            by = append(by, sk[i].char)
        }
    }

    return string(by)
}

1190.反转每对括号间的子串

// 字符串的反转,那么,我们不能在原字符串串上进行修改,可以将其转为[]byte,然后进行调整;
func reverseParentheses(s string) string {
    arr := []byte(s)
    // 存左右括号的下标
    sk := []int{}

    for i := 0; i < len(s); i++ {
        if s[i] == '(' {
            sk = append(sk, i)
        } else if s[i] == ')' {
            star, end := sk[len(sk)-1]+1, i-1
            for star < end {
                arr[star], arr[end] = arr[end], arr[star]
                star++
                end--
            }

            sk = sk[:len(sk)-1]
        } 
    }

    // 这里为啥不使用
    sb := strings.Builder{}
    for i := 0; i < len(arr); i++ {
        if arr[i] != '(' && arr[i] != ')' {
            sb.WriteByte(arr[i])
        }
    }

    //  这种代码一定不要写,会有问题;
    // for i := 0; i < len(arr); i++ {
    //     if arr[i] == '(' || arr[i] == ')' {
    //         arr = append(arr[:i], arr[i+1:]...)
    //     }
    // }

    return sb.String()
}

1111.有效括号的嵌套深度

//  这个新概念真的不好理解,首先,明白什么是嵌套深度;
//  如果当前处理的是(,将其入栈以后,栈的高度,就是当前字符的嵌套深度;
//  如果当前处理的是),当前栈的高度,就是当前的字符的嵌套深度,然后将(出栈;
//  我们将嵌套深度为奇数的分配个给一个序列,嵌套深度为偶数的分配个另外一个序列;
func maxDepthAfterSplit(seq string) []int {
    cnt := 0 
    ans := make([]int, 0)

    for i := 0; i < len(seq); i++ {
        if seq[i] == '(' {
            cnt++
            ans = append(ans, cnt % 2)
        } else {
            ans = append(ans, cnt % 2)
            cnt--
        }
    }

    return ans 
}

1047.删除字符串中的所有相邻重复项

func removeDuplicates(s string) string {
    // 需要将当前字符与前面的字符进行对比,那么可以考虑使用栈来解决这个问题;
    // 这道题,用来提高信息是非常好的
    sk := []byte{}

    for i := 0; i < len(s); i++ {
        // 判断是否需要出栈
        if len(sk) != 0 && sk[len(sk)-1] == s[i] {
            sk = sk[:len(sk)-1]
        } else {
            sk = append(sk, s[i])
        }
    }

    return string(sk)
}

1021.删除最外层的括号

//  有效括号,题给定的就是有效括号
//  将题意转为栈的形式,入栈,栈高++,出栈,栈高--;
//  当栈为0时,去掉最外层的括号,就是所求的值;
//  入栈时候,栈为0的字符去掉,出栈是,为0 的字符去掉;
//  我们的目的还是修改原字符,按照规定保留部分字符
func removeOuterParentheses(s string) string {
    cnt := 0 
    ans := []byte{}

    //  遍历原字符串,按照规定取部分字符出来
    for i := 0; i < len(s); i++ {

        //  这两者之间的顺序,需要注意,也就是,检查 - 执行的顺序;
        if s[i] == ')' {
            cnt--
        }

        if cnt >0  {
            ans = append(ans, s[i])
        }

        if s[i] == '(' {
            cnt++
        } 
    }

    return string(ans)
}

1019.链表中的下一个更大节点

/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
//  下一个更大的节点,往右边看,第一个比当前节点大的节点
// 这里可以考虑使用单调栈的方式来求解:单调递减栈
// 如果当前元素,比栈顶元素小,入栈;
// 如果当前元素,比栈顶元素大,出栈,直到栈为空,或者当前元素比栈顶元素小;
// 因为需要修改元素的值,所以栈中元素需要保留结果值所在的下标
func nextLargerNodes(head *ListNode) []int {
    type Node struct {
        Index int 
        Val  int  
    }

    sk := []Node{} 
    res := []int{}

    cur := head 
    for cur != nil {
        //  出栈逻辑
        for len(sk) != 0 && sk[len(sk)-1].Val < cur.Val {
            // 出栈节点的坐标,为res中的坐标
            res[sk[len(sk)-1].Index] = cur.Val 
            sk = sk[:len(sk)-1]
        }

        res = append(res, 0)
        sk = append(sk, Node{len(res)-1, cur.Val})

        cur = cur.Next 
    }

    return res 
}

1003.检查替换后的词是否有效

// 重构问题,本题给定字符串s,如果s有效,需要保证是abc的倍数,并且abc是有序的
// 所以可以考虑使用栈来解决这个问题,当我们使用栈的时候,需要考虑什么时候入栈,什么时候出栈
// 入栈:任何字符均可入栈;
// 出栈:当字符串与栈内组成“abc”的时候,考虑出栈;
// 这里需要注意两个常识:sk[len(sk)-3:], 有3个元素;
// sk[:len(sk)-3],去掉栈尾的三个元素
func isValid(s string) bool {
    sk := []byte{}
    for i := 0; i < len(s); i++ {
        sk = append(sk, s[i])
        if len(sk) >= 3 && string(sk[len(sk)-3:]) == "abc" {
            sk = sk[:len(sk)-3]
        }
    }

    return len(sk) == 0 
}

946.验证栈序列

解法一:看到这道题,第一想法是这样的,但是过于复杂;

// 重构问题
// 这道题,肯定是用栈来解决,用栈的话,需要考虑两个问题
// 什么时候入栈?当pushed处理的元素,不等于poped处理元素时入栈;
// 什么时候出栈?当两者相等时,出栈,直到不相等为止;
func validateStackSequences(pushed []int, popped []int) bool {
    i, j := 0, 0
    sk := []int{}

    for i < len(pushed) && j < len(popped) {
        if pushed[i] != popped[j] {
            sk = append(sk, pushed[i])
            i++
        } else {
            i++
            j++

            for j < len(popped) && len(sk) >0 && sk[len(sk)-1] == popped[j] {
                sk = sk[:len(sk)-1]
                j++
            }
        }
    }

    return len(sk) == 0 
}

解法二:霜神的解法

// 刚刚那解法,过于繁琐了,因为,我们控制的是,待处理的字符什么时候入栈,换个思路
// 将所有的字符,全部入栈,入栈以后,在检测是否满足出栈的条件
// 最后,只要待处理的字符等于pushed的长度,表示符合要求
func validateStackSequences(pushed []int, popped []int) bool {
    sk, j, N := []int{}, 0, len(pushed)

    for i := 0; i < N; i++ {
        sk = append(sk, pushed[i])
        for j < N && len(sk) > 0 && sk[len(sk)-1] == popped[j] {
            sk = sk[:len(sk)-1]
            j++
        }
    }

    return j == N 
}

921.使括号有效的最少添加

// 有效括号的题,写过挺多回了
// 在脑子里面,寻找以前有过的经验,发现可以用栈来求解,最后栈的长度,就表示需要添加的最少括号数
func minAddToMakeValid(s string) int {
    sk := []byte{}
    for i := 0; i < len(s); i++ {
        if s[i] == '(' {
            sk = append(sk, s[i])
        } else if s[i] == ')' && len(sk) > 0 && sk[len(sk)-1] == '(' {
            sk = sk[:len(sk)-1]
        } else {
            sk = append(sk, s[i])
        }
    }

    return len(sk)
}

901.股票价格跨度

type Node struct {
    Val int 
    Res  int 
}


type StockSpanner struct {
    Item []Node 
}


// 【重构问题】这道题的第一映像,就应该使用栈,那么栈里面存什么呢?存下标,还是具体的数值呢?
// 这里,我们需要计算两个差值,所以,应该存的是下标
// 这,应该是一个单调递减栈,所以,当元素小于等于栈等元素时,入栈,此时为1;
// 出栈,当元素大于栈顶元素时,出栈,直到符合条件
func Constructor() StockSpanner {

    stockSpanner := StockSpanner{make([]Node, 0)}
    return stockSpanner
}


// 我觉得,霜神的这种写法,更加合适,如果这样写,我们就不需要关注小标的各种问题
// 当前处理函数,就是栈顶元素+1
func (this *StockSpanner) Next(price int) int {
    // 提前定义一个res变量,这样更加好处理,也可简化后续的代码逻辑
    res := 1 

    if len(this.Item) == 0 {
        this.Item = append(this.Item, Node{price, res})
        return res 
    }

    // 单调栈的惯用逻辑,单调递减栈
    for len(this.Item) > 0 && this.Item[len(this.Item)-1].Val <= price {
        res = res + this.Item[len(this.Item)-1].Res 
        this.Item = this.Item[:len(this.Item)-1]
    }

    this.Item = append(this.Item, Node{price, res})

    return res 
}


/**
 * Your StockSpanner object will be instantiated and called as such:
 * obj := Constructor();
 * param_1 := obj.Next(price);
 */

【注】:单调栈,请都用这种写法,更加有利于建立个人体系;

739.每日温度

//  这里明显使用单调栈来来求解,而且是单递减栈
//  首先,定义一个输出的数组,当元素出栈的时候,更新数组中的值
//  更新的规则是,将当前数的下标,减去出栈数的下标
func dailyTemperatures(temperatures []int) []int {
    res := make([]int, len(temperatures))
    sk := []int{}

    for i, v  := range temperatures {
        for len(sk) > 0 && temperatures[sk[len(sk)-1]] < v {
            idx := sk[len(sk)-1]
            res[idx] = i - idx
            sk = sk[:len(sk)-1]
        }

        sk = append(sk, i)
    }

    return res 
}

682.棒球比赛

//  根据当前处理的数,对数组前面的数进行处理,所以这里考虑使用栈来求解
func calPoints(operations []string) int {
    sk := []int{}

    for i := 0; i < len(operations); i++ {
        op := operations[i]

        switch op {
        case "+":
            last1 := sk[len(sk)-1]
            last2 := sk[len(sk)-2]

            sk = append(sk, last1 + last2)
        case "D":
            last1 := sk[len(sk)-1]

            sk = append(sk, last1 * 2)
        case "C":
            sk = sk[:len(sk)-1]
        default:
            c, _ := strconv.Atoi(op)
            
            sk = append(sk, c)
        }
    }

    var res int 
    for i := 0; i < len(sk); i++ {
        res += sk[i]
    }

    return res 
}

【注】:这些常用的字符串处理API还是要好好考虑一下的啊;

907.子数组的最小值之和

// 重构问题:给定一个数组,找出其中所有的子数组,然后获取每个子数组的最小值,最后计算所有最小值的和
func sumSubarrayMins(arr []int) int {
    res, mod := 0, 1000000007

    for i := 0; i < len(arr); i++{
        tmp := arr[i]

        for j := i; j < len(arr); j++ {
            if arr[j] < tmp {
                tmp = arr[j] 
            }

            res += tmp
        }
    }

    return res % mod
}

【注】该写法会超时,但是也有必要好好理解下

单调栈+动态规划

// 递归+单调栈的方法,这道题,还是很有意思的呢
// 之前,我们说过,这道题的暴力解法是,找出所有的子数组,然后计算每个子数组的最小值,最后将所有最小值相加
// 换个思路,所有子数组,是不是 等价于 所有以A[i]结尾的子数组
//  当我们找全了所有以A[i]结尾的子数组之后,结果集就完备了
//  所以,我们确定用栈来保存以A[i]结尾的所有子数组,当我们遍历每个元素的时候,就相当于栈的入栈和出栈了

type Node struct {
    num  int   // 元素的值
    cnt  int   // 元素的个数
}

func sumSubarrayMins(arr []int) int {
    sk := []Node{}

    // res, 最后的结果集
    // tmp,为以A[i]结尾,所有子数组最小值的和
    res, tmp := 0, 0 
    mod := 1000000007

    // 单调递增栈
    for i := 0; i < len(arr); i++ {
        cnt := 1 
        
        // 出栈操作
        for len(sk) > 0 && sk[len(sk)-1].num > arr[i] {
            node := sk[len(sk)-1]
            tmp -= node.num * node.cnt

            cnt += node.cnt
            sk = sk[:len(sk)-1]
        }

        //  以nums[i]结尾的所有子数组中,nums[i]为最小的次数为cnt
        sk = append(sk, Node{arr[i], cnt})
        tmp += arr[i] * cnt 

        res += tmp
    }

    return res % mod
}