当前位置:   article > 正文

算法学习笔记Day2——双指针技巧之单链表_labuladong

labuladong

一、双指针之单链表

1、合并两个有序链表

分析:双指针技巧,双在哪? 两个链表一条一个指针,从头开始,每次比较大小,然后把小的一个节点赋值到新链表,并且对应的的指针移动。

例题: 合并两个有序链表

代码

  1. class Solution {
  2. public:
  3. ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
  4. ListNode* dum = new ListNode(0);
  5. ListNode* cur = dum;
  6. while(list1 != nullptr && list2 != nullptr){
  7. if(list1->val < list2->val){
  8. cur->next = list1;
  9. list1 = list1->next;
  10. }
  11. else{
  12. cur->next = list2;
  13. list2 = list2->next;
  14. }
  15. cur = cur->next;
  16. }
  17. cur->next = list1 != nullptr ? list1 : list2;
  18. return dum->next;
  19. }
  20. };

总结

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的,最后把他们连接起来。

例题: 分隔链表

代码

  1. class Solution {
  2. public:
  3. ListNode* partition(ListNode* head, int x) {
  4. ListNode* dum1 = new ListNode(0);
  5. ListNode* dum2 = new ListNode(0);
  6. ListNode* cur1 = dum1;
  7. ListNode* cur2 = dum2;
  8. while(head != nullptr){
  9. if(head->val < x){
  10. cur1->next = head;
  11. cur1 = cur1->next;
  12. }
  13. else{
  14. cur2->next = head;
  15. cur2 = cur2->next;
  16. }
  17. //断开的前进方式
  18. ListNode* tmp = head->next;
  19. head->next = nullptr;
  20. head = tmp;
  21. }
  22. cur1->next = dum2->next;
  23. return dum1->next;
  24. }
  25. };

总结

i). 断开前进方式,防止输出链表中出现环,其本质目的是为了每次赋值给cur1和cur2是一个节点而非节点及其后整段链表。

3、合并 k 个有序链表(STL的运用——priority_queue)

分析:这里很像合并两个升序链表,区别在于,两个节点的大小关系一下就可以比较出来,但是K个(并且不知道K具体大小)就不可能一下子判断出来,如果用普通的函数比较,需要传不定长的参数,所以这里需要借助优先级队列来判断。

优先级队列:辅助找出最小值的数据结构,其底层是用最小堆来实现的。

例题: 合并 K 个升序链表

代码

  1. class Solution {
  2. public:
  3. ListNode* mergeKLists(vector<ListNode*>& lists) {
  4. ListNode* dummy = new ListNode(0);
  5. ListNode* cur = dummy;
  6. auto cmp = [](ListNode* a, ListNode* b){
  7. return a->val > b->val;
  8. };
  9. priority_queue<ListNode*, vector<ListNode*>, decltype(cmp)> pq;
  10. for(auto head : lists){
  11. if(head) pq.push(head);
  12. }
  13. while(!pq.empty()){
  14. auto node = pq.top();
  15. pq.pop();
  16. if(node->next){
  17. pq.push(node->next);
  18. }
  19. cur->next = node;
  20. cur = cur->next;
  21. }
  22. return dummy->next;
  23. }
  24. };

总结

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了。

例题删除链表的倒数第 N 个结点

代码

  1. class Solution {
  2. public:
  3. ListNode* removeNthFromEnd(ListNode* head, int n) {
  4. ListNode* dummy = new ListNode(0, head);
  5. ListNode* fast = dummy;
  6. ListNode* slow = dummy;
  7. for(int i = 0; i<n; i++){
  8. fast = fast->next;
  9. }
  10. while(fast->next){
  11. slow = slow->next;
  12. fast = fast->next;
  13. }
  14. slow->next = slow->next->next;
  15. return dummy->next;
  16. }
  17. };

总结

i) 哨兵节点一定要加上,非常有用。

ii)最后返回用哨兵节点的next返回而不是原生的head,它们有轻微的差别。(因为改变的是dummy的next而不是head,当dummy->next为空时它不指向head)

iii) fast是用来定位slow位置的,最后赋值是没有用的,全靠slow的赋值来删除节点。一开始被例子所迷惑了,认为slow和fast中间的就是需要删除的节点,实际上是样本数量太少导致的巧合。

5、寻找单链表的中点(步差指针)

分析

平凡的思路:一个指针走到头后,新设立一个指针,然后相向而行

巧妙的思路:两个指针同时走,一个走两步,一个走一步,当fast走到头时,slow就到了重点(细节,比如奇偶问题,在搭建算法总体架构时不用考虑)

例题: 链表的中间结点

代码

  1. class Solution {
  2. public:
  3. ListNode* middleNode(ListNode* head) {
  4. ListNode* dummy = new ListNode(0, head);
  5. ListNode* fast = dummy;
  6. ListNode* slow = dummy;
  7. while(fast){
  8. slow = slow->next;
  9. fast = fast->next;
  10. if(fast){
  11. fast = fast->next;
  12. }
  13. }
  14. return slow;
  15. }
  16. };

总结

i) 边界情况处理用奇偶举例来写出一个通用的代码。

ii)依然使用哨兵节点dummy

6、判断单链表是否包含环并找出环起点(画图+对结构的理解)

分析

一对快慢指针,步长为1、2,如果有环那么他们会相遇.

