文章目录
- 特点分析
- 栈结构
- 定义
- 出现意义
- 存储方式
- 栈的常用操作
- 栈的细分
- 栈的“上溢”和“下溢”
- 顺序栈
- 代码实现
- 链栈
- 代码实现
- 队列结构
- 定义
- 结构特点
- 单端队列
- 单向顺序队列
- 循环顺序队列
- 代码示例
- 链式队列
- 代码示例
- 双端队列
- 顺序存储代码
- 链式存储方式
- 其他特殊队列
- LRU缓存淘汰
- 延迟队列
- 常用场景
- 阻塞队列
特点分析
-
线性表逻辑上是线性关系,它的操作是可以任意位置进行删除和添加的
-
当把线性表的操作进行了一定的约束,就有了特殊的结构
- 只能在一端进行添加和删除,栈(stack)
- 只能在一端进行添加,另外一端进行删除,队列(Queue)
栈结构
定义
- 栈是限制在一端进行插入操作和删除操作的线性表(俗称堆栈),允许进行操作的一端称为“栈顶”,另一固定端称为“栈底”,当栈中没有元素时称为“空栈”。特点 :后进先出(LIFO)。
出现意义
-
历史记录
-
倒着走的逻辑,回溯、暂存状态的需求
-
函数的调用意味着入栈,函数返回意味着出栈
存储方式
栈的存储方式上,仍然可以使用顺序存储和链式存储
栈的常用操作
-
入栈
-
出栈
-
判断栈是否为空、为满
栈的细分
栈顶指针指向的是有效数据还是下一个待插入的数据
- 满栈
- 空栈
栈顶指针指向下一个元素时,是向内存空间上还是下的位置
- 递增栈
- 递减栈
栈的“上溢”和“下溢”
-
栈的“上溢”和“下溢”,可以总结为:栈满还存会“上溢”,栈空再取会“下溢”。
-
对于栈的两种表示方式来说,顺序栈两种情况都有可能发生;而链栈由于“随时需要,随时申请空间”的存储结构,不会出现“上溢”的情况。
顺序栈
采用顺序存储结构实现。
初始化一个栈,栈底固定不变,在下标0位置,栈顶是活动的,需要一个变量(int)标记栈顶(top)----------为了描述方便,通常会借用"指针"两字称呼top变量:”栈顶指针"
- 初始化,top=-1 ,top标记的是真正的栈顶元素。
- 入栈(Push),top++,data[top] = element;
- 出栈(Pop),top–;
- 判满,top=maxsize-1;
- 判空,if(top=-1)
- 得到栈顶元素,cout<<data[top];
代码实现
#include <iostream>
using namespace std;
#define maxSize 10
typedef struct {int* data; //数据域int top; //栈顶“指针”
}sstack;//初始化顺序栈
sstack initial(){sstack s;s.data =(int*) malloc(sizeof(int)*maxSize); if(s.data==NULL){cout<<"内存分配失败!"<<endl;exit(1); //退出程序}s.top = -1; //栈顶“指针”所指的元素是栈顶元素return s;
}//判满
bool isFull(sstack* s){if(s->top == maxSize-1){return true;}return false;
}//判空
bool isEmpty(sstack* s){if(s->top==-1){return true;}return false;
}//入栈
void push(sstack* s,int element){if(!isFull(s)){s->top++;s->data[s->top] = element;}
}//出栈
void pop(sstack* s){if(!isEmpty(s)){s->top--;}
}//得到栈顶元素
int getTOP(sstack* s){if(!isEmpty(s)){cout<<"栈顶元素为:"<<s->data[s->top]<<endl;}else{return -1;}
}
int main(){sstack s = initial();push(&s,1);getTOP(&s);push(&s,3);push(&s,6);getTOP(&s);pop(&s);getTOP(&s);
}
链栈
用链式存储来实现栈:链栈------>基于单链表(带头节点)
-
初始化,单链表 初始化就行
-
入栈:头插法 栈顶一直是首元节点,头指针做栈顶指针 L
-
出栈:删除首元节点
-
判空:L->next==NULL;
-
得到栈顶元素:X = L->next->data;
代码实现
#include <iostream>
using namespace std;
typedef struct stackNode{int data; //数据域struct stackNode* next; //指针指向下一个元素的地址
}stackNode,*sstack;//初始化链栈
sstack initial(){stackNode* s = (stackNode*)malloc(sizeof(stackNode));if(s==NULL){cout<<"初始化失败!"<<endl;exit(1);}s->next = NULL;return s;
}//得到栈顶元素
int getTop(sstack top){if(top->next!=NULL){cout<<"栈顶元素:"<<top->next->data<<endl;}else{cout<<"空栈"<<endl;}
}//判空
bool isEmpty(sstack& top){if(top->next == NULL){cout<<"空栈"<<endl;return true;}else{return false;}
}//入栈
void push(sstack& top,int element){stackNode* s =(stackNode*) malloc(sizeof(stackNode));s->data = element;//头插法s->next = top->next;top->next =s;
}//出栈
void pop(sstack& top){if(!isEmpty(top)){stackNode* p = top->next;top->next = p->next;free(p);}
}
int main(){sstack top = initial();push(top,1);getTop(top);push(top,3);push(top,6);getTop(top);pop(top);getTop(top);
}
队列结构
定义
-
队列是一种特殊的线性表,特殊之处就在于它只允许在表的前端进行删除操作,在表的后端进行 插入操作。
-
和栈一样,队列也是一种操作受到限制的线性表。进行插入操作的端称之为队尾,进行删除 操作的端称之为队头。
-
队列中没有队列的时候,称之为空队列。队列的数据元素,⼜叫做队列元素。在 队列中插入一个队列元素称之为入队,在队列中删除一个队列元素,称之为出队。
-
因为队列只允许在一 端插入,在另一端删除,所以只有最早进入的队列元素才可以从队列中删除,故队列⼜称为先进先出线 性表。
结构特点
-
支持顺序存储和链式存储
单端队列
单向顺序队列
- 用一维数组来存放顺序队列的数据元素
- 队头设置在即将离开队列的元素处
- 队尾设置在即将插入队列的位置处
-
初始化空的队列:f=0;r=0; r是指向真正队尾元素的下一个位置(如果r=-1,则r指向真正的队尾元素)
-
入队:data[r] = k; r++;
-
出队:f++;
-
判满:r==maxSize(假溢出);
-
判空:f==r
-
得到队首元素 x= data[f]
循环顺序队列
-
当队尾索引指向了size - 1时,代表已经满了,但对头由于之前的出队事件,队列中应该还有多余的空间,这种称之为假溢出。
-
为了解决假溢出的问题,就是将我们的顺序队列看成是⾸位相接的循环结构。⾸尾指示器不变, 这种队列就叫做,循环顺序队列。
-
初始化
f=r=0; //其中r指向的是真正队尾元素的下一个位置
-
入队
data[r] = element; r = (r+1)%maxSize;
-
出队
f=(f+1)%maxSize;
-
判满
//方法一:牺牲一个存储空间来判满 (r+1)%maxSize==f; //方法二:队满之前操作一定是入队,队空之前的操作的出队,设置flag变量。入队时falg=0;出队时,flag=1;
-
判空
r==f
-
得到队首元素
cout<<"队首元素:"<<data[f];
代码示例
#include <iostream>
using namespace std;
#define maxSize 10 //定义最大长度
typedef struct {int* data; //数组int f,r; //队首队尾指针
}Queue;//初始化
Queue initial(){Queue s;s.data = (int*)malloc(sizeof(int)*maxSize);if(s.data==NULL){//TODOexit(1);}s.f = s.r = 0;return s;
}//得到队首元素
int getHeadQueueElement(Queue queue){if(queue.r!=queue.f){cout<<"队首元素:"<<queue.data[queue.f]<<endl;}else{cout<<"队空,无元素!"<<endl;return -1;}
}//入队
void enterQueue(Queue& queue,int element){if((queue.r+1)%maxSize != queue.f){ //牺牲一个空间来,判满queue.data[queue.r] = element; //队尾“指针”指向队尾元素的下一个位置queue.r++;}
}//出队
void leaveQueue(Queue& queue){queue.f = (queue.f+1)%maxSize;
}int main(){Queue queue = initial();enterQueue(queue,1);getHeadQueueElement(queue); //1enterQueue(queue,3);enterQueue(queue,7);getHeadQueueElement(queue); //1leaveQueue(queue);getHeadQueueElement(queue); //3
}
链式队列
- 不用担心队满
- 头指针就是队列的队首指针,队首元素在首元节点
- 添加一个一直指向尾节点的尾指针
- 入队,带尾指针的单链表的尾插法
- 出队,删除队首节点(如果删除时,r指向首元节点,要对r进行处理,防止r成为野指针
- )
- 判空,f->next==NULL
代码示例
#include <iostream>
using namespace std;
typedef struct QNode{int data; //数据域struct QNode* next; //指针域
}QNode,*Queue;
QNode* f; //头指针
QNode* r; //尾指针//初始化
Queue initial(){QNode* s = (QNode*)malloc(sizeof(QNode));if(s==NULL){cout<<"分配内存失败"<<endl;exit(1);}s->next = NULL;f=r=s;return s;
}//得到队首元素
int getLeaderQueue(Queue queue){//先判空if(queue->next!=NULL){cout<<"队首元素:"<<queue->next->data<<endl;}
}//出队(出队前判空)删除首元节点
void leaveQueue(Queue& queue){QNode* p = queue->next;if(p->next==NULL){r=f;}queue->next = p->next;free(p);
}//入队,带尾指针的单链表的尾插法
void enterQueue(Queue& queue, int element){QNode* s = (QNode*)malloc(sizeof(QNode));s->data = element;s->next = f->next;f->next = s;f=s;
}
int main(){Queue queue = initial();enterQueue(queue,1);getLeaderQueue(queue);enterQueue(queue,3);enterQueue(queue,6);getLeaderQueue(queue);leaveQueue(queue);getLeaderQueue(queue);}
双端队列
-
两端都是结尾的队列,队列的每一端都可以插入数据和移除数据
-
这种定义,是方便一个结构来实现栈和队列,当我们只允许使用一端出队、入队操作的时候, 他等价于一个栈。当限制一端只能出队,另一端只能入队,他就等价于一个普通队列。
顺序存储代码
#include <iostream>
using namespace std;
#define maxSize 10
typedef struct {int* data = NULL; //指针模拟开数组int f,r; //首尾“指针” int size; //全局变量,代表队列长度
}doubleQueue;//双端队列初始化
doubleQueue initial(){doubleQueue s;s.data = (int*)malloc(sizeof(int)*maxSize);if(s.data==NULL){cout<<"内存分配失败!"<<endl;exit(1);}s.f=s.r=0;s.size=0;return s;
}//左边入队(一定要用循环数组来实现)
void left_enter(doubleQueue& queue, int element){//先判满if(queue.size!=maxSize){queue.data[queue.f] = element;queue.f = (queue.f-1+maxSize)%maxSize;queue.size++;}
}
//左边出队
void left_leave(doubleQueue& queue){if(queue.size==0){cout<<"队空不能出队!"<<endl;return;}cout<<queue.data[(queue.f+1)%maxSize] <<"出队"<<endl;queue.f = (queue.f+1)%maxSize;queue.size--;
}//右边入队
void right_enter(doubleQueue& queue, int element){if(queue.size==maxSize){cout<<"队满,不能入队!"<<endl;return;}queue.data[(queue.r+1)%maxSize]=element;queue.r++;queue.size++;
}//右边出队
void right_left(doubleQueue& queue){if(queue.size==0){return;}cout<<queue.data[queue.r]<<"出队!"<<endl;queue.r=(queue.r-1+maxSize)%maxSize;queue.size--;
}
int main()
{doubleQueue queue=initial();left_enter(queue,1);left_enter(queue,3);left_leave(queue); //3出队left_leave(queue); //1出队left_leave(queue); //队空不能出队right_enter(queue,6);right_enter(queue,7);right_enter(queue,9);right_left(queue); //9出队
}
链式存储方式
#include <iostream>
using namespace std;
typedef struct queueNode{int data; //数据域struct queueNode* next;struct queueNode* pre;
}queueNode;
queueNode* L; //队首指针 //指向队首元素的前一个位置
queueNode* r; //队尾指针 //指向队尾元素//初始化链式双端队列
void initial(){queueNode* s =(queueNode*)malloc(sizeof(queueNode));if(s==NULL){exit(1);}s->next=s->pre=NULL;L=r=s;
}//左边入队
void left_enter(int element){queueNode* s =(queueNode*)malloc(sizeof(queueNode));L->data = element;s->pre = L->pre;L->pre = s;s->next = L;L=s;
}
//左边出队
void left_leave(){//判空if(r==L){cout<<"空双端队列,不能出队!"<<endl;return;}cout<<L->next->data<<"出队!"<<endl;queueNode* p = L;L=p->next;L->pre = p->pre;free(p);
}
//右边入队
void right_enter(int element){queueNode* s =(queueNode*)malloc(sizeof(queueNode));s->data =element;s->next = r->next;s->pre = r;r=s;
}
//右边出队
void right_leave(){//判空if(r==L){cout<<"空双端队列,不能出队!"<<endl;return;}queueNode* p =r;cout<<r->data<<"出队!"<<endl;r=p->pre;r->next = p->next;free(p);
}
int main(){initial();left_enter(1);left_enter(3);left_enter(6);left_leave(); //6出队 left_leave(); //3出队right_enter(9);right_enter(7);right_leave();right_leave();right_leave();right_leave();
}
其他特殊队列
LRU缓存淘汰
-
新数据插入到链表头部;
-
每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
-
当链表满的时候,将链表尾部的数据丢弃。
过程描述:
-
这⾥我们是根据时间来来判断的,就是最近最久未使用的。如果根据使用次数来判断,做缓存的命中,那就叫做LFU (Least Freequently used)。⽬前Redis应该就是用的LFU。
延迟队列
- 延时队列中的元素在入队时会制定一个延迟时间,表示其希望能够在经过该指定时间后处理。从某 种意义来说,他不像是一个队列。更像是一个以时间为权重的堆。
用户可以在⼩程序中订阅不同的微信或者 QQ 的 模板消息,产品同学可以在⼩程序的管理端新建消息推送计划,
当到达指定的时间节点的时候给所有订 阅模板消息的用户进行消息推送。
如果仅仅是服务单一的⼩程序,那也许起个定时任务,或者甚⾄⼈⼯的定时去执行能够最便捷最 快速的去完成这
项需求,但我们希望能够抽象出一个消息订阅的模块服务出来给所有业务使用,这时候 就需要一种通用的系统的解决方案,这时候便需要使用到延迟队列了。
常用场景
阻塞队列
-
它是一个队列(类似于一个List),可以存放0到N个元素
-
可以执行插入或弹出元素操作
-
当队列中没有元素时,对这个队列的弹出操作 将会被阻塞,直到有元素被插入时才会被唤醒;
-
当队列已满时,对这个队列的插入操作就会被阻塞,直 到有元素被弹出后才会被唤醒。
在线程池中,往往就会用阻塞队列来保存那些暂时没有空闲线程可以直接执行的任务,等到线程 空闲之后再从阻塞队列中弹出任务来执行。一旦队列为空,那么线程就会被阻塞,直到有新任务被插入 为⽌。