一个双向链表的单指针实现
2007-11-27 15:11
706 查看
Save precious bytes with a new twist on a standard data type.
In the quest to make small devices cost effective, manufacturers often need to think about reducing the memory size. One option is to find alternative implementations of the abstract data types (ADTs) we are used to for our day-to-day implementations. One such ADT is a doubly linked list structure.In this article, I present a conventional implementation and an alternative implementation of the doubly linked list ADT, with insertion, traversal and deletion operations. I also provide the time and memory measurements of each to compare the pros and cons. The alternative implementation is based on pointer distance, so I call it the pointer distance implementation for this discussion. Each node would carry only one pointer field to traverse the list back and forth. In a conventional implementation, we need to keep a forward pointer to the next item on the list and a backward pointer to the previous item. The overhead is 66% for a conventional node and 50% for the pointer distance implementation. If we use multidimensional doubly linked lists, such as a dynamic grid, the savings would be even greater.
A detailed discussion of the conventional implementation of doubly linked lists is not offered here, because they are discussed in almost every data structure and algorithm book. The conventional and the distance pointer implementations are even used in the same fashion to have comparable memory and time usage statistics.
Node Definition
We define a node of pointer distance implementation like this:typedef int T;
typedef struct listNode{
T elm;
struct listNode * ptrdiff;
};
The ptrdiff pointer field holds the difference between the pointer to the next node and the pointer to the previous node. Pointer difference is captured by using exclusive OR. Any instance of such a list has a StartNode and an EndNode. StartNode points to the head of the list, and EndNode points to the tail of the list. By definition, the previous node of the StartNode is a NULL node; the next node of the EndNode also is a NULL node. For a singleton list, both the previous node and next node are NULL nodes, so the ptrdiff field holds the NULL pointer. In a two-node list, the previous node to the StartNode is NULL and the next node is the EndNode. The ptrdiff of the StartNode is the exclusive OR of EndNode and NULL node: EndNode. And, the ptrdiff of the EndNode is StartNode.
Traversal
The insertion and deletion of a specific node depends on traversal. We need only one simple routine to traverse back and forth. If we provide the StartNode as an argument and because the previous node is NULL, our direction of traversal implicitly is defined to be left to right. On the other hand, if we provide the EndNode as an argument, the implicitly defined direction of traversal is right to left. The present implementation does not support traversal from the middle of the list, but it should be an easy enhancement. The NextNode is defined as follows:typedef listNode * plistNode;
plistNode NextNode( plistNode pNode, plistNode pPrevNode)
{
return ((plistNode) ((int) pNode->ptrdiff ^ ( int)pPrevNode) );
}
Given an element, we keep the pointer difference of the element by exclusive ORing of the next node and previous node. Therefore, if we perform another exclusive OR with the previous node, we get the pointer to the next node.
Insertion
Given a new node and the element of an existing node, we would like to insert the new node after the first node in the direction of traversal that has the given element (Listing 1). Inserting a node in an existing doubly linked list requires pointer fixing of three nodes: the current node, the next node of the current node and the new node. When we provide the element of the last node as an argument, this insertion degenerates into insertion at the end of the list. We build the list this way to obtain our timing statistics. If the InsertAfter() routine does not find the given element, it would not insert the new element.Listing 1. Function to Insert a New Node
void insertAfter(plistNode pNew, T theElm)
{
plistNode pPrev, pCurrent, pNext;
pPrev = NULL;
pNext = 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;
}
}
First, we traverse the list up to the node containing the given element by using the NextNode() routine. If we find it, we then place the node after this found node. Because the next node has pointer difference, we dissolve it by exclusive ORing with the found node. Next, we do exclusive ORing with the new node, as the new node would be its previous node. Fixing the current node by following the same logic, we first dissolve the pointer difference by exclusive ORing with the next current node. We then do another exclusive ORing with the new node, which provides us with the correct pointer difference. Finally, since the new node would sit between the found current node and the next node, we get the pointer difference of it by exclusively ORing them.
Deletion
The current delete implementation erases the whole list. For this article, our objective is to show the dynamic memory usage and execution times for the implemented primitives. It should not be difficult to come up with a canonical set of primitive operations for all the known operations of a doubly linked list.Because our traversal depends on having pointers to two nodes, we cannot delete the current node as soon as we find the next node. Instead, we always delete the previous node once the next node is found. Moreover, if the current node is the end, when we free the current node, we are done. A node is considered to be an end node if the NextNode() function applied to it returns a null node.
Use of Memory and Time
A sample program to test the implementation discussed here is available as Listing 2 from the Linux Journal FTP site (ftp.ssc.com/pub/lj/listings/issue129/6828.tgz). On my Pentium II (349MHz, 32MB of RAM and 512KB of level 2 cache), when I run the pointer distance implementation, it takes 15 seconds to create 20,000 nodes. This is the time needed for the insertion of 20,000 nodes. Traversal and deletion of the whole list does not take even a second, hence the profiling at that granularity is not helpful. For system-level implementation, one might want to measure timings in terms of milliseconds.When we run the same pointer distance implementation on 10,000 nodes, insertion takes only three seconds. Traversal through the list and deletion of the entire list both take less than a second. For 20,000 nodes the memory being used for the whole list is 160,000 bytes, and for 10,000 nodes it is 80,000 bytes. On 30,000 nodes it takes 37 seconds to run the insertion. Again it takes less than a second to finish either the traversal or the deletion of the whole list. It is somewhat predictable that we would see this kind of timing, as the dynamic memory (heap) used here is being used more and more as the number of nodes increases. Hence, finding a memory slot from the dynamic memory takes longer and longer in a nonlinear, rather hyperlinear fashion.
For the conventional implementation, the insertion of 10,000 nodes takes the same three seconds. Traversal takes less than a second for both forward and backward traversal. Total memory taken for 10,000 nodes is 120,000 bytes. For 20,000 nodes, the insertion takes 13 seconds. The traversal and deletion individually takes less than a second. Total memory taken for 20,000 nodes is 240,000 bytes. On 30,000 nodes it takes 33 seconds to run the insertion and less than a second to run the traversal and the deletion. Total memory taken by 30,000 nodes is 360,000 bytes.
Conclusion
A memory-efficient implementation of a doubly linked list is possible to have without compromising much timing efficiency. A clever design would give us a canonical set of primitive operations for both implementations, but the time consumptions would not be significantly different for those comparable primitives.原文地址:http://www.linuxjournal.com/article/6828
这个实现的优势是只要你知道了当前结点的前驱指针或者后驱指针,你就能通过异或运算来获知另一个指针。在有些场合,节省下来的空间还是比较明显的,而且时间复杂度并没有提高很多。
相关文章推荐
- 一个指针实现双向链表
- 算法导论10.2-8-用一个整数地址替代前后指针实现双向链表
- 使用一个指针实现双向链表
- 一个优雅的双向链表的实现
- 用只含一个链域的节点实现循环链表的双向遍历
- 一个链表的每个节点,有一个指向next指针指向下一个节点,还有一个rand指针指向这个链表中的一个随机节点或NULL,现在要求复制一个单链表来实现这个链表,返回复制后的新链表。
- java实现输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head
- 使用LINUX C实现一个链表,要求:链表节点构成:姓名、分数、下一个节点指针...
- C++使用单指针Struct实现双向链表
- 编写算法实现建立一个带头结点的含n个元素的双向循环链表H,并在链表H中的第i个位置插入一个元素e
- 双向链表的单指针实现(算法导论习题)
- C语言一个双向链表的实现
- 单指针 实现 双向链表
- 二叉搜索树转换成一个排序的双向链表和实现一个线程安全且高效单例类——题集(二十一)
- 实现双向链表删除一个节点P,在节点P后插入一个节点
- 使用单指针域实现双向链表
- C语言一个双向链表的实现
- C语言一个双向链表的实现
- (Java实现)输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向
- 一个简单的双向循环链表的实现