手写一个双向链表

在这里插入图片描述
本次要实现的链表如图所示,first和last分别指向首部和尾部.该链表首尾不循环.
首先建一个interface定义一些链表应该具有的方法.

package List;

/**
 * @InterfaceName SingalLinkedList
 * @Description TODO
 * @Author lixiang
 * @Date 2020/2/26 11:24
 * @Version 1.0
 */

public interface SingalLinkedList<E> {

    //---------增------------
    boolean add(E data);//尾插数据
    boolean add(int index,E data);//指定位置插入
    void addFirst(E data);//头插
    void addLast(E data);//尾插

    //---------删------------
    E removeFirst();//删除头节点
    E removeLast();//删除尾节点
    boolean remove(Object o);//删除指定元素
    E remove(int index);//删除指定位置元素
    void clear();//清空链表

    //---------改------------
    E set(int index,E element);//修改指定位置数据,并返回旧数据

    //---------查------------
    int size();//获取链表长度
    E getFirst();//获取头数据
    E getLast();//获取尾数据
    E get(int index);//获取指定位置数据
    int indexOf(Object o);//获取指定数据下标
    boolean isEmpty();//判断链表是否为空

}

然后定义节点 每一个节点都包括 ,element (存数据) ,prev(指向前面的节点地址) ,next(指向后面的节点地址)
在这里插入图片描述

//--------------------------------节点---------------------------------------

    private static class Node<E>{
        //数据
        E item;

        //next域
        Node<E> next;

        //prev域
        Node<E> prev;

        //构造方法
        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }


然后定义链表的全局变量,和构造方法
size 容量 ,每次添加或者删除一个节点,size相应的加一或者减一 ,默认是0 即链表没有节点.
first 指向头节点地址,初始化时为null
last 指向尾节点地址,初始化时为null
first和last指针本身也是Node节点类型.注意每次增删节点,first和last都会相应变化,以保证first和last永远指向链表的首尾.

    //容量
    transient int size = 0;

    //头指针
    transient Node<E> first;

    //尾指针
    transient Node<E> last;

    //构造一个空链表
    public MyLinkedList(){

    }

接下来,先写一个添加节点的方法,默认是向尾部添加.
注意添加的时候需要考虑两种情况.
1.该链表刚刚初始化,没有任何节点.此时size是0,first是null,last也是null.
所以,首次添加节点后,first和last应该都指向这个节点.此时这个节点既是头节点也是尾节点.如下图
在这里插入图片描述
该节点的prev和next不指向任何节点,所以应该都为null.
2.该链表已经存在至少一个节点,这时向尾部添加需要将新建的节点跟前一个节点链接上,然后将last指向这个新节点.如下图
在这里插入图片描述
代码如下

//---------------------------------添加----------------------------------------

    /**
     * 添加节点
     * @param e
     * @return
     */
    public boolean add(E e) {
        //默认向尾部添加
        linkLast(e);
        return true;
    }

    public void addLast(E e) {
        linkLast(e);
    }
    /**
     * 尾插法
     * @param e
     */
    private void linkLast(E e) {
        //1.将指针指向尾节点
        final Node<E> l = last;

        //2.新建一个节点(prev指向当前的尾节点l,next指向null)
        Node<E> newNode = new Node<>(l,e,null);

        //3.把新节点的地址给last
        last = newNode;

        //4.判断l指针是否为空,
        // 即之前的尾节点是否为空(初次初始化链表时,调用无参构造,这时给链表 MyLinkedList 分配内存地址(例如0X4192)
        // 但是first和last是null这里需要想明白)
        if (l == null){
            //该链表里没有任何节点,那么就把last和first都指向第一次新建的节点,
            //此时链表里仅有一个节点,它既是头节点也是尾节点
            first = newNode;
        }else {
            //说明链表里已经有节点 last此时已经指向它(看第一步),那就把这个节点的next域赋值 新节点的地址
            l.next = newNode;
        }

        //5.节点连接完成,容量加一
        size++;
    }

