簡體   English   中英

為什么列表的連接需要O(n)?

[英]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年代制定了一種以函數式編程語言處理這種列表的方法; 我有一次有效地重新發現了它 但是,良好的運行時保證需要H​​askell中不可用的語言級支持。

這是因為不可改變的狀態。 列表是一個對象+一個指針,所以如果我們將列表想象為一個元組,它可能看起來像這樣:

let tupleList = ("a", ("b", ("c", [])))

現在讓我們使用“head”函數獲取此“列表”中的第一個項目。 這個頭函數需要O(1)時間,因為我們可以使用fst:

> fst tupleList

如果我們想要將列表中的第一項替換為另一項,我們可以這樣做:

let tupleList2 = ("x",snd tupleList)

這也可以在O(1)中完成。 為什么? 因為列表中絕對沒有其他元素存儲對第一個條目的引用。 由於不可變狀態,我們現在有兩個列表, tupleListtupleList2 當我們制作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)中。 但是為了做到這一點,我們需要修改列表中的最后一個條目以給它一個新的指針。 更新指針會改變純功能語言中不允許的狀態。 希望我的帖子的其余部分清楚地表明了這一點。

為了連接兩個列表(稱為xsys ),我們需要修改xs中的最終節點,以便將它鏈接到(即指向) ys的第一個節點。

但是Haskell列表是不可變的,所以我們必須先創建一個xs的副本。 該操作是O(n) (其中nxs的長度)。

例:

xs
|
v
1 -> 2 -> 3

1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7
^              ^
|              |
xs ++ ys       ys

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM