编程 | 队 与 栈 (Queue and Stack)

今天我们一起学习2种重要的数据结构:队列 和 栈。
本文根据LeetCode上的Explore教程 Introduction to Data Structure - Queue & Stack 整理而成。

Introduction

最常用的Collection是数组(Array),其最常使用的获取数据的操作是随机获取(Random access), 在C++中一般称作 subscribe。
但是有时,我们想要限制处理数据的顺序。最常见的限制是:先进先出(First in first out), 后进先出(Last in first out)。分别对应2种数据结构 队列(Queue) 和 栈(Stack)。

我们从 定义、实现 和 每种数据结构的内置操作 分别学习 队列 和 栈。
学习目标:

  1. 理解数据处理顺序FIFO和LIFO的原则;
  2. 手动实现数据结构;
  3. 熟悉语言内置的Queue和Stack;
  4. 解决基础的Queue-related问题,尤其是BFS;
  5. 解决基础的Stack-related问题;
  6. 理解系统的栈如何帮助你,在解决dfs和其他递归问题的时候。

Queue: First-in-first-out Data Structure

定义

先进先出 最普遍的比喻是排队(也就是队列), 最早进入队列的人最早被服务到。
所以队列总共只有2个modify 方法:

  • enqueue
  • dequeue

实现

Queue并不是基础的数据结构,我们可以用内置的数组来实现它。在C++中,Queue是container adapter, 并不是真正的container,其内部其实是deque。

实现实例:

#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;
    }
}

循环队列 Circular Queue

在之前的实现中,p_start之前的内存空间是被浪费掉的。为了充分利用,我们可以在内部使用循环利用array。

Circular Queue也叫做"Ring Buffer"。

Ring Buffer的实现:

class MyCircularQueue {
    vector<int> data;
    int head;
    int size;
public:
    /** Initialize your data structure here. Set the size of the queue to be k. */
    MyCircularQueue(int k) {
        data.insert(data.begin(), k, 0);
        head = 0;
        size = 0;
    }
    
    /** Insert an element into the circular queue. Return true if the operation is successful. */
    bool enQueue(int value) {
        if (isFull())
            return false;
        
        data[(head + size) % data.size()] = value;
        size++;
        return true;
    }
    
    /** Delete an element from the circular queue. Return true if the operation is successful. */
    bool deQueue() {
        if (isEmpty())
            return false;
        
        head = (head + 1) % data.size();
        size--;
        return true;
    }
    
    /** Get the front item from the queue. */
    int Front() {
        if (isEmpty()) return -1;
        return data[head];
    }
    
    /** Get the last item from the queue. */
    int Rear() {
        if (isEmpty()) return -1;
        return data[(head + size - 1) % data.size()];
    }
    
    /** Checks whether the circular queue is empty or not. */
    bool isEmpty() {
        return size == 0;
    }
    
