Leetcode练习题:数组与链表
24:两两交换链表中的节点
问题描述
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
示例 1:
给定 1->2->3->4, 你应该返回 2->1->4->3.
解题思路
自己语言:简单的分成两个一组来看,显然是要进行q->next=p操作再返回q,关键是p->next指向哪里?很容易想到就是下一组进行操作后的q。
递归实现:
注意终止条件:如果链表为空或者只有一个节点时无需再交换
代码实现
ListNode* swapPairs(ListNode* head) {
if(!head||!head->next)
{
return head;
}
ListNode *p,*q;
p=head;
q=head->next;
p->next=swapPairs(q->next);
q->next=p;
return q;
}
反思与收获
其实递归的思想在实际应用中总是很难想到…
虽然这题用非递归的方法也可以解决,通过增加了一个dummyhead指针来实现
部分代码如下:
if(dummy->next&&dummy->next->next)
{
ListNode *p=dummy->next;
ListNode *q=dummy->next->next;
dummy->next=q;
p->next=q->next;
q->next=p;
dummy=p;
}
25:K 个一组翻转链表
问题描述
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
示例:
给你这个链表:1->2->3->4->5
当 k = 2 时,应当返回: 2->1->4->3->5
当 k = 3 时,应当返回: 3->2->1->4->5
说明:
你的算法只能使用常数的额外空间。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
解题思路
自己语言:感觉就是一道倒置链表的升级题目,只不过这一次倒置的链表长度有了要求。那在明确是倒置的基本操作后,问题就在于怎么取长度为k的每段链表以及如何将其重新链接起来。
1.用循环来实现就可以了
2.建两个指针来记录前驱和后继
代码实现
pair<ListNode*,ListNode*> Rerverse(ListNode* head,ListNode* tail)
{
//找后面的好找 倒置基础操作
ListNode *p=head,*r,*L=tail->next;
while(L!=tail)
{
r=p->next;
p->next=L;
L=p;
p=r;
}
return {tail,head};
}
ListNode* reverseKGroup(ListNode* head, int k) {
ListNode *pre=new ListNode(0);
pre->next=head;
ListNode *p=pre;
ListNode *tail,*nextp;
while(head)
{
tail=p;
for(int i=0;i<k;i++)
{
tail=tail->next;
if(!tail){
//不够直接返回
return pre->next;
}
}
nextp=tail->next;
pair<ListNode*,ListNode*>res=Rerverse(head,tail);
head=res.first;
tail=res.second;
p->next=head;
tail->next=nextp;
p=tail;
head=tail->next;
}
return pre->next;
}
反思与收获
一定要熟练掌握一些十分常用的基础操作,之前实现倒置时都是从前面开始插入,这次使用的是从后面开始插入,根据情况不同选择适合的方式。
61:旋转链表
问题描述
给定一个链表,旋转链表,将链表每个节点向右移动 k 个位置,其中 k 是非负数。
示例 1:
输入: 1->2->3->4->5->NULL, k = 2 输出: 4->5->1->2->3->NULL
解释:
向右旋转 1 步: 5->1->2->3->4->NULL 向右旋转 2 步: 4->5->1->2->3->NULL
示例 2:
输入: 0->1->2->NULL, k = 4 输出: 2->0->1->NULL
解释:
向右旋转 1 步: 2->0->1->NULL 向右旋转 2 步: 1->2->0->NULL 向右旋转 3 步:
0->1->2->NULL 向右旋转 4 步: 2->0->1->NULL
解题思路
自己语言:实际上这跟以前做到过的旋转数组题目是一样的,就是要先对k取余,找到实际变化移动了几步。而且链表更好实现,只需要变成循环链表,找到首个节点再断开就可以了。
代码实现
ListNode* rotateRight(ListNode* head, int k) {
//链表为空
if(!head)
{
return NULL;
}
//只有一个元素
if(!head->next)
{
return head;
}
ListNode *p=head,*q;
int len=1;
//找到尾节点
while(p->next)
{
len++;
p=p->next;
}
//变成循环链表
p->next=head;
p=head;
//注意:这是找到首节点的前一个,所以是len-k%len-1,方便实现断链
for(int i=0;i<len-k%len-1;i++)
{
p=p->next;
}
q=p->next;
p->next=NULL;
return q;
}
反思与收获
这题将链表变成循环链表的操作十分巧妙,利用链表的特性来快速实现。
143:重排链表
问题描述
给定一个单链表 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.
解题思路
自己的话:一开始肯定是想头上摘取一个节点,尾部摘取一个节点一步步来实现,但这不是循环链表,从后往前的操作不好实现。这时思维有点框住了,
可以想到将链表分成两个部分,一部分是原顺序元素,另一部分是要插入改变顺序(即倒序)的元素。一次遍历获取链表长度,
偶数长度,从n/2+1位分割
奇数长度,从n/2位分割
代码实现
void reorderList(ListNode* head)
{
//填充本函数完成功能
if(!head)
{
return ;
}
ListNode *p,*q,*L,*r;
p=q=L=head;
int len=1;
while(q->next)
{
q=q->next;
len++;
}
int n=len/2;
while(n--)
{
p=p->next;
}
q=p->next;
p->next=NULL;
//头插入 倒置
while(q)
{
r=q->next;
q->next=p->next;
p->next=q;
q=r;
}
q=p->next;
p->next=NULL;
//不断插入中间
while(q)
{
r=q->next;
q->next=L->next;
L->next=q;
L=q->next;
q=r;
}
}
反思与收获
在模拟算法过程是往往不是只有一种方式,要多进行思考和训练,防止思维定式。
148:排序链表
问题描述
在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。
示例 1:
输入: 4->2->1->3 输出: 1->2->3->4
示例 2:
输入: -1->5->3->4->0 输出: -1->0->3->4->5
解题思路
首先复习一下各个常用排序算法的时间复杂度和空间复杂度
算法种类 | 时间复杂度 最好 | 最坏 | 平均 | 空间复杂度 | 是否稳定 |
---|---|---|---|---|---|
直接插入 | O(n) | O(n^2) | O(n^2) | O(1) | 是 |
冒泡 | O(n) | O(n^2) | O(n^2) | O(1) | 是 |
简单选择 | O(n^2) | O(n^2) | O(n^2) | O(1) | 否 |
希尔 | O(1) | 否 | |||
快速 | O(nlog2n) | O(n^2) | O(nlog2n) | O(nlog2n) | 否 |
堆 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(1) | 否 |
2路归并 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(n) | 是 |
基数 | O(d(n+r)) | O(d(n+r)) | O(d(n+r)) | O(r) | 是 |
ps:用Markdown无法进行单元格合并,但可以通过HTML实现,有点违背Markdown简单的意义就不合并了。
ps:(r)为变成商标符号 通过(r)实现转义
根据题目时间复杂度的要求,会想到使用二分法,但如果递归实现的话不满足空间复杂度的要求,采用从底到顶的方法可以满足。
(但我这边还是偷懒用的递归…)
知识点:
1. 采用快慢指针来二分链表
ListNode *slow=head,*fast=head->next;
while(fast&&fast->next)
{
slow=slow->next;
fast=fast->next->next;
}
2. 建立空的头指针来连接左右两个链表
代码实现
递归法
ListNode* sortList(ListNode* head)
{
//填充本函数完成功能在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序
//二分法
if(!head||!head->next)
{
return head;
}
//中间节点 偶数则找左边的
ListNode *slow=head;
ListNode *fast=head->next;
while(fast && fast->next)
{
slow=slow->next;
fast=fast->next->next;
}
ListNode *r=slow->next;
slow->next=NULL;
ListNode *left=sortList(head),*right=sortList(r);
ListNode *L=new ListNode(0);
ListNode *ans=L;
while(left&&right)
{
if(left->val<right->val)
{
L->next=left;
L=L->next;
left=left->next;
}else{
L->next=right;
L=L->next;
right=right->next;
}
}
L->next= left==NULL?right:left;
return ans->next;
}
非递归法
代码会有点长不写了,看链接学习 148题解 参考代码
反思与收获
排序是数据结构中最基础的操作,要牢记每一个排序算法的思想和时间复杂度。
328:奇偶链表
问题描述
给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。
请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。
示例 1:
输入: 1->2->3->4->5->NULL 输出: 1->3->5->2->4->NULL
示例 2:
输入: 2->1->3->5->6->4->7->NULL 输出: 2->3->6->7->1->5->4->NULL
说明:
应当保持奇数节点和偶数节点的相对顺序。
链表的第一个节点视为奇数节点,第二个节点视为偶数节点,以此类推。
解题思路
这题相对来说算简单的,只需要将奇数的节点组成一条链,偶数的节点组成一条链,最后将偶数链接下奇数链后面即可。
注:需要一个节点来记住偶数链最初的头结点。
并且这题不需要分链表长度奇偶分析,判断条件是next不是next->next
代码实现
遍历了一次,使用四个指针,满足时间和空间复杂度的要求。
ListNode* oddEvenList(ListNode* head) {
//填充本函数完成功能
if(!head)
{
return head;
}
ListNode *p,*q,*h;
p=head;
h=q=head->next;
while(p->next&&q->next)
{
p->next=p->next->next;
q->next=q->next->next;
p=p->next;
q=q->next;
}
p->next=h;
return head;
}
反思与收获
做题的时候画一下图,更加好理解,即使是一眼看上去是简单的题目。
725:分割链表
问题描述
给定一个头结点为 root 的链表, 编写一个函数以将链表分隔为 k 个连续的部分。
每部分的长度应该尽可能的相等: 任意两部分的长度差距不能超过 1,也就是说可能有些部分为 null。
这k个部分应该按照在链表中出现的顺序进行输出,并且排在前面的部分的长度应该大于或等于后面的长度。
返回一个符合上述规则的链表的列表。
举例:
1->2->3->4, k = 5 // 5 结果 [ [1], [2], [3], [4], null ]
示例 1:
输入: root = [1, 2, 3], k = 5 输出: [[1],[2],[3],[],[]]
解释:
输入输出各部分都应该是链表,而不是数组。 例如, 输入的结点 root 的 val= 1, root.next.val = 2,root.next.next.val = 3, 且 root.next.next.next = null。
第一个输出 output[0]是 output[0].val = 1, output[0].next = null。 最后一个元素 output[4] 为 null,
它代表了最后一个部分为空链表。
示例 2:
输入: root = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], k = 3 输出: [[1, 2, 3, 4],[5, 6, 7], [8, 9, 10]]
解释:
输入被分成了几个连续的部分,并且每部分的长度相差不超过1.前面部分的长度大于等于后面部分的长度。
提示:
root 的长度范围: [0, 1000].
输入的每个节点的大小范围:[0, 999].
k 的取值范围: [1, 50].
解题思路
其实题目不难,就是根据长度分成k个部分,要求前面的链表长一点。
首先获取链表长度,再除以k获取每部分长度,再对k取余即获取需要加长一个单位的部分个数,
举例解释这么多,就是为了说明需要返回的值是,每个部分链表的头结点。
稍微要注意的是:
需要一个前驱节点,用来进行断链,所以p实际是指向下一段链的头结点的。
代码实现
vector<ListNode*> splitListToParts(ListNode* head, int k)
{
int len=0;
ListNode *p=head;
while(p)
{
len++;
p=p->next;
}
int avg=len/k,m=len%k;
vector<ListNode*> ans(k,nullptr);
ListNode *pre=nullptr;
p=head;
int temp;
for(int i=0;i<k;i++)
{
ans[i]=p;
temp=(m==0)?avg:(avg+1);
while(temp--)
{
pre=p;
p=p->next;
}
if(pre)
{
pre->next=nullptr;
}
if(m)
{
m--;
}
}
return ans;
}
反思与收获
1. 在做不带头结点的链表题目时,常常考虑增加一个头结点使算法更加简洁。
2. 遇到:每部分的长度应该尽可能的相等: 任意两部分的长度差距不能超过 1,也就是说可能有些部分为 null。直接除整取余,循环实现。
3. vector带参数初始化
// 初始化size,但每个元素值为默认值
vector abc(10); // 初始化了10个默认值为0的元素
// 初始化size,并且设置初始值
vector cde(10, 1); // 初始化了10个值为1的元素
817:链表组件
给定链表头结点 head,该链表上的每个结点都有一个 唯一的整型值 。
同时给定列表 G,该列表是上述链表中整型值的一个子集。
返回列表 G 中组件的个数,这里对组件的定义为:链表中一段最长连续结点的值(该值必须在列表 G 中)构成的集合。
示例 1:
输入: head: 0->1->2->3 G = [0, 1, 3]
输出: 2
解释: 链表中,0 和 1 是相连接的,且 G中不包含 2,所以 [0, 1] 是 G 的一个组件,同理 [3] 也是一个组件,故返回 2。
示例 2:
输入: head: 0->1->2->3->4 G = [0, 3, 1, 4]
输出: 2
解释: 链表中,0 和 1 是相连接的,3和 4 是相连接的,所以 [0, 1] 和 [3, 4] 是两个组件,故返回 2。
提示:
如果 N 是给定链表 head 的长度,1 <= N <= 10000。
链表中每个结点的值所在范围为 [0, N - 1]。
1 <= G.length <= 10000
G 是链表中所有结点的值的一个子集.
解题思路
题目说的略微复杂,要仔细读几遍理解题目意思。
最初理解:
从链表中找相连的每一段,与G进行对比,是否每一个元素都在G中,如果是则组件数+1。
但不可能是找出所有的段,这样会重复计算,得是最长的。
升级理解:
遍历链表,如果元素在G中就继续往后走,如果有元素不在G中,相当于完成一个断链,组件数+1。
官方题解说明:
我们对链表进行一次扫描,一个组件在链表中对应一段极长的连续节点,因此如果当前的节点在列表 G 中,并且下一个节点不在列表 G 中,我们就找到了一个组件的尾节点,可以将答案加 1。
代码实现
理解了算法思想之后,代码很好写。
int numComponents(ListNode* head, vector<int>& G)
{
ListNode *p=head;
int count=0;
while(p)
{
if(std::find(G.begin(),G.end(),p->val)!=G.end()&&(!p->next||std::find(G.begin(),G.end(),p->next->val)==G.end()))
{
count++;
}
p=p->next;
}
return count;
}
反思与收获
遇到描述比较长的题目,要先多阅读几遍,千万不要自己想难了,它只是为了让你更好的理解。
不要想得太复杂
1019:链表中的下一个更大节点
问题描述
给出一个以头节点 head 作为第一个节点的链表。链表中的节点分别编号为:node_1, node_2, node_3, … 。
每个节点都可能有下一个更大值(next larger value):对于 node_i,如果其 next_larger(node_i) 是 node_j.val,那么就有 j > i 且 node_j.val > node_i.val,而 j 是可能的选项中最小的那个。如果不存在这样的 j,那么下一个更大值为 0 。
返回整数答案数组 answer,其中 answer[i] = next_larger(node_{i+1}) 。
注意:在下面的示例中,诸如 [2,1,5] 这样的输入(不是输出)是链表的序列化表示,其头节点的值为 2,第二个节点值为 1,第三个节点值为 5 。
示例 1:
输入:[2,1,5] 输出:[5,5,0]
示例 2:
输入:[2,7,4,3,5] 输出:[7,0,5,5,0]
示例 3:
输入:[1,7,5,1,9,2,5,1] 输出:[7,9,9,9,0,5,0,0]
提示:
对于链表中的每个节点,1 <= node.val <= 10^9
给定列表的长度在 [0, 10000] 范围内
解题思路
注意题目说的 而 j 是可能的选项中最小的那个所以只要找到第一个比它大的值就是答案,没有找到就是0。
用p指针来遍历链表,用q=p->next指针来寻找第一个比它的值。
代码实现
vector<int> nextLargerNodes(ListNode* head)
{
ListNode *p=head,*q;
vector<int> ans;
while(p)
{
q=p->next;
while(q)
{
if(q->val>p->val)
{
ans.push_back(q->val);
break;
}
q=q->next;
}
if(!q)
{
ans.push_back(0);
}
p=p->next;
}
return ans;
}
反思与收获
看清题意
1171:从链表中删去总和值为零的连续节点
问题描述
给你一个链表的头节点 head,请你编写代码,反复删去链表中由 总和 值为 0 的连续节点组成的序列,直到不存在这样的序列为止。
删除完毕后,请你返回最终结果链表的头节点。
你可以返回任何满足题目要求的答案。
(注意,下面示例中的所有序列,都是对 ListNode 对象序列化的表示。)
示例 1:
输入:head = [1,2,-3,3,1]
输出:[3,1]
提示:答案 [1,2,1] 也是正确的。
示例 2:
输入:head = [1,2,3,-3,4]
输出:[1,2,4]
示例 3:
输入:head = [1,2,3,-3,-2]
输出:[1]
提示:
给你的链表中可能有 1 到 1000 个节点。
对于链表中的每个节点,节点的值:-1000 <= node.val <= 1000.
解题思路
不要去算每一小段连续链表之和是多少,而是通过计算前缀和来实现,如果出现前缀和一样的情况,就说明中间节点之和必定是0,直接用next的操作来实现。
代码实现
ListNode* removeZeroSumSublists(ListNode* head)
{
map<int,ListNode*> m;
ListNode *res=new ListNode(0);
res->next=head;
int sum=0;
ListNode *p=res;
/*
首次遍历建立 节点处链表和<->节点 哈希表
若同一和出现多次会覆盖,即记录该sum出现的最后一次节点
*/
while(p)
{
sum+=p->val;
m[sum]=p;
p=p->next;
}
sum=0;
p=res;
/*
第二遍遍历 若当前节点处sum在下一处出现了则表明两结点之间所有节点和为0 直接删除区间所有节点
*/
while(p)
{
sum+=p->val;
p->next=m[sum]->next;
p=p->next;
}
return res->next;
}
反思与收获
每次遇到这种长度不限连续链表或数组的题目都不会做,好像动态规划这一类,会想的很复杂,还没有学到动态规划的套路。
这题借鉴的是大佬的解题思路,一下子把复杂的问题变简单了。
要注意的是哈希表的用法,平时很少用到map,遇到一一对应的情况要考虑使用哈希表。
————————————————————
2020.7.19 终于写完第一篇博客 原来归纳整理真正写下来是这样的感觉 希望能养成习惯吧
但其实最重要的是 是把这些都记到脑子里面啊!!!∑(゚Д゚ノ)ノ