ETH官方钱包

前往
大廳
主題

【Unity / System / 工具】自製通用物件池優(yōu)化,順便學(xué)一下O(1)的神器:LinkedList / ArrayList

%%鼠 拒收病婿 | 2021-06-19 00:08:06 | 巴幣 2312 | 人氣 831

前言:
在上一篇【Unity C#小工具】簡單通用物件池 中提出的GCManager腳本,感謝@派大星教授的指導(dǎo),今天做了些修正後更新在Github上了。

最近讀優(yōu)化的東西剛好看到ArrayList竟然是O(1),當(dāng)下還懷疑自己有沒有看錯,所以藉機(jī)了解一下。


主要修正的點:
以前回收語法是:
LinkedListNode<object> to_remove_obj = dicts[_key].Find(obj);
如同@派大星教授說的:
《上面這樣.Find()又要尋找整個Linklist, 時間依舊O(N), 使用Linkedlist時正確應(yīng)該是把Node給使用者自行管理》

原本是希望有了這個工具,使用者就可以透過Key去取得物件就好,如果還多了"保存"的義務(wù)的話,勢必要多些使用者的麻煩。所以我就加了個dict去保管node。
static Dictionary<string, Vector2> s_m_registerScale = new Dictionary<string, Vector2>();
以內(nèi)容物為key,在Instancitate時更新對應(yīng)的node。

在回收時能直接使用。

因此使用者呼叫方法依舊,不用做其他改變。


[1]. When to use linkedlist over arraylist in java
LinkedList :
使用doubly-linked list結(jié)構(gòu) (頭接著上一個的尾端)
get(int index): O(n)、平均 n / 4。 只讀頭尾是O(1) 。

add(int index, E element):O(n),平均n/4。只寫頭尾是O(1) 。

remove(int index) :O(n),平均n/4。 只刪頭尾是O(1) 。

適合重複使用的資料鏈,且需頻繁插入、移除。

比ArrayList 多一些overhead,所以占用比較多的空間。




ArrayList :
Dynamically re-sizing array.

get(int index) :O(1)。
原理:假設(shè)一個int是4 bytes,起頭位址是1000,則若要訪問第20個,1000+4*20=1080,只要算一次。。

add(E element): 攤銷O(1),Array擴(kuò)充最遭情況是O(n)。因為每次擴(kuò)充都要安排一個新的、更大的array,然後把舊的資料複製到新的array上。

add(int index, E element):O(n)、平均O(n/2)

remove(int index):O(n),平均O(n/2)。

一開始要多給更空的空間,不然新增空間很慢。

理論上ArrayList 平均會比LinkedList 快一些,但差距不太大。

使用LinkedList 的好處是刪除頭尾是O(1),ArrayList 是O(n)。

如果記憶體大小很重要,請避免使用LinkedList 。


Algorithm           ArrayList   LinkedList
seek front            O(1)         O(1)
seek back             O(1)         O(1)
seek to index         O(1)         O(N)
insert at front       O(N)         O(1)
insert at back        O(1)         O(1)
insert after an item  O(N)         O(1)


後記:
了解兩者差別後,以O(shè)bject pool來說,是大量生成一定數(shù)量的物件,之後就是取值而已,照理來說應(yīng)該用ArrayList而不是LinkedList


參考資料:



送禮物贊助創(chuàng)作者 !
0
留言

創(chuàng)作回應(yīng)

派大星
其實比較是術(shù)語問題啦 每個語言都在那發(fā)明自己術(shù)語 像C# List 其實內(nèi)部就是動態(tài)Array而已 (加上一些設(shè)計好的API介面add, remove, ..., 還有自動管理擴(kuò)容) C++還叫做"Vector", Java叫做"ArrayList", 是覺得知道內(nèi)部是啥就好, 這些語言設(shè)計用詞很容易混淆和溝通困難(像是沒學(xué)過C#的人聽到List可能會直覺認(rèn)為是在講LinkedList )
2021-06-21 16:43:18
派大星
然後你講的, "超過原本長度大小,就得安排一個新的、更大的陣列位置" , 其實不會多浪費的,因為擴(kuò)容的方法是倍增法double sizing(通常), 擴(kuò)容瞬間cost是O(N), 但在多次add分?jǐn)傁聛頃荗(1)。假設(shè)現(xiàn)在容量有8個, 也剛好塞滿8個元素, 這時候你用add, 這時內(nèi)部會分配新的記憶體16, 拷貝原本8個過去, 再插入, 之後的連續(xù)插入Add, 10, 11, 12...都不用分配,直到9,10,11,...16用滿。依此類推。這樣你一直連續(xù)插入Add, 假設(shè)總共觸發(fā)N次分配記憶體拷貝的Cost是1+2+4+8+...+2^(N-1) = (2^N) - 1 (等比幾何數(shù)列公式),而此時你call Add的次數(shù)(也就是容量最大Size為) 2^(N-1), Add的單次平均花費 = Add 總共次數(shù)/ N次擴(kuò)容花費 = 2^(N-1) / (2^N) - 1 ~= 1/2 = O(1) 所以Add是O(1)
2021-06-21 16:54:27
派大星
" 好奇list宣告不用給長度,那預(yù)設(shè)是多長呢? " 看程式語言預(yù)設(shè)實作,像是上面的分析,一開始1+2+4的擴(kuò)容顯得很蠢很浪費, 或許可以一開始直接分配一個小數(shù)目的Capacity, 如40個(Capacity = 40), Size = 0, 之後你call Add 40以後,才擴(kuò)容,Capacity = 80, Size = 40, 但根據(jù)語言實作Default會不一樣。
2021-06-21 16:58:35
派大星
但一般在Constructor都可以直接要求一開始的Capacity, 如C#的List ??梢詤⒖糷ttps://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1.-ctor?view=net-5.0#System_Collections_Generic_List_1__ctor_System_Int32_
2021-06-21 16:59:07
派大星
哦可以看一下這個條目: https://en.wikipedia.org/wiki/Dynamic_array Geometric expansion and amortized cost (上面Add的O(1),是多次插入後, "分?jǐn)侰ost"去算的平均單次, 術(shù)語amortized cost) 。當(dāng)然如果我們只在特別關(guān)注"單次Add", 那連續(xù)插入的Cost就是,O(1), O(1), O(1), O(1), 突然O(N), O(1), O(1), O(1).....突然O(N) ....這種看法, 當(dāng)然要知道這種突然O(N)冒出來行為,在有些程式是不被允許的。(像是考慮到N=很大,程式突然卡住一下,雖然"平均"而言很順是O(1))。這點要注意一下。Amortized Cost分析和傳統(tǒng)Cost分析的差異~
2021-06-21 17:14:02
追蹤 創(chuàng)作集

作者相關(guān)創(chuàng)作

相關(guān)創(chuàng)作

更多創(chuàng)作