以CURD的角度手撕LinkedList源码

上文书说到ArrayList的简要源码分析,就不得不提到和它相近的类似的LinkedList,同样都是列表,让我们一起来看看有何相同和不同之处。


talk is cheap,show me the code  ---undefined


老规矩,先来一段代码示例

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

public class LinkedListTest {
    
        public static void main(String[] args) {

            //无参构造函数new一个linklist
            List linklist=new LinkedList();

            //linklist的add操作
            linklist.add("hello world");
            linklist.add("world hello");
            System.out.println("当前linklist的容量大小"+linklist.size());

            //linklist的getter setter操作
            System.out.println("getter"+linklist.get(3));
            System.out.println("setter"+linklist.set(3,"huhu"));
            System.out.println("当前linklist的容量大小"+linklist.size());

            //linklist的addall操作
            linklist.addAll(linklist);
            System.out.println("当前linklist的容量大小"+linklist.size());

            //linklist的index操作
            System.out.println("这是索引"+linklist.indexOf("world hello"));
            System.out.println("这是最后索引"+linklist.lastIndexOf("world"));
            System.out.println("当前linklist的容量大小"+linklist.size());
            System.out.println("是否包含有"+linklist.contains("huhu"));
            System.out.println("是否包含所有"+linklist.containsAll(linklist));
            System.out.println("当前linklist的容量大小"+linklist.size());

            //linklist的remove操作
            System.out.println("删除元素"+linklist.remove("world hello"));
            System.out.println("删除元素"+linklist.remove(4));
            System.out.println("当前linklist的容量大小"+linklist.size());

            //linklist的retain操作
            List linklist2=new LinkedList();
            linklist2.add("hu hu");
            linklist2.add("world hello");
            linklist2.add(1);
            linklist2.add(100);
            linklist2.add(150);


            System.out.println("保留指定集合中的内容"+linklist.retainAll(linklist2));
            System.out.println("删除指定集合中的内容"+linklist.removeAll(linklist2));
            System.out.println("当前linklist的容量大小"+linklist.size());

            LinkedListTest linklistTest=new LinkedListTest();
            linklistTest.linklistop(linklist2);

        }

        public boolean linklistop(List linklist) {
            Iterator iterator = linklist.iterator();
            while (iterator.hasNext()) {
                System.out.println("这是第几个元素 \t " + iterator.next());
            }
            return true;
        }
}

1. 无参构造函数


    public LinkedList() {
    }

可以看到,无参构造函数是空的,即什么也没有做。

注1:和ArrayList无参构造函数不同,由于采用的底层数据结构不同,ArrayList的底层数据结构为数组,因此无参构造在初始化时,就需要提前申请一个数组的空间;而LinkedList采用的是双向链表+双向队列,链表的一个特性就是即用即申请,因此在LinkedList中的无参构造就什么也没有做啦。

Q1:LinkedList有哪些构造函数?分别对应哪种情况?

Q2:为什么LinkedList的底层数据结构是双向链表?为什么不是单向链表?这两种链表有何处异同以及好坏处?

Q3:为什么LinkedList还实现了Deque(队列接口)?实现了双向链表+双向队列的数据存储结构?

2. add函数


    public boolean add(E e) {
        linkLast(e);
        return true;
    }

    void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }

add实现了LinkedList的末尾追加,也就是添加元素的功能,其步骤如下

1. add函数调用了linkLast函数

2. linkLast函数以插入节点数据为形参

2.1 将LinkedList中的last(末尾标志指针,永远指向最后一个元素节点)赋值给l节点

2.2 创建新的节点newNode,节点的前驱指向l节点,节点的后驱设置为空

2.3 将last指向刚刚创建的新节点newNode

2.4 判断l节点是否是空节点,如果是将first(首部指针,永远指向第一个元素节点)指向新创建的newNode节点,如果不是,将l的后驱指向newNode

2.5 LinkedList大小加1

2.6 标志结构性变化的变量加1 

3. 返回真值

注2:很显然,add函数调用了linkLast函数,而linkLast函数采用的是尾插法,也就是说,作为末尾追加元素时,LinkedList采用了尾插法作为元素新建和插入。

Q4:为什么LinkedList的add是末尾追加元素呢?追加元素为什么会采用尾插法,而不是头插法呢?

Q5:前面提到了LinkedList是双向链表,也就说头插法和尾插法都可以?那么什么情况适合头插法?什么情况适合尾插法呢?

3. getter setter函数

