本篇内容以知识整理为主,会结合萨特吉-萨尼的数据结构书籍和网络上的一些知识整理做一下总结,语言使用c++,有问题请及时指正,欢迎交流。
栈与队列
栈
1、栈的定义与结构
1、栈的定义
栈(stack)是一种特殊的线性表,其插入(也称入栈或压栈)和删除(也称出栈或弹栈)操作都在表的同一端进行,这一端称为栈顶(top),另一端称为栈底(bottom)。通俗地来说,栈是一种后进先出(LIFO)的数据结构。
对于栈的元素,进行出栈操作时,最后进栈的元素,不一定只能最后出栈。栈对线性表的插入和删除的位置进行了限制,并没有对元素进出的时间进行限制,也就是说,并不是所有元素都进栈的情况下,事先进去的元素也可以出栈,只要保证栈顶元素出栈就可以了。
例如现在有1、2、3三个元素依次进栈,出栈次序有321、213、231、123、132。没有312这种次序。
2、栈的结构
栈有两种实现的方式。一种是顺序存储,和数组类似;一种是链式存储,和单链表类似。
//顺序存储
typedef struct SqStack
{
int data[MAX];
int top;//用于栈顶指针
}SqStack;
//链式存储
typedef struct StackNode //结点
{
int data;
struct StackNode *next;
}StackNode,*LinkStackPtr;
typedef struct Node
{
LinkStackPtr top;
int count;
}LinkStack;
2、栈的创建及相关操作实现
1、初始化栈
void InitStack(SqStack *s) {
s->top = -1;
}
2、判断栈是否为空
bool Stackempty(SqStack *s) {
if (s->top == -1) {
return true;
}
else return false;
}
3、进栈
int push(SqStack *s, int e) {
if (s->top == MAX - 1) {
cout << "栈满";
return 0;
}
s->data[++s->top] = e;
return 1;
}
4、出栈
//出栈操作
int pop(SqStack *s, int *e) {
if (s->top == -1) {
cout << "栈是空的";
return 0;
}
*e = s->data[s->top--];
return 1;
}
5、链式结构相关代码
//进栈
int push(LinkStack *s,int e)
{
LinkStackPtr p=new(StackNode); //插入p结点作为新栈顶,注意是用结点结构申请空间的
p->next=NULL;
p->data=e;
p->next=s->top; //s->top为原栈顶
s->top=p;
s->count++;
return 1;
}
//出栈
int pop(LinkStack *s,int *e)
{
LinkStackPtr p; //定义一个变量存储需要删除的栈顶元素
if(!s)
{
cout<<"栈空了"
return 0;
}
*e=s->top->data;
p=s->top;
s->top=s->top->next;
delete(p);
s->count--;
return 1;
}
//判断是否为空
int Stackempty(LinkStack *s)
{
if(s->top==NULL)
return 0;
else
return 1;
}
//初始化链栈
void InitStack(LinkStack *s)
{
s=new(Node);
s->top=NULL;
}
5、两栈共享空间
以下内容来自Change_Improve的博客,我觉得讲的很好。
我们可以用一个数组来存储两个栈。做法如下图所示,数组有两个端点,两个栈有两个栈底,让一个栈的栈底为数组的始端,即下标为0处;另一个栈的栈底为此栈的末端,也就是数组的顶端,即下标为数组长度n-1处。这样,两个栈如果增加元素,就是两端点向中间延伸。
6、栈溢出
栈溢出是指向向栈中写入了超出限定长度的数据,溢出的数据会覆盖栈中其它数据,从而影响程序的运行。
出现栈内存溢出的常见原因有3个:
1> 递归调用层次过深,每调用一次,函数的参数、局部变量等信息就压一次栈。
2> 局部静态变量体积太大。局部变量是存储在栈中的,解决这类问题的办法有两个,一个是增大栈空间,另一个是改用动态分配,使用堆(heap)。
3>指针或数组越界。例如字符串拷贝、处理用户输入等。
3、栈的递归应用
递归:一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。
//经典递归之斐波那契数列
#include<iostream>
using namespace std;
int Fbi(int i)
{
if (i < 2) //递归终止条件
{
return i == 0 ? 0 : 1; //i为0时返回0,不为0时,说明i为1,则返回1
}
return Fbi(i - 1) + Fbi(i - 2); //这里Fbi就是函数自己,它在调用自己
}
int main()
{
for (int i = 0; i < 40; ++i)
{
cout << Fbi(i) << endl; //调用Fbi函数
}
return 0;
}
队列
1、队列的定义与结构
1、定义
队列(queue)是一个线性表,其插入和删除分别在表的不同端进行。插入元素的那一端称为队尾(back或rear),删除元素的那一端称为队首(front)。通俗来讲是一种先进先出(FIFO)的数据结构。本文讲述顺序队列和循环队列。
队列长度:(r-f+1+MAX)%MAX(最大长度)或:
//队列长度
int QueueLen(SQType *q)
{
return(q->tail - q->head);
}
顺序队列:每次插入,指针rear(队尾)加一,每次删除,指针front(队头)加一;
循环队列:
初始化时,rear = front=0,当队列不为空时,front指向队列中的第一个元素,rear指向队列中最后一个元素的下一个位置,当队列满时 rear=front,但不一定是位置0;
插入后rear+1,删除后front+1,但是,无论是删除还是插入,一旦rear或front加一超过了所分配的空间,则让指针指向这片空间的起始位置;设所分配的空间为Maxsize,一旦rear+1,或front+1 =Maxsize, 则rear或front指向起始位置。
因此,循环序列判空的方法是rear = front; 判满的方法是 (rear+1)%Maxsize ==front;
2、队列的结构
【不同教材对于队头队尾位置的定义方式不同,但其核心思想是一致的,对于同一问题的研究需要统一该定义方式,否则容易产生歧义。常见的定义方式分为两种,一种是从队头标记的下一个位置到队尾标记之间的序列为实际使用队列,另一种为从队头标记到队尾标记的前一个位置之间的序列为实际使用队列】
//队列结构体
#define MAX 10
struct SQType {
int data[MAX];//队列数据,可以自己建新结构体,这里以整型为例
int head;//队头
int tail;//队尾
};
2、队列的初始化及相关操作代码
queue 的基本操作有:
入队,如例:q.push(x); 将x 接到队列的末端。
出队,如例:q.pop(); 弹出队列的第一个元素,注意,并不会返回被弹出元素的值。
访问队首元素,如例:q.front(),即最早被压入队列的元素。
访问队尾元素,如例:q.back(),即最后被压入队列的元素。
判断队列空,如例:q.empty(),当队列空时,返回true。
访问队列中的元素个数,如例:q.size()
1、初始化队列
//顺序队列初始化
SQType *InitQueue(){
//申请队列的内存空间
SQType *q;
if (q = new SQType) {
//设置head=0和tail=0,表示一个空栈。
q->head = 0;
q->tail = 0;
return q;
}
else {
cout << "申请内存失败";
return NULL;
}
}
2、判断队列是否为空
若队列为空,那么对首队尾值相同
bool QueueIsEmpty(SQType *q) {
if (q->head == q->tail)
return true;
else return false;
}
3、判断队列是否为满队列
若队列满了,那么队尾值为队列最大长度
bool QueueIsFull(SQType *q) {
if (q->tail == MAX) {
return true;
}
else return false;
}
4、清空队列
让队首队尾的值都变成0,成为一个空队列
//清空队列
int QueueClear(SQType *q) {
//队尾队首都设为0即成为一个空队列
q->head = 0;
q->tail = 0;
}
//删除队列
void QueueFree(SQType *q) {
if (q != NULL) delete q;
}
5、入队
先判断队列是否满了,不满则在队尾添加数据,队尾++
int Queuepush(SQType *q, int data) {
if (q->tail == MAX) {
cout << "队列已满";
return;
}
else {
q->data[q->tail++] = data;
return 1;
}
}
6、出队
先判断队列是否为空,若不为空则弹出队首,队首++
int OutQueue(SQType *q) {
if (q->head == q->tail) {
cout << "队列已空";
return -1;
}
else {
return q->data[q->head++];
}
}
7、读取节点数据
与出队不同的是,读取节点数据只是显示数据,而不需要将数据弹出,所以队首值不变
int PeektQueue(SQType *q) {
if (q->head == q->tail) {
cout << "队列已空"<<endl;
return ;
}
else {
return q->data[q->head];
}
}
3、队列的相关应用
1、两个栈实现一个队列
看到这个题,我们就要想到栈和队列的不同,所谓用两个栈实现一个队列是指,我们要实现队列的“尾插”和“头删”操作。
首先,假如我们要插入一些数据“abcd”,我们知道按照这个顺序队列出现的结果也是“abcd”,而栈会出现“dcba”,刚好相反,因此将该栈的到的数据在插入另外一个栈中就会出现我们想要的结果。因此,我们定义两个栈为“stack1”和“stack2”,栈1只用来插入数据,栈2用来删除数据栈1插入进来的数据。
图(1):将队列中的元素“abcd”压入stack1中,此时stack2为空;
图(2):将stack1中的元素pop进stack2中,此时pop一下stack2中的元素,就可以达到和队列删除数据一样的顺序了;
图(3):可能有些人很疑惑,就像图3,当stack2只pop了一个元素a时,satck1中可能还会插入元素e,这时如果将stack1中的元素e插入stack2中,在a之后出栈的元素就是e了,显然,这样想是不对的,我们必须规定当stack2中的元素pop完之后,也就是satck2为空时,再插入stack1中的元素。
LeetCode 剑指Offer 09 用两个栈实现一个队列
class CQueue {
public:
stack<int> a;
stack<int> b;
CQueue() {
}
void appendTail(int value) {
a.push(value);
}
int deleteHead() {
if (b.empty() && a.empty())
return -1;
else if (b.empty() && !a.empty())
{
while (!a.empty())
{
b.push(a.top());
a.pop();
}
}
int temp = b.top();
b.pop();
return temp;
}
};