Self contained double ended linked list.

Good evening everyone, I've recently been issued a challenge. I've got a brief to write a self contained doubly linked list that only consists of element objects. I was just wondering, how exactly you would begin such a task.

I have to begin by creating the node like so:

ListNode *pointer = new ListNode("Example");

Only using a char array, no templates or anything like that. I just need an idea of how to design the base class. I normally would make two classes; a LinkedList class then a Node class which contains a previous and next pointer and the single char. I'm not sure exactly how this differs.
Last edited on
What you you mean by "self contained"?
Only has one node class?
Should the line you posted create a list 0 <- E <-> x <-> a <-> m <-> p <-> l <-> e -> 0 ?
I'm not sure what is meant by "self contained" myself.
Last edited on
I'd use something like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class ListNode
{
public:
  typedef int PayloadType; // to easier migrate into a template later on
  ListNode (const PayloadType payload)
  : _previousOrZero(0)
  , _nextOrZero(0)
  , _payload(payload)
  {} ; // create a new doubly linked list

  ListNode* appendAndReturnPointerToNewNode(const Payloadype& payload);
  ListNode* prependAndReturnPointerToNewNode(const Payloadype& payload);
  bool hasNextNode() const;
  bool hasPreviousNode() const;
  ListNode* advance() const; // or use operator++
  ListNode* goBack() const; // or use operator--
  PayloadType getValue() const;

  ListNode* removeSelfAndReturnPointerToPrevious();
  ListNode* removeSelfAndReturnPointerToNext();
  void removeSelfAndAllNodesLeftAndRight();
  // maybe add a more iterator like interface and some more auxiliary functions

private:
  ListNode* _previousOrZero;
  ListNode* _nextOrZero;
  PayloadType _payload;
};


This is how the class could be used:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ListNode startNode(0);
ListNode* current = & startNode;
for (int i = 0; i < 10; ++i)
{
  current = current.appendAndReturnPointerToNewNode();
}
// doubly linked list now contains 0,1,2,...,8,9
//access 3rd element
current = & startNode;
for (int i = 0; i < 3;++i)
  current = current.advance();
std::cout << current.getValue();

startNode.removeSelfAndAllNodesLeftAndRight();

Could you give me an example of how to append also? It would be much appreciated!
adding a node or nodes is like this, assuming end() returns a reference to the end of the list:
1
2
3
4
addnode(node *n){
	n->prev=end();
	end().next=&n;
}
Last edited on
What is the general way of implementing a Begin and End function in linked lists?
Normally you would have a List class which holds pointers to the first and the last nodes.
In this case, you'll have to simply iterate through nodes until you find a null pointer.
That would be for the end of the list, but what about the beginning?
It's a doubly linked list. It's symmetrical. It doesn't matter which way you go (as long as first node . prev == 0).
In doubly-linked list the pointer-to-previous of the first node is null and the pointer-to-next of the last node is null. You can recognize them both.
EDIT: as hamsterman said :)

A problem with this approach is that you will not be able to save pointers to the first and last nodes somewhere aside. And this is usually the advantage that a List class provides over a node-based solution.

One way to hack this is to have a dummy start node and a dummy end node. They don't even need the storage slot for the payload. You will then iterate the nodes in the list starting from the second and finishing with the next to last node. The very first and very last nodes will never be relocated in memory (as your code will know not to mess with them) and you can use them to anchor parts of your code safely to the beginning and end of your list.

IMHO node-based lists are potentially useful stuff. Boost intrusive lists are a bit like that.

Regards
Last edited on
Hey Simeonz, thanks for your informative response. Could you give me an example in some code about using dummy start and end nodes? It would be very helpful if I could visualise it a bit better!

At the moment in order to find the last node, I'm doing something like this:

1
2
3
4
5
6
7
8
9
10
11
	ListNode* Last()
	{
		ListNode *current = mFirst;

		while(current != NULL)
		{
			current = current->mNext;
		}

		return current;
	}


