19. 删除链表的倒数第N个结点

19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)

首先, 这个链表是一个单项链表, 其次, 需要逆序删除结点,

我首先想的, 能不能递归? 要不然就用一个栈存所有, 最后出栈解决问题,

这样想来, 用堆栈解决应该是比较正常的

那么, 首先用栈, 虽然很拉胯, 但是用用嘛

嗯, 还是去看解法思路了, 本来只是想看个标题就跑, 但是只看标题还是不明白具体实现

  1. 递归, 真的有人用递归做出来了, 说明是可行的, 那我的看看怎么做到

那么, 让我来梳理一下关键词, 链表, 递归, 计数

同时要注意, 递归是同样用到栈操作的, 你是不是注意到了什么, 想到什么了, 说说, 压到栈里, 说明递归到最后, 结束的标志是 node.next == null 这点没法反驳对吧, 所以这里才是关键, 达到这个标志就是最后的地方,从这里, 开始从0计数, 达到n咱们就跑路, 但是还是有问题, 计数是到了节点, 只是刚刚好的节点, 但是如何删除这个节点呢

2022年8月19日15:03 更新, 发现计数的位置刚好是走到要删除节点的前一个位置, 是我疏忽了

所以需要从最开始的角度来思考, 判断, 如果只有一个的情况下, 要删除一个怎么办, 其他的, 就找它的前一个节点进行删除

我们先来梳理一下链表的结构

graph LR 节点1-->节点2-->节点3--->哒哒哒--->尾节点

那么, 如果是单一节点, 删除一个

只要直接null就好

如果是两个节点及以上, 就用.next = null, 如果是头结点另行处理,

但是自己的处理结果, 怎么都不会觉得满意, 不仅繁杂, 而且, 没法处理头结点的情况,

一开始, 我的处理方式是这样

 // 统一, 一定需要头指针进行比对操作
    public static int removeNode(ListNode head, ListNode node, int n) {

        // 特殊情况:倒数第一个
        if (node.next == null) {
            // 同时, 如果刚好删除倒数第一个结点,
            if (n == 1) {
                // 同时这个节点就是头指针
                if (node == head) {
                    head = null;
                }
                node = null;
            }
            return 1;
        }else{
            int m = removeNode(head,node.next,n);
            if(m == n){
                if(n == 1){
                    node.next = null;
                }else{
                    node.next = node.next.next;
                }
            }
            // 针对删除头结点的情况进行特殊处理, 由于到头节点的情况下不能再递归, 那我就手动处理, 反正到了头结点我也不再需要额外内容的了
            if(node == head) {
                if ((m + 1) == n) {
                    node = node.next;
                    head = node;
                }
            }
            return m+1;
        }
    }

不仅需要头指针, 而且每次遍历还需要判断, 最重要的的是, 我这套思想是照着之前数据结构里边C语言来实现的, 在指针情况下, 我修改head没有问题, 但是, 这里, 是! Java! 传进来的Head你居然想改它让它传出去, 这是一个非常不好的想法, 传不出去, 同时在递归

所以, 根据大佬的思想, 我重新构建了一下代码, 这个是评论区大佬的递归方法, 本来想标注一下名字的, 但是过了一天回去找, 发现评论区那边翻不到了, 有点尴尬

确实,优雅

/*
* @param null:
* @return
* @author/作者 TenderFlow
* @description/描述 Tip
* @date/时间 2022/8/17 10:08
*/
public static ListNode removeNthFromEnd1(ListNode head, int n) {
    //        递归如何做到倒数
    int result = removeNode(head, n);
    // 我们会发现, 在里边只是处理了非头结点的问题, 那么头结点, 不就在这里吗, 我们只需要在这里,完成递归的最后一项, 太优雅了
    if(result == n){
        return head.next;
    }
    return head;
}


// 处理删除非头结点的情况
public static int removeNode(ListNode node, int n) {
    if (node == null){
        return 0;
    }
    int m = removeNode(node.next,n);
    if(m == n){
        node.next = node.next.next;
    }
    return m+1;
}

