前端学习 数据结构与算法 快速入门 系列 —— 数组

其他章节请看:

前端学习 数据结构与算法 快速入门 系列

数组数据结构

数组是最简单的数据结构。

几乎所有编程语言都原始支持数组。

数组存储一系列同一种数据类型的值。虽然 javascript 中的数组能保存不同类型的值,但我们还是遵循最佳实践,因为大多数语言都没这个能力。

:本篇文章不会介绍如何实现一个数组,更多的是有关数组的功能和特性,这对我们后续编写自己的算法非常有用。

为什么使用数组

假如有这样一个需求,记录上周每天的开销。可以这么做:

let day1 = 110
let day2 = 120
let day3 = 130
let day4 = 140
let day5 = 150
let day6 = 160
let day7 = 170

这肯定不是最好的方案。按照这个方案,如果要记录上月每天的开销,岂不是要声明 30 来个变量!幸好,我们可以使用数组来解决:

let week = [];
week[0] = 110
week[1] = 120
week[2] = 130
week[3] = 140
week[4] = 150
week[5] = 160
week[6] = 170

创建和初始化数组

可以通过 Array 构造函数创建数组:

let a = new Array() // []
let b = new Array(3) // [ <3 empty items> ] - 指定长度的数组
let c = new Array('a', 'b') // ['a', 'b']

Tip:也可以使用下文介绍的 Array.of() 方法来创建数组

使用 new 创建数组不是最好的方式,你还可以使用中括号:

let a = []
let b = ['a', 'b']

我们来看一个例子:求斐波那契数列的前20个数。已知斐波那契数列中前两项是1,从第散列开始,每一项等于前两项之和。

const result = [1, 1];
for (let i = 2; i <= 20; i++) {
    result[i] = result[i - 2] + result[i - 1]
}
console.log(result);

// [ 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946 ]

添加元素

假如我们有一个数组 numbers:

let numbers = [1, 2, 3, 4, 5]

在数组末尾插入元素

使用 push 方法:

// arr.push(element1, ..., elementN)
numbers.push(6)
numbers.push(7, 8)

还可以把值赋给数组中最后一个空位上:

numbers[numbers.length] = 9;

Tip:javascript 中,数组是一个可以修改的对象。如果添加元素,它会自动增长。而在 C 和 java 等其他语言,我们要决定数组的大小,想要添加元素就要创建一个全新的数组,不能直接往其中添加新的元素。

在数组开头插入元素

希望在数组开头插入一个数,可以使用 unshift 方法:

// arr.unshift(element1, ..., elementN)
numbers.unshift(0)
numbers.unshift(-2, -1)

unshift 的原理其实就是:将数组内的元素统一往后挪,最后将元素放在第一位上。就像这样:

Array.prototype.insertFirst = function (v) {
    for (let i = this.length; i >= 1; i--) {
        this[i] = this[i - 1]
    }
    this[0] = v
}
numbers.insertFirst(0)

删除元素

从元素末尾删除元素

pop() 方法从数组中删除最后一个元素,并返回该元素的值。此方法更改数组的长度。

numbers.pop()

Tip:通过 push 和 pop 就可以模拟栈数据结构

从元素开头删除元素

shift() 方法从数组中删除第一个元素,并返回该元素的值。此方法更改数组的长度。

numbers.shift()

Tip:通过 shift 和 push 可以模拟队列数据结构

如果不使用 shift 方法,我们这么做:

Array.prototype.removeFirst = function () {
    const result = this[0]
    // 最后依次循环,i + 1引用了数组中未初始化的一个位置,
    // 在java、C/C+或C#等语言中,可能会抛出异常
    for (let i = 0, length = this.length; i < length; i++) {
        this[i] = this[i + 1]
    }
    
    this.length--;
    return result;
}

numbers.removeFirst()

Tip:上面代码只是用作示范,真实项目中应该始终使用 shift 方法。

在任意位置添加或删除元素

在任意位置添加或删除元素,可以使用 splice 方法。

array.splice(start[, deleteCount[, item1[, item2[, ...]]]])

