设计一个栈使得 getMinimum() 的时间复杂度为 O(1)。

126

这是一道面试题。

您需要设计一个存储整数值的栈,使得 getMinimum() 函数返回栈中的最小元素。

例如:

案例 #1

5 ← 栈顶
1
4
6
2

当调用 getMinimum() 时,应该返回 1,即栈中的最小元素。

案例 #2

stack.pop()
stack.pop()

注意:5 和 1 都已经弹出了栈。 弹出后,栈看起来像这样:

4 ← 栈顶
6
2

当调用 getMinimum() 时,应该返回 2,这是栈中的最小元素。

约束条件:

  1. getMinimum 函数的时间复杂度应为 O(1)
  2. 在设计时还必须考虑空间限制,并且如果使用额外的空间,则应为常量空间。

GeeksforGeeks 设计一个支持在 O(1) 时间和 O(1) 额外空间内获取最小值的栈。 - greybeard
31个回答

183

编辑:这不符合“常数空间”约束条件-它基本上会使所需的空间加倍。但我非常怀疑,如果没有在某个地方破坏运行时复杂性(例如使push/pop O(n)),就不会有解决方案。请注意,这并不改变所需空间的复杂度,例如,如果您有一个具有O(n)空间要求的堆栈,则仍将是O(n),只是具有不同的常数因子。

非常数空间解决方案

保留一个“重复”的堆栈,其中包含“堆栈中所有较低值的最小值”。当您弹出主堆栈时,请同时弹出min堆栈。当您推入主堆栈时,请推入新元素或当前最小值中的较低者。getMinimum()实现为minStack.peek()

因此,使用您的示例,我们将拥有:

Real stack        Min stack

5  --> TOP        1
1                 1
4                 2
6                 2
2                 2

弹出两次后,您将得到:

Real stack        Min stack

4                 2
6                 2
2                 2

如果这不够详细,请告诉我。一旦你理解了,它就很简单了,但一开始可能需要一点思考:)

当然,缺点是它会使空间需求加倍。执行时间并没有显著的影响,即复杂度仍然相同。

编辑:有一个稍微棘手一些但通常具有更好的空间的变体。我们仍然有最小堆栈,但只有在从主堆栈弹出的值等于最小堆栈上的值时才从中弹出。只有在将要推到主堆栈的值小于或等于当前最小值时,我们才将其推到最小堆栈上。这允许重复的最小值。getMinimum()仍然只是一个查看操作。例如,对原始版本再次推入1,我们将得到:

Real stack        Min stack

1  --> TOP        1
5                 1
1                 2
4                 
6                 
2                 

从上面的两个堆栈中弹出,因为1 == 1,留下:
Real stack        Min stack

5  --> TOP        1
1                 2
4                 
6                 
2                 

再次执行弹出操作,只会从主栈中弹出元素,因为 5 > 1:
Real stack        Min stack

1                 1
4                 2
6                 
2                 

再次弹出时,两个堆栈都会弹出,因为1 == 1。
Real stack        Min stack

4                 2
6                 
2                 

这会导致最坏情况下的空间复杂度相同(原堆栈的两倍),但如果我们很少遇到“新最小值或相等”,则空间利用率更好。
编辑:这里是Pete邪恶计划的实现。我没有进行全面测试,但我认为它还可以 :)
using System.Collections.Generic;

public class FastMinStack<T>
{
    private readonly Stack<T> stack = new Stack<T>();
    // Could pass this in to the constructor
    private readonly IComparer<T> comparer = Comparer<T>.Default;

    private T currentMin;

    public T Minimum
    {
        get { return currentMin; }
    }

    public void Push(T element)
    {
        if (stack.Count == 0 ||
            comparer.Compare(element, currentMin) <= 0)
        {
            stack.Push(currentMin);
            stack.Push(element);
            currentMin = element;
        }
        else
        {
            stack.Push(element);
        }
    }

    public T Pop()
    {
        T ret = stack.Pop();
        if (comparer.Compare(ret, currentMin) == 0)
        {
            currentMin = stack.Pop();
        }
        return ret;
    }
}

