【LeetCode】栈与队列

目录

1. 用栈实现队列(232)

2. 用队列实现栈(225)

3. 有效的括号(20)

4. 逆波兰表达式求值(150)

5. 滑动窗口最大值(239)

6. 前k个高频元素(347)

7. 接雨水(42)


1. 用栈实现队列(232)

【题目描述】

使用两个栈实现队列的功能:

  • pop(): 弹出队头元素。
  • peek(): 获取队头元素。
  • push(): 向队尾添加元素。
  • empty(): 队列是否为空。

【示例】

Input
["MyQueue", "push", "push", "peek", "pop", "empty"]
[[], [1], [2], [], [], []]
Output
[null, null, null, 1, 1, false]

Explanation
MyQueue myQueue = new MyQueue();
myQueue.push(1); // queue is: [1]
myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
myQueue.peek(); // return 1
myQueue.pop(); // return 1, queue is [2]
myQueue.empty(); // return false

【思路】

        使用栈模拟队列的行为,需要两个栈,一个是输入栈,另一个是输出栈,要注意输入栈和输出栈的关系。在“push”数据时,只要将数据放进输入栈即可。但在“pop”数据时,如果输出栈为空,则把输入栈的数据全部导入输出栈;如果输出栈不为空,则直接从输出栈中弹出数据即可。判断队空:如果输入栈和输出栈都为空,则说明模拟的队列为空。

【Go代码】

type MyQueue struct {
	input []int
	output []int
}

func Constructor() MyQueue {
	return MyQueue{
		input: make([]int, 0),
		output: make([]int, 0),
	}
}

func (this *MyQueue) Push(x int)  {
	this.input = append(this.input, x)
}

func (this *MyQueue) Pop() int {
	if this.Empty() {
		return 0
	}
	if len(this.output) == 0 {
		for len(this.input) != 0 {
			this.output = append(this.output, this.input[len(this.input) - 1])
			this.input = this.input[:len(this.input)-1]
		}
	}
	res := this.output[len(this.output) - 1]
	this.output = this.output[:len(this.output)-1]
	return res
}

func (this *MyQueue) Peek() int {
	if this.Empty() {
		return 0
	}
	if len(this.output) == 0 {
		for len(this.input) != 0 {
			this.output = append(this.output, this.input[len(this.input) - 1])
			this.input = this.input[:len(this.input)-1]
		}
	}
	return this.output[len(this.output) - 1]
}

func (this *MyQueue) Empty() bool {
	return len(this.input) == 0 && len(this.output) == 0
}

/**
 * Your MyQueue object will be instantiated and called as such:
 * obj := Constructor();
 * obj.Push(x);
 * param_2 := obj.Pop();
 * param_3 := obj.Peek();
 * param_4 := obj.Empty();
 */

2. 用队列实现栈(225)

【题目描述】

使用两个单向队列实现栈的功能:

  • pop(): 弹出栈顶元素。
  • push(x): 将 x 入栈。
  • top(): 获取栈顶元素。
  • empty(): 判断栈是否为空。

【示例】

Input
["MyStack", "push", "push", "top", "pop", "empty"]
[[], [1], [2], [], [], []]
Output
[null, null, null, 2, 2, false]

Explanation
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.top(); // return 2
myStack.pop(); // return 2
myStack.empty(); // return False

【思路】

        队列的规则时先进先出,把一个队列的数据导入另一个队列,数据的顺序并没有改变。如果用两个队列实现栈,那么两个队列就没有输入队列和输出队列的关系,另一个队列完全是用来备份的。

        用两个队列 queue1 和 queue2 实现栈的功能,模拟入栈用 queue1 。模拟出栈的时候 queue2 的作用就是备份,把 queue1 中除队列最后一个元素外的所有元素都备份到 queue2 中,然后弹出 queue1 中剩下的最后一个元素,再把 queue2 的所有元素导回 queue1 。

        其实使用一个队列也可以实现栈,在模拟栈弹出元素的时候只要将队列中除了最后一个元素之外的所有元素重新添加到队列尾部,此时出队操作就相当于从栈顶弹出元素了。

【Go代码】

