LinkedList的深入探究And手写实现LinkedList

什么是Linked List?

概述

  • LinkedList和前面讲到的ArrayList都实现了List接口,但是各自实现的方式不同,ArrayList是基于动态数组随机访问实现的,访问效率高,直接通过下标即可获得值。而LinkedList是基于链表实现的一种数据结构,每一个数据都是一个节点Node,他们通过指针连在一起,插入和删除效率极高,但是查询效率却不如Array List。
  • 在严老师的数据结果里面,单链表是这样定义的:用一组任意的存储单元存储线性表的数据元素。数据是通过结点连接起来的。这个结点包括两部分,一部分是数据,另一部分是后继元素的指针。
  • 其实很好理解,将链表看出车链子的结点,结点这么连起来就构成了链表。然后就是在上面增加一个结点,替换节点,查询节点,删除节点等操作了。LinkedList是使用的双链表来实现的,直接看源码不是那么容易理解。我们先来看看自己用单链表实现的LinkeList,搞清楚 单链表的 节点的增删改查之后 ,再来看看双链表。

1.1自己的LinkedList

  • 先看看节点Node的结构
  private class Node{
        public E e;
        public Node next;//指向下一个元素,即指针
       /**
       *构造一个结点
       */
        public Node(E e ,Node next){
            this.e=e;
            this.next=next;
        }
        public Node(E e){
            this(e,null);
        }
        public Node(){
            this(null,null);
        }

        @Override
        public String toString() {
            return e.toString();
        }
    }
  • LinkedList这里只有两个属性 一个是Node一个是size
    //设置为虚拟头节点  所谓虚拟头节点其实就是链表存有数据的第一个结点(头节点)的前一个结点(有点绕)
    private Node dummyHead;
    private int size;
  • 构造函数
  public LinkList(){
        //这个虚拟头节点 初始化就是个空节点
        dummyHead=new Node(null,null);
        size=0;
    }
   //获取链表个数
    public int getSize(){
        return size;
    }
    //链表是否为空
    public boolean isEmpty(){
        return size==0;
    }

1.2常规增删改查

  • add(int index,E e):根据索引添加元素e
  public void add(int index,E e){
  //判断索引是否合理,  一般来说 这个判断封装成一个方面比较好,因为可以实现代码复用。
        if (index<0||index>size)
            throw new IllegalArgumentException("Add Failed,Illegal index");
            //prev先指向头节点
            Node prev=dummyHead;
            //找到要插入的位置的前一个节点
            for (int i=0;i<index;i++)
                //prev指向了下一个节点
                prev=prev.next;
              //1
//            Node node =new Node(e);
            node.next=prev.next;
//           prev.next=node;
        //2
            prev.next=new Node(e,prev.next);

            //1和2 是等价的
            size++;

    }
  • 这里有个地方是比较有意思和值得思考的:
    • Node prev=dummyHead;:这句话其实是prev拿到了dummyHead的引用。有思考过为什么我们对prev进行操作却给dummyHead增加了节点呢?
    • 看看这个图
      在这里插入图片描述
  • 插入添加操作就是这样的。实在不清楚的可以去看一下这个java四种引用方式
  • 后续的增加操作都是继续节点的插入操作
  • addLast()和addFrist():在头部或者尾部插入节点
  //在链表头添加新的元素e
    public void addFirst(E e){
//        Node node=new Node(e);
//        node.next=head;
//        head=node;
        add(0,e);
    }
    //在链表末尾添加新的元素
    public void addLast(E e ){
        add(size,e);
    }
  • 查询操作,非常简单之间看代码
 //获得链表的第index个位置的元素
    public E get(int index){
        if (index<0||index>=size)
            throw new IllegalArgumentException("index is Illegal");
        //当前节点
        Node cur=dummyHead.next;
        for (int i=0;i<index;i++)
            cur=cur.next;
        return cur.e;
    }
    //获得链表第一个元素
    public E getFirst(){
       return get(0);
    }
    //获得最后一个元素
    public E getLast(){
        return get(size-1);
    }
  • 删除操作
  //删除一个节点
    public E remove(int index){
        if (index<0||index>=size)
            throw new IllegalArgumentException(" set failed ;index is Illegal");
        Node pre=dummyHead;
        for (int i=0;i<index;i++)
            pre=pre.next;
            //pre表示待删除结点的前一个结点.
            //待删除结点记为retNode
        Node retNode=pre.next;
        //前一个结点与待删除结点的一个结点相连,这样就将 待删除结点retNode与链表断开联系
        pre.next=retNode.next;
        //retNode.next不再指向链表中的结点而是为空  等待被GC回收
        retNode.next=null;
        //链表数量减一
        size--;
        return retNode.e;
    }
    //删除第一个元素
    public E removeFirst(){
        return remove(0);
    }
    //删除最后一个元素
    public E removeLast(){
        return  remove(size-1);
    }
  • 修改操作 set(int index,E e)
 //修改链表的第index 个的元素为e
    public void set(int index,E e){
        if (index<0||index>=size)
            throw new IllegalArgumentException(" set failed ;index is Illegal");
        Node cur=dummyHead.next;
        for(int i=0;i<index;i++)
            cur=cur.next;
         //找到待修改结点之后 重新赋值即可
        cur.e =e;
    }

