一、链表
链表是由一组在内存中不必相连(不必相连:可以连续也可以不连续)的内存结构Node,按特定的顺序链接在一起的抽象数据类型。
我们常见的链表结构有单链表和双向链表。
单链表,保存了下一个结点的指针,可以根据下一结点指针向下遍历
双向链表,保存了向上一个结点和向下一个结点的指针,所以可以向上下两个方向遍历。
单链表结构一般如下:
template<typename T>
struct Node
{
T data; //数据
Node* next; //指向下一个结点的指针
};
双向链表的结构一般如下:
template<typename T>
struct Node
{
T data; //数据
Node* previous; //指向上一个结点的指针
Node* next; //指向下一个结点的指针
};
在算法中,我们遇到的链表题目一般可以使用一些经典的思路求解。本篇博客主要讨论这些经典的链表解题思路之一,快慢指针。
二、快慢指针
快慢指针也称龟兔赛跑算法,又叫判圈算法。具体方法是声明两个指针fast 指针和slow 指针,这两个指针按照题目的需求有不同的行进方案(例如fast每次行进2步,slow每次行进1步),但基本上步长是不同的,所以叫龟兔赛跑算法。
比较经典的题目如下,判断链表是否有环,所以又叫判圈算法。
【题目】环形链表
判断一个单链表是否有环,并且返回第一个入环节点。
一个单链表有环,即如下形式,图中第一个入环节点为2

<算法简述>
使用快慢指针求解这个问题。我们使fast和slow同时从头结点出发,同频行进。
其中fast每次走2个结点,slow每次走一个结点。
如果fast或 slow能够为nullptr,则链表无环,如果fast和slow相遇,则链表有环。
这里很容易比较得出快慢指针的一个特点1:可以判圈
使slow指针在原地(相遇位置)保持不动,并使fast指针返回头节点处,二者同频出发,每次均走一步。则再次相遇的节点为第一个入环节点。
<C++代码实现>
ListNode* hasCycle(ListNode* head)
{
ListNode* fast = head;
ListNode* slow = head;
//判断是否有环
while (nullptr != fast && nullptr != slow)
{
if (nullptr == fast->next) return nullptr;
fast = fast->next->next;
slow = slow->next;
//无环,返回空指针
if (nullptr == fast || nullptr == slow)
{
return nullptr;
}
if (fast == slow)
{
break;
}
}
//快指针回到头结点,快慢指针每次均走一步,相遇节点为第一个入环节点
fast = head;
while (nullptr != slow)
{
if (fast == slow)
{
break;
}
fast = fast->next;
slow = slow->next;
}
return slow;
}
<复杂度分析>
时间复杂度为O(N),额外空间复杂度为O(1)。
【题目】判断一个单链表是否是回文结构
回文结构:正向遍历和反向遍历每个结点都是相同数据的结构。例如1->2->1,1->2->2->1都是回文结构。
【方法一】使用栈结构(对照组)
想到判断回文结构 ,我们可以想到栈结构,因为栈是符合先入后出的数据结构,所以只要我们先遍历一遍链表,对数据进行压栈。再依次弹出栈顶,就会得到链表的反向输出。这样可以同时取到原链表的正向输出和反向输出,一一比较之后,就可以判断是否回文。
<C++代码实现>
bool isPalindrome(Node* hd)
{
stack<int> st;
Node* temp = hd;
while (nullptr != temp)
{
st.push(temp->data);
temp = temp->next;
}
temp = hd;
while (nullptr != temp)
{
if (temp ->data != st.top())
{
return false;
}
temp = temp->next;
st.pop();
}
return true;
}
<复杂度分析>
上述额外使用栈结构,所以额外空间复杂度为O(N),时间复杂度为O(N)。
【方法二】快慢指针方法
分析:这个题目一般想不到使用快慢指针方法,但是快慢指针具有一个和此题吻合的特点2:利用快慢指针的不同步数,可以找到链表的中点。如果此题找到中点,我们也可以镜像地比较中点左右的元素,得出是否是回文结构的结论。
而使用下列方法,可以使额外空间复杂度为O(1),这就是这个算法的优势。
bool isPalindrome(Node* hd)
{
if (nullptr == hd)
{
return true;
}
Node* fast = hd->next;
Node* slow = hd;
//快慢指针同时遍历,快指针每次走2步,慢指针每次走1步
//当快指针到终点的时候,慢指针正好处于中点位置
while (nullptr != fast && nullptr != fast->next)
{
fast = fast->next->next;
slow = slow->next;
}
//慢指针继续往下遍历,遍历的同时将指向反向
Node *temp1 = slow; //temp1用于记录slow的前一个结点,用于逆序链表的后半部分
Node *temp2 = slow->next; //temp1用于记录slow的后一个结点,放在链表断开后指针丢失
Node *mid = slow; //保存中点结点,便于后续使用
while (nullptr != slow)
{
slow->next = temp1;
temp1 = slow;
slow = temp2;
if (nullptr != slow)
{
temp2 = slow->next;
}
}
//此时temp1为原链表终点结点,使temp2指向头结点,同时遍历temp1,temp2比较数值
Node *ed = temp1; //保存终点结点,便于后续还原链表
temp2 = hd;
bool ret = true; //是否是回文结构
while (temp1 != mid)
{
if (temp1->data != temp2->data)
{
ret = false;
break;
}
temp1 = temp1->next;
temp2 = temp2->next;
}
//从终点开始,还原原链表
temp1 = ed->next;
ed->next = nullptr;
while (mid != ed)
{
temp2 = ed;
ed = temp1;
temp1 = ed->next;
ed->next = temp2;
}
return ret;
}
<复杂度分析>
可见此算法的coding复杂很多,但是额外空间复杂度可以做到O(1),时间复杂度为O(N)。
【题目】删除链表的倒数第 N 个结点
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
<分析>
此题的重点难点是:如何找到倒数第N个结点。
做完上面的判断回文结构的题,我们认识到快慢指针的特点2,应该可以很容易想到:我们使fast先走N个结点,再让slow和fast同频同步行进,当fast为最后一个结点的时候,slow所在结点即为倒数第N个结点。
<C++代码实现>
ListNode* removeNthFromEnd(ListNode* head, int n)
{
ListNode* fast = head;
ListNode* slow = head;
//fast指针先走n步
while (nullptr != fast && --n > 0)
{
fast = fast->next;
}
//如果链表长度小于n,则直接返回头指针
if (nullptr == fast)
{
return head;
}
//如果倒数第n个就是头节点,则换头节点
if (nullptr == fast->next)
{
head = head->next;
delete slow;
return head;
}
//fast和slow都前进,fast走到倒数第一个,则删除slow->next节点
while (nullptr != fast->next->next)
{
slow = slow->next;
fast = fast->next;
}
fast = slow->next;
slow->next = slow->next->next;
delete fast;
return head;
}
三、总结
从上面的题目中,总结下快慢指针的一般特点
1、可以判断链表是否有环,并得出入环节点;
2、可以定位链表中的某个指定位置的结点;
后续在求解问题的过程中,如果还发现其他妙用,博主会持续更新。

653

被折叠的 条评论
为什么被折叠?



