模拟单链表

简介: 模拟单链表

一、单向、不带头、非循环链表的结构



模拟五个节点,每个节点有两个域:


一个是数据域,存放的是元素


另一个是指针域,存放的是当前节点的下一个节点的地址


d525fa3abb764cbbb512d2ed63fc8722.jpg


二、代码实现



1. 第一个 java 文件放的是两个类


一个类实现节点 Node,另一个类实现链表 LinkedList


class Node{
    public int val;
    public Node next;
    public Node (int newval){
        this.val = newval;
    }
}
public class LinkedList {
    //头指针
    public Node head;
    //初始化链表  ✓
    public void initialList(){
        Node node1 = new Node(12);
        Node node2 = new Node(23);
        Node node3 = new Node(34);
        Node node4 = new Node(45);
        Node node5 = new Node(56);
        this.head = node1;
        node1.next = node2;
        node2.next = node3;
        node3.next = node4;
        node4.next = node5;
        //此时的node5.next默认就是null
    }
    //打印链表每个节点存的元素  ✓
    public void print(){
        Node cur = this.head;
        while(cur != null){
            System.out.print(cur.val + " ");
            cur = cur.next;
        }
        System.out.println();
    }
    //得到链表的长度  ✓
    public int size(){
        Node cur = this.head;
        int count = 0;
        while(cur != null){
            count++;
            cur = cur.next;
        }
        return count;
    }
    //头插法  ✓
    public void addFirst(int data){
        Node newnode = new Node(data);
        if(this.head == null){
            this.head = newnode;
            return;
        }
        newnode.next = this.head;
        this.head = newnode;
    }
    //尾插法  ✓
    public void addLast(int data){
        Node newnode = new Node(data);
        if(this.head == null){
            this.head = newnode;
            return;
        }
        Node cur = this.head;
        while(cur.next != null){
            cur = cur.next;
        }
        cur.next = newnode;
    }
    //寻找要插入的位置,返回插入节点的前驱  ✓
    public Node searchPos(int pos){
        int sign = 0;
        Node cur = this.head;
        while(sign + 1 != pos){
            sign++;
            cur = cur.next;
        }
        return cur;
    }
    //任意位置插入,假设第一个数据节点为 0 号下标  ✓
    public void insertData(int pos,int data){
        //1. 位置不合法
        if(pos<0 || pos>size()){
            System.out.println("链表的插入位置不合法!");
        }
        //2. 头插
        else if(pos == 0){
            addFirst(data);
        }
        //3. 尾插
        else if(pos == size()){
            addLast(data);
        }
        //4. 中间插
        else{
            Node newnode = new Node(data);
            Node cur = searchPos(pos);
            newnode.next = cur.next;
            cur.next = newnode;
        }
    }
    //查找是否包含关键字key是否在单链表当中  ✓
    public boolean contains(int key){
        Node cur = this.head;
        while(cur != null){
            if(cur.val == key){
                return true;
            }else{
                cur = cur.next;
            }
        }
        return false;
    }
    //删除第一次出现关键字为key的节点  ✓
    public void remove(int key){
        //1. 链表为空
        if(this.head == null){
            System.out.println("链表为空,无法删除!");
            return;
        }
        //2. 头删
        if(this.head.val == key){
            this.head = this.head.next;
            return;
        }
        //3. 中间删 + 尾删
        Node prev = this.head;
        Node del = this.head;
        while(prev.next != null){
            if(prev.next.val != key){
                prev = prev.next;
            }
           else{
               del = prev;
               del.next = del.next.next;
               return;
            }
        }
        System.out.println("你所删除的节点在链表中没有找到!");
    }
    //删除所有值为key的节点  ✓
    public void removeAllKey(int key){
        //1. 链表为空
        if(this.head == null){
            System.out.println("链表为空,无法删除!");
            return;
        }
        //2. 开始删除重复的节点
        Node cur = this.head;
        Node prev = this.head;
        while(cur != null){
            if(cur.val == key){
                prev.next = cur.next;
                cur = cur.next;
            }
            else{
                prev = cur;
                cur = cur.next;
            }
        }
        //3. 考虑头节点
        if(this.head.val == key){
            this.head = this.head.next;
        }
    }
    //清空链表  ✓
    public void clear(){
        this.head = null;
    }
}


