数据结构 双链表与LinkedList

本节目标:

  1. 认识并且能够实现一个双链表
  2. 认识LinkedList类并且知道如何去使用

1.双链表

概念

在数据结构中,双链表(Doubly Linked List) 是一种常见的线性数据结构,它由一系列节点组成,每个节点不仅包含数据域,还包含两个指针域,分别指向其前驱节点(前一个节点)后继节点(后一个节点)。这种结构相比单链表(仅含后继指针),在节点访问和操作上更加灵活,可以表示成这样:

相较于单链表,双链表多了一个前驱节点prev,它储存的是这个节点的前一个节点的地址,即它指向这个节点的前一个节点

实现双链表

在上一节中,我们已经实现不带头不循环的单链表了,那么这里实现的是不带头不循环的双链表。以下是这个双链表包含的一些操作方法:

// 不带头双向链表实现

public class MyLinkedList {

//头插法

public void addFirst(int data){ }

//尾插法

public void addLast(int data){}

//任意位置插入,第一个数据节点为0号下标

public void addIndex(int index,int data){}

//判断双链表当中是否有包含key节点

public boolean contains(int key){

return false;

}

//删除第一次出现关键字为key的节点

public void remove(int key){}

//删除所有值为key的节点

public void removeAllKey(int key){}

//得到双链表的长度

public int size(){

return -1;

}

//打印双链表中的数据

public void display(){}

//清空双链表

public void clear(){}

在实现这些操作之前,先创建节点类 ListNode:

java 复制代码
public class ListNode {
    public int val;
    public ListNode prev;
    public ListNode next;

    public ListNode(int val) {
        this.val = val;
    }
}

还是老规矩,实现顺序由简到繁。

(1) 打印双链表中的数据

要求:将双链表中的数据一个个打印出来。

思路:实现方式与单链表的打印一样,通过遍历的方式,一个个去打印。

java 复制代码
public class MyLinkedList {
    ListNode head;
    ListNode last;