getter函数

      public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }

    private void checkElementIndex(int index) {
        if (!isElementIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

   private boolean isElementIndex(int index) {
        return index >= 0 && index < size;
    }

    
    Node<E> node(int index) {
        // assert isElementIndex(index);

        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

1. get函数调用了checkElementIndex函数

2. checkElementIndex函数调用了isElementIndex函数

3. isElementIndex则是简单的对index索引值的判断,判断该值是否合法,如果不合法就抛出IndexOutOfBoundsException异常

4. 合法就继续调用了node函数

5. node函数实现比较有技巧性(重点!请注意一下)

注3:node函数采用了二分法来循环遍历来查找所需要的index上的元素值

5.1 在if(index<(size>>1))判断语句中,可以看见了判断是从size右移了一位除以2,即判断了index是否小于size的一半,如果是从前往后遍历,根据后驱慢慢查找,直到处于索引的位置

5.2 如果index大于size的一半,则是从后往前遍历,根据前驱慢慢查找,直到处于索引的位置

Q6:关于node节点的查找函数为什么是二分法进行查找?在二分法中为什么又采用了位运算?

Q7:node节点的查找遍历的时间复杂度如何?

setter函数

      public E set(int index, E element) {
        checkElementIndex(index);
        Node<E> x = node(index);
        E oldVal = x.item;
        x.item = element;
        return oldVal;
    }

1. 先校验索引值是否合法

2. 合法就调用了node函数

3. node函数其实就是在上文getter方法中也出现过的node函数,这个函数实际上就是利用了二分法来查找对应index位置上的node节点

4. 将该位置的节点作为旧值返回

5. 设置传入的新值

注4:值得注意的是,LinkedList和ArrayList不同,其索引值不能同数组下标一样,直接返回(实际上ArrayList是随机访问,LinkedList则是顺序访问),因此需要设立循环,根据前驱或者后驱,循环遍历直到该索引值的节点处,因此,时间复杂度就是O(n),比起ArrayList的O(1)要大一些。

4. addAll函数

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


   public boolean addAll(int index, Collection<? extends E> c) {
        checkPositionIndex(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        if (numNew == 0)
            return false;

        Node<E> pred, succ;
        if (index == size) {
            succ = null;
            pred = last;
        } else {
            succ = node(index);
            pred = succ.prev;
        }

        for (Object o : a) {
            @SuppressWarnings("unchecked") E e = (E) o;
            Node<E> newNode = new Node<>(pred, e, null);
            if (pred == null)
                first = newNode;
            else
                pred.next = newNode;
            pred = newNode;
        }

        if (succ == null) {
            last = pred;
        } else {
            pred.next = succ;
            succ.prev = pred;
        }

        size += numNew;
        modCount++;
        return true;
    }

    
     private void checkPositionIndex(int index) {
        if (!isPositionIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }


       private boolean isPositionIndex(int index) {
        return index >= 0 && index <= size;
    }

针对addAll函数所实现的功能大概都有这么几个情况

1. 原列表为空,没有任何节点,传入的集合节点,直接添加进去

2. 原列表有节点,在节点末尾追加传入的集合节点,这是addAll传入参数collection的情况

3. 原列表有节点,在指定的index位置上追加传入的集合节点,这是allAll传入index,collection的情况

可以看见addAll(collection)也是调用了addAll(index,collection),换句话说,addAll主要依靠addAll(index,collection)实现

那么我们来看下这个函数的实现

1. 首先是调用了checkPositionIndex函数针对index所处位置合法性的校验,如果不合法就要抛出异常

2. 将集合中元素转为数组

3. 对数组大小进行判断,如果数组是空的,就返回false,否则就下一步

4. 设立两个节点,一个节点pred指向要添加节点的前一个节点,一个节点succ指向要添加节点的位置

5. 如果要添加index的位置等于大小,就说明了是在末尾处添加(情况3),将要succ指向null,pred指向原列表的最后一个节点;如果不是,则将succ指向要添加的节点的位置(实际上是在添加的节点的前一个节点处),pred指向该节点的前驱节点(情况2)

6. 循环遍历数组,遍历一个,就创建一个节点,使用尾插法在要添加的节点处进行链接,此处需要对pred是否为空进行判断,如果为空(情况1),就指向新创建的节点,如果不是,则将pred后驱指向新节点

7. 如果succ是null,则将last指针指向添加链接完成后的pred节点

8. 如果不是,则将上面的pred的后驱指向succ,然后succ的前驱指向pred

9. 改变大小,modCount++,返回真值

注5:针对批量添加的情况,比较复杂,建议读者们手动画图模拟一下插入的过程

Q8:为什么会将集合元素要先转为数组,再遍历创建新节点进行链接呢?和直接在集合中遍历创建节点链接有什么异同?

Q9:设立pred,succ两个临时节点指针是否多余?

5. indexOf、lastindexOf、contains、containsAll函数

关于indexOf的函数实现如下

    public int indexOf(Object o) {
        int index = 0;
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null)
                    return index;
                index++;
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item))
                    return index;
                index++;
            }
        }
        return -1;
    }

