用java简单模拟单链表和双向链表 and LinkedList类的介绍

168 阅读13分钟

1.链表是什么?

1.1 链表的概念与结构

  链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的

eca8c127b22f44f4941f2c53f27ae160.png

  其中 Data 用来存储数据元素,而 Next用来存储后一个Data的地址,最后一个结点的指针域为空。

  这就是单链表,当然链表不止是只有这一种形式,还有双链表:

3c9cf0a9ea5d4426a2b2bc107a4056f9.png

  双链表的特点就是不仅知道后一个节点的位置,还知道前一个节点的位置,这篇文章就是要模拟这两种链表。

2.模拟实现单链表

2.1 开始模拟

  (1)单链表是一个一个节点连接而成的,我们用什么来表示这一个一个节点呢?答案是内部类:

public class MySingleList {

    private static class Node{
        public int val;//存储的元素
        public Node next;//下一个节点

        //构造方法
        public Node(int val){
            this.val = val;
        }
    }
}

  (2)由于单链表的物理存储位置是松散的,不像数组那样自带开始位置的标识(也就是 数组名[0]),这里的单链表需要一个变量来代表开始位置的节点,以方便我们找到它。(这里没有实际的头节点,仅仅是用一个变量来指向第一个元素来代表头节点,初始值为null。)

public class MySingleList {

    private static class Node{
        public int val;//存储的元素
        public Node next;//下一个节点

        //构造方法
        public Node(int val){
            this.val = val;
        }
    }
    
    //用 head 变量来指向第一个元素
    private Node head;
}

  (3)实现打印单链表的方法,要实现这个方法必须要知道如何遍历单链表:

//打印单链表
public void print(){
    Node cur = this.head;//从第一个元素开始
    
    //遍历单链表,当cur为null时就表示链表遍历完了。
    while(cur != null){
        System.out.print(cur.val + " ");
        cur = cur.next;//将cur移到下一个元素,这里的 cur.next 就表示 cur 的后一个元素。
    }
    System.out.println();//换行
}

  (4)头插的实现:是什么头插?就是插入元素的时候,把它插到最前面,这时它就是第一个节点了。

单链表.drawio.png

//头插
public void addFirst(int val){

    //当head为空的时候下面代码也成立
    Node newNode = new Node(val);
    newNode.next = head;
    head = newNode;
}

  (5)头插已经实现了,现在来实现尾插,要注意的是,尾插的时候我们必须要知道最后一个节点才能实现尾插,如何找到最后一个节点?答案是遍历。

单链表-第 2 页.drawio.png

//尾插法
public void addLast(int val){

    Node newNode = new Node(val);//新节点

    if(head == null){//当链表为空的时候
        head = newNode;
    }else {
        Node cur = head;//cur用来当作工具
        //遍历找到最后一个节点
        while(cur.next != null){//要注意这里是 cur.next
            cur = cur.next;
        }
        //找到后将 newNode 插入到最后
        cur.next = newNode;
    }
}

  (6)在index前插入数据:我们需要知道index前一个节点才能进行插入操作,这里就写一个findIndexNode方法来查找节点。

//查找index下标的节点
public Node findIndexNode(int index){

    Node cur = head;
    //查找index下标的节点
    while(index-- != 0){
        cur = cur.next;
    }
    return cur;
}

//获取链表的长度
public int size(){
    Node cur = head;
    int size = 0;
    //遍历链表
    while(cur != null){
        size++;
        cur = cur.next;
    }
    return size;
}

//在index前插入指定的数据(第一个数据为 0 下标)
public void add(int index,int val) throws IndexOutOfBoundsException{
    //index == size() 的时候是尾插,所以这里 “>” 就行了
    if(index < 0 || index > size()){
        //抛出一个异常(这个异常是java自带的,表示下标越界)
        throw new IndexOutOfBoundsException("index 不合法!");
    } else if (index == 0) {
        addFirst(val);//头插
    } else if (index == size()) {
        addLast(val);//尾插
    } else {
        //先找到前一个节点
        Node cur = findIndexNode(index - 1);
        Node newNode = new Node(val);
        //修改指向
        newNode.next = cur.next;
        cur.next = newNode;
    }
}
单链表-第 3 页.drawio.png

  (7)对于查找板块,我们要实现如下方法:

//查找单链表当中是否包含关键字key
public boolean contains(int key){
    Node cur = head;

    while(cur != null){
        if(cur.val == key){
            return true;
        }
        cur = cur.next;
    }
    return false;
}

//查找下标为 index 位置的元素
public  int get(int index){
    if(index < 0 || index >= size()){
        throw new IndexOutOfBoundsException("index 非法!");
    }
    Node cur = head;
    //为什么从 1 开始?因为cur = head
    for (int i = 1; i <= index; i++) {
        cur = cur.next;
    }
    return cur.val;
}

  (8)删除板块:删除第一次出现关键字为key的节点,我们得先找到key节点的前一个节点,还要记录key这个节点。

单链表-第 4 页.drawio.png
//查找关键字 key 的前一个节点
private Node findPrevOfKey(int key) {
    Node cur = head;
    
    while(cur.next != null){
        if(cur.next.val == key){
            return cur;
        }
        cur = cur.next;
    }
    return null;
}

//删除第一次出现关键字为key的节点
public void remove(int key){
    //如果链表为空
    if(head == null){
        return;
    }
    //如果key是头节点
    if(head.val == key){
        head = head.next;//如果只有 head 这一个节点也是可以这样操作的,这时head = null。
        return;
    }
    //找到 key 的前一个节点
    Node cur = findPrevOfKey(key);
    if(cur == null){
        System.out.println("没有你要删除的数字!");
        return;
    }
    //删除节点
    Node tmp = cur.next;
    cur.next = tmp.next;
}

  (9)删除所有值为key的节点,删除逻辑与上一个类似,但是这里需要两个指针来遍历整个链表。

//删除所有值为key的节点
public void removeAllKey(int key){
    if(head == null){
        return;
    }
    //头删:当头节点是key时
    while(head.val == key){
        head = head.next;
    }

    Node cur = head.next;
    Node prev = head;//指向cur的前一个
    //遍历链表
    while(cur != null){
        if(cur.val == key){
            prev.next = cur.next;
        } else {
            prev = cur;
        }
        cur = cur.next;
    }
}

2.2 汇总

public class MySingleList {

    private static class Node{
        public int val;//存储的元素
        public Node next;//下一个节点

        //构造方法
        public Node(int val){
            this.val = val;
        }
    }

    //用 head 变量来指向第一个元素
    private Node head;

    //打印单链表
    public void print(){
        Node cur = this.head;//从第一个元素开始

        while(cur != null){
            System.out.print(cur.val + " ");
            cur = cur.next;//将cur移到下一个元素,这里的 cur.next 就表示 cur 的后一个元素。
        }
        System.out.println();//换行
    }

    //头插
    public void addFirst(int val){
        //当head为空的时候下面代码也成立
        Node newNode = new Node(val);
        newNode.next = head;
        head = newNode;
    }

    //尾插法
    public void addLast(int val){

        Node newNode = new Node(val);//新节点

        if(head == null){//当链表为空的时候
            head = newNode;
        }else {
            Node cur = head;//cur用来当作工具
            //遍历找到最后一个节点
            while(cur.next != null){//要注意这里是 cur.next
                cur = cur.next;
            }
            //找到后将 newNode 插入到最后
            cur.next = newNode;
        }
    }

    //查找index下标的节点
    public Node findIndexNode(int index){

        Node cur = head;
        //查找index下标的节点
        while(index-- != 0){
            cur = cur.next;
        }
        return cur;
    }

    //获取链表的长度
    public int size(){
        Node cur = head;
        int size = 0;
        //遍历链表
        while(cur != null){
            size++;
            cur = cur.next;
        }
        return size;
    }