    //打印双链表中的数据
    public void display() {
        ListNode cur = this.head;
        while (cur != null) {
            System.out.print(cur.val + " ");
            cur = cur.next;
        }
        System.out.println();
    }
}

因为双链表中前驱结点指向该节点的前一个节点,因此它能够从后往前访问,所以用 last

来表示双链表的尾节点。

(2) 得到双链表的长度

要求:通过这个方法获得双链表的长度。

思路:定义一个变量用于计数,通过遍历的方式进行计数,最后返回这个变量。

java 复制代码
//得到双链表的长度
    public int size() {
        ListNode cur = this.head;
        int size = 0; 
        while (cur != null) {
            size++;
            cur = cur.next;
        }
        return size;
    }

(3) 判断双链表当中是否有包含key节点

要求:判断双链表中是否有包含key的节点,如果有就返回true,否则返回false。

思路:依旧通过遍历的方式去一个个判断。

java 复制代码
//判断双链表当中是否有包含key节点
    public boolean contains(int key){
        ListNode cur = this.head;
        while (cur != null) {
            if (cur.val == key) {
                return true;
            }
            cur = cur.next;
        }
        return false;
    }

(4) 头插法

要求:在双链表的头部位置插入一个新节点。

思路:这里与单链表不一样,需要考虑两种情况:1.插入前链表是空的;2.插入前链表不为空。

即下图所示:

java 复制代码
//头插法
    public void addFirst(int data){ 
        ListNode cur = new ListNode(data);
        if (this.head == null) {
            this.head = this.last = cur;
        }else {
            cur.next = this.head;
            this.head.prev = cur;
            this.head = cur;
        }
    }

进行测试:

java 复制代码
public class Test {
    public static void main(String[] args) {
        MyLinkedList myLinkedList = new MyLinkedList();
        myLinkedList.addFirst(1);
        myLinkedList.addFirst(2);
        myLinkedList.addFirst(3);
        myLinkedList.display();
    }
}

//运行结果
3 2 1 

符合预期。

(5) 尾插法

要求:在双链表的尾部插入一个新节点。

思路:这里也分两种情况,与头插法一样。需要注意的是,在链表不为空的情况下,我们应该对尾节点last进行操作,而不是头节点head。

java 复制代码
//尾插法
    public void addLast(int data){
        ListNode cur = new ListNode(data);
        if (this.head == null) {
            this.head = this.last = cur;
        }else {
            this.last.next = cur;
            cur.prev = this.last;
            this.last = cur;
        }
    }

进行测试:

java 复制代码
public class Test {
    public static void main(String[] args) {
        MyLinkedList myLinkedList = new MyLinkedList();
        myLinkedList.addLast(1);
        myLinkedList.addLast(2);
        myLinkedList.addLast(3);
        myLinkedList.display();
    }
}

//运行结果
1 2 3 

符合预期。

(6) 任意位置插入,第一个数据节点为0号下标

要求:在双链表中合法的任一位置插入一个新节点,第一个节点为0号下标。

思路:可以分3种情况处理:头插、尾插和中间插入。头插与尾插已经解决了,因此现在只需要解决中间插入即可。如下图所示:

当然,在进行插入操作前,需要判断插入位置是否合法。

插入位置非法异常类:

java 复制代码
public class InsertIllegalException extends RuntimeException {
    public InsertIllegalException() {
        
    }
    public InsertIllegalException(String str) {
        super(str);
    }
}

判断插入位置是否合法:

java 复制代码
//判断插入位置是否合法
    private void isIllegal(int index) {
        if (index < 0 || index > size()) {
            throw new InsertIllegalException("插入位置不合法!");
        }
    }

插入操作:

java 复制代码
//任意位置插入,第一个数据节点为0号下标
    public void addIndex(int index,int data){
        try {
            isIllegal(index);
            int len = size();
            //头插
            if (index == 0) {
                addFirst(data);
                return;
            }
            //尾插
            if (index == len) {
                addLast(data);
                return;
            }
            //中间插入
            ListNode newN = new ListNode(data);
            ListNode cur = this.head;
            while (index != 0) {
                cur = cur.next;
                index--;
            }
            newN.next = cur;
            cur.prev.next = newN;
            newN.prev = cur.prev;
            cur.prev = newN;
        }catch (InsertIllegalException e) {
            e.printStackTrace();
        }
    }

进行测试:

java 复制代码
public class Test {
    public static void main(String[] args) {
        MyLinkedList myLinkedList = new MyLinkedList();
        myLinkedList.addLast(1);
        myLinkedList.addLast(2);
        myLinkedList.addLast(3);
        myLinkedList.display();
        myLinkedList.addIndex(1,100);
        myLinkedList.display();
    }
}

//运行结果
1 2 3 
1 100 2 3 

符合预期。

(7) 删除第一次出现关键字为key的节点

要求:将双链表中第一次出现包含key的节点删掉。

思路:这里要分为三种情况:key在链表头部、key在链表尾部和key在链表中间,但在开始前需要判断链表是否为空。分析如下图所示:

java 复制代码
//删除第一次出现关键字为key的节点
    public void remove(int key){
        if (this.head == null) {
            System.out.println("链表为空!");
            return;
        }
        ListNode cur = this.head;
        boolean mark = false;  //用于标记链表中是否有key,有的话置为true
        while (cur != null) {
            //先找key
            if (cur.val == key) {
                mark = true;
                //当cur位于头节点
                if (cur == this.head) {
                    this.head = this.head.next;
                    this.head.prev = null;
                }else {
                    //位于中间或者尾节点
                    cur.prev.next = cur.next;
                    //位于尾节点
                    if (cur.next == null) {
                        this.last = this.last.prev;
                    }else {
                        //位于中间
                        cur.next.prev = cur.prev;
                    }
                }
                //删除完成!
                return;
            }
            cur = cur.next;
        }
        if (mark == false) {
            System.out.println("链表中没有" + key);
        }
    }

但是这里还有一个不足的地方,就是当链表中仅有一个节点时(即head == last且该节点的值为key),这时候 head.prev就会引发空指针异常,所以要对key在头节点的情况进行补充处理:

java 复制代码
//删除第一次出现关键字为key的节点
    public void remove(int key){
        if (this.head == null) {
            System.out.println("链表为空!");
            return;
        }
        ListNode cur = this.head;
        boolean mark = false;  //用于标记链表中是否有key,有的话置为true
        while (cur != null) {
            //先找key
            if (cur.val == key) {
                mark = true;
                //当cur位于头节点
                if (cur == this.head) {
                    this.head = this.head.next;
                    //当删除后链表不为空,即链表不仅仅有一个节点
                    if (this.head != null) {
                        this.head.prev = null;
                    }else {
                        //删除后链表为空,即链表仅有一个节点,这里更新尾节点last
                        this.last = null;
                    }
                }else {
                    //位于中间或者尾节点
                    cur.prev.next = cur.next;
                    //位于尾节点
                    if (cur.next == null) {
                        this.last = this.last.prev;
                    }else {
                        //位于中间
                        cur.next.prev = cur.prev;
                    }
                }
                //删除完成!
                return;
            }
            cur = cur.next;
        }
        if (mark == false) {
            System.out.println("链表中没有" + key);
        }
    }

进行测试:

java 复制代码
public class Test {
    public static void main(String[] args) {
        MyLinkedList myLinkedList = new MyLinkedList();
        myLinkedList.addLast(1);
        myLinkedList.addLast(2);
        myLinkedList.addLast(3);
        myLinkedList.addLast(1);
        myLinkedList.display();
        myLinkedList.remove(1);
        myLinkedList.display();
    }
}

//运行结果:
1 2 3 1 
2 3 1 

符合预期。

(8) 删除所有值为key的节点

要求:将链表中所有值为key的节点都删掉。

思路:只需要在删除第一次出现关键字为key的节点的方法的基础上进行修改即可,把return去掉,使得循环继续进行,继续找key的节点,继续删除,直到遍历结束,此时已将所有值为key的节点删除。

java 复制代码
//删除所有值为key的节点
    public void removeAllKey(int key) {
        if (this.head == null) {
            System.out.println("链表为空!");
            return;
        }
        ListNode cur = this.head;
        boolean mark = false;  //用于标记链表中是否有key,有的话置为true
        while (cur != null) {
            //先找key
            if (cur.val == key) {
                mark = true;
                //当cur位于头节点
                if (cur == this.head) {
                    this.head = this.head.next;
                    //当删除后链表不为空,即链表不仅仅有一个节点
                    if (this.head != null) {
                        this.head.prev = null;
                    } else {
                        //删除后链表为空,即链表仅有一个节点,这里更新尾节点last
                        this.last = null;
                    }
                } else {
                    //位于中间或者尾节点
                    cur.prev.next = cur.next;
                    //位于尾节点
                    if (cur.next == null) {
                        this.last = this.last.prev;
                    } else {
                        //位于中间
                        cur.next.prev = cur.prev;
                    }
                }
            }
            cur = cur.next;
        }
        if (mark == false) {
            System.out.println("链表中没有" + key);
        }
    }

不过这里可能会有一个潜在的问题,就是当我们删除某一个节点,cur指向的节点已经被移除,此时执行cur = cur.next可能会出现问题。举个例子:

现在有一个双链表:

2 <-> 3 <-> 2 <-> 4

现在要删掉所有的2

1.首先,cur指向第一个2,删除后head变为3节点;

2.接着执行cur = cur.next时,此时的cur仍然指向已经被删除的第一个 2 节点;

3.这个节点的next虽然可能还指向 3,但这是不安全的(已删除节点的引用应该被视为无效)

因此出于安全考虑,可以用一个变量记录cur的=指向的下一个节点。

java 复制代码
//删除所有值为key的节点
    public void removeAllKey(int key) {
        if (this.head == null) {
            System.out.println("链表为空!");
            return;
        }
        ListNode cur = this.head;
        boolean mark = false;  //用于标记链表中是否有key,有的话置为true
        while (cur != null) {
            ListNode curN = cur.next;
            //先找key
            if (cur.val == key) {
                mark = true;
                //当cur位于头节点
                if (cur == this.head) {
                    this.head = this.head.next;
                    //当删除后链表不为空,即链表不仅仅有一个节点
                    if (this.head != null) {
                        this.head.prev = null;
                    } else {
                        //删除后链表为空,即链表仅有一个节点,这里更新尾节点last
                        this.last = null;
                    }
                } else {
                    //位于中间或者尾节点
                    cur.prev.next = cur.next;
                    //位于尾节点
                    if (cur.next == null) {
                        this.last = this.last.prev;
                    } else {
                        //位于中间
                        cur.next.prev = cur.prev;
                    }
                }
            }
            cur = curN;
        }
        if (mark == false) {
            System.out.println("链表中没有" + key);
        }
    }

进行测试:

java 复制代码
public class Test {
    public static void main(String[] args) {
        MyLinkedList myLinkedList = new MyLinkedList();
        myLinkedList.addLast(2);
        myLinkedList.addLast(1);
        myLinkedList.addLast(2);
        myLinkedList.addLast(3);
        myLinkedList.addLast(2);
        myLinkedList.display();
        myLinkedList.removeAllKey(2);
        myLinkedList.display();
    }
}

//运行结果:
2 1 2 3 2 
1 3 

符合预期。

(9) 清空双链表

要求:将双链表清空。

思路:粗暴的方式就不介绍了(直接将head和last置空),温柔的方式:通过遍历,一个个将节点的prev和next置空,最后将head和last置空。

java 复制代码
//清空双链表
    public void clear() {
        ListNode cur = this.head;
        while (cur != null) {
            ListNode curN = cur.next;
            cur.prev = null;
            cur.next = null;
            cur = curN;
        }
        this.head = null;
        this.last = null;
    }

进行测试:

java 复制代码
public class Test {
    public static void main(String[] args) {
        MyLinkedList myLinkedList = new MyLinkedList();
        myLinkedList.addLast(2);
        myLinkedList.addLast(1);
        myLinkedList.addLast(2);
        myLinkedList.addLast(3);
        myLinkedList.addLast(2);
        myLinkedList.display();
        myLinkedList.clear();
        myLinkedList.display();
    }
}

//运行结果:
2 1 2 3 2 

符合预期。

到此,我们就成功实现了一个不带头不循环的双链表。

完整代码

java 复制代码
public class MyLinkedList {
    ListNode head;
    ListNode last;

    //打印双链表中的数据
    public void display() {
        ListNode cur = this.head;
        while (cur != null) {
            System.out.print(cur.val + " ");
            cur = cur.next;
        }
        System.out.println();
    }

    //得到双链表的长度
    public int size() {
        ListNode cur = this.head;
        int size = 0;
        while (cur != null) {
            size++;
            cur = cur.next;
        }
        return size;
    }

    //判断双链表当中是否有包含key节点
    public boolean contains(int key){
        ListNode cur = this.head;
        while (cur != null) {
            if (cur.val == key) {
                return true;
            }
            cur = cur.next;
        }
        return false;
    }

    //头插法
    public void addFirst(int data){
        ListNode cur = new ListNode(data);
        if (this.head == null) {
            this.head = this.last = cur;
        }else {
            cur.next = this.head;
            this.head.prev = cur;
            this.head = cur;
        }
    }

    //尾插法
    public void addLast(int data){
        ListNode cur = new ListNode(data);
        if (this.head == null) {
            this.head = this.last = cur;
        }else {
            this.last.next = cur;
            cur.prev = this.last;
            this.last = cur;
        }
    }

    //判断插入位置是否合法
    private void isIllegal(int index) {
        if (index < 0 || index > size()) {
            throw new InsertIllegalException("插入位置不合法!");
        }
    }
    //任意位置插入,第一个数据节点为0号下标
    public void addIndex(int index,int data){
        try {
            isIllegal(index);
            int len = size();
            //头插
            if (index == 0) {
                addFirst(data);
                return;
            }
            //尾插
            if (index == len) {
                addLast(data);
                return;
            }
            //中间插入
            ListNode newN = new ListNode(data);
            ListNode cur = this.head;
            while (index != 0) {
                cur = cur.next;
                index--;
            }
            newN.next = cur;
            cur.prev.next = newN;
            newN.prev = cur.prev;
            cur.prev = newN;
        }catch (InsertIllegalException e) {
            e.printStackTrace();
        }
    }

    //删除第一次出现关键字为key的节点
    public void remove(int key){
        if (this.head == null) {
            System.out.println("链表为空!");
            return;
        }
        ListNode cur = this.head;
        boolean mark = false;  //用于标记链表中是否有key,有的话置为true
        while (cur != null) {
            //先找key
            if (cur.val == key) {
                mark = true;
                //当cur位于头节点
                if (cur == this.head) {
                    this.head = this.head.next;
                    //当删除后链表不为空,即链表不仅仅有一个节点
                    if (this.head != null) {
                        this.head.prev = null;
                    }else {
                        //删除后链表为空,即链表仅有一个节点,这里更新尾节点last
                        this.last = null;
                    }
                }else {
                    //位于中间或者尾节点
                    cur.prev.next = cur.next;
                    //位于尾节点
                    if (cur.next == null) {
                        this.last = this.last.prev;
                    }else {
                        //位于中间
                        cur.next.prev = cur.prev;
                    }
                }
                //删除完成!
                return;
            }
            cur = cur.next;
        }
        if (mark == false) {
            System.out.println("链表中没有" + key);
        }
    }

    //删除所有值为key的节点
    public void removeAllKey(int key) {
        if (this.head == null) {
            System.out.println("链表为空!");
            return;
        }
        ListNode cur = this.head;
        boolean mark = false;  //用于标记链表中是否有key,有的话置为true
        while (cur != null) {
            ListNode curN = cur.next;
            //先找key
            if (cur.val == key) {
                mark = true;
                //当cur位于头节点
                if (cur == this.head) {
                    this.head = this.head.next;
                    //当删除后链表不为空,即链表不仅仅有一个节点
                    if (this.head != null) {
                        this.head.prev = null;
                    } else {
                        //删除后链表为空,即链表仅有一个节点,这里更新尾节点last
                        this.last = null;
                    }
                } else {
                    //位于中间或者尾节点
                    cur.prev.next = cur.next;
                    //位于尾节点
                    if (cur.next == null) {
                        this.last = this.last.prev;
                    } else {
                        //位于中间
                        cur.next.prev = cur.prev;
                    }
                }
            }
            cur = curN;
        }
        if (mark == false) {
            System.out.println("链表中没有" + key);
        }
    }

    //清空双链表
    public void clear() {
        ListNode cur = this.head;
        while (cur != null) {
            ListNode curN = cur.next;
            cur.prev = null;
            cur.next = null;
            cur = curN;
        }
        this.head = null;
        this.last = null;
    }
}

2.LinkedList

2.1 概念

LinkedList的底层是双向链表结构,由于链表没有将元素存储在连续的空间中,元素存储在单独的节点中,然后通过引用将节点连接起来了,因此在在任意位置插入或者删除元素时,不需要搬移元素,效率比较高。

在集合框架中,LinkedList也实现了List接口,具体如下:

【说明】

  1. LinkedList实现了List接口
  2. LinkedList的底层使用了双向链表
  3. LinkedList没有实现RandomAccess接口,因此LinkedList不支持随机访问
  4. LinkedList的任意位置插入和删除元素时效率比较高,时间复杂度为O(1)
  5. LinkedList比较适合任意位置插入的场景

2.2 使用

构造方法

LinkedList的构造方法:

方法 解释
LinkedList() 无参构造
public LinkedList(Collection <? extends E> c) 使用其他集合容器中元素构造List

举例:

java 复制代码
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class Test {
    public static void main(String[] args) {
        //使用第一种构造方法,构造一个空的LinkedList
        LinkedList<Integer> linkedList = new LinkedList<>();
        //List<Integer> linkedList = new LinkedList<>();这种写法等于上一条语句,
        //因此LinkedList实现了List接口,因此可以用List去接收
        //可以插入数据
        linkedList.addFirst(1);
        linkedList.addFirst(2);
        linkedList.addFirst(3);
        System.out.println(linkedList);
        System.out.println("=============");

        //使用第二种构造方法
        List<Integer> list = new ArrayList<>();  //创建一个ArrayList,并往里面插入一些数据
        list.add(1);
        list.add(2);
        list.add(3);
        System.out.println(list);
        System.out.println("=============");

        //可以ArrayList构造LinkedList
        List<Integer> list1 = new LinkedList<>(list);
        System.out.println(list1);
    }
}

//运行结果:
[3, 2, 1]
=============
[1, 2, 3]
=============
[1, 2, 3]

其他常用方法

方法 解释
boolean add(E e) 尾插 e
void add(int index, E element) 将 e 插入到 index 位置
boolean addAll(Collection <? extends E> c) 尾插 c 中的元素
E remove(int index) 删除 index 位置元素
boolean remove(Object o) 删除遇到的第一个 o
E get(int index) 获取下标 index 位置元素
E set(int index, E element) 将下标 index 位置元素设置为 element
void clear() 清空
boolean contains(Object o) 判断 o 是否在线性表中
int indexOf(Object o) 返回第一个 o 所在下标
int lastIndexOf(Object o) 返回最后一个 o 的下标
List subList(int fromIndex, int toIndex) 截取部分 list

LinkedList的遍历

LinkedList的遍历与ArrayList的遍历一样,有三种方法,分别是通过for遍历、通过for-each遍历和通过迭代器遍历。具体请看:

数据结构 ArrayList与顺序表-CSDN博客

2.3 ArrayList与LinkedList的区别

不同点 ArrayList LinkedList
存储空间上 物理上一定连续 逻辑上连续,但物理上不一定连续
随机访问 支持:O(1) 不支持:O(N)
头插 需要搬移元素,效率低O(N) 只需修改引用的指向,时间复杂度为O(1)
插入 空间不够时需要扩容 没有容量的概念
应用场景 元素高效存储+频繁访问 任意位置插入和删除频繁

到此,双链表与LinkedList的内容完结。感谢您的观看,如有不对的地方还请指出,谢谢!

相关推荐
数据智能老司机5 分钟前
图算法趣味学——最短路径
数据结构·算法·云计算
gopher_looklook21 分钟前
Go并发实战:singleflight 源码解读与二次封装
数据结构·后端·go
终焉代码1 小时前
【C++】STL二叉搜索树——map与set容器的基础结构
开发语言·数据结构·c++
啊阿狸不会拉杆4 小时前
《算法导论》第 15 章 - 动态规划
数据结构·c++·算法·排序算法·动态规划·代理模式
重生之我是Java开发战士8 小时前
【数据结构】深入理解顺序表与通讯录项目的实现
数据结构·算法
啊阿狸不会拉杆18 小时前
《算法导论》第 14 章 - 数据结构的扩张
数据结构·c++·算法·排序算法
闪电麦坤9518 小时前
数据结构:栈(Stack)
数据结构
我今晚不熬夜20 小时前
使用单调栈解决力扣第42题--接雨水
java·数据结构·算法·leetcode
lifallen21 小时前
Hadoop MapReduce过程
大数据·数据结构·hadoop·分布式·apache