栈和队列

1.栈

1.定义

1、栈是一种特殊的线性表只允许在一端进行插入或删除操作

2、栈顶:允许插入和删除的一端

3、栈底:不允许插入和删除的一端

4、空栈:元素为空的栈

2.特点

后进先出

3.基本操作

InitStack(&S);    //初始化化栈,构造一个空栈,分配内存空间
DestroyStack(&S); //销毁栈,回收内存空间

Push(&S, x);   //入栈,若栈未满,则将x加入使之成为新的栈顶
Pop(&S, &x);   //出栈,若栈s非空,则弹出栈顶元素,用x返回

GetTop(S,&x);  //读取栈顶元素,若栈S非空,则用x返回,并不弹出

StackEmpty(S);  //栈判空操作,空,返回true

4.出栈序列

n个不同元素进栈,出栈元素不同排列个数为:

图片说明

2.顺序栈

1.定义

#define MaxSize 10
typedef struct{
  int data[MaxSize];
  int top;
}SqStack;

//初始化
void InitStack(SqStack &S){
  S.top = -1;    //初始化栈顶指针,也可以实现成初始值为0的情况
}

2.进栈

//新元素进栈操作
bool Push(SqStack &S, int x){
  if(S.top == MaxSize - 1)
    return false;
  S.top = S.top + 1;
  S.data[S.top] = x;
  return true;
}

3.出栈

//出栈操作
bool Pop(SqStack &S, int &x){
  if(S.top == -1){
    return false;
  }
  x = S.data[S.top--];
  return true;
}

4.读栈顶元素

bool GetTop(SqStack S, int &x){
  if(S.top == -1){
    return false;
  }
  x = S.data[S.top];
  return true;
}

注意:top指针初始值为-1,表示指向栈顶元素,如果初始值为0,则表示指向栈顶元素的下一个位置。

5.缺点

顺序栈是通过静态数组实现的,内存固定,不方便拓展,但是如果一开始开辟一个比较大的空间用于栈,则会导致内存的使用效率降低;解决方式如下

3.共享栈

1.定义

两个栈共享同一片空间

#define MaxSize 10

typedef struct{
  int data[MaxSize];
  int top0;
  int top1;
}ShStack;

//初始化栈顶指针
void InitStack(ShStack &S){
  S.top0 = -1;
  S.top1 = MaxSize;
}

判断栈是否满了的条件是:top0 + 1 == top1

2.使用场景?

对于共享栈的使用场景,未找到相关资料,暂时保留?

4.链栈

1.定义

通过链表的方式实现栈;即规定只在单链表的链首进行插入和删除操作

typedef struct LinkNode{
  int data;
  struct LinkNode *next;
}*LiStack;

同样可以实现带头结点和不带头结点的版本,建议实现不带头结点的版本

2.相关操作(无头结点)

//初始化栈
InitStack(LiStack &S);

//进栈
PushLiStack(LiStack &S, int e);
//出栈
PopLiStack(LiStack &S, int& e);

//获取栈顶元素
GetLiStackTop(LiStack S, int& e);

//链栈是否存在判空判满操作,个人觉得,没必要

5.队列——顺序队

1.定义

1、只允许在一端进行插入,在另一端删除线性表

2、队尾:可以进行插入元素的一端

3、队头:可以进行删除元素的一端

4、空对:元素为空的队列

5、特点:先进先出

2.基本操作

//创销
InitQueue(&Q);
DestroyQueue(&Q);
//入队,出队
EnQueue(&Q,x);
DeQueue(&Q,&x);
//读对头元素
GetHead(Q,&x);
//判队空
QueueEmpty(Q);

3.顺序存储实现

//注意,此种实现方式,rear初值为0,队尾指针指向的是队尾元素的下一个应该插入的位置
//如果需要指向队尾元素,则rear初值应该为-1

#define MaxSize 10
typedef Struct{
  ElemType data[MaxSize];
  int front,rear;
}SqQueue;

