2.基础的动态数据结构:链表

二、基础的动态数据结构:链表

在这里插入图片描述

1. 链表介绍

1.1 指针和引用

数据加载进内存
指针/引用:内存中的地址
在这里插入图片描述

1.2 为什么需要链表?

  • 用一组连续的内存空间,来存储一组具有相同类型的数据:
    int[] data = new int[5];
    在这里插入图片描述在这里插入图片描述
  • 堆内存中没有足够的连续内存空间,申请一个更大的数组,内存不够,数组初始化失败
    在这里插入图片描述能不能设计一种数据结构,合理的充分的利益非连续的内存空间

怎样去访问在非连续内存空间的数据呢?

  • 链表:

链表也是一个线性结构
链表不需要连续的内存空间,所以链表可以充分的利用存储空间
在这里插入图片描述

1.3 链表对比数组

每个内存空间除了存储数据外,还需要记录下一块内存空间的地址
在这里插入图片描述

  • 链表
    • 链表是动态数据结构,链表天然的支持动态扩容,数组需要进行resize才可以扩容
    • 链表可以充分的利用非连续的内存空间,提高内存的利用率
    • 在存储相同数量的数据时,链表需要的内存空间比数组大

1.4 节点表示

 class{
     E e;    //元素值
     Node next;    //下一个节点
 }

在这里插入图片描述

2. 单向链表

2.1 链表的基础实现

 public class LinkedList<E> {
     //节点内部类
    private class Node{
        E e;
        Node next;public Node(E e) {
            this.e = e;
        }public Node() {
        }public Node(E e, Node next) {
            this.e = e;
            this.next = next;
        }@Override
        public String toString() {
            return e.toString();
        }
    }//头节点
     private Node head;
    //长度
     private int size;public LinkedList() {
         head = null;
         size = 0;
     }public int getSize(){
         return size;
     }public boolean isEmpty(){
         return size ==0;
     }
 }

2.2 查询指定索引的元素

链表的索引概念

  • 查询索引为3的节点的值
    在这里插入图片描述
    定义临时变量curr指向头节点
    在这里插入图片描述
  • 索引和size的关系
    在这里插入图片描述
    size永远比链表最大的索引值大1,指向最后一个节点的next为null的节点
    在这里插入图片描述
  • 代码实现查询指定索引的元素
    查询索引为3的节点的值
/**
     * 查询指定索引的节点的值
     * @param index
     * @return
     */
public E get(int index) {
    //检查index的合法性
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("get failed,index must >=0 and <size");
    }
    //定义临时变量指向头节点
    Node curr = head;
    for (int i = 0; i < index; i++) {
        curr = curr.next;
    }
    return curr.e;
}

public E getFirst() {
    return get(0);
}

private E getLast() {
    return get(size - 1);
}

2.3 修改指定索引节点的值

  • 修改索引为3的节点的值为88
    在这里插入图片描述
/**
     * 修改指定索引的节点的值
     * @param index
     * @param e
     */
public void set(int index,E e){
    //检查index的合法性
    if(index<0||index>=size){
        throw new IllegalArgumentException("get failed,index must >=0 and <size");
    }
    //定义临时变量指向头节点
    Node curr = head;
    for (int i = 0; i < index; i++) {
        curr = curr.next;
    }
    curr.e = e;
}

2.4 添加

两种情况,一种是在表头添加,另外一种是在链表中间部分添加

添加头节点

在这里插入图片描述

/**
     * 在链表表头新增头节点
     * @param e 新增节点需要存储的数据
     */
public void addFirst(E e){
    //新节点的值为e,新节点的下一个节点为head,再把新节点赋值给head
    //简化后:
    head = new Node(e,head);
    size++;
}
在链表中间添加节点

在这里插入图片描述
注意,第三步和第四步不能更换顺序
在这里插入图片描述

//表尾插入
public void addLast(E e){
    add(size,e);
}

/**
     * 在指定索引的位置插入新的节点
     * @param index 需要插入的位置
     * @param e 需要插入的数据
     */
public void add(int index,E e){
    //检查index的合法性
    if (index < 0 || index > size) {
        throw new IllegalArgumentException("get failed,index must >=0 and <size");
    }
    //如果是在表头插入元素
    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++;
    }
}

2.5 删除节点

删除头节点

在这里插入图片描述

/**
     * 删除链表的头节点
     * @return
     */
public E removeFirst(){
    if(head == null){
        return null;
    }
    Node delNode = head;
    head = head.next;
    delNode.next = null;
    size--;
    return delNode.e;
}
删除中间节点

