【DS】链表的介绍和实现(单/双链表)

一. ArrayList的缺陷

上节我们已经熟悉了ArrayList的使用,并且进行了简单模拟实现。通过源码知道,ArrayList底层使用数组来存储元素:

public class ArrayList extends AbstractList
implements List, RandomAccess, Cloneable, java.io.Serializable
{
// ...
// 默认容量是10
private static final int DEFAULT_CAPACITY = 10;
//...
// 数组:用来存储元素
transient Object[] elementData; // non-private to simplify nested class access
// 有效元素个数
private int size;
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
// ...
}

由于其底层是一段连续空间,当在ArrayList任意位置插入或者删除元素时,就需要将后序元素整体往前或者往后搬移,时间复杂度为O(n),效率比较低,因此ArrayList不适合做任意位置插入和删除比较多的场景。因此:java集合中又引入了LinkedList,即链表结构。

二. 链表的概念和分类

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

【DS】链表的介绍和实现(单/双链表)_第1张图片

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:

  • 单向或者双向

【DS】链表的介绍和实现(单/双链表)_第2张图片

  • 带头或者不带头

【DS】链表的介绍和实现(单/双链表)_第3张图片

  • 循环或者非循环

【DS】链表的介绍和实现(单/双链表)_第4张图片

这里对于带头和不带头要注意区分一下 , 带头链表中链表的头节点是固定不变的且头节点的数值域是虚拟的 (无效的 , 不存放数据) , 不管数据在哪里插入和删除 , 头节点都不会变化 ; 而不带头链表 , 链表的第一个节点 (头节点) 是有效节点 , 数值域是有效的 , 如果在不带头链表中进行头插或者删除第一个节点 , 头节点会发生变化 .

虽然有这么多的链表的结构,但是我们重点掌握两种:

  • 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

【DS】链表的介绍和实现(单/双链表)_第5张图片
  • 无头双向链表:在Java的集合框架库中LinkedList底层实现就是无头双向循环链表。

三. 无头单向非循环链表实现

下图所示为无头单向非循环链表的结构

【DS】链表的介绍和实现(单/双链表)_第6张图片

Java实现

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User:YY
 * Date:2023-02-11
 * Time:11:30
 */
public class MySingleList {

    /**
     * 结点内部类
     */

     static class ListNode{
        public int val;
        public ListNode next;

        public ListNode(int val) {
            this.val = val;
        }
    }
    public  ListNode head; // 默认 就是NULL
    /**
     * 默认从头开始打印
     */
    public void display(){
        ListNode cal = this.head;
        while(cal != null){
            System.out.print(cal.val + " ");
            cal = cal.next;
        }
        System.out.println();
    }

    /**
     * 用返回值 的display
     * 从指定节点newHead打印链表
     * @param newHead
     */

    public void display(ListNode newHead){
        ListNode cal = newHead;
        while(cal != null){
            System.out.print(cal.val + " ");
            cal = cal.next;
        }
        System.out.println();
    }

    public int size() {
        ListNode cal = this.head;
        int count = 0;
        while(cal != null){
            count++;
            cal = cal.next;
        }
        return count;
    }

    //头插法
    public void addFirst(int data){
        ListNode node = new ListNode(data);

        node.next = this.head;
        this.head = node;
    }

    //尾插法
    public void addLast(int data){
        ListNode node = new ListNode(data);
        ListNode cal = this.head;
        if(cal == null){
            this.head = node;
        }else {//找出最后一个节点
            while(cal.next != null){
                cal = cal.next;
            }
            cal.next = node;
        }

    }

    //任意位置插入,第一个数据节点为0号下标
    public void addIndex(int index,int data){
        if(index == 0) {
            addFirst(data);
            return;
        }
        if(index == this.size()){
            addLast(data);
            return;
        }
        if(index < 0 || index > this.size()){
            System.out.println("index不合法");
        }
        //pos合法 且【1,size-1】
        ListNode node = new ListNode(data);
        //找到需要插入位置的前一个位置
        ListNode cal = findCal(index);
        //先将插入的指向后面 再讲前面的指向插入的
        node.next = cal.next;
        cal.next = node;
    }
    public ListNode findCal(int index){
        ListNode cal = this.head;
        while(index-1 != 0){
            cal = cal.next;
            index--;
        }
        return cal;
    }

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

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

