大家好,都吃晚饭了吗?我是Kaiqisan,是一个已经走出社恐的一般生徒,今天再更新算法题目,(注,本题来自左神算法,力扣也有该题)
https://leetcode-cn.com/problems/palindrome-linked-list/
问题
请判断一个链表是否为回文链表。
条件:一串链表是否在数据上左右对称
1->2->2->3->2->2->1 // 这是一个回文链表
1->2->3->3->2->2->1 // 这不是一个回文链表
思路1
使用额外数组进行判断(也是最低级的方法)
static boolean doCompare1(LinkList head) {
ArrayList<Integer> list = new ArrayList<>();
while (head != null) {
list.add(head.val);
head = head.next;
}
for (int i = 0; i < list.size() / 2; i++) {
if (list.get(i) != list.get(list.size() - 1 - i)) {
return false;
}
}
return true;
}
思路2
使用栈和快慢指针进行比对
在快指针到达终点前使用栈收录慢指针遍历到的点
然后慢指针接着走,同时让栈中元素出栈,把出栈元素和慢指针遍历到的元素进行比较,如果有不同的就返回false
static boolean doCompare2(LinkList head) {
int size = 0;
LinkList temp = head;
LinkList quickHead = head;
while (head != null) {
head = head.next;
size++;
}
Stack s = new Stack(size);
head = temp;
while (quickHead != null && head != null) {
if (quickHead.next == null) { // 链表有单数个元素
break;
}
if (quickHead.next.next == null) { // 链表有双数个元素
s.push(head.val);
break;
}
s.push(head.val);
quickHead = quickHead.next.next;
head = head.next;
}
head = head.next;
while (head != null) {
if (s.pop() != head.val) {
return false;
}
head = head.next;
}
return true;
}
代码执行过程
demo 1->2->3->2->1
快指针遍历 1 3 1
慢指针遍历 1 2 3
快指针结束后栈中元素 (从先到后顺序) 1 2 (这个链表成员数量为单数个,第三个元素没有统计,会跳过)
然后后面的比对是从链表第四个元素开始遍历顺序为 2 1
二次遍历同时出栈,出栈顺序为 2 1
所以判断这个demo是一个回文链表
demo 1->2->3->3->2->1
快指针遍历 1 3 2
慢指针遍历 1 2 3
快指针结束后栈中元素 (从先到后顺序) 1 2 3 (这个链表成员数量为双数个,为第三(n /2)个元素会统计,不会跳过)
然后后面的比对是从链表第四个元素开始遍历顺序为 3 2 1
二次遍历同时出栈,出栈顺序为 3 2 1
所以判断这个demo是一个回文链表
然后是这个链表的单双数个成员的判断可以通过快指针遍历到的最后一个元素的位置判断。
通过这个图,我们发现单数个成员的链表通过快指针遍历到的最后一个成员的下一个元素就是null
对应上面代码
if (quickHead.next == null) { // 链表有单数个元素
break;
}
同理,双数个成员的链表通过快指针遍历到的最后一个成员的下一个元素的下一个元素就是null
对应上面代码
if (quickHead.next.next == null) { // 链表有双数个元素
s.push(head.val);
break;
}
思路三
利用快慢指针,然后翻转后半段链表的指针方向,这个方法消耗内存最小,也是最难理解的,只要懂了,以后翻转链表指针的操作都可以完成了!
static boolean doCompare3(LinkList head) {
LinkList _head = head;
LinkList quickHead = head;
// 快慢指针遍历,使慢指针停留在链表中间位置
while (quickHead.next != null && quickHead.next.next != null ) {
_head = _head.next;
quickHead = quickHead.next.next;
}
LinkList temp = _head.next;
// 本来可以重新拿回已经遍历结束的快指针,但为了便于大家理解,我们这里开辟新的空间
_head.next = null;
LinkList temp2 = null;
// 指针交换
while (temp != null) {
temp2 = temp.next;
temp.next = _head;
_head = temp;
temp = temp2;
}
// 利用上面的参数重新给地址,对多余内存进行利用
temp = _head; // 赋值链表尾部
temp2 = head; // 赋值链表头部
// 遍历判断
while (temp != null && temp2 != null) {
if (temp.val != temp2.val) {
return false;
}
temp = temp.next;
temp2 = temp2.next;
}
return true;
}
一张图概括上面的思路(指针交换部分可以自己一步一步来理解)
这个过程不检查链表成员是否为单双数,因为单双数并不影响最后一步的判断操作
PS:当头尾指针的其中一个指向为null的时候,即遍历完成的时候,就会停止遍历,当成员数量为单数的时候,会同时完成遍历,当成员数量为双数的时候,头指针就会先完成遍历,这个时候头尾指针刚刚好把所有的元素都遍历完成,判断无差错地完成
总结
引用左神的那一句话,笔试的时候你想怎么写就怎么写,结果对就可以了,如果是面试的时候,这是需要你展示你思维的时候,你需要去取悦面试官(不要用你的屁股去取悦),链表这里本来就很难在时间复杂度进行优化(T(n)),所以我们要从空间复杂度下手,这个空间复杂度是越小越好,这样才能体现你的实力是优于其他面试者的,这样人家才会要你。