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);
*/