iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 8
0

今天我們來探討跟 Array 相關的話題吧!當然不是每個語言都有內建 Array,而是有其他類似的,我們會一起來看看有什麼樣的不一樣囉!(今天比較像是自由探索,哈哈!)


Scala

package com.ryan.scala.oop

import scala.collection.mutable.ArrayBuffer


object Solution {
    def main(args: Array[String]) {
        val intArray1 = new Array[Int](5)
        println(intArray1.mkString(","))
        intArray1(0) = 100
        println(intArray1.mkString(","))

        val intArray2 = ArrayBuffer[Int]()
        intArray2 += 1
        intArray2 += (2, 3)
        println(intArray2.mkString(","))

        val intArray3 = ArrayBuffer[Int](4, 5, 6)
        val intArray4 = intArray2 ++= intArray3
        println(intArray4.mkString(","))

    }
}
  • 在 Scala 的世界有 Array,但我們比較常用的是 List,因為 List 是 immutable 的,比較符合我們用 Functional way 去撰寫程式。不過因為這裡是談 Array 為主,所以我們還是來講講 Scala 中的 Array 。
    在 Scala 中 Array 是 mutable 的,也就是內容是可以改變,舉個例子, intArray1 雖然定義的時候是 val ,但是那是說 intArray1 只能一直是這個 Array,但是 Array 的內容是可以修改的 ( Array[Int],表示這是一個 Int 的 Array,這個部分就是 Scala 的 Generics,以後會再提到),例如這個例子我們把第一個元素改成 100 ( intArray1(0) = 100 ),而此 Array 的第一個元素真的是可以被改成 100 。Scala 的 Array 當定義之後,Size 就不能再修改了。
  • 如果我們要一個可以變更長度的 Array 就要使用 ArrayBuffer (要 import scala.collection.mutable.ArrayBuffer),像是 intArray2 ,這裡不用 new 是因為我們先前講過的 Companion Object 中的 apply Method。而 intArray2 可以看到我們會透過 += 來把元素加到裡頭。越先加進來的 index 越小。假使今天有兩個 ArrayBuffer 要接在一起,例如 intArray2intArray3 要接在一起就要使用 ++= 。因為 ArrayBuffer 可以改變長度,所以也提供了像是 remove , insert 等等 Method,而與 Array 之間也可以透過 toArray , toBuffer 互相轉換囉!而不管是 Array 還是 ArrayBuffer 都有提供像是 sum , max , min 等等方法可以讓你在一些情境下更方便使用囉!最後我們來看當我們想把全部元素印出來,彼此之間用逗號隔開時,我們用了 mkString(","),這會 iterate 全部的元素並且以逗號來連接各元素後,回傳 String 囉!更多可以參考這裡這裡
  • 最後我來簡單談談 List。在 Scala 的 List 是 immutable,而且是個遞迴結構 (linked list),對初學者來說與 Array 在使用上可能沒什麼差別,但是因為底層實作不同,所以在一些情境上的效能以及記憶體的使用也會不同。而因為其 immutable 的性質,所以大多數的時候我們都是使用 List 的。更多關於 List 的大小事,我將會在之後 List 的主題詳述囉!
  • 對了對了,Scala 還有一個叫做 Vector 的東西,是用來解決 linked list 在隨機存取時的效能問題,也是等我們到了 List 的主題會一並探討囉!如果你對於這些資料結構在效能跟空間使用上的效率,真的迫不及待想了解的話可以參考這個 Benchmark

Python 3

