链表的概念和结构及基本功能函数的实现(单链表的实现)

Posted 小海浪.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了链表的概念和结构及基本功能函数的实现(单链表的实现)相关的知识,希望对你有一定的参考价值。

🎓✏️引言

❓❓顺序表的问题及思考,之前我写了一篇顺序表的博客,发现数组和顺序表基本一样,很简单地就实现了,但存在一些问题比如:

  • 顺序表中间/头部的插入删除,时间复杂度为O(N)
  • 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
  • 增容一般是呈2倍的增长也可以是其他,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。

那么 如何解决以上问题呢?答案是我们可以通过链表来解决上面存在的问题,链表适合插入和删除元素,可以做到随用随取不浪费空间,顺序表则适合给定要查找元素的下标进行查找,

各有各的有点,下面给出了链表的结构来简单地看一下。

🎓✏️链表的概念及结构

链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用变量来实现的 ,如下图的链表:

下面是链表节点的基本结构:

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
单/双向—>带/不带头——>循环/非循环(2x2x2)

  • 单向带头循环
  • 双向带头循环
  • 单向带头非循环
  • 双向带头非循环
  • 单向不带头循环
  • 双向不带头循环
  • 单向不带头非循环
  • 双向不带头非循环

📝📐本篇博客重点实现单链表的基本功能,双链表后期博文讲解,而单向链表中,比较难,重要,面试经常遇到的就是单向不带头非循环的链表了,因此我们重点讲解它。单向的带头和循环的很简单,我们把不带头的非循环的明白了,其他的学习起来也很简单了。
注意事项:
对于这里所说的链表带不带头,我们需要好好理解下,

  • 不带头的单向非循环链表,这个链表没有真正的头结点,但是我们把第一个节点叫做头结点,这个节点是个虚拟的,只起一个标识作用,标识这个链表的头结点
  • 这个头结点的位置随时可能发生这变化,是不固定的,之后通过这个头结点我们要完成一些链表的增删查改。从头开始,因此头很重要
  • 带头结点的链表,有正真的头结点,这个头结点也是用来标识头结点的位置的,这个节点的位置不会发生改变, 但这个头结点里面的值不被使用,带头的就很简单,比如每次头插,头都不变。

下面看代码的时候你就会真正地感受到了,下面是几种循环的结构:

单向不带头非循环链表

单向带头非循环链表

单向不带头循环链表

🎓✏️单链表的实现及图示分析

通过分析上面的链表,我们可以抽象出两个类,一个是链表本身是一个类(链表这种类型)定义在MyLinkedList.java这个java文件中,另外链表里面的节点也可以看做一个类,定义在Node.java这个文件中,这两个链表里面有不同的成员属性,利用面向对象的思想来实现我们所要的单链表。

📝🔷单链表之创建单链表

以穷举的方式创建单链表
分别实例化了四个对象,并且调用构造方法进行值的初始化,在通过节点里面引用变量的指向使所有节点连接起来。

public void createList() {
        Node node1 = new Node(12);
        Node node2 = new Node(3);
        Node node3 = new Node(5);
        Node node4 = new Node(2);
        node1.next = node2;
        node2.next = node3;
        node3.next = node4;
        this.head = node1;
    }

📝🔷单链表之计算单链表的长度

这个很简单遍历链表元素就行, 创建一个临时节点,代替头结点遍历,头结点的位置很重要起到标识作用不能随便改变, 没有了头就不能找到这个链表了,还要注意遍历的结束条件。

 //得到单链表的长度
    public int size() {
        Node cur = this.head;//创建一个临时节点,代替头结点遍历,头结点的位置很重要起到标识作用不能随便改变,没有了头就不能找到这个链表了
        int count = 0;
        while (cur != null) {
            count++;//4
            cur = cur.next;//cur这个节点向后移动一步
        }
        return count;
    }

📝🔷单链表之打印单链表

打印单链表也很简单,遍历打印就行,只是链表元素向后移动是通过引用变量指向对象的移动移动的。

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

📝🔷单链表之单链表的增删查改

📝🔷头插法插入元素

头插法插入元素的时候需要注意:

  1. 在插入一个节点的时候,一定要先使插入节点的空指针域指向下一个节点的地址,再更换头结点
  2. 新插入的节点是头结点
  3. 如果插入时,head为空,则证明之前链表没有数据,直接this.head=node即可,之后又按第一个节点不是null的方法插入即可
//头插法
    public void addFirst(int data) {
        Node node = new Node(data);
        if(this.head == null) {
            this.head = node;
        }else {
            node.next = this.head;
            this.head = node;
        }
    }

🐮🐵尾插法插入元素

如果一个链表为空,也就是头结点head==null,直接插入一个元素既是头也是尾,不为空的时候通过while循环找到尾巴节点,**当 cur.next为null时候是尾巴节点,**再使这个尾巴节点里面存储node节点的地址,这样把所有的节点都连接起来了。

//尾插法
    public void addLast(int data) {
        Node node = new Node(data);
        if(this.head == null) {//链表为空没有元素
            this.head = node;
        }else {
            Node cur = this.head;
            while (cur.next != null) {
                cur = cur.next;
            }
            cur.next = node;//连接下一个节点
        }
    }

🐮🐵把一个节点插入到链表的任意位置

