算法刷题一:链表。反转链表,环形链表,交换链表的节点

文章目录

  • 数组和链表
  • 本篇设计到的题目
  • 链表题目练习(包含代码)
    • ListNode节点的定义以及相应的操作方法
    • LeetCode206. 反转链表
      • 方法一:使用三个指针进行迭代
      • 方法二:使用递归
    • LeetCode141. 环形链表
      • 方法一:使用快慢指针
      • 方法二:暴力法
      • 方法三:使用Set存储遍历过的节点
    • LeetCode24.两两交换链表中的节点
      • 方法一:使用递归
      • 方法二:使用指针进行迭代(从左至右)
    • LeetCode142. 环形链表 II
      • 方法一:使用Set存储遍历过的节点
      • 方法二:使用快慢指针
    • LeetCode25. K 个一组翻转链表
      • 方法一:使用递归
      • 方法二:使用迭代

数组和链表

提及数组和链表。习惯性地就想起了数组的查找是O(1)、平均插入和删除的时间复杂度为O(N)。链表查找是O(N)、插入和删除是O(1)。

仔细想想,这样的说法并不完全正确。这里的查找是说根据下标来查找,而不是查找根据元素值来进行查找。比如有个数组,a[ ],访问数组的第三个元素(下标从0开始),直接用a[2]就可以得到第三个元素,而链表的话并不支持这样的快速访问,你要得到第三个元素,必须一个一个遍历,需要一直数到第三个元素。原因在于,数组的地址是连续的,我们知道第一个元素的地址:base。知道数组中元素的大小:size,比如一个int占4个字节。然后就可以通过这么一个计算公式得到第k个元素的地址:base + (k - 1)size 然后就可以进行访问。而链表的存储空间并不连续,所以无法这样计算。

也就是说:根据下标来查找,数组的时间复杂度为O(1),链表为O(N)

如果不是根据下标来查找,而是根据值来查找呢?也就是说我们不是要找第三个元素,而是要找有没有3这个元素。那不好意思,数组和链表的时间复杂度都是O(N),因为必须得从头开始遍历,一个一个找。如果数组中的元素是有序的,那么可以用二分查找,时间复杂度可以降低为O(logn),但是达不到O(1)。如果链表中的元素也是有序的呢,可以用跳表这种数据结构,用空间换时间,也可以将查找的时间复杂度降低为O(logn)。

那有没有一种数据结构,根据元素值来进行查找可以达到O(1)的时间复杂度呢,目前我所了解的算法里面只有Hash表能达到这样的效果,而且还得看Hash函数是否设计得好。

本篇设计到的题目

LeetCode206. 反转链表

LeetCode141. 环形链表

LeetCode24. 两两交换链表中的节点

LeetCode142. 环形链表 II

LeetCode25. K 个一组翻转链表

链表题目练习(包含代码)

ListNode节点的定义以及相应的操作方法

为了方便在本机调试,我还写了一些方法用于生成链表返回链表中的第K个节点返回链表的遍历结果。为了简单,我也没使用泛型,假设链表中存储的都是int类型的数据。

import java.util.ArrayList;
import java.util.List;

/**
 * 链表的定义
 * @author simon zhao
 */
public class ListNode {
  int val;
  ListNode next;

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

  /**
   * 根据数组生成LinkedList
   *
   * @param nums
   * @return
   */
  public static ListNode generateLinkedList(int[] nums) {
    if (nums == null || nums.length == 0) {
      return null;
    }
    ListNode head = new ListNode(nums[0]);
    ListNode cur = head;
    for (int i = 1; i < nums.length; ++i) {
      ListNode node = new ListNode(nums[i]);
      cur.next = node;
      cur = node;
    }
    return head;
  }

  /**
   * 遍历LinkedList,返回一个List
   *
   * @param head
   * @return
   */
  public static List<Integer> traverseLinkedList(ListNode head) {
    if (head == null) {
      return null;
    }
    List<Integer> traverse = new ArrayList<>();
    ListNode cur = head;
    while (cur != null) {
      traverse.add(cur.val);
      cur = cur.next;
    }
    return traverse;
  }

  /**
   * 返回LinkedList中index这个ListNode
   *
   * @param head
   * @param index
   * @return
   */
  public static ListNode getIndexNode(ListNode head, int index) {
    if (head == null) {
      return null;
    }
    int count = 0;
    ListNode cur = head;
    while (cur != null) {
      if (count == index) {
        return cur;
      }
      if (count < index) {
        count++;
        cur = cur.next;
      }
    }
    //如果LinkedList都已经遍历完毕,但是count依然小于index,说明index过大,超出了LinkedList的长度
    return null;
  }
}