2. 第二个 java 文件放的是一个类 Test


目的是使用主函数来测试单链表的一系列功能


public class Test {
    public static void main(String[] args) {
        LinkedList test = new LinkedList();
        test.initialList();
        test.print();
    }
}


三、框架分析



0. 对框架进行说明


(1)我所创建的框架是拆解了代码的方法(函数),把类中的每一个方法都拿出来仔细分析。


(2)我画图的地方可能有些不规范,比如地址的表示形式,比如箭头指向等等,我旨在表达逻辑,所以请读者自行理解,也请大佬多多指点。


1. 创建一个类 Node,表示节点


class Node {
    public int val;
    public Node next;
}


创建一个类 Node,表示节点,每次插入一个节点,就使用这个类

每个节点有两个元素:


一个存的是当前节点的值 val


另一个存的是下一个节点的地址 next,next 的类型是引用,即当前的 Node.


5bb78c23b8f34e8384e9faf8c51d2428.jpg


2. 模拟链表内部结构


class Node {
    public int val;
    public Node next;
    public Node(int newval){
        this.val = newval;
    }
}
public class LinkedList {
    public static void main(String[] args) {
        Node node1 = new Node(12);
        Node node2 = new Node(23);
        Node node3 = new Node(34);
    }
}


说明:类 Node 中的 public Node(int newval) { },这是构造方法,以便于 LinkedList 中实例化对象,并赋值。


为了展现出清晰的链表结构,我们创建一个新的类 LinkedList,表示整个链表里面的结构,并在这个类中实现各种接口。我们尝试着直接往里面一个一个放数据,为了展示底层原理,我先放在主函数中以帮助理解。


我们为什么不把引用类型 next 置成 null 呢?因为 next 是一个成员变量,如果不将其初始化,那么默认为 null,即不指向任何对象的地址,下图辅助理解。


0cc937b744754a07800bcfee4db57567.jpg


3. 初始化一个链表


public class LinkedList {
    //头指针
    public Node head;
    //初始化链表  ✓
    public void initialList(){
        Node node1 = new Node(12);
        Node node2 = new Node(23);
        Node node3 = new Node(34);
        Node node4 = new Node(45);
        Node node5 = new Node(56);
        this.head = node1;
        node1.next = node2;
        node2.next = node3;
        node3.next = node4;
        node4.next = node5;
        //此时的node5.next默认就是null
    }
}


在上述的解释后,我们已经清楚地了解了关于链表的一些基础结构。


那么接下来,在类 LinkedList 中,创建一个方法 initialList,来初始化一个链表,并利用实例化后的对象来将各个节点串起来,并注意,我们要引用一个成员变量 head,以此来记住链表是从哪开始的!


而链表的尾部我们不关心,因为它肯定是 null.


从目前来看,这是一个很笨的穷举方法,然而,我们的目的是深入理解链表。


7883341d85a5432cba71a31a5f7a0842.jpg


4. 打印每个节点对应的 val


    //打印链表每个节点  ✓
    public void print(){
        Node cur = this.head;
        while(cur != null){
            System.out.print(cur.val + " ");
            cur = cur.next;
        }
        System.out.println();
    }


我们的思想是:遍历链表,当引用指向 null 时,那么就到了最后一个节点。

当然,我们要十分注意,要创建一个新的引用类型 cur,让这个 cur 去跑。因为你如果让 head 去遍历,head 不再指向第一个节点了,以此带来的结果是:我们不再能够控制链表,它就像一个无头苍蝇一样。


此外,我们让代码 cur = cur.next,来不断地拿到下一个节点的地址,以此来遍历,这和 C语言中的指针相似,只是实现方法不同。