list1 = ['a', 'b', 1, 2]
print(list1[1:3])
list1.append('c')
print(list1)
list1.remove('b')
print(list1)
list2 = list1
list2[0] = 'd'
print(list1)
print(list2)
  • Python 並沒有內建 Array 而是 List,但這裡的 List 其實並不是像 Scala 中的 List 是 linked list。Python 的 List 其實可以當成是可變長度的 Array,而 Array 中的元素都是 Pointer,每個 Pointer 指向一個 object,所以在 List 中的資料是可以不同 Type 的,例如一個 List 裏頭又有 Integer 又有 String,如同 list1。因為可變,所以不管是添加 ( list1.append('c'))或刪除元素 ( list1.remove('b'))也都可以,而每次增加元素的時候,底層的 Array 就會改變大小,當然 Python 本身有做一些優化來避免不斷去變更長度。而在用 index 取值的時候,因為底層是 Array,所以效能跟一般的 Array 類似,也就是跟你 index 值的大小無關。假使我們要取 list 中的某一段可以以 <list>[<start>:<stop>] 的方式,例如 list1[1:3] 會取得以 list1[1] , list1[2] 組成的 list,也就是 [‘b’, 1]。要注意的是,假使我們把 list1 賦予給 list2 ,那麼修改 list2[0]list1[0] 也會跟著看到改變後的結果囉!可以思考看看為什麼會這樣。(提示:剛才說 list 裡頭存的是 Pointer)
  • 那麼 Python 有沒有比較像是一般 Array,固定長度而且固定元素的 Type 呢?這邊我們要選用一個 Python 很強大的 Library 叫做 Numpy。因為原生 List 在處理大資料的時候效能不好, 而 Numpy 底層以 C 和 Fortran 實作,加上其有平行處理的能力,所以在目前的資料科學領域相當被倚重,許多知名的像是 Pandas、scikit-learn 都是奠基其上。而 numpy 的 Array 比起原生 List 用的記憶體 Size 比較小,效能也比較好囉! 有興趣的可以看看這裡
  • 以上結果會印出
['b', 1]
['a', 'b', 1, 2, 'c']
['a', 1, 2, 'c']
['d', 1, 2, 'c']
['d', 1, 2, 'c']

Golang

package main

import "fmt"