3
很聪明!@ Ganesh:运行时间为什么会成为问题?它只需要比单个栈慢两倍,即push(),pop()和getMinimum()的时间复杂度仍然是O(1) -- 这是优秀的性能! - j_random_hacker
4
如果你只有一个变量,在你的例子中弹出“1”会发生什么?它必须计算出先前的最小值是“2”,但如果没有扫描全部内容,它无法完成这个任务。 - Jon Skeet
2
仅仅阅读你之前的评论,当你说“在栈设计本身”时,你是指“在每个元素中”吗?如果是这样,那么根据元素类型的大小,你仍然可能将内存需求几乎增加一倍。从概念上讲,这与两个栈是相同的。 - Jon Skeet
1
我没有看到谁回答了这个问题,不必要地读了其他的答案! - Praveen S
1
@committedandroider:读一下评论,我想。请记住,我已经五年没有详细研究过这个了... - Jon Skeet
显示剩余22条评论

41
添加一个字段来保存最小值并在 Pop() 和 Push() 过程中更新它。这样 getMinimum() 的时间复杂度就变为 O(1),但是 Pop() 和 Push() 需要做更多的工作。
如果弹出的是最小值,Pop() 将会是 O(n),否则它们仍然都是 O(1)。当重新调整栈大小时,Push() 的时间复杂度将成为栈实现的 O(n)。
以下是一个快速的实现。
public sealed class MinStack {
    private int MinimumValue;
    private readonly Stack<int> Stack = new Stack<int>();

    public int GetMinimum() {
        if (IsEmpty) {
            throw new InvalidOperationException("Stack is empty");
        }
        return MinimumValue;
    }

    public int Pop() {
        var value = Stack.Pop();
        if (value == MinimumValue) {
            MinimumValue = Stack.Min();
        }
        return value;
    }

    public void Push(int value) {
        if (IsEmpty || value < MinimumValue) {
            MinimumValue = value;
        }
        Stack.Push(value);
    }

    private bool IsEmpty { get { return Stack.Count() == 0; } }
}

11
在使用 pop() 操作时,需要找到“新”的最小元素,这需要 O(n) 的时间。而 push() 操作不受影响,因为该操作仍然是 O(1)。 - Georg Schölly
4
@sigjuice:没错,我想把“suffer”这个词换成一个不那么夸张的词。 :) 翻译:我认为我会将“suffer”一词改为更加温和的措辞。 :) - Brian Rasmussen
2
如果您在N个元素中有一个额外的字段,则“元素添加字段”不是常量空间,而是额外的O(N)。 - Pete Kirkham
1
如果在操作期间从堆栈中弹出了最小值,那么下一个最小值如何找到? 这种方法不支持该场景... - Sharat Chandra
@BrianRasmussen 您已经使用了 Stack.min() 来获取 Stack.pop() 后的最小值。但是 Stack.min() 的时间复杂度是多少?它是否会在内部扫描整个 Stack 来获取最小元素? - SkrewEverything
显示剩余9条评论

16
public class StackWithMin {
    int min;
    int size;
    int[] data = new int[1024];

    public void push ( int val ) {
        if ( size == 0 ) {
            data[size] = val;
            min = val;
        } else if ( val < min) {
            data[size] = 2 * val - min;
            min = val;

            assert (data[size] < min); 
        } else {
            data[size] = val;
        }

        ++size;

        // check size and grow array
    }

    public int getMin () {
        return min;
    }

    public int pop () {
        --size;

        int val = data[size];

        if ( ( size > 0 ) && ( val < min ) ) {
            int prevMin = min;
            min += min - val;
            return prevMin;
        } else {
            return val;
        }
    }

    public boolean isEmpty () {
        return size == 0;
    }

    public static void main (String...args) {
        StackWithMin stack = new StackWithMin();

        for ( String arg: args ) 
            stack.push( Integer.parseInt( arg ) );

        while ( ! stack.isEmpty() ) {
            int min = stack.getMin();
            int val = stack.pop();

            System.out.println( val + " " + min );
        }

        System.out.println();
    }

}

它显式地存储当前的最小值,如果最小值发生变化,则不会将该值推入栈中,而是推入一个相同差异的值到新最小值的另一侧(例如,如果min = 7并且您push 5,则会推入3而不是5-|7-5|=3),然后将min设置为5;如果在min为5时弹出3,则它会看到弹出的值小于min,所以逆转过程以获得7作为新的最小值,然后返回先前的min。由于任何不改变当前最小值的值都大于当前最小值,因此可以用它来区分改变最小值和不改变最小值的值。

