题目
题解
解法一: 还是跟上道题差不多,可以使用数组(可变数组),把链表的节点的值存入数组,然后使用双指针判断,一个从左到中间,另一个从右到中间。
public static boolean isPalindrome(ListNode head) {
ArrayList<Integer> list = new ArrayList<>();
// 把链表的值复制到数组中
ListNode cur = head;
while(cur != null) {
// O(n)
list.add(cur.val);
cur = cur.next;
}
// 双指针法判断回文
int first = 0;
int last = list.size() - 1;
while(first < last) {
// O(n/2)
// 这里犯了一个错误,我用了!=来比较,比较的是地址,这涉及到Integer底层,
// 对于Integer,会先创建一个池,里面存储-128到127的值,如果值在该范围中,那么就从该池直接拿取,
// 所以如果创建两个值相同并且在该范围的话,那么它们的地址是一样的。
// 所以下面得使用equal比较,因为比较的是内容
if(!list.get(first).equals(list.get(last))) {
return false;
}
first++;
last--;
}
return true;
}
复杂度:
-
时间复杂度:每次访问需要O(1),一个需要访问N个元素,所以时间复杂度为O(N)
-
空间复杂度:使用了一个可变数组来存储,一共有N个元素,虽然ArrayList有扩容机制(每次扩容为原来长度的1.5倍)但是可以不计,所以空间复杂度为O(N)。
解法二: 还可以使用栈,将链表的每一个节点压入栈,弹出,形成新链表,然后跟旧链表的每一个节点的值一一比较。
复杂度同上。
解法三: 在第一种解法中,使用了N个空间,其实还可以优化,前面学到的反转链表,但是反转整条链表,结果会错误,原因是会改变了原来的链表,所以就比较就会出错,除非创建一条链表的副本,然后再反转再比较。但是可以这样:反转后半段链表,然后跟前半段比较,最后最好把反转的链表接回去。算法实现步骤:
- 步骤一:先找出前半段链表的尾节点
- 实现一:计算节点个数,然后根据节点个数遍历到中间节点。(需要遍历2次)
- 实现二:使用快慢指针寻找中间节点,慢指针每次走一步,快指针每次走两步,只要当快指针的next遇到null并且快指针的next的next遇到null就表示找到。(需要遍历1次)
- 步骤二:然后就可以反转后半段的链表。
- 步骤三:前后半段一一比较。
- 步骤四:结束时把后半段链表反转回去。虽然不反转不造成影响。
public static boolean isPalindrome(ListNode head) {
if(head == null) {
return true;
}
// 1.前半段的尾节点
ListNode firstHalfEndNode = endOfFirstHalf(head);
// 2.从前半段的尾节点的下一个节点开始反转链表
ListNode secondHalfStart = reverseLink(firstHalfEndNode.next);
// 3. 比较
ListNode firstHalfNode = head;
ListNode secondHalfNode = secondHalfStart;
boolean result = true; // 本来不需要这个,可以直接在循环中直接返回,但是最后还得把后半段链表反转回来
while(result && secondHalfNode != null) {
// 刚开始用 firstHalf != firstHalfEndNode 出错了,原因是在这个位置还得比较,画画图就懂
if(firstHalfNode.val != secondHalfNode.val) {
result = false;
}
firstHalfNode = firstHalfNode.next;
secondHalfNode = secondHalfNode.next;
}
// 4.反转回来
firstHalfEndNode.next = reverseLink(secondHalfStart);
return result;
}
private static ListNode reverseLink(ListNode secondHalfStart) {
ListNode cur = secondHalfStart;
ListNode tempNode = null;
ListNode newNode = null;
while(cur != null) {
// 先把cur的next暂存
tempNode = cur.next;
// 然后把cur的next指向新链表
cur.next = newNode;
// 在新链表中把头指针指向cur
newNode = cur;
// 归还
cur = tempNode;
}
return newNode;
}
private static ListNode endOfFirstHalf(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while(fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
图解:(如果动态图太快,就截图看)
复杂度:
- 时间复杂度:O(N):一共需要遍历N个节点。
- 空间复杂度:O(1):使用的是常数时间的额外空间,都记为O(1)。
这种算法的缺点是,在并发环境下,函数运行时需要锁定其他线程或进程对链表的访问,因为在函数执执行过程中链表暂时断开。