(C++)数据结构课程笔记3/9 - 栈和队列

245 阅读19分钟

§3 - 栈和队列

1 - 栈的定义与实现(后进先出)

相关概念

栈(Stack)是限定仅在表尾进行插入和删除操作的线性表。进行插入和删除操作的一端(表尾)为栈顶(Top),另一端(表头)为栈底(Base)。当表中没有元素时,称为空栈

抽象数据类型

ADT Stack {
	数据对象:
		D={a_i|a_i∈ElemSet,i=1,2,...,n}
	数据关系:
		R={<a_i-1,a_i>|a_i-1,a_i∈D,i=2,...,n}
		(约定a_n为栈顶,a_1为栈底)
	基本操作:
		InitStack(&S)
		DestroyStack(&S)
		StackLength(S)
		StackEmpty(S)
		GetTop(S,&e) // 需判空
		ClearStack(&S)
		Push(&S,e) // 需判满
		Pop(&S,&e) // 需判空
		StackTravers(S,visit())
} ADT Stack

用顺序存储结构实现栈

利用一组地址连续的存贮单元依次存放从栈底到栈顶的若干数据元素。

第一种实现:定长数组

规定:栈顶指针Top指向最后一个元素

// ----------Sqstack.h----------
#ifndef SQSTACK_H_
#define SQSTACK_H_

#define MAXSIZE 100 // 栈的最大容量
typedef int T;

typedef struct {
    T   Stack[MAXSIZE]; // 静态分配,相应的动态分配方式为:T* Stack;
    int Top; // 栈顶元素在数组中的位置
} SqStack;

void InitStack(SqStack &S);
int StackLength(SqStack S);
int StackEmpty(SqStack S);
int GetTop(SqStack S,T &e);
void ClearStack(SqStack &S);
int Push(SqStack &S,T e);
int Pop(SqStack &S,T &e);

#endif
// ----------Sqstack.cpp----------
#include "Sqstack.h"

void InitStack(SqStack &S) {
    S.Top=-1; // 栈空
}

int StackLength(SqStack S) {
    return S.Top+1;
}

int StackEmpty(SqStack S) {
    return S.Top<0?1:0;
}

int GetTop(SqStack S,T &e) {
    if (S.Top<0) // 判空
		return 0;
    e=S.Stack[S.Top];
    return 1;
}

void ClearStack(SqStack &S) {
    S.Top=-1;
}

int Push(SqStack &S,T e) {
    if (S.Top>=MAXSIZE-1) // 判满
		return 0;
    S.Stack[++S.Top]=e;
    return 1;
}

int Pop(SqStack &S,T &e) {
    if (S.Top<0) // 判空
        return 0;
	e=S.Stack[S.Top--];
	return 1;
}
第二种实现:不定长数组

规定:栈顶指针Top指向最后一个元素的下一个位置

判满:S.Top-S.Base>=S.Stacksize

判空:S.Base==S.Top

// ----------Sqstack.h----------
#ifndef SQSTACK_H_
#define SQSTACK_H_

#define STACK_INIT_SIZE 80
#define STACK_INCREMENT 10
typedef int T;

typedef struct {
    T*  Base;
    T*  Top;
    int Stacksize;
} SqStack;

void InitStack(SqStack &S);
int StackLength(SqStack S);
int StackEmpty(SqStack S);
int GetTop(SqStack S,T &e);
void ClearStack(SqStack &S);
int Push(SqStack &S,T e);
int Pop(SqStack &S,T &e);
void DestroyStack(SqStack &S);

#endif
// ----------Sqstack.cpp----------
#include <stdio.h>
#include <stdlib.h>
#include "Sqstack.h"

void InitStack(SqStack &S) {
    S.Base=(T*)malloc(STACK_INIT_SIZE*sizeof(T));
    if (!S.Base)
        exit(1);
    S.Top=S.Base;
    S.Stacksize=STACK_INIT_SIZE;
}

int StackLength(SqStack S) {
    return S.Top-S.Base;
}

int StackEmpty(SqStack S) {
    return S.Base==S.Top?1:0;
}

int GetTop(SqStack S,T &e) {
    if (S.Base==S.Top) // 判空
		return 0;
    e=*(S.Top-1);
    return 1;
}

void ClearStack(SqStack &S) {
    free(S.Base);
    S.Base=(T*)malloc(STACK_INIT_SIZE*sizeof(T));
    if (!S.Base)
        exit(1);
    S.Top=S.Base;
    S.Stacksize=STACK_INIT_SIZE;
}

int Push(SqStack &S,T e) {
    // 判满,追加存储空间
	if (S.Top-S.Base>=S.Stacksize) {
        S.Base=(T*)realloc(S.Base,sizeof(T)*(S.Stacksize+STACK_INCREMENT));
    	if (!S.Base)
        	exit(1);
    	S.Top=S.Base+S.Stacksize;
        S.Stacksize+=STACK_INCREMENT;
    }
    *S.Top=e;
    S.Top++;
    return 1;
}

int Pop(SqStack &S,T &e) {
    if (S.Base==S.Top) // 判空
        return 0;
    S.Top--;
    e=*S.Top;
	return 1;
}

void DestroyStack(SqStack &S) {
    free(S.Base);
}
第三种实现:共享栈

如果在一个程序中需要使用多个栈,最好能采用多个栈共享空间的方法,即给多个栈分配一个足够大的数组空间,利用栈的动态特性,使其存储空间互相补充。

