栈和队列
栈和队列
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.稀疏矩阵
非零元素远远少于矩阵元素的个数 
 
 投递福建联通等公司10个岗位
投递福建联通等公司10个岗位