下图辅助理解


d5b310cebd3343038896641814d3d211.jpg


5. 得到链表的长度


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


6. 头插法


    //头插法  ✓
    public void addFirst(int data){
        Node newnode = new Node(data);
        if(this.head == null){
            this.head = newnode;
            return;
        }
        newnode.next = this.head;
        this.head = newnode;
    }


头插法的最关键的思想就是,改变头指针 head 指向的地址。

在没有插入数据之前,head 指向原先的 node1,在插入之后,新的节点 newnode 存放两个数据,一个是整型的 data,一个是空指针 null。


而我们要做的就是让 head 指向 newnode,而 newnode 这个新的节点存放的又是原先的 node1,所以我们通过以下两行代码进行实现


newnode.next = this.head <=> node1;
this.head = newnode;


而这两行核心代码不能颠倒顺序,否则,会导致死循环。


下图为辅助理解,应当注意的是,整个过程全部都是由地址实现转换的,另外,也要把整个链表为空的情况考虑进去。


ba1163c9f4a84b6996f5239c4495c016.jpg


7. 尾插法


    //尾插法  ✓
    public void addLast(int data){
        Node newnode = new Node(data);
        if(this.head == null){
            this.head = newnode;
            return;
        }
        Node cur = this.head;
        while(cur.next != null){
            cur = cur.next;
        }
        cur.next = newnode;
    }


如果链表中有元素,那么就遍历链表找到最后一个节点存的地址,也就是 cur.next 必定为 null,然后就把新的节点地址赋给 cur.next。


下图为辅助理解,应当注意的是,整个过程全部都是由地址实现转换的,另外,也要把整个链表为空的情况考虑进去。


尾插法相比于头插法,较为简单,但是实现的思想相同。


6dc4f9d316e84cad9c57819f953fff97.jpg


8. 在任一位置插入节点


    //寻找要插入的位置,返回插入节点的前驱  ✓
    public Node searchPos(int pos){
        int sign = 0;
        Node cur = this.head;
        while(sign + 1 != pos){
            sign++;
            cur = cur.next;
        }
        return cur;
    }
    //任意位置插入,第一个数据节点为0号下标  ✓
    public void insertData(int pos,int data){
        //1. 位置不合法
        if(pos<0 || pos>size()){
            System.out.println("链表的插入位置不合法!");
        }
        //2. 头插
        else if(pos == 0){
            addFirst(data);
        }
        //3. 尾插
        else if(pos == size()){
            addLast(data);
        }
        //4. 中间插
        else{
            Node newnode = new Node(data);
            Node cur = searchPos(pos);
            newnode.next = cur.next;
            cur.next = newnode;
        }
    }


在中间位置插入数据之前,每个节点的 “内存” 是连着的,所以我们考虑插入数据之后,也要满足上面的要求,就像一根线把零散的珠子串起来一样,上述代码分为几种情况,读者可自行体会,而我们下面只分析中间插入节点的情况。


思路如下:


在类中创建两个方法


一个函数是 searchPos,用来找到要插入节点位置下标的,并返回节点的前驱 cur,因为前驱即将存的是我们插入节点的地址,我们假设把第一个节点的下标设置成 0,第二个节点的下标设置成 1…


另一个函数是 insertData,用来执行插入节点后的地址转换


举个例子,我们在链表中插入的位置是下标 2,那么我们通过下面的图和代码进行演示,


而这两行核心代码不能颠倒顺序,否则,会导致死循环。


newcode.next = cur.next;
cur.next = newcode;


5b0c486fc3ac4d5da91b73f13fc415a3.jpg5cad3df58fa14bfa9d4820201c64e36c.png