LeetCode206. 反转链表

题目链接:LeetCode206. 反转链表

题目概要

反转一个单链表。

示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

一段精简的Python代码

def reverseList(self, head):
    cur, prev = head, None
    while cur:
        cur.next, prev, cur = prev, cur, cur.next
    return prev

需要注意的是:cur.next, prev, cur = prev, cur, cur.next并不是先进行cur.next = prev,然后进行prev = cur。而是多步赋值操作一起进行的,堪称一步到位。
算法刷题一:链表。反转链表,环形链表,交换链表的节点_第1张图片
用Java代码实现

方法一:使用三个指针进行迭代

public ListNode reverseList(ListNode head) {
   if (head == null) {
     return head;
   }
   ListNode cur = head, pre = null, next;
   while (cur != null) {
     next = cur.next;
     cur.next = pre;
     pre = cur;
     cur = next;
   }
   return pre;
 }

方法二:使用递归

public ListNode reverseList(ListNode head) {
  if (head == null || head.next == null) {
    return head;
  }
  ListNode p = reverseList(head.next);
  head.next.next = head;
  head.next = null;
  return p;
}

递归代码不好理解,可以看图
算法刷题一:链表。反转链表,环形链表,交换链表的节点_第2张图片

LeetCode141. 环形链表

题目链接:LeetCode141. 环形链表

题目概要

给定一个链表,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
如果链表中存在环,则返回 true 。 否则,返回 false 。

方法一:使用快慢指针

采用快慢指针,快指针每次走两步,慢指针每次走一步,如果快慢指针能相遇,则说明有环。

 public boolean hasCycle(ListNode head) {
    if (head == null || head.next == null) {
      return false;
    }
    ListNode quick = head.next.next, slow = head;
    while (quick != slow) {
      if (quick == null || quick.next == null) {
        return false;
      }
      quick = quick.next.next;
      slow = slow.next;
    }
    return true;
  }

虽然上面这段代码能通过LeetCode,但是逻辑有问题,应该将slow = head修改为slow = head.next。不然slow指针就起跑慢了。
算法刷题一:链表。反转链表,环形链表,交换链表的节点_第3张图片

方法二:暴力法

暴力法。循环Integer.MAX_VALUE / 30次,若能发现null,则说明无环,在给定次数内,都没发现null,则认为是有环的,,该方法不一定准确,但是能通过LeetCode。

 public boolean hasCycle(ListNode head) {
    ListNode cur = head;
    for (int i = 0; i < Integer.MAX_VALUE / 30; ++i) {
      if (cur == null || cur.next == null) {
        return false;
      }
      cur = cur.next;
    }
    return true;
  }

方法三:使用Set存储遍历过的节点

使用Set将遍历过的节点保存下来,每次走到新的节点时先查询Set中是否存在该节点,若存在,则说明有环

 public boolean hasCycle(ListNode head) {
    Set<ListNode> visited = new HashSet<>();
    ListNode cur = head;
    while (true) {
      if (cur == null) {
        return false;
      }
      if (visited.contains(cur)) {
        return true;
      }
      visited.add(cur);
      cur = cur.next;
    }
  }

LeetCode24.两两交换链表中的节点

题目链接:LeetCode24. 两两交换链表中的节点

题目概要

给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

方法一:使用递归

想到了反转链表中的递归解决,这个解法就很容易想到了
需要注意的是,当节点个数为奇数时,最后一个是不用交换的。节点个数为偶数时,两两相邻节点会被交换。
算法刷题一:链表。反转链表,环形链表,交换链表的节点_第4张图片

public ListNode swapPairs1(ListNode head) {
   if (head.next == null || head == null) {
     return head;
   }
   ListNode next = swapPairs1(head.next.next);
   ListNode pre = head.next;
   head.next = next;
   pre.next = head;
   return pre;
 }

如果对这段代码不是很理解,可以结合下图来辅助理解,整个过程是从右到左的。
算法刷题一:链表。反转链表,环形链表,交换链表的节点_第5张图片

方法二:使用指针进行迭代(从左至右)

使用递归的解法是从右到左的顺序来进行交换。使用指针从左到右来进行交换。

public ListNode swapPairs(ListNode head) {
    if (head == null || head.next == null) {
      return head;
    }
    ListNode cur = head;
    head = head.next;
    //这是一个工具节点,用来指示当前节点的上一个节点
    ListNode last = new ListNode(-1);
    while (cur != null && cur.next != null) {
      ListNode next = cur.next.next;
      ListNode prev = cur.next;

      prev.next = cur;
      cur.next = next;
      last.next = prev;
      
      last = cur;
      cur = next;
    }
    return head;
  }

