高效率(内存)的双向指针链表

本文探讨了一种使用单个指针域实现双向链表的方法,通过异或运算保存前后节点地址,提高内存利用率。节点内存利用率从传统实现的34%提升到50%。介绍了遍历、插入和删除操作,并展示了内存和时间使用情况。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

问题:
如何只用一个指针节点实现双向链表?

英语能力有限,翻译起来不会很准确,因此在这里先做一下这篇文章的核心思想阐述:
使用一个指针,通过异或保存上一个节点的地址以及下一个节点的地址。
比如:

2^3 = 1; // 保存 1,就是本文中 ptrdiff 保存的值
2^3^2 = 3; // 异或 2 之后可以得到 3
假设现在有三个节点 prev, current, next
current->ptrdiff =(Node*) prev ^ next;
这样我们就通通过一个节点保存了另个地址,现在如何拿到前一个节点?显然
Node *tmp = (Node*)current->ptrdiff ^ next;

对于小型设备来说,每一小块内存都弥足珍贵。设备制造商需要考虑如何节省内存的使用。一种办法是,为日常使用的抽象数据结构寻找另一种实现。例如双向指针链表。

在本文中,我会用传统的方法和另一种替代方法来实现双指针链表,该双指针链表会有插入、遍历和删除操作。同时,我会打印出内存和时间使用情况,以供参考。这种替代实现的方法是基于指针距离(pointer distance),因此在本文中,我们称为指针距离实现(the pointer distance implementation)。每一个节点只需要一个指针域,就能实现向前或者向后遍历。在传统的实现中,我们需要一个指针指向前一个节点,以及另一个指针指向后一个节点。传统的节点的内存利用率只有34%,而指针距离实现的节点内存利用率是50%。如果我们使用多维的双向指针链表,比如动态网格,节点内存利用率还会更低。

在这里,我们不会讨论过多的去讨论传统的双向指针链表实现,你随便谷歌一下,会有很多相关文章。

节点定义:
typedef int T;
typedef struct listNode{
    T elm;
    struct listNode * ptrdiff;
};

ptrdiff 存着当前这个节点和下一个节点的差别(difference)以及当前这个节点和前一个节点的差别(difference)。指针区别是通过 异或 运算得到的。任何这种链表实例,都一个StartNode和一个EndNodeStartNode 指向链表的头部,EndNode 指向链表的尾部。我们定义StartNode 指向的前一个节点是 NULL 节点,EndNode 指向的下一个节点是NULL 节点。对于单节点链表,它的前一个节点和下一个节点都是NULL 。 简单来说就是当前节点的 ptrdiff 保存着当前节点的前一个节点和下一个节点的异或结果。

遍历

一个特定节点的插入和删除操作都依赖于遍历。我们很容易就能实现向前或者向后遍历。如果把 StartNode 作为一个参数,并且因为它的前一个节点是 NULL,那么,很显然,这是一个从左往右遍历的操作。同样的,如果把EndNode 作为一个参数,那么,就是从右往左遍历。在本文中,尚未支持从链表中间遍历,但是要支持这种操作并不难。

NextNode() 定义如下

typedef listNode * plistNode;
plistNode NextNode( plistNode pNode,
                    plistNode pPrevNode){
    return ((plistNode)
      ((int) pNode->ptrdiff ^ ( int)pPrevNode) );
}

给定一个元素元素(element),用 ptrdiff 来保存这个元素和下一个节点以及前一个节点的异或结果。因此,如何和前一个节点再做一次异或操作,那么指针就会指向下一个节点。

插入

给定一个新的节点和一个已经存在的节点元素,通过遍历找到这个元素所在的节点,然后将新的节点插到后面(看链表1)。将一个新的节点插入到双向链表,需要三个节点:当前节点,当前节点的上一个和下一个节点。如果所给的元素是最后一个节点的元素,就把新节点插到链表的最后面。如果在insertAfter() 轮询中,没有找到给定的元素,则放弃插入新的节点。

