04-虛擬頭節(jié)點(diǎn)與雙向鏈表

  • 虛擬頭節(jié)點(diǎn)

再前面的add(int index, E element)或者remove(int index)函數(shù)中盟蚣,我們針對index == 0的情況進(jìn)行了特殊處理,因為一般來講徐伐,在頭尾兩部分的時候,可能存在一些特殊情況付翁。

有時候為了讓代碼更加精簡筛欢,統(tǒng)一所有節(jié)點(diǎn)的處理邏輯,可以再最前面增加一個虛擬頭節(jié)點(diǎn)(不存儲數(shù)據(jù))

在前面的鏈表當(dāng)中战秋,我們是直接將first指針指向0的節(jié)點(diǎn)

1566905748814.png

現(xiàn)在我們增加一個虛擬頭節(jié)點(diǎn)璧亚,來指向0的節(jié)點(diǎn),然后讓first指針指向虛擬的頭節(jié)點(diǎn)

1566905982701.png

需要做如下的調(diào)整

  1. 為LinkedList增加一個構(gòu)造方法

        public LinkedList() {
            first = new Node<>(null,null);
        }
    
  2. 調(diào)整node方法脂信,由于我們之前是直接重first開始取癣蟋,現(xiàn)在改為虛擬頭節(jié)點(diǎn)后,應(yīng)該重first的下一個節(jié)點(diǎn)開始取

        private Node<E> node(int index) {
            rangeCheck(index);
            Node<E> node = first.next;
            for (int i = 0; i < index; i++) {
                node = node.next;
            }
            return node;
        }
    
  3. 由于現(xiàn)在增加虛擬頭節(jié)點(diǎn)后狰闪,為0節(jié)點(diǎn)疯搅,前面又一個虛擬頭節(jié)點(diǎn),因此埋泵,在add(int index, E element)幔欧、remove(int index)等函數(shù)中罪治,就不需要處理index == 0 的情況,源碼查看LinkedList2類

這種方式礁蔗,不是很常見觉义,也不是很推薦,主要是對處理問題思路的拓展

  • 復(fù)雜度分析

我們來針對前面ArrayList和LinkedList兩個類中浴井,E get(int index)晒骇、void add(int index, E element)、E set(int index, E element)磺浙、E remove(int index)洪囤、E remove(int index)方法進(jìn)行復(fù)雜度的分析

ArrayList中E get(int index)

    public E get(int index) {
        //對索引進(jìn)行判斷
        rangeCheck(index); //時間復(fù)雜度O(1)
        return elements[index];//時間復(fù)雜度O(1)
    }

E get(int index)的時間復(fù)雜度為O(1)

E set(int index, E element)

    public E set(int index,E element) {
        rangeCheck(index);//時間復(fù)雜度O(1)
        E old = elements[index];//時間復(fù)雜度O(1)
        elements[index] = element;//時間復(fù)雜度O(1)
        return old;
    }

E set(int index, E element)的時間復(fù)雜度為O(1)

void add(int index, E element)

void add(int index, E element) {
        //對索引進(jìn)行判斷
       rangeCheckForAdd(index);//時間復(fù)雜度O(1)
        //對數(shù)組進(jìn)行擴(kuò)容
        ensureCapacity(size + 1);//時間復(fù)雜度O(1)
    //在這個地方,數(shù)據(jù)規(guī)模為size
        for (int i = size - 1; i >= index; i--) {
            elements[i + 1] = elements[i];
        }
        elements[index] = element;
        size++;
    }

在add函數(shù)這個地方撕氧,數(shù)據(jù)規(guī)模為size箍鼓,并且由于復(fù)雜度與size和index有關(guān),因此就復(fù)雜度會有多種情況呵曹,即下方表示的

復(fù)雜度一般分為以下幾種

  • [ ] 最好情況復(fù)雜度

最好情況就是在最后添加元素款咖,這個時候不需要執(zhí)行循環(huán),那么復(fù)雜度為O(1)

  • [ ] 最壞情況復(fù)雜度

最壞的情況是在最前面添加元素奄喂,這個時候循環(huán)執(zhí)行的次數(shù)為數(shù)據(jù)規(guī)模铐殃,那么復(fù)雜度為O(n)

  • [ ] 平均情況復(fù)雜度

在本例中,由于情況比較特殊跨新,假設(shè)傳入index的概率都一樣時富腊,那么index的平均值為(1 + 2 + 3 + . ... + n)/ n,那么復(fù)雜度為O(n / 2) -> O(n)

E remove(int index)

public E remove(int index) {
        rangeCheck(index);
        E old = elements[index];
        for (int i = index + 1; i <= size - 1; i++) {
            elements[i - 1] = elements[i];
        }
        elements[--size] = null;
    return old;
    }

同樣的,在刪除元素時域帐,也存在最好和最壞的情況赘被,最好的情況即為刪除最后一個元素,則復(fù)雜度為O(1)肖揣,最壞即為刪除最前面的一個元素民假,則復(fù)雜度為O(n),平均復(fù)雜度也為O(n)

接下來我們來看LinkedList中的E get(int index)

    public E get(int index) {
        //該地方的復(fù)雜度取決于node(int index)的復(fù)雜度
        return node(index).element;
    }

鏈表的結(jié)構(gòu)如下所示

1566310888924.png

因此在查找元素的時候龙优,需要一個一個的往下進(jìn)行遍歷羊异,那么復(fù)雜度也就和ArrayList中的E remove(int index)相同,因此彤断,也存在最好和最壞的情況野舶,最好的情況即為刪除最后一個元素,則復(fù)雜度為O(1)宰衙,最壞即為刪除最前面的一個元素平道,則復(fù)雜度為O(n),平均復(fù)雜度也為O(n)

E set(int index, E element)

    public E set(int index, E element) {
        Node<E> node = node(index);
        E old = node.element;
        node.element = element;
        return old;
    }

在set(int index, E element)里面供炼,也調(diào)用了node(int index)一屋,那么復(fù)雜度和LinkedList中的E get(int index)相同

void add(int index, E element)

 public void add(int index, E element) {
        rangeCheckForAdd(index);
        if (index == 0) {
          first =  new Node<E>(element,first);
        } else  {
            Node<E> prev = node(index - 1);
            prev.next = new Node<>(element,prev.next);
        }
        size++;
    }

可以看出句伶,最好的情況為index == 0的情況,最壞的情況陆淀,則是找到最后一個元素,復(fù)雜度為O(n)先嬉,平均復(fù)雜度也為O(n)

E remove(int index)

    public E remove(int index) {
        rangeCheck(index);
        Node<E> node = first;
        if (index == 0) {
            first = first.next;
        } else  {
            Node<E> prev = node(index - 1);
            node = prev.next;
            prev.next = node.next;
        }
        size--;
        return node.element;
    }

刪除節(jié)點(diǎn)的情況轧苫,最終也和添加節(jié)點(diǎn)的情況一樣,復(fù)雜度相同

所以最終的結(jié)果為下圖

1566992841371.png
  • 動態(tài)數(shù)組add(E element)復(fù)雜度分析

    public void add(E element) {
        add(size,element);
    }

在這種情況下疫蔓,我們看到含懊,永遠(yuǎn)都是在size的位置進(jìn)行添加元素,這種情況不需要挪動元素衅胀,那么復(fù)雜度為O(1),不過也存在最壞復(fù)雜度的情況岔乔,就是擴(kuò)容的時候,即是O(n)滚躯,但是絕大多數(shù)情況是O(1),則平均復(fù)雜度可以視為O(1)雏门,在這種情況下,純在均攤復(fù)雜度掸掏,其均攤復(fù)雜度為O(1)

分析

假設(shè)此時有一個數(shù)組茁影,最大容量為4,那么在前面4次添加操作時丧凤,復(fù)雜度均為O(1)

1566993703821.png

但是當(dāng)我們繼續(xù)往下添加的時候募闲,就會擴(kuò)容,在擴(kuò)容時愿待,復(fù)雜度為O(4)

1566993749648.png

所以在此時浩螺,擴(kuò)容操作的復(fù)雜度就會均攤到前面4次添加操作中去,應(yīng)為擴(kuò)容與前面的添加操作存在關(guān)系

1566993895040.png

相當(dāng)于是每次add操作的次數(shù)為2仍侥,那么復(fù)雜度還是為O(1)

  • 雙向鏈表

此前所介紹的鏈表要出,也叫做單向鏈表

使用雙向鏈表,可以提升鏈表的綜合性能

再前面單向鏈表的時候农渊,示意圖是這樣的

1567824839951.png

單向鏈表只有一條線厨幻,當(dāng)前節(jié)點(diǎn)指向的永遠(yuǎn)是其下一個節(jié)點(diǎn),所以其缺點(diǎn)是只能重頭部往后面搜索

然而雙向鏈表腿时,當(dāng)中有一個指向上一個節(jié)點(diǎn)的指針况脆,可以通過下一個節(jié)點(diǎn),找到上一個節(jié)點(diǎn)批糟,其示意圖是這樣的

1567825075829.png

如果當(dāng)前節(jié)點(diǎn)為頭節(jié)點(diǎn)格了,則該節(jié)點(diǎn)的prev指針指向null

同樣的,鏈表對象中徽鼎,就對應(yīng)的多了一個last指針盛末,該指針指向鏈表的位節(jié)點(diǎn)弹惦,鏈表對象如下所示

1567825194902.png

整個鏈表的示意圖這應(yīng)該是這樣的

1567825581248.png

有了雙向鏈表,那么我們查找元素可能會從兩個方向來進(jìn)行擦找悄但,一個是從頭節(jié)點(diǎn)開始查找棠隐,一個是從尾節(jié)點(diǎn)開始擦找

??什么時候從左往右開始找,什么時候從右往左開始找檐嚣,如何進(jìn)行判斷助泽?

查找節(jié)點(diǎn)

可以通過定位節(jié)點(diǎn)的索引,通過size進(jìn)行判斷嚎京,判斷方式如下

Node<E> node(int index) {
        rangeCheck(index);
        if (index < (size >> 1)) {
            //在左邊
            Node<E> node = first;
            for (int i = 0; i < index; i++) {
                node = node.next;
            }
            return node;
        } else {
            //在右邊
            Node<E> node = last;
            for (int i = size - 1; i > index; i--) {
                node = node.prev;
            }
            return node;
        }
    }

清空鏈表

在單向鏈表時嗡贺,我們清空節(jié)點(diǎn)是直接將first指針置為null,那我們在雙向鏈表中鞍帝,如果同樣的把last置為null的話诫睬,會有問題存在嗎?我們先通過圖片進(jìn)行分析帕涌,清空鏈表的代碼如下

public void clear() {
    size = 0;
    first = null;
    last = null;
}

首先一個完整的鏈表是這樣的

1567825581248.png

當(dāng)我們把first和last都置為null之后摄凡,是這樣的

1567827407159.png

置空以后,就存在一個問題蚓曼,那么鏈表中的節(jié)點(diǎn)會被釋放掉嗎架谎?因為我們看到,現(xiàn)在每個節(jié)點(diǎn)辟躏,都有指針被上一個節(jié)點(diǎn)或者下一個節(jié)點(diǎn)引用著谷扣,其實在Java當(dāng)中,這種情況是會被釋放的捎琐,應(yīng)為在Java虛擬機(jī)當(dāng)中会涎,存在一個gc root對象,凡是沒有被gc root對象所引用的內(nèi)存瑞凑,都會被系統(tǒng)回收掉末秃。

那問題來了,什么樣的對象是gc root對象呢籽御?

在Java中练慕,只要被棧空間對象指向的對象技掏,屬于gc root對象铃将,由于我們在創(chuàng)建鏈表對象是是通過這種方式進(jìn)行創(chuàng)建的

List<Integer> list = new LinkedList<>();

其中通過new LinkedList<>()創(chuàng)建的對象,其內(nèi)存在堆空間當(dāng)中哑梳,而方法內(nèi)創(chuàng)建的局部變量List<Integer> list在椌⒀郑空間中,此時在楌妫空間的list引用著new LinkedList<>()內(nèi)存地址悯仙,那么通過new LinkedList<>()創(chuàng)建出來的對象龄毡,就叫做gc root對象。

知道什么叫做gc root對象以后锡垄,當(dāng)我們清空鏈表時沦零,將first和last引用都置為null之后,鏈表節(jié)點(diǎn)就不再有g(shù)c root對象引用著货岭,因此節(jié)點(diǎn)的內(nèi)存就會被系統(tǒng)回收掉路操,這就是Java虛擬機(jī)的做法。

鏈表的添加 add(int index, E element)

現(xiàn)在如下的一個雙向鏈表茴她,假設(shè)我們需要在節(jié)點(diǎn)為2的地方,添加節(jié)點(diǎn)

1567845965610.png

添加過程中的引用發(fā)生了如下變化

1567846273387.png

上面這種過程程奠,通過代碼表示為

Node<E> next = node(index);
Node<E> prev = next.prev;
Node<E> node = new Node<>(prev,element,next);
prev.next = node;
next.prev = node;

不過這種添加是比較通用的一種情況丈牢,那么假設(shè)我們現(xiàn)在添加的位置為序號為0的位置,那么應(yīng)該做特殊的處理瞄沙,應(yīng)為節(jié)點(diǎn)為0的prev為null己沛,因此需要特殊的處理

通過代碼表示為

Node<E> next = node(index);
Node<E> prev = next.prev;
Node<E> node = new Node<>(prev,element,next);
next.prev = node;
if (prev == null) {
    first = node;
} else  {
    prev.next = node;
}
1567847021486.png

同樣的,如果往最有一個位置添加元素距境,則需要特殊處理申尼,因此特殊處理代碼如下

Node<E> oldLast = last;
last = new Node<>(oldLast,element,null);
oldLast.next = last;
1567847631345.png

但是,這樣寫垫桂,還是存在問題师幕,即為鏈表當(dāng)中不純在任何節(jié)點(diǎn)的時候,Node<E> oldLast = last;為null诬滩,這種情況需要特俗處理霹粥,代碼為

Node<E> oldLast = last;
last = new Node<>(oldLast,element,null);
if (oldLast == null) {
    first = last;
} else  {
    oldLast.next = last;
}
1567847970263.png

完整的代碼邏輯是這樣的

public void add(int index, E element) {
        rangeCheckForAdd(index);
        if (index == size) {
            Node<E> oldLast = last;
            last = new Node<>(oldLast,element,null);
            if (oldLast == null) {
                first = last;
            } else  {
                oldLast.next = last;
            }
        } else  {
            Node<E> next = node(index);
            Node<E> prev = next.prev;
            Node<E> node = new Node<>(prev,element,next);
            next.prev = node;
            if (prev == null) {
                first = node;
            } else  {
                prev.next = node;
            }
        }
        size++;
    }

鏈表的刪除 E remove(int index)

同樣的,假設(shè)現(xiàn)在有如下的鏈表疼鸟,我們想要刪除下標(biāo)為2的節(jié)點(diǎn)

1567848382053.png

首先我們找到即將被刪除的節(jié)點(diǎn)后控,我們將即將被刪除節(jié)點(diǎn)的上一個節(jié)點(diǎn)中的next指向被刪除節(jié)點(diǎn)的next,被刪除節(jié)點(diǎn)的下一個節(jié)點(diǎn)的prev指向被刪除節(jié)點(diǎn)的prev空镜,通過圖示表示為

1567848579979.png

通過這樣修改以后浩淘,node為2的節(jié)點(diǎn),就從鏈表中成功的刪除吴攒,并且該節(jié)點(diǎn)不再有引用指向它张抄,其內(nèi)存最終被系統(tǒng)回收

1567848684927.png

代碼表示為:

public E remove(int index) {
        rangeCheck(index);
        Node<E> node = node(index);
        Node<E> prev = node.prev;
        Node<E> next = node.next;
        if (prev == null) {//等價于index == 0
            first = next;
        } else  {
            prev.next = next;
        }
        if (next == null) {//等價于index == size - 1
            last = prev;
        } else  {
            next.prev = prev;
        }
        size--;
        return node.element;
    }

雙向鏈表 VS 單向鏈表

