栈和队列的实现与转换 & 循环队列

Stack

Introduction

栈(stack)是一个特殊的线性表(linear list)线性表_百度百科 (baidu.com),是仅能在表尾进行插入和删除操作的线性表。因此它有后进先出的性质。
在这里插入图片描述

Structure

Stack可以用链式结构实现,也可以用顺序结构,这里我们用顺序结构来实现。

/* 栈的结构 */
typedef int StackDataType;
typedef struct Stack {
	StackDataType* array; //指向数据的数组
	int capacity; //容量
	int top; //记录栈顶元素
}Stack;

Basic Interfaces

/* 初始化 */
void StackInit(Stack* ps) {
	assert(ps);
	StackDataType* tmp = (StackDataType*)malloc(sizeof(StackDataType) * 4);
	if (tmp == NULL) {
		perror("StackInit::malloc failed\n");
		exit(-1);
	}
	ps->array = tmp;
	ps->capacity = 4;
	ps->top = 0;
}

/* 销毁栈 */
void StackDestroy(Stack* ps) {
	assert(ps);
	free(ps->array);
	ps->capacity = 0;
	ps->top = 0;
}

/* 入栈 */
void StackPush(Stack* ps, StackDataType x){
	assert(ps);
	//检查容量
	if (ps->capacity == ps->top) {
		StackDataType* tmp = (StackDataType*)realloc(ps->array, sizeof(StackDataType) * ps->capacity * 2);
		if (tmp == NULL) {
			perror("StackPush::realloc failed\n");
			exit(-1);
		}
		ps->array = tmp;
		ps->capacity *= 2;
	}
	//尾插
	ps->array[ps->top] = x;
	ps->top++;
}

/* 栈数据可视化 */
void StackPrint(Stack* ps) {
	assert(ps);
	assert(ps->top > 0);
	int i = 0;
	for (i = 0; i < ps->top; i++) {
		printf("%d ", ps->array[i]);
	}
	printf("\n");
}

/* 出栈 */
void StackPop(Stack* ps) {
	assert(ps);
	assert(ps->top > 0);

	ps->top--;
}

/* 判断栈是否数据为空 */
bool StackisEmpty(Stack* ps) {
	assert(ps);
	return ps->top == 0;
}

/* 获取栈内元素个数 */
size_t StackSize(Stack* ps) {
	assert(ps);
	return ps->top;
}
/*
	注:为什么要设计StackSize(Stack* ps)接口?
		因为在StackInit时,top初始化可以为0或-1。
		1.当top初始为0时,指代的是栈顶元素的下一个位置;
		2.当top初始为-1时,指代的时栈顶元素本身。
		用户不知道你设计的到底是第1种还是第2种,如果直接调用 stack.top 会产生歧义。
*/

/* 获取栈顶元素 */
StackDataType StackTop(Stack* ps) {
	assert(ps);
	return ps->array[ps->top - 1];
}

Usage

int main() {
	Stack st;
	StackInit(&st);

	StackPush(&st, 1);
	StackPush(&st, 2);
	StackPush(&st, 3);
	StackPush(&st, 4);
	StackPush(&st, 5);

	StackPrint(&st);

	printf("%zd\n", StackSize(&st));
	StackPop(&st);

	printf("%d\n", StackTop(&st));

	return 0;
}

Queue

Introduction

队列(queue)和栈一样是特殊的线性表,只能在队头(front)进行删除操作,在队尾(rear)进行插入操作。因此它有先进先出的性质。

在这里插入图片描述

Structure

显然,队列更适合用链式结构来实现。

因为如果是数组,头删、头插都需要进行挪动数据的操作,时间复杂度为O(N),因此无论让数组的头部作为队头还是队尾,总会有一个操作需要O(N)的复杂度;

而链表虽然在实现上,尾插的时候需要找尾,也是O(N),但我们会给队列的结构一个rear指针指向尾结点,就不用去找尾了。

再者,数组队列存在假溢出的缺陷。假溢出_百度百科 (baidu.com)

typedef int QueueDataType;

/* 队列的结点 */
typedef struct QueueNode {
	QueueDataType value;
	struct QueueNode* next;
}QNode;

/* 队列结构 */
typedef struct Queue {
	QNode* front; //队头结点
	QNode* rear; //队尾结点
	int size; //记录结点个数
}Queue;

【注意】队列的结构中,最好要有一个size,用来记录队列结点的个数,否则,想要实现QueueSize()接口就必须遍历队列,导致时间复杂度为O(N);而如果有size,只需要在Push和Pop的时候分别++和–,最后QueueSize()接口直接返回size即可。

我一开始的想法是:弄个全局变量size来记录,但后来发现这样绝对不行,因为如果有多个队列就惨了,全局变量是所有结构都共享的,一旦Queue queue_1进行Push操作,它的size++,Queue queue_2的size也会跟着++,但queue_2显然没有进行Push操作!

Basic Interfaces

/* 初始化队列 */
void QInit(Queue* pq) {
	assert(pq);

	pq->front = NULL;
	pq->rear = NULL;
	pq->size = 0;

}

/* 销毁队列 */
void QDestroy(Queue* pq) {
	assert(pq);

	QNode* del = pq->front;
	while (del) {
		QNode* next = del->next;
		free(del);
		del = next;
	}
	
	pq->front = pq->rear = NULL; //必须置空
	pq->size = 0;
}

/* 入队 */
void QPush(Queue* pq, QueueDataType x) {
	assert(pq);

	QNode* newNode = (QNode*)malloc(sizeof(QNode));
	if (NULL == newNode) {
		perror("QPush::malloc failed\n");
		exit(-1);
	}
	newNode->value = x;
	newNode->next = NULL;

	if (pq->front == NULL) {
		pq->front = pq->rear = newNode;
	}
	else {
		pq->rear->next = newNode;
		pq->rear = newNode;
	}
	pq->size++;
}

/* 出队 */
void QPop(Queue* pq) {
	assert(pq);

	assert(!QisEmpty(pq));

	if (NULL == pq->front->next) { //Pop最后1个结点时,顺便把rear也置空
		free(pq->front);
		pq->front = pq->rear = NULL;
	}
	else {
		QNode* newFront = pq->front->next;
		free(pq->front);
		pq->front = newFront;
	}
	

	pq->size--;
}

/* 队列是否为空 */
bool QisEmpty(Queue* pq) {
	assert(pq);
	return NULL == pq->front && NULL == pq->rear;
}

/* 获取队头数据 */
QueueDataType QFront(Queue* pq) {
	assert(pq);
	assert(!QisEmpty(pq));
	return pq->front->value;
}

/* 获取队尾数据 */
QueueDataType QRear(Queue* pq) {
	assert(pq);
	assert(!QisEmpty(pq));
	return pq->rear->value;
}

/* 获取队列数据个数 */
size_t QSize(Queue* pq) {
	assert(pq);
	return (size_t)pq->size;
}

Usage

void testQueue() {
	Queue q;
	QInit(&q);

	int i = 0;
	for (i = 0; i < 10; i++) {// 入队顺序:0 1 2 3 4 5 6 7 8 9
		QPush(&q, i);
	}
	
	while (!QisEmpty(&q)) {// 出队顺序:0 1 2 3 4 5 6 7 8 9 (先进先出)
		printf("%d ", QFront(&q));
		QPop(&q);
	}
	printf("\n");

	QDestroy(&q);// 使用完之后必须销毁,否则会造成内存泄漏
}

Transformation between stack and queue

一定程度上,队列和栈的性质很相似,它们之间是可以互相转换。(不考虑时间效率)

懂个思路即可,实际工程中,不会这么大费周章的。

implement Stack with 2 queues

在leetcode上有题目:225. 用队列实现栈 - 力扣(LeetCode)

My Method

2个队列,永远保持1个队列是空的。

入栈:Push到非空队列中;

出栈:非空队列的前size-1个结点Push到空队列里,剩下一个结点的数据返回并Pop。

在这里插入图片描述

/* 栈的结构 */
typedef struct {
    Queue q1; //两个Queue的结构
    Queue q2;
} MyStack;

/* 创建栈 */
MyStack* myStackCreate() {
    MyStack* st =(MyStack*) malloc(sizeof(MyStack));
    QInit(&st->q1);
    QInit(&st->q2);
    return st;
}

/* 入栈 */
void myStackPush(MyStack* obj, int x) {
    //数据进入非空的队列
    if(!QisEmpty(&obj->q1)){
        QPush(&obj->q1,x);
    }else{
        QPush(&obj->q2,x);
    }
}

/* 出栈 */
int myStackPop(MyStack* obj) {
    /* 非空队列的前size-1个数据进入空队列,
       返回非空队列尾结点数据并Pop. */
    
    Queue* emptyQ = &obj->q1;
    Queue* noEmptyQ = &obj->q2;
    if(!QisEmpty(emptyQ)){
        emptyQ = &obj->q2;
        noEmptyQ = &obj->q1;
    }
    
    while(QSize(noEmptyQ) > 1){
        QPush(emptyQ,QFront(noEmptyQ));
        QPop(noEmptyQ);
    }
    int popData = QRear(noEmptyQ);//返回非空队列的尾结点
    QPop(noEmptyQ);

    return popData;
}

/* 获取栈顶元素数据 */
int myStackTop(MyStack* obj) {
    Queue* emptyQ = &obj->q1;
    Queue* noEmptyQ = &obj->q2;
    if(!QisEmpty(emptyQ)){
        emptyQ = &obj->q2;
        noEmptyQ = &obj->q1;
    }
    return QRear(noEmptyQ);//非空队列尾结点即栈顶
}

/* 判断栈是否空 */
bool myStackEmpty(MyStack* obj) {
    return QisEmpty(&obj->q1) && QisEmpty(&obj->q2);
}

void myStackFree(MyStack* obj) {
    QDestroy(&obj->q1);
    QDestroy(&obj->q2);
    free(obj);
}
/**
 * Your MyStack struct will be instantiated and called as such:
 * MyStack* obj = myStackCreate();
 * myStackPush(obj, x);
 
 * int param_2 = myStackPop(obj);
 
 * int param_3 = myStackTop(obj);
 
 * bool param_4 = myStackEmpty(obj);
 
 * myStackFree(obj);
*/

implement Queue with 2 stacks

在leetcode上有题目:232. 用栈实现队列 - 力扣(LeetCode)

My Method

2个栈,一个存放Push进来的数据,一个存放将要出队的数据。

入队:直接Push到PushSt中;

出队:先判断PopSt是否为空,

​ 若不为空,则Pop掉PopSt的栈顶;

​ 若为空,则将PushSt中的数据Push到PopSt中,并Pop掉PopSt的栈顶。

如图所示:

在这里插入图片描述

typedef struct {
    Stack PushSt;
    Stack PopSt;
} MyQueue;


MyQueue* myQueueCreate() {
    MyQueue* q = (MyQueue*)malloc(sizeof(MyQueue));
    StackInit(&q->PushSt);
    StackInit(&q->PopSt);
    return q;
}

void myQueuePush(MyQueue* obj, int x) {
    StackPush(&obj->PushSt,x);
}

int myQueuePop(MyQueue* obj) {
    int topData = myQueuePeek(obj);
    StackPop(&obj->PopSt);
    return topData;
}

int myQueuePeek(MyQueue* obj) {
    if(StackisEmpty(&obj->PopSt)){ //如果Pop栈为空,将Push栈的数据Push到Pop栈
        while(StackSize(&obj->PushSt)){
            StackPush(&obj->PopSt,StackTop(&obj->PushSt));
            StackPop(&obj->PushSt);
        }
    }
    return StackTop(&obj->PopSt);
}

bool myQueueEmpty(MyQueue* obj) {
    return StackisEmpty(&obj->PushSt) && StackisEmpty(&obj->PopSt);
}

void myQueueFree(MyQueue* obj) {
    StackDestroy(&obj->PushSt);
    StackDestroy(&obj->PopSt);
    free(obj);
}

Tips:上面两个代码,都没有进行assert,不是很严谨,但在力扣上,能过就行了。实际工程中需要断言一下。(虽然但是,实际工程也不会大费周章用队列实现栈或用栈实现队列)

Circular Queue

前面我们讨论的队列都是单向线性的,现在我们来讨论首尾相连的队列——循环队列。

其实,循环队列的结构首先解决的就是 数组队列的假溢出的缺陷:

