java数据结构之链表

java数据结构之链表

一、链表是什么?

list -->ArrayList 顺序表:元素处在连续的内存空间上
list -->LinkList 链表:元素处在不连续的内存空间上

二、使用链表

1.定义链表

链表由两部分组成:

1.元素
2.节点

在这里插入图片描述

2.链表的作用:

解决顺序表中的"搬运"问题,中间插入/删除的时间复杂度是O(N).

  • 顺序表插入元素 100 的过程
    在这里插入图片描述
    链表实现元素 100 的插入
    在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
删除同理,顺序表也是需要搬运的
在这里插入图片描述
链表删除元素 2
在这里插入图片描述

  • 链表和顺序表的取下标操作

     1.顺序表数组,索引(index)指向数组下标,顺序表是直接封装了数组,可以直接使
     用index 来取元素。
     2.链表不是数组,无法使用 index 来直接取下标,可以使用 get / set 方法取下
     标,只不过此方法比顺序表的操作效率低。
    

3.单向链表和双向链表

  • 单向链表:只能通过当前节点,找到下一个节点~,无法找到上一个节点
class Node {
    int val; // 链表中要保存的元素
    Node next; // 保存指向下一个结点的引用
}
  • next == null 表示链表到达了末尾
  • 双向节点:通过当前节点,能找到下一个节点,也可以找到上一个节点
class Node {
    int val; 
    Node next; 
    Node prev;
}

4.带傀儡节点的链表和不带傀儡节点的链表

傀儡节点:不实际存储数据,占用位置

在这里插入图片描述

5.带环的链表和不带环的链表

  • 不带环的链表:最后一个元素指向null
    在这里插入图片描述

  • 带环的链表:最后一个元素不指向空,而是指向链表的某个节点
    在这里插入图片描述

总结:以上六种链表是正交的,可以随意任意组合,衍生出八种链表

在这里插入图片描述
链表的头结点:
针对单向链表,为了获取其头结点,通常使用头结点代替整个链表

6.链表遍历

package java;

//使用 Node 表示链表的节点
public class Node {
    public int val;
    public Node next = null;

    public Node(int val){
        this.val = val;
    }
    @Override
    public String toString() {
        return "[" + val + "]";
    }
}
package java;
public class Main {
    //此方法是创建出一个固定内容的链表
    //使用头结点来代指整个链表
    //方法返回头结点
    public static Node createList(){
        Node a = new Node(1);
        Node b = new Node(2);
        Node c = new Node(3);
        Node d = new Node(4);
        a.next = b;
        b.next = c;
        c.next = d;
        d.next = null;
        return a;
    }

    public static void main(String[] args) {
        Node head = createList();

        for (Node cur = head; cur != null; cur = cur.next ) {
            System.out.println(cur.val);
        }
    }
}

输出结果
在这里插入图片描述

通过遍历,找到链表的最后一个结点

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

运行结果
在这里插入图片描述

  • 为了保持代码稳健,头结点不可以为空
  • 如若不带傀儡节点的链表表示空,直接用 head = null 来表示;如若带傀儡节点的链表表示空链表,则使用 head.next = null 来表示。(带傀儡节点意味着无论如何都有一个节点)

通过遍历,找到链表的倒数第二个结点

        int N = 3;
        Node cur = head;
        for (int i = 1; i < N; i++) {
            cur = cur.next;
        }
        //此时 cur 指向的元素,就是正数第 N 个元素
        System.out.println(cur.val);

运行结果:
在这里插入图片描述

通过遍历,找到链表的第 n 个结点。(从 1 开始,链表的长度 >= n)


        int N = 3;
        Node cur = head;
        for (int i = 1; i < N; i++) {
            cur = cur.next;
        }
        //此时 cur 指向的元素,就是正数第 N 个元素
        System.out.println(cur.val);

运行结果:
在这里插入图片描述