type MyStack struct {
    queue1 []int
    queue2 []int
}

func Constructor() MyStack {
    return MyStack{
        queue1: make([]int, 0),
        queue2: make([]int, 0),
    }
}

func (this *MyStack) Push(x int)  {
    this.queue1 = append(this.queue1, x)
}

func (this *MyStack) Pop() int {
    length := len(this.queue1)
    if length == 0 {
        return 0
    }
    // 1.把queue1中除了最后一个元素之外的所有元素导入queue2
    for i := 0; i < length - 1; i++ {
        this.queue2 = append(this.queue2, this.queue1[0])
        this.queue1 = this.queue1[1:]
    }
    // 2.queue1出队,即模拟出栈操作
    res := this.queue1[0]
    this.queue1 = this.queue1[0:0]
    // 3.把queue2所有元素导回queue1
    for len(this.queue2) != 0 {
        this.queue1 = append(this.queue1, this.queue2[0])
        this.queue2 = this.queue2[1:]
    }
    return res
}

func (this *MyStack) Top() int {
    return this.queue1[len(this.queue1) - 1]
}

func (this *MyStack) Empty() bool {
    return len(this.queue1) == 0
}

/**
 * Your MyStack object will be instantiated and called as such:
 * obj := Constructor();
 * obj.Push(x);
 * param_2 := obj.Pop();
 * param_3 := obj.Top();
 * param_4 := obj.Empty();
 */

3. 有效的括号(20)

【题目描述】

        一个字符串中,只要有"(",就要有")"来闭合,"{" "}" "[" "]" 也同理。字符串只包含以上字符,判断字符串是否合法。

【示例1】

Input: s = "()[]{}"
Output: true

【示例2】

Input: s = "(]"
Output: false

【思路】

        遍历字符串 s ,遇到左括号则进栈,遇到右括号,则判断栈顶元素是否与该右括号匹配,若匹配,则弹出栈顶元素;若不匹配,则返回 false 。还有两种情况需返回 false :1. 遇到右括号时栈空;2. 字符串遍历完时栈不空。若字符串遍历完时栈空,则返回 true 。

【Go代码】

func isValid(s string) bool {
    hash := map[byte]byte{')': '(', ']': '[', '}': '{'}
    stack := make([]byte, 0)
    if s == "" {
        return true
    }
    for i := 0; i < len(s); i++ {
        if s[i] == '(' || s[i] == '[' || s[i] == '{' {
            stack = append(stack, s[i])
        } else if len(stack) > 0 && hash[s[i]] == stack[len(stack) - 1] {
            stack = stack[:len(stack)-1]
        } else {
            return false
        }
    }
    return len(stack) == 0
}

【性能分析】

        时间复杂度:O(n)

        空间复杂度:O(n)

4. 逆波兰表达式求值(150)

【题目描述】

        给出逆波兰表达式,求得对应的值。

【示例】

