BM4.合并两个排序的链表
方法一:双指针迭代
双指针指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个指针(特殊情况甚至可以多个),两个指针或是同方向访问两个链表、或是同方向访问一个链表(快慢指针)、或是相反方向扫描(对撞指针),从而达到我们需要的目的。
思路:
这道题既然两个链表已经是排好序的,都是从小到大的顺序,那我们要将其组合,可以使用归并排序的思想:每次比较两个头部,从中取出最小的元素,然后依次往后。这样两个链表依次往后,我们肯定需要两个指针同方向访问才能实现。
具体过程:
- step 1:判断空链表的情况,只要有一个链表为空,那答案必定就是另一个链表了,就算另一个链表也为空。
- step 2:新建一个空的表头后面连接两个链表排序后的节点,两个指针分别指向两链表头。
- step 3:遍历两个链表都不为空的情况,取较小值添加在新的链表后面,每次只把被添加的链表的指针后移。
- step 4:遍历到最后肯定有一个链表还有剩余的节点,它们的值将大于前面所有的,直接连在新的链表后面即可。
#include <cstddef>
class Solution {
public:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param pHead1 ListNode类
* @param pHead2 ListNode类
* @return ListNode类
*/
ListNode* Merge(ListNode* pHead1, ListNode* pHead2) {
// write code here
if(pHead1 == nullptr) return pHead2;
if(pHead2 == nullptr) return pHead1;
ListNode* start = new ListNode(0);
ListNode* cur = start;
while(pHead1 && pHead2){
if(pHead1->val <= pHead2->val){
cur->next = pHead1;
pHead1 = pHead1->next;
}else{
cur->next = pHead2;
pHead2 = pHead2->next;
}
cur = cur->next;
}
if(pHead1) cur->next = pHead1;
else cur->next = pHead2;
return start->next;
}
};
方法二:双指针递归
思路:
上述方法一中,我们利用归并思想不断合并两个链表,每当我们添加完一个节点后,该节点指针后移,相当于这个链表剩余部分与另一个链表剩余部分合并,两个链表剩余部分合并就是原问题两个有序链表合并的子问题,因此也可以使用递归:
- 终止条件: 当一个链表已经因为递归到了末尾,另一个链表剩余部分一定都大于前面的,因此我们可以将另一个链表剩余部分拼在结果后面,结束递归。
- 返回值: 每次返回拼接好的较大部分的子链表。
- 本级任务: 每级不断进入下一个较小的值后的链表部分与另一个链表剩余部分,再将本次的节点接在后面较大值拼好的结果前面。
具体做法:
- step 1:每次比较两个链表当前节点的值,然后取较小值的链表指针往后,另一个不变,两段子链表作为新的链表送入递归中。
- step 2:递归回来的结果我们要加在当前较小值的节点后面,相当于不断在较小值后面添加节点。
- step 3:递归的终止是两个链表有一个为空。
#include <cstddef>
class Solution {
public:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param pHead1 ListNode类
* @param pHead2 ListNode类
* @return ListNode类
*/
ListNode* Merge(ListNode* pHead1, ListNode* pHead2) {
// write code here
if(pHead1 == nullptr) return pHead2;
if(pHead2 == nullptr) return pHead1;
if(pHead1->val <= pHead2->val){
pHead1->next = Merge(pHead1->next, pHead2);
return pHead1;
}else{
pHead2->next = Merge(pHead1, pHead2->next);
return pHead2;
}
}
};
BM5.合并k个已排序的链表
方法一:归并排序思想(推荐使用)
知识点1:双指针
双指针指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个指针(特殊情况甚至可以多个),两个指针或是同方向访问两个链表、或是同方向访问一个链表(快慢指针)、或是相反方向扫描(对撞指针),从而达到我们需要的目的。
知识点2:分治
分治即“分而治之”,“分”指的是将一个大而复杂的问题划分成多个性质相同但是规模更小的子问题,子问题继续按照这样划分,直到问题可以被轻易解决;“治”指的是将子问题单独进行处理。经过分治后的子问题,需要将解进行合并才能得到原问题的解,因此整个分治过程经常用递归来实现。
思路:
如果是两个有序链表合并,我们可能会利用归并排序合并阶段的思想:准备双指针分别放在两个链表头,每次取出较小的一个元素加入新的大链表,将其指针后移,继续比较,这样我们出去的都是最小的元素,自然就完成了排序。
其实这道题我们也可以两两比较啊,只要遍历链表数组,取出开头的两个链表,按照上述思路合并,然后新链表再与后一个继续合并,如此循环,知道全部合并完成。但是,这样太浪费时间了。
既然都是归并排序的思想了,那我们可不可以直接归并的分治来做,而不是顺序遍历合并链表呢?答案是可以的!
归并排序是什么?简单来说就是将一个数组每次划分成等长的两部分,对两部分进行排序即是子问题。对子问题继续划分,直到子问题只有1个元素。还原的时候呢,将每个子问题和它相邻的另一个子问题利用上述双指针的方式,1个与1个合并成2个,2个与2个合并成4个,因为这每个单独的子问题合并好的都是有序的,直到合并成原本长度的数组。
对于这k个链表,就相当于上述合并阶段的k个子问题,需要划分为链表数量更少的子问题,直到每一组合并时是两两合并,然后继续往上合并,这个过程基于递归:
- 终止条件: 划分的时候直到左右区间相等或左边大于右边。
- 返回值: 每级返回已经合并好的子问题链表。
- 本级任务: 对半划分,将划分后的子问题合并成新的链表。
具体做法:
- step 1:从链表数组的首和尾开始,每次划分从中间开始划分,划分成两半,得到左边�/2n/2个链表和右边�/2n/2个链表。
- step 2:继续不断递归划分,直到每部分链表数为1.
- step 3:将划分好的相邻两部分链表,按照两个有序链表合并的方式合并,合并好的两部分继续往上合并,直到最终合并成一个链表。
ListNode* divideMerge(vector<ListNode*>lists, int left, int right){
if(left > right)
return nullptr;
else if (left == right)
return lists[left];
int mid = (left + right) / 2;
return Merge(divideMerge(lists, left, mid), divideMerge(lists, mid + 1, right));
}
ListNode* mergeKLists(vector<ListNode*>& lists) {
// write code here
return divideMerge(lists, 0, lists.size() - 1);
}
方法二:优先队列(扩展思路)
知识点:优先队列
优先队列即PriorityQueue,是一种内置的机遇堆排序的容器,分为大顶堆与小顶堆,大顶堆的堆顶为最大元素,其余更小的元素在堆下方,小顶堆与其刚好相反。且因为容器内部的次序基于堆排序,因此每次插入元素时间复杂度都是�(���2�)O(log2n),而每次取出堆顶元素都是直接取出。
思路:
如果非要按照归并排序的合并思路,双指针不够用,我们可以直接准备�k个指针,每次比较得出�k个数字中的最小值。为了快速比较�k个数字得到最小值,我们可以利用Java提供的PriorityQueue或者C++SLT提供的优先队列或者Python提供的PriorityQueue可以实现,它是一种参照堆排序的容器,容器中的元素是有序的,如果是小顶堆,顶部元素就是最小的,每次可以直接取出最小的元素。也就是说
每次该容器中有k个元素,我们可以直接拿出最小的元素,再插入下一个元素,相当于每次都是链表的k个指针在比较大小,只移动最小元素的指针。
具体做法:
- step 1:不管是Java还是C++都需要重载比较方法,构造一个比较链表节点大小的小顶堆。(Python版本直接加入节点值)
- step 2:先遍历k个链表头,将不是空节点的节点加入优先队列。
- step 3:每次依次弹出优先队列中的最小元素,将其连接在合并后的链表后面,然后将这个节点在原本链表中的后一个节点(如果不为空的话)加入队列,类似上述归并排序双指针的过程。
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* ListNode(int x) : val(x), next(nullptr) {}
* };
*/
#include <queue>
class Solution {
public:
struct cmp{
bool operator()(ListNode* a, ListNode* b){
return a->val > b->val;
}
};
ListNode* mergeKLists(vector<ListNode*>& lists) {
// write code here
priority_queue<ListNode*, vector<ListNode*>, cmp> pq;
for(int i = 0; i < lists.size(); i++){
if(lists[i] != NULL)
pq.push(lists[i]);
}
ListNode* res = new ListNode(-1);
ListNode* head = res;
while(!pq.empty()){
ListNode* temp = pq.top();
pq.pop();
head->next = temp;
head = head->next;
if(temp->next != nullptr)
pq.push(temp->next);
}
return res->next;
}
};
BM6.判断链表中是否有环
方法:双指针(推荐使用)
知识点:双指针
双指针指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个指针(特殊情况甚至可以多个),两个指针或是同方向访问两个链表、或是同方向访问一个链表(快慢指针)、或是相反方向扫描(对撞指针),从而达到我们需要的目的。
思路:
我们都知道链表不像二叉树,每个节点只有一个val值和一个next指针,也就是说一个节点只能有一个指针指向下一个节点,不能有两个指针,那这时我们就可以说一个性质:环形链表的环一定在末尾,末尾没有NULL了。为什么这样说呢?仔细看上图,在环2,0,-4中,没有任何一个节点可以指针指出环,它们只能在环内不断循环,因此环后面不可能还有一条尾巴。如果是普通线形链表末尾一定有NULL,那我们可以根据链表中是否有NULL判断是不是有环。
但是,环形链表遍历过程中会不断循环,线形链表遍历到NULL结束了,但是环形链表何时能结束呢?我们可以用双指针技巧,同向访问的双指针,速度是快慢的,只要有环,二者就会在环内不断循环,且因为有速度差异,二者一定会相遇。
具体做法:
- step 1:设置快慢两个指针,初始都指向链表头。
- step 2:遍历链表,快指针每次走两步,慢指针每次走一步。
- step 3:如果快指针到了链表末尾,说明没有环,因为它每次走两步,所以要验证连续两步是否为NULL。
- step 4:如果链表有环,那快慢双指针会在环内循环,因为快指针每次走两步,因此快指针会在环内追到慢指针,二者相遇就代表有环。
class Solution {
public:
bool hasCycle(ListNode *head) {
//先判断链表为空的情况
if(head == NULL)
return false;
//快慢双指针
ListNode* fast = head;
ListNode* slow = head;
//如果没环快指针会先到链表尾
while(fast != NULL && fast->next != NULL){
//快指针移动两步
fast = fast->next->next;
//慢指针移动一步
slow = slow->next;
//相遇则有环
if(fast == slow)
return true;
}
//到末尾则没有环
return false;
}
};