views:

492

answers:

10

My question is very simple, can one using C++, implment a link-list data structure without using pointers (next nodes)? To further qualify my question, I'm mean can one create a Linked-List data structure using only class instantiations.

A common node definition might be like so:

template<typename T>
struct node
{
   T t;
   node<T>* next;
   node<T>* prev;
};

I'm aware of std::list etc, I'm just curious to know if its possible or not - and if so how? code examples will be greatly appreciated.

More clarifications:

  1. Insertions should be O(1)
  2. Traversal should be no more than O(n)
  3. A real node and a null node should be differentiable
  4. The size of the linked-list should only be limited by the amount of memory available.
A: 

Can one using C++, implment a link-list data structure without using pointers (next nodes)?
No.

Billy ONeal
the link doesn't have to be an absolute memory address, it can be relative to a pointer, aka an array index. See the other answers.
Ramónster
@Ramónster: I have responded to the other answers with why I believe them incorrect.
Billy ONeal
+7  A: 

Yes, it's possible. Use array indexes instead of pointers.

DVK
Sorry, no code example as this smells like homework to me and I prefer not to answer h/w quesyions unless the poster is up-front about the fact.
DVK
you mean something like a std::vector<std::pair<T,std::size_t>> ?
sonicoder
@DVK: But then it's not a linked list.
Billy ONeal
@DVK: No just curiosity.
sonicoder
@DVK: True, then nor would your array be one either.
sonicoder
It's still a linked list, but it is also still using pointers. The term for this is "based pointer".
Ben Voigt
Billy if an entry in the list has a link to the "previous" and "next" entry in hte list then it is a linked list.The original "linked List" implementations were database structures on disk which had logical pointers for "previous' and "next"
James Anderson
+10  A: 

Sure, if you don't mind the linked list having a maximum size, you could statically allocate an array of list nodes, and then use integer indices into the array as your "previous" and "next" values for each node, rather than pointers. I've done in this in the past to save a bit of memory (since an integer can be either 2 or 4 bytes, whereas on a 64-bit system a pointer will be 8 bytes)