到这里我们自己简化版的LinkeList就差不多完成了,还是附上完整代码 供参考

public class LinkList<E> {
    //节点 私有内部类
    private class Node{
        public E e;
        public Node next;//下一个节点

        public Node(E e ,Node next){
            this.e=e;
            this.next=next;
        }
        public Node(E e){
            this(e,null);
        }
        public Node(){
            this(null,null);
        }

        @Override
        public String toString() {
            return e.toString();
        }
    }
    //设置为虚拟头节点
    private Node dummyHead;
    private int size;
    public LinkList(){
        dummyHead=new Node(null,null);
        size=0;
    }
    //获取链表个数
    public int getSize(){
        return size;
    }
    //链表是否为空
    public boolean isEmpty(){
        return size==0;
    }

    /**
     *  在链表index位置添加元素e
     */

    public void add(int index,E e){
        if (index<0||index>size)
            throw new IllegalArgumentException("Add Failed,Illegal index");
            //prev先指向头节点
            Node prev=dummyHead;
            //找到要插入的位置的前一个节点
            for (int i=0;i<index;i++)
                //prev指向了下一个节点
                prev=prev.next;
              //1
//            Node node =new Node(e);
            node.next=prev.next;
//           prev.next=node;
        //2
            prev.next=new Node(e,prev.next);

            //1和2 是等价的
            size++;

    }
    //在链表头添加新的元素e
    public void addFirst(E e){
//        Node node=new Node(e);
//        node.next=head;
//        head=node;
        add(0,e);
    }
    //在链表末尾添加新的元素
    public void addLast(E e ){
        add(size,e);
    }
    //获得链表的第index个位置的元素
    public E get(int index){
        if (index<0||index>=size)
            throw new IllegalArgumentException("index is Illegal");
        //当前节点
        Node cur=dummyHead.next;
        for (int i=0;i<index;i++)
            cur=cur.next;
        return cur.e;
    }
    //获得链表第一个元素
    public E getFirst(){
       return get(0);
    }
    //获得最后一个元素
    public E getLast(){
        return get(size-1);
    }
    //修改链表的第index 个的元素为e
    public void set(int index,E e){
        if (index<0||index>=size)
            throw new IllegalArgumentException(" set failed ;index is Illegal");
        Node cur=dummyHead.next;
        for(int i=0;i<index;i++)
            cur=cur.next;
        cur.e =e;
    }
    //查找链表中是否有元素e
    public boolean contains(E e){
        Node cur=dummyHead.next;
        while (cur!=null) {
            if (cur.e.equals(e))
                return true;
            cur = cur.next;
        }
        return false;
    }
    //删除一个节点
    public E remove(int index){
        if (index<0||index>=size)
            throw new IllegalArgumentException(" set failed ;index is Illegal");
        Node pre=dummyHead;
        for (int i=0;i<index;i++)
            pre=pre.next;
        Node retNode=pre.next;
        pre.next=retNode.next;
        retNode.next=null;
        size--;
        return retNode.e;
    }
    //删除第一个元素
    public E removeFirst(){
        return remove(0);
    }
    //删除最后一个元素
    public E removeLast(){
        return  remove(size-1);
    }

