Heap & Stack & Queue Exercise 1
堆(Heap)
-
优先队列 是一种抽象的数据类型,而 堆 是一种数据结构。所以 堆 并不是 优先队列 , 堆 是实现 优先队列 的一种方式。
-
实现 优先队列 的方式有很多种,比如数组和链表。但是,这些实现方式只能保证插入操作和删除操作中的一种操作可以在 O(1)的时间复杂度内完成,而另外一个操作则需要在 O(N)的时间复杂度内完成
-
而 堆 能够使 优先队列 的插入操作在 O(log N)的时间复杂度内完成,删除操作在 O(log N)的时间复杂度内完成
- 堆 是一种特别的二叉树,满足以下条件的二叉树,可以称之为 堆:
- 完全二叉树;
- 每一个节点的值都必须 大于等于或者小于等于 其孩子节点的值。
-
堆 具有以下的特点:
- 可以在 O(logN)的时间复杂度内向 堆 中插入元素;
- 可以在 O(logN)的时间复杂度内向 堆 中删除元素;
- 可以在 O(1) 的时间复杂度内获取 堆 中的最大值或最小值
-
堆的分类 堆 有两种类型:最大堆 和 最小堆。
最大堆:堆中每一个节点的值 都大于等于 其孩子节点的值。所以最大堆的特性是 堆顶元素(根节点)是堆中的最大值。
最小堆:堆中每一个节点的值 都小于等于 其孩子节点的值。所以最小堆的特性是 堆顶元素(根节点)是堆中的最小值。
1. 创建堆
-
对于Python,使用heapq,但是只有最小堆,没有最大堆,最大堆是用取反
-
-
时间复杂度: O(N)
空间复杂度: O(N)
-
2. 往堆插入元素
3. 获取堆顶元素
4. 删除堆顶元素
5. 获取堆的长度
6. 堆排序
7. TOP K问题 和 The Kth问题
方法一:
同理,获得第k个就是重复k次,但是只取第k次
方法二:
队列和栈
-
队列是先进先出
-
栈是先进后出
232. 用栈实现队列
[^尝试用deque实现]:
难度简单524
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push
、pop
、peek
、empty
):
实现 MyQueue
类:
void push(int x)
将元素 x 推到队列的末尾int pop()
从队列的开头移除并返回元素int peek()
返回队列开头的元素boolean empty()
如果队列为空,返回true
;否则,返回false
说明:
- 你只能使用标准的栈操作 —— 也就是只有
push to top
,peek/pop from top
,size
, 和is empty
操作是合法的。 - 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。
进阶:
- 你能否实现每个操作均摊时间复杂度为
O(1)
的队列?换句话说,执行n
个操作的总时间复杂度为O(n)
,即使其中一个操作可能花费较长时间。
示例:
输入:
["MyQueue", "push", "push", "peek", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 1, 1, false]
解释:
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和peek。每次需要输出的时候,输入栈将所有的内容pop到输出栈,这时输出栈里面是反向的
-
首先是最简单的empty,只要看两个栈是不是空的判断
-
python的list的append符合push的功能,在list的末尾加上元素。所以对于push,只要在输入栈后面加上append(x)
-
至于pop,需要考虑的是当前输出栈里面是不是空的
- 如果是空的,将当前的输入栈内容pop进来
- 如果非空,pop输出栈的第一个内容,使用的是python自带的pop,他会返回list末尾元素
-
而peak可以服用pop,再把pop的元素push到stack_out
-
初始化init时,别忘了self
而之后提及这两个,也别忘加self
此外,之后调用功能,比如empty,也要self!
-
empty也有问题,但是没太理解
-
代码:
225. 用队列实现栈
难度简单420
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push
、top
、pop
和 empty
)。
实现 MyStack
类:
void push(int x)
将元素 x 压入栈顶。int pop()
移除并返回栈顶元素。int top()
返回栈顶元素。boolean empty()
如果栈是空的,返回true
;否则,返回false
。
注意:
- 你只能使用队列的基本操作 —— 也就是
push to back
、peek/pop from front
、size
和is empty
这些操作。 - 你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。
示例:
输入:
["MyStack", "push", "push", "top", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 2, 2, false]
解释:
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.top(); // 返回 2
myStack.pop(); // 返回 2
myStack.empty(); // 返回 False
思路:
- 需要使用到python的deque,也就是双向队列来完成
- 它的操作很像list 同时相比于list实现的队列,deque实现拥有更低的时间和空间复杂度。
- list实现在出队(pop)和插入(insert)时的空间复杂度大约为O(n),deque在出队(pop)和入队(append)时的时间复杂度是O(1)。
- deque的内部还是list,所以append和pop都可以操作,都是末尾的。对于左边的,可以使用popleft和appendleft。而这道题,我们只是模拟基础的队列,所以我们用且只会用到append和popleft
- 首先是push操作,就是对于queue_in进行append
- 然后是empty,可以使用和之前一样的写法,区别在于这里只用关注in
- 但是不能是==[], 而必须是长度 ==0
- 然后是pop,对于queue_in,先popleft到只剩下一个后,和queue_out进行交换,然后queue_out再popleft。
- 我们可以看到,queue_out是一直保持空的状态,所以,empty只需要关于queue_in即可
- 最后是top,top要出去的是栈顶,但是queue没有pop,只有popleft。所以我们使用deque的索引,找到queue_in的最后一位
代码:
20. 有效的括号
难度简单2853
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
示例 1:
输入:s = "()"
输出:true
示例 2:
输入:s = "()[]{}"
输出:true
示例 3:
输入:s = "(]"
输出:false
示例 4:
输入:s = "([)]"
输出:false
示例 5:
输入:s = "{[]}"
输出:true
思路:
- 首先思考什么情况下会是False
- 左边的括号多了
- 右边的括号多了
- 左边和右边匹配不上
- 使用栈来存储左侧括号,如果匹配,从栈的尾部pop出去
- 第一个False,对应遍历完但是栈仍然非空
- 第二个False,对应栈空了,但是没有遍历完
- 第三个False,单纯不匹配
- 具体实现上,对于左括号的三种形式({[, 如果匹配,在stack中存入他们的对偶形式。当发现不是这三种,也就是不是左括号时,看stack[-1],和输入中的元素是否一样。
- 如果一样,把stack末尾pop
- 如果不一样return False
- 如果这个时候stack为空,但是输入还是有的,return False
- 上面是对输入遍历,如果遍历完输入,stack非空,return False
代码:
1047. 删除字符串中的所有相邻重复项
难度简单310
给出由小写字母组成的字符串 S
,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
思路:
- 和上一题很相似,使用栈来存储,再进行匹配判断,如果匹配,就pop,栈中剩下的作为输出
- 输出结果要用“”.join()
代码:
示例:
输入:"abbaca"
输出:"ca"
解释:
例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。
150. 逆波兰表达式求值
难度中等438
根据 逆波兰表示法,求表达式的值。
有效的算符包括 +
、-
、*
、/
。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
说明:
- 整数除法只保留整数部分。
- 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
示例 1:
输入:tokens = ["2","1","+","3","*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
示例 2:
输入:tokens = ["4","13","5","/","+"]
输出:6
解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6
示例 3:
输入:tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"]
输出:22
解释:
该算式转化为常见的中缀算术表达式为:
((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
思路:
- 使用栈解决,把数字存储到stack里面,一旦遇到符号,把栈后两位pop出来,进行运算,把结果放回栈
代码:
239. 滑动窗口最大值
难度困难1325
给你一个整数数组 nums
,有一个大小为 k
的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k
个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[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
示例 2:
输入:nums = [1], k = 1
输出:[1]
示例 3:
输入:nums = [1,-1], k = 1
输出:[1,-1]
示例 4:
输入:nums = [9,11], k = 2
输出:[11]
示例 5:
输入:nums = [4,-2], k = 2
输出:[4]
思路:
-
使用的方法是单调队列:同时弹出队首和队尾的元素是双端队列。满足这种单调性的双端队列一般称作「单调队列」。这里的单调性是队列中存储的是位置,位置对应的数字是单调递减的
-
随着窗口滑动,会有一个新的元素进来.
- 如果新的元素比队尾元素小,nums[i] <nums[queue[-1]], 则存入队列,
- 如果比队尾元素大或一样,则将队尾元素pop出去,直到发现有一个队列中元素比他大,或者他来到了队列最前端。
- 对于前k个可以这么做来找到queue[0],存储到ans中
-
对于k到n,此时每滑动一次都要将queue[0]对应的值放入ans
- 相比起前k个。需要额外考虑的是:万一一直是最先进来的元素最大,比如[5,4,3,2,1],那么首部将不会被赶走。所以需要不断看当前queue中第一位所存储的位置是否满足queue[0]>i-k,如果不满足需要popleft把头部去掉
-
注意,生成第一个queue的元素,要经历好几次pop。所以不能分开来,要在一个while里面:
代码:
347. 前 K 个高频元素
难度中等961
给你一个整数数组 nums
和一个整数 k
,请你返回其中出现频率前 k
高的元素。你可以按 任意顺序 返回答案。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:
输入: nums = [1], k = 1
输出: [1]
思路:
-
三件事情,计算并存储频率,频率比较,拿出前k个高频的
-
对于计算并且存储频率,使用Python字典实现hashmap,使用hashmap存储每个数字的频率。
-
使用Python 字典get() 函数返回指定键的值
-
dict.get(key, default=None)
-
key – 字典中要查找的键。
-
default – 如果指定的键不存在时,返回该默认值。
-
根据每个key,不断查询该key的出现频率,并进行更新
-
-
对于频率比较,使用优先队列。
- 这里使用heapq模块来进行堆操作。堆(heap)是一种优先队列。优先队列让你能够以任意顺序添加对象,并随时找出(并删除)最小的元素。相比于列表方法min,这样做的效率要高得多。
- 创建
heap=[]
。然后将刚才得到的键值对压入heap中,注意压入负值,即负的频率。这样到时候pop出来的就是从大到小的顺序。 - 此外,需要注意遍历heapmap的
key, value
的时候要加.items()
-
最后是前k个,进行
heappop(heap)[1]
。heappop得到的是最前面的,是负的最厉害的频率,也就是最大频率。[1]是他对于的键,因为heap里面存的是-value和key
代码:
155. 最小栈
难度简单1124
设计一个支持 push
,pop
,top
操作,并能在常数时间内检索到最小元素的栈。
push(x)
—— 将元素 x 推入栈中。pop()
—— 删除栈顶的元素。top()
—— 获取栈顶元素。getMin()
—— 检索栈中的最小元素。
示例:
输入:
["MinStack","push","push","push","getMin","pop","top","getMin"]
[[],[-2],[0],[-3],[],[],[],[]]
输出:
[null,null,null,null,-3,null,0,-2]
解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2
Input: heights = [2,4]
Output: 4