【问题标题】:Javascript sorting function incorrectly changes elements' position in arrayJavascript 排序函数错误地改变元素在数组中的位置
【发布时间】:2016-03-25 01:48:08
【问题描述】:

我在我的 AngularJS 应用程序中使用数组排序功能。它使用变量direction 来确定是以升序(direction === -1)还是降序(direction === 1)方式对数据进行排序。

出了什么问题:有时当我进行排序时,数组中应该位于相同位置的元素被返回到不同的位置,而数组中没有任何实际变化。

例如,如果我有:

   var arr = [
       {id: 3, name: 'd'},
       {id: 2, name: 'b'},
       {id: 1, name: 'a'},
       {id: 2, name: 'c'}
   ];

我按“id”对它进行排序,方向为-1,它将返回名称为“a,b,c,d”的名称,正如您所期望的那样。然后我会再次排序(将方向更改为1),它会反转方向。但是,如果我再次对其进行排序(使用direction === -1),它将以“a,c,b,d”的顺序返回。

这是一个简化的例子;实际上,它远比这更难以预测。

我感觉我没有正确使用direction。见下文:

this.sortData = function (data, type, direction) {

    return data.sort(sortFunct);

    function sortFunct(a, b) {
        var numberTypes = ['Thread', 'Job', 'FolderCount', 'MessageCount', 'EmailCount', 'CalendarCount', 'TaskCount', 'OtherCount', 'JobId', 'BatchId', 'ItemsTotal', 'ItemsRemaining', 'ItemsFailed', 'Size', 'progress', 'PercentComplete'];
        var stringTypes = ['Level', 'StatusMessage', 'Author', 'ItemStatus', 'JobStatus', 'SourceMailbox', 'TargetMailbox', 'Subject', 'Folder', 'MessageClass', 'StatusMessage', 'Path', 'Owner1',];

        if (numberTypes.indexOf(type) !== -1) {
            return direction * (a[type] - b[type]);
        } else if (stringTypes.indexOf(type) !== -1) {
            if (!a[type]) {
                return 1;
            } else if (!b[type]) {
                return -1;
            } else {
                return a[type].localeCompare(b[type]) * direction;
            }
        } else if (type === 'DiscoveryDate' || type === 'ReceivedDate' || type === 'Timestamp') {
            if (a[type] > b[type]) {
                return direction * 1;
            } else if (a[type] < b[type]) {
                return direction * -1;
            } else {
                return 0;
            }
        } else {
            return direction * (a[type] - b[type]);
        }
    } // End sortFunct
};