​ 用数组实现的单向队列,由于Pop只能在队头,Push只能在队尾,那么Pop掉的数据就用不上了。在这里插入图片描述

​ 如果将这样的数组队列首尾相连,形成循环队列,就可以重新使用到前面空出来的空间了。

那么如何设计一个循环队列?622. 设计循环队列 - 力扣(LeetCode)

循环队列的逻辑视图:

在这里插入图片描述

而在实际的物理存储空间中,数组的头和尾并没有相连:
在这里插入图片描述

Empty && Full

如何判断队列是否为空?

​ 只需要判断是否front == rear即可。

如何判断队列是否为满?

​ 在这里,就不得不考虑清楚了。如果我们的数组所有空间都存储数据,那么判断队列是否为满,似乎也得是front == rear,这就和判断空冲突了。如图所示

在这里插入图片描述

​ 为了解决这个冲突,我们可以多开1块空间来辅助判断。如图所示

在这里插入图片描述

​ 这样,判空和判满两个条件就不冲突了。那么,为了将队列形成循环,判满的条件就是:(rear+1) % (k+1) == front(k是有效数据个数),这个条件翻译成人话就是rear处于front的前一个位置,以及front在头rear在尾的情况。(取模操作是关键。当然,不取模,单独加个条件判断也可以)

Push && Pop

增加数据和删除数据也都需要考虑让队列形成循环。例如,增加数据到rear == k的时候,就需要让rear返回到数组首元素,删除数据是,front也一样要考虑回到数组头部。

完整题解代码:

typedef struct {
    int* a;
    int front;
    int rear;
    int k;
} MyCircularQueue;

//构造器,设置队列长度为 k
MyCircularQueue* myCircularQueueCreate(int k) {
    MyCircularQueue* obj = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
    obj->a = (int*)malloc(sizeof(int)*(k + 1));//多开1个空间
    obj->front = obj->rear = 0;
    obj->k = k;
    return obj;
}

//检查循环队列是否为空
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
    assert(obj);
    return obj->front == obj->rear;
}

//检查循环队列是否已满
bool myCircularQueueIsFull(MyCircularQueue* obj) {
    assert(obj);
    return ((obj->rear + 1) % (obj->k + 1)) == obj->front;
}

//向循环队列插入一个元素。如果成功插入则返回真
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
    assert(obj);
    if(myCircularQueueIsFull(obj)){ //如果队列已满,则插入失败
        return false;
    }

    obj->a[obj->rear++] = value;
    if(obj->rear == obj->k+1){
        obj->rear = 0;
    }

    return true;
}

//从循环队列中删除一个元素。如果成功删除则返回真
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
    assert(obj);
    if(myCircularQueueIsEmpty(obj)){ //如果队列为空,则删除失败
        return false;
    }
    obj->front ++;
    if(obj->front == obj->k+1){
        obj->front = 0;
    }
    return true;
}

//从队首获取元素。如果队列为空,返回 -1
int myCircularQueueFront(MyCircularQueue* obj) {
    assert(obj);
    if(myCircularQueueIsEmpty(obj)){
        return -1;
    }
    return obj->a[obj->front];
}

//获取队尾元素。如果队列为空,返回 -1
int myCircularQueueRear(MyCircularQueue* obj) {
    assert(obj);
    if(myCircularQueueIsEmpty(obj)){
        return -1;
    }
    return obj->a[obj->rear == 0 ? obj->k : obj->rear-1];
}

//释放空间
void myCircularQueueFree(MyCircularQueue* obj) {
    free(obj->a);
    free(obj);
}

/**
 * Your MyCircularQueue struct will be instantiated and called as such:
 * MyCircularQueue* obj = myCircularQueueCreate(k);
 * bool param_1 = myCircularQueueEnQueue(obj, value);
 
 * bool param_2 = myCircularQueueDeQueue(obj);
 
 * int param_3 = myCircularQueueFront(obj);
 
 * int param_4 = myCircularQueueRear(obj);
 
 * bool param_5 = myCircularQueueIsEmpty(obj);
 
 * bool param_6 = myCircularQueueIsFull(obj);
 
 * myCircularQueueFree(obj);
*/
  • 19
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 22
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值