链表
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。
来源:力扣(LeetCode)
这题思路很简单,实现也很简单
第一种:遍历
遍历链表取得长度size,然后在让指针从头,循环size-k次就好
class Solution {
public:
ListNode* getKthFromEnd(ListNode* head, int k) {
ListNode* p = head;
int size = 0;
while(p != nullptr)
{
size++;
p = p->next;
}
if(size < k) return nullptr;
p = head;
for(int i = 0;i < size - k;i++)
{
p = p->next;
}
return p;
}
};
花费2n - k;
第二种:快慢指针法
让快指针fast始终比慢指针slow快k步,当fast到达尾部,slow自然就在size - k处
class Solution {
public:
ListNode* getKthFromEnd(ListNode* head, int k) {
ListNode* fast = head , *slow = head;
int i = 0;
while(i != k && fast != nullptr)
{
fast = fast->next;
i++;
}
while(fast != nullptr)
{
fast = fast->next;
slow = slow->next;
}
return slow;
}
};
时间花费 n ;
迭代:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* cur = head , *pre = nullptr;
while(cur)
{
ListNode* next = cur->next;;
cur->next = pre;
pre = cur;
cur = next;
}
return pre;
}
};
递归
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(head == nullptr || head->next == nullptr)
return head;
ListNode* res = reverseList(head->next);
head->next->next = head;
head->next = nullptr
return head;
}
};
这一题虽然本质上还是反转链表,但是是局部反转,思路由下
代码:
class Solution {
public:
void reverselist(ListNode* head)//反转链表的程序
{
ListNode* curr = head , *prev = nullptr;
while(curr)
{
ListNode* next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
}
ListNode* reverseBetween(ListNode* head, int left, int right)
{
ListNode* newhead = new ListNode(0);//定义头结点
newhead->next = head;//让头结点指向链表
ListNode* rt , *lt , *R ,*L = newhead;
for(int i = 0;i<left-1;i++)//移动L到被反转区间之前
{
L = L->next;
}
lt = L->next;//让lt移动到反转区间的开头
rt = lt;
for(int i = 0;i<right-left;i++)//让rt移动到反转区间的末尾
{
rt = rt->next;
}
R = rt->next;//移动R到反转区间的后面
L->next = nullptr;//截断
rt->next = nullptr;
reverselist(lt);
L->next = rt;//链接
lt->next = R;
return newhead->next;
}
};
25. K 个一组翻转链表
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
进阶:
你可以设计一个只使用常数额外空间的算法来解决此问题吗?
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
来源:力扣(LeetCode)
思路是一样的,切断,反转,重连,不过会有些差别,因为上一题只要求切断一次,这题要切 lenght / k次
思路:
特殊情况:
特殊情况 1 :lenght 刚好被 k 整除 ,这种情况下我们用R来判断
特殊情况 2 :有剩余,用end来判断
代码:
class Solution {
public:
//这个反转链表的程序,只改了输入参数和返回值而已,其他没改
pair<ListNode*, ListNode*> reverse(ListNode*head, ListNode*end)
{
ListNode*cur = head, *pre = nullptr;
while(cur)
{
ListNode* next = cur->next;
cur->next = pre;
pre = cur;
cur = next;
}
return {end,head};
}
ListNode* reverseKGroup(ListNode* head, int k) {
ListNode* newhead = new ListNode(-1);
newhead->next = head;
ListNode* L = newhead, *R = head;
while(R)//解决特殊情况 1
{
ListNode* end = head;//让end定位到head
for(int i = 1;i<k;++i)
{//然后用循环让end到合适的位置
end = end->next;
//如果end为nullptr,就说明最后一段长度不够,只能返回了
if(!end) return newhead->next;//解决特殊情况 12
}
R = end->next;//移动R到end的后边
L->next = nullptr;//切断
end->next = nullptr;//同上
tie(head,end) = reverse(head,end);//反转并更新head和end
L->next = head;end->next = R;//接回去
head = R;//
L = end;//
}
return newhead->next;
}
};
从头到尾打印链表
用reverse反转数组
class Solution {
public:
vector<int> reversePrint(ListNode* head) {
vector<int> res;
ListNode* p = head;
while(p!=nullptr)
{
res.push_back(p->val);
p = p->next;
}
reserve(res.begin(),res.end());
return res;
}
};
朴素法(纯算法,不用辅助函数)
先遍历链表求得长度,然后重新分配数组的空间,当然也可以不用resize,可以直接vector< int > res(size , 0);
但是不能用vector::reserve,因为reserve不改变size,只是预分配空间而已,后面还需要用下标赋值,如果用reserve的画程序就会崩溃,所以直接初始化或者用resize;
改变size后就用下标从后面赋值就完事了;
class Solution {
public:
vector<int> reversePrint(ListNode* head) {
vector<int> res;
ListNode* p = head;
int size;
while(p!=nullptr)
{
size++;
p = p->next;
}
res.resize(size);
p = head;
for(;size>0;size--)
{
res[size-1] = p->val;
p = p->next;
}
return res;
}
};
改变链表结构
这种也很好理解,就是上面那题的做法,反转链表以后再按顺序添加元素进数组
class Solution {
public:
vector<int> reversePrint(ListNode* head) {
vector<int> res;
ListNode* before = head , after = nullptr;
while(before!=nullptr)
{
head = before->next;
before->next = after;
after = before;
before = head;
}
while(after != nullptr)
{
res.emplace_back(after->val);
after = after->next;
}
return res;
}
};
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
ListNode *res = new ListNode(0);//定义一个新的头结点
ListNode * p = res;
//如果有一个是空指针的话,在下面的值比较的过程程序就会崩溃
while(l1 && l1)
{
if(l1->val < l2->val)
{
p->next = l1;
p = p->next;
l1 = l1->next;
}
if(l2->val < l1->val)
{
p->next = l2;
p = p->next;
l2 = l2->next;
}
}
//这里的if就是为了填满最后一个空缺,看最后面的两个,谁被漏了
if(l1 != nullptr) p->next = l1;
else p->next = l2;
return res->next;
}
};
输入两个链表,找出它们的第一个公共节点。
思路是双指针,一个从A链开始,一个从B链开始,一直往前走,当指针A到达A链的末尾的时候,让指针A指向B的头结点,反之对指针B也是一样的操作
我一开始也是用的双指针,但是在遍历完成后没有交换到对面去,结果运行了400多毫秒
给我吓傻了😱,还是第一次看见运行这么久的题目,赶紧去看看大佬们的题解,才发现自己的问题,如果不交换,虽然也能出结果,但是这样两个指针相遇的问题就变成了求最小公倍数的问题了,交换以后呢,就变成了加法的问题,
假设链A不公共的结点数为N,链表B不公共的结点数为M,两者公共的结点数为X,交换以后,两个指针走过的步数都是N+M+K,明显比公倍数好多了,改进后结果为
可以看到,差距很明显
ps:过了半个月再回来做这题,居然做不出来了,按照记忆中的思路,发现会当量链表五交点时,会死循环,然后再次研究了一下,发现是条件设置的问题,两链表不相交的话,我们可以把它们当成再最后的nullptr处相交,反正走的路径一样长,有交点的话两者就会相交,没有的话,两者还是保持了一样的步伐,直到他们都为nullptr
所以
我们必须得让两个指针有能成为nullptr的机会,我们在while的条件里面比较两指针是否相同,所以,两指针必须能以nullptr的身份到达while循环的末尾,这样才能在下一次循环开始前判断两指针是否都为nullptr
具体写法:
if(A==nullptr) A = headB;
else A = A->next;
B指针也照葫芦画瓢
也可以这么写
if(A!=nullptr) A = A->next;
else A = headB;
反正就是两步都得有条件约束着,假如有一个没有,就会出错
比如下面的(错误)写法:
A = A->next;
if(A == nullptr) A = headB;
这么写的话,就算A指向nullptr,也会因为下面的if而指向headB,最后的结果就是死循环
代码:
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode *A = headA , *B = headB;
while(A != B)
{
A = A != nullptr? A->next : headB;//记得交换位置
B = B != nullptr? B->next : headA;
}
return A;
}
};
2. 两数相加
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
来源:力扣(LeetCode)
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
int carry = 0;
ListNode* res = new ListNode(0);//头结点
ListNode* p = res;
while(l1 || l2)//为了能让短的能够补长
{
//精髓所在,把短的链表后面补0,好与长的链表相加
int n1 = l1?l1->val:0;
int n2 = l2?l2->val:0;
sum = n1 + n1 + carry;
p->next = new ListNode(sum%10);//保留个位
p = p->next;
carry = sum/10;//保留进位
//防止无效地址
if(l1) l1 = l1->next;
if(l2) l2 = l2->next;
}
//最后一位别忘了
if(carry > 0) p->next = new ListNode(carry);
return res->next;
}
};
给定一个链表,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
如果链表中存在环,则返回 true 。 否则,返回 false 。
进阶:
你能用 O(1)(即,常量)内存解决此问题吗?
来源:力扣(LeetCode)
既然有进阶,那我们肯定要达到进阶的标准啦,要求空间使用度O(1)那就不能用哈希表了,老老实实快慢指针,快慢指针就怕越界,指向未知的内存,导致程序崩溃,因此得好好考虑一下,这里我用了两个判断,一个是while(fast),还有一个是while里面的 if (!fast) 有两个条件保护,就不会越界了
class Solution {
public:
bool hasCycle(ListNode *head) {
ListNode* slow = head , *fast = head;
while(fast)//第一层保护
{
fast = fast->next;
if(!fast) return false;//第二层
fast = fast->next;
slow = slow-<next;
if(slow == fast) return true;//找到了就直接返回
}
//如果跳出了循环,那只有可能是fast == nullptr的情况了,所以返回false
return false;
}
};
其实我早就看过快慢指针的原理,但一直没看过代码,也没写这题,这次是用自己的思路实现的,也没看大佬的代码,发现效率还挺高,挺高兴的哈哈
差不多的另一题:
142. 环形链表 II
这一题要返回环的第一个结点,如果没有环就返回null
这题思路也一样,用快慢指针判断是不是有环,然后呢,如果有环,那快慢指针肯定相遇了,这时候完美让另一个指针从头开始,与快(慢也一样)指针一起往后走,最终他们两就会相遇在环的开头
具体数学分析见 👉这里
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode*slow = head , *fast = head , *res = head;
while(fast)
{
fast = fast->next;
if(!fast) return nullptr;
fast = fast->next;
slow = slow->next;
if(slow == fast) break;//这里改成break;
}
//这里可能是break跳出的,也有可能是不满足while跳出的,
//所以if一下,看是不是有环链表
if(!fast) return nullptr;
while(res!=fast)
{
res = res->next;
fast = fast->next;
}
return res;
}
};
给定一个单链表 L:L0→L1→…→Ln-1→Ln ,
将其重新排列后变为: L0→Ln→L1→Ln-1→L2→Ln-2→…
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
示例 1:
给定链表 1->2->3->4, 重新排列为 1->4->2->3.
示例 2:
给定链表 1->2->3->4->5, 重新排列为 1->5->2->4->3.
来源:力扣(LeetCode)
思路:
class Solution {
public:
void reverselist(ListNode* head)//反转程序
{
ListNode* pre = nullptr , *cur = head;
while(cur)
{
ListNode*next = cur->next;
cur->next = pre;
pre = cur;
cur = next;
}
}
void reorderList(ListNode* head) {
int N = 0 , left = 0 , right = 0;
ListNode* size = head , *L = head , *rt = head , *lt = nullptr, *R = nullptr;
while(size->next)//获得长度,并且保留size在最后一位的地方
{
N++;
size = size->next;
}
N++;
if(N == 2 || N == 1) return ;//长度为1 或2的话就没必要反转
if(N%2!=0)//求得中点的范围,奇数,偶数的情况要分开处理
{
right = left = N/2+1;
}
else
{
left = N/2;
right = N/2+1;
}
while(left>2)//移动L指针到中点范围左边
{
L = L->next;
left--;
}
lt = L->next;//让lt指向中点开头
while(right>1)
{//让rt指向中点结尾
rt = rt->next;
right--;
}
R = rt->next;//让R指向要反转的子链表的开头
L->next = nullptr;
rt->next = nullptr;
reverselist(R);//反转
//这里pre2是反转后的链表的开头,size的存在就为了这里
ListNode* pre1 = head , *pre2 = size;
//这里的条件用啥都行,与,或,或者只用pre1和pre2也可以
while(pre1 && pre2)
{//合并
ListNode*cur1 = pre1->next , *cur2 = pre2->next;
pre1->next = pre2;
pre2->next = cur1;
pre1 = cur1;
pre2 = cur2;
}
ListNode* buf = head;
while(buf->next)
{//找到合并和的链表的尽头
buf = buf->next;
}
buf->next = lt;//链接
}
};
83. 删除排序链表中的重复元素
存在一个按升序排列的链表,给你这个链表的头节点 head ,请你删除所有重复的元素,使每个元素 只出现一次 。
返回同样按升序排列的结果链表。
思路很简单,和数组的一样,双指针(pre , cur)遍历,遇见两指针对应值数字不相同的话,就把pre连接搭配cur,如果相同, 就不管他,让cur继续往前走,直到遇到不重复的
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
ListNode* newhead = new ListNode(-10);
newhead->next = head;
ListNode* pre = newhead, *cur = pre->next;
while(cur)
{
if(pre->val != cur->val)
{
pre->next = cur;
pre = cur;
}
cur = cur->next;
}
//这一步比较特殊,如果最后的那几个数字重复了,
//没有这步的话,就起不到删除的作用了
pre->next = cur;
return newhead->next;
}
};
自己的常规思路,第一次遍历,用哈希表记录重复的的值,
第二次,如果值在哈希表里能找到,就跳过,如果找不到就让pre连接到cur
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
unordered_set<int> mset;
ListNode* newhead = new ListNode(-100);
newhead->next = head;
ListNode* pre = newhead, *cur = head, *buf = newhead;
while(buf->next)
{
if(buf->val == buf->next->val)
mset.insert(buf->val);//记录重复的值到哈希表里
buf = buf->next;
}
while(cur)
{
if(mset.find(cur->val) == mset.end())
{
pre->next = cur;
pre = cur;
}
cur = cur->next;
}
pre->next = cur;//这一步和上面那一题是一样的
return newhead->next;
}
插入排序大家都熟,链表版的思路也差不多,用start和end两个指针维护已排序的部分,然后cur指针每次从end后面开始判断该结点的值应该插入到什么地方,当然别忘了删除多余的cur
不过这里是链表,不用删除,改变指向就好
分三个情况:
1:cur的值小于start,这种情况就直接插在最前面
cur < start
2:cur的值大于等于end,插在最后面
cur >= end
3:cur的值大于或等于start,并小于end,就遍历已排序序列,找到合适的位置插入
start <= cur < end
注意,为了保持插入排序的稳定性,必须这样设置条件,其实其它的条件设置也是完全没问题的,只是不保证稳定性而已
代码:
class Solution {
public:
ListNode* insertionSortList(ListNode* head) {
if(!head || !head->next) return head;
ListNode* newhead = new ListNode(-1);
newhead->next = head;
ListNode* pend = head, *pstart = head;
ListNode* cur = pend->next;
while(cur)
{
if(cur->val < pstart->val)//小于start就插在最前面,并更新start
{
ListNode* temp = cur->next;
pend->next = temp;
newhead->next = cur;
cur->next = pstart;
pstart = cur;
}
else if(cur->val >= pend->val)//大于end的就不用管,因为本身cur就在end后面
//直接更新end的位置就好
{
pend = cur;
}
else
{//其余情况就要找位置插入了
pend->next = cur->next;
ListNode* temp = pstart;
while(temp->next->val<=cur->val)
{
temp = temp->next;
}
ListNode* temp2 = temp->next;
temp->next = cur;
cur->next = temp2;
}
cur = pend->next;
}
pend->next = nullptr;
return newhead->next;
}
};
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
先说说最容易想到的
合并两个有序链表很简单,如果我们不追求最高效率,那么写出这题也很简单,直接遍历数组中的链表,先将第一个和第二个合并,然后再将第三个合并进去…依次类推
代码:()
ListNode* mergeKLists(vector<ListNode*>& lists) {
if(lists.size() == 0) return nullptr;
ListNode* newhead = new ListNode(-1);
newhead->next = lists[0];
for(int i = 1;i<lists.size();++i)
{//让头结点始终指向被合并的新链表
newhead->next = Merge(lists[i], newhead->next);
}
return newhead->next;
}
这是合并两个链表的程序,需要在上面调用
ListNode* Merge(ListNode*head1, ListNode*head2)
{
if(!head1 || !head2) return head1?head1:head2;
ListNode* newhead = new ListNode(-1);
ListNode* cur = newhead, *p1 = head1, *p2 = head2;
while(head1 && head2)
{
p1 = head1->next;p2 = head2->next;
if(head1->val < head2->val)
{
cur->next = head1; head1 = p1;
}
else
{
cur->next = head2; head2 = p2;
}
cur = cur->next;
}
cur->next = head1?head1:head2;
return newhead->next;
}
然后就是效率较高的分而治之,类似归并排序,可以说是非常像,但是由于给出的是数组形式的链表,所以写起来怪怪的,但是总体思路和归并排序差不多
这里的注意一下,再递归的进行分割的时候,使用数组的下标,就像数组的归并排序里的分割一样
但是
合并的时候就不能用数组下标了!
为什么呢?想一想在数组的归并排序时,我们把排过序的数组重新填回原数组了,所以用下标合并时合并的就是两个有序的序列,但是在这个链表排序里,我们写的合并的程序没有这一步,所以合并的时候不能用数组下标了,但是我们合并的时候返回了新链表的结点,所以直接用这两个返回的结点继续进行合并就行,当然如果你不信邪,硬要在合并程序里填回原数组,应该也是可行的,但是没必要
代码:
class Solution {
ListNode* Merge(ListNode*head1, ListNode*head2)
{
if(!head1 || !head2) return head1?head1:head2;
ListNode* newhead = new ListNode(-1);
ListNode* cur = newhead, *p1 = head1, *p2 = head2;
while(head1 && head2){
p1 = head1->next;p2 = head2->next;
if(head1->val < head2->val){
cur->next = head1; head1 = p1;
}
else{
cur->next = head2; head2 = p2;
}
cur = cur->next;
}
cur->next = head1?head1:head2;
ListNode* res = newhead->next;
delete newhead;
return res;
}
ListNode* MSort(vector<ListNode*>& lists, int start, int end)
{
if(start == end) return lists[end];//如果要合并的链表数量为一,那就直接返回原链表
else if(start > end) return nullptr;
else
{
int mid = start + ((end-start)>>1);
//这里在合并时别用下标,别用下标,别用下标
return Merge(MSort(lists,start,mid), MSort(lists,mid+1,end));
}
}
public:
ListNode* mergeKLists(vector<ListNode*>& lists) {
if(lists.size() == 0) return nullptr;
ListNode* newhead = new ListNode(-1);
int start = 0, end = lists.size()-1;
newhead->next = MSort(lists, start,end);
ListNode*res = newhead->next;
delete newhead;
return res;
}
};
给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。
输入:head = [1,2,3,4,5], k = 2
输出:[4,5,1,2,3]
这题思路很简单,没啥难度:
找到链表的尾部并同时计算链表长度,然后就是改变头结点而已了,将头节点移动x位,
然后就有了一个以新头结点开始的环形链表,再以size为依据,找到尾部,断开环,就OK了
,唯一需要思考一下的地方就是x的计算,x是由size和k决定的,我们可以直到,旋转一次,其实就是将当前链表的为结点作为新的头结点而已,而且如果k大于了size,那么旋转k次和旋转k%size次没有区别
因为我们从头结点开始移动来确定新的头结点,于是得出
x = size-(k%size)
最后注意一下特殊情况:
k为0,链表长度为0或1
k为0直接返回head,长度为1也直接返回head,长度为0返回nullptr,但是因为此时head也是nullptr,所以三种情况就统一了,所以这三种情况可以直接在开头判断
x = size
一个环形链表,如果头结点移动size次的话,并不是到达末尾,而是返回原位,所以直接返回head,这个需要size,所以不能在开头判断
代码:
class Solution {
public:
ListNode* rotateRight(ListNode* head, int k) {
if (k == 0 || head == nullptr || head->next == nullptr) return head;//特殊情况
ListNode*cur = head; int size = 1;
while(cur->next)
{//找到末尾并求出size
cur = cur->next;
++size;
}
k = (size)-(k%size);
if(k == size)return head;//特殊情况
cur->next = head;//让链表成环
for(int i = 0;i<k;++i)
{//移动到新的头结点处
head = head->next;
}
ListNode*end = head;
for(int i = 1;i<size;++i)
{//找到当前的末尾
end = end->next;
}
end->next = nullptr;//断开环
return head;
}
};
86. 分隔链表
给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。
你应当 保留 两个分区中每个节点的初始相对位置。
来源:力扣(LeetCode)
输入:head = [1,4,3,2,5,2], x = 3
输出:[1,2,2,4,3,5]
思路:
很简单,分割链表再组合就行了,如果想着在原链表上操作,那就太难了
简单来说就是,用cur遍历当前链表,发现cur->val小于x,就接到新链表1后面,大于或等于x,就接到新链表2后面
就这么简单,只需注意一点:
记得已经被分配过的结点,记得切断它与后续结点的链接
不过,这一题其实也可以直接用原链表头 + 一个新链表头完成了,不过我懒了,直接两个新链表思路简单,实现也简单,就不纠结了
代码:
class Solution {
public:
ListNode* partition(ListNode* head, int x) {
if(!head)return nullptr;//特殊情况
ListNode* head1 = new ListNode(-999);
ListNode* head2 = new ListNode(-999);
ListNode* p1 = head1, *p2 = head2;
ListNode*cur = head;
while(cur)
{
if(cur->val < x)
{
p1->next = cur;
p1 = p1->next;
}
else
{
p2->next = cur;
p2 = p2->next;
}
//这里别忘了,已经分配过的结点得切断它与后续得链接,不然只会碍事
ListNode*temp = cur;
cur = cur->next;
temp->next = nullptr;//切断链接
}
p1->next = head2->next;//连上两个链表
return head1->next;
}
};