利用Java手写LinkedList

Posted F3nGaoXS

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了利用Java手写LinkedList相关的知识,希望对你有一定的参考价值。

利用Java手写LinkedList

和ArrayList不同的是,LinkedList是采用链表实现的,链表的特点就是每个节点存储的是value和下个节点的地址,所以不存在类似ArrayList的扩容问题,添加节点只需要一个新的节点对象然后链表末尾指向它就可以了。参考Java官方的LinkedList实现:java.util.LinkedList。不过Java官方使用双向链表实现。

链表和节点

链表有n多个链表节点组成,每个节点存储的都是元素+下个节点的内存地址。如何得到节点中存储的元素实际上是通过从链表的头一个节点开始迭代,然后得到当前节点。

私有属性

动态数组大小size,以及用于存放该动态数组的头节点first

注意:为什么要有头节点?

是因为链表元素的获取是利用迭代来不断得到next元素的,也就是说你迭代到了中间某一元素后,你就没有办法回溯了,你这个链表就暂时停留在这里了(因为该元素前的所以链表的内存地址已丢失),所以就需要保存一个头节点的内存地址,以确保每次都能够从头开始依次访问链表中的元素

public class LinkedList<E> 

    /**
     * 动态数组大小
     */
    private int size;

    /**
     * 头节点
     */
    private Node<E> first;
    
	

节点类

通过静态内部类的方式来声明链表中的节点Node类

public class LinkedList<E> 

    /**
     * 动态数组大小
     */
    private int size;

    /**
     * 头节点
     */
    private Node<E> first;
    
    /**
     * 静态内部类
     */
    private static class Node<E>

        /**
         * 该节点存储的元素
         */
        private E element;

        /**
         * 该节点所指向的下一个节点
         */
        private Node<E> next;

        public Node(E element, Node<E> next) 
            this.element = element;
            this.next = next;
        
    
    
	

Node类主要包含节点存储的具体元素、下个节点的内存地址,以及节点的构造方法(指定元素,以及指定下一个节点)。

通过下标获得具体node节点

链表与数组不同的是,它是利用迭代来定位到具体某个节点的,再很多add、remove的场景中我们可能会经常需要定位到某个具体的节点(因为需要获取该节点前后的节点)

	/**
     * 获取具体某一下标的节点
     * @param index      下标
     * @return           该下标的node节点
     */
    private Node<E> node(int index)
        Node<E> node = first;
        for (int i = 0; i < index; i++) 
            node = node.next;
        
        return node;
    

可以看到上述方法就是通过for循环的方式,node.next不断迭代获得node节点,最后获取到位于index位置的node元素。

构造方法

LinkedList不同于ArrayList,因为它不需要初始化数组容量,所以它没有构造函数。

基本方法

和ArrayList类似,LinkedList具有以下基本方法

public class LinkedList<E> 


    /**
     * 动态数组大小
     * @return      动态数组大小
     */
    public int size()

    /**
     * 动态数组是否为空
     * @return      动态数组是否为空
     */
    public Boolean isEmpty()

    /**
     * 添加元素
     * @param element   元素
     */
    public void add(E element)

    /**
     *
     *  0 1 2 3 4 5 6 7 8 9
     *  1 2 3   4 5 6 7 8
     * 向指定位置添加元素
     * @param index     位置
     * @param element   元素
     */
    public void add(int index,E element)

    /**
     *
     *  0 1 2 3 4 5 6 7 8 9
     *  a b c 1 d e f g h
     * 移除指定位置的元素
     * @param index     位置
     */
    public void remove(int index)

    /**
     * 删除指定元素
     * @param element   元素
     */
    public void remove(E element)

    /**
     * 清空动态数组中的元素
     */
    public void clear()

    /**
     * 修改指定位置的元素
     * @param index      位置
     * @param element    元素
     */
    public void set(int index,E element)

    /**
     * 获得指定位置的元素
     * @param index     位置
     * @return          元素
     */
    public E get(int index)

    /**
     * 判断数组是否包含该元素
     * @param element    元素
     * @return           true包含,false不包含
     */
    public Boolean contains(E element)

    /**
     * 该元素第一次出现的下标
     * @param element    元素
     * @return           下标
     */
    public int indexOf(E element)

    @Override
    public String toString() 


size()

返回动态数组的大小

public int size()  
    return size; 

isEmpty()

返回该动态数组是否为空,即判断size是否为0

public boolean isEmpty()  
    return size == 0; 

toString()

打印该链表,依旧是从头节点开始迭代然后打印

	@Override
    public String toString() 
        StringBuilder string = new StringBuilder();
        string.append("LinkedList");
        string.append("size=" + size + ", elements=[");
        Node<E> node = first;
        for (int i = 0; i < size; i++) 
            string.append(node.element);
            node = node.next;
            if (i!=size-1)
                string.append(", ");
            
        
        string.append("]");
        string.append("");
        return string.toString();
    

indexOf()和contains()

indexOf(E element)就是返回该元素所在的位置下标,如果不存在则返回-1。同样地,利用从first开始迭代node,如果节点的元素等于传入的元素则直接返回该下标。

    @Override
    public int indexOf(E element) 
        Node<E> node = first;
        for (int i = 0; i < size; i++) 
            if ( (node.element).equals(element) ) return i;
            node = node.next;
        
        return -1;
    

contains(E element)就是判断链表是否包含该元素,如果包含则返回true。

	@Override
    public boolean contains(E element) 
        return indexOf(element) >= 0;
    

get()

通过节点下标获得该节点

	@Override
    public E get(int index) 
        return node(index).element;
    

set()

修改某位置节点的元素

	@Override
    public void set(int index, E element) 
        node(index).element = element;
    

add()

链表添加元素。链表添加元素的原理大致是:

  1. 获取该下标位置的前一个节点(目的是拿到该下标位置的地址)
  2. 创建一个新的Node节点,将当前位置的地址赋值给该Node节点,同时插入的元素也赋值给该节点
  3. 前一个节点next指向新创建的Node节点。
  4. 这样原先的前一个节点的next指向新创建的节点,新创建的节点指向原本该下标的节点,这样就完成了插入。最后size++

步骤图:

  1. 原链表

  2. 执行插入

@Override
    public void add(int index, E element) 
        if (index == 0)
            first = new Node<>(element, first);
         else 
            Node<E> pNode = node(index - 1);             //当前index的前一个节点
            pNode.next = new Node<>(element, pNode.next);      //当前节点的下一个节点赋给待添加的节点
        
        size++;
    

**注意:**需要特别注意的就是,如果向第一个位置进行插入,那么利用上述的node(int index)方法是没有办法获取到头节点的前一个节点的(因为不存在)。所以如果是往index为0的位置插入需要额外写方法。逻辑就是直接将first指向新创建的node节点就可以了,同时将原有的first指向存入新创建的node节点。

remove()

链表移除元素。原理同样是改变元素指向:

  1. 获取到移除元素的前一个节点,命名为pNode
  2. 将要移除元素的next赋值给pNode.next
  3. 这样pNode直接指向要移除元素的下一个元素。最后size–

步骤图:

  1. 原链表

  2. 执行删除

	@Override
    public void remove(int index) 
        if (index == 0) 
            first = first.next;
         else 
            Node<E> pNode = node(index - 1);
            Node<E> node = pNode.next;
            pNode.next = node.next;
        
        size--;
    

pNode直接指向当前node.next后,当前node没有被指向了,那么它就会被Java的垃圾回收机制给自动清理掉。同样需要注意的是,如果需要删除第一个元素,你同样也是没办法获取到first的。所以也是需要额外处理,直接将当前的first指向first.next,那么原来first指向的节点也会被Java垃圾回收机制给清理掉。

clear()

清空数组的所有元素,直接置size为0,并且first指向null就可以了,因为first指向null,所以first以及first后面所指向的所有节点都会被销毁。

	@Override
    public void clear() 
        size = 0;
        first = null;
    

下标越界

和ArrayList同样的,在执行add或者remove前都需要检查一下传入的index是否在范围内。

注意

add()和remove()、get()、set()的index范围有所区别。添加元素允许往动态数组的最后一个位置添加元素,所以index是可以访问到size的,但是删除、查询和修改都是在元素已经在动态数组中存在的基础上,所以index是不可以访问到size的,切记!!!

checkIndex()

检查非添加时的下标index,不能小于0或者不能大于等于size(0<index<size)

private void checkIndex(int index)
    if (index<0||index>=size) indexOutOfBoundsException(index);

checkIndexAdd()

检查添加时下标index,不能小于0或者不能大于size(0<index ⩽ \\leqslant size)

private void checkIndexAdd(int index)
    if (index<0||index>size) indexOutOfBoundsException(index);

indexOutOfBoundsException()

下标越界时抛出的自定义异常

private void indexOutOfBoundsException(int index)
    throw new IndexOutOfBoundsException("index="+index+", size="+size);

List接口

在很多时候一种思想可以用不同的解决办法来实现,比如动态数组就可以利用底层分别是数组和链表的ArrayList和LinkedList来实现,他们需要实现的方法是一样的,但是方法内部的实现是不一样的,所以就可以为他们共同指定一个**“标准”,这个标准就是接口**。接口里只定义需要实现的方法(返回值类型、传入的参数、方法名),接口不需要关心方法如何具体实现,只需要指定方法就可以了,这样不同的解决方法就可以有不同的具体实现,虽然是实现同一个方法。就类似公司领导给两个人下达了同一任务,只告诉了这个任务需要什么,并且要得到什么,但是他不关心具体实现过程,由员工来按照要求进行实现。不同员工实现该任务肯定各有优劣,领导就会根据他想要的目的来选用哪个具体的解决方案。

那么List接口应该有如下方法:

public interface List<E> 

    /**
     * 动态数组大小
     * @return      动态数组大小
     */
    int size();

    /**
     * 动态数组是否为空
     * @return      动态数组是否为空
     */
    boolean isEmpty();

    /**
     * 添加元素
     * @param element   元素
     */
    void add(E element);

    /**
     *
     *  0 1 2 3 4 5 6 7 8 9
     *  1 2 3   4 5 6 7 8
     * 向指定位置添加元素
     * @param index     位置
     * @param element   元素
     */
    void add(int index,E element);

    /**
     *
     *  0 1 2 3 4 5 6 7 8 9
     *  a b c 1 d e f g h
     * 移除指定位置的元素
     * @param index     位置
     */
    void remove(int index);

    /**
     * 删除指定元素
     * @param element   元素
     */
    void remove(E element);

    /**
     * 清空动态数组中的元素
     */
    void clear();

    /**
     * 修改指定位置的元素
     * @param index      位置
     * @param element    元素
     */
    void set(int index,E element);

    /**
     * 获得指定位置的元素
     * @param index     位置
     * @return          元素
     */
    E get(int index);

    /**
     * 判断数组是否包含该元素
     * @param element    元素
     * @return           true包含,false不包含
     */
    boolean contains(E element);

    /**
     * 该元素第一次出现的下标
     * @param element    元素
     * @return           下标
     */
    int indexOf(E element);


AbstractList抽象类

如果说接口是严格的标准,那么抽象类就是宽松一点的标准,抽象类里可以实现接口,甚至可以预先实现部分方法。通常抽象类实现接口后可以将可能会用到的重复代码或者方法或者属性抽离到抽象类中。

