数据结构与算法之美--队列学习

如何理解“队列”

  • 队列特征属性
    • 队列种类
      • 顺序队列
      • 链式队列
      • 循环队列
      • 阻塞队列
      • 并发队列

队列特征属性

队列的显著特征为先进先出 ,类似排队买票,先来的人先买,后来的人只能排在队尾,先到先得。队列跟栈很相似,支持的操作有限,基本操作有入队 enqueue 和出队dequeue()。队列和栈一样,也是一种操作受限的线性表数据结构。

队列种类

具有额外特性的队列,比如 循环队列阻塞队列并发队列。在很对偏底层的系统开发中起着关键作用。比如linux 环形缓存 就用到了循环并发队列。

顺序队列

用数组实现的队列叫作顺序队列,用链表实现的队列叫作链式队列
下面是用数组实现的顺序队列:
arr_queue.h


#ifndef ARR_QUEUE_
#define ARR_QUEUE_

#include "stdbool.h"

typedef int ElementType;
typedef struct _arr_queue {
    ElementType arr[100];
    ElementType capacity;
    ElementType head; //标记队头索引
    ElementType tail; //标记队尾索引
}arrQueue;

void CreateArrQueue(arrQueue *aqueue);
bool enqueue(arrQueue *aqueue, ElementType date);
ElementType dequeue(arrQueue *arrqueue);

#endif
#include <stdio.h>
#include <stdlib.h>
#include "arr_queue.h"
/*
@parameter
*/
void CreateArrQueue(arrQueue *aqueue)
{
    aqueue->capacity = sizeof(aqueue->arr) / sizeof(ElementType);
    aqueue->head = 0;
    aqueue->tail = 0;
}
/*
@func : insert a date to tail of queue
@parameter1: address of queue
@parameter1: the date insert to tail of queue
@return:  true: enqueue success false: enqueue failed
*/
bool enqueue(arrQueue *arrQueue, ElementType date)
{
    if (!arrQueue) {
        perror("arrQueue is NULL.");
        exit(1);
    }
    /*判断队列是否满了*/
    if(arrQueue->tail == arrQueue->capacity) {
        printf("Queue is full.\n");
        return false;
    }
    arrQueue->arr[arrQueue->tail] = date;
    arrQueue->tail++;
    
    
    return true;
}
/*
@func : delete the head of queue
*/
ElementType dequeue(arrQueue *arrQueue)
{
    int val = 0;
    if (!arrQueue) {
        perror("arrQueue is NULL.");
        exit(1);
    }
    /*判断队列是否为空*/
    if (arrQueue->head == arrQueue->tail) {
        printf("Queue is empty.\n");
        return false;
    }
    val = arrQueue->arr[arrQueue->head];
    arrQueue->head++;
    
    return val;
}

int main()
{
    int i;
    //test code
    arrQueue aq;
    CreateArrQueue(&aq);
    enqueue(&aq, 10);
    enqueue(&aq, 20);
    printf("val --->%d\n", dequeue(&aq));
    printf("val --->%d\n", dequeue(&aq));
    printf("val --->%d\n", dequeue(&aq));
    return 0;
}

这样实现有个缺陷, 当tail 移到数组的最右边,即使数组中还有空闲的空间,也无法再进行入队操作,造成空间浪费。我们可以通过数据搬移来解决这个问题。在入队时集中触发一次搬移操作,出队函数dequeue()保持不变,修改入队函数enqueue()。

bool optimize_enqueue(arrQueue *aqueue, ElementType date)
{
    if (!aqueue) {
        perror("aqueue is NULL.");
        exit(1);
    }
    if(aqueue->tail == aqueue->capacity) {
        if (aqueue->head == 0) {
            printf("Don't have useful space.\n");
            return false;
        }
        int i;
        int head = aqueue->head;
        /*数据搬移*/
        for(i = aqueue->head; i < aqueue->tail; ++i) {
            aqueue->arr[i-head] = aqueue->arr[i];
        }
        /*更新tail 索引*/
        aqueue->tail -= aqueue->head;
    }
    aqueue->arr[aqueue->tail] = date;
    aqueue->tail++;
        
    return true;
}

链式队列

链式队列的实现,同样需要两个指针:head指针和tail指针,他们分别指向链表的第一个节点和最后一个节点。入队时,tail->next = new_node,tail = tail->next;出队时,head = head->next。具体实现如下:
linklist_queue.h

#ifndef LINKLIST_QUEUE_
#define LINKLIST_QUEUE_

#include "stdbool.h"

typedef int ElementType;
typedef struct _node {
    ElementType date;
    struct _node* next;
}q_node;

typedef struct _link_queue {
    q_node *head;
    q_node *tail;
}queue;

bool initLinklistQueue(queue *queue);
bool enqueue(queue* head, int val);
ElementType dequeue(queue* head);

#endif

linklist_queue.c

