算法通关村第一关——链表青铜挑战笔记

1 理解Java是如何构造出链表的?

首先要理解 JVM 是怎么构建出链表的,JVM 里有栈区和堆区,栈区主要存引用,也就是一个指向实际对象的地址,而堆区存的才是创建的对象

假如这样定义一个类:

public class Course{
    int val;
    Course next;
}

这时候 next 就指向了下一个同为 Course 类型的对象了,例如:

image-20230911220233877

这里通过栈中的引用(也就是地址)就可以找到 val(1) ,然后 val(1) 结点又存了指向 val(2) 的地址,而 val(3) 又存了指向 val(4) 的地址,所以就构造出了一个链条访问结构。

从 head 开始 next 会发现是这样的:

image-20230911220450363

这就是一个简单的线性访问了,所以链表就是从 head 开始,逐个开始向后访问,而每次所访问对象的类型都是一样的。

根据面向对象的理论,在 Java 里规范的链表应该这么定义:

public class ListNode  {
    private int data;
    private ListNode  next;

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

    public int getData() {
        return data;
    }

    public void setData(int data) {
        this.data = data;
    }

    public ListNode  getNext() {
        return next;
    }

    public void setNext(ListNode  next) {
        this.next = next;
    }
}

但是在算法中经常使用这样的方式来创建链表:

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

    public ListNode(int x) {
        val = x;
        next = null;
    }

    public static void main(String[] args) {
        ListNode listnode=new ListNode(1);
    }
}

这里的 val 就是当前结点的值,next 指向下一个结点。因为两个变量都是 public 的,创建对象后能直接使用 istnode.val 和 listnode.next 来操作,虽然违背了面向对象的设计要求,但是上面的代码更为精简,因此在算法题目中应用广泛。

2 链表增加元素,首部、中间和尾部分别会有什么问题,该如何处理?

单链表的插入,和数组的插入一样,过程不复杂,但是在编码时会发现处处是坑。单链表的插入操作需要要考虑三种情况:首部、中部和尾部。

2.1 头部插入

链表表头插入新结点非常简单,容易出错的是经常会忘了 head 需要重新指向表头。 我们创建一个新结点 newNode,怎么连接到原来的链表上呢? 执行newNode.next = head 即可。之后我们要遍历新链表就要从 newNode 开始一路 next 向下了是吧,但是我们还是习惯让head来表示,所以让head=newNode就行了,如下图:

image-20230911223058734

2.2 中间插入

在中间位置插入,我们必须先遍历找到要插入的位置,然后将当前位置接入到前驱结点和后继结点之间,但是到了该位置之后我们却不能获得前驱结点了,也就无法将结点接入进来了。这就好比一边过河一边拆桥,结果自己也回不去了。

为此,我们要在目标结点的前一个位置停下来,也就是使用 ur.next 的值而不是 cur 的值来判断,这是链表最常用的策略。

列如下图中,如果要在7的前面插入,当 cur.next=node(7) 了就应该停下来,此时 cur.val=15 。然后需要给 newNode 前后接两根线,此时只能先让new.next=node(15).next (图中虚线),然后 node(15).next=new ,而且顺序还不能错。

image-20230911223642343

思考:为什么不能颠倒顺序?
由于每个节点都只有一个next,因此执行了node(15).next=new之后,结点15和7之间的连线就自动断开了,如上图所示。

2.3 尾部插入

尾部插入,是最简单的情况,把最后一个节点的next指针指向新插入的节点即可。

image-20230911223930152

只要内存空间允许,能够插入链表的元素是无穷无尽的,不需要像数组那样考虑扩容的问题。

综上所述,链表插入方法代码如下:

 /**
     * 链表插入
     *
     * @param head       链表头节点
     * @param nodeInsert 待插入节点
     * @param position   待插入位置,取值从2开始
     * @return 插入后得到的链表头节点
     */
    public static Node insertNode(Node head, Node nodeInsert, int position) {
        // 需要判空,否则后面可能会有空指针异常
        if (head == null) {
            return nodeInsert;
        }
        //越界判断
        int size = getLength(head);
        if (position > size + 1 || position < 1) {
            System.out.println("位置参数越界");
            return head;
        }

        //在链表开头插入
        if (position == 1) {
            nodeInsert.next = head;
//            return nodeInsert;
            //上面return还可以这么写:
            head = nodeInsert;
            return head;
        }

        Node pNode = head;
        int count = 1;
        while (count < position - 1) {
            pNode = pNode.next;
            count++;
        }
        nodeInsert.next = pNode.next;
        pNode.next = nodeInsert;

        return head;
    }

补充:head = null 的时候该执行什么操作呢?

如果是 null 的话,你要插入的结点就是链表的头结点,也可以直接抛出不能插入的异常,两种处理都可以,一般来说我们更倾向前者。

3 链表删除元素,首部、中间和尾部分别会有什么问题,该如何处理?

链表的删除操作同样分为3种情况。

3.1 头部删除

删除表头元素还是比较简单的,一般只要执行 head=head.next 就行了。如下图,将 head 向前移动一次之后,原来的结点不可达,会被 JVM 回收掉。

image-20230911225429004

3.2 尾部删除

删除的过程不算复杂,也是找到要删除的结点的前驱结点,这里同样要在提前一个位置判断,例如下图中删除 40,其前驱结点为 7。遍历的时候需要判断 cur.next 是否为 40,如果是,则只要执行 cur.next=null 即可,此时结点 40 变得不可达,最终会被 JVM 回收掉。

image-20230911225457084

3.3 中间删除

删除中间结点时,也会要用 cur.next 来比较,找到位置后,将 cur.next 指针的值更新为 cur.next.next 就可以解决,如下图所示:

image-20230911225649306

综上所述,代码如下:

/**
     * 删除节点
     *
     * @param head     链表头节点
     * @param position 删除节点位置,取值从1开始
     * @return 删除后的链表头节点
     */
    public static Node deleteNode(Node head, int position) {
        if (head == null) {
            return null;
        }
        int size = getLength(head);
        //思考一下,这里为什么是size,而不是size+1
        if (position > size || position <1) {
            System.out.println("输入的参数有误");
            return head;
        }
        if (position == 1) {
            //curNode就是链表的新head
            return head.next;
        } else {
            Node cur = head;
            int count = 1;
            while (count < position - 1) {
                cur = cur.next;
                count++;
            }
            Node curNode = cur.next;
            cur.next = curNode.next;
        }
        return head;
    }

4 双向链表是如何构造的,如何实现元素的插入和删除?

双向链表的每一个节点除了拥有 data 和 next 指针,还拥有指向前置节点的 prev 指针。

image-20230911214337432

双向链表的一个基本实现通常需要节点类(Node)和双向链表类(DoublyLinkedList)。节点类存储数据和指向前一个以及后一个节点的指针。双向链表类包含指向链表头部的指针,以及管理链表的各种方法,例如插入节点和删除节点。

双向链表实现示例:

class Node {
    int data;
    Node prev;
    Node next;

    Node(int data) {
        this.data = data;
        this.prev = null;
        this.next = null;
    }
}

class DoublyLinkedList {
    Node head;

    // 插入节点到链表的末尾
    public void append(int data) {
        if (head == null) {
            head = new Node(data);
        } else {
            Node current = head;
            while (current.next != null) {
                current = current.next;
            }
            Node newNode = new Node(data);
            current.next = newNode;
            newNode.prev = current;
        }
    }

    // 删除指定数据的节点
    public void delete(int data) {
        Node current = head;
        while (current != null) {
            if (current.data == data) {
                if (current.prev != null) {
                    current.prev.next = current.next;
                    if (current.next != null) {
                        current.next.prev = current.prev;
                    }
                } else {
                    head = current.next;
                    if (current.next != null) {
                        current.next.prev = null;
                    }
                }
                return;
            }
            current = current.next;
        }
    }

    // 打印链表的所有元素
    public void printList() {
        Node current = head;
        while (current != null) {
            System.out.println(current.data);
            current = current.next;
        }
    }
}

在这个例子中,append 方法在链表的末尾添加一个新的节点,delete 方法删除具有指定数据的节点,而 printList 方法打印出链表的所有元素。

点击查看微信公众号【程序员小新儿】详细笔记

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值