Jeremy Friesner
But then it's not a linked list, it's a vector.
Billy ONeal
It is a linked list.
Nyan
@Jeremy: As Billy mentioned, what you describe is not a linked-list wrt the true definition of the structure.
sonicoder
It's not a vector or a linked list. It requires contiguous memory allocation, but it allows some linked list benefits (inserting and removing in anywhere in O(1)).
Matthew Flaschen
@Matthew Flaschen: vector == contiguous memory allocation. I say it's a vector because the actual container in which the objects are stored is a vector. Just because those objects have references to each other doesn't mean it magically becomes a linked list. (+1 to comment though because of mentioning benefits of such a hybrid data structure)
Billy ONeal
It is a linked list with all nodes occupying a continuous memory space.From Wikipedia: "a linked list is a data structure that consists of a sequence of data records such that in each record there is a field that contains a reference (i.e., a link) to the next record in the sequence."What @Jeremy Friesner describes satisfies this definition.
BipedalShark
@BipedalShark: No, the elements are in a vector. The fact that they hold indexes to that vector makes absolutely no difference. Array indexes are not references or links.
Billy ONeal
Nodes are in a vector (either a `std::vector` or otherwise), but it doesn't have the performance characteristics of `std::vector<T>`.
Matthew Flaschen
@Billy ONeal: A C++ array is a pointer to the first element of the array. An array index is an offset from this pointer. How is this not a reference (in a general computer science sense, not the C++ data type) when used as a reference?
BipedalShark
In other breaking news... I've just been informed that this URL is *not* a linked list (it is, in fact, a *bitmap*!!): http://upload.wikimedia.org/wikipedia/commons/thumb/6/6d/Singly-linked-list.svg/408px-Singly-linked-list.svg.png
Hostile Fork
@BipedalShark: Because the memory is still being managed as a vector. The elements are in a vector, and the vector controls the lifetime, etc. of the objects. It still has many of the liabilities of a vector, namely that iterators and references are invalidated after insertions, and that an insertion can require reallocation of the entire underlying array. Linked lists don't reallocate and are always consistent even after insertions or deletions.
Billy ONeal
It seems a bit silly to quibble about definitions, but I'd call it a linked list with an extremely simple custom node-allocator algorithm. @Billy, I think you are incorrect about the liabilities; this can be implemented without requiring the invalidation of iterators, and the reallocation of the entire underlying array is avoided simply by having the insertion fail if the array is full.
Jeremy Friesner
@Jeremy: Then you're just trading what I've described earlier for the liability that the container is limited in size.
Billy ONeal
@billy: by your logic a std::map using a pooled allocator would also be a vector if the pool use contiguous memory.
caspin
@Billy ONeal: Give up already. Pointer is technically index, and index is technically pointer. If a list is stored in array AND uses indicies for parent/child, it is still a list. AND searching for a free element in such list can be easily optimized down to O(n) operation - at the cost of sizeof(int)*maximumSize of memory.
SigTerm
@Caspin: No, because a map can decide to allocate a new memory chunk which is completely unrelated to the old one. Therefore it has none of the liabilities that a vector does. @Sigterm: Yes, but the problem lies in what happens when your array runs out of space. You can't have constant time insertion with such a structure, only constant amortized time. And you can't maintain consistency after insertions either. Given that those are the only benefits of using a linked list data structure, even if it was a list there'd be no point. If you find yourself doing this, use a `std::deque`.
Billy ONeal
@Billy: Yes, I am making that tradeoff, but I said that up front in the original answer. I think the result is still a linked list in the ways that count (O(1) insertion/removal, O(N) lookup time, previous/next links, immutable/persistent node addresses)
Jeremy Friesner
@Jeremy: It's not O(1) insertion, it's O(1) *amortized* insertion. And you don't have immutable or persistent node addresses with the exception of the integers stored for first/previous node. Pointers to items in those nodes are invalidated upon insertion.
Billy ONeal
@Billy: As a fortran77 programmer who has used dynamic memory managers on implemented on `interger*4` arrays in common blocks, allow me to assure you that array offsets have *all* the characteristics of pointers. Indeed, native pointers can be though of as an array offset in the flat memory space offered by modern architectures. Certainly, mixing hardware-pointers with array-pointers when the array-store may move causes trouble, but so what? That is equivalent to the observation that you can't serialize a linked-list to disk without providing for reconstituting the links properly later.
dmckee
@dmckee: I never stated that array offsets have differing characteristics. I did state that in the data structure described, the objects in the array are managed as an array, not as a list. The objects are constructed when the array is constructed, and destroyed when the array is destroyed, not the pseudo-list. The data structure described does not provide the consistency or runtime guarantees that a list provides. Does it have some of the advantages of a list? Sure. But does it provide the same semantics as a list? No.
Billy ONeal
@Billy, no, insertion definitely is O(1) always -- remember, I'm disallowing array reallocations. Therefore, insertion only requires 'popping' the first item in the free-items-list, then 'pushing' it into the in-use-items list; both operations can be done just by updating a small/fixed number of index values -- no loops of any kind are required. Also, pointers to items are never invalidated upon insertion, because the items never move to different memory locations. (Their _prevIndex and _nextIndex values might change, but the items themselves don't move)
Jeremy Friesner
@Jeremy: "I'm disallowing array reallocations" <-- Okay, so you've traded consistency for extremely limited use of space. It's still does not have the semantics of a list.
Billy ONeal
It has all of the semantics of a list, except for being arbitrarily extensible. As for being "extremely limited", that's up to the implementer. E.g. there's nothing stopping you from setting the array size (and thus, the maximum list length) to 10 million if that's what you want to do.
Jeremy Friesner
*"Okay, so you've traded consistency for extremely limited use of space. It's still does not have the semantics of a list"* No. Though we have grown used to the idea that a "link list" has access to the whole address space provided by the chip, that has historically not been the case. If you insist on it, you are claiming that no one ever implemented a "linked list" on the classic Mac OSs. Pure nonsense. Limited allocators are less useful than a machine native lists residing in a fully virtualized heap (which is still not arbitrarily extensible), but they are still linked lists.
dmckee
A: 