头插法的原理类似,代码如下

   /**
     * 头插法
     * @param e
     */
    public void addFirst(E e) {

        linkFirst(e);
    }

    private void linkFirst(E e) {
        //1.把f指针指向first节点
        final Node<E> f = first;

        //2.新建一个节点 prev指向null,next指向当前这个first节点
        Node<E> newNode = new Node<>(null, e, f);

        //3.更新first指针,将新建的这个节点的地址赋给first指针.
        first = newNode;

        //4.判断新建的这个节点是不是链表里的第一个节点,
        //如果是那么就把last指针也指向这个节点
        if (f == null){
            last = newNode;
        }else {
            //在创建这个节点之前链表已经有节点了
            //那就让之前的first节点的prev域指向现在的新节点
            f.prev = newNode;
        }
        //5.容量加一
        size++;

    }


向指定位置插入

    /**
     * 向指定位置插入
     * @param index
     * @param element
     * @return
     */
    public boolean add(int index, E element) {
        checkPositionIndex(index);

        if (index == size){
            //如果向最后一个节点后面添加直接调用尾插法
            linkLast(element);
        }else {
            //向index前面插入
            linkBefore(element,node(index));
        }
        return true;
    }

    /**
     * 向指定位置的前面插入
     * @param element
     * @param succ
     */
    private void linkBefore(E element, Node<E> succ) {
        //1.得到插入位置的前一个节点地址
        final Node<E> pred = succ.prev;

        //2.新建节点
        Node<E> newNode = new Node<>(pred, element, succ);

        //3.插入位置节点的prev 指向新节点
        succ.prev = newNode;

        //4.判断插入位置的前一个节点是否存在
        if (pred == null){
            //如果不存在说明这个新节点就是头节点
            first = newNode;
        }else {
            //否则让这个节点的next指向新节点
            pred.next = newNode;
        }

        //5.容量加一
        size++;

    }




    /**
     * 返回要index位置的节点的地址
     * @param index
     * @return
     */
    Node<E> node(int index) {
        //如果index在左半边从头开始查,否则从尾部开始查 使用右移运算
        if ( index < (size >> 1) ){
            //把指针指向头节点
            Node<E> x = first;
            for (int i = 0; i < index; i++) {
                //向后找
                x = x.next;
            }
            //返回index处节点的地址
            return x;
        }else {
            //把指针指向尾节点
            Node<E> x = last;
            for (int i = size - 1; i > index; i--) {
                //向前找
                x = x.prev;
            }
            return x;
        }
    }

    private void checkPositionIndex(int index) {
        if (index > size || index < 0){
            throw new IndexOutOfBoundsException("下标越界"+index);
        }
    }

    /**
     * 将一个集合加入list
     * @param c
     * @return
     */
    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;
        //如果通过只有一个参数的addAll添加,默认index就是size,
        // 用户也可以直接调用含有2个参数的方法 指定index 和 集合
        if (index == size){
            //在原链表后面插入集合数据
            succ = null;
            pred = last;
        }else {
            //向index位置前插入,所以调用node()方法拿到index位置节点的地址

            //将succ指针指向index位置的节点
            succ = node(index);
            //将pred指针指向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){
                //如果pred是空说明之前的链表index位置上的节点是头节点
                //那么这就相当于头插法 新加入的节点就变成头节点
                first = newNode;
            }else {
                //否则pred指向的这个节点的next指向新节点
                pred.next = newNode;
            }
            //把pred指向这个新加入的节点
            pred = newNode;
        }

        //此时已经把集合里的数据都依次加到了index所指节点的前一个节点的后面
        //接下把index所指节点跟加入后的节点连接到一块
        //可以想象成一列火车,要在这列火车中部插入几节车厢
        if (succ == null){
            last = pred;
        }else {
            pred.next = succ;
            succ.prev = pred;
        }

        size += numNew;

        return true;

    }

修改指定位置的节点

