不要在循環體中使用 array_merge ()

2021-03-02 php中文網最新課程

標題是不要在循環體中使用 array_merge(),其實這只是本篇文章的結論之一

下面我們一起研究一下 php 語言中數組的合併(這裡先不考慮遞歸合併)

四種合併數組的方式對比

四種常見的合併數組的方式對比

寫代碼

我們知道 array_merge() 和 運算符 + 都可以拼接數組

創建一個類

ArrayMerge()
● eachOne() 循環體使用 array_merge() 合併
● eachTwo() 循環體結束後使用 array_merge() 合併
● eachThree() 循環體嵌套實現數組合併
● eachFour() 循環體使用 運算符 + 拼接合併
● getNiceFileSize() 將內存佔用轉化成人類可讀的方式

/**
* Class ArrayMerge
*/
class ArrayMerge{
/**
* @param int $times
* @return array
*/
public static function eachOne(int $times): array{
$a = [];
$b = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
for ($i = 0; $i < $times; $i++) {
$a = array_merge($a, $b);
}
return $a;
}
/**
* @param int $times
* @return array
*/
public static function eachTwo(int $times): array{
$a = [[]];
$b = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
for ($i = 0; $i < $times; $i++) {
$a[] = $b;
}
return array_merge(...$a);
}
/**
* @param int $times
* @return array
*/
public static function eachThree(int $times): array{
$a = [];
$b = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
for ($i = 0; $i < $times; $i++) {
foreach ($b as $item) {
$a[] = $item;
}
}
return $a;
}
/**
* @param int $times
* @return array
*/
public static function eachFour(int $times): array{
$a = [];
$b = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
for ($i = 0; $i < $times; $i++) {
$a = $b + $a;
}
return $a;
}
/**
* 轉化內存信息
* @param $bytes
* @param bool $binaryPrefix
* @return string
*/
public static function getNiceFileSize(int $bytes, $binaryPrefix = true): ?string{
if ($binaryPrefix) {
$unit = array('B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB');
if ($bytes === 0) {
return '0 ' . $unit[0];
}
return @round($bytes / (1024 ** ($i = floor(log($bytes, 1024)))),
2) . ' ' . ($unit[(int)$i] ?? 'B');
}
$unit = array('B', 'KB', 'MB', 'GB', 'TB', 'PB');
if ($bytes === 0) {
return '0 ' . $unit[0];
}
return @round($bytes / (1000 ** ($i = floor(log($bytes, 1000)))),
2) . ' ' . ($unit[(int)$i] ?? 'B');
}
}

使用

先分配多點內存

輸出內存佔用,合併後的數組長度,並記錄每一步的用時

ini_set('memory_limit', '4000M');
$timeOne = microtime(true);
$a = ArrayMerge::eachOne(10000);
echo 'count eachOne Result | ' . count($a) . PHP_EOL;
echo 'memory eachOne Result | ' . ArrayMerge::getNiceFileSize(memory_get_usage(true)) . PHP_EOL;
$timeTwo = microtime(true);
$b = ArrayMerge::eachTwo(10000);
echo 'count eachTwo Result | ' . count($b) . PHP_EOL;
echo 'memory eachTwo Result | ' . ArrayMerge::getNiceFileSize(memory_get_usage(true)) . PHP_EOL;
$timeThree = microtime(true);
$c = ArrayMerge::eachThree(10000);
echo 'count eachThree Result | ' . count($c) . PHP_EOL;
echo 'memory eachThree Result | ' . ArrayMerge::getNiceFileSize(memory_get_usage(true)) . PHP_EOL;
$timeFour = microtime(true);
$d = ArrayMerge::eachFour(10000);
echo 'count eachFour Result | ' . count($d) . PHP_EOL;
echo 'memory eachFour Result | ' . ArrayMerge::getNiceFileSize(memory_get_usage(true)) . PHP_EOL;
$timeFive = microtime(true);
echo PHP_EOL;
echo 'eachOne | ' . ($timeTwo - $timeOne) . PHP_EOL;
echo 'eachTwo | ' . ($timeThree - $timeTwo) . PHP_EOL;
echo 'eachThree | ' . ($timeFour - $timeThree) . PHP_EOL;
echo 'eachFour | ' . ($timeFive - $timeFour) . PHP_EOL;
echo PHP_EOL;

結果