    //删除第一次出现关键字为key的节点
    public void remove(int key){
        ListNode cal = this.head;
        if(cal == null){
            System.out.println("空异常");
        }
        //头位置就查到了 直接结束了
        if(cal.val == key){
            this.head = cal.next;
            return;
        }

        //从第二个位置开始查
        ListNode call = findCall(key);
        //没查到
        if(call == null) {
            System.out.println("没有你要删除的数字!");
            return;
        }
        //将下下一个给下一个
        call.next = call.next.next;
    }
    public ListNode findCall(int key){
        ListNode cal = this.head;
        //cal从头开始  但从第二个开始比
        while (cal.next != null){
            if(cal.next.val == key){
                return cal;
            }
            cal = cal.next;
        }
        return null;
    }

    /*public void removeAllKey(int key){
        ListNode cur = this.head;
        while (cur != null){
            remove(key);
            cur = cur.next;
        }
    }*/
    //删除所有值为key的节点
    public void removeAllKey(int key){
        if(this.head == null ){
            return;
        }
        //从第二个节点开始判断
        ListNode prev = this.head;
        ListNode cal = this.head.next;

        while (cal != null){
            if (cal.val == key){
                prev.next = cal.next;
                cal = cal.next;
            }else {
                prev = cal;
                cal = cal.next;
            }
        }
        if(this.head.val == key ){
            this.head = this.head.next;
        }
    }
    public void clear(){
        this.head = null;
    }
}

注意事项

1. 在代码中需要进行遍历链表时 , 要注意区分 cur != null 和 cur.next != null 的使用 , 虽然二者都可以去遍历链表 , 但cur != null , 最后一次循环判断使cur指向为null ; 而cur.next != null 的最后一次循环判断使cur指向的是链表的最后一个节点 .
2. 单链表中插入和删除数据 , 需要先找到要处理位置的上一个位置 , 然后再进行指针指向的修改 .
3. Java当中没有指针的概念 , 这里的节点通过类来实现 , 创建一个引用类型变量 , 这个引用就是Java当中的 “指针” 了 .
4. 单链表中实现清空单链表只需要置空头节点即可 , 要与双链表中的清空区分

四. 无头双向非循环链表实现

下图所示为无头双向非循环链表的结构

【DS】链表的介绍和实现(单/双链表)_第7张图片

Java实现

import java.util.LinkedList;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User:YY
 * Date:2023-02-14
 * Time:14:00
 */
public class MyLinkedList {

    static class ListNode {
        private int val;
        private ListNode prev; //记录前一个节点
        private ListNode next; //记录下一个节点

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

    public ListNode head;  // 标记双向链表的头
    public ListNode tail;  // 标记双向链表的尾巴

