03 栈与递归 | 数据结构与算法
1. 栈
- 栈的定义:限定在表尾进行插入和删除操作的线性表
- 空栈:不含任何元素的栈
- 栈顶
top
:允许插入删除的一端 - 栈的输入输出序列:给一组互不相同元素的序列,长度为\(n\),出栈入栈可以交替进行,经过栈操作所得到的所有序列的数目为卡特兰数
\[\frac{1}{n+1}C_{2n}^n
\]
- 栈的操作(连续设计)
- 置空栈
make_null_stack()
#define maxn 1000 // max number of stack frames typedef struct{ datatype elements[maxn]; int top; } Stack; void make_null_stack(Stack& stack){ stack.top = -1; }
- 判断栈是否为空
empty()
bool empty(Stack& stack){ return stack.top == -1; }
- 入栈
push()
void push(Stack& stack , datatype element){ if(stack.top == maxn - 1) cout << "Overflow!" << endl; else stack[++top] = element; }
- 出栈
pop()
void pop(Stack& stack){ if(empty(stack)) cout << "Stack is NULL" << endl; else --stack.top; }
- 获得栈顶元素
peek()
datatype peek(Stack& stack){ return (empty(stack)) ? NULL : stack[top]; }
- 置空栈
- 栈的操作(单链表):当多个栈共享空间时,连续存储已经无法满足空间需要
- 置空栈
make_null_stack()
typedef struct Node{ datatype data; Node* next; Node(datatype data):Node(data , nullptr){}; }Stack; Stack make_null_stack(){ Stack top = new Node(0 , nullptr); return top; }
- 判断栈是否为空
empty()
bool empty(Stack& top){ return top -> next == nullptr; }
- 入栈
push()
void push(Stack& top , datatype element){ Stack s = new Node(element); s->next = top -> next; top -> next = s; }
- 出栈
pop()
void pop(Stack& top){ if(empty(top)) cout << "Stack is NULL" << endl; else { Stack s ; s = top -> next; top -> next = s -> next; delete s; } }
- 获得栈顶元素
peek()
datatype peek(Stack& top){ return (empty(top)) ? NULL : top->next->data; }
- 置空栈
2. 栈的应用
-
进制转换:十进制转
base
进制classDiagram direction RL Stack <|-- 竖式除法 class 竖式除法{ 8 | 159 ---7 8 | 19 ---3 8 | 12 ---2 0 } class Stack{ 2 3 7 }void convertion(int number , int base){ stack<int> stk; while(number){ stk.push(stk , number % base); number /= base; } while(!empty(stk)){ int ele = stk.peek(); cout << ele; stk.pop(); } }
-
表达式处理
-
表达式形式
(a + b) * (a - b)
- 前缀表达式:
*+ab-ab
(波兰表达式) - 中缀表达式:
(a + b) * (a - b)
- 后缀表达式:
ab+ab-*
(逆波兰表达式)
- 前缀表达式:
-
中缀表达式求值
- 规则:\(\theta 1\) 在 \(\theta 2\) 的前面
\(\theta1/\theta2\) + - * / ( ) # + > > < < < > > - > > < < < > > * > > > > < > > / > > > > < > > ( < < < < < = ) > > > > > > # < < < < < = - 操作
- 操作数栈置空,操作符栈压入
#
(终止符) - 依次读入表达式的每个单词
- 如果是操作数,压入操作数栈
- 如果是操作符,将操作符栈顶元素 \(\theta1\) 与读入的操作符 \(\theta2\) 进行优先级比较
- 如果栈顶元素优先级低 , 将 \(\theta2\) 压入操作符栈
- 如果优先级相等 , 弹出
pop()
操作符栈 - 如果栈顶元素优先级高 , 弹出两个操作数,一个运算符,进行计算,并将计算结果压入操作数栈,重复第4步
- 全部处理完毕,
operator_stack.top() == '#'
- 操作数栈置空,操作符栈压入
-
中缀转为后缀表达式(后缀表达式更利于计算机计算)
- 操作数栈置空,操作符栈压入
#
(终止符) - 依次读入表达式的每个单词
- 如果是操作数,直接输出
- 如果是操作符,将操作符栈顶元素 \(\theta1\) 与读入的操作符 \(\theta2\) 进行优先级比较
- 如果栈顶元素优先级低 , 将 \(\theta2\) 压入操作符栈
- 如果优先级相等 , 弹出
pop()
操作符栈 - 如果栈顶元素优先级高 , 弹出
pop()
操作符栈顶元素并且输出,重复第4步
- 全部处理完毕,
operator_stack.top() == '#'
- 操作数栈置空,操作符栈压入
-
3. 递归 \(Recursion\)
1. 递归基本思想
- 递归调用的定义:子程序(或函数)直接调用自己或者一系列调用语句间接调用自己。是一种描述问题和解决问题的基本方法
- 基本思想:将大问题化为重复的小问题,直至每个小问题都可以得到直接解决
- 算法
- 递归基:最小子问题
- 递归步:通过较为简单的函数值定义一般情况下的函数值
- 适用问题:问题具有某种可借用的类同自身的子问题的描述的性质
- 分类:
- 单路递归:一个递归过程只有一个递归入口
- 多路递归:一个递归过程有多个出口
- 间接递归:函数可以通过其他函数间接调用自己
- 迭代递归:每次递归调用都包含一次循环递归
2. 递归算法实例
- 排列问题
- 有 \(n\) 个元素,编号为 \(1,2,\cdots,n\),用一个具有 \(n\) 个元素的数组
A
来存放所生成的排列,然后输出它们 - 分析:
- 递归基:\(k = 1\) , 只有一个元素,显然构成排列
- 递归步:\(k > 1\) , 如果可由算法
perm(A,k-1,n)
完成数组后面k-1
个元素的排列,为完成数组后面k
个元素的排列perm(A,k,n)
,逐一对数组第n-k
元素与数组中第n-k
~n
元素进行互换,每互换一次,就执行一次perm(A,k-1,n)
操作,产生一个排列
- 算法
void perm(vector<int> arr , int k , int n){ if(k == 1) { //递归基 for(int i = 0 ; i < n ; ++i) cout << arr[i] << ' '; } else { for(int i = n - k ; i < n ; ++i){ swap(arr[i] , arr[n - k]); perm(arr , k - 1 , n); //递归步 swap(arr[i] , arr[n - k]) } } }
- 有 \(n\) 个元素,编号为 \(1,2,\cdots,n\),用一个具有 \(n\) 个元素的数组
- \(Hanoi\)汉诺塔问题
- 设
A
,B
,C
是3个塔座。开始时,在塔座A
上有一叠共n
个圆盘,这些圆盘自下而上,由大到小地叠在一起。各圆盘从小到大编号为1
,2
,...
,n
,现要求将塔座A
上的这一叠圆盘移到塔座B
上,并仍按同样顺序叠置。在移动圆盘时应遵守以下移动规则:- 每次只能移动1个圆盘;
- 任何时刻都不允许将较大的圆盘压在较小的圆盘之上;
- 在满足移动规则1和2的前提下,可将圆盘移至
A
,B
,C
中任一塔座上
- 分析:
- 把
n-1
个盘子移到B
上(通过C
移到从A
移到B
) - 把第
n
个盘子移到C
上 - 把
n-1
个盘子移到C
上 (通过A
移到从B
移到C
)
- 把
- 算法
void move(char pillar_1, int index, char pillar_2){ cout << "move plate " << index << " from " << pillar_1 << " to " << pillar_2 << endl; } void hanoi(int n, char a, char b, char c){ if(n == 1) move(a, 1, c); //将编号1的盘子从a移到c else { hanoi(n - 1, a, c, b); //通过c移到从a移到b move(a, n, c); hanoi(n - 1, b, a, c); //通过a移到从b移到c } }
- 设
3. 函数递归调用的内部执行过程
- 运行开始时,首先为递归调用一个工作 栈 ,其结构包括值参,局部变量与返回地址
- 每次执行递归调用之前,将递归函数的值参和局部变量的当前值以及调用后的返回地址压入栈中
- 每次递归调用结束之后,将栈顶元素弹出栈,是相应的值参和局部变量恢复为调用前的值,然后转向送回返回地址指定的位置继续执行