    //在index前插入指定的数据(第一个数据为 0 下标)
    public void add(int index,int val) throws IndexOutOfBoundsException{
        //index == size() 的时候是尾插,所以这里 “>” 就行了
        if(index < 0 || index > size()){
            //抛出一个异常(这个异常是java自带的)
            throw new IndexOutOfBoundsException("index 不合法!");
        } else if (index == 0) {
            addFirst(val);//头插
        } else if (index == size()) {
            addLast(val);//尾插
        } else {
            //先找到前一个节点
            Node cur = findIndexNode(index - 1);
            Node newNode = new Node(val);
            //修改指向
            newNode.next = cur.next;
            cur.next = newNode;
        }
    }


    //查找单链表当中是否包含关键字key
    public boolean contains(int key){
        Node cur = head;

        while(cur != null){
            if(cur.val == key){
                return true;
            }
            cur = cur.next;
        }
        return false;
    }


    //查找下标为 index 位置的元素
    public  int get(int index){
        if(index < 0 || index >= size()){
            throw new IndexOutOfBoundsException("index 非法!");
        }
        Node cur = head;
        //为什么从 1 开始?因为cur = head
        for (int i = 1; i <= index; i++) {
            cur = cur.next;
        }
        return cur.val;
    }

    //查找关键字 key 的前一个节点
    private Node findPrevOfKey(int key) {
        Node cur = head;

        while(cur.next != null){
            if(cur.next.val == key){
                return cur;
            }
            cur = cur.next;
        }
        return null;
    }

    //删除第一次出现关键字为key的节点
    public void remove(int key){
        //如果链表为空
        if(head == null){
            return;
        }
        //如果key是头节点
        if(head.val == key){
            head = head.next;//如果只有 head 这一个节点也是可以这样操作的,这时head = null。
            return;
        }
        //找到 key 的前一个节点
        Node cur = findPrevOfKey(key);
        if(cur == null){
            System.out.println("没有你要删除的数字!");
            return;
        }
        //删除节点
        Node tmp = cur.next;
        cur.next = tmp.next;
    }

    //删除所有值为key的节点
    public void removeAllKey(int key){
        if(head == null){
            return;
        }
        //头删:当头节点是key时
        while(head.val == key){
            head = head.next;
        }

        Node cur = head.next;
        Node prev = head;//指向cur的前一个
        //遍历链表
        while(cur != null){
            if(cur.val == key){
                prev.next = cur.next;
            } else {
                prev = cur;
            }
            cur = cur.next;
        }
    }
}

3.模拟实现双向链表

3.1 开始模拟

  双向链表的示意图:

单链表-第 6 页.drawio.png

  每个节点包含两个指针:一个指向前一个节点,一个指向后一个节点。与单向链表不同的是,双向链表可以在任意方向上遍历。因此,它可以从前往后遍历,也可以从后往前遍历。

  其实双向链表的很多操作都与单向链表相同。

  (1)节点的实现:

public class MyLinkedList {
    
    private static class Node{
        public int val;
        public Node prev;//前一个节点
        public Node next;//后一个节点

        public Node(int val) {
            this.val = val;
        }
    }
    
    private Node head;//指向头节点,默认值为null
    private Node tail;//指向尾节点,默认值为null
}

  (2)打印整个双向链表、获取链表长度,对于双向链表的遍历,跟单向链表的操作是相同的。

//打印双向链表
public void print(){

    if (this.head == null) {
        System.out.println("null");
        return;
    }

    Node cur = head;
    while (cur != null) {
        System.out.print(cur.val+" ");
        cur = cur.next;
    }
    System.out.println();
}

//获取双向链表的长度
public int size(){
    Node cur = head;
    int tmp = 0;
    while(cur != null){
        tmp++;
        cur = cur.next;
    }
    return tmp;
}

  (3)头插:

单链表-第 6.1 页.drawio.png

//头插
public void addFirst(int data){
    Node newNode = new Node(data);
    //当插入第一个数据的时候,就把这个数据当成 头节点与尾节点
    if(head == null){
        head = newNode;
        tail = newNode;
    } else {
        newNode.next = head;
        head.prev = newNode;
        head = newNode;
    }
}

  (4)尾插:

//尾插
public void addLast(int data){
    Node newNode = new Node(data);
    //当插入第一个数据的时候,就把这个数据当成 头节点与尾节点
    if(head == null){
        head = newNode;
        tail = newNode;
    } else {
        tail.next = newNode;
        newNode.prev = tail;
        tail = newNode;
    }
}

  (5)任意位置插入:

单链表-第 7 页.drawio.png


//查找 index 位置的节点,第一个下标为0
public Node findIndexNode(int index) throws IndexOutOfBoundsException{
    if(index<0 || index >= this.size()){
        //这里我们用java自带的异常
        throw new IndexOutOfBoundsException("index位置非法!");
    }
    Node cur = head;
    while(index != 0){
        cur = cur.next;
        index--;
    }
    return cur;
}

//任意位置插入,第一个数据节点为0号下标
public void addIndex (int index,int data){
    if(index<0 || index > this.size()){
        //这里我们用java自带的异常
        throw new IndexOutOfBoundsException("index位置非法!");
    }
    if(index == 0){//当下标为0时,头插
        addFirst(data);
        return;
    } else if (index == size()) {//当下标为 size() 时(最后一个数据的后一个),尾插
        addLast(data);
        return;
    }

    //找到 index 的前一个节点
    Node cur = findIndexNode(index - 1);//调用了上面的方法
    Node newNode = new Node(data);
    newNode.prev = cur;
    newNode.next = cur.next;
    cur.next.prev = newNode;
    cur.next = newNode;
}

  (6)查找是否包含关键字key:

//查找是否包含关键字key 是否在单链表当中
public boolean contains(int key){
    Node cur = head;

    while(cur != null){
        if(cur.val == key){
            return true;
        }
        cur = cur.next;
    }
    return false;
}

  (7)删除第一次出现关键字为key的节点:这个删除操作要分很多种情况。

① 当我删除的keyhead的时候,这个时候又要分两种情况:1.只有一个节点。2.多个节点。

key是尾节点tail的时候

key是中间节点的时候

//删除第一次出现关键字为key的节点
public void remove(int key){
    if(head == null){
        return;
    }

    //如果删除的是头节点
    if(head.val == key){
        //如果只有一个节点
        if(head.next == null){
            head = null;
            tail = null;
            return;
        }
        //head后面还有节点的时候。
        head = head.next;
        head.prev = null;
    }

    //移动到 第一个 key
    Node cur = head.next;
    while(cur.val != key){
        cur = cur.next;
        //链表中没有key这个数据
        if(cur == null){
            return;
        }
    }
    //(在while循环后,是保证第一次遇到key)如果key是最后一个节点,要最特殊处理。
    if(tail == cur){
        tail = tail.prev;
        tail.next = null;
        return;
    }

    //key在中间节点的情况
    Node curPrev = cur.prev;
    curPrev.next = cur.next;
    cur.next.prev = cur.prev;
    cur = null;
}

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

//删除所有值为key的节点
public void removeAllKey(int key){
    if(head == null){
        return;
    }

    //这里为什么是用 while ? 因为我们是要删除所有的key,只要 key 是 head 节点都是需要特殊处理的。
    while(head.val == key){

        //当只有head节点的时候
        if(head.next == null){
            head = null;
            tail = null;//必须也要让 tail = null
            return;
        }

        head = head.next;
        head.prev = null;//必须要做这一步,这样才能让前一个节点彻底断开
    }
    //普通情况
    Node cur = head.next;
    while(cur != null){
        if(cur.val == key){
            //如果key是tail的时候
            if(cur.next == null){
                tail = tail.prev;
                tail.next = null;
                return;
            }
            cur.prev.next = cur.next;
            cur.next.prev = cur.prev;
        }
        cur = cur.next;
    }
}

  (9)删除整个链表,注意,这不是简单的将头尾指针置为null就够了,需要将每一个节点的prevnext都要置为null

//删除整个链表
public void clear(){
    if(head == null){
        return;
    }

    Node cur = head;
    Node curNext = cur.next;
    //先将头尾指针释放
    tail = null;
    head = null;
    //遍历每一个节点,将每一个节点的 prev 和 next 置为null
    while(cur != null){
        cur.prev = null;
        cur.next = null;
        
        cur = curNext;
        //当 curNext = null时 遍历结束
        if(curNext != null){
            curNext = cur.next;  
        }
    }
}

3.2 汇总


public class MyLinkedList {

    private static class Node{
        public int val;
        public Node prev;//前一个节点
        public Node next;//后一个节点

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

    private Node head;//指向头节点
    private Node tail;//指向尾节点

    //打印双向链表
    public void print(){

        if (this.head == null) {
            System.out.println("null");
            return;
        }

        Node cur = head;
        while (cur != null) {
            System.out.print(cur.val+" ");
            cur = cur.next;
        }
        System.out.println();
    }

    //获取双向链表的长度
    public int size(){
        Node cur = head;
        int tmp = 0;
        while(cur != null){
            tmp++;
            cur = cur.next;
        }
        return tmp;
    }

    //头插
    public void addFirst(int data){
        Node newNode = new Node(data);
        //当插入第一个数据的时候,就把这个数据当成 头节点与尾节点
        if(head == null){
            head = newNode;
            tail = newNode;
        } else {
            newNode.next = head;
            head.prev = newNode;
            head = newNode;
        }
    }

    //尾插
    public void addLast(int data){
        Node newNode = new Node(data);
        if(head == null){
            head = newNode;
            tail = newNode;
        } else {
            tail.next = newNode;
            newNode.prev = tail;
            tail = newNode;
        }
    }

    //查找 index 位置的节点,第一个下标为0
    public Node findIndexNode(int index) throws IndexOutOfBoundsException{
        if(index<0 || index >= this.size()){
            //这里我们用java自带的异常
            throw new IndexOutOfBoundsException("index位置非法!");
        }
        Node cur = head;
        while(index != 0){
            cur = cur.next;
            index--;
        }
        return cur;
    }
    //任意位置插入,第一个数据节点为0号下标
    public void addIndex (int index,int data){
        if(index<0 || index > this.size()){
            //这里我们用java自带的异常
            throw new IndexOutOfBoundsException("index位置非法!");
        }
        if(index == 0){//当下标为0时,头插
            addFirst(data);
            return;
        } else if (index == size()) {//当下标为 size() 时(最后一个数据的后一个),尾插
            addLast(data);
            return;
        }

        //找到 index 的前一个节点
        Node cur = findIndexNode(index - 1);
        Node newNode = new Node(data);
        newNode.prev = cur;
        newNode.next = cur.next;
        cur.next.prev = newNode;
        cur.next = newNode;
    }

    //查找是否包含关键字key 是否在单链表当中
    public boolean contains(int key){
        Node cur = head;

        while(cur != null){
            if(cur.val == key){
                return true;
            }
            cur = cur.next;
        }
        return false;
    }

    //删除第一次出现关键字为key的节点
    public void remove(int key){
        if(head == null){
            return;
        }

        //如果删除的是头节点
        if(head.val == key){
            //如果只有一个节点
            if(head.next == null){
                head = null;
                tail = null;
                return;
            }
            //多个节点
            head = head.next;
            head.prev = null;
        }

        //移动到 第一个 key
        Node cur = head.next;
        while(cur.val != key){
            cur = cur.next;
            //链表中没有key这个数据
            if(cur == null){
                return;
            }
        }
        //(在while循环后,是保证第一次遇到key)如果key是最后一个节点,要最特殊处理。
        if(tail == cur){
            tail = tail.prev;
            tail.next = null;
            return;
        }

        //key在中间节点的情况
        Node curPrev = cur.prev;
        curPrev.next = cur.next;
        cur.next.prev = cur.prev;
        cur = null;
    }