一种两个栈共享空间的方法是:将这两个栈的栈底设在数组的两端,让它们的栈顶向数组中间增长。这样当一个栈中元素较多时,就可以越过数组的中点,延伸到另一个栈的部分空间中去。当两个栈的栈顶相遇时,才会发生“上溢”。

// 结构体声明
#define MAXSIZE 100
typedef int T;

typedef struct {
    T   Stack[MAXSIZE];
	int Top1,Top2;
} DuStack;
// 共享栈入栈
int Push(int i,T e) {
	if (S.Top1==S.Top2-1) // 判满
        return 0;
    if (i==1) // 对第一个栈进行入栈操作
        S.Stack[++S.Top1]=e;
    else // 对第二个栈进行入栈操作
        S.Stack[--S.Top2]=e;
    return 1;
}

// 共享栈出栈
int Pop(int i,T &e) {
    if (i==1) { // 对第一个栈进行出栈操作
        if (S.Top1==-1) // 判空
            return 0;
		else {
            e=S.Stack[S.Top1--];
            return 1;
        }
    }
    else { // 对第二个栈进行出栈操作
        if (S.Top2==MAXSIZE) // 判空
            return 0;
        else {
            e=S.Stack[S.Top2++];
            return 1;
        }
}

用链式存储结构实现栈

上面一种实现 Pop 操作时间复杂度为 O(n),故使用下面一种实现。

// 结构体声明
typedef int T;

typedef struct Snode {
    T Data;
    struct Snode* Next;
} *LinkStack;
// 链式栈入栈
int Push(LinkStack &Top,T e) {
    LinkStack t=(LinkStack)malloc(sizeof(Snode));
    if (t==NULL)
        return 0; // 内存无可用空间,栈上溢
	t->Data=e;
    t->Next=Top;
    Top=t;
    return 1;
}

// 链式栈出栈
int Pop(LinkStack &Top,T &e) {
    if (Top==NULL) // 判空
        return 0;
    LinkStack p=Top;
    Top=p->Next;
    e=p->Data;
    free (p);
    return 1;
}

对于链栈,不会产生单个栈满而其余栈空的情形,只有当系统空间全部用完,malloc 过程无法实现时才会发生上溢,因此多个链栈共享空间也就是自然的事了。

顺序栈还是链式栈?

栈广泛应用于程序设计中。当要解决的问题具有先进后出的特点时就可以用栈。

顺序栈、链栈的基本操作的时间复杂度都是 O(1)。顺序栈需要预先分配空间,链栈动态生成。如果需要访问内部节点,顺序栈更快。当最大需要容量事先不能估计时,采用链式存储结构是有效的方法。

2 - 栈的应用举例

例一:数制转换

示例

(1348)10 = (2504)8,其运算过程如下:

NN/8N%8
13481684
168210
2125
202
实现

N%8 从上往下得出,从下往上输出,用栈实现。

#include <stdio.h>

#define stacksize 100

typedef struct {
    int base[stacksize];
    int top;
} stack;

void InitStack(stack* s) {
    s->top=-1;
}

int StackEmpty(stack* s) {
    return s->top<0?1:0;
}

int Push(stack* s,int e) { // C语言风格的写法
    if (s->top>=stacksize-1)
        return 0;
    s->base[++s->top]=e;
    return 1;
}

int Pop(stack* s,int* e) {
    if (s->top<0)
        return 0;
    *e=s->base[s->top--];
    return 1;
}

void PrintConversion(int format,int n) {
    stack s;
    InitStack(&s);
    while (n) {
        Push(&s,n%format);
        n/=format;
    }
    while (!StackEmpty(&s)) {
        int e;
        Pop(&s,&e);
        printf("%d",e);
    }
    printf("\n");
}

int main() {
    int n; // n<=10
    scanf("%d",&n);
    PrintConversion(8,n);
    return 0;
}

例二:括号的匹配检验

示例

假设在表达式中,[([][])]为正确的格式,[([)[])]([][])][([][])均为不正确的格式。

实现

简化实现,假设括号都为圆括号:

  1. 凡出现左括弧:进栈
  2. 凡出现右括弧:首先检查栈空,若栈空,则表明该“右括弧”多余,表达式不正确;否则和栈顶元素比较,若匹配,则“左括弧出栈”,否则表达式不正确
  3. 表达式检验结束时:检查栈空,若栈空,则表达式正确,否则表达式不正确
int Matching(String &exp) {
    Stack s;
    InitStack(s);
    int i=1,state=1; // 表达式正确状态为1,不正确为0
    while (i<=Length(exp)&&state) {
        switch (exp[i]) {
            case "(":
                Push(s,exp[i]);
                i++;
                break;
            case ")":
                if (!StackEmpty(s)&&GetTop(s)=="(")
                    Pop(s);
                	i++;
                else
                    state=0;
                break;
        }
    }
    if (StackEmpty(s)&&state)
        return 1;
	else
        return 0;
}

例三:迷宫问题 Maze Problem

背景

迷宫问题

实现(栈实现)
#include <iostream>
#include <cstring>

using namespace std;

const int N=15;

int n,m;
int maze[N][N];

typedef struct {
	int top;
	pair<int,int> s[N*N];
} stack;

void Init(stack &st) {
	st.top=-1;
}

bool isEmpty(stack &st) {
	return st.top<0?true:false;
}

pair<int,int> GetTop(stack &st) {
	return make_pair(st.s[st.top].first,st.s[st.top].second);
}

void Push(stack &st,pair<int,int> l) {
	++st.top;
	st.s[st.top].first=l.first;
	st.s[st.top].second=l.second;
}

void Pop(stack &st) {
	--st.top;
}

stack st;

int d[N][N]; // 规定:东0 南1 西2 北3
int dr[]={1,0,-1,0};
int dc[]={0,1,0,-1};

int main() {
	cin >> n >> m;
	for (int i=0;i<n;i++)
		for (int j=0;j<m;j++)
			cin >> maze[i][j];
	Init(st);
	memset(d,-1,sizeof d);
	pair<int,int> l=make_pair(0,0);
	do {
		int i;
		for (i=d[l.first][l.second]+1;i<4;i++) {
			int r=l.first+dr[i];
			int c=l.second+dc[i];
			if (maze[r][c]==0&&r>=0&&r<n&&c>=0&&c<m&&(r!=GetTop(st).first||c!=GetTop(st).second))
				break;
		}
		if (i<4) {
			Push(st,l);
			d[l.first][l.second]=i;
			l.first+=dr[i];
			l.second+=dc[i];
		}
		else {
			if (l.first==n-1&&l.second==m-1) {
				Push(st,l);
				break;
			}
			else {
				l.first=GetTop(st).first;
				l.second=GetTop(st).second;
				Pop(st);
			}
		}
	} while (!isEmpty(st));
	stack st2;
	Init(st2);
	while (!isEmpty(st)) {
		Push(st2,GetTop(st));
		Pop(st);
	}
	while (!isEmpty(st2)) {
		cout << "(" << GetTop(st2).first << "," << GetTop(st2).second << ")" << endl;
		Pop(st2);
	}
	return 0;
}

例四:表达式求值

背景

在计算机中,表达式求值广为使用的是一种简单直观的算法“算符优先法”。它是根据算符优先关系的规定来实现对表达式的编译或解释的。它的实现是栈应用的又一个典型例子。

为了叙述的简洁,仅讨论简单算术表达式求值的问题,这种表达式仅包含加、减、乘、除、左右括号以及约定的结束符“#”六种算符。不难将它推广到更一般的表达式上。

以下是简单算术表达式中涉及到的算符的优先关系(左边栏算符和上边栏算符是表达式中相继出现的两个算符;左边栏算符>=<上边栏算符):

+-*/()#
+<<<>
-<<<>
*<>
/<>
(<<<<<=
)>
#<<<<<=
实现

两个工作栈:

  • OPTR 栈:寄存算符
  • OPND 栈:寄存操作数或运算结果

算法基本思想:

  1. 初始化 OPTR 栈和 OPND 栈,将起始符“#” Push 进入 OPTR 栈;
  2. 读入表达式中的每个字符:若是操作数,则进 OPND 栈;若是算符,则和 OPTR 栈的栈顶算符比较优先关系后作相应操作,直至整个表达式求值完毕(即 OPTR 栈的栈顶元素和当前读入的字符均为“#”);
  3. 相应操作为:
    • 若后来算符 > 栈顶算符,则后来算符入栈;
    • 若后来算符 = 栈顶算符(括号情况),则 Pop 一个算符,后来算符不入栈(脱括号并接收下一字符);
    • 若后来算符 < 栈顶算符,则 Pop 一个算符和两个操作数,将运算结果 Push 进入 OPND 栈,继续比较至后来算符 > 或 = 栈顶算符。
// 为简化,仅支持0~9的简单算术表达式求值,每步四则运算结果都为0~9
#include <stdio.h>

typedef struct {
    char s[100];
    int top;
} Stack;

void InitStack(Stack &S) {
    S.top=-1;
}

char GetTop(Stack S) {
    return S.s[S.top];
}

int Push(Stack &S,char e) {
    if (S.top>=99)
		return 0;
    S.s[++S.top]=e;
    return 1;
}

int Pop(Stack &S,char &e) {
    if (S.top<0)
        return 0;
	e=S.s[S.top--];
	return 1;
}

int isNumber(char c) {
    return c>='0'&&c<='9'?1:0;
}

char Precede(char stop,char c) {
    switch (stop) {
        case '+':
            switch (c) {
                case '+':return '>';
                case '-':return '>';
                case '*':return '<';
                case '/':return '<';
                case '(':return '<';
                case ')':return '>';
                case '#':return '>';
            }
        case '-':
            switch (c) {
                case '+':return '>';
                case '-':return '>';
                case '*':return '<';
                case '/':return '<';
                case '(':return '<';
                case ')':return '>';
                case '#':return '>';
            }
        case '*':
            switch (c) {
                case '+':return '>';
                case '-':return '>';
                case '*':return '>';
                case '/':return '>';
                case '(':return '<';
                case ')':return '>';
                case '#':return '>';
            }
        case '/':
            switch (c) {
                case '+':return '>';
                case '-':return '>';
                case '*':return '>';
                case '/':return '>';
                case '(':return '<';
                case ')':return '>';
                case '#':return '>';
            }
        case '(':
            switch (c) {
                case '+':return '<';
                case '-':return '<';
                case '*':return '<';
                case '/':return '<';
                case '(':return '<';
                case ')':return '=';
            }
        case ')':
            switch (c) {
                case '+':return '>';
                case '-':return '>';
                case '*':return '>';
                case '/':return '>';
                case ')':return '>';
                case '#':return '>';
            }
        case '#':
        	switch (c) {
        		case '+':return '<';
                case '-':return '<';
                case '*':return '<';
                case '/':return '<';
                case '(':return '<';
                case '#':return '=';
			}
    }
}

char Operate(char a,char theta,char b) {
    if (theta=='+')
        return a+b-'0';
    else if (theta=='-')
        return a-b+'0';
    else if (theta=='*')
        return (a-'0')*(b-'0')+'0';
    else if (theta=='/')
        return (a-'0')/(b-'0')+'0';
}

char EvaluateExpression() {
    Stack OPND,OPTR;
    InitStack(OPND);
    InitStack(OPTR);
    Push(OPTR,'#');
    char c=getchar();
    while (c!='#'||GetTop(OPTR)!='#') { // 退出条件:c=='#'&&GetTop(OPTR)=='#'
        if (isNumber(c)) {
            Push(OPND,c);
            c=getchar();
        }
        else switch (Precede(GetTop(OPTR),c)) {
            case '<':
                Push(OPTR,c);
                c=getchar();
                break;
            case '=':
            	char x;
                Pop(OPTR,x);
                c=getchar();
                break;
            case '>':
                char theta,a,b;
                Pop(OPTR,theta);
                Pop(OPND,b);
                Pop(OPND,a);
                Push(OPND,Operate(a,theta,b));
                break; // 继续比较至后来算符>或=栈顶算符
        }
    }
	return GetTop(OPND);
}

int main() {
	printf("%c\n",EvaluateExpression());
	return 0;
}
另一种实现

二元运算符表达式的另外三种表示方法:(以表达式 a * b + (c - d / e) * f 为例)

  • 前缀表达式:算符 操作数 1 操作数 2,原表达式转化为 +*ab*-c/def
  • 中缀表达式:操作数 1 算符 操作数 2,原表达式转化为 a*b+c-d/e*f
  • 后缀表达式:操作数 1 操作数 2 算符,原表达式转化为 ab*cde/-f*+

结论:

  1. 操作数的相对次序相同
  2. 算符的相对次序不同
  3. 中缀表达式丢失括号信息,导致运算顺序不确定
  4. 前缀表达式运算规则为:连续出现的两个操作数和在它们之前且紧靠它们的算符构成一个最小表达式
  5. 后缀表达式运算规则为:每个算符和在它之前且紧靠它的两个操作数构成一个最小表达式

所以,可以利用前缀表达式或后缀表达式实现表达式求值,以下为利用后缀表达式求值的思路:

  1. 将原表达式转化为后缀表达式

    后缀表达式的特点:原表达式中算符的运算顺序由它之后的一个算符决定,而后缀表达式中算符的运算顺序恰为其在表达式中的出现顺序,即优先级高的算符领先于优先级低的算符

    算法描述:

    1. 设置算符栈,栈底置“#”,表达式结束符也为“#”
    2. 读入表达式中的每个字符后判断并进行相关操作至栈空:
      • 若当前字符是操作数,则直接发送给后缀表达式
      • 若当前字符是算符,则和栈顶算符比较优先关系后作相应操作:
        • 若当前算符 > 栈顶算符,则当前算符入栈
        • 若当前算符 = 栈顶算符,即左右括号或结束符与栈底的“#”相遇的情况,则“(”或“#”出栈,“)”或“#”不入栈
        • 若当前算符 < 栈顶算符,则栈顶算符出栈发送给后缀表达式,继续比较直至“>”或“=”
    int isNumber(char c) {
        return c>='0'&&c<='9'?1:0;
    }
    
    char Precede(char stop,char c) {
        switch (stop) {
            case '+':
                switch (c) {
                    case '+':return '>';
                    case '-':return '>';
                    case '*':return '<';
                    case '/':return '<';
                    case '(':return '<';
                    case ')':return '>';
                    case '#':return '>';
                }
            case '-':
                switch (c) {
                    case '+':return '>';
                    case '-':return '>';
                    case '*':return '<';
                    case '/':return '<';
                    case '(':return '<';
                    case ')':return '>';
                    case '#':return '>';
                }
            case '*':
                switch (c) {
                    case '+':return '>';
                    case '-':return '>';
                    case '*':return '>';
                    case '/':return '>';
                    case '(':return '<';
                    case ')':return '>';
                    case '#':return '>';
                }
            case '/':
                switch (c) {
                    case '+':return '>';
                    case '-':return '>';
                    case '*':return '>';
                    case '/':return '>';
                    case '(':return '<';
                    case ')':return '>';
                    case '#':return '>';
                }
            case '(':
                switch (c) {
                    case '+':return '<';
                    case '-':return '<';
                    case '*':return '<';
                    case '/':return '<';
                    case '(':return '<';
                    case ')':return '=';
                }
            case ')':
                switch (c) {
                    case '+':return '>';
                    case '-':return '>';
                    case '*':return '>';
                    case '/':return '>';
                    case ')':return '>';
                    case '#':return '>';
                }
            case '#':
            	switch (c) {
            		case '+':return '<';
                    case '-':return '<';
                    case '*':return '<';
                    case '/':return '<';
                    case '(':return '<';
                    case '#':return '=';
    			}
        }
    }
    
    void Transform(char suffix[],char exp[]) {
        Stack S;
        InitStack(S);
        Push(S,'#');
        
        int i=0;
        char* p=exp;
        while (!StackEmpty(S)) {
            if (isNumber(*p)) {
                suffix[i++]=*p;
                ++p;
            }
            else switch (Precede(GetTop(S),*p)) {
                case '<':
                    Push(S,*p);
                    ++p;
                    break;
                case '=':
                    Pop(S);
                    ++p;
                    break;
                case '>':
                    suffix[i++]=Pop(S);
                    break;
            }
        }
    }
    
  2. 后缀表达式求值

    算法描述:

    1. 设置操作数栈
    2. 读入表达式中的每个字符后判断并进行相关操作至表达式读入完:
      • 若当前字符是操作数,则当前字符入栈
      • 若当前字符是算符,则从栈中弹出两个数,运算结果入栈
    // 为简化,仅支持0~9的简单算术表达式求值,每步四则运算结果都为0~9
    char Operate(char a,char theta,char b) {
        if (theta=='+')
            return a+b-'0';
        else if (theta=='-')
            return a-b+'0';
        else if (theta=='*')
            return (a-'0')*(b-'0')+'0';
        else if (theta=='/')
            return (a-'0')/(b-'0')+'0';
    }
    
    char EvaluateExpression(char suffix[]) {
        Stack S;
        InitStack(S);
        
        int i=0;
        while (i!=strlen(suffix)) {
            if (isNumber(suffix[i]))
                Push(S,suffix[i++]);
            else {
                char a=Pop(S),b=Pop(S);
                Push(S,Operate(b,suffix[i++],a)); // 注意:这里要考虑-和/的情况,后缀表达式中b在a前,所以应该是b-a或b/a
            }
        }
        return GetTop(S);
    }
    

例五:实现递归

铺垫:多个函数嵌套调用的规则

当一个函数运行期间调用另一个函数时,在运行被调用函数前,需先完成以下任务:

  1. 将所有实在参数、返回地址等信息传递给被调用函数保存
  2. 为被调用函数分配一个存储区
  3. 控制转移到被调用函数

在从被调用函数返回调用函数前,需先完成以下任务:

  1. 保存被调用函数的计算结果
  2. 释放被调用函数的存储区
  3. 按照被调用函数保存的返回地址将控制转移到调用函数

调用函数与被调用函数间的信息传递和控制转移需要通过来实现:系统将整个程序运行期间所需要的数据空间安排在一个栈内;每当调用一个函数,就为它在栈顶分配一个存储区;每当从一个函数退出,就从栈顶释放它的存储区;所以当前运行的函数的存储区必在栈顶

递归实现与栈实现

递归的过程就是函数不断调用自己,是调用函数与被调用函数为同一函数的函数嵌套调用:每当进入一层递归,就产生一个新的工作记录(包括上一层的实在参数、返回地址等)压入栈顶;每当从一层递归退出,就从栈顶弹出一个工作记录

以上都是对递归实现的说明,而栈实现本质上就是对系统运行递归程序过程的模拟,所有递归实现都可以转化为栈实现;因此,递归实现与栈实现的时间复杂度、空间复杂度相当(“非递归实现的复杂度都优于递归实现”是第一个误区,应该说递推实现的总体复杂度优于递归实现)

递归的形式

递归的形式有尾递归、头递归、树递归、嵌套递归和间接递归等,大致可以分为线性递归和树形递归:

  • 线性递归

    func() {
    	func();
    }
    

    线性递归与递推的时间复杂度相当(“递归实现的时间复杂度都很高”是第二个误区);而因为要利用栈存储,线性递归的空间复杂度远高于递推,数据过大会“爆栈”

  • 树形递归

    func() {
    	func();
    	func();
    }
    

    树形递归的时间复杂度是指数级别;树形递归的空间复杂度也远高于递推

汉诺塔问题

问题描述:有三个塔座 X, Y, Z,塔座 X 上插有 n 个大小各不相同且从上到下大小递增的圆盘,按从小到大编号为 1, 2, ..., n;要求将塔座 X 上的 n 个圆盘移至塔座 Z 上,并仍按同样顺序叠排;移动圆盘时遵循以下规则:

  1. 一次移动一个圆盘
  2. 圆盘可以插在 X, Y, Z 中的任一塔座上
  3. 任何时刻都不能将一个较大的圆盘压在较小的圆盘之上

递归实现

  • 当 n = 1 时,将一个圆盘从 X 移至 Z
  • 当 n > 1 时,将压在圆盘 n 上的 n-1 个圆盘从 X 移至 Y(递归),将圆盘 n 从 X 移至 Z,再将 Y 上的 n-1 个圆盘从 Y 移至 Z(递归)
// 借助塔座y,将塔座x最上面的n个圆盘移至塔座z
void Hanoi(int n,char x,char y,char z) {
    if (n==1)
        Move(x,1,z); // 将x上编号为1的圆盘移至z
    else {
        Hanoi(n-1,x,z,y);
        Move(x,n,z); // 将x上编号为n的圆盘移至z
        Hanoi(n-1,y,x,z);
    }
}

递归过程栈的情况

    int main() {
        Hanoi(3,'A','B','C');
(0)     return 0;
    }
    void Hanoi(int n,char x,char y,char z) {
(1)     if (n==1)
(2)         Move(x,1,z);
(3)     else {
(4)         Hanoi(n-1,x,z,y);
(5)         Move(x,n,z);
(6)         Hanoi(n-1,y,x,z);
(7)     }
(8) }

返回地址用 (0) 至 (8) 标记,每个工作记录含实在参数和返回地址,则递归过程栈的情况如下:

八皇后问题

问题描述:在 8×8 格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问一共有多少种摆法;输出整个 8×8 棋盘,“Q”表示该位置放皇后,“.”表示该位置不放皇后(以下为拓展的 n 皇后问题的代码)

递归实现

“回溯”的思想:

#include <iostream>

using namespace std;

const int N=25;

int n=8; // 皇后数
char b[N][N]; // 棋盘
bool row[N],col[N],dg[N],udg[N]; // 行、列、主对角线和反对角线标记

void queenProblem(int x,int y,int s) { // x表示行,y表示列,s表示已放皇后数
	if (y==n) {
		x++;
		y=0;
	}
	if (x==n) {
		if (s==n) {
			for (int i=0;i<n;i++) {
				for (int j=0;j<n;j++)
					cout << b[i][j];
				cout << endl;
			}
			cout << endl;
		}
		return; // 回溯
	}
	// 不放
	queenProblem(x,y+1,s);
	// 放
	if (!row[x]&&!col[y]&&!dg[x-y+n-1]&&!udg[x+y]) {
		b[x][y]='Q';
		row[x]=col[y]=dg[x-y+n-1]=udg[x+y]=true;
		queenProblem(x+1,0,s+1);
		b[x][y]='.';
		row[x]=col[y]=dg[x-y+n-1]=udg[x+y]=false;
	}
}

int main() {
    cin >> n;
	for (int i=0;i<n;i++)
		for (int j=0;j<n;j++)
			b[i][j]='.';
	queenProblem(0,0,0);
	return 0;
}

栈实现

#include <iostream>

using namespace std;

const int N=25;

typedef struct {
	int s[N];
	int t;
} Stack;

void InitStack(Stack &stack) {
	stack.t=-1;
}

int StackLength(Stack stack) {
	return stack.t+1;
}

void Push(Stack &stack,int e) {
	stack.s[++stack.t]=e;
}

int Pop(Stack &stack) {
	int e=stack.s[stack.t--];
	return e;
}

int n=8; // 皇后数
bool row[N],dg[N],udg[N]; // 行、主对角线和反对角线标记

void queenProblem() {
	Stack s;
	InitStack(s);
	
	int pos=-1; // 此时pos相当于一个flag
	while (1) {
		if (StackLength(s)==n) {
			for (int i=0;i<n;i++) {
				for (int j=0;j<n;j++) {
					if (i==s.s[j])
						cout << "Q";
					else
						cout << ".";
				}
				cout << endl;
			}
			cout << endl;
			pos=Pop(s);
			row[pos]=false;
			dg[pos-StackLength(s)+n-1]=false;
			udg[pos+StackLength(s)]=false;
		}
		int i=(pos==-1?0:pos+1); // i=pos会死循环
		for (;i<n;i++)
			if (!row[i]&&!dg[i-StackLength(s)+n-1]&&!udg[i+StackLength(s)]) {
				Push(s,i);
				row[i]=true;
				dg[i-(StackLength(s)-1)+n-1]=true;
				udg[i+StackLength(s)-1]=true;
				pos=-1; // 此时pos相当于一个flag
				break;
			}
		if (i==n) {
			pos=Pop(s);
			row[pos]=false;
			dg[pos-StackLength(s)+n-1]=false;
			udg[pos+StackLength(s)]=false;
		}
		if (pos==n-1&&StackLength(s)==0)
			break;
	}
}

int main() {
	if (n==1) cout << "Q" << endl;
	else queenProblem();
	return 0;
}

3 - 队列的定义与实现(先进先出)

相关概念

队列(Queue)是限定仅在表的一端插入元素,在另一端删除元素的线性表。允许插入的一端称为队尾(Rear),允许删除的一端称为队头(Front)。当队列中没有元素时,称为空队列

抽象数据类型

ADT Queue {
	数据对象:
		D={a_i|a_i∈ElemSet,i=1,2,...,n}
	数据关系:
		R={<a_i-1,a_i>|a_i-1,a_i∈D,i=2,...,n}
		(约定a_1为队列头,a_n为队列尾)
	基本操作:
		InitQueue(&Q)
		DestroyQueue(&Q)
		QueueLength(Q)
		QueueEmpty(Q)
		GetHead(Q,&e) // 需判空
		ClearQueue(&Q)
		EnQueue(&Q,e) // 需判满
		DeQueue(&Q,&e) // 需判空
		QueueTravers(Q,visit())
} ADT Queue

用顺序存储结构实现队列

第一种实现:定长数组

规定:队头指针 Front 指向队头元素在队列中的当前位置,队尾指针 Rear 指向队尾元素的下一个位置

判空:Q.Front==Q.Rear

// ----------Sqqueue.h----------
#ifndef SQQUEUE_H_
#define SQQUEUE_H_

#define MAXSIZE 100
typedef int T;

typedef struct {
    T   Queue[MAXSIZE]; // 静态分配,相应的动态分配方式为:T* Queue;
    int Front,Rear;
} SqQueue;

void InitQueue(SqQueue &Q);
int QueueLength(SqQueue Q);
int QueueEmpty(SqQueue Q);
int GetHead(SqQueue Q,T &e);
void ClearQueue(SqQueue &Q);
int EnQueue(SqQueue &Q,T e);
int DeQueue(SqQueue &Q,T &e);

#endif
// ----------Sqqueue.cpp----------
#include "Sqqueue.h"

void InitQueue(SqQueue &Q) {
    Q.Front=Q.Rear=0;
}

int QueueLength(SqQueue Q) {
    return Q.Rear-Q.Front;
}

int QueueEmpty(SqQueue Q) {
    return Q.Front==Q.Rear?1:0;
}

int GetHead(SqQueue Q,T &e) {
    if (Q.Front==Q.Rear) // 判空
        return 0;
    e=Q.Queue[Q.Front];
    return 1;
}

void ClearQueue(SqQueue &Q) {
    Q.Front=Q.Rear=0;
}

int EnQueue(SqQueue &Q,T e) {
    if (Q.Rear==MAXSIZE) // 判满
		return 0;
    Q.Queue[Q.Rear++]=e;
    return 1;
}

int DeQueue(SqQueue &Q,T &e) {
    if (Q.Front==Q.Rear) // 判空
        return 0;
	e=Q.Queue[Q.Front++];
	return 1;
}
第二种实现:循环队列

问题1:Rear==MAXSIZE 不一定说明队列中有 MAXSIZE 个元素,称“假溢出”。解决“假溢出”的办法一般有两种:

  1. 将整个队列左移,使队列的第一个元素重新位于 Queue[0](效率低);
  2. 设想 Queue[0] 接在 Queue[MAXSIZE-1] 之后,使一维数组 Queue 成为一个首尾相接的环。

问题2:直接处理成循环队列,队满和队空的条件都为:Q.Front==Q.Rear。在循环队列中如何判定队满和队空?两种方法:

  1. 设置一个标志 flag,以区别队列是满还是空。flag 初始化为“delete”;每一次插入操作后,被置为“entry”;每一次删除操作后,被置为“delete”。当出现 Front==Rear 时,如果flag此时为“entry”,则可判断队列是满的,否则队列是空的(效率相对较低);
  2. 少用一个数据元素空间,以队尾指针加 1 等于队头指针来表示队列满。

判满:(Q.Rear+1)%MAXSIZE==Q.Front

判空:Q.Front==Q.Rear

// 一般队列的入队算法
int EnQueue(SqQueue &Q,T e) {
    if (Q.Rear==MAXSIZE) // 判满
		return 0;
    Q.Queue[Q.Rear++]=e;
    return 1;
}

// 循环队列的入队算法
int EnCycque(SqQueue &Q,T e) {
    if ((Q.Rear+1)%MAXSIZE==Q.Front) // 判满
        return 0;
    Q.Queue[Q.Rear]=e;
    Q.Rear=(Q.Rear+1)%MAXSIZE;
	return 1;
}

// 一般队列出队算法
int DeQueue(SqQueue &Q,T &e) {
    if (Q.Front==Q.Rear) // 判空
        return 0;
	e=Q.Queue[Q.Front++];
	return 1;
}

// 循环队列出队算法
int DeCycque(SqQueue &Q,T &e) {
    if (Q.Front==Q.Rear) // 判空
        return 0;
    e=Q.Queue[Q.Front];
    Q.Front=(Q.Front+1)%MAXSIZE;
    return 1;
}

用链式存储结构实现队列

一个链队列需要两个分别指示队头和队尾的指针;为了操作方便,给链队列添加一个头结点,头指针指向头结点;所以,队列空的判定条件为头指针与尾指针均指向头结点

typedef int T;

// 结点结构
typedef struct Qnode {
    T Data;
    struct Qnode* Next;
} Qnode;

// 链队列结构
typedef struct {
    Qnode* Front;
    Qnode* Rear;
} LinkQueue;
// 链队列初始化
void InitQueue(LinkQueue &Q) {
    Q.Front=(Qnode*)malloc(sizeof(Qnode));
    if (!Q.Front)
        exit(1);
    Q.Rear=Q.Front;
    Q.Front->Next=NULL;
}

// 链队列销毁
void DestroyQueue(LinkQueue &Q) {
    while (Q.Front) {
        Q.Rear=Q.Front->Next;
        free(Q.Front);
        Q.Front=Q.Rear;
    }
}

// 链队列入队
int EnQueue(LinkQueue &Q,T e) {
    Qnode* p=(Qnode*)malloc(sizeof(Qnode));
    if (!p)
        return 0; // 内存无可用空间
    p->Data=e;
    p->Next=NULL;
    Q.Rear->Next=p;
    Q.Rear=p;
    return 1;
}

// 链队列出队(当链队列长度大于1时,只需修改头结点的Next域,尾指针不用变化;当链队列长度等于1时,除修改头结点的Next域外,还要修改尾指针)
int DeQueue(LinkQueue &Q,T &e) {
    if (Q.Front==Q.Rear)
        return 0; // 判空
    Qnode* p=Q.Front->Next;
    Q.Front->Next=p->Next;
    e=p->Data;
    if (Q.Rear==p)
        Q.Rear=Q.Front;
    free(p);
    return 1;
}

顺序队列还是链队列?

队列广泛应用于程序设计中,是现实世界排队的仿真。当要解决的问题具有先进先出的特点时就可以用队列。

顺序队列、链队列的基本操作的时间复杂度都是 O(1)。当需要访问中间结点时,顺序队列更宜;对于在使用中数据元素变动较大的情况,用链式存储结构比用顺序存储结构更有利。

4 - 队列的应用举例

例一:k 阶斐波那契数列

背景

k 阶斐波那契数列:约定 f0f_0 = 0, f1f_1 = 0, ..., fk2f_{k-2} = 0, fk1f_{k-1} = 1,则 fnf_n = fn1f_{n-1} + fn2f_{n-2} + ... + fnkf_{n-k} (n = k, k+1, ...)

问题描述:编写求 k 阶斐波那契数列前 n + 1 项(f0f_0, f1f_1, ..., fnf_n)的算法,要求满足 fnf_n ≤ max 且 fn+1f_{n+1} > max(max ≥ 0,为某个约定的常数)

实现
// 第一种实现(循环队列容量为k)
int Fb(int k,int max,int f[]) { // 返回数列最后一项下标n
    SqQueue cq;
    InitQueue(cq);
    
    for (int i=0;i<=k-2;i++) {
        f[i]=0;
        cq.Queue[i]=0;
    }
    f[k-1]=1;
    cq.Queue[k-1]=1;
    cq.Rear=k-1;
    
    int n=k;
    while (cq.Queue[cq.Rear]<max) {
        f[n]=0;
        for (int j=0;j<=k-1;j++)
            f[n]+=cq.Queue[j];
        cq.Rear=(cq.Rear+1)%k; // 因为容量恰好为k,所以将队列中所有数相加即可,cq.Front没有意义
        cq.Queue[cq.Rear]=f[n++];
    }
    if (cq.Queue[cq.Rear]>max)
        n-=2;
    else
        n-=1;
    if (max==1) {
        n=k;
        f[k]=1;
    }
    return n;
}

/*
 * f_i=f_{i-1}+f_{i-2}+...+f_{i-k}
 * f_{i+1}=f_i+f_{i-1}+...+f_{i-k+1}
 * 两式相减得:f_{i+1}=2f_i-f_{i-k}
 *
 * 第二种实现(利用f_{i+1}=2f_i-f_{i-k},循环队列容量为k+1)
 */
int Fb(int k,int max,int f[]) { // 接口不变
    SqQueue cq;
    InitQueue(cq);
    
    for (int i=0;i<=k-2;i++) {
        f[i]=0;
        cq.Queue[i]=0;
    }
    f[k]=f[k-1]=1;
    cq.Queue[k]=cq.Queue[k-1]=1;
    cq.Rear=k;
    
    int n=k+1;
    while (cq.Queue[cq.Rear]<max) {
        int j=(cq.Rear+1)%(k+1);
        f[n]=cq.Queue[cq.Rear]*2-cq.Queue[j];
        cq.Rear=j;
        cq.Queue[cq.Rear]=f[n++];
    }
    if (cq.Queue[cq.Rear]>max)
        n-=2;
    else
        n-=1;
    if (max==0)
        n=k-2;
    return n;
}

例二:划分子集

背景

已知集合 A = {a1, a2, ..., an} 及集合上的关系 R = {(ai, aj)|ai, aj∈A, i≠j},其中 (ai, aj) 表示 ai 与 aj 间存在冲突关系;要求将 A 划分成互不相交的子集 A1, A2, ..., Ak (k≤n),使任何子集中的元素均无冲突关系,同时要求子集个数尽可能少

实现

算法描述:(将 a1~an 映射到 1~n)

  1. 设置以下数据结构:

    • 冲突关系矩阵 cf
      • cf[i][j] = 1 表示 ai, aj 有冲突
      • cf[i][j] = 0 表示 ai, aj 无冲突
    • 循环队列 cq
    • 数组 result
    • 数组 newr
  2. 初始化:

    • 遍历集合 R,初始化 cf,注意 (ai, aj) 表示 cf[i][j] = 1 且 cf[j][i] = 1(预处理)
    • cq 元素从队头到队尾初始化为 1~n
    • result 和 newr 元素初始化为 0
    • 组号 group 初始化为 1
  3. cq 第一个元素 1 出队,并将 cf 中第一行的 1 拷入 newr 中对应位置(凡与 cq 第一个元素 1 有冲突的元素在 newr 中对应位置处均为 1);下一个元素出队:

    • 若其在 newr 中对应位置处为 1,即有冲突,则重新入队参加下一次分组
    • 若其在 newr 中对应位置处为 0,即无冲突,则可划归该组,并将 cf 中该元素对应行的 1 拷入 newr 中对应位置

    如此循环操作,直至 cq 所有元素依次出队,由 newr 中为 0 的单元对应的元素构成第 1 组,将 group 值 1 写入 result 中值为 0 的对应单元中

  4. group 自增,newr 清零,对 cq 中元素重复上述操作,直至队空

实现
void SubsetPartition(int n,int cf[][N],int result[]) {
	SqQueue cq;
	int newr[N],group=1;
	for (int i=1;i<=n;++i) {
		cq.Queue[i]=i;
		result[i]=0;
		newr[i]=0;
	}
	cq.Front=1;
	cq.Rear=(n+1)%N;
	
	while (!QueueEmpty(cq)) {
		int len=QueueLength(cq); // (cq.Rear+N-cq.Front)%N
		for (int i=0;i<len;++i) {
			int t=DeCycque(cq);
			if (newr[t])
				EnCycque(cq,t);
			else
				for (int j=1;j<=n;++j)
					if (cf[t][j])
						newr[j]=1;
		}
		for (int i=1;i<=n;++i)
			if (!newr[i]&&!result[i]) // 将group值写入result中值为0的对应单元中
				result[i]=group;
		++group;
		for (int i=1;i<=n;++i)
			newr[i]=0;
	}
}

5 - 其他运算受限的线性表

  1. 双端队列:允许在队列两端自由地插入和删除

  2. 双栈:允许在两端插入和删除,但在哪端插入就在哪端删除(两个底部相连的栈)

  3. 超队列:允许在队列两端插入、一端删除

  4. 超栈:允许在队列一端插入、两端删除(对栈溢出的一种特殊处理,即当栈溢出时,可将栈中保存最久的元素删除)