3
\$\begingroup\$

Issues with generics < E > and < E extends Comparable > in code below. What changes are needed in the code below to prevent DLList.push_front() and DLList.push_back() from accepting strings instead of Integers?

The following code is a sub-set of C++ std::list, implemented as a circular double linked list that includes an internal node list where list.next is a reference to the first node, and list.prev is a reference to the last node. list is the equivalent of std::list::end(). References to nodes are used instead of std::list::iterator.

DLList.sort() is a recursive merge sort, but instead of scanning lists to split them, it recursively divides a count (size) by 2 until a base case of size == 1 is reached. DLList.merge() uses DLList.splice() to move one or more nodes at a time within a list to sort the list. Visual Studio 2022 now uses this method in std::list::sort(). On my old desktop with Intel 3770K CPU, it takes about 2 seconds to sort 4,194,304 Integers from sequentially allocated nodes, and about 3 seconds for scattered nodes (tested by refilling sorted list with random numbers and sorting again).

In the Visual Studio 2022 and my C++ implementations for std::list::sort() a pointer to the beginning node is passed by reference and a pointer to the ending node is returned. Java doesn't have pass by reference, so a static instance (one time allocation) of a node pair np is used instead, passed to and returned by sortr(). sortr() uses np.beg and sz as inputs, returns np, and only sets np.end in base cases where sz == 1.

I use one working directory with NetBeans, copying source files to x.java for testing, which is why package x is used.

package x;

import java.util.Random;

class DLNode<E>{
    DLNode<E> next;
    DLNode<E> prev;
    E element;
    DLNode(){
        next = null;
        prev = null;
        element = null;
    }
    DLNode(E e){
        next = null;
        prev = null;
        element = e;
    }
}
class NodePair{
    DLNode beg;
    DLNode end;
    NodePair(){
        beg = null;
        end = null;
    }
}
class DLList<E>{
    DLNode<E> list;
    int size;
    DLList(){
        list = new <E> DLNode();
        list.next = list;
        list.prev = list;
        size = 0;
    }
    public int size() {
        return size;
    }
    public DLNode<E> begin(){
        return list.next;
    }
    public DLNode<E> end(){
        return list;
    }
    public E front() {
        if(size == 0)
            return null;
        return (E) list.next.element;
    }
    public E back() {
        if(size == 0)
            return null;
        return (E) list.prev.element;
    }
    public void push_front(E element) {
        DLNode<E> node = new DLNode(element);
        size++;
        node.next = list.next;
        node.prev = list;
        list.next.prev = node;
        list.next = node;
    }
    public void push_back(E element) {
        DLNode<E> node = new DLNode(element);
        size++;
        node.next = list;
        node.prev = list.prev;
        list.prev.next = node;
        list.prev = node;
    }
    public E pop_front() {
        if(size == 0)
            return null;
        size--;
        DLNode<E> node = list.next;
        DLNode<E> next = node.next;
        next.prev = list;
        list.next = next;
        return (E) node.element;
    }
    public E pop_back() {
        if(size == 0)
            return null;
        size--;
        DLNode<E> node = list.prev;
        DLNode<E> prev = node.prev;
        prev.next = list;
        list.prev = prev;
        return (E) node.element;
    }
    // move rgt node to just before lft node
    public void splice(DLNode lft, DLNode rgt){
        rgt.prev.next = rgt.next;           // remove rgt node
        rgt.next.prev = rgt.prev;
        rgt.prev = lft.prev;                // insert before lft
        rgt.next = lft;
        lft.prev.next = rgt;
        lft.prev = rgt;
    }
    // move rgt to end.prev nodes to just before lft node
    public void splice(DLNode lft, DLNode rgt, DLNode end){
        DLNode lst = end.prev;              // reference to last node
        rgt.prev.next = end;                // remove rgt nodes
        end.prev = rgt.prev;
        rgt.prev = lft.prev;                // insert before lft
        lst.next = lft;
        lft.prev.next = rgt;
        lft.prev = lst;
    }
    // merge two sorted runs using splice to rearrange nodes within list
    private <E extends Comparable> DLNode<E> merge(DLNode<E> lft, DLNode<E> rgt, DLNode<E> end){
        DLNode<E> nxt;
        DLNode<E> rtn = lft;                // set reference to first merged node
        if(lft.element.compareTo(rgt.element) > 0)
            rtn = rgt;
        while(true){                        // merge runs
            // advance lft until lft > rgt
            while(lft.element.compareTo(rgt.element) < 1){
                lft = lft.next;
                if(lft == rgt)
                    return rtn;
            }
            // advance nxt undil nxt >= lft */
            nxt = rgt.next;
            while(nxt != end && nxt.element.compareTo(lft.element) < 1)
                nxt = nxt.next;
            // move rgt to nxt.prev to before lft
            splice(lft, rgt, nxt);
            rgt = nxt;
            if(rgt == end)
                return rtn;
        }
    }
    // merge sort using stack to track sorted run boundaries
    // lft and sz are local, np is one time allocation
    private NodePair sortr(NodePair np, int sz){
        if(sz == 1){
            np.end = np.beg.next;
            return np;
        }
        np = sortr(np, sz-sz/2);            // lft run
        DLNode lft = np.beg;
        np.beg = np.end;                    // rgt run
        np = sortr(np,    sz/2);
        np.beg = merge(lft,np.beg,np.end);  // merge
        return np;
    }
    public void sort(){
        if(size() < 2)
            return;
        NodePair np = new NodePair();
        np.beg = list.next;
        sortr(np, size);
    }
}

public class x {
    public static void main(String[] args) {
        DLList list = new <Integer> DLList();
        final int COUNT = 4*1024*1024;
        Random r = new Random();
        DLNode<Integer> node;
        Integer i;
        Integer j;
        long bgn, end;
        // test sort with sequentially allocated nodes
        for(i = 0; i < COUNT; i++)
            list.push_back((Integer)r.nextInt());
        bgn = System.currentTimeMillis();
        list.sort();
        end = System.currentTimeMillis();
        System.out.println("milliseconds " + (end-bgn));
        // test sort with scattered nodes
        for(node = list.begin(); node != list.end(); node = node.next)
            node.element = (Integer)r.nextInt();
        bgn = System.currentTimeMillis();
        list.sort();
        end = System.currentTimeMillis();
        System.out.println("milliseconds " + (end-bgn));
        // verify sort worked
        node = list.begin();
        i = node.element;
        j = i;
        for(node = node.next; node != list.end(); node = node.next){
            j = node.element;
            if(i.compareTo(j) > 0)
                break;
            i = j;
        }
        if(i.compareTo(j) == 0)
            System.out.println("passed");
        else
            System.out.println("failed");
        // remove all nodes from list
        while (0 != list.size())
            list.pop_front();
    }
}
```
\$\endgroup\$
5
  • \$\begingroup\$ @vnp - thanks. I messed up on the title, and left out the part about efficient std::list::sort. You can read more about Visual Studio 2015 needlessly switching to an inefficient top down merge sort when the prior bottom up sort could have been updated to handle no memory allocation and exception safety mentioned in the other thread. Visual Studio 2022 switched to the efficient and simple method shown in my question: stackoverflow.com/a/40629882/3282056 . I don't use Java much, and it's not clear what parts of the code really need generic typing. \$\endgroup\$
    – rcgldr
    Commented Sep 26 at 4:52
  • 4
    \$\begingroup\$ Please don't modify the code in your question once it has been answered. You could post improved code as a new question, as an answer, or as a link to an external site - as described in I improved my code based on the reviews. What next?. I have rolled back the edit, so that it's clear exactly what version has been reviewed. \$\endgroup\$ Commented Sep 26 at 10:27
  • \$\begingroup\$ The page I link does give other options - you could post a self-answer, or you could ask to have your new code reviewed. \$\endgroup\$ Commented Sep 26 at 14:34
  • \$\begingroup\$ @TobySpeight - I accepted an answer (partially from the chat in answer). No need to post new code. \$\endgroup\$
    – rcgldr
    Commented Sep 27 at 13:24
  • \$\begingroup\$ Thanks for all of the responses. The suggestions fixed the issues in this question's code, specifically preventing push_front and push_back from accepting strings. Performance remains the same. As noted in answers and comments, it's faster to copy list to array, sort array, copy array back to list, but the goal here was a list based sort similar to C++ std::list::sort(). \$\endgroup\$
    – rcgldr
    Commented Sep 28 at 20:40

3 Answers 3

8
\$\begingroup\$

Code Structure / Encapsulation

Class DLList representing your doubly-linked list implementation should be public and its attributes private.

Auxiliary classes should be encapsulated within the list as nested private static since they are only supposed to be used internally by the list.

External code should have no notion of list-nodes, list should be a designed as a user-friendly abstraction instead of leaking its implementation details.

Comparable

Your list is broken because it allows adding elements that are non-comparable. And then when it comes to sorting, it stipulates that values should implement Comparable.

This issue can be resolved in two ways.

  • You can mandate upfront that list-elements should be comparable by changing the class declaration:
public class DLList<E extends Comparable<? super E>>

Where Comparable<? super E> means that either E or one of its super-types implements Comparable (which is more flexible than E extends Comparable<E>, i.e. E type itself implements comparable, and not inherits implementation).

Since Comparable is supposed to be implemented only by types that have natural ordering, demanding that elements should be comparable is a very strict constraint that will limit the usage of the list.

For example, it's almost never the case when domain types have natural ordering (there's no unified widely recognized way to compare students, employees, books, etc.).

A better option would be to use a Comparator to determine the ordering of elements.

public class DLList<E> {
    private final Comparator<? super E> comparator;
}

Where Comparator<? super E> means that this comparator is capable of dialing with instances of type E or one of its super-types.

If you choose this route, your list would need to expose a constructor expecting a Comparator as a parameter.

Optionally it might also expose a constructor which does not require a comparator, and in such a case while sorting elements they should be treated as comparable; i.e. you need to cast the values of nodes into a Comparable of raw type. That's one of the rare cases when use raw types is justifiable (but you have many others that are not, more on that below), and don't forget to apply annotation @SuppressWarnings("unchecked") while performing this cast.


Regardless of which of the two strategies listed above you choose, method merge() should not declare its own generic type parameter before its return type.

Redundant Constructors / Initialization to null

  • No-args constructors (in classes that do not declare parameterized constructors), which initialize attributes to default values defined by the language specification (such as null for reference types) only create noise.

You don't need them, Java-compiler will autogenerate them for you.

NodePair() {
    beg = null;
    end = null;
}
  • When a class has more than one constructor, you can call constructors from one another (so-called telescopic constructors). But, please, doesn't perform the automatic assignment to null manually. If a developer has chosen to use a certain technology (language, framework), it should be well-understood and trusted.

So instead of this:

DLNode() {
    next = null;
    prev = null;
    element = null;
}

DLNode(E e) {
    next = null;
    prev = null;
    element = e;
}

I'll suggest the following:

DLNode() {
    this(null);
}

DLNode(E e) {
    element = e;
}

Raw Types

You're using raw types in many places of the code without good reason.

  • NodePair should be declared as generic, as well as its fields.

  • Generic type variables are missing in the parameters type declarations in the splice() and sortr() methods (not a very descriptive name, by the way).

Generic types should be provided while instantiating a generic class. Either explicitly (e.g. new DLNode<E>()), or through the type-inference mechanism by using so-called diamond operator which is more convenient.

  • DLNode<E> node = new DLNode(element); -> DLNode<E> node = new DLNode<>(element);

  • list = new <E> DLNode(); -> list = new DLNode<>();

The Java compiler will spot all such cases for you and will issue a warning for each of them.

Naming

  • In Java, it's not considered a good practice to skimp on characters. Especially, when it comes to publicly exposed names, such as the names of public types and methods, they clearly communicate the purpose to the user.

For instance, DLList is not a very descriptive name, DL isn't a widely-used abbreviation, and it's discouraged by the Java language naming convention

Try to keep your class names simple and descriptive. Use whole words-avoid acronyms and abbreviations (unless the abbreviation is much more widely used than the long form, such as URL or HTML).

A proper convention-aligned name would be DoublyLinkedList.

  • Also according to the naming convention method names should be in camel-case: push_front() -> pushFront().
\$\endgroup\$
11
  • 1
    \$\begingroup\$ I don't use Java much, and I'm trying to keep the code as similar as possible to my C++ implementation. Since the only intended way to add nodes to a list is with generically typed push_front() and push_back(), there shouldn't be an issue. I used references to nodes to replace C++ std::list::iterator . I don't know how to make custom iterators for Java. Functions like C++ std::list::splice() require iterators, so external code is aware of nodes and can get or set node values via those iterators. I upvoted your answer since it gave me guidance on Java. \$\endgroup\$
    – rcgldr
    Commented Sep 26 at 9:06
  • \$\begingroup\$ @rcgldr Iterator implementation might look like this (link is not runnable -> copy iterator() method and update the class declaration by making the list implement Iterable). After this change, you'll be able to plug a list instance in an enhanced for-loop or use iterator directly. \$\endgroup\$ Commented Sep 26 at 13:34
  • \$\begingroup\$ @rcgldr "prevent push_back() or push_front() from accepting string instead of Integer" - your implementations of these methods are type-safe, just don't create lists of raw type like you did in the main (they accept any type). Instead of DLList list -> DLList<Integer> list. And if you'll try to add a String in this list, the compiler will give you a compilation error. \$\endgroup\$ Commented Sep 26 at 17:02
  • \$\begingroup\$ @rcgldr "The public version of splice() takes iterators and std::list as parameters" - if we are talking about implementing methods that are not present in your initial code, then it's a bit too far away from the intended purpose of this site, which is reviewing the code shared in the question. Such request would be more suitable on Stack Overflow(stackoverflow.com), but I'll advise you at least trying to implement these methods (it'll increase your chances to get constructive feedback on SO). If you succeed, then you can post the next iteration of your code here on CodeReview. \$\endgroup\$ Commented Sep 26 at 17:05
  • 1
    \$\begingroup\$ @JohnBollinger Fair point, the wording was not clear. Revised the answer. \$\endgroup\$ Commented Sep 26 at 17:32
6
\$\begingroup\$
  • The two-argument overload void splice(DLNode lft, DLNode rgt) is never called. push_front and pop_back are never called either. If you write a method, at least write a unit test for it.

  • I am not sure I understand what do you mean by scattered. Both tests fill the (same) list with the random numbers, don't they?

  • The verify sort worked loop deserves a name. Consider making an is_sorted method.

    That said, the first (sequential) sort is not verified.

  • sortr base case doesn't feel right. If sz == 1, the np.end must be equal to np.beg.next; if it doesn't, something went awfully wrong. Use an assertion perhaps, and terminate if it failed, but don't blindly reassign np.end.

  • I do not endorse use of tla. Please spell left, right, begin, next, etc.

\$\endgroup\$
4
  • 1
    \$\begingroup\$ std::is_sorted. It is not about std::list, it works for any collection which implements forward iterator (and std::list does of course). \$\endgroup\$
    – vnp
    Commented Sep 26 at 5:56
  • \$\begingroup\$ Re base case. Isn't (sz == 1) && (np.beg.next == np.end) an invariant? \$\endgroup\$
    – vnp
    Commented Sep 26 at 6:01
  • \$\begingroup\$ Yes, is_sorted shall be factored out. \$\endgroup\$
    – vnp
    Commented Sep 26 at 6:02
  • \$\begingroup\$ I added is_sorted to the class. I changed np to be a final object member of DLList class instead of constantly passing and returning it in sortr(). np.end is only set in base case instances, which will occur for every node in the list. Once at the last node, np.end is set to the sentinel node list, and never changed again, always representing the end of the right sub-list, which grows towards the middle of the list while going back up the call chain as sorted sub-lists are merged. \$\endgroup\$
    – rcgldr
    Commented Sep 26 at 15:05
3
\$\begingroup\$

To answer your explicit question:

Revising question: what changes are needed in the code below to prevent DLList.push_front() and DLList.push_back() from accepting strings instead of Integers?

The minimal code change required to prevent that is to replace this line in your main method:

        DLList list = new <Integer> DLList();

with:

        DLList<Integer> list = new DLList<Integer>();

Actually even fixing the constructor call isn't needed, although you should still do it. The absolute minimum change needed is to replace DLList list on that line with DLList<Integer> list.

Specifically, DLList list declares list as having a raw type. This syntax is allowed for backwards compatibility with (very) old Java code that didn't support generic types at all, and basically bypasses Java's compile-time generic type checking. You shouldn't use it.

Any half-decent Java IDE will warn you if you accidentally try to use a raw type. So will the Java compiler itself, if you run it with the -Xlint:unchecked option.

(Heck, even if you don't, it will still warn you once with a message like Note: x/x.java uses unchecked or unsafe operations. But using -Xlint:unchecked will give you more detailed warnings.)

Frame challenge: Is this wheel worth reinventing?

Just out of curiosity, I tried running your sorting benchmarks and comparing them with equivalent benchmarks using the ArrayList and LinkedList classes from the Java standard library. The results on my laptop (2023 M2 MacBook Pro) look like this:

  1. ArrayList: 0.9 seconds (always sequential)
  2. LinkedList: 1.2 seconds (sequential) / 1.1 seconds(!) (non-sequential)
  3. DLList: 1.4 seconds (sequential) / 2.7 seconds (non-sequential)

Of course this isn't anything like a proper JMH benchmark, and you can clearly see that there's something funny going on e.g. with LinkedList being seemingly faster to sort when the nodes are not initially allocated in sequential order. But I did run the benchmarks several times and got at least qualitatively consistent results: ArrayList is always fastest, followed by LinkedList, and your DLList is consistently slower than either of them.

Of course this doesn't mean that your code is useless as a learning exercise, nor that you couldn't, with sufficient effort, come up with a list implementation that's faster (at least for some use cases) than the ones in the Java standard library. But if that's your goal, you have quite some way to go yet. Especially since you can count on the standard library code being quite well optimized already.

On the other hand, if you just want to have a fast list implementation in Java, the easy path is simple: just use ArrayList. It works. :)

\$\endgroup\$
5
  • \$\begingroup\$ Thanks, I had already made that change to DLList<Integer> list in my test code, but code in questions not to be updated to fix the issues being asked about. DLList<Integer> list = new DLList<Integer>(); produces a warning for redundant type arguments. DLList<Integer> list = new DLList<>(); was another option, but not needed. Another issue in my test code was using <E> for DLList, but overriding it using public <E extends Comparable> void push_back(E data){ . My test code is now using <E extends Comparable> on the class declarations such as class DLList<T extends Comparable>{ . \$\endgroup\$
    – rcgldr
    Commented Sep 27 at 19:56
  • \$\begingroup\$ Fast implementation if container is not an array: copy container to an array, sort the array, fill (or re-create) container from sorted array. I suspect this is what Java native sort does, and would result in the "non-sequential" ending up as sequential, resulting in about the same time, sometimes a bit less, sometimes a bit more, due to the random numbers. If the data is integers (or float or double), a faster still radix sort could be used. However, the goal here is a fast linked list sort similar to std::list::sort(), without "cheating" by using an array. \$\endgroup\$
    – rcgldr
    Commented Sep 27 at 20:48
  • \$\begingroup\$ Scaling my results to match yours for DLList, a fast implementation using array of Integers will result in the same time 1.2 seconds as you report. Cheating further still using an array of ints would reduce time on your system to 0.5 seconds. \$\endgroup\$
    – rcgldr
    Commented Sep 27 at 21:25
  • 2
    \$\begingroup\$ @rcgldr by looking at the source code for JDK 21's List.java, I can confirm your suspicion is correct - it involves a toArray followed by Arrays.sort \$\endgroup\$ Commented Sep 28 at 16:47
  • \$\begingroup\$ @TamoghnaChowdhury - Another interesting factor, at least on my system, is after the first sort, filling the list with random numbers and sorting again gets faster, fill and sort a second time, faster still, fill and sort a third time is fastest. Fourth time is about the same as third. Any cache related performance should have leveled out on first or second fill and sort. Link to test code, rename file name to x.java or rename package to llsortn : rcgldr.net/code/llsortn.java . \$\endgroup\$
    – rcgldr
    Commented Sep 28 at 20:32

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.