Static Header as Sentinel
Massimo Anconay y
Walter Cazzolaz
DISI-University of Genova, Via Dodecaneso 32, 16100 Genova, Italy
e-mails:
[email protected]
z
DSI-University of Milano, Via Comelico 39-41, 20135 Milano, Italy
e-mails:
[email protected]
April 14, 1998
Abstract
1. Not all items of the structure have the same properties, eg. the tree root has not parent item, leaves have no children ;
Writing code to handle dynamic data structures might seem to be an easy task, but write an ef cient, readable and maintainable code is not such a simple task. In this short note we investigate some problems in developing code for handling dynamic data structures, and we propose techniques to overcome them. We take into account the interesting method proposed by Qiu in SIGPLAN Notices [3]. Many authors have addressed the problem of handling dynamic data structures by suggesting several clever tricks to simplify dynamic data structures management [4, 2] and to avoid the non-uniform behavior of empty data structures, and access to the rst and the last element. In particular, the use of data sentinels and dummy headers makes the implementation more orthogonal and easy to maintain. The cost is a slightly increased size in space which is negligible when compared with the bene ts produced.
2. structures are potentially unbounded, but defacto limited, ie. each structure always has a terminal item. Such problems are simple, but hide several traps. In order to avoid such traps algorithms become redundant and include special cases resulting dicult to read, to maintain and often inecient.
1 The Problem
The use of sentinels and dummy headers is recommended by most books introducing elementary data structures and algorithms [4, 2]. The purpose of a sentinel and of a dummy header is to simplify termination and starting tests in a repetition command (usually a while statement) used for visiting the data structure. The presence of an initial dummy header and of a terminal sentinel makes the management of all data elements uniform, because each of them has a predecessor and a successor. Recently Qiu [3] improved the use of data sentinels by proposing a method oering the advantage provided by a dummy header without paying the associated extra cost.
Several books and papers address the problem of managing dynamic data structures [4, 2]. When we write code to handle dynamic data structures like lists, queues, trees and so on, we must be handle two related problems:
In this paper, we go ahead in that direction by generalizing the approach of Qiu and proposing a method that uses both a dummy header and a terminal sentinel without paying extra costs or, at least, minimizing the extra space required for
Keywords:
Programming Methodologies.
1
them depending on the implementation language used. We demonstrate our method for the case of linked lists implemented in C language, but the method can be easily extended to other languages and other dynamic data structures.
g
g
f
if (l2 == NULL) =? inserts the key as the rst item ?= l3 next = l1; return l3; =? inserts the key as a generic item ?= l3 next = l2 next; l2 next = l3; return l;
!
! !
!
In the rest of the paper, we will use the following de nition of linked list. On the other hand, the above code points out the problem of handling special items (problem 1), like, typedef struct node node, *list; for example, the rst one. First and last items struct node f int key; in general require special attention and additional list next; code in order to be correctly handled. The addig; tional code increases code dimension, while reducing readability and maintainability. As running examples we use two simple operations: the rst is a key search in a list and the second is 2 Qiu's Solution a key insertion in an ordered list. In [3] Qiu presented static header, a new technique to retain the advantages of using a dummy header list general search( list l, int k ) f to overcome the drawbacks due to problem 2 =? search for the key ?= without extra space. for( ; l != NULL && =? list boundness test ?= The idea is to declare the dummy item local to the l!key != k; l = l!next ); function; in this way it is automatically discarted =? return the right item ?= at function termination.
g;
return ((l != NULL)?l:NULL);
The bene t achieved by Qiu method is shown by the following insertion function (see for comparison In this case it could be noted that the problem 2 the related example of the previous section). requires a test performed for all item of the list (l list qiu's ordered insert( list l, int k ) f != NULL). Such test is executed, in the worst case, node header; list l1, l2; for all items in the list. =? initialize & insert ?= Moreover, depending on whether the languaheader.next = l; gejcompiler performs a conditional evaluation of =? search where insert the new item ?= boolean expression (i.e. it short circuits boolean for(l2 = &header; expressions or not), the evaluation of condition l != NULL && =? list boundness test ?= (l!key != k) may produce a run-time error. l!key < k; list general_ordered_insert(list l, int k ) list l1, l2, l3; =? looks for where insert the key ?= for( l2 = NULL, l1 = l; l1 != NULL && =? list boundness test ?= l1 key < k; l2 = l1, l1 = l1 next ); if (l1 != NULL && l1 key == k) return l1; l3 = (list)malloc(sizeof(node)); l3 key = d;
!
!
! !
! !
l2 = l, l = l next); =? insert the new item ?= if (l == NULL || l key != k) l1 = (node *)malloc(sizeof(node)); l1 key = k; l1 next = l; l2 next = l1;
f
g;
g
! ! !
return header.next;
f
Root Item
Figure 1: Our Model of a List
f
list insert( list l, int k, char d ) node *new; =? initialize the new item ?= new = (node *)malloc(sizeof(node)); new key = k; =? link the new item to the list ?= new next = l next; l next = new; =? 'h' is a tag for the insertion on head ?= return ((d == 'h')?l:new); ;
!
! !
!
g
3 Our Extension
The basic idea beyond our proposal is to extend dynamic data with both a dummy header and a 3.1 The Search Operation terminal sentinel integrated into a single item. In this way we add just one item to the structure that Search operation takes advantage of our model can be managed like the static header of Qiu. To when a static sentinel is inserted between the last this aim we use a dierent list model: a circular and rst item of the list, like in the gure below. list. In a circular list the last item is linked to the rst one and no NULL pointer is used. Root Item Our idea is to merge the dummy header with the sentinel into a single item and to consider the last item to act as entry element of the list. Our list structure is sketched in gure 1. We use the following data type to encapsulate our list model: list: init : key type list first, next : list list key : list key type
! !
!
Sentinel
For our purposes only the implementation of the Such an item has the advantage of the static header of Qiu and that of a sentinel that eliminates any init operation is relevant. test on NULL pointers. list init( int k ) f Thus, the algorithm shows a uniform behavior on new = (node *)malloc(sizeof(node)); all items, while the sentinel guarantees a simple new!key = k; and safe termination test.
g
!
new next = new; return new;
The init operation is used to create a new list; it sets all references, according to our list model. An interesting side-eect of our model is the capability of adding a new item both at the head and at the tail of the list with the same operation. The only dierence is about which item is the new root item after insertion. In the latter case is the new one, while in the former case nothing is changed. Furthermore such an insert operation has a constant time complexity (1).
f
list search( list l, int k ) node sentinel; =? automatic variable ?= list l1; =? initialize & insert the sentinel ?= sentinel.key = k; sentinel.next = l next; l next = &sentinel; =? search for the key ?= for( l1 = sentinel.next; l1 key != k; l1 = l1 next ); =? to avoid the dangling reference ?= l next = sentinel.next; =? return the right item ?=
!
!
!
!
!
return ((&sentinel != l1)?l1:NULL);
g;
f ! !
else header next = l1; l2 next = header;
g
Note that the sentinel has the same structure of return l; all other items of the list. g; The sentinel is a name local to the search function body which is automatically discarted at function When the searched item already exists in the list we exit (see [1]). have to free the memory used by the header. This The algorithm does not take care about not nding is a negligible drawback because there exist very efthe key in the list. cient ways to handle dynamic memory allocation There is the risk of introducing a dangling reference and because this operation is not performed often. when the sentinel is discarted; thus, it is mandatory With a slight redundant implementation, it is posto re-link the last item of the list to the rst one sible to maintain an automatic variable for implementing the header with a method very close to before leaving the function. that of Qiu.
3.2 The
Operation
Similar bene ts are also obtained when operation 4 Conclusion insert is implemented for an ordered list. Our approach has the same advantages of that of Qiu. It supports management of list items that is The dierence with Qiu's proposal is due to the independent of item position, i.e. the rst and the absence of the test on the list termination. last items are handled in the same way, thus reducing the number of tests performed for each item. The algorithm starts by inserting a new item; such The number of operations performed by our implean item plays the role of list header (it is placed mentation of search and of ordered insert is, in the at the rst position) and that of a sentinel (it is worst case, equal to n tests performed in the main used as trailer item terminating the search when loop, while Qiu's algorithm requires 2n tests, where the key is not present in the list). n is the number of items in the list. Then the algorithm looks for a place where to Thus, our method has two advantages: it permits insert the new item. When such a place is found, to write a more readable and more ecient code. the new item is moved to the right position. Ordered Insert
It is obvious that the sentinel cannot be de ned as a local (automatic) variable.
f
list ordered insert( list l, int k ) list header, l1, l2; =? initialize & insert header-sentinel ?= header = (list)malloc(sizeof(node)); header key = k; header next = l next; l next = header; =? search where insert the new item ?= for(l2 = l, l1 = header next; l1 key < k; l2 = l1, l1 = l1 next); =? insert the new item ?= if (l1 == header) return header; l next = header next; if (l1 key == k) free(header);
!
! !
!
!
!
!
!
!
!
References
[1] A. V. Aho, R. Sethi, and J. D. Ullman. Compilers: Principles, Techniques, and Tools. Addison Wesley, Reading, Massachusset, 1986. [2] T. H. Cormen, C. E. Leiseison, and R. L. Rivest. Introduction to Algorithms. MIT Press, 1990. [3] Z. Qiu. Static head nodes in the operations on dynamic structures { an useful programming technique. ACM Sigplan Notices, 32(9):68 { 71, September 1997. [4] N. Wirth. Algorithms + Data Structures = Programs. Prentice-Hall, Englewood Cli, 1st edition, 1976.