目录
1、链表类算法题常用技巧:
【技巧】
1、画图!!!!!--->直观 + 形象 + 便于我们理解
2、引入虚拟头节点
链表类型的算法题一般都是不带头节点的单向链表,一般都是从第一个位置开始就已经存储有效数据了。 这类链表一般需要考虑很多的边界情况,所以给链表引入头节点,这个节点不存储数据,只是起到一个哨兵的作用。 好处:1)便于处理边界情况 2)方便我们对链表进行操作
3、不要吝啬空间,大胆去定义变量。
直接定义一个指针,就不用去担心语句的执行顺序,因为节点已经被保存起来了,不用害怕丢失,这样写就会很方便。
4、快慢双指针
特别好用在:判断链表是否有环,或者找链表环的入口、找链表倒数第n个节点
【链表中的常用操作】
1、创建一个新节点new
2、尾插:先定义一个变量指向尾节点,tail->next = 新节点 tail 指向新节点,方便下一次尾插(初始化尾指针的时候尾指针指向最后一个节点)
3、头插 先来一个虚拟头节点,然后让新节点的next指向虚拟头节点的next(不用担心虚拟头节点的next是一个null,是null不会影响,新节点指向空也是ok的),然后让虚拟头节点的next指向新节点即可 【头插其实可以直接完成逆序链表这道题】
只需定义一个cur,再完成头插即可实现逆序链表
【注意链表题一定要特别注意空节点!!!!!】
2、2.两数相加

题目解析:
2、4、3
5、6、4
因为是逆序存储的所以:342+465=807
逆序存储,返回708
这个题逆序其实是方便我们操作了,因为我们是从最低位开始相加的,刚好对应链表的2、5
【可以去牛客网去做链表相加Ⅱ这道题】-----相似
先来一个虚拟头节点(newhead),再初始化一个t用来存储两数加的结果,把cur1的值加到t中,把cur2的值加到t中,然后我求出t的个位,创建一个新节点,把这个数放进去,链接到虚拟头结点的后面。

cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution
{
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2)
{
ListNode* cur1 = l1, *cur2 = l2;
ListNode* newhead = new ListNode(0);//创建一个虚拟头节点
//因为我们要一直尾插,所以要再定义一个尾指针
ListNode* prev = newhead;//尾指针
int t = 0;//记录进位
while(cur1 || cur2 || t)
{
//先加上第一个链表
if(cur1)
{
t += cur1->val;
cur1 = cur1->next;
}
//加上第二个链表
if(cur2)
{
t += cur2->val;
cur2 = cur2 -> next;
}
//此时,cur1+cur2已经加完了,就把这个数的个位尾插到结果链表中
prev->next = new ListNode(t % 10);//只把个位放进来
prev = prev->next;//尾指针往右移动 方便下一个位置进来
t /= 10;//把个位干掉剩下的就是进位
}
prev = newhead->next;//先存一下,再释放
delete newhead;//释放内存防止内存泄露
return prev;
}
};
3、24.两两交换链表中的节点



cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution
{
public:
ListNode* swapPairs(ListNode* head)
{
if(head == nullptr || head->next == nullptr) return head;
ListNode* newhead = new ListNode(0);
newhead->next = head;
//*next = cur->next cur指针必须存在,又因为cur指向的是head所以先给head判空,*nnext = next->next,所以next也不能为空,如果next为空那么链表中就只有一个数head,也就不用交换 --保证不会出现空指针的解引用
ListNode* prev = newhead, *cur = prev->next, *next = cur->next, *nnext = next->next;
while(cur && next)//cur和cur都不等于空
{
//交换节点
prev->next = next;
cur->next = nnext;
next->next = cur;
//修改指针---按照原来指针从前到后的位置重新定义指针
prev = cur; //注意顺序
cur = nnext;
if(cur) next = cur->next;//cur为空的时候不能解指针
if(next) nnext = next->next;
}
cur = newhead->next;
delete newhead;
return cur;
}
};
4、143.重排链表