在使用固定大小整数的语言中,你从值的表示中借用了一点空间,因此可能会下溢并导致assert失败。但是除此之外,它是恒定的额外空间,所有操作仍然是O(1)。

基于链表的堆栈有其他可以借用的地方,例如在C中使用next指针的最低有效位,在Java中使用链接列表中对象的类型。对于Java来说,这意味着与连续堆栈相比使用了更多的空间,因为每个链接都有对象开销:

public class LinkedStackWithMin {
    private static class Link {
        final int value;
        final Link next;

        Link ( int value, Link next ) {
            this.value = value;
            this.next = next;
        }

        int pop ( LinkedStackWithMin stack ) {
            stack.top = next;
            return value;
        }
    }

    private static class MinLink extends Link {
        MinLink ( int value, Link next ) {
            super( value, next );
        }

        int pop ( LinkedStackWithMin stack ) {
            stack.top = next;
            int prevMin = stack.min;
            stack.min = value;
            return prevMin;
        }
    }

    Link top;
    int min;

    public LinkedStackWithMin () {
    }

    public void push ( int val ) {
        if ( ( top == null ) || ( val < min ) ) {
            top = new MinLink(min, top);
            min = val;
        } else {
            top = new Link(val, top);
        }
    }

    public int pop () {
        return top.pop(this);
    }

    public int getMin () {
        return min;
    }

    public boolean isEmpty () {
        return top == null;
    }

C语言中并不存在这样的开销,而且您可以借用下一个指针的最低位:

typedef struct _stack_link stack_with_min;

typedef struct _stack_link stack_link;

struct _stack_link {
    size_t  next;
    int     value;
};

stack_link* get_next ( stack_link* link ) 
{
    return ( stack_link * )( link -> next & ~ ( size_t ) 1 );
}

bool is_min ( stack_link* link )
{
    return ( link -> next & 1 ) ! = 0;
}

void push ( stack_with_min* stack, int value )
{
    stack_link *link = malloc ( sizeof( stack_link ) );

    link -> next = ( size_t ) stack -> next;

    if ( (stack -> next == 0) || ( value == stack -> value ) ) {
        link -> value = stack -> value;
        link -> next |= 1; // mark as min
    } else {
        link -> value = value;
    }

    stack -> next = link;
}

etc.;

然而,这些算法都不是真正的O(1)。它们在实践中不需要更多的空间,因为它们利用了这些语言中数字、对象或指针表示中的空洞。但是,一个使用更紧凑表示的理论机器在每种情况下都需要添加一个额外的比特位到该表示中。


非常优雅... C++ 版本轻松移植 在 ideone 运行。干杯。 - Tony Delroy
在Java中,如果最后一个推送的值是Integer.MIN_VALUE(例如:push 1,push Integer.MIN_VALUE,pop),则会产生错误的pop()结果。这是由于上面提到的下溢。否则,对于所有整数值都有效。 - Theo
表扬提到在存储差异时添加一个二进制位。 - greybeard

15

我找到了一种解决方案,它满足所有提到的限制(常数时间操作)和常数额外空间

思路是存储最小值与输入数字之间的差异,并在不再是最小值时更新最小值。

代码如下:

public class MinStack {
    long min;
    Stack<Long> stack;

    public MinStack(){
        stack = new Stack<>();
    }

    public void push(int x) {
        if (stack.isEmpty()) {
            stack.push(0L);
            min = x;
        } else {
            stack.push(x - min); //Could be negative if min value needs to change
            if (x < min) min = x;
        }
    }

    public int pop() {
        if (stack.isEmpty()) return;

        long pop = stack.pop();

        if (pop < 0) {
            long ret = min
            min = min - pop; //If negative, increase the min value
            return (int)ret;
        }
        return (int)(pop + min);

    }

    public int top() {
        long top = stack.peek();
        if (top < 0) {
            return (int)min;
        } else {
           return (int)(top + min);
        }
    }