    @Override
    public String toString() {
        StringBuilder res=new StringBuilder();
        Node cur=dummyHead.next;
        while (cur!=null){
            res.append(cur+"->");
            cur=cur.next;
        }
        res.append("NULL");
        return res.toString();
    }
}

开始吃荤菜了 上源码!

2.1 LinkedListd 继承与实现

public class LinkedList<E>
   extends AbstractSequentialList<E>
   implements List<E>, Deque<E>, Cloneable, java.io.Serializable

-实现了List ,Deque,克隆和序列化。继承了 AbstractSequentialList<E>:提供了 List 接口的骨干实现,从而最大限度地减少了实现受“连续访问”数据存储(如链接列表)支持的此接口所需的工作,从而以减少实现List接口的复杂度。
Deque一个线性 collection,支持在两端插入和移除元素,定义了双端队列的操作。
Serializable:序列化接口,这个接口什么东西都没有,就是用于标识实现改接口的类是可以序列化的。

public interface Serializable {
}
  • 这里有个面试常问的问题 :什么是序列化和反序列化呢?

    • 回答:Java 序列化就是指将对象转换为字节序列的过程,而反序列化则是只将字节序列转换成目标对象的过程。(类似于你在网上买了一个女朋友,为了方便女朋友的运输肯定要将起打包处理,这就是序列化,你把包裹拆开 取出女朋友(反序列化))
    • 追问:为什么我们要序列化呢?
      • 回答: 把的内存中的对象状态保存到一个文件中或者数据库中时候;
        用套接字在网络上传送对象的时候;
        通过RMI传输对象的时候;

        seriallization 序列化 : 将对象转化为便于传输的格式, 常见的序列化格式:二进制格式,字节数组 json字符串(现在前后端交互就是用json来传输数据的) xml字符串。
        de seriallization 反序列化:将序列化的数据恢复为对象的过程。

    2.2 属性

    • 属性有 链表数量size,第一个节点first和最后一个节点last
    transient int size = 0;
    
      /**
       * Pointer to first node.
       * Invariant: (first == null && last == null) ||
       *            (first.prev == null && first.item != null)
       */
      transient Node<E> first;
    
      /**
       * Pointer to last node.
       * Invariant: (first == null && last == null) ||
       *            (last.next == null && last.item != null)
       */
      transient Node<E> last;
    
    
    • 来看看Node这个东西
    private static class Node<E> {
          E item;
          //下一个结点
          Node<E> next;
          //上一个节点
          Node<E> prev;
         //构造函数
          Node(Node<E> prev, E element, Node<E> next) {
              this.item = element;
              this.next = next;
              this.prev = prev;
          }
      }
    
    • LinkedList在java里面是实现的双链表,相对于我们刚刚的单链表,也只是复杂了一点点。

2.3构造函数

  public LinkedList() {
    }

    /**
     构造一个包含指定集合中的元素,他们的顺序是由集合的迭代器返回一个列表。.
     *
     * @param  c 其元素将被放置在此列表中的集合
     * @throws NullPointerException if the specified collection is null
     */
    public LinkedList(Collection<? extends E> c) {
        this();
        //调用了封装了的方法。 我们每次阅读源码的时候总能够看到许多 封装 看的多了,我们自然慢慢就明白了 什么时候要封装一下, 提升代码的质量
        addAll(c);
    }

