前端學習 資料結構與演算法 快速入門 系列 —— 陣列

彭加李發表於2021-07-28

陣列資料結構

陣列是最簡單的資料結構。

幾乎所有程式語言都原始支援陣列。

陣列儲存一系列同一種資料型別的值。雖然 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 庫在處理陣列方面很有用,裡面定義了很多有關陣列的方法供我們使用。

相關文章