仅使用单个指针字段存储双向链表

Posted

技术标签:

【中文标题】仅使用单个指针字段存储双向链表【英文标题】:Storing a doubly-linked list using just a single pointer field 【发布时间】:2014-02-27 00:02:40 【问题描述】:

所以最近,我读了一篇文章,向我展示了如何实现一个只有一个指针字段的双向链表,即像一个单链表。与将 XOR 上一个和下一个地址存储在单个字段中有关。我不明白这如何帮助我们前后遍历?谁可以给我解释一下这个?我已经阅读了here 上的文章。谁能给我解释一下?更详细一点?以及 XOR 与这些地址有何关系。

【问题讨论】:

下面有几个答案很好地解释了这一点,所以我将跳过它并简单地评论一件需要注意的事情。存在支持的平台(事实上,如果指针值不确定或不是专门来自“正确的”分配函数或&-operator)。它通常是作为一种节省宝贵字节的方式来完成的,通常是在 ASM 级别。今天很少(通常是嵌入式)现在需要这样的东西,并且几乎不会使代码难以阅读和维护。好技巧,好知识,现在忘了它=P。 啊,是的。我意识到这是一种旧的、未使用的方法。问题是,我在某处读过这个作为面试问题。很好奇它是如何工作的。当然,今天我们没有必要挤在这么小的空间里。然而,这个想法是创新的,纯粹是为了我的理解。 【参考方案1】:

XOR 有一个有趣的性质:如果给定 A 和 C = A^B,则可以计算 A^C = A^(A^B) = (A^A)^B = B。

在链表的情况下,如果给定前向指针或后向指针以及两者的异或,则可以通过单个异或找到另一个指针。当您迭代列表时,您已经拥有其中一个,因此您只需要 XOR 即可找到另一个;因此无需同时存储两者。

【讨论】:

嗯。我明白。但是您能否通过一个真实的例子(带有虚拟地址值)来详细说明这一点,以便我们更清楚地理解这个概念? 好的。假设您有一个具有后向指针 1 和前向指针 3 的节点。该节点存储 1^3 = 2。现在,如果您向前迭代列表,您将从地址为 1 的节点获取到该节点。 1^2 = 3 所以下一个节点是 3。同样,如果你向后迭代,你会从地址为 3 的节点到这个节点。现在 3^2 = 1,所以下一个节点的地址为 1。 有趣。但这似乎有点奇怪。让我把一些事情弄清楚。你说“假设你有一个带有后向指针和前向指针的节点”。这不是使它本身成为一个双向链表吗?并且有一个后向指针 1 和一个前向指针 3,它不会使当前节点成为地址为 2 的节点吗? 您正在实现一个双向链表,因此节点将拥有两个指针,或者至少以某种方式获取它们。有了这个技巧,指针不会直接存储,但仍有一种方法可以计算它们。当前节点可以有任何地址,这并不重要,重要的是它存储了两个指针的异或 2。【参考方案2】:

据我了解,它依赖于 XOR 运算符的属性,比如:

A XOR A = 0

使用双向链表时,您必须在结构中保存“下一个”和“上一个”地址。 作者说为了节省空间,你可以存储:

下一个异或上一个

然后通过以下方式浏览您的列表:

下一个 = 当前 XOR 上一个

next = (next XOR prev) XOR prev

next = 下一个 XOR (prev XOR prev)

但我真的不明白这一点,因为在这个例子中你仍然需要知道“prev”才能进行计算......

【讨论】:

这个想法是,如果你正在向前遍历列表,你知道 prev 并想要 next。如果您向后遍历,则您知道下一个并想要上一个。无论哪种方式,您都知道 XOR 输入中的一个,并且需要知道另一个。 好的,但如果您只提供了一个指向您被卡住的元素的指针。所以这是一个兴趣有限的优化(即它只在非常特定的情况下有用) 在我作为专业程序员的 30 多年里,我从未遇到过这样的情况:使用它所获得的内存足以证明灵活性和代码清晰度的成本是合理的。我认为这是一种理论上的好奇心,而不是一种有用的模式。【参考方案3】:

是这样的:

你在某个节点。你需要去下一个。但是你有一个变量需要存储两个指针的值。这怎么可能?

我们利用这样一个事实,当我们遍历列表时,我们知道之前访问过的节点的节点地址。但是怎么做?

所以,问题归结为:

我们需要将两个值存储在一个变量中。在任何时候,我们都知道其中任何一个。我们需要找到另一个。有可能吗?

答案是

v = a^b;
then v^b = a and v^a = b

现在,将这个概念应用到 DLL。

存储XOR of previous and next node's addresses in the current node

当你想遍历到下一个节点时,XOR 前一个节点的地址和当前节点中存储的值。您可以遍历到下一个。类似地,我们可以向后移动。

【讨论】:

【参考方案4】:

正如文章指出的那样,这种技术只有在您有一个指针位于列表的头部或尾部时才有用;如果列表中间只有一个指针,则无处可去。

关于技术:考虑以下链表:

|0|A|0x01|<->|0x01|B|0x02|<->|0x02|C|0|

列表包含 3 个节点,其值为 A、B、C 和 prev/next 指针,其中包含列表中 prev/next 元素的十六进制值(地址)。值 0 为空

我们可以只使用一个,而不是存储 2 个指针,如文章中所述:

|A|0x01|<->|B|0x03|<->|C|0x03| 

接下来我们将调用新字段 link = prev XOR。所以考虑到这一点:

    A.link = 0^0x01 = 0x01
    B.link = 0x01^0x02 = 0x03
    C.link = 0x03^0x0 = 0x03. 

假设您有一个指向列表头部的指针(您知道它的 prev 指针设置为 null),下面是您遍历列表的方式:

 p=head; 
 prev = 0;
 while(p.link!=prev)
 
   next = p.link^prev
   prev=p
   p=next 
 

您使用相同的逻辑在列表中倒退

【讨论】:

潘德雷解释得很好。我现在明白了这个概念。但是,我需要一点时间才能得到代码。我猜我得追查它。此刻似乎有些混乱。不过非常感谢!这个解释非常好。谢谢大家。每个人的信息都帮助我在这里和那里理解整个概念。【参考方案5】:

nitish 的解释很好。这种解释很容易理解这个概念。

我会让它更简单,然后它会更有用。假设您有 3 个节点,即 A、B 和 C。存储在 A 中的地址是 1(即二进制中的 01),B 中是 2(即二进制中的 10),C 中是 3(即二进制中的 11)。这种方式更清楚。

A    B    C
01   10   11  --->>this 01,10,11 are addresses. 

现在,XOR 属性表明,当位相同时,我们得到 0,否则为 1。 因此,当您当前的节点是 B(即地址 10)时,您想要继续前进。 这意味着您要做的是A XOR B01 XOR 10,即

     01
xor  10
    =11 i.e by doing xor of 01 and 10 it will make you reach at 11 address that is C. 

类似地,当您执行 10 xor 11 时,答案是 01,即 A。

【讨论】:

以上是关于仅使用单个指针字段存储双向链表的主要内容,如果未能解决你的问题,请参考以下文章

双向链表

双向链表

[数据结构]双向链表(C语言)

数据结构第四篇——线性表的链式存储之双向链表

数据结构 链表_双向链表的实现与分析

[数据结构(ywm版)] 异或指针双向链表