Am I on the right track?
Last edited on
So you are returning NULL.
Yeah at the moment, my question is how would I get this working correctly the way I want it to?
current->mNext != NULL But be careful to do not dereference a null pointer
Last edited on
Here is a prototype of the solution for dummy/sentinel nodes with some implementations left for you. Its a bit excessive and probably not the best approach for your purposes. A lot of tight coupling. There are many choices that could be made differently. Like, the links are bound to one type and the list manages the lifetime and storage for the nodes. There are other approaches.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
#include <iostream>

using std::cout;
using std::endl;
using std::cin;

template <typename T>
class Node;

template <typename T>
class List;

// Link is a helper class that contains only the hooks that a node
// uses to connect to the other nodes. It contains no payload.

template <typename T>
class Link {

public:

    // Return the node corresponding to the next link.
    Node<T> *next() const
    {
        if (m_next->m_next)
        {
            return static_cast<Node<T>*>(m_next);
        }
        else
        {
            return NULL;
        }
    }

    // Return the node corresponding to the next link.
    Node<T> *prev() const
    {
        if (m_prev->m_prev)
        {
            return static_cast<Node<T>*>(m_prev);
        }
        else
        {
            return NULL;
        }
    }

    // Create a node and link it after this link.
    void insertBefore(const T &data);

    // Create a node and link it before this link.
    void insertAfter(const T &data);

protected:

    // Decouple the link from its two neighbours in the list.
    void unlink();

    // Connect link between this and the preceding one.
    void linkBefore(Link *prev);

    // Connect link between this and the next one.
    void linkAfter(Link *next);

    // Disallows constructing links directly
    Link() {}
    
    // Disallows destroying links directly
    ~Link() {}

private:

    Link *m_prev;
    Link *m_next;

    // List and Link are tightly coupled.
    // They must both understand dummy nodes.
    
    friend class List<T>;
};

// The nodes augments the links with payload.

template <typename T>
class Node : public Link<T> {

public:

    // These friend functions need to construct nodes.
    // The nodes are otherwise not constructible.
    friend
    void Link<T>::insertBefore(const T &data);

    friend
    void Link<T>::insertAfter(const T &data);

    // Destroy and deallocate the node.
    void kill();

    T &data() { return m_data; }

private:

    // Initialize the payload. Not user constructible.
    Node(const T &data);

    // Unlink the node. Not user destructible.
    ~Node();

    T m_data;
};

// List is functionality that does not have to exist in a class.
// This code is template for the way that the program can use the
// dummy nodes. Insertion/removal) are still possible by using
// the nodes entirely.

template <typename T> class List {

public:

    List()
    {
        m_start.m_prev = NULL;
        m_start.m_next = &m_end;
        m_end.m_prev = &m_start;
        m_end.m_next = NULL;
    }

    ~List()
    {
        Node<T> *current, *next = begin();

        while((current = next) != NULL)
        {
            next = current->next();
            current->kill();
        }
    }

    Node<T> *begin() const
    {
        m_start.next();
    }

    Node<T> *end() const
    {
        m_end.prev();
    }

    void push_front(const T &data)
    {
        m_start.insertAfter(data);
    }

    void push_back(const T &data)
    {
        m_end.insertBefore(data);
    }

private:

    Link<T> m_start;
    Link<T> m_end;
};

int main()
{
    {
        List<int> list;

        for(int i = 0; i < 10; ++i)
        {
            list.push_back(i);
        }

        Node<int> *next;

        // Print the numbers in the list and remove the odd ones.
        for (Node<int> *node = list.begin(); node != NULL; /*node == next*/)
        {
            cout << node->data() << endl;

            next = node->next();

            if (node->data() % 2)
            {
                node->kill();
            }

            node = next;
        }

        cout << endl;

        // Print the numbers in the list again.
        for (Node<int> *node = list.begin(); node != NULL; node = node->next())
        {
            cout << node->data() << endl;
        }
        
        // Destroy the list.
    }

    cin.get();
}
Just wondering, how would writing these data structures ever be useful? I would imagine the C++ std lib would have this covered. In what scenarios would this be useful?
It is very useful in the scenario where you want to learn something.
Topic archived. No new replies allowed.