簡體   English   中英

與ArrayList相比,為什么我們不將線性搜索成本算作鏈表插入操作的前提瓶頸?

[英]Why don't we count linear search cost as a prerequisite bottleneck for the insertion operation of a linked list, compared to ArrayList?

我這個問題已經有一段時間了,但是我對答案不滿意,因為區別似乎是任意的,更像是被盲目接受而不是嚴格評估的傳統智慧。

在ArrayList中,據說插入成本(對於單個元素)是線性的。 如果我們在索引p處插入0 <= p <n,其中n是列表的大小,則在將新元素復制到位置p之前,將首先移動其余np個元素。

在LinkedList中,據說插入成本(對於單個元素)是恆定的。 例如,如果我們已經有一個節點並且想要在其后插入,我們可以重新排列一些指針,並且可以很快完成。 但是首先獲得該節點,除了首先進行線性搜索外,我不知道該如何完成(假設它不是像在列表的開頭或末尾添加前綴這樣的普通情況)。

但是,對於LinkedList,我們不計算初始搜索時間。 對我來說,這是令人困惑的,因為它就像是說“冰淇淋是免費的……在您付款后,它是免費的”。 就像,當然,是的。。。但是這跳過了花錢的困難部分。 當然,如果您已經有了所需的節點,則在LinkedList中插入將是固定的時間,但是首先將該節點插入可能會花費一些額外的時間! 我可以很容易地說,插入ArrayList是固定時間...在我移動其余np元素之后。

因此,我不明白為什么要對一個進行區分,而對另一個卻沒有。 您可能會爭辯說,對於LinkedLists,插入被認為是常量,因為您需要在不需要線性時間操作的前面或后面插入,而在ArrayList中,插入需要在位置p之后復制后綴數組,但是我可以很容易地將其復制通過說如果我們在ArrayList的后面插入來反駁,它是攤銷的固定時間,在大多數情況下,除非我們達到容量,否則不需要額外的復制。

換句話說,對於LinkedList,我們將線性填充與常量填充分離開來,但是對於ArrayList,我們並未將它們分離,即使在兩種情況下,線性操作都可能不會被調用或未被調用。

那么為什么我們認為它們對於LinkedList而不是ArrayList是分開的呢? 還是僅在其中LinkedList絕大多數用於頭/尾附加和前置而不是中間元素的上下文中定義它們?

這基本上是ListLinkedList的Java接口的限制,而不是鏈接列表的基本限制。 也就是說,在Java中沒有方便的“指向列表節點的指針”概念。

每種類型的列表都有一些與指向特定項目的想法松散相關的不同概念:

  • “引用”列表中特定項目的想法
  • 列表中項目的整數位置
  • 可能在列表中的項目的值(可能多次)

最籠統的概念是第一個,通常封裝在迭代器的概念中。 碰巧的是,為數組支持的列表實現迭代器的簡單方法是簡單地包裝一個整數,該整數引用該項目在列表中的位置。 因此,僅對於數組列表,引用項的第一種和第二種方法是緊密綁定的。

但是,對於其他列表類型,甚至對於大多數其他容器類型(樹,哈希等),情況並非如此。 對項目的通用引用通常類似於指向某個項目的包裝器結構的指針(例如HashMap.EntryLinkedList.Entry )。 對於這些結構,訪問第n個元素的想法不是必然的,甚至是不可能的(例如,無序集合(如集合和許多哈希圖))。

也許不幸的是,Java提出了通過索引獲取項目的想法,這是一流的操作。 直接在List對象上進行的許多操作都是根據列表索引實現的: remove(int index)add(int index, ...)get(int index)等。因此,想到這些操作是很自然的作為基本的。

對於LinkedList盡管使用指向節點的指針來引用對象更為基本。 與其傳遞列表索引,不如傳遞指針。 插入元素后,您將獲得一個指向該元素的指針。

在C ++中,此概念體現在iterator的概念中,這是引用集合中項目(包括列表)的一流方法。 那么Java中是否存在這樣的“指針”? 當然可以-這是Iterator對象! 通常,您將Iterator視為要進行迭代,但也可以將其視為指向特定對象。

因此,關鍵的觀察結果是:給定對象的指針(迭代器),您可以在恆定時間內刪除和添加鏈表,但是從類似數組的列表中,這通常需要線性時間 在刪除對象之前並不需要先搜索對象:在很多情況下,您可以維護或作為參考輸入,或者正在處理整個列表,在這里恆定時間刪除鏈接列表確實可以更改算法復雜度。

當然,如果您需要執行類似的操作,則刪除包含值“ foo”的第一個條目,該值意味着搜索刪除操作。 基於數組的列表和鏈接列表都采用O(n)進行搜索,因此它們在這里沒有變化-但是您可以有意義地分隔搜索刪除操作。

因此,原則上,至少在用例支持的情況下,可以傳遞Iterator對象而不是列表索引或對象值。 但是,在開頭我說過“ Java沒有方便的指向列表節點的指針的概念”。 為什么?

好吧,因為實際上使用Iterator實際上很不方便。 首先,很難首先獲得一個Iterator到對象:例如,與C ++不同, add()方法不會返回Iterator-因此要獲得指向剛添加的項目的指針,您需要需要繼續遍歷列表或使用listIterator(int index)調用,這對於鏈接列表而言本質上效率低下。 許多方法(例如subList() )僅支持采用索引的版本,而不支持迭代器-即使可以有效地支持這種方法。

此外,修改列表時,圍繞迭代器失效的限制實際上對於引用不可變列表中的元素實際上變得毫無用處。

因此,Java對列表元素的指針的支持是三心二意的,因此很難利用鏈接列表提供的恆定時間操作,除非在諸如添加到列表的開頭或在迭代過程中刪除項目之類的情況下。

它也不限於列表ConcurrentQueue也是一個鏈接結構,支持恆定時間刪除,但是您不能從Java中可靠地使用該功能

如果您使用的是LinkedList,則可能不會將其用於隨機訪問插入。 LinkedList為推送(在開始處插入)或添加(因為對最終元素I​​IRC的引用)提供了恆定的時間。 您猜對了,對隨機索引的插入(例如,已排序的插入)將花費線性時間-不是常數,這是正確的。

相比之下,ArrayList是線性的最壞情況 在大多數情況下,它只是簡單地執行數組復制來移動索引(這是恆定時間的低級移動)。 僅當需要調整支持數組的大小時,它才會花費線性時間。

暫無
暫無

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

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