    public int getMin() {
        return (int)min;
    }
}

该解法来自:https://leetcode.com/discuss/15679/share-my-java-solution-with-only-one-stack


这个可以用。我也在堆栈中尝试了负数。而且也很简单易记。谢谢。 - r9891

6

那么,pushpop的运行时间限制是什么?如果它们不需要是常数级别的,则只需计算这两个操作中的最小值(使它们为On))。否则,我看不出如何使用恒定的额外空间来完成此操作。


4
+1,呵呵……老套路“打擦边球”啊……类似地,我知道一种排序算法可以在O(1)时间内对任何大小的数组进行排序,但访问结果中的任何元素的第一次操作会产生O(nlog n)的开销… :) - j_random_hacker
3
在Haskell中,所有的操作都是常数时间!(除了你想要打印结果的情况) - Henk
2
对于糟糕的问题规范,点个赞。 “我不知道这怎么做” - 我也不知道,但Pete Kirkham的解决方案非常优雅... - Tony Delroy

2

假设我们要处理的堆栈是这样的:

最初的回答:

6 , minvalue=2
2 , minvalue=2
5 , minvalue=3
3 , minvalue=3
9 , minvalue=7
7 , minvalue=7
8 , minvalue=8

在上面的表示中,栈仅由左值构建,右值[minvalue]仅用于说明目的,将存储在一个变量中。
实际问题是当最小值被删除时:此时我们如何知道下一个最小元素是什么,而不必遍历整个栈。
例如,在我们的堆栈中,当6弹出时,我们知道这不是最小元素,因为最小元素是2,因此我们可以安全地删除它而不更新我们的min值。
但是当我们弹出2时,我们可以看到最小值现在是2,如果这个值被弹出,那么我们需要将最小值更新为3。
点1:
现在,如果您仔细观察,我们需要从此特定状态[2,minvalue = 2]生成minvalue = 3。 或者,如果您在堆栈中深入,我们需要从此特定状态[3,minvalue = 3]生成minvalue = 7 或者,如果您在堆栈中继续深入,则需要从此特定状态[7,minvalue = 7]生成minvalue = 8
您是否在上述三种情况中注意到了共同之处?我们需要生成的值取决于两个相等的变量。正确的。 为什么会发生这种情况?因为当我们推送一些比当前minvalue小的元素时,我们基本上将该元素推入堆栈并在minvalue中更新相同的数字。
点2:
因此,我们基本上在堆栈和minvalue变量中存储了相同数字的副本。 我们需要专注于避免这种重复,并在堆栈或minvalue中存储一些有用的数据,以生成上述CASES中的先前最小值。
让我们关注当要存储的值小于最小值时应该在堆栈中存储什么。 让我们将此变量命名为y,因此现在我们的堆栈将如下所示:
6 , minvalue=2
y1 , minvalue=2
5 , minvalue=3
y2 , minvalue=3
9 , minvalue=7
y3 , minvalue=7
8 , minvalue=8

我已将它们重命名为y1、y2、y3,以避免混淆,因为它们都具有相同的值。
第三点:
现在让我们试着找出一些关于y1、y2和y3的限制条件。 你还记得我们何时需要在pop()时更新minvalue吗?只有当我们弹出的元素等于minvalue时才需要更新。 如果我们弹出的是大于minvalue的元素,则不需要更新minvalue。 因此,为了触发minvalue的更新,y1、y2和y3应该小于它们对应的minvalue。[我们避免相等以避免重复[点2]] 所以约束条件是[y < minValue]。
现在让我们回到填充y,我们需要在push时生成一些值并放入y中,请记住。 让我们将要推送的值x取为小于prevMinvalue的值,并将我们实际推送到堆栈中的值取为y。 所以显然新的MinValue=x,而y < newMinvalue。
现在我们需要计算y(记住y可以是任何小于newMinValue(x)的数字),以帮助我们满足约束条件,使用prevMinvalue和x(newMinvalue)。
Let's do the math:
    x < prevMinvalue [Given]
    x - prevMinvalue < 0 
    x - prevMinValue + x < 0 + x [Add x on both side]
    2*x - prevMinValue < x      
this is the y which we were looking for less than x(newMinValue).
y = 2*x - prevMinValue. 'or' y = 2*newMinValue - prevMinValue 'or' y = 2*curMinValue - prevMinValue [taking curMinValue=newMinValue].

