5

Context:

I have a LinkedList class which contains and automatically inserts nodes in the correct order. Note this is a linked list data structure (the array in which the nodes/elements are kept represents RAM, and the pointers - head, tail, as well as next and prev represent addresses in RAM (but in this example, they are indexes of the array in which the nodes are kept).

Eg

myLinkedList.insert(2);
myLinkedList.insert(1);
myLinkedList.output(); // => [{value:2, next:null, prev:1}, {value:1,next:0,prev:null]}, head = 1, tail = 0

So now when I call my printInOrder function, it will output 1, then 2, then stop.

Note: When I insert a new node, it is pushed to the end of the array, but the pointers of what would be its adjacent nodes are changed (so next and prev) so that the train-like path from the head to the tail includes all nodes in a particular order (default is ascending). So the node which gets inserted always goes at the end, only its pointers signify its position.

My problem:

Here is my problem: (see the code at the end of the question)

Imagine I have created a linked list, which is sorted by default (ascending order), and I have the values 2, 1, and 3. So when I iterate through the list, I will receive 1,2,3. Now, I want to re-order the linked list. What that would mean is, that the index of each node does not change, but the pointers of the nodes do. After all, the pointers are what create the order. So how would I use, some sorting algorithm, say merge or bubble, to sort my nodes without actually changing their order, just their next and prev pointers (and the global head and tail).

My Code

Here is the code so far for the re-sort function, which currently uses bubble sorting but does not work:

class LinkedList {
  constructor(sortingFunction) {
    this.head;
    this.tail;
    this.list = [];
    this.sortingFunction = sortingFunction ? ? ((a, b) => {
      return a < b
    });
  }
  sort(sortingFunction) {
    if (!sortingFunction) {
      return false;
    }
    this.head = null;
    this.tail = null;
    const arr = this.list.map(x => x);
    for (let i = 0; i < arr.length; i++) {
      for (let j = 0; j < arr.length; j++) {
        if (!arr[j + 1] ? .value) {
          console.log("no");
          continue;
        }
        if (sortingFunction(arr[j].value, arr[j + 1].value)) {
          let tmp_next = arr[j].next;
          let tmp_prev = arr[j].previous;
          arr[j].next = arr[j + 1].next;
          arr[j].previous = arr[j + 1].previous;
          arr[j + 1].next = tmp_next;
          arr[j + 1].previous = tmp_prev;
        }
      }
    }
    this.list = arr;
  }
}

And here is the entire code of my LinkedList class, which will allow you to recreate my problem - which is that the nodes simply do not sort themselves. Their pointers change but not the way they should, and I cannot understand why.

class LinkedList {
   constructor(sortingFunction) {
      this.head;
      this.tail;
      this.list = [];
      this.sortingFunction = sortingFunction ?? ((a,b) => {return a < b});
   }

   some(func) {
      let currentNode = this.list[this.head];
      let index = this.head;
      while(!func(currentNode)) {
         index = currentNode.next;
         currentNode = this.list[index];
         if(index == undefined || index == null) { return -1; }
      }
      return index;
   }
   forEachInOrder(func) {
      let current = this.head;
      while(current != undefined) {
         const node = this.list[current];
         func(node);
         current = node.next;
      }
   }
   * iterator() {
      let current = this.head;
      while(current != undefined) {
         const node = this.list[current];
         yield node;
         current = node.next;
      }
   }
   
   insert(value) {
      if(!this.list.length) {
         this.head = 0;
         this.tail = 0;
         this.list.push({value, next:null,previous:null});
         return 0;
      }
      let nodeToInsert = {value, next:null,previous:null};
      let indexToInsert = this.head;
      let nthnode = this.list[this.head];
      while(nthnode && this.sortingFunction(nthnode.value, value)) {
         indexToInsert = nthnode.next;
         nthnode = this.list[indexToInsert];
      }
      if(indexToInsert === null) {
         // new tail (biggest)
         nodeToInsert.previous = this.tail;
         this.list[this.tail].next = this.list.length;
         this.tail = this.list.length;
      } else if(indexToInsert === this.head) {
         // new head (smallest)
         nodeToInsert.next = this.head;
         this.list[this.head].previous = this.list.length;
         this.head = this.list.length;
      } else {
         nodeToInsert.next = indexToInsert;
         if(this.list[this.list[indexToInsert].previous]?.next != undefined) {
            this.list[this.list[indexToInsert].previous].next = this.list.length;
         }
         nodeToInsert.previous = this.list[indexToInsert].previous;
         this.list[indexToInsert].previous = this.list.length;
      }
      this.list.push(nodeToInsert);
      return 1;
   }

   reverse() {
      let _temp;
      for(let i = 0; i < this.list.length; i++) {
         _temp = this.list[i].next;
         this.list[i].next = this.list[i].previous;
         this.list[i].previous = _temp;
      }
      _temp = this.head;
      this.head = this.tail;
      this.tail = _temp;
   }
   sort(sortingFunction) {
      if(!sortingFunction) {return false;}
      this.head = null;
      this.tail = null;
      const arr = this.list.map(x=>x);
      for (let i = 0; i < arr.length; i++) {
         for (let j = 0; j < arr.length; j++) {
            if(!arr[j+1]?.value) {continue;}
            if (sortingFunction(arr[j].value, arr[j+1].value)) {
               let tmp_next = arr[j].next;
               let tmp_prev = arr[j].previous;
               arr[j].next = arr[j+1].next;
               arr[j].previous = arr[j+1].previous;
               arr[j+1].next = tmp_next;
               arr[j+1].previous = tmp_prev;
            }
         }
      }
      this.list = arr;
   }

   print() {
      console.log(this.list);
      console.log("Head:",this.head,"\nTail:",this.tail, "\nDefault is ascending order.");
   }
   printInOrder() {
      let current = this.list[this.head];
      while(current) {
         console.log(current.value);
         current = this.list[current.next];
      }
   }
}


const list = new LinkedList();

list.insert(100);
list.insert(30);
list.insert(50);
list.insert(400);
list.insert(10);
list.insert(200);
list.insert(-90);



console.log("When each node is sorted when it is inserted:")

list.print();

list.sort((a, b) => {
  return a > b;
});

console.log("Now, when re-sorted:");

list.print();

0

2 Answers 2

1

Some issues in your sort function:

  • The inner loop looks at pairs which are consecutive by index, but it should compare pairs that are consecutive by links, since that is where you should decide a swap is necessary or not.

  • Your code's swap involves 4 assignments to links, but there are actually 6 links involved:

enter image description here

  • this.head and this.tail are set to null but never set to their appropriate values again.

Your code has a nice iterator() method, which I would like to reuse in your bubble sort, but to avoid that it will use next references that have been altered by a recent swap, I would suggest a tiny change in that generator so it will be immune to that:

   * iterator() {
      let current = this.head;
      while(current != undefined) {
         const node = this.list[current];
         current = node.next; // <--- move this here!
         yield node; // ...so the consumer can safely alter node.next
      }
   }

And now your sort method can become:

   sort(sortingFunction) {
      if (!sortingFunction) return;
      
      for (let i = 0; i < this.list.length; i++) {
         let prevNode = null;
         // Iterate in the current order, so by following the links:
         for (let currNode of this.iterator()) { // Requires the change in the generator 
            if (prevNode && sortingFunction(currNode.value, prevNode.value)) {
               // Get the indexes of the current pair of nodes:
               let currIndex = prevNode.next;
               let prevIndex = currNode.previous;
               // Update links from outer neighbors to these two nodes
               if (currNode.next != null) this.list[currNode.next].previous = prevIndex;
               else this.tail = prevIndex;
               if (prevNode.previous != null) this.list[prevNode.previous].next = currIndex;
               else this.head = currIndex;
               // Update links from these two nodes to outer neighbors:
               currNode.previous = prevNode.previous;
               prevNode.next = currNode.next;
               // Update links between the two nodes themselves:
               prevNode.previous = currIndex;
               currNode.next = prevIndex;
            } else {
               prevNode = currNode;
            }
         }
      }
   }

Here is the whole script where I couldn't resist to use your generator function also in forEachInOrder and some:

class LinkedList {
   constructor(sortingFunction) {
      this.head;
      this.tail;
      this.list = [];
      this.sortingFunction = sortingFunction ?? ((a,b) => {return a < b});
   }

   some(func) {  // I adapted this to use the iterator
      for (const node of this.iterator()) {
         if (func(node)) {
            return node.previous == null ? this.head : this.list[node.previous].next;
         }
      }
      return -1;
   }

   forEachInOrder(func) { // I adapted this to use the iterator
      for (const node of this.iterator()) func(node);
   }

   * iterator() {
      let current = this.head;
      while(current != undefined) {
         const node = this.list[current];
         current = node.next; // <--- move this here!
         yield node;
      }
   }
   
   insert(value) {
      if(!this.list.length) {
         this.head = 0;
         this.tail = 0;
         this.list.push({value, next:null,previous:null});
         return 0;
      }
      let nodeToInsert = {value, next:null,previous:null};
      let indexToInsert = this.head;
      let nthnode = this.list[this.head];
      while(nthnode && this.sortingFunction(nthnode.value, value)) {
         indexToInsert = nthnode.next;
         nthnode = this.list[indexToInsert];
      }
      if(indexToInsert === null) {
         // new tail (biggest)
         nodeToInsert.previous = this.tail;
         this.list[this.tail].next = this.list.length;
         this.tail = this.list.length;
      } else if(indexToInsert === this.head) {
         // new head (smallest)
         nodeToInsert.next = this.head;
         this.list[this.head].previous = this.list.length;
         this.head = this.list.length;
      } else {
         nodeToInsert.next = indexToInsert;
         if(this.list[this.list[indexToInsert].previous]?.next != undefined) {
            this.list[this.list[indexToInsert].previous].next = this.list.length;
         }
         nodeToInsert.previous = this.list[indexToInsert].previous;
         this.list[indexToInsert].previous = this.list.length;
      }
      this.list.push(nodeToInsert);
      return 1;
   }

   reverse() { // I adapted this to use the (modified) iterator
      for (const node of this.iterator()) {
         [node.next, node.previous] = [node.previous, node.next];
      }
      [this.head, this.tail] = [this.tail, this.head];
   }
   
   sort(sortingFunction) {
      if (!sortingFunction) {return false;}
      
      for (let i = 0; i < this.list.length; i++) {
         let prevNode = null;
         // Iterate in the current order, so by following the links:
         for (let currNode of this.iterator()) { // Requires the change in the generator 
            if (prevNode && sortingFunction(currNode.value, prevNode.value)) {
               // Get the indexes of the current pair of nodes:
               let currIndex = prevNode.next;
               let prevIndex = currNode.previous;
               // Update links from outer neighbors to these two nodes
               if (currNode.next != null) this.list[currNode.next].previous = prevIndex;
               else this.tail = prevIndex;
               if (prevNode.previous != null) this.list[prevNode.previous].next = currIndex;
               else this.head = currIndex;
               // Update links from these two nodes to outer neighbors:
               currNode.previous = prevNode.previous;
               prevNode.next = currNode.next;
               // Update links between the two nodes themselves:
               prevNode.previous = currIndex;
               currNode.next = prevIndex;
            } else {
               prevNode = currNode;
            }
         }
      }
   }

   print() { // I adapted this a bit to get more condense output and add a call to printInOrder
      console.log(JSON.stringify(this.list));
      console.log("Head:",this.head,"\nTail:",this.tail, "\nDefault is ascending order.");
      this.printInOrder();
   }
   
   printInOrder() { // I adapted this to use your nice iterator()
      console.log(...Array.from(this.iterator(), node => node.value));
   }
}


const list = new LinkedList();

list.insert(100);
list.insert(30);
list.insert(50);
list.insert(400);
list.insert(10);
list.insert(200);
list.insert(-90);

console.log("When each node is sorted when it is inserted:")

list.print();

list.sort((a, b) => {
  return a > b;
});

console.log("Now, when re-sorted:");

list.print();

Just one more note: your sortingFunction returns a boolean (indicating whether the two given arguments are in their correct order), which is unlike the comparator function that can be passed to the native Array#sort method: there it is expected to return a number, which allows to indicate whether the arguments are in their correct order by returning a negative value, equality with 0 and inversion with a positive value. You might want to follow the same "contract" in your implementation.

Using a more efficient sorting algorithm

Instead of the O(n²) bubble sort, you could go for a O(nlogn) merge sort.

I have here added a mergeSort method, and a helper method to make a partition: splice. It extracts a section out of the linked list (removing it) and returns it as a new instance. For this splicing to work well, it uses the same list like a shared memory pool, but with its own head and tail. This means that the length of the list is no longer an indication of the size of the linked list, and so some code that referenced length had to change, like the outer loop in the bubble sort routine:

class LinkedList {
   constructor(sortingFunction) {
      this.head = null; // Initialise with null explicitly
      this.tail = null;
      this.list = [];
      this.sortingFunction = sortingFunction ?? ((a,b) => {return a < b});
   }

   some(func) {  // I adapted this to use the iterator
      for (const node of this.iterator()) {
         if (func(node)) {
            return node.previous == null ? this.head : this.list[node.previous].next;
         }
      }
      return -1;
   }

   forEachInOrder(func) { // I adapted this to use the iterator
      for (const node of this.iterator()) func(node);
   }

   * iterator() {
      let current = this.head;
      while(current != undefined) {
         const node = this.list[current];
         current = node.next; // <--- move this here!
         yield node;
      }
   }
   
   insert(value) {
      if (this.head == null) { // Avoid using list.length
         this.head = this.list.length; // While here it is appropriate to use list.length!
         this.tail = this.list.length;
         this.list.push({value, next:null,previous:null});
         return 0;
      }
      let nodeToInsert = {value, next:null,previous:null};
      let indexToInsert = this.head;
      let nthnode = this.list[this.head];
      while(nthnode && this.sortingFunction(nthnode.value, value)) {
         indexToInsert = nthnode.next;
         nthnode = this.list[indexToInsert];
      }
      if(indexToInsert === null) {
         // new tail (biggest)
         nodeToInsert.previous = this.tail;
         this.list[this.tail].next = this.list.length;
         this.tail = this.list.length;
      } else if(indexToInsert === this.head) {
         // new head (smallest)
         nodeToInsert.next = this.head;
         this.list[this.head].previous = this.list.length;
         this.head = this.list.length;
      } else {
         nodeToInsert.next = indexToInsert;
         this.list[this.list[indexToInsert].previous].next = this.list.length;
         nodeToInsert.previous = this.list[indexToInsert].previous;
         this.list[indexToInsert].previous = this.list.length;
      }
      this.list.push(nodeToInsert);
      return 1;
   }

   reverse() {
      for (const node of this.iterator()) {
         [node.next, node.previous] = [node.previous, node.next];
      }
      [this.head, this.tail] = [this.tail, this.head];
   }
   
   sort(sortingFunction) {
      if (!sortingFunction) return false;
      let dirty = true;
      while (dirty) { // Removed dependency on length
         dirty = false;
         let prevNode = null;
         // Iterate in the current order, so by following the links:
         for (const currNode of this.iterator()) { // Requires the change in the generator 
            if (prevNode && sortingFunction(currNode.value, prevNode.value)) {
               // Get the indexes of the current pair of nodes:
               let currIndex = prevNode.next;
               let prevIndex = currNode.previous;
               // Update links from outer neighbors to these two nodes
               if (currNode.next != null) this.list[currNode.next].previous = prevIndex;
               else this.tail = prevIndex;
               if (prevNode.previous != null) this.list[prevNode.previous].next = currIndex;
               else this.head = currIndex;
               // Update links from these two nodes to outer neighbors:
               currNode.previous = prevNode.previous;
               prevNode.next = currNode.next;
               // Update links between the two nodes themselves:
               prevNode.previous = currIndex;
               currNode.next = prevIndex;
               dirty = true;
            } else {
               prevNode = currNode;
            }
         }
      }
   }
   
   // Added this method which can also be of use for algorithms that use partitioning
   splice(head, tail) {
      // Remove this slice from the current linked list
      if (tail != this.tail) this.list[this.list[tail].next].previous = this.list[head].previous;
      else this.tail = this.list[head].previous;
      if (head != this.head) this.list[this.list[head].previous].next = this.list[tail].next;
      else this.head = this.list[tail].next;
      this.list[tail].next = null;
      this.list[head].previous = null;
      // Wrap the removed slice into a new linked list instance, but one that shares the memory list
      let slice = new LinkedList(this.sortingFunction);
      slice.list = this.list;
      slice.head = head;
      slice.tail = tail;
      return slice;
   }

   mergeSort(sortingFunction) {
      if (!sortingFunction || this.head == null || this.head == this.tail) return; // base case
      // Find last node of first half
      let fastIter = this.iterator();
      fastIter.next();
      let half;
      for (half of this.iterator()) {
         if (fastIter.next().done || fastIter.next().done) break;
      }
      // Split list into two halves
      let right = this.splice(half.next, this.tail);
      let left = this; // ...what remains after the splice.

      // Recursively sort the two shorter lists
      left.mergeSort(sortingFunction);
      right.mergeSort(sortingFunction);
      // Make sure the "left" sublist is the one with a head value that comes before the other head value
      if (sortingFunction(this.list[right.head].value, this.list[left.head].value)) [left, right] = [right, left];
      // Merge the two sorted lists
      let tailIndex = left.head;
      let otherIndex = right.head;
      for (let currIndex = this.list[tailIndex].next; currIndex != null || otherIndex != null; currIndex = this.list[tailIndex].next) {
         if (currIndex == null || otherIndex != null && sortingFunction(this.list[otherIndex].value, this.list[currIndex].value)) {
            this.list[tailIndex].next = otherIndex;
            this.list[otherIndex].previous = tailIndex;
            tailIndex = otherIndex;
            otherIndex = currIndex;
         } else {
            tailIndex = currIndex;
         }
      }
      this.head = left.head;
      this.tail = tailIndex;
   }
   
   print() { // I adapted this a bit to get more condense output and add a call to printInOrder
      console.log(JSON.stringify(this.list));
      console.log("Head:",this.head,"\nTail:",this.tail, "\nDefault is ascending order.");
      this.printInOrder();
   }
   
   printInOrder() { // I adapted this to use your nice iterator()
      console.log(...Array.from(this.iterator(), node => node.value));
   }
}


const linked = new LinkedList();

linked.insert(100);
linked.insert(30);
linked.insert(50);
linked.insert(400);
linked.insert(10);
linked.insert(200);
linked.insert(-90);

console.log("When each node is sorted when it is inserted:")

linked.print();

linked.mergeSort((a, b) => {
  return a > b;
});

console.log("Now, when re-sorted:");

linked.print();

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

2 Comments

Thank you, this works perfectly, and you went to greater lengths than I asked, as well as explain everything extremely well, which is why this is the best answer I can think of (which I have personally seen on this site) so far.
Note I initially just answered to the question about the problem in your code. I now added a merge sort alternative which runs in O(nlogn). I did not add a variant that uses the native array sort, since pilchard has provided that already. Note that iterators are not so fast in JS, but I like the coding pattern.
1

Here's a quick and dirty solution leveraging Array#sort()

const list = [
  { value: { num: 1, str: 'a' }, next: 3, prev: 2 },
  { value: { num: 2, str: 'e' }, next: 2, prev: null },
  { value: { num: 3, str: 'v' }, next: 0, prev: 1 },
  { value: { num: 4, str: 'g' }, next: null, prev: 0 }];
let head = 1, tail = 3;

function sort(sortingFunction) {
  const sorted = list
    .map((node, idx) => ({ node, idx }))
    .sort(({ node: { value: a } }, { node: { value: b } }) => sortingFunction(a, b));

  for (const [i, { node, idx }] of sorted.entries()) {
    if (!i) {
      node.prev = null;
      head = idx;
    } else node.prev = sorted[i - 1].idx;

    if (i === sorted.length - 1) {
      node.next = null;
      tail = idx;
    } else node.next = sorted[i + 1].idx;

  }
}

const log_list = () => { let node = list[head]; while (node !== undefined) { console.log(node.value); node = list[node.next]; } };

// initial order logging
console.log('initial - ', 'head: ', head, ' tail: ', tail);
log_list();
console.log('list order: ', ...list.map((o) => o.value.num));

// sort descending
sort((a, b) => b.num - a.num);

// after sort logging
console.log('\nsorted descending - ', 'head: ', head, ' tail: ', tail);
log_list();
console.log('list order: ', ...list.map((o) => o.value.num));

// sort alphabetic
sort((a, b) => a.str.localeCompare(b.str));

// after sort logging
console.log('\nsorted alphabetic - ', 'head: ', head, ' tail: ', tail);
log_list();
console.log('list order: ', ...list.map((o) => o.value.num));
.as-console-wrapper { max-height: 100% !important; top: 0; }

The above gives you the advantage of the optimized sort provided by the javascript engine (timsort in V8), but if you want to implement your own sort you may as well sort within the list, rather than sorting the array that holds it.

Your requirement of keeping nodes in order in the array by value makes this harder, because when swapping you'll need to remember to update neighboring nodes to the nodes being compared to keep the list contiguous.

Here's a quick bubble sort that traverses the list from head to tail until no swaps are occur. It accepts a callback that should return >0 to sort b before a.

const list = [
  { value: { num: 1, str: 'a' }, next: 3, prev: 2 },
  { value: { num: 2, str: 'e' }, next: 2, prev: null },
  { value: { num: 3, str: 'v' }, next: 0, prev: 1 },
  { value: { num: 4, str: 'g' }, next: null, prev: 0 }];
let head = 1, tail = 3;

function sort(sortingFunction) {
  let flag = true, node = list[head];
  while (flag) {
    flag = false;
    while (node.next !== null) {
      if (sortingFunction(node.value, list[node.next].value) > 0) {
        const prev_node = list[node.prev];
        const _node = list[node.next];
        const next_node = list[_node.next];

        const temp = node.prev;

        if (prev_node) prev_node.next = node.next;
        node.prev = node.next;
        node.next = _node.next;

        if (next_node) next_node.prev = _node.prev;
        _node.next = _node.prev;
        _node.prev = temp;

        if (_node.prev === null) head = node.prev;
        if (node.next === null) tail = _node.next;

        flag = true;
      } else {
        node = list[node.next];
      }
    }
    node = list[head];
  }
}

const log_list = () => { let node = list[head]; while (node !== undefined) { console.log(node.value); node = list[node.next]; } };

// // initial order logging
console.log('initial - ', 'head: ', head, ' tail: ', tail);
log_list();
console.log('list order: ', ...list.map((o) => o.value.num));

// sort numeric
sort((a, b) => b.num - a.num);

// after sort logging
console.log('\nsorted descending - ', 'head: ', head, ' tail: ', tail);
log_list();
console.log('list order: ', ...list.map((o) => o.value.num));

// sort str
sort((a, b) => a.str.localeCompare(b.str));

// after sort logging
console.log('\nsorted alphabetic - ', 'head: ', head, ' tail: ', tail);
log_list();
console.log('list order: ', ...list.map((o) => o.value.num));
.as-console-wrapper { max-height: 100% !important; top: 0; }

5 Comments

The second block works really well, thank you.
no worries, trincots explanation is definitely more thorough and more aligned with your code, even though we covered the same ground :)
Though I just read through trincots again and because of the way it's implemented using nested loops of a defined length it has a fixed complexity of O(n^2) (even if the list is already sorted) where as my second example has a worst case of O(n^2) but will exit as soon no swaps are registered.
After some tests, your answer is significantly faster. Trincot's explanation is very thorough and helpful, but your answer has better logic. This makes both answers just as good, so I don't know what to do with the checkmark.
It's fine to leave it on trincot's, I just wanted to point out the differences.

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.