隊列
隊列是先近先出的數據結構(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 循環和棧來模擬遞歸期間的系統調用棧。