如果在推入x的时候它小于prevMinValue,那么我们就推入y[2*x-prevMinValue]并更新newMinValue = x。

而当弹出时,如果栈中包含小于minValue的元素,那么这就是我们更新minValue的时机。我们需要从curMinValue和y计算prevMinValue。
  y = 2*curMinValue - prevMinValue [已证明]
  prevMinValue = 2*curMinvalue - y。

2*curMinValue - y 是我们现在需要更新为prevMinValue的数字。

以下是采用相同逻辑的代码,时间复杂度为O(1),空间复杂度为O(1)。

// C++ program to implement a stack that supports 
// getMinimum() in O(1) time and O(1) extra space. 
#include <bits/stdc++.h> 
using namespace std; 
  
// A user defined stack that supports getMin() in 
// addition to push() and pop() 
struct MyStack 
{ 
    stack<int> s; 
    int minEle; 
  
    // Prints minimum element of MyStack 
    void getMin() 
    { 
        if (s.empty()) 
            cout << "Stack is empty\n"; 
  
        // variable minEle stores the minimum element 
        // in the stack. 
        else
            cout <<"Minimum Element in the stack is: "
                 << minEle << "\n"; 
    } 
  
    // Prints top element of MyStack 
    void peek() 
    { 
        if (s.empty()) 
        { 
            cout << "Stack is empty "; 
            return; 
        } 
  
        int t = s.top(); // Top element. 
  
        cout << "Top Most Element is: "; 
  
        // If t < minEle means minEle stores 
        // value of t. 
        (t < minEle)? cout << minEle: cout << t; 
    } 
  
    // Remove the top element from MyStack 
    void pop() 
    { 
        if (s.empty()) 
        { 
            cout << "Stack is empty\n"; 
            return; 
        } 
  
        cout << "Top Most Element Removed: "; 
        int t = s.top(); 
        s.pop(); 
  
        // Minimum will change as the minimum element 
        // of the stack is being removed. 
        if (t < minEle) 
        { 
            cout << minEle << "\n"; 
            minEle = 2*minEle - t; 
        } 
  
        else
            cout << t << "\n"; 
    } 
  
    // Removes top element from MyStack 
    void push(int x) 
    { 
        // Insert new number into the stack 
        if (s.empty()) 
        { 
            minEle = x; 
            s.push(x); 
            cout <<  "Number Inserted: " << x << "\n"; 
            return; 
        } 
  
        // If new number is less than minEle 
        if (x < minEle) 
        { 
            s.push(2*x - minEle); 
            minEle = x; 
        } 
  
        else
           s.push(x); 
  
        cout <<  "Number Inserted: " << x << "\n"; 
    } 
}; 
  
// Driver Code 
int main() 
{ 
    MyStack s; 
    s.push(3); 
    s.push(5); 
    s.getMin(); 
    s.push(2); 
    s.push(1); 
    s.getMin(); 
    s.pop(); 
    s.getMin(); 
    s.pop(); 
    s.peek(); 
  
    return 0; 
} 

1
我们可以以O(n)的时间复杂度和O(1)的空间复杂度来完成这个任务,代码如下:
class MinStackOptimized:
  def __init__(self):
      self.stack = []
      self.min = None

  def push(self, x): 
      if not self.stack:
          # stack is empty therefore directly add
          self.stack.append(x)
          self.min = x 
      else:
          """
          Directly add (x-self.min) to the stack. This also ensures anytime we have a 
          negative number on the stack is when x was less than existing minimum
          recorded thus far.
          """
          self.stack.append(x-self.min)
          if x < self.min:
              # Update x to new min
              self.min = x 

  def pop(self):
      x = self.stack.pop()
      if x < 0:
          """ 
          if popped element was negative therefore this was the minimum
          element, whose actual value is in self.min but stored value is what
          contributes to get the next min. (this is one of the trick we use to ensure
          we are able to get old minimum once current minimum gets popped proof is given
          below in pop method), value stored during push was:
          (x - self.old_min) and self.min = x therefore we need to backtrack
          these steps self.min(current) - stack_value(x) actually implies to
              x (self.min) - (x - self.old_min)
          which therefore gives old_min back and therefore can now be set
          back as current self.min.
          """
          self.min = self.min - x 

  def top(self):
      x = self.stack[-1]
      if x < 0:
          """ 
          As discussed above anytime there is a negative value on stack, this
          is the min value so far and therefore actual value is in self.min,
          current stack value is just for getting the next min at the time
          this gets popped.
          """
          return self.min
      else:
          """ 
          if top element of the stack was positive then it's simple, it was
          not the minimum at the time of pushing it and therefore what we did
          was x(actual) - self.min(min element at current stage) let's say `y`
          therefore we just need to reverse the process to get the actual
          value. Therefore self.min + y, which would translate to
              self.min + x(actual) - self.min, thereby giving x(actual) back
          as desired.
          """
          return x + self.min

  def getMin(self):
      # Always self.min variable holds the minimum so for so easy peezy.
      return self.min

