BM7.链表中的入口结点
解法1(快慢指针)
- 我们知道,用快慢指针可以很容易判断一条链表是否存在环,快指针fast每次走两步,慢指针slow每次走一步,那么若进入环中,每次他们之间的相对距离都会-1,直到两者相遇。虽然这能很快的知道是否存在环,但是它能否帮我们找到环的入口呢,答案是肯定的
- 假设从头节点到环的入口节点的前一个节点一共有a个,环中的节点有b个,设fast指针走过的节点数是f,slow指针走过的节点数是s,那么有以下两个结论:
- f = 2 * s (即快指针走过的节点数一定是慢指针的两倍)
- f = s + nb (当两者相遇时,快指针一定已经绕环走了n圈)
由上面两个等式可以得出,f = 2nb
,s = nb
故可知,两指针相遇时,慢指针已经走了nb步,已知我们要走到入口节点,需要走a + kb步,而这时s = nb只要再走a即可到达入口,我们把快指针移动到头节点,然后两个指针一步一步往后走,当它们相遇时所处的位置就是入口节点。
class Solution {
public:
ListNode* EntryNodeOfLoop(ListNode* pHead) {
ListNode* fast = pHead;
ListNode* slow = pHead;
while(fast->next != nullptr && fast->next->next != nullptr){
slow = slow->next;
fast = fast->next->next;
if(slow == fast){
ListNode* index1 = fast;
ListNode* index2 = pHead;
while(index1 != index2){
index1 = index1->next;
index2 = index2->next;
}
return index1;
}
}
return nullptr;
}
};
解法2(哈希集合)
一个直观的想法就是用哈希表存下我们从链表头往下走路径所见过的节点指针,当出现已经记录过的节点时,这个节点就是环的入口节点。
#include <unordered_set>
class Solution {
public:
ListNode* EntryNodeOfLoop(ListNode* pHead) {
unordered_set<ListNode*> st;
while(pHead){
if(st.count(pHead)) return pHead;
st.insert(pHead);
pHead = pHead->next;
}
return nullptr;
}
};
BM8.链表中倒数最后第k个结点
方法一:快慢双指针(推荐使用)
知识点:双指针
双指针指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个指针(特殊情况甚至可以多个),两个指针或是同方向访问两个链表、或是同方向访问一个链表(快慢指针)、或是相反方向扫描(对撞指针),从而达到我们需要的目的。
思路:
我们无法逆序遍历链表,就很难得到链表的倒数第�k个元素,那我们可以试试反过来考虑,如果当前我们处于倒数第�k的位置上,即距离链表尾的距离是�k,那我们假设双指针指向这两个位置,二者同步向前移动,当前面个指针到了链表头的时候,两个指针之间的距离还是�k。虽然我们没有办法让指针逆向移动,但是我们刚刚这个思路却可以正向实施。
具体做法:
- step 1:准备一个快指针,从链表头开始,在链表上先走�k步。
- step 2:准备慢指针指向原始链表头,代表当前元素,则慢指针与快指针之间的距离一直都是�k。
- step 3:快慢指针同步移动,当快指针到达链表尾部的时候,慢指针正好到了倒数�k个元素的位置。
#include <cmath>
class Solution {
public:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param pHead ListNode类
* @param k int整型
* @return ListNode类
*/
ListNode* FindKthToTail(ListNode* pHead, int k) {
ListNode* index1 = pHead;
ListNode* index2 = pHead;
if(k < 0) return nullptr;
while(k--){
if(index2 != nullptr)
index2 = index2->next;
else
return nullptr;
}
while(index2 != nullptr){
index1 = index1->next;
index2 = index2->next;
}
return index1;
}
};
方法二:先找长度再找最后k(扩展思路)
思路:
链表不能逆向遍历,也不能直接访问。但是对于倒数第�k个位置,我们只需要知道是正数多少位还是可以直接遍历得到的。
具体做法:
- step 1:可以先遍历一次链表找到链表的长度。
- step 2:然后比较链表长度是否比�k小,如果比�k小返回一个空节点。
- step 3:如果链表足够长,则我们从头节点往后遍历�−�n−k次即可找到所求。
class Solution {
public:
ListNode* FindKthToTail(ListNode* pHead, int k) {
int n = 0;
ListNode* p = pHead;
//统计链表长度
while(p != NULL){
n++;
p = p->next;
}
//长度过小,返回空链表
if(n < k)
return NULL;
p = pHead;
//遍历n-k次
for(int i = 0; i < n - k; i++)
p = p->next;
return p;
}
};
收获总结:
1.新学习了哈希解法和扩展思路,可以记下以后遇到相似题型也可使用
2.链表题一般对链表的基本操作和双指针考察较多
3.注意while和for的使用,有时候while不好判断可以改用for。
BM9.删除链表的倒数第n个节点
方法一:双指针(推荐使用)
双指针指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个指针(特殊情况甚至可以多个),两个指针或是同方向访问两个链表、或是同方向访问一个链表(快慢指针)、或是相反方向扫描(对撞指针),从而达到我们需要的目的。
思路: 我们无法逆序遍历链表,就很难得到链表的倒数第�n个元素,那我们可以试试反过来考虑,如果当前我们处于倒数第�n的位置上,即距离链表尾的距离是�n,那我们假设双指针指向这两个位置,二者同步向前移动,当前面个指针到了链表头的时候,两个指针之间的距离还是�n。虽然我们没有办法让指针逆向移动,但是我们刚刚这个思路却可以正向实施。
具体做法:
- step 1:给链表添加一个表头,处理删掉第一个元素时比较方便。
- step 2:准备一个快指针,在链表上先走�n步。
- step 3:准备慢指针指向原始链表头,代表当前元素,前序节点指向添加的表头,这样两个指针之间相距就是一直都是�n。
- step 4:快慢指针同步移动,当快指针到达链表尾部的时候,慢指针正好到了倒数�n个元素的位置。
- step 5:最后将该节点前序节点的指针指向该节点后一个节点,删掉这个节点。
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* ListNode(int x) : val(x), next(nullptr) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
// write code here
ListNode* dummy = new ListNode(0);
dummy->next = head;
ListNode* fast = dummy;
ListNode* slow = dummy;
while(n-- && fast != nullptr){
fast = fast->next;
}
fast = fast->next;
while(fast != nullptr){
slow = slow->next;
fast = fast->next;
}
slow->next = slow->next->next;
return dummy->next;
}
};
方法二:长度统计法(思路扩展)
思路:
既然要删掉倒数第n个元素,那肯定得先找到这个元素,试想一下,如果是数组我们怎么找这个元素的?肯定是先统计数组长度�L,然后根据�−�L−n得到下标,再根据下标访问。
但是很可惜,链表无法直接统计长度,也无法根据下标直接访问,我们能做的就是遍历链表。既然如此,那就两次遍历,第一次统计长度:
1 2 |
|
第二次找到倒数第n个位置:
1 |
|
具体做法:
- step 1:给链表添加一个表头,处理删掉第一个元素时比较方便。
- step 2:遍历整个链表,统计链表长度。
- step 3:准备两个指针,一个指向原始链表头,表示当前节点,一个指向我们添加的表头,表示前序节点。两个指针同步遍历�−�L−n次找到倒数第�n个位置。
- step 4:前序指针的next指向当前节点的next,代表越过当前节点连接,即删去该节点。
- step 5:返回去掉添加的表头。
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
//记录链表长度
int length = 0;
//添加表头
ListNode* res = new ListNode(-1);
res->next = head;
//当前节点
ListNode* cur = head;
//前序节点
ListNode* pre = res;
//找到链表长度
while(cur != NULL){
length++;
cur = cur->next;
}
//回到头部
cur = head;
//从头遍历找到倒数第n个位置
for(int i = 0; i < length - n; i++){
pre = cur;
cur = cur->next;
}
//删去倒数第n个节点
pre->next = cur->next;
//返回去掉头节点
return res->next;
}
};
收获总结:
1.链表题为不需要特殊处理第一个头节点,可设置虚拟头节点,统计操作,返回dummy->next。
BM10.两个链表的第一个公共结点
方法一:双指针 求链表长度再相减
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
class Solution {
public:
ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
int index1 = 0, index2 = 0;
ListNode* one = pHead1;
ListNode* two = pHead2;
ListNode* slow = pHead1;
ListNode* fast = pHead2;
while(one){
one = one->next;
index1++;
}
while(two){
two = two->next;
index2++;
}
if(index1 > index2)
{
slow = pHead2;
fast = pHead1;
}
int cha = (index1 <= index2) ? index2 - index1 : index1 - index2;
while(cha--){
fast = fast->next;
}
while(slow != fast){
slow = slow->next;
fast = fast->next;
}
return slow;
}
};
方法二:双指针 长度变成一样
这里先假设链表A
头结点与结点8
的长度 与 链表B
头结点与结点8
的长度相等,那么就可以用双指针。
- 初始化:指针
ta
指向链表A
头结点,指针tb
指向链表B
头结点 - 如果
ta == tb
, 说明找到了第一个公共的头结点,直接返回即可。 - 否则,
ta != tb
,则++ta,++tb
所以现在的问题就变成,如何让本来长度不相等的变为相等的?
假设链表A
长度为a
, 链表B
的长度为b
,此时a != b
但是,a+b == b+a
因此,可以让a+b作为链表A的新长度,b+a作为链表B的新长度。
class Solution {
public:
ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
ListNode *ta = pHead1, *tb = pHead2;
while (ta != tb) {
ta = ta ? ta->next : pHead2;
tb = tb ? tb->next : pHead1;
}
return ta;
}
};
收获总结:
1.方法二很妙,一种思路是相减求差,第二种方法直接利用遍历结点时为空则转接另一条链表,变相地统一长度一致。用两个指针扫描”两个链表“,最终两个指针到达 null 或者到达公共结点。