【问题标题】:Removing equivalent but unique objects from a Javascript array从 Javascript 数组中删除等效但唯一的对象
【发布时间】:2016-09-20 23:04:04
【问题描述】:

我有一个类似于以下的对象数组:

var routeArr = [
    {start: 1, end: 2},
    {start: 1, end: 3},
    {start: 1, end: 4},
    {start: 2, end: 1},
    {start: 3, end: 1},
    {start: 4, end: 1}
];

这些对象代表线的起点和终点,因此,{start: 1, end: 2}{start: 2, end: 1} 代表同一条线。

我正在尝试从数组中删除所有重复的行,但找不到有效或优雅的方法来执行此操作。我尝试过嵌套循环,但有人告诉我这是不好的做法(而且我的实现出现错误,而且很丑陋)。

for(var i = 0, numRoutes = routeArr.length; i < numRoutes; i++) {
    var primaryRoute = routeArr[i];

    for(var j = 0; j < numRoutes; j++) {
        var secondRoute = routeArr[j];

        if(primaryRoute.start === secondRoute.end && primaryRoute.end === secondRoute.start) {
            routeArr.splice(j, 1);
            continue;
        }
    }
}

谁能给点建议?

【问题讨论】:

  • 执行此操作的正常方法是:首先对其进行排序(在您的情况下,如果 (end > start),您应该反转开始和结束)。然后重复的行将彼此完全相同。然后只需循环删除重复的
  • 当你删除数组的一个元素时,永远不要运行从 0 到长度的循环。这是不安全的,因为删除后您必须调整索引。更好地按降序运行循环,即从长度 1 到 0,这将在您删除数组元素并且永远不会返回具有更大索引的元素的情况下起作用。此外,您的 if 语句仅检查一个相同行的条件,您还必须使用 or 语句添加其他检查。

标签: javascript arrays javascript-objects


【解决方案1】:

在 javascript 中创建一个对象/映射并保留唯一对象的索引,将“min(start,end):max(start,end)”作为键存储,将索引作为值存储。这是您的问题在 javascript 中的实现:

// your initial array
var routeArr = [
    {start: 1, end: 2},
    {start: 1, end: 3},
    {start: 1, end: 4},
    {start: 2, end: 1},
    {start: 3, end: 1},
    {start: 4, end: 1}
];

// map where we will store key => value where key is a joined start,end of your array's item and value is an item index 
var keyToRouteIndexMap = {};

for (var i in routeArr){
    // calculating min and max from start and end to understand {start:1, end:2} and {start:2, end:1} object as duplicates
    var min = Math.min(routeArr[i].start,routeArr[i].end);
    var max = Math.max(routeArr[i].start,routeArr[i].end);
    // unique key 
    var key = min+':'+max;
    if (!keyToRouteIndexMap.hasOwnProperty(key)){
        keyToRouteIndexMap[key] = i;
    }
}

for(var key in keyToRouteIndexMap){
    if(keyToRouteIndexMap.hasOwnProperty(key)){
        console.log(routeArr[keyToRouteIndexMap[key]]);
    }
}

【讨论】:

  • 请注意,如果您使用的是 ES6,则可以改用 the Set object
  • 还有一些用于模仿哈希集的 javascript 实现:github.com/timdown/jshashtable
  • @Hamms:Set 对象有什么帮助?它只比较对象引用。
  • @Vahan Simonyan:您可能应该在遍历对象键时检查 hasOwnProperty()。
  • @le_m 您显然不能将对象本身用作 Set 的元素,但是您可以像此示例一样构造一个键,并将其简单地放入一个集合中而不是重新使用作为集合的对象。
【解决方案2】:

这里是从javascript数组中删除重复值问题的一般解决方案:

/**
 * Takes an input array and returns a new array without identical elements.
 *
 * @param {array} input
 * @callback id   identity function returning identical values for identical elements
 */
function uniquify(input, id) {
    result = [];
    map = {};
    for (var i = 0, length = input.length; i < length; ++i) {
        var element = input[i], identity = id(element);
        if (!map.hasOwnProperty(identity)) {
            result.push(element);
            map[identity] = true;
        }
    }
    return result;
}

应用于你给定的routeArr:

var routeArr = [
    {start: 1, end: 2},
    {start: 1, end: 3},
    {start: 1, end: 4},
    {start: 2, end: 1},
    {start: 3, end: 1},
    {start: 4, end: 1}
];

routeArr = uniquify(routeArr, function(route) {
    return route.start < route.end ? '' + route.start + ':' + route.end : '' + route.end + ':' + route.start;
});

【讨论】:

  • 我猜回调函数会将 {start: 2, end: 3} 和 {start: 6, end: 1} 映射到同一个位置。
【解决方案3】:

你可以这样做。我想这非常快,因为根本没有搜索。一个 Array.prototype.reduce() 操作同时构造哈希表(查找表)和缩减对象。然后映射对象键以获得结果。在这里;

var routeArr = [
    {start: 1, end: 2},
    {start: 1, end: 3},
    {start: 1, end: 4},
    {start: 2, end: 1},
    {start: 3, end: 1},
    {start: 4, end: 1}
],

reduced = routeArr.reduce((p,c) => {!(p[c.start+"-"+c.end] || p[c.end+"-"+c.start]) && (p[c.start+"-"+c.end] = c);
                                     return p;},{}),
 result = Object.keys(reduced).map(e => reduced[e]);
console.log(result);

再想一想,我消除了多余的 Object.keys() 部分。现在这只不过是一个 Array.prototype.reduce() 传递,所有这些都在 O(n) 中完成。我想这可能与性能有关。看看吧。

var routeArr = [
    {start: 1, end: 2},
    {start: 1, end: 3},
    {start: 1, end: 4},
    {start: 2, end: 1},
    {start: 3, end: 1},
    {start: 4, end: 1}
],

     reduced = routeArr.reduce((p,c) => {!(p[c.start+"-"+c.end]  ||
                                           p[c.end+"-"+c.start]) &&
                                          (p[c.start+"-"+c.end] = true,
                                           p.result.push(c));
                                           return p;
                                        },{"result":[]});
console.log(reduced.result);

好吧,是的,我不得不同意它看起来有点神秘,但它非常简单。

  • 我们在这里使用带有初始值的Array.prototype.reduce() 方法。这是我们的初始值{"result":[]}。在减少 routeArr 数组时,我们的初始元素现在是一个对象,它具有一个名为 result 的属性和一个空数组的值。
  • reduce 提供了一个匿名回调函数,它接受两个参数 (p,c) p 代表先前,c 代表当前。所以在第一次运行中p 是我们的初始化对象,我的意思是这个{"result":[]}c 是我们调用reduce 的数组(routeArr) 索引0 处的项目。所以在第一轮c{start: 1, end: 2}
  • 在每一轮开始时,我们检查p 对象是否包含一个属性,该属性表示两个顺序中的当前元素值。所以检查就像这样!(p[c.start+"-"+c.end] || p[c.end+"-"+c.start]),用人类的话来说意味着“你真的没有像 c.start-c.end 或 c.end-c.start 这样的字符串属性”.. 例如在第一轮检查就像“你真的没有像“1-2”或“2-1”这样的字符串属性吗?如果有(假)我们什么也不做,但如果没有,我们执行以下操作;
  • &amp;&amp; (p[c.start+"-"+c.end] = true, p.result.push(c)); return p;。好的,第一个&amp;&amp; 将括号中的两条指令与前一条指令的条件联系起来,以评估为真。在a &amp;&amp; b 指令中,如果a 评估为真,JS 引擎只会评估b。所以你明白了。再次用人类的术语来说,这就是发生的事情。 “是不是你没有像“1-2”或“2-1”这样的字符串属性变为真,我们创建了一个值为真的属性“1-2”。所以在接下来的回合中,如果我们遇到一个1-2 或 2-1 我们什么都不做。然后我们将这个当前对象推送到同一个对象的结果属性 (p.result) 以成为它所有副本或孪生的唯一代表。然后我们返回@ 987654344@ 以健康地继续减少循环。

我希望它很清楚。

【讨论】:

  • 美观实用的解决方案。现在我真的很想看看与非功能性方法的性能比较。
  • 我认为现在您在代码缩小方面做得有点过头了。选择自我记录的变量名称而不是将分配进行比较可能有助于 OP 更好地理解您的好解决方案:)
  • @le_m 是的,我想你可能是对的。我将在下面做一些解释。对我来说,这看起来像一首诗。 :)