9. 查找关键字key是否在单链表当中


    //查找是否包含关键字key是否在单链表当中  ✓
    public boolean contains(int key){
        Node cur = this.head;
        while(cur != null){
            if(cur.val == key){
                return true;
            }else{
                cur = cur.next;
            }
        }
        return false;
    }


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


    //删除第一次出现关键字为key的节点  ✓
    public void remove(int key){
        //1. 链表为空
        if(this.head == null){
            System.out.println("链表为空,无法删除!");
            return;
        }
        //2. 头删
        if(this.head.val == key){
            this.head = this.head.next;
            return;
        }
        //3. 中间删 + 尾删
        Node prev = this.head;
        Node del = this.head;
        while(prev.next != null){
            if(prev.next.val != key){
                prev = prev.next;
            }
           else{
               del = prev;
               del.next = del.next.next;
               return;
            }
        }
        System.out.println("你所删除的节点在链表中没有找到!");
    }


这里就不再画图描述,思想和任意位置插入是一样的,所以我主要阐明一下步骤:

删除节点有如下几种情况:


(1)删除的链表是否为空

(2)如果链表不为空,删除的节点是否有对应的元素

(3)头删

(4)尾删

(5)中间删


假设我们删除的是 key 为 34,也就是 2 号位,那么我们一定要让 cur 走到 1号位,因为 1 号位是 2 号位的前驱,即1 号位存放着 2 号位的地址,然后通过如下代码,就可以完成目的,而删除尾部的节点,和此时的思想是一样的。


cur.next = cur.next.next


11. 删除所有出现 key 的节点


    //删除所有出现 key 的节点  ✓
    public void removeAllKey(int key){
        //1. 链表为空
        if(this.head == null){
            System.out.println("链表为空,无法删除!");
            return;
        }
        //2. 开始删除重复的节点
        Node cur = this.head;
        Node prev = this.head;
        while(cur != null){
            if(cur.val == key){
                prev.next = cur.next;
                cur = cur.next;
            }
            else{
                prev = cur;
                cur = cur.next;
            }
        }
        //3. 考虑第一个节点
        if(this.head.val == key){
            this.head = this.head.next;
        }
    }


方法一:我们可以不断调用 remove 函数,一次调用删一个值,遍历很多次,然而这样的效率太低,所以我们可以尝试方法二。


方法二:我们可以只经过一次遍历,就可以达到所有删除的目的,下面我主要讲解方法二,方法二有两种情况。


(情况1)指定的节点在链表中非连续存放


举个例子:假设我们要删除的节点对应值 key 为 23


那么下图开始我的分析


38a140dcbbed40029e6671858e876ecb.png65ec7faa2f5c46f5877bfafa376d5123.png634c1fdc1add4ca2bd5ce752f274f73c.png


拆解步骤:


步骤①:我们创建两个引用(指针)


一个是 cur, 代表搜寻指针,指向 head.next,目的是让 cur 遍历整个链表,遇到对应的 key,那么就停下,没遇到对应的 key,就继续跑,直到遇到 null,就跳出循环。


另一个是 prev, 代表 cur 的前驱指针,指向 head,目的是跟随指针 cur,起到串联链表中零散节点的作用。


后面的步骤开始执行:


例如步骤②,如果遇到了 key ,就执行如下代码


prev.next = cur.next; //串联零散节点
cur = cur.next; //继续遍历


例如步骤③,如果没遇到 key ,就执行如下代码


prev = cur; //重置前驱
cur = cur.next; //继续遍历


最后判断第一个节点 head.val 是否 == key,是 key 就重新指向,读者可以思考,如果 head 放在程序最开始判断,那么 head 将不断被改变,因为我们要删除 key 对应的节点的位置是不可预知的,这样考虑下来,直接将头指针放置在程序最后判断较为方便。


而最后一个节点是否有对应的 key 我们不用关心,因为它仍然符合以上代码。

此外,还有一种情况:


(情况2)指定的节点在链表中连续存放


此情况被被情况(1)包含,所以仍然符合以上代码,读者可以自行画图分析。


12. 清空链表


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


四、测试程序



1. 初始化链表


public class Test {
    public static void main(String[] args) {
        LinkedList test = new LinkedList();
        test.initialList();
        test.print();
    }
}


