在 this earlier question 中,OP要求提供类似于堆栈的数据结构,在每个O(1)时间内支持以下操作:
几分钟前,我发现 this related question 要求澄清一个类似的数据结构,而不是允许查询max和min,允许中间元素要查询的堆栈。这两个数据结构似乎是支持以下操作的更通用数据结构的特例:
可以通过存储堆栈和保存前k个元素的平衡二叉搜索树来支持所有这些操作,这将使所有这些操作在O(log k)时间内运行。我的问题是:是否有可能比这更快地实现上述数据结构?也就是说,我们可以为所有三个操作获得O(1)吗?或者也许O(1)用于推送和弹出,O(log k)用于订单统计查找?
答案 0 :(得分:6)
由于该结构可用于使用O(k)push和find-kth操作对k个元素进行排序,因此每个基于比较的实现至少有一个成本Omega(log k),即使是在摊销意义上,随机化。
推送可以是O(log k),pop / find-kth可以是O(1)(使用持久数据结构;推送应该预先计算订单统计数据)。基于比较算法的下限工作,我的直觉是O(1)push / pop和O(log k)find-kth是可行的,但需要摊销。
答案 1 :(得分:2)
这实际上是否比log k实现更快,取决于最常使用的操作,我建议使用O(1)Find-kth和Pop和O(n)Push实现,其中n是堆栈大小。而且我也希望与SO分享这个,因为它只是一个热闹的数据结构,但可能是合理的。
最好由双重双向链接堆栈描述,或者更容易被描述为链接堆栈和双向链接排序列表的混合。基本上每个节点维护对其他节点的4个引用,堆栈顺序中的下一个和前一个,以及元素大小的排序顺序中的下一个和上一个。这两个链表可以使用相同的节点实现,但它们完全分开工作,即排序的链表不必知道堆栈顺序,反之亦然。
与普通的链接堆栈一样,集合本身需要维护对顶层节点(以及底层?)的引用。为了适应Find-kth方法的O(1)性质,该集合还将保留对第k个最大元素的引用。
pop方法的工作原理如下: 弹出的节点将从已排序的双向链表中删除,就像从正常的已排序链表中删除一样。它需要O(1),因为集合具有对顶部的引用。根据弹出元素是大于还是小于第k个元素,对第k个最大元素的引用设置为前一个或下一个。因此该方法仍具有O(1)复杂度。
push方法就像对已排序链表的正常添加一样,这是一个O(n)操作。它从最小的元素开始,并在遇到更大的元素时插入新节点。为了保持对第k个最大元素的正确引用,再次选择当前第k个最大元素的前一个或下一个元素,具体取决于推送节点是大于还是小于第k个最大元素。
接下来,必须在两种方法中设置对堆栈“顶部”的引用。还有k>的问题。 n,您没有指定数据结构应该做什么。我希望它清楚如何运作,否则我可以添加一个例子。
但是好吧,不完全是你所希望的复杂性,但我发现这是一个有趣的“解决方案”。
在这个问题上发出了一笔赏金,这表明我原来的答案不够好:P也许OP希望看到实施?
我在C#中实现了中位数问题和固定k问题。中位数跟踪器的实现只是第k个元素跟踪器的包装,其中k可以变异。
回顾复杂性:
我已在原帖中详细描述了算法。然后实现相当简单(但不是那么简单,因为有很多不平等的迹象和if语句需要考虑)。我只评论是什么已经完成,但没有详细说明如何,否则会变得太大。 SO帖子的代码已经很长了。
我确实想提供所有非平凡公众成员的合同:
K
是已排序链接列表中元素的索引,用于保留引用。它是否可变,当设置时,结构会立即得到纠正。 KthValue
是该索引处的值,除非该结构还没有k个元素,在这种情况下它返回一个默认值。 HasKthValue
的存在是为了轻松地将这些默认值与恰好是其类型的默认值的元素区分开来。Constructors
:null枚举被解释为空的可枚举,并且null比较器被解释为默认值。该比较器定义了确定第k个值时使用的顺序。所以这就是代码:
public sealed class KthTrackingStack<T>
{
private readonly Stack<Node> stack;
private readonly IComparer<T> comparer;
private int k;
private Node smallestNode;
private Node kthNode;
public int K
{
get { return this.k; }
set
{
if (value < 0) throw new ArgumentOutOfRangeException();
for (; k < value; k++)
{
if (kthNode.NextInOrder == null)
return;
kthNode = kthNode.NextInOrder;
}
for (; k >= value; k--)
{
if (kthNode.PreviousInOrder == null)
return;
kthNode = kthNode.PreviousInOrder;
}
}
}
public T KthValue
{
get { return HasKthValue ? kthNode.Value : default(T); }
}
public bool HasKthValue
{
get { return k < Count; }
}
public int Count
{
get { return this.stack.Count; }
}
public KthTrackingStack(int k, IEnumerable<T> initialElements = null, IComparer<T> comparer = null)
{
if (k < 0) throw new ArgumentOutOfRangeException("k");
this.k = k;
this.comparer = comparer ?? Comparer<T>.Default;
this.stack = new Stack<Node>();
if (initialElements != null)
foreach (T initialElement in initialElements)
this.Push(initialElement);
}
public void Push(T value)
{
//just a like a normal sorted linked list should the node before the inserted node be found.
Node nodeBeforeNewNode;
if (smallestNode == null || comparer.Compare(value, smallestNode.Value) < 0)
nodeBeforeNewNode = null;
else
{
nodeBeforeNewNode = smallestNode;//untested optimization: nodeBeforeNewNode = comparer.Compare(value, kthNode.Value) < 0 ? smallestNode : kthNode;
while (nodeBeforeNewNode.NextInOrder != null && comparerCompare(value, nodeBeforeNewNode.NextInOrder.Value) > 0)
nodeBeforeNewNode = nodeBeforeNewNode.NextInOrder;
}
//the following code includes the new node in the ordered linked list
Node newNode = new Node
{
Value = value,
PreviousInOrder = nodeBeforeNewNode,
NextInOrder = nodeBeforeNewNode == null ? smallestNode : nodeBeforeNewNode.NextInOrder
};
if (newNode.NextInOrder != null)
newNode.NextInOrder.PreviousInOrder = newNode;
if (newNode.PreviousInOrder != null)
newNode.PreviousInOrder.NextInOrder = newNode;
else
smallestNode = newNode;
//the following code deals with changes to the kth node due the adding the new node
if (kthNode != null && comparer.Compare(value, kthNode.Value) < 0)
{
if (HasKthValue)
kthNode = kthNode.PreviousInOrder;
}
else if (!HasKthValue)
{
kthNode = newNode;
}
stack.Push(newNode);
}
public T Pop()
{
Node result = stack.Pop();
//the following code deals with changes to the kth node
if (HasKthValue)
{
if (comparer.Compare(result.Value, kthNode.Value) <= 0)
kthNode = kthNode.NextInOrder;
}
else if(kthNode.PreviousInOrder != null || Count == 0)
{
kthNode = kthNode.PreviousInOrder;
}
//the following code maintains the order in the linked list
if (result.NextInOrder != null)
result.NextInOrder.PreviousInOrder = result.PreviousInOrder;
if (result.PreviousInOrder != null)
result.PreviousInOrder.NextInOrder = result.NextInOrder;
else
smallestNode = result.NextInOrder;
return result.Value;
}
public T Peek()
{
return this.stack.Peek().Value;
}
private sealed class Node
{
public T Value { get; set; }
public Node NextInOrder { get; internal set; }
public Node PreviousInOrder { get; internal set; }
}
}
public class MedianTrackingStack<T>
{
private readonly KthTrackingStack<T> stack;
public void Push(T value)
{
stack.Push(value);
stack.K = stack.Count / 2;
}
public T Pop()
{
T result = stack.Pop();
stack.K = stack.Count / 2;
return result;
}
public T Median
{
get { return stack.KthValue; }
}
public MedianTrackingStack(IEnumerable<T> initialElements = null, IComparer<T> comparer = null)
{
stack = new KthTrackingStack<T>(initialElements == null ? 0 : initialElements.Count()/2, initialElements, comparer);
}
}
当然,您总是可以自由地询问有关此代码的任何问题,因为我发现某些内容可能不会从描述和零星评论中显而易见
答案 2 :(得分:2)
我认为tophat所说的是,实现一个纯粹的功能数据结构,它只支持O(log k)插入和O(1)find-kth(由insert缓存),然后构建这些结构的堆栈。将插入推送到顶部版本并推送更新,pop弹出顶部版本,find-kth在顶部版本上运行。这是O(log k)/ O(1)/(1)但是超线性空间。
编辑:我正在研究O(1)push / O(1)pop / O(log k)find-kth,我认为无法完成。所提到的排序算法可以适用于在时间O(k +(√k)log k)中获得长度为k的阵列的均匀间隔顺序统计。问题是,算法必须知道每个订单统计数据与所有其他元素的比较(否则可能是错误的),这意味着它已将所有内容分配到√k+ 1个桶中的一个,这需要Ω(k log(√k+) 1))=Ω(k log k)在信息理论基础上的比较。糟糕。
用任何eps替换k eps 的√k> 0,O(1)push / O(1)pop,我不认为find-kth可以是O(k 1 - eps ),即使是随机化和摊销。
答案 3 :(得分:1)
您可以使用skip list。 (我首先想到的是链接列表,但是插入是O(n)并且使用跳过列表来修正我。我认为这种数据结构在你的情况下可能非常有趣)
With this data structure, inserting/deleting would take O(ln(k))
and finding the maximum O(1)
我会用:
(我意识到它是Kth最大的元素。但它几乎是同样的问题)
推(O(ln(k)):
如果元素小于第k个元素,则删除第k个元素(O(ln(k))将其放入LIFO堆(O(1))中,然后将该元素插入跳过列表O(ln(k) )
否则它不在跳过列表中,只是把它放在堆上(O(1))
当您按下时,将新的跳过列表添加到历史记录中,因为这类似于写入时的副本,它不会超过O(ln(k))
弹出时(O(1):
你只是从两个堆栈中弹出
获取第k个元素O(1):
始终采用列表中的最大元素(O(1))
所有ln(k)均为摊销成本。
示例:强>
我将采用与您相同的示例(在Stack上使用find-min / find-max比O(n)更有效):
假设我们有一个堆栈并按顺序添加值2,7,1,8,3和9。和k = 3
我将用这种方式表示:
[number in the stack] [ skip list linked with that number]
首先我推2,7和1(它不会让你感觉到在少于k个元素的列表中寻找第k个元素)
1 [7,2,1]
7 [7,2,null]
2 [2,null,null]
如果我想要第k个元素,我只需要在链表中取最大值:7
现在我推8,3,9
我在堆栈顶部的:
8 [7,2,1] since 8 > kth element therefore skip list doesn't change
然后:
3 [3,2,1] since 3 < kth element, the kth element has changed. I first delete 7 who was the previous kth element (O(ln(k))) then insert 3 O(ln(k)) => total O(ln(k))
然后:
9 [3,2,1] since 9 > kth element
这是我得到的堆栈:
9 [3,2,1]
3 [3,2,1]
8 [7,2,1]
1 [7,2,1]
7 [7,2,null]
2 [2,null,null]
找到第k个元素:
I get 3 in O(1)
现在我可以弹出9和3(取O(1)):
8 [7,2,1]
1 [7,2,1]
7 [7,2,null]
2 [2,null,null]
找到第k个元素:
I get 7 in O(1)
并按0(取O(ln(k) - 插入)
0 [2,1,0]
8 [7,2,1]
1 [7,2,1]
7 [7,2,null]
2 [2,null,null]
答案 4 :(得分:1)
我可以解决的唯一实际工作实现是Push / Pop O(log k)和Kth O(1)。
PUSH:
POP:
KTH:
答案 5 :(得分:1)
如果您将堆栈与一对Fibonacci Heaps配对,该怎么办?这可以给出摊销的O(1)Push和FindKth,以及O(lgN)删除。
堆栈存储[value,heapPointer]对。堆堆栈指针。
创建一个MaxHeap,一个MinHeap。
On Push:
如果MaxHeap少于K个项目,请将堆栈顶部插入MaxHeap;
否则,如果新值小于MaxHeap的顶部,则首先在MinHeap中插入DeleteMax的结果,然后将新项插入MaxHeap;
否则将其插入MinHeap。 O(1) (或 O(lgK),如果需要DeleteMax)
在FindKth上,返回MaxHeap的顶部。的 O(1) 强>
在Pop上,也可以从弹出项目的堆中执行删除(节点)
如果它在MinHeap中,你就完成了。的 O(LGN) 强>
如果它在MaxHeap中,也从MinHeap执行DeleteMin并将结果插入MaxHeap。的 O(LGK)+ O(LGN)+ O(1) 强>
更新:
我意识到我把它写成K'最小,而不是K'最大。
当新值小于当前K'最小值时,我也忘了一步。那一步
将最坏的情况插入O(lg K)。对于均匀分布的输入和小K,这可能仍然可以,因为它只会在K / N插入时遇到这种情况。
* 将新想法转移到不同的答案 - 它太大了。
答案 6 :(得分:1)
@tophat是正确的 - 因为这个结构可以用来实现排序,它的复杂性不会低于等效的排序算法。那么你如何在低于O(lg N)的情况下进行排序?使用Radix排序。
这是一个使用Binary Trie的实现。将项目插入二进制Trie与执行基数排序的操作基本相同。插入和删除s O(m)的成本,其中m是常数:密钥中的位数。找到下一个最大或最小的密钥也是O(m),通过在有序深度优先遍历中采取下一步来完成。
所以一般的想法是使用压入堆栈的值作为trie中的键。要存储的数据是堆栈中该项目的出现次数。对于每个推送的项目:如果它存在于trie中,则递增其计数,否则将其存储为计数1.当您弹出一个项目时,找到它,递减计数,并在计数为0时将其删除。操作是O(m)。
要获取O(1)FindKth,请跟踪2个值:第K个项目的值,以及该值在第一个K项目中的实例数。 (例如,对于K = 4和[1,2,3,2,0,2]的堆栈,Kth值为2,“iCount”为2。)然后当你推送值&lt; KthValue,您只需减少实例计数,如果为0,则在trie上执行FindPrev以获得下一个较小的值。
当您弹出大于KthValue的值时,如果存在更多该vaue的实例,则增加实例计数,否则执行FindNext以获取下一个更大的值。
(如果少于K项则规则不同。在这种情况下,您只需跟踪最大插入值。当有K项时,最大值将是Kth。)
这是一个C实现。它依赖于BinaryTrie (使用PineWiki上的示例构建作为基础)与此接口:
BTrie* BTrieInsert(BTrie* t, Item key, int data);
BTrie* BTrieFind(BTrie* t, Item key);
BTrie* BTrieDelete(BTrie* t, Item key);
BTrie* BTrieNextKey(BTrie* t, Item key);
BTrie* BTriePrevKey(BTrie* t, Item key);
这是推送功能。
void KSStackPush(KStack* ks, Item val)
{
BTrie* node;
//resize if needed
if (ks->ct == ks->sz) ks->stack = realloc(ks->stack,sizeof(Item)*(ks->sz*=2));
//push val
ks->stack[ks->ct++]=val;
//record count of value instances in trie
node = BTrieFind(ks->trie, val);
if (node) node->data++;
else ks->trie = BTrieInsert(ks->trie, val, 1);
//adjust kth if needed
ksCheckDecreaseKth(ks,val);
}
这是跟踪KthValue
的助手//check if inserted val is in set of K
void ksCheckDecreaseKth(KStack* ks, Item val)
{
//if less than K items, track the max.
if (ks->ct <= ks->K) {
if (ks->ct==1) { ks->kthValue = val; ks->iCount = 1;} //1st item
else if (val == ks->kthValue) { ks->iCount++; }
else if (val > ks->kthValue) { ks->kthValue = val; ks->iCount = 1;}
}
//else if value is one of the K, decrement instance count
else if (val < ks->kthValue && (--ks->iCount<=0)) {
//if that was only instance in set,
//find the previous value, include all its instances
BTrie* node = BTriePrev(ks->trie, ks->kthValue);
ks->kthValue = node->key;
ks->iCount = node->data;
}
}
这是Pop功能
Item KSStackPop(KStack* ks)
{
//pop val
Item val = ks->stack[--ks->ct];
//find in trie
BTrie* node = BTrieFind(ks->trie, val);
//decrement count, remove if no more instances
if (--node->data == 0)
ks->trie = BTrieDelete(ks->trie, val);
//adjust kth if needed
ksCheckIncreaseKth(ks,val);
return val;
}
帮助增加KthValue
//check if removing val causes Kth to increase
void ksCheckIncreaseKth(KStack* ks, Item val)
{
//if less than K items, track max
if (ks->ct < ks->K)
{ //if removing the max,
if (val==ks->kthValue) {
//find the previous node, and set the instance count.
BTrie* node = BTriePrev(ks->trie, ks->kthValue);
ks->kthValue = node->key;
ks->iCount = node->data;
}
}
//if removed val was among the set of K,add a new item
else if (val <= ks->kthValue)
{
BTrie* node = BTrieFind(ks->trie, ks->kthValue);
//if more instances of kthValue exist, add 1 to set.
if (node && ks->iCount < node->data) ks->iCount++;
//else include 1 instance of next value
else {
BTrie* node = BTrieNext(ks->trie, ks->kthValue);
ks->kthValue = node->key;
ks->iCount = 1;
}
}
}
因此,对于所有3个操作,算法都是O(1)。它还可以支持中值运算:从KthValue =第一个值开始,每当堆栈大小改变2时,执行IncreaseKth或DecreasesKth操作。缺点是常数很大。当m
答案 7 :(得分:0)
使用Trie存储您的值。尝试已经具有O(1)插入复杂度。你只需要担心弹出和搜索两件事,但是如果你稍微调整你的程序,那就很容易了。
插入(推送)时,每个路径都有一个计数器,用于存储插入其中的元素数量。这将允许每个节点跟踪使用该路径插入了多少元素,即该数字表示存储在该路径下的元素的数量。这样,当你试图寻找第k个元素时,它将在每个路径上进行简单的比较。
对于弹出,您可以拥有一个静态对象,该对象具有指向最后一个存储对象的链接。可以从根对象访问该对象,因此可以访问O(1)。当然,您需要添加函数来检索插入的最后一个对象,这意味着新推送的节点必须具有指向先前推送元素的指针(在推送过程中实现;非常简单,也是O(1))。您还需要递减计数器,这意味着每个节点必须有一个指向父节点的指针(也很简单)。
用于查找第k个元素(这是针对最小的第k个元素,但是找到最大的第k个元素非常相似):当您输入每个节点时,您传入k和分支的最小索引(对于根,它将为0)。然后你做一个简单的比较每个路径:if(k在最小索引和最小索引+ pathCounter之间),你输入传递k的路径和新的最小索引为(最小索引+所有先前pathCounters的总和,不包括一个你拿了)。我认为这是O(1),因为在一定范围内增加数据数量并不会增加找到k的难度。
我希望这会有所帮助,如果有什么不太清楚,请告诉我。