一、栈
1.1 栈的基本概念:
栈是一种特殊的线性表,其特殊性体现在元素插入和删除运算上,它的插入和删除运算仅限定在表的某一端进行,不能在表中间和另一端进行。允许进行插入和删除的一端称为栈顶(top),另一端称为栈底(bottom)。栈的插入操作称为进栈(Push),删除操作称为出栈(pop)。处于栈顶位置的数据元素称为栈顶元素。不含任何数据元素的栈称为空栈。
1.2 基本操作
(1)进栈:插入[ Push(x) ]
(2)出栈:删除[ Pop( ) ]
(3)取栈顶元素 Top( )
(4)判断栈是否为空/满 IsEmpty( ) IsFull( )
【创销,增删查(改)】
注:这些操作都可以在常数时间内被完成。
先进后出,后进先出。
1.3 应用
(1)函数调用、递归
(2)编辑器的撤回操作。
(3)检查括号是否平衡(有左括号必须得有右括号)
二、栈的实现
2.1 用数组实现一个栈
#include <iostream>
using namespace std;
#define MAX_SIZE 101
int A[MAX_SIZE];//全局变量
int top = -1;//索引变量,类似于表明位置的指针
void Push(int x)
{
if (top == MAX_SIZE - 1)//top到最后一个位置了,在加就要溢出了
{
cout << "Error:stack is full of elements!" << endl;
return;
}
A[++top] = x;
}
bool Pop(int &x)
{
if (top == -1)//已经是空栈了
{
cout << "Error:No element to pop!" << endl;
return false;
}
x = A[top];
top--;
return true;
}//x将会得到取出来的值
void Print()
{
int i;
for (i = 0; i <= top; i++)//注意这里是小于等于,因为top也是从0开始算的
cout << A[i] << endl;
}
int Top()
{
return A[top];//返回栈顶元素值
}
int main()
{
Push(2);
Push(5);
Push(7);
Push(4);
int x;
int TestVictory = Pop(x);
cout << "TestVictory= " << TestVictory << " the got element is "<<x<< endl;
Print();
return 0;
}
数组栈的缺点:栈的大小不可变,有可能出现栈溢出或者栈浪费
共享栈:
(1)定义:两个栈共享同一片内存空间,两个栈从两边往中间增长。
(2)初始化:0号栈的栈顶指针top0=-1;1号栈的栈顶指针top1=MaxSize;【放元素时都是先动指针,后存入值】
(3)栈满条件:top0+1==top1;
2.2 用单链表实现一个栈
(1)在链表尾插入/删除,时间复杂度为O(n)
(2)在链表头部插入/删除,时间复杂度仅为O(1)
其实就是头插法和头删法。
#include <iostream>
using namespace std;
typedef struct Node
{
int data;
Node* link;
}Node;
Node* top = NULL;//就是之前的“头指针”
void Push(int x)
{
Node* temp = new Node;
temp->data = x;
temp->link = top;//之前的值在你后面
top = temp;//你作为链表的头结点,同时为栈顶
}
void Pop()
{
if (top == NULL)//空栈
{
cout << "Error:No element to pop!\n";
return;
}
Node* temp = top;//新建一个指向头结点的temp来记录当前的头结点(因为马上要释放它了)
top = top->link;//让头指针和第二个结点建立链接
free(temp);//释放被删除的原头结点
}
void Print()
{
Node* temp = top;
cout << "stack is:\n";
while (temp != NULL)
{
cout << temp->data << endl;
temp = temp->link;
}
}
void IsEmpty();
void Top();
int main()
{
Push(1);//stack:1
Push(4);//stack:4 1
Push(7);//stack:7 4 1
Print();
Pop();//stack:4 1
Print();
return 0;
}
用单链表表示栈的优点:一般不考虑溢出的问题,除非用光计算机的全部内存。
合法输出数目为卡特兰数:即
2.3 栈的应用
(1)反转
a.反转字符串
#pragma once
#include "iostream"//不用加.h
#include <string>
using namespace std;
#include<stack>
void Reverse(char* C, int n)
{
stack<char> S;//声明一个可以传入char类型的栈
for (int i = 0; i < n; i++)//将C数组中元素从首元素压入char类型的栈底下
{
S.push(C[i]);//注意stack标准库中的push是小写(自定义的函数才使用首字母大写)
}
for (int i = 0; i < n; i++)
{
C[i] = S.top();//将栈顶上的元素放前面,实现数组倒序
S.pop();//赋值完,需要把栈顶元素丢掉
}
}
int main()
{
char C[51];
cout << "Please input a string:\n";
cin>>(C);
Reverse(C, strlen(C));//标明string类才能使用strlen
cout << "Output is:";
for (int i = 0; i < strlen(C); i++)
//这里不是字符串string,所以没法采用直接cout<<C的命令
//C中采用%s
cout << C[i];
return 0;
}
b.反转链表
void Reverse()
{
if (head == NULL)
return;
stack<Node*>S;//定义一个存结点地址的栈,这里的Node*,类似于int,放的数据类型
Node* temp = head;//先把head指向head,方便后面压入栈中
while (temp != NULL)
{
S.push(temp);//压进去
temp = temp->next;//往下走,这里原链表没有消失
}
temp = S.top();//针对栈中第一个元素需要特殊对待,因为涉及到头指针
head = temp;//栈顶元素当头指针【这里不能让head直接等于S.top(),因为也需要改变temp的值】
S.pop();//把栈顶元素T走
while (!S.empty())//栈不为空
{
temp->next = S.top();//排一个新链条
S.pop();//弹出
temp->next = temp;//预备排到后面
}
}
(2)检查括号匹配
typedef struct Node
{
int data;
Node* next;
}Node;
typedef struct
{
Node* front, *rear;
}SqStack;
//初始化栈
void InitStack(SqStack& S);
//判断栈是否为空
bool IsEmpty(SqStack S);
//新元素入栈
bool Push(SqStack& S, char x);
//出栈,并用x储存栈顶元素
bool Pop(SqStack& S,char x);
/*利用栈判断括号是否匹配*/
bool BracketCheck(char str[], int length)
{
SqStack S;
for (int i = 0; i < length;i++)
{
if (str[i] == '(' || str[i] == '[' || str[i] == '{')
Push(S, str[i]);
else
{
if (!IsEmpty(S))
return false;
char topElem;
Pop(S, topElem);//弹出来最后栈顶元素,并用topElem储存
if (str[i] == '(' && topElem != ')')
return false;
else if
(str[i] == '[' && topElem != ']')
return false;
else if
(str[i] == '{' && topElem != '}')
return false;//这里最好不要合在一起写,因为逻辑判断顺序可能会乱,要么就是加额外的括号,更乱
}
}
return IsEmpty(S);//上述判断完还不能说明一定成功,最后还需对栈判空决定是否匹配成功
}
ps:
步骤:依次扫描所有字符,遇到左括号入栈,遇右括号则弹出栈顶元素检查是否匹配。
匹配失败情况:
1.右括号单身(右括号进来,发现栈空)
2.左右括号不匹配(进来的右括号发现栈顶元素不是对应的左括号)
3.左括号单身(最后判空,发现栈中含有多的左括号)
(3)栈在表达式求值中的应用【通过栈分别实现机转、机算】
a.中缀转后缀:【手转】(一般是习惯由内而外地写)
1.确定中缀表达式中各个运算符的运算顺序
2.选择下一运算符,按照【左操作数 右操作数 运算符】 的方式组合成一个新操作数
3.如果还有运算符没被处理,就重复步骤2.
由于运算顺序不唯一,因此手算对应的后缀表达式也不唯一。
算法的确定性:机算结果确定,因为有左优先原则,只要左边的运算符能先计算就先左。
b.中缀转后缀的机算方法【机转】(一般是习惯由前而后地写)
/*需要一个栈(存取暂时无法确定运算顺序的运算符或界限符)和一个字符串数组(存放后缀表达式)*/
1.从左到右扫描
a.如果是操作数,则直接存入后缀表达式
b.如果是界限符【括号】,则判断,若是左括号就直接入运算符栈。如果是右括号,则依次弹出运算符栈中运算符,直到弹出第一个左括号为止
c.如果是运算符【+-*/】,则依次弹出栈中高于或等于自己的运算符【若比我低,你凭什么先进后缀表达式先计算,给我在栈里呆着;若不比我低,则按照左优先原则,你是大哥你先算】,并依次弹出放入后缀表达式,若碰到左括号或者栈空则停止(碰到左括号证明需要优先考虑括号里的,别弹了,你也进来呆着吧)。再把当前运算符压入栈。
2.最后判断栈中是否为空,有的话依次弹出
机理其实和我们眼睛看一样,从左往右看,碰到优先级高的【人:最内层小括号】【机器:符号先出现的】就先运算这两个数(注意先压入栈的一定是左操作数),并把结果先放到这。
这样转化后只是改变了运算符的位置,便可以得到无括号的运算式【但是操作数的相对位置没有改变】
后缀的机器求值方法:【机算】
/*需要一个栈:存取暂时无法确定运算顺序的操作数*/
1.从左往右扫描下一个元素,直到处理完所有元素。
2.若扫描到数则压入栈中,并回到步骤1;否则执行步骤3
3.若扫描到运算符,则弹出两个栈顶元素,执行该运算,运算结果压回栈顶,并回到步骤1
中缀的机器求值方法:【机算】
/*需要一个运算符栈(存取暂时无法确定运算顺序的操作符或界限符)和一个操作数栈(存放不确定运算顺序的操作数)*/
1.从左到右扫描
a.如果是操作数,则直接存入后缀表达式
b.如果是运算符或者界限符,则按照“中缀转后缀”的逻辑压入运算符栈(期间也会弹出运算符,若弹出来一个,就得在操作栈弹两个的栈顶元素进行运算,运算结果再压入操作数栈)
2.最后判断运算符栈中是否为空,有的话依次弹出、计算、压回(操作数栈)
三、队列
3.1 定义
只允许在一端插入,在另一端删除的线性表。【一种操作受限的线性表】
3.2 基本操作
创销、增删查改【入队-增、出队-删】
(1)队列的顺序表(数组)实现
方案一:仅利用代码逻辑判断,定义序号指针front和rear 【牺牲一个存储位置】
注:这里rear指向是队尾元素的后一个位置,rear在前面走,front在后面跟
#include <iostream>
using namespace std;
#define MaxSize 10
typedef struct
{
int data[MaxSize];//使用静态数组存放队列元素
int front, rear;//定义两个“指针”来表明序号位置,注意rear指向的位置是即将插入的位置
//即rear指向队尾后一个元素
}SqQueue;//顺序队列
void InitQueue(SqQueue &Q)
{
Q.front = Q.rear = 0;//队头队尾都指向0,这是初始状态
}
void testQueue()
{
SqQueue Q;
InitQueue(Q);
}
bool QueueEmpty(SqQueue Q)
{
if (Q.front = Q.rear)//队空条件
return true;
else
return false;
}
bool EnQueue(SqQueue& Q, int x)//入队操作,不需要取出x,所以不用引用符号
{
if ((Q.rear + 1) % MaxSize == Q.front)//rear的后一位为front时为满队列,否则还可以插入
return false;
else
Q.data[Q.rear] = x;//Q.rear是一个整体,表示序号(类似指针)
Q.rear = (Q.rear + 1) % MaxSize;//先填入,再让rear指针后移。
//【+1表示后移,但是不是简单的后移】这一句让rear的排列升华了,不是线性队列而是“循环队列”。
//后面站不下了,可以站到前面来,直到全部占满
}
bool DeQueue(SqQueue& Q, int &x)//出队操作,需要让参数x改变,所以要加引用符号
{
if (Q.rear == Q.front)
return false;//队空
x = Q.data[Q.front];//[出队头,入队尾]
Q.front=(Q.front+1)%MaxSize;//每出队一个元素,front指针往后移一位
return true;
}
队列元素个数: (rear+MaxSize-front)%MaxSize
方案二:利用辅助变量size,记录元素个数
入队一个元素时,size++;出队一个元素时,size--;
在front和rear相等的情况下,可以分Size==MaxSize和Size==0的情况。
方案三:增加辅助变量tag,记录刚刚的动作是插入还是删除
队满条件:front==rear&&tag==1;//tag=1表示刚刚插入了数据
队空条件:front==rear&&tag==0;//tag=0表示刚刚删除了数据
ps:如果rear指向的不是队尾元素的后一个位置,而直接指向的队尾元素呢?
1.入队操作:(先移后填)
让front指向0,而rear指向n-1的位置【如10 个元素,则front在0,rear在9】
先后移rear,再填入。【填入第一个元素时,rear又指向了0,再填】
2.出队操作:
这个没有变化,还是先出后移
3.判空:
(Q.rear+1)%MaxSize==Q.front
4.判满:
a:牺牲最后一个存储单元【不能填入9这个单元,反之如果可以填入9,则和队空条件一致,无法区分】
b:增加辅助变量(size或者tag)
(2)队列的链式实现
优点:永远不用怕存满【除非把计算机内存用完,基本不会,所以入队不需要判满】
基本信息:
#include <iostream>
using namespace std;
typedef struct Node
{
int data;
Node* next;
}Node;//每个结点类型
typedef struct
{
Node* front, *rear;//定义链式队列中的两个指针,分别指向头结点和尾结点
//【注意rear前面也要有*】
}LinkQueue;
void InitQueue(LinkQueue& Q)//链式不用判满,只会判空,所以这里不用bool
{
Q.front = Q.rear = new Node;//让头指针和尾指针都先指向头结点
Q.front->next = NULL;//头结点的后面为空
}
void testQueue()
{
LinkQueue Q;//声明一个链式队列【未分配内存】
InitQueue(Q);//初始化后,给Q分配了内存
//后续操作...
}
方案一:带头结点
bool IsEmpty(LinkQueue Q)
{
if (Q.front == Q.rear)
//if(Q.front==NULL)时,队列也为空
return true;
else
return false;
}
void EnQueue(LinkQueue& Q, int x)
{
Node* temp = new Node;
//malloc用法:Node* temp=(Node*)malloc(sizeof(Node ))
temp->data = x;
temp->next = NULL;
Q.rear->next = temp;//你作为我的最后一个结点
Q.rear = temp;//rear指针指向你
}
bool DeQueue(LinkQueue& Q, int& x)//出队操作有可能失败,用bool类型
{
if (Q.front = Q.rear)
return false;
Node* temp = Q.front->next;
x = temp->data;
Q.front->next = temp->next;
if (Q.rear == temp)
Q.rear = Q.front;
free(temp);//别忘了释放不用的结点空间
return true;
}
方案二:不带头结点
void EnQueue(LinkQueue &Q,int x)
{
Node* temp = new Node;
temp->data = x;
temp->next = NULL;
if (Q.front == NULL)//插入第一个结点时要特别注意
{
Q.front = temp;
Q.rear = temp;
}
else//如果插入之前有结点,则跟之前一样
{
Q.rear->next = temp;
Q.rear = temp;
}
}
3.3 队列的应用
(1)树的层次遍历
后面入队加children,前面出队parent
(2) 图的广度优先遍历
入队相邻的,出队已有的
(3)队列在操作系统中的应用
先来先服务(FCFS,First Come First Service),一个CPU处理多个程序
三、特殊矩阵的压缩存储
1.一维数组 int a[i],
则 location(a[i])=location(a[0])+i*sizeof(int);
2.二维数组int b[i][j]
若采取行优先存储,则 location(b[i][j])=location{a[0][0]}+(i*M+j)*sizeof(int);【M为每排元素个数】
若采取列优先存储,则 location(b[i][j])=location{a[0][0]}+(j*M+i)*sizeof(int);【M为每排元素个数】
3.特殊矩阵【可以压缩存储空间】
(1)对称矩阵
1.基础知识:n阶矩阵中aij=aji;所以只需储存主对角线和上三角数据。
2.数据总大小:1+2+3+...+n,所以一共有(n+1)*n/2个数据。数组下标从0至(n+1)*n/2-1
3.如何应用(访问):实现一个“映射”函数,实现从矩阵下标【aij】到一位数组下标【Bk】
按照行优先原则:现在是(1+2+3...+(i-1)+j)元素,即k=(i*(i-1)+j)-1
(2)三角矩阵
1.基础知识:上三角矩阵是指除了主对角线和上三角区(乱的),其他元素都相同(常数c)
2.数据总大小:比上一个内存多1,即数组下标从0至(n+1)*n/2,因为还需要一个空间存c
3.以下三角为例:
若i<j , 则k=(i*(i-1)+j)-1,和上面一样不变
若i>j , 则k=(n+1)*n/2,因为常数的永远存在最后一个
上三角:
若i<j,则k=(i-1)*(2n-i+2)/2
若i>j,则k=(n+1)*n/2,因为常数的永远存在最后一个
(3)三对角矩阵
1.又称带状矩阵
当|i-j|>1时,有aij=0 (
)【外面的全为0】
2.数据总大小:3n-2【第一行和最后一行少个元素,其余都是3个元素】
3.已知行列号aij,如何得到数组下标k:
(1)当i=1时,aij是第j-i+1个元素
(2)当i>1时,aij的前i-1行有3(i-1)-1【先假设全满,再减去一个】
aij是第i行的第j-i+2个元素
所以aij是第2i+j-2个元素
k=2i+j-3
已知数组下标k,如何得到行列号aij:
数组下标k,则是第k+1个元素
前(i-1)行一共有3(i-1)-1;
前i行一共有3i-1;
若该元素刚好在第i行,则有(3i-4)<=k+1<(3i-1)
(4)稀疏矩阵(0太多了,有效元素少)
1.顺序存储——用三元组(struct)存“行、列、值”
2.十字链表法——利用非零结点定义结点形成链表
typedef struct Node { int row; int line; int value; Node* down; Node* right; }Node;