可以看出,步骤都比较简单

1. 设立index为0

2. 对object进行判空

3. 如果空值,从first节点开始,按照后驱遍历查找为空值的节点,如果找到了,就返回该节点的index

4. 如果非空,也同样的是从firs节点开始,按照后驱遍历查找为空值的节点,如果找到了,就返回该节点的index

5. 都没找到的,就返回-1

注6:针对空值和非空值的查找,其查找方式都是一样的,只是在对比值的时候,由于空值的特殊性只能采用==来判断,而非空值可以直接equals来判断

关于lastIndexOf函数实现

        public int lastIndexOf(Object o) {
        int index = size;
        if (o == null) {
            for (Node<E> x = last; x != null; x = x.prev) {
                index--;
                if (x.item == null)
                    return index;
            }
        } else {
            for (Node<E> x = last; x != null; x = x.prev) {
                index--;
                if (o.equals(x.item))
                    return index;
            }
        }
        return -1;
    }

同样可以看出,跟indexOf的实现方式是一样的,只不过遍历是从后驱开始慢慢查找的

再看看contains和containsAll函数实现

  public boolean contains(Object o) {
        return indexOf(o) != -1;
    }

//AbstractCollection中的实现
    public boolean containsAll(Collection<?> c) {
        for (Object e : c)
            if (!contains(e))
                return false;
        return true;
    }

很显然,contains调用了indexOf的方法,即其实现也是用indexOf实现的,将indexOf返回跟-1进行判断就可以得出元素是否在列表中的结果了,然后返回结果

注7:需要注意的是,containsAll的实现在LinkedList中并没有,取而代之则是AbstractCollection中的containsAll实现,而AbstractCollection中的containsAll实现也是循环调用了contains方法

Q10:不知道为什么在LinkedList中没有实现containsAll的方法?

6. remove函数

      public boolean remove(Object o) {
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    unlink(x);
                    return true;
                }
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    }

    public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }


   E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;

        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }

        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }

        x.item = null;
        size--;
        modCount++;
        return element;
    }

remove重载了两个函数,一个是参数是index,一个参数是object,一个是根据index移除节点,一个是根据object移除节点

根据index索引来移除节点的函数实现中要先对index进行合法性检查,再调用了unlink函数

根据object移除节点要对object是否为空值进行判断,采取不同的判断方式,遍历查找到要移除的节点位置,再调用unlink函数

重点来了,我们需要看看unlink是怎么移除节点的

1. 设立要返回的元素为该节点的元素值,设立prev为该节点的前驱节点,next为该节点的后驱节点

2. 对前驱节点prev进行判空,说明是否是首节点,如果不是首节点,则将前驱的后驱节点指向next的节点,然后把该节点的前驱置空

3. 对后驱节点next进行判空,说明是否是尾节点,如果不是尾节点,则将后驱的前驱节点指向prev节点,然后把该节点的后驱置空

4. 节点上的元素置空,大小减一,mouCount加一,返回删除节点的元素值

7. retainAll、removeAll函数

 public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        boolean modified = false;
        Iterator<?> it = iterator();
        while (it.hasNext()) {
            if (c.contains(it.next())) {
                it.remove();
                modified = true;
            }
        }
        return modified;
    }

    public boolean retainAll(Collection<?> c) {
        Objects.requireNonNull(c);
        boolean modified = false;
        Iterator<E> it = iterator();
        while (it.hasNext()) {
            if (!c.contains(it.next())) {
                it.remove();
                modified = true;
            }
        }
        return modified;
    }

可以看出,removeAll和retainAll其实现逻辑基本都是一样的

1. 调用requireNonNull对集合是否为空进行判断

2. 把修改标志modified设置为false

3. 取得迭代器iterator

4. 迭代器循环获取下一个节点元素

5. removeAll是针对集合中存在元素进行判断,然后调用remove进行删除;retainAll则是针对集合中非存在的元素进行判断,不存在就调用remove删除

6. 返回modified

Q11:为什么retainAll、removeAll使用了迭代器进行删除或者保留?和ArrayList有什么异同?

8. 其他常用的函数

 

9. 迭代器

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章