    //头插法
    public void addFirst(int data){
        ListNode node = new ListNode(data);
        if(this.head == null){
            this.head = node;
            this.tail = node;
        }else {
            node.next = this.head;
            this.head.prev = node;
            this.head = node;
        }
    }
    //尾插法
    public void addLast(int data){
        ListNode node = new ListNode(data);
        if(this.head == null){
            this.head = node;
            this.tail = node;
        }else {
            this.tail.next = node;
            node.prev = this.tail;
            this.tail = node;
        }
    }
    //任意位置插入,第一个数据节点为0号下标
    public void addIndex(int index,int data){
        //index合法性
        if(index < 0 || index > this.size()){
            System.out.println("index不合法");
        }
        //头插
        if(index == 0){
            addFirst(data);
            return;
        }
        //尾插
        if(index == this.size()){
            addLast(data);
            return;
        }
        //位置肯定合法 找需要插入的index位置
        ListNode cur = findCal(index);
        //开始插入
        ListNode node = new ListNode(data);
        // 前面的next改为node  node的prev改为前面的  cur的前面的改为node node后面的改为cur
        // 具体怎么造轮子,看自己 保证可以找到前驱后继即可
        node.next = cur;
        cur.prev.next = node;
        node.prev = cur.prev;
        cur.prev = node;
        /**
         *          cur.prev.next = node;
         *         node.prev = cur.prev;
         *         cur.prev = node;
         *         node.next = cur;
         */
    }
    public ListNode findCal(int index){
        ListNode cal = this.head;
        while(index != 0){
            cal = cal.next;
            index--;
        }
        return cal;
    }
    //查找是否包含关键字key是否在单链表当中
    public boolean contains(int key){
        ListNode cur = this.head;
        while(cur != null){
            if(cur.val == key){
                return true;
            }
            cur = cur.next;
        }
        return false;
    }
    //删除第一次出现关键字为key的节点
    public void remove(int key){
        ListNode cur = this.head;
       while (cur != null){
           //查到的话
           if(cur.val == key){
               //第一个就查到了
               if(cur == this.head){
                   this.head = this.head.next;
                   //是不是只有一个节点
                   if(this.head != null){
                       this.head.prev = null;
                   } else {
                       this.tail = null;
                   }
               }else {//第一个没查到
                    cur.prev.next = cur.next;
                    //删除的是不是最后一个节点
                    if(cur.next != null){
                        cur.next.prev = cur.prev;
                    }else {
                        this.tail = cur.prev;
                    }
               }
            return;
           }
           cur = cur.next;
       }
    }

    //删除所有值为key的节点
    public void removeAllKey(int key){
        ListNode cur = this.head;
        while (cur != null){
            //查到的话
            if(cur.val == key){
                //第一个就查到了
                if(cur == this.head){
                    this.head = this.head.next;
                    //是不是只有一个节点
                    if(this.head != null){
                        this.head.prev = null;
                    } else {
                        this.tail = null;
                    }
                }else {//第一个没查到
                    cur.prev.next = cur.next;
                    //删除的是不是最后一个节点
                    if(cur.next != null){
                        cur.next.prev = cur.prev;
                    }else {
                        this.tail = cur.prev;
                    }
                }
            }
            cur = cur.next;
        }
    }

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

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

    public void clear(){
        ListNode cur = this.head;
        while(cur != null){
            ListNode curNext = cur.next;
            cur.next = null;
            cur.prev = null;
            cur = curNext;
        }
        this.head = null;
        this.tail = null;
    }
}

注意事项


1. 与单链表中的插入和删除实现进行区分 , 这里双链表中的插入和删除 , 因为此时链表是双向的 , 所以不需要像单链表一样找要处理位置的前一个位置 , 只需要找到要处理的位置去改变前驱和后记指针指向即可 .
2. 在进行删除元素操作时 , 需要考虑的细节比较多 , 特别需要注意删除头节点与尾节点时的操作(考虑prev为null和next为null , 与删除中间节点不同) , 具体实现看上面给出的代码 .
3. 注意双链表的清空链表实现 , 与单链表中的进行区分 , 双链表中需要手动去将每个节点的两个指针域置为null , 最后再将head和tail去置空 .

五.往年相关面试题链接

203. 移除链表元素 - 力扣(LeetCode)

206. 反转链表 - 力扣(LeetCode)

876. 链表的中间结点 - 力扣(LeetCode)

链表中倒数第k个结点_牛客题霸_牛客网 (nowcoder.com)

21. 合并两个有序链表 - 力扣(LeetCode)

链表的回文结构_牛客题霸_牛客网 (nowcoder.com)

160. 相交链表 - 力扣(LeetCode)

链表分割_牛客题霸_牛客网 (nowcoder.com)

141. 环形链表 - 力扣(LeetCode)

142. 环形链表 II - 力扣(LeetCode)

链表知识点题库 - 力扣(LeetCode)

牛客网在线编程_编程学习|练习题_数据结构|系统设计题库 (nowcoder.com)

你可能感兴趣的:(java)