对于链表里面的元素,假设第一个元素的下标为0,第二个元素下标为2,以此类推,插入元素之前要判断元素位置的合法性,插入位置,0=<index<=size,当插入位置为0的时候,相当于头插法,当index=size的时候,相当于尾插法,插入元素在中间的时候,插入到插入位置的前面,之后连接前后两个元素即可。

寻找插入点的前一个节点

//寻找前驱节点
 public Node searchPrev(int index) {
        Node cur = this.head;
        int count = 0;
        while(count != index-1) {
            cur = cur.next;
            count++;
        }
        return cur;//
    }

中间插入元素

    //任意位置插入,第一个数据节点为0号下标
    public void addIndex(int index,int data){
        if(index < 0 || index > size()) {
            System.out.println("下标不合法");
            return;
        }
        //头插法
        if(index == 0) {
            addFirst(data);
            return;
        }
        //尾插法
        if(index == size()) {
            addLast(data);
            return;
        }
        Node cur = searchPrev(index);
        Node node = new Node(data);
        node.next = cur.next;
        cur.next = node;
    }

🐮🐵查找链表中是否含有某个元素

从链表的头到尾遍历,看是否含有某个值, 注意遍历链表的条件是cur==null;

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

🐮🐵删除链表中第一次出现value值的节点

删除链表中第一次出现value值的节点,首先这个链表要不为空,为null就直接返回什么都不用做了,不为空,找到要删除节点的前面一个节点,**使前驱节点连接要删除节点后面的一个节点就把那个值给删除了,**另外如果头结点为删除的节点要单独判断,更换头结点就删除了。详细解释看图

public Node searchPrevNode(int val) {
        Node cur = this.head;
        while (cur.next != null) {
            if(cur.next.val == val) {
                return cur;
            }
            cur = cur.next;
        }
        return null;
    }

    //删除第一次出现关键字为key的节点
    public void remove(int val) {
        if(this.head == null) return;
        //单独判断头节点的问题
        if(this.head.val == val) {
            this.head = this.head.next;
            return;
        }
        Node cur = searchPrevNode(val);
        if(cur == null) {
            System.out.println("没有你要删除的节点!");
            return;
        }
        Node del = cur.next;
        cur.next = del.next;
    }

如果是头结点是要删除的节点

🐮🐵删除链表中出现value值得所有节点

👀😄删除链表中出现的value值的所有节点,和之前删除出现value值的方法差不多,定义一个prev标记要删除节点的前驱,找到要删除的节点跳过它就行了, 但要注意出现连续要删除的value值和头节点是要删除的节点的情况。

//删除所有值为key的节点
    public void removeAllKey(int val) {
        if(this.head == null) {
            return;
        }
        Node prev = this.head;
        Node cur = this.head.next;
        while (cur != null) {
            if(cur.val == val) {
                prev.next = cur.next;
                cur = cur.next;
            }else {
                prev = cur;
                cur = cur.next;
            }
        }
        //最后判断头节点
        if(this.head.val == val) {
            this.head = this.head.next;
        }
    }

因为在删除节点时,**前驱节点一定是在判断是否要删除的节点cur的前面,**也就是说通过上面的方法删除节点,头结点的值没判断,要删除后面出现的value值后,要单独判断头结点的情况。

🐮🐵清空单链表

👀😄jvm有自动回收机制,当一个对象没有被引用的时候就被回收了,当然我们也可以手动的将其设置了不被引用,而回收,最简单的清空链表的方法把头结点置为null,它不引用别的节点,别的节点也都不被引用,整个链表就被jvm回收了,当然最好还是一个一个节点的next域置为null,使他们都不被引用。

 public void clear() {
        while (this.head != null) {
            Node curNext = this.head.next;
            this.head.next = null;
            this.head = curNext;
        }
    }

📝📐有三个类文件,最后把所有链表功能代码放在代码组合在一起放在MyLinkedList.java就是完整功能代码了,下面是测试类的代码,用于测试我们单链表的功能,还有一个链表节点的类文件Node.java。

public class Test {
    public static void main(String[] args) {
        MyLinkedList myLinkedList = new MyLinkedList();
        //myLinkedList.createList();
        myLinkedList.addLast(8);
        myLinkedList.addLast(9);
        myLinkedList.addLast(1);
        myLinkedList.addLast(1);
        myLinkedList.addLast(16);
        myLinkedList.addLast(7);
        myLinkedList.show();//1  2  3  4
        myLinkedList.removeAllKey(1);
        myLinkedList.show();
   System.out.println(myLinkedList.contains(21));//false*/

    }
}

Node.java类文件

public class Node {
    public int val;
    public Node next;//null

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

如🈶❓题请🈯正,记🉐点👍🏻🍹持,🦀🦀,🦀🦀

以上是关于链表的概念和结构及基本功能函数的实现(单链表的实现)的主要内容,如果未能解决你的问题,请参考以下文章

单链表基本操作

数据结构之单链表的增删查改等操作画图详解

C数据结构单链表接口函数逻辑解析与代码实现(含详细代码注释)

数据结构--单链表的实现(c语言实现)

数据结构--单链表的实现(c语言实现)

数据结构[双链表的实现,以及双链表和单链表之间的比较,链表和顺序表的优劣]