//--------------------------------修改----------------------------------

    /**
     * 修改指定位置的节点
     * @param index
     * @param element
     * @return
     */
    public E set(int index, E element) {
        //检查下标合法性
        checkElementIndex(index);
        //得到index位置节点的地址
        Node<E> x = node(index);
        //保存旧数据
        E oldValue = x.item;
        //更新数据
        x.item = element;
        //返回旧数据
        return oldValue;
    }

    private void checkElementIndex(int index) {
        if(index < 0 || index > size-1 ){
            throw new IndexOutOfBoundsException("下标 "+ index +" 不合法");
        }
    }

查找方法

//-------------------------------------查找--------------------------------------

    /**
     * 查询链表容量
     * @return
     */
    public int size() {
        return size;
    }


    /**
     * 查找头节点
     * @return
     */
    public E getFirst() {
        //将头指针赋值给f
        final Node<E> f = first;
        //2.判断头节点是否为空
        if (f == null){
            throw new NoSuchElementException("头节点为空");
        }
        return f.item;
    }

    /**
     * 查找尾节点数据
     * @return
     */
    public E getLast() {
        //1.将尾指针赋值给l
        final Node<E> l =last;
        if (l ==null){
            throw new NoSuchElementException("尾节点为空");
        }
        return l.item;
    }


    /**
     * 查找指定位置节点
     * @param index
     * @return
     */
    public E get(int index) {
        //1.下标合法性校验
        checkPositionIndex(index);
        //2.返回index位置对应节点的地址
        Node<E> adress = node(index);
        //3.将这个节点里的数据返回
        return adress.item;
    }


    /**
     * 查询数据在哪一个节点里
     * @param o
     * @return
     */
    public int indexOf(Object o) {
        int index = 0;
        //1.链表允许存储null 所以先判断要查的是不是null
        if (o == null){
            for (Node<E> x = first; x != null; x = x.next){
                //x指的是地址
                if (x.item == null){
                    return index;
                }else {
                    index++;
                }
            }
        }else {
            for (Node<E> x = first; x != null; x = x.next){
                //x指的是地址
                if (o.equals(x.item)){
                    return index;
                }else {
                    index++;
                }
            }
        }
        //数据不存在返回-1
        return -1;
    }

    /**
     * 查看链表是否为空
     * @return
     */
    public boolean isEmpty() {

        return size() == 0;
    }

删除方法

