為什麼我要放棄javaScript資料結構與演算法(第二章)——陣列

賴同學發表於2018-10-31

第二章 陣列

幾乎所有的程式語言都原生支援陣列型別,因為陣列是最簡單的記憶體資料結構。JavaScript裡也有陣列型別,雖然它的第一個版本並沒有支援陣列。本章將深入學習陣列資料結構和它的能力。

為什麼用陣列

需求:儲存所在城市每個月的平均溫度,可以這麼實現

var averageTemp1 = 43.3;
var averageTemp2 = 53.2;
var averageTemp3 = 14.2;
var averageTemp4 = 42.8;
var averageTemp5 = 14.8;
var averageTemp6 = 78.9;

只是儲存前六個月就用了6個變數,顯然這種方式不適合儲存這類需求。通過陣列可以簡單地實現我們的需求。

var averageTemp = [];
averageTemp[0] = 43.3;
averageTemp[1] = 53.2;
averageTemp[2] = 14.2;
averageTemp[3] = 42.8;
averageTemp[4] = 14.8;
averageTemp[5] = 78.9;

建立和初始化陣列

宣告、建立和初始化陣列的方式很簡單

var temp = new Array(); // 使用 new 關鍵字,簡單宣告並初始化一個陣列
var temp = new Array(8);  // 還可以建立一個指定長度的陣列
var temp = new Array(1,2,4,9); // 直接將陣列元素作為引數傳遞給它的構造器

除了用 new建立陣列,還可以通過中括號 []簡單建立陣列。

var temp = [1,2,4,9];

訪問元素和迭代陣列

通過在陣列裡指定特定位置的元素,可以獲取該值或者賦值。而要知道一個陣列裡所有的元素,可以通過迴圈遍歷陣列。

for(var i = 0; i < temp.length; i++){
    console.log(temp[i]); // 1,2,4,9
}
案例:斐波那契數列

已知斐波那契數列中的第一個數字是1,第二個數字是2,從第三項開始,每一項都等於前兩項之和。求斐波那契數列的前20個數

var fibonacci = [];
fibonacci[1] = 1;
fibonacci[2] = 2;

for(var i =3; i < 20; i++){
    fibonacci[i] = fibonacci[i-1] + fibonacci[i-2];
}
console.log(fibonacci); // [ 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]

新增元素

var number = [1,2,3];
number[number.length] = 4;
number // 1,2,3,4

上述程式碼可以在陣列的最後一位新增元素,但其實還有更加簡便的方法:

push

push 能新增任意個元素在陣列的末尾

number.push(5); // 5
number.push(6,7); //7
number // [1,2,3,4,5,6,7]

陣列使用 push 會返回陣列的長度

插入元素到陣列首位

首先我們要騰出陣列的第一個元素的位置,把所有的元素向右移動一位。我們可以迴圈陣列中的元素,從最後一位+1(長度)開始,將其對應的前一個元素的值賦給它,依次處理,最後把我們想要的值賦給第一個位置(-1)上。

for(var i = number.length; i>=0; i--){
    number[i] = number[i-1];
}
number[0] = -0;
unshift

或者直接 使用 unshift 方法,可以將數值插入陣列的首位:

var number = [1,2,3,4];
number.unshift(-2); // 5
number.unshift(-4,-3); // 7
number // [-4, -3, -2, 1, 2, 3, 4]

陣列使用 unshift 會返回陣列的長度

刪除元素

從陣列尾部刪除元素

pop

要刪除最靠後的元素可以使用 pop 方法,會刪除並返回陣列的最後一個元素。如果陣列已經為空,則 pop() 不改變陣列,並返回 undefined 值。

var number = [1,2,3,4];
number.pop(); //4
number // [1,2,3]
number.pop() // 3
number // [1]

從陣列首部刪除元素

如果要移除陣列裡的第一個元素,可以使用下面的程式碼

var number = [1,2,3,4];
for(var i = 0;i < number.length; i++){
    number[i] = number[i+1];
}
number // [2, 3, 4, undefined]

可以看出,我們將陣列左移了一位,但陣列的長度仍然沒有變化,這意味著陣列中有一個額外的元素,因為沒有定義,所以是 undefined

shift

shift() 方法用於把陣列的第一個元素從其中刪除,並返回第一個元素的值。 陣列的長度也會發生變化。如果陣列是空的,那麼 shift() 方法將不進行任何操作,返回 undefined 值。

