花10分钟用Java来自己实现链表吧(图解)

先声明:我比较懒,所以没有画图,不理解代码的请自行百度或者查看相关书籍🤭

链表是最常用的一种数据结构,作为线性表的一种,与数组相比,链表在插入修改操作多的环境中有着非常大的优势。下面我们用Java实现一个完整的链表。

链表是有多个节点构成的,每个节点应当包含 数据域(用来存放数据)和 next指针(Java中是下一个节点的引用)两部分。

在这里插入图片描述

同时需要提供以下操作链表的方法:

  • 初始化链表
  • 判断链表是否为空
  • 清空链表
  • 获取第i个位置的元素
  • 根据对应的值查找元素的位置
  • 插入新元素(头插法和尾插法)
  • 删除元素
  • 获取链表长度

分析完毕,下面开始实现~

public class LinkList {

}

节点

首先我们需要实现链表中用来存储数据的节点结构。我们使用一个静态内部类来表示节点,假设我们实现的链表是用来存取整形的数据。

代码实现如下:

/**
     * 链表的节点
     */
    public static class Node{
        /**
         * 节点存储的值
         */
        public int value;
        /**
         * 下一个节点
         */
        public Node next;
    }

操作

  • 初始化链表

    要想后边使用链表,我们必须拥有一个指向链表头部的引用,否则我们无法对其进行任何操作。这里我又添加了一个尾节点,为的是后面尾插法方便。我们再链表类的构造方法中调用初始化方法,主要是用来初始化头节点和尾节点。其中头节点的next指向链表的第一个节点,尾节点指向链表的最后一个节点。


    在这里插入图片描述

    代码如下:

    // 用来存储链表的头部
        private Node head;
        // 用来存储链表的尾部
        private Node tail;
    
        public LinkList(){
            // 初始化链表
            initList();
        }
    
        /**
         * 初始化链表
         */
        public void initList(){
            // 创建头节点 链表的第一个节点的引用将保存在head.next中
            head = new Node();
            head.value = 0;
            head.next = null;
            // 初始时没有元素 尾节点指向头节点
            tail = head;
        }
    
  • 判断链表是否为空

    这个实现就比较简单了,如果链表为空,那么我们头节点的next引用一定时null,所以代码实现如下:


    在这里插入图片描述
    /**
         * 判断链表是否为空
         * @return
         */
        public boolean isEmpty(){
            return head.next == null;
        }
    
  • 清空链表

    这个操作再C/C++中比较麻烦,但是用Java实现确实非常简单,我们仅仅只需要把head节点next引用改为null即可,因为可以获得这个链表的引用没有,再GC的可达性分析中,这个链表被认为时不可达的,在GC时链表就会作为垃圾被从内存中清除掉。所以,我们要做的仅仅是把head节点的next引用置为null。


    在这里插入图片描述
    /**
         * 清空链表
         */
        public void clear(){
            head.next = null;
        }
    
  • 获取链表第i个位置的值

    终于需要遍历链表了😂,我们需要一个计数器,用来记录我们遍历的位置,这一点与数组相比就比较难受了,数组因为在内存中是连续存储的,因此可以直接使用数组头部的地址和每个元素的大小直接计算出需要查找元素的位置,而链表由于在内存中不是连续存储的,所以无法通过计算得出,只能是遍历链表。因此在查询比较多的场景中,数组更占优势。


    在这里插入图片描述
    /**
         * 获取第i个节点的元素
         * @param i
         * @return
         */
        public int getNode(int i){
            // 获取第一个节点
            Node node = head.next;
            // 记录遍历到的索引
            int index = 0;
            // 遍历
            while(node != null){
                // 找到对应的索引后返回节点存储的值
                if(index == i){
                    return node.value;
                }
                // 指向下一个节点
                node = node.next;
                index++;
            }
            // 遍历完毕后还没有找到,说明索引越界 返回-1
            return -1;
        }
    
  • 获取对应值的索引

    这个实现与上面的思路就比较类似了,不在进行赘述,直接实现

    /**
         * 获取对应值的索引
         * @param value
         * @return
         */
        public int getIndex(int value){
            // 获取第一个节点
            Node node = head.next;
            // 记录索引
            int index = 0;
            while(node != null){
                // 将遍历到节点的值与要查找的值比较
                if(node.value == value){
                    return index;
                }
                node = node.next;
                index++;
            }
            return -1;
        }
    
  • 插入元素

    插入元素分为两种,一种是尾插法,一种是头插法。相比而言,尾插法比较简单,我们先实现尾插法。


    在这里插入图片描述
    /**
         * 尾部插入
         */
        public void tailInsert(int value){
            // 构造节点
            Node node = new Node();
            node.value = value;
            node.next = null;
            if(isEmpty()){
                // 链表为空时,此时head节点指向为null
                // node会作为第一个元素,因此需要将head的next指向node
                head.next = node;
            }else{
                // 链表不为空时,head已经指向链表的第一个元素了,只需要将尾节点的next指向新插入的节点即可
                tail.next = node;
            }
            // 更新尾节点
            tail = node;
        }
    

    头插法实现起来稍微复杂,因为head的next已经指向了链表的第一个节点(链表不为空时),因为是头插法,新插入的节点将会作为新的头节点,因此,需要更新head的next指向新插入的节点,同时需要将新插入的节点的next指向原来的第一个节点,代码实现如下:


    在这里插入图片描述
    /**
         * 头部插入法
         */
        public void headInsert(int value){
            // 构造节点
            Node node = new Node();
            node.value = value;
            node.next = null;
    
            if(isEmpty()){
                // 链表为空时比较简单,只需要将头节点的next指向新插入的节点
                // 注意:需要更新尾节点
                head.next = node;
                tail = node;
            }else{
                // 链表不为空时,顺序不能乱,不需要更新尾节点
                // 先将新节点的next指向原来的第一个节点
                node.next = head.next;
                // 再将头节点的next指向新插入的节点
                head.next = node;
            }
        }
    
  • 删除第i个元素

    这个要考虑的情况比较复杂,我直接写在注释上了,结合注释看应该更加好


    在这里插入图片描述
    /**
         * 删除
         * @param i 要删除的索引
         * @return 删除元素的值
         */
        public int delete(int i){
            // 链表为空时返回-1
            if(isEmpty())return -1;
            // 遍历的索引 这次从-1开始
            int index = -1;
            // 这次使用的时head 不是head的next,因为下面的遍历包含删除第一个节点的情况
            Node node = head;
            while(node != null){
                // 遍历到删除目标节点的前一个节点
                if( index == (i-1) ){
                    // 获取目标节点
                    Node target = node.next;
                    if(target == null){
                        // 如果要删除的节点刚好越界 返回-1
                        return -1;
                    }
                    // 将目标节点上个节点的next引用直接修改为目标节点的next指向的节点
                    // 可以理解为用下下个节点覆盖下个节点
                    node.next = node.next.next;
                    if(target.next == null){
                        // 如果删除的节点是尾节点 需要更新尾节点
                        tail = node;
                    }
                    // 成功删除后返回删除的值
                    return target.value;
                }
    
                node = node.next;
                index++;
            }
            return -1;
        }
    
  • 链表长度

    这个实现的方式比较多,可以在链表类中添加一个记录长度的字段len,在添加元素时len++,在删除元素时len--,获取链表长度的方法只需要返回这个字段的值即可。还有一种方法是遍历链表时计数,这里实现的时第二种方法:

    /**
         * 返回链表长度
         * @return
         */
        public int length(){
            int count = 0;
            Node node = head.next;
            while(node != null){
                count++;
                node = node.next;
            }
            return count;
        }
    