//初始化队列
void InitQueue(SqQueue &Q){
  Q.rear = Q.front = 0;
}
//判断队列是否为空
bool QueueEmpty(SqQueue Q){
  if(Q.rear == Q.front){
    return true;
  }
  return false;
}

//入队,只能在队尾入队
bool EnQueue(SqQueue &Q, ElemType x){
  if((Q.rear + 1)%MaxSize == Q.front){
    return false;
  }
  Q.data[Q.rear] = x;
  Q.rear = (Q.rear + 1)%MaxSize;
  return true;
}

//出队,只能在队头进行出队
bool DeQueue(SqQueue &Q, ElemType &x){
  if(Q.rear == Q.front){
    return false;
  }
  X = Q.data[Q.front];
  Q.front = (Q.front + 1)%MaxSize;
  return true;
}

//获得对头元素,用x返回
bool GetHead(SqQueue Q, ElemType &x){
  if(Q.rear == Q.front){
    return false;
  }
  x = Q.data[Q.front];
  return true;
}

1、注意:上面实现情况中,判断队空和队满是通过牺牲一个存储单元实现的;队空rear == front;队满rear + 1 = front;

2、队列元素个数 = (rear + front + MaxSize)%MaxSize

4.判断队满队空

1、如果不能牺牲存储空间,rear初值为0,则判断队满和队空可以通过以下方式:

//方式一
/*
在struct中定义size变量,用来表示当前队列的长度
size初始值0;入队size++;出队size--;
*/
typedef struct{
  int front,rear;
  int size;
}SqQueue;

//方式二
/*
设置tag,表示最近进行的操作是删除/插入
每次删除操作成功时,tag = false;
每次插入操作成功时,tag = true;

只有插入操作,才会导致队满,队满条件:
front == rear && tag == true;
只有删除操作,才会导致队空,队空条件
front == rear && tag == false;
*/
typedef struct{
  int front,rear;
  bool tag;
}SqQueue;

2、如果不能牺牲存储空间,rear初值为1,则判断队满和队空可以通过以下方式:

//判空
(Q.rear + 1)%MaxSize == Q.front;

//判满
/*
方案一:牺牲一个存储单元
(Q.rear + 2)%MaxSize == Q.front;
或
(Q.rear + 1)%MaxSize == Q.front - 1;

方案二:增加辅助变量

*/

6.队列——链式存储

1.定义

可以通过链表实现队列,队尾插入,队头删除,可以实现带头结点和不带头结点的版本

typedef struct LinkNode{
  ElemType data;
  struct LinNode* next;
}LinkNode;

typedef struct{
  LinkNode* front,*rear;     //对头和队尾指针
}LinkQueue;

//初始化,带头结点
void InitQueue(LinkQueue &Q){
  Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode));
  Q.front->next = NULL;
}
//判断队列是否为空
bool IsEmpty(LinkQueue Q){
  if(Q.front == Q.rear)
    return true;
  return false;
}

/*______________________________________________________
*/

//初始化,不带头结点
void InitQueue(LinkQueue &Q){
  Q.front = Q.rear = NULL;
}
//判断队列是否为空
bool IsEmpty(LinkQueue Q){
  if(Q.front == NULL)
    return true;
  return false;
}

2.入队

//带头结点
void EnQueue(LinkQueue &Q, ElemType x){
  LinkNode *s = (LinkNode*)malloc(sizeof(LinkNode));
  s->data = x;
  s->next = NULL;
  Q.rear->next = s;
  Q.rear = s;
}


//不带头结点
void EnQueue(LinkQueue &Q, ElemType x){
  LinkNode *s = (LinkNode*)malloc(sizeof(LinkNode));
  s->data = x;
  s->next = NULL;
  if(Q.front == NULL){
    //第一个节点
    Q.front = s;
    Q.rear = s;
  }
  else{
    Q.rear->next = s;
    Q.rear = s;
  }
}

3.出队

//带头结点
bool DeQueue(LinkQueue &Q,ElemType &x){
  if(Q.front == Q.rear){
    return false;            //空队
  }
  LinkQueue* p = Q.front->next;
  x = p->data;
  Q.front->next = p->next;
  //表尾节点,需要修改表尾指针
  if(Q.rear == p)
    Q.rear = Q.front;
  free(p);
  return true;
}

//不带头结点
bool DeQueue(LinkQueue &Q,ElemType &x){
  if(Q.front == NULL){
    return false;     //空队
  }
  LinkNode *p = Q.front;
  x = p->data;
  Q.front = p->next;
  //最后一个队
  if(Q.rear == P){
    Q.front = NULL;
    Q.rear = NULL;
  }
  free(p);
  return true;
}

注意:

1、在链式队列中,一般不需要进行判断队满的条件

2、如果需要经常访问队列元素的大小,由于是链表实现的,所以很不方便,每次都要经过O(N)的时间复杂度,因此,可以在struct中定义一个辅助变量length,来存储队列的大小

7.双端队列

1.定义

1、允许两端进行插入、两端删除的线性表

2、输入受限的双端队列:只允许从一端插入、两端删除的线性表

3、输出受限的双端队列:只允许从两端插入、一端删除的线性表

2.输出序列

主要考点是判断输出序列是否合法

8.栈的应用

1.括号匹配问题

bool bracketCheck(char str[], int length){
  SqStack S;
  InitStack(S);
  for(int i = 0; i < length; ++i){
    if(str[i] == '(' || str[i] == '[' || str[i] == '{'){
      Push(S, str[i]);
    }
    else{
      if(StackEmpty(S)){
        //扫描到有括号,栈空,匹配失败
        return false;
      }
      char topElem;
      Pop(S, topElem);
      if(str[i] == ')' && topElem != '('){
        return false;
      }
      if(str[i] == ']' && topElem != '['){
        return false;
      }
      if(str[i] == '}' && topElem != '{'){
        return false;
      }
    }
  }
  //检索完全部括号后,栈空说明匹配成功
  return StackEmpty(S);
}

1、用栈实现括号匹配:

依次扫描所有字符,遇到左括号入栈,遇到有括号则弹出栈顶元素检查是否匹配

2、匹配失败的情况:

1)左括号单身

2)有括号单身

3)左右括号不匹配

2.表达式求值

注意:做操作数和有操作数的位置在转化表达式后不能发生变化

1.三种表达式

中缀表达式、前缀表达式、后缀表达式
图片说明
图片说明
图片说明

2.中缀表达式转后缀表达式

图片说明
图片说明

左优先可以保证顺序唯一

3.后缀表达式的计算

图片说明

注意:两个操作数有先后顺序

通过栈实现机算,也可以将后缀转化为中缀表达式

4.中缀表达式转成前缀表达式

图片说明

5.前缀表达式的计算

图片说明

注意:后缀表达式从左往右扫描,先弹出的是右操作数前缀表达式从右往左扫描,先弹出的是左操作数

3.中缀表达式转后缀表达式

1.算法思想

图片说明

2.代码

简单实现,此代码存在问题,大致逻辑没问题,只能处理10以内的算术运算,只支持()小括号,只支持 +-*/

static map<char, int> recordOfOperator = {
    {'*',2},
    {'/',2},
    {'+',1},
    {'-',1}
};

//判断是否为运算符
bool isOperator(char ch){
    return recordOfOperator.find(ch) != recordOfOperator.end();
}
//判断两个操作符的优先级,等于返回0,first > second 返回 1;first < second 返回2;
int priorityOfOp(char first, char second){
    int operator1 = recordOfOperator.find(first)->second;
    int operator2 = recordOfOperator.find(second)->second;
    return operator1 - operator2;
}