小結

修改陣列的方法 描述
push push() 方法可向陣列的末尾新增一個或多個元素,並返回新的長度。
unshift unshift() 方法可向陣列的開頭新增一個或更多元素,並返回新的長度。
pop pop() 方法用於刪除並返回陣列的最後一個元素。 如果陣列已經為空,則 pop() 不改變陣列,並返回 undefined 值。
shift shift() 方法用於把陣列的第一個元素從其中刪除,並返回第一個元素的值。 如果陣列是空的,那麼 shift() 方法將不進行任何操作,返回 undefined 值

push() 方法和 pop() 方法,能用陣列模擬基本的棧的資料結構(先進後出)。

shift()方法和unshift()方法,能用陣列模擬基本的佇列的資料結構(先進先出 )。

在任意位置新增或者刪除元素

已經知道如何刪除陣列開頭和結尾的元素,那麼該怎麼在陣列中的任意位置刪除或者新增元素?

splice

splice() 方法向/從陣列中新增/刪除專案,然後返回被刪除的專案。 splice() 方法可刪除從 index 處開始的零個或多個元素,並且用引數列表中宣告的一個或多個值來替換那些被刪除的元素。

語法

arrayObject.splice(index,howmany,item1,.....,itemX)

例子

var number = [1,2,3,4];
number.splice(2,0,4,4,5); // []
number //[1, 2, 4, 4, 5, 3, 4]
number.splice(2,5,7); // [4, 4, 5, 3, 4]
number //[1, 2, 7]

二維或者多維陣列

我們知道如果要記錄數天內每個小時的氣溫,可以使用陣列來儲存這些資料。那麼要儲存兩天每小時氣溫的資料的時候可以這樣。

var averageTemp1 = [32,53,45,23,46,53];
var averageTemp2 = [98,32,74,34,63,73];

然而這不是最好的方法。可以使用矩陣(二維陣列)來儲存這些資訊。矩陣的行儲存每天的資料,列對應小時級別的資料。

var averageTemp = [];
averageTemp[0] = [32,53,45,23,46,53];
averageTemp[1] = [98,32,74,34,63,73];

JavaScript只支援一維陣列,並不支援矩陣。但是,可以用陣列套陣列來模擬矩陣或者任一多維陣列。

迭代二維陣列的元素

如果想看到這矩陣的輸出,可以建立一個通用函式,專門輸出其中的值:

function printMatrix(x){
    for(var i = 0; i < x.length; i++){
        for(var j = 0; j< x[i].length; j++){
            console.log(x[i][j]);
        }
    }
}
printMatrix(averageTemp);

多維陣列

我們也可以用這種方式來處理多維陣列。假如我們要建立一個3x3x3的矩陣,每一個格子裡包含矩陣的i(行)、j(列)、z(深度)之和:

var matrix3x3x3 = [];

for(var i = 0; i < 3; i++){
    matrix3x3x3[i] = [];
    for(var j = 0; j < 3; j++){
        matrix3x3x3[i][j] = [];
        for(var z = 0; z < 3; z++){
            matrix3x3x3[i][j][z] = i+j+z;
        }
    }
}

資料結構中有幾個維度都沒有關係,都可以用迴圈遍歷每個維度來訪問所有格子

    for(var i = 0; i < matrix3x3x3.length; i++){
        for(var j = 0; j< matrix3x3x3[i].length; j++){
            for(var z = 0; z < matrix3x3x3[i][j].length; z++){
                console.log(matrix3x3x3[i][j][z]);
            }
        }
    }

如果是一個3x3x3x3的矩陣,程式碼中就會用四層巢狀的 for 語句,以此類推。

JavaScript 的陣列方法參考

在JavaScript裡,陣列是可以修改的物件。這意味著建立的每一個陣列都有一些可用的方法。

下面表格是陣列的一些核心方法。

