算法与数据结构_队列

队列

一、如何理解“队列”?

  1. 队列是一种操作受限的线性表数据结构
  2. 队列最大的特点就是先进先出 (First In First Out,FIFO)
  3. 最基本的操作:入队,放一个数据到队列尾部;出队,从队列头部取一个元素

二、顺序队列和链式队列

  1. 用数组实现的队列叫顺序队列,用链表实现的队列叫链式队列。
  2. 队列需要两个指针:一个是 head 指针,指向队头;一个是 tail 指针,指向队尾。
  3. 随着不停地进行入队、出队操作,head 和 tail 都会持续往后移动。当 tail 移动到最右边,即使数组中还有空闲空间,也无法继续往队列中添加数据了。
    实际上,我们在出队时可以不用搬移数据。如果没有空闲空间了,只需要在入队时,再集中触发一次数据的搬移操作。出队函数 dequeue() 保持不变,我们稍加改造一下入队函数 enqueue() 的实现,当队列的 tail 指针移动到数组的最右边后,如果有新的数据入队,我们可以将 head 到 tail 之间的数据,整体搬移到数组中 0 到 tail-head 的位置。
  4. 基于链表的实现,同样需要两个指针:head 指针和 tail 指针。分别指向链表的第一个结点和最后一个结点。入队时,tail->next= new_node, tail = tail->next;出队时,head = head->next。
//JAVA实现
// 用数组实现的队列
public class ArrayQueue {
  // 数组:items,数组大小:n
  private String[] items;
  private int n = 0;
  // head表示队头下标,tail表示队尾下标
  private int head = 0;
  private int tail = 0;

  // 申请一个大小为capacity的数组
  public ArrayQueue(int capacity) {
    items = new String[capacity];
    n = capacity;
  }

  // 入队
  public boolean enqueue(String item) {
    // 如果tail == n 表示队列已经满了
    if (tail == n) return false;
    items[tail] = item;
    ++tail;
    return true;
  }

  // 出队
  public String dequeue() {
    // 如果head == tail 表示队列为空
    if (head == tail) return null;
    // 为了看的更加明确,把--操作放到单独一行来写了
    String ret = items[head];
    ++head;
    return ret;
  }
}
//随着数字不断入队,head tail不断后移,最终队满,如何搬移数据让队列继续可用呢?
   // 入队操作,将item放入队尾
  public boolean enqueue(String item) {
    // tail == n表示队列末尾没有空间了
    if (tail == n) {
      // tail ==n && head==0,表示整个队列都占满了
      if (head == 0) return false;
      // 数据搬移
      for (int i = head; i < tail; ++i) {
        items[i-head] = items[i];
      }
      // 搬移完之后重新更新head和tail
      tail -= head;
      head = 0;
    }
    
    items[tail] = item;
    ++tail;
    return true;
  }

三、循环队列

  1. 循环队列,原本数组是有头有尾的,是一条直线。把首尾相连,扳成了一个环。

循环队列

  1. 在数组实现队列的时候,会有数据搬移操作,要想解决数据搬移的问题,需要像环一样的循环队列。

  2. 要想写出没有 bug 的循环队列的实现代码,最关键的是,确定好队空和队满的判定条件。

    1. 队列为空的判断条件仍然是 head == tail。
    2. 当队满时,(tail+1)%n=head。 tail 指向的位置实际上是没有存储数据的。所以,循环队列会浪费一个数组的存储空间。
    //JAVA 循环队列
    public class CircularQueue {
      // 数组:items,数组大小:n
      private String[] items;
      private int n = 0;
      // head表示队头下标,tail表示队尾下标
      private int head = 0;
      private int tail = 0;
    
      // 申请一个大小为capacity的数组
      public CircularQueue(int capacity) {
        items = new String[capacity];
        n = capacity;
      }
    
      // 入队
      public boolean enqueue(String item) {
        // 队列满了
        if ((tail + 1) % n == head) return false;
        items[tail] = item;
        tail = (tail + 1) % n;
        return true;
      }
    
      // 出队
      public String dequeue() {
        // 如果head == tail 表示队列为空
        if (head == tail) return null;
        String ret = items[head];
        head = (head + 1) % n;
        return ret;
      }
    }
    

四、阻塞队列和并发队列

  1. 阻塞队列

    1. 阻塞队列就是在队列基础上增加了阻塞操作。

    2. 在队列为空的时候,从队头取数据会被阻塞。因为此时还没有数据可取,直到队列中有了数据才能返回;如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回。

    3. 基于阻塞队列实现的“生产者 - 消费者模型”,可以有效地协调生产和消费的速度。

  2. 并发队列

    1. 线程安全的队列,叫作并发队列。
    2. 最简单直接的实现方式是直接在 enqueue()、dequeue() 方法上加锁,但是锁粒度大并发度会比较低,同一时刻仅允许一个存或者取操作。
    3. 实际上,基于数组的循环队列,利用 CAS 原子操作,可以实现非常高效的并发队列。这也是循环队列比链式队列应用更加广泛的原因。

五、线程池没有空闲线程时,新的任务请求线程资源时,线程池该如何处理?各种处理策略又是如何实现的呢?

一般有两种处理策略。

第一种是非阻塞的处理方式,直接拒绝任务请求

另一种是阻塞的处理方式,将请求排队,等到有空闲线程时,取出排队的请求继续处理。

  1. 基于链表的实现方式,可以实现一个支持无限排队的无界队列(unbounded queue),但是可能会导致过多的请求排队等待,请求处理的响应时间过长。所以,针对响应时间比较敏感的系统,基于链表实现的无限排队的线程池是不合适的
  2. 基于数组实现的有界队列(bounded queue),队列的大小有限,所以线程池中排队的请求超过队列大小时,接下来的请求就会被拒绝,这种方式对响应时间敏感的系统来说,就相对更加合理。不过,设置一个合理的队列大小,也是非常有讲究的。队列太大导致等待的请求太多,队列太小会导致无法充分利用系统资源、发挥最大性能。
    (除了前面讲到队列应用在线程池请求排队的场景之外,队列可以应用在任何有限资源池中,用于排队请求,比如数据库连接池等。实际上,对于大部分资源有限的场景,当没有空闲资源时,基本上都可以通过“队列”这种数据结构来实现请求排队。)

六、【思考】

Q: 除了线程池这种池结构会用到队列排队请求,你还知道有哪些类似的池结构或者场景中会用到队列的排队请求呢?

A:

Q: 今天讲到并发队列,关于如何实现无锁并发队列,网上有非常多的讨论。对这个问题,你怎么看呢?

A:

说实话现在我还不太懂,这些名词都还没接触过。。。空着吧

习题

设计循环双端数组(LC:641)

LC:641——设计循环双端数组

//首先想到双端链表,创建一个头几点head 一个尾节点tail;
// he  ->  ta
// ad  <-  il
//初始化:头指向尾,尾指向头 新的节点就在中间操作,靠近头的部分以头为基准。靠近尾的部分以尾为基准
//同时维护一个统计队列元素个数的Dsize();
//维护一个队列内最大元素个数max_Dsize;
//在操作之前首先判断队是否满,是否空,然后在操作。
//我承认非常粗糙。。但是写的时候印象很深刻
class MyCircularDeque {
private:
    struct ListNode{
        int val;
        ListNode* next;
        ListNode* pre;
        ListNode(ListNode* node):val(node->val),next(node->next),pre(node->pre){}
        ListNode():val(-1),next(nullptr),pre(nullptr){}
        ListNode(int x):val(x),next(nullptr),pre(nullptr){}
        ListNode(int x,ListNode* pre, ListNode* next):val(x),pre(pre),next(next){}
    };
    int Dsize = 0;
    int max_Dsize;

public:
    ListNode* tail;
    ListNode* head;
    /** Initialize your data structure here. Set the size of the deque to be k. */
    MyCircularDeque(int k) {
        max_Dsize = k;
        head = new ListNode();
        tail = new ListNode();
        head->next = tail;
        tail->pre = head;
    }
    
    /** Adds an item at the front of Deque. Return true if the operation is successful. */
    bool insertFront(int value) {
        if(Dsize == max_Dsize) return false;
        ListNode* temp = new ListNode(value);
        head->next->pre = temp;
        temp->next = head->next;
        head->next = temp;
        temp->pre = head;
        Dsize++;
        return true;
    }
    
    /** Adds an item at the rear of Deque. Return true if the operation is successful. */
    bool insertLast(int value) {
        if(Dsize == max_Dsize) return false;
        ListNode* temp = new ListNode(value);
        tail->pre->next = temp;
        temp->pre = tail->pre;
        tail->pre = temp;
        temp->next = tail;
        Dsize++;
        return true;
    }
    