题意:输出第一个,倒数第一个,第二个,倒数第二个这样输出
算法思想就是进行模拟
1、找到链表的中间节点 (使用快慢指针)
2、把后面的部分逆序(头插法)
3、合并两个链表----之前做过题合并两个有序链表----(使用双指针)
分类讨论:当链表元素有奇数个的时候,slow指针刚好就是指向链表中间的,但是当元素个数为偶数个的时候,slow指向中间偏右的位置。
我们需要对后半部分的链表进行反转操作:1、将slow->next(也就是slow之后进行反转)
2、包含slow所指向的节点一起反转,这两种对于奇数个和偶数个都是适用的。
只是,第二种方式对于奇数个元素时,要把head指向slow(head的下一个指针)要拆开,因为我们要将链表分为两部分。这样拆其实会发生错误,解决这种错误的方法就是给链表加一个虚拟头节点。
第二步:反转--->头插----虚拟头节点
第三步:合并--->尾插---虚拟头节点+尾指针
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution
{
public:
void reorderList(ListNode* head)
{
//处理边界情况:当传入的链表为空或者只有一个或者两个的时候都是不需要重拍的
if(head == nullptr || head->next == nullptr || head->next->next == nullptr) return;
//1、找到链表的中间节点 - 快慢双指针 (一定要画图考虑slow的落点在哪里)
ListNode* slow = head, *fast = head;//定义快慢指针
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
//2、把slow后面的部分给逆序 -- 使用头插法
ListNode* head2 = new ListNode(0);
ListNode* cur = slow->next;//要把slow后面的节点插到head2的后面
slow->next = nullptr;//因为slow指向第一个链表的最后一个元素,所以让slow的next为空,就可以把链表断开
while(cur)
{
//进行头插操作
ListNode* next = cur->next;//先把cur的下一个位置保存一下,防止丢失
cur->next = head2->next;
head2->next = cur;
cur = next;//让cur继续向后移动
}
//3、合并两个链表--使用双指针 -- 创建一个虚拟头节点然后把节点都放在虚拟头节点的后面
ListNode* ret = new ListNode(0);
ListNode* prev = ret;//合并两个链表需要尾插,所以需要一个尾指针 一直尾插到prev的后面就可以了
ListNode* cur1 = head, *cur2 = head2->next;//用两个指针分别指向两个分开的链表的第一个有效元素
while(cur1) //仅需判断cur1不为空即可 --- cur1比较长
{
//放入第一个链表的元素
prev->next = cur1;
cur1 = cur1->next;
prev = prev->next;
//放入第二个链表的元素
if(cur2) //因为第二个链表比第一个链表短,所以要防止cur2为空的情况,不为空才向后挪动
{
prev->next = cur2;
cur2 = cur2->next;
prev = prev->next;
}
}
//释放所有new出来的节点
delete head2;
delete ret;
}
};
5、23.合并K个升序链表

利用优先级队列:
创建一个小根堆,使用k个指针,先指向第一个节点,然后把这些元素全放进小根堆当中,拿出堆顶的元素,这个堆顶元素肯定是输入的k个链表中某个节点,如果这个节点的后面还有节点就放到堆里。
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution
{
//实现小根堆的比较函数
struct cmp
{
bool operator()(const ListNode* l1, const ListNode* l2)//重载一下()运算符
{
return l1->val > l2->val;//谁大谁就向下调整
}
};
public:
ListNode* mergeKLists(vector<ListNode*>& lists)
{
//创建一个小根堆
priority_queue<ListNode*,vector<ListNode*>, cmp> heap;
//让所有的头节点进入小根堆
for(auto l : lists)//遍历传进的链表 当链表不为空的时候再把头节点放进来
if(l) heap.push(l);
//合并k个有序链表 ---每次拿出堆顶元素进行合并
ListNode* ret = new ListNode(0);
ListNode* prev = ret;
while(!heap.empty())
{
//当堆不为空的时候,就一直拿出堆顶元素
ListNode* t = heap.top();
heap.pop();
prev->next = t;
prev = t;
if(t->next) heap.push(t->next);
}
prev = ret->next;
delete ret;
return prev;
}
};
6、25.K个一组翻转链表

1、先求出需要逆序多少组:n = 长度/k
2、重复n次,长度为k的链表的逆序即可----链表逆序用头插法
但是需要注意:
当反转完一组之后,进入下一组的时候,我们不能把下一组的元素放到上一组的前面(因为我们在利用头插来反转),所以当我们进入每组的反转循环的时候,就要先记录一下第一个节点,然后当这组反转完之后,把下组放在这个节点的后面开始头插。
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution
{
public:
ListNode* reverseKGroup(ListNode* head, int k)
{
//1、先求出需要逆序多少组
int n = 0;
ListNode* cur = head;
while(cur)
{
cur = cur->next;
n++;
}
n /= k;
//2、重复n次:长度为k的链表的反转
ListNode* newhead = new ListNode(0);
ListNode* prev = newhead;//头插开始的位置
cur = head;//cur从head位置开始
for(int i = 0; i < n; i++)//n次
{
//每次头插之前要记录第一个节点 就是下一次头插的前驱
ListNode* tmp = cur;
for(int j = 0; j < k; j++)//长度为k
{
//头插之前先用一个变量记录一下cur的下一个位置
ListNode* next = cur->next;
cur->next = prev->next;
prev->next = cur;
cur = next;
}
//第一组反转完之后,跟新prev就知道下一次头插的前驱 就是头插之前记录的
prev = tmp;
}
//两个for循环之后就剩下后面不成一组的,就不需要反转
prev->next = cur;//把不反转的接上
cur = newhead->next;//我们把结果放在newhead的后边,但是newhead需要进行释放,所以先把他的next存到cur,返回cur即可 虚拟头节点的下一个进行返回
delete newhead;
return cur;
}
};