方法名 描述
concat 連線2個或者更多陣列,並返回結果
every 對陣列中的每一項執行給定函式,如果該函式對每一項都但返回true,則返回true
filter 對陣列中度過每一項執行給定函式,返回該函式會返回true的項組成分陣列
forEach 對陣列中更多每一項執行給定函式,這個方法沒有返回值
join 將所有的陣列元素連線成一個字串
indexOf 返回第一個與給定引數相等的陣列元素的索引,沒有找到則返回-1
lastIndexOf 返回在陣列中搜尋到的與給定引數相等的元素的索引裡最大的值
map 對陣列中的每一項執行給定函式,返回每次函式呼叫結果組成的陣列
reverse 顛倒陣列中的元素的順序,原先第一個元素現在變成了最後一個,同樣原先的最後一個元素變成了現在的第一個
slice 傳入索引值,將陣列裡對應索引範圍內的元素作為新陣列返回
some 對陣列中每一項執行給定函式,如果任一項返回true,則返回true
sort 按照字母的順序對陣列排序,支援傳入指定排序方法的函式作為引數
toString 將陣列作為字串返回
valueOf 和 toString 相似,將陣列作為字串返回

陣列合並

有多個陣列,需要合併起來成為一個陣列。我們可以迭代各個陣列,然後把每個元素加入最終的陣列。

JavaScript也有提供相對應的方法 concat()

var a = 0;
var b = [1,2,3];
var c = [-3,-2,-1];
var s = c.concat(a,b);
s // [-3, -2, -1, 0, 1, 2, 3]

迭代器函式

有時候,我們需要迭代陣列中的元素。可以使用迴圈語句(前面提到的for語句等)。而其實 JavaScript 內建了許多陣列可以使用的迭代方法。

對於本節的例子,我們需要函式和陣列。假如有一個陣列,值是從1到15,如果陣列裡面的元素可以被2整除(偶數),函式就要返回true,否則就返回false:

var isEven = function(x){
    // 如果是 2的倍數,就返回 true
    return (x % 2 == 0);
}
var number = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15];
every

every 會迭代陣列中的每個元素,直到返回 false。

number.every(isEven)

在這個例子中,陣列number第一個元素是1,不是2的倍數,因此 isEven 函式返回false,然後 every 執行結束。

some

some 方法和 every 相似,不過some方法會迭代陣列中的每個元素,直到函式返回true

number.some(isEven)

這個例子中,陣列的第二個引數是2,為2的倍數,因此返回true,迭代結束

forEach

如果要迭代整個陣列可以用 forEach 方法,和使用 for 迴圈相同:

number.forEach(function(x){
    console.log((x % 2 == 0));
});
map & filter

JavaScript還有兩個會返回新陣列的遍歷方法。第一個是 map:

var myMap = number.map(isEven);
myMap // [false, true, false, true, false, true, false, true, false, true, false, true, false, true, false]

從上面程式碼可以看出,myMap儲存了傳入 map 方法的 isEven函式執行的結果。這樣就可以很容易知道一個元素是否偶數。

還有一個filter方法,它返回的新陣列由使函式返回 true 的元素組成:

var evenNumbers = number.filter(isEven);
evenNumbers // [2, 4, 6, 8, 10, 12, 14]
reduce

reduce() 方法接收一個函式作為累加器,陣列中的每個值(從左到右)開始縮減,最終計算為一個值。

語法

array.reduce(function(total, currentValue, currentIndex, arr), initialValue)
引數 描述
total 必需。初始值, 或者計算結束後的返回值。
currentValue 必需。當前元素
currentIndex 可選。當前元素的索引
arr 可選。當前元素所屬的陣列物件。

如果要對一個陣列中所有元素進行求和,這就很有用

number.reduce(function(total,currentValue,index){
    return total + currentValue;
});
// 120

ES6 和陣列的新功能

下表是ES6/7新增的陣列方法

方法 描述
@@iterator 返回一個包含陣列鍵值對的迭代器物件,可以通過同步呼叫得到陣列元素的鍵值對
copyWithin 複製陣列中一系列元素到同一陣列指定的起始位置
entries 返回包含陣列所有鍵值對的@@iterator
includes 如果陣列中存在某個元素則返回 true,否則返回false,ES7新增
find 根據回撥函式給定的條件從陣列中查詢元素,如果找到就返回該元素
findIndex 根據回撥函式給定的條件從陣列中尋找元素,如果找到就返回該元素在陣列中的索引
fill 用靜態值填充陣列
from 根據已有陣列建立一個新陣列
keys 返回包含陣列所有索引的@@iterator
of 根據傳入的引數建立一個新陣列
values 返回包含陣列中所有值的@@iterator

除了這些新的方法,還有一種用 for… of迴圈來迭代陣列的新做法,以及可以從陣列例項得到的迭代器物件。