比如ArrayList和LinkedList都需要用到获取动态数组大小和判断动态数组是否为空,那么就可以将这两个方法抽离出来,以及判断index下标是否越界也是可以抽离的。那么AbstractList抽象类如下:

public abstract class AbstractList<E> implements List<E> 

    /**
     * list大小
     */
    protected int size;

    public int size()  return size; 

    public boolean isEmpty()  return size == 0; 

    /**
     * index超出范围后的报错信息
     */
    private void indexOutOfBoundsException(int index)
        throw new IndexOutOfBoundsException("index="+index+", size="+size);
    

    /**
     * 检查删改查元素的index
     */
    protected void checkIndex(int index)
        if (index<0 || index>=size) indexOutOfBoundsException(index);
    

    /**
     * 检查增元素的index
     */
    protected void checkIndexForAdd(int index)
        if (index<0 || index>size) indexOutOfBoundsException(index);
    


需要特别注意方法和属性的访问范围限制,protected是当前类和子类都能访问,private是只能当前类访问。

所以List接口、AbstractList抽象类、ArrayList和LinkedList类的继承关系如下:

内存或时间消耗

ArrayList底层就是数组,数组的存储空间是一串连续的内存地址,是比较省空间的。在add或者remove中间的元素需要不断移动后面的元素,存储空间存储的内存地址不断被重新赋值。往数组末尾添加或者删除元素的时直接加在数组尾或者移除数组尾元素,是不需要移动任何元素的。扩容的时候需要请求内存并且重新循环给数组赋值。如果数组没有被元素填满的话,可能存在浪费。

LinkedList底层是链表,每个节点存储的是元素和下一个节点的内存地址,内存地址是散乱的,在add或者remove的时候只需要改变节点存储的下一个节点的指向就可以了,比较方便,并且不需要扩容,即加一个元素就直接加一个元素,移除一个元素就移除一个元素。

总结

public class LinkedList<E> extends AbstractList<E>

    /**
     * 头节点
     */
    private Node<E> first;

    /**
     * 静态内部类
     */
    private static class Node<E>

        /**
         * 该节点存储的元素
         */
        private E element;

        /**
         * 该节点所指向的下一个节点
         */
        private Node<E> next;

        public Node(E element, Node<E> next) 
            this.element = element;
            this.next = next;
        
    

    /**
     * 获取具体某一下标的节点
     * @param index      下标
     * @return           该下标的node节点
     */
    private Node<E> node(int index)
        Node<E> node = first;
        for (int i = 0; i < index; i++) 
            node = node.next;
        
        return node;
    

    @Override
    public void add(E element) 
        add(size,element);
    

    @Override
    public void add(int index, E element) 
        checkIndexForAdd(index);
        if (index == 0)
            first = new Node<>(element, first);
         else 
            Node<E> pNode = node(index - 1);             //当前index的前一个节点
            pNode.next = new Node<>(element, pNode.next);      //当前节点的下一个节点赋给待添加的节点
        
        size++;
    

    @Override
    public void remove(int index) 
        checkIndex(index);
        if (index == 0) 
            first = first.next;
         else 
            Node<E> pNode = node(index - 1);
            Node<E> node = pNode.next;
            pNode.next = node.next;
        
        size--;
    

    @Override
    public void remove(E element) 
        remove(indexOf(element));
    

    @Override
    public void clear() 
        size = 0;
        first = null;
    

    @Override
    public void set(int index, E element) 
        checkIndex(index);
        node(index).element = element;
    

    @Override
    public E get(int index) 
        checkIndex(index);
        return node(index).element;
    

    @Override
    public boolean contains(E element) 
        return indexOf(element) >= 0;

以上是关于利用Java手写LinkedList的主要内容,如果未能解决你的问题,请参考以下文章

恋上数据结构 链表(手写LinkedList+练习)

手写LinkedList

面试总结

Java入门系列之集合LinkedList入门

手写集合框架LinkedList实现篇

3 手写Java HashMap核心源码