count eachOne Result | 100000
memory eachOne Result | 9 MiB
count eachTwo Result | 100000
memory eachTwo Result | 14 MiB
count eachThree Result | 100000
memory eachThree Result | 18 MiB
count eachFour Result | 10 #注意這裡
memory eachFour Result | 18 MiB
eachOne | 5.21253490448 # 循環體中使用array_merge()最慢,而且耗費內存
eachTwo | 0.0071840286254883 # 循環體結束後使用array_merge()最快
eachThree | 0.037622928619385 # 循環體嵌套比循環體結束後使用array_merge()慢三倍
eachFour | 0.0072360038757324 # 看似也很快,但是合併的結果有問題

● 循環體中使用 array_merge () 最慢,而且耗費內存

● 循環體結束後使用 array_merge () 最快

● 循環體嵌套比循環體結束後使用 array_merge () 慢三倍

● 看似也很快,但是合併的結果有問題

合併數組的坑

我們注意到剛剛的 eachFour 的結果長度只有 10

下面探究為什麼會出現這樣的結果

這裡拿遞歸合併一起做下對比

代碼

public static function test(): void{
$testA = [
'111' => 'testA1',
'abc' => 'testA1',
'222' => 'testA2',
];
$testB = [
'111' => 'testB1',
'abc' => 'testB1',
'222' => 'testB2',
'www' => 'testB1',
];
echo 'array_merge($testA, $testB) | ' . PHP_EOL;
print_r(array_merge($testA, $testB));
echo '$testA + $testB | ' . PHP_EOL;
print_r($testA + $testB);
echo '$testB + $testA | ' . PHP_EOL;
print_r($testB + $testA);
echo 'array_merge_recursive($testA, $testB) | ' . PHP_EOL;
print_r(array_merge_recursive($testA, $testB));
}

結果

+ 號拼接兩個數組,後者只會補充前者沒有的 key,但是會保留數字索引

array_merge() 和 array_merge_recursive() 會抹去數字索引,所有的數字索引按順序從 0 開始了

array_merge($testA, $testB) |    #數字索引強制從0開始了 字符key相同的以後者為準
Array
(
[0] => testA1
[abc] => testB1
[1] => testA2
[2] => testB1
[3] => testB2
[www] => testB1
)
$testA + $testB | #testA得到保留,testB補充了testA中沒有的key,數字索引得到保留
Array
(
[111] => testA1
[abc] => testA1
[222] => testA2
[www] => testB1
)
$testB + $testA | #testB得到保留,testA補充了testB中沒有的key,數字索引得到保留
Array
(
[111] => testB1
[abc] => testB1
[222] => testB2
[www] => testB1
)

array_merge_recursive($testA, $testB) |   #數字索引從0開始連續了,但數組的順序沒有被破壞,相同的字符串 `key` 合併為一個數組

Array
(
[0] => testA1
[abc] => Array
(
[0] => testA1
[1] => testB1
)
[1] => testA2
[2] => testB1
[3] => testB2
[www] => testB1
)

分析

看到這裡,你一定非常疑惑,沒想到 array_merge() 還有這樣的坑

我們先來看一看官方的手冊

array_merge ( array $array1 [, array $... ] ) : array

array_merge () 將一個或多個數組的單元合併起來,一個數組中的值附加在前一個數組的後面。返回作為結果的數組。

如果輸入的數組中有相同的字符串鍵名,則該鍵名後面的值將覆蓋前一個值。然而,如果數組包含數字鍵名,後面的值將不會覆蓋原來的值,而是附加到後面。

如果只給了一個數組並且該數組是數字索引的,則鍵名會以連續方式重新索引。

只有相同的字符串鍵名,後邊的值才會覆蓋前面的值。(但是手冊中沒有解釋為什麼數字鍵名的索引被重置了)

那麼我們來看一下源碼

PHPAPI int php_array_merge(HashTable *dest, HashTable *src)
{
zval *src_entry;
zend_string *string_key;
if ((dest->u.flags & HASH_FLAG_PACKED) && (src->u.flags & HASH_FLAG_PACKED)) {
// 自然數組的合併,HASH_FLAG_PACKED表示數組是自然數組([0,1,2]) 參考http://ju.outofmemory.cn/entry/197064
zend_hash_extend(dest, zend_hash_num_elements(dest) + zend_hash_num_elements(src), 1);
ZEND_HASH_FILL_PACKED(dest) {
ZEND_HASH_FOREACH_VAL(src, src_entry) {
if (UNEXPECTED(Z_ISREF_P(src_entry)) &&
UNEXPECTED(Z_REFCOUNT_P(src_entry) == 1)) {
ZVAL_UNREF(src_entry);
}
Z_TRY_ADDREF_P(src_entry);
ZEND_HASH_FILL_ADD(src_entry);
} ZEND_HASH_FOREACH_END();
} ZEND_HASH_FILL_END();
} else {
//遍歷獲取key和vaule
ZEND_HASH_FOREACH_STR_KEY_VAL(src, string_key, src_entry) {
if (UNEXPECTED(Z_ISREF_P(src_entry) &&
Z_REFCOUNT_P(src_entry) == 1)) {
ZVAL_UNREF(src_entry);
}
Z_TRY_ADDREF_P(src_entry);
// 參考https://github.com/pangudashu/php7-internal/blob/master/7/var.md
if (string_key) {
// 字符串key(zend_string) 插入或者更新元素,會增加key的計數
zend_hash_update(dest, string_key, src_entry);
} else {
//插入新元素,使用自動的索引值(破案了,索引被重置的原因在此)
zend_hash_next_index_insert_new(dest, src_entry);
}
} ZEND_HASH_FOREACH_END();
}
return 1;
}

總結

綜上所述,合併數組的不同方式都存在一定的缺陷,但是通過我們上面的探究,我們了解到

● 循環體中使用 array_merge() 合併數組不可取,速度差距達百倍

● array_merge() 合併數組要慎用,如果重視 key ,且 key 可能為數字,不能使用 array_merge() 來合併,我們可以採用循環體嵌套的方式(注意內層循環使用 key 進行賦值操作)

● 如果重視 key ,且 key 可能為數字,簡單合併數組可以使用運算符 + ,但是一定不要在循環體中使用,因為每次運算的的結果都是生成了一個新的數組。

-END-

▼請點擊下方:「閱讀原文」,在線查看全部文章內容!

