LeetCode:https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/

题目描述

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

进阶:你能尝试使用一趟扫描实现吗?

示例 1:

输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

示例 2:

输入:head = [1], n = 1
输出:[]

示例 3:

输入:head = [1,2], n = 1
输出:[1]
 

提示:

链表中结点的数目为 sz
1 <= sz <= 30
0 <= Node.val <= 100
1 <= n <= sz

 

解题思路:

若链表为空,则无法删除

若删除链表头结点,则需要改动头结点起始位置

因为要删除倒数第n个节点,因此需要知道链表总长度(实际不需要,优化方案有说明)

 

解法1、两次遍历法,正序删除

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode first = head;
		int size = 0;

		if(head == null) return head;

        // 第一次遍历, 求出链表长度
		while(first != null) {
			size++;
			first = first.next;
		}

		int index = size - n;   // 顺序要删除的下标, 起始下标为0
		int count = 0;          // 对链表头开始遍历的元素个数
		first = head;           // 重置链表头(求链表长度已遍历过一遍)
		
        // 删除第一个元素
		if(index == 0) {
			first = first.next;
            head = first;   // 更改链表头
			return head;
		}
		
        // 第二次遍历, 顺序删除链表第index个元素
        // 1->2 3 4->5  删除倒数第3个元素
		while(first != null) {
            count++;
			if(count == index) {
                first.next = first.next.next;
				break;
			} else {
                first = first.next;
            }
		}
        return head;
    }
}

说明:

此算法遍历了两次链表,第一次求链表长度,第二次删除第index个节点

时间复杂度:O(n)  ,空间复杂度:O(1) ,只用了常量级的额外空间。

 

 

解法2、两次遍历,引入了最顶临时节点,简化首节点判断

注意到这个问题可以容易地简化成另一个问题:删除从列表开头数起的第 (size - n + 1)个结点,其中 size 是列表的长度。只要我们找到列表的长度 size,这个问题就很容易解决。

1)我们将添加一个最顶临时结点作为辅助,该结点位于列表头部。最顶临时结点用来简化了某些极端情况,例如列表中只含有一个结点,或需要删除列表的头部。

2)在第一次遍历中,我们找出列表的长度 size。然后设置一个指向最顶临时结点的指针,并移动它遍历列表,直至它到达第 (size - n)个结点那里。我们把第 (size - n)个结点的 next 指针重新链接至第 (size - n + 2)个结点,完成这个算法。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode first = head;
		int size = 0;

        ListNode top = new ListNode(0);     // 增加了一个临时最顶节点
        top.next = head;                    // 临时最顶节点指向head头, 解决删除头结点的问题

        while(first != null) {
            size++;
            first = first.next;
        }

        int index = size - n;
        first = top;    // 指向最顶临时节点
        while(index-- > 0) {
            first = first.next;
        }
        first.next = first.next.next;   // 最顶临时节点的next指向删除第index节点后的头结点
        return top.next;                // 返回删除后的头结点
    }
}

复杂度分析

时间复杂度:O(n) ,该算法对列表进行了两次遍历,首先计算了列表的长度 [公式] 其次找到第 (size - n)个结点。 操作执行了(2*size - n)步,时间复杂度为 O(n)

空间复杂度:O(1) ,只用了常量级的额外空间。

 

 

解法3、一次遍历,快慢两个指针相差步长为n,精妙

上述算法可以优化为只使用一次遍历。我们可以使用两个指针而不是一个指针。

第一个指针从列表的开头向前移动 (n + 1) 步,而第二个指针将从列表的开头出发。

现在,这两个指针被 n 个结点分开。我们通过同时移动两个指针向前来保持这个恒定的间隔(解决了倒数第n个节点的问题,从链表尾部向前计算,相差为n,精妙吧),直到第一个指针到达最后一个结点。此时第二个指针将指向从最后一个结点数起的第 n 个结点(非常关键,解题的核心,结束条件)。

我们重新链接第二个指针所引用的结点的 next 指针指向该结点的下下个结点。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode fast = head;   // 快指针, 先遍历n个元素
        ListNode slow = head;   // 慢指针, 后开始遍历, 与fast同步
		
        // fast 与 slow 距离为 n + 1
        while(fast != null && n > 0) {
            fast = fast.next;
            n--;
        }

        // 1->2->3->4->5  n=2  1->2->3->5
        while(fast != null && fast.next != null) {   // fast不等于null, slow肯定也不等于null
            fast = fast.next;
            slow = slow.next;
        }

        if(fast == null) {  // fast == null 是结束条件
            head = head.next;
        } else {
            slow.next = slow.next.next;
        }
        
        return head;
    }
}

