栈和队列的基础算法学习(EPI)
今天学习的时间虽然挺多的,但是总觉效率不高。其实今天没有按照计划进行EPI题目的浏览,白天去看了其他的书籍。准备找工作可能需要的状态是一定量经典的书,偶尔温习才可。书是看不完的,知识点也是固定的。所以从把手头的几本书在浏览完毕之后就要着手复习之前的知识啦。C++的知识,leetcode的题目,操作系统,数据库,网络的学习笔记~。
1. 实现一个栈,支持返回当前栈中最大值的操作。要求,返回最大值的操作时间复杂度O(1)。可以使用额外的O(n)的空间复杂度。
题目之前见到过,所以思路一下子就有了,利用额外的栈存储当前栈中最大值即可。
简单的思路,在元素入栈的时候,辅助栈同时压入当前栈的最大值,元素出栈的时候,弹出辅助栈的元素。取得最大值直接从辅助栈栈頂获取即可。
另外,可以减少一下压入辅助栈中的元素的数量,只有压入当前栈的元素大于等于当前辅助栈栈顶元素,才将该至压入辅助栈栈顶,出栈的时候,只有出栈元素等于辅助栈栈顶元素时,才将辅助栈栈顶弹出。
另外,能在常数项更优化的一个方法是,辅助栈中压入一个数对,数对的意义是当前的最大值,以及当前最大值的数量,push的时候如果相等,则增加数对的第二个值,pop的时候如果相等,减去数对中的第二个值,如果减至0,则pop辅助栈即可。
扩展问题,实现一个队列,支持返回当前最大值的操作,能否优化至O(1)的时间复杂度。
这个题目还是比较复杂的,需要结合两个知识来把时间复杂度均摊为O(1),及利用两个已经实现最大值操作的堆栈来模拟队列,可以达到均摊O(1)复杂度的返回最大值的队列。
其实利用deque也能够达到O(1)的均摊成本,但是时间复杂度分析比较复杂。
均摊的理解是每个元素最多进入D和离开D一次,不会第二次进入和离开D。所以入队的均摊O(1)。
2.实现逆波兰表达式的计算并返回计算结果。
利用栈不断的压入数字,遇到运算符号,从数字栈中取出两个数字进行计算,结果继续压入数字栈,直到计算完毕,数字栈中存储的运算结果。
3.迭代方法实现BST的中序遍历。
利用栈的迭代实现不是很复杂,而且可以完成O(1)空间复杂度的morris中序遍历。
而且迭代实现二叉树的遍历方法中,后序遍历的实现是难度最大的。morris的后序遍历实现起来难度也很大。
4.针对随机链表,即除了next域之外,增加一个random指针域的链表,迭代的方法进行random-first序的遍历。
递归的方法比较容易实现,这里让进行迭代即利用stack来模拟递归的方法即可。
template <typename T> void search_postings_list(const shared_ptr<node_t<T> > &L) { stack<shared_ptr<node_t<T> > > s; int order = 0; s.emplace(L); while(!s.empty()) { shared_ptr<node_t<T> > curr = s.top(); s.pop(); if( curr && curr->order == -1 ) { curr->order = order++; s.emplace(curr->next); s.emplace(curr->jump); } } }
5.利用栈记录汉诺塔的移动过程,模拟。
void transfer(const int &n, array<stack<int>,3> &pegs, const int &from, const int & to, const int &use) { if(n > 0) { transfer(n-1,pegs,from,use,to); pegs[to].push(pegs[from].top()); pegs[from].pop(); cout<<"Move from peg"<<from<<"to peg"<<to<<endl; transfer(n-1,pegs,use,to,from); } } void move_tower_hanoi(const int &n) { array<stack<int>,3> pegs; for(int i = n;i >= 1;--i) { pegs[0].push(i); } transfer(n,pegs,0,1,2); }
6.一排楼房所有的窗户都面朝西,太阳落下的时候如果一个楼房的西面不存在比该楼房高的楼房,则该楼房可以看到阳光,从东至西计算出所有可以看到阳光的楼房。
扩展问题:从西至东计算出所有可以看到阳光的楼房。
问题的根本思路是利用栈,从东至西维护一个递减的序列。从西至东则需要维护一个递增的序列。其中序列的内容及为可看到阳光的楼房。这种利用栈维护单调序列的方法在求最大直方图面积中应用可以减少时间复杂度至O(n)。
从东至西的递减序列code
template <typename T> vector<pair<int,T>> examine_buildings_with_sunset(istringstream &sin) { int idx = 0;//building's index T height; //Stores(buiding_idx,building_height) pair with sunset views vector<pair<int,T> > buildings_with_sunset; while(sin >> height) { while(buildings_with_sunset.empty() == false && height >= buildings_with_sunset.back().second) { buildings_with_sunset.pop_back(); } buildings_with_sunset.emplace_back(idx++,height); } return buildings_with_sunset; }
7.设计一个排序堆栈的算法仅仅利用push,pop,empty,top操作,并且不显示的开辟额外空间。
如果不显示的开辟额外空间,就想想利用函数递归调用的堆栈存储信息,这样就能够进行排序。比如插入排序的递归实现方法。
template <typename T> void sort(stack<T> &S) { if(!S.emtpy()) { T e = S.top(); S.pop(); sort(S); insert(S,e); } } template <typename T> void insert(stack<T> &S,const T &e) { if(S.empty() || S.top() <= e) { S.push(e); }else { T f = S.top(); S.pop(); insert(S,e); S.push(f); } }
8.将包含..和.的文件路径名简化至最短路径。
思路就是保持一个栈,当遇到..的时候讲栈顶的目录名弹出,遇到.则忽略。这样就可以得到最短路径了。该题目边界情况较多,属于细节题。
9.层序遍历BST的方法。
层序遍历,即广度优先搜索,需要借助队列来完成功能。
10.利用两个整数实现一个队列的功能。队列中能够加入的元素为[0,9]。
首先看到这个题目想到的是将整数看成位数组,4位可以表示[0,15]之间的数字,所以可以当成数组。
后来参考答案看到按照十进制的位来表示[0,9]计算起来更加方便。刚好十进制的每位都是[0,9],一个数字作为数组,另外一个数字作为队列的长度。注意全为0的边界情况。
扩展问题:如果只有一个整数,可以使用其中一位保存长度。
11.利用两个栈实现一个队列,使得pop和push的均摊成本O(1)。
一个栈负责进入队列,另外一个栈负责弹出队列。
12.O(1)返回队列max值的队列的应用。
应用场景是现在有一个数组,数组是一个数对,第一个元素表示一个时间戳,第二个元素表示该事件的流量,数组已经根据时间戳有序的,现在需要计算每个时间戳至时间戳+w这段时间内的最大流量是多少。
如果直接解法则需要遍历当前时间戳的流量至当前时间戳+w这段时间内所有流量,时间复杂度O(nw)。
如果利用优先队列的话因为是最小堆实现的,所以可以优化至O(nlogw)的时间复杂度。
如果利用O(1)返回最大值的队列,则优化至了O(n)的时间复杂度。