You could make a linked-list using references, but that would probably be more complicated than necessary. you would have to implement an immutable linked-list which would be complicated without a built in garbage collector.

luke
No you can't. The reference has to point to something that's allocated *somewhere*, and you can't have a linked list manage that memory like it can if you are using pointers. You'd still have to hold the allocated objects in your linked list somehow, and that wouldn't be a linked list.
Billy ONeal
@Billy ONeal i know, thats why it would be complicated. instead of a null pointer at the end you would create a special singleton instance for an empty list. the references would be set in constructors. if you look at Ken Blooms code this is pretty much what he is doing.
luke
@Luke: You'd still have the problem of the fact that the thing the reference points at will die when you try to put it into the list. There's no way to ensure the references remain valid. If you look at Ken Bloom's code, note that none of the structure remains after a single statement.
Billy ONeal
@Billy ONeal my C++ is a little rusty, but couldn't you just construct the object on the heap and then create a reference from the pointer and then pass these 'new'd objects to the next constructor, this would extend the lifetime of the list objects. Now obviously it would be difficult to manage all these allocations.
luke
@Luke: I think use of the heap requires pointers. ;)
Billy ONeal
yeah but not in the datastructure itself
luke
+3  A: 

One could create a list of cons-cells using temporaries, const references, and inheritance. But you'd have to be very careful not to keep any references to it beyond its lifetime. And you probably couldn't get away with anything mutable.

This is based roughly on the Scala implementation of these lists (in particular the idea of using inheritance and a NilList subclass rather than using null pointers).

template<class T>
struct ConsList{
   virtual T const & car() const=0;
   virtual ConsList<T> const & cdr() const=0;
}

template<class T>
struct ConsCell:ConsList{
   ConsCell(T const & data_, ConsList<T> const & next_):
        data(data_),next(next_){}
   virtual T const & car() const{return data;}
   virtual ConstList<T> const & cdr() const{return next;}

   private:
     T data;
     ConsList<T> const & next;
}

template<class T>
struct NilList:ConsList{  
   // replace std::exception with some other kind of exception class
   virtual T const & car() const{throw std::exception;}
   virtual ConstList<T> const & cdr() const{throw std::exception;}
}

void foo(ConsList<int> const & l){
   if(l != NilList<int>()){
      //...
      foo(NilList.cdr());
   }
}

foo(ConsList<int>(1,ConsList(2,ConsList<int>(3,NilList<int>()))));
// the whole list is destructed here, so you better not have
// any references to it stored away when you reach this comment.
Ken Bloom
Actually, you can avoid the issue with temporaries by giving each node a variable with a name, and making references to those. But that could also be living dangerously.
Ken Bloom
@Ken: Your solution is no different than a static array, as the size of the structure has to be known at compile-time. Furthermore the size of the structure is entirely dependent on the compilers templating depth and not the amount of memory you have available.
sonicoder
@sonicoder, this structure is not using nested templates, so the compiler's templating depth shouldn't affect anything. Moreover, if you used a recursive function to build the list and continuation passing style to operate on it then you could build a dynamically-sized list.
Ken Bloom
@Ken: Could you please demonstrate how one would insert a node between say, nodes 2 and 3?
sonicoder
@sonicoder: that can't be done. This is a cons-list, like one would find in a functional language, and those aren't intended to support insertions in the middle. (That's the only reason I can get away with this silly example. I if you want mutability you'll have to use pointers.)
Ken Bloom
+2  A: 

While I'm not sure just what the context behind your question is, if you do a little out of the box thinking I'm sure you can.

DVK suggested arrays, which is quite true, but arrays are simply thin wrappers around pointer arithmetic.

