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

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

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

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

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
二.内核链表 内核链表是一种链表,Linux内核中的链表都是用这种形式实现的 1.特性 内核链表是一种双向循环链表,内核链表的节点节点结构中只有指针域 使用内核链表的时候,将内核链表作为一个成员放入到一个结构体中使用 我们在链表中找到内核链表结构的地址,通过这个地址就可以找到外部大结构体的地址,通过大结构体就可以访问其中的成员 优势: 内核链表突破了保数据的限制,可以用内核链表来保任何数据(使用一种链表示各种类型的数据,通用性很强) 内核链表中只有指针域,维护起来更加方便,效率更高 2.使用 内核链表在内核中已经被实现,我们只需要调用其接口直接使用即可 内核链表的实现代码在内核源代码的list.h文件中 3.源代码分析 (1)节点结构: struct list_head { struct list_head *next, *prev;//前置指针 后置指针 }; (2)初始化 #define INIT_LIST_HEAD(ptr) do { \ (ptr)->next = (ptr); (ptr)->prev = (ptr); \ } while (0) (3)插入 //从头部插入 static inline void list_add(struct list_head *new, struct list_head *head)//传入要插入的节点和要插入的链表 { __list_add(new, head, head->next); } //从尾部插入 static inline void list_add_tail(struct list_head *new, struct list_head *head) { __list_add(new, head->prev, head); } (4)通过节点找到外部结构体的地址 //返回外部结构体的地址,第一个参数是节点地址,第二个参数是外部结构体的类型名,第三个参数是节点在外部结构体中的成员名 #define list_entry(ptr, type, member) ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member))) (5)遍历内核链表 //遍历内核链表 #define list_for_each(pos, head) \ for (pos = (head)->next; pos != (head); \ pos = pos->next) //安全遍历内核链表 #define list_for_each_safe(pos, n, head) \ for (pos = (head)->next, n = pos->next; pos != (head); \ pos = n, n = pos->next) C语言的双链表,可以增加。插入,删除,修改,销毁等函数接口。
二.内核链表 内核链表是一种链表,Linux内核中的链表都是用这种形式实现的 1.特性 内核链表是一种双向循环链表,内核链表的节点节点结构中只有指针域 使用内核链表的时候,将内核链表作为一个成员放入到一个结构体中使用 我们在链表中找到内核链表结构的地址,通过这个地址就可以找到外部大结构体的地址,通过大结构体就可以访问其中的成员 优势: 内核链表突破了保数据的限制,可以用内核链表来保任何数据(使用一种链表示各种类型的数据,通用性很强) 内核链表中只有指针域,维护起来更加方便,效率更高 2.使用 内核链表在内核中已经被实现,我们只需要调用其接口直接使用即可 内核链表的实现代码在内核源代码的list.h文件中 3.源代码分析 (1)节点结构: struct list_head { struct list_head *next, *prev;//前置指针 后置指针 }; (2)初始化 #define INIT_LIST_HEAD(ptr) do { \ (ptr)->next = (ptr); (ptr)->prev = (ptr); \ } while (0) (3)插入 //从头部插入 static inline void list_add(struct list_head *new, struct list_head *head)//传入要插入的节点和要插入的链表 { __list_add(new, head, head->next); } //从尾部插入 static inline void list_add_tail(struct list_head *new, struct list_head *head) { __list_add(new, head->prev, head); } (4)通过节点找到外部结构体的地址 //返回外部结构体的地址,第一个参数是节点地址,第二个参数是外部结构体的类型名,第三个参数是节点在外部结构体中的成员名 #define list_entry(ptr, type, member) ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member))) (5)遍历内核链表 //遍历内核链表 #define list_for_each(pos, head) \ for (pos = (head)->next; pos != (head); \ pos = pos->next) //安全遍历内核链表 #define list_for_each_safe(pos, n, head) \ for (pos = (head)->next, n = pos->next; pos != (head); \ pos = n, n = pos->next) 二.内核链表 内核链表是一种链表,Linux内核中的链表都是用这种形式实现的 1.特性 内核链表是一种双向循环链表,内核链表的节点节点结构中只有指针域 使用内核链表的时候,将内核链表作为一个成员放入到一个结构体中使用 我们在链表中找到内核链表结构的地址,通过这个地址就可以找到外部大结构体的地址,通过大结构体就可以访问其中的成员 优势: 内核链表突破了保数据的限制,可以用内核链表来保任何数据(使用一种链表示各种类型的数据,通用性很强) 内核链表中只有指针域,维护起来更加方便,效率更高 2.使用 内核链表在内核中已经被实现,我们只需要调用其接口直接使用即可 内核链表的实现代码在内核源代码的list.h文件中 3.源代码分析 (1)节点结构: struct list_head { struct list_head *next, *prev;//前置指针 后置指针 }; (2)初始化 #define INIT_LIST_HEAD(ptr) do { \ (ptr)->next = (ptr); (ptr)->prev = (ptr); \ } while (0) (3)插入 //从头部插入 static inline void list_add(struct list_head *new, struct list_head *head)//传入要插入的节点和要插入的链表 { __list_add(new, head, head->next); } //从尾部插入 static inline void list_add_tail(struct list_head *new, struct list_head *head) { __list_add(new, head->prev, head); } (4)通过节点找到外部结构体的地址 //返回外部结构体的地址,第一个参数是节点地址,第二个参数是外部结构体的类型名,第三个参数是节点在外部结构体中的成员名 #define list_entry(ptr, type, member) ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member))) (5)遍历内核链表 //遍历内核链表 #define list_for_each(pos, head) \ for (pos = (head)->next; pos != (head); \ pos = pos->next) //安全遍历内核链表 #define list_for_each_safe(pos, n, head) \ for (pos = (head)->next, n = pos->next; pos != (head); \ pos = n, n = pos->next) 二.内核链表 内核链表是一种链表,Linux内核中的链表都是用这种形式实现的 1.特性 内核链表是一种双向循环链表,内核链表的节点节点结构中只有指针域 使用内核链表的时候,将内核链表作为一个成员放入到一个结构体中使用 我们在链表中找到内核链表结构的地址,通过这个地址就可以找到外部大结构体的地址,通过大结构体就可以访问其中的成员 优势: 内核链表突破了保数据的限制,可以用内核链表来保任何数据(使用一种链表示各种类型的数据,通用性很强) 内核链表中只有指针域,维护起来更加方便,效率更高 2.使用 内核链表在内核中已经被实现,我们只需要调用其接口直接使用即可 内核链表的实现代码在内核源代码的list.h文件中 3.源代码分析 (1)节点结构: struct list_head { struct list_head *next, *prev;//前置指针 后置指针 }; (2)初始化 #define INIT_LIST_HEAD(ptr) do { \ (ptr)->next = (ptr); (ptr)->prev = (ptr); \ } while (0) (3)插入 //从头部插入 static inline void list_add(struct list_head *new, struct list_head *head)//传入要插入的节点和要插入的链表 { __list_add(new, head, head->next); } //从尾部插入 static inline void list_add_tail(struct list_head *new, struct list_head *head) { __list_add(new, head->prev, head); } (4)通过节点找到外部结构体的地址 //返回外部结构体的地址,第一个参数是节点地址,第二个参数是外部结构体的类型名,第三个参数是节点在外部结构体中的成员名 #define list_entry(ptr, type, member) ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member))) (5)遍历内核链表 //遍历内核链表 #define list_for_each(pos, head) \ for (pos = (head)->next; pos != (head); \ pos = pos->next) //安全遍历内核链表 #define list_for_each_safe(pos, n, head) \ for (pos = (head)->next, n = pos->next; pos != (head); \ pos = n, n = pos->next) C语言下的单链表,可以增加,删除,查找,销毁节点。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值