2.4 依旧 增删改查

  • 增加 add(E e):向尾部插入一个元素
 public boolean add(E e) {
 //你看 是不是又封装了  ,到就可以在哪里需要 就在哪里调用即可
        linkLast(e);
        return true;
    }
    //这个方法对外是不可见的
    //链接Ë作为最后一个元素
    void linkLast(E e) {
        //l拿到最后一个节点的引用
        final Node<E> l = last;
        //新的节点直接接到l后面,
        final Node<E> newNode = new Node<>(l, e, null);
          //last移动到newNode的位置
        last = newNode;
        /******这一步并不是特别好理解 我将其拆开看看
        //生成一个 普通节点
        Node node=new Node(null,e,null);
        //将这个节点插入到last这个尾巴节点
        //node先和前一个结点接上
        node.pre=l.pre;
        //前一个的结点不再指向l 而是指向了node
        l.pre.next=node;
        //node的下一个结点指向了l
        node.next=l;
        //l的前一个结点再指向node
        l.pre=node;
        //*****/上面这四步其实就是等于 源代码两句代码的结果。
        /*构造函数
      Node(Node<E> prev, E element, Node<E> next) {
          this.item = element;
          this.next = next;
          this.prev = prev;
      }*/
      
        //双端队列 首位相接
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        //链表节点数量加一
        size++;
        modCount++;
    }
  • 同学再看这个的时候 最好拿笔把这个过程画一遍,加深影响。其实我们在做链表或者数相关的算法题时,都是基于结点的操作,把单链表和双向链表的结点的插入和删除操作搞明白之后,做那些操作结点的题目也是手到擒来的事情了😀。
  • addFirst(E e): 在头部插入
//在插入此列表的开头指定的元素
public void addFirst(E e) {
        linkFirst(e);
    }

/**
     * Links e as first element.
     */
    private void linkFirst(E e) {
        final Node<E> f = first;
        final Node<E> newNode = new Node<>(null, e, f);
        
        first = newNode;
        //操作和linkLast一样的  只是接入的是头节点而已
        if (f == null)
            last = newNode;
        else
        //在头节点的前一个就是被插入的节点
            f.prev = newNode;
        size++;
        modCount++;
    }
  • 再来最后一个 addAll(int index, Collection<? extends E> c):插入所有指定集合中的元素插入此列表,开始在指定的位置。 目前移动的元件在该位置(如果有的话)和任何后续元素向右(增加其索引)。 新元素将出现在它们被指定collection的迭代器返回的顺序列表
  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;
        //如果索引的大小和size相等 则直接从末尾开始插入
        if (index == size) {
            succ = null;
            pred = last;
        } else {
            //否则 通过node(index)这个函数 计算出插入位置的节点
            succ = node(index);
            pred = succ.prev;
        }
           
        for (Object o : a) {
            @SuppressWarnings("unchecked")
            //强制转换  叫做向上转型,  因为所有的类都是Object的子类
             E e = (E) o;
             //和前面的插入操作一样的
             //把node 插到pred的后面
            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;
    }
     //找到下标所对应的节点并返回
     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;
        }
    }
  • 删除: remove() 删除第一个元素
public E remove() {

        return removeFirst();
    }
     public E removeFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }
    //其实最终就是调用的这个函数
     private E unlinkFirst(Node<E> f) {
        // assert f == first && f != null;
        final E element = f.item;
        //头节点的下一个节点
        final Node<E> next = f.next;
        //另头节点的元素值为null
        f.item = null;
        //不再指向链表的下一个节点  等待被GC回收
        f.next = null; // help GC
        //头节点右移动一个
        first = next;
        //如果next==null的话 那么这个链表也为空
        if (next == null)
            last = null;
        else //不为的空话  头节点的前一个节点就应该为空了
            next.prev = null;
        size--;
        modCount++;
        return element;
    }
  • 总结 熟练的操作节点的增删改 将会是突破算法的关键点之一,同学们加油!。
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值