在这里插入图片描述

//移除表尾
public E removeLast(){
    return remove(size-1);
}

/**
     * 删除指定索引的节点,并返回删除的节点的值
     * @param index
     * @return
     */
public E remove(int index){
    //检查index的合法性
    if(index<0||index>=size){
        throw new IllegalArgumentException("get failed,index must >=0 and <size");
    }

    if(index==0){
        return removeFirst();
    }else{
        Node prev = head;
        for (int i = 0; i < index - 1; i++) {
            prev = prev.next;
        }
        Node delNode = prev.next;
        prev.next = delNode.next;
        delNode.next = null;
        size--;
        return delNode.e;
    }
}

2.6总代码(未使用虚拟头节点)

 package com.douma.line.linkedList;public class LinkedList<E> {//节点类
     private class Node {
         E e;
         Node next;public Node(E e) {
             this.e = e;
         }public Node() {
         }public Node(E e, Node next) {
             this.e = e;
             this.next = next;
         }@Override
         public String toString() {
             return e.toString();
         }
     }//头节点
     private Node head;
     //长度
     private int size;public LinkedList() {
         head = null;
         size = 0;
     }public int getSize() {
         return size;
     }public boolean isEmpty() {
         return size == 0;
     }/**
      * 查询指定索引的节点的值
      * @param index
      * @return
      */
     public E get(int index) {
         //检查index的合法性
         if (index < 0 || index >= size) {
             throw new IllegalArgumentException("get failed,index must >=0 and <size");
         }
         //定义临时变量指向头节点
         Node curr = head;
         for (int i = 0; i < index; i++) {
             curr = curr.next;
         }
         return curr.e;
     }public E getFirst() {
         return get(0);
     }private E getLast() {
         return get(size - 1);
     }/**
      * 修改指定索引的节点的值
      *
      * @param index
      * @param e
      */
     public void set(int index, E e) {
         //检查index的合法性
         if (index < 0 || index >= size) {
             throw new IllegalArgumentException("get failed,index must >=0 and <size");
         }
         //定义临时变量指向头节点
         Node curr = head;
         for (int i = 0; i < index; i++) {
             curr = curr.next;
         }
         curr.e = e;
     }/**
      * 在链表表头新增头节点
      * @param e 新增节点需要存储的数据
      */
     public void addFirst(E e){
         //新节点的值为e,新节点的下一个节点为head,再把新节点赋值给head
         head = new Node(e,head);
         size++;
     }//表尾插入
     public void addLast(E e){
         add(size,e);
     }/**
      * 在指定索引的位置插入新的节点
      * @param index 需要插入的位置
      * @param e 需要插入的数据
      */
     public void add(int index,E e){
         //检查index的合法性
         if (index < 0 || index >= size) {
             throw new IllegalArgumentException("get failed,index must >=0 and <size");
         }
         //如果是在表头插入元素
         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++;
         }
     }/**
      * 删除链表的头节点
      * @return
      */
     public E removeFirst(){
         if(head == null){
             return null;
         }
         Node delNode = head;
         head = head.next;
         delNode.next = null;
         size--;
         return delNode.e;
     }//移除表尾
     public E removeLast(){
         return remove(size-1);
     }/**
      * 删除指定索引的节点,并返回删除的节点的值
      * @param index
      * @return
      */
     public E remove(int index){
         //检查index的合法性
         if(index<0||index>=size){
             throw new IllegalArgumentException("get failed,index must >=0 and <size");
         }if(index==0){
             return removeFirst();
         }else{
             Node prev = head;
             for (int i = 0; i < index - 1; i++) {
                 prev = prev.next;
             }
             Node delNode = prev.next;
             prev.next = delNode.next;
             delNode.next = null;
             size--;
             return delNode.e;
         }
     }
 }

3. 单向链表优化

3.1 虚拟(哨兵)节点

简化头节点和其他节点的处理逻辑
在之前的新增或删除时都要特殊考虑头节点,因为都用到了待新增(删除)节点的前一个节点
在这里插入图片描述
加入哨兵节点后会使得处理头节点的逻辑和处理中间节点的逻辑变的一致

package com.douma.line.linkedList;

public class LinkedList<E> {

    //节点类
    private class Node {
        E e;
        Node next;

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

        public Node() {
        }

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

        @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;
    }

    /**
     * 查询指定索引的节点的值
     * @param index
     * @return
     */
    public E get(int index) {
        //检查index的合法性
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("get failed,index must >=0 and <size");
        }
        //定义临时变量指向头节点
        Node curr = dummyHead.next;
        for (int i = 0; i < index; i++) {
            curr = curr.next;
        }
        return curr.e;
    }

    public E getFirst() {
        return get(0);
    }

    private E getLast() {
        return get(size - 1);
    }

    /**
     * 修改指定索引的节点的值
     *
     * @param index
     * @param e
     */
    public void set(int index, E e) {
        //检查index的合法性
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("get failed,index must >=0 and <size");
        }
        //定义临时变量指向头节点
        Node curr = dummyHead.next;
        for (int i = 0; i < index; i++) {
            curr = curr.next;
        }
        curr.e = e;
    }

    /**
     * 在链表表头新增头节点
     * @param e 新增节点需要存储的数据
     */
    public void addFirst(E e){
        add(0,e);
    }

    //表尾插入
    public void addLast(E e){
        add(size,e);
    }

    /**
     * 在指定索引的位置插入新的节点
     * @param index 需要插入的位置
     * @param e 需要插入的数据
     */
    public void add(int index,E e){
        //检查index的合法性
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("get failed,index must >=0 and <size");
        }

        //要插入位置的前一个节点
        Node prev = dummyHead;
        for (int i = 0; i < index; i++) {
            prev = prev.next;
        }
        //优化后:
        prev.next = new Node(e,prev.next);
        size++;
    }

    /**
     * 删除链表的头节点
     * @return
     */
    public E removeFirst(){
       return remove(0);
    }

    //移除表尾
    public E removeLast(){
        return remove(size-1);
    }

    /**
     * 删除指定索引的节点,并返回删除的节点的值
     * @param index
     * @return
     */
    public E remove(int index){
        //检查index的合法性
        if(index<0||index>=size){
            throw new IllegalArgumentException("get failed,index must >=0 and <size");
        }

        Node prev = dummyHead;
        for (int i = 0; i < index; i++) {
            prev = prev.next;
        }
        Node delNode = prev.next;
        prev.next = delNode.next;
        delNode.next = null;
        size--;
        return delNode.e;
    }
}

3.2 链表遍历的另一种方式

在这里插入图片描述

/**
     * 判断链表中是否存在指定元素
     * @param e
     * @return
     */
public boolean contains(E e){
    Node curr = dummyHead.next;
    while (curr!=null){
        if(curr.e.equals((e))){
            return true;
        }
        curr = curr.next;
    }
    return false;
}

3.3 覆写toString方法

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

3.4 链表方法测试

 public class LinkedListTest {
     public static void main(String[] args) {
         LinkedList<Integer> list = new LinkedList<>();
         list.addLast(5);
         System.out.println(list);
 ​
         list.addFirst(10);
         System.out.println(list);
 ​
         list.add(2,34);
         System.out.println(list);
 ​
         list.addFirst(5);
         System.out.println(list);System.out.println(list.get(1));
 ​
         list.remove(3);
         System.out.println(list);
     }
 }

3.5 链表时间复杂度分析

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4. 双向链表

4.1 基本实现

在这里插入图片描述

 //节点类
 private class Node {
     E e;
     Node prev;
     Node next;public Node(E e) {
         this.e = e;
     }public Node() {
     }public Node(E e, Node prev, Node next) {
         this.e = e;
         this.prev = prev;
         this.next = next;
     }@Override
     public String toString() {
         return e.toString();
     }
 }private int size;public DoubleLinkedList(){
     first = last = null;
 }public int getSize(){
     return size;
 }public boolean isEmpty(){
     return size==0;
 }

4.2 优缺点

耗费内存
有时要比单向链表性能好很多
例如:
在这里插入图片描述
空间换时间

/**
     *  找到指定索引index所在的节点的元素值
     * @param index
     * @return
     */
//时间复杂度O(n)
public E get(int index){
    //检查index的合法性
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("get failed,index must >=0 and <size");
    }

    if(index<size/2){
        Node curr = first;
        for (int i = 0; i < index; i++) {
            curr = curr.next;
        }
        return curr.e;
    }else {
        Node curr = last;
        for (int i = 0; i < size - index - 1; i++) {
            curr = curr.prev;
        }
        return curr.e;
    }
}

4.3 优势

//表头和表尾时间复杂度为O(1)
public Node getFirst(){
    return first;
}

public Node getLast(){
    return last;
}

4.4 修改双向链表节点的值

将通过指定索引查询节点的方法抽离出来

/**
     * 返回指定索引的节点
     * @param index
     * @return
     */
private Node node(int index){
    //检查index的合法性
    if (index < 0 || index >= size) {
        throw null;
    }

    if(index<size/2){
        Node curr = first;
        for (int i = 0; i < index; i++) {
            curr = curr.next;
        }
        return curr;
    }else {
        Node curr = last;
        for (int i = 0; i < size - index - 1; i++) {
            curr = curr.prev;
        }
        return curr;
    }
}

修改通过指定索引查找元素值的方法

/**
     *  找到指定索引index所在的节点的元素值
     * @param index
     * @return
     */
//时间复杂度O(n)
public E get(int index){
    Node node = node(index);
    if(node == null){
        throw new IllegalArgumentException("get failed,index must >=0 and <size");
    }
    return node.e;
}

创建修改指定索引元素值的方法

/**
     * 修改双向链表指定索引的值
     * @param index
     * @param e
     */
public void set(int index,E e){
    Node node = node(index);
    if(node==null){
        throw new IllegalArgumentException("get failed,index must >=0 and <size");
    }
    node.e = e;
}

4.5 插入节点

插入头节点

链表一个节点都没有的情况:
在这里插入图片描述
链表中本来就存在节点:
在这里插入图片描述

 /**
      * 往链表的表头插入节点
      * @param e
      */
 //时间复杂度为O(1)
 public void addFirst(E e){
     Node newNode = new Node(e);
     if(first == null){
         last = newNode;
     }else{
         newNode.next = first;
         first.prev = newNode;
     }
     first = newNode;
     size++;
 }
插入尾节点

链表为空:
在这里插入图片描述
链表中本身已经存在节点:
在这里插入图片描述

/**
     * 往链表的表尾插入节点
     * @param e
     */
//时间复杂度为O(1)
public void addLast(E e){
    Node newNode = new Node(e);
    if(first == null){
        first = newNode;
    }else{
        newNode.prev = last;
        last.next = newNode;
    }
    last = newNode;
    size++;
}
插入中间节点

在这里插入图片描述

/**
     * 往指定索引位置插入节点
     * @param index
     * @param e
     */
public void add(int index,E e){
    //检查index的合法性
    if (index < 0 || index > size) {
        throw new IllegalArgumentException("get failed,index must >=0 and <=size");
    }
    if(index == size){
        addLast(e);
    }else if(index == 0){
        addFirst(e);
    }else{
        Node oldNode = node(index);
        Node prev = oldNode.prev;
        Node newNode = new Node(e,prev,oldNode);
        oldNode.prev = newNode;
        prev.next = newNode;
        size++;
    }
}

4.6 删除节点

删除头节点

链表只有一个节点:
在这里插入图片描述
链表中不止一个节点:
在这里插入图片描述

 /**
      * 删除头节点
      * @return
      */
 //时间复杂度O(1)
 public E removeFirst(){
     if(first == null){
         throw new NoSuchElementException();
     }
     E e = first.e;
     Node next = first.next;
     if(next == null){
         first = null;
         last = null;
     }else{
         first.next = null;
         next.prev = null;
         first = next;
     }
     size--;
     return e;
 }
删除尾节点

链表只有一个节点:
在这里插入图片描述
链表不止一个节点:
在这里插入图片描述

/**
     * 删除尾节点
     * @return
     */
//时间复杂度O(1)
public E removeLast(){
    if(last == null){
        throw new NoSuchElementException();
    }
    E e = last.e;
    Node prev = last.prev;
    if(prev == null){
        last = null;
        first = null;
    }else{
        last.prev = null;
        prev.next = null;
        last = prev;
    }
    size--;
    return e;
}
删除中间节点

在这里插入图片描述

 /**
      * 删除指定索引的节点
      * @return
      */
 public E remove(int index){
     //检查index的合法性
     if (index < 0 || index >= size) {
         throw new IllegalArgumentException("get failed,index must >=0 and <size");
     }
     if(index == 0){
         return removeFirst();
     }else if(index == size-1){
         return removeLast();
     }Node delNode = node(index);
     E e = delNode.e;Node prev = delNode.prev;
     Node next = delNode.next;//将删除节点的前后节点联系起来
     prev.next = next;
     next.prev = prev;//将删除节点从链表中断开
     delNode.next = null;
     delNode.prev = null;
     size--;
     return e;
 }

4.7 总代码

 import java.util.NoSuchElementException;public class DoubleLinkedList<E> {//节点类
     private class Node {
         E e;
         Node prev;
         Node next;public Node(E e) {
             this.e = e;
         }public Node() {
         }public Node(E e, Node prev, Node next) {
             this.e = e;
             this.prev = prev;
             this.next = next;
         }@Override
         public String toString() {
             return e.toString();
         }
     }private Node first;
     private Node last;
     private int size;public DoubleLinkedList(){
         first = last = null;
     }public int getSize(){
         return size;
     }public boolean isEmpty(){
         return size==0;
     }/**
      *  找到指定索引index所在的节点的元素值
      * @param index
      * @return
      */
     //时间复杂度O(n)
     public E get(int index){
         Node node = node(index);
         if(node == null){
             throw new IllegalArgumentException("get failed,index must >=0 and <size");
         }
         return node.e;
     }//表头和表尾时间复杂度为O(1)
     public Node getFirst(){
         return first;
     }public Node getLast(){
         return last;
     }/**
      * 返回指定索引的节点
      * @param index
      * @return
      */
     //时间复杂度O(n/2)本质上也是O(n)
     private Node node(int index){
         //检查index的合法性
         if (index < 0 || index >= size) {
             throw null;
         }Node curr = null;
         if(index<size/2){
             curr = first;
             for (int i = 0; i < index; i++) {
                 curr = curr.next;
             }
         }else {
             curr = last;
             for (int i = 0; i < size - index - 1; i++) {
                 curr = curr.prev;
             }
         }
         return curr;
     }/**
      * 修改双向链表指定索引的值
      * @param index
      * @param e
      */
     //时间复杂度O(n)
     public void set(int index,E e){
         Node node = node(index);
         if(node==null){
             throw new IllegalArgumentException("get failed,index must >=0 and <size");
         }
         node.e = e;
     }/**
      * 往链表的表头插入节点
      * @param e
      */
     //时间复杂度为O(1)
     public void addFirst(E e){
         Node newNode = new Node(e);
         if(first == null){
             last = newNode;
         }else{
             newNode.next = first;
             first.prev = newNode;
         }
         first = newNode;
         size++;
     }/**
      * 往链表的表尾插入节点
      * @param e
      */
     //时间复杂度为O(1)
     public void addLast(E e){
         Node newNode = new Node(e);
         if(first == null){
             first = newNode;
         }else{
             newNode.prev = last;
             last.next = newNode;
         }
         last = newNode;
         size++;
     }/**
      * 往指定索引位置插入节点
      * @param index
      * @param e
      */
     //时间复杂度O(n)
     public void add(int index,E e){
         //检查index的合法性
         if (index < 0 || index > size) {
             throw new IllegalArgumentException("get failed,index must >=0 and <=size");
         }
         if(index == size){
             addLast(e);
         }else if(index == 0){
             addFirst(e);
         }else{
             Node oldNode = node(index);
             Node prev = oldNode.prev;
             Node newNode = new Node(e,prev,oldNode);
             oldNode.prev = newNode;
             prev.next = newNode;
             size++;
         }
     }/**
      * 删除头节点
      * @return
      */
     //时间复杂度O(1)
     public E removeFirst(){
         if(first == null){
             throw new NoSuchElementException();
         }
         E e = first.e;
         Node next = first.next;
         if(next == null){
             first = null;
             last = null;
         }else{
             first.next = null;
             next.prev = null;
             first = next;
         }
         size--;
         return e;
     }/**
      * 删除尾节点
      * @return
      */
     //时间复杂度O(1)
     public E removeLast(){
         if(last == null){
             throw new NoSuchElementException();
         }
         E e = last.e;
         Node prev = last.prev;
         if(prev == null){
             last = null;
             first = null;
         }else{
             last.prev = null;
             prev.next = null;
             last = prev;
         }
         size--;
         return e;
     }/**
      * 删除指定索引的节点
      * @return
      */
     //时间复杂度O(n)
     public E remove(int index){
         //检查index的合法性
         if (index < 0 || index >= size) {
             throw new IllegalArgumentException("get failed,index must >=0 and <size");
         }
         if(index == 0){
             return removeFirst();
         }else if(index == size-1){
             return removeLast();
         }Node delNode = node(index);
         E e = delNode.e;Node prev = delNode.prev;
         Node next = delNode.next;//将删除节点的前后节点联系起来
         prev.next = next;
         next.prev = prev;//将删除节点从链表中断开
         delNode.next = null;
         delNode.prev = null;
         size--;
         return e;
     }
 }

5. 学习与分享

在这里插入图片描述

  • 15
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

懿所思

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值