此方法的返回值由被删除的元素组成的一个数组

  • 如果只删除了一个元素,则返回只包含一个元素的数组
  • 如果没有删除元素,则返回空数组。
let numbers = [1, 2, 3, 4, 5]

// 删除从索引1开始的2个元素
// [ 1, 4, 5 ]
numbers.splice(1, 2)

// 在索引 1 的位置插入两个数
// deleteCount 是 0 或者负数,则不移除元素
// [ 1, 2, 3, 4, 5 ]
numbers.splice(1, 0, 2, 3)

Tip:对于数组和对象,我们可以使用 delete 运算符删除其中元素,例如 delete numbers[0],数组位置 0 的值会变成 undefined,等同于 numbers[0] = undefined。因此我们应该使用 splice、pop、shift 方法来删除数组元素。

二维和多维数组

比如我要记录前2周每天的开销,可以这样做:

let week1 = [110, 120, 130, 140, 150, 160, 170];
let week2 = [120, 130, 140, 150, 160, 170, 180];

我们可以使用矩阵(二维数组)来存储这些信息:

let week = []
week[0] = [110, 120, 130, 140, 150, 160, 170]
week[1] = [120, 130, 140, 150, 160, 170, 180]

数组内容如下:

  | 0 | 1 | 2 | 3 | 4 | 5 | 6

  • | - | - | - | - |- | - | -
    0 |110|120|130|140|150|160|170
    1 |120|130|140|150|160|170|180

Tip:javascript 只支持一维数组,并不支持矩阵,但我们可以用数组套数组来实现矩阵或任一多维数组

如果想看这个矩阵的输出,可以这样:

week.forEach(item => {
    item.forEach(_item => {
        console.log('_item: ', _item);
    })
})

Tip:在浏览器控制台或通过 node 运行,可以使用 console.table(),它会显示一个更加友好的结果。就像这样:

console.table(week);

// 输出
┌─────────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ (index) │  0  │  1  │  2  │  3  │  4  │  5  │  6  │
├─────────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│    0    │ 110 │ 120 │ 130 │ 140 │ 150 │ 160 │ 170 │
│    1    │ 120 │ 130 │ 140 │ 150 │ 160 │ 170 │ 180 │
└─────────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘

多维数组

假如有一个 3x3x3 的魔方,共27个格子,每个格子都有一个数字,我们可以使用多维数组来表示:

// 数字,递增
let seed = 1;

// 一维
let result = []
for (let i = 0; i < 3; i++) {
    // 二维
    result[i] = []
    for (let j = 0; j < 3; j++) {
        // 三维
        result[i][j] = []
        for (let k = 0; k < 3; k++) {
            result[i][j][k] = seed++;
        }
    }
}

console.log(result);

// 输出
[
    [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
    [[10, 11, 12], [13, 14, 15], [16, 17, 18]],
    [[19, 20, 21], [22, 23, 24], [25, 26, 27]]
]

如果是4维,就再多嵌套一层即可。开发中,很少使用四维数组,二维数组比较常见。

js 中数组方法参考

数组很有趣,也很强大。js 中的数组相对于其他语言,有许多很好用的方法。

一些核心方法有:concat、every、filter、forEach、join、indexOf、lastIndexOf、map、reverse、slice、some、sort、toString、valueOf、find等。

Tip:编写数据结构和算法时,会大量使用这些方法。

数组合并

考虑如下场景:有多个数组,需要合并成一个数组。

我们可以迭代各个数组,然后把每个元素放入最终的数组。幸好有 concat 方法:

var new_array = old_array.concat(value1[, value2[, ...[, valueN]]])
let arr1 = 1;
let arr2 = [2, 3]
let arr3 = [4, 5]
let result = arr3.concat(arr1, arr2)

// result:  [ 4, 5, 1, 2, 3 ]
console.log('result: ', result);

迭代器函数

js 内置了许多数组可用的迭代方法,例如 every、some、map、filter、reduce:

let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

// 偶数
const isEven = v => v % 2 === 0

// false
numbers.every(isEven)

// true
numbers.some(isEven)

// 1 2 3 4 5 6 7 8 9 10
numbers.forEach(v => console.log(v))

// [ false, true, false, true, false, true, false, true, false, true ]
numbers.map(isEven)

// [ 2, 4, 6, 8, 10 ]
numbers.filter(isEven)

// arr.reduce(callback[accumulator, currentValue, currentIndex, array], initialValue)
// 55
numbers.reduce((previous, current) => previous + current)

ECMAScript 6 和数组的新功能

es6 和 es7 新增的数组方法:@@iterator、copyWithin、entries、includes、find、findIndex、from、keys、of、values等等

for...of

for...of语句在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句

for (const item of ['a', 'b', 'c']) {
    console.log(item);
}
// a b c
Array.prototype[@@iterator]()

@@iterator 需要通过 Symbol.iterator 来访问

Tip:Symbol.iterator 为每一个对象定义了默认的迭代器

const arr = ['a', 'b']
let iterator = arr[Symbol.iterator]()
console.log(iterator.next().value); // a
console.log(iterator.next().value); // b
console.log(iterator.next().value); // undefined

Tip:有关迭代器的详细请看[迭代器 (Iterator) 和 生成器 (Generator)][迭代器 (Iterator) 和 生成器 (Generator)]

entries()、keys() 和 values()

这三个方法都是返回迭代器。比如 entries() 方法:

const arr = ['a', 'b']
for (const entry of arr.entries()) {
    console.log(entry);
}
// [ 0, 'a' ]
// [ 1, 'b' ]
from()

Array.from() 方法从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。

// 将类数组转为数组
let obj = { 0: 'a', 1: 'b', length: 2 }
console.log(Array.from(obj)) // [ 'a', 'b' ]
// 浅拷贝
let arr = [1, 2]
let arr2 = Array.from(arr)
console.log(arr2) // [ 1, 2 ]

还可以传入一个回调函数:

let arr = [2, 9, 3, 8]
console.log(Array.from(arr, x => x < 5)) // [ true, false, true, false ]
console.log(arr.map(x => x < 5))         // [ true, false, true, false ]
Array.of()

Array.of 方法根据传入的参数创建一个新数组。

console.log(Array.of(1)) // [ 1 ]
console.log(Array.of("1",1)) // [ '1', 1 ]

也可以使用该方法复制已有的数组:

let arr = [2, 3]
let arr2 = Array.of(...arr)
console.log(arr2) // [ 2, 3 ]
fill()

fill() 方法用一个固定值填充一个数组中从起始索引到终止索引内的全部元素。不包括终止索引。

arr.fill(value[, start[, end]])
const array1 = [1, 2, 3, 4];

// fill with 0 from position 2 until position 4
console.log(array1.fill(0, 2, 4)) // [1, 2, 0, 0]

// fill with 5 from position 1
console.log(array1.fill(5, 1)) // [1, 5, 5, 5]

console.log(array1.fill(6)) // [ 6, 6, 6, 6 ]

创建和初始化的时候,fill 方法很好用,就像这样:

let arr = new Array(5).fill(1)
console.log(arr) // [ 1, 1, 1, 1, 1 ]
copyWithin()

copyWithin() 方法浅复制数组的一部分到同一数组中的另一个位置,并返回它,不会改变原数组的长度。

const array1 = ['a', 'b', 'c', 'd', 'e'];

// copy to index 0 the element at index 3
console.log(array1.copyWithin(0, 3, 4)) // ["d", "b", "c", "d", "e"]

// copy to index 1 all elements from index 3 to the end
console.log(array1.copyWithin(1, 3)) // ["d", "d", "e", "d", "e"]

排序元素

通过本系列,我们能学会如何编写最常用的搜索和排序算法。而 js 也提供了排序和搜索的方法。

reverse() 方法将数组中元素的位置颠倒,并返回该数组:

let array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
array.reverse()
// [ 15, 14, 13, 12, 11, 10, 9,  8,  7,  6,  5,  4, 3,  2,  1 ]
console.log(array)

然后,我们使用 sort() 方法,情况看起来不太对:

// array 已经是 15, 14, 13...
array.sort()
// [ 1, 10, 11, 12, 13, 14, 15,  2,  3,  4,  5,  6, 7,  8,  9 ]
console.log(array)

这是因为 sort 方法对数组排序,默认使用字符串比较。

我们可以传入自己的比较函数:

array.sort((a, b) => a - b)
// [ 1,  2,  3,  4,  5,  6, 7,  8,  9, 10, 11, 12, 13, 14, 15 ]
console.log(array)
自定义排序

我们可以给对象类型的数组进行排序:

let people = [
    { name: 'a', age: 18 },
    { name: 'b', age: 28 },
    { name: 'c', age: 8 }, // es2017 允许存在尾逗号
]

function compareFunction(a, b) {
    if (a.age < b.age) {
        return -1
    }

    if (a.age > b.age) {
        return 1
    }

    return 0
}
people.sort(compareFunction)
console.log(people)

/* 
[
    { name: 'c', age: 8 },
    { name: 'a', age: 18 },
    { name: 'b', age: 28 }
] 
*/
字符串排序

请看这段代码:

let arr = ['A', 'a', 'B', 'b']
arr.sort()
console.log(arr) // [ 'A', 'B', 'a', 'b' ]

为什么 B 在 a 的前面?因为 js 在做字符串比较的时候,是根据字符对应的 ASCII 值来比较的。

let arr = ['A', 'a', 'B', 'b']
arr.forEach(v => {
    console.log(v.codePointAt(0));
})
// 65 97 66 98

B对应66,a对应97,所以 B 排在 a 前面。

如果传入给 sort 传入一个忽略大小写的比较函数:

let arr = ['B', 'b', 'A', 'a']

function compareFunction(a, b) {
    if (a.toLowerCase() < b.toLowerCase()) {
        return -1
    }
    if (a.toLowerCase() > b.toLowerCase()) {
        return 1
    }
    return 0
}
arr.sort(compareFunction)
console.log(arr) // [ 'A', 'a', 'B', 'b' ]

如果希望小写字母排在前面,可以使用 localeCompare() 方法:

('a').localeCompare('A') // -1
('A').localeCompare('a') // 1
('A').localeCompare('b') // -1
let arr = ['A', 'a', 'B', 'b']
arr.sort((a, b) => a.localeCompare(b))
console.log(arr) // [ 'a', 'A', 'b', 'B' ]

搜索

indexOf() 和 lastIndexOf()

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

const beasts = ['ant', 'bison', 'camel', 'duck', 'bison'];

console.log(beasts.indexOf('bison')) // 1

// start from index 2
console.log(beasts.indexOf('bison', 2)) // 4

console.log(beasts.indexOf('giraffe')) // -1

lastIndexOf() 方法返回指定元素(也即有效的 JavaScript 值或变量)在数组中的最后一个的索引,如果不存在则返回 -1。从数组的后面向前查找,从 fromIndex 处逆向查找。

const animals = ['Dodo', 'Tiger', 'Penguin', 'Dodo'];

console.log(animals.lastIndexOf('Dodo')) // 3

console.log(animals.lastIndexOf('Tiger')) // 1
includes()

includes() 方法用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回false。

console.log([1, 2, 3].includes(1)) // true
// 从索引 1 开始找
console.log([1, 2, 3].includes(1,1)) // false
find() 和 findIndex()

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

arr.find(callback[, thisArg])
const array1 = [5, 12, 8, 130, 44]
const found = array1.find(element => element > 10)

console.log(found) // 12

findIndex() 与 find() 唯一区别:前者返回索引(没有找到则返回 -1),后者返回找到的值。

输出数组为字符串

Array.prototype.toString() 返回一个字符串,表示指定的数组及其元素。

console.log([1, 2, 3].toString()) // 1,2,3

Tip: 可以通过 toString 实现数组扁平化

console.log([1, [2, [3, [4]]]].toString()) // 1,2,3,4

如果想用一个不同的分隔符(比如-)把元素分开,可以使用 join 方法:

console.log([1, 2, 3].join('-')) // 1-2-3

Tip: lodash 库在处理数组方面很有用,里面定义了很多有关数组的方法供我们使用。

其他章节请看:

前端学习 数据结构与算法 快速入门 系列

posted @ 2021-07-28 16:21  彭加李  阅读(363)  评论(0编辑  收藏  举报