使用 forEach 和箭頭函式

箭頭函式可以簡化使用 forEach迭代陣列元素的做法

number.forEach(function(x){
    console.log (x % 2 == 0);
})
// 等於
number.forEach(x => {
    console.log(x % 2 == 0);
});

使用 for…of 迴圈迭代

for(let n of number){
    console.log(n % 2 == 0);
}

使用ES6新的迭代器(@@iterator)

ES6還為 Array 類增加了一個 @@iterator 屬性,需要通過 Symbol.iterator來訪問。

let iterator = number[Symbol.iterator]();
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3
console.log(iterator.next().value); // 4
console.log(iterator.next().value); // 5
console.log(iterator.next().value); // 6

因為number陣列中有15個值,所以需要呼叫15次 iterator.next().value ,陣列中所有值都迭代完之後,就會返回 undefined。

  • 陣列的 entries、keys 和 values 方法

    ES6還增加了三種從陣列中得到迭代器的方法。

    entries 方法返回包含鍵值對的 @@iterator

    let aEntries = number.entries(); // 得到鍵值對的迭代器
    console.log(aEntries.next().value); // [0,1] -- 位置0的值為1
    console.log(aEntries.next().value); // [1,2] -- 位置1的值為2
    console.log(aEntries.next().value); // [2,3] -- 位置2的值為3

    number 陣列中都是數字,key是陣列中的位置,value是儲存在陣列中索引的值

    使用集合、欄位、雜湊表等資料結構時,能夠取出鍵值對是很有用的。後面會詳細講解。

    key 方法返回包含陣列索引的 @@iterator

    let aKeys  = number.entries(); // 得到陣列索引的迭代器
    console.log(aKeys.next()); // { value: 0, done: false}
    console.log(aKeys.next()); // { value: 1, done: false}
    console.log(aKeys.next()); // { value: 2, done: false}

    keys方法會返回number陣列的索引。一旦沒有可以迭代的值,aKeys.next() 就會返回一個value屬性為undefined,done屬性為 true的物件。如果 done屬性為false,就意味著還有可以迭代的值。

使用from方法

Array.from方法根據已有的陣列建立一個新數。比如複製number陣列:

let number2 = Array.from(number);
number2 // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

使用Array.of

Array根據傳入的引數建立一個新陣列、

let number3 = Array.of(1);
let number4 = Array.of(1,2,3,4,5,6);
number3 // [1]
number4 // [1,2,3,4,5,6]
// 複製已有的陣列
let numberCopy = Array.of(...number4);
numberCopy // [1,2,3,4,5,6]

使用fill方法

fill方法用靜態值充填陣列。

let numberCopy = Array.of(1,2,3,4,5,6);
numberCopy.fill(0); // [0, 0, 0, 0, 0, 0]
// 指定開始充填的索引
numberCopy.fill(2,1); // [0, 2, 2, 2, 2, 2]
// 指定結束的索引(結束的索引不包括本身)
numberCopy.fill(1,3,5); // [0, 2, 2, 1, 1, 2] 

建立陣列並初始化的時候,fill 方法就非常好用。

let ones = Array(6).fill(1); // 建立了一個長度為6,所有值都是1的陣列

使用copyWithin方法

copyWithin方法複製陣列中的一系列元素到同一個陣列指定的起始位置。

語法:

array.copyWithin(target, start, end)
引數 描述
target 必需。複製到指定目標索引位置。
start 可選。元素複製的起始位置。
end 可選。停止複製的索引位置 (預設為 array.length)。如果為負值,表示倒數。
let copyArray = [1,2,3,4,5,6];
copyArray.copyWithin(0,3); // [4, 5, 6, 4, 5, 6]

let copyArray1 = [1,2,3,4,5,6];
copyArray1.copyWithin(1,3,5); // [1, 4, 5, 4, 5, 6]

排序元素

反序輸出最開始的長度為15的number陣列。

number.reverse();
// [15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

嘗試使用JavaScript自帶的排序函式 sort();

number.sort();
//[1, 10, 11, 12, 13, 14, 15, 2, 3, 4, 5, 6, 7, 8, 9]

跟我們預期的有些不一樣,這是因為 sort 方法在對陣列進行排序 的時候,把元素預設成字串進行相互比較。所以我們要自己定義一個比較函式。

