一、双指针之单链表
1、合并两个有序链表
分析:双指针技巧,双在哪? 两个链表一条一个指针,从头开始,每次比较大小,然后把小的一个节点赋值到新链表,并且对应的的指针移动。
例题: 合并两个有序链表
代码:
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode* dum = new ListNode(0);
ListNode* cur = dum;
while(list1 != nullptr && list2 != nullptr){
if(list1->val < list2->val){
cur->next = list1;
list1 = list1->next;
}
else{
cur->next = list2;
list2 = list2->next;
}
cur = cur->next;
}
cur->next = list1 != nullptr ? list1 : list2;
return dum->next;
}
};
总结:
i). 拼接最后的部分不需要用两个while(数组才需要这么做),直接把节点的结构体赋值过去就可以了。
ii). C++中所有NULL都用nullptr代替,为了迎合C++的重载特性。(*NULL 在C++ 里表示空指针,我们调用testWork(NULL),期望是调用的是testWork(int *index), 但实际调用了testWork(int index))
iii). 虚拟头结点技巧: 也就是 dummy
节点. 如果不使用 dummy
虚拟节点,代码会复杂一些,需要额外处理指针cur为空的情况。而有了 dummy
节点这个占位符,可以避免处理空指针的情况,降低代码的复杂性
什么时候需要用虚拟头结点?当你需要创造一条新链表的时候,可以使用虚拟头结点简化边界情况的处理。
2、链表的分解
分析:只需要创建两条链表即可,一个用来存大于x的,一个用来存小于x的,最后把他们连接起来。
例题: 分隔链表
代码:
class Solution {
public:
ListNode* partition(ListNode* head, int x) {
ListNode* dum1 = new ListNode(0);
ListNode* dum2 = new ListNode(0);
ListNode* cur1 = dum1;
ListNode* cur2 = dum2;
while(head != nullptr){
if(head->val < x){
cur1->next = head;
cur1 = cur1->next;
}
else{
cur2->next = head;
cur2 = cur2->next;
}
//断开的前进方式
ListNode* tmp = head->next;
head->next = nullptr;
head = tmp;
}
cur1->next = dum2->next;
return dum1->next;
}
};
总结:
i). 断开的前进方式,防止输出链表中出现环,其本质目的是为了每次赋值给cur1和cur2是一个节点而非节点及其后整段链表。
3、合并 k
个有序链表(STL的运用——priority_queue)
分析:这里很像合并两个升序链表,区别在于,两个节点的大小关系一下就可以比较出来,但是K个(并且不知道K具体大小)就不可能一下子判断出来,如果用普通的函数比较,需要传不定长的参数,所以这里需要借助优先级队列来判断。
优先级队列:辅助找出最小值的数据结构,其底层是用最小堆来实现的。
例题: 合并 K 个升序链表
代码:
class Solution {
public:
ListNode* mergeKLists(vector<ListNode*>& lists) {
ListNode* dummy = new ListNode(0);
ListNode* cur = dummy;
auto cmp = [](ListNode* a, ListNode* b){
return a->val > b->val;
};
priority_queue<ListNode*, vector<ListNode*>, decltype(cmp)> pq;
for(auto head : lists){
if(head) pq.push(head);
}
while(!pq.empty()){
auto node = pq.top();
pq.pop();
if(node->next){
pq.push(node->next);
}
cur->next = node;
cur = cur->next;
}
return dummy->next;
}
};
总结:
i)auto这种语法糖很好用
ii)自定义的比较函数,注意两个点:第一是a>b表示最小堆(优先级队列pop出来是最小值),第二是匿名函数的写法 auto fun = [接收参数](形参){函数体}
iii)迭代器遍历的时候,记得加上if(head),处理一开始就为空的情况。
4、寻找单链表的倒数第 k
个节点(时差指针)
分析:倒数第k个节点也就是正数第n-k+1个,但是这种题给出来一般是不会给链表长度n的,比较平庸的做法是,先遍历一遍链表得到n,然后找到第n-k+1个元素,这样就需要遍历两边链表,那么有没有遍历一次的方法呢?
关键是在不数链表长度的情况下找到n-k+1这个量,这种情况就需要用到双指针了
第一个指针走k步后,剩下的就是n-k步了,这时候再加一个指针同步进行,就能够找到n-k+1了。
代码:
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0, head);
ListNode* fast = dummy;
ListNode* slow = dummy;
for(int i = 0; i<n; i++){
fast = fast->next;
}
while(fast->next){
slow = slow->next;
fast = fast->next;
}
slow->next = slow->next->next;
return dummy->next;
}
};
总结:
i) 哨兵节点一定要加上,非常有用。
ii)最后返回用哨兵节点的next返回而不是原生的head,它们有轻微的差别。(因为改变的是dummy的next而不是head,当dummy->next为空时它不指向head)
iii) fast是用来定位slow位置的,最后赋值是没有用的,全靠slow的赋值来删除节点。一开始被例子所迷惑了,认为slow和fast中间的就是需要删除的节点,实际上是样本数量太少导致的巧合。
5、寻找单链表的中点(步差指针)
分析:
平凡的思路:一个指针走到头后,新设立一个指针,然后相向而行
巧妙的思路:两个指针同时走,一个走两步,一个走一步,当fast走到头时,slow就到了重点(细节,比如奇偶问题,在搭建算法总体架构时不用考虑)
例题: 链表的中间结点
代码:
class Solution {
public:
ListNode* middleNode(ListNode* head) {
ListNode* dummy = new ListNode(0, head);
ListNode* fast = dummy;
ListNode* slow = dummy;
while(fast){
slow = slow->next;
fast = fast->next;
if(fast){
fast = fast->next;
}
}
return slow;
}
};
总结:
i) 边界情况处理用奇偶举例来写出一个通用的代码。
ii)依然使用哨兵节点dummy
6、判断单链表是否包含环并找出环起点(画图+对结构的理解)
分析:
一对快慢指针,步长为1、2,如果有环那么他们会相遇.
问题1:如果不会相遇,什么时候判断结束?答:当快指针走到头的时候。
问题2:如何得到环的起点?答:当相遇后,只要把其中一个指针设置为head,然后同步步长为1,再次相遇时,就得到了环起点。
例题: 环形链表 II
代码:
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* fast = head;
ListNode* slow = head;
while(fast){
slow = slow->next;
if(fast->next == nullptr){
return nullptr;
}
fast = fast->next->next;
if(fast == slow){
slow = head;
while(slow != fast){
slow = slow->next;
fast = fast->next;
}
return fast;
}
}
return nullptr;
}
};
总结:
i)要学会画图分析,不要空想
7、判断两个单链表是否相交并找出交点(对偶法)
分析:
我的思路:(Trivial的思路)先两个一起走,其中一个走到头后,算另一个还差多少,然后这个差算出来就可以让这两个链表同步,然后再从头对齐一起走,如果再到头之前,他们的listnode能一样就说明有相交的。
巧妙的思路:(对偶法)把两个链表拼接在一起,让他们同时结束。如果结束时,两个指针都是null,说明无相交,否则出来的指针就是相交起点。
两个思路的共同点都是让链表同步!
但是第一个思路处理起来会很麻烦,因为不知道谁先结束,所以需要对两个链表进行特殊的处理。
用对偶法可以很简便的化解这种麻烦。
例题: 相交链表
代码:
我的算法:
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* ans = nullptr;
ListNode* A = headA;
ListNode* B = headB;
while(A != nullptr && B != nullptr){
A = A->next;
B = B->next;
}
//不知道谁先结束需要做的处理
ListNode* tmp = A==nullptr?B:A;
ListNode* tmphead = A==nullptr?headB:headA;
ListNode* tmphead2 = A==nullptr?headA:headB;
//
int cnt = 0;
while(tmp != nullptr){
cnt++;
tmp = tmp->next;
}
for(int i = 0; i<cnt; i++){
tmphead = tmphead->next;
}
while(tmphead){
if(tmphead != tmphead2){
tmphead = tmphead->next;
tmphead2 = tmphead2->next;
}
else{
ans = tmphead;
break;
}
}
return ans;
}
};
拼接算法:
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if(headA == nullptr || headB == nullptr){
return nullptr;
}
ListNode* A = headA;
ListNode* B = headB;
while(A!=B){
A = A==nullptr?headB:A->next;
B = B==nullptr?headA:B->next;
}
return A;
}
};
总结:
i)ListNode这个数据结构可以直接比较,来判断是不是一个节点,很方便。
8、 链表旋转(时差指针)
例题:旋转链表
代码:
思路1:双指针法
class Solution {
public:
ListNode* rotateRight(ListNode* head, int k) {
//算长度和取模
int length = 0;
ListNode* tmp = head;
while(tmp){
length++;
tmp = tmp->next;
}
if(k == 0 || length == 0){
return head;
}
k %= length;
//正式的业务逻辑
ListNode* fast = head, *slow =head;
for(int i = 0; i< k; i++){
fast = fast->next;
}
while(fast->next){
fast = fast->next;
slow = slow->next;
}
fast->next = head;
head = slow->next;
slow->next = nullptr;
return head;
}
};
思路2:闭合为环(对于链表类型的题目可能有意想不到的作用)
class Solution {
public:
ListNode* rotateRight(ListNode* head, int k) {
int length = 0;
ListNode* tmp = head, *ret, *last;
//算长度,需要一个last指针辅助
while(tmp){
length++;
last = tmp;
tmp = tmp->next;
}
//判断边界
if( k== 0 || length == 0){
return head;
}
//开始成环,找到节点然后断开
last->next = head;
k%= length;
for(int i = 0; i<length - 1- k; i++){
head = head->next;
}
ret = head->next;
head->next = nullptr;
return ret;
}
};
Day2总结
1. 双指针分为位置和速度不同的指针,其中速度不同的指针为快慢指针,比如:步长的快慢——步差指针,开始时间的快慢——时差指针...
2. STL很好用,需要重点学习。
3. dummy节点很有用,建议每次都加上。