简体   繁体   中英

sort an array by relationship in javascript

I want to sort an array.

The items in the array have relationships.

eg. list[5] should be before list[9] but after list[3]


The expected value in the sample is just for testing. It does not really exist.


Here's a sample array with the relationships and an expected index.

var list = [{
  id: '0001',
  before: '0002',
  expected: 0
}, {
  id: '0002',
  before: '0007',
  after: '0001',
  expected: 4
}, {
  id: '0003',
  before: '0006',
  after: '0001',
  expected: 2
}, {
  id: '0004',
  after: '0007',
  expected: 11
}, {
  id: '0005',
  before: '0003',
  after: '0001',
  expected: 1
}, {
  id: '0006',
  before: '0002',
  after: '0001',
  expected: 3
}, {
  id: '0007',
  before: '00010',
  after: '0002',
  expected: 5
}, {
  id: '0008',
  before: '00012',
  after: '0007',
  expected: 9
}, {
  id: '0009',
  before: '0011',
  after: '0001',
  expected: 7
}, {
  id: '0010',
  before: '0009',
  after: '0007',
  expected: 6
}, {
  id: '0011',
  before: '0008',
  after: '0001',
  expected: 8
}, {
  id: '0012',
  before: '0004',
  after: '0010',
  expected: 10
}];

Expanding on Jan Turoň: here is an example of how your problem (finding a total order in a directed acyclic graph) is unsolvable:

var list = [{ id:A, before:B }, // "First" in total order
            { id:B, after:A }, // "Last" in total order
            { id:C, after:A, before:B },
            { id:D, after:A, before:B }];

There is no total ordering between C and D : you could call them equal, but what if instead of D you have a list D0 -> D1 -> D2 ?

Depending on your type of problem this can be solved by preprocessing: reducing a path of nodes of degree 2 to a single node and calling parallel paths through nodes of degree 2 identical (also to be reduced to a single node). At the end of such a preprocessing you're left with a tree - which in your case should be a list / path (or a single node, since you reduce paths of vertices of degree 2).

Note that the information "before" and "after" are redundant: you only really require one of them. The statement "A is before B" is equivalent to "B is after A" and your acyclic graph only needs to reflect either the direction "before" or "after". What you're then looking for is a path through the graph containing all nodes (since they are directed "before" or "after" you automatically get a path from first to last in order - if such a path exists):

// First build the adjacency list for "X before Y"
var befores = { };
for(var i = 0; i < count; ++i)
    befores[list[i].id] = null;
function insert(before, after) {
    if(!before || !after)
        return;
    befores[before] = { next:befores[before], id:after };
}
for(var i = 0; i < count; ++i) {
    var item = list[i];
    insert(item.after, item.id); // "X after Y" -> "Y before X"
    insert(item.id, item.before);
}

// build complete the graph as a lookup table
// id is "before" all elements in lookup[id]
var lookup = { };
for(var i = 0; i < count; ++i) {
    var id = list[i].id;
    var beforeList = [id];
    var beforeSet = { };
    beforeSet[id] = 1;
    // use "A before B" and "B before C" to gain "A before C"
    for(var j = 0; j < beforeList.length; ++j) {
        for(var item = befores[beforeList[j]]; item != null; item = item.next) {
            if(!beforeSet[item.id]) {
                beforeList.push(item.id);
                beforeSet[item.id] = 1;
            }
        }
    }
    // for our comparison we don't care if id is in beforeSet
    lookup[id] = beforeSet;
    // slice beforeList to get only the elements after id here:
    //beforeList = beforeList.slice(1, beforeList.length);
}

// Now sort using the following
// a) if rhs is present in "before"-set of lhs then lhs < rhs
// b) if rhs is not present then rhs < lhs
// c) there is information missing from our graph if a) and b) for lhs analogous lead to a different conclusion!
list.sort(function(lhs, rhs) {
    if(!lhs.after || !rhs.before) return -1;
    if(!lhs.before || !rhs.after) return 1;
    if(lhs.id === rhs.id) return 0;
    // different ids guaranteed, doesn't matter if lookup[id] contains id itself
    var result = lookup[lhs.id][rhs.id] ? -1 : 1;
    // expect reversing lhs and rhs to get inverse result
    var expected = lookup[rhs.id][lhs.id] ? 1 : -1;
    if(result != expected) {
        // alert: there is no adjacency information between lhs and rhs!
    }
    return result;
});