number.sort((a,b) =>{
    return a -b;
})
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

上述程式碼,如果 b 大於 a,會返回負數,反之就是正數。如果相等的話,就會返回0。下面的寫法會清晰一點

function compare(a, b){
    if(a < b){
        return -1;
    }
    if(a > b){
        return 1;
    }
    return 0;
}
number.sort(compare);

sort 方法接受 compareFunction作為引數來比較元素。然後sort 會用它來排序陣列

自定義排序

我們可以對任何物件型別的陣列排序,也可以建立 compareFuntion 來比較元素。例如物件 Person 有名字和屬性,我們希望根據年齡排序。

var friends = [
    {name: `John`, age: 30},
    {name: `Ana`, age: 20},
    {name: `Chris`, age: 25}
];

friends.sort((a, b) =>{
    return a.age - b.age;
})
// {name: "Ana", age: 20}
   {name: "Chris", age: 25}
   {name: "John", age: 30}

字串排序

var names = [`Ana`, `ana`, `John`, `john`];
names.sort();
// ["Ana", "John", "ana", "john"] 

字串的比較是根據對應的 ASCⅡ值來比較的。例如 A、J、a、j對應分別是65、74、97、106。

雖然字母表的 a 是排靠前的,但是由於 ASCⅡ值 比較大,所以沒有排在首位。如果忽略大小寫呢?會是怎麼樣

names.sort((a, b) =>{
    if(a.toLowerCase() < b.toLowerCase()){
        return -1;
    }
    if(a.toLowerCase() > b.toLowerCase()){
        return 1;
    }
    return 0; 
})
// ["Ana", "ana", "John", "john"]

搜尋

搜尋有兩個方法:indexOf方法返回與引數匹配的第一個元素的索引,lastIndexOf返回與引數匹配的最後一個元素的索引。

number.indexOf(10); // 9
number.indexOf(100); // -1 (因為100不存在陣列裡面)

number.puhs(10);
number.lastIndexOf(10); // 15
number.lastIndexOf(100) // -1

ES6 find 和 findIndex方法

let number =  [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
const multipleof13 = (element, index, array) => {
    return (element % 13 == 0);
}
number.find(multipleof13); //13
number.findIndex(multipleof13); // 12

find 和 findIndex方法接受一個回撥函式,搜尋一個滿足回撥函式條件的值。上面的例子中,我們要從陣列裡找有個13的倍數。

ES7 使用includes方法

如果陣列存在某個元素,includes 方法就會返回 true,否則就返回 false。

number.includes(15) // true
number.includes(20) // false

number.includes(4,4) // false 第二個引數為開始搜尋的索引

輸出字串

toString 和 jion 方法

如果想把陣列裡所有元素輸出位一個字串,可以使用 toString 方法

number.toString(); // "1,2,3,4,5,6,7,8,9,10,11,12,13,14,15"

還可以用不同的分隔符將元素隔開

number.join(`-`); // "1-2-3-4-5-6-7-8-9-10-11-12-13-14-15"

型別陣列

JavaScript由於不是強型別的,因此可以儲存任意型別的資料,而型別陣列則用於儲存單一的資料。

語法:

let myArray = new TypedArray(length);
型別陣列 資料型別
Int8Array 8位二進位制補碼整數
Unit8Array 8位無符號整數
Unit8ClampedArray 8位無符號整數
Int16Array 16位二進位制補碼整數
Unit16Array 16位無符號整數
Int32Array 32位二進位制補碼整數
Unit32Array 32位無符號整數
Float32Array 32位IEEE浮點數
Float64Array 64位IEEE浮點數
let length = 5;
let int16 = new Int16Array(length);

let array16 = [];
array16.length = length;

for(let i = 0;i < length; i++){
    int16[i] = i + 1;
}
console.log(int16); // [1, 2, 3, 4, 5]

使用 webGl API、進行位操作、處理檔案和影像時候,型別陣列都可以大展拳腳。它用起來和普通陣列也毫無二致,本節所學的陣列方法和功能都可以用於型別陣列。

小結

學習了常見的資料結構:陣列。學習了怎麼宣告和初始化陣列,給陣列賦值後,以及新增和移除陣列元素,學了多維陣列和陣列的一些操作方法。

下一章,學習棧,一種具有特殊行為的陣列。

書籍連結: 學習JavaScript資料結構與演算法


相關文章