很遗憾,docstring中应该有一个提到这只适用于非负数。 - greybeard

0
看到一个很棒的解决方案: https://www.geeksforgeeks.org/design-a-stack-that-supports-getmin-in-o1-time-and-o1-extra-space/ 下面是我按照算法编写的Python代码:
class Node:
    def __init__(self, value):
        self.value = value
        self.next = None

class MinStack:
    def __init__(self):
        self.head = None
        self.min = float('inf')

    # @param x, an integer
    def push(self, x):
        if self.head == None:
            self.head = Node(x)
            self.min = x
        else:
            if x >= self.min:
                n = Node(x)
                n.next = self.head
                self.head = n
            else:
                v = 2 * x - self.min
                n = Node(v)
                n.next = self.head
                self.head = n
                self.min = x

    # @return nothing
    def pop(self):
        if self.head:
            if self.head.value < self.min:
                self.min = self.min * 2 - self.head.value
            self.head = self.head.next

    # @return an integer
    def top(self):
        if self.head:
            if self.head.value < self.min:
                self.min = self.min * 2 - self.head.value
                return self.min
            else:
                return self.head.value
        else:
            return -1

    # @return an integer
    def getMin(self):
        if self.head:
            return self.min
        else:
            return -1

0

从栈中获取最小元素。我们必须使用两个栈,即栈s1和栈s2。

  1. 最初,两个栈都为空,因此将元素添加到两个栈中

---------------------递归调用步骤2到4-----------------------

  1. 如果向栈s1添加新元素,则从栈s2中弹出元素

  2. 将新元素与s2进行比较。哪一个更小,就推入s2。

  3. 从包含最小元素的栈s2中弹出

代码如下:

package Stack;
import java.util.Stack;
public class  getMin 
{  

        Stack<Integer> s1= new Stack<Integer>();
        Stack<Integer> s2 = new Stack<Integer>();

        void push(int x)
        {
            if(s1.isEmpty() || s2.isEmpty())

            {
                 s1.push(x);
                 s2.push(x);
            }
            else
            {

               s1. push(x);
                int y = (Integer) s2.pop();
                s2.push(y);
                if(x < y)
                    s2.push(x);
                        }
        }
        public Integer pop()
        {
            int x;
            x=(Integer) s1.pop();
            s2.pop();
            return x;

        }
    public  int getmin()
        {
            int x1;
            x1= (Integer)s2.pop();
            s2.push(x1);
            return x1;
        }

    public static void main(String[] args) {
        getMin s = new getMin();
            s.push(10);
            s.push(20);
            s.push(30);
            System.out.println(s.getmin());
            s.push(1);
            System.out.println(s.getmin());
        }

}

0

这是我使用链表的Java解决方案。

class Stack{
    int min;
    Node top;
    static class Node{
        private int data;
        private Node next;
        private int min;

        Node(int data, int min){
           this.data = data;
           this.min = min;
           this.next = null; 
    }
}

  void push(int data){
        Node temp;
        if(top == null){
            temp = new Node(data,data);
            top = temp;
            top.min = data;
        }
        if(top.min > data){
            temp = new Node(data,data);
            temp.next = top;
            top = temp;
        } else {
            temp = new Node(data, top.min);
            temp.next = top;
            top = temp;
        }
  }

  void pop(){
    if(top != null){
        top = top.next;
    }
  }

  int min(){
    return top.min;
  }

}


这个失败了 常量额外空间 - greybeard

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接