数据结构与算法----栈和队列

137 阅读9分钟

一、栈

栈的特性就是先进后出,指针入口指向栈顶

例如:有一个袋子,每次都往里面放一个跟袋口差不多大的西瓜,第一个放进去的会在最下面,如果想拿出最底下的西瓜,只能先把上面的拿出去,才能拿到下面的

如上图所示,指针指向栈顶,只能读取栈顶元素,每次存放一个元素,栈内元素增长一,top指针向上增长一位

下面介绍常见的两种栈的设计方案:

顺序栈

与顺序表一样,顺序栈也是一个提前声明固定空间一个顺序表,通过top指针维持栈的特性

top指针默认索引为-1,即指向栈底;每次增加数据,只能通过push的方式压栈,top索引值+1, 到达设置最大值后不可在增加;每次删除数据,只能通过pop的方式来弹出顶层数据,索引值-1,索引值为-1后不能再减少

先定义一个顺序栈接结构

定义一个最大容量,存放一个MAXSize大小的数组,top为栈顶索引

#define MAXSIZE 1024  //定义最大容量
typedef struct OrderStack {
    char data[MAXSIZE];
    int top;
} LSOrderStack;

初始化

初始化的过程给栈顶索引赋值-1,标识默认指向栈底

LSOrderStack *stack = (LSOrderStack *)malloc(sizeof(LSOrderStack));
stack->top = -1;

push

push的时候传入栈结构和数据,如果栈满了则不push, 否则加入新元素,栈顶索引+1

_Bool push(LSOrderStack *stack, char c) {
    if (stack->top + 1 > MAXSIZE) return 0; //栈满了
    stack->top++;
    stack->data[stack->top] = c;
    return 1;
}

pop

pop的时候传入栈结构和数据,如果栈为空(索引为-1),则不pop, 否则置空栈顶元素,栈顶索引-1

void orderPop(LSOrderStack *stack) {
    if (stack->top == -1) return; //栈为空无需操作
    stack->data[stack->top] = 0;
    stack->top--;
}

链栈

与链表相似,链栈的相邻元素通过next指针有序连接,且没有大小显示,当然可以手动限制

链栈的栈顶其实就是链表的head节点,通过控制head的位置,来控制栈的push和pop

因此当栈为空的时候,栈顶指针top为NULL;当存在元素的时候栈顶指针一直指向最新的元素,而最新的元素的next则指向加入前的栈顶元素

如上图所示,每增加一个新的元素,栈顶top都会向上移动,且指针指向先前的栈顶元素,

先定义一个顺序栈接结构

定义一个最大容量,存放一个MAXSize大小的数组,top指向栈顶

typedef struct ListStack {
    char data;
    struct ListStack *next;
} LSListStack;

LSListStack *top = NULL; //指向栈顶元素

初始化

由于不需要特别的结构来维护链栈,所以不需要初始化,如果硬要拿出那就是给top置空

push

push的时候传入栈结构和数据,如果栈满了则不push, 否则加入新元素,且更新数据next指向, 更改栈顶指针

void listPush(char c) {
    LSListStack *next = (LSListStack *)malloc(sizeof(LSListStack));
    next->data = c;
    next->next = stack;
	top = next;
}

pop

pop的时候传入栈结构和数据,如果栈为空(top指针为空),则不pop, 否则置空栈顶元素,栈顶指针后移

LSListStack *listPop(LSListStack *stack) {
    if (!top) return stack; //栈为空
    LSListStack *p = stack;
    stack = stack->next;
    free(p);
    return stack;
}

二、队列

队列的特性就是先进先出,后进后出,分为队头和队尾, 队尾进,队头出

同栈结构一样,队列也有顺序队列和链队列

队列简易示意图如下所示:

顺序队列

与顺序表一样,顺序队列也是一个提前声明好的一个固定长度的顺序表,通过两个队列索引,队首(front)和队尾(rear)来维护

顺序表的队首和队尾默认都在初始位,每次队尾增加一项,则队尾索引值+1,减少则队首索引+1,因此可以看出顺序队列是从队首到队尾是按照索引从小到大的,如果队首和队尾指针指向同一个索引,那么此队列为空

假设原本入队和出队的索引值为0--2,那么经过入队或者出队后变化如下

因此如果入队和出队比较频繁的话,那么出队会造成数组前面的空闲空间很多,却无法使用的情况,实属浪费,因此引出环形结构队列。 本顺序栈也只讲解环形队列

环形队列

环形队列就是假设声明的顺序表数组是一个首尾相接的结构,每次操作通过检查索引位置是否超过两端,然后进行映射入数组即可实现,即取余法

环形结构的简易样式图如下所示:

那么接下一步一步实现队列的操作,从操作中一步一步解决环形队列问题

初始化队列

环形队列是逻辑上的一个环,需要我们在使用过程中来维护,即入队和出队过程来维护,因此初始化不需要,初始化只需要创建好结构,然后队首队尾指向初始节点即可

此处声明了顺序队列结构体,创建了队列结构,并把队列索引都指向了数组的最后一项,默认队列为空,

因此遍历队列的时候是从队首的下一个开始的,即front+1,而队尾元素则为队尾索引,即rear

#define MAXQUEUE 8

typedef struct OrderQueue {
    char data[MAXQUEUE];
    int near, front;  //尾巴进,头出
} LSOrderQueue; //声明顺序队列结构体