输出结果:


160d94ebb7c84e2db399821ab61b1e2e.png


2. 插入功能


public class Test {
    public static void main(String[] args) {
        LinkedList test = new LinkedList();
        test.addFirst(1);
        test.addFirst(3);
        test.addFirst(5);
        test.addFirst(7);
        test.print();
        test.addLast(2);
        test.addLast(4);
        test.addLast(6);
        test.addLast(8);
        test.print();
        test.insertData(8,77);
        test.print();
        test.insertData(0,99);
        test.print();
        test.insertData(2,88);
        test.print();
    }
}


输出结果:


38749bc7cc564e92919f0725d970322f.png


3. 删除单节点


public class Test {
    public static void main(String[] args) {
        LinkedList test = new LinkedList();
        test.addLast(1);
        test.addLast(3);
        test.addLast(5);
        test.addLast(7);
        test.addLast(9);
        test.print();
        test.remove(9);
        test.print();
        test.remove(1);
        test.print();
        test.remove(5);
        test.print();
        test.remove(10);
    }
}


输出结果:


103b242ddcaf45f69f5e338308870773.png


4. 删除指定的重复节点


public class Test {
    public static void main(String[] args) {
        LinkedList test = new LinkedList();
        test.addLast(2);
        test.addLast(3);
        test.addLast(5);
        test.addLast(2);
        test.addLast(2);
        test.addLast(9);
        test.addLast(2);
        test.print();
        test.removeAllKey(2);
        test.print();
    }
}


输出结果:


9bde52c7d09c41379802c2ac05106274.png


总结


  1. 刚接触单链表的时候,花了我很长时间,可能我和大家不一样,底层思想我是很快就懂了,但是使用 Java 代码实现的时候,却花了我很长时间。


  1. 总体来说,Java 实现链表确实要简洁一点,C / C++ 实现我没用过,但是肯定少不了指针和结构体,因为目前刚刚接触到类与对象的概念,从面向过程到面向对象,这是一个思维的大改变,确实不容易,使用引用的时候,很不习惯。


  1. 现在慢慢接触数据结构,还是很吃力的,我发现画图也是很重要的,如果草图表达的清楚了,并且自己可以耐心地一步步往下推,那么剩下的就是语法实现而已。


image.jpeg


Over. 谢谢观看哟~


目录
相关文章
【C++】模拟实现二叉搜索树的增删查改功能
【C++】模拟实现二叉搜索树的增删查改功能
|
存储 机器学习/深度学习 算法
模拟实现单链表、双链表、栈、队列——数组模拟
我们在数据结构中都学到过单链表、双链表、栈和队列,当我们实现的时候时使用结构体指针实现的。定义一个结构体,结构体中存储指针变量和存放数值的变量。当然,C++的STL库中已经有实现好的栈和队列,我们可以直接用。但是在做算法题时,有时候我们会发现超出时间限制。原因是我们用STL库中的栈和队列容器时,效率相对来说较慢。我们这时就引出用数组模拟实现栈和队列。用数组模拟实现的使用起来效率更高、更方便。当然,我们也会讲到用数组模拟实现单链表和双链表。
69 0
|
存储 关系型数据库 容器
一篇文章带你了解红黑树并将其模拟实现(上)
红黑树的概念和性质 1. 概念 红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的
|
6月前
|
存储 算法
数据结构和算法学习记录——线性表之单链表(上)-初始单链表及其尾插函数(顺序表缺陷、单链表优点、链表打印)
数据结构和算法学习记录——线性表之单链表(上)-初始单链表及其尾插函数(顺序表缺陷、单链表优点、链表打印)
41 0
|
7月前
|
存储 算法 程序员
顺序表的模拟
顺序表的模拟
|
7月前
|
存储 测试技术
单链表的模拟实现
单链表的模拟实现
52 0
|
存储
模拟实现顺序表
模拟实现顺序表
43 0