目录


引言
共享栈和双端队列是除了栈和队列以外,限定性的数据结构。接下来一起来从算法思想上理解它们。
共享栈
共享栈是将两个栈(Typedef struct {}Stack1,Stack2 )存放在同一段连续数组空间中的结构。数组的两端分别作为两个栈的栈底:栈1从数组的下标0处,向数组中部延伸,栈2从数组的下标MAXSIZE-1处,相对逆向延伸。这两个栈的栈顶指针在各自的相反方向移动,当两个指针相邻时表示栈满,即共享栈的终止条件:top1 +1 == top2。共享栈存在的意义在于,提高数组的空间利用率,避免单个栈空间固定造成的浪费。
共享栈通过top1++/++top1 和top2++/++top2实现相向移动。
算法思想:
共享栈是指两个栈共用同一块连续存储空间(数组)。通常将数组的两端分别作为两个栈的栈底:
-
栈1 的栈底在数组下标
0处,栈顶指针top1初始为-1,入栈时top1++,出栈时top1--。 -
栈2 的栈底在数组下标
MAXSIZE-1处,栈顶指针top2初始为MAXSIZE,入栈时top2--,出栈时top2++。
当 top1 + 1 == top2 时,说明栈满,无法再插入新元素。这种结构能够最大限度地利用数组空间,避免了一个栈空闲、另一个栈溢出的情况。
C/C++实现,关键之处给出注释:
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#define MAXSIZE 100 // 共享栈的总容量
// 共享栈结构体
typedef struct {
int dataMAXSIZE; // 存储元素的数组
int top1; // 栈1的栈顶指针(指向栈顶元素)
int top2; // 栈2的栈顶指针(指向栈顶元素)
} SharedStack;
// 初始化共享栈
void initSharedStack(SharedStack *s) {
s->top1 = -1; // 栈1初始栈顶为-1
s->top2 = MAXSIZE; // 栈2初始栈顶为MAXSIZE(指向数组最后一个元素的下一个位置)
}
// 判断栈1是否为空
bool isEmpty1(SharedStack *s) {
return s->top1 == -1;
}
// 判断栈2是否为空
bool isEmpty2(SharedStack *s) {
return s->top2 == MAXSIZE;
}
// 判断共享栈是否已满(两个栈顶相邻)
bool isFull(SharedStack *s) {
return s->top1 + 1 == s->top2;
}
// 栈1入栈
bool push1(SharedStack *s, int value) {
if (isFull(s)) {
printf("共享栈已满,无法入栈!\n");
return false;
}
s->data++s-\>top1 = value; // top1先加1,再赋值
return true;
}
// 栈2入栈
bool push2(SharedStack *s, int value) {
if (isFull(s)) {
printf("共享栈已满,无法入栈!\n");
return false;
}
s->data--s-\>top2 = value; // top2先减1,再赋值
return true;
}
// 栈1出栈
bool pop1(SharedStack *s, int *value) {
if (isEmpty1(s)) {
printf("栈1为空,无法出栈!\n");
return false;
}
*value = s->datas-\>top1--; // 取出栈顶元素,然后top1减1
return true;
}
// 栈2出栈
bool pop2(SharedStack *s, int *value) {
if (isEmpty2(s)) {
printf("栈2为空,无法出栈!\n");
return false;
}
*value = s->datas-\>top2++; // 取出栈顶元素,然后top2加1
return true;
}
// 获取栈1栈顶元素(不出栈)
bool getTop1(SharedStack *s, int *value) {
if (isEmpty1(s)) return false;
*value = s->datas-\>top1;
return true;
}
// 获取栈2栈顶元素(不出栈)
bool getTop2(SharedStack *s, int *value) {
if (isEmpty2(s)) return false;
*value = s->datas-\>top2;
return true;
}
// 简单测试
int main() {
SharedStack ss;
initSharedStack(&ss);
push1(&ss, 10);
push1(&ss, 20);
push2(&ss, 100);
push2(&ss, 200);
int val;
pop1(&ss, &val);
printf("栈1出栈: %d\n", val); // 20
pop2(&ss, &val);
printf("栈2出栈: %d\n", val); // 200
return 0;
}
双端队列
双端队列(Deque,Double Ended Queue)是一种允许在两端进行插入和删除操作的线性表。其特性为既能在队列头部进行插入和删除操作,也能在队尾进行插入和删除操作。
权限探究:
队列头部 插入和删除权限 || 队列尾部 插入和删除权限,栈
队列头部 插入和删除操作&& 队列尾部 插入和删除操作,双端队列
队列头部插入和删除 && 队列尾部 插入 || 队列头部插入 && 队列尾部 插入和删除,共享栈
队列头部插入 && 队列尾部 删除 || 队列头部删除 && 队列尾部 插入,队列
队列头部&&队列尾部 删除/插入,NULL
通常用循环数组或者双向链表实现,采用"牺牲一个单元"的方法区分队空和队满:队空条件,front == rear; 队满条件,(rear+1)%MAXSIZAE == front.双端队列存在的的意义在于,提供更灵活的数据操作方式,适用于滑动窗口、双端优先调度等场景。
双端队列通过(front++)%MAXSIZE/(front--)%MAXSIZE/(++front)%MAXSIZE/(--front)%MAXSIZE实现相向移动。
算法实现:
双端队列(Deque)允许在队列的两端进行插入和删除操作。通常用循环数组 或双向链表 实现。此处采用循环数组方式:
-
设定两个指针
front和rear,front指向队头元素的位置,rear指向队尾元素的下一个位置(或指向队尾元素,取决于实现习惯)。 -
为了区分队空和队满,通常牺牲一个数组单元 ,即当
(rear + 1) % MAXSIZE == front时认为队满;当front == rear时认为队空。 -
入队操作:从队头入队时,
front向前移动(front = (front - 1 + MAXSIZE) % MAXSIZE);从队尾入队时,rear向后移动(rear = (rear + 1) % MAXSIZE)。 -
出队操作:从队头出队时,
front向后移动;从队尾出队时,rear向前移动。
这种结构可以在 O(1) 时间内完成两端操作,且空间利用率高。
C/C++实现,关键之处给出注释:
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#define MAXSIZE 10 // 双端队列最大容量(实际可用 MAXSIZE-1 个元素)
// 双端队列结构体(循环数组)
typedef struct {
int dataMAXSIZE;
int front; // 指向队头元素的位置
int rear; // 指向队尾元素的下一个位置
} Deque;
// 初始化双端队列
void initDeque(Deque *dq) {
dq->front = 0;
dq->rear = 0;
}
// 判断队列是否为空
bool isEmpty(Deque *dq) {
return dq->front == dq->rear;
}
// 判断队列是否已满(牺牲一个单元)
bool isFull(Deque *dq) {
return (dq->rear + 1) % MAXSIZE == dq->front;
}
// 从队头插入元素
bool pushFront(Deque *dq, int value) {
if (isFull(dq)) {
printf("双端队列已满,无法在队头插入!\n");
return false;
}
// 队头指针前移一位(循环)
dq->front = (dq->front - 1 + MAXSIZE) % MAXSIZE;
dq->datadq-\>front = value;
return true;
}
// 从队尾插入元素
bool pushBack(Deque *dq, int value) {
if (isFull(dq)) {
printf("双端队列已满,无法在队尾插入!\n");
return false;
}
dq->datadq-\>rear = value;
dq->rear = (dq->rear + 1) % MAXSIZE;
return true;
}
// 从队头删除元素
bool popFront(Deque *dq, int *value) {
if (isEmpty(dq)) {
printf("双端队列为空,无法从队头删除!\n");
return false;
}
*value = dq->datadq-\>front;
dq->front = (dq->front + 1) % MAXSIZE;
return true;
}
// 从队尾删除元素
bool popBack(Deque *dq, int *value) {
if (isEmpty(dq)) {
printf("双端队列为空,无法从队尾删除!\n");
return false;
}
// 队尾指针前移一位(指向最后一个有效元素)
dq->rear = (dq->rear - 1 + MAXSIZE) % MAXSIZE;
*value = dq->datadq-\>rear;
return true;
}
// 获取队头元素(不出队)
bool getFront(Deque *dq, int *value) {
if (isEmpty(dq)) return false;
*value = dq->datadq-\>front;
return true;
}
// 获取队尾元素(不出队)
bool getBack(Deque *dq, int *value) {
if (isEmpty(dq)) return false;
int last = (dq->rear - 1 + MAXSIZE) % MAXSIZE;
*value = dq->datalast;
return true;
}
// 简单测试
int main() {
Deque dq;
initDeque(&dq);
pushBack(&dq, 1);
pushBack(&dq, 2);
pushFront(&dq, 0);
pushFront(&dq, -1);
int val;
popFront(&dq, &val);
printf("队头出队: %d\n", val); // -1
popBack(&dq, &val);
printf("队尾出队: %d\n", val); // 2
return 0;
}
总结
共享栈与双端队列的共性。两者都优先采用数组作为底层容器,利用连续内存空间,基于顺序存储(链表)具备随机存储特性。双端操作的思想,共享栈的两个栈分别从数组两端延伸;双端队列允许两端插入/删除。对比于常规的栈和队列,上述限定数据结构有更高的空间利用率,共享栈动态分配空间为两个栈;双端队列循环利用数组空间,避免"假溢出"。共享栈和双端队列的基本操作的时间复杂度均为O(1)/O(n)。
提问
双端队列的"假溢出"?
时间复杂度什么时候用O(1)/O(n)?
是否有只插入或者删除的应用场景?
数据结构还有哪些限定的结构?


