数据结构_4:链表

链表 LinkedList

写在开头

  • 许久之前文章提到过的:动态数组队列,其底层均依托于静态数组,通过resize()进行动态扩容操作。
  • 链表,则为真正的动态数据结构,同样也是最简单的动态数据结构
  • 链表这种数据结构可以帮助我们了解计算机中指针(引用)递归等概念。

节点 Node

  • 数据存储在节点中,需要多少节点就生产多少节点进行挂接,但失去了随机访问的能力,适合索引无语义的情况。

    class node {
          
          
        E e;
        Node next;
    }
    

链表数据结构创建 LinkedList,为保证节点信息安全性,采用内部类方式进行构造

 /**
 * @author by Jiangyf
 * @classname LinkedList
 * @description 链表
 * @date 2019/9/28 13:08
 */
 public class LinkedList<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 "Node{" +
                     "e=" + e +
                     ", next=" + next +
                     '}';
         }
     }

     private Node head;
     int size;

     public LinkedList() {
    
    
         head = null;
         size = 0;
     }

     // 获取链表容量
     public int getSize() {
    
    
         return size;
     }

     // 判断链表是否为空
     public boolean isEmpty() {
    
    
         return size == 0;
     }
 }
添加操作方法
  • 从链表头部添加元素

     public void addFirst(E e) {
          
          
          head = new Node(e, head);
          size ++;
      }
    
  • 从链表中间位置index处添加元素,注意:先连后断

    public void add(int index, E e) throws IllegalAccessException {
          
          
               // 索引校验
               if (index < 0 || index > size) {
          
          
                   throw new IllegalAccessException("Add failed. Illegal index.");
               }
               // 判断是操作是否为头部添加
               if (index == 0) {
          
          
                   addFirst(e);
               } else {
          
          
                   // 创建前置节点
                   Node prev = head;
                   // 定位到待插入节点前一个节点
                   for (int i = 0; i < index -1 ; i++) {
          
          
                       prev = prev.next;
                   }
                   prev.next = new Node(e, prev.next);
                   size ++;
               }
           }
    
  • 在链表尾部位置添加元素

     public void addLast(E e) throws IllegalAccessException {
          
          
         add(size, e);
     }
    
  • 为链表设立虚拟头结点(dummyHead),解决从头部添加和其他位置添加的逻辑不一致情况

    • 虚拟头结点作为链表内部机制进行设置,在原有head节点改进为dummyHead.next = head,适配节点添加逻辑。
    • 修改代码
      • 添加虚拟头结点dummyHead,不存放任何内容

          	private Node dummyHead;
        			
        	public LinkedList() {
                  
                  
        	     dummyHead = new Node(null, null);
        	     size = 0;
        	 }
        
      • 修改add(index, e)方法

          // 从链表中间添加元素 先连后断
           public void add(int index, E e) throws IllegalAccessException {
                  
                  
               // 索引校验
               if (index < 0 || index > size) {
                  
                  
                   throw new IllegalAccessException("Add failed. Illegal index.");
               }
               // 创建前置节点
               Node prev = dummyHead;
               // 定位到待插入节点前一个节点,遍历index次原因为 dummyHead为head节点前一个节点
               for (int i = 0; i < index ; i++) {
                  
                  
                   prev = prev.next;
               }
               prev.next = new Node(e, prev.next);
               size ++;
           }
        
      • 修改addFirst(e)方法

        public void addFirst(E e) throws IllegalAccessException {
                  
                  
              add(0, e);
        }
        
  • 获取指定位置index的节点元素

     public E get(int index) throws IllegalAccessException {
          
          
         // 索引校验
         if (index < 0 || index > size) {
          
          
             throw new IllegalAccessException("Get failed. Illegal index.");
         }
         // 定位到head节点
         Node cur = dummyHead.next;
         for (int i = 0; i < index; i++) 
             cur = cur.next;
         return cur.e;
     }
    
  • 获取头结点、尾结点

     public E getFirst() throws IllegalAccessException {
          
          
        return get(0);
     }
    
     public E getLast() throws IllegalAccessException {
          
          
         return get(size - 1);
     }
    
  • 更新指定位置元素

    public void set(int index, E e) throws IllegalAccessException {
          
          
        // 索引校验
         if (index < 0 || index > size) {
          
          
             throw new IllegalAccessException("Set failed. Illegal index.");
         }
         Node cur = dummyHead.next;
         for (int i = 0; i < index ; i++)
             cur = cur.next;
         cur.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) throws IllegalAccessException {
          
          
         // 索引校验
         if (index < 0 || index > size) {
          
          
             throw new IllegalAccessException("Remove failed. Illegal index.");
         }
         // 定位到待删除节点的前一节点
         Node prev = dummyHead;
         for (int i = 0; i < index - 1 ; i++)
             prev = prev.next;
         // 保存待删除节点
         Node retNode = prev.next;
         // 跨过待删除节点进行连接
         prev.next = retNode.next;
         // 待删除节点next置空
         retNode.next = null;
         size --;
         return retNode.e;
     }
    
     public E removeFirst() throws IllegalAccessException {
          
          
         return remove(0);
     }
    
     public E removeLast() throws IllegalAccessException {
          
          
         return remove(size - 1);
     }
    
  • 通过上述方法,我们可以分析得出:链表的CURD操作的平均时间复杂度均为O(n),链表的操作均要进行遍历。


仔细想想,如果对链表的操作仅限于头部呢? 细思极恐,是不是复杂度就降为O(1)啦?又由于链表是动态的,不会造成空间的浪费,所以当且仅当头部操作下,优势是很明显的!

  • 基于头部操作,用链表实现,关于Stack接口,可以查看 数据结构_2:栈

     public class LinkedListStack<E> implements Stack<E> {
          
          
    
           private LinkedList<E> list;
    
           public LinkedListStack(LinkedList<E> list) {
          
          
               this.list = new LinkedList<>();
           }
    
           @Override
           public int getSize() {
          
          
               return list.getSize();
           }
    
           @Override
           public boolean isEmpty() {
          
          
               return list.isEmpty();
           }
    
           @Override
           public void push(E e) throws IllegalAccessException {
          
          
               list.addFirst(e);
    
           }
    
           @Override
           public E pop() throws IllegalAccessException {
          
          
               return list.removeFirst();
           }
    
           @Override
           public E peek() throws IllegalAccessException {
          
          
               return list.getFirst();
           }
       }
    
  • 既然有了实现,那就拿链表栈数组栈比较下吧,创建测试函数

    private static double testStack(Stack<Integer> stack, int opCount) throws IllegalAccessException {
          
          
            long startTime = System.nanoTime();
            Random random = new Random();
            for (int i = 0; i < opCount; i ++)
                stack.push(random.nextInt(Integer.MAX_VALUE));
            for (int i = 0; i < opCount; i ++)
                stack.pop();
            long endTime = System.nanoTime();
            return (endTime - startTime) / 1000000000.0;
    }
    
  • 分别创建链表栈数组栈进行一百万次的入栈和出站操作,比较两者用时,貌似好像链表栈好一点。
    在这里插入图片描述

  • 继续加大数据量到一千万次的入栈和出栈操作,此时链表栈的表现就不佳了。在这里插入图片描述
    原因大致是:数组栈pop和push操作基于数组尾部进行处理;而链表栈pop和push操作基于链表头部操作,且投保操作含有创建新节点的操作(new Node),因此比较耗时。

  • 结构已经创建,那么队列也是必不可少的,前文中的数组队列的构建是从头部和尾部均进行了操作,由于出列操作的复杂度为O(n),入列操作的复杂度为O(1),进行了队列结构的优化,于是产生了数组实现的循环队列,且性能要远高于普通的数组队列。于是我们对链表这一结构进行分析:

    • 由于存在head头指针,头部操作的复杂度为O(1)【dummyHead的设定】

    • 那么基于这个原理,添加上tail尾指针,记录链表尾部(索引),是否可以将尾部的操作复杂度降低呢? head指针的定位是依赖于虚拟头指针的结构设定,而tail指针无此设定,若要进行尾部元素删除操作,还需要定位到待删除元素的前一元素,仍需要进行遍历。

    • 基于上述,链表节点Node的next设定,更有利于我们从链表首部进行出队操作链表尾部进行入队操作

    • 采用head + tail改造我们的LinkedListQueue

      /**
      * @author by Jiangyf
      * @classname LinkedListQueue
      * @description 链表队列
      * @date 2019/9/28 16:35
      */
      public class LinkedListQueue<E> implements Queue<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 "Node{" +
                          "e=" + e +
                          ", next=" + next +
                          '}';
              }
          }
      
          private Node head, tail;
          private int size;
      
          public LinkedListQueue() {
              
              
              this.head = null;
              this.tail = null;
              this.size = 0;
          }
      
          @Override
          public int getSize() {
              
              
              return size;
          }
      
          @Override
          public boolean isEmpty() {
              
              
              return size == 0;
          }
      
          @Override
          public void enqueue(E e) {
              
              
              // 入队 从链表尾部进行
              if (tail == null) {
              
              
                  // 表示链表为空
                  tail = new Node(e);
                  head = tail;
              } else {
              
              
                  // 不为空,指向新创建的元素,尾指针后移
                  tail.next = new Node(e);
                  tail = tail.next;
              }
              size ++;
          }
      
          @Override
          public E dequeue() {
              
              
              // 出队 从链表头部进行
              if (isEmpty()) {
              
              
                  throw new IllegalArgumentException("Queue is empty");
              }
              // 获取待出队元素
              Node retNode = head;
              // 头指针后移
              head = head.next;
              // 待删除元素与链表断开
              retNode.next = null;
              if (head == null) {
              
              
                  // 链表中仅有一个元素的情况,头指针移动后变为空链表
                  tail = null;
              }
              size --;
              return retNode.e;
          }
      
          @Override
          public E getFront() {
              
              
              if (isEmpty()) {
              
              
                  throw new IllegalArgumentException("Queue is empty");
              }
              return head.e;
          }
      }
      
    • 同样,与之前的数组队列循环队列链表队列进行下性能测试(10万数量级)
      在这里插入图片描述
      可见,循环队列和链表队列的性能远高于数组队列,其原因就是头尾指针动态控制数据结构,而数组队列出列时要反复的进行数据复制,因此消耗时间较长。


最后,上述代码已经上传个人仓库,需要的小伙伴可以下载查看

猜你喜欢

转载自blog.csdn.net/Nerver_77/article/details/101619454