相關焦點

  • php如何使用array_merge()函數?(代碼示例)
    array_merge()是PHP中的一個內置函數,它可以將兩個或多個數組的元素或值合併為一個數組,然後返回這個數組。下面我們就來具體介紹一下array_merge()函數的用法,希望對大家有所幫助。array_merge()函數array_merge()函數用逗號(',')分隔的數組列表作為需要合併的參數,然後將後一個數組的值附加在前一個數組的末尾,形成一個新數組並返回,這個數組中包含在參數中傳遞的數組的合併值。
  • 【數組分享】PHP函數array_merge ()分享(2020-11-12)
    array_merge () 合併一個或多個數組。 array array_merge ( array $array1 [, array $... ] )array_merge() 將一個或多個數組的單元合併起來,一個數組中的值附加在前一個數組的後面。返回作為結果的數組。
  • 【數組分享】PHP函數array_merge_recursive ()分享(2020-11-11)
    array_merge_recursive () 遞歸地合併一個或多個數組。 array array_merge_recursive ( array $array1 [, array $... ] )array_merge_recursive() 將一個或多個數組的單元合併起來,一個數組中的值附加在前一個數組的後面。
  • Javascript中 Array.sort 原理學習
    其實在代碼中一個sort()就可以解決,並且時間複雜度和空間複雜度相對都不會過高。其實sort()不光可以對數組進行排序, 基本數據類型的數組都可以, 並且可以實現對對象數組的排序。當長度大於定值時,有可能會merge排序或者是quick排序, merge和quick會將整個數組進行劃分,進行遞歸,一旦劃分的子數組長度小於定值時, 將不再遞歸劃分,直接進行插入排序。為什麼會這麼排序呢?
  • Sort an Array 數組排序
    它們的時間複雜度不盡相同,這道題貌似對於平方級複雜度的排序方法會超時,所以只能使用那些速度比較快的排序方法啦。題目給定了每個數字的範圍是 [-50000, 50000],並不是特別大,這裡可以使用記數排序 Count Sort,在 LeetCode 中也有直接利用這個解法的題Sort Colors,建立一個大小為 100001 的數組 count,然後統計 nums 中每個數字出現的個數,然後再從0遍歷到 100000,對於每個遍歷到的數字i,若個數不為0,則加入 count 數組中對應個數的 i-50000
  • PHP數組使用之道(乾貨)
    array_combine() 作為數組函數中的一員,用於通過使用一個數組的值作為其鍵名,另一個數組的值作為其值來創建一個全新數組:<?$value;});print_r($fruits);數組連接操作在 PHP 中合併數組的最佳方式是使用 array_merge() 函數。所有的數組選項會合併到一個數組中,具有相同鍵名的值會被最後一個值所覆蓋:<?
  • 你面試中,遇到的php數組的面試題,快收藏
    不斷添加中。<?思路:先把數組截取相應的長度( array_slice ),再把2段數組拼接( array_merge )<?方法①使用array_merge()函數array_merge($arr1,$arr2);方法②使用array_merge_recursive()函數遞歸追加數組( array_merge_recursive() 函數與 array_merge
  • Python語言中使用array模塊實現動態數組的操作
    背景對於動態數組諸如創建、插入、刪除、查詢大小等操作,在C/C++語言中,可以使用標準庫中的vector類實現,而在python語言中,也同樣提供了內置的array模塊實現類似的功能。Python中的array類似於列表list,如都可以動態增刪元素,但又有所區別,list中存儲的元素類型可以不一樣,但array中元素類型必須完全一樣。另外,由於list中每個元素同時存儲了其地址即指針(用以標記每個元素的數據類型)和實際的數據,所以,在存儲及操作效率上,array又遠遠高於列表。
  • Git命令解析 - merge、rebase
    Git 鼓勵在工作流程中頻繁地使用分支與合併,這完全不會增加倉庫負擔,並且可以基於這一特性創建更自由和更可靠的合作開發流程。許多使用Git 的開發者都喜歡使用這種方式來工作:僅在master分支上保留完全穩定的代碼,這些代碼通常處於已發布或等待發布的狀態。
  • Merge k Sorted Lists
    小編和實驗室同學之前面試找工作,也只刷了劍指offer和這top 100算法題,在實際面試中也遇到了很多LeetCode上的原題。劍指offer算法最優解之前和大家分享了,LeetCode Top 100這100道算法題,每道題小編都刷了很多遍,並且總結了一種最適合面試時手撕算法的最優解法。後續每天和大家分享一道LeetCode top 100高頻算法題,以及小編總結的最優解法。
  • Java 8 中 Map 騷操作之 merge() 的用法
    前段時間無意間發現了 map.merge() 方法,感覺還是很好用的,此文簡單做一些相關介紹。首先我們先看一個例子。merge() 怎麼用?假設我們有這麼一段業務邏輯,我有一個學生成績對象的列表,對象包含學生姓名、科目、科目分數三個屬性,要求求得每個學生的總成績。
  • C++語言中std::array的神奇用法總結
    自動推導數組大小很多項目中都會有類似這樣的全局數組作為配置參數:uint32_t g_cfgPara[] = {1, 2, 5, 6, 7, 9, 3, 4};當程式設計師想要使用std::array替換原生數組時,麻煩來了:
  • 【譯文】Git merge 和 Git rebase比較
    但如果使用得當的話,它能給你的團隊開發省去太多煩惱。在這篇文章中,我們會比較git rebase和類似的git merge命令,找到Git工作流中rebase的所有用法。概述   你要知道的第一件事是,git rebase 和git merge 做的事其實是一樣的。它們都被設計來將一個分支的更改併入另一個分支,只不過他們實現方式有些不同。
  • 經驗:停止 cherry-pick,請開始 merge!
    然而在最近的開發過程中,卻時不時的遇到 merge 衝突。在下文中,我將會詳細的分析 cherry-pick 造成衝突的原因,以及 cherry-pick 可能造成的其他更嚴重問題。我們以一個簡單的例子來進行分析:如上圖:我們有一個 master 分支,以及一個 feature 分支。
  • 從零開始寫git:理解rebase與merge
    上圖中左邊的部分就是使用rebase命令之前的倉庫狀態,而右邊則是使用rebase之後的狀態,可以看出在rebase之後,原本的feature分支下的E、F兩個commits都被「移動」到master分支的頂端了(事實上,這裡並不是「移動」而是「複製」,具體見下文)。
  • Python 中 NaN 和 None 的詳細比較
    的影響如果數據中含有None,會導致整個array的類型變成object。中的情況In[34]:np.array([1,None]) == np.array([1,None])Out[34]:array([ True,  True], dtype=bool)In[35]:np.array([1,NaN]) == np.array([1,NaN])Out[35
  • 【譯】Rust中的array、vector和slice
    Rust 中的 array、vector 和 slice。一個 array 是一組相同類型的數據集合,這些數據位於連續的內存塊中。比起 C/C++的未定義行為,我寧願使用 Rust 的 panic。VectorArray 最大的一個限制是它的固定大小。
  • 小白學R(一):根據共同列合併兩excel數據表(merge函數的使用)
    首先我們有想要合併的excel表A和B:A :                                                   B:可以看到A和B有共同的列,「住院號」, 在A中是「住院號」,B中是「住院號2」。