文章目录

  • 前言
    • 回溯算法:
  • 一、合并两个有序链表(简单,可略过)
    • 迭代遍历
    • 一开始没有想到的递归解法
  • 二、反转链表
    • 迭代遍历(头插法):
    • 递归:
  • 三、组合
    • 回溯:
  • 四、全排列
    • 回溯(交换):
    • 回溯:
  • 五、字母大小写全排列
    • 回溯:
  • 总结

前言

Leetcode算法系列:https://leetcode-cn.com/study-plan/algorithms/?progress=njjhkd2

简单总结一下递归回溯相关的算法题:

回溯算法:

回溯思想个人觉得还是挺好理解,但目前理解的还是不够深|><|。以下摘自百度百科:

回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。八皇后问题就是回溯算法的典型,第一步按照顺序放一个皇后,然后第二步符合要求放第2个皇后,如果没有位置符合要求,那么就要改变第一个皇后的位置,重新放第2个皇后的位置,直到找到符合条件的位置就可以了。回溯在迷宫搜索中使用很常见,就是这条路走不通,然后返回前一个路口,继续下一条路。回溯算法说白了就是穷举法。不过回溯算法使用剪枝函数,剪去一些不可能到达 最终状态(即答案状态)的节点,从而减少状态空间树节点的生成。

回溯法是一个既带有系统性又带有跳跃性的的搜索算法。它在包含问题的所有解的解空间树中,按照深度优先的策略,从根结点出发搜索解空间树。算法搜索至解空间树的任一结点时,总是先判断该结点是否肯定不包含问题的解。如果肯定不包含,则跳过对以该结点为根的子树的系统搜索,逐层向其祖先结点回溯。否则,进入该子树,继续按深度优先的策略进行搜索。回溯法在用来求问题的所有解时,要回溯到根,且根结点的所有子树都已被搜索遍才结束。而回溯法在用来求问题的任一解时,只要搜索到问题的一个解就可以结束。这种以深度优先的方式系统地搜索问题的解的算法称为回溯法,它适用于解一些组合数较大的问题。

一、合并两个有序链表(简单,可略过)

题目链接:https://leetcode-cn.com/problems/merge-two-sorted-lists/

题目描述:将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

迭代遍历