真的是, 完美解法, 我悟了

接下来就是解法2, 最难的解法我都解决了, 我已经全部理解了(叉腰),蛐蛐双指针, 何足挂齿

  1. 双指针, 是大名鼎鼎的双指针哇, 确实也是很好使的东西

确实, 好写很多, 整体写下来, 对于逻辑冲突的解决是比较流畅的

原理就是两个节点对象(ListNode),一个先沿着链表走n步, 走到以后另一个才开始走, 这样最先走的一个走到倒数第一个了, 第二个节点对象就在要删除的内容附近了(可能就是, 可能在要删除的节点前一个, 这个具体还是看我们一开始走的实现上可能有区别, 一般是在要删除节点的前一个节点停下来), 也是很优雅的写法, 哦吼吼吼, 感觉自己也变得优雅了

/*
* @param null:
* @return
* @author/作者 TenderFlow
* @description/描述 Tip
* @date/时间 2022/8/17 17:05:23
*/
public static ListNode removeNthFromEnd1(ListNode head, int n) {
    ListNode nodeFast = head;
    ListNode nodeLow = head;
    for (int i = 0; i < n; i++) {
        nodeFast = nodeFast.next;
    }
    if (nodeFast == null) {
        return head.next;
    } else {
        while (nodeFast.next != null) {
            nodeFast = nodeFast.next;
            nodeLow = nodeLow.next;
        }
        nodeLow.next = nodeLow.next.next;
        return head;
    }
}

总结:

  1. 可以使用栈, 但是栈和递归在理念上是完全相同的, 两者基本是差不多的

    1. 使用双指针, 前后两个指针差距几个节点, 然后向一把尺子移动到终点, 那么尺子的起点就是我们需要删除的节点, 不过是处理删除节点的时候有些问题罢了, 其他, 都没什么好说的
    2. 是否需要使用哑节点,哑节点只是为了简便头节点的删除工作, 但是实际上看起来, 头结点的删除其实并不难, 头结点处理的难处是为了替换掉原先的指针, 在一个函数内, 但是, 对于真正熟悉指针的人来说, 这个其实并不是非常难以理解和处理的问题,因为如果使用返回值的话, 我们想怎么处理就怎么处理, 确实好使, 以前被void折磨太多次了, 反而被束缚了手脚
方法3: 最简单的傻瓜式方法

先遍历一遍找长度, 再遍历一遍找节点, 让我比较奇怪的是, 这个方法在测试的时候反而效果好,大概是因为数据量其实并不大的缘故吧, 大家都是0ms,而这种方式又把递归的堆栈所需的空间节省了, 所以看起来比较快, 那么, 我的兴趣就来了, 既然大家都是最快, 都是时间击败100%的人, 我可不可以, 在空间上击败100%的人呢?

/*
 * @param null: 
 * @return 
 * @author/作者 TenderFlow
 * @description/描述 Tip
 * @date/时间 2022/8/19 15:25
 */
public ListNode removeNthFromEnd1(ListNode head, int n) {
    int len = 0;
    ListNode cur = head;
    // 获取长度
    while (cur != null){
        cur = cur.next;
        len = len+1;
    }
    if(len == 1 && n == 1) return null;
    if(len == n) return head.next;
    cur = head;
    int cnt = 0; //计数
    while (cnt < len-n-1){
        cur = cur.next;
        cnt++;
    }
    if(n == 1) cur.next = null;
    else cur.next = cur.next.next;

    return head;
}

但是一试我就知道我错了, 我的代码相比对方甚至经过简化, 但是内存使用大小比他大, 完全无法理解, 甚至使用了一下相同的代码, 我的提交记录相比两天前内存消耗量是增加的, 这东西也看玄学的吗? 无法理解, 不管了, 梦碎了, 咱开摆

posted @ 2022-08-19 15:29  TenderFlow  阅读(80)  评论(0)    收藏  举报