18

Say I have an array already with these articles, ordered by lowest to highest price:

[
  { title: "Article 3", price: 1.49 },
  { title: "Article 1", price: 3.00 },
  { title: "Article 4", price: 5.99 },
  { title: "Article 2", price: 19.99 }
]

Basically I would like to push another article into the array at the correct position (by price). How can I do so?

The new article I'm pushing may have the following properties:

{ title: "Article 5", price: 12.00 }

I'm expecting the article to appear at index 3 (between article 4 and 2).

UPDATE (SOLUTION)

I created a prototype method using @klutt's answer with binary search algorithm:

Array.prototype.pushSorted = function(el, compareFn) {
  this.splice((function(arr) {
    var m = 0;
    var n = arr.length - 1;

    while(m <= n) {
      var k = (n + m) >> 1;
      var cmp = compareFn(el, arr[k]);

      if(cmp > 0) m = k + 1;
        else if(cmp < 0) n = k - 1;
        else return k;
    }

    return -m - 1;
  })(this), 0, el);

  return this.length;
};

const sortByPrice = (a, b) => a.price > b.price;
theArray.pushSorted({ title: "Article 5", price: 12.00 }, sortByPrice);
Nordling Art
  • 857
  • 2
  • 9
  • 19
  • It's a bit unclear what you mean with "without knowing current elements". I guess you have access to the list, right? So you can read it? – klutt Mar 22 '17 at 08:51
  • I can read it if it was a manually arranged array. However, let's say I'm fetching results from an API. I don't know what items I receive. The sorting must be done without me (as a developer) knowing which items are pushed. – Nordling Art Mar 22 '17 at 10:20
  • But you are getting an array from the API, and after that you can read that array without problem? You also get an element from the API, which you also can read without any problem? Your quest is to create a new, sorted array with that element inserted at the right place. Have I understood your question? – klutt Mar 22 '17 at 10:26
  • Well, to be more detailed: First I fetch, say 4 articles from the API. Next, one make a "pull to refresh" on the list that displays the array. This pull refresh fetches, say 1 new article. This article can have any price, so it needs to be pushed it to the array at its sorted index. – Nordling Art Mar 22 '17 at 10:32
  • So, in short you want a function that takes an array and an element and returns an array with that element inserted in the right place? – klutt Mar 22 '17 at 10:55
  • Then what difference does it make whether we personally know the elements or not. We or our program can know/read them is all that matters, doesn't it? – Anurag Awasthi Mar 22 '17 at 10:55
  • @klutt - I read you answer with binary search. That's exactly what I was looking for! Will mark it as answer :) – Nordling Art Mar 22 '17 at 10:57
  • Thank you. But for the future, I just want you to know that saying you don't know the arrays current elements is extremely confusing, because in all relevant ways of looking at the problem you DO know the elements. You just fetched the array and you can view it however you want. – klutt Mar 22 '17 at 10:59
  • @klutt - Yeah, my bad. Sorry for confusing! – Nordling Art Mar 22 '17 at 11:01
  • @NordlingArt No worries, but please remove that part from the question for future readers. – klutt Mar 22 '17 at 11:18
  • @klutt - Done :) – Nordling Art Mar 22 '17 at 11:27
  • 2
    @NordlingArt FYI, the splice function is unable to insert el to the last of array, when there is a need. shoud return m instead of -m-1? – Vincent Nov 10 '20 at 15:25
  • 1
    @NordlingArt this prototype solution DOES NOT WORK! with a simple list of integers i end up with a very unsorted array. still trying to figure out what the problem is... – jpro Nov 14 '20 at 02:52
  • The solution here does not work for me, but another answer on this question does: https://stackoverflow.com/a/56588334/559415 – Peter Jan 17 '22 at 06:13

8 Answers8

10

If it is a very long list, you don't want to sort the list everytime. A sorting with floats is at best O(n*log n). If you do a linear search you will have O(n) instead.

function search(a, v) {
    if(a[0]['price'] > v['price']) {
        return 0;
    }
    var i=1;
    while (i<a.length && !(a[i]['price'] > v['price'] && a[i-1]['price'] <= v['price'])) {
        i=i+1;
        }
    return i;
}

myArray.splice(search(myArray, newItem), 0, newItem)

However, if the list is VERY long it would be a good idea to do a binary search instead of a linear search. That would take it down to O(log n). A binary search is pretty simple. You start in the middle. If the element is larger than what you're searching for, apply the same algorithm to the upper half of the list, otherwise to lower part. It's easy to find code examples of binary search on the web.

Here is an example:

function binarySearch(ar, el, compare_fn) {
    if (el.price < ar[0].price)
        return 0;
    if (el.price > ar[ar.length-1].price)
        return ar.length;
    var m = 0;
    var n = ar.length - 1;
    while (m <= n) {
        var k = (n + m) >> 1;
        var cmp = compare_fn(el, ar[k]);
        if (cmp > 0) {
            m = k + 1;
        } else if(cmp < 0) {
            n = k - 1;
        } else {
            return k;
        }
    }
    return -m - 1;
}

function comp(a, b) {
    return a['price']>b['price']
}

myArray.splice(binarySearch(myArray, element, comp), 0, element)

(Stolen from here Binary Search in Javascript)

But to wrap it up. Adding the element and then sorting is usually a bad idea. Best case is that it does not matter, but since you know that the list is sorted, why not do at least a linear search?

If the lists are small, this does not matter, but if the lists have millions of elements the difference will be quite noticeable.

EDIT:

I made a quick and primitive benchmark.

            10,000   100,000  1000,000   10,000,000  
Sort            80       900     13000          N/A
Linear           2         2        25         5000
Binary           2         2         5           21

I measured the time it took to run the three algorithms on four different sizes. I did not want to wait for the sorting to end on ten million elements. Therefore the N/A. Time is in milliseconds. Note that the benchmark were very primitive, but it gives an idea about how much it affects when sizes grow.

Community
  • 1
  • 1
