设计包含min函数的栈
题目:定义栈的数据结构,要求添加一个min函数,能够得到栈的最小元素。要求函数min、push以及pop的时间复杂度都是O(1)。
分析:这是去年google的一道面试题。
我看到这道题目时,第一反应就是每次push一个新元素时,将栈里所有逆序元素排序。这样栈顶元素将是最小元素。但由于不能保证最后push进栈的元素最先出栈,这种思路设计的数据结构已经不是一个栈了。
在栈里添加一个成员变量存放最小元素(或最小元素的位置)。每次push一个新元素进栈的时候,如果该元素比当前的最小元素还要小,则更新最小元素。
乍一看这样思路挺好的。但仔细一想,该思路存在一个重要的问题:如果当前最小元素被pop出去,如何才能得到下一个最小元素?
因此仅仅只添加一个成员变量存放最小元素(或最小元素的位置)是不够的。我们需要一个辅助栈。每次push一个新元素的时候,同时将最小元素(或最小元素的位置。考虑到栈元素的类型可能是复杂的数据结构,用最小元素的位置将能减少空间消耗)push到辅助栈中;每次pop一个元素出栈的时候,同时pop辅助栈。
参考代码:
1 #include <deque> 2 #include <assert.h> 3 4 template <typename T> class CStackWithMin 5 { 6 public: 7 CStackWithMin(void) {} 8 virtual ~CStackWithMin(void) {} 9 10 T& top(void); 11 const T& top(void) const; 12 13 void push(const T& value); 14 void pop(void); 15 16 const T& min(void) const; 17 18 private: 19 deque<T> m_data; // the elements of stack 20 deque<size_t> m_minIndex; // the indices of minimum elements 21 }; 22 23 // get the last element of mutable stack 24 template <typename T> T& CStackWithMin<T>::top() 25 { 26 return m_data.back(); 27 } 28 29 // get the last element of non-mutable stack 30 template <typename T> const T& CStackWithMin<T>::top() const 31 { 32 return m_data.back(); 33 } 34 35 // insert an elment at the end of stack 36 template <typename T> void CStackWithMin<T>::push(const T& value) 37 { 38 // append the data into the end of m_data 39 m_data.push_back(value); 40 41 // set the index of minimum elment in m_data at the end of m_minIndex 42 if(m_minIndex.size() == 0) 43 m_minIndex.push_back(0); 44 else 45 { 46 if(value < m_data[m_minIndex.back()]) 47 m_minIndex.push_back(m_data.size() - 1); 48 else 49 m_minIndex.push_back(m_minIndex.back()); 50 } 51 } 52 53 // erease the element at the end of stack 54 template <typename T> void CStackWithMin<T>::pop() 55 { 56 // pop m_data 57 m_data.pop_back(); 58 59 // pop m_minIndex 60 m_minIndex.pop_back(); 61 } 62 63 // get the minimum element of stack 64 template <typename T> const T& CStackWithMin<T>::min() const 65 { 66 assert(m_data.size() > 0); 67 assert(m_minIndex.size() > 0); 68 69 return m_data[m_minIndex.back()]; 70 }
举个例子演示上述代码的运行过程:
步骤 数据栈 辅助栈 最小值
1.push 3 3 0 3
2.push 4 3,4 0,0 3
3.push 2 3,4,2 0,0,2 2
4.push 1 3,4,2,1 0,0,2,3 1
5.pop 3,4,2 0,0,2 2
6.pop 3,4 0,0 3
7.push 0 3,4,0 0,0,2 0
讨论:如果思路正确,编写上述代码不是一件很难的事情。但如果能注意一些细节无疑能在面试中加分。比如我在上面的代码中做了如下的工作:
· 用模板类实现。如果别人的元素类型只是int类型,模板将能给面试官带来好印象;
· 两个版本的top函数。在很多类中,都需要提供const和非const版本的成员访问函数;
· min函数中assert。把代码写的尽量安全是每个软件公司对程序员的要求;
· 添加一些注释。注释既能提高代码的可读性,又能增加代码量,何乐而不为?
总之,在面试时如果时间允许,尽量把代码写的漂亮一些。说不定代码中的几个小亮点就能让自己轻松拿到心仪的Offer。
以上转自何海涛博客
后面回帖中,有人提出了一个优化算法,可以减小维护最小值的辅助空间。
除了题目要求的栈之外新开一个栈,用来记录最小值,每当在原栈中push数据后,与最小值栈中的栈顶元素比较,如果新值较小,则在最小值栈中push新值;否则再次push栈顶元素.
pop的时候,只要将最小值栈也pop一下就行了.
这样,min函数只需要返回最小值栈的栈顶元素即可.
常规解空间上的一个优化:
一般说来,最小值不会每次都需要更新,因此最小值栈里面会有很多重复元素.因此一个简单的优化就是在新值只当<=原最小值时才 pushIntoMin,注意这个==的条件是不可少的,这是为了防止在pop的时候错误的pop最小值.pop的是, 当待pop值==最小值时popMinStack, 其他时候不对最小值栈进行pop
下面说一种具有常数空间复杂度的方法:
在这个方法里,只需要额外开一个用于存放当前最小值的变量min即可.因此下面提到的push和pop操作都是对于题目中要求的栈来操作的,当然,这也是这个算法里唯一的栈.
设push的参数为v_push,pop的返回值为v_pop.
先说下整体思路:因为栈中所有元素的值都不会小于当其为栈顶元素时min函数的值,所以在栈中其实只需要保存某元素比相应最小值大出来的值就可以 了.而对于最小值更新的位置,栈元素肯定为0,因此可以利用这个位置来保存更多的信息,在这里是更新后前两个最小值的差值,而这个值肯定是非正的.
根据上面的思路,push函数按照如下策略进行:
首先push (v_push-min),如果v_push < min,更新min为v_push.
相应的,pop函数按照如下策略进行(称栈顶元素为top):
如果top >= 0, v_pop = min+top, 如果top < 0, v_pop = min,然后更新min为min-top.
显然,对于min函数来说,只需要返回min空间的内容即可.
与张霄学长交流后,学长也讲了一个类似的方法:
push时候 如果 v_push >= min, v_push 直接入栈, 如果 v_push < min, 那么入栈的是 2 * v_push - min, 然后 min = v_push. 出栈时, 如果栈顶的top >= min 直接出,如果 top < min 则出现异常,将min作为pop的返回值,另外需要还原前一个最小值,方法是 min = 2 * min - top
其实这两种方法在思路上是完全一样的,只是学长提供的方法里,栈中所有元素都比我的方法里大v_push.不过,这种变化直接带来的好处是,在不更 新最小值的情况下,压栈值和出栈值都不需要额外的计算,在高级语言层面上,一次加减法运算比单纯的赋值至少多了一次访存操作和一次alu的运算,这样估计 来我的方法耗时会是学长方法的2倍左右,虽然时间复杂度都是一样的.
以上转自anchor89