Test it out yourself:

 var list = [{ id: '0001', before: '0002', expected: 0}, { id: '0002', before: '0007', after: '0001', expected: 4}, { id: '0003', before: '0006', after: '0001', expected: 2}, { id: '0004', after: '0007', expected: 11}, { id: '0005', before: '0003', after: '0001', expected: 1}, { id: '0006', before: '0002', after: '0001', expected: 3}, { id: '0007', before: '0010', after: '0002', expected: 5}, { id: '0008', before: '0012', after: '0007', expected: 9}, { id: '0009', before: '0011', after: '0001', expected: 7}, { id: '0010', before: '0009', after: '0007', expected: 6}, { id: '0011', before: '0008', after: '0001', expected: 8}, { id: '0012', before: '0004', after: '0010', expected: 10 }]; // re-used variable var count = list.length; var out = document.getElementById("out"); function toHTMLItem(item) { var result = item.expected + " ("; if(item.after) result += item.after + " &lt; "; result += "<b>" + item.id + "</b>"; if(item.before) result += " &lt; " + item.before; result += ")"; return result; } function toHTMLList(list) { var result = "<p>"; for(var i = 0; i < count; ++i) { result += toHTMLItem(list[i]) + "<br>"; } result += "</p>"; return result; } // out.innerHTML += toHTMLList(list); var befores = { }; for(var i = 0; i < count; ++i) befores[list[i].id] = null; function insert(before, after) { if(!before || !after) return; befores[before] = { next:befores[before], id:after }; } for(var i = 0; i < count; ++i) { var item = list[i]; insert(item.after, item.id); insert(item.id, item.before); } function toHTMLTable(table, list) { var result = "<p>"; var count = list.length; for(var i = 0; i < count; ++i) { var id = list[i].id; result += id + " < "; for(var item = table[id]; item != null; item = item.next) { result += item.id + ", "; } result += "o<br>"; } result += "</p>"; return result; } // out.innerHTML += toHTMLTable(befores, list); // next build a lookup-table of a completed adjacency list var lookup = { }; for(var i = 0; i < count; ++i) { var id = list[i].id; var beforeList = [id]; var beforeSet = { }; beforeSet[id] = 1; // use "A before B" and "B before C" to gain "A before C" for(var j = 0; j < beforeList.length; ++j) { for(var item = befores[beforeList[j]]; item != null; item = item.next) { if(!beforeSet[item.id]) { beforeList.push(item.id); beforeSet[item.id] = 1; } } } beforeList = beforeList.slice(1, beforeList.length); beforeList.sort(); lookup[id] = beforeSet; } function toHTMLLookup(lookup, list) { var result = "<p>"; for(var i = 0, imax = list.length; i < imax; ++i) { var id = list[i].id; var bs = lookup[id]; result += id + " < "; for(var j = 0, jmax = imax; j < jmax; ++j) { if(j == i) continue; if(bs[list[j].id]) result += list[j].id + ", "; } result += "o<br>"; } result += "</p>"; return result; } // out.innerHTML += toHTMLLookup(lookup, list); // Search function in befores: // a) if rhs is present in union of befores set lhs < rhs // b) if rhs is not present in union of befores set rhs < lhs list.sort((function() { var enableAlert = true; return function(lhs, rhs) { if(!lhs.after || !rhs.before) return -1; if(!lhs.before || !rhs.after) return 1; if(lhs.id === rhs.id) return 0; // different ids guaranteed, doesn't matter if lookup[id] contains id itself var result = lookup[lhs.id][rhs.id] ? -1 : 1; // expect reversing lhs and rhs to get inverse result var expected = lookup[rhs.id][lhs.id] ? 1 : -1; if(enableAlert && result != expected) { // restrict to a single alert per execution enableAlert = false; out.innerHTML += "<p><b>ALERT</b> unresolved adjacency between " + lhs.id + " and " + rhs.id + "!</p>"; } return result; }; })()); // out.innerHTML += toHTMLList(list); var error = count; for(var i = 0; i < error; ++i) { if(list[i].expected != i) error = i; } if(error < count) { out.innerHTML += "<h2>error!</h2><p>list[" + error + "] contains " + toHTMLItem(list[i]) + "</p>"; } else { out.innerHTML += "<h2>success!</h2>"; } // Finally print the output out.innerHTML += toHTMLList(list); 
 <div id="out"/> 

For the general topic of this question consider topological sorting .

Partial answer, too long for a comment. You probably figured this out, I just hope it helps someone to find the solution.

  1. The solution can be impossible or one of many
  2. If there is oriented cycle in "before" or "after" traversing, there is no solution (@BeyelerStudios' credit)
  3. The first one doesn't have "after", the last one doesn't have "before"
  4. The second one has to have the first one id in "after", it can be set
  5. The set can be sorted recursively
  6. How to determine the position of the remaining elements I couldn't figure out.

This function can be useful in in-place sort: move an item in array:

Object.defineProperty(Array.prototype,"move",{
  value: function(from,to) {
    var x = this.splice(from,1);
    this.splice(to,0,x[0]);
  }
});

var list = ['a','b','c','d','e'];
// move the index 1 (b) to position 3 (after d)
list.move(1,3); // acdbe

Good luck.

If I understand this right, expected is the index you'd like the element to be? In that case, you can use .sort() with a custom sorting function.

list.sort(function (a, b) {
  var ae = a.expected,
      be = b.expected;

  if (ae > be) return 1;
  if (ae < be) return -1;
  if (ae === be) return 0;
});

(You can make that block shorter if you'd like at the sacrifice of readability.)

What you want is a custom sorting function, as mentioned by Mike. I think what you really want is this:

list.sort(function(a, b) {
  if (/* a before b, b after a */) {
    return -1;
  } else if (/* b before a, a after b */) {
    return 1;
  } 
  return 0;
});

Basically, what you return in a custom sort function determines the relationship that the two arguments have with one another. Returning -1 means that the first item is before, +1 means that the second is before, and 0 means that there is no relationship available.

For your "a after b, b before a" criteria, perhaps try a.id === b.before|| b.id === a.after a.id === b.before|| b.id === a.after , with the reverse for b after a.

This may or may not return as expected depending on your exact list - if you have a lot of missing or conflicting data, the sort might end up not returning exactly as you expect.

In general, I would avoid having both "before" and "after" information - you should only need one or the other, and having both can introduce even more conflicts than just having a single relation.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM