检查评定数组中是否带有有个别成分,再谈Java

时间:2020-01-24 08:51来源:亚洲城ca88唯一官方网站
时间: 2019-07-08阅读: 187标签: 性能 JavaScript的数组去重是一个老生常谈的话题了。随便搜一搜就能找到非常多不同版本的解法。 1.indexOf() Array.prototype.indexOf():返回在数组中可以找到一个

时间: 2019-07-08阅读: 187标签: 性能

JavaScript的数组去重是一个老生常谈的话题了。随便搜一搜就能找到非常多不同版本的解法。

1.indexOf()

Array.prototype.indexOf():返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回 -1

['foo', 'baz'].indexOf('foo') // 0

我确信有很多开发人员坚持使用基本的全局对象:数字,字符串,对象,数组和布尔值。对于许多用例,这些都是需要的。 但是如果想让你的代码尽可能快速和可扩展,那么这些基本类型并不总是足够好。

细想一下,这样一个看似简单的需求,如果要做到完备,涉及的知识和需要注意的地方着实不少。定义重复

2.find()

返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined

['foo', 'baz'].find(i => i == 'foo') // foo

在本文中,我们将讨论JS 中Set对象如何让代码更快— 特别扩展性方便。Array和Set工作方式存在大量的交叉。但是使用Set会比Array在代码运行速度更有优势。

要去重,首先得定义,什么叫作“重复”,即具体到代码而言,两个数据在什么情况下可以算是相等的。这并不是一个很容易的问题。

3.findIndex()

返回数组中满足提供的测试函数的第一个元素的索引。否则返回 -1

[0, 1].findIndex(i => i == 1) // 1

Set 有何不同

对于原始值而言,我们很容易想到1和1是相等的,'1'和'1'也是相等的。那么,1和'1'是相等的么?

4.includes() (es7,为什么还添加了includes())

Array.prototype.includes() 是用于检测数组中是否包含某个元素

[0, 1].includes(1) // true
['foo', 'bar'].includes('baz') // false

ECMAScript 7 中新增了用于检测数组中是否包含某个元素 Array.prototype.includes() API,想到了 Array 其实有很多相关 API 可以检测到是否包含某个元素,比如 Array.prototype.indexOf,于是好奇为什么要实现这样一个 "看起来功能有点重复的 API"。

最根本的区别是数组是一个索引集合,这说明数组中的数据值按索引排序。

如果这个问题还好说,只要回答“是”或者“不是”即可。那么下面这些情况就没那么容易了。NaN

Array.prototype.includes 前身

早前的 Array.prototype.includes 的提案名为 Array.prototype.contains,但由于有很多网站自行 hack 了 Array.prototype.contains(其实主要是因为 MooTools 导致的),看起来就跟上面的方法类似。

Array.prototype.includes 可以枚举
JavaScript 中所有原生提供的方法属性都是 不可枚举的( enumerable ) 的,

Object.getOwnPropertyDescriptor(Array.prototype, 'indexOf')
// output { writable: true, enumerable: false, configurable: true, value: ƒ() }

给对象赋值,是不会改变原属性的属性描述符,我们可以给 Array.prototype.indexOf 重新赋值,之后获取它的属性描述符,会发现 indexOf 仍是不可枚举的:

Array.prototype.indexOf = () => { return -1 }
Object.getOwnPropertyDescriptor(Array.prototype, 'indexOf')
// output { writable: true, enumerable: false, configurable: true, value: ƒ() }

而这些网站自行 hack 的 contains() 是可以被枚举的,也就是可以通过 for..in 读出来。

如果规范实现 contains(),会导致 contains() 无法被 for..in 读出来,而之前自行 hack 的 contains() 是可以被读出来的,所以会出现代码没变动,但是在新规范推出后会产生 bug 的情况。

在 Array.prototype.contains 初稿阶段,考虑到新的规范不能让世界上许多现有的网站出问题,所以改名成了 Array.prototype.includes。
虽然我们可以使用 indexOf() 来模拟 includes() 的行为,但是 indexOf() 在语义上无法清晰的描述这个场景。

const arr = [A, B, C, D];console.log(arr.indexOf(A)); // Result: 0console.log(arr.indexOf(C)); // Result: 2

初看NaN时,很容易把它当成和null、undefined一样的独立数据类型。但其实,它是数字类型。// number

起源

includes() 是明确的判断 "是否包含该项",而 indexOf() 是 "查找数组中第一次出现对应元素的索引是什么,再针对返回的索引进一步处理逻辑",例如下面的代码:

// indexOf
if (arr.indexOf(1) !=-1) { 
   // do something
}

// includes
if (arr.includes(1)) { 
   // do something
}

相比之下,set是一个键的集合。set不使用索引,而是使用键对数据排序。set中的元素按插入顺序是可迭代的,它不能包含任何重复的数据。换句话说,set中的每一项都必须是惟一的。

console.log(typeof NaN);

为什么叫做 includes 而不是 has

has 是用于 key 的,而 includes 是检测 value 的:

let foo = new Map()
foo.set('name', 'linkFly')
foo.has('name') // true

主要的好处是什么

根据规范,比较运算中只要有一个值为NaN,则比较结果为false,所以会有下面这些看起来略蛋疼的结论:// 全都是false

判断相等的办法

Array.prototype.includes 底层使用了 SameValueZero() 进行元素比较。

目前有4种判断相等的办法:
抽象标准相等比较:实现接口是 == 运算符
严格相等比较:实现接口是 ===运算符,Array.prototype.indexOf 就是使用这种比较
SameValueZero():没有直接暴露的接口,内部实现接口是 Map与 Set(不区分 0、-0)

const foo = new Map()
foo.set(0, '0') // Map(1) {0 => "0"}
foo.set('0', 'zero') // Map(2) {0 => "0", "0" => "zero"}
foo.get(0) // 0
foo.get('0') // zero

所以 Array.prototype.includes 也不区分 0 和 -0 ,当然也可以检测 NaN:

[-0].includes( 0) // true
[NaN].includes(NaN) // true
[NaN].indexOf(NaN) // -1

SameValue():实现接口是 Object.is()

NaN === NaN // false
Object.is(NaN, NaN) // true

-0 ===  0 // true
Object.is(-0,  0) // false

参考和引用:https://segmentfault.com/a/1190000010911972

set相对于数组有几个优势,特别是在运行时间方面:

0 < NaN;

查看元素:使用indexOf()或includes()检查数组中的项是否存在是比较慢的。删除元素:在Set中,可以根据每项的的value来删除该项。在数组中,等价的方法是使用基于元素的索引的splice()。与前一点一样,依赖于索引的速度很慢。保存 NaN:不能使用indexOf()或includes()来查找值NaN,而Set可以保存此值。删除重复项:Set对象只存储惟一的值,如果不想有重复项存在,相对于数组的一个显著优势,因为数组需要额外的代码来处理重复。时间复杂度?

0 > NaN;

数组用来搜索元素的方法时间复杂度为0(N)。换句话说,运行时间的增长速度与数据大小的增长速度相同。

0 == NaN;

相比之下,Set用于搜索、删除和插入元素的方法的时间复杂度都只有O(1),这意味着数据的大小实际上与这些方法的运行时间无关。

0 === NaN;

Set 究竟有多快?

以最后一个表达式0 === NaN为例,在规范中有明确规定:

虽然运行时间可能会有很大差异,具体取决于所使用的系统,所提供数据的大小以及其他变量,但我希望我的测试结果能够让你真实地了解Set的速度。 我将分享三个简单的测试和我得到的结果。

  1. If Type is Number, then

准备测试

a. If x is NaN, return false.

在运行任何测试之前,创建一个数组和一个 Set,每个数组和 Set 都有100万个元素。为了简单起见,我从0开始,一直数到999999。

b. If y is NaN, return false.

let arr = [], set = new Set(), n = 1000000;for (let i = 0; i  n; i  ) { arr.push(i); set.add(i);}

c. If x is the same Number value as y, return true.

测试1:查找元素

d. If x is 0 and y is −0, return true.

我们搜索数字123123

e. If x is −0 and y is 0, return true.

let result;console.time('Array'); result = arr.indexOf(123123) !== -1; console.timeEnd('Array');console.time('Set'); result = set.has(123123); console.timeEnd('Set');

f. Return false.

Array: 0.173msSet: 0.023ms

这意味着任何涉及到NaN的情况都不能简单地使用比较运算来判定是否相等。比较科学的方法只能是使用isNaN():var a = NaN;

Set速度快了7.54倍

var b = NaN;

测试2:添加元素

// true

console.time('Array'); arr.push(n);console.timeEnd('Array');console.time('Set'); set.add(n);console.timeEnd('Set');

console.log && isNaN;

Array: 0.018msSet: 0.003ms

原始值和包装对象

Set速度快了6.73倍

看完NaN是不是头都大了。好了,我们来轻松一下,看一看原始值和包装对象这一对冤家。

测试3:删除元素

如果你研究过'a'.trim()这样的代码的话,不知道是否产生过这样的疑问:'a'明明是一个原始值,它为什么可以直接调用.trim()方法呢?当然,很可能你已经知道答案:因为JS在执行这样的代码的时候会对原始值做一次包装,让'a'变成一个字符串对象,然后执行这个对象的方法,执行完之后再把这个包装对象脱掉。可以用下面的代码来理解:// 'a'.trim();

最后,删除一个元素,由于数组没有内置方法,首先先创建一个辅助函数:

var tmp = new String;

const deleteFromArr = (arr, item) = { let index = arr.indexOf(item); return index !== -1  arr.splice(index, 1);};

tmp.trim();

这是测试的代码:

这段代码只是辅助我们理解的。但包装对象这个概念在JS中却是真实存在的。var a = new String;

console.time('Array'); deleteFromArr(arr, n);console.timeEnd('Array');console.time('Set'); set.delete(n);console.timeEnd('Set');

var b = 'b';

Array: 1.122msSet: 0.015ms

a即是一个包装对象,它和b一样,代表一个字符串。它们都可以使用字符串的各种方法,也可以参与字符串运算。

Set速度快了74.13倍

但他们有一个关键的区别:类型不同!typeof a; // object

总的来说,我们可以看到,使用Set极大地改善运行时间。再来看看一些Set有用的实际例子。

typeof b; // string

案例1:从数组中删除重复的值

在做字符串比较的时候,类型的不同会导致结果有一些出乎意料:var a1 = 'a';

如果想快速地从数组中删除重复的值,可以将其转换为一个Set。这是迄今为止过滤惟一值最简洁的方法:

var a2 = new String;

const duplicateCollection = ['A', 'B', 'B', 'C', 'D', 'B', 'C'];// 将数组转换为 Setlet uniqueCollection = new Set(duplicateCollection);console.log(uniqueCollection) // Result: Set(4) {"A", "B", "C", "D"}// 值保存在数组中let uniqueCollection = [...new Set(duplicateCollection)];console.log(uniqueCollection) // Result: ["A", "B", "C", "D"]

var a3 = new String;

案例2:谷歌面试问题

a1 == a2; // true

问题:

a1 == a3; // true

给定一个整数无序数组和变量sum,如果存在数组中任意两项和使等于sum的值,则返回true。否则,返回false。例如,数组[3,5,1,4]和sum = 9,函数应该返回true,因为4 5 = 9。

a2 == a3; // false

解答

a1 === a2; // false

解决这个问题的一个很好的方法是遍历数组,创建Set保存相对差值。

a1 === a3; // false

当我们遇到3时,我们可以把6加到Set中, 因为我们知道我们需要找到9的和。然后,每当我们接触到数组中的新值时,我们可以检查它是否在Set中。当遇到5时,在 Set 加上4。最后,当我们最终遇到4时,可以在Set中找到它,就返回true。

a2 === a3; // false

const findSum = (arr, val) = { let searchValues = new Set(); searchValues.add(val - arr[0]); for (let i = 1, length = arr.length; i  length; i  ) { let searchVal = val - arr[i]; if (searchValues.has(arr[i])) { return true; } else { searchValues.add(searchVal); } }; return false;};

同样是表示字符串a的变量,在使用严格比较时竟然不是相等的,在直觉上这是一件比较难接受的事情,在各种开发场景下,也非常容易忽略这些细节。对象和对象

简洁的版本:

在涉及比较的时候,还会碰到对象。具体而言,大致可以分为三种情况:纯对象、实例对象、其它类型的对象。

const findSum = (arr, sum) = arr.some((set = n = set.has(n) || !set.add(sum - n))(new Set));

纯对象纯对象(plain object)具体指什么并不是非常明确,为减少不必要的争议,下文中使用纯对象指代由字面量生成的、成员中不含函数和日期、正则表达式等类型的对象。

因为Set.prototype.has()的时间复杂度仅为O(1),所以使用 Set 来代替数组,最终使整个解决方案的线性运行时为O(N)。

如果直接拿两个对象进行比较,不管是==还是===,毫无疑问都是不相等的。但是在实际使用时,这样的规则是否一定满足我们的需求?举个例子,我们的应用中有两个配置项:// 原来有两个属性

如果使用Array.prototype.indexOf()或Array.prototype.includes(),它们的时间复杂度都为 O(N),则总运行时间将为O(N²),慢得多!

// var prop1 = 1;

原文来自:@bretcameron/how-to-make-your-code-faster-using-javascript-sets-b432457a4a77

// var prop2 = 2;

// 重构代码时两个属性被放到同一个对象中

var config = {

prop1: 1,

prop2: 2

};

假设在某些场景下,我们需要比较两次运行的配置项是否相同。在重构前,我们分别比较两次运行的prop1和prop2即可。而在重构后,我们可能需要比较config对象所代表的配置项是否一致。在这样的场景下,直接用==或者===来比较对象,得到的并不是我们期望的结果。

在这样的场景下,我们可能需要自定义一些方法来处理对象的比较。常见的可能是通过JSON.stringify()对对象进行序列化之后再比较字符串,当然这个过程并非完全可靠,只是一个思路。如果你觉得这个场景是无中生有的话,可以再回想一下断言库,同样是基于对象成员,判断结果是否和预期相符。

实例对象

实例对象主要指通过构造函数生成的对象。这样的对象和纯对象一样,直接比较都是不等的,但也会碰到需要判断是否是同一对象的情况。一般而言,因为这种对象有比较复杂的内部结构(甚至有一部分数据在原型上),无法直接从外部比较是否相等。比较靠谱的判断方法是由构造函数来提供静态方法或者实例方法来判断是否相等。var a = Klass();

var b = Klass();

Klass.isEqual;

其它对象

其它对象主要指数组、日期、正则表达式等这类在Object基础上派生出来的对象。这类对象各有各的特殊性,一般需要根据场景来构造判断方法,决定两个对象是否相等。

比如,日期对象,可能需要通过Date.prototype.getTime()方法获取时间戳来判断是否表示同一时刻。正则表达式可能需要通过toString()方法获取到原始字面量来判断是否是相同的正则表达式。==和===

在一些文章中,看到某一些数组去重的方法,在判断元素是否相等时,使用的是==比较运算符。众所周知,这个运算符在比较前会先查看元素类型,当类型不一致时会做隐式类型转换。这其实是一种非常不严谨的做法。因为无法区分在做隐匿类型转换后值一样的元素,例如0、''、false、null、undefined等。

同时,还有可能出现一些只能黑人问号的结果,例如:[] == ![]; //true

Array.prototype.indexOf()

在一些版本的去重中,用到了Array.prototype.indexOf()方法:

图片 1

既然==和===在元素相等的比较中是有巨大差别的,那么indexOf的情况又如何呢?大部分的文章都没有提及这点,于是只好求助规范。通过规范,我们知道了indexOf()使用的是严格比较,也就是===。再次强调:按照前文所述,===不能处理NaN的相等性判断。Array.prototype.includes()

Array.prototype.includes()是ES2016中新增的方法,用于判断数组中是否包含某个元素,所以上面使用indexOf()方法的第二个版本可以改写成如下版本:

图片 2

那么,你猜猜,includes()又是用什么方法来比较的呢?如果想当然的话,会觉得肯定跟indexOf()一样喽。但是,程序员的世界里最怕想当然。翻一翻规范,发现它其实是使用的另一种比较方法,叫作“SameValueZero”比较。

  1. If Type is different from Type, return false.

  2. If Type is Number, then

a. If x is NaN and y is NaN, return true.

b. If x is 0 and y is -0, return true.

c. If x is -0 and y is 0, return true.

d. If x is the same Number value as y, return true.

e. Return false.

  1. Return SameValueNonNumber.

注意2.a,如果x和y都是NaN,则返回true!也就是includes()是可以正确判断是否包含了NaN的。我们写一段代码验证一下:var arr = [1, 2, NaN];

arr.indexOf; // -1

arr.includes; // true

可以看到indexOf()和includes()对待NaN的行为是完全不一样的。一些方案

从上面的一大段文字中,我们可以看到,要判断两个元素是否相等并不是一件简单的事情。在了解了这个背景后,我们来看一些前面没有涉及到的去重方案。遍历

双重遍历是最容易想到的去重方案:

图片 3

双重遍历还有一个优化版本,但是原理和复杂度几乎完全一样:

图片 4

这种方案没什么大问题,用于去重的比较部分也是自己编写实现(arr[i] === arr[j]),所以相等性可以自己针对上文说到的各种情况加以特殊处理。唯一比较受诟病的是使用了双重循环,时间复杂度比较高,性能一般。使用对象key来去重

图片 5

这种方法是利用了对象的key不可以重复的特性来进行去重。但由于对象key只能为字符串,因此这种去重方法有许多局限性:

  1. 无法区分隐式类型转换成字符串后一样的值,比如1和'1'

  2. 无法处理复杂数据类型,比如对象(因为对象作为key会变成[object Object])

3. 特殊数据,比如'__proto__'会挂掉,因为tmp对象的__proto__属性无法被重写

对于第一点,有人提出可以为对象的key增加一个类型,或者将类型放到对象的value中来解决:

图片 6

该方案也同时解决第三个问题。

而第二个问题,如果像上文所说,在允许对对象进行自定义的比较规则,也可以将对象序列化之后作为key来使用。这里为简单起见,使用JSON.stringify()进行序列化。

图片 7

Map Key

可以看到,使用对象key来处理数组去重的问题,其实是一件比较麻烦的事情,处理不好很容易导致结果不正确。而这些问题的根本原因就是因为key在使用时有限制。

那么,能不能有一种key使用没有限制的对象呢?答案是——真的有!那就是ES2015中的Map。Map是一种新的数据类型,可以把它想象成key类型没有限制的对象。此外,它的存取使用单独的get接口。var tmp = new Map();

tmp.set;

tmp.get; // 1

tmp.set;

tmp.get; // 2

tmp.set;

tmp.get; // 3

tmp.set(undefined, 4);

tmp.get(undefined); // 4

tmp.set;

tmp.get; // 5

var arr = [], obj = {};

tmp.set;

tmp.get; // 6

tmp.set;

tmp.get; // 7

由于Map使用单独的接口来存取数据,所以不用担心key会和内置属性重名(如上文提到的__proto__)。使用Map改写一下我们的去重方法:

图片 8

Set

既然都用到了ES2015,数组这件事情不能再简单一点么?当然可以。

除了Map以外,ES2015还引入了一种叫作Set的数据类型。顾名思义,Set就是集合的意思,它不允许重复元素出现,这一点和数学中对集合的定义还是比较像的。var s = new Set();

s.add;

s.add;

s.add;

s.add(undefined);

s.add;

s.add;

s.add;

s.add;

如果你重复添加同一个元素的话,Set中只会存在一个。包括NaN也是这样。于是我们想到,这么好的特性,要是能和数组互相转换,不就可以去重了吗?function unique{

var set = new Set;

return Array.from;

}

我们讨论了这么久的事情,居然两行代码搞定了,简直不可思议。

然而,不要只顾着高兴了。有一句话是这么说的“不要因为走得太远而忘了为什么出发”。我们为什么要为数组去重呢?因为我们想得到不重复的元素列表。而既然已经有Set了,我们为什么还要舍近求远,使用数组呢?是不是在需要去重的情况下,直接使用Set就解决问题了?这个问题值得思考。小结

最后,用一个测试用例总结一下文中出现的各种去重方法:

图片 9

测试中没有定义对象的比较方法,因此默认情况下,对象不去重是正确的结果,去重是不正确的结果。

图片 10

最后的最后:任何脱离场景谈技术都是妄谈,本文也一样。去重这道题,没有正确答案,请根据场景选择合适的去重方法。

编辑:亚洲城ca88唯一官方网站 本文来源:检查评定数组中是否带有有个别成分,再谈Java

关键词: 亚洲城ca88