队列
队列是先近先出的数据结构(FIFO
)。插入(insert)操作也称作入队(enqueue),新元素始终被添加在队列的末尾。 删除(delete)操作也被称为出队(dequeue)。 你只能移除第一个元素。
#include <iostream>
class MyQueue {
private:
// store elements
vector<int> data;
// a pointer to indicate the start position
int p_start;
public:
MyQueue() {p_start = 0;}
/** Insert an element into the queue. Return true if the operation is successful. */
bool enQueue(int x) {
data.push_back(x);
return true;
}
/** Delete an element from the queue. Return true if the operation is successful. */
bool deQueue() {
if (isEmpty()) {
return false;
}
p_start++;
return true;
};
/** Get the front item from the queue. */
int Front() {
return data[p_start];
};
/** Checks whether the queue is empty or not. */
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;
}
}
上述实现方法效率低下。 随着起始指针的移动,浪费了越来越多的空间。 当我们有空间限制时,这将是难以接受的。
- 循环队列
更有效的方法是循环队列的方式。具体来说,我们可以使用固定大小的数组和两个指针来指示起始位置和结束位置。 目的是重用我们之前提到的被浪费的存储。
class MyCircularQueue {
private:
vector<int> data;
int size;
int head;
int tail;
int capacity;
public:
/** Initialize your data structure here. Set the size of the queue to be k. */
MyCircularQueue(int k) {
data.resize(k);
size=k;
head=0;
tail=0;
capacity=0;
}
/** Insert an element into the circular queue. Return true if the operation is successful. */
bool enQueue(int value) {
if(capacity<size){
data[tail]=value;
tail = (tail+1)%size;
++capacity;
return true;
} else return false;
}
/** Delete an element from the circular queue. Return true if the operation is successful. */
bool deQueue() {
if(capacity>0){
head=(head+1)%size;
--capacity;
return true;
} else return false;
}
/** Get the front item from the queue. */
int Front() {
if(!isEmpty()){
return data[head];
} else return -1;
}
/** Get the last item from the queue. */
int Rear() {
if(!isEmpty()){
return tail==0?data[size-1]:data[tail-1];
} else return -1;
}
/** Checks whether the circular queue is empty or not. */
bool isEmpty() {
return capacity==0;
}
/** Checks whether the circular queue is full or not. */
bool isFull() {
return capacity==size;
}
};
/**
* Your MyCircularQueue object will be instantiated and called as such:
* MyCircularQueue* obj = new MyCircularQueue(k);
* bool param_1 = obj->enQueue(value);
* bool param_2 = obj->deQueue();
* int param_3 = obj->Front();
* int param_4 = obj->Rear();
* bool param_5 = obj->isEmpty();
* bool param_6 = obj->isFull();
*/
这里的程序要注意用Rear()
函数取最后一个元素的时候,由于tail
需要减1
,因此当tail
等于0
的时候减1
是会越界的,因此需要处理一下为0
的特殊情况。
大多数的编程语言都有内置的队列库,我们无需重新造轮子。队列最基本的两个操作就是入队(enqueue
)和出队(dequeue
)。其用法如下:
#include <iostream>
int main() {
// 1. Initialize a queue.
queue<int> q;
// 2. Push new element.
q.push(5);
q.push(13);
q.push(8);
q.push(6);
// 3. Check if queue is empty.
if (q.empty()) {
cout << "Queue is empty!" << endl;
return 0;
}
// 4. Pop an element.
q.pop();
// 5. Get the first element.
cout << "The first element is: " << q.front() << endl;
// 6. Get the last element.
cout << "The last element is: " << q.back() << endl;
// 7. Get the size of the queue.
cout << "The size is: " << q.size() << endl;
}
队列和广度优先搜索
若使用广度优先搜索,第零轮中首先需要处理根节点,即A
进入队列;第一轮中处理根节点旁边的结点BCD
近队列,第二轮处理距离根结点两步的结点后面依此类推。
与树的层序遍历类似,越是接近根结点的结点将越早地遍历。
如果在第 k
轮中将结点 X
添加到队列中,则根结点与X
之间的最短路径的长度恰好是 k
。也就是说,第一次找到目标结点时,你已经处于最短路径中。
结点的处理顺序与它们添加到队列的顺序是完全相同的顺序,即先进先出(FIFO
)。这就是我们在 BFS
中使用队列的原因。
- 实现
在程序执行BFS
时,确定节点和边缘是非常重要的。其大体的实现思路如下所示:
/**
* Return the length of the shortest path between root and target node.
*/
int BFS(Node root, Node target) {
Queue<Node> queue; // store all nodes which are waiting to be processed
int step = 0; // number of steps neeeded from root to current node
// initialize
add root to queue;
// BFS
while (queue is not empty) {
step = step + 1;
// iterate the nodes which are already in the queue
int size = queue.size();
for (int i = 0; i < size; ++i) {
Node cur = the first node in queue;
return step if cur is target;
for (Node next : the neighbors of cur) {
add next to queue;
}
remove the first node from queue;
}
}
return -1; // there is no path from root to target
}
这道题目需要用到沉没岛屿这个思想,当前位置座标为1时,四周为1的岛屿都需要置1沉没。递归调用。下面提供广度优先搜索代码,用于理解题目意思和解题思路,之后再介绍深度优先搜索的时候再看与之不同点。
- 广度优先搜索:
class Solution {
private:
void bfs(vector<vector<char>>& grid, int row, int col){
queue<pair<int, int>> neighbors;
neighbors.push({row, col});
while(!neighbors.empty()){
auto rc = neighbors.front();
neighbors.pop();
int row = rc.first;
int col=rc.second;
if (row - 1 >= 0 && grid[row-1][col] == '1') {
neighbors.push({row-1, col});
grid[row-1][col] = '0';
}
if (row + 1 < grid.size() && grid[row+1][col] == '1') {
neighbors.push({row+1, col});
grid[row+1][col] = '0';
}
if (col - 1 >= 0 && grid[row][col-1] == '1') {
neighbors.push({row, col-1});
grid[row][col-1] = '0';
}
if (col + 1 < grid[0].size() && grid[row][col+1] == '1') {
neighbors.push({row, col+1});
grid[row][col+1] = '0';
}
}
}
public:
int numIslands(vector<vector<char>>& grid) {
int ans=0;
for(int row=0; row<grid.size();++row){
for(int col=0;col<grid[0].size();++col){
if(grid[row][col] == '1'){
++ans;
bfs(grid, row, col);
}
}
}
return ans;
}
};
栈
与队列不同,栈是后入先出的数据结构。通常插入操作在栈中被称作入栈push
,与队列类似,总是在堆栈的末尾添加一个新元素,但是,删除操作,退栈pop
,将始终删除队列中相对于它的最后一个元素。
栈的实现比队列容易。动态数组足以实现堆栈结构。其原理实现如下所示:
#include <iostream>
class MyStack {
private:
vector<int> data; // store elements
public:
/** Insert an element into the stack. */
void push(int x) {
data.push_back(x);
}
/** Checks whether the queue is empty or not. */
bool isEmpty() {
return data.empty();
}
/** Get the top item from the queue. */
int top() {
return data.back();
}
/** Delete an element from the queue. Return true if the operation is successful. */
bool pop() {
if (isEmpty()) {
return false;
}
data.pop_back();
return true;
}
};
int main() {
MyStack s;
s.push(1);
s.push(2);
s.push(3);
for (int i = 0; i < 4; ++i) {
if (!s.isEmpty()) {
cout << s.top() << endl;
}
cout << (s.pop() ? "true" : "false") << endl;
}
}
大多数流行的语言都提供了内置的栈库,因此你不必重新发明轮子。除了初始化,我们还需要知道如何使用两个最重要的操作:入栈和退栈。除此之外,你应该能够从栈中获得顶部元素。下面是一些供你参考的代码示例:
#include <iostream>
int main() {
// 1. Initialize a stack.
stack<int> s;
// 2. Push new element.
s.push(5);
s.push(13);
s.push(8);
s.push(6);
// 3. Check if stack is empty.
if (s.empty()) {
cout << "Stack is empty!" << endl;
return 0;
}
// 4. Pop an element.
s.pop();
// 5. Get the top element.
cout << "The top element is: " << s.top() << endl;
// 6. Get the size of the stack.
cout << "The size is: " << s.size() << endl;
}
栈和深度优先搜索
同样对于上图所示这个问题,采用深度优先搜索算法的时候,我们从根结点 A
开始。首先,我们选择结点 B
的路径,并进行回溯,直到我们到达结点 E
,我们无法更进一步深入。然后我们回溯到 A
并选择第二条路径到结点 C
。从 C
开始,我们尝试第一条路径到 E
但是 E
已被访问过。所以我们回到 C
并尝试从另一条路径到 F
。最后,我们找到了 G
。
在到达最深处的结点之后开始回溯,回溯时从栈中弹出最深的结点,所以也叫做深度优先搜索。但是DFS
找到的路径并不总是最短的路径。
以之前岛屿的例子举例,大多数情况下能使用BFS
时也可以使用DFS
。与 BFS
不同,更早访问的结点可能不是更靠近根结点的结点。因此,你在 DFS
中找到的第一条路径可能不是最短路径。
- 深度优先搜索:
class Solution {
private:
void dfs(vector<vector<char>>& grid, int row, int col){
if(row<0||row>=grid.size()||col<0||col>=grid[0].size()
||grid[row][col]=='0'){
return;
}
else {
grid[row][col] ='0';
dfs(grid, row-1,col);
dfs(grid, row+1,col);
dfs(grid, row,col-1);
dfs(grid, row,col+1);
}
}
public:
int numIslands(vector<vector<char>>& grid) {
int ans=0;
for(int row=0; row<grid.size();++row){
for(int col=0;col<grid[0].size();++col){
if(grid[row][col] == '1'){
++ans;
dfs(grid, row, col);
}
}
}
return ans;
}
};
深度优先搜索模板可总结为:
/*
* Return true if there is a path from cur to target.
*/
boolean DFS(Node cur, Node target, Set<Node> visited) {
return true if cur is target;
for (next : each neighbor of cur) {
if (next is not in visited) {
add next to visted;
return true if DFS(next, target, visited) == true;
}
}
return false;
}
当我们递归地实现 DFS
时,似乎不需要使用任何栈。但实际上,我们使用的是由系统提供的隐式栈,也称为调用栈(Call Stack)。
栈的大小正好是DFS
的深度。因此,在最坏的情况下,维护系统栈需要 ,其中 h
是 DFS
的最大深度。
递归解决方案的优点是它更容易实现。 但是,存在一个很大的缺点:如果递归的深度太高,你将遭受堆栈溢出。 在这种情况下,您可能会希望使用 BFS,或使用显式栈实现 DFS。
使用显式栈的模板:
/*
* Return true if there is a path from cur to target.
*/
boolean DFS(int root, int target) {
Set<Node> visited;
Stack<Node> s;
add root to s;
while (s is not empty) {
Node cur = the top element in s;
return true if cur is target;
for (Node next : the neighbors of cur) {
if (next is not in visited) {
add next to s;
add next to visited;
}
}
remove cur from s;
}
return false;
}
该逻辑与递归解决方案完全相同。 但我们使用 while 循环和栈来模拟递归期间的系统调用栈。