【解决方案4】:

我已经写了下面的函数来巧妙地做到这一点

var routeArr = [{
  start: 1,
  end: 2
}, {
  start: 1,
  end: 3
}, {
  start: 1,
  end: 5
}, {
  start: 2,
  end: 1
}, {
  start: 3,
  end: 1
}, {
  start: 4,
  end: 1
}];

routeArr.IsDuplicate = function(obj) {
    var i = this.length;
    var count = 0 
    while (i--) {
        if ((this[i].start === obj.start && this[i].end === obj.end ) || (this[i].start === obj.end && this[i].end === obj.start) ) {
            count++;
        }
    }
    return count>1;
}

for(var i = routeArr.length-1; i--;){
    if (routeArr.IsDuplicate(routeArr[i])) routeArr.splice(i, 1);
}

【讨论】:

  • 这在操作上是低效的。它需要您多次评估每一对。例如,如果将 routeApp 长度增加到 19,则会进行 189 次评估。地图似乎是一种更清洁的方法。在 Java 中,对于这种类型的实现,Hashmap 将是一个很好的数据结构。 github.com/timdown/jshashtable 是 javascript 中的 hashset 实现。
  • javascript 对象已经提供了一个'hashmap'实现,唯一的问题是键不能是对象 - 所以你需要一个'hash'函数,或者通过对象到散列的一般映射(你的链接)效率低下,因为您需要遍历原型链或用户提供的性能更高的函数。
【解决方案5】:

您的嵌套循环方法很“丑陋”- 但这不是你的问题。

您的实现错误是由于您的两个 for 循环都假定数组结构在您对其进行变异时不会改变,这导致您跳过数组中的某些项目。

'i' 和 'j' 是“愚蠢”的增量器 - for 循环不是告诉代码在每次迭代时转到数组中的下一项,而是告诉它转到 (array[last_index_i_used+1] - 所以当你拼接一些你正在查看的数组的变化,然后一行中的下一个项目被传递。

我看到了很多花哨的数组方法和 ES6 建议,但我从你的问题中假设你对 JS 还是有点陌生​​,可能需要一些时间来构建基础知识(无意冒犯)。

尝试递归递减函数:

function uniquify(inputArray, ind){
    var checkStart = inputArray[ind].start, checkEnd =inputArray[ind].end
    for (var i=(ind-1);i > -1; --i){
        var thisStart = inputArray[i].start, thisEnd = inputArray[i].end
        if ((thisStart == checkStart || thisStart == checkEnd) && (thisEnd == checkStart || thisEnd == checkEnd)){

            inputArray.splice(i,1)
        }
    }

    --ind
    if (ind > -1){
        uniquify(inputArray,ind)
    }
}
uniquify(routeArr,routeArr.length -1);

我更喜欢嵌套的 for 循环,因为你永远不会比你需要的更频繁地达到相同的值,无论数组的大小如何都能保持性能一致。

但您可能想问自己,定义“routeArr”的东西是否以一种智能的方式在做它正在做的事情 - 充其量,它似乎是在浪费内存和 CPU 以低效的方式存储数据。

【讨论】:

  • 我建议在任何地方都使用分号,即使在 JS 中并不总是需要它们。此外,您的循环索引 i 是一个全局变量,最好使用 var 使其成为本地变量。
  • 您认为我是 JS 新手是正确的,至少在这种规模的项目中是正确的。长话短说,我们正在重写一个旧的 C++ 应用程序以在 iPad 和 Android 上运行。决定将 HTML5 和 Javascript 与 Cordova 一起使用(我一开始支持,但现在有疑虑)。不幸的是,在以前的 C++ 版本中做出了一些设计决策,这迫使我们以非常规的方式处理应用程序数据。我试图找到一个优雅而有效的解决方案。感谢您的回复,我正在尝试。
  • @le_m - 感谢您发现我的循环中缺少 var。编辑修复。
猜你喜欢
  • 2016-11-08
  • 2016-05-19
  • 1970-01-01
  • 1970-01-01
  • 2016-05-12
  • 1970-01-01
  • 2018-11-14
  • 2020-12-07
相关资源
最近更新 更多