《算法笔记》编程笔记——第七章与第六章部分 栈和递归、队列、链表
一、栈
-
栈的使用:#include + using namespace std;
-
栈的定义:stack st;
-
栈内元素的访问:st.top()。只有这个方法进行元素的访问
-
常用函数:
-
st.push(x);//压入数值 st.pop();//弹出栈顶元素 st.top();//获取栈顶元素 st.empty();//栈判空,如果为空,返回true,否则返回false st.size();//返回栈内元素个数 //上述操作的时间复杂度均为O(1);
-
栈的清空:重新定义一个栈:
-
-
栈的常见用途:
- 通常用栈来模拟递归,防止出现程序崩溃的情况
-
题目
-
题目描述:
-
读入一个只包含+, -, *, / 的非负整数计算表达式,计算该表达式的值。(结果精确到小数点后2位)
-
思路:
①中缀表达式转后缀表达式
②计算后缀表达式
步骤一:中缀表达式转为后缀表达式
1)设立一个操作符栈,用来临时存放操作符;设立一个数组或者队列,用来存放后缀表达式
2)从左至右扫描中缀表达式,如果碰到操作数(注意:操作数可能不止一位,因此需要一位一位读入然后合并到一起,具体实现见代码),就把操作数加入到后缀表达式中。
3)如果碰到操作符op,就将其优先级与操作符栈栈顶的操作符优先级进行比较。 如果op优先级高于栈顶操作符,就将op压入操作符栈中; 如果op优先级低于或等于栈顶操作符优先级,则将操作符栈的操作符不断弹出到后缀表达式中,直到op的优先级高于栈顶操作符的优先级。
4)不断重复上述操作,直到中缀表达式扫描完毕,之后若操作符栈中仍有元素,就将它们依次弹出到后缀表达式中。
步骤二:计算后缀表达式
从左到右扫描后缀表达式,如果是操作数,就压入栈中;如果是操作符,就连续弹出两个操作数(注意:后弹出的是第一操作数,先弹出的是第二操作数),然后进行操作符的操作计算,生成的新操作数再压入栈中。反复到后缀表达式扫描完毕,这时栈中仅存的最后一个数就是答案。
-
代码实现
/* 测试数据: ①1 + 3 * 5 / 4 * 8 / 9 * 6 * 2 / 3 / 7 + 3 * 8 / 2 = 14.90 ②2 / 3 * 4 = 2.67 ③5 + 2 * 3 / 49 - 4 / 13 = 4.81 */ #include<bits/stdc++.h> using namespace std; struct node{ double num; char op; bool flag; //判断是操作符还是操作数 }; string str; stack<node> s; //操作符栈 queue<node> q; //后缀表达式 map<char, int> op; //将中缀表达式转为后缀表达式 void Change(){ node temp; //注意for循环括号里没有"i++” for(int i = 0; i < str.length(); ){ if(str[i] >= '0' && str[i] <= '9'){ temp.flag = 1; //表明是操作数 //计算操作数,注意:操作数可能不止1位!!! temp.num = str[i++] -'0'; while(i < str.length() && str[i] >= '0' && str[i] <= '9'){ temp.num = temp.num * 10 + (str[i] - '0'); i++; } q.push(temp);//将操作数压入到后缀表达式队列中 }else{//如果是操作符 temp.flag = 0; //标记是操作符 //如果op优先级低于等于栈顶元素优先级,将栈顶元素弹出到后缀表达式队列中 while(!s.empty() && op[str[i]] <= op[s.top().op]){ q.push(s.top()); s.pop(); } temp.op = str[i]; s.push(temp); i++; } } //中缀表达式扫描完毕,但栈中仍有操作符,将其放入到后缀表达式队列中 while(!s.empty()){ q.push(s.top()); s.pop(); } } //计算后缀表达式 double Cal(){ double temp1, temp2; node cur, temp; while(!q.empty()){ //后缀表达式队列非空 cur = q.front(); q.pop(); if(cur.flag == true)s.push(cur);//是操作数直接压入到栈中 else{ temp2 = s.top().num;//弹出第二操作数 s.pop(); temp1 = s.top().num;//弹出第一操作数 s.pop(); temp.flag = true; //临时记录操作数 if(cur.op == '+')temp.num = temp1 + temp2; else if(cur.op == '-')temp.num = temp1 - temp2; else if(cur.op == '*')temp.num = temp1 * temp2; else temp.num = temp1 / temp2; s.push(temp);//把计算得到的新操作数压入栈中 } } return s.top().num; //栈中剩下的最后一个数就是答案 } int main(){ op['+'] = op['-'] = 1; //设定操作符的优先级 op['*'] = op['/'] = 2; //输入一行字符串,包括有空格的也输入;当一行中只有0时结束循环 while(getline(cin, str), str != "0"){ for(string::iterator it = str.end(); it != str.begin(); it--){ if(*it == ' ')str.erase(it);//把表达式中的空格去掉;这个代码模板很值得学习 } while(!s.empty())s.pop();//初始化栈 Change();//将中缀表达式转为后缀表达式 printf("%.2f\n", Cal());//计算后缀表达式 } return 0; }
-
-
二、1队列
-
队列的使用:#include + using namespace std;
-
队列的定义:queue q;
-
队列中元素的访问:只有两种——q.front();访问队列首元素;q.back()访问队列尾元素
-
队列的函数:
-
q.push(x);//压入数值 q.front();q.back();//访问队首和队尾 q.pop();//队首元素出队 q.empty();//判空,若为空,返回true,否则返回false q.size();//返回队列中的元素个数 //上述操作的时间复杂度均为O(1)
-
队列的清空:一种为while循环,逐个pop(),一种为重新定义一个队列。
-
-
队列的常见用途:
- 当需要实现广度优先遍历的时候,可以用queue,而不用自己去编写队列
二、2优先队列
-
优先队列:priority_queue,在优先队列中,队首元素一定是优先级别最高的一个。使用为:#include+using namespace std;【此处写法和上面队列一致】
-
优先队列的定义:priority_queue q;
-
优先队列的元素访问:与队列不同的是,只有一个q.top();对队首元素进行访问,类似于栈
-
优先队列的常用函数:
-
q.push(x);//时间复杂度为O(logn) q.top(); q.pop();//令堆顶元素出队,时间复杂度为O(logn)。 q.empty();//true为空,否则为非空 q.size();//返回个数
-
-
优先队列中元素优先级的设置:
-
基本数据类型与结构体:
//基本数据类型 priority_queue<int,vector<int>,less<int> > q;//vector<int>是来填写元素的参数类型的,如int、char等,less<int>表示数字大的优先级高 priority_queue<char,vector<char>,greater<char> > q;//数字小的优先级别高 //结构体类型 struct fruit{ string name; int price; }f1,f2,f3; struct cmp{//优先级函数 bool operator () (fruit f1,fruit f2){ return f1.price>f2.price; } } //上述优先级函数中,bool operator (),是一个定式。f1.price>f2.price表示价格低的优先级高。因为此函数本身是重载小于符号,也就是本身定义的是数值高的优先级高,如果需要定义数值小的优先级别高,则需要用大于符号。 priority_queue<fruit,vector<fruit>,cmp> q; //如果传入的数据较为庞大,如字符串或者数组类型,可以加const和&【引用】 bool operator()(const fruit &f1,const fruit &f2){ return f1.price>f2.price; }
-
优先级设置与sort中cmp函数的符号相反。因为sort中return的符号就是本身的意思;而此处由于本身是数值大的优先级别高,返回的是<号,所以当数值小的优先级别高时,返回的就是>号。
-
-
优先队列的常见用途:
- 解决一些贪心问题【前后联系!!】,也可以对Dijkstra算法进行优化。
三、递归、快速幂
-
递归最重要的是递归边界和递归式,以实现将问题规模减小。
-
递归题型:全排列【可以结合STL中的next_permutation()函数】、斐波那契数列。
-
递归的几个片段
-
x + ( x − 1 ) + … … + y x + (x-1) + …… + y x+(x−1)+……+y
int f(int x, int y){ if(x == y) return x; return x + f(x-1,y); }
-
x ∗ ( x − 1 ) ∗ ( x − 2 ) ∗ … … ∗ 4 x * (x-1) * (x-2) *…… * 4 x∗(x−1)∗(x−2)∗……∗4
int f(int x){ if(x <= 3){ return 1; } return x * f(x-1);//x-1在递归中会一直减下去 }
-
f ( x ) = { 1 x = 1 2 x = 2 f ( x − 1 ) + 2 f ( x − 2 ) x > 2 f(x) = \begin{cases} 1 & x = 1\\ 2 & x = 2\\ f(x-1) + 2f(x-2) & x > 2 \end{cases} f(x)=⎩⎪⎨⎪⎧12f(x−1)+2f(x−2)x=1x=2x>2
int f(int x){ if( x == 1 || x == 2){ return x; } return f(x - 1) + 2 * f(x - 2); }
-
-
快速幂的模板
-
求a ^ b % m的值
- 如果b为奇数:a^b = a * a ^ (b-1);
- 如果b为偶数: a^b = a^(b/2) * a^(b/2);
typedef long long LL; LL binaryPow(LL a, LL b, LL m){ if(b == 0)return 1;//一个数的0次方为1 if(b % 2 == 0)return a * binaryPow(a, b - 1, m) % m;//此处b % 2 == 0 可以用 if(b & 1)代替,判断b的末尾是否为1,进行了位与操作。可以加快执行速度 else{//b为偶数 LL mul = binaryPow(a, b / 2, m);//此处先算出单个,最后再相乘,降低时间复杂度 return mul * mul % m;//得到结果 } }
- 在进入递归函数前需要进行的操作
- 如果a的值大于等于m,可以先让a%m,再传值,减少计算量
- 如果p == 1,则直接判断最后结果为0。
-
四、链表
-
new动态分配内存:int *p = new int; node *p = new node;
搭配的delete(p)
-
动态链表的创建、查询、元素插入、元素删除操作
//创建结点 struct node{ typename data;//数据域 node* next;//指针域 } //创建链表 node* create(int array[]){ node* p, *pre, *head;//pre保存当前结点的前驱结点,head为头结点 head = new node; //创建头结点 head -> next = NULL;//头结点不需要数据域,指针域初始化为NULL pre = head; for(int i = 0; i < 5; i++){ p = new node; //将array数组中数赋值给结点作为数据域 p->data = array[i]; p->next = NULL;//新结点的指针域设置为NULL pre->next = p; pre = p;//把pre设置为p,作为下一个结点的前驱结点 } return head;//返回头结点指针 } int main(){ int array[5] = {5, 3, 6,1, 2}; node* L = create(array); L = L -> next; //从第一个结点开始有数据域 while(L != NULL){ printf("%d", L->data); L = L->next; } return 0; } //查找元素 //在以head为头结点的链表上计数元素x的个数 int search(node* head, int x){ int count = 0; node* p = head -> next;//从第一个结点开始 while(p!=NULL){ if(p->data == x){ count++: } p = p->next; } return count;//返回计数器 } //插入元素 //将x插入以head为头结点的链表的第pos个位置上 void insert(node* head, int pos, int x){ node *p = head; for(int i = 0; i < pos - 1; i++){//pos-1是为了到插入位置的前一个结点,这样子方便操作 p = p->next; } node* q = new node; q->data = x; q->next = p->next; p->next = q;//前一个位置的结点指向新结点。 } //删除元素 //删除以head为头结点的链表中所有数据域为x的元素 void del(node* head, int x){ node* p = head->next; node* pre = head; //pre始终保存p的前驱结点的指针。 while(p != NULL){ if(p->data == x){ pre->next = p->next; delete(p); p = pre -> next; }else{//如果数据域不是x,那么pre和p指针都往后移。这里保证pre始终是p的前驱结点的位置。 pre = p; p = p->next; } } }
-
静态链表
-
有些问题结点的地址不是很大的时候(比如5位数的地址),就可以使用静态链表来做。静态链表的原理是hash。它不需要头结点
-
静态链表的结点定义如下:
struct Node{ typename data; int next;//指针域,作为下标 }node[size];
-
注意:静态链表的结构体类型名和结构体变量名不能相同(像上面就是Node与node不能相同)!如果相同,sort函数会出错。
-
-
题目多为静态链表,结合sort函数来使用。