1.栈
1.1 栈的基本概念
- 只允许在一端(栈顶top)进行插入或删除操作的受限的线性表。
- 后进先出(Last In First Out)LIFO。或者说先进后出FILO。
进栈顺序:a1 > a2 > a3 > a4 > a5
出栈顺序:a5 > a4 > a3 > a2 > a1
1.2 栈的基本操作
InitStack(&S):初始化栈。构造一个空栈 S,分配内存空间。
DestroyStack(&S):销毁栈。销毁并释放栈 S 所占用的内存空间。
Push(&S, x):进栈。若栈 S 未满,则将 x 加入使其成为新的栈顶元素。
Pop(&S, &x):出栈。若栈 S 非空,则弹出(删除)栈顶元素,并用 x 返回。
GetTop(S, &x):读取栈顶元素。若栈 S 非空,则用 x 返回栈顶元素。
StackEmpty(S):判空。断一个栈 S 是否为空,若 S 为空,则返回 true,否则返回 false。
1.2.1 栈的顺序存储实现
![](https://i-blog.csdnimg.cn/blog_migrate/ddcdb89ee03cfe4d7562897a54c0bcac.png)
【顺序栈的定义】
#define MaxSize 10 //定义栈中元素的最大个数
typedef struct{
ElemType data[MaxSize]; //静态数组存放栈中元素
int top; //栈顶元素
}SqStack;
void testStack(){
SqStack S; //声明一个顺序栈(分配空间)
//连续的存储空间大小为 MaxSize*sizeof(ElemType)
}
【顺序栈的初始化】
#define MaxSize 10
typedef struct{
ElemType data[MaxSize];
int top;
}SqStack;
// 初始化栈顶为-1,栈顶指针指向栈顶
void InitStack(SqStack &S){
S.top = -1; //初始化栈顶指针
}
// 判断栈是否为空
bool StackEmpty(SqStack S){
if(S.top == -1)
return true;
else
return false;
}
// 初始化栈顶为0,栈顶指针指向栈顶的下一个空位
void InitStack(SqStack &S){
S.top = 0; //初始化栈顶指针
}
// 判断栈是否为空
bool StackEmpty(SqStack S){
if(S.top == 0)
return true;
else
return false;
}
【顺序栈的入栈出栈】
初始化为-1时
// 新元素进栈
bool Push(SqStack &S, ElemType x){
if(S.top == MaxSize - 1) // 判断栈是否已满
return false;
S.data[++S.top] = x;
return true;
}
// 出栈
bool Pop(SqStack &x, ElemType &x){
if(S.top == -1) // 判断栈是否为空
return false;
x = S.data[S.top--];
return true;
}
初始化为0时
// 新元素进栈
bool Push(SqStack &S, ElemType x){
if(S.top == MaxSize) // 判断栈是否已满
return false;
S.data[S.top++] = x;
return true;
}
// 出栈
bool Pop(SqStack &x, ElemType &x){
if(S.top == 0) // 判断栈是否为空
return false;
x = S.data[--S.top];
return true;
}
【读取栈顶元素】
// 读栈顶元素
初始化为-1时
bool GetTop(SqStack S, ElemType &x){
if(S.top == -1) 先判空,非空读取才有意义
return false;
x = S.data[S.top];
return true;
}
初始化为-1时
bool GetTop(SqStack S, ElemType &x){
if(S.top == 0)
return false;
x = S.data[S.top-1];
return true;
}
【读取栈的长度】
// 获取当前栈长
当初始化为-1
int GetSize(SqStack S){
return S.top + 1;
}
当初始化为0
int GetSize(SqStack S){
return S.top;
}
共享栈(两个栈共享同一片空间)】
- 共享栈--特殊的顺序栈
- 将栈底设计在共享空间的两端,栈顶向中间靠拢
#define MaxSize 10 //定义栈中元素的最大个数
typedef struct{
ElemType data[MaxSize]; //静态数组存放栈中元素
int top0; //0号栈栈顶指针
int top1; //1号栈栈顶指针
}ShStack;
//初始化栈
void InitSqStack(ShStack &S){
S.top0 = -1; //初始化栈顶指针
S.top1 = MaxSize;
}
1.2.2 栈的链式存储
【链栈的定义】
- 定义:采用链式存储的栈称为链栈。
- 优点:链栈的优点是便于多个栈共享存储空间和提高其效率,且不存在栈满上溢的情况。
- 特点:进栈和出栈都只能在栈顶一端进行(链头作为栈顶)
链表的头部作为栈顶,意味着:
- 1. 在实现数据"入栈"操作时,需要将数据从链表的头部插入;
- 2. 在实现数据"出栈"操作时,需要删除链表头部的首元节点;
因此,链栈实际上就是一个只能采用头插法插入或删除数据的链表;
栈的链式存储结构可描述为:
【链栈的定义】
typedef struct Linknode{
ElemType data; //数据域
Linknode *next; //指针域
}Linknode,*LiStack;
void testStack(){
LiStack L; //声明一个链栈
}
【链栈的初始化】
typedef struct Linknode{
ElemType data;
Linknode *next;
}Linknode,*LiStack;
// 初始化栈
bool InitStack(LiStack &L){ // 生成虚拟头节点,并将其next指针置空
L = (Linknode *)malloc(sizeof(Linknode));
if(L == NULL)
return false;
L->next = NULL;
return true;
}
// 判断栈是否为空
bool isEmpty(LiStack &L){
if(L->next == NULL)
return true;
else
return false;
}
【入栈出栈】
// 新元素入栈
bool pushStack(LiStack &L,ElemType x){
Linknode *s = (Linknode *)malloc(sizeof(Linknode));
if(s == NULL)
return false;
s->data = x;
// 头插法
s->next = L->next;
L->next = s;
return true;
}
// 出栈
bool popStack(LiStack &L, int &x){
// 栈空不能出栈
if(L->next == NULL)
return false;
Linknode *s = L->next;
x = s->data;
L->next = s->next;
free(s);
s = NULL;
return true;
}
2. 队列
2.1 队列的基本概念
- 只允许在表的一端(队尾)插入,表的另一端(队头)进行删除操作的受限的线性表。
- 特点:先进先出(先入队的元素先出队)、FIFO(First In First Out),后入后出LILO。
2.2 队列的基本操作
InitQueue(&Q):初始化队列,构造一个空队列Q。
QueueEmpty(Q):判队列空,若队列Q为空返回true,否则返回false。
EnQueue(&Qx):入队,若队列Q未满,则将x加入使之成为新的队尾。
DeQueue(&Q&x):出队,若队列Q非空,则删除队头元素,并用x返回。
GetHead(Q&x):读队头元素,若队列Q非空则用x返回队头元素。
ClearQueue(&Q):销毁队列,并释放队列Q占用的内存空间。
【队列的顺序存储实现 】
队头指针:指向队头元素
队尾指针:指向队尾元素或者队尾的下一个位置
【顺序队列的定义】
#define MaxSize 10; //定义队列中元素的最大个数
typedef struct{
ElemType data[MaxSize]; //用静态数组存放队列元素
int front, rear; //队头指针和队尾指针
}SqQueue;
void test{
SqQueue Q; //声明一个队列
}
【顺序队列的初始化】
#define MaxSize 10;
typedef struct{
ElemType data[MaxSize];
int front, rear;
}SqQueue;
// 初始化队列
void InitQueue(SqQueue &Q){
// 初始化时,队头、队尾指针指向0
// 队尾指针指向的是即将插入数据的数组下标
// 队头指针指向的是队头元素的数组下标
Q.rear = Q.front = 0;
}
// 判断队列是否为空
bool QueueEmpty(SqQueue Q){
if(Q.rear == Q.front)
return true;
else
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;
}
- 循环队列不能直接使用Q.rear == Q.front作为判空的条件,因为当队列已满时也符合该条件,会与判空发生冲突!
解决方法一:牺牲一个单元来区分队空和队满,即将(Q.rear+1)%MaxSize == Q.front作为判断队列是否已满的条件。(主流方法)
解决方法二:设置 size 变量记录队列长度。
#define MaxSize 10;
typedef struct{
ElemType data[MaxSize];
int front, rear;
int size;
}SqQueue;
// 初始化队列
void InitQueue(SqQueue &Q){
Q.rear = Q.front = 0;
Q.size = 0;
}
// 判断队列是否为空
bool QueueEmpty(SqQueue 0){
if(Q.size == 0)
return true;
else
return false;
}
// 新元素入队
bool EnQueue(SqQueue &Q, ElemType x){
if(Q.size == MaxSize)
return false;
Q.size++;
Q.data[Q.rear] = x;
Q.rear = (Q.rear+1)%MaxSize;
return true;
}
// 出队
bool DeQueue(SqQueue &Q, ElemType &x){
if(Q.size == 0)
return false;
Q.size--;
x = Q.data[Q.front];
Q.front = (Q.front+1)%MaxSize;
return true;
}
解决方法三:设置 tag 变量记录队列最近的操作。(tag=0
:最近进行的是删除操作;tag=1
:最近进行的是插入操作)
#define MaxSize 10;
typedef struct{
ElemType data[MaxSize];
int front, rear;
int tag;
}SqQueue;
// 初始化队列
void InitQueue(SqQueue &Q){
Q.rear = Q.front = 0;
Q.tag = 0;
}
// 判断队列是否为空,只有tag==0即初始化或者出队后才可能为空
bool QueueEmpty(SqQueue 0){
if(Q.front == Q.rear && Q.tag == 0)
return true;
else
return false;
}
// 新元素入队 判断队列是否满,只有tag==1即入队后才可能满
bool EnQueue(SqQueue &Q, ElemType x){
if(Q.rear == Q.front && tag == 1)
return false;
Q.data[Q.rear] = x;
Q.rear = (Q.rear+1)%MaxSize;
Q.tag = 1;
return true;
}
// 出队
bool DeQueue(SqQueue &Q, ElemType &x){
if(Q.rear == Q.front && tag == 0)
return false;
x = Q.data[Q.front];
Q.front = (Q.front+1)%MaxSize;
Q.tag = 0;
return true;
}
【获得队头元素】
// 获取队头元素并存入x
bool GetHead(SqQueue &Q, ElemType &x){
if(Q.rear == Q.front)
return false;
x = Q.data[Q.front];
return true;
}
队列的链式存储实现
【链队列的定义】
// 链式队列结点
typedef struct LinkNode{
ElemType data;
struct LinkNode *next;
}
// 链式队列
typedef struct{
// 头指针和尾指针
LinkNode *front, *rear;
}LinkQueue;
【 链队列的初始化(带头结点)】
typedef struct LinkNode{
ElemType data;
struct LinkNode *next;
}LinkNode;
typedef struct{
LinkNode *front, *rear;
}LinkQueue;
// 初始化队列
void InitQueue(LinkQueue &Q){
// 初始化时,front、rear都指向头结点
Q.front = Q.rear = (LinkNode *)malloc(sizeof(LinkNode));
Q.front -> next = NULL;
}
// 判断队列是否为空
bool IsEmpty(LinkQueue Q){
if(Q.front == Q.rear)
return true;
else
return false;
}
【入队出队(带头结点)】
// 新元素入队
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;
}
// 队头元素出队
bool DeQueue(LinkQueue &Q, ElemType &x){
if(Q.front == Q.rear) // 判空
return false;
LinkNode *p = Q.front->next;
x = p->data;
Q.front->next = p->next;
// 如果p是最后一个结点,此时Q.rear已经要被删除了,则将队尾指针也指向队首指针
if(Q.rear == p)
Q.rear = Q.front;
free(p);
p = NULL;
return true;
}
【不带头结点的链队列操作】
typedef struct LinkNode{
ElemType data;
struct LinkNode *next;
}LinkNode;
typedef struct{
LinkNode *front, *rear;
}LinkQueue;
// 初始化队列
void InitQueue(LinkQueue &Q){
// 不带头结点的链队列初始化,头指针和尾指针都指向NULL
Q.front = NULL;
Q.rear = NULL;
}
// 判断队列是否为空
bool IsEmpty(LinkQueue Q){
if(Q.front == NULL)
return true;
else
return false;
}
// 新元素入队
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;
}
}
//队头元素出队
bool DeQueue(LinkQueue &Q, ElemType &x){
if(Q.front == NULL)
return false;
LinkNode *s = Q.front;
x = s->data;
if(Q.front == Q.rear){
Q.front = Q.rear = NULL;
}else{
Q.front = Q.front->next;
}
free(s);
return true;
}
双端队列
双端队列定义
- 双端队列是允许从两端插入、两端删除的线性表。
- 如果只使用其中一端的插入、删除操作,则等同于栈。
- 输入受限的双端队列:允许一端插入,两端删除的线性表。
- 输出受限的双端队列:允许两端插入,一端删除的线性表。
双端队列考点:判断输出序列的合法化
- 例:数据元素输入序列为 1,2,3,4,判断 4! = 24 个输出序列的合法性
输入受限的双端队列:只有 4213 和 4231 不合法
输出受限的双端队列:只有 4132 和 4231 不合法
3. 栈与队列的应用
3.1栈在括号匹配中的应用
- 用栈实现括号匹配:
- 最后出现的左括号最先被匹配 (栈的特性——LIFO)。
- 遇到左括号就入栈(可以把对应右括号入栈,这样遇到右括号只需要判断是否和栈顶相等。
- 遇到右括号,就“消耗”一个左括号(出栈)。
- 匹配失败情况:
- 扫描到右括号且栈空,则该右括号单身。
- 扫描完所有括号后,栈非空,则该左括号单身。
- 左右括号不匹配。
#define MaxSize 10
typedef struct{
char data[MaxSize];
int top;
}SqStack;
void InitStack(SqStack &S);
bool StackEmpty(SqStack &S);
bool Push(SqStack &S, char x);
bool Pop(SqStack &S, char &x);
// 判断长度为length的字符串str中的括号是否匹配
bool bracketCheck(char str[], int length){
SqStack S;
InitStack(S);
// 遍历str
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;
// 用topElem接收栈顶元素
Pop(S, topElem);
// 括号不匹配
if(str[i] == ')' && topElem != '(' )
return false;
if(str[i] == ']' && topElem != '[' )
return false;
if(str[i] == '}' && topElem != '{' )
return false; }
}
// 扫描完毕若栈空则说明字符串str中括号匹配
return StackEmpty(S);
}
3.2栈在表达式求值中的应用
中缀表达式:中缀表达式是一种通用的算术或逻辑公式表示方法,运算符以中缀形式处于操作数的中间。对于计算机来说中缀表达式是很复杂的,因此计算表达式的值时,通常需要先将中缀表达式转换为前缀或后缀表达式,然后再进行求值。
前缀表达式(波兰表达式):前缀表达式的运算符位于两个操作数之前。
后缀表达式(逆波兰表达式):后缀表达式的运算符位于两个操作数之后。
中缀表达式转后缀表达式-手算
步骤1: 确定中缀表达式中各个运算符的运算顺序
步骤2: 选择下一个运算符,按照[左操作数 右操作数 运算符]的方式组合成一个新的操作数
步骤3: 如果还有运算符没被处理,继续步骤2
“左优先”原则: 只要左边的运算符能先计算,就优先算左边的 (保证运算顺序唯一);
中缀:A + B - C * D / E + F
① ④ ② ③ ⑤
后缀:A B + C D * E / - F +
后缀表达式转中缀的计算—手算:
从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应的运算,合体为一个操作数
后缀表达式的计算—机算
用栈实现后缀表达式的计算(栈用来存放当前暂时不能确定运算次序的操作数)
步骤1: 从左往后扫描下一个元素,直到处理完所有元素;
步骤2: 若扫描到操作数,则压入栈,并回到步骤1;否则执行步骤3;
步骤3: 若扫描到运算符,则弹出两个栈顶元素,执行相应的运算,运算结果压回栈顶,回到步骤1;
中缀表达式转后缀表达式(机算)
初始化一个栈,用于保存暂时还不能确定运算顺序的运算符从左到右处理各个元素,直到末尾。可能遇到三种情况:
1.遇到操作数:直接加入后缀表达式。
2.遇到界限符:遇到“(”直接入栈;遇到“)”则依次弹出栈内运算符并加入后缀表达式,直到 弹出“(”为止。注意:“(”不加入后缀表达式。
3.遇到运算符:依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式, 若碰到“(” 或栈空则停止。之后再把当前运算符入栈。
#define MaxSize 40
typedef struct{
char data[MaxSize];
int top;
}SqStack;
typedef struct{
char data[MaxSize];
int front,rear;
}SqQueue;
void InitStack(SqStack &S);
bool StackEmpty(SqStack S);
bool Push(SqStack &S, char x);
bool Pop(SqStack &S, char &x);
void InitQueue(SqQueue &Q);
bool EnQueue(LQueue &Q, char x);
bool DeQueue(LQueue &Q, char &x);
bool QueueEmpty(SqQueue Q);
// 判断元素ch是否入栈
int JudgeEnStack(SqStack &S, char ch){
char tp = S.data[S->top];
// 如果ch是a~z则返回-1
if(ch >= 'a' && ch <= 'z')
return -1;
// 如果ch是+、-、*、/且栈顶元素优先级大于等于ch则返回0
else if(ch == '+' && (tp == '+' || tp == '-' || tp == '*' || tp == '/'))
return 0;
else if(ch == '-' && (tp == '+' || tp == '-' || tp == '*' || tp == '/'))
return 0;
else if(ch == '*' && (tp == '*' || tp == '/'))
return 0;
else if(ch == '/' && (tp == '*' || tp == '/'))
return 0;
// 如果ch是右括号则返回2
else if(ch == ')')
return 2;
// 其他情况ch入栈,返回1
else return 1;
}
// 中缀表达式转后缀表达式
int main(int argc, char const *argv[]) {
SqStack S;
SqQueue Q;
InitStack(S);
InitQueue(Q);
char ch;
printf("请输入表达式,以“#”结束:");
scanf("%c", &ch);
while (ch != '#'){
// 当栈为空时
if(StackEmpty(&S)){
// 如果输入的是数即a~z,直接入队
if(ch >= 'a' && ch <= 'z')
EnQueue(Q, ch);
// 如果输入的是运算符,直接入栈
else
Puch(S, ch);
}else{
// 当栈非空时,判断ch是否需要入栈
int n = JudgeEnStack(S, ch);
// 当输入是数字时直接入队
if(n == -1){
EnQueue(Q, ch);
}else if(n == 0){
// 当输入是运算符且运算符优先级不高于栈顶元素时
while (1){
// 取栈顶元素入队
char tp;
Pop(S, tp);
EnQueue(Q, tp);
// 再次判断是否需要入栈
n = JudgeEnStack(S, ch);
// 当栈头优先级低于输入运算符或者栈头为‘)’时,入栈并跳出循环
if(n != 0){
EnStack(S, ch);
break;
}
}
}else if(n == 2){
// 当出现‘)’时 将()中间的运算符全部出栈入队
while(1){
char tp;
Pop(S, tp);
if(tp == '(')
break;
else
EnQueue(Q, tp);
}
}else{
// 当运算符优先级高于栈顶元素或出现‘(’时直接入栈
Push(S, ch);
}
}
scanf("%c", &ch);
}
// 将最后栈中剩余的运算符出栈入队
while (!StackEmpty(S)){
char tp;
Pop(S, tp);
EnQueue(Q, tp);
}
// 输出队中元素
while (!QueueEmpety(Q)){
printf("%c ", DeQueue(Q));
}
return 0;
}
用栈实现中缀表达式的计算:
1.初始化两个栈,操作数栈和运算符栈;
2.若扫描到操作数,压入操作数栈;
3.若扫描到运算符或界限符,则按照“中缀转后缀”相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应运算,运算结果再压回操作数栈)
3.3栈在递归中的应用
函数调用的特点:最后被调用的函数最先执行结束(LIFO)
函数调用时,需要用一个栈存储:
- 调用返回地址
- 实参
- 局部变量
递归调用时,函数调用栈称为 “递归工作栈”:
每进入一层递归,就将递归调用所需信息压入栈顶;
每退出一层递归,就从栈顶弹出相应信息;
缺点:太多层递归可能回导致栈溢出;适合用“递归”算法解决:可以把原始问题转换为属性相同,但规模较小的问题
3.4队列的应用
- 队列应用:树的层次遍历
- 队列应用:图的广度优先遍历
- 队列应用:操作系统中多个进程争抢着使用有限的系统资源时,先来先服务算法(First Come First Service)是是一种常用策略。
3.5 特殊矩阵的压缩存储
3.5.1 数组的存储
1. 一维数组的存储:
各数组元素大小相同,且物理上连续存放。设起始地址为LOC,则数组元素的存放地址 = LOC + i * sizeof(ElemType) (0≤i<10)
如果询问是第 i个元素(从 1到n ,注意和下标的差别,与下标相差1 ),此时第i 个元素的存放地址为 LOC + (i-1) * sizeof(ElemType) ,此处 1≤i<=10
2. 二维数组的存储 :
设二维数组 A[m][n] 按行优先存储, 每个元素占 p 个字节,
则 Loc(i, j) 的地址为 (i * n + j) * p, 第 i 行前面有0到i-1共 i 行, 每行有 n 个元素, 加上 第 i 行前面 j 个元素,所以地址 为 (i * n + j) * p,
注意如果采用i从0-9,j从0-8此时m= 10, n= 9;
1. 若 j 从下标 1 开始, 则 Loc(i, j) = (i * n + j - 1) * p
第 i 行的 第 j 个元素,在第 i 行中 前面只有 j - 1 个元素,
2. 若 i 从下标 1开始, 则 Loc(i, j) = ((i - 1) * n + j) * p
3. 若 i, j 均从 下标 1 开始, 则 Loc(i, j) = ((i - 1) * n + j - 1) * p
若该数组按列优先存储,
则 Loc(i, j) 为 (j * m + i) * p, 第 j 列前面有 j 列,每列有 m 个元素, 加上 第 j 列前的 i 个元素,所以为 (j * m + i) * p
1. 若 j 从下标 1 开始, 则 Loc(i, j) = ((j - 1) * m + i) * p;
因为 第 j 列前面只有 (j - 1) 列
2. 若 i 从下标 1开始, 则 Loc(i, j) = (j * m + i - 1) * p
第 i 个元素前面实际上只有 i - 1 个元素
3. 若 i, j 均从 下标 1 开始, 则 Loc(i, j) = ((j - 1) * m + i - 1) * p
总结:
按行优先 Loc(i, j) = (i * n + j) * p, 按列优先 Loc(i, j) = (j * m + i) * p, 行从下标1 开始 i 就减一, 列从下标 1 开始 , j 就减一
一维数组与二维数组下标的转换(下标是从0开始的)
- 一维数组转换为二维数组
row = index / m;
col = index % m;
- 二维数组转换为一维数组
index = row * m + col;
3.5.2对称矩阵的压缩存储
对称矩阵的压缩存储:若n阶方阵中任意一个元素,都有
则该矩阵为对称矩阵,对于对称矩阵,只需存储主对角线+下(上)三角区。若按照行优先原则将各元素存入一维数组中,即
存入到数组
中,那么数组
共有
个元素。
1.行优先且以下三角区
和主对角线
为核心
若想求出对应的 的映射一维数组下标(此处下标是从 0开始的),先计算出
前面有多少个元素(前
-1行总共有
个元素,第
行有
个元素),坐标为前面元素个数-1;
若想求出对应的 的映射一维数组下标(此处下标是从 0开始的),先计算出
前面有多少个元素(由于对称矩阵可看作是
进行交换);
对于k,有:
若是求A[i][j]对应的下标k,(即i,j下标是从 0开始的) ,则对应的i,j要+1,
此时对于k,有:
2. 行优先且以上三角区和主对角线为核心
若想求出对应的 的映射一维数组下标(此处下标是从 0开始的),先计算出
前面有多少个元素(前
-1行总共有
个元素(高斯错位相加法),第
行有
个元素),坐标为前面元素个数-1;
若想求出对应的 的映射一维数组下标(此处下标是从 0开始的),先计算出
前面有多少个元素(由于对称矩阵可看作是
进行交换);
对于k,有:
若是求A[i][j]对应的下标k,(即i,j下标是从 0开始的) ,则对应的i,j要+1,才是实际序号
3.列优先且以下三角区
和主对角线
为核心
若是求A[i][j]对应的下标k,(即i,j下标是从 0开始的) ,则对应的i,j要+1,才是实际序号
4. 列优先且以上三角区
和主对角线
为核心
若是求A[i][j]对应的下标k,(即i,j下标是从 0开始的) ,则对应的i,j要+1,才是实际序号
3.5.3三角矩阵的压缩存储
将主对角线+下三角区存入一维数组中,并在最后一个位置存储常量。即存入到数组
中,那么数组
共有
+ 1个元素。
1.下三角矩阵:
定义:除了主对角线和下三角区,其余的元素都相同。
压缩存储策略:可参考对称矩阵( 1行优先, 3列优先)的存储
若是求A[i][j]对应的下标pos,(即i,j下标是从 0开始的) ,则对应的i,j要+1,才是实际序号 ,若B从下标1开始存,k=pos+1
2.上三角矩阵:
定义:除了主对角线和上三角区,其余的元素都相同。
压缩存储策略:可参考对称矩阵(2 行优先, 4列优先)的存储
若是求A[i][j]对应的下标pos,(即i,j下标是从 0开始的) ,则对应的i,j要+1,才是实际序号 ,若B从下标1开始存,k=pos+1
3.5.4三对角矩阵的压缩存储
三对角矩阵,又称带状矩阵: 当时,有
,
压缩后的元素个数是 。
若想求出对应的 的映射一维数组下标(此处下标是从0开始的),先计算出
前面有多少个元素(前
行总共有
个元素,第
行有
个元素,其中
在第i行前有
个,注意:算坐标时该元素个数也要计算,算内存地址时只计算该元素前面的元素个数);从0开始的坐标等于包括该元素在内前面元素个数-1,也等于
前面元素的个数(不包含自己);(只存储带状部分,其余部分无定义,不在一维数组中保存,默认为0)
一维数组从1开始存的坐标等于包括该元素在内前面元素个数(只存储带状部分,其余部分无定义,不在一维数组中保存,默认为0)
特殊考虑第一行代入公式亦成立,
若一维数组下标从1开始的,需要整体+1,若是求A[i][j]对应的下标k,(即i,j下标是从 0开始的) ,则对应的i,j要+1,才是实际序号
若已知数组下标k,则 ,
。
若是求A[i][j]对应的下标pos,(即i,j下标是从 0开始的) ,则对应的i,j要+1,才是实际序号 ,若B从下标1开始存,k=pos+1hjb
3.5.5 稀疏矩阵的压缩存储
设矩阵元素个数为m ,非零元素的个数为n ,其中非零元素极少,即 ,称为稀疏矩阵.只存储
非零元素
进行空间压缩。
压缩存储策略:
1. 顺序存储:三元组 <行,列,值>
为了运算方便,矩阵的行数,列数,非零元素的个数也同时存储。
2. 链式存储:十字链表法
3.6 汉诺塔问题
3.6.1 汉诺塔问题的规则
假设有 A、B、C 三根柱子。其中在 A 柱子上,从下往上有 N 个从大到小叠放的盘子。我们的目标是,希望用尽可能少的移动次数,把所有的盘子由 A 柱移动到 C 柱。过程中,每次只能移动一个盘子,且在任何时候,大盘子都不可以在小盘子上面。
3.6.2 递归实现
算法思想:
n个盘子时:
- 把n-1个圆盘从A经过C移到B上
- 把第n个圆盘从A移到C上
- 把n-1个小圆盘经过A从B移到C上
代码实现:
#include<iostream>
using namespace std;
int times;
void move(char x,char y)
{
cout<<x<<"-->"<<y<<endl;
times++;
}
void hanoi(int n,char A,char B,char C)
{ //将n个盘子从A座借助B座移动到C座
if(n==1)
move(A,C);
else
{
hanoi(n-1,A,C,B);
move(A,C);
hanoi(n-1,B,A,C);
}
}
int main()
{
int n;
cin>>n;
hanoi(n,'A','B','C');
cout << times;
return 0;
}
3.6.2汉诺塔的非递归实现
用栈来模拟递归过程
#include <iostream>
#include <stack>
using namespace std;
int times;
// 用note来记录一件要解决的事,将n个盘子从a借助b移动到c。
struct note
{
int n;
char a;
char b;
char c;
};
// 要解决的问题集合,当栈为空即全部已解决。
stack<note> st;
void Move(char A, char C)
{
cout << A << " -> " << C << endl;
times++;
}
void Hanio(int n)
{
st.push(note{n, 'a', 'b', 'c'}); // 初始化,此时栈内只有一个大问题,就是n个盘子从a到c.
while (!st.empty())
{
note t = st.top();
st.pop();
// 上面两行是取出栈顶问题,进行解决。
if (t.n == 1) // 如果只有一个盘子,直接移动。
{
Move(t.a, t.c);
}
else
{
// 如果n!=1,将问题分为三个小问题入栈。!!!注意入栈顺序使最先执行的在栈顶。
st.push(note{t.n - 1, t.b, t.a, t.c});
st.push(note{1, t.a, t.b, t.c});
st.push(note{t.n - 1, t.a, t.c, t.b});
}
}
}
int main()
{
int n;
cin >> n;
Hanio(n);
cout << times << endl;
return 0;
}
3.6.3 算法分析
在这个过程中,问题由全部 N 个盘子由 A 移动到 C,转变为 N-1 个“合并盘”从 A 移动到 B 再移动 C。新的问题和原问题是完全一致的,但盘子数量由 N 个减少为 N-1 个。如果继续用上面的思想,就能把 N-1 个“合并盘”再度减少为 N-2 个,直到只剩一个。
根据我们第一次的分解可知 H(N)=H(N-1)+1+H(N-1)。
求解递推公式可得,所以汉诺塔问题的时间复杂度为O(2^n)
在解决汉诺塔问题的过程中,递归栈的深度等于递归调用的层数,即 n。
汉诺塔问题的空间复杂度为 O(n),其中 n 是圆盘的数量。这是因为在解决问题的过程中,我们需要使用一个递归栈来保存每个递归调用的状态。由于递归栈的深度等于递归调用的层数,因此空间复杂度为 O(n)。
3.7 用栈实现队列
3.7.1 要求
使用栈实现队列的下列操作:
- push(x) -- 将一个元素放入队列的尾部。
- pop() -- 从队列首部移除元素。
- peek() -- 返回队列首部的元素。
- empty() -- 返回队列是否为空。
说明:
- 你只能使用标准的栈操作 -- 也就是只有 push to top, peek/pop from top, size, 和 isempty 操作是合法的。
- 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。
- 假设所有操作都是有效的 (例如,一个空的队列不会调用 pop 或者 peek 操作)。
3.7.2 算法思想
需要两个栈一个输入栈,一个输出栈
- push(x) -- 在push数据的时候,只要数据放进输入栈就好,
- pop() -- 但在pop的时候,操作就复杂一些,输出栈如果为空,就把进栈数据全部导入进来(注意是全部导入),再从出栈弹出数据,如果输出栈不为空,则直接从出栈弹出数据就可以了。
- peek() -- pop() 和 peek()两个函数功能类似,代码实现上也是类似的,只不过不需要弹出,可以调用pop弹出后再压入.
- empty() -- 最后如何判断队列为空呢?如果进栈和出栈都为空的话,说明模拟的队列为空了。
class MyQueue {
public:
stack<int> stIn;
stack<int> stOut;
/** Initialize your data structure here. */
MyQueue() {
}
/** Push element x to the back of queue. */
void push(int x) {
stIn.push(x);
}
/** Removes the element from in front of queue and returns that element. */
int pop() {
// 只有当stOut为空的时候,再从stIn里导入数据(导入stIn全部数据)
if (stOut.empty()) {
// 从stIn导入数据直到stIn为空
while(!stIn.empty()) {
stOut.push(stIn.top());
stIn.pop();
}
}
int result = stOut.top();
stOut.pop();
return result;
}
/** Get the front element. */
int peek() {
int res = this->pop(); // 直接使用已有的pop函数
stOut.push(res); // 因为pop函数弹出了元素res,所以再添加回去
return res;
}
/** Returns whether the queue is empty. */
bool empty() {
return stIn.empty() && stOut.empty();
}
};
- 时间复杂度: push和empty为O(1), pop和peek为O(n)
- 空间复杂度: O(n)
3.8 用队列实现栈
3.8.1 要求
使用队列实现栈的下列操作:
- push(x) -- 元素 x 入栈
- pop() -- 移除栈顶元素
- top() -- 获取栈顶元素
- empty() -- 返回栈是否为空
注意:
- 你只能使用队列的基本操作-- 也就是 push to back, peek/pop from front, size, 和 isempty 这些操作是合法的。
- 你所使用的语言也许不支持队列。 你可以使用 list 或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。
- 你可以假设所有操作都是有效的(例如, 对一个空的栈不会调用 pop 或者 top 操作)。
3.8.2 算法思想
最简单的两个队列模拟:
用两个队列que1和que2实现队列的功能,que2其实完全就是一个备份的作用,把que1最后一个元素以外的元素都备份到que2,然后弹出最后面的元素,再把其他元素从que2导回que1。
class MyStack {
public:
queue<int> que1;
queue<int> que2; // 辅助队列,用来备份
/** Initialize your data structure here. */
MyStack() {
}
/** Push element x onto stack. */
void push(int x) {
que1.push(x);
}
/** Removes the element on top of the stack and returns that element. */
int pop() {
int size = que1.size();
size--;
while (size--) { // 将que1 导入que2,但要留下最后一个元素
que2.push(que1.front());
que1.pop();
}
int result = que1.front(); // 留下的最后一个元素就是要返回的值
que1.pop();
que1 = que2; // 再将que2赋值给que1
while (!que2.empty()) { // 清空que2
que2.pop();
}
return result;
}
/** Get the top element. */
int top() {
return que1.back();
}
/** Returns whether the stack is empty. */
bool empty() {
return que1.empty();
}
};
- 时间复杂度: pop为O(n),其他为O(1)
- 空间复杂度: O(n)
3.8.3 优化 —— 只用一个队列
一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时再去弹出元素就是栈的顺序了。
class MyStack {
public:
queue<int> que;
/** Initialize your data structure here. */
MyStack() {
}
/** Push element x onto stack. */
void push(int x) {
que.push(x);
}
/** Removes the element on top of the stack and returns that element. */
int pop() {
int size = que.size();
size--;
while (size--) { // 将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部
que.push(que.front());
que.pop();
}
int result = que.front(); // 此时弹出的元素顺序就是栈的顺序了
que.pop();
return result;
}
/** Get the top element. */
int top() {
return que.back();
}
/** Returns whether the stack is empty. */
bool empty() {
return que.empty();
}
};
- 时间复杂度: pop为O(n),其他为O(1)
- 空间复杂度: O(n)