27

My question is very simple, can one using C++, implement 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.
5
  • 3
    @Mike Atlas: No just curiosity. Commented Jun 9, 2010 at 2:39
  • 1
    I assume using smart pointers like auto_ptr or shared_ptr would be cheating. Commented Jun 9, 2010 at 2:47
  • 5
    @Ken: They would defeat the purpose of the question. Commented Jun 9, 2010 at 2:52
  • I assume that in rule 4, it's valid to be limited by the amount of available runtime stack space. Commented Jun 9, 2010 at 3:03
  • He's only come up with the rule list because he doesn't understand my answer. (He asked about both of those things on my solution before he added the rule list to the question.) Commented Jun 9, 2010 at 3:08

14 Answers 14

18

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)

Sign up to request clarification or add additional context in comments.

23 Comments

But then it's not a linked list, it's a vector.
@Jeremy: As Billy mentioned, what you describe is not a linked-list wrt the true definition of the structure.
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.
Nodes are in a vector (either a std::vector or otherwise), but it doesn't have the performance characteristics of std::vector<T>.
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.
|
11

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

5 Comments

you mean something like a std::vector<std::pair<T,std::size_t>> ?
@DVK: But then it's not a linked list.
@DVK: True, then nor would your array be one either.
It's still a linked list, but it is also still using pointers. The term for this is "based pointer".
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"
5

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.)

Comments

5

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

Comments

4

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.

7 Comments

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: 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, 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: Could you please demonstrate how one would insert a node between say, nodes 2 and 3?
@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.)
|
4

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)

6 Comments

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.
@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.
@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.
@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...
What is main memory but a very, very large array, then? :-p
|
3

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);
}

Comments

3

Yes you can, it is not necessary to use pointers for a link list. It is possible to link a list without using pointers. You can statically allocate an array for the nodes, and instead of using next and previous pointer, you can just use indexes. You can do that to save some memory, if your link list is not greater than 255 for example, you can use 'unsigned char' as index (referencing C), and save 6 bytes for next and previous indications.

You may need this kind of array in embedded programming, since memory limitations can be troublesome sometimes.

Also keep in mind that your link list nodes will not necessary be contiguous in the memory.

Let's say your link list will have 60000 nodes. Allocating a free node from the array using a linear search should be inefficient. Instead, you can just keep the next free node index everytime:

Just initialize your array as each next index shows the current array index + 1, and firstEmptyIndex = 0.

When allocating a free node from the array, grab the firstEmptyIndex node, update the firstEmptyIndex as next index of the current array index (do not forget to update the next index as Null or empty or whatever after this).

When deallocating, update the next index of the deallocating node as firstEmptyIndex, then do firstEmptyIndex = deallocating node index.

In this way you create yourself a shortcut for allocating free nodes from the array.

Comments

1

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.

5 Comments

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 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: 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 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.
yeah but not in the datastructure itself
1

Languages that do not support any type of reference can still create links by replacing pointers with array indices. The approach is to keep an array of records, where each record has integer fields indicating the index of the next (and possibly previous) node in the array. Not all nodes in the array need be used. If records are also not supported, parallel arrays can often be used instead.

As an example, consider the following linked list record that uses arrays instead of pointers:

record Entry {
integer next; // index of next entry in array
string data; // can be anything a struct also. }

Creata an array with a high number. And point listHead to the first indice element at the array

integer listHead;
Entry Records[10000];

Check wiki page: http://en.wikipedia.org/wiki/Linked_list for details, search for "Linked lists using arrays of nodes"

Comments

0

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

Comments

0

A possible approach would be to use an array of Nodes where each node stores an (array) index to the prev and next Node. Thus, your Node would look something like:

struct Node 
{
    T data;
    int prev;    // index of the previous Node of the list
    int next;    // index of the next Node of the list
}

Additionally, you will probably have to dynamically allocate the Node array, implement some bookkeeping to get and free space in the array: for example a bool array that stores the unoccupied indexes in the Node array, along with two functions that will update it every time a new Node / index is added or removed (it will be fragmented as nodes won't be always contiguous); find an index of a Node in the array: for example by subtracting the address of the Node from the first address of the array.

Here is how a possible interface of a doubly linked list using the above technique would look like:

template <typename T>                          // T type Node in your case
class DList
{
    T** head;                                  // array of pointers of type Node
    int first;                                 // index of first Node
    int last;                                  // index of last Node

    bool* available;                           // array of available indexes 
    int list_size;                             // size of list

    int get_index();                           // search for index with value true in bool available
    void return_index(int i);                  // mark index as free in bool available

    std::ptrdiff_t link_index(T*& l) const;    // get index of Node

    void init();                               // initialize data members
    void create();                             // allocate arrays: head and available

    void clear();                              // delete array elements
    void destroy();                            // delete head

public:
    DList();                                   // call create() and init()
    ~DList();                                  // call clear() and destroy()

    void push_back(T* l);
    void push_front(T* l);
    void insert(T*& ref, T* l);                // insert l before ref

    T* erase(T* l);                            // erase current (i.e. l) and return next
    T* advance(T* l, int n);                   // return n-th link before or after currnet

    std::size_t size() const;
    int capacity () const { return list_size; }
};

You could use that as a benchmark and implement something on your own.

template <typename T>
void DList<T>::push_back(T* l)
{
    if (l == nullptr)
    {
        throw std::invalid_argument("Dlist::push_back()::Null pointer as argument!\n");
    }

    int index = get_index();
    head[index] = l;

    if (last != -1)
    {
        head[last]->next = index;
        head[index]->prev = last;
    }
    else
    {
        first = index;
        head[index]->prev = -1;
    }

    last = index;
    head[index]->next = -1;
}

Comments

0

As an addition to the existing answers of using a previous and a next vector/array, we could build on top of a more dynamically resizing structure, i.e. losing the amortization on the resizing operation.

Why do I think this is suitable? Well, we have gained some advantages by using vectors/arrays, but we got the amortized resizing in return. If we can rid ourselves of the latter, we may have turned the trade entirely in our favor!

Specifically, I am referring to Resizable Arrays in Optimal Time and Space. It's a very interesting data structure, particularly as the basis for other data structures such as the one we are talking about.

Note that I have linked to the technical report, which, unlike the regular paper, also includes the (highly interesting) explanation of how doubly-resizable arrays were achieved.

Comments

-10

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

2 Comments

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: I have responded to the other answers with why I believe them incorrect.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.