Input: tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"]
Output: 22
Explanation: ((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22

【思路】

        从左到右遍历表达式,遇到数字则进栈,遇到运算符则弹出栈顶的两个元素(先弹出的作为 num2 ,后弹出的作为 num1 ),并进行计算( num1 运算符 num2 ),再将计算结果压入栈中。最后返回栈顶元素的值。

【Go代码】

func evalRPN(tokens []string) int {
    stack := make([]int, 0)
    for _, token := range tokens {
        val, err := strconv.Atoi(token)
        if err == nil {
            stack = append(stack, val)
        } else {
            num1, num2 := stack[len(stack) - 2], stack[len(stack) - 1]
            stack = stack[:len(stack) - 2]
            switch token {
            case "+":
                stack = append(stack, num1 + num2)
            case "-":
                stack = append(stack, num1 - num2)
            case "*":
                stack = append(stack, num1 * num2)
            case "/":
                stack = append(stack, num1 / num2)
            }
        }
    }
    return stack[0]
}

【性能分析】

        时间复杂度:O(n)

        空间复杂度:O(n)

5. 滑动窗口最大值(239)

【题目描述】

        一个大小为 k 的滑动窗口,从前向后在数组 nums 上移动,返回滑动窗口每移动一次时窗口中的最大值。

【示例】

Input: nums = [1,3,-1,-3,5,3,6,7], k = 3
Output: [3,3,5,5,6,7]
Explanation: 
Window position                Max
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

【思路】

使用一个维护元素单调递减的队列(即单调队列)记录滑动窗口的最大值。

设计该单调队列(队头只出,队尾可进可出)时,poppush 操作要保持如下原则:

(1)pop(val): 如果窗口移除的元素 val 等于单调队列的队头元素,那么队头元素出队,否则不进行任何操作;

(2)push(val): 如果窗口添加的元素 val 大于单调队列的队尾元素,那么队尾元素从队尾出队,直到当前元素的数值小于或等于队尾元素再将 val 入队。

【Go代码】

func maxSlidingWindow(nums []int, k int) []int {
    queue := NewMyQueue()
	res := make([]int, 0)
    // 前k个元素入队
	for i := 0; i < k; i++ {
		queue.Push(nums[i])
	}
    // 记录前k个元素的最大值
	res = append(res, queue.Front())
	for i := k; i < len(nums); i++ {
        // 滑动窗口移除最前面的元素
		queue.Pop(nums[i-k])
        // 滑动窗口添加最后面的元素
		queue.Push(nums[i])
        // 记录滑动窗口的最大值
		res = append(res, queue.Front())
	}
	return res
}


type MyQueue struct {
	queue []int
}

func NewMyQueue() MyQueue {
	return MyQueue{
		queue: make([]int, 0),
	}
}

func (this *MyQueue) Front() int {
	return this.queue[0]
}

func (this *MyQueue) Push(val int) {
	for !this.Empty() && val > this.queue[len(this.queue)-1] {
		this.queue = this.queue[:len(this.queue)-1]
	}
	this.queue = append(this.queue, val)
}

func (this *MyQueue) Pop(val int) {
	if !this.Empty() && val == this.Front() {
		this.queue = this.queue[1:]
	}
}

func (this *MyQueue) Empty() bool {
	return len(this.queue) == 0
}

【性能分析】

        时间复杂度:O(n)

        空间复杂度:O(n)

6. 前k个高频元素(347)

【题目描述】

        在一个数组中找到出现频率前 k 高的元素。

【示例】

Input: nums = [1,1,1,2,2,3], k = 2
Output: [1,2]

【思路】

  1. 遍历数组,用 map 记录不同元素出现的次数;
  2. 构建长度为 k 的小根堆,堆顶始终存放出现次数最少的元素;
  3. 遍历 map ,与堆顶元素进行比较,如果当前元素出现的次数大于堆顶元素,则用当前元素替换堆顶元素,并再次进行堆排序,否则不做处理;
  4. 遍历完成后,堆中的 k 个元素即为结果。

【Go代码】

func topKFrequent(nums []int, k int) []int {
	// 遍历数组,记录每个元素出现的次数
	numTimeMap := map[int]int{}
	for _, item := range nums {
		numTimeMap[item]++
	}
	
	heap := make([]Elem, 0)
	// heap[0]不属于堆,用于暂存元素
	heap = append(heap, Elem{Num:0, Times:0})
	res := make([]int, 0)	// 存放结果
	firstBuildFlag := false	// 判断数组长度是否达到k,用于建堆
	for num, times := range numTimeMap {
		e := Elem{Num:num, Times:times}
		if len(heap) <= k {	// 数组长度没有达到k,则继续添加
			heap = append(heap, e)
			continue
		} else if !firstBuildFlag {	// 建堆
			topKBuildMinHeap(heap, len(heap)-1)
			firstBuildFlag = true
		}
		if e.Times > heap[1].Times {
			// 如果元素出现的次数大于堆顶元素,则替换堆顶元素
			heap[1] = e
			topKHeadAdjust(heap, len(heap)-1, 1)
		}
	}
	for _, e := range heap {
		res = append(res, e.Num)
	}
	return res[1:]
}

type Elem struct {
	Num int		// 元素
	Times int	// 元素出现的次数
}

func topKBuildMinHeap(heap []Elem, length int) {
	for i := length/2; i > 0; i-- {
		topKHeadAdjust(heap, length, i)
	}
}

func topKHeadAdjust(heap []Elem, length, k int) {
	heap[0] = heap[k]
	for i := 2*k; i <= length; i *= 2 {
		if i < length && heap[i].Times > heap[i+1].Times {
			i++
		}
		if heap[0].Times > heap[i].Times {
			heap[k] = heap[i]
			k = i
		} else {
			break
		}
	}
	heap[k] = heap[0]
}

【性能分析】

        时间复杂度:O(nlogk)

        空间复杂度:O(n)

7. 接雨水(42)

【题目描述】

        给出一排宽度为 1 、高度为 n 的柱子,求可以接到雨水的面积。

【示例】

Input: height = [0,1,0,2,1,0,1,3,2,1,2,1]
Output: 6
Explanation: The above elevation map (black section) is represented by array [0,1,0,2,1,0,1,3,2,1,2,1]. 
In this case, 6 units of rain water (blue section) are being trapped.

【思路1】

双指针法:

        按竖直方向计算每一列雨水的高度。每列雨水的高度取决于该列左侧最高的柱子和右侧最高的柱子之间的最矮柱子的高度。从头遍历所有的列,求出每一列雨水的面积,相加即为结果。注意第一列和最后一列不接雨水。

【Go代码1】

func trap(height []int) int {
	sum := 0
	for i := 1; i < len(height) - 1; i++ {    // 第一列和最后一列不接雨水
		h := 0    // 当前列的雨水高度
		lHeight := height[i]    // 当前列的左边的最高柱子的高度
		rHeight := height[i]    // 当前列的右边的最高柱子的高度
		for l := i - 1; l >= 0; l-- {
			if height[l] > lHeight {
				lHeight = height[l]
			}
		}
		for r := i + 1; r < len(height); r++ {
			if height[r] > rHeight {
				rHeight = height[r]
			}
		}
		if lHeight < rHeight {
			h = lHeight - height[i]
		} else {
			h = rHeight - height[i]
		}
		if h > 0 {
			sum += h
		}
	}
	return sum
}

【性能分析1】

        时间复杂度:O(n^{2})

        空间复杂度:O(1)

【思路2】

动态规划法:

        在双指针法中,我们可以知道只要记录左边柱子的最高高度和右边柱子的最高高度,就可以计算当前位置的雨水面积。

        当前位置的雨水面积:[ min(左边柱子的最高高度, 右边柱子的最高高度) - 当前柱子高度 ] * 单位宽度。这里的单位宽度是 1 。

        为了得到两边的最高高度,使用双指针遍历其实是有重复计算的。我们可以将每个位置的左边最高高度记录在一个数组中(maxLeft),将右边最高高度记录在另一个数组中(maxRight),这就用到了动态规划,可以避免重复计算。

        当前位置的左边最高高度是前一个位置的左边最高高度和本高度比较后的最大值。

        从左向右遍历:maxLeft[i] = max(height[i], maxLeft[i-1]) 。

        从右向左遍历:maxRight[i] = max(height[i], maxRight[i+1]) 。

【Go代码2】

func trap(height []int) int {
	if len(height) <= 2 {
		return 0
	}
	length := len(height)
	maxLeft := make([]int, length)
	maxRight := make([]int, length)
    // 记录每个柱子左边柱子的最大高度
	maxLeft[0] = height[0]
	for i := 1; i < length; i++ {
		maxLeft[i] = max(height[i], maxLeft[i-1])
	}
    // 记录每个柱子右边柱子的最大高度
	maxRight[length-1] = height[length-1]
	for i := length - 2; i >= 0; i-- {
		maxRight[i] = max(height[i], maxRight[i+1])
	}
    // 求和
	sum := 0
	for i := 1; i < length - 1; i++ {
		count := min(maxLeft[i], maxRight[i]) - height[i]
		if count > 0 {
			sum += count
		}
	}
	return sum
}

func max(a int, b int) int {
	if a > b {
		return a
	} else {
		return b
	}
}

func min(a int, b int) int {
	if a < b {
		return a
	} else {
		return b
	}
}

【性能分析2】

        时间复杂度:O(n)

        空间复杂度:O(n)

【思路3】

单调栈法:

        单调栈类似单调队列(5.滑动窗口最大值[239]),就是保持栈内元素有序。单调栈只需保存数组的下标,通过 height[s.Top()] 就能知道下标对应的柱子的高度,按照行方向计算雨水面积,栈内元素从栈底到栈顶按柱子高度从大到小的顺序排列。一旦发现欲入栈的柱子的高度大于栈顶元素,说明此时出现凹槽,栈顶元素就是凹槽底部的柱子,栈顶第二个元素就是凹槽底部左边的柱子,而欲入栈的元素就是凹槽底部右边的柱子。

        首先将下标 0 的柱子入栈,然后从下标 1 开始遍历所有的柱子。有三种情况:

  1. 如果当前遍历的柱子的高度小于栈顶元素的高度,则直接入栈;
  2. 如果当前遍历的柱子的高度等于栈顶元素的高度,则更新栈顶元素;
  3. 如果当前遍历的柱子的高度大于栈顶元素的高度,那么就出现凹槽了:将栈顶元素弹出,弹出元素就是凹槽的底部高度的下标,也就是中间位置,下标记为 mid ,对应的高度为 height[mid] ;弹出元素之后的栈顶元素就是凹槽的左边柱子,下标为 s.Top() ,对应的高度为 height[s.Top()] ;当前遍历的元素就是凹槽右边的柱子,下标为 i ,对应的高度为 height[i] 。雨水的高度就是 min(凹槽左边高度, 凹槽右边高度)-凹槽底部高度,雨水的宽度就是 凹槽右边的下标-凹槽左边的下标-1

【Go代码3】

func trap(height []int) int {
	if len(height) <= 2 {
		return 0
	}
    // s存放下标,计算时使用下标对应的柱子高度
	s := Stack{index: make([]int, 0)}
	s.Push(0)
	sum := 0
	for i := 1; i < len(height); i++ {
		if height[i] < height[s.Top()] {    // 情况一
			s.Push(i)
		} else if height[i] == height[s.Top()] {    // 情况二
			s.Pop()    // 其实可以不加这一行,效果是一样的
			s.Push(i)
		} else {
			for !s.Empty() && height[i] > height[s.Top()] {    // 情况三
				mid := s.Top()
				s.Pop()
				if !s.Empty() {
					h := min(height[s.Top()], height[i]) - height[mid]    // 高
					w := i - s.Top() - 1    // 宽
					sum += h * w
				}
			}
			s.Push(i)
		}
	}
	return sum
}

type Stack struct {
	index []int
}

func (s *Stack) Push(val int) {
	s.index = append(s.index, val)
}

func (s *Stack) Pop() {
	s.index = s.index[:len(s.index)-1]
}

func (s *Stack) Top() int {
	return s.index[len(s.index)-1]
}

func (s *Stack) Empty() bool {
	return len(s.index) == 0
}

func min(a int, b int) int {
	if a < b {
		return a
	} else {
		return b
	}
}
// 精简版(好像只处理情况三,其实是把情况一和二融合了)
func trap(height []int) int {
	s := Stack{index: make([]int, 0)}
	s.Push(0)
	sum := 0
	for i := 1; i < len(height); i++ {
		for !s.Empty() && height[i] > height[s.Top()] {
			mid := s.Top()
			s.Pop()
			if !s.Empty() {
				h := min(height[s.Top()], height[i]) - height[mid]
				w := i - s.Top() - 1
				sum += h * w
			}
		}
		s.Push(i)
	}
	return sum
}

type Stack struct {
	index []int
}

func (s *Stack) Push(val int) {
	s.index = append(s.index, val)
}

func (s *Stack) Pop() {
	s.index = s.index[:len(s.index)-1]
}

func (s *Stack) Top() int {
	return s.index[len(s.index)-1]
}

func (s *Stack) Empty() bool {
	return len(s.index) == 0
}

func min(a int, b int) int {
	if a < b {
		return a
	} else {
		return b
	}
}

【性能分析3】

        时间复杂度:O(n)

        空间复杂度:O(n)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值