LSOrderQueue * initOrderQueue() {
    LSOrderQueue *queue = (LSOrderQueue *)malloc(sizeof(LSOrderQueue));
    queue->front = queue->near = MAXQUEUE-1; //初始化索引
    return queue;
}

入队

入队的过程,需要队尾元素自增,且队列满了不能继续入队

由于是环形队列,因此判断队列满需要在入队前判断队首队尾指针是否即将相遇(入队后索引相遇了),如果相遇则队满结束,否则队尾索引自增,通过取余法把索引映射到数组的对应位置入队(环形结构首尾相接,队首前面为空使用前面控件),以此来解决出队导致的空间浪费问题

_Bool orderQueueEnter(LSOrderQueue *queue, char c) {
    if (queue->near + 1 % MAXQUEUE == queue->front) return 0;//队列满了不能入队
    queue->near = ++queue->near % MAXQUEUE;// 取余法入队到相应的位置
    queue->data[queue->near] = c; //赋值
    return 1;
}

出队

出队的过程,需要队首元素自增,且队列为空不能出队

由于是环形结构,判断队列为空的时候判断两个指针是否已经相遇了,如果相遇,则队列为空,结束,否则队首元素自增,通过取余法把索引映射到数组的对应位置出队,其实就是剔除到队首->队尾这边区间之外

void orderQueueLeave(LSOrderQueue *queue) {
    if (queue->front == queue->near) return; //队列为空
    queue->front = ++queue->front % MAXQUEUE; 取余法出队到相应的位置
}

链队列

链队列和链表一样,为一个非连续的链式结构,不会存在空间浪费问题,因此不用直接以环式结构来维护,默认只需要额外增加两个指针,队首front和队尾rear,类似于顺序队列,只要维护好front和rear即可

然而此过程需要新增front和rear两个指针,多了一个结构,显得不够简洁,借鉴顺序队列的环式结构,因此又衍生除了第二种方案,环形队列结构

方案一:线性链队列

1.初始化

只需要初始化队列结构即可维护链式结构,默认两个指针指向

typedef struct endpointListQueue {
    LSListNode *front, *near;
}LSPointListQueue; //队列基础指针

2.入队

入队前,先判断队伍是否存在元素,如果不存在,则需要将队首、队尾指针指向新元素,否则链表加入新元素,尾指针指向新节点

(2).非第一次入队,队列链表插入新元素,队尾指针指向新节点

void pointEnter(LSPointListQueue *queue, char c) {
    LSListNode *node = (LSListNode *)malloc(sizeof(LSListNode));
    node->data = c;
    
    //不存在队首默认指向第一个
    if (!queue->front) {
        queue->near = queue->front = node;
        return;
    }
    
    //队尾指向新增元素
    queue->near->next = node;
    queue->near = node;
}

3.出队

出队前先判断队伍是否为空,为空结束,否则队首指向的元素移除,且队首元素后移一位即可

void pointLeave(LSPointListQueue *queue) {
    if (!queue->front) return; //队为空不需要出队
    LSListNode *front = queue->front;
    queue->front = queue->front->next; //出队,队伍指针后移
    free(front);
}

方案二:环形链队列(推荐)

环形链队列由节点本身,依照头尾指针特点,参考环状结构衍生出来的结构,其默认只有一个初节点来维护,示意图如下:

由上图结合默认只有一个初节点来维护可以得到两个信息:

1.一个节点时队首队尾在一起;

2.队首节点在前,队尾节点灾后,最后队尾节点指向队首节点,形成环状,固定节点就是队尾节点rear, 固定节点next就是队首节点front,因此通过默认固定节点,加上其next可以同时获取队尾和队首节点;

由此条件可以推测入队出队操作是否可行: 入队时,链表可以随时入队,一个节点时next指向自己,多个节点时,新节点next指向队首节点,固定节点next指向新节点,固定节点指向新节点,实现了一次入队;出队时,队伍为空则不出队伍,不为空固定节点next指向队首节点的next,然后释放原队首节点即可

1.初始化

因为只需要一个固定节点来保证索引,所以不需要刻意初始化,因此只需要申明默认节点数据结构即可

typedef struct listNode {
    char data;
    struct listNode *next;
} LSListNode; //节点基础数据结构

2.入队

入队时,如果没有固定节点(其实就是尾结点),则节点指向自己

如果有固定节点,则取出队首节点,固定节点指向新节点,新节点的next指向取出的队首节点即可

LSListNode *listQueueEnter(LSListNode *queue, char c) {
    LSListNode *node = (LSListNode *)malloc(sizeof(LSListNode));
    node->data = c;
    if (queue) {
        //多个节点首尾相接
        LSListNode *front = queue->next;
        queue->next = node;
        node->next = front;
    }else {
        queue = node->next = node; //一个节点尾指针指向自己
    }
    return queue;
}

3.出队

出队时,如果队伍不存在,直接结束,如果队首节点(固定节点next)是自己,那么直接释放,置空固定节点,否则固定节点的next指向队首节点next,然后释放队首节点即可

LSListNode *listQueueLeave(LSListNode *queue) {
    if (!queue) return NULL;//没有节点
    
    if (queue->next == queue) {
        free(queue); //只有一个
        queue = NULL;
    }else {
        LSListNode *front = queue->next->next; //指向头指针的下一个,两个和多个一样
        free(queue->next);
        queue->next = front;
    }
    return queue;
}

总结

1.栈分为顺序栈和链栈,先进后出
2.队列分为顺序队列和链式队列,顺序队列和链式队列有线状和环状,并且他们的实现方式有所不同,且方式为先进先出