Listing 1. Function to Insert a New Node
void insertAfter(plistNode pNew, T theElm)
{
   plistNode pPrev, pCurrent, pNext;
   pPrev = NULL;
   pCurrent = pStart;

   while (pCurrent) {
      pNext = NextNode(pCurrent, pPrev);
      if (pCurrent->elm == theElm) {
         /* traversal is done */
         if (pNext) {
            /* fix the existing next node */
            pNext->ptrdiff =
                (plistNode) ((int) pNext->ptrdiff
                           ^ (int) pCurrent
                           ^ (int) pNew);

            /* fix the current node */
            pCurrent->ptrdiff =
              (plistNode) ((int) pNew ^ (int) pNext
                         ^ (int) pCurrent->ptrdiff);

            /* fix the new node */
            pNew->ptrdiff =
                (plistNode) ((int) pCurrent
                           ^ (int) pNext);
         break;
      }
      pPrev = pCurrent;
      pCurrent = pNext;
   }
}

首先通过 nextNode() 和遍历找到给定元素所在的节点,如果找到把新的节点插入到该节点的后面。

因为下一个节点有ptrdiff,可以通过和已经找到的节点进行异或操作解出地址。
接着,和新节点做异或操作,将新节点作为前一个节点。同样的,当前节点也是一样的逻辑。首先通过和下一个节点做异或操作解出ptrdiff;接着和新节点再做一次异或操作,这样就可以得到正确的ptrdiff。 最后,因为新节点处于已经找到的节点和下一个节点中间,通过和它们做异或操作得到了ptrdiff

删除

当前的删除操作支持删除整个链表。因为本文旨在阐述这种实现的动态内存分配和执行时间的消耗。因此这里不会实现各种双向链表的操作,但是实现它们不会太难。

因为我们的遍历依赖于指向两个节点的指针,因此没有办法在找到下一个节点的时候就可以直接删除当前节点。因此,我们依赖于下面这种规则,当找到下一个节点的时候,删除前一个节点。如果,当前节点是最后一个节点,当它被释放之后,删除结束。如果 nextNode() 返回 NULL,当前节点就是最后一个节点。

内存和时间的使用

这里不翻译,只贴上我自己运行的结果。
运行效率几乎相同,但是内存省了一大半。

地址实现方式
Before insert(prt.dist.) Thu Apr 12 19:39:56 2018

After insert(prt.dist.) Thu Apr 12 19:39:59 2018

Total Memory taken by ptr distance structure = 480000 bytes.
Before traverse(pStart) of (prt.dist.)  Thu Apr 12 19:39:59 2018

After traverse(pStart) of(prt.dist.)  Thu Apr 12 19:39:59 2018

Before traverse(pEnd) of (prt.dist.)  Thu Apr 12 19:39:59 2018

After traverse(pEnd) of (prt.dist.)  Thu Apr 12 19:39:59 2018

Before delList () of (prt.dist.)  Thu Apr 12 19:39:59 2018

 Final node being deleted in prt.dist. =29999
After delList () of (prt.dist.)  Thu Apr 12 19:39:59 2018

--------------
传统实现方式
Before conventional insert() Thu Apr 12 19:39:59 2018

After conventional insert()  Thu Apr 12 19:40:02 2018

Total Memory taken by conventional structure = 720000 bytes.
Before conventioal  dtraversefw(pdHead )  Thu Apr 12 19:40:02 2018

After conventioal  dtraversefw(pdHead )  Thu Apr 12 19:40:02 2018

Before conventioal dtraversebw(pdEnd)  Thu Apr 12 19:40:02 2018

After conventioal dtraversebw(pdEnd)  Thu Apr 12 19:40:02 2018

Before conventioal ddelList ()  Thu Apr 12 19:40:02 2018

After conventioal ddelList ()  Thu Apr 12 19:40:02 2018

源代码地址 GitHub

A Memory-Efficient Doubly Linked List

还可以阅读 XOR linked list

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值