对上面的代码不理解,可以参照下图便于理解
算法刷题一:链表。反转链表,环形链表,交换链表的节点_第6张图片

LeetCode142. 环形链表 II

题目链接:LeetCode142. 环形链表 II
题目概要

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。
说明:不允许修改给定的链表。

这个题目和LeetCode141. 环形链表看起来差不多。区别在于一个仅需要判断有环没环,另一个则需要返回入环的第一个节点。

刚做这个题目时,第一时间想到了快慢指针,因此将LeetCode141. 环形链表的快慢指针的代码修改了下便进行提交,发现根本通过不了,这才想到了问题所在:快慢指针相遇的节点并不一定是入环的第一个节点

方法一:使用Set存储遍历过的节点

public ListNode detectCycle(ListNode head) {
   if (head == null || head.next == null) {
     return null;
   }
   Set<ListNode> visited = new HashSet<>();
   ListNode cur = head;
   while (cur != null) {
     if (visited.contains(cur)) {
       return cur;
     }
     visited.add(cur);
     cur = cur.next;
   }
   return null;
 }

方法二:使用快慢指针

快慢指针,可以在环的任一位置相遇。因此不能将快指针 = 慢指针的这个节点作为入环的第一个节点返回。
算法刷题一:链表。反转链表,环形链表,交换链表的节点_第7张图片

那么入环的第一个节点与快慢指针相遇的节点之间是否有关系呢?当然有!

任意时刻,快指针走过的距离都为慢指针的 2 倍。因此,我们有:
a+(n+1)b+nc=2(a+b)⟹a=c+(n−1)(b+c)。n代表的是快指针绕环的圈数。
当快指针和慢指针相遇时,我们再额外使用一个指针ptr,它指向链表头部;随后,它和 慢指针每次向前移动一个位置。最终,它们会在入环点相遇。为什么呢?
看这个等式:a=c+(n−1)(b+c)。当ptr指针从head遍历到入环的第一个节点时,ptr走过的距离为a。
那慢指针呢?根据等式,慢指针走过的距离为c + (n−1)(b+c)。n代表的是快指针绕环的圈数,肯定会大于等于1,而且一定是个正整数。b+c刚好就是整个环的距离(也就是环的周长)。慢指针走过了c这么一段距离以后来到了入环的第一个节点,还需要走(n−1)(b+c)这么长的距离,相当于是b还要绕n - 1圈。不管是绕几圈,b最终都会回到入环的第一个节点。而此时ptr也走过了a这么长的距离来到了入环的第一个节点,两个指针便相遇了。

详情可以查看官方题解:LeetCode官方题解

第一次写的代码,直接根据LeetCode141. 环形链表中的快慢指针改了改。
对比这两段代码,我仅仅新增了蓝色方框的这部分代码。
看起来好像没什么错误,一提交就发现过不了,显示超时。为什么呢?问题出在红色方框的这部分。
quick = head.next.next的时候,slow应该为head.next。即快指针走两步时,慢指针应该走一步。但是图中的代码却是slow = head。明显slow起跑晚了。
算法刷题一:链表。反转链表,环形链表,交换链表的节点_第8张图片
这会导致什么问题嗯?左边这段代码是LeetCode141. 环形链表中的,这代码能在LeetCode中通过,尽管这是一段错误的代码。
为什么呢?

在仅仅需要判断是否有环的时候,尽管slow指针的起步慢了。但是slow指针终究还是会进入环。一旦进入环,由于quick的移动速度是slow的两倍,相当于是在环形跑道上进行追逐,不管slow在这个环形跑道的任何位置,由于quick的速度是slow的两倍,quick一定会追上slow的。可以看一下下面这个图便于理解:

算法刷题一:链表。反转链表,环形链表,交换链表的节点_第9张图片

但是,如果继续保持这样的代码,在本题就不适用了。

看下图。如果初始是slow = head,那么最终slowquick会相遇在2这个节点。这个时候,ptrslow是永远也不会相遇的,那就更别提在2这个节点相遇。

如果初始是slow = head.next,那么最终slowquick会相遇在-4这个节点,这个时候ptrslow会在2这个节点相遇。
算法刷题一:链表。反转链表,环形链表,交换链表的节点_第10张图片
最终通过的代码如下:

public boolean hasCycle(ListNode head) {
  if (head == null || head.next == null) {
    return false;
  }
  ListNode quick = head.next.next, slow = head.next;
  while (quick != slow) {
    if (quick == null || quick.next == null) {
      return false;
    }
    quick = quick.next.next;
    slow = slow.next;
  }
  return true;
}

LeetCode25. K 个一组翻转链表