    /** Deletes an item from the front of Deque. Return true if the operation is successful. */
    bool deleteFront() {
        if(Dsize == 0) return false;
        auto temp = head->next;
        head->next = temp->next;
        temp->next->pre = head;
        //delete temp;
        Dsize--;
        return true;
    }
    
    /** Deletes an item from the rear of Deque. Return true if the operation is successful. */
    bool deleteLast() {
        if(Dsize == 0) return false;
        ListNode* temp = tail->pre;
        temp->pre->next = tail;
        tail->pre = temp->pre;
        Dsize--;
        return true;
    }
    
    /** Get the front item from the deque. */
    int getFront() {
        return head->next->val;
    }
    
    /** Get the last item from the deque. */
    int getRear() {
        return tail->pre->val;
    }
    
    /** Checks whether the circular deque is empty or not. */
    bool isEmpty() {
        return Dsize == 0;
    }
    
    /** Checks whether the circular deque is full or not. */
    bool isFull() {
        return Dsize == max_Dsize;
    }
};

/**
 * Your MyCircularDeque object will be instantiated and called as such:
 * MyCircularDeque* obj = new MyCircularDeque(k);
 * bool param_1 = obj->insertFront(value);
 * bool param_2 = obj->insertLast(value);
 * bool param_3 = obj->deleteFront();
 * bool param_4 = obj->deleteLast();
 * int param_5 = obj->getFront();
 * int param_6 = obj->getRear();
 * bool param_7 = obj->isEmpty();
 * bool param_8 = obj->isFull();
 */
//循环数组,这个高级,高效
class MyCircularDeque {
public:
    /** Initialize your data structure here. Set the size of the deque to be k. */
    MyCircularDeque(int k):size(k+1) {
        head = 0;
        tail = 0;
        arr = new int[size];
    }
    ~MyCircularDeque(){
        delete[] arr;
    }
    /** Adds an item at the front of Deque. Return true if the operation is successful. */
    bool insertFront(int value) {
        if(!isFull()){
            if(isEmpty()){
                arr[head] = value;
                tail = (tail+1)%size;
                // head = (head-1+size)%size;
            }else{
                head = (head-1+size)%size;
                arr[head] = value;
            }
            return true;
        }else
            return false;
    }
    
    /** Adds an item at the rear of Deque. Return true if the operation is successful. */
    bool insertLast(int value) {
        if(!isFull()){
            arr[tail] = value;
            tail = (tail+1)%size;
            return true;
        }else
            return false;
    }
    
    /** Deletes an item from the front of Deque. Return true if the operation is successful. */
    bool deleteFront() {
        if(!isEmpty()){
            head = (head+1)%size;
            return true;
        }else
            return false;
    }
    
    /** Deletes an item from the rear of Deque. Return true if the operation is successful. */
    bool deleteLast() {
        if(!isEmpty()){
            tail = (tail-1+size)%size;
            return true;
        }else
            return false;
    }
    
    /** Get the front item from the deque. */
    int getFront() {
        if(!isEmpty()){
            return arr[head];
        }else
            return -1;
    }
    
    /** Get the last item from the deque. */
    int getRear() {
        if(!isEmpty()){
            return arr[(tail-1+size)%size];
        }else
            return -1;
    }
    
    /** Checks whether the circular deque is empty or not. */
    bool isEmpty() {
        if(head == tail) 
            return true;
        else
            return false;
    }
    
    /** Checks whether the circular deque is full or not. */
    bool isFull() {
        if((tail+1)%size == head){
            // cout<<tail<<" "<<head<<endl;
            return true;
        }
        else 
            return false;
    }
private:
    int head;
    int tail;
    int size;
    int* arr;
};

/**
 * Your MyCircularDeque object will be instantiated and called as such:
 * MyCircularDeque* obj = new MyCircularDeque(k);
 * bool param_1 = obj->insertFront(value);
 * bool param_2 = obj->insertLast(value);
 * bool param_3 = obj->deleteFront();
 * bool param_4 = obj->deleteLast();
 * int param_5 = obj->getFront();
 * int param_6 = obj->getRear();
 * bool param_7 = obj->isEmpty();
 * bool param_8 = obj->isFull();
 */

困难题,摇了我吧。明天再看

滑动窗口最大值(LC:239)

LC:239——滑动窗口最大值

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值