【问题标题】:How exactly does a XOR Linked list work?XOR 链表究竟是如何工作的?
【发布时间】:2013-04-22 03:32:42
【问题描述】:

下面的 link 解释了它。
据说该实现通过存储前一个地址和下一个地址(比如 nxp)的 XOR 来工作,而不是分别存储(前一个和下一个地址)。但是,进一步据说该实现通过xor-ing前一个地址和nxp来工作,以获得下一个地址


但这实际上不是使用相同的空间作为具有上一个和下一个指针吗?

【问题讨论】:

  • 比较XOR链表的每个节点2个指针和每个节点1个指针的正常实现,你会看到区别。 (注意这是双向链表)

标签: algorithm


【解决方案1】:

在双向链表中,每个节点存储两个指针:prev 和 next。在 XOR 链表中,每个节点存储一个“指针”,即 prev 和 next 的 XOR(或者如果其中一个不存在,则只是另一个(与 0 的 XORing 相同))。您仍然可以双向遍历 XOR 链表的原因取决于 XOR 的属性和双链表中固有的信息冗余。


假设您的 XOR 链表中有三个节点。

A 是头部,并且有一个未混淆的指向 B 的指针(B XOR 0,仅下一个)

B 是中间元素,具有指向 A 和指向 C 的指针的异或。

C 是尾部,并且是指向 B 的未混淆指针(0 XOR B,仅 prev)

当我遍历这个列表时,我从 A 开始。当我前往 B 时,我记下了 A 在内存中的位置。当我想前往 C 时,我将 B 的指针与 A 进行异或,从而获得指向 C 的指针。然后我记下 B 在记忆中的位置并前往 C。

这是因为 XOR 具有在应用两次时自行撤消的特性:C XOR A XOR A == C。另一种思考方式是,双向链表不存储单链表不存储的额外信息(因为它只是将所有先前的指针作为下一个指针的副本存储在内存中的其他位置),因此通过利用这种冗余,我们可以拥有具有所需链接数量的双向链表属性。 然而,这只有在我们从头或尾开始 XOR 链表遍历时才有效——就好像我们只是跳到中间的一个随机节点,我们没有开始遍历所需的信息。


虽然 XOR 链表具有内存使用量较小的优点,但它也有缺点——它将混淆编译器、调试和静态分析工具,因为除了代码之外的任何指针都无法正确识别两个指针的 XOR .它还减慢了指针访问速度,必须首先执行 XOR 操作才能恢复真正的指针。它也不能在托管代码中使用——垃圾收集器不会识别 XOR 混淆指针。

【讨论】:

  • @nhahtdh 我正在回答这个问题'但这实际上不是使用与具有前一个和下一个指针相同的空间吗?通过解释异或链表如何在内存中的同一空间中同时容纳前一个和下一个指针。我错过了什么? :)
  • @nhahtdh 我解释说,通过利用双向链表中固有的信息冗余,您可以在两个方向上遍历 XOR 链表,但内存存储量与单链表相同。这怎么不回答问题?
  • 我认为您可以通过在答案中添加点 an XOR linked list fits both previous and next pointers in the same space in memory. 来改进您的答案。目前,我看不到您的答案与问题之间的关系。
  • 我认为你没有抓住重点; OP可能不会误解XOR(或者至少可能有同样的问题)。 OP 可能遗漏了 DLL 为每个节点存储 p/n 的事实,而 XLL 为每个节点存储 x(p/n) 并且 last_traversed 一次而不是每个节点。
【解决方案2】:

让我们考虑以下 XOR 列表

A->B->C->D

假设您在下面以这种格式创建了节点

密钥|链接|

A|0^addr(B)| ->  B|addr(A)^addr(C)|  ->  C|addr(B)^addr(D)| -> D|addr(C)^0|

案例#1:[前向遍历] 现在假设你在 B (current_node=>B) 想要访问 C ,所以你需要 C 的地址。你将如何获得?

Addressof(Next_node) = addressof(Prev_node) ^ Current_node(Link)

addr(A)^ ( addr(A)^ addr(C) )
=>(addr(A) ^ addr(A)) ^ addr(C) 
=> 0 ^ addr(C)
=>addr(C)

案例#2:[反向遍历]现在假设你在 C (current_node=> C) 想要访问 B ,所以你需要 B 的地址。你将如何获得?

Addressof(Prev_node) = addressof(Next_node) ^ Current_node(Link)

addr(D) ^ ((addr(B) ^ addr(D)) 
=> (addr(D)^ addr(D)) ^ addr(B)
=> 0^addr(B)
=> addr(B)

遍历: 要遍历整个列表,您需要 3 个指针 prevPtr 、 currPtr 、 nextPtr 来存储从 head 开始的相对当前、上一个和下一个节点的地址。 然后在每次迭代中,这些指针需要移动到前面的一个位置。

struct Node *currPtr = head;
struct Node *prevPtr = NULL;
struct Node *nextPtr;

printf ("Following are the nodes of Linked List: \n");

while (currPtr != NULL)
{
    // print current node
    printf ("%d ", currPtr->key);

    // Save the address of next node
    nextPtr = XOR (prevPtr, currPtr->link);

    //move prevPtr and currPtr one position for next iteration

    prevPtr = currPtr;
    currPtr = nextPtr;
}

【讨论】:

    【解决方案3】:

    但这实际上不是使用与先前和 下一个指针?

    不 - 它使用了大约一半的空间,因为“prev”和“next”的异或结果的大小等于两者中较大者的大小。

    【讨论】:

      【解决方案4】:

      XOR 有一个非常特殊的性质,即给定a XOR b = c,计算第三个变量只需要两个(任意两个)变量,有一些限制。请参阅 XOR swap algorithm 了解其工作原理。

      在这种情况下,前一个(或下一个)指针仍然必须携带,但只能通过遍历计算,而不是作为单独的成员。

      【讨论】:

      • 如果您引用了链接中的相关摘录,那就太好了。
      【解决方案5】:

      双向链表需要为 N 个节点存储 2*N 个指针,再加上至少一个额外的指针(头,或者可能是头和尾)。

      XOR 链表需要为 N 个节点存储 N 个指针,加上至少两个额外的指针(头和最后访问的节点,或者可能是头和尾和最后访问的节点)。遍历时,您存储一个节点(最后访问的节点),但是当您转到下一个节点时,您将其重写为现在的前一个节点的地址。

      【讨论】:

      • 其实你需要N+2个指针对应N个节点。要沿任一方向遍历,您需要一个指向第一个节点的非异或指针,另一个指向最后一个节点。
      • 不同意(至少,对于一般情况)。如果您希望能够同时(独立地)从两个方向穿越,当然,您需要它;但是,如果您只想从当前节点向后或向前移动,您只需要一个指向遍历的最后一个节点的指针。然后,您可以返回到该指针,或转发到该指针和列表中的指针的 XOR。并非所有(甚至大多数)DLL 都被创建为允许从两端进行遍历。
      • @JerryCoffin:双向链表也需要这些,因此您不妨只关注实际节点中的指针,在这种情况下,它是 2*NN
      • 已编辑以使上述内容更加清晰。我想即使这样也不准确,就好像你不需要记住头部或尾部指针一样,它仍然是 N+1,但我认为这比这里需要的更令人困惑。
      猜你喜欢
      • 2013-10-28
      • 2016-08-07
      • 2014-12-16
      • 2011-06-26
      • 2021-08-15
      • 2012-06-08
      • 2011-10-11
      • 2013-07-05
      • 1970-01-01
      相关资源
      最近更新 更多