测试使用

为了方便测试,我在链表类中添加了一个展示的方法,可以遍历输出链表的所有元素,实现如下:

    public void display(){
        if(isEmpty())System.out.println("链表为空");
        Node node = head.next;
        while(node != null){
            System.out.print(node.value + "-");
            node = node.next;
        }
        System.out.println();
    }

下面使用我们自己实现的链表来完成测试

public class Test {
    public static void main(String[] args) {
        LinkList linkList = new LinkList();
        System.out.println("===初始化===");
        linkList.display();
        System.out.println("===尾插2 头插1 尾插3 ===");
        linkList.tailInsert(2);
        linkList.headInsert(1);
        linkList.tailInsert(3);
        linkList.display();

        System.out.println("===获取值为1的索引===");
        System.out.println(linkList.getIndex(1));
        System.out.println("===获取索引为1的值===");
        System.out.println(linkList.getNode(1));

        System.out.println("===删除索引为1的节点==");
        linkList.delete(1);
        linkList.display();

        System.out.println("===查看链表长度===");
        System.out.println(linkList.length());

        System.out.println("===清空链表===");
        linkList.clear();
        linkList.display();
       
    }
}

输出如下:

===初始化===
链表为空

===尾插2 头插1 尾插3 ===
1-2-3-
===获取值为1的索引===
0
===获取索引为1的值===
2
===删除索引为1的节点==
1-3-
===查看链表长度===
2
===清空链表===
链表为空

