这是“算法设计手册”一书中的练习(3-15)。
设计一种数据结构,允许人们在O(1)时间内搜索,插入和删除整数X(即,恒定时间,与存储的整数总数无关)。假设1≤X≤n并且有m + n个可用空间单位,其中m是任何时候表中可以存在的最大整数数。 (提示:使用两个数组A [1..n]和B [1..m]。)不允许初始化A或B,因为这将执行O(m)或O(n)操作。这意味着阵列中充满了随机垃圾,所以你必须非常小心。
我并不是真的想要答案,因为我甚至不明白这个练习是什么问题。
从第一句开始:
设计一个数据结构,允许用户在O(1)时间内搜索,插入和删除整数X
我可以轻松设计出类似的数据结构。例如:
因为1< = X< = n,所以我只有n个槽的位向量,并且让X是数组的索引,当插入例如5时,则a [5] = 1;当删除例如5时,则a [5] = 0;当搜索,例如5,那么我可以简单地返回[5],对吗?
我知道这个练习比我想象的要难,但这个问题的关键点是什么?
答案 0 :(得分:16)
您基本上实现了一个具有有界大小的多集,包括元素数量(#elements <= m
)和元素的有效范围(1 <= elementValue <= n
)。
myCollection.search(x)
- &gt;如果x在里面则返回True,否则返回False myCollection.insert(x)
- &gt;只添加一个x到集合myCollection.delete(x)
- &gt;从集合中删除一个x 考虑一下如果尝试存储两次,例如
,会发生什么myCollection.insert(5)
myCollection.insert(5)
这就是为什么你不能使用位向量。但它说&#34;单位&#34;对于空间,所以你的方法的详细说明就是保持每个元素的统计。例如,您可能会[_,_,_,_,1,_,...]
,然后[_,_,_,_,2,_,...]
。
为什么这不起作用呢?它似乎工作正常,例如,如果你插入5然后删除5 ...但是如果你在未初始化的数组上执行.search(5)
会发生什么?特别告诉你不能初始化它,所以你无法判断你在那段内存e.g. 24753
中找到的值是否实际意味着&#34;有5
&#34;的24753个实例或者如果它是垃圾。
注意:您必须允许自己O(1)
初始化空间,否则问题无法解决。 (否则.search()
将无法将内存中的随机垃圾与实际数据区分开来,因为您总是可以得到看似实际数据的随机垃圾。)例如,您可能会考虑使用布尔值&#34;我已经开始使用我的记忆&#34;您初始化为False,并在您开始写入m
记忆单词时设置为True。
如果你想要一个完整的解决方案,你可以将鼠标悬停在灰色块上以显示我想出的那个。它只有几行代码,但证明有点长:
SPOILER:完整解决方案
的设置强>:
使用N个字作为调度表:locationOfCounts[i]
是一个大小为N的数组,其值在location=[0,M]
范围内。这是存储i
计数的位置,但是如果我们可以证明它不是垃圾,我们只能信任这个值。 &GT ;!
(旁注:这相当于一个指针数组,但指针数组使您能够查找垃圾,因此您必须使用指针范围检查来编写该实现。)
要了解集合中有多少i
,您可以从上方查找值counts[loc]
。我们使用M个单词作为计数本身:counts
是一个大小为N的数组,每个元素有两个值。第一个值是它代表的数字,第二个值是该数字的计数(在[1,m]范围内)。例如,值(5,2)
意味着集合中存储了2个数字5
的实例。
(M字是所有计数的足够空间。证明:我们知道永远不会超过M个元素,因此最坏的情况是我们有M个值= 1.QED)
(我们还选择仅跟踪计数&gt; = 1,否则我们将没有足够的内存。)
使用名为numberOfCountsStored
的数字,该数字已初始化为0,但只要项目类型的数量发生更改,就会更新。例如,{}
的此数字为0,{5:[1 times]}
为1,{5:[2 times]}
为1,{5:[2 times],6:[4 times]}
为2。
1 2 3 4 5 6 7 8...
locationOfCounts[<N]: [☠, ☠, ☠, ☠, ☠, 0, 1, ☠, ...]
counts[<M]: [(5,⨯2), (6,⨯4), ☠, ☠, ☠, ☠, ☠, ☠, ☠, ☠..., ☠]
numberOfCountsStored: 2
下面我们清除每项操作的详细信息并证明其正确性:
的算法强>:
有两个主要的想法:1)我们永远不会允许自己读取内存而不验证首先不是垃圾,或者如果我们这样做,我们必须能够证明它是垃圾,2)我们需要能够在{ {1}}已初始化O(1)
内存的时间,仅counter
个空格。要解决此问题,我们使用的O(1)
空间为O(1)
。每次我们进行操作时,我们都会回到这个数字来证明一切都是正确的(例如见下面的★)。表示不变量是我们将始终在numberOfItemsStored
中从左到右存储计数,因此counts
将始终是有效数组的最大索引。
numberOfItemsStored
- 检查.search(e)
。我们假设现在该值已正确初始化并且可以信任。我们继续检查locationsOfCounts[e]
,但首先我们检查counts[loc]
是否已初始化:如果0&lt; =counts[loc]
&lt;loc
,则初始化它(如果没有,数据是荒谬的,所以我们返回False)。检查完毕后,我们会查找numberOfCountsStored
,它会给我们一个counts[loc]
对。如果number,count
!=number
,我们通过跟随随机垃圾(荒谬)来到这里,所以我们返回False(再次如上)......但如果确实e
== {{1 }},这证明计数是正确的(★证明:number
证明此特定e
有效,而numberOfCountsStored
是counts[loc]
有效的见证人,因此我们原来的查找不是垃圾。),所以我们将返回True。
counts[loc].number
- 执行locationOfCounts[number]
中的步骤。如果它已经存在,我们只需要将计数增加1.但是如果它不存在,我们必须使用.insert(e)
子阵列右侧的新条目。首先,我们增加.search(e)
以反映这个新计数有效的事实:counts
。然后我们讨论新条目:numberOfCountsStored
。最后,我们在调度表中添加一个引用,以便我们快速查找loc = numberOfCountsStored++
。
counts[loc] = (e,⨯1)
- 执行locationOfCounts[e] = loc
中的步骤。如果它不存在,则抛出错误。如果计数是> = 2,我们需要做的就是将计数递减1.否则计数为1,这里的技巧是确保整个.delete(e)
-.search(e)
不变(即一切仍然存储在numberOfCountsStored
的左侧部分)是为了执行交换。如果删除将删除最后一个元素,我们将丢失counts[...]
对,在我们的数组中留下一个洞:counts
。我们将此漏洞与最后一次countPair交换,递减counts
以使漏洞无效,并更新[countPair0, countPair1, _hole_, countPair2, countPair{numberOfItemsStored-1}, ☠, ☠, ☠..., ☠]
,使其现在指向计数记录的新位置。
答案 1 :(得分:3)
这是一个想法:
将数组B [1..m]视为一个堆栈,并使指针p指向堆栈的顶部(让p = 0表示没有元素插入到数据结构中)。现在,要插入整数X,请使用以下过程:
p++;
A[X] = p;
B[p] = X;
这里的搜索应该很容易看到(让X'成为你要搜索的整数,然后检查1&lt; = A [X']&lt; = p,那个B [A [X' ]] == X')。删除更棘手,但仍然是恒定的时间。我们的想法是搜索元素以确认它在那里,然后将某些东西移动到B中的位置(一个好的选择是B [p])。然后更新A以反映替换元素的指针值并弹出堆栈的顶部(例如,设置B [p] = -1并递减p)。
答案 2 :(得分:1)
一旦你知道答案就更容易理解这个问题:如果A[X]<total_integers_stored && B[A[X]]==X
,整数就在集合中。
问题是你是否可以弄清楚如何创建一个只需最少初始化即可使用的数据结构。
答案 3 :(得分:1)
我第一次看到Cameron在Jon Bentley Programming Pearls中的回答。
这个想法非常简单,但要理解为什么未初始化数组上的初始随机值无关紧要并不简单。这个link很好地解释了插入和搜索操作。删除留作练习,但其中一位评论者回答:
remove-member(i):
if not is-member(i): return
j = dense[n-1];
dense[sparse[i]] = j;
sparse[j] = sparse[i];
n = n - 1