题目链接:LeetCode25. K 个一组翻转链表
题目概要

给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
示例:
给你这个链表:1->2->3->4->5
当 k = 2 时,应当返回: 2->1->4->3->5
当 k = 3 时,应当返回: 3->2->1->4->5

方法一:使用递归

前面的链表题目用来很多递归的解法,写着写着手就顺了。
这个题目就是LeetCode24. 两两交换链表中的节点的困难版。在两两交换链表中的节点中我们只用考虑两个节点之间的交换,而在这里面我们要考虑K个节点的交换,因此还要结合一下LeetCode206. 反转链表 中的内容。
我们要将这K个节点看作一个整体,每次移动K个节点进行交换。根据递归,首先移动都最后的节点,如果最后的节点数不满K,就直接返回这一串链表,不需要对这些链表翻转。
还有一种情况,就是当K=1的时候,其实也不需要进行翻转,保持原样即可。

在递归的程序中,我是先判断从当前的head节点开始(包括head节点)是否有K个节点,如果节点数目根本不够K个,则不需要对其做任何操作,直接返回head。返回的这个节点被作为上一组节点的next节点。

如果从当前的head结点开始计数,发现链表中节点的数目满足K个,则反转从head节点开始(包括head节点)的K个节点。反转的代码其实和LeetCode206. 反转链表中的递归代码类似。
算法刷题一:链表。反转链表,环形链表,交换链表的节点_第11张图片

/**
  * k个一组翻转链表,如果k==1,则不发生翻转
  * 

* 给定链表:1->2->3->4->5 * 当 k = 2 时,应当返回: 2->1->4->3->5 * 当 k = 3 时,应当返回: 3->2->1->4->5 * * @param head * @param k * @return */ public ListNode reverseKGroup(ListNode head, int k) { //k = 1,时,不发生翻转,head == null说明是走到了末尾或者是head为null if (head == null || k == 1) { return head; } ListNode next; ListNode kthNodeFromHead = getKthNodeFromHead(head, k); if (kthNodeFromHead != null) { next = reverseKGroup(kthNodeFromHead.next, k); } else { return head; } //对当前的k个元素进行翻转,翻转以后,最后一个元素变为第一个元素,第一个元素变为最后一个元素 ListNode last = reverseKNode(head, kthNodeFromHead); last.next = next; return kthNodeFromHead; } /** * 获得从当前的head节点开始的第K个节点,比如head -> 1 -> 2 -> 3 -> 4获得k = 3的节点, * 即返回值为3的这个节点 * * @param head * @param k * @return 如果能得到第K个节点,则返回该节点,否则返回Null */ public ListNode getKthNodeFromHead(ListNode head, int k) { int count = 1; ListNode cur = head; while (cur != null) { cur = cur.next; count++; if (count == k) { return cur; } } return null; } /** * 返回翻转以后的链表的最后一个节点,比如翻转[head -> 1 -> 2 -> 3 ] * 翻转以后变为[3 -> 2 -> 1],返回1这个节点 * @param head * @param end * @return */ public ListNode reverseKNode(ListNode head, ListNode end) { if (head == end) { return head; } ListNode prev = reverseKNode(head.next, end); prev.next = head; return head; }

方法二:使用迭代

复用了方法一的一些代码。

 public ListNode reverseKGroup(ListNode head, int k) {
   if (head == null || k == 1 || getKthNodeFromCur(head, k) == null) {
     return head;
   }
   ListNode prev = new ListNode(-1), cur = head;
   head = getKthNodeFromCur(head, k);
   while (getKthNodeFromCur(cur, k) != null) {
     ListNode kthNodeFromCur = getKthNodeFromCur(cur, k);
     //存储kthNodeFromCur的下一个节点
     ListNode next = kthNodeFromCur.next;
     //翻转以后kthNodeFromCur变为这个区间内链表的头节点,reverseKNode返回这个区间的最后一个节点
     ListNode last = reverseKNode(cur, kthNodeFromCur);
     prev.next = kthNodeFromCur;
     prev = last;
     cur = next;
   }
   prev.next = cur;
   return head;
 }

 public ListNode getKthNodeFromCur(ListNode head, int k) {
   int count = 1;
   ListNode cur = head;
   while (cur != null) {
     cur = cur.next;
     count++;
     if (count == k) {
       return cur;
     }
   }
   return null;
 }

 public ListNode reverseKNode(ListNode head, ListNode end) {
   if (head == end) {
     return head;
   }
   ListNode prev = reverseKNode(head.next, end);
   prev.next = head;
   return head;
 }

你可能感兴趣的:(算法,链表,数据结构,算法,leetcode)