这里只是实现了链表的基本操作,其他的诸如在指定位置插入元素等方法可以自行尝试实现!

下面是完整的链表类代码:

public class LinkList {
    
    /**
     * 链表的节点
     */
    public static class Node{
        /**
         * 节点存储的值
         */
        public int value;
        /**
         * 下一个节点
         */
        public Node next;
    }

    // 用来存储链表的头部
    private Node head;
    // 用来存储链表的尾部
    private Node tail;

    public LinkList(){
        // 初始化链表
        initList();
    }

    /**
     * 初始化链表
     */
    public void initList(){
        // 创建头节点 链表的第一个节点的引用将保存在head.next中
        head = new Node();
        head.value = 0;
        head.next = null;
        // 初始时没有元素 尾节点指向头节点
        tail = head;
    }

    /**
     * 判断链表是否为空
     * @return
     */
    public boolean isEmpty(){
        return head.next == null;
    }

    /**
     * 清空链表
     */
    public void clear(){
        head.next = null;
    }

    /**
     * 获取第i个节点的元素
     * @param i
     * @return
     */
    public int getNode(int i){
        // 获取第一个节点
        Node node = head.next;
        // 记录遍历到的索引
        int index = 0;
        // 遍历
        while(node != null){
            // 找到对应的索引后返回节点存储的值
            if(index == i){
                return node.value;
            }
            // 指向下一个节点
            node = node.next;
            index++;
        }
        // 遍历完毕后还没有找到,说明索引越界 返回-1
        return -1;
    }

    /**
     * 获取对应值的索引
     * @param value
     * @return
     */
    public int getIndex(int value){
        // 获取第一个节点
        Node node = head.next;
        // 记录索引
        int index = 0;
        while(node != null){
            // 将遍历到节点的值与要查找的值比较
            if(node.value == value){
                return index;
            }
            node = node.next;
            index++;
        }
        return -1;
    }
    /**
     * 尾部插入
     */
    public void tailInsert(int value){
        // 构造节点
        Node node = new Node();
        node.value = value;
        node.next = null;
        if(isEmpty()){
            // 链表为空时,此时head节点指向为null
            // node会作为第一个元素,因此需要将head的next指向node
            head.next = node;
        }else{
            // 链表不为空时,head已经指向链表的第一个元素了,只需要将尾节点的next指向新插入的节点即可
            tail.next = node;
        }
        // 更新尾节点
        tail = node;
    }
    
    /**
     * 头部插入法
     */
    public void headInsert(int value){
        // 构造节点
        Node node = new Node();
        node.value = value;
        node.next = null;

        if(isEmpty()){
            // 链表为空时比较简单,只需要将头节点的next指向新插入的节点
            // 注意:需要更新尾节点
            head.next = node;
            tail = node;
        }else{
            // 链表不为空时,顺序不能乱,不需要更新尾节点
            // 先将新节点的next指向原来的第一个节点
            node.next = head.next;
            // 再将头节点的next指向新插入的节点
            head.next = node;
        }
    }

    /**
     * 删除
     * @param i 要删除的索引
     * @return 删除元素的值
     */
    public int delete(int i){
        // 链表为空时返回-1
        if(isEmpty())return -1;
        // 遍历的索引 这次从-1开始
        int index = -1;
        // 这次使用的时head 不是head的next,因为下面的遍历包含删除第一个节点的情况
        Node node = head;
        while(node != null){
            // 遍历到删除目标节点的前一个节点
            if( index == (i-1) ){
                // 获取目标节点
                Node target = node.next;
                if(target == null){
                    // 如果要删除的节点刚好越界 返回-1
                    return -1;
                }
                // 将目标节点上个节点的next引用直接修改为目标节点的next指向的节点
                // 可以理解为用下下个节点覆盖下个节点
                node.next = node.next.next;
                if(target.next == null){
                    // 如果删除的节点是尾节点 需要更新尾节点
                    tail = node;
                }
                // 成功删除后返回删除的值
                return target.value;
            }

            node = node.next;
            index++;
        }
        return -1;
    }

    /**
     * 返回链表长度
     * @return
     */
    public int length(){
        int count = 0;
        Node node = head.next;
        while(node != null){
            count++;
            node = node.next;
        }
        return count;
    }

    public void display(){
        if(isEmpty())System.out.println("链表为空");
        Node node = head.next;
        while(node != null){
            System.out.print(node.value + "-");
            node = node.next;
        }
        System.out.println();
    }

    


}

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

推荐阅读更多精彩内容