diff --git a/README.md b/README.md index 1290b29f..75e5ef03 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,157 @@ (待补充.....) +# 算法模板 + +## 二分法 +(待补充....) + +## KMP + +(待补充....) + +## 二叉树 + +### 深度优先遍历(递归) + +前序遍历(中左右) +``` +void traversal(TreeNode* cur, vector& vec) { + if (cur == NULL) return; + vec.push_back(cur->val); // 中 ,同时也是处理节点逻辑的地方 + traversal(cur->left, vec); // 左 + traversal(cur->right, vec); // 右 +} +``` +中序遍历(左中右) +``` +void traversal(TreeNode* cur, vector& vec) { + if (cur == NULL) return; + traversal(cur->left, vec); // 左 + vec.push_back(cur->val); // 中 ,同时也是处理节点逻辑的地方 + traversal(cur->right, vec); // 右 +} +``` +中序遍历(中左右) +``` +void traversal(TreeNode* cur, vector& vec) { + if (cur == NULL) return; + vec.push_back(cur->val); // 中 ,同时也是处理节点逻辑的地方 + traversal(cur->left, vec); // 左 + traversal(cur->right, vec); // 右 +} +``` + +### 深度优先遍历:迭代法(使用栈模拟递归) + +相关题解:[0094.二叉树的中序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0094.二叉树的中序遍历.md) + +前序遍历(中左右) +``` +vector preorderTraversal(TreeNode* root) { + vector result; + stack st; + if (root != NULL) st.push(root); + while (!st.empty()) { + TreeNode* node = st.top(); + if (node != NULL) { + st.pop(); + if (node->right) st.push(node->right); // 右 + if (node->left) st.push(node->left); // 左 + st.push(node); // 中 + st.push(NULL); + } else { + st.pop(); + node = st.top(); + st.pop(); + result.push_back(node->val); // 节点处理逻辑 + } + } + return result; +} + +``` + +中序遍历(左中右) +``` +vector inorderTraversal(TreeNode* root) { + vector result; // 存放中序遍历的元素 + stack st; + if (root != NULL) st.push(root); + while (!st.empty()) { + TreeNode* node = st.top(); + if (node != NULL) { + st.pop(); + if (node->right) st.push(node->right); // 右 + st.push(node); // 中 + st.push(NULL); + if (node->left) st.push(node->left); // 左 + } else { + st.pop(); + node = st.top(); + st.pop(); + result.push_back(node->val); // 节点处理逻辑 + } + } + return result; +} +``` + +后序遍历(左右中) +``` +vector postorderTraversal(TreeNode* root) { + vector result; + stack st; + if (root != NULL) st.push(root); + while (!st.empty()) { + TreeNode* node = st.top(); + if (node != NULL) { + st.pop(); + st.push(node); // 中 + st.push(NULL); + if (node->right) st.push(node->right); // 右 + if (node->left) st.push(node->left); // 左 + + } else { + st.pop(); + node = st.top(); + st.pop(); + result.push_back(node->val); // 节点处理逻辑 + } + } + return result; +} +``` +### 广度优先遍历(队列) + +相关题解:[0102.二叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0102.二叉树的层序遍历.md) + +``` +vector> levelOrder(TreeNode* root) { + queue que; + if (root != NULL) que.push(root); + vector> result; + while (!que.empty()) { + int size = que.size(); + vector vec; + for (int i = 0; i < size; i++) {// 这里一定要使用固定大小size,不要使用que.size() + TreeNode* node = que.front(); + que.pop(); + vec.push_back(node->val); // 节点处理的逻辑 + if (node->left) que.push(node->left); + if (node->right) que.push(node->right); + } + result.push_back(vec); + } + return result; +} + +``` + +### 二叉树深度 + +### 二叉树节点数量 + # LeetCode 最强题解: |题目 | 类型 | 难度 | 解题方法 | diff --git a/problems/0094.二叉树的中序遍历.md b/problems/0094.二叉树的中序遍历.md index 75b3716e..212f5862 100644 --- a/problems/0094.二叉树的中序遍历.md +++ b/problems/0094.二叉树的中序遍历.md @@ -3,6 +3,9 @@ https://leetcode-cn.com/problems/binary-tree-inorder-traversal/ ## 思路 + + + 094.二叉树的中序遍历 144.二叉树的前序遍历 145.二叉树的后序遍历 diff --git a/problems/0101.对称二叉树.md b/problems/0101.对称二叉树.md index b872017b..55ca224b 100644 --- a/problems/0101.对称二叉树.md +++ b/problems/0101.对称二叉树.md @@ -3,7 +3,110 @@ https://leetcode-cn.com/problems/symmetric-tree/ ## 思路 -这是考察二叉树基本操作的经典题目,递归的方式相对好理解一些,迭代的方法 我看大家清一色使用队列,其实使用栈也是可以的,只不过遍历的顺序不同而已,关键是要理解只要是对称比较就可以了,遍历的顺序无所谓的。 +这是考察二叉树基本操作的经典题目,递归的方式相对好理解一些,迭代法看大家清一色使用队列,其实使用栈也是可以的,只不过遍历的顺序不同而已,关键是要理解只要是对称比较就可以了,遍历的顺序无所谓的。 + + +### 递归法 + +#### 递归三部曲 + +* 确定递归函数的参数和返回值 +* 确定终止条件 +* 确定单层递归的逻辑 + +##### 确定递归函数的参数和返回值 + +判断左右孩子是否对称,所以传入的参数为左指针和右指针,那么就返回是否是对称的就可以了,所以返回值是布尔类型。 + +代码如下: +``` +bool compare(TreeNode* left, TreeNode* right) +``` + +##### 确定终止条件 + +* 左孩子为空,右孩子不为空,不对称,return false +* 左不为空,右为空,不对称 return false +* 左右都为空,对称,返回true +* 左右都不为空,比较节点数值,不相同就return false + +代码如下: +``` +if (left == NULL && right != NULL) return false; +else if (left != NULL && right == NULL) return false; +else if (left == NULL && right == NULL) return true; +else if (left->val != right->val) return false; +``` + +##### 确定单层递归的逻辑 + +* 比较二叉树外侧是否对称:传入的是左孩子的左指针,右孩子的右指针。 +* 比较内测是否对称,传入左孩子的右指针,右孩子的左指针。 +* 如果左右都对称就返回true ,有一侧不对称就返回false 。 + +代码如下: + +``` +bool outside = compare(left->left, right->right); +bool inside = compare(left->right, right->left); +return outside && inside; +``` + +这样递归的C++代码就写出来了,如下: + +``` +class Solution { +public: + bool compare(TreeNode* left, TreeNode* right) { + if (left == NULL && right != NULL) return false; + else if (left != NULL && right == NULL) return false; + else if (left == NULL && right == NULL) return true; + else if (left->val != right->val) return false; + else return compare(left->left, right->right) && compare(left->right, right->left); + + } + bool isSymmetric(TreeNode* root) { + if (root == NULL) return true; + return compare(root->left, root->right); + } +}; +``` + +### 迭代法 + +通过队列来判断二叉树内侧和外侧是否相等,如动画所示: + + + +代码如下: + +``` +class Solution { +public: + bool isSymmetric(TreeNode* root) { + if (root == NULL) return true; + queue que; + que.push(root->left); + que.push(root->right); + while (!que.empty()) { + TreeNode* leftNode = que.front(); que.pop(); + TreeNode* rightNode = que.front(); que.pop(); + if (!leftNode && !rightNode) { + continue; + } + if ((!leftNode || !rightNode || (leftNode->val != rightNode->val))) { + return false; + } + que.push(leftNode->left); + que.push(rightNode->right); + que.push(leftNode->right); + que.push(rightNode->left); + } + return true; + } +}; +``` +其实使用栈也是可以的,只要把队列原封不动的改成栈就可以了,我下面也给出了代码。 ## C++代码 diff --git a/problems/0104.二叉树的最大深度.md b/problems/0104.二叉树的最大深度.md index d4c052b9..528441c3 100644 --- a/problems/0104.二叉树的最大深度.md +++ b/problems/0104.二叉树的最大深度.md @@ -21,7 +21,7 @@ public: }; ``` -### BFS +### 迭代法 ``` class Solution { diff --git a/problems/0111.二叉树的最小深度.md b/problems/0111.二叉树的最小深度.md index 1767e71b..5b4214e3 100644 --- a/problems/0111.二叉树的最小深度.md +++ b/problems/0111.二叉树的最小深度.md @@ -29,7 +29,7 @@ public: ``` -### BFS +### 迭代法 ``` class Solution { public: diff --git a/problems/0142.环形链表II.md b/problems/0142.环形链表II.md index 701d2236..6a5abce7 100644 --- a/problems/0142.环形链表II.md +++ b/problems/0142.环形链表II.md @@ -5,7 +5,12 @@ https://leetcode-cn.com/problems/linked-list-cycle-ii/ 这道题目,不仅考察对链表的操作,而且还需要一些数学运算。 -**首先如何判断链表有环呢** +主要考察两知识点: + +* 判断链表是否环 +* 如果有环,如何找到这个环的入口 + +### **首先如何判断链表有环呢** 可以使用快慢指针法, 分别定义 fast 和 slow指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。 @@ -26,7 +31,9 @@ fast和slow各自再走一步, fast和slow就相遇了 这是因为fast是走两步,slow是走一步,**其实相对于slow来说,fast是一个节点一个节点的靠近slow的**,所以fast一定可以和slow重合。 -此时我们已经可以判断链表是否有环了,那么接下来要找这个环的入口了 +### 如果有环,如何找到这个环的入口 + +**此时我们已经可以判断链表是否有环了,那么接下来要找这个环的入口了** 假设从头结点到环形入口节点 的节点数为x。 diff --git a/problems/0144.二叉树的前序遍历.md b/problems/0144.二叉树的前序遍历.md index cb3f9b31..092a4b3b 100644 --- a/problems/0144.二叉树的前序遍历.md +++ b/problems/0144.二叉树的前序遍历.md @@ -3,13 +3,215 @@ https://leetcode-cn.com/problems/binary-tree-preorder-traversal/ ## 思路 -094.二叉树的中序遍历 -144.二叉树的前序遍历 -145.二叉树的后序遍历 -建议一起做一下 +这篇文章,彻底讲清楚应该如何写递归,并给出了前中后序三种不同的迭代法,最后分析为什么迭代法代码风格不能统一,最后给出统一的前中后序迭代法的代码,帮大家彻底吃透二叉树的深度优先遍历。 -实现树的遍历有使用递归和使用栈两种思路 +这里想帮大家一下,明确一下二叉树的遍历规则 +* 二叉树深度优先遍历 + * 前序遍历: 144.二叉树的前序遍历 + * 后序遍历: 145.二叉树的后序遍历 + * 中序遍历: 094.二叉树的中序遍历 +* 二叉树广度优先遍历 + * 层序遍历:0145.二叉树的后序遍历 + +这几道题目建议大家都做一下,本题解先只写二叉树深度优先遍历,二叉树广度优先遍历请看[0102.二叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0102.二叉树的层序遍历.md) + +![二叉树前后中遍历](https://img-blog.csdnimg.cn/20200808191505393.png) + +以上述中,前中后序遍历顺序如下 +前序遍历(中左右):5 4 1 2 6 7 8 +中序遍历(左中右):1 4 2 5 7 6 8 +后序遍历(左右中):1 2 4 7 8 6 5 + +### 递归法 + +接下来我们来好好谈一谈递归,为什么很多同学看递归算法都是“一看就会,一写就废”。主要是对递归不成体系,没有方法论,每次写递归算法 ,都是靠玄学来写代码,代码能不能编过都靠运气。 + +这里帮助大家确定下来递归算法的三个要素。每次写递归,都按照这三要素来写,可以保证大家写出正确的递归算法! + + +1. **确定递归函数的参数和返回值:** + +确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。 + + +2. **确定终止条件:** + +写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。 + + +3. **确定单层递归的逻辑:** +确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。 + +好了,我们确认了递归的三要素,接下来就来练练手: + +**以下以前序遍历为例:** + +1. **确定递归函数的参数和返回值**:因为要打印出前序遍历节点的数值,所以参数里需要传入vector在放节点的数值,除了这一点就不需要在处理什么数据了也不需要有返回值,所以递归函数返回类型就是void,代码如下: + +``` +void traversal(TreeNode* cur, vector& vec) +``` + +2. **确定终止条件**:在递归的过程中,如何算是递归结束了呢,当然是当前遍历的节点是空了,那么本层递归就要要结束了,所以如果当前遍历的这个节点是空,就直接return,代码如下: + +``` +if (cur == NULL) return; +``` + +3. 确定单层递归的逻辑:前序遍历是中左右的循序,所以在单层递归的逻辑,是要先取中节点的数值,代码如下: + +``` +vec.push_back(cur->val); // 中 +traversal(cur->left, vec); // 左 +traversal(cur->right, vec); // 右 +``` + +单层递归的逻辑就是按照中左右的顺序来处理的 + +这样二叉树的前序遍历,基本就写完了,在看一下完整代码: + +前序遍历 + +``` +class Solution { +public: + void traversal(TreeNode* cur, vector& vec) { + if (cur == NULL) return; + vec.push_back(cur->val); // 中 + traversal(cur->left, vec); // 左 + traversal(cur->right, vec); // 右 + } + vector preorderTraversal(TreeNode* root) { + vector result; + traversal(root, result); + return result; + } +}; +``` + +中序遍历 + +``` + void traversal(TreeNode* cur, vector& vec) { + if (cur == NULL) return; + traversal(cur->left, vec); // 左 + vec.push_back(cur->val); // 中 + traversal(cur->right, vec); // 右 + } +``` + +后序遍历 + +``` + void traversal(TreeNode* cur, vector& vec) { + if (cur == NULL) return; + traversal(cur->left, vec); // 左 + traversal(cur->right, vec); // 右 + vec.push_back(cur->val); // 中 + } +``` + +### 迭代法 + +实践过的同学,应该会发现使用迭代法实现先中后序遍历,很难写出统一的代码,不像是递归法,实现了其中的一种遍历方式,其他两种只要稍稍改一下节点顺序就可以了。而迭代法,貌似需要每一种遍历都要写出不同风格的代码。 + +那么接下来我先带大家看一看其中的根本原因,其实是可以针对三种遍历方式,使用迭代法可以写出统一风格的代码。 + +前序遍历(迭代法)不难写出如下代码: +``` +class Solution { +public: + vector preorderTraversal(TreeNode* root) { + stack st; + vector result; + st.push(root); + while (!st.empty()) { + TreeNode* node = st.top(); + st.pop(); + if (node != NULL) result.push_back(node->val); + else continue; + st.push(node->right); + st.push(node->left); + } + return result; + } +}; +``` + +这时会发现貌似使用迭代法写出先序遍历并不难,确实不难,但难却难在,我们再用迭代法写中序遍历的时候,发现套路又不一样了,目前的这个逻辑无法直接应用到中序遍历上。 + +#### 为什么迭代法不容易写出统一风格的代码 + +为了解释清楚,我说明一下 刚刚在迭代的过程中,其实我们有两个操作,**一个是处理:将元素放进result数组中,一个是访问:遍历节点。** + +分析一下为什么刚刚写的前序遍历的代码,不能和中序遍历通用呢,因为前序遍历的顺序是中左右,要先访问的元素是中间节点,要处理的元素也是中间节点,所以刚刚才能写出相对简洁的代码,**因为要访问的元素和要处理的元素顺序是一致的,都是中间节点。** + +那么再看看中序遍历,中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了**处理顺序和访问顺序是不一致的。** + +那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。 + +**中序遍历,可以写出如下代码:** + +``` +class Solution { +public: + vector inorderTraversal(TreeNode* root) { + vector result; + stack st; + TreeNode* cur = root; + while (cur != NULL || !st.empty()) { + if (cur != NULL) { + st.push(cur); + cur = cur->left; + } else { + cur = st.top(); + st.pop(); + result.push_back(cur->val); + cur = cur->right; + } + } + return result; + } +}; + +``` + +那么后序遍历呢 + +先序遍历是中左右,后续遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序 +,然后在反转result数组,输出的结果顺序就是左右中了,如下图: +![前序到后序](https://img-blog.csdnimg.cn/20200808200338924.png) + +所以后序遍历只需要前序遍历的代码稍作修改就可以了,代码如下: + +``` +class Solution { +public: + + vector postorderTraversal(TreeNode* root) { + stack st; + vector result; + st.push(root); + while (!st.empty()) { + TreeNode* node = st.top(); + st.pop(); + if (node != NULL) result.push_back(node->val); + else continue; + st.push(node->left); // 相对于前序遍历,这更改一下入栈顺序 + st.push(node->right); + } + reverse(result.begin(), result.end()); // 将结果反转之后就是左右中的顺序了 + return result; + } +}; + +``` + + + + +我们再来看一下代码。 ## C++代码 ### 递归 diff --git a/problems/0226.翻转二叉树.md b/problems/0226.翻转二叉树.md index fd411101..b8140ff8 100644 --- a/problems/0226.翻转二叉树.md +++ b/problems/0226.翻转二叉树.md @@ -3,7 +3,61 @@ https://leetcode-cn.com/problems/invert-binary-tree/ ## 思路 -写递归算法的时候,要想一想是采用前中后序那种遍历方式 +### 递归法 +写递归算法的时候,要想一想是采用前中后序哪种遍历方式 + +我们先看看递归算法,对于二叉树的递归方式有三种前中后序,先来看看前序遍历。 + +通过动画来看一下翻转的过程: + + + +递归三部曲: + +1. 确定递归函数的参数和返回值 + +参数就是要传入节点的指针,不需要其他参数了,返回值的话其实也不需要,但是题目中给出的要返回root节点的指针,可以直接使用题目定义好的函数,所以就返回一个指针。 + +``` +TreeNode* invertTree(TreeNode* root) +``` + +2. 确定终止条件 + +当前节点为空的时候,就返回 + +``` +if (root == NULL) return root; +``` + +3. 确定单层递归的逻辑 + +因为是先序遍历,所以先进行交换左右孩子节点,然后反转左子树,反转右子树。 + +``` +swap(root->left, root->right); +invertTree(root->left); +invertTree(root->right); +``` + +基于这递归三步法,代码基本写完,C++代码如下: + +``` +class Solution { +public: + TreeNode* invertTree(TreeNode* root) { + if (root == NULL) return root; + swap(root->left, root->right); + invertTree(root->left); + invertTree(root->right); + return root; + } +}; +``` + +### 迭代法 + +[leetcode-master](https://github.com/youngyangyang04/leetcode-master) 中给出了 前中后序迭代法统一的模板,使用前序遍历,只需要改动一行就可以了,代码在下面已经给出。 ## C++代码 @@ -40,4 +94,34 @@ public: } }; ``` + +### 迭代法(前序遍历)(模板) + +模板地址:[leetcode-master](https://github.com/youngyangyang04/leetcode-master) + +``` +class Solution { +public: + TreeNode* invertTree(TreeNode* root) { + stack st; + if (root != NULL) st.push(root); + while (!st.empty()) { + TreeNode* node = st.top(); + if (node != NULL) { + st.pop(); + if (node->right) st.push(node->right); // 添加右节点 + if (node->left) st.push(node->left); // 添加左节点 + st.push(node); // 添加中节点 + st.push(NULL); + } else { + st.pop(); + node = st.top(); + st.pop(); + swap(node->left, node->right); // 节点处理逻辑 + } + } + return root; + } +}; +``` > 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/video/对称二叉树.mp4 b/video/对称二叉树.mp4 new file mode 100644 index 00000000..96d907fd Binary files /dev/null and b/video/对称二叉树.mp4 differ diff --git a/video/翻转二叉树.mp4 b/video/翻转二叉树.mp4 new file mode 100644 index 00000000..73636b7b Binary files /dev/null and b/video/翻转二叉树.mp4 differ