1567850759251.png

雙向鏈表 VS 動態(tài)數(shù)組

動態(tài)數(shù)組

  • 開辟、銷毀內(nèi)存空間的次數(shù)相對較少洼怔,但是可能造成內(nèi)存空間的浪費(fèi)(可以通過縮容解決)

雙向鏈表

  • 開辟欣鳖、銷毀內(nèi)存空間的次數(shù)相對較多,但不會造成內(nèi)存空間的浪費(fèi)
  • 如果頻繁在尾部進(jìn)行添加茴厉、刪除操作泽台,動態(tài)數(shù)組什荣、雙向鏈表均可選擇
  • 如果頻繁在頭部進(jìn)行添加刪除操作怀酷, 建議選擇使用雙向鏈表
  • 如果有頻繁的(在任意位置)進(jìn)行添加稻爬、刪除操作, 建議選擇使用雙向鏈表
  • 如果有頻繁的查詢操作(隨機(jī)訪問操作)蜕依,建議選擇使用動態(tài)數(shù)組

不過桅锄,看到這里,我們不禁有疑問样眠,有了雙向鏈表友瘤,單向鏈表是否就沒有任何用處了呢?

并非如此檐束,在哈希表中的設(shè)計就用到了單向鏈表辫秧,至于原因,我會在哈希表章節(jié)中詳細(xì)說明被丧。

demo源碼下載地址

本節(jié)完盟戏!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市甥桂,隨后出現(xiàn)的幾起案子柿究,更是在濱河造成了極大的恐慌,老刑警劉巖黄选,帶你破解...
    沈念sama閱讀 218,036評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蝇摸,死亡現(xiàn)場離奇詭異,居然都是意外死亡办陷,警方通過查閱死者的電腦和手機(jī)探入,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來懂诗,“玉大人蜂嗽,你說我怎么就攤上這事⊙旰悖” “怎么了植旧?”我有些...
    開封第一講書人閱讀 164,411評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長离唐。 經(jīng)常有香客問我病附,道長,這世上最難降的妖魔是什么亥鬓? 我笑而不...
    開封第一講書人閱讀 58,622評論 1 293
  • 正文 為了忘掉前任完沪,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘覆积。我一直安慰自己听皿,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,661評論 6 392
  • 文/花漫 我一把揭開白布宽档。 她就那樣靜靜地躺著尉姨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪吗冤。 梳的紋絲不亂的頭發(fā)上又厉,一...
    開封第一講書人閱讀 51,521評論 1 304
  • 那天,我揣著相機(jī)與錄音椎瘟,去河邊找鬼覆致。 笑死,一個胖子當(dāng)著我的面吹牛肺蔚,可吹牛的內(nèi)容都是我干的煌妈。 我是一名探鬼主播,決...
    沈念sama閱讀 40,288評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼婆排,長吁一口氣:“原來是場噩夢啊……” “哼声旺!你這毒婦竟也來了笔链?” 一聲冷哼從身側(cè)響起段只,我...
    開封第一講書人閱讀 39,200評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎鉴扫,沒想到半個月后赞枕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,644評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡坪创,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,837評論 3 336
  • 正文 我和宋清朗相戀三年炕婶,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片莱预。...
    茶點(diǎn)故事閱讀 39,953評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡柠掂,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出依沮,到底是詐尸還是另有隱情涯贞,我是刑警寧澤,帶...
    沈念sama閱讀 35,673評論 5 346
  • 正文 年R本政府宣布危喉,位于F島的核電站宋渔,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏辜限。R本人自食惡果不足惜皇拣,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,281評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望薄嫡。 院中可真熱鬧氧急,春花似錦颗胡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至钾恢,卻和暖如春手素,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瘩蚪。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評論 1 269
  • 我被黑心中介騙來泰國打工泉懦, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人疹瘦。 一個月前我還...
    沈念sama閱讀 48,119評論 3 370
  • 正文 我出身青樓崩哩,卻偏偏與公主長得像,于是被迫代替她去往敵國和親言沐。 傳聞我的和親對象是個殘疾皇子邓嘹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,901評論 2 355

推薦閱讀更多精彩內(nèi)容