复杂度分析

时间复杂度:O(n) ,该算法对含有 size 个结点的列表进行了一次遍历。因此时间复杂度为 O(n)

空间复杂度:O(1) ,我们只用了常量级的额外空间

 

 

解法4、一次遍历,删除倒数第n个元素,链表转数组存储,获取删除元素的前后元素,直观处理

核心思路:删除链表倒数第n个元素,即倒数第n个元素的前一个元素的next指向n元素的next

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode first = head;

        Map<Integer, ListNode> map = new HashMap<Integer, ListNode>();

        int size = 0;
        int index = 0;
        while(first != null) {
            map.put(index++, first);
            first = first.next;
        }

        size = map.size();
        if(size == 0) return null;

        // 1->2->3->4->5  n=2  1->2->3->5
        if(n == size) {
            head = head.next;   // 删除链表第一个元素, 需修改链表头
        } else {
            map.get(size-n-1).next = map.get(size-n).next;  // 倒数第n个元素的前一个元素next, 指向n的next
        }
        return head;
}

 

 

解法5、一次遍历,删除倒数第n个元素,运用栈的特性:后进先出顺序

因为是删除链表的倒数第n个元素,逆向思维:即删除压栈后,先弹出的第n个元素

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode first = head;

        // 删除倒数第n个元素, 运用栈的后进先出特性, 来直观求解
        Stack<ListNode> stack = new Stack<ListNode>();
        while(first != null) {
            stack.push(first);
            first = first.next;
        }

        // 栈顶后入先出特性, 获取倒数的第n个元素
        ListNode delNode = null;    // 记录删除的元素
        while(n-- > 0) {
            delNode = stack.pop();
        }

        if(stack.size() > 0) {  
            stack.pop().next = delNode.next;
        } else {        // 栈为空, 即全部删除完了
            head = head.next;
        }
        return head;
}

 

 

解法6、一次遍历,删除倒数第n个元素,优化解法5:凡事用栈解决的,用递归也可解决

核心思想:跟算法5类似,采用递归压栈,再出栈,代码非常简洁、统一、完美

注意:递归函数的变量,要在函数外赋值全局变量,例如 int count = 0; 在递归函数外

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    int count = 0;
    public ListNode removeNthFromEnd(ListNode head, int n) {
        // 2) 遇到最后一个元素为null, 则开始出栈, 返回
        if(head == null) return null;   

        // 1) 先把全部元素压栈(类似于算法5的Stack)
        // 1->2->3->4->5  n=2  1->2->3->5
        head.next = removeNthFromEnd(head.next, n); 

        // 3) 递归一次加1, 类似于解法5的压栈后就开始出栈, 出栈的第n个元素即是删除倒数第n个元素 
        if(n == ++count) return head.next;    // 返回删除元素n的next
        return head;
    }
}

晒一下成就:Java 执行用时 0ms,击败了 100%用户

 

总结

本文对一道算法题,用了六种解法,分别是:

1)两次遍历链表,把倒数第n个元素,转成了顺数第 size - n 个元素

2)优化算法1,新增了一个链表最顶部节点(假节点),解决删除头结点的判定问题

3)一次遍历,精妙设计快慢指针,二者相差为n个元素,fast快指针遍历到尾即结束

4)一次遍历,链表转数组,通过数组的下标直观进行链表的next链接

5)一次遍历,增加了Stack栈数据结构,把倒数第n个元素转成栈顶弹出第n个元素,更容易理解

6)一次遍历,凡是用栈能够实现的,大多也可以用递归函数实现(栈实现一般用深度递归队列也可实现)

刷算法题,注重数量的同时,更要注重质量,深入思考,不断优化递进,培养思维的深度

对一道题深度挖掘,举一反三,例如倒数第n个元素,用栈来实现,逆向思维,太美好啦

 

 

参考推荐:

八大排序算法图文讲解

LeetCode 622. 设计循环队列

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

经典排序算法的复杂度分析

10大程序员基础实用算法

面试最常用的10大算法

KMP 字符串匹配算法

哈希表算法原理

ZeroMQ 异步消息队列通信

Python学习入门(29)——消息队列

Python 常用加密算法 base64, md5, sha1

FIFO、LRU、LFU 缓存淘汰算法的原理

Bloom Filter 算法处理海量数据

SimHash 算法原理及实现

Java 实现线程间通知和唤醒

HTTP 协议的历史进化演变

区块链核心算法与技术原理

聚类算法之KMeans(Java实现)

程序猿追MM的各种算法