[英]Why does concatenation of lists take O(n)?
根據ADT(代數數據類型)理論,兩個列表的串聯必須采用O(n)
,其中n
是第一個列表的長度。 基本上,您必須遞歸遍歷第一個列表,直到找到結束。
從不同的角度來看,可以說第二個列表可以簡單地鏈接到第一個元素的最后一個元素。 如果知道第一個列表的結尾,這將花費恆定的時間。
我在這里錯過了什么?
在操作上,Haskell列表通常由指向單鏈表的第一個單元的指針(粗略地)表示。 通過這種方式, tail
只返回指向下一個單元格的指針(它不必復制任何東西),並且在列表前面使用x :
分配一個新單元格,使其指向舊列表,並返回新指針。 舊指針訪問的列表未更改,因此無需復制它。
如果您改為使用++ [x]
附加一個值,那么除非您知道永遠不會訪問原始列表,否則您無法通過更改其最后一個指針來修改原始首選列表。 更具體地說,考慮一下
x = [1..5]
n = length (x ++ [6]) + length x
如果在執行x++[6]
時修改x
,則n
的值將變為12,這是錯誤的。 最后一個x
指的是長度為5
的未更改列表,因此n
的結果必須為11。
實際上,您不能指望編譯器對此進行優化,即使在不再使用x
情況下,理論上也可以在適當的位置更新(“線性”使用)。 發生的事情是x++[6]
的評估必須為最壞情況做好准備,其中x
后來被重用,因此它必須復制整個列表x
。
正如@Ben所說,“列表被復制”是不精確的。 實際發生的是具有指針的單元格被復制(列表中所謂的“脊柱”),但元素不是。 例如,
x = [[1,2],[2,3]]
y = x ++ [[3,4]]
只需要分配[1,2],[2,3],[3,4]
一次 。 列表x,y
列表將共享指向整數列表的指針,這些指針不必重復。
您要求的是與我在一段時間內為TCS Stackexchange編寫的問題有關:支持功能列表的常量時間連接的數據結構是一個差異列表 。
Yasuhiko Minamide在90年代制定了一種以函數式編程語言處理這種列表的方法; 我有一次有效地重新發現了它 。 但是,良好的運行時保證需要Haskell中不可用的語言級支持。
這是因為不可改變的狀態。 列表是一個對象+一個指針,所以如果我們將列表想象為一個元組,它可能看起來像這樣:
let tupleList = ("a", ("b", ("c", [])))
現在讓我們使用“head”函數獲取此“列表”中的第一個項目。 這個頭函數需要O(1)時間,因為我們可以使用fst:
> fst tupleList
如果我們想要將列表中的第一項替換為另一項,我們可以這樣做:
let tupleList2 = ("x",snd tupleList)
這也可以在O(1)中完成。 為什么? 因為列表中絕對沒有其他元素存儲對第一個條目的引用。 由於不可變狀態,我們現在有兩個列表, tupleList
和tupleList2
。 當我們制作tupleList2
我們沒有復制整個列表。 因為原始指針是不可變的,所以我們可以繼續引用它們,但在列表的開頭使用其他東西。
現在讓我們嘗試獲取3個項目列表的最后一個元素:
> snd . snd $ fst tupleList
這發生在O(3)中,它等於我們列表的長度,即O(n)。
但是我們不能存儲指向列表中最后一個元素的指針並在O(1)中訪問它嗎? 要做到這一點,我們需要一個數組,而不是一個列表。 數組允許任何元素的O(1)查找時間,因為它是在寄存器級別上實現的原始數據結構。
(ASIDE:如果你不確定為什么我們會使用鏈接列表而不是數組,那么你應該做更多關於數據結構,數據結構算法和各種操作的Big-O時間復雜性的閱讀,比如get,poll,insert ,刪除,排序等)。
現在我們已經建立了這個,讓我們來看看連接。 讓我們用新列表("e", ("f", []))
tupleList
。 要做到這一點,我們必須遍歷整個列表,就像獲取最后一個元素:
tupleList3 = (fst tupleList, (snd $ fst tupleList, (snd . snd $ fst tupleList, ("e", ("f", [])))
上面的操作實際上比O(n)時間更糟 ,因為對於列表中的每個元素,我們必須重新讀取列表到該索引。 但是如果我們暫時忽略它並關注關鍵方面:為了到達列表中的最后一個元素,我們必須遍歷整個結構。
您可能會問,為什么我們不在內存中存儲最后一個列表項? 附加到列表末尾的那種方式將在O(1)中完成。 但不是那么快,我們無法在不更改整個列表的情況下更改最后一個列表項。 為什么?
讓我們來看看它的外觀:
data Queue a = Queue { last :: Queue a, head :: a, next :: Queue a} | Empty
appendEnd :: a -> Queue a -> Queue a
appendEnd a2 (Queue l, h, n) = ????
如果我修改“last”,這是一個不可變的變量,我實際上不會修改隊列中最后一項的指針。 我將創建最后一項的副本。 引用該原始項目的所有其他內容將繼續引用原始項目。
因此,為了更新隊列中的最后一項,我必須更新所有引用它的內容。 這只能在最佳O(n)時間內完成。
所以在我們的傳統列表中,我們有最終項目:
List a []
但是如果我們想要改變它,我們會復制它。 現在,倒數第二個項目引用了舊版本。 所以我們需要更新該項目。
List a (List a [])
但如果我們更新第二個項目,我們會復制它。 現在第三個最后一項有一個舊的參考。 所以我們需要更新它。 重復,直到我們到達列表的頭部。 我們走了一圈。 沒有任何東西保留對列表頭部的引用,因此編輯需要O(1)。
這就是Haskell沒有雙鏈表的原因。 這也是無法以傳統方式實現“隊列”(或至少FIFO隊列)的原因。 在Haskell中創建隊列需要對傳統數據結構進行一些認真的重新思考。
如果您對所有這些工作方式變得更加好奇,請考慮使用Purely Funtional Data Structures這本書。
編輯:如果您曾經見過這個: http : //visualgo.net/list.html您可能會注意到可視化“插入尾部”發生在O(1)中。 但是為了做到這一點,我們需要修改列表中的最后一個條目以給它一個新的指針。 更新指針會改變純功能語言中不允許的狀態。 希望我的帖子的其余部分清楚地表明了這一點。
為了連接兩個列表(稱為xs
和ys
),我們需要修改xs
中的最終節點,以便將它鏈接到(即指向) ys
的第一個節點。
但是Haskell列表是不可變的,所以我們必須先創建一個xs
的副本。 該操作是O(n)
(其中n
是xs
的長度)。
例:
xs
|
v
1 -> 2 -> 3
1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7
^ ^
| |
xs ++ ys ys
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.