#include <stdio.h>
#include <stdlib.h>
#include "linklist_queue.h"
/*
@func: creat a linklist queue
*/
bool initLinklistQueue(queue *queue)
{
    if (!queue) {
        perror("queue is NULL.");
        exit(1);
    }
    queue->tail = (q_node *)malloc(sizeof(q_node));
    if (!queue->head) {
        perror("malloc fialed.");
        exit(1);
    }
    queue->head = queue->tail;
    queue->head->next = NULL;
    
    return true;
}
/*
@func : insert a date to linklist queue
*/
bool enqueue(queue* h_queue, int val)
{
    if (!h_queue) {
        perror("h_queue is NULL.");
        exit(1);
    }
    q_node* node = (q_node *)malloc(sizeof(q_node));
    if (!node) {
        perror("malloc failed.");
        exit(1);
    }
    node->date = val;
    h_queue->tail->next = node;
    h_queue->tail = h_queue->tail->next;
    
    return true;
}
/*
@func : delete a date from head of linklist queue
*/
ElementType dequeue(queue* h_queue)
{
    int val = 0;
    if (!h_queue) {
        perror("queue is empty.");
        exit(1);
    }
    h_queue->head = h_queue->head->next;
    if (h_queue->head) {
        val = h_queue->head->date;
        free(h_queue->head);
        h_queue->head = NULL;
        return val;
    }else {
        printf("linklist queue is empty...\n");
        exit(1);
    }
}

int main()
{
    /*test code*/
    queue que;
    initLinklistQueue(&que);
    enqueue(&que, 100);
    printf("val ---> %d\n", dequeue(&que));
    return 0;
}

循环队列

在用数组实现队列中,当tail = n时,会有数据搬移操作,入队性能会受到影响,我们可以看看循环队列的解决思路。

循环队列,顾名思义就是一个环,将数组收尾相连,要写好循环队列,最关键的应该是确定好队空队满的判定条件

循环队列队空判断条件仍然为head = tail,队满的判断条件为 (tail + 1) % n = head

下面是用数组实现循环队列实例。
robin_queue.h

#ifndef ROBIN_QUEUE_
#define ROBIN_QUEUE_

#include "stdbool.h"

typedef int ElementType;
typedef struct _robin_queue {
    ElementType arr[100];
    ElementType capacity;
    ElementType head; //标记队头索引
    ElementType tail; //标记队尾索引
}robinQueue;

void initRobinQueue(robinQueue *aqueue);
bool enqueue(robinQueue *aqueue, ElementType date);
ElementType dequeue(robinQueue *robinqueue);

#endif

robin_queue.c

#include <stdio.h>
#include <stdlib.h>
#include "robin_queue.h"
/*
@parameter
*/
void initRobinQueue(robinQueue *aqueue)
{
    aqueue->capacity = sizeof(aqueue->arr) / sizeof(ElementType);
    aqueue->head = 0;
    aqueue->tail = 0;
}
/*
@func : insert a date to tail of queue
@parameter1: address of queue
@parameter1: the date insert to tail of queue
@return:  true: enqueue success false: enqueue failed
*/
bool enqueue(robinQueue *aqueue, ElementType date)
{
    if (!aqueue) {
        perror("aqueue is NULL.");
        exit(1);
    }
    /*判断队满*/
    if ((aqueue->tail + 1)%aqueue->capacity == aqueue->head) {
        printf("**queue is full**\n");
        return false;
    }
    aqueue->arr[aqueue->tail] = date;
    printf("date[%d]: %d\n", aqueue->tail, aqueue->arr[aqueue->tail]);
    aqueue->tail = (aqueue->tail + 1) % aqueue->capacity;
    return true;
}

/*
@func : delete the head of queue
*/
ElementType dequeue(robinQueue *robinqueue)
{
    if (!robinqueue) {
        perror("robinQueue is empty.");
        exit(1);
    }
    /*判断队空*/
    if (robinqueue->head == robinqueue->tail) {
        printf("robinQueue is empty.\n");
        exit(1);
    }
    ElementType val = robinqueue->arr[robinqueue->head];
    robinqueue->head = (robinqueue->head + 1) % robinqueue->capacity;
    return val;
}
int main()
{
    /*test code*/
    robinQueue rqueue;
    int i;
    initRobinQueue(&rqueue);
    for(i = 0; i < 100; i++) {
        enqueue(&rqueue, i);
    }
    printf("val ---> %d\n", dequeue(&rqueue));
    printf("val ---> %d\n", dequeue(&rqueue));
    printf("val ---> %d\n", dequeue(&rqueue));
    enqueue(&rqueue, 17);
    enqueue(&rqueue, 18);
    
    
}

阻塞队列

在队列的基础上添加阻塞操作,在队列为空的时候,从队头取数据会被阻塞,此时队列中没有数据可取,知道队列中有数据可取才能返回;如果队列满了,那么插入数据的操作将会被阻塞,知道队列中有空闲的空间才能继续插入数据,然后再返回。

是否有种似曾相似的感觉,上述描述的实际上就是一个“生产者-消费者模型”!我们可以用阻塞队列实现一个“生产者-消费者模型”。代码后续补上。

并发队列

在多线程的情况下,会有多个线程同时操作队列,会存在线程安全问题,并发队列是一种线程安全队列。最简单最直接的实现方式是直接在enqueue()、dequeue()方法上枷锁,同一时刻只允许一个存或者取操作。

你可能感兴趣的:(数据结构与算法之美学习笔记)