通过遍历,计算链表中元素的个数

        //每次循环访问一个节点之后 ,计数器count++
        int count = 0;
        for (Node cur = head;cur != null;cur = cur.next) 
            count++;
        }
        System.out.println(count);

运行结果:
在这里插入图片描述

通过遍历,找到链表的倒数第二个结点

通过遍历,找到链表中是否包含某个元素。

        int toFind = 3;
        Node cur = head;
        for (; cur != null; cur = cur.next) {
            if (cur.val == toFind) {
                break;
            }
        }
            if(cur != null){
                System.out.println("找到了");
            }
            else{
                System.out.println("没找到");
        }

运行结果:
在这里插入图片描述

7.链表插入

中间插入

将100插入1和2之间
在这里插入图片描述

思路:

	step1:先获取元素1 节点的引用,假设Node one 对应 1 节点的引用
	step2:让新节点的 next 指向 2 节点(one.next)
newNode.next = one.next;

在这里插入图片描述

step3:让 one 的 next 指向新节点
one.next = newNode;

在这里插入图片描述

  • 垃圾回收
public class Main {
    public static Node creatList(){
        //此处的 a b c d 四个引用为局部变量,方法使用完就会被销毁,在其后面new的对象在垃圾回收的时候销毁
        //什么时候垃圾回收:没有任何(强)引用指向的时候
        Node a = new Node(1);
        Node b = new Node(2);
        Node c = new Node(3);
        Node d = new Node(4);
        a.next = b;
        b.next = c;
        c.next = d;
        d.next = null;
        return a; //将a中存的地址返回到 Node head 中,即 Node head 指向 元素 1
    }
    public static void main(String[] args) {
        Node head = creatList();

tips:对于带环链表是不是存在垃圾回收只剩环的情况呢?
答案是:不会的
理由:对于带环的链表,如果删除头结点指向,虽然其他节点也有引用吗,但是还是会被回收的,因为该节点对象为“不可达”,GC在分析对象能否被释放时,会对其进行可达性分析,即从一些特殊的引用开始进行搜索,例如:每个栈针中的局部变量,类的静态变量,对这些特殊的引用有一个别称-- gcroots。

链表元素的插入(不带傀儡节点)—头插入

  • 头插入需要考虑的最大问题是将会影响头结点的引用。
    在这里插入图片描述

第一步:让 newNode 的 next 指向链表的第一个节点
在这里插入图片描述

第二步:让head指向新节点
在这里插入图片描述

插入小结

对于无傀儡节点的中间插入和头插入是两种不同的思想和过程,对于开发者使用起来较为繁琐,故若引用傀儡节点,则可将头插入转换成中间插入,使得开发效率大大提高。

带傀儡节点的链表插入

  1. 中间插入

在节点 1 和 2 之间插入新节点 100
在这里插入图片描述

  • step0:准备工作
    在这里插入图片描述

  • step1:在这里插入图片描述

  • step2:在这里插入图片描述

        //先创建带傀儡节点的链表
        Node head = creatListWithDummy();
        Node newNode = new Node(100);

        //在 1 和 2 之间插入元素,需要知道前一个(prev)元素的位置
        //prev 是指向 1 的位置。prev 表示前一个元素
        Node prev = head.next;
        newNode.next = prev.next;
        prev.next = newNode;
  • 头插入

     由于是带傀儡节点,对其头插入相当于插入到 head 的后面
    
        Node prev = head;
        newNode.next = prev.next;
        prev.next = newNode;
  • step0:准备工作

在这里插入图片描述

  • step1:
    在这里插入图片描述
  • step2:
    在这里插入图片描述
  • tips:此处的头结点是傀儡节点,head 是一个班指向傀儡节点的引用;傀儡节点是无意义的,遍历的时候不遍历傀儡节点。

链表尾插(带头结点和不带头结点)

此处以不带头结点为例进行说明

  • step:准备工作
    在这里插入图片描述
  • step1:
    在这里插入图片描述
  • step2:
    在这里插入图片描述

空链表插入

对于空链表的插入,只需要让 head 的引用指向新的节点即可。因为空链表中无任何节点,故不存在“前一个位置”。

  • 如若采用之前常规的方法插入是无法插入的
    //空链表插入操作
    public static void insertTail(Node head, int val) {
        Node newNode = new Node(val);
        if(head == null){
            head = newNode;
            return;
        }
        Node prev = head;
        Node cur = prev;
        while (cur.next != null){
            cur = cur.next;
        }//循环结束,cur 就是最后一个节点吧
        newNode.next = prev.next;
        prev.next = newNode;
    }
    public static void main(String[] args) {
        //空链表插入操作
        Node head = null;
        insertTail(head,100);
        print(head);        
    }

运行结果
在这里插入图片描述
《原因分析》
在这里插入图片描述

  • tips1:此处主函数中的 head 和 插入函数(insertTail)中的 head 不一样。
  • tips2:插入函数回调仅仅修改插入函数内部的 head 值,返回时,并不改变主函数中的head 值。
  • 总结:故使用常规方法无法插将新元素入到空链表中。

正确做法如下:

  1. 给插入函数设置返回值
    public static Node insertTail(Node head, int val) {
        Node newNode = new Node(val);
        if(head == null){
            return newNode;   //返回 newNode
        }
        Node prev = head;
        Node cur = prev;
        while (cur.next != null){
            cur = cur.next;
        }//循环结束,cur 就是最后一个节点吧
        newNode.next = prev.next;
        prev.next = newNode;
        return head; //返回 head
    }
    public static void main(String[] args) {
        //空链表插入操作
        Node head = null;
        head = insertTail(head,100);  //接收返回值
        print(head);        
    }

运行结果:
在这里插入图片描述

8.链表删除(不带傀儡节点)

1.删除一般节点
  • step1

在这里插入图片描述

  • step2

在这里插入图片描述
时间复杂度为O(N)–按值删除

//(1)删除节点(按照值删除)
    public static void remove(Node head,int value){
        //1.先找到 value 值对应的位置
        //  还需要找到 val 的前一个位置
        Node prev = head;
        //遍历循环找到 value 的前一个位置
        while (prev != null
                && prev.next != null
                && prev.next.val != value){
            prev = prev.next;
        }
        //循环结束之后,prev 指向了待删除节点的前一个节点
        if (prev == null || prev.next == null){
            return; //没有找到值为 val 的节点
        }

        //2.删除操作,toDelete 指向要被删除的节点
        Node toDelete = prev.next;
        prev.next = toDelete.next;
    }

时间复杂度为O(N)–按位置删除

//(2)删除节点,按照位置来删除
    public static void remove(Node head,Node toDelete){
        //step1:找到 toDelete 的前一个节点的位置
        Node prev = head;
        while (prev != null && prev.next != toDelete){
            prev = prev.next;
        }
        if(prev == null){
            return; //没找到
        }
        prev.next = toDelete.next;
    }

时间复杂度为O(1)的按位置删除操作:
在这里插入图片描述

    //时间复杂度为O(1)的按位置删除操作
    public static void remove2(Node head,Node toDelete){
        Node nextNode = toDelete.next;
        toDelete.val = nextNode.val;
        toDelete.next = nextNode.next;
    }
  • tips: 该方法无法处理最后一个节点
2.一般节点按照下标删除
    //按照下标(抽象概念)删除
    public static void remove3(Node head,int index){

        if (index < 0 || index >= size(head)){
            return;
        }

        //如果 index = 0,则删除头结点(后面讲)
        if(index == 0){
            //TODO
        }

        //step1:找到待删除节点前一个位置的节点,即:index - 1
        Node prev = head;
        for (int i = 0;i < index -1;i++){
            prev = prev.next;
        }
        //循环结束之后,prev 指向了待删除节点的前一个位置

        //step2:进行删除
        Node toDelete = prev.next;
        prev.next = toDelete.next;
    }
链表删除总结
  • 链表的删除大多数情况下都需要遍历找到前一个元素的位置,故其时间复杂度是O(N)
  • 链表和顺序表的时间复杂度无法具体比较出孰优孰劣。其两者的本质区别是顺序表的存储再内存上是连续的空间;而链表的存储在内存上可以不连续。
链表删除头结点

在这里插入图片描述

链表删除总结

为了实现形参的顺利返回,对原先实现的删除函数需要设置返回值 Node。

//(1)删除节点(按照值删除) 时间复杂度为O(N)
    public static Node remove(Node head, int value){
        if(head == null){
            return null;
        }

        //删除头结点
        if (head.val == value){
            //删除的节点即为头结点
            head = head.next;
            return head;
        }

        //1.先找到 value 值对应的位置
        //  还需要找到 val 的前一个位置
        Node prev = head;
        //遍历循环找到 value 的前一个位置
        while (prev != null
                && prev.next != null
                && prev.next.val != value){
            prev = prev.next;
        }
        //循环结束之后,prev 指向了待删除节点的前一个节点
        if (prev == null || prev.next == null){
            return null; //没有找到值为 val 的节点
        }

        //2.删除操作,toDelete 指向要被删除的节点
        Node toDelete = prev.next;
        prev.next = toDelete.next;
        return head;
    }

    //(2)删除节点,按照位置来删除  时间复杂度为O(N)
    public static Node remove(Node head,Node toDelete){
        if (head == null){
            return null;
        }

        if(head == toDelete){
            //要删除的即为头结点
            head = head.next;
            return head;
        }

        //step1:找到 toDelete 的前一个节点的位置
        Node prev = head;
        while (prev != null && prev.next != toDelete){
            prev = prev.next;
        }
        if(prev == null){
            return null; //没找到
        }
        prev.next = toDelete.next;
        return head;
    }


    //时间复杂度为O(1)的按位置删除操作
    public static Node remove2(Node head,Node toDelete){
        if(head == null){
            return null;
        }
        if (head == toDelete){
            head = head.next;
            return head;
        }

        Node nextNode = toDelete.next;
        toDelete.val = nextNode.val;
        toDelete.next = nextNode.next;
        return head;
    }
    //注意:该方法无法处理最后一个节点


    public static int size(Node head){
        int size = 0;
        for (Node cur = head;cur != null;cur = cur.next){
            size++;
        }
        return size;
    }


    //按照下标(抽象概念)删除
    public static Node remove3(Node head,int index){

        if (index < 0 || index >= size(head)){
            return head;
        }

        //如果 index = 0,则删除头结点
        if(index == 0){
            head = head.next;
        }

        //step1:找到待删除节点前一个位置的节点,即:index - 1
        Node prev = head;
        for (int i = 0;i < index -1;i++){
            prev = prev.next;
        }
        //循环结束之后,prev 指向了待删除节点的前一个位置

        //step2:进行删除
        Node toDelete = prev.next;
        prev.next = toDelete.next;
        return head;
    }

主函数调用

    public static void main(String[] args) {
        Node head = creatList();
        head = remove(head,1);
        print(head);
        }
    }

运行结果:
在这里插入图片描述

9.链表删除(带傀儡节点)

1.删除原理
  • step1
    在这里插入图片描述

  • step2
    在这里插入图片描述

2.删除过程
    //带傀儡节点的删除
    public static void removeWithDummy(Node head,int val){
        //使用带傀儡节点的好处:
        // 1.避免了 head 引用修改的问题
        // 2.避免了删除头结点需要判断的问题

        //循环寻找 val 匹配的值
        Node prev = head;
        while (prev != null && prev.next != null
                && prev.next.val != val){
            prev = prev.next;
            //循环结束的两种情况:
            //1.prev 到达了链表末尾也没找到和 val 匹配的值
            //2.找到了和 prev 匹配的值

            if (prev == null && prev.next == null){
                //没找到对应节点
                return;
            }
            //找到了对应节点
            Node toDelete = prev.next;
            prev.next = toDelete.next;
            return;
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值