klutt
  • 30,332
  • 17
  • 55
  • 95
  • That is it, thanks! So which one would you recommend? Linear or binary search in such case as this scenario? – Nordling Art Mar 22 '17 at 11:02
  • In practice, if I just needed to get the job done and I knew that the lists were small and that performance never would be an issue I would go for the shortest and most readable code I could produce, which would probably be appending the element and then sort it. Recommending one for a general purpose is impossible. The raw performance facts is that binary search is fastest, linear comes between and sorting afterwards is slowest. If the lists have less than 10 elements I can pretty much guarantee that it does not matter. – klutt Mar 22 '17 at 11:09
  • Got it. Well, in practice this is for an app feed that displays articles. The amount of articles can vary but think of the list as an Instagram list, which means there could be an extreme amount of items. Hence me going with the binary search method. – Nordling Art Mar 22 '17 at 11:12
  • I just realized this doesn't actually work. Could you please [check this fiddle](https://jsfiddle.net/bob23trg/) and see what's wrong? If the push value is the lowest, it appears at an incorrect index. Same with highest. Only if it's in between other values, the index order is correct. – Nordling Art Mar 22 '17 at 13:49
  • @NordlingArt Done. Also added some benchmark comparision. – klutt Mar 22 '17 at 14:03
  • Still not entirely correct. First, the compare function returns a boolean value, while in the binarySearch function you handle it as integer. Second, you locked the binarySearch function to look for a certain object property (`price`), meaning other types of elements (or ones that lack the `price` property) aren't compatible. I found a similar solution elsewhere that is a little bit different than yours. Mind checking it out? https://jsfiddle.net/bob23trg/4/ – Nordling Art Mar 22 '17 at 14:19
  • First: From what I can see it works anyway, no matter the Boolean issue. Second: You wanted a way to insert on right position based on price, so no I cannot find the right place if the element does not have it. – klutt Mar 22 '17 at 14:43
  • The second return (`-m - 1`) of this binary search function is not meant to be used as an index/insertion point, so this will not work as expected. The returned number is meant to provide 1) information about whether the target value exists, and 2) a number from which the index can be calculated. You can fix this by either changing the return value to just `m`, or calculate it afterwards as seen in Pablote's answer. Here's the same thing in java: [Java's Binary Search API — SitePoint](https://www.sitepoint.com/javas-binary-search-api-tutorial/#binarysearchforinsertion) – Moon Moon Jun 26 '20 at 12:05
  • @MoonMoon This answer is three years old, and it was a very long time since I last touched JS. I turned it into a community wiki. Feel free to edit any way you want. – klutt Jun 26 '20 at 12:29
3

To avoid sorting your array each time, you can iterate through your array until an element with greater price is found, and then use array.splice(index, 0, element) to insert your element into the correct position:

var array = [
  { title: "Article 3", price: 1.49 },
  { title: "Article 1", price: 3.00 },
  { title: "Article 4", price: 5.99 },
  { title: "Article 2", price: 19.99 }
]

function insertSorted (array, element, comparator) {
  for (var i = 0; i < array.length && comparator(array[i], element) < 0; i++) {}
  array.splice(i, 0, element)
}

function compareByPrice (a, b) { return a.price - b.price }

insertSorted(array, { title: "Article 5", price: 12.00 }, compareByPrice)

console.log(array)
gyre
  • 16,369
  • 3
  • 37
  • 47
2

The Update (Solution) still doesn't work great. Items that should go either to the start or end get placed in the wrong position, see this revised solution based on the same code:

Array.prototype.pushSorted = function(el, compareFn) {
  let index = (function(arr) {
    var m = 0;
    var n = arr.length - 1;

    while(m <= n) {
      var k = (n + m) >> 1;
      var cmp = compareFn(el, arr[k]);

      if(cmp > 0) m = k + 1;
        else if(cmp < 0) n = k - 1;
        else {
          console.log(`m=${m} k=${k}`)
          return k;
        };
    }


    return -m - 1;
  })(this);

  if (index >= 0)
    this.splice(index, 0, el);
  else if (index < 0)
    this.splice((index * -1) - 1, 0, el);

  return this.length;
};

Sample usage:

a = [1, 2, 3, 4, 5]
a.pushSorted(2.5, (a,b) => a - b);
a.pushSorted(8, (a,b) => a - b);
a.pushSorted(0.5, (a,b) => a - b);
Pablote
  • 4,745
  • 9
  • 39
  • 46
1

I implemented this in Angular, by tweaking the code. Someone could find it useful.

Array.prototype.sortedInsert = sortedInsert;

interface Array<T> {
    sortedInsert(element: T, comparator: any): number;
}

/**
 *  This function takes an element, and a comparator function.
 *  It returns position of the inserted record.
 */

function sortedInsert<T>(element: T, comparatorFn: any): number {
    const insertionIndex: number = findInsertionIndex(this, element, comparatorFn);
    this.splice(insertionIndex, 0, element);
    return insertionIndex;
}

function findInsertionIndex<T>(arr: Array<T>, element: T, comparatorFn: any): number {
    if (arr.length === 0) {
        return 0;
    }

    let low: number = 0;
    let high: number = arr.length - 1;

    while (low <= high) {
        const mid: number = Math.floor(low + (high - low) / 2);
        const comparison: number = comparatorFn(element, arr[mid]);
        if (comparison === 0) {
            return mid;
        } else if (comparison < 0) {
            high = mid - 1;
        } else {
            low = mid + 1;
        }
    }
    return low;
}

To use this, you'll have to import this file in app.module.ts.
Usage example:

const arr = [];
const comp = (a: number, b: number) => a - b;
arr.sortedInsert(2, comp);
arr.sortedInsert(1, comp);
arr.sortedInsert(3, comp);
console.log(arr);
Lakshmikant Deshpande
  • 826
  • 1
  • 12
  • 30
1

Here's my current typescript implementation:

enum SortDirection {
  Ascending,
  Descending
}

const propertyComparer = <T>(
  a: T,
  b: T,
  property: keyof T,
  direction: SortDirection
): number => {
  if (a[property] < b[property]) {
    return direction === SortDirection.Ascending ? 1 : -1;
  }
  if (a[property] > b[property]) {
    return direction === SortDirection.Ascending ? -1 : 1;
  }
  return 0;
};

const findAndInsertByProperty = <T>(
  arr: T[],
  item: T,
  property: keyof T,
  direction: SortDirection
): number => {
  let start = 0;
  let end = arr.length;

  while (start < end) {
    const mid = (start + end) >> 1;
    const result = propertyComparer<T>(item, arr[mid], property, direction);

    if (result === 0) {
      return mid;
    } else if (result < 0) {
      end = mid;
    } else {
      start = mid + 1;
    }
  }

  return end;
};

const myArray = [
  { title: "Article 3", price: 1.49 },
  { title: "Article 1", price: 3.00 },
  { title: "Article 4", price: 5.99 },
  { title: "Article 2", price: 19.99 }
];
const value = { title: "Article 5", price: 12.00 };
const valueLowest = { title: "Article 6", price: 0.49 };
const valueHighest = { title: "Article 7", price: 20.00 };

myArray.splice(findAndInsertByProperty(myArray, value, 'price', SortDirection.Descending), 0, value);
myArray.splice(findAndInsertByProperty(myArray, valueLowest, 'price', SortDirection.Descending), 0, valueLowest);
myArray.splice(findAndInsertByProperty(myArray, valueHighest, 'price', SortDirection.Descending), 0, valueHighest);

console.log(myArray);
Icehunter
  • 241
  • 2
  • 5
0

Just push your item

var myArray = [
  { title: "Article 3", price: 1.49 },
  { title: "Article 1", price: 3.00 },
  { title: "Article 4", price: 5.99 },
  { title: "Article 2", price: 19.99 }
]

myArray.push({ title: "Article 5", price: 12.00 })

And sort you array :

myArray.sort(function(a, b) {
    return parseFloat(a.price) - parseFloat(b.price);
});
Lionel B
  • 1,172
  • 2
  • 9
  • 24
  • 1
    I'm downvoting this, becuase it does not take any advantage of the fact that the array is sorted from the beginnning. – klutt Mar 22 '17 at 10:02
-1

Consider my solution as follow;

var articles=[
  { title: "Article 3", price: 1.49 },
  { title: "Article 1", price: 3.00 },
  { title: "Article 4", price: 5.99 },
  { title: "Article 2", price: 19.99 }
]

Now write a function in this way;

function sortByKey(array, key) {
    return array.sort(function(a, b) {
        var x = a[key]; var y = b[key];
       return ((x < y) ? -1 : ((x > y) ? 1 : 0));
   });
}

Now insert an element;

articles.push({ title: "Article 5", price: 12.00 });

And finally sort it;

articles=sortByKey(articles, 'price');
KOUSIK MANDAL
  • 2,002
  • 1
  • 21
  • 46
  • 2
    I'm downvoting this, becuase it does not take any advantage of the fact that the array is sorted from the beginnning. – klutt Mar 22 '17 at 10:02
-2

Not sure if this is optimal, but you can push it into another (duplicated) array, then sort it, and then check for its index in order to push it at the correct index position:

Array.prototype.pushSorted = function(value, compareFunction) {
  const arrCopy = this.map(element => element);

  arrCopy.push(value);
  arrCopy.sort(compareFunction);
  this.splice(arrCopy.indexOf(value), 0, value);

  return this.length;
};

First we create a copy of the array instance. Then push the element into this array copy. Then sort the array copy. Then push (via splice method) the element into the real array while checking for its index inside the array copy. We can then return with the array length (just like a normal push).

Example:

const sortByPrice = (a, b) => a > b ? 1 : -1;
const newArticle = { title: "Article 5", price: 12.00 };

theArray.pushSorted(newArticle, sortByPrice);
Nordling Art
  • 857
  • 2
  • 9
  • 19
  • I'm downvoting this, becuase it does not take any advantage of the fact that the array is sorted from the beginnning. – klutt Mar 22 '17 at 10:03
  • Take a look at my and gyres answer to see what's wrong. If you know that the array is sorted, you should not need to sort it. You only search for the right place and insert the element. – klutt Mar 22 '17 at 10:18
  • It does work, but in the question it is specifically stated that the original array is sorted. It is only a question about optimization, that much is true. But since you say that the list is sorted, a good answer should take advantage of that, especially since it's not even complicated to do so in this case. – klutt Mar 22 '17 at 10:21
  • Modifying an object's prototype is not recommended. https://stackoverflow.com/questions/6223449/why-is-it-frowned-upon-to-modify-javascript-objects-prototypes – robertmain Sep 03 '20 at 15:46