//--------------------------------------删除--------------------------------------------


    /**
     * 默认删除头节点
     * @return
     */
    public E remove() {
        return removeFirst();
    }

    /**
     * 删除头节点
     * @return
     */
    public E removeFirst() {
        //1.将f指向头节点
        final Node<E> f = first;
        //2.检查链表是否为空
        // 当只有一个节点的时候,first和last都指向这个节点,当没有节点的时候first和last都是空
        if (f == null){
            throw new NoSuchElementException("没有头节点");
        }
        //3.保存头节点后面节点的地址和数据,因为一会要清除头节点
        Node<E> temp = f.next;
        E oldValue = f.item;
        //4.删除头节点数据
        f.item = null;
        //5.断链,将头节点与后面的节点断开 帮助jvm GC
        f.next = null;
        //6.链表的第二个节点将变成头节点
        // 因此将头指针first指向当前的头节点(即放在temp里的地址)
        first = temp;

        //7.这时需要注意,如果链表只有一个节点,那么这个节点既是头节点也是尾节点
        //当执行完删除后链表就空了,这时last里面的地址还是之前那一个节点的地址,
        // 并没有因为删除节点而更新,因此需要把last也置空

        if (temp == null){
            //temp记录的是头节点后面节点的地址,如果他是null
            //说明链表只有一个节点,这时删除完后,链表为空,所以last也要置空
            last = null;
        }else {
            //否则链表节点个数大于1 那么就可以将当前头节点的prev
            //如果不判断那么当链表只有一个节点时,删除该节点后temp指向null
            //那么执行temp.prev = null;时就会报错
            temp.prev = null;
        }

        size--;
        //7.返回被删的头节点里的数据
        return oldValue;
    }

    public E removeLast() {
        //1.把指针指向尾节点
        Node<E> l = last;
        //2.检查链表是否为空
        if (l == null){
            throw new NoSuchElementException("链表为空");
        }
        //3.链表可能只有一个节点,也可能有多个 所以需要分别处理
        //先记录这个尾节点的数据,以及它前面一个节点的地址,以便于断链
        final E oldValue = l.item;
        final Node<E> temp = l.prev;

        //4.清理掉数据
        l.item = null;
        //5.把尾节点的prev置为null 帮助GC
        l.prev = null;
        //6.把last前移指向新的尾节点,注意当链表只有一个节点时此时temp就是null,last也是null
        last = temp;

        //7.判断该链表是不是只有一个节点,
        // 如果只有一个那么first和last都指向它,删除它之后要把first也置为null
        if (temp == null){
            first = null;
        }else {
            //如果链表超过一个节点,删除完尾节点,
            // 把它前一个节点的next域置为null,然后把last指向它
            temp.next = null;
        }
        size--;
        return oldValue;
    }


    /**
     * 删除指定数据
     * @param o
     * @return
     */
    public boolean remove(Object o) {
        //1.链表可以存null值 先判断o是不是null
        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;
    }

    E unlink(Node<E> x) {
        //记录被删节点信息
        final E element = x.item;
        final Node<E> prev = x.prev;
        final Node<E> next = x.next;

        //判断被删节点是不是首节点
        if (prev == null){
            //如果是头节点,那么要把first指针后移
            first = next;
        }else {
            //如果不是则直接断链,这一步只跟它前面的节点断链
            prev.next = next;
            x.prev = null;
        }

        //判断被删节点是不是尾节点
        if (next == null){
            //如果是尾节点,要把last指针前移
            last = prev;
        }else {
            //如果不是则直接断链,这一步只跟它后面的节点断链
            next.prev = prev;
            x.next = null;
        }

        //清除原节点的data域
        x.item = null;

        size--;

        return element;
    }


    /**
     * 删除指定位置节点
     * @param index
     * @return
     */
    public E remove(int index) {
        //检查index合法性
        checkElementIndex(index);
        //返回index位置节点的地址
        Node<E> node = node(index);
        //断链
        E oldValue = unlink(node);
        return oldValue;
    }

    /**
     * 清空链表
     */
    public void clear() {
        //记录下一个节点的地址
        //清空当前节点
        if (size == 0){
            throw new NoSuchElementException("链表为空");
        }
        //我采用的双指针的方法 从节点的首尾向中间删.jdk8的方法是从头向尾删
        int left = 0;
        int right = size - 1;
        //初始化首尾指针,分别指向首尾节点
        Node<E> leftNode = first;
        Node<E> rightNode = last;
        //只要左右指针没重合就一直删
        while (left <= right){
            //先记录被删节点后面节点的地址,要不然把这个节点删了就找不到它后面的节点了
            Node<E> tempL = leftNode.next;
            leftNode.item = null;
            leftNode.prev = null;
            leftNode.next = null;
            //把之前记录的地址给到leftNode,下次就可以直接删它了
            leftNode = tempL;

            //从后面向中间删的原理相同
            Node<E> tempR = rightNode.prev;
            rightNode.item = null;
            rightNode.next = null;
            rightNode.prev = null;
            rightNode = tempR;

            //双指针向中间移动 直到重合
            left++;
            right--;
        }

        //所有的节点都删除后,把首尾节点的指针置为null
        first = null;
        last = null;

        size = 0;
    }

其他方法

//------------------------------------其他方法------------------------------------
    /**
     * 找到并返回头节点的数据
     * @return
     */
    public E peek() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;
    }

    /**
     *
     * 使用链表做栈 首部插入 首部删除
     * @param e
     */
    public void push(E e) {
        addFirst(e);
    }

    public E pop() {
        return removeFirst();
    }


    /**
     * 返回链表里的所有数据
     * @return
     */
    public Object[] toArray() {
        Object[] result = new Object[size];
        int i = 0;
        for (Node<E> x = first; x != null; x=x.next){
            result[i++] = x.item;
        }
        return result;
    }



    public E peekFirst() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;
    }

    public E peekLast() {
        final Node<E> l = last;
        return (l == null) ? null : l.item;
    }


方法上都有注释,理解起来应该不难,需要源码的可以留言

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值