func main() {
	var array1 [5]string
	array1[0] = "a"
	array1[1] = "b"
	array1[2] = "c"
  	array1[3] = "d"
  	array1[4] = "e"

	slice1 := make([]string, 5, 5)
	slice1[0] = "a"
	slice1[1] = "b"
	slice1[2] = "c"
  	fmt.Println(len(slice1))
  	fmt.Println(cap(slice1))

 	slice2 := array1[2:4]
  	slice2[0] = "C"
  	fmt.Println(len(slice2))
  	fmt.Println(cap(slice2))
  	fmt.Println(array1)

  	slice3 := make([]string, len(slice2))
	copy(slice3, slice2)
  	slice3[0] = "A" 
  	fmt.Println(slice2)
  	fmt.Println(slice3) 
  • Golang 的部分就讓我們談談 Array 跟 Slice 吧!
  • Golang 的 Array 長度固定且 Type 一致。Slice 則是長度不用在一開始的時候就固定下來,然而當我們去看 Slice 的實作 (可參考這個),其實一個 Slice 的資料結構會存放三個東西,第一個是存放指向 Array 的 Pointer (ptr),也就是真正存放資料的 Array,由此可以知道 Slice 其實是一個 Array 的 “某一段”,第二個是 len,也就是這個 “某一段” 裡頭有多少元素,第三個是 capacity,也就是從 ptr 指到的地方開始,一直到最後一個元素到底共有幾個。由於 Slice 存放的是 Pointer,會不會有別的 Slice 也指向同一個 Array 呢?答案是會的喲!所以可能會造成某個 Slice 修改了某值,而另一個 Slice 也看到了修改後的結果,造成非預期的影響。如果要解決這個問題,那麼我們就要使用內建的 copy ,讓不同 Slice 都有自己指向的獨一無二 Array,就能避免這個問題。
  • var array1 [5]string 宣告一個長度是 5 的 Array array1 ,並且將每個元素都賦值。而 slice1 是透過 make 來創造 ( make用來創造 slice, map, 以及 channel。後兩者是什麼以後會提到囉),而 make([]string,5,5)表示創造一個 Slice of string 並且 lencapacity 都是 5。再來我們一樣給 slice1賦值,此時透過 lencap 分別得到 slice1len5,而 cap也是 5。為什麼 len5 不是 3 呢?因為這時候其實沒被特別賦值的部分在被宣告的時候也有初始值唷!這時我們讓 slice2array1[2:4] ,也就是把 slice2ptr 指到 array1[2] 之處,這時可以看到 slice2len2 (因為 array1[2:4]有 3 個元素),cap3 (因為從 array1[2] 開始到最尾端還有 3 個元素)。這時我們試著改變 slice2[0] = "C" ,也就是 array1[2] 的位置,結果發現印出 array1 的時候,確實值也被改變囉!(就是因為 slice 只是用一個 Pointer 指到 array)。如果要避免這個狀況就要像 slice3copy 來創造出另一個新的空間,這樣一來, slice3slice2 就互不干擾了!
  • 至於 Golang 有沒有像是 linked list 的東西呢?答案是有的,而且有包在 Standard library 裏頭,待我們之後到了實作 List 的章節之後再來一起看看囉!
  • 以上結果會印出如下:
5
5
2
3
[a b C d e]
[C d]
[A d]

Rust

fn main() {
  let mut array1 = [1, 2, 3, 4, 5];
  let mut array2 = array1;
  array2[0] = 6;

  for x in array1.iter() {
    println!("{}", x);
  }

  let mut vec1 = Vec::new();
  vec1.push(1);

  let mut vec2 = vec![1, 2, 3];
  match vec2.pop()  {
    Some(x) => println!("{}", x),
    None => println!("None"),
  }
  println!("{}", vec2.pop().unwrap());
  vec2.pop().unwrap();
  // vec2.pop().unwrap();

  let slice1 = &mut[1, 2, 3, 4, 5];
  slice1[0] = 6;

}
  • 在 Rust 裏頭,有 Array, Slice (跟 Golang 類似),另外還有 Vector,讓我們來看看這三者有什麼差異吧!
  • 在 Rust 中,Array 是固定大小,跟一般語言沒什麼差別。不過要是你要改變 Array 的內容,就要用 let mut 來進行變數綁定唷!Array 本身不能直接被 iterate,必須像我們之前所說,要像是 array1.iter() 變成一個迭代器才可以。那麼今天如果我們需要一個比較彈性,可以調整長度,但又像是 Array,我們就會選用 Vector。Vector 因為比較靈活,所以也滿常被使用的。建立 Vector 的方式有兩種,第一種是我們透過 Vec::new() ,先建立出一個空的 Vector 後,再把值透過 push 放入 Vector。第二種是透過 vec! 這個 Macro 來建立。至於使用上跟 Array 差不多,都是可以修改資料,只要一開始用 mut 做綁定。我來看一個比較有趣的是 vec2.pop() ,這個 Function 會從尾端把元素給取出來 (Vector 的長度就會減一),那是如果一直 pop() ,最後沒值會怎樣呢?答案是不會怎樣,但是我要怎麼知道已經沒值了?答案是 vec2.pop() 會回傳的是一個 Option 的 enum type,沒錯,我們之前有講過。所以有值的話,就會被包在 Some ,不然就會回 None 囉!另外來講下,因為每次如果遇到像 Option這種,用 match 的邏輯都差不多的話,那麼每次都寫成像上述的 Code 就太累贅,所以 Rust 提供另一個 Function 叫 unwrap ,像是 vec2.pop().unwrap() ,如果有值就會直接取出來給你,會自動判斷是不是 Some 。但是如果是 None 的話就會直接送你一個 Panic 啦!如果你覺得直接送 Panic 太殘忍,想要改給一個 default 值的話,可以用 unwrap_or(<default value>) 囉!有興趣可以參考這裡
  • 最後我們看下 Slice,跟 Golang 概念一樣,Slice 本身不存資料,而是存指向某個 Array 或 Vector 的 reference。這裡我們看到 slice1 = &mut[1,2,3,4,5] ,是說 slice1 是一個指向 [1,2,3,4,5] 這個 Array 的 reference (透過 &),而 mut 表示可以修改的權限。然後我們就可以像修改 Array 一樣來修改 Slice 囉!
  • Rust 在 Standard library 也有 linked list,也是等到 linked list 的主題時讓我們來探討吧!

結語

今天把四個語言跟 Array 相關的資料結構大致上掃過一遍,至於每個語言在不同資料結構詳細的用法,大家有興趣我相信都可以在網路上找到很多資訊,主要就是讓大家一次可以去看看不同語言的不同與相同之處。那麼我們就明天見囉!


上一篇
[Day 6] 反轉字串大亂鬥!(reverse string)
下一篇
[Day 8] 談談映射這件事
系列文
30 天把自己榨好榨滿的四週四語言大挑戰!30

尚未有邦友留言

立即登入留言