直接采用归并思想既可。参考算法如下:

    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {ListNode realh=new ListNode(0);//自定义一个头节点。ListNode tmp=realh;while(list1!=null&&list2!=null){if(list1.val<list2.val){tmp.next=list1;list1=list1.next;}else{tmp.next=list2;list2=list2.next;}tmp=tmp.next;}tmp.next=list1==null?list2:list1;return realh.next;}

一开始没有想到的递归解法

当发现也可以采用递归来做的时候,试了一下,先做终止条件判断,递归主体判断当前两个结点值之间的大小,值小的结点需要返回,注意要在返回之前完成下一轮的递归合并。参考算法如下:

    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {if(list1==null)return list2;if(list2==null)return list1;if(list1.val<list2.val){list1.next=merge(list1.next,list2);return list1;}else{list2.next=merge(list1,list2.next);return list2;}}

二、反转链表

题目链接:https://leetcode-cn.com/problems/reverse-linked-list/

题目描述:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

迭代遍历(头插法):

参考算法如下:

    public ListNode reverseList(ListNode head) {if(head==null)return head;ListNode realh=new ListNode(0),tmp;//仍建立一个头节点来保存最终的链表while(head!=null){tmp=head;//tmp 为待插入的结点,插入位置为 realh 的next处head=head.next; //插入之前需要保证下一次迭代元素访问的正确性tmp.next=realh.next;realh.next=tmp;}return realh.next;}

递归:

递归稍微有点难理解,可参考题解。下面算法的大体思路为:递归终止条件有两个,当前结点为 null 或当前结点的 next 为 null。1,当前结点为 null 仅针对 head 为一个空链表的情况,应返回 null;2,当前结点的 next 为 null 才是递归的终止条件。在对链表的递归遍历中,每一层递归中,我们都可以顺序的得到链表的每一个结点。

下面代码做的是从从后往前将链表逆序,由于我们的终止条件,第一次调整指针是在倒数第二个结点 a处(假设 a->next = b; b->next = null),此时可采用操作: a->next->next=a; a->next =null; 来将当前仅有两个元素的链表逆序。在原链表中依次往前,对倒数第三个结点、第四个…也是如此,最后返回结点 b 为新链表的第一个结点。

    public ListNode reverseList(ListNode head) {if(head==null||head.next==null)return head;ListNode tmp=reverseList(head.next);  //tmp 为原链表最后一个结点,新链表的第一个结点。head.next.next=head; //这两行代码从倒数第二个结点处开始调整结点的指向关系,一步步进行逆序。head.next=null;return tmp;}

目前仅有的粗浅印象为:从后往前对链表结点进行操作需要将递归语句放在操作语句的前面;从前往后按照前后顺序操作,需要将操作语句放在递归语句的前面。

三、组合

题目链接:https://leetcode-cn.com/problems/combinations/

题目描述:给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。

回溯:

可参考对应的官方题解,初始的算法如下:

1:算法的退出条件代码块b放在保存合适的组合情况代码块a后面的原因在于代码块a在代码块c的前面,即先进行访问、判定操作,后进行递归遍历。对于 n,k = 4,2 的情况,在 start 为 5 的递归遍历中,代码块 a 先判断的是包含 4 的情况,然后再准备添加 5。 若将代码块 b 放在 a 的前面,需要更改 b 中的 if 语句为 if(start>end+1)。

    //对 n 个数的组合,假设每个数均有两种状态,选取和不选取,每一种成功的情况发生在选取的数有 k 个的情况,保存每一个可能的情况。List<List<Integer>> ans=new LinkedList<>();List<Integer> tmp=new LinkedList<>();public List<List<Integer>> combine(int n, int k) {solve(1,n,k);return ans;}public void solve(int start,int end,int k){if(tmp.size()==k){ //碰到每一种成功的情况时应该保存                      aans.add(new LinkedList<>(tmp));return;}//start == end,此时表示到达最后一个元素处                                bif(start>end)return;   //设置算法的退出条件tmp.add(start); //选取当前值 start 的情况                                 c solve(start+1,end,k);tmp.remove(tmp.size()-1); //不选取的情况solve(start+1,end,k);}

当 n=4,k=2 时的执行结果如下(在 solve 方法的第一行添加 System.out.println(tmp);):
                                                []
                                                [1]
                                                [1, 2]
                                                [1]
                                                [1, 3]
                                                [1]
                                                [1, 4]
                                                [1]
                                                []
                                                [2]
                                                [2, 3]
                                                [2]
                                                [2, 4]
                                                [2]
                                                []
                                                [3]
                                                [3, 4]
                                                [3]
                                                []
                                                [4]
                                                []
      2:进一步修改,添加剪枝条件并在此时可以删除代码块 b 的算法参考如下:

现在我们知道,代码块 a 中判断的元素范围不包括当前的 start ,而 end-start+1 表示还未添加进 tmp 集合的的剩余元素,如果 tmp 中元素数量加上剩余元素数量之和小于 k 的话,绝对不可能出现成功的情况,所以程序可直接返回,不需要进行冗余的判断。

并且在此时,代码块 b 可以删除,它不会再被执行。原因在于 b 针对的是当前 tmp 元素数量达不到 k ,且即将要扩大区间到 n+1 的时候,作为此时的边界条件,程序应该返回。

而新加的代码块 d,目的是为了避免没必要的递归运算,即剪枝。那么 b 表示的返回条件是否被 d 囊括呢?答案是肯定的。假设对于 n=4,k=2 的情况,当前 tmp 中的元素为 1,start 为 5 时,此时在 d 中程序直接就返回了(即代码块 b 的作用)。

b 针对的边界条件:如果当前 tmp 元素数量达不到 k ,且即将要扩大区间到 n+1 的时候,此时 end-start+1 的值为 0,而 tmp.size() 又是小于 k 的,在 d 中程序也就直接返回了。 所以代码块 b 可删除掉。

    List<List<Integer>> ans=new LinkedList<>();List<Integer> tmp=new LinkedList<>();public List<List<Integer>> combine(int n, int k) {solve(1,n,k);return ans;}public void solve(int start,int end,int k){if(tmp.size()+(end-start+1)<k)                                          dreturn;if(tmp.size()==k){ //碰到每一种成功的情况时应该保存                      aans.add(new LinkedList<>(tmp));return;}
/*//start == end,此时表示到达最后一个元素处                               bif(start>end)return;   //设置算法的退出条件
*/tmp.add(start); //选取当前值 start 的情况                               c solve(start+1,end,k);tmp.remove(tmp.size()-1); //不选取的情况solve(start+1,end,k);}

四、全排列

题目链接:https://leetcode-cn.com/problems/permutations/

题目描述:给定一个不含重复数字的数组 nums ,返回其所有可能的全排列 。你可以 按任意顺序 返回答案。

回溯(交换):

先将初始数组保存到 tmp 列表中,方便直接采用 swap 方法来交换。大体思想是:假设数组元素为{1,2,3,4},全排列的顺序为:先得到以 1 开头的所有排列,然后是 2,3,4;假如在以 1 开头的排列中,先得到其余的 2,3,4开头的排列;假如在以1 ,2 开头的排列,分别得到 3,4 开头的排列;假如在以 1,2,3开头的排列中,最后一个数只能是 4。

    ArrayList<Integer> tmp=new ArrayList<>();List<List<Integer>> ans=new LinkedList<List<Integer>>();//用交换的方法做!public List<List<Integer>> permute(int[] nums) {for (int num : nums)tmp.add(num);solve(0,nums.length-1);return ans;}public void solve(int cur,int end) {if (cur == end) {// cur 表示当前的元素下标ans.add(new ArrayList<>(tmp));return;}else{for(int i=cur;i<=end;i++){Collections.swap(tmp,cur,i);//交换solve(cur+1,end);Collections.swap(tmp,cur,i);//复位}}}

回溯:

原文链接为对应题解:https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liweiw/ 不同于交换,这里为依次选择每一个元素。见下图:

      对应的代码为(稍加修改后):

    LinkedList<Integer> tmp=new LinkedList<>();List<List<Integer>> ans=new LinkedList<>();boolean[] flag;public List<List<Integer>> permute(int[] nums) {int len=nums.length;flag=new boolean[len];//由于每次选择一个元素之后,该元素就不能再被选取了,所以需要标记状态solve(nums,len-1);return ans;}public void solve(int[] nums,int end) {if(tmp.size()==end+1){//得到一次排列时,保存结果ans.add(new LinkedList<>(tmp));return;}for(int i=0;i<=end;i++){//依次选择每一个元素,由于有标记数组存在,所以此处循环每次都从下标 0 开始。if(flag[i]==false){tmp.addLast(nums[i]);flag[i]=true;solve(nums,end);flag[i]=false;tmp.removeLast();}}}

五、字母大小写全排列

题目链接:https://leetcode-cn.com/problems/letter-case-permutation/

题目描述:给定一个字符串 s ,通过将字符串 s 中的每个字母转变大小写,我们可以获得一个新的字符串。

返回 所有可能得到的字符串集合 。以 任意顺序 返回输出。

回溯:

这道题和求组合的思路有些类似,组合是一个元素选或不选,本体是一个字母是否发生的大小写转换,然后给出所有的字符串集合。参考算法如下:

    List<String> ans=new LinkedList<>();public List<String> letterCasePermutation(String s) {char[] ss=s.toCharArray();int start=0,end=ss.length-1;solve(ss,start,end);return ans;}public void solve(char[] ss,int start,int end){if(start>end){ans.add(new String(ss));return;}if(ss[start]>='0'&&ss[start]<='9')solve(ss,start+1,end);  //数字跳过else{change(ss,start);solve(ss,start+1,end);change(ss,start);solve(ss,start+1,end);} }public void change(char[] ss, int i){//char tmp='a'-'A';if(ss[i]<='z'&&ss[i]>='a')ss[i]-=('a'-'A');else if(ss[i]<='Z'&&ss[i]>='A')ss[i]+=('a'-'A');}

总结

LeetCode算法题8:递归和回溯1相关推荐

  1. leetcode算法题--完全平方数★

    原题链接:https://leetcode-cn.com/problems/perfect-squares/ 相关题目:leetcode算法题–零钱兑换 1.回溯法 就是暴力法,套路就是递归,但是有很 ...

  2. leetcode算法题--不同的二叉搜索树

    原题链接:https://leetcode-cn.com/problems/unique-binary-search-trees/ 相关题目:leetcode算法题--不同的二叉搜索树 II 1.递归 ...

  3. LeetCode算法题-Minimum Depth of Binary Tree(Java实现)

    这是悦乐书的第168次更新,第170篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第27题(顺位题号是111).给定二叉树,找到它的最小深度.最小深度是沿从根节点到最近的 ...

  4. LeetCode算法题-Factorial Trailing Zeroes(Java实现)

    这是悦乐书的第183次更新,第185篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第42题(顺位题号是172).给定一个整数n,返回n!中的尾随零数.例如: 输入:3 输 ...

  5. LeetCode算法题整理(200题左右)

    目录 前言 一.树(17) 1.1.后序遍历 1.2.层次遍历 1.3.中序 1.4.前序 二.回溯(20) 2.1.普通回溯 2.2.线性回溯:组合.排列.子集.分割 2.3.矩阵回溯 三.二分查找 ...

  6. Leetcode算法题:两个有序数组求中位数

    Leetcode算法题:两个有序数组求中位数 要求时间复杂度为O(log(m+n)) 思路: 暴力解决:合并数组并排序,简单且一定能实现,时间复杂度O(m+n) 由于两个数组已经排好序,可一边排序一边 ...

  7. LeetCode算法题-Nth Digit(Java实现)

    这是悦乐书的第215次更新,第228篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第83题(顺位题号是400).找到无限整数序列的第n个数字1,2,3,4,5,6,7,8 ...

  8. LeetCode算法题-Reverse Linked List(Java实现)

    这是悦乐书的第192次更新,第195篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第51题(顺位题号是206).反转单链表.例如: 输入:1-> 2-> 3- ...

  9. LeetCode算法题-Convert a Number to Hexadecimal(Java实现)

    这是悦乐书的第219次更新,第231篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第86题(顺位题号是405).给定一个整数,写一个算法将其转换为十六进制.对于负整数,使 ...

  10. leetcode算法题--零钱兑换

    原题链接:https://leetcode-cn.com/problems/coin-change/ 相关题目:leetcode算法题–完全平方数★ 动态规划 dp[i] i从0到amount,dp[ ...

最新文章

  1. 阿里软件测试工程师手把手教学——自动化测试报告太丑,怎么办?
  2. 量子计算赛道上的巨头拉锯战
  3. 将数字转化为特殊符号的密码
  4. PAT甲级1113 Integer Set Partition:[C++题解]贪心
  5. 卷积神经网络初探 | 数据科学家联盟 http://dataunion.org/20942.html
  6. Shell 变量的作用域
  7. ElementUI改变el-table的表头颜色以及各行的颜色
  8. c#中中读取嵌入和使用资源文件的方法
  9. 【BZOJ3218】a+b problem (最小割 + 主席树)
  10. 爱因斯坦和高中几何问题
  11. 【MySQL】MySQL 一些 使用 案例
  12. iOS 开发 - 绘制辉光效果
  13. css工程师技巧,web前端工程师必须掌握的技巧–CSS Sprites技术(附基础操作教程)...
  14. php笔刷怎么安装,ps笔刷怎么用?PS笔刷使用教程
  15. rapidxml解析xml文档
  16. 多媒体计算机主机系统,多媒体计算机系统的组成
  17. python提取句子_关于python:从句子中提取介词短语
  18. 网站打开速度的查询 测试
  19. 党² - 李超线段树
  20. 作业20171127-4 事后诸葛亮会议

热门文章

  1. 查询每日规定时间段内的记录
  2. P1516 青蛙的约会
  3. 瀑布模型(经典的生命周期模型)
  4. 【转】hex和bin文件格式的区别
  5. Sa身份登陆SQL SERVER失败的解决方案
  6. Python: UTF8转换代码实例
  7. VC2008 忽然无法调试DLL的解决方法
  8. UVA11019KMP(二维矩阵匹配出现次数)
  9. 操作系统原理第一章:操作系统概述
  10. 【Linux 内核 内存管理】优化内存屏障 ① ( barrier 优化屏障 | 编译器优化 | CPU 执行优化 | 优化屏障源码 barrier 宏 )