【算法学习计划】回溯 -- 递归

目录

leetcode 面试题08.06.汉诺塔问题

leetcode 21.合并两个有序链表

leetcode 206.反转链表

leetcode 24.两两交换链表中的节点

leetcode 50. Pow(x, n)


本篇文章将是我们回溯专题的第一篇文章,在这里我先浅浅讲一下什么是回溯

其实就是递归,只不过递归的时候还有一个搜索的目的,也叫做深度优先遍历,dfs

所以大家对于递归不用太过于害怕,因为各位害怕的其实是递归展开图的复杂,至于递归本身,如果将其看做一个黑盒,从宏观角度去看待这个问题的话,那么递归其实清晰特别逻辑

接下来我们就用 5 道题目来带各位初识一下递归

(下文中的标题都是leedcode对应题目的链接)

leetcode 面试题08.06.汉诺塔问题

这道题目非常的简单

首先我们先从宏观看一下这道问题,我们要从A柱移动到C柱,那么我们就应该是将最后一个盘上面的所有盘都当成一个整体,然后就是将上面所有的盘移动到B柱,最后一个盘移动到C柱,最后再将一刚开始移动到B柱的盘全部移动到C柱

这其实就是重复子问题了,因为我们每一种情况都是将最下面上面的所有和最下面

所以代码就是:

class Solution {
public:
    void dfs(vector<int>& begin, vector<int>& mid, vector<int>& end, int n)
    {
        if(n == 1)
        {
            end.push_back(begin.back());
            begin.pop_back();
            return;
        }
        dfs(begin, end, mid, n-1);
        end.push_back(begin.back());
        begin.pop_back();
        dfs(mid, begin, end, n-1);
    }

    void hanota(vector<int>& A, vector<int>& B, vector<int>& C) 
    {
        dfs(A, B, C, A.size());
    }
};

我们来细细解析一下:

首先,if 是递归出口,也就是递归到这个条件的时候,就没有办法再进行递归了

放在这道题目上面就是,当A柱上面只有一个盘子的时候,我们就直接将这个盘放到C柱上面

接着第一个dfs代表将最下面那个盘上面的所有盘,将这些盘放到B柱上面

接着就是将最后一个盘移动到C柱上的工作

最后一个dfs就是将那 n-1 个盘全部递归式地放到C柱上面

leetcode 21.合并两个有序链表

这道题其实有更简单的解法,也就是一个一个节点的对比,设两个指针分别指向两个链表的头节点

对比完之后,再看两个指针还有哪个没有到结尾的,再将那一段结尾全部加进返回的结果里就可以了

但是这题也是可以用递归来做的,我们可以用这道题目锻炼以下我们的递归思维

首先我们要先找到重复子问题,因为只有找到了我们才知道怎么递归

假设上面那个链表节点的第一个小一点,那么我们就把这个节点先屏蔽

那么除开这个节点之后,我们的任务就变成了将剩下的所有节点全部合并

再拿开一个节点,还是将剩下的全部节点合并,我们的重复子问题就出现了

由于这是两个升序链表,所以我们可以直接改变原链表的指向

代码如下:

/**
 * 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* mergeTwoLists(ListNode* l1, ListNode* l2) 
    {
        if(!l1)return l2;
        if(!l2)return l1;

        if(l1->val < l2->val) l1->next = mergeTwoLists(l1->next, l2);
        else l2->next = mergeTwoLists(l1, l2->next);

        return l1->val < l2->val ? l1 : l2;
    }
};

最上面两个 if 是递归出口

下来的递归,就是,如果第一个小,那么我们就将第一个作为头节点,然后后面的递归函数会帮我们将后面的所有节点合并为有序的,然后我们第一个节点和他链接起来,最后返回第一个节点即可

第二个小的情况同理

leetcode 206.反转链表

这道反转链表我们可以用递归的方式完成

首先是重复子问题,当我们第一个节点排除的时候,我们剩下的任务就是将除了第一个之外的所有节点反转

那我们可以将第一个节点单独拿出来,让head->next放进递归中

接着这个递归函数过后,后面的链表肯定被反转好了,那么我们要做的其实就是让第二个节点的next指针指向第一个,接着第一个的指针指向空这样我们的数组就被反转了

最后我们要一直返回最后一个元素的指针,所以我们可以再碰到最后一个节点的时候,返回他自己,接着剩下所有的情况我们都那一个指针接受返回值,然后将这个指针一路返回回去

或可以搞一个全局指针,当碰到最后一个节点的时候赋值给这个指针,最后返回这个节点即可

代码如下:

/**
 * 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* reverseList(ListNode* head) 
    {
        if(head == nullptr || head->next == nullptr) return head;
        ListNode* ret = reverseList(head->next);//将head之后的链表全部反转
        head->next->next = head;// 将head放到反转后链表的最后面
        head->next = nullptr;
        return ret;
    }
};

leetcode 24.两两交换链表中的节点

这道题目和反转链表差不多,但是我们是将当前节点的下下个节点放入递归中,然后我们执行当前两个节点的交换逻辑

然后递归出口就是,当我们当前节点的下一个节点为空(只有一个,不是一对),或者当前节点就是空(刚好都是成双成对)

代码如下:

/**
 * 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* ret = swapPairs(head->next->next);
        ListNode* n1 = head;
        ListNode* n2 = head->next;
        n1->next = ret;
        n2->next = n1;
        return n2;
    }
};

leetcode 50. Pow(x, n)

题意很简单,其实就是求一个数的多少次方而已

但是我们直接遍历 n 次的话,一定会超时,因为这道题目的 n 最大是非常大的,包超时的

所以我们需要想一个对策

试想一下,我们需要求出 3^8,正常是直接乘 8 次,但是我们可以先将 3^4 求出来,这样我们只用循环 4 次,接下来就是结果乘两边就行了

而 3^4 可以再分为 3^2,这样循环下去

所以我们的重复子问题就出来了

因为我们可以判断以下当前数是否是奇数,如果是的话那就乘 待平方数(假设是3),然后再拿一个数来接收返回值,假设是ret

那么结果就是ret * ret * 3,而如果是偶数的话,就是 ret * ret

代码如下:

class Solution {
public:
    double dfs(double x, long n)
    {
        if(n == 0) return 1;
        if(n == 1) return x;

        double ret = dfs(x, n/2);
        if(n % 2 == 1) return ret*ret*x;
        return ret*ret;
    }
    double myPow(double x, int n) 
    {
        int flag = 0;
        long n1 = (long)n;
        if(n < 0)
        {
            flag = 1;
            n1 = -n1;
        }
        double ret = dfs(x, n1);
        if(flag == 1) return 1/ret;
        return ret;
    }
};

这篇文章到这里就结束了

我们这篇文章主要是讲一讲简单的递归,用一些简单递归来帮助大家消除恐惧的

接下来我们还会有更多递归系列的博客,如果觉得对你有帮助的可以关注一下!!~( ̄▽ ̄)~*