【LeetCode】算法与数据结构笔记(二) 队列和栈

队列

  队列是先近先出的数据结构(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和目标结点G之间的最短路径

  若使用广度优先搜索,第零轮中首先需要处理根节点,即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和目标结点G之间的最短路径

  同样对于上图所示这个问题,采用深度优先搜索算法的时候,我们从根结点 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 的深度。因此,在最坏的情况下,维护系统栈需要 O(h)O(h),其中 hDFS 的最大深度。

  递归解决方案的优点是它更容易实现。 但是,存在一个很大的缺点:如果递归的深度太高,你将遭受堆栈溢出。 在这种情况下,您可能会希望使用 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 循环和栈来模拟递归期间的系统调用栈。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章