本文已参与「新人创作礼」活动,一起开启掘金创作之路。
从现在开始,我们要引进快慢指针的概念,它在做一些链表题目时非常方便。
1. 链表的中间结点
给定一个头结点为 head 的非空单链表,返回链表的中间结点。 如果有两个中间结点,则返回第二个中间结点。
示例 1: 输入:[1,2,3,4,5] 输出:此列表中的结点 3 (序列化形式:[3,4,5]) 返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。 注意,我们返回了一个 ListNode 类型的对象 ans,这样:ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.
示例 2: 输入:[1,2,3,4,5,6] 输出:此列表中的结点 4 (序列化形式:[4,5,6]) 由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。
提示: 给定链表的结点数介于 1 和 100 之间。
解题思路:
慢指针一次走一步,快指针一次走两步,这样的话当快指针走到终点时,慢指针刚好走到中间节点处。
第一种可能:单数个节点
第二种可能:双数个节点
struct ListNode* middleNode(struct ListNode* head){
struct ListNode* fast,*slow;
fast = slow = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
链接:
2. 链表中倒数第k个结点
输入一个链表,输出该链表中倒数第k个结点。
示例 输入: 1,{1,2,3,4,5} 返回值:{5}
解题思路:
让我们换一个角度看这道题,倒数第k个结点也可以看做正数第n-k个节点(n是链表的节点总数)。 这种思路就可以借助快慢指针轻松解决问题了。 首先让快指针走k步,之后让慢指针从起点开始走,快慢指针一起走,快指针走到终点时慢指针走到的地方就是链表中倒数第k个结点了。是不是很巧妙呀。 ==但要注意检查k的大小。==
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) {
// write code here
struct ListNode* fast,*slow;
fast = slow = pListHead;
while(k--)
{
if(fast ==NULL)//小心k过大
return NULL;
fast = fast->next;
}
while(fast)
{
fast = fast->next;
slow = slow->next;
}
return slow;
}
链接:
3. 链表的回文结构
描述: 对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。 给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。
测试样例: 1->2->2->1 返回:true
解题思路:
回文通俗点讲就是,从前往后读和从后往前读是一样的。 这道题有一个取巧的办法,题目中有讲“保证链表长度小于等于900”,由此想到能开一个数组把链表里的数据按顺序放到数组里,再判断数组元素是否是回文。但不建议使用这种解法,因为如果没有告诉链表最大长度,则不能用此解法。
class PalindromeList {
public:
bool chkPalindrome(ListNode* A) {
// write code here
int a[900] = {0};
ListNode* cur = A;
int n = 0;
//保存链表元素
while(cur)
{
a[n++] = cur->val;
cur = cur->next;
}
//判断数组是否为回文结构
int begin = 0, end = n-1;
while(begin < end)
{
if(a[begin] != a[end])
return false;
++begin;
--end;
}
return true;
}
};
还有一种更巧妙的,灵活性更强解法:将后半段链表反转,如果链表是回文结构,反转后的和前半段的数据是一一对应的。
你肯定在想要不要分情况讨论,但这种方法巧就巧在这里,以上图为例,1和2比较到时候没有任何问题,listA和listB的下一个节点是同一个3,那肯定一样啦。之后再一起走向NULL,循环终止。 这就是要讲的相交链表的雏形,很快就讲相交链表咯 程序我就直接用了之前写的找中点和反转的函数。
class PalindromeList {
struct ListNode* reverseList(struct ListNode* head)
{
struct ListNode* newhead = NULL, *cur = head, *next;
while(cur)
{
next = cur->next;
cur->next = newhead;
newhead = cur;
cur = next;
}
return newhead;
}
struct ListNode* middleNode(struct ListNode* head)
{
struct ListNode* fast,*slow;
fast = slow = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
public:
bool chkPalindrome(ListNode* A) {
// write code here
struct ListNode *mid = middleNode(A);
struct ListNode *listB = reverseList(mid);
while(A&&listB)
{
if(A->val == listB->val)
{
A = A->next;
listB = listB->next;
}
else return false;
}
return true;
}
};
链接:
在讲下一题之前我们要复习一下带头节点单链表和无头单链表的区别。顾名思义,带头结点的单链表有一个哨兵卫的头结点,但这个节点不存储有效数据。那么它有什么意义吗?它能够简化过程,让第一次插入和非第一次插入合并起来,详情请见上一篇下面这题我们就用这个结构。