diff --git a/README.md b/README.md index d0653064..809989bb 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ * [字符串:这道题目,使用库函数一行代码搞定](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA) * [字符串:简单的反转还不够!](https://mp.weixin.qq.com/s/XGSk1GyPWhfqj2g7Cb1Vgw) * [字符串:替换空格](https://mp.weixin.qq.com/s/t0A9C44zgM-RysAQV3GZpg) +* [字符串:花式反转还不够!](https://mp.weixin.qq.com/s/X3qpi2v5RSp08mO-W5Vicw) (持续更新中....) @@ -330,6 +331,24 @@ vector> levelOrder(TreeNode* root) { ``` +## 回溯算法 + +``` +backtracking() { + if (终止条件) { + 存放结果; + } + + for (选择:选择列表(可以想成树中节点孩子的数量)) { + 递归,处理节点; + backtracking(); + 回溯,撤销处理结果 + } +} + +``` + + 可以直接解决如下题目: * [0102.二叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0102.二叉树的层序遍历.md) @@ -439,6 +458,8 @@ int countNodes(TreeNode* root) { |[0841.钥匙和房间](https://github.com/youngyangyang04/leetcode/blob/master/problems/0841.钥匙和房间.md) |孤岛问题 |中等|**bfs** **dfs**| |[1047.删除字符串中的所有相邻重复项](https://github.com/youngyangyang04/leetcode/blob/master/problems/1047.删除字符串中的所有相邻重复项.md) |栈 |简单|**栈**| |[剑指Offer05.替换空格](https://github.com/youngyangyang04/leetcode/blob/master/problems/剑指Offer05.替换空格.md) |字符串 |简单|**双指针**| +|[ 剑指Offer58-I.翻转单词顺序](https://github.com/youngyangyang04/leetcode/blob/master/problems/剑指Offer05.替换空格.md) |字符串 |简单|**模拟/双指针**| +|[剑指Offer58-II.左旋转字符串](https://github.com/youngyangyang04/leetcode/blob/master/problems/剑指Offer58-II.左旋转字符串.md) |字符串 |简单|**反转操作**| |[面试题02.07.链表相交](https://github.com/youngyangyang04/leetcode/blob/master/problems/面试题02.07.链表相交.md) |链表 |简单|**模拟**| 持续更新中.... diff --git a/pics/257.二叉树的所有路径.png b/pics/257.二叉树的所有路径.png new file mode 100644 index 00000000..f46fc31a Binary files /dev/null and b/pics/257.二叉树的所有路径.png differ diff --git a/pics/51.N皇后.png b/pics/51.N皇后.png index 6410647d..5126a270 100644 Binary files a/pics/51.N皇后.png and b/pics/51.N皇后.png differ diff --git a/problems/0077.组合.md b/problems/0077.组合.md index 09367483..2000e956 100644 --- a/problems/0077.组合.md +++ b/problems/0077.组合.md @@ -21,25 +21,24 @@ ``` class Solution { private: - vector> result; - void backtracking(int n, int k, vector& vec, int startIndex) { + vector> result; // 存放符合条件结果的集合 + vector vec; // 用来存放符合条件结果 + void backtracking(int n, int k, int startIndex) { if (vec.size() == k) { result.push_back(vec); return; } // 这个for循环有讲究,组合的时候 要用startIndex,排列的时候就要从0开始 - // 这个过程好难理解,需要画图 for (int i = startIndex; i <= n; i++) { vec.push_back(i); - backtracking(n, k, vec, i + 1); + backtracking(n, k, i + 1); vec.pop_back(); } } public: vector> combine(int n, int k) { - vector vec; - backtracking(n, k, vec, 1); + backtracking(n, k, 1); return result; } }; diff --git a/problems/0257.二叉树的所有路径.md b/problems/0257.二叉树的所有路径.md new file mode 100644 index 00000000..411bf1e9 --- /dev/null +++ b/problems/0257.二叉树的所有路径.md @@ -0,0 +1,224 @@ +## 题目地址 +https://leetcode-cn.com/problems/binary-tree-paths/ + + +## 思路 + +首先要知道遍历二叉树有两种遍历方式:二叉树深度优先遍历和二叉树广度优先遍历,那么每种遍历方式下还有不同的顺序。如下所示: +* 二叉树深度优先遍历 + * 前序遍历: [0144.二叉树的前序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0144.二叉树的前序遍历.md) + * 后序遍历: [0145.二叉树的后序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0145.二叉树的后序遍历.md) + * 中序遍历: [0094.二叉树的中序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0094.二叉树的中序遍历.md) +* 二叉树广度优先遍历 + * 层序遍历:[0102.二叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0102.二叉树的层序遍历.md) + +这道题目要打印出根节点到叶子节点的所有路径,很明显广度优先遍历不合适,那么深度优先遍历中,应该选哪一种循序来遍历呢? + +**要打印路径,就要选前序遍历**,因为中序和后序遍历都不能打印出路径来。 + +一些同学可能代码都写出来,而且都提交通过了,却不知道自己用了哪一种遍历,以及那种顺序来遍历的。 + +前序遍历如题: + + + +确定了是前序遍历,那么就是中左右的顺序。前序遍历 框架如下: +``` +void traversal(TreeNode* cur, vector& vec) { + if (cur == NULL) return; + vec.push_back(cur->val); // 中 ,同时也是处理节点逻辑的地方 + traversal(cur->left, vec); // 左 + traversal(cur->right, vec); // 右 +} +``` + +我们先使用递归的方式,来做前序遍历。那么要知道递归和回溯就是一家的,本题也需要回溯。 + +1. 递归函数函数参数以及返回值 + +要传入跟节点,记录每一条路径的path,和存放结果集的result,这里递归不需要返回值,代码如下: + +``` +void traversal(TreeNode* cur, vector& path, vector& result) +``` + +2. 确定递归终止条件 + +在写递归的时候都习惯了这么写: + +``` +if (cur == NULL) { + 终止处理逻辑 +} +``` + +但是本题的终止条件这样写会很麻烦,因为本题要找到叶子节点,就开始结束的处理逻辑了(把路径放进result里)。 + +**那么什么时候算是找到了叶子节点?** 是当 cur不为空,其左右孩子都为空的时候,就找到叶子节点。 + +所以本题的终止条件是: +``` +if (cur->left == NULL && cur->right == NULL) { + 终止处理逻辑 +} +``` + +为什么没有判断cur是否为空呢,下文在讲解单层递归逻辑的时候会提到。 + +再来看一下终止处理的逻辑。 + +这里使用vector 结构来记录路径,所以要把路径转为string格式,在把这个string 放进 result里。 + +**那么为什么使用了vector 结构来记录路径呢?** 因为在下面处理单层递归逻辑的时候,要做回溯,使用vector方便来做回溯。 + +那么有的同学问了,我看有些人的代码也没有回溯啊。 + +其实是有的,只不过隐藏在 函数调用时的参数赋值里,下文我还会提到。 + +这里我们先使用vector 结构来记录路径,那么终止处理逻辑如下: + +``` +if (cur->left == NULL && cur->right == NULL) { + string sPath; + for (int i = 0; i < path.size() - 1; i++) { + sPath += to_string(path[i]); + sPath += "->"; + } + sPath += to_string(path[path.size() - 1]); + result.push_back(sPath); + return; +} +``` + +3. 确定单层递归逻辑 + +因为是前序遍历,需要先处理中间节点,中间节点就是我们要记录路径上的节点,先放进path中。 + +`path.push_back(cur->val);` + +然后是递归和回溯的过程,上面说过没有判断cur是否为空,那么在这里递归的时候,如果为空就不进行下一层递归了。 + +所以递归前要加上判断语句,下面要递归的节点是否为空,如下 + +``` +if (cur->left) { + traversal(cur->left, path, result); +} +if (cur->right) { + traversal(cur->right, path, result); +} +``` + +此时还没完,递归完,要做回溯啊,因为path 不能一直加入节点,它还要删节点,然后才能加入新的节点。 + +那么回溯要怎么回溯呢,一些同学会这么写,如下: + +``` +if (cur->left) { + traversal(cur->left, path, result); +} +if (cur->right) { + traversal(cur->right, path, result); +} +path.pop_back(); +``` + +这个回溯就要很大的问题,我们知道,**回溯和递归是一一对应的,有一个递归,就要有一个回溯**,这么写的话相当于把递归和回溯拆开了, 一个在花括号里,一个在花括号外。 + +**所以回溯要和递归永远在一起,世界上最遥远的距离是你在花括号里,而我在花括号外!** + +那么代码应该这么写: + +``` +if (cur->left) { + traversal(cur->left, path, result); + path.pop_back(); // 回溯 +} +if (cur->right) { + traversal(cur->right, path, result); + path.pop_back(); // 回溯 +} +``` + +那么本题整体代码如下: + +## C++代码第一种写法 + +``` +class Solution { +private: + + void traversal(TreeNode* cur, vector& path, vector& result) { + path.push_back(cur->val); + // 这才到了叶子节点 + if (cur->left == NULL && cur->right == NULL) { + string sPath; + for (int i = 0; i < path.size() - 1; i++) { + sPath += to_string(path[i]); + sPath += "->"; + } + sPath += to_string(path[path.size() - 1]); + result.push_back(sPath); + return; + } + if (cur->left) { + traversal(cur->left, path, result); + path.pop_back(); // 回溯 + } + if (cur->right) { + traversal(cur->right, path, result); + path.pop_back(); // 回溯 + } + } + +public: + vector binaryTreePaths(TreeNode* root) { + vector result; + vector path; + if (root == NULL) return result; + traversal(root, path, result); + return result; + } +}; +``` + +## C++代码第二种写法 +接下来我介绍另一种写法,如下写法就是一个标准的前序遍历的过程。 + +``` +class Solution { +private: + + void traversal(TreeNode* cur, string path, vector& result) { + path += to_string(cur->val); + if (cur->left == NULL && cur->right == NULL) { + result.push_back(path); + return; + } + if (cur->left) traversal(cur->left, path + "->", result); + if (cur->right) traversal(cur->right, path + "->", result); + } + +public: + vector binaryTreePaths(TreeNode* root) { + vector result; + string path; + if (root == NULL) return result; + traversal(root, path, result); + return result; + + } +}; +``` + +注意在函数定义的时候`void traversal(TreeNode* cur, string path, vector& result)` ,定义的是`string path`,说明每次都是复制赋值。 + +那么在如上代码中,貌似没有看到回溯的逻辑,其实不然,回溯就隐藏在`traversal(cur->left, path + "->", result);`中的 `path + "->"`。 每次函数调用完,path依然是没有+ 上"->" 的,这就是回溯了。 + +**综合以上,第二种写法更简洁,但是把很多重要的点隐藏在了代码细节里,第一种写法虽然代码多一些,但是每一个处理逻辑都完整的展现了出来。** + +至于还有非递归的方式,我在这篇题解[彻底吃透前中后序递归法(递归三部曲)和迭代法(不统一写法与统一写法)](https://leetcode-cn.com/problems/binary-tree-preorder-traversal/solution/dai-ma-sui-xiang-lu-chi-tou-qian-zhong-hou-xu-de-d/) 已经彻底介绍过了,感兴趣的同学可以去看一看。 + + + + diff --git a/problems/剑指Offer58-I.翻转单词顺序.md b/problems/剑指Offer58-I.翻转单词顺序.md new file mode 100644 index 00000000..5267fa11 --- /dev/null +++ b/problems/剑指Offer58-I.翻转单词顺序.md @@ -0,0 +1,2 @@ + +详见:[0151.翻转字符串里的单词](https://github.com/youngyangyang04/leetcode/blob/master/problems/0151.翻转字符串里的单词.md) diff --git a/problems/剑指Offer58-II.左旋转字符串.md b/problems/剑指Offer58-II.左旋转字符串.md new file mode 100644 index 00000000..d6972f81 --- /dev/null +++ b/problems/剑指Offer58-II.左旋转字符串.md @@ -0,0 +1,80 @@ +# 题目地址 +https://leetcode-cn.com/problems/zuo-xuan-zhuan-zi-fu-chuan-lcof/ + +> 反转个字符串还有这么多用处? + +# 题目:剑指Offer58-II.左旋转字符串 + +字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。 + + +示例 1: +输入: s = "abcdefg", k = 2 +输出: "cdefgab" + +示例 2: +输入: s = "lrloseumgh", k = 6 +输出: "umghlrlose" +  +限制: +1 <= k < s.length <= 10000 + +# 思路 + +为了让本题更有意义,提升一下本题难度:**不能申请额外空间,只能在本串上操作**。 + +不能使用额外空间的话,模拟在本串操作要实现左旋转字符串的功能还是有点困难的。 + + +那么我们可以想一下上一题目[字符串:花式反转还不够!](https://mp.weixin.qq.com/s/X3qpi2v5RSp08mO-W5Vicw)中讲过,使用整体反转+局部反转就可以实现,反转单词顺序的目的。 + +这道题目也非常类似,依然可以通过局部反转+整体反转 达到左旋转的目的。 + +具体步骤为: + +1. 反转区间为前n的子串 +2. 反转区间为n到末尾的子串 +3. 反转整个字符串 + +最后就可以得到左旋n的目的,而不用定义新的字符串,完全在本串上操作。 + +例如 :示例1中 输入:字符串abcdefg,n=2 + +1. 反转区间为前n的子串 : bacdefg +2. 反转区间为n到末尾的子串:bagfedc +3. 反转整个字符串:cdefgab + +最终得到左旋2个单元的字符串:cdefgab + +思路明确之后,那么代码实现就很简单了 + +# C++代码 + +``` +class Solution { +public: + string reverseLeftWords(string s, int n) { + reverse(s.begin(), s.begin() + n); + reverse(s.begin() + n, s.end()); + reverse(s.begin(), s.end()); + return s; + } +}; +``` +是不是发现这代码也太简单了,哈哈。 + +# 总结 + +此时我们已经反转好多次字符串了,来一起回顾一下吧。 + +在这篇文章[字符串:这道题目,使用库函数一行代码搞定](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA),第一次讲到反转一个字符串应该怎么做,使用了双指针法。 + +然后发现[字符串:简单的反转还不够!](https://mp.weixin.qq.com/s/XGSk1GyPWhfqj2g7Cb1Vgw),这里开始给反转加上了一些条件,当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章。 + +后来在[字符串:花式反转还不够!](https://mp.weixin.qq.com/s/X3qpi2v5RSp08mO-W5Vicw)中,要对一句话里的单词顺序进行反转,发现先整体反转再局部反转 是一个很妙的思路。 + +最后再讲到本地,本题则是先局部反转再 整体反转,与[字符串:花式反转还不够!](https://mp.weixin.qq.com/s/X3qpi2v5RSp08mO-W5Vicw)类似,但是也是一种新的思路。 + +好了,反转字符串一共就介绍到这里,相信大家此时对反转字符串的常见操作已经很了解了。 + +> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/面试题02.07.链表相交.md b/problems/面试题02.07.链表相交.md index 3c30c0dc..77024d7c 100644 --- a/problems/面试题02.07.链表相交.md +++ b/problems/面试题02.07.链表相交.md @@ -63,3 +63,4 @@ public: } }; ``` +> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。