Java数据结构——LinkedList源码简介

LinkedList是Java中的数据结构之一,即链表。本篇文章将从源码角度简单介绍LinkedList的基本实现原理。

在阅读下面内容之前,请确保你已经了解链表的基本属性与特点,在此不会再做详细解释。

本文只介绍链表的核心操作方法,如add、remove、get、set,其他一些不太常用的方法暂时跳过。

简介:

在Java中使用的LinkedList,是一个双向循环带头节点的链表。至于为什么是这样的链表,在后面可以从源码中看到。

  • 双向:一个节点可以访问其前一个节点和后一个节点
  • 循环:可以从头节点访问最后一个节点,也可以从最后一个节点访问头节点
  • 带头节点:头节点不存储数据,仅表示链表的起始节点。

LinkedList类中基本属性:

Hprivate transient int size = 0;  

表示存储了多少个元素,即节点个数

private transient Entry<E> header = new Entry<E>(null, null, null); 

header,即头节点,是一个Entry类型的对象。

private static class Entry<E> {
    E element;
    Entry<E> next;
    Entry<E> previous;

    Entry(E element, Entry<E> next, Entry<E> previous) {
        this.element = element;
        this.next = next;
        this.previous = previous;
    }
}

Entry类中包含几个基本变量,previous,next,element

  • Previous:是一个Entry对象,表示当前节点的前一个节点
  • Next:是一个Entry对象,表示当前节点的后一个节点
  • Element:指定的数据对象,即此节点存储的数据

构造方法:

    public LinkedList() {  
        header.next = header.previous = header;  
    }

    public LinkedList(Collection<? extends E> c) {  
        this();  
        addAll(c);  
    }

默认构造方法中将header的next,previous均指向header,header自身成环,至此已经可以看出LinkedList是一个双向循环链表

构造方法还可以将已有元素追加到链表上,add方法将在后面介绍。

LinkedList中最常用的方法包括:add、set、get、remove,这些方法可能有不同的参数而形成了多个重载函数。

add方法:

public boolean add(E e) {
    addBefore(e, header);
    return true;
}

添加一个节点到指定位置:

public void add(int index, E element) {
    addBefore(element, (index == size ? header : entry(index)));
}

两个方法均是调用了addBefore这个方法,其中在指定index的add方法中,传入的第二个参数是由entry方法返回的结果,其实就是进行循环查找,找到第index个节点。
entry方法:作用是找到第index位置,然后返回这个节点对象

    private Entry<E> entry(int index) {
        if (index < 0 || index >= size)
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
        Entry<E> e = header;
        if (index < (size >> 1)) {
            for (int i = 0; i <= index; i++)
                e = e.next;
        } else {
            for (int i = size; i > index; i--)
                e = e.previous;
        }
        return e;
    }

这段代码中进行了判断,当index < (size >> 1) 时候(size>>1等价于size/2),通过next对象向后遍历链表,因为此时index节点在链表的前半部分。否则的话,通过previous对象向前遍历链表,因为此时index节点在链表的后半部分。这样可以提高遍历的效率。

下面继续查看addBefore方法的源码:

    private Entry<E> addBefore(E e, Entry<E> entry) {
        Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
        newEntry.previous.next = newEntry;
        newEntry.next.previous = newEntry;
        size++;
        modCount++;
        return newEntry;
    }

这个方法很简单,创建一个新的节点对象,这个新节点的next指向传入的entry,它的previous指向entry的前一节点。随后代码中修改新节点的previous.next(即entry),指向新节点,修改其next.previous(即entry的previous)指向新节点,形成双向循环。

这样依据代码的表述可能比较抽象,下面画图解释一下这个过程:

如此新的节点被插入到entry前,形成了新的链表。

除了add方法外,链表也有addAll方法

public boolean addAll(Collection<? extends E> c) {  
        return addAll(size, c);  
}  

引用了addAll的另一个重载函数,插入位置是链表尾端,主要实现在这个方法中:

public boolean addAll(int index, Collection<? extends E> c) {  
        if (index < 0 || index > size)  
            throw new IndexOutOfBoundsException("Index: "+index+  
                                                ", Size: "+size);  
        Object[] a = c.toArray();  
        int numNew = a.length;  
        if (numNew==0)  
            return false;  
    modCount++;  
  
        Entry<E> successor = (index==size ? header : entry(index));  
        Entry<E> predecessor = successor.previous;  
    for (int i=0; i<numNew; i++) {  
            Entry<E> e = new Entry<E>((E)a[i], successor, predecessor);  
            predecessor.next = e;  
            predecessor = e;  
        }  
        successor.previous = predecessor;  
  
        size += numNew;  
        return true;  
    }  

将要插入的数据转为Object数组,然后循环将元素逐个插入到指定的位置。

插入的方法,基本思路和addBefore方法一样,略有不同的地方是每次先修改了predecessor的值为新插入的节点,循环完毕后,再执行successor.previous = predecessor,使链表形成完整回路。

这样做是因为现在要插入的是很多个节点,而不是一个,每当插入一个新节点,index的位置就发生了变化,predecessor被替换为新插入的那个节点。只有最后一个新节点被插入以后,才能修改successor的previous指向位置,这样链表才能完整。

插入过程示意图:


添加的相关方法到此介绍完毕。主要核心思路就是修改节点的previous与next存放的对象,以此来形成新的完整链表。

get和set两个方法分别是对链表中的某个节点进行取值和赋值,其本质是通过entry方法实现的,entry如何实现的在之前已经介绍过。

get方法:

看下get方法源码

public E get(int index) {  
       return entry(index).element;  
}  

其实就是通过entry方法获取到index位置的节点,然后只要返回其中的数据即可。

set方法:

public E set(int index, E element) {  
        Entry<E> e = entry(index);  
        E oldVal = e.element;  
        e.element = element;  
        return oldVal;  
    }  

先通过entry方法获取到index位置的节点,然后修改其中的值。

remove:

remove方法即删除链表中某个节点,具体实现方法也是修改指向位置,是add的一个逆过程而已。

remove一共有3个重载函数

public E remove(int index) {  
        return remove(entry(index));  
    }  

这是最常用的一个方法,删除index位置的节点,其实是调用了另一个重载函数。

public boolean remove(Object o) {  
        if (o==null) {  
            for (Entry<E> e = header.next; e != header; e = e.next) {  
                if (e.element==null) {  
                    remove(e);  
                    return true;  
                }  
            }  
        } else {  
            for (Entry<E> e = header.next; e != header; e = e.next) {  
                if (o.equals(e.element)) {  
                    remove(e);  
                    return true;  
                }  
            }  
        }  
        return false;  
    }  

这个方法是按照元素的值进行删除,循环、查找、找到这个节点后调用另一个重载函数删除掉这个节点。

remove的核心实现:

private E remove(Entry<E> e) {  
    if (e == header)  
        throw new NoSuchElementException();  
  
        E result = e.element;  
        e.previous.next = e.next;  
        e.next.previous = e.previous;  
        e.next = e.previous = null;  
        e.element = null;  
        size--;  
        modCount++;  
        return result;  
}  

要删除的节点就是传入的参数e

将e的下一个节点的previous指向e的前一个节点

将e的前一个节点的next指向e的下一个节点

最后将e置为null,就可以完成删除操作了

删除节点操作示意图:


总结:

链表存储的数据在内存中是不连续的,通过previous和next来指向一个节点的前后节点,使得其连起来,形成完整的表结构。

由于其在内存中是连续的,所以其删除、插入操作效率很高,因为只是简单的修改previous和next的值,那么新的节点就被插入了。而ArrayList插入一个新的元素,将使得其他大量数据进行位置移动,消耗比链表高很多。

虽然删除和插入效率高,但链表的查询效率比较低,entry方法就是查询的核心,每次都要进行循环操作,虽然说根据数据位置尽量减少了循环次数,但依然不能避免本质问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容