diff --git a/README.md b/README.md index 6f8dc435..53180718 100644 --- a/README.md +++ b/README.md @@ -272,7 +272,7 @@ ## 动态规划 -动态规划专题已经开始啦,小伙伴快上车! +动态规划专题已经开始啦,来不及解释了,小伙伴们上车别掉队! * [关于动态规划,你该了解这些!](https://leetcode-cn.com/circle/article/tNuNnM/) diff --git a/pics/100.相同的树.png b/pics/100.相同的树.png deleted file mode 100644 index 88da7600..00000000 Binary files a/pics/100.相同的树.png and /dev/null differ diff --git a/pics/1002.查找常用字符.png b/pics/1002.查找常用字符.png deleted file mode 100644 index 23d75057..00000000 Binary files a/pics/1002.查找常用字符.png and /dev/null differ diff --git a/pics/101. 对称二叉树.png b/pics/101. 对称二叉树.png deleted file mode 100644 index 6485026d..00000000 Binary files a/pics/101. 对称二叉树.png and /dev/null differ diff --git a/pics/101. 对称二叉树1.png b/pics/101. 对称二叉树1.png deleted file mode 100644 index 98286136..00000000 Binary files a/pics/101. 对称二叉树1.png and /dev/null differ diff --git a/pics/102.二叉树的层序遍历.png b/pics/102.二叉树的层序遍历.png deleted file mode 100644 index dcfcf972..00000000 Binary files a/pics/102.二叉树的层序遍历.png and /dev/null differ diff --git a/pics/104. 二叉树的最大深度.png b/pics/104. 二叉树的最大深度.png deleted file mode 100644 index 74873c76..00000000 Binary files a/pics/104. 二叉树的最大深度.png and /dev/null differ diff --git a/pics/1047.删除字符串中的所有相邻重复项.png b/pics/1047.删除字符串中的所有相邻重复项.png deleted file mode 100644 index 57587100..00000000 Binary files a/pics/1047.删除字符串中的所有相邻重复项.png and /dev/null differ diff --git a/pics/105. 从前序与中序遍历序列构造二叉树.png b/pics/105. 从前序与中序遍历序列构造二叉树.png deleted file mode 100644 index 7430be27..00000000 Binary files a/pics/105. 从前序与中序遍历序列构造二叉树.png and /dev/null differ diff --git a/pics/106. 从中序与后序遍历序列构造二叉树1.png b/pics/106. 从中序与后序遍历序列构造二叉树1.png deleted file mode 100644 index ab8fa4b9..00000000 Binary files a/pics/106. 从中序与后序遍历序列构造二叉树1.png and /dev/null differ diff --git a/pics/106.从中序与后序遍历序列构造二叉树.png b/pics/106.从中序与后序遍历序列构造二叉树.png deleted file mode 100644 index 081a7813..00000000 Binary files a/pics/106.从中序与后序遍历序列构造二叉树.png and /dev/null differ diff --git a/pics/106.从中序与后序遍历序列构造二叉树2.png b/pics/106.从中序与后序遍历序列构造二叉树2.png deleted file mode 100644 index 8726f4ce..00000000 Binary files a/pics/106.从中序与后序遍历序列构造二叉树2.png and /dev/null differ diff --git a/pics/107.二叉树的层次遍历II.png b/pics/107.二叉树的层次遍历II.png deleted file mode 100644 index b4021c44..00000000 Binary files a/pics/107.二叉树的层次遍历II.png and /dev/null differ diff --git a/pics/108.将有序数组转换为二叉搜索树.png b/pics/108.将有序数组转换为二叉搜索树.png deleted file mode 100644 index 7d25f99d..00000000 Binary files a/pics/108.将有序数组转换为二叉搜索树.png and /dev/null differ diff --git a/pics/110.平衡二叉树.png b/pics/110.平衡二叉树.png deleted file mode 100644 index f227f1ad..00000000 Binary files a/pics/110.平衡二叉树.png and /dev/null differ diff --git a/pics/110.平衡二叉树1.png b/pics/110.平衡二叉树1.png deleted file mode 100644 index 36e0a09c..00000000 Binary files a/pics/110.平衡二叉树1.png and /dev/null differ diff --git a/pics/110.平衡二叉树2.png b/pics/110.平衡二叉树2.png deleted file mode 100644 index 53d46be6..00000000 Binary files a/pics/110.平衡二叉树2.png and /dev/null differ diff --git a/pics/111.二叉树的最小深度.png b/pics/111.二叉树的最小深度.png deleted file mode 100644 index b1980df8..00000000 Binary files a/pics/111.二叉树的最小深度.png and /dev/null differ diff --git a/pics/111.二叉树的最小深度1.png b/pics/111.二叉树的最小深度1.png deleted file mode 100644 index a0ac70cb..00000000 Binary files a/pics/111.二叉树的最小深度1.png and /dev/null differ diff --git a/pics/112.路径总和.png b/pics/112.路径总和.png deleted file mode 100644 index 2a1b5100..00000000 Binary files a/pics/112.路径总和.png and /dev/null differ diff --git a/pics/112.路径总和1.png b/pics/112.路径总和1.png deleted file mode 100644 index 4c6c0f60..00000000 Binary files a/pics/112.路径总和1.png and /dev/null differ diff --git a/pics/113.路径总和II.png b/pics/113.路径总和II.png deleted file mode 100644 index 931c2a9a..00000000 Binary files a/pics/113.路径总和II.png and /dev/null differ diff --git a/pics/113.路径总和II1.png b/pics/113.路径总和II1.png deleted file mode 100644 index e1d5a2d1..00000000 Binary files a/pics/113.路径总和II1.png and /dev/null differ diff --git a/pics/116.填充每个节点的下一个右侧节点指针.png b/pics/116.填充每个节点的下一个右侧节点指针.png deleted file mode 100644 index bec25c0a..00000000 Binary files a/pics/116.填充每个节点的下一个右侧节点指针.png and /dev/null differ diff --git a/pics/1207.独一无二的出现次数.png b/pics/1207.独一无二的出现次数.png deleted file mode 100644 index bbb036e6..00000000 Binary files a/pics/1207.独一无二的出现次数.png and /dev/null differ diff --git a/pics/122.买卖股票的最佳时机II.png b/pics/122.买卖股票的最佳时机II.png deleted file mode 100644 index 9799abfd..00000000 Binary files a/pics/122.买卖股票的最佳时机II.png and /dev/null differ diff --git a/pics/123.买卖股票的最佳时机III.png b/pics/123.买卖股票的最佳时机III.png deleted file mode 100644 index 1037b690..00000000 Binary files a/pics/123.买卖股票的最佳时机III.png and /dev/null differ diff --git a/pics/127.单词接龙.png b/pics/127.单词接龙.png deleted file mode 100644 index 581bb558..00000000 Binary files a/pics/127.单词接龙.png and /dev/null differ diff --git a/pics/129.求根到叶子节点数字之和.png b/pics/129.求根到叶子节点数字之和.png deleted file mode 100644 index 58288a9d..00000000 Binary files a/pics/129.求根到叶子节点数字之和.png and /dev/null differ diff --git a/pics/131.分割回文串.png b/pics/131.分割回文串.png deleted file mode 100644 index 0b50823f..00000000 Binary files a/pics/131.分割回文串.png and /dev/null differ diff --git a/pics/134.加油站.png b/pics/134.加油站.png deleted file mode 100644 index 120f714d..00000000 Binary files a/pics/134.加油站.png and /dev/null differ diff --git a/pics/135.分发糖果.png b/pics/135.分发糖果.png deleted file mode 100644 index cacd2cbe..00000000 Binary files a/pics/135.分发糖果.png and /dev/null differ diff --git a/pics/135.分发糖果1.png b/pics/135.分发糖果1.png deleted file mode 100644 index 27209df9..00000000 Binary files a/pics/135.分发糖果1.png and /dev/null differ diff --git a/pics/1356.根据数字二进制下1的数目排序.png b/pics/1356.根据数字二进制下1的数目排序.png deleted file mode 100644 index 0fca9fed..00000000 Binary files a/pics/1356.根据数字二进制下1的数目排序.png and /dev/null differ diff --git a/pics/1365.有多少小于当前数字的数字.png b/pics/1365.有多少小于当前数字的数字.png deleted file mode 100644 index 9d67d07b..00000000 Binary files a/pics/1365.有多少小于当前数字的数字.png and /dev/null differ diff --git a/pics/142环形链表1.png b/pics/142环形链表1.png deleted file mode 100644 index 7814be45..00000000 Binary files a/pics/142环形链表1.png and /dev/null differ diff --git a/pics/142环形链表2.png b/pics/142环形链表2.png deleted file mode 100644 index 86294647..00000000 Binary files a/pics/142环形链表2.png and /dev/null differ diff --git a/pics/142环形链表3.png b/pics/142环形链表3.png deleted file mode 100644 index 9bd96f6b..00000000 Binary files a/pics/142环形链表3.png and /dev/null differ diff --git a/pics/142环形链表4.png b/pics/142环形链表4.png deleted file mode 100644 index e37690d8..00000000 Binary files a/pics/142环形链表4.png and /dev/null differ diff --git a/pics/142环形链表5.png b/pics/142环形链表5.png deleted file mode 100644 index b99a79f1..00000000 Binary files a/pics/142环形链表5.png and /dev/null differ diff --git a/pics/143.重排链表.png b/pics/143.重排链表.png deleted file mode 100644 index 2fc94408..00000000 Binary files a/pics/143.重排链表.png and /dev/null differ diff --git a/pics/147.对链表进行插入排序.png b/pics/147.对链表进行插入排序.png deleted file mode 100644 index 807f66f4..00000000 Binary files a/pics/147.对链表进行插入排序.png and /dev/null differ diff --git a/pics/151_翻转字符串里的单词.png b/pics/151_翻转字符串里的单词.png deleted file mode 100644 index dc844950..00000000 Binary files a/pics/151_翻转字符串里的单词.png and /dev/null differ diff --git a/pics/17. 电话号码的字母组合.jpeg b/pics/17. 电话号码的字母组合.jpeg deleted file mode 100644 index f80ec767..00000000 Binary files a/pics/17. 电话号码的字母组合.jpeg and /dev/null differ diff --git a/pics/17. 电话号码的字母组合.png b/pics/17. 电话号码的字母组合.png deleted file mode 100644 index b11f7114..00000000 Binary files a/pics/17. 电话号码的字母组合.png and /dev/null differ diff --git a/pics/19.删除链表的倒数第N个节点.png b/pics/19.删除链表的倒数第N个节点.png deleted file mode 100644 index 6f6aa9c5..00000000 Binary files a/pics/19.删除链表的倒数第N个节点.png and /dev/null differ diff --git a/pics/19.删除链表的倒数第N个节点1.png b/pics/19.删除链表的倒数第N个节点1.png deleted file mode 100644 index cca947b4..00000000 Binary files a/pics/19.删除链表的倒数第N个节点1.png and /dev/null differ diff --git a/pics/19.删除链表的倒数第N个节点2.png b/pics/19.删除链表的倒数第N个节点2.png deleted file mode 100644 index 0d8144cd..00000000 Binary files a/pics/19.删除链表的倒数第N个节点2.png and /dev/null differ diff --git a/pics/19.删除链表的倒数第N个节点3.png b/pics/19.删除链表的倒数第N个节点3.png deleted file mode 100644 index d15d05e7..00000000 Binary files a/pics/19.删除链表的倒数第N个节点3.png and /dev/null differ diff --git a/pics/199.二叉树的右视图.png b/pics/199.二叉树的右视图.png deleted file mode 100644 index e5244117..00000000 Binary files a/pics/199.二叉树的右视图.png and /dev/null differ diff --git a/pics/203_链表删除元素1.png b/pics/203_链表删除元素1.png deleted file mode 100644 index b32aa50e..00000000 Binary files a/pics/203_链表删除元素1.png and /dev/null differ diff --git a/pics/203_链表删除元素2.png b/pics/203_链表删除元素2.png deleted file mode 100644 index 5519a69d..00000000 Binary files a/pics/203_链表删除元素2.png and /dev/null differ diff --git a/pics/203_链表删除元素3.png b/pics/203_链表删除元素3.png deleted file mode 100644 index cd50ce13..00000000 Binary files a/pics/203_链表删除元素3.png and /dev/null differ diff --git a/pics/203_链表删除元素4.png b/pics/203_链表删除元素4.png deleted file mode 100644 index 02aaf115..00000000 Binary files a/pics/203_链表删除元素4.png and /dev/null differ diff --git a/pics/203_链表删除元素5.png b/pics/203_链表删除元素5.png deleted file mode 100644 index e24ad3d3..00000000 Binary files a/pics/203_链表删除元素5.png and /dev/null differ diff --git a/pics/203_链表删除元素6.png b/pics/203_链表删除元素6.png deleted file mode 100644 index 13f9b6d6..00000000 Binary files a/pics/203_链表删除元素6.png and /dev/null differ diff --git a/pics/206_反转链表.png b/pics/206_反转链表.png deleted file mode 100644 index f26ad08f..00000000 Binary files a/pics/206_反转链表.png and /dev/null differ diff --git a/pics/216.组合总和III.png b/pics/216.组合总和III.png deleted file mode 100644 index 1b71e67c..00000000 Binary files a/pics/216.组合总和III.png and /dev/null differ diff --git a/pics/216.组合总和III1.png b/pics/216.组合总和III1.png deleted file mode 100644 index e495191d..00000000 Binary files a/pics/216.组合总和III1.png and /dev/null differ diff --git a/pics/222.完全二叉树的节点个数.png b/pics/222.完全二叉树的节点个数.png deleted file mode 100644 index 2eb1eb94..00000000 Binary files a/pics/222.完全二叉树的节点个数.png and /dev/null differ diff --git a/pics/222.完全二叉树的节点个数1.png b/pics/222.完全二叉树的节点个数1.png deleted file mode 100644 index 49e1772c..00000000 Binary files a/pics/222.完全二叉树的节点个数1.png and /dev/null differ diff --git a/pics/226.翻转二叉树.png b/pics/226.翻转二叉树.png deleted file mode 100644 index 1e3aec91..00000000 Binary files a/pics/226.翻转二叉树.png and /dev/null differ diff --git a/pics/226.翻转二叉树1.png b/pics/226.翻转二叉树1.png deleted file mode 100644 index 9435a12d..00000000 Binary files a/pics/226.翻转二叉树1.png and /dev/null differ diff --git a/pics/234.回文链表.png b/pics/234.回文链表.png deleted file mode 100644 index 285c73bb..00000000 Binary files a/pics/234.回文链表.png and /dev/null differ diff --git a/pics/235.二叉搜索树的最近公共祖先.png b/pics/235.二叉搜索树的最近公共祖先.png deleted file mode 100644 index 277b8165..00000000 Binary files a/pics/235.二叉搜索树的最近公共祖先.png and /dev/null differ diff --git a/pics/236.二叉树的最近公共祖先.png b/pics/236.二叉树的最近公共祖先.png deleted file mode 100644 index 023dd955..00000000 Binary files a/pics/236.二叉树的最近公共祖先.png and /dev/null differ diff --git a/pics/236.二叉树的最近公共祖先1.png b/pics/236.二叉树的最近公共祖先1.png deleted file mode 100644 index 4542ad5c..00000000 Binary files a/pics/236.二叉树的最近公共祖先1.png and /dev/null differ diff --git a/pics/236.二叉树的最近公共祖先2.png b/pics/236.二叉树的最近公共祖先2.png deleted file mode 100644 index 1681fbcc..00000000 Binary files a/pics/236.二叉树的最近公共祖先2.png and /dev/null differ diff --git a/pics/239.滑动窗口最大值.png b/pics/239.滑动窗口最大值.png deleted file mode 100644 index 49734e5d..00000000 Binary files a/pics/239.滑动窗口最大值.png and /dev/null differ diff --git a/pics/24.两两交换链表中的节点.png b/pics/24.两两交换链表中的节点.png deleted file mode 100644 index 3051c5a1..00000000 Binary files a/pics/24.两两交换链表中的节点.png and /dev/null differ diff --git a/pics/24.两两交换链表中的节点1.png b/pics/24.两两交换链表中的节点1.png deleted file mode 100644 index e79642c3..00000000 Binary files a/pics/24.两两交换链表中的节点1.png and /dev/null differ diff --git a/pics/24.两两交换链表中的节点2.png b/pics/24.两两交换链表中的节点2.png deleted file mode 100644 index e6dba912..00000000 Binary files a/pics/24.两两交换链表中的节点2.png and /dev/null differ diff --git a/pics/24.两两交换链表中的节点3.png b/pics/24.两两交换链表中的节点3.png deleted file mode 100644 index 7d706cd2..00000000 Binary files a/pics/24.两两交换链表中的节点3.png and /dev/null differ diff --git a/pics/257.二叉树的所有路径.png b/pics/257.二叉树的所有路径.png deleted file mode 100644 index 0a0683e0..00000000 Binary files a/pics/257.二叉树的所有路径.png and /dev/null differ diff --git a/pics/257.二叉树的所有路径1.png b/pics/257.二叉树的所有路径1.png deleted file mode 100644 index 97452f51..00000000 Binary files a/pics/257.二叉树的所有路径1.png and /dev/null differ diff --git a/pics/26_封面.png b/pics/26_封面.png deleted file mode 100644 index b8a2f1a1..00000000 Binary files a/pics/26_封面.png and /dev/null differ diff --git a/pics/27_封面.png b/pics/27_封面.png deleted file mode 100644 index a4a2b009..00000000 Binary files a/pics/27_封面.png and /dev/null differ diff --git a/pics/31.下一个排列.png b/pics/31.下一个排列.png deleted file mode 100644 index f88ef392..00000000 Binary files a/pics/31.下一个排列.png and /dev/null differ diff --git a/pics/332.重新安排行程.png b/pics/332.重新安排行程.png deleted file mode 100644 index 56f3ab02..00000000 Binary files a/pics/332.重新安排行程.png and /dev/null differ diff --git a/pics/332.重新安排行程1.png b/pics/332.重新安排行程1.png deleted file mode 100644 index 69d51882..00000000 Binary files a/pics/332.重新安排行程1.png and /dev/null differ diff --git a/pics/347.前K个高频元素.png b/pics/347.前K个高频元素.png deleted file mode 100644 index 54b1417d..00000000 Binary files a/pics/347.前K个高频元素.png and /dev/null differ diff --git a/pics/35_封面.png b/pics/35_封面.png deleted file mode 100644 index dc2971b5..00000000 Binary files a/pics/35_封面.png and /dev/null differ diff --git a/pics/35_搜索插入位置.png b/pics/35_搜索插入位置.png deleted file mode 100644 index 5fa65490..00000000 Binary files a/pics/35_搜索插入位置.png and /dev/null differ diff --git a/pics/35_搜索插入位置2.png b/pics/35_搜索插入位置2.png deleted file mode 100644 index 71fca151..00000000 Binary files a/pics/35_搜索插入位置2.png and /dev/null differ diff --git a/pics/35_搜索插入位置3.png b/pics/35_搜索插入位置3.png deleted file mode 100644 index ec1ce1eb..00000000 Binary files a/pics/35_搜索插入位置3.png and /dev/null differ diff --git a/pics/35_搜索插入位置4.png b/pics/35_搜索插入位置4.png deleted file mode 100644 index efb88e22..00000000 Binary files a/pics/35_搜索插入位置4.png and /dev/null differ diff --git a/pics/35_搜索插入位置5.png b/pics/35_搜索插入位置5.png deleted file mode 100644 index ec021c9d..00000000 Binary files a/pics/35_搜索插入位置5.png and /dev/null differ diff --git a/pics/37.解数独.png b/pics/37.解数独.png deleted file mode 100644 index 46b0a15a..00000000 Binary files a/pics/37.解数独.png and /dev/null differ diff --git a/pics/376.摆动序列.png b/pics/376.摆动序列.png deleted file mode 100644 index c6a06dfe..00000000 Binary files a/pics/376.摆动序列.png and /dev/null differ diff --git a/pics/376.摆动序列1.png b/pics/376.摆动序列1.png deleted file mode 100644 index 212e7722..00000000 Binary files a/pics/376.摆动序列1.png and /dev/null differ diff --git a/pics/39.组合总和.png b/pics/39.组合总和.png deleted file mode 100644 index 95430a66..00000000 Binary files a/pics/39.组合总和.png and /dev/null differ diff --git a/pics/39.组合总和1.png b/pics/39.组合总和1.png deleted file mode 100644 index 88f7635b..00000000 Binary files a/pics/39.组合总和1.png and /dev/null differ diff --git a/pics/40.组合总和II.png b/pics/40.组合总和II.png deleted file mode 100644 index 104e5f4e..00000000 Binary files a/pics/40.组合总和II.png and /dev/null differ diff --git a/pics/40.组合总和II1.png b/pics/40.组合总和II1.png deleted file mode 100644 index 75fda6b3..00000000 Binary files a/pics/40.组合总和II1.png and /dev/null differ diff --git a/pics/404.左叶子之和.png b/pics/404.左叶子之和.png deleted file mode 100644 index 81b6d8c2..00000000 Binary files a/pics/404.左叶子之和.png and /dev/null differ diff --git a/pics/404.左叶子之和1.png b/pics/404.左叶子之和1.png deleted file mode 100644 index 8c050e04..00000000 Binary files a/pics/404.左叶子之和1.png and /dev/null differ diff --git a/pics/406.根据身高重建队列.png b/pics/406.根据身高重建队列.png deleted file mode 100644 index 9ea8f0b2..00000000 Binary files a/pics/406.根据身高重建队列.png and /dev/null differ diff --git a/pics/416.分割等和子集.png b/pics/416.分割等和子集.png deleted file mode 100644 index 8d1b9ef6..00000000 Binary files a/pics/416.分割等和子集.png and /dev/null differ diff --git a/pics/416.分割等和子集1.png b/pics/416.分割等和子集1.png deleted file mode 100644 index 6be70dc6..00000000 Binary files a/pics/416.分割等和子集1.png and /dev/null differ diff --git a/pics/42.接雨水1.png b/pics/42.接雨水1.png deleted file mode 100644 index 26624f4b..00000000 Binary files a/pics/42.接雨水1.png and /dev/null differ diff --git a/pics/42.接雨水2.png b/pics/42.接雨水2.png deleted file mode 100644 index 94eda97e..00000000 Binary files a/pics/42.接雨水2.png and /dev/null differ diff --git a/pics/42.接雨水3.png b/pics/42.接雨水3.png deleted file mode 100644 index 1e4b528a..00000000 Binary files a/pics/42.接雨水3.png and /dev/null differ diff --git a/pics/42.接雨水4.png b/pics/42.接雨水4.png deleted file mode 100644 index a18539ce..00000000 Binary files a/pics/42.接雨水4.png and /dev/null differ diff --git a/pics/42.接雨水5.png b/pics/42.接雨水5.png deleted file mode 100644 index 066792b0..00000000 Binary files a/pics/42.接雨水5.png and /dev/null differ diff --git a/pics/429. N叉树的层序遍历.png b/pics/429. N叉树的层序遍历.png deleted file mode 100644 index d28c543c..00000000 Binary files a/pics/429. N叉树的层序遍历.png and /dev/null differ diff --git a/pics/435.无重叠区间.png b/pics/435.无重叠区间.png deleted file mode 100644 index a45913c1..00000000 Binary files a/pics/435.无重叠区间.png and /dev/null differ diff --git a/pics/45.跳跃游戏II.png b/pics/45.跳跃游戏II.png deleted file mode 100644 index 77130cb2..00000000 Binary files a/pics/45.跳跃游戏II.png and /dev/null differ diff --git a/pics/45.跳跃游戏II1.png b/pics/45.跳跃游戏II1.png deleted file mode 100644 index 7850187f..00000000 Binary files a/pics/45.跳跃游戏II1.png and /dev/null differ diff --git a/pics/45.跳跃游戏II2.png b/pics/45.跳跃游戏II2.png deleted file mode 100644 index aa45f60a..00000000 Binary files a/pics/45.跳跃游戏II2.png and /dev/null differ diff --git a/pics/452.用最少数量的箭引爆气球.png b/pics/452.用最少数量的箭引爆气球.png deleted file mode 100644 index 64080914..00000000 Binary files a/pics/452.用最少数量的箭引爆气球.png and /dev/null differ diff --git a/pics/455.分发饼干.png b/pics/455.分发饼干.png deleted file mode 100644 index 17e15e09..00000000 Binary files a/pics/455.分发饼干.png and /dev/null differ diff --git a/pics/459.重复的子字符串_1.png b/pics/459.重复的子字符串_1.png deleted file mode 100644 index 2feddd6b..00000000 Binary files a/pics/459.重复的子字符串_1.png and /dev/null differ diff --git a/pics/46.全排列.png b/pics/46.全排列.png deleted file mode 100644 index d8c484ec..00000000 Binary files a/pics/46.全排列.png and /dev/null differ diff --git a/pics/463.岛屿的周长.png b/pics/463.岛屿的周长.png deleted file mode 100644 index 5b0fa013..00000000 Binary files a/pics/463.岛屿的周长.png and /dev/null differ diff --git a/pics/463.岛屿的周长1.png b/pics/463.岛屿的周长1.png deleted file mode 100644 index bb14dbd6..00000000 Binary files a/pics/463.岛屿的周长1.png and /dev/null differ diff --git a/pics/47.全排列II1.png b/pics/47.全排列II1.png deleted file mode 100644 index 8160dd86..00000000 Binary files a/pics/47.全排列II1.png and /dev/null differ diff --git a/pics/47.全排列II2.png b/pics/47.全排列II2.png deleted file mode 100644 index d2e096b1..00000000 Binary files a/pics/47.全排列II2.png and /dev/null differ diff --git a/pics/47.全排列II3.png b/pics/47.全排列II3.png deleted file mode 100644 index b6f5a862..00000000 Binary files a/pics/47.全排列II3.png and /dev/null differ diff --git a/pics/486.预测赢家.png b/pics/486.预测赢家.png deleted file mode 100644 index 03ddfaa8..00000000 Binary files a/pics/486.预测赢家.png and /dev/null differ diff --git a/pics/486.预测赢家1.png b/pics/486.预测赢家1.png deleted file mode 100644 index dbddef26..00000000 Binary files a/pics/486.预测赢家1.png and /dev/null differ diff --git a/pics/486.预测赢家2.png b/pics/486.预测赢家2.png deleted file mode 100644 index 7d26de9a..00000000 Binary files a/pics/486.预测赢家2.png and /dev/null differ diff --git a/pics/486.预测赢家3.png b/pics/486.预测赢家3.png deleted file mode 100644 index 506778c4..00000000 Binary files a/pics/486.预测赢家3.png and /dev/null differ diff --git a/pics/486.预测赢家4.png b/pics/486.预测赢家4.png deleted file mode 100644 index 8a679b74..00000000 Binary files a/pics/486.预测赢家4.png and /dev/null differ diff --git a/pics/491. 递增子序列1.png b/pics/491. 递增子序列1.png deleted file mode 100644 index 19da907e..00000000 Binary files a/pics/491. 递增子序列1.png and /dev/null differ diff --git a/pics/491. 递增子序列2.png b/pics/491. 递增子序列2.png deleted file mode 100644 index 24988010..00000000 Binary files a/pics/491. 递增子序列2.png and /dev/null differ diff --git a/pics/491. 递增子序列3.png b/pics/491. 递增子序列3.png deleted file mode 100644 index df293dd5..00000000 Binary files a/pics/491. 递增子序列3.png and /dev/null differ diff --git a/pics/491. 递增子序列4.png b/pics/491. 递增子序列4.png deleted file mode 100644 index 395fc515..00000000 Binary files a/pics/491. 递增子序列4.png and /dev/null differ diff --git a/pics/501.二叉搜索树中的众数.png b/pics/501.二叉搜索树中的众数.png deleted file mode 100644 index a7197b08..00000000 Binary files a/pics/501.二叉搜索树中的众数.png and /dev/null differ diff --git a/pics/501.二叉搜索树中的众数1.png b/pics/501.二叉搜索树中的众数1.png deleted file mode 100644 index 200bc2fa..00000000 Binary files a/pics/501.二叉搜索树中的众数1.png and /dev/null differ diff --git a/pics/51.N皇后.png b/pics/51.N皇后.png deleted file mode 100644 index 730b7cb8..00000000 Binary files a/pics/51.N皇后.png and /dev/null differ diff --git a/pics/51.N皇后1.png b/pics/51.N皇后1.png deleted file mode 100644 index e6c9c72d..00000000 Binary files a/pics/51.N皇后1.png and /dev/null differ diff --git a/pics/513.找树左下角的值.png b/pics/513.找树左下角的值.png deleted file mode 100644 index 67bda96f..00000000 Binary files a/pics/513.找树左下角的值.png and /dev/null differ diff --git a/pics/513.找树左下角的值1.png b/pics/513.找树左下角的值1.png deleted file mode 100644 index a5421ad6..00000000 Binary files a/pics/513.找树左下角的值1.png and /dev/null differ diff --git a/pics/515.在每个树行中找最大值.png b/pics/515.在每个树行中找最大值.png deleted file mode 100644 index e8f2f431..00000000 Binary files a/pics/515.在每个树行中找最大值.png and /dev/null differ diff --git a/pics/530.二叉搜索树的最小绝对差.png b/pics/530.二叉搜索树的最小绝对差.png deleted file mode 100644 index 04ca9a65..00000000 Binary files a/pics/530.二叉搜索树的最小绝对差.png and /dev/null differ diff --git a/pics/538.把二叉搜索树转换为累加树.png b/pics/538.把二叉搜索树转换为累加树.png deleted file mode 100644 index 27077f7b..00000000 Binary files a/pics/538.把二叉搜索树转换为累加树.png and /dev/null differ diff --git a/pics/541_反转字符串II.png b/pics/541_反转字符串II.png deleted file mode 100644 index 45f0ce6c..00000000 Binary files a/pics/541_反转字符串II.png and /dev/null differ diff --git a/pics/55.跳跃游戏.png b/pics/55.跳跃游戏.png deleted file mode 100644 index 012bb635..00000000 Binary files a/pics/55.跳跃游戏.png and /dev/null differ diff --git a/pics/559.N叉树的最大深度.png b/pics/559.N叉树的最大深度.png deleted file mode 100644 index d28c543c..00000000 Binary files a/pics/559.N叉树的最大深度.png and /dev/null differ diff --git a/pics/56.合并区间.png b/pics/56.合并区间.png deleted file mode 100644 index ff905c72..00000000 Binary files a/pics/56.合并区间.png and /dev/null differ diff --git a/pics/57.插入区间.png b/pics/57.插入区间.png deleted file mode 100644 index 67290167..00000000 Binary files a/pics/57.插入区间.png and /dev/null differ diff --git a/pics/57.插入区间1.png b/pics/57.插入区间1.png deleted file mode 100644 index 69835dee..00000000 Binary files a/pics/57.插入区间1.png and /dev/null differ diff --git a/pics/59_封面.png b/pics/59_封面.png deleted file mode 100644 index 795e8f04..00000000 Binary files a/pics/59_封面.png and /dev/null differ diff --git a/pics/617.合并二叉树.png b/pics/617.合并二叉树.png deleted file mode 100644 index 182f959c..00000000 Binary files a/pics/617.合并二叉树.png and /dev/null differ diff --git a/pics/62.不同路径.png b/pics/62.不同路径.png deleted file mode 100644 index d82c6ed0..00000000 Binary files a/pics/62.不同路径.png and /dev/null differ diff --git a/pics/62.不同路径1.png b/pics/62.不同路径1.png deleted file mode 100644 index 0d84838d..00000000 Binary files a/pics/62.不同路径1.png and /dev/null differ diff --git a/pics/62.不同路径2.png b/pics/62.不同路径2.png deleted file mode 100644 index 91984901..00000000 Binary files a/pics/62.不同路径2.png and /dev/null differ diff --git a/pics/637.二叉树的层平均值.png b/pics/637.二叉树的层平均值.png deleted file mode 100644 index 57018dab..00000000 Binary files a/pics/637.二叉树的层平均值.png and /dev/null differ diff --git a/pics/654.最大二叉树.png b/pics/654.最大二叉树.png deleted file mode 100644 index e8fc63aa..00000000 Binary files a/pics/654.最大二叉树.png and /dev/null differ diff --git a/pics/657.机器人能否返回原点.png b/pics/657.机器人能否返回原点.png deleted file mode 100644 index 6ea5b69b..00000000 Binary files a/pics/657.机器人能否返回原点.png and /dev/null differ diff --git a/pics/669.修剪二叉搜索树.png b/pics/669.修剪二叉搜索树.png deleted file mode 100644 index 89fe4104..00000000 Binary files a/pics/669.修剪二叉搜索树.png and /dev/null differ diff --git a/pics/669.修剪二叉搜索树1.png b/pics/669.修剪二叉搜索树1.png deleted file mode 100644 index 1b46e8d0..00000000 Binary files a/pics/669.修剪二叉搜索树1.png and /dev/null differ diff --git a/pics/685.冗余连接II1.png b/pics/685.冗余连接II1.png deleted file mode 100644 index ab833087..00000000 Binary files a/pics/685.冗余连接II1.png and /dev/null differ diff --git a/pics/685.冗余连接II2.png b/pics/685.冗余连接II2.png deleted file mode 100644 index 6dbb2ac7..00000000 Binary files a/pics/685.冗余连接II2.png and /dev/null differ diff --git a/pics/700.二叉搜索树中的搜索.png b/pics/700.二叉搜索树中的搜索.png deleted file mode 100644 index 1fd15466..00000000 Binary files a/pics/700.二叉搜索树中的搜索.png and /dev/null differ diff --git a/pics/700.二叉搜索树中的搜索1.png b/pics/700.二叉搜索树中的搜索1.png deleted file mode 100644 index 037627b1..00000000 Binary files a/pics/700.二叉搜索树中的搜索1.png and /dev/null differ diff --git a/pics/763.划分字母区间.png b/pics/763.划分字母区间.png deleted file mode 100644 index 45d1b93c..00000000 Binary files a/pics/763.划分字母区间.png and /dev/null differ diff --git a/pics/77.组合.png b/pics/77.组合.png deleted file mode 100644 index 17cde493..00000000 Binary files a/pics/77.组合.png and /dev/null differ diff --git a/pics/77.组合1.png b/pics/77.组合1.png deleted file mode 100644 index a6a4a272..00000000 Binary files a/pics/77.组合1.png and /dev/null differ diff --git a/pics/77.组合2.png b/pics/77.组合2.png deleted file mode 100644 index 94e305b6..00000000 Binary files a/pics/77.组合2.png and /dev/null differ diff --git a/pics/77.组合3.png b/pics/77.组合3.png deleted file mode 100644 index 4ba73549..00000000 Binary files a/pics/77.组合3.png and /dev/null differ diff --git a/pics/77.组合4.png b/pics/77.组合4.png deleted file mode 100644 index b2519ccd..00000000 Binary files a/pics/77.组合4.png and /dev/null differ diff --git a/pics/78.子集.png b/pics/78.子集.png deleted file mode 100644 index 1700030f..00000000 Binary files a/pics/78.子集.png and /dev/null differ diff --git a/pics/841.钥匙和房间.png b/pics/841.钥匙和房间.png deleted file mode 100644 index 3bfdeea4..00000000 Binary files a/pics/841.钥匙和房间.png and /dev/null differ diff --git a/pics/90.子集II.png b/pics/90.子集II.png deleted file mode 100644 index 972010f6..00000000 Binary files a/pics/90.子集II.png and /dev/null differ diff --git a/pics/90.子集II1.png b/pics/90.子集II1.png deleted file mode 100644 index 92a18238..00000000 Binary files a/pics/90.子集II1.png and /dev/null differ diff --git a/pics/90.子集II2.png b/pics/90.子集II2.png deleted file mode 100644 index e42a56f7..00000000 Binary files a/pics/90.子集II2.png and /dev/null differ diff --git a/pics/925.长按键入.png b/pics/925.长按键入.png deleted file mode 100644 index c6bee552..00000000 Binary files a/pics/925.长按键入.png and /dev/null differ diff --git a/pics/93.复原IP地址.png b/pics/93.复原IP地址.png deleted file mode 100644 index 63d11f2e..00000000 Binary files a/pics/93.复原IP地址.png and /dev/null differ diff --git a/pics/941.有效的山脉数组.png b/pics/941.有效的山脉数组.png deleted file mode 100644 index e261242c..00000000 Binary files a/pics/941.有效的山脉数组.png and /dev/null differ diff --git a/pics/968.监控二叉树1.png b/pics/968.监控二叉树1.png deleted file mode 100644 index 7e6a75df..00000000 Binary files a/pics/968.监控二叉树1.png and /dev/null differ diff --git a/pics/968.监控二叉树2.png b/pics/968.监控二叉树2.png deleted file mode 100644 index 664a656e..00000000 Binary files a/pics/968.监控二叉树2.png and /dev/null differ diff --git a/pics/968.监控二叉树3.png b/pics/968.监控二叉树3.png deleted file mode 100644 index 0a3a9ea6..00000000 Binary files a/pics/968.监控二叉树3.png and /dev/null differ diff --git a/pics/977.有序数组的平方.png b/pics/977.有序数组的平方.png deleted file mode 100644 index cb9ccf6c..00000000 Binary files a/pics/977.有序数组的平方.png and /dev/null differ diff --git a/pics/98.验证二叉搜索树.png b/pics/98.验证二叉搜索树.png deleted file mode 100644 index 8a164140..00000000 Binary files a/pics/98.验证二叉搜索树.png and /dev/null differ diff --git a/pics/leetcode_209.png b/pics/leetcode_209.png deleted file mode 100644 index 2278be41..00000000 Binary files a/pics/leetcode_209.png and /dev/null differ diff --git a/pics/剑指Offer05.替换空格.png b/pics/剑指Offer05.替换空格.png deleted file mode 100644 index e4a2dfbc..00000000 Binary files a/pics/剑指Offer05.替换空格.png and /dev/null differ diff --git a/pics/剑指Offer58-II.左旋转字符串.png b/pics/剑指Offer58-II.左旋转字符串.png deleted file mode 100644 index 9b41754e..00000000 Binary files a/pics/剑指Offer58-II.左旋转字符串.png and /dev/null differ diff --git a/pics/动态规划-背包问题1.png b/pics/动态规划-背包问题1.png deleted file mode 100644 index 0dada375..00000000 Binary files a/pics/动态规划-背包问题1.png and /dev/null differ diff --git a/pics/动态规划-背包问题2.png b/pics/动态规划-背包问题2.png deleted file mode 100644 index ca639197..00000000 Binary files a/pics/动态规划-背包问题2.png and /dev/null differ diff --git a/pics/动态规划-背包问题3.png b/pics/动态规划-背包问题3.png deleted file mode 100644 index f56f316d..00000000 Binary files a/pics/动态规划-背包问题3.png and /dev/null differ diff --git a/pics/动态规划-背包问题4.png b/pics/动态规划-背包问题4.png deleted file mode 100644 index 8b26e102..00000000 Binary files a/pics/动态规划-背包问题4.png and /dev/null differ diff --git a/pics/动态规划-背包问题5.png b/pics/动态规划-背包问题5.png deleted file mode 100644 index f35fa67d..00000000 Binary files a/pics/动态规划-背包问题5.png and /dev/null differ diff --git a/pics/动态规划-背包问题6.png b/pics/动态规划-背包问题6.png deleted file mode 100644 index f1017f19..00000000 Binary files a/pics/动态规划-背包问题6.png and /dev/null differ diff --git a/pics/回溯算法理论基础.png b/pics/回溯算法理论基础.png deleted file mode 100644 index f8f2eb2f..00000000 Binary files a/pics/回溯算法理论基础.png and /dev/null differ diff --git a/pics/我要打十个.gif b/pics/我要打十个.gif deleted file mode 100644 index c64eb70a..00000000 Binary files a/pics/我要打十个.gif and /dev/null differ diff --git a/pics/螺旋矩阵.png b/pics/螺旋矩阵.png deleted file mode 100644 index 5148db46..00000000 Binary files a/pics/螺旋矩阵.png and /dev/null differ diff --git a/pics/面试题02.07.链表相交_1.png b/pics/面试题02.07.链表相交_1.png deleted file mode 100644 index 678311d1..00000000 Binary files a/pics/面试题02.07.链表相交_1.png and /dev/null differ diff --git a/pics/面试题02.07.链表相交_2.png b/pics/面试题02.07.链表相交_2.png deleted file mode 100644 index 97881bec..00000000 Binary files a/pics/面试题02.07.链表相交_2.png and /dev/null differ diff --git a/problems/0001.两数之和.md b/problems/0001.两数之和.md deleted file mode 100644 index b7c9831b..00000000 --- a/problems/0001.两数之和.md +++ /dev/null @@ -1,137 +0,0 @@ -# 题目地址 -https://leetcode-cn.com/problems/two-sum/ - -> 只用数组和set还是不够的! - -# 第1题. 两数之和 - -给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。 - -你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。 - -**示例:** - -给定 nums = [2, 7, 11, 15], target = 9 - -因为 nums[0] + nums[1] = 2 + 7 = 9 - -所以返回 [0, 1] - - -# 思路 - -很明显暴力的解法是两层for循环查找,时间复杂度是O(n^2)。 - -建议大家做这道题目之前,先做一下这两道 -* [242. 有效的字母异位词](https://mp.weixin.qq.com/s/vM6OszkM6L1Mx2Ralm9Dig) -* [349. 两个数组的交集](https://mp.weixin.qq.com/s/N9iqAchXreSVW7zXUS4BVA) - -[242. 有效的字母异位词](https://mp.weixin.qq.com/s/vM6OszkM6L1Mx2Ralm9Dig) 这道题目是用数组作为哈希表来解决哈希问题,[349. 两个数组的交集](https://mp.weixin.qq.com/s/N9iqAchXreSVW7zXUS4BVA)这道题目是通过set作为哈希表来解决哈希问题。 - -本题呢,则要使用map,那么来看一下使用数组和set来做哈希法的局限。 - -* 数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。 -* set是一个集合,里面放的元素只能是一个key,而两数之和这道题目,不仅要判断y是否存在而且还要记录y的下表位置,因为要返回x 和 y的下表。所以set 也不能用。 - -此时就要选择另一种数据结构:map ,map是一种key value的存储结构,可以用key保存数值,用value在保存数值所在的下表。 - -C++中map,有三种类型: - -|映射 |底层实现 | 是否有序 |数值是否可以重复 | 能否更改数值|查询效率 |增删效率| -|---|---| --- |---| --- | --- | ---| -|std::map |红黑树 |key有序 |key不可重复 |key不可修改 | O(logn)|O(logn) | -|std::multimap | 红黑树|key有序 | key可重复 | key不可修改|O(logn) |O(logn) | -|std::unordered_map |哈希表 | key无序 |key不可重复 |key不可修改 |O(1) | O(1)| - -std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。 - -同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。 更多哈希表的理论知识请看[关于哈希表,你该了解这些!](https://mp.weixin.qq.com/s/g8N6WmoQmsCUw3_BaWxHZA)。 - -**这道题目中并不需要key有序,选择std::unordered_map 效率更高!** - -解题思路动画如下: - - - -# C++代码 - -``` -class Solution { -public: - vector twoSum(vector& nums, int target) { - std::unordered_map map; - for(int i = 0; i < nums.size(); i++) { - auto iter = map.find(target - nums[i]); - if(iter != map.end()) { - return {iter->second, i}; - } - map.insert(pair(nums[i], i)); - } - return {}; - } -}; -``` - -## 一般解法 - -代码: - -```C++ -``` - -## 优化解法 - -```C++ -class Solution { -public: - vector twoSum(vector& nums, int target) { - for (int i = 0; i < nums.size(); i ++) { - for (int j = i + 1; j < nums.size(); j++) { - if (nums[i] + nums[j] == target) { - return {i, j}; - } - } - } - return {}; - } -}; - -``` - -``` -class Solution { -public: - vector twoSum(vector& nums, int target) { - std::map map; - for(int i = 0; i < nums.size(); i++) { - auto iter = map.find(target - nums[i]); - if(iter != map.end()) { - return {iter->second,i}; - } - map.insert({nums, i}); - } - return {}; - } -}; -``` - -``` -class Solution { -public: - vector twoSum(vector& nums, int target) { - std::unordered_map map; - for(int i = 0; i < nums.size(); i++) { - auto iter = map.find(target - nums[i]); - if(iter != map.end()) { - return {iter->second, i}; - break; - } - map.emplace(nums[i], i); - } - return {}; - } -}; -``` - -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0015.三数之和.md b/problems/0015.三数之和.md deleted file mode 100644 index c437cadb..00000000 --- a/problems/0015.三数之和.md +++ /dev/null @@ -1,166 +0,0 @@ -# 题目地址 -https://leetcode-cn.com/problems/3sum/ - -> 用哈希表解决了[两数之和](https://mp.weixin.qq.com/s/uVAtjOHSeqymV8FeQbliJQ),那么三数之和呢? - -# 第15题. 三数之和 - -给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。 - -**注意:** 答案中不可以包含重复的三元组。 - -示例: - -给定数组 nums = [-1, 0, 1, 2, -1, -4], - -满足要求的三元组集合为: -[ - [-1, 0, 1], - [-1, -1, 2] -] - - -# 思路 - -**注意[0, 0, 0, 0] 这组数据** - -## 哈希解法 - -两层for循环就可以确定 a 和b 的数值了,可以使用哈希法来确定 0-(a+b) 是否在 数组里出现过,其实这个思路是正确的,但是我们有一个非常棘手的问题,就是题目中说的不可以包含重复的三元组。 - -把符合条件的三元组放进vector中,然后在去去重,这样是非常费时的,很容易超时,也是这道题目通过率如此之低的根源所在。 - -去重的过程不好处理,有很多小细节,如果在面试中很难想到位。 - -时间复杂度可以做到O(n^2),但还是比较费时的,因为不好做剪枝操作。 - -大家可以尝试使用哈希法写一写,就知道其困难的程度了。 - -## 哈希法C++代码 -``` -class Solution { -public: - vector> threeSum(vector& nums) { - vector> result; - sort(nums.begin(), nums.end()); - // 找出a + b + c = 0 - // a = nums[i], b = nums[j], c = -(a + b) - for (int i = 0; i < nums.size(); i++) { - // 排序之后如果第一个元素已经大于零,那么不可能凑成三元组 - if (nums[i] > 0) { - continue; - } - if (i > 0 && nums[i] == nums[i - 1]) { //三元组元素a去重 - continue; - } - unordered_set set; - for (int j = i + 1; j < nums.size(); j++) { - if (j > i + 2 - && nums[j] == nums[j-1] - && nums[j-1] == nums[j-2]) { // 三元组元素b去重 - continue; - } - int c = 0 - (nums[i] + nums[j]); - if (set.find(c) != set.end()) { - result.push_back({nums[i], nums[j], c}); - set.erase(c);// 三元组元素c去重 - } else { - set.insert(nums[j]); - } - } - } - return result; - } -}; -``` - -## 双指针 - -**其实这道题目使用哈希法并不十分合适**,因为在去重的操作中有很多细节需要注意,在面试中很难直接写出没有bug的代码。 - -而且使用哈希法 在使用两层for循环的时候,能做的剪枝操作很有限,虽然时间复杂度是O(n^2),也是可以在leetcode上通过,但是程序的执行时间依然比较长 。 - -接下来我来介绍另一个解法:双指针法,**这道题目使用双指针法 要比哈希法高效一些**,那么来讲解一下具体实现的思路。 - -动画效果如下: - - - -拿这个nums数组来举例,首先将数组排序,然后有一层for循环,i从下表0的地方开始,同时定一个下表left 定义在i+1的位置上,定义下表right 在数组结尾的位置上。 - -依然还是在数组中找到 abc 使得a + b +c =0,我们这里相当于 a = nums[i] b = nums[left] c = nums[right]。 - -接下来如何移动left 和right呢, 如果nums[i] + nums[left] + nums[right] > 0 就说明 此时三数之和大了,因为数组是排序后了,所以right下表就应该向左移动,这样才能让三数之和小一些。 - -如果 nums[i] + nums[left] + nums[right] < 0 说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。 - -时间复杂度:O(n^2)。 - - -## 双指针法C++代码 - -``` -class Solution { -public: - vector> threeSum(vector& nums) { - vector> result; - sort(nums.begin(), nums.end()); - // 找出a + b + c = 0 - // a = nums[i], b = nums[left], c = nums[right] - for (int i = 0; i < nums.size(); i++) { - // 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了 - if (nums[i] > 0) { - return result; - } - // 错误去重方法,将会漏掉-1,-1,2 这种情况 - /* - if (nums[i] == nums[i + 1]) { - continue; - } - */ - // 正确去重方法 - if (i > 0 && nums[i] == nums[i - 1]) { - continue; - } - int left = i + 1; - int right = nums.size() - 1; - while (right > left) { - // 去重复逻辑如果放在这里,0,0,0 的情况,可能直接导致 right<=left 了,从而漏掉了 0,0,0 这种三元组 - /* - while (right > left && nums[right] == nums[right - 1]) right--; - while (right > left && nums[left] == nums[left + 1]) left++; - */ - if (nums[i] + nums[left] + nums[right] > 0) { - right--; - } else if (nums[i] + nums[left] + nums[right] < 0) { - left++; - } else { - result.push_back(vector{nums[i], nums[left], nums[right]}); - // 去重逻辑应该放在找到一个三元组之后 - while (right > left && nums[right] == nums[right - 1]) right--; - while (right > left && nums[left] == nums[left + 1]) left++; - - // 找到答案时,双指针同时收缩 - right--; - left++; - } - } - - } - return result; - } -}; -``` - -# 思考题 - -既然三数之和可以使用双指针法,我们之前讲过的[两数之和](https://mp.weixin.qq.com/s/uVAtjOHSeqymV8FeQbliJQ),可不可以使用双指针法呢? - -如果不能,题意如何更改就可以使用双指针法呢? **大家留言说出自己的想法吧!** - -两数之和 就不能使用双指针法,因为[两数之和](https://mp.weixin.qq.com/s/uVAtjOHSeqymV8FeQbliJQ)要求返回的是索引下表, 而双指针法一定要排序,一旦排序之后原数组的索引就被改变了。 - -如果[两数之和](https://mp.weixin.qq.com/s/uVAtjOHSeqymV8FeQbliJQ)要求返回的是数值的话,就可以使用双指针法了。 - -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0017.电话号码的字母组合.md b/problems/0017.电话号码的字母组合.md deleted file mode 100644 index e6d28496..00000000 --- a/problems/0017.电话号码的字母组合.md +++ /dev/null @@ -1,235 +0,0 @@ - -> 多个集合求组合问题。 - -# 17.电话号码的字母组合 - -题目链接:https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/ - -给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。 - -给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。 - -![17.电话号码的字母组合](https://img-blog.csdnimg.cn/2020102916424043.png) - -示例: -输入:"23" -输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]. - -说明:尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。 - -# 思路 - -从示例上来说,输入"23",最直接的想法就是两层for循环遍历了吧,正好把组合的情况都输出了。 - -如果输入"233"呢,那么就三层for循环,如果"2333"呢,就四层for循环....... - -大家应该感觉出和[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)遇到的一样的问题,就是这for循环的层数如何写出来,此时又是回溯法登场的时候了。 - -理解本题后,要解决如下三个问题: - -1. 数字和字母如何映射 -2. 两个字母就两个for循环,三个字符我就三个for循环,以此类推,然后发现代码根本写不出来 -3. 输入1 * #按键等等异常情况 - -## 数字和字母如何映射 - -可以使用map或者定义一个二位数组,例如:string letterMap[10],来做映射,我这里定义一个二维数组,代码如下: - -``` -const string letterMap[10] = { - "", // 0 - "", // 1 - "abc", // 2 - "def", // 3 - "ghi", // 4 - "jkl", // 5 - "mno", // 6 - "pqrs", // 7 - "tuv", // 8 - "wxyz", // 9 -}; -``` - -## 回溯法来解决n个for循环的问题 - -对于回溯法还不了解的同学看这篇:[关于回溯算法,你该了解这些!](https://mp.weixin.qq.com/s/gjSgJbNbd1eAA5WkA-HeWw) - - -例如:输入:"23",抽象为树形结构,如图所示: - -![17. 电话号码的字母组合](https://img-blog.csdnimg.cn/20201123200304469.png) - -图中可以看出遍历的深度,就是输入"23"的长度,而叶子节点就是我们要收集的结果,输出["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]。 - -回溯三部曲: - -* 确定回溯函数参数 - -首先需要一个字符串s来收集叶子节点的结果,然后用一个字符串数组result保存起来,这两个变量我依然定义为全局。 - -再来看参数,参数指定是有题目中给的string digits,然后还要有一个参数就是int型的index。 - -注意这个index可不是 [回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)和[回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w)中的startIndex了。 - -这个index是记录遍历第几个数字了,就是用来遍历digits的(题目中给出数字字符串),同时index也表示树的深度。 - -代码如下: - -``` -vector result; -string s; -void backtracking(const string& digits, int index) -``` - -* 确定终止条件 - -例如输入用例"23",两个数字,那么根节点往下递归两层就可以了,叶子节点就是要收集的结果集。 - -那么终止条件就是如果index 等于 输入的数字个数(digits.size)了(本来index就是用来遍历digits的)。 - -然后收集结果,结束本层递归。 - -代码如下: - -``` -if (index == digits.size()) { - result.push_back(s); - return; -} -``` - -* 确定单层遍历逻辑 - -首先要取index指向的数字,并找到对应的字符集(手机键盘的字符集)。 - -然后for循环来处理这个字符集,代码如下: - -``` -int digit = digits[index] - '0'; // 将index指向的数字转为int -string letters = letterMap[digit]; // 取数字对应的字符集 -for (int i = 0; i < letters.size(); i++) { - s.push_back(letters[i]); // 处理 - backtracking(digits, index + 1); // 递归,注意index+1,一下层要处理下一个数字了 - s.pop_back(); // 回溯 -} -``` - -**注意这里for循环,可不像是在[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)和[回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w)中从startIndex开始遍历的**。 - -**因为本题每一个数字代表的是不同集合,也就是求不同集合之间的组合,而[77. 组合](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)和[216.组合总和III](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w)都是是求同一个集合中的组合!** - - -## 输入1 * #按键等等异常情况 - -代码中最好考虑这些异常情况,但题目的测试数据中应该没有异常情况的数据,所以我就没有加了。 - -**但是要知道会有这些异常,如果是现场面试中,一定要考虑到!** - - -# C++代码 -关键地方都讲完了,按照[关于回溯算法,你该了解这些!](https://mp.weixin.qq.com/s/gjSgJbNbd1eAA5WkA-HeWw)中的回溯法模板,不难写出如下C++代码: - - -``` -// 版本一 -class Solution { -private: - const string letterMap[10] = { - "", // 0 - "", // 1 - "abc", // 2 - "def", // 3 - "ghi", // 4 - "jkl", // 5 - "mno", // 6 - "pqrs", // 7 - "tuv", // 8 - "wxyz", // 9 - }; -public: - vector result; - string s; - void backtracking(const string& digits, int index) { - if (index == digits.size()) { - result.push_back(s); - return; - } - int digit = digits[index] - '0'; // 将index指向的数字转为int - string letters = letterMap[digit]; // 取数字对应的字符集 - for (int i = 0; i < letters.size(); i++) { - s.push_back(letters[i]); // 处理 - backtracking(digits, index + 1); // 递归,注意index+1,一下层要处理下一个数字了 - s.pop_back(); // 回溯 - } - } - vector letterCombinations(string digits) { - s.clear(); - result.clear(); - if (digits.size() == 0) { - return result; - } - backtracking(digits, 0); - return result; - } -}; -``` - -一些写法,是把回溯的过程放在递归函数里了,例如如下代码,我可以写成这样:(注意注释中不一样的地方) - -``` -// 版本二 -class Solution { -private: - const string letterMap[10] = { - "", // 0 - "", // 1 - "abc", // 2 - "def", // 3 - "ghi", // 4 - "jkl", // 5 - "mno", // 6 - "pqrs", // 7 - "tuv", // 8 - "wxyz", // 9 - }; -public: - vector result; - void getCombinations(const string& digits, int index, const string& s) { // 注意参数的不同 - if (index == digits.size()) { - result.push_back(s); - return; - } - int digit = digits[index] - '0'; - string letters = letterMap[digit]; - for (int i = 0; i < letters.size(); i++) { - getCombinations(digits, index + 1, s + letters[i]); // 注意这里的不同 - } - } - vector letterCombinations(string digits) { - result.clear(); - if (digits.size() == 0) { - return result; - } - getCombinations(digits, 0, ""); - return result; - - } -}; -``` - -我不建议把回溯藏在递归的参数里这种写法,很不直观,我在[二叉树:以为使用了递归,其实还隐藏着回溯](https://mp.weixin.qq.com/s/ivLkHzWdhjQQD1rQWe6zWA)这篇文章中也深度分析了,回溯隐藏在了哪里。 - -所以大家可以按照版本一来写就可以了。 - -# 总结 - -本篇将题目的三个要点一一列出,并重点强调了和前面讲解过的[77. 组合](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)和[216.组合总和III](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w)的区别,本题是多个集合求组合,所以在回溯的搜索过程中,都有一些细节需要注意的。 - -其实本题不算难,但也处处是细节,大家还要自己亲自动手写一写。 - -**就酱,如果学到了,就帮Carl转发一波吧,让更多小伙伴知道这里!** - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/0018.四数之和.md b/problems/0018.四数之和.md deleted file mode 100644 index 34430d1c..00000000 --- a/problems/0018.四数之和.md +++ /dev/null @@ -1,108 +0,0 @@ -# 题目地址 -https://leetcode-cn.com/problems/4sum/ - -> 一样的道理,能解决四数之和 - -> 那么五数之和、六数之和、N数之和呢? - -# 第18题. 四数之和 - -题意:给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。 - -**注意:** - -答案中不可以包含重复的四元组。 - -示例: -给定数组 nums = [1, 0, -1, 0, -2, 2],和 target = 0。 -满足要求的四元组集合为: -[ - [-1, 0, 0, 1], - [-2, -1, 1, 2], - [-2, 0, 0, 2] -] - -# 思路 - -四数之和,和[三数之和](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A)是一个思路,都是使用双指针法, 基本解法就是在[三数之和](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A) 的基础上再套一层for循环。 - -但是有一些细节需要注意,例如: 不要判断`nums[k] > target` 就返回了,三数之和 可以通过 `nums[i] > 0` 就返回了,因为 0 已经是确定的数了,四数之和这道题目 target是任意值。(大家亲自写代码就能感受出来) - -[三数之和](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A)的双指针解法是一层for循环num[i]为确定值,然后循环内有left和right下表作为双指针,找到nums[i] + nums[left] + nums[right] == 0。 - -四数之和的双指针解法是两层for循环nums[k] + nums[i]为确定值,依然是循环内有left和right下表作为双指针,找出nums[k] + nums[i] + nums[left] + nums[right] == target的情况,三数之和的时间复杂度是O(n^2),四数之和的时间复杂度是O(n^3) 。 - -那么一样的道理,五数之和、六数之和等等都采用这种解法。 - -对于[三数之和](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A)双指针法就是将原本暴力O(n^3)的解法,降为O(n^2)的解法,四数之和的双指针解法就是将原本暴力O(n^4)的解法,降为O(n^3)的解法。 - -之前我们讲过哈希表的经典题目:[四数相加II](https://mp.weixin.qq.com/s/Ue8pKKU5hw_m-jPgwlHcbA),相对于本题简单很多,因为本题是要求在一个集合中找出四个数相加等于target,同时四元组不能重复。 - -而[四数相加II](https://mp.weixin.qq.com/s/Ue8pKKU5hw_m-jPgwlHcbA)是四个独立的数组,只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不用考虑有重复的四个元素相加等于0的情况,所以相对于本题还是简单了不少! - -我们来回顾一下,几道题目使用了双指针法。 - -双指针法将时间复杂度O(n^2)的解法优化为 O(n)的解法。也就是降一个数量级,题目如下: -* [0027.移除元素](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA) -* [15.三数之和](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A) -* [18.四数之和](https://mp.weixin.qq.com/s/nQrcco8AZJV1pAOVjeIU_g) - -双指针来记录前后指针实现链表反转: - -* [206.反转链表](https://mp.weixin.qq.com/s/pnvVP-0ZM7epB8y3w_Njwg) - -使用双指针来确定有环: - -* [142题.环形链表II](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA) - -双指针法在数组和链表中还有很多应用,后面还会介绍到。 - -# C++代码 -``` -class Solution { -public: - vector> fourSum(vector& nums, int target) { - vector> result; - sort(nums.begin(), nums.end()); - for (int k = 0; k < nums.size(); k++) { - // 这种剪枝是错误的,这道题目target 是任意值 - // if (nums[k] > target) { - // return result; - // } - // 去重 - if (k > 0 && nums[k] == nums[k - 1]) { - continue; - } - for (int i = k + 1; i < nums.size(); i++) { - // 正确去重方法 - if (i > k + 1 && nums[i] == nums[i - 1]) { - continue; - } - int left = i + 1; - int right = nums.size() - 1; - while (right > left) { - if (nums[k] + nums[i] + nums[left] + nums[right] > target) { - right--; - } else if (nums[k] + nums[i] + nums[left] + nums[right] < target) { - left++; - } else { - result.push_back(vector{nums[k], nums[i], nums[left], nums[right]}); - // 去重逻辑应该放在找到一个四元组之后 - while (right > left && nums[right] == nums[right - 1]) right--; - while (right > left && nums[left] == nums[left + 1]) left++; - - // 找到答案时,双指针同时收缩 - right--; - left++; - } - } - - } - } - return result; - } - -}; -``` -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0019.删除链表的倒数第N个节点.md b/problems/0019.删除链表的倒数第N个节点.md deleted file mode 100644 index b332c9de..00000000 --- a/problems/0019.删除链表的倒数第N个节点.md +++ /dev/null @@ -1,49 +0,0 @@ - - -## 思路 - -双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。 - -思路是这样的,但要注意一些细节。 - -分为如下几步: - -* 首先这里我推荐大家使用虚拟头结点,这样方面处理删除实际头结点的逻辑,如果虚拟头结点不清楚,可以看这篇: [链表:听说用虚拟头节点会方便很多?](https://mp.weixin.qq.com/s/slM1CH5Ew9XzK93YOQYSjA) - - -* 定义fast指针和slow指针,初始值为虚拟头结点,如图: - - - -* fast首先走n + 1步 ,为什么是n+1呢,因为只有这样同时移动的时候slow才能指向删除节点的上一个节点(方便做删除操作),如图: - - -* fast和slow同时移动,之道fast指向末尾,如题: - - -* 删除slow指向的下一个节点,如图: - - -此时不难写出如下C++代码: - -``` -class Solution { -public: - ListNode* removeNthFromEnd(ListNode* head, int n) { - ListNode* dummyHead = new ListNode(0); - dummyHead->next = head; - ListNode* slow = dummyHead; - ListNode* fast = dummyHead; - while(n-- && fast != NULL) { - fast = fast->next; - } - fast = fast->next; // fast再提前走一步,因为需要让slow指向删除节点的上一个节点 - while (fast != NULL) { - fast = fast->next; - slow = slow->next; - } - slow->next = slow->next->next; - return dummyHead->next; - } -}; -``` diff --git a/problems/0020.有效的括号.md b/problems/0020.有效的括号.md deleted file mode 100644 index 293c53dd..00000000 --- a/problems/0020.有效的括号.md +++ /dev/null @@ -1,128 +0,0 @@ - -## 题目地址 - -https://leetcode-cn.com/problems/valid-parentheses/ - -> 数据结构与算法应用往往隐藏在我们看不到的地方 - -# 20. 有效的括号 - -给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。 - -有效字符串需满足: -* 左括号必须用相同类型的右括号闭合。 -* 左括号必须以正确的顺序闭合。 -* 注意空字符串可被认为是有效字符串。 - -示例 1: -输入: "()" -输出: true - -示例 2: -输入: "()[]{}" -输出: true - -示例 3: -输入: "(]" -输出: false - -示例 4: -输入: "([)]" -输出: false - -示例 5: -输入: "{[]}" -输出: true - -# 思路 - -## 题外话 - -**括号匹配是使用栈解决的经典问题。** - -题意其实就像我们在写代码的过程中,要求括号的顺序是一样的,有左括号,相应的位置必须要有右括号。 - -如果还记得编译原理的话,编译器在 词法分析的过程中处理括号、花括号等这个符号的逻辑,也是使用了栈这种数据结构。 - -再举个例子,linux系统中,cd这个进入目录的命令我们应该再熟悉不过了。 - -``` -cd a/b/c/../../ -``` - -这个命令最后进入a目录,系统是如何知道进入了a目录呢 ,这就是栈的应用(其实可以出一道相应的面试题了) - -所以栈在计算机领域中应用是非常广泛的。 - -有的同学经常会想学的这些数据结构有什么用,也开发不了什么软件,大多数同学说的软件应该都是可视化的软件例如APP、网站之类的,那都是非常上层的应用了,底层很多功能的实现都是基础的数据结构和算法。 - -**所以数据结构与算法的应用往往隐藏在我们看不到的地方!** - -这里我就不过多展开了,先来看题。 - -## 进入正题 - -由于栈结构的特殊性,非常适合做对称匹配类的题目。 - -首先要弄清楚,字符串里的括号不匹配有几种情况。 - -**一些同学,在面试中看到这种题目上来就开始写代码,然后就越写越远。** - -建议要写代码之前要分析好有哪几种不匹配的情况,如果不动手之前分析好,写出的代码也会有很多问题。 - -先来分析一下 这里有三种不匹配的情况, - -1. 第一种情况,字符串里左方向的括号多余了 ,所以不匹配。 -![括号匹配1](https://img-blog.csdnimg.cn/2020080915505387.png) -2. 第二种情况,括号没有多余,但是 括号的类型没有匹配上。 -![括号匹配2](https://img-blog.csdnimg.cn/20200809155107397.png) -3. 第三种情况,字符串里右方向的括号多余了,所以不匹配。 -![括号匹配3](https://img-blog.csdnimg.cn/20200809155115779.png) - -我们的代码只要覆盖了这三种不匹配的情况,就不会出问题,可以看出 动手之前分析好题目的重要性。 - -动画如下: - - - - -第一种情况:已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false - -第二种情况:遍历字符串匹配的过程中,发现栈里没有要匹配的字符。所以return false - -第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号return false - -那么什么时候说明左括号和右括号全都匹配了呢,就是字符串遍历完之后,栈是空的,就说明全都匹配了。 - -分析完之后,代码其实就比较好写了, - -但还有一些技巧,在匹配左括号的时候,右括号先入栈,就只需要比较当前元素和栈顶相不相等就可以了,比左括号先入栈代码实现要简单的多了! - -实现代码如下: - -## C++代码 - - -``` -class Solution { -public: - bool isValid(string s) { - stack st; - for (int i = 0; i < s.size(); i++) { - if (s[i] == '(') st.push(')'); - else if (s[i] == '{') st.push('}'); - else if (s[i] == '[') st.push(']'); - // 第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号 return false - // 第二种情况:遍历字符串匹配的过程中,发现栈里没有我们要匹配的字符。所以return false - else if (st.empty() || st.top() != s[i]) return false; - else st.pop(); // st.top() 与 s[i]相等,栈弹出元素 - } - // 第一种情况:此时我们已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false,否则就return true - return st.empty(); - } -}; -``` -技巧性的东西没有固定的学习方法,还是要多看多练,自己总灵活运用了。 - -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0021.合并两个有序链表.md b/problems/0021.合并两个有序链表.md deleted file mode 100644 index 0272272a..00000000 --- a/problems/0021.合并两个有序链表.md +++ /dev/null @@ -1,60 +0,0 @@ -> 更多精彩文章持续更新,微信搜索「代码随想录」第一时间围观,本文[https://github.com/youngyangyang04/TechCPP](https://github.com/youngyangyang04/TechCPP) 已经收录,里面有更多干货等着你,欢迎Star! - -## 题目地址 -https://leetcode-cn.com/problems/merge-two-sorted-lists/ - -## 思路 - -链表的基本操作,一下代码中有详细注释 - - -## 代码 - -``` -/** - * 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: -// 题目中我们为什么定义了一个 新的head - ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) { - ListNode* head = new ListNode(); - ListNode* index = head; - while(l1 != nullptr && l2 != nullptr) { // 注意null 和nullptr的区别 - if (l1->val < l2->val) { - index->next = l1; - l1 = l1->next; - } else { - index->next = l2; - l2 = l2->next; - } - index = index->next; - } - if (l1 != nullptr) { - index->next = l1; - l1 = l1->next; - } - if (l2 != nullptr) { - index->next = l2; - l2 = l2->next; - } - ListNode* tmp = head; // 注意清理内存,不清理也没事 - head = head->next; - delete tmp; - return head; - // return head->next - - } -}; -``` - -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0024.两两交换链表中的节点.md b/problems/0024.两两交换链表中的节点.md deleted file mode 100644 index 92f9b294..00000000 --- a/problems/0024.两两交换链表中的节点.md +++ /dev/null @@ -1,70 +0,0 @@ - - -## 题目地址 - -## 思路 - -这道题目正常模拟就可以了。 - -建议使用虚拟头结点,这样会方便很多,要不然每次针对头结点(没有前一个指针指向头结点),还要单独处理。 - -对虚拟头结点的操作,还不熟悉的话,可以看这篇[链表:听说用虚拟头节点会方便很多?](https://mp.weixin.qq.com/s/slM1CH5Ew9XzK93YOQYSjA)。 - -接下来就是交换相邻两个元素了,**此时一定要画图,不画图,操作多个指针很容易乱,而且要操作的先后顺序** - -初始时,cur指向虚拟头结点,然后进行如下三步: - - - -操作之后,链表如下: - - - - -看这个可能就更直观一些了: - - - - -对应的C++代码实现如下: - -``` -class Solution { -public: - ListNode* swapPairs(ListNode* head) { - ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点 - dummyHead->next = head; // 将虚拟头结点指向head,这样方面后面做删除操作 - ListNode* cur = dummyHead; - while(cur->next != nullptr && cur->next->next != nullptr) { - ListNode* tmp = cur->next; // 记录临时节点 - ListNode* tmp1 = cur->next->next->next; // 记录临时节点 - - cur->next = cur->next->next; // 步骤一 - cur->next->next = tmp; // 步骤二 - cur->next->next->next = tmp1; // 步骤三 - - cur = cur->next->next; // cur移动两位,准备下一轮交换 - } - return dummyHead->next; - } -}; -``` -时间复杂度:O(n) -空间复杂度:O(1) - -## 拓展 - -**这里还是说一下,大家不必太在意leetcode上执行用时,打败多少多少用户,这个就是一个玩具,非常不准确。** - -做题的时候自己能分析出来时间复杂度就可以了,至于leetcode上执行用时,大概看一下就行。 - -上面的代码我第一次提交执行用时8ms,打败6.5%的用户,差点吓到我了。 - -心想应该没有更好的方法了吧,也就O(n)的时间复杂度,重复提交几次,这样了: - - - -所以,不必过于在意leetcode上这个统计。 - - - diff --git a/problems/0026.删除排序数组中的重复项.md b/problems/0026.删除排序数组中的重复项.md deleted file mode 100644 index 114f2da2..00000000 --- a/problems/0026.删除排序数组中的重复项.md +++ /dev/null @@ -1,49 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/ - -# 思路 - -此题使用双指针法,O(n)的时间复杂度,拼速度的话,可以剪剪枝。 - -注意题目中:不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。 - -双指针法,动画如下: - - - -其实**双指针法在在数组和链表的操作中是非常常见的,很多考察数组和链表操作的面试题,都使用双指针法,可以将时间复杂度O(n^2)的解法优化为 O(n)的解法,例如:** - -* [0015.三数之和](https://github.com/youngyangyang04/leetcode/blob/master/problems/0015.三数之和.md) -* [0018.四数之和](https://github.com/youngyangyang04/leetcode/blob/master/problems/0018.四数之和.md) -* [0026.删除排序数组中的重复项](https://github.com/youngyangyang04/leetcode/blob/master/problems/0026.删除排序数组中的重复项.md) -* [0206.翻转链表](https://github.com/youngyangyang04/leetcode/blob/master/problems/0206.翻转链表.md) -* [0344.反转字符串](https://github.com/youngyangyang04/leetcode/blob/master/problems/0344.反转字符串.md) -* [剑指Offer05.替换空格](https://github.com/youngyangyang04/leetcode/blob/master/problems/剑指Offer05.替换空格.md) - -**还有链表找环,也用到双指针:** - -* [0142.环形链表II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0142.环形链表II.md) - -大家都可以去做一做,感受一下双指针法的内在逻辑! - - -# C++ 代码 - - -``` -class Solution { -public: - int removeDuplicates(vector& nums) { - if (nums.empty()) return 0; // 别忘记空数组的判断 - int slowIndex = 0; - for (int fastIndex = 0; fastIndex < (nums.size() - 1); fastIndex++){ - if(nums[fastIndex] != nums[fastIndex + 1]) { // 发现和后一个不相同 - nums[++slowIndex] = nums[fastIndex + 1]; //slowIndex = 0 的数据一定是不重复的,所以直接 ++slowIndex - } - } - return slowIndex + 1; //别忘了slowIndex是从0开始的,所以返回slowIndex + 1 - } -}; -``` -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0027.移除元素.md b/problems/0027.移除元素.md deleted file mode 100644 index 637bd5ad..00000000 --- a/problems/0027.移除元素.md +++ /dev/null @@ -1,127 +0,0 @@ -

- -

-

- - - - - - -

- -> 移除元素想要高效的话,不是很简单! - -# 编号:27. 移除元素 - -题目地址:https://leetcode-cn.com/problems/remove-element/ - -给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。 - -不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并**原地**修改输入数组。 - -元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。 - -示例 1: -给定 nums = [3,2,2,3], val = 3, -函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。 -你不需要考虑数组中超出新长度后面的元素。 - -示例 2: -给定 nums = [0,1,2,2,3,0,4,2], val = 2, -函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。 - -**你不需要考虑数组中超出新长度后面的元素。** - -# 思路 - -有的同学可能说了,多余的元素,删掉不就得了。 - -**要知道数组的元素在内存地址中是连续的,不能单独删除数组中的某个元素,只能覆盖。** - -数组的基础知识可以看这里[程序员算法面试中,必须掌握的数组理论知识](https://mp.weixin.qq.com/s/X7R55wSENyY62le0Fiawsg)。 - -# 暴力解法 - -这个题目暴力的解法就是两层for循环,一个for循环遍历数组元素 ,第二个for循环更新数组。 - -删除过程如下: - - - -很明显暴力解法的时间复杂度是O(n^2),这道题目暴力解法在leetcode上是可以过的。 - -# 暴力解法C++代码 - -``` -// 时间复杂度:O(n^2) -// 空间复杂度:O(1) -class Solution { -public: - int removeElement(vector& nums, int val) { - int size = nums.size(); - for (int i = 0; i < size; i++) { - if (nums[i] == val) { // 发现需要移除的元素,就将数组集体向前移动一位 - for (int j = i + 1; j < size; j++) { - nums[j - 1] = nums[j]; - } - i--; // 因为下表i以后的数值都向前移动了一位,所以i也向前移动一位 - size--; // 此时数组的大小-1 - } - } - return size; - - } -}; -``` - -# 双指针法 - -双指针法(快慢指针法): **通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。** - -删除过程如下: - - - -**双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组和链表操作的面试题,都使用双指针法。** - -我们来回顾一下,之前已经讲过有四道题目使用了双指针法。 - -双指针法将时间复杂度O(n^2)的解法优化为 O(n)的解法。也就是降一个数量级,题目如下: - -* [15.三数之和](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A) -* [18.四数之和](https://mp.weixin.qq.com/s/nQrcco8AZJV1pAOVjeIU_g) - -双指针来记录前后指针实现链表反转: - -* [206.反转链表](https://mp.weixin.qq.com/s/pnvVP-0ZM7epB8y3w_Njwg) - -使用双指针来确定有环: - -* [142题.环形链表II](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA) - -双指针法在数组和链表中还有很多应用,后面还会介绍到。 - -# 双指针法C++代码: -``` -// 时间复杂度:O(n) -// 空间复杂度:O(1) -class Solution { -public: - int removeElement(vector& nums, int val) { - int slowIndex = 0; - for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) { - if (val != nums[fastIndex]) { - nums[slowIndex++] = nums[fastIndex]; - } - } - return slowIndex; - } -}; -``` - -**循序渐进学算法,认准「代码随想录」,Carl手把手带你过关斩将!** - -

- -

diff --git a/problems/0028.实现strStr().md b/problems/0028.实现strStr().md deleted file mode 100644 index f6e689d7..00000000 --- a/problems/0028.实现strStr().md +++ /dev/null @@ -1,538 +0,0 @@ - -## 题目地址 -https://leetcode-cn.com/problems/implement-strstr/ - -> 在一个串中查找是否出现过另一个串,这是KMP的看家本领。 - -# 题目:28. 实现 strStr() - -实现 strStr() 函数。 - -给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回  -1。 - -示例 1: -输入: haystack = "hello", needle = "ll" -输出: 2 - -示例 2: -输入: haystack = "aaaaa", needle = "bba" -输出: -1 - -说明: -当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。 -对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与C语言的 strstr() 以及 Java的 indexOf() 定义相符。 - - -# 思路 - -本题是KMP 经典题目。 - -以下文字如果看不进去,可以看我的B站视频: - -* [帮你把KMP算法学个通透!B站(理论篇)](https://www.bilibili.com/video/BV1PD4y1o7nd/) -* [帮你把KMP算法学个通透!(求next数组代码篇)](https://www.bilibili.com/video/BV1M5411j7Xx) - -KMP的经典思想就是:**当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。** - -本篇将以如下顺序来讲解KMP, - - -* 什么是KMP -* KMP有什么用 -* 什么是前缀表 -* 为什么一定要用前缀表 -* 如何计算前缀表 -* 前缀表与next数组 -* 使用next数组来匹配 -* 时间复杂度分析 -* 构造next数组 -* 使用next数组来做匹配 -* 前缀表统一减一 C++代码实现 -* 前缀表(不减一)C++实现 -* 总结 - - -读完本篇可以顺便,把leetcode上28.实现strStr()题目做了。 - -如果文字实在看不下去,就看我在B站上的视频吧,如下: - -* [帮你把KMP算法学个通透!(理论篇)B站](https://www.bilibili.com/video/BV1PD4y1o7nd/) -* [帮你把KMP算法学个通透!(求next数组代码篇)B站](https://www.bilibili.com/video/BV1M5411j7Xx/) - - -# 什么是KMP - -说到KMP,先说一下KMP这个名字是怎么来的,为什么叫做KMP呢。 - -因为是由这三位学者发明的:Knuth,Morris和Pratt,所以取了三位学者名字的首字母。所以叫做KMP - -# KMP有什么用 - -KMP主要应用在字符串匹配上。 - -KMP的主要思想是**当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。** - -所以如何记录已经匹配的文本内容,是KMP的重点,也是next数组肩负的重任。 - -其实KMP的代码不好理解,一些同学甚至直接把KMP代码的模板背下来。 - -没有彻底搞懂,懵懵懂懂就把代码背下来太容易忘了。 - -不仅面试的时候可能写不出来,如果面试官问:**next数组里的数字表示的是什么,为什么这么表示?** - -估计大多数候选人都是懵逼的。 - -下面Carl就带大家把KMP的精髓,next数组弄清楚。 - -# 什么是前缀表 - -写过KMP的同学,一定都写过next数组,那么这个next数组究竟是个啥呢? - -next数组就是一个前缀表(prefix table)。 - -前缀表有什么作用呢? - -**前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。** - -为了清楚的了解前缀表的来历,我们来举一个例子: - -要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。 - -请记住文本串和模式串的作用,对于理解下文很重要,要不然容易看懵。所以说三遍: - -要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。 - -要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。 - -要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。 - -如动画所示: - - - -动画里,我特意把 子串`aa` 标记上了,这是有原因的,大家先注意一下,后面还会说道。 - -可以看出,文本串中第六个字符b 和 模式串的第六个字符f,不匹配了。如果暴力匹配,会发现不匹配,此时就要从头匹配了。 - -但如果使用前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配,找到了模式串中第三个字符b继续开始匹配。 - -此时就要问了**前缀表是如何记录的呢?** - -首先要知道前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,在重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置。 - -那么什么是前缀表:**记录下表i之前(包括i)的字符串中,有多大长度的相同前缀后缀。** - -# 为什么一定要用前缀表 - -这就是前缀表那为啥就能告诉我们 上次匹配的位置,并跳过去呢? - -回顾一下,刚刚匹配的过程在下表5的地方遇到不匹配,模式串是指向f,如图: - - - -然后就找到了下表2,指向b,继续匹配:如图: - - -以下这句话,对于理解为什么使用前缀表可以告诉我们匹配失败之后跳到哪里重新匹配 非常重要! - -**下表5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面从新匹配就可以了。** - -所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。 - -**很多介绍KMP的文章或者视频并没有把为什么要用前缀表?这个问题说清楚,而是直接默认使用前缀表。** - -# 如何计算前缀表 - -接下来就要说一说怎么计算前缀表。 - -如图: - - - -长度为前1个字符的子串`a`,最长相同前后缀的长度为0。(注意字符串的**前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串**;**后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串**。) - - -长度为前2个字符的子串`aa`,最长相同前后缀的长度为1。 - - -长度为前3个字符的子串`aab`,最长相同前后缀的长度为0。 - -以此类推: -长度为前4个字符的子串`aaba`,最长相同前后缀的长度为1。 -长度为前5个字符的子串`aabaa`,最长相同前后缀的长度为2。 -长度为前6个字符的子串`aabaaf`,最长相同前后缀的长度为0。 - -那么把求得的最长相同前后缀的长度就是对应前缀表的元素,如图: - - -可以看出模式串与前缀表对应位置的数字表示的就是:**下表i之前(包括i)的字符串中,有多大长度的相同前缀后缀。** - -再来看一下如何利用 前缀表找到 当字符不匹配的时候应该指针应该移动的位置。如动画所示: - - - -找到的不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少。 - -为什么要前一个字符的前缀表的数值呢,因为要找前面字符串的最长相同的前缀和后缀。 - -所以要看前一位的 前缀表的数值。 - -前一个字符的前缀表的数值是2, 所有把下表移动到下表2的位置继续比配。 可以再反复看一下上面的动画。 - -最后就在文本串中找到了和模式串匹配的子串了。 - -# 前缀表与next数组 - -很多KMP算法的时间都是使用next数组来做回退操作,那么next数组与前缀表有什么关系呢? - -next数组就可以是前缀表,但是很多实现都是把前缀表统一减一(右移一位,初始位置为-1)之后作为next数组。 - -为什么这么做呢,其实也是很多文章视频没有解释清楚的地方。 - -其实**这并不涉及到KMP的原理,而是具体实现,next数组即可以就是前缀表,也可以是前缀表统一减一(右移一位,初始位置为-1)。** - -后面我会提供两种不同的实现代码,大家就明白了了。 - -# 使用next数组来匹配 - -以下我们以前缀表统一减一之后的next数组来做演示。 - -有了next数组,就可以根据next数组来 匹配文本串s,和模式串t了。 - -注意next数组是新前缀表(旧前缀表统一减一了)。 - -匹配过程动画如下: - - - -# 时间复杂度分析 - - -其中n为文本串长度,m为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。 - -暴力的解法显而易见是O(n * m),所以**KMP在字符串匹配中极大的提高的搜索的效率。** - -为了和[字符串:KMP是时候上场了(一文读懂系列)](https://mp.weixin.qq.com/s/70OXnZ4Ez29CKRrUpVJmug)字符串命名统一,方便大家理解,以下文章统称haystack为文本串, needle为模式串。 - -都知道使用KMP算法,一定要构造next数组。 - -# 构造next数组 - -我们定义一个函数getNext来构建next数组,函数参数为指向next数组的指针,和一个字符串。 代码如下: - -``` -void getNext(int* next, const string& s) -``` - -**构造next数组其实就是计算模式串s,前缀表的过程。** 主要有如下三步: - -1. 初始化 -2. 处理前后缀不相同的情况 -3. 处理前后缀相同的情况 - -接下来我们详解详解一下。 - -1. 初始化: - -定义两个指针i和j,j指向前缀终止位置(严格来说是终止位置减一的位置),i指向后缀终止位置(与j同理)。 - -然后还要对next数组进行初始化赋值,如下: - -``` -int j = -1; -next[0] = j; -``` - -j 为什么要初始化为 -1呢,因为之前说过 前缀表要统一减一的操作仅仅是其中的一种实现,我们这里选择j初始化为-1,下文我还会给出j不初始化为-1的实现代码。 - -next[i] 表示 i(包括i)之前最长相等的前后缀长度(其实就是j) - -所以初始化next[0] = j 。 - - -2. 处理前后缀不相同的情况 - - -因为j初始化为-1,那么i就从1开始,进行s[i] 与 s[j+1]的比较。 - -所以遍历模式串s的循环下表i 要从 1开始,代码如下: - -``` -for(int i = 1; i < s.size(); i++) { -``` - -如果 s[i] 与 s[j+1]不相同,也就是遇到 前后缀末尾不相同的情况,就要向前回溯。 - -怎么回溯呢? - -next[j]就是记录着j(包括j)之前的子串的相同前后缀的长度。 - -那么 s[i] 与 s[j+1] 不相同,就要找 j+1前一个元素在next数组里的值(就是next[j])。 - -所以,处理前后缀不相同的情况代码如下: - -``` -while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了 -    j = next[j]; // 向前回溯 -} -``` - -3. 处理前后缀相同的情况 - -如果s[i] 与 s[j + 1] 相同,那么就同时向后移动i 和j 说明找到了相同的前后缀,同时还要将j(前缀的长度)赋给next[i], 因为next[i]要记录相同前后缀的长度。 - -代码如下: - -``` -if (s[i] == s[j + 1]) { // 找到相同的前后缀 -    j++; -} -next[i] = j; -``` - -最后整体构建next数组的函数代码如下: - -``` -void getNext(int* next, const string& s){ -    int j = -1; -    next[0] = j; -    for(int i = 1; i < s.size(); i++) { // 注意i从1开始 -        while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了 -            j = next[j]; // 向前回溯 -        } -        if (s[i] == s[j + 1]) { // 找到相同的前后缀 -            j++; -        } -        next[i] = j; // 将j(前缀的长度)赋给next[i] -    } -} -``` - - -代码构造next数组的逻辑流程动画如下: - - - - -得到了next数组之后,就要用这个来做匹配了。 - -# 使用next数组来做匹配 - -在文本串s里 找是否出现过模式串t。 - -定义两个下表j 指向模式串起始位置,i指向文本串起始位置。 - -那么j初始值依然为-1,为什么呢? **依然因为next数组里记录的起始位置为-1。** - -i就从0开始,遍历文本串,代码如下: - -``` -for (int i = 0; i < s.size(); i++)  -``` - -接下来就是 s[i] 与 t[j + 1] (因为j从-1开始的) 经行比较。 - -如果 s[i] 与 t[j + 1] 不相同,j就要从next数组里寻找下一个匹配的位置。 - -代码如下: - -``` -while(j >= 0 && s[i] != t[j + 1]) { -    j = next[j]; -} -``` - -如果 s[i] 与 t[j + 1] 相同,那么i 和 j 同时向后移动, 代码如下: - -``` -if (s[i] == t[j + 1]) { -    j++; // i的增加在for循环里 -} -``` - -如何判断在文本串s里出现了模式串t呢,如果j指向了模式串t的末尾,那么就说明模式串t完全匹配文本串s里的某个子串了。 - -本题要在文本串字符串中找出模式串出现的第一个位置 (从0开始),所以返回当前在文本串匹配模式串的位置i 减去 模式串的长度,就是文本串字符串中出现模式串的第一个位置。 - -代码如下: - -``` -if (j == (t.size() - 1) ) { -    return (i - t.size() + 1); -} -``` - -那么使用next数组,用模式串匹配文本串的整体代码如下: - -``` -int j = -1; // 因为next数组里记录的起始位置为-1 -for (int i = 0; i < s.size(); i++) { // 注意i就从0开始 -    while(j >= 0 && s[i] != t[j + 1]) { // 不匹配 -        j = next[j]; // j 寻找之前匹配的位置 -    } -    if (s[i] == t[j + 1]) { // 匹配,j和i同时向后移动 -        j++; // i的增加在for循环里 -    } -    if (j == (t.size() - 1) ) { // 文本串s里出现了模式串t -        return (i - t.size() + 1); -    } -} -``` - -此时所有逻辑的代码都已经写出来了,本题整体代码如下: - -# 前缀表统一减一 C++代码实现 - -``` -class Solution { -public: -    void getNext(int* next, const string& s) { -        int j = -1; -        next[0] = j; -        for(int i = 1; i < s.size(); i++) { // 注意i从1开始 -            while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了 -                j = next[j]; // 向前回溯 -            } -            if (s[i] == s[j + 1]) { // 找到相同的前后缀 -                j++; -            } -            next[i] = j; // 将j(前缀的长度)赋给next[i] -        } -    } - int strStr(string haystack, string needle) { - if (needle.size() == 0) { - return 0; - } - int next[needle.size()]; - getNext(next, needle); - int j = -1; // // 因为next数组里记录的起始位置为-1 - for (int i = 0; i < haystack.size(); i++) { // 注意i就从0开始 - while(j >= 0 && haystack[i] != needle[j + 1]) { // 不匹配 - j = next[j]; // j 寻找之前匹配的位置 - } - if (haystack[i] == needle[j + 1]) { // 匹配,j和i同时向后移动 - j++; // i的增加在for循环里 - } - if (j == (needle.size() - 1) ) { // 文本串s里出现了模式串t - return (i - needle.size() + 1); - } - } - return -1; - } -}; - -``` - -# 前缀表(不减一)C++实现 - -那么前缀表就不减一了,也不右移的,到底行不行呢?行! - -我之前说过,这仅仅是KMP算法实现上的问题,如果就直接使用前缀表可以换一种回退方式,找j=next[j-1] 来进行回退。 - -主要就是j=next[x]这一步最为关键! - -我给出的getNext的实现为:(前缀表统一减一) - -``` -void getNext(int* next, const string& s) { -    int j = -1; -    next[0] = j; -    for(int i = 1; i < s.size(); i++) { // 注意i从1开始 -        while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了 -            j = next[j]; // 向前回溯 -        } -        if (s[i] == s[j + 1]) { // 找到相同的前后缀 -            j++; -        } -        next[i] = j; // 将j(前缀的长度)赋给next[i] -    } -} - -``` -此时如果输入的模式串为aabaaf,对应的next为-1 0 -1 0 1 -1。 - -这里j和next[0]初始化为-1,整个next数组是以 前缀表减一之后的效果来构建的。 - -那么前缀表不减一来构建next数组,代码如下: - -``` - void getNext(int* next, const string& s) { - int j = 0; - next[0] = 0; - for(int i = 1; i < s.size(); i++) { - while (j > 0 && s[i] != s[j]) { // j要保证大于0,因为下面有取j-1作为数组下表的操作 - j = next[j - 1]; // 注意这里,是要找前一位的对应的回退位置了 - } - if (s[i] == s[j]) { - j++; - } - next[i] = j; - } - } - -``` - -此时如果输入的模式串为aabaaf,对应的next为 0 1 0 1 2 0,(其实这就是前缀表的数值了)。 - -那么用这样的next数组也可以用来做匹配,代码要有所改动。 - -实现代码如下: - -``` -class Solution { -public: - void getNext(int* next, const string& s) { - int j = 0; - next[0] = 0; - for(int i = 1; i < s.size(); i++) { - while (j > 0 && s[i] != s[j]) { - j = next[j - 1]; - } - if (s[i] == s[j]) { - j++; - } - next[i] = j; - } - } - int strStr(string haystack, string needle) { - if (needle.size() == 0) { - return 0; - } - int next[needle.size()]; - getNext(next, needle); - int j = 0; - for (int i = 0; i < haystack.size(); i++) { - while(j > 0 && haystack[i] != needle[j]) { - j = next[j - 1]; - } - if (haystack[i] == needle[j]) { - j++; - } - if (j == needle.size() ) { - return (i - needle.size() + 1); - } - } - return -1; - } -}; -``` - -# 总结 - -我们介绍了什么是KMP,KMP可以解决什么问题,然后分析KMP算法里的next数组,知道了next数组就是前缀表,再分析为什么要是前缀表而不是什么其他表。 - -接着从给出的模式串中,我们一步一步的推导出了前缀表,得出前缀表无论是统一减一还是不同意减一得到的next数组仅仅是kmp的实现方式的不同。 - -其中还分析了KMP算法的时间复杂度,并且和暴力方法做了对比。 - -然后先用前缀表统一减一得到的next数组,求得文本串s里是否出现过模式串t,并给出了具体分析代码。 - -又给出了直接用前缀表作为next数组,来做匹配的实现代码。 - -可以说把KMP的每一个细微的细节都扣了出来,毫无遮掩的展示给大家了! - - - - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0031.下一个排列.md b/problems/0031.下一个排列.md deleted file mode 100644 index 68665d6b..00000000 --- a/problems/0031.下一个排列.md +++ /dev/null @@ -1,58 +0,0 @@ - -## 思路 - -一些同学可能手动写排列的顺序,都没有写对,那么写程序的话思路一定是有问题的了,我这里以1234为例子,把全排列都列出来。可以参考一下规律所在: - -``` -1 2 3 4 -1 2 4 3 -1 3 2 4 -1 3 4 2 -1 4 2 3 -1 4 3 2 -2 1 3 4 -2 1 4 3 -2 3 1 4 -2 3 4 1 -2 4 1 3 -2 4 3 1 -3 1 2 4 -3 1 4 2 -3 2 1 4 -3 2 4 1 -3 4 1 2 -3 4 2 1 -4 1 2 3 -4 1 3 2 -4 2 1 3 -4 2 3 1 -4 3 1 2 -4 3 2 1 -``` - -如图: - -以求1243为例,流程如图: - - - -对应的C++代码如下: - -``` -class Solution { -public: - void nextPermutation(vector& nums) { - for (int i = nums.size() - 1; i >= 0; i--) { - for (int j = nums.size() - 1; j > i; j--) { - if (nums[j] > nums[i]) { - swap(nums[j], nums[i]); - sort(nums.begin() + i + 1, nums.end()); - return; - } - } - } - // 到这里了说明整个数组都是倒叙了,反转一下便可 - reverse(nums.begin(), nums.end()); - } -}; -``` diff --git a/problems/0034.在排序数组中查找元素的第一个和最后一个位置.md b/problems/0034.在排序数组中查找元素的第一个和最后一个位置.md deleted file mode 100644 index a62261e9..00000000 --- a/problems/0034.在排序数组中查找元素的第一个和最后一个位置.md +++ /dev/null @@ -1,134 +0,0 @@ -> **如果对二分查找比较模糊,建议看这篇:[为什么每次遇到二分法,都是一看就会,一写就废](https://mp.weixin.qq.com/s/fCf5QbPDtE6SSlZ1yh_q8Q),这里详细介绍了二分的两种写法,以及循环不变量的重要性,顺便还可以把「leetcode:35.搜索插入位置」题目刷了**。 - -这道题目如果基础不是很好,不建议大家看简短的代码,简短的代码隐藏了太多逻辑,结果就是稀里糊涂把题AC了,但是没有想清楚具体细节! - -下面我来把所有情况都讨论一下。 - -寻找target在数组里的左右边界,有如下三种情况: - -* 情况一:target 在数组范围的右边或者左边,例如数组{3, 4, 5},target为2或者数组{3, 4, 5},target为6,此时应该返回{-1, -1} -* 情况二:target 在数组范围中,且数组中不存在target,例如数组{3,6,7},target为5,此时应该返回{-1, -1} -* 情况三:target 在数组范围中,且数组中存在target,例如数组{3,6,7},target为6,此时应该返回{1, 1} - -这三种情况都考虑到,说明就想的很清楚了。 - -接下来,在去寻找左边界,和右边界了。 - -采用二分法来取寻找左右边界,为了让代码清晰,我分别写两个二分来寻找左边界和右边界。 - -**刚刚接触二分搜索的同学不建议上来就像如果用一个二分来查找左右边界,很容易把自己绕进去,建议扎扎实实的写两个二分分别找左边界和右边界** - -## 寻找右边界 - -先来寻找右边界,至于二分查找,如果看过[为什么每次遇到二分法,都是一看就会,一写就废](https://mp.weixin.qq.com/s/fCf5QbPDtE6SSlZ1yh_q8Q)就会知道,二分查找中什么时候用while (left <= right),有什么时候用while (left < right),其实只要清楚**循环不变量**,很容易区分两种写法。 - -那么这里我采用while (left <= right)的写法,区间定义为[left, right],即左闭又闭的区间(如果这里有点看不懂了,强烈建议把[为什么每次遇到二分法,都是一看就会,一写就废](https://mp.weixin.qq.com/s/fCf5QbPDtE6SSlZ1yh_q8Q)这篇文章先看了,在把「leetcode:35.搜索插入位置」做了之后在做这道题目就好很多了) - -确定好:计算出来的右边界是不包好target的右边界,左边界同理。 - -可以写出如下代码 - -``` -// 二分查找,寻找target的右边界(不包括target) -// 如果rightBorder为没有被赋值(即target在数组范围的左边,例如数组[3,3],target为2),为了处理情况一 -int getRightBorder(vector& nums, int target) { - int left = 0; - int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right] - int rightBorder = -2; // 记录一下rightBorder没有被赋值的情况 - while (left <= right) { // 当left==right,区间[left, right]依然有效 - int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2 - if (nums[middle] > target) { - right = middle - 1; // target 在左区间,所以[left, middle - 1] - } else { // 当nums[middle] == target的时候,更新left,这样才能得到target的右边界 - left = middle + 1; - rightBorder = left; - } - } - return rightBorder; -} -``` - -## 寻找左边界 - -``` -// 二分查找,寻找target的左边界leftBorder(不包括target) -// 如果leftBorder没有被赋值(即target在数组范围的右边,例如数组[3,3],target为4),为了处理情况一 -int getLeftBorder(vector& nums, int target) { - int left = 0; - int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right] - int leftBorder = -2; // 记录一下leftBorder没有被赋值的情况 - while (left <= right) { - int middle = left + ((right - left) / 2); - if (nums[middle] >= target) { // 寻找左边界,就要在nums[middle] == target的时候更新right - right = middle - 1; - leftBorder = right; - } else { - left = middle + 1; - } - } - return leftBorder; -} -``` - -## 处理三种情况 - -左右边界计算完之后,看一下主体代码,这里把上面讨论的三种情况,都覆盖了 - -``` -class Solution { -public: - vector searchRange(vector& nums, int target) { - int leftBorder = getLeftBorder(nums, target); - int rightBorder = getRightBorder(nums, target); - // 情况一 - if (leftBorder == -2 || rightBorder == -2) return {-1, -1}; - // 情况三 - if (rightBorder - leftBorder > 1) return {leftBorder + 1, rightBorder - 1}; - // 情况二 - return {-1, -1}; - } -private: - int getRightBorder(vector& nums, int target) { - int left = 0; - int right = nums.size() - 1; - int rightBorder = -2; // 记录一下rightBorder没有被赋值的情况 - while (left <= right) { - int middle = left + ((right - left) / 2); - if (nums[middle] > target) { - right = middle - 1; - } else { // 寻找右边界,nums[middle] == target的时候更新left - left = middle + 1; - rightBorder = left; - } - } - return rightBorder; - } - int getLeftBorder(vector& nums, int target) { - int left = 0; - int right = nums.size() - 1; - int leftBorder = -2; // 记录一下leftBorder没有被赋值的情况 - while (left <= right) { - int middle = left + ((right - left) / 2); - if (nums[middle] >= target) { // 寻找左边界,nums[middle] == target的时候更新right - right = middle - 1; - leftBorder = right; - } else { - left = middle + 1; - } - } - return leftBorder; - } -}; -``` - -这份代码在简洁性很有大的优化空间,例如把寻找左右区间函数合并一起。 - -但拆开更清晰一些,而且把三种情况以及对应的处理逻辑完整的展现出来了。 - -# 总结 - -初学者建议大家一块一块的去分拆这道题目,正如本题解描述,想清楚三种情况之后,先专注于寻找右区间,然后专注于寻找左区间,左右根据左右区间做最后判断。 - -不要上来就想如果一起寻找左右区间,搞着搞着就会顾此失彼,绕进去拔不出来了。 - - diff --git a/problems/0035.搜索插入位置.md b/problems/0035.搜索插入位置.md deleted file mode 100644 index 5c6a1545..00000000 --- a/problems/0035.搜索插入位置.md +++ /dev/null @@ -1,208 +0,0 @@ - -

- -

-

- - - - - - -

- - -> 二分查找法是数组里的常用方法,彻底掌握它是十分必要的。 - -# 编号35:搜索插入位置 - -题目地址:https://leetcode-cn.com/problems/search-insert-position/ - -给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。 - -你可以假设数组中无重复元素。 - -示例 1: -输入: [1,3,5,6], 5 -输出: 2 - -示例 2: -输入: [1,3,5,6], 2 -输出: 1 - -示例 3: -输入: [1,3,5,6], 7 -输出: 4 - -示例 4: -输入: [1,3,5,6], 0 -输出: 0 - -# 思路 - -这道题目不难,但是为什么通过率相对来说并不高呢,我理解是大家对边界处理的判断有所失误导致的。 - -这道题目,要在数组中插入目标值,无非是这四种情况。 - -![35_搜索插入位置3](https://img-blog.csdnimg.cn/20201216232148471.png) - -* 目标值在数组所有元素之前 -* 目标值等于数组中某一个元素 -* 目标值插入数组中的位置 -* 目标值在数组所有元素之后 - -这四种情况确认清楚了,就可以尝试解题了。 - -接下来我将从暴力的解法和二分法来讲解此题,也借此好好讲一讲二分查找法。 - -## 暴力解法 - -暴力解题 不一定时间消耗就非常高,关键看实现的方式,就像是二分查找时间消耗不一定就很低,是一样的。 - -## 暴力解法C++代码 - -``` -class Solution { -public: - int searchInsert(vector& nums, int target) { - for (int i = 0; i < nums.size(); i++) { - // 分别处理如下三种情况 - // 目标值在数组所有元素之前 - // 目标值等于数组中某一个元素 - // 目标值插入数组中的位置 - if (nums[i] >= target) { // 一旦发现大于或者等于target的num[i],那么i就是我们要的结果 - return i; - } - } - // 目标值在数组所有元素之后的情况 - return nums.size(); // 如果target是最大的,或者 nums为空,则返回nums的长度 - } -}; -``` - -时间复杂度:O(n) -时间复杂度:O(1) - -效率如下: - -![35_搜索插入位置](https://img-blog.csdnimg.cn/20201216232127268.png) - -## 二分法 - -既然暴力解法的时间复杂度是O(n),就要尝试一下使用二分查找法。 - -![35_搜索插入位置4](https://img-blog.csdnimg.cn/202012162326354.png) - -大家注意这道题目的前提是数组是有序数组,这也是使用二分查找的基础条件。 - -以后大家**只要看到面试题里给出的数组是有序数组,都可以想一想是否可以使用二分法。** - -同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下表可能不是唯一的。 - -大体讲解一下二分法的思路,这里来举一个例子,例如在这个数组中,使用二分法寻找元素为5的位置,并返回其下标。 - -![35_搜索插入位置5](https://img-blog.csdnimg.cn/20201216232659199.png) - -二分查找涉及的很多的边界条件,逻辑比较简单,就是写不好。 - -相信很多同学对二分查找法中边界条件处理不好。 - -例如到底是 `while(left < right)` 还是 `while(left <= right)`,到底是`right = middle`呢,还是要`right = middle - 1`呢? - -这里弄不清楚主要是因为**对区间的定义没有想清楚,这就是不变量**。 - -要在二分查找的过程中,保持不变量,这也就是**循环不变量** (感兴趣的同学可以查一查)。 - -## 二分法第一种写法 - -以这道题目来举例,以下的代码中定义 target 是在一个在左闭右闭的区间里,**也就是[left, right] (这个很重要)**。 - -这就决定了这个二分法的代码如何去写,大家看如下代码: - -**大家要仔细看注释,思考为什么要写while(left <= right), 为什么要写right = middle - 1**。 - -``` -class Solution { -public: - int searchInsert(vector& nums, int target) { - int n = nums.size(); - int left = 0; - int right = n - 1; // 定义target在左闭右闭的区间里,[left, right] - while (left <= right) { // 当left==right,区间[left, right]依然有效 - int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2 - if (nums[middle] > target) { - right = middle - 1; // target 在左区间,所以[left, middle - 1] - } else if (nums[middle] < target) { - left = middle + 1; // target 在右区间,所以[middle + 1, right] - } else { // nums[middle] == target - return middle; - } - } - // 分别处理如下四种情况 - // 目标值在数组所有元素之前 [0, -1] - // 目标值等于数组中某一个元素 return middle; - // 目标值插入数组中的位置 [left, right],return right + 1 - // 目标值在数组所有元素之后的情况 [left, right], return right + 1 - return right + 1; - } -}; -``` -时间复杂度:O(logn) -时间复杂度:O(1) - -效率如下: -![35_搜索插入位置2](https://img-blog.csdnimg.cn/2020121623272877.png) - -## 二分法第二种写法 - -如果说定义 target 是在一个在左闭右开的区间里,也就是[left, right) 。 - -那么二分法的边界处理方式则截然不同。 - -不变量是[left, right)的区间,如下代码可以看出是如何在循环中坚持不变量的。 - -**大家要仔细看注释,思考为什么要写while (left < right), 为什么要写right = middle**。 - -``` -class Solution { -public: - int searchInsert(vector& nums, int target) { - int n = nums.size(); - int left = 0; - int right = n; // 定义target在左闭右开的区间里,[left, right) target - while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间 - int middle = left + ((right - left) >> 1); - if (nums[middle] > target) { - right = middle; // target 在左区间,在[left, middle)中 - } else if (nums[middle] < target) { - left = middle + 1; // target 在右区间,在 [middle+1, right)中 - } else { // nums[middle] == target - return middle; // 数组中找到目标值的情况,直接返回下标 - } - } - // 分别处理如下四种情况 - // 目标值在数组所有元素之前 [0,0) - // 目标值等于数组中某一个元素 return middle - // 目标值插入数组中的位置 [left, right) ,return right 即可 - // 目标值在数组所有元素之后的情况 [left, right),return right 即可 - return right; - } -}; -``` - -时间复杂度:O(logn) -时间复杂度:O(1) - -# 总结 - -希望通过这道题目,大家会发现平时写二分法,为什么总写不好,就是因为对区间定义不清楚。 - -确定要查找的区间到底是左闭右开[left, right),还是左闭又闭[left, right],这就是不变量。 - -然后在**二分查找的循环中,坚持循环不变量的原则**,很多细节问题,自然会知道如何处理了。 - -**循序渐进学算法,认准「代码随想录」,Carl手把手带你过关斩将!** - -

- -

diff --git a/problems/0037.解数独.md b/problems/0037.解数独.md deleted file mode 100644 index 4e3a74e2..00000000 --- a/problems/0037.解数独.md +++ /dev/null @@ -1,215 +0,0 @@ -> 解数独,理解二维递归是关键 - -如果对回溯法理论还不清楚的同学,可以先看这个视频[视频来了!!带你学透回溯算法(理论篇)](https://mp.weixin.qq.com/s/wDd5azGIYWjbU0fdua_qBg) - -# 37. 解数独 - -题目地址:https://leetcode-cn.com/problems/sudoku-solver/ - -编写一个程序,通过填充空格来解决数独问题。 - -一个数独的解法需遵循如下规则: -数字 1-9 在每一行只能出现一次。 -数字 1-9 在每一列只能出现一次。 -数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。 -空白格用 '.' 表示。 - -![解数独](https://img-blog.csdnimg.cn/202011171912586.png) - -一个数独。 - -![解数独](https://img-blog.csdnimg.cn/20201117191340669.png) - -答案被标成红色。 - -提示: -* 给定的数独序列只包含数字 1-9 和字符 '.' 。 -* 你可以假设给定的数独只有唯一解。 -* 给定数独永远是 9x9 形式的。 - -# 思路 - -棋盘搜索问题可以使用回溯法暴力搜索,只不过这次我们要做的是**二维递归**。 - -怎么做二维递归呢? - -大家已经跟着「代码随想录」刷过了如下回溯法题目,例如:[77.组合(组合问题)](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ),[131.分割回文串(分割问题)](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q),[78.子集(子集问题)](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA),[46.全排列(排列问题)](https://mp.weixin.qq.com/s/SCOjeMX1t41wcvJq49GhMw),以及[51.N皇后(N皇后问题)](https://mp.weixin.qq.com/s/lU_QwCMj6g60nh8m98GAWg),其实这些题目都是一维递归。 - -**如果以上这几道题目没有做过的话,不建议上来就做这道题哈!** - -[N皇后问题](https://mp.weixin.qq.com/s/lU_QwCMj6g60nh8m98GAWg)是因为每一行每一列只放一个皇后,只需要一层for循环遍历一行,递归来来遍历列,然后一行一列确定皇后的唯一位置。 - -本题就不一样了,**本题中棋盘的每一个位置都要放一个数字,并检查数字是否合法,解数独的树形结构要比N皇后更宽更深**。 - -因为这个树形结构太大了,我抽取一部分,如图所示: - -![37.解数独](https://img-blog.csdnimg.cn/2020111720451790.png) - - -## 回溯三部曲 - -* 递归函数以及参数 - -**递归函数的返回值需要是bool类型,为什么呢?** - -因为解数独找到一个符合的条件(就在树的叶子节点上)立刻就返回,相当于找从根节点到叶子节点一条唯一路径,所以需要使用bool返回值,这一点在[回溯算法:N皇后问题](https://mp.weixin.qq.com/s/lU_QwCMj6g60nh8m98GAWg)中已经介绍过了,一样的道理。 - -代码如下: - -``` -bool backtracking(vector>& board) -``` - -* 递归终止条件 - -本题递归不用终止条件,解数独是要遍历整个树形结构寻找可能的叶子节点就立刻返回。 - -**不用终止条件会不会死循环?** - -递归的下一层的棋盘一定比上一层的棋盘多一个数,等数填满了棋盘自然就终止(填满当然好了,说明找到结果了),所以不需要终止条件! - -**那么有没有永远填不满的情况呢?** - -这个问题我在递归单层搜索逻辑里在来讲! - -* 递归单层搜索逻辑 - -![37.解数独](https://img-blog.csdnimg.cn/2020111720451790.png) - -在树形图中可以看出我们需要的是一个二维的递归(也就是两个for循环嵌套着递归) - -**一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!** - - -代码如下:(**详细看注释**) - -```C++ -bool backtracking(vector>& board) { - for (int i = 0; i < board.size(); i++) { // 遍历行 - for (int j = 0; j < board[0].size(); j++) { // 遍历列 - if (board[i][j] != '.') continue; - for (char k = '1'; k <= '9'; k++) { // (i, j) 这个位置放k是否合适 - if (isValid(i, j, k, board)) { - board[i][j] = k; // 放置k - if (backtracking(board)) return true; // 如果找到合适一组立刻返回 - board[i][j] = '.'; // 回溯,撤销k - } - } - return false; // 9个数都试完了,都不行,那么就返回false - } - } - return true; // 遍历完没有返回false,说明找到了合适棋盘位置了 -} -``` - -**注意这里return false的地方,这里放return false 是有讲究的**。 - -因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解! - -那么会直接返回, **这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去!** - -## 判断棋盘是否合法 - -判断棋盘是否合法有如下三个维度: - -* 同行是否重复 -* 同列是否重复 -* 9宫格里是否重复 - -代码如下: - -```C++ -bool isValid(int row, int col, char val, vector>& board) { - for (int i = 0; i < 9; i++) { // 判断行里是否重复 - if (board[row][i] == val) { - return false; - } - } - for (int j = 0; j < 9; j++) { // 判断列里是否重复 - if (board[j][col] == val) { - return false; - } - } - int startRow = (row / 3) * 3; - int startCol = (col / 3) * 3; - for (int i = startRow; i < startRow + 3; i++) { // 判断9方格里是否重复 - for (int j = startCol; j < startCol + 3; j++) { - if (board[i][j] == val ) { - return false; - } - } - } - return true; -} -``` - -最后整体代码如下: - -# C++代码 - -```C++ -class Solution { -private: -bool backtracking(vector>& board) { - for (int i = 0; i < board.size(); i++) { // 遍历行 - for (int j = 0; j < board[0].size(); j++) { // 遍历列 - if (board[i][j] != '.') continue; - for (char k = '1'; k <= '9'; k++) { // (i, j) 这个位置放k是否合适 - if (isValid(i, j, k, board)) { - board[i][j] = k; // 放置k - if (backtracking(board)) return true; // 如果找到合适一组立刻返回 - board[i][j] = '.'; // 回溯,撤销k - } - } - return false; // 9个数都试完了,都不行,那么就返回false - } - } - return true; // 遍历完没有返回false,说明找到了合适棋盘位置了 -} -bool isValid(int row, int col, char val, vector>& board) { - for (int i = 0; i < 9; i++) { // 判断行里是否重复 - if (board[row][i] == val) { - return false; - } - } - for (int j = 0; j < 9; j++) { // 判断列里是否重复 - if (board[j][col] == val) { - return false; - } - } - int startRow = (row / 3) * 3; - int startCol = (col / 3) * 3; - for (int i = startRow; i < startRow + 3; i++) { // 判断9方格里是否重复 - for (int j = startCol; j < startCol + 3; j++) { - if (board[i][j] == val ) { - return false; - } - } - } - return true; -} -public: - void solveSudoku(vector>& board) { - backtracking(board); - } -}; -``` - -# 总结 - -解数独可以说是非常难的题目了,如果还一直停留在单层递归的逻辑中,这道题目可以让大家瞬间崩溃。 - -所以我在开篇就提到了**二维递归**,这也是我自创词汇,希望可以帮助大家理解解数独的搜索过程。 - -一波分析之后,在看代码会发现其实也不难,唯一难点就是理解**二维递归**的思维逻辑。 - -**这样,解数独这么难的问题,也被我们攻克了**。 - -**恭喜一路上坚持打卡的录友们,回溯算法已经接近尾声了,接下来就是要一波总结了**。 - -如果一直跟住「代码随想录」的节奏,你会发现自己进步飞快,从思维方式到刷题习惯,都会有质的飞跃,「代码随想录」绝对值得推荐给身边的同学朋友们! - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/0039.组合总和.md b/problems/0039.组合总和.md deleted file mode 100644 index 9ec073e5..00000000 --- a/problems/0039.组合总和.md +++ /dev/null @@ -1,227 +0,0 @@ - -> 看懂很容易,彻底掌握需要下功夫 - -# 第39题. 组合总和 - -题目链接:https://leetcode-cn.com/problems/combination-sum/ - -给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。 - -candidates 中的数字可以无限制重复被选取。 - -说明: - -* 所有数字(包括 target)都是正整数。 -* 解集不能包含重复的组合。  - -示例 1: -输入:candidates = [2,3,6,7], target = 7, -所求解集为: -[ - [7], - [2,2,3] -] - -示例 2: -输入:candidates = [2,3,5], target = 8, -所求解集为: -[ -  [2,2,2,2], -  [2,3,3], -  [3,5] -] - -# 思路 - -题目中的**无限制重复被选取,吓得我赶紧想想 出现0 可咋办**,然后看到下面提示:1 <= candidates[i] <= 200,我就放心了。 - -本题和[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ),[回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w)和区别是:本题没有数量要求,可以无限重复,但是有总和的限制,所以间接的也是有个数的限制。 - -本题搜索的过程抽象成树形结构如下: - -![39.组合总和](https://img-blog.csdnimg.cn/20201223170730367.png) -注意图中叶子节点的返回条件,因为本题没有组合数量要求,仅仅是总和的限制,所以递归没有层数的限制,只要选取的元素总和超过target,就返回! - -而在[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)和[回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w) 中都可以知道要递归K层,因为要取k个元素的组合。 - -## 回溯三部曲 - -* 递归函数参数 - -这里依然是定义两个全局变量,二维数组result存放结果集,数组path存放符合条件的结果。(这两个变量可以作为函数参数传入) - -首先是题目中给出的参数,集合candidates, 和目标值target。 - -此外我还定义了int型的sum变量来统计单一结果path里的总和,其实这个sum也可以不用,用target做相应的减法就可以了,最后如何target==0就说明找到符合的结果了,但为了代码逻辑清晰,我依然用了sum。 - -**本题还需要startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢?** - -我举过例子,如果是一个集合来求组合的话,就需要startIndex,例如:[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ),[回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w)。 - -如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:[回溯算法:电话号码的字母组合](https://mp.weixin.qq.com/s/e2ua2cmkE_vpYjM3j6HY0A) - -**注意以上我只是说求组合的情况,如果是排列问题,又是另一套分析的套路,后面我再讲解排列的时候就重点介绍**。 - -代码如下: - -``` -vector> result; -vector path; -void backtracking(vector& candidates, int target, int sum, int startIndex) -``` - -* 递归终止条件 - -在如下树形结构中: - -![39.组合总和](https://img-blog.csdnimg.cn/20201223170730367.png) - -从叶子节点可以清晰看到,终止只有两种情况,sum大于target和sum等于target。 - -sum等于target的时候,需要收集结果,代码如下: - -``` -if (sum > target) { - return; -} -if (sum == target) { - result.push_back(path); - return; -} -``` - -* 单层搜索的逻辑 - -单层for循环依然是从startIndex开始,搜索candidates集合。 - -**注意本题和[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)、[回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w)的一个区别是:本题元素为可重复选取的**。 - -如何重复选取呢,看代码,注释部分: - -``` -for (int i = startIndex; i < candidates.size(); i++) { - sum += candidates[i]; - path.push_back(candidates[i]); - backtracking(candidates, target, sum, i); // 关键点:不用i+1了,表示可以重复读取当前的数 - sum -= candidates[i]; // 回溯 - path.pop_back(); // 回溯 -} -``` - -按照[关于回溯算法,你该了解这些!](https://mp.weixin.qq.com/s/gjSgJbNbd1eAA5WkA-HeWw)中给出的模板,不难写出如下C++完整代码: - -``` -// 版本一 -class Solution { -private: - vector> result; - vector path; - void backtracking(vector& candidates, int target, int sum, int startIndex) { - if (sum > target) { - return; - } - if (sum == target) { - result.push_back(path); - return; - } - - for (int i = startIndex; i < candidates.size(); i++) { - sum += candidates[i]; - path.push_back(candidates[i]); - backtracking(candidates, target, sum, i); // 不用i+1了,表示可以重复读取当前的数 - sum -= candidates[i]; - path.pop_back(); - } - } -public: - vector> combinationSum(vector& candidates, int target) { - result.clear(); - path.clear(); - backtracking(candidates, target, 0, 0); - return result; - } -}; -``` - -## 剪枝优化 - -在这个树形结构中: - -![39.组合总和](https://img-blog.csdnimg.cn/20201223170730367.png) - -以及上面的版本一的代码大家可以看到,对于sum已经大于target的情况,其实是依然进入了下一层递归,只是下一层递归结束判断的时候,会判断sum > target的话就返回。 - -其实如果已经知道下一层的sum会大于target,就没有必要进入下一层递归了。 - -那么可以在for循环的搜索范围上做做文章了。 - -**对总集合排序之后,如果下一层的sum(就是本层的 sum + candidates[i])已经大于target,就可以结束本轮for循环的遍历**。 - -如图: - - -![39.组合总和1](https://img-blog.csdnimg.cn/20201223170809182.png) - -for循环剪枝代码如下: - -``` -for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) -``` - -整体代码如下:(注意注释的部分) - -``` -class Solution { -private: - vector> result; - vector path; - void backtracking(vector& candidates, int target, int sum, int startIndex) { - if (sum == target) { - result.push_back(path); - return; - } - - // 如果 sum + candidates[i] > target 就终止遍历 - for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) { - sum += candidates[i]; - path.push_back(candidates[i]); - backtracking(candidates, target, sum, i); - sum -= candidates[i]; - path.pop_back(); - - } - } -public: - vector> combinationSum(vector& candidates, int target) { - result.clear(); - path.clear(); - sort(candidates.begin(), candidates.end()); // 需要排序 - backtracking(candidates, target, 0, 0); - return result; - } -}; -``` - -# 总结 - -本题和我们之前讲过的[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)、[回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w)有两点不同: - -* 组合没有数量要求 -* 元素可无限重复选取 - -针对这两个问题,我都做了详细的分析。 - -并且给出了对于组合问题,什么时候用startIndex,什么时候不用,并用[回溯算法:电话号码的字母组合](https://mp.weixin.qq.com/s/e2ua2cmkE_vpYjM3j6HY0A)做了对比。 - -最后还给出了本题的剪枝优化,这个优化如果是初学者的话并不容易想到。 - -**在求和问题中,排序之后加剪枝是常见的套路!** - -可以看出我写的文章都会大量引用之前的文章,就是要不断作对比,分析其差异,然后给出代码解决的方法,这样才能彻底理解题目的本质与难点。 - -**就酱,如果感觉很给力,就帮Carl宣传一波吧,哈哈**。 - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/0040.组合总和II.md b/problems/0040.组合总和II.md deleted file mode 100644 index 40179710..00000000 --- a/problems/0040.组合总和II.md +++ /dev/null @@ -1,206 +0,0 @@ -> 这篇可以说是全网把组合问题如何去重,讲的最清晰的了! - -# 40.组合总和II - -题目链接:https://leetcode-cn.com/problems/combination-sum-ii/ - -给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。 - -candidates 中的每个数字在每个组合中只能使用一次。 - -说明: -所有数字(包括目标数)都是正整数。 -解集不能包含重复的组合。  - -示例 1: -输入: candidates = [10,1,2,7,6,1,5], target = 8, -所求解集为: -[ - [1, 7], - [1, 2, 5], - [2, 6], - [1, 1, 6] -] - -示例 2: -输入: candidates = [2,5,2,1,2], target = 5, -所求解集为: -[ -  [1,2,2], -  [5] -] - -# 思路 - -这道题目和[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)如下区别: - -1. 本题candidates 中的每个数字在每个组合中只能使用一次。 -2. 本题数组candidates的元素是有重复的,而[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)是无重复元素的数组candidates - -最后本题和[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)要求一样,解集不能包含重复的组合。 - -**本题的难点在于区别2中:集合(数组candidates)有重复元素,但还不能有重复的组合**。 - -一些同学可能想了:我把所有组合求出来,再用set或者map去重,这么做很容易超时! - -所以要在搜索的过程中就去掉重复组合。 - -很多同学在去重的问题上想不明白,其实很多题解也没有讲清楚,反正代码是能过的,感觉是那么回事,稀里糊涂的先把题目过了。 - -这个去重为什么很难理解呢,**所谓去重,其实就是使用过的元素不能重复选取。** 这么一说好像很简单! - -都知道组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。**没有理解这两个层面上的“使用过” 是造成大家没有彻底理解去重的根本原因。** - -那么问题来了,我们是要同一树层上使用过,还是统一树枝上使用过呢? - -回看一下题目,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。 - - -**所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重**。 - -为了理解去重我们来举一个例子,candidates = [1, 1, 2], target = 3,(方便起见candidates已经排序了) - -**强调一下,树层去重的话,需要对数组排序!** - -选择过程树形结构如图所示: - -![40.组合总和II](https://img-blog.csdnimg.cn/20201123202736384.png) - -可以看到图中,每个节点相对于 [39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)我多加了used数组,这个used数组下面会重点介绍。 - -## 回溯三部曲 - -* **递归函数参数** - -与[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)套路相同,此题还需要加一个bool型数组used,用来记录同一树枝上的元素是否使用过。 - -这个集合去重的重任就是used来完成的。 - -代码如下: - -``` -vector> result; // 存放组合集合 -vector path; // 符合条件的组合 -void backtracking(vector& candidates, int target, int sum, int startIndex, vector& used) { -``` - -* **递归终止条件** - -与[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)相同,终止条件为 `sum > target` 和 `sum == target`。 - -代码如下: - -``` -if (sum > target) { // 这个条件其实可以省略 - return; -} -if (sum == target) { - result.push_back(path); - return; -} -``` - -`sum > target` 这个条件其实可以省略,因为和在递归单层遍历的时候,会有剪枝的操作,下面会介绍到。 - -* **单层搜索的逻辑** - -这里与[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)最大的不同就是要去重了。 - -前面我们提到:要去重的是“同一树层上的使用过”,如果判断同一树层上元素(相同的元素)是否使用过了呢。 - -**如果`candidates[i] == candidates[i - 1]` 并且 `used[i - 1] == false`,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]**。 - -此时for循环里就应该做continue的操作。 - -这块比较抽象,如图: - -![40.组合总和II1](https://img-blog.csdnimg.cn/20201123202817973.png) - -我在图中将used的变化用橘黄色标注上,可以看出在candidates[i] == candidates[i - 1]相同的情况下: - -* used[i - 1] == true,说明同一树支candidates[i - 1]使用过 -* used[i - 1] == false,说明同一树层candidates[i - 1]使用过 - -**这块去重的逻辑很抽象,网上搜的题解基本没有能讲清楚的,如果大家之前思考过这个问题或者刷过这道题目,看到这里一定会感觉通透了很多!** - -那么单层搜索的逻辑代码如下: - -``` -for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) { - // used[i - 1] == true,说明同一树支candidates[i - 1]使用过 - // used[i - 1] == false,说明同一树层candidates[i - 1]使用过 - // 要对同一树层使用过的元素进行跳过 - if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) { - continue; - } - sum += candidates[i]; - path.push_back(candidates[i]); - used[i] = true; - backtracking(candidates, target, sum, i + 1, used); // 和39.组合总和的区别1:这里是i+1,每个数字在每个组合中只能使用一次 - used[i] = false; - sum -= candidates[i]; - path.pop_back(); -} -``` - -**注意sum + candidates[i] <= target为剪枝操作,在[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)有讲解过!** - -## C++代码 - -回溯三部曲分析完了,整体C++代码如下: - -``` -class Solution { -private: - vector> result; - vector path; - void backtracking(vector& candidates, int target, int sum, int startIndex, vector& used) { - if (sum == target) { - result.push_back(path); - return; - } - for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) { - // used[i - 1] == true,说明同一树支candidates[i - 1]使用过 - // used[i - 1] == false,说明同一树层candidates[i - 1]使用过 - // 要对同一树层使用过的元素进行跳过 - if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) { - continue; - } - sum += candidates[i]; - path.push_back(candidates[i]); - used[i] = true; - backtracking(candidates, target, sum, i + 1, used); // 和39.组合总和的区别1,这里是i+1,每个数字在每个组合中只能使用一次 - used[i] = false; - sum -= candidates[i]; - path.pop_back(); - } - } - -public: - vector> combinationSum2(vector& candidates, int target) { - vector used(candidates.size(), false); - path.clear(); - result.clear(); - // 首先把给candidates排序,让其相同的元素都挨在一起。 - sort(candidates.begin(), candidates.end()); - backtracking(candidates, target, 0, 0, used); - return result; - } -}; - -``` - -# 总结 - -本题同样是求组合总和,但就是因为其数组candidates有重复元素,而要求不能有重复的组合,所以相对于[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)难度提升了不少。 - -**关键是去重的逻辑,代码很简单,网上一搜一大把,但几乎没有能把这块代码含义讲明白的,基本都是给出代码,然后说这就是去重了,究竟怎么个去重法也是模棱两可**。 - -所以Carl有必要把去重的这块彻彻底底的给大家讲清楚,**就连“树层去重”和“树枝去重”都是我自创的词汇,希望对大家理解有帮助!** - -**就酱,如果感觉「代码随想录」诚意满满,就帮Carl宣传一波吧,感谢啦!** - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/0042.接雨水.md b/problems/0042.接雨水.md deleted file mode 100644 index 05cf9e08..00000000 --- a/problems/0042.接雨水.md +++ /dev/null @@ -1,340 +0,0 @@ -# 题目链接 - -# 思路 - -接雨水问题在面试中还是常见题目的,有必要好好讲一讲。 - -本文深度讲解如下三种方法: -* 双指针法 -* 动态规划 -* 单调栈 - -## 双指针解法 - -这道题目暴力解法并不简单,我们来看一下思路。 - -首先要明确,要按照行来计算,还是按照列来计算。如图所示: - - - - - -一些同学在实现暴力解法的时候,很容易一会按照行来计算一会按照列来计算,这样就会越写越乱。 - -我个人倾向于按照列来计算,比较容易理解,接下来看一下按照列如何计算。 - -首先,**如果按照列来计算的话,宽度一定是1了,我们再把每一列的雨水的高度求出来就可以了。** - -可以看出每一列雨水的高度,取决于,该列 左侧最高的柱子和右侧最高的柱子中最矮的那个柱子的高度。 - -这句话可以有点绕,来举一个理解,例如求列4的雨水高度,如图: - - - -列4 左侧最高的柱子是列3,高度为2(以下用lHeight表示)。 - -列4 右侧最高的柱子是列7,高度为3(以下用rHeight表示)。 - -列4 柱子的高度为1(以下用height表示) - -那么列4的雨水高度为 列3和列7的高度最小值减列4高度,即: min(lHeight, rHeight) - height。 - -列4的雨水高度求出来了,宽度为1,相乘就是列4的雨水体积了。 - -此时求出了列4的雨水体积。 - -一样的方法,只要从头遍历一遍所有的列,然后求出每一列雨水的体积,相加之后就是总雨水的体积了。 - -首先从头遍历所有的列,并且**要注意第一个柱子和最后一个柱子不接雨水**,代码如下: -``` -for (int i = 0; i < height.size(); i++) { - // 第一个柱子和最后一个柱子不接雨水 - if (i == 0 || i == height.size() - 1) continue; -} -``` - -在for循环中求左右两边最高柱子,代码如下: - -``` -int rHeight = height[i]; // 记录右边柱子的最高高度 -int lHeight = height[i]; // 记录左边柱子的最高高度 -for (int r = i + 1; r < height.size(); r++) { - if (height[r] > rHeight) rHeight = height[r]; -} -for (int l = i - 1; l >= 0; l--) { - if (height[l] > lHeight) lHeight = height[l]; -} -``` - -最后,计算该列的雨水高度,代码如下: - -``` -int h = min(lHeight, rHeight) - height[i]; -if (h > 0) sum += h; // 注意只有h大于零的时候,在统计到总和中 -``` - -整体代码如下: - -``` -class Solution { -public: - int trap(vector& height) { - int sum = 0; - for (int i = 0; i < height.size(); i++) { - // 第一个柱子和最后一个柱子不接雨水 - if (i == 0 || i == height.size() - 1) continue; - - int rHeight = height[i]; // 记录右边柱子的最高高度 - int lHeight = height[i]; // 记录左边柱子的最高高度 - for (int r = i + 1; r < height.size(); r++) { - if (height[r] > rHeight) rHeight = height[r]; - } - for (int l = i - 1; l >= 0; l--) { - if (height[l] > lHeight) lHeight = height[l]; - } - int h = min(lHeight, rHeight) - height[i]; - if (h > 0) sum += h; - } - return sum; - } -}; -``` - -因为每次遍历列的时候,还要向两边寻找最高的列,所以时间复杂度为O(n^2)。 -空间复杂度为O(1)。 - -## 动态规划解法 - -在上面的双指针解法,我们可以看到,只要知道左边柱子的最高高度 和 记录右边柱子的最高高度,就可以计算当前位置的雨水面积,这也是也列来计算的。 - -即,当前列雨水面积:min(左边柱子的最高高度,记录右边柱子的最高高度) - 当前柱子高度 - -为了的到两边的最高高度,使用了双指针来遍历,每到一个柱子都向两边遍历一波。 - -这其实是有重复计算的。 - -我们把每一个位置的左边最高高度记录在一个数组上(maxLeft),右边最高高度记录在一个数组上(maxRight)。 - -避免的重复计算,者就用到了动态规划。 - -当前位置,左边的最高高度,是前一个位置的最高高度和本高度的最大值。 - -即从左向右遍历:maxLeft[i] = max(height[i], maxLeft[i - 1]); - -从右向左遍历:maxRight[i] = max(height[i], maxRight[i + 1]); - -这样就找到递推公式。 - -是不是地推公式还挺简单的,其实动态规划就是这样,只要想到了递推公式,其实就比较简单了。 - -代码如下: - -``` -class Solution { -public: - int trap(vector& height) { - if (height.size() <= 2) return 0; - vector maxLeft(height.size(), 0); - vector maxRight(height.size(), 0); - int size = maxRight.size(); - - // 记录每个柱子左边柱子最大高度 - maxLeft[0] = height[0]; - for (int i = 1; i < size; i++) { - maxLeft[i] = max(height[i], maxLeft[i - 1]); - } - // 记录每个柱子右边柱子最大高度 - maxRight[size - 1] = height[size - 1]; - for (int i = size - 2; i >= 0; i--) { - maxRight[i] = max(height[i], maxRight[i + 1]); - } - // 求和 - int sum = 0; - for (int i = 0; i < size; i++) { - int count = min(maxLeft[i], maxRight[i]) - height[i]; - if (count > 0) sum += count; - } - return sum; - } -}; -``` - -## 单调栈解法 - -这个解法可以说是最不好理解的了,所以下面我花了大量的篇幅来介绍这种方法。 - -单调栈就是保持栈内元素有序。和[栈与队列:单调队列](https://mp.weixin.qq.com/s/8c6l2bO74xyMjph09gQtpA)一样,需要我们自己维持顺序,没有现成的容器可以用。 - - -### 准备工作 - -那么本题使用单调栈有如下几个问题: - -1. 首先单调栈是按照行方向来计算雨水,如图: - - - -知道这一点,后面的就可以理解了。 - -2. 使用单调栈内元素的顺序 - -从大到小还是从小打到呢? - -要从栈底到栈头(元素从栈头弹出)是从大到小的顺序。 - -因为一旦发现添加的柱子高度大于栈头元素了,此时就出现凹槽了,栈头元素就是凹槽底部的柱子,栈头第二个元素就是凹槽左边的柱子,而添加的元素就是凹槽右边的柱子。 - -如图: - - - - -3. 遇到相同高度的柱子怎么办。 - -遇到相同的元素,更新栈内下表,就是将栈里元素(旧下标)弹出,将新元素(新下标)加入栈中。 - -例如 5 5 1 3 这种情况。如果添加第二个5的时候就应该将第一个5的下标弹出,把第二个5添加到栈中。 - -因为我们要求宽度的时候 如果遇到相同高度的柱子,需要使用最右边的柱子来计算宽度。 - -如图所示: - - - - - -4. 栈里要保存什么数值 - -是用单调栈,其实是通过 长 * 宽 来计算雨水面积的。 - -长就是通过柱子的高度来计算,宽是通过柱子之间的下表来计算, - -那么栈里有没有必要存一个pair类型的元素,保存柱子的高度和下表呢。 - -其实不用,栈里就存放int类型的元素就行了,表示下表,想要知道对应的高度,通过height[stack.top()] 就知道弹出的下表对应的高度了。 - -所以栈的定义如下: - -``` -stack st; // 存着下标,计算的时候用下标对应的柱子高度 -``` - -明确了如上几点,我们再来看处理逻辑。 - -### 单调栈处理逻辑 - -先将下表0的柱子加入到栈中,`st.push(0);`。 - -然后开始从下表1开始遍历所有的柱子,`for (int i = 1; i < height.size(); i++)`。 - -如果当前遍历的元素(柱子)高度小于栈顶元素的高度,就把这个元素加入栈中,因为栈里本来就要保持从大到小的顺序(从栈底到栈头)。 - -代码如下: - -``` -if (height[i] < height[st.top()]) st.push(i); -``` - -如果当前遍历的元素(柱子)高度等于栈顶元素的高度,要跟更新栈顶元素,因为遇到相相同高度的柱子,需要使用最右边的柱子来计算宽度。 - -代码如下: - -``` -if (height[i] == height[st.top()]) { // 例如 5 5 1 7 这种情况 - st.pop(); - st.push(i); -} -``` - -如果当前遍历的元素(柱子)高度大于栈顶元素的高度,此时就出现凹槽了,如图所示: - - - -取栈顶元素,将栈顶元素弹出,这个就是凹槽的底部,也就是中间位置,下表记为mid,对应的高度为height[mid](就是图中的高度1)。 - -栈顶元素st.top(),就是凹槽的左边位置,下表为st.top(),对应的高度为height[st.top()](就是图中的高度2)。 - -当前遍历的元素i,就是凹槽右边的位置,下表为i,对应的高度为height[i](就是图中的高度3)。 - -那么雨水高度是 min(凹槽左边高度, 凹槽右边高度) - 凹槽底部高度,代码为:`int h = min(height[st.top()], height[i]) - height[mid];` - -雨水的宽度是 凹槽右边的下表 - 凹槽左边的下表 - 1(因为只求中间宽度),代码为:`int w = i - st.top() - 1 ;` - -当前凹槽雨水的体积就是:`h * w`。 - -求当前凹槽雨水的体积代码如下: - -``` -while (!st.empty() && height[i] > height[st.top()]) { // 注意这里是while,持续跟新栈顶元素 - int mid = st.top(); - st.pop(); - if (!st.empty()) { - int h = min(height[st.top()], height[i]) - height[mid]; - int w = i - st.top() - 1; // 注意减一,只求中间宽度 - sum += h * w; - } -} -``` - -关键部分讲完了,整体代码如下: - -``` -class Solution { -public: - int trap(vector& height) { - if (height.size() <= 2) return 0; // 可以不加 - stack st; // 存着下标,计算的时候用下标对应的柱子高度 - st.push(0); - int sum = 0; - for (int i = 1; i < height.size(); i++) { - if (height[i] < height[st.top()]) { // 情况一 - st.push(i); - } if (height[i] == height[st.top()]) { // 情况二 - st.pop(); // 其实这一句可以不加,效果是一样的,但处理相同的情况的思路却变了。 - st.push(i); - } else { // 情况三 - while (!st.empty() && height[i] > height[st.top()]) { // 注意这里是while - int mid = st.top(); - st.pop(); - if (!st.empty()) { - int h = min(height[st.top()], height[i]) - height[mid]; - int w = i - st.top() - 1; // 注意减一,只求中间宽度 - sum += h * w; - } - } - st.push(i); - } - } - return sum; - } -}; -``` - -以上代码冗余了一些,但是思路是清晰的,下面我将代码精简一下,如下: - -``` -class Solution { -public: - int trap(vector& height) { - stack st; - st.push(0); - int sum = 0; - for (int i = 1; i < height.size(); i++) { - while (!st.empty() && height[i] > height[st.top()]) { - int mid = st.top(); - st.pop(); - if (!st.empty()) { - int h = min(height[st.top()], height[i]) - height[mid]; - int w = i - st.top() - 1; - sum += h * w; - } - } - st.push(i); - } - return sum; - } -}; -``` - -精简之后的代码,大家就看不出去三种情况的处理了,貌似好像只处理的情况三,其实是把情况一和情况二融合了。 这样的代码不太利于理解。 - diff --git a/problems/0045.跳跃游戏II.md b/problems/0045.跳跃游戏II.md deleted file mode 100644 index 2474b6d1..00000000 --- a/problems/0045.跳跃游戏II.md +++ /dev/null @@ -1,138 +0,0 @@ -> 相对于[贪心算法:跳跃游戏](https://mp.weixin.qq.com/s/606_N9j8ACKCODoCbV1lSA)难了不少,做好心里准备! - -# 45.跳跃游戏II - -题目地址:https://leetcode-cn.com/problems/jump-game-ii/ - -给定一个非负整数数组,你最初位于数组的第一个位置。 - -数组中的每个元素代表你在该位置可以跳跃的最大长度。 - -你的目标是使用最少的跳跃次数到达数组的最后一个位置。 - -示例: -输入: [2,3,1,1,4] -输出: 2 -解释: 跳到最后一个位置的最小跳跃数是 2。从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。 - -说明: -假设你总是可以到达数组的最后一个位置。 - - -# 思路 - -本题相对于[贪心算法:跳跃游戏](https://mp.weixin.qq.com/s/606_N9j8ACKCODoCbV1lSA)还是难了不少。 - -但思路是相似的,还是要看最大覆盖范围。 - -本题要计算最小步数,那么就要想清楚什么时候步数才一定要加一呢? - -贪心的思路,局部最优:当前可移动距离尽可能多走,如果还没到终点,步数再加一。整体最优:一步尽可能多走,从而达到最小步数。 - -思路虽然是这样,但在写代码的时候还不能真的就能跳多远跳远,那样就不知道下一步最远能跳到哪里了。 - -**所以真正解题的时候,要从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最小步数!** - -**这里需要统计两个覆盖范围,当前这一步的最大覆盖和下一步最大覆盖**。 - -如果移动下标达到了当前这一步的最大覆盖最远距离了,还没有到终点的话,那么就必须再走一步来增加覆盖范围,直到覆盖范围覆盖了终点。 - -如图: - -![45.跳跃游戏II](https://img-blog.csdnimg.cn/20201201232309103.png) - -**图中覆盖范围的意义在于,只要红色的区域,最多两步一定可以到!(不用管具体怎么跳,反正一定可以跳到)** - -## 方法一 - -从图中可以看出来,就是移动下标达到了当前覆盖的最远距离下标时,步数就要加一,来增加覆盖距离。最后的步数就是最少步数。 - -这里还是有个特殊情况需要考虑,当移动下标达到了当前覆盖的最远距离下标时 - -* 如果当前覆盖最远距离下标不是是集合终点,步数就加一,还需要继续走。 -* 如果当前覆盖最远距离下标就是是集合终点,步数不用加一,因为不能再往后走了。 - -C++代码如下:(详细注释) - -```C++ -// 版本一 -class Solution { -public: - int jump(vector& nums) { - if (nums.size() == 1) return 0; - int curDistance = 0; // 当前覆盖最远距离下标 - int ans = 0; // 记录走的最大步数 - int nextDistance = 0; // 下一步覆盖最远距离下标 - for (int i = 0; i < nums.size(); i++) { - nextDistance = max(nums[i] + i, nextDistance); // 更新下一步覆盖最远距离下标 - if (i == curDistance) { // 遇到当前覆盖最远距离下标 - if (curDistance != nums.size() - 1) { // 如果当前覆盖最远距离下标不是终点 - ans++; // 需要走下一步 - curDistance = nextDistance; // 更新当前覆盖最远距离下标(相当于加油了) - if (nextDistance >= nums.size() - 1) break; // 下一步的覆盖范围已经可以达到终点,结束循环 - } else break; // 当前覆盖最远距离下标是集合终点,不用做ans++操作了,直接结束 - } - } - return ans; - } -}; -``` - -## 方法二 - -依然是贪心,思路和方法一差不多,代码可以简洁一些。 - -**针对于方法一的特殊情况,可以统一处理**,即:移动下标只要遇到当前覆盖最远距离的下标,直接步数加一,不考虑是不是终点的情况。 - -想要达到这样的效果,只要让移动下标,最大只能移动到nums.size - 2的地方就可以了。 - -因为当移动下标指向nums.size - 2时: - -* 如果移动下标等于当前覆盖最大距离下标, 需要再走一步(即ans++),因为最后一步一定是可以到的终点。(题目假设总是可以到达数组的最后一个位置),如图: -![45.跳跃游戏II2](https://img-blog.csdnimg.cn/20201201232445286.png) - -* 如果移动下标不等于当前覆盖最大距离下标,说明当前覆盖最远距离就可以直接达到终点了,不需要再走一步。如图: - -![45.跳跃游戏II1](https://img-blog.csdnimg.cn/20201201232338693.png) - -代码如下: - -```C++ -// 版本二 -class Solution { -public: - int jump(vector& nums) { - int curDistance = 0; // 当前覆盖的最远距离下标 - int ans = 0; // 记录走的最大步数 - int nextDistance = 0; // 下一步覆盖的最远距离下标 - for (int i = 0; i < nums.size() - 1; i++) { // 注意这里是小于nums.size() - 1,这是关键所在 - nextDistance = max(nums[i] + i, nextDistance); // 更新下一步覆盖的最远距离下标 - if (i == curDistance) { // 遇到当前覆盖的最远距离下标 - curDistance = nextDistance; // 更新当前覆盖的最远距离下标 - ans++; - } - } - return ans; - } -}; -``` - -可以看出版本二的代码相对于版本一简化了不少! - -其精髓在于控制移动下标i只移动到nums.size() - 2的位置,所以移动下标只要遇到当前覆盖最远距离的下标,直接步数加一,不用考虑别的了。 - -# 总结 - -相信大家可以发现,这道题目相当于[贪心算法:跳跃游戏](https://mp.weixin.qq.com/s/606_N9j8ACKCODoCbV1lSA)难了不止一点。 - -但代码又十分简单,贪心就是这么巧妙。 - -理解本题的关键在于:**以最小的步数增加最大的覆盖范围,直到覆盖范围覆盖了终点**,这个范围内最小步数一定可以跳到,不用管具体是怎么跳的,不纠结于一步究竟跳一个单位还是两个单位。 - -就酱,如果感觉「代码随想录」很不错,就分享给身边的朋友同学吧! - - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20201124161234338.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/0046.全排列.md b/problems/0046.全排列.md deleted file mode 100644 index 7e1b87a2..00000000 --- a/problems/0046.全排列.md +++ /dev/null @@ -1,145 +0,0 @@ - -> 开始排列问题 - -# 46.全排列 - -题目链接:https://leetcode-cn.com/problems/permutations/ - -给定一个 没有重复 数字的序列,返回其所有可能的全排列。 - -示例: -输入: [1,2,3] -输出: -[ - [1,2,3], - [1,3,2], - [2,1,3], - [2,3,1], - [3,1,2], - [3,2,1] -] - -## 思路 - -此时我们已经学习了[组合问题](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)、[切割问题](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q)和[子集问题](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA),接下来看一看排列问题。 - -相信这个排列问题就算是让你用for循环暴力把结果搜索出来,这个暴力也不是很好写。 - -所以正如我们在[关于回溯算法,你该了解这些!](https://mp.weixin.qq.com/s/gjSgJbNbd1eAA5WkA-HeWw)所讲的为什么回溯法是暴力搜索,效率这么低,还要用它? - -**因为一些问题能暴力搜出来就已经很不错了!** - -我以[1,2,3]为例,抽象成树形结构如下: - -![46.全排列](https://img-blog.csdnimg.cn/20201209174225145.png) - -## 回溯三部曲 - -* 递归函数参数 - -**首先排列是有序的,也就是说[1,2] 和[2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方**。 - -可以看出元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题就不用使用startIndex了。 - -但排列问题需要一个used数组,标记已经选择的元素,如图橘黄色部分所示: - -![46.全排列](https://img-blog.csdnimg.cn/20201209174225145.png) - -代码如下: - -``` -vector> result; -vector path; -void backtracking (vector& nums, vector& used) -``` - -* 递归终止条件 - -![46.全排列](https://img-blog.csdnimg.cn/20201209174225145.png) - -可以看出叶子节点,就是收割结果的地方。 - -那么什么时候,算是到达叶子节点呢? - -当收集元素的数组path的大小达到和nums数组一样大的时候,说明找到了一个全排列,也表示到达了叶子节点。 - -代码如下: - -``` -// 此时说明找到了一组 -if (path.size() == nums.size()) { - result.push_back(path); - return; -} -``` - -* 单层搜索的逻辑 - -这里和[组合问题](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)、[切割问题](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q)和[子集问题](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA)最大的不同就是for循环里不用startIndex了。 - -因为排列问题,每次都要从头开始搜索,例如元素1在[1,2]中已经使用过了,但是在[2,1]中还要再使用一次1。 - -**而used数组,其实就是记录此时path里都有哪些元素使用了,一个排列里一个元素只能使用一次**。 - -代码如下: - -``` -for (int i = 0; i < nums.size(); i++) { - if (used[i] == true) continue; // path里已经收录的元素,直接跳过 - used[i] = true; - path.push_back(nums[i]); - backtracking(nums, used); - path.pop_back(); - used[i] = false; -} -``` - -整体C++代码如下: - -## C++代码 - -``` -class Solution { -public: - vector> result; - vector path; - void backtracking (vector& nums, vector& used) { - // 此时说明找到了一组 - if (path.size() == nums.size()) { - result.push_back(path); - return; - } - for (int i = 0; i < nums.size(); i++) { - if (used[i] == true) continue; // path里已经收录的元素,直接跳过 - used[i] = true; - path.push_back(nums[i]); - backtracking(nums, used); - path.pop_back(); - used[i] = false; - } - } - vector> permute(vector& nums) { - result.clear(); - path.clear(); - vector used(nums.size(), false); - backtracking(nums, used); - return result; - } -}; -``` - -# 总结 - -大家此时可以感受出排列问题的不同: - -* 每层都是从0开始搜索而不是startIndex -* 需要used数组记录path里都放了哪些元素了 - -排列问题是回溯算法解决的经典题目,大家可以好好体会体会。 - -就酱,如果感觉「代码随想录」诚意满满,就帮Carl宣传一波吧! - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/0047.全排列II.md b/problems/0047.全排列II.md deleted file mode 100644 index dbe8a2e6..00000000 --- a/problems/0047.全排列II.md +++ /dev/null @@ -1,150 +0,0 @@ - -> 排列也要去重了 - -# 47.全排列 II - -题目链接:https://leetcode-cn.com/problems/permutations-ii/ - -给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。 - -示例 1: -输入:nums = [1,1,2] -输出: -[[1,1,2], - [1,2,1], - [2,1,1]] - -示例 2: -输入:nums = [1,2,3] -输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]] - -提示: -* 1 <= nums.length <= 8 -* -10 <= nums[i] <= 10 - -## 思路 - -这道题目和[回溯算法:排列问题!](https://mp.weixin.qq.com/s/SCOjeMX1t41wcvJq49GhMw)的区别在与**给定一个可包含重复数字的序列**,要返回**所有不重复的全排列**。 - -这里又涉及到去重了。 - -在[回溯算法:求组合总和(三)](https://mp.weixin.qq.com/s/_1zPYk70NvHsdY8UWVGXmQ) 、[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)我们分别详细讲解了组合问题和子集问题如何去重。 - -那么排列问题其实也是一样的套路。 - -**还要强调的是去重一定要对元素经行排序,这样我们才方便通过相邻的节点来判断是否重复使用了**。 - -我以示例中的 [1,1,2]为例 (为了方便举例,已经排序)抽象为一棵树,去重过程如图: - -![47.全排列II1](https://img-blog.csdnimg.cn/20201124201331223.png) - -图中我们对同一树层,前一位(也就是nums[i-1])如果使用过,那么就进行去重。 - -**一般来说:组合问题和排列问题是在树形结构的叶子节点上收集结果,而子集问题就是取树上所有节点的结果**。 - -在[回溯算法:排列问题!](https://mp.weixin.qq.com/s/SCOjeMX1t41wcvJq49GhMw)中已经详解讲解了排列问题的写法,在[回溯算法:求组合总和(三)](https://mp.weixin.qq.com/s/_1zPYk70NvHsdY8UWVGXmQ) 、[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)中详细讲解的去重的写法,所以这次我就不用回溯三部曲分析了,直接给出代码,如下: - -## C++代码 - -``` -class Solution { -private: - vector> result; - vector path; - void backtracking (vector& nums, vector& used) { - // 此时说明找到了一组 - if (path.size() == nums.size()) { - result.push_back(path); - return; - } - for (int i = 0; i < nums.size(); i++) { - // used[i - 1] == true,说明同一树支nums[i - 1]使用过 - // used[i - 1] == false,说明同一树层nums[i - 1]使用过 - // 如果同一树层nums[i - 1]使用过则直接跳过 - if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) { - continue; - } - if (used[i] == false) { - used[i] = true; - path.push_back(nums[i]); - backtracking(nums, used); - path.pop_back(); - used[i] = false; - } - } - } -public: - vector> permuteUnique(vector& nums) { - result.clear(); - path.clear(); - sort(nums.begin(), nums.end()); // 排序 - vector used(nums.size(), false); - backtracking(nums, vec, used); - return result; - } -}; - -``` - -## 拓展 - -大家发现,去重最为关键的代码为: - -``` -if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) { - continue; -} -``` - -**如果改成 `used[i - 1] == true`, 也是正确的!**,去重代码如下: -``` -if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) { - continue; -} -``` - -这是为什么呢,就是上面我刚说的,如果要对树层中前一位去重,就用`used[i - 1] == false`,如果要对树枝前一位去重用`used[i - 1] == true`。 - -**对于排列问题,树层上去重和树枝上去重,都是可以的,但是树层上去重效率更高!** - -这么说是不是有点抽象? - -来来来,我就用输入: [1,1,1] 来举一个例子。 - -树层上去重(used[i - 1] == false),的树形结构如下: - -![47.全排列II2](https://img-blog.csdnimg.cn/20201124201406192.png) - -树枝上去重(used[i - 1] == true)的树型结构如下: - -![47.全排列II3](https://img-blog.csdnimg.cn/20201124201431571.png) - -大家应该很清晰的看到,树层上对前一位去重非常彻底,效率很高,树枝上对前一位去重虽然最后可以得到答案,但是做了很多无用搜索。 - -# 总结 - -这道题其实还是用了我们之前讲过的去重思路,但有意思的是,去重的代码中,这么写: -``` -if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) { - continue; -} -``` -和这么写: -``` -if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) { - continue; -} -``` - -都是可以的,这也是很多同学做这道题目困惑的地方,知道`used[i - 1] == false`也行而`used[i - 1] == true`也行,但是就想不明白为啥。 - -所以我通过举[1,1,1]的例子,把这两个去重的逻辑分别抽象成树形结构,大家可以一目了然:为什么两种写法都可以以及哪一种效率更高! - -是不是豁然开朗了!! - -就酱,很多录友表示和「代码随想录」相见恨晚,那么大家帮忙多多宣传,让更多的同学知道这里,感谢啦! - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/0051.N皇后.md b/problems/0051.N皇后.md deleted file mode 100644 index 7475325e..00000000 --- a/problems/0051.N皇后.md +++ /dev/null @@ -1,234 +0,0 @@ -> 开始棋盘问题,如果对回溯法还不了解的同学可以看这个视频 - -如果对回溯法理论还不清楚的同学,可以先看这个视频[视频来了!!带你学透回溯算法(理论篇)](https://mp.weixin.qq.com/s/wDd5azGIYWjbU0fdua_qBg) - - -# 第51题. N皇后 - -题目链接: https://leetcode-cn.com/problems/n-queens/ - -n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。 - -上图为 8 皇后问题的一种解法。 -![51n皇后](https://img-blog.csdnimg.cn/20200821152118456.png) - -给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。 - -每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。 - -示例: -输入: 4 -输出: [ - [".Q..", // 解法 1 - "...Q", - "Q...", - "..Q."], - - ["..Q.", // 解法 2 - "Q...", - "...Q", - ".Q.."] -] -解释: 4 皇后问题存在两个不同的解法。 - -提示: -> 皇后,是国际象棋中的棋子,意味着国王的妻子。皇后只做一件事,那就是“吃子”。当她遇见可以吃的棋子时,就迅速冲上去吃掉棋子。当然,她横、竖、斜都可走一到七步,可进可退。(引用自 百度百科 - 皇后 ) - - -# 思路 - -都知道n皇后问题是回溯算法解决的经典问题,但是用回溯解决多了组合、切割、子集、排列问题之后,遇到这种二位矩阵还会有点不知所措。 - -首先来看一下皇后们的约束条件: - -1. 不能同行 -2. 不能同列 -3. 不能同斜线 - -确定完约束条件,来看看究竟要怎么去搜索皇后们的位置,其实搜索皇后的位置,可以抽象为一棵树。 - -下面我用一个3 * 3 的棋牌,将搜索过程抽象为一颗树,如图: - -![51.N皇后](https://img-blog.csdnimg.cn/20201118225433127.png) - -从图中,可以看出,二维矩阵中矩阵的高就是这颗树的高度,矩阵的宽就是树形结构中每一个节点的宽度。 - -那么我们用皇后们的约束条件,来回溯搜索这颗树,**只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了**。 - -## 回溯三部曲 - -按照我总结的如下回溯模板,我们来依次分析: - -``` -void backtracking(参数) { - if (终止条件) { - 存放结果; - return; - } - for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { - 处理节点; - backtracking(路径,选择列表); // 递归 - 回溯,撤销处理结果 - } -} -``` - -* 递归函数参数 - -我依然是定义全局变量二维数组result来记录最终结果。 - -参数n是棋牌的大小,然后用row来记录当前遍历到棋盘的第几层了。 - -代码如下: - -``` -vector> result; -void backtracking(int n, int row, vector& chessboard) { -``` - -* 递归终止条件 - -在如下树形结构中: -![51.N皇后](https://img-blog.csdnimg.cn/20201118225433127.png) - -可以看出,当递归到棋盘最底层(也就是叶子节点)的时候,就可以收集结果并返回了。 - -代码如下: - -``` -if (row == n) { - result.push_back(chessboard); - return; -} -``` - -* 单层搜索的逻辑 - -递归深度就是row控制棋盘的行,每一层里for循环的col控制棋盘的列,一行一列,确定了放置皇后的位置。 - -每次都是要从新的一行的起始位置开始搜,所以都是从0开始。 - -代码如下: - -``` -for (int col = 0; col < n; col++) { - if (isValid(row, col, chessboard, n)) { // 验证合法就可以放 - chessboard[row][col] = 'Q'; // 放置皇后 - backtracking(n, row + 1, chessboard); - chessboard[row][col] = '.'; // 回溯,撤销皇后 - } -} -``` - -* 验证棋牌是否合法 - -按照如下标准去重: - -1. 不能同行 -2. 不能同列 -3. 不能同斜线 (45度和135度角) - -代码如下: - -``` -bool isValid(int row, int col, vector& chessboard, int n) { - int count = 0; - // 检查列 - for (int i = 0; i < row; i++) { // 这是一个剪枝 - if (chessboard[i][col] == 'Q') { - return false; - } - } - // 检查 45度角是否有皇后 - for (int i = row - 1, j = col - 1; i >=0 && j >= 0; i--, j--) { - if (chessboard[i][j] == 'Q') { - return false; - } - } - // 检查 135度角是否有皇后 - for(int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) { - if (chessboard[i][j] == 'Q') { - return false; - } - } - return true; -} -``` - -在这份代码中,细心的同学可以发现为什么没有在同行进行检查呢? - -因为在单层搜索的过程中,每一层递归,只会选for循环(也就是同一行)里的一个元素,所以不用去重了。 - -那么按照这个模板不难写出如下代码: - -## C++代码 - -``` -class Solution { -private: -vector> result; -// n 为输入的棋盘大小 -// row 是当前递归到棋牌的第几行了 -void backtracking(int n, int row, vector& chessboard) { - if (row == n) { - result.push_back(chessboard); - return; - } - for (int col = 0; col < n; col++) { - if (isValid(row, col, chessboard, n)) { // 验证合法就可以放 - chessboard[row][col] = 'Q'; // 放置皇后 - backtracking(n, row + 1, chessboard); - chessboard[row][col] = '.'; // 回溯,撤销皇后 - } - } -} -bool isValid(int row, int col, vector& chessboard, int n) { - int count = 0; - // 检查列 - for (int i = 0; i < row; i++) { // 这是一个剪枝 - if (chessboard[i][col] == 'Q') { - return false; - } - } - // 检查 45度角是否有皇后 - for (int i = row - 1, j = col - 1; i >=0 && j >= 0; i--, j--) { - if (chessboard[i][j] == 'Q') { - return false; - } - } - // 检查 135度角是否有皇后 - for(int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) { - if (chessboard[i][j] == 'Q') { - return false; - } - } - return true; -} -public: - vector> solveNQueens(int n) { - result.clear(); - std::vector chessboard(n, std::string(n, '.')); - backtracking(n, 0, chessboard); - return result; - } -}; -``` - -可以看出,除了验证棋盘合法性的代码,省下来部分就是按照回溯法模板来的。 - -# 总结 - -本题是我们解决棋盘问题的第一道题目。 - -如果从来没有接触过N皇后问题的同学看着这样的题会感觉无从下手,可能知道要用回溯法,但也不知道该怎么去搜。 - -**这里我明确给出了棋盘的宽度就是for循环的长度,递归的深度就是棋盘的高度,这样就可以套进回溯法的模板里了**。 - -大家可以在仔细体会体会! - -就酱,如果感觉「代码随想录」干货满满,就分享给身边的朋友同学吧,他们可能也需要! - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/0052.N皇后II.md b/problems/0052.N皇后II.md deleted file mode 100644 index 4cf103fb..00000000 --- a/problems/0052.N皇后II.md +++ /dev/null @@ -1,85 +0,0 @@ -# 题目链接 -https://leetcode-cn.com/problems/n-queens-ii/ - -# 第51题. N皇后 - -n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。 - -上图为 8 皇后问题的一种解法。 -![51n皇后](https://img-blog.csdnimg.cn/20200821152118456.png) - -给定一个整数 n,返回 n 皇后不同的解决方案的数量。 - -示例: - -输入: 4 -输出: 2 -解释: 4 皇后问题存在如下两个不同的解法。 -[ - [".Q..",  // 解法 1 -  "...Q", -  "Q...", -  "..Q."], - - ["..Q.",  // 解法 2 -  "Q...", -  "...Q", -  ".Q.."] -] -# 思路 - -这道题目和 51.N皇后 基本没有区别 - -# C++代码 - -``` -class Solution { -private: -int count = 0; -void backtracking(int n, int row, vector& chessboard) { - if (row == n) { - count++; - return; - } - for (int col = 0; col < n; col++) { - if (isValid(row, col, chessboard, n)) { - chessboard[row][col] = 'Q'; // 放置皇后 - backtracking(n, row + 1, chessboard); - chessboard[row][col] = '.'; // 回溯 - } - } -} -bool isValid(int row, int col, vector& chessboard, int n) { - int count = 0; - // 检查列 - for (int i = 0; i < row; i++) { // 这是一个剪枝 - if (chessboard[i][col] == 'Q') { - return false; - } - } - // 检查 45度角是否有皇后 - for (int i = row - 1, j = col - 1; i >=0 && j >= 0; i--, j--) { - if (chessboard[i][j] == 'Q') { - return false; - } - } - // 检查 135度角是否有皇后 - for(int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) { - if (chessboard[i][j] == 'Q') { - return false; - } - } - return true; -} - -public: - int totalNQueens(int n) { - std::vector chessboard(n, std::string(n, '.')); - backtracking(n, 0, chessboard); - return count; - - } -}; -``` - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0053.最大子序和.md b/problems/0053.最大子序和.md deleted file mode 100644 index ab83d861..00000000 --- a/problems/0053.最大子序和.md +++ /dev/null @@ -1,141 +0,0 @@ - -> 从本题开始,贪心题目都比较难了! -通知:一些录友表示经常看不到每天的文章,现在公众号已经不按照发送时间推荐了,而是根据一些规则乱序推送,所以可能关注了「代码随想录」也一直看不到文章,建议把「代码随想录」设置星标哈,设置星标之后,每天就按发文时间推送了,我每天都是定时8:35发送的,嗷嗷准时! - -# 53. 最大子序和 - -题目地址:https://leetcode-cn.com/problems/maximum-subarray/ - -给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 - -示例: -输入: [-2,1,-3,4,-1,2,1,-5,4] -输出: 6 -解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。 - -# 思路 - -## 暴力解法 - -暴力解法的思路,第一层for 就是设置起始位置,第二层for循环遍历数组寻找最大值 - -时间复杂度:O(n^2) -空间复杂度:O(1) -``` -class Solution { -public: - int maxSubArray(vector& nums) { - int result = INT32_MIN; - int count = 0; - for (int i = 0; i < nums.size(); i++) { // 设置起始位置 - count = 0; - for (int j = i; j < nums.size(); j++) { // 每次从起始位置i开始遍历寻找最大值 - count += nums[j]; - result = count > result ? count : result; - } - } - return result; - } -}; -``` - -以上暴力的解法C++勉强可以过,其他语言就不确定了。 - -## 贪心解法 - -**贪心贪的是哪里呢?** - -如果 -2 1 在一起,计算起点的时候,一定是从1开始计算,因为负数只会拉低总和,这就是贪心贪的地方! - -局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。 - -全局最优:选取最大“连续和” - -**局部最优的情况下,并记录最大的“连续和”,可以推出全局最优**。 - - -从代码角度上来讲:遍历nums,从头开始用count累积,如果count一旦加上nums[i]变为负数,那么就应该从nums[i+1]开始从0累积count了,因为已经变为负数的count,只会拖累总和。 - -**这相当于是暴力解法中的不断调整最大子序和区间的起始位置**。 - - -**那有同学问了,区间终止位置不用调整么? 如何才能得到最大“连续和”呢?** - -区间的终止位置,其实就是如果count取到最大值了,及时记录下来了。例如如下代码: - -``` -if (count > result) result = count; -``` - -**这样相当于是用result记录最大子序和区间和(变相的算是调整了终止位置)**。 - -如动画所示: - - - -红色的起始位置就是贪心每次取count为正数的时候,开始一个区间的统计。 - -那么不难写出如下C++代码(关键地方已经注释) - -``` -class Solution { -public: - int maxSubArray(vector& nums) { - int result = INT32_MIN; - int count = 0; - for (int i = 0; i < nums.size(); i++) { - count += nums[i]; - if (count > result) { // 取区间累计的最大值(相当于不断确定最大子序终止位置) - result = count; - } - if (count <= 0) count = 0; // 相当于重置最大子序起始位置,因为遇到负数一定是拉低总和 - } - return result; - } -}; -``` -时间复杂度:O(n) -空间复杂度:O(1) - -当然题目没有说如果数组为空,应该返回什么,所以数组为空的话返回啥都可以了。 - -## 动态规划 - -当然本题还可以用动态规划来做,当前[「代码随想录」](https://img-blog.csdnimg.cn/20201124161234338.png)主要讲解贪心系列,后续到动态规划系列的时候会详细讲解本题的dp方法。 - -那么先给出我的dp代码如下,有时间的录友可以提前做一做: - -``` -class Solution { -public: - int maxSubArray(vector& nums) { - if (nums.size() == 0) return 0; - vector dp(nums.size(), 0); // dp[i]表示包括i之前的最大连续子序列和 - dp[0] = nums[0]; - int result = dp[0]; - for (int i = 1; i < nums.size(); i++) { - dp[i] = max(dp[i - 1] + nums[i], nums[i]); // 状态转移公式 - if (dp[i] > result) result = dp[i]; // result 保存dp[i]的最大值 - } - return result; - } -}; -``` - -时间复杂度:O(n) -空间复杂度:O(n) - -# 总结 - -本题的贪心思路其实并不好想,这也进一步验证了,别看贪心理论很直白,有时候看似是常识,但贪心的题目一点都不简单! - -后续将介绍的贪心题目都挺难的,哈哈,所以贪心很有意思,别小看贪心! - -就酱,如果感觉「代码随想录」干货满满,就帮忙转发一波吧,让更多的小伙伴知道这里! - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20201124161234338.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** - - - diff --git a/problems/0055.跳跃游戏.md b/problems/0055.跳跃游戏.md deleted file mode 100644 index 2f0ab898..00000000 --- a/problems/0055.跳跃游戏.md +++ /dev/null @@ -1,79 +0,0 @@ - -> 通知 - -# 55. 跳跃游戏 - -题目链接:https://leetcode-cn.com/problems/jump-game/ - -给定一个非负整数数组,你最初位于数组的第一个位置。 - -数组中的每个元素代表你在该位置可以跳跃的最大长度。 - -判断你是否能够到达最后一个位置。 - -示例 1: -输入: [2,3,1,1,4] -输出: true -解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3 步到达最后一个位置。 - -示例 2: -输入: [3,2,1,0,4] -输出: false -解释: 无论怎样,你总会到达索引为 3 的位置。但该位置的最大跳跃长度是 0 , 所以你永远不可能到达最后一个位置。 - - -## 思路 - -刚看到本题一开始可能想:当前位置元素如果是3,我究竟是跳一步呢,还是两步呢,还是三步呢,究竟跳几步才是最优呢? - -其实跳几步无所谓,关键在于可跳的覆盖范围! - -不一定非要明确一次究竟跳几步,每次取最大的跳跃步数,这个就是可以跳跃的覆盖范围。 - -这个范围内,别管是怎么跳的,反正一定可以跳过来。 - -**那么这个问题就转化为跳跃覆盖范围究竟可不可以覆盖到终点!** - -每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围。 - -**贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点**。 - -局部最优推出全局最优,找不出反例,试试贪心! - -如图: - -![55.跳跃游戏](https://img-blog.csdnimg.cn/20201124154758229.png) - -i每次移动只能在cover的范围内移动,每移动一个元素,cover得到该元素数值(新的覆盖范围)的补充,让i继续移动下去。 - -而cover每次只取 max(该元素数值补充后的范围, cover本身范围)。 - -如果cover大于等于了终点下标,直接return true就可以了。 - -C++代码如下: - -```C++ -class Solution { -public: - bool canJump(vector& nums) { - int cover = 0; - if (nums.size() == 1) return true; // 只有一个元素,就是能达到 - for (int i = 0; i <= cover; i++) { // 注意这里是小于等于cover - cover = max(i + nums[i], cover); - if (cover >= nums.size() - 1) return true; // 说明可以覆盖到终点了 - } - return false; - } -}; -``` -# 总结 - -这道题目关键点在于:不用拘泥于每次究竟跳跳几步,而是看覆盖范围,覆盖范围内一定是可以跳过来的,不用管是怎么跳的。 - -大家可以看出思路想出来了,代码还是非常简单的。 - -一些同学可能感觉,我在讲贪心系列的时候,题目和题目之间貌似没有什么联系? - -**是真的就是没什么联系,因为贪心无套路!**没有个整体的贪心框架解决一些列问题,只能是接触各种类型的题目锻炼自己的贪心思维! - -就酱,「代码随想录」值得推荐给身边的朋友同学们! diff --git a/problems/0056.合并区间.md b/problems/0056.合并区间.md deleted file mode 100644 index 1e0e1b9a..00000000 --- a/problems/0056.合并区间.md +++ /dev/null @@ -1,128 +0,0 @@ -> 「代码随想录」出品,毕竟精品! - -# 56. 合并区间 - -题目链接:https://leetcode-cn.com/problems/merge-intervals/ - -给出一个区间的集合,请合并所有重叠的区间。 - -示例 1: -输入: intervals = [[1,3],[2,6],[8,10],[15,18]] -输出: [[1,6],[8,10],[15,18]] -解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6]. - -示例 2: -输入: intervals = [[1,4],[4,5]] -输出: [[1,5]] -解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。 -注意:输入类型已于2019年4月15日更改。 请重置默认代码定义以获取新方法签名。 - -提示: - -* intervals[i][0] <= intervals[i][1] - -# 思路 - -大家应该都感觉到了,此题一定要排序,那么按照左边界排序,还是右边界排序呢? - -都可以! - -那么我按照左边界排序,排序之后局部最优:每次合并都取最大的右边界,这样就可以合并更多的区间了,整体最优:合并所有重叠的区间。 - -局部最优可以推出全局最优,找不出反例,试试贪心。 - -那有同学问了,本来不就应该合并最大右边界么,这和贪心有啥关系? - -有时候贪心就是常识!哈哈 - -按照左边界从小到大排序之后,如果 `intervals[i][0] < intervals[i - 1][1]` 即intervals[i]左边界 < intervals[i - 1]右边界,则一定有重复,因为intervals[i]的左边界一定是大于等于intervals[i - 1]的左边界。 - -即:intervals[i]的左边界在intervals[i - 1]左边界和右边界的范围内,那么一定有重复! - -这么说有点抽象,看图:(**注意图中区间都是按照左边界排序之后了**) - -![56.合并区间](https://img-blog.csdnimg.cn/20201223200632791.png) - -知道如何判断重复之后,剩下的就是合并了,如何去模拟合并区间呢? - -其实就是用合并区间后左边界和右边界,作为一个新的区间,加入到result数组里就可以了。如果没有合并就把原区间加入到result数组。 - -C++代码如下: - -```C++ -class Solution { -public: - // 按照区间左边界从小到大排序 - static bool cmp (const vector& a, const vector& b) { - return a[0] < b[0]; - } - vector> merge(vector>& intervals) { - vector> result; - if (intervals.size() == 0) return result; - sort(intervals.begin(), intervals.end(), cmp); - bool flag = false; // 标记最后一个区间有没有合并 - int length = intervals.size(); - - for (int i = 1; i < length; i++) { - int start = intervals[i - 1][0]; // 初始为i-1区间的左边界 - int end = intervals[i - 1][1]; // 初始i-1区间的右边界 - while (i < length && intervals[i][0] <= end) { // 合并区间 - end = max(end, intervals[i][1]); // 不断更新右区间 - if (i == length - 1) flag = true; // 最后一个区间也合并了 - i++; // 继续合并下一个区间 - } - // start和end是表示intervals[i - 1]的左边界右边界,所以最优intervals[i]区间是否合并了要标记一下 - result.push_back({start, end}); - } - // 如果最后一个区间没有合并,将其加入result - if (flag == false) { - result.push_back({intervals[length - 1][0], intervals[length - 1][1]}); - } - return result; - } -}; -``` - -当然以上代码有冗余一些,可以优化一下,如下:(思路是一样的) - -```C++ -class Solution { -public: - vector> merge(vector>& intervals) { - vector> result; - if (intervals.size() == 0) return result; - // 排序的参数使用了lamda表达式 - sort(intervals.begin(), intervals.end(), [](const vector& a, const vector& b){return a[0] < b[0];}); - - result.push_back(intervals[0]); - for (int i = 1; i < intervals.size(); i++) { - if (result.back()[1] >= intervals[i][0]) { // 合并区间 - result.back()[1] = max(result.back()[1], intervals[i][1]); - } else { - result.push_back(intervals[i]); - } - } - return result; - } -}; -``` - -* 时间复杂度:O(nlogn) ,有一个快排 -* 空间复杂度:O(1),我没有算result数组(返回值所需容器占的空间) - - -# 总结 - -对于贪心算法,很多同学都是:**如果能凭常识直接做出来,就会感觉不到自己用了贪心, 一旦第一直觉想不出来, 可能就一直想不出来了**。 - -跟着「代码随想录」刷题的录友应该感受过,贪心难起来,真的难。 - -那应该怎么办呢? - -正如我贪心系列开篇词[关于贪心算法,你该了解这些!](https://mp.weixin.qq.com/s/O935TaoHE9Eexwe_vSbRAg)中讲解的一样,贪心本来就没有套路,也没有框架,所以各种常规解法需要多接触多练习,自然而然才会想到。 - -「代码随想录」会把贪心常见的经典题目覆盖到,大家只要认真学习打卡就可以了。 - -就酱,学算法,就在「代码随想录」,值得介绍给身边的朋友同学们! - - diff --git a/problems/0057.插入区间.md b/problems/0057.插入区间.md deleted file mode 100644 index 79dba1c4..00000000 --- a/problems/0057.插入区间.md +++ /dev/null @@ -1,90 +0,0 @@ - -# 链接 -https://leetcode-cn.com/problems/insert-interval/ - -# 思路 -这道题目合并的情况有很多种,想想都让人头疼。 - -我把这道题目化为三步: - -## 步骤一:找到需要合并的区间 - -找到插入区间需要插入或者合并的位置。 - -代码如下: - -``` -int index = 0; // intervals的索引 -while (index < intervals.size() && intervals[index][1] < newInterval[0]) { - result.push_back(intervals[index++]); -} -``` - -此时intervals[index]就需要合并的区间了 - -## 步骤二:合并区间 - -合并区间还有两种情况 - -1. intervals[index]需要合并,如图: - - - -对于这种情况,只要是intervals[index]起始位置 <= newInterval终止位置,就要一直合并下去。 - -代码如下: - -``` -while (index < intervals.size() && intervals[index][0] <= newInterval[1]) { // 注意防止越界 - newInterval[0] = min(intervals[index][0], newInterval[0]); - newInterval[1] = max(intervals[index][1], newInterval[1]); - index++; -} -``` -合并之后,将newInterval放入result就可以了 - -2. intervals[index]不用合并,插入区间直接插入就行,如图: - - - -对于这种情况,就直接把newInterval放入result就可以了 - -## 步骤三:处理合并区间之后的区间 - -合并之后,就应该把合并之后的区间,以此加入result中。 - -代码如下: - -``` -while (index < intervals.size()) { - result.push_back(intervals[index++]); -} -``` - -# 整体C++代码 - -``` -class Solution { -public: - vector> insert(vector>& intervals, vector& newInterval) { - vector> result; - int index = 0; // intervals的索引 - // 步骤一:找到需要合并的区间 - while (index < intervals.size() && intervals[index][1] < newInterval[0]) { - result.push_back(intervals[index++]); - } - // 步骤二:合并区间 - while (index < intervals.size() && intervals[index][0] <= newInterval[1]) { - newInterval[0] = min(intervals[index][0], newInterval[0]); - newInterval[1] = max(intervals[index][1], newInterval[1]); - index++; - } - result.push_back(newInterval); - // 步骤三:处理合并区间之后的区间 - while (index < intervals.size()) { - result.push_back(intervals[index++]); - } - return result; - } -}; -``` diff --git a/problems/0059.螺旋矩阵II.md b/problems/0059.螺旋矩阵II.md deleted file mode 100644 index 111be4c4..00000000 --- a/problems/0059.螺旋矩阵II.md +++ /dev/null @@ -1,129 +0,0 @@ - -

- -

-

- - - - - - -

- - -> 一进循环深似海,从此offer是路人 - -# 题目59.螺旋矩阵II - -题目地址:https://leetcode-cn.com/problems/spiral-matrix-ii/ -给定一个正整数 n,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。 - -示例: - -输入: 3 -输出: -[ - [ 1, 2, 3 ], - [ 8, 9, 4 ], - [ 7, 6, 5 ] -] - -# 思路 - -这道题目可以说在面试中出现频率较高的题目,**本题并不涉及到什么算法,就是模拟过程,但却十分考察对代码的掌控能力。** - -要如何画出这个螺旋排列的正方形矩阵呢? - -相信很多同学刚开始做这种题目的时候,上来就是一波判断猛如虎。 - -结果运行的时候各种问题,然后开始各种修修补补,最后发现改了这里哪里有问题,改了那里这里又跑不起来了。 - -大家还记得我们在这篇文章[数组:每次遇到二分法,都是一看就会,一写就废](https://mp.weixin.qq.com/s/fCf5QbPDtE6SSlZ1yh_q8Q)中讲解了二分法,提到如果要写出正确的二分法一定要坚持**循环不变量原则**。 - -而求解本题依然是要坚持循环不变量原则。 - -模拟顺时针画矩阵的过程: - -* 填充上行从左到右 -* 填充右列从上到下 -* 填充下行从右到左 -* 填充左列从下到上 - -由外向内一圈一圈这么画下去。 - -可以发现这里的边界条件非常多,在一个循环中,如此多的边界条件,如果不按照固定规则来遍历,那就是**一进循环深似海,从此offer是路人**。 - -这里一圈下来,我们要画每四条边,这四条边怎么画,每画一条边都要坚持一致的左闭右开,或者左开又闭的原则,这样这一圈才能按照统一的规则画下来。 - -那么我按照左闭右开的原则,来画一圈,大家看一下: - -![螺旋矩阵](https://img-blog.csdnimg.cn/2020121623550681.png) - -这里每一种颜色,代表一条边,我们遍历的长度,可以看出每一个拐角处的处理规则,拐角处让给新的一条边来继续画。 - -这也是坚持了每条边左闭右开的原则。 - -一些同学做这道题目之所以一直写不好,代码越写越乱。 - -就是因为在画每一条边的时候,一会左开又闭,一会左闭右闭,一会又来左闭右开,岂能不乱。 - -代码如下,已经详细注释了每一步的目的,可以看出while循环里判断的情况是很多的,代码里处理的原则也是统一的左闭右开。 - -# C++代码 - -```C++ -class Solution { -public: - vector> generateMatrix(int n) { - vector> res(n, vector(n, 0)); // 使用vector定义一个二维数组 - int startx = 0, starty = 0; // 定义每循环一个圈的起始位置 - int loop = n / 2; // 每个圈循环几次,例如n为奇数3,那么loop = 1 只是循环一圈,矩阵中间的值需要单独处理 - int mid = n / 2; // 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(2, 2) - int count = 1; // 用来给矩阵中每一个空格赋值 - int offset = 1; // 每一圈循环,需要控制每一条边遍历的长度 - int i,j; - while (loop --) { - i = startx; - j = starty; - - // 下面开始的四个for就是模拟转了一圈 - // 模拟填充上行从左到右(左闭右开) - for (j = starty; j < starty + n - offset; j++) { - res[startx][j] = count++; - } - // 模拟填充右列从上到下(左闭右开) - for (i = startx; i < startx + n - offset; i++) { - res[i][j] = count++; - } - // 模拟填充下行从右到左(左闭右开) - for (; j > starty; j--) { - res[i][j] = count++; - } - // 模拟填充左列从下到上(左闭右开) - for (; i > startx; i--) { - res[i][j] = count++; - } - - // 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1) - startx++; - starty++; - - // offset 控制每一圈里每一条边遍历的长度 - offset += 2; - } - - // 如果n为奇数的话,需要单独给矩阵最中间的位置赋值 - if (n % 2) { - res[mid][mid] = count; - } - return res; - } -}; -``` - -**循序渐进学算法,认准「代码随想录」,Carl手把手带你过关斩将!** - -

- -

diff --git a/problems/0062.不同路径.md b/problems/0062.不同路径.md deleted file mode 100644 index 7eee77b3..00000000 --- a/problems/0062.不同路径.md +++ /dev/null @@ -1,176 +0,0 @@ - -# 思路 - -## 深搜 - -这道题目,刚一看最直观的想法就是用图论里的深搜,来枚举出来有多少种路径。 - -注意题目中说机器人每次只能向下或者向右移动一步,那么其实**机器人走过的路径可以抽象为一颗二叉树,而叶子节点就是终点!** - -如图举例: - -![62.不同路径](https://img-blog.csdnimg.cn/20201209113602700.png) - -此时问题就可以转化为求二叉树叶子节点的个数,代码如下: - -```C++ -class Solution { -private: - int dfs(int i, int j, int m, int n) { - if (i > m || j > n) return 0; // 越界了 - if (i == m && j == n) return 1; // 找到一种方法,相当于找到了叶子节点 - return dfs(i + 1, j, m, n) + dfs(i, j + 1, m, n); - } -public: - int uniquePaths(int m, int n) { - return dfs(1, 1, m, n); - } -}; -``` - -大家如果提交了代码就会发现超时了! - -来分析一下时间复杂度,这个深搜的算法,其实就是要遍历整个二叉树。 - -这颗树的深度其实就是m+n-1(深度按从1开始计算)。 - -那二叉树的节点个数就是 2^(m + n - 1) - 1。可以理解深搜的算法就是遍历了整个满二叉树(其实没有把搜索节点都遍历到,只是近似而已) - -所以上面深搜代码的时间复杂度为O(2^(m + n - 1) - 1),可以看出,这是指数级别的时间复杂度,是非常大的。 - -## 动态规划 - -机器人从(0 , 0) 位置触发,到(m - 1, n - 1)终点。 - -按照动规三部曲来分析: - -* dp数组表述啥 - -这里设计一个dp二维数组,dp[i][j] 表示从(0 ,0)出发,到(i, j) 有几条不同的路径。 - -* dp数组的初始化 - -如何初始化呢,首先dp[i][0]一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条,那么dp[0][j]也同理。 - -所以初始化代码为: - -``` -for (int i = 0; i < m; i++) dp[i][0] = 1; -for (int j = 0; j < n; j++) dp[0][j] = 1; -``` - -* 递推公式 - -想要求dp[i][j],只能有两个方向来推导出来,即dp[i - 1][j] 和 dp[i][j - 1]。 - -此时在回顾一下 dp[i-1][j] 表示啥,是从(0, 0)的位置到(i-1, j)有几条路径,dp[i][j - 1]同理。 - -那么很自然,dp[i][j] = dp[i-1][j] + dp[i][j - 1],因为dp[i][j]只有这两个方向过来。 - -如图所示: - -![62.不同路径1](https://img-blog.csdnimg.cn/20201209113631392.png) - -C++代码如下: - -```C++ -class Solution { -public: - int uniquePaths(int m, int n) { - vector> dp(m, vector(n, 0)); - for (int i = 0; i < m; i++) dp[i][0] = 1; - for (int j = 0; j < n; j++) dp[0][j] = 1; - for (int i = 1; i < m; i++) { - for (int j = 1; j < n; j++) { - dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; - } - } - return dp[m - 1][n - 1]; - } -}; -``` -* 时间复杂度:O(m * n) -* 空间复杂度:O(m * n) - -其实用一个一维数组(也可以理解是滚动数组)就可以了,但是不利于理解,可以优化点空间,建议先理解了二维,在理解一维,C++代码如下: - -```C++ -class Solution { -public: - int uniquePaths(int m, int n) { - vector dp(n); - for (int i = 0; i < n; i++) dp[i] = 1; - for (int j = 1; j < m; j++) { - for (int i = 1; i < n; i++) { - dp[i] += dp[i - 1]; - } - } - return dp[n - 1]; - } -}; -``` -* 时间复杂度:O(m * n) -* 空间复杂度:O(n) - -# 数论方法 - -在这个图中,可以看出一共 m,n的话,无论怎么走,走到终点都需要 m + n - 2 步。 - -![62.不同路径](https://img-blog.csdnimg.cn/20201209113602700.png) - -在这m + n - 2 步中,一定有 m - 1 步是要向下走的,不用管什么时候向下走。 - -那么有几种走法呢? 可以转化为,给你m + n - 2个不同的数,随便取m - 1个数,有几种取法。 - -那么这就是一个组合问题了。 - -那么答案,如图所示: - -![62.不同路径2](https://img-blog.csdnimg.cn/20201209113725324.png) - -**求组合的时候,要防止两个int相乘溢出!** 所以不能把算式的分子都算出来,分母都算出来再做除法。 - -``` -class Solution { -public: - int uniquePaths(int m, int n) { - int numerator = 1, denominator = 1; - int count = m - 1; - int t = m + n - 2; - while (count--) numerator *= (t--); // 计算分子,此时分子就会溢出 - for (int i = 1; i <= m - 1; i++) denominator *= i; // 计算分母 - return numerator / denominator; - } -}; - -``` - -需要在计算分子的时候,不算除以分母,代码如下: - -``` -class Solution { -public: - int uniquePaths(int m, int n) { - long long numerator = 1; // 分子 - int denominator = m - 1; // 分母 - int count = m - 1; - int t = m + n - 2; - while (count--) { - numerator *= (t--); - while (denominator != 0 && numerator % denominator == 0) { - numerator /= denominator; - denominator--; - } - } - return numerator; - } -}; -``` - -计算组合问题的代码还是有难度的,特别是处理溢出的情况! - -最后这个代码还有点复杂了,还是可以优化,我就不继续优化了,有空在整理一下,哈哈,就酱! - - - - diff --git a/problems/0070.爬楼梯.md b/problems/0070.爬楼梯.md deleted file mode 100644 index bb98afca..00000000 --- a/problems/0070.爬楼梯.md +++ /dev/null @@ -1,86 +0,0 @@ - -# 思路 - -本题大家多举一个例子,就发现这其实就是斐波那契数列。 - -题目509. 斐波那契数中的代码初始化部分稍加改动,就可以过了本题。 - -C++代码如下: -``` -class Solution { -public: - int climbStairs(int n) { - if (n <= 1) return n; - vector dp(n + 1); - dp[0] = 1; - dp[1] = 1; - for (int i = 2; i <= n; i++) { - dp[i] = dp[i - 1] + dp[i - 2]; - } - return dp[n]; - - } -}; -``` - -既然这么简单为什么还要讲呢,其实本题稍加改动就是一道面试好题,如果每次可以爬 1 或 2或3或者m 个台阶呢,走到楼顶有几种方法? - -* 确定dp数组以及下标的含义 - -dp[i]:爬到有i个台阶的楼顶,有dp[i]种方法 - -* 确定递推公式 - -dp[i]有几种来源,dp[i - 1],dp[i - 2],dp[i - 3] 等等,即:dp[i - j] - -那么递推公式为:dp[i] += dp[i - j] - -* dp数组如何初始化 - -既然递归公式是 dp[i] += dp[i - j],那么dp[0] 一定为1,dp[0]是递归中一切数值的基础所在,如果dp[0]是0的话,其他数值都是0了。 - -下标非0的dp[i]初始化为0,因为dp[i]是靠dp[i-j]累计上来的,dp[i]本身为0这样才不会影响结果 - -* 确定遍历顺序 - -这是背包里求排列问题,即:1 2 步 和 2 1 步都是上三个台阶,但是这两种方法不! - -所以需将target放在外循环,将nums放在内循环。 - -每一步可以走多次,说明这是完全背包,内循环需要从前向后遍历。 - - -C++代码如下: -``` -class Solution { -public: - int climbStairs(int n) { - vector dp(n + 1, 0); - dp[0] = 1; - for (int i = 1; i <= n; i++) { - for (int j = 1; j <= m; j++) { - if (i - j >= 0) dp[i] += dp[i - j]; - } - } - return dp[n]; - } -}; -``` - -代码中m表示最多可以爬m个台阶,代码中把m改成2就是本题70.爬楼梯的代码了。 - -# 总结 - -如果我来面试的话,我就会想给候选人出一个 本题原题,看其表现,如果顺利写出来,进而在要求每次可以爬[1 - m]个台阶应该怎么写。 - -顺便再考察一下两个for循环的嵌套顺序,为什么target放外面,nums放里面。这就能反馈出对背包问题本质的掌握程度,是不是刷题背公式,一眼就看出来。 - -这么一连套下来,如果候选人都能答出来,相信任何一位面试官都是非常满意的。 - -**本题代码不长,题目也很普通,当稍稍一进阶就可以考察本质问题,而且题目进阶的内容在leetcode上并没有,一定程度上就可以排除掉刷题党了,简直是面试题目的绝佳选择!** - -相信通过这道简单的斐波那契数列题目,大家能感受到大厂面试官最喜欢什么样的面试题目了,并不是手撕红黑树! - - -所以本题是一道非常好的题目。 - diff --git a/problems/0077.组合.md b/problems/0077.组合.md deleted file mode 100644 index 0a7187d1..00000000 --- a/problems/0077.组合.md +++ /dev/null @@ -1,248 +0,0 @@ - -> 回溯法的第一道题目,就不简单呀! - -# 第77题. 组合 - -题目链接:https://leetcode-cn.com/problems/combinations/ - -给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。 - -示例: -输入: n = 4, k = 2 -输出: -[ - [2,4], - [3,4], - [2,3], - [1,2], - [1,3], - [1,4], -] - -也可以直接看我的B站视频:[带你学透回溯算法-组合问题(对应力扣题目:77.组合)](https://www.bilibili.com/video/BV1ti4y1L7cv#reply3733925949) - -# 思路 - -本题这是回溯法的经典题目。 - -直接的解法当然是使用for循环,例如示例中k为2,很容易想到 用两个for循环,这样就可以输出 和示例中一样的结果。 - -代码如下: -``` -int n = 4; -for (int i = 1; i <= n; i++) { - for (int j = i + 1; j <= n; j++) { - cout << i << " " << j << endl; - } -} -``` - -输入:n = 100, k = 3 -那么就三层for循环,代码如下: - -``` -int n = 100; -for (int i = 1; i <= n; i++) { - for (int j = i + 1; j <= n; j++) { - for (int u = j + 1; u <= n; n++) { - cout << i << " " << j << " " << u << endl; - } - } -} -``` - -**如果n为100,k为50呢,那就50层for循环,是不是开始窒息**。 - -**此时就会发现虽然想暴力搜索,但是用for循环嵌套连暴力都写不出来!** - -咋整? - -回溯搜索法来了,虽然回溯法也是暴力,但至少能写出来,不像for循环嵌套k层让人绝望。 - -那么回溯法怎么暴力搜呢? - -上面我们说了**要解决 n为100,k为50的情况,暴力写法需要嵌套50层for循环,那么回溯法就用递归来解决嵌套层数的问题**。 - -递归来做层叠嵌套(可以理解是开k层for循环),**每一次的递归中嵌套一个for循环,那么递归就可以用于解决多层嵌套循环的问题了**。 - -此时递归的层数大家应该知道了,例如:n为100,k为50的情况下,就是递归50层。 - -一些同学本来对递归就懵,回溯法中递归还要嵌套for循环,可能就直接晕倒了! - -如果脑洞模拟回溯搜索的过程,绝对可以让人窒息,所以需要抽象图形结构来进一步理解。 - -**我们在[关于回溯算法,你该了解这些!](https://mp.weixin.qq.com/s/gjSgJbNbd1eAA5WkA-HeWw)中说道回溯法解决的问题都可以抽象为树形结构(N叉树),用树形结构来理解回溯就容易多了**。 - -那么我把组合问题抽象为如下树形结构: - -![77.组合](https://img-blog.csdnimg.cn/20201123195223940.png) - -可以看出这个棵树,一开始集合是 1,2,3,4, 从左向右取数,取过的数,不在重复取。 - -第一次取1,集合变为2,3,4 ,因为k为2,我们只需要再取一个数就可以了,分别取2,3,4,得到集合[1,2] [1,3] [1,4],以此类推。 - -**每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围**。 - -**图中可以发现n相当于树的宽度,k相当于树的深度**。 - -那么如何在这个树上遍历,然后收集到我们要的结果集呢? - -**图中每次搜索到了叶子节点,我们就找到了一个结果**。 - -相当于只需要把达到叶子节点的结果收集起来,就可以求得 n个数中k个数的组合集合。 - -在[关于回溯算法,你该了解这些!](https://mp.weixin.qq.com/s/gjSgJbNbd1eAA5WkA-HeWw)中我们提到了回溯法三部曲,那么我们按照回溯法三部曲开始正式讲解代码了。 - - -## 回溯法三部曲 - -* 递归函数的返回值以及参数 - -在这里要定义两个全局变量,一个用来存放符合条件单一结果,一个用来存放符合条件结果的集合。 - -代码如下: - -``` -vector> result; // 存放符合条件结果的集合 -vector path; // 用来存放符合条件结果 -``` - -其实不定义这两个全局遍历也是可以的,把这两个变量放进递归函数的参数里,但函数里参数太多影响可读性,所以我定义全局变量了。 - -函数里一定有两个参数,既然是集合n里面取k的数,那么n和k是两个int型的参数。 - -然后还需要一个参数,为int型变量startIndex,这个参数用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,...,n] )。 - -为什么要有这个startIndex呢? - -**每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围,就是要靠startIndex**。 - -从下图中红线部分可以看出,在集合[1,2,3,4]取1之后,下一层递归,就要在[2,3,4]中取数了,那么下一层递归如何知道从[2,3,4]中取数呢,靠的就是startIndex。 - -![77.组合2](https://img-blog.csdnimg.cn/20201123195328976.png) - -所以需要startIndex来记录下一层递归,搜索的起始位置。 - -那么整体代码如下: - -``` -vector> result; // 存放符合条件结果的集合 -vector path; // 用来存放符合条件单一结果 -void backtracking(int n, int k, int startIndex) -``` - -* 回溯函数终止条件 - -什么时候到达所谓的叶子节点了呢? - -path这个数组的大小如果达到k,说明我们找到了一个子集大小为k的组合了,在图中path存的就是根节点到叶子节点的路径。 - -如图红色部分: - -![77.组合3](https://img-blog.csdnimg.cn/20201123195407907.png) - -此时用result二维数组,把path保存起来,并终止本层递归。 - -所以终止条件代码如下: - -``` -if (path.size() == k) { - result.push_back(path); - return; -} -``` - -* 单层搜索的过程 - -回溯法的搜索过程就是一个树型结构的遍历过程,在如下图中,可以看出for循环用来横向遍历,递归的过程是纵向遍历。 - -![77.组合1](https://img-blog.csdnimg.cn/20201123195242899.png) - -如此我们才遍历完图中的这棵树。 - -for循环每次从startIndex开始遍历,然后用path保存取到的节点i。 - -代码如下: - -``` -for (int i = startIndex; i <= n; i++) { // 控制树的横向遍历 - path.push_back(i); // 处理节点 - backtracking(n, k, i + 1); // 递归:控制树的纵向遍历,注意下一层搜索要从i+1开始 - path.pop_back(); // 回溯,撤销处理的节点 -} -``` - -可以看出backtracking(递归函数)通过不断调用自己一直往深处遍历,总会遇到叶子节点,遇到了叶子节点就要返回。 - -backtracking的下面部分就是回溯的操作了,撤销本次处理的结果。 - -关键地方都讲完了,组合问题C++完整代码如下: - - -``` -class Solution { -private: - vector> result; // 存放符合条件结果的集合 - vector path; // 用来存放符合条件结果 - void backtracking(int n, int k, int startIndex) { - if (path.size() == k) { - result.push_back(path); - return; - } - for (int i = startIndex; i <= n; i++) { - path.push_back(i); // 处理节点 - backtracking(n, k, i + 1); // 递归 - path.pop_back(); // 回溯,撤销处理的节点 - } - } -public: - vector> combine(int n, int k) { - result.clear(); // 可以不写 - path.clear(); // 可以不写 - backtracking(n, k, 1); - return result; - } -}; -``` - -还记得我们在[关于回溯算法,你该了解这些!](https://mp.weixin.qq.com/s/gjSgJbNbd1eAA5WkA-HeWw)中给出的回溯法模板么? - -如下: -``` -void backtracking(参数) { - if (终止条件) { - 存放结果; - return; - } - - for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { - 处理节点; - backtracking(路径,选择列表); // 递归 - 回溯,撤销处理结果 - } -} -``` - -**对比一下本题的代码,是不是发现有点像!** 所以有了这个模板,就有解题的大体方向,不至于毫无头绪。 - -# 总结 - -组合问题是回溯法解决的经典问题,我们开始的时候给大家列举一个很形象的例子,就是n为100,k为50的话,直接想法就需要50层for循环。 - -从而引出了回溯法就是解决这种k层for循环嵌套的问题。 - -然后进一步把回溯法的搜索过程抽象为树形结构,可以直观的看出搜索的过程。 - -接着用回溯法三部曲,逐步分析了函数参数、终止条件和单层搜索的过程。 - -**本题其实是可以剪枝优化的,大家可以思考一下,具体如何剪枝我会在下一篇详细讲解,敬请期待!** - -**就酱,如果对你有帮助,就帮Carl转发一下吧,让更多的同学发现这里!** - - -**[本题剪枝操作文章链接](https://mp.weixin.qq.com/s/Ri7spcJMUmph4c6XjPWXQA)** - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/0077.组合优化.md b/problems/0077.组合优化.md deleted file mode 100644 index 6f12abd0..00000000 --- a/problems/0077.组合优化.md +++ /dev/null @@ -1,129 +0,0 @@ - -> 如果想在电脑上看文章的话,可以看这里:https://github.com/youngyangyang04/leetcode-master,已经按照顺序整理了「代码随想录」的所有文章,可以fork到自己仓库里,随时复习。**那么重点来了,来都来了,顺便给一个star吧,哈哈** - - -在[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)中,我们通过回溯搜索法,解决了n个数中求k个数的组合问题。 - -文中的回溯法是可以剪枝优化的,本篇我们继续来看一下题目77. 组合。 - -链接:https://leetcode-cn.com/problems/combinations/ - -**看本篇之前,需要先看[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)**。 - -大家先回忆一下[77. 组合]给出的回溯法的代码: - -``` -class Solution { -private: - vector> result; // 存放符合条件结果的集合 - vector path; // 用来存放符合条件结果 - void backtracking(int n, int k, int startIndex) { - if (path.size() == k) { - result.push_back(path); - return; - } - for (int i = startIndex; i <= n; i++) { - path.push_back(i); // 处理节点 - backtracking(n, k, i + 1); // 递归 - path.pop_back(); // 回溯,撤销处理的节点 - } - } -public: - vector> combine(int n, int k) { - result.clear(); // 可以不写 - path.clear(); // 可以不写 - backtracking(n, k, 1); - return result; - } -}; -``` - -## 剪枝优化 - -我们说过,回溯法虽然是暴力搜索,但也有时候可以有点剪枝优化一下的。 - -在遍历的过程中有如下代码: - -``` -for (int i = startIndex; i <= n; i++) { - path.push_back(i); - backtracking(n, k, i + 1); - path.pop_back(); -} -``` - -这个遍历的范围是可以剪枝优化的,怎么优化呢? - -来举一个例子,n = 4,k = 4的话,那么第一层for循环的时候,从元素2开始的遍历都没有意义了。 在第二层for循环,从元素3开始的遍历都没有意义了。 - -这么说有点抽象,如图所示: - - - -图中每一个节点(图中为矩形),就代表本层的一个for循环,那么每一层的for循环从第二个数开始遍历的话,都没有意义,都是无效遍历。 - -**所以,可以剪枝的地方就在递归中每一层的for循环所选择的起始位置**。 - -**如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了**。 - -注意代码中i,就是for循环里选择的起始位置。 -``` -for (int i = startIndex; i <= n; i++) { -``` - -接下来看一下优化过程如下: - -1. 已经选择的元素个数:path.size(); - -2. 还需要的元素个数为: k - path.size(); - -3. 在集合n中至多要从该起始位置 : n - (k - path.size()) + 1,开始遍历 - -为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。 - -举个例子,n = 4,k = 3, 目前已经选取的元素为0(path.size为0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。 - -从2开始搜索都是合理的,可以是组合[2, 3, 4]。 - -这里大家想不懂的话,建议也举一个例子,就知道是不是要+1了。 - -所以优化之后的for循环是: - -``` -for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置 -``` - -优化后整体代码如下: - -``` -class Solution { -private: - vector> result; - vector path; - void backtracking(int n, int k, int startIndex) { - if (path.size() == k) { - result.push_back(path); - return; - } - for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) { // 优化的地方 - path.push_back(i); // 处理节点 - backtracking(n, k, i + 1); - path.pop_back(); // 回溯,撤销处理的节点 - } - } -public: - - vector> combine(int n, int k) { - backtracking(n, k, 1); - return result; - } -}; -``` - -# 总结 - -本篇我们准对求组合问题的回溯法代码做了剪枝优化,这个优化如果不画图的话,其实不好理解,也不好讲清楚。 - -所以我依然是把整个回溯过程抽象为一颗树形结构,然后可以直观的看出,剪枝究竟是剪的哪里。 - -**就酱,学到了就帮Carl转发一下吧,让更多的同学知道这里!** diff --git a/problems/0078.子集.md b/problems/0078.子集.md deleted file mode 100644 index 8a2ff8a3..00000000 --- a/problems/0078.子集.md +++ /dev/null @@ -1,175 +0,0 @@ -> 认识本质之后,这就是一道模板题 - -# 第78题. 子集 - -题目地址:https://leetcode-cn.com/problems/subsets/ - -给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。 - -说明:解集不能包含重复的子集。 - -示例: -输入: nums = [1,2,3] -输出: -[ - [3], -  [1], -  [2], -  [1,2,3], -  [1,3], -  [2,3], -  [1,2], -  [] -] - -# 思路 - -求子集问题和[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)和[回溯算法:分割问题!](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q)又不一样了。 - -如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,**那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!** - -其实子集也是一种组合问题,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。 - -**那么既然是无序,取过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始,而不是从0开始!** - -有同学问了,什么时候for可以从0开始呢? - -求排列问题的时候,就要从0开始,因为集合是有序的,{1, 2} 和{2, 1}是两个集合,排列问题我们后续的文章就会讲到的。 - -以示例中nums = [1,2,3]为例把求子集抽象为树型结构,如下: - -![78.子集](https://img-blog.csdnimg.cn/202011232041348.png) - -从图中红线部分,可以看出**遍历这个树的时候,把所有节点都记录下来,就是要求的子集集合**。 - -## 回溯三部曲 - -* 递归函数参数 - -全局变量数组path为子集收集元素,二维数组result存放子集组合。(也可以放到递归函数参数里) - -递归函数参数在上面讲到了,需要startIndex。 - -代码如下: - -``` -vector> result; -vector path; -void backtracking(vector& nums, int startIndex) { -``` - -* 递归终止条件 - -从图中可以看出: - -![78.子集](https://img-blog.csdnimg.cn/202011232041348.png) - -剩余集合为空的时候,就是叶子节点。 - -那么什么时候剩余集合为空呢? - -就是startIndex已经大于数组的长度了,就终止了,因为没有元素可取了,代码如下: - -``` -if (startIndex >= nums.size()) { - return; -} -``` - -**其实可以不需要加终止条件,因为startIndex >= nums.size(),本层for循环本来也结束了**。 - -* 单层搜索逻辑 - -**求取子集问题,不需要任何剪枝!因为子集就是要遍历整棵树**。 - -那么单层递归逻辑代码如下: - -``` -for (int i = startIndex; i < nums.size(); i++) { - path.push_back(nums[i]); // 子集收集元素 - backtracking(nums, i + 1); // 注意从i+1开始,元素不重复取 - path.pop_back(); // 回溯 -} -``` - -## C++代码 - -根据[关于回溯算法,你该了解这些!](https://mp.weixin.qq.com/s/gjSgJbNbd1eAA5WkA-HeWw)给出的回溯算法模板: - -``` -void backtracking(参数) { - if (终止条件) { - 存放结果; - return; - } - - for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { - 处理节点; - backtracking(路径,选择列表); // 递归 - 回溯,撤销处理结果 - } -} -``` - -可以写出如下回溯算法C++代码: - -``` -class Solution { -private: - vector> result; - vector path; - void backtracking(vector& nums, int startIndex) { - result.push_back(path); // 收集子集,要放在终止添加的上面,否则会漏掉自己 - if (startIndex >= nums.size()) { // 终止条件可以不加 - return; - } - for (int i = startIndex; i < nums.size(); i++) { - path.push_back(nums[i]); - backtracking(nums, i + 1); - path.pop_back(); - } - } -public: - vector> subsets(vector& nums) { - result.clear(); - path.clear(); - backtracking(nums, 0); - return result; - } -}; - -``` - -在注释中,可以发现可以不写终止条件,因为本来我们就要遍历整颗树。 - -有的同学可能担心不写终止条件会不会无限递归? - -并不会,因为每次递归的下一层就是从i+1开始的。 - -# 总结 - -相信大家经过了 -* 组合问题: - * [回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ) - * [回溯算法:组合问题再剪剪枝](https://mp.weixin.qq.com/s/Ri7spcJMUmph4c6XjPWXQA) - * [回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w) - * [回溯算法:电话号码的字母组合](https://mp.weixin.qq.com/s/e2ua2cmkE_vpYjM3j6HY0A) - * [回溯算法:求组合总和(二)](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw) - * [回溯算法:求组合总和(三)](https://mp.weixin.qq.com/s/_1zPYk70NvHsdY8UWVGXmQ) -* 分割问题: - * [回溯算法:分割回文串](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q) - * [回溯算法:复原IP地址](https://mp.weixin.qq.com/s/v--VmA8tp9vs4bXCqHhBuA) - -洗礼之后,发现子集问题还真的有点简单了,其实这就是一道标准的模板题。 - -但是要清楚子集问题和组合问题、分割问题的的区别,**子集是收集树形结构中树的所有节点的结果**。 - -**而组合问题、分割问题是收集树形结构中叶子节点的结果**。 - -**就酱,如果感觉收获满满,就帮Carl宣传一波「代码随想录」吧!** - - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/0083.删除排序链表中的重复元素.md b/problems/0083.删除排序链表中的重复元素.md deleted file mode 100644 index b725f7f8..00000000 --- a/problems/0083.删除排序链表中的重复元素.md +++ /dev/null @@ -1,40 +0,0 @@ - -## 题目地址 - -https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list/ - -## 思路 - -这道题目没有必要设置虚拟节点,因为不会删除头结点 - -## 代码 - -``` -/** - * Definition for singly-linked list. - * struct ListNode { - * int val; - * ListNode *next; - * ListNode(int x) : val(x), next(NULL) {} - * }; - */ -class Solution { -public: - ListNode* deleteDuplicates(ListNode* head) { - ListNode* p = head; - while (p != NULL && p->next!= NULL) { - if (p->val == p->next->val) { - ListNode* tmp = p->next; - p->next = p->next->next; - delete tmp; - } else { - p = p->next; - } - } - return head; - } -}; -``` - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0084.柱状图中最大的矩形.md b/problems/0084.柱状图中最大的矩形.md deleted file mode 100644 index e5057e55..00000000 --- a/problems/0084.柱状图中最大的矩形.md +++ /dev/null @@ -1,104 +0,0 @@ - -# 链接 - -https://leetcode-cn.com/problems/largest-rectangle-in-histogram/ - -## 思路 - -``` -class Solution { -public: - int largestRectangleArea(vector& heights) { - int sum = 0; - for (int i = 0; i < heights.size(); i++) { - int left = i; - int right = i; - for (; left >= 0; left--) { - if (heights[left] < heights[i]) break; - } - for (; right < heights.size(); right++) { - if (heights[right] < heights[i]) break; - } - int w = right - left - 1; - int h = heights[i]; - sum = max(sum, w * h); - } - return sum; - } -}; -``` - -如上代码并不能通过leetcode,超时了,因为时间复杂度是O(n^2)。 - -## 思考一下动态规划 - -## 单调栈 - -单调栈的思路还是不容易理解的, - -想清楚从大到小,还是从小到大, - -本题是从栈底到栈头 从小到大,和 接雨水正好反过来。 - - -``` -class Solution { -public: - int largestRectangleArea(vector& heights) { - stack st; - heights.insert(heights.begin(), 0); // 数组头部加入元素0 - heights.push_back(0); // 数组尾部加入元素0 - st.push(0); - int result = 0; - // 第一个元素已经入栈,从下表1开始 - for (int i = 1; i < heights.size(); i++) { - // 注意heights[i] 是和heights[st.top()] 比较 ,st.top()是下表 - if (heights[i] > heights[st.top()]) { - st.push(i); - } else if (heights[i] == heights[st.top()]) { - st.pop(); // 这个可以加,可以不加,效果一样,思路不同 - st.push(i); - } else { - while (heights[i] < heights[st.top()]) { // 注意是while - int mid = st.top(); - st.pop(); - int left = st.top(); - int right = i; - int w = right - left - 1; - int h = heights[mid]; - result = max(result, w * h); - } - st.push(i); - } - } - return result; - } -}; - -``` - -代码精简之后: - -``` -class Solution { -public: - int largestRectangleArea(vector& heights) { - stack st; - heights.insert(heights.begin(), 0); // 数组头部加入元素0 - heights.push_back(0); // 数组尾部加入元素0 - st.push(0); - int result = 0; - for (int i = 1; i < heights.size(); i++) { - while (heights[i] < heights[st.top()]) { - int mid = st.top(); - st.pop(); - int w = i - st.top() - 1; - int h = heights[mid]; - result = max(result, w * h); - } - st.push(i); - } - return result; - } -}; -``` diff --git a/problems/0090.子集II.md b/problems/0090.子集II.md deleted file mode 100644 index c8e29763..00000000 --- a/problems/0090.子集II.md +++ /dev/null @@ -1,132 +0,0 @@ - -> 子集问题加去重! - -# 第90题.子集II - -题目链接:https://leetcode-cn.com/problems/subsets-ii/ - -给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。 - -说明:解集不能包含重复的子集。 - -示例: -输入: [1,2,2] -输出: -[ - [2], - [1], - [1,2,2], - [2,2], - [1,2], - [] -] - - -# 思路 - -做本题之前一定要先做[78.子集](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA)。 - -这道题目和[回溯算法:求子集问题!](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA)区别就是集合里有重复元素了,而且求取的子集要去重。 - -那么关于回溯算法中的去重问题,**在[40.组合总和II](https://mp.weixin.qq.com/s/_1zPYk70NvHsdY8UWVGXmQ)中已经详细讲解过了,和本题是一个套路**。 - -**剧透一下,后期要讲解的排列问题里去重也是这个套路,所以理解“树层去重”和“树枝去重”非常重要**。 - -用示例中的[1, 2, 2] 来举例,如图所示: (**注意去重需要先对集合排序**) - -![90.子集II](https://img-blog.csdnimg.cn/20201124195411977.png) - -从图中可以看出,同一树层上重复取2 就要过滤掉,同一树枝上就可以重复取2,因为同一树枝上元素的集合才是唯一子集! - -本题就是其实就是[回溯算法:求子集问题!](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA)的基础上加上了去重,去重我们在[回溯算法:求组合总和(三)](https://mp.weixin.qq.com/s/_1zPYk70NvHsdY8UWVGXmQ)也讲过了,所以我就直接给出代码了: - -# C++代码 - -``` -class Solution { -private: - vector> result; - vector path; - void backtracking(vector& nums, int startIndex, vector& used) { - result.push_back(path); - for (int i = startIndex; i < nums.size(); i++) { - // used[i - 1] == true,说明同一树支candidates[i - 1]使用过 - // used[i - 1] == false,说明同一树层candidates[i - 1]使用过 - // 而我们要对同一树层使用过的元素进行跳过 - if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) { - continue; - } - path.push_back(nums[i]); - used[i] = true; - backtracking(nums, i + 1, used); - used[i] = false; - path.pop_back(); - } - } - -public: - vector> subsetsWithDup(vector& nums) { - result.clear(); - path.clear(); - vector used(nums.size(), false); - sort(nums.begin(), nums.end()); // 去重需要排序 - backtracking(nums, 0, used); - return result; - } -}; - -``` - -使用set去重的版本。 -``` -class Solution { -private: - vector> result; - vector path; - void backtracking(vector& nums, int startIndex, vector& used) { - result.push_back(path); - unordered_set uset; - for (int i = startIndex; i < nums.size(); i++) { - if (uset.find(nums[i]) != uset.end()) { - continue; - } - uset.insert(nums[i]); - path.push_back(nums[i]); - backtracking(nums, i + 1, used); - path.pop_back(); - } - } - -public: - vector> subsetsWithDup(vector& nums) { - result.clear(); - path.clear(); - vector used(nums.size(), false); - sort(nums.begin(), nums.end()); // 去重需要排序 - backtracking(nums, 0, used); - return result; - } -}; - -``` - -# 总结 - -其实这道题目的知识点,我们之前都讲过了,如果之前讲过的子集问题和去重问题都掌握的好,这道题目应该分分钟AC。 - -当然本题去重的逻辑,也可以这么写 - -``` -if (i > startIndex && nums[i] == nums[i - 1] ) { - continue; -} -``` - -**就酱,如果感觉融会贯通了,就把「代码随想录」介绍给自己的同学朋友吧,也许他们也需要!** - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20201124161234338.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** - - - diff --git a/problems/0093.复原IP地址.md b/problems/0093.复原IP地址.md deleted file mode 100644 index e691852e..00000000 --- a/problems/0093.复原IP地址.md +++ /dev/null @@ -1,253 +0,0 @@ -> 一些录友表示跟不上现在的节奏,想从头开始打卡学习起来,可以在公众号左下方,「算法汇总」可以找到历史文章,都是按系列排好顺序的,挨个看就可以了,看文章下的留言你就会发现,有很多录友都在从头打卡,你并不孤单! - -# 93.复原IP地址 - -题目地址:https://leetcode-cn.com/problems/restore-ip-addresses/ - -给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。 - -有效的 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。 - -例如:"0.1.2.201" 和 "192.168.1.1" 是 有效的 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "192.168@1.1" 是 无效的 IP 地址。 - -示例 1: -输入:s = "25525511135" -输出:["255.255.11.135","255.255.111.35"] - -示例 2: -输入:s = "0000" -输出:["0.0.0.0"] - -示例 3: -输入:s = "1111" -输出:["1.1.1.1"] - -示例 4: -输入:s = "010010" -输出:["0.10.0.10","0.100.1.0"] - -示例 5: -输入:s = "101023" -输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"] - -提示: -0 <= s.length <= 3000 -s 仅由数字组成 - - -# 思路 - -做这道题目之前,最好先把[回溯算法:分割回文串](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q)这个做了。 - -这道题目相信大家刚看的时候,应该会一脸茫然。 - -其实只要意识到这是切割问题,**切割问题就可以使用回溯搜索法把所有可能性搜出来**,和刚做过的[回溯算法:分割回文串](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q)就十分类似了。 - -切割问题可以抽象为树型结构,如图: - -![93.复原IP地址](https://img-blog.csdnimg.cn/20201123203735933.png) - - -## 回溯三部曲 - -* 递归参数 - -在[回溯算法:分割回文串](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q)中我们就提到切割问题类似组合问题。 - -startIndex一定是需要的,因为不能重复分割,记录下一层递归分割的起始位置。 - -本题我们还需要一个变量pointNum,记录添加逗点的数量。 - -所以代码如下: - -``` - vector result;// 记录结果 - // startIndex: 搜索的起始位置,pointNum:添加逗点的数量 - void backtracking(string& s, int startIndex, int pointNum) { -``` - -* 递归终止条件 - -终止条件和[回溯算法:分割回文串](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q)情况就不同了,本题明确要求只会分成4段,所以不能用切割线切到最后作为终止条件,而是分割的段数作为终止条件。 - -pointNum表示逗点数量,pointNum为3说明字符串分成了4段了。 - -然后验证一下第四段是否合法,如果合法就加入到结果集里 - -代码如下: - -``` -if (pointNum == 3) { // 逗点数量为3时,分隔结束 - // 判断第四段子字符串是否合法,如果合法就放进result中 - if (isValid(s, startIndex, s.size() - 1)) { - result.push_back(s); - } - return; -} -``` - -* 单层搜索的逻辑 - -在[回溯算法:分割回文串](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q)中已经讲过在循环遍历中如何截取子串。 - -在`for (int i = startIndex; i < s.size(); i++)`循环中 [startIndex, i]这个区间就是截取的子串,需要判断这个子串是否合法。 - -如果合法就在字符串后面加上符号`.`表示已经分割。 - -如果不合法就结束本层循环,如图中剪掉的分支: - -![93.复原IP地址](https://img-blog.csdnimg.cn/20201123203735933.png) - -然后就是递归和回溯的过程: - -递归调用时,下一层递归的startIndex要从i+2开始(因为需要在字符串中加入了分隔符`.`),同时记录分割符的数量pointNum 要 +1。 - -回溯的时候,就将刚刚加入的分隔符`.` 删掉就可以了,pointNum也要-1。 - -代码如下: - -``` -for (int i = startIndex; i < s.size(); i++) { - if (isValid(s, startIndex, i)) { // 判断 [startIndex,i] 这个区间的子串是否合法 - s.insert(s.begin() + i + 1 , '.'); // 在i的后面插入一个逗点 - pointNum++; - backtracking(s, i + 2, pointNum); // 插入逗点之后下一个子串的起始位置为i+2 - pointNum--; // 回溯 - s.erase(s.begin() + i + 1); // 回溯删掉逗点 - } else break; // 不合法,直接结束本层循环 -} -``` - -## 判断子串是否合法 - -最后就是在写一个判断段位是否是有效段位了。 - -主要考虑到如下三点: - -* 段位以0为开头的数字不合法 -* 段位里有非正整数字符不合法 -* 段位如果大于255了不合法 - -代码如下: - -``` -// 判断字符串s在左闭又闭区间[start, end]所组成的数字是否合法 -bool isValid(const string& s, int start, int end) { - if (start > end) { - return false; - } - if (s[start] == '0' && start != end) { // 0开头的数字不合法 - return false; - } - int num = 0; - for (int i = start; i <= end; i++) { - if (s[i] > '9' || s[i] < '0') { // 遇到非数字字符不合法 - return false; - } - num = num * 10 + (s[i] - '0'); - if (num > 255) { // 如果大于255了不合法 - return false; - } - } - return true; -} -``` - -## C++代码 - - -根据[关于回溯算法,你该了解这些!](https://mp.weixin.qq.com/s/gjSgJbNbd1eAA5WkA-HeWw)给出的回溯算法模板: - -``` -void backtracking(参数) { - if (终止条件) { - 存放结果; - return; - } - - for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { - 处理节点; - backtracking(路径,选择列表); // 递归 - 回溯,撤销处理结果 - } -} -``` - -可以写出如下回溯算法C++代码: - -``` -class Solution { -private: - vector result;// 记录结果 - // startIndex: 搜索的起始位置,pointNum:添加逗点的数量 - void backtracking(string& s, int startIndex, int pointNum) { - if (pointNum == 3) { // 逗点数量为3时,分隔结束 - // 判断第四段子字符串是否合法,如果合法就放进result中 - if (isValid(s, startIndex, s.size() - 1)) { - result.push_back(s); - } - return; - } - for (int i = startIndex; i < s.size(); i++) { - if (isValid(s, startIndex, i)) { // 判断 [startIndex,i] 这个区间的子串是否合法 - s.insert(s.begin() + i + 1 , '.'); // 在i的后面插入一个逗点 - pointNum++; - backtracking(s, i + 2, pointNum); // 插入逗点之后下一个子串的起始位置为i+2 - pointNum--; // 回溯 - s.erase(s.begin() + i + 1); // 回溯删掉逗点 - } else break; // 不合法,直接结束本层循环 - } - } - // 判断字符串s在左闭又闭区间[start, end]所组成的数字是否合法 - bool isValid(const string& s, int start, int end) { - if (start > end) { - return false; - } - if (s[start] == '0' && start != end) { // 0开头的数字不合法 - return false; - } - int num = 0; - for (int i = start; i <= end; i++) { - if (s[i] > '9' || s[i] < '0') { // 遇到非数字字符不合法 - return false; - } - num = num * 10 + (s[i] - '0'); - if (num > 255) { // 如果大于255了不合法 - return false; - } - } - return true; - } -public: - vector restoreIpAddresses(string s) { - result.clear(); - if (s.size() > 12) return result; // 算是剪枝了 - backtracking(s, 0, 0); - return result; - } -}; - -``` - -# 总结 - -在[回溯算法:分割回文串](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q)中我列举的分割字符串的难点,本题都覆盖了。 - -而且本题还需要操作字符串添加逗号作为分隔符,并验证区间的合法性。 - -可以说是[回溯算法:分割回文串](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q)的加强版。 - -在本文的树形结构图中,我已经把详细的分析思路都画了出来,相信大家看了之后一定会思路清晰不少! - -**就酱,「代码随想录」值得推荐给你的朋友们!** - - -一些录友表示跟不上现在的节奏,想从头开始打卡学习起来,可以在在公众号左下方,「算法汇总」可以找到历史文章,都是按系列排好顺序的,挨个看就可以了,别忘了打卡。 - -**很多录友都在从头开始打卡学习,看看前面文章的留言区就知道了,你并不孤单!** - - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/0094.二叉树的中序遍历.md b/problems/0094.二叉树的中序遍历.md deleted file mode 100644 index e63819c1..00000000 --- a/problems/0094.二叉树的中序遍历.md +++ /dev/null @@ -1,84 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/binary-tree-inorder-traversal/ - -## 思路 - -详细题解请看这篇:[一文学通二叉树前中后序递归法与迭代法](https://github.com/youngyangyang04/leetcode/blob/master/problems/0144.二叉树的前序遍历.md) - -## C++代码 - -### 递归 -``` -class Solution { -public: - void traversal(TreeNode* cur, vector& vec) { - if (cur == NULL) return; - traversal(cur->left, vec); - vec.push_back(cur->val); - traversal(cur->right, vec); - } - vector inorderTraversal(TreeNode* root) { - vector result; - traversal(root, result); - return 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; - } -}; -``` - -### 栈 通用模板 - -``` -class Solution { -public: - 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; - } -}; -``` - - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0098.验证二叉搜索树.md b/problems/0098.验证二叉搜索树.md deleted file mode 100644 index ac827324..00000000 --- a/problems/0098.验证二叉搜索树.md +++ /dev/null @@ -1,246 +0,0 @@ -## 题目地址 - -> 学习完二叉搜索树的特性了,那么就验证一波 - -# 98.验证二叉搜索树 - -给定一个二叉树,判断其是否是一个有效的二叉搜索树。 - -假设一个二叉搜索树具有如下特征: - -* 节点的左子树只包含小于当前节点的数。 -* 节点的右子树只包含大于当前节点的数。 -* 所有左子树和右子树自身必须也是二叉搜索树。 - - - -# 思路 - -要知道中序遍历下,输出的二叉搜索树节点的数值是有序序列。 - -有了这个特性,**验证二叉搜索树,就相当于变成了判断一个序列是不是递增的了。** - -## 递归法 - -可以递归中序遍历将二叉搜索树转变成一个数组,代码如下: - -``` -vector vec; -void traversal(TreeNode* root) { - if (root == NULL) return; - traversal(root->left); - vec.push_back(root->val); // 将二叉搜索树转换为有序数组 - traversal(root->right); -} -``` - -然后只要比较一下,这个数组是否是有序的,**注意二叉搜索树中不能有重复元素**。 - -``` -traversal(root); -for (int i = 1; i < vec.size(); i++) { - // 注意要小于等于,搜索树里不能有相同元素 - if (vec[i] <= vec[i - 1]) return false; -} -return true; -``` - -整体代码如下: - -``` -class Solution { -private: - vector vec; - void traversal(TreeNode* root) { - if (root == NULL) return; - traversal(root->left); - vec.push_back(root->val); // 将二叉搜索树转换为有序数组 - traversal(root->right); - } -public: - bool isValidBST(TreeNode* root) { - vec.clear(); // 不加这句在leetcode上也可以过,但最好加上 - traversal(root); - for (int i = 1; i < vec.size(); i++) { - // 注意要小于等于,搜索树里不能有相同元素 - if (vec[i] <= vec[i - 1]) return false; - } - return true; - } -}; -``` - -以上代码中,我们把二叉树转变为数组来判断,是最直观的,但其实不用转变成数组,可以在递归遍历的过程中直接判断是否有序。 - - -这道题目比较容易陷入两个陷阱: - -* 陷阱1 - -**不能单纯的比较左节点小于中间节点,右节点大于中间节点就完事了**。 - -写出了类似这样的代码: - -``` -if (root->val > root->left->val && root->val < root->right->val) { - return true; -} else { - return false; -} -``` - -**我们要比较的是 左子树所有节点小于中间节点,右子树所有节点大于中间节点。**所以以上代码的判断逻辑是错误的。 - -例如: [10,5,15,null,null,6,20] 这个case: - -![二叉搜索树](https://img-blog.csdnimg.cn/20200812191501419.png) - -节点10小于左节点5,大于右节点15,但右子树里出现了一个6 这就不符合了! - -* 陷阱2 - -样例中最小节点 可能是int的最小值,如果这样使用最小的int来比较也是不行的。 - -此时可以初始化比较元素为longlong的最小值。 - -问题可以进一步演进:如果样例中根节点的val 可能是longlong的最小值 又要怎么办呢?文中会解答。 - -了解这些陷阱之后我们来看一下代码应该怎么写: - -递归三部曲: - -* 确定递归函数,返回值以及参数 - -要定义一个longlong的全局变量,用来比较遍历的节点是否有序,因为后台测试数据中有int最小值,所以定义为longlong的类型,初始化为longlong最小值。 - -注意递归函数要有bool类型的返回值, 我们在[二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?](https://mp.weixin.qq.com/s/6TWAVjxQ34kVqROWgcRFOg) 中讲了,只有寻找某一条边(或者一个节点)的时候,递归函数会有bool类型的返回值。 - -其实本题是同样的道理,我们在寻找一个不符合条件的节点,如果没有找到这个节点就遍历了整个树,如果找到不符合的节点了,立刻返回。 - -代码如下: - -``` -long long maxVal = LONG_MIN; // 因为后台测试数据中有int最小值 -bool isValidBST(TreeNode* root) -``` - -* 确定终止条件 - -如果是空节点 是不是二叉搜索树呢? - -是的,二叉搜索树也可以为空! - -代码如下: - -``` -if (root == NULL) return true; -``` - -* 确定单层递归的逻辑 - -中序遍历,一直更新maxVal,一旦发现maxVal >= root->val,就返回false,注意元素相同时候也要返回false。 - -代码如下: - -``` -bool left = isValidBST(root->left); // 左 - -// 中序遍历,验证遍历的元素是不是从小到大 -if (maxVal < root->val) maxVal = root->val; // 中 -else return false; - -bool right = isValidBST(root->right); // 右 -return left && right; -``` - -整体代码如下: -``` -class Solution { -public: - long long maxVal = LONG_MIN; // 因为后台测试数据中有int最小值 - bool isValidBST(TreeNode* root) { - if (root == NULL) return true; - - bool left = isValidBST(root->left); - // 中序遍历,验证遍历的元素是不是从小到大 - if (maxVal < root->val) maxVal = root->val; - else return false; - bool right = isValidBST(root->right); - - return left && right; - } -}; -``` - -以上代码是因为后台数据有int最小值测试用例,所以都把maxVal改成了longlong最小值。 - -如果测试数据中有 longlong的最小值,怎么办? - -不可能在初始化一个更小的值了吧。 建议避免 初始化最小值,如下方法取到最左面节点的数值来比较。 - -代码如下: - -``` -class Solution { -public: - TreeNode* pre = NULL; // 用来记录前一个节点 - bool isValidBST(TreeNode* root) { - if (root == NULL) return true; - bool left = isValidBST(root->left); - - if (pre != NULL && pre->val >= root->val) return false; - pre = root; // 记录前一个节点 - - bool right = isValidBST(root->right); - return left && right; - } -}; -``` - -最后这份代码看上去整洁一些,思路也清晰。 - -## 迭代法 - -可以用迭代法模拟二叉树中序遍历,对前中后序迭代法生疏的同学可以看这两篇[二叉树:听说递归能做的,栈也能做!](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg),[二叉树:前中后序迭代方式统一写法](https://mp.weixin.qq.com/s/WKg0Ty1_3SZkztpHubZPRg) - -迭代法中序遍历稍加改动就可以了,代码如下: - -``` -class Solution { -public: - bool isValidBST(TreeNode* root) { - stack st; - TreeNode* cur = root; - TreeNode* pre = NULL; // 记录前一个节点 - while (cur != NULL || !st.empty()) { - if (cur != NULL) { - st.push(cur); - cur = cur->left; // 左 - } else { - cur = st.top(); // 中 - st.pop(); - if (pre != NULL && cur->val <= pre->val) - return false; - pre = cur; //保存前一个访问的结点 - - cur = cur->right; // 右 - } - } - return true; - } -}; -``` - -在[二叉树:二叉搜索树登场!](https://mp.weixin.qq.com/s/vsKrWRlETxCVsiRr8v_hHg)中我们分明写出了痛哭流涕的简洁迭代法,怎么在这里不行了呢,因为本题是要验证二叉搜索树啊。 - -# 总结 - -这道题目是一个简单题,但对于没接触过的同学还是有难度的。 - -所以初学者刚开始学习算法的时候,看到简单题目没有思路很正常,千万别怀疑自己智商,学习过程都是这样的,大家智商都差不多,哈哈。 - -只要把基本类型的题目都做过,总结过之后,思路自然就开阔了。 - -**就酱,学到了的话,就转发给身边需要的同学吧!** - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0100.相同的树.md b/problems/0100.相同的树.md deleted file mode 100644 index ac495f51..00000000 --- a/problems/0100.相同的树.md +++ /dev/null @@ -1,163 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/same-tree/ - - -# 100. 相同的树 - -给定两个二叉树,编写一个函数来检验它们是否相同。 - -如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。 - - - -# 思路 - -在[二叉树:我对称么?](https://mp.weixin.qq.com/s/Kgf0gjvlDlNDfKIH2b1Oxg)中,我们讲到对于二叉树是否对称,要比较的是根节点的左子树与右子树是不是相互翻转的,理解这一点就知道了**其实我们要比较的是两个树(这两个树是根节点的左右子树)**,所以在递归遍历的过程中,也是要同时遍历两棵树。 - -理解这一本质之后,就会发现,求二叉树是否对称,和求二叉树是否相同几乎是同一道题目。 - -**如果没有读过[二叉树:我对称么?](https://mp.weixin.qq.com/s/Kgf0gjvlDlNDfKIH2b1Oxg)这一篇,请认真读完再做这道题,就会有感觉了。** - -递归三部曲中: - -1. 确定递归函数的参数和返回值 - -我们要比较的是两个树是否是相互相同的,参数也就是两个树的根节点。 - -返回值自然是bool类型。 - -代码如下: -``` -bool compare(TreeNode* tree1, TreeNode* tree2) -``` - -分析过程同[二叉树:我对称么?](https://mp.weixin.qq.com/s/Kgf0gjvlDlNDfKIH2b1Oxg)。 - -2. 确定终止条件 - -**要比较两个节点数值相不相同,首先要把两个节点为空的情况弄清楚!否则后面比较数值的时候就会操作空指针了。** - -节点为空的情况有: - -* tree1为空,tree2不为空,不对称,return false -* tree1不为空,tree2为空,不对称 return false -* tree1,tree2都为空,对称,返回true - -此时已经排除掉了节点为空的情况,那么剩下的就是tree1和tree2不为空的时候: - -* tree1、tree2都不为空,比较节点数值,不相同就return false - -此时tree1、tree2节点不为空,且数值也不相同的情况我们也处理了。 - -代码如下: -``` -if (tree1 == NULL && tree2 != NULL) return false; -else if (tree1 != NULL && tree2 == NULL) return false; -else if (tree1 == NULL && tree2 == NULL) return true; -else if (tree1->val != tree2->val) return false; // 注意这里我没有使用else -``` - -分析过程同[二叉树:我对称么?](https://mp.weixin.qq.com/s/Kgf0gjvlDlNDfKIH2b1Oxg) - -3. 确定单层递归的逻辑 - -* 比较二叉树是否相同 :传入的是tree1的左孩子,tree2的右孩子。 -* 如果左右都相同就返回true ,有一侧不相同就返回false 。 - -代码如下: - -``` -bool left = compare(tree1->left, tree2->left); // 左子树:左、 右子树:左 -bool right = compare(tree1->right, tree2->right); // 左子树:右、 右子树:右 -bool isSame = left && right; // 左子树:中、 右子树:中(逻辑处理) -return isSame; -``` -最后递归的C++整体代码如下: - -``` -class Solution { -public: - bool compare(TreeNode* tree1, TreeNode* tree2) { - if (tree1 == NULL && tree2 != NULL) return false; - else if (tree1 != NULL && tree2 == NULL) return false; - else if (tree1 == NULL && tree2 == NULL) return true; - else if (tree1->val != tree2->val) return false; // 注意这里我没有使用else - - // 此时就是:左右节点都不为空,且数值相同的情况 - // 此时才做递归,做下一层的判断 - bool left = compare(tree1->left, tree2->left); // 左子树:左、 右子树:左 - bool right = compare(tree1->right, tree2->right); // 左子树:右、 右子树:右 - bool isSame = left && right; // 左子树:中、 右子树:中(逻辑处理) - return isSame; - - } - bool isSameTree(TreeNode* p, TreeNode* q) { - return compare(p, q); - } -}; -``` - - -**我给出的代码并不简洁,但是把每一步判断的逻辑都清楚的描绘出来了。** - -如果上来就看网上各种简洁的代码,看起来真的很简单,但是很多逻辑都掩盖掉了,而题解可能也没有把掩盖掉的逻辑说清楚。 - -**盲目的照着抄,结果就是:发现这是一道“简单题”,稀里糊涂的就过了,但是真正的每一步判断逻辑未必想到清楚。** - -当然我可以把如上代码整理如下: - -## 递归 - -``` -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->left) && compare(left->right, right->right); - - } - bool isSameTree(TreeNode* p, TreeNode* q) { - return compare(p, q); - } -}; -``` - -## 迭代法 - -``` -lass Solution { -public: - - bool isSameTree(TreeNode* p, TreeNode* q) { - if (p == NULL && q == NULL) return true; - if (p == NULL || q == NULL) return false; - queue que; - que.push(p); // - que.push(q); // - 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->left); // - que.push(leftNode->right); // - que.push(rightNode->right); // - } - return true; - } -}; -``` - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20201124161234338.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/0101.对称二叉树.md b/problems/0101.对称二叉树.md deleted file mode 100644 index b454aa7d..00000000 --- a/problems/0101.对称二叉树.md +++ /dev/null @@ -1,246 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/symmetric-tree/ - -> 又是一道“简单题” - -# 101. 对称二叉树 - -给定一个二叉树,检查它是否是镜像对称的。 - - - -# 思路 - -**首先想清楚,判断对称二叉树要比较的是哪两个节点,要比较的可不是左右节点!** - -对于二叉树是否对称,要比较的是根节点的左子树与右子树是不是相互翻转的,理解这一点就知道了**其实我们要比较的是两个树(这两个树是根节点的左右子树)**,所以在递归遍历的过程中,也是要同时遍历两棵树。 - -那么如果比较呢? - -比较的是两个子树的里侧和外侧的元素是否相等。如图所示: - - - -那么遍历的顺序应该是什么样的呢? - -本题遍历只能是“后序遍历”,因为我们要通过递归函数的返回值来判断两个子树的内侧节点和外侧节点是否相等。 - -**正是因为要遍历两棵树而且要比较内侧和外侧节点,所以准确的来说是一个树的遍历顺序是左右中,一个树的遍历顺序是右左中。** - -但都可以理解算是后序遍历,尽管已经不是严格上在一个树上进行遍历的后序遍历了。 - -其实后序也可以理解为是一种回溯,当然这是题外话,讲回溯的时候会重点讲的。 - -说到这大家可能感觉我有点啰嗦,哪有这么多道理,上来就干就完事了。别急,我说的这些在下面的代码讲解中都有身影。 - -那么我们先来看看递归法的代码应该怎么写。 - -## 递归法 - -### 递归三部曲 - -1. 确定递归函数的参数和返回值 - -因为我们要比较的是根节点的两个子树是否是相互翻转的,进而判断这个树是不是对称树,所以要比较的是两个树,参数自然也是左子树节点和右子树节点。 - -返回值自然是bool类型。 - -代码如下: -``` -bool compare(TreeNode* left, TreeNode* right) -``` - -2. 确定终止条件 - -要比较两个节点数值相不相同,首先要把两个节点为空的情况弄清楚!否则后面比较数值的时候就会操作空指针了。 - -节点为空的情况有:(**注意我们比较的其实不是左孩子和右孩子,所以如下我称之为左节点右节点**) - -* 左节点为空,右节点不为空,不对称,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; // 注意这里我没有使用else -``` - -注意上面最后一种情况,我没有使用else,而是elseif, 因为我们把以上情况都排除之后,剩下的就是 左右节点都不为空,且数值相同的情况。 - -3. 确定单层递归的逻辑 - -此时才进入单层递归的逻辑,单层递归的逻辑就是处理 右节点都不为空,且数值相同的情况。 - - -* 比较二叉树外侧是否对称:传入的是左节点的左孩子,右节点的右孩子。 -* 比较内测是否对称,传入左节点的右孩子,右节点的左孩子。 -* 如果左右都对称就返回true ,有一侧不对称就返回false 。 - -代码如下: - -``` -bool outside = compare(left->left, right->right); // 左子树:左、 右子树:右 -bool inside = compare(left->right, right->left); // 左子树:右、 右子树:左 -bool isSame = outside && inside; // 左子树:中、 右子树:中(逻辑处理) -return isSame; -``` - -如上代码中,我们可以看出使用的遍历方式,左子树左右中,右子树右左中,所以我把这个遍历顺序也称之为“后序遍历”(尽管不是严格的后序遍历)。 - -最后递归的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; - - // 此时就是:左右节点都不为空,且数值相同的情况 - // 此时才做递归,做下一层的判断 - bool outside = compare(left->left, right->right); // 左子树:左、 右子树:右 - bool inside = compare(left->right, right->left); // 左子树:右、 右子树:左 - bool isSame = outside && inside; // 左子树:中、 右子树:中 (逻辑处理) - return isSame; - - } - bool isSymmetric(TreeNode* root) { - if (root == NULL) return true; - return compare(root->left, root->right); - } -}; -``` - -**我给出的代码并不简洁,但是把每一步判断的逻辑都清楚的描绘出来了。** - -如果上来就看网上各种简洁的代码,看起来真的很简单,但是很多逻辑都掩盖掉了,而题解可能也没有把掩盖掉的逻辑说清楚。 - -**盲目的照着抄,结果就是:发现这是一道“简单题”,稀里糊涂的就过了,但是真正的每一步判断逻辑未必想到清楚。** - -当然我可以把如上代码整理如下: -``` -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; - } - - // 左右一个节点不为空,或者都不为空但数值不相同,返回false - 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; - } -}; -``` - -### 使用栈 - -细心的话,其实可以发现,这个迭代法,其实是把左右两个子树要比较的元素顺序放进一个容器,然后成对成对的取出来进行比较,那么其实使用栈也是可以的。 - -只要把队列原封不动的改成栈就可以了,我下面也给出了代码。 - -``` -class Solution { -public: - bool isSymmetric(TreeNode* root) { - if (root == NULL) return true; - stack st; // 这里改成了栈 - st.push(root->left); - st.push(root->right); - while (!st.empty()) { - TreeNode* leftNode = st.top(); st.pop(); - TreeNode* rightNode = st.top(); st.pop(); - if (!leftNode && !rightNode) { - continue; - } - if ((!leftNode || !rightNode || (leftNode->val != rightNode->val))) { - return false; - } - st.push(leftNode->left); - st.push(rightNode->right); - st.push(leftNode->right); - st.push(rightNode->left); - } - return true; - } -}; -``` - -# 总结 - -这次我们又深度剖析了一道二叉树的“简单题”,大家会发现,真正的把题目搞清楚其实并不简单,leetcode上accept了和真正掌握了还是有距离的。 - -我们介绍了递归法和迭代法,递归依然通过递归三部曲来解决了这道题目,如果只看精简的代码根本看不出来递归三部曲是如果解题的。 - -在迭代法中我们使用了队列,需要注意的是这不是层序遍历,而且仅仅通过一个容器来成对的存放我们要比较的元素,知道这一本质之后就发现,用队列,用栈,甚至用数组,都是可以的。 - -如果已经做过这道题目的同学,读完文章可以再去看看这道题目,思考一下,会有不一样的发现! - - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0102.二叉树的层序遍历.md b/problems/0102.二叉树的层序遍历.md deleted file mode 100644 index 7b404623..00000000 --- a/problems/0102.二叉树的层序遍历.md +++ /dev/null @@ -1,393 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/binary-tree-level-order-traversal/ - -> 我要打十个! - -看完这篇文章虽然不能打十个,但是可以迅速打八个!而且够快! - -学会二叉树的层序遍历,可以一口气撸完leetcode上八道题目: - -* 102.二叉树的层序遍历 -* 107.二叉树的层次遍历II -* 199.二叉树的右视图 -* 637.二叉树的层平均值 -* 429.N叉树的前序遍历 -* 515.在每个树行中找最大值 -* 116. 填充每个节点的下一个右侧节点指针 -* 117.填充每个节点的下一个右侧节点指针II - - -# 102.二叉树的层序遍历 - -给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。 - - - -## 思路 - -我们之前讲过了三篇关于二叉树的深度优先遍历的文章: - -* [二叉树:前中后序递归法](https://mp.weixin.qq.com/s/PwVIfxDlT3kRgMASWAMGhA) -* [二叉树:前中后序迭代法](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg) -* [二叉树:前中后序迭代方式统一写法](https://mp.weixin.qq.com/s/WKg0Ty1_3SZkztpHubZPRg) - -接下来我们再来介绍二叉树的另一种遍历方式:层序遍历。 - -层序遍历一个二叉树。就是从左到右一层一层的去遍历二叉树。这种遍历的方式和我们之前讲过的都不太一样。 - -需要借用一个辅助数据结构即队列来实现,**队列先进先出,符合一层一层遍历的逻辑,而是用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。** - -**而这种层序遍历方式就是图论中的广度优先遍历,只不过我们应用在二叉树上。** - -使用队列实现二叉树广度优先遍历,动画如下: - -![102二叉树的层序遍历.mp4](ad3d58a5-b8ee-42a5-bc89-6ad4d9e3cbf2) - - -这样就实现了层序从左到右遍历二叉树。 - -代码如下:**这份代码也可以作为二叉树层序遍历的模板,以后再打七个就靠它了**。 - -## C++代码 - -``` -class Solution { -public: - vector> levelOrder(TreeNode* root) { - queue que; - if (root != NULL) que.push(root); - vector> result; - while (!que.empty()) { - int size = que.size(); - vector vec; - // 这里一定要使用固定大小size,不要使用que.size(),因为que.size是不断变化的 - for (int i = 0; i < size; i++) { - 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上的题目,只需要修改模板的一两行代码(不能再多了),便可打倒!** - -# 107.二叉树的层次遍历 II - -给定一个二叉树,返回其节点值自底向上的层次遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历) - - - -## 思路 - -相对于102.二叉树的层序遍历,就是最后把result数组反转一下就可以了。 - -## C++代码 - -``` -class Solution { -public: - vector> levelOrderBottom(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++) { - 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); - } - reverse(result.begin(), result.end()); // 在这里反转一下数组即可 - return result; - - } -}; -``` - - -# 199.二叉树的右视图 - -给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。 - - - -## 思路 - -层序遍历的时候,判断是否遍历到单层的最后面的元素,如果是,就放进result数组中,随后返回result就可以了。 - -## C++代码 - -``` -class Solution { -public: - vector rightSideView(TreeNode* root) { - queue que; - if (root != NULL) que.push(root); - vector result; - while (!que.empty()) { - int size = que.size(); - for (int i = 0; i < size; i++) { - TreeNode* node = que.front(); - que.pop(); - if (i == (size - 1)) result.push_back(node->val); // 将每一层的最后元素放入result数组中 - if (node->left) que.push(node->left); - if (node->right) que.push(node->right); - } - } - return result; - } -}; -``` - -# 637.二叉树的层平均值 - -给定一个非空二叉树, 返回一个由每层节点平均值组成的数组。 - - - -## 思路 - -本题就是层序遍历的时候把一层求个总和在取一个均值。 - -## C++代码 - -``` -class Solution { -public: - vector averageOfLevels(TreeNode* root) { - queue que; - if (root != NULL) que.push(root); - vector result; - while (!que.empty()) { - int size = que.size(); - double sum = 0; // 统计每一层的和 - for (int i = 0; i < size; i++) { - TreeNode* node = que.front(); - que.pop(); - sum += node->val; - if (node->left) que.push(node->left); - if (node->right) que.push(node->right); - } - result.push_back(sum / size); // 将每一层均值放进结果集 - } - return result; - } -}; - -``` - -# 429.N叉树的层序遍历 - -给定一个 N 叉树,返回其节点值的层序遍历。 (即从左到右,逐层遍历)。 - -例如,给定一个 3叉树 : - - - - -返回其层序遍历: - -[ - [1], - [3,2,4], - [5,6] -] - - -## 思路 - -这道题依旧是模板题,只不过一个节点有多个孩子了 - -## C++代码 - -``` -class Solution { -public: - vector> levelOrder(Node* 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++) { - Node* node = que.front(); - que.pop(); - vec.push_back(node->val); - for (int i = 0; i < node->children.size(); i++) { // 将节点孩子加入队列 - if (node->children[i]) que.push(node->children[i]); - } - } - result.push_back(vec); - } - return result; - - } -}; -``` - -# 515.在每个树行中找最大值 - -您需要在二叉树的每一行中找到最大的值。 - - - -## 思路 - -层序遍历,取每一层的最大值 - -## C++代码 - -``` -class Solution { -public: - vector largestValues(TreeNode* root) { - queue que; - if (root != NULL) que.push(root); - vector result; - while (!que.empty()) { - int size = que.size(); - int maxValue = INT_MIN; // 取每一层的最大值 - for (int i = 0; i < size; i++) { - TreeNode* node = que.front(); - que.pop(); - maxValue = node->val > maxValue ? node->val : maxValue; - if (node->left) que.push(node->left); - if (node->right) que.push(node->right); - } - result.push_back(maxValue); // 把最大值放进数组 - } - return result; - } -}; -``` - -# 116.填充每个节点的下一个右侧节点指针 - -给定一个完美二叉树,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下: - -struct Node { - int val; - Node *left; - Node *right; - Node *next; -} -填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。 - -初始状态下,所有 next 指针都被设置为 NULL。 - - - -## 思路 - -本题依然是层序遍历,只不过在单层遍历的时候记录一下本层的头部节点,然后在遍历的时候让前一个节点指向本节点就可以了 - -## C++代码 - -``` -class Solution { -public: - Node* connect(Node* root) { - queue que; - if (root != NULL) que.push(root); - while (!que.empty()) { - int size = que.size(); - vector vec; - Node* nodePre; - Node* node; - for (int i = 0; i < size; i++) { - if (i == 0) { - nodePre = que.front(); // 取出一层的头结点 - que.pop(); - node = nodePre; - } else { - node = que.front(); - que.pop(); - nodePre->next = node; // 本层前一个节点next指向本节点 - nodePre = nodePre->next; - } - if (node->left) que.push(node->left); - if (node->right) que.push(node->right); - } - nodePre->next = NULL; // 本层最后一个节点指向NULL - } - return root; - - } -}; -``` - -# 117.填充每个节点的下一个右侧节点指针II - -## 思路 - -这道题目说是二叉树,但116题目说是完整二叉树,其实没有任何差别,一样的代码一样的逻辑一样的味道 - -## C++代码 - -``` -class Solution { -public: - Node* connect(Node* root) { - queue que; - if (root != NULL) que.push(root); - while (!que.empty()) { - int size = que.size(); - vector vec; - Node* nodePre; - Node* node; - for (int i = 0; i < size; i++) { - if (i == 0) { - nodePre = que.front(); // 取出一层的头结点 - que.pop(); - node = nodePre; - } else { - node = que.front(); - que.pop(); - nodePre->next = node; // 本层前一个节点next指向本节点 - nodePre = nodePre->next; - } - if (node->left) que.push(node->left); - if (node->right) que.push(node->right); - } - nodePre->next = NULL; // 本层最后一个节点指向NULL - } - return root; - } -}; -``` - - -# 总结 - -二叉树的层序遍历,就是图论中的广度优先搜索在二叉树中的应用,需要借助队列来实现(此时是不是又发现队列的应用了)。 - -虽然不能一口气打十个,打八个也还行。 - -* 102.二叉树的层序遍历 -* 107.二叉树的层次遍历II -* 199.二叉树的右视图 -* 637.二叉树的层平均值 -* 429.N叉树的前序遍历 -* 515.在每个树行中找最大值 -* 116. 填充每个节点的下一个右侧节点指针 -* 117.填充每个节点的下一个右侧节点指针II - -如果非要打十个,还得找叶师傅! - - - - - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0104.二叉树的最大深度.md b/problems/0104.二叉树的最大深度.md deleted file mode 100644 index 3aa5c4b6..00000000 --- a/problems/0104.二叉树的最大深度.md +++ /dev/null @@ -1,226 +0,0 @@ -(寻找更节点可以用unordered_map来优化一下,元素都是独一无二的) - -## 题目地址 -https://leetcode-cn.com/problems/maximum-depth-of-binary-tree/ - -> “简单题”系列 - -看完本篇可以一起做了如下两道题目: -* 104.二叉树的最大深度 -* 559.N叉树的最大深度 - -# 104.二叉树的最大深度 - -给定一个二叉树,找出其最大深度。 - -二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。 - -说明: 叶子节点是指没有子节点的节点。 - -示例: -给定二叉树 [3,9,20,null,null,15,7], - - - -返回它的最大深度 3 。 - -# 思路 - -## 递归法 - -本题其实也要后序遍历(左右中),依然是因为要通过递归函数的返回值做计算树的高度。 - -按照递归三部曲,来看看如何来写。 - -1. 确定递归函数的参数和返回值:参数就是传入树的根节点,返回就返回这棵树的深度,所以返回值为int类型。 - -代码如下: -``` -int getDepth(TreeNode* node) -``` - -2. 确定终止条件:如果为空节点的话,就返回0,表示高度为0。 - -代码如下: -``` -if (node == NULL) return 0; -``` - -3. 确定单层递归的逻辑:先求它的左子树的深度,再求的右子树的深度,最后取左右深度最大的数值 再+1 (加1是因为算上当前中间节点)就是目前节点为根节点的树的深度。 - -代码如下: - -``` -int leftDepth = getDepth(node->left); // 左 -int rightDepth = getDepth(node->right); // 右 -int depth = 1 + max(leftDepth, rightDepth); // 中 -return depth; -``` - -所以整体C++代码如下: - -``` -class Solution { -public: - int getDepth(TreeNode* node) { - if (node == NULL) return 0; - int leftDepth = getDepth(node->left); // 左 - int rightDepth = getDepth(node->right); // 右 - int depth = 1 + max(leftDepth, rightDepth); // 中 - return depth; - } - int maxDepth(TreeNode* root) { - return getDepth(root); - } -}; -``` - -代码精简之后C++代码如下: -``` -class Solution { -public: - int maxDepth(TreeNode* root) { - if (root == NULL) return 0; - return 1 + max(maxDepth(root->left), maxDepth(root->right)); - } -}; - -``` - -**精简之后的代码根本看不出是哪种遍历方式,也看不出递归三部曲的步骤,所以如果对二叉树的操作还不熟练,尽量不要直接照着精简代码来学。** - - -## 迭代法 - -使用迭代法的话,使用层序遍历是最为合适的,因为最大的深度就是二叉树的层数,和层序遍历的方式极其吻合。 - -在二叉树中,一层一层的来遍历二叉树,记录一下遍历的层数就是二叉树的深度,如图所示: - -![层序遍历](https://img-blog.csdnimg.cn/20200810193056585.png) - -所以这道题的迭代法就是一道模板题,可以使用二叉树层序遍历的模板来解决的。 - -如果对层序遍历还不清楚的话,可以看这篇:[二叉树:层序遍历登场!](https://mp.weixin.qq.com/s/Gb3BjakIKGNpup2jYtTzog) - -C++代码如下: - -``` -class Solution { -public: - int maxDepth(TreeNode* root) { - if (root == NULL) return 0; - int depth = 0; - queue que; - que.push(root); - while(!que.empty()) { - int size = que.size(); - depth++; // 记录深度 - for (int i = 0; i < size; i++) { - TreeNode* node = que.front(); - que.pop(); - if (node->left) que.push(node->left); - if (node->right) que.push(node->right); - } - } - return depth; - } -}; -``` - -那么我们可以顺便解决一下N叉树的最大深度问题 - -# 559.N叉树的最大深度 -https://leetcode-cn.com/problems/maximum-depth-of-n-ary-tree/ - -给定一个 N 叉树,找到其最大深度。 - -最大深度是指从根节点到最远叶子节点的最长路径上的节点总数。 - -例如,给定一个 3叉树 : - - - -我们应返回其最大深度,3。 - -# 思路 - -依然可以提供递归法和迭代法,来解决这个问题,思路是和二叉树思路一样的,直接给出代码如下: - -## 递归法 - -C++代码: - -``` -class Solution { -public: - int maxDepth(Node* root) { - if (root == 0) return 0; - int depth = 0; - for (int i = 0; i < root->children.size(); i++) { - depth = max (depth, maxDepth(root->children[i])); - } - return depth + 1; - } -}; -``` -## 迭代法 - -依然是层序遍历,代码如下: - -``` -class Solution { -public: - int maxDepth(Node* root) { - queue que; - if (root != NULL) que.push(root); - int depth = 0; - while (!que.empty()) { - int size = que.size(); - depth++; // 记录深度 - for (int i = 0; i < size; i++) { - Node* node = que.front(); - que.pop(); - for (int j = 0; j < node->children.size(); j++) { - if (node->children[j]) que.push(node->children[j]); - } - } - } - return depth; - } -}; -``` - -使用栈来模拟后序遍历依然可以 - -``` -class Solution { -public: - int maxDepth(TreeNode* root) { - stack st; - if (root != NULL) st.push(root); - int depth = 0; - int result = 0; - while (!st.empty()) { - TreeNode* node = st.top(); - if (node != NULL) { - st.pop(); - st.push(node); // 中 - st.push(NULL); - depth++; - if (node->right) st.push(node->right); // 右 - if (node->left) st.push(node->left); // 左 - - } else { - st.pop(); - node = st.top(); - st.pop(); - depth--; - } - result = result > depth ? result : depth; - } - return result; - - } -}; -``` -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0105.从前序与中序遍历序列构造二叉树.md b/problems/0105.从前序与中序遍历序列构造二叉树.md deleted file mode 100644 index eb39b313..00000000 --- a/problems/0105.从前序与中序遍历序列构造二叉树.md +++ /dev/null @@ -1,8 +0,0 @@ -# 链接 - -https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/ - -# 思路: - -详细见 -[0106.从中序与后序遍历序列构造二叉树](https://github.com/youngyangyang04/leetcode/blob/master/problems/0106.从中序与后序遍历序列构造二叉树.md) diff --git a/problems/0106.从中序与后序遍历序列构造二叉树.md b/problems/0106.从中序与后序遍历序列构造二叉树.md deleted file mode 100644 index 892f2167..00000000 --- a/problems/0106.从中序与后序遍历序列构造二叉树.md +++ /dev/null @@ -1,576 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/construct-binary-tree-from-inorder-and-postorder-traversal/ - -> 给出两个序列 (可以加unorder_map优化一下) - -看完本文,可以一起解决如下两道题目 - -* 106.从中序与后序遍历序列构造二叉树 -* 105.从前序与中序遍历序列构造二叉树 - - -# 106.从中序与后序遍历序列构造二叉树 - -根据一棵树的中序遍历与后序遍历构造二叉树。 - -注意: -你可以假设树中没有重复的元素。 - -例如,给出 - -中序遍历 inorder = [9,3,15,20,7] -后序遍历 postorder = [9,15,7,20,3] -返回如下的二叉树: - - - -## 思路 - -首先回忆一下如何根据两个顺序构造一个唯一的二叉树,相信理论知识大家应该都清楚,就是以 后序数组的最后一个元素为切割点,先切中序数组,根据中序数组,反过来在切后序数组。一层一层切下去,每次后序数组最后一个元素就是节点元素。 - -如果让我们肉眼看两个序列,画一颗二叉树的话,应该分分钟都可以画出来。 - -流程如图: - - - -那么代码应该怎么写呢? - -说到一层一层切割,就应该想到了递归。 - -来看一下一共分几步: - -* 第一步:如果数组大小为零的话,说明是空节点了。 - -* 第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。 - -* 第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点 - -* 第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组) - -* 第五步:切割后序数组,切成后序左数组和后序右数组 - -* 第六步:递归处理左区间和右区间 - -不难写出如下代码:(先把框架写出来) - -``` - TreeNode* traversal (vector& inorder, vector& postorder) { - - // 第一步 - if (postorder.size() == 0) return NULL; - - // 第二步:后序遍历数组最后一个元素,就是当前的中间节点 - int rootValue = postorder[postorder.size() - 1]; - TreeNode* root = new TreeNode(rootValue); - - // 叶子节点 - if (postorder.size() == 1) return root; - - // 第三步:找切割点 - int delimiterIndex; - for (delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++) { - if (inorder[delimiterIndex] == rootValue) break; - } - - // 第四步:切割中序数组,得到 中序左数组和中序右数组 - // 第五步:切割后序数组,得到 后序左数组和后序右数组 - - // 第六步 - root->left = traversal(中序左数组, 后序左数组); - root->right = traversal(中序右数组, 后序右数组); - - return root; - } -``` - -**难点大家应该发现了,就是如何切割,以及边界值找不好很容易乱套。** - -此时应该注意确定切割的标准,是左闭右开,还有左开又闭,还是左闭又闭,这个就是不变量,要在递归中保持这个不变量。 - -**在切割的过程中会产生四个区间,把握不好不变量的话,一会左闭右开,一会左闭又闭,必然乱套!** - -我在[数组:每次遇到二分法,都是一看就会,一写就废](https://mp.weixin.qq.com/s/fCf5QbPDtE6SSlZ1yh_q8Q)和[数组:这个循环可以转懵很多人!](https://mp.weixin.qq.com/s/KTPhaeqxbMK9CxHUUgFDmg)中都强调过循环不变量的重要性,在二分查找以及螺旋矩阵的求解中,坚持循环不变量非常重要,本题也是。 - - -首先要切割中序数组,为什么先切割中序数组呢? - -切割点在后序数组的最后一个元素,就是用这个元素来切割中序数组的,所以必要先切割中序数组。 - -中序数组相对比较好切,找到切割点(后序数组的最后一个元素)在中序数组的位置,然后切割,如下代码中我坚持左闭右开的原则: - - -``` -// 找到中序遍历的切割点 -int delimiterIndex; -for (delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++) { - if (inorder[delimiterIndex] == rootValue) break; -} - -// 左闭右开区间:[0, delimiterIndex) -vector leftInorder(inorder.begin(), inorder.begin() + delimiterIndex); -// [delimiterIndex + 1, end) -vector rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end() ); -``` - -接下来就要切割后序数组了。 - -首先后序数组的最后一个元素指定不能要了,这是切割点 也是 当前二叉树中间节点的元素,已经用了。 - -后序数组的切割点怎么找? - -后序数组没有明确的切割元素来进行左右切割,不像中序数组有明确的切割点,切割点左右分开就可以了。 - -**此时有一个很重的点,就是中序数组大小一定是和后序数组的大小相同的(这是必然)。** - -中序数组我们都切成了左中序数组和右中序数组了,那么后序数组就可以按照左中序数组的大小来切割,切成左后序数组和右后序数组。 - -代码如下: - -``` -// postorder 舍弃末尾元素,因为这个元素就是中间节点,已经用过了 -postorder.resize(postorder.size() - 1); - -// 左闭右开,注意这里使用了左中序数组大小作为切割点:[0, leftInorder.size) -vector leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size()); -// [leftInorder.size(), end) -vector rightPostorder(postorder.begin() + leftInorder.size(), postorder.end()); -``` - -此时,中序数组切成了左中序数组和右中序数组,后序数组切割成左后序数组和右后序数组。 - -接下来可以递归了,代码如下: - -``` -root->left = traversal(leftInorder, leftPostorder); -root->right = traversal(rightInorder, rightPostorder); -``` - -完整代码如下: - -### C++完整代码 - -``` -class Solution { -private: - TreeNode* traversal (vector& inorder, vector& postorder) { - if (postorder.size() == 0) return NULL; - - // 后序遍历数组最后一个元素,就是当前的中间节点 - int rootValue = postorder[postorder.size() - 1]; - TreeNode* root = new TreeNode(rootValue); - - // 叶子节点 - if (postorder.size() == 1) return root; - - // 找到中序遍历的切割点 - int delimiterIndex; - for (delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++) { - if (inorder[delimiterIndex] == rootValue) break; - } - - // 切割中序数组 - // 左闭右开区间:[0, delimiterIndex) - vector leftInorder(inorder.begin(), inorder.begin() + delimiterIndex); - // [delimiterIndex + 1, end) - vector rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end() ); - - // postorder 舍弃末尾元素 - postorder.resize(postorder.size() - 1); - - // 切割后序数组 - // 依然左闭右开,注意这里使用了左中序数组大小作为切割点 - // [0, leftInorder.size) - vector leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size()); - // [leftInorder.size(), end) - vector rightPostorder(postorder.begin() + leftInorder.size(), postorder.end()); - - root->left = traversal(leftInorder, leftPostorder); - root->right = traversal(rightInorder, rightPostorder); - - return root; - } -public: - TreeNode* buildTree(vector& inorder, vector& postorder) { - if (inorder.size() == 0 || postorder.size() == 0) return NULL; - return traversal(inorder, postorder); - } -}; - -``` - -相信大家自己就算是思路清晰, 代码写出来一定是各种问题,所以一定要加日志来调试,看看是不是按照自己思路来切割的,不要大脑模拟,那样越想越糊涂。 - -加了日志的代码如下:(加了日志的代码不要在leetcode上提交,容易超时) - - -``` -class Solution { -private: - TreeNode* traversal (vector& inorder, vector& postorder) { - if (postorder.size() == 0) return NULL; - - int rootValue = postorder[postorder.size() - 1]; - TreeNode* root = new TreeNode(rootValue); - - if (postorder.size() == 1) return root; - - int delimiterIndex; - for (delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++) { - if (inorder[delimiterIndex] == rootValue) break; - } - - vector leftInorder(inorder.begin(), inorder.begin() + delimiterIndex); - vector rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end() ); - - postorder.resize(postorder.size() - 1); - - vector leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size()); - vector rightPostorder(postorder.begin() + leftInorder.size(), postorder.end()); - - // 一下为日志 - cout << "----------" << endl; - - cout << "leftInorder :"; - for (int i : leftInorder) { - cout << i << " "; - } - cout << endl; - - cout << "rightInorder :"; - for (int i : rightInorder) { - cout << i << " "; - } - cout << endl; - - cout << "leftPostorder :"; - for (int i : leftPostorder) { - cout << i << " "; - } - cout << endl; - cout << "rightPostorder :"; - for (int i : rightPostorder) { - cout << i << " "; - } - cout << endl; - - root->left = traversal(leftInorder, leftPostorder); - root->right = traversal(rightInorder, rightPostorder); - - return root; - } -public: - TreeNode* buildTree(vector& inorder, vector& postorder) { - if (inorder.size() == 0 || postorder.size() == 0) return NULL; - return traversal(inorder, postorder); - } -}; -``` - -**此时应该发现了,如上的代码性能并不好,应为每层递归定定义了新的vector(就是数组),既耗时又耗空间,但上面的代码是最好理解的,为了方便读者理解,所以用如上的代码来讲解。** - -下面给出用下表索引写出的代码版本:(思路是一样的,只不过不用重复定义vector了,每次用下表索引来分割) - -### C++优化版本 -``` -class Solution { -private: - // 中序区间:[inorderBegin, inorderEnd),后序区间[postorderBegin, postorderEnd) - TreeNode* traversal (vector& inorder, int inorderBegin, int inorderEnd, vector& postorder, int postorderBegin, int postorderEnd) { - if (postorderBegin == postorderEnd) return NULL; - - int rootValue = postorder[postorderEnd - 1]; - TreeNode* root = new TreeNode(rootValue); - - if (postorderEnd - postorderBegin == 1) return root; - - int delimiterIndex; - for (delimiterIndex = inorderBegin; delimiterIndex < inorderEnd; delimiterIndex++) { - if (inorder[delimiterIndex] == rootValue) break; - } - // 切割中序数组 - // 左中序区间,左闭右开[leftInorderBegin, leftInorderEnd) - int leftInorderBegin = inorderBegin; - int leftInorderEnd = delimiterIndex; - // 右中序区间,左闭右开[rightInorderBegin, rightInorderEnd) - int rightInorderBegin = delimiterIndex + 1; - int rightInorderEnd = inorderEnd; - - // 切割后序数组 - // 左后序区间,左闭右开[leftPostorderBegin, leftPostorderEnd) - int leftPostorderBegin = postorderBegin; - int leftPostorderEnd = postorderBegin + delimiterIndex - inorderBegin; // 终止位置是 需要加上 中序区间的大小size - // 右后序区间,左闭右开[rightPostorderBegin, rightPostorderEnd) - int rightPostorderBegin = postorderBegin + (delimiterIndex - inorderBegin); - int rightPostorderEnd = postorderEnd - 1; // 排除最后一个元素,已经作为节点了 - - root->left = traversal(inorder, leftInorderBegin, leftInorderEnd, postorder, leftPostorderBegin, leftPostorderEnd); - root->right = traversal(inorder, rightInorderBegin, rightInorderEnd, postorder, rightPostorderBegin, rightPostorderEnd); - - return root; - } -public: - TreeNode* buildTree(vector& inorder, vector& postorder) { - if (inorder.size() == 0 || postorder.size() == 0) return NULL; - // 左闭右开的原则 - return traversal(inorder, 0, inorder.size(), postorder, 0, postorder.size()); - } -}; -``` - -那么这个版本写出来依然要打日志进行调试,打日志的版本如下:(**该版本不要在leetcode上提交,容易超时**) - -``` -class Solution { -private: - TreeNode* traversal (vector& inorder, int inorderBegin, int inorderEnd, vector& postorder, int postorderBegin, int postorderEnd) { - if (postorderBegin == postorderEnd) return NULL; - - int rootValue = postorder[postorderEnd - 1]; - TreeNode* root = new TreeNode(rootValue); - - if (postorderEnd - postorderBegin == 1) return root; - - int delimiterIndex; - for (delimiterIndex = inorderBegin; delimiterIndex < inorderEnd; delimiterIndex++) { - if (inorder[delimiterIndex] == rootValue) break; - } - // 切割中序数组 - // 左中序区间,左闭右开[leftInorderBegin, leftInorderEnd) - int leftInorderBegin = inorderBegin; - int leftInorderEnd = delimiterIndex; - // 右中序区间,左闭右开[rightInorderBegin, rightInorderEnd) - int rightInorderBegin = delimiterIndex + 1; - int rightInorderEnd = inorderEnd; - - // 切割后序数组 - // 左后序区间,左闭右开[leftPostorderBegin, leftPostorderEnd) - int leftPostorderBegin = postorderBegin; - int leftPostorderEnd = postorderBegin + delimiterIndex - inorderBegin; // 终止位置是 需要加上 中序区间的大小size - // 右后序区间,左闭右开[rightPostorderBegin, rightPostorderEnd) - int rightPostorderBegin = postorderBegin + (delimiterIndex - inorderBegin); - int rightPostorderEnd = postorderEnd - 1; // 排除最后一个元素,已经作为节点了 - - cout << "----------" << endl; - cout << "leftInorder :"; - for (int i = leftInorderBegin; i < leftInorderEnd; i++) { - cout << inorder[i] << " "; - } - cout << endl; - - cout << "rightInorder :"; - for (int i = rightInorderBegin; i < rightInorderEnd; i++) { - cout << inorder[i] << " "; - } - cout << endl; - - cout << "leftpostorder :"; - for (int i = leftPostorderBegin; i < leftPostorderEnd; i++) { - cout << postorder[i] << " "; - } - cout << endl; - - cout << "rightpostorder :"; - for (int i = rightPostorderBegin; i < rightPostorderEnd; i++) { - cout << postorder[i] << " "; - } - cout << endl; - - root->left = traversal(inorder, leftInorderBegin, leftInorderEnd, postorder, leftPostorderBegin, leftPostorderEnd); - root->right = traversal(inorder, rightInorderBegin, rightInorderEnd, postorder, rightPostorderBegin, rightPostorderEnd); - - return root; - } -public: - TreeNode* buildTree(vector& inorder, vector& postorder) { - if (inorder.size() == 0 || postorder.size() == 0) return NULL; - return traversal(inorder, 0, inorder.size(), postorder, 0, postorder.size()); - } -}; -``` - -# 105.从前序与中序遍历序列构造二叉树 - -根据一棵树的前序遍历与中序遍历构造二叉树。 - -注意: -你可以假设树中没有重复的元素。 - -例如,给出 - -前序遍历 preorder = [3,9,20,15,7] -中序遍历 inorder = [9,3,15,20,7] -返回如下的二叉树: - - - -## 思路 - -本题和106是一样的道理。 - -我就直接给出代码了。 - -带日志的版本C++代码如下: (**带日志的版本仅用于调试,不要在leetcode上提交,会超时**) - -``` -class Solution { -private: - TreeNode* traversal (vector& inorder, int inorderBegin, int inorderEnd, vector& preorder, int preorderBegin, int preorderEnd) { - if (preorderBegin == preorderEnd) return NULL; - - int rootValue = preorder[preorderBegin]; // 注意用preorderBegin 不要用0 - TreeNode* root = new TreeNode(rootValue); - - if (preorderEnd - preorderBegin == 1) return root; - - int delimiterIndex; - for (delimiterIndex = inorderBegin; delimiterIndex < inorderEnd; delimiterIndex++) { - if (inorder[delimiterIndex] == rootValue) break; - } - // 切割中序数组 - // 中序左区间,左闭右开[leftInorderBegin, leftInorderEnd) - int leftInorderBegin = inorderBegin; - int leftInorderEnd = delimiterIndex; - // 中序右区间,左闭右开[rightInorderBegin, rightInorderEnd) - int rightInorderBegin = delimiterIndex + 1; - int rightInorderEnd = inorderEnd; - - // 切割前序数组 - // 前序左区间,左闭右开[leftPreorderBegin, leftPreorderEnd) - int leftPreorderBegin = preorderBegin + 1; - int leftPreorderEnd = preorderBegin + 1 + delimiterIndex - inorderBegin; // 终止位置是起始位置加上中序左区间的大小size - // 前序右区间, 左闭右开[rightPreorderBegin, rightPreorderEnd) - int rightPreorderBegin = preorderBegin + 1 + (delimiterIndex - inorderBegin); - int rightPreorderEnd = preorderEnd; - - cout << "----------" << endl; - cout << "leftInorder :"; - for (int i = leftInorderBegin; i < leftInorderEnd; i++) { - cout << inorder[i] << " "; - } - cout << endl; - - cout << "rightInorder :"; - for (int i = rightInorderBegin; i < rightInorderEnd; i++) { - cout << inorder[i] << " "; - } - cout << endl; - - cout << "leftPreorder :"; - for (int i = leftPreorderBegin; i < leftPreorderEnd; i++) { - cout << preorder[i] << " "; - } - cout << endl; - - cout << "rightPreorder :"; - for (int i = rightPreorderBegin; i < rightPreorderEnd; i++) { - cout << preorder[i] << " "; - } - cout << endl; - - - root->left = traversal(inorder, leftInorderBegin, leftInorderEnd, preorder, leftPreorderBegin, leftPreorderEnd); - root->right = traversal(inorder, rightInorderBegin, rightInorderEnd, preorder, rightPreorderBegin, rightPreorderEnd); - - return root; - } - -public: - TreeNode* buildTree(vector& preorder, vector& inorder) { - if (inorder.size() == 0 || preorder.size() == 0) return NULL; - return traversal(inorder, 0, inorder.size(), preorder, 0, preorder.size()); - - } -}; -``` - -105.从前序与中序遍历序列构造二叉树,最后版本,C++代码: - -``` -class Solution { -private: - TreeNode* traversal (vector& inorder, int inorderBegin, int inorderEnd, vector& preorder, int preorderBegin, int preorderEnd) { - if (preorderBegin == preorderEnd) return NULL; - - int rootValue = preorder[preorderBegin]; // 注意用preorderBegin 不要用0 - TreeNode* root = new TreeNode(rootValue); - - if (preorderEnd - preorderBegin == 1) return root; - - int delimiterIndex; - for (delimiterIndex = inorderBegin; delimiterIndex < inorderEnd; delimiterIndex++) { - if (inorder[delimiterIndex] == rootValue) break; - } - // 切割中序数组 - // 中序左区间,左闭右开[leftInorderBegin, leftInorderEnd) - int leftInorderBegin = inorderBegin; - int leftInorderEnd = delimiterIndex; - // 中序右区间,左闭右开[rightInorderBegin, rightInorderEnd) - int rightInorderBegin = delimiterIndex + 1; - int rightInorderEnd = inorderEnd; - - // 切割前序数组 - // 前序左区间,左闭右开[leftPreorderBegin, leftPreorderEnd) - int leftPreorderBegin = preorderBegin + 1; - int leftPreorderEnd = preorderBegin + 1 + delimiterIndex - inorderBegin; // 终止位置是起始位置加上中序左区间的大小size - // 前序右区间, 左闭右开[rightPreorderBegin, rightPreorderEnd) - int rightPreorderBegin = preorderBegin + 1 + (delimiterIndex - inorderBegin); - int rightPreorderEnd = preorderEnd; - - root->left = traversal(inorder, leftInorderBegin, leftInorderEnd, preorder, leftPreorderBegin, leftPreorderEnd); - root->right = traversal(inorder, rightInorderBegin, rightInorderEnd, preorder, rightPreorderBegin, rightPreorderEnd); - - return root; - } - -public: - TreeNode* buildTree(vector& preorder, vector& inorder) { - if (inorder.size() == 0 || preorder.size() == 0) return NULL; - - // 参数坚持左闭右开的原则 - return traversal(inorder, 0, inorder.size(), preorder, 0, preorder.size()); - } -}; -``` - -# 思考题 - -前序和中序可以唯一确定一颗二叉树。 - -后序和中序可以唯一确定一颗二叉树。 - -那么前序和后序可不可以唯一确定一颗二叉树呢? - -**前序和后序不能唯一确定一颗二叉树!**,因为没有中序遍历无法确定左右部分,也就是无法分割。 - -举一个例子: - - - -tree1 的前序遍历是[1 2 3], 后序遍历是[3 2 1]。 - -tree2 的前序遍历是[1 2 3], 后序遍历是[3 2 1]。 - -那么tree1 和 tree2 的前序和后序完全相同,这是一棵树么,很明显是两棵树! - -所以前序和后序不能唯一确定一颗二叉树! - -# 总结 - -之前我们讲的二叉树题目都是各种遍历二叉树,这次开始构造二叉树了,思路其实比较简单,但是真正代码实现出来并不容易。 - -所以要避免眼高手低,踏实的把代码写出来。 - -我同时给出了添加日志的代码版本,因为这种题目是不太容易写出来调一调就能过的,所以一定要把流程日志打出来,看看符不符合自己的思路。 - -大家遇到这种题目的时候,也要学会打日志来调试(如何打日志有时候也是个技术活),不要脑动模拟,脑动模拟很容易越想越乱。 - -最后我还给出了为什么前序和中序可以唯一确定一颗二叉树,后序和中序可以唯一确定一颗二叉树,而前序和后序却不行。 - -认真研究完本篇,相信大家对二叉树的构造会清晰很多。 - -如果学到了,就赶紧转发给身边需要的同学吧! - -加个油! - - diff --git a/problems/0107.二叉树的层次遍历II.md b/problems/0107.二叉树的层次遍历II.md deleted file mode 100644 index 91ca6a94..00000000 --- a/problems/0107.二叉树的层次遍历II.md +++ /dev/null @@ -1,46 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/binary-tree-level-order-traversal-ii/ - -## 思路 - -这道题目相对于[0102.二叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0102.二叉树的层序遍历.md),就把结果倒叙过来,就可以了。 - -层序遍历一个二叉树。就是从左到右一层一层的去遍历二叉树。这种遍历的方式和我们之前讲过的都不太一样。 - -需要借用一个辅助数据结构队列来实现,**队列先进先出,符合一层一层遍历的逻辑,而是用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。** - -使用队列实现广度优先遍历,动画如下: - - - -这样就实现了层序从左到右遍历二叉树。 - -代码如下:这份代码也可以作为二叉树层序遍历的模板。 - -## C++代码 - -``` -class Solution { -public: - vector> levelOrderBottom(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); - } - reverse(result.begin(), result.end()); - return result; - - } -}; -``` diff --git a/problems/0108.将有序数组转换为二叉搜索树.md b/problems/0108.将有序数组转换为二叉搜索树.md deleted file mode 100644 index 3afc8d6a..00000000 --- a/problems/0108.将有序数组转换为二叉搜索树.md +++ /dev/null @@ -1,197 +0,0 @@ -> 构造二叉搜索树,一不小心就平衡了 - -# 108.将有序数组转换为二叉搜索树 - -将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。 - -本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。 - -示例: - -![108.将有序数组转换为二叉搜索树](https://img-blog.csdnimg.cn/20201022164420763.png) - -# 思路 - -做这道题目之前大家可以了解一下这几道: - -* [106.从中序与后序遍历序列构造二叉树](https://mp.weixin.qq.com/s/7r66ap2s-shvVvlZxo59xg) -* [654.最大二叉树](https://mp.weixin.qq.com/s/1iWJV6Aov23A7xCF4nV88w)中其实已经讲过了,如果根据数组构造一颗二叉树。 -* [701.二叉搜索树中的插入操作](https://mp.weixin.qq.com/s/lwKkLQcfbCNX2W-5SOeZEA) -* [450.删除二叉搜索树中的节点](https://mp.weixin.qq.com/s/-p-Txvch1FFk3ygKLjPAKw) - - -进入正题: - -题目中说要转换为一棵高度平衡二叉搜索树。这和转换为一棵普通二叉搜索树有什么差别呢? - -其实这里不用强调平衡二叉搜索树,数组构造二叉树,构成平衡树是自然而然的事情,因为大家默认都是从数组中间位置取值作为节点元素,一般不会随机取,**所以想构成不平衡的二叉树是自找麻烦**。 - - -在[二叉树:构造二叉树登场!](https://mp.weixin.qq.com/s/7r66ap2s-shvVvlZxo59xg)和[二叉树:构造一棵最大的二叉树](https://mp.weixin.qq.com/s/1iWJV6Aov23A7xCF4nV88w)中其实已经讲过了,如果根据数组构造一颗二叉树。 - -**本质就是寻找分割点,分割点作为当前节点,然后递归左区间和右区间**。 - -本题其实要比[二叉树:构造二叉树登场!](https://mp.weixin.qq.com/s/7r66ap2s-shvVvlZxo59xg) 和 [二叉树:构造一棵最大的二叉树](https://mp.weixin.qq.com/s/1iWJV6Aov23A7xCF4nV88w)简单一些,因为有序数组构造二叉搜索树,寻找分割点就比较容易了。 - -分割点就是数组中间位置的节点。 - -那么为问题来了,如果数组长度为偶数,中间节点有两个,取哪一个? - -取哪一个都可以,只不过构成了不同的平衡二叉搜索树。 - -例如:输入:[-10,-3,0,5,9] - -如下两棵树,都是这个数组的平衡二叉搜索树: - - - -如果要分割的数组长度为偶数的时候,中间元素为两个,是取左边元素 就是树1,取右边元素就是树2。 - -**这也是题目中强调答案不是唯一的原因。 理解这一点,这道题目算是理解到位了**。 - -## 递归 - -递归三部曲: - -* 确定递归函数返回值及其参数 - -删除二叉树节点,增加二叉树节点,都是用递归函数的返回值来完成,这样是比较方便的。 - -相信大家如果仔细看了[二叉树:搜索树中的插入操作](https://mp.weixin.qq.com/s/lwKkLQcfbCNX2W-5SOeZEA)和[二叉树:搜索树中的删除操作](https://mp.weixin.qq.com/s/-p-Txvch1FFk3ygKLjPAKw),一定会对递归函数返回值的作用深有感触。 - -那么本题要构造二叉树,依然用递归函数的返回值来构造中节点的左右孩子。 - -再来看参数,首先是传入数组,然后就是左下表left和右下表right,我们在[二叉树:构造二叉树登场!](https://mp.weixin.qq.com/s/7r66ap2s-shvVvlZxo59xg)中提过,在构造二叉树的时候尽量不要重新定义左右区间数组,而是用下表来操作原数组。 - -所以代码如下: - -``` -// 左闭右闭区间[left, right] -TreeNode* traversal(vector& nums, int left, int right) -``` - -这里注意,**我这里定义的是左闭右闭区间,在不断分割的过程中,也会坚持左闭右闭的区间,这又涉及到我们讲过的循环不变量**。 - -在[二叉树:构造二叉树登场!](https://mp.weixin.qq.com/s/7r66ap2s-shvVvlZxo59xg),[35.搜索插入位置](https://mp.weixin.qq.com/s/fCf5QbPDtE6SSlZ1yh_q8Q) 和[59.螺旋矩阵II](https://mp.weixin.qq.com/s/KTPhaeqxbMK9CxHUUgFDmg)都详细讲过循环不变量。 - - -* 确定递归终止条件 - -这里定义的是左闭右闭的区间,所以当区间 left > right的时候,就是空节点了。 - -代码如下: - -``` -if (left > right) return nullptr; -``` - -* 确定单层递归的逻辑 - -首先取数组中间元素的位置,不难写出`int mid = (left + right) / 2;`,**这么写其实有一个问题,就是数值越界,例如left和right都是最大int,这么操作就越界了,在[二分法](https://mp.weixin.qq.com/s/fCf5QbPDtE6SSlZ1yh_q8Q)中尤其需要注意!** - -所以可以这么写:`int mid = left + ((right - left) / 2);` - -但本题leetcode的测试数据并不会越界,所以怎么写都可以。但需要有这个意识! - -取了中间位置,就开始以中间位置的元素构造节点,代码:`TreeNode* root = new TreeNode(nums[mid]);`。 - -接着划分区间,root的左孩子接住下一层左区间的构造节点,右孩子接住下一层右区间构造的节点。 - -最后返回root节点,单层递归整体代码如下: - -``` -int mid = left + ((right - left) / 2); -TreeNode* root = new TreeNode(nums[mid]); -root->left = traversal(nums, left, mid - 1); -root->right = traversal(nums, mid + 1, right); -return root; -``` - -这里`int mid = left + ((right - left) / 2);`的写法相当于是如果数组长度为偶数,中间位置有两个元素,取靠左边的。 - -* 递归整体代码如下: - -``` -class Solution { -private: - TreeNode* traversal(vector& nums, int left, int right) { - if (left > right) return nullptr; - int mid = left + ((right - left) / 2); - TreeNode* root = new TreeNode(nums[mid]); - root->left = traversal(nums, left, mid - 1); - root->right = traversal(nums, mid + 1, right); - return root; - } -public: - TreeNode* sortedArrayToBST(vector& nums) { - TreeNode* root = traversal(nums, 0, nums.size() - 1); - return root; - } -}; -``` - -**注意:在调用traversal的时候为什么传入的left和right为什么是0和nums.size() - 1,因为定义的区间为左闭右闭**。 - - -## 迭代法 - -迭代法可以通过三个队列来模拟,一个队列放遍历的节点,一个队列放左区间下表,一个队列放右区间下表。 - -模拟的就是不断分割的过程,C++代码如下:(我已经详细注释) - -``` -class Solution { -public: - TreeNode* sortedArrayToBST(vector& nums) { - if (nums.size() == 0) return nullptr; - - TreeNode* root = new TreeNode(0); // 初始根节点 - queue nodeQue; // 放遍历的节点 - queue leftQue; // 保存左区间下表 - queue rightQue; // 保存右区间下表 - nodeQue.push(root); // 根节点入队列 - leftQue.push(0); // 0为左区间下表初始位置 - rightQue.push(nums.size() - 1); // nums.size() - 1为右区间下表初始位置 - - while (!nodeQue.empty()) { - TreeNode* curNode = nodeQue.front(); - nodeQue.pop(); - int left = leftQue.front(); leftQue.pop(); - int right = rightQue.front(); rightQue.pop(); - int mid = left + ((right - left) / 2); - - curNode->val = nums[mid]; // 将mid对应的元素给中间节点 - - if (left <= mid - 1) { // 处理左区间 - curNode->left = new TreeNode(0); - nodeQue.push(curNode->left); - leftQue.push(left); - rightQue.push(mid - 1); - } - - if (right >= mid + 1) { // 处理右区间 - curNode->right = new TreeNode(0); - nodeQue.push(curNode->right); - leftQue.push(mid + 1); - rightQue.push(right); - } - } - return root; - } -}; -``` - -# 总结 - -**在[二叉树:构造二叉树登场!](https://mp.weixin.qq.com/s/7r66ap2s-shvVvlZxo59xg) 和 [二叉树:构造一棵最大的二叉树](https://mp.weixin.qq.com/s/1iWJV6Aov23A7xCF4nV88w)之后,我们顺理成章的应该构造一下二叉搜索树了,一不小心还是一棵平衡二叉搜索树**。 - -其实思路也是一样的,不断中间分割,然后递归处理左区间,右区间,也可以说是分治。 - -此时相信大家应该对通过递归函数的返回值来增删二叉树很熟悉了,这也是常规操作。 - -在定义区间的过程中我们又一次强调了循环不变量的重要性。 - -最后依然给出迭代的方法,其实就是模拟取中间元素,然后不断分割去构造二叉树的过程。 - -**就酱,如果对你有帮助的话,也转发给身边需要的同学吧!** - diff --git a/problems/0110.平衡二叉树.md b/problems/0110.平衡二叉树.md deleted file mode 100644 index 09d5598a..00000000 --- a/problems/0110.平衡二叉树.md +++ /dev/null @@ -1,345 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/balanced-binary-tree/ - -> 求高度还是求深度,你搞懂了不? - -# 110.平衡二叉树 - -给定一个二叉树,判断它是否是高度平衡的二叉树。 - -本题中,一棵高度平衡二叉树定义为:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。 - -示例 1: - -给定二叉树 [3,9,20,null,null,15,7] - - - -返回 true 。 - -示例 2: - -给定二叉树 [1,2,2,3,3,null,null,4,4] - - - -返回 false 。 - -# 题外话 - -咋眼一看这道题目和[二叉树:看看这些树的最大深度](https://mp.weixin.qq.com/s/guKwV-gSNbA1CcbvkMtHBg)很像,其实有很大区别。 - -这里强调一波概念: - -* 二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数。 -* 二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数。 - -但leetcode中强调的深度和高度很明显是按照节点来计算的,如图: - - - -关于根节点的深度究竟是1 还是 0,不同的地方有不一样的标准,leetcode的题目中都是以节点为一度,即根节点深度是1。但维基百科上定义用边为一度,即根节点的深度是0,我们暂时以leetcode为准(毕竟要在这上面刷题)。 - -因为求深度可以从上到下去查 所以需要前序遍历(中左右),而高度只能从下到上去查,所以只能后序遍历(左右中) - -有的同学一定疑惑,为什么[二叉树:看看这些树的最大深度](https://mp.weixin.qq.com/s/guKwV-gSNbA1CcbvkMtHBg)中求的是二叉树的最大深度,也用的是后序遍历。 - -**那是因为代码的逻辑其实是求的根节点的高度,而根节点的高度就是这颗树的最大深度,所以才可以使用后序遍历。** - -在[二叉树:看看这些树的最大深度](https://mp.weixin.qq.com/s/guKwV-gSNbA1CcbvkMtHBg)中,如果真正求取二叉树的最大深度,代码应该写成如下:(前序遍历) - -``` -class Solution { -public: - int result; - void getDepth(TreeNode* node, int depth) { - result = depth > result ? depth : result; // 中 - - if (node->left == NULL && node->right == NULL) return ; - - if (node->left) { // 左 - depth++; // 深度+1 - getDepth(node->left, depth); - depth--; // 回溯,深度-1 - } - if (node->right) { // 右 - depth++; // 深度+1 - getDepth(node->right, depth); - depth--; // 回溯,深度-1 - } - return ; - } - int maxDepth(TreeNode* root) { - result = 0; - if (root == 0) return result; - getDepth(root, 1); - return result; - } -}; -``` - -**可以看出使用了前序(中左右)的遍历顺序,这才是真正求深度的逻辑!** - -注意以上代码是为了把细节体现出来,简化一下代码如下: - -``` -class Solution { -public: - int result; - void getDepth(TreeNode* node, int depth) { - result = depth > result ? depth : result; // 中 - if (node->left == NULL && node->right == NULL) return ; - if (node->left) { // 左 - getDepth(node->left, depth + 1); - } - if (node->right) { // 右 - getDepth(node->right, depth + 1); - } - return ; - } - int maxDepth(TreeNode* root) { - result = 0; - if (root == 0) return result; - getDepth(root, 1); - return result; - } -}; -``` - -# 本题思路 - -## 递归 - -此时大家应该明白了既然要求比较高度,必然是要后序遍历。 - -递归三步曲分析: - -1. 明确递归函数的参数和返回值 - -参数的话为传入的节点指针,就没有其他参数需要传递了,返回值要返回传入节点为根节点树的深度。 - -那么如何标记左右子树是否差值大于1呢。 - -如果当前传入节点为根节点的二叉树已经不是二叉平衡树了,还返回高度的话就没有意义了。 - -所以如果已经不是二叉平衡树了,可以返回-1 来标记已经不符合平衡树的规则了。 - -代码如下: - - -``` -// -1 表示已经不是平衡二叉树了,否则返回值是以该节点为根节点树的高度 -int getDepth(TreeNode* node) -``` - -2. 明确终止条件 - -递归的过程中依然是遇到空节点了为终止,返回0,表示当前节点为根节点的书高度为0 - -代码如下: - -``` -if (node == NULL) { - return 0; -} -``` - -3. 明确单层递归的逻辑 - -如何判断当前传入节点为根节点的二叉树是否是平衡二叉树呢,当然是左子树高度和右子树高度相差。 - -分别求出左右子树的高度,然后如果差值小于等于1,则返回当前二叉树的高度,否则则返回-1,表示已经不是二叉树了。 - -代码如下: - -``` -int leftDepth = depth(node->left); // 左 -if (leftDepth == -1) return -1; -int rightDepth = depth(node->right); // 右 -if (rightDepth == -1) return -1; - -int result; -if (abs(leftDepth - rightDepth) > 1) { // 中 - result = -1; -} else { - result = 1 + max(leftDepth, rightDepth); // 以当前节点为根节点的最大高度 -} - -return result; -``` - -代码精简之后如下: - -``` -int leftDepth = getDepth(node->left); -if (leftDepth == -1) return -1; -int rightDepth = getDepth(node->right); -if (rightDepth == -1) return -1; -return abs(leftDepth - rightDepth) > 1 ? -1 : 1 + max(leftDepth, rightDepth); -``` - -此时递归的函数就已经写出来了,这个递归的函数传入节点指针,返回以该节点为根节点的二叉树的高度,如果不是二叉平衡树,则返回-1。 - -getDepth整体代码如下: - -``` -int getDepth(TreeNode* node) { - if (node == NULL) { - return 0; - } - int leftDepth = getDepth(node->left); - if (leftDepth == -1) return -1; - int rightDepth = getDepth(node->right); - if (rightDepth == -1) return -1; - return abs(leftDepth - rightDepth) > 1 ? -1 : 1 + max(leftDepth, rightDepth); -} -``` - -最后本题整体递归代码如下: - -``` -class Solution { -public: - // 返回以该节点为根节点的二叉树的高度,如果不是二叉搜索树了则返回-1 - int getDepth(TreeNode* node) { - if (node == NULL) { - return 0; - } - int leftDepth = getDepth(node->left); - if (leftDepth == -1) return -1; // 说明左子树已经不是二叉平衡树 - int rightDepth = getDepth(node->right); - if (rightDepth == -1) return -1; // 说明右子树已经不是二叉平衡树 - return abs(leftDepth - rightDepth) > 1 ? -1 : 1 + max(leftDepth, rightDepth); - } - bool isBalanced(TreeNode* root) { - return getDepth(root) == -1 ? false : true; - } -}; -``` - -## 迭代 - -在[二叉树:看看这些树的最大深度](https://mp.weixin.qq.com/s/guKwV-gSNbA1CcbvkMtHBg)中我们可以使用层序遍历来求深度,但是就不能直接用层序遍历来求高度了,这就体现出求高度和求深度的不同。 - -本题的迭代方式可以先定义一个函数,专门用来求高度。 - -这个函数通过栈模拟的后序遍历找每一个节点的高度(其实是通过求传入节点为根节点的最大深度来求的高度) - -代码如下: - -``` -// cur节点的最大深度,就是cur的高度 -int getDepth(TreeNode* cur) { - stack st; - if (cur != NULL) st.push(cur); - int depth = 0; // 记录深度 - int result = 0; - while (!st.empty()) { - TreeNode* node = st.top(); - if (node != NULL) { - st.pop(); - st.push(node); // 中 - st.push(NULL); - depth++; - if (node->right) st.push(node->right); // 右 - if (node->left) st.push(node->left); // 左 - - } else { - st.pop(); - node = st.top(); - st.pop(); - depth--; - } - result = result > depth ? result : depth; - } - return result; -} -``` - -然后再用栈来模拟前序遍历,遍历每一个节点的时候,再去判断左右孩子的高度是否符合,代码如下: - -``` -bool isBalanced(TreeNode* root) { - stack st; - if (root == NULL) return true; - st.push(root); - while (!st.empty()) { - TreeNode* node = st.top(); // 中 - st.pop(); - if (abs(getDepth(node->left) - getDepth(node->right)) > 1) { // 判断左右孩子高度是否符合 - return false; - } - if (node->right) st.push(node->right); // 右(空节点不入栈) - if (node->left) st.push(node->left); // 左(空节点不入栈) - } - return true; -} -``` - -整体代码如下: - -``` -class Solution { -private: - int getDepth(TreeNode* cur) { - stack st; - if (cur != NULL) st.push(cur); - int depth = 0; // 记录深度 - int result = 0; - while (!st.empty()) { - TreeNode* node = st.top(); - if (node != NULL) { - st.pop(); - st.push(node); // 中 - st.push(NULL); - depth++; - if (node->right) st.push(node->right); // 右 - if (node->left) st.push(node->left); // 左 - - } else { - st.pop(); - node = st.top(); - st.pop(); - depth--; - } - result = result > depth ? result : depth; - } - return result; - } - -public: - bool isBalanced(TreeNode* root) { - stack st; - if (root == NULL) return true; - st.push(root); - while (!st.empty()) { - TreeNode* node = st.top(); // 中 - st.pop(); - if (abs(getDepth(node->left) - getDepth(node->right)) > 1) { - return false; - } - if (node->right) st.push(node->right); // 右(空节点不入栈) - if (node->left) st.push(node->left); // 左(空节点不入栈) - } - return true; - } -}; -``` - -当然此题用迭代法,其实效率很低,因为没有很好的模拟回溯的过程,所以迭代法有很多重复的计算。 - -虽然理论上所有的递归都可以用迭代来实现,但是有的场景难度可能比较大。 - -**例如:都知道回溯法其实就是递归,但是很少人用迭代的方式去实现回溯算法!** - -因为对于回溯算法已经是非常复杂的递归了,如果在用迭代的话,就是自己给自己找麻烦,效率也并不一定高。 - -# 总结 - -通过本题可以了解求二叉树深度 和 二叉树高度的差异,求深度适合用前序遍历,而求高度适合用后序遍历。 - -本题迭代法其实有点复杂,大家可以有一个思路,也不一定说非要写出来。 - -但是递归方式是一定要掌握的! - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0111.二叉树的最小深度.md b/problems/0111.二叉树的最小深度.md deleted file mode 100644 index eebfd76e..00000000 --- a/problems/0111.二叉树的最小深度.md +++ /dev/null @@ -1,185 +0,0 @@ - -## 题目地址 -https://leetcode-cn.com/problems/minimum-depth-of-binary-tree/ - -> 和求最大深度一个套路? - -# 111.二叉树的最小深度 - -给定一个二叉树,找出其最小深度。 - -最小深度是从根节点到最近叶子节点的最短路径上的节点数量。 - -说明: 叶子节点是指没有子节点的节点。 - -示例: - -给定二叉树 [3,9,20,null,null,15,7], - - - -返回它的最小深度 2. - -# 思路 - -看完了这篇[二叉树:看看这些树的最大深度](https://mp.weixin.qq.com/s/guKwV-gSNbA1CcbvkMtHBg),再来看看如何求最小深度。 - -直觉上好像和求最大深度差不多,其实还是差不少的。 - -遍历顺序上依然是后序遍历(因为要比较递归返回之后的结果),但在处理中间节点的逻辑上,最大深度很容易理解,最小深度可有一个误区,如图: - - - -这就重新审题了,题目中说的是:**最小深度是从根节点到最近叶子节点的最短路径上的节点数量。**,注意是**叶子节点**。 - -什么是叶子节点,左右孩子都为空的节点才是叶子节点! - -## 递归法 - -来来来,一起递归三部曲: - -1. 确定递归函数的参数和返回值 - -参数为要传入的二叉树根节点,返回的是int类型的深度。 - -代码如下: - -``` -int getDepth(TreeNode* node) -``` - -2. 确定终止条件 - -终止条件也是遇到空节点返回0,表示当前节点的高度为0。 - -代码如下: - -``` -if (node == NULL) return 0; -``` - -3. 确定单层递归的逻辑 - -这块和求最大深度可就不一样了,一些同学可能会写如下代码: -``` -int leftDepth = getDepth(node->left); -int rightDepth = getDepth(node->right); -int result = 1 + min(leftDepth, rightDepth); -return result; -``` - -这个代码就犯了此图中的误区: - - - -如果这么求的话,没有左孩子的分支会算为最短深度。 - -所以,如果左子树为空,右子树不为空,说明最小深度是 1 + 右子树的深度。 - -反之,右子树为空,左子树不为空,最小深度是 1 + 左子树的深度。 最后如果左右子树都不为空,返回左右子树深度最小值 + 1 。 - -代码如下: - -``` -int leftDepth = getDepth(node->left); // 左 -int rightDepth = getDepth(node->right); // 右 - // 中 -// 当一个左子树为空,右不为空,这时并不是最低点 -if (node->left == NULL && node->right != NULL) {  -    return 1 + rightDepth; -}    -// 当一个右子树为空,左不为空,这时并不是最低点 -if (node->left != NULL && node->right == NULL) {  -    return 1 + leftDepth; -} -int result = 1 + min(leftDepth, rightDepth); -return result; -``` - -遍历的顺序为后序(左右中),可以看出:**求二叉树的最小深度和求二叉树的最大深度的差别主要在于处理左右孩子不为空的逻辑。** - -整体递归代码如下: -``` -class Solution { -public: - int getDepth(TreeNode* node) { - if (node == NULL) return 0; - int leftDepth = getDepth(node->left); // 左 - int rightDepth = getDepth(node->right); // 右 - // 中 - // 当一个左子树为空,右不为空,这时并不是最低点 - if (node->left == NULL && node->right != NULL) {  -     return 1 + rightDepth; - }    - // 当一个右子树为空,左不为空,这时并不是最低点 - if (node->left != NULL && node->right == NULL) {  -     return 1 + leftDepth; - } - int result = 1 + min(leftDepth, rightDepth); - return result; - } - - int minDepth(TreeNode* root) { - return getDepth(root); - } -}; -``` - -精简之后代码如下: - -``` -class Solution { -public: - int minDepth(TreeNode* root) { - if (root == NULL) return 0; - if (root->left == NULL && root->right != NULL) { - return 1 + minDepth(root->right); - } - if (root->left != NULL && root->right == NULL) { - return 1 + minDepth(root->left); - } - return 1 + min(minDepth(root->left), minDepth(root->right)); - } -}; -``` - -**精简之后的代码根本看不出是哪种遍历方式,所以依然还要强调一波:如果对二叉树的操作还不熟练,尽量不要直接照着精简代码来学。** - -## 迭代法 - -相对于[二叉树:看看这些树的最大深度](https://mp.weixin.qq.com/s/guKwV-gSNbA1CcbvkMtHBg),本题还可以使用层序遍历的方式来解决,思路是一样的。 - -如果对层序遍历还不清楚的话,可以看这篇:[二叉树:层序遍历登场!](https://mp.weixin.qq.com/s/Gb3BjakIKGNpup2jYtTzog) - -**需要注意的是,只有当左右孩子都为空的时候,才说明遍历的最低点了。如果其中一个孩子为空则不是最低点** - -代码如下:(详细注释) - -``` -class Solution { -public: - - int minDepth(TreeNode* root) { - if (root == NULL) return 0; - int depth = 0; - queue que; - que.push(root); - while(!que.empty()) { - int size = que.size(); - depth++; // 记录最小深度 - for (int i = 0; i < size; i++) { - TreeNode* node = que.front(); - que.pop(); - if (node->left) que.push(node->left); - if (node->right) que.push(node->right); - if (!node->left && !node->right) { // 当左右孩子都为空的时候,说明是最低点的一层了,退出 - return depth; - } - } - } - return depth; - } -}; -``` - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0112.路径总和.md b/problems/0112.路径总和.md deleted file mode 100644 index 29fc1ca2..00000000 --- a/problems/0112.路径总和.md +++ /dev/null @@ -1,291 +0,0 @@ -## 题目地址 - -> 递归函数什么时候需要返回值 - -相信很多同学都会疑惑,递归函数什么时候要有返回值,什么时候没有返回值,特别是有的时候递归函数返回类型为bool类型。那么 - -接下来我通过详细讲解如下两道题,来回答这个问题: - -* 112. 路径总和 -* 113. 路径总和II - -# 112. 路径总和 - -给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。 - -说明: 叶子节点是指没有子节点的节点。 - -示例:  -给定如下二叉树,以及目标和 sum = 22, - - - -返回 true, 因为存在目标和为 22 的根节点到叶子节点的路径 5->4->11->2。 - -# 思路 - - -这道题我们要遍历从根节点到叶子节点的的路径看看总和是不是目标和。 - -## 递归 - -可以使用深度优先遍历的方式(本题前中后序都可以,无所谓,因为中节点也没有处理逻辑)来遍历二叉树 - -1. 确定递归函数的参数和返回类型 - -参数:需要二叉树的根节点,还需要一个计数器,这个计数器用来计算二叉树的一条边之和是否正好是目标和,计数器为int型。 - -**再来看返回值,递归函数什么时候需要返回值?什么时候不需要返回值?** - -在文章[二叉树:我的左下角的值是多少?](https://mp.weixin.qq.com/s/MH2gbLvzQ91jHPKqiub0Nw)中,我给出了一个结论: - -**如果需要搜索整颗二叉树,那么递归函数就不要返回值,如果要搜索其中一条符合条件的路径,递归函数就需要返回值,因为遇到符合条件的路径了就要及时返回。** - -在[二叉树:我的左下角的值是多少?](https://mp.weixin.qq.com/s/MH2gbLvzQ91jHPKqiub0Nw)中,因为要遍历树的所有路径,找出深度最深的叶子节点,所以递归函数不要返回值。 - -而本题我们要找一条符合条件的路径,所以递归函数需要返回值,及时返回,那么返回类型是什么呢? - -如图所示: - - - -图中可以看出,遍历的路线,并不要遍历整棵树,所以递归函数需要返回值,可以用bool类型表示。 - -所以代码如下: - -``` -bool traversal(TreeNode* cur, int count) // 注意函数的返回类型 -``` - - -2. 确定终止条件 - -首先计数器如何统计这一条路径的和呢? - -不要去累加然后判断是否等于目标和,那么代码比较麻烦,可以用递减,让计数器count初始为目标和,然后每次减去遍历路径节点上的数值。 - -如果最后count == 0,同时到了叶子节点的话,说明找到了目标和。 - -如果遍历到了叶子节点,count不为0,就是没找到。 - -递归终止条件代码如下: - -``` -if (!cur->left && !cur->right && count == 0) return true; // 遇到叶子节点,并且计数为0 -if (!cur->left && !cur->right) return false; // 遇到叶子节点而没有找到合适的边,直接返回 -``` - -3. 确定单层递归的逻辑 - -因为终止条件是判断叶子节点,所以递归的过程中就不要让空节点进入递归了。 - -递归函数是有返回值的,如果递归函数返回true,说明找到了合适的路径,应该立刻返回。 - -代码如下: - -``` -if (cur->left) { // 左 (空节点不遍历) - // 遇到叶子节点返回true,则直接返回true - if (traversal(cur->left, count - cur->left->val)) return true; // 注意这里有回溯的逻辑 -} -if (cur->right) { // 右 (空节点不遍历) - // 遇到叶子节点返回true,则直接返回true - if (traversal(cur->right, count - cur->right->val)) return true; // 注意这里有回溯的逻辑 -} -return false; -``` - -以上代码中是包含着回溯的,没有回溯,如何后撤重新找另一条路径呢。 - -回溯隐藏在`traversal(cur->left, count - cur->left->val)`这里, 因为把`count - cur->left->val` 直接作为参数传进去,函数结束,count的数值没有改变。 - -为了把回溯的过程体现出来,可以改为如下代码: - -``` -if (cur->left) { // 左 - count -= cur->left->val; // 递归,处理节点; - if (traversal(cur->left, count)) return true; - count += cur->left->val; // 回溯,撤销处理结果 -} -if (cur->right) { // 右 - count -= cur->right->val; - if (traversal(cur->right, count - cur->right->val)) return true; - count += cur->right->val; -} -return false; -``` - - -整体代码如下: - -``` -class Solution { -private: - bool traversal(TreeNode* cur, int count) { - if (!cur->left && !cur->right && count == 0) return true; // 遇到叶子节点,并且计数为0 - if (!cur->left && !cur->right) return false; // 遇到叶子节点直接返回 - - if (cur->left) { // 左 - count -= cur->left->val; // 递归,处理节点; - if (traversal(cur->left, count)) return true; - count += cur->left->val; // 回溯,撤销处理结果 - } - if (cur->right) { // 右 - count -= cur->right->val; // 递归,处理节点; - if (traversal(cur->right, count)) return true; - count += cur->right->val; // 回溯,撤销处理结果 - } - return false; - } - -public: - bool hasPathSum(TreeNode* root, int sum) { - if (root == NULL) return false; - return traversal(root, sum - root->val); - } -}; -``` - -以上代码精简之后如下: - -``` -class Solution { -public: - bool hasPathSum(TreeNode* root, int sum) { - if (root == NULL) return false; - if (!root->left && !root->right && sum == root->val) { - return true; - } - return hasPathSum(root->left, sum - root->val) || hasPathSum(root->right, sum - root->val); - } -}; -``` - -**是不是发现精简之后的代码,已经完全看不出分析的过程了,所以我们要把题目分析清楚之后,在追求代码精简。** 这一点我已经强调很多次了! - - -## 迭代 - -如果使用栈模拟递归的话,那么如果做回溯呢? - -**此时栈里一个元素不仅要记录该节点指针,还要记录从头结点到该节点的路径数值总和。** - -C++就我们用pair结构来存放这个栈里的元素。 - -定义为:`pair` pair<节点指针,路径数值> - -这个为栈里的一个元素。 - -如下代码是使用栈模拟的前序遍历,如下:(详细注释) - -``` -class Solution { - -public: - bool hasPathSum(TreeNode* root, int sum) { - if (root == NULL) return false; - // 此时栈里要放的是pair<节点指针,路径数值> - stack> st; - st.push(pair(root, root->val)); - while (!st.empty()) { - pair node = st.top(); - st.pop(); - // 如果该节点是叶子节点了,同时该节点的路径数值等于sum,那么就返回true - if (!node.first->left && !node.first->right && sum == node.second) return true; - - // 右节点,压进去一个节点的时候,将该节点的路径数值也记录下来 - if (node.first->right) { - st.push(pair(node.first->right, node.second + node.first->right->val)); - } - - // 左节点,压进去一个节点的时候,将该节点的路径数值也记录下来 - if (node.first->left) { - st.push(pair(node.first->left, node.second + node.first->left->val)); - } - } - return false; - } -}; -``` - -如果大家完全理解了本地的递归方法之后,就可以顺便把leetcode上113. 路径总和II做了。 - -# 113. 路径总和II - -给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。 - -说明: 叶子节点是指没有子节点的节点。 - -示例: -给定如下二叉树,以及目标和 sum = 22, - - - - -## 思路 - -113.路径总和II要遍历整个树,找到所有路径,**所以递归函数不要返回值!** - -如图: - - - - -为了尽可能的把细节体现出来,我写出如下代码(**这份代码并不简洁,但是逻辑非常清晰**) - -``` -class Solution { -private: - vector> result; - vector path; - // 递归函数不需要返回值,因为我们要遍历整个树 - void traversal(TreeNode* cur, int count) { - if (!cur->left && !cur->right && count == 0) { // 遇到了叶子节点切找到了和为sum的路径 - result.push_back(path); - return; - } - - if (!cur->left && !cur->right) return ; // 遇到叶子节点而没有找到合适的边,直接返回 - - if (cur->left) { // 左 (空节点不遍历) - path.push_back(cur->left->val); - count -= cur->left->val; - traversal(cur->left, count); // 递归 - count += cur->left->val; // 回溯 - path.pop_back(); // 回溯 - } - if (cur->right) { // 右 (空节点不遍历) - path.push_back(cur->right->val); - count -= cur->right->val; - traversal(cur->right, count); // 递归 - count += cur->right->val; // 回溯 - path.pop_back(); // 回溯 - } - return ; - } - -public: - vector> pathSum(TreeNode* root, int sum) { - result.clear(); - path.clear(); - if (root == NULL) return result; - path.push_back(root->val); // 把根节点放进路径 - traversal(root, sum - root->val); - return result; - } -}; -``` - -至于113. 路径总和II 的迭代法我并没有写,用迭代方式记录所有路径比较麻烦,也没有必要,如果大家感兴趣的话,可以再深入研究研究。 - -# 总结 - -本篇通过leetcode上112. 路径总和 和 113. 路径总和II 详细的讲解了 递归函数什么时候需要返回值,什么不需要返回值。 - -这两道题目是掌握这一知识点非常好的题目,大家看完本篇文章再去做题,就会感受到搜索整棵树和搜索某一路径的差别。 - -对于112. 路径总和,我依然给出了递归法和迭代法,这种题目其实用迭代法会复杂一些,能掌握递归方式就够了! - -今天是长假最后一天了,内容多一些,也是为了尽快让大家恢复学习状态,哈哈。 - -加个油! diff --git a/problems/0113.路径总和II.md b/problems/0113.路径总和II.md deleted file mode 100644 index b12f27f2..00000000 --- a/problems/0113.路径总和II.md +++ /dev/null @@ -1,76 +0,0 @@ - -## 链接 - -## 思路 - -这道题目与其说是递归,不如说是回溯问题,题目要找到所有的路径。 - -这道题目相对于[112. 路径总和](https://leetcode-cn.com/problems/path-sum/) ,是要求出所有的路径和。 - - -**相信很多同学都疑惑递归的过程中究竟什么时候需要返回值,什么时候不需要返回值?** - -我在[112. 路径总和题解](https://leetcode-cn.com/problems/path-sum/solution/112-lu-jing-zong-he-di-gui-hui-su-die-dai-xiang-ji/)中给出了详细的解释。 - -**如果需要搜索整颗二叉树,那么递归函数就不要返回值,如果要搜索其中一条符合条件的路径,递归函数就需要返回值,因为遇到符合条件的路径了就要及时返回。** - -而本题要遍历整个树,找到所有路径,**所以本题的递归函数不要返回值!** - -如图: - - - - -这道题目其实比[112. 路径总和](https://leetcode-cn.com/problems/path-sum/)简单一些,大家做完了本题,可以在做[112. 路径总和](https://leetcode-cn.com/problems/path-sum/)。 - -为了尽可能的把回溯过程体现出来,我写出如下代码(**这个代码一定不是最简洁的,但是比较清晰的,过于简洁的代码不方便读者理解**) - - -## 回溯C++代码 - -``` -class Solution { -private: - vector> result; - vector path; - // 递归函数不需要返回值,因为我们要遍历整个树 - void traversal(TreeNode* cur, int count) { - if (!cur->left && !cur->right && count == 0) { // 遇到了叶子节点切找到了和为sum的路径 - result.push_back(path); - return; - } - - if (!cur->left && !cur->right) return ; // 遇到叶子节点而没有找到合适的边,直接返回 - - if (cur->left) { // 左 (空节点不遍历) - path.push_back(cur->left->val); - count -= cur->left->val; - traversal(cur->left, count); // 递归 - count += cur->left->val; // 回溯 - path.pop_back(); // 回溯 - } - if (cur->right) { // 右 (空节点不遍历) - path.push_back(cur->right->val); - count -= cur->right->val; - traversal(cur->right, count); // 递归 - count += cur->right->val; // 回溯 - path.pop_back(); // 回溯 - } - return ; - } - -public: - vector> pathSum(TreeNode* root, int sum) { - result.clear(); - path.clear(); - if (root == NULL) return result; - path.push_back(root->val); // 把根节点放进路径 - traversal(root, sum - root->val); - return result; - } -}; -``` - -这道题目也可以用迭代法,相对于112.路径总和,每个节点不仅要保存当前路径和,也要保存当前路径,其实比较麻烦,也没有必要,因为回溯法虽然也是递归,但是如果用迭代来实现回溯法的话,是很费劲的,因为回溯的过程需要用栈模拟出来非常麻烦。 - -这也是为什么我在后面讲解回溯算法的时候,都是使用递归,也没有人会有栈模拟回溯算法(自己找麻烦,哈哈)。 diff --git a/problems/0116.填充每个节点的下一个右侧节点指针.md b/problems/0116.填充每个节点的下一个右侧节点指针.md deleted file mode 100644 index def34c1d..00000000 --- a/problems/0116.填充每个节点的下一个右侧节点指针.md +++ /dev/null @@ -1,101 +0,0 @@ - - -# 链接 -https://leetcode-cn.com/problems/populating-next-right-pointers-in-each-node/ - -## 思路 - - -注意题目提示内容,: -* 你只能使用常量级额外空间。 -* 使用递归解题也符合要求,本题中递归程序占用的栈空间不算做额外的空间复杂度。 - -基本上就是要求使用递归了,迭代的方式一定会用到栈或者队列。 - -### 递归 - -一想用递归怎么做呢,虽然层序遍历是最直观的,但是递归的方式确实不好想。 - -如图,假如当前操作的节点是cur: - - - -最关键的点是可以通过上一层递归 搭出来的线,进行本次搭线。 - -图中cur节点为元素4,那么搭线的逻辑代码:(**注意注释中操作1和操作2和图中的对应关系**) - -``` -if (cur->left) cur->left->next = cur->right; // 操作1 -if (cur->right) { - if (cur->next) cur->right->next = cur->next->left; // 操作2 - else cur->right->next = NULL; -} -``` - -理解到这里,使用前序遍历,那么不难写出如下代码: - -如果对二叉树的前中后序不了解看这篇:[二叉树:一入递归深似海,从此offer是路人](https://mp.weixin.qq.com/s/PwVIfxDlT3kRgMASWAMGhA) - - -``` -class Solution { -private: - void traversal(Node* cur) { - if (cur == NULL) return; - // 中 - if (cur->left) cur->left->next = cur->right; // 操作1 - if (cur->right) { - if (cur->next) cur->right->next = cur->next->left; // 操作2 - else cur->right->next = NULL; - } - traversal(cur->left); // 左 - traversal(cur->right); // 右 - } -public: - Node* connect(Node* root) { - traversal(root); - return root; - } -}; -``` - -### 迭代(层序遍历) - -本题使用层序遍历是最为直观的,如果对层序遍历不了解,看这篇:[二叉树:层序遍历登场!](https://mp.weixin.qq.com/s/Gb3BjakIKGNpup2jYtTzog)。 - -层序遍历本来就是一层一层的去遍历,记录一层的头结点(nodePre),然后让nodePre指向当前遍历的节点就可以了。 - -代码如下: - -``` - -class Solution { -public: - Node* connect(Node* root) { - queue que; - if (root != NULL) que.push(root); - while (!que.empty()) { - int size = que.size(); - vector vec; - Node* nodePre; - Node* node; - for (int i = 0; i < size; i++) { // 开始每一层的遍历 - if (i == 0) { - nodePre = que.front(); // 记录一层的头结点 - que.pop(); - node = nodePre; - } else { - node = que.front(); - que.pop(); - nodePre->next = node; // 本层前一个节点next指向本节点 - nodePre = nodePre->next; - } - if (node->left) que.push(node->left); - if (node->right) que.push(node->right); - } - nodePre->next = NULL; // 本层最后一个节点指向NULL - } - return root; - } -}; -``` diff --git a/problems/0117.填充每个节点的下一个右侧节点指针II.md b/problems/0117.填充每个节点的下一个右侧节点指针II.md deleted file mode 100644 index 43b56b91..00000000 --- a/problems/0117.填充每个节点的下一个右侧节点指针II.md +++ /dev/null @@ -1,41 +0,0 @@ - -# 链接 -https://leetcode-cn.com/problems/populating-next-right-pointers-in-each-node-ii/ - -## 思路 - -这道题目使用递归还是有难度的,不是完美二叉树,应该怎么办。 - -## C++代码 - -``` -class Solution { -public: - Node* connect(Node* root) { - queue que; - if (root != NULL) que.push(root); - while (!que.empty()) { - int size = que.size(); - vector vec; - Node* nodePre; - Node* node; - for (int i = 0; i < size; i++) { - if (i == 0) { - nodePre = que.front(); // 取出一层的头结点 - que.pop(); - node = nodePre; - } else { - node = que.front(); - que.pop(); - nodePre->next = node; // 本层前一个节点next指向本节点 - nodePre = nodePre->next; - } - if (node->left) que.push(node->left); - if (node->right) que.push(node->right); - } - nodePre->next = NULL; // 本层最后一个节点指向NULL - } - return root; - } -}; -``` diff --git a/problems/0121.买卖股票的最佳时机.md b/problems/0121.买卖股票的最佳时机.md deleted file mode 100644 index f59e761e..00000000 --- a/problems/0121.买卖股票的最佳时机.md +++ /dev/null @@ -1,71 +0,0 @@ - -# 思路 - -## 暴力 - -这道题目最直观的想法,就是暴力,找优间距了。 - -``` -class Solution { -public: - int maxProfit(vector& prices) { - int result = 0; - for (int i = 0; i < prices.size(); i++) { - for (int j = i + 1; j < prices.size(); j++){ - result = max(result, prices[j] - prices[i]); - } - } - return result; - } -}; -``` - -* 时间复杂度:O(n^2) -* 空间复杂度:O(1) - -当然该方法超时了。 - - -## 贪心 - -因为股票就买卖一次,那么贪心的想法很自然就是取最左最小值,取最右最大值,那么得到的差值就是最大利润。 - -C++代码如下: - -```C++ -class Solution { -public: - int maxProfit(vector& prices) { - int low = INT_MAX; - int result = 0; - for (int i = 0; i < prices.size(); i++) { - low = min(low, prices[i]); // 取最左最小价格 - result = max(result, prices[i] - low); // 直接取最大区间利润 - } - return result; - } -}; -``` - -## 动态规划 - -dp[i][0] 表示第i天持有股票所得现金 -dp[i][1] 表示第i天不持有股票所得现金 - -``` -class Solution { -public: - int maxProfit(vector& prices) { - int n = prices.size(); - if (n == 0) return 0; - vector> dp(n, vector(2, 0)); - dp[0][0] -= prices[0]; // 持股票 - for (int i = 1; i < n; i++) { - dp[i][0] = max(dp[i - 1][0], -prices[i]); // 买入 - dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]); // 卖出 - } - return dp[n - 1][1]; - } -}; -``` - diff --git a/problems/0122.买卖股票的最佳时机II.md b/problems/0122.买卖股票的最佳时机II.md deleted file mode 100644 index 00470497..00000000 --- a/problems/0122.买卖股票的最佳时机II.md +++ /dev/null @@ -1,134 +0,0 @@ -> 贪心有时候比动态规划更巧妙,更好用! - -# 122.买卖股票的最佳时机II - -题目链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/ - -给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。 - -设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。 - -注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 - - -示例 1: -输入: [7,1,5,3,6,4] -输出: 7 -解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4。随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。 - -示例 2: -输入: [1,2,3,4,5] -输出: 4 -解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。 - -示例 3: -输入: [7,6,4,3,1] -输出: 0 -解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。 - -提示: -* 1 <= prices.length <= 3 * 10 ^ 4 -* 0 <= prices[i] <= 10 ^ 4 - -# 思路 - -本题首先要清楚两点: - -* 只有一只股票! -* 当前只有买股票或者买股票的操作 - -想获得利润至少要两天为一个交易单元。 - -## 贪心算法 - -这道题目可能我们只会想,选一个低的买入,在选个高的卖,在选一个低的买入.....循环反复。 - -**如果想到其实最终利润是可以分解的,那么本题就很容易了!** - -如果分解呢? - -假如第0天买入,第3天卖出,那么利润为:prices[3] - prices[0]。 - -相当于(prices[3] - prices[2]) + (prices[2] - prices[1]) + (prices[1] - prices[0])。 - -**此时就是把利润分解为每天为单位的维度,而不是从0天到第3天整体去考虑!** - -那么根据prices可以得到每天的利润序列:(prices[i] - prices[i - 1]).....(prices[1] - prices[0])。 - -如图: - -![122.买卖股票的最佳时机II](https://img-blog.csdnimg.cn/2020112917480858.png) - -一些同学陷入:第一天怎么就没有利润呢,第一天到底算不算的困惑中。 - -第一天当然没有利润,至少要第二天才会有利润,所以利润的序列比股票序列少一天! - -从图中可以发现,其实我们需要收集每天的正利润就可以,**收集正利润的区间,就是股票买卖的区间,而我们只需要关注最终利润,不需要记录区间**。 - -那么只收集正利润就是贪心所贪的地方! - -**局部最优:收集每天的正利润,全局最优:求得最大利润**。 - -局部最优可以推出全局最优,找不出反例,试一试贪心! - -对应C++代码如下: - -```C++ -class Solution { -public: - int maxProfit(vector& prices) { - int result = 0; - for (int i = 1; i < prices.size(); i++) { - result += max(prices[i] - prices[i - 1], 0); - } - return result; - } -}; -``` -* 时间复杂度O(n) -* 空间复杂度O(1) - -## 动态规划 - -动态规划将在下一个系列详细讲解,本题解先给出我的C++代码(带详细注释),感兴趣的同学可以自己先学习一下。 - -```C++ -class Solution { -public: - int maxProfit(vector& prices) { - // dp[i][1]第i天持有的最多现金 - // dp[i][0]第i天持有股票后的最多现金 - int n = prices.size(); - vector> dp(n, vector(2, 0)); - dp[0][0] -= prices[0]; // 持股票 - for (int i = 1; i < n; i++) { - // 第i天持股票所剩最多现金 = max(第i-1天持股票所剩现金, 第i-1天持现金-买第i天的股票) - dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]); - // 第i天持有最多现金 = max(第i-1天持有的最多现金,第i-1天持有股票的最多现金+第i天卖出股票) - dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]); - } - return max(dp[n - 1][0], dp[n - 1][1]); - } -}; -``` -* 时间复杂度O(n) -* 空间复杂度O(n) - -# 总结 - -股票问题其实是一个系列的,属于动态规划的范畴,因为目前在讲解贪心系列,所以股票问题会在之后的动态规划系列中详细讲解。 - -**可以看出有时候,贪心往往比动态规划更巧妙,更好用,所以别小看了贪心算法**。 - -**本题中理解利润拆分是关键点!** 不要整块的去看,而是把整体利润拆为每天的利润。 - -一旦想到这里了,很自然就会想到贪心了,即:只收集每天的正利润,最后稳稳的就是最大利润了。 - -就酱,「代码随想录」是技术公众号里的一抹清流,值得推荐给你的朋友同学们! - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20201124161234338.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** - - - diff --git a/problems/0123.买卖股票的最佳时机III.md b/problems/0123.买卖股票的最佳时机III.md deleted file mode 100644 index c515f669..00000000 --- a/problems/0123.买卖股票的最佳时机III.md +++ /dev/null @@ -1,140 +0,0 @@ -# 思路 - -这道题目相对 121.买卖股票的最佳时机 和 122.买卖股票的最佳时机II 难了不少。 - -关键在于至多买卖两次,这意味着可以买卖一次,可以买卖两次,也可以不买卖。 - -接来下我用动态规划五部曲详细分析一下: - -### 确定dp数组以及下标的含义 - -一天一共就有五个状态, -0. 没有操作 -1. 第一次买入 -2. 第一次卖出 -3. 第二次买入 -4. 第二次卖出 - -dp[i][j]中 i表示第i天,j为 [0 - 4] 五个状态,dp[i][j]表示第i天状态j所剩最大现金。 - -### 确定递推公式 - -dp[i][0] = dp[i - 1][0]; - -需要注意:dp[i][1],**表示的是第i天,买入股票的状态,并不是说一定要第i天买入股票,这是很多同学容易陷入的误区**。 - -达到dp[i][1]状态,有两个具体操作: - -* 操作一:第i天买入股票了,那么dp[i][1] = dp[i-1][0] - prices[i] -* 操作二:第i天没有操作,而是沿用前一天买入的状态,即:dp[i][1] = dp[i - 1][0] - -那么dp[i][1]究竟选 dp[i-1][0] - prices[i],还是dp[i - 1][0]呢? - -一定是选最大的,所以 dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][0]); - -同理dp[i][2]也有两个操作: - -* 操作一:第i天卖出股票了,那么dp[i][2] = dp[i - 1][i] + prices[i] -* 操作二:第i天没有操作,沿用前一天卖出股票的状态,即:dp[i][2] = dp[i - 1][2] - -所以dp[i][2] = max(dp[i - 1][i] + prices[i], dp[i][2]) - -同理可推出剩下状态部分: - -dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]); -dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]); - - -### dp数组如何初始化 - - -第0天没有操作,这个最容易想到,就是0,即:dp[0][0] = 0; - -第0天做第一次买入的操作,dp[0][1] = -prices[0]; - -第0天做第一次卖出的操作,这个初始值应该是多少呢? - -首先卖出的操作一定是收获利润,整个股票买卖最差情况也就是没有盈利即全程无操作现金为0, - -从递推公式中可以看出每次是取最大值,那么既然是收获利润如果比0还小了就没有必要收获这个利润了。 - -所以dp[0][2] = 0; - -第0天第二次买入操作,初始值应该是多少呢? - -不用管第几次,现在手头上没有现金,只要买入,现金就做相应的减少。 - -所以第二次买入操作,初始化为:dp[0][3] = -prices[0]; - -同理第二次卖出初始化dp[0][4] = 0; - -### 确定遍历顺序 - -从递归公式其实已经可以看出,一定是从前向后遍历,因为dp[i],依靠dp[i - 1]的数值。 - - -### 举例推导dp数组 - -以输入[1,2,3,4,5]为例 - - -![123.买卖股票的最佳时机III](https://img-blog.csdnimg.cn/20201228181724295.png) - -红色为最终求解。 - -因为利润最大一定是卖出的状态,所以最终最大利润是max(dp[4][2], dp[4][4]); - -### C++代码 - -以上五步都分析完了,不难写出如下代码: - -``` -class Solution { -public: - int maxProfit(vector& prices) { - if (prices.size() == 0) return 0; - vector> dp(prices.size(), vector(5, 0)); - dp[0][0] = 0; - dp[0][1] = -prices[0]; - dp[0][3] = -prices[0]; - for (int i = 1; i < prices.size(); i++) { - dp[i][0] = dp[i - 1][0]; - dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]); - dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]); - dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]); - dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]); - } - return max(dp[prices.size() - 1][2], dp[prices.size() - 1][4]); - - } -}; -``` - -* 时间复杂度:O(n) -* 空间复杂度:O(n * 5) - - - -当然,大家在网上看到的题解还有一种优化空间写法,如下: - -``` -class Solution { -public: - int maxProfit(vector& prices) { - if (prices.size() == 0) return 0; - vector dp(5, 0); - dp[0] = 0; - dp[1] = -prices[0]; - dp[3] = -prices[0]; - for (int i = 1; i < prices.size(); i++) { - dp[1] = max(dp[1], dp[0] - prices[i]); - dp[2] = max(dp[2], dp[1] + prices[i]); - dp[3] = max(dp[3], dp[2] - prices[i]); - dp[4] = max(dp[4], dp[3] + prices[i]); - } - return max(dp[2], dp[4]); - - } -}; -``` -但这种写法,dp[2] 利用的是当天的dp[1],我还没有理解为什么这种写法也可以通过,网上的题解也没有做出解释,可能这就是神代码吧,欢迎大家来讨论一波! diff --git a/problems/0127.单词接龙.md b/problems/0127.单词接龙.md deleted file mode 100644 index 575fc4c0..00000000 --- a/problems/0127.单词接龙.md +++ /dev/null @@ -1,73 +0,0 @@ - -## 题目链接 - -https://leetcode-cn.com/problems/word-ladder/ - -## 思路 - -以示例1为例,从这个图中可以看出 hit 到 cog的路线,不止一条,有三条,两条是最短的长度为5,一条长度为6。 - - - -本题只需要求出最短长度就可以了,不用找出路径。 - -所以这道题要解决两个问题: - -* 图中的线是如何连在一起的 -* 起点和终点的最短路径长度 - - -首先题目中并没有给出点与点之间的连线,而是要我们自己去连,条件是字符只能差一个,所以判断点与点之间的关系,要自己判断是不是差一个字符,如果差一个字符,那就是有链接。 - -然后就是求起点和终点的最短路径长度,**这里无向图求最短路,广搜最为合适,广搜只要搜到了终点,那么一定是最短的路径**。因为广搜就是以起点中心向四周扩散的搜索。 - -本题如果用深搜,会非常麻烦。 - -另外需要有一个注意点: - -* 本题是一个无向图,需要用标记位,标记着节点是否走过,否则就会死循环! -* 本题给出集合是数组型的,可以转成set结构,查找更快一些 - -C++代码如下:(详细注释) - -``` -class Solution { -public: - int ladderLength(string beginWord, string endWord, vector& wordList) { - // 将vector转成unordered_set,提高查询速度 - unordered_set wordSet(wordList.begin(), wordList.end()); - // 如果endWord没有在wordSet出现,直接返回0 - if (wordSet.find(endWord) == wordSet.end()) return 0; - // 记录word是否访问过 - unordered_map visitMap; // - // 初始化队列 - queue que; - que.push(beginWord); - // 初始化visitMap - visitMap.insert(pair(beginWord, 1)); - - while(!que.empty()) { - string word = que.front(); - que.pop(); - int path = visitMap[word]; // 这个word的路径长度 - for (int i = 0; i < word.size(); i++) { - string newWord = word; // 用一个新单词替换word,因为每次置换一个字母 - for (int j = 0 ; j < 26; j++) { - newWord[i] = j + 'a'; - if (newWord == endWord) return path + 1; // 找到了end,返回path+1 - // wordSet出现了newWord,并且newWord没有被访问过 - if (wordSet.find(newWord) != wordSet.end() - && visitMap.find(newWord) == visitMap.end()) { - // 添加访问信息 - visitMap.insert(pair(newWord, path + 1)); - que.push(newWord); - } - } - } - } - return 0; - } -}; -``` -> 我是[程序员Carl](https://github.com/youngyangyang04),组队刷题可以找我,本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),期待你的关注! - diff --git a/problems/0129.求根到叶子节点数字之和.md b/problems/0129.求根到叶子节点数字之和.md deleted file mode 100644 index 7d061a43..00000000 --- a/problems/0129.求根到叶子节点数字之和.md +++ /dev/null @@ -1,160 +0,0 @@ -## 链接 -https://leetcode-cn.com/problems/sum-root-to-leaf-numbers/ - -## 思路 - -本题和[113.路径总和II](https://github.com/youngyangyang04/leetcode-master/blob/master/problems/0113.%E8%B7%AF%E5%BE%84%E6%80%BB%E5%92%8CII.md)是类似的思路,做完这道题,可以顺便把[113.路径总和II](https://github.com/youngyangyang04/leetcode-master/blob/master/problems/0113.%E8%B7%AF%E5%BE%84%E6%80%BB%E5%92%8CII.md) 和 [112.路径总和](https://github.com/youngyangyang04/leetcode/blob/master/problems/0112.路径总和.md) 做了。 - -结合112.路径总和 和 113.路径总和II,我在讲了[二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?](https://mp.weixin.qq.com/s/6TWAVjxQ34kVqROWgcRFOg),如果大家对二叉树递归函数什么时候需要返回值很迷茫,可以看一下。 - -接下来在看本题,就简单多了,本题其实需要使用回溯,但一些同学可能都不知道自己用了回溯,在[二叉树:以为使用了递归,其实还隐藏着回溯](https://mp.weixin.qq.com/s/ivLkHzWdhjQQD1rQWe6zWA)中,我详细讲解了二叉树的递归中,如何使用了回溯。 - -接下来我们来看题: - -首先思路很明确,就是要遍历整个树把更节点到叶子节点组成的数字相加。 - -那么先按递归三部曲来分析: - -### 递归三部曲 - -如果对递归三部曲不了解的话,可以看这里:[二叉树:前中后递归详解](https://mp.weixin.qq.com/s/PwVIfxDlT3kRgMASWAMGhA) - -* 确定递归函数返回值及其参数 - -这里我们要遍历整个二叉树,且需要要返回值做逻辑处理,所有返回值为void,在[二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?](https://mp.weixin.qq.com/s/6TWAVjxQ34kVqROWgcRFOg)中,详细讲解了返回值问题。 - -参数只需要把根节点传入,此时还需要定义两个全局遍历,一个是result,记录最终结果,一个是vector path。 - -**为什么用vector类型(就是数组)呢? 因为用vector方便我们做回溯!** - -所以代码如下: - -``` -int result; -vector path; -void traversal(TreeNode* cur) -``` - -* 确定终止条件 - -递归什么时候终止呢? - -当然是遇到叶子节点,此时要收集结果了,通知返回本层递归,因为单条路径的结果使用vector,我们需要一个函数vectorToInt把vector转成int。 - -终止条件代码如下: - -``` -if (!cur->left && !cur->right) { // 遇到了叶子节点 - result += vectorToInt(path); - return; -} -``` - -这里vectorToInt函数就是把数组转成int,代码如下: - -``` -int vectorToInt(const vector& vec) { - int sum = 0; - for (int i = 0; i < vec.size(); i++) { - sum = sum * 10 + vec[i]; - } - return sum; -} -``` - - -* 确定递归单层逻辑 - -本题其实采用前中后序都不无所谓, 因为也没有中间几点的处理逻辑。 - -这里主要是当左节点不为空,path收集路径,并递归左孩子,右节点同理。 - -**但别忘了回溯**。 - -如图: - - - - -代码如下: - -``` - // 中 -if (cur->left) { // 左 (空节点不遍历) - path.push_back(cur->left->val); - traversal(cur->left); // 递归 - path.pop_back(); // 回溯 -} -if (cur->right) { // 右 (空节点不遍历) - path.push_back(cur->right->val); - traversal(cur->right); // 递归 - path.pop_back(); // 回溯 -} -``` - -这里要注意回溯和递归要永远在一起,一个递归,对应一个回溯,是一对一的关系,有的同学写成如下代码: - -``` -if (cur->left) { // 左 (空节点不遍历) - path.push_back(cur->left->val); - traversal(cur->left); // 递归 -} -if (cur->right) { // 右 (空节点不遍历) - path.push_back(cur->right->val); - traversal(cur->right); // 递归 -} -path.pop_back(); // 回溯 -``` -**把回溯放在花括号外面了,世界上最遥远的距离,是你在花括号里,而我在花括号外!** 这就不对了。 - -### 整体C++代码 - -关键逻辑分析完了,整体C++代码如下: - -``` -class Solution { -private: - int result; - vector path; - // 把vector转化为int - int vectorToInt(const vector& vec) { - int sum = 0; - for (int i = 0; i < vec.size(); i++) { - sum = sum * 10 + vec[i]; - } - return sum; - } - void traversal(TreeNode* cur) { - if (!cur->left && !cur->right) { // 遇到了叶子节点 - result += vectorToInt(path); - return; - } - - if (cur->left) { // 左 (空节点不遍历) - path.push_back(cur->left->val); // 处理节点 - traversal(cur->left); // 递归 - path.pop_back(); // 回溯,撤销 - } - if (cur->right) { // 右 (空节点不遍历) - path.push_back(cur->right->val); // 处理节点 - traversal(cur->right); // 递归 - path.pop_back(); // 回溯,撤销 - } - return ; - } -public: - int sumNumbers(TreeNode* root) { - path.clear(); - if (root == nullptr) return 0; - path.push_back(root->val); - traversal(root); - return result; - } -}; -``` - -# 总结 - -过于简洁的代码,很容易让初学者忽视了本题中回溯的精髓,甚至作者本身都没有想清楚自己用了回溯。 - -**我这里提供的代码把整个回溯过程充分体现出来,希望可以帮助大家看的明明白白!** diff --git a/problems/0131.分割回文串.md b/problems/0131.分割回文串.md deleted file mode 100644 index ee2e6079..00000000 --- a/problems/0131.分割回文串.md +++ /dev/null @@ -1,240 +0,0 @@ - -> 切割问题其实是一种组合问题! - -# 131.分割回文串 - -题目链接:https://leetcode-cn.com/problems/palindrome-partitioning/ - -给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。 - -返回 s 所有可能的分割方案。 - -示例: -输入: "aab" -输出: -[ - ["aa","b"], - ["a","a","b"] -] - - -# 思路 - -本题这涉及到两个关键问题: - -1. 切割问题,有不同的切割方式 -2. 判断回文 - -相信这里不同的切割方式可以搞懵很多同学了。 - -这种题目,想用for循环暴力解法,可能都不那么容易写出来,所以要换一种暴力的方式,就是回溯。 - -一些同学可能想不清楚 回溯究竟是如果切割字符串呢? - -我们来分析一下切割,**其实切割问题类似组合问题**。 - -例如对于字符串abcdef: - -* 组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中在选组第三个.....。 -* 切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中在切割第三段.....。 - -感受出来了不? - -所以切割问题,也可以抽象为一颗树形结构,如图: - -![131.分割回文串](https://img-blog.csdnimg.cn/20201123203228309.png) - -递归用来纵向遍历,for循环用来横向遍历,切割线(就是图中的红线)切割到字符串的结尾位置,说明找到了一个切割方法。 - -此时可以发现,切割问题的回溯搜索的过程和组合问题的回溯搜索的过程是差不多的。 - -## 回溯三部曲 - -* 递归函数参数 - -全局变量数组path存放切割后回文的子串,二维数组result存放结果集。 (这两个参数可以放到函数参数里) - -本题递归函数参数还需要startIndex,因为切割过的地方,不能重复切割,和组合问题也是保持一致的。 - -在[回溯算法:求组合总和(二)](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)中我们深入探讨了组合问题什么时候需要startIndex,什么时候不需要startIndex。 - -代码如下: - -``` -vector> result; -vector path; // 放已经回文的子串 -void backtracking (const string& s, int startIndex) { -``` - -* 递归函数终止条件 - -![131.分割回文串](https://img-blog.csdnimg.cn/20201123203228309.png) - -从树形结构的图中可以看出:切割线切到了字符串最后面,说明找到了一种切割方法,此时就是本层递归的终止终止条件。 - -**那么在代码里什么是切割线呢?** - -在处理组合问题的时候,递归参数需要传入startIndex,表示下一轮递归遍历的起始位置,这个startIndex就是切割线。 - -所以终止条件代码如下: - -``` -void backtracking (const string& s, int startIndex) { - // 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了 - if (startIndex >= s.size()) { - result.push_back(path); - return; - } -} -``` - -* 单层搜索的逻辑 - -**来看看在递归循环,中如何截取子串呢?** - -在`for (int i = startIndex; i < s.size(); i++)`循环中,我们 定义了起始位置startIndex,那么 [startIndex, i] 就是要截取的子串。 - -首先判断这个子串是不是回文,如果是回文,就加入在`vector path`中,path用来记录切割过的回文子串。 - -代码如下: - -``` -for (int i = startIndex; i < s.size(); i++) { - if (isPalindrome(s, startIndex, i)) { // 是回文子串 - // 获取[startIndex,i]在s中的子串 - string str = s.substr(startIndex, i - startIndex + 1); - path.push_back(str); - } else { // 如果不是则直接跳过 - continue; - } - backtracking(s, i + 1); // 寻找i+1为起始位置的子串 - path.pop_back(); // 回溯过程,弹出本次已经填在的子串 -} -``` - -**注意切割过的位置,不能重复切割,所以,backtracking(s, i + 1); 传入下一层的起始位置为i + 1**。 - -## 判断回文子串 - -最后我们看一下回文子串要如何判断了,判断一个字符串是否是回文。 - -可以使用双指针法,一个指针从前向后,一个指针从后先前,如果前后指针所指向的元素是相等的,就是回文字符串了。 - -那么判断回文的C++代码如下: - -```C++ - bool isPalindrome(const string& s, int start, int end) { - for (int i = start, j = end; i < j; i++, j--) { - if (s[i] != s[j]) { - return false; - } - } - return true; - } -``` - -如果大家对双指针法有生疏了,传送门:[双指针法:总结篇!](https://mp.weixin.qq.com/s/_p7grwjISfMh0U65uOyCjA) - -此时关键代码已经讲解完毕,整体代码如下(详细注释了) - -# C++整体代码 - -根据Carl给出的回溯算法模板: - -``` -void backtracking(参数) { - if (终止条件) { - 存放结果; - return; - } - - for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { - 处理节点; - backtracking(路径,选择列表); // 递归 - 回溯,撤销处理结果 - } -} - -``` - -不难写出如下代码: - -```C++ -class Solution { -private: - vector> result; - vector path; // 放已经回文的子串 - void backtracking (const string& s, int startIndex) { - // 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了 - if (startIndex >= s.size()) { - result.push_back(path); - return; - } - for (int i = startIndex; i < s.size(); i++) { - if (isPalindrome(s, startIndex, i)) { // 是回文子串 - // 获取[startIndex,i]在s中的子串 - string str = s.substr(startIndex, i - startIndex + 1); - path.push_back(str); - } else { // 不是回文,跳过 - continue; - } - backtracking(s, i + 1); // 寻找i+1为起始位置的子串 - path.pop_back(); // 回溯过程,弹出本次已经填在的子串 - } - } - bool isPalindrome(const string& s, int start, int end) { - for (int i = start, j = end; i < j; i++, j--) { - if (s[i] != s[j]) { - return false; - } - } - return true; - } -public: - vector> partition(string s) { - result.clear(); - path.clear(); - backtracking(s, 0); - return result; - } -}; -``` - -# 总结 - -这道题目在leetcode上是中等,但可以说是hard的题目了,但是代码其实就是按照模板的样子来的。 - -那么难究竟难在什么地方呢? - -**我列出如下几个难点:** - -* 切割问题可以抽象为组合问题 -* 如何模拟那些切割线 -* 切割问题中递归如何终止 -* 在递归循环中如何截取子串 -* 如何判断回文 - -**我们平时在做难题的时候,总结出来难究竟难在哪里也是一种需要锻炼的能力**。 - -一些同学可能遇到题目比较难,但是不知道题目难在哪里,反正就是很难。其实这样还是思维不够清晰,这种总结的能力需要多接触多锻炼。 - -**本题我相信很多同学主要卡在了第一个难点上:就是不知道如何切割,甚至知道要用回溯法,也不知道如何用。也就是没有体会到按照求组合问题的套路就可以解决切割**。 - -如果意识到这一点,算是重大突破了。接下来就可以对着模板照葫芦画瓢。 - -**但接下来如何模拟切割线,如何终止,如何截取子串,其实都不好想,最后判断回文算是最简单的了**。 - -除了这些难点,**本题还有细节,例如:切割过的地方不能重复切割所以递归函数需要传入i + 1**。 - -所以本题应该是一个道hard题目了。 - -**可能刷过这道题目的录友都没感受到自己原来克服了这么多难点,就把这道题目AC了**,这应该叫做无招胜有招,人码合一,哈哈哈。 - -当然,本题131.分割回文串还可以用暴力搜索一波,132.分割回文串II和1278.分割回文串III 爆搜就会超时,需要使用动态规划了,我们会在动态规划系列中,详细讲解! - -**就酱,如果感觉「代码随想录」不错,就把Carl宣传一波吧!** - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/0134.加油站.md b/problems/0134.加油站.md deleted file mode 100644 index 491dc61a..00000000 --- a/problems/0134.加油站.md +++ /dev/null @@ -1,198 +0,0 @@ -今天开始继续贪心题目系列,让大家久等啦! - -# 134. 加油站 - -题目链接:https://leetcode-cn.com/problems/gas-station/ - -在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。 - -你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。 - -如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。 - -说明:  - -* 如果题目有解,该答案即为唯一答案。 -* 输入数组均为非空数组,且长度相同。 -* 输入数组中的元素均为非负数。 - -示例 1: -输入: -gas = [1,2,3,4,5] -cost = [3,4,5,1,2] - -输出: 3 -解释: -从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油 -开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油 -开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油 -开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油 -开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油 -开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。 -因此,3 可为起始索引。 - -示例 2: -输入: -gas = [2,3,4] -cost = [3,4,3] - -输出: -1 -解释: -你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。 -我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油 -开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油 -开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油 -你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。 -因此,无论怎样,你都不可能绕环路行驶一周。 - -# 思路 - -## 暴力方法 - -暴力的方法很明显就是O(n^2)的,遍历每一个加油站为起点的情况,模拟一圈。 - -如果跑了一圈,中途没有断油,而且最后油量大于等于0,说明这个起点是ok的。 - -暴力的方法思路比较简单,但代码写起来也不是很容易,关键是要模拟跑一圈的过程。 - -**for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历,要善于使用while!** - -C++代码如下: - -``` -class Solution { -public: - int canCompleteCircuit(vector& gas, vector& cost) { - for (int i = 0; i < cost.size(); i++) { - int rest = gas[i] - cost[i]; // 记录剩余油量 - int index = (i + 1) % cost.size(); - while (rest > 0 && index != i) { // 模拟以i为起点行驶一圈 - rest += gas[index] - cost[index]; - index = (index + 1) % cost.size(); - } - // 如果以i为起点跑一圈,剩余油量>=0,返回该起始位置 - if (rest >= 0 && index == i) return i; - } - return -1; - } -}; -``` -* 时间复杂度O(n^2) -* 空间复杂度O(n) - -C++暴力解法在leetcode上提交也可以过。 - -## 贪心算法(方法一) - -直接从全局进行贪心选择,情况如下: - -* 情况一:如果gas的总和小于cost总和,那么无论从哪里出发,一定是跑不了一圈的 -* 情况二:rest[i] = gas[i]-cost[i]为一天剩下的油,i从0开始计算累加到最后一站,如果累加没有出现负数,说明从0出发,油就没有断过,那么0就是起点。 - -* 情况三:如果累加的最小值是负数,汽车就要从非0节点出发,从后向前,看哪个节点能这个负数填平,能把这个负数填平的节点就是出发节点。 - -C++代码如下: - -```C++ -class Solution { -public: - int canCompleteCircuit(vector& gas, vector& cost) { - int curSum = 0; - int min = INT_MAX; // 从起点出发,油箱里的油量最小值 - for (int i = 0; i < gas.size(); i++) { - int rest = gas[i] - cost[i]; - curSum += rest; - if (curSum < min) { - min = curSum; - } - } - if (curSum < 0) return -1; // 情况1 - if (min >= 0) return 0; // 情况2 - // 情况3 - for (int i = gas.size() - 1; i >= 0; i--) { - int rest = gas[i] - cost[i]; - min += rest; - if (min >= 0) { - return i; - } - } - return -1; - } -}; -``` -* 时间复杂度:O(n) -* 空间复杂度:O(1) - -**其实我不认为这种方式是贪心算法,因为没有找出局部最优,而是直接从全局最优的角度上思考问题**。 - -但这种解法又说不出是什么方法,这就是一个从全局角度选取最优解的模拟操作。 - -所以对于本解法是贪心,我持保留意见! - -但不管怎么说,解法毕竟还是巧妙的,不用过于执着于其名字称呼。 - -## 贪心算法(方法二) - -可以换一个思路,首先如果总油量减去总消耗大于等于零那么一定可以跑完一圈,说明 各个站点的加油站 剩油量rest[i]相加一定是大于等于零的。 - -每个加油站的剩余量rest[i]为gas[i] - cost[i]。 - -i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,起始位置从i+1算起,再从0计算curSum。 - -如图: -![134.加油站](https://img-blog.csdnimg.cn/20201213162821958.png) - -那么为什么一旦[i,j] 区间和为负数,起始位置就可以是j+1呢,j+1后面就不会出现更大的负数? - -如果出现更大的负数,就是更新j,那么起始位置又变成新的j+1了。 - -而且j之前出现了多少负数,j后面就会出现多少正数,因为耗油总和是大于零的(前提我们已经确定了一定可以跑完全程)。 - -**那么局部最优:当前累加rest[j]的和curSum一旦小于0,起始位置至少要是j+1,因为从j开始一定不行。全局最优:找到可以跑一圈的起始位置**。 - -局部最优可以推出全局最优,找不出反例,试试贪心! - -C++代码如下: - -```C++ -class Solution { -public: - int canCompleteCircuit(vector& gas, vector& cost) { - int curSum = 0; - int totalSum = 0; - int start = 0; - for (int i = 0; i < gas.size(); i++) { - curSum += gas[i] - cost[i]; - totalSum += gas[i] - cost[i]; - if (curSum < 0) { // 当前累加rest[i]和 curSum一旦小于0 - start = i + 1; // 起始位置更新为i+1 - curSum = 0; // curSum从0开始 - } - } - if (totalSum < 0) return -1; // 说明怎么走都不可能跑一圈了 - return start; - } -}; -``` -* 时间复杂度:O(n) -* 空间复杂度:O(1) - -**说这种解法为贪心算法,才是是有理有据的,因为全局最优解是根据局部最优推导出来的**。 - -# 总结 - -对于本题首先给出了暴力解法,暴力解法模拟跑一圈的过程其实比较考验代码技巧的,要对while使用的很熟练。 - -然后给出了两种贪心算法,对于第一种贪心方法,其实我认为就是一种直接从全局选取最优的模拟操作,思路还是好巧妙的,值得学习一下。 - -对于第二种贪心方法,才真正体现出贪心的精髓,用局部最优可以推出全局最优,进而求得起始位置。 - -就酱,「代码随想录」值得推荐给身边每一位学习算法的同学朋友,很多录友关注后都感觉相见恨晚! - - -> **我是[程序员Carl](https://github.com/youngyangyang04),[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png)可以找我,本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** - - - diff --git a/problems/0135.分发糖果.md b/problems/0135.分发糖果.md deleted file mode 100644 index 80fbf596..00000000 --- a/problems/0135.分发糖果.md +++ /dev/null @@ -1,128 +0,0 @@ -> 好了 - - -# 135. 分发糖果 - -链接:https://leetcode-cn.com/problems/candy/ - -老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。 - -你需要按照以下要求,帮助老师给这些孩子分发糖果: - -* 每个孩子至少分配到 1 个糖果。 -* 相邻的孩子中,评分高的孩子必须获得更多的糖果。 - -那么这样下来,老师至少需要准备多少颗糖果呢? - -示例 1: -输入: [1,0,2] -输出: 5 -解释: 你可以分别给这三个孩子分发 2、1、2 颗糖果。 - -示例 2: -输入: [1,2,2] -输出: 4 -解释: 你可以分别给这三个孩子分发 1、2、1 颗糖果。 -第三个孩子只得到 1 颗糖果,这已满足上述两个条件。 - - -# 思路 - -这道题目一定是要确定一边之后,再确定另一边,例如比较每一个孩子的左边,然后再比较右边,**如果两边一起考虑一定会顾此失彼**。 - - -先确定右边评分大于左边的情况(也就是从前向后遍历) - -此时局部最优:只要右边评分比左边大,右边的孩子就多一个糖果,全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果 - -局部最优可以推出全局最优。 - -如果ratings[i] > ratings[i - 1] 那么[i]的糖 一定要比[i - 1]的糖多一个,所以贪心:candyVec[i] = candyVec[i - 1] + 1 - -代码如下: - -```C++ -// 从前向后 -for (int i = 1; i < ratings.size(); i++) { - if (ratings[i] > ratings[i - 1]) candyVec[i] = candyVec[i - 1] + 1; -} -``` - -如图: - -![135.分发糖果](https://img-blog.csdnimg.cn/20201117114916878.png) - -再确定左孩子大于右孩子的情况(从后向前遍历) - -遍历顺序这里有同学可能会有疑问,为什么不能从前向后遍历呢? - -因为如果从前向后遍历,根据 ratings[i + 1] 来确定 ratings[i] 对应的糖果,那么每次都不能利用上前一次的比较结果了。 - -**所以确定左孩子大于右孩子的情况一定要从后向前遍历!** - -如果 ratings[i] > ratings[i + 1],此时candyVec[i](第i个小孩的糖果数量)就有两个选择了,一个是candyVec[i + 1] + 1(从右边这个加1得到的糖果数量),一个是candyVec[i](之前比较右孩子大于左孩子得到的糖果数量)。 - -那么又要贪心了,局部最优:取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,保证第i个小孩的糖果数量即大于左边的也大于右边的。全局最优:相邻的孩子中,评分高的孩子获得更多的糖果。 - -局部最优可以推出全局最优。 - -所以就取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,**candyVec[i]只有取最大的才能既保持对左边candyVec[i - 1]的糖果多,也比右边candyVec[i + 1]的糖果多**。 - -如图: - -![135.分发糖果1](https://img-blog.csdnimg.cn/20201117115658791.png) - -所以该过程代码如下: - -```C++ -// 从后向前 -for (int i = ratings.size() - 2; i >= 0; i--) { - if (ratings[i] > ratings[i + 1] ) { - candyVec[i] = max(candyVec[i], candyVec[i + 1] + 1); - } -} -``` - -整体代码如下: -```C++ -class Solution { -public: - int candy(vector& ratings) { - vector candyVec(ratings.size(), 1); - // 从前向后 - for (int i = 1; i < ratings.size(); i++) { - if (ratings[i] > ratings[i - 1]) candyVec[i] = candyVec[i - 1] + 1; - } - // 从后向前 - for (int i = ratings.size() - 2; i >= 0; i--) { - if (ratings[i] > ratings[i + 1] ) { - candyVec[i] = max(candyVec[i], candyVec[i + 1] + 1); - } - } - // 统计结果 - int result = 0; - for (int i = 0; i < candyVec.size(); i++) result += candyVec[i]; - return result; - } -}; -``` - -# 总结 - -这在leetcode上是一道困难的题目,其难点就在于贪心的策略,如果在考虑局部的时候想两边兼顾,就会顾此失彼。 - -那么本题我采用了两次贪心的策略: - -* 一次是从左到右遍历,只比较右边孩子评分比左边大的情况。 -* 一次是从右到左遍历,只比较左边孩子评分比右边大的情况。 - -这样从局部最优推出了全局最优,即:相邻的孩子中,评分高的孩子获得更多的糖果。 - -就酱,如果感觉「代码随想录」干货满满,就推荐给身边的朋友同学们吧,关注后就会发现相见恨晚! - - -> **我是[程序员Carl](https://github.com/youngyangyang04),[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png)可以找我,本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** - - diff --git a/problems/0139.单词拆分.md b/problems/0139.单词拆分.md deleted file mode 100644 index 31d67ac5..00000000 --- a/problems/0139.单词拆分.md +++ /dev/null @@ -1,122 +0,0 @@ - -[回溯算法:分割回文串](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q) - -回溯法代码: -``` -class Solution { -private: - bool backtracking (const string& s, const unordered_set& wordSet, int startIndex) { - if (startIndex >= s.size()) { - return true; - } - for (int i = startIndex; i < s.size(); i++) { - string word = s.substr(startIndex, i - startIndex + 1); - if (wordSet.find(word) != wordSet.end() && backtracking(s, wordSet, i + 1)) { - return true; - } - } - return false; - } -public: - bool wordBreak(string s, vector& wordDict) { - unordered_set wordSet(wordDict.begin(), wordDict.end()); - return backtracking(s, wordSet, 0); - } -}; -``` - -``` -"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab" -["a","aa","aaa","aaaa","aaaaa","aaaaaa","aaaaaaa","aaaaaaaa","aaaaaaaaa","aaaaaaaaaa"] -``` - -可以使用一个一维数组保存一下,递归过程中计算的结果,C++代码如下: - -使用memory数组保存 每次计算的以startIndex起始的计算结果,如果memory[startIndex]里已经被赋值了,直接用memory[startIndex]的结果。 -``` -class Solution { -private: - bool backtracking (const string& s, - const unordered_set& wordSet, - vector& memory, - int startIndex) { - if (startIndex >= s.size()) { - return true; - } - // 如果memory[startIndex]不是初始值了,直接使用memory[startIndex]的结果 - if (memory[startIndex] != -1) return memory[startIndex]; - for (int i = startIndex; i < s.size(); i++) { - string word = s.substr(startIndex, i - startIndex + 1); - if (wordSet.find(word) != wordSet.end() && backtracking(s, wordSet, memory, i + 1)) { - memory[startIndex] = 1; // 记录以startIndex开始的子串是可以被拆分的 - return true; - } - } - memory[startIndex] = 0; // 记录以startIndex开始的子串是不可以被拆分的 - return false; - } -public: - bool wordBreak(string s, vector& wordDict) { - unordered_set wordSet(wordDict.begin(), wordDict.end()); - vector memory(s.size(), -1); // -1 表示初始化状态 - return backtracking(s, wordSet, memory, 0); - } -}; -``` - -# 背包 - -* 确定dp数组以及下标的含义 - -dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词 - -* 确定递推公式 - -如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true。(j < i ) - -所以递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j] ==true) 那么 dp[i] = true - - -* dp数组如何初始化 - -从递归公式中可以看出,dp[i] 的状态依靠 dp[j]是否为true,那么dp[0]就是递归的根基,dp[0]一定要为true,否则递归下去后面都都是false了。 - -同时也表示如果字符串为空的话,说明出现在字典里。 - -下标非0的dp[i]初始化为false,只要没有被覆盖说明都是不可拆分为一个或多个在字典中出现的单词。 - -* 确定遍历顺序 - -题目中说是拆分为一个或多个在字典中出现的单词,所以这是完全背包。 - -同时也说明出现的单词集合是组合还是排列,并不在意,最终要求的是 是否都出现过。 - -所以本题使用求排列的方式,还是求组合的方式都可以。 - -我采用的求排列的方式,所以遍历顺序:target放在外循环,将nums放在内循环。内循环从前到后。 - -C++代码如下: - -```C++ -class Solution { -public: - bool wordBreak(string s, vector& wordDict) { - unordered_set wordSet(wordDict.begin(), wordDict.end()); - vector dp(s.size() + 1, false); - dp[0] = true; - for (int i = 1; i <= s.size(); i++) { - for (int j = 0; j < i; j++) { - string word = s.substr(j, i - j); //substr(起始位置,截取的个数) - if (wordSet.find(word) != wordSet.end() && dp[j]) { - dp[i] = true; - } - } - //for (int k = 0; k <=i; k++) cout << dp[k] << " "; - //cout << endl; - } - return dp[s.size()]; - } -}; -``` -* 时间复杂度:O(n^3),因为substr返回子串的副本是O(n)的复杂度(这里的n是substring的长度) -* 空间复杂度:O(n) diff --git a/problems/0141.环形链表.md b/problems/0141.环形链表.md deleted file mode 100644 index bdb12c5b..00000000 --- a/problems/0141.环形链表.md +++ /dev/null @@ -1,50 +0,0 @@ - -## 思路 - -可以使用快慢指针法, 分别定义 fast 和 slow指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。 - -为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢? - -首先第一点: **fast指针一定先进入环中,如果fast 指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。** - -那么来看一下,**为什么fast指针和slow指针一定会相遇呢?** - -可以画一个环,然后让 fast指针在任意一个节点开始追赶slow指针。 - -会发现最终都是这种情况, 如下图: - - - -fast和slow各自再走一步, fast和slow就相遇了 - -这是因为fast是走两步,slow是走一步,**其实相对于slow来说,fast是一个节点一个节点的靠近slow的**,所以fast一定可以和slow重合。 - -动画如下: - - - - - -## C++代码如下 - -``` -class Solution { -public: - bool hasCycle(ListNode *head) { - ListNode* fast = head; - ListNode* slow = head; - while(fast != NULL && fast->next != NULL) { - slow = slow->next; - fast = fast->next->next; - // 快慢指针相遇,说明有环 - if (slow == fast) return true; - } - return false; - } -}; -``` -## 扩展 - -做完这道题目,可以在做做142.环形链表II,不仅仅要找环,还要找环的入口。 - -142.环形链表II题解:[链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA) diff --git a/problems/0142.环形链表II.md b/problems/0142.环形链表II.md deleted file mode 100644 index 48aaed93..00000000 --- a/problems/0142.环形链表II.md +++ /dev/null @@ -1,177 +0,0 @@ -> 找到有没有环已经很不容易了,还要让我找到环的入口? - -# 题目地址 -https://leetcode-cn.com/problems/linked-list-cycle-ii/ - - -# 第142题.环形链表II - -题意: -给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。 - -为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。 - -**说明**:不允许修改给定的链表。 - -![循环链表](https://img-blog.csdnimg.cn/20200816110112704.png) - -# 思路 - -这道题目,不仅考察对链表的操作,而且还需要一些数学运算。 - -主要考察两知识点: - -* 判断链表是否环 -* 如果有环,如何找到这个环的入口 - -## 判断链表是否有环 - -可以使用快慢指针法, 分别定义 fast 和 slow指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。 - -为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢 - -首先第一点: **fast指针一定先进入环中,如果fast 指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。** - -那么来看一下,**为什么fast指针和slow指针一定会相遇呢?** - -可以画一个环,然后让 fast指针在任意一个节点开始追赶slow指针。 - -会发现最终都是这种情况, 如下图: - - - -fast和slow各自再走一步, fast和slow就相遇了 - -这是因为fast是走两步,slow是走一步,**其实相对于slow来说,fast是一个节点一个节点的靠近slow的**,所以fast一定可以和slow重合。 - -动画如下: - - - - - -## 如果有环,如何找到这个环的入口 - -**此时已经可以判断链表是否有环了,那么接下来要找这个环的入口了。** - -假设从头结点到环形入口节点 的节点数为x。 -环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 -从相遇节点 再到环形入口节点节点数为 z。 如图所示: - - - -那么相遇时: -slow指针走过的节点数为: `x + y`, -fast指针走过的节点数:` x + y + n (y + z)`,n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数A。 - -因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2: - -`(x + y) * 2 = x + y + n (y + z)` - -两边消掉一个(x+y): `x + y = n (y + z) ` - -因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。 - -所以要求x ,将x单独放在左面:`x = n (y + z) - y` , - -再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:`x = (n - 1) (y + z) + z ` 注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。 - -这个公式说明什么呢? - -先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。 - -当 n为1的时候,公式就化解为 `x = z`, - -这就意味着,**从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点**。 - - -也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。 - -让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。 - -动画如下: - - - - -那么 n如果大于1是什么情况呢,就是fast指针在环形转n圈之后才遇到 slow指针。 - -其实这种情况和n为1的时候 效果是一样的,一样可以通过这个方法找到 环形的入口节点,只不过,index1 指针在环里 多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。 - - -# C++代码 - -``` -/** - * Definition for singly-linked list. - * struct ListNode { - * int val; - * ListNode *next; - * ListNode(int x) : val(x), next(NULL) {} - * }; - */ -class Solution { -public: - ListNode *detectCycle(ListNode *head) { - ListNode* fast = head; - ListNode* slow = head; - while(fast != NULL && fast->next != NULL) { - slow = slow->next; - fast = fast->next->next; - // 快慢指针相遇,此时从head 和 相遇点,同时查找直至相遇 - if (slow == fast) { - ListNode* index1 = fast; - ListNode* index2 = head; - while (index1 != index2) { - index1 = index1->next; - index2 = index2->next; - } - return index2; // 返回环的入口 - } - } - return NULL; - } -}; -``` - -## 补充 - -在推理过程中,大家可能有一个疑问就是:**为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y 呢?** - -即文章[链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA)中如下的地方: - - - - -首先slow进环的时候,fast一定是先进环来了。 - -如果slow进环入口,fast也在环入口,那么把这个环展开成直线,就是如下图的样子: - - - -可以看出如果slow 和 fast同时在环入口开始走,一定会在环入口3相遇,slow走了一圈,fast走了两圈。 - -重点来了,slow进环的时候,fast一定是在环的任意一个位置,如图: - - - -那么fast指针走到环入口3的时候,已经走了k + n 个节点,slow相应的应该走了(k + n) / 2 个节点。 - -因为k是小于n的(图中可以看出),所以(k + n) / 2 一定小于n。 - -**也就是说slow一定没有走到环入口3,而fast已经到环入口3了**。 - -这说明什么呢? - -**在slow开始走的那一环已经和fast相遇了**。 - -那有同学又说了,为什么fast不能跳过去呢? 在刚刚已经说过一次了,**fast相对于slow是一次移动一个节点,所以不可能跳过去**。 - -好了,这次把为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y ,用数学推理了一下,算是对[链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA)的补充。 - -# 总结 - -这次可以说把环形链表这道题目的各个细节,完完整整的证明了一遍,说这是全网最详细讲解不为过吧,哈哈。 - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0143.重排链表.md b/problems/0143.重排链表.md deleted file mode 100644 index 9011692b..00000000 --- a/problems/0143.重排链表.md +++ /dev/null @@ -1,161 +0,0 @@ - -# 思路 - -本篇将给出三种C++实现的方法 - -* 数组模拟 -* 双向队列模拟 -* 直接分割链表 - -## 方法一 - -把链表放进数组中,然后通过双指针法,一前一后,来遍历数组,构造链表。 - -代码如下: - -``` -class Solution { -public: - void reorderList(ListNode* head) { - vector vec; - ListNode* cur = head; - if (cur == nullptr) return; - while(cur != nullptr) { - vec.push_back(cur); - cur = cur->next; - } - cur = head; - int i = 1; - int j = vec.size() - 1; // i j为之前前后的双指针 - int count = 0; // 计数,偶数去后面,奇数取前面 - while (i <= j) { - if (count % 2 == 0) { - cur->next = vec[j]; - j--; - } else { - cur->next = vec[i]; - i++; - } - cur = cur->next; - count++; - } - if (vec.size() % 2 == 0) { // 如果是偶数,还要多处理中间的一个 - cur->next = vec[i]; - cur = cur->next; - } - cur->next = nullptr; // 注意结尾 - } -}; -``` - -## 方法二 - -把链表放进双向队列,然后通过双向队列一前一后弹出数据,来构造新的链表。这种方法比操作数组容易一些,不用双指针模拟一前一后了 -``` -class Solution { -public: - void reorderList(ListNode* head) { - deque que; - ListNode* cur = head; - if (cur == nullptr) return; - - while(cur->next != nullptr) { - que.push_back(cur->next); - cur = cur->next; - } - - cur = head; - int count = 0; // 计数,偶数去后面,奇数取前面 - ListNode* node; - while(que.size()) { - if (count % 2 == 0) { - node = que.back(); - que.pop_back(); - } else { - node = que.front(); - que.pop_front(); - } - count++; - cur->next = node; - cur = cur->next; - } - cur->next = nullptr; // 注意结尾 - } -}; -``` - -## 方法三 - -将链表分割成两个链表,然后把第二个链表反转,之后在通过两个链表拼接成新的链表。 - -如图: - - - -这种方法,比较难,平均切割链表,看上去很简单,真正代码写的时候有很多细节,同时两个链表最后拼装整一个新的链表也有一些细节需要注意! - -代码如下: - -``` -class Solution { -private: - // 反转链表 - ListNode* reverseList(ListNode* head) { - ListNode* temp; // 保存cur的下一个节点 - ListNode* cur = head; - ListNode* pre = NULL; - while(cur) { - temp = cur->next; // 保存一下 cur的下一个节点,因为接下来要改变cur->next - cur->next = pre; // 翻转操作 - // 更新pre 和 cur指针 - pre = cur; - cur = temp; - } - return pre; - } - -public: - void reorderList(ListNode* head) { - if (head == nullptr) return; - // 使用快慢指针法,将链表分成长度均等的两个链表head1和head2 - // 如果总链表长度为奇数,则head1相对head2多一个节点 - ListNode* fast = head; - ListNode* slow = head; - while (fast && fast->next && fast->next->next) { - fast = fast->next->next; - slow = slow->next; - } - ListNode* head1 = head; - ListNode* head2; - head2 = slow->next; - slow->next = nullptr; - - // 对head2进行翻转 - head2 = reverseList(head2); - - // 将head1和head2交替生成新的链表head - ListNode* cur1 = head1; - ListNode* cur2 = head2; - ListNode* cur = head; - cur1 = cur1->next; - int count = 0; // 偶数取head2的元素,奇数取head1的元素 - while (cur1 && cur2) { - if (count % 2 == 0) { - cur->next = cur2; - cur2 = cur2->next; - } else { - cur->next = cur1; - cur1 = cur1->next; - } - count++; - cur = cur->next; - } - if (cur2 != nullptr) { // 处理结尾 - cur->next = cur2; - } - if (cur1 != nullptr) { - cur->next = cur1; - } - } -}; -``` diff --git a/problems/0144.二叉树的前序遍历.md b/problems/0144.二叉树的前序遍历.md deleted file mode 100644 index 7e2e1260..00000000 --- a/problems/0144.二叉树的前序遍历.md +++ /dev/null @@ -1,387 +0,0 @@ -# 题目地址 -https://leetcode-cn.com/problems/binary-tree-preorder-traversal/ - -# 思路 -这篇文章,**彻底讲清楚应该如何写递归,并给出了前中后序三种不同的迭代法,然后分析迭代法的代码风格为什么没有统一,最后给出统一的前中后序迭代法的代码,帮大家彻底吃透二叉树的深度优先遍历。** - -对二叉树基础理论还不清楚的话,可以看看这个[关于二叉树,你该了解这些!](https://mp.weixin.qq.com/s/_ymfWYvTNd2GvWvC5HOE4A)。 - -[二叉树:一入递归深似海,从此offer是路人](https://mp.weixin.qq.com/s/PwVIfxDlT3kRgMASWAMGhA)介绍了二叉树的前后中序的递归遍历方式。 - -[二叉树:听说递归能做的,栈也能做!](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg)介绍了二叉树的前后中序迭代写法。 - -[二叉树:前中后序迭代方式的写法就不能统一一下么?](https://mp.weixin.qq.com/s/WKg0Ty1_3SZkztpHubZPRg) 介绍了二叉树前中后迭代方式的统一写法。 - -以下开始开始正文: - -* 二叉树深度优先遍历 - * 前序遍历: [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) - -这几道题目建议大家都做一下,本题解先只写二叉树深度优先遍历,二叉树广度优先遍历请看题解[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; - } -}; - -``` - -此时我们实现了前后中遍历的三种迭代法,**是不是发现迭代法实现的先中后序,其实风格也不是那么统一,除了先序和后序,有关联,中序完全就是另一个风格了,一会用栈遍历,一会又用指针来遍历。** - -**重头戏来了,接下来介绍一下统一写法。** - -## 前后中遍历迭代法统一的写法 - -我们以中序遍历为例,之前说使用栈的话,**无法同时解决处理过程和访问过程不一致的情况**,那我们就将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记,标记就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 - -中序遍历代码如下: -``` -class Solution { -public: - 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; - } -}; -``` - -看代码有点抽象我们来看一下动画(中序遍历): - - - -前序遍历代码如下: (**注意此时我们仅仅和中序遍历相对改变了两行代码的顺序**) - -``` -class Solution { -public: - 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; - } -}; -``` - -后续遍历代码如下: (**注意此时我们仅仅和中序遍历相对改变了两行代码的顺序**) - -``` -class Solution { -public: - 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; - } -}; -``` - -## C++代码 - -### 递归 -``` -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; - } -}; -``` - -### 栈 -``` -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; - } -}; -``` - -### 栈 通用模板 - -``` -class Solution { -public: - 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; - } -}; -``` - - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0145.二叉树的后序遍历.md b/problems/0145.二叉树的后序遍历.md deleted file mode 100644 index 74727ba6..00000000 --- a/problems/0145.二叉树的后序遍历.md +++ /dev/null @@ -1,88 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/binary-tree-postorder-traversal/ - -## 思路 - -详细题解请看这篇:[一文学通二叉树前中后序递归法与迭代法](https://github.com/youngyangyang04/leetcode/blob/master/problems/0144.二叉树的前序遍历.md) - -## C++代码 - -### 递归 - -``` -class Solution { -public: - void traversal(TreeNode* root, vector& vec) { - if (root == NULL) return; - traversal(root->left, vec); - traversal(root->right, vec); - vec.push_back(root->val); - } - vector postorderTraversal(TreeNode* root) { - vector result; - traversal(root, result); - return result; - - } -}; -``` - -### 栈 - -先中右左遍历,然后再反转 - -``` -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; - } -}; -``` -### 栈 通用模板 - -详细代码注释看 [0094.二叉树的中序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0094.二叉树的中序遍历.md) - -``` -class Solution { -public: - 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; - } -}; -``` - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0147.对链表进行插入排序.md b/problems/0147.对链表进行插入排序.md deleted file mode 100644 index f834e0ba..00000000 --- a/problems/0147.对链表进行插入排序.md +++ /dev/null @@ -1,47 +0,0 @@ - -## 思路 - -这道题目还是很考察链表操作的。 - -如果不用虚拟头结点的话,这道题会很麻烦,对虚拟头结点不熟悉的同学,可以看这篇:[链表:听说用虚拟头节点会方便很多?](https://mp.weixin.qq.com/s/slM1CH5Ew9XzK93YOQYSjA)。 - -如果想整体掌握链表操作的话,看一下这篇[链表大总结](https://mp.weixin.qq.com/s/vK0JjSTHfpAbs8evz5hH8A)。 - -本题在模拟插入排序的过程中,一共要有三次改变节点指针的操作,如果不画一个图,很容易搞蒙了。 - -我举一个排序插入节点2的例子,并详细标注了每一个步骤,晚上一个插入操作有五步,如下: - - - -代码中注释的步骤和图中都是一一对应的,对着图看代码,就比较清晰了,C++代码如下: - -``` -class Solution { -public: - ListNode* insertionSortList(ListNode* head) { - if (head == nullptr) return head; - - ListNode* dummyHead = new ListNode(0); // 定一个虚拟头结点 - ListNode* cur = head; - ListNode* pre = dummyHead; - - while (cur != nullptr) { - while (pre->next != nullptr && pre->next->val < cur->val) { - pre = pre->next; - } - // 在pre和prenext之间插入数据 - ListNode* next = cur->next; // 步骤一:保存curnext - cur->next = pre->next; // 步骤二 - pre->next = cur; // 步骤三 - pre = dummyHead; // 步骤四:pre重新指向虚拟头结点来找下一个插入位置 - cur = next; // 步骤五:cur的前一个节点的下一个节点指向保存的next - } - return dummyHead->next; - } -}; -``` - -> **我是[程序员Carl](https://github.com/youngyangyang04),[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png)可以找我,本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/0150.逆波兰表达式求值.md b/problems/0150.逆波兰表达式求值.md deleted file mode 100644 index dc85b481..00000000 --- a/problems/0150.逆波兰表达式求值.md +++ /dev/null @@ -1,119 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/evaluate-reverse-polish-notation/ - -> 这不仅仅是一道好题,也展现出计算机的思考方式 - -# 150. 逆波兰表达式求值 -根据 逆波兰表示法,求表达式的值。 - -有效的运算符包括 + ,  - ,  * ,  / 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。 - -说明: - -整数除法只保留整数部分。 -给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。 -  - -示例 1: -输入: ["2", "1", "+", "3", " * "] -输出: 9 -解释: 该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9 - -示例 2: -输入: ["4", "13", "5", "/", "+"] -输出: 6 -解释: 该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6 - -示例 3: -输入: ["10", "6", "9", "3", "+", "-11", " * ", "/", " * ", "17", "+", "5", "+"] -输出: 22 -解释: -该算式转化为常见的中缀算术表达式为: - ((10 * (6 / ((9 + 3) * -11))) + 17) + 5 -= ((10 * (6 / (12 * -11))) + 17) + 5 -= ((10 * (6 / -132)) + 17) + 5 -= ((10 * 0) + 17) + 5 -= (0 + 17) + 5 -= 17 + 5 -= 22 -  - -逆波兰表达式:是一种后缀表达式,所谓后缀就是指算符写在后面。 - -平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 。 - -该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * ) 。 - -逆波兰表达式主要有以下两个优点: - -* 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。 - -* 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中。 - -# 思路 - -在上一篇文章中[栈与队列:匹配问题都是栈的强项](https://mp.weixin.qq.com/s/eynAEbUbZoAWrk0ZlEugqg)提到了 递归就是用栈来实现的。 - -所以**栈与递归之间在某种程度上是可以转换的!**这一点我们在后续讲解二叉树的时候,会更详细的讲解到。 - -那么来看一下本题,**其实逆波兰表达式相当于是二叉树中的后序遍历**。 大家可以把运算符作为中间节点,按照后序遍历的规则画出一个二叉树。 - -但我们没有必要从二叉树的角度去解决这个问题,只要知道逆波兰表达式是用后续遍历的方式把二叉树序列化了,就可以了。 - -在进一步看,本题中每一个子表达式要得出一个结果,然后拿这个结果再进行运算,那么**这岂不就是一个相邻字符串消除的过程,和[栈与队列:匹配问题都是栈的强项](https://mp.weixin.qq.com/s/eynAEbUbZoAWrk0ZlEugqg)中的对对碰游戏是不是就非常像了。** - -如动画所示: - - -相信看完动画大家应该知道,这和[1047. 删除字符串中的所有相邻重复项](https://mp.weixin.qq.com/s/eynAEbUbZoAWrk0ZlEugqg)是差不错的,只不过本题不要相邻元素做消除了,而是做运算! - -代码如下: - -## C++代码 - -``` -class Solution { -public: - int evalRPN(vector& tokens) { - stack st; - for (int i = 0; i < tokens.size(); i++) { - if (tokens[i] == "+" || tokens[i] == "-" || tokens[i] == "*" || tokens[i] == "/") { - int num1 = st.top(); - st.pop(); - int num2 = st.top(); - st.pop(); - if (tokens[i] == "+") st.push(num2 + num1); - if (tokens[i] == "-") st.push(num2 - num1); - if (tokens[i] == "*") st.push(num2 * num1); - if (tokens[i] == "/") st.push(num2 / num1); - } else { - st.push(stoi(tokens[i])); - } - } - int result = st.top(); - st.pop(); // 把栈里最后一个元素弹出(其实不弹出也没事) - return result; - } -}; -``` - -# 题外话 - -我们习惯看到的表达式都是中缀表达式,因为符合我们的习惯,但是中缀表达式对于计算机来说就不是很友好了。 - -例如:4 + 13 / 5,这就是中缀表达式,计算机从左到右去扫描的话,扫到13,还要判断13后面是什么运算法,还要比较一下优先级,然后13还和后面的5做运算,做完运算之后,还要向前回退到 4 的位置,继续做加法,你说麻不麻烦! - -那么将中缀表达式,转化为后缀表达式之后:["4", "13", "5", "/", "+"] ,就不一样了,计算机可以利用栈里顺序处理,不需要考虑优先级了。也不用回退了, **所以后缀表达式对计算机来说是非常友好的。** - -可以说本题不仅仅是一道好题,也展现出计算机的思考方式。 - -在1970年代和1980年代,惠普在其所有台式和手持式计算器中都使用了RPN(后缀表达式),直到2020年代仍在某些模型中使用了RPN。 - -参考维基百科如下: - -> During the 1970s and 1980s, Hewlett-Packard used RPN in all of their desktop and hand-held calculators, and continued to use it in some models into the 2020s. - - - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0151.翻转字符串里的单词.md b/problems/0151.翻转字符串里的单词.md deleted file mode 100644 index 35258a55..00000000 --- a/problems/0151.翻转字符串里的单词.md +++ /dev/null @@ -1,199 +0,0 @@ - -## 题目地址 -https://leetcode-cn.com/problems/reverse-words-in-a-string/ - -> 综合考察字符串操作的好题。 - -# 题目:151.翻转字符串里的单词 - -给定一个字符串,逐个翻转字符串中的每个单词。 - -示例 1: -输入: "the sky is blue" -输出: "blue is sky the" - -示例 2: -输入: "  hello world!  " -输出: "world! hello" -解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。 - -示例 3: -输入: "a good   example" -输出: "example good a" -解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。 - - -# 思路 - -**这道题目可以说是综合考察了字符串的多种操作。** - - -一些同学会使用split库函数,分隔单词,然后定义一个新的string字符串,最后再把单词倒序相加,那么这道题题目就是一道水题了,失去了它的意义。 - -所以这里我还是提高一下本题的难度:**不要使用辅助空间,空间复杂度要求为O(1)。** - -不能使用辅助空间之后,那么只能在原字符串上下功夫了。 - -想一下,我们将整个字符串都反转过来,那么单词的顺序指定是倒序了,只不过单词本身也倒叙了,那么再把单词反转一下,单词不就正过来了。 - -所以解题思路如下: - -* 移除多余空格 -* 将整个字符串反转 -* 将每个单词反转 - -如动画所示: - - - -这样我们就完成了翻转字符串里的单词。 - -思路很明确了,我们说一说代码的实现细节,就拿移除多余空格来说,一些同学会上来写如下代码: - -``` -void removeExtraSpaces(string& s) { - for (int i = s.size() - 1; i > 0; i--) { - if (s[i] == s[i - 1] && s[i] == ' ') { - s.erase(s.begin() + i); - } - } - // 删除字符串最后面的空格 - if (s.size() > 0 && s[s.size() - 1] == ' ') { - s.erase(s.begin() + s.size() - 1); - } - // 删除字符串最前面的空格 - if (s.size() > 0 && s[0] == ' ') { - s.erase(s.begin()); - } -} -``` - -逻辑很简单,从前向后遍历,遇到空格了就erase。 - -如果不仔细琢磨一下erase的时间复杂读,还以为以上的代码是O(n)的时间复杂度呢。 - -想一下真正的时间复杂度是多少,一个erase本来就是O(n)的操作,erase实现原理题目:[数组:就移除个元素很难么?](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA),最优的算法来移除元素也要O(n)。 - -erase操作上面还套了一个for循环,那么以上代码移除冗余空格的代码时间复杂度为O(n^2)。 - -那么使用双指针法来去移除空格,最后resize(重新设置)一下字符串的大小,就可以做到O(n)的时间复杂度。 - -如果对这个操作比较生疏了,可以再看一下这篇文章:[数组:就移除个元素很难么?](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA)是如何移除元素的。 - -那么使用双指针来移除冗余空格代码如下: fastIndex走的快,slowIndex走的慢,最后slowIndex就标记着移除多余空格后新字符串的长度。 - -``` -void removeExtraSpaces(string& s) { - int slowIndex = 0, fastIndex = 0; // 定义快指针,慢指针 - // 去掉字符串前面的空格 - while (s.size() > 0 && fastIndex < s.size() && s[fastIndex] == ' ') { - fastIndex++; - } - for (; fastIndex < s.size(); fastIndex++) { - // 去掉字符串中间部分的冗余空格 - if (fastIndex - 1 > 0 - && s[fastIndex - 1] == s[fastIndex] - && s[fastIndex] == ' ') { - continue; - } else { - s[slowIndex++] = s[fastIndex]; - } - } - if (slowIndex - 1 > 0 && s[slowIndex - 1] == ' ') { // 去掉字符串末尾的空格 - s.resize(slowIndex - 1); - } else { - s.resize(slowIndex); // 重新设置字符串大小 - } -} -``` - -有的同学可能发现用erase来移除空格,在leetcode上性能也还行。主要是以下几点;: - -1. leetcode上的测试集里,字符串的长度不够长,如果足够长,性能差距会非常明显。 -2. leetcode的测程序耗时不是很准确的。 - -此时我们已经实现了removeExtraSpaces函数来移除冗余空格。 - -还做实现反转字符串的功能,支持反转字符串子区间,这个实现我们分别在[字符串:这道题目,使用库函数一行代码搞定](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA)和[字符串:简单的反转还不够!](https://mp.weixin.qq.com/s/XGSk1GyPWhfqj2g7Cb1Vgw)里已经讲过了。 - -代码如下: - -``` -// 反转字符串s中左闭又闭的区间[start, end] -void reverse(string& s, int start, int end) { - for (int i = start, j = end; i < j; i++, j--) { - swap(s[i], s[j]); - } -} -``` - -## 本题C++整体代码 - -效率: - - - -``` -class Solution { -public: - // 反转字符串s中左闭又闭的区间[start, end] - void reverse(string& s, int start, int end) { - for (int i = start, j = end; i < j; i++, j--) { - swap(s[i], s[j]); - } - } - - // 移除冗余空格:使用双指针(快慢指针法)O(n)的算法 - void removeExtraSpaces(string& s) { - int slowIndex = 0, fastIndex = 0; // 定义快指针,慢指针 - // 去掉字符串前面的空格 - while (s.size() > 0 && fastIndex < s.size() && s[fastIndex] == ' ') { - fastIndex++; - } - for (; fastIndex < s.size(); fastIndex++) { - // 去掉字符串中间部分的冗余空格 - if (fastIndex - 1 > 0 - && s[fastIndex - 1] == s[fastIndex] - && s[fastIndex] == ' ') { - continue; - } else { - s[slowIndex++] = s[fastIndex]; - } - } - if (slowIndex - 1 > 0 && s[slowIndex - 1] == ' ') { // 去掉字符串末尾的空格 - s.resize(slowIndex - 1); - } else { - s.resize(slowIndex); // 重新设置字符串大小 - } - } - - string reverseWords(string s) { - removeExtraSpaces(s); // 去掉冗余空格 - reverse(s, 0, s.size() - 1); // 将字符串全部反转 - int start = 0; // 反转的单词在字符串里起始位置 - int end = 0; // 反转的单词在字符串里终止位置 - bool entry = false; // 标记枚举字符串的过程中是否已经进入了单词区间 - for (int i = 0; i < s.size(); i++) { // 开始反转单词 - if ((!entry) || (s[i] != ' ' && s[i - 1] == ' ')) { - start = i; // 确定单词起始位置 - entry = true; // 进入单词区间 - } - // 单词后面有空格的情况,空格就是分词符 - if (entry && s[i] == ' ' && s[i - 1] != ' ') { - end = i - 1; // 确定单词终止位置 - entry = false; // 结束单词区间 - reverse(s, start, end); - } - // 最后一个结尾单词之后没有空格的情况 - if (entry && (i == (s.size() - 1)) && s[i] != ' ' ) { - end = i;// 确定单词终止位置 - entry = false; // 结束单词区间 - reverse(s, start, end); - } - } - return s; - } -}; -``` -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0155.最小栈.md b/problems/0155.最小栈.md deleted file mode 100644 index 6b5fb643..00000000 --- a/problems/0155.最小栈.md +++ /dev/null @@ -1,51 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/min-stack/ - -## 思路 - -有的同学一开始会把这道题目想简单了,用一个变量记录最小值不就得了,其实是如果要是弹出了这个最小值的话,我们还要记录次小值,所以需要一个辅助数组来记录次小值。 - -我这里使用数组来实现栈,在用一个数组来放当前栈里最小数值,同时使用辅助数组来记录 - -## C++代码 - -``` -class MinStack { -public: - vector vec; - vector minVec; - /** initialize your data structure here. */ - MinStack() { - } - void push(int x) { - vec.push_back(x); - if (minVec.size() == 0) { - minVec.push_back(x); - } else if (x <= minVec[minVec.size() - 1]) { // 这里一定是下小于等于,防止多个最小值的情况 - minVec.push_back(x); - } - } - void pop() { - if (vec.size() == 0) { // 防止下面的操作会导致越界 - return; - } - if (vec[vec.size() - 1] == minVec[minVec.size() - 1]) { - minVec.pop_back(); - } - vec.pop_back(); - } - - int top() { - // 这里有越界的危险,但是题目也没有说如果栈为空,top()应该返回啥,所以就默认测试用例没有上来直接top的用例了 - return vec[vec.size() - 1]; - } - - int getMin() { - // 这里有越界的危险,但是题目也没有说如果栈为空,getMin()应该返回啥,所以就默认测试用例没有上来直接getMin的用例了 - return minVec[minVec.size() - 1]; - } -}; - -``` -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0188.买卖股票的最佳时机IV.md b/problems/0188.买卖股票的最佳时机IV.md deleted file mode 100644 index e67b785b..00000000 --- a/problems/0188.买卖股票的最佳时机IV.md +++ /dev/null @@ -1,127 +0,0 @@ - -# 思路 - -这道题目可以说是123.买卖股票的最佳时机III的进阶版, 这里要求至多有k次交易。 - -在123.买卖股票的最佳时机III中,我是定义了一个二维dp数组,本题其实依然可以用一个二维dp数组。 - -动规五部曲,分析如下: - -### 确定dp数组以及下标的含义 - -使用二维数组 dp[i][j] :第i天的状态为j,所剩下的最大现金是dp[i][j] - -j的状态表示为: - -* 0 表示不操作 -* 1 第一次买入 -* 2 第一次卖出 -* 3 第一次买入 -* 4 第一次卖出 -..... - -大家应该发现规律了吧 ,除了0以外,偶数就是卖出,奇数就是买入。 - -题目要求是至多有K笔交易,那么j的范围就定义为 2 * k + 1 就可以了。 - -所以二维dp数组的C++定义为: - -``` -vector> dp(prices.size(), vector(2 * k + 1, 0)); -``` - -### 确定递推公式 - -在123.买卖股票的最佳时机III中 - -需要注意:dp[i][1],**表示的是第i天,买入股票的状态,并不是说一定要第i天买入股票,这是很多同学容易陷入的误区**。 - -达到dp[i][1]状态,有两个具体操作: - -* 操作一:第i天买入股票了,那么dp[i][1] = dp[i-1][0] - prices[i] -* 操作二:第i天没有操作,而是沿用前一天买入的状态,即:dp[i][1] = dp[i - 1][0] - -那么dp[i][1]究竟选 dp[i-1][0] - prices[i],还是dp[i - 1][0]呢? - -一定是选最大的,所以 dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][0]); - -同理dp[i][2]也有两个操作: - -* 操作一:第i天卖出股票了,那么dp[i][2] = dp[i - 1][i] + prices[i] -* 操作二:第i天没有操作,沿用前一天卖出股票的状态,即:dp[i][2] = dp[i - 1][2] - -所以dp[i][2] = max(dp[i - 1][i] + prices[i], dp[i][2]) - -同理可推出剩下状态部分: - -dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]); -dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]); - -* dp数组如何初始化 -* 确定遍历顺序 -* 举例推导dp数组 - -``` -class Solution { -public: - int maxProfit(int k, vector& prices) { - - if (prices.size() == 0) return 0; - vector> dp(prices.size(), vector(2 * k + 1, 0)); - for (int j = 1; j < 2 * k; j += 2) { - dp[0][j] = -prices[0]; - } - for (int i = 1;i < prices.size(); i++) { - for (int j = 0; j < 2 * k - 1; j += 2) { // 注意这里是等于 - dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]); - dp[i][j + 2] = max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]); - } - } - int result = 0; - for (int j = 2; j <= 2 * k; j += 2) { - result = max(result, dp[prices.size() - 1][j]); - } - return result; - } -}; -``` - -``` -class Solution { -public: - int maxProfit(int k, vector& prices) { - const int n = prices.size(); - if (n == 0) return 0; - vector dp(2 * k + 1, 0); - for (int i = 1;i < 2 * k;i += 2) - dp[i] = -prices[0]; - for (int i = 1;i < n;++i) { - for (int j = 0;j < 2 * k;j += 2) { - dp[j] = max(dp[j], dp[j + 1] + prices[i]); - dp[j + 1] = max(dp[j + 1], dp[j + 2] - prices[i]); - } - } - return dp[0]; - } -}; - - -class Solution { -public: - int maxProfit(vector& prices) { - if (prices.size() == 0) return 0; - vector> dp(prices.size(), vector(5, 0)); - dp[0][0] = 0; - dp[0][1] = -prices[0]; - dp[0][3] = -prices[0]; - for (int i = 1; i < prices.size(); i++) { - dp[i][0] = dp[i - 1][0]; - dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]); - dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]); - dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]); - dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]); - } - return max(dp[prices.size() - 1][2], dp[prices.size() - 1][4]); - - } -}; diff --git a/problems/0199.二叉树的右视图.md b/problems/0199.二叉树的右视图.md deleted file mode 100644 index ce17ca72..00000000 --- a/problems/0199.二叉树的右视图.md +++ /dev/null @@ -1,46 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/binary-tree-right-side-view/ - -## 思路 - -这里再讲一遍二叉树的广度优先遍历(层序遍历) - -需要借用一个辅助数据结构队列来实现,**队列先进先出,符合一层一层遍历的逻辑,而是用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。** - -使用队列实现广度优先遍历,动画如下: - - - -这样就实现了层序从左到右遍历二叉树。 - -建议先做一下这道题目[0102.二叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0102.二叉树的层序遍历.md), 在做这道,就会发现这是一道 广度优先搜索模板题目,层序遍历的时候,将每一层的最后元素放入result数组中, - -层序遍历的时候,将单层的最后面的元素放进数组中,随后返回数组就可以了。 - -代码如下: - -## C++代码 - -``` -class Solution { -public: - vector rightSideView(TreeNode* root) { - queue que; - if (root != NULL) que.push(root); - vector result; - while (!que.empty()) { - int size = que.size(); - for (int i = 0; i < size; i++) { - TreeNode* node = que.front(); - que.pop(); - if (i == (size - 1)) result.push_back(node->val);//将每一层的最后元素放入result数组中 - if (node->left) que.push(node->left); - if (node->right) que.push(node->right); - } - } - return result; - } -}; -``` - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0202.快乐数.md b/problems/0202.快乐数.md deleted file mode 100644 index f0e52376..00000000 --- a/problems/0202.快乐数.md +++ /dev/null @@ -1,71 +0,0 @@ -# 题目地址 -https://leetcode-cn.com/problems/happy-number/ - -> 该用set的时候,还是得用set - -# 第202题. 快乐数 - -编写一个算法来判断一个数 n 是不是快乐数。 - -「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为  1,那么这个数就是快乐数。 - -如果 n 是快乐数就返回 True ;不是,则返回 False 。 - -**示例:** - -输入:19 -输出:true -解释: -1^2 + 9^2 = 82 -8^2 + 2^2 = 68 -6^2 + 8^2 = 100 -1^2 + 0^2 + 0^2 = 1 - -# 思路 - -这道题目看上去貌似一道数学问题,其实并不是! - -题目中说了会 **无限循环**,那么也就是说**求和的过程中,sum会重复出现,这对解题很重要!** - -正如:[关于哈希表,你该了解这些!](https://mp.weixin.qq.com/s/g8N6WmoQmsCUw3_BaWxHZA)中所说,**当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法了。** - -所以这道题目使用哈希法,来判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum为1为止。 - -判断sum是否重复出现就可以使用unordered_set。 - -**还有一个难点就是求和的过程,如果对取数值各个位上的单数操作不熟悉的话,做这道题也会比较艰难。** - -# C++代码 - -``` -class Solution { -public: - // 取数值各个位上的单数之和 - int getSum(int n) { - int sum = 0; - while (n) { - sum += (n % 10) * (n % 10); - n /= 10; - } - return sum; - } - bool isHappy(int n) { - unordered_set set; - while(1) { - int sum = getSum(n); - if (sum == 1) { - return true; - } - // 如果这个sum曾经出现过,说明已经陷入了无限循环了,立刻return false - if (set.find(sum) != set.end()) { - return false; - } else { - set.insert(sum); - } - n = sum; - } - } -}; -``` -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0203.移除链表元素.md b/problems/0203.移除链表元素.md deleted file mode 100644 index f2a4909d..00000000 --- a/problems/0203.移除链表元素.md +++ /dev/null @@ -1,127 +0,0 @@ -## 题目地址 - -https://leetcode-cn.com/problems/remove-linked-list-elements/ - -> 链表操作中,可以使用原链表来直接进行删除操作,也可以设置一个虚拟头结点在进行删除操作,接下来看一看哪种方式更方便。 - -# 第203题:移除链表元素 - -题意:删除链表中等于给定值 val 的所有节点。 - -![203题目示例](https://img-blog.csdnimg.cn/20200814104441179.png) - -# 思路 - -这里以链表 1 4 2 4 来举例,移除元素4。 - - - -如果使用C,C++编程语言的话,不要忘了还要从内存中删除这两个移除的节点, 清理节点内存之后如图: - - - -**当然如果使用java ,python的话就不用手动管理内存了。** - -还要说明一下,就算使用C++来做leetcode,如果移除一个节点之后,没有手动在内存中删除这个节点,leetcode依然也是可以通过的,只不过,内存使用的空间大一些而已,但建议依然要养生手动清理内存的习惯。 - -这种情况下的移除操作,就是让节点next指针直接指向下下一个节点就可以了, - -那么因为单链表的特殊性,只能指向下一个节点,刚刚删除的是链表的中第二个,和第四个节点,那么如果删除的是头结点又该怎么办呢? - -这里就涉及如下链表操作的两种方式: -* **直接使用原来的链表来进行删除操作。** -* **设置一个虚拟头结点在进行删除操作。** - - -来看第一种操作:直接使用原来的链表来进行移除。 - - - -移除头结点和移除其他节点的操作是不一样的,因为链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。 - -所以头结点如何移除呢,其实只要将头结点向后移动一位就可以,这样就从链表中移除了一个头结点。 - - - - -依然别忘将原头结点从内存中删掉。 - - - -这样移除了一个头结点,是不是发现,在单链表中移除头结点 和 移除其他节点的操作方式是不一样,其实在写代码的时候也会发现,需要单独写一段逻辑来处理移除头结点的情况。 - -那么可不可以 以一种统一的逻辑来移除 链表的节点呢。 - -其实**可以设置一个虚拟头结点**,这样原链表的所有节点就都可以按照统一的方式进行移除了。 - -来看看如何设置一个虚拟头。依然还是在这个链表中,移除元素1。 - - - -这里来给链表添加一个虚拟头结点为新的头结点,此时要移除这个旧头结点元素1。 - -这样是不是就可以使用和移除链表其他节点的方式统一了呢? - -来看一下,如何移除元素1 呢,还是熟悉的方式,然后从内存中删除元素1。 - -最后呢在题目中,return 头结点的时候,别忘了 `return dummyNode->next;`, 这才是新的头结点 - - -# C++代码 - -**直接使用原来的链表来进行移除节点操作:** - -``` -class Solution { -public: - ListNode* removeElements(ListNode* head, int val) { - // 删除头结点 - while (head != NULL && head->val == val) { // 注意这里不是if - ListNode* tmp = head; - head = head->next; - delete tmp; - } - - // 删除非头结点 - ListNode* cur = head; - while (cur != NULL && cur->next!= NULL) { - if (cur->next->val == val) { - ListNode* tmp = cur->next; - cur->next = cur->next->next; - delete tmp; - } else { - cur = cur->next; - } - } - return head; - } -}; -``` - -**设置一个虚拟头结点在进行移除节点操作:** - -``` -class Solution { -public: - ListNode* removeElements(ListNode* head, int val) { - ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点 - dummyHead->next = head; // 将虚拟头结点指向head,这样方面后面做删除操作 - ListNode* cur = dummyHead; - - while (cur->next != NULL) { - if(cur->next->val == val) { - ListNode* tmp = cur->next; - cur->next = cur->next->next; - delete tmp; - } else { - cur = cur->next; - } - } - return dummyHead->next; - } -}; -``` -**我将算法学习相关的资料已经整理到了Github上:https://github.com/youngyangyang04/leetcode-master,里面还有leetcode刷题攻略、各个类型经典题目刷题顺序、思维导图看一看一定会有所收获!** - -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0205.同构字符串.md b/problems/0205.同构字符串.md deleted file mode 100644 index 071a562d..00000000 --- a/problems/0205.同构字符串.md +++ /dev/null @@ -1,36 +0,0 @@ - -## 题目地址 -https://leetcode-cn.com/problems/isomorphic-strings/ - -## 思路 - -字符串没有说都是小写字母之类的,所以用数组不合适了,用map来做映射。 - -使用两个map 保存 s[i] 到 t[j] 和 t[j] 到 s[i] 的映射关系,如果发现对应不上,立刻返回 false - -## C++代码 - -``` -class Solution { -public: - bool isIsomorphic(string s, string t) { - unordered_map map1; - unordered_map map2; - for (int i = 0, j = 0; i < s.size(); i++, j++) { - if (map1.find(s[i]) == map1.end()) { // map1保存s[i] 到 t[j]的映射 - map1[s[i]] = t[j]; - } - if (map2.find(t[j]) == map2.end()) { // map2保存t[j] 到 s[i]的映射 - map2[t[j]] = s[i]; - } - // 发现映射 对应不上,立刻返回false - if (map1[s[i]] != t[j] || map2[t[j]] != s[i]) { - return false; - } - } - return true; - } -}; -``` -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0206.翻转链表.md b/problems/0206.翻转链表.md deleted file mode 100644 index 8fc9d9cd..00000000 --- a/problems/0206.翻转链表.md +++ /dev/null @@ -1,93 +0,0 @@ -## 题目地址 - -https://leetcode-cn.com/problems/reverse-linked-list/ - -> 反转链表的写法很简单,一些同学甚至可以背下来但过一阵就忘了该咋写,主要是因为没有理解真正的反转过程。 - -# 第206题:反转链表 - -题意:反转一个单链表。 - -示例: -输入: 1->2->3->4->5->NULL -输出: 5->4->3->2->1->NULL - -# 思路 - -如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。 - -其实只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表,如图所示: - - - -之前链表的头节点是元素1, 反转之后头结点就是元素5 ,这里并没有添加或者删除节点,仅仅是改表next指针的方向。 - -那么接下来看一看是如何反转呢? - -我们拿有示例中的链表来举例,如动画所示: - - - -首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。 - -然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。 - -为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。 - -接下来,就是循环走如下代码逻辑了,继续移动pre和cur指针。 - -最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。 此时我们return pre指针就可以了,pre指针就指向了新的头结点。 - -# C++代码 - -## 双指针法 -``` -class Solution { -public: - ListNode* reverseList(ListNode* head) { - ListNode* temp; // 保存cur的下一个节点 - ListNode* cur = head; - ListNode* pre = NULL; - while(cur) { - temp = cur->next; // 保存一下 cur的下一个节点,因为接下来要改变cur->next - cur->next = pre; // 翻转操作 - // 更新pre 和 cur指针 - pre = cur; - cur = temp; - } - return pre; - } -}; -``` - -## 递归法 - -递归法相对抽象一些,但是其实和双指针法是一样的逻辑,同样是当cur为空的时候循环结束,不断将cur指向pre的过程。 - -关键是初始化的地方,可能有的同学会不理解, 可以看到双指针法中初始化 cur = head,pre = NULL,在递归法中可以从如下代码看出初始化的逻辑也是一样的,只不过写法变了。 - -具体可以看代码(已经详细注释),**双指针法写出来之后,理解如下递归写法就不难了,代码逻辑都是一样的。** -``` -class Solution { -public: - ListNode* reverse(ListNode* pre,ListNode* cur){ - if(cur == NULL) return pre; - ListNode* temp = cur->next; - cur->next = pre; - // 可以和双指针法的代码进行对比,如下递归的写法,其实就是做了这两步 - // pre = cur; - // cur = temp; - return reverse(cur,temp); - } - ListNode* reverseList(ListNode* head) { - // 和双指针法初始化是一样的逻辑 - // ListNode* cur = head; - // ListNode* pre = NULL; - return reverse(NULL, head); - } - -}; -``` - -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0209.长度最小的子数组.md b/problems/0209.长度最小的子数组.md deleted file mode 100644 index 1dd5ba15..00000000 --- a/problems/0209.长度最小的子数组.md +++ /dev/null @@ -1,126 +0,0 @@ -

- -

-

- - - - - - -

- -> 滑动窗口拯救了你 - -# 题目209.长度最小的子数组 - -题目链接: https://leetcode-cn.com/problems/minimum-size-subarray-sum/ - -给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。 - -示例: - -输入:s = 7, nums = [2,3,1,2,4,3] -输出:2 -解释:子数组 [4,3] 是该条件下的长度最小的子数组。 - - -# 暴力解法 - -这道题目暴力解法当然是 两个for循环,然后不断的寻找符合条件的子序列,时间复杂度很明显是O(n^2) 。 - -代码如下: - -``` -class Solution { -public: - int minSubArrayLen(int s, vector& nums) { - int result = INT32_MAX; // 最终的结果 - int sum = 0; // 子序列的数值之和 - int subLength = 0; // 子序列的长度 - for (int i = 0; i < nums.size(); i++) { // 设置子序列起点为i - sum = 0; - for (int j = i; j < nums.size(); j++) { // 设置子序列终止位置为j - sum += nums[j]; - if (sum >= s) { // 一旦发现子序列和超过了s,更新result - subLength = j - i + 1; // 取子序列的长度 - result = result < subLength ? result : subLength; - break; // 因为我们是找符合条件最短的子序列,所以一旦符合条件就break - } - } - } - // 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列 - return result == INT32_MAX ? 0 : result; - } -}; -``` -时间复杂度:O(n^2) -空间复杂度:O(1) - -# 滑动窗口 - -接下来就开始介绍数组操作中另一个重要的方法:**滑动窗口**。 - -所谓滑动窗口,**就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果**。 - -这里还是以题目中的示例来举例,s=7, 数组是 2,3,1,2,4,3,来看一下查找的过程: - - - -最后找到 4,3 是最短距离。 - -其实从动画中可以发现滑动窗口也可以理解为双指针法的一种!只不过这种解法更像是一个窗口的移动,所以叫做滑动窗口更适合一些。 - -在本题中实现滑动窗口,主要确定如下三点: - -* 窗口内是什么? -* 如何移动窗口的起始位置? -* 如何移动窗口的结束位置? - -窗口就是 满足其和 ≥ s 的长度最小的 连续 子数组。 - -窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。 - -窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,窗口的起始位置设置为数组的起始位置就可以了。 - -解题的关键在于 窗口的起始位置如何移动,如图所示: - - - -可以发现**滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)的暴力解法降为O(n)。** - - -# C++滑动窗口代码 - -``` -class Solution { -public: - int minSubArrayLen(int s, vector& nums) { - int result = INT32_MAX; - int sum = 0; // 滑动窗口数值之和 - int i = 0; // 滑动窗口起始位置 - int subLength = 0; // 滑动窗口的长度 - for (int j = 0; j < nums.size(); j++) { - sum += nums[j]; - // 注意这里使用while,每次更新 i(起始位置),并不断比较子序列是否符合条件 - while (sum >= s) { - subLength = (j - i + 1); // 取子序列的长度 - result = result < subLength ? result : subLength; - sum -= nums[i++]; // 这里体现出滑动窗口的精髓之处,不断变更i(子序列的起始位置) - } - } - // 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列 - return result == INT32_MAX ? 0 : result; - } -}; -``` - -时间复杂度:O(n) -空间复杂度:O(1) - - -**循序渐进学算法,认准「代码随想录」,Carl手把手带你过关斩将!** - -

- -

diff --git a/problems/0216.组合总和III.md b/problems/0216.组合总和III.md deleted file mode 100644 index cdcb3aac..00000000 --- a/problems/0216.组合总和III.md +++ /dev/null @@ -1,218 +0,0 @@ - -> 别看本篇选的是组合总和III,而不是组合总和,本题和上一篇[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)相比难度刚刚好! - -# 第216题.组合总和III - -链接:https://leetcode-cn.com/problems/combination-sum-iii/ - -找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。 - -说明: -* 所有数字都是正整数。 -* 解集不能包含重复的组合。  - -示例 1: -输入: k = 3, n = 7 -输出: [[1,2,4]] - -示例 2: -输入: k = 3, n = 9 -输出: [[1,2,6], [1,3,5], [2,3,4]] - - -# 思路 - -本题就是在[1,2,3,4,5,6,7,8,9]这个集合中找到和为n的k个数的组合。 - -相对于[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ),无非就是多了一个限制,本题是要找到和为n的k个数的组合,而整个集合已经是固定的了[1,...,9]。 - -想到这一点了,做过[77. 组合](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)之后,本题是简单一些了。 - -本题k相当于了树的深度,9(因为整个集合就是9个数)就是树的宽度。 - -例如 k = 2,n = 4的话,就是在集合[1,2,3,4,5,6,7,8,9]中求 k(个数) = 2, n(和) = 4的组合。 - -选取过程如图: - -![216.组合总和III](https://img-blog.csdnimg.cn/20201123195717975.png) - -图中,可以看出,只有最后取到集合(1,3)和为4 符合条件。 - - -## 回溯三部曲 - -* **确定递归函数参数** - -和[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)一样,依然需要一维数组path来存放符合条件的结果,二维数组result来存放结果集。 - -这里我依然定义path 和 result为全局变量。 - -至于为什么取名为path?从上面树形结构中,可以看出,结果其实就是一条根节点到叶子节点的路径。 - -``` -vector> result; // 存放结果集 -vector path; // 符合条件的结果 -``` - -接下来还需要如下参数: - -* targetSum(int)目标和,也就是题目中的n。 -* k(int)就是题目中要求k个数的集合。 -* sum(int)为已经收集的元素的总和,也就是path里元素的总和。 -* startIndex(int)为下一层for循环搜索的起始位置。 - -所以代码如下: - -``` -vector> result; -vector path; -void backtracking(int targetSum, int k, int sum, int startIndex) -``` -其实这里sum这个参数也可以省略,每次targetSum减去选取的元素数值,然后判断如果targetSum为0了,说明收集到符合条件的结果了,我这里为了直观便于理解,还是加一个sum参数。 - -还要强调一下,回溯法中递归函数参数很难一次性确定下来,一般先写逻辑,需要啥参数了,填什么参数。 - -* 确定终止条件 - -什么时候终止呢? - -在上面已经说了,k其实就已经限制树的深度,因为就取k个元素,树再往下深了没有意义。 - -所以如果path.size() 和 k相等了,就终止。 - -如果此时path里收集到的元素和(sum) 和targetSum(就是题目描述的n)相同了,就用result收集当前的结果。 - -所以 终止代码如下: - -``` -if (path.size() == k) { - if (sum == targetSum) result.push_back(path); - return; // 如果path.size() == k 但sum != targetSum 直接返回 -} -``` - -* **单层搜索过程** - -本题和[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)区别之一就是集合固定的就是9个数[1,...,9],所以for循环固定i<=9 - -如图: -![216.组合总和III](https://img-blog.csdnimg.cn/20201123195717975.png) - -处理过程就是 path收集每次选取的元素,相当于树型结构里的边,sum来统计path里元素的总和。 - -代码如下: - -``` -for (int i = startIndex; i <= 9; i++) { - sum += i; - path.push_back(i); - backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex - sum -= i; // 回溯 - path.pop_back(); // 回溯 -} -``` - -**别忘了处理过程 和 回溯过程是一一对应的,处理有加,回溯就要有减!** - -参照[关于回溯算法,你该了解这些!](https://mp.weixin.qq.com/s/gjSgJbNbd1eAA5WkA-HeWw)中的模板,不难写出如下C++代码: - -``` -class Solution { -private: - vector> result; // 存放结果集 - vector path; // 符合条件的结果 - // targetSum:目标和,也就是题目中的n。 - // k:题目中要求k个数的集合。 - // sum:已经收集的元素的总和,也就是path里元素的总和。 - // startIndex:下一层for循环搜索的起始位置。 - void backtracking(int targetSum, int k, int sum, int startIndex) { - if (path.size() == k) { - if (sum == targetSum) result.push_back(path); - return; // 如果path.size() == k 但sum != targetSum 直接返回 - } - for (int i = startIndex; i <= 9; i++) { - sum += i; // 处理 - path.push_back(i); // 处理 - backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex - sum -= i; // 回溯 - path.pop_back(); // 回溯 - } - } - -public: - vector> combinationSum3(int k, int n) { - result.clear(); // 可以不加 - path.clear(); // 可以不加 - backtracking(n, k, 0, 1); - return result; - } -}; -``` - -## 剪枝 - -这道题目,剪枝操作其实是很容易想到了,想必大家看上面的树形图的时候已经想到了。 - -如图: -![216.组合总和III1](https://img-blog.csdnimg.cn/2020112319580476.png) - -已选元素总和如果已经大于n(图中数值为4)了,那么往后遍历就没有意义了,直接剪掉。 - -那么剪枝的地方一定是在递归终止的地方剪,剪枝代码如下: - -``` -if (sum > targetSum) { // 剪枝操作 - return; -} -``` - -和[回溯算法:组合问题再剪剪枝](https://mp.weixin.qq.com/s/Ri7spcJMUmph4c6XjPWXQA) 一样,for循环的范围也可以剪枝,i <= 9 - (k - path.size()) + 1就可以了。 - -最后C++代码如下: - -``` -class Solution { -private: - vector> result; // 存放结果集 - vector path; // 符合条件的结果 - void backtracking(int targetSum, int k, int sum, int startIndex) { - if (sum > targetSum) { // 剪枝操作 - return; // 如果path.size() == k 但sum != targetSum 直接返回 - } - if (path.size() == k) { - if (sum == targetSum) result.push_back(path); - return; - } - for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) { // 剪枝 - sum += i; // 处理 - path.push_back(i); // 处理 - backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex - sum -= i; // 回溯 - path.pop_back(); // 回溯 - } - } - -public: - vector> combinationSum3(int k, int n) { - result.clear(); // 可以不加 - path.clear(); // 可以不加 - backtracking(n, k, 0, 1); - return result; - } -}; -``` - -# 总结 - -开篇就介绍了本题与[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)的区别,相对来说加了元素总和的限制,如果做完[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)再做本题在合适不过。 - -分析完区别,依然把问题抽象为树形结构,按照回溯三部曲进行讲解,最后给出剪枝的优化。 - -相信做完本题,大家对组合问题应该有初步了解了。 - -**就酱,如果感觉对你有帮助,就帮Carl转发一下吧,让更多小伙伴知道这里!** - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/0219.存在重复元素II.md b/problems/0219.存在重复元素II.md deleted file mode 100644 index 4835a7f0..00000000 --- a/problems/0219.存在重复元素II.md +++ /dev/null @@ -1,51 +0,0 @@ - -## 题目地址 -https://leetcode-cn.com/problems/contains-duplicate-ii/ - -## 思路 - -使用哈希策略,map数据结构来记录数组元素和对应的元素所在下表,看代码,已经详细注释。 - -## C++代码 - -``` -class Solution { -public: - bool containsNearbyDuplicate(vector& nums, int k) { - unordered_map map; // key: 数组元素, value:元素所在下表 - for (int i = 0; i < nums.size(); i++) { - // 找到了在索引i之前就出现过nums[i]这个元素 - if (map.find(nums[i]) != map.end()) { - int distance = i - map[nums[i]]; - if (distance <= k) { - return true; - } - map[nums[i]] = i; // 更新元素nums[i]所在的最新位置i - } else { // 如果map里面没有,就把插入一条数据<元素,元素所在的下表> - map[nums[i]] = i; - } - } - return false; - } -}; -``` - -代码精简之后 - -``` -class Solution { -public: - bool containsNearbyDuplicate(vector& nums, int k) { - unordered_map map; - for (int i = 0; i < nums.size(); i++) { - if (map.find(nums[i]) != map.end() && i - map[nums[i]] <= k) return true; - map[nums[i]] = i; - } - return false; - } -}; -``` - - -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0220.存在重复元素III.md b/problems/0220.存在重复元素III.md deleted file mode 100644 index 42fcb2f4..00000000 --- a/problems/0220.存在重复元素III.md +++ /dev/null @@ -1,29 +0,0 @@ -## 题目地址 - -## 思路 - - -## C++代码 - - -``` -class Solution { -public: - bool containsNearbyAlmostDuplicate(vector& nums, int k, int t) { - multiset s; - if(nums.empty() || k==0) return false; - for(int i=0;ik){ - s.erase(nums[i-k-1]); - } - auto index = s.lower_bound(nums[i]-t); - if(index!=s.end() && abs(*index-nums[i])<=t) return true; - s.insert(nums[i]); - - } - return false; - } -}; -``` -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0222.完全二叉树的节点个数.md b/problems/0222.完全二叉树的节点个数.md deleted file mode 100644 index d3be52c9..00000000 --- a/problems/0222.完全二叉树的节点个数.md +++ /dev/null @@ -1,172 +0,0 @@ - -# 222.完全二叉树的节点个数 - -题目地址:https://leetcode-cn.com/problems/count-complete-tree-nodes/ - -给出一个完全二叉树,求出该树的节点个数。 - -示例: - - - -# 思路 - -本篇给出按照普通二叉树的求法以及利用完全二叉树性质的求法。 - -## 普通二叉树 - -首先按照普通二叉树的逻辑来求。 - -这道题目的递归法和求二叉树的深度写法类似, 而迭代法,[二叉树:层序遍历登场!](https://mp.weixin.qq.com/s/Gb3BjakIKGNpup2jYtTzog)遍历模板稍稍修改一下,记录遍历的节点数量就可以了。 - -递归遍历的顺序依然是后序(左右中)。 - -### 递归 - -如果对求二叉树深度还不熟悉的话,看这篇:[二叉树:看看这些树的最大深度](https://mp.weixin.qq.com/s/guKwV-gSNbA1CcbvkMtHBg)。 - -1. 确定递归函数的参数和返回值:参数就是传入树的根节点,返回就返回以该节点为根节点二叉树的节点数量,所以返回值为int类型。 - -代码如下: -``` -int getNodesNum(TreeNode* cur) { -``` - -2. 确定终止条件:如果为空节点的话,就返回0,表示节点数为0。 - -代码如下: - -``` -if (cur == NULL) return 0; -``` - -3. 确定单层递归的逻辑:先求它的左子树的节点数量,再求的右子树的节点数量,最后取总和再加一 (加1是因为算上当前中间节点)就是目前节点为根节点的节点数量。 - -代码如下: - -``` -int leftNum = getNodesNum(cur->left); // 左 -int rightNum = getNodesNum(cur->right); // 右 -int treeNum = leftNum + rightNum + 1; // 中 -return treeNum; -``` - -所以整体C++代码如下: - -``` -// 版本一 -class Solution { -private: - int getNodesNum(TreeNode* cur) { - if (cur == 0) return 0; - int leftNum = getNodesNum(cur->left); // 左 - int rightNum = getNodesNum(cur->right); // 右 - int treeNum = leftNum + rightNum + 1; // 中 - return treeNum; - } -public: - int countNodes(TreeNode* root) { - return getNodesNum(root); - } -}; -``` - -代码精简之后C++代码如下: -``` -// 版本二 -class Solution { -public: - int countNodes(TreeNode* root) { - if (root == NULL) return 0; - return 1 + countNodes(root->left) + countNodes(root->right); - } -}; -``` - -时间复杂度:O(n) -空间复杂度:O(logn),算上了递归系统栈占用的空间 - -**网上基本都是这个精简的代码版本,其实不建议大家照着这个来写,代码确实精简,但隐藏了一些内容,连遍历的顺序都看不出来,所以初学者建议学习版本一的代码,稳稳的打基础**。 - - -### 迭代法 - -如果对求二叉树层序遍历还不熟悉的话,看这篇:[二叉树:层序遍历登场!](https://mp.weixin.qq.com/s/Gb3BjakIKGNpup2jYtTzog)。 - -那么只要模板少做改动,加一个变量result,统计节点数量就可以了 - -``` -class Solution { -public: - int countNodes(TreeNode* root) { - queue que; - if (root != NULL) que.push(root); - int result = 0; - while (!que.empty()) { - int size = que.size(); - for (int i = 0; i < size; i++) { - TreeNode* node = que.front(); - que.pop(); - result++; // 记录节点数量 - if (node->left) que.push(node->left); - if (node->right) que.push(node->right); - } - } - return result; - } -}; -``` -时间复杂度:O(n) -空间复杂度:O(n) - -## 完全二叉树 - -以上方法都是按照普通二叉树来做的,对于完全二叉树特性不了解的同学可以看这篇 [关于二叉树,你该了解这些!](https://mp.weixin.qq.com/s/_ymfWYvTNd2GvWvC5HOE4A),这篇详细介绍了各种二叉树的特性。 - -完全二叉树只有两种情况,情况一:就是满二叉树,情况二:最后一层叶子节点没有满。 - -对于情况一,可以直接用 2^树深度 - 1 来计算,注意这里根节点深度为1。 - -对于情况二,分别递归左孩子,和右孩子,递归到某一深度一定会有左孩子或者右孩子为满二叉树,然后依然可以按照情况1来计算。 - -完全二叉树(一)如图: -![222.完全二叉树的节点个数](https://img-blog.csdnimg.cn/20201124092543662.png) - -完全二叉树(二)如图: -![222.完全二叉树的节点个数1](https://img-blog.csdnimg.cn/20201124092634138.png) - -可以看出如果整个树不是满二叉树,就递归其左右孩子,直到遇到满二叉树为止,用公式计算这个子树(满二叉树)的节点数量。 - -C++代码如下: - -```C++ -class Solution { -public: - int countNodes(TreeNode* root) { - if (root == nullptr) return 0; - TreeNode* left = root->left; - TreeNode* right = root->right; - int leftHeight = 0, rightHeight = 0; // 这里初始为0是有目的的,为了下面求指数方便 - while (left) { // 求左子树深度 - left = left->left; - leftHeight++; - } - while (right) { // 求右子树深度 - right = right->right; - rightHeight++; - } - if (leftHeight == rightHeight) { - return (2 << leftHeight) - 1; // 注意(2<<1) 相当于2^2,所以leftHeight初始为0 - } - return countNodes(root->left) + countNodes(root->right) + 1; - } -}; -``` - -时间复杂度:O(logn * logn) -空间复杂度:O(logn) - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/0225.用队列实现栈.md b/problems/0225.用队列实现栈.md deleted file mode 100644 index 00aae08f..00000000 --- a/problems/0225.用队列实现栈.md +++ /dev/null @@ -1,150 +0,0 @@ -## 题目地址 - -https://leetcode-cn.com/problems/implement-stack-using-queues/ - -> 用队列实现栈还是有点别扭。 - -# 225. 用队列实现栈 - -使用队列实现栈的下列操作: - -* push(x) -- 元素 x 入栈 -* pop() -- 移除栈顶元素 -* top() -- 获取栈顶元素 -* empty() -- 返回栈是否为空 - -注意: - -* 你只能使用队列的基本操作-- 也就是 push to back, peek/pop from front, size, 和 is empty 这些操作是合法的。 -* 你所使用的语言也许不支持队列。 你可以使用 list 或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。 -* 你可以假设所有操作都是有效的(例如, 对一个空的栈不会调用 pop 或者 top 操作)。 - - -# 思路 - -(这里要强调是单向队列) - -有的同学可能疑惑这种题目有什么实际工程意义,**其实很多算法题目主要是对知识点的考察和教学意义远大于其工程实践的意义,所以面试题也是这样!** - -刚刚做过[栈与队列:我用栈来实现队列怎么样?](https://mp.weixin.qq.com/s/P6tupDwRFi6Ay-L7DT4NVg)的同学可能依然想着用一个输入队列,一个输出队列,就可以模拟栈的功能,仔细想一下还真不行! - -**队列模拟栈,其实一个队列就够了**,那么我们先说一说两个队列来实现栈的思路。 - -**队列是先进先出的规则,把一个队列中的数据导入另一个队列中,数据的顺序并没有变,并有变成先进后出的顺序。** - -所以用栈实现队列, 和用队列实现栈的思路还是不一样的,这取决于这两个数据结构的性质。 - -但是依然还是要用两个队列来模拟栈,只不过没有输入和输出的关系,而是另一个队列完全用又来备份的! - -如下面动画所示,**用两个队列que1和que2实现队列的功能,que2其实完全就是一个备份的作用**,把que1最后面的元素以外的元素都备份到que2,然后弹出最后面的元素,再把其他元素从que2导回que1。 - -模拟的队列执行语句如下: -queue.push(1); -queue.push(2); -queue.pop(); // 注意弹出的操作 -queue.push(3); -queue.push(4); -queue.pop(); // 注意弹出的操作 -queue.pop(); -queue.pop(); -queue.empty(); - - - -详细如代码注释所示: - -# C++代码 - -``` -class MyStack { -public: - queue que1; - queue que2; // 辅助队列,用来备份 - /** Initialize your data structure here. */ - MyStack() { - - } - - /** Push element x onto stack. */ - void push(int x) { - que1.push(x); - } - - /** Removes the element on top of the stack and returns that element. */ - int pop() { - int size = que1.size(); - size--; - while (size--) { // 将que1 导入que2,但要留下最后一个元素 - que2.push(que1.front()); - que1.pop(); - } - - int result = que1.front(); // 留下的最后一个元素就是要返回的值 - que1.pop(); - que1 = que2; // 再将que2赋值给que1 - while (!que2.empty()) { // 清空que2 - que2.pop(); - } - return result; - } - - /** Get the top element. */ - int top() { - return que1.back(); - } - - /** Returns whether the stack is empty. */ - bool empty() { - return que1.empty(); - } -}; -``` - -# 优化 - -其实这道题目就是用一个队里就够了。 - -**一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时在去弹出元素就是栈的顺序了。** - -代码如下: - -# C++优化代码 - -``` -class MyStack { -public: - queue que; - /** Initialize your data structure here. */ - MyStack() { - - } - /** Push element x onto stack. */ - void push(int x) { - que.push(x); - } - /** Removes the element on top of the stack and returns that element. */ - int pop() { - int size = que.size(); - size--; - while (size--) { // 将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部 - que.push(que.front()); - que.pop(); - } - int result = que.front(); // 此时弹出的元素顺序就是栈的顺序了 - que.pop(); - return result; - } - - /** Get the top element. */ - int top() { - return que.back(); - } - - /** Returns whether the stack is empty. */ - bool empty() { - return que.empty(); - } -}; -``` -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0226.翻转二叉树.md b/problems/0226.翻转二叉树.md deleted file mode 100644 index 04780c6f..00000000 --- a/problems/0226.翻转二叉树.md +++ /dev/null @@ -1,194 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/invert-binary-tree/ - -# 226.翻转二叉树 - -翻转一棵二叉树。 - - - -这道题目背后有一个让程序员心酸的故事,听说 Homebrew的作者Max Howell,就是因为没在白板上写出翻转二叉树,最后被Google拒绝了。(真假不做判断,权当一个乐子哈) - -# 题外话 - -这道题目是非常经典的题目,也是比较简单的题目(至少一看就会)。 - -但正是因为这道题太简单,一看就会,一些同学都没有抓住起本质,稀里糊涂的就把这道题目过了。 - -如果做过这道题的同学也建议认真看完,相信一定有所收获! - -# 思路 - -我们之前介绍的都是各种方式遍历二叉树,这次要翻转了,感觉还是有点懵逼。 - -这得怎么翻转呢? - -如果要从整个树来看,翻转还真的挺复杂,整个树以中间分割线进行翻转,如图: - - - -可以发现想要翻转它,其实就把每一个节点的左右孩子交换一下就可以了。 - -关键在于遍历顺序,前中后序应该选哪一种遍历顺序? (一些同学这道题都过了,但是不知道自己用的是什么顺序) - -遍历的过程中去翻转每一个节点的左右孩子就可以达到整体翻转的效果。 - -**注意只要把每一个节点的左右孩子翻转一下,就可以达到整体翻转的效果** - -**这道题目使用前序遍历和后序遍历都可以,唯独中序遍历不行,因为中序遍历会把某些节点的左右孩子翻转了两次!建议拿纸画一画,就理解了** - -那么层序遍历可以不可以呢?**依然可以的!只要把每一个节点的左右孩子翻转一下的遍历方式都是可以的!** - -## 递归法 - -对于二叉树的递归法的前中后序遍历,已经在[二叉树:前中后序递归遍历](https://mp.weixin.qq.com/s/PwVIfxDlT3kRgMASWAMGhA)详细讲解了。 - -我们下文以前序遍历为例,通过动画来看一下翻转的过程: - - - -我们来看一下递归三部曲: - -1. 确定递归函数的参数和返回值 - -参数就是要传入节点的指针,不需要其他参数了,通常此时定下来主要参数,如果在写递归的逻辑中发现还需要其他参数的时候,随时补充。 - -返回值的话其实也不需要,但是题目中给出的要返回root节点的指针,可以直接使用题目定义好的函数,所以就函数的返回类型为`TreeNode*`。 - -``` -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; - } -}; -``` - -## 迭代法 - -### 深度优先遍历 - -[二叉树:听说递归能做的,栈也能做!](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg)中给出了前中后序迭代方式的写法,所以本地可以很轻松的切出如下迭代法的代码: - -C++代码迭代法(前序遍历) - -``` -class Solution { -public: - TreeNode* invertTree(TreeNode* root) { - if (root == NULL) return root; - stack st; - st.push(root); - while(!st.empty()) { - TreeNode* node = st.top(); // 中 - st.pop(); - swap(node->left, node->right); - if(node->right) st.push(node->right); // 右 - if(node->left) st.push(node->left); // 左 - } - return root; - } -}; -``` -如果这个代码看不懂的话可以在回顾一下[二叉树:听说递归能做的,栈也能做!](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg)。 - - -我们在[二叉树:前中后序迭代方式的统一写法](https://mp.weixin.qq.com/s/WKg0Ty1_3SZkztpHubZPRg)中介绍了统一的写法,所以,本题也只需将文中的代码少做修改便可。 - -C++代码如下迭代法(前序遍历) - -``` -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; - } -}; -``` - -如果上面这个代码看不懂,回顾一下文章[二叉树:前中后序迭代方式的统一写法](https://mp.weixin.qq.com/s/WKg0Ty1_3SZkztpHubZPRg)。 - -### 广度优先遍历 - -也就是层序遍历,层数遍历也是可以翻转这棵树的,因为层序遍历也可以把每个节点的左右孩子都翻转一遍,代码如下: - -``` -class Solution { -public: - TreeNode* invertTree(TreeNode* root) { - queue que; - if (root != NULL) que.push(root); - while (!que.empty()) { - int size = que.size(); - for (int i = 0; i < size; i++) { - TreeNode* node = que.front(); - que.pop(); - swap(node->left, node->right); // 节点处理 - if (node->left) que.push(node->left); - if (node->right) que.push(node->right); - } - } - return root; - } -}; -``` -如果对以上代码不理解,或者不清楚二叉树的层序遍历,可以看这篇[二叉树:层序遍历登场!](https://mp.weixin.qq.com/s/Gb3BjakIKGNpup2jYtTzog) - -# 总结 - -针对二叉树的问题,解题之前一定要想清楚究竟是前中后序遍历,还是层序遍历。 - -**二叉树解题的大忌就是自己稀里糊涂的过了(因为这道题相对简单),但是也不知道自己是怎么遍历的。** - -这也是造成了二叉树的题目“一看就会,一写就废”的原因。 - -**针对翻转二叉树,我给出了一种递归,三种迭代(两种模拟深度优先遍历,一种层序遍历)的写法,都是之前我们讲过的写法,融汇贯通一下而已。** - -大家一定也有自己的解法,但一定要成方法论,这样才能通用,才能举一反三! - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0232.用栈实现队列.md b/problems/0232.用栈实现队列.md deleted file mode 100644 index 754443dd..00000000 --- a/problems/0232.用栈实现队列.md +++ /dev/null @@ -1,122 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/implement-queue-using-stacks/ - -> 工作上一定没人这么搞,但是考察对栈、队列理解程度的好题 - -# 232. 用栈实现队列 - -使用栈实现队列的下列操作: - -push(x) -- 将一个元素放入队列的尾部。 -pop() -- 从队列首部移除元素。 -peek() -- 返回队列首部的元素。 -empty() -- 返回队列是否为空。 -  - -示例: - -``` -MyQueue queue = new MyQueue(); -queue.push(1); -queue.push(2); -queue.peek(); // 返回 1 -queue.pop(); // 返回 1 -queue.empty(); // 返回 false -``` - -说明: - -* 你只能使用标准的栈操作 -- 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。 -* 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。 -* 假设所有操作都是有效的 (例如,一个空的队列不会调用 pop 或者 peek 操作)。 - -# 思路 - -这是一道模拟题,不涉及到具体算法,考察的就是对栈和队列的掌握程度。 - -使用栈来模式队列的行为,如果仅仅用一个栈,是一定不行的,所以需要两个栈**一个输入栈,一个输出栈**,这里要注意输入栈和输出栈的关系。 - -下面动画模拟以下队列的执行过程如下: - -执行语句: -queue.push(1); -queue.push(2); -queue.pop(); **注意此时的输出栈的操作** -queue.push(3); -queue.push(4); -queue.pop(); -queue.pop();**注意此时的输出栈的操作** -queue.pop(); -queue.empty(); - - - -在push数据的时候,只要数据放进输入栈就好,**但在pop的时候,操作就复杂一些,输出栈如果为空,就把进栈数据全部导入进来(注意是全部导入)**,再从出栈弹出数据,如果输出栈不为空,则直接从出栈弹出数据就可以了。 - -最后如何判断队列为空呢?**如果进栈和出栈都为空的话,说明模拟的队列为空了。** - -在代码实现的时候,会发现pop() 和 peek()两个函数功能类似,代码实现上也是类似的,可以思考一下如何把代码抽象一下。 - -代码如下: - -# C++代码 - -``` -class MyQueue { -public: - stack stIn; - stack stOut; - /** Initialize your data structure here. */ - MyQueue() { - - } - /** Push element x to the back of queue. */ - void push(int x) { - stIn.push(x); - } - - /** Removes the element from in front of queue and returns that element. */ - int pop() { - // 只有当stOut为空的时候,再从stIn里导入数据(导入stIn全部数据) - if (stOut.empty()) { - // 从stIn导入数据直到stIn为空 - while(!stIn.empty()) { - stOut.push(stIn.top()); - stIn.pop(); - } - } - int result = stOut.top(); - stOut.pop(); - return result; - } - - /** Get the front element. */ - int peek() { - int res = this->pop(); // 直接使用已有的pop函数 - stOut.push(res); // 因为pop函数弹出了元素res,所以再添加回去 - return res; - } - - /** Returns whether the queue is empty. */ - bool empty() { - return stIn.empty() && stOut.empty(); - } -}; - -``` - -# 拓展 - -可以看出peek()的实现,直接复用了pop()。 - -再多说一些代码开发上的习惯问题,在工业级别代码开发中,最忌讳的就是 实现一个类似的函数,直接把代码粘过来改一改就完事了。 - -这样的项目代码会越来越乱,**一定要懂得复用,功能相近的函数要抽象出来,不要大量的复制粘贴,很容易出问题!(踩过坑的人自然懂)** - -工作中如果发现某一个功能自己要经常用,同事们可能也会用到,自己就花点时间把这个功能抽象成一个好用的函数或者工具类,不仅自己方便,也方面了同事们。 - -同事们就会逐渐认可你的工作态度和工作能力,自己的口碑都是这么一点一点积累起来的!在同事圈里口碑起来了之后,你就发现自己走上了一个正循环,以后的升职加薪才少不了你!哈哈哈 - - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0234.回文链表.md b/problems/0234.回文链表.md deleted file mode 100644 index d37044d7..00000000 --- a/problems/0234.回文链表.md +++ /dev/null @@ -1,122 +0,0 @@ - -## 题目链接 -https://leetcode-cn.com/problems/palindrome-linked-list/ - -## 思路 - -### 数组模拟 - -最直接的想法,就是把链表装成数组,然后再判断是否回文。 - -代码也比较简单。如下: - -``` -class Solution { -public: - bool isPalindrome(ListNode* head) { - vector vec; - ListNode* cur = head; - while (cur) { - vec.push_back(cur->val); - cur = cur->next; - } - // 比较数组回文 - for (int i = 0, j = vec.size() - 1; i < j; i++, j--) { - if (vec[i] != vec[j]) return false; - } - return true; - } -}; -``` - -上面代码可以在优化,就是先求出链表长度,然后给定vector的初始长度,这样避免vector每次添加节点重新开辟空间 - -``` -class Solution { -public: - bool isPalindrome(ListNode* head) { - - ListNode* cur = head; - int length = 0; - while (cur) { - length++; - cur = cur->next; - } - vector vec(length, 0); // 给定vector的初始长度,这样避免vector每次添加节点重新开辟空间 - cur = head; - int index = 0; - while (cur) { - vec[index++] = cur->val; - cur = cur->next; - } - // 比较数组回文 - for (int i = 0, j = vec.size() - 1; i < j; i++, j--) { - if (vec[i] != vec[j]) return false; - } - return true; - } -}; - -``` - -### 反转后半部分链表 - -分为如下几步: - -* 用快慢指针,快指针有两步,慢指针走一步,快指针遇到终止位置时,慢指针就在链表中间位置 -* 同时用pre记录慢指针指向节点的前一个节点,用来分割链表 -* 将链表分为前后均等两部分,如果链表长度是奇数,那么后半部分多一个节点 -* 将后半部分反转 ,得cur2,前半部分为cur1 -* 按照cur1的长度,一次比较cur1和cur2的节点数值 - -如图所示: - - - -代码如下: - -``` -class Solution { -public: - bool isPalindrome(ListNode* head) { - if (head == nullptr || head->next == nullptr) return true; - ListNode* slow = head; // 慢指针,找到链表中间分位置,作为分割 - ListNode* fast = head; - ListNode* pre = head; // 记录慢指针的前一个节点,用来分割链表 - while (fast && fast->next) { - pre = slow; - slow = slow->next; - fast = fast->next->next; - } - pre->next = nullptr; // 分割链表 - - ListNode* cur1 = head; // 前半部分 - ListNode* cur2 = reverseList(slow); // 反转后半部分,总链表长度如果是奇数,cur2比cur1多一个节点 - - // 开始两个链表的比较 - while (cur1) { - if (cur1->val != cur2->val) return false; - cur1 = cur1->next; - cur2 = cur2->next; - } - return true; - } - // 反转链表 - ListNode* reverseList(ListNode* head) { - ListNode* temp; // 保存cur的下一个节点 - ListNode* cur = head; - ListNode* pre = nullptr; - while(cur) { - temp = cur->next; // 保存一下 cur的下一个节点,因为接下来要改变cur->next - cur->next = pre; // 翻转操作 - // 更新pre 和 cur指针 - pre = cur; - cur = temp; - } - return pre; - } -}; -``` - -栈 - diff --git a/problems/0235.二叉搜索树的最近公共祖先.md b/problems/0235.二叉搜索树的最近公共祖先.md deleted file mode 100644 index 3104b297..00000000 --- a/problems/0235.二叉搜索树的最近公共祖先.md +++ /dev/null @@ -1,223 +0,0 @@ -## 链接 -https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-search-tree/ - -> 二叉搜索树的最近公共祖先问题如约而至 - -# 235. 二叉搜索树的最近公共祖先 - -链接:https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-search-tree/ - -给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。 - -百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。” - -例如,给定如下二叉搜索树:  root = [6,2,8,0,4,7,9,null,null,3,5] - -![235. 二叉搜索树的最近公共祖先](https://img-blog.csdnimg.cn/20201018172243602.png) - -示例 1: - -输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8 -输出: 6 -解释: 节点 2 和节点 8 的最近公共祖先是 6。 -示例 2: - -输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4 -输出: 2 -解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。 -  - -说明: - -* 所有节点的值都是唯一的。 -* p、q 为不同节点且均存在于给定的二叉搜索树中。 - -## 思路 - -做过[二叉树:公共祖先问题](https://mp.weixin.qq.com/s/n6Rk3nc_X3TSkhXHrVmBTQ)题目的同学应该知道,利用回溯从底向上搜索,遇到一个节点的左子树里有p,右子树里有q,那么当前节点就是最近公共祖先。 - -那么本题是二叉搜索树,二叉搜索树是有序的,那得好好利用一下这个特点。 - -在有序树里,如果判断一个节点的左子树里有p,右子树里有q呢? - -其实只要从上到下遍历的时候,cur节点是数值在[p, q]区间中则说明该节点cur就是最近公共祖先了。 - -理解这一点,本题就很好解了。 - -和[二叉树:公共祖先问题](https://mp.weixin.qq.com/s/n6Rk3nc_X3TSkhXHrVmBTQ)不同,普通二叉树求最近公共祖先需要使用回溯,从底向上来查找,二叉搜索树就不用了,因为搜索树有序(相当于自带方向),那么只要从上向下遍历就可以了。 - -那么我们可以采用前序遍历(其实这里没有中节点的处理逻辑,遍历顺序无所谓了)。 - -如图所示:p为节点3,q为节点5 - - - -可以看出直接按照指定的方向,就可以找到节点4,为最近公共祖先,而且不需要遍历整棵树,找到结果直接返回! - - -递归三部曲如下: - -* 确定递归函数返回值以及参数 - -参数就是当前节点,以及两个结点 p、q。 - -返回值是要返回最近公共祖先,所以是TreeNode * 。 - -代码如下: - -``` -TreeNode* traversal(TreeNode* cur, TreeNode* p, TreeNode* q) -``` - -* 确定终止条件 - -遇到空返回就可以了,代码如下: - -``` -if (cur == NULL) return cur; -``` - -其实都不需要这个终止条件,因为题目中说了p、q 为不同节点且均存在于给定的二叉搜索树中。也就是说一定会找到公共祖先的,所以并不存在遇到空的情况。 - -* 确定单层递归的逻辑 - -在遍历二叉搜索树的时候就是寻找区间[p->val, q->val](注意这里是左闭又闭) - -那么如果 cur->val 大于 p->val,同时 cur->val 大于q->val,那么就应该向左遍历(说明目标区间在左子树上)。 - -**需要注意的是此时不知道p和q谁大,所以两个都要判断** - -代码如下: - -``` -if (cur->val > p->val && cur->val > q->val) { - TreeNode* left = traversal(cur->left, p, q); - if (left != NULL) { - return left; - } -} -``` - -**细心的同学会发现,在这里调用递归函数的地方,把递归函数的返回值left,直接return**。 - - -在[二叉树:公共祖先问题](https://mp.weixin.qq.com/s/n6Rk3nc_X3TSkhXHrVmBTQ)中,如果递归函数有返回值,如何区分要搜索一条边,还是搜索整个树。 - -搜索一条边的写法: - -``` -if (递归函数(root->left)) return ; - -if (递归函数(root->right)) return ; -``` - -搜索整个树写法: - -``` -left = 递归函数(root->left); -right = 递归函数(root->right); -left与right的逻辑处理; -``` - -本题就是标准的搜索一条边的写法,遇到递归函数的返回值,如果不为空,立刻返回。 - - -如果 cur->val 小于 p->val,同时 cur->val 小于 q->val,那么就应该向右遍历(目标区间在右子树)。 - -``` -if (cur->val < p->val && cur->val < q->val) { - TreeNode* right = traversal(cur->right, p, q); - if (right != NULL) { - return right; - } -} -``` - -剩下的情况,就是cur节点在区间(p->val <= cur->val && cur->val <= q->val)或者 (q->val <= cur->val && cur->val <= p->val)中,那么cur就是最近公共祖先了,直接返回cur。 - -代码如下: -``` -return cur; - -``` - -那么整体递归代码如下: - -``` -class Solution { -private: - TreeNode* traversal(TreeNode* cur, TreeNode* p, TreeNode* q) { - if (cur == NULL) return cur; - // 中 - if (cur->val > p->val && cur->val > q->val) { // 左 - TreeNode* left = traversal(cur->left, p, q); - if (left != NULL) { - return left; - } - } - - if (cur->val < p->val && cur->val < q->val) { // 右 - TreeNode* right = traversal(cur->right, p, q); - if (right != NULL) { - return right; - } - } - return cur; - } -public: - TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { - return traversal(root, p, q); - } -}; -``` - -精简后代码如下: - -``` -class Solution { -public: - TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { - if (root->val > p->val && root->val > q->val) { - return lowestCommonAncestor(root->left, p, q); - } else if (root->val < p->val && root->val < q->val) { - return lowestCommonAncestor(root->right, p, q); - } else return root; - } -}; -``` - -## 迭代法 - -对于二叉搜索树的迭代法,大家应该在[二叉树:二叉搜索树登场!](https://mp.weixin.qq.com/s/vsKrWRlETxCVsiRr8v_hHg)就了解了。 - -利用其有序性,迭代的方式还是比较简单的,解题思路在递归中已经分析了。 - -迭代代码如下: - -``` -class Solution { -public: - TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { - while(root) { - if (root->val > p->val && root->val > q->val) { - root = root->left; - } else if (root->val < p->val && root->val < q->val) { - root = root->right; - } else return root; - } - return NULL; - } -}; -``` - -灵魂拷问:是不是又被简单的迭代法感动到痛哭流涕? - -# 总结 - -对于二叉搜索树的最近祖先问题,其实要比[普通二叉树公共祖先问题](https://mp.weixin.qq.com/s/n6Rk3nc_X3TSkhXHrVmBTQ)简单的多。 - -不用使用回溯,二叉搜索树自带方向性,可以方便的从上向下查找目标区间,遇到目标区间内的节点,直接返回。 - -最后给出了对应的迭代法,二叉搜索树的迭代法甚至比递归更容易理解,也是因为其有序性(自带方向性),按照目标区间找就行了。 - -**就酱,学到了,就转发给身边需要学习的同学吧!** diff --git a/problems/0236.二叉树的最近公共祖先.md b/problems/0236.二叉树的最近公共祖先.md deleted file mode 100644 index 5b4ac984..00000000 --- a/problems/0236.二叉树的最近公共祖先.md +++ /dev/null @@ -1,213 +0,0 @@ -## 链接 -https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/ - -> 本来是打算将二叉树和二叉搜索树的公共祖先问题一起讲,后来发现篇幅过长了,只能先说一说二叉树的公共祖先问题。 - -# 236. 二叉树的最近公共祖先 - -给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 - -百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。” - -例如,给定如下二叉树:  root = [3,5,1,6,2,0,8,null,null,7,4] - -![236. 二叉树的最近公共祖先](https://img-blog.csdnimg.cn/20201016173414722.png) - -示例 1: -输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1 -输出: 3 -解释: 节点 5 和节点 1 的最近公共祖先是节点 3。 - -示例 2: -输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4 -输出: 5 -解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。 - -说明: -* 所有节点的值都是唯一的。 -* p、q 为不同节点且均存在于给定的二叉树中。 - -## 思路 - -遇到这个题目首先想的是要是能自底向上查找就好了,这样就可以找到公共祖先了。 - -那么二叉树如何可以自底向上查找呢? - -回溯啊,二叉树回溯的过程就是从低到上。 - -后序遍历就是天然的回溯过程,最先处理的一定是叶子节点。 - -接下来就看如何判断一个节点是节点q和节点p的公共公共祖先呢。 - -**如果找到一个节点,发现左子树出现结点p,右子树出现节点q,或者 左子树出现结点q,右子树出现节点p,那么该节点就是节点p和q的最近公共祖先。** - -使用后序遍历,回溯的过程,就是从低向上遍历节点,一旦发现如何这个条件的节点,就是最近公共节点了。 - -递归三部曲: - -* 确定递归函数返回值以及参数 - -需要递归函数返回值,来告诉我们是否找到节点q或者p,那么返回值为bool类型就可以了。 - -但我们还要返回最近公共节点,可以利用上题目中返回值是TreeNode * ,那么如果遇到p或者q,就把q或者p返回,返回值不为空,就说明找到了q或者p。 - -代码如下: - -``` -TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) -``` - -* 确定终止条件 - -如果找到了 节点p或者q,或者遇到空节点,就返回。 - -代码如下: - -``` -if (root == q || root == p || root == NULL) return root; -``` - -* 确定单层递归逻辑 - -值得注意的是 本题函数有返回值,是因为回溯的过程需要递归函数的返回值做判断,但本题我们依然要遍历树的所有节点。 - -我们在[二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?](https://mp.weixin.qq.com/s/6TWAVjxQ34kVqROWgcRFOg)中说了 递归函数有返回值就是要遍历某一条边,但有返回值也要看如何处理返回值! - -如果递归函数有返回值,如何区分要搜索一条边,还是搜索整个树呢? - -搜索一条边的写法: - -``` -if (递归函数(root->left)) return ; - -if (递归函数(root->right)) return ; -``` - -搜索整个树写法: - -``` -left = 递归函数(root->left); -right = 递归函数(root->right); -left与right的逻辑处理; -``` - -看出区别了没? - -**在递归函数有返回值的情况下:如果要搜索一条边,递归函数返回值不为空的时候,立刻返回,如果搜索整个树,直接用一个变量left、right接住返回值,这个left、right后序还有逻辑处理的需要,也就是后序遍历中处理中间节点的逻辑(也是回溯)**。 - -那么为什么要遍历整颗树呢?直观上来看,找到最近公共祖先,直接一路返回就可以了。 - -如图: - - - -就像图中一样直接返回7,多美滋滋。 - -但事实上还要遍历根节点右子树(即使此时已经找到了目标节点了),也就是图中的节点4、15、20。 - -因为在如下代码的后序遍历中,如果想利用left和right做逻辑处理, 不能立刻返回,而是要等left与right逻辑处理完之后才能返回。 - -``` -left = 递归函数(root->left); -right = 递归函数(root->right); -left与right的逻辑处理; -``` - -所以此时大家要知道我们要遍历整棵树。知道这一点,对本题就有一定深度的理解了。 - - -那么先用left和right接住左子树和右子树的返回值,代码如下: - -``` -TreeNode* left = lowestCommonAncestor(root->left, p, q); -TreeNode* right = lowestCommonAncestor(root->right, p, q); - -``` - -**如果left 和 right都不为空,说明此时root就是最近公共节点。这个比较好理解** - -**如果left为空,right不为空,就返回right,说明目标节点是通过right返回的,反之依然**。 - -这里有的同学就理解不了了,为什么left为空,right不为空,目标节点通过right返回呢? - -如图: - - - -图中节点10的左子树返回null,右子树返回目标值7,那么此时节点10的处理逻辑就是把右子树的返回值(最近公共祖先7)返回上去! - -这里点也很重要,可能刷过这道题目的同学,都不清楚结果究竟是如何从底层一层一层传到头结点的。 - -那么如果left和right都为空,则返回left或者right都是可以的,也就是返回空。 - -代码如下: - -``` -if (left == NULL && right != NULL) return right; -else if (left != NULL && right == NULL) return left; -else { // (left == NULL && right == NULL) - return NULL; -} - -``` - -那么寻找最小公共祖先,完整流程图如下: - - - -**从图中,大家可以看到,我们是如何回溯遍历整颗二叉树,将结果返回给头结点的!** - -整体代码如下: - -``` -class Solution { -public: - TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { - if (root == q || root == p || root == NULL) return root; - TreeNode* left = lowestCommonAncestor(root->left, p, q); - TreeNode* right = lowestCommonAncestor(root->right, p, q); - if (left != NULL && right != NULL) return root; - - if (left == NULL && right != NULL) return right; - else if (left != NULL && right == NULL) return left; - else { // (left == NULL && right == NULL) - return NULL; - } - - } -}; -``` - -稍加精简,代码如下: - -``` -class Solution { -public: - TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { - if (root == q || root == p || root == NULL) return root; - TreeNode* left = lowestCommonAncestor(root->left, p, q); - TreeNode* right = lowestCommonAncestor(root->right, p, q); - if (left != NULL && right != NULL) return root; - if (left == NULL) return right; - return left; - } -}; -``` - -# 总结 - -这道题目刷过的同学未必真正了解这里面回溯的过程,以及结果是如何一层一层传上去的。 - -**那么我给大家归纳如下三点**: - -1. 求最小公共祖先,需要从底向上遍历,那么二叉树,只能通过后序遍历(即:回溯)实现从低向上的遍历方式。 - -2. 在回溯的过程中,必然要遍历整颗二叉树,即使已经找到结果了,依然要把其他节点遍历完,因为要使用递归函数的返回值(也就是代码中的left和right)做逻辑判断。 - -3. 要理解如果返回值left为空,right不为空为什么要返回right,为什么可以用返回right传给上一层结果。 - -可以说这里每一步,都是有难度的,都需要对二叉树,递归和回溯有一定的理解。 - -本题没有给出迭代法,因为迭代法不适合模拟回溯的过程。理解递归的解法就够了。 - -**就酱,转发给身边需要学习的同学吧!** diff --git a/problems/0237.删除链表中的节点.md b/problems/0237.删除链表中的节点.md deleted file mode 100644 index b2ae2be9..00000000 --- a/problems/0237.删除链表中的节点.md +++ /dev/null @@ -1,23 +0,0 @@ - -## 题目地址 -https://leetcode-cn.com/problems/delete-node-in-a-linked-list/ - -## 思路 - -题目中已经说了是非末尾节点,所以不用对末尾节点的情况经行判断 - -**链表中所有节点的值都是唯一的。** 题目题意强调这一点,是为什么,因为 我们删除的node在内存地址 删除的并不是这个。 -## 代码 - -``` -class Solution { -public: - void deleteNode(ListNode* node) { - node->val = node->next->val;// 将前一个值赋给当前node - node->next = node->next->next; // 将当前node的next 指向下下node - } -}; -``` - -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0239.滑动窗口最大值.md b/problems/0239.滑动窗口最大值.md deleted file mode 100644 index 1a0b32ab..00000000 --- a/problems/0239.滑动窗口最大值.md +++ /dev/null @@ -1,197 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/sliding-window-maximum/ - -> 要用啥数据结构呢? - -# 239. 滑动窗口最大值 - -给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 - -返回滑动窗口中的最大值。 - -进阶: - -你能在线性时间复杂度内解决此题吗? - -  - - -提示: - -1 <= nums.length <= 10^5 --10^4 <= nums[i] <= 10^4 -1 <= k <= nums.length - - - -# 思路 - -这是使用单调队列的经典题目。 - -难点是如何求一个区间里的最大值呢? (这好像是废话),暴力一下不就得了。 - -暴力方法,遍历一遍的过程中每次从窗口中在找到最大的数值,这样很明显是O(n * k)的算法。 - -有的同学可能会想用一个大顶堆(优先级队列)来存放这个窗口里的k个数字,这样就可以知道最大的最大值是多少了, **但是问题是这个窗口是移动的,而大顶堆每次只能弹出最大值,我们无法移除其他数值,这样就造成大顶堆维护的不是滑动窗口里面的数值了。所以不能用大顶堆。** - -此时我们需要一个队列,这个队列呢,放进去窗口里的元素,然后随着窗口的移动,队列也一进一出,每次移动之后,队列告诉我们里面的最大值是什么。 - - -这个队列应该长这个样子: - -``` -class MyQueue { -public: - void pop(int value) { - } - void push(int value) { - } - int front() { - return que.front(); - } -}; -``` - -每次窗口移动的时候,调用que.pop(滑动窗口中移除元素的数值),que.push(滑动窗口添加元素的数值),然后que.front()就返回我们要的最大值。 - -这么个队列香不香,要是有现成的这种数据结构是不是更香了! - -**可惜了,没有! 我们需要自己实现这么个队列。** - -然后在分析一下,队列里的元素一定是要排序的,而且要最大值放在出队口,要不然怎么知道最大值呢。 - -但如果把窗口里的元素都放进队列里,窗口移动的时候,队列需要弹出元素。 - -那么问题来了,已经排序之后的队列 怎么能把窗口要移除的元素(这个元素可不一定是最大值)弹出呢。 - -大家此时应该陷入深思..... - -**其实队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队里里的元素数值是由大到小的。** - -那么这个维护元素单调递减的队列就叫做**单调队列,即单调递减或单调递增的队列。C++中没有直接支持单调队列,需要我们自己来一个单调队列** - -**不要以为实现的单调队列就是 对窗口里面的数进行排序,如果排序的话,那和优先级队列又有什么区别了呢。** - -来看一下单调队列如何维护队列里的元素。 - -动画如下: - - - -对于窗口里的元素{2, 3, 5, 1 ,4},单调队列里只维护{5, 4} 就够了,保持单调队列里单调递减,此时队列出口元素就是窗口里最大元素。 - -此时大家应该怀疑单调队列里维护着{5, 4} 怎么配合窗口经行滑动呢? - -设计单调队列的时候,pop,和push操作要保持如下规则: - -1. pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作 -2. push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止 - -保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。 - -为了更直观的感受到单调队列的工作过程,以题目示例为例,输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3,动画如下: - - - -那么我们用什么数据结构来实现这个单调队列呢? - -使用deque最为合适,在文章[栈与队列:来看看栈和队列不为人知的一面](https://mp.weixin.qq.com/s/VZRjOccyE09aE-MgLbCMjQ)中,我们就提到了常用的queue在没有指定容器的情况下,deque就是默认底层容器。 - -基于刚刚说过的单调队列pop和push的规则,代码不难实现,如下: - -``` -class MyQueue { //单调队列(从大到小) -public: - deque que; // 使用deque来实现单调队列 - // 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。 - // 同时pop之前判断队列当前是否为空。 - void pop(int value) { - if (!que.empty() && value == que.front()) { - que.pop_front(); - } - } - // 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。 - // 这样就保持了队列里的数值是单调从大到小的了。 - void push(int value) { - while (!que.empty() && value > que.back()) { - que.pop_back(); - } - que.push_back(value); - - } - // 查询当前队列里的最大值 直接返回队列前端也就是front就可以了。 - int front() { - return que.front(); - } -}; -``` - - -这样我们就用deque实现了一个单调队列,接下来解决滑动窗口最大值的问题就很简单了,直接看代码吧。 - -# C++代码 - -``` -class Solution { -private: - class MyQueue { //单调队列(从大到小) - public: - deque que; // 使用deque来实现单调队列 - // 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。 - // 同时pop之前判断队列当前是否为空。 - void pop(int value) { - if (!que.empty() && value == que.front()) { - que.pop_front(); - } - } - // 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。 - // 这样就保持了队列里的数值是单调从大到小的了。 - void push(int value) { - while (!que.empty() && value > que.back()) { - que.pop_back(); - } - que.push_back(value); - - } - // 查询当前队列里的最大值 直接返回队列前端也就是front就可以了。 - int front() { - return que.front(); - } - }; -public: - vector maxSlidingWindow(vector& nums, int k) { - MyQueue que; - vector result; - for (int i = 0; i < k; i++) { // 先将前k的元素放进队列 - que.push(nums[i]); - } - result.push_back(que.front()); // result 记录前k的元素的最大值 - for (int i = k; i < nums.size(); i++) { - que.pop(nums[i - k]); // 滑动窗口移除最前面元素 - que.push(nums[i]); // 滑动窗口前加入最后面的元素 - result.push_back(que.front()); // 记录对应的最大值 - } - return result; - } -}; -``` - -在来看一下时间复杂度,使用单调队列的时间复杂度是 O(n)。 - -有的同学可能想了,在队列中 push元素的过程中,还有pop操作呢,感觉不是纯粹的O(n)。 - -其实,大家可以自己观察一下单调队列的实现,nums 中的每个元素最多也就被 push_back 和 pop_back 各一次,没有任何多余操作,所以整体的复杂度还是 O(n)。 - -空间复杂度因为我们定义一个辅助队列,所以是O(k)。 - -# 扩展 - -大家貌似对单调队列 都有一些疑惑,首先要明确的是,题解中单调队列里的pop和push接口,仅适用于本题哈。单调队列不是一成不变的,而是不同场景不同写法,总之要保证队列里单调递减或递增的原则,所以叫做单调队列。 不要以为本地中的单调队列实现就是固定的写法哈。 - -大家貌似对deque也有一些疑惑,C++中deque是stack和queue默认的底层实现容器(这个我们之前已经讲过啦),deque是可以两边扩展的,而且deque里元素并不是严格的连续分布的。 - - - - -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0242.有效的字母异位词.md b/problems/0242.有效的字母异位词.md deleted file mode 100644 index 92326b02..00000000 --- a/problems/0242.有效的字母异位词.md +++ /dev/null @@ -1,78 +0,0 @@ -## 题目地址 - -https://leetcode-cn.com/problems/valid-anagram/ - -> 数组就是简单的哈希表,但是数组的大小可不是无限开辟的 - -# 第242题.有效的字母异位词 - -给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。 - -![](https://img-blog.csdnimg.cn/202008171902298.png) - -**说明:** -你可以假设字符串只包含小写字母。 - -# 思路 - -先看暴力的解法,两层for循环,同时还要记录字符是否重复出现,很明显时间复杂度是 O(n^2)。 - -暴力的方法这里就不做介绍了,直接看一下有没有更优的方式。 - -**数组其实就是一个简单哈希表**,而且这道题目中字符串只有小写字符,那么就可以定义一个数组,来记录字符串s里字符出现的次数。 - -如果对哈希表的理论基础关于数组,set,map不了解的话可以看这篇:[关于哈希表,你该了解这些!](https://mp.weixin.qq.com/s/g8N6WmoQmsCUw3_BaWxHZA) - -需要定义一个多大的数组呢,定一个数组叫做record,大小为26 就可以了,初始化为0,因为字符a到字符z的ASCII也是26个连续的数值。 - -为了方便举例,判断一下字符串s= "aee", t = "eae"。 - -操作动画如下: - - - -定义一个数组叫做record用来上记录字符串s里字符出现的次数。 - -需要把字符映射到数组也就是哈希表的索引下表上,**因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下表0,相应的字符z映射为下表25。** - -再遍历 字符串s的时候,**只需要将 s[i] - ‘a’ 所在的元素做+1 操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了。** 这样就将字符串s中字符出现的次数,统计出来了。 - -那看一下如何检查字符串t中是否出现了这些字符,同样在遍历字符串t的时候,对t中出现的字符映射哈希表索引上的数值再做-1的操作。 - -那么最后检查一下,**record数组如果有的元素不为零0,说明字符串s和t一定是谁多了字符或者谁少了字符,return false。** - -最后如果record数组所有元素都为零0,说明字符串s和t是字母异位词,return true。 - -时间复杂度为O(n),空间上因为定义是的一个常量大小的辅助数组,所以空间复杂度为O(1)。 - -看完这篇哈希表总结:[哈希表:总结篇!(每逢总结必经典)](https://mp.weixin.qq.com/s/1s91yXtarL-PkX07BfnwLg),详细就可以哈希表的各种用法非常清晰了。 - -# C++ 代码 -``` -class Solution { -public: - bool isAnagram(string s, string t) { - int record[26] = {0}; - for (int i = 0; i < s.size(); i++) { - // 并不需要记住字符a的ASCII,只要求出一个相对数值就可以了 - record[s[i] - 'a']++; - } - for (int i = 0; i < t.size(); i++) { - record[t[i] - 'a']--; - } - for (int i = 0; i < 26; i++) { - if (record[i] != 0) { - // record数组如果有的元素不为零0,说明字符串s和t 一定是谁多了字符或者谁少了字符。 - return false; - } - } - // record数组所有元素都为零0,说明字符串s和t是字母异位词 - return true; - } -}; -``` -> **我是[程序员Carl](https://github.com/youngyangyang04),[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png)可以找我,本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** - - diff --git a/problems/0257.二叉树的所有路径.md b/problems/0257.二叉树的所有路径.md deleted file mode 100644 index d63fa41d..00000000 --- a/problems/0257.二叉树的所有路径.md +++ /dev/null @@ -1,271 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/binary-tree-paths/ - -> 以为只用了递归,其实还用了回溯 - -# 257. 二叉树的所有路径 - -给定一个二叉树,返回所有从根节点到叶子节点的路径。 - -说明: 叶子节点是指没有子节点的节点。 - -示例: - - -# 思路 - -这道题目要求从根节点到叶子的路径,所以需要前序遍历,这样才方便让父节点指向孩子节点,找到对应的路径。 - -在这道题目中将第一次涉及到回溯,因为我们要把路径记录下来,需要回溯来回退一一个路径在进入另一个路径。 - -前序遍历以及回溯的过程如图: - - - -我们先使用递归的方式,来做前序遍历。**要知道递归和回溯就是一家的,本题也需要回溯。** - -## 递归 - -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 结构path来记录路径,所以要把vector 结构的path转为string格式,在把这个string 放进 result里。 - -**那么为什么使用了vector 结构来记录路径呢?** 因为在下面处理单层递归逻辑的时候,要做回溯,使用vector方便来做回溯。 - -可能有的同学问了,我看有些人的代码也没有回溯啊。 - -**其实是有回溯的,只不过隐藏在函数调用时的参数赋值里**,下文我还会提到。 - -这里我们先使用vector结构的path容器来记录路径,那么终止处理逻辑如下: - -``` -if (cur->left == NULL && cur->right == NULL) { // 遇到叶子节点 - string sPath; - for (int i = 0; i < path.size() - 1; i++) { // 将path里记录的路径转为string格式 - 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(); // 回溯 -} -``` - -那么本题整体代码如下: - -``` -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://mp.weixin.qq.com/s/ivLkHzWdhjQQD1rQWe6zWA),我这这篇中详细的解释了递归中如何隐藏着回溯。 ** - - - -**综合以上,第二种递归的代码虽然精简但把很多重要的点隐藏在了代码细节里,第一种递归写法虽然代码多一些,但是把每一个逻辑处理都完整的展现了出来了。** - - - -## 迭代法 - -至于非递归的方式,我们可以依然可以使用前序遍历的迭代方式来模拟遍历路径的过程,对该迭代方式不了解的同学,可以看文章[二叉树:听说递归能做的,栈也能做!](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg)和[二叉树:前中后序迭代方式的写法就不能统一一下么?](https://mp.weixin.qq.com/s/WKg0Ty1_3SZkztpHubZPRg)。 - -这里除了模拟递归需要一个栈,同时还需要一个栈来存放对应的遍历路径。 - -C++代码如下: - -``` -class Solution { -public: - vector binaryTreePaths(TreeNode* root) { - stack treeSt;// 保存树的遍历节点 - stack pathSt; // 保存遍历路径的节点 - vector result; // 保存最终路径集合 - if (root == NULL) return result; - treeSt.push(root); - pathSt.push(to_string(root->val)); - while (!treeSt.empty()) { - TreeNode* node = treeSt.top(); treeSt.pop(); // 取出节点 中 - string path = pathSt.top();pathSt.pop(); // 取出该节点对应的路径 - if (node->left == NULL && node->right == NULL) { // 遇到叶子节点 - result.push_back(path); - } - if (node->right) { // 右 - treeSt.push(node->right); - pathSt.push(path + "->" + to_string(node->right->val)); - } - if (node->left) { // 左 - treeSt.push(node->left); - pathSt.push(path + "->" + to_string(node->left->val)); - } - } - return result; - } -}; -``` -当然,使用java的同学,可以直接定义一个成员变量为object的栈`Stack stack = new Stack<>();`,这样就不用定义两个栈了,都放到一个栈里就可以了。 - -# 总结 - -**本文我们开始初步涉及到了回溯,很多同学过了这道题目,可能都不知道自己其实使用了回溯,回溯和递归都是相伴相生的。** - -我在第一版递归代码中,把递归与回溯的细节都充分的展现了出来,大家可以自己感受一下。 - -第二版递归代码对于初学者其实非常不友好,代码看上去简单,但是隐藏细节于无形。 - -最后我依然给出了迭代法。 - -对于本地充分了解递归与回溯的过程之后,有精力的同学可以在去实现迭代法。 - - - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0279.完全平方数.md b/problems/0279.完全平方数.md deleted file mode 100644 index 21178942..00000000 --- a/problems/0279.完全平方数.md +++ /dev/null @@ -1,67 +0,0 @@ -没有问你组合方式,而是问你最小个数 - -和322 一个套路 - -// 组合的逻辑 -``` -// 版本一 -class Solution { -public: - int numSquares(int n) { - vector sum; - for (int i = 1; i * i <= n; i++) { - sum.push_back(i * i); - } - vector dp(n + 1, INT_MAX); - dp[0] = 0; - for (int i = 0; i < sum.size(); i++) { - for (int j = 1; j <= n; j++) { - if (j - sum[i] >= 0 && dp[j - sum[i]] != INT_MAX) { - dp[j] = min(dp[j - sum[i]] + 1, dp[j]); - } - } - } - return dp[n]; - } -}; -``` - -优化一下代码,可以不用预先用sum数组来装i * i,但是版本一更清晰一些,代码如下: -``` -// 版本二 -class Solution { -public: - int numSquares(int n) { - vector dp(n + 1, INT_MAX); - dp[0] = 0; - for (int i = 1; i * i <= n; i++) { - for (int j = 1; j <= n; j++) { - if (j - i * i >= 0 && dp[j - i * i ] != INT_MAX) { - dp[j] = min(dp[j - i * i ] + 1, dp[j]); - } - } - } - return dp[n]; - } -}; - -``` - - -// 排列的逻辑 -``` -// 版本三 -class Solution { -public: - int numSquares(int n) { - vector dp(n + 1, 0); - for (int i = 0; i <= n; i++) { - dp[i] = i; // 最多也就是i都是1组成的情况 - for (int j = 1; j * j <= i; j++) { - dp[i] = min(dp[i - j * j] + 1, dp[i]); - } - } - return dp[n]; - } -}; -``` diff --git a/problems/0283.移动零.md b/problems/0283.移动零.md deleted file mode 100644 index 638f2cdf..00000000 --- a/problems/0283.移动零.md +++ /dev/null @@ -1,40 +0,0 @@ - -# 思路 - -做这道题目之前,大家可以做一做[27.移除元素](https://leetcode-cn.com/problems/remove-element/),我在讲解数组系列的时候针对移除元素写了这篇题解:[数组:就移除个元素很难么?](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA) - -这道题目,使用暴力的解法,可以两层for循环,模拟数组删除元素(也就是向前覆盖)的过程。 - -好了,我们说一说双指针法,大家如果对双指针还不熟悉,可以看我的这篇总结[双指针法:总结篇!](https://mp.weixin.qq.com/s/_p7grwjISfMh0U65uOyCjA)。 - -双指针法在数组移除元素中,可以达到O(n)的时间复杂度,在[数组:就移除个元素很难么?](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA)里已经详细讲解了,那么本题和移除元素其实是一个套路。 - -**相当于对整个数组移除元素0,然后slowIndex之后都是移除元素0的冗余元素,把这些元素都赋值为0就可以了**。 - -如动画所示: - - - -C++代码如下: - -``` -class Solution { -public: - void moveZeroes(vector& nums) { - int slowIndex = 0; - for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) { - if (nums[fastIndex] != 0) { - nums[slowIndex++] = nums[fastIndex]; - } - } - // 将slowIndex之后的冗余元素赋值为0 - for (int i = slowIndex; i < nums.size(); i++) { - nums[i] = 0; - } - } -}; -``` -> **我是[程序员Carl](https://github.com/youngyangyang04),[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png)可以找我,本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/0300.最长上升子序列.md b/problems/0300.最长上升子序列.md deleted file mode 100644 index 5582c8ac..00000000 --- a/problems/0300.最长上升子序列.md +++ /dev/null @@ -1,37 +0,0 @@ - -## 思路 -* dp[i]的定义 - -dp[i]表示i之前包括i的最长上升子序列。 - - -* dp[i]的初始化 - -每一个i,对应的dp[i](即最长上升子序列)起始大小至少是1. - - -* 状态转移方程 - -位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 + 1 的最大值。 - -if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1); - -``` -class Solution { -public: - int lengthOfLIS(vector& nums) { - if (nums.size() <= 1) return nums.size(); - vector dp(nums.size(), 1); - int result = 0; - for (int i = 1; i < nums.size(); i++) { - for (int j = 0; j < i; j++) { - if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1); - } - if (dp[i] > result) result = dp[i]; // 取长的子序列 - //for (int j = 0 ; j < nums.size(); j++) cout << dp[j] << " "; - //cout << endl; - } - return result; - } -}; -``` diff --git a/problems/0316.去除重复字母.md b/problems/0316.去除重复字母.md deleted file mode 100644 index 787cc371..00000000 --- a/problems/0316.去除重复字母.md +++ /dev/null @@ -1,52 +0,0 @@ -# 思路 - -// 浓浓的单调栈气息 - -这道题目一点都不简单,输入单调栈里情况比较多的题目。 - -需要解决如下问题: -// 这不是单纯遇到小的 栈里就弹出,判断是否在栈里,情况2 -// 如何记录它之前有没有出现过呢,也就是情况1 - - -情况1: -输入:"bbcaac" -输出:"ac" -预期结果:"bac" - -情况2 -输入:"abacb" -输出:"acb" -预期结果:"abc" - -情况3: -aba 输出 a 预期是ab - -``` -class Solution { -public: - string removeDuplicateLetters(string s) { - int letterCount[26] = {0}; - for (int i = 0; i < s.size(); i++) { - letterCount[s[i] - 'a']++; - } - bool isIn[26] = {false}; // 1 已经在栈里,0 不在栈里 - string st; - for (int i = 0; i < s.size(); i++) { - while(!st.empty() - && s[i] < st.back() - && letterCount[st.back() - 'a'] > 0 // 保证字符串i之后还有这个栈顶元素,栈才能做弹出操作,情况3 - && isIn[s[i] - 'a'] == false) { // 如果栈里已经有s[i]了,跳过:情况2 - isIn[st.back() - 'a'] = false; - st.pop_back(); - } - if (isIn[s[i] - 'a'] == false) { - st.push_back(s[i]); - isIn[s[i] - 'a'] = true; - } - letterCount[s[i] - 'a']--; // 只要用过了就减一:情况1 - } - return st; - } -}; -``` diff --git a/problems/0322.零钱兑换.md b/problems/0322.零钱兑换.md deleted file mode 100644 index c308c7ab..00000000 --- a/problems/0322.零钱兑换.md +++ /dev/null @@ -1,155 +0,0 @@ - -# 思路 -* 确定dp数组以及下标的含义 -dp[j]:凑足总额为j所需钱币的最少个数为dp[j] - -* 确定递推公式 - -得到dp[j](考虑coins[i]),只有一个来源,dp[j - coins[i]](没有考虑coins[i])。 - -凑足总额为j - coins[i]的最少个数为dp[j - coins[i]],那么只需要加上一个钱币coins[i]即dp[j - coins[i]] + 1就是dp[j](考虑coins[i]) - -所以dp[j] 要去所有 dp[j - coins[i]] + 1 中最小的。 - -递推公式:dp[j] = min(dp[j - coins[i]] + 1, dp[j]); - -* dp数组如何初始化 - -首先凑足总金额为0所需钱币的个数一定是0,那么dp[0] = 0; - -其他下标对应的数值呢? - -考虑到递推公式的特性,dp[j]必须初始化为一个最大的数,否则就会在min(dp[j - coins[i]] + 1, dp[j])比较的过程中比初始值覆盖。 - -所以下标非0的元素都是应该是最大值。 - -代码如下: - -``` -vector dp(amount + 1, INT_MAX); -dp[0] = 0; -``` - -* 确定遍历顺序 - -求钱币最小个数,那么钱币有顺序,和钱币没有顺序都可以,都不影响钱币的最小个数。可以用背包组合方式或者排列方式来求。 - -如果本题要是求组成amount的有几种方式,那么钱币循序就有影响了。 - -所以两个for循环的关系是:coins放在外循环,target在内循环、或者target放在外循环,coins在内循环都是可以的! - -那么我采用coins放在外循环,target在内循环的方式。 - -本题钱币数量可以无限使用,那么是完全背包。所以遍历的内循环是正序 - -综上所述,遍历顺序为:coins放在外循环,target在内循环。且内循环正序。 - - -C++ 代码如下: - -``` -class Solution { -public: - int coinChange(vector& coins, int amount) { - vector dp(amount + 1, INT_MAX); - dp[0] = 0; - for (int i = 0 ;i < coins.size(); i++) { // 遍历钱币 - for (int j = coins[i]; j <= amount; j++) { // 遍历target - if (dp[j - coins[i]] != INT_MAX) { // 如果dp[j - coins[i]]是初始值则跳过 - dp[j] = min(dp[j - coins[i]] + 1, dp[j]); - } - } - } - if (dp[amount] == INT_MAX) return -1; - return dp[amount]; - } -}; -``` - -# 拓展 - -对于遍历方式target放在外循环,coins在内循环都是可以的,只不过对应的初始化操作有点微调,我就直接给出代码了 - -```C++ -class Solution { -public: - int coinChange(vector& coins, int amount) { - vector dp(amount + 1, INT_MAX); - dp[0] = 0; - for (int i = 1; i <= amount; i++) { - for (int j = 0; j < coins.size(); j++) { - if (i - coins[j] >= 0 && dp[i - coins[j]] != INT_MAX ) { - dp[i] = min(dp[i - coins[j]] + 1, dp[i]); - } - } - } - if (dp[amount] == INT_MAX) return -1; - return dp[amount]; - } -}; -``` - -# 总结 - -相信大家看网上的题解,一篇是遍历amount的for循环放外面,一篇是遍历amount的for循环放里面,看多了都看晕了,能把 遍历顺序讲明白的文章非常少。 - -这也是大多数同学学习动态规划的苦恼所在,有的时候递推公式其实很简单,但遍历顺序很难把握! - -那么Carl就把遍历顺序分析的清清楚楚,相信大家看完之后,对背包问题又了更深的理解了。 - -# tmp - -``` -// dp初始化很重要 -class Solution { -public: - int coinChange(vector& coins, int amount) { - //int dp[10003] = {0}; // 并没有给所有元素赋值0 - if (amount == 0) return 0; // 这个要注意 - vector dp(10003, 0); - // 不能这么初始化啊,[2147483647],2 这种例子 直接gg,但是这种初始化有助于理解 - for (int i = 0; i < coins.size(); i++) { - if (coins[i] <= amount) // 还必须要加这个判断 - dp[coins[i]] = 1; - } - for (int i = 1; i <= amount; i++) { - for (int j = 0; j < coins.size(); j++) { - if (i - coins[j] >= 0 && dp[i - coins[j]]!=0 ) { - if (dp[i] == 0) dp[i] = dp[i - coins[j]] + 1; - else dp[i] = min(dp[i - coins[j]] + 1, dp[i]); - } - } - //for (int k = 0 ; k<= amount; k++) { - // cout << dp[k] << " "; - //} - //cout << endl; - } - if (dp[amount] == 0) return -1; - return dp[amount]; - - } -}; -``` - -``` -class Solution { -public: - int coinChange(vector& coins, int amount) { - //int dp[10003] = {0}; // 并没有给所有元素赋值0 - if (amount == 0) return 0; // 这个要注意 - vector dp(amount + 1, 0); - for (int i = 1; i <= amount; i++) { - dp[i] = INT_MAX; - for (int j = 0; j < coins.size(); j++) { - if (i - coins[j] >= 0 && dp[i - coins[j]] != INT_MAX) { - dp[i] = min(dp[i - coins[j]] + 1, dp[i]); - } - } - } - if (dp[amount] == INT_MAX) return -1; - return dp[amount]; - } -}; -``` - - diff --git a/problems/0332.重新安排行程.md b/problems/0332.重新安排行程.md deleted file mode 100644 index 8ea8fc7a..00000000 --- a/problems/0332.重新安排行程.md +++ /dev/null @@ -1,252 +0,0 @@ - -> 这也可以用回溯法? 其实深搜和回溯也是相辅相成的,毕竟都用递归。 - -# 332.重新安排行程 - -题目地址:https://leetcode-cn.com/problems/reconstruct-itinerary/ - -给定一个机票的字符串二维数组 [from, to],子数组中的两个成员分别表示飞机出发和降落的机场地点,对该行程进行重新规划排序。所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。 - -提示: -* 如果存在多种有效的行程,请你按字符自然排序返回最小的行程组合。例如,行程 ["JFK", "LGA"] 与 ["JFK", "LGB"] 相比就更小,排序更靠前 -* 所有的机场都用三个大写字母表示(机场代码)。 -* 假定所有机票至少存在一种合理的行程。 -* 所有的机票必须都用一次 且 只能用一次。 -  - -示例 1: -输入:[["MUC", "LHR"], ["JFK", "MUC"], ["SFO", "SJC"], ["LHR", "SFO"]] -输出:["JFK", "MUC", "LHR", "SFO", "SJC"] - -示例 2: -输入:[["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]] -输出:["JFK","ATL","JFK","SFO","ATL","SFO"] -解释:另一种有效的行程是 ["JFK","SFO","ATL","JFK","ATL","SFO"]。但是它自然排序更大更靠后。 - -# 思路 - -这道题目还是很难的,之前我们用回溯法解决了如下问题:[组合问题](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ),[分割问题](https://mp.weixin.qq.com/s/v--VmA8tp9vs4bXCqHhBuA),[子集问题](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA),[排列问题](https://mp.weixin.qq.com/s/SCOjeMX1t41wcvJq49GhMw)。 - -直觉上来看 这道题和回溯法没有什么关系,更像是图论中的深度优先搜索。 - -实际上确实是深搜,但这是深搜中使用了回溯的例子,在查找路径的时候,如果不回溯,怎么能查到目标路径呢。 - -所以我倾向于说本题应该使用回溯法,那么我也用回溯法的思路来讲解本题,其实深搜一般都使用了回溯法的思路,在图论系列中我会再详细讲解深搜。 - -**这里就是先给大家拓展一下,原来回溯法还可以这么玩!** - -**这道题目有几个难点:** - -1. 一个行程中,如果航班处理不好容易变成一个圈,成为死循环 -2. 有多种解法,字母序靠前排在前面,让很多同学望而退步,如何该记录映射关系呢 ? -3. 使用回溯法(也可以说深搜) 的话,那么终止条件是什么呢? -4. 搜索的过程中,如何遍历一个机场所对应的所有机场。 - -针对以上问题我来逐一解答! - -## 如何理解死循环 - -对于死循环,我来举一个有重复机场的例子: - -![332.重新安排行程](https://img-blog.csdnimg.cn/20201115180537865.png) - -为什么要举这个例子呢,就是告诉大家,出发机场和到达机场也会重复的,**如果在解题的过程中没有对集合元素处理好,就会死循环。** - -## 该记录映射关系 - -有多种解法,字母序靠前排在前面,让很多同学望而退步,如何该记录映射关系呢 ? - -一个机场映射多个机场,机场之间要靠字母序排列,一个机场映射多个机场,可以使用std::unordered_map,如果让多个机场之间再有顺序的话,就是用std::map 或者std::multimap 或者 std::multiset。 - -如果对map 和 set 的实现机制不太了解,也不清楚为什么 map、multimap就是有序的同学,可以看这篇文章[关于哈希表,你该了解这些!](https://mp.weixin.qq.com/s/g8N6WmoQmsCUw3_BaWxHZA)。 - -这样存放映射关系可以定义为 `unordered_map> targets` 或者 `unordered_map> targets`。 - -含义如下: - -`unordered_map> targets`:`unordered_map<出发机场, 到达机场的集合> targets` -`unordered_map> targets`:`unordered_map<出发机场, map<到达机场, 航班次数>> targets` - -这两个结构,我选择了后者,因为如果使用`unordered_map> targets` 遍历multiset的时候,不能删除元素,一旦删除元素,迭代器就失效了。 - -**再说一下为什么一定要增删元素呢,正如开篇我给出的图中所示,出发机场和到达机场是会重复的,搜索的过程没及时删除目的机场就会死循环。** - -所以搜索的过程中就是要不断的删multiset里的元素,那么推荐使用`unordered_map> targets`。 - -在遍历 `unordered_map<出发机场, map<到达机场, 航班次数>> targets`的过程中,**可以使用"航班次数"这个字段的数字做相应的增减,来标记到达机场是否使用过了。** - - -如果“航班次数”大于零,说明目的地还可以飞,如果如果“航班次数”等于零说明目的地不能飞了,而不用对集合做删除元素或者增加元素的操作。 - -**相当于说我不删,我就做一个标记!** - -## 回溯法 - -这道题目我使用回溯法,那么下面按照我总结的回溯模板来: - -``` -void backtracking(参数) { - if (终止条件) { - 存放结果; - return; - } - - for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { - 处理节点; - backtracking(路径,选择列表); // 递归 - 回溯,撤销处理结果 - } -} -``` - -本题以输入:[["JFK", "KUL"], ["JFK", "NRT"], ["NRT", "JFK"]为例,抽象为树形结构如下: - -![332.重新安排行程1](https://img-blog.csdnimg.cn/2020111518065555.png) - -开始回溯三部曲讲解: - -* 递归函数参数 - -在讲解映射关系的时候,已经讲过了,使用`unordered_map> targets;` 来记录航班的映射关系,我定义为全局变量。 - -当然把参数放进函数里传进去也是可以的,我是尽量控制函数里参数的长度。 - -参数里还需要ticketNum,表示有多少个航班(终止条件会用上)。 - -代码如下: - -``` -// unordered_map<出发机场, map<到达机场, 航班次数>> targets -unordered_map> targets; -bool backtracking(int ticketNum, vector& result) { -``` - -**注意函数返回值我用的是bool!** - -我们之前讲解回溯算法的时候,一般函数返回值都是void,这次为什么是bool呢? - -因为我们只需要找到一个行程,就是在树形结构中唯一的一条通向叶子节点的路线,如图: - -![332.重新安排行程1](https://img-blog.csdnimg.cn/2020111518065555.png) - -所以找到了这个叶子节点了直接返回,这个递归函数的返回值问题我们在讲解二叉树的系列的时候,在这篇[二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?](https://mp.weixin.qq.com/s/6TWAVjxQ34kVqROWgcRFOg)详细介绍过。 - -当然本题的targets和result都需要初始化,代码如下: -``` -for (const vector& vec : tickets) { - targets[vec[0]][vec[1]]++; // 记录映射关系 -} -result.push_back("JFK"); // 起始机场 -``` - -* 递归终止条件 - -拿题目中的示例为例,输入: [["MUC", "LHR"], ["JFK", "MUC"], ["SFO", "SJC"], ["LHR", "SFO"]] ,这是有4个航班,那么只要找出一种行程,行程里的机场个数是5就可以了。 - -所以终止条件是:我们回溯遍历的过程中,遇到的机场个数,如果达到了(航班数量+1),那么我们就找到了一个行程,把所有航班串在一起了。 - -代码如下: - -``` -if (result.size() == ticketNum + 1) { - return true; -} -``` - -已经看习惯回溯法代码的同学,到叶子节点了习惯性的想要收集结果,但发现并不需要,本题的result相当于 [回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w)中的path,也就是本题的result就是记录路径的(就一条),在如下单层搜索的逻辑中result就添加元素了。 - -* 单层搜索的逻辑 - -回溯的过程中,如何遍历一个机场所对应的所有机场呢? - -这里刚刚说过,在选择映射函数的时候,不能选择`unordered_map> targets`, 因为一旦有元素增删multiset的迭代器就会失效,当然可能有牛逼的容器删除元素迭代器不会失效,这里就不在讨论了。 - -**可以说本题既要找到一个对数据进行排序的容器,而且还要容易增删元素,迭代器还不能失效**。 - -所以我选择了`unordered_map> targets` 来做机场之间的映射。 - -遍历过程如下: - -``` - for (pair& target : targets[result[result.size() - 1]]) { - if (target.second > 0 ) { // 记录到达机场是否飞过了 - result.push_back(target.first); - target.second--; - if (backtracking(ticketNum, result)) return true; - result.pop_back(); - target.second++; - } - } -``` - -可以看出 通过`unordered_map> targets`里的int字段来判断 这个集合里的机场是否使用过,这样避免了直接去删元素。 - -分析完毕,此时完整C++代码如下: - -# C++代码 - -``` -class Solution { -private: -// unordered_map<出发机场, map<到达机场, 航班次数>> targets -unordered_map> targets; -bool backtracking(int ticketNum, vector& result) { - if (result.size() == ticketNum + 1) { - return true; - } - for (pair& target : targets[result[result.size() - 1]]) { - if (target.second > 0 ) { // 记录到达机场是否飞过了 - result.push_back(target.first); - target.second--; - if (backtracking(ticketNum, result)) return true; - result.pop_back(); - target.second++; - } - } - return false; -} -public: - vector findItinerary(vector>& tickets) { - targets.clear(); - vector result; - for (const vector& vec : tickets) { - targets[vec[0]][vec[1]]++; // 记录映射关系 - } - result.push_back("JFK"); // 起始机场 - backtracking(tickets.size(), result); - return result; - } -}; -``` - -一波分析之后,可以看出我就是按照回溯算法的模板来的。 - -代码中 -``` -for (pair& target : targets[result[result.size() - 1]]) -``` -pair里要有const,因为map中的key是不可修改的,所以是`pair`。 - -如果不加const,也可以复制一份pair,例如这么写: -``` -for (pairtarget : targets[result[result.size() - 1]]) -``` - - -# 总结 - -本题其实可以算是一道hard的题目了,关于本题的难点我在文中已经列出了。 - -**如果单纯的回溯搜索(深搜)并不难,难还难在容器的选择和使用上**。 - -本题其实是一道深度优先搜索的题目,但是我完全使用回溯法的思路来讲解这道题题目,**算是给大家拓展一下思维方式,其实深搜和回溯也是分不开的,毕竟最终都是用递归**。 - -如果最终代码,发现照着回溯法模板画的话好像也能画出来,但难就难如何知道可以使用回溯,以及如果套进去,所以我再写了这么长的一篇来详细讲解。 - -就酱,很多录友表示和「代码随想录」相见恨晚,那么帮Carl宣传一波吧,让更多同学知道这里! - - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/0343.整数拆分.md b/problems/0343.整数拆分.md deleted file mode 100644 index 3ba67945..00000000 --- a/problems/0343.整数拆分.md +++ /dev/null @@ -1,45 +0,0 @@ - -// 拆成两个 还是拆成三个呢 - -# 思路 - -## 动态规划 - -* 明确dp[i]的含义 - -dp[i]表示 分拆数字i,可以得到的最大乘积。 - -* dp的初始化 - -初始化dp[i] = i,这里的初始化 不是为了让 i的最大乘积是dp[i],而是为了做递推公式的时候,dp[i]可以表示i这个数字,好用来做乘法。 - -* 递归公式 - -可以想 dp[i]的最大乘积是怎么得到的呢? - -**一定是dp[j]的最大乘积 * dp[i - j]的最大乘积,那么只需要遍历一下j(取值范围[2,i-1)),取此时dp[i]的最大值就可以了** - -递推公式:dp[i] = max(dp[i], dp[i - j] * dp[j]); - -``` -class Solution { -public: - int integerBreak(int n) { - if (n <= 3) return 1 * (n - 1); // 处理 2和3的情况 - int dp[60]; - for (int i = 0; i <= n; i++) dp[i] = i; // 初始化 - for (int i = 4; i <= n ; i++) { - for (int j = 2; j < i - 1; j++) { - dp[i] = max(dp[i], dp[i - j] * dp[j]); - } - } - return dp[n]; - } -}; -``` - -# 贪心 - -本题也可以用贪心,但是真的需要数学证明证明其合理性,网上有很多贪心的代码,每次拆成3就可以了,代码很简单,大家如果感兴趣可以自己去查一查。 - -我这里就不做证明了。 diff --git a/problems/0344.反转字符串.md b/problems/0344.反转字符串.md deleted file mode 100644 index 89605974..00000000 --- a/problems/0344.反转字符串.md +++ /dev/null @@ -1,126 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/reverse-string/ - -> 打基础的时候,不要太迷恋于库函数。 - -# 题目:344. 反转字符串 - -编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。 - -不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。 - -你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。 - -示例 1: - -输入:["h","e","l","l","o"] -输出:["o","l","l","e","h"] -示例 2: - -输入:["H","a","n","n","a","h"] -输出:["h","a","n","n","a","H"] - - -# 思路 - -先说一说题外话: - -对于这道题目一些同学直接用C++里的一个库函数 reverse,调一下直接完事了, 相信每一门编程语言都有这样的库函数。 - -如果这么做题的话,这样大家不会清楚反转字符串的实现原理了。 - -但是也不是说库函数就不能用,是要分场景的。 - -如果在现场面试中,我们什么时候使用库函数,什么时候不要用库函数呢? - -**如果题目关键的部分直接用库函数就可以解决,建议不要使用库函数。** - -毕竟面试官一定不是考察你对库函数的熟悉程度, 如果使用python和java 的同学更需要注意这一点,因为python、java提供的库函数十分丰富。 - -**如果库函数仅仅是 解题过程中的一小部分,并且你已经很清楚这个库函数的内部实现原理的话,可以考虑使用库函数。** - -建议大家平时在leetcode上练习算法的时候本着这样的原则去练习,这样才有助于我们对算法的理解。 - -不要沉迷于使用库函数一行代码解决题目之类的技巧,不是说这些技巧不好,而是说这些技巧可以用来娱乐一下。 - -真正自己写的时候,要保证理解可以实现是相应的功能。 - -接下来再来讲一下如何解决反转字符串的问题。 - -大家应该还记得,我们已经讲过了[206.反转链表](https://mp.weixin.qq.com/s/pnvVP-0ZM7epB8y3w_Njwg)。 - -在反转链表中,使用了双指针的方法。 - -那么反转字符串依然是使用双指针的方法,只不过对于字符串的反转,其实要比链表简单一些。 - -因为字符串也是一种数组,所以元素在内存中是连续分布,这就决定了反转链表和反转字符串方式上还是有所差异的。 - -如果对数组和链表原理不清楚的同学,可以看这两篇,[关于链表,你该了解这些!](https://mp.weixin.qq.com/s/ntlZbEdKgnFQKZkSUAOSpQ),[必须掌握的数组理论知识](https://mp.weixin.qq.com/s/X7R55wSENyY62le0Fiawsg)。 - -对于字符串,我们定义两个指针(也可以说是索引下表),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。 - -以字符串`hello`为例,过程如下: - - - -不难写出如下C++代码: - -``` -void reverseString(vector& s) { - for (int i = 0, j = s.size() - 1; i < s.size()/2; i++, j--) { - swap(s[i],s[j]); - } -} -``` - -循环里只要做交换s[i] 和s[j]操作就可以了,那么我这里使用了swap 这个库函数。大家可以使用。 - -因为相信大家都知道交换函数如何实现,而且这个库函数仅仅是解题中的一部分, 所以这里使用库函数也是可以的。 - -swap可以有两种实现。 - -一种就是常见的交换数值: - -``` -int tmp = s[i]; -s[i] = s[j]; -s[j] = tmp; - -``` - -一种就是通过位运算: - -``` -s[i] ^= s[j]; -s[j] ^= s[i]; -s[i] ^= s[j]; - -``` - -这道题目还是比较简单的,但是我正好可以通过这道题目说一说在刷题的时候,使用库函数的原则。 - -如果题目关键的部分直接用库函数就可以解决,建议不要使用库函数。 - -如果库函数仅仅是 解题过程中的一小部分,并且你已经很清楚这个库函数的内部实现原理的话,可以考虑使用库函数。 - -本着这样的原则,我没有使用reverse库函数,而使用swap库函数。 - -**在字符串相关的题目中,库函数对大家的诱惑力是非常大的,因为会有各种反转,切割取词之类的操作**,这也是为什么字符串的库函数这么丰富的原因。 - -相信大家本着我所讲述的原则来做字符串相关的题目,在选择库函数的角度上会有所原则,也会有所收获。 - - -## C++代码 - -``` -class Solution { -public: - void reverseString(vector& s) { - for (int i = 0, j = s.size() - 1; i < s.size()/2; i++, j--) { - swap(s[i],s[j]); - } - } -}; -``` -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0347.前K个高频元素.md b/problems/0347.前K个高频元素.md deleted file mode 100644 index 2a532aa4..00000000 --- a/problems/0347.前K个高频元素.md +++ /dev/null @@ -1,122 +0,0 @@ -## 题目地址 - -https://leetcode-cn.com/problems/top-k-frequent-elements/ - -> 前K个大数问题,老生常谈,不得不谈 - -# 347.前 K 个高频元素 - -给定一个非空的整数数组,返回其中出现频率前 k 高的元素。 - -示例 1: -输入: nums = [1,1,1,2,2,3], k = 2 -输出: [1,2] - -示例 2: -输入: nums = [1], k = 1 -输出: [1] - -提示: -你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。 -你的算法的时间复杂度必须优于 O(n log n) , n 是数组的大小。 -题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的。 -你可以按任意顺序返回答案。 - -# 思路 - -这道题目主要涉及到如下三块内容: -1. 要统计元素出现频率 -2. 对频率排序 -3. 找出前K个高频元素 - -首先统计元素出现的频率,这一类的问题可以使用map来进行统计。 - -然后是对频率进行排序,这里我们可以使用一种 容器适配器就是**优先级队列**。 - -什么是优先级队列呢? - -其实**就是一个披着队列外衣的堆**,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。 - -而且优先级队列内部元素是自动依照元素的权值排列。那么它是如何有序排列的呢? - -缺省情况下priority_queue利用max-heap(大顶堆)完成对元素的排序,这个大顶堆是以vector为表现形式的complete binary tree(完全二叉树)。 - -什么是堆呢? - -**堆是一颗完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。** 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。 - -所以大家经常说的大顶堆(堆头是最大元素),小顶堆(堆头是最小元素),如果懒得自己实现的话,就直接用priority_queue(优先级队列)就可以了,底层实现都是一样的,从小到大排就是小顶堆,从大到小排就是大顶堆。 - -本题我们就要使用优先级队列来对部分频率进行排序。 - -为什么不用快排呢, 使用快排要将map转换为vector的结构,然后对整个数组进行排序, 而这种场景下,我们其实只需要维护k个有序的序列就可以了,所以使用优先级队列是最优的。 - -此时要思考一下,是使用小顶堆呢,还是大顶堆? - -有的同学一想,题目要求前 K 个高频元素,那么果断用大顶堆啊。 - -那么问题来了,定义一个大小为k的大顶堆,在每次移动更新大顶堆的时候,每次弹出都把最大的元素弹出去了,那么怎么保留下来前K个高频元素呢。 - -**所以我们要用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。** - -寻找前k个最大元素流程如图所示:(图中的频率只有三个,所以正好构成一个大小为3的小顶堆,如果频率更多一些,则用这个小顶堆进行扫描) - - - - -我们来看一下代码: - -# C++代码 - -``` -// 时间复杂度:O(nlogk) -// 空间复杂度:O(n) -class Solution { -public: - // 小顶堆 - class mycomparison { - public: - bool operator()(const pair& lhs, const pair& rhs) { - return lhs.second > rhs.second; - } - }; - vector topKFrequent(vector& nums, int k) { - // 要统计元素出现频率 - unordered_map map; // map - for (int i = 0; i < nums.size(); i++) { - map[nums[i]]++; - } - - // 对频率排序 - // 定义一个小顶堆,大小为k - priority_queue, vector>, mycomparison> pri_que; - - // 用固定大小为k的小顶堆,扫面所有频率的数值 - for (unordered_map::iterator it = map.begin(); it != map.end(); it++) { - pri_que.push(*it); - if (pri_que.size() > k) { // 如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k - pri_que.pop(); - } - } - - // 找出前K个高频元素,因为小顶堆先弹出的是最小的,所以倒叙来输出到数组 - vector result(k); - for (int i = k - 1; i >= 0; i--) { - result[i] = pri_que.top().first; - pri_que.pop(); - } - return result; - - } -}; -``` -# 拓展 -大家对这个比较运算在建堆时是如何应用的,为什么左大于右就会建立小顶堆,反而建立大顶堆比较困惑。 - -确实 例如我们在写快排的cmp函数的时候,`return left>right` 就是从大到小,`return left 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0349.两个数组的交集.md b/problems/0349.两个数组的交集.md deleted file mode 100644 index 2b4fef56..00000000 --- a/problems/0349.两个数组的交集.md +++ /dev/null @@ -1,63 +0,0 @@ - -## 题目地址 -https://leetcode-cn.com/problems/intersection-of-two-arrays/ - -> 如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费! - -# 第349题. 两个数组的交集 - -题意:给定两个数组,编写一个函数来计算它们的交集。 - -![349. 两个数组的交集](https://img-blog.csdnimg.cn/20200818193523911.png) - -**说明:** -输出结果中的每个元素一定是唯一的。 -我们可以不考虑输出结果的顺序。 - -# 思路 - -这道题目,主要要学会使用一种哈希数据结构:unordered_set,这个数据结构可以解决很多类似的问题。 - -注意题目特意说明:**输出结果中的每个元素一定是唯一的,也就是说输出的结果的去重的, 同时可以不考虑输出结果的顺序** - -这道题用暴力的解法时间复杂度是O(n^2),那来看看使用哈希法进一步优化。 - -那么用数组来做哈希表也是不错的选择,例如[242. 有效的字母异位词](https://mp.weixin.qq.com/s/vM6OszkM6L1Mx2Ralm9Dig),[0383.赎金信](https://mp.weixin.qq.com/s/sYZIR4dFBrw_lr3eJJnteQ) - -但是要注意,**使用数据来做哈希的题目,都限制了数值的大小。** - -而这道题目没有限制数值的大小,就无法使用数组来做哈希表了。 - -**而且如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。** - -此时就要使用另一种结构体了,set ,关于set,C++ 给提供了如下三种可用的数据结构: - -* std::set -* std::multiset -* std::unordered_set - -std::set和std::multiset底层实现都是红黑树,std::unordered_set的底层实现是哈希表, 使用unordered_set 读写效率是最高的,并不需要对数据进行排序,而且还不要让数据重复,所以选择unordered_set。 - -思路如图所示: - -![set哈希法](https://img-blog.csdnimg.cn/2020080918570417.png) - -# C++代码 -``` -class Solution { -public: - vector intersection(vector& nums1, vector& nums2) { - unordered_set result_set; // 存放结果 - unordered_set nums_set(nums1.begin(), nums1.end()); - for (int num : nums2) { - // 发现nums2的元素 在nums_set里又出现过 - if (nums_set.find(num) != nums_set.end()) { - result_set.insert(num); - } - } - return vector(result_set.begin(), result_set.end()); - } -}; -``` -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0350.两个数组的交集II.md b/problems/0350.两个数组的交集II.md deleted file mode 100644 index 2320b801..00000000 --- a/problems/0350.两个数组的交集II.md +++ /dev/null @@ -1,36 +0,0 @@ - -## 题目地址 - -https://leetcode-cn.com/problems/intersection-of-two-arrays-ii/ - -## 思路 - -这道题目,看上去和349两个数组的交集题目描述是一样的,其实这两道题解法相差还是很大的,编号349题目结果是去重的 - - -而本题才求的真正的交集,求这两个集合元素的交集,需要掌握另一个哈希数据结构unordered_map - - -## 代码 - -``` -class Solution { -public: - vector intersect(vector& nums1, vector& nums2) { - vector result; - unordered_map map; - for (int num : nums1) { - map[num]++; - } - for (int num : nums2) { - if (map[num] > 0) { - result.push_back(num); - map[num]--; - } - } - return result; - } -}; -``` -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0376.摆动序列.md b/problems/0376.摆动序列.md deleted file mode 100644 index c6682a21..00000000 --- a/problems/0376.摆动序列.md +++ /dev/null @@ -1,106 +0,0 @@ - -> 本周讲解了[贪心理论基础](https://mp.weixin.qq.com/s/O935TaoHE9Eexwe_vSbRAg),以及第一道贪心的题目:[贪心算法:分发饼干](https://mp.weixin.qq.com/s/YSuLIAYyRGlyxbp9BNC1uw),可能会给大家一种贪心算法比较简单的错觉,好了,接下来几天的题目难度要上来了,哈哈。 - -# 376. 摆动序列 - -题目链接:https://leetcode-cn.com/problems/wiggle-subsequence/ - -如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。 - -例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。 - -给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。 - -示例 1: -输入: [1,7,4,9,2,5] -输出: 6 -解释: 整个序列均为摆动序列。 - -示例 2: -输入: [1,17,5,10,13,15,10,5,16,8] -输出: 7 -解释: 这个序列包含几个长度为 7 摆动序列,其中一个可为[1,17,10,13,10,16,8]。 - -示例 3: -输入: [1,2,3,4,5,6,7,8,9] -输出: 2 - - -## 思路 - -本题要求通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。 - -相信这么一说吓退不少同学,这要求最大摆动序列又可以修改数组,这得如何修改呢? - -来分析一下,要求删除元素使其达到最大摆动序列,应该删除什么元素呢? - -用示例二来举例,如图所示: - -![376.摆动序列](https://img-blog.csdnimg.cn/20201124174327597.png) - -**局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值**。 - -**整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列**。 - -局部最优推出全局最优,并举不出反例,那么试试贪心! - -(为方便表述,以下说的峰值都是指局部峰值) - -**实际操作上,其实连删除的操作都不用做,因为题目要求的是最长摆动子序列的长度,所以只需要统计数组的峰值数量就可以了(相当于是删除单一坡度上的节点,然后统计长度)** - -**这就是贪心所贪的地方,让峰值尽可能的保持峰值,然后删除单一坡度上的节点**。 - -本题代码实现中,还有一些技巧,例如统计峰值的时候,数组最左面和最右面是最不好统计的。 - -例如序列[2,5],它的峰值数量是2,如果靠统计差值来计算峰值个数就需要考虑数组最左面和最右面的特殊情况。 - -所以可以针对序列[2,5],可以假设为[2,2,5],这样它就有坡度了即preDiff = 0,如图: - -![376.摆动序列1](https://img-blog.csdnimg.cn/20201124174357612.png) - -针对以上情形,result初始为1(默认最右面有一个峰值),此时curDiff > 0 && preDiff <= 0,那么result++(计算了左面的峰值),最后得到的result就是2(峰值个数为2即摆动序列长度为2) - -C++代码如下(和上图是对应的逻辑): - -```C++ -class Solution { -public: - int wiggleMaxLength(vector& nums) { - if (nums.size() <= 1) return nums.size(); - int curDiff = 0; // 当前一对差值 - int preDiff = 0; // 前一对差值 - int result = 1; // 记录峰值个数,序列默认序列最右边有一个峰值 - for (int i = 0; i < nums.size() - 1; i++) { - curDiff = nums[i + 1] - nums[i]; - // 出现峰值 - if ((curDiff > 0 && preDiff <= 0) || (preDiff >= 0 && curDiff < 0)) { - result++; - preDiff = curDiff; - } - } - return result; - } -}; -``` -时间复杂度O(n) -空间复杂度O(1) - -# 总结 - -**贪心的题目说简单有的时候就是常识,说难就难在都不知道该怎么用贪心**。 - -本题大家如果要去模拟删除元素达到最长摆动子序列的过程,那指定绕里面去了,一时半会拔不出来。 - -而这道题目有什么技巧说一下子能想到贪心么? - -其实也没有,类似的题目做过了就会想到。 - -此时大家就应该了解了:保持区间波动,只需要把单调区间上的元素移除就可以了。 - -就酱,「代码随想录」值得介绍给身边每一位学习算法的同学! - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20201124161234338.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** - - diff --git a/problems/0377.组合总和Ⅳ.md b/problems/0377.组合总和Ⅳ.md deleted file mode 100644 index 812ad08f..00000000 --- a/problems/0377.组合总和Ⅳ.md +++ /dev/null @@ -1,103 +0,0 @@ -# 思路 - -本题题目描述说是求组合,但又说是可以元素相同顺序不同的组合算两个组合,**其实就是求排列!** - -弄清什么是组合,什么是排列很重要。 - -组合不强调顺序,(1,5)和(5,1)是同一个组合。 - -排列强调顺序,(1,5)和(5,1)是两个不同的排列。 - -大家在学习回溯算法系列的时候,一定做过这两道题目[回溯算法:39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)和[回溯算法:40.组合总和II](https://mp.weixin.qq.com/s/_1zPYk70NvHsdY8UWVGXmQ) - -大家会感觉很像,但其本质是本题求的是排列总和,而且仅仅是求排列总和的个数,并不是把所有的排列都列出来。 - -如果本题要把排列都列出来的话,只能使用回溯算法爆搜。 - - -* 确定dp数组以及下标的含义 - -dp[i]: 凑成目标正整数为i的组合个数为dp[i] - -* 确定递推公式 - -dp[i](考虑nums[j])可以由 dp[i - nums[j]](不考虑nums[j]) 推导出来。 - -因为只要得到nums[j],排列个数dp[i - nums[j]],就是dp[i]的一部分。 - -那题目中的示例来例子,target=4,那么dp[4] 是求和为4的排列个数,这个排列个数一定等于以元素3为结尾的排列的个数,以元素2为结尾的排列的个数,以元素1为结尾的排列的个数 之和! - -以元素3为结尾的排列的个数就是dp[1](dp[4 - 3]),以元素2为结尾的排列的个数就是dp[2](dp[4 - 2]),以元素1为结尾的排列的个数就是dp[3](dp[4 - 1])。 - -dp[4] 就等于 dp[1],dp[2],dp[3]之和 - -所以dp[i] 就是 dp[i - nums[j]]之和,而nums[j]就是 1, 2, 3。 - -此时不难理解,递归公式为:dp[i] += dp[i - nums[j]]; - -* dp数组如何初始化 - -因为递推公式dp[i] += dp[i - nums[j]]的缘故,dp[0]要初始化为1,这样递归其他dp[i]的时候才会有数值基础。 - -非0下标的dp[i]初始化为0,这样才不会影响dp[i]累加所有的dp[i-nums[j]] - -* 确定遍历顺序 - -个数可以不限使用,这是一个完全背包,且得到的集合是排列(需要考虑元素之间的顺序)。 - -所以将target放在外循环,将nums放在内循环,内循环从前到后遍历。 - -本题要求的是排列,那么这个for循环嵌套的顺序可以有说法了。 - -需要把target放在外循环,将nums放在内循环,为什么呢? - -还是拿本题示例来举例,只有吧遍历target放在外面,dp[4] 才能得到 以元素3为结尾的排列的个数dp[1],以元素2为结尾的排列的个数就是dp[2],以元素1为结尾的排列的个数就是dp[3] 之和。 - -那么以元素3为结尾的排列的个数dp[1] 其实就已经包含了元素1了,而以元素1为结尾的排列的个数就是dp[3]也已经包含了元素3。 - -所以这两个就算成了两个集合了,即:排列。 - -如果把遍历nums放在外循环,遍历target的作为内循环的话呢 - -举一个例子:计算dp[4]的时候,结果集只有 (1,3) 这样的集合,不会有(3,1)这样的集合,因为nums遍历放在外层,3只能出现在1后面! - -所以本题遍历顺序最终遍历顺序:target放在外循环,将nums放在内循环,内循环从前到后遍历。 - -* 举例来推导dp数组 - -我们再来用示例中的例子推导一下: - -dp[0] = 1 -dp[1] = dp[0] = 1 -dp[2] = dp[1] + dp[0] = 2 -dp[3] = dp[2] + dp[1] + dp[0] = 4 -dp[4] = dp[3] + dp[2] + dp[1] = 7 - -如果代码运行处的结果不是想要的结果,就把dp[i]都打出来,看看和我们推导的一不一样。 - -经过以上的分析,C++代码如下: - -``` -class Solution { -public: - int combinationSum4(vector& nums, int target) { - vector dp(target + 1, 0); - dp[0] = 1; - for (int i = 0; i <= target; i++) { - for (int j = 0; j < nums.size(); j++) { - if (i - nums[j] >= 0) { - dp[i] += dp[i - nums[j]]; - } - } - } - return dp[target]; - } -}; -``` - -C++测试用例有超过两个树相加超过int的数据,所以需要在if里加上dp[i] < INT_MAX - dp[i - num]。 - -但java就不用考虑这个限制,我理解java里的int也是四个字节吧,也有可能leetcode后台对不同语言的测试数据不一样。 - - - diff --git a/problems/0383.赎金信.md b/problems/0383.赎金信.md deleted file mode 100644 index b0440583..00000000 --- a/problems/0383.赎金信.md +++ /dev/null @@ -1,98 +0,0 @@ -# 题目地址 -https://leetcode-cn.com/problems/ransom-note/ - -> 在哈希法中有一些场景就是为数组量身定做的。 - -# 第383题. 赎金信 - -给定一个赎金信 (ransom) 字符串和一个杂志(magazine)字符串,判断第一个字符串 ransom 能不能由第二个字符串 magazines 里面的字符构成。如果可以构成,返回 true ;否则返回 false。 - -(题目说明:为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思。杂志字符串中的每个字符只能在赎金信字符串中使用一次。) - -**注意:** - -你可以假设两个字符串均只含有小写字母。 - -canConstruct("a", "b") -> false -canConstruct("aa", "ab") -> false -canConstruct("aa", "aab") -> true - -# 思路 - -这道题目和[242.有效的字母异位词](https://mp.weixin.qq.com/s/vM6OszkM6L1Mx2Ralm9Dig)很像,[242.有效的字母异位词](https://mp.weixin.qq.com/s/vM6OszkM6L1Mx2Ralm9Dig)相当于求 字符串a 和 字符串b 是否可以相互组成 ,而这道题目是求 字符串a能否组成字符串b,而不用管字符串b 能不能组成字符串a。 - -本题判断第一个字符串ransom能不能由第二个字符串magazines里面的字符构成,但是这里需要注意两点。 - -*  第一点“为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思”  这里*说明杂志里面的字母不可重复使用。* - -* 第二点 “你可以假设两个字符串均只含有小写字母。” *说明只有小写字母*,这一点很重要 - -# 暴力解法 - -那么第一个思路其实就是暴力枚举了,两层for循环,不断去寻找,代码如下: - -```C++ -// 时间复杂度: O(n^2) -// 空间复杂度:O(1) -class Solution { -public: - bool canConstruct(string ransomNote, string magazine) { - for (int i = 0; i < magazine.length(); i++) { - for (int j = 0; j < ransomNote.length(); j++) { - // 在ransomNote中找到和magazine相同的字符 - if (magazine[i] == ransomNote[j]) { - ransomNote.erase(ransomNote.begin() + j); // ransomNote删除这个字符 - break; - } - } - } - // 如果ransomNote为空,则说明magazine的字符可以组成ransomNote - if (ransomNote.length() == 0) { - return true; - } - return false; - } -}; -``` - -这里时间复杂度是比较高的,而且里面还有一个字符串删除也就是erase的操作,也是费时的,当然这段代码也可以过这道题。 - - -# 哈希解法 - -因为题目所只有小写字母,那可以采用空间换取时间的哈希策略, 用一个长度为26的数组还记录magazine里字母出现的次数。 - -然后再用ransomNote去验证这个数组是否包含了ransomNote所需要的所有字母。 - -依然是数组在哈希法中的应用。 - -一些同学可能想,用数组干啥,都用map完事了,**其实在本题的情况下,使用map的空间消耗要比数组大一些的,因为map要维护红黑树或者哈希表,而且还要做哈希函数。 所以数组更加简单直接有效!** - -代码如下: - -```C++ -// 时间复杂度: O(n) -// 空间复杂度:O(1) -class Solution { -public: - bool canConstruct(string ransomNote, string magazine) { - int record[26] = {0}; - for (int i = 0; i < magazine.length(); i++) { - // 通过recode数据记录 magazine里各个字符出现次数 - record[magazine[i]-'a'] ++; - } - for (int j = 0; j < ransomNote.length(); j++) { - // 遍历ransomNote,在record里对应的字符个数做--操作 - record[ransomNote[j]-'a']--; - // 如果小于零说明 magazine里出现的字符,ransomNote没有 - if(record[ransomNote[j]-'a'] < 0) { - return false; - } - } - return true; - } -}; -``` - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0402.移掉K位数字.md b/problems/0402.移掉K位数字.md deleted file mode 100644 index c7112bff..00000000 --- a/problems/0402.移掉K位数字.md +++ /dev/null @@ -1,11 +0,0 @@ - -## 思路 - -https://www.cnblogs.com/gzshan/p/12560566.html 图不错 - -有点难度 - - -暴力的解法,其实也不是那么好写的, 首字符去0,k没消耗完,等等这些情况 - -有点难,卡我很久 diff --git a/problems/0404.左叶子之和.md b/problems/0404.左叶子之和.md deleted file mode 100644 index 1050a90c..00000000 --- a/problems/0404.左叶子之和.md +++ /dev/null @@ -1,151 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/sum-of-left-leaves/ - -> 有的题目就是 - -# 404.左叶子之和 - -计算给定二叉树的所有左叶子之和。 - -示例: - - - -# 思路 - -**首先要注意是判断左叶子,不是二叉树左侧节点,所以不要上来想着层序遍历。** - -因为题目中其实没有说清楚左叶子究竟是什么节点,那么我来给出左叶子的明确定义:**如果左节点不为空,且左节点没有左右孩子,那么这个节点就是左叶子** - -大家思考一下如下图中二叉树,左叶子之和究竟是多少? - - - -**其实是0,因为这棵树根本没有左叶子!** - -那么**判断当前节点是不是左叶子是无法判断的,必须要通过节点的父节点来判断其左孩子是不是左叶子。** - - -如果该节点的左节点不为空,该节点的左节点的左节点为空,该节点的左节点的右节点为空,则找到了一个左叶子,判断代码如下: - -``` -if (node->left != NULL && node->left->left == NULL && node->left->right == NULL) { - 左叶子节点处理逻辑 -} -``` - -## 递归法 - -递归的遍历顺序为后序遍历(左右中),是因为要通过递归函数的返回值来累加求取左叶子数值之和。。 - -递归三部曲: - -1. 确定递归函数的参数和返回值 - -判断一个树的左叶子节点之和,那么一定要传入树的根节点,递归函数的返回值为数值之和,所以为int - -使用题目中给出的函数就可以了。 - -2. 确定终止条件 - -依然是 -``` -if (root == NULL) return 0; -``` - -3. 确定单层递归的逻辑 - -当遇到左叶子节点的时候,记录数值,然后通过递归求取左子树左叶子之和,和 右子树左叶子之和,相加便是整个树的左叶子之和。 - -代码如下: - -``` -int leftValue = sumOfLeftLeaves(root->left); // 左 -int rightValue = sumOfLeftLeaves(root->right); // 右 - // 中 -int midValue = 0; -if (root->left && !root->left->left && !root->left->right) { - midValue = root->left->val; -} -int sum = midValue + leftValue + rightValue; -return sum; - -``` - - -整体递归代码如下: - -``` -class Solution { -public: - int sumOfLeftLeaves(TreeNode* root) { - if (root == NULL) return 0; - - int leftValue = sumOfLeftLeaves(root->left); // 左 - int rightValue = sumOfLeftLeaves(root->right); // 右 - // 中 - int midValue = 0; - if (root->left && !root->left->left && !root->left->right) { // 中 - midValue = root->left->val; - } - int sum = midValue + leftValue + rightValue; - return sum; - } -}; -``` - -以上代码精简之后如下: - -``` -class Solution { -public: - int sumOfLeftLeaves(TreeNode* root) { - if (root == NULL) return 0; - int midValue = 0; - if (root->left != NULL && root->left->left == NULL && root->left->right == NULL) { - midValue = root->left->val; - } - return midValue + sumOfLeftLeaves(root->left) + sumOfLeftLeaves(root->right); - } -}; -``` - -## 迭代法 - - -本题迭代法使用前中后序都是可以的,只要把左叶子节点统计出来,就可以了,那么参考文章 [二叉树:听说递归能做的,栈也能做!](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg)和[二叉树:前中后序迭代方式的写法就不能统一一下么?](https://mp.weixin.qq.com/s/WKg0Ty1_3SZkztpHubZPRg)中的写法,可以写出一个前序遍历的迭代法。 - -判断条件都是一样的,代码如下: - -``` - -class Solution { -public: - int sumOfLeftLeaves(TreeNode* root) { - stack st; - if (root == NULL) return 0; - st.push(root); - int result = 0; - while (!st.empty()) { - TreeNode* node = st.top(); - st.pop(); - if (node->left != NULL && node->left->left == NULL && node->left->right == NULL) { - result += node->left->val; - } - if (node->right) st.push(node->right); - if (node->left) st.push(node->left); - } - return result; - } -}; -``` - -# 总结 - -这道题目要求左叶子之和,其实是比较绕的,因为不能判断本节点是不是左叶子节点。 - -此时就要通过节点的父节点来判断其左孩子是不是左叶子了。 - -**平时我们解二叉树的题目时,已经习惯了通过节点的左右孩子判断本节点的属性,而本题我们要通过节点的父节点判断本节点的属性。** - -希望通过这道题目,可以扩展大家对二叉树的解题思路。 diff --git a/problems/0406.根据身高重建队列.md b/problems/0406.根据身高重建队列.md deleted file mode 100644 index f4388a9e..00000000 --- a/problems/0406.根据身高重建队列.md +++ /dev/null @@ -1,195 +0,0 @@ -

- -

-

- - - - - - -

- -> 就不能好好站个队 - -# 406.根据身高重建队列 - -题目链接:https://leetcode-cn.com/problems/queue-reconstruction-by-height/ - -假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。 - -请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。 - -示例 1: -输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]] -输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] -解释: -编号为 0 的人身高为 5 ,没有身高更高或者相同的人排在他前面。 -编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。 -编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。 -编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。 -编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。 -编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。 -因此 [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。 - -示例 2: -输入:people = [[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]] -输出:[[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]] - -提示: - -* 1 <= people.length <= 2000 -* 0 <= hi <= 10^6 -* 0 <= ki < people.length - -题目数据确保队列可以被重建 - -# 思路 - -本题有两个维度,h和k,看到这种题目一定要想如何确定一个维度,然后在按照另一个维度重新排列。 - -其实如果大家认真做了[贪心算法:分发糖果](https://mp.weixin.qq.com/s/8MwlgFfvaNYmjGwjuMlETQ),就会发现和此题有点点的像。 - -在[贪心算法:分发糖果](https://mp.weixin.qq.com/s/8MwlgFfvaNYmjGwjuMlETQ)我就强调过一次,遇到两个维度权衡的时候,一定要先确定一个维度,再确定另一个维度。 - -**如果两个维度一起考虑一定会顾此失彼**。 - -对于本题相信大家困惑的点是先确定k还是先确定h呢,也就是究竟先按h排序呢,还先按照k排序呢? - -如果按照k来从小到大排序,排完之后,会发现k的排列并不符合条件,身高也不符合条件,两个维度哪一个都没确定下来。 - -那么按照身高h来排序呢,身高一定是从大到小排(身高相同的话则k小的站前面),让高个子在前面。 - -**此时我们可以确定一个维度了,就是身高,前面的节点一定都比本节点高!** - -那么只需要按照k为下标重新插入队列就可以了,为什么呢? - -以图中{5,2} 为例: - -![406.根据身高重建队列](https://img-blog.csdnimg.cn/20201216201851982.png) - - -按照身高排序之后,优先按身高高的people的k来插入,后序插入节点也不会影响前面已经插入的节点,最终按照k的规则完成了队列。 - -所以在按照身高从大到小排序后: - -**局部最优:优先按身高高的people的k来插入。插入操作过后的people满足队列属性** - -**全局最优:最后都做完插入操作,整个队列满足题目队列属性** - -局部最优可推出全局最优,找不出反例,那就试试贪心。 - -一些同学可能也会疑惑,你怎么知道局部最优就可以推出全局最优呢? 有数学证明么? - -在贪心系列开篇词[关于贪心算法,你该了解这些!](https://mp.weixin.qq.com/s/O935TaoHE9Eexwe_vSbRAg)中,我已经讲过了这个问题了。 - -刷题或者面试的时候,手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心,至于严格的数学证明,就不在讨论范围内了。 - -如果没有读过[关于贪心算法,你该了解这些!](https://mp.weixin.qq.com/s/O935TaoHE9Eexwe_vSbRAg)的同学建议读一下,相信对贪心就有初步的了解了。 - -回归本题,整个插入过程如下: - -排序完的people: -[[7,0], [7,1], [6,1], [5,0], [5,2],[4,4]] - -插入的过程: -插入[7,0]:[[7,0]] -插入[7,1]:[[7,0],[7,1]] -插入[6,1]:[[7,0],[6,1],[7,1]] -插入[5,0]:[[5,0],[7,0],[6,1],[7,1]] -插入[5,2]:[[5,0],[7,0],[5,2],[6,1],[7,1]] -插入[4,4]:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] - -此时就按照题目的要求完成了重新排列。 - -C++代码如下: - -```C++ -// 版本一 -class Solution { -public: - static bool cmp(const vector a, const vector b) { - if (a[0] == b[0]) return a[1] < b[1]; - return a[0] > b[0]; - } - vector> reconstructQueue(vector>& people) { - sort (people.begin(), people.end(), cmp); - vector> que; - for (int i = 0; i < people.size(); i++) { - int position = people[i][1]; - que.insert(que.begin() + position, people[i]); - } - return que; - } -}; -``` -* 时间复杂度O(nlogn + n^2) -* 空间复杂度O(n) - -但使用vector是非常费时的,C++中vector(可以理解是一个动态数组,底层是普通数组实现的)如果插入元素大于预先普通数组大小,vector底部会有一个扩容的操作,即申请两倍于原先普通数组的大小,然后把数据拷贝到另一个更大的数组上。 - -所以使用vector(动态数组)来insert,是费时的,插入再拷贝的话,单纯一个插入的操作就是O(n^2)了,甚至可能拷贝好几次,就不止O(n^2)了。 - -改成链表之后,C++代码如下: - -```C++ -// 版本二 -class Solution { -public: - // 身高从大到小排(身高相同k小的站前面) - static bool cmp(const vector a, const vector b) { - if (a[0] == b[0]) return a[1] < b[1]; - return a[0] > b[0]; - } - vector> reconstructQueue(vector>& people) { - sort (people.begin(), people.end(), cmp); - list> que; // list底层是链表实现,插入效率比vector高的多 - for (int i = 0; i < people.size(); i++) { - int position = people[i][1]; // 插入到下标为position的位置 - std::list>::iterator it = que.begin(); - while (position--) { // 寻找在插入位置 - it++; - } - que.insert(it, people[i]); - } - return vector>(que.begin(), que.end()); - } -}; -``` - -* 时间复杂度O(nlogn + n^2) -* 空间复杂度O(n) - -大家可以把两个版本的代码提交一下试试,就可以发现其差别了! - -关于本题使用数组还是使用链表的性能差异,我在[贪心算法:根据身高重建队列(续集)](https://mp.weixin.qq.com/s/K-pRN0lzR-iZhoi-1FgbSQ)中详细讲解了一波 - -# 总结 - -关于出现两个维度一起考虑的情况,我们已经做过两道题目了,另一道就是[贪心算法:分发糖果](https://mp.weixin.qq.com/s/8MwlgFfvaNYmjGwjuMlETQ)。 - -**其技巧都是确定一边然后贪心另一边,两边一起考虑,就会顾此失彼**。 - -这道题目可以说比[贪心算法:分发糖果](https://mp.weixin.qq.com/s/8MwlgFfvaNYmjGwjuMlETQ)难不少,其贪心的策略也是比较巧妙。 - -最后我给出了两个版本的代码,可以明显看是使用C++中的list(底层链表实现)比vector(数组)效率高得多。 - -**对使用某一种语言容器的使用,特性的选择都会不同程度上影响效率**。 - -所以很多人都说写算法题用什么语言都可以,主要体现在算法思维上,其实我是同意的但也不同意。 - -对于看别人题解的同学,题解用什么语言其实影响不大,只要题解把所使用语言特性优化的点讲出来,大家都可以看懂,并使用自己语言的时候注意一下。 - -对于写题解的同学,刷题用什么语言影响就非常大,如果自己语言没有学好而强调算法和编程语言没关系,其实是会误伤别人的。 - -**这也是我为什么统一使用C++写题解的原因**,其实用其他语言java、python、php、go啥的,我也能写,我的Github上也有用这些语言写的小项目,但写题解的话,我就不能保证把语言特性这块讲清楚,所以我始终坚持使用最熟悉的C++写题解。 - -**而且我在写题解的时候涉及语言特性,一般都会后面加上括号说明一下。没办法,认真负责就是我,哈哈**。 - -就酱,「代码随想录」一直都是干货满满,值得介绍给身边的朋友同学们! - -**循序渐进学算法,认准「代码随想录」,Carl手把手带你过关斩将!** - -

- -

diff --git a/problems/0416.分割等和子集.md b/problems/0416.分割等和子集.md deleted file mode 100644 index b5eb3f75..00000000 --- a/problems/0416.分割等和子集.md +++ /dev/null @@ -1,175 +0,0 @@ - - -* 473. 火柴拼正方形 (回溯算法) -* 698. 划分为k个相等的子集 - -一起再回忆一下回溯算法 - -|[0473.火柴拼正方形](https://github.com/youngyangyang04/leetcode/blob/master/problems/0473.火柴拼正方形.md) |深度优先搜索|中等| **回溯算法** 和698.划分为k个相等的子集差不多| - -## 思路 - -这道题目是要找是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。 - -那么只要找到集合里能够出现 sum / 2 的集合,就算是可以分割成两个相同元素和子集了。 - -本来是我是想用回溯暴力搜索出所有答案的,各种剪枝,还是超时了,不想在调了,放弃回溯,直接上01背包吧。 - -如下的讲解中,我讲的重点是如何把01背包应用到此题,而不是讲01背包,如果对01背包本身还不理解的同学,需要额外学习一下基础知识,我后面也会在[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png)里深度讲解背包问题。 - -### 背包问题 - -背包问题,大家都知道,就是书包,书包可以容纳的体积n, 然后有各种商品,每一种商品体积为m,价值为z,问如果把书包塞满(不一定能塞满),书包里的商品最大价值总和是多少。 - -**背包问题有多种背包方式,常见的有:01背包、完全背包、多重背包、分组背包和混合背包等等。** - -要注意背包问题问题中商品是不是可以重复放入。 - -**即一个商品如果可以重复多次放入是完全背包,而只能放入一次是01背包,写法还是不一样的。** - -**要明确本题中我们要使用的是01背包,因为元素我们只能用一次。** - -为了让大家对背包问题有一个整体的了解,可以看如下图: - - - -回归主题:首先,本题要求集合里能否出现总和为 sum / 2 的子集。 - -那么来一一对应一下本题,看看背包问题如果来解决。 - -**只有确定了如下四点,才能把背包问题,套到本题上来。** - -* 背包的体积为sum / 2 -* 背包要放入的商品(集合里的元素)体积为 元素的数值,价值也为元素的数值 -* 背包如何正好装满,说明找到了总和为 sum / 2 的子集。 -* 背包中每一个元素一定是不可重复放入。 - -**按照这四部在分析一波** -* 确定dp数组以及下标的含义 -* 确定递推公式 -* dp数组如何初始化 -* 确定遍历顺序 - -定义里数组为dp[],dp[i] 表示 背包中放入体积为i的商品,最大价值为dp[i]。 - -套到本题,dp[i]表示 背包中总和是i,最大可以凑成总和为i的元素总和为dp[i]。 - -dp[i]一定是小于等于i的,因为背包不能装入超过自身体积的商品(这里理解为元素数值)。 - -**如果dp[i] == i 说明,集合中的元素正好可以凑成总和i,理解这一点很重要。** - -## C++代码如下(详细注释 ) -``` -class Solution { -public: - bool canPartition(vector& nums) { - int sum = 0; - - // dp[i]中的i表示背包内总和 - // 题目中说:每个数组中的元素不会超过 100,数组的大小不会超过 200 - // 那么背包内总和不会大于20000,所以定义一个20000大的数组。 - vector dp(20001, 0); - for (int i = 0; i < nums.size(); i++) { - sum += nums[i]; - } - if (sum % 2 == 1) return false; - int target = sum / 2; - - // 开始 01背包 - for(int i = 0; i < nums.size(); i++) { - for(int j = target; j >= nums[i]; j--) { // 每一个元素一定是不可重复放入,所以从大到小遍历 - dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); - } - } - // 集合中的元素正好可以凑成总和target - if (dp[target] == target) return true; - return false; - } -}; -``` - -### 暴力 - -本来是想用回溯暴力搜索出所有答案的,各种剪枝,还是超时了,不想在调了,放弃回溯,直接上01背包吧。 - -**需要尝试一下记忆化递归** - -回溯搜索超时的代码如下: - -``` -class Solution { -private: - int target; - bool backtracking(vector& nums, int startIndex, int pathSum, vector& used) { - for (int i = startIndex; i < nums.size(); i++) { - if (pathSum > target) break; // 剪枝 - if (target < nums[i]) break; // 剪枝 - if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) { // 去重 - continue; - } - pathSum += nums[i]; - used[i] = true; - if (pathSum == target) return true; - if (backtracking(nums, i + 1, pathSum, used)) return true; - used[i] = false; - pathSum -= nums[i]; - } - return false; - } - -public: - bool canPartition(vector& nums) { - vector used(nums.size(), false); - sort(nums.begin(), nums.end()); - int sum = 0; - for (int i = 0; i < nums.size(); i++) sum += nums[i]; - if (sum % 2 == 1) return false; - target = sum / 2; - cout << "sum:" << sum << " target:" << target << endl; - return backtracking(nums, 0, 0, used); - } -}; - -``` - -``` -class Solution { -private: - bool backtracking(vector& nums, - int k, - int target, // 子集目标和 - int cur, // 当前目标和 - int startIndex, // 起始位置 - vector& used) { // 标记是否使用过 - if (k == 0) return true; // 找到了k个相同子集 - if (cur == target) { // 发现一个合格子集,然后重新开始寻找 - return backtracking(nums, k - 1, target, 0, 0, used); // k-1 - } - for (int i = startIndex; i < nums.size(); i++) { - if (cur + nums[i] <= target && !used[i]) { - used[i] = true; - if (backtracking(nums, k, target, cur + nums[i], i + 1, used)) { - return true; - } - used[i] = false; - } - } - return false; - } - -public: - bool canPartition(vector& nums) { - if (nums.size() < 2) return false; // 火柴数量小于4凑不上正方形 - int sum = 0; - for (int i = 0; i < nums.size(); i++) { - sum += nums[i]; - } - if (sum % 2 != 0) return false; - int target = sum / 2; - vector used(nums.size(), false); - - return backtracking(nums, 2, target, 0, 0, used); - - } -}; -``` diff --git a/problems/0429.N叉树的层序遍历.md b/problems/0429.N叉树的层序遍历.md deleted file mode 100644 index 0b495446..00000000 --- a/problems/0429.N叉树的层序遍历.md +++ /dev/null @@ -1,37 +0,0 @@ - -## 题目地址 -https://leetcode-cn.com/problems/sum-of-left-leaves/ - -## 思路 - -层序遍历,和 102题目一个套路 - -## C++代码 - -### 递归法 - -``` -class Solution { -public: - vector> levelOrder(Node* 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() - Node* node = que.front(); - que.pop(); - vec.push_back(node->val); - for (int i = 0; i < node->children.size(); i++) { - if (node->children[i]) que.push(node->children[i]); - } - } - result.push_back(vec); - } - return result; - - } -}; -``` diff --git a/problems/0434.字符串中的单词数.md b/problems/0434.字符串中的单词数.md deleted file mode 100644 index 14adc189..00000000 --- a/problems/0434.字符串中的单词数.md +++ /dev/null @@ -1,30 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/number-of-segments-in-a-string/ - -## 思路 - -可以把问题抽象为 遇到s[i]是空格,s[i+1]不是空格,就统计一个单词, 然后特别处理一下第一个字符不是空格的情况 - -## C++代码 - -``` -class Solution { -public: - int countSegments(string s) { - int count = 0; - for (int i = 0; i < s.size(); i++) { - // 第一个字符不是空格的情况 - if (i == 0 && s[i] != ' ') { - count++; - } - // 只要s[i]是空格,s[i+1]不是空格,count就加1 - if (i + 1 < s.size() && s[i] == ' ' && s[i + 1] != ' ') { - count++; - } - } - return count; - } -}; -``` -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0435.无重叠区间.md b/problems/0435.无重叠区间.md deleted file mode 100644 index a1579858..00000000 --- a/problems/0435.无重叠区间.md +++ /dev/null @@ -1,192 +0,0 @@ -

- -

-

- - - - - - -

- - -> 代码很简单,思路很高端 - -# 435. 无重叠区间 - -题目链接:https://leetcode-cn.com/problems/non-overlapping-intervals/ - -给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。 - -注意: -可以认为区间的终点总是大于它的起点。 -区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。 - -示例 1: -输入: [ [1,2], [2,3], [3,4], [1,3] ] -输出: 1 -解释: 移除 [1,3] 后,剩下的区间没有重叠。 - -示例 2: -输入: [ [1,2], [1,2], [1,2] ] -输出: 2 -解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。 - -示例 3: -输入: [ [1,2], [2,3] ] -输出: 0 -解释: 你不需要移除任何区间,因为它们已经是无重叠的了。 - -## 思路 - -**相信很多同学看到这道题目都冥冥之中感觉要排序,但是究竟是按照右边界排序,还是按照左边界排序呢?** - -这其实是一个难点! - -按照右边界排序,就要从左向右遍历,因为右边界越小越好,只要右边界越小,留给下一个区间的空间就越大,所以从左向右遍历,优先选右边界小的。 - -按照左边界排序,就要从右向左遍历,因为左边界数值越大越好(越靠右),这样就给前一个区间的空间就越大,所以可以从右向左遍历。 - -如果按照左边界排序,还从左向右遍历的话,其实也可以,逻辑会有所不同。 - -一些同学做这道题目可能真的去模拟去重复区间的行为,这是比较麻烦的,还要去删除区间。 - -题目只是要求移除区间的个数,没有必要去真实的模拟删除区间! - -**我来按照右边界排序,从左向右记录非交叉区间的个数。最后用区间总数减去非交叉区间的个数就是需要移除的区间个数了**。 - -此时问题就是要求非交叉区间的最大个数。 - -右边界排序之后,局部最优:优先选右边界小的区间,所以从左向右遍历,留给下一个区间的空间大一些,从而尽量避免交叉。全局最优:选取最多的非交叉区间。 - -局部最优推出全局最优,试试贪心! - -这里记录非交叉区间的个数还是有技巧的,如图: - -![435.无重叠区间](https://img-blog.csdnimg.cn/20201221201553618.png) - -区间,1,2,3,4,5,6都按照右边界排好序。 - -每次取非交叉区间的时候,都是可右边界最小的来做分割点(这样留给下一个区间的空间就越大),所以第一条分割线就是区间1结束的位置。 - -接下来就是找大于区间1结束位置的区间,是从区间4开始。**那有同学问了为什么不从区间5开始?别忘已经是按照右边界排序的了**。 - -区间4结束之后,在找到区间6,所以一共记录非交叉区间的个数是三个。 - -总共区间个数为6,减去非交叉区间的个数3。移除区间的最小数量就是3。 - -C++代码如下: - -``` -class Solution { -public: - // 按照区间右边界排序 - static bool cmp (const vector& a, const vector& b) { - return a[1] < b[1]; - } - int eraseOverlapIntervals(vector>& intervals) { - if (intervals.size() == 0) return 0; - sort(intervals.begin(), intervals.end(), cmp); - int count = 1; // 记录非交叉区间的个数 - int end = intervals[0][1]; // 记录区间分割点 - for (int i = 1; i < intervals.size(); i++) { - if (end <= intervals[i][0]) { - end = intervals[i][1]; - count++; - } - } - return intervals.size() - count; - } -}; -``` -* 时间复杂度:O(nlogn) ,有一个快排 -* 空间复杂度:O(1) - -大家此时会发现如此复杂的一个问题,代码实现却这么简单! - -# 总结 - -本题我认为难度级别可以算是hard级别的! - -总结如下难点: - -* 难点一:一看题就有感觉需要排序,但究竟怎么排序,按左边界排还是右边界排。 -* 难点二:排完序之后如何遍历,如果没有分析好遍历顺序,那么排序就没有意义了。 -* 难点三:直接求重复的区间是复杂的,转而求最大非重复区间个数。 -* 难点四:求最大非重复区间个数时,需要一个分割点来做标记。 - -**这四个难点都不好想,但任何一个没想到位,这道题就解不了**。 - -一些录友可能看网上的题解代码很简单,照葫芦画瓢稀里糊涂的就过了,但是其题解可能并没有把问题难点讲清楚,然后自己再没有钻研的话,那么一道贪心经典区间问题就这么浪费掉了。 - -贪心就是这样,代码有时候很简单(不是指代码短,而是逻辑简单),但想法是真的难! - -这和动态规划还不一样,动规的代码有个递推公式,可能就看不懂了,而贪心往往是直白的代码,但想法读不懂,哈哈。 - -**所以我把本题的难点也一一列出,帮大家不仅代码看的懂,想法也理解的透彻!** - -# 补充 - -本题其实和[贪心算法:用最少数量的箭引爆气球](https://mp.weixin.qq.com/s/HxVAJ6INMfNKiGwI88-RFw)非常像,弓箭的数量就相当于是非交叉区间的数量,只要把弓箭那道题目代码里射爆气球的判断条件加个等号(认为[0,1][1,2]不是相邻区间),然后用总区间数减去弓箭数量 就是要移除的区间数量了。 - -把[贪心算法:用最少数量的箭引爆气球](https://mp.weixin.qq.com/s/HxVAJ6INMfNKiGwI88-RFw)代码稍做修改,别可以AC本题。 - -```C++ -class Solution { -public: - // 按照区间右边界排序 - static bool cmp (const vector& a, const vector& b) { - return a[1] < b[1]; - } - int eraseOverlapIntervals(vector>& intervals) { - if (intervals.size() == 0) return 0; - sort(intervals.begin(), intervals.end(), cmp); - - int result = 1; // points 不为空至少需要一支箭 - for (int i = 1; i < intervals.size(); i++) { - if (intervals[i][0] >= intervals[i - 1][1]) { - result++; // 需要一支箭 - } - else { // 气球i和气球i-1挨着 - intervals[i][1] = min(intervals[i - 1][1], intervals[i][1]); // 更新重叠气球最小右边界 - } - } - return intervals.size() - result; - } -}; -``` - -这里按照 左区间遍历,或者按照右边界遍历,都可以AC,具体原因我还没有仔细看,后面有空再补充。 -```C++ -class Solution { -public: - // 按照区间左边界排序 - static bool cmp (const vector& a, const vector& b) { - return a[0] < b[0]; - } - int eraseOverlapIntervals(vector>& intervals) { - if (intervals.size() == 0) return 0; - sort(intervals.begin(), intervals.end(), cmp); - - int result = 1; // points 不为空至少需要一支箭 - for (int i = 1; i < intervals.size(); i++) { - if (intervals[i][0] >= intervals[i - 1][1]) { - result++; // 需要一支箭 - } - else { // 气球i和气球i-1挨着 - intervals[i][1] = min(intervals[i - 1][1], intervals[i][1]); // 更新重叠气球最小右边界 - } - } - return intervals.size() - result; - } -}; - -``` - -循序渐进学算法,认准「代码随想录」就够了,值得介绍给身边的朋友同学们! - -> 我是[程序员Carl](https://github.com/youngyangyang04),组队刷题可以找我,本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),期待你的关注! - - -是不是尽可能重叠,其实它都在那里,没有尽可能这一说 diff --git a/problems/0450.删除二叉搜索树中的节点.md b/problems/0450.删除二叉搜索树中的节点.md deleted file mode 100644 index eb607879..00000000 --- a/problems/0450.删除二叉搜索树中的节点.md +++ /dev/null @@ -1,251 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/delete-node-in-a-bst/ - -> 二叉搜索树删除节点就涉及到结构调整了 - -# 450.删除二叉搜索树中的节点 - -题目链接: https://leetcode-cn.com/problems/delete-node-in-a-bst/ - -给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。 - -一般来说,删除节点可分为两个步骤: - -首先找到需要删除的节点; -如果找到了,删除它。 -说明: 要求算法时间复杂度为 O(h),h 为树的高度。 - -示例: - -![450.删除二叉搜索树中的节点](https://img-blog.csdnimg.cn/20201020171048265.png) - -# 思路 - -搜索树的节点删除要比节点增加复杂的多,有很多情况需要考虑,做好心里准备。 - -## 递归 - -递归三部曲: - -* 确定递归函数参数以及返回值 - -说道递归函数的返回值,在[二叉树:搜索树中的插入操作](https://mp.weixin.qq.com/s/lwKkLQcfbCNX2W-5SOeZEA)中通过递归返回值来加入新节点, 这里也可以通过递归返回值删除节点。 - -代码如下: - -``` -TreeNode* deleteNode(TreeNode* root, int key) -``` - -* 确定终止条件 - -遇到空返回,其实这也说明没找到删除的节点,遍历到空节点直接返回了 - -``` -if (root == nullptr) return root; -``` - -* 确定单层递归的逻辑 - -这里就把平衡二叉树中删除节点遇到的情况都搞清楚。 - -有以下五种情况: - -* 第一种情况:没找到删除的节点,遍历到空节点直接返回了 -* 找到删除的节点 - * 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点 - * 第三种情况:删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点 - * 第四种情况:删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点 - * 第五种情况:左右孩子节点都不为空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点。 - -第五种情况有点难以理解,看下面动画: - - - -动画中颗二叉搜索树中,删除元素7, 那么删除节点(元素7)的左孩子就是5,删除节点(元素7)的右子树的最左面节点是元素8。 - -将删除节点(元素7)的左孩子放到删除节点(元素7)的右子树的最左面节点(元素8)的左孩子上,就是把5为根节点的子树移到了8的左孩子的位置。 - -要删除的节点(元素7)的右孩子(元素9)为新的根节点。. - -这样就完成删除元素7的逻辑,最好动手画一个图,尝试删除一个节点试试。 - -代码如下: - -``` -if (root->val == key) { - // 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点 - // 第三种情况:其左孩子为空,右孩子不为空,删除节点,右孩子补位 ,返回右孩子为根节点 - if (root->left == nullptr) return root->right; - // 第四种情况:其右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点 - else if (root->right == nullptr) return root->left; - // 第五种情况:左右孩子节点都不为空,则将删除节点的左子树放到删除节点的右子树的最左面节点的左孩子的位置 - // 并返回删除节点右孩子为新的根节点。 - else { - TreeNode* cur = root->right; // 找右子树最左面的节点 - while(cur->left != nullptr) { - cur = cur->left; - } - cur->left = root->left; // 把要删除的节点(root)左子树放在cur的左孩子的位置 - TreeNode* tmp = root; // 把root节点保存一下,下面来删除 - root = root->right; // 返回旧root的右孩子作为新root - delete tmp; // 释放节点内存(这里不写也可以,但C++最好手动释放一下吧) - return root; - } -} -``` - -这里相当于把新的节点返回给上一层,上一层就要用 root->left 或者 root->right接住,代码如下: - -``` -if (root->val > key) root->left = deleteNode(root->left, key); -if (root->val < key) root->right = deleteNode(root->right, key); -return root; -``` - -**整体代码如下:(注释中:情况1,2,3,4,5和上面分析严格对应)** - -``` -class Solution { -public: - TreeNode* deleteNode(TreeNode* root, int key) { - if (root == nullptr) return root; // 第一种情况:没找到删除的节点,遍历到空节点直接返回了 - if (root->val == key) { - // 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点 - // 第三种情况:其左孩子为空,右孩子不为空,删除节点,右孩子补位 ,返回右孩子为根节点 - if (root->left == nullptr) return root->right; - // 第四种情况:其右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点 - else if (root->right == nullptr) return root->left; - // 第五种情况:左右孩子节点都不为空,则将删除节点的左子树放到删除节点的右子树的最左面节点的左孩子的位置 - // 并返回删除节点右孩子为新的根节点。 - else { - TreeNode* cur = root->right; // 找右子树最左面的节点 - while(cur->left != nullptr) { - cur = cur->left; - } - cur->left = root->left; // 把要删除的节点(root)左子树放在cur的左孩子的位置 - TreeNode* tmp = root; // 把root节点保存一下,下面来删除 - root = root->right; // 返回旧root的右孩子作为新root - delete tmp; // 释放节点内存(这里不写也可以,但C++最好手动释放一下吧) - return root; - } - } - if (root->val > key) root->left = deleteNode(root->left, key); - if (root->val < key) root->right = deleteNode(root->right, key); - return root; - } -}; -``` - -### 普通二叉树的删除方式 - -这里我在介绍一种通用的删除,普通二叉树的删除方式(没有使用搜索树的特性,遍历整棵树),用交换值的操作来删除目标节点。 - -代码中目标节点(要删除的节点)被操作了两次: - -* 第一次是和目标节点的右子树最左面节点交换。 -* 第二次直接被NULL覆盖了。 - -思路有点绕,感兴趣的同学可以画图自己理解一下。 - -代码如下:(关键部分已经注释) - -``` -class Solution { -public: - TreeNode* deleteNode(TreeNode* root, int key) { - if (root == nullptr) return root; - if (root->val == key) { - if (root->right == nullptr) { // 这里第二次操作目标值:最终删除的作用 - return root->left; - } - TreeNode *cur = root->right; - while (cur->left) { - cur = cur->left; - } - swap(root->val, cur->val); // 这里第一次操作目标值:交换目标值其右子树最左面节点。 - } - root->left = deleteNode(root->left, key); - root->right = deleteNode(root->right, key); - return root; - } -}; -``` - -这个代码是简短一些,思路也巧妙,但是不太好想,实操性不强,推荐第一种写法! - -## 迭代法 - -删除节点的迭代法还是复杂一些的,但其本质我在递归法里都介绍了,最关键就是删除节点的操作(动画模拟的过程) - -代码如下: - -``` -class Solution { -private: - // 将目标节点(删除节点)的左子树放到 目标节点的右子树的最左面节点的左孩子位置上 - // 并返回目标节点右孩子为新的根节点 - // 是动画里模拟的过程 - TreeNode* deleteOneNode(TreeNode* target) { - if (target == nullptr) return target; - if (target->right == nullptr) return target->left; - TreeNode* cur = target->right; - while (cur->left) { - cur = cur->left; - } - cur->left = target->left; - return target->right; - } -public: - TreeNode* deleteNode(TreeNode* root, int key) { - if (root == nullptr) return root; - TreeNode* cur = root; - TreeNode* pre = nullptr; // 记录cur的父节点,用来删除cur - while (cur) { - if (cur->val == key) break; - pre = cur; - if (cur->val > key) cur = cur->left; - else cur = cur->right; - } - if (pre == nullptr) { // 如果搜索树只有头结点 - return deleteOneNode(cur); - } - // pre 要知道是删左孩子还是右孩子 - if (pre->left && pre->left->val == key) { - pre->left = deleteOneNode(cur); - } - if (pre->right && pre->right->val == key) { - pre->right = deleteOneNode(cur); - } - return root; - } -}; -``` - -# 总结 - -读完本篇,大家会发现二叉搜索树删除节点比增加节点复杂的多。 - -**因为二叉搜索树添加节点只需要在叶子上添加就可以的,不涉及到结构的调整,而删除节点操作涉及到结构的调整**。 - -这里我们依然使用递归函数的返回值来完成把节点从二叉树中移除的操作。 - -**这里最关键的逻辑就是第五种情况(删除一个左右孩子都不为空的节点),这种情况一定要想清楚**。 - -而且就算想清楚了,对应的代码也未必可以写出来,所以**这道题目即考察思维逻辑,也考察代码能力**。 - -递归中我给出了两种写法,推荐大家学会第一种(利用搜索树的特性)就可以了,第二种递归写法其实是比较绕的。 - -最后我也给出了相应的迭代法,就是模拟递归法中的逻辑来删除节点,但需要一个pre记录cur的父节点,方便做删除操作。 - -迭代法其实不太容易写出来,所以如果是初学者的话,彻底掌握第一种递归写法就够了。 - -**就酱,又是干货满满的一篇,大家加油!** - - - - - - - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0452.用最少数量的箭引爆气球.md b/problems/0452.用最少数量的箭引爆气球.md deleted file mode 100644 index 5a197fd9..00000000 --- a/problems/0452.用最少数量的箭引爆气球.md +++ /dev/null @@ -1,145 +0,0 @@ -

- -

-

- - - - - - -

- -> 思路很直接,但代码并不好写 - -# 452. 用最少数量的箭引爆气球 - -在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。 - -一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足  xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。 - -给你一个数组 points ,其中 points [i] = [xstart,xend] ,返回引爆所有气球所必须射出的最小弓箭数。 - - -示例 1: -输入:points = [[10,16],[2,8],[1,6],[7,12]] - -输出:2 -解释:对于该样例,x = 6 可以射爆 [2,8],[1,6] 两个气球,以及 x = 11 射爆另外两个气球 - -示例 2: -输入:points = [[1,2],[3,4],[5,6],[7,8]] -输出:4 - -示例 3: -输入:points = [[1,2],[2,3],[3,4],[4,5]] -输出:2 - -示例 4: -输入:points = [[1,2]] -输出:1 - -示例 5: -输入:points = [[2,3],[2,3]] -输出:1 - -提示: - -* 0 <= points.length <= 10^4 -* points[i].length == 2 -* -2^31 <= xstart < xend <= 2^31 - 1 - -# 思路 - -如何使用最少的弓箭呢? - -直觉上来看,貌似只射重叠最多的气球,用的弓箭一定最少,那么有没有当前重叠了三个气球,我射两个,留下一个和后面的一起射这样弓箭用的更少的情况呢? - -尝试一下举反例,发现没有这种情况。 - -那么就试一试贪心吧!局部最优:当气球出现重叠,一起射,所用弓箭最少。全局最优:把所有气球射爆所用弓箭最少。 - -**算法确定下来了,那么如何模拟气球射爆的过程呢?是在数组中移除元素还是做标记呢?** - -如果真实的模拟射气球的过程,应该射一个,气球数组就remove一个元素,这样最直观,毕竟气球被射了。 - -但仔细思考一下就发现:如果把气球排序之后,从前到后遍历气球,被射过的气球仅仅跳过就行了,没有必要让气球数组remote气球,只要记录一下箭的数量就可以了。 - -以上为思考过程,已经确定下来使用贪心了,那么开始解题。 - -**为了让气球尽可能的重叠,需要对数组进行排序**。 - -那么按照气球起始位置排序,还是按照气球终止位置排序呢? - -其实都可以!只不过对应的遍历顺序不同,我就按照气球的起始位置排序了。 - -既然按照其实位置排序,那么就从前向后遍历气球数组,靠左尽可能让气球重复。 - -从前向后遍历遇到重叠的气球了怎么办? - -**如果气球重叠了,重叠气球中右边边界的最小值 之前的区间一定需要一个弓箭**。 - -以题目示例: [[10,16],[2,8],[1,6],[7,12]]为例,如图:(方便起见,已经排序) - -![452.用最少数量的箭引爆气球](https://img-blog.csdnimg.cn/20201123101929791.png) - -可以看出首先第一组重叠气球,一定是需要一个箭,气球3,的左边界大于了 第一组重叠气球的最小右边界,所以再需要一支箭来射气球3了。 - -C++代码如下: - -```C++ -class Solution { -private: - static bool cmp(const vector& a, const vector& b) { - return a[0] < b[0]; - } -public: - int findMinArrowShots(vector>& points) { - if (points.size() == 0) return 0; - sort(points.begin(), points.end(), cmp); - - int result = 1; // points 不为空至少需要一支箭 - for (int i = 1; i < points.size(); i++) { - if (points[i][0] > points[i - 1][1]) { // 气球i和气球i-1不挨着,注意这里不是>= - result++; // 需要一支箭 - } - else { // 气球i和气球i-1挨着 - points[i][1] = min(points[i - 1][1], points[i][1]); // 更新重叠气球最小右边界 - } - } - return result; - } -}; -``` - -* 时间复杂度O(nlogn),因为有一个快排 -* 空间复杂度O(1) - -可以看出代码并不复杂。 - -# 注意事项 - -注意题目中说的是:满足 xstart ≤ x ≤ xend,则该气球会被引爆。那么说明两个气球挨在一起不重叠也可以一起射爆, - -所以代码中 `if (points[i][0] > points[i - 1][1])` 不能是>= - -# 总结 - -这道题目贪心的思路很简单也很直接,就是重复的一起射了,但本题我认为是有难度的。 - -就算思路都想好了,模拟射气球的过程,很多同学真的要去模拟了,实时把气球从数组中移走,这么写的话就复杂了。 - -而且寻找重复的气球,寻找重叠气球最小右边界,其实都有代码技巧。 - -贪心题目有时候就是这样,看起来很简单,思路很直接,但是一写代码就感觉贼复杂无从下手。 - -这里其实是需要代码功底的,那代码功底怎么练? - -**多看多写多总结!** - - -**循序渐进学算法,认准[「代码随想录」](https://img-blog.csdnimg.cn/20200815195519696.png),Carl手把手带你过关斩将!** - -

- -

diff --git a/problems/0454.四数相加II.md b/problems/0454.四数相加II.md deleted file mode 100644 index 7695d7ec..00000000 --- a/problems/0454.四数相加II.md +++ /dev/null @@ -1,76 +0,0 @@ -# 题目地址 -https://leetcode-cn.com/problems/4sum-ii/ - -> 需要哈希的地方都能找到map的身影 - -# 第454题.四数相加II - -给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0。 - -为了使问题简单化,所有的 A, B, C, D 具有相同的长度 N,且 0 ≤ N ≤ 500 。所有整数的范围在 -2^28 到 2^28 - 1 之间,最终结果不会超过 2^31 - 1 。 - -**例如:** - -输入: -A = [ 1, 2] -B = [-2,-1] -C = [-1, 2] -D = [ 0, 2] - -输出: -2 - -**解释:** -两个元组如下: -1. (0, 0, 0, 1) -> A[0] + B[0] + C[0] + D[1] = 1 + (-2) + (-1) + 2 = 0 -2. (1, 1, 0, 0) -> A[1] + B[1] + C[0] + D[0] = 2 + (-1) + (-1) + 0 = 0 - - -# 思路 - -本题咋眼一看好像和[0015.三数之和](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A),[0018.四数之和](https://mp.weixin.qq.com/s/nQrcco8AZJV1pAOVjeIU_g)差不多,其实差很多。 - -**本题是使用哈希法的经典题目,而[0015.三数之和](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A),[0018.四数之和](https://mp.weixin.qq.com/s/nQrcco8AZJV1pAOVjeIU_g)并不合适使用哈希法**,因为三数之和和四数之和这两道题目使用哈希法在不超时的情况下做到对结果去重是很困难的,很有多细节需要处理。 - -**而这道题目是四个独立的数组,只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不用考虑有重复的四个元素相加等于0的情况,所以相对于题目18. 四数之和,题目15.三数之和,还是简单了不少!** - -如果本题想难度升级:就是给出一个数组(而不是四个数组),在这里找出四个元素相加等于0,答案中不可以包含重复的四元组,大家可以思考一下,后续的文章我也会讲到的。 - -本题解题步骤: - -1. 首先定义 一个unordered_map,key放a和b两数之和,value 放a和b两数之和出现的次数。 -2. 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中。 -3. 定义int变量count,用来统计a+b+c+d = 0 出现的次数。 -4. 在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就用count把map中key对应的value也就是出现次数统计出来。 -5. 最后返回统计值 count 就可以了 - -# C++代码 - -``` -class Solution { -public: - int fourSumCount(vector& A, vector& B, vector& C, vector& D) { - unordered_map umap; //key:a+b的数值,value:a+b数值出现的次数 - // 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中 - for (int a : A) { - for (int b : B) { - umap[a + b]++; - } - } - int count = 0; // 统计a+b+c+d = 0 出现的次数 - // 在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就把map中key对应的value也就是出现次数统计出来。 - for (int c : C) { - for (int d : D) { - if (umap.find(0 - (c + d)) != umap.end()) { - count += umap[0 - (c + d)]; - } - } - } - return count; - } -}; - -``` - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0455.分发饼干.md b/problems/0455.分发饼干.md deleted file mode 100644 index d86e775d..00000000 --- a/problems/0455.分发饼干.md +++ /dev/null @@ -1,117 +0,0 @@ -> 贪心的第一道题目,快看看你够不够贪心 - -通知:一些录友表示经常看不到每天的文章,现在公众号已经不按照发送时间推荐了,而是根据一些规则乱序推送,所以可能关注了「代码随想录」也一直看不到文章,建议把「代码随想录」设置星标哈,设置星标之后,每天就按发文时间推送了,我每天都是定时8:35发送的,嗷嗷准时! - -# 455.分发饼干 - -题目链接:https://leetcode-cn.com/problems/assign-cookies/ - -假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。 - -对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。 - -示例 1: -输入: g = [1,2,3], s = [1,1] -输出: 1 -解释: -你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。 -虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。 -所以你应该输出1。 - -示例 2: -输入: g = [1,2], s = [1,2,3] -输出: 2 -解释: -你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。 -你拥有的饼干数量和尺寸都足以让所有孩子满足。 -所以你应该输出2. -  - -提示: -* 1 <= g.length <= 3 * 10^4 -* 0 <= s.length <= 3 * 10^4 -* 1 <= g[i], s[j] <= 2^31 - 1 - - -## 思路 - -为了了满足更多的小孩,就不要造成饼干尺寸的浪费。 - -大尺寸的饼干既可以满足胃口大的孩子也可以满足胃口小的孩子,那么就应该优先满足胃口大的。 - -**这里的局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩**。 - -可以尝试使用贪心策略,先将饼干数组和小孩数组排序。 - -然后从后向前遍历小孩数组,用大饼干优先满足胃口大的,并统计满足小孩数量。 - -如图: - -![455.分发饼干](https://img-blog.csdnimg.cn/20201123161809624.png) - -这个例子可以看出饼干9只有喂给胃口为7的小孩,这样才是整体最优解,并想不出反例,那么就可以撸代码了。 - - -C++代码整体如下: - -``` -// 时间复杂度:O(nlogn) -// 空间复杂度:O(1) -class Solution { -public: - int findContentChildren(vector& g, vector& s) { - sort(g.begin(), g.end()); - sort(s.begin(), s.end()); - int index = s.size() - 1; // 饼干数组的下表 - int result = 0; - for (int i = g.size() - 1; i >= 0; i--) { - if (index >= 0 && s[index] >= g[i]) { - result++; - index--; - } - } - return result; - } -}; -``` - -从代码中可以看出我用了一个index来控制饼干数组的遍历,遍历饼干并没有再起一个for循环,而是采用自减的方式,这也是常用的技巧。 - -有的同学看到要遍历两个数组,就想到用两个for循环,那样逻辑其实就复杂了。 - -**也可以换一个思路,小饼干先喂饱小胃口** - -代码如下: - -``` -class Solution { -public: - int findContentChildren(vector& g, vector& s) { - sort(g.begin(),g.end()); - sort(s.begin(),s.end()); - int res = 0; - int index = 0; - for(int i = 0;i < s.size();++i){ - if(index < g.size() && g[index] <= s[i]){ - index++; - res++; - } - } - return res; - } -}; -``` - -# 总结 - -这道题是贪心很好的一道入门题目,思路还是比较容易想到的。 - -文中详细介绍了思考的过程,**想清楚局部最优,想清楚全局最优,感觉局部最优是可以推出全局最优,并想不出反例,那么就试一试贪心**。 - -就酱,「代码随想录」值得介绍给身边的朋友同学们! - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** - - diff --git a/problems/0459.重复的子字符串.md b/problems/0459.重复的子字符串.md deleted file mode 100644 index da163490..00000000 --- a/problems/0459.重复的子字符串.md +++ /dev/null @@ -1,138 +0,0 @@ - -## 题目地址 -https://leetcode-cn.com/problems/repeated-substring-pattern/ - -> KMP算法还能干这个 - -# 题目459.重复的子字符串 - -给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。 - -示例 1: -输入: "abab" -输出: True -解释: 可由子字符串 "ab" 重复两次构成。 - -示例 2: -输入: "aba" -输出: False - -示例 3: -输入: "abcabcabcabc" -输出: True -解释: 可由子字符串 "abc" 重复四次构成。 (或者子字符串 "abcabc" 重复两次构成。) - -# 思路 - -这又是一道标准的KMP的题目。 - -如果KMP还不够了解,可以看我的B站: - -* [帮你把KMP算法学个通透!B站(理论篇)](https://www.bilibili.com/video/BV1PD4y1o7nd/) -* [帮你把KMP算法学个通透!(求next数组代码篇)](https://www.bilibili.com/video/BV1M5411j7Xx) - - -如果KMP还不够了解,可以看我的这个视频[帮你把KMP算法学个通透!B站](https://www.bilibili.com/video/BV1PD4y1o7nd/) - -我们在[字符串:都来看看KMP的看家本领!](https://mp.weixin.qq.com/s/Gk9FKZ9_FSWLEkdGrkecyg)里提到了,在一个串中查找是否出现过另一个串,这是KMP的看家本领。 - -那么寻找重复子串怎么也涉及到KMP算法了呢? - -这里就要说一说next数组了,next 数组记录的就是最长相同前后缀( [字符串:听说你对KMP有这些疑问?](https://mp.weixin.qq.com/s/mqx6IM2AO4kLZwvXdPtEeQ) 这里介绍了什么是前缀,什么是后缀,什么又是最长相同前后缀), 如果 next[len - 1] != -1,则说明字符串有最长相同的前后缀(就是字符串里的前缀子串和后缀子串相同的最长长度)。 - -最长相等前后缀的长度为:next[len - 1] + 1。 - -数组长度为:len。 - -如果len % (len - (next[len - 1] + 1)) == 0 ,则说明 (数组长度-最长相等前后缀的长度) 正好可以被 数组的长度整除,说明有该字符串有重复的子字符串。 - -**强烈建议大家把next数组打印出来,看看next数组里的规律,有助于理解KMP算法** - -如图: - - - -此时next[len - 1] = 7,next[len - 1] + 1 = 8,8就是此时 字符串asdfasdfasdf的最长相同前后缀的长度。 - - -(len - (next[len - 1] + 1)) 也就是: 12(字符串的长度) - 8(最长公共前后缀的长度) = 4, 4正好可以被 12(字符串的长度) 整除,所以说明有重复的子字符串(asdf)。 - -代码如下: - -# C++代码 -``` -class Solution { -public: - // KMP里标准构建next数组的过程 - void getNext (int* next, const string& s){ - next[0] = -1; - int j = -1; - for(int i = 1;i < s.size(); i++){ - while(j >= 0 && s[i] != s[j+1]) { - j = next[j]; - } - if(s[i] == s[j+1]) { - j++; - } - next[i] = j; - } - } - bool repeatedSubstringPattern (string s) { - if (s.size() == 0) { - return false; - } - int next[s.size()]; - getNext(next, s); - int len = s.size(); - if (next[len - 1] != -1 && len % (len - (next[len - 1] + 1)) == 0) { - return true; - } - return false; - } -}; -``` - -# 前缀表不右移 C++代码 - -``` -class Solution { -public: - // KMP里标准构建next数组的过程 - void getNext (int* next, const string& s){ - next[0] = 0; - int j = 0; - for(int i = 1;i < s.size(); i++){ - while(j > 0 && s[i] != s[j]) { - j = next[j - 1]; - } - if(s[i] == s[j]) { - j++; - } - next[i] = j; - } - } - bool repeatedSubstringPattern (string s) { - if (s.size() == 0) { - return false; - } - int next[s.size()]; - getNext(next, s); - int len = s.size(); - if (next[len - 1] != 0 && len % (len - (next[len - 1] )) == 0) { - return true; - } - return false; - } -}; -``` - -# 拓展 - -此时我们已经分享了三篇KMP的文章,首先是[字符串:KMP是时候上场了(一文读懂系列)](https://mp.weixin.qq.com/s/70OXnZ4Ez29CKRrUpVJmug)讲解KMP算法的基础理论,给出next数组究竟是如何来了,前缀表又是怎么回事,为什么要选择前缀表。 - -然后通过[字符串:都来看看KMP的看家本领!](https://mp.weixin.qq.com/s/Gk9FKZ9_FSWLEkdGrkecyg)讲解一道KMP的经典题目,判断文本串里是否出现过模式串,这里涉及到构造next数组的代码实现,以及使用next数组完成模式串与文本串的匹配过程。 - -后来很多同学反馈说:搞不懂前后缀,什么又是最长相同前后缀(最长公共前后缀我认为这个用词不准确),以及为什么前缀表要统一减一(右移)呢,不减一行不行?针对这些问题,我在[字符串:听说你对KMP有这些疑问?](https://mp.weixin.qq.com/s/mqx6IM2AO4kLZwvXdPtEeQ)中又给出了详细的讲解。 - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0463.岛屿的周长.md b/problems/0463.岛屿的周长.md deleted file mode 100644 index b2a40d8c..00000000 --- a/problems/0463.岛屿的周长.md +++ /dev/null @@ -1,80 +0,0 @@ -## 题目链接 -https://leetcode-cn.com/problems/island-perimeter/ - -## 思路 - -岛屿问题最容易让人想到BFS或者DFS,但是这道题还真的没有必要,别把简单问题搞复杂了。 - -### 解法一: - -遍历每一个空格,遇到岛屿,计算其上下左右的情况,遇到水域或者出界的情况,就可以计算边了。 - -如图: - - - -C++代码如下:(详细注释) - -```C++ -class Solution { -public: - int direction[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; - int islandPerimeter(vector>& grid) { - int result = 0; - for (int i = 0; i < grid.size(); i++) { - for (int j = 0; j < grid[0].size(); j++) { - if (grid[i][j] == 1) { - for (int k = 0; k < 4; k++) { // 上下左右四个方向 - int x = i + direction[k][0]; - int y = j + direction[k][1]; // 计算周边坐标x,y - if (x < 0 // i在边界上 - || x >= grid.size() // i在边界上 - || y < 0 // j在边界上 - || y >= grid[0].size() // j在边界上 - || grid[x][y] == 0) { // x,y位置是水域 - result++; - } - } - } - } - } - return result; - } -}; -``` - -### 解法二: - -计算出总的岛屿数量,因为有一对相邻两个陆地,边的总数就减2,那么在计算出相邻岛屿的数量就可以了。 - -result = 岛屿数量 * 4 - cover * 2; - -如图: - - - -C++代码如下:(详细注释) - -```C++ -class Solution { -public: - int islandPerimeter(vector>& grid) { - int sum = 0; // 陆地数量 - int cover = 0; // 相邻数量 - for (int i = 0; i < grid.size(); i++) { - for (int j = 0; j < grid[0].size(); j++) { - if (grid[i][j] == 1) { - sum++; - // 统计上边相邻陆地 - if(i - 1 >= 0 && grid[i - 1][j] == 1) cover++; - // 统计左边相邻陆地 - if(j - 1 >= 0 && grid[i][j - 1] == 1) cover++; - // 为什么没统计下边和右边? 因为避免重复计算 - } - } - } - return sum * 4 - cover * 2; - } -}; -``` - diff --git a/problems/0473.火柴拼正方形.md b/problems/0473.火柴拼正方形.md deleted file mode 100644 index fccfedfb..00000000 --- a/problems/0473.火柴拼正方形.md +++ /dev/null @@ -1,42 +0,0 @@ - -698.划分为k个相等的子集 的代码几乎不用改动,就可以AC -``` -class Solution { -private: - bool backtracking(vector& nums, - int k, - int target, // 子集目标和 - int cur, // 当前目标和 - int startIndex, // 起始位置 - vector& used) { // 标记是否使用过 - if (k == 0) return true; // 找到了k个相同子集 - if (cur == target) { // 发现一个合格子集,然后重新开始寻找 - return backtracking(nums, k - 1, target, 0, 0, used); // k-1 - } - for (int i = startIndex; i < nums.size(); i++) { - if (cur + nums[i] <= target && !used[i]) { - used[i] = true; - if (backtracking(nums, k, target, cur + nums[i], i + 1, used)) { - return true; - } - used[i] = false; - } - } - return false; - } -public: - bool makesquare(vector& nums) { - if (nums.size() < 4) return false; // 火柴数量小于4凑不上正方形 - int sum = 0; - for (int i = 0; i < nums.size(); i++) { - sum += nums[i]; - } - if (sum % 4 != 0) return false; - int target = sum / 4; - vector used(nums.size(), false); - - return backtracking(nums, 4, target, 0, 0, used); - - } -}; -``` diff --git a/problems/0474.一和零.md b/problems/0474.一和零.md deleted file mode 100644 index 3d8359cd..00000000 --- a/problems/0474.一和零.md +++ /dev/null @@ -1,36 +0,0 @@ - -// 即使做了很多动态规划的题目,做这个依然懵逼 -// 这道题目有点 程序员自己给自己出难进急转弯的意思 -// 该子集中 最多 有 m 个 0 和 n 个 1 。 指的是整体子集 -// 这是二维背包,多重背包 -// dp[i][j] 有i个0,j个1最大有多少个子集,但是遍历的时候 顶部是哪里呢? - -搞不懂 leetcode后台是什么牛逼的编译器,初始化int dp[101][101] = {0}; 可以 ,int dp[101][101];就不行,有其他默认值,坑死。 -代码我做了实验,后台会拿findMaxForm,运行两次,取第二次的结果,dp有上次记录的数值。 - -本题其实不是多重背包问题,还是一个01背包问题,m 和 n 可以理解是一个二维的背包,而不同长度的字符串就是不同大小的待装物品。 - - -``` -class Solution { -public: - int findMaxForm(vector& strs, int m, int n) { - int dp[101][101] = {0}; // 默认初始化0 - for (int i = 0; i < strs.size(); i++) { - int oneNum = 0, zeroNum = 0; - for (char c : strs[i]) { - if (c == '0') zeroNum++; - else oneNum++; - } - // 果然还是从后向前,模拟01背包 - for (int j = m; j >= zeroNum; j--) { - for (int k = n; k >= oneNum; k--) { - dp[j][k] = max(dp[j][k], dp[j - zeroNum][k - oneNum] + 1); - } - } - } - return dp[m][n]; - } -}; - -``` diff --git a/problems/0486.预测赢家.md b/problems/0486.预测赢家.md deleted file mode 100644 index 7b0e083c..00000000 --- a/problems/0486.预测赢家.md +++ /dev/null @@ -1,279 +0,0 @@ -## 题目地址 - -## 思路 - -在做这道题目的时候,最直接的想法,就是计算出player1可能得到的最大分数,然后用数组总和减去player1的得分就是player2的得分,然后两者比较一下就可以了。 - -那么问题是如何计算player1可能得到的最大分数呢。 - -## 单独计算玩家得分 - -以player1选数字的过程,画图如下: - - - -可以发现是一个递归的过程。 - -按照递归三部曲来: - -1. 确定递归函数的含义,参数以及返回值。 - -定义函数getScore,就是用来获取玩家1的最大得分。 参数为start 和 end 代表获取[start, end]这个区间的最大值,当然还需要传入nums。 - -返回值就是玩家1的最大得分。 - -代码如下: - -``` -int getScore(vector& nums, int start, int end) { -``` - - -2. 确定终止条件 - -当start == end的时候,玩家A的得分就是nums[start],代码如下: -``` - if (start == end) { - return nums[start]; - } -``` - -3. 确定单层递归逻辑 - -玩家1的得分,等于集合左元素的数值+ 玩家2选择后集合的最小值(因为玩家2也是最聪明的) - - -而且剩余集合中的元素数量为2,或者大于2,的处理逻辑是不一样的! - -如图:当集合中的元素数量大于2,那么玩家1先选,玩家2依然有选择的权利。 - -所以代码如下: -``` - if ((end - start) >= 2) { - selectLeft = nums[start] + min(getScore(nums, start + 2, end), getScore(nums, start + 1, end - 1)); - selectRight = nums[end] + min(getScore(nums, start + 1, end - 1), getScore(nums, start, end - 2)); - } -``` - - -如图:当集合中的元素数量等于2,那么玩家1先选,玩家2没得选。 - - -所以代码如下: -``` - if ((end - start) == 1) { - selectLeft = nums[start]; - selectRight = nums[end]; - } -``` - -单层递归逻辑代码如下: - -``` - int selectLeft, selectRight; - if ((end - start) >= 2) { - selectLeft = nums[start] + min(getScore(nums, start + 2, end), getScore(nums, start + 1, end - 1)); - selectRight = nums[end] + min(getScore(nums, start + 1, end - 1), getScore(nums, start, end - 2)); - } - if ((end - start) == 1) { - selectLeft = nums[start]; - selectRight = nums[end]; - } - - return max(selectLeft, selectRight); -``` - -这些可以写出这道题目整体代码如下: - -``` -class Solution { -private: -int getScore(vector& nums, int start, int end) { - if (start == end) { - return nums[start]; - } - int selectLeft, selectRight; - if ((end - start) >= 2) { - selectLeft = nums[start] + min(getScore(nums, start + 2, end), getScore(nums, start + 1, end - 1)); - selectRight = nums[end] + min(getScore(nums, start + 1, end - 1), getScore(nums, start, end - 2)); - } - if ((end - start) == 1) { - selectLeft = nums[start]; - selectRight = nums[end]; - } - - return max(selectLeft, selectRight); -} -public: - bool PredictTheWinner(vector& nums) { - int sum = 0; - for (int i : nums) { - sum += i; - } - int player1 = getScore(nums, 0, nums.size() - 1); - int player2 = sum - player1; - return player1 >= player2; - } -}; -``` - -可以有一个优化,就是把重复计算的数值提取出来,如下: -``` -class Solution { -private: -int getScore(vector& nums, int start, int end) { - int selectLeft, selectRight; - int gap = end - start; - if (gap == 0) { - return nums[start]; - } else if (gap == 1) { // 此时直接取左右的值就可以 - selectLeft = nums[start]; - selectRight = nums[end]; - } else if (gap >= 2) { // 如果gap大于2,递归计算selectLeft和selectRight - // 计算的过程为什么用min,因为要按照对手也是最聪明的来计算。 - int num = getScore(nums, start + 1, end - 1); - selectLeft = nums[start] + - min(getScore(nums, start + 2, end), num); - selectRight = nums[end] + - min(num, getScore(nums, start, end - 2)); - } - return max(selectLeft, selectRight); -} -public: - bool PredictTheWinner(vector& nums) { - int sum = 0; - for (int i : nums) { - sum += i; - } - int player1 = getScore(nums, 0, nums.size() - 1); - int player2 = sum - player1; - // 如果最终两个玩家的分数相等,那么玩家 1 仍为赢家,所以是大于等于。 - return player1 >= player2; - } -}; -``` - -## 计算两个玩家的差值 - -以上是单独计算出两个选手的得分,逻辑上直观,但是代码确实比较冗余。 - -因为就我们要求的结果其实就是两个选手的胜负,那么不用两个选手的得分,而是把问题转换为两个选手所拿元素的差值。 - -代码如下: - -``` -class Solution { -private: -int getScore(vector& nums, int start, int end) { - if (end == start) { - return nums[start]; - } - int selectLeft = nums[start] - getScore(nums, start + 1, end); - int selectRight = nums[end] - getScore(nums, start, end - 1); - return max(selectLeft, selectRight); -} -public: - bool PredictTheWinner(vector& nums) { - return getScore(nums, 0, nums.size() - 1) >=0 ; - } -}; -``` - -计算的过程有一些是冗余的,在递归的过程中,可以使用一个memory数组记录一下中间结果,代码如下: - -``` -class Solution { -private: -int getScore(vector& nums, int start, int end, int memory[21][21]) { - if (end == start) { - return nums[start]; - } - if (memory[start][end]) return memory[start][end]; - int selectLeft = nums[start] - getScore(nums, start + 1, end, memory); - int selectRight = nums[end] - getScore(nums, start, end - 1, memory); - memory[start][end] = max(selectLeft, selectRight); - return memory[start][end]; -} -public: - bool PredictTheWinner(vector& nums) { - int memory[21][21] = {0}; // 记录递归中中间结果 - return getScore(nums, 0, nums.size() - 1, memory) >= 0 ; - } -}; -``` - -此时效率已经比较高了 - - -那么在看一下动态规划的思路。 - - -## 动态规划 - - -定义一个二维数组,先明确是用来干什么的,dp[i][j] 表示两个玩家在数组 i 到 j 区间内游戏能赢对方的差值(i <= j)。 - -假如玩家1先取左端 nums[i],那么玩家2能赢对方的差值是dp[i+1][j] ,如果玩家1先取取右端 nums[j],玩家2能赢对方的差值就是dp[i][j-1], - -那么 不难理解如下公式: - -`dp[i][j] = max((nums[i] - dp[i + 1][j]), (nums[j] - dp[i][j - 1])); ` - - -确定了状态转移公式之后,就要想想如何遍历。 - -一些同学确定的方程,却不知道该如何遍历这个遍历推算出方程的结果,我们来看一下。 - -首先要给dp[i][j]进行初始化,首先当i == j的时候,nums[i]就是dp[i][j]的值。 - -代码如下: - -``` -// 当i == j的时候,nums[i]就是dp[i][j] -for (int i = 0; i < nums.size(); i++) { - dp[i][i] = nums[i]; -} -``` - -接下来就要推导公式了,首先要知道最终求是dp[0][nums.size() - 1]是否大于等于0,也就是求dp[0][nums.size() - 1] 至关重要。 - -从下图中,可以看出在推导方程的时候一定要从右下角向上推导,而且矩阵左半部分根本不用管! - - - -按照上图中的规则,不难列出推导公式的循环方式如下: - -``` -for(int i = nums.size() - 2; i >= 0; i--) { - for (int j = i + 1; j < nums.size(); j++) { - dp[i][j] = max((nums[i] - dp[i + 1][j]), (nums[j] - dp[i][j - 1])); - } -} - -``` - -最后整体动态规划的代码: - -## - -``` -class Solution { -public: - bool PredictTheWinner(vector& nums) { - // dp[i][j] 表示两个玩家在数组 i 到 j 区间内游戏能赢对方的差值(i <= j) - int dp[22][22] = {0}; - // 当i == j的时候,nums[i]就是dp[i][j] - for (int i = 0; i < nums.size(); i++) { - dp[i][i] = nums[i]; - } - for(int i = nums.size() - 2; i >= 0; i--) { - for (int j = i + 1; j < nums.size(); j++) { - dp[i][j] = max((nums[i] - dp[i + 1][j]), (nums[j] - dp[i][j - 1])); - } - } - return dp[0][nums.size() - 1] >= 0; - } -}; -``` - - diff --git a/problems/0491.递增子序列.md b/problems/0491.递增子序列.md deleted file mode 100644 index 15112c04..00000000 --- a/problems/0491.递增子序列.md +++ /dev/null @@ -1,196 +0,0 @@ - -> 和子集问题有点像,但又处处是陷阱 - -# 491.递增子序列 - -题目链接:https://leetcode-cn.com/problems/increasing-subsequences/ - -给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。 - -示例: - -输入: [4, 6, 7, 7] -输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]] - -说明: -* 给定数组的长度不会超过15。 -* 数组中的整数范围是 [-100,100]。 -* 给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况。 - - -# 思路 - -这个递增子序列比较像是取有序的子集。而且本题也要求不能有相同的递增子序列。 - -这又是子集,又是去重,是不是不由自主的想起了刚刚讲过的[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)。 - -就是因为太像了,更要注意差别所在,要不就掉坑里了! - -在[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)中我们是通过排序,再加一个标记数组来达到去重的目的。 - -而本题求自增子序列,是不能对原数组经行排序的,排完序的数组都是自增子序列了。 - -**所以不能使用之前的去重逻辑!** - -本题给出的示例,还是一个有序数组 [4, 6, 7, 7],这更容易误导大家按照排序的思路去做了。 - -为了有鲜明的对比,我用[4, 7, 6, 7]这个数组来举例,抽象为树形结构如图: - -![491. 递增子序列1](https://img-blog.csdnimg.cn/20201124200229824.png) - - -## 回溯三部曲 - -* 递归函数参数 - -本题求子序列,很明显一个元素不能重复使用,所以需要startIndex,调整下一层递归的起始位置。 - -代码如下: - -``` -vector> result; -vector path; -void backtracking(vector& nums, int startIndex) -``` - -* 终止条件 - -本题其实类似求子集问题,也是要遍历树形结构找每一个节点,所以和[回溯算法:求子集问题!](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA)一样,可以不加终止条件,startIndex每次都会加1,并不会无限递归。 - -但本题收集结果有所不同,题目要求递增子序列大小至少为2,所以代码如下: - -``` -if (path.size() > 1) { - result.push_back(path); - // 注意这里不要加return,因为要取树上的所有节点 -} -``` - -* 单层搜索逻辑 - -![491. 递增子序列1](https://img-blog.csdnimg.cn/20201124200229824.png) -在图中可以看出,**同一父节点下的同层上使用过的元素就不能在使用了** - -那么单层搜索代码如下: - -``` -unordered_set uset; // 使用set来对本层元素进行去重 -for (int i = startIndex; i < nums.size(); i++) { - if ((!path.empty() && nums[i] < path.back()) - || uset.find(nums[i]) != uset.end()) { - continue; - } - uset.insert(nums[i]); // 记录这个元素在本层用过了,本层后面不能再用了 - path.push_back(nums[i]); - backtracking(nums, i + 1); - path.pop_back(); -} -``` - -**对于已经习惯写回溯的同学,看到递归函数上面的`uset.insert(nums[i]);`,下面却没有对应的pop之类的操作,应该很不习惯吧,哈哈** - -**这也是需要注意的点,`unordered_set uset;` 是记录本层元素是否重复使用,新的一层uset都会重新定义(清空),所以要知道uset只负责本层!** - - -最后整体C++代码如下: - -## C++代码 - -``` -// 版本一 -class Solution { -private: - vector> result; - vector path; - void backtracking(vector& nums, int startIndex) { - if (path.size() > 1) { - result.push_back(path); - // 注意这里不要加return,要取树上的节点 - } - unordered_set uset; // 使用set对本层元素进行去重 - for (int i = startIndex; i < nums.size(); i++) { - if ((!path.empty() && nums[i] < path.back()) - || uset.find(nums[i]) != uset.end()) { - continue; - } - uset.insert(nums[i]); // 记录这个元素在本层用过了,本层后面不能再用了 - path.push_back(nums[i]); - backtracking(nums, i + 1); - path.pop_back(); - } - } -public: - vector> findSubsequences(vector& nums) { - result.clear(); - path.clear(); - backtracking(nums, 0); - return result; - } -}; -``` - -## 优化 - -以上代码用我用了`unordered_set`来记录本层元素是否重复使用。 - -**其实用数组来做哈希,效率就高了很多**。 - -注意题目中说了,数值范围[-100,100],所以完全可以用数组来做哈希。 - -程序运行的时候对unordered_set 频繁的insert,unordered_set需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)相对费时间,而且每次重新定义set,insert的时候其底层的符号表也要做相应的扩充,也是费事的。 - -那么优化后的代码如下: - -``` -// 版本二 -class Solution { -private: - vector> result; - vector path; - void backtracking(vector& nums, int startIndex) { - if (path.size() > 1) { - result.push_back(path); - } - int used[201] = {0}; // 这里使用数组来进行去重操作,题目说数值范围[-100, 100] - for (int i = startIndex; i < nums.size(); i++) { - if ((!path.empty() && nums[i] < path.back()) - || used[nums[i] + 100] == 1) { - continue; - } - used[nums[i] + 100] = 1; // 记录这个元素在本层用过了,本层后面不能再用了 - path.push_back(nums[i]); - backtracking(nums, i + 1); - path.pop_back(); - } - } -public: - vector> findSubsequences(vector& nums) { - result.clear(); - path.clear(); - backtracking(nums, 0); - return result; - } -}; -``` - -这份代码在leetcode上提交,要比版本一耗时要好的多。 - -**所以正如在[哈希表:总结篇!(每逢总结必经典)](https://mp.weixin.qq.com/s/1s91yXtarL-PkX07BfnwLg)中说的那样,数组,set,map都可以做哈希表,而且数组干的活,map和set都能干,但如何数值范围小的话能用数组尽量用数组**。 - - - -# 总结 - -本题题解清一色都说是深度优先搜索,但我更倾向于说它用回溯法,而且本题我也是完全使用回溯法的逻辑来分析的。 - -相信大家在本题中处处都能看到是[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)的身影,但处处又都是陷阱。 - -**对于养成思维定式或者套模板套嗨了的同学,这道题起到了很好的警醒作用。更重要的是拓展了大家的思路!** - -**就酱,如果感觉「代码随想录」很干货,就帮Carl宣传一波吧!** - - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/0494.目标和.md b/problems/0494.目标和.md deleted file mode 100644 index 8adbd8a2..00000000 --- a/problems/0494.目标和.md +++ /dev/null @@ -1,188 +0,0 @@ - - -// 这道题小细节很多 -// 转为01 背包 思路不好想啊 -// dp数组难在如何初始化 -// dp 数组 通常比较长 - -如果跟着「代码随想录」一起学过[回溯算法系列](https://mp.weixin.qq.com/s/r73thpBnK1tXndFDtlsdCQ)的录友,看到这道题,应该有一种直觉,就是感觉好像回溯法可以爆搜出来。 - -事实确实如此,下面我也会给出响应的代码,只不过会超时,哈哈。 - -这道题目咋眼一看和动态规划背包啥的也没啥关系。 - -本题要如何是表达式结果为target, - -既然为target,那么就一定有 left组合 - right组合 = target,中的left 和right一定是固定大小的,因为left + right要等于sum,而sum是固定的。 - -公式来了, left - (sum - left) = target -> left = (target + sum)/2 。 - -target是固定的,sum是固定的,left就可以求出来。 - -此时问题就是在集合nums中找出和为left的组合。 - -在回溯算法系列中,一起学过这道题目[回溯算法:39. 组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)的录友应该感觉很熟悉,这不就是组合总和问题么? - -此时可以套组合总和的回溯法代码,几乎不用改动。 - -当然,也可以转变成序列区间选+ 或者 -,使用回溯法,那就是另一个解法。 - -但无论哪种回溯法,时间复杂度都是是O(2^n)级别,**所以最后超时了**。 - -我也把代码给出来吧,大家可以了解一下,回溯的解法,以下是本题转变为组合总和问题的回溯法代码: -``` -class Solution { -private: - vector> result; - vector path; - void backtracking(vector& candidates, int target, int sum, int startIndex) { - if (sum == target) { - result.push_back(path); - } - - // 如果 sum + candidates[i] > target 就终止遍历 - for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) { - sum += candidates[i]; - path.push_back(candidates[i]); - backtracking(candidates, target, sum, i + 1); - sum -= candidates[i]; - path.pop_back(); - - } - } -public: - int findTargetSumWays(vector& nums, int S) { - int sum = 0; - for (int i = 0; i < nums.size(); i++) sum += nums[i]; - if (S > sum) return 0; // 此时没有方案 - if ((S + sum) % 2) return 0; // 此时没有方案,两个int相加的时候要各位小心数值溢出的问题 - int bagSize = (S + sum) / 2; // 转变为组合总和问题,bagsize就是要求的和 - - // 以下为回溯法代码 - result.clear(); - path.clear(); - sort(nums.begin(), nums.end()); // 需要排序 - backtracking(nums, bagSize, 0, 0); - return result.size(); - } -}; -``` - -## 动态规划 - -如何转化为01背包问题呢。 - -假设加法的总和为x,那么减法对应的总和就是sum - x。 - -所以我们要求的是 x - (sum - x) = S - -x = (S + sum) / 2 - -此时问题就转化为,装满容量为x背包,有几种方法。 - -大家看到(S + sum) / 2 应该担心计算的过程中向下取整有没有影响。 - -这么担心就对了,例如sum 是5,S是2的话其实就是无解的,所以: - -``` -if ((S + sum) % 2 == 1) return 0; // 此时没有方案,两个int相加的时候要各位小心数值溢出的问题 -``` - -看到这种表达式,应该本能的反应,两个int相加数值可能溢出的问题,当然本题并没有溢出。 - -在回归到01背包问题, - -这次和之前遇到的背包问题不一样了,之前都是求容量为j的背包,最多能装多少。 - -本题则是装满有几种方法。其实这就是一个组合问题了。 - -* 确定dp数组以及下标的含义 - -dp[j] 表示:填满j(包括j)这么大容积的包,有dp[i]种方法 - -* 确定递推公式 - -有哪些来源可以推出dp[j]呢? - -不考虑nums[i]的情况下,填满容量为j - nums[i]的背包,有dp[j - nums[i]]中方法。 - -那么如果考虑nums[i]呢,dp[j] = dp[j] + dp[j - nums[i]]; - -公式右面的dp[j]:填满容量为j的背包(没有考虑nums[i])有dp[j]种方法, - -公式右面的dp[j - nums[i]]:填满容量为j - nums[i]的背包有dp[j - nums[i]]种方法 - -那么只要搞到nums[i]的话,就应该dp[j](考虑nums[i])= dp[j](没考虑nums[i]) + dp[j - nums[i]] - - -举一个例子,nums[i] = 2: dp[5] = dp[5] + dp[3],公式右边的dp[5]没考虑这个2,就有dp[5]种方法。 - -填满背包容量为3的话,有dp[3]种方法。 - -那么只需要搞到一个2(nums[i]),有dp[3]方法可以凑齐容量为3的背包,相应的就有多少种方法可以凑齐容量为5的背包。 - -所以 dp[5](考虑2) = dp[5](没考虑2) + dp[3]。 - -所以求组合类问题的公式,都是类似这种: - -``` -dp[j] += dp[j - num[i]] -``` - -**这个公式在后面在讲解背包解决排列组合问题的时候还会用到!** - -* dp数组如何初始化 - -从递归公式可以看出,在初始化的时候dp[0] 一定要初始化为1,因为dp[0]是在公式中一切递推结果的起源,如果dp[0]是0的话,递归结果将都是0。 - -dp[0] = 1,理论上也很好解释,装满容量为0的背包,有1中方法,就是装0件物品。 - -dp[j]其他下标对应的数值应该初始化为0,从递归公式也可以看出,dp[j]要保证是0的初始值,才能正确的由dp[j - nums[i]]推导出来。 - - -* 确定遍历顺序 - -对于01背包问题一维dp的遍历,nums放在外循环,target在内循环,且内循环倒序。 - -C++代码如下: - -``` -class Solution { -public: - int findTargetSumWays(vector& nums, int S) { - int sum = 0; - for (int i = 0; i < nums.size(); i++) sum += nums[i]; - if (S > sum) return 0; // 此时没有方案 - if ((S + sum) % 2 == 1) return 0; // 此时没有方案,两个int相加的时候要各位小心数值溢出的问题 - - int bagSize = (S + sum) / 2; - int dp[1001] = {1}; // 注意这个语法是第一个元素为1,其他都是0 - for (int i = 0; i < nums.size(); i++) { - for (int j = bagSize; j >= nums[i]; j--) { - dp[j] += dp[j - nums[i]]; - } - } - return dp[bagSize]; - } -}; -``` -* 时间复杂度O(n * m),n为正数个数,m为背包容量 -* 空间复杂度:O(n),也可以说是O(1),因为每次申请的辅助数组的大小是一个常数 - -dp数组中的数值变化:(从[0 - 4]) - -``` -1 1 0 0 0 -1 2 1 0 0 -1 3 3 1 0 -1 4 6 4 1 -1 5 10 10 5 -``` - -# 总结 - -此时 大家应该不仅想起,我们之前讲过的[回溯算法:39. 组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)是不是应该也可以用dp来做啊? - -是的,如果仅仅是求个数的话,就可以用dp,但[回溯算法:39. 组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)要求的是把所有组合列出来,还是要使用回溯法爆搜的。 - - diff --git a/problems/0496.下一个更大元素I.md b/problems/0496.下一个更大元素I.md deleted file mode 100644 index 28ae9de1..00000000 --- a/problems/0496.下一个更大元素I.md +++ /dev/null @@ -1,35 +0,0 @@ -## 链接 -https://leetcode-cn.com/problems/next-greater-element-i/ - -## 思路 - -两个数组逻辑还是有点绕 -最好还是把所有情况列出来 - -``` -class Solution { -public: - vector nextGreaterElement(vector& nums1, vector& nums2) { - stack st; - vector result(nums1.size(), -1); - if (nums1.size() == 0) return result; - - unordered_map umap; // key:下表元素,value:下表 - for (int i = 0; i < nums1.size(); i++) { - umap[nums1[i]] = i; - } - st.push(0); - for (int i = 1; i < nums2.size(); i++) { - while (!st.empty() && nums2[i] > nums2[st.top()]) { - if (umap.count(nums2[st.top()]) > 0) { // 看map里是否存在这个元素 - int index = umap[nums2[st.top()]]; // 根据map找到nums2[st.top()] 在 nums1中的下表 - result[index] = nums2[i]; - } - st.pop(); - } - st.push(i); - } - return result; - } -}; -``` diff --git a/problems/0501.二叉搜索树中的众数.md b/problems/0501.二叉搜索树中的众数.md deleted file mode 100644 index 5e727433..00000000 --- a/problems/0501.二叉搜索树中的众数.md +++ /dev/null @@ -1,336 +0,0 @@ -## 题目地址 - -https://leetcode-cn.com/problems/find-mode-in-binary-search-tree/solution/ - -> 二叉树上应该怎么求,二叉搜索树上有应该怎么求 - -# 501.二叉搜索树中的众数 - -给定一个有相同值的二叉搜索树(BST),找出 BST 中的所有众数(出现频率最高的元素)。 - -假定 BST 有如下定义: - -* 结点左子树中所含结点的值小于等于当前结点的值 -* 结点右子树中所含结点的值大于等于当前结点的值 -* 左子树和右子树都是二叉搜索树 - -例如: - -给定 BST [1,null,2,2], - -![501. 二叉搜索树中的众数](https://img-blog.csdnimg.cn/20201014221532206.png) - -返回[2]. - -提示:如果众数超过1个,不需考虑输出顺序 - -进阶:你可以不使用额外的空间吗?(假设由递归产生的隐式调用栈的开销不被计算在内) - -# 思路 - -这道题目呢,递归法我从两个维度来讲。 - -首先如果不是二叉搜索树的话,应该怎么解题,是二叉搜索树,又应该如何解题,两种方式做一个比较,可以加深大家对二叉树的理解。 - -## 递归法 - -### 如果不是二叉搜索树 - -如果不是二叉搜索树,最直观的方法一定是把这个树都遍历了,用map统计频率,把频率排个序,最后取前面高频的元素的集合。 - -具体步骤如下: - -1. 这个树都遍历了,用map统计频率 - -至于用前中后序那种遍历也不重要,因为就是要全遍历一遍,怎么个遍历法都行,层序遍历都没毛病! - -这里采用前序遍历,代码如下: - -``` -// map key:元素,value:出现频率 -void searchBST(TreeNode* cur, unordered_map& map) { // 前序遍历 - if (cur == NULL) return ; - map[cur->val]++; // 统计元素频率 - searchBST(cur->left, map); - searchBST(cur->right, map); - return ; -} -``` - -2. 把统计的出来的出现频率(即map中的value)排个序 - -有的同学可能可以想直接对map中的value排序,还真做不到,C++中如果使用std::map或者std::multimap可以对key排序,但不能对value排序。 - -所以要把map转化数组即vector,再进行排序,当然vector里面放的也是`pair`类型的数据,第一个int为元素,第二个int为出现频率。 - -代码如下: - -``` -bool static cmp (const pair& a, const pair& b) { - return a.second > b.second; // 按照频率从大到小排序 -} - -vector> vec(map.begin(), map.end()); -sort(vec.begin(), vec.end(), cmp); // 给频率排个序 -``` - -3. 取前面高频的元素 - -此时数组vector中已经是存放着按照频率排好序的pair,那么把前面高频的元素取出来就可以了。 - -代码如下: - -``` -result.push_back(vec[0].first); -for (int i = 1; i < vec.size(); i++) { - // 取最高的放到result数组中 - if (vec[i].second == vec[0].second) result.push_back(vec[i].first); - else break; -} -return result; -``` - - -整体C++代码如下: - -``` -class Solution { -private: - -void searchBST(TreeNode* cur, unordered_map& map) { // 前序遍历 - if (cur == NULL) return ; - map[cur->val]++; // 统计元素频率 - searchBST(cur->left, map); - searchBST(cur->right, map); - return ; -} -bool static cmp (const pair& a, const pair& b) { - return a.second > b.second; -} -public: - vector findMode(TreeNode* root) { - unordered_map map; // key:元素,value:出现频率 - vector result; - if (root == NULL) return result; - searchBST(root, map); - vector> vec(map.begin(), map.end()); - sort(vec.begin(), vec.end(), cmp); // 给频率排个序 - result.push_back(vec[0].first); - for (int i = 1; i < vec.size(); i++) { - // 取最高的放到result数组中 - if (vec[i].second == vec[0].second) result.push_back(vec[i].first); - else break; - } - return result; - } -}; -``` - -**所以如果本题没有说是二叉搜索树的话,那么就按照上面的思路写!** - -### 是二叉搜索树 - -**既然是搜索树,它中序遍历就是有序的**。 - -如图: - - - -中序遍历代码如下: - -``` -void searchBST(TreeNode* cur) { - if (cur == NULL) return ; - searchBST(cur->left); // 左 - (处理节点) // 中 - searchBST(cur->right); // 右 - return ; -} -``` - -遍历有序数组的元素出现频率,从头遍历,那么一定是相邻两个元素作比较,然后就把出现频率最高的元素输出就可以了。 - -关键是在有序数组上的话,好搞,在树上怎么搞呢? - -这就考察对树的操作了。 - -在[二叉树:搜索树的最小绝对差](https://mp.weixin.qq.com/s/Hwzml6698uP3qQCC1ctUQQ)中我们就使用了pre指针和cur指针的技巧,这次又用上了。 - -弄一个指针指向前一个节点,这样每次cur(当前节点)才能和pre(前一个节点)作比较。 - -而且初始化的时候pre = NULL,这样当pre为NULL时候,我们就知道这是比较的第一个元素。 - -代码如下: - -``` -if (pre == NULL) { // 第一个节点 - count = 1; // 频率为1 -} else if (pre->val == cur->val) { // 与前一个节点数值相同 - count++; -} else { // 与前一个节点数值不同 - count = 1; -} -pre = cur; // 更新上一个节点 -``` - -此时又有问题了,因为要求最大频率的元素集合(注意是集合,不是一个元素,可以有多个众数),如果是数组上大家一般怎么办? - -应该是先遍历一遍数组,找出最大频率(maxCount),然后再重新遍历一遍数组把出现频率为maxCount的元素放进集合。(因为众数有多个) - -这种方式遍历了两遍数组。 - -那么我们遍历两遍二叉搜索树,把众数集合算出来也是可以的。 - -但这里其实只需要遍历一次就可以找到所有的众数。 - -那么如何只遍历一遍呢? - -如果 频率count 等于 maxCount(最大频率),当然要把这个元素加入到结果集中(以下代码为result数组),代码如下: - -``` -if (count == maxCount) { // 如果和最大值相同,放进result中 - result.push_back(cur->val); -} -``` - -是不是感觉这里有问题,result怎么能轻易就把元素放进去了呢,万一,这个maxCount此时还不是真正最大频率呢。 - -所以下面要做如下操作: - -频率count 大于 maxCount的时候,不仅要更新maxCount,而且要清空结果集(以下代码为result数组),因为结果集之前的元素都失效了。 - -``` -if (count > maxCount) { // 如果计数大于最大值 - maxCount = count; // 更新最大频率 - result.clear(); // 很关键的一步,不要忘记清空result,之前result里的元素都失效了 - result.push_back(cur->val); -} -``` - -关键代码都讲完了,完整代码如下:(**只需要遍历一遍二叉搜索树,就求出了众数的集合**) - - -``` -class Solution { -private: - int maxCount; // 最大频率 - int count; // 统计频率 - TreeNode* pre; - vector result; - void searchBST(TreeNode* cur) { - if (cur == NULL) return ; - - searchBST(cur->left); // 左 - // 中 - if (pre == NULL) { // 第一个节点 - count = 1; - } else if (pre->val == cur->val) { // 与前一个节点数值相同 - count++; - } else { // 与前一个节点数值不同 - count = 1; - } - pre = cur; // 更新上一个节点 - - if (count == maxCount) { // 如果和最大值相同,放进result中 - result.push_back(cur->val); - } - - if (count > maxCount) { // 如果计数大于最大值频率 - maxCount = count; // 更新最大频率 - result.clear(); // 很关键的一步,不要忘记清空result,之前result里的元素都失效了 - result.push_back(cur->val); - } - - searchBST(cur->right); // 右 - return ; - } - -public: - vector findMode(TreeNode* root) { - count = 0; - maxCount = 0; - TreeNode* pre = NULL; // 记录前一个节点 - result.clear(); - - searchBST(root); - return result; - } -}; -``` - - -## 迭代法 - -只要把中序遍历转成迭代,中间节点的处理逻辑完全一样。 - -二叉树前中后序转迭代,传送门: - -* [二叉树:前中后序迭代法](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg) -* [二叉树:前中后序统一风格的迭代方式](https://mp.weixin.qq.com/s/WKg0Ty1_3SZkztpHubZPRg) - -下面我给出其中的一种中序遍历的迭代法,其中间处理逻辑一点都没有变(我从递归法直接粘过来的代码,连注释都没改,哈哈) - -代码如下: - -``` -class Solution { -public: - vector findMode(TreeNode* root) { - stack st; - TreeNode* cur = root; - TreeNode* pre = NULL; - int maxCount = 0; // 最大频率 - int count = 0; // 统计频率 - vector result; - while (cur != NULL || !st.empty()) { - if (cur != NULL) { // 指针来访问节点,访问到最底层 - st.push(cur); // 将访问的节点放进栈 - cur = cur->left; // 左 - } else { - cur = st.top(); - st.pop(); // 中 - if (pre == NULL) { // 第一个节点 - count = 1; - } else if (pre->val == cur->val) { // 与前一个节点数值相同 - count++; - } else { // 与前一个节点数值不同 - count = 1; - } - if (count == maxCount) { // 如果和最大值相同,放进result中 - result.push_back(cur->val); - } - - if (count > maxCount) { // 如果计数大于最大值频率 - maxCount = count; // 更新最大频率 - result.clear(); // 很关键的一步,不要忘记清空result,之前result里的元素都失效了 - result.push_back(cur->val); - } - pre = cur; - cur = cur->right; // 右 - } - } - return result; - } -}; -``` - -# 总结 - -本题在递归法中,我给出了如果是普通二叉树,应该怎么求众数。 - -知道了普通二叉树的做法时候,我再进一步给出二叉搜索树又应该怎么求众数,这样鲜明的对比,相信会对二叉树又有更深层次的理解了。 - -在递归遍历二叉搜索树的过程中,我还介绍了一个统计最高出现频率元素集合的技巧, 要不然就要遍历两次二叉搜索树才能把这个最高出现频率元素的集合求出来。 - - -**为什么没有这个技巧一定要遍历两次呢? 因为要求的是集合,会有多个众数,如果规定只有一个众数,那么就遍历一次稳稳的了。** - -最后我依然给出对应的迭代法,其实就是迭代法中序遍历的模板加上递归法中中间节点的处理逻辑,分分钟就可以写出来,中间逻辑的代码我都是从递归法中直接粘过来的。 - -**求二叉搜索树中的众数其实是一道简单题,但大家可以发现我写了这么一大篇幅的文章来讲解,主要是为了尽量从各个角度对本题进剖析,帮助大家更快更深入理解二叉树**。 - -**就酱,如果学到了的话,就转发给身边需要的同学吧,可能他们也需要!** - - - -**需要强调的是 leetcode上的耗时统计是非常不准确的,看个大概就行,一样的代码耗时可以差百分之50以上**,所以leetcode的耗时统计别太当回事,知道理论上的效率优劣就行了。 diff --git a/problems/0503.下一个更大元素II.md b/problems/0503.下一个更大元素II.md deleted file mode 100644 index fed5dd56..00000000 --- a/problems/0503.下一个更大元素II.md +++ /dev/null @@ -1,62 +0,0 @@ - -## 链接 -https://leetcode-cn.com/problems/next-greater-element-ii/ - -## 思路 - -可以把两个数组拼接在一起 - -代码性能不行啊,得找找优化的方式 - -``` -class Solution { -public: - vector nextGreaterElements(vector& nums) { - vector nums1(nums.begin(), nums.end()); - nums.insert(nums.end(), nums1.begin(), nums1.end()); - vector result(nums.size(), -1); - if (nums.size() == 0) return result; - stack st; - st.push(0); - for (int i = 0; i < nums.size(); i++) { - while (!st.empty() && nums[i] > nums[st.top()]) { - result[st.top()] = nums[i]; - st.pop(); - } - st.push(i); - } - result.resize(nums.size() / 2); - return result; - } -}; -``` - - -这样好像能高一些 -``` -class Solution { -public: - vector nextGreaterElements(vector& nums) { - vector nums1(nums.begin(), nums.end()); - nums.resize(nums.size() * 2); - int h = nums.size() / 2; - for (int i = 0; i < nums.size() / 2; i++) { - nums[h + i] = nums[i]; - } - //nums.insert(nums.end(), nums1.begin(), nums1.end()); - vector result(nums.size(), -1); - if (nums.size() == 0) return result; - stack st; - st.push(0); - for (int i = 0; i < nums.size(); i++) { - while (!st.empty() && nums[i] > nums[st.top()]) { - result[st.top()] = nums[i]; - st.pop(); - } - st.push(i); - } - result.resize(nums.size() / 2); - return result; - } -}; -``` diff --git a/problems/0509.斐波那契数.md b/problems/0509.斐波那契数.md deleted file mode 100644 index e8f19546..00000000 --- a/problems/0509.斐波那契数.md +++ /dev/null @@ -1,24 +0,0 @@ - -用简单题来把动态规划的解题思路练一遍。 - -题目已经把动态规划最难的一步给我们了:状态转移方程 dp[i] = dp[i-1] +dp[i-2]; - -dp[i]含义:斐波那契数列中第i个数值。 - -``` -class Solution { -public: - int fib(int N) { - if (N <= 1) return N; - vector dp(N + 1); - dp[0] = 0; - dp[1] = 1; - for (int i = 2; i <= N; i++) { - dp[i] = dp[i - 1] + dp[i - 2]; - } - return dp[N]; - } -}; -``` - -拓展可以顺便把 70. 爬楼梯 这个做了 diff --git a/problems/0513.找树左下角的值.md b/problems/0513.找树左下角的值.md deleted file mode 100644 index ec4d041c..00000000 --- a/problems/0513.找树左下角的值.md +++ /dev/null @@ -1,208 +0,0 @@ - -> 我的左下角的数值是多少? - -# 513.找树左下角的值 - -给定一个二叉树,在树的最后一行找到最左边的值。 - -示例 1: - - - -示例 2: - - - -# 思路 - -本地要找出树的最后一行找到最左边的值。此时大家应该想起用层序遍历是非常简单的了,反而用递归的话会比较难一点。 - -我们依然还是先介绍递归法。 - -## 递归 - -咋眼一看,这道题目用递归的话就就一直向左遍历,最后一个就是答案呗? - -没有这么简单,一直向左遍历到最后一个,它未必是最后一行啊。 - -我们来分析一下题目:在树的**最后一行**找到**最左边的值**。 - -首先要是最后一行,然后是最左边的值。 - -如果使用递归法,如何判断是最后一行呢,其实就是深度最大的叶子节点一定是最后一行。 - -如果对二叉树深度和高度还有点疑惑的话,请看:[二叉树:我平衡么?](https://mp.weixin.qq.com/s/isUS-0HDYknmC0Rr4R8mww)。 - -所以要找深度最大的叶子节点。 - -那么如果找最左边的呢?可以使用前序遍历,这样才先优先左边搜索,然后记录深度最大的叶子节点,此时就是树的最后一行最左边的值。 - -递归三部曲: - -1. 确定递归函数的参数和返回值 - -参数必须有要遍历的树的根节点,还有就是一个int型的变量用来记录最长深度。 这里就不需要返回值了,所以递归函数的返回类型为void。 - -本题还需要类里的两个全局变量,maxLen用来记录最大深度,maxleftValue记录最大深度最左节点的数值。 - -代码如下: - -``` -int maxLen = INT_MIN; // 全局变量 记录最大深度 -int maxleftValue; // 全局变量 最大深度最左节点的数值 -void traversal(TreeNode* root, int leftLen) -``` - -有的同学可能疑惑,为啥不能递归函数的返回值返回最长深度呢? - -其实很多同学都对递归函数什么时候要有返回值,什么时候不能有返回值很迷茫。 - -**如果需要遍历整颗树,递归函数就不能有返回值。如果需要遍历某一条固定路线,递归函数就一定要有返回值!** - -初学者可能对这个结论不太理解,别急,后面我会安排一道题目专门讲递归函数的返回值问题。这里大家暂时先了解一下。 - -本题我们是要遍历整个树找到最深的叶子节点,需要遍历整颗树,所以递归函数没有返回值。 - -2. 确定终止条件 - -当遇到叶子节点的时候,就需要统计一下最大的深度了,所以需要遇到叶子节点来更新最大深度。 - -代码如下: - -``` -if (root->left == NULL && root->right == NULL) { - if (leftLen > maxLen) { - maxLen = leftLen; // 更新最大深度 - maxleftValue = root->val; // 最大深度最左面的数值 - } - return; -} -``` - -3. 确定单层递归的逻辑 - -在找最大深度的时候,递归的过程中依然要使用回溯,代码如下: - -``` - // 中 -if (root->left) { // 左 - leftLen++; // 深度加一 - traversal(root->left, leftLen); - leftLen--; // 回溯,深度减一 -} -if (root->right) { // 右 - leftLen++; // 深度加一 - traversal(root->right, leftLen); - leftLen--; // 回溯,深度减一 -} -return; -``` - -完整代码如下: - -``` -class Solution { -public: - int maxLen = INT_MIN; - int maxleftValue; - void traversal(TreeNode* root, int leftLen) { - if (root->left == NULL && root->right == NULL) { - if (leftLen > maxLen) { - maxLen = leftLen; - maxleftValue = root->val; - } - return; - } - if (root->left) { - leftLen++; - traversal(root->left, leftLen); - leftLen--; // 回溯 - } - if (root->right) { - leftLen++; - traversal(root->right, leftLen); - leftLen--; // 回溯 - } - return; - } - int findBottomLeftValue(TreeNode* root) { - traversal(root, 0); - return maxleftValue; - } -}; -``` - -当然回溯的地方可以精简,精简代码如下: - -``` -class Solution { -public: - int maxLen = INT_MIN; - int maxleftValue; - void traversal(TreeNode* root, int leftLen) { - if (root->left == NULL && root->right == NULL) { - if (leftLen > maxLen) { - maxLen = leftLen; - maxleftValue = root->val; - } - return; - } - if (root->left) { - traversal(root->left, leftLen + 1); // 隐藏着回溯 - } - if (root->right) { - traversal(root->right, leftLen + 1); // 隐藏着回溯 - } - return; - } - int findBottomLeftValue(TreeNode* root) { - traversal(root, 0); - return maxleftValue; - } -}; -``` - -如果对回溯部分精简的代码 不理解的话,可以看这篇[二叉树:找我的所有路径?](https://mp.weixin.qq.com/s/Osw4LQD2xVUnCJ-9jrYxJA)和[二叉树:以为使用了递归,其实还隐藏着回溯](https://mp.weixin.qq.com/s/ivLkHzWdhjQQD1rQWe6zWA) 。这两篇文章详细分析了回溯隐藏在了哪里。 - - -## 迭代法 - -本题使用层序遍历再合适不过了,比递归要好理解的多! - -只需要记录最后一行第一个节点的数值就可以了。 - -如果对层序遍历不了解,看这篇[二叉树:层序遍历登场!](https://mp.weixin.qq.com/s/Gb3BjakIKGNpup2jYtTzog),这篇里也给出了层序遍历的模板,稍作修改就一过刷了这道题了。 - -代码如下: - -``` -class Solution { -public: - int findBottomLeftValue(TreeNode* root) { - queue que; - if (root != NULL) que.push(root); - int result = 0; - while (!que.empty()) { - int size = que.size(); - for (int i = 0; i < size; i++) { - TreeNode* node = que.front(); - que.pop(); - if (i == 0) result = node->val; // 记录最后一行第一个元素 - if (node->left) que.push(node->left); - if (node->right) que.push(node->right); - } - } - return result; - } -}; -``` - -# 总结 - -本题涉及如下几点: - -* 递归求深度的写法,我们在[二叉树:我平衡么?](https://mp.weixin.qq.com/s/isUS-0HDYknmC0Rr4R8mww)中详细的分析了深度应该怎么求,高度应该怎么求。 -* 递归中其实隐藏了回溯,在[二叉树:以为使用了递归,其实还隐藏着回溯](https://mp.weixin.qq.com/s/ivLkHzWdhjQQD1rQWe6zWA)中讲解了究竟哪里使用了回溯,哪里隐藏了回溯。 -* 层次遍历,在[二叉树:层序遍历登场!](https://mp.weixin.qq.com/s/Gb3BjakIKGNpup2jYtTzog)深度讲解了二叉树层次遍历。 - -所以本题涉及到的点,我们之前都讲解过,这些知识点需要同学们灵活运用,这样就举一反三了。 diff --git a/problems/0514.自由之路.md b/problems/0514.自由之路.md deleted file mode 100644 index ede77835..00000000 --- a/problems/0514.自由之路.md +++ /dev/null @@ -1,42 +0,0 @@ - -//dp[i][j],key的0~i位字符拼写后,ring的第j位对齐12:00方向,需要的最小步数 -//前提:key[i] = ring[j],若不满足,dp[i][j] = INT_MAX - -这道题目我服! 没做出来 - -https://blog.csdn.net/qq_41855420/article/details/89058979 - -``` -class Solution { -public: - int findRotateSteps(string ring, string key) { - //int dp[101][101] = {0}; - int n = ring.size(); - vector> dp(key.size() + 1, vector(ring.size(), 0)); - for (int i = key.size() - 1; i >= 0; i--) { - for (int j = 0; j < ring.size(); j++) { - dp[i][j] = INT_MAX; - for (int k = 0; k < ring.size(); k++) { - if (ring[k] == key[i]) { - int diff = abs(j - k); - int step = min(diff, n - diff); - dp[i][j] = min(dp[i][j], step + dp[i + 1][k]); - } - } - } - } - for (int i = 0; i < dp.size(); i++) { - for (int j = 0; j < dp[0].size(); j++) { - cout << dp[i][j] << " "; - } - cout << endl; - } - return dp[0][0] + key.size(); - } -}; -``` - -2 3 4 5 5 4 3 -2 1 0 0 1 2 3 - - diff --git a/problems/0515.在每个树行中找最大值.md b/problems/0515.在每个树行中找最大值.md deleted file mode 100644 index e1c4863a..00000000 --- a/problems/0515.在每个树行中找最大值.md +++ /dev/null @@ -1,6 +0,0 @@ - -## 题目地址 -https://leetcode-cn.com/problems/find-largest-value-in-each-tree-row/ -## 思路 -详见: - * [0102.二叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0102.二叉树的层序遍历.md) diff --git a/problems/0518.零钱兑换II.md b/problems/0518.零钱兑换II.md deleted file mode 100644 index 41019f82..00000000 --- a/problems/0518.零钱兑换II.md +++ /dev/null @@ -1,46 +0,0 @@ - -# 思路 - -这是一道典型的背包问题,一看到钱币数量不限,就知道这是一个完全背包 - -那么用动规四步曲来进行分析: - -* 确定dp数组以及下标的含义 - -dp[j]:凑成总金额j的货币组合数为dp[j] - -* 确定递推公式 - -dp[j] (考虑coins[i]的组合总和) 就是所有的dp[j - coins[i]](不考虑coins[i])相加。 - -所以递推公式:dp[j] += dp[j - coins[i]]; - -* dp数组如何初始化 - -首先dp[0]一定要为1,dp[0]=1是 递归公式的基础。 - -下标非0的dp[j]初始化为0,这样累计加dp[j - coins[i]]的时候才不会影响真正的dp[j] - -* 确定遍历顺序 - -本题是完全背包,而且求的是组合,不涉及钱币的顺序 - -所以 nums放在外循环,target在内循环,内循环正序遍历。 - -C++代码如下: - -``` -class Solution { -public: - int change(int amount, vector& coins) { - vector dp(amount + 1, 0); - dp[0] = 1; - for (int i = 0; i < coins.size(); i++) { - for (int j = coins[i]; j <= amount; j++) { - dp[j] += dp[j - coins[i]]; - } - } - return dp[amount]; - } -}; -``` diff --git a/problems/0530.二叉搜索树的最小绝对差.md b/problems/0530.二叉搜索树的最小绝对差.md deleted file mode 100644 index 6a7f8e8e..00000000 --- a/problems/0530.二叉搜索树的最小绝对差.md +++ /dev/null @@ -1,141 +0,0 @@ - -## 题目地址 - -https://leetcode-cn.com/problems/minimum-absolute-difference-in-bst/ - -> 利用二叉搜索树的特性搞起! - -# 530.二叉搜索树的最小绝对差 - -给你一棵所有节点为非负值的二叉搜索树,请你计算树中任意两节点的差的绝对值的最小值。 - -示例: - -![530二叉搜索树的最小绝对差](https://img-blog.csdnimg.cn/20201014223400123.png) - -提示:树中至少有 2 个节点。 - -# 思路 - -题目中要求在二叉搜索树上任意两节点的差的绝对值的最小值。 - -**注意是二叉搜索树**,二叉搜索树可是有序的。 - -遇到在二叉搜索树上求什么最值啊,差值之类的,就把它想成在一个有序数组上求最值,求差值,这样就简单多了。 - -## 递归 - -那么二叉搜索树采用中序遍历,其实就是一个有序数组。 - -**在一个有序数组上求两个数最小差值,这是不是就是一道送分题了。** - -最直观的想法,就是把二叉搜索树转换成有序数组,然后遍历一遍数组,就统计出来最小差值了。 - -代码如下: - -``` -class Solution { -private: -vector vec; -void traversal(TreeNode* root) { - if (root == NULL) return; - traversal(root->left); - vec.push_back(root->val); // 将二叉搜索树转换为有序数组 - traversal(root->right); -} -public: - int getMinimumDifference(TreeNode* root) { - vec.clear(); - traversal(root); - if (vec.size() < 2) return 0; - int result = INT_MAX; - for (int i = 1; i < vec.size(); i++) { // 统计有序数组的最小差值 - result = min(result, vec[i] - vec[i-1]); - } - return result; - } -}; -``` - -以上代码是把二叉搜索树转化为有序数组了,其实在二叉搜素树中序遍历的过程中,我们就可以直接计算了。 - -需要用一个pre节点记录一下cur节点的前一个节点。 - -如图: - - - -一些同学不知道在递归中如何记录前一个节点的指针,其实实现起来是很简单的,大家只要看过一次,写过一次,就掌握了。 - -代码如下: - -``` -class Solution { -private: -int result = INT_MAX; -TreeNode* pre; -void traversal(TreeNode* cur) { - if (cur == NULL) return; - traversal(cur->left); // 左 - if (pre != NULL){ // 中 - result = min(result, cur->val - pre->val); - } - pre = cur; // 记录前一个 - traversal(cur->right); // 右 -} -public: - int getMinimumDifference(TreeNode* root) { - traversal(root); - return result; - } -}; -``` - -是不是看上去也并不复杂! - -## 迭代 - -看过这两篇[二叉树:听说递归能做的,栈也能做!](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg),[二叉树:前中后序迭代方式的写法就不能统一一下么?](https://mp.weixin.qq.com/s/WKg0Ty1_3SZkztpHubZPRg)文章之后,不难写出两种中序遍历的迭代法。 - -下面我给出其中的一种中序遍历的迭代法,代码如下: - -``` -class Solution { -public: - int getMinimumDifference(TreeNode* root) { - stack st; - TreeNode* cur = root; - TreeNode* pre = NULL; - int result = INT_MAX; - while (cur != NULL || !st.empty()) { - if (cur != NULL) { // 指针来访问节点,访问到最底层 - st.push(cur); // 将访问的节点放进栈 - cur = cur->left; // 左 - } else { - cur = st.top(); - st.pop(); - if (pre != NULL) { // 中 - result = min(result, cur->val - pre->val); - } - pre = cur; - cur = cur->right; // 右 - } - } - return result; - } -}; -``` - -# 总结 - -**遇到在二叉搜索树上求什么最值,求差值之类的,都要思考一下二叉搜索树可是有序的,要利用好这一特点。** - -同时要学会在递归遍历的过程中如何记录前后两个指针,这也是一个小技巧,学会了还是很受用的。 - -后面我将继续介绍一系列利用二叉搜索树特性的题目。 - -**就酱,感觉学到了,就转发给身边需要的同学吧** - - - - diff --git a/problems/0538.把二叉搜索树转换为累加树.md b/problems/0538.把二叉搜索树转换为累加树.md deleted file mode 100644 index 84dada49..00000000 --- a/problems/0538.把二叉搜索树转换为累加树.md +++ /dev/null @@ -1,173 +0,0 @@ - -## 题目地址 - -https://leetcode-cn.com/problems/convert-bst-to-greater-tree/ - -> 祝大家1024节日快乐!! - -今天应该是一个程序猿普天同庆的日子,所以今天的题目比较简单,只要认真把前面每天的文章都看了,今天的题目就是分分钟的事了,大家可以愉快过节! - -# 538.把二叉搜索树转换为累加树 - -题目链接:https://leetcode-cn.com/problems/convert-bst-to-greater-tree/ - -给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。 - -提醒一下,二叉搜索树满足下列约束条件: - -节点的左子树仅包含键 小于 节点键的节点。 -节点的右子树仅包含键 大于 节点键的节点。 -左右子树也必须是二叉搜索树。 - -示例 1: - -![538.把二叉搜索树转换为累加树](https://img-blog.csdnimg.cn/20201023160751832.png) - -输入:[4,1,6,0,2,5,7,null,null,null,3,null,null,null,8] -输出:[30,36,21,36,35,26,15,null,null,null,33,null,null,null,8] - -示例 2: -输入:root = [0,null,1] -输出:[1,null,1] - -示例 3: -输入:root = [1,0,2] -输出:[3,3,2] - -示例 4: -输入:root = [3,2,4,1] -输出:[7,9,4,10] - -提示: - -* 树中的节点数介于 0 和 104 之间。 -* 每个节点的值介于 -104 和 104 之间。 -* 树中的所有值 互不相同 。 -* 给定的树为二叉搜索树。 - -# 思路 - -一看到累加树,相信很多小伙伴都会疑惑:如何累加?遇到一个节点,然后在遍历其他节点累加?怎么一想这么麻烦呢。 - -然后再发现这是一颗二叉搜索树,二叉搜索树啊,这是有序的啊。 - -那么有序的元素如果求累加呢? - -**其实这就是一棵树,大家可能看起来有点别扭,换一个角度来看,这就是一个有序数组[2, 5, 13],求从后到前的累加数组,也就是[20, 18, 13],是不是感觉这就简单了。** - -为什么变成数组就是感觉简单了呢? - -因为数组大家都知道怎么遍历啊,从后向前,挨个累加就完事了,这换成了二叉搜索树,看起来就别扭了一些是不是。 - -那么知道如何遍历这个二叉树,也就迎刃而解了,**从树中可以看出累加的顺序是右中左,所以我们需要反中序遍历这个二叉树,然后顺序累加就可以了**。 - -## 递归 - -遍历顺序如图所示: - - - -本题依然需要一个pre指针记录当前遍历节点cur的前一个节点,这样才方便做累加。 - -pre指针的使用技巧,我们在[二叉树:搜索树的最小绝对差](https://mp.weixin.qq.com/s/Hwzml6698uP3qQCC1ctUQQ)和[二叉树:我的众数是多少?](https://mp.weixin.qq.com/s/KSAr6OVQIMC-uZ8MEAnGHg)都提到了,这是常用的操作手段。 - -* 递归函数参数以及返回值 - -这里很明确了,不需要递归函数的返回值做什么操作了,要遍历整棵树。 - -同时需要定义一个全局变量pre,用来保存cur节点的前一个节点的数值,定义为int型就可以了。 - -代码如下: - -``` -int pre; // 记录前一个节点的数值 -void traversal(TreeNode* cur) -``` - -* 确定终止条件 - -遇空就终止。 - -``` -if (cur == NULL) return; -``` - -* 确定单层递归的逻辑 - -注意**要右中左来遍历二叉树**, 中节点的处理逻辑就是让cur的数值加上前一个节点的数值。 - -代码如下: - -``` -traversal(cur->right); // 右 -cur->val += pre; // 中 -pre = cur->val; -traversal(cur->left); // 左 -``` - -递归法整体代码如下: - -``` -class Solution { -private: - int pre; // 记录前一个节点的数值 - void traversal(TreeNode* cur) { // 右中左遍历 - if (cur == NULL) return; - traversal(cur->right); - cur->val += pre; - pre = cur->val; - traversal(cur->left); - } -public: - TreeNode* convertBST(TreeNode* root) { - pre = 0; - traversal(root); - return root; - } -}; -``` - -## 迭代法 - -迭代法其实就是中序模板题了,在[二叉树:前中后序迭代法](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg)和[二叉树:前中后序统一方式迭代法](https://mp.weixin.qq.com/s/WKg0Ty1_3SZkztpHubZPRg)可以选一种自己习惯的写法。 - -这里我给出其中的一种,代码如下: - -``` -class Solution { -private: - int pre; // 记录前一个节点的数值 - void traversal(TreeNode* root) { - stack st; - TreeNode* cur = root; - while (cur != NULL || !st.empty()) { - if (cur != NULL) { - st.push(cur); - cur = cur->right; // 右 - } else { - cur = st.top(); // 中 - st.pop(); - cur->val += pre; - pre = cur->val; - cur = cur->left; // 左 - } - } - } -public: - TreeNode* convertBST(TreeNode* root) { - pre = 0; - traversal(root); - return root; - } -}; -``` - -# 总结 - -经历了前面各种二叉树增删改查的洗礼之后,这道题目应该比较简单了。 - -**好了,二叉树已经接近尾声了,接下来就是要对二叉树来一个大总结了**。 - -最后再次祝大家1024节日快乐,哈哈哈! - - diff --git a/problems/0541.反转字符串II.md b/problems/0541.反转字符串II.md deleted file mode 100644 index ca31a744..00000000 --- a/problems/0541.反转字符串II.md +++ /dev/null @@ -1,89 +0,0 @@ - -# 题目地址 - -https://leetcode-cn.com/problems/reverse-string-ii/ - -> 简单的反转还不够,我要花式反转 - -# 题目:541. 反转字符串II - -给定一个字符串 s 和一个整数 k,你需要对从字符串开头算起的每隔 2k 个字符的前 k 个字符进行反转。 - -如果剩余字符少于 k 个,则将剩余字符全部反转。 - -如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。 - -示例: - -输入: s = "abcdefg", k = 2 -输出: "bacdfeg" - -# 思路 - -这道题目其实也是模拟,实现题目中规定的反转规则就可以了。 - -一些同学可能为了处理逻辑:每隔2k个字符的前k的字符,写了一堆逻辑代码或者再搞一个计数器,来统计2k,再统计前k个字符。 - -其实在遍历字符串的过程中,只要让 i += (2 * k),i 每次移动 2 * k 就可以了,然后判断是否需要有反转的区间。 - -因为要找的也就是每2 * k 区间的起点,这样写,程序会高效很多。 - -**所以当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章。** - -性能如下: - - -那么这里具体反转的逻辑我们要不要使用库函数呢,其实用不用都可以,使用reverse来实现反转也没毛病,毕竟不是解题关键部分。 - -# C++代码 - -使用C++库函数reverse的版本如下: - -``` -class Solution { -public: - string reverseStr(string s, int k) { - for (int i = 0; i < s.size(); i += (2 * k)) { - // 1. 每隔 2k 个字符的前 k 个字符进行反转 - // 2. 剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符 - if (i + k <= s.size()) { - reverse(s.begin() + i, s.begin() + i + k ); - continue; - } - // 3. 剩余字符少于 k 个,则将剩余字符全部反转。 - reverse(s.begin() + i, s.begin() + s.size()); - } - return s; - } -}; -``` - -那么我们也可以实现自己的reverse函数,其实和题目[344. 反转字符串](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA)道理是一样的。 - -下面我实现的reverse函数区间是左闭右闭区间,代码如下: -``` -class Solution { -public: - void reverse(string& s, int start, int end) { - for (int i = start, j = end; i < j; i++, j--) { - swap(s[i], s[j]); - } - } - string reverseStr(string s, int k) { - for (int i = 0; i < s.size(); i += (2 * k)) { - // 1. 每隔 2k 个字符的前 k 个字符进行反转 - // 2. 剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符 - if (i + k <= s.size()) { - reverse(s, i, i + k - 1); - continue; - } - // 3. 剩余字符少于 k 个,则将剩余字符全部反转。 - reverse(s, i, s.size() - 1); - } - return s; - } -}; -``` - -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0559.N叉树的最大深度.md b/problems/0559.N叉树的最大深度.md deleted file mode 100644 index ad424230..00000000 --- a/problems/0559.N叉树的最大深度.md +++ /dev/null @@ -1,46 +0,0 @@ -## 地址 - -## 思路 - -## C++代码 - -### 递归法 - -``` -class Solution { -public: - int maxDepth(Node* root) { - if (root == 0) return 0; - int depth = 0; - for (int i = 0; i < root->children.size(); i++) { - depth = max (depth, maxDepth(root->children[i])); - } - return depth + 1; - } -}; -``` -### 迭代法 - -``` -class Solution { -public: - int maxDepth(Node* root) { - queue que; - if (root != NULL) que.push(root); - int depth = 0; - while (!que.empty()) { - int size = que.size(); - vector vec; - depth++; // 记录深度 - for (int i = 0; i < size; i++) { - Node* node = que.front(); - que.pop(); - for (int j = 0; j < node->children.size(); j++) { - if (node->children[j]) que.push(node->children[j]); - } - } - } - return depth; - } -}; -``` diff --git a/problems/0572.另一个树的子树.md b/problems/0572.另一个树的子树.md deleted file mode 100644 index 7b787736..00000000 --- a/problems/0572.另一个树的子树.md +++ /dev/null @@ -1,70 +0,0 @@ - - -判断二叉树是否对称,是否相等,是否对称,都是一个套路 - -## 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->left) && compare(left->right, right->right); - - } - bool isSubtree(TreeNode* s, TreeNode* t) { - if (s == NULL) return false; - if (compare(s, t)) return true; - if(isSubtree(s->left, t)) return true; - if (isSubtree(s->right, t)) return true; - return false; - } -}; -``` - - -``` -class Solution { -public: - bool isSameTree(TreeNode* p, TreeNode* q) { - if (p == NULL && q == NULL) return true; - if (p == NULL || q == NULL) return false; - queue que; - que.push(p); // - que.push(q); // - 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->left); // - que.push(leftNode->right); // - que.push(rightNode->right); // - } - return true; - } - - bool isSubtree(TreeNode* s, TreeNode* t) { - stack st; - vector result; - if (s == NULL) return false; - st.push(s); - while (!st.empty()) { - TreeNode* node = st.top(); // 中 - st.pop(); - if(isSameTree(node, t)) return true; - if (node->right) st.push(node->right); // 右(空节点不入栈) - if (node->left) st.push(node->left); // 左(空节点不入栈) - } - return false; - } -}; -``` diff --git a/problems/0575.分糖果.md b/problems/0575.分糖果.md deleted file mode 100644 index 831fc9b8..00000000 --- a/problems/0575.分糖果.md +++ /dev/null @@ -1,40 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/distribute-candies/ - -## 思路 - -糖果的种类是可妹妹先来,所以思路先求出一共有多少种类型的糖果,需要注意: 数组中数字的大小也就是糖果的种类取值范围在[负十万和 正十万之间], 依然可以定义一个数组,通过哈希法求出有多少类型的糖果,那么糖果种类可以是负数 怎么办呢,可以把 定一个 20万大小的数组 ,就可以把糖果的全部类型映射到数组的下表了。 - -通过哈希法,可以求出了糖果的类型数量,如果糖果种类大于糖果总数的一半了,返回 糖果数量的一半就好,因为妹妹已经得到种类最多的糖果了,否则,就是返回 糖果的种类。 - -## 代码 - -``` -class Solution { -public: - int distributeCandies(vector& candies) { - // 初始化一个record数组,因为糖果种类的数值在范围[-100,000, 100,000]内 - // 将这个范围的数值统一加上100000,可以映射到record数组的索引下表了 - // record数组大小必须大于等于200001,这样才能取到200000这个下表索引 - int record[200001] = {0}; - // 通过record来记录糖果的种类 - for (int i = 0; i < candies.size(); i++) { - record[candies[i] + 100000]++; - } - // 统计糖果种类的数量 - int count = 0; - for (int i = 0; i < 200001; i++) { - if (record[i] != 0) { - count ++; - } - } - int half = candies.size() / 2; - // 如果糖果种类大于糖果总数的一半了,return 糖果数量的一半 - // 否则,就是return 糖果的种类。 - return count > half ? half : count; - } -}; -``` - -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0589.N叉树的前序遍历.md b/problems/0589.N叉树的前序遍历.md deleted file mode 100644 index dfb7cf78..00000000 --- a/problems/0589.N叉树的前序遍历.md +++ /dev/null @@ -1,56 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/n-ary-tree-preorder-traversal/ - -## 思路 - - - -## 递归C++代码 - -``` -class Solution { -private: - vector result; - void traversal (Node* root) { - if (root == NULL) return; - result.push_back(root->val); - for (int i = 0; i < root->children.size(); i++) { - traversal(root->children[i]); - } - } - -public: - vector preorder(Node* root) { - result.clear(); - traversal(root); - return result; - } -}; -``` - -## 迭代法C++代码 - -``` -class Solution { - -public: - vector preorder(Node* root) { - vector result; - if (root == NULL) return result; - stack st; - st.push(root); - while (!st.empty()) { - Node* node = st.top(); - st.pop(); - result.push_back(node->val); - // 注意要倒叙,这样才能达到前序(中左右)的效果 - for (int i = node->children.size() - 1; i >= 0; i--) { - if (node->children[i] != NULL) { - st.push(node->children[i]); - } - } - } - return result; - } -}; -``` diff --git a/problems/0590.N叉树的后序遍历.md b/problems/0590.N叉树的后序遍历.md deleted file mode 100644 index 36c98fef..00000000 --- a/problems/0590.N叉树的后序遍历.md +++ /dev/null @@ -1,54 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/n-ary-tree-postorder-traversal/ -## 思路 - -## 递归C++代码 - -``` -class Solution { -private: - vector result; - void traversal (Node* root) { - if (root == NULL) return; - for (int i = 0; i < root->children.size(); i++) { // 子孩子 - traversal(root->children[i]); - } - result.push_back(root->val); // 中 - - } - -public: - vector postorder(Node* root) { - result.clear(); - traversal(root); - return result; - - } -}; -``` - -## 迭代法C++代码 - -``` -class Solution { -public: - vector postorder(Node* root) { - vector result; - if (root == NULL) return result; - stack st; - st.push(root); - while (!st.empty()) { - Node* node = st.top(); - st.pop(); - result.push_back(node->val); - for (int i = 0; i < node->children.size(); i++) { // 相对于前序遍历,这里反过来 - if (node->children[i] != NULL) { - st.push(node->children[i]); - } - } - } - reverse(result.begin(), result.end()); // 反转数组 - return result; - } -}; -``` diff --git a/problems/0617.合并二叉树.md b/problems/0617.合并二叉树.md deleted file mode 100644 index cfecb9a1..00000000 --- a/problems/0617.合并二叉树.md +++ /dev/null @@ -1,250 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/merge-two-binary-trees/ - -> 合并一下 - -# 617.合并二叉树 - -给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。 - -你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。 - -示例 1: - - - -注意: 合并必须从两个树的根节点开始。 - -# 思路 - -相信这道题目很多同学疑惑的点是如何同时遍历两个二叉树呢? - -其实和遍历一个树逻辑是一样的,只不过传入两个树的节点,同时操作。 - -## 递归 - -二叉树使用递归,就要想使用前中后哪种遍历方式? - -**本题使用哪种遍历都是可以的!** - -我们下面以前序遍历为例。 - -动画如下: - - - -那么我们来按照递归三部曲来解决: - -1. **确定递归函数的参数和返回值:** - -首先那么要合入两个二叉树,那么参数至少是要传入两个二叉树的根节点,返回值就是合并之后二叉树的根节点。 - -代码如下: - -``` -TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { -``` - -2. **确定终止条件:** - -因为是传入了两个树,那么就有两个树遍历的节点t1 和 t2,如果t1 == NULL 了,两个树合并就应该是 t2 了啊(如果t2也为NULL也无所谓,合并之后就是NULL)。 - -反过来如果t2 == NULL,那么两个数合并就是t1(如果t1也为NULL也无所谓,合并之后就是NULL)。 - -代码如下: - -``` -if (t1 == NULL) return t2; // 如果t1为空,合并之后就应该是t2 -if (t2 == NULL) return t1; // 如果t2为空,合并之后就应该是t1 -``` - - -3. **确定单层递归的逻辑:** - -单层递归的逻辑就比较好些了,这里我们用重复利用一下t1这个树,t1就是合并之后树的根节点(就是修改了原来树的结构)。 - -那么单层递归中,就要把两棵树的元素加到一起。 -``` -t1->val += t2->val; -``` - -接下来t1 的左子树是:合并 t1左子树 t2左子树之后的左子树。 - -t1 的右子树:是 合并 t1右子树 t2右子树之后的右子树。 - -最终t1就是合并之后的根节点。 - -代码如下: - -``` - t1->left = mergeTrees(t1->left, t2->left); - t1->right = mergeTrees(t1->right, t2->right); - return t1; -``` - -此时前序遍历,完整代码就写出来了,如下: - -``` -class Solution { -public: - TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { - if (t1 == NULL) return t2; // 如果t1为空,合并之后就应该是t2 - if (t2 == NULL) return t1; // 如果t2为空,合并之后就应该是t1 - // 修改了t1的数值和结构 - t1->val += t2->val; // 中 - t1->left = mergeTrees(t1->left, t2->left); // 左 - t1->right = mergeTrees(t1->right, t2->right); // 右 - return t1; - } -}; -``` - -那么中序遍历也是可以的,代码如下: - -``` -class Solution { -public: - TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { - if (t1 == NULL) return t2; // 如果t1为空,合并之后就应该是t2 - if (t2 == NULL) return t1; // 如果t2为空,合并之后就应该是t1 - // 修改了t1的数值和结构 - t1->left = mergeTrees(t1->left, t2->left); // 左 - t1->val += t2->val; // 中 - t1->right = mergeTrees(t1->right, t2->right); // 右 - return t1; - } -}; -``` - -后序遍历依然可以,代码如下: - -``` -class Solution { -public: - TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { - if (t1 == NULL) return t2; // 如果t1为空,合并之后就应该是t2 - if (t2 == NULL) return t1; // 如果t2为空,合并之后就应该是t1 - // 修改了t1的数值和结构 - t1->left = mergeTrees(t1->left, t2->left); // 左 - t1->right = mergeTrees(t1->right, t2->right); // 右 - t1->val += t2->val; // 中 - return t1; - } -}; -``` - -**但是前序遍历是最好理解的,我建议大家用前序遍历来做就OK。** - -如上的方法修改了t1的结构,当然也可以不修改t1和t2的结构,重新定一个树。 - -不修改输入树的结构,前序遍历,代码如下: - -``` -class Solution { -public: - TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { - if (t1 == NULL) return t2; - if (t2 == NULL) return t1; - // 重新定义新的节点,不修改原有两个树的结构 - TreeNode* root = new TreeNode(0); - root->val = t1->val + t2->val; - root->left = mergeTrees(t1->left, t2->left); - root->right = mergeTrees(t1->right, t2->right); - return root; - } -}; -``` - -## 迭代法 - -使用迭代法,如何同时处理两棵树呢? - -思路我们在[二叉树:我对称么?](https://mp.weixin.qq.com/s/Kgf0gjvlDlNDfKIH2b1Oxg)中的迭代法已经讲过一次了,求二叉树对称的时候就是把两个树的节点同时加入队列进行比较。 - -本题我们也使用队列,模拟的层序遍历,代码如下: - -``` -class Solution { -public: - TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { - if (t1 == NULL) return t2; - if (t2 == NULL) return t1; - queue que; - que.push(t1); - que.push(t2); - while(!que.empty()) { - TreeNode* node1 = que.front(); que.pop(); - TreeNode* node2 = que.front(); que.pop(); - // 此时两个节点一定不为空,val相加 - node1->val += node2->val; - - // 如果两棵树左节点都不为空,加入队列 - if (node1->left != NULL && node2->left != NULL) { - que.push(node1->left); - que.push(node2->left); - } - // 如果两棵树右节点都不为空,加入队列 - if (node1->right != NULL && node2->right != NULL) { - que.push(node1->right); - que.push(node2->right); - } - - // 当t1的左节点 为空 t2左节点不为空,就赋值过去 - if (node1->left == NULL && node2->left != NULL) { - node1->left = node2->left; - } - // 当t1的右节点 为空 t2右节点不为空,就赋值过去 - if (node1->right == NULL && node2->right != NULL) { - node1->right = node2->right; - } - } - return t1; - } -}; -``` - -# 拓展 - -当然也可以秀一波指针的操作,这是我写的野路子,大家就随便看看就行了,以防带跑遍了。 - -如下代码中,想要更改二叉树的值,应该传入指向指针的指针。 - -代码如下:(前序遍历) -``` -class Solution { -public: - void process(TreeNode** t1, TreeNode** t2) { - if ((*t1) == NULL && (*t2) == NULL) return; - if ((*t1) != NULL && (*t2) != NULL) { - (*t1)->val += (*t2)->val; - } - if ((*t1) == NULL && (*t2) != NULL) { - *t1 = *t2; - return; - } - if ((*t1) != NULL && (*t2) == NULL) { - return; - } - process(&((*t1)->left), &((*t2)->left)); - process(&((*t1)->right), &((*t2)->right)); - } - TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { - process(&t1, &t2); - return t1; - } -}; -``` - -# 总结 - -合并二叉树,也是二叉树操作的经典题目,如果没有接触过的话,其实并不简单,因为我们习惯了操作一个二叉树,一起操作两个二叉树,还会有点懵懵的。 - -这不是我们第一次操作两颗二叉树了,在[二叉树:我对称么?](https://mp.weixin.qq.com/s/Kgf0gjvlDlNDfKIH2b1Oxg)中也一起操作了两棵二叉树。 - -迭代法中,一般一起操作两个树都是使用队列模拟类似层序遍历,同时处理两个树的节点,这种方式最好理解,如果用模拟递归的思路的话,要复杂一些。 - -最后拓展中,我给了一个操作指针的野路子,大家随便看看就行了,如果学习C++的话,可以在去研究研究。 - -就酱,学到了的话,就转发给身边需要的同学吧! - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0637.二叉树的层平均值.md b/problems/0637.二叉树的层平均值.md deleted file mode 100644 index 5181d342..00000000 --- a/problems/0637.二叉树的层平均值.md +++ /dev/null @@ -1,50 +0,0 @@ -## 题目地址 - -## 思路 - -这道题目就是考察二叉树的层序遍历。 - -二叉树的层序遍历还有这两道题目,大家可以先做一下,其实都是一个思路 - -* [0102.二叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0102.二叉树的层序遍历.md) 单纯的层寻遍历 -* |[0107.二叉树的层次遍历II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0107.二叉树的层次遍历II.md) 相对于[0102.二叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0102.二叉树的层序遍历.md)就是把每一层倒叙输出就可以了 - -想要学习二叉树的深度遍历可以看这里[彻底吃透前中后序递归法](https://leetcode-cn.com/problems/binary-tree-preorder-traversal/solution/dai-ma-sui-xiang-lu-chi-tou-qian-zhong-hou-xu-de-d/)。 - -而本题呢,就是把每一层的结果求一个和,然后取平均值。 - -层序遍历一个二叉树,需要借用一个辅助数据结构队列来实现,**队列先进先出,符合一层一层遍历的逻辑,而是用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。** - -使用队列实现广度优先遍历,动画如下: - - - -这样就实现了层序从左到右遍历二叉树。 - -代码如下:(这份代码也可以作为二叉树层序遍历的模板)。 - -## C++代码 - -``` -class Solution { -public: - vector averageOfLevels(TreeNode* root) { - queue que; - if (root != NULL) que.push(root); - vector result; - while (!que.empty()) { - int size = que.size(); - double sum = 0; // 统计每一层的和 - for (int i = 0; i < size; i++) { // 这里一定要使用固定大小size,不要使用que.size() - TreeNode* node = que.front(); - que.pop(); - sum += node->val; - if (node->left) que.push(node->left); - if (node->right) que.push(node->right); - } - result.push_back(sum / size); // 将每一层放进结果集 - } - return result; - } -}; -``` diff --git a/problems/0649.Dota2参议院.md b/problems/0649.Dota2参议院.md deleted file mode 100644 index d6a2a12f..00000000 --- a/problems/0649.Dota2参议院.md +++ /dev/null @@ -1,70 +0,0 @@ - -## 思路 - -这道题 题意太绕了,我举一个更形象的例子给大家捋顺一下。 - -例如输入"RRDDD",执行过程应该是什么样呢? - -* 第一轮:senate[0]的R消灭senate[2]的D,senate[1]的R消灭senate[3]的D,senate[4]的D消灭senate[0]的R,此时剩下"RD",第一轮结束! -* 第二轮:senate[0]的R消灭senate[1]的D,第二轮结束 -* 第三轮:只有R了,R胜利 - -估计不少同学都困惑,R和D数量相同怎么办,究竟谁赢,**其实这是一个持续消灭的过程!** 即:如果同时存在R和D就继续进行下一轮消灭,轮数直到只剩下R或者D为止! - -那么每一轮消灭的策略应该是什么呢? - -例如:RDDRD - -第一轮:senate[0]的R消灭senate[1]的D,那么senate[2]的D,是消灭senate[0]的R还是消灭senate[3]的R呢? - -当然是消灭senate[3]的R,因为当轮到这个R的时候,它可以消灭senate[4]的D。 - -**所以消灭的策略是,尽量消灭自己后面的对手,因为前面的对手已经使用过权利了,而后序的对手依然可以使用权利消灭自己的同伴!** - -那么局部最优:有一次权利机会,就消灭自己后面的对手。全局最优:为自己的阵营赢取最大利益。 - -局部最优可以退出全局最优,举不出反例,那么试试贪心。 - -如果对贪心算法理论基础还不了解的话,可以看看这篇:[关于贪心算法,你该了解这些!](https://mp.weixin.qq.com/s/O935TaoHE9Eexwe_vSbRAg) ,相信看完之后对贪心就有基本的了解了。 - -## 代码实现 - -实现代码,在每一轮循环的过程中,去过模拟优先消灭身后的对手,其实是比较麻烦的。 - -这里有一个技巧,就是用一个变量记录当前参议员之前有几个敌对对手了,进而判断自己是否被消灭了。这个变量我用flag来表示。 - -C++代码如下: - - -``` -class Solution { -public: - string predictPartyVictory(string senate) { - // R = true表示本轮循环结束后,字符串里依然有R。D同理 - bool R = true, D = true; - // 当flag大于0时,R在D前出现,R可以消灭D。当flag小于0时,D在R前出现,D可以消灭R - int flag = 0; - while (R && D) { // 一旦R或者D为false,就结束循环,说明本轮结束后只剩下R或者D了 - R = false; - D = false; - for (int i = 0; i < senate.size(); i++) { - if (senate[i] == 'R') { - if (flag < 0) senate[i] = 0; // 消灭R,R此时为false - else R = true; // 如果没被消灭,本轮循环结束有R - flag++; - } - if (senate[i] == 'D') { - if (flag > 0) senate[i] = 0; - else D = true; - flag--; - } - } - } - // 循环结束之后,R和D只能有一个为true - return R == true ? "Radiant" : "Dire"; - } -}; -``` - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/0654.最大二叉树.md b/problems/0654.最大二叉树.md deleted file mode 100644 index 64efda5c..00000000 --- a/problems/0654.最大二叉树.md +++ /dev/null @@ -1,219 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/maximum-binary-tree/ - -> 用数组构建二叉树都是一样的套路 - -# 654.最大二叉树 - -给定一个不含重复元素的整数数组。一个以此数组构建的最大二叉树定义如下: - -* 二叉树的根是数组中的最大元素。 -* 左子树是通过数组中最大值左边部分构造出的最大二叉树。 -* 右子树是通过数组中最大值右边部分构造出的最大二叉树。 - -通过给定的数组构建最大二叉树,并且输出这个树的根节点。 - -示例 : - - - -提示: - -给定的数组的大小在 [1, 1000] 之间。 - -## 思路 - -最大二叉树的构建过程如下: - - - -构造树一般采用的是前序遍历,因为先构造中间节点,然后递归构造左子树和右子树。 - -* 确定递归函数的参数和返回值 - -参数就是传入的是存放元素的数组,返回该数组构造的二叉树的头结点,返回类型是指向节点的指针。 - -代码如下: - -``` -TreeNode* constructMaximumBinaryTree(vector& nums) - -``` -* 确定终止条件 - -题目中说了输入的数组大小一定是大于等于1的,所以我们不用考虑小于1的情况,那么当递归遍历的时候,如果传入的数组大小为1,说明遍历到了叶子节点了。 - -那么应该定义一个新的节点,并把这个数组的数值赋给新的节点,然后返回这个节点。 这表示一个数组大小是1的时候,构造了一个新的节点,并返回。 - -代码如下: - -``` -TreeNode* node = new TreeNode(0); -if (nums.size() == 1) { -    node->val = nums[0]; -    return node; -} -``` - -* 确定单层递归的逻辑 - -这里有三步工作 - -1. 先要找到数组中最大的值和对应的下表, 最大的值构造根节点,下表用来下一步分割数组。 - -代码如下: -``` -int maxValue = 0; -int maxValueIndex = 0; -for (int i = 0; i < nums.size(); i++) { -    if (nums[i] > maxValue) { -        maxValue = nums[i]; -        maxValueIndex = i; -    } -} -TreeNode* node = new TreeNode(0); -node->val = maxValue; -``` - -2. 最大值所在的下表左区间 构造左子树 - -这里要判断maxValueIndex > 0,因为要保证左区间至少有一个数值。 - -代码如下: -``` -if (maxValueIndex > 0) { -    vector newVec(nums.begin(), nums.begin() + maxValueIndex); -    node->left = constructMaximumBinaryTree(newVec); -} -``` - -3. 最大值所在的下表右区间 构造右子树 - -判断maxValueIndex < (nums.size() - 1),确保右区间至少有一个数值。 - -代码如下: - -``` -if (maxValueIndex < (nums.size() - 1)) { -    vector newVec(nums.begin() + maxValueIndex + 1, nums.end()); -    node->right = constructMaximumBinaryTree(newVec); -} -``` -这样我们就分析完了,整体代码如下:(详细注释) - -``` -class Solution { -public: - TreeNode* constructMaximumBinaryTree(vector& nums) { - TreeNode* node = new TreeNode(0); - if (nums.size() == 1) { - node->val = nums[0]; - return node; - } - // 找到数组中最大的值和对应的下表 - int maxValue = 0; - int maxValueIndex = 0; - for (int i = 0; i < nums.size(); i++) { - if (nums[i] > maxValue) { - maxValue = nums[i]; - maxValueIndex = i; - } - } - node->val = maxValue; - // 最大值所在的下表左区间 构造左子树 - if (maxValueIndex > 0) { - vector newVec(nums.begin(), nums.begin() + maxValueIndex); - node->left = constructMaximumBinaryTree(newVec); - } - // 最大值所在的下表右区间 构造右子树 - if (maxValueIndex < (nums.size() - 1)) { - vector newVec(nums.begin() + maxValueIndex + 1, nums.end()); - node->right = constructMaximumBinaryTree(newVec); - } - return node; - } -}; -``` - -以上代码比较冗余,效率也不高,每次还要切割的时候每次都要定义新的vector(也就是数组),但逻辑比较清晰。 - -和文章[二叉树:构造二叉树登场!](https://mp.weixin.qq.com/s/7r66ap2s-shvVvlZxo59xg)中一样的优化思路,就是每次分隔不用定义新的数组,而是通过下表索引直接在原数组上操作。 - -优化后代码如下: - -``` -class Solution { -private: - // 在左闭右开区间[left, right),构造二叉树 - TreeNode* traversal(vector& nums, int left, int right) { - if (left >= right) return nullptr; - - // 分割点下表:maxValueIndex - int maxValueIndex = left; - for (int i = left + 1; i < right; ++i) { - if (nums[i] > nums[maxValueIndex]) maxValueIndex = i; - } - - TreeNode* root = new TreeNode(nums[maxValueIndex]); - - // 左闭右开:[left, maxValueIndex) - root->left = traversal(nums, left, maxValueIndex); - - // 左闭右开:[maxValueIndex + 1, right) - root->right = traversal(nums, maxValueIndex + 1, right); - - return root; - } -public: - TreeNode* constructMaximumBinaryTree(vector& nums) { - return traversal(nums, 0, nums.size()); - } -}; -``` - -# 拓展 - -可以发现上面的代码看上去简洁一些,**主要是因为第二版其实是允许空节点进入递归,所以不用在递归的时候加判断节点是否为空** - -第一版递归过程:(加了if判断,为了不让空节点进入递归) -``` - -if (maxValueIndex > 0) { // 这里加了判断是为了不让空节点进入递归 - vector newVec(nums.begin(), nums.begin() + maxValueIndex); - node->left = constructMaximumBinaryTree(newVec); -} - -if (maxValueIndex < (nums.size() - 1)) { // 这里加了判断是为了不让空节点进入递归 - vector newVec(nums.begin() + maxValueIndex + 1, nums.end()); - node->right = constructMaximumBinaryTree(newVec); -} -``` - -第二版递归过程: (如下代码就没有加if判断) - -``` -root->left = traversal(nums, left, maxValueIndex); - -root->right = traversal(nums, maxValueIndex + 1, right); -``` - -第二版代码是允许空节点进入递归,所以没有加if判断,当然终止条件也要有相应的改变。 - -第一版终止条件,是遇到叶子节点就终止,因为空节点不会进入递归。 - -第二版相应的终止条件,是遇到空节点,也就是数组区间为0,就终止了。 - - -# 总结 - -这道题目其实和 [二叉树:构造二叉树登场!](https://mp.weixin.qq.com/s/7r66ap2s-shvVvlZxo59xg) 是一个思路,比[二叉树:构造二叉树登场!](https://mp.weixin.qq.com/s/7r66ap2s-shvVvlZxo59xg) 还简单一些。 - -**注意类似用数组构造二叉树的题目,每次分隔尽量不要定义新的数组,而是通过下表索引直接在原数组上操作,这样可以节约时间和空间上的开销。** - -一些同学也会疑惑,什么时候递归函数前面加if,什么时候不加if,这个问题我在最后也给出了解释。 - -其实就是不同代码风格的实现,**一般情况来说:如果让空节点(空指针)进入递归,就不加if,如果不让空节点进入递归,就加if限制一下, 终止条件也会相应的调整。** - - - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0657.机器人能否返回原点.md b/problems/0657.机器人能否返回原点.md deleted file mode 100644 index fb74c751..00000000 --- a/problems/0657.机器人能否返回原点.md +++ /dev/null @@ -1,38 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/robot-return-to-origin/ - -## 思路 - -这道题目还是挺简单的,大家不要想复杂了,一波哈希法又一波图论算法啥的,哈哈。 - -其实就是,x,y坐标,初始为0,然后: -* if (moves[i] == 'U') y++; -* if (moves[i] == 'D') y--; -* if (moves[i] == 'L') x--; -* if (moves[i] == 'R') x++; - -最后判断一下x,y是否回到了(0, 0)位置就可以了。 - -如图所示: - - -## C++代码 - -``` -class Solution { -public: - bool judgeCircle(string moves) { - int x = 0, y = 0; - for (int i = 0; i < moves.size(); i++) { - if (moves[i] == 'U') y++; - if (moves[i] == 'D') y--; - if (moves[i] == 'L') x--; - if (moves[i] == 'R') x++; - } - if (x == 0 && y == 0) return true; - return false; - } -}; -``` - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0669.修剪二叉搜索树.md b/problems/0669.修剪二叉搜索树.md deleted file mode 100644 index 641416b0..00000000 --- a/problems/0669.修剪二叉搜索树.md +++ /dev/null @@ -1,238 +0,0 @@ - -## 题目链接 - -https://leetcode-cn.com/problems/trim-a-binary-search-tree/ - -> 如果不对递归有深刻的理解,本题有点难 - -> 单纯移除一个节点那还不够,要修剪! - -# 669. 修剪二叉搜索树 - -题目链接:https://leetcode-cn.com/problems/trim-a-binary-search-tree/ - -给定一个二叉搜索树,同时给定最小边界L 和最大边界 R。通过修剪二叉搜索树,使得所有节点的值在[L, R]中 (R>=L) 。你可能需要改变树的根节点,所以结果应当返回修剪好的二叉搜索树的新的根节点。 - -![669.修剪二叉搜索树](https://img-blog.csdnimg.cn/20201014173115788.png) - -![669.修剪二叉搜索树1](https://img-blog.csdnimg.cn/20201014173219142.png) - -# 思路 - -相信看到这道题目大家都感觉是一道简单题(事实上leetcode上也标明是简单)。 - -但还真的不简单! - -## 递归法 - -直接想法就是:递归处理,然后遇到 `root->val < low || root->val > high` 的时候直接return NULL,一波修改,赶紧利落。 - -不难写出如下代码: - -``` -class Solution { -public: - TreeNode* trimBST(TreeNode* root, int low, int high) { - if (root == nullptr || root->val < low || root->val > high) return nullptr; - root->left = trimBST(root->left, low, high); - root->right = trimBST(root->right, low, high); - return root; - } -}; -``` - -**然而[1, 3]区间在二叉搜索树的中可不是单纯的节点3和左孩子节点0就决定的,还要考虑节点0的右子树**。 - -我们在重新关注一下第二个示例,如图: - - - -**所以以上的代码是不可行的!** - -从图中可以看出需要重构二叉树,想想是不是本题就有点复杂了。 - -其实不用重构那么复杂。 - -在上图中我们发现节点0并不符合区间要求,那么将节点0的右孩子 节点2 直接赋给 节点3的左孩子就可以了(就是把节点0从二叉树中移除),如图: - - - - -理解了最关键部分了我们在递归三部曲: - -* 确定递归函数的参数以及返回值 - -这里我们为什么需要返回值呢? - -因为是要遍历整棵树,做修改,其实不需要返回值也可以,我们也可以完成修剪(其实就是从二叉树中移除节点)的操作。 - -但是有返回值,更方便,可以通过递归函数的返回值来移除节点。 - -这样的做法在[二叉树:搜索树中的插入操作](https://mp.weixin.qq.com/s/lwKkLQcfbCNX2W-5SOeZEA)和[二叉树:搜索树中的删除操作](https://mp.weixin.qq.com/s/-p-Txvch1FFk3ygKLjPAKw)中大家已经了解过了。 - -代码如下: - -``` -TreeNode* trimBST(TreeNode* root, int low, int high) -``` - -* 确定终止条件 - -修剪的操作并不是在终止条件上进行的,所以就是遇到空节点返回就可以了。 - -``` -if (root == nullptr ) return nullptr; -``` - -* 确定单层递归的逻辑 - -如果root(当前节点)的元素小于low的数值,那么应该递归右子树,并返回右子树符合条件的头结点。 - -代码如下: - -``` -if (root->val < low) { - TreeNode* right = trimBST(root->right, low, high); // 寻找符合区间[low, high]的节点 - return right; -} -``` - -如果root(当前节点)的元素大于high的,那么应该递归左子树,并返回左子树符合条件的头结点。 - -代码如下: - -``` -if (root->val > high) { - TreeNode* left = trimBST(root->left, low, high); // 寻找符合区间[low, high]的节点 - return left; -} -``` - -接下来要将下一层处理完左子树的结果赋给root->left,处理完右子树的结果赋给root->right。 - -最后返回root节点,代码如下: - -``` -root->left = trimBST(root->left, low, high); // root->left接入符合条件的左孩子 -root->right = trimBST(root->right, low, high); // root->right接入符合条件的右孩子 -return root; -``` - -此时大家是不是还没发现这多余的节点究竟是如何从二叉树中移除的呢? - -在回顾一下上面的代码,针对下图中二叉树的情况: - - - -如下代码相当于把节点0的右孩子(节点2)返回给上一层, -``` -if (root->val < low) { - TreeNode* right = trimBST(root->right, low, high); // 寻找符合区间[low, high]的节点 - return right; -} -``` - -然后如下代码相当于用节点3的左孩子 把下一层返回的 节点0的右孩子(节点2) 接住。 - -``` -root->left = trimBST(root->left, low, high); -``` - -此时节点3的右孩子就变成了节点2,将节点0从二叉树中移除了。 - -最后整体代码如下: - -``` -class Solution { -public: - TreeNode* trimBST(TreeNode* root, int low, int high) { - if (root == nullptr ) return nullptr; - if (root->val < low) { - TreeNode* right = trimBST(root->right, low, high); // 寻找符合区间[low, high]的节点 - return right; - } - if (root->val > high) { - TreeNode* left = trimBST(root->left, low, high); // 寻找符合区间[low, high]的节点 - return left; - } - root->left = trimBST(root->left, low, high); // root->left接入符合条件的左孩子 - root->right = trimBST(root->right, low, high); // root->right接入符合条件的右孩子 - return root; - } -}; -``` - -精简之后代码如下: - -``` -class Solution { -public: - TreeNode* trimBST(TreeNode* root, int low, int high) { - if (root == nullptr) return nullptr; - if (root->val < low) return trimBST(root->right, low, high); - if (root->val > high) return trimBST(root->left, low, high); - root->left = trimBST(root->left, low, high); - root->right = trimBST(root->right, low, high); - return root; - } -}; -``` - -只看代码,其实不太好理解节点是符合移除的,这一块大家可以自己在模拟模拟! - -## 迭代法 - -因为二叉搜索树的有序性,不需要使用栈模拟递归的过程。 - -在剪枝的时候,可以分为三步: - -* 将root移动到[L, R] 范围内,注意是左闭右闭区间 -* 剪枝左子树 -* 剪枝右子树 - -代码如下: - -``` -class Solution { -public: - TreeNode* trimBST(TreeNode* root, int L, int R) { - if (!root) return nullptr; - - // 处理头结点,让root移动到[L, R] 范围内,注意是左闭右闭 - while (root->val < L || root->val > R) { - if (root->val < L) root = root->right; // 小于L往右走 - else root = root->left; // 大于R往左走 - } - TreeNode *cur = root; - // 此时root已经在[L, R] 范围内,处理左孩子元素小于L的情况 - while (cur != nullptr) { - while (cur->left && cur->left->val < L) { - cur->left = cur->left->right; - } - cur = cur->left; - } - cur = root; - - // 此时root已经在[L, R] 范围内,处理右孩子大于R的情况 - while (cur != nullptr) { - while (cur->right && cur->right->val > R) { - cur->right = cur->right->left; - } - cur = cur->right; - } - return root; - } -}; -``` - -# 总结 - -修剪二叉搜索树其实并不难,但在递归法中大家可看出我费了很大的功夫来讲解如何删除节点的,这个思路其实是比较绕的。 - -最终的代码倒是很简洁。 - -**如果不对递归有深刻的理解,这道题目还是有难度的!** - -本题我依然给出递归法和迭代法,初学者掌握递归就可以了,如果想进一步学习,就把迭代法也写一写。 - -**就酱,如果学到了,就转发给身边需要的同学吧!** diff --git a/problems/0685.冗余连接II.md b/problems/0685.冗余连接II.md deleted file mode 100644 index d8b5f5ec..00000000 --- a/problems/0685.冗余连接II.md +++ /dev/null @@ -1,175 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/redundant-connection-ii/ - -## 思路 - -先重点读懂题目中的这句**该图由一个有着N个节点 (节点值不重复1, 2, ..., N) 的树及一条附加的边构成。附加的边的两个顶点包含在1到N中间,这条附加的边不属于树中已存在的边。** - -**这说明题目中的图原本是是一棵树,只不过在不增加节点的情况下多加了一条边!** - -还有**若有多个答案,返回最后出现在给定二维数组的答案。**这说明在两天边都可以删除的情况下,要删顺序靠后的! - - -那么有如下三种情况,前两种情况是出现入度为2的点,如图: - - - -且只有一个节点入度为2,为什么不看出度呢,出度没有意义,一颗树中随便一个父节点就有多个出度。 - - -第三种情况是没有入度为2的点,那么图中一定出现了有向环(**注意这里强调是有向环!**) - -如图: - - - - -首先先计算节点的入度,代码如下: - -``` - int inDegree[N] = {0}; // 记录节点入度 - n = edges.size(); // 边的数量 - for (int i = 0; i < n; i++) { - inDegree[edges[i][1]]++; // 统计入度 - } -``` - -前两种入度为2的情况,一定是删除指向入度为2的节点的两条边其中的一条,如果删了一条,判断这个图是一个树,那么这条边就是答案,同时注意要从后向前遍历,因为如果两天边删哪一条都可以成为树,就删最后那一条。 - -代码如下: - -``` - vector vec; // 记录入度为2的边(如果有的话就两条边) - // 找入度为2的节点所对应的边,注意要倒叙,因为优先返回最后出现在二维数组中的答案 - for (int i = n - 1; i >= 0; i--) { - if (inDegree[edges[i][1]] == 2) { - vec.push_back(i); - } - } - // 处理图中情况1 和 情况2 - // 如果有入度为2的节点,那么一定是两条边里删一个,看删哪个可以构成树 - if (vec.size() > 0) { - if (isTreeAfterRemoveEdge(edges, vec[0])) { - return edges[vec[0]]; - } else { - return edges[vec[1]]; - } - } -``` - -在来看情况三,明确没有入度为2的情况,那么一定有有向环,找到构成环的边就是要删除的边。 - -可以定义一个函数,代码如下: - -``` -// 在有向图里找到删除的那条边,使其变成树,返回值就是要删除的边 -vector getRemoveEdge(const vector>& edges) -``` - -此时 大家应该知道了,我们要实现两个最为关键的函数: - -* `isTreeAfterRemoveEdge()` 判断删一个边之后是不是树了 -* `getRemoveEdge` 确定图中一定有了有向环,那么要找到需要删除的那条边 - -此时应该是用到**并查集**了,并查集为什么可以判断 一个图是不是树呢? - -**因为如果两个点所在的边在添加图之前如果就可以在并查集里找到了相同的根,那么这条边添加上之后 这个图一定不是树了** - -这里对并查集就不展开过多的讲解了,翻到了自己九年前写过了一篇并查集的文章[并查集学习](https://blog.csdn.net/youngyangyang04/article/details/6447435),哈哈,那时候还太年轻,写不咋地,有空我会重写一篇! - -本题代码如下:(详细注释了) - -## C++代码 - -``` - -class Solution { -private: - static const int N = 1010; // 如题:二维数组大小的在3到1000范围内 - int father[N]; - int n; // 边的数量 - // 并查集初始化 - void init() { - for (int i = 1; i <= n; ++i) { - father[i] = i; - } - } - // 并查集里寻根的过程 - int find(int u) { - return u == father[u] ? u : father[u] = find(father[u]); - } - // 将v->u 这条边加入并查集 - void join(int u, int v) { - u = find(u); - v = find(v); - if (u == v) return ; - father[v] = u; - } - // 判断 u 和 v是否找到同一个根 - bool same(int u, int v) { - u = find(u); - v = find(v); - return u == v; - } - // 在有向图里找到删除的那条边,使其变成树 - vector getRemoveEdge(const vector>& edges) { - init(); // 初始化并查集 - for (int i = 0; i < n; i++) { // 遍历所有的边 - if (same(edges[i][0], edges[i][1])) { // 构成有向环了,就是要删除的边 - return edges[i]; - } - join(edges[i][0], edges[i][1]); - } - return {}; - } - - // 删一条边之后判断是不是树 - bool isTreeAfterRemoveEdge(const vector>& edges, int deleteEdge) { - init(); // 初始化并查集 - for (int i = 0; i < n; i++) { - if (i == deleteEdge) continue; - if (same(edges[i][0], edges[i][1])) { // 构成有向环了,一定不是树 - return false; - } - join(edges[i][0], edges[i][1]); - } - return true; - } -public: - - vector findRedundantDirectedConnection(vector>& edges) { - int inDegree[N] = {0}; // 记录节点入度 - n = edges.size(); // 边的数量 - for (int i = 0; i < n; i++) { - inDegree[edges[i][1]]++; // 统计入度 - } - vector vec; // 记录入度为2的边(如果有的话就两条边) - // 找入度为2的节点所对应的边,注意要倒叙,因为优先返回最后出现在二维数组中的答案 - for (int i = n - 1; i >= 0; i--) { - if (inDegree[edges[i][1]] == 2) { - vec.push_back(i); - } - } - // 处理图中情况1 和 情况2 - // 如果有入度为2的节点,那么一定是两条边里删一个,看删哪个可以构成树 - if (vec.size() > 0) { - if (isTreeAfterRemoveEdge(edges, vec[0])) { - return edges[vec[0]]; - } else { - return edges[vec[1]]; - } - } - // 处理图中情况3 - // 明确没有入度为2的情况,那么一定有有向环,找到构成环的边返回就可以了 - return getRemoveEdge(edges); - - } -}; -``` - - - - - - - diff --git a/problems/0698.划分为k个相等的子集.md b/problems/0698.划分为k个相等的子集.md deleted file mode 100644 index 23382bbe..00000000 --- a/problems/0698.划分为k个相等的子集.md +++ /dev/null @@ -1,42 +0,0 @@ - -使用回溯法,首先要明确这是组合问题,即集合里是不强调顺序的,所以要用startIndex - -``` -class Solution { -private: - bool backtracking(vector& nums, - int k, - int target, // 子集目标和 - int cur, // 当前目标和 - int startIndex, // 起始位置 - vector& used) { // 标记是否使用过 - if (k == 0) return true; // 找到了k个相同子集 - if (cur == target) { // 发现一个合格子集,然后重新开始寻找 - return backtracking(nums, k - 1, target, 0, 0, used); // k-1 - } - for (int i = startIndex; i < nums.size(); i++) { - if (cur + nums[i] <= target && !used[i]) { - used[i] = true; - if (backtracking(nums, k, target, cur + nums[i], i + 1, used)) { - return true; - } - used[i] = false; - } - } - return false; - } -public: - bool canPartitionKSubsets(vector& nums, int k) { - //sort(nums.begin(), nums.end()); 不需要排序 - int sum = 0; - for (int i = 0; i < nums.size(); i++) { - sum += nums[i]; - } - if (sum % k != 0) return false; - int target = sum / k; - vector used(nums.size(), false); - - return backtracking(nums, k, target, 0, 0, used); - } -}; -``` diff --git a/problems/0700.二叉搜索树中的搜索.md b/problems/0700.二叉搜索树中的搜索.md deleted file mode 100644 index cdf713be..00000000 --- a/problems/0700.二叉搜索树中的搜索.md +++ /dev/null @@ -1,137 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/search-in-a-binary-search-tree/ - -> 二叉搜索树登场! - -# 700.二叉搜索树中的搜索 - -给定二叉搜索树(BST)的根节点和一个值。 你需要在BST中找到节点值等于给定值的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 NULL。 - -例如, - - - -在上述示例中,如果要找的值是 5,但因为没有节点值为 5,我们应该返回 NULL。 - -# 思路 - -之前我们讲了都是普通二叉树,那么接下来看看二叉搜索树。 - -在[关于二叉树,你该了解这些!](https://mp.weixin.qq.com/s/_ymfWYvTNd2GvWvC5HOE4A)中,我们已经讲过了二叉搜索树。 - -二叉搜索树是一个有序树: - -* 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; -* 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; -* 它的左、右子树也分别为二叉搜索树 - -这就决定了,二叉搜索树,递归遍历和迭代遍历和普通二叉树都不一样。 - -本题,其实就是在二叉搜索树中搜索一个节点。那么我们来看看应该如何遍历。 - -## 递归法 - -1. 确定递归函数的参数和返回值 - -递归函数的参数传入的就是根节点和要搜索的数值,返回的就是以这个搜索数值所在的节点。 - -代码如下: - -``` -TreeNode* searchBST(TreeNode* root, int val) -``` - -2. 确定终止条件 - -如果root为空,或者找到这个数值了,就返回root节点。 - -``` -if (root == NULL || root->val == val) return root; -``` - -3. 确定单层递归的逻辑 - -看看二叉搜索树的单层递归逻辑有何不同。 - -因为二叉搜索树的节点是有序的,所以可以有方向的去搜索。 - -如果root->val > val,搜索左子树,如果root->val < val,就搜索右子树,最后如果都没有搜索到,就返回NULL。 - -代码如下: - -``` -if (root->val > val) return searchBST(root->left, val); // 注意这里加了return -if (root->val < val) return searchBST(root->right, val); -return NULL; -``` - -这里可能会疑惑,在递归遍历的时候,什么时候直接return 递归函数的返回值,什么时候不用加这个 return呢。 - -我们在[二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?](https://mp.weixin.qq.com/s/6TWAVjxQ34kVqROWgcRFOg)中讲了,如果要搜索一条边,递归函数就要加返回值,这里也是一样的道理。 - -**因为搜索到目标节点了,就要立即return了,这样才是找到节点就返回(搜索某一条边),如果不加return,就是遍历整棵树了。** - -整体代码如下: - -``` -class Solution { -public: - TreeNode* searchBST(TreeNode* root, int val) { - if (root == NULL || root->val == val) return root; - if (root->val > val) return searchBST(root->left, val); - if (root->val < val) return searchBST(root->right, val); - return NULL; - } -}; -``` - -## 迭代法 - -一提到二叉树遍历的迭代法,可能立刻想起使用栈来模拟深度遍历,使用队列来模拟广度遍历。 - -对于二叉搜索树可就不一样了,因为二叉搜索树的特殊性,也就是节点的有序性,可以不使用辅助栈或者队列就可以写出迭代法。 - -对于一般二叉树,递归过程中还有回溯的过程,例如走一个左方向的分支走到头了,那么要调头,在走右分支。 - -而**对于二叉搜索树,不需要回溯的过程,因为节点的有序性就帮我们确定了搜索的方向。** - -例如要搜索元素为3的节点,**我们不需要搜索其他节点,也不需要做回溯,查找的路径已经规划好了。** - -中间节点如果大于3就向左走,如果小于3就向右走,如图: - -![二叉搜索树](https://img-blog.csdnimg.cn/20200812190213280.png) - -所以迭代法代码如下: - -``` -class Solution { -public: - TreeNode* searchBST(TreeNode* root, int val) { - while (root != NULL) { - if (root->val > val) root = root->left; - else if (root->val < val) root = root->right; - else return root; - } - return NULL; - } -}; -``` - -第一次看到了如此简单的迭代法,是不是感动的痛哭流涕,哭一会~ - -# 总结 - -本篇我们介绍了二叉搜索树的遍历方式,因为二叉搜索树的有序性,遍历的时候要比普通二叉树简单很多。 - -但是一些同学很容易忽略二叉搜索树的特性,所以写出遍历的代码就未必真的简单了。 - -所以针对二叉搜索树的题目,一样要利用其特性。 - -文中我依然给出递归和迭代两种方式,可以看出写法都非常简单,就是利用了二叉搜索树有序的特点。 - -就酱,如果学到了,就转发给身边需要的同学吧! - - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20201124161234338.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** diff --git a/problems/0701.二叉搜索树中的插入操作.md b/problems/0701.二叉搜索树中的插入操作.md deleted file mode 100644 index babb3025..00000000 --- a/problems/0701.二叉搜索树中的插入操作.md +++ /dev/null @@ -1,206 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/insert-into-a-binary-search-tree/ - -> 开始修改二叉搜索树 - -# 701.二叉搜索树中的插入操作 - -链接:https://leetcode-cn.com/problems/insert-into-a-binary-search-tree/ - -给定二叉搜索树(BST)的根节点和要插入树中的值,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据保证,新值和原始二叉搜索树中的任意节点值都不同。 - -注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回任意有效的结果。 - -![701.二叉搜索树中的插入操作](https://img-blog.csdnimg.cn/20201019173259554.png) -  -提示: - -* 给定的树上的节点数介于 0 和 10^4 之间 -* 每个节点都有一个唯一整数值,取值范围从 0 到 10^8 -* -10^8 <= val <= 10^8 -* 新值和原始二叉搜索树中的任意节点值都不同 - -# 思路 - -其实这道题目其实是一道简单题目,**但是题目中的提示:有多种有效的插入方式,还可以重构二叉搜索树,一下子吓退了不少人**,瞬间感觉题目复杂了很多。 - -其实**可以不考虑题目中提示所说的改变树的结构的插入方式。** - -如下演示视频中可以看出:只要按照二叉搜索树的规则去遍历,遇到空节点就插入节点就可以了。 - - - -例如插入元素10 ,需要找到末尾节点插入便可,一样的道理来插入元素15,插入元素0,插入元素6,**需要调整二叉树的结构么? 并不需要。**。 - -只要遍历二叉搜索树,找到空节点 插入元素就可以了,那么这道题其实就简单了。 - -接下来就是遍历二叉搜索树的过程了。 - -## 递归 - -递归三部曲: - -* 确定递归函数参数以及返回值 - -参数就是根节点指针,以及要插入元素,这里递归函数要不要有返回值呢? - -可以有,也可以没有,但递归函数如果没有返回值的话,实现是比较麻烦的,下面也会给出其具体实现代码。 - -**有返回值的话,可以利用返回值完成新加入的节点与其父节点的赋值操作**。(下面会进一步解释) - -递归函数的返回类型为节点类型TreeNode * 。 - -代码如下: - -``` -TreeNode* insertIntoBST(TreeNode* root, int val) -``` - -* 确定终止条件 - -终止条件就是找到遍历的节点为null的时候,就是要插入节点的位置了,并把插入的节点返回。 - -代码如下: - -``` -if (root == NULL) { - TreeNode* node = new TreeNode(val); - return node; -} -``` - -这里把添加的节点返回给上一层,就完成了父子节点的赋值操作了,详细再往下看。 - -* 确定单层递归的逻辑 - -此时要明确,需要遍历整棵树么? - -别忘了这是搜索树,遍历整颗搜索树简直是对搜索树的侮辱,哈哈。 - -搜索树是有方向了,可以根据插入元素的数值,决定递归方向。 - -代码如下: - -``` -if (root->val > val) root->left = insertIntoBST(root->left, val); -if (root->val < val) root->right = insertIntoBST(root->right, val); -return root; -``` - -**到这里,大家应该能感受到,如何通过递归函数返回值完成了新加入节点的父子关系赋值操作了,下一层将加入节点返回,本层用root->left或者root->right将其接住**。 - - -整体代码如下: - -``` -class Solution { -public: - TreeNode* insertIntoBST(TreeNode* root, int val) { - if (root == NULL) { - TreeNode* node = new TreeNode(val); - return node; - } - if (root->val > val) root->left = insertIntoBST(root->left, val); - if (root->val < val) root->right = insertIntoBST(root->right, val); - return root; - } -}; -``` - -可以看出代码并不复杂。 - -刚刚说了递归函数不用返回值也可以,找到插入的节点位置,直接让其父节点指向插入节点,结束递归,也是可以的。 - -那么递归函数定义如下: - -``` -TreeNode* parent; // 记录遍历节点的父节点 -void traversal(TreeNode* cur, int val) -``` - -没有返回值,需要记录上一个节点(parent),遇到空节点了,就让parent左孩子或者右孩子指向新插入的节点。然后结束递归。 - -代码如下: - -``` -class Solution { -private: - TreeNode* parent; - void traversal(TreeNode* cur, int val) { - if (cur == NULL) { - TreeNode* node = new TreeNode(val); - if (val > parent->val) parent->right = node; - else parent->left = node; - return; - } - parent = cur; - if (cur->val > val) traversal(cur->left, val); - if (cur->val < val) traversal(cur->right, val); - return; - } - -public: - TreeNode* insertIntoBST(TreeNode* root, int val) { - parent = new TreeNode(0); - if (root == NULL) { - root = new TreeNode(val); - } - traversal(root, val); - return root; - } -}; -``` - -可以看出还是麻烦一些的。 - -我之所以举这个例子,是想说明通过递归函数的返回值完成父子节点的赋值是可以带来便利的。 - -**网上千变一律的代码,可能会误导大家认为通过递归函数返回节点 这样的写法是天经地义,其实这里是有优化的!** - - -## 迭代 - -再来看看迭代法,对二叉搜索树迭代写法不熟悉,可以看这篇:[二叉树:二叉搜索树登场!](https://mp.weixin.qq.com/s/vsKrWRlETxCVsiRr8v_hHg) - -在迭代法遍历的过程中,需要记录一下当前遍历的节点的父节点,这样才能做插入节点的操作。 - -在[二叉树:搜索树的最小绝对差](https://mp.weixin.qq.com/s/Hwzml6698uP3qQCC1ctUQQ)和[二叉树:我的众数是多少?](https://mp.weixin.qq.com/s/KSAr6OVQIMC-uZ8MEAnGHg)中,都是用了记录pre和cur两个指针的技巧,本题也是一样的。 - -代码如下: - -``` -class Solution { -public: - TreeNode* insertIntoBST(TreeNode* root, int val) { - if (root == NULL) { - TreeNode* node = new TreeNode(val); - return node; - } - TreeNode* cur = root; - TreeNode* parent = root; // 这个很重要,需要记录上一个节点,否则无法赋值新节点 - while (cur != NULL) { - parent = cur; - if (cur->val > val) cur = cur->left; - else cur = cur->right; - } - TreeNode* node = new TreeNode(val); - if (val < parent->val) parent->left = node;// 此时是用parent节点的进行赋值 - else parent->right = node; - return root; - } -}; -``` - -# 总结 - -首先在二叉搜索树中的插入操作,大家不用恐惧其重构搜索树,其实根本不用重构。 - -然后在递归中,我们重点讲了如果通过递归函数的返回值完成新加入节点和其父节点的赋值操作,并强调了搜索树的有序性。 - -最后依然给出了迭代的方法,迭代的方法就需要记录当前遍历节点的父节点了,这个和没有返回值的递归函数实现的代码逻辑是一样的。 - -**就酱,学到了就转发给身边需要的同学吧!** - - - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0705.设计哈希集合.md b/problems/0705.设计哈希集合.md deleted file mode 100644 index f6d56e1c..00000000 --- a/problems/0705.设计哈希集合.md +++ /dev/null @@ -1,35 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/design-hashset/ - -## 思路 - -使用数组便可以实现这个哈希集合 - -## 代码 -``` -class MyHashSet { - -public: - vector hashTable; - /** Initialize your data structure here. */ - MyHashSet() { - vector table(1000001, 0); - hashTable = table; - } - - void add(int key) { - hashTable[key] = 1; - } - - void remove(int key) { - hashTable[key] = 0; - } - - /** Returns true if this set contains the specified element */ - bool contains(int key) { - return hashTable[key]; - } -}; -``` -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0707.设计链表.md b/problems/0707.设计链表.md deleted file mode 100644 index 01bd71f3..00000000 --- a/problems/0707.设计链表.md +++ /dev/null @@ -1,147 +0,0 @@ -# 题目地址 - -https://leetcode-cn.com/problems/design-linked-list/ - -> 听说这道题目把链表常见的五个操作都覆盖了? - -# 第707题:设计链表 - -题意: - -在链表类中实现这些功能: - -* get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。 -* addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。 -* addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。 -* addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val  的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。 -* deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。 - - -![707示例](https://img-blog.csdnimg.cn/20200814200558953.png) - -# 思路 - -如果对链表的基础知识还不太懂,可以看这篇文章:[关于链表,你该了解这些!](https://mp.weixin.qq.com/s/ntlZbEdKgnFQKZkSUAOSpQ) - -如果对链表的虚拟头结点不清楚,可以看这篇文章:[链表:听说用虚拟头节点会方便很多?](https://mp.weixin.qq.com/s/slM1CH5Ew9XzK93YOQYSjA) - -删除链表节点: -![链表-删除节点](https://img-blog.csdnimg.cn/20200806195114541.png) - -添加链表节点: -![链表-添加节点](https://img-blog.csdnimg.cn/20200806195134331.png) - -这道题目设计链表的五个接口: -* 获取链表第index个节点的数值 -* 在链表的最前面插入一个节点 -* 在链表的最后面插入一个节点 -* 在链表第index个节点前面插入一个节点 -* 删除链表的第index个节点 - -可以说这五个接口,已经覆盖了链表的常见操作,是练习链表操作非常好的一道题目 - -**链表操作的两种方式:** - -1. 直接使用原来的链表来进行操作。 -2. 设置一个虚拟头结点在进行操作。 - -下面采用的设置一个虚拟头结点(这样更方便一些,大家看代码就会感受出来)。 - - -## 代码 -``` -class MyLinkedList { -public: - // 定义链表节点结构体 - struct LinkedNode { - int val; - LinkedNode* next; - LinkedNode(int val):val(val), next(nullptr){} - }; - - // 初始化链表 - MyLinkedList() { - _dummyHead = new LinkedNode(0); // 这里定义的头结点 是一个虚拟头结点,而不是真正的链表头结点 - _size = 0; - } - - // 获取到第index个节点数值,如果index是非法数值直接返回-1, 注意index是从0开始的,第0个节点就是头结点 - int get(int index) { - if (index > (_size - 1) || index < 0) { - return -1; - } - LinkedNode* cur = _dummyHead->next; - while(index--){ // 如果--index 就会陷入死循环 - cur = cur->next; - } - return cur->val; - } - - // 在链表最前面插入一个节点,插入完成后,新插入的节点为链表的新的头结点 - void addAtHead(int val) { - LinkedNode* newNode = new LinkedNode(val); - newNode->next = _dummyHead->next; - _dummyHead->next = newNode; - _size++; - } - - // 在链表最后面添加一个节点 - void addAtTail(int val) { - LinkedNode* newNode = new LinkedNode(val); - LinkedNode* cur = _dummyHead; - while(cur->next != nullptr){ - cur = cur->next; - } - cur->next = newNode; - _size++; - } - - // 在第index个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。 - // 如果index 等于链表的长度,则说明是新插入的节点为链表的尾结点 - // 如果index大于链表的长度,则返回空 - void addAtIndex(int index, int val) { - if (index > _size) { - return; - } - LinkedNode* newNode = new LinkedNode(val); - LinkedNode* cur = _dummyHead; - while(index--) { - cur = cur->next; - } - newNode->next = cur->next; - cur->next = newNode; - _size++; - } - - // 删除第index个节点,如果index 大于等于链表的长度,直接return,注意index是从0开始的 - void deleteAtIndex(int index) { - if (index >= _size || index < 0) { - return; - } - LinkedNode* cur = _dummyHead; - while(index--) { - cur = cur ->next; - } - LinkedNode* tmp = cur->next; - cur->next = cur->next->next; - delete tmp; - _size--; - } - - // 打印链表 - void printLinkedList() { - LinkedNode* cur = _dummyHead; - while (cur->next != nullptr) { - cout << cur->next->val << " "; - cur = cur->next; - } - cout << endl; - } -private: - int _size; - LinkedNode* _dummyHead; - -}; -``` -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0714.买卖股票的最佳时机含手续费.md b/problems/0714.买卖股票的最佳时机含手续费.md deleted file mode 100644 index f9e632d8..00000000 --- a/problems/0714.买卖股票的最佳时机含手续费.md +++ /dev/null @@ -1,150 +0,0 @@ -> - -# 714. 买卖股票的最佳时机含手续费 - -题目链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/ - -给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。 - -你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。 - -返回获得利润的最大值。 - -注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。 - -示例 1: -输入: prices = [1, 3, 2, 8, 4, 9], fee = 2 -输出: 8 - -解释: 能够达到的最大利润: -在此处买入 prices[0] = 1 -在此处卖出 prices[3] = 8 -在此处买入 prices[4] = 4 -在此处卖出 prices[5] = 9 -总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8. - -注意: -* 0 < prices.length <= 50000. -* 0 < prices[i] < 50000. -* 0 <= fee < 50000. - -# 思路 - -本题相对于[贪心算法:122.买卖股票的最佳时机II](https://mp.weixin.qq.com/s/VsTFA6U96l18Wntjcg3fcg),多添加了一个条件就是手续费。 - -## 贪心算法 - -在[贪心算法:122.买卖股票的最佳时机II](https://mp.weixin.qq.com/s/VsTFA6U96l18Wntjcg3fcg)中使用贪心策略不用关心具体什么时候买卖,只要收集每天的正利润,最后稳稳的就是最大利润了。 - -而本题有了手续费,就要关系什么时候买卖了,因为计算所获得利润,需要考虑买卖利润可能不足以手续费的情况。 - -如果使用贪心策略,就是最低值买,最高值(如果算上手续费还盈利)就卖。 - -此时无非就是要找到两个点,买入日期,和卖出日期。 - -* 买入日期:其实很好想,遇到更低点就记录一下。 -* 卖出日期:这个就不好算了,但也没有必要算出准确的卖出日期,只要当前价格大于(最低价格+手续费),就可以收获利润,至于准确的卖出日期,就是连续收获利润区间里的最后一天(并不需要计算是具体哪一天)。 - -所以我们在做收获利润操作的时候其实有三种情况: - -* 情况一:收获利润的这一天并不是收获利润区间里的最后一天(不是真正的卖出,相当于持有股票),所以后面要继续收获利润。 -* 情况二:前一天是收获利润区间里的最后一天(相当于真正的卖出了),今天要重新记录最小价格了。 -* 情况三:不作操作,保持原有状态(买入,卖出,不买不卖) - -贪心算法C++代码如下: - -```C++ -class Solution { -public: - int maxProfit(vector& prices, int fee) { - int result = 0; - int minPrice = prices[0]; // 记录最低价格 - for (int i = 1; i < prices.size(); i++) { - // 情况二:相当于买入 - if (prices[i] < minPrice) minPrice = prices[i]; - - // 情况三:保持原有状态(因为此时买则不便宜,卖则亏本) - if (prices[i] >= minPrice && prices[i] <= minPrice + fee) { - continue; - } - - // 计算利润,可能有多次计算利润,最后一次计算利润才是真正意义的卖出 - if (prices[i] > minPrice + fee) { - result += prices[i] - minPrice - fee; - minPrice = prices[i] - fee; // 情况一,这一步很关键 - } - } - return result; - } -}; -``` - -* 时间复杂度:O(n) -* 空间复杂度:O(1) - -从代码中可以看出对情况一的操作,因为如果还在收获利润的区间里,表示并不是真正的卖出,而计算利润每次都要减去手续费,**所以要让minPrice = prices[i] - fee;,这样在明天收获利润的时候,才不会多减一次手续费!** - -大家也可以发现,情况三,那块代码是可以删掉的,我是为了让代码表达清晰,所以没有精简。 - -## 动态规划 - -我在公众号「代码随想录」里将在下一个系列详细讲解动态规划,所以本题解先给出我的C++代码(带详细注释),感兴趣的同学可以自己先学习一下。 - -相对于[贪心算法:122.买卖股票的最佳时机II](https://mp.weixin.qq.com/s/VsTFA6U96l18Wntjcg3fcg)的动态规划解法中,只需要在计算卖出操作的时候减去手续费就可以了,代码几乎是一样的。 - -C++代码如下: - -```C++ -class Solution { -public: - int maxProfit(vector& prices, int fee) { - // dp[i][1]第i天持有的最多现金 - // dp[i][0]第i天持有股票所剩的最多现金 - int n = prices.size(); - vector> dp(n, vector(2, 0)); - dp[0][0] -= prices[0]; // 持股票 - for (int i = 1; i < n; i++) { - dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]); - dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i] - fee); - } - return max(dp[n - 1][0], dp[n - 1][1]); - } -}; -``` - -* 时间复杂度:O(n) -* 空间复杂度:O(n) - -当然可以对空间经行优化,因为当前状态只是依赖前一个状态。 - -C++ 代码如下: - -```C++ -class Solution { -public: - int maxProfit(vector& prices, int fee) { - int n = prices.size(); - int holdStock = (-1) * prices[0]; // 持股票 - int saleStock = 0; // 卖出股票 - for (int i = 1; i < n; i++) { - int previousHoldStock = holdStock; - holdStock = max(holdStock, saleStock - prices[i]); - saleStock = max(saleStock, previousHoldStock + prices[i] - fee); - } - return saleStock; - } -}; -``` -* 时间复杂度:O(n) -* 空间复杂度:O(1) - -# 总结 - -本题贪心的思路其实是比较难的,动态规划才是常规做法,但也算是给大家拓展一下思路,感受一下贪心的魅力。 - -后期我们在讲解 股票问题系列的时候,会用动规的方式把股票问题穿个线。 - -就酱,学算法,认准「代码随想录」,值得推荐给身边的朋友同学们! - - - diff --git a/problems/0738.单调递增的数字.md b/problems/0738.单调递增的数字.md deleted file mode 100644 index 66e46bc0..00000000 --- a/problems/0738.单调递增的数字.md +++ /dev/null @@ -1,123 +0,0 @@ - -> - -# 738.单调递增的数字 - -给定一个非负整数 N,找出小于或等于 N 的最大的整数,同时这个整数需要满足其各个位数上的数字是单调递增。 - -(当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。) - -示例 1: -输入: N = 10 -输出: 9 - -示例 2: -输入: N = 1234 -输出: 1234 - -示例 3: -输入: N = 332 -输出: 299 - -说明: N 是在 [0, 10^9] 范围内的一个整数。 - -# 思路 - -## 暴力解法 - -题意很简单,那么首先想的就是暴力解法了,来我提大家暴力一波,结果自然是超时! - -代码如下: -```C++ -class Solution { -private: - bool checkNum(int num) { - int max = 10; - while (num) { - int t = num % 10; - if (max >= t) max = t; - else return false; - num = num / 10; - } - return true; - } -public: - int monotoneIncreasingDigits(int N) { - for (int i = N; i > 0; i--) { - if (checkNum(i)) return i; - } - return 0; - } -}; -``` -* 时间复杂度:O(n * m) m为n的数字长度 -* 空间复杂度:O(1) - -## 贪心算法 - -题目要求小于等于N的最大单调递增的整数,那么拿一个两位的数字来举例。 - -例如:98,一旦出现strNum[i - 1] > strNum[i]的情况(非单调递增),首先想让strNum[i - 1]--,然后strNum[i]给为9,这样这个整数就是89,即小于98的最大的单调递增整数。 - -这一点如果想清楚了,这道题就好办了。 - -**局部最优:遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]--,然后strNum[i]给为9,可以保证这两位变成最大单调递增整数**。 - -**全局最优:得到小于等于N的最大单调递增的整数**。 - -**但这里局部最优推出全局最优,还需要其他条件,即遍历顺序,和标记从哪一位开始统一改成9**。 - -此时是从前向后遍历还是从后向前遍历呢? - -从前向后遍历的话,遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]减一,但此时如果strNum[i - 1]减一了,可能又小于strNum[i - 2]。 - -这么说有点抽象,举个例子,数字:332,从前向后遍历的话,那么就把变成了329,此时2又小于了第一位的3了,真正的结果应该是299。 - -**所以从前后向遍历会改变已经遍历过的结果!** - -那么从后向前遍历,就可以重复利用上次比较得出的结果了,从后向前遍历332的数值变化为:332 -> 329 -> 299 - -确定了遍历顺序之后,那么此时局部最优就可以推出全局,找不出反例,试试贪心。 - -C++代码如下: - -```C++ -class Solution { -public: - int monotoneIncreasingDigits(int N) { - string strNum = to_string(N); - // flag用来标记赋值9从哪里开始 - // 设置为这个默认值,为了防止第二个for循环在flag没有被赋值的情况下执行 - int flag = strNum.size(); - for (int i = strNum.size() - 1; i > 0; i--) { - if (strNum[i - 1] > strNum[i] ) { - flag = i; - strNum[i - 1]--; - } - } - for (int i = flag; i < strNum.size(); i++) { - strNum[i] = '9'; - } - return stoi(strNum); - } -}; - -``` - -* 时间复杂度:O(n) n 为数字长度 -* 空间复杂度:O(n) 需要一个字符串,转化为字符串操作更方便 - -# 总结 - -本题只要想清楚个例,例如98,一旦出现strNum[i - 1] > strNum[i]的情况(非单调递增),首先想让strNum[i - 1]减一,strNum[i]赋值9,这样这个整数就是89。就可以很自然想到对应的贪心解法了。 - -想到了贪心,还要考虑遍历顺序,只有从后向前遍历才能重复利用上次比较的结果。 - -最后代码实现的时候,也需要一些技巧,例如用一个flag来标记从哪里开始赋值9。 - -就酱,循序渐进学算法,认准「代码随想录」! - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20201124161234338.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/0739.每日温度.md b/problems/0739.每日温度.md deleted file mode 100644 index 12d9a55f..00000000 --- a/problems/0739.每日温度.md +++ /dev/null @@ -1,54 +0,0 @@ - -# 思路 - -使用递减栈(从大到小 栈底到栈头) - -``` -class Solution { -public: - vector dailyTemperatures(vector& T) { - // 递减栈 - stack st; - vector result(T.size(), 0); - st.push(0); - for (int i = 1; i < T.size(); i++) { - if (T[i] < T[st.top()]) { // 把i放入栈 - st.push(i); - } else if (T[i] == T[st.top()]) { // 相同也要把i放进去,例如 1 1 1 1 2 - st.push(i); - } else { - while (!st.empty() && T[i] > T[st.top()]) { // 注意栈不能为空 - result[st.top()] = i - st.top(); - st.pop(); - } - st.push(i); - } - } - return result; - } -}; -``` -建议一开始 都把每种情况分析好,不要上来看简短的代码,关键逻辑都被隐藏了。 - -精简代码如下: - -``` -class Solution { -public: - vector dailyTemperatures(vector& T) { - stack st; // 递减栈 - vector result(T.size(), 0); - st.push(0); - for (int i = 1; i < T.size(); i++) { - while (!st.empty() && T[i] > T[st.top()]) { // 注意栈不能为空 - result[st.top()] = i - st.top(); - st.pop(); - } - st.push(i); - - } - return result; - } -}; -``` -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0746.使用最小花费爬楼梯.md b/problems/0746.使用最小花费爬楼梯.md deleted file mode 100644 index 12126ffe..00000000 --- a/problems/0746.使用最小花费爬楼梯.md +++ /dev/null @@ -1,106 +0,0 @@ - -这个爬楼梯的问题和斐波那契数列问题很像。 - -读完题大家应该知道指定需要动态规划的,贪心是不可能了。 - -本题在动态规划里相对简单一些,以至于大家可能看个公式就会了,但为了讲清楚思考过程,我按照我总结的动规四步曲来详细讲解: - -1. 确定dp数组以及下标的含义 -2. 确定递推公式 -3. dp数组如何初始化 -4. 确定遍历顺序 - - -* 确定dp数组以及下标的含义 - -使用动态规划,就要有一个数组来记录状态,本题只需要一个一维数组dp[i]就可以了。 - -dp[i]的定义:第i个台阶所花费的最少体力为dp[i]。 - -**对于dp数组的定义,大家一定要清晰!** - -* 确定递推公式 - -**可以有两个途径得到dp[i],一个是dp[i-1] 一个是dp[i-2]**。 - -那么究竟是选dp[i-1]还是dp[i-2]呢? - -一定是选最小的,所以dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]; - -**注意这里为什么是加cost[i],而不是cost[i-1],cost[i-2]之类的**,因为题目中说了:第i个阶梯对应着一个非负数的体力花费值 cost[i] - -* dp数组如何初始化 - -根据dp数组的定义,dp数组初始化其实是比较难的,因为不可能初始化为第i台阶所花费的最少体力。 - -那么看一下递归公式,dp[i]由dp[i-1],dp[i-2]推出,既然初始化所有的dp[i]是不可能的,那么只初始化dp[0]和dp[1]就够了,其他的最终都是dp[0]dp[1]推出。 - -所以初始化代码为: - -``` -vector dp(cost.size()); -dp[0] = cost[0]; -dp[1] = cost[1]; -``` - -* 确定遍历顺序 - -最后一步,递归公式有了,初始化有了,如何遍历呢? - -本题的遍历顺序其实比较简单,简单到很多同学都忽略了思考这一步直接就把代码写出来了。 - -因为是模拟台阶,而且dp[i]又dp[i-1]dp[i-2]推出,所以是从前到后遍历cost数组就可以了。 - -但是稍稍有点难度的动态规划,其遍历顺序并不容易确定下来。 - -例如01背包,都知道两个for循环,一个for遍历物品嵌套一个for遍历背包容量,那么为什么不是一个for遍历背包容量嵌套一个for遍历物品呢? 以及在使用一维dp数组的时候遍历背包容量为什么要倒叙呢? 这些都是遍历顺序息息相关。 - -公众号「代码随想录」后面在讲解动态规划的时候,还会详细说明这些细节,大家可以微信搜一波「代码随想录」,关注后就会发现相见恨晚! - -分析完动规四步曲,整体C++代码如下: - -```C++ -// 版本一 -class Solution { -public: - int minCostClimbingStairs(vector& cost) { - vector dp(cost.size()); - dp[0] = cost[0]; - dp[1] = cost[1]; - for (int i = 2; i < cost.size(); i++) { - dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]; - } - return min(dp[cost.size() - 1], dp[cost.size() - 2]); - } -}; -``` - -* 时间复杂度:O(n) -* 空间复杂度:O(n) - -当然还可以优化空间复杂度,因为dp[i]就是由前两位推出来的,那么也不用dp数组了,C++代码如下: - -```C++ -// 版本二 -class Solution { -public: - int minCostClimbingStairs(vector& cost) { - int dp0 = cost[0]; - int dp1 = cost[1]; - for (int i = 2; i < cost.size(); i++) { - int dpi = min(dp0, dp1) + cost[i]; - dp0 = dp1; // 记录一下前两位 - dp1 = dpi; - } - return min(dp0, dp1); - } -}; - -``` - -* 时间复杂度:O(n) -* 空间复杂度:O(1) - -当然我不建议这么写,能写出版本一就可以了,直观简洁! - - diff --git a/problems/0763.划分字母区间.md b/problems/0763.划分字母区间.md deleted file mode 100644 index b0b3c3c8..00000000 --- a/problems/0763.划分字母区间.md +++ /dev/null @@ -1,77 +0,0 @@ -> 看起来有点难,看仅仅是看起来难而已 - -# 763.划分字母区间 - -题目链接: https://leetcode-cn.com/problems/partition-labels/ - -字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。 - -示例: -输入:S = "ababcbacadefegdehijhklij" -输出:[9,7,8] -解释: -划分结果为 "ababcbaca", "defegde", "hijhklij"。 -每个字母最多出现在一个片段中。 -像 "ababcbacadefegde", "hijhklij" 的划分是错误的,因为划分的片段数较少。 -  -提示: - -* S的长度在[1, 500]之间。 -* S只包含小写字母 'a' 到 'z' 。 - -# 思路 - -一想到分割字符串就想到了回溯,但本题其实不用回溯去暴力搜索。 - -题目要求同一字母最多出现在一个片段中,那么如何把同一个字母的都圈在同一个区间里呢? - -如果没有接触过这种题目的话,还挺有难度的。 - -在遍历的过程中相当于是要找每一个字母的边界,**如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了**。此时前面出现过所有字母,最远也就到这个边界了。 - -可以分为如下两步: - -* 统计每一个字符最后出现的位置 -* 从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点 - -如图: - -![763.划分字母区间](https://img-blog.csdnimg.cn/20201222191924417.png) - -明白原理之后,代码并不复杂,如下: - -```C++ -class Solution { -public: - vector partitionLabels(string S) { - int hash[27] = {0}; // i为字符,hash[i]为字符出现的最后位置 - for (int i = 0; i < S.size(); i++) { // 统计每一个字符最后出现的位置 - hash[S[i] - 'a'] = i; - } - vector result; - int left = 0; - int right = 0; - for (int i = 0; i < S.size(); i++) { - right = max(right, hash[S[i] - 'a']); // 找到字符出现的最远边界 - if (i == right) { - result.push_back(right - left + 1); - left = i + 1; - } - } - return result; - } -}; -``` - -* 时间复杂度:O(n) -* 空间复杂度:O(1) 使用的hash数组是固定大小 - -# 总结 - -这道题目leetcode标记为贪心算法,说实话,我没有感受到贪心,找不出局部最优推出全局最优的过程。就是用最远出现距离模拟了圈字符的行为。 - -但这道题目的思路是很巧妙的,所以有必要介绍给大家做一做,感受一下。 - -就酱,循序渐进寻算法,认准「代码随想录」,直接介绍给身边的朋友同学们! - - diff --git a/problems/0767.重构字符串.md b/problems/0767.重构字符串.md deleted file mode 100644 index 3498f9af..00000000 --- a/problems/0767.重构字符串.md +++ /dev/null @@ -1,60 +0,0 @@ - -> **如果对做了一些题目,对字符串还没有整体了解的话,可以看这篇:[字符串经典题目大总结!](https://mp.weixin.qq.com/s/gtycjyDtblmytvBRFlCZJg),相信字符串各种操作就非常清晰了。** - -扫了一圈题解,感觉大家的解答都比较高端,我来一个朴实无华的版本。 - -分为如下三步: - -* 用map统计频率字符频率 -* 转为vector(即数组)按频率从大到小排序 -* 按奇数位顺序插入,插满之后按偶数位顺序插入 - -**为什么要先按奇数位插入呢?** - -先按奇数位插入,保证最大的字符分散开,因为奇数位总是>=偶数位! - -C++代码如下: - -``` -class Solution { -private: - bool static cmp (const pair& a, const pair& b) { - return a.second > b.second; // 按照频率从大到小排序 - } -public: - string reorganizeString(string S) { - unordered_map umap; - int maxFreq = 0; - for (char s : S) { - umap[s]++; - maxFreq = max(umap[s], maxFreq); - } - if (2 * maxFreq - 1 > S.size()) return ""; - - vector> vec(umap.begin(), umap.end()); - sort(vec.begin(), vec.end(), cmp); // 给频率排个序 - - string result(S); - int index = 0;// 先按奇数位散开 - for (int i = 0; i < vec.size(); i++) { - while (vec[i].second--) { - result[index] = vec[i].first; - index += 2; - if (index >= S.size()) index = 1; // 奇数位插满了插偶数位 - } - } - return result; - } -}; -``` - -* 时间复杂度O(nlogn) -* 空间复杂度O(n) - -关于leetcode统计的击败多少多少用户,大家不必过于在意,想好代码的时间复杂度就够了,这个击败多少用户,多提交几次可能就击败100%了。 -![767. 重构字符串](https://img-blog.csdnimg.cn/202011301035035.png) - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20201124161234338.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/0841.钥匙和房间.md b/problems/0841.钥匙和房间.md deleted file mode 100644 index 16473f1a..00000000 --- a/problems/0841.钥匙和房间.md +++ /dev/null @@ -1,84 +0,0 @@ -# 题目地址 -https://leetcode-cn.com/problems/keys-and-rooms/ - -## 思路 - -其实这道题的本质就是判断各个房间所连成的有向图,说明不用访问所有的房间。 - -如图所示: - - - -示例1就可以访问所有的房间,因为通过房间里的key将房间连在了一起。 - -示例2中,就不能访问所有房间,从图中就可以看出,房间2是一个孤岛,我们从0出发,无论怎么遍历,都访问不到房间2。 - -认清本质问题之后,**使用 广度优先搜索(BFS) 还是 深度优先搜索(DFS) 都是可以的。** - -代码如下: - -## BFS C++代码 - -``` -class Solution { -bool bfs(const vector>& rooms) { - vector visited(rooms.size(), 0); // 标记房间是否被访问过 - visited[0] = 1; // 0 号房间开始 - queue que; - que.push(0); // 0 号房间开始 - - // 广度优先搜索的过程 - while (!que.empty()) { - int key = que.front(); que.pop(); - vector keys = rooms[key]; - for (int key : keys) { - if (!visited[key]) { - que.push(key); - visited[key] = 1; - } - } - } - // 检查房间是不是都遍历过了 - for (int i : visited) { - if (i == 0) return false; - } - return true; - -} -public: - bool canVisitAllRooms(vector>& rooms) { - return bfs(rooms); - } -}; -``` - -## DFS C++代码 - -``` -class Solution { -private: - void dfs(int key, const vector>& rooms, vector& visited) { - if (visited[key]) { - return; - } - visited[key] = 1; - vector keys = rooms[key]; - for (int key : keys) { - // 深度优先搜索遍历 - dfs(key, rooms, visited); - } - } -public: - bool canVisitAllRooms(vector>& rooms) { - vector visited(rooms.size(), 0); - dfs(0, rooms, visited); - //检查是否都访问到了 - for (int i : visited) { - if (i == 0) return false; - } - return true; - } -}; -``` - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0844.比较含退格的字符串.md b/problems/0844.比较含退格的字符串.md deleted file mode 100644 index 47031a70..00000000 --- a/problems/0844.比较含退格的字符串.md +++ /dev/null @@ -1,122 +0,0 @@ -感觉像是使用栈 - -## 思路 - -本文将给出 空间复杂度O(n)的栈模拟方法 以及空间复杂度是O(1)的双指针方法。 - -### 普通方法(使用栈的思路) - -这道题目一看就是要使用栈的节奏,这种匹配(消除)问题也是栈的擅长所在,跟着一起刷题的同学应该知道,在[栈与队列:匹配问题都是栈的强项](https://mp.weixin.qq.com/s/eynAEbUbZoAWrk0ZlEugqg),我就已经提过了一次使用栈来做类似的事情了。 - -**那么本题,确实可以使用栈的思路,但是没有必要使用栈,因为最后比较的时候还要比较栈里的元素,有点麻烦**。 - -这里直接使用字符串string,来作为栈,末尾添加和弹出,string都有相应的接口,最后比较的时候,只要比较两个字符串就可以了,比比较栈里的元素方便一些。 - -代码如下: - -``` -class Solution { -public: - bool backspaceCompare(string S, string T) { - string s; // 当栈来用 - string t; // 当栈来用 - for (int i = 0; i < S.size(); i++) { - if (S[i] != '#') s += S[i]; - else if (!s.empty()) { - s.pop_back(); - - } - for (int i = 0; i < T.size(); i++) { - if (T[i] != '#') t += T[i]; - else if (!t.empty()) { - t.pop_back(); - } - } - if (s == t) return true; // 直接比较两个字符串是否相等,比用栈来比较方便多了 - return false; - } -}; -``` -* 时间复杂度:O(n + m), n为S的长度,m为T的长度 ,也可以理解是O(n)的时间复杂度 -* 空间复杂度:O(n + m) - -当然以上代码,大家可以发现有重复的逻辑处理S,处理T,可以把这块公共逻辑抽离出来,代码精简如下: - -``` -class Solution { -private: -string getString(const string& S) { - string s; - for (int i = 0; i < S.size(); i++) { - if (S[i] != '#') s += S[i]; - else if (!s.empty()) { - s.pop_back(); - } - } - return s; -} -public: - bool backspaceCompare(string S, string T) { - return getString(S) == getString(T); - } -}; -``` -性能依然是: -* 时间复杂度:O(n + m) -* 空间复杂度:O(n + m) - -### 优化方法(从后向前双指针) - -当然还可以有使用 O(1) 的空间复杂度来解决该问题。 - -同时从后向前遍历S和T(i初始为S末尾,j初始为T末尾),记录#的数量,模拟消除的操作,如果#用完了,就开始比较S[i]和S[j]。 - -动画如下: - - - -如果S[i]和S[j]不相同返回false,如果有一个指针(i或者j)先走到的字符串头部位置,也返回false。 - -代码如下: - -``` -class Solution { -public: - bool backspaceCompare(string S, string T) { - int sSkipNum = 0; // 记录S的#数量 - int tSkipNum = 0; // 记录T的#数量 - int i = S.size() - 1; - int j = T.size() - 1; - while (1) { - while (i >= 0) { // 从后向前,消除S的# - if (S[i] == '#') sSkipNum++; - else { - if (sSkipNum > 0) sSkipNum--; - else break; - } - i--; - } - while (j >= 0) { // 从后向前,消除T的# - if (T[j] == '#') tSkipNum++; - else { - if (tSkipNum > 0) tSkipNum--; - else break; - } - j--; - } - // 后半部分#消除完了,接下来比较S[i] != T[j] - if (i < 0 || j < 0) break; // S 或者T 遍历到头了 - if (S[i] != T[j]) return false; - i--;j--; - } - // 说明S和T同时遍历完毕 - if (i == -1 && j == -1) return true; - return false; - } -}; -``` - -* 时间复杂度:O(n + m) -* 空间复杂度:O(1) - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0860.柠檬水找零.md b/problems/0860.柠檬水找零.md deleted file mode 100644 index 21bc66bc..00000000 --- a/problems/0860.柠檬水找零.md +++ /dev/null @@ -1,125 +0,0 @@ - -> 给我来一杯柠檬水 - -# 860.柠檬水找零 - -题目链接:https://leetcode-cn.com/problems/lemonade-change/ - -在柠檬水摊上,每一杯柠檬水的售价为 5 美元。 - -顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。 - -每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。 - -注意,一开始你手头没有任何零钱。 - -如果你能给每位顾客正确找零,返回 true ,否则返回 false 。 - -示例 1: -输入:[5,5,5,10,20] -输出:true -解释: -前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。 -第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。 -第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。 -由于所有客户都得到了正确的找零,所以我们输出 true。 - -示例 2: -输入:[5,5,10] -输出:true - -示例 3: -输入:[10,10] -输出:false - -示例 4: -输入:[5,5,10,10,20] -输出:false -解释: -前 2 位顾客那里,我们按顺序收取 2 张 5 美元的钞票。 -对于接下来的 2 位顾客,我们收取一张 10 美元的钞票,然后返还 5 美元。 -对于最后一位顾客,我们无法退回 15 美元,因为我们现在只有两张 10 美元的钞票。 -由于不是每位顾客都得到了正确的找零,所以答案是 false。 - -提示: - -* 0 <= bills.length <= 10000 -* bills[i] 不是 5 就是 10 或是 20  - -# 思路 - -这是前几天的leetcode每日一题,感觉不错,给大家讲一下。 - -这道题目刚一看,可能会有点懵,这要怎么找零才能保证完整全部账单的找零呢? - -**但仔细一琢磨就会发现,可供我们做判断的空间非常少!** - -只需要维护三种金额的数量,5,10和20。 - -有如下三种情况: - -* 情况一:账单是5,直接收下。 -* 情况二:账单是10,消耗一个5,增加一个10 -* 情况三:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5 - -此时大家就发现 情况一,情况二,都是固定策略,都不用我们来做分析了,而唯一不确定的其实在情况三。 - -而情况三逻辑也不复杂甚至感觉纯模拟就可以了,其实情况三这里是有贪心的。 - -账单是20的情况,为什么要优先消耗一个10和一个5呢? - -**因为美元10只能给账单20找零,而美元5可以给账单10和账单20找零,美元5更万能!** - -所以局部最优:遇到账单20,优先消耗美元10,完成本次找零。全局最优:完成全部账单的找零。 - -局部最优可以推出全局最优,并找不出反例,那么就试试贪心算法! - -C++代码如下: - -```C++ -class Solution { -public: - bool lemonadeChange(vector& bills) { - int five = 0, ten = 0, twenty = 0; - for (int bill : bills) { - // 情况一 - if (bill == 5) five++; - // 情况二 - if (bill == 10) { - if (five <= 0) return false; - ten++; - five--; - } - // 情况三 - if (bill == 20) { - // 优先消耗10美元,因为5美元的找零用处更大,能多留着就多留着 - if (five > 0 && ten > 0) { - five--; - ten--; - twenty++; // 其实这行代码可以删了,因为记录20已经没有意义了,不会用20来找零 - } else if (five >= 3) { - five -= 3; - twenty++; // 同理,这行代码也可以删了 - } else return false; - } - } - return true; - } -}; -``` - -# 总结 - -咋眼一看好像很复杂,分析清楚之后,会发现逻辑其实非常固定。 - -这道题目可以告诉大家,遇到感觉没有思路的题目,可以静下心来把能遇到的情况分析一下,只要分析到具体情况了,一下子就豁然开朗了。 - -如果一直陷入想从整体上寻找找零方案,就会把自己陷进去,各种情况一交叉,只会越想越复杂了。 - -就酱,如果感觉「代码随想录」干货满满,就帮忙宣传一波吧,感激不尽! - - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20201124161234338.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/0879.盈利计划.md b/problems/0879.盈利计划.md deleted file mode 100644 index 00f50de4..00000000 --- a/problems/0879.盈利计划.md +++ /dev/null @@ -1,39 +0,0 @@ - -(待完整) -https://www.cnblogs.com/grandyang/p/11108205.html - -* 如果g >= group[k - 1],则说明犯罪k可以执行,因为人数已经足够了 -* 如果profit[k - 1] >= p,则说明只执行犯罪k就能达到利润p,可以单独执行,接着我们考虑必须执行犯罪k,然后加上前面的利润不低于p - profit[k - 1]的方案,这样凑出的方案利润也会超过p -* 否则犯罪k不能执行,此时只能直接将前面 dp[k - 1][g][p]利润超过p的方案搬过来 - -``` -class Solution { -public: - int profitableSchemes(int G, int P, vector& group, vector& profit) { - int K = group.size(), MOD = 1e9 + 7; - //dp[k][g][p]使用计划[1,2,3,... k],最多g个人,盈利不少于p个方案个数 - vector > > dp(K + 1, vector >(G + 1, vector(P + 1, 0))); - for (int k = 1; k <= K; ++k){ - for (int g = 1; g <= G; ++g){ - for (int p = 0; p <= P; ++p){ - //如果人数g能够执行第k种犯罪(人数g >= 第k种犯罪需要的人数group[k - 1]) - if (g >= group[k - 1]){ - if (profit[k - 1] >= p){ - //如果执行第k种犯罪获得的利润profit[k - 1]不小于p,则可以单独执行该犯罪 - dp[k][g][p] += 1; - } - //将第k个方案产生的利润profit[k]与之前方案产生的利润凑出不少于p - //dp[k - 1][g - group[k]][p > profit[k] ? p - profit[k] : 0]表示在使用[1, k - 1]这些犯罪,使用人数不超过g - group[k],获得路润不少于p - profit[k] - //这样前面的产生p - profit[k]的利润再加上执行第k个犯罪的利润profit[k]就能到达到利润不少于p(注意p 可能大于 profit[k],所以不能为负数) - dp[k][g][p] = (dp[k][g][p] + dp[k - 1][g - group[k - 1]][p > profit[k - 1] ? p - profit[k - 1] : 0]) % MOD; - } - //不执行第k个犯罪,直接将前面的利润超过p的方案搬过来 - dp[k][g][p] = (dp[k][g][p] + dp[k - 1][g][p]) % MOD; - } - } - } - return dp[K][G][P]; - } -}; - -``` diff --git a/problems/0901.股票价格跨度.md b/problems/0901.股票价格跨度.md deleted file mode 100644 index e3b546ea..00000000 --- a/problems/0901.股票价格跨度.md +++ /dev/null @@ -1,39 +0,0 @@ - -这道题也不是单调栈的简单题 - -需要记录 传入的所有price - -注意 股票价格相同的情况 - -需要自己维护一个数组,用stack来记录下表, 因为数组下标从0开始的,注意 栈为空的两种情况(代码中注释)。 - -如果这道题,不用这个应用场景,而是直接给出输入数组的话,会简单一些。 - -这道题目他们都用两个栈来实现,貌似代码简介一些,但我这个最直观。 可以画一个图 - -``` -class StockSpanner { -public: - stack st; - vector prices; - StockSpanner() { - } - - int next(int price) { - prices.push_back(price); - while (!st.empty() && prices[st.top()] <= price) { // 注意这里等于的情况 - st.pop(); - } - int result; - int curPriceIndex = prices.size() - 1; - if (!st.empty()) { // 栈不为空,求差值 - result = curPriceIndex - st.top(); - } else { // 栈为空 - if (prices.size() == 1) result = 1; // 如果是放入第一个元素就是,result是1 - else result = curPriceIndex + 1; // 不是放入的第一个元素了,那么就应该是当前索引+1 - } - st.push(curPriceIndex); - return result; - } -}; -``` diff --git a/problems/0922.按奇偶排序数组II.md b/problems/0922.按奇偶排序数组II.md deleted file mode 100644 index fb04c7ee..00000000 --- a/problems/0922.按奇偶排序数组II.md +++ /dev/null @@ -1,88 +0,0 @@ - -## 思路 -这道题目直接的想法可能是两层for循环再加上used数组表示使用过的元素。这样的的时间复杂度是O(n^2)。 - -### 方法一 -其实这道题可以用很朴实的方法,时间复杂度就就是O(n)了,C++代码如下: - -``` -class Solution { -public: - vector sortArrayByParityII(vector& A) { - vector even(A.size() / 2); // 初始化就确定数组大小,节省开销 - vector odd(A.size() / 2); - vector result(A.size()); - int evenIndex = 0; - int oddIndex = 0; - int resultIndex = 0; - // 把A数组放进偶数数组,和奇数数组 - for (int i = 0; i < A.size(); i++) { - if (A[i] % 2 == 0) even[evenIndex++] = A[i]; - else odd[oddIndex++] = A[i]; - } - // 把偶数数组,奇数数组分别放进result数组中 - for (int i = 0; i < evenIndex; i++) { - result[resultIndex++] = even[i]; - result[resultIndex++] = odd[i]; - } - return result; - } -}; -``` - -时间复杂度:O(n) -空间复杂度:O(n) - -### 方法二 -以上代码我是建了两个辅助数组,而且A数组还相当于遍历了两次,用辅助数组的好处就是思路清晰,优化一下就是不用这两个辅助树,代码如下: - -``` -class Solution { -public: - vector sortArrayByParityII(vector& A) { - vector result(A.size()); - int evenIndex = 0; // 偶数下表 - int oddIndex = 1; // 奇数下表 - for (int i = 0; i < A.size(); i++) { - if (A[i] % 2 == 0) { - result[evenIndex] = A[i]; - evenIndex += 2; - } - else { - result[oddIndex] = A[i]; - oddIndex += 2; - } - } - return result; - } -}; -``` - -时间复杂度O(n) -空间复杂度O(n) - -### 方法三 - -当然还可以在原数组上修改,连result数组都不用了。 - -``` -class Solution { -public: - vector sortArrayByParityII(vector& A) { - int oddIndex = 1; - for (int i = 0; i < A.size(); i += 2) { - if (A[i] % 2 == 1) { // 在偶数位遇到了奇数 - while(A[oddIndex] % 2 != 0) oddIndex += 2; // 在奇数位找一个偶数 - swap(A[i], A[oddIndex]); // 替换 - } - } - return A; - } -}; -``` - -时间复杂度:O(n) -空间复杂度:O(1) - -这里时间复杂度并不是O(n^2),因为偶数位和奇数位都只操作一次,不是n/2 * n/2的关系,而是n/2 + n/2的关系! - diff --git a/problems/0925.长按键入.md b/problems/0925.长按键入.md deleted file mode 100644 index 6c6ba699..00000000 --- a/problems/0925.长按键入.md +++ /dev/null @@ -1,61 +0,0 @@ - -## 思路 - -这道题目一看以为是哈希,仔细一看不行,要有顺序。 - -所以模拟同时遍历两个数组,进行对比就可以了。 - -对比的时候需要一下几点: - -* name[i] 和 typed[j]相同,则i++,j++ (继续向后对比) -* name[i] 和 typed[j]不相同 - * 看是不是第一位就不相同了,也就是j如果等于0,那么直接返回false - * 不是第一位不相同,就让j跨越重复项,移动到重复项之后的位置,再次比较name[i] 和typed[j] - * 如果 name[i] 和 typed[j]相同,则i++,j++ (继续向后对比) - * 不相同,返回false -* 对比完之后有两种情况 - * name没有匹配完,例如name:"pyplrzzzzdsfa" type:"ppyypllr" - * type没有匹配完,例如name:"alex" type:"alexxrrrrssda" - -动画如下: - - - -上面的逻辑想清楚了,不难写出如下C++代码: - -``` -class Solution { -public: - bool isLongPressedName(string name, string typed) { - int i = 0, j = 0; - while (i < name.size() && j < typed.size()) { - if (name[i] == typed[j]) { // 相同则同时向后匹配 - j++; i++; - } else { // 不相同 - if (j == 0) return false; // 如果是第一位就不相同直接返回false - // j跨越重复项,向后移动,同时防止j越界 - while(j < typed.size() && typed[j] == typed[j - 1]) j++; - if (name[i] == typed[j]) { // j跨越重复项之后再次和name[i]匹配 - j++; i++; // 相同则同时向后匹配 - } - else return false; - } - } - // 说明name没有匹配完,例如 name:"pyplrzzzzdsfa" type:"ppyypllr" - if (i < name.size()) return false; - - // 说明type没有匹配完,例如 name:"alex" type:"alexxrrrrssda" - while (j < typed.size()) { - if (typed[j] == typed[j - 1]) j++; - else return false; - } - return true; - } -}; - -``` - -时间复杂度:O(n) -空间复杂度:O(1) - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0941.有效的山脉数组.md b/problems/0941.有效的山脉数组.md deleted file mode 100644 index 85a9c745..00000000 --- a/problems/0941.有效的山脉数组.md +++ /dev/null @@ -1,44 +0,0 @@ -## 题目链接 - -https://leetcode-cn.com/problems/valid-mountain-array/ - -## 思路 - -判断是山峰,主要就是要严格的保存左边到中间,和右边到中间是递增的。 - -这样可以使用两个指针,left和right,让其按照如下规则移动,如图: - - - -**注意这里还是有一些细节,例如如下两点:** - -* 因为left和right是数组下表,移动的过程中注意不要数组越界 -* 如果left或者right没有移动,说明是一个单调递增或者递减的数组,依然不是山峰 - -C++代码如下: - -``` -class Solution { -public: - bool validMountainArray(vector& A) { - if (A.size() < 3) return false; - int left = 0; - int right = A.size() - 1; - - // 注意防止越界 - while (left < A.size() - 1 && A[left] < A[left + 1]) left++; - - // 注意防止越界 - while (right > 0 && A[right] < A[right - 1]) right--; - - // 如果left或者right都在起始位置,说明不是山峰 - if (left == right && left != 0 && right != A.size() - 1) return true; - return false; - } -}; -``` -如果想系统学一学双指针的话, 可以看一下这篇[双指针法:总结篇!](https://mp.weixin.qq.com/s/_p7grwjISfMh0U65uOyCjA) - - -> 我是[程序员Carl](https://github.com/youngyangyang04),组队刷题可以找我,本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),期待你的关注! - diff --git a/problems/0968.监控二叉树.md b/problems/0968.监控二叉树.md deleted file mode 100644 index a5dbdefb..00000000 --- a/problems/0968.监控二叉树.md +++ /dev/null @@ -1,220 +0,0 @@ -# 题目地址 -https://leetcode-cn.com/problems/binary-tree-cameras/ - -## 思路 - -这道题目其实不是那么好理解的,题目举的示例不是很典型,会误以为摄像头必须要放在中间,其实放哪里都可以只要覆盖了就行。 - -这道题目难在两点: - -1. 需要确定遍历方式 -2. 需要状态转移的方程 - -我们之前做动态规划的时候,只要最难的地方在于确定状态转移方程,至于遍历方式无非就是在数组或者二维数组上。 - -**本题并不是动态规划,其本质是贪心,但我们要确定状态转移方式,而且要在树上进行推导,所以难度就上来了,一些同学知道这道题目难,但其实说不上难点究竟在哪。** - -1. 需要确定遍历方式 - -首先先确定遍历方式,才能确定转移方程,那么该如何遍历呢? - -在安排选择摄像头的位置的时候,**我们要从底向上进行推导,因为尽量让叶子节点的父节点安装摄像头,这样摄像头的数量才是最少的**,这也是本道贪心的原理所在! - -如何从低向上推导呢? - -就是后序遍历也就是左右中的顺序,这样就可以从下到上进行推导了。 - -后序遍历代码如下: - -``` - int traversal(TreeNode* cur) { - - // 空节点,该节点有覆盖 - if (终止条件) return ; - - int left = traversal(cur->left); // 左 - int right = traversal(cur->right); // 右 - - 逻辑处理 // 中 - - return ; - } -``` - -**注意在以上代码中我们取了左孩子的返回值,右孩子的返回值,即left 和 right, 以后推导中间节点的状态** - -2. 需要状态转移的方程 - -确定了遍历顺序,再看看这个状态应该如何转移,先来看看每个节点可能有几种状态: - -可以说有如下三种: - -* 该节点无覆盖 -* 本节点有摄像头 -* 本节点有覆盖 - -我们分别有三个数字来表示: - -* 0:该节点无覆盖 -* 1:本节点有摄像头 -* 2:本节点有覆盖 - -大家应该找不出第四个节点的状态了。 - -**一些同学可能会想有没有第四种状态:本节点无摄像头,其实无摄像头就是 无覆盖 或者 有覆盖的状态,所以一共还是三个状态。** - -**那么问题来了,空节点究竟是哪一种状态呢? 空节点表示无覆盖? 表示有摄像头?还是有覆盖呢? ** - -回归本质,为了让摄像头数量最少,我们要尽量让叶子节点的父节点安装摄像头,这样才能摄像头的数量最少。 - -那么空节点不能是无覆盖的状态,这样叶子节点就可以放摄像头了,空节点也不能是有摄像头的状态,这样叶子节点的父节点就没有必要放摄像头了,而是可以把摄像头放在叶子节点的爷爷节点上。 - -**所以空节点的状态只能是有覆盖,这样就可以在叶子节点的父节点放摄像头了** - -接下来就是递推关系。 - -那么递归的终止条件应该是遇到了空节点,此时应该返回2(有覆盖),原因上面已经解释过了。 - -代码如下: - -``` - // 空节点,该节点有覆盖 - if (cur == NULL) return 2; -``` - -递归的函数,以及终止条件已经确定了,再来看单层逻辑处理。 - -主要有如下四类情况: - -1. 情况1:左右节点都有覆盖 - -左孩子有覆盖,右孩子有覆盖,那么此时中间节点应该就是无覆盖的状态了。 - -如图: - - - -代码如下: - -``` - // 左右节点都有覆盖 - if (left == 2 && right == 2) return 0; -``` - -2. 情况2:左右节点至少有一个无覆盖的情况 - -如果是以下情况,则中间节点(父节点)应该放摄像头: - -left == 0 && right == 0 左右节点无覆盖 -left == 1 && right == 0 左节点有摄像头,右节点无覆盖 -left == 0 && right == 1 左节点有无覆盖,右节点摄像头 -left == 0 && right == 2 左节点无覆盖,右节点覆盖 -left == 2 && right == 0 左节点覆盖,右节点无覆盖 - -这个不难理解,毕竟有一个孩子没有覆盖,父节点就应该放摄像头。 - -此时摄像头的数量要加一,并且return 1,代表中间节点放摄像头。 - -代码如下: -``` - if (left == 0 || right == 0) { - result++; - return 1; - } -``` - -3. 情况3:左右节点至少有一个有摄像头 - -如果是以下情况,其实就是 左右孩子节点有一个有摄像头了,那么其父节点就应该是2(覆盖的状态) - -left == 1 && right == 2 左节点有摄像头,右节点有覆盖 -left == 2 && right == 1 左节点有覆盖,右节点有摄像头 -left == 1 && right == 1 左右节点都有摄像头 - -代码如下: - -``` - if (left == 1 || right == 1) return 2; -``` - -**从这个代码中,可以看出,如果left == 1, right == 0 怎么办?其实这种条件在情况2中已经判断过了**,如图: - - - -这种情况也是大多数同学容易迷惑的情况。 - -4. 情况4:头结点没有覆盖 - -以上都处理完了,递归结束之后,可能头结点 还有一个无覆盖的情况,如图: - - - -所以递归结束之后,还要判断根节点,如果没有覆盖,result++,代码如下: - -``` - int minCameraCover(TreeNode* root) { - result = 0; - if (traversal(root) == 0) { // root 无覆盖 - result++; - } - return result; - } -``` - -以上四种情况我们分析完了,代码也差不多了,整体代码如下: - -(**以下我的代码是可以精简的,但是我是为了把情况说清楚,特别把每种情况列出来,因为精简之后的代码读者不好理解。**) - -## C++代码 - -``` -class Solution { -private: - int result; - int traversal(TreeNode* cur) { - - // 空节点,该节点有覆盖 - if (cur == NULL) return 2; - - int left = traversal(cur->left); // 左 - int right = traversal(cur->right); // 右 - - // 情况1 - // 左右节点都有覆盖 - if (left == 2 && right == 2) return 0; - - // 情况2 - // left == 0 && right == 0 左右节点无覆盖 - // left == 1 && right == 0 左节点有摄像头,右节点无覆盖 - // left == 0 && right == 1 左节点有无覆盖,右节点摄像头 - // left == 0 && right == 2 左节点无覆盖,右节点覆盖 - // left == 2 && right == 0 左节点覆盖,右节点无覆盖 - if (left == 0 || right == 0) { - result++; - return 1; - } - - // 情况3 - // left == 1 && right == 2 左节点有摄像头,右节点有覆盖 - // left == 2 && right == 1 左节点有覆盖,右节点有摄像头 - // left == 1 && right == 1 左右节点都有摄像头 - // 其他情况前段代码均已覆盖 - if (left == 1 || right == 1) return 2; - - // 以上代码我没有使用else,主要是为了把各个分支条件展现出来,这样代码有助于读者理解 - // 这个 return -1 逻辑不会走到这里。 - return -1; - } - -public: - int minCameraCover(TreeNode* root) { - result = 0; - // 情况4 - if (traversal(root) == 0) { // root 无覆盖 - result++; - } - return result; - } -}; -``` -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0973.最接近原点的K个点.md b/problems/0973.最接近原点的K个点.md deleted file mode 100644 index 5090614f..00000000 --- a/problems/0973.最接近原点的K个点.md +++ /dev/null @@ -1,83 +0,0 @@ - -## 思路 - -这道题其实我在讲解队列的时候,就已经讲过了,在[栈与队列:求前 K 个高频元素和队列有啥关系?](https://mp.weixin.qq.com/s/8hMwxoE_BQRbzCc7CA8rng)中,我介绍了一种队列, 优先级队列,其实就是大(小)顶堆。 - -大家有精力的话也可以做做[347.前 K 个高频元素](https://mp.weixin.qq.com/s/8hMwxoE_BQRbzCc7CA8rng),347是求前k的高频元素,本题呢,其实是求前k的低频元素。 - -所以套路都是一样一样的。 - -有的同学会用快排,其实快排的话把所有元素都排序了,时间复杂度是O(nlogn),而使用优先级队列时间复杂度为O(nlogk),因为只需要维护k个元素有序。 - -然后就是为什么要定义大顶堆呢? - -因为本地要求最小k个数,每次添加堆,都是从顶部把最大的弹出去,然后堆里留下的就是最小的k个数了。 - -C++代码如下: - -``` -// 版本一 -class Solution { -public: - // 大顶堆比较函数 - class mycomparison { - public: - bool operator()(const pair>& lhs, const pair>& rhs) { - return lhs.first < rhs.first; - } - }; - vector> kClosest(vector>& points, int K) { - // 定义一个大顶堆 - priority_queue>, vector>>, mycomparison> pri_que; - for(int i = 0; i < points.size(); i++) { - int x = points[i][0]; - int y = points[i][1]; - pair> p(x * x + y * y, points[i]); // key:距离,value是(x,y) - pri_que.push(p); - if (pri_que.size() > K) { // 如果队列的大小大于了K,则队列弹出,保证队列的大小一直为k - pri_que.pop(); - } - } - vector> result(K); // 把队列里元素放入数组 - for (int i = 0; i < K; i++) { - result[i] = pri_que.top().second; - pri_que.pop(); - } - return result; - } -}; -``` - -以上是为了完整的体现出优先级队列的定义以及比较过程。 - -如果要简化一下,就用默认的配置就可以。代码如下: - -``` -// 版本二 -class Solution { -public: - vector> kClosest(vector>& points, int K) { - // 默认大顶堆,按照pair的key排序 - priority_queue>, vector>>> pri_que; - for(int i = 0; i < points.size(); i++) { - int x = points[i][0]; - int y = points[i][1]; - pair> p(x * x + y * y, points[i]); // key:距离,value是(x,y) - pri_que.push(p); - if (pri_que.size() > K) { // 如果队列的大小大于了K,则队列弹出,保证队列的大小一直为k - pri_que.pop(); - } - } - vector> result(K); // 把队列里元素放入数组 - for (int i = 0; i < K; i++) { - result[i] = pri_que.top().second; - pri_que.pop(); - } - return result; - } -}; -``` - - -> 我是[程序员Carl](https://github.com/youngyangyang04),组队刷题可以找我,本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),期待你的关注! - diff --git a/problems/0977.有序数组的平方.md b/problems/0977.有序数组的平方.md deleted file mode 100644 index 61af8ab6..00000000 --- a/problems/0977.有序数组的平方.md +++ /dev/null @@ -1,76 +0,0 @@ - -## 思路 - -### 暴力排序 - -最直观的相反,莫过于:每个数平方之后,排个序,美滋滋,代码如下: - -``` -class Solution { -public: - vector sortedSquares(vector& A) { - for (int i = 0; i < A.size(); i++) { - A[i] *= A[i]; - } - sort(A.begin(), A.end()); // 快速排序 - return A; - } -}; -``` - -这个时间复杂度是 O(n + nlogn), 可以说是O(nlogn)的时间复杂度,但为了和下面双指针法算法时间复杂度有鲜明对比,我记为 O(n + nlogn)。 - -### 双指针法 - -数组其实是有序的, 只不过负数平方之后可能成为最大数了。 - -那么数组平方的最大值就在数组的两端,不是最左边就是最右边,不可能是中间。 - -此时可以考虑双指针法了,i指向起始位置,j指向终止位置。 - -定义一个新数组result,和A数组一样的大小,让k指向result数组终止位置。 - -如果`A[i] * A[i] < A[j] * A[j]` 那么`result[k--] = A[j] * A[j];` 。 - -如果`A[i] * A[i] >= A[j] * A[j]` 那么`result[k--] = A[i] * A[i];` 。 - -如动画所示: - - - -不难写出如下代码: - -``` -class Solution { -public: - vector sortedSquares(vector& A) { - int k = A.size() - 1; - vector result(A.size(), 0); - for (int i = 0, j = A.size() - 1; i <= j;) { // 注意这里要i <= j,因为最后要处理两个元素 - if (A[i] * A[i] < A[j] * A[j]) { - result[k--] = A[j] * A[j]; - j--; - } - else { - result[k--] = A[i] * A[i]; - i++; - } - } - return result; - } -}; -``` - -此时的时间复杂度为O(n),相对于暴力排序的解法O(n + nlogn)还是提升不少的。 - -效率如下: - - - -**这里还是说一下,大家不必太在意leetcode上执行用时,打败多少多少用户,这个就是一个玩具,非常不准确。** - -做题的时候自己能分析出来时间复杂度就可以了,至于leetcode上执行用时,大概看一下就行,只要达到最优的时间复杂度就可以了, - -一样的代码多提交几次可能就击败百分之百了..... - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/1002.查找常用字符.md b/problems/1002.查找常用字符.md deleted file mode 100644 index f9506010..00000000 --- a/problems/1002.查找常用字符.md +++ /dev/null @@ -1,112 +0,0 @@ -# 题目地址 -https://leetcode-cn.com/problems/find-common-characters/ - -## 思路 - -这道题意一起就有点绕,不是那么容易懂,其实就是26个小写字符中有字符 在所有字符串里都出现的话,就输出,重复的也算。 - -例如: - -输入:["ll","ll","ll"] -输出:["l","l"] - -这道题目一眼看上去,就是用哈希法,**“小写字符”,“出现频率”, 这些关键字都是为哈希法量身定做的啊** - -首先可以想到的是暴力解法,一个字符串一个字符串去搜,时间复杂度是O(n^m),n是字符串长度,m是有几个字符串。 - -可以看出这是指数级别的时间复杂度,非常高,而且代码实现也不容易,因为要统计 重复的字符,还要适当的替换或者去重。 - -那我们还是哈希法吧。如果对哈希法不了解,可以这这篇文章:[关于哈希表,你该了解这些!](https://mp.weixin.qq.com/s/g8N6WmoQmsCUw3_BaWxHZA)。 - -如果对用数组来做哈希法不了解的话,可以看这篇:[哈希表:可以拿数组当哈希表来用,但哈希值不要太大](https://mp.weixin.qq.com/s/vM6OszkM6L1Mx2Ralm9Dig)。 - -了解了哈希法,理解了数组在哈希法中的应用之后,可以来看解题思路了。 - -整体思路就是统计出搜索字符串里26个字符的出现的频率,然后取每个字符频率最小值,最后转成输出格式就可以了。 - -如图: - - - -先统计第一个字符串所有字符出现的次数,代码如下: - -``` -int hash[26] = {0}; // 用来统计所有字符串里字符出现的最小频率 -for (int i = 0; i < A[0].size(); i++) { // 用第一个字符串给hash初始化 - hash[A[0][i] - 'a']++; -} -``` - -接下来,把其他字符串里字符的出现次数也统计出来一次放在hashOtherStr中。 - -然后hash 和 hashOtherStr 取最小值,这是本题关键所在,此时取最小值,就是 一个字符在所有字符串里出现的最小次数了。 - -代码如下: - -``` -int hashOtherStr[26] = {0}; // 统计除第一个字符串外字符的出现频率 -for (int i = 1; i < A.size(); i++) { - memset(hashOtherStr, 0, 26 * sizeof(int)); - for (int j = 0; j < A[i].size(); j++) { - hashOtherStr[A[i][j] - 'a']++; - } - // 这是关键所在 - for (int k = 0; k < 26; k++) { // 更新hash,保证hash里统计26个字符在所有字符串里出现的最小次数 - hash[k] = min(hash[k], hashOtherStr[k]); - } -} -``` -此时hash里统计着字符在所有字符串里出现的最小次数,那么把hash转正题目要求的输出格式就可以了。 - -代码如下: - -``` -// 将hash统计的字符次数,转成输出形式 -for (int i = 0; i < 26; i++) { - while (hash[i] != 0) { // 注意这里是while,多个重复的字符 - string s(1, i + 'a'); // char -> string - result.push_back(s); - hash[i]--; - } -} -``` - -整体C++代码如下: - -``` -class Solution { -public: - vector commonChars(vector& A) { - vector result; - if (A.size() == 0) return result; - int hash[26] = {0}; // 用来统计所有字符串里字符出现的最小频率 - for (int i = 0; i < A[0].size(); i++) { // 用第一个字符串给hash初始化 - hash[A[0][i] - 'a']++; - } - - int hashOtherStr[26] = {0}; // 统计除第一个字符串外字符的出现频率 - for (int i = 1; i < A.size(); i++) { - memset(hashOtherStr, 0, 26 * sizeof(int)); - for (int j = 0; j < A[i].size(); j++) { - hashOtherStr[A[i][j] - 'a']++; - } - // 更新hash,保证hash里统计26个字符在所有字符串里出现的最小次数 - for (int k = 0; k < 26; k++) { - hash[k] = min(hash[k], hashOtherStr[k]); - } - } - // 将hash统计的字符次数,转成输出形式 - for (int i = 0; i < 26; i++) { - while (hash[i] != 0) { // 注意这里是while,多个重复的字符 - string s(1, i + 'a'); // char -> string - result.push_back(s); - hash[i]--; - } - } - - return result; - } -}; -``` - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/1005.K次取反后最大化的数组和.md b/problems/1005.K次取反后最大化的数组和.md deleted file mode 100644 index 75e8be97..00000000 --- a/problems/1005.K次取反后最大化的数组和.md +++ /dev/null @@ -1,98 +0,0 @@ -> 很多录友都反馈昨天的题目:[贪心算法:跳跃游戏II](https://mp.weixin.qq.com/s/kJBcsJ46DKCSjT19pxrNYg) 很难,这样我就放心了,哈哈,因为我刚刚讲解贪心的时候一些录友会建议我:贪心没有必要单独讲,直接讲动规就可以了。应该不少没有深入接触过贪心的同学,都会感觉就贪心嘛,有啥难的。现在我们可以发现贪心的道理虽然简单,但解决问题都很巧妙,难度上不照动规差多少。 -> 今天是一道简单题,关键在于培养贪心的解题思路! - - -# 1005.K次取反后最大化的数组和 - -题目地址:https://leetcode-cn.com/problems/maximize-sum-of-array-after-k-negations/ - -给定一个整数数组 A,我们只能用以下方法修改该数组:我们选择某个索引 i 并将 A[i] 替换为 -A[i],然后总共重复这个过程 K 次。(我们可以多次选择同一个索引 i。) - -以这种方式修改数组后,返回数组可能的最大和。 - -示例 1: -输入:A = [4,2,3], K = 1 -输出:5 -解释:选择索引 (1,) ,然后 A 变为 [4,-2,3]。 - -示例 2: -输入:A = [3,-1,0,2], K = 3 -输出:6 -解释:选择索引 (1, 2, 2) ,然后 A 变为 [3,1,0,2]。 - -示例 3: -输入:A = [2,-3,-1,5,-4], K = 2 -输出:13 -解释:选择索引 (1, 4) ,然后 A 变为 [2,3,-1,5,4]。 -  -提示: - -* 1 <= A.length <= 10000 -* 1 <= K <= 10000 -* -100 <= A[i] <= 100 - -# 思路 - -本题思路其实比较好想了,如何可以让数组和最大呢? - -贪心的思路,局部最优:让绝对值大的负数变为正数,当前数值达到最大,整体最优:整个数组和达到最大。 - -局部最优可以推出全局最优。 - -那么如果将负数都转变为正数了,K依然大于0,此时的问题是一个有序正整数序列,如何转变K次正负,让 数组和 达到最大。 - -那么又是一个贪心:局部最优:只找数值最小的正整数进行反转,当前数值可以达到最大(例如正整数数组{5, 3, 1},反转1 得到-1 比 反转5得到的-5 大多了),全局最优:整个 数组和 达到最大。 - -虽然这道题目大家做的时候,可能都不会去想什么贪心算法,一鼓作气,就AC了。 - -**我这里其实是为了给大家展现出来 经常被大家忽略的贪心思路,这么一道简单题,就用了两次贪心!** - -那么本题的解题步骤为: - -* 第一步:将数组按照绝对值大小从大到小排序,**注意要按照绝对值的大小** -* 第二步:从前向后遍历,遇到负数将其变为正数,同时K-- -* 第三步:如果K还大于0,那么反复转变数值最小的元素,将K用完 -* 第四步:求和 - -对应C++代码如下: - -```C++ -class Solution { -static bool cmp(int a, int b) { - return abs(a) > abs(b); -} -public: - int largestSumAfterKNegations(vector& A, int K) { - sort(A.begin(), A.end(), cmp); // 第一步 - for (int i = 0; i < A.size(); i++) { // 第二步 - if (A[i] < 0 && K > 0) { - A[i] *= -1; - K--; - } - } - if (K % 2 == 1) A[A.size() - 1] *= -1; // 第三步 - int result = 0; - for (int a : A) result += a; // 第四步 - return result; - } -}; -``` - -# 总结 - -贪心的题目如果简单起来,会让人简单到开始怀疑:本来不就应该这么做么?这也算是算法?我认为这不是贪心? - -本题其实很简单,不会贪心算法的同学都可以做出来,但是我还是全程用贪心的思路来讲解。 - -因为贪心的思考方式一定要有! - -**如果没有贪心的思考方式(局部最优,全局最优),很容易陷入贪心简单题凭感觉做,贪心难题直接不会做,其实这样就锻炼不了贪心的思考方式了**。 - -所以明知道是贪心简单题,也要靠贪心的思考方式来解题,这样对培养解题感觉很有帮助。 - -此时,有没有感觉Carl为了给大家写出优质的题解真的是煞费苦心啊!! 哈哈,还不赶紧帮忙宣传一波「代码随想录」,让更多的小伙伴知道这里,这样Carl也更有动力写下去![加油] - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20201124161234338.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/1047.删除字符串中的所有相邻重复项.md b/problems/1047.删除字符串中的所有相邻重复项.md deleted file mode 100644 index 1962e78d..00000000 --- a/problems/1047.删除字符串中的所有相邻重复项.md +++ /dev/null @@ -1,110 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/remove-all-adjacent-duplicates-in-string/ - -> 匹配问题都是栈的强项 - -# 1047. 删除字符串中的所有相邻重复项 - -给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。 - -在 S 上反复执行重复项删除操作,直到无法继续删除。 - -在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。 - - -示例: -输入:"abbaca" -输出:"ca" -解释: -例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。 -  - -提示: -1 <= S.length <= 20000 -S 仅由小写英文字母组成。 - -# 思路 - -## 题外话 - -这道题目就像是我们玩过的游戏对对碰,如果相同的元素放在挨在一起就要消除。 - -可能我们在玩游戏的时候感觉理所当然应该消除,但程序又怎么知道该如果消除呢,特别是消除之后又有新的元素可能挨在一起。 - -此时游戏的后端逻辑就可以用一个栈来实现(我没有实际考察对对碰或者爱消除游戏的代码实现,仅从原理上进行推断)。 - -游戏开发可能使用栈结构,编程语言的一些功能实现也会使用栈结构,实现函数递归调用就需要栈,但不是每种编程语言都支持递归,例如: - - - -**递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中**,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。 - -相信大家应该遇到过一种错误就是栈溢出,系统输出的异常是`Segmentation fault`(当然不是所有的`Segmentation fault` 都是栈溢出导致的) ,如果你使用了递归,就要想一想是不是无限递归了,那么系统调用栈就会溢出。 - -而且**在企业项目开发中,尽量不要使用递归!**在项目比较大的时候,由于参数多,全局变量等等,使用递归很容易判断不充分return的条件,非常容易无限递归(或者递归层级过深),**造成栈溢出错误(这种问题还不好排查!)** - -好了,题外话over,我们进入正题。 - -## 正题 - -本题要删除相邻相同元素,其实也是匹配问题,相同左元素相当于左括号,相同右元素就是相当于右括号,匹配上了就删除。 - -那么再来看一下本题:可以把字符串顺序放到一个栈中,然后如果相同的话 栈就弹出,这样最后栈里剩下的元素都是相邻不相同的元素了。 - - -如动画所示: - - - -从栈中弹出剩余元素,此时是字符串ac,因为从栈里弹出的元素是倒叙的,所以在对字符串进行反转一下,就得到了最终的结果。 - -## C++代码 - -``` -class Solution { -public: - string removeDuplicates(string S) { - stack st; - for (char s : S) { - if (st.empty() || s != st.top()) { - st.push(s); - } else { - st.pop(); // s 与 st.top()相等的情况 - } - } - string result = ""; - while (!st.empty()) { // 将栈中元素放到result字符串汇总 - result += st.top(); - st.pop(); - } - reverse (result.begin(), result.end()); // 此时字符串需要反转一下 - return result; - - } -}; -``` - -当然可以拿字符串直接作为栈,这样省去了栈还要转为字符串的操作。 - -代码如下: - -``` -class Solution { -public: - string removeDuplicates(string S) { - string result; - for(char s : S) { - if(result.empty() || result.back() != s) { - result.push_back(s); - } - else { - result.pop_back(); - } - } - return result; - } -}; -``` - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/1049.最后一块石头的重量II.md b/problems/1049.最后一块石头的重量II.md deleted file mode 100644 index 17bcdfc8..00000000 --- a/problems/1049.最后一块石头的重量II.md +++ /dev/null @@ -1,63 +0,0 @@ - -# 思路 - -尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,**这样就化解成01背包问题了**。 - -只不过物品的重量为store[i],物品的价值也为store[i]。 - -对应着01背包里的物品重量weight[i]和 物品价值value[i]。 - -接下来进行动规四步曲: - -* 确定dp数组以及下标的含义 - -我习惯直接使用一维dp数组,如果习惯使用二维dp数组的同学可以看这篇: - -dp[j]表示容量(这里说容量更形象,其实就是重量)为j的背包,最多可以背dp[j]这么重的石头。 - -* 确定递推公式 - -dp[j]有两个来源方向,一个是dp[j]自己,一个是dp[j - stones[i]]。 - -那么dp[j]就是取最大的:**dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);** - -一些同学可能看到这dp[j - stones[i]] + stones[i]中 又有- stones[i] 又有+stones[i],看着有点晕乎。 - -还是要牢记dp[j]的含义,要知道dp[j - stones[i]]为 容量为j - stones[i]的背包最大所背重量。 - -* dp数组如何初始化 - -既然 dp[j]中的j表示容量,那么最大容量(重量)是多少呢,就是所有石头的重量和。 - -因为提示中给出1 <= stones.length <= 30,1 <= stones[i] <= 1000,所以最大重量就是30 * 1000 - -如何初始化呢,只要因为重量都不会是负数,所以dp[j]都初始化为0就可以了,这样在递归公式dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);中dp[j]才不会初始值所覆盖。 - -* 确定遍历顺序 - -for循环遍历石头的数量嵌套一个for循环遍历背包容量,且因为是01背包,每一个物品只使用一次,所以遍历背包容量的时候要倒序。 - -具体原因我在[01背包一维数组实现](https://github.com/youngyangyang04/leetcode-master/blob/master/problems/%E8%83%8C%E5%8C%85%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%8001%E8%83%8C%E5%8C%85-2.md)详细讲解过了。大家感兴趣可以去看一下。 - - -最后C++代码如下: -```C++ -class Solution { -public: - int lastStoneWeightII(vector& stones) { - vector dp(30001, 0); - int sum = 0; - for (int i = 0; i < stones.size(); i++) sum += stones[i]; - int target = sum / 2; - for (int i = 0; i < stones.size(); i++) { - for (int j = target; j >= stones[i]; j--) { - dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]); - } - } - return sum - dp[target] - dp[target]; - } -}; -``` - -* 时间复杂度:O(m * n) , m是石头总重量,n为石头块数 -* 空间复杂度:O(m) diff --git a/problems/1207.独一无二的出现次数.md b/problems/1207.独一无二的出现次数.md deleted file mode 100644 index e6488cf1..00000000 --- a/problems/1207.独一无二的出现次数.md +++ /dev/null @@ -1,46 +0,0 @@ - -## 链接 -https://leetcode-cn.com/problems/unique-number-of-occurrences/ - -## 思路 - -这道题目数组在是哈希法中的经典应用,如果对数组在哈希法中的使用还不熟悉的同学可以看这两篇:[数组在哈希法中的应用](https://mp.weixin.qq.com/s/vM6OszkM6L1Mx2Ralm9Dig)和[哈希法:383. 赎金信](https://mp.weixin.qq.com/s/sYZIR4dFBrw_lr3eJJnteQ) - -进而可以学习一下[set在哈希法中的应用](https://mp.weixin.qq.com/s/N9iqAchXreSVW7zXUS4BVA),以及[map在哈希法中的应用](https://mp.weixin.qq.com/s/uVAtjOHSeqymV8FeQbliJQ) - -回归本题,**本题强调了-1000 <= arr[i] <= 1000**,那么就可以用数组来做哈希,arr[i]作为哈希表(数组)的下标,那么arr[i]可以是负数,怎么办?负数不能做数组下标。 - - -**此时可以定义一个2000大小的数组,例如int count[2002];**,统计的时候,将arr[i]统一加1000,这样就可以统计arr[i]的出现频率了。 - -题目中要求的是是否有相同的频率出现,那么需要再定义一个哈希表(数组)用来记录频率是否重复出现过,bool fre[1002]; 定义布尔类型的就可以了,**因为题目中强调1 <= arr.length <= 1000,所以哈希表大小为1000就可以了**。 - -如图所示: - - - - -C++代码如下: - -``` -class Solution { -public: - bool uniqueOccurrences(vector& arr) { - int count[2002] = {0}; // 统计数字出现的频率 - for (int i = 0; i < arr.size(); i++) { - count[arr[i] + 1000]++; - } - bool fre[1002] = {false}; // 看相同频率是否重复出现 - for (int i = 0; i <= 2000; i++) { - if (count[i]) { - if (fre[count[i]] == false) fre[count[i]] = true; - else return false; - } - } - return true; - } -}; -``` -> **我是[程序员Carl](https://github.com/youngyangyang04),更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),你值得关注!** - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** diff --git a/problems/1221.分割平衡字符串.md b/problems/1221.分割平衡字符串.md deleted file mode 100644 index 757c483f..00000000 --- a/problems/1221.分割平衡字符串.md +++ /dev/null @@ -1,19 +0,0 @@ - -这是贪心啊,LRLR 这本身就是平衡子串 , 但要LR这么分割,这是贪心 - - -``` -class Solution { -public: - int balancedStringSplit(string s) { - int result = 0; - int count = 0; - for (int i = 0; i < s.size(); i++) { - if (s[i] == 'R') count++; - else count--; - if (count == 0) result++; - } - return result; - } -}; -``` diff --git a/problems/1356.根据数字二进制下1的数目排序.md b/problems/1356.根据数字二进制下1的数目排序.md deleted file mode 100644 index 6437eb39..00000000 --- a/problems/1356.根据数字二进制下1的数目排序.md +++ /dev/null @@ -1,71 +0,0 @@ - -## 题目链接 -https://leetcode-cn.com/problems/sort-integers-by-the-number-of-1-bits/ - -## 思路 - -这道题其实是考察如何计算一个数的二进制中1的数量。 - -我提供两种方法: - -* 方法一: - -朴实无华挨个计算1的数量,最多就是循环n的二进制位数,32位。 - -``` -int bitCount(int n) { - int count = 0; // 计数器 - while (n > 0) { - if((n & 1) == 1) count++; // 当前位是1,count++ - n >>= 1 ; // n向右移位 - } - return count; -} -``` - -* 方法二 - -这种方法,只循环n的二进制中1的个数次,比方法一高效的多 - -``` -int bitCount(int n) { - int count = 0; - while (n) { - n &= (n - 1); // 清除最低位的1 - count++; - } - return count; -} -``` -以计算12的二进制1的数量为例,如图所示: - - - -下面我就使用方法二,来做这道题目: - -## C++代码 - -``` -class Solution { -private: - static int bitCount(int n) { // 计算n的二进制中1的数量 - int count = 0; - while(n) { - n &= (n -1); // 清除最低位的1 - count++; - } - return count; - } - static bool cmp(int a, int b) { - int bitA = bitCount(a); - int bitB = bitCount(b); - if (bitA == bitB) return a < b; // 如果bit中1数量相同,比较数值大小 - return bitA < bitB; // 否则比较bit中1数量大小 - } -public: - vector sortByBits(vector& arr) { - sort(arr.begin(), arr.end(), cmp); - return arr; - } -}; -``` diff --git a/problems/1365.有多少小于当前数字的数字.md b/problems/1365.有多少小于当前数字的数字.md deleted file mode 100644 index 723547d3..00000000 --- a/problems/1365.有多少小于当前数字的数字.md +++ /dev/null @@ -1,75 +0,0 @@ - -两层for循环暴力查找,时间复杂度明显为O(n^2)。 - -那么我们来看一下如何优化。 - -首先要找小于当前数字的数字,那么从小到大排序之后,该数字之前的数字就都是比它小的了。 - -所以可以定义一个新数组,将数组排个序。 - -**排序之后,其实每一个数值的下标就代表这前面有几个比它小的了**。 - -代码如下: - -``` -vector vec = nums; -sort(vec.begin(), vec.end()); // 从小到大排序之后,元素下标就是小于当前数字的数字 -``` - -此时用一个哈希表hash(本题可以就用一个数组)来做数值和下标的映射。这样就可以通过数值快速知道下标(也就是前面有几个比它小的)。 - -此时有一个情况,就是数值相同怎么办? - -例如,数组:1 2 3 4 4 4 ,第一个数值4的下标是3,第二个数值4的下标是4了。 - -这里就需要一个技巧了,**在构造数组hash的时候,从后向前遍历,这样hash里存放的就是相同元素最左面的数值和下标了**。 -代码如下: - -``` -int hash[101]; -for (int i = vec.size() - 1; i >= 0; i--) { // 从后向前,记录 vec[i] 对应的下标 - hash[vec[i]] = i; -} -``` - -最后在遍历原数组nums,用hash快速找到每一个数值 对应的 小于这个数值的个数。存放在将结果存放在另一个数组中。 - -代码如下: - -``` -// 此时hash里保存的每一个元素数值 对应的 小于这个数值的个数 -for (int i = 0; i < nums.size(); i++) { - vec[i] = hash[nums[i]]; -} -``` - -流程如图: - - - - -关键地方讲完了,整体C++代码如下: -``` -class Solution { -public: - vector smallerNumbersThanCurrent(vector& nums) { - vector vec = nums; - sort(vec.begin(), vec.end()); // 从小到大排序之后,元素下标就是小于当前数字的数字 - int hash[101]; - for (int i = vec.size() - 1; i >= 0; i--) { // 从后向前,记录 vec[i] 对应的下标 - hash[vec[i]] = i; - } - // 此时hash里保存的每一个元素数值 对应的 小于这个数值的个数 - for (int i = 0; i < nums.size(); i++) { - vec[i] = hash[nums[i]]; - } - return vec; - } -}; -``` -可以排序之后加哈希,时间复杂度为O(nlogn) - - -今天上午有事情,题解写的匆忙,下午我会继续完善题解。 - - diff --git a/problems/1382.将二叉搜索树变平衡.md b/problems/1382.将二叉搜索树变平衡.md deleted file mode 100644 index 9d3a2b65..00000000 --- a/problems/1382.将二叉搜索树变平衡.md +++ /dev/null @@ -1,46 +0,0 @@ - -## 题目地址 -https://leetcode-cn.com/problems/balance-a-binary-search-tree/ - -## 思路 - -这道题目,可以中序遍历把二叉树转变为有序数组,然后在根据有序数组构造平衡二叉搜索树。 - -建议做这道题之前,先看如下两篇题解: -* [98.验证二叉搜索树](https://mp.weixin.qq.com/s/8odY9iUX5eSi0eRFSXFD4Q) 学习二叉搜索树的特性 -* [108.将有序数组转换为二叉搜索树](https://mp.weixin.qq.com/s/sy3ygnouaZVJs8lhFgl9mw) 学习如何通过有序数组构造二叉搜索树 - -这两道题目做过之后,本题分分钟就可以做出来了。 - -代码如下: - -``` -class Solution { -private: - vector vec; - // 有序树转成有序数组 - void traversal(TreeNode* cur) { - if (cur == nullptr) { - return; - } - traversal(cur->left); - vec.push_back(cur->val); - traversal(cur->right); - } - 有序数组转平衡二叉树 - TreeNode* getTree(vector& nums, int left, int right) { - if (left > right) return nullptr; - int mid = left + ((right - left) / 2); - TreeNode* root = new TreeNode(nums[mid]); - root->left = getTree(nums, left, mid - 1); - root->right = getTree(nums, mid + 1, right); - return root; - } - -public: - TreeNode* balanceBST(TreeNode* root) { - traversal(root); - return getTree(vec, 0, vec.size() - 1); - } -}; -``` diff --git a/problems/1403.非递增顺序的最小子序列.md b/problems/1403.非递增顺序的最小子序列.md deleted file mode 100644 index 1957678c..00000000 --- a/problems/1403.非递增顺序的最小子序列.md +++ /dev/null @@ -1,25 +0,0 @@ - -你说这是简单题吧,也是这就使用了贪心算法 - -``` -class Solution { -private: - static bool cmp(int a, int b) { - return a > b; - } -public: - vector minSubsequence(vector& nums) { - sort(nums.begin(), nums.end(), cmp); - int sum = 0; - for (int i = 0; i < nums.size(); i++) sum += nums[i]; - vector result; - int resultSum = 0; - for (int i = 0; i < nums.size(); i++) { - resultSum += nums[i]; - result.push_back(nums[i]); - if (resultSum > (sum - resultSum)) break; - } - return result; - } -}; -``` diff --git a/problems/1518.换酒问题.md b/problems/1518.换酒问题.md deleted file mode 100644 index 3dbd7739..00000000 --- a/problems/1518.换酒问题.md +++ /dev/null @@ -1,69 +0,0 @@ - -# 1518.换酒问题 - -小区便利店正在促销,用 numExchange 个空酒瓶可以兑换一瓶新酒。你购入了 numBottles 瓶酒。 - -如果喝掉了酒瓶中的酒,那么酒瓶就会变成空的。 - -请你计算 最多 能喝到多少瓶酒。 - - -![1518.换酒问题](https://img-blog.csdnimg.cn/20201215173958151.png) - -输入:numBottles = 9, numExchange = 3 -输出:13 -解释:你可以用 3 个空酒瓶兑换 1 瓶酒。 -所以最多能喝到 9 + 3 + 1 = 13 瓶酒。 - -![1518.换酒问题](https://img-blog.csdnimg.cn/20201215174130529.png) - -输入:numBottles = 15, numExchange = 4 -输出:19 -解释:你可以用 4 个空酒瓶兑换 1 瓶酒。 -所以最多能喝到 15 + 3 + 1 = 19 瓶酒。 - -示例 3: -输入:numBottles = 5, numExchange = 5 -输出:6 - -示例 4: -输入:numBottles = 2, numExchange = 3 -输出:2 - -提示: - -* 1 <= numBottles <= 100 -* 2 <= numExchange <= 100 - -# 思路 - -这道题目其实是很简单的了,简单到大家都不以为这是贪心算法,哈哈 - -来分析一下: - -局部最优:每次换酒用尽可能多的酒瓶。全局最优:喝到最多的酒。 - -局部最优可以推出全局最优,那么这就是贪心! - -本题其实 - - -每次环境 - -其实思路是简单的,但本题在 - -``` -// 这道题还是有陷阱啊,15 4 这个例子,答案应该是19 而不是18 -class Solution { -public: - int numWaterBottles(int numBottles, int numExchange) { - int result = numBottles; - while (numBottles / numExchange) { - result += numBottles / numExchange; - // 所以不是 numBottles = (numBottles / numExchange) - numBottles = (numBottles / numExchange) + (numBottles % numExchange); - } - return result; - } -}; -``` diff --git a/problems/leetcode的耗时统计.md b/problems/leetcode的耗时统计.md deleted file mode 100644 index 8e38e30e..00000000 --- a/problems/leetcode的耗时统计.md +++ /dev/null @@ -1,8 +0,0 @@ - -还有一些录友会很关心leetcode上的耗时统计。 - -这个是很不准确的,相同的代码多提交几次,大家就知道怎么回事了。 - -leetcode上的计时应该是以4ms为单位,有的多提交几次,多个4ms就多击败50%,所以比较夸张,如果程序运行是几百ms的级别,可以看看leetcode上的耗时,因为它的误差10几ms对最终影响不大。 - -**所以我的题解基本不会写击败百分之多少多少,没啥意义,时间复杂度分析清楚了就可以了**,至于回溯算法不用分析时间复杂度了,都是一样的爆搜,就看谁剪枝厉害了。 diff --git a/problems/二叉树中递归带着回溯.md b/problems/二叉树中递归带着回溯.md deleted file mode 100644 index d6633ec7..00000000 --- a/problems/二叉树中递归带着回溯.md +++ /dev/null @@ -1,163 +0,0 @@ -# 二叉树:以为使用了递归,其实还隐藏着回溯 - -> 补充一波 - -昨天的总结篇中[还在玩耍的你,该总结啦!(本周小结之二叉树)](https://mp.weixin.qq.com/s/QMBUTYnoaNfsVHlUADEzKg),有两处问题需要说明一波。 - -## 求相同的树 - -[还在玩耍的你,该总结啦!(本周小结之二叉树)](https://mp.weixin.qq.com/s/QMBUTYnoaNfsVHlUADEzKg)中求100.相同的树的代码中,我笔误贴出了 求对称树的代码了,细心的同学应该都发现了。 - -那么如下我再给出求100. 相同的树 的代码,如下: - -``` -class Solution { -public: - bool compare(TreeNode* tree1, TreeNode* tree2) { - if (tree1 == NULL && tree2 != NULL) return false; - else if (tree1 != NULL && tree2 == NULL) return false; - else if (tree1 == NULL && tree2 == NULL) return true; - else if (tree1->val != tree2->val) return false; // 注意这里我没有使用else - - // 此时就是:左右节点都不为空,且数值相同的情况 - // 此时才做递归,做下一层的判断 - bool compareLeft = compare(tree1->left, tree2->left); // 左子树:左、 右子树:左 - bool compareRight = compare(tree1->right, tree2->right); // 左子树:右、 右子树:右 - bool isSame = compareLeft && compareRight; // 左子树:中、 右子树:中(逻辑处理) - return isSame; - - } - bool isSameTree(TreeNode* p, TreeNode* q) { - return compare(p, q); - } -}; -``` - -以上的代码相对于:[二叉树:我对称么?](https://mp.weixin.qq.com/s/Kgf0gjvlDlNDfKIH2b1Oxg) 仅仅修改了变量的名字(为了符合判断相同树的语境)和 遍历的顺序。 - -大家应该会体会到:**认清[判断对称树](https://mp.weixin.qq.com/s/Kgf0gjvlDlNDfKIH2b1Oxg)本质之后, 对称树的代码 稍作修改 就可以直接用来AC 100.相同的树。** - -## 递归中隐藏着回溯 - -在[二叉树:找我的所有路径?](https://mp.weixin.qq.com/s/Osw4LQD2xVUnCJ-9jrYxJA)中我强调了本题其实是用到了回溯的,并且给出了第一个版本的代码,把回溯的过程充分的提现了出来。 - -如下的代码充分的体现出回溯:(257. 二叉树的所有路径) - -``` -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; - } -}; -``` - - -如下为精简之后的递归代码:(257. 二叉树的所有路径) -``` -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; - } -}; -``` - -上面的代码,大家貌似感受不到回溯了,其实**回溯就隐藏在traversal(cur->left, path + "->", result);中的 path + "->"。 每次函数调用完,path依然是没有加上"->" 的,这就是回溯了。** - -为了把这份精简代码的回溯过程展现出来,大家可以试一试把: - -``` -if (cur->left) traversal(cur->left, path + "->", result); // 左 回溯就隐藏在这里 -``` - -改成如下代码: - -``` -path += "->"; -traversal(cur->left, path, result); // 左 -``` - -即: - -``` - -if (cur->left) { - path += "->"; - traversal(cur->left, path, result); // 左 -} -if (cur->right) { - path += "->"; - traversal(cur->right, path, result); // 右 -} -``` - -此时就没有回溯了,这个代码就是通过不了的了。 - -如果想把回溯加上,就要 在上面代码的基础上,加上回溯,就可以AC了。 - -``` -if (cur->left) { - path += "->"; - traversal(cur->left, path, result); // 左 - path.pop_back(); // 回溯 - path.pop_back(); -} -if (cur->right) { - path += "->"; - traversal(cur->right, path, result); // 右 - path.pop_back(); // 回溯 - path.pop_back(); -} -``` - -**大家应该可以感受出来,如果把 `path + "->"`作为函数参数就是可以的,因为并有没有改变path的数值,执行完递归函数之后,path依然是之前的数值(相当于回溯了)** - -如果有点遗忘了,建议把这篇[二叉树:找我的所有路径?](https://mp.weixin.qq.com/s/Osw4LQD2xVUnCJ-9jrYxJA)在仔细看一下,然后再看这里的总结,相信会豁然开朗。 - -这里我尽量把逻辑的每一个细节都抠出来展现了,希望对大家有所帮助! - diff --git a/problems/二叉树总结.md b/problems/二叉树总结.md index 299b0e3a..b7b0004f 100644 --- a/problems/二叉树总结.md +++ b/problems/二叉树总结.md @@ -1,4 +1,15 @@ -> 力扣二叉树大总结! +

+ +

+

+ + + + + + +

+ 不知不觉二叉树已经和我们度过了**三十三天**,[「代码随想录」](https://img-blog.csdnimg.cn/20200815195519696.png)里已经发了**三十三篇二叉树的文章**,详细讲解了**30+二叉树经典题目**,一直坚持下来的录友们一定会二叉树有深刻理解了。 diff --git a/problems/二叉树的理论基础.md b/problems/二叉树的理论基础.md index a305f05b..7f6f752f 100644 --- a/problems/二叉树的理论基础.md +++ b/problems/二叉树的理论基础.md @@ -1,3 +1,15 @@ +

+ +

+

+ + + + + + +

+ # 二叉树理论基础 我们要开启新的征程了,大家跟上! diff --git a/problems/二叉树的统一迭代法.md b/problems/二叉树的统一迭代法.md deleted file mode 100644 index fe8466e3..00000000 --- a/problems/二叉树的统一迭代法.md +++ /dev/null @@ -1,136 +0,0 @@ - -> 统一写法是一种什么感觉 - -此时我们在[二叉树:一入递归深似海,从此offer是路人](https://mp.weixin.qq.com/s/PwVIfxDlT3kRgMASWAMGhA)中用递归的方式,实现了二叉树前中后序的遍历。 - -在[二叉树:听说递归能做的,栈也能做!](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg)中用栈实现了二叉树前后中序的迭代遍历(非递归)。 - -之后我们发现**迭代法实现的先中后序,其实风格也不是那么统一,除了先序和后序,有关联,中序完全就是另一个风格了,一会用栈遍历,一会又用指针来遍历。** - -实践过的同学,也会发现使用迭代法实现先中后序遍历,很难写出统一的代码,不像是递归法,实现了其中的一种遍历方式,其他两种只要稍稍改一下节点顺序就可以了。 - -其实**针对三种遍历方式,使用迭代法是可以写出统一风格的代码!** - -**重头戏来了,接下来介绍一下统一写法。** - -我们以中序遍历为例,在[二叉树:听说递归能做的,栈也能做!](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg)中提到说使用栈的话,**无法同时解决访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况**。 - -**那我们就将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。** - -如何标记呢,**就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。** 这种方法也可以叫做标记法。 - -# 迭代法中序遍历 - -中序遍历代码如下:(详细注释) - -``` -class Solution { -public: - 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; - } -}; -``` - -看代码有点抽象我们来看一下动画(中序遍历): - - - -动画中,result数组就是最终结果集。 - -可以看出我们将访问的节点直接加入到栈中,但如果是处理的节点则后面放入一个空节点, 这样只有空节点弹出的时候,才将下一个节点放进结果集。 - -此时我们再来看前序遍历代码。 - -# 迭代法前序遍历 - -迭代法前序遍历代码如下: (**注意此时我们和中序遍历相比仅仅改变了两行代码的顺序**) - -``` -class Solution { -public: - 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; - } -}; -``` - -# 迭代法后序遍历 - -后续遍历代码如下: (**注意此时我们和中序遍历相比仅仅改变了两行代码的顺序**) - -``` -class Solution { -public: - 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; - } -}; -``` - -# 总结 - -此时我们写出了统一风格的迭代法,不用在纠结于前序写出来了,中序写不出来的情况了。 - -但是统一风格的迭代法并不好理解,而且想在面试直接写出来还有难度的。 - -所以大家根据自己的个人喜好,对于二叉树的前中后序遍历,选择一种自己容易理解的递归和迭代法。 - diff --git a/problems/二叉树的迭代遍历.md b/problems/二叉树的迭代遍历.md deleted file mode 100644 index 17403bb3..00000000 --- a/problems/二叉树的迭代遍历.md +++ /dev/null @@ -1,141 +0,0 @@ -> 听说还可以用非递归的方式 - -看完本篇大家可以使用迭代法,再重新解决如下三道leetcode上的题目: - -* 144.二叉树的前序遍历 -* 94.二叉树的中序遍历 -* 145.二叉树的后序遍历 - -为什么可以用迭代法(非递归的方式)来实现二叉树的前后中序遍历呢? - -我们在[栈与队列:匹配问题都是栈的强项](https://mp.weixin.qq.com/s/eynAEbUbZoAWrk0ZlEugqg)中提到了,**递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中**,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。 - -此时大家应该知道我们用栈也可以是实现二叉树的前后中序遍历了。 - -## 前序遍历(迭代法) - -我们先看一下前序遍历。 - -前序遍历是中左右,每次先处理的是中间节点,那么先将跟节点放入栈中,然后将右孩子加入栈,再加入左孩子。 - -为什么要先加入 右孩子,再加入左孩子呢? 因为这样出栈的时候才是中左右的顺序。 - -动画如下: - - - -不难写出如下代码: (**注意代码中空节点不入栈**) - -``` -class Solution { -public: - vector preorderTraversal(TreeNode* root) { - stack st; - vector result; - if (root == NULL) return result; - st.push(root); - while (!st.empty()) { - TreeNode* node = st.top(); // 中 - st.pop(); - result.push_back(node->val); - if (node->right) st.push(node->right); // 右(空节点不入栈) - if (node->left) st.push(node->left); // 左(空节点不入栈) - } - return result; - } -}; -``` - -此时会发现貌似使用迭代法写出前序遍历并不难,确实不难。 - -**此时是不是想改一点前序遍历代码顺序就把中序遍历搞出来了?** - -其实还真不行! - -但接下来,**再用迭代法写中序遍历的时候,会发现套路又不一样了,目前的前序遍历的逻辑无法直接应用到中序遍历上。** - -## 中序遍历(迭代法) - -为了解释清楚,我说明一下 刚刚在迭代的过程中,其实我们有两个操作: - -1. **处理:将元素放进result数组中** -2. **访问:遍历节点** - -分析一下为什么刚刚写的前序遍历的代码,不能和中序遍历通用呢,因为前序遍历的顺序是中左右,先访问的元素是中间节点,要处理的元素也是中间节点,所以刚刚才能写出相对简洁的代码,**因为要访问的元素和要处理的元素顺序是一致的,都是中间节点。** - -那么再看看中序遍历,中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进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(); // 从栈里弹出的数据,就是要处理的数据(放进result数组里的数据) - 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; - if (root == NULL) return result; - st.push(root); - while (!st.empty()) { - TreeNode* node = st.top(); - st.pop(); - result.push_back(node->val); - if (node->left) st.push(node->left); // 相对于前序遍历,这更改一下入栈顺序 (空节点不入栈) - if (node->right) st.push(node->right); // 空节点不入栈 - } - reverse(result.begin(), result.end()); // 将结果反转之后就是左右中的顺序了 - return result; - } -}; - -``` - -# 总结 - -此时我们用迭代法写出了二叉树的前后中序遍历,大家可以看出前序和中序是完全两种代码风格,并不想递归写法那样代码稍做调整,就可以实现前后中序。 - -**这是因为前序遍历中访问节点(遍历节点)和处理节点(将元素放进result数组中)可以同步处理,但是中序就无法做到同步!** - -上面这句话,可能一些同学不太理解,建议自己亲手用迭代法,先写出来前序,再试试能不能写出中序,就能理解了。 - -**那么问题又来了,难道 二叉树前后中序遍历的迭代法实现,就不能风格统一么(即前序遍历 改变代码顺序就可以实现中序 和 后序)?** - -当然可以,这种写法,还不是很好理解,我们将在下一篇文章里重点讲解,敬请期待! - diff --git a/problems/二叉树的递归遍历.md b/problems/二叉树的递归遍历.md deleted file mode 100644 index 1bc9a00b..00000000 --- a/problems/二叉树的递归遍历.md +++ /dev/null @@ -1,100 +0,0 @@ - -# 二叉树: 一入递归深似海,从此offer是路人 - -> 一看就会,一写就废! - -这次我们要好好谈一谈递归,为什么很多同学看递归算法都是“一看就会,一写就废”。 - -主要是对递归不成体系,没有方法论,**每次写递归算法 ,都是靠玄学来写代码**,代码能不能编过都靠运气。 - -**本篇将介绍前后中序的递归写法,一些同学可能会感觉很简单,其实不然,我们要通过简单题目把方法论确定下来,有了方法论,后面才能应付复杂的递归。** - -这里帮助大家确定下来递归算法的三个要素。**每次写递归,都按照这三要素来写,可以保证大家写出正确的递归算法!** - -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); // 中 - } -``` - -此时大家可以做一做leetcode上三道题目,分别是: - -* 144.二叉树的前序遍历 -* 145.二叉树的后序遍历 -* 94.二叉树的中序遍历 - -可能有同学感觉前后中序遍历的递归太简单了,要打迭代法(非递归),别急,我们明天打迭代法,打个通透! - - diff --git a/problems/剑指Offer05.替换空格.md b/problems/剑指Offer05.替换空格.md deleted file mode 100644 index 129044d5..00000000 --- a/problems/剑指Offer05.替换空格.md +++ /dev/null @@ -1,119 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/ti-huan-kong-ge-lcof/ - -> 遇到对字符串或者数组做填充或删除的操作时,都要想想从后向前操作怎么样。 - -# 题目:剑指Offer 05.替换空格 - -请实现一个函数,把字符串 s 中的每个空格替换成"%20"。 - -示例 1: -输入:s = "We are happy." -输出:"We%20are%20happy." - -# 思路 - -如果想把这道题目做到极致,就不要只用额外的辅助空间了! - -首先扩充数组到每个空格替换成"%20"之后的大小。 - -然后从后向前替换空格,也就是双指针法,过程如下: - -i指向新长度的末尾,j指向旧长度的末尾。 - - - -有同学问了,为什么要从后向前填充,从前向后填充不行么? - -从前向后填充就是O(n^2)的算法了,因为每次添加元素都要将添加元素之后的所有元素向后移动。 - -**其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。** - -这么做有两个好处: - -1. 不用申请新数组。 -2. 从后向前填充元素,避免了从前先后填充元素要来的 每次添加元素都要将添加元素之后的所有元素向后移动。 - -时间复杂度,空间复杂度均超过100%的用户。 - - - -## C++代码 - -``` -class Solution { -public: - string replaceSpace(string s) { - int count = 0; // 统计空格的个数 - int sOldSize = s.size(); - for (int i = 0; i < s.size(); i++) { - if (s[i] == ' ') { - count++; - } - } - // 扩充字符串s的大小,也就是每个空格替换成"%20"之后的大小 - s.resize(s.size() + count * 2); - int sNewSize = s.size(); - // 从后先前将空格替换为"%20" - for (int i = sNewSize - 1, j = sOldSize - 1; j < i; i--, j--) { - if (s[j] != ' ') { - s[i] = s[j]; - } else { - s[i] = '0'; - s[i - 1] = '2'; - s[i - 2] = '%'; - i -= 2; - } - } - return s; - } -}; - -``` -时间复杂度:O(n) -空间复杂度:O(1) - -此时算上本题,我们已经做了七道双指针相关的题目了分别是: - -* [27.移除元素](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA) -* [15.三数之和](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A) -* [18.四数之和](https://mp.weixin.qq.com/s/nQrcco8AZJV1pAOVjeIU_g) -* [206.翻转链表](https://mp.weixin.qq.com/s/pnvVP-0ZM7epB8y3w_Njwg) -* [142.环形链表II](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA) -* [344.反转字符串](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA) - -# 拓展 - -这里也给大家拓展一下字符串和数组有什么差别, - -字符串是若干字符组成的有限序列,也可以理解为是一个字符数组,但是很多语言对字符串做了特殊的规定,接下来我来说一说C/C++中的字符串。 - -在C语言中,把一个字符串存入一个数组时,也把结束符 '\0'存入数组,并以此作为该字符串是否结束的标志。 - -例如这段代码: - -``` -char a[5] = "asd"; -for (int i = 0; a[i] != '\0'; i++) { -} -``` - -在C++中,提供一个string类,string类会提供 size接口,可以用来判断string类字符串是否结束,就不用'\0'来判断是否结束。 - -例如这段代码: - -``` -string a = "asd"; -for (int i = 0; i < a.size(); i++) { -} -``` - -那么vector< char > 和 string 又有什么区别呢? - -其实在基本操作上没有区别,但是 string提供更多的字符串处理的相关接口,例如string 重载了+,而vector却没有。 - -所以想处理字符串,我们还是会定义一个string类型。 - - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/剑指Offer52.两个链表的第一个公共节点.md b/problems/剑指Offer52.两个链表的第一个公共节点.md deleted file mode 100644 index eadf3af4..00000000 --- a/problems/剑指Offer52.两个链表的第一个公共节点.md +++ /dev/null @@ -1,2 +0,0 @@ - -面试题02.07.链表相交 重复题目 diff --git a/problems/剑指Offer58-I.翻转单词顺序.md b/problems/剑指Offer58-I.翻转单词顺序.md deleted file mode 100644 index 5267fa11..00000000 --- a/problems/剑指Offer58-I.翻转单词顺序.md +++ /dev/null @@ -1,2 +0,0 @@ - -详见:[0151.翻转字符串里的单词](https://github.com/youngyangyang04/leetcode/blob/master/problems/0151.翻转字符串里的单词.md) diff --git a/problems/剑指Offer58-II.左旋转字符串.md b/problems/剑指Offer58-II.左旋转字符串.md deleted file mode 100644 index 3429ee99..00000000 --- a/problems/剑指Offer58-II.左旋转字符串.md +++ /dev/null @@ -1,87 +0,0 @@ -# 题目地址 -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 - -如图: - - - -最终得到左旋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)类似,但是也是一种新的思路。 - -好了,反转字符串一共就介绍到这里,相信大家此时对反转字符串的常见操作已经很了解了。 - -# 题外话 - -一些同学热衷于使用substr,来做这道题。 -其实使用substr 和 反转 时间复杂度是一样的 ,都是O(n),但是使用substr申请了额外空间,所以空间复杂度是O(n),而反转方法的空间复杂度是O(1)。 - -**如果想让这套题目有意义,就不要申请额外空间。** - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/剑指Offer59-I.滑动窗口的最大值.md b/problems/剑指Offer59-I.滑动窗口的最大值.md deleted file mode 100644 index ec8fee37..00000000 --- a/problems/剑指Offer59-I.滑动窗口的最大值.md +++ /dev/null @@ -1,45 +0,0 @@ - -原理:[0239.滑动窗口最大值](https://github.com/youngyangyang04/leetcode/blob/master/problems/0239.滑动窗口最大值.md) |滑动窗口/队列 |困难| **单调队列**| - -``` -class Solution { -public: - class MyQueue { //单调队列(从大到小) - public: - deque que; // 使用deque来实现单调队列 - void pop(int value) { - if (!que.empty() && value == que.front()) { - que.pop_front(); - } - } - void push(int value) { - while (!que.empty() && value > que.back()) { - que.pop_back(); - } - que.push_back(value); - - } - int front() { - return que.front(); - } - }; - vector maxSlidingWindow(vector& nums, int k) { - MyQueue que; - vector result; - if (nums.empty()) { - return result; - } - for (int i = 0; i < k; i++) { // 先将前k的元素放进队列 - que.push(nums[i]); - } - result.push_back(que.front()); // result 记录前k的元素的最大值 - for (int i = k; i < nums.size(); i++) { - que.pop(nums[i - k]); // 模拟滑动窗口的移动 - que.push(nums[i]); // 模拟滑动窗口的移动 - result.push_back(que.front()); // 记录对应的最大值 - } - return result; - } -}; - -``` diff --git a/problems/双指针总结.md b/problems/双指针总结.md index 166bfe56..c98eefb3 100644 --- a/problems/双指针总结.md +++ b/problems/双指针总结.md @@ -1,3 +1,15 @@ +

+ +

+

+ + + + + + +

+ > 又是一波总结 相信大家已经对双指针法很熟悉了,但是双指针法并不隶属于某一种数据结构,我们在讲解数组,链表,字符串都用到了双指针法,所有有必要针对双指针法做一个总结。 diff --git a/problems/哈希表总结.md b/problems/哈希表总结.md index edecef77..597e2830 100644 --- a/problems/哈希表总结.md +++ b/problems/哈希表总结.md @@ -1,4 +1,16 @@ +

+ +

+

+ + + + + + +

+ > 哈希表总结篇如约而至 哈希表系列也是早期讲解的时候没有写总结篇,所以选个周末给补上,毕竟「代码随想录」的系列怎么能没有总结篇呢[机智]。 diff --git a/problems/哈希表理论基础.md b/problems/哈希表理论基础.md new file mode 100644 index 00000000..4c8ea1b2 --- /dev/null +++ b/problems/哈希表理论基础.md @@ -0,0 +1,134 @@ +

+ +

+

+ + + + + + +

+ +# 哈希表 + +首先什么是 哈希表,哈希表(英文名字为Hash table,国内也有一些算法书籍翻译为散列表,大家看到这两个名称知道都是指hash table就可以了)。 + +> 哈希表是根据关键码的值而直接进行访问的数据结构。 + +这么这官方的解释可能有点懵,其实直白来讲其实数组就是一张哈希表。 + +哈希表中关键码就是数组的索引下表,然后通过下表直接访问数组中的元素,如下图所示: + +![哈希表1](https://img-blog.csdnimg.cn/20210104234805168.png) + +那么哈希表能解决什么问题呢,**一般哈希表都是用来快速判断一个元素是否出现集合里。** + +例如要查询一个名字是否在这所学校里。 + +要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1) 就可以做到。 + +我们只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。 + +将学生姓名映射到哈希表上就涉及到了**hash function ,也就是哈希函数**。 + +# 哈希函数 + +哈希函数,把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下表快速知道这位同学是否在这所学校里了。 + +哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。 + +![哈希表2](https://img-blog.csdnimg.cn/2021010423484818.png) + +如果hashCode得到的数值大于 哈希表的大小了,也就是大于tableSize了,怎么办呢? + +此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,就要我们就保证了学生姓名一定可以映射到哈希表上了。 + +此时问题又来了,哈希表我们刚刚说过,就是一个数组。 + +如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表 同一个索引下表的位置。 + +接下来**哈希碰撞**登场 + +# 哈希碰撞 + +如图所示,小李和小王都映射到了索引下表 1的位置,**这一现象叫做哈希碰撞**。 + +![哈希表3](https://img-blog.csdnimg.cn/2021010423494884.png) + +一般哈希碰撞有两种解决方法, 拉链法和线性探测法。 + +## 拉链法 + +刚刚小李和小王在索引1的位置发生了冲突,发生冲突的元素都被存储在链表中。 这样我们就可以通过索引找到小李和小王了 + +![哈希表4](https://img-blog.csdnimg.cn/20210104235015226.png) + +(数据规模是dataSize, 哈希表的大小为tableSize) + +其实拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。 + +## 线性探测法 + +使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。 + +例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放 冲突的数据了。如图所示: + +![哈希表5](https://img-blog.csdnimg.cn/20210104235109950.png) + +其实关于哈希碰撞还有非常多的细节,感兴趣的同学可以再好好研究一下,这里我就不再赘述了。 + +# 常见的三种哈希结构 + +当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。 + +* 数组 +* set (集合) +* map(映射) + +这里数组就没啥可说的了,我们来看一下set。 + +在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示: + +|集合 |底层实现 | 是否有序 |数值是否可以重复 | 能否更改数值|查询效率 |增删效率| +|---|---| --- |---| --- | --- | ---| +|std::set |红黑树 |有序 |否 |否 | O(logn)|O(logn) | +|std::multiset | 红黑树|有序 |是 | 否| O(logn) |O(logn) | +|std::unordered_set |哈希表 |无序 |否 |否 |O(1) | O(1)| + +std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。 + +|映射 |底层实现 | 是否有序 |数值是否可以重复 | 能否更改数值|查询效率 |增删效率| +|---|---| --- |---| --- | --- | ---| +|std::map |红黑树 |key有序 |key不可重复 |key不可修改 | O(logn)|O(logn) | +|std::multimap | 红黑树|key有序 | key可重复 | key不可修改|O(logn) |O(logn) | +|std::unordered_map |哈希表 | key无序 |key不可重复 |key不可修改 |O(1) | O(1)| + +std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。 + +当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。 + +那么再来看一下map ,在map 是一个key value 的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。 + +其他语言例如:java里的HashMap ,TreeMap 都是一样的原理。可以灵活贯通。 + +虽然std::set、std::multiset 的底层实现是红黑树,不是哈希表,但是std::set、std::multiset 依然使用哈希函数来做映射,只不过底层的符号表使用了红黑树来存储数据,所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 map也是一样的道理。 + +这里在说一下,一些C++的经典书籍上 例如STL源码剖析,说到了hash_set hash_map,这个与unordered_set,unordered_map又有什么关系呢? + +实际上功能都是一样一样的, 但是unordered_set在C++11的时候被引入标准库了,而hash_set并没有,所以建议还是使用unordered_set比较好,这就好比一个是官方认证的,hash_set,hash_map 是C++11标准之前民间高手自发造的轮子。 + +![哈希表6](https://img-blog.csdnimg.cn/20210104235134572.png) + +# 总结 + +总结一下,**当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法**。 + +但是哈希法也是**牺牲了空间换取了时间**,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。 + +如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法! + +预告下篇讲解一波哈希表面试题的解题套路,我们下期见! + +都看到这了,还有sei!sei没读懂单独找我! + diff --git a/problems/回溯总结.md b/problems/回溯总结.md index d8b9ed06..7090c364 100644 --- a/problems/回溯总结.md +++ b/problems/回溯总结.md @@ -1,3 +1,15 @@ +

+ +

+

+ + + + + + +

+ > 20张树形结构图、14道精选回溯题目,21篇回溯法精讲文章,由浅入深,一气呵成,这是全网最强回溯算法总结! # 回溯法理论基础 diff --git a/problems/回溯算法去重问题的另一种写法.md b/problems/回溯算法去重问题的另一种写法.md deleted file mode 100644 index aa022631..00000000 --- a/problems/回溯算法去重问题的另一种写法.md +++ /dev/null @@ -1,245 +0,0 @@ - -# 本周小结!(回溯算法系列三)续集 - -> 在 [本周小结!(回溯算法系列三)](https://mp.weixin.qq.com/s/tLkt9PSo42X60w8i94ViiA) 中一位录友对 整颗树的本层和同一节点的本层有疑问,也让我重新思考了一下,发现这里确实有问题,所以专门写一篇来纠正,感谢录友们的积极交流哈! - -接下来我再把这块再讲一下。 - -在[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)中的去重和 [回溯算法:递增子序列](https://mp.weixin.qq.com/s/ePxOtX1ATRYJb2Jq7urzHQ)中的去重 都是 同一父节点下本层的去重。 - -[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)也可以使用set针对同一父节点本层去重,但子集问题一定要排序,为什么呢? - -我用没有排序的集合{2,1,2,2}来举例子画一个图,如图: - -![90.子集II2](https://img-blog.csdnimg.cn/2020111316440479.png) - -图中,大家就很明显的看到,子集重复了。 - -那么下面我针对[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ) 给出使用set来对本层去重的代码实现。 - -# 90.子集II - -used数组去重版本: [回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ) - -使用set去重的版本如下: - -```C++ -class Solution { -private: - vector> result; - vector path; - void backtracking(vector& nums, int startIndex, vector& used) { - result.push_back(path); - unordered_set uset; // 定义set对同一节点下的本层去重 - for (int i = startIndex; i < nums.size(); i++) { - if (uset.find(nums[i]) != uset.end()) { // 如果发现出现过就pass - continue; - } - uset.insert(nums[i]); // set跟新元素 - path.push_back(nums[i]); - backtracking(nums, i + 1, used); - path.pop_back(); - } - } - -public: - vector> subsetsWithDup(vector& nums) { - result.clear(); - path.clear(); - vector used(nums.size(), false); - sort(nums.begin(), nums.end()); // 去重需要排序 - backtracking(nums, 0, used); - return result; - } -}; - -``` - -针对留言区录友们的疑问,我再补充一些常见的错误写法, - - -## 错误写法一 - -把uset定义放到类成员位置,然后模拟回溯的样子 insert一次,erase一次。 - -例如: - -```C++ -class Solution { -private: - vector> result; - vector path; - unordered_set uset; // 把uset定义放到类成员位置 - void backtracking(vector& nums, int startIndex, vector& used) { - result.push_back(path); - - for (int i = startIndex; i < nums.size(); i++) { - if (uset.find(nums[i]) != uset.end()) { - continue; - } - uset.insert(nums[i]); // 递归之前insert - path.push_back(nums[i]); - backtracking(nums, i + 1, used); - path.pop_back(); - uset.erase(nums[i]); // 回溯再erase - } - } - -``` - -在树形结构中,**如果把unordered_set uset放在类成员的位置(相当于全局变量),就把树枝的情况都记录了,不是单纯的控制某一节点下的同一层了**。 - -如图: - -![90.子集II1](https://img-blog.csdnimg.cn/202011131625054.png) - -可以看出一旦把unordered_set uset放在类成员位置,它控制的就是整棵树,包括树枝。 - -**所以这么写不行!** - -## 错误写法二 - -有同学把 unordered_set uset; 放到类成员位置,然后每次进入单层的时候用uset.clear()。 - -代码如下: - -```C++ -class Solution { -private: - vector> result; - vector path; - unordered_set uset; // 把uset定义放到类成员位置 - void backtracking(vector& nums, int startIndex, vector& used) { - result.push_back(path); - uset.clear(); // 到每一层的时候,清空uset - for (int i = startIndex; i < nums.size(); i++) { - if (uset.find(nums[i]) != uset.end()) { - continue; - } - uset.insert(nums[i]); // set记录元素 - path.push_back(nums[i]); - backtracking(nums, i + 1, used); - path.pop_back(); - } - } -``` -uset已经是全局变量,本层的uset记录了一个元素,然后进入下一层之后这个uset(和上一层是同一个uset)就被清空了,也就是说,层与层之间的uset是同一个,那么就会相互影响。 - -**所以这么写依然不行!** - -**组合问题和排列问题,其实也可以使用set来对同一节点下本层去重,下面我都分别给出实现代码**。 - -# 40. 组合总和 II - -使用used数组去重版本:[回溯算法:求组合总和(三)](https://mp.weixin.qq.com/s/_1zPYk70NvHsdY8UWVGXmQ) - -使用set去重的版本如下: - -```C++ -class Solution { -private: - vector> result; - vector path; - void backtracking(vector& candidates, int target, int sum, int startIndex) { - if (sum == target) { - result.push_back(path); - return; - } - unordered_set uset; // 控制某一节点下的同一层元素不能重复 - for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) { - if (uset.find(candidates[i]) != uset.end()) { - continue; - } - uset.insert(candidates[i]); // 记录元素 - sum += candidates[i]; - path.push_back(candidates[i]); - backtracking(candidates, target, sum, i + 1); - sum -= candidates[i]; - path.pop_back(); - } - } - -public: - vector> combinationSum2(vector& candidates, int target) { - path.clear(); - result.clear(); - sort(candidates.begin(), candidates.end()); - backtracking(candidates, target, 0, 0); - return result; - } -}; -``` - -# 47. 全排列 II - -使用used数组去重版本:[回溯算法:排列问题(二)](https://mp.weixin.qq.com/s/9L8h3WqRP_h8LLWNT34YlA) - -使用set去重的版本如下: - -```C++ -class Solution { -private: - vector> result; - vector path; - void backtracking (vector& nums, vector& used) { - if (path.size() == nums.size()) { - result.push_back(path); - return; - } - unordered_set uset; // 控制某一节点下的同一层元素不能重复 - for (int i = 0; i < nums.size(); i++) { - if (uset.find(nums[i]) != uset.end()) { - continue; - } - if (used[i] == false) { - uset.insert(nums[i]); // 记录元素 - used[i] = true; - path.push_back(nums[i]); - backtracking(nums, used); - path.pop_back(); - used[i] = false; - } - } - } -public: - vector> permuteUnique(vector& nums) { - result.clear(); - path.clear(); - sort(nums.begin(), nums.end()); // 排序 - vector used(nums.size(), false); - backtracking(nums, used); - return result; - } -}; -``` - -# 两种写法的性能分析 - -需要注意的是:**使用set去重的版本相对于used数组的版本效率都要低很多**,大家在leetcode上提交,能明显发现。 - -原因在[回溯算法:递增子序列](https://mp.weixin.qq.com/s/ePxOtX1ATRYJb2Jq7urzHQ)中也分析过,主要是因为程序运行的时候对unordered_set 频繁的insert,unordered_set需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)相对费时间,而且insert的时候其底层的符号表也要做相应的扩充,也是费时的。 - -**而使用used数组在时间复杂度上几乎没有额外负担!** - -**使用set去重,不仅时间复杂度高了,空间复杂度也高了**,在[本周小结!(回溯算法系列三)](https://mp.weixin.qq.com/s/tLkt9PSo42X60w8i94ViiA)中分析过,组合,子集,排列问题的空间复杂度都是O(n),但如果使用set去重,空间复杂度就变成了O(n^2),因为每一层递归都有一个set集合,系统栈空间是n,每一个空间都有set集合。 - -那有同学可能疑惑 用used数组也是占用O(n)的空间啊? - -used数组可是全局变量,每层与每层之间公用一个used数组,所以空间复杂度是O(n + n),最终空间复杂度还是O(n)。 - -# 总结 - -本篇本打算是对[本周小结!(回溯算法系列三)](https://mp.weixin.qq.com/s/tLkt9PSo42X60w8i94ViiA)的一个点做一下纠正,没想到又写出来这么多! - -**这个点都源于一位录友的疑问,然后我思考总结了一下,就写着这一篇,所以还是得多交流啊!** - -如果大家对「代码随想录」文章有什么疑问,尽管打卡留言的时候提出来哈,或者在交流群里提问。 - -**其实这就是相互学习的过程,交流一波之后都对题目理解的更深刻了,我如果发现文中有问题,都会在评论区或者下一篇文章中即时修正,保证不会给大家带跑偏!** - -就酱,「代码随想录」一直都是干货满满,公众号里的一抹清流,值得推荐给身边的每一位同学朋友! - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/回溯算法理论基础.md b/problems/回溯算法理论基础.md index d3581599..f93d2975 100644 --- a/problems/回溯算法理论基础.md +++ b/problems/回溯算法理论基础.md @@ -1,3 +1,15 @@ +

+ +

+

+ + + + + + +

+ > 今天开始正式回溯法的讲解,老规矩,先概述 diff --git a/problems/子集组合分割排列棋盘问题总结.md b/problems/子集组合分割排列棋盘问题总结.md deleted file mode 100644 index e69de29b..00000000 diff --git a/problems/字符串总结.md b/problems/字符串总结.md index 98d9b232..ba6ca736 100644 --- a/problems/字符串总结.md +++ b/problems/字符串总结.md @@ -1,4 +1,15 @@ -> 该做一个总结了 +

+ +

+

+ + + + + + +

+ # 字符串:总结篇 diff --git a/problems/导读.md b/problems/导读.md deleted file mode 100644 index 1908d048..00000000 --- a/problems/导读.md +++ /dev/null @@ -1,196 +0,0 @@ -很多刚刚关注「代码随想录」的录友,表示想从头开始打卡学习。 - -**打卡方式**就是在文章留言区记录:第n天打卡(或者第n次打卡)+自己的总结。很多录友都正在从头开始打卡,看看留言就知道了,你并不孤独,哈哈。 - -**以下是我整理的文章列表,每个系列都排好了顺序,文章顺序即刷题顺序,这是全网最详细的刷题顺序了,所以录友们挨个看就OK!** - -文章留言区的想法和总结都非常不错,大家也可以看看留言作为拓展和补充,最好同时也写一写自己的想法。 - -如果对文章有疑问,留言区一般都有相应的解答了,或者可以打卡的时候直接留言,留言的疑问我都会看到。 - -**准备好了么? 开启征程,gogogo** - -# 文章篇 - -* 编程素养 - * [看了这么多代码,谈一谈代码风格!](https://mp.weixin.qq.com/s/UR9ztxz3AyL3qdHn_zMbqw) - -* 求职 - * [程序员的简历应该这么写!!(附简历模板)](https://mp.weixin.qq.com/s/nCTUzuRTBo1_R_xagVszsA) - * [互联网大厂技术面试流程和注意事项](https://mp.weixin.qq.com/s/815qCyFGVIxwut9I_7PNFw) - * [北京有这些互联网公司,你都知道么?](https://mp.weixin.qq.com/s/BKrjK4myNB-FYbMqW9f3yw) - * [上海有这些互联网公司,你都知道么?]() - * [深圳有这些互联网公司,你都知道么?](https://mp.weixin.qq.com/s/3VJHF2zNohBwDBxARFIn-Q) - * [广州有这些互联网公司,你都知道么?](https://mp.weixin.qq.com/s/Ir_hQP0clbnvHrWzDL-qXg) - * [成都有这些互联网公司,你都知道么?](https://mp.weixin.qq.com/s/Y9Qg22WEsBngs8B-K8acqQ) - - -* 算法性能分析 - * [关于时间复杂度,你不知道的都在这里!](https://mp.weixin.qq.com/s/LWBfehW1gMuEnXtQjJo-sw) - * [O(n)的算法居然超时了,此时的n究竟是多大?](https://mp.weixin.qq.com/s/73ryNsuPFvBQkt6BbhNzLA) - * [通过一道面试题目,讲一讲递归算法的时间复杂度!](https://mp.weixin.qq.com/s/I6ZXFbw09NR31F5CJR_geQ) - -* 数组 - * [必须掌握的数组理论知识](https://mp.weixin.qq.com/s/X7R55wSENyY62le0Fiawsg) - * [数组:每次遇到二分法,都是一看就会,一写就废](https://mp.weixin.qq.com/s/fCf5QbPDtE6SSlZ1yh_q8Q) - * [数组:就移除个元素很难么?](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA) - * [数组:滑动窗口拯救了你](https://mp.weixin.qq.com/s/UrZynlqi4QpyLlLhBPglyg) - * [数组:这个循环可以转懵很多人!](https://mp.weixin.qq.com/s/KTPhaeqxbMK9CxHUUgFDmg) - * [数组:总结篇](https://mp.weixin.qq.com/s/LIfQFRJBH5ENTZpvixHEmg) -* 链表 - * [关于链表,你该了解这些!](https://mp.weixin.qq.com/s/ntlZbEdKgnFQKZkSUAOSpQ) - * [链表:听说用虚拟头节点会方便很多?](https://mp.weixin.qq.com/s/slM1CH5Ew9XzK93YOQYSjA) - * [链表:一道题目考察了常见的五个操作!](https://mp.weixin.qq.com/s/Cf95Lc6brKL4g2j8YyF3Mg) - * [链表:听说过两天反转链表又写不出来了?](https://mp.weixin.qq.com/s/pnvVP-0ZM7epB8y3w_Njwg) - * [链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA) - * [链表:总结篇!](https://mp.weixin.qq.com/s/vK0JjSTHfpAbs8evz5hH8A) - -* 哈希表 - * [关于哈希表,你该了解这些!](https://mp.weixin.qq.com/s/g8N6WmoQmsCUw3_BaWxHZA) - * [哈希表:可以拿数组当哈希表来用,但哈希值不要太大](https://mp.weixin.qq.com/s/vM6OszkM6L1Mx2Ralm9Dig) - * [哈希表:哈希值太大了,还是得用set](https://mp.weixin.qq.com/s/N9iqAchXreSVW7zXUS4BVA) - * [哈希表:今天你快乐了么?](https://mp.weixin.qq.com/s/G4Q2Zfpfe706gLK7HpZHpA) - * [哈希表:map等候多时了](https://mp.weixin.qq.com/s/uVAtjOHSeqymV8FeQbliJQ) - * [哈希表:其实需要哈希的地方都能找到map的身影](https://mp.weixin.qq.com/s/Ue8pKKU5hw_m-jPgwlHcbA) - * [哈希表:这道题目我做过?](https://mp.weixin.qq.com/s/sYZIR4dFBrw_lr3eJJnteQ) - * [哈希表:解决了两数之和,那么能解决三数之和么?](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A) - * [双指针法:一样的道理,能解决四数之和](https://mp.weixin.qq.com/s/nQrcco8AZJV1pAOVjeIU_g) - * [哈希表:总结篇!(每逢总结必经典)](https://mp.weixin.qq.com/s/1s91yXtarL-PkX07BfnwLg) - - -* 字符串 - * [字符串:这道题目,使用库函数一行代码搞定](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) - * [字符串:反转个字符串还有这个用处?](https://mp.weixin.qq.com/s/PmcdiWSmmccHAONzU0ScgQ) - * [视频来了!!带你学透KMP算法(理论篇&代码篇)](https://mp.weixin.qq.com/s/SFAs4tbo2jDgzST9AsF2xg) - * [字符串:都来看看KMP的看家本领!](https://mp.weixin.qq.com/s/Gk9FKZ9_FSWLEkdGrkecyg) - * [字符串:KMP算法还能干这个!](https://mp.weixin.qq.com/s/lR2JPtsQSR2I_9yHbBmBuQ) - * [字符串:总结篇!](https://mp.weixin.qq.com/s/gtycjyDtblmytvBRFlCZJg) - -* 双指针法 - * [数组:就移除个元素很难么?](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA) - * [字符串:这道题目,使用库函数一行代码搞定](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA) - * [字符串:替换空格](https://mp.weixin.qq.com/s/t0A9C44zgM-RysAQV3GZpg) - * [字符串:花式反转还不够!](https://mp.weixin.qq.com/s/X3qpi2v5RSp08mO-W5Vicw) - * [链表:听说过两天反转链表又写不出来了?](https://mp.weixin.qq.com/s/pnvVP-0ZM7epB8y3w_Njwg) - * [链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA) - * [哈希表:解决了两数之和,那么能解决三数之和么?](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A) - * [双指针法:一样的道理,能解决四数之和](https://mp.weixin.qq.com/s/nQrcco8AZJV1pAOVjeIU_g) - * [双指针法:总结篇!](https://mp.weixin.qq.com/s/_p7grwjISfMh0U65uOyCjA) - -* 栈与队列 - * [栈与队列:来看看栈和队列不为人知的一面](https://mp.weixin.qq.com/s/VZRjOccyE09aE-MgLbCMjQ) - * [栈与队列:我用栈来实现队列怎么样?](https://mp.weixin.qq.com/s/P6tupDwRFi6Ay-L7DT4NVg) - * [栈与队列:用队列实现栈还有点别扭](https://mp.weixin.qq.com/s/yzn6ktUlL-vRG3-m5a8_Yw) - * [栈与队列:系统中处处都是栈的应用](https://mp.weixin.qq.com/s/nLlmPMsDCIWSqAtr0jbrpQ) - * [栈与队列:匹配问题都是栈的强项](https://mp.weixin.qq.com/s/eynAEbUbZoAWrk0ZlEugqg) - * [栈与队列:有没有想过计算机是如何处理表达式的?](https://mp.weixin.qq.com/s/hneh2nnLT91rR8ms2fm_kw) - * [栈与队列:滑动窗口里求最大值引出一个重要数据结构](https://mp.weixin.qq.com/s/8c6l2bO74xyMjph09gQtpA) - * [栈与队列:求前 K 个高频元素和队列有啥关系?](https://mp.weixin.qq.com/s/8hMwxoE_BQRbzCc7CA8rng) - * [栈与队列:总结篇!](https://mp.weixin.qq.com/s/xBcHyvHlWq4P13fzxEtkPg) - -* 二叉树 - * [关于二叉树,你该了解这些!](https://mp.weixin.qq.com/s/_ymfWYvTNd2GvWvC5HOE4A) - * [二叉树:一入递归深似海,从此offer是路人](https://mp.weixin.qq.com/s/PwVIfxDlT3kRgMASWAMGhA) - * [二叉树:听说递归能做的,栈也能做!](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg) - * [二叉树:前中后序迭代方式的写法就不能统一一下么?](https://mp.weixin.qq.com/s/WKg0Ty1_3SZkztpHubZPRg) - * [二叉树:层序遍历登场!](https://mp.weixin.qq.com/s/Gb3BjakIKGNpup2jYtTzog) - * [二叉树:你真的会翻转二叉树么?](https://mp.weixin.qq.com/s/6gY1MiXrnm-khAAJiIb5Bg) - * [本周小结!(二叉树)](https://mp.weixin.qq.com/s/JWmTeC7aKbBfGx4TY6uwuQ) - * [二叉树:我对称么?](https://mp.weixin.qq.com/s/Kgf0gjvlDlNDfKIH2b1Oxg) - * [二叉树:看看这些树的最大深度](https://mp.weixin.qq.com/s/guKwV-gSNbA1CcbvkMtHBg) - * [二叉树:看看这些树的最小深度](https://mp.weixin.qq.com/s/BH8-gPC3_QlqICDg7rGSGA) - * [二叉树:我有多少个节点?](https://mp.weixin.qq.com/s/2_eAjzw-D0va9y4RJgSmXw) - * [二叉树:我平衡么?](https://mp.weixin.qq.com/s/isUS-0HDYknmC0Rr4R8mww) - * [二叉树:找我的所有路径?](https://mp.weixin.qq.com/s/Osw4LQD2xVUnCJ-9jrYxJA) - * [还在玩耍的你,该总结啦!(本周小结之二叉树)](https://mp.weixin.qq.com/s/QMBUTYnoaNfsVHlUADEzKg) - * [二叉树:以为使用了递归,其实还隐藏着回溯](https://mp.weixin.qq.com/s/ivLkHzWdhjQQD1rQWe6zWA) - * [二叉树:做了这么多题目了,我的左叶子之和是多少?](https://mp.weixin.qq.com/s/gBAgmmFielojU5Wx3wqFTA) - * [二叉树:我的左下角的值是多少?](https://mp.weixin.qq.com/s/MH2gbLvzQ91jHPKqiub0Nw) - * [二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?](https://mp.weixin.qq.com/s/6TWAVjxQ34kVqROWgcRFOg) - * [二叉树:构造二叉树登场!](https://mp.weixin.qq.com/s/7r66ap2s-shvVvlZxo59xg) - * [二叉树:构造一棵最大的二叉树](https://mp.weixin.qq.com/s/1iWJV6Aov23A7xCF4nV88w) - * [本周小结!(二叉树系列三)](https://mp.weixin.qq.com/s/JLLpx3a_8jurXcz6ovgxtg) - * [二叉树:合并两个二叉树](https://mp.weixin.qq.com/s/3f5fbjOFaOX_4MXzZ97LsQ) - * [二叉树:二叉搜索树登场!](https://mp.weixin.qq.com/s/vsKrWRlETxCVsiRr8v_hHg) - * [二叉树:我是不是一棵二叉搜索树](https://mp.weixin.qq.com/s/8odY9iUX5eSi0eRFSXFD4Q) - * [二叉树:搜索树的最小绝对差](https://mp.weixin.qq.com/s/Hwzml6698uP3qQCC1ctUQQ) - * [二叉树:我的众数是多少?](https://mp.weixin.qq.com/s/KSAr6OVQIMC-uZ8MEAnGHg) - * [二叉树:公共祖先问题](https://mp.weixin.qq.com/s/n6Rk3nc_X3TSkhXHrVmBTQ) - * [本周小结!(二叉树系列四)](https://mp.weixin.qq.com/s/CbdtOTP0N-HIP7DR203tSg) - * [二叉树:搜索树的公共祖先问题](https://mp.weixin.qq.com/s/Ja9dVw2QhBcg_vV-1fkiCg) - * [二叉树:搜索树中的插入操作](https://mp.weixin.qq.com/s/lwKkLQcfbCNX2W-5SOeZEA) - * [二叉树:搜索树中的删除操作](https://mp.weixin.qq.com/s/-p-Txvch1FFk3ygKLjPAKw) - * [二叉树:修剪一棵搜索树](https://mp.weixin.qq.com/s/QzmGfYUMUWGkbRj7-ozHoQ) - * [二叉树:构造一棵搜索树](https://mp.weixin.qq.com/s/sy3ygnouaZVJs8lhFgl9mw) - * [二叉树:搜索树转成累加树](https://mp.weixin.qq.com/s/hZtJh4T5lIGBarY-lZJf6Q) - * [二叉树:总结篇!(需要掌握的二叉树技能都在这里了)](https://mp.weixin.qq.com/s/-ZJn3jJVdF683ap90yIj4Q) - -* 回溯算法 - * [关于回溯算法,你该了解这些!](https://mp.weixin.qq.com/s/gjSgJbNbd1eAA5WkA-HeWw) - * [回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ) - * [回溯算法:组合问题再剪剪枝](https://mp.weixin.qq.com/s/Ri7spcJMUmph4c6XjPWXQA) - * [回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w) - * [回溯算法:电话号码的字母组合](https://mp.weixin.qq.com/s/e2ua2cmkE_vpYjM3j6HY0A) - * [本周小结!(回溯算法系列一)](https://mp.weixin.qq.com/s/m2GnTJdkYhAamustbb6lmw) - * [回溯算法:求组合总和(二)](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw) - * [回溯算法:求组合总和(三)](https://mp.weixin.qq.com/s/_1zPYk70NvHsdY8UWVGXmQ) - * [回溯算法:分割回文串](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q) - * [回溯算法:复原IP地址](https://mp.weixin.qq.com/s/v--VmA8tp9vs4bXCqHhBuA) - * [回溯算法:求子集问题!](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA) - * [本周小结!(回溯算法系列二)](https://mp.weixin.qq.com/s/uzDpjrrMCO8DOf-Tl5oBGw) - * [回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ) - * [回溯算法:递增子序列](https://mp.weixin.qq.com/s/ePxOtX1ATRYJb2Jq7urzHQ) - * [回溯算法:排列问题!](https://mp.weixin.qq.com/s/SCOjeMX1t41wcvJq49GhMw) - * [回溯算法:排列问题(二)](https://mp.weixin.qq.com/s/9L8h3WqRP_h8LLWNT34YlA) - * [本周小结!(回溯算法系列三)](https://mp.weixin.qq.com/s/tLkt9PSo42X60w8i94ViiA) - * [本周小结!(回溯算法系列三)续集](https://mp.weixin.qq.com/s/kSMGHc_YpsqL2j-jb_E_Ag) - * [视频来了!!带你学透回溯算法(理论篇)](https://mp.weixin.qq.com/s/wDd5azGIYWjbU0fdua_qBg) - * [视频来了!!回溯算法:组合问题](https://mp.weixin.qq.com/s/a_r5JR93K_rBKSFplPGNAA) - * [视频来了!!回溯算法:组合问题的剪枝操作](https://mp.weixin.qq.com/s/CK0kj9lq8-rFajxL4amyEg) - * [视频来了!!回溯算法:组合总和](https://mp.weixin.qq.com/s/4M4Cr04uFOWosRMc_5--gg) - * [回溯算法:重新安排行程](https://mp.weixin.qq.com/s/3kmbS4qDsa6bkyxR92XCTA) - * [回溯算法:N皇后问题](https://mp.weixin.qq.com/s/lU_QwCMj6g60nh8m98GAWg) - * [回溯算法:解数独](https://mp.weixin.qq.com/s/eWE9TapVwm77yW9Q81xSZQ) - * [一篇总结带你彻底搞透回溯算法!](https://mp.weixin.qq.com/s/r73thpBnK1tXndFDtlsdCQ) - -* 贪心算法 - * [关于贪心算法,你该了解这些!](https://mp.weixin.qq.com/s/O935TaoHE9Eexwe_vSbRAg) - * [贪心算法:分发饼干](https://mp.weixin.qq.com/s/YSuLIAYyRGlyxbp9BNC1uw) - * [贪心算法:摆动序列](https://mp.weixin.qq.com/s/Xytl05kX8LZZ1iWWqjMoHA) - * [贪心算法:最大子序和](https://mp.weixin.qq.com/s/DrjIQy6ouKbpletQr0g1Fg) - * [本周小结!(贪心算法系列一)](https://mp.weixin.qq.com/s/KQ2caT9GoVXgB1t2ExPncQ) - * [贪心算法:买卖股票的最佳时机II](https://mp.weixin.qq.com/s/VsTFA6U96l18Wntjcg3fcg) - * [贪心算法:跳跃游戏](https://mp.weixin.qq.com/s/606_N9j8ACKCODoCbV1lSA) - * [贪心算法:跳跃游戏II](https://mp.weixin.qq.com/s/kJBcsJ46DKCSjT19pxrNYg) - * [贪心算法:K次取反后最大化的数组和](https://mp.weixin.qq.com/s/dMTzBBVllRm_Z0aaWvYazA) - * [本周小结!(贪心算法系列二)](https://mp.weixin.qq.com/s/RiQri-4rP9abFmq_mlXNiQ) - * [贪心算法:加油站](https://mp.weixin.qq.com/s/aDbiNuEZIhy6YKgQXvKELw) - * [贪心算法:分发糖果](https://mp.weixin.qq.com/s/8MwlgFfvaNYmjGwjuMlETQ) - * [贪心算法:柠檬水找零](https://mp.weixin.qq.com/s/0kT4P-hzY7H6Ae0kjQqnZg) - * [贪心算法:根据身高重建队列](https://mp.weixin.qq.com/s/-2TgZVdOwS-DvtbjjDEbfw) - * [本周小结!(贪心算法系列三)](https://mp.weixin.qq.com/s/JfeuK6KgmifscXdpEyIm-g) - * [贪心算法:根据身高重建队列(续集)](https://mp.weixin.qq.com/s/K-pRN0lzR-iZhoi-1FgbSQ) - * [贪心算法:用最少数量的箭引爆气球](https://mp.weixin.qq.com/s/HxVAJ6INMfNKiGwI88-RFw) - - - - -(持续更新.....) - - -# 视频篇 - -* 算法 - * [带你学透KMP算法(理论篇&代码篇)](https://mp.weixin.qq.com/s/SFAs4tbo2jDgzST9AsF2xg) - * [带你学透回溯算法(理论篇)](https://mp.weixin.qq.com/s/wDd5azGIYWjbU0fdua_qBg) - * [回溯算法:组合问题](https://mp.weixin.qq.com/s/a_r5JR93K_rBKSFplPGNAA) - * [回溯算法:组合问题的剪枝操作](https://mp.weixin.qq.com/s/CK0kj9lq8-rFajxL4amyEg) -* C++ - * 听说C++ primer 太厚了 看不进去?:https://www.bilibili.com/video/BV1Z5411874t - * C++ primer 第一章,你要知道的知识点还有这些!:https://www.bilibili.com/video/BV1Kv41117Ya - * C++ primer 第二章,开讲咯!:https://www.bilibili.com/video/BV1MA411j74g - -(持续更新.....) diff --git a/problems/帮你把KMP算法学个通透.md b/problems/帮你把KMP算法学个通透.md deleted file mode 100644 index 28f8efe2..00000000 --- a/problems/帮你把KMP算法学个通透.md +++ /dev/null @@ -1,509 +0,0 @@ -> leetcode上版本 - -本题是KMP 经典题目。 - -以下文字如果看不进去,可以看我的B站视频: - -* [帮你把KMP算法学个通透!B站(理论篇)](https://www.bilibili.com/video/BV1PD4y1o7nd/) -* [帮你把KMP算法学个通透!(求next数组代码篇)](https://www.bilibili.com/video/BV1M5411j7Xx) - - -KMP的经典思想就是:**当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。** - -本篇将以如下顺序来讲解KMP, - -1. 什么是KMP -2. KMP可以解决什么问题 -3. 分析KMP算法里的next数组 -4. 什么是前缀表 -5. 再分析为什么要是前缀表而不是什么哈希表其他表等等,偏偏要是前缀表。 -6. 一步一步推导前缀表是怎么求的 -7. 时间复杂度分析 -8. 前缀表与next数组的关系 -9. 如何使用next数组来做一遍匹配的过程 -10. 构造next数组 -11. 使用next数组进行匹配 -12. 前缀表统一减一(右移)的KMP实现方式 -13. 前缀表不减一的KMP实现方式 -14. 总结 - -可以说步步相扣,大家要跟紧,哈哈。 - -# 什么是KMP - -说到KMP,先说一下KMP这个名字是怎么来的,为什么叫做KMP呢。 - -因为是由这三位学者发明的:Knuth,Morris和Pratt,所以取了三位学者名字的首字母。所以叫做KMP - -# KMP有什么用 - -KMP主要应用在字符串匹配上。 - -KMP的主要思想是**当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。** - -所以如何记录已经匹配的文本内容,是KMP的重点,也是next数组肩负的重任。 - -其实KMP的代码不好理解,一些同学甚至直接把KMP代码的模板背下来。 - -没有彻底搞懂,懵懵懂懂就把代码背下来太容易忘了。 - -不仅面试的时候可能写不出来,如果面试官问:**next数组里的数字表示的是什么,为什么这么表示?** - -估计大多数候选人都是懵逼的。 - -下面Carl就带大家把KMP的精髓,next数组弄清楚。 - -# 什么是前缀表 - -写过KMP的同学,一定都写过next数组,那么这个next数组究竟是个啥呢? - -next数组就是一个前缀表(prefix table)。 - -前缀表有什么作用呢? - -**前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。** - -为了清楚的了解前缀表的来历,我们来举一个例子: - -要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。 - -请记住文本串和模式串的作用,对于理解下文很重要,要不然容易看懵。所以说三遍: - -要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。 - -要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。 - -要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。 - -如动画所示: -![KMP精讲1.gif](https://pic.leetcode-cn.com/1599638214-EBKEhd-KMP%E7%B2%BE%E8%AE%B21.gif) - -动画里,我特意把 子串`aa` 标记上了,这是有原因的,大家先注意一下,后面还会说道。 - -可以看出,文本串中第六个字符b 和 模式串的第六个字符f,不匹配了。如果暴力匹配,会发现不匹配,此时就要从头匹配了。 - -但如果使用前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配,找到了模式串中第三个字符b继续开始匹配。 - -此时就要问了**前缀表是如何记录的呢?** - -首先要知道前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,在重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置。 - -那么什么是前缀表:**记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。** - -# 为什么一定要用前缀表 - -这就是前缀表那为啥就能告诉我们 上次匹配的位置,并跳过去呢? - -回顾一下,刚刚匹配的过程在下标5的地方遇到不匹配,模式串是指向f,如图: - -![KMP精讲1.png](https://pic.leetcode-cn.com/1599638640-IlQSkx-image.png) - - -然后就找到了下标2,指向b,继续匹配:如图: -![KMP精讲2.png](https://pic.leetcode-cn.com/1599638670-fAVIed-image.png) - - -以下这句话,对于理解为什么使用前缀表可以告诉我们匹配失败之后跳到哪里重新匹配 非常重要! - -**下标5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面从新匹配就可以了。** - -所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。 - -**很多介绍KMP的文章或者视频并没有把为什么要用前缀表?这个问题说清楚,而是直接默认使用前缀表。** - -# 如何计算前缀表 - -接下来就要说一说怎么计算前缀表。 - -如图: -![KMP精讲5.png](https://pic.leetcode-cn.com/1599638703-pxKuJI-image.png) - - -长度为前1个字符的子串`a`,最长相同前后缀的长度为0。(注意字符串的**前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串**;**后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串**。) - -![KMP精讲6.png](https://pic.leetcode-cn.com/1599638728-EedLFL-image.png) - -长度为前2个字符的子串`aa`,最长相同前后缀的长度为1。 -![KMP精讲7.png](https://pic.leetcode-cn.com/1599638751-jYbBlh-image.png) - -长度为前3个字符的子串`aab`,最长相同前后缀的长度为0。 - -以此类推: -长度为前4个字符的子串`aaba`,最长相同前后缀的长度为1。 -长度为前5个字符的子串`aabaa`,最长相同前后缀的长度为2。 -长度为前6个字符的子串`aabaaf`,最长相同前后缀的长度为0。 - -那么把求得的最长相同前后缀的长度就是对应前缀表的元素,如图: -![KMP精讲8.png](https://pic.leetcode-cn.com/1599638777-OKHNjS-image.png) - - -可以看出模式串与前缀表对应位置的数字表示的就是:**下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。** - -再来看一下如何利用 前缀表找到 当字符不匹配的时候应该指针应该移动的位置。如动画所示: - -![KMP精讲2.gif](https://pic.leetcode-cn.com/1599638354-SppDsh-KMP%E7%B2%BE%E8%AE%B22.gif) - -找到的不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少。 - -为什么要前一个字符的前缀表的数值呢,因为要找前面字符串的最长相同的前缀和后缀。 - -所以要看前一位的 前缀表的数值。 - -前一个字符的前缀表的数值是2, 所有把下标移动到下标2的位置继续比配。 可以再反复看一下上面的动画。 - -最后就在文本串中找到了和模式串匹配的子串了。 - -# 前缀表与next数组 - -很多KMP算法的时间都是使用next数组来做回退操作,那么next数组与前缀表有什么关系呢? - -next数组就可以是前缀表,但是很多实现都是把前缀表统一减一(右移一位,初始位置为-1)之后作为next数组。 - -为什么这么做呢,其实也是很多文章视频没有解释清楚的地方。 - -其实**这并不涉及到KMP的原理,而是具体实现,next数组即可以就是前缀表,也可以是前缀表统一减一(右移一位,初始位置为-1)。** - -后面我会提供两种不同的实现代码,大家就明白了了。 - -# 使用next数组来匹配 - -以下我们以前缀表统一减一之后的next数组来做演示。 - -有了next数组,就可以根据next数组来 匹配文本串s,和模式串t了。 - -注意next数组是新前缀表(旧前缀表统一减一了)。 - -匹配过程动画如下: -![KMP精讲4.gif](https://pic.leetcode-cn.com/1599638403-eQrdyh-KMP%E7%B2%BE%E8%AE%B24.gif) - - -# 时间复杂度分析 - - -其中n为文本串长度,m为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。 - -暴力的解法显而易见是O(n * m),所以**KMP在字符串匹配中极大的提高的搜索的效率。** - -为了和[字符串:KMP是时候上场了(一文读懂系列)](https://mp.weixin.qq.com/s/70OXnZ4Ez29CKRrUpVJmug)字符串命名统一,方便大家理解,以下文章统称haystack为文本串, needle为模式串。 - -都知道使用KMP算法,一定要构造next数组。 - -# 构造next数组 - -我们定义一个函数getNext来构建next数组,函数参数为指向next数组的指针,和一个字符串。 代码如下: - -``` -void getNext(int* next, const string& s) -``` - -**构造next数组其实就是计算模式串s,前缀表的过程。** 主要有如下三步: - -1. 初始化 -2. 处理前后缀不相同的情况 -3. 处理前后缀相同的情况 - -接下来我们详解详解一下。 - -1. 初始化: - -定义两个指针i和j,j指向前缀终止位置(严格来说是终止位置减一的位置),i指向后缀终止位置(与j同理)。 - -然后还要对next数组进行初始化赋值,如下: - -``` -int j = -1; -next[0] = j; -``` - -j 为什么要初始化为 -1呢,因为之前说过 前缀表要统一减一的操作仅仅是其中的一种实现,我们这里选择j初始化为-1,下文我还会给出j不初始化为-1的实现代码。 - -next[i] 表示 i(包括i)之前最长相等的前后缀长度(其实就是j) - -所以初始化next[0] = j 。 - - -2. 处理前后缀不相同的情况 - - -因为j初始化为-1,那么i就从1开始,进行s[i] 与 s[j+1]的比较。 - -所以遍历模式串s的循环下标i 要从 1开始,代码如下: - -``` -for(int i = 1; i < s.size(); i++) { -``` - -如果 s[i] 与 s[j+1]不相同,也就是遇到 前后缀末尾不相同的情况,就要向前回溯。 - -怎么回溯呢? - -next[j]就是记录着j(包括j)之前的子串的相同前后缀的长度。 - -那么 s[i] 与 s[j+1] 不相同,就要找 j+1前一个元素在next数组里的值(就是next[j])。 - -所以,处理前后缀不相同的情况代码如下: - -``` -while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了 -    j = next[j]; // 向前回溯 -} -``` - -3. 处理前后缀相同的情况 - -如果s[i] 与 s[j + 1] 相同,那么就同时向后移动i 和j 说明找到了相同的前后缀,同时还要将j(前缀的长度)赋给next[i], 因为next[i]要记录相同前后缀的长度。 - -代码如下: - -``` -if (s[i] == s[j + 1]) { // 找到相同的前后缀 -    j++; -} -next[i] = j; -``` - -最后整体构建next数组的函数代码如下: - -``` -void getNext(int* next, const string& s){ -    int j = -1; -    next[0] = j; -    for(int i = 1; i < s.size(); i++) { // 注意i从1开始 -        while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了 -            j = next[j]; // 向前回溯 -        } -        if (s[i] == s[j + 1]) { // 找到相同的前后缀 -            j++; -        } -        next[i] = j; // 将j(前缀的长度)赋给next[i] -    } -} -``` - - -代码构造next数组的逻辑流程动画如下: - -![KMP精讲3.gif](https://pic.leetcode-cn.com/1599638458-sHaHqX-KMP%E7%B2%BE%E8%AE%B23.gif) - -得到了next数组之后,就要用这个来做匹配了。 - -# 使用next数组来做匹配 - -在文本串s里 找是否出现过模式串t。 - -定义两个下标j 指向模式串起始位置,i指向文本串其实位置。 - -那么j初始值依然为-1,为什么呢? **依然因为next数组里记录的起始位置为-1。** - -i就从0开始,遍历文本串,代码如下: - -``` -for (int i = 0; i < s.size(); i++)  -``` - -接下来就是 s[i] 与 t[j + 1] (因为j从-1开始的) 经行比较。 - -如果 s[i] 与 t[j + 1] 不相同,j就要从next数组里寻找下一个匹配的位置。 - -代码如下: - -``` -while(j >= 0 && s[i] != t[j + 1]) { -    j = next[j]; -} -``` - -如果 s[i] 与 t[j + 1] 相同,那么i 和 j 同时向后移动, 代码如下: - -``` -if (s[i] == t[j + 1]) { -    j++; // i的增加在for循环里 -} -``` - -如何判断在文本串s里出现了模式串t呢,如果j指向了模式串t的末尾,那么就说明模式串t完全匹配文本串s里的某个子串了。 - -本题要在文本串字符串中找出模式串出现的第一个位置 (从0开始),所以返回当前在文本串匹配模式串的位置i 减去 模式串的长度,就是文本串字符串中出现模式串的第一个位置。 - -代码如下: - -``` -if (j == (t.size() - 1) ) { -    return (i - t.size() + 1); -} -``` - -那么使用next数组,用模式串匹配文本串的整体代码如下: - -``` -int j = -1; // 因为next数组里记录的起始位置为-1 -for (int i = 0; i < s.size(); i++) { // 注意i就从0开始 -    while(j >= 0 && s[i] != t[j + 1]) { // 不匹配 -        j = next[j]; // j 寻找之前匹配的位置 -    } -    if (s[i] == t[j + 1]) { // 匹配,j和i同时向后移动 -        j++; // i的增加在for循环里 -    } -    if (j == (t.size() - 1) ) { // 文本串s里出现了模式串t -        return (i - t.size() + 1); -    } -} -``` - -此时所有逻辑的代码都已经写出来了,本题整体代码如下: - -# 前缀表统一减一 C++代码实现 - -``` -class Solution { -public: -    void getNext(int* next, const string& s) { -        int j = -1; -        next[0] = j; -        for(int i = 1; i < s.size(); i++) { // 注意i从1开始 -            while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了 -                j = next[j]; // 向前回溯 -            } -            if (s[i] == s[j + 1]) { // 找到相同的前后缀 -                j++; -            } -            next[i] = j; // 将j(前缀的长度)赋给next[i] -        } -    } - int strStr(string haystack, string needle) { - if (needle.size() == 0) { - return 0; - } - int next[needle.size()]; - getNext(next, needle); - int j = -1; // // 因为next数组里记录的起始位置为-1 - for (int i = 0; i < haystack.size(); i++) { // 注意i就从0开始 - while(j >= 0 && haystack[i] != needle[j + 1]) { // 不匹配 - j = next[j]; // j 寻找之前匹配的位置 - } - if (haystack[i] == needle[j + 1]) { // 匹配,j和i同时向后移动 - j++; // i的增加在for循环里 - } - if (j == (needle.size() - 1) ) { // 文本串s里出现了模式串t - return (i - needle.size() + 1); - } - } - return -1; - } -}; - -``` - -# 前缀表(不减一)C++实现 - -那么前缀表就不减一了,也不右移的,到底行不行呢?行! - -我之前说过,这仅仅是KMP算法实现上的问题,如果就直接使用前缀表可以换一种回退方式,找j=next[j-1] 来进行回退。 - -主要就是j=next[x]这一步最为关键! - -我给出的getNext的实现为:(前缀表统一减一) - - -``` -void getNext(int* next, const string& s) { -    int j = -1; -    next[0] = j; -    for(int i = 1; i < s.size(); i++) { // 注意i从1开始 -        while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了 -            j = next[j]; // 向前回溯 -        } -        if (s[i] == s[j + 1]) { // 找到相同的前后缀 -            j++; -        } -        next[i] = j; // 将j(前缀的长度)赋给next[i] -    } -} - -``` -此时如果输入的模式串为aabaaf,对应的next为-1 0 -1 0 1 -1。 - -这里j和next[0]初始化为-1,整个next数组是以 前缀表减一之后的效果来构建的。 - -那么前缀表不减一来构建next数组,代码如下: - -``` - void getNext(int* next, const string& s) { - int j = 0; - next[0] = 0; - for(int i = 1; i < s.size(); i++) { - while (j > 0 && s[i] != s[j]) { // j要保证大于0,因为下面有取j-1作为数组下标的操作 - j = next[j - 1]; // 注意这里,是要找前一位的对应的回退位置了 - } - if (s[i] == s[j]) { - j++; - } - next[i] = j; - } - } - -``` - -此时如果输入的模式串为aabaaf,对应的next为 0 1 0 1 2 0,(其实这就是前缀表的数值了)。 - -那么用这样的next数组也可以用来做匹配,代码要有所改动。 - -实现代码如下: - -``` -class Solution { -public: - void getNext(int* next, const string& s) { - int j = 0; - next[0] = 0; - for(int i = 1; i < s.size(); i++) { - while (j > 0 && s[i] != s[j]) { - j = next[j - 1]; - } - if (s[i] == s[j]) { - j++; - } - next[i] = j; - } - } - int strStr(string haystack, string needle) { - if (needle.size() == 0) { - return 0; - } - int next[needle.size()]; - getNext(next, needle); - int j = 0; - for (int i = 0; i < haystack.size(); i++) { - while(j > 0 && haystack[i] != needle[j]) { - j = next[j - 1]; - } - if (haystack[i] == needle[j]) { - j++; - } - if (j == needle.size() ) { - return (i - needle.size() + 1); - } - } - return -1; - } -}; -``` - -# 总结 - -我们介绍了什么是KMP,KMP可以解决什么问题,然后分析KMP算法里的next数组,知道了next数组就是前缀表,再分析为什么要是前缀表而不是什么其他表。 - -接着从给出的模式串中,我们一步一步的推导出了前缀表,得出前缀表无论是统一减一还是不同意减一得到的next数组仅仅是kmp的实现方式的不同。 - -其中还分析了KMP算法的时间复杂度,并且和暴力方法做了对比。 - -然后先用前缀表统一减一得到的next数组,求得文本串s里是否出现过模式串t,并给出了具体分析代码。 - -又给出了直接用前缀表作为next数组,来做匹配的实现代码。 - -可以说把KMP的每一个细微的细节都扣了出来,毫无遮掩的展示给大家了! - -> **我是[程序员Carl](https://github.com/youngyangyang04),[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png)可以找我,本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/数组总结篇.md b/problems/数组总结篇.md index e4ba175b..748e0f4f 100644 --- a/problems/数组总结篇.md +++ b/problems/数组总结篇.md @@ -1,4 +1,15 @@ -> 这个周末我们对数组做一个总结 +

+ +

+

+ + + + + + +

+ # 数组理论基础 diff --git a/problems/数组理论基础.md b/problems/数组理论基础.md index c7245b2e..035fe744 100644 --- a/problems/数组理论基础.md +++ b/problems/数组理论基础.md @@ -1,3 +1,14 @@ +

+ +

+

+ + + + + + +

# 数组理论基础 diff --git a/problems/栈与队列总结.md b/problems/栈与队列总结.md index 81a150c5..bd2cef42 100644 --- a/problems/栈与队列总结.md +++ b/problems/栈与队列总结.md @@ -1,5 +1,15 @@ +

+ +

+

+ + + + + + +

-> 学习不总结等于白学 # 栈与队列的理论基础 diff --git a/problems/栈与队列理论基础.md b/problems/栈与队列理论基础.md new file mode 100644 index 00000000..7111bbcb --- /dev/null +++ b/problems/栈与队列理论基础.md @@ -0,0 +1,94 @@ +

+ +

+

+ + + + + + +

+ + +> 来看看栈和队列不为人知的一面 + +我想栈和队列的原理大家应该很熟悉了,队列是先进先出,栈是先进后出。 + +如图所示: + +![栈与队列理论1](https://img-blog.csdnimg.cn/20210104235346563.png) + +那么我这里在列出四个关于栈的问题,大家可以思考一下。以下是以C++为例,相信使用其他编程语言的同学也对应思考一下,自己使用的编程语言里栈和队列是什么样的。 + +1. C++中stack 是容器么? +2. 我们使用的stack是属于那个版本的STL? +3. 我们使用的STL中stack是如何实现的? +4. stack 提供迭代器来遍历stack空间么? + +相信这四个问题并不那么好回答, 因为一些同学使用数据结构会停留在非常表面上的应用,稍稍往深一问,就会有好像懂,好像也不懂的感觉。 + +有的同学可能仅仅知道有栈和队列这么个数据结构,却不知道底层实现,也不清楚所使用栈和队列和STL是什么关系。 + +所以这里我在给大家扫一遍基础知识, + +首先大家要知道 栈和队列是STL(C++标准库)里面的两个数据结构。 + +C++标准库是有多个版本的,要知道我们使用的STL是哪个版本,才能知道对应的栈和队列的实现原理。 + +那么来介绍一下,三个最为普遍的STL版本: + +1. HP STL +其他版本的C++ STL,一般是以HP STL为蓝本实现出来的,HP STL是C++ STL的第一个实现版本,而且开放源代码。 + +2. P.J.Plauger STL +由P.J.Plauger参照HP STL实现出来的,被Visual C++编译器所采用,不是开源的。 + +3. SGI STL +由Silicon Graphics Computer Systems公司参照HP STL实现,被Linux的C++编译器GCC所采用,SGI STL是开源软件,源码可读性甚高。 + +接下来介绍的栈和队列也是SGI STL里面的数据结构, 知道了使用版本,才知道对应的底层实现。 + +来说一说栈,栈先进后出,如图所示: + +![栈与队列理论2](https://img-blog.csdnimg.cn/20210104235434905.png) + +栈提供push 和 pop 等等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)。 不像是set 或者map 提供迭代器iterator来遍历所有元素。 + +**栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。** + +所以STL中栈往往不被归类为容器,而被归类为container adapter(容器适配器)。 + +那么问题来了,STL 中栈是用什么容器实现的? + +从下图中可以看出,栈的内部结构,栈的底层实现可以是vector,deque,list 都是可以的, 主要就是数组和链表的底层实现。 + +![栈与队列理论3](https://img-blog.csdnimg.cn/20210104235459376.png) + + +**我们常用的SGI STL,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的低层结构。** + +deque是一个双向队列,只要封住一段,只开通另一端就可以实现栈的逻辑了。 + +**SGI STL中 队列底层实现缺省情况下一样使用deque实现的。** + +我们也可以指定vector为栈的底层实现,初始化语句如下: + +``` +std::stack > third; // 使用vector为底层容器的栈 +``` + +刚刚讲过栈的特性,对应的队列的情况是一样的。 + +队列中先进先出的数据结构,同样不允许有遍历行为,不提供迭代器, **SGI STL中队列一样是以deque为缺省情况下的底部结构。** + +也可以指定list 为起底层实现,初始化queue的语句如下: + +``` +std::queue> third; // 定义以list为底层容器的队列 +``` + +所以STL 队列也不被归类为容器,而被归类为container adapter( 容器适配器)。 + +我这里讲的都是(clck)C++ 语言中情况, 使用其他语言的同学也要思考栈与队列的底层实现问题, 不要对数据结构的使用浅尝辄止,而要深挖起内部原理,才能夯实基础。 + diff --git a/problems/贪心算法理论基础.md b/problems/贪心算法理论基础.md deleted file mode 100644 index 0276d987..00000000 --- a/problems/贪心算法理论基础.md +++ /dev/null @@ -1,80 +0,0 @@ -# 关于贪心算法,你该了解这些! - -> 正式开始新的系列了,贪心算法! -通知:一些录友表示经常看不到每天的文章,现在公众号已经不按照发送时间推荐了,而是根据一些规则乱序推送,所以可能关注了「代码随想录」也一直看不到文章,建议把「代码随想录」设置星标哈,设置星标之后,每天就按发文时间推送了,我每天都是定时8:35发送的,嗷嗷准时,哈哈。 - -# 什么是贪心 - -**贪心的本质是选择每一阶段的局部最优,从而达到全局最优**。 - -这么说有点抽象,来举一个例子: - -例如,有一堆钞票,你可以拿走十张,如果想达到最大的金额,你要怎么拿? - -指定每次拿最大的,最终结果就是拿走最大数额的钱。 - -每次拿最大的就是局部最优,最后拿走最大数额的钱就是推出全局最优。 - -再举一个例子如果是 有一堆盒子,你有一个背包体积为n,如何把背包尽可能装满,如果还每次选最大的盒子,就不行了。这时候就需要动态规划。动态规划的问题在下一个系列会详细讲解。 - -# 贪心的套路(什么时候用贪心) - -很多同学做贪心的题目的时候,想不出来是贪心,想知道有没有什么套路可以一看就看出来是贪心。 - -**说实话贪心算法并没有固定的套路**。 - -所以唯一的难点就是如何通过局部最优,推出整体最优。 - -那么如何能看出局部最优是否能推出整体最优呢?有没有什么固定策略或者套路呢? - -**不好意思,也没有!** 靠自己手动模拟,如果模拟可行,就可以试一试贪心策略,如果不可行,可能需要动态规划。 - -有同学问了如何验证可不可以用贪心算法呢? - -**最好用的策略就是举反例,如果想不到反例,那么就试一试贪心吧**。 - -可有有同学认为手动模拟,举例子得出的结论不靠谱,想要严格的数学证明。 - -一般数学证明有如下两种方法: - -* 数学归纳法 -* 反证法 - -看教课书上讲解贪心可以是一堆公式,估计大家连看都不想看,所以数学证明就不在我要讲解的范围内了,大家感兴趣可以自行查找资料。 - -**面试中基本不会让面试者现场证明贪心的合理性,代码写出来跑过测试用例即可,或者自己能自圆其说理由就行了**。 - -举一个不太恰当的例子:我要用一下1+1 = 2,但我要先证明1+1 为什么等于2。严谨是严谨了,但没必要。 - -虽然这个例子很极端,但可以表达这么个意思:**刷题或者面试的时候,手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心**。 - -**例如刚刚举的拿钞票的例子,就是模拟一下每次拿做大的,最后就能拿到最多的钱,这还要数学证明的话,其实就不在算法面试的范围内了,可以看看专业的数学书籍!** - -所以这也是为什么很多同学通过(accept)了贪心的题目,但都不知道自己用了贪心算法,**因为贪心有时候就是常识性的推导,所以会认为本应该就这么做!** - -**那么刷题的时候什么时候真的需要数学推导呢?** - -例如这道题目:[链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA),这道题不用数学推导一下,就找不出环的起始位置,想试一下就不知道怎么试,这种题目确实需要数学简单推导一下。 - -# 贪心一般解题步骤 - -贪心算法一般分为如下四步: - -* 将问题分解为若干个子问题 -* 找出适合的贪心策略 -* 求解每一个子问题的最优解 -* 将局部最优解堆叠成全局最优解 - -其实这个分的有点细了,真正做题的时候很难分出这么详细的解题步骤,可能就是因为贪心的题目往往还和其他方面的知识混在一起。 - -# 总结 - -本篇给出了什么是贪心以及大家关心的贪心算法固定套路。 - -**不好意思了,贪心没有套路,说白了就是常识性推导加上举反例**。 - -最后给出贪心的一般解题步骤,大家可以发现这个解题步骤也是比较抽象的,不像是二叉树,回溯算法,给出了那么具体的解题套路和模板。 - -本篇没有配图,其实可以找一些动漫周边或者搞笑的图配一配(符合大多数公众号文章的作风),但这不是我的风格,所以本篇文字描述足以! - -就酱,「代码随想录」值得你的关注! diff --git a/problems/链表总结篇.md b/problems/链表总结篇.md index 16954683..8129ed25 100644 --- a/problems/链表总结篇.md +++ b/problems/链表总结篇.md @@ -1,4 +1,15 @@ -> 之前链表篇没有做总结,所以是时候总结一波 +

+ +

+

+ + + + + + +

+ # 链表的理论基础 diff --git a/problems/链表理论基础.md b/problems/链表理论基础.md new file mode 100644 index 00000000..63f0396c --- /dev/null +++ b/problems/链表理论基础.md @@ -0,0 +1,145 @@ +

+ +

+

+ + + + + + +

+ + +# 关于链表,你该了解这些! + +什么是链表,链表是一种通过指针串联在一起的线性结构,每一个节点是又两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。 + +链接的入口点称为列表的头结点也就是head。 + +如图所示: +![链表1](https://img-blog.csdnimg.cn/20200806194529815.png) + +# 链表的类型 + +接下来说一下链表的几种类型: + +## 单链表 + +刚刚说的就是单链表。 + +## 双链表 + +单链表中的节点只能指向节点的下一个节点。 + +双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。 + +双链表 既可以向前查询也可以向后查询。 + +如图所示: +![链表2](https://img-blog.csdnimg.cn/20200806194559317.png) + +## 循环链表 + +循环链表,顾名思义,就是链表首尾相连。 + +循环链表可以用来解决约瑟夫环问题。 + +![链表4](https://img-blog.csdnimg.cn/20200806194629603.png) + + +# 链表的存储方式 + +了解完链表的类型,再来说一说链表在内存中的存储方式。 + +数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。 + +链表是通过指针域的指针链接在内存中各个节点。 + +所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。 + +如图所示: + +![链表3](https://img-blog.csdnimg.cn/20200806194613920.png) + +这个链表起始节点为2, 终止节点为7, 各个节点分布在内存个不同地址空间上,通过指针串联在一起。 + +# 链表的定义 + +接下来说一说链表的定义。 + +链表节点的定义,很多同学在面试的时候都写不好。 + +这是因为平时在刷leetcode的时候,链表的节点都默认定义好了,直接用就行了,所以同学们都没有注意到链表的节点是如何定义的。 + +而在面试的时候,一旦要自己手写链表,就写的错漏百出。 + +这里我给出C/C++的定义链表节点方式,如下所示: + +``` +// 单链表 +struct ListNode { + int val; // 节点上存储的元素 + ListNode *next; // 指向下一个节点的指针 + ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数 +}; +``` + +有同学说了,我不定义构造函数行不行,答案是可以的,C++默认生成一个构造函数。 + +但是这个构造函数不会初始化任何成员变化,下面我来举两个例子: + +通过自己定义构造函数初始化节点: + +``` +ListNode* head = new ListNode(5); +``` + +使用默认构造函数初始化节点: + +``` +ListNode* head = new ListNode(); +head->val = 5; +``` + +所以如果不定义构造函数使用默认构造函数的话,在初始化的时候就不能直接给变量赋值! + +# 链表的操作 + +## 删除节点 + +删除D节点,如图所示: + +![链表-删除节点](https://img-blog.csdnimg.cn/20200806195114541.png) + +只要将C节点的next指针 指向E节点就可以了。 + +那有同学说了,D节点不是依然存留在内存里么?只不过是没有在这个链表里而已。 + +是这样的,所以在C++里最好是再手动释放这个D节点,释放这块内存。 + +其他语言例如Java、Python,就有自己的内存回收机制,就不用自己手动释放了。 + +## 添加节点 + +如图所示: + +![链表-添加节点](https://img-blog.csdnimg.cn/20200806195134331.png) + +可以看出链表的增添和删除都是O(1)操作,也不会影响到其他节点。 + +但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。 + +# 性能分析 + +再把链表的特性和数组的特性进行一个对比,如图所示: + +![链表-链表与数据性能对比](https://img-blog.csdnimg.cn/20200806195200276.png) + +数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。 + +链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。 + +相信大家已经对链表足够的了解,后面我会讲解关于链表的高频面试题目,我们下期见! + + diff --git a/problems/面试题02.07.链表相交.md b/problems/面试题02.07.链表相交.md deleted file mode 100644 index 77024d7c..00000000 --- a/problems/面试题02.07.链表相交.md +++ /dev/null @@ -1,66 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/intersection-of-two-linked-lists-lcci/ - -## 思路 - -本来很简洁明了的一道题,让题目描述搞的云里雾里的。 - -简单来说,就是求两个链表交点节点的**指针**。 这里同学们要注意,交点不是数值相等,而是指针相等。 - -为了方便举例,假设节点元素数值相等,则节点指针相等。 - -看如下两个链表,目前curA指向链表A的头结点,curB指向链表B的头结点: - - - -我们求出两个链表的长度,并求出两个链表长度的差值,然后让curA移动到,和curB 末尾对齐的位置,如图: - - - -此时我们就可以比较curA和curB是否相同,如果不相同,同时向后移动curA和curB,如果遇到curA == curB,则找到焦点。 - -否则循环退出返回空指针。 - -## C++代码 - -``` -class Solution { -public: - ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) { - ListNode* curA = headA; - ListNode* curB = headB; - int lenA = 0, lenB = 0; - while (curA != NULL) { // 求链表A的长度 - lenA++; - curA = curA->next; - } - while (curB != NULL) { // 求链表B的长度 - lenB++; - curB = curB->next; - } - curA = headA; - curB = headB; - // 让curA为最长链表的头,lenA为其长度 - if (lenB > lenA) { - swap (lenA, lenB); - swap (curA, curB); - } - // 求长度差 - int gap = lenA - lenB; - // 让curA和curB在同一起点上(末尾位置对齐) - while (gap--) { - curA = curA->next; - } - // 遍历curA 和 curB,遇到相同则直接返回 - while (curA != NULL) { - if (curA == curB) { - return curA; - } - curA = curA->next; - curB = curB->next; - } - return NULL; - } -}; -``` -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/video/.DS_Store b/video/.DS_Store deleted file mode 100644 index 5008ddfc..00000000 Binary files a/video/.DS_Store and /dev/null differ diff --git a/video/1.两数之和.mp4 b/video/1.两数之和.mp4 deleted file mode 100644 index dd6849c6..00000000 Binary files a/video/1.两数之和.mp4 and /dev/null differ diff --git a/video/101.对称二叉树.gif b/video/101.对称二叉树.gif deleted file mode 100644 index 01ee5492..00000000 Binary files a/video/101.对称二叉树.gif and /dev/null differ diff --git a/video/102二叉树的层序遍历.mp4 b/video/102二叉树的层序遍历.mp4 deleted file mode 100644 index f6f66bba..00000000 Binary files a/video/102二叉树的层序遍历.mp4 and /dev/null differ diff --git a/video/1047.删除字符串中的所有相邻重复项.gif b/video/1047.删除字符串中的所有相邻重复项.gif deleted file mode 100644 index a45c37d1..00000000 Binary files a/video/1047.删除字符串中的所有相邻重复项.gif and /dev/null differ diff --git a/video/1047.删除字符串中的所有相邻重复项.mp4 b/video/1047.删除字符串中的所有相邻重复项.mp4 deleted file mode 100644 index b8512f24..00000000 Binary files a/video/1047.删除字符串中的所有相邻重复项.mp4 and /dev/null differ diff --git a/video/141.环形链表.gif b/video/141.环形链表.gif deleted file mode 100644 index 5e5308aa..00000000 Binary files a/video/141.环形链表.gif and /dev/null differ diff --git a/video/141.环形链表.mp4 b/video/141.环形链表.mp4 deleted file mode 100644 index 19a7f223..00000000 Binary files a/video/141.环形链表.mp4 and /dev/null differ diff --git a/video/142.环形链表II.mp4 b/video/142.环形链表II.mp4 deleted file mode 100644 index b6ced104..00000000 Binary files a/video/142.环形链表II.mp4 and /dev/null differ diff --git a/video/142.环形链表II1.mp4 b/video/142.环形链表II1.mp4 deleted file mode 100644 index 317ce246..00000000 Binary files a/video/142.环形链表II1.mp4 and /dev/null differ diff --git a/video/142.环形链表II(求入口).gif b/video/142.环形链表II(求入口).gif deleted file mode 100644 index 9e5f2038..00000000 Binary files a/video/142.环形链表II(求入口).gif and /dev/null differ diff --git a/video/15.三数之和.gif b/video/15.三数之和.gif deleted file mode 100644 index 82e1eabc..00000000 Binary files a/video/15.三数之和.gif and /dev/null differ diff --git a/video/15.三数之和.mp4 b/video/15.三数之和.mp4 deleted file mode 100644 index 29bef5fc..00000000 Binary files a/video/15.三数之和.mp4 and /dev/null differ diff --git a/video/150.逆波兰表达式求值.gif b/video/150.逆波兰表达式求值.gif deleted file mode 100644 index 8e71e591..00000000 Binary files a/video/150.逆波兰表达式求值.gif and /dev/null differ diff --git a/video/150.逆波兰表达式求值.mp4 b/video/150.逆波兰表达式求值.mp4 deleted file mode 100644 index bd991a98..00000000 Binary files a/video/150.逆波兰表达式求值.mp4 and /dev/null differ diff --git a/video/151翻转字符串里的单词.gif b/video/151翻转字符串里的单词.gif deleted file mode 100644 index cd8a60fb..00000000 Binary files a/video/151翻转字符串里的单词.gif and /dev/null differ diff --git a/video/20.有效括号.mp4 b/video/20.有效括号.mp4 deleted file mode 100644 index 84eab69f..00000000 Binary files a/video/20.有效括号.mp4 and /dev/null differ diff --git a/video/206.翻转链表.gif b/video/206.翻转链表.gif deleted file mode 100644 index ad2fdafd..00000000 Binary files a/video/206.翻转链表.gif and /dev/null differ diff --git a/video/206.翻转链表.mp4 b/video/206.翻转链表.mp4 deleted file mode 100644 index 02d66ccb..00000000 Binary files a/video/206.翻转链表.mp4 and /dev/null differ diff --git a/video/209.长度最小的子数组.gif b/video/209.长度最小的子数组.gif deleted file mode 100644 index f120acc1..00000000 Binary files a/video/209.长度最小的子数组.gif and /dev/null differ diff --git a/video/209.长度最小的子数组.mp4 b/video/209.长度最小的子数组.mp4 deleted file mode 100644 index df034f52..00000000 Binary files a/video/209.长度最小的子数组.mp4 and /dev/null differ diff --git a/video/209.长度最小的子数组(缩小版).gif b/video/209.长度最小的子数组(缩小版).gif deleted file mode 100644 index f1c0b4cb..00000000 Binary files a/video/209.长度最小的子数组(缩小版).gif and /dev/null differ diff --git a/video/225.用队列实现栈.mp4 b/video/225.用队列实现栈.mp4 deleted file mode 100644 index 9aa76c34..00000000 Binary files a/video/225.用队列实现栈.mp4 and /dev/null differ diff --git a/video/232.用栈实现队列.mp4 b/video/232.用栈实现队列.mp4 deleted file mode 100644 index 1ba21a36..00000000 Binary files a/video/232.用栈实现队列.mp4 and /dev/null differ diff --git a/video/232.用栈实现队列版本2.mp4 b/video/232.用栈实现队列版本2.mp4 deleted file mode 100644 index 32db272e..00000000 Binary files a/video/232.用栈实现队列版本2.mp4 and /dev/null differ diff --git a/video/239.滑动窗口最大值.gif b/video/239.滑动窗口最大值.gif deleted file mode 100644 index 90c8f11a..00000000 Binary files a/video/239.滑动窗口最大值.gif and /dev/null differ diff --git a/video/239.滑动窗口最大值.mp4 b/video/239.滑动窗口最大值.mp4 deleted file mode 100644 index 5497d70d..00000000 Binary files a/video/239.滑动窗口最大值.mp4 and /dev/null differ diff --git a/video/242.有效的字母异位词.gif b/video/242.有效的字母异位词.gif deleted file mode 100644 index febc0bad..00000000 Binary files a/video/242.有效的字母异位词.gif and /dev/null differ diff --git a/video/242.有效的字母异位词.mp4 b/video/242.有效的字母异位词.mp4 deleted file mode 100644 index 33504290..00000000 Binary files a/video/242.有效的字母异位词.mp4 and /dev/null differ diff --git a/video/26.删除排序数组中的重复项.mp4 b/video/26.删除排序数组中的重复项.mp4 deleted file mode 100644 index 846d10ac..00000000 Binary files a/video/26.删除排序数组中的重复项.mp4 and /dev/null differ diff --git a/video/27.移除元素-双指针法.gif b/video/27.移除元素-双指针法.gif deleted file mode 100644 index 74aa479e..00000000 Binary files a/video/27.移除元素-双指针法.gif and /dev/null differ diff --git a/video/27.移除元素-暴力解法.gif b/video/27.移除元素-暴力解法.gif deleted file mode 100644 index 0ccba4dc..00000000 Binary files a/video/27.移除元素-暴力解法.gif and /dev/null differ diff --git a/video/27.移除元素-暴力解法.mp4 b/video/27.移除元素-暴力解法.mp4 deleted file mode 100644 index 5024f2d7..00000000 Binary files a/video/27.移除元素-暴力解法.mp4 and /dev/null differ diff --git a/video/27.移除元素.mp4 b/video/27.移除元素.mp4 deleted file mode 100644 index 9dc869a1..00000000 Binary files a/video/27.移除元素.mp4 and /dev/null differ diff --git a/video/283.移动零.gif b/video/283.移动零.gif deleted file mode 100644 index b42c3bf6..00000000 Binary files a/video/283.移动零.gif and /dev/null differ diff --git a/video/283.移动零.mp4 b/video/283.移动零.mp4 deleted file mode 100644 index 227b9840..00000000 Binary files a/video/283.移动零.mp4 and /dev/null differ diff --git a/video/344.反转字符串.gif b/video/344.反转字符串.gif deleted file mode 100644 index b99ef086..00000000 Binary files a/video/344.反转字符串.gif and /dev/null differ diff --git a/video/344.反转字符串.mp4 b/video/344.反转字符串.mp4 deleted file mode 100644 index 80877057..00000000 Binary files a/video/344.反转字符串.mp4 and /dev/null differ diff --git a/video/450.删除二叉搜索树中的节点.gif b/video/450.删除二叉搜索树中的节点.gif deleted file mode 100644 index d6f8a639..00000000 Binary files a/video/450.删除二叉搜索树中的节点.gif and /dev/null differ diff --git a/video/450.删除二叉搜索树中的节点.mp4 b/video/450.删除二叉搜索树中的节点.mp4 deleted file mode 100644 index a835887d..00000000 Binary files a/video/450.删除二叉搜索树中的节点.mp4 and /dev/null differ diff --git a/video/53.最大子序和.gif b/video/53.最大子序和.gif deleted file mode 100644 index 7514a5ac..00000000 Binary files a/video/53.最大子序和.gif and /dev/null differ diff --git a/video/53.最大子序和.mp4 b/video/53.最大子序和.mp4 deleted file mode 100644 index 94b62a73..00000000 Binary files a/video/53.最大子序和.mp4 and /dev/null differ diff --git a/video/617.合并二叉树.gif b/video/617.合并二叉树.gif deleted file mode 100644 index 00a8479b..00000000 Binary files a/video/617.合并二叉树.gif and /dev/null differ diff --git a/video/654.最大二叉树.gif b/video/654.最大二叉树.gif deleted file mode 100644 index 3baa8158..00000000 Binary files a/video/654.最大二叉树.gif and /dev/null differ diff --git a/video/654.最大二叉树.mp4 b/video/654.最大二叉树.mp4 deleted file mode 100644 index 70515bfb..00000000 Binary files a/video/654.最大二叉树.mp4 and /dev/null differ diff --git a/video/701.二叉搜索树中的插入操作.gif b/video/701.二叉搜索树中的插入操作.gif deleted file mode 100644 index 184937d0..00000000 Binary files a/video/701.二叉搜索树中的插入操作.gif and /dev/null differ diff --git a/video/701.二叉搜索树中的插入操作.mp4 b/video/701.二叉搜索树中的插入操作.mp4 deleted file mode 100644 index a3f00c95..00000000 Binary files a/video/701.二叉搜索树中的插入操作.mp4 and /dev/null differ diff --git a/video/844.比较含退格的字符串.gif b/video/844.比较含退格的字符串.gif deleted file mode 100644 index 39cf9b33..00000000 Binary files a/video/844.比较含退格的字符串.gif and /dev/null differ diff --git a/video/925.长按键入.gif b/video/925.长按键入.gif deleted file mode 100644 index 5e986d1a..00000000 Binary files a/video/925.长按键入.gif and /dev/null differ diff --git a/video/977.有序数组的平方.gif b/video/977.有序数组的平方.gif deleted file mode 100644 index 38b3788d..00000000 Binary files a/video/977.有序数组的平方.gif and /dev/null differ diff --git a/video/KMP精讲1.gif b/video/KMP精讲1.gif deleted file mode 100644 index 74faeba8..00000000 Binary files a/video/KMP精讲1.gif and /dev/null differ diff --git a/video/KMP精讲2.gif b/video/KMP精讲2.gif deleted file mode 100644 index 5a88d49b..00000000 Binary files a/video/KMP精讲2.gif and /dev/null differ diff --git a/video/KMP精讲3.gif b/video/KMP精讲3.gif deleted file mode 100644 index 715e2aa8..00000000 Binary files a/video/KMP精讲3.gif and /dev/null differ diff --git a/video/KMP精讲4.gif b/video/KMP精讲4.gif deleted file mode 100644 index 0f0f68ca..00000000 Binary files a/video/KMP精讲4.gif and /dev/null differ diff --git a/video/中序遍历迭代(统一写法).mp4 b/video/中序遍历迭代(统一写法).mp4 deleted file mode 100644 index 6f3c1cc2..00000000 Binary files a/video/中序遍历迭代(统一写法).mp4 and /dev/null differ diff --git a/video/二叉树中序遍历(迭代法).gif b/video/二叉树中序遍历(迭代法).gif deleted file mode 100644 index ef868ec6..00000000 Binary files a/video/二叉树中序遍历(迭代法).gif and /dev/null differ diff --git a/video/二叉树中序遍历(迭代法).mp4 b/video/二叉树中序遍历(迭代法).mp4 deleted file mode 100644 index e5eeec05..00000000 Binary files a/video/二叉树中序遍历(迭代法).mp4 and /dev/null differ diff --git a/video/二叉树前序遍历(迭代法).gif b/video/二叉树前序遍历(迭代法).gif deleted file mode 100644 index 78d2187b..00000000 Binary files a/video/二叉树前序遍历(迭代法).gif and /dev/null differ diff --git a/video/二叉树前序遍历(迭代法).mp4 b/video/二叉树前序遍历(迭代法).mp4 deleted file mode 100644 index fb92953e..00000000 Binary files a/video/二叉树前序遍历(迭代法).mp4 and /dev/null differ diff --git a/video/对称二叉树.mp4 b/video/对称二叉树.mp4 deleted file mode 100644 index 96d907fd..00000000 Binary files a/video/对称二叉树.mp4 and /dev/null differ diff --git a/video/替换空格.gif b/video/替换空格.gif deleted file mode 100644 index c98dae4f..00000000 Binary files a/video/替换空格.gif and /dev/null differ diff --git a/video/替换空格.mp4 b/video/替换空格.mp4 deleted file mode 100644 index 223743cd..00000000 Binary files a/video/替换空格.mp4 and /dev/null differ diff --git a/video/翻转二叉树.gif b/video/翻转二叉树.gif deleted file mode 100644 index afdfe7c0..00000000 Binary files a/video/翻转二叉树.gif and /dev/null differ diff --git a/video/翻转二叉树.mp4 b/video/翻转二叉树.mp4 deleted file mode 100644 index 73636b7b..00000000 Binary files a/video/翻转二叉树.mp4 and /dev/null differ