I am implementing some basic data structures to freshen up my memory and I wanted my DoubleLinkedList
remove method to be reviewed. I feel like it has extra checks but I could not think of another way to implement.
My implementation holds both the head
and the tail
of the list making things a bit more complex.
remove
method:
public void remove(T data) {
if (isEmpty()) {
throw new NoSuchElementException();
}
for (Node<T> current = this.head; current != null; current = current.getNext()) {
if (current.getData().equals(data)) {
Node<T> previous = current.getPrevious();
Node<T> next = current.getNext();
if (this.size == 1) { // At head with size == 1
this.head = this.tail = null;
} else if (previous == null) { // At head with size > 1
this.head = next;
next.setPrevious(previous);
} else if (next == null) { // At tail
previous.setNext(next);
this.tail = previous;
} else { // Rest of cases
previous.setNext(next);
next.setPrevious(previous);
}
this.size--;
return;
}
}
throw new NoSuchElementException();
}
3 Answers 3
I feel like it has extra checks
You could remove the isEmpty
check (which probably just checks this.head == null
), as in that case, the for loop will not be entered and the exception at the end will be thrown.
And I think that your if
cases are fine as they are. If you want to, you could pull one or more of them out of the loop:
public void remove(T data) {
if (isEmpty()) {
throw new NoSuchElementException();
}
if (this.size == 1 && this.head.getData().equals(data)) {
this.head = this.tail = null; // removing only item from list
this.size--;
return;
}
if (this.head.getData().equals(data)) { // removing head
this.head = this.head.getNext();
this.head.setPrevious(null);
this.size--;
return;
}
if (this.tail.getData().equals(data)) { // removing tail
this.tail = this.tail.getPrevious();
this.tail.setNext(null);
this.size--;
return;
}
for (Node<T> current = this.head; current != null; current = current.getNext()) {
if (current.getData().equals(data)) {
Node<T> previous = current.getPrevious();
Node<T> next = current.getNext();
previous.setNext(next);
next.setPrevious(previous);
this.size--;
return;
}
}
throw new NoSuchElementException();
}
If you do this, you would need the isEmpty
check.
This approach would also speed up your code (less checks in loop) and I think it might also be more readable.
You could probably remove the size == 1
case with some restructuring, but I think the resulting code would be worse than what you have.
Misc
if (next == null) { previous.setNext(next); }
: this works, but I would make it explicit that you are setting next to null:if (next == null) { previous.setNext(null); }
, same with theprevious == null
check.
-
1\$\begingroup\$ Don't you need to decrement the size still when you are outside the loop? I.e. everywhere there's a
return
, there should be athis.size--;
\$\endgroup\$Brythan– Brythan2014年10月12日 16:25:07 +00:00Commented Oct 12, 2014 at 16:25 -
\$\begingroup\$ @Brythan oh thanks. Yes, size should be decremented everywhere, I overlooked that. \$\endgroup\$tim– tim2014年10月12日 16:28:50 +00:00Commented Oct 12, 2014 at 16:28
-
\$\begingroup\$ I didn't put them outside the loop because I thought the code was less readable since I would have multiple
return;
and multiplesize--
. Also the "removing tail" if, would not work because if I had a list with: {2, 3, 15, 89, 3} and I didremove(3)
then the first 3 should have been removed not the last therefore I would have to move that if inside the for loop making my code seem a little bit fragmented. \$\endgroup\$Aki K– Aki K2014年10月12日 16:32:27 +00:00Commented Oct 12, 2014 at 16:32 -
\$\begingroup\$ @SilliconTouch I think that it is a lot more readable, and also makes more sense (in the loop, your checking it each time, even though you know that it can only happen on the first/last loop). Good point with the duplicate entries though. To solve that, you could move the tail check to the end of the method, and let the loop run up to
current.getNext() == null
. \$\endgroup\$tim– tim2014年10月12日 16:46:21 +00:00Commented Oct 12, 2014 at 16:46 -
\$\begingroup\$ I think I will implement the separate logic like you said on the other answer's comment and that way I can just "dump" the removal and checks there and have a separate search function. \$\endgroup\$Aki K– Aki K2014年10月12日 16:52:34 +00:00Commented Oct 12, 2014 at 16:52
I have couple suggestions:
Separate Logic
I would separate lookup and modify logic. Add a function to remove element and call it from this one leaving in this one only logic to lookup an item.
public void remove(Node<T> data) {
if (!data) {
throw new NoSuchElementException();
}
// Removing logic here
...
}
public void remove(T data) {
return remove(search(data));
}
public Node<T> search(T data) {
for (Node<T> current = this.head; current != null; current = current.getNext()) {
if (current.getData().equals(data)) {
return current;
}
}
return null;
}
Simplify Checks
I think this should work and cover all your cases, no?
// If this is head of the list
if (previous == null) {
this.head = next;
}
else {
previous.setNext(next);
}
// If this is tail of the list
if (next == null) {
this.tail = previous;
}
else {
next.setPrevious(previous);
}
-
1\$\begingroup\$ Your simplified checks should work, but I think they are a bit harder to understand (and technically, it's still four cases). I also wouldn't split the method, but if you do, I would do it like this: Change your
void remove(T)
method toNode<T> search(T)
, then add a newremove(T)
method which usessearch
andremove
. That way, you can reusesearch
for other operations (likecontains
). Oh, and make all methods containingNode
in the signature private, as it shouldn't be exposed. \$\endgroup\$tim– tim2014年10月12日 16:16:44 +00:00Commented Oct 12, 2014 at 16:16 -
\$\begingroup\$ I agree with @tim that your ifs even though they look better they do not convey as well what they are for I think. The separation of the logic though is something that I might do. \$\endgroup\$Aki K– Aki K2014年10月12日 16:45:50 +00:00Commented Oct 12, 2014 at 16:45
-
\$\begingroup\$ oh, but still +1 for this solution. It's pretty much how oracle implemented this. \$\endgroup\$tim– tim2014年10月12日 16:49:22 +00:00Commented Oct 12, 2014 at 16:49
-
\$\begingroup\$ I agree that we better separate search into its own function. As for readability - I think adding comments would solve it. I will update the answer. \$\endgroup\$sha– sha2014年10月12日 17:15:44 +00:00Commented Oct 12, 2014 at 17:15
A lot of those here try to avoid nesting. In your case, this would mean switching the equality check to an inequality check.
If you move your head and tail checking outside of the for loop, you could make the loop much simpler:
for (Node<T> current = this.head.getNext(); current != this.tail; current = current.getNext()) {
if ( ! current.getData().equals(data) ) {
continue;
}
Note that the prior checks (as shown in Tim's answer) ensure that the list is at least of size 2 and that you aren't removing either the head or the tail. The start and end conditions ensure that you only remove things between the head and the tail.
remove
. 2. Instead, they return the removed element, if any. 3. I'm unsure, ifNoSuchElementException
is appropriate, if you really want to throw. 4. Using a cycle with a dummy element would reduce all four cases to one, but I'm not claiming you should waste the memory for it. \$\endgroup\$NoSuchElementException
that is why I did it like that. \$\endgroup\$