玩转数据结构3-链表

前面两节课程主要介绍了动态数组、栈以及队列这样三种数据结构,这三种数据结构的底层都是依托于静态数组构建的,靠resize解决固定容量的问题。本节课介绍一种真正的动态数据结构-链表,链表也是一种线性数据结构,是最简单的动态数据结构。

1. 链表基础

1.1 链表的特点
  • 链表的数据存储在节点(Node)中,节点中还包含下一节点的地址

      class Node{
          E e;
          Node next;
      }
    
LinkedList.png
  • 前面讲到,数组最好用于索引有语意的情形,其最大的优点是支持快速查询

  • 而与数组相比,链表的优点是它是一种真正的动态结构,不需要处理固定容量的问题;缺点则是丧失了随机访问的能力

  • 链表的基本结构(支持泛型):

      public class LinkedList<E> {
          // 私有节点(Node)类
          private class Node{
              private E e;
              private 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 head; // 头节点
          private int size; // 链表当前所存节点数
          
          public LinkedList() {
              head = new Node();
              size = 0;
          }
    
          // 获取链表中的元素个数
          public int getSize(){
              return size;
          }
          
          // 返回链表是否为空
          public boolean isEmpty() {
              return size == 0;
          }
      }
    
1.2 添加元素
LinkedList.png

链表中添加元素分为两种情况:

  • 一种是向链表头添加元素


    AddHead.png

    此时需要将链表头(head)作为待添加元素node的下一节点:node.next = head


    node.next=head

    然后将node赋为新的head节点:head = node;
    head=node
      // 在链表头添加新的元素e
      public void addFirst(E e){
          Node  node = new Node(e,null);
          node.next = head;
          head = node;
          // head = new Node(e,head);
          size ++;
      }
    
  • 一种是向链表中间添加元素
    例如将node 添加到索引位置为2的位置(非典型应用,链表中一般不讨论索引的概念),关键点在于定义一个prev节点,指向待添加位置的前一个节点


    initPrev

    prev

    找到prev之后,将prev的下一节点赋为node的下一节点: node.next = prev.next


    node.next=prev.next

    然后将node赋为prev的下一节点:prev.next = node
    prev.next = node
      // 在链表的index(0-based)位置添加新的元素e
      // 非常规方法
      public void add(int index,E e) {
          if(index<0 || index >=size)
              throw new IllegalArgumentException("Add failed. Index is illegal. ");
          if (index == 0) {
              Node node = new Node(e);
              node.next = head;
              head = node;
          }
          else {
              // 从链表头开始,找到索引位置的前一位
              Node pre = head;
              for(int i=0;i<index - 1;i++) {
                  pre = pre.next;
              }
    
              Node newNode = new Node(e);
              newNode.next = pre.next;
              pre.next = newNode;
              
              // pre.next = new Node(e,pre.next);
              size ++;
          }
      }
    
  • 上述实现方法,如果在头节点插入元素,要采取特殊处理,为了避免插入逻辑的不同,可以为链表设立虚拟头节点:


    dummyHead
    // 设立虚拟头节点的链表  
    public class LinkedList<E> {
      
      private class Node{
          private E e;
          private 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 LinkedList() {
          dummyHead = new Node();
          size = 0;
      }
      
      // 获取链表中的元素个数
      public int getSize(){
          return size;
      }
      
      // 返回链表是否为空
      public boolean isEmpty() {
          return size == 0;
      }
      
      // 在链表的index(0-based)位置添加新的元素e
      // 使用虚拟链表头,可以不单独处理链表头的插入操作
      public void add(int index,E e) {
          if(index<0 || index >size)
              throw new IllegalArgumentException("Add failed. Index is illegal. ");
          
          // 从虚拟链表头开始,找到索引位置的前一位
          Node pre = dummyHead;
          for(int i=0;i<index;i++) {
              pre = pre.next;
          }
          Node newNode = new Node(e);
          
          newNode.next = pre.next;
          pre.next = newNode;
          
          // pre.next = new Node(e,pre.next);
          size ++;
      }
      
      // 在链表头插入新元素e
      public void addFirst(E e) {
          add(0,e);
      }
      
      // 在链表尾添加新元素e
      public void addLast(E e) {
          add(size,e);
      }
    }
    
1.3 链表的遍历、查询和修改
  • 在链表中查找元素
    从头节点开始,遍历整个链表,依次对比,找到后返回真,找不到则返回假:

      // 在链表中查找元素
      public boolean contains(E e) {
          Node cur = dummyHead.next;
          while(cur!=null) {
              if(cur.e.equals(e))
                  return true;
              cur = cur.next;
          }
          return false;
      }
    
  • 获取链表中指定索引位置的元素
    非典型应用:

      // 获得链表的第index(0-based)个位置的元素
      public E get(int index) {
          if(index<0 || index >=size)
              throw new IllegalArgumentException("Get failed. 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(0-based)个位置的元素为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;
      }
    
1.4 链表元素的删除
  • 删除索引为2位置的元素
    找到指定索引位置的前一节点prev:


    Prev

    Prev2

    Prev3

    将prev的下一节点赋为delNode的下一节点:prev.next = delNode.next


    prev.next = delNode.next

    将待删除节点delNode的下一节点置为空:delNode.next = null
    delNode.next = null
      // 从链表中删除元素e
      public void removeElement(E e) {
          Node pre = dummyHead;
          // 从虚拟头节点出发,找到待删除元素的前一节点
          while(pre.next!=null) {
              if(pre.next.e.equals(e))
                  break;
              pre = pre.next;
          }
          // 如果找到的前一节点不是最后一个元素,则执行删除操作
          if(pre.next!=null) {
              Node delNode = pre.next;
              pre.next = delNode.next;
              delNode.next = null;
              size --;
          }
          // 如果找到最后元素,还没有找到元素e
          else {
              // throw new IllegalArgumentException("Remove failed. The element is not included in list.");           
          }
          
      }
    

2. 使用链表实现栈

2.1 链表的时间复杂度分析
  • 添加操作:
函数名 描述 时间复杂度
addLast(e) 在链表尾节点后添加元素,需要遍历整个链表 O(n)
addFirst(e) 在链表头节点前添加元素,只需一步操作 O(1)
add(index,e) 在指定索引位置添加元素,计算复杂度期望值 O(n/2)=O(n)
  • 删除操作
函数名 描述 时间复杂度
removeLast(e) 删除链表尾节点,需要遍历整个链表 O(n)
removeFirst(e) 删除链表头节点,只需一步操作 O(1)
remove(index,e) 删除指定索引位置的节点,计算复杂度期望值 O(n/2)=O(n)
  • 修改与查找
函数名 描述 时间复杂度
set(index,e) 修改指定索引位置的节点,计算复杂度期望值 O(n/2)=O(n)
get(index) 获取指定索引位置的节点元素,同上 O(n)
contains(e) 判断链表中是否存在指定元素,遍历整个链表 O(n)

仔细观察链表的增删改查操作,如果只对链表头节点进行增删操作,其复杂度均为O(1);而修改操作并非常规操作;查找操作如果只查找头节点,时间复杂度也为O(1),因此链表的一个典型应用就是实现栈(在同一端增加和删除)。

回顾栈的几个功能函数:

  • pop() 弹栈
  • push() 压栈
  • isEmpty() 判断栈是否为空
  • getSize() 栈内元素个数
  • peek() 查看栈顶元素
2.2 栈接口
public interface Stack<E> {
    
    int getSize();
    boolean isEmpty();
    E peek();
    E pop();
    void push(E e);

}
2.3 链表数据结构类
public class LinkedList<E> {
    
    private class Node{
        private E e;
        private 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 LinkedList() {
        dummyHead = new Node();
        size = 0;
    }
    
    // 获取链表中的元素个数
    public int getSize(){
        return size;
    }
    
    // 返回链表是否为空
    public boolean isEmpty() {
        return size == 0;
    }
    
    // 在链表的index(0-based)位置添加新的元素e
    // 使用虚拟链表头,可以不单独处理链表头的插入操作
    public void add(int index,E e) {
        if(index<0 || index >size)
            throw new IllegalArgumentException("Add failed. Index is illegal. ");
        
        // 从虚拟链表头开始,找到索引位置的前一位
        Node pre = dummyHead;
        for(int i=0;i<index;i++) {
            pre = pre.next;
        }
        Node newNode = new Node(e);
        
        newNode.next = pre.next;
        pre.next = newNode;
        
        // pre.next = new Node(e,pre.next);
        size ++;
    }
    
    // 在链表头插入新元素e
    public void addFirst(E e) {
        add(0,e);
    }
    
    // 在链表尾添加新元素e
    public void addLast(E e) {
        add(size,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;
    }
    
    // 获得链表的第index(0-based)个位置的元素
    public E get(int index) {
        if(index<0 || index >=size)
            throw new IllegalArgumentException("Get failed. 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(0-based)个位置的元素为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;
    }
    
    // 从链表中删除index(0-based)位置的元素, 返回删除的元素
    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 cur = pre.next;
        pre.next = cur.next;
        cur.next = null;
        
        size--;
        
        return cur.e;
        
    }
    
    // 从链表删除第一个元素,返回删除的元素
    public E removeFirst() {
        return remove(0);
    }
    
    // 删除链表尾的元素,返回删除的元素
    public E removeLast() {
        return remove(size-1);
    }
    // 从链表中删除元素e
    public void removeElement(E e) {
        Node pre = dummyHead;
        while(pre.next!=null) {
            if(pre.next.e.equals(e))
                break;
            pre = pre.next;
        }
        if(pre.next!=null) {
            Node delNode = pre.next;
            pre.next = delNode.next;
            delNode.next = null;
            size --;
        }
        else {
            // throw new IllegalArgumentException("Remove failed. The element is not included in list.");           
        }
        
    }
    
    @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.4 基于链表的栈数据结构
public class LinkedListStack<E> implements Stack<E> {
    // 私有成员变量LinkedList用来存储栈元素
    private LinkedList<E> list;
    
    public LinkedListStack() {
        list = new LinkedList<E>();
    }
    
    @Override
    public int getSize() {
        return list.getSize();
    }
    
    @Override
    public boolean isEmpty() {
        return list.isEmpty();
    }
    @Override
    public void push(E e) {
        list.addFirst(e);   
    }
    
    @Override
    public E pop() {
        return list.removeFirst();
    }
    
    @Override
    public E peek() {
        return list.getFirst();
    }
    
    @Override
    public String toString() {
        StringBuilder res = new StringBuilder();
        res.append("Stack: Top ");
        res.append(list);
        return res.toString();
    }
    
    public static void main(String[] args) {
        LinkedListStack<Integer> stack = new LinkedListStack<>(); 
        for(int i = 0;i<5;i++
            stack.push(i);
            System.out.println(stack);
            // Stack: Top 0-->NULL
            // Stack: Top 1-->0-->NULL
            // Stack: Top 2-->1-->0-->NULL
            // Stack: Top 3-->2-->1-->0-->NULL
            // Top 4-->3-->2-->1-->0-->NULL
        }
        stack.pop();
        System.out.println(stack);
        // Stack: Top 3-->2-->1-->0-->NULL
    }
}
2.5 数组栈与链表栈实现效率对比
import java.util.Random;

public class Main {
    public static double costTime(Stack<Integer> stack,int nCount) {
        
        Random random = new Random();
        long startTime = System.nanoTime();
        for(int i = 0;i<nCount;i++) {
            stack.push(random.nextInt(Integer.MAX_VALUE));
        }
        for(int i = 0;i<nCount;i++) {
            stack.pop();
        }
        
        long endTime = System.nanoTime();
        return (endTime-startTime) / 1000000000.0 ;
        
    }

    public static void main(String[] args) {
        ArrayStack<Integer> stack = new ArrayStack<>(); 
        for(int i = 0;i<5;i++) {
            stack.push(i);
            System.out.println(stack);
            // Stack: [0] top
            // Stack: [0,1] top
            // Stack: [0,1,2] top
            // Stack: [0,1,2,3] top
            // Stack: [0,1,2,3,4] top
        }
        stack.pop();
        System.out.println(stack);
        // Stack: [0,1,2,3] top
        
        int nCount = 100000;
        ArrayStack<Integer> arraystack = new ArrayStack<>(); 
        LinkedListStack<Integer> linkedstack = new LinkedListStack<>(); 
        
        System.out.println("ArrayStack:"+costTime(arraystack,nCount)); // 0.077
        System.out.println("LinkedListStack:"+costTime(linkedstack,nCount)); // 0.036
            
    }
}

可以看到使用链表实现栈和使用数组实现栈,两者之间的效率是一致的,这与前一节的时间复杂度分析相互映证。

stack

3. 使用链表实现队列

队列的特征是一端进,另一端出,因此上述链表结构并不适合用来实现队列,需要对结构进行一定的改进。

对上述链表结构增加一个tail标签来标记尾节点的位置,从head和tail端增加元素,都只需要一步操作:

  • head端: head = new Node(e,head)
  • tail端:tail.next = new Node(e); tail = tail.next;

而删除操作:

  • head端(一步操作):head = head.next;
  • tail端:遍历整个链表找到前一节点prev

因此,使用改进后的链表结构实现队列,从head端出队,tail端进队:


LinkedQueue
3.1 队列接口
public interface Queue<E> {
    int getSize();
    boolean isEmpty();
    void enqueue(E e);
    E getFront();
    E dequeue();
}
3.2 私有节点类
private class Node{
    private E e;
    private Node next;
    
    public Node(E e,Node next) {
        this.e = e;
        this.next = next;
    }
    
    public Node(E e) {
        this.e = e;
        next = null;
    }
    
    public Node() {
        this(null,null);
    }
    
    @Override
    public String toString() {
        return e.toString();
    }
}
3.3 链表队列
public class LinkedListQueue<E> implements Queue<E> {
    private int size;
    private Node head; // 头节点,出队
    private Node tail; // 尾节点,入队
    
    public LinkedListQueue() {
        head = new Node();
        tail = new Node();
        size = 0;
    }
    
    @Override
    public int getSize() {
        return size;
    }
    
    @Override
    public boolean isEmpty() {
        return size == 0;
    }
    
    @Override
    // 从tail端入队
    public void enqueue(E e) {
        // 如果队列为空,head,tail均指向第一个入队元素
        if(isEmpty()) {
            tail = new Node(e);
            head = tail;
        }
        // 队列不为空,则添加到尾节点,并维护tail指向
        else {
            tail.next = new Node(e);
            tail = tail.next;           
        }
        size ++;    
    }
    
    @Override
    // 从head端出队,返回出队元素
    public E dequeue() {
        // 若队列为空,抛异常
        if(isEmpty())
            throw new IllegalArgumentException("dequeue Failed. The queue is empty.");
        // 队列不为空,删除头节点,维护head指向
        Node delNode = head;
        head = head.next;
        // 删除后,若队列为空,维护tail指向(若不维护,tail仍指向待删除元素)
        if(head == null) {
            tail = null;
        }
        delNode.next = null;
        size --;
        return delNode.e;
    }
    
    @Override
    public E getFront() {
        if(isEmpty())
            throw new IllegalArgumentException("Get Failed. The queue is empty.");
        return head.e;
    }
    
    @Override
    public String toString() {
        StringBuilder res = new StringBuilder();
        res.append("Queue: head ");
        Node cur = head;
        while(cur != null) {
            res.append(cur.e+"<-");             
            cur = cur.next;
        }
        res.append("tail");
        return res.toString();
    }
    
    public static void main(String[] args) {
        LinkedListQueue<Integer> queue = new LinkedListQueue<>();
        for(int i = 0;i<10;i++) {
            queue.enqueue(i);
            System.out.println(queue);
            if(i%3==2) {
                queue.dequeue();
                System.out.println(queue);
//              Queue: head 0<-tail
//              Queue: head 0<-1<-tail
//              Queue: head 0<-1<-2<-tail
//              Queue: head 1<-2<-tail
//              Queue: head 1<-2<-3<-tail
//              Queue: head 1<-2<-3<-4<-tail
//              Queue: head 1<-2<-3<-4<-5<-tail
//              Queue: head 2<-3<-4<-5<-tail
//              Queue: head 2<-3<-4<-5<-6<-tail
//              Queue: head 2<-3<-4<-5<-6<-7<-tail
//              Queue: head 2<-3<-4<-5<-6<-7<-8<-tail
//              Queue: head 3<-4<-5<-6<-7<-8<-tail
//              Queue: head 3<-4<-5<-6<-7<-8<-9<-tail
            }
        }
    }
}

4. 总结

本节课主要学习了具有真正的动态数据结构的链表,链表是最简单的动态数据结构,其主要特点是不需要处理固定容量的问题。课程首先分析了链表的结构特性,然后依次实现了链表的增删改查操作。结合链表的时间复杂度分析结果,使用链表实现了前一节介绍的栈结构,并进一步改进链表结构,实现了队列功能。关于链表,还有双向链表等一些特殊的结构,这里暂不讨论,目前主要的工作是拓展学习的广度,后续用到时再进一步深度研究。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,923评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,154评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,775评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,960评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,976评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,972评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,893评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,709评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,159评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,400评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,552评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,265评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,876评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,528评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,701评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,552评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,451评论 2 352

推荐阅读更多精彩内容