<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??一站式輕松地調用各大LLM模型接口,支持GPT4、智譜、豆包、星火、月之暗面及文生圖、文生視頻 廣告
                ## 06 LinkedList 源碼解析 > 智慧,不是死的默念,而是生的沉思。 > ——斯賓諾莎 ## 引導語 LinkedList 適用于集合元素先入先出和先入后出的場景,在隊列源碼中被頻繁使用,面試也經常問到,本小節讓我們通過源碼來加深對 LinkedList 的了解。 ## 1 整體架構 LinkedList 底層數據結構是一個雙向鏈表,整體結構如下圖所示: ![圖片描述](https://img1.sycdn.imooc.com/5d5fc67a0001f59212400288.png)上圖代表了一個雙向鏈表結構,鏈表中的每個節點都可以向前或者向后追溯,我們有幾個概念如下: * 鏈表每個節點我們叫做 Node,Node 有 prev 屬性,代表前一個節點的位置,next 屬性,代表后一個節點的位置; * first 是雙向鏈表的頭節點,它的前一個節點是 null。 * last 是雙向鏈表的尾節點,它的后一個節點是 null; * 當鏈表中沒有數據時,first 和 last 是同一個節點,前后指向都是 null; * 因為是個雙向鏈表,只要機器內存足夠強大,是沒有大小限制的。 鏈表中的元素叫做 Node,我們看下 Node 的組成部分: ~~~java private static class Node<E> { E item;// 節點值 Node<E> next; // 指向的下一個節點 Node<E> prev; // 指向的前一個節點 // 初始化參數順序分別是:前一個節點、本身節點值、后一個節點 Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } } ~~~ ## 2 源碼解析 ### 2.1 追加(新增) 追加節點時,我們可以選擇追加到鏈表頭部,還是追加到鏈表尾部,add 方法默認是從尾部開始追加,addFirst 方法是從頭部開始追加,我們分別來看下兩種不同的追加方式: **從尾部追加(add)** ~~~java // 從尾部開始追加節點 void linkLast(E e) { // 把尾節點數據暫存 final Node<E> l = last; // 新建新的節點,初始化入參含義: // l 是新節點的前一個節點,當前值是尾節點值 // e 表示當前新增節點,當前新增節點后一個節點是 null final Node<E> newNode = new Node<>(l, e, null); // 新建節點追加到尾部 last = newNode; //如果鏈表為空(l 是尾節點,尾節點為空,鏈表即空),頭部和尾部是同一個節點,都是新建的節點 if (l == null) first = newNode;![圖片描述](//img1.sycdn.imooc.com/5d5fc69600013e4803600240.gif) //否則把前尾節點的下一個節點,指向當前尾節點。 else l.next = newNode; //大小和版本更改 size++; modCount++; } ~~~ 從源碼上來看,尾部追加節點比較簡單,只需要簡單地把指向位置修改下即可,我們做個動圖來描述下整個過程: ![圖片描述](https://img1.sycdn.imooc.com/5d5fc6a300013e4803600240.gif) **從頭部追加(addFirst)** ~~~java // 從頭部追加 private void linkFirst(E e) { // 頭節點賦值給臨時變量 final Node<E> f = first; // 新建節點,前一個節點指向null,e 是新建節點,f 是新建節點的下一個節點,目前值是頭節點的值 final Node<E> newNode = new Node<>(null, e, f); // 新建節點成為頭節點 first = newNode; // 頭節點為空,就是鏈表為空,頭尾節點是一個節點 if (f == null) last = newNode; //上一個頭節點的前一個節點指向當前節點 else f.prev = newNode; size++; modCount++; } ~~~ 頭部追加節點和尾部追加節點非常類似,只是前者是移動頭節點的 prev 指向,后者是移動尾節點的 next 指向。 ### 2.2 節點刪除 節點刪除的方式和追加類似,我們可以選擇從頭部刪除,也可以選擇從尾部刪除,刪除操作會把節點的值,前后指向節點都置為 null,幫助 GC 進行回收。 **從頭部刪除** ~~~java //從頭刪除節點 f 是鏈表頭節點 private E unlinkFirst(Node<E> f) { // 拿出頭節點的值,作為方法的返回值 final E element = f.item; // 拿出頭節點的下一個節點 final Node<E> next = f.next; //幫助 GC 回收頭節點 f.item = null; f.next = null; // 頭節點的下一個節點成為頭節點 first = next; //如果 next 為空,表明鏈表為空 if (next == null) last = null; //鏈表不為空,頭節點的前一個節點指向 null else next.prev = null; //修改鏈表大小和版本 size--; modCount++; return element; } ~~~ 從尾部刪除節點代碼也是類似的,就不貼了。 **從源碼中我們可以了解到,鏈表結構的節點新增、刪除都非常簡單,僅僅把前后節點的指向修改下就好了,所以 LinkedList 新增和刪除速度很快。** ### 2.3 節點查詢 鏈表查詢某一個節點是比較慢的,需要挨個循環查找才行,我們看看 LinkedList 的源碼是如何尋找節點的: ~~~java // 根據鏈表索引位置查詢節點 Node<E> node(int index) { // 如果 index 處于隊列的前半部分,從頭開始找,size >> 1 是 size 除以 2 的意思。 if (index < (size >> 1)) { Node<E> x = first; // 直到 for 循環到 index 的前一個 node 停止 for (int i = 0; i < index; i++) x = x.next; return x; } else {// 如果 index 處于隊列的后半部分,從尾開始找 Node<E> x = last; // 直到 for 循環到 index 的后一個 node 停止 for (int i = size - 1; i > index; i--) x = x.prev; return x; } } ~~~ 從源碼中我們可以發現,LinkedList 并沒有采用從頭循環到尾的做法,而是采取了簡單二分法,首先看看 index 是在鏈表的前半部分,還是后半部分。如果是前半部分,就從頭開始尋找,反之亦然。通過這種方式,使循環的次數至少降低了一半,提高了查找的性能,這種思想值得我們借鑒。 ### 2.4 方法對比 LinkedList 實現了 Queue 接口,在新增、刪除、查詢等方面增加了很多新的方法,這些方法在平時特別容易混淆,在鏈表為空的情況下,返回值也不太一樣,我們列一個表格,方便大家記錄: | 方法含義 | 返回異常 | 返回特殊值 | 底層實現 | | --- | --- | --- | --- | | 新增 | add(e) | offer(e) | 底層實現相同 | | 刪除 | remove() | poll(e) | 鏈表為空時,remove 會拋出異常,poll 返回 null。 | | 查找 | element() | peek() | 鏈表為空時,element 會拋出異常,peek 返回 null。 | PS:Queue 接口注釋建議 add 方法操作失敗時拋出異常,但 LinkedList 實現的 add 方法一直返回 true。 LinkedList 也實現了 Deque 接口,對新增、刪除和查找都提供從頭開始,還是從尾開始兩種方向的方法,比如 remove 方法,Deque 提供了 removeFirst 和 removeLast 兩種方向的使用方式,但當鏈表為空時的表現都和 remove 方法一樣,都會拋出異常。 ### 2.5 迭代器 因為 LinkedList 要實現雙向的迭代訪問,所以我們使用 Iterator 接口肯定不行了,因為 Iterator 只支持從頭到尾的訪問。Java 新增了一個迭代接口,叫做:ListIterator,這個接口提供了向前和向后的迭代方法,如下所示: | 迭代順序 | 方法 | | --- | --- | | 從尾到頭迭代方法 | hasPrevious、previous、previousIndex | | 從頭到尾迭代方法 | hasNext、next、nextIndex | LinkedList 實現了 ListIterator 接口,如下圖所示: ~~~java // 雙向迭代器 private class ListItr implements ListIterator<E> { private Node<E> lastReturned;//上一次執行 next() 或者 previos() 方法時的節點位置 private Node<E> next;//下一個節點 private int nextIndex;//下一個節點的位置 //expectedModCount:期望版本號;modCount:目前最新版本號 private int expectedModCount = modCount; ………… } ~~~ 我們先來看下從頭到尾方向的迭代: ~~~java // 判斷還有沒有下一個元素 public boolean hasNext() { return nextIndex < size;// 下一個節點的索引小于鏈表的大小,就有 } // 取下一個元素 public E next() { //檢查期望版本號有無發生變化 checkForComodification(); if (!hasNext())//再次檢查 throw new NoSuchElementException(); // next 是當前節點,在上一次執行 next() 方法時被賦值的。 // 第一次執行時,是在初始化迭代器的時候,next 被賦值的 lastReturned = next; // next 是下一個節點了,為下次迭代做準備 next = next.next; nextIndex++; return lastReturned.item; } ~~~ 上述源碼的思路就是直接取當前節點的下一個節點,而從尾到頭迭代稍微復雜一點,如下: ~~~java // 如果上次節點索引位置大于 0,就還有節點可以迭代 public boolean hasPrevious() { return nextIndex > 0; } // 取前一個節點 public E previous() { checkForComodification(); if (!hasPrevious()) throw new NoSuchElementException(); // next 為空場景:1:說明是第一次迭代,取尾節點(last);2:上一次操作把尾節點刪除掉了 // next 不為空場景:說明已經發生過迭代了,直接取前一個節點即可(next.prev) lastReturned = next = (next == null) ? last : next.prev; // 索引位置變化 nextIndex--; return lastReturned.item; } ~~~ 這里復雜點體現在需要判斷 next 不為空和為空的場景,代碼注釋中有詳細的描述。 **迭代器刪除** LinkedList 在刪除元素時,也推薦通過迭代器進行刪除,刪除過程如下: ~~~java public void remove() { checkForComodification(); // lastReturned 是本次迭代需要刪除的值,分以下空和非空兩種情況: // lastReturned 為空,說明調用者沒有主動執行過 next() 或者 previos(),直接報錯 // lastReturned 不為空,是在上次執行 next() 或者 previos()方法時賦的值 if (lastReturned == null) throw new IllegalStateException(); Node<E> lastNext = lastReturned.next; //刪除當前節點 unlink(lastReturned); // next == lastReturned 的場景分析:從尾到頭遞歸順序,并且是第一次迭代,并且要刪除最后一個元素的情況下 // 這種情況下,previous() 方法里面設置了 lastReturned = next = last,所以 next 和 lastReturned會相等 if (next == lastReturned) // 這時候 lastReturned 是尾節點,lastNext 是 null,所以 next 也是 null,這樣在 previous() 執行時,發現 next 是 null,就會把尾節點賦值給 next next = lastNext; else nextIndex--; lastReturned = null; expectedModCount++; } ~~~ ## 總結 LinkedList 適用于要求有順序、并且會按照順序進行迭代的場景,主要是依賴于底層的鏈表結構,在面試中的頻率還是蠻高的,相信理清楚上面的源碼后,應對面試應該沒有問題。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看