题目描述(中等难度)

19. Remove Nth Node From End of List - 图1

给定一个链表,将倒数第 n 个结点删除。

解法一

删除一个结点,无非是遍历链表找到那个结点前边的结点,然后改变下指向就好了。但由于它是链表,它的长度我们并不知道,我们得先遍历一遍得到它的长度,之后用长度减去 n 就是要删除的结点的位置,然后遍历到结点的前一个位置就好了。

  1. public ListNode removeNthFromEnd(ListNode head, int n) {
  2. int len = 0;
  3. ListNode h = head;
  4. while (h != null) {
  5. h = h.next;
  6. len++;
  7. }
  8. //长度等于 1 ,再删除一个结点就为 null 了
  9. if (len == 1) {
  10. return null;
  11. }
  12. int rm_node_index = len - n;
  13. //如果删除的是头结点
  14. if (rm_node_index == 0) {
  15. return head.next;
  16. }
  17. //找到被删除结点的前一个结点
  18. h = head;
  19. for (int i = 0; i < rm_node_index - 1; i++) {
  20. h = h.next;
  21. }
  22. //改变指向
  23. h.next = h.next.next;
  24. return head;
  25. }

时间复杂度:假设链表长度是 L ,那么就第一个循环是 L 次,第二个循环是 L - n 次,总共 2L - n 次,所以时间复杂度就是 O(L)。

空间复杂度:O(1)。

我们看到如果长度等于 1 和删除头结点的时候需要单独判断,其实我们只需要在 head 前边加一个空节点,就可以避免单独判断。

  1. public ListNode removeNthFromEnd(ListNode head, int n) {
  2. ListNode dummy = new ListNode(0);
  3. dummy.next = head;
  4. int length = 0;
  5. ListNode first = head;
  6. while (first != null) {
  7. length++;
  8. first = first.next;
  9. }
  10. length -= n;
  11. first = dummy;
  12. while (length > 0) {
  13. length--;
  14. first = first.next;
  15. }
  16. first.next = first.next.next;
  17. return dummy.next;
  18. }

解法二 遍历一次链表

上边我们遍历链表进行了两次,我们如何只遍历一次呢。

看了 leetcode 的讲解。

想象一下,两个人进行 100m 赛跑,假设他们的速度相同。开始的时候,第一个人就在第二个人前边 10m ,这样当第一个人跑到终点的时候,第二个人相距第一个人依旧是 10m ,也就是离终点 10m。

对比于链表,我们设定两个指针,先让第一个指针遍历 n 步,然后再让它俩同时开始遍历,这样的话,当第一个指针到头的时候,第二个指针就离第一个指针有 n 的距离,所以第二个指针的位置就刚好是倒数第 n 个结点。

  1. public ListNode removeNthFromEnd(ListNode head, int n) {
  2. ListNode dummy = new ListNode(0);
  3. dummy.next = head;
  4. ListNode first = dummy;
  5. ListNode second = dummy;
  6. //第一个指针先移动 n 步
  7. for (int i = 1; i <= n + 1; i++) {
  8. first = first.next;
  9. }
  10. //第一个指针到达终点停止遍历
  11. while (first != null) {
  12. first = first.next;
  13. second = second.next;
  14. }
  15. second.next = second.next.next;
  16. return dummy.next;
  17. }

时间复杂度:

第一个指针从 0 到 n ,然后「第一个指针再从 n 到结束」和「第二个指针从 0 到倒数第 n 个结点的位置」同时进行。

而解法一无非是先从 0 到 结束,然后从 0 到倒数第 n 个结点的位置。

所以其实它们语句执行的次数其实是一样的,从 0 到倒数第 n 个结点的位置都被遍历了 2 次,所以总共也是 2L - n 次。只不过这个解法把解法一的两次循环合并了一下,使得第二个指针看起来是顺便遍历,想法很 nice。

所以本质上,它们其实是一样的,时间复杂度依旧是 O(n)。

空间复杂度:O(1)。

解法三

没看讲解前,和室友讨论下,如何只遍历一次链表。室友给出了一个我竟然无法反驳的观点,哈哈哈哈。

第一次遍历链表确定长度的时候,顺便把每个结点存到数组里,这样找结点的时候就不需要再遍历一次了,空间换时间???哈哈哈哈哈哈哈哈哈。

  1. public ListNode removeNthFromEnd(ListNode head, int n) {
  2. List<ListNode> l = new ArrayList<ListNode>();
  3. ListNode h = head;
  4. int len = 0;
  5. while (h != null) {
  6. l.add(h);
  7. h = h.next;
  8. len++;
  9. }
  10. if (len == 1) {
  11. return null;
  12. }
  13. int remove = len - n;
  14. if (remove == 0) {
  15. return head.next;
  16. }
  17. //直接得到,不需要再遍历了
  18. ListNode r = l.get(remove - 1);
  19. r.next = r.next.next;
  20. return head;
  21. }

时间复杂度:O(L)。

空间复杂度:O(L)。

利用两个指针先固定间隔,然后同时遍历,真的是很妙!另外室友的想法也很棒,哈哈哈哈哈。

windliang wechat

添加好友一起进步~

如果觉得有帮助的话,可以点击 这里 给一个 star 哦 ^^

如果想系统的学习数据结构和算法,强烈推荐一个我之前学过的课程,可以点击 这里 查看详情