How about something entirely different: use the filesystem to do the storage for you!

for example, the file /linked-list/1 contains the data:

Data 1!

5

and /linked-list/5 is the next node in the list...

If you're willing to hack enough, anything is possible :-p

Note that said implementation's complexity / speed is entirely dependent on your filesystem (i.e. it's not necessarily O(1) for everything)

Steven Schlansker
That's still not a linked list. Insertion or removal requires pushing up or down elements in your file. It's more like a vector data structure.
Billy ONeal
@Steven: Lets just assume the solution I'm looking for should not be based on anything other than standard OS memory allocation and code execution facilities.
sonicoder
@Billy: I don't see how this is anything like a vector. If you want to insert between 1 and 5, you take 1 and change its reference to 6534, and then make 6534 point at 5. No "pushing" or "popping" needed.
Steven Schlansker
@sonicoder: Your question was weird enough that I felt compelled to be a little silly with it. Clearly this does not fit your desire to only use standard memory allocation, etc...
Steven Schlansker
@Steven Schlansker: Because you had to put your new item into the array in order to update those references. You do get some of the benefits of a linked list (i.e. constant time insertion in the middle) with something like this, but the fact remains that the memory is being managed as a vector, not a list.
Billy ONeal
What is main memory but a very, very large array, then? :-p
Steven Schlansker
+4  A: 

From Wikipedia:

In computer science, a linked list is a data structure that consists of a sequence of data records such that in each record there is a field that contains a reference (i.e., a link) to the next record in the sequence.

Nothing in that definition specifies the manner in which the reference is stored or used. If you don't store a reference, it isn't a linked list -- it's something else.

If your goal is merely to avoid using a pointer (or object reference), then using a vector with an index is a common implementation. (One of the reasons for using the vector/index implementation is persistence: it's really hard to correctly persist pointers / object references outside of the active memory space.)

Craig Trader
A: 

I suppose using references is cheating and, technically, this causes UB, but here you go:

// Beware, un-compiled code ahead!
template< typename T >
struct node;

template< typename T >
struct links {
  node<T>& prev;
  node<T>& next;
  link(node<T>* prv, node<T>* nxt); // omitted
};

template< typename T >
struct node {
  T data;
  links<T> linked_nodes;
  node(const T& d, node* prv, node* nxt); // omitted
};

// technically, this causes UB...
template< typename T >
void my_list<T>::link_nodes(node<T>* prev, node<T>* next)
{
  node<T>* prev_prev = prev.linked_nodes.prev;
  node<T>* next_next = next.linked_nodes.next;
  prev.linked_nodes.~links<T>();
  new (prev.linked_nodes) links<T>(prev_prev, next);
  next.linked_nodes.~links<T>();
  new (next.linked_nodes) links<T>(next, next_next);
}

template< typename T >
void my_list<T>::insert(node<T>* at, const T& data)
{
  node<T>* prev = at;
  node<T>* next = at.linked_nodes.next;
  node<T>* new_node = new node<T>(data, prev, next);

  link_nodes(prev, new_node);
  link_nodes(new_node, next);
}
sbi
+3  A: 

Yes:

class node { 
  std::string filenameOfNextNode;
  std::string filenameOfPrevNode;
  std::string data;
  node nextNode() {
    node retVal;
    std::ifstream file(filenameOfNextNode.c_str());
    retVal.filenameOfNextNode = file.getline();
    retVal.filenameOfPrevNode = file.getline();
    retVal.data = file.getline();
    return retVal;
  }
};

Inspired by a comment about the origins of linked lists

MSalters
A: 

Wow, NO? Surely you guys are not serious?

All a linked list needs is a link. Nothing says it has to be a pointer. Consider the case of wanting to store a linked list in shared mem, where the base addr is dynamic? Answer is simply, store the link as a Offset from the start of the mem block, (or some other constant) and redefine the iterator to do the actual logic of adding the base addr. Obviously, insert etc would have to be changed as well.

But fairly trivial!

Allan

Allan