力扣 234. 回文链表
234. 回文链表
请判断一个链表是否为回文链表。
示例 1:
输入: 1->2 输出: false
示例 2:
输入: 1->2->2->1 输出: true
进阶:
你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
思路一:
借助一个列表,先遍历链表存下所有结点,然后第二次遍历链表比较列表中的结点
1 class Solution { 2 3 public boolean isPalindrome(ListNode head) { 4 ArrayList<ListNode> list = new ArrayList<>(); 5 for(ListNode node = head; node != null; node = node.next){ 6 list.add(node); 7 } 8 int i = list.size()-1; 9 for(ListNode node = head; node != null && i >= 0; node = node.next, i--){ 10 if(node.val != list.get(i).val){ 11 return false; 12 } 13 } 14 return true; 15 } 16 }
力扣测试时间为2ms, 空间为43.2MB
复杂度分析:
时间复杂度:遍历了两次链表,所以时间复杂度为O(n)
空间复杂度:O(n)
思路二:
先使用快慢指针找到中间结点,如果链表长度为奇数,那slow所指结点刚好是中间结点,如果长度为偶数,那slow所指结点刚好是等分后的链表的后半段的第一个结点,
随后翻转slow所指的后半段链表,完成翻转后pre是后半段链表的头结点
同时遍历后半段链表和前半段链表,判断是否是回文链表
1 class Solution { 2 public boolean isPalindrome(ListNode head) { 3 if(head == null || head.next == null){ 4 return true; 5 } 6 // 使用快慢指针找到中间结点 7 ListNode fast = head, slow = head; 8 while(fast != null && fast.next != null){ 9 fast = fast.next.next; 10 slow = slow.next; 11 } 12 13 // 如果链表长度为奇数,那slow所指结点刚好是中间结点,如果长度为偶数,那slow所指结点刚好是等分后的链表的后半段的第一个结点 14 ListNode left = head; // 前半段链表 15 ListNode cur = slow, pre = null, next = null; // 最后pre所指的链表就是后半段链表 16 // 翻转slow所指的后半段链表 17 while(cur != null){ 18 next = cur.next; 19 cur.next = pre; 20 pre = cur; 21 cur = next; 22 } 23 24 // 同时遍历后半段链表和前半段链表,判断是否是回文链表 25 ListNode right = pre; 26 while(left != null && right != null){ // 后半段比前半段长度大于等于0 27 if(left.val != right.val){ 28 return false; 29 } 30 left = left.next; 31 right = right.next; 32 } 33 return true; 34 } 35 }
复杂度分析:
时间复杂度:O(n), 整体分成3部分,寻找中间结点的时间花费为O(n/2), 翻转后半段链表的时间花费也为O(n/2), 最后同时遍历左右链表的时间复杂的为O(n/2), 所以整体时间复杂度为O(n)
空间复杂度: 没有借助递归或者其他容器,所以空间复杂度为O(1)
改进版
因为我们对原链表的后半段进行了翻转,所以修改了原链表,所以其实得到结果后应该把链表回复成原样。
回复原样的过程其实也很简单,仔细观察,我们找到中间结点后,并没有断开前半段链表对后半段链表的连接,所以其实翻转后的链表是这样的,以1 -> 2->3->2->1为例,翻转后后的链表为1-2->3<-2<-1, 翻转后,pre指针指向了后半段链表的头结点,slow指针指向了中间结点,所以恢复链表,只需把pre 到 slow之间的结点再翻转一次即可。如果链表为1->2->3->3->2->1, 则翻转后的链表为 1->2->3->3<-2<-1. 此时slow指向了第二个3, pre指向了最后一个1,翻转pre到slow之间的结点即完成了链表的恢复工作。
根据上面标红的右半段链表,可以看出做半段链表其实总是大于等于右半段链表的长度的,所以我们在判断是否回文的时候,可以使用遍历右半段端链表的指针为空来作为结束循环的条件。
1 class Solution { 2 public boolean isPalindrome(ListNode head) { 3 if(head == null || head.next == null){ 4 return true; 5 } 6 // 使用快慢指针找到中间结点 7 ListNode fast = head, slow = head; 8 while(fast != null && fast.next != null){ 9 fast = fast.next.next; 10 slow = slow.next; 11 } 12 13 // 如果链表长度为奇数,那slow所指结点刚好是中间结点,如果长度为偶数,那slow所指结点刚好是等分后的链表的后半段的第一个结点 14 ListNode left = head; // 前半段链表 15 ListNode cur = slow, pre = null, next = null; // 最后pre所指的链表就是后半段链表 16 // 翻转slow所指的后半段链表 17 while(cur != null){ 18 next = cur.next; 19 cur.next = pre; 20 pre = cur; 21 cur = next; 22 } 23 24 // 同时遍历后半段链表和前半段链表,判断是否是回文链表 25 ListNode right = pre; 26 // 根据上面的推论,可知右半段链表其实长度小于等于做半段链表 27 while(right != null){ 28 if(left.val != right.val){ 29 return false; 30 } 31 left = left.next; 32 right = right.next; 33 } 34 35 // 恢复链表,将pre到slow之间的结点再翻转一次即可 36 cur = pre; 37 pre = null; 38 next = null; 39 while(cur != slow){ 40 next = cur.next; 41 cur.next = pre; 42 pre = cur; 43 cur = next; 44 } 45 return true; 46 } 47 }
复杂度分析:
时间复杂度:O(n), 整体分成3部分,寻找中间结点的时间花费为O(n/2), 翻转后半段链表的时间花费也为O(n/2), 最后同时遍历左右链表的时间复杂的为O(n/2), 恢复链表的时间复杂度为O(n/2). 所以整体时间复杂度为O(n)
空间复杂度: 没有借助递归或者其他容器,所以空间复杂度为O(1)