【问题讨论】:

    标签: javascript arrays sorting


    【解决方案1】:

    .sort() 的 ECMAScript 规范不要求它是稳定的,因此您的自定义排序函数所说的相同元素(例如返回 0)彼此之间的顺序没有保证,并且在事实上,它们最终的顺序可能会受到它们在排序之前的顺序的影响。

    如果你想要一个一致的排序,那么你永远不需要从你的自定义排序函数中返回0,除非这两个项目完全相同,以至于你永远无法分辨哪个是哪个。

    当主键相同时,您需要测试一个辅助键并返回该比较的结果。如果二级密钥相同,则比较三级密钥等等,只要你值得去。

    这种二次比较可以使排序稳定,使其始终以可预测的顺序出现,无论您之前的顺序是什么。


    我还看到了创建唯一键并将其分配给每个对象并将其用作二级或三级键的排序方案,从而保证如果您关心的键相同,那么您可以比较唯一的键并始终具有一致的稳定排序。


    这是一个方案示例,如果比较字段(本例中的年龄具有相同的值),则保留原始顺序。要知道原始顺序是什么,它会使用标识原始顺序的origOrder 属性标记每个对象。

    var data = [
      {name: "Amy", age: 13},
      {name: "Anne", age: 13},
      {name: "John", age: 11},
      {name: "Jack", age: 12},
      {name: "Ted", age: 11},
      {name: "Alice", age: 12}
    ];
      
    // mark each object with the original order  
    data.forEach(function(item, index) {
      item.origOrder = index;
    });  
      
    data.sort(function(a, b) {
      var diff = a.age - b.age;
      if (diff !== 0) {
        return diff;
      } else {
        return a.origOrder - b.origOrder;
      }
    });  
    
    // show results in snippet
    document.write(JSON.stringify(data));  

    使用 ES6 中的 Map 对象,我们也可以通过创建用于解析关系并将其存储在 Map 对象中的临时索引来实现此操作,而无需触及数组中的原始对象。可以这样工作:

        var data = [
          {name: "Amy", age: 13},
          {name: "Anne", age: 13},
          {name: "John", age: 11},
          {name: "Jack", age: 12},
          {name: "Ted", age: 11},
          {name: "Alice", age: 12}
        ];
        
        // put each object in a temporary map with its original index
        let tempMap = new Map();
        data.forEach((item, index) => {
            tempMap.set(item, index);
        });
          
        data.sort(function(a, b) {
          var diff = a.age - b.age;
          if (diff !== 0) {
            return diff;
          } else {
            return tempMap.get(a) - tempMap.get(b);
          }
        });  
    
        // show results in snippet
        document.write(JSON.stringify(data));  

    通过查看排序结果可以看出,两个相同年龄的条目的相对顺序被保留了,没有以任何方式修改原始对象。

    【讨论】:

    • 在 cmets 中询问了另一个答案,但是是否可以通过 sort 函数在原始数组中返回 abs 索引,比较它并返回 -1 或 @987654330 @ 作为辅助比较?
    • @Jascination - .sort() 不提供回调的原始索引。我的答案的最后一段讨论了为对象数组中排序的每个对象添加唯一键,如果您简单地使唯一键升序,然后将其用作关系的最终仲裁器,则提供了该功能。这将保留相同值的排序键的原始顺序。
    • 好吧,如果data = data.sort(sortFunction),我不能只在sortFunction 中做if(data.indexOf(a) &gt; data.indexOf(b) 而不是做一个额外的循环吗?尝试在这里考虑性能(尽管我只是尝试过,但没有什么不同,所以我可能错了)
    • @Jascination - 你假设一堆不正确的事情,比如数组中没有 dup,元素尚未移动,等等......记住,@987654335 @ 对原始数组进行排序,以便元素在排序过程中移动。除非在排序之前以某种方式将其记录在另一个数据结构中,否则原始位置很快就会丢失。
    • 啊,这很好,没有意识到数据会在排序过程中移动!做一个单独的循环可能是最好的。为您的帮助干杯。
    【解决方案2】:

    ECMAScript 不要求排序是稳定的 (related question)。此外,即使排序是稳定的,您也不能指望代码知道何时应该反转相同的记录(2 和 2)以及何时不反转。为了能够做到这一点,您永远无法从比较器返回0 - 即,如果出现平局,您需要一个辅助键来帮助消除订单的歧义。如果你想不出更好的,可以给每个对象a unique ID

    【讨论】:

    • 有趣,不知道要在这里搜索的正确术语。你会说返回ab的原始索引,然后通过辅助键进行比较会使排序更稳定吗?
    • 不确定您所说的“返回原始索引”是什么意思 - 从哪里返回?我的意思是,拥有一个真正独特的领域是一件大事。如果这是未修改的数组索引,那很好,但必须事先将它放到对象上,以便比较器可以访问它。
    • 我确实有一个唯一的 ID 字段,但我看不出比较它会如何影响排序数组的稳定性。我的意思是数组中元素的原始索引;如果比较 === 0,并且 a 在数组中的原始索引低于 b,则返回 1,否则返回 -1。
    【解决方案3】:

    “sort() 方法将数组的元素就地排序并返回数组。排序不一定稳定。”

    见:

    https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort

    还有:

    https://en.wikipedia.org/wiki/Sorting_algorithm#Stability

    'HTH,

    【讨论】:

      猜你喜欢
      • 2012-09-03
      • 1970-01-01
      • 2015-09-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-03-17
      • 2021-08-19
      • 1970-01-01
      相关资源
      最近更新 更多