队列 —— 先入先出的数据结构
目录:
1.基本队列实现
队列是先入先出(First In First Out)的数据结构,插入数据(入队)的位置称为“队尾(Tail)”,出队的位置称为“队头(Front)”。
一种简单的队列实现如下:
// 来自 leetcode
#include iostream
class MyQueue{
private:
vector<int> data;
int p_start; // 注意其指示的是队头的位置
public:
MyQueue() {p_start = 0;}
bool enQueue(int x) {
data.push(x);
return true;
}
bool deQueue() {
if (isEmpty()) return false;
++p_start;
return true;
}
int Front() {
if (isEmpty()) return -1;
return data[p_start];
}
bool isEmpty() {
return (p_start >=data.size());
}
}
int main() {
MyQueue q;
q.enQueue(5);
q.enQueue(3);
if (!q.isEmpty()) {
cout << q.Front() << endl;
}
q.deQueue();
if (!q.isEmpty()) {
cout << q.Front() << endl;
}
q.deQueue();
if (!q.isEmpty()) {
cout << q.Front() << endl;
}
}
上面的实现很简单,但在某些情况下效率很低。 随着起始指针的移动,浪费了越来越多的空间。 当我们有空间限制时,这将是难以接受的。
2.循环队列
我们可以使用固定大小的数组和两个指针来指示起始位置和结束位置。 目的是重用我们之前提到的被浪费的存储。
class MyCircularQueue {
private:
int * m_pQueueData;
int m_iQueueCapacity;
int m_iQueueLen;
int m_iHead;
int m_iTail;
public:
MyCircularQueue(int queueCapacity) {
m_iQueueCapacity = queueCapacity;
m_pQueueData = new int[m_iQueueCapacity];
clearQueue();
}
~MyCircularQueue() {
delete[] m_pQueueData;
m_pQueueData = NULL;
}
void clearQueue() {
m_iHead = 0;
m_iTail = 0;
m_iQueueLen = 0;
}
bool enQueue(int value) {
if (isFull()){
return false;
} else {
m_pQueueData[m_iTail] = value;
// 每次入队后,队尾将自动移动到下一个位置
++m_iTail;
m_iTail = m_iTail % m_iQueueCapacity;
++m_iQueueLen; // 别忘了增加队列长度
return true;
}
}
bool deQueue() {
if (isEmpty()) {
return false;
} else {
--m_iQueueLen;
++m_iHead;
m_iHead = m_iHead % m_iQueueCapacity;
return true;
}
}
int Front() {
if (isEmpty()) {
return -1;
} else {
return m_pQueueData[m_iHead];
}
}
int Rear() {
if (isEmpty()) {
return -1;
} else {
// 注意队尾元素的获取
return m_iTail == 0 ? m_pQueueData[m_iQueueCapacity-1] : m_pQueueData[m_iTail-1];
}
}
bool isEmpty() {
return m_iQueueLen == 0;
}
bool isFull() {
return m_iQueueLen == queueCapacity;
}
}
C++ 提供了内置队列库,平时使用时我们无需重复发明轮子。
int main() {
queue<int> q;
q.push(5);
q.front(); // 获得队头元素 5
q.size(); // 1
q.pop(); // 出队 返回空
q.empty() // true
}
3.循环队列小试:数据流中的移动平均值(LeetCode 364)
leetcode 346-数据流中的移动平均值
给定一个整数数据流和一个窗口大小,根据该滑动窗口的大小,计算其所有整数的移动平均值。
示例:
MovingAverage m = new MovingAverage(3);
m.next(1) = 1
m.next(10) = (1 + 10) / 2
m.next(3) = (1 + 10 + 3) / 3
m.next(5) = (10 + 3 + 5) / 3
实现如下:
class MovingAverage {
private:
int m_iPreviousSum;
int m_iQueueCapacity;
queue<int> m_iQueue;
public:
MovingAverage(int capacity) {
m_iQueueCapacity = capacity;
m_iPreviousSum = 0;
}
double next(int value) {
if (m_iQueue.size() == m_iQueueCapacity) {
m_iPreviousSum -= m_iQueue.front();
m_iQueue.pop();
}
m_iPreviousSum += value;
m_iQueue.push(value);
return (double)m_iPreviousSum / m_iQueue.size();
}
}
4.队列和广度优先搜索(BFS)
采用 BFS 算法的时候常用队列作为容器。
因为对于 BFS 来说,与树的层序遍历类似,越是接近根结点的结点将越早地遍历。
结点的处理顺序与它们添加到队列的顺序是完全相同的顺序,即先进先出(FIFO)。这就是我们在 BFS 中使用队列的原因。
4.1 墙与门(LeetCode 286)
你被给定一个 m × n 的二维网格,网格中有以下三种可能的初始化值:
-1 表示墙或是障碍物
0 表示一扇门
INF 无限表示一个空的房间
然后,我们用 231 - 1 = 2147483647 代表 INF。你可以认为通往门的距离总是小于 2147483647 的。
你要给每个空房间位上填上该房间到 最近 门的距离,如果无法到达门,则填 INF 即可。
输入:
INF -1 0 INF
INF INF INF -1
INF -1 INF -1
0 -1 INF INF
输出:
3 -1 0 1
2 2 1 -1
1 -1 2 -1
0 -1 3 4
void wallsAndGates(vector<vector<int>> &rooms) {
if (rooms.empty() || rooms[0].empty()) return;
const int INF = 2147483647;
queue<int> myQue;
int numSteps = 0;
vector<pair<int,int>> directions = {{1,0}, {0,1}, {-1,0}, {0,-1}};
int rows = rooms.size(), cols = rooms[0].size();
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (rooms[i][j] == 0) {
myQue.push({i,j});
}
}
}
while (!myQue.empty()) {
auto pos = myQue.front();
myQue.pop();
int x = pos.first, y = pos.second;
numSteps = rooms[x][y] + 1;
for (auto d : driections) {
int nx = x + d.first, ny = y + d.second;
// 注意坐标合法条件判定
if (nx < 0 || nx >= rows ||
ny < 0 || ny >= cols ||
rooms[nx][ny] != INF) {
continue;
}
rooms[nx][ny] = numSteps;
myQue.push({nx, ny});
}
}
}
4.2 岛屿数量(LeetCode 200)
给定一个由 '1'(陆地)和 '0'(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。
输入:
11110
11010
11000
00000
输出: 1
输入:
11000
11000
00100
00011
输出: 3
int numIslands(vector<vector<char>>& grid) {
if (grid.empty() || grid[0].empty()) return 0;
queue<pair<int,int>> myQue;
int numIsland = 0;
int rows = grid.size(), cols = grid[0].size();
vector<pair<int,int>> directions = {{1,0}, {0,1}, {-1,0}, {0,-1}};
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (grid[i][j] == '1') {
++numIsland;
myQue.push({i,j});
while(!myQue.empty()) {
auto pos = myQue.front();
myQue.pop();
int x = pos.first, y = pos.second;
for (auto d : directions) {
int nx = x + d.first, ny = y + d.second;
if (nx < 0 || nx >= rows ||
ny < 0 || ny >= cols ||
gird[nx][ny] != '1') {
continue;
}
gird[nx][ny] == '0';
myQue.push({nx, ny});
}
}
}
}
}
return numIsland;
}
4.3 打开转盘锁(LeetCode 752)
你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字: '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' 。每个拨轮可以自由旋转:例如把 '9' 变为 '0','0' 变为 '9' 。每次旋转都只能旋转一个拨轮的一位数字。
锁的初始数字为 '0000' ,一个代表四个拨轮的数字的字符串。
列表 deadends 包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。
字符串 target 代表可以解锁的数字,你需要给出最小的旋转次数,如果无论如何不能解锁,返回 -1。
示例 1:
输入:deadends = ["0201","0101","0102","1212","2002"], target = "0202"
输出:6
解释:
可能的移动序列为 "0000" -> "1000" -> "1100" -> "1200" -> "1201" -> "1202" -> "0202"。
注意 "0000" -> "0001" -> "0002" -> "0102" -> "0202" 这样的序列是不能解锁的,
因为当拨动到 "0102" 时这个锁就会被锁定。
示例 2:
输入: deadends = ["8888"], target = "0009"
输出:1
解释:
把最后一位反向旋转一次即可 "0000" -> "0009"。
示例 3:
输入: deadends = ["8887","8889","8878","8898","8788","8988","7888","9888"], target = "8888"
输出:-1
解释:
无法旋转到目标数字且不被锁定。
示例 4:
输入: deadends = ["0000"], target = "8888"
输出:-1
提示:
死亡列表 deadends 的长度范围为 [1, 500]。
目标数字 target 不会在 deadends 之中。
每个 deadends 和 target 中的字符串的数字会在 10,000 个可能的情况 '0000' 到 '9999' 中产生。
这个题目可以将初始密码 “0000” 视为图的初始中心,每次对四位密码中的一位进行 +1 和 -1 的操作,产生邻接节点,进行广度优先遍历,每次迭代后对邻接节点创建新的邻接节点,添加到队列中。
如果发现新创建的邻接节点在 死亡数字组 里,则代表此图分支不通,不会将此分支加入到遍历中。
int openLock(vector<string>& deadends, string target) {
unordered_set<string> deadPoints(deadends.begin(), deadends.end());
if (deadPoints.find("0000") != deadPoints.end()) return -1;
queue<string> myQue;
myQue.push("0000");
unordered_set<string> visited;
visited.insert("0000");
int numSteps = 0, tempSize;
string tempValue, s1, s2;
while (!myQue.empty()) {
tempSize = myQue.size();
for (int i = 0; i < tempSize; i++) {
tempValue = myQue.front();
myQue.pop();
// 当前队列元素就是目标,寻找成功
if (tempValue == target) return numStep;
for (int j = 0; j < 4; j++) {
s1 = s2 = tempValue;
s1[j] = s1[j] == '9' ? '0' : s1[j] + 1;
s2[j] = s2[j] == '0' ? '9' : s2[j] - 1;
if (deadPoints.find(s1) == deadPoints.end() &&
visited.find(s1) == deadPoints.end()) {
myQue.push(s1);
visited.insert(s1);
}
if (deadPoints.find(s2) == deadPoints.end() &&
visited.find(s2) == deadPoints.end()) {
myQue.push(s2);
visited.insert(s2);
}
}
}
++numSteps;
}
return -1; // 寻找不到的情况
}
4.4 完全平方数(LeetCode 279)
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
示例 1:
输入: n = 12
输出: 3
解释: 12 = 4 + 4 + 4.
示例 2:
输入: n = 13
输出: 2
解释: 13 = 4 + 9.
解法一:
int numSquares(int n) {
if (n == 0) return 0;
queue<int> myQue;
myQue.push(n);
int numSteps = 0, tempSize, tempValue;
while(!myQue.empty()) {
tempSize = myQue.size();
for (int i = 0; i < tempSize; i++) {
tempValue = myQue.front();
myQue.pop();
for (int j = sqrt(tempValue); j > 0; j--) {
if (tempValue = j*j) {
return numSteps+1;
} else {
myQue.push(tempValue - j*j);
}
}
}
++numSteps;
}
return 0;
}
另一种解法:
int numSquares(int n) {
if(n==0)
return 0;
queue<pair<int,int>> v;
v.push(make_pair(n,0));
vector<bool> sp(n+1,false);
sp[n]=true;
while(v.size())
{
int size=v.size();
while(size--)
{
auto pt=v.front();
v.pop();
int d=pt.first;
sp[d]=true;
for(int i=1;d-i*i>=0;++i)
{
int a=d-i*i;
if(a==0)
{
int k=pt.second;
k++;
return k;
}
if(sp[a]==false) //做的优化,如果这个数,在前面已经访问过了,就不要将它入队列,因为后续结果一样
v.push(make_pair(a,pt.second+1));
}
}
}
return 0;
}