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

一、双指针之单链表

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

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

代码

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节点很有用,建议每次都加上。

  • 34
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
好的,我知道了,下面是我的回答: 聚类算法是一种无监督学习算法,它可以将数据集中的数据按照某种规则进行分组,使得同一组内的数据相似度较高,不同组之间的数据相似度较低。聚类算法可以帮助我们对数据进行分类、分析和理解,同时也可以为其他机器学习任务提供数据预处理和特征提取的支持。 聚类算法的基本流程包括:确定聚类算法的目标函数、选择相似度度量方法、选择聚类算法、确定聚类的数量、进行聚类操作以及评估聚类效果。常见的聚类算法包括K-Means算法、层次聚类算法、密度聚类算法等。 K-Means算法是一种基于距离的聚类算法,它的基本思想是将数据集中的数据划分为K个簇,使得同一簇内的数据相似度较高,不同簇之间的数据相似度较低。K-Means算法的优点是计算复杂度较低,容易实现,但是需要预先指定簇的数量和初始聚类中心。 层次聚类算法是一种基于相似度的聚类算法,它的基本思想是不断合并数据集中相似度最高的数据,直到所有数据都被合并为一个簇或达到预先设定的簇的数量。层次聚类算法的优点是不需要预先指定簇的数量和初始聚类中心,但是计算复杂度较高。 密度聚类算法是一种基于密度的聚类算法,它的基本思想是将数据集中的数据划分为若干个密度相连的簇,不同簇之间的密度差距较大。密度聚类算法的优点是可以发现任意形状的簇,但是对于不同密度的簇分割效果不佳。 以上是聚类算法的基础知识,希望能对您有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值