    //删除所有值为key的节点
    public void removeAllKey(int key){
        if(head == null){
            return;
        }

        //这里为什么是用 while ? 因为我们是要删除所有的key,只要 key 是 head 节点都是需要特殊处理的。
        while(head.val == key){

            //当只有head节点的时候
            if(head.next == null){
                head = null;
                tail = null;//必须也要让 tail = null
                return;
            }

            head = head.next;
            head.prev = null;//必须要做这一步,这样才能让前一个节点彻底断开
        }
        //普通情况
        Node cur = head.next;
        while(cur != null){
            if(cur.val == key){

                //如果key是tail的时候
                if(cur.next == null){
                    tail = tail.prev;
                    tail.next = null;
                    return;
                }
                cur.prev.next = cur.next;
                cur.next.prev = cur.prev;
            }
            cur = cur.next;
        }
    }


    //删除整个链表
    public void clear(){
        if(head == null){
            return;
        }

        Node cur = head;
        Node curNext = cur.next;
        //先将头尾指针释放
        tail = null;
        head = null;
        //遍历每一个节点,将每一个节点的 prev 和 next 置为null
        while(cur != null){
            cur.prev = null;
            cur.next = null;

            cur = curNext;
            //当 curNext = null时 遍历结束
            if(curNext != null){
                curNext = cur.next;
            }
        }
    }
}

  上面只是完成了常用的方法,大家在练习的时候可以进行扩展。

4.LinkedList 类

4.1 什么是 LinkedList ?

详细解读Java中的ArrayList集合类 以及 用Java简单模拟实现顺序表 - 掘金 (juejin.cn)

  Java中的LinkedList类是一种实现了List接口的双向链表数据结构。它可以在任意位置添加或删除元素。

  与ArrayList不同,LinkedList的访问时间复杂度是O(n),因为需要遍历链表来查找元素,但是在添加和删除元素时的时间复杂度是O(1),只需要修改指针即可。LinkedList适用于频繁的插入和删除操作,但不适用于随机访问。

4.2 LinkedList 类的使用

4.2.1 LinkedList 类的构造方法

构造方法描述
LinkedList()创建一个空的链表
LinkedList(Collection<? extends E> c)创建一个包含指定集合中所有元素的链表

  其中,LinkedList()构造方法创建一个空的链表,LinkedList(Collection<? extends E> c)构造方法创建一个包含指定集合中所有元素的链表。如果需要创建一个已经包含元素的链表对象,可以使用该构造方法。

import java.util.LinkedList;
import java.util.ArrayList;
import java.util.List;

public class LinkedListExample {
    public static void main(String[] args) {
        // 创建一个ArrayList对象
        List<String> arrayList = new ArrayList<>();
        arrayList.add("apple");
        arrayList.add("banana");
        arrayList.add("orange");

        // 使用LinkedList(Collection<? extends E> c)构造方法创建一个包含指定集合中所有元素的链表
        LinkedList<String> linkedList = new LinkedList<>(arrayList);

        // 输出链表中的元素
        System.out.println(linkedList); // [apple, banana, orange]
    }
}

4.2.2 LinkedList 类中常用方法

方法名描述
add(E e)在链表末尾添加元素
add(int index, E element)在指定位置插入元素
addFirst(E e)在链表头部添加元素
addLast(E e)在链表末尾添加元素
clear()删除链表中的所有元素
contains(Object o)判断链表中是否包含指定元素
get(int index)获取指定位置的元素
getFirst()获取链表头部的元素
getLast()获取链表尾部的元素
indexOf(Object o)返回指定元素在链表中第一次出现的位置
isEmpty()判断链表是否为空
iterator()返回链表的迭代器
remove(Object o)删除链表中第一次出现的指定元素
remove(int index)删除指定位置的元素
removeFirst()删除链表头部的元素
removeLast()删除链表尾部的元素
size()返回链表中元素的个数
toArray()将链表转换为数组

4.2.3 ArrayList 和 LinkedList 的区别

区别ArrayListLinkedList
内部实现基于动态数组基于双向链表
随机访问时间复杂度为O(1)时间复杂度为O(n)
插入和删除操作时间复杂度为O(n)时间复杂度为O(1)
内存空间内存空间连续内存空间不连续
迭代器支持快速随机访问不支持快速随机访问
适用场景随机访问频繁,插入和删除操作较少插入和删除操作频繁,随机访问较少