/*
中缀表达式转后缀表达式
中缀表达式 : 1 + 2 *(3 + 4) - 5
后缀表达式:1 2 3 4 + * + 5 -

默认中缀表达式无特殊字符
*/
bool InfixToPostfixExpression(string str,string &res){
    if(str.empty()){
        return false;
    }
    stack<char> op;
    for(int i = 0; i < str.size(); ++i){
        if(isOperator(str[i])){
            //遇到操作符,依次弹出栈中操作符比当前操作符优先级高的,直到栈空或者遇到 (
            while(!op.empty() && ( op.top() != '(' && priorityOfOp(op.top(), str[i]) >= 0)){
                res.push_back(op.top());
                op.pop();
            }
            op.push(str[i]);
        }
        else if(str[i] == '(' || str[i] == ')'){
            //遇到限定符
            if(str[i] == '('){
                op.push(str[i]);
            }
            else{
                //依次弹出栈内运算符,并加入到后缀表达式,直到弹出'('为止
                while(!op.empty() && op.top() != '('){
                    res.push_back(op.top());
                    op.pop();
                }
                if(op.top() == '('){
                    op.pop();
                }
            }
        }
        else{
            //遇到操作数
            res.push_back(str[i]);
        }
    }
    while(!op.empty()){
        res.push_back(op.top());
        op.pop();
    }
    return true;
}

4.中缀表达式的求值

1.算法思想

中缀表达式计算 = 中缀表达式转后缀表达式 + 后缀表达式的计算

图片说明
图片说明

2.代码

可以先转后缀,再通过后缀表达式计算,后缀表达式计算代码如下:

此代码存在的问题和中缀转后缀的一样,尚未作出调整和改进

//通过两个操作数和操作符进行计算
int calculate(int first, int second, char op){
    int res = 0;
    switch (op) {
        case '+':
            res = first + second;
            break;
        case '-':
            res = first - second;
            break;
        case '/':
            res = first / second;
            break;
        case '*':
            res = first * second;
            break;
    }
    return res;
}

//后缀表达式的计算
int postfix_calculate(string str){
    if(str.empty()){
        return 0;
    }
    stack<char> op;
    for(int i = 0; i < str.size(); ++i){
        //isOperator函数在中缀转后缀中实现
        if(!isOperator(str[i])){
            op.push(str[i]);
        }
        else{
            int second = op.top() - '0';
            op.pop();
            int first = op.top() - '0';
            op.pop();
            int temp = calculate(first, second, str[i]);
            op.push(temp + '0');
        }
    }

    return op.top() - '0';
}

5.栈的应用——递归

1.函数调用:函数调用时,需要用一个栈存储以下内容:

1)调用返回地址

2)实参

3)局部变量

2.适合用“递归”算法解决:可以把原始问题转换为属性相同,但规模较小的问题;递归需要考虑的是递归表达式(递归体)和边界条件(递归出口);递归算法的缺点就是:太多层可能会导致栈溢出,也可能包含重复计算;

9.队列的应用

1.树的层序遍历

2.图的广度优先遍历

3.操作系统中的应用

1.先来先服务FCFS服务

2.I/O调度

3.进程调度

10.特殊矩阵压缩存储

1.数组的存储结构

1.各数组元素大小相同,且物理上连续存放

2.二维数组

将非线性的数据结构在物理上存储为线性的

1)行优先

2)列优先

2.普通矩阵

1、普通矩阵在计算机中的存储方式是通过二维数组存储

2、对于特殊矩阵,可以通过特殊的存储方式来节省存储空间

3、特殊矩阵:对称矩阵、上三角矩阵、稀疏矩阵

3.对称矩阵

1.只存储主对角线和下三角区;按行优先原则将各个元素存入一维数组中。

2.需要开辟的数组大小 = 1 + 2 + ... + n = (n + 1)*n/2

3.怎么样才能实现对称举证压缩存储后方便使用:

图片说明

5.三对角矩阵

又称带状矩阵

图片说明
图片说明

6.稀疏矩阵

非零元素远远少于矩阵元素的个数
图片说明

图片说明