问题1:如果不会相遇,什么时候判断结束?答:当快指针走到头的时候。

问题2:如何得到环的起点?答:当相遇后,只要把其中一个指针设置为head,然后同步步长为1,再次相遇时,就得到了环起点。

例题: 环形链表 II

代码

  1. class Solution {
  2. public:
  3. ListNode *detectCycle(ListNode *head) {
  4. ListNode* fast = head;
  5. ListNode* slow = head;
  6. while(fast){
  7. slow = slow->next;
  8. if(fast->next == nullptr){
  9. return nullptr;
  10. }
  11. fast = fast->next->next;
  12. if(fast == slow){
  13. slow = head;
  14. while(slow != fast){
  15. slow = slow->next;
  16. fast = fast->next;
  17. }
  18. return fast;
  19. }
  20. }
  21. return nullptr;
  22. }
  23. };

总结

i)要学会画图分析,不要空想

7、判断两个单链表是否相交并找出交点(对偶法)

分析

我的思路:(Trivial的思路)先两个一起走,其中一个走到头后,算另一个还差多少,然后这个差算出来就可以让这两个链表同步,然后再从头对齐一起走,如果再到头之前,他们的listnode能一样就说明有相交的。

巧妙的思路:(对偶法)把两个链表拼接在一起,让他们同时结束。如果结束时,两个指针都是null,说明无相交,否则出来的指针就是相交起点。

两个思路的共同点都是让链表同步!

但是第一个思路处理起来会很麻烦,因为不知道谁先结束,所以需要对两个链表进行特殊的处理。

用对偶法可以很简便的化解这种麻烦。

例题: 相交链表

代码

我的算法: 

  1. class Solution {
  2. public:
  3. ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
  4. ListNode* ans = nullptr;
  5. ListNode* A = headA;
  6. ListNode* B = headB;
  7. while(A != nullptr && B != nullptr){
  8. A = A->next;
  9. B = B->next;
  10. }
  11. //不知道谁先结束需要做的处理
  12. ListNode* tmp = A==nullptr?B:A;
  13. ListNode* tmphead = A==nullptr?headB:headA;
  14. ListNode* tmphead2 = A==nullptr?headA:headB;
  15. //
  16. int cnt = 0;
  17. while(tmp != nullptr){
  18. cnt++;
  19. tmp = tmp->next;
  20. }
  21. for(int i = 0; i<cnt; i++){
  22. tmphead = tmphead->next;
  23. }
  24. while(tmphead){
  25. if(tmphead != tmphead2){
  26. tmphead = tmphead->next;
  27. tmphead2 = tmphead2->next;
  28. }
  29. else{
  30. ans = tmphead;
  31. break;
  32. }
  33. }
  34. return ans;
  35. }
  36. };

 拼接算法:

  1. class Solution {
  2. public:
  3. ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
  4. if(headA == nullptr || headB == nullptr){
  5. return nullptr;
  6. }
  7. ListNode* A = headA;
  8. ListNode* B = headB;
  9. while(A!=B){
  10. A = A==nullptr?headB:A->next;
  11. B = B==nullptr?headA:B->next;
  12. }
  13. return A;
  14. }
  15. };

总结

i)ListNode这个数据结构可以直接比较,来判断是不是一个节点,很方便。

8、 链表旋转(时差指针)

例题旋转链表

代码

思路1:双指针法

  1. class Solution {
  2. public:
  3. ListNode* rotateRight(ListNode* head, int k) {
  4. //算长度和取模
  5. int length = 0;
  6. ListNode* tmp = head;
  7. while(tmp){
  8. length++;
  9. tmp = tmp->next;
  10. }
  11. if(k == 0 || length == 0){
  12. return head;
  13. }
  14. k %= length;
  15. //正式的业务逻辑
  16. ListNode* fast = head, *slow =head;
  17. for(int i = 0; i< k; i++){
  18. fast = fast->next;
  19. }
  20. while(fast->next){
  21. fast = fast->next;
  22. slow = slow->next;
  23. }
  24. fast->next = head;
  25. head = slow->next;
  26. slow->next = nullptr;
  27. return head;
  28. }
  29. };

思路2:闭合为环(对于链表类型的题目可能有意想不到的作用)

  1. class Solution {
  2. public:
  3. ListNode* rotateRight(ListNode* head, int k) {
  4. int length = 0;
  5. ListNode* tmp = head, *ret, *last;
  6. //算长度,需要一个last指针辅助
  7. while(tmp){
  8. length++;
  9. last = tmp;
  10. tmp = tmp->next;
  11. }
  12. //判断边界
  13. if( k== 0 || length == 0){
  14. return head;
  15. }
  16. //开始成环,找到节点然后断开
  17. last->next = head;
  18. k%= length;
  19. for(int i = 0; i<length - 1- k; i++){
  20. head = head->next;
  21. }
  22. ret = head->next;
  23. head->next = nullptr;
  24. return ret;
  25. }
  26. };

 

Day2总结

1. 双指针分为位置和速度不同的指针,其中速度不同的指针为快慢指针,比如:步长的快慢——步差指针,开始时间的快慢——时差指针...

2. STL很好用,需要重点学习。

3. dummy节点很有用,建议每次都加上。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/933775
推荐阅读
相关标签
  

闽ICP备14008679号