    /** Checks whether the circular queue is full or not. */
    bool isFull() {
        return size == data.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();
 */

Queue的应用

最典型的应用,BFS。
BFS(Breadth-first Search)一般用来发现从根节点到目标节点的最短距离。

应用BFS的场景:

  • do traversal
  • find the shortest path

场景中常使用的数据结构:

在具体应用中,BFS里的节点可能是真正的节点状态,边可能是真正的边状态间的转移

BFS的模版,一定要背下来,以后面试或做题提高速度和bug-free的可能。

template 1

/**
 * 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
}

template 2

/**
 * 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
    Set<Node> visited;  // store all the nodes that we've visited
    int step = 0;       // number of steps neeeded from root to current node
    // initialize
    add root to queue;
    add root to visited;
    // 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) {
                if (next is not in used) {
                    add next to queue;
                    add next to visited;
                }
                remove the first node from queue;   
            }
        }
    }
    return -1;          // there is no path from root to target
}
  1. 在每一轮中,队列中的节点都是等待被处理的。
  2. 没经历一次外层的while循环,都离root更远一步, step++。

template 2

在图中,确保每个节点不被多次访问很重要。否则BFS会陷入无限循环。此时,我们增加一个hashset用来标注是否节点已经被访问到。

/**
 * 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
    Set<Node> visited;  // store all the nodes that we've visited
    int step = 0;       // number of steps neeeded from root to current node
    // initialize
    add root to queue;
    add root to visited;
    // 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) {
                if (next is not in used) {
                    add next to queue;
                    add next to visited;
                }
            }
            remove the first node from queue;   
        }
    }
    return -1;          // there is no path from root to target
}

什么情况下可以不使用visited呢?

  1. 你确定不会出现重复访问的情况。比如,遍历树的时候。
  2. 你确实想要把一个节点加入队列多次。

栈 Stack

提到栈,我就想起周杰伦的《七里香》。歌中唱到,

雨下整夜,我的栈溢出就像雨水。

在LIFO数据结构中,最新被添加的元素最早被处理。在栈中,添加元素的操作叫做push(压栈),移除元素的操作叫做pop(弹出)。虽然在C++中,queue的操作也是同样的名字,但在大多数语言中,push 和 pop是stack专有的。

像队列一样,绝大多数语言提供了built-in的stack库,你不需要重复造轮子,只需要熟悉stack的常用操作,包括 push, pop, top(获取栈顶元素)。

单调栈的应用

https://leetcode.com/explore/featured/card/queue-stack/230/usage-stack/1363/

Intution: 维护一个单调递减的栈,遍历一遍数组T,如果将其放入栈中,把栈中比它小的元素都pop出来,并计算出相应的间隔。

时间复杂度:O(n),
空间复杂度: O(n).

class Solution {
public:
    vector<int> dailyTemperatures(vector<int>& T) {
        vector<int> result(T.size(), 0);
        stack<pair<int, int>> s; // temperature, day
        for (int i = 0; i < T.size(); ++i) {
            while (!s.empty() && s.top().first < T[i]) {
                pair<int, int> current = s.top();
                result[current.second] = i - current.second;
                s.pop();
            }
            s.push({T[i], i});
        }
        
        return result;
    }
};

stack 和 DFS

DFS是stack的重要应用之一,可以用来寻找从根节点到目标节点的路径(注意不一定是最短)。DFS是回溯的一种算法,只有到达最深的节点才进行回溯,尝试其他路径。

DFS 模版 1,递归版本

/*
 * 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 模版 2,迭代版本

递归版本的优点是实现起来更加简单。缺点是,如果递归深度太深,会stack overflow。
这时,你可能会想要用 BFS 或者 用显式的栈实现DFS。

/*
 * Return true if there is a path from cur to target.
 */
boolean DFS(int root, int target) {
    Set<Node> visited;
    Stack<Node> stack;
    add root to stack;
    while (s is not empty) {
        Node cur = the top element in stack;
        remove the cur from the stack;
        return true if cur is target;
        for (Node next : the neighbors of cur) {
            if (next is not in visited) {
                add next to visited;
                add next to stack;
            }
        }
    }
    return false;
}

实现的逻辑和递归解法相同。只不过我们使用while循环和显式的stack来模仿系统栈。

Implement Queue using Stacks

用栈来实现队列。
我记得在程序员面试金典上遇到过一样的题目,感觉是一道很经典的题目。需要面试者对队列和栈都非常熟悉才行。

Intuition: 队列是FIFO,栈是LIFO,所以我们可以用2个栈来实现一个队列。定义将一个栈装入另一个栈的操作为颠倒。我们可以通过颠倒, 将LIFO变为FIFO,而且只有在需要dequeue的时候,才需要做颠倒的操作。

class MyQueue {
    stack<int> a, b;
public:
    /** Initialize your data structure here. */
    MyQueue() {
        
    }
    
    /** Push element x to the back of queue. */
    void push(int x) {
        a.push(x);
    }
    
    /** Removes the element from in front of queue and returns that element. */
    int pop() {
        if (b.empty()) {
            while (!a.empty()) {
                b.push(a.top());
                a.pop();
            }
        }
        int ret = b.top();
        b.pop();
        return ret;
    }
    
    /** Get the front element. */
    int peek() {
        if (b.empty()) {
            while (!a.empty()) {
                b.push(a.top());
                a.pop();
            }
        }
        return b.top();
    }
    
    /** Returns whether the queue is empty. */
    bool empty() {
        return a.empty() && b.empty();
    }
};

/**
 * Your MyQueue object will be instantiated and called as such:
 * MyQueue obj = new MyQueue();
 * obj.push(x);
 * int param_2 = obj.pop();
 * int param_3 = obj.peek();
 * bool param_4 = obj.empty();
 */

Implement Stack using Queues

那么如何用FIFO实现LIFO呢?解法可能不是那么明显,但却十分简单。每次push时,都把队列里的元素重新入对一遍,就把最后入对的放到第一个位置了。

总结

dfs 和 bfs 属于想法简单,但实现起来不容易,尤其是bug-free并且快速地实现更属不易。
由于考察的编程基础涉及广泛,面试官也特别喜欢考类似的题目。几个模版必须熟练背会才行。

队 和 栈 更是基础的数据结构。在计算机科学中随处可见,虽然很多时候不会直接考察,但很多算法都要用到。熟练手写stack 和 queue 和使用built-in的stack 和 queue是每个合格程序员的基础。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容