diff --git a/.gitignore b/.gitignore index 33bd03c6a..44e4ee16f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -*~ -_book/ +*~ +_book/ .DS_Store \ No newline at end of file diff --git a/README.md b/README.md index b6c29d9e5..2d5682cc4 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,21 @@ -# 介绍 - -题解预览地址:[https://leetcode.wang](https://leetcode.wang),推荐电脑端打开,手机打开的话将页面滑到最上边,左上角是菜单 - -leetcode 题目地址 https://leetcode.com/problemset/all/ - -github 项目地址:https://github.com/wind-liang/leetcode - -为什么刷题:https://leetcode.wang/leetcode100%E6%96%A9%E5%9B%9E%E9%A1%BE.html - -知乎开设了专栏,同步更新:[https://zhuanlan.zhihu.com/leetcode1024](https://zhuanlan.zhihu.com/leetcode1024),关注后可以及时收到更新,网站有时候可能出问题打不开,建议关注一下知乎专栏备用 - -准备刷一道,总结一道。 - -可以加好友一起交流。 - -微信: 17771420231 - -公众号: windliang,更新编程相关 - -如果觉得对你有帮助,记得给一个 star 哦 ^ ^ +# 介绍 + +题解预览地址:[https://leetcode.wang](https://leetcode.wang),推荐电脑端打开,手机打开的话将页面滑到最上边,左上角是菜单 + +leetcode 题目地址 https://leetcode.com/problemset/all/ + +github 项目地址:https://github.com/wind-liang/leetcode + +为什么刷题:https://leetcode.wang/leetcode100%E6%96%A9%E5%9B%9E%E9%A1%BE.html + +知乎开设了专栏,同步更新:[https://zhuanlan.zhihu.com/leetcode1024](https://zhuanlan.zhihu.com/leetcode1024),关注后可以及时收到更新,网站有时候可能出问题打不开,建议关注一下知乎专栏备用 + +准备刷一道,总结一道。 + +可以加好友一起交流。 + +微信: 17771420231 + +公众号: windliang,更新编程相关 + +如果觉得对你有帮助,记得给一个 star 哦 ^ ^ diff --git a/SUMMARY.md b/SUMMARY.md index afecb2e53..d9c9a76f0 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -1,255 +1,255 @@ -# Summary - -* [Introduction](README.md) -* [leetcode 100 斩!回顾](leetcode100斩回顾.md) -* [leetcode 力扣刷题 1 到 300 的感受](leetcode力扣刷题1到300的感受.md) -* [极客时间优惠](极客时间优惠红包返现.md) -* [1. Two Sum](leetCode-1-Two-Sum.md) -* [2. Add Two Numbers](leetCode-2-Add-Two-Numbers.md) -* [3. Longest Substring Without Repeating Characters](leetCode-3-Longest-Substring-Without-Repeating-Characters.md) -* [4*. Median of Two Sorted Arrays](leetCode-4-Median-of-Two-Sorted-Arrays.md) -* [5*. Longest Palindromic Substring](leetCode-5-Longest-Palindromic-Substring.md) -* [6. ZigZag Conversion](leetCode-6-ZigZag-Conversion.md) -* [7. Reverse Integer](leetCode-7-Reverse-Integer.md) -* [8. String to Integer](leetCode-8-String-to-Integer.md) -* [9. Palindrome Number](leetCode-9-Palindrome-Number.md) -* [10. Regular Expression Matching](leetCode-10-Regular-Expression-Matching.md) -* [11. Container With Most Water](leetCode-11-Container-With-Most-Water.md) -* [12. Integer to Roman](leetCode-12-Integer-to-Roman.md) -* [13. Roman to Integer](leetCode-13-Roman-to-Integer.md) -* [14. Longest Common Prefix](leetCode-14-Longest-Common-Prefix.md) -* [15. 3Sum](leetCode-15-3Sum.md) -* [16. 3Sum Closest](leetCode-16-3Sum-Closest.md) -* [17. Letter Combinations of a Phone Number](leetCode-17-Letter-Combinations-of-a-Phone-Number.md) -* [18. 4Sum](leetCode-18-4Sum.md) -* [19. Remove Nth Node From End of List](leetCode-19-Remov-Nth-Node-From-End-of-List.md) -* [20. Valid Parentheses](leetCode-20-Valid Parentheses.md) -* [21. Merge Two Sorted Lists](leetCode-21-Merge-Two-Sorted-Lists.md) -* [22. Generate Parentheses](leetCode-22-Generate-Parentheses.md)Merge k Sorted Lists -* [23. Merge k Sorted Lists](leetCode-23-Merge-k-Sorted-Lists.md) -* [24. Swap Nodes in Pairs](leetCode-24-Swap-Nodes-in-Pairs.md) -* [25. Reverse Nodes in k-Group](leetCode-25-Reverse-Nodes-in-k-Group.md) -* [26. Remove Duplicates from Sorted Array](leetCode-26-Remove-Duplicates-from-Sorted-Array.md) -* [27. Remove Element](leetCode-27-Remove-Element.md) -* [28. Implement strStr()](leetCode-28-Implement-strStr.md) -* [29. Divide Two Integers](leetCode-29-Divide-Two-Integers.md) -* [30. Substring with Concatenation of All Words](leetCode-30-Substring-with-Concatenation-of-All-Words.md) -* [31. Next Permutation](leetCode-31-Next-Permutation.md) -* [32. Longest Valid Parentheses](leetCode-32-Longest-Valid-Parentheses.md) -* [33. Search in Rotated Sorted Array](leetCode-33-Search-in-Rotated-Sorted-Array.md) -* [34. Find First and Last Position of Element in Sorted Array](leetCode-34-Find-First-and-Last-Position-of-Element-in-Sorted-Array.md) -* [35. Search Insert Position](leetCode-35-Search-Insert-Position.md) -* [36. Valid Sudoku](leetCode-36-Valid-Sudoku.md) -* [37. Sudoku Solver](leetCode-37-Sudoku-Solver.md) -* [38. Count and Say](leetCode-38-Count-and-Say.md) -* [39. Combination Sum](leetCode-39-Combination-Sum.md) -* [40. Combination Sum II](leetCode-40-Combination-Sum-II.md) -* [41. First Missing Positive](leetCode-41-First-Missing-Positive.md) -* [42. Trapping Rain Water](leetCode-42-Trapping-Rain-Water.md) -* [43. Multiply Strings](leetCode-43-Multiply-Strings.md) -* [44. Wildcard Matching](leetCode-44-Wildcard-Matching.md) -* [45. Jump Game II](leetCode-45-Jump-Game-II.md) -* [46. Permutations](leetCode-46-Permutations.md) -* [47. Permutations II](leetCode-47-Permutations-II.md) -* [48. Rotate Image](leetCode-48-Rotate-Image.md) -* [49. Group Anagrams](leetCode-49-Group-Anagrams.md) -* [50*. Pow(x, n)](leetCode-50-Pow.md) -* [51. N-Queens](leetCode-51-N-Queens.md) -* [52. N-Queens II](leetCode-52-N-QueensII.md) -* [53. Maximum Subarray](leetCode-53-Maximum-Subarray.md) -* [54. Spiral Matrix](leetCode-54-Spiral-Matrix.md) -* [55. Jump Game](leetCode-55-Jump-Game.md) -* [56. Merge Intervals](leetCode-56-Merge-Intervals.md) -* [57. Insert Interval](leetCode-57-Insert-Interval.md) -* [58. Length of Last Word](leetCode-58-Length-of-Last-Word.md) -* [59. Spiral Matrix II](leetCode-59-Spiral-MatrixII.md) -* [60. Permutation Sequence](leetCode-60-Permutation-Sequence.md) -* [61. Rotate List](leetCode-61-Rotate-List.md) -* [62. Unique Paths](leetCode-62-Unique-Paths.md) -* [63. Unique Paths II](leetCode-63-Unique-PathsII.md) -* [64. Minimum Path Sum](leetCode-64-Minimum-PathSum.md) -* [65. Valid Number](leetCode-65-Valid-Number.md) -* [66. Plus One](leetCode-66-Plus-One.md) -* [67. Add Binary](leetCode-67-Add Binary.md) -* [68. Text Justification](leetCode-68-Text-Justification.md) -* [69. Sqrt x](leetCode-69-Sqrtx.md) -* [70. Climbing Stairs](leetCode-70-Climbing-Stairs.md) -* [71. Simplify Path](leetCode-71-Simplify-Path.md) -* [72. Edit Distance](leetCode-72-Edit-Distance.md) -* [73. Set Matrix Zeroes](leetcode-73-Set-Matrix-Zeroes.md) -* [74. Search a 2D Matrix](leetCode-74-Search-a-2D-Matrix.md) -* [75. Sort Colors](leetCode-75-Sort-Colors.md) -* [76. Minimum Window Substring](leetCode-76-Minimum-Window-Substring.md) -* [77. Combinations](leetCode-77-Combinations.md) -* [78. Subsets](leetCode-78-Subsets.md) -* [79. Word Search](leetCode-79-Word-Search.md) -* [80. Remove Duplicates from Sorted Array II](leetCode-80-Remove-Duplicates-from-Sorted-ArrayII.md) -* [81. Search in Rotated Sorted Array II](leetCode-81-Search-in-Rotated-Sorted-ArrayII.md) -* [82. Remove Duplicates from Sorted List II](leetCode-82-Remove-Duplicates-from-Sorted-ListII.md) -* [83. Remove Duplicates from Sorted List](leetCode-83-Remove-Duplicates-from-Sorted-List.md) -* [84. Largest Rectangle in Histogram](leetCode-84-Largest-Rectangle-in-Histogram.md) -* [85. Maximal Rectangle](leetCode-85-Maximal-Rectangle.md) -* [86. Partition List](leetCode-86-Partition-List.md) -* [87. Scramble String](leetCode-87-Scramble-String.md) -* [88. Merge Sorted Array](leetCode-88-Merge-Sorted-Array.md) -* [89. Gray Code](leetCode-89-Gray-Code.md) -* [90. Subsets II](leetCode-90-SubsetsII.md) -* [91. Decode Ways](leetcode-91-Decode-Ways.md) -* [92. Reverse Linked List II](leetCode-92-Reverse-Linked-ListII.md) -* [93. Restore IP Addresses](leetCode-93-Restore-IP-Addresses.md) -* [94. Binary Tree Inorder Traversal](leetCode-94-Binary-Tree-Inorder-Traversal.md) -* [95*. Unique Binary Search Trees II](leetCode-95-Unique-Binary-Search-TreesII.md) -* [96. Unique Binary Search Trees](leetCode-96-Unique-Binary-Search-Trees.md) -* [97. Interleaving String](leetCode-97-Interleaving-String.md) -* [98. Validate Binary Search Tree](leetCode-98-Validate-Binary-Search-Tree.md) -* [99. Recover Binary Search Tree](leetcode-99-Recover-Binary-Search-Tree.md) -* [100. Same Tree](leetcode-100-Same-Tree.md) -* [101 题到 200 题](leetcode-101-200.md) - * [101. Symmetric Tree](leetcode-101-Symmetric-Tree.md) - * [102. Binary Tree Level Order Traversal](leetcode-102-Binary-Tree-Level-Order-Traversal.md) - * [103. Binary Tree Zigzag Level Order Traversal](leetcode-103-Binary-Tree-Zigzag-Level-Order-Traversal.md) - * [104. Maximum Depth of Binary Tree](leetcode-104-Maximum-Depth-of-Binary-Tree.md) - * [105. Construct Binary Tree from Preorder and Inorder Traversal](leetcode-105-Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal.md) - * [106. Construct Binary Tree from Inorder and Postorder Traversal](leetcode-106-Construct-Binary-Tree-from-Inorder-and-Postorder-Traversal.md) - * [107. Binary Tree Level Order Traversal II](leetcode-107-Binary-Tree-Level-Order-TraversalII.md) - * [108. Convert Sorted Array to Binary Search Tree](leetcode-108-Convert-Sorted-Array-to-Binary-Search-Tree.md) - * [109. Convert Sorted List to Binary Search Tree](leetcode-109-Convert-Sorted-List-to-Binary-Search-Tree.md) - * [110. Balanced Binary Tree](leetcode-110-Balanced-Binary-Tree.md) - * [111. Minimum Depth of Binary Tree](leetcode-111-Minimum-Depth-of-Binary-Tree.md) - * [112. Path Sum](leetcode-112-Path-Sum.md) - * [113. Path Sum II](leetcode-113-Path-SumII.md) - * [114. Flatten Binary Tree to Linked List](leetcode-114-Flatten-Binary-Tree-to-Linked-List.md) - * [115*. Distinct Subsequences](leetcode-115-Distinct-Subsequences.md) - * [116. Populating Next Right Pointers in Each Node](leetcode-116-Populating-Next-Right-Pointers-in-Each-Node.md) - * [117. Populating Next Right Pointers in Each Node II](leetcode-117-Populating-Next-Right-Pointers-in-Each-NodeII.md) - * [118. Pascal's Triangle](leetcode-118-Pascal's-Triangle.md) - * [119. Pascal's Triangle II](leetcode-119-Pascal's-TriangleII.md) - * [120. Triangle](leetcode-120-Triangle.md) - * [121. Best Time to Buy and Sell Stock](leetcode-121-Best-Time-to-Buy-and-Sell-Stock.md) - * [122. Best Time to Buy and Sell Stock II](leetcode-122-Best-Time-to-Buy-and-Sell-StockII.md) - * [123*. Best Time to Buy and Sell Stock III](leetcode-123-Best-Time-to-Buy-and-Sell-StockIII.md) - * [124*. Binary Tree Maximum Path Sum](leetcode-124-Binary-Tree-Maximum-Path-Sum.md) - * [125. Valid Palindrome](leetcode-125-Valid-Palindrome.md) - * [126*. Word Ladder II](leetCode-126-Word-LadderII.md) - * [127. Word Ladder](leetCode-127-Word-Ladder.md) - * [128. Longest Consecutive Sequence](leetcode-128-Longest-Consecutive-Sequence.md) - * [129. Sum Root to Leaf Numbers](leetcode-129-Sum-Root-to-Leaf-Numbers.md) - * [130*. Surrounded Regions](leetcode-130-Surrounded-Regions.md) - * [131. Palindrome Partitioning](leetcode-131-Palindrome-Partitioning.md) - * [132. Palindrome Partitioning II](leetcode-132-Palindrome-PartitioningII.md) - * [133. Clone Graph](leetcode-133-Clone-Graph.md) - * [134. Gas Station](leetcode-134-Gas-Station.md) - * [135. Candy](leetcode-135-Candy.md) - * [136. Single Number](leetcode-136-Single-Number.md) - * [137*. Single Number II](leetcode-137-Single-NumberII.md) - * [138. Copy List with Random Pointer](leetcode-138-Copy-List-with-Random-Pointer.md) - * [139. Word Break](leetcode-139-Word-Break.md) - * [140. Word Break II](leetcode-140-Word-BreakII.md) - * [141. Linked List Cycle](leetcode-141-Linked-List-Cycle.md) - * [142. Linked List Cycle II](leetcode-142-Linked-List-CycleII.md) - * [143. Reorder List](leetcode-143-Reorder-List.md) - * [144. Binary Tree Preorder Traversal](leetcode-144-Binary-Tree-Preorder-Traversal.md) - * [145*. Binary Tree Postorder Traversal](leetcode-145-Binary-Tree-Postorder-Traversal.md) - * [146. LRU Cache](leetcode-146-LRU-Cache.md) - * [147. Insertion Sort List](leetcode-147-Insertion-Sort-List.md) - * [148. Sort List](leetcode-148-Sort-List.md) - * [149*. Max Points on a Line](leetcode-149-Max-Points-on-a-Line.md) - * [150. Evaluate Reverse Polish Notation](leetcode-150-Evaluate-Reverse-Polish-Notation.md) - * [151. Reverse Words in a String](leetcode-151-Reverse-Words-in-a-String.md) - * [152. Maximum Product Subarray](leetcode-152-Maximum-Product-Subarray.md) - * [153. Find Minimum in Rotated Sorted Array](leetcode-153-Find-Minimum-in-Rotated-Sorted-Array.md) - * [154*. Find Minimum in Rotated Sorted Array II](leetcode-154-Find-Minimum-in-Rotated-Sorted-ArrayII.md) - * [155. Min Stack](leetcode-155-Min-Stack.md) - * [160. Intersection of Two Linked Lists](leetcode-160-Intersection-of-Two-Linked-Lists.md) - * [162. Find Peak Element](leetcode-162-Find-Peak-Element.md) - * [164. Maximum Gap](leetcode-164-Maximum-Gap.md) - * [165. Compare Version Numbers](leetcode-165-Compare-Version-Numbers.md) - * [166. Fraction to Recurring Decimal](leetcode-166-Fraction-to-Recurring-Decimal.md) - * [167. Two Sum II - Input array is sorted](leetcode-167-Two-SumII-Input-array-is-sorted.md) - * [168. Excel Sheet Column Title](leetcode-168-Excel-Sheet-Column-Title.md) - * [169. Majority Element](leetcode-169-Majority-Element.md) - * [171. Excel Sheet Column Number](leetcode-171-Excel-Sheet-Column-Number.md) - * [172. Factorial Trailing Zeroes](leetcode-172-Factorial-Trailing-Zeroes.md) - * [173. Binary Search Tree Iterator](leetcode-173-Binary-Search-Tree-Iterator.md) - * [174*. Dungeon Game](leetcode-174-Dungeon-Game.md) - * [179. Largest Number](leetcode-179-Largest-Number.md) - * [187. Repeated DNA Sequences](leetcode-187-Repeated-DNA-Sequences.md) - * [188. Best Time to Buy and Sell Stock IV](leetcode-188-Best-Time-to-Buy-and-Sell-StockIV.md) - * [189. Rotate Array](leetcode-189-Rotate-Array.md) - * [190. Reverse Bits](leetcode-190-Reverse-Bits.md) - * [191. Number of 1 Bits](leetcode-191-Number-of-1-Bits.md) - * [198. House Robber](leetcode-198-House-Robber.md) - * [199. Binary Tree Right Side View](leetcode-199-Binary-Tree-Right-Side-View.md) - * [200. Number of Islands](leetcode-200-Number-of-Islands.md) -* [201 题到 300 题](leetcode-201-300.md) - * [201. Bitwise AND of Numbers Range](leetcode-201-Bitwise-AND-of-Numbers-Range.md) - * [202. Happy Number](leetcode-202-Happy-Number.md) - * [203. Remove Linked List Elements](leetcode-203-Remove-Linked-List-Elements.md) - * [204. Count Primes](leetcode-204-Count-Primes.md) - * [205. Isomorphic Strings](leetcode-205-Isomorphic-Strings.md) - * [206. Reverse Linked List](leetcode-206-Reverse-Linked-List.md) - * [207. Course Schedule](leetcode-207-Course-Schedule.md) - * [208. Implement Trie(Prefix Tree)](leetcode-208-Implement-Trie-Prefix-Tree.md) - * [209. Minimum Size Subarray Sum](leetcode-209-Minimum-Size-Subarray-Sum.md) - * [210. Course Schedule II](leetcode-210-Course-ScheduleII.md) - * [211. Add and Search Word - Data structure design](leetcode-211-Add-And-Search-Word-Data-structure-design.md) - * [212. Word Search II](leetcode-212-Word-SearchII.md) - * [213. House Robber II](leetcode-213-House-RobberII.md) - * [214*. Shortest Palindrome](leetcode-214-Shortest-Palindrome.md) - * [215. Kth Largest Element in an Array](leetcode-215-Kth-Largest-Element-in-an-Array.md) - * [216. Combination Sum III](leetcode-216-Combination-SumIII.md) - * [217. Contains Duplicate](leetcode-217-Contains-Duplicate.md) - * [218. The Skyline Problem](leetcode-218-The-Skyline-Problem.md) - * [219. Contains Duplicate II](leetcode-219-ContainsDuplicateII.md) - * [220*. Contains Duplicate III](leetcode-220-Contains-DuplicateIII.md) - * [221. Maximal Square](leetcode-221-Maximal-Square.md) - * [222. Count Complete Tree Nodes](leetcode-222-Count-Complete-Tree-Nodes.md) - * [223. Rectangle Area](leetcode-223-Rectangle-Area.md) - * [224*. Basic Calculator](leetcode-224-Basic-Calculator.md) - * [225. Implement Stack using Queues](leetcode-225-Implement-Stack-using-Queues.md) - * [226. Invert Binary Tree](leetcode-226-Invert-Binary-Tree.md) - * [227. Basic Calculator II](leetcode-227-Basic-CalculatorII.md) - * [228. Summary Ranges](leetcode-228-Summary-Ranges.md) - * [229. Majority Element II](leetcode-229-Majority-ElementII.md) - * [230. Kth Smallest Element in a BST](leetcode-230-Kth-Smallest-Element-in-a-BST.md) - * [231*. Power of Two](leetcode-231-Power-of-Two.md) - * [232. Implement Queue using Stacks](leetcode-232-Implement-Queue-using-Stacks.md) - * [233. Number of Digit One](leetcode-233-Number-of-Digit-One.md) - * [234. Palindrome Linked List](leetcode-234-Palindrome-Linked-List.md) - * [235. Lowest Common Ancestor of a Binary Search Tree](leetcode-235-Lowest-Common-Ancestor-of-a-Binary-Search-Tree.md) - * [236. Lowest Common Ancestor of a Binary Tree](leetcode-236-Lowest-Common-Ancestor-of-a-Binary-Tree.md) - * [237. Delete Node in a Linked List](leetcode-237-Delete-Node-in-a-Linked-List.md) - * [238. Product of Array Except Self](leetcode-238-Product-of-Array-Except-Self.md) - * [239. Sliding Window Maximum](leetcode-239-Sliding-Window-Maximum.md) - * [240. Search a 2D Matrix II](leetcode-240-Search-a-2D-MatrixII.md) - * [241. Different Ways to Add Parentheses](leetcode-241-Different-Ways-to-Add-Parentheses.md) - * [242. Valid Anagram](leetcode-242-Valid-Anagram.md) - * [257. Binary Tree Paths](leetcode-257-Binary-Tree-Paths.md) - * [258. Add Digits](leetcode-258-Add-Digits.md) - * [260. Single Number III](leetcode-260-Single-NumberIII.md) - * [263. Ugly Number](leetcode-263-Ugly-Number.md) - * [264. Ugly Number II](leetcode-264-Ugly-NumberII.md) - * [268. Missing Number](leetcode-268-Missing-Number.md) - * [273. Integer to English Words](leetcode-273-Intege-to-English-Words.md) - * [274. H-Index](leetcode-274-H-Index.md) - * [275. H-Index II](leetcode-275-H-IndexII.md) - * [278. First Bad Version](leetcode-278-First-Bad-Version.md) - * [279. Perfect Squares](leetcode-279-Perfect-Squares.md) - * [282. Expression Add Operators](leetcode-282-Expression-Add-Operators.md) - * [283. Move Zeroes](leetcode-283-Move-Zeroes.md) - * [284. Peeking Iterator](leetcode-284-Peeking-Iterator.md) - * [287*. Find the Duplicate Number](leetcode-287-Find-the-Duplicate-Number.md) - * [289*. Game of Life](leetcode-289-Game-of-Life.md) - * [290. Word Pattern](leetcode-290-Word-Pattern.md) - * [292*. Nim Game](leetcode-292-Nim-Game.md) - * [295*. Find Median from Data Stream](leetcode-295-Find-Median-from-Data-Stream.md) - * [297. Serialize and Deserialize Binary Tree](leetcode-297-Serialize-and-Deserialize-Binary-Tree.md) - * [299. Bulls and Cows](leetcode-299-Bulls-and-Cows.md) - * [300. Longest Increasing Subsequence](leetcode-300-Longest-Increasing-Subsequence.md) -* [301 题到 400 题](leetcode-301-400.md) - * [301. Remove Invalid Parentheses](leetcode-301-Remove-Invalid-Parentheses.md) - * [303. Range Sum Query - Immutable](leetcode-303-Range-Sum-Query-Immutable.md) - * [304. Range Sum Query 2D - Immutable](leetcode-304-Range-Sum-Query-2D-Immutable.md) - * [306. Additive Number](leetcode-306-Additive-Number.md) - * [307. Range Sum Query - Mutable](leetcode-307-Range-Sum-Query-Mutable.md) +# Summary + +* [Introduction](README.md) +* [leetcode 100 斩!回顾](leetcode100斩回顾.md) +* [leetcode 力扣刷题 1 到 300 的感受](leetcode力扣刷题1到300的感受.md) +* [极客时间优惠](极客时间优惠红包返现.md) +* [1. Two Sum](leetCode-1-Two-Sum.md) +* [2. Add Two Numbers](leetCode-2-Add-Two-Numbers.md) +* [3. Longest Substring Without Repeating Characters](leetCode-3-Longest-Substring-Without-Repeating-Characters.md) +* [4*. Median of Two Sorted Arrays](leetCode-4-Median-of-Two-Sorted-Arrays.md) +* [5*. Longest Palindromic Substring](leetCode-5-Longest-Palindromic-Substring.md) +* [6. ZigZag Conversion](leetCode-6-ZigZag-Conversion.md) +* [7. Reverse Integer](leetCode-7-Reverse-Integer.md) +* [8. String to Integer](leetCode-8-String-to-Integer.md) +* [9. Palindrome Number](leetCode-9-Palindrome-Number.md) +* [10. Regular Expression Matching](leetCode-10-Regular-Expression-Matching.md) +* [11. Container With Most Water](leetCode-11-Container-With-Most-Water.md) +* [12. Integer to Roman](leetCode-12-Integer-to-Roman.md) +* [13. Roman to Integer](leetCode-13-Roman-to-Integer.md) +* [14. Longest Common Prefix](leetCode-14-Longest-Common-Prefix.md) +* [15. 3Sum](leetCode-15-3Sum.md) +* [16. 3Sum Closest](leetCode-16-3Sum-Closest.md) +* [17. Letter Combinations of a Phone Number](leetCode-17-Letter-Combinations-of-a-Phone-Number.md) +* [18. 4Sum](leetCode-18-4Sum.md) +* [19. Remove Nth Node From End of List](leetCode-19-Remov-Nth-Node-From-End-of-List.md) +* [20. Valid Parentheses](leetCode-20-Valid Parentheses.md) +* [21. Merge Two Sorted Lists](leetCode-21-Merge-Two-Sorted-Lists.md) +* [22. Generate Parentheses](leetCode-22-Generate-Parentheses.md)Merge k Sorted Lists +* [23. Merge k Sorted Lists](leetCode-23-Merge-k-Sorted-Lists.md) +* [24. Swap Nodes in Pairs](leetCode-24-Swap-Nodes-in-Pairs.md) +* [25. Reverse Nodes in k-Group](leetCode-25-Reverse-Nodes-in-k-Group.md) +* [26. Remove Duplicates from Sorted Array](leetCode-26-Remove-Duplicates-from-Sorted-Array.md) +* [27. Remove Element](leetCode-27-Remove-Element.md) +* [28. Implement strStr()](leetCode-28-Implement-strStr.md) +* [29. Divide Two Integers](leetCode-29-Divide-Two-Integers.md) +* [30. Substring with Concatenation of All Words](leetCode-30-Substring-with-Concatenation-of-All-Words.md) +* [31. Next Permutation](leetCode-31-Next-Permutation.md) +* [32. Longest Valid Parentheses](leetCode-32-Longest-Valid-Parentheses.md) +* [33. Search in Rotated Sorted Array](leetCode-33-Search-in-Rotated-Sorted-Array.md) +* [34. Find First and Last Position of Element in Sorted Array](leetCode-34-Find-First-and-Last-Position-of-Element-in-Sorted-Array.md) +* [35. Search Insert Position](leetCode-35-Search-Insert-Position.md) +* [36. Valid Sudoku](leetCode-36-Valid-Sudoku.md) +* [37. Sudoku Solver](leetCode-37-Sudoku-Solver.md) +* [38. Count and Say](leetCode-38-Count-and-Say.md) +* [39. Combination Sum](leetCode-39-Combination-Sum.md) +* [40. Combination Sum II](leetCode-40-Combination-Sum-II.md) +* [41. First Missing Positive](leetCode-41-First-Missing-Positive.md) +* [42. Trapping Rain Water](leetCode-42-Trapping-Rain-Water.md) +* [43. Multiply Strings](leetCode-43-Multiply-Strings.md) +* [44. Wildcard Matching](leetCode-44-Wildcard-Matching.md) +* [45. Jump Game II](leetCode-45-Jump-Game-II.md) +* [46. Permutations](leetCode-46-Permutations.md) +* [47. Permutations II](leetCode-47-Permutations-II.md) +* [48. Rotate Image](leetCode-48-Rotate-Image.md) +* [49. Group Anagrams](leetCode-49-Group-Anagrams.md) +* [50*. Pow(x, n)](leetCode-50-Pow.md) +* [51. N-Queens](leetCode-51-N-Queens.md) +* [52. N-Queens II](leetCode-52-N-QueensII.md) +* [53. Maximum Subarray](leetCode-53-Maximum-Subarray.md) +* [54. Spiral Matrix](leetCode-54-Spiral-Matrix.md) +* [55. Jump Game](leetCode-55-Jump-Game.md) +* [56. Merge Intervals](leetCode-56-Merge-Intervals.md) +* [57. Insert Interval](leetCode-57-Insert-Interval.md) +* [58. Length of Last Word](leetCode-58-Length-of-Last-Word.md) +* [59. Spiral Matrix II](leetCode-59-Spiral-MatrixII.md) +* [60. Permutation Sequence](leetCode-60-Permutation-Sequence.md) +* [61. Rotate List](leetCode-61-Rotate-List.md) +* [62. Unique Paths](leetCode-62-Unique-Paths.md) +* [63. Unique Paths II](leetCode-63-Unique-PathsII.md) +* [64. Minimum Path Sum](leetCode-64-Minimum-PathSum.md) +* [65. Valid Number](leetCode-65-Valid-Number.md) +* [66. Plus One](leetCode-66-Plus-One.md) +* [67. Add Binary](leetCode-67-Add Binary.md) +* [68. Text Justification](leetCode-68-Text-Justification.md) +* [69. Sqrt x](leetCode-69-Sqrtx.md) +* [70. Climbing Stairs](leetCode-70-Climbing-Stairs.md) +* [71. Simplify Path](leetCode-71-Simplify-Path.md) +* [72. Edit Distance](leetCode-72-Edit-Distance.md) +* [73. Set Matrix Zeroes](leetcode-73-Set-Matrix-Zeroes.md) +* [74. Search a 2D Matrix](leetCode-74-Search-a-2D-Matrix.md) +* [75. Sort Colors](leetCode-75-Sort-Colors.md) +* [76. Minimum Window Substring](leetCode-76-Minimum-Window-Substring.md) +* [77. Combinations](leetCode-77-Combinations.md) +* [78. Subsets](leetCode-78-Subsets.md) +* [79. Word Search](leetCode-79-Word-Search.md) +* [80. Remove Duplicates from Sorted Array II](leetCode-80-Remove-Duplicates-from-Sorted-ArrayII.md) +* [81. Search in Rotated Sorted Array II](leetCode-81-Search-in-Rotated-Sorted-ArrayII.md) +* [82. Remove Duplicates from Sorted List II](leetCode-82-Remove-Duplicates-from-Sorted-ListII.md) +* [83. Remove Duplicates from Sorted List](leetCode-83-Remove-Duplicates-from-Sorted-List.md) +* [84. Largest Rectangle in Histogram](leetCode-84-Largest-Rectangle-in-Histogram.md) +* [85. Maximal Rectangle](leetCode-85-Maximal-Rectangle.md) +* [86. Partition List](leetCode-86-Partition-List.md) +* [87. Scramble String](leetCode-87-Scramble-String.md) +* [88. Merge Sorted Array](leetCode-88-Merge-Sorted-Array.md) +* [89. Gray Code](leetCode-89-Gray-Code.md) +* [90. Subsets II](leetCode-90-SubsetsII.md) +* [91. Decode Ways](leetcode-91-Decode-Ways.md) +* [92. Reverse Linked List II](leetCode-92-Reverse-Linked-ListII.md) +* [93. Restore IP Addresses](leetCode-93-Restore-IP-Addresses.md) +* [94. Binary Tree Inorder Traversal](leetCode-94-Binary-Tree-Inorder-Traversal.md) +* [95*. Unique Binary Search Trees II](leetCode-95-Unique-Binary-Search-TreesII.md) +* [96. Unique Binary Search Trees](leetCode-96-Unique-Binary-Search-Trees.md) +* [97. Interleaving String](leetCode-97-Interleaving-String.md) +* [98. Validate Binary Search Tree](leetCode-98-Validate-Binary-Search-Tree.md) +* [99. Recover Binary Search Tree](leetcode-99-Recover-Binary-Search-Tree.md) +* [100. Same Tree](leetcode-100-Same-Tree.md) +* [101 题到 200 题](leetcode-101-200.md) + * [101. Symmetric Tree](leetcode-101-Symmetric-Tree.md) + * [102. Binary Tree Level Order Traversal](leetcode-102-Binary-Tree-Level-Order-Traversal.md) + * [103. Binary Tree Zigzag Level Order Traversal](leetcode-103-Binary-Tree-Zigzag-Level-Order-Traversal.md) + * [104. Maximum Depth of Binary Tree](leetcode-104-Maximum-Depth-of-Binary-Tree.md) + * [105. Construct Binary Tree from Preorder and Inorder Traversal](leetcode-105-Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal.md) + * [106. Construct Binary Tree from Inorder and Postorder Traversal](leetcode-106-Construct-Binary-Tree-from-Inorder-and-Postorder-Traversal.md) + * [107. Binary Tree Level Order Traversal II](leetcode-107-Binary-Tree-Level-Order-TraversalII.md) + * [108. Convert Sorted Array to Binary Search Tree](leetcode-108-Convert-Sorted-Array-to-Binary-Search-Tree.md) + * [109. Convert Sorted List to Binary Search Tree](leetcode-109-Convert-Sorted-List-to-Binary-Search-Tree.md) + * [110. Balanced Binary Tree](leetcode-110-Balanced-Binary-Tree.md) + * [111. Minimum Depth of Binary Tree](leetcode-111-Minimum-Depth-of-Binary-Tree.md) + * [112. Path Sum](leetcode-112-Path-Sum.md) + * [113. Path Sum II](leetcode-113-Path-SumII.md) + * [114. Flatten Binary Tree to Linked List](leetcode-114-Flatten-Binary-Tree-to-Linked-List.md) + * [115*. Distinct Subsequences](leetcode-115-Distinct-Subsequences.md) + * [116. Populating Next Right Pointers in Each Node](leetcode-116-Populating-Next-Right-Pointers-in-Each-Node.md) + * [117. Populating Next Right Pointers in Each Node II](leetcode-117-Populating-Next-Right-Pointers-in-Each-NodeII.md) + * [118. Pascal's Triangle](leetcode-118-Pascal's-Triangle.md) + * [119. Pascal's Triangle II](leetcode-119-Pascal's-TriangleII.md) + * [120. Triangle](leetcode-120-Triangle.md) + * [121. Best Time to Buy and Sell Stock](leetcode-121-Best-Time-to-Buy-and-Sell-Stock.md) + * [122. Best Time to Buy and Sell Stock II](leetcode-122-Best-Time-to-Buy-and-Sell-StockII.md) + * [123*. Best Time to Buy and Sell Stock III](leetcode-123-Best-Time-to-Buy-and-Sell-StockIII.md) + * [124*. Binary Tree Maximum Path Sum](leetcode-124-Binary-Tree-Maximum-Path-Sum.md) + * [125. Valid Palindrome](leetcode-125-Valid-Palindrome.md) + * [126*. Word Ladder II](leetCode-126-Word-LadderII.md) + * [127. Word Ladder](leetCode-127-Word-Ladder.md) + * [128. Longest Consecutive Sequence](leetcode-128-Longest-Consecutive-Sequence.md) + * [129. Sum Root to Leaf Numbers](leetcode-129-Sum-Root-to-Leaf-Numbers.md) + * [130*. Surrounded Regions](leetcode-130-Surrounded-Regions.md) + * [131. Palindrome Partitioning](leetcode-131-Palindrome-Partitioning.md) + * [132. Palindrome Partitioning II](leetcode-132-Palindrome-PartitioningII.md) + * [133. Clone Graph](leetcode-133-Clone-Graph.md) + * [134. Gas Station](leetcode-134-Gas-Station.md) + * [135. Candy](leetcode-135-Candy.md) + * [136. Single Number](leetcode-136-Single-Number.md) + * [137*. Single Number II](leetcode-137-Single-NumberII.md) + * [138. Copy List with Random Pointer](leetcode-138-Copy-List-with-Random-Pointer.md) + * [139. Word Break](leetcode-139-Word-Break.md) + * [140. Word Break II](leetcode-140-Word-BreakII.md) + * [141. Linked List Cycle](leetcode-141-Linked-List-Cycle.md) + * [142. Linked List Cycle II](leetcode-142-Linked-List-CycleII.md) + * [143. Reorder List](leetcode-143-Reorder-List.md) + * [144. Binary Tree Preorder Traversal](leetcode-144-Binary-Tree-Preorder-Traversal.md) + * [145*. Binary Tree Postorder Traversal](leetcode-145-Binary-Tree-Postorder-Traversal.md) + * [146. LRU Cache](leetcode-146-LRU-Cache.md) + * [147. Insertion Sort List](leetcode-147-Insertion-Sort-List.md) + * [148. Sort List](leetcode-148-Sort-List.md) + * [149*. Max Points on a Line](leetcode-149-Max-Points-on-a-Line.md) + * [150. Evaluate Reverse Polish Notation](leetcode-150-Evaluate-Reverse-Polish-Notation.md) + * [151. Reverse Words in a String](leetcode-151-Reverse-Words-in-a-String.md) + * [152. Maximum Product Subarray](leetcode-152-Maximum-Product-Subarray.md) + * [153. Find Minimum in Rotated Sorted Array](leetcode-153-Find-Minimum-in-Rotated-Sorted-Array.md) + * [154*. Find Minimum in Rotated Sorted Array II](leetcode-154-Find-Minimum-in-Rotated-Sorted-ArrayII.md) + * [155. Min Stack](leetcode-155-Min-Stack.md) + * [160. Intersection of Two Linked Lists](leetcode-160-Intersection-of-Two-Linked-Lists.md) + * [162. Find Peak Element](leetcode-162-Find-Peak-Element.md) + * [164. Maximum Gap](leetcode-164-Maximum-Gap.md) + * [165. Compare Version Numbers](leetcode-165-Compare-Version-Numbers.md) + * [166. Fraction to Recurring Decimal](leetcode-166-Fraction-to-Recurring-Decimal.md) + * [167. Two Sum II - Input array is sorted](leetcode-167-Two-SumII-Input-array-is-sorted.md) + * [168. Excel Sheet Column Title](leetcode-168-Excel-Sheet-Column-Title.md) + * [169. Majority Element](leetcode-169-Majority-Element.md) + * [171. Excel Sheet Column Number](leetcode-171-Excel-Sheet-Column-Number.md) + * [172. Factorial Trailing Zeroes](leetcode-172-Factorial-Trailing-Zeroes.md) + * [173. Binary Search Tree Iterator](leetcode-173-Binary-Search-Tree-Iterator.md) + * [174*. Dungeon Game](leetcode-174-Dungeon-Game.md) + * [179. Largest Number](leetcode-179-Largest-Number.md) + * [187. Repeated DNA Sequences](leetcode-187-Repeated-DNA-Sequences.md) + * [188. Best Time to Buy and Sell Stock IV](leetcode-188-Best-Time-to-Buy-and-Sell-StockIV.md) + * [189. Rotate Array](leetcode-189-Rotate-Array.md) + * [190. Reverse Bits](leetcode-190-Reverse-Bits.md) + * [191. Number of 1 Bits](leetcode-191-Number-of-1-Bits.md) + * [198. House Robber](leetcode-198-House-Robber.md) + * [199. Binary Tree Right Side View](leetcode-199-Binary-Tree-Right-Side-View.md) + * [200. Number of Islands](leetcode-200-Number-of-Islands.md) +* [201 题到 300 题](leetcode-201-300.md) + * [201. Bitwise AND of Numbers Range](leetcode-201-Bitwise-AND-of-Numbers-Range.md) + * [202. Happy Number](leetcode-202-Happy-Number.md) + * [203. Remove Linked List Elements](leetcode-203-Remove-Linked-List-Elements.md) + * [204. Count Primes](leetcode-204-Count-Primes.md) + * [205. Isomorphic Strings](leetcode-205-Isomorphic-Strings.md) + * [206. Reverse Linked List](leetcode-206-Reverse-Linked-List.md) + * [207. Course Schedule](leetcode-207-Course-Schedule.md) + * [208. Implement Trie(Prefix Tree)](leetcode-208-Implement-Trie-Prefix-Tree.md) + * [209. Minimum Size Subarray Sum](leetcode-209-Minimum-Size-Subarray-Sum.md) + * [210. Course Schedule II](leetcode-210-Course-ScheduleII.md) + * [211. Add and Search Word - Data structure design](leetcode-211-Add-And-Search-Word-Data-structure-design.md) + * [212. Word Search II](leetcode-212-Word-SearchII.md) + * [213. House Robber II](leetcode-213-House-RobberII.md) + * [214*. Shortest Palindrome](leetcode-214-Shortest-Palindrome.md) + * [215. Kth Largest Element in an Array](leetcode-215-Kth-Largest-Element-in-an-Array.md) + * [216. Combination Sum III](leetcode-216-Combination-SumIII.md) + * [217. Contains Duplicate](leetcode-217-Contains-Duplicate.md) + * [218. The Skyline Problem](leetcode-218-The-Skyline-Problem.md) + * [219. Contains Duplicate II](leetcode-219-ContainsDuplicateII.md) + * [220*. Contains Duplicate III](leetcode-220-Contains-DuplicateIII.md) + * [221. Maximal Square](leetcode-221-Maximal-Square.md) + * [222. Count Complete Tree Nodes](leetcode-222-Count-Complete-Tree-Nodes.md) + * [223. Rectangle Area](leetcode-223-Rectangle-Area.md) + * [224*. Basic Calculator](leetcode-224-Basic-Calculator.md) + * [225. Implement Stack using Queues](leetcode-225-Implement-Stack-using-Queues.md) + * [226. Invert Binary Tree](leetcode-226-Invert-Binary-Tree.md) + * [227. Basic Calculator II](leetcode-227-Basic-CalculatorII.md) + * [228. Summary Ranges](leetcode-228-Summary-Ranges.md) + * [229. Majority Element II](leetcode-229-Majority-ElementII.md) + * [230. Kth Smallest Element in a BST](leetcode-230-Kth-Smallest-Element-in-a-BST.md) + * [231*. Power of Two](leetcode-231-Power-of-Two.md) + * [232. Implement Queue using Stacks](leetcode-232-Implement-Queue-using-Stacks.md) + * [233. Number of Digit One](leetcode-233-Number-of-Digit-One.md) + * [234. Palindrome Linked List](leetcode-234-Palindrome-Linked-List.md) + * [235. Lowest Common Ancestor of a Binary Search Tree](leetcode-235-Lowest-Common-Ancestor-of-a-Binary-Search-Tree.md) + * [236. Lowest Common Ancestor of a Binary Tree](leetcode-236-Lowest-Common-Ancestor-of-a-Binary-Tree.md) + * [237. Delete Node in a Linked List](leetcode-237-Delete-Node-in-a-Linked-List.md) + * [238. Product of Array Except Self](leetcode-238-Product-of-Array-Except-Self.md) + * [239. Sliding Window Maximum](leetcode-239-Sliding-Window-Maximum.md) + * [240. Search a 2D Matrix II](leetcode-240-Search-a-2D-MatrixII.md) + * [241. Different Ways to Add Parentheses](leetcode-241-Different-Ways-to-Add-Parentheses.md) + * [242. Valid Anagram](leetcode-242-Valid-Anagram.md) + * [257. Binary Tree Paths](leetcode-257-Binary-Tree-Paths.md) + * [258. Add Digits](leetcode-258-Add-Digits.md) + * [260. Single Number III](leetcode-260-Single-NumberIII.md) + * [263. Ugly Number](leetcode-263-Ugly-Number.md) + * [264. Ugly Number II](leetcode-264-Ugly-NumberII.md) + * [268. Missing Number](leetcode-268-Missing-Number.md) + * [273. Integer to English Words](leetcode-273-Intege-to-English-Words.md) + * [274. H-Index](leetcode-274-H-Index.md) + * [275. H-Index II](leetcode-275-H-IndexII.md) + * [278. First Bad Version](leetcode-278-First-Bad-Version.md) + * [279. Perfect Squares](leetcode-279-Perfect-Squares.md) + * [282. Expression Add Operators](leetcode-282-Expression-Add-Operators.md) + * [283. Move Zeroes](leetcode-283-Move-Zeroes.md) + * [284. Peeking Iterator](leetcode-284-Peeking-Iterator.md) + * [287*. Find the Duplicate Number](leetcode-287-Find-the-Duplicate-Number.md) + * [289*. Game of Life](leetcode-289-Game-of-Life.md) + * [290. Word Pattern](leetcode-290-Word-Pattern.md) + * [292*. Nim Game](leetcode-292-Nim-Game.md) + * [295*. Find Median from Data Stream](leetcode-295-Find-Median-from-Data-Stream.md) + * [297. Serialize and Deserialize Binary Tree](leetcode-297-Serialize-and-Deserialize-Binary-Tree.md) + * [299. Bulls and Cows](leetcode-299-Bulls-and-Cows.md) + * [300. Longest Increasing Subsequence](leetcode-300-Longest-Increasing-Subsequence.md) +* [301 题到 400 题](leetcode-301-400.md) + * [301. Remove Invalid Parentheses](leetcode-301-Remove-Invalid-Parentheses.md) + * [303. Range Sum Query - Immutable](leetcode-303-Range-Sum-Query-Immutable.md) + * [304. Range Sum Query 2D - Immutable](leetcode-304-Range-Sum-Query-2D-Immutable.md) + * [306. Additive Number](leetcode-306-Additive-Number.md) + * [307. Range Sum Query - Mutable](leetcode-307-Range-Sum-Query-Mutable.md) * [更多](more.md) \ No newline at end of file diff --git a/book.json b/book.json index 166ded11c..9a87d4411 100644 --- a/book.json +++ b/book.json @@ -1,52 +1,66 @@ -{ - "title": "leetcode", - "author": "windliang", - "description": "leetcode刷题", - "language" : "zh-hans", - "links": { - "sidebar": { - "Home": "https://windliang.wang" - }, - "gitbook": true - }, - "structure": { - "readme": "README.md", - "summary": "SUMMARY.md" - - }, - "plugins": ["katex","splitter","anchor-navigation-ex","github","copy-code-button","-lunr", "-search", "search-plus","ad","-livereload","meta","sitemap","expandable-chapters-small"], - "pluginsConfig": { - "github": { - "url": "https://github.com/wind-liang/leetcode" - }, - "anchor-navigation-ex": { - "multipleH1": false - }, - "ad": { - "contentBottom": "%3Cdiv%20id%3D%22wechat_subscriber%22%20style%3D%22display%3A%20block%3B%20padding%3A%2010px%200%3B%20margin%3A%2020px%20auto%3B%20width%3A%20100%25%3B%20text-align%3A%20center%22%3E%0A%20%20%20%20%20%20%20%20%3Cimg%20id%3D%22wechat_subscriber_qcode%22%20src%3D%22https%3A//windliang.oss-cn-beijing.aliyuncs.com/leetcode.jpg%22%20alt%3D%22windliang%20wechat%22%20style%3D%22width%3A%20800px%3B%20max-width%3A%20100%25%3B%22%3E%0A%20%20%20%20%20%20%20%20%3Cbr%3E%20%0A%20%3Cbr%3E%20%0A%20%3Cbr%3E%20%0A%20%20%20%20%20%20%20%20%20%3Cdiv%3E%u5982%u679C%u89C9%u5F97%u6709%u5E2E%u52A9%u7684%u8BDD%uFF0C%u53EF%u4EE5%u70B9%u51FB%20%3Ca%20href%3D%22https%3A//github.com/wind-liang/leetcode%22%20target%20%3D%20%22_blank%22%3E%u8FD9%u91CC%3C/a%3E%20%u5728%20github%20%u7ED9%u4E00%u4E2A%20star%20%u54E6%20%5E%5E%3C/div%3E%0A%3Cbr%3E%0A%3Cdiv%3E%u5982%u679C%u60F3%u7CFB%u7EDF%u7684%u5B66%u4E60%u6570%u636E%u7ED3%u6784%u548C%u7B97%u6CD5%uFF0C%u5F3A%u70C8%u63A8%u8350%u4E00%u4E2A%u6211%u4E4B%u524D%u5B66%u8FC7%u7684%u8BFE%u7A0B%uFF0C%u53EF%u4EE5%u70B9%u51FB%20%3Ca%20href%3D%22http%3A//gk.link/a/105Cw%22%20target%20%3D%20%22_blank%22%3E%u8FD9%u91CC%3C/a%3E%20%u67E5%u770B%u8BE6%u60C5%3C/div%3E%0A%3Cbr%3E%0A%3Cdiv%3E%u8BFE%u7A0B%u5728%u516C%u4F17%u53F7%u300C%u8BFE%u7A0B%u51CF%u51CF%u300D%u8D2D%u4E70%u4F1A%20%3Ca%20href%3D%22https%3A//windliang.wang/2020/05/31/%25E6%259E%2581%25E5%25AE%25A2%25E6%2597%25B6%25E9%2597%25B4%25E4%25BC%2598%25E6%2583%25A0%25E7%25BA%25A2%25E5%258C%2585%25E8%25BF%2594%25E7%258E%25B0/%22%20target%20%3D%20%22_blank%22%3E%u66F4%u4F18%u60E0%u4E00%u4E9B%3C/a%3E%3C/div%3E%0A%20%20%20%20%20%20%20%20%3C/div%3E%3Cscript%20async%20src%3D%22https%3A//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js%22%3E%3C/script%3E%3Cins%20class%3D%22adsbygoogle%22%20style%3D%22display%3Ablock%22%20data-ad-client%3D%22ca-pub-1480477141602191%22%20data-ad-slot%3D%228293832619%22%20data-ad-format%3D%22auto%22%20data-full-width-responsive%3D%22true%22%3E%3C/ins%3E%3Cscript%3E%20%28adsbygoogle%20%3D%20window.adsbygoogle%20%7C%7C%20%5B%5D%29.push%28%7B%7D%29%3B%3C/script%3E%0A%3Cdiv%20%20style%3D%22display%3A%20block%3B%20padding%3A%2010px%200%3B%20margin%3A%2020px%20auto%3B%20width%3A%20100%25%3B%20text-align%3A%20center%22%3E%0A%3Ca%20href%3D%22http%3A//www.beian.miit.gov.cn%22%20rel%3D%22nofollow%22%20%20target%3D%22_blank%22%20style%3D%22text-decoration%3Anone%3Bcolor%3A%23000%3B%22%3E%u664BICP%u590719005643%u53F7%3C/a%3E%0A%3C/div%3E" - }, - "meta": { - "data": [ - { - "name": "baidu-site-verification", - "content": "iFzduP9Qsi" - }, - { - "name": "google-site-verification", - "content": "qwgHd8kl7vEvBa4OR81jLieQG9UKcY2r8ocztMhzA8o" - } - ] - }, - "sitemap": { - "hostname": "https://leetcode.wang" - } - - }, - "styles":{ - "website":"websiteStyle.css" - }, - "pdf": { - "fontSize": 16 - }, - "gitbook" : "3.2.3" - } \ No newline at end of file +{ + "title": "leetcode", + "author": "windliang", + "description": "leetcode刷题", + "language": "zh-hans", + "links": { + "sidebar": { + "个人博客": "https://windliang.wang", + "前端的设计模式系列": "https://pattern.windliang.wang/", + "Vue2源码详解": "https://vue.windliang.wang/" + }, + "gitbook": true + }, + "structure": { + "readme": "README.md", + "summary": "SUMMARY.md" + }, + "plugins": [ + "katex", + "splitter", + "anchor-navigation-ex", + "github", + "copy-code-button", + "-lunr", + "-search", + "search-plus", + "ad", + "-livereload", + "meta", + "sitemap", + "expandable-chapters-small" + ], + "pluginsConfig": { + "github": { + "url": "https://github.com/wind-liang/leetcode" + }, + "anchor-navigation-ex": { + "multipleH1": false + }, + "ad": { + "contentBottom": "%3Cdiv%20id%3D%22wechat_subscriber%22%20style%3D%22display%3A%20block%3B%20padding%3A%2010px%200%3B%20margin%3A%2020px%20auto%3B%20width%3A%20100%25%3B%20text-align%3A%20center%22%3E%0A%20%20%20%20%20%20%20%20%3Cimg%20id%3D%22wechat_subscriber_qcode%22%20src%3D%22https%3A//windliang.oss-cn-beijing.aliyuncs.com/leetcode.jpg%22%20alt%3D%22windliang%20wechat%22%20style%3D%22width%3A%20800px%3B%20max-width%3A%20100%25%3B%22%3E%0A%20%20%20%20%20%20%20%20%3Cbr%3E%20%0A%20%3Cbr%3E%20%0A%20%3Cbr%3E%20%0A%20%20%20%20%20%20%20%20%20%3Cdiv%3E%u5982%u679C%u89C9%u5F97%u6709%u5E2E%u52A9%u7684%u8BDD%uFF0C%u53EF%u4EE5%u70B9%u51FB%20%3Ca%20href%3D%22https%3A//github.com/wind-liang/leetcode%22%20target%20%3D%20%22_blank%22%3E%u8FD9%u91CC%3C/a%3E%20%u5728%20github%20%u7ED9%u4E00%u4E2A%20star%20%u54E6%20%5E%5E%3C/div%3E%0A%3Cbr%3E%0A%3Cdiv%3E%u5982%u679C%u60F3%u7CFB%u7EDF%u7684%u5B66%u4E60%u6570%u636E%u7ED3%u6784%u548C%u7B97%u6CD5%uFF0C%u5F3A%u70C8%u63A8%u8350%u4E00%u4E2A%u6211%u4E4B%u524D%u5B66%u8FC7%u7684%u8BFE%u7A0B%uFF0C%u53EF%u4EE5%u70B9%u51FB%20%3Ca%20href%3D%22http%3A//gk.link/a/105Cw%22%20target%20%3D%20%22_blank%22%3E%u8FD9%u91CC%3C/a%3E%20%u67E5%u770B%u8BE6%u60C5%3C/div%3E%0A%3Cbr%3E%0A%3Cdiv%3E%u8BFE%u7A0B%u5728%u516C%u4F17%u53F7%u300C%u8BFE%u7A0B%u51CF%u51CF%u300D%u8D2D%u4E70%u4F1A%20%3Ca%20href%3D%22https%3A//windliang.wang/2020/05/31/%25E6%259E%2581%25E5%25AE%25A2%25E6%2597%25B6%25E9%2597%25B4%25E4%25BC%2598%25E6%2583%25A0%25E7%25BA%25A2%25E5%258C%2585%25E8%25BF%2594%25E7%258E%25B0/%22%20target%20%3D%20%22_blank%22%3E%u66F4%u4F18%u60E0%u4E00%u4E9B%3C/a%3E%3C/div%3E%0A%3Cbr%3E%0A%3Cdiv%3E%u4E4B%u524D%u81EA%u5DF1%u5199%u8FC7%u4E00%u4E2A%u597D%u73A9%u7684%20%3Ca%20href%3D%22https%3A//windliang.wang/2019/05/06/%25E5%25B0%258F%25E7%25A8%258B%25E5%25BA%258F%25E7%25A5%259E%25E5%25A5%2587%25E5%25AD%2597%25E4%25BD%2593%25E7%259A%2584%25E4%25BB%258E%25E9%259B%25B6%25E5%2588%25B0%25E4%25B8%2580/%22%3E%u5C0F%u7A0B%u5E8F%3C/a%3E%uFF0C%u6B22%u8FCE%u5927%u5BB6%u4F53%u9A8C%u2B07%uFE0F%3C/div%3E%0A%3Ca%20href%3D%22https%3A//windliang.wang/2019/05/06/%25E5%25B0%258F%25E7%25A8%258B%25E5%25BA%258F%25E7%25A5%259E%25E5%25A5%2587%25E5%25AD%2597%25E4%25BD%2593%25E7%259A%2584%25E4%25BB%258E%25E9%259B%25B6%25E5%2588%25B0%25E4%25B8%2580/%22%20rel%3D%22nofollow%22%20target%3D%22_blank%22%20%3E%3Cimg%20src%3D%22https%3A//windliangblog.oss-cn-beijing.aliyuncs.com/posterfont.jpeg%22%20width%3D%22750%22%20style%3D%22margin-top%3A20px%3Bcursor%3A%20pointer%3B%22%3E%3C/a%3E%0A%20%20%20%20%20%20%20%20%3C/div%3E%3Cscript%20async%20src%3D%22https%3A//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js%22%3E%3C/script%3E%3Cins%20class%3D%22adsbygoogle%22%20style%3D%22display%3Ablock%22%20data-ad-client%3D%22ca-pub-1480477141602191%22%20data-ad-slot%3D%228293832619%22%20data-ad-format%3D%22auto%22%20data-full-width-responsive%3D%22true%22%3E%3C/ins%3E%3Cscript%3E%20%28adsbygoogle%20%3D%20window.adsbygoogle%20%7C%7C%20%5B%5D%29.push%28%7B%7D%29%3B%3C/script%3E%0A%3Cdiv%20%20style%3D%22display%3A%20block%3B%20padding%3A%2010px%200%3B%20margin%3A%2020px%20auto%3B%20width%3A%20100%25%3B%20text-align%3A%20center%22%3E%0A%3Ca%20href%3D%22https%3A//beian.miit.gov.cn%22%20rel%3D%22nofollow%22%20%20target%3D%22_blank%22%20style%3D%22text-decoration%3Anone%3Bcolor%3A%23000%3B%22%3E%u6CAAICP%u59072021019937%u53F7-3%3C/a%3E%0A%3C/div%3E" + }, + "meta": { + "data": [ + { + "name": "baidu-site-verification", + "content": "iFzduP9Qsi" + }, + { + "name": "google-site-verification", + "content": "qwgHd8kl7vEvBa4OR81jLieQG9UKcY2r8ocztMhzA8o" + } + ] + }, + "sitemap": { + "hostname": "https://leetcode.wang" + } + }, + "styles": { + "website": "websiteStyle.css" + }, + "pdf": { + "fontSize": 16 + }, + "gitbook": "3.2.3" +} diff --git a/leetCode-1-Two-Sum.md b/leetCode-1-Two-Sum.md index 35e3a5a91..6513227ab 100644 --- a/leetCode-1-Two-Sum.md +++ b/leetCode-1-Two-Sum.md @@ -1,101 +1,101 @@ -## 题目描述 (简单难度) - -![](http://windliang.oss-cn-beijing.aliyuncs.com/1_two_sum.jpg) - -给定一个数组和一个目标和,从数组中找两个数字相加等于目标和,输出这两个数字的下标。 - -## 解法一 - -简单粗暴些,两重循环,遍历所有情况看相加是否等于目标和,如果符合直接输出。 - -``` JAVA -public int[] twoSum(int[] nums, int target) { - int []ans=new int[2]; - for(int i=0;i map=new HashMap<>(); - for(int i=0;i map=new HashMap<>(); - for(int i=0;i map=new HashMap<>(); + for(int i=0;i map=new HashMap<>(); + for(int i=0;i= 2 && pattern.charAt(1) == '*'){ - //两种情况 - //pattern 直接跳过两个字符。表示 * 前边的字符出现 0 次 - //pattern 不变,例如 text = aa ,pattern = a*,第一个 a 匹配,然后 text 的第二个 a 接着和 pattern 的第一个 a 进行匹配。表示 * 用前一个字符替代。 - return (isMatch(text, pattern.substring(2)) || - (first_match && isMatch(text.substring(1), pattern))); - } else { - return first_match && isMatch(text.substring(1), pattern.substring(1)); - } - } -``` - -时间复杂度:有点儿小复杂,待更。 - -空间复杂度:有点儿小复杂,待更。 - -# 解法二 动态规划 - -上边的递归,为了方便理解,简化下思路。 - -为了判断 text [ 0,len ] 的情况,需要知道 text [ 1,len ] - -为了判断 text [ 1,len ] 的情况,需要知道 text [ 2,len ] - -为了判断 text [ 2,len ] 的情况,需要知道 text [ 3,len ] - -... - -为了判断 text [ len - 1,len ] 的情况,需要知道 text [ len,len ] - - text [ len,len ] 肯定好求 - -求出 text [ len,len ] 的情况,就知道了 text [ len - 1,len ] - -求出 text [ len - 1,len ] 的情况,就知道了 text [ len - 2,len ] - -... - -求出 text [ 2,len ] 的情况,就知道了 text [1,len ] - -求出 text [ l1,len ] 的情况,就知道了 text [ 0,len ] - -从而知道了 text [ 0,len ] 的情况,求得问题的解。 - - - -上边就是先压栈,然后出栈,其实我们可以直接倒过来求,可以省略压栈的过程。 - -我们先求 text [ len,len ] 的情况 - -利用 text [ len,len ] 的情况 ,再求 text [ len - 1,len ] 的情况 - -... - -利用 text [ 2,len ] 的情况 ,再求 text [ 1,len ] 的情况 - -利用 text [1,len ] 的情况 ,再求 text [ 0,len ] 的情况 - -从而求出问题的解 - -我们用 $$dp[i][j]$$表示 text 从 i 开始到最后,pattern 从 j 开始到最后,此时 text 和 pattern 是否匹配。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/10_2.jpg) - -$$dp[2][2]$$就是图中橙色的部分. - -```java -public boolean isMatch(String text, String pattern) { - // 多一维的空间,因为求 dp[len - 1][j] 的时候需要知道 dp[len][j] 的情况, - // 多一维的话,就可以把 对 dp[len - 1][j] 也写进循环了 - boolean[][] dp = new boolean[text.length() + 1][pattern.length() + 1]; - // dp[len][len] 代表两个空串是否匹配了,"" 和 "" ,当然是 true 了。 - dp[text.length()][pattern.length()] = true; - - // 从 len 开始减少 - for (int i = text.length(); i >= 0; i--) { - for (int j = pattern.length(); j >= 0; j--) { - // dp[text.length()][pattern.length()] 已经进行了初始化 - if(i==text.length()&&j==pattern.length()) continue; - - boolean first_match = (i < text.length() && j < pattern.length() - && (pattern.charAt(j) == text.charAt(i) || pattern.charAt(j) == '.')); - if (j + 1 < pattern.length() && pattern.charAt(j + 1) == '*') { - dp[i][j] = dp[i][j + 2] || first_match && dp[i + 1][j]; - } else { - dp[i][j] = first_match && dp[i + 1][j + 1]; - } - } - } - return dp[0][0]; -} -``` - -时间复杂度:假设 text 的长度是 T,pattern 的长度是 P ,空间复杂度就是 O(TP)。 - -空间复杂度:申请了 dp 空间,所以是 O(TP),因为每次循环我们只需要知道 i 和 i + 1 时候的情况,所以我们可以向 [第 5 题](https://leetcode.windliang.cc/leetCode-5-Longest-Palindromic-Substring.html) 一样进行优化。 - -```java - public boolean isMatch(String text, String pattern) { - // 多一维的空间,因为求 dp[len - 1][j] 的时候需要知道 dp[len][j] 的情况, - // 多一维的话,就可以把 对 dp[len - 1][j] 也写进循环了 - boolean[][] dp = new boolean[2][pattern.length() + 1]; - dp[text.length()%2][pattern.length()] = true; - - // 从 len 开始减少 - for (int i = text.length(); i >= 0; i--) { - for (int j = pattern.length(); j >= 0; j--) { - if(i==text.length()&&j==pattern.length()) continue; - boolean first_match = (i < text.length() && j < pattern.length() - && (pattern.charAt(j) == text.charAt(i) || pattern.charAt(j) == '.')); - if (j + 1 < pattern.length() && pattern.charAt(j + 1) == '*') { - dp[i%2][j] = dp[i%2][j + 2] || first_match && dp[(i + 1)%2][j]; - } else { - dp[i%2][j] = first_match && dp[(i + 1)%2][j + 1]; - } - } - } - return dp[0][0]; - } -``` - -时间复杂度:不变, O(TP)。 - -空间复杂度:主要用了两个数组进行轮换,O(P)。 - -# 总 - +# 题目描述(困难难度) + +![](http://windliang.oss-cn-beijing.aliyuncs.com/10_1.png) + +一个简单规则的匹配,「点.」代表任意字符,「星号\*」 代表前一个字符重复 0 次或任意次。 + +# 解法一 递归 + +假如没有通配符 \* ,这道题的难度就会少了很多,我们只需要一个字符,一个字符匹配就行。如果对递归不是很了解,强烈建议看下[这篇文章](https://zhuanlan.zhihu.com/p/42664697),可以理清一下递归的思路。 + +* 我们假设存在这么个函数 isMatch,它将告诉我们 text 和 pattern 是否匹配 + + boolean isMatch ( String text, String pattern ) ; + +* 递归规模减小 + + text 和 pattern 匹配,等价于 text 和 patten 的第一个字符匹配并且剩下的字符也匹配,而判断剩下的字符是否匹配,我们就可以调用 isMatch 函数。也就是 + + ```java + (pattern.charAt(0) == text.charAt(0) || pattern.charAt(0) == '.')&&isMatch(text.substring(1), pattern.substring(1)); + ``` +* 递归出口 + + 随着规模的减小, 当 pattern 为空时,如果 text 也为空,就返回 True,不然的话就返回 False 。 + + ```java + if (pattern.isEmpty()) return text.isEmpty(); + ``` + +综上,我们的代码是 + +```java +public boolean isMatch(String text, String pattern) { + if (pattern.isEmpty()) return text.isEmpty(); + + //判断 text 是否为空,防止越界,如果 text 为空, 表达式直接判为 false, text.charAt(0)就不会执行了 + boolean first_match = (!text.isEmpty() && + (pattern.charAt(0) == text.charAt(0) || pattern.charAt(0) == '.')); + return first_match && isMatch(text.substring(1), pattern.substring(1)); + } +``` + + + +当我们考虑了 \* 呢,对于递归规模的减小,会增加对于 \* 的判断,直接看代码吧。 + +```java +public boolean isMatch(String text, String pattern) { + if (pattern.isEmpty()) return text.isEmpty(); + + boolean first_match = (!text.isEmpty() && + (pattern.charAt(0) == text.charAt(0) || pattern.charAt(0) == '.')); + //只有长度大于 2 的时候,才考虑 * + if (pattern.length() >= 2 && pattern.charAt(1) == '*'){ + //两种情况 + //pattern 直接跳过两个字符。表示 * 前边的字符出现 0 次 + //pattern 不变,例如 text = aa ,pattern = a*,第一个 a 匹配,然后 text 的第二个 a 接着和 pattern 的第一个 a 进行匹配。表示 * 用前一个字符替代。 + return (isMatch(text, pattern.substring(2)) || + (first_match && isMatch(text.substring(1), pattern))); + } else { + return first_match && isMatch(text.substring(1), pattern.substring(1)); + } + } +``` + +时间复杂度:有点儿小复杂,待更。 + +空间复杂度:有点儿小复杂,待更。 + +# 解法二 动态规划 + +上边的递归,为了方便理解,简化下思路。 + +为了判断 text [ 0,len ] 的情况,需要知道 text [ 1,len ] + +为了判断 text [ 1,len ] 的情况,需要知道 text [ 2,len ] + +为了判断 text [ 2,len ] 的情况,需要知道 text [ 3,len ] + +... + +为了判断 text [ len - 1,len ] 的情况,需要知道 text [ len,len ] + + text [ len,len ] 肯定好求 + +求出 text [ len,len ] 的情况,就知道了 text [ len - 1,len ] + +求出 text [ len - 1,len ] 的情况,就知道了 text [ len - 2,len ] + +... + +求出 text [ 2,len ] 的情况,就知道了 text [1,len ] + +求出 text [ l1,len ] 的情况,就知道了 text [ 0,len ] + +从而知道了 text [ 0,len ] 的情况,求得问题的解。 + + + +上边就是先压栈,然后出栈,其实我们可以直接倒过来求,可以省略压栈的过程。 + +我们先求 text [ len,len ] 的情况 + +利用 text [ len,len ] 的情况 ,再求 text [ len - 1,len ] 的情况 + +... + +利用 text [ 2,len ] 的情况 ,再求 text [ 1,len ] 的情况 + +利用 text [1,len ] 的情况 ,再求 text [ 0,len ] 的情况 + +从而求出问题的解 + +我们用 $$dp[i][j]$$表示 text 从 i 开始到最后,pattern 从 j 开始到最后,此时 text 和 pattern 是否匹配。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/10_2.jpg) + +$$dp[2][2]$$就是图中橙色的部分. + +```java +public boolean isMatch(String text, String pattern) { + // 多一维的空间,因为求 dp[len - 1][j] 的时候需要知道 dp[len][j] 的情况, + // 多一维的话,就可以把 对 dp[len - 1][j] 也写进循环了 + boolean[][] dp = new boolean[text.length() + 1][pattern.length() + 1]; + // dp[len][len] 代表两个空串是否匹配了,"" 和 "" ,当然是 true 了。 + dp[text.length()][pattern.length()] = true; + + // 从 len 开始减少 + for (int i = text.length(); i >= 0; i--) { + for (int j = pattern.length(); j >= 0; j--) { + // dp[text.length()][pattern.length()] 已经进行了初始化 + if(i==text.length()&&j==pattern.length()) continue; + + boolean first_match = (i < text.length() && j < pattern.length() + && (pattern.charAt(j) == text.charAt(i) || pattern.charAt(j) == '.')); + if (j + 1 < pattern.length() && pattern.charAt(j + 1) == '*') { + dp[i][j] = dp[i][j + 2] || first_match && dp[i + 1][j]; + } else { + dp[i][j] = first_match && dp[i + 1][j + 1]; + } + } + } + return dp[0][0]; +} +``` + +时间复杂度:假设 text 的长度是 T,pattern 的长度是 P ,空间复杂度就是 O(TP)。 + +空间复杂度:申请了 dp 空间,所以是 O(TP),因为每次循环我们只需要知道 i 和 i + 1 时候的情况,所以我们可以向 [第 5 题](https://leetcode.windliang.cc/leetCode-5-Longest-Palindromic-Substring.html) 一样进行优化。 + +```java + public boolean isMatch(String text, String pattern) { + // 多一维的空间,因为求 dp[len - 1][j] 的时候需要知道 dp[len][j] 的情况, + // 多一维的话,就可以把 对 dp[len - 1][j] 也写进循环了 + boolean[][] dp = new boolean[2][pattern.length() + 1]; + dp[text.length()%2][pattern.length()] = true; + + // 从 len 开始减少 + for (int i = text.length(); i >= 0; i--) { + for (int j = pattern.length(); j >= 0; j--) { + if(i==text.length()&&j==pattern.length()) continue; + boolean first_match = (i < text.length() && j < pattern.length() + && (pattern.charAt(j) == text.charAt(i) || pattern.charAt(j) == '.')); + if (j + 1 < pattern.length() && pattern.charAt(j + 1) == '*') { + dp[i%2][j] = dp[i%2][j + 2] || first_match && dp[(i + 1)%2][j]; + } else { + dp[i%2][j] = first_match && dp[(i + 1)%2][j + 1]; + } + } + } + return dp[0][0]; + } +``` + +时间复杂度:不变, O(TP)。 + +空间复杂度:主要用了两个数组进行轮换,O(P)。 + +# 总 + 这道题对于递归的解法,感觉难在怎么去求时间复杂度,现在还没有什么思路,以后再来补充吧。整体来说,只要理清思路,两种算法还是比较好理解的。 \ No newline at end of file diff --git a/leetCode-11-Container-With-Most-Water.md b/leetCode-11-Container-With-Most-Water.md index b2f51ffa6..99a581040 100644 --- a/leetCode-11-Container-With-Most-Water.md +++ b/leetCode-11-Container-With-Most-Water.md @@ -1,70 +1,70 @@ -# 题目描述(中等难度) - -![](http://windliang.oss-cn-beijing.aliyuncs.com/11_1.jpg) - -每个数组代表一个高度,选两个任意的柱子往里边倒水,能最多倒多少水。 - -# 解法一 暴力解法 - -直接遍历任意两根柱子,求出能存水的大小,用一个变量保存最大的。 - -```java -public int maxArea(int[] height) { - int max = 0; - for (int i = 0; i < height.length; i++) { - for (int j = i + 1; j < height.length; j++) { - int h = Math.min(height[i], height[j]); - if (h * (j - i) > max) { - max = h * (j - i); - } - } - } - return max; -} -``` - -时间复杂度:O(n²)。 - -空间复杂度:O(1)。 - -# 解法二 - - - -![](http://windliang.oss-cn-beijing.aliyuncs.com/11_2.jpg) - -我们理一下思路,大小是由长度和高度决定,如果选 0 到 8 就保证了长度最长,此时大小是 0 号柱子的高度 1 乘以长度 8 。我们如果想面积更大怎么做呢,只能减小长度,增加高度。是左边的柱子向右移动变成 1 号柱子呢?还是右边的柱子向左移动变成 7 号柱子呢?当然是哪边的柱子短就改哪边的!只有这样,高度才有可能增加。 - -例如我们如果把 8 号柱子变成 7 号柱子,此时长度减少了,然而高度还是 0 号柱子没有变化,所以面积就会减少。把 1 号柱子变成 2 号柱子就很好了,因为此时高度就变成了 8 号柱子的高度,面积就有可能会增加。 - -如果左右两边柱子相等该怎么办呢?随意! - -我们假设 1 号 和 8 号 柱子高度是相等的。如果他们之间的柱子只有 1 根比它俩高或者没有比它俩高的,那么最大面积就一定选取是 1 号和 8 号了,所以 1 号接着变大,或者 8 号接着减小都是无所谓的,因为答案已经确定了。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/11_3.jpg) - -假设 1 号 和 8 号之间有 2 根或以上的柱子比它俩高,假设是 4 号和 6 号比它俩高。1 号会变到 2 号、3 号,最终为 4 号,8 号会变到 7 号, 6 号,而在这个过程中产生的面积一定不会比 1 号和 8 号产生的面积大,因为过程中的柱子都比 1 号和 8 号低。所以是先变 1 号还是先变 8 号是无所谓的,无非是谁先到达更长的柱子而已。 - -看一下下边的算法,会更加清楚一些。 - -```java -public int maxArea2(int[] height) { - int maxarea = 0, l = 0, r = height.length - 1; - while (l < r) { - maxarea = Math.max(maxarea, Math.min(height[l], height[r]) * (r - l)); - if (height[l] < height[r]) - l++; - else - r--; - } - return maxarea; -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(1)。 - -# 总 - +# 题目描述(中等难度) + +![](http://windliang.oss-cn-beijing.aliyuncs.com/11_1.jpg) + +每个数组代表一个高度,选两个任意的柱子往里边倒水,能最多倒多少水。 + +# 解法一 暴力解法 + +直接遍历任意两根柱子,求出能存水的大小,用一个变量保存最大的。 + +```java +public int maxArea(int[] height) { + int max = 0; + for (int i = 0; i < height.length; i++) { + for (int j = i + 1; j < height.length; j++) { + int h = Math.min(height[i], height[j]); + if (h * (j - i) > max) { + max = h * (j - i); + } + } + } + return max; +} +``` + +时间复杂度:O(n²)。 + +空间复杂度:O(1)。 + +# 解法二 + + + +![](http://windliang.oss-cn-beijing.aliyuncs.com/11_2.jpg) + +我们理一下思路,大小是由长度和高度决定,如果选 0 到 8 就保证了长度最长,此时大小是 0 号柱子的高度 1 乘以长度 8 。我们如果想面积更大怎么做呢,只能减小长度,增加高度。是左边的柱子向右移动变成 1 号柱子呢?还是右边的柱子向左移动变成 7 号柱子呢?当然是哪边的柱子短就改哪边的!只有这样,高度才有可能增加。 + +例如我们如果把 8 号柱子变成 7 号柱子,此时长度减少了,然而高度还是 0 号柱子没有变化,所以面积就会减少。把 1 号柱子变成 2 号柱子就很好了,因为此时高度就变成了 8 号柱子的高度,面积就有可能会增加。 + +如果左右两边柱子相等该怎么办呢?随意! + +我们假设 1 号 和 8 号 柱子高度是相等的。如果他们之间的柱子只有 1 根比它俩高或者没有比它俩高的,那么最大面积就一定选取是 1 号和 8 号了,所以 1 号接着变大,或者 8 号接着减小都是无所谓的,因为答案已经确定了。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/11_3.jpg) + +假设 1 号 和 8 号之间有 2 根或以上的柱子比它俩高,假设是 4 号和 6 号比它俩高。1 号会变到 2 号、3 号,最终为 4 号,8 号会变到 7 号, 6 号,而在这个过程中产生的面积一定不会比 1 号和 8 号产生的面积大,因为过程中的柱子都比 1 号和 8 号低。所以是先变 1 号还是先变 8 号是无所谓的,无非是谁先到达更长的柱子而已。 + +看一下下边的算法,会更加清楚一些。 + +```java +public int maxArea2(int[] height) { + int maxarea = 0, l = 0, r = height.length - 1; + while (l < r) { + maxarea = Math.max(maxarea, Math.min(height[l], height[r]) * (r - l)); + if (height[l] < height[r]) + l++; + else + r--; + } + return maxarea; +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(1)。 + +# 总 + 为了减少暴力解法的时间复杂度,只能去深层次的理解题意,从而找出突破点。 \ No newline at end of file diff --git a/leetCode-12-Integer-to-Roman.md b/leetCode-12-Integer-to-Roman.md index 7abc825b2..f8f537ebb 100644 --- a/leetCode-12-Integer-to-Roman.md +++ b/leetCode-12-Integer-to-Roman.md @@ -1,110 +1,110 @@ -# 题目描述(中等难度) - -![](http://windliang.oss-cn-beijing.aliyuncs.com/12_1.jpg) - -把数字转换成罗马数字,正常情况就是把每个字母相加,并且大字母在前,小字母在后,上边也介绍了像 4 和 9 那些特殊情况。 - -# 解法一 - -这个是自己的解法,主要思想就是每次取出一位,然后得到相应的罗马数字,然后合起来就行。 - -```java -public String getRoman(int num,int count){ //count 表示当前的位数,个位,十位... - char[]ten={'I','X','C','M'}; //1,10,100,1000 - char[]five={'V','L','D'};//5,50,500 - String r=""; - if(num<=3){ - while(num!=0){ - r+=ten[count]; - num--; - } - } - if(num==4){ - r=(ten[count]+"")+(five[count]+""); - } - if(num==5){ - r=five[count]+""; - } - if(num>5&&num<9){ - r=five[count]+""; - num-=5; - while(num!=0){ - r+=ten[count]; - num--; - } - } - if(num==9){ - r = (ten[count] + "") + (ten[count + 1] + ""); - } - return r; -} -public String intToRoman(int num) { - String r=""; - int count=0; - while(num!=0){ - int pop=num%10; - r=getRoman(pop,count)+r; - count++; - num/=10; - } - return r; -} -``` - -时间复杂度:num 的位数 $$log_{10}(num)+1$$所以时间复杂度是 O(log(n))。 - -空间复杂度:常数个变量,O(1)。 - -下边在分享一些 LeetCode 讨论里的一些解法。 - -# 解法二 - -https://leetcode.com/problems/integer-to-roman/discuss/6310/My-java-solution-easy-to-understand - -```java -public String intToRoman(int num) { - int[] values = {1000,900,500,400,100,90,50,40,10,9,5,4,1}; - String[] strs = {"M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"}; - - StringBuilder sb = new StringBuilder(); - - for(int i=0;i= values[i]) { - num -= values[i]; - sb.append(strs[i]); - } - } - return sb.toString(); -} -``` - -相当简洁了,主要就是把所有的组合列出来,因为罗马数字表示的大小就是把所有字母相加,所以每次 append 那个,再把对应的值减去就行了。 - -时间复杂度:不是很清楚,也许是 O(1)?因为似乎和问题规模没什么关系了。 - -空间复杂度:O(1). - -# 解法三 - -https://leetcode.com/problems/integer-to-roman/discuss/6376/Simple-JAVA-solution - -```java -public String intToRoman(int num) { - String M[] = {"", "M", "MM", "MMM"};//0,1000,2000,3000 - String C[] = {"", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"};//0,100,200,300... - String X[] = {"", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"};//0,10,20,30... - String I[] = {"", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"};//0,1,2,3... - return M[num/1000] + C[(num%1000)/100]+ X[(num%100)/10] + I[num%10]; -} -``` - -这就更加暴力了,把每位的情况都列出来然后直接返回,但思路清晰明了呀。 - -时间复杂度:O(1)或者说是 num 的位数,不是很确定。 - -空间复杂度:O(1)。 - -# 总 - -这道题感觉难度应该是 easy ,没有那么难,就是理清楚题意,然后就可以往出列举就行了。 - +# 题目描述(中等难度) + +![](http://windliang.oss-cn-beijing.aliyuncs.com/12_1.jpg) + +把数字转换成罗马数字,正常情况就是把每个字母相加,并且大字母在前,小字母在后,上边也介绍了像 4 和 9 那些特殊情况。 + +# 解法一 + +这个是自己的解法,主要思想就是每次取出一位,然后得到相应的罗马数字,然后合起来就行。 + +```java +public String getRoman(int num,int count){ //count 表示当前的位数,个位,十位... + char[]ten={'I','X','C','M'}; //1,10,100,1000 + char[]five={'V','L','D'};//5,50,500 + String r=""; + if(num<=3){ + while(num!=0){ + r+=ten[count]; + num--; + } + } + if(num==4){ + r=(ten[count]+"")+(five[count]+""); + } + if(num==5){ + r=five[count]+""; + } + if(num>5&&num<9){ + r=five[count]+""; + num-=5; + while(num!=0){ + r+=ten[count]; + num--; + } + } + if(num==9){ + r = (ten[count] + "") + (ten[count + 1] + ""); + } + return r; +} +public String intToRoman(int num) { + String r=""; + int count=0; + while(num!=0){ + int pop=num%10; + r=getRoman(pop,count)+r; + count++; + num/=10; + } + return r; +} +``` + +时间复杂度:num 的位数 $$log_{10}(num)+1$$所以时间复杂度是 O(log(n))。 + +空间复杂度:常数个变量,O(1)。 + +下边在分享一些 LeetCode 讨论里的一些解法。 + +# 解法二 + +https://leetcode.com/problems/integer-to-roman/discuss/6310/My-java-solution-easy-to-understand + +```java +public String intToRoman(int num) { + int[] values = {1000,900,500,400,100,90,50,40,10,9,5,4,1}; + String[] strs = {"M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"}; + + StringBuilder sb = new StringBuilder(); + + for(int i=0;i= values[i]) { + num -= values[i]; + sb.append(strs[i]); + } + } + return sb.toString(); +} +``` + +相当简洁了,主要就是把所有的组合列出来,因为罗马数字表示的大小就是把所有字母相加,所以每次 append 那个,再把对应的值减去就行了。 + +时间复杂度:不是很清楚,也许是 O(1)?因为似乎和问题规模没什么关系了。 + +空间复杂度:O(1). + +# 解法三 + +https://leetcode.com/problems/integer-to-roman/discuss/6376/Simple-JAVA-solution + +```java +public String intToRoman(int num) { + String M[] = {"", "M", "MM", "MMM"};//0,1000,2000,3000 + String C[] = {"", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"};//0,100,200,300... + String X[] = {"", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"};//0,10,20,30... + String I[] = {"", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"};//0,1,2,3... + return M[num/1000] + C[(num%1000)/100]+ X[(num%100)/10] + I[num%10]; +} +``` + +这就更加暴力了,把每位的情况都列出来然后直接返回,但思路清晰明了呀。 + +时间复杂度:O(1)或者说是 num 的位数,不是很确定。 + +空间复杂度:O(1)。 + +# 总 + +这道题感觉难度应该是 easy ,没有那么难,就是理清楚题意,然后就可以往出列举就行了。 + diff --git a/leetCode-126-Word-LadderII.md b/leetCode-126-Word-LadderII.md index 905dabcc5..3eb88533d 100644 --- a/leetCode-126-Word-LadderII.md +++ b/leetCode-126-Word-LadderII.md @@ -1,810 +1,810 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/126.png) - -给定两个单词,一个作为开始,一个作为结束,还有一个单词列表。然后依次选择单词,只有当前单词到下一个单词只有一个字母不同才能被选择,然后新的单词再作为当前单词,直到选到结束的单词。输出这个的最短路径,如果有多组,则都输出。 - -# 思路分析 - -结合了开始自己的想法,又看了 [Discuss](),这道题有些难讲清楚,一个原因就是解法的代码会很长,这里理一下整个的思路。 - -如果我们从开始的单词,把与之能够转换的单词连起来,它就会长成下边的样子。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/126_3.jpg) - -橙色表示结束单词,上图橙色的路线就是我们要找的最短路径。所以我们要做的其实就是遍历上边的树,然后判断当前节点是不是结束单词,找到结束单词后,还要判断当前是不是最短的路径。说到遍历当然就是两种思路了,`DFS` 或者 `BFS`。 - -# 解法一 DFS - -利用回溯的思想,做一个 DFS。 - -首先要解决的问题是怎么找到节点的所有孩子节点。这里有两种方案。 - -第一种,遍历 `wordList` 来判断每个单词和当前单词是否只有一个字母不同。 - -```java -for (int i = 0; i < wordList.size(); i++) { - String curWord = wordList.get(i); - //符合只有一个单词不同,就进入递归 - if (oneChanged(beginWord, curWord)) { - //此时代表可以从 beginWord -> curWord - } -} - -private boolean oneChanged(String beginWord, String curWord) { - int count = 0; - for (int i = 0; i < beginWord.length(); i++) { - if (beginWord.charAt(i) != curWord.charAt(i)) { - count++; - } - if (count == 2) { - return false; - } - } - return count == 1; -} -``` - -这种的时间复杂度的话,如果 `wordList` 长度为 `m`,每个单词的长度为 `n`。那么就是 `O(mn)`。 -第二种,将要找的节点单词的每个位置换一个字符,然后看更改后的单词在不在 `wordList` 中。 - -```java -//dict 就是 wordList,为了提高速度,从 List 转为 HashSet -//cur 是我们要考虑的单词 -private List getNext(String cur, Set dict) { - List res = new ArrayList<>(); - char[] chars = cur.toCharArray(); - //考虑每一位 - for (int i = 0; i < chars.length; i++) { - char old = chars[i]; - //考虑变成其他所有的字母 - for (char c = 'a'; c <= 'z'; c++) { - if (c == old) { - continue; - } - chars[i] = c; - String next = new String(chars); - //判断 wordList 是否包含修改后的单词 - if (dict.contains(next)) { - res.add(next); - } - } - chars[i] = old; - } - return res; -} -``` - -这种的话,由于用到了 `HashSet` ,所以 `contains` 函数就是 `O(1)`。所以整个计算量就是 `26n`,所以是 `O(n)`。 - -还要解决的一个问题是,因为我们要找的是最短的路径。但是事先我们并不知道最短的路径是多少,我们需要一个全局变量来保存当前找到的路径的长度。如果找到的新的路径的长度比之前的路径短,就把之前的结果清空,重新找,如果是最小的长度,就加入到结果中。 - -看下一递归出口。 - -```java -//到了结尾单词 -if (beginWord.equals(endWord)) { - //当前长度更小,清空之前的,加新的路径加入到结果中 - if (min > temp.size()) { - ans.clear(); - min = temp.size(); - ans.add(new ArrayList(temp)); - //相等的话就直接加路径加入到结果中 - } else if (min == temp.size()) { - ans.add(new ArrayList(temp)); - } - return; -} -//当前的长度到达了 min,还是没有到达结束单词就提前结束 -if (temp.size() >= min) { - return; -} -``` - -得到下一个节点刚才讲了两种思路,我们先采用第一种解法,看一下效果。 - -```java -public List> findLadders(String beginWord, String endWord, List wordList) { - List> ans = new ArrayList<>(); - ArrayList temp = new ArrayList(); - //temp 用来保存当前的路径 - temp.add(beginWord); - findLaddersHelper(beginWord, endWord, wordList, temp, ans); - return ans; -} - -int min = Integer.MAX_VALUE; - -private void findLaddersHelper(String beginWord, String endWord, List wordList, - ArrayList temp, List> ans) { - if (beginWord.equals(endWord)) { - if (min > temp.size()) { - ans.clear(); - min = temp.size(); - ans.add(new ArrayList(temp)); - } else if (min == temp.size()) { - ans.add(new ArrayList(temp)); - } - return; - } - //当前的长度到达了 min,还是没有到达结束单词就提前结束 - if (temp.size() >= min) { - return; - } - //遍历当前所有的单词 - for (int i = 0; i < wordList.size(); i++) { - String curWord = wordList.get(i); - //路径中已经含有当前单词,如果再把当前单词加到路径,那肯定会使得路径更长,所以跳过 - if (temp.contains(curWord)) { - continue; - } - //符合只有一个单词不同,就进入递归 - if (oneChanged(beginWord, curWord)) { - temp.add(curWord); - findLaddersHelper(curWord, endWord, wordList, temp, ans); - temp.remove(temp.size() - 1); - } - } -} -private boolean oneChanged(String beginWord, String curWord) { - int count = 0; - for (int i = 0; i < beginWord.length(); i++) { - if (beginWord.charAt(i) != curWord.charAt(i)) { - count++; - } - if (count == 2) { - return false; - } - } - return count == 1; -} -``` - -但是对于普通的输入可以解决,如果 `wordList` 过长的话就会造成超时了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/126_2.jpg) - -得到下一个的节点,如果采用第二种解法呢? - -```java -int min = Integer.MAX_VALUE; -public List> findLadders(String beginWord, String endWord, List wordList) { - List> ans = new ArrayList<>(); - ArrayList temp = new ArrayList(); - temp.add(beginWord); - //temp 用来保存当前的路径 - findLaddersHelper(beginWord, endWord, wordList, temp, ans); - return ans; -} - - -private void findLaddersHelper(String beginWord, String endWord, List wordList, - ArrayList temp, List> ans) { - if (beginWord.equals(endWord)) { - if (min > temp.size()) { - ans.clear(); - min = temp.size(); - ans.add(new ArrayList(temp)); - } else if (min == temp.size()) { - ans.add(new ArrayList(temp)); - } - return; - } - - if (temp.size() >= min) { - return; - } - Set dict = new HashSet<>(wordList); - //一次性到达所有的下一个的节点 - ArrayList neighbors = getNeighbors(beginWord, dict); - for (String neighbor : neighbors) { - if (temp.contains(neighbor)) { - continue; - } - temp.add(neighbor); - findLaddersHelper(neighbor, endWord, wordList, temp, ans); - temp.remove(temp.size() - 1); - } -} - - -private ArrayList getNeighbors(String node, Set dict) { - ArrayList res = new ArrayList(); - char chs[] = node.toCharArray(); - - for (char ch = 'a'; ch <= 'z'; ch++) { - for (int i = 0; i < chs.length; i++) { - if (chs[i] == ch) - continue; - char old_ch = chs[i]; - chs[i] = ch; - if (dict.contains(String.valueOf(chs))) { - res.add(String.valueOf(chs)); - } - chs[i] = old_ch; - } - - } - return res; -} -``` - -快了一些,但是还是超时。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/126_4.jpg) - -我们继续来优化,首先想一下为什么会超时,看一下之前的图。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/126_3.jpg) - -`DFS` 的过程的话,结合上图,就是先考虑了最左边的路径,然后再回溯一下,继续到达底部。然后回溯回溯,终于到了一条含有结束单词的路径,然而事实上这条并不是最短路径。综上,我们会多判断很多无用的路径。 - -如果我们事先知道了最短路径长度是 `4`,那么我们只需要考虑前 `4` 层就足够了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/126_5.jpg) - -怎么知道结束单词在哪一层呢?只能一层层的找了,也就是 `BFS`。此外,因为上图需要搜索的树提前是没有的,我们需要边找边更新这个树。而在 `DFS` 中,我们也需要这个树,其实就是需要每个节点的所有相邻节点。 - -所以我们在 `BFS` 中,就把每个节点的所有相邻节点保存到 `HashMap` 中,就省去了 `DFS` 再去找相邻节点的时间。 - -此外,`BFS` 的过程中,把最短路径的高度用 `min` 也记录下来,在 `DFS` 的时候到达高度后就可以提前结束。 - -```java -int min = 0; -public List> findLadders(String beginWord, String endWord, List wordList) { - List> ans = new ArrayList<>(); - //如果不含有结束单词,直接结束,不然后边会造成死循环 - if (!wordList.contains(endWord)) { - return ans; - } - //利用 BFS 得到所有的邻居节点 - HashMap> map = bfs(beginWord, endWord, wordList); - ArrayList temp = new ArrayList(); - // temp 用来保存当前的路径 - temp.add(beginWord); - findLaddersHelper(beginWord, endWord, map, temp, ans); - return ans; -} - -private void findLaddersHelper(String beginWord, String endWord, HashMap> map, - ArrayList temp, List> ans) { - if (beginWord.equals(endWord)) { - ans.add(new ArrayList(temp)); - - return; - } - if(temp.size() - 1== min){ - return; - } - // 得到所有的下一个的节点 - ArrayList neighbors = map.getOrDefault(beginWord, new ArrayList()); - for (String neighbor : neighbors) { - if (temp.contains(neighbor)) { - continue; - } - temp.add(neighbor); - findLaddersHelper(neighbor, endWord, map, temp, ans); - temp.remove(temp.size() - 1); - } -} - -public HashMap> bfs(String beginWord, String endWord, List wordList) { - Queue queue = new LinkedList<>(); - queue.offer(beginWord); - HashMap> map = new HashMap<>(); - boolean isFound = false; - - Set dict = new HashSet<>(wordList); - while (!queue.isEmpty()) { - int size = queue.size(); - min++; - for (int j = 0; j < size; j++) { - String temp = queue.poll(); - // 一次性得到所有的下一个的节点 - ArrayList neighbors = getNeighbors(temp, dict); - map.put(temp, neighbors); - for (String neighbor : neighbors) { - if (neighbor.equals(endWord)) { - isFound = true; - } - queue.offer(neighbor); - } - } - if (isFound) { - break; - } - } - return map; -} -private ArrayList getNeighbors(String node, Set dict) { - ArrayList res = new ArrayList(); - char chs[] = node.toCharArray(); - - for (char ch = 'a'; ch <= 'z'; ch++) { - for (int i = 0; i < chs.length; i++) { - if (chs[i] == ch) - continue; - char old_ch = chs[i]; - chs[i] = ch; - if (dict.contains(String.valueOf(chs))) { - res.add(String.valueOf(chs)); - } - chs[i] = old_ch; - } - - } - return res; -} -``` - -然而这个优化,对于 `leetcode` 的 `tests` 并没有什么影响。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/126_4.jpg) - -让我们继续考虑优化方案,回到之前的图。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/126_6.jpg) - -假如我们在考虑上图中黄色节点的相邻节点,发现第三层的 `abc` 在第二层已经考虑过了。所以第三层的 `abc` 其实不用再考虑了,第三层的 `abc` 后边的结构一定和第二层后边的结构一样,因为我们要找最短的路径,所以如果产生了最短路径,一定是第二层的 `abc` 首先达到结束单词。 - -所以其实我们在考虑第 `k` 层的某一个单词,如果这个单词在第 `1` 到 `k-1` 层已经出现过,我们其实就不过继续向下探索了。 - -在之前的代码中,我们其实已经考虑了部分这个问题。 - -```java -if (temp.contains(neighbor)) { - continue; -} -``` - -但我们只考虑了当前路径是否含有该单词,而就像上图表示的,其他路径之前已经考虑过了当前单词,我们也是可以跳过的。 - -根据这个优化思路,有两种解决方案。 - -第一种,再利用一个 `HashMap`,记为 `distance` 变量。在 `BFS` 的过程中,把第一次遇到的单词当前的层数存起来。之后遇到也不进行更新,就会是下边的效果。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/126_7.jpg) - -这样我们就可以在 `DFS` 的时候来判断当前黄色的节点的 `distance` 是不是比邻接节点的小 `1`。上图中 `distance` 都是 `1` ,所以不符合,就可以跳过。 - -此外,在 `DFS` 中,因为我们每次都根据节点的层数来进行深搜,所以之前保存最短路径的全局变量 `min` 在这里也就不需要了。 - -```java -public List> findLadders(String beginWord, String endWord, List wordList) { - List> ans = new ArrayList<>(); - // 如果不含有结束单词,直接结束,不然后边会造成死循环 - if (!wordList.contains(endWord)) { - return ans; - } - // 利用 BFS 得到所有的邻居节点,以及每个节点的所在层数 - HashMap distance = new HashMap<>(); - HashMap> map = new HashMap<>(); - bfs(beginWord, endWord, wordList, map, distance); - ArrayList temp = new ArrayList(); - // temp 用来保存当前的路径 - temp.add(beginWord); - findLaddersHelper(beginWord, endWord, map, distance, temp, ans); - return ans; -} - -private void findLaddersHelper(String beginWord, String endWord, HashMap> map, - HashMap distance, ArrayList temp, List> ans) { - if (beginWord.equals(endWord)) { - ans.add(new ArrayList(temp)); - return; - } - // 得到所有的下一个的节点 - /* - "a" - "c" - ["a","b","c"]*/ - //之所以是 map.getOrDefault 而不是 get,就是上边的情况 get 会出错 - ArrayList neighbors = map.getOrDefault(beginWord, new ArrayList()); - for (String neighbor : neighbors) { - //判断层数是否符合 - if (distance.get(beginWord) + 1 == distance.get(neighbor)) { - temp.add(neighbor); - findLaddersHelper(neighbor, endWord, map, distance, temp, ans); - temp.remove(temp.size() - 1); - } - } -} - -public void bfs(String beginWord, String endWord, List wordList, HashMap> map, - HashMap distance) { - Queue queue = new LinkedList<>(); - queue.offer(beginWord); - distance.put(beginWord, 0); - boolean isFound = false; - int depth = 0; - Set dict = new HashSet<>(wordList); - while (!queue.isEmpty()) { - int size = queue.size(); - depth++; - for (int j = 0; j < size; j++) { - String temp = queue.poll(); - // 一次性得到所有的下一个的节点 - ArrayList neighbors = getNeighbors(temp, dict); - map.put(temp, neighbors); - for (String neighbor : neighbors) { - if (!distance.containsKey(neighbor)) { - distance.put(neighbor, depth); - if (neighbor.equals(endWord)) { - isFound = true; - } - queue.offer(neighbor); - } - - } - } - if (isFound) { - break; - } - } -} - -private ArrayList getNeighbors(String node, Set dict) { - ArrayList res = new ArrayList(); - char chs[] = node.toCharArray(); - - for (char ch = 'a'; ch <= 'z'; ch++) { - for (int i = 0; i < chs.length; i++) { - if (chs[i] == ch) - continue; - char old_ch = chs[i]; - chs[i] = ch; - if (dict.contains(String.valueOf(chs))) { - res.add(String.valueOf(chs)); - } - chs[i] = old_ch; - } - - } - return res; -} - -``` - -终于,上边的算法 `AC` 了。上边讲到我们提前存储了 `distance` ,方便在 `DFS` 中来判断我们是否继续深搜。 - -这里再讲一下另一种思路,再回顾一下这个要进行优化的图。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/126_7.jpg) - -我们就是减少了第三层的 `abc` 的情况的判断。我们其实可以不用 `distance` ,在 `BFS` 中,如果发现有邻接节点在之前已经出现过了,我们直接把这个邻接节点删除不去。这样的话,在 `DFS` 中就不用再判断了,直接取邻居节点就可以了。 - -判断之前是否已经处理过,可以用一个 `HashSet` 来把之前的节点存起来进行判断。 - -这里删除邻接节点需要用到一个语言特性,`java` 中遍历 `List` 过程中,不能对 `List` 元素进行删除。如果想边遍历边删除,可以借助迭代器。 - -```java -Iterator it = neighbors.iterator();//把元素导入迭代器 -while (it.hasNext()) { - String neighbor = it.next(); - if (!visited.contains(neighbor)) { - if (neighbor.equals(endWord)) { - isFound = true; - } - queue.offer(neighbor); - subVisited.add(neighbor); - }else{ - it.remove(); - } -} -``` - -此外我们要判断的是当前节点在之前层有没有出现过,当前层正在遍历的节点先加到 `subVisited` 中。 - -```java -public List> findLadders(String beginWord, String endWord, List wordList) { - List> ans = new ArrayList<>(); - if (!wordList.contains(endWord)) { - return ans; - } - // 利用 BFS 得到所有的邻居节点 - HashMap> map = new HashMap<>(); - bfs(beginWord, endWord, wordList, map); - ArrayList temp = new ArrayList(); - // temp 用来保存当前的路径 - temp.add(beginWord); - findLaddersHelper(beginWord, endWord, map, temp, ans); - return ans; -} - -private void findLaddersHelper(String beginWord, String endWord, HashMap> map, - ArrayList temp, List> ans) { - if (beginWord.equals(endWord)) { - ans.add(new ArrayList(temp)); - return; - } - // 得到所有的下一个的节点 - ArrayList neighbors = map.getOrDefault(beginWord, new ArrayList()); - for (String neighbor : neighbors) { - temp.add(neighbor); - findLaddersHelper(neighbor, endWord, map, temp, ans); - temp.remove(temp.size() - 1); - - } -} - -public void bfs(String beginWord, String endWord, List wordList, HashMap> map) { - Queue queue = new LinkedList<>(); - queue.offer(beginWord); - boolean isFound = false; - int depth = 0; - Set dict = new HashSet<>(wordList); - Set visited = new HashSet<>(); - visited.add(beginWord); - while (!queue.isEmpty()) { - int size = queue.size(); - depth++; - Set subVisited = new HashSet<>(); - for (int j = 0; j < size; j++) { - String temp = queue.poll(); - // 一次性得到所有的下一个的节点 - ArrayList neighbors = getNeighbors(temp, dict); - Iterator it = neighbors.iterator();//把元素导入迭代器 - while (it.hasNext()) { - String neighbor = it.next(); - if (!visited.contains(neighbor)) { - if (neighbor.equals(endWord)) { - isFound = true; - } - queue.offer(neighbor); - subVisited.add(neighbor); - }else{ - it.remove(); - } - } - map.put(temp, neighbors); - } - visited.addAll(subVisited); - if (isFound) { - break; - } - } -} - -private ArrayList getNeighbors(String node, Set dict) { - ArrayList res = new ArrayList(); - char chs[] = node.toCharArray(); - - for (char ch = 'a'; ch <= 'z'; ch++) { - for (int i = 0; i < chs.length; i++) { - if (chs[i] == ch) - continue; - char old_ch = chs[i]; - chs[i] = ch; - if (dict.contains(String.valueOf(chs))) { - res.add(String.valueOf(chs)); - } - chs[i] = old_ch; - } - - } - return res; -} -``` - -# 解法二 BFS - -如果理解了上边的 `DFS` 过程,接下来就很好讲了。上边 `DFS` 借助了 `BFS` 把所有的邻接关系保存了起来,再用 `DFS` 进行深度搜索。 - -我们可不可以只用 `BFS`,一边进行层次遍历,一边就保存结果。当到达结束单词的时候,就把结果存储。省去再进行 `DFS` 的过程。 - -是完全可以的,`BFS` 的队列就不去存储 `String` 了,直接去存到目前为止的路径,也就是一个 `List`。 - -```java -public List> findLadders(String beginWord, String endWord, List wordList) { - List> ans = new ArrayList<>(); - // 如果不含有结束单词,直接结束,不然后边会造成死循环 - if (!wordList.contains(endWord)) { - return ans; - } - bfs(beginWord, endWord, wordList, ans); - return ans; -} - -public void bfs(String beginWord, String endWord, List wordList, List> ans) { - Queue> queue = new LinkedList<>(); - List path = new ArrayList<>(); - path.add(beginWord); - queue.offer(path); - boolean isFound = false; - Set dict = new HashSet<>(wordList); - Set visited = new HashSet<>(); - visited.add(beginWord); - while (!queue.isEmpty()) { - int size = queue.size(); - Set subVisited = new HashSet<>(); - for (int j = 0; j < size; j++) { - List p = queue.poll(); - //得到当前路径的末尾单词 - String temp = p.get(p.size() - 1); - // 一次性得到所有的下一个的节点 - ArrayList neighbors = getNeighbors(temp, dict); - for (String neighbor : neighbors) { - //只考虑之前没有出现过的单词 - if (!visited.contains(neighbor)) { - //到达结束单词 - if (neighbor.equals(endWord)) { - isFound = true; - p.add(neighbor); - ans.add(new ArrayList(p)); - p.remove(p.size() - 1); - } - //加入当前单词 - p.add(neighbor); - queue.offer(new ArrayList(p)); - p.remove(p.size() - 1); - subVisited.add(neighbor); - } - } - } - visited.addAll(subVisited); - if (isFound) { - break; - } - } -} - -private ArrayList getNeighbors(String node, Set dict) { - ArrayList res = new ArrayList(); - char chs[] = node.toCharArray(); - for (char ch = 'a'; ch <= 'z'; ch++) { - for (int i = 0; i < chs.length; i++) { - if (chs[i] == ch) - continue; - char old_ch = chs[i]; - chs[i] = ch; - if (dict.contains(String.valueOf(chs))) { - res.add(String.valueOf(chs)); - } - chs[i] = old_ch; - } - - } - return res; -} -``` - -代码看起来简洁了很多。 - -# 解法三 DFS + BFS 双向搜索(two-end BFS) - -在解法一的思路上,我们还能够继续优化。 - -解法一中,我们利用了 `BFS` 建立了每个节点的邻居节点。在之前的示意图中,我们把同一个字符串也画在了不同节点。这里把同一个节点画在一起,再看一下。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/126_8.jpg) - -我们可以从结束单词反向进行 `BFS`。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/126_9.jpg) - -这样的话,当两个方向产生了共同的节点,就是我们的最短路径了。 - -至于每次从哪个方向扩展,我们可以每次选择需要扩展的节点数少的方向进行扩展。 - -例如上图中,一开始需要向下扩展的个数是 `1` 个,需要向上扩展的个数是 `1` 个。个数相等,我们就向下扩展。然后需要向下扩展的个数就变成了 `4` 个,而需要向上扩展的个数是 `1` 个,所以此时我们向上扩展。接着,需要向上扩展的个数变成了 `6` 个,需要向下扩展的个数是 `4` 个,我们就向下扩展......直到相遇。 - -双向扩展的好处,我们粗略的估计一下时间复杂度。 - -假设 `beginword` 和 `endword` 之间的距离是 `d`。每个节点可以扩展出 `k` 个节点。 - -那么正常的时间复杂就是 $$k^d$$。 - -双向搜索的时间复杂度就是 $$k^{d/2} + k^{d/2}$$。 - -```java -public List> findLadders(String beginWord, String endWord, List wordList) { - List> ans = new ArrayList<>(); - if (!wordList.contains(endWord)) { - return ans; - } - // 利用 BFS 得到所有的邻居节点 - HashMap> map = new HashMap<>(); - bfs(beginWord, endWord, wordList, map); - ArrayList temp = new ArrayList(); - // temp 用来保存当前的路径 - temp.add(beginWord); - findLaddersHelper(beginWord, endWord, map, temp, ans); - return ans; -} - -private void findLaddersHelper(String beginWord, String endWord, HashMap> map, - ArrayList temp, List> ans) { - if (beginWord.equals(endWord)) { - ans.add(new ArrayList(temp)); - return; - } - // 得到所有的下一个的节点 - ArrayList neighbors = map.getOrDefault(beginWord, new ArrayList()); - for (String neighbor : neighbors) { - temp.add(neighbor); - findLaddersHelper(neighbor, endWord, map, temp, ans); - temp.remove(temp.size() - 1); - } -} - -//利用递归实现了双向搜索 -private void bfs(String beginWord, String endWord, List wordList, HashMap> map) { - Set set1 = new HashSet(); - set1.add(beginWord); - Set set2 = new HashSet(); - set2.add(endWord); - Set wordSet = new HashSet(wordList); - bfsHelper(set1, set2, wordSet, true, map); -} - -// direction 为 true 代表向下扩展,false 代表向上扩展 -private boolean bfsHelper(Set set1, Set set2, Set wordSet, boolean direction, - HashMap> map) { - //set1 为空了,就直接结束 - //比如下边的例子就会造成 set1 为空 - /* "hot" - "dog" - ["hot","dog"]*/ - if(set1.isEmpty()){ - return false; - } - // set1 的数量多,就反向扩展 - if (set1.size() > set2.size()) { - return bfsHelper(set2, set1, wordSet, !direction, map); - } - // 将已经访问过单词删除 - wordSet.removeAll(set1); - wordSet.removeAll(set2); - - boolean done = false; - - // 保存新扩展得到的节点 - Set set = new HashSet(); - - for (String str : set1) { - //遍历每一位 - for (int i = 0; i < str.length(); i++) { - char[] chars = str.toCharArray(); - - // 尝试所有字母 - for (char ch = 'a'; ch <= 'z'; ch++) { - if(chars[i] == ch){ - continue; - } - chars[i] = ch; - - String word = new String(chars); - - // 根据方向得到 map 的 key 和 val - String key = direction ? str : word; - String val = direction ? word : str; - - ArrayList list = map.containsKey(key) ? map.get(key) : new ArrayList(); - - //如果相遇了就保存结果 - if (set2.contains(word)) { - done = true; - list.add(val); - map.put(key, list); - } - - //如果还没有相遇,并且新的单词在 word 中,那么就加到 set 中 - if (!done && wordSet.contains(word)) { - set.add(word); - list.add(val); - map.put(key, list); - } - } - } - } - - //一般情况下新扩展的元素会多一些,所以我们下次反方向扩展 set2 - return done || bfsHelper(set2, set, wordSet, !direction, map); - -} -``` - -# 总 - -最近事情比较多,这道题每天想一想,陆陆续续拖了好几天了。这道题本质上就是在正常的遍历的基础上,去将一些分支剪去,从而提高速度。至于方法的话,除了我上边介绍的实现方式,应该也会有很多其它的方式,但其实本质上是为了实现一样的东西。另外,双向搜索的方法,自己第一次遇到,网上搜了一下,看样子还是比较经典的一个算法。主要就是用于解决已知起点和终点,去求图的最短路径的问题。 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/126.png) + +给定两个单词,一个作为开始,一个作为结束,还有一个单词列表。然后依次选择单词,只有当前单词到下一个单词只有一个字母不同才能被选择,然后新的单词再作为当前单词,直到选到结束的单词。输出这个的最短路径,如果有多组,则都输出。 + +# 思路分析 + +结合了开始自己的想法,又看了 [Discuss](),这道题有些难讲清楚,一个原因就是解法的代码会很长,这里理一下整个的思路。 + +如果我们从开始的单词,把与之能够转换的单词连起来,它就会长成下边的样子。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/126_3.jpg) + +橙色表示结束单词,上图橙色的路线就是我们要找的最短路径。所以我们要做的其实就是遍历上边的树,然后判断当前节点是不是结束单词,找到结束单词后,还要判断当前是不是最短的路径。说到遍历当然就是两种思路了,`DFS` 或者 `BFS`。 + +# 解法一 DFS + +利用回溯的思想,做一个 DFS。 + +首先要解决的问题是怎么找到节点的所有孩子节点。这里有两种方案。 + +第一种,遍历 `wordList` 来判断每个单词和当前单词是否只有一个字母不同。 + +```java +for (int i = 0; i < wordList.size(); i++) { + String curWord = wordList.get(i); + //符合只有一个单词不同,就进入递归 + if (oneChanged(beginWord, curWord)) { + //此时代表可以从 beginWord -> curWord + } +} + +private boolean oneChanged(String beginWord, String curWord) { + int count = 0; + for (int i = 0; i < beginWord.length(); i++) { + if (beginWord.charAt(i) != curWord.charAt(i)) { + count++; + } + if (count == 2) { + return false; + } + } + return count == 1; +} +``` + +这种的时间复杂度的话,如果 `wordList` 长度为 `m`,每个单词的长度为 `n`。那么就是 `O(mn)`。 +第二种,将要找的节点单词的每个位置换一个字符,然后看更改后的单词在不在 `wordList` 中。 + +```java +//dict 就是 wordList,为了提高速度,从 List 转为 HashSet +//cur 是我们要考虑的单词 +private List getNext(String cur, Set dict) { + List res = new ArrayList<>(); + char[] chars = cur.toCharArray(); + //考虑每一位 + for (int i = 0; i < chars.length; i++) { + char old = chars[i]; + //考虑变成其他所有的字母 + for (char c = 'a'; c <= 'z'; c++) { + if (c == old) { + continue; + } + chars[i] = c; + String next = new String(chars); + //判断 wordList 是否包含修改后的单词 + if (dict.contains(next)) { + res.add(next); + } + } + chars[i] = old; + } + return res; +} +``` + +这种的话,由于用到了 `HashSet` ,所以 `contains` 函数就是 `O(1)`。所以整个计算量就是 `26n`,所以是 `O(n)`。 + +还要解决的一个问题是,因为我们要找的是最短的路径。但是事先我们并不知道最短的路径是多少,我们需要一个全局变量来保存当前找到的路径的长度。如果找到的新的路径的长度比之前的路径短,就把之前的结果清空,重新找,如果是最小的长度,就加入到结果中。 + +看下一递归出口。 + +```java +//到了结尾单词 +if (beginWord.equals(endWord)) { + //当前长度更小,清空之前的,加新的路径加入到结果中 + if (min > temp.size()) { + ans.clear(); + min = temp.size(); + ans.add(new ArrayList(temp)); + //相等的话就直接加路径加入到结果中 + } else if (min == temp.size()) { + ans.add(new ArrayList(temp)); + } + return; +} +//当前的长度到达了 min,还是没有到达结束单词就提前结束 +if (temp.size() >= min) { + return; +} +``` + +得到下一个节点刚才讲了两种思路,我们先采用第一种解法,看一下效果。 + +```java +public List> findLadders(String beginWord, String endWord, List wordList) { + List> ans = new ArrayList<>(); + ArrayList temp = new ArrayList(); + //temp 用来保存当前的路径 + temp.add(beginWord); + findLaddersHelper(beginWord, endWord, wordList, temp, ans); + return ans; +} + +int min = Integer.MAX_VALUE; + +private void findLaddersHelper(String beginWord, String endWord, List wordList, + ArrayList temp, List> ans) { + if (beginWord.equals(endWord)) { + if (min > temp.size()) { + ans.clear(); + min = temp.size(); + ans.add(new ArrayList(temp)); + } else if (min == temp.size()) { + ans.add(new ArrayList(temp)); + } + return; + } + //当前的长度到达了 min,还是没有到达结束单词就提前结束 + if (temp.size() >= min) { + return; + } + //遍历当前所有的单词 + for (int i = 0; i < wordList.size(); i++) { + String curWord = wordList.get(i); + //路径中已经含有当前单词,如果再把当前单词加到路径,那肯定会使得路径更长,所以跳过 + if (temp.contains(curWord)) { + continue; + } + //符合只有一个单词不同,就进入递归 + if (oneChanged(beginWord, curWord)) { + temp.add(curWord); + findLaddersHelper(curWord, endWord, wordList, temp, ans); + temp.remove(temp.size() - 1); + } + } +} +private boolean oneChanged(String beginWord, String curWord) { + int count = 0; + for (int i = 0; i < beginWord.length(); i++) { + if (beginWord.charAt(i) != curWord.charAt(i)) { + count++; + } + if (count == 2) { + return false; + } + } + return count == 1; +} +``` + +但是对于普通的输入可以解决,如果 `wordList` 过长的话就会造成超时了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/126_2.jpg) + +得到下一个的节点,如果采用第二种解法呢? + +```java +int min = Integer.MAX_VALUE; +public List> findLadders(String beginWord, String endWord, List wordList) { + List> ans = new ArrayList<>(); + ArrayList temp = new ArrayList(); + temp.add(beginWord); + //temp 用来保存当前的路径 + findLaddersHelper(beginWord, endWord, wordList, temp, ans); + return ans; +} + + +private void findLaddersHelper(String beginWord, String endWord, List wordList, + ArrayList temp, List> ans) { + if (beginWord.equals(endWord)) { + if (min > temp.size()) { + ans.clear(); + min = temp.size(); + ans.add(new ArrayList(temp)); + } else if (min == temp.size()) { + ans.add(new ArrayList(temp)); + } + return; + } + + if (temp.size() >= min) { + return; + } + Set dict = new HashSet<>(wordList); + //一次性到达所有的下一个的节点 + ArrayList neighbors = getNeighbors(beginWord, dict); + for (String neighbor : neighbors) { + if (temp.contains(neighbor)) { + continue; + } + temp.add(neighbor); + findLaddersHelper(neighbor, endWord, wordList, temp, ans); + temp.remove(temp.size() - 1); + } +} + + +private ArrayList getNeighbors(String node, Set dict) { + ArrayList res = new ArrayList(); + char chs[] = node.toCharArray(); + + for (char ch = 'a'; ch <= 'z'; ch++) { + for (int i = 0; i < chs.length; i++) { + if (chs[i] == ch) + continue; + char old_ch = chs[i]; + chs[i] = ch; + if (dict.contains(String.valueOf(chs))) { + res.add(String.valueOf(chs)); + } + chs[i] = old_ch; + } + + } + return res; +} +``` + +快了一些,但是还是超时。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/126_4.jpg) + +我们继续来优化,首先想一下为什么会超时,看一下之前的图。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/126_3.jpg) + +`DFS` 的过程的话,结合上图,就是先考虑了最左边的路径,然后再回溯一下,继续到达底部。然后回溯回溯,终于到了一条含有结束单词的路径,然而事实上这条并不是最短路径。综上,我们会多判断很多无用的路径。 + +如果我们事先知道了最短路径长度是 `4`,那么我们只需要考虑前 `4` 层就足够了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/126_5.jpg) + +怎么知道结束单词在哪一层呢?只能一层层的找了,也就是 `BFS`。此外,因为上图需要搜索的树提前是没有的,我们需要边找边更新这个树。而在 `DFS` 中,我们也需要这个树,其实就是需要每个节点的所有相邻节点。 + +所以我们在 `BFS` 中,就把每个节点的所有相邻节点保存到 `HashMap` 中,就省去了 `DFS` 再去找相邻节点的时间。 + +此外,`BFS` 的过程中,把最短路径的高度用 `min` 也记录下来,在 `DFS` 的时候到达高度后就可以提前结束。 + +```java +int min = 0; +public List> findLadders(String beginWord, String endWord, List wordList) { + List> ans = new ArrayList<>(); + //如果不含有结束单词,直接结束,不然后边会造成死循环 + if (!wordList.contains(endWord)) { + return ans; + } + //利用 BFS 得到所有的邻居节点 + HashMap> map = bfs(beginWord, endWord, wordList); + ArrayList temp = new ArrayList(); + // temp 用来保存当前的路径 + temp.add(beginWord); + findLaddersHelper(beginWord, endWord, map, temp, ans); + return ans; +} + +private void findLaddersHelper(String beginWord, String endWord, HashMap> map, + ArrayList temp, List> ans) { + if (beginWord.equals(endWord)) { + ans.add(new ArrayList(temp)); + + return; + } + if(temp.size() - 1== min){ + return; + } + // 得到所有的下一个的节点 + ArrayList neighbors = map.getOrDefault(beginWord, new ArrayList()); + for (String neighbor : neighbors) { + if (temp.contains(neighbor)) { + continue; + } + temp.add(neighbor); + findLaddersHelper(neighbor, endWord, map, temp, ans); + temp.remove(temp.size() - 1); + } +} + +public HashMap> bfs(String beginWord, String endWord, List wordList) { + Queue queue = new LinkedList<>(); + queue.offer(beginWord); + HashMap> map = new HashMap<>(); + boolean isFound = false; + + Set dict = new HashSet<>(wordList); + while (!queue.isEmpty()) { + int size = queue.size(); + min++; + for (int j = 0; j < size; j++) { + String temp = queue.poll(); + // 一次性得到所有的下一个的节点 + ArrayList neighbors = getNeighbors(temp, dict); + map.put(temp, neighbors); + for (String neighbor : neighbors) { + if (neighbor.equals(endWord)) { + isFound = true; + } + queue.offer(neighbor); + } + } + if (isFound) { + break; + } + } + return map; +} +private ArrayList getNeighbors(String node, Set dict) { + ArrayList res = new ArrayList(); + char chs[] = node.toCharArray(); + + for (char ch = 'a'; ch <= 'z'; ch++) { + for (int i = 0; i < chs.length; i++) { + if (chs[i] == ch) + continue; + char old_ch = chs[i]; + chs[i] = ch; + if (dict.contains(String.valueOf(chs))) { + res.add(String.valueOf(chs)); + } + chs[i] = old_ch; + } + + } + return res; +} +``` + +然而这个优化,对于 `leetcode` 的 `tests` 并没有什么影响。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/126_4.jpg) + +让我们继续考虑优化方案,回到之前的图。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/126_6.jpg) + +假如我们在考虑上图中黄色节点的相邻节点,发现第三层的 `abc` 在第二层已经考虑过了。所以第三层的 `abc` 其实不用再考虑了,第三层的 `abc` 后边的结构一定和第二层后边的结构一样,因为我们要找最短的路径,所以如果产生了最短路径,一定是第二层的 `abc` 首先达到结束单词。 + +所以其实我们在考虑第 `k` 层的某一个单词,如果这个单词在第 `1` 到 `k-1` 层已经出现过,我们其实就不过继续向下探索了。 + +在之前的代码中,我们其实已经考虑了部分这个问题。 + +```java +if (temp.contains(neighbor)) { + continue; +} +``` + +但我们只考虑了当前路径是否含有该单词,而就像上图表示的,其他路径之前已经考虑过了当前单词,我们也是可以跳过的。 + +根据这个优化思路,有两种解决方案。 + +第一种,再利用一个 `HashMap`,记为 `distance` 变量。在 `BFS` 的过程中,把第一次遇到的单词当前的层数存起来。之后遇到也不进行更新,就会是下边的效果。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/126_7.jpg) + +这样我们就可以在 `DFS` 的时候来判断当前黄色的节点的 `distance` 是不是比邻接节点的小 `1`。上图中 `distance` 都是 `1` ,所以不符合,就可以跳过。 + +此外,在 `DFS` 中,因为我们每次都根据节点的层数来进行深搜,所以之前保存最短路径的全局变量 `min` 在这里也就不需要了。 + +```java +public List> findLadders(String beginWord, String endWord, List wordList) { + List> ans = new ArrayList<>(); + // 如果不含有结束单词,直接结束,不然后边会造成死循环 + if (!wordList.contains(endWord)) { + return ans; + } + // 利用 BFS 得到所有的邻居节点,以及每个节点的所在层数 + HashMap distance = new HashMap<>(); + HashMap> map = new HashMap<>(); + bfs(beginWord, endWord, wordList, map, distance); + ArrayList temp = new ArrayList(); + // temp 用来保存当前的路径 + temp.add(beginWord); + findLaddersHelper(beginWord, endWord, map, distance, temp, ans); + return ans; +} + +private void findLaddersHelper(String beginWord, String endWord, HashMap> map, + HashMap distance, ArrayList temp, List> ans) { + if (beginWord.equals(endWord)) { + ans.add(new ArrayList(temp)); + return; + } + // 得到所有的下一个的节点 + /* + "a" + "c" + ["a","b","c"]*/ + //之所以是 map.getOrDefault 而不是 get,就是上边的情况 get 会出错 + ArrayList neighbors = map.getOrDefault(beginWord, new ArrayList()); + for (String neighbor : neighbors) { + //判断层数是否符合 + if (distance.get(beginWord) + 1 == distance.get(neighbor)) { + temp.add(neighbor); + findLaddersHelper(neighbor, endWord, map, distance, temp, ans); + temp.remove(temp.size() - 1); + } + } +} + +public void bfs(String beginWord, String endWord, List wordList, HashMap> map, + HashMap distance) { + Queue queue = new LinkedList<>(); + queue.offer(beginWord); + distance.put(beginWord, 0); + boolean isFound = false; + int depth = 0; + Set dict = new HashSet<>(wordList); + while (!queue.isEmpty()) { + int size = queue.size(); + depth++; + for (int j = 0; j < size; j++) { + String temp = queue.poll(); + // 一次性得到所有的下一个的节点 + ArrayList neighbors = getNeighbors(temp, dict); + map.put(temp, neighbors); + for (String neighbor : neighbors) { + if (!distance.containsKey(neighbor)) { + distance.put(neighbor, depth); + if (neighbor.equals(endWord)) { + isFound = true; + } + queue.offer(neighbor); + } + + } + } + if (isFound) { + break; + } + } +} + +private ArrayList getNeighbors(String node, Set dict) { + ArrayList res = new ArrayList(); + char chs[] = node.toCharArray(); + + for (char ch = 'a'; ch <= 'z'; ch++) { + for (int i = 0; i < chs.length; i++) { + if (chs[i] == ch) + continue; + char old_ch = chs[i]; + chs[i] = ch; + if (dict.contains(String.valueOf(chs))) { + res.add(String.valueOf(chs)); + } + chs[i] = old_ch; + } + + } + return res; +} + +``` + +终于,上边的算法 `AC` 了。上边讲到我们提前存储了 `distance` ,方便在 `DFS` 中来判断我们是否继续深搜。 + +这里再讲一下另一种思路,再回顾一下这个要进行优化的图。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/126_7.jpg) + +我们就是减少了第三层的 `abc` 的情况的判断。我们其实可以不用 `distance` ,在 `BFS` 中,如果发现有邻接节点在之前已经出现过了,我们直接把这个邻接节点删除不去。这样的话,在 `DFS` 中就不用再判断了,直接取邻居节点就可以了。 + +判断之前是否已经处理过,可以用一个 `HashSet` 来把之前的节点存起来进行判断。 + +这里删除邻接节点需要用到一个语言特性,`java` 中遍历 `List` 过程中,不能对 `List` 元素进行删除。如果想边遍历边删除,可以借助迭代器。 + +```java +Iterator it = neighbors.iterator();//把元素导入迭代器 +while (it.hasNext()) { + String neighbor = it.next(); + if (!visited.contains(neighbor)) { + if (neighbor.equals(endWord)) { + isFound = true; + } + queue.offer(neighbor); + subVisited.add(neighbor); + }else{ + it.remove(); + } +} +``` + +此外我们要判断的是当前节点在之前层有没有出现过,当前层正在遍历的节点先加到 `subVisited` 中。 + +```java +public List> findLadders(String beginWord, String endWord, List wordList) { + List> ans = new ArrayList<>(); + if (!wordList.contains(endWord)) { + return ans; + } + // 利用 BFS 得到所有的邻居节点 + HashMap> map = new HashMap<>(); + bfs(beginWord, endWord, wordList, map); + ArrayList temp = new ArrayList(); + // temp 用来保存当前的路径 + temp.add(beginWord); + findLaddersHelper(beginWord, endWord, map, temp, ans); + return ans; +} + +private void findLaddersHelper(String beginWord, String endWord, HashMap> map, + ArrayList temp, List> ans) { + if (beginWord.equals(endWord)) { + ans.add(new ArrayList(temp)); + return; + } + // 得到所有的下一个的节点 + ArrayList neighbors = map.getOrDefault(beginWord, new ArrayList()); + for (String neighbor : neighbors) { + temp.add(neighbor); + findLaddersHelper(neighbor, endWord, map, temp, ans); + temp.remove(temp.size() - 1); + + } +} + +public void bfs(String beginWord, String endWord, List wordList, HashMap> map) { + Queue queue = new LinkedList<>(); + queue.offer(beginWord); + boolean isFound = false; + int depth = 0; + Set dict = new HashSet<>(wordList); + Set visited = new HashSet<>(); + visited.add(beginWord); + while (!queue.isEmpty()) { + int size = queue.size(); + depth++; + Set subVisited = new HashSet<>(); + for (int j = 0; j < size; j++) { + String temp = queue.poll(); + // 一次性得到所有的下一个的节点 + ArrayList neighbors = getNeighbors(temp, dict); + Iterator it = neighbors.iterator();//把元素导入迭代器 + while (it.hasNext()) { + String neighbor = it.next(); + if (!visited.contains(neighbor)) { + if (neighbor.equals(endWord)) { + isFound = true; + } + queue.offer(neighbor); + subVisited.add(neighbor); + }else{ + it.remove(); + } + } + map.put(temp, neighbors); + } + visited.addAll(subVisited); + if (isFound) { + break; + } + } +} + +private ArrayList getNeighbors(String node, Set dict) { + ArrayList res = new ArrayList(); + char chs[] = node.toCharArray(); + + for (char ch = 'a'; ch <= 'z'; ch++) { + for (int i = 0; i < chs.length; i++) { + if (chs[i] == ch) + continue; + char old_ch = chs[i]; + chs[i] = ch; + if (dict.contains(String.valueOf(chs))) { + res.add(String.valueOf(chs)); + } + chs[i] = old_ch; + } + + } + return res; +} +``` + +# 解法二 BFS + +如果理解了上边的 `DFS` 过程,接下来就很好讲了。上边 `DFS` 借助了 `BFS` 把所有的邻接关系保存了起来,再用 `DFS` 进行深度搜索。 + +我们可不可以只用 `BFS`,一边进行层次遍历,一边就保存结果。当到达结束单词的时候,就把结果存储。省去再进行 `DFS` 的过程。 + +是完全可以的,`BFS` 的队列就不去存储 `String` 了,直接去存到目前为止的路径,也就是一个 `List`。 + +```java +public List> findLadders(String beginWord, String endWord, List wordList) { + List> ans = new ArrayList<>(); + // 如果不含有结束单词,直接结束,不然后边会造成死循环 + if (!wordList.contains(endWord)) { + return ans; + } + bfs(beginWord, endWord, wordList, ans); + return ans; +} + +public void bfs(String beginWord, String endWord, List wordList, List> ans) { + Queue> queue = new LinkedList<>(); + List path = new ArrayList<>(); + path.add(beginWord); + queue.offer(path); + boolean isFound = false; + Set dict = new HashSet<>(wordList); + Set visited = new HashSet<>(); + visited.add(beginWord); + while (!queue.isEmpty()) { + int size = queue.size(); + Set subVisited = new HashSet<>(); + for (int j = 0; j < size; j++) { + List p = queue.poll(); + //得到当前路径的末尾单词 + String temp = p.get(p.size() - 1); + // 一次性得到所有的下一个的节点 + ArrayList neighbors = getNeighbors(temp, dict); + for (String neighbor : neighbors) { + //只考虑之前没有出现过的单词 + if (!visited.contains(neighbor)) { + //到达结束单词 + if (neighbor.equals(endWord)) { + isFound = true; + p.add(neighbor); + ans.add(new ArrayList(p)); + p.remove(p.size() - 1); + } + //加入当前单词 + p.add(neighbor); + queue.offer(new ArrayList(p)); + p.remove(p.size() - 1); + subVisited.add(neighbor); + } + } + } + visited.addAll(subVisited); + if (isFound) { + break; + } + } +} + +private ArrayList getNeighbors(String node, Set dict) { + ArrayList res = new ArrayList(); + char chs[] = node.toCharArray(); + for (char ch = 'a'; ch <= 'z'; ch++) { + for (int i = 0; i < chs.length; i++) { + if (chs[i] == ch) + continue; + char old_ch = chs[i]; + chs[i] = ch; + if (dict.contains(String.valueOf(chs))) { + res.add(String.valueOf(chs)); + } + chs[i] = old_ch; + } + + } + return res; +} +``` + +代码看起来简洁了很多。 + +# 解法三 DFS + BFS 双向搜索(two-end BFS) + +在解法一的思路上,我们还能够继续优化。 + +解法一中,我们利用了 `BFS` 建立了每个节点的邻居节点。在之前的示意图中,我们把同一个字符串也画在了不同节点。这里把同一个节点画在一起,再看一下。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/126_8.jpg) + +我们可以从结束单词反向进行 `BFS`。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/126_9.jpg) + +这样的话,当两个方向产生了共同的节点,就是我们的最短路径了。 + +至于每次从哪个方向扩展,我们可以每次选择需要扩展的节点数少的方向进行扩展。 + +例如上图中,一开始需要向下扩展的个数是 `1` 个,需要向上扩展的个数是 `1` 个。个数相等,我们就向下扩展。然后需要向下扩展的个数就变成了 `4` 个,而需要向上扩展的个数是 `1` 个,所以此时我们向上扩展。接着,需要向上扩展的个数变成了 `6` 个,需要向下扩展的个数是 `4` 个,我们就向下扩展......直到相遇。 + +双向扩展的好处,我们粗略的估计一下时间复杂度。 + +假设 `beginword` 和 `endword` 之间的距离是 `d`。每个节点可以扩展出 `k` 个节点。 + +那么正常的时间复杂就是 $$k^d$$。 + +双向搜索的时间复杂度就是 $$k^{d/2} + k^{d/2}$$。 + +```java +public List> findLadders(String beginWord, String endWord, List wordList) { + List> ans = new ArrayList<>(); + if (!wordList.contains(endWord)) { + return ans; + } + // 利用 BFS 得到所有的邻居节点 + HashMap> map = new HashMap<>(); + bfs(beginWord, endWord, wordList, map); + ArrayList temp = new ArrayList(); + // temp 用来保存当前的路径 + temp.add(beginWord); + findLaddersHelper(beginWord, endWord, map, temp, ans); + return ans; +} + +private void findLaddersHelper(String beginWord, String endWord, HashMap> map, + ArrayList temp, List> ans) { + if (beginWord.equals(endWord)) { + ans.add(new ArrayList(temp)); + return; + } + // 得到所有的下一个的节点 + ArrayList neighbors = map.getOrDefault(beginWord, new ArrayList()); + for (String neighbor : neighbors) { + temp.add(neighbor); + findLaddersHelper(neighbor, endWord, map, temp, ans); + temp.remove(temp.size() - 1); + } +} + +//利用递归实现了双向搜索 +private void bfs(String beginWord, String endWord, List wordList, HashMap> map) { + Set set1 = new HashSet(); + set1.add(beginWord); + Set set2 = new HashSet(); + set2.add(endWord); + Set wordSet = new HashSet(wordList); + bfsHelper(set1, set2, wordSet, true, map); +} + +// direction 为 true 代表向下扩展,false 代表向上扩展 +private boolean bfsHelper(Set set1, Set set2, Set wordSet, boolean direction, + HashMap> map) { + //set1 为空了,就直接结束 + //比如下边的例子就会造成 set1 为空 + /* "hot" + "dog" + ["hot","dog"]*/ + if(set1.isEmpty()){ + return false; + } + // set1 的数量多,就反向扩展 + if (set1.size() > set2.size()) { + return bfsHelper(set2, set1, wordSet, !direction, map); + } + // 将已经访问过单词删除 + wordSet.removeAll(set1); + wordSet.removeAll(set2); + + boolean done = false; + + // 保存新扩展得到的节点 + Set set = new HashSet(); + + for (String str : set1) { + //遍历每一位 + for (int i = 0; i < str.length(); i++) { + char[] chars = str.toCharArray(); + + // 尝试所有字母 + for (char ch = 'a'; ch <= 'z'; ch++) { + if(chars[i] == ch){ + continue; + } + chars[i] = ch; + + String word = new String(chars); + + // 根据方向得到 map 的 key 和 val + String key = direction ? str : word; + String val = direction ? word : str; + + ArrayList list = map.containsKey(key) ? map.get(key) : new ArrayList(); + + //如果相遇了就保存结果 + if (set2.contains(word)) { + done = true; + list.add(val); + map.put(key, list); + } + + //如果还没有相遇,并且新的单词在 word 中,那么就加到 set 中 + if (!done && wordSet.contains(word)) { + set.add(word); + list.add(val); + map.put(key, list); + } + } + } + } + + //一般情况下新扩展的元素会多一些,所以我们下次反方向扩展 set2 + return done || bfsHelper(set2, set, wordSet, !direction, map); + +} +``` + +# 总 + +最近事情比较多,这道题每天想一想,陆陆续续拖了好几天了。这道题本质上就是在正常的遍历的基础上,去将一些分支剪去,从而提高速度。至于方法的话,除了我上边介绍的实现方式,应该也会有很多其它的方式,但其实本质上是为了实现一样的东西。另外,双向搜索的方法,自己第一次遇到,网上搜了一下,看样子还是比较经典的一个算法。主要就是用于解决已知起点和终点,去求图的最短路径的问题。 + diff --git a/leetCode-127-Word-Ladder.md b/leetCode-127-Word-Ladder.md index cc2a4859b..8f5b97059 100644 --- a/leetCode-127-Word-Ladder.md +++ b/leetCode-127-Word-Ladder.md @@ -1,339 +1,339 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/127.jpg) - -给定一个开始单词和一个结束单词,一个单词列表,两个单词间转换原则是有且仅有一个字母不同。求出从开始单词转换到结束单词的最短路径长度是多少。 - -# 思路分析 - -基本上就是 [126 题]() 的简化版了,可以先看一下 [126 题]() 的解法思路。接下来就按照 126 题的思路讲了。把之前的图贴过来。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/126_3.jpg) - -要求最短的路径,`DFS` 肯定在这里就不合适了。而在之前我们用 `BFS` 将每个节点的邻接节点求了出来,这个过程其实我们相当于已经把最短路径的长度求出来了。所以我们只需要把之前的 `BFS` 拿过来稍加修改即可。 - -# 解法一 BFS - -添加一个变量 `len`,遍历完每一层就将 `len` 加 `1`。 - -```java -public int ladderLength(String beginWord, String endWord, List wordList) { - if (!wordList.contains(endWord)) { - return 0; - } - int len = 0; - Queue queue = new LinkedList<>(); - queue.offer(beginWord); - boolean isFound = false; - Set dict = new HashSet<>(wordList); - Set visited = new HashSet<>(); - visited.add(beginWord); - while (!queue.isEmpty()) { - int size = queue.size(); - Set subVisited = new HashSet<>(); - for (int j = 0; j < size; j++) { - String temp = queue.poll(); - // 一次性得到所有的下一个的节点 - ArrayList neighbors = getNeighbors(temp, dict); - for (String neighbor : neighbors) { - if (!visited.contains(neighbor)) { - subVisited.add(neighbor); - //到达了结束单词,提前结束 - if (neighbor.equals(endWord)) { - isFound = true; - break; - } - queue.offer(neighbor); - } - } - - } - //当前层添加了元素,长度加一 - if (subVisited.size() > 0) { - len++; - } - visited.addAll(subVisited); - //找到以后,提前结束 while 循环,并且因为这里的层数从 0 计数,所以还需要加 1 - if (isFound) { - len++; - break; - } - } - return len; -} - -private ArrayList getNeighbors(String node, Set dict) { - ArrayList res = new ArrayList(); - char chs[] = node.toCharArray(); - - for (char ch = 'a'; ch <= 'z'; ch++) { - for (int i = 0; i < chs.length; i++) { - if (chs[i] == ch) - continue; - char old_ch = chs[i]; - chs[i] = ch; - if (dict.contains(String.valueOf(chs))) { - res.add(String.valueOf(chs)); - } - chs[i] = old_ch; - } - - } - return res; -} -``` - ---- - -`2020.6.22` 更新,感谢 @cicada 指出,`leetcode` 增加了 `case` ,上边的代码不能 `AC` 了,我们需要考虑从第一个单词无法转到最后一个单词的情况,所以 `return` 前需要判断下。 - -```java -public int ladderLength(String beginWord, String endWord, List wordList) { - if (!wordList.contains(endWord)) { - return 0; - } - int len = 0; - Queue queue = new LinkedList<>(); - queue.offer(beginWord); - boolean isFound = false; - Set dict = new HashSet<>(wordList); - Set visited = new HashSet<>(); - visited.add(beginWord); - while (!queue.isEmpty()) { - int size = queue.size(); - Set subVisited = new HashSet<>(); - for (int j = 0; j < size; j++) { - String temp = queue.poll(); - // 一次性得到所有的下一个的节点 - ArrayList neighbors = getNeighbors(temp, dict); - for (String neighbor : neighbors) { - if (!visited.contains(neighbor)) { - subVisited.add(neighbor); - //到达了结束单词,提前结束 - if (neighbor.equals(endWord)) { - isFound = true; - break; - } - queue.offer(neighbor); - } - } - - } - //当前层添加了元素,长度加一 - if (subVisited.size() > 0) { - len++; - } - visited.addAll(subVisited); - //找到以后,提前结束 while 循环,并且因为这里的层数从 0 计数,所以还需要加 1 - if (isFound) { - len++; - break; - } - } - if(isFound){ - return len; - }else{ - return 0; - } - -} - -private ArrayList getNeighbors(String node, Set dict) { - ArrayList res = new ArrayList(); - char chs[] = node.toCharArray(); - - for (char ch = 'a'; ch <= 'z'; ch++) { - for (int i = 0; i < chs.length; i++) { - if (chs[i] == ch) - continue; - char old_ch = chs[i]; - chs[i] = ch; - if (dict.contains(String.valueOf(chs))) { - res.add(String.valueOf(chs)); - } - chs[i] = old_ch; - } - - } - return res; -} -``` - ---- - -[126 题]() 中介绍了得到当前节点的相邻节点的两种方案,[官方题解]() 中又提供了一种思路,虽然不容易想到,但蛮有意思,分享一下。 - -就是把所有的单词归类,具体的例子会好理解一些。 - -```java -一个单词会产生三个类别,比如 hot 会产生 -*ot -h*t -ho* -然后考虑每一个单词,如果产生了相同的类,就把这些单词放在一起 - -假如我们的单词列表是 ["hot","dot","dog","lot","log","cog"] - -考虑 hot,当前的分类结果如下 -*ot -> [hot] -h*t -> [hot] -ho* -> [hot] - -再考虑 dot,当前的分类结果如下 -*ot -> [hot dot] -h*t -> [hot] -ho* -> [hot] - -d*t -> [dot] -do* -> [dot] - -再考虑 dog,当前的分类结果如下 -*ot -> [hot dot] -h*t -> [hot] -ho* -> [hot] - -d*t -> [dot] -do* -> [dot dog] - -*og -> [dog] -d*g -> [dog] - -再考虑 lot,当前的分类结果如下 -*ot -> [hot dot lot] -h*t -> [hot] -ho* -> [hot] - -d*t -> [dot] -do* -> [dot dog] - -*og -> [dog] -d*g -> [dog] - -l*t -> [lot] -lo* -> [lot] - -然后把每个单词都放到对应的类中,这样最后找当前单词邻居节点的时候就方便了。 -比如找 hot 的邻居节点,因为它可以产生 *ot, h*t, ho* 三个类别,所有它的相邻节点就是上边分好类的相应单词 -``` - -# 解法二 双向搜索 - -在 [126 题]() 最后一种解法中介绍了双向搜索,大大降低了时间复杂度。当然这里也可以直接用,同样是增加 `len` 变量即可,只不过之前用的递归,把 `len` 加到全局变量会更加方便些。 - -```java -int len = 2; //因为把 beginWord 和 endWord 都加入了路径,所以初始化 2 -public int ladderLength(String beginWord, String endWord, List wordList) { - if (!wordList.contains(endWord)) { - return 0; - } - // 利用 BFS 得到所有的邻居节点 - Set set1 = new HashSet(); - set1.add(beginWord); - Set set2 = new HashSet(); - set2.add(endWord); - Set wordSet = new HashSet(wordList); - //最后没找到返回 0 - if (!bfsHelper(set1, set2, wordSet)) { - return 0; - } - return len; -} - -private boolean bfsHelper(Set set1, Set set2, Set wordSet) { - if (set1.isEmpty()) { - return false; - } - // set1 的数量多,就反向扩展 - if (set1.size() > set2.size()) { - return bfsHelper(set2, set1, wordSet); - } - // 将已经访问过单词删除 - wordSet.removeAll(set1); - wordSet.removeAll(set2); - // 保存新扩展得到的节点 - Set set = new HashSet(); - for (String str : set1) { - // 遍历每一位 - for (int i = 0; i < str.length(); i++) { - char[] chars = str.toCharArray(); - - // 尝试所有字母 - for (char ch = 'a'; ch <= 'z'; ch++) { - if (chars[i] == ch) { - continue; - } - chars[i] = ch; - String word = new String(chars); - if (set2.contains(word)) { - return true; - } - // 如果还没有相遇,并且新的单词在 word 中,那么就加到 set 中 - if (wordSet.contains(word)) { - set.add(word); - } - } - } - } - //如果当前进行了扩展,长度加 1 - if (set.size() > 0) { - len++; - } - // 一般情况下新扩展的元素会多一些,所以我们下次反方向扩展 set2 - return bfsHelper(set2, set, wordSet); - -} -``` - -当然,也可以不用递归,可以用两个队列就行了,直接把 [这里]() 的代码贴过来供参考把,思想还是不变的。 - -```C++ -public class Solution { - public int ladderLength(String beginWord, String endWord, List wordList) { - if(!wordList.contains(endWord)) return 0; - Set beginSet = new HashSet(), endSet = new HashSet(); - int len = 1; - HashSet visited = new HashSet(); - HashSet dict = new HashSet(wordList); - beginSet.add(beginWord); - endSet.add(endWord); - while (!beginSet.isEmpty() && !endSet.isEmpty()) { - if (beginSet.size() > endSet.size()) { - Set set = beginSet; - beginSet = endSet; - endSet = set; - } - - Set temp = new HashSet(); - for (String word : beginSet) { - char[] chs = word.toCharArray(); - - for (int i = 0; i < chs.length; i++) { - for (char c = 'a'; c <= 'z'; c++) { - char old = chs[i]; - chs[i] = c; - String target = String.valueOf(chs); - - if (endSet.contains(target)) { - return len + 1; - } - - if (!visited.contains(target) && dict.contains(target)) { - temp.add(target); - visited.add(target); - } - chs[i] = old; - } - } - } - beginSet = temp; - len++; - } - return 0; - } -} -``` - -# 总 - -基本上和 [126 题]() 解决思路是一样的,主要就是 `BFS` 的应用。解法二中,直接在递归中的基础上用全局变量,有时候确实很方便,哈哈,比如之前的 [124 题]()。 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/127.jpg) + +给定一个开始单词和一个结束单词,一个单词列表,两个单词间转换原则是有且仅有一个字母不同。求出从开始单词转换到结束单词的最短路径长度是多少。 + +# 思路分析 + +基本上就是 [126 题]() 的简化版了,可以先看一下 [126 题]() 的解法思路。接下来就按照 126 题的思路讲了。把之前的图贴过来。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/126_3.jpg) + +要求最短的路径,`DFS` 肯定在这里就不合适了。而在之前我们用 `BFS` 将每个节点的邻接节点求了出来,这个过程其实我们相当于已经把最短路径的长度求出来了。所以我们只需要把之前的 `BFS` 拿过来稍加修改即可。 + +# 解法一 BFS + +添加一个变量 `len`,遍历完每一层就将 `len` 加 `1`。 + +```java +public int ladderLength(String beginWord, String endWord, List wordList) { + if (!wordList.contains(endWord)) { + return 0; + } + int len = 0; + Queue queue = new LinkedList<>(); + queue.offer(beginWord); + boolean isFound = false; + Set dict = new HashSet<>(wordList); + Set visited = new HashSet<>(); + visited.add(beginWord); + while (!queue.isEmpty()) { + int size = queue.size(); + Set subVisited = new HashSet<>(); + for (int j = 0; j < size; j++) { + String temp = queue.poll(); + // 一次性得到所有的下一个的节点 + ArrayList neighbors = getNeighbors(temp, dict); + for (String neighbor : neighbors) { + if (!visited.contains(neighbor)) { + subVisited.add(neighbor); + //到达了结束单词,提前结束 + if (neighbor.equals(endWord)) { + isFound = true; + break; + } + queue.offer(neighbor); + } + } + + } + //当前层添加了元素,长度加一 + if (subVisited.size() > 0) { + len++; + } + visited.addAll(subVisited); + //找到以后,提前结束 while 循环,并且因为这里的层数从 0 计数,所以还需要加 1 + if (isFound) { + len++; + break; + } + } + return len; +} + +private ArrayList getNeighbors(String node, Set dict) { + ArrayList res = new ArrayList(); + char chs[] = node.toCharArray(); + + for (char ch = 'a'; ch <= 'z'; ch++) { + for (int i = 0; i < chs.length; i++) { + if (chs[i] == ch) + continue; + char old_ch = chs[i]; + chs[i] = ch; + if (dict.contains(String.valueOf(chs))) { + res.add(String.valueOf(chs)); + } + chs[i] = old_ch; + } + + } + return res; +} +``` + +--- + +`2020.6.22` 更新,感谢 @cicada 指出,`leetcode` 增加了 `case` ,上边的代码不能 `AC` 了,我们需要考虑从第一个单词无法转到最后一个单词的情况,所以 `return` 前需要判断下。 + +```java +public int ladderLength(String beginWord, String endWord, List wordList) { + if (!wordList.contains(endWord)) { + return 0; + } + int len = 0; + Queue queue = new LinkedList<>(); + queue.offer(beginWord); + boolean isFound = false; + Set dict = new HashSet<>(wordList); + Set visited = new HashSet<>(); + visited.add(beginWord); + while (!queue.isEmpty()) { + int size = queue.size(); + Set subVisited = new HashSet<>(); + for (int j = 0; j < size; j++) { + String temp = queue.poll(); + // 一次性得到所有的下一个的节点 + ArrayList neighbors = getNeighbors(temp, dict); + for (String neighbor : neighbors) { + if (!visited.contains(neighbor)) { + subVisited.add(neighbor); + //到达了结束单词,提前结束 + if (neighbor.equals(endWord)) { + isFound = true; + break; + } + queue.offer(neighbor); + } + } + + } + //当前层添加了元素,长度加一 + if (subVisited.size() > 0) { + len++; + } + visited.addAll(subVisited); + //找到以后,提前结束 while 循环,并且因为这里的层数从 0 计数,所以还需要加 1 + if (isFound) { + len++; + break; + } + } + if(isFound){ + return len; + }else{ + return 0; + } + +} + +private ArrayList getNeighbors(String node, Set dict) { + ArrayList res = new ArrayList(); + char chs[] = node.toCharArray(); + + for (char ch = 'a'; ch <= 'z'; ch++) { + for (int i = 0; i < chs.length; i++) { + if (chs[i] == ch) + continue; + char old_ch = chs[i]; + chs[i] = ch; + if (dict.contains(String.valueOf(chs))) { + res.add(String.valueOf(chs)); + } + chs[i] = old_ch; + } + + } + return res; +} +``` + +--- + +[126 题]() 中介绍了得到当前节点的相邻节点的两种方案,[官方题解]() 中又提供了一种思路,虽然不容易想到,但蛮有意思,分享一下。 + +就是把所有的单词归类,具体的例子会好理解一些。 + +```java +一个单词会产生三个类别,比如 hot 会产生 +*ot +h*t +ho* +然后考虑每一个单词,如果产生了相同的类,就把这些单词放在一起 + +假如我们的单词列表是 ["hot","dot","dog","lot","log","cog"] + +考虑 hot,当前的分类结果如下 +*ot -> [hot] +h*t -> [hot] +ho* -> [hot] + +再考虑 dot,当前的分类结果如下 +*ot -> [hot dot] +h*t -> [hot] +ho* -> [hot] + +d*t -> [dot] +do* -> [dot] + +再考虑 dog,当前的分类结果如下 +*ot -> [hot dot] +h*t -> [hot] +ho* -> [hot] + +d*t -> [dot] +do* -> [dot dog] + +*og -> [dog] +d*g -> [dog] + +再考虑 lot,当前的分类结果如下 +*ot -> [hot dot lot] +h*t -> [hot] +ho* -> [hot] + +d*t -> [dot] +do* -> [dot dog] + +*og -> [dog] +d*g -> [dog] + +l*t -> [lot] +lo* -> [lot] + +然后把每个单词都放到对应的类中,这样最后找当前单词邻居节点的时候就方便了。 +比如找 hot 的邻居节点,因为它可以产生 *ot, h*t, ho* 三个类别,所有它的相邻节点就是上边分好类的相应单词 +``` + +# 解法二 双向搜索 + +在 [126 题]() 最后一种解法中介绍了双向搜索,大大降低了时间复杂度。当然这里也可以直接用,同样是增加 `len` 变量即可,只不过之前用的递归,把 `len` 加到全局变量会更加方便些。 + +```java +int len = 2; //因为把 beginWord 和 endWord 都加入了路径,所以初始化 2 +public int ladderLength(String beginWord, String endWord, List wordList) { + if (!wordList.contains(endWord)) { + return 0; + } + // 利用 BFS 得到所有的邻居节点 + Set set1 = new HashSet(); + set1.add(beginWord); + Set set2 = new HashSet(); + set2.add(endWord); + Set wordSet = new HashSet(wordList); + //最后没找到返回 0 + if (!bfsHelper(set1, set2, wordSet)) { + return 0; + } + return len; +} + +private boolean bfsHelper(Set set1, Set set2, Set wordSet) { + if (set1.isEmpty()) { + return false; + } + // set1 的数量多,就反向扩展 + if (set1.size() > set2.size()) { + return bfsHelper(set2, set1, wordSet); + } + // 将已经访问过单词删除 + wordSet.removeAll(set1); + wordSet.removeAll(set2); + // 保存新扩展得到的节点 + Set set = new HashSet(); + for (String str : set1) { + // 遍历每一位 + for (int i = 0; i < str.length(); i++) { + char[] chars = str.toCharArray(); + + // 尝试所有字母 + for (char ch = 'a'; ch <= 'z'; ch++) { + if (chars[i] == ch) { + continue; + } + chars[i] = ch; + String word = new String(chars); + if (set2.contains(word)) { + return true; + } + // 如果还没有相遇,并且新的单词在 word 中,那么就加到 set 中 + if (wordSet.contains(word)) { + set.add(word); + } + } + } + } + //如果当前进行了扩展,长度加 1 + if (set.size() > 0) { + len++; + } + // 一般情况下新扩展的元素会多一些,所以我们下次反方向扩展 set2 + return bfsHelper(set2, set, wordSet); + +} +``` + +当然,也可以不用递归,可以用两个队列就行了,直接把 [这里]() 的代码贴过来供参考把,思想还是不变的。 + +```C++ +public class Solution { + public int ladderLength(String beginWord, String endWord, List wordList) { + if(!wordList.contains(endWord)) return 0; + Set beginSet = new HashSet(), endSet = new HashSet(); + int len = 1; + HashSet visited = new HashSet(); + HashSet dict = new HashSet(wordList); + beginSet.add(beginWord); + endSet.add(endWord); + while (!beginSet.isEmpty() && !endSet.isEmpty()) { + if (beginSet.size() > endSet.size()) { + Set set = beginSet; + beginSet = endSet; + endSet = set; + } + + Set temp = new HashSet(); + for (String word : beginSet) { + char[] chs = word.toCharArray(); + + for (int i = 0; i < chs.length; i++) { + for (char c = 'a'; c <= 'z'; c++) { + char old = chs[i]; + chs[i] = c; + String target = String.valueOf(chs); + + if (endSet.contains(target)) { + return len + 1; + } + + if (!visited.contains(target) && dict.contains(target)) { + temp.add(target); + visited.add(target); + } + chs[i] = old; + } + } + } + beginSet = temp; + len++; + } + return 0; + } +} +``` + +# 总 + +基本上和 [126 题]() 解决思路是一样的,主要就是 `BFS` 的应用。解法二中,直接在递归中的基础上用全局变量,有时候确实很方便,哈哈,比如之前的 [124 题]()。 + diff --git a/leetCode-13-Roman-to-Integer.md b/leetCode-13-Roman-to-Integer.md index 25611ef68..86bc733e4 100644 --- a/leetCode-13-Roman-to-Integer.md +++ b/leetCode-13-Roman-to-Integer.md @@ -1,236 +1,236 @@ -# 题目描述(简单难度) - -![](http://windliang.oss-cn-beijing.aliyuncs.com/13_1.jpg) - -和上一道题相反,将罗马数字转换成阿拉伯数字。 - -# 解法一 - -先来一种不优雅的,也就是我开始的想法。就是遍历字符串,然后转换就可以,但同时得考虑 IV,IX 那些特殊情况。 - -```java -public int getInt(char r) { - int ans = 0; - switch (r) { - case 'I': - ans = 1; - break; - case 'V': - ans = 5; - break; - case 'X': - ans = 10; - break; - case 'L': - ans = 50; - break; - case 'C': - ans = 100; - break; - case 'D': - ans = 500; - break; - case 'M': - ans = 1000; - } - return ans; - } - - public int getInt(char r, char r_after) { - int ans = 0; - switch (r) { - case 'I': - ans = 1; - break; - case 'V': - ans = 5; - break; - case 'X': - ans = 10; - break; - case 'L': - ans = 50; - break; - case 'C': - ans = 100; - break; - case 'D': - ans = 500; - break; - case 'M': - ans = 1000; - break; - } - if (r == 'I') { - switch (r_after) { - case 'V': - ans = 4; - break; - case 'X': - ans = 9; - } - } - if (r == 'X') { - switch (r_after) { - case 'L': - ans = 40; - break; - case 'C': - ans = 90; - } - } - if (r == 'C') { - switch (r_after) { - case 'D': - ans = 400; - break; - case 'M': - ans = 900; - } - } - return ans; - - } - - public boolean isGetTwoInt(char r, char r_after) { - if (r == 'I') { - switch (r_after) { - case 'V': - return true; - case 'X': - return true; - } - } - if (r == 'X') { - switch (r_after) { - case 'L': - return true; - case 'C': - return true; - } - } - if (r == 'C') { - switch (r_after) { - case 'D': - return true; - case 'M': - return true; - } - } - return false; - - } - - public int romanToInt(String s) { - int ans = 0; - for (int i = 0; i < s.length() - 1; i++) { - ans += getInt(s.charAt(i), s.charAt(i + 1)); - //判断是否是两个字符的特殊情况 - if (isGetTwoInt(s.charAt(i), s.charAt(i + 1))) { - i++; - } - } - //将最后一个字符单独判断,如果放到上边的循环会越界 - if (!(s.length() >= 2 && isGetTwoInt(s.charAt(s.length() - 2), s.charAt(s.length() - 1)))) { - ans += getInt(s.charAt(s.length() - 1)); - } - - return ans; - } -``` - -时间复杂度:O(n),n 是字符串的长度。 - -空间复杂度:O(1)。 - -下边分享一些优雅的。 - -# 解法二 - -https://leetcode.com/problems/roman-to-integer/description/ - -```java -public int romanToInt(String s) { - int sum=0; - if(s.indexOf("IV")!=-1){sum-=2;} - if(s.indexOf("IX")!=-1){sum-=2;} - if(s.indexOf("XL")!=-1){sum-=20;} - if(s.indexOf("XC")!=-1){sum-=20;} - if(s.indexOf("CD")!=-1){sum-=200;} - if(s.indexOf("CM")!=-1){sum-=200;} - - char c[]=s.toCharArray(); - int count=0; - - for(;count<=s.length()-1;count++){ - if(c[count]=='M') sum+=1000; - if(c[count]=='D') sum+=500; - if(c[count]=='C') sum+=100; - if(c[count]=='L') sum+=50; - if(c[count]=='X') sum+=10; - if(c[count]=='V') sum+=5; - if(c[count]=='I') sum+=1; - - } - - return sum; - -} -``` - -把出现的特殊情况,提前减了就可以。 - -时间复杂度:O(1)。 - -空间复杂度:O(1)。 - -# 解法三 - -https://leetcode.com/problems/roman-to-integer/discuss/6509/7ms-solution-in-Java.-easy-to-understand - -利用到罗马数字的规则,一般情况是表示数字大的字母在前,数字小的字母在后,如果不是这样,就说明出现了特殊情况,此时应该做减法。 - -```java - private int getVal(char c){ - switch (c){ - case 'M': - return 1000; - case 'D': - return 500; - case 'C': - return 100; - case 'L': - return 50; - case 'X' : - return 10; - case 'V': - return 5; - case 'I': - return 1; - } - throw new IllegalArgumentException("unsupported character"); - } - - public int romanToInt(String s) { - int res = 0; - if(s.length() == 0) return res; - for (int i = 0; i < s.length() - 1; i++) { - int cur = getVal(s.charAt(i)); - int nex = getVal(s.charAt(i+1)); - if(cur < nex){ - res -= cur; - }else{ - res += cur; - } - } - return res + getVal(s.charAt(s.length()-1)); - } -``` - -时间复杂度:O(1)。 - -空间复杂度:O(1)。 - -# 总 - +# 题目描述(简单难度) + +![](http://windliang.oss-cn-beijing.aliyuncs.com/13_1.jpg) + +和上一道题相反,将罗马数字转换成阿拉伯数字。 + +# 解法一 + +先来一种不优雅的,也就是我开始的想法。就是遍历字符串,然后转换就可以,但同时得考虑 IV,IX 那些特殊情况。 + +```java +public int getInt(char r) { + int ans = 0; + switch (r) { + case 'I': + ans = 1; + break; + case 'V': + ans = 5; + break; + case 'X': + ans = 10; + break; + case 'L': + ans = 50; + break; + case 'C': + ans = 100; + break; + case 'D': + ans = 500; + break; + case 'M': + ans = 1000; + } + return ans; + } + + public int getInt(char r, char r_after) { + int ans = 0; + switch (r) { + case 'I': + ans = 1; + break; + case 'V': + ans = 5; + break; + case 'X': + ans = 10; + break; + case 'L': + ans = 50; + break; + case 'C': + ans = 100; + break; + case 'D': + ans = 500; + break; + case 'M': + ans = 1000; + break; + } + if (r == 'I') { + switch (r_after) { + case 'V': + ans = 4; + break; + case 'X': + ans = 9; + } + } + if (r == 'X') { + switch (r_after) { + case 'L': + ans = 40; + break; + case 'C': + ans = 90; + } + } + if (r == 'C') { + switch (r_after) { + case 'D': + ans = 400; + break; + case 'M': + ans = 900; + } + } + return ans; + + } + + public boolean isGetTwoInt(char r, char r_after) { + if (r == 'I') { + switch (r_after) { + case 'V': + return true; + case 'X': + return true; + } + } + if (r == 'X') { + switch (r_after) { + case 'L': + return true; + case 'C': + return true; + } + } + if (r == 'C') { + switch (r_after) { + case 'D': + return true; + case 'M': + return true; + } + } + return false; + + } + + public int romanToInt(String s) { + int ans = 0; + for (int i = 0; i < s.length() - 1; i++) { + ans += getInt(s.charAt(i), s.charAt(i + 1)); + //判断是否是两个字符的特殊情况 + if (isGetTwoInt(s.charAt(i), s.charAt(i + 1))) { + i++; + } + } + //将最后一个字符单独判断,如果放到上边的循环会越界 + if (!(s.length() >= 2 && isGetTwoInt(s.charAt(s.length() - 2), s.charAt(s.length() - 1)))) { + ans += getInt(s.charAt(s.length() - 1)); + } + + return ans; + } +``` + +时间复杂度:O(n),n 是字符串的长度。 + +空间复杂度:O(1)。 + +下边分享一些优雅的。 + +# 解法二 + +https://leetcode.com/problems/roman-to-integer/description/ + +```java +public int romanToInt(String s) { + int sum=0; + if(s.indexOf("IV")!=-1){sum-=2;} + if(s.indexOf("IX")!=-1){sum-=2;} + if(s.indexOf("XL")!=-1){sum-=20;} + if(s.indexOf("XC")!=-1){sum-=20;} + if(s.indexOf("CD")!=-1){sum-=200;} + if(s.indexOf("CM")!=-1){sum-=200;} + + char c[]=s.toCharArray(); + int count=0; + + for(;count<=s.length()-1;count++){ + if(c[count]=='M') sum+=1000; + if(c[count]=='D') sum+=500; + if(c[count]=='C') sum+=100; + if(c[count]=='L') sum+=50; + if(c[count]=='X') sum+=10; + if(c[count]=='V') sum+=5; + if(c[count]=='I') sum+=1; + + } + + return sum; + +} +``` + +把出现的特殊情况,提前减了就可以。 + +时间复杂度:O(1)。 + +空间复杂度:O(1)。 + +# 解法三 + +https://leetcode.com/problems/roman-to-integer/discuss/6509/7ms-solution-in-Java.-easy-to-understand + +利用到罗马数字的规则,一般情况是表示数字大的字母在前,数字小的字母在后,如果不是这样,就说明出现了特殊情况,此时应该做减法。 + +```java + private int getVal(char c){ + switch (c){ + case 'M': + return 1000; + case 'D': + return 500; + case 'C': + return 100; + case 'L': + return 50; + case 'X' : + return 10; + case 'V': + return 5; + case 'I': + return 1; + } + throw new IllegalArgumentException("unsupported character"); + } + + public int romanToInt(String s) { + int res = 0; + if(s.length() == 0) return res; + for (int i = 0; i < s.length() - 1; i++) { + int cur = getVal(s.charAt(i)); + int nex = getVal(s.charAt(i+1)); + if(cur < nex){ + res -= cur; + }else{ + res += cur; + } + } + return res + getVal(s.charAt(s.length()-1)); + } +``` + +时间复杂度:O(1)。 + +空间复杂度:O(1)。 + +# 总 + 这道题也不难,自己一开始没有充分利用罗马数字的特点,而是用一些 if,switch 语句判断是否是特殊情况,看起来就很繁琐了。 \ No newline at end of file diff --git a/leetCode-14-Longest-Common-Prefix.md b/leetCode-14-Longest-Common-Prefix.md index a2992e004..ec07f43d2 100644 --- a/leetCode-14-Longest-Common-Prefix.md +++ b/leetCode-14-Longest-Common-Prefix.md @@ -1,160 +1,160 @@ -# 题目描述(简单难度) - -![](http://windliang.oss-cn-beijing.aliyuncs.com/14_1.jpg) - -# 解法一 垂直比较 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/14_2.jpg) - -我们把所有字符串垂直排列,然后一列一列的比较,直到某一个字符串到达结尾或者该列字符不完全相同。 - -下边看一下我的代码,看起来比较多 - -```java -//这个函数判断 index 列的字符是否完全相同 -public boolean isSameAtIndex(String[] strs, int index) { - int i = 0; - while (i < strs.length - 1) { - if (strs[i].charAt(index) == strs[i + 1].charAt(index)) { - i++; - } else { - return false; - } - } - return true; -} - -public String longestCommonPrefix(String[] strs) { - if (strs.length == 0) - return ""; - //得到最短的字符串的长度 - int minLength = Integer.MAX_VALUE; - for (int i = 0; i < strs.length; i++) { - if (strs[i].length() < minLength) { - minLength = strs[i].length(); - } - } - int j = 0; - //遍历所有列 - for (; j < minLength; j++) { - //如果当前列字符不完全相同,则结束循环 - if (!isSameAtIndex(strs, j)) { - break; - } - } - return strs[0].substring(0, j); - -} -``` - -下边看一下,官方的代码 - -```java -public String longestCommonPrefix(String[] strs) { - if (strs == null || strs.length == 0) return ""; - //遍历所有列 - for (int i = 0; i < strs[0].length() ; i++){ - char c = strs[0].charAt(i); // 保存 i 列第 0 行的字符便于后续比较 - //比较第 1,2,3... 行的字符和第 0 行是否相等 - for (int j = 1; j < strs.length; j ++) { - /** - * i == strs[j].length() 表明当前行已经到达末尾 - * strs[j].charAt(i) != c 当前行和第 0 行字符不相等 - * 上边两种情况返回结果 - */ - if (i == strs[j].length() || strs[j].charAt(i) != c) - return strs[0].substring(0, i); - } - } - return strs[0]; -} -``` - -时间复杂度:最坏的情况就是 n 个 长度为 m 的完全一样的字符串,假设 S 是所有字符的和,那么 S = m \* n,时间复杂度就是 O(S)。当然正常情况下并不需要比较所有字符串,最多比较 n \* minLen 个字符就可以了。 - -空间复杂度:O(1),常数个额外空间。 - -# 解法二 水平比较 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/14_3.jpg) - -我们将字符串水平排列,第 0 个和第 1 个字符串找最长子串,结果为 leet,再把结果和第 2 个字符串比较,结果为 leet,再把结果和第 3 个字符串比较,结果为 lee,即为最终结果。 - -```java -public String longestCommonPrefix3(String[] strs) { - if (strs.length == 0) - return ""; - String prefix = strs[0]; // 保存结果 - // 遍历每一个字符串 - for (int i = 1; i < strs.length; i++) { - // 找到上次得到的结果 prefix 和当前字符串的最长子串 - int minLen = Math.min(prefix.length(), strs[i].length()); - int j = 0; - for (; j < minLen; j++) { - if (prefix.charAt(j) != strs[i].charAt(j)) { - break; - } - } - prefix = prefix.substring(0, j); - } - return prefix; - } -``` - -时间复杂度:最坏情况和解法一是一样,n 个长度为 m 的完全相同的字符,就要比较所有的字符 S,S = n \* m 。但对于正常情况,处于最短字符串前的字符串依旧要比较所有字符,而不是最短字符串个字符,相对于解法一较差。 - -空间复杂度:O(1)。 - -# 解法三 递归 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/14_4.jpg) - -我们把原来的数组分成两部分,求出左半部分的最长公共前缀,求出右半部分的最长公共前缀,然后求出的两个结果再求最长公共前缀,就是最后的结果了。 - -求左半部分的最长公共前缀,我们可以继续把它分成两部分,按照上边的思路接着求。然后一直分成两部分,递归下去。 - -直到该部分只有 1 个字符串,那么最长公共子串就是它本身了,直接返回就可以了。 - -```java -public String longestCommonPrefix(String[] strs) { - if (strs == null || strs.length == 0) return ""; - return longestCommonPrefix(strs, 0 , strs.length - 1); -} - -//递归不断分成两部分 -private String longestCommonPrefix(String[] strs, int l, int r) { - if (l == r) { - return strs[l]; - } - else { - int mid = (l + r)/2; - String lcpLeft = longestCommonPrefix(strs, l , mid); - String lcpRight = longestCommonPrefix(strs, mid + 1,r); - return commonPrefix(lcpLeft, lcpRight); - } -} -//求两个结果的最长公共前缀 -String commonPrefix(String left,String right) { - int min = Math.min(left.length(), right.length()); - for (int i = 0; i < min; i++) { - if ( left.charAt(i) != right.charAt(i) ) - return left.substring(0, i); - } - return left.substring(0, min); -} -``` - -时间复杂度: - -空间复杂度: - -每次遇到递归的情况,总是有些理不清楚,先空着吧。 - -# 总 - -进行了垂直比较和水平比较,又用到了递归,[solution](https://leetcode.com/problems/longest-common-prefix/solution/) 里还介绍了二分查找,感觉这里用二分查找有些太僵硬了,反而使得时间复杂度变高了。还介绍了前缀树,这里后边遇到再总结吧。 - - - - - +# 题目描述(简单难度) + +![](http://windliang.oss-cn-beijing.aliyuncs.com/14_1.jpg) + +# 解法一 垂直比较 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/14_2.jpg) + +我们把所有字符串垂直排列,然后一列一列的比较,直到某一个字符串到达结尾或者该列字符不完全相同。 + +下边看一下我的代码,看起来比较多 + +```java +//这个函数判断 index 列的字符是否完全相同 +public boolean isSameAtIndex(String[] strs, int index) { + int i = 0; + while (i < strs.length - 1) { + if (strs[i].charAt(index) == strs[i + 1].charAt(index)) { + i++; + } else { + return false; + } + } + return true; +} + +public String longestCommonPrefix(String[] strs) { + if (strs.length == 0) + return ""; + //得到最短的字符串的长度 + int minLength = Integer.MAX_VALUE; + for (int i = 0; i < strs.length; i++) { + if (strs[i].length() < minLength) { + minLength = strs[i].length(); + } + } + int j = 0; + //遍历所有列 + for (; j < minLength; j++) { + //如果当前列字符不完全相同,则结束循环 + if (!isSameAtIndex(strs, j)) { + break; + } + } + return strs[0].substring(0, j); + +} +``` + +下边看一下,官方的代码 + +```java +public String longestCommonPrefix(String[] strs) { + if (strs == null || strs.length == 0) return ""; + //遍历所有列 + for (int i = 0; i < strs[0].length() ; i++){ + char c = strs[0].charAt(i); // 保存 i 列第 0 行的字符便于后续比较 + //比较第 1,2,3... 行的字符和第 0 行是否相等 + for (int j = 1; j < strs.length; j ++) { + /** + * i == strs[j].length() 表明当前行已经到达末尾 + * strs[j].charAt(i) != c 当前行和第 0 行字符不相等 + * 上边两种情况返回结果 + */ + if (i == strs[j].length() || strs[j].charAt(i) != c) + return strs[0].substring(0, i); + } + } + return strs[0]; +} +``` + +时间复杂度:最坏的情况就是 n 个 长度为 m 的完全一样的字符串,假设 S 是所有字符的和,那么 S = m \* n,时间复杂度就是 O(S)。当然正常情况下并不需要比较所有字符串,最多比较 n \* minLen 个字符就可以了。 + +空间复杂度:O(1),常数个额外空间。 + +# 解法二 水平比较 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/14_3.jpg) + +我们将字符串水平排列,第 0 个和第 1 个字符串找最长子串,结果为 leet,再把结果和第 2 个字符串比较,结果为 leet,再把结果和第 3 个字符串比较,结果为 lee,即为最终结果。 + +```java +public String longestCommonPrefix3(String[] strs) { + if (strs.length == 0) + return ""; + String prefix = strs[0]; // 保存结果 + // 遍历每一个字符串 + for (int i = 1; i < strs.length; i++) { + // 找到上次得到的结果 prefix 和当前字符串的最长子串 + int minLen = Math.min(prefix.length(), strs[i].length()); + int j = 0; + for (; j < minLen; j++) { + if (prefix.charAt(j) != strs[i].charAt(j)) { + break; + } + } + prefix = prefix.substring(0, j); + } + return prefix; + } +``` + +时间复杂度:最坏情况和解法一是一样,n 个长度为 m 的完全相同的字符,就要比较所有的字符 S,S = n \* m 。但对于正常情况,处于最短字符串前的字符串依旧要比较所有字符,而不是最短字符串个字符,相对于解法一较差。 + +空间复杂度:O(1)。 + +# 解法三 递归 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/14_4.jpg) + +我们把原来的数组分成两部分,求出左半部分的最长公共前缀,求出右半部分的最长公共前缀,然后求出的两个结果再求最长公共前缀,就是最后的结果了。 + +求左半部分的最长公共前缀,我们可以继续把它分成两部分,按照上边的思路接着求。然后一直分成两部分,递归下去。 + +直到该部分只有 1 个字符串,那么最长公共子串就是它本身了,直接返回就可以了。 + +```java +public String longestCommonPrefix(String[] strs) { + if (strs == null || strs.length == 0) return ""; + return longestCommonPrefix(strs, 0 , strs.length - 1); +} + +//递归不断分成两部分 +private String longestCommonPrefix(String[] strs, int l, int r) { + if (l == r) { + return strs[l]; + } + else { + int mid = (l + r)/2; + String lcpLeft = longestCommonPrefix(strs, l , mid); + String lcpRight = longestCommonPrefix(strs, mid + 1,r); + return commonPrefix(lcpLeft, lcpRight); + } +} +//求两个结果的最长公共前缀 +String commonPrefix(String left,String right) { + int min = Math.min(left.length(), right.length()); + for (int i = 0; i < min; i++) { + if ( left.charAt(i) != right.charAt(i) ) + return left.substring(0, i); + } + return left.substring(0, min); +} +``` + +时间复杂度: + +空间复杂度: + +每次遇到递归的情况,总是有些理不清楚,先空着吧。 + +# 总 + +进行了垂直比较和水平比较,又用到了递归,[solution](https://leetcode.com/problems/longest-common-prefix/solution/) 里还介绍了二分查找,感觉这里用二分查找有些太僵硬了,反而使得时间复杂度变高了。还介绍了前缀树,这里后边遇到再总结吧。 + + + + + diff --git a/leetCode-15-3Sum.md b/leetCode-15-3Sum.md index e6f3e01a0..99bd6e430 100644 --- a/leetCode-15-3Sum.md +++ b/leetCode-15-3Sum.md @@ -1,109 +1,109 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/15_1.jpg) - -# 解法一 暴力解法 - -无脑搜索,三层循环,遍历所有的情况。但需要注意的是,我们需要把重复的情况去除掉,就是 [1, -1 ,0] 和 [0, -1, 1] 是属于同一种情况的。 - -```java -public List> threeSum(int[] nums) { - List> res = new ArrayList>(); - for (int i = 0; i < nums.length; i++) { - for (int j = i + 1; j < nums.length; j++) - for (int k = j + 1; k < nums.length; k++) { - if (nums[i] + nums[j] + nums[k] == 0) { - List temp = new ArrayList(); - temp.add(nums[i]); - temp.add(nums[j]); - temp.add(nums[k]); - //判断结果中是否已经有 temp 。 - if (isInList(res, temp)) { - continue; - } - res.add(temp); - } - } - } - return res; - -} - -public boolean isInList(List> l, List a) { - for (int i = 0; i < l.size(); i++) { - //判断两个 List 是否相同 - if (isSame(l.get(i), a)) { - return true; - } - } - return false; -} - -public boolean isSame(List a, List b) { - int count = 0; - Collections.sort(a); - Collections.sort(b); - //排序后判断每个元素是否对应相等 - for (int i = 0; i < a.size(); i++) { - if (a.get(i) != b.get(i)) { - return false; - } - } - return true; -} -``` - -时间复杂度:n 表示 num 的个数,三个循环 O(n³),而 isInList 也需要 O(n),总共就是 $$O(n^4)$$,leetCode 复杂度到了 $$O(n^3)$$ 一般就报超时错误了,所以算法还得优化。 - -空间复杂度:最坏情况,即 O(N), N 是指 n 个元素的排列组合个数,即 $$N=C^3_n$$,用来保存结果。 - -# 解法二 - -参考了[这里](https://leetcode.com/problems/3sum/discuss/7380/Concise-O(N2)-Java-solution) - -主要思想是,遍历数组,用 0 减去当前的数,作为 sum ,然后再找两个数使得和为 sum。 - -这样看来遍历需要 O(n),再找两个数需要 O(n²)的复杂度,还是需要 O(n³)。 - -巧妙之处在于怎么找另外两个数。 - -最最优美的地方就是,首先将给定的 num 排序。 - -这样我们就可以用两个指针,一个指向头,一个指向尾,去找这两个数字,这样的话,找另外两个数时间复杂度就会从 O(n²),降到 O(n)。 - -而怎么保证不加入重复的 list 呢? - -要记得我们的 nums 已经有序了,所以只需要找到一组之后,当前指针要移到和当前元素不同的地方。其次在遍历数组的时候,如果和上个数字相同,也要继续后移。文字表述比较困难,可以先看下代码。 - -```java -public List> threeSum(int[] num) { - Arrays.sort(num); //排序 - List> res = new LinkedList<>(); - for (int i = 0; i < num.length-2; i++) { - //为了保证不加入重复的 list,因为是有序的,所以如果和前一个元素相同,只需要继续后移就可以 - if (i == 0 || (i > 0 && num[i] != num[i-1])) { - //两个指针,并且头指针从i + 1开始,防止加入重复的元素 - int lo = i+1, hi = num.length-1, sum = 0 - num[i]; - while (lo < hi) { - if (num[lo] + num[hi] == sum) { - res.add(Arrays.asList(num[i], num[lo], num[hi])); - //元素相同要后移,防止加入重复的 list - while (lo < hi && num[lo] == num[lo+1]) lo++; - while (lo < hi && num[hi] == num[hi-1]) hi--; - lo++; hi--; - } else if (num[lo] + num[hi] < sum) lo++; - else hi--; - } - } - } - return res; -} -``` - -时间复杂度:O(n²),n 指的是 num - -空间复杂度:O(N),最坏情况,即 N 是指 n 个元素的排列组合个数,即 $$N=C^3_n$$,用来保存结果。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/15_1.jpg) + +# 解法一 暴力解法 + +无脑搜索,三层循环,遍历所有的情况。但需要注意的是,我们需要把重复的情况去除掉,就是 [1, -1 ,0] 和 [0, -1, 1] 是属于同一种情况的。 + +```java +public List> threeSum(int[] nums) { + List> res = new ArrayList>(); + for (int i = 0; i < nums.length; i++) { + for (int j = i + 1; j < nums.length; j++) + for (int k = j + 1; k < nums.length; k++) { + if (nums[i] + nums[j] + nums[k] == 0) { + List temp = new ArrayList(); + temp.add(nums[i]); + temp.add(nums[j]); + temp.add(nums[k]); + //判断结果中是否已经有 temp 。 + if (isInList(res, temp)) { + continue; + } + res.add(temp); + } + } + } + return res; + +} + +public boolean isInList(List> l, List a) { + for (int i = 0; i < l.size(); i++) { + //判断两个 List 是否相同 + if (isSame(l.get(i), a)) { + return true; + } + } + return false; +} + +public boolean isSame(List a, List b) { + int count = 0; + Collections.sort(a); + Collections.sort(b); + //排序后判断每个元素是否对应相等 + for (int i = 0; i < a.size(); i++) { + if (a.get(i) != b.get(i)) { + return false; + } + } + return true; +} +``` + +时间复杂度:n 表示 num 的个数,三个循环 O(n³),而 isInList 也需要 O(n),总共就是 $$O(n^4)$$,leetCode 复杂度到了 $$O(n^3)$$ 一般就报超时错误了,所以算法还得优化。 + +空间复杂度:最坏情况,即 O(N), N 是指 n 个元素的排列组合个数,即 $$N=C^3_n$$,用来保存结果。 + +# 解法二 + +参考了[这里](https://leetcode.com/problems/3sum/discuss/7380/Concise-O(N2)-Java-solution) + +主要思想是,遍历数组,用 0 减去当前的数,作为 sum ,然后再找两个数使得和为 sum。 + +这样看来遍历需要 O(n),再找两个数需要 O(n²)的复杂度,还是需要 O(n³)。 + +巧妙之处在于怎么找另外两个数。 + +最最优美的地方就是,首先将给定的 num 排序。 + +这样我们就可以用两个指针,一个指向头,一个指向尾,去找这两个数字,这样的话,找另外两个数时间复杂度就会从 O(n²),降到 O(n)。 + +而怎么保证不加入重复的 list 呢? + +要记得我们的 nums 已经有序了,所以只需要找到一组之后,当前指针要移到和当前元素不同的地方。其次在遍历数组的时候,如果和上个数字相同,也要继续后移。文字表述比较困难,可以先看下代码。 + +```java +public List> threeSum(int[] num) { + Arrays.sort(num); //排序 + List> res = new LinkedList<>(); + for (int i = 0; i < num.length-2; i++) { + //为了保证不加入重复的 list,因为是有序的,所以如果和前一个元素相同,只需要继续后移就可以 + if (i == 0 || (i > 0 && num[i] != num[i-1])) { + //两个指针,并且头指针从i + 1开始,防止加入重复的元素 + int lo = i+1, hi = num.length-1, sum = 0 - num[i]; + while (lo < hi) { + if (num[lo] + num[hi] == sum) { + res.add(Arrays.asList(num[i], num[lo], num[hi])); + //元素相同要后移,防止加入重复的 list + while (lo < hi && num[lo] == num[lo+1]) lo++; + while (lo < hi && num[hi] == num[hi-1]) hi--; + lo++; hi--; + } else if (num[lo] + num[hi] < sum) lo++; + else hi--; + } + } + } + return res; +} +``` + +时间复杂度:O(n²),n 指的是 num + +空间复杂度:O(N),最坏情况,即 N 是指 n 个元素的排列组合个数,即 $$N=C^3_n$$,用来保存结果。 + +# 总 + 对于遍历,这里用到了从两头同时遍历,从而降低了时间复杂度,很妙! \ No newline at end of file diff --git a/leetCode-16-3Sum-Closest.md b/leetCode-16-3Sum-Closest.md index e8dcea7d9..97181a4bd 100644 --- a/leetCode-16-3Sum-Closest.md +++ b/leetCode-16-3Sum-Closest.md @@ -1,67 +1,67 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/16_1.jpg) - -和[上一道题](https://leetcode.windliang.cc/leetCode-15-3Sum.html)很类似,只不过这个是给一个目标值,找三个数,使得他们的和最接近目标值。 - -# 解法一 暴力解法 - -遍历所有的情况,然后求出三个数的和,和目标值进行比较,选取差值最小的即可。本以为时间复杂度太大了,神奇的是,竟然 AC 了。 - -```java -public int threeSumClosest(int[] nums, int target) { - int sub = Integer.MAX_VALUE; //保存和 target 的差值 - int sum = 0; //保存当前最接近 target 的三个数的和 - for (int i = 0; i < nums.length; i++) { - for (int j = i + 1; j < nums.length; j++) - for (int k = j + 1; k < nums.length; k++) { - if (Math.abs((nums[i] + nums[j] + nums[k] - target)) < sub) { - sum = nums[i] + nums[j] + nums[k]; - sub = Math.abs(sum - target); - } - } - } - return sum; -} -``` - -时间复杂度:O(n³),三层循环。 - -空间复杂度:O(1),常数个。 - -# 解法二 - -受到[上一题](https://leetcode.windliang.cc/leetCode-15-3Sum.html)的启发,没有看的,推荐大家可以看一下。我们完全可以先将数组排序,然后先固定一个数字,然后利用头尾两个指针进行遍历,降低一个 O(n)的时间复杂度。 - -如果 sum 大于 target 就减小右指针,反之,就增加左指针。 - -```java -public int threeSumClosest(int[] nums, int target) { - Arrays.sort(nums); - int sub=Integer.MAX_VALUE; - int sum=0; - for(int i=0;itarget){ - hi--; - }else{ - lo++; - } - } - } - return sum; -} -``` - -时间复杂度:如果是快速排序的 $$O(log_n)$$ 再加上 O(n²),所以就是 O(n²)。 - -空间复杂度:O(1)。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/16_1.jpg) + +和[上一道题](https://leetcode.windliang.cc/leetCode-15-3Sum.html)很类似,只不过这个是给一个目标值,找三个数,使得他们的和最接近目标值。 + +# 解法一 暴力解法 + +遍历所有的情况,然后求出三个数的和,和目标值进行比较,选取差值最小的即可。本以为时间复杂度太大了,神奇的是,竟然 AC 了。 + +```java +public int threeSumClosest(int[] nums, int target) { + int sub = Integer.MAX_VALUE; //保存和 target 的差值 + int sum = 0; //保存当前最接近 target 的三个数的和 + for (int i = 0; i < nums.length; i++) { + for (int j = i + 1; j < nums.length; j++) + for (int k = j + 1; k < nums.length; k++) { + if (Math.abs((nums[i] + nums[j] + nums[k] - target)) < sub) { + sum = nums[i] + nums[j] + nums[k]; + sub = Math.abs(sum - target); + } + } + } + return sum; +} +``` + +时间复杂度:O(n³),三层循环。 + +空间复杂度:O(1),常数个。 + +# 解法二 + +受到[上一题](https://leetcode.windliang.cc/leetCode-15-3Sum.html)的启发,没有看的,推荐大家可以看一下。我们完全可以先将数组排序,然后先固定一个数字,然后利用头尾两个指针进行遍历,降低一个 O(n)的时间复杂度。 + +如果 sum 大于 target 就减小右指针,反之,就增加左指针。 + +```java +public int threeSumClosest(int[] nums, int target) { + Arrays.sort(nums); + int sub=Integer.MAX_VALUE; + int sum=0; + for(int i=0;itarget){ + hi--; + }else{ + lo++; + } + } + } + return sum; +} +``` + +时间复杂度:如果是快速排序的 $$O(log_n)$$ 再加上 O(n²),所以就是 O(n²)。 + +空间复杂度:O(1)。 + +# 总 + 和上一道题非常非常的相似了,先对数组排序,然后利用两头的指针,可以说是十分的优雅了。 \ No newline at end of file diff --git a/leetCode-17-Letter-Combinations-of-a-Phone-Number.md b/leetCode-17-Letter-Combinations-of-a-Phone-Number.md index be6598538..7c20b178b 100644 --- a/leetCode-17-Letter-Combinations-of-a-Phone-Number.md +++ b/leetCode-17-Letter-Combinations-of-a-Phone-Number.md @@ -1,120 +1,120 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/17.jpg) - -给一串数字,每个数可以代表数字键下的几个字母,返回这些数字下的字母的所有组成可能。 - -# 解法一 定义相乘 - -自己想了用迭代,用递归,都理不清楚,灵机一动,想出了这个算法。 - -把字符串 "23" 看成 ["a","b",c] * ["d","e","f"] ,而相乘就用两个 for 循环实现即可,看代码应该就明白了。 - -```java -public List letterCombinations(String digits) { - List ans = new ArrayList(); - for (int i = 0; i < digits.length(); i++) { - ans = mul(ans, getList(digits.charAt(i) - '0')); - } - return ans; - -} - -public List getList(int digit) { - String digitLetter[] = { "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" }; - List ans = new ArrayList(); - for (int i = 0; i < digitLetter[digit].length(); i++) { - ans.add(digitLetter[digit].charAt(i) + ""); - } - return ans; - } -//定义成两个 List 相乘 -public List mul(List l1, List l2) { - if (l1.size() != 0 && l2.size() == 0) { - return l1; - } - if (l1.size() == 0 && l2.size() != 0) { - return l2; - } - List ans = new ArrayList(); - for (int i = 0; i < l1.size(); i++) - for (int j = 0; j < l2.size(); j++) { - ans.add(l1.get(i) + l2.get(j)); - } - return ans; -} -``` - - - -# 解法二 队列迭代 - -参考[这里](https://leetcode.com/problems/letter-combinations-of-a-phone-number/discuss/8064/My-java-solution-with-FIFO-queue),果然有人用迭代写了出来。主要用到了队列。 - -```java -public List letterCombinations(String digits) { - LinkedList ans = new LinkedList(); - if(digits.isEmpty()) return ans; - String[] mapping = new String[] {"0", "1", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"}; - ans.add(""); - for(int i =0; i letterCombinations(String digits) { - if(digits.equals("")) { - return new ArrayList(); - } - List ret = new LinkedList(); - combination("", digits, 0, ret); - return ret; -} - -private void combination(String prefix, String digits, int offset, List ret) { - //offset 代表在加哪个数字 - if (offset == digits.length()) { - ret.add(prefix); - return; - } - String letters = KEYS[(digits.charAt(offset) - '0')]; - for (int i = 0; i < letters.length(); i++) { - combination(prefix + letters.charAt(i), digits, offset + 1, ret); - } -} - -``` - -![](https://windliang.oss-cn-beijing.aliyuncs.com/17_2.jpg) - -从 a 开始 ,然后递归到 d ,然后 g ,就把 adg 加入,然后再加入 adh,再加入 adi ... 从左到右,递归到底之后就将其加入。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/17.jpg) + +给一串数字,每个数可以代表数字键下的几个字母,返回这些数字下的字母的所有组成可能。 + +# 解法一 定义相乘 + +自己想了用迭代,用递归,都理不清楚,灵机一动,想出了这个算法。 + +把字符串 "23" 看成 ["a","b",c] * ["d","e","f"] ,而相乘就用两个 for 循环实现即可,看代码应该就明白了。 + +```java +public List letterCombinations(String digits) { + List ans = new ArrayList(); + for (int i = 0; i < digits.length(); i++) { + ans = mul(ans, getList(digits.charAt(i) - '0')); + } + return ans; + +} + +public List getList(int digit) { + String digitLetter[] = { "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" }; + List ans = new ArrayList(); + for (int i = 0; i < digitLetter[digit].length(); i++) { + ans.add(digitLetter[digit].charAt(i) + ""); + } + return ans; + } +//定义成两个 List 相乘 +public List mul(List l1, List l2) { + if (l1.size() != 0 && l2.size() == 0) { + return l1; + } + if (l1.size() == 0 && l2.size() != 0) { + return l2; + } + List ans = new ArrayList(); + for (int i = 0; i < l1.size(); i++) + for (int j = 0; j < l2.size(); j++) { + ans.add(l1.get(i) + l2.get(j)); + } + return ans; +} +``` + + + +# 解法二 队列迭代 + +参考[这里](https://leetcode.com/problems/letter-combinations-of-a-phone-number/discuss/8064/My-java-solution-with-FIFO-queue),果然有人用迭代写了出来。主要用到了队列。 + +```java +public List letterCombinations(String digits) { + LinkedList ans = new LinkedList(); + if(digits.isEmpty()) return ans; + String[] mapping = new String[] {"0", "1", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"}; + ans.add(""); + for(int i =0; i letterCombinations(String digits) { + if(digits.equals("")) { + return new ArrayList(); + } + List ret = new LinkedList(); + combination("", digits, 0, ret); + return ret; +} + +private void combination(String prefix, String digits, int offset, List ret) { + //offset 代表在加哪个数字 + if (offset == digits.length()) { + ret.add(prefix); + return; + } + String letters = KEYS[(digits.charAt(offset) - '0')]; + for (int i = 0; i < letters.length(); i++) { + combination(prefix + letters.charAt(i), digits, offset + 1, ret); + } +} + +``` + +![](https://windliang.oss-cn-beijing.aliyuncs.com/17_2.jpg) + +从 a 开始 ,然后递归到 d ,然后 g ,就把 adg 加入,然后再加入 adh,再加入 adi ... 从左到右,递归到底之后就将其加入。 + +# 总 + 这种题的时间复杂度和空间复杂度自己理的不太清楚就没有写了。 \ No newline at end of file diff --git a/leetCode-18-4Sum.md b/leetCode-18-4Sum.md index 37752d693..04a78868e 100644 --- a/leetCode-18-4Sum.md +++ b/leetCode-18-4Sum.md @@ -1,48 +1,48 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/18.jpg) - -和[3Sum](https://leetcode.windliang.cc/leetCode-15-3Sum.html)类似,只不过是找四个数,使得和为 target,并且不能有重复的序列。 - -如果之前没有做过[3Sum](https://leetcode.windliang.cc/leetCode-15-3Sum.html)可以先看看,自己在上边的基础上加了一个循环而已。 - -```java -public List> fourSum(int[] num, int target) { - Arrays.sort(num); - List> res = new LinkedList<>(); - //多加了层循环 - for (int j = 0; j < num.length - 3; j++) { - //防止重复的 - if (j == 0 || (j > 0 && num[j] != num[j - 1])) - for (int i = j + 1; i < num.length - 2; i++) { - //防止重复的,不再是 i == 0 ,因为 i 从 j + 1 开始 - if (i == j + 1 || num[i] != num[i - 1]) { - int lo = i + 1, hi = num.length - 1, sum = target - num[j] - num[i]; - while (lo < hi) { - if (num[lo] + num[hi] == sum) { - res.add(Arrays.asList(num[j], num[i], num[lo], num[hi])); - while (lo < hi && num[lo] == num[lo + 1]) - lo++; - while (lo < hi && num[hi] == num[hi - 1]) - hi--; - lo++; - hi--; - } else if (num[lo] + num[hi] < sum) - lo++; - else - hi--; - } - } - } - } - return res; -} -``` - -时间复杂度:O(n³)。 - -空间复杂度:O(N),最坏情况,即 N 是指 n 个元素的排列组合个数,即 $$N=C^4_n$$,用来保存结果。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/18.jpg) + +和[3Sum](https://leetcode.windliang.cc/leetCode-15-3Sum.html)类似,只不过是找四个数,使得和为 target,并且不能有重复的序列。 + +如果之前没有做过[3Sum](https://leetcode.windliang.cc/leetCode-15-3Sum.html)可以先看看,自己在上边的基础上加了一个循环而已。 + +```java +public List> fourSum(int[] num, int target) { + Arrays.sort(num); + List> res = new LinkedList<>(); + //多加了层循环 + for (int j = 0; j < num.length - 3; j++) { + //防止重复的 + if (j == 0 || (j > 0 && num[j] != num[j - 1])) + for (int i = j + 1; i < num.length - 2; i++) { + //防止重复的,不再是 i == 0 ,因为 i 从 j + 1 开始 + if (i == j + 1 || num[i] != num[i - 1]) { + int lo = i + 1, hi = num.length - 1, sum = target - num[j] - num[i]; + while (lo < hi) { + if (num[lo] + num[hi] == sum) { + res.add(Arrays.asList(num[j], num[i], num[lo], num[hi])); + while (lo < hi && num[lo] == num[lo + 1]) + lo++; + while (lo < hi && num[hi] == num[hi - 1]) + hi--; + lo++; + hi--; + } else if (num[lo] + num[hi] < sum) + lo++; + else + hi--; + } + } + } + } + return res; +} +``` + +时间复杂度:O(n³)。 + +空间复杂度:O(N),最坏情况,即 N 是指 n 个元素的排列组合个数,即 $$N=C^4_n$$,用来保存结果。 + +# 总 + 完全是按照 3Sum 的思路写的,比较好理解。 \ No newline at end of file diff --git a/leetCode-19-Remov-Nth-Node-From-End-of-List.md b/leetCode-19-Remov-Nth-Node-From-End-of-List.md index 0cbe4c00e..4fcb4efd6 100644 --- a/leetCode-19-Remov-Nth-Node-From-End-of-List.md +++ b/leetCode-19-Remov-Nth-Node-From-End-of-List.md @@ -1,148 +1,148 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/19.jpg) - -给定一个链表,将倒数第 n 个结点删除。 - -# 解法一 - -删除一个结点,无非是遍历链表找到那个结点前边的结点,然后改变下指向就好了。但由于它是链表,它的长度我们并不知道,我们得先遍历一遍得到它的长度,之后用长度减去 n 就是要删除的结点的位置,然后遍历到结点的前一个位置就好了。 - -```java -public ListNode removeNthFromEnd(ListNode head, int n) { - int len = 0; - ListNode h = head; - while (h != null) { - h = h.next; - len++; - } - //长度等于 1 ,再删除一个结点就为 null 了 - if (len == 1) { - return null; - } - - int rm_node_index = len - n; - - //如果删除的是头结点 - if (rm_node_index == 0) { - return head.next; - } - - //找到被删除结点的前一个结点 - h = head; - for (int i = 0; i < rm_node_index - 1; i++) { - h = h.next; - } - - //改变指向 - h.next = h.next.next; - return head; -} -``` - -时间复杂度:假设链表长度是 L ,那么就第一个循环是 L 次,第二个循环是 L - n 次,总共 2L - n 次,所以时间复杂度就是 O(L)。 - -空间复杂度:O(1)。 - -我们看到如果长度等于 1 和删除头结点的时候需要单独判断,其实我们只需要在 head 前边加一个空节点,就可以避免单独判断。 - -```java -public ListNode removeNthFromEnd(ListNode head, int n) { - ListNode dummy = new ListNode(0); - dummy.next = head; - int length = 0; - ListNode first = head; - while (first != null) { - length++; - first = first.next; - } - length -= n; - first = dummy; - while (length > 0) { - length--; - first = first.next; - } - first.next = first.next.next; - return dummy.next; -} -``` - -# 解法二 遍历一次链表 - -上边我们遍历链表进行了两次,我们如何只遍历一次呢。 - -看了 [leetcode](https://leetcode.com/problems/remove-nth-node-from-end-of-list/solution/) 的讲解。 - -想象一下,两个人进行 100m 赛跑,假设他们的速度相同。开始的时候,第一个人就在第二个人前边 10m ,这样当第一个人跑到终点的时候,第二个人相距第一个人依旧是 10m ,也就是离终点 10m。 - -对比于链表,我们设定两个指针,先让第一个指针遍历 n 步,然后再让它俩同时开始遍历,这样的话,当第一个指针到头的时候,第二个指针就离第一个指针有 n 的距离,所以第二个指针的位置就刚好是倒数第 n 个结点。 - -```java -public ListNode removeNthFromEnd(ListNode head, int n) { - ListNode dummy = new ListNode(0); - dummy.next = head; - ListNode first = dummy; - ListNode second = dummy; - //第一个指针先移动 n 步 - for (int i = 1; i <= n + 1; i++) { - first = first.next; - } - //第一个指针到达终点停止遍历 - while (first != null) { - first = first.next; - second = second.next; - } - second.next = second.next.next; - return dummy.next; -} -``` - -时间复杂度: - -第一个指针从 0 到 n ,然后「第一个指针再从 n 到结束」和「第二个指针从 0 到倒数第 n 个结点的位置」同时进行。 - -而解法一无非是先从 0 到 结束,然后从 0 到倒数第 n 个结点的位置。 - -所以其实它们语句执行的次数其实是一样的,从 0 到倒数第 n 个结点的位置都被遍历了 2 次,所以总共也是 2L - n 次。只不过这个解法把解法一的两次循环合并了一下,使得第二个指针看起来是顺便遍历,想法很 nice。 - -所以本质上,它们其实是一样的,时间复杂度依旧是 O(n)。 - -空间复杂度:O(1)。 - -# 解法三 - -没看讲解前,和室友讨论下,如何只遍历一次链表。室友给出了一个我竟然无法反驳的观点,哈哈哈哈。 - -第一次遍历链表确定长度的时候,顺便把每个结点存到数组里,这样找结点的时候就不需要再遍历一次了,空间换时间???哈哈哈哈哈哈哈哈哈。 - -```java -public ListNode removeNthFromEnd(ListNode head, int n) { - List l = new ArrayList(); - ListNode h = head; - int len = 0; - while (h != null) { - l.add(h); - h = h.next; - len++; - } - if (len == 1) { - return null; - } - int remove = len - n; - if (remove == 0) { - return head.next; - } - //直接得到,不需要再遍历了 - ListNode r = l.get(remove - 1); - r.next = r.next.next; - return head; -} -``` - -时间复杂度:O(L)。 - -空间复杂度:O(L)。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/19.jpg) + +给定一个链表,将倒数第 n 个结点删除。 + +# 解法一 + +删除一个结点,无非是遍历链表找到那个结点前边的结点,然后改变下指向就好了。但由于它是链表,它的长度我们并不知道,我们得先遍历一遍得到它的长度,之后用长度减去 n 就是要删除的结点的位置,然后遍历到结点的前一个位置就好了。 + +```java +public ListNode removeNthFromEnd(ListNode head, int n) { + int len = 0; + ListNode h = head; + while (h != null) { + h = h.next; + len++; + } + //长度等于 1 ,再删除一个结点就为 null 了 + if (len == 1) { + return null; + } + + int rm_node_index = len - n; + + //如果删除的是头结点 + if (rm_node_index == 0) { + return head.next; + } + + //找到被删除结点的前一个结点 + h = head; + for (int i = 0; i < rm_node_index - 1; i++) { + h = h.next; + } + + //改变指向 + h.next = h.next.next; + return head; +} +``` + +时间复杂度:假设链表长度是 L ,那么就第一个循环是 L 次,第二个循环是 L - n 次,总共 2L - n 次,所以时间复杂度就是 O(L)。 + +空间复杂度:O(1)。 + +我们看到如果长度等于 1 和删除头结点的时候需要单独判断,其实我们只需要在 head 前边加一个空节点,就可以避免单独判断。 + +```java +public ListNode removeNthFromEnd(ListNode head, int n) { + ListNode dummy = new ListNode(0); + dummy.next = head; + int length = 0; + ListNode first = head; + while (first != null) { + length++; + first = first.next; + } + length -= n; + first = dummy; + while (length > 0) { + length--; + first = first.next; + } + first.next = first.next.next; + return dummy.next; +} +``` + +# 解法二 遍历一次链表 + +上边我们遍历链表进行了两次,我们如何只遍历一次呢。 + +看了 [leetcode](https://leetcode.com/problems/remove-nth-node-from-end-of-list/solution/) 的讲解。 + +想象一下,两个人进行 100m 赛跑,假设他们的速度相同。开始的时候,第一个人就在第二个人前边 10m ,这样当第一个人跑到终点的时候,第二个人相距第一个人依旧是 10m ,也就是离终点 10m。 + +对比于链表,我们设定两个指针,先让第一个指针遍历 n 步,然后再让它俩同时开始遍历,这样的话,当第一个指针到头的时候,第二个指针就离第一个指针有 n 的距离,所以第二个指针的位置就刚好是倒数第 n 个结点。 + +```java +public ListNode removeNthFromEnd(ListNode head, int n) { + ListNode dummy = new ListNode(0); + dummy.next = head; + ListNode first = dummy; + ListNode second = dummy; + //第一个指针先移动 n 步 + for (int i = 1; i <= n + 1; i++) { + first = first.next; + } + //第一个指针到达终点停止遍历 + while (first != null) { + first = first.next; + second = second.next; + } + second.next = second.next.next; + return dummy.next; +} +``` + +时间复杂度: + +第一个指针从 0 到 n ,然后「第一个指针再从 n 到结束」和「第二个指针从 0 到倒数第 n 个结点的位置」同时进行。 + +而解法一无非是先从 0 到 结束,然后从 0 到倒数第 n 个结点的位置。 + +所以其实它们语句执行的次数其实是一样的,从 0 到倒数第 n 个结点的位置都被遍历了 2 次,所以总共也是 2L - n 次。只不过这个解法把解法一的两次循环合并了一下,使得第二个指针看起来是顺便遍历,想法很 nice。 + +所以本质上,它们其实是一样的,时间复杂度依旧是 O(n)。 + +空间复杂度:O(1)。 + +# 解法三 + +没看讲解前,和室友讨论下,如何只遍历一次链表。室友给出了一个我竟然无法反驳的观点,哈哈哈哈。 + +第一次遍历链表确定长度的时候,顺便把每个结点存到数组里,这样找结点的时候就不需要再遍历一次了,空间换时间???哈哈哈哈哈哈哈哈哈。 + +```java +public ListNode removeNthFromEnd(ListNode head, int n) { + List l = new ArrayList(); + ListNode h = head; + int len = 0; + while (h != null) { + l.add(h); + h = h.next; + len++; + } + if (len == 1) { + return null; + } + int remove = len - n; + if (remove == 0) { + return head.next; + } + //直接得到,不需要再遍历了 + ListNode r = l.get(remove - 1); + r.next = r.next.next; + return head; +} +``` + +时间复杂度:O(L)。 + +空间复杂度:O(L)。 + +# 总 + 利用两个指针先固定间隔,然后同时遍历,真的是很妙!另外室友的想法也很棒,哈哈哈哈哈。 \ No newline at end of file diff --git a/leetCode-2-Add-Two-Numbers.md b/leetCode-2-Add-Two-Numbers.md index 930f64e8c..95f688246 100644 --- a/leetCode-2-Add-Two-Numbers.md +++ b/leetCode-2-Add-Two-Numbers.md @@ -1,184 +1,184 @@ -## 题目描述(中等难度) - -![](http://windliang.oss-cn-beijing.aliyuncs.com/TIM%E6%88%AA%E5%9B%BE20180714105005.jpg) - -就是两个链表表示的数相加,这样就可以实现两个很大的数相加了,无需考虑数值 int ,float 的限制了。 - - - -由于自己实现的很乱,直接按答案的讲解了。 - -## 图示 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/2_pic.jpg) - -链表最左边表示个位数,代表 342 + 465 =807 。 - -## 思路 - -首先每一位相加肯定会产生进位,我们用 carry 表示。进位最大会是 1 ,因为最大的情况是无非是 9 + 9 + 1 = 19 ,也就是两个最大的数相加,再加进位,这样最大是 19 ,不会产生进位 2 。下边是伪代码。 - -* 初始化一个节点的头,dummy head ,但是这个头不存储数字。并且将 curr 指向它。 -* 初始化进位 carry 为 0 。 -* 初始化 p 和 q 分别为给定的两个链表 l1 和 l2 的头,也就是个位。 -* 循环,直到 l1 和 l2 全部到达 null 。 - * 设置 x 为 p 节点的值,如果 p 已经到达了 null,设置 x 为 0 。 - * 设置 y 为 q 节点的值,如果 q 已经到达了 null,设置 y 为 0 。 - * 设置 sum = x + y + carry 。 - * 更新 carry = sum / 10 。 - * 创建一个值为 sum mod 10 的节点,并将 curr 的 next 指向它,同时 curr 指向变为当前的新节点。 - * 向前移动 p 和 q 。 -* 判断 carry 是否等于 1 ,如果等于 1 ,在链表末尾增加一个为 1 的节点。 -* 返回 dummy head 的 next ,也就是个位数开始的地方。 - -初始化的节点 dummy head 没有存储值,最后返回 dummy head 的 next 。这样的好处是不用单独对 head 进行判断改变值。也就是如果一开始的 head 就是代表个位数,那么开始初始化的时候并不知道它的值是多少,所以还需要在进入循环前单独对它进行值的更正,不能像现在一样只用一个循环简洁。 - -## 代码 - -``` JAVA -class ListNode { - int val; - ListNode next; - ListNode(int x) { val = x; } -} -public ListNode addTwoNumbers(ListNode l1, ListNode l2) { - ListNode dummyHead = new ListNode(0); - ListNode p = l1, q = l2, curr = dummyHead; - int carry = 0; - while (p != null || q != null) { - int x = (p != null) ? p.val : 0; - int y = (q != null) ? q.val : 0; - int sum = carry + x + y; - carry = sum / 10; - curr.next = new ListNode(sum % 10); - curr = curr.next; - if (p != null) p = p.next; - if (q != null) q = q.next; - } - if (carry > 0) { - curr.next = new ListNode(carry); - } - return dummyHead.next; -} -``` - -时间复杂度:O(max(m,n)),m 和 n 代表 l1 和 l2 的长度。 - -空间复杂度:O(max(m,n)),m 和 n 代表 l1 和 l2 的长度。而其实新的 List 最大长度是 O(max(m,n))+ 1,因为我们的 head 没有存储值。 - -## 扩展 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/2_add.jpg) - -如果链表存储的顺序反过来怎么办? - -我首先想到的是链表先逆序计算,然后将结果再逆序呗,这就转换到我们之前的情况了。不知道还有没有其他的解法。下边分析下单链表逆序的思路。 - -## 迭代思想 - -首先看一下原链表。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/l0.jpg) - -总共需要添加两个指针,pre 和 next。 - -初始化 pre 指向 NULL 。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/l00.jpg) - -然后就是迭代的步骤,总共四步,顺序一步都不能错。 - -* next 指向 head 的 next ,防止原链表丢失 - - ![](http://windliang.oss-cn-beijing.aliyuncs.com/l1.jpg) - -* head 的 next 从原来链表脱离,指向 pre 。 - - ![](http://windliang.oss-cn-beijing.aliyuncs.com/l2.jpg) - -* pre 指向 head - - ![](http://windliang.oss-cn-beijing.aliyuncs.com/l3.jpg) - -* head 指向 next - - ![](http://windliang.oss-cn-beijing.aliyuncs.com/l4.jpg) - -一次迭代就完成了,如果再进行一次迭代就变成下边的样子。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/l5.jpg) - -可以看到整个过程无非是把旧链表的 head 取下来,添加的新的链表上。代码怎么写呢? - -```java -next = head -> next; //保存 head 的 next , 以防取下 head 后丢失 -head -> next = pre; //将 head 从原链表取下来,添加到新链表上 -pre = head;// pre 右移 -head = next; // head 右移 -``` - -接下来就是停止条件了,我们再进行一次循环。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/l6.jpg) - -可以发现当 head 或者 next 指向 null 的时候,我们就可以停止了。此时将 pre 返回,便是逆序了的链表了。 - -## 迭代代码 - -```JAVA -public ListNode reverseList(ListNode head){ - if(head==null) return null; - ListNode pre=null; - ListNode next; - while(head!=null){ - next=head.next; - head.next=pre; - pre=head; - head=next; - } - return pre; - } -``` - -## 递归思想 - -* 首先假设我们实现了将单链表逆序的函数,ListNode reverseListRecursion(ListNode head) ,传入链表头,返回逆序后的链表头。 - -* 接着我们确定如何把问题一步一步的化小,我们可以这样想。 - - 把 head 结点拿出来,剩下的部分我们调用函数 reverseListRecursion ,这样剩下的部分就逆序了,接着我们把 head 结点放到新链表的尾部就可以了。这就是整个递归的思想了。 - - ​ - - ![](http://windliang.oss-cn-beijing.aliyuncs.com/ll0.jpg) - - * head 结点拿出来 - - ![](http://windliang.oss-cn-beijing.aliyuncs.com/ll1.jpg) - - * 剩余部分调用逆序函数 reverseListRecursion ,并得到了 newhead - - ![](http://windliang.oss-cn-beijing.aliyuncs.com/ll2.jpg) - - * 将 2 指向 1 ,1 指向 null,将 newhead 返回即可。 - - ![](http://windliang.oss-cn-beijing.aliyuncs.com/ll3.jpg) - -* 找到递归出口 - - 当然就是如果结点的个数是一个,那么逆序的话还是它本身,直接 return 就够了。怎么判断结点个数是不是一个呢?它的 next 等于 null 就说明是一个了。但如果传进来的本身就是 null,那么直接找它的 next 会报错,所以先判断传进来的是不是 null ,如果是,也是直接返回就可以了。 -## 代码 - -``` JAVA -public ListNode reverseListRecursion(ListNode head){ - ListNode newHead; - if(head==null||head.next==null ){ - return head; - } - newHead=reverseListRecursion(head.next); //head.next 作为剩余部分的头指针 - head.next.next=head; //head.next 代表新链表的尾,将它的 next 置为 head,就是将 head 加到最后了。 - head.next=null; - return newHead; - } -``` - +## 题目描述(中等难度) + +![](http://windliang.oss-cn-beijing.aliyuncs.com/TIM%E6%88%AA%E5%9B%BE20180714105005.jpg) + +就是两个链表表示的数相加,这样就可以实现两个很大的数相加了,无需考虑数值 int ,float 的限制了。 + + + +由于自己实现的很乱,直接按答案的讲解了。 + +## 图示 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/2_pic.jpg) + +链表最左边表示个位数,代表 342 + 465 =807 。 + +## 思路 + +首先每一位相加肯定会产生进位,我们用 carry 表示。进位最大会是 1 ,因为最大的情况是无非是 9 + 9 + 1 = 19 ,也就是两个最大的数相加,再加进位,这样最大是 19 ,不会产生进位 2 。下边是伪代码。 + +* 初始化一个节点的头,dummy head ,但是这个头不存储数字。并且将 curr 指向它。 +* 初始化进位 carry 为 0 。 +* 初始化 p 和 q 分别为给定的两个链表 l1 和 l2 的头,也就是个位。 +* 循环,直到 l1 和 l2 全部到达 null 。 + * 设置 x 为 p 节点的值,如果 p 已经到达了 null,设置 x 为 0 。 + * 设置 y 为 q 节点的值,如果 q 已经到达了 null,设置 y 为 0 。 + * 设置 sum = x + y + carry 。 + * 更新 carry = sum / 10 。 + * 创建一个值为 sum mod 10 的节点,并将 curr 的 next 指向它,同时 curr 指向变为当前的新节点。 + * 向前移动 p 和 q 。 +* 判断 carry 是否等于 1 ,如果等于 1 ,在链表末尾增加一个为 1 的节点。 +* 返回 dummy head 的 next ,也就是个位数开始的地方。 + +初始化的节点 dummy head 没有存储值,最后返回 dummy head 的 next 。这样的好处是不用单独对 head 进行判断改变值。也就是如果一开始的 head 就是代表个位数,那么开始初始化的时候并不知道它的值是多少,所以还需要在进入循环前单独对它进行值的更正,不能像现在一样只用一个循环简洁。 + +## 代码 + +``` JAVA +class ListNode { + int val; + ListNode next; + ListNode(int x) { val = x; } +} +public ListNode addTwoNumbers(ListNode l1, ListNode l2) { + ListNode dummyHead = new ListNode(0); + ListNode p = l1, q = l2, curr = dummyHead; + int carry = 0; + while (p != null || q != null) { + int x = (p != null) ? p.val : 0; + int y = (q != null) ? q.val : 0; + int sum = carry + x + y; + carry = sum / 10; + curr.next = new ListNode(sum % 10); + curr = curr.next; + if (p != null) p = p.next; + if (q != null) q = q.next; + } + if (carry > 0) { + curr.next = new ListNode(carry); + } + return dummyHead.next; +} +``` + +时间复杂度:O(max(m,n)),m 和 n 代表 l1 和 l2 的长度。 + +空间复杂度:O(max(m,n)),m 和 n 代表 l1 和 l2 的长度。而其实新的 List 最大长度是 O(max(m,n))+ 1,因为我们的 head 没有存储值。 + +## 扩展 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/2_add.jpg) + +如果链表存储的顺序反过来怎么办? + +我首先想到的是链表先逆序计算,然后将结果再逆序呗,这就转换到我们之前的情况了。不知道还有没有其他的解法。下边分析下单链表逆序的思路。 + +## 迭代思想 + +首先看一下原链表。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/l0.jpg) + +总共需要添加两个指针,pre 和 next。 + +初始化 pre 指向 NULL 。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/l00.jpg) + +然后就是迭代的步骤,总共四步,顺序一步都不能错。 + +* next 指向 head 的 next ,防止原链表丢失 + + ![](http://windliang.oss-cn-beijing.aliyuncs.com/l1.jpg) + +* head 的 next 从原来链表脱离,指向 pre 。 + + ![](http://windliang.oss-cn-beijing.aliyuncs.com/l2.jpg) + +* pre 指向 head + + ![](http://windliang.oss-cn-beijing.aliyuncs.com/l3.jpg) + +* head 指向 next + + ![](http://windliang.oss-cn-beijing.aliyuncs.com/l4.jpg) + +一次迭代就完成了,如果再进行一次迭代就变成下边的样子。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/l5.jpg) + +可以看到整个过程无非是把旧链表的 head 取下来,添加的新的链表上。代码怎么写呢? + +```java +next = head -> next; //保存 head 的 next , 以防取下 head 后丢失 +head -> next = pre; //将 head 从原链表取下来,添加到新链表上 +pre = head;// pre 右移 +head = next; // head 右移 +``` + +接下来就是停止条件了,我们再进行一次循环。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/l6.jpg) + +可以发现当 head 或者 next 指向 null 的时候,我们就可以停止了。此时将 pre 返回,便是逆序了的链表了。 + +## 迭代代码 + +```JAVA +public ListNode reverseList(ListNode head){ + if(head==null) return null; + ListNode pre=null; + ListNode next; + while(head!=null){ + next=head.next; + head.next=pre; + pre=head; + head=next; + } + return pre; + } +``` + +## 递归思想 + +* 首先假设我们实现了将单链表逆序的函数,ListNode reverseListRecursion(ListNode head) ,传入链表头,返回逆序后的链表头。 + +* 接着我们确定如何把问题一步一步的化小,我们可以这样想。 + + 把 head 结点拿出来,剩下的部分我们调用函数 reverseListRecursion ,这样剩下的部分就逆序了,接着我们把 head 结点放到新链表的尾部就可以了。这就是整个递归的思想了。 + + ​ + + ![](http://windliang.oss-cn-beijing.aliyuncs.com/ll0.jpg) + + * head 结点拿出来 + + ![](http://windliang.oss-cn-beijing.aliyuncs.com/ll1.jpg) + + * 剩余部分调用逆序函数 reverseListRecursion ,并得到了 newhead + + ![](http://windliang.oss-cn-beijing.aliyuncs.com/ll2.jpg) + + * 将 2 指向 1 ,1 指向 null,将 newhead 返回即可。 + + ![](http://windliang.oss-cn-beijing.aliyuncs.com/ll3.jpg) + +* 找到递归出口 + + 当然就是如果结点的个数是一个,那么逆序的话还是它本身,直接 return 就够了。怎么判断结点个数是不是一个呢?它的 next 等于 null 就说明是一个了。但如果传进来的本身就是 null,那么直接找它的 next 会报错,所以先判断传进来的是不是 null ,如果是,也是直接返回就可以了。 +## 代码 + +``` JAVA +public ListNode reverseListRecursion(ListNode head){ + ListNode newHead; + if(head==null||head.next==null ){ + return head; + } + newHead=reverseListRecursion(head.next); //head.next 作为剩余部分的头指针 + head.next.next=head; //head.next 代表新链表的尾,将它的 next 置为 head,就是将 head 加到最后了。 + head.next=null; + return newHead; + } +``` + diff --git a/leetCode-20-Valid Parentheses.md b/leetCode-20-Valid Parentheses.md index 38fc3bb17..28f9b665d 100644 --- a/leetCode-20-Valid Parentheses.md +++ b/leetCode-20-Valid Parentheses.md @@ -1,70 +1,70 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/20.png) - -括号匹配问题。 - -如果只有一种括号,我们完全可以用一个计数器 count ,遍历整个字符串,遇到左括号加 1 ,遇到右括号减 1,遍历结束后,如果 count 等于 0 ,则表示全部匹配。但如果有多种括号,像 ( [ ) ] 这种情况它依旧会得到 0,所以我们需要用其他的方法。 - -栈! - -遍历整个字符串,遇到左括号就入栈,然后遇到和栈顶对应的右括号就出栈,遍历结束后,如果栈为空,就表示全部匹配。 - -```java -public boolean isValid(String s) { - Stack brackets = new Stack(); - for(int i = 0;i < s.length();i++){ - char c = s.charAt(i); - switch(c){ - case '(': - case '[': - case '{': - brackets.push(c); - break; - case ')': - if(!brackets.empty()){ - if(brackets.peek()== '('){ - brackets.pop(); - }else{ - return false; - } - }else{ - return false; - } - break; - case ']': - if(!brackets.empty()){ - if(brackets.peek()=='['){ - brackets.pop(); - }else{ - return false; - } - }else{ - return false; - } - break; - case '}': - if(!brackets.empty()){ - if(brackets.peek()=='{'){ - brackets.pop(); - }else{ - return false; - } - }else{ - return false; - } - - } - } - - return brackets.empty(); -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(n)。 - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/20.png) + +括号匹配问题。 + +如果只有一种括号,我们完全可以用一个计数器 count ,遍历整个字符串,遇到左括号加 1 ,遇到右括号减 1,遍历结束后,如果 count 等于 0 ,则表示全部匹配。但如果有多种括号,像 ( [ ) ] 这种情况它依旧会得到 0,所以我们需要用其他的方法。 + +栈! + +遍历整个字符串,遇到左括号就入栈,然后遇到和栈顶对应的右括号就出栈,遍历结束后,如果栈为空,就表示全部匹配。 + +```java +public boolean isValid(String s) { + Stack brackets = new Stack(); + for(int i = 0;i < s.length();i++){ + char c = s.charAt(i); + switch(c){ + case '(': + case '[': + case '{': + brackets.push(c); + break; + case ')': + if(!brackets.empty()){ + if(brackets.peek()== '('){ + brackets.pop(); + }else{ + return false; + } + }else{ + return false; + } + break; + case ']': + if(!brackets.empty()){ + if(brackets.peek()=='['){ + brackets.pop(); + }else{ + return false; + } + }else{ + return false; + } + break; + case '}': + if(!brackets.empty()){ + if(brackets.peek()=='{'){ + brackets.pop(); + }else{ + return false; + } + }else{ + return false; + } + + } + } + + return brackets.empty(); +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(n)。 + +# 总 + 如果学过数据结构,一定写过计算器,括号匹配问题一定遇到过的。 \ No newline at end of file diff --git a/leetCode-21-Merge-Two-Sorted-Lists.md b/leetCode-21-Merge-Two-Sorted-Lists.md index e5ccd30a6..0c932c2c0 100644 --- a/leetCode-21-Merge-Two-Sorted-Lists.md +++ b/leetCode-21-Merge-Two-Sorted-Lists.md @@ -1,65 +1,65 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/21.jpg) - -合并两个有序链表。 - -# 解法一 迭代 - -遍历两个链表。 - -```java -public ListNode mergeTwoLists(ListNode l1, ListNode l2) { - ListNode h = new ListNode(0); - ListNode ans=h; - while (l1 != null && l2 != null) { - if (l1.val < l2.val) { - h.next = l1; - h = h.next; - l1 = l1.next; - } else { - h.next = l2; - h = h.next; - l2 = l2.next; - } - } - if(l1==null){ - h.next=l2; - } - if(l2==null){ - h.next=l1; - } - return ans.next; -} -``` - -时间复杂度:O(m + n)。 - -空间复杂度:O(1)。 - -# 解法二 递归 - -参考[这里](Merge Two Sorted Lists) - -```java -ListNode mergeTwoLists(ListNode l1, ListNode l2) { - if(l1 == null) return l2; - if(l2 == null) return l1; - - if(l1.val < l2.val) { - l1.next = mergeTwoLists(l1.next, l2); - return l1; - } else { - l2.next = mergeTwoLists(l2.next, l1); - return l2; - } -} -``` - -时间复杂度: - -空间复杂度: - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/21.jpg) + +合并两个有序链表。 + +# 解法一 迭代 + +遍历两个链表。 + +```java +public ListNode mergeTwoLists(ListNode l1, ListNode l2) { + ListNode h = new ListNode(0); + ListNode ans=h; + while (l1 != null && l2 != null) { + if (l1.val < l2.val) { + h.next = l1; + h = h.next; + l1 = l1.next; + } else { + h.next = l2; + h = h.next; + l2 = l2.next; + } + } + if(l1==null){ + h.next=l2; + } + if(l2==null){ + h.next=l1; + } + return ans.next; +} +``` + +时间复杂度:O(m + n)。 + +空间复杂度:O(1)。 + +# 解法二 递归 + +参考[这里](Merge Two Sorted Lists) + +```java +ListNode mergeTwoLists(ListNode l1, ListNode l2) { + if(l1 == null) return l2; + if(l2 == null) return l1; + + if(l1.val < l2.val) { + l1.next = mergeTwoLists(l1.next, l2); + return l1; + } else { + l2.next = mergeTwoLists(l2.next, l1); + return l2; + } +} +``` + +时间复杂度: + +空间复杂度: + +# 总 + 递归看起来,两个字,优雅!但是关于递归的时间复杂度,空间复杂度的求法,先留个坑吧。 \ No newline at end of file diff --git a/leetCode-22-Generate-Parentheses.md b/leetCode-22-Generate-Parentheses.md index 710491e6d..64cd5a0c2 100644 --- a/leetCode-22-Generate-Parentheses.md +++ b/leetCode-22-Generate-Parentheses.md @@ -1,190 +1,190 @@ -## 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/22.jpg) - -给一个数字 n ,返回所有合法的括号匹配,刚好和[20题](https://leetcode.wang/leetCode-20-Valid%20Parentheses.html)相反。 - -自己没想出来,全部参考 LeetCode 给出的 [Solution](https://leetcode.com/problems/generate-parentheses/solution/)。 - -# 解法一 暴力破解 - -列举所有的情况,每一位有左括号和右括号两种情况,总共 2n 位,所以总共 $$2^{2n}$$ 种情况。 - -```java -public List generateParenthesis(int n) { - List combinations = new ArrayList(); - generateAll(new char[2 * n], 0, combinations); - return combinations; -} - -public void generateAll(char[] current, int pos, List result) { - if (pos == current.length) { - if (valid(current)) - result.add(new String(current)); - } else { - current[pos] = '('; - generateAll(current, pos+1, result); - current[pos] = ')'; - generateAll(current, pos+1, result); - } -} - -public boolean valid(char[] current) { - int balance = 0; - for (char c: current) { - if (c == '(') balance++; - else balance--; - if (balance < 0) return false; - } - return (balance == 0); -} -``` - -时间复杂度:对每种情况判断是否合法需要 O(n),所以时间复杂度是 $$O(2^{2n}n)$$ 。 - -空间复杂度:$$O(2^{2n}n)$$,乘以 n 是因为每个串的长度是 2n。此外这是假设所有情况都符合的时候,但其实不可能都符合,后边会给出更精确的情况。 - -# 解法二 - -解法一中,我们不停的加左括号,其实如果左括号超过 n 的时候,它肯定不是合法序列了。因为合法序列一定是 n 个左括号和 n 个右括号。 - -还有一种情况就是如果添加括号的过程中,如果右括号的总数量大于左括号的总数量了,后边不论再添加什么,它都不可能是合法序列了。因为每个右括号必须和之前的某个左括号匹配,如果右括号数量多于左括号,那么一定有一个右括号没有与之匹配的左括号,后边不论加多少左括号都没有用了。例如 n = 3 ,总共会有 6 个括号,我们加到 ( ) ) 3 个括号的情况的时候,有 1 个左括号,2 个右括号,此时后边 3 个括号无论是什么,已经注定它不会是合法序列了。 - -基于上边的两点,我们只要避免它们,就可以保证我们生成的括号一定是合法的了。 - -```java -public List generateParenthesis(int n) { - List ans = new ArrayList(); - backtrack(ans, "", 0, 0, n); - return ans; -} - -public void backtrack(List ans, String cur, int left, int right, int n){ - if (cur.length() == n * 2) { - ans.add(cur); - return; - } - //左括号不要超过 n - if (left < n) - backtrack(ans, cur+"(", left+1, right, n); - //右括号不要超过左括号 - if (right < left) - backtrack(ans, cur+")", left, right+1, n); -} -``` - -时间复杂度: - -空间复杂度: - -递归的复杂度分析,继续留坑 =.=。 - -# 解法三 - -解法二中是用列举的方法,仔细想想,我们每次用递归的时候,都是把大问题换成小问题然后去解决,这道题有没有这个思路呢? - -我们想一下之前的列举过程,第 0 个位置一定会是左括号,然后接着添加左括号或右括号,过程中左括号数一定大于或等于右括号数,当第一次出现左括号数等于右括号数的时候,假如此时的位置是 c 。那么位置 1 到 c - 1 之间一定是合法序列,此外 c + 1 到最后的 2n -1 也是合法序列。而假设总共是 n 组括号,1 到 c - 1 是 a 组括号, c + 1 到 2n - 1 之间则是 n - 1 - a 组括号,如下图 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/22_2.jpg) - - - -最重要的是,每一个合法序列都会有这么一个数 c ,且唯一。所以我们如果要求 n 组括号的所有序列,只需要知道 a 组括号以及 ( n - a - 1) 组括号的所有序列,然后两两组合即可。 - -以 n = 3 为例,我们把 0 到 c 之间的括号数记为 a 组, c + 1 到最后的括号数记为 b 组,则 - -a = 0,b = 2 对应 ()(())以及 ()()() 两种情况,此时 c = 1。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/22_3.jpg) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/22_4.jpg) - -a = 1,b = 1,对应 (())(()) 一种情况,此时 c = 3。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/22_5.jpg) - -a = 2,b = 0 对应 ((())), (()()) 两种情况,此时 c = 5。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/22_6.jpg) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/22_7.jpg) - -所以我们如果要想求 n 组括号,只需要知道 a 组和 b 组的情况,然后组合起来就可以了。 - -看起来我们在迭代 a ,其实本质上是在迭代 c ,c = 2a + 1,迭代 a 从 0 到 n - 1 ,就是迭代 c 从 1 到 2n - 1。看起来 c 都是奇数,其实是可以理解的,因为 0 到 c 间都是一组组的括号, 所以 c 一定是奇数。为什么可以迭代 c ,因为上边说到每一个合法序列都对应着一个 c ,遍历 c 的话,就能得到所有的情况了,看一下代码吧。 - -```java -public List generateParenthesis(int n) { - List ans = new ArrayList(); - if (n == 0) { - ans.add(""); - } else { - for (int a = 0; a < n; a++) - for (String left: generateParenthesis(a)) - for (String right: generateParenthesis(n-1-a)) - ans.add("(" + left + ")" + right); - } - return ans; -} -``` - -时间复杂度: - -空间复杂度: - -留坑。 - -# 扩展 卡塔兰数 - -如果这道题不是让你列举所有的情况, 而是仅仅让你输出 n 对应下有多少种合法序列,该怎么做呢? - -答案就是 $$\frac{1}{n+1}C^n_{2n}$$,也可以写成$$\frac{1}{n+1}\binom{2n}{n}$$。怎么证明呢?我主要参考了[这里](http://lanqi.org/interests/10939/),说一下。 - -我们假设不考虑是不是合法序列,那么就一共有$$C^n_{2n}$$种情况,然后我们只需要把里边的非法情况减去就可以了,一共有多少种非法情况呢? - -首先我们用$$C^n_{2n}$$就保证了一定是有 n 个左括号,n 个右括号,那么为什么出现了非法序列? - -为了方便论述,我们把左括号记为 +1,右括号记为 -1. - -ps:下边的 和 都是指两个数的和,不是你和我中的和。 - -我们假设非法序列的**集合是 M** ,而非法序列就是列举过程中右括号数比左括号数多了,也就是和小于 0 了,变成 -1 了。这种情况一旦出现,后边无论是什么括号都改变不了它是非法序列的命了。我们将第一次和等于 -1 的时候的位置记为 d 。每一个非法序列一定存在这样一个 d 。然后关键的地方到了! - -此时我们把 0 到 d 所有的 -1 变成 1,1 变成 -1,我们将每一个非法序列都这样做,就构成了一个**新的集合 N** ,并且这个集合 N 一定和 M 中的元素一一对应( N -> M,在集合 N 中第一次出现和为 1 的位置也就是 d ,把 0 到 d 中所有的 -1 变成 1,1 变成 -1 就回到了 M),从而集合 M 的数量就等于集合 N 的数量。集合 N 的数量是多少呢? - -我们来分析下集合 N 是什么样的,集合 N 对应的集合 M 原来的序列本来是这样的,在 0 到 d 之间和是 -1 ,也就是 -1 比 +1 多一个,d + 1 到最后的和一定是 1(因为 n 个 +1 和 n 个 -1 的和一定是 0 ,由于 0 到 d 和是 -1,后边的和一定是 1),也就意味着 +1 比 -1 多一个。而在集合 N 中,我们把 0 到 d 的 -1 变成了 +1 ,+1 变成了 -1 ,所以也变成了 +1 比 -1 多一个,所以集合 N 总共就是 +1 比 -1 多 2 个的集合,也就是 n + 1 个 +1 和 n - 1 个 -1 。 - -所以集合 N 就是 2n 个位置中选 n - 1 个位置放 -1,其他位置放 +1,总共就有 $$C^{n - 1}_{2n}$$,所以集合 M 也有 $$C^{n - 1}_{2n}$$种。 - -所有合法序列就有 $$C^n_{2n}-C^{n-1}_{2n}=\frac{1}{n+1}C^n_{2n}$$ 。 - -将集合 M 和集合 N 建立了一一映射,从而解决了问题,神奇!!!!!!!!!!其实,这个数列就是卡塔兰数,可以看下[维基百科](https://zh.wikipedia.org/wiki/%E5%8D%A1%E5%A1%94%E5%85%B0%E6%95%B0)的定义。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/22_8.jpg) - -而这个数列,其实除了括号匹配,还有很多类似的问题,其本质是一样的,例如, - -2n 个人排队买票,其中 n 个人持 50 元,n 个人持 100 元。每张票 50 元,且一人只买一张票。初始时售票处没有零钱找零。请问这 2n 个人一共有多少种排队顺序,不至于使售票处找不开钱? - -对于一个无限大的栈,一共n个元素,请问有几种合法的入栈出栈形式? - -P = a1 * a2 * a3 * ... * an,其中 ai 是矩阵。根据乘法结合律,不改变矩阵的相互顺序,只用括号表示成对的乘积,试问一共有几种括号化方案? - -n 个结点可构造多少个不同的二叉树? - -... ... - -更多例子可以看[维基百科](https://zh.wikipedia.org/wiki/%E5%8D%A1%E5%A1%94%E5%85%B0%E6%95%B0)和[这里](http://www.cnblogs.com/wuyuegb2312/p/3016878.html)。 - -而 Solutin 给出的时间复杂度,其实就是卡特兰数。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/22_9.jpg) - -[维基百科](https://zh.wikipedia.org/wiki/%E5%8D%A1%E5%A1%94%E5%85%B0%E6%95%B0)的给出的性质。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/22_10.jpg) - -# 总 - -本以为这道题挺常规的,然后自己一直卡在解法三的理解上,查来查去,竟然查出了卡塔兰数,虽然似乎和解法三也没什么关系,但又开阔了很多思路。解法三分析出来的迭代方法,以及用映射证明卡塔兰数的求法,棒! - +## 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/22.jpg) + +给一个数字 n ,返回所有合法的括号匹配,刚好和[20题](https://leetcode.wang/leetCode-20-Valid%20Parentheses.html)相反。 + +自己没想出来,全部参考 LeetCode 给出的 [Solution](https://leetcode.com/problems/generate-parentheses/solution/)。 + +# 解法一 暴力破解 + +列举所有的情况,每一位有左括号和右括号两种情况,总共 2n 位,所以总共 $$2^{2n}$$ 种情况。 + +```java +public List generateParenthesis(int n) { + List combinations = new ArrayList(); + generateAll(new char[2 * n], 0, combinations); + return combinations; +} + +public void generateAll(char[] current, int pos, List result) { + if (pos == current.length) { + if (valid(current)) + result.add(new String(current)); + } else { + current[pos] = '('; + generateAll(current, pos+1, result); + current[pos] = ')'; + generateAll(current, pos+1, result); + } +} + +public boolean valid(char[] current) { + int balance = 0; + for (char c: current) { + if (c == '(') balance++; + else balance--; + if (balance < 0) return false; + } + return (balance == 0); +} +``` + +时间复杂度:对每种情况判断是否合法需要 O(n),所以时间复杂度是 $$O(2^{2n}n)$$ 。 + +空间复杂度:$$O(2^{2n}n)$$,乘以 n 是因为每个串的长度是 2n。此外这是假设所有情况都符合的时候,但其实不可能都符合,后边会给出更精确的情况。 + +# 解法二 + +解法一中,我们不停的加左括号,其实如果左括号超过 n 的时候,它肯定不是合法序列了。因为合法序列一定是 n 个左括号和 n 个右括号。 + +还有一种情况就是如果添加括号的过程中,如果右括号的总数量大于左括号的总数量了,后边不论再添加什么,它都不可能是合法序列了。因为每个右括号必须和之前的某个左括号匹配,如果右括号数量多于左括号,那么一定有一个右括号没有与之匹配的左括号,后边不论加多少左括号都没有用了。例如 n = 3 ,总共会有 6 个括号,我们加到 ( ) ) 3 个括号的情况的时候,有 1 个左括号,2 个右括号,此时后边 3 个括号无论是什么,已经注定它不会是合法序列了。 + +基于上边的两点,我们只要避免它们,就可以保证我们生成的括号一定是合法的了。 + +```java +public List generateParenthesis(int n) { + List ans = new ArrayList(); + backtrack(ans, "", 0, 0, n); + return ans; +} + +public void backtrack(List ans, String cur, int left, int right, int n){ + if (cur.length() == n * 2) { + ans.add(cur); + return; + } + //左括号不要超过 n + if (left < n) + backtrack(ans, cur+"(", left+1, right, n); + //右括号不要超过左括号 + if (right < left) + backtrack(ans, cur+")", left, right+1, n); +} +``` + +时间复杂度: + +空间复杂度: + +递归的复杂度分析,继续留坑 =.=。 + +# 解法三 + +解法二中是用列举的方法,仔细想想,我们每次用递归的时候,都是把大问题换成小问题然后去解决,这道题有没有这个思路呢? + +我们想一下之前的列举过程,第 0 个位置一定会是左括号,然后接着添加左括号或右括号,过程中左括号数一定大于或等于右括号数,当第一次出现左括号数等于右括号数的时候,假如此时的位置是 c 。那么位置 1 到 c - 1 之间一定是合法序列,此外 c + 1 到最后的 2n -1 也是合法序列。而假设总共是 n 组括号,1 到 c - 1 是 a 组括号, c + 1 到 2n - 1 之间则是 n - 1 - a 组括号,如下图 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/22_2.jpg) + + + +最重要的是,每一个合法序列都会有这么一个数 c ,且唯一。所以我们如果要求 n 组括号的所有序列,只需要知道 a 组括号以及 ( n - a - 1) 组括号的所有序列,然后两两组合即可。 + +以 n = 3 为例,我们把 0 到 c 之间的括号数记为 a 组, c + 1 到最后的括号数记为 b 组,则 + +a = 0,b = 2 对应 ()(())以及 ()()() 两种情况,此时 c = 1。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/22_3.jpg) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/22_4.jpg) + +a = 1,b = 1,对应 (())(()) 一种情况,此时 c = 3。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/22_5.jpg) + +a = 2,b = 0 对应 ((())), (()()) 两种情况,此时 c = 5。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/22_6.jpg) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/22_7.jpg) + +所以我们如果要想求 n 组括号,只需要知道 a 组和 b 组的情况,然后组合起来就可以了。 + +看起来我们在迭代 a ,其实本质上是在迭代 c ,c = 2a + 1,迭代 a 从 0 到 n - 1 ,就是迭代 c 从 1 到 2n - 1。看起来 c 都是奇数,其实是可以理解的,因为 0 到 c 间都是一组组的括号, 所以 c 一定是奇数。为什么可以迭代 c ,因为上边说到每一个合法序列都对应着一个 c ,遍历 c 的话,就能得到所有的情况了,看一下代码吧。 + +```java +public List generateParenthesis(int n) { + List ans = new ArrayList(); + if (n == 0) { + ans.add(""); + } else { + for (int a = 0; a < n; a++) + for (String left: generateParenthesis(a)) + for (String right: generateParenthesis(n-1-a)) + ans.add("(" + left + ")" + right); + } + return ans; +} +``` + +时间复杂度: + +空间复杂度: + +留坑。 + +# 扩展 卡塔兰数 + +如果这道题不是让你列举所有的情况, 而是仅仅让你输出 n 对应下有多少种合法序列,该怎么做呢? + +答案就是 $$\frac{1}{n+1}C^n_{2n}$$,也可以写成$$\frac{1}{n+1}\binom{2n}{n}$$。怎么证明呢?我主要参考了[这里](http://lanqi.org/interests/10939/),说一下。 + +我们假设不考虑是不是合法序列,那么就一共有$$C^n_{2n}$$种情况,然后我们只需要把里边的非法情况减去就可以了,一共有多少种非法情况呢? + +首先我们用$$C^n_{2n}$$就保证了一定是有 n 个左括号,n 个右括号,那么为什么出现了非法序列? + +为了方便论述,我们把左括号记为 +1,右括号记为 -1. + +ps:下边的 和 都是指两个数的和,不是你和我中的和。 + +我们假设非法序列的**集合是 M** ,而非法序列就是列举过程中右括号数比左括号数多了,也就是和小于 0 了,变成 -1 了。这种情况一旦出现,后边无论是什么括号都改变不了它是非法序列的命了。我们将第一次和等于 -1 的时候的位置记为 d 。每一个非法序列一定存在这样一个 d 。然后关键的地方到了! + +此时我们把 0 到 d 所有的 -1 变成 1,1 变成 -1,我们将每一个非法序列都这样做,就构成了一个**新的集合 N** ,并且这个集合 N 一定和 M 中的元素一一对应( N -> M,在集合 N 中第一次出现和为 1 的位置也就是 d ,把 0 到 d 中所有的 -1 变成 1,1 变成 -1 就回到了 M),从而集合 M 的数量就等于集合 N 的数量。集合 N 的数量是多少呢? + +我们来分析下集合 N 是什么样的,集合 N 对应的集合 M 原来的序列本来是这样的,在 0 到 d 之间和是 -1 ,也就是 -1 比 +1 多一个,d + 1 到最后的和一定是 1(因为 n 个 +1 和 n 个 -1 的和一定是 0 ,由于 0 到 d 和是 -1,后边的和一定是 1),也就意味着 +1 比 -1 多一个。而在集合 N 中,我们把 0 到 d 的 -1 变成了 +1 ,+1 变成了 -1 ,所以也变成了 +1 比 -1 多一个,所以集合 N 总共就是 +1 比 -1 多 2 个的集合,也就是 n + 1 个 +1 和 n - 1 个 -1 。 + +所以集合 N 就是 2n 个位置中选 n - 1 个位置放 -1,其他位置放 +1,总共就有 $$C^{n - 1}_{2n}$$,所以集合 M 也有 $$C^{n - 1}_{2n}$$种。 + +所有合法序列就有 $$C^n_{2n}-C^{n-1}_{2n}=\frac{1}{n+1}C^n_{2n}$$ 。 + +将集合 M 和集合 N 建立了一一映射,从而解决了问题,神奇!!!!!!!!!!其实,这个数列就是卡塔兰数,可以看下[维基百科](https://zh.wikipedia.org/wiki/%E5%8D%A1%E5%A1%94%E5%85%B0%E6%95%B0)的定义。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/22_8.jpg) + +而这个数列,其实除了括号匹配,还有很多类似的问题,其本质是一样的,例如, + +2n 个人排队买票,其中 n 个人持 50 元,n 个人持 100 元。每张票 50 元,且一人只买一张票。初始时售票处没有零钱找零。请问这 2n 个人一共有多少种排队顺序,不至于使售票处找不开钱? + +对于一个无限大的栈,一共n个元素,请问有几种合法的入栈出栈形式? + +P = a1 * a2 * a3 * ... * an,其中 ai 是矩阵。根据乘法结合律,不改变矩阵的相互顺序,只用括号表示成对的乘积,试问一共有几种括号化方案? + +n 个结点可构造多少个不同的二叉树? + +... ... + +更多例子可以看[维基百科](https://zh.wikipedia.org/wiki/%E5%8D%A1%E5%A1%94%E5%85%B0%E6%95%B0)和[这里](http://www.cnblogs.com/wuyuegb2312/p/3016878.html)。 + +而 Solutin 给出的时间复杂度,其实就是卡特兰数。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/22_9.jpg) + +[维基百科](https://zh.wikipedia.org/wiki/%E5%8D%A1%E5%A1%94%E5%85%B0%E6%95%B0)的给出的性质。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/22_10.jpg) + +# 总 + +本以为这道题挺常规的,然后自己一直卡在解法三的理解上,查来查去,竟然查出了卡塔兰数,虽然似乎和解法三也没什么关系,但又开阔了很多思路。解法三分析出来的迭代方法,以及用映射证明卡塔兰数的求法,棒! + diff --git a/leetCode-23-Merge-k-Sorted-Lists.md b/leetCode-23-Merge-k-Sorted-Lists.md index 5c8fa5628..b66573340 100644 --- a/leetCode-23-Merge-k-Sorted-Lists.md +++ b/leetCode-23-Merge-k-Sorted-Lists.md @@ -1,270 +1,270 @@ -## 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/23.jpg) - -k 个有序链表的合并。 - -我们用 N 表示链表的总长度,考虑最坏情况,k 个链表的长度相等,都为 n 。 - -# 解法一 暴力破解 - -简单粗暴,遍历所有的链表,将数字存到一个数组里,然后用快速排序,最后再将排序好的数组存到一个链表里。 - -```java -public ListNode mergeKLists(ListNode[] lists) { - List l = new ArrayList(); - //存到数组 - for (ListNode ln : lists) { - while (ln != null) { - l.add(ln.val); - ln = ln.next; - } - } - //数组排序 - Collections.sort(l); - //存到链表 - ListNode head = new ListNode(0); - ListNode h = head; - for (int i : l) { - ListNode t = new ListNode(i); - h.next = t; - h = h.next; - } - h.next = null; - return head.next; -} -``` - -时间复杂度:假设 N 是所有的数字个数,存到数组是 O(N),排序如果是用快速排序就是 $$O(Nlog_N)$$ ,存到链表是 O(N),所以取个最大的,就是 $$O(Nlog_N)$$。 - -空间复杂度:新建了一个链表,O(N)。 - -# 解法二 一列一列比较 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/23_2.jpg) - -我们可以一列一列的比较,将最小的一个存到一个新的链表里。 - -```java -public ListNode mergeKLists(ListNode[] lists) { - int min_index = 0; - ListNode head = new ListNode(0); - ListNode h = head; - while (true) { - boolean isBreak = true;//标记是否遍历完所有链表 - int min = Integer.MAX_VALUE; - for (int i = 0; i < lists.length; i++) { - if (lists[i] != null) { - //找出最小下标 - if (lists[i].val < min) { - min_index = i; - min = lists[i].val; - } - //存在一个链表不为空,标记改完 false - isBreak = false; - } - - } - if (isBreak) { - break; - } - //加到新链表中 - ListNode a = new ListNode(lists[min_index].val); - h.next = a; - h = h.next; - //链表后移一个元素 - lists[min_index] = lists[min_index].next; - } - h.next = null; - return head.next; -} -``` - -时间复杂度:假设最长的链表长度是 n ,那么 while 循环将循环 n 次。假设链表列表里有 k 个链表,for 循环执行 k 次,所以时间复杂度是 O(kn)。 - -空间复杂度:N 表示最终链表的长度,则为 O(N)。 - -其实我们不需要创建一个新链表保存,我们只需要改变得到的最小结点的指向就可以了。 - -```java -public ListNode mergeKLists(ListNode[] lists) { - int min_index = 0; - ListNode head = new ListNode(0); - ListNode h = head; - while (true) { - boolean isBreak = true; - int min = Integer.MAX_VALUE; - for (int i = 0; i < lists.length; i++) { - if (lists[i] != null) { - if (lists[i].val < min) { - min_index = i; - min = lists[i].val; - } - isBreak = false; - } - - } - if (isBreak) { - break; - } - //最小的节点接过来 - h.next = lists[min_index]; - h = h.next; - lists[min_index] = lists[min_index].next; - } - h.next = null; - return head.next; -} - -``` - -时间复杂度:假设最长的链表长度是 n ,那么 while 循环将循环 n 次。假设链表列表里有 k 个链表,for 循环执行 k 次,所以时间复杂度是 O(kn)。 - -空间复杂度:O(1)。 - -# 解法三 优先队列 - -解法二中,我们每次都是取出一个最小的,然后加入一个新的, O(1)的复杂度,再找最小的,O(k) 的复杂度。我们完全可以用一个优先队列。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/23_3.jpg) - -我们将优先级定义为数越小优先级越高,如果用堆实现优先队列,这样我们每次找最小不再需要 O(k),而是 O(log(k)),当然这样的话,我们加入新的话不再是 O(1),也需要 O(log(k))。可以看看[这里](http://blog.51cto.com/ahalei/1425314?source=dra)和[这里](http://blog.51cto.com/ahalei/1427156)。 - -``` java -public ListNode mergeKLists(ListNode[] lists) { - //定义优先队列的比较器 - Comparator cmp; - cmp = new Comparator() { - @Override - public int compare(ListNode o1, ListNode o2) { - // TODO Auto-generated method stub - return o1.val-o2.val; - } - }; - - //建立队列 - Queue q = new PriorityQueue(cmp); - for(ListNode l : lists){ - if(l!=null){ - q.add(l); - } - } - ListNode head = new ListNode(0); - ListNode point = head; - while(!q.isEmpty()){ - //出队列 - point.next = q.poll(); - point = point.next; - //判断当前链表是否为空,不为空就将新元素入队 - ListNode next = point.next; - if(next!=null){ - q.add(next); - } - } - return head.next; - } -``` - -时间复杂度:假如总共有 N 个节点,每个节点入队出队都需要 log(k),所有时间复杂度是 O(N log(k))。 - -空间复杂度:优先队列需要 O(k)的复杂度。 - -# 解法四 两两合并 - -利用[之前](https://leetcode.windliang.cc/leetCode-21-Merge-Two-Sorted-Lists.html)合并两个链表的算法,我们直接两两合并,第 0 个和第 1 个链表合并,新生成的再和第 2 个链表合并,新生成的再和第 3 个链表合并...直到全部合并完。 - -```java -public ListNode mergeTwoLists(ListNode l1, ListNode l2) { - ListNode h = new ListNode(0); - ListNode ans=h; - while (l1 != null && l2 != null) { - if (l1.val < l2.val) { - h.next = l1; - h = h.next; - l1 = l1.next; - } else { - h.next = l2; - h = h.next; - l2 = l2.next; - } - } - if(l1==null){ - h.next=l2; - } - if(l2==null){ - h.next=l1; - } - return ans.next; -} -public ListNode mergeKLists(ListNode[] lists) { - if(lists.length==1){ - return lists[0]; - } - if(lists.length==0){ - return null; - } - ListNode head = mergeTwoLists(lists[0],lists[1]); - for (int i = 2; i < lists.length; i++) { - head = mergeTwoLists(head,lists[i]); - } - return head; -} -``` - -时间复杂度:不妨假设是 k 个链表并且长度相同,链表总长度为 N,那么第一次合并就是 N/k 和 N/k ,第二次合并就是 2 \* N/k 和 N/k,第三次合并就是 3 \* N/k 和 N / k,总共进行 n - 1 次合并,每次合并的时间复杂度是 O(n),所以总时间复杂度就是$$O(\sum_{i=1}^{k-1}(i*\frac{N}{k}+\frac{N}{k}))=O(kN)$$,可以将两项分开,N/k 其实是常数,分开的第一项是等差数列。 - -空间复杂度:O(1)。 - -#解法五 两两合并优化 - -依旧假设是 k 个链表,合并的过程优化下,使得只需要合并 log(k)次。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/23_4.jpg) - -```java -public ListNode mergeTwoLists(ListNode l1, ListNode l2) { - ListNode h = new ListNode(0); - ListNode ans=h; - while (l1 != null && l2 != null) { - if (l1.val < l2.val) { - h.next = l1; - h = h.next; - l1 = l1.next; - } else { - h.next = l2; - h = h.next; - l2 = l2.next; - } - } - if(l1==null){ - h.next=l2; - } - if(l2==null){ - h.next=l1; - } - return ans.next; -} -public ListNode mergeKLists(ListNode[] lists) { - if(lists.length==0){ - return null; - } - int interval = 1; - while(interval l = new ArrayList(); + //存到数组 + for (ListNode ln : lists) { + while (ln != null) { + l.add(ln.val); + ln = ln.next; + } + } + //数组排序 + Collections.sort(l); + //存到链表 + ListNode head = new ListNode(0); + ListNode h = head; + for (int i : l) { + ListNode t = new ListNode(i); + h.next = t; + h = h.next; + } + h.next = null; + return head.next; +} +``` + +时间复杂度:假设 N 是所有的数字个数,存到数组是 O(N),排序如果是用快速排序就是 $$O(Nlog_N)$$ ,存到链表是 O(N),所以取个最大的,就是 $$O(Nlog_N)$$。 + +空间复杂度:新建了一个链表,O(N)。 + +# 解法二 一列一列比较 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/23_2.jpg) + +我们可以一列一列的比较,将最小的一个存到一个新的链表里。 + +```java +public ListNode mergeKLists(ListNode[] lists) { + int min_index = 0; + ListNode head = new ListNode(0); + ListNode h = head; + while (true) { + boolean isBreak = true;//标记是否遍历完所有链表 + int min = Integer.MAX_VALUE; + for (int i = 0; i < lists.length; i++) { + if (lists[i] != null) { + //找出最小下标 + if (lists[i].val < min) { + min_index = i; + min = lists[i].val; + } + //存在一个链表不为空,标记改完 false + isBreak = false; + } + + } + if (isBreak) { + break; + } + //加到新链表中 + ListNode a = new ListNode(lists[min_index].val); + h.next = a; + h = h.next; + //链表后移一个元素 + lists[min_index] = lists[min_index].next; + } + h.next = null; + return head.next; +} +``` + +时间复杂度:假设最长的链表长度是 n ,那么 while 循环将循环 n 次。假设链表列表里有 k 个链表,for 循环执行 k 次,所以时间复杂度是 O(kn)。 + +空间复杂度:N 表示最终链表的长度,则为 O(N)。 + +其实我们不需要创建一个新链表保存,我们只需要改变得到的最小结点的指向就可以了。 + +```java +public ListNode mergeKLists(ListNode[] lists) { + int min_index = 0; + ListNode head = new ListNode(0); + ListNode h = head; + while (true) { + boolean isBreak = true; + int min = Integer.MAX_VALUE; + for (int i = 0; i < lists.length; i++) { + if (lists[i] != null) { + if (lists[i].val < min) { + min_index = i; + min = lists[i].val; + } + isBreak = false; + } + + } + if (isBreak) { + break; + } + //最小的节点接过来 + h.next = lists[min_index]; + h = h.next; + lists[min_index] = lists[min_index].next; + } + h.next = null; + return head.next; +} + +``` + +时间复杂度:假设最长的链表长度是 n ,那么 while 循环将循环 n 次。假设链表列表里有 k 个链表,for 循环执行 k 次,所以时间复杂度是 O(kn)。 + +空间复杂度:O(1)。 + +# 解法三 优先队列 + +解法二中,我们每次都是取出一个最小的,然后加入一个新的, O(1)的复杂度,再找最小的,O(k) 的复杂度。我们完全可以用一个优先队列。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/23_3.jpg) + +我们将优先级定义为数越小优先级越高,如果用堆实现优先队列,这样我们每次找最小不再需要 O(k),而是 O(log(k)),当然这样的话,我们加入新的话不再是 O(1),也需要 O(log(k))。可以看看[这里](http://blog.51cto.com/ahalei/1425314?source=dra)和[这里](http://blog.51cto.com/ahalei/1427156)。 + +``` java +public ListNode mergeKLists(ListNode[] lists) { + //定义优先队列的比较器 + Comparator cmp; + cmp = new Comparator() { + @Override + public int compare(ListNode o1, ListNode o2) { + // TODO Auto-generated method stub + return o1.val-o2.val; + } + }; + + //建立队列 + Queue q = new PriorityQueue(cmp); + for(ListNode l : lists){ + if(l!=null){ + q.add(l); + } + } + ListNode head = new ListNode(0); + ListNode point = head; + while(!q.isEmpty()){ + //出队列 + point.next = q.poll(); + point = point.next; + //判断当前链表是否为空,不为空就将新元素入队 + ListNode next = point.next; + if(next!=null){ + q.add(next); + } + } + return head.next; + } +``` + +时间复杂度:假如总共有 N 个节点,每个节点入队出队都需要 log(k),所有时间复杂度是 O(N log(k))。 + +空间复杂度:优先队列需要 O(k)的复杂度。 + +# 解法四 两两合并 + +利用[之前](https://leetcode.windliang.cc/leetCode-21-Merge-Two-Sorted-Lists.html)合并两个链表的算法,我们直接两两合并,第 0 个和第 1 个链表合并,新生成的再和第 2 个链表合并,新生成的再和第 3 个链表合并...直到全部合并完。 + +```java +public ListNode mergeTwoLists(ListNode l1, ListNode l2) { + ListNode h = new ListNode(0); + ListNode ans=h; + while (l1 != null && l2 != null) { + if (l1.val < l2.val) { + h.next = l1; + h = h.next; + l1 = l1.next; + } else { + h.next = l2; + h = h.next; + l2 = l2.next; + } + } + if(l1==null){ + h.next=l2; + } + if(l2==null){ + h.next=l1; + } + return ans.next; +} +public ListNode mergeKLists(ListNode[] lists) { + if(lists.length==1){ + return lists[0]; + } + if(lists.length==0){ + return null; + } + ListNode head = mergeTwoLists(lists[0],lists[1]); + for (int i = 2; i < lists.length; i++) { + head = mergeTwoLists(head,lists[i]); + } + return head; +} +``` + +时间复杂度:不妨假设是 k 个链表并且长度相同,链表总长度为 N,那么第一次合并就是 N/k 和 N/k ,第二次合并就是 2 \* N/k 和 N/k,第三次合并就是 3 \* N/k 和 N / k,总共进行 n - 1 次合并,每次合并的时间复杂度是 O(n),所以总时间复杂度就是$$O(\sum_{i=1}^{k-1}(i*\frac{N}{k}+\frac{N}{k}))=O(kN)$$,可以将两项分开,N/k 其实是常数,分开的第一项是等差数列。 + +空间复杂度:O(1)。 + +#解法五 两两合并优化 + +依旧假设是 k 个链表,合并的过程优化下,使得只需要合并 log(k)次。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/23_4.jpg) + +```java +public ListNode mergeTwoLists(ListNode l1, ListNode l2) { + ListNode h = new ListNode(0); + ListNode ans=h; + while (l1 != null && l2 != null) { + if (l1.val < l2.val) { + h.next = l1; + h = h.next; + l1 = l1.next; + } else { + h.next = l2; + h = h.next; + l2 = l2.next; + } + } + if(l1==null){ + h.next=l2; + } + if(l2==null){ + h.next=l1; + } + return ans.next; +} +public ListNode mergeKLists(ListNode[] lists) { + if(lists.length==0){ + return null; + } + int interval = 1; + while(interval 0) { - toNull = toNull.next; - if (toNull == null) { - return dummy.next; - } - i--; - } - ListNode temp = toNull.next; - //将子链表断开 - toNull.next = null; - ListNode new_sub_head = reverse(sub_head); - //将倒置后的链表接到 tail 后边 - tail.next = new_sub_head; - //更新 tail - tail = sub_head; //sub_head 由于倒置其实是新链表的尾部 - sub_head = temp; - toNull = sub_head; - //将后边断开的链表接回来 - tail.next = sub_head; - } - return dummy.next; -} -public ListNode reverse(ListNode head) { - ListNode current_head = null; - while (head != null) { - ListNode next = head.next; - head.next = current_head; - current_head = head; - head = next; - } - return current_head; -} -``` - -时间复杂度:while 循环中本质上我们只是将每个结点访问了一次,加上结点倒置访问的一次,所以总共加起来每个结点其实只访问了 2 次。所以时间复杂度是 O(n)。 - -空间复杂度:O(1)。 - -# 解法二递归 - -有没有被解法一的各种指针绕晕呢,我们有一个更好的选择,递归,这样看起来就会简洁很多。 - -```java -public ListNode reverseKGroup(ListNode head, int k) { - if (head == null) - return null; - ListNode point = head; - //找到子链表的尾部 - int i = k; - while(i - 1 >0){ - point = point.next; - if (point == null) { - return head; - } - i--; - } - ListNode temp = point.next; - //将子链表断开 - point.next = null; - - //倒置子链表,并接受新的头结点 - ListNode new_head = reverse(head); - - //head 其实是倒置链表的尾部,然后我们将后边的倒置结果接过来就可以了 - //temp 是链表断开后的头指针,可以参考解法一的图示 - head.next = reverseKGroup(temp,k); - return new_head; -} -public ListNode reverse(ListNode head) { - ListNode current_head = null; - while (head != null) { - ListNode next = head.next; - head.next = current_head; - current_head = head; - head = next; - } - return current_head; -} -``` - -复杂度:递归留坑。 - -# 总 - -还是那句话,涉及到链表的,我们就画下图,把各个指针的移动理清楚,一般没啥问题。 - - - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/25.jpg) + +将一个链表,每 k 个倒置,最后一组不足 k 个就不倒置。 + +# 解法一 迭代 + +关于单链表倒置,我们在[第 2 题](https://leetcode.windliang.cc/leetCode-2-Add-Two-Numbers.html)就讨论过。有了单链表倒置,这道题无非就是用一个循环,每次将 k 个结点取下来,倒置后再接回去,然后再取 k 个,以此循环,到了最后一组如果不足 k 个,不做处理,直接返回头结点就可以了。所以关键就是,指针指来指去,大家不晕掉就好,我做了图示,大家参考一下。 + +为了将头结点也一般化,我们创建一个 dummy 结点,然后整个过程主要运用三个指针, tail 指针表示已经倒置后的链表的尾部,subhead 指针表示要进行倒置的子链表,toNull 指针为了将子链表从原来链表中取下来。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/25_2.jpg) + +一个 while 循环,让 toNull 指针走 k - 1 步使其指向子链表的尾部。中间的 if 语句就是判断当前节点数够不够 k 个了,不够的话直接返回结果就可以了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/25_3.jpg) + +将子链表指向 null ,脱离出来。并且用 temp 保存下一个结点的位置。 + + + +![](https://windliang.oss-cn-beijing.aliyuncs.com/25_4.jpg) + +然后调用倒置函数,将子链表倒置。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/25_5.jpg) + +接下来四步分别是,新链表接到 tail(注意下边的图 tail 是更新后的位置,之前 tail 在 dummy 的位置) 的后边;更新 tail 到新链表的尾部,也就是之前的 subhead (下图 subhead 也是更新后的位置,之前的位置参见上边的图);sub_head 更新到 temp 的位置;toNull 到 sub_head 的位置;然后将新的尾部 tail 把之前断开的链表连起来,接到 sub_head 上。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/25_6.jpg) + +整理下其实就是下边的样子 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/25_7.jpg) + +和初始的时候(下边的图)对比一下,发现 tail,subhead 和 toNull 三个指针已经就位,可以愉快的重复上边的步骤了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/25_2.jpg) + +看下代码吧。 + +```java +public ListNode reverseKGroup(ListNode head, int k) { + if (head == null) + return null; + ListNode sub_head = head; + ListNode dummy = new ListNode(0); + dummy.next = head; + ListNode tail = dummy; + ListNode toNull = head; + while (sub_head != null) { + int i = k; + //找到子链表的尾部 + while (i - 1 > 0) { + toNull = toNull.next; + if (toNull == null) { + return dummy.next; + } + i--; + } + ListNode temp = toNull.next; + //将子链表断开 + toNull.next = null; + ListNode new_sub_head = reverse(sub_head); + //将倒置后的链表接到 tail 后边 + tail.next = new_sub_head; + //更新 tail + tail = sub_head; //sub_head 由于倒置其实是新链表的尾部 + sub_head = temp; + toNull = sub_head; + //将后边断开的链表接回来 + tail.next = sub_head; + } + return dummy.next; +} +public ListNode reverse(ListNode head) { + ListNode current_head = null; + while (head != null) { + ListNode next = head.next; + head.next = current_head; + current_head = head; + head = next; + } + return current_head; +} +``` + +时间复杂度:while 循环中本质上我们只是将每个结点访问了一次,加上结点倒置访问的一次,所以总共加起来每个结点其实只访问了 2 次。所以时间复杂度是 O(n)。 + +空间复杂度:O(1)。 + +# 解法二递归 + +有没有被解法一的各种指针绕晕呢,我们有一个更好的选择,递归,这样看起来就会简洁很多。 + +```java +public ListNode reverseKGroup(ListNode head, int k) { + if (head == null) + return null; + ListNode point = head; + //找到子链表的尾部 + int i = k; + while(i - 1 >0){ + point = point.next; + if (point == null) { + return head; + } + i--; + } + ListNode temp = point.next; + //将子链表断开 + point.next = null; + + //倒置子链表,并接受新的头结点 + ListNode new_head = reverse(head); + + //head 其实是倒置链表的尾部,然后我们将后边的倒置结果接过来就可以了 + //temp 是链表断开后的头指针,可以参考解法一的图示 + head.next = reverseKGroup(temp,k); + return new_head; +} +public ListNode reverse(ListNode head) { + ListNode current_head = null; + while (head != null) { + ListNode next = head.next; + head.next = current_head; + current_head = head; + head = next; + } + return current_head; +} +``` + +复杂度:递归留坑。 + +# 总 + +还是那句话,涉及到链表的,我们就画下图,把各个指针的移动理清楚,一般没啥问题。 + + + diff --git a/leetCode-26-Remove-Duplicates-from-Sorted-Array.md b/leetCode-26-Remove-Duplicates-from-Sorted-Array.md index 77cdf37ac..31b9d1681 100644 --- a/leetCode-26-Remove-Duplicates-from-Sorted-Array.md +++ b/leetCode-26-Remove-Duplicates-from-Sorted-Array.md @@ -1,61 +1,61 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/26.jpg) - -返回非重复数字的个数,并且把 nums 里重复的数字也去掉。 - -例如,nums = [ 1, 1, 2 ] ,那么就返回 2 ,并且把 nums 变成 [ 1, 2 ]。 - -这道题,蛮简单的,但是自己写的时候多加了个 while 循环,但和给出的 Solution 本质还是一样的。 - -# 我写的 - -for 循环遍历每个数,while 循环判断当前数和它的后一个数是否相等,相等就后移一个数,并且接着判断后移的数和它后边的数是否相等,然后一直循环下去。不相等就将后一个数保存起来,并且长度加 1,然后结束循环。 - -```java -public int removeDuplicates(int[] nums) { - int len = 1; - for (int i = 0; i < nums.length - 1; i++) { - while (i < nums.length - 1) { - if (nums[i] == nums[i + 1]) { - i++; - } else { - nums[len] = nums[i + 1]; - len = len + 1; - break; - } - } - } - return len; -} -``` - -时间复杂度: O(n)。 - -空间复杂度:O(1)。 - -# Solution 给出的 - -利用快慢指针,i 指针从 0 开始,j 指针从 1 开始,如果 i 和 j 所指数字相等,就一直后移 j 。如果不相等,i 指针后移一位用来保存当前 j 所指的值,然后继续回到 j 的后移中去。 - -```java -public int removeDuplicates(int[] nums) { - if (nums.length == 0) return 0; - int i = 0; - for (int j = 1; j < nums.length; j++) { - if (nums[j] != nums[i]) { - i++; - nums[i] = nums[j]; - } - } - return i + 1; -} -``` - -时间复杂度: O(n)。 - -空间复杂度:O(1)。 - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/26.jpg) + +返回非重复数字的个数,并且把 nums 里重复的数字也去掉。 + +例如,nums = [ 1, 1, 2 ] ,那么就返回 2 ,并且把 nums 变成 [ 1, 2 ]。 + +这道题,蛮简单的,但是自己写的时候多加了个 while 循环,但和给出的 Solution 本质还是一样的。 + +# 我写的 + +for 循环遍历每个数,while 循环判断当前数和它的后一个数是否相等,相等就后移一个数,并且接着判断后移的数和它后边的数是否相等,然后一直循环下去。不相等就将后一个数保存起来,并且长度加 1,然后结束循环。 + +```java +public int removeDuplicates(int[] nums) { + int len = 1; + for (int i = 0; i < nums.length - 1; i++) { + while (i < nums.length - 1) { + if (nums[i] == nums[i + 1]) { + i++; + } else { + nums[len] = nums[i + 1]; + len = len + 1; + break; + } + } + } + return len; +} +``` + +时间复杂度: O(n)。 + +空间复杂度:O(1)。 + +# Solution 给出的 + +利用快慢指针,i 指针从 0 开始,j 指针从 1 开始,如果 i 和 j 所指数字相等,就一直后移 j 。如果不相等,i 指针后移一位用来保存当前 j 所指的值,然后继续回到 j 的后移中去。 + +```java +public int removeDuplicates(int[] nums) { + if (nums.length == 0) return 0; + int i = 0; + for (int j = 1; j < nums.length; j++) { + if (nums[j] != nums[i]) { + i++; + nums[i] = nums[j]; + } + } + return i + 1; +} +``` + +时间复杂度: O(n)。 + +空间复杂度:O(1)。 + +# 总 + 不同的思想,决定了写出来的代码不同,但就时间复杂度来看,它们本质还是一样的。 \ No newline at end of file diff --git a/leetCode-27-Remove-Element.md b/leetCode-27-Remove-Element.md index e84db40c6..4816f2305 100644 --- a/leetCode-27-Remove-Element.md +++ b/leetCode-27-Remove-Element.md @@ -1,65 +1,65 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/27.jpg) - -和[上一题](https://leetcode.windliang.cc/leetCode-26-Remove-Duplicates-from-Sorted-Array.html)类似,只不过这个是去除给定的值,看起来还更简单些。 - -例如给了 nums = [ 3, 2, 2, 3 ],val = 3, 然后我们返回 len = 2,并且 nums 修改为 [ 2, 2 ] 。 - -# 解法一 - -和上道题一样,我们利用快慢指针,此外我们还得用下反向的思维。快指针 fast 和慢指针 slow,一直移动 fast ,如果 fast 指向的值不等于给定的 val ,我们就将值赋给 slow 指向的位置,slow 后移一位。如果 fast 指向的值等于 val 了,此时 fast 后移一位就可以了,不做其他操作。 - -```java -public int removeElement(int[] nums, int val) { - int fast = 0; - int slow = 0; - while (fast < nums.length) { - if (nums[fast] != val) { - nums[slow++] = nums[fast]; - } - fast++; - } - return slow; -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(1)。 - -# 解法二 - -参考给出的[Soloution](https://leetcode.com/problems/remove-element/solution/)。 - -上边的解法,我们是如果**不等于** val 就赋值。但如果按题目的想法,应该是如果**等于** val 就移除。我们从正方面去想,也就是等于 val 的话,我们怎么体现移除呢? - -题目中有个说明我们没利用到,他告诉我们说 the order of those five elements can be arbitrary,就是说数组的顺序可以随便换,我们怎么充分利用呢? - -我们可以这样,如果当前元素等于 val 了,我们就把它扔掉,然后将最后一个值赋值到当前位置,并且长度减去 1。什么意思呢? - -比如 1 2 2 4 6,如果 val 等于 2 。那么当移动到 2 的时候,等于 val 了。我们就把最后一个位置的 6 赋值过来,长度减去 1 。就变成了 1 6 2 4。完美!达到了移除的效果。然后当又移动到新的 2 的时候,就把最后的 4 拿过来,变成 1 6 4,达到了移除的效果。看下代码吧。 - -```java -public int removeElement(int[] nums, int val) { - int i = 0; - int n = nums.length; - while (i < n) { - if (nums[i] == val) { - nums[i] = nums[n - 1]; - n--; - } else { - i++; - } - } - return n; -} -``` - -时间复杂度:同样是 O(n),但如果等于 val 的值比较少,解法二会更有效率些。比如 1 2 3 4,val = 2。解法一 while 循环中将调用 3 次赋值。而解法二中,仅仅当等于 val 的时候赋值 1 次。 - -空间复杂度:O(1)。 - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/27.jpg) + +和[上一题](https://leetcode.windliang.cc/leetCode-26-Remove-Duplicates-from-Sorted-Array.html)类似,只不过这个是去除给定的值,看起来还更简单些。 + +例如给了 nums = [ 3, 2, 2, 3 ],val = 3, 然后我们返回 len = 2,并且 nums 修改为 [ 2, 2 ] 。 + +# 解法一 + +和上道题一样,我们利用快慢指针,此外我们还得用下反向的思维。快指针 fast 和慢指针 slow,一直移动 fast ,如果 fast 指向的值不等于给定的 val ,我们就将值赋给 slow 指向的位置,slow 后移一位。如果 fast 指向的值等于 val 了,此时 fast 后移一位就可以了,不做其他操作。 + +```java +public int removeElement(int[] nums, int val) { + int fast = 0; + int slow = 0; + while (fast < nums.length) { + if (nums[fast] != val) { + nums[slow++] = nums[fast]; + } + fast++; + } + return slow; +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(1)。 + +# 解法二 + +参考给出的[Soloution](https://leetcode.com/problems/remove-element/solution/)。 + +上边的解法,我们是如果**不等于** val 就赋值。但如果按题目的想法,应该是如果**等于** val 就移除。我们从正方面去想,也就是等于 val 的话,我们怎么体现移除呢? + +题目中有个说明我们没利用到,他告诉我们说 the order of those five elements can be arbitrary,就是说数组的顺序可以随便换,我们怎么充分利用呢? + +我们可以这样,如果当前元素等于 val 了,我们就把它扔掉,然后将最后一个值赋值到当前位置,并且长度减去 1。什么意思呢? + +比如 1 2 2 4 6,如果 val 等于 2 。那么当移动到 2 的时候,等于 val 了。我们就把最后一个位置的 6 赋值过来,长度减去 1 。就变成了 1 6 2 4。完美!达到了移除的效果。然后当又移动到新的 2 的时候,就把最后的 4 拿过来,变成 1 6 4,达到了移除的效果。看下代码吧。 + +```java +public int removeElement(int[] nums, int val) { + int i = 0; + int n = nums.length; + while (i < n) { + if (nums[i] == val) { + nums[i] = nums[n - 1]; + n--; + } else { + i++; + } + } + return n; +} +``` + +时间复杂度:同样是 O(n),但如果等于 val 的值比较少,解法二会更有效率些。比如 1 2 3 4,val = 2。解法一 while 循环中将调用 3 次赋值。而解法二中,仅仅当等于 val 的时候赋值 1 次。 + +空间复杂度:O(1)。 + +# 总 + Solution 给出的想法让人耳目一新,对于移除的值少的情况,优化了不少。 \ No newline at end of file diff --git a/leetCode-28-Implement-strStr.md b/leetCode-28-Implement-strStr.md index 860849350..c1fe2ab9c 100644 --- a/leetCode-28-Implement-strStr.md +++ b/leetCode-28-Implement-strStr.md @@ -1,55 +1,55 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/28.jpg) - -返回一个字符串 needle 在另一个字符串 haystack 中开始的位置,如果不存在就返回 -1 ,如果 needle 长度是 0 ,就返回 0 。 - -就是一一比较就好,看下代码吧。 - -```java -public int strStr(String haystack, String needle) { - if (needle.length() == 0) { - return 0; - } - int j = 0; - //遍历每个字符 - for (int i = 0; i < haystack.length(); i++) { - //相等的话计数加 1 - if (haystack.charAt(i) == needle.charAt(j)) { - j++; - //长度够了就返回 - if (j == needle.length()) { - return i - j + 1; - } - // 不相等的话 j 清零, - // 并且 i 回到最初的位置,之后就进入 for 循环中的 i++,从下个位置继续判断 - } else { - i = i - j; - j = 0; - } - } - return -1; -} -``` - -时间复杂度:假设 haystack 和 needle 的长度分别是 n 和 k,对于每一个 i ,我们最多执行 k - 1 次,总共会有 n 个 i ,所以时间复杂度是 O(kn)。 - -空间复杂度:O(1)。 - -我们再看下别人的[代码](https://leetcode.com/problems/implement-strstr/discuss/12807/Elegant-Java-solution),用两个 for 循环。但本质其实是一样的,但可能会更好理解些吧。 - -```java -public int strStr(String haystack, String needle) { - for (int i = 0; ; i++) { - for (int j = 0; ; j++) { - if (j == needle.length()) return i; - if (i + j == haystack.length()) return -1; - if (needle.charAt(j) != haystack.charAt(i + j)) break; - } - } -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/28.jpg) + +返回一个字符串 needle 在另一个字符串 haystack 中开始的位置,如果不存在就返回 -1 ,如果 needle 长度是 0 ,就返回 0 。 + +就是一一比较就好,看下代码吧。 + +```java +public int strStr(String haystack, String needle) { + if (needle.length() == 0) { + return 0; + } + int j = 0; + //遍历每个字符 + for (int i = 0; i < haystack.length(); i++) { + //相等的话计数加 1 + if (haystack.charAt(i) == needle.charAt(j)) { + j++; + //长度够了就返回 + if (j == needle.length()) { + return i - j + 1; + } + // 不相等的话 j 清零, + // 并且 i 回到最初的位置,之后就进入 for 循环中的 i++,从下个位置继续判断 + } else { + i = i - j; + j = 0; + } + } + return -1; +} +``` + +时间复杂度:假设 haystack 和 needle 的长度分别是 n 和 k,对于每一个 i ,我们最多执行 k - 1 次,总共会有 n 个 i ,所以时间复杂度是 O(kn)。 + +空间复杂度:O(1)。 + +我们再看下别人的[代码](https://leetcode.com/problems/implement-strstr/discuss/12807/Elegant-Java-solution),用两个 for 循环。但本质其实是一样的,但可能会更好理解些吧。 + +```java +public int strStr(String haystack, String needle) { + for (int i = 0; ; i++) { + for (int j = 0; ; j++) { + if (j == needle.length()) return i; + if (i + j == haystack.length()) return -1; + if (needle.charAt(j) != haystack.charAt(i + j)) break; + } + } +} +``` + +# 总 + 总的来说,还是比较简单的,就是简单的遍历就实现了。 \ No newline at end of file diff --git a/leetCode-29-Divide-Two-Integers.md b/leetCode-29-Divide-Two-Integers.md index 8c68ab899..3928c44f8 100644 --- a/leetCode-29-Divide-Two-Integers.md +++ b/leetCode-29-Divide-Two-Integers.md @@ -1,362 +1,362 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/29.jpg) - -两个数相除,给出商。不能用乘法,除法和模操作。 - -本来觉得这道题蛮简单的,记录下自己的坎坷历程。 - -# 尝试1 - -先确定商的符号,然后把被除数和除数通通转为正数,然后用被除数不停的减除数,直到小于除数的时候,用一个计数遍历记录总共减了多少次,即为商了。 - -确定商的符号的时候,以及返回最终结果的时候,我们可能需要进行乘 -1 操作,即取相反数。而题目规定不让用乘法,所以我们需要知道计算机是怎么进行存数的。 - -计算机为了算减法,利用了同余的性质。 - -同余的定义是 a ≡ b ( mod m ) ,即 a mod m == b mod m ,例如 5 ≡ 17 mod ( 12 )。[百度百科](https://baike.baidu.com/item/%E5%90%8C%E4%BD%99%E5%AE%9A%E7%90%86/1212360?fromtitle=%E5%90%8C%E4%BD%99&fromid=1432545) - -同余有两个性质 - -反身性:a ≡ a ( mod m ); - -同余式相加:若 a ≡ b ( mod m ),c ≡ d ( mod m ),则 a + c ≡ b + d ( mod m ); - -现在我们进行模 16 的加法操作,先熟悉下下边的几个式子。 - -2 + 14 = 0 - -2 + (-3) = 15 - -5 + 15 = 4 - -重点来了! - -计算 4 - 2 怎么算呢? - -也就是 4 + (- 2) - -4 ≡ 4(mod 16) - --2 ≡ 14(mod 16) - -所以 4 + (- 2)= 4 + 14 = 2。 - -我们利用同余的性质,把减法成功转换成了加法,所以我们只需要在计算机里边将 -2 存成 14 就行了。我们这里减去 2 就等价于加上 14。 - -再比如 13 - 7 ,也就是 13 + (-7) - -13 ≡ 13 (mod 16) - --7 ≡ 9(mod 16) - -所有 13 + (- 7)= 13 + 9 = 6 - -我们成功把减 7 转换成了加上 9。 - -减 2 转换成加 14,减 7 转换成加 9,这几组数有什么联系呢?是的 2 + 14 = 16,7 + 9 = 16,他们相加通通等于 16,也就是我们取的模。有种互补的感觉,所以我们把 14 叫做 - 2 的补数,9 叫做 - 7 的补数。 - -可以看到,我们用一些正数表示了负数,总共有 16 个数,除去 0,还剩 15 个数,不可避免的是,这 16 个数,正数和负数的个数会相差 1,我们来看看是正数多,还是负数多。 - -| 补数 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -| ------------ | :--- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | -| 所代表的的数 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 等下 | -| 补数 | | 15 | 14 | 13 | 12 | 11 | 10 | 9 | | -| 所代表的数 | | -1 | -2 | -3 | -4 | -5 | -6 | -7 | | - -上边的列出的数,应该都没异议吧,那么正数多还是负数多呢?就取决于 8 代表多少了。 - -8 + 1 = 9 ,9 代表 -7 ,而 - 8 + 1 = - 7,所以 8 其实代表 - 8 。 - -所以 0 到 15 这 16 个数字可以表示的范围是 -8 ~ 7,-8 没有对称的正数。 - -我们再来看看计算机里是怎么存的,我们都知道,计算机中是以二进制的方式存储的。假设我们计算机能存储 4 位。范围就是 0000 到 1111,也就是 0 到 15。 - -| 补数 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | -| ---------- | :--- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | -| 二进制表示 | 0000 | 0001 | 0010 | 0011 | 0100 | 0101 | 0110 | 0111 | | -| 所代表的数 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | -| 补数 | | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | -| 二进制表示 | | 1111 | 1110 | 1101 | 1100 | 1011 | 1010 | 1001 | 1000 | -| 所代表的数 | | -1 | -2 | -3 | -4 | -5 | -6 | -7 | -8 | - -我们利用这个表格,求几个例子。 - -2 - 3 = 2 + (-3)= 2 的补数 + - 3 的补数 = 0010 + 1101 = 1111 - -而看表格, 1111 代表的数就是 -1 ,从而我们用加法计算出了 2 - 3 = - 1。 - --3 - 2 = (-3)+(-2)= -3 的补数 + -2 的补数 = 1101 + 1110 = 1011 - -我们可以看到 1101 + 1110 本来等于 1 1011 ,因为只存储 4 位,所以最高位被丢掉了,其实这就进行了取模的操作,减去了 16 。如果我们看所对应的十进制是怎么操作的, 1101 表示 13,1110 表示 14 ,13 + 14 = 27 ,如果是模 16 操作下,就是 11 ,而 11 就是上边的结果 1011,看表格它代表的数是 - 5,- 3 - 2 = - 5 ,没毛病。 - -而且我们发现用这种表示方式,所有的正数首位都是 0 ,负数的首位都是 1 ,我们可以这样想。 - -假设正数和负数的首位相同,假如首位都是 0。 - -那么比如 a = 0010 这个正数,如果我们去找它的相反数 b,也就是对应的负数。由假设可以知道,它的相反数的最高位也是 0。即 0xxx 的形式。为了使得 a 和它的相反数相加等于 0,我们必须使得相反数 b 的第 3 位是 1,即 0x10,才能使得第 3 位的和是 0。但这样的话,第 3 位产生了进位, b 的第 2 位也得是 1,所以 b 就成了 0110,但这样虽然使得最后 3 位的和变成了 0,但是第 1 位我们假设了它是 0,由于第 2 位产生的进位,这样 a 和 b 相加不是 0 了,产生矛盾。所以假设不成立。所以正数和负数的首位一定不同,如果首位 0 代表正数,那么负数的首位一定是 1。 - -接下来的问题,给出一个数我们总不能查表去看它的补码吧,我们如何得出补码? - -对于正数,看表格,我们直接写原码就可以了,例如 7 就是 0111 。 - -负数呢? - -我们之前讨论过,对于模 16 的话,- 2 的补码是 14,也就是 16 - 2。- 7 的补码是 9,也就是 16 - 7 = 9。 - -我们从二进制的方式看一下。 - -我们来求 - 2 的补码,用 16 - 2 = 1 0000 - 0010 = ( 1111 + 1 ) - 0010 = ( 1111 - 0010 ) + 1 = 1101 + 1 = 1110 。 - -为什么转换成 1111 减去一个数,因为用 1111 减去一个数,虽然是减法,但其实只要把这个数按位求反即可。也就是把 2, 0010 按位求反变成 1101,再加上 1 就是 -2 的补码形式了,「按位取反,末位加 1 」这个口诀是不是很熟悉,哈哈,这就是快速求补码的法则。但我们不要忘了它的本质,其实是用模长减去它,但是计算机并不会减法,而是巧妙的转换到了取反再加 1 。 - -逆过程呢?如果我们知道了计算机存了个数 1110,那么它代表多少呢?首先首位是 1 ,它一定是一个负数,其次它是怎么得来的呢?往上翻,其实是用 16 - 2 =1110 得到的,我们现在是准备求 2 ,用 16 减去它就可以了,也就是 16 - 1110 = 1 0000 - 1110 = (1111 + 1)- 1110 = (1111 - 1110) + 1 = 0010。巧了,依旧是按位取反,末位加 1。而 0010 就是 2,所以 1110 就代表 - 2。 - -综上,其实我们就是用原来的一部分正数(其实说它是正数也无非是我们自己定义的,想起一句话,数学就像一门宗教,你要么完全相信,要么完全不信,哈哈)表示了负数,而现在为了实现减法,我们把 1xxx 的不当做正数了,把它定义为负数,是的没有负号,但开头是 1 ,我们就说它是负数,再取个名字就叫补数吧(其实就是它代表的负数离它最近的一个和它同余的数,例如 - 3,和它同余的最近的正数就是 13 了,所以 -3 的补数就是 13),再利用余数定理,以及计算机高位溢出等效于求模的性质,巧妙的用取反以及加法实现了减法。 - -说了这么多,回到开头的部分,怎么不用乘法,来实现求相反数呢? - -求 x 的相反数,我们用 0 减去 x 就行。也就是 x 的相反数 = 0 - x = 0 + ( - x ) = -x,-x 在计算机中怎么存的呢,存的是 -x 的补码,-x 的补码怎么求?把 x 按位取反,末位加 1 。Java 中就是 ~x + 1 了,此时所存的就是 x 对应的那个负数,即它的相反数了。 - -3 的相反数怎么求?这求什么求呀,添个负号就行了,-3 呀!但是计算机可没我们这么智能,它只存储 01,所以我们把 -3 的补码求出来存到计算机里就可以了。 即把 3 (0011) 按位取反,末位加 1,得到 1101 就是它的补码,我们然后把 1101 存到了计算机中,我们以为它是 13 ,但我们给计算机重新定义了规则,它是补码,首位就代表了它是负数,计算机根据规则(按位取反,末位加 1 ,再添个负号)把它又还原成了我们所理解的 - 3。从而我们不进行乘法,根据我们给计算机制定的规则,实现了求相反数。 - -```java -public int divide(int dividend, int divisor) { - int ans = 0; - int sign = 1; - if (dividend < 0) { - sign = opposite(sign); - dividend = opposite(dividend); - } - if (divisor < 0) { - sign = opposite(sign); - divisor = opposite(divisor); - } - while (divisor <= dividend) { - ans = ans + 1; - dividend = dividend - divisor; - } - return sign > 0 ? ans : opposite(ans); -} - -public int opposite(int x) { - return ~x + 1; -} -``` - -本来信心满满,结果 Wrong Answer。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/29_2.jpg) - -为什么出错了? -1247483648 这个数有什么特殊之处吗? - -我们知道 int 是用 4 个字节存储,也就是 32 位,那它表示的范围是多少呢?有多少个正数呢?除了第 1 位是 0 固定不变,其它位可以取 0 也可以 取 1,所以是 $$2^{31}$$,但这样的话还就包括了 0 ,所以还得减去 1 个数。也就是 $$2^{31}-1=2147483647$$。那负数有多少个呢,同理除了第 1 位是 1 固定不变,其它位可以取 0 也可以取 1,所以是 $$2^{31}=2147483648$$ 个负数,所以所表示的范围就是 -2147483648 到 2147483647。和之前我们讨论的是一致的,负数比正数多 1 个。 - -算法中,我们首先对被除数 - 2147483648 取相反数,变成了多少呢?这个不好想,那我们看之前的例子,再模 16 的基础上,最小的负数 - 8 ,取相反数变成了多少,- 8 的补码 1000,按位取反,末位加1,0111 + 1 = 1000,又回到了 1000,所以依旧是 - 8。所以题目中的 - 2147483648 取相反数,依旧是 - 2147483648(有没有发现很神奇 - 2147483648 * - 1 依旧是 - 2147483648)。所以上边的算法中,由于被除数依旧是个负数,所以根本没有进 while 循环,所以直接返回了 0 。 - -# 尝试二 - -既然 - 2147483648 这么特殊,那我们对它单独判断吧,如果被除数是 - 2147483648,除数是 -1 ,我们就直接返回题目所要求的 2147483647 吧,并且如果除数是 1 就返回 - 2147483648。 - -```java -public int divide(int dividend, int divisor) { - int ans = 0; - int sign = 1; - - if (dividend < 0) { - sign = opposite(sign); - dividend = opposite(dividend); - } - if (divisor < 0) { - sign = opposite(sign); - divisor = opposite(divisor); - } - //单独判断一下 - if (dividend == Integer.MIN_VALUE && divisor == 1) { - return sign > 0 ? Integer.MAX_VALUE : Integer.MIN_VALUE; - } - while (divisor <= dividend) { - ans = ans + 1; - dividend = dividend - divisor; - } - return sign > 0 ? ans : opposite(ans); -} - -public int opposite(int x) { - return ~x + 1; -} -``` - -接着意外又发生了,这次竟然是 Time Limit Exceeded 了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/29_3.jpg) - -# 尝试三 - -逛了逛 Discuss,由于我们每次只减 1 次除数,循环太多了,找到了[解决方案](https://leetcode.com/problems/divide-two-integers/discuss/13397/Clean-Java-solution-with-some-comment.)。 - -我们每次减 1 次除数,我们其实可以每次减多次。比如 10 / 1 ,之前是 10 - 1 = 9,计数器加 1 变成 1,然后 9 - 1 = 8,计数器加 1 变成 2,然后 8 - 1= 7,计数器加 1 变成 3,直至减到 0 < 1,我们结束了循环。我们其实可以翻倍减, 减完 1 ,减 2 ,再减 4 ,在减 8,当然计数器也不能只加 1 了,减数是翻倍减的,所以计数器也会一直翻倍的加。这里肯定会遇到一个问题,比如 10 - 1 = 9,9 - 2 = 7,7 - 4 = 3,3 - 8 = -5 < 1,它就走出了 while 循环。但是 3 本来还可以减 3 次 1,所以我们只要再递归就可以了。再看 3 / 1 的商,然后把之前的计数器的值加上 3 / 1 的商就够了。 - -```java -public int divide(int dividend, int divisor) { - int ans = 1; - int sign = 1; - if (dividend < 0) { - sign = opposite(sign); - dividend = opposite(dividend); - } - if (divisor < 0) { - sign = opposite(sign); - divisor = opposite(divisor); - } - if (dividend == Integer.MIN_VALUE && divisor == 1) { - return sign > 0 ? Integer.MAX_VALUE : Integer.MIN_VALUE; - } - int origin_dividend = dividend; - int origin_divisor = divisor; - //由于 ans 初始值是 1 ,所以如果被除数小于除数直接返回 0 - if (dividend < divisor) { - return 0; - } - dividend -= divisor; - while (divisor <= dividend) { - ans = ans + ans; - divisor += divisor; - dividend -= divisor; - } - int a = ans + divide(origin_dividend - divisor, origin_divisor); - return sign > 0 ? a : opposite(a); -} -public int opposite(int x) { - return ~x + 1; -} -``` - -不是超时了,神奇的错误又出现了, - -![](https://windliang.oss-cn-beijing.aliyuncs.com/29_4.jpg) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/29_5.jpg) - -我们又看到了,-2147483648 的出现,当除数是它的时候,又出现了神奇的错误,那我们再单独判断一下除数是它,总该可以了吧,继续加上。 - -```java -if(divisor == Integer.MIN_VALUE){ - return 0; -} -``` - -其他的错误又出现了 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/29_6.jpg) - -被除数是 -2147483648 ,咦?我们之前不是考虑了吗,不不不,我们只考虑了除数是 1 和 -1 的时候,所以这个问题其实我们一直没有解决。我们必须修改算法了,我们的算法开始的部分是不管三七二十一,通通转换成正数,而出现 -2147483648 的时候,它无法转换成正数,我们怎么该解决呢? - -# 解法一 - -虽然感觉很投机取巧,但也是最直接的方法了,既然 int 存不了,那我通通用 long 存就行了吧,最后返回的时候看看是不是 int 不能表示的 2147483648,是的话按题目要求就返回 2147483647。 - -```java -public int divide(int dividend, int divisor) { - long ans = divide((long)dividend,(long)(divisor)); - long m = 2147483648L; - if(ans == m ){ - return Integer.MAX_VALUE; - }else{ - return (int)ans; - } -} -public long divide(long dividend, long divisor) { - long ans = 1; - long sign = 1; - if (dividend < 0) { - sign = opposite(sign); - dividend = opposite(dividend); - } - if (divisor < 0) { - sign = opposite(sign); - divisor = opposite(divisor); - } - long origin_dividend = dividend; - long origin_divisor = divisor; - - if (dividend < divisor) { - return 0; - } - - dividend -= divisor; - while (divisor <= dividend) { - ans = ans + ans; - divisor += divisor; - dividend -= divisor; - } - long a = ans + divide(origin_dividend - divisor, origin_divisor); - return sign > 0 ? a : opposite(a); -} -public long opposite(long x) { - return ~x + 1; -} -``` - -时间复杂度:最坏的情况,除数是 1,如果一次一次减除数,那么我们将减 n 次,但由于每次都翻倍了,所以总共减了 log ( n ) 次,所以时间复杂度是 O(log (n))。 - -空间复杂度: O(1)。 - -# 解法二 - -上边的解法总归不够优雅,那么如何不用 long 呢? - -负数比正数多一个,我们之前的思路是把负数变成正数,但由于最小的负数无法变成正数,所以出现了上边奇奇怪怪的问题。我们为什么不把思路转过来,把正数通通转为求负数呢?然后很多加法会变成减法,大于号随之也会变成小于号。 - -```java -public int divide(int dividend, int divisor) { - int ans = -1; - int sign = 1; - if (dividend > 0) { - sign = opposite(sign); - dividend = opposite(dividend); - } - if (divisor > 0) { - sign = opposite(sign); - divisor = opposite(divisor); - } - - int origin_dividend = dividend; - int origin_divisor = divisor; - if (dividend > divisor) { - return 0; - } - - dividend -= divisor; - while (divisor >= dividend) { - ans = ans + ans; - divisor += divisor; - dividend -= divisor; - } - //此时我们传进的是两个负数,正常情况下,它就返回正数,但我们是在用负数累加,所以要取相反数 - int a = ans + opposite(divide(origin_dividend - divisor, origin_divisor)); - if(a == Integer.MIN_VALUE){ - if( sign > 0){ - return Integer.MAX_VALUE; - }else{ - return Integer.MIN_VALUE; - } - }else{ - if(sign > 0){ - return opposite(a); - }else{ - return a; - } - } - } - public int opposite(int x) { - return ~x + 1; - } -} -``` - -时间复杂度和空间复杂度没有变化,但是我们优雅的实现了这个算法,没有借用 long 。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/29.jpg) + +两个数相除,给出商。不能用乘法,除法和模操作。 + +本来觉得这道题蛮简单的,记录下自己的坎坷历程。 + +# 尝试1 + +先确定商的符号,然后把被除数和除数通通转为正数,然后用被除数不停的减除数,直到小于除数的时候,用一个计数遍历记录总共减了多少次,即为商了。 + +确定商的符号的时候,以及返回最终结果的时候,我们可能需要进行乘 -1 操作,即取相反数。而题目规定不让用乘法,所以我们需要知道计算机是怎么进行存数的。 + +计算机为了算减法,利用了同余的性质。 + +同余的定义是 a ≡ b ( mod m ) ,即 a mod m == b mod m ,例如 5 ≡ 17 mod ( 12 )。[百度百科](https://baike.baidu.com/item/%E5%90%8C%E4%BD%99%E5%AE%9A%E7%90%86/1212360?fromtitle=%E5%90%8C%E4%BD%99&fromid=1432545) + +同余有两个性质 + +反身性:a ≡ a ( mod m ); + +同余式相加:若 a ≡ b ( mod m ),c ≡ d ( mod m ),则 a + c ≡ b + d ( mod m ); + +现在我们进行模 16 的加法操作,先熟悉下下边的几个式子。 + +2 + 14 = 0 + +2 + (-3) = 15 + +5 + 15 = 4 + +重点来了! + +计算 4 - 2 怎么算呢? + +也就是 4 + (- 2) + +4 ≡ 4(mod 16) + +-2 ≡ 14(mod 16) + +所以 4 + (- 2)= 4 + 14 = 2。 + +我们利用同余的性质,把减法成功转换成了加法,所以我们只需要在计算机里边将 -2 存成 14 就行了。我们这里减去 2 就等价于加上 14。 + +再比如 13 - 7 ,也就是 13 + (-7) + +13 ≡ 13 (mod 16) + +-7 ≡ 9(mod 16) + +所有 13 + (- 7)= 13 + 9 = 6 + +我们成功把减 7 转换成了加上 9。 + +减 2 转换成加 14,减 7 转换成加 9,这几组数有什么联系呢?是的 2 + 14 = 16,7 + 9 = 16,他们相加通通等于 16,也就是我们取的模。有种互补的感觉,所以我们把 14 叫做 - 2 的补数,9 叫做 - 7 的补数。 + +可以看到,我们用一些正数表示了负数,总共有 16 个数,除去 0,还剩 15 个数,不可避免的是,这 16 个数,正数和负数的个数会相差 1,我们来看看是正数多,还是负数多。 + +| 补数 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | +| ------------ | :--- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | +| 所代表的的数 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 等下 | +| 补数 | | 15 | 14 | 13 | 12 | 11 | 10 | 9 | | +| 所代表的数 | | -1 | -2 | -3 | -4 | -5 | -6 | -7 | | + +上边的列出的数,应该都没异议吧,那么正数多还是负数多呢?就取决于 8 代表多少了。 + +8 + 1 = 9 ,9 代表 -7 ,而 - 8 + 1 = - 7,所以 8 其实代表 - 8 。 + +所以 0 到 15 这 16 个数字可以表示的范围是 -8 ~ 7,-8 没有对称的正数。 + +我们再来看看计算机里是怎么存的,我们都知道,计算机中是以二进制的方式存储的。假设我们计算机能存储 4 位。范围就是 0000 到 1111,也就是 0 到 15。 + +| 补数 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | +| ---------- | :--- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | +| 二进制表示 | 0000 | 0001 | 0010 | 0011 | 0100 | 0101 | 0110 | 0111 | | +| 所代表的数 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | +| 补数 | | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | +| 二进制表示 | | 1111 | 1110 | 1101 | 1100 | 1011 | 1010 | 1001 | 1000 | +| 所代表的数 | | -1 | -2 | -3 | -4 | -5 | -6 | -7 | -8 | + +我们利用这个表格,求几个例子。 + +2 - 3 = 2 + (-3)= 2 的补数 + - 3 的补数 = 0010 + 1101 = 1111 + +而看表格, 1111 代表的数就是 -1 ,从而我们用加法计算出了 2 - 3 = - 1。 + +-3 - 2 = (-3)+(-2)= -3 的补数 + -2 的补数 = 1101 + 1110 = 1011 + +我们可以看到 1101 + 1110 本来等于 1 1011 ,因为只存储 4 位,所以最高位被丢掉了,其实这就进行了取模的操作,减去了 16 。如果我们看所对应的十进制是怎么操作的, 1101 表示 13,1110 表示 14 ,13 + 14 = 27 ,如果是模 16 操作下,就是 11 ,而 11 就是上边的结果 1011,看表格它代表的数是 - 5,- 3 - 2 = - 5 ,没毛病。 + +而且我们发现用这种表示方式,所有的正数首位都是 0 ,负数的首位都是 1 ,我们可以这样想。 + +假设正数和负数的首位相同,假如首位都是 0。 + +那么比如 a = 0010 这个正数,如果我们去找它的相反数 b,也就是对应的负数。由假设可以知道,它的相反数的最高位也是 0。即 0xxx 的形式。为了使得 a 和它的相反数相加等于 0,我们必须使得相反数 b 的第 3 位是 1,即 0x10,才能使得第 3 位的和是 0。但这样的话,第 3 位产生了进位, b 的第 2 位也得是 1,所以 b 就成了 0110,但这样虽然使得最后 3 位的和变成了 0,但是第 1 位我们假设了它是 0,由于第 2 位产生的进位,这样 a 和 b 相加不是 0 了,产生矛盾。所以假设不成立。所以正数和负数的首位一定不同,如果首位 0 代表正数,那么负数的首位一定是 1。 + +接下来的问题,给出一个数我们总不能查表去看它的补码吧,我们如何得出补码? + +对于正数,看表格,我们直接写原码就可以了,例如 7 就是 0111 。 + +负数呢? + +我们之前讨论过,对于模 16 的话,- 2 的补码是 14,也就是 16 - 2。- 7 的补码是 9,也就是 16 - 7 = 9。 + +我们从二进制的方式看一下。 + +我们来求 - 2 的补码,用 16 - 2 = 1 0000 - 0010 = ( 1111 + 1 ) - 0010 = ( 1111 - 0010 ) + 1 = 1101 + 1 = 1110 。 + +为什么转换成 1111 减去一个数,因为用 1111 减去一个数,虽然是减法,但其实只要把这个数按位求反即可。也就是把 2, 0010 按位求反变成 1101,再加上 1 就是 -2 的补码形式了,「按位取反,末位加 1 」这个口诀是不是很熟悉,哈哈,这就是快速求补码的法则。但我们不要忘了它的本质,其实是用模长减去它,但是计算机并不会减法,而是巧妙的转换到了取反再加 1 。 + +逆过程呢?如果我们知道了计算机存了个数 1110,那么它代表多少呢?首先首位是 1 ,它一定是一个负数,其次它是怎么得来的呢?往上翻,其实是用 16 - 2 =1110 得到的,我们现在是准备求 2 ,用 16 减去它就可以了,也就是 16 - 1110 = 1 0000 - 1110 = (1111 + 1)- 1110 = (1111 - 1110) + 1 = 0010。巧了,依旧是按位取反,末位加 1。而 0010 就是 2,所以 1110 就代表 - 2。 + +综上,其实我们就是用原来的一部分正数(其实说它是正数也无非是我们自己定义的,想起一句话,数学就像一门宗教,你要么完全相信,要么完全不信,哈哈)表示了负数,而现在为了实现减法,我们把 1xxx 的不当做正数了,把它定义为负数,是的没有负号,但开头是 1 ,我们就说它是负数,再取个名字就叫补数吧(其实就是它代表的负数离它最近的一个和它同余的数,例如 - 3,和它同余的最近的正数就是 13 了,所以 -3 的补数就是 13),再利用余数定理,以及计算机高位溢出等效于求模的性质,巧妙的用取反以及加法实现了减法。 + +说了这么多,回到开头的部分,怎么不用乘法,来实现求相反数呢? + +求 x 的相反数,我们用 0 减去 x 就行。也就是 x 的相反数 = 0 - x = 0 + ( - x ) = -x,-x 在计算机中怎么存的呢,存的是 -x 的补码,-x 的补码怎么求?把 x 按位取反,末位加 1 。Java 中就是 ~x + 1 了,此时所存的就是 x 对应的那个负数,即它的相反数了。 + +3 的相反数怎么求?这求什么求呀,添个负号就行了,-3 呀!但是计算机可没我们这么智能,它只存储 01,所以我们把 -3 的补码求出来存到计算机里就可以了。 即把 3 (0011) 按位取反,末位加 1,得到 1101 就是它的补码,我们然后把 1101 存到了计算机中,我们以为它是 13 ,但我们给计算机重新定义了规则,它是补码,首位就代表了它是负数,计算机根据规则(按位取反,末位加 1 ,再添个负号)把它又还原成了我们所理解的 - 3。从而我们不进行乘法,根据我们给计算机制定的规则,实现了求相反数。 + +```java +public int divide(int dividend, int divisor) { + int ans = 0; + int sign = 1; + if (dividend < 0) { + sign = opposite(sign); + dividend = opposite(dividend); + } + if (divisor < 0) { + sign = opposite(sign); + divisor = opposite(divisor); + } + while (divisor <= dividend) { + ans = ans + 1; + dividend = dividend - divisor; + } + return sign > 0 ? ans : opposite(ans); +} + +public int opposite(int x) { + return ~x + 1; +} +``` + +本来信心满满,结果 Wrong Answer。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/29_2.jpg) + +为什么出错了? -1247483648 这个数有什么特殊之处吗? + +我们知道 int 是用 4 个字节存储,也就是 32 位,那它表示的范围是多少呢?有多少个正数呢?除了第 1 位是 0 固定不变,其它位可以取 0 也可以 取 1,所以是 $$2^{31}$$,但这样的话还就包括了 0 ,所以还得减去 1 个数。也就是 $$2^{31}-1=2147483647$$。那负数有多少个呢,同理除了第 1 位是 1 固定不变,其它位可以取 0 也可以取 1,所以是 $$2^{31}=2147483648$$ 个负数,所以所表示的范围就是 -2147483648 到 2147483647。和之前我们讨论的是一致的,负数比正数多 1 个。 + +算法中,我们首先对被除数 - 2147483648 取相反数,变成了多少呢?这个不好想,那我们看之前的例子,再模 16 的基础上,最小的负数 - 8 ,取相反数变成了多少,- 8 的补码 1000,按位取反,末位加1,0111 + 1 = 1000,又回到了 1000,所以依旧是 - 8。所以题目中的 - 2147483648 取相反数,依旧是 - 2147483648(有没有发现很神奇 - 2147483648 * - 1 依旧是 - 2147483648)。所以上边的算法中,由于被除数依旧是个负数,所以根本没有进 while 循环,所以直接返回了 0 。 + +# 尝试二 + +既然 - 2147483648 这么特殊,那我们对它单独判断吧,如果被除数是 - 2147483648,除数是 -1 ,我们就直接返回题目所要求的 2147483647 吧,并且如果除数是 1 就返回 - 2147483648。 + +```java +public int divide(int dividend, int divisor) { + int ans = 0; + int sign = 1; + + if (dividend < 0) { + sign = opposite(sign); + dividend = opposite(dividend); + } + if (divisor < 0) { + sign = opposite(sign); + divisor = opposite(divisor); + } + //单独判断一下 + if (dividend == Integer.MIN_VALUE && divisor == 1) { + return sign > 0 ? Integer.MAX_VALUE : Integer.MIN_VALUE; + } + while (divisor <= dividend) { + ans = ans + 1; + dividend = dividend - divisor; + } + return sign > 0 ? ans : opposite(ans); +} + +public int opposite(int x) { + return ~x + 1; +} +``` + +接着意外又发生了,这次竟然是 Time Limit Exceeded 了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/29_3.jpg) + +# 尝试三 + +逛了逛 Discuss,由于我们每次只减 1 次除数,循环太多了,找到了[解决方案](https://leetcode.com/problems/divide-two-integers/discuss/13397/Clean-Java-solution-with-some-comment.)。 + +我们每次减 1 次除数,我们其实可以每次减多次。比如 10 / 1 ,之前是 10 - 1 = 9,计数器加 1 变成 1,然后 9 - 1 = 8,计数器加 1 变成 2,然后 8 - 1= 7,计数器加 1 变成 3,直至减到 0 < 1,我们结束了循环。我们其实可以翻倍减, 减完 1 ,减 2 ,再减 4 ,在减 8,当然计数器也不能只加 1 了,减数是翻倍减的,所以计数器也会一直翻倍的加。这里肯定会遇到一个问题,比如 10 - 1 = 9,9 - 2 = 7,7 - 4 = 3,3 - 8 = -5 < 1,它就走出了 while 循环。但是 3 本来还可以减 3 次 1,所以我们只要再递归就可以了。再看 3 / 1 的商,然后把之前的计数器的值加上 3 / 1 的商就够了。 + +```java +public int divide(int dividend, int divisor) { + int ans = 1; + int sign = 1; + if (dividend < 0) { + sign = opposite(sign); + dividend = opposite(dividend); + } + if (divisor < 0) { + sign = opposite(sign); + divisor = opposite(divisor); + } + if (dividend == Integer.MIN_VALUE && divisor == 1) { + return sign > 0 ? Integer.MAX_VALUE : Integer.MIN_VALUE; + } + int origin_dividend = dividend; + int origin_divisor = divisor; + //由于 ans 初始值是 1 ,所以如果被除数小于除数直接返回 0 + if (dividend < divisor) { + return 0; + } + dividend -= divisor; + while (divisor <= dividend) { + ans = ans + ans; + divisor += divisor; + dividend -= divisor; + } + int a = ans + divide(origin_dividend - divisor, origin_divisor); + return sign > 0 ? a : opposite(a); +} +public int opposite(int x) { + return ~x + 1; +} +``` + +不是超时了,神奇的错误又出现了, + +![](https://windliang.oss-cn-beijing.aliyuncs.com/29_4.jpg) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/29_5.jpg) + +我们又看到了,-2147483648 的出现,当除数是它的时候,又出现了神奇的错误,那我们再单独判断一下除数是它,总该可以了吧,继续加上。 + +```java +if(divisor == Integer.MIN_VALUE){ + return 0; +} +``` + +其他的错误又出现了 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/29_6.jpg) + +被除数是 -2147483648 ,咦?我们之前不是考虑了吗,不不不,我们只考虑了除数是 1 和 -1 的时候,所以这个问题其实我们一直没有解决。我们必须修改算法了,我们的算法开始的部分是不管三七二十一,通通转换成正数,而出现 -2147483648 的时候,它无法转换成正数,我们怎么该解决呢? + +# 解法一 + +虽然感觉很投机取巧,但也是最直接的方法了,既然 int 存不了,那我通通用 long 存就行了吧,最后返回的时候看看是不是 int 不能表示的 2147483648,是的话按题目要求就返回 2147483647。 + +```java +public int divide(int dividend, int divisor) { + long ans = divide((long)dividend,(long)(divisor)); + long m = 2147483648L; + if(ans == m ){ + return Integer.MAX_VALUE; + }else{ + return (int)ans; + } +} +public long divide(long dividend, long divisor) { + long ans = 1; + long sign = 1; + if (dividend < 0) { + sign = opposite(sign); + dividend = opposite(dividend); + } + if (divisor < 0) { + sign = opposite(sign); + divisor = opposite(divisor); + } + long origin_dividend = dividend; + long origin_divisor = divisor; + + if (dividend < divisor) { + return 0; + } + + dividend -= divisor; + while (divisor <= dividend) { + ans = ans + ans; + divisor += divisor; + dividend -= divisor; + } + long a = ans + divide(origin_dividend - divisor, origin_divisor); + return sign > 0 ? a : opposite(a); +} +public long opposite(long x) { + return ~x + 1; +} +``` + +时间复杂度:最坏的情况,除数是 1,如果一次一次减除数,那么我们将减 n 次,但由于每次都翻倍了,所以总共减了 log ( n ) 次,所以时间复杂度是 O(log (n))。 + +空间复杂度: O(1)。 + +# 解法二 + +上边的解法总归不够优雅,那么如何不用 long 呢? + +负数比正数多一个,我们之前的思路是把负数变成正数,但由于最小的负数无法变成正数,所以出现了上边奇奇怪怪的问题。我们为什么不把思路转过来,把正数通通转为求负数呢?然后很多加法会变成减法,大于号随之也会变成小于号。 + +```java +public int divide(int dividend, int divisor) { + int ans = -1; + int sign = 1; + if (dividend > 0) { + sign = opposite(sign); + dividend = opposite(dividend); + } + if (divisor > 0) { + sign = opposite(sign); + divisor = opposite(divisor); + } + + int origin_dividend = dividend; + int origin_divisor = divisor; + if (dividend > divisor) { + return 0; + } + + dividend -= divisor; + while (divisor >= dividend) { + ans = ans + ans; + divisor += divisor; + dividend -= divisor; + } + //此时我们传进的是两个负数,正常情况下,它就返回正数,但我们是在用负数累加,所以要取相反数 + int a = ans + opposite(divide(origin_dividend - divisor, origin_divisor)); + if(a == Integer.MIN_VALUE){ + if( sign > 0){ + return Integer.MAX_VALUE; + }else{ + return Integer.MIN_VALUE; + } + }else{ + if(sign > 0){ + return opposite(a); + }else{ + return a; + } + } + } + public int opposite(int x) { + return ~x + 1; + } +} +``` + +时间复杂度和空间复杂度没有变化,但是我们优雅的实现了这个算法,没有借用 long 。 + +# 总 + 这道题看起来简单,却藏了不少坑。首先,我们用一次一次减造成了超时,然后我们用递归实现了加倍加倍的减,接着由于 int 表示的数的范围不是对称的,最小的负数并不能转换为对应的相反数,所以我们将之前的算法思路完全逆过来,正数边负数,大于变小于,还是蛮有意思的。 \ No newline at end of file diff --git a/leetCode-3-Longest-Substring-Without-Repeating-Characters.md b/leetCode-3-Longest-Substring-Without-Repeating-Characters.md index 3b60b8fc0..9eea91f75 100644 --- a/leetCode-3-Longest-Substring-Without-Repeating-Characters.md +++ b/leetCode-3-Longest-Substring-Without-Repeating-Characters.md @@ -1,156 +1,156 @@ -## 题目描述(中等难度) - -![](http://windliang.oss-cn-beijing.aliyuncs.com/3_long.jpg) - -给定一个字符串,找到没有重复字符的最长子串,返回它的长度。 - -## 解法一 - -简单粗暴些,找一个最长子串,那么我们用两个循环穷举所有子串,然后再用一个函数判断该子串中有没有重复的字符。 - -```JAVA -public int lengthOfLongestSubstring(String s) { - int n = s.length(); - int ans = 0;//保存当前得到满足条件的子串的最大值 - for (int i = 0; i < n; i++) - for (int j = i + 1; j <= n; j++) //之所以 j<= n,是因为我们子串是 [i,j),左闭右开 - if (allUnique(s, i, j)) ans = Math.max(ans, j - i); //更新 ans - return ans; -} - -public boolean allUnique(String s, int start, int end) { - Set set = new HashSet<>();//初始化 hash set - for (int i = start; i < end; i++) {//遍历每个字符 - Character ch = s.charAt(i); - if (set.contains(ch)) return false; //判断字符在不在 set 中 - set.add(ch);//不在的话将该字符添加到 set 里边 - } - return true; -} -``` - -时间复杂度:两个循环,加上判断子串满足不满足条件的函数中的循环,O(n³)。 - -空间复杂度:使用了一个 set,判断子串中有没有重复的字符。由于 set 中没有重复的字符,所以最长就是整个字符集,假设字符集的大小为 m ,那么 set 最长就是 m 。另一方面,如果字符串的长度小于 m ,是 n 。那么 set 最长也就是 n 了。综上,空间复杂度为 O(min(m,n))。 - -## 解法二 - -遗憾的是上边的算法没有通过 leetCode,时间复杂度太大,造成了超时。我们怎么来优化一下呢? - -上边的算法中,我们假设当 i 取 0 的时候, - -j 取 1,判断字符串 str[0,1) 中有没有重复的字符。 - -j 取 2,判断字符串 str[0,2) 中有没有重复的字符。 - -j 取 3,判断字符串 str[0,3) 中有没有重复的字符。 - -j 取 4,判断字符串 str[0,4) 中有没有重复的字符。 - -做了很多重复的工作,因为如果 str[0,3) 中没有重复的字符,我们不需要再判断整个字符串 str[0,4) 中有没有重复的字符,而只需要判断 str[3] 在不在 str[0,3) 中,不在的话,就表明 str[0,4) 中没有重复的字符。 - -如果在的话,那么 str[0,5) ,str[0,6) ,str[0,7) 一定有重复的字符,所以此时后边的 j 也不需要继续增加了。i ++ 进入下次的循环就可以了。 - -此外,我们的 j 也不需要取 j + 1,而只需要从当前的 j 开始就可以了。 - -综上,其实整个关于 j 的循环我们完全可以去掉了,此时可以理解变成了一个「滑动窗口」。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/slide.jpg) - -整体就是橘色窗口在依次向右移动。 - -判断一个字符在不在字符串中,我们需要可以遍历整个字符串,遍历需要的时间复杂度就是 O(n),加上最外层的 i 的循环,总体复杂度就是 O(n²)。我们可以继续优化,判断字符在不在一个字符串,我们可以将已有的字符串存到 Hash 里,这样的时间复杂度是 O(1),总的时间复杂度就变成了 O(n)。 - -```java -public class Solution { - public int lengthOfLongestSubstring(String s) { - int n = s.length(); - Set set = new HashSet<>(); - int ans = 0, i = 0, j = 0; - while (i < n && j < n) { - if (!set.contains(s.charAt(j))){ - set.add(s.charAt(j++)); - ans = Math.max(ans, j - i); - } - else { - set.remove(s.charAt(i++)); - } - } - return ans; - } -} -``` - -时间复杂度:在最坏的情况下,while 循环中的语句会执行 2n 次,例如 abcdefgg,开始的时候 j 一直后移直到到达第二个 g 的时候固定不变 ,然后 i 开始一直后移直到 n ,所以总共执行了 2n 次,时间复杂度为 O(n)。 - -空间复杂度:和上边的类似,需要一个 Hash 保存子串,所以是 O(min(m,n))。 - -## 解法三 - -继续优化,我们看上边的算法的一种情况。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/slide11.jpg) - -当 j 指向的 c 存在于前边的子串 abcd 中,此时 i 向前移到 b ,此时子串中仍然含有 c,还得继续移动,所以这里其实可以优化。我们可以一步到位,直接移动到子串 c 的位置的下一位! - -![](http://windliang.oss-cn-beijing.aliyuncs.com/slide22.jpg) - -实现这样的话,我们将 set 改为 map ,将字符存为 key ,将对应的下标存到 value 里就实现了。 - -```java -public class Solution { - public int lengthOfLongestSubstring(String s) { - int n = s.length(), ans = 0; - Map map = new HashMap<>(); - for (int j = 0, i = 0; j < n; j++) { - if (map.containsKey(s.charAt(j))) { - i = Math.max(map.get(s.charAt(j)), i); - } - ans = Math.max(ans, j - i + 1); - map.put(s.charAt(j), j + 1);//下标 + 1 代表 i 要移动的下个位置 - } - return ans; - } -} -``` - -与解法二相比 - -由于采取了 i 跳跃的形式,所以 map 之前存的字符没有进行 remove ,所以 if 语句中进行了Math.max ( map.get ( s.charAt ( j ) ) , i ),要确认得到的下标不是 i 前边的。 - -还有个不同之处是 j 每次循环都进行了自加 1 ,因为 i 的跳跃已经保证了 str[ i , j] 内没有重复的字符串,所以 j 直接可以加 1 。而解法二中,要保持 j 的位置不变,因为不知道和 j 重复的字符在哪个位置。 - -最后个不同之处是, ans 在每次循环中都进行更新,因为 ans 更新前 i 都进行了更新,已经保证了当前的子串符合条件,所以可以更新 ans 。而解法二中,只有当当前的子串不包含当前的字符时,才进行更新。 - -时间复杂度:我们将 2n 优化到了 n ,但最终还是和之前一样,O(n)。 - -空间复杂度:也是一样的,O(min(m,n))。 - -## 解法四 - -和解法三思路一样,区别的地方在于,我们不用 Hash ,而是直接用数组,字符的 ASCII 码值作为数组的下标,数组存储该字符所在字符串的位置。适用于字符集比较小的情况,因为我们会直接开辟和字符集等大的数组。 - -```java -public class Solution { - public int lengthOfLongestSubstring(String s) { - int n = s.length(), ans = 0; - int[] index = new int[128]; - for (int j = 0, i = 0; j < n; j++) { - i = Math.max(index[s.charAt(j)], i); - ans = Math.max(ans, j - i + 1); - index[s.charAt(j)] = j + 1;//(下标 + 1) 代表 i 要移动的下个位置 - } - return ans; - } -} -``` - -和解法 3 不同的地方在于,没有了 if 的判断,因为如果 index[ s.charAt ( j ) ] 不存在的话,它的值会是 0 ,对最终结果不会影响。 - -时间复杂度:O(n)。 - -空间复杂度:O(m),m 代表字符集的大小。这次不论原字符串多小,都会利用这么大的空间。 - -## 总结 - +## 题目描述(中等难度) + +![](http://windliang.oss-cn-beijing.aliyuncs.com/3_long.jpg) + +给定一个字符串,找到没有重复字符的最长子串,返回它的长度。 + +## 解法一 + +简单粗暴些,找一个最长子串,那么我们用两个循环穷举所有子串,然后再用一个函数判断该子串中有没有重复的字符。 + +```JAVA +public int lengthOfLongestSubstring(String s) { + int n = s.length(); + int ans = 0;//保存当前得到满足条件的子串的最大值 + for (int i = 0; i < n; i++) + for (int j = i + 1; j <= n; j++) //之所以 j<= n,是因为我们子串是 [i,j),左闭右开 + if (allUnique(s, i, j)) ans = Math.max(ans, j - i); //更新 ans + return ans; +} + +public boolean allUnique(String s, int start, int end) { + Set set = new HashSet<>();//初始化 hash set + for (int i = start; i < end; i++) {//遍历每个字符 + Character ch = s.charAt(i); + if (set.contains(ch)) return false; //判断字符在不在 set 中 + set.add(ch);//不在的话将该字符添加到 set 里边 + } + return true; +} +``` + +时间复杂度:两个循环,加上判断子串满足不满足条件的函数中的循环,O(n³)。 + +空间复杂度:使用了一个 set,判断子串中有没有重复的字符。由于 set 中没有重复的字符,所以最长就是整个字符集,假设字符集的大小为 m ,那么 set 最长就是 m 。另一方面,如果字符串的长度小于 m ,是 n 。那么 set 最长也就是 n 了。综上,空间复杂度为 O(min(m,n))。 + +## 解法二 + +遗憾的是上边的算法没有通过 leetCode,时间复杂度太大,造成了超时。我们怎么来优化一下呢? + +上边的算法中,我们假设当 i 取 0 的时候, + +j 取 1,判断字符串 str[0,1) 中有没有重复的字符。 + +j 取 2,判断字符串 str[0,2) 中有没有重复的字符。 + +j 取 3,判断字符串 str[0,3) 中有没有重复的字符。 + +j 取 4,判断字符串 str[0,4) 中有没有重复的字符。 + +做了很多重复的工作,因为如果 str[0,3) 中没有重复的字符,我们不需要再判断整个字符串 str[0,4) 中有没有重复的字符,而只需要判断 str[3] 在不在 str[0,3) 中,不在的话,就表明 str[0,4) 中没有重复的字符。 + +如果在的话,那么 str[0,5) ,str[0,6) ,str[0,7) 一定有重复的字符,所以此时后边的 j 也不需要继续增加了。i ++ 进入下次的循环就可以了。 + +此外,我们的 j 也不需要取 j + 1,而只需要从当前的 j 开始就可以了。 + +综上,其实整个关于 j 的循环我们完全可以去掉了,此时可以理解变成了一个「滑动窗口」。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/slide.jpg) + +整体就是橘色窗口在依次向右移动。 + +判断一个字符在不在字符串中,我们需要可以遍历整个字符串,遍历需要的时间复杂度就是 O(n),加上最外层的 i 的循环,总体复杂度就是 O(n²)。我们可以继续优化,判断字符在不在一个字符串,我们可以将已有的字符串存到 Hash 里,这样的时间复杂度是 O(1),总的时间复杂度就变成了 O(n)。 + +```java +public class Solution { + public int lengthOfLongestSubstring(String s) { + int n = s.length(); + Set set = new HashSet<>(); + int ans = 0, i = 0, j = 0; + while (i < n && j < n) { + if (!set.contains(s.charAt(j))){ + set.add(s.charAt(j++)); + ans = Math.max(ans, j - i); + } + else { + set.remove(s.charAt(i++)); + } + } + return ans; + } +} +``` + +时间复杂度:在最坏的情况下,while 循环中的语句会执行 2n 次,例如 abcdefgg,开始的时候 j 一直后移直到到达第二个 g 的时候固定不变 ,然后 i 开始一直后移直到 n ,所以总共执行了 2n 次,时间复杂度为 O(n)。 + +空间复杂度:和上边的类似,需要一个 Hash 保存子串,所以是 O(min(m,n))。 + +## 解法三 + +继续优化,我们看上边的算法的一种情况。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/slide11.jpg) + +当 j 指向的 c 存在于前边的子串 abcd 中,此时 i 向前移到 b ,此时子串中仍然含有 c,还得继续移动,所以这里其实可以优化。我们可以一步到位,直接移动到子串 c 的位置的下一位! + +![](http://windliang.oss-cn-beijing.aliyuncs.com/slide22.jpg) + +实现这样的话,我们将 set 改为 map ,将字符存为 key ,将对应的下标存到 value 里就实现了。 + +```java +public class Solution { + public int lengthOfLongestSubstring(String s) { + int n = s.length(), ans = 0; + Map map = new HashMap<>(); + for (int j = 0, i = 0; j < n; j++) { + if (map.containsKey(s.charAt(j))) { + i = Math.max(map.get(s.charAt(j)), i); + } + ans = Math.max(ans, j - i + 1); + map.put(s.charAt(j), j + 1);//下标 + 1 代表 i 要移动的下个位置 + } + return ans; + } +} +``` + +与解法二相比 + +由于采取了 i 跳跃的形式,所以 map 之前存的字符没有进行 remove ,所以 if 语句中进行了Math.max ( map.get ( s.charAt ( j ) ) , i ),要确认得到的下标不是 i 前边的。 + +还有个不同之处是 j 每次循环都进行了自加 1 ,因为 i 的跳跃已经保证了 str[ i , j] 内没有重复的字符串,所以 j 直接可以加 1 。而解法二中,要保持 j 的位置不变,因为不知道和 j 重复的字符在哪个位置。 + +最后个不同之处是, ans 在每次循环中都进行更新,因为 ans 更新前 i 都进行了更新,已经保证了当前的子串符合条件,所以可以更新 ans 。而解法二中,只有当当前的子串不包含当前的字符时,才进行更新。 + +时间复杂度:我们将 2n 优化到了 n ,但最终还是和之前一样,O(n)。 + +空间复杂度:也是一样的,O(min(m,n))。 + +## 解法四 + +和解法三思路一样,区别的地方在于,我们不用 Hash ,而是直接用数组,字符的 ASCII 码值作为数组的下标,数组存储该字符所在字符串的位置。适用于字符集比较小的情况,因为我们会直接开辟和字符集等大的数组。 + +```java +public class Solution { + public int lengthOfLongestSubstring(String s) { + int n = s.length(), ans = 0; + int[] index = new int[128]; + for (int j = 0, i = 0; j < n; j++) { + i = Math.max(index[s.charAt(j)], i); + ans = Math.max(ans, j - i + 1); + index[s.charAt(j)] = j + 1;//(下标 + 1) 代表 i 要移动的下个位置 + } + return ans; + } +} +``` + +和解法 3 不同的地方在于,没有了 if 的判断,因为如果 index[ s.charAt ( j ) ] 不存在的话,它的值会是 0 ,对最终结果不会影响。 + +时间复杂度:O(n)。 + +空间复杂度:O(m),m 代表字符集的大小。这次不论原字符串多小,都会利用这么大的空间。 + +## 总结 + 综上,我们一步一步的寻求可优化的地方,对算法进行了优化。又加深了 Hash 的应用,以及利用数组巧妙的实现了 Hash 的作用。 \ No newline at end of file diff --git a/leetCode-30-Substring-with-Concatenation-of-All-Words.md b/leetCode-30-Substring-with-Concatenation-of-All-Words.md index 48a412230..8a5c2c99a 100644 --- a/leetCode-30-Substring-with-Concatenation-of-All-Words.md +++ b/leetCode-30-Substring-with-Concatenation-of-All-Words.md @@ -1,211 +1,211 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/30.jpg) - -给定一个字符串 s ,给定 n 个单词 word,找出所有子串的开始下标,使得子串包含了给定的所有单词,顺序可以不对应。如果有重复的单词,比如有 [ " foo " , " foo " ] 那么子串也必须含有两个 " foo ",也就是说个数必须相同。 - -# 解法一 - -参考 leetCode 里的 [solution](https://leetcode.com/problems/substring-with-concatenation-of-all-words/discuss/13658/Easy-Two-Map-Solution-(C%2B%2BJava)) - -首先,最直接的思路,判断每个子串是否符合,符合就把下标保存起来,最后返回即可。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/30_1.jpg) - -如上图,利用循环变量 i ,依次后移,判断每个子串是否符合即可。 - -怎么判断子串是否符合?这也是这个题的难点了,由于子串包含的单词顺序并不需要固定,如果是两个单词 A,B,我们只需要判断子串是否是 AB 或者 BA 即可。如果是三个单词 A,B,C 也还好,只需要判断子串是否是 ABC,或者 ACB,BAC,BCA,CAB,CBA 就可以了,但如果更多单词呢?那就崩溃了。 - -[链接](https://leetcode.com/problems/substring-with-concatenation-of-all-words/discuss/13658/Easy-Two-Map-Solution-(C%2B%2BJava))的作者提出了,用两个 HashMap 来解决。首先,我们把所有的单词存到 HashMap 里,key 直接存单词,value 存单词出现的个数(因为给出的单词可能会有重复的,所以可能是 1 或 2 或者其他)。然后扫描子串的单词,如果当前扫描的单词在之前的 HashMap 中,就把该单词存到新的 HashMap 中,并判断新的 HashMap 中该单词的 value 是不是大于之前的 HashMap 该单词的 value ,如果大了,就代表该子串不是我们要找的,接着判断下一个子串就可以了。如果不大于,那么我们接着判断下一个单词的情况。子串扫描结束,如果子串的全部单词都符合,那么该子串就是我们找的其中一个。看下具体的例子。 - -看下图,我们把 words 存到一个 HashMap 中。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/30_2.jpg) - -然后遍历子串的每个单词。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/30_3.jpg) - -第一个单词在 HashMap1 中,然后我们把 foo 存到 HashMap2 中。并且比较此时 foo 的 value 和 HashMap1 中 foo 的 value,1 < 2,所以我们继续扫描。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/30_4.jpg) - -第二个单词也在 HashMap1 中,然后把 foo 存到 HashMap2 中,因为之前已经存过了,所以更新它的 value 为 2 ,然后继续比较此时 foo 的 value 和 HashMap1 中 foo 的 value,2 <= 2,所以继续扫描下一个单词。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/30_5.jpg) - -第三个单词也在 HashMap1 中,然后把 foo 存到 HashMap2 中,因为之前已经存过了,所以更新它的 value 为 3,然后继续比较此时 foo 的 value 和 HashMap1 中 foo 的 value,3 > 2,所以表明该字符串不符合。然后判断下个子串就好了。 - -当然上边的情况都是单词在 HashMap1 中,如果不在的话就更好说了,不在就表明当前子串肯定不符合了,直接判断下个子串就好了。 - -看一下代码吧 - -```java -public List findSubstring(String s, String[] words) { - List res = new ArrayList(); - int wordNum = words.length; - if (wordNum == 0) { - return res; - } - int wordLen = words[0].length(); - //HashMap1 存所有单词 - HashMap allWords = new HashMap(); - for (String w : words) { - int value = allWords.getOrDefault(w, 0); - allWords.put(w, value + 1); - } - //遍历所有子串 - for (int i = 0; i < s.length() - wordNum * wordLen + 1; i++) { - //HashMap2 存当前扫描的字符串含有的单词 - HashMap hasWords = new HashMap(); - int num = 0; - //判断该子串是否符合 - while (num < wordNum) { - String word = s.substring(i + num * wordLen, i + (num + 1) * wordLen); - //判断该单词在 HashMap1 中 - if (allWords.containsKey(word)) { - int value = hasWords.getOrDefault(word, 0); - hasWords.put(word, value + 1); - //判断当前单词的 value 和 HashMap1 中该单词的 value - if (hasWords.get(word) > allWords.get(word)) { - break; - } - } else { - break; - } - num++; - } - //判断是不是所有的单词都符合条件 - if (num == wordNum) { - res.add(i); - } - } - return res; -} -``` - -时间复杂度:假设 s 的长度是 n,words 里有 m 个单词,那么时间复杂度就是 O(n * m)。 - -空间复杂度:两个 HashMap,假设 words 里有 m 个单词,就是 O(m)。 - -# 解法二 - -参考 https://leetcode.com/problems/substring-with-concatenation-of-all-words/discuss/13656/An-O(N)-solution-with-detailed-explanation。 - -我们在解法一中,每次移动一个字符。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/30_1.jpg) - -现在为了方便讨论,我们每次移动一个单词的长度,也就是 3 个字符,这样所有的移动被分成了三类。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/30_6.jpg) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/30_7.jpg) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/30_8.jpg) - -以上三类我们以第一类从 0 开始移动为例,讲一下如何对算法进行优化,有三种需要优化的情况。 - -* 情况一:当子串完全匹配,移动到下一个子串的时候。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/30_9.jpg) - - 在解法一中,对于 i = 3 的子串,我们肯定是从第一个 foo 开始判断。但其实前两个 foo 都不用判断了 ,因为在判断上一个 i = 0 的子串的时候我们已经判断过了。所以解法一中的 HashMap2 每次并不需要清空从 0 开始,而是可以只移除之前 i = 0 子串的第一个单词 bar 即可,然后直接从箭头所指的 foo 开始就可以了。 - -* 情况二:当判断过程中,出现不符合的单词。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/30_10.jpg) - - 但判断 i = 0 的子串的时候,出现了 the ,并不在所给的单词中。所以此时 i = 3,i = 6 的子串,我们其实并不需要判断了。我们直接判断 i = 9 的情况就可以了。 - -* 情况三:判断过程中,出现的是符合的单词,但是次数超了。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/30_11.jpg) - - 对于 i = 0 的子串,此时判断的 bar 其实是在 words 中的,但是之前已经出现了一次 bar,所以 i = 0 的子串是不符合要求的。此时我们只需要往后移动窗口,i = 3 的子串将 foo 移除,此时子串中一定还是有两个 bar,所以该子串也一定不符合。接着往后移动,当之前的 bar 被移除后,此时 i = 6 的子串,就可以接着按正常的方法判断了。 - - 所以对于出现 i = 0 的子串的情况,我们可以直接从 HashMap2 中依次移除单词,当移除了之前次数超的单词的时候,我们就可以正常判断了,直接从移除了超出了次数的单词后,也就是 i = 6 开始判断就可以了。 - - 看一下代码吧。 - - ```java - public List findSubstring(String s, String[] words) { - List res = new ArrayList(); - int wordNum = words.length; - if (wordNum == 0) { - return res; - } - int wordLen = words[0].length(); - HashMap allWords = new HashMap(); - for (String w : words) { - int value = allWords.getOrDefault(w, 0); - allWords.put(w, value + 1); - } - //将所有移动分成 wordLen 类情况 - for (int j = 0; j < wordLen; j++) { - HashMap hasWords = new HashMap(); - int num = 0; //记录当前 HashMap2(这里的 hasWords 变量)中有多少个单词 - //每次移动一个单词长度 - for (int i = j; i < s.length() - wordNum * wordLen + 1; i = i + wordLen) { - boolean hasRemoved = false; //防止情况三移除后,情况一继续移除 - while (num < wordNum) { - String word = s.substring(i + num * wordLen, i + (num + 1) * wordLen); - if (allWords.containsKey(word)) { - int value = hasWords.getOrDefault(word, 0); - hasWords.put(word, value + 1); - //出现情况三,遇到了符合的单词,但是次数超了 - if (hasWords.get(word) > allWords.get(word)) { - // hasWords.put(word, value); - hasRemoved = true; - int removeNum = 0; - //一直移除单词,直到次数符合了 - while (hasWords.get(word) > allWords.get(word)) { - String firstWord = s.substring(i + removeNum * wordLen, i + (removeNum + 1) * wordLen); - int v = hasWords.get(firstWord); - hasWords.put(firstWord, v - 1); - removeNum++; - } - num = num - removeNum + 1; //加 1 是因为我们把当前单词加入到了 HashMap 2 中 - i = i + (removeNum - 1) * wordLen; //这里依旧是考虑到了最外层的 for 循环,看情况二的解释 - break; - } - //出现情况二,遇到了不匹配的单词,直接将 i 移动到该单词的后边(但其实这里 - //只是移动到了出现问题单词的地方,因为最外层有 for 循环, i 还会移动一个单词 - //然后刚好就移动到了单词后边) - } else { - hasWords.clear(); - i = i + num * wordLen; - num = 0; - break; - } - num++; - } - if (num == wordNum) { - res.add(i); - - } - //出现情况一,子串完全匹配,我们将上一个子串的第一个单词从 HashMap2 中移除 - if (num > 0 && !hasRemoved) { - String firstWord = s.substring(i, i + wordLen); - int v = hasWords.get(firstWord); - hasWords.put(firstWord, v - 1); - num = num - 1; - } - - } - - } - return res; - } - - ``` - - 时间复杂度:算法中外层的两个for 循环的次数肯定是所有的子串,假设是 n。考虑一下,最极端的情况,每个子串的判断都进了 while 循环,wordNum 等于 m。对于解法一,因为每次都是从头判断,所以 while 循环循环了 m 次。但这里我们由于没有清空,所以每次只判断新加入的单词就可以了,只需判断一次,所以时间复杂度是 O(n)。 - - 或者换一种理解方式,判断子串是否符合,本质上也就是判断每个单词符不符合,假设 s 的长度是 n,那么就会大约有 n 个子串,也就是会有 n 个单词。而对于每个单词,我们只有刚开始判断符不符合的时候访问一次,还有就是把它移除的时候访问一次,所以每个单词最多访问 2 次,所以时间复杂度是 O(n)。 - - 空间复杂度:没有变化,依旧是两个 HashMap, 假设 words 里有 m 个单词,就是 O(m)。 - - # 总 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/30.jpg) + +给定一个字符串 s ,给定 n 个单词 word,找出所有子串的开始下标,使得子串包含了给定的所有单词,顺序可以不对应。如果有重复的单词,比如有 [ " foo " , " foo " ] 那么子串也必须含有两个 " foo ",也就是说个数必须相同。 + +# 解法一 + +参考 leetCode 里的 [solution](https://leetcode.com/problems/substring-with-concatenation-of-all-words/discuss/13658/Easy-Two-Map-Solution-(C%2B%2BJava)) + +首先,最直接的思路,判断每个子串是否符合,符合就把下标保存起来,最后返回即可。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/30_1.jpg) + +如上图,利用循环变量 i ,依次后移,判断每个子串是否符合即可。 + +怎么判断子串是否符合?这也是这个题的难点了,由于子串包含的单词顺序并不需要固定,如果是两个单词 A,B,我们只需要判断子串是否是 AB 或者 BA 即可。如果是三个单词 A,B,C 也还好,只需要判断子串是否是 ABC,或者 ACB,BAC,BCA,CAB,CBA 就可以了,但如果更多单词呢?那就崩溃了。 + +[链接](https://leetcode.com/problems/substring-with-concatenation-of-all-words/discuss/13658/Easy-Two-Map-Solution-(C%2B%2BJava))的作者提出了,用两个 HashMap 来解决。首先,我们把所有的单词存到 HashMap 里,key 直接存单词,value 存单词出现的个数(因为给出的单词可能会有重复的,所以可能是 1 或 2 或者其他)。然后扫描子串的单词,如果当前扫描的单词在之前的 HashMap 中,就把该单词存到新的 HashMap 中,并判断新的 HashMap 中该单词的 value 是不是大于之前的 HashMap 该单词的 value ,如果大了,就代表该子串不是我们要找的,接着判断下一个子串就可以了。如果不大于,那么我们接着判断下一个单词的情况。子串扫描结束,如果子串的全部单词都符合,那么该子串就是我们找的其中一个。看下具体的例子。 + +看下图,我们把 words 存到一个 HashMap 中。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/30_2.jpg) + +然后遍历子串的每个单词。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/30_3.jpg) + +第一个单词在 HashMap1 中,然后我们把 foo 存到 HashMap2 中。并且比较此时 foo 的 value 和 HashMap1 中 foo 的 value,1 < 2,所以我们继续扫描。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/30_4.jpg) + +第二个单词也在 HashMap1 中,然后把 foo 存到 HashMap2 中,因为之前已经存过了,所以更新它的 value 为 2 ,然后继续比较此时 foo 的 value 和 HashMap1 中 foo 的 value,2 <= 2,所以继续扫描下一个单词。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/30_5.jpg) + +第三个单词也在 HashMap1 中,然后把 foo 存到 HashMap2 中,因为之前已经存过了,所以更新它的 value 为 3,然后继续比较此时 foo 的 value 和 HashMap1 中 foo 的 value,3 > 2,所以表明该字符串不符合。然后判断下个子串就好了。 + +当然上边的情况都是单词在 HashMap1 中,如果不在的话就更好说了,不在就表明当前子串肯定不符合了,直接判断下个子串就好了。 + +看一下代码吧 + +```java +public List findSubstring(String s, String[] words) { + List res = new ArrayList(); + int wordNum = words.length; + if (wordNum == 0) { + return res; + } + int wordLen = words[0].length(); + //HashMap1 存所有单词 + HashMap allWords = new HashMap(); + for (String w : words) { + int value = allWords.getOrDefault(w, 0); + allWords.put(w, value + 1); + } + //遍历所有子串 + for (int i = 0; i < s.length() - wordNum * wordLen + 1; i++) { + //HashMap2 存当前扫描的字符串含有的单词 + HashMap hasWords = new HashMap(); + int num = 0; + //判断该子串是否符合 + while (num < wordNum) { + String word = s.substring(i + num * wordLen, i + (num + 1) * wordLen); + //判断该单词在 HashMap1 中 + if (allWords.containsKey(word)) { + int value = hasWords.getOrDefault(word, 0); + hasWords.put(word, value + 1); + //判断当前单词的 value 和 HashMap1 中该单词的 value + if (hasWords.get(word) > allWords.get(word)) { + break; + } + } else { + break; + } + num++; + } + //判断是不是所有的单词都符合条件 + if (num == wordNum) { + res.add(i); + } + } + return res; +} +``` + +时间复杂度:假设 s 的长度是 n,words 里有 m 个单词,那么时间复杂度就是 O(n * m)。 + +空间复杂度:两个 HashMap,假设 words 里有 m 个单词,就是 O(m)。 + +# 解法二 + +参考 https://leetcode.com/problems/substring-with-concatenation-of-all-words/discuss/13656/An-O(N)-solution-with-detailed-explanation。 + +我们在解法一中,每次移动一个字符。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/30_1.jpg) + +现在为了方便讨论,我们每次移动一个单词的长度,也就是 3 个字符,这样所有的移动被分成了三类。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/30_6.jpg) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/30_7.jpg) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/30_8.jpg) + +以上三类我们以第一类从 0 开始移动为例,讲一下如何对算法进行优化,有三种需要优化的情况。 + +* 情况一:当子串完全匹配,移动到下一个子串的时候。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/30_9.jpg) + + 在解法一中,对于 i = 3 的子串,我们肯定是从第一个 foo 开始判断。但其实前两个 foo 都不用判断了 ,因为在判断上一个 i = 0 的子串的时候我们已经判断过了。所以解法一中的 HashMap2 每次并不需要清空从 0 开始,而是可以只移除之前 i = 0 子串的第一个单词 bar 即可,然后直接从箭头所指的 foo 开始就可以了。 + +* 情况二:当判断过程中,出现不符合的单词。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/30_10.jpg) + + 但判断 i = 0 的子串的时候,出现了 the ,并不在所给的单词中。所以此时 i = 3,i = 6 的子串,我们其实并不需要判断了。我们直接判断 i = 9 的情况就可以了。 + +* 情况三:判断过程中,出现的是符合的单词,但是次数超了。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/30_11.jpg) + + 对于 i = 0 的子串,此时判断的 bar 其实是在 words 中的,但是之前已经出现了一次 bar,所以 i = 0 的子串是不符合要求的。此时我们只需要往后移动窗口,i = 3 的子串将 foo 移除,此时子串中一定还是有两个 bar,所以该子串也一定不符合。接着往后移动,当之前的 bar 被移除后,此时 i = 6 的子串,就可以接着按正常的方法判断了。 + + 所以对于出现 i = 0 的子串的情况,我们可以直接从 HashMap2 中依次移除单词,当移除了之前次数超的单词的时候,我们就可以正常判断了,直接从移除了超出了次数的单词后,也就是 i = 6 开始判断就可以了。 + + 看一下代码吧。 + + ```java + public List findSubstring(String s, String[] words) { + List res = new ArrayList(); + int wordNum = words.length; + if (wordNum == 0) { + return res; + } + int wordLen = words[0].length(); + HashMap allWords = new HashMap(); + for (String w : words) { + int value = allWords.getOrDefault(w, 0); + allWords.put(w, value + 1); + } + //将所有移动分成 wordLen 类情况 + for (int j = 0; j < wordLen; j++) { + HashMap hasWords = new HashMap(); + int num = 0; //记录当前 HashMap2(这里的 hasWords 变量)中有多少个单词 + //每次移动一个单词长度 + for (int i = j; i < s.length() - wordNum * wordLen + 1; i = i + wordLen) { + boolean hasRemoved = false; //防止情况三移除后,情况一继续移除 + while (num < wordNum) { + String word = s.substring(i + num * wordLen, i + (num + 1) * wordLen); + if (allWords.containsKey(word)) { + int value = hasWords.getOrDefault(word, 0); + hasWords.put(word, value + 1); + //出现情况三,遇到了符合的单词,但是次数超了 + if (hasWords.get(word) > allWords.get(word)) { + // hasWords.put(word, value); + hasRemoved = true; + int removeNum = 0; + //一直移除单词,直到次数符合了 + while (hasWords.get(word) > allWords.get(word)) { + String firstWord = s.substring(i + removeNum * wordLen, i + (removeNum + 1) * wordLen); + int v = hasWords.get(firstWord); + hasWords.put(firstWord, v - 1); + removeNum++; + } + num = num - removeNum + 1; //加 1 是因为我们把当前单词加入到了 HashMap 2 中 + i = i + (removeNum - 1) * wordLen; //这里依旧是考虑到了最外层的 for 循环,看情况二的解释 + break; + } + //出现情况二,遇到了不匹配的单词,直接将 i 移动到该单词的后边(但其实这里 + //只是移动到了出现问题单词的地方,因为最外层有 for 循环, i 还会移动一个单词 + //然后刚好就移动到了单词后边) + } else { + hasWords.clear(); + i = i + num * wordLen; + num = 0; + break; + } + num++; + } + if (num == wordNum) { + res.add(i); + + } + //出现情况一,子串完全匹配,我们将上一个子串的第一个单词从 HashMap2 中移除 + if (num > 0 && !hasRemoved) { + String firstWord = s.substring(i, i + wordLen); + int v = hasWords.get(firstWord); + hasWords.put(firstWord, v - 1); + num = num - 1; + } + + } + + } + return res; + } + + ``` + + 时间复杂度:算法中外层的两个for 循环的次数肯定是所有的子串,假设是 n。考虑一下,最极端的情况,每个子串的判断都进了 while 循环,wordNum 等于 m。对于解法一,因为每次都是从头判断,所以 while 循环循环了 m 次。但这里我们由于没有清空,所以每次只判断新加入的单词就可以了,只需判断一次,所以时间复杂度是 O(n)。 + + 或者换一种理解方式,判断子串是否符合,本质上也就是判断每个单词符不符合,假设 s 的长度是 n,那么就会大约有 n 个子串,也就是会有 n 个单词。而对于每个单词,我们只有刚开始判断符不符合的时候访问一次,还有就是把它移除的时候访问一次,所以每个单词最多访问 2 次,所以时间复杂度是 O(n)。 + + 空间复杂度:没有变化,依旧是两个 HashMap, 假设 words 里有 m 个单词,就是 O(m)。 + + # 总 + 这道题最大的亮点就是应用了 HashMap 了吧,使得我们不再纠结于子串包含单词的顺序。然后对于算法的优化上,还是老思路,去分析哪些判断是不必要的,然后把它除之。 \ No newline at end of file diff --git a/leetCode-31-Next-Permutation.md b/leetCode-31-Next-Permutation.md index 9e559eadb..6590075aa 100644 --- a/leetCode-31-Next-Permutation.md +++ b/leetCode-31-Next-Permutation.md @@ -1,88 +1,88 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/31.jpg) - -这道题的的难度我觉得理解题意就占了一半。题目的意思是给定一个数,然后将这些数字的位置重新排列,得到一个刚好比原数字大的一种排列。如果没有比原数字大的,就升序输出。 - -关键就是刚好是什么意思?比如说原数字是 A,然后将原数字的每位重新排列产生了 B C D E,然后把这 5 个数字从小到大排列,比如是 D **A** B E C ,那么,我们要找的就是 B,就是那个刚好比 A 大的数字。 - -再比如 123,其他排列有 132,213,231,312,321,从小到大排列就是 **123** 132 213 231 312 321,那么我们要找的就是 132。 - -题目还要求空间复杂度必须是 O(1)。 - -#解法一 - -我们想几个问题。 - -要想使得数字变大,只要任意一位变大就可以。 - -要想得到刚好大于原来的数字,要变个位。 - -这里变大数字,只能利用交换。 - -如果从个位开始,从右往左进行,找一个比个位大的,交换过来,个位的数字交换到了更高位,由于个位的数字较小,所以交换过去虽然个位变大了,但数字整体变小了。例如 1 3 2,把 2 和 3 交换,变成 1 2 3,个位变大了,但整体数字变小了。 - -个位不行,我们再看十位,如果从十位左边找一个更大的数字交换过来,和个位的情况是一样的,数字会变小。例如 4 1 2 3,把 2 和 4 交换,2 1 4 3,数字会变小。如果从右边找一个更大的数字交换过来,由于是从低位交换过来的,所以数字满足了会变大。如 4 1 2 3,把 2 和 3 交换,变成 4 1 3 2 数字变大了。 - -如果十位右边没有比十位数字大的,我们就左移看下一位,再看当前位右边,有没有更大的数字,没有就一直左移就可以。 - -还有一个问题,如果右边有不止一个大于当前位的数字选哪个?选那个刚好大于当前位的,这样会保证数字整体尽可能的小。 - -交换完结束了吗?并没有。因为交换完数字变大了,但并不一定是刚好大于原数字的。例如 158476531,我们从十位开始,十位右边没有大于 3 的。再看百位,百位右边没有大于 5 的。直到 4 ,右边出现了很多大于 4 的,选那个刚好大于 4 的,也就是 5 。然后交换,变成 158**5**76**4**31,数字变大了,但并不是刚好大于 158476531,我们还需要将 5 右边的数字从小到大排列。变成158**5**13467,就可以结束了。 - -而最后的排序,我们其实并不需要用排序函数,因为交换的位置也就是 5 的右边的数字一定是降序的,我们只需要倒序即可了。看一下 LeetCode 提供的动图更好理解一些。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/31_Next_Permutation.gif) - -再看这个过程,我们其实是从右向左找到第一个数字不再递增的位置,然后从右边找到一个刚好大于当前位的数字即可。 - -再看下代码吧。 - -```java -public void nextPermutation(int[] nums) { - int i = nums.length - 2; - //找到第一个不再递增的位置 - while (i >= 0 && nums[i + 1] <= nums[i]) { - i--; - } - //如果到了最左边,就直接倒置输出 - if (i < 0) { - reverse(nums, 0); - return; - } - //找到刚好大于 nums[i]的位置 - int j = nums.length - 1; - while (j >= 0 && nums[j] <= nums[i]) { - j--; - } - //交换 - swap(nums, i, j); - //利用倒置进行排序 - reverse(nums, i + 1); - -} - -private void swap(int[] nums, int i, int j) { - int temp = nums[j]; - nums[j] = nums[i]; - nums[i] = temp; -} - -private void reverse(int[] nums, int start) { - int i = start, j = nums.length - 1; - while (i < j) { - swap(nums, i, j); - i++; - j--; - } -} -``` - -时间复杂度:最坏的情况就是遍历完所有位,O(n),倒置也是 O(n),所以总体依旧是 O(n)。 - -空间复杂度:O(1)。 - -# 总 - -开始看题的时候一直没理解,后来理解了题试了几种也没想出来,然后看了 Solution,理了下思路。 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/31.jpg) + +这道题的的难度我觉得理解题意就占了一半。题目的意思是给定一个数,然后将这些数字的位置重新排列,得到一个刚好比原数字大的一种排列。如果没有比原数字大的,就升序输出。 + +关键就是刚好是什么意思?比如说原数字是 A,然后将原数字的每位重新排列产生了 B C D E,然后把这 5 个数字从小到大排列,比如是 D **A** B E C ,那么,我们要找的就是 B,就是那个刚好比 A 大的数字。 + +再比如 123,其他排列有 132,213,231,312,321,从小到大排列就是 **123** 132 213 231 312 321,那么我们要找的就是 132。 + +题目还要求空间复杂度必须是 O(1)。 + +#解法一 + +我们想几个问题。 + +要想使得数字变大,只要任意一位变大就可以。 + +要想得到刚好大于原来的数字,要变个位。 + +这里变大数字,只能利用交换。 + +如果从个位开始,从右往左进行,找一个比个位大的,交换过来,个位的数字交换到了更高位,由于个位的数字较小,所以交换过去虽然个位变大了,但数字整体变小了。例如 1 3 2,把 2 和 3 交换,变成 1 2 3,个位变大了,但整体数字变小了。 + +个位不行,我们再看十位,如果从十位左边找一个更大的数字交换过来,和个位的情况是一样的,数字会变小。例如 4 1 2 3,把 2 和 4 交换,2 1 4 3,数字会变小。如果从右边找一个更大的数字交换过来,由于是从低位交换过来的,所以数字满足了会变大。如 4 1 2 3,把 2 和 3 交换,变成 4 1 3 2 数字变大了。 + +如果十位右边没有比十位数字大的,我们就左移看下一位,再看当前位右边,有没有更大的数字,没有就一直左移就可以。 + +还有一个问题,如果右边有不止一个大于当前位的数字选哪个?选那个刚好大于当前位的,这样会保证数字整体尽可能的小。 + +交换完结束了吗?并没有。因为交换完数字变大了,但并不一定是刚好大于原数字的。例如 158476531,我们从十位开始,十位右边没有大于 3 的。再看百位,百位右边没有大于 5 的。直到 4 ,右边出现了很多大于 4 的,选那个刚好大于 4 的,也就是 5 。然后交换,变成 158**5**76**4**31,数字变大了,但并不是刚好大于 158476531,我们还需要将 5 右边的数字从小到大排列。变成158**5**13467,就可以结束了。 + +而最后的排序,我们其实并不需要用排序函数,因为交换的位置也就是 5 的右边的数字一定是降序的,我们只需要倒序即可了。看一下 LeetCode 提供的动图更好理解一些。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/31_Next_Permutation.gif) + +再看这个过程,我们其实是从右向左找到第一个数字不再递增的位置,然后从右边找到一个刚好大于当前位的数字即可。 + +再看下代码吧。 + +```java +public void nextPermutation(int[] nums) { + int i = nums.length - 2; + //找到第一个不再递增的位置 + while (i >= 0 && nums[i + 1] <= nums[i]) { + i--; + } + //如果到了最左边,就直接倒置输出 + if (i < 0) { + reverse(nums, 0); + return; + } + //找到刚好大于 nums[i]的位置 + int j = nums.length - 1; + while (j >= 0 && nums[j] <= nums[i]) { + j--; + } + //交换 + swap(nums, i, j); + //利用倒置进行排序 + reverse(nums, i + 1); + +} + +private void swap(int[] nums, int i, int j) { + int temp = nums[j]; + nums[j] = nums[i]; + nums[i] = temp; +} + +private void reverse(int[] nums, int start) { + int i = start, j = nums.length - 1; + while (i < j) { + swap(nums, i, j); + i++; + j--; + } +} +``` + +时间复杂度:最坏的情况就是遍历完所有位,O(n),倒置也是 O(n),所以总体依旧是 O(n)。 + +空间复杂度:O(1)。 + +# 总 + +开始看题的时候一直没理解,后来理解了题试了几种也没想出来,然后看了 Solution,理了下思路。 + diff --git a/leetCode-32-Longest-Valid-Parentheses.md b/leetCode-32-Longest-Valid-Parentheses.md index 122b85c5f..fc5b22147 100644 --- a/leetCode-32-Longest-Valid-Parentheses.md +++ b/leetCode-32-Longest-Valid-Parentheses.md @@ -1,250 +1,250 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/32.jpg) - -给一个一堆括号的字符串,然后返回最长的合法的括号的长度。关于括号的问题,我们在 [20](https://leetcode.windliang.cc/leetCode-20-Valid%20Parentheses.html) 题和 [22](https://leetcode.windliang.cc/leetCode-22-Generate-Parentheses.html) 题也讨论过。 - -# 解法一 暴力解法 - -列举所有的字符串,然后判断每个字符串是不是符合。当然这里可以做个优化就是,因为合法字符串一定是偶数个,所以可以只列举偶数长度的字符串。列举从 0 开始的,长度是 2、4、6 ……的字符串,列举下标从 1 开始的,长度是 2、4、6 ……的字符串,然后循环下去。当然判断字符串是否符合,利用栈来实现,在[之前](https://leetcode.windliang.cc/leetCode-20-Valid%20Parentheses.html)已经讨论过了。 - -```java -public boolean isValid(String s) { - Stack stack = new Stack(); - for (int i = 0; i < s.length(); i++) { - if (s.charAt(i) == '(') { - stack.push('('); - } else if (!stack.empty() && stack.peek() == '(') { - stack.pop(); - } else { - return false; - } - } - return stack.empty(); -} -public int longestValidParentheses(String s) { - int maxlen = 0; - for (int i = 0; i < s.length(); i++) { - for (int j = i + 2; j <= s.length(); j+=2) { - if (isValid(s.substring(i, j))) { - maxlen = Math.max(maxlen, j - i); - } - } - } - return maxlen; -} -``` - -时间复杂度: 列举字符串是 O(n²),判断是否是合法序列是 O(n),所以总共是 O(n³)。 - -空间复杂度:O(n),每次判断的时候,栈的大小。 - -这个算法,leetCode 会报时间超时。 - -# 解法二 暴力破解优化 - -解法一中,我们会做很多重复的判断,例如类似于这样的,()()(),从下标 0 开始,我们先判断长度为 2 的是否是合法序列。然后再判断长度是 4 的字符串是否符合,但会从下标 0 开始判断。判断长度为 6 的字符串的时候,依旧从 0 开始,但其实之前已经确认前 4 个已经组成了合法序列,所以我们其实从下标 4 开始判断就可以了。 - -基于此,我们可以换一个思路,我们判断从每个位置开始的最长合法子串是多长即可。而判断是否是合法子串,我们不用栈,而是用一个变量记录当前的括号情况,遇到左括号加 1,遇到右括号减 1,如果变成 0 ,我们就更新下最长合法子串。 - -```java -public int longestValidParentheses(String s) { - int count = 0; - int max = 0; - for (int i = 0; i < s.length(); i++) { - count = 0; - for (int j = i; j < s.length(); j++) { - if (s.charAt(j) == '(') { - count++; - } else { - count--; - } - //count < 0 说明右括号多了,此时无论后边是什么,一定是非法字符串了,所以可以提前结束循环 - if (count < 0) { - break; - - } - //当前是合法序列,更新最长长度 - if (count == 0) { - if (j - i + 1 > max) { - max = j - i + 1; - } - } - } - } - return max; -} -``` - -时间复杂度:O(n²)。 - -空间复杂度:O(1)。 - -# 解法三 动态规划 - -首先定义动态规划的数组代表什么 - -dp [ i ] 代表以下标 i 结尾的合法序列的最长长度,例如下图 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/32_1.jpg) - -下标 1 结尾的最长合法字符串长度是 2,下标 3 结尾的最长字符串是 str [ 0 , 3 ],长度是 4 。 - -我们来分析下 dp 的规律。 - -首先我们初始化所有的 dp 都等于零。 - -以左括号结尾的字符串一定是非法序列,所以 dp 是零,不用更改。 - -以右括号结尾的字符串分两种情况。 - -* 右括号前边是 ( ,类似于 ……()。 - - dp [ i ] = dp [ i - 2] + 2 (前一个合法序列的长度,加上当前新增的长度 2) - - 类似于上图中 index = 3 的时候的情况。 - - dp [ 3 ] = dp [ 3 - 2 ] + 2 = dp [ 1 ] + 2 = 2 + 2 = 4 - -* 右括号前边是 ),类似于 ……))。 - - 此时我们需要判断 i - dp[i - 1] - 1 (前一个合法序列的前边一个位置) 是不是左括号。 - - 例如上图的 index = 7 的时候,此时 index - 1 也是右括号,我们需要知道 i - dp[i - 1] - 1 = 7 - dp [ 6 ] - 1 = 4 位置的括号的情况。 - - 而刚好 index = 4 的位置是左括号,此时 dp [ i ] = dp [ i - 1 ] + dp [ i - dp [ i - 1] - 2 ] + 2 (当前位置的前一个合法序列的长度,加上匹配的左括号前边的合法序列的长度,加上新增的长度 2),也就是 dp [ 7 ] = dp [ 7 - 1 ] + dp [ 7 - dp [ 7 - 1] - 2 ] + 2 = dp [ 6 ] + dp [7 - 2 - 2] + 2 = 2 + 4 + 2 = 8。 - - 如果 index = 4 不是左括号,那么此时位置 7 的右括号没有匹配的左括号,所以 dp [ 7 ] = 0 ,不需要更新。 - -上边的分析可以结合图看一下,可以更好的理解,下边看下代码。 - -```java -public int longestValidParentheses(String s) { - int maxans = 0; - int dp[] = new int[s.length()]; - for (int i = 1; i < s.length(); i++) { - if (s.charAt(i) == ')') { - //右括号前边是左括号 - if (s.charAt(i - 1) == '(') { - dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2; - //右括号前边是右括号,并且除去前边的合法序列的前边是左括号 - } else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') { - dp[i] = dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2; - } - maxans = Math.max(maxans, dp[i]); - } - } - return maxans; -} -``` - -时间复杂度:遍历了一次,O(n)。 - -空间复杂度:O(n)。 - -# 解法四 使用栈 - -从左到右扫描字符串,栈顶保存当前扫描的时候,合法序列前的一个位置位置下标是多少,啥意思嘞? - -我们扫描到左括号,就将当前位置入栈。 - -扫描到右括号,就将栈顶出栈(代表栈顶的左括号匹配到了右括号),然后分两种情况。 - -* 栈不空,那么就用当前的位置减去栈顶的存的位置,然后就得到当前合法序列的长度,然后更新一下最长长度。 - -* 栈是空的,说明之前没有与之匹配的左括号,那么就将当前的位置入栈。 - -看下图示,更好的理解一下。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/32_9.jpg) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/32_3.jpg) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/32_4.jpg) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/32_5.jpg) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/32_6.jpg) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/32_7.jpg) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/32_8.jpg) - -再看下代码 - -```java -public int longestValidParentheses(String s) { - int maxans = 0; - Stack stack = new Stack<>(); - stack.push(-1); - for (int i = 0; i < s.length(); i++) { - if (s.charAt(i) == '(') { - stack.push(i); - } else { - stack.pop(); - if (stack.empty()) { - stack.push(i); - } else { - maxans = Math.max(maxans, i - stack.peek()); - } - } - } - return maxans; -} -``` - -时间复杂度: O(n)。 - -空间复杂度:O(n)。 - -# 解法五 神奇解法 - -保持时间复杂度是 O(n),将空间复杂度优化到了 O(1),它的动机是怎么想到的没有理出来,就介绍下它的想法吧。 - -从左到右扫描,用两个变量 left 和 right 保存的当前的左括号和右括号的个数,都初始化为 0 。 - -* 如果左括号个数等于右括号个数了,那么就更新合法序列的最长长度。 -* 如果左括号个数大于右括号个数了,那么就接着向右边扫描。 -* 如果左括号数目小于右括号个数了,那么后边无论是什么,此时都不可能是合法序列了,此时 left 和 right 归 0,然后接着扫描。 - -从左到右扫描完毕后,同样的方法从右到左再来一次,因为类似这样的情况 ( ( ( ) ) ,如果从左到右扫描到最后,left = 3,right = 2,期间不会出现 left == right。但是如果从右向左扫描,扫描到倒数第二个位置的时候,就会出现 left = 2,right = 2 ,就会得到一种合法序列。 - -```java -public int longestValidParentheses(String s) { - int left = 0, right = 0, maxlength = 0; - for (int i = 0; i < s.length(); i++) { - if (s.charAt(i) == '(') { - left++; - } else { - right++; - } - if (left == right) { - maxlength = Math.max(maxlength, 2 * right); - } else if (right >= left) { - left = right = 0; - } - } - left = right = 0; - for (int i = s.length() - 1; i >= 0; i--) { - if (s.charAt(i) == '(') { - left++; - } else { - right++; - } - if (left == right) { - maxlength = Math.max(maxlength, 2 * left); - } else if (left >= right) { - left = right = 0; - } - } - return maxlength; -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(1)。 - -# 总 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/32.jpg) + +给一个一堆括号的字符串,然后返回最长的合法的括号的长度。关于括号的问题,我们在 [20](https://leetcode.windliang.cc/leetCode-20-Valid%20Parentheses.html) 题和 [22](https://leetcode.windliang.cc/leetCode-22-Generate-Parentheses.html) 题也讨论过。 + +# 解法一 暴力解法 + +列举所有的字符串,然后判断每个字符串是不是符合。当然这里可以做个优化就是,因为合法字符串一定是偶数个,所以可以只列举偶数长度的字符串。列举从 0 开始的,长度是 2、4、6 ……的字符串,列举下标从 1 开始的,长度是 2、4、6 ……的字符串,然后循环下去。当然判断字符串是否符合,利用栈来实现,在[之前](https://leetcode.windliang.cc/leetCode-20-Valid%20Parentheses.html)已经讨论过了。 + +```java +public boolean isValid(String s) { + Stack stack = new Stack(); + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) == '(') { + stack.push('('); + } else if (!stack.empty() && stack.peek() == '(') { + stack.pop(); + } else { + return false; + } + } + return stack.empty(); +} +public int longestValidParentheses(String s) { + int maxlen = 0; + for (int i = 0; i < s.length(); i++) { + for (int j = i + 2; j <= s.length(); j+=2) { + if (isValid(s.substring(i, j))) { + maxlen = Math.max(maxlen, j - i); + } + } + } + return maxlen; +} +``` + +时间复杂度: 列举字符串是 O(n²),判断是否是合法序列是 O(n),所以总共是 O(n³)。 + +空间复杂度:O(n),每次判断的时候,栈的大小。 + +这个算法,leetCode 会报时间超时。 + +# 解法二 暴力破解优化 + +解法一中,我们会做很多重复的判断,例如类似于这样的,()()(),从下标 0 开始,我们先判断长度为 2 的是否是合法序列。然后再判断长度是 4 的字符串是否符合,但会从下标 0 开始判断。判断长度为 6 的字符串的时候,依旧从 0 开始,但其实之前已经确认前 4 个已经组成了合法序列,所以我们其实从下标 4 开始判断就可以了。 + +基于此,我们可以换一个思路,我们判断从每个位置开始的最长合法子串是多长即可。而判断是否是合法子串,我们不用栈,而是用一个变量记录当前的括号情况,遇到左括号加 1,遇到右括号减 1,如果变成 0 ,我们就更新下最长合法子串。 + +```java +public int longestValidParentheses(String s) { + int count = 0; + int max = 0; + for (int i = 0; i < s.length(); i++) { + count = 0; + for (int j = i; j < s.length(); j++) { + if (s.charAt(j) == '(') { + count++; + } else { + count--; + } + //count < 0 说明右括号多了,此时无论后边是什么,一定是非法字符串了,所以可以提前结束循环 + if (count < 0) { + break; + + } + //当前是合法序列,更新最长长度 + if (count == 0) { + if (j - i + 1 > max) { + max = j - i + 1; + } + } + } + } + return max; +} +``` + +时间复杂度:O(n²)。 + +空间复杂度:O(1)。 + +# 解法三 动态规划 + +首先定义动态规划的数组代表什么 + +dp [ i ] 代表以下标 i 结尾的合法序列的最长长度,例如下图 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/32_1.jpg) + +下标 1 结尾的最长合法字符串长度是 2,下标 3 结尾的最长字符串是 str [ 0 , 3 ],长度是 4 。 + +我们来分析下 dp 的规律。 + +首先我们初始化所有的 dp 都等于零。 + +以左括号结尾的字符串一定是非法序列,所以 dp 是零,不用更改。 + +以右括号结尾的字符串分两种情况。 + +* 右括号前边是 ( ,类似于 ……()。 + + dp [ i ] = dp [ i - 2] + 2 (前一个合法序列的长度,加上当前新增的长度 2) + + 类似于上图中 index = 3 的时候的情况。 + + dp [ 3 ] = dp [ 3 - 2 ] + 2 = dp [ 1 ] + 2 = 2 + 2 = 4 + +* 右括号前边是 ),类似于 ……))。 + + 此时我们需要判断 i - dp[i - 1] - 1 (前一个合法序列的前边一个位置) 是不是左括号。 + + 例如上图的 index = 7 的时候,此时 index - 1 也是右括号,我们需要知道 i - dp[i - 1] - 1 = 7 - dp [ 6 ] - 1 = 4 位置的括号的情况。 + + 而刚好 index = 4 的位置是左括号,此时 dp [ i ] = dp [ i - 1 ] + dp [ i - dp [ i - 1] - 2 ] + 2 (当前位置的前一个合法序列的长度,加上匹配的左括号前边的合法序列的长度,加上新增的长度 2),也就是 dp [ 7 ] = dp [ 7 - 1 ] + dp [ 7 - dp [ 7 - 1] - 2 ] + 2 = dp [ 6 ] + dp [7 - 2 - 2] + 2 = 2 + 4 + 2 = 8。 + + 如果 index = 4 不是左括号,那么此时位置 7 的右括号没有匹配的左括号,所以 dp [ 7 ] = 0 ,不需要更新。 + +上边的分析可以结合图看一下,可以更好的理解,下边看下代码。 + +```java +public int longestValidParentheses(String s) { + int maxans = 0; + int dp[] = new int[s.length()]; + for (int i = 1; i < s.length(); i++) { + if (s.charAt(i) == ')') { + //右括号前边是左括号 + if (s.charAt(i - 1) == '(') { + dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2; + //右括号前边是右括号,并且除去前边的合法序列的前边是左括号 + } else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') { + dp[i] = dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2; + } + maxans = Math.max(maxans, dp[i]); + } + } + return maxans; +} +``` + +时间复杂度:遍历了一次,O(n)。 + +空间复杂度:O(n)。 + +# 解法四 使用栈 + +从左到右扫描字符串,栈顶保存当前扫描的时候,合法序列前的一个位置位置下标是多少,啥意思嘞? + +我们扫描到左括号,就将当前位置入栈。 + +扫描到右括号,就将栈顶出栈(代表栈顶的左括号匹配到了右括号),然后分两种情况。 + +* 栈不空,那么就用当前的位置减去栈顶的存的位置,然后就得到当前合法序列的长度,然后更新一下最长长度。 + +* 栈是空的,说明之前没有与之匹配的左括号,那么就将当前的位置入栈。 + +看下图示,更好的理解一下。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/32_9.jpg) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/32_3.jpg) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/32_4.jpg) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/32_5.jpg) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/32_6.jpg) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/32_7.jpg) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/32_8.jpg) + +再看下代码 + +```java +public int longestValidParentheses(String s) { + int maxans = 0; + Stack stack = new Stack<>(); + stack.push(-1); + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) == '(') { + stack.push(i); + } else { + stack.pop(); + if (stack.empty()) { + stack.push(i); + } else { + maxans = Math.max(maxans, i - stack.peek()); + } + } + } + return maxans; +} +``` + +时间复杂度: O(n)。 + +空间复杂度:O(n)。 + +# 解法五 神奇解法 + +保持时间复杂度是 O(n),将空间复杂度优化到了 O(1),它的动机是怎么想到的没有理出来,就介绍下它的想法吧。 + +从左到右扫描,用两个变量 left 和 right 保存的当前的左括号和右括号的个数,都初始化为 0 。 + +* 如果左括号个数等于右括号个数了,那么就更新合法序列的最长长度。 +* 如果左括号个数大于右括号个数了,那么就接着向右边扫描。 +* 如果左括号数目小于右括号个数了,那么后边无论是什么,此时都不可能是合法序列了,此时 left 和 right 归 0,然后接着扫描。 + +从左到右扫描完毕后,同样的方法从右到左再来一次,因为类似这样的情况 ( ( ( ) ) ,如果从左到右扫描到最后,left = 3,right = 2,期间不会出现 left == right。但是如果从右向左扫描,扫描到倒数第二个位置的时候,就会出现 left = 2,right = 2 ,就会得到一种合法序列。 + +```java +public int longestValidParentheses(String s) { + int left = 0, right = 0, maxlength = 0; + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) == '(') { + left++; + } else { + right++; + } + if (left == right) { + maxlength = Math.max(maxlength, 2 * right); + } else if (right >= left) { + left = right = 0; + } + } + left = right = 0; + for (int i = s.length() - 1; i >= 0; i--) { + if (s.charAt(i) == '(') { + left++; + } else { + right++; + } + if (left == right) { + maxlength = Math.max(maxlength, 2 * left); + } else if (left >= right) { + left = right = 0; + } + } + return maxlength; +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(1)。 + +# 总 + 这几种算法,暴力破解和动态规划我觉得想的话,还是能分析出来的话,最后两种算法感觉是去挖掘题的本质得到的算法,普适性不是很强。但最后一种算法,从左到右,从右到左,是真的强。 \ No newline at end of file diff --git a/leetCode-33-Search-in-Rotated-Sorted-Array.md b/leetCode-33-Search-in-Rotated-Sorted-Array.md index b0d944506..0ebfed0d7 100644 --- a/leetCode-33-Search-in-Rotated-Sorted-Array.md +++ b/leetCode-33-Search-in-Rotated-Sorted-Array.md @@ -1,264 +1,264 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/33.jpg) - -开始的时候想复杂了,其实就是一个排序好的数组,把前边的若干的个数,一起移动到末尾就行了。然后在 log (n) 下找到给定数字的下标。 - -总的来说,log(n),我们肯定得用二分的方法了。 - -# 解法一 - -参考[这里](https://leetcode.com/problems/search-in-rotated-sorted-array/discuss/14425/Concise-O(log-N)-Binary-search-solution)首先我们想一下变化前,正常的升序。我们怎么找给定的数字。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/33_2.jpg) - -我们每次只关心**中间位置的值(这一点很重要)**,也就是上图 3 位置的数值,如果 target 小于 3 位置的值,我们就把 3 4 5 6 抛弃。然后看新的中间的位置,也就是 1 位置的数值。 3 位置, 1 位置的值是多少呢?我们有一个数组。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/33_3.jpg) - -3 位置的值,刚好就是数组下标为 3 的值,1 位置的值刚好就是下标为 1 的值。 - -那么如果,按题目要求的,变化后,3 位置 和 1 位置的值怎么求呢? 此时我们的数组变成下边这样,我们依旧把值从小到大排列。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/33_4.jpg) - -此时 3 位置的数值对应为数组下标是 0 的值,1 位置的值对应数组下标是 5 的值。任意位置的对应规则是什么呢?0 -> 4, 1 - > 5,4 ->1,就是就是 (位置 + 偏移 )% 数组的长度。这里就是加上 4 模 7。 - -问题转换为怎么去求出这个偏移。 - -我们只要知道任意一个位置对应的数组下标就可以了,为了方便我们可以求位置为 0 的值对应的下标(数组中最小的数对应的下标),0 位置对应的下标就是我们要求的偏移了(0 + 偏移 = 数组下标)。这里 nums = [ 4, 5, 6, 7, 0, 1, 2] ,我们就需要去求数值 0 的下标。 - -求最小值的下标,因为题目要求时间复杂度是 O(log ( n )),所以我们必须采取二分的方法去找,二分的方法就要保证每次比较后,去掉一半的元素。这里我们去比较中点和端点值的情况,那么是根据中点和起点比较,还是中点和终点比较呢?我们来分析下。 - -* mid 和 start 比较 - - mid > start : 最小值在左半部分。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/33_5.jpg) - - mid < start: 最小值在左半部分。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/33_6.jpg) - - 无论大于小于,最小值都在左半部分,所以 mid 和 start 比较是不可取的。 - -* mid 和 end 比较 - - mid < end:最小值在左半部分。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/33_5.jpg) - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/33_6.jpg) - - mid > end:最小值在右半部分。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/33_7.jpg) - - 所以我们只需要把 mid 和 end 比较,mid < end 丢弃右半部分(更新 end = mid),mid > end 丢弃左半部分(更新 start = mid)。直到 end 等于 start 时候结束就可以了。 - -但这样会有一个问题的,对于下边的例子,就会遇到死循环了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/33_8.jpg) - -问题出在,当数组剩偶数长度的时候,mid = (start + end)/ 2,mid 取的是左端点。上图的例子, mid > end, 更新 start = mid,start 位置并不会变化。那么下一次 mid 的值也不会变,就死循环了。所以,我们要更新 start = mid + 1。 - -综上,找最小值的下标的代码就出来了,同时,由于我们找的是位置 0 对应的下标,所以偏移就是最小值的下标。 - -```java -while (start < end) { - int mid = (start + end) / 2; - if (nums[mid] > nums[end]) { - start = mid + 1 ; - } else { - end = mid; - } -} -int bias = start; -``` - -当然,我们是找最小值对应的下标,然后求出了偏移。我们也可以找最大值的对应的下标,分析思路和之前是一样的,主要还是要注意一下边界的情况,然后就可以求出偏移。 - -```java -while (start < end) { - int mid = Math.round(((float)start + end) / 2); - if (nums[mid] < nums[start]) { - end = mid - 1; - } else { - start = mid; - } - -} -int n = nums.length; -bias = (start + n) - (n - 1); //此时 start 是最大值的数组下标,加上模长 n,减去最大值的位置 n - 1 ,就得到了偏移。因为 (位置 + 偏移)% n = 数组下标,即 (n - 1 + 偏移)% n = start,n - 1 加偏移超过了 n,所以取模理解成减 n 。 -``` - -有了偏移,我们就可以愉快的找目标值的数组下标了。 - -```java -public int search (int[] nums, int target) { - int start = 0; - int end = nums.length - 1; - //找出最小值的数组下标 - /* while (start < end) { - int mid = (start + end) / 2; - if (nums[mid] > nums[end]) { - start = mid + 1 ; - } else { - end = mid; - } - } - int bias = start;*/ - //找出最大值的数组下标 - while (start < end) { - int mid = Math.round(((float)start + end) / 2); - if (nums[mid] < nums[start]) { - end = mid - 1; - } else { - start = mid; - } - - } - int n = nums.length; - int bias = (start + n) - (n - 1); //得到偏移 - start = 0; - end = nums.length - 1; - while (start <= end) { - int mid = (start + end) / 2;//中间的位置 - int mid_change = (mid + bias) % nums.length;//中间的位置对应的数组下标 - int value = nums[mid_change];//中间位置的值 - if (target == value) { - return mid_change; - } - if (target < value) { - end = mid - 1; - } else { - start = mid + 1; - } - } - return -1; -} -``` - -时间复杂度:O(log(n))。 - -空间复杂度:O(1)。 - -# 解法二 - -参考[这里](https://leetcode.com/problems/search-in-rotated-sorted-array/discuss/14435/Clever-idea-making-it-simple),题目中的数组,其实是两段有序的数组。例如 - -[ 4 5 6 7 1 2 3 ] ,[ 4 5 6 7 ] 和 [ 1 2 3 ] 两段有序。 - -而对于 [ 1 2 3 4] 这种,可以看做 [ 1 2 3 4 ] 和 [ ] 特殊的两段有序。 - -而对于我们要找的 target , target 不在的那一段,所有数字可以看做无穷大,这样**整个数组就可以看做有序的了,可以用正常的二分法去找 target 了**,例如 - -[ 4 5 6 7 1 2 3] ,如果 target = 5,那么数组可以看做 [ 4 5 6 7 inf inf inf ]。 - -[ 4 5 6 7 1 2 3] ,如果 target = 2,那么数组可以看做 [ -inf -inf - inf -inf 1 2 3]。 - -和解法一一样,我们每次只关心 mid 的值,所以 mid 要么就是 nums [ mid ],要么就是 inf 或者 -inf。 - -什么时候是 nums [ mid ] 呢? - -当 nums [ mid ] 和 target 在同一段里边。 - -* 怎么判断 nums [ mid ] 和 target 在同一段? - - 把 nums [ mid ] 和 target 同时与 nums [ 0 ] 比较,如果它俩都大于 nums [ 0 ] 或者都小于 nums [ 0 ],那么就代表它俩在同一段。例如 - - [ 4 5 6 7 1 2 3],如果 target = 5,此时数组看做 [ 4 5 6 7 inf inf inf ]。nums [ mid ] = 7,target > nums [ 0 ],nums [ mid ] > nums [ 0 ],所以它们在同一段 nums [ mid ] = 7,不用变化。 - -* 怎么判断 nums [ mid ] 和 target 不在同一段? - - 把 nums [ mid ] 和 target 同时与 nums [ 0 ] 比较,如果它俩一个大于 nums [ 0 ] 一个小于 nums [ 0 ],那么就代表它俩不在同一段。例如 - - [ 4 5 6 7 1 2 3],如果 target = 2,此时数组看做 [ - inf - inf - inf - inf 1 2 3]。nums [ mid ] = 7,target < nums [ 0 ],nums [ mid ] > nums [ 0 ],一个大于,一个小于,所以它们不在同一段 nums [ mid ] = - inf,变成了负无穷大。 - -看下代码吧 - -```java -public int search(int[] nums, int target) { - int lo = 0, hi = nums.length - 1; - while (lo <= hi) { - int mid = lo + (hi - lo) / 2; - int num = nums[mid]; - //nums [ mid ] 和 target 在同一段 - if ((nums[mid] < nums[0]) == (target < nums[0])) { - num = nums[mid]; - //nums [ mid ] 和 target 不在同一段,同时还要考虑下变成 -inf 还是 inf。 - } else { - num = target < nums[0] ? Integer.MIN_VALUE : Integer.MAX_VALUE; - } - - if (num < target) - lo = mid + 1; - else if (num > target) - hi = mid - 1; - else - return mid; - } - return -1; -} -``` - -时间复杂度:O(log(n))。 - -空间复杂度:O(1)。 - -# 解法三 - -参考[这里](https://leetcode.com/problems/search-in-rotated-sorted-array/discuss/14436/Revised-Binary-Search),算法基于一个事实,数组从任意位置劈开后,至少有一半是有序的,什么意思呢? - -比如 [ 4 5 6 7 1 2 3] ,从 7 劈开,左边是 [ 4 5 6 7] 右边是 [ 7 1 2 3],左边是有序的。 - -基于这个事实。 - -我们可以先找到哪一段是有序的 (只要判断端点即可),然后看 target 在不在这一段里,如果在,那么就把另一半丢弃。如果不在,那么就把这一段丢弃。 - -```java -public int search(int[] nums, int target) { - int start = 0; - int end = nums.length - 1; - while (start <= end) { - int mid = (start + end) / 2; - if (target == nums[mid]) { - return mid; - } - //左半段是有序的 - if (nums[start] <= nums[mid]) { - //target 在这段里 - if (target >= nums[start] && target < nums[mid]) { - end = mid - 1; - } else { - start = mid + 1; - } - //右半段是有序的 - } else { - if (target > nums[mid] && target <= nums[end]) { - start = mid + 1; - } else { - end = mid - 1; - } - } - - } - return -1; - } -``` - -时间复杂度:O(log(n))。 - -空间复杂度:O(1)。 - -# 总 - -三种解法是从不同的思路去理解题意,但本质上都是找到丢弃一半的规则,从而达到 log (n) 的时间复杂度,对二分查找的本质的理解更加深刻了。 - - - - - - - - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/33.jpg) + +开始的时候想复杂了,其实就是一个排序好的数组,把前边的若干的个数,一起移动到末尾就行了。然后在 log (n) 下找到给定数字的下标。 + +总的来说,log(n),我们肯定得用二分的方法了。 + +# 解法一 + +参考[这里](https://leetcode.com/problems/search-in-rotated-sorted-array/discuss/14425/Concise-O(log-N)-Binary-search-solution)首先我们想一下变化前,正常的升序。我们怎么找给定的数字。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/33_2.jpg) + +我们每次只关心**中间位置的值(这一点很重要)**,也就是上图 3 位置的数值,如果 target 小于 3 位置的值,我们就把 3 4 5 6 抛弃。然后看新的中间的位置,也就是 1 位置的数值。 3 位置, 1 位置的值是多少呢?我们有一个数组。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/33_3.jpg) + +3 位置的值,刚好就是数组下标为 3 的值,1 位置的值刚好就是下标为 1 的值。 + +那么如果,按题目要求的,变化后,3 位置 和 1 位置的值怎么求呢? 此时我们的数组变成下边这样,我们依旧把值从小到大排列。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/33_4.jpg) + +此时 3 位置的数值对应为数组下标是 0 的值,1 位置的值对应数组下标是 5 的值。任意位置的对应规则是什么呢?0 -> 4, 1 - > 5,4 ->1,就是就是 (位置 + 偏移 )% 数组的长度。这里就是加上 4 模 7。 + +问题转换为怎么去求出这个偏移。 + +我们只要知道任意一个位置对应的数组下标就可以了,为了方便我们可以求位置为 0 的值对应的下标(数组中最小的数对应的下标),0 位置对应的下标就是我们要求的偏移了(0 + 偏移 = 数组下标)。这里 nums = [ 4, 5, 6, 7, 0, 1, 2] ,我们就需要去求数值 0 的下标。 + +求最小值的下标,因为题目要求时间复杂度是 O(log ( n )),所以我们必须采取二分的方法去找,二分的方法就要保证每次比较后,去掉一半的元素。这里我们去比较中点和端点值的情况,那么是根据中点和起点比较,还是中点和终点比较呢?我们来分析下。 + +* mid 和 start 比较 + + mid > start : 最小值在左半部分。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/33_5.jpg) + + mid < start: 最小值在左半部分。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/33_6.jpg) + + 无论大于小于,最小值都在左半部分,所以 mid 和 start 比较是不可取的。 + +* mid 和 end 比较 + + mid < end:最小值在左半部分。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/33_5.jpg) + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/33_6.jpg) + + mid > end:最小值在右半部分。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/33_7.jpg) + + 所以我们只需要把 mid 和 end 比较,mid < end 丢弃右半部分(更新 end = mid),mid > end 丢弃左半部分(更新 start = mid)。直到 end 等于 start 时候结束就可以了。 + +但这样会有一个问题的,对于下边的例子,就会遇到死循环了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/33_8.jpg) + +问题出在,当数组剩偶数长度的时候,mid = (start + end)/ 2,mid 取的是左端点。上图的例子, mid > end, 更新 start = mid,start 位置并不会变化。那么下一次 mid 的值也不会变,就死循环了。所以,我们要更新 start = mid + 1。 + +综上,找最小值的下标的代码就出来了,同时,由于我们找的是位置 0 对应的下标,所以偏移就是最小值的下标。 + +```java +while (start < end) { + int mid = (start + end) / 2; + if (nums[mid] > nums[end]) { + start = mid + 1 ; + } else { + end = mid; + } +} +int bias = start; +``` + +当然,我们是找最小值对应的下标,然后求出了偏移。我们也可以找最大值的对应的下标,分析思路和之前是一样的,主要还是要注意一下边界的情况,然后就可以求出偏移。 + +```java +while (start < end) { + int mid = Math.round(((float)start + end) / 2); + if (nums[mid] < nums[start]) { + end = mid - 1; + } else { + start = mid; + } + +} +int n = nums.length; +bias = (start + n) - (n - 1); //此时 start 是最大值的数组下标,加上模长 n,减去最大值的位置 n - 1 ,就得到了偏移。因为 (位置 + 偏移)% n = 数组下标,即 (n - 1 + 偏移)% n = start,n - 1 加偏移超过了 n,所以取模理解成减 n 。 +``` + +有了偏移,我们就可以愉快的找目标值的数组下标了。 + +```java +public int search (int[] nums, int target) { + int start = 0; + int end = nums.length - 1; + //找出最小值的数组下标 + /* while (start < end) { + int mid = (start + end) / 2; + if (nums[mid] > nums[end]) { + start = mid + 1 ; + } else { + end = mid; + } + } + int bias = start;*/ + //找出最大值的数组下标 + while (start < end) { + int mid = Math.round(((float)start + end) / 2); + if (nums[mid] < nums[start]) { + end = mid - 1; + } else { + start = mid; + } + + } + int n = nums.length; + int bias = (start + n) - (n - 1); //得到偏移 + start = 0; + end = nums.length - 1; + while (start <= end) { + int mid = (start + end) / 2;//中间的位置 + int mid_change = (mid + bias) % nums.length;//中间的位置对应的数组下标 + int value = nums[mid_change];//中间位置的值 + if (target == value) { + return mid_change; + } + if (target < value) { + end = mid - 1; + } else { + start = mid + 1; + } + } + return -1; +} +``` + +时间复杂度:O(log(n))。 + +空间复杂度:O(1)。 + +# 解法二 + +参考[这里](https://leetcode.com/problems/search-in-rotated-sorted-array/discuss/14435/Clever-idea-making-it-simple),题目中的数组,其实是两段有序的数组。例如 + +[ 4 5 6 7 1 2 3 ] ,[ 4 5 6 7 ] 和 [ 1 2 3 ] 两段有序。 + +而对于 [ 1 2 3 4] 这种,可以看做 [ 1 2 3 4 ] 和 [ ] 特殊的两段有序。 + +而对于我们要找的 target , target 不在的那一段,所有数字可以看做无穷大,这样**整个数组就可以看做有序的了,可以用正常的二分法去找 target 了**,例如 + +[ 4 5 6 7 1 2 3] ,如果 target = 5,那么数组可以看做 [ 4 5 6 7 inf inf inf ]。 + +[ 4 5 6 7 1 2 3] ,如果 target = 2,那么数组可以看做 [ -inf -inf - inf -inf 1 2 3]。 + +和解法一一样,我们每次只关心 mid 的值,所以 mid 要么就是 nums [ mid ],要么就是 inf 或者 -inf。 + +什么时候是 nums [ mid ] 呢? + +当 nums [ mid ] 和 target 在同一段里边。 + +* 怎么判断 nums [ mid ] 和 target 在同一段? + + 把 nums [ mid ] 和 target 同时与 nums [ 0 ] 比较,如果它俩都大于 nums [ 0 ] 或者都小于 nums [ 0 ],那么就代表它俩在同一段。例如 + + [ 4 5 6 7 1 2 3],如果 target = 5,此时数组看做 [ 4 5 6 7 inf inf inf ]。nums [ mid ] = 7,target > nums [ 0 ],nums [ mid ] > nums [ 0 ],所以它们在同一段 nums [ mid ] = 7,不用变化。 + +* 怎么判断 nums [ mid ] 和 target 不在同一段? + + 把 nums [ mid ] 和 target 同时与 nums [ 0 ] 比较,如果它俩一个大于 nums [ 0 ] 一个小于 nums [ 0 ],那么就代表它俩不在同一段。例如 + + [ 4 5 6 7 1 2 3],如果 target = 2,此时数组看做 [ - inf - inf - inf - inf 1 2 3]。nums [ mid ] = 7,target < nums [ 0 ],nums [ mid ] > nums [ 0 ],一个大于,一个小于,所以它们不在同一段 nums [ mid ] = - inf,变成了负无穷大。 + +看下代码吧 + +```java +public int search(int[] nums, int target) { + int lo = 0, hi = nums.length - 1; + while (lo <= hi) { + int mid = lo + (hi - lo) / 2; + int num = nums[mid]; + //nums [ mid ] 和 target 在同一段 + if ((nums[mid] < nums[0]) == (target < nums[0])) { + num = nums[mid]; + //nums [ mid ] 和 target 不在同一段,同时还要考虑下变成 -inf 还是 inf。 + } else { + num = target < nums[0] ? Integer.MIN_VALUE : Integer.MAX_VALUE; + } + + if (num < target) + lo = mid + 1; + else if (num > target) + hi = mid - 1; + else + return mid; + } + return -1; +} +``` + +时间复杂度:O(log(n))。 + +空间复杂度:O(1)。 + +# 解法三 + +参考[这里](https://leetcode.com/problems/search-in-rotated-sorted-array/discuss/14436/Revised-Binary-Search),算法基于一个事实,数组从任意位置劈开后,至少有一半是有序的,什么意思呢? + +比如 [ 4 5 6 7 1 2 3] ,从 7 劈开,左边是 [ 4 5 6 7] 右边是 [ 7 1 2 3],左边是有序的。 + +基于这个事实。 + +我们可以先找到哪一段是有序的 (只要判断端点即可),然后看 target 在不在这一段里,如果在,那么就把另一半丢弃。如果不在,那么就把这一段丢弃。 + +```java +public int search(int[] nums, int target) { + int start = 0; + int end = nums.length - 1; + while (start <= end) { + int mid = (start + end) / 2; + if (target == nums[mid]) { + return mid; + } + //左半段是有序的 + if (nums[start] <= nums[mid]) { + //target 在这段里 + if (target >= nums[start] && target < nums[mid]) { + end = mid - 1; + } else { + start = mid + 1; + } + //右半段是有序的 + } else { + if (target > nums[mid] && target <= nums[end]) { + start = mid + 1; + } else { + end = mid - 1; + } + } + + } + return -1; + } +``` + +时间复杂度:O(log(n))。 + +空间复杂度:O(1)。 + +# 总 + +三种解法是从不同的思路去理解题意,但本质上都是找到丢弃一半的规则,从而达到 log (n) 的时间复杂度,对二分查找的本质的理解更加深刻了。 + + + + + + + + diff --git a/leetCode-34-Find-First-and-Last-Position-of-Element-in-Sorted-Array.md b/leetCode-34-Find-First-and-Last-Position-of-Element-in-Sorted-Array.md index ad32b1588..8b8f0ff9e 100644 --- a/leetCode-34-Find-First-and-Last-Position-of-Element-in-Sorted-Array.md +++ b/leetCode-34-Find-First-and-Last-Position-of-Element-in-Sorted-Array.md @@ -1,280 +1,280 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/34.jpg) - -找到目标值的第一次出现和最后一次出现的位置,同样要求 log ( n ) 下完成。 - -先分享 [leetcode](https://leetcode.com/problems/find-first-and-last-position-of-element-in-sorted-array/solution/) 提供的两个解法。 - -# 解法一 线性扫描 - -从左向右遍历,一旦出现等于 target 的值就结束,保存当前下标。如果从左到右没有找到 target,那么就直接返回 [ -1 , -1 ] 就可以了,因为从左到右没找到,那么从右到左也一定不会找到的。如果找到了,然后再从右到左遍历,一旦出现等于 target 的值就结束,保存当前下标。 - -时间复杂度是 O(n)并不满足题意,但可以了解下这个思路,从左到右,从右到左之前也遇到过。 - -```java -public int[] searchRange(int[] nums, int target) { - int[] targetRange = {-1, -1}; - - // 从左到右扫描 - for (int i = 0; i < nums.length; i++) { - if (nums[i] == target) { - targetRange[0] = i; - break; - } - } - - // 如果之前没找到,直接返回 [ -1 , -1 ] - if (targetRange[0] == -1) { - return targetRange; - } - - //从右到左扫描 - for (int j = nums.length-1; j >= 0; j--) { - if (nums[j] == target) { - targetRange[1] = j; - break; - } - } - - return targetRange; -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(1)。 - -# 解法二 二分查找 - -让我们先看下正常的二分查找。 - -```java -int start = 0; -int end = nums.length - 1; -while (start <= end) { - int mid = (start + end) / 2; - if (target == nums[mid]) { - return mid; - } else if (target < nums[mid]) { - end = mid - 1; - } else { - start = mid + 1; - } -} -``` - -二分查找中,我们找到 target 就结束了,这里我们需要修改下。 - -我们如果找最左边等于 target 的值,找到 target 时候并不代表我们找到了我们所需要的,例如下边的情况, - -![](https://windliang.oss-cn-beijing.aliyuncs.com/34_2.jpg) - -此时虽然 mid 指向的值等于 target 了,但是我们要找的其实还在左边,为了达到 log 的时间复杂度,我们依旧是丢弃一半,我们需要更新 end = mid - 1,图示如下。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/34_3.jpg) - -此时 tartget > nums [ mid ] ,更新 start = mid + 1。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/34_6.jpg) - -此时 target == nums [ mid ] ,但由于我们改成了 end = mid - 1,所以继续更新,end 就到了 mid 的左边,此时 start > end 了,就会走出 while 循环, 我们要找的值刚好就是 start 指向的了。那么我们修改的代码如下: - -```java -while (start <= end) { - int mid = (start + end) / 2; - if (target == nums[mid]) { - end = mid - 1; - } else if (target < nums[mid]) { - end = mid -1 ; - } else { - start = mid + 1; - } -} -``` - -找右边的同样的分析思路,就是判断需要丢弃哪一边。 - -所以最后的代码就出来了。leetcode 中是把找左边和找右边的合并起来了,本质是一样的。 - -```java -public int[] searchRange(int[] nums, int target) { - int start = 0; - int end = nums.length - 1; - int[] ans = { -1, -1 }; - if (nums.length == 0) { - return ans; - } - while (start <= end) { - int mid = (start + end) / 2; - if (target == nums[mid]) { - end = mid - 1; - } else if (target < nums[mid]) { - end = mid - 1; - } else { - start = mid + 1; - } - } - //考虑 tartget 是否存在,判断我们要找的值是否等于 target 并且是否越界 - if (start == nums.length || nums[ start ] != target) { - return ans; - } else { - ans[0] = start; - } - ans[0] = start; - start = 0; - end = nums.length - 1; - while (start <= end) { - int mid = (start + end) / 2; - if (target == nums[mid]) { - start = mid + 1; - } else if (target < nums[mid]) { - end = mid - 1; - } else { - start = mid + 1; - } - } - ans[1] = end; - return ans; -} -``` - -时间复杂度:O(log(n))。 - -空间复杂度:O(1)。 - -# 解法三 - -以上是 leetcode 提供的思路,我觉得不是很好,因为它所有的情况都一定是循环 log(n)次,讲一下我最开始想到的。 - -相当于在解法二的基础上优化了一下,下边是解法二的代码。 - -```java -while (start <= end) { - int mid = (start + end) / 2; - if (target == nums[mid]) { - end = mid - 1; - } else if (target < nums[mid]) { - end = mid -1 ; - } else { - start = mid + 1; - } -} -``` - -考虑下边的一种情况,如果我们找最左边等于 target 的,此时 mid 的位置已经是我们要找的了,而解法二更新成了 end = mid - 1,然后继续循环了,而此时我们其实完全可以终止了。只需要判断 nums[ mid - 1] 是不是小于 nums [ mid ] ,如果小于就刚好是我们要找的了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/34_4.jpg) - -当然,找最右边也是同样的思路,看下代码吧。 - -```java -public int[] searchRange(int[] nums, int target) { - int start = 0; - int end = nums.length - 1; - int[] ans = { -1, -1 }; - if (nums.length == 0) { - return ans; - } - while (start <= end) { - int mid = (start + end) / 2; - if (target == nums[mid]) { - //这里是为了处理 mid - 1 越界的问题,可以仔细想下。 - //如果 mid == 0,那么 mid 一定是我们要找的了,而此时 mid - 1 就会越界了, - //为了使得下边的 target > n 一定成立,我们把 n 赋成最小值 - //如果 mid > 0,直接吧 nums[mid - 1] 赋给 n 就可以了。 - int n = mid > 0 ? nums[mid - 1] : Integer.MIN_VALUE; - if (target > n) { - ans[0] = mid; - break; - } - end = mid - 1; - } else if (target < nums[mid]) { - end = mid - 1; - } else { - start = mid + 1; - } - } - start = 0; - end = nums.length - 1; - while (start <= end) { - int mid = (start + end) / 2; - if (target == nums[mid]) { - int n = mid < nums.length - 1 ? nums[mid + 1] : Integer.MAX_VALUE; - if (target < n) { - ans[1] = mid; - break; - } - start = mid + 1; - } else if (target < nums[mid]) { - end = mid - 1; - } else { - start = mid + 1; - } - } - return ans; -} -``` - -时间复杂度:O(log(n))。 - -空间复杂度:O(1)。 - -@JZW 的提醒下,上边的虽然能 AC,但是如果要找的数字刚好就是 Integer.MIN_VALUE 的话,就会出现错误。可以修改一下。 - -主要是这两句,除了小于 n,还判断了当前是不是在两端。 - -```java -if (target > n || mid == 0) { -if (target < n || mid == nums.length - 1) { -``` - -```java -public int[] searchRange(int[] nums, int target) { - int start = 0; - int end = nums.length - 1; - int[] ans = { -1, -1 }; - if (nums.length == 0) { - return ans; - } - while (start <= end) { - int mid = (start + end) / 2; - if (target == nums[mid]) { - int n = mid > 0 ? nums[mid - 1] : Integer.MIN_VALUE; - if (target > n || mid == 0) { - ans[0] = mid; - break; - } - end = mid - 1; - } else if (target < nums[mid]) { - end = mid - 1; - } else { - start = mid + 1; - } - } - start = 0; - end = nums.length - 1; - while (start <= end) { - int mid = (start + end) / 2; - if (target == nums[mid]) { - int n = mid < nums.length - 1 ? nums[mid + 1] : Integer.MAX_VALUE; - if (target < n || mid == nums.length - 1) { - ans[1] = mid; - break; - } - start = mid + 1; - } else if (target < nums[mid]) { - end = mid - 1; - } else { - start = mid + 1; - } - } - return ans; -} -``` - - - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/34.jpg) + +找到目标值的第一次出现和最后一次出现的位置,同样要求 log ( n ) 下完成。 + +先分享 [leetcode](https://leetcode.com/problems/find-first-and-last-position-of-element-in-sorted-array/solution/) 提供的两个解法。 + +# 解法一 线性扫描 + +从左向右遍历,一旦出现等于 target 的值就结束,保存当前下标。如果从左到右没有找到 target,那么就直接返回 [ -1 , -1 ] 就可以了,因为从左到右没找到,那么从右到左也一定不会找到的。如果找到了,然后再从右到左遍历,一旦出现等于 target 的值就结束,保存当前下标。 + +时间复杂度是 O(n)并不满足题意,但可以了解下这个思路,从左到右,从右到左之前也遇到过。 + +```java +public int[] searchRange(int[] nums, int target) { + int[] targetRange = {-1, -1}; + + // 从左到右扫描 + for (int i = 0; i < nums.length; i++) { + if (nums[i] == target) { + targetRange[0] = i; + break; + } + } + + // 如果之前没找到,直接返回 [ -1 , -1 ] + if (targetRange[0] == -1) { + return targetRange; + } + + //从右到左扫描 + for (int j = nums.length-1; j >= 0; j--) { + if (nums[j] == target) { + targetRange[1] = j; + break; + } + } + + return targetRange; +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(1)。 + +# 解法二 二分查找 + +让我们先看下正常的二分查找。 + +```java +int start = 0; +int end = nums.length - 1; +while (start <= end) { + int mid = (start + end) / 2; + if (target == nums[mid]) { + return mid; + } else if (target < nums[mid]) { + end = mid - 1; + } else { + start = mid + 1; + } +} +``` + +二分查找中,我们找到 target 就结束了,这里我们需要修改下。 + +我们如果找最左边等于 target 的值,找到 target 时候并不代表我们找到了我们所需要的,例如下边的情况, + +![](https://windliang.oss-cn-beijing.aliyuncs.com/34_2.jpg) + +此时虽然 mid 指向的值等于 target 了,但是我们要找的其实还在左边,为了达到 log 的时间复杂度,我们依旧是丢弃一半,我们需要更新 end = mid - 1,图示如下。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/34_3.jpg) + +此时 tartget > nums [ mid ] ,更新 start = mid + 1。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/34_6.jpg) + +此时 target == nums [ mid ] ,但由于我们改成了 end = mid - 1,所以继续更新,end 就到了 mid 的左边,此时 start > end 了,就会走出 while 循环, 我们要找的值刚好就是 start 指向的了。那么我们修改的代码如下: + +```java +while (start <= end) { + int mid = (start + end) / 2; + if (target == nums[mid]) { + end = mid - 1; + } else if (target < nums[mid]) { + end = mid -1 ; + } else { + start = mid + 1; + } +} +``` + +找右边的同样的分析思路,就是判断需要丢弃哪一边。 + +所以最后的代码就出来了。leetcode 中是把找左边和找右边的合并起来了,本质是一样的。 + +```java +public int[] searchRange(int[] nums, int target) { + int start = 0; + int end = nums.length - 1; + int[] ans = { -1, -1 }; + if (nums.length == 0) { + return ans; + } + while (start <= end) { + int mid = (start + end) / 2; + if (target == nums[mid]) { + end = mid - 1; + } else if (target < nums[mid]) { + end = mid - 1; + } else { + start = mid + 1; + } + } + //考虑 tartget 是否存在,判断我们要找的值是否等于 target 并且是否越界 + if (start == nums.length || nums[ start ] != target) { + return ans; + } else { + ans[0] = start; + } + ans[0] = start; + start = 0; + end = nums.length - 1; + while (start <= end) { + int mid = (start + end) / 2; + if (target == nums[mid]) { + start = mid + 1; + } else if (target < nums[mid]) { + end = mid - 1; + } else { + start = mid + 1; + } + } + ans[1] = end; + return ans; +} +``` + +时间复杂度:O(log(n))。 + +空间复杂度:O(1)。 + +# 解法三 + +以上是 leetcode 提供的思路,我觉得不是很好,因为它所有的情况都一定是循环 log(n)次,讲一下我最开始想到的。 + +相当于在解法二的基础上优化了一下,下边是解法二的代码。 + +```java +while (start <= end) { + int mid = (start + end) / 2; + if (target == nums[mid]) { + end = mid - 1; + } else if (target < nums[mid]) { + end = mid -1 ; + } else { + start = mid + 1; + } +} +``` + +考虑下边的一种情况,如果我们找最左边等于 target 的,此时 mid 的位置已经是我们要找的了,而解法二更新成了 end = mid - 1,然后继续循环了,而此时我们其实完全可以终止了。只需要判断 nums[ mid - 1] 是不是小于 nums [ mid ] ,如果小于就刚好是我们要找的了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/34_4.jpg) + +当然,找最右边也是同样的思路,看下代码吧。 + +```java +public int[] searchRange(int[] nums, int target) { + int start = 0; + int end = nums.length - 1; + int[] ans = { -1, -1 }; + if (nums.length == 0) { + return ans; + } + while (start <= end) { + int mid = (start + end) / 2; + if (target == nums[mid]) { + //这里是为了处理 mid - 1 越界的问题,可以仔细想下。 + //如果 mid == 0,那么 mid 一定是我们要找的了,而此时 mid - 1 就会越界了, + //为了使得下边的 target > n 一定成立,我们把 n 赋成最小值 + //如果 mid > 0,直接吧 nums[mid - 1] 赋给 n 就可以了。 + int n = mid > 0 ? nums[mid - 1] : Integer.MIN_VALUE; + if (target > n) { + ans[0] = mid; + break; + } + end = mid - 1; + } else if (target < nums[mid]) { + end = mid - 1; + } else { + start = mid + 1; + } + } + start = 0; + end = nums.length - 1; + while (start <= end) { + int mid = (start + end) / 2; + if (target == nums[mid]) { + int n = mid < nums.length - 1 ? nums[mid + 1] : Integer.MAX_VALUE; + if (target < n) { + ans[1] = mid; + break; + } + start = mid + 1; + } else if (target < nums[mid]) { + end = mid - 1; + } else { + start = mid + 1; + } + } + return ans; +} +``` + +时间复杂度:O(log(n))。 + +空间复杂度:O(1)。 + +@JZW 的提醒下,上边的虽然能 AC,但是如果要找的数字刚好就是 Integer.MIN_VALUE 的话,就会出现错误。可以修改一下。 + +主要是这两句,除了小于 n,还判断了当前是不是在两端。 + +```java +if (target > n || mid == 0) { +if (target < n || mid == nums.length - 1) { +``` + +```java +public int[] searchRange(int[] nums, int target) { + int start = 0; + int end = nums.length - 1; + int[] ans = { -1, -1 }; + if (nums.length == 0) { + return ans; + } + while (start <= end) { + int mid = (start + end) / 2; + if (target == nums[mid]) { + int n = mid > 0 ? nums[mid - 1] : Integer.MIN_VALUE; + if (target > n || mid == 0) { + ans[0] = mid; + break; + } + end = mid - 1; + } else if (target < nums[mid]) { + end = mid - 1; + } else { + start = mid + 1; + } + } + start = 0; + end = nums.length - 1; + while (start <= end) { + int mid = (start + end) / 2; + if (target == nums[mid]) { + int n = mid < nums.length - 1 ? nums[mid + 1] : Integer.MAX_VALUE; + if (target < n || mid == nums.length - 1) { + ans[1] = mid; + break; + } + start = mid + 1; + } else if (target < nums[mid]) { + end = mid - 1; + } else { + start = mid + 1; + } + } + return ans; +} +``` + + + +# 总 + 总体来说,这道题并不难,本质就是对二分查找的修改,以便满足我们的需求。 \ No newline at end of file diff --git a/leetCode-35-Search-Insert-Position.md b/leetCode-35-Search-Insert-Position.md index 2dc9e2147..0f0d1795d 100644 --- a/leetCode-35-Search-Insert-Position.md +++ b/leetCode-35-Search-Insert-Position.md @@ -1,108 +1,108 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/35.jpg) - -给定一个有序数组,依旧是二分查找,不同之处是如果没有找到指定数字,需要返回这个数字应该插入的位置。 - -这道题比较简单,在二分查找的基础上,只要想清楚返回啥就够了。想的话,就考虑最简单的情况如果数组只剩下 2 5,target 是 1, 3, 6 的时候,此时我们应该返回什么就行。 - -```java -public int searchInsert(int[] nums, int target) { - int start = 0; - int end = nums.length - 1; - if (nums.length == 0) { - return 0; - } - while (start < end) { - int mid = (start + end) / 2; - if (target == nums[mid]) { - return mid; - } else if (target < nums[mid]) { - end = mid; - } else { - start = mid + 1; - } - } - //目标值在不在当前停的位置的前边还是后边 - if(target>nums[start]){ - return start + 1; - } - //如果小于的话,就返回当前位置,跑步超过第二名还是第二名,所以不用减 1。 - else{ - return start; - } -} -``` - -时间复杂度:O(log(n))。 - -空间复杂度:O(1)。 - -这道题不难,但是对于二分查找又有了一些新认识。 - -首先,一定要注意,数组剩下偶数个元素的时候,中点取的是左端点。例如 1 2 3 4,中点取的是 2。正因为如此,我们更新 start 的时候不是直接取 mid ,而是 mid + 1。因为剩下两个元素的时候,mid 和 start 是相同的,如果不进行加 1 会陷入死循环。 - -然后上边的算法,返回最终值的时候,我们进行了一个 if 的判断,那么能不能避免呢。 - -* 第一种思路,参考[这里](https://leetcode.com/problems/search-insert-position/discuss/15080/My-8-line-Java-solution)。 - - 首先为了让 start 在循环的时候多加 1,我们将循环的 start < end 改为 start <= end。 - - 这样就会出现一个问题,当 start == end,此时 mid 不仅等于了 start 还会等于 end,所以之前更新 end 是直接赋 mid,现在需要改成 end = mid - 1,防止死循环。这样就达到了目标。 - - ```java - public int searchInsert(int[] nums, int target) { - int start = 0; - int end = nums.length - 1; - if (nums.length == 0) { - return 0; - } - while (start <= end) { - int mid = (start + end) / 2; - if (target == nums[mid]) { - return mid; - } else if (target < nums[mid]) { - end = mid - 1; - } else { - start = mid + 1; - } - } - - return start; - - } - ``` - - -* 第二种思路,参考[这里](https://leetcode.com/problems/search-insert-position/discuss/15110/Very-concise-and-efficient-solution-in-Java)。 - - 我们开始更新 start 的时候,是 mid + 1,如果剩两个元素,例如 2 4,target = 6 的话,此时 mid = 0,start = mid + 1 = 1,我们返回 start + 1 = 2。如果 mid 是右端点,那么 mid = 1,start = mid + 1 = 2,这样就可以直接返回 start 了,不需要在返回的时候加 1 了。 - - 怎么做到呢?最最开始的时候我们取 end 的时候是 end = nums.length - 1。如果我们改成 end = nums.length,这样每次取元素的时候,如果和之前对比,取到的就是右端点了。这样的话,最后返回的时候就不需要多加 1 了。 - - ```java - public int searchInsert(int[] nums, int target) { - int start = 0; - int end = nums.length; - if (nums.length == 0) { - return 0; - } - while (start < end) { - int mid = (start + end) / 2; - if (target == nums[mid]) { - return mid; - } else if (target < nums[mid]) { - end = mid; - } else { - start = mid + 1; - } - } - - return start; - - } - ``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/35.jpg) + +给定一个有序数组,依旧是二分查找,不同之处是如果没有找到指定数字,需要返回这个数字应该插入的位置。 + +这道题比较简单,在二分查找的基础上,只要想清楚返回啥就够了。想的话,就考虑最简单的情况如果数组只剩下 2 5,target 是 1, 3, 6 的时候,此时我们应该返回什么就行。 + +```java +public int searchInsert(int[] nums, int target) { + int start = 0; + int end = nums.length - 1; + if (nums.length == 0) { + return 0; + } + while (start < end) { + int mid = (start + end) / 2; + if (target == nums[mid]) { + return mid; + } else if (target < nums[mid]) { + end = mid; + } else { + start = mid + 1; + } + } + //目标值在不在当前停的位置的前边还是后边 + if(target>nums[start]){ + return start + 1; + } + //如果小于的话,就返回当前位置,跑步超过第二名还是第二名,所以不用减 1。 + else{ + return start; + } +} +``` + +时间复杂度:O(log(n))。 + +空间复杂度:O(1)。 + +这道题不难,但是对于二分查找又有了一些新认识。 + +首先,一定要注意,数组剩下偶数个元素的时候,中点取的是左端点。例如 1 2 3 4,中点取的是 2。正因为如此,我们更新 start 的时候不是直接取 mid ,而是 mid + 1。因为剩下两个元素的时候,mid 和 start 是相同的,如果不进行加 1 会陷入死循环。 + +然后上边的算法,返回最终值的时候,我们进行了一个 if 的判断,那么能不能避免呢。 + +* 第一种思路,参考[这里](https://leetcode.com/problems/search-insert-position/discuss/15080/My-8-line-Java-solution)。 + + 首先为了让 start 在循环的时候多加 1,我们将循环的 start < end 改为 start <= end。 + + 这样就会出现一个问题,当 start == end,此时 mid 不仅等于了 start 还会等于 end,所以之前更新 end 是直接赋 mid,现在需要改成 end = mid - 1,防止死循环。这样就达到了目标。 + + ```java + public int searchInsert(int[] nums, int target) { + int start = 0; + int end = nums.length - 1; + if (nums.length == 0) { + return 0; + } + while (start <= end) { + int mid = (start + end) / 2; + if (target == nums[mid]) { + return mid; + } else if (target < nums[mid]) { + end = mid - 1; + } else { + start = mid + 1; + } + } + + return start; + + } + ``` + + +* 第二种思路,参考[这里](https://leetcode.com/problems/search-insert-position/discuss/15110/Very-concise-and-efficient-solution-in-Java)。 + + 我们开始更新 start 的时候,是 mid + 1,如果剩两个元素,例如 2 4,target = 6 的话,此时 mid = 0,start = mid + 1 = 1,我们返回 start + 1 = 2。如果 mid 是右端点,那么 mid = 1,start = mid + 1 = 2,这样就可以直接返回 start 了,不需要在返回的时候加 1 了。 + + 怎么做到呢?最最开始的时候我们取 end 的时候是 end = nums.length - 1。如果我们改成 end = nums.length,这样每次取元素的时候,如果和之前对比,取到的就是右端点了。这样的话,最后返回的时候就不需要多加 1 了。 + + ```java + public int searchInsert(int[] nums, int target) { + int start = 0; + int end = nums.length; + if (nums.length == 0) { + return 0; + } + while (start < end) { + int mid = (start + end) / 2; + if (target == nums[mid]) { + return mid; + } else if (target < nums[mid]) { + end = mid; + } else { + start = mid + 1; + } + } + + return start; + + } + ``` + +# 总 + 虽然题很简单,但对二分查找有了更多的理解。 \ No newline at end of file diff --git a/leetCode-36-Valid-Sudoku.md b/leetCode-36-Valid-Sudoku.md index a84ed38e4..15ef17c28 100644 --- a/leetCode-36-Valid-Sudoku.md +++ b/leetCode-36-Valid-Sudoku.md @@ -1,154 +1,154 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/36.png) - -一个 9 * 9 的数独的棋盘。判断已经写入数字的棋盘是不是合法。需要满足下边三点, - -* 每一行的数字不能重复 - -* 每一列的数字不能重复 -* 9 个 3 * 3 的小棋盘中的数字也不能重复。 - -只能是 1 - 9 中的数字,不需要考虑数独最后能不能填满。 - -# 解法一 暴力解法 - -需要满足三条,那就一条一条判断。 - -```java -public boolean isValidSudoku(char[][] board) { - //判断每一行 - for (int i = 0; i < 9; i++) { - if (!isValidRows(board[i])) { - return false; - } - } - //判断每一列 - for (int i = 0; i < 9; i++) { - if (!isValidCols(i, board)) { - return false; - } - } - //判断每个小棋盘 - for (int i = 0; i < 9; i = i + 3) { - for (int j = 0; j < 9; j = j + 3) { - if (!isValidSmall(i, j, board)) { - return false; - } - } - - } - return true; -} - -public boolean isValidRows(char[] board) { - HashMap hashMap = new HashMap<>(); - for (char c : board) { - if (c != '.') { - if (hashMap.getOrDefault(c, 0) != 0) { - return false; - } else { - hashMap.put(c, 1); - } - } - } - return true; -} - -public boolean isValidCols(int col, char[][] board) { - HashMap hashMap = new HashMap<>(); - for (int i = 0; i < 9; i++) { - char c = board[i][col]; - if (c != '.') { - if (hashMap.getOrDefault(c, 0) != 0) { - return false; - } else { - hashMap.put(c, 1); - } - } - } - return true; -} - -public boolean isValidSmall(int row, int col, char[][] board) { - HashMap hashMap = new HashMap<>(); - for (int i = 0; i < 3; i++) { - for (int j = 0; j < 3; j++) { - char c = board[row + i][col + j]; - if (c != '.') { - if (hashMap.getOrDefault(c, 0) != 0) { - return false; - } else { - hashMap.put(c, 1); - } - } - } - } - return true; -} -``` - -时间复杂度:整个棋盘访问了三次,如果棋盘大小是 n,那么就是 3n。也就是 O(n)。 - -空间复杂度:O(1)。 - -# 解法二 - -参考[这里](https://leetcode.com/problems/valid-sudoku/discuss/15472/Short%2BSimple-Java-using-Strings),上边的算法遍历了三遍,我们能不能只遍历一遍。 - -我们可以这样想一下,如果有一副纸牌,怎么看它有没有重复的? - -第一种我们可以像之前一样,第一遍先看红桃,再看黑桃,再看方片,再看梅花,这样就看了四遍。我们其实可以每拿到一张牌,就把它放在一个位置,我们把一类放在同一位置。红桃放在一起,黑桃放在一起……放的过程中如果有重复的就可以结束了。 - -在这里的话,我们就可以把第一行的放在一起,第二行的放在一起……第一列的放在一起,第二列的放在一起……第一个小棋盘的放在一起,第二个小棋盘的放在一起…… - -我们用 HashSet 实现放在一起的作用,但是这样的话总共就是 9 行,9 列,9 个小棋盘,27 个 HashSet 了。我们其实可以在放的时候标志一下,例如 - -* 如果第 4 行有一个数字 8,我们就 (8)4,把 "(8)4"放进去。 -* 如果第 5 行有一个数字 6,我们就 5(6),把 "5(6)"放进去。 -* 小棋盘看成一个整体,总共是 9 个,3 行 3 列,如果第 2 行第 1 列的小棋盘里有个数字 3,我们就把 "2(3)1" 放进去。 - -这样 1 个 HashSet 就够了。 - -```java -public boolean isValidSudoku(char[][] board) { - Set seen = new HashSet(); - for (int i=0; i<9; ++i) { - for (int j=0; j<9; ++j) { - if (board[i][j] != '.') { - String b = "(" + board[i][j] + ")"; - if (!seen.add(b + i) || !seen.add(j + b) || !seen.add(i/3 + b + j/3)) - return false; - } - } - } - return true; -} -``` - -时间复杂度:如果棋盘大小总共是 n,那么只遍历了一次,就是 O(n)。 - -空间复杂度:如果棋盘大小总共是 n,最坏的情况就是每个地方都有数字,就需要存三次,O(n)。 - -其实,想到了标识,其实我们可以标识的更彻底些,直接写出来。 - -```java -public boolean isValidSudoku(char[][] board) { - Set seen = new HashSet(); - for (int i=0; i<9; ++i) { - for (int j=0; j<9; ++j) { - char number = board[i][j]; - if (number != '.') - if (!seen.add(number + " in row " + i) || - !seen.add(number + " in column " + j) || - !seen.add(number + " in block " + i/3 + "-" + j/3)) - return false; - } - } - return true; -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/36.png) + +一个 9 * 9 的数独的棋盘。判断已经写入数字的棋盘是不是合法。需要满足下边三点, + +* 每一行的数字不能重复 + +* 每一列的数字不能重复 +* 9 个 3 * 3 的小棋盘中的数字也不能重复。 + +只能是 1 - 9 中的数字,不需要考虑数独最后能不能填满。 + +# 解法一 暴力解法 + +需要满足三条,那就一条一条判断。 + +```java +public boolean isValidSudoku(char[][] board) { + //判断每一行 + for (int i = 0; i < 9; i++) { + if (!isValidRows(board[i])) { + return false; + } + } + //判断每一列 + for (int i = 0; i < 9; i++) { + if (!isValidCols(i, board)) { + return false; + } + } + //判断每个小棋盘 + for (int i = 0; i < 9; i = i + 3) { + for (int j = 0; j < 9; j = j + 3) { + if (!isValidSmall(i, j, board)) { + return false; + } + } + + } + return true; +} + +public boolean isValidRows(char[] board) { + HashMap hashMap = new HashMap<>(); + for (char c : board) { + if (c != '.') { + if (hashMap.getOrDefault(c, 0) != 0) { + return false; + } else { + hashMap.put(c, 1); + } + } + } + return true; +} + +public boolean isValidCols(int col, char[][] board) { + HashMap hashMap = new HashMap<>(); + for (int i = 0; i < 9; i++) { + char c = board[i][col]; + if (c != '.') { + if (hashMap.getOrDefault(c, 0) != 0) { + return false; + } else { + hashMap.put(c, 1); + } + } + } + return true; +} + +public boolean isValidSmall(int row, int col, char[][] board) { + HashMap hashMap = new HashMap<>(); + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + char c = board[row + i][col + j]; + if (c != '.') { + if (hashMap.getOrDefault(c, 0) != 0) { + return false; + } else { + hashMap.put(c, 1); + } + } + } + } + return true; +} +``` + +时间复杂度:整个棋盘访问了三次,如果棋盘大小是 n,那么就是 3n。也就是 O(n)。 + +空间复杂度:O(1)。 + +# 解法二 + +参考[这里](https://leetcode.com/problems/valid-sudoku/discuss/15472/Short%2BSimple-Java-using-Strings),上边的算法遍历了三遍,我们能不能只遍历一遍。 + +我们可以这样想一下,如果有一副纸牌,怎么看它有没有重复的? + +第一种我们可以像之前一样,第一遍先看红桃,再看黑桃,再看方片,再看梅花,这样就看了四遍。我们其实可以每拿到一张牌,就把它放在一个位置,我们把一类放在同一位置。红桃放在一起,黑桃放在一起……放的过程中如果有重复的就可以结束了。 + +在这里的话,我们就可以把第一行的放在一起,第二行的放在一起……第一列的放在一起,第二列的放在一起……第一个小棋盘的放在一起,第二个小棋盘的放在一起…… + +我们用 HashSet 实现放在一起的作用,但是这样的话总共就是 9 行,9 列,9 个小棋盘,27 个 HashSet 了。我们其实可以在放的时候标志一下,例如 + +* 如果第 4 行有一个数字 8,我们就 (8)4,把 "(8)4"放进去。 +* 如果第 5 行有一个数字 6,我们就 5(6),把 "5(6)"放进去。 +* 小棋盘看成一个整体,总共是 9 个,3 行 3 列,如果第 2 行第 1 列的小棋盘里有个数字 3,我们就把 "2(3)1" 放进去。 + +这样 1 个 HashSet 就够了。 + +```java +public boolean isValidSudoku(char[][] board) { + Set seen = new HashSet(); + for (int i=0; i<9; ++i) { + for (int j=0; j<9; ++j) { + if (board[i][j] != '.') { + String b = "(" + board[i][j] + ")"; + if (!seen.add(b + i) || !seen.add(j + b) || !seen.add(i/3 + b + j/3)) + return false; + } + } + } + return true; +} +``` + +时间复杂度:如果棋盘大小总共是 n,那么只遍历了一次,就是 O(n)。 + +空间复杂度:如果棋盘大小总共是 n,最坏的情况就是每个地方都有数字,就需要存三次,O(n)。 + +其实,想到了标识,其实我们可以标识的更彻底些,直接写出来。 + +```java +public boolean isValidSudoku(char[][] board) { + Set seen = new HashSet(); + for (int i=0; i<9; ++i) { + for (int j=0; j<9; ++j) { + char number = board[i][j]; + if (number != '.') + if (!seen.add(number + " in row " + i) || + !seen.add(number + " in column " + j) || + !seen.add(number + " in block " + i/3 + "-" + j/3)) + return false; + } + } + return true; +} +``` + +# 总 + 第二种解法的作者太太聪明了!自己规定格式这种思想,很棒。 \ No newline at end of file diff --git a/leetCode-37-Sudoku-Solver.md b/leetCode-37-Sudoku-Solver.md index 4011e83ca..25d4f7b57 100644 --- a/leetCode-37-Sudoku-Solver.md +++ b/leetCode-37-Sudoku-Solver.md @@ -1,72 +1,72 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/37.png) - -给定一个数独棋盘,输出它的一个解。 - -# 解法一 回溯法 - -从上到下,从左到右遍历每个空位置。在第一个位置,随便填一个可以填的数字,再在第二个位置填一个可以填的数字,一直执行下去直到最后一个位置。期间如果出现没有数字可以填的话,就回退到上一个位置,换一下数字,再向后进行下去。 - -```java -public void solveSudoku(char[][] board) { - solver(board); -} -private boolean solver(char[][] board) { - for (int i = 0; i < 9; i++) { - for (int j = 0; j < 9; j++) { - if (board[i][j] == '.') { - char count = '1'; - while (count <= '9') { - if (isValid(i, j, board, count)) { - board[i][j] = count; - if (solver(board)) { - return true; - } else { - //下一个位置没有数字,就还原,然后当前位置尝试新的数 - board[i][j] = '.'; - } - } - count++; - } - return false; - } - } - } - return true; -} - -private boolean isValid(int row, int col, char[][] board, char c) { - for (int i = 0; i < 9; i++) { - if (board[row][i] == c) { - return false; - } - } - - for (int i = 0; i < 9; i++) { - if (board[i][col] == c) { - return false; - } - } - - int start_row = row / 3 * 3; - int start_col = col / 3 * 3; - for (int i = 0; i < 3; i++) { - for (int j = 0; j < 3; j++) { - if (board[start_row + i][start_col + j] == c) { - return false; - } - } - - } - return true; -} -``` - -时间复杂度: - -空间复杂度:O(1)。 - -# 总 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/37.png) + +给定一个数独棋盘,输出它的一个解。 + +# 解法一 回溯法 + +从上到下,从左到右遍历每个空位置。在第一个位置,随便填一个可以填的数字,再在第二个位置填一个可以填的数字,一直执行下去直到最后一个位置。期间如果出现没有数字可以填的话,就回退到上一个位置,换一下数字,再向后进行下去。 + +```java +public void solveSudoku(char[][] board) { + solver(board); +} +private boolean solver(char[][] board) { + for (int i = 0; i < 9; i++) { + for (int j = 0; j < 9; j++) { + if (board[i][j] == '.') { + char count = '1'; + while (count <= '9') { + if (isValid(i, j, board, count)) { + board[i][j] = count; + if (solver(board)) { + return true; + } else { + //下一个位置没有数字,就还原,然后当前位置尝试新的数 + board[i][j] = '.'; + } + } + count++; + } + return false; + } + } + } + return true; +} + +private boolean isValid(int row, int col, char[][] board, char c) { + for (int i = 0; i < 9; i++) { + if (board[row][i] == c) { + return false; + } + } + + for (int i = 0; i < 9; i++) { + if (board[i][col] == c) { + return false; + } + } + + int start_row = row / 3 * 3; + int start_col = col / 3 * 3; + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + if (board[start_row + i][start_col + j] == c) { + return false; + } + } + + } + return true; +} +``` + +时间复杂度: + +空间复杂度:O(1)。 + +# 总 + 回溯法一个很典型的应用了。 \ No newline at end of file diff --git a/leetCode-38-Count-and-Say.md b/leetCode-38-Count-and-Say.md index 9544457d4..53a8d5aaa 100644 --- a/leetCode-38-Count-and-Say.md +++ b/leetCode-38-Count-and-Say.md @@ -1,116 +1,116 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/38.jpg) - -难在了题目是什么意思呢? - -初始值第一行是 1。 - -第二行读第一行,1 个 1,去掉个字,所以第二行就是 11。 - -第三行读第二行,2 个 1,去掉个字,所以第三行就是 21。 - -第四行读第三行,1 个 2,1 个 1,去掉所有个字,所以第四行就是 1211。 - -第五行读第四行,1 个 1,1 个 2,2 个 1,去掉所有个字,所以第五航就是 111221。 - -第六行读第五行,3 个 1,2 个 2,1 个 1,去掉所以个字,所以第六行就是 312211。 - -然后题目要求输入 1 - 30 的任意行数,输出该行是啥。 - -# 解法一 递归 - -可以看出来,我们只要知道了 n - 1 行,就可以写出第 n 行了,首先想到的就是递归。 - -第五行是 111221,求第六行的话,我们只需要知道每个字符重复的次数加上当前字符就行啦。 - -1 重复 3 次,就是 31,2 重复 2 次就是 22,1 重复 1 次 就是 11,所以最终结果就是 312211。 - -```java -public String countAndSay(int n) { - //第一行就直接输出 - if (n == 1) { - return "1"; - } - //得到上一行的字符串 - String last = countAndSay(n - 1); - //输出当前行的字符串 - return getNextString(last); - -} - -private String getNextString(String last) { - //长度为 0 就返回空字符串 - if (last.length() == 0) { - return ""; - } - //得到第 1 个字符重复的次数 - int num = getRepeatNum(last); - // 次数 + 当前字符 + 其余的字符串的情况 - return num + "" + last.charAt(0) + getNextString(last.substring(num)); -} - -//得到字符 string[0] 的重复个数,例如 "111221" 返回 3 -private int getRepeatNum(String string) { - int count = 1; - char same = string.charAt(0); - for (int i = 1; i < string.length(); i++) { - if (same == string.charAt(i)) { - count++; - } else { - break; - } - } - return count; -} -``` - -时间复杂度: - -空间复杂度:O(1)。 - -# 解法二 迭代 - -既然有递归,那就一定可以写出它的迭代形式。 - -```java -public String countAndSay(int n) { - String res = "1"; - //从第一行开始,一行一行产生 - while (n > 1) { - String temp = ""; - for (int i = 0; i < res.length(); i++) { - int num = getRepeatNum(res.substring(i)); - temp = temp + num + "" + res.charAt(i); - //跳过重复的字符 - i = i + num - 1; - } - n--; - //更新 - res = temp; - } - return res; - -} -//得到字符 string[0] 的重复个数,例如 "111221" 返回 3 -private int getRepeatNum(String string) { - int count = 1; - char same = string.charAt(0); - for (int i = 1; i < string.length(); i++) { - if (same == string.charAt(i)) { - count++; - } else { - break; - } - } - return count; -} -``` - -时间复杂度: - -空间复杂度:O(1)。 - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/38.jpg) + +难在了题目是什么意思呢? + +初始值第一行是 1。 + +第二行读第一行,1 个 1,去掉个字,所以第二行就是 11。 + +第三行读第二行,2 个 1,去掉个字,所以第三行就是 21。 + +第四行读第三行,1 个 2,1 个 1,去掉所有个字,所以第四行就是 1211。 + +第五行读第四行,1 个 1,1 个 2,2 个 1,去掉所有个字,所以第五航就是 111221。 + +第六行读第五行,3 个 1,2 个 2,1 个 1,去掉所以个字,所以第六行就是 312211。 + +然后题目要求输入 1 - 30 的任意行数,输出该行是啥。 + +# 解法一 递归 + +可以看出来,我们只要知道了 n - 1 行,就可以写出第 n 行了,首先想到的就是递归。 + +第五行是 111221,求第六行的话,我们只需要知道每个字符重复的次数加上当前字符就行啦。 + +1 重复 3 次,就是 31,2 重复 2 次就是 22,1 重复 1 次 就是 11,所以最终结果就是 312211。 + +```java +public String countAndSay(int n) { + //第一行就直接输出 + if (n == 1) { + return "1"; + } + //得到上一行的字符串 + String last = countAndSay(n - 1); + //输出当前行的字符串 + return getNextString(last); + +} + +private String getNextString(String last) { + //长度为 0 就返回空字符串 + if (last.length() == 0) { + return ""; + } + //得到第 1 个字符重复的次数 + int num = getRepeatNum(last); + // 次数 + 当前字符 + 其余的字符串的情况 + return num + "" + last.charAt(0) + getNextString(last.substring(num)); +} + +//得到字符 string[0] 的重复个数,例如 "111221" 返回 3 +private int getRepeatNum(String string) { + int count = 1; + char same = string.charAt(0); + for (int i = 1; i < string.length(); i++) { + if (same == string.charAt(i)) { + count++; + } else { + break; + } + } + return count; +} +``` + +时间复杂度: + +空间复杂度:O(1)。 + +# 解法二 迭代 + +既然有递归,那就一定可以写出它的迭代形式。 + +```java +public String countAndSay(int n) { + String res = "1"; + //从第一行开始,一行一行产生 + while (n > 1) { + String temp = ""; + for (int i = 0; i < res.length(); i++) { + int num = getRepeatNum(res.substring(i)); + temp = temp + num + "" + res.charAt(i); + //跳过重复的字符 + i = i + num - 1; + } + n--; + //更新 + res = temp; + } + return res; + +} +//得到字符 string[0] 的重复个数,例如 "111221" 返回 3 +private int getRepeatNum(String string) { + int count = 1; + char same = string.charAt(0); + for (int i = 1; i < string.length(); i++) { + if (same == string.charAt(i)) { + count++; + } else { + break; + } + } + return count; +} +``` + +时间复杂度: + +空间复杂度:O(1)。 + +# 总 + 递归里边又用了一个递归,我觉得这点有点意思。 \ No newline at end of file diff --git a/leetCode-39-Combination-Sum.md b/leetCode-39-Combination-Sum.md index 454247231..2afe3515e 100644 --- a/leetCode-39-Combination-Sum.md +++ b/leetCode-39-Combination-Sum.md @@ -1,245 +1,245 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/39.jpg) - -给几个数字,一个目标值,输出所有和等于目标值的组合。 - -# 解法一 回溯法 - -参考[这里](https://leetcode.com/problems/combination-sum/discuss/16502/A-general-approach-to-backtracking-questions-in-Java-(Subsets-Permutations-Combination-Sum-Palindrome-Partitioning)) ,就是先向前列举所有情况,得到一个解或者走不通的时候就回溯。和[37](https://leetcode.windliang.cc/leetCode-37-Sudoku-Solver.html)题有异曲同工之处,也算是回溯法很典型的应用,直接看代码吧。 - -```java -public List> combinationSum(int[] nums, int target) { - List> list = new ArrayList<>(); - backtrack(list, new ArrayList<>(), nums, target, 0); - return list; -} - -private void backtrack(List> list, List tempList, int [] nums, int remain, int start){ - if(remain < 0) return; - else if(remain == 0) list.add(new ArrayList<>(tempList)); - else{ - for(int i = start; i < nums.length; i++){ - tempList.add(nums[i]); - backtrack(list, tempList, nums, remain - nums[i], i); - //找到了一个解或者 remain < 0 了,将当前数字移除,然后继续尝试 - tempList.remove(tempList.size() - 1); - } - } -} -``` - -时间复杂度: - -空间复杂度: - -# 解法二 动态规划 - -参考[这里](https://leetcode.com/problems/combination-sum/discuss/16656/Dynamic-Programming-Solution?orderBy=most_votes)。动态规划的关键就是找到递进关系,看到了下边的评论想通的。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/39_1.jpg) - -我们用一个 opt 的 list,然后依次求出 opt [ 0 ],opt [ 1 ] ... opt [ target ]。 - -opt[0],表示和为 0 的所有情况的组合。 - -opt[1],表示和为 1 的所有情况的组合。 - -opt[2],表示和为 2 的所有情况的组合。 - -... - -opt[target],表示和为 target 的所有情况的组合,也就是题目所要求的。 - -递进关系就是,sum 代表要求的和,如果想求 opt [ sum ] ,就遍历给定的数组 nums,然后分两种情况。 - -* 如果 sum 刚好等于 nums [ i ],那么就直接把 nums [ i ] 加到 list 里,算作一种情况。 - - 例如 nums = [ 2, 3, 6, 7 ] , target = 7。 - - 当求 sum = 3 的时候,也就是求 opt [ 3 ] 的时候,此时当遍历到 nums [ 1 ],此时 nums [ 1 ] == sum == 3,所以此时 opt [ 3 ] = [ [ 3 ] ]。 - -* 如果 sum 大于 nums [ i ],那么我们就把 opt [ sum - nums [ i ] ] 的所有情况都加上 nums [ i ] 然后作为 opt [ sum ] 。 - - 例如 nums = [ 1, 2, 3, 6, 7 ] , target = 7。 - - 当 sum 等于 5 的时候,也就是求 opt [ 5 ] 的时候,此时当遍历到 nums [ 1 ],此时 nums [ 1 ] = 2 < sum。然后,就看 opt [ sum - nums [ i ] ] = opt [ 5 - 2 ] = opt [ 3 ],而 opt [ 3 ] 在之前已经求好了,opt [ 3 ] = [ [ 1, 2 ], [ 3 ] ],然后把 opt [ 3 ] 中的每一种情况都加上 nums [ 1 ] ,也就是 2,就变成了 [ [ 1, 2, 2 ], [ 3, 2 ] ],这个就是遍历到 nums [ 1 ] 时候的 opt [ 5 ]了。 - -上边的想法看起来没什么问题,但跑了下遇到一个问题。 - -比如求 nums = [ 2, 3, 6, 7 ] , target = 7 的时候。 - -求 opt [ 5 ],然后遍历到 nums [ 0 ] = 2 的时候,就把 opt [ 3 ] = [ [ 3 ] ] 的所有情况加上 2,也就是[ 3 2 ] 加到 opt [ 5 ] 上。接着遍历到 nums [ 2 ] = 3 的时候,就把 opt [ 2 ] = [ [ 2 ] ] 的所有情况加上 3,然后 [ 2 3 ] 这种情况加到 opt [ 5 ] 上,此时 opt [ 5 ] = [ [ 3 2],[ 2 3 ] ]。这样出现了重复的情况,需要解决一下。 - -这样就相当于二维数组去重,也就是 [ [ 3 2 ],[ 2 3 ] , [ 1 ] ] 这样的列表去重变成 [ [ 2 3 ] , [ 1 ] ] 。最普通的想法就是两个 for 循环然后一个一个比对,把重复的去掉。但这样实在是太麻烦了,因为比对的时候又要比对列表是否相等,比对列表是否相等又比较麻烦。 - -[这里](https://bbs.csdn.net/topics/360189572)看到一个方法,就是把每个 list 转成 string,然后利用 HashMap 的 key 是唯一的,把每个 list 当做 key 加入到 HashMap 中,这样就实现了去重,然后再把 string 还原为 list。 - -```java -private List> removeDuplicate(List> list) { - Map ans = new HashMap(); - for (int i = 0; i < list.size(); i++) { - List l = list.get(i); - Collections.sort(l); - String key = ""; - //[ 2 3 4] 转为 "2,3,4" - for (int j = 0; j < l.size() - 1; j++) { - key = key + l.get(j) + ","; - } - key = key + l.get(l.size() - 1); - ans.put(key, ""); - } - //根据逗号还原 List - List> ans_list = new ArrayList>(); - for (String k : ans.keySet()) { - String[] l = k.split(","); - List temp = new ArrayList(); - for (int i = 0; i < l.length; i++) { - int c = Integer.parseInt(l[i]); - temp.add(c); - } - ans_list.add(temp); - } - return ans_list; -} -``` - -然后结合去重的方法,我们的问题就解决了。 - -```java -public List> combinationSum(int[] nums, int target) { - List>> ans = new ArrayList<>(); //opt 数组 - Arrays.sort(nums);// 将数组有序,这样可以提现结束循环 - for (int sum = 0; sum <= target; sum++) { // 从 0 到 target 求出每一个 opt - List> ans_sum = new ArrayList>(); - for (int i = 0; i < nums.length; i++) { //遍历 nums - if (nums[i] == sum) { - List temp = new ArrayList(); - temp.add(nums[i]); - ans_sum.add(temp); - } else if (nums[i] < sum) { - List> ans_sub = ans.get(sum - nums[i]); - //每一个加上 nums[i] - for (int j = 0; j < ans_sub.size(); j++) { - List temp = new ArrayList(ans_sub.get(j)); - temp.add(nums[i]); - ans_sum.add(temp); - } - } else { - break; - } - } - ans.add(sum, ans_sum); - } - return removeDuplicate(ans.get(target)); -} - -private List> removeDuplicate(List> list) { - Map ans = new HashMap(); - for (int i = 0; i < list.size(); i++) { - List l = list.get(i); - Collections.sort(l); - String key = ""; - //[ 2 3 4 ] 转为 "2,3,4" - for (int j = 0; j < l.size() - 1; j++) { - key = key + l.get(j) + ","; - } - key = key + l.get(l.size() - 1); - ans.put(key, ""); - } - //根据逗号还原 List - List> ans_list = new ArrayList>(); - for (String k : ans.keySet()) { - String[] l = k.split(","); - List temp = new ArrayList(); - for (int i = 0; i < l.length; i++) { - int c = Integer.parseInt(l[i]); - temp.add(c); - } - ans_list.add(temp); - } - return ans_list; -} - -``` - -时间复杂度: - -空间复杂度: - -还有另一种思路可以解决重复的问题。 - -之前对于 nums = [ 2, 3, 6, 7 ] , target = 7 ,我们用了两层 for 循环,分别对 opt 和 nums 进行遍历。 - -我们先求 opt [ 0 ],通过遍历 nums [ 0 ], nums [ 1 ], nums [ 2 ], nums [ 3 ] - -然后再求 opt [ 1 ],通过遍历 nums [ 0 ], nums [ 1 ], nums [ 2 ], nums [ 3 ] - -然后再求 opt [ 2 ],通过遍历 nums [ 0 ], nums [ 1 ], nums [ 2 ], nums [ 3 ] - -... - -最后再求 opt [ 7 ],通过遍历 nums [ 0 ], nums [ 1 ], nums [ 2 ], nums [ 3 ]。 - -求 opt [ 5 ] 的时候,出现了 [ 2 3 ],[ 3 2 ] 这样重复的情况。 - -我们可以把两个 for 循环的遍历颠倒一下,外层遍历 nums,内层遍历 opt。 - -考虑 nums [ 0 ],求出 opt [ 0 ],求出 opt [ 1 ],求出 opt [ 2 ],求出 opt [ 3 ] ... 求出 opt [ 7 ]。 - -考虑 nums [ 1 ],求出 opt [ 0 ],求出 opt [ 1 ],求出 opt [ 2 ],求出 opt [ 3 ] ... 求出 opt [ 7 ]。 - -考虑 nums [ 2 ],求出 opt [ 0 ],求出 opt [ 1 ],求出 opt [ 2 ],求出 opt [ 3 ] ... 求出 opt [ 7 ]。 - -考虑 nums [ 3 ],求出 opt [ 0 ],求出 opt [ 1 ],求出 opt [ 2 ],求出 opt [ 3 ] ... 求出 opt [ 7 ]。 - -这样的话,每次循环会更新一次 opt [ 7 ],最后次更新的 opt [ 7 ] 就是我们想要的了。 - -这样之前的问题,求 opt [ 5 ] 的时候,出现了 [ 2 3 ],[ 3 2 ] 这样重复的情况就不会出现了,因为当考虑 nums [ 2 ] 的时候,opt [ 3 ] 里边还没有加入 [ 3 ] 。 - -思路就是上边说的了,但是写代码的时候遇到不少坑,大家也可以先尝试写一下。 - -```java -public List> combinationSum(int[] nums, int target) { - List>> ans = new ArrayList<>(); - Arrays.sort(nums); - if (nums[0] > target) { - return new ArrayList>(); - } - // 先初始化 ans[0] 到 ans[target],因为每次循环是更新 ans,会用到 ans.get() 函数,如果不初始化会报错 - for (int i = 0; i <= target; i++) { - List> ans_i = new ArrayList>(); - ans.add(i, ans_i); - } - - for (int i = 0; i < nums.length; i++) { - for (int sum = nums[i]; sum <= target; sum++) { - List> ans_sum = ans.get(sum); - List> ans_sub = ans.get(sum - nums[i]); - //刚开始 ans_sub 的大小是 0,所以单独考虑一下这种情况 - if (sum == nums[i]) { - ArrayList temp = new ArrayList(); - temp.add(nums[i]); - ans_sum.add(temp); - - } - //如果 ans.get(sum - nums[i])大小不等于 0,就可以按之前的想法更新了。 - //每个 ans_sub[j] 都加上 nums[i] - if (ans_sub.size() > 0) { - for (int j = 0; j < ans_sub.size(); j++) { - ArrayList temp = new ArrayList(ans_sub.get(j)); - temp.add(nums[i]); - ans_sum.add(temp); - } - } - } - } - return ans.get(target); -} -``` - - - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/39.jpg) + +给几个数字,一个目标值,输出所有和等于目标值的组合。 + +# 解法一 回溯法 + +参考[这里](https://leetcode.com/problems/combination-sum/discuss/16502/A-general-approach-to-backtracking-questions-in-Java-(Subsets-Permutations-Combination-Sum-Palindrome-Partitioning)) ,就是先向前列举所有情况,得到一个解或者走不通的时候就回溯。和[37](https://leetcode.windliang.cc/leetCode-37-Sudoku-Solver.html)题有异曲同工之处,也算是回溯法很典型的应用,直接看代码吧。 + +```java +public List> combinationSum(int[] nums, int target) { + List> list = new ArrayList<>(); + backtrack(list, new ArrayList<>(), nums, target, 0); + return list; +} + +private void backtrack(List> list, List tempList, int [] nums, int remain, int start){ + if(remain < 0) return; + else if(remain == 0) list.add(new ArrayList<>(tempList)); + else{ + for(int i = start; i < nums.length; i++){ + tempList.add(nums[i]); + backtrack(list, tempList, nums, remain - nums[i], i); + //找到了一个解或者 remain < 0 了,将当前数字移除,然后继续尝试 + tempList.remove(tempList.size() - 1); + } + } +} +``` + +时间复杂度: + +空间复杂度: + +# 解法二 动态规划 + +参考[这里](https://leetcode.com/problems/combination-sum/discuss/16656/Dynamic-Programming-Solution?orderBy=most_votes)。动态规划的关键就是找到递进关系,看到了下边的评论想通的。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/39_1.jpg) + +我们用一个 opt 的 list,然后依次求出 opt [ 0 ],opt [ 1 ] ... opt [ target ]。 + +opt[0],表示和为 0 的所有情况的组合。 + +opt[1],表示和为 1 的所有情况的组合。 + +opt[2],表示和为 2 的所有情况的组合。 + +... + +opt[target],表示和为 target 的所有情况的组合,也就是题目所要求的。 + +递进关系就是,sum 代表要求的和,如果想求 opt [ sum ] ,就遍历给定的数组 nums,然后分两种情况。 + +* 如果 sum 刚好等于 nums [ i ],那么就直接把 nums [ i ] 加到 list 里,算作一种情况。 + + 例如 nums = [ 2, 3, 6, 7 ] , target = 7。 + + 当求 sum = 3 的时候,也就是求 opt [ 3 ] 的时候,此时当遍历到 nums [ 1 ],此时 nums [ 1 ] == sum == 3,所以此时 opt [ 3 ] = [ [ 3 ] ]。 + +* 如果 sum 大于 nums [ i ],那么我们就把 opt [ sum - nums [ i ] ] 的所有情况都加上 nums [ i ] 然后作为 opt [ sum ] 。 + + 例如 nums = [ 1, 2, 3, 6, 7 ] , target = 7。 + + 当 sum 等于 5 的时候,也就是求 opt [ 5 ] 的时候,此时当遍历到 nums [ 1 ],此时 nums [ 1 ] = 2 < sum。然后,就看 opt [ sum - nums [ i ] ] = opt [ 5 - 2 ] = opt [ 3 ],而 opt [ 3 ] 在之前已经求好了,opt [ 3 ] = [ [ 1, 2 ], [ 3 ] ],然后把 opt [ 3 ] 中的每一种情况都加上 nums [ 1 ] ,也就是 2,就变成了 [ [ 1, 2, 2 ], [ 3, 2 ] ],这个就是遍历到 nums [ 1 ] 时候的 opt [ 5 ]了。 + +上边的想法看起来没什么问题,但跑了下遇到一个问题。 + +比如求 nums = [ 2, 3, 6, 7 ] , target = 7 的时候。 + +求 opt [ 5 ],然后遍历到 nums [ 0 ] = 2 的时候,就把 opt [ 3 ] = [ [ 3 ] ] 的所有情况加上 2,也就是[ 3 2 ] 加到 opt [ 5 ] 上。接着遍历到 nums [ 2 ] = 3 的时候,就把 opt [ 2 ] = [ [ 2 ] ] 的所有情况加上 3,然后 [ 2 3 ] 这种情况加到 opt [ 5 ] 上,此时 opt [ 5 ] = [ [ 3 2],[ 2 3 ] ]。这样出现了重复的情况,需要解决一下。 + +这样就相当于二维数组去重,也就是 [ [ 3 2 ],[ 2 3 ] , [ 1 ] ] 这样的列表去重变成 [ [ 2 3 ] , [ 1 ] ] 。最普通的想法就是两个 for 循环然后一个一个比对,把重复的去掉。但这样实在是太麻烦了,因为比对的时候又要比对列表是否相等,比对列表是否相等又比较麻烦。 + +[这里](https://bbs.csdn.net/topics/360189572)看到一个方法,就是把每个 list 转成 string,然后利用 HashMap 的 key 是唯一的,把每个 list 当做 key 加入到 HashMap 中,这样就实现了去重,然后再把 string 还原为 list。 + +```java +private List> removeDuplicate(List> list) { + Map ans = new HashMap(); + for (int i = 0; i < list.size(); i++) { + List l = list.get(i); + Collections.sort(l); + String key = ""; + //[ 2 3 4] 转为 "2,3,4" + for (int j = 0; j < l.size() - 1; j++) { + key = key + l.get(j) + ","; + } + key = key + l.get(l.size() - 1); + ans.put(key, ""); + } + //根据逗号还原 List + List> ans_list = new ArrayList>(); + for (String k : ans.keySet()) { + String[] l = k.split(","); + List temp = new ArrayList(); + for (int i = 0; i < l.length; i++) { + int c = Integer.parseInt(l[i]); + temp.add(c); + } + ans_list.add(temp); + } + return ans_list; +} +``` + +然后结合去重的方法,我们的问题就解决了。 + +```java +public List> combinationSum(int[] nums, int target) { + List>> ans = new ArrayList<>(); //opt 数组 + Arrays.sort(nums);// 将数组有序,这样可以提现结束循环 + for (int sum = 0; sum <= target; sum++) { // 从 0 到 target 求出每一个 opt + List> ans_sum = new ArrayList>(); + for (int i = 0; i < nums.length; i++) { //遍历 nums + if (nums[i] == sum) { + List temp = new ArrayList(); + temp.add(nums[i]); + ans_sum.add(temp); + } else if (nums[i] < sum) { + List> ans_sub = ans.get(sum - nums[i]); + //每一个加上 nums[i] + for (int j = 0; j < ans_sub.size(); j++) { + List temp = new ArrayList(ans_sub.get(j)); + temp.add(nums[i]); + ans_sum.add(temp); + } + } else { + break; + } + } + ans.add(sum, ans_sum); + } + return removeDuplicate(ans.get(target)); +} + +private List> removeDuplicate(List> list) { + Map ans = new HashMap(); + for (int i = 0; i < list.size(); i++) { + List l = list.get(i); + Collections.sort(l); + String key = ""; + //[ 2 3 4 ] 转为 "2,3,4" + for (int j = 0; j < l.size() - 1; j++) { + key = key + l.get(j) + ","; + } + key = key + l.get(l.size() - 1); + ans.put(key, ""); + } + //根据逗号还原 List + List> ans_list = new ArrayList>(); + for (String k : ans.keySet()) { + String[] l = k.split(","); + List temp = new ArrayList(); + for (int i = 0; i < l.length; i++) { + int c = Integer.parseInt(l[i]); + temp.add(c); + } + ans_list.add(temp); + } + return ans_list; +} + +``` + +时间复杂度: + +空间复杂度: + +还有另一种思路可以解决重复的问题。 + +之前对于 nums = [ 2, 3, 6, 7 ] , target = 7 ,我们用了两层 for 循环,分别对 opt 和 nums 进行遍历。 + +我们先求 opt [ 0 ],通过遍历 nums [ 0 ], nums [ 1 ], nums [ 2 ], nums [ 3 ] + +然后再求 opt [ 1 ],通过遍历 nums [ 0 ], nums [ 1 ], nums [ 2 ], nums [ 3 ] + +然后再求 opt [ 2 ],通过遍历 nums [ 0 ], nums [ 1 ], nums [ 2 ], nums [ 3 ] + +... + +最后再求 opt [ 7 ],通过遍历 nums [ 0 ], nums [ 1 ], nums [ 2 ], nums [ 3 ]。 + +求 opt [ 5 ] 的时候,出现了 [ 2 3 ],[ 3 2 ] 这样重复的情况。 + +我们可以把两个 for 循环的遍历颠倒一下,外层遍历 nums,内层遍历 opt。 + +考虑 nums [ 0 ],求出 opt [ 0 ],求出 opt [ 1 ],求出 opt [ 2 ],求出 opt [ 3 ] ... 求出 opt [ 7 ]。 + +考虑 nums [ 1 ],求出 opt [ 0 ],求出 opt [ 1 ],求出 opt [ 2 ],求出 opt [ 3 ] ... 求出 opt [ 7 ]。 + +考虑 nums [ 2 ],求出 opt [ 0 ],求出 opt [ 1 ],求出 opt [ 2 ],求出 opt [ 3 ] ... 求出 opt [ 7 ]。 + +考虑 nums [ 3 ],求出 opt [ 0 ],求出 opt [ 1 ],求出 opt [ 2 ],求出 opt [ 3 ] ... 求出 opt [ 7 ]。 + +这样的话,每次循环会更新一次 opt [ 7 ],最后次更新的 opt [ 7 ] 就是我们想要的了。 + +这样之前的问题,求 opt [ 5 ] 的时候,出现了 [ 2 3 ],[ 3 2 ] 这样重复的情况就不会出现了,因为当考虑 nums [ 2 ] 的时候,opt [ 3 ] 里边还没有加入 [ 3 ] 。 + +思路就是上边说的了,但是写代码的时候遇到不少坑,大家也可以先尝试写一下。 + +```java +public List> combinationSum(int[] nums, int target) { + List>> ans = new ArrayList<>(); + Arrays.sort(nums); + if (nums[0] > target) { + return new ArrayList>(); + } + // 先初始化 ans[0] 到 ans[target],因为每次循环是更新 ans,会用到 ans.get() 函数,如果不初始化会报错 + for (int i = 0; i <= target; i++) { + List> ans_i = new ArrayList>(); + ans.add(i, ans_i); + } + + for (int i = 0; i < nums.length; i++) { + for (int sum = nums[i]; sum <= target; sum++) { + List> ans_sum = ans.get(sum); + List> ans_sub = ans.get(sum - nums[i]); + //刚开始 ans_sub 的大小是 0,所以单独考虑一下这种情况 + if (sum == nums[i]) { + ArrayList temp = new ArrayList(); + temp.add(nums[i]); + ans_sum.add(temp); + + } + //如果 ans.get(sum - nums[i])大小不等于 0,就可以按之前的想法更新了。 + //每个 ans_sub[j] 都加上 nums[i] + if (ans_sub.size() > 0) { + for (int j = 0; j < ans_sub.size(); j++) { + ArrayList temp = new ArrayList(ans_sub.get(j)); + temp.add(nums[i]); + ans_sum.add(temp); + } + } + } + } + return ans.get(target); +} +``` + + + +# 总 + 对回溯法又有了更深的了解,一般的架构就是一个大的 for 循环,然后先 add,接着利用递归进行向前遍历,然后再 remove ,继续循环。而解法二的动态规划就是一定要找到递进的规则,开始的时候就想偏了,导致迟迟想不出来。 \ No newline at end of file diff --git a/leetCode-4-Median-of-Two-Sorted-Arrays.md b/leetCode-4-Median-of-Two-Sorted-Arrays.md index 760b385d6..0341877ed 100644 --- a/leetCode-4-Median-of-Two-Sorted-Arrays.md +++ b/leetCode-4-Median-of-Two-Sorted-Arrays.md @@ -1,337 +1,337 @@ -## 题目描述(困难难度) - -![](http://windliang.oss-cn-beijing.aliyuncs.com/3_median.jpg) - -已知两个有序数组,找到两个数组合并后的中位数。 - -## 解法一 - -简单粗暴,先将两个数组合并,两个有序数组的合并也是归并排序中的一部分。然后根据奇数,还是偶数,返回中位数。 - -## 代码 - -```java -public double findMedianSortedArrays(int[] nums1, int[] nums2) { - int[] nums; - int m = nums1.length; - int n = nums2.length; - nums = new int[m + n]; - if (m == 0) { - if (n % 2 == 0) { - return (nums2[n / 2 - 1] + nums2[n / 2]) / 2.0; - } else { - - return nums2[n / 2]; - } - } - if (n == 0) { - if (m % 2 == 0) { - return (nums1[m / 2 - 1] + nums1[m / 2]) / 2.0; - } else { - return nums1[m / 2]; - } - } - - int count = 0; - int i = 0, j = 0; - while (count != (m + n)) { - if (i == m) { - while (j != n) { - nums[count++] = nums2[j++]; - } - break; - } - if (j == n) { - while (i != m) { - nums[count++] = nums1[i++]; - } - break; - } - - if (nums1[i] < nums2[j]) { - nums[count++] = nums1[i++]; - } else { - nums[count++] = nums2[j++]; - } - } - - if (count % 2 == 0) { - return (nums[count / 2 - 1] + nums[count / 2]) / 2.0; - } else { - return nums[count / 2]; - } -} -``` - -时间复杂度:遍历全部数组,O(m + n) - -空间复杂度:开辟了一个数组,保存合并后的两个数组,O(m + n) - -## 解法二 - -其实,我们不需要将两个数组真的合并,我们只需要找到中位数在哪里就可以了。 - -开始的思路是写一个循环,然后里边判断是否到了中位数的位置,到了就返回结果,但这里对偶数和奇数的分类会很麻烦。当其中一个数组遍历完后,出了 for 循环对边界的判断也会分几种情况。总体来说,虽然复杂度不影响,但代码会看起来很乱。然后在 [这里](https://blog.csdn.net/lxhpkm01/article/details/53823595) 找到了另一种思路。 - - - -首先是怎么将奇数和偶数的情况合并一下。 - -用 len 表示合并后数组的长度,如果是奇数,我们需要知道第 (len + 1)/ 2 个数就可以了,如果遍历的话需要遍历 int ( len / 2 ) + 1 次。如果是偶数,我们需要知道第 len / 2 和 len / 2 + 1 个数,也是需要遍历 len / 2 + 1 次。所以遍历的话,奇数和偶数都是 len / 2 + 1 次。 - - - -返回中位数的话,奇数需要最后一次遍历的结果就可以了,偶数需要最后一次和上一次遍历的结果。所以我们用两个变量 left 和 right ,right 保存当前循环的结果,在每次循环前将 right 的值赋给 left 。这样在最后一次循环的时候,left 将得到 right 的值,也就是上一次循环的结果,接下来 right 更新为最后一次的结果。 - - - -循环中该怎么写,什么时候 A 数组后移,什么时候 B 数组后移。用 aStart 和 bStart 分别表示当前指向 A 数组和 B 数组的位置。如果 aStart 还没有到最后并且此时 A 位置的数字小于 B 位置的数组,那么就可以后移了。也就是aStart < m && A[aStart] < B[bStart]。 - -但如果 B 数组此刻已经没有数字了,继续取数字B [ bStart ],则会越界,所以判断下 bStart 是否大于数组长度了,这样 || 后边的就不会执行了,也就不会导致错误了,所以增加为 aStart < m && ( bStart >= n || A [ aStart ] < B [ bStart ] ) 。 - -## 代码 - -``` java -public double findMedianSortedArrays(int[] A, int[] B) { - int m = A.length; - int n = B.length; - int len = m + n; - int left = -1, right = -1; - int aStart = 0, bStart = 0; - for (int i = 0; i <= len / 2; i++) { - left = right; - if (aStart < m && (bStart >= n || A[aStart] < B[bStart])) { - right = A[aStart++]; - } else { - right = B[bStart++]; - } - } - if ((len & 1) == 0) - return (left + right) / 2.0; - else - return right; -} -``` - -时间复杂度:遍历 len/2 + 1 次,len = m + n ,所以时间复杂度依旧是 O(m + n)。 - -空间复杂度:我们申请了常数个变量,也就是 m,n,len,left,right,aStart,bStart 以及 i 。 - -总共 8 个变量,所以空间复杂度是 O(1)。 - -## 解法三 - -上边的两种思路,时间复杂度都达不到题目的要求 O ( log ( m + n ) )。看到 log ,很明显,我们只有用到二分的方法才能达到。我们不妨用另一种思路,题目是求中位数,其实就是求第 k 小数的一种特殊情况,而求第 k 小数有一种算法。 - -解法二中,我们一次遍历就相当于去掉不可能是中位数的一个值,也就是一个一个排除。由于数列是有序的,其实我们完全可以一半儿一半儿的排除。假设我们要找第 k 小数,我们可以每次循环排除掉 k / 2 个数。看下边一个例子。 - -假设我们要找第 7 小的数字。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/mid1.jpg) - -我们比较两个数组的第 k / 2 个数字,如果 k 是奇数,向下取整。也就是比较第 3 个数字,上边数组中的 4 和 下边数组中的 3 ,如果哪个小,就表明该数组的前 k / 2 个数字都不是第 k 小数字,所以可以排除。也就是 1,2,3 这三个数字不可能是第 7 小的数字,我们可以把它排除掉。将 1349 和 45678910 两个数组作为新的数组进行比较。 - -更一般的情况 A [ 1 ],A [ 2 ],A [ 3 ],A [ k / 2] ... ,B[ 1 ],B [ 2 ],B [ 3 ],B[ k / 2] ... ,如果 A [ k / 2 ] < B [ k / 2 ] ,那么 A [ 1 ],A [ 2 ],A [ 3 ],A [ k / 2] 都不可能是第 k 小的数字。 - -A 数组中比 A [ k / 2 ] 小的数有 k / 2 - 1 个,B 数组中,B [ k / 2 ] 比 A [ k / 2 ] 大,假设 B [ k / 2 ] 前边的数字都比 A [ k / 2 ] 小,也只有 k / 2 - 1 个,所以比 A [ k / 2 ] 小的数字最多有 k / 2 - 1 + k / 2 - 1 = k - 2 个,所以 A [ k / 2 ] 最多是第 k - 1 小的数。而比 A [ k / 2 ] 小的数更不可能是第 k 小的数了,所以可以把它们排除。 - -橙色的部分表示已经去掉的数字。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/mid2.jpg) - -由于我们已经排除掉了 3 个数字,就是这 3 个数字一定在最前边,所以在两个新数组中,我们只需要找第 7 - 3 = 4 小的数字就可以了,也就是 k = 4 。此时两个数组,比较第 2 个数字,3 < 5,所以我们可以把小的那个数组中的 1 ,3 排除掉了。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/mid3.jpg) - -我们又排除掉 2 个数字,所以现在找第 4 - 2 = 2 小的数字就可以了。此时比较两个数组中的第 k / 2 = 1 个数,4 == 4 ,怎么办呢?由于两个数相等,所以我们无论去掉哪个数组中的都行,因为去掉 1 个总会保留 1 个的,所以没有影响。为了统一,我们就假设 4 > 4 吧,所以此时将下边的 4 去掉。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/mid4.jpg) - -由于又去掉 1 个数字,此时我们要找第 1 小的数字,所以只需判断两个数组中第一个数字哪个小就可以了,也就是 4 。 - -所以第 7 小的数字是 4 。 - -我们每次都是取 k / 2 的数进行比较,有时候可能会遇到数组长度小于 k / 2 的时候。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/mid5.jpg) - -此时 k / 2 等于 3 ,而上边的数组长度是 2 ,我们此时将箭头指向它的末尾就可以了。这样的话,由于 2 < 3 ,所以就会导致上边的数组 1,2 都被排除。造成下边的情况。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/mid6.jpg) - -由于 2 个元素被排除,所以此时 k = 5 ,又由于上边的数组已经空了,我们只需要返回下边的数组的第 5 个数字就可以了。 - -从上边可以看到,无论是找第奇数个还是第偶数个数字,对我们的算法并没有影响,而且在算法进行中,k 的值都有可能从奇数变为偶数,最终都会变为 1 或者由于一个数组空了,直接返回结果。 - -所以我们采用递归的思路,为了防止数组长度小于 k / 2 ,所以每次比较 min ( k / 2,len ( 数组 ) ) 对应的数字,把小的那个对应的数组的数字排除,将两个新数组进入递归,并且 k 要减去排除的数字的个数。递归出口就是当 k = 1 或者其中一个数字长度是 0 了。 - -## 代码 - -```java -public double findMedianSortedArrays(int[] nums1, int[] nums2) { - int n = nums1.length; - int m = nums2.length; - int left = (n + m + 1) / 2; - int right = (n + m + 2) / 2; - //将偶数和奇数的情况合并,如果是奇数,会求两次同样的 k 。 - return (getKth(nums1, 0, n - 1, nums2, 0, m - 1, left) + getKth(nums1, 0, n - 1, nums2, 0, m - 1, right)) * 0.5; -} - - private int getKth(int[] nums1, int start1, int end1, int[] nums2, int start2, int end2, int k) { - int len1 = end1 - start1 + 1; - int len2 = end2 - start2 + 1; - //让 len1 的长度小于 len2,这样就能保证如果有数组空了,一定是 len1 - if (len1 > len2) return getKth(nums2, start2, end2, nums1, start1, end1, k); - if (len1 == 0) return nums2[start2 + k - 1]; - - if (k == 1) return Math.min(nums1[start1], nums2[start2]); - - int i = start1 + Math.min(len1, k / 2) - 1; - int j = start2 + Math.min(len2, k / 2) - 1; - - if (nums1[i] > nums2[j]) { - return getKth(nums1, start1, end1, nums2, j + 1, end2, k - (j - start2 + 1)); - } - else { - return getKth(nums1, i + 1, end1, nums2, start2, end2, k - (i - start1 + 1)); - } - } -``` - -时间复杂度:每进行一次循环,我们就减少 k / 2 个元素,所以时间复杂度是 O(log(k)),而 k = (m + n)/ 2 ,所以最终的复杂也就是 O(log(m + n))。 - -空间复杂度:虽然我们用到了递归,但是可以看到这个递归属于尾递归,所以编译器不需要不停地堆栈,所以空间复杂度为 O(1)。 - -## 解法四 - -我们首先理一下中位数的定义是什么 - -> 中位数(又称中值,英语:Median),[统计学](https://baike.baidu.com/item/%E7%BB%9F%E8%AE%A1%E5%AD%A6/2630438)中的专有名词,代表一个样本、种群或[概率分布](https://baike.baidu.com/item/%E6%A6%82%E7%8E%87%E5%88%86%E5%B8%83/828907)中的一个数值,其可将数值集合划分为相等的上下两部分。 - -所以我们只需要将数组进行切。 - -一个长度为 m 的数组,有 0 到 m 总共 m + 1 个位置可以切。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/mid7.jpg) - -我们把数组 A 和数组 B 分别在 i 和 j 进行切割。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/mid8.jpg) - -将 i 的左边和 j 的左边组合成「左半部分」,将 i 的右边和 j 的右边组合成「右半部分」。 - -* 当 A 数组和 B 数组的总长度是偶数时,如果我们能够保证 - - * 左半部分的长度等于右半部分 - - i + j = m - i + n - j , 也就是 j = ( m + n ) / 2 - i - - * 左半部分最大的值小于等于右半部分最小的值 max ( A [ i - 1 ] , B [ j - 1 ])) <= min ( A [ i ] , B [ j ])) - - 那么,中位数就可以表示如下 - - (左半部分最大值 + 右半部分最小值 )/ 2 。 - - (max ( A [ i - 1 ] , B [ j - 1 ])+ min ( A [ i ] , B [ j ])) / 2 - -* 当 A 数组和 B 数组的总长度是奇数时,如果我们能够保证 - - * 左半部分的长度比右半部分大 1 - - i + j = m - i + n - j + 1也就是 j = ( m + n + 1) / 2 - i - - * 左半部分最大的值小于等于右半部分最小的值 max ( A [ i - 1 ] , B [ j - 1 ])) <= min ( A [ i ] , B [ j ])) - - 那么,中位数就是 - - 左半部分最大值,也就是左半部比右半部分多出的那一个数。 - - max ( A [ i - 1 ] , B [ j - 1 ]) - - -上边的第一个条件我们其实可以合并为 j = ( m + n + 1) / 2 - i,因为如果 m + n 是偶数,由于我们取的是 int 值,所以加 1 也不会影响结果。当然,由于 0 <= i <= m ,为了保证 0 <= j <= n ,我们必须保证 m <= n 。 - -$$m\leq n,i(m+m+1)/2-m=0$$ - -$$m\leq n,i>0,j=(m+n+1)/2-i\leq (n+n+1)/2-i<(n+n+1)/2=n$$ - -最后一步由于是 int 间的运算,所以 1 / 2 = 0。 - -而对于第二个条件,奇数和偶数的情况是一样的,我们进一步分析。为了保证 max ( A [ i - 1 ] , B [ j - 1 ])) <= min ( A [ i ] , B [ j ])),因为 A 数组和 B 数组是有序的,所以 A [ i - 1 ] <= A [ i ],B [ i - 1 ] <= B [ i ] 这是天然的,所以我们只需要保证 B [ j - 1 ] < = A [ i ] 和 A [ i - 1 ] <= B [ j ] 所以我们分两种情况讨论: - -* B [ j - 1 ] > A [ i ],并且为了不越界,要保证 j != 0,i != m - - ![](http://windliang.oss-cn-beijing.aliyuncs.com/mid9.jpg) - - 此时很明显,我们需要增加 i ,为了数量的平衡还要减少 j ,幸运的是 j = ( m + n + 1) / 2 - i,i 增大,j 自然会减少。 - - -* A [ i - 1 ] > B [ j ] ,并且为了不越界,要保证 i != 0,j != n - - ![](http://windliang.oss-cn-beijing.aliyuncs.com/mid10.jpg) - - 此时和上边的情况相反,我们要减少 i ,增大 j 。 - -上边两种情况,我们把边界都排除了,需要单独讨论。 - -* 当 i = 0 , 或者 j = 0 ,也就是切在了最前边。 - - ![](http://windliang.oss-cn-beijing.aliyuncs.com/mid11.jpg) - - 此时左半部分当 j = 0 时,最大的值就是 A [ i - 1 ] ;当 i = 0 时 最大的值就是 B [ j - 1] 。右半部分最小值和之前一样。 - - -* 当 i = m 或者 j = n ,也就是切在了最后边。 - - ![](http://windliang.oss-cn-beijing.aliyuncs.com/mid12.jpg) - - 此时左半部分最大值和之前一样。右半部分当 j = n 时,最小值就是 A [ i ] ;当 i = m 时,最小值就是B [ j ] 。 - - 所有的思路都理清了,最后一个问题,增加 i 的方式。当然用二分了。初始化 i 为中间的值,然后减半找中间的,减半找中间的,减半找中间的直到答案。 - -```java -class Solution { - public double findMedianSortedArrays(int[] A, int[] B) { - int m = A.length; - int n = B.length; - if (m > n) { - return findMedianSortedArrays(B,A); // 保证 m <= n - } - int iMin = 0, iMax = m; - while (iMin <= iMax) { - int i = (iMin + iMax) / 2; - int j = (m + n + 1) / 2 - i; - if (j != 0 && i != m && B[j-1] > A[i]){ // i 需要增大 - iMin = i + 1; - } - else if (i != 0 && j != n && A[i-1] > B[j]) { // i 需要减小 - iMax = i - 1; - } - else { // 达到要求,并且将边界条件列出来单独考虑 - int maxLeft = 0; - if (i == 0) { maxLeft = B[j-1]; } - else if (j == 0) { maxLeft = A[i-1]; } - else { maxLeft = Math.max(A[i-1], B[j-1]); } - if ( (m + n) % 2 == 1 ) { return maxLeft; } // 奇数的话不需要考虑右半部分 - - int minRight = 0; - if (i == m) { minRight = B[j]; } - else if (j == n) { minRight = A[i]; } - else { minRight = Math.min(B[j], A[i]); } - - return (maxLeft + minRight) / 2.0; //如果是偶数的话返回结果 - } - } - return 0.0; - } -} -``` - -时间复杂度:我们对较短的数组进行了二分查找,所以时间复杂度是 O(log(min(m,n)))。 - -空间复杂度:只有一些固定的变量,和数组长度无关,所以空间复杂度是 O ( 1 ) 。 - -## 总结 - -解法二中体会到了对情况的转换,有时候即使有了思路,代码也不一定写的优雅,需要多锻炼才可以。解法三和解法四充分发挥了二分查找的优势,将时间复杂度降为 log 级别。 - +## 题目描述(困难难度) + +![](http://windliang.oss-cn-beijing.aliyuncs.com/3_median.jpg) + +已知两个有序数组,找到两个数组合并后的中位数。 + +## 解法一 + +简单粗暴,先将两个数组合并,两个有序数组的合并也是归并排序中的一部分。然后根据奇数,还是偶数,返回中位数。 + +## 代码 + +```java +public double findMedianSortedArrays(int[] nums1, int[] nums2) { + int[] nums; + int m = nums1.length; + int n = nums2.length; + nums = new int[m + n]; + if (m == 0) { + if (n % 2 == 0) { + return (nums2[n / 2 - 1] + nums2[n / 2]) / 2.0; + } else { + + return nums2[n / 2]; + } + } + if (n == 0) { + if (m % 2 == 0) { + return (nums1[m / 2 - 1] + nums1[m / 2]) / 2.0; + } else { + return nums1[m / 2]; + } + } + + int count = 0; + int i = 0, j = 0; + while (count != (m + n)) { + if (i == m) { + while (j != n) { + nums[count++] = nums2[j++]; + } + break; + } + if (j == n) { + while (i != m) { + nums[count++] = nums1[i++]; + } + break; + } + + if (nums1[i] < nums2[j]) { + nums[count++] = nums1[i++]; + } else { + nums[count++] = nums2[j++]; + } + } + + if (count % 2 == 0) { + return (nums[count / 2 - 1] + nums[count / 2]) / 2.0; + } else { + return nums[count / 2]; + } +} +``` + +时间复杂度:遍历全部数组,O(m + n) + +空间复杂度:开辟了一个数组,保存合并后的两个数组,O(m + n) + +## 解法二 + +其实,我们不需要将两个数组真的合并,我们只需要找到中位数在哪里就可以了。 + +开始的思路是写一个循环,然后里边判断是否到了中位数的位置,到了就返回结果,但这里对偶数和奇数的分类会很麻烦。当其中一个数组遍历完后,出了 for 循环对边界的判断也会分几种情况。总体来说,虽然复杂度不影响,但代码会看起来很乱。然后在 [这里](https://blog.csdn.net/lxhpkm01/article/details/53823595) 找到了另一种思路。 + + + +首先是怎么将奇数和偶数的情况合并一下。 + +用 len 表示合并后数组的长度,如果是奇数,我们需要知道第 (len + 1)/ 2 个数就可以了,如果遍历的话需要遍历 int ( len / 2 ) + 1 次。如果是偶数,我们需要知道第 len / 2 和 len / 2 + 1 个数,也是需要遍历 len / 2 + 1 次。所以遍历的话,奇数和偶数都是 len / 2 + 1 次。 + + + +返回中位数的话,奇数需要最后一次遍历的结果就可以了,偶数需要最后一次和上一次遍历的结果。所以我们用两个变量 left 和 right ,right 保存当前循环的结果,在每次循环前将 right 的值赋给 left 。这样在最后一次循环的时候,left 将得到 right 的值,也就是上一次循环的结果,接下来 right 更新为最后一次的结果。 + + + +循环中该怎么写,什么时候 A 数组后移,什么时候 B 数组后移。用 aStart 和 bStart 分别表示当前指向 A 数组和 B 数组的位置。如果 aStart 还没有到最后并且此时 A 位置的数字小于 B 位置的数组,那么就可以后移了。也就是aStart < m && A[aStart] < B[bStart]。 + +但如果 B 数组此刻已经没有数字了,继续取数字B [ bStart ],则会越界,所以判断下 bStart 是否大于数组长度了,这样 || 后边的就不会执行了,也就不会导致错误了,所以增加为 aStart < m && ( bStart >= n || A [ aStart ] < B [ bStart ] ) 。 + +## 代码 + +``` java +public double findMedianSortedArrays(int[] A, int[] B) { + int m = A.length; + int n = B.length; + int len = m + n; + int left = -1, right = -1; + int aStart = 0, bStart = 0; + for (int i = 0; i <= len / 2; i++) { + left = right; + if (aStart < m && (bStart >= n || A[aStart] < B[bStart])) { + right = A[aStart++]; + } else { + right = B[bStart++]; + } + } + if ((len & 1) == 0) + return (left + right) / 2.0; + else + return right; +} +``` + +时间复杂度:遍历 len/2 + 1 次,len = m + n ,所以时间复杂度依旧是 O(m + n)。 + +空间复杂度:我们申请了常数个变量,也就是 m,n,len,left,right,aStart,bStart 以及 i 。 + +总共 8 个变量,所以空间复杂度是 O(1)。 + +## 解法三 + +上边的两种思路,时间复杂度都达不到题目的要求 O ( log ( m + n ) )。看到 log ,很明显,我们只有用到二分的方法才能达到。我们不妨用另一种思路,题目是求中位数,其实就是求第 k 小数的一种特殊情况,而求第 k 小数有一种算法。 + +解法二中,我们一次遍历就相当于去掉不可能是中位数的一个值,也就是一个一个排除。由于数列是有序的,其实我们完全可以一半儿一半儿的排除。假设我们要找第 k 小数,我们可以每次循环排除掉 k / 2 个数。看下边一个例子。 + +假设我们要找第 7 小的数字。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/mid1.jpg) + +我们比较两个数组的第 k / 2 个数字,如果 k 是奇数,向下取整。也就是比较第 3 个数字,上边数组中的 4 和 下边数组中的 3 ,如果哪个小,就表明该数组的前 k / 2 个数字都不是第 k 小数字,所以可以排除。也就是 1,2,3 这三个数字不可能是第 7 小的数字,我们可以把它排除掉。将 1349 和 45678910 两个数组作为新的数组进行比较。 + +更一般的情况 A [ 1 ],A [ 2 ],A [ 3 ],A [ k / 2] ... ,B[ 1 ],B [ 2 ],B [ 3 ],B[ k / 2] ... ,如果 A [ k / 2 ] < B [ k / 2 ] ,那么 A [ 1 ],A [ 2 ],A [ 3 ],A [ k / 2] 都不可能是第 k 小的数字。 + +A 数组中比 A [ k / 2 ] 小的数有 k / 2 - 1 个,B 数组中,B [ k / 2 ] 比 A [ k / 2 ] 大,假设 B [ k / 2 ] 前边的数字都比 A [ k / 2 ] 小,也只有 k / 2 - 1 个,所以比 A [ k / 2 ] 小的数字最多有 k / 2 - 1 + k / 2 - 1 = k - 2 个,所以 A [ k / 2 ] 最多是第 k - 1 小的数。而比 A [ k / 2 ] 小的数更不可能是第 k 小的数了,所以可以把它们排除。 + +橙色的部分表示已经去掉的数字。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/mid2.jpg) + +由于我们已经排除掉了 3 个数字,就是这 3 个数字一定在最前边,所以在两个新数组中,我们只需要找第 7 - 3 = 4 小的数字就可以了,也就是 k = 4 。此时两个数组,比较第 2 个数字,3 < 5,所以我们可以把小的那个数组中的 1 ,3 排除掉了。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/mid3.jpg) + +我们又排除掉 2 个数字,所以现在找第 4 - 2 = 2 小的数字就可以了。此时比较两个数组中的第 k / 2 = 1 个数,4 == 4 ,怎么办呢?由于两个数相等,所以我们无论去掉哪个数组中的都行,因为去掉 1 个总会保留 1 个的,所以没有影响。为了统一,我们就假设 4 > 4 吧,所以此时将下边的 4 去掉。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/mid4.jpg) + +由于又去掉 1 个数字,此时我们要找第 1 小的数字,所以只需判断两个数组中第一个数字哪个小就可以了,也就是 4 。 + +所以第 7 小的数字是 4 。 + +我们每次都是取 k / 2 的数进行比较,有时候可能会遇到数组长度小于 k / 2 的时候。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/mid5.jpg) + +此时 k / 2 等于 3 ,而上边的数组长度是 2 ,我们此时将箭头指向它的末尾就可以了。这样的话,由于 2 < 3 ,所以就会导致上边的数组 1,2 都被排除。造成下边的情况。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/mid6.jpg) + +由于 2 个元素被排除,所以此时 k = 5 ,又由于上边的数组已经空了,我们只需要返回下边的数组的第 5 个数字就可以了。 + +从上边可以看到,无论是找第奇数个还是第偶数个数字,对我们的算法并没有影响,而且在算法进行中,k 的值都有可能从奇数变为偶数,最终都会变为 1 或者由于一个数组空了,直接返回结果。 + +所以我们采用递归的思路,为了防止数组长度小于 k / 2 ,所以每次比较 min ( k / 2,len ( 数组 ) ) 对应的数字,把小的那个对应的数组的数字排除,将两个新数组进入递归,并且 k 要减去排除的数字的个数。递归出口就是当 k = 1 或者其中一个数字长度是 0 了。 + +## 代码 + +```java +public double findMedianSortedArrays(int[] nums1, int[] nums2) { + int n = nums1.length; + int m = nums2.length; + int left = (n + m + 1) / 2; + int right = (n + m + 2) / 2; + //将偶数和奇数的情况合并,如果是奇数,会求两次同样的 k 。 + return (getKth(nums1, 0, n - 1, nums2, 0, m - 1, left) + getKth(nums1, 0, n - 1, nums2, 0, m - 1, right)) * 0.5; +} + + private int getKth(int[] nums1, int start1, int end1, int[] nums2, int start2, int end2, int k) { + int len1 = end1 - start1 + 1; + int len2 = end2 - start2 + 1; + //让 len1 的长度小于 len2,这样就能保证如果有数组空了,一定是 len1 + if (len1 > len2) return getKth(nums2, start2, end2, nums1, start1, end1, k); + if (len1 == 0) return nums2[start2 + k - 1]; + + if (k == 1) return Math.min(nums1[start1], nums2[start2]); + + int i = start1 + Math.min(len1, k / 2) - 1; + int j = start2 + Math.min(len2, k / 2) - 1; + + if (nums1[i] > nums2[j]) { + return getKth(nums1, start1, end1, nums2, j + 1, end2, k - (j - start2 + 1)); + } + else { + return getKth(nums1, i + 1, end1, nums2, start2, end2, k - (i - start1 + 1)); + } + } +``` + +时间复杂度:每进行一次循环,我们就减少 k / 2 个元素,所以时间复杂度是 O(log(k)),而 k = (m + n)/ 2 ,所以最终的复杂也就是 O(log(m + n))。 + +空间复杂度:虽然我们用到了递归,但是可以看到这个递归属于尾递归,所以编译器不需要不停地堆栈,所以空间复杂度为 O(1)。 + +## 解法四 + +我们首先理一下中位数的定义是什么 + +> 中位数(又称中值,英语:Median),[统计学](https://baike.baidu.com/item/%E7%BB%9F%E8%AE%A1%E5%AD%A6/2630438)中的专有名词,代表一个样本、种群或[概率分布](https://baike.baidu.com/item/%E6%A6%82%E7%8E%87%E5%88%86%E5%B8%83/828907)中的一个数值,其可将数值集合划分为相等的上下两部分。 + +所以我们只需要将数组进行切。 + +一个长度为 m 的数组,有 0 到 m 总共 m + 1 个位置可以切。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/mid7.jpg) + +我们把数组 A 和数组 B 分别在 i 和 j 进行切割。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/mid8.jpg) + +将 i 的左边和 j 的左边组合成「左半部分」,将 i 的右边和 j 的右边组合成「右半部分」。 + +* 当 A 数组和 B 数组的总长度是偶数时,如果我们能够保证 + + * 左半部分的长度等于右半部分 + + i + j = m - i + n - j , 也就是 j = ( m + n ) / 2 - i + + * 左半部分最大的值小于等于右半部分最小的值 max ( A [ i - 1 ] , B [ j - 1 ])) <= min ( A [ i ] , B [ j ])) + + 那么,中位数就可以表示如下 + + (左半部分最大值 + 右半部分最小值 )/ 2 。 + + (max ( A [ i - 1 ] , B [ j - 1 ])+ min ( A [ i ] , B [ j ])) / 2 + +* 当 A 数组和 B 数组的总长度是奇数时,如果我们能够保证 + + * 左半部分的长度比右半部分大 1 + + i + j = m - i + n - j + 1也就是 j = ( m + n + 1) / 2 - i + + * 左半部分最大的值小于等于右半部分最小的值 max ( A [ i - 1 ] , B [ j - 1 ])) <= min ( A [ i ] , B [ j ])) + + 那么,中位数就是 + + 左半部分最大值,也就是左半部比右半部分多出的那一个数。 + + max ( A [ i - 1 ] , B [ j - 1 ]) + + +上边的第一个条件我们其实可以合并为 j = ( m + n + 1) / 2 - i,因为如果 m + n 是偶数,由于我们取的是 int 值,所以加 1 也不会影响结果。当然,由于 0 <= i <= m ,为了保证 0 <= j <= n ,我们必须保证 m <= n 。 + +$$m\leq n,i(m+m+1)/2-m=0$$ + +$$m\leq n,i>0,j=(m+n+1)/2-i\leq (n+n+1)/2-i<(n+n+1)/2=n$$ + +最后一步由于是 int 间的运算,所以 1 / 2 = 0。 + +而对于第二个条件,奇数和偶数的情况是一样的,我们进一步分析。为了保证 max ( A [ i - 1 ] , B [ j - 1 ])) <= min ( A [ i ] , B [ j ])),因为 A 数组和 B 数组是有序的,所以 A [ i - 1 ] <= A [ i ],B [ i - 1 ] <= B [ i ] 这是天然的,所以我们只需要保证 B [ j - 1 ] < = A [ i ] 和 A [ i - 1 ] <= B [ j ] 所以我们分两种情况讨论: + +* B [ j - 1 ] > A [ i ],并且为了不越界,要保证 j != 0,i != m + + ![](http://windliang.oss-cn-beijing.aliyuncs.com/mid9.jpg) + + 此时很明显,我们需要增加 i ,为了数量的平衡还要减少 j ,幸运的是 j = ( m + n + 1) / 2 - i,i 增大,j 自然会减少。 + + +* A [ i - 1 ] > B [ j ] ,并且为了不越界,要保证 i != 0,j != n + + ![](http://windliang.oss-cn-beijing.aliyuncs.com/mid10.jpg) + + 此时和上边的情况相反,我们要减少 i ,增大 j 。 + +上边两种情况,我们把边界都排除了,需要单独讨论。 + +* 当 i = 0 , 或者 j = 0 ,也就是切在了最前边。 + + ![](http://windliang.oss-cn-beijing.aliyuncs.com/mid11.jpg) + + 此时左半部分当 j = 0 时,最大的值就是 A [ i - 1 ] ;当 i = 0 时 最大的值就是 B [ j - 1] 。右半部分最小值和之前一样。 + + +* 当 i = m 或者 j = n ,也就是切在了最后边。 + + ![](http://windliang.oss-cn-beijing.aliyuncs.com/mid12.jpg) + + 此时左半部分最大值和之前一样。右半部分当 j = n 时,最小值就是 A [ i ] ;当 i = m 时,最小值就是B [ j ] 。 + + 所有的思路都理清了,最后一个问题,增加 i 的方式。当然用二分了。初始化 i 为中间的值,然后减半找中间的,减半找中间的,减半找中间的直到答案。 + +```java +class Solution { + public double findMedianSortedArrays(int[] A, int[] B) { + int m = A.length; + int n = B.length; + if (m > n) { + return findMedianSortedArrays(B,A); // 保证 m <= n + } + int iMin = 0, iMax = m; + while (iMin <= iMax) { + int i = (iMin + iMax) / 2; + int j = (m + n + 1) / 2 - i; + if (j != 0 && i != m && B[j-1] > A[i]){ // i 需要增大 + iMin = i + 1; + } + else if (i != 0 && j != n && A[i-1] > B[j]) { // i 需要减小 + iMax = i - 1; + } + else { // 达到要求,并且将边界条件列出来单独考虑 + int maxLeft = 0; + if (i == 0) { maxLeft = B[j-1]; } + else if (j == 0) { maxLeft = A[i-1]; } + else { maxLeft = Math.max(A[i-1], B[j-1]); } + if ( (m + n) % 2 == 1 ) { return maxLeft; } // 奇数的话不需要考虑右半部分 + + int minRight = 0; + if (i == m) { minRight = B[j]; } + else if (j == n) { minRight = A[i]; } + else { minRight = Math.min(B[j], A[i]); } + + return (maxLeft + minRight) / 2.0; //如果是偶数的话返回结果 + } + } + return 0.0; + } +} +``` + +时间复杂度:我们对较短的数组进行了二分查找,所以时间复杂度是 O(log(min(m,n)))。 + +空间复杂度:只有一些固定的变量,和数组长度无关,所以空间复杂度是 O ( 1 ) 。 + +## 总结 + +解法二中体会到了对情况的转换,有时候即使有了思路,代码也不一定写的优雅,需要多锻炼才可以。解法三和解法四充分发挥了二分查找的优势,将时间复杂度降为 log 级别。 + diff --git a/leetCode-40-Combination-Sum-II.md b/leetCode-40-Combination-Sum-II.md index 0ddcea54d..1a1323fe6 100644 --- a/leetCode-40-Combination-Sum-II.md +++ b/leetCode-40-Combination-Sum-II.md @@ -1,307 +1,307 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/40.jpg) - -和[上一道题](https://leetcode.windliang.cc/leetCode-39-Combination-Sum.html)非常像了,区别在于这里给的数组中有重复的数字,每个数字只能使用一次,然后同样是给出所有和等于 target 的情况。 - -# 解法一 回溯法 - -只需要在上题的基础上改一下就行了。直接看代码吧。 - -```java -public List> combinationSum2(int[] candidates, int target) { - List> ans = new ArrayList<>(); - getAnswer(ans, new ArrayList<>(), candidates, target, 0); - /*************修改的地方*******************/ - // 如果是 Input: candidates = [2,5,2,1,2], target = 5, - // 输出会出现 [2 2 1] [2 1 2] 这样的情况,所以要去重 - return removeDuplicate(ans); - /****************************************/ -} - -private void getAnswer(List> ans, ArrayList temp, int[] candidates, int target, int start) { - if (target == 0) { - ans.add(new ArrayList(temp)); - } else if (target < 0) { - return; - } else { - for (int i = start; i < candidates.length; i++) { - temp.add(candidates[i]); - /*************修改的地方*******************/ - //i -> i + 1 ,因为每个数字只能用一次,所以下次遍历的时候不从自己开始 - getAnswer(ans, temp, candidates, target - candidates[i], i + 1); - /****************************************/ - temp.remove(temp.size() - 1); - } - } - -} - -private List> removeDuplicate(List> list) { - Map ans = new HashMap(); - for (int i = 0; i < list.size(); i++) { - List l = list.get(i); - Collections.sort(l); - String key = ""; - for (int j = 0; j < l.size() - 1; j++) { - key = key + l.get(j) + ","; - } - key = key + l.get(l.size() - 1); - ans.put(key, ""); - } - List> ans_list = new ArrayList>(); - for (String k : ans.keySet()) { - String[] l = k.split(","); - List temp = new ArrayList(); - for (int i = 0; i < l.length; i++) { - int c = Integer.parseInt(l[i]); - temp.add(c); - } - ans_list.add(temp); - } - return ans_list; -} -``` - -时间复杂度: - -空间复杂度: - -看到[这里](https://leetcode.com/problems/combination-sum-ii/discuss/16878/Combination-Sum-I-II-and-III-Java-solution-(see-the-similarities-yourself)),想法很棒,为了解决重复的情况,我们可以先把数组先排序,这样就好说了。 - -```java -public List> combinationSum2(int[] candidates, int target) { - List> ans = new ArrayList<>(); - Arrays.sort(candidates); //排序 - getAnswer(ans, new ArrayList<>(), candidates, target, 0); - return ans; -} - -private void getAnswer(List> ans, ArrayList temp, int[] candidates, int target, int start) { - if (target == 0) { - ans.add(new ArrayList(temp)); - } else if (target < 0) { - return; - } else { - for (int i = start; i < candidates.length; i++) { - //跳过重复的数字 - if(i > start && candidates[i] == candidates[i-1]) continue; - temp.add(candidates[i]); - /*************修改的地方*******************/ - //i -> i + 1 ,因为每个数字只能用一次,所以下次遍历的时候不从自己开始 - getAnswer(ans, temp, candidates, target - candidates[i], i + 1); - /****************************************/ - temp.remove(temp.size() - 1); - } - } -} - -``` - - - -# 解法二 动态规划 - -怎么去更改上题的算法满足本题,暂时没想到,只想到就是再写个函数对答案再过滤一次。先记录给定的 nums 中的每个数字出现的次数,然后判断每个 list 的数字出现的次数是不是满足小于等于给定的 nums 中的每个数字出现的次数,不满足的话就剔除掉。如果大家有直接改之前算法的好办法可以告诉我,谢谢了。 - -此外,要注意一点就是上题中,给定的 nums 没有重复的,而这题中是有重复的。为了使得和之前一样,所以我们在算法中都得加上 - -```java -if (i > 0 && nums[i] == nums[i - 1]) { - continue; -} -``` - -跳过重复的数字,不然是不能 AC 的,至于原因下边分析下。 - -```java -public List> combinationSum2(int[] nums, int target) { - List>> ans = new ArrayList<>(); //opt 数组 - Arrays.sort(nums);// 将数组有序,这样可以提现结束循环 - for (int sum = 0; sum <= target; sum++) { // 从 0 到 target 求出每一个 opt - List> ans_sum = new ArrayList>(); - for (int i = 0; i < nums.length; i++) { //遍历 nums - /*******************修改的地方********************/ - if (i > 0 && nums[i] == nums[i - 1]) { - continue; - } - /***********************************************/ - if (nums[i] == sum) { - List temp = new ArrayList(); - temp.add(nums[i]); - ans_sum.add(temp); - } else if (nums[i] < sum) { - List> ans_sub = ans.get(sum - nums[i]); - //每一个加上 nums[i] - for (int j = 0; j < ans_sub.size(); j++) { - List temp = new ArrayList(ans_sub.get(j)); - temp.add(nums[i]); - ans_sum.add(temp); - } - } else { - break; - } - } - ans.add(sum, ans_sum); - } - return remove(removeDuplicate(ans.get(target)),nums); -} - -private List> removeDuplicate(List> list) { - Map ans = new HashMap(); - for (int i = 0; i < list.size(); i++) { - List l = list.get(i); - Collections.sort(l); - String key = ""; - //[ 2 3 4 ] 转为 "2,3,4" - for (int j = 0; j < l.size() - 1; j++) { - key = key + l.get(j) + ","; - } - key = key + l.get(l.size() - 1); - ans.put(key, ""); - } - //根据逗号还原 List - List> ans_list = new ArrayList>(); - for (String k : ans.keySet()) { - String[] l = k.split(","); - List temp = new ArrayList(); - for (int i = 0; i < l.length; i++) { - int c = Integer.parseInt(l[i]); - temp.add(c); - } - ans_list.add(temp); - } - return ans_list; -} - -//过滤不满足答案的情况 -private List> remove(List> list, int[] nums) { - HashMap nh = new HashMap(); - List> ans = new ArrayList>(list); - //记录每个数字出现的次数 - for (int n : nums) { - int s = nh.getOrDefault(n, 0); - nh.put(n, s + 1); - } - for (int i = 0; i < list.size(); i++) { - List l = list.get(i); - HashMap temp = new HashMap(); - //记录每个 list 中数字出现的次数 - for (int n : l) { - int s = temp.getOrDefault(n, 0); - temp.put(n, s + 1); - } - for (int n : l) { - //进行比较 - if (temp.get(n) > nh.get(n)) { - ans.remove(l); - break; - } - } - } - return ans; -} -``` - -如果不加跳过重复的数字的话,下边的样例不会通过。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/40_3.jpg) - -这是因为我们求 opt 的时候每个列表的数量在以指数级增加,在上一个 opt 的基础上,每一个列表都增加 5 个 列表。 - -opt [ 1 ] = [ [ 1 ],[ 1 ],[ 1 ],[ 1 ],[ 1 ] ] 数量是 5, - -opt [ 2 ] = [ - -​ [ 1,1 ], [ 1,1 ],[ 1,1 ],[ 1,1 ],[ 1,1 ], - -​ [ 1,1 ], [ 1,1 ],[ 1,1 ],[ 1,1 ],[ 1,1 ] - -​ [ 1,1 ], [ 1,1 ],[ 1,1 ],[ 1,1 ],[ 1,1 ], - -​ [ 1,1 ], [ 1,1 ],[ 1,1 ],[ 1,1 ],[ 1,1 ], - -​ [ 1,1 ], [ 1,1 ],[ 1,1 ],[ 1,1 ],[ 1,1 ],, - -​ ] 数量是 5 * 5。 - -opt [ 3 ] 数量是 5 \* 5 \* 5。 - -到了 opt [ 9 ] 就是 5 的 9 次方,数量是 1953125 内存爆炸了。 - -另一个算法也可以改一下 - -```java -public List> combinationSum2(int[] nums, int target) { - List>> ans = new ArrayList<>(); - Arrays.sort(nums); - if (nums[0] > target) { - return new ArrayList>(); - } - for (int i = 0; i <= target; i++) { - List> ans_i = new ArrayList>(); - ans.add(i, ans_i); - } - - for (int i = 0; i < nums.length; i++) { - /*******************修改的地方********************/ - if (i > 0 && nums[i] == nums[i - 1]) { - continue; - } - /***********************************************/ - for (int sum = nums[i]; sum <= target; sum++) { - List> ans_sum = ans.get(sum); - List> ans_sub = ans.get(sum - nums[i]); - if (sum == nums[i]) { - ArrayList temp = new ArrayList(); - temp.add(nums[i]); - ans_sum.add(temp); - - } - if (ans_sub.size() > 0) { - for (int j = 0; j < ans_sub.size(); j++) { - ArrayList temp = new ArrayList(ans_sub.get(j)); - temp.add(nums[i]); - ans_sum.add(temp); - } - } - } - } - return remove(ans.get(target), nums); - -} - -private List> remove(List> list, int[] nums) { - HashMap nh = new HashMap(); - List> ans = new ArrayList>(list); - for (int n : nums) { - int s = nh.getOrDefault(n, 0); - nh.put(n, s + 1); - } - for (int i = 0; i < list.size(); i++) { - List l = list.get(i); - HashMap temp = new HashMap(); - for (int n : l) { - int s = temp.getOrDefault(n, 0); - temp.put(n, s + 1); - } - for (int n : l) { - if (temp.get(n) > nh.get(n)) { - ans.remove(l); - break; - } - } - } - return ans; -} -``` - -如果不加跳过重复的数字的话,下边的样例不会通过 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/40_2.jpg) - -会发现出现了很多重复的结果,就是因为没有跳过重复的 1。在求 opt [ 1 ] 的时候就变成了 [ [ 1 ],[ 1 ] ] 这样子,由于后边求的时候都是直接在原来每一个列表里加数字,所有后边都是加了两次了。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/40.jpg) + +和[上一道题](https://leetcode.windliang.cc/leetCode-39-Combination-Sum.html)非常像了,区别在于这里给的数组中有重复的数字,每个数字只能使用一次,然后同样是给出所有和等于 target 的情况。 + +# 解法一 回溯法 + +只需要在上题的基础上改一下就行了。直接看代码吧。 + +```java +public List> combinationSum2(int[] candidates, int target) { + List> ans = new ArrayList<>(); + getAnswer(ans, new ArrayList<>(), candidates, target, 0); + /*************修改的地方*******************/ + // 如果是 Input: candidates = [2,5,2,1,2], target = 5, + // 输出会出现 [2 2 1] [2 1 2] 这样的情况,所以要去重 + return removeDuplicate(ans); + /****************************************/ +} + +private void getAnswer(List> ans, ArrayList temp, int[] candidates, int target, int start) { + if (target == 0) { + ans.add(new ArrayList(temp)); + } else if (target < 0) { + return; + } else { + for (int i = start; i < candidates.length; i++) { + temp.add(candidates[i]); + /*************修改的地方*******************/ + //i -> i + 1 ,因为每个数字只能用一次,所以下次遍历的时候不从自己开始 + getAnswer(ans, temp, candidates, target - candidates[i], i + 1); + /****************************************/ + temp.remove(temp.size() - 1); + } + } + +} + +private List> removeDuplicate(List> list) { + Map ans = new HashMap(); + for (int i = 0; i < list.size(); i++) { + List l = list.get(i); + Collections.sort(l); + String key = ""; + for (int j = 0; j < l.size() - 1; j++) { + key = key + l.get(j) + ","; + } + key = key + l.get(l.size() - 1); + ans.put(key, ""); + } + List> ans_list = new ArrayList>(); + for (String k : ans.keySet()) { + String[] l = k.split(","); + List temp = new ArrayList(); + for (int i = 0; i < l.length; i++) { + int c = Integer.parseInt(l[i]); + temp.add(c); + } + ans_list.add(temp); + } + return ans_list; +} +``` + +时间复杂度: + +空间复杂度: + +看到[这里](https://leetcode.com/problems/combination-sum-ii/discuss/16878/Combination-Sum-I-II-and-III-Java-solution-(see-the-similarities-yourself)),想法很棒,为了解决重复的情况,我们可以先把数组先排序,这样就好说了。 + +```java +public List> combinationSum2(int[] candidates, int target) { + List> ans = new ArrayList<>(); + Arrays.sort(candidates); //排序 + getAnswer(ans, new ArrayList<>(), candidates, target, 0); + return ans; +} + +private void getAnswer(List> ans, ArrayList temp, int[] candidates, int target, int start) { + if (target == 0) { + ans.add(new ArrayList(temp)); + } else if (target < 0) { + return; + } else { + for (int i = start; i < candidates.length; i++) { + //跳过重复的数字 + if(i > start && candidates[i] == candidates[i-1]) continue; + temp.add(candidates[i]); + /*************修改的地方*******************/ + //i -> i + 1 ,因为每个数字只能用一次,所以下次遍历的时候不从自己开始 + getAnswer(ans, temp, candidates, target - candidates[i], i + 1); + /****************************************/ + temp.remove(temp.size() - 1); + } + } +} + +``` + + + +# 解法二 动态规划 + +怎么去更改上题的算法满足本题,暂时没想到,只想到就是再写个函数对答案再过滤一次。先记录给定的 nums 中的每个数字出现的次数,然后判断每个 list 的数字出现的次数是不是满足小于等于给定的 nums 中的每个数字出现的次数,不满足的话就剔除掉。如果大家有直接改之前算法的好办法可以告诉我,谢谢了。 + +此外,要注意一点就是上题中,给定的 nums 没有重复的,而这题中是有重复的。为了使得和之前一样,所以我们在算法中都得加上 + +```java +if (i > 0 && nums[i] == nums[i - 1]) { + continue; +} +``` + +跳过重复的数字,不然是不能 AC 的,至于原因下边分析下。 + +```java +public List> combinationSum2(int[] nums, int target) { + List>> ans = new ArrayList<>(); //opt 数组 + Arrays.sort(nums);// 将数组有序,这样可以提现结束循环 + for (int sum = 0; sum <= target; sum++) { // 从 0 到 target 求出每一个 opt + List> ans_sum = new ArrayList>(); + for (int i = 0; i < nums.length; i++) { //遍历 nums + /*******************修改的地方********************/ + if (i > 0 && nums[i] == nums[i - 1]) { + continue; + } + /***********************************************/ + if (nums[i] == sum) { + List temp = new ArrayList(); + temp.add(nums[i]); + ans_sum.add(temp); + } else if (nums[i] < sum) { + List> ans_sub = ans.get(sum - nums[i]); + //每一个加上 nums[i] + for (int j = 0; j < ans_sub.size(); j++) { + List temp = new ArrayList(ans_sub.get(j)); + temp.add(nums[i]); + ans_sum.add(temp); + } + } else { + break; + } + } + ans.add(sum, ans_sum); + } + return remove(removeDuplicate(ans.get(target)),nums); +} + +private List> removeDuplicate(List> list) { + Map ans = new HashMap(); + for (int i = 0; i < list.size(); i++) { + List l = list.get(i); + Collections.sort(l); + String key = ""; + //[ 2 3 4 ] 转为 "2,3,4" + for (int j = 0; j < l.size() - 1; j++) { + key = key + l.get(j) + ","; + } + key = key + l.get(l.size() - 1); + ans.put(key, ""); + } + //根据逗号还原 List + List> ans_list = new ArrayList>(); + for (String k : ans.keySet()) { + String[] l = k.split(","); + List temp = new ArrayList(); + for (int i = 0; i < l.length; i++) { + int c = Integer.parseInt(l[i]); + temp.add(c); + } + ans_list.add(temp); + } + return ans_list; +} + +//过滤不满足答案的情况 +private List> remove(List> list, int[] nums) { + HashMap nh = new HashMap(); + List> ans = new ArrayList>(list); + //记录每个数字出现的次数 + for (int n : nums) { + int s = nh.getOrDefault(n, 0); + nh.put(n, s + 1); + } + for (int i = 0; i < list.size(); i++) { + List l = list.get(i); + HashMap temp = new HashMap(); + //记录每个 list 中数字出现的次数 + for (int n : l) { + int s = temp.getOrDefault(n, 0); + temp.put(n, s + 1); + } + for (int n : l) { + //进行比较 + if (temp.get(n) > nh.get(n)) { + ans.remove(l); + break; + } + } + } + return ans; +} +``` + +如果不加跳过重复的数字的话,下边的样例不会通过。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/40_3.jpg) + +这是因为我们求 opt 的时候每个列表的数量在以指数级增加,在上一个 opt 的基础上,每一个列表都增加 5 个 列表。 + +opt [ 1 ] = [ [ 1 ],[ 1 ],[ 1 ],[ 1 ],[ 1 ] ] 数量是 5, + +opt [ 2 ] = [ + +​ [ 1,1 ], [ 1,1 ],[ 1,1 ],[ 1,1 ],[ 1,1 ], + +​ [ 1,1 ], [ 1,1 ],[ 1,1 ],[ 1,1 ],[ 1,1 ] + +​ [ 1,1 ], [ 1,1 ],[ 1,1 ],[ 1,1 ],[ 1,1 ], + +​ [ 1,1 ], [ 1,1 ],[ 1,1 ],[ 1,1 ],[ 1,1 ], + +​ [ 1,1 ], [ 1,1 ],[ 1,1 ],[ 1,1 ],[ 1,1 ],, + +​ ] 数量是 5 * 5。 + +opt [ 3 ] 数量是 5 \* 5 \* 5。 + +到了 opt [ 9 ] 就是 5 的 9 次方,数量是 1953125 内存爆炸了。 + +另一个算法也可以改一下 + +```java +public List> combinationSum2(int[] nums, int target) { + List>> ans = new ArrayList<>(); + Arrays.sort(nums); + if (nums[0] > target) { + return new ArrayList>(); + } + for (int i = 0; i <= target; i++) { + List> ans_i = new ArrayList>(); + ans.add(i, ans_i); + } + + for (int i = 0; i < nums.length; i++) { + /*******************修改的地方********************/ + if (i > 0 && nums[i] == nums[i - 1]) { + continue; + } + /***********************************************/ + for (int sum = nums[i]; sum <= target; sum++) { + List> ans_sum = ans.get(sum); + List> ans_sub = ans.get(sum - nums[i]); + if (sum == nums[i]) { + ArrayList temp = new ArrayList(); + temp.add(nums[i]); + ans_sum.add(temp); + + } + if (ans_sub.size() > 0) { + for (int j = 0; j < ans_sub.size(); j++) { + ArrayList temp = new ArrayList(ans_sub.get(j)); + temp.add(nums[i]); + ans_sum.add(temp); + } + } + } + } + return remove(ans.get(target), nums); + +} + +private List> remove(List> list, int[] nums) { + HashMap nh = new HashMap(); + List> ans = new ArrayList>(list); + for (int n : nums) { + int s = nh.getOrDefault(n, 0); + nh.put(n, s + 1); + } + for (int i = 0; i < list.size(); i++) { + List l = list.get(i); + HashMap temp = new HashMap(); + for (int n : l) { + int s = temp.getOrDefault(n, 0); + temp.put(n, s + 1); + } + for (int n : l) { + if (temp.get(n) > nh.get(n)) { + ans.remove(l); + break; + } + } + } + return ans; +} +``` + +如果不加跳过重复的数字的话,下边的样例不会通过 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/40_2.jpg) + +会发现出现了很多重复的结果,就是因为没有跳过重复的 1。在求 opt [ 1 ] 的时候就变成了 [ [ 1 ],[ 1 ] ] 这样子,由于后边求的时候都是直接在原来每一个列表里加数字,所有后边都是加了两次了。 + +# 总 + 和上题很像,基本上改一改就好了。排序的来排除重复的情况也很妙。还有就是改算法的时候,要考虑到题的要求的变化之处。 \ No newline at end of file diff --git a/leetCode-41-First-Missing-Positive.md b/leetCode-41-First-Missing-Positive.md index b77d851e1..c95fc637f 100644 --- a/leetCode-41-First-Missing-Positive.md +++ b/leetCode-41-First-Missing-Positive.md @@ -1,198 +1,198 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/41.jpg) - -给一串数字,找出缺失的**最小**正数。限制了时间复杂度为 O(n),空间复杂度为 O(1)。 - -# 解法一 交换 - -参考[这里](https://leetcode.com/problems/first-missing-positive/discuss/17071/My-short-c++-solution-O(1)-space-and-O(n)-time?orderBy=most_votes)。 - -如果没限制空间复杂度,我们可以这样想。用一个等大的数组去顺序保存这些数字。 - -比如说,数组 nums [ 3 4 -1 1 8],它的大小是 5。然后再创建一个等大的数组 a,初始化为 [ - 1,- 1,- 1,- 1,-1] 。然后我们遍历 nums,把数字分别存到对应的位置。1 就存到数组 a 的第 1 个位置(a [ 0 ]),2 就存到数组 a 的第 2 个位置(a [ 1 ]),3 就存到数组 a 的第 3 个位置(a [ 2 ])... - -nums [ 0 ] 等于 3,更新 a [ - 1,- 1,3,- 1,-1] 。 - -nums [ 1 ] 等于 4,更新 a [ - 1,- 1,3,4,-1 ] 。 - -nums [ 2 ] 等于 - 1,不是正数,忽略。 - -nums [ 3 ] 等于 1,更新 a [ 1,- 1,3,4,-1 ] 。 - -nums [ 4 ] 等于 8,我们的 a 数组只能存 1 到 5,所以同样忽略。 - -最后,我们只需要遍历 a 数组,遇到第一次 a [ i ] != i + 1,就说明缺失了 i + 1。因为我们的 a 数组每个位置都存着比下标大 1 的数。 - -当然,上边都是基于有一个额外空间讲的。如果没有额外空间,怎么办呢? - -我们直接把原数组当成 a 数组去用。 这样的话,会出现的问题就是之前的数就会被覆盖掉。覆盖之前我们把它放回到当前数字的位置, 换句话说就是交换一下位置。然后把交换回来的数字放到应该在的位置,又交换回来的新数字继续判断,直到交换回来的数字小于 0,或者大于了数组的大小,或者它就是当前位置放的数字了。接着遍历 nums 的下一个数。具体看一下。 - -nums = [ 3 4 -1 1 8 ] - -nums [ 0 ] 等于 3,把 3 放到第 3 个位置,并且把之前第 3 个位置的 -1 放回来,更新 nums [ **-1**, 4, **3**, 1, 8 ]。 - -然后继续判断交换回来的数字,nums [ 0 ] 等于 -1,不是正数,忽略。 - -nums [ 1 ] 等于 4,把 4 放到第 4 个位置,并且把之前第 4个位置的 1 放回来,更新 nums [ -1, **1**, 3, **4**, 8 ]。 - -然后继续判断交换回来的数字,nums [ 1 ] 等于 1,把 1 放到第 1 个位置,并且把之前第 1 个位置的 -1 放回来,更新 nums [ **1**, **-1**, 3, 4, 8 ]。 - -然后继续判断交换回来的数字,nums [ 1 ] 等于 -1,不是正数,忽略。 - -nums [ 2 ] 等于 3,刚好在第 3 个位置,不用管。 - -nums [ 3 ] 等于 4,刚好在第 4 个位置,不用管。 - -nums [ 4 ] 等于 8,我们的 nums 数组只能存 1 到 5,所以同样忽略。 - -最后,我们只需要遍历 nums 数组,遇到第一次 nums [ i ] != i + 1,就说明缺失了 i + 1。因为我们的 nums 数组每个位置都存着比下标大 1 的数。 - -看下代码吧,一个 for 循环,里边再 while 循环。 - -```java -public int firstMissingPositive(int[] nums) { - int n = nums.length; - //遍历每个数字 - for (int i = 0; i < n; i++) { - //判断交换回来的数字 - while (nums[i] > 0 && nums[i] <= n && nums[i] != nums[nums[i] - 1]) { - //第 nums[i] 个位置的下标是 nums[i] - 1 - swap(nums, i, nums[i] - 1); - } - } - //找出第一个 nums[i] != i + 1 的位置 - for (int i = 0; i < n; i++) { - if (nums[i] != i + 1) { - return i + 1; - } - } - //如果之前的数都满足就返回 n + 1 - return n + 1; -} - -private void swap(int[] nums, int i, int j) { - int temp = nums[i]; - nums[i] = nums[j]; - nums[j] = temp; -} -``` - -时间复杂度:for 循环里边套了个 while 循环,如果粗略的讲,那时间复杂度就是 O(n²)了。我们再从算法的逻辑上分析一下。因为每交换一次,就有一个数字放到了应该在的位置,只有 n 个数字,所以 while 里边的交换函数,最多执行 n 次。所以时间复杂度更精确的说,应该是 O(n)。 - -空间复杂度:O(1)。 - -# 解法二 标记 - -参考[这里](https://leetcode.com/problems/first-missing-positive/discuss/17073/Share-my-O(n)-time-O(1)-space-solution)。 - -同样的,我们先考虑如果可以有额外的空间该怎么做。 - -还是一样,对于 nums = [ 3 4 -1 1 8] ,我们创建一个等大的数组 a,初始化为 [ false,false,false,false,false ]。然后如果 nums 里有 1 就把,第一个位置 a [ 0 ] 改为 true。如果 nums 里有 m ,就把 a [ m - 1 ] 改为 true。看下具体的例子。 - -nums = [ 3 4 -1 1 8] - -nums [ 0 ] 等于 3,更新 a [ false,false,true,false,false ]。 - -nums [ 1 ] 等于 4,更新 a [ false,false,true,true,false ] 。 - -nums [ 2 ] 等于 - 1,不是正数,忽略。 - -nums [ 3 ] 等于 1,更新 a [ true,false,true,true,false ] 。 - -nums [ 4 ] 等于 8,我们的 a 数组只能存 1 到 5,所以同样忽略。 - -然后遍历数组 a ,如果 a [ i ] != true。那么,我们就返回 i + 1。因为 a [ i ] 等于 true 就意味着 i + 1 存在。 - -问题又来了,其实我们没有额外空间,我们只能利用原来的数组 nums。 - -同样我们直接把 nums 用作数组 a。 - -但当我们更新的时候,如果直接把数组的数赋值成 true,那么原来的数字就没了。这里有个很巧妙的技巧。 - -考虑到我们真正关心的只有正数。开始 a 数组的初始化是 false,所以我们把正数当做 false,负数当成 true。如果我们想要把 nums [ i ] 赋值成 true,如果 nums [ i ] 是正数,我们直接取相反数作为标记就行,如果是负数就不用管了。这样做的好处就是,遍历数字的时候,我们只需要取绝对值,就是原来的数了。 - -当然这样又带来一个问题,我们取绝对值的话,之前的负数该怎么办?一取绝对值的话,就会造成干扰。简单粗暴些,我们把正数都放在前边,我们只考虑正数。负数和 0 就丢到最后,遍历的时候不去遍历就可以了。 - -看下具体的例子。 - -nums = [ 3 4 -1 1 8] - -先把所有正数放前边,并且只考虑正数。nums = [ 3 4 1 8 ],正数当作 false,负数当做 true。所以 nums 就可以看成 [ false,false,false,false ]。 - -nums [ 0 ] 等于 3,把第 3 个位置的数字变为负数, 更新 nums [ 3, 4, **- 1**, 8 ],可以看做 [ false,false,true,false]。 - -nums [ 1 ] 等于 4,把第 4 个位置的数字变为负数,更新 nums [ 3, 4, - 1, **- 8** ],可以看做 [ false,false,true,true] 。 - -nums [ 2 ] 等于 - 1,取绝对值为 1,把第 1 个位置的数字变为负数,更新 nums [ **- 3**, 4, - 1, - 8 ],可以看做 [ true,false,true,true] 。 - -nums [ 3 ] 等于 - 8,取绝对值为 8,我们的 nums 数组只考虑 1 到 4,所以忽略。 - -最后再遍历 nums,如果 nums [ i ] 大于 0,就代表缺失了 i + 1。因为正数代表 false。 - -把正数移到最前边,写了两种算法,代码里注释了,大家可以参考下。 - -```java -public int firstMissingPositive(int[] nums) { - int n = nums.length; - //将正数移到前边,并且得到正数的个数 - int k = positiveNumber(nums); - for (int i = 0; i < k; i++) { - //得到要标记的下标 - int index = Math.abs(nums[i]) - 1; - if (index < k) { - //判断要标记的位置的数是不是小于 0,不是小于 0 就取相反数 - int temp = Math.abs(nums[index]); - nums[index] = temp < 0 ? temp : -temp; - } - } - //找到第一个大于 0 的位置 - for (int i = 0; i < k; i++) { - if (nums[i] > 0) { - return i + 1; - } - } - return k + 1; -} - -private int positiveNumber(int[] nums) { - //解法一 把负数和 0 全部交换到最后 - /* int n = nums.length; - for (int i = 0; i < n; i++) { - while (nums[i] <= 0) { - swap(nums, i, n - 1); - n--; - if (i == n) { - break; - } - } - } - return n;*/ - - //解法二 用一个指针 p ,保证 p 之前的都是正数。遍历 nums,每遇到一个正数就把它交换到 p 指针的位置,并且 p 指针后移 - int n = nums.length; - int p = 0; - for (int i = 0; i < n; i++) { - if (nums[i] > 0) { - swap(nums, i, p); - p++; - } - } - return p; - -} - -private void swap(int[] nums, int i, int j) { - int temp = nums[i]; - nums[i] = nums[j]; - nums[j] = temp; -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(1)。 - -# 总 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/41.jpg) + +给一串数字,找出缺失的**最小**正数。限制了时间复杂度为 O(n),空间复杂度为 O(1)。 + +# 解法一 交换 + +参考[这里](https://leetcode.com/problems/first-missing-positive/discuss/17071/My-short-c++-solution-O(1)-space-and-O(n)-time?orderBy=most_votes)。 + +如果没限制空间复杂度,我们可以这样想。用一个等大的数组去顺序保存这些数字。 + +比如说,数组 nums [ 3 4 -1 1 8],它的大小是 5。然后再创建一个等大的数组 a,初始化为 [ - 1,- 1,- 1,- 1,-1] 。然后我们遍历 nums,把数字分别存到对应的位置。1 就存到数组 a 的第 1 个位置(a [ 0 ]),2 就存到数组 a 的第 2 个位置(a [ 1 ]),3 就存到数组 a 的第 3 个位置(a [ 2 ])... + +nums [ 0 ] 等于 3,更新 a [ - 1,- 1,3,- 1,-1] 。 + +nums [ 1 ] 等于 4,更新 a [ - 1,- 1,3,4,-1 ] 。 + +nums [ 2 ] 等于 - 1,不是正数,忽略。 + +nums [ 3 ] 等于 1,更新 a [ 1,- 1,3,4,-1 ] 。 + +nums [ 4 ] 等于 8,我们的 a 数组只能存 1 到 5,所以同样忽略。 + +最后,我们只需要遍历 a 数组,遇到第一次 a [ i ] != i + 1,就说明缺失了 i + 1。因为我们的 a 数组每个位置都存着比下标大 1 的数。 + +当然,上边都是基于有一个额外空间讲的。如果没有额外空间,怎么办呢? + +我们直接把原数组当成 a 数组去用。 这样的话,会出现的问题就是之前的数就会被覆盖掉。覆盖之前我们把它放回到当前数字的位置, 换句话说就是交换一下位置。然后把交换回来的数字放到应该在的位置,又交换回来的新数字继续判断,直到交换回来的数字小于 0,或者大于了数组的大小,或者它就是当前位置放的数字了。接着遍历 nums 的下一个数。具体看一下。 + +nums = [ 3 4 -1 1 8 ] + +nums [ 0 ] 等于 3,把 3 放到第 3 个位置,并且把之前第 3 个位置的 -1 放回来,更新 nums [ **-1**, 4, **3**, 1, 8 ]。 + +然后继续判断交换回来的数字,nums [ 0 ] 等于 -1,不是正数,忽略。 + +nums [ 1 ] 等于 4,把 4 放到第 4 个位置,并且把之前第 4个位置的 1 放回来,更新 nums [ -1, **1**, 3, **4**, 8 ]。 + +然后继续判断交换回来的数字,nums [ 1 ] 等于 1,把 1 放到第 1 个位置,并且把之前第 1 个位置的 -1 放回来,更新 nums [ **1**, **-1**, 3, 4, 8 ]。 + +然后继续判断交换回来的数字,nums [ 1 ] 等于 -1,不是正数,忽略。 + +nums [ 2 ] 等于 3,刚好在第 3 个位置,不用管。 + +nums [ 3 ] 等于 4,刚好在第 4 个位置,不用管。 + +nums [ 4 ] 等于 8,我们的 nums 数组只能存 1 到 5,所以同样忽略。 + +最后,我们只需要遍历 nums 数组,遇到第一次 nums [ i ] != i + 1,就说明缺失了 i + 1。因为我们的 nums 数组每个位置都存着比下标大 1 的数。 + +看下代码吧,一个 for 循环,里边再 while 循环。 + +```java +public int firstMissingPositive(int[] nums) { + int n = nums.length; + //遍历每个数字 + for (int i = 0; i < n; i++) { + //判断交换回来的数字 + while (nums[i] > 0 && nums[i] <= n && nums[i] != nums[nums[i] - 1]) { + //第 nums[i] 个位置的下标是 nums[i] - 1 + swap(nums, i, nums[i] - 1); + } + } + //找出第一个 nums[i] != i + 1 的位置 + for (int i = 0; i < n; i++) { + if (nums[i] != i + 1) { + return i + 1; + } + } + //如果之前的数都满足就返回 n + 1 + return n + 1; +} + +private void swap(int[] nums, int i, int j) { + int temp = nums[i]; + nums[i] = nums[j]; + nums[j] = temp; +} +``` + +时间复杂度:for 循环里边套了个 while 循环,如果粗略的讲,那时间复杂度就是 O(n²)了。我们再从算法的逻辑上分析一下。因为每交换一次,就有一个数字放到了应该在的位置,只有 n 个数字,所以 while 里边的交换函数,最多执行 n 次。所以时间复杂度更精确的说,应该是 O(n)。 + +空间复杂度:O(1)。 + +# 解法二 标记 + +参考[这里](https://leetcode.com/problems/first-missing-positive/discuss/17073/Share-my-O(n)-time-O(1)-space-solution)。 + +同样的,我们先考虑如果可以有额外的空间该怎么做。 + +还是一样,对于 nums = [ 3 4 -1 1 8] ,我们创建一个等大的数组 a,初始化为 [ false,false,false,false,false ]。然后如果 nums 里有 1 就把,第一个位置 a [ 0 ] 改为 true。如果 nums 里有 m ,就把 a [ m - 1 ] 改为 true。看下具体的例子。 + +nums = [ 3 4 -1 1 8] + +nums [ 0 ] 等于 3,更新 a [ false,false,true,false,false ]。 + +nums [ 1 ] 等于 4,更新 a [ false,false,true,true,false ] 。 + +nums [ 2 ] 等于 - 1,不是正数,忽略。 + +nums [ 3 ] 等于 1,更新 a [ true,false,true,true,false ] 。 + +nums [ 4 ] 等于 8,我们的 a 数组只能存 1 到 5,所以同样忽略。 + +然后遍历数组 a ,如果 a [ i ] != true。那么,我们就返回 i + 1。因为 a [ i ] 等于 true 就意味着 i + 1 存在。 + +问题又来了,其实我们没有额外空间,我们只能利用原来的数组 nums。 + +同样我们直接把 nums 用作数组 a。 + +但当我们更新的时候,如果直接把数组的数赋值成 true,那么原来的数字就没了。这里有个很巧妙的技巧。 + +考虑到我们真正关心的只有正数。开始 a 数组的初始化是 false,所以我们把正数当做 false,负数当成 true。如果我们想要把 nums [ i ] 赋值成 true,如果 nums [ i ] 是正数,我们直接取相反数作为标记就行,如果是负数就不用管了。这样做的好处就是,遍历数字的时候,我们只需要取绝对值,就是原来的数了。 + +当然这样又带来一个问题,我们取绝对值的话,之前的负数该怎么办?一取绝对值的话,就会造成干扰。简单粗暴些,我们把正数都放在前边,我们只考虑正数。负数和 0 就丢到最后,遍历的时候不去遍历就可以了。 + +看下具体的例子。 + +nums = [ 3 4 -1 1 8] + +先把所有正数放前边,并且只考虑正数。nums = [ 3 4 1 8 ],正数当作 false,负数当做 true。所以 nums 就可以看成 [ false,false,false,false ]。 + +nums [ 0 ] 等于 3,把第 3 个位置的数字变为负数, 更新 nums [ 3, 4, **- 1**, 8 ],可以看做 [ false,false,true,false]。 + +nums [ 1 ] 等于 4,把第 4 个位置的数字变为负数,更新 nums [ 3, 4, - 1, **- 8** ],可以看做 [ false,false,true,true] 。 + +nums [ 2 ] 等于 - 1,取绝对值为 1,把第 1 个位置的数字变为负数,更新 nums [ **- 3**, 4, - 1, - 8 ],可以看做 [ true,false,true,true] 。 + +nums [ 3 ] 等于 - 8,取绝对值为 8,我们的 nums 数组只考虑 1 到 4,所以忽略。 + +最后再遍历 nums,如果 nums [ i ] 大于 0,就代表缺失了 i + 1。因为正数代表 false。 + +把正数移到最前边,写了两种算法,代码里注释了,大家可以参考下。 + +```java +public int firstMissingPositive(int[] nums) { + int n = nums.length; + //将正数移到前边,并且得到正数的个数 + int k = positiveNumber(nums); + for (int i = 0; i < k; i++) { + //得到要标记的下标 + int index = Math.abs(nums[i]) - 1; + if (index < k) { + //判断要标记的位置的数是不是小于 0,不是小于 0 就取相反数 + int temp = Math.abs(nums[index]); + nums[index] = temp < 0 ? temp : -temp; + } + } + //找到第一个大于 0 的位置 + for (int i = 0; i < k; i++) { + if (nums[i] > 0) { + return i + 1; + } + } + return k + 1; +} + +private int positiveNumber(int[] nums) { + //解法一 把负数和 0 全部交换到最后 + /* int n = nums.length; + for (int i = 0; i < n; i++) { + while (nums[i] <= 0) { + swap(nums, i, n - 1); + n--; + if (i == n) { + break; + } + } + } + return n;*/ + + //解法二 用一个指针 p ,保证 p 之前的都是正数。遍历 nums,每遇到一个正数就把它交换到 p 指针的位置,并且 p 指针后移 + int n = nums.length; + int p = 0; + for (int i = 0; i < n; i++) { + if (nums[i] > 0) { + swap(nums, i, p); + p++; + } + } + return p; + +} + +private void swap(int[] nums, int i, int j) { + int temp = nums[i]; + nums[i] = nums[j]; + nums[j] = temp; +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(1)。 + +# 总 + 对于这种要求空间复杂度的,我们可以先考虑如果有一个等大的空间,我们可以怎么做。然后再考虑如果直接用原数组怎么做,主要是要保证数组的信息不要丢失。目前遇到的,主要有两种方法就是交换和取相反数。 \ No newline at end of file diff --git a/leetCode-42-Trapping-Rain-Water.md b/leetCode-42-Trapping-Rain-Water.md index ea576f308..97098829a 100644 --- a/leetCode-42-Trapping-Rain-Water.md +++ b/leetCode-42-Trapping-Rain-Water.md @@ -1,412 +1,412 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/42.jpg) - -黑色的看成墙,蓝色的看成水,宽度一样,给定一个数组,每个数代表从左到右墙的高度,求出能装多少单位的水。也就是图中蓝色正方形的个数。 - -# 解法一 按行求 - -这是我最开始想到的一个解法,提交后直接 AC 了,自己都震惊了。就是先求高度为 1 的水,再求高度为 2 的水,再求高度为 3 的水。 - -整个思路就是,求第 i 层的水,遍历每个位置,如果当前的高度小于 i,并且两边有高度大于等于 i 的,说明这个地方一定有水,水就可以加 1。 - -如果求高度为 i 的水,首先用一个变量 temp 保存当前累积的水,初始化为 0 。从左到右遍历墙的高度,遇到高度大于等于 i 的时候,开始更新 temp。更新原则是遇到高度小于 i 的就把 temp 加 1,遇到高度大于等于 i 的,就把 temp 加到最终的答案 ans 里,并且 temp 置零,然后继续循环。 - -我们就以题目的例子讲一下。 - -先求第 1 行的水。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/42_2.jpg) - -也就是红色区域中的水,数组是 height = [ 0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1 ] 。 - -原则是高度小于 1,temp ++,高度大于等于 1,ans = ans + temp,temp = 0。 - -temp 初始化为 0 ,ans = 0 - -height [ 0 ] 等于 0 < 1,不更新。 - -height [ 1 ] 等于 1 >= 1,开始更新 temp。 - -height [ 2 ] 等于 0 < 1, temp = temp + 1 = 1。 - -height [ 3 ] 等于 2 >= 1, ans = ans + temp = 1,temp = 0。 - -height [ 4 ] 等于 1 >= 1,ans = ans + temp = 1,temp = 0。 - -height [ 5 ] 等于 0 < 1, temp = temp + 1 = 1。 - -height [ 6 ] 等于 1 >= 1,ans = ans + temp = 2,temp = 0。 - -剩下的 height [ 7 ] 到最后,高度都大于等于 1,更新 ans = ans + temp = 2,temp = 0。而其实 temp 一直都是 0 ,所以 ans 没有变化。 - -再求第 2 行的水。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/42_3.jpg) - -也就是红色区域中的水, - -数组是 height = [ 0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1 ] 。 - -原则是高度小于 2,temp ++,高度大于等于 2,ans = ans + temp,temp = 0。 - -temp 初始化为 0 ,ans 此时等于 2。 - -height [ 0 ] 等于 0 < 2,不更新。 - -height [ 1 ] 等于 1 < 2,不更新。 - -height [ 2 ] 等于 0 < 2, 不更新。 - -height [ 3 ] 等于 2 >= 2, 开始更新 - -height [ 4 ] 等于 1 < 2,temp = temp + 1 = 1。 - -height [ 5 ] 等于 0 < 2, temp = temp + 1 = 2。 - -height [ 6 ] 等于 1 < 2, temp = temp + 1 = 3。 - -height [ 7 ] 等于 3 >= 2, ans = ans + temp = 5,temp = 0。 - -height [ 8 ] 等于 2 >= 2, ans = ans + temp = 3,temp = 0。 - -height [ 9 ] 等于 1 < 2, temp = temp + 1 = 1。 - -height [ 10 ] 等于 2 >= 2, ans = ans + temp = 6,temp = 0。 - -height [ 11 ] 等于 1 < 2, temp = temp + 1 = 1。 - -然后结束循环,此时的 ans 就是 6。 - -再看第 3 层。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/42_4.jpg) - -按照之前的算法,之前的都是小于 3 的,不更新 temp,然后到 height [ 7 ] 等于 3,开始更新 temp,但是后边没有 height 大于等于 3 了,所以 ans 没有更新。 - -所以最终的 ans 就是 6。 - -看下代码吧。 - -```java -public int trap(int[] height) { - int sum = 0; - int max = getMax(height);//找到最大的高度,以便遍历。 - for (int i = 1; i <= max; i++) { - boolean isStart = false; //标记是否开始更新 temp - int temp_sum = 0; - for (int j = 0; j < height.length; j++) { - if (isStart && height[j] < i) { - temp_sum++; - } - if (height[j] >= i) { - sum = sum + temp_sum; - temp_sum = 0; - isStart = true; - } - } - } - return sum; -} - -private int getMax(int[] height) { - int max = 0; - for (int i = 0; i < height.length; i++) { - if (height[i] > max) { - max = height[i]; - } - } - return max; -} -``` - -时间复杂度:如果最大的数是 m,个数是 n,那么就是 O(m * n)。 - -空间复杂度: O (1)。 - -经过他人提醒,这个解法现在 AC 不了了,会报超时,但还是放在这里吧。 下边讲一下, leetcode [solution](https://leetcode.com/problems/trapping-rain-water/solution/) 提供的 4 个算法。 - -# 解法二 按列求 - -求每一列的水,我们只需要关注当前列,以及左边最高的墙,右边最高的墙就够了。 - -装水的多少,当然根据木桶效应,我们只需要看左边最高的墙和右边最高的墙中较矮的一个就够了。 - -所以,根据较矮的那个墙和当前列的墙的高度可以分为三种情况。 - -* 较矮的墙的高度大于当前列的墙的高度 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/42_5.jpg) - - 把正在求的列左边最高的墙和右边最高的墙确定后,然后为了方便理解,我们把无关的墙去掉。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/42_6.jpg) - - 这样就很清楚了,现在想象一下,往两边最高的墙之间注水。正在求的列会有多少水? - - 很明显,较矮的一边,也就是左边的墙的高度,减去当前列的高度就可以了,也就是 2 - 1 = 1,可以存一个单位的水。 - -* 较矮的墙的高度小于当前列的墙的高度 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/42_7.jpg) - - 同样的,我们把其他无关的列去掉。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/42_8.jpg) - - 想象下,往两边最高的墙之间注水。正在求的列会有多少水? - - 正在求的列不会有水,因为它大于了两边较矮的墙。 - -* 较矮的墙的高度等于当前列的墙的高度。 - - 和上一种情况是一样的,不会有水。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/42_9.jpg) - -明白了这三种情况,程序就很好写了,遍历每一列,然后分别求出这一列两边最高的墙。找出较矮的一端,和当前列的高度比较,结果就是上边的三种情况。 - -```java -public int trap(int[] height) { - int sum = 0; - //最两端的列不用考虑,因为一定不会有水。所以下标从 1 到 length - 2 - for (int i = 1; i < height.length - 1; i++) { - int max_left = 0; - //找出左边最高 - for (int j = i - 1; j >= 0; j--) { - if (height[j] > max_left) { - max_left = height[j]; - } - } - int max_right = 0; - //找出右边最高 - for (int j = i + 1; j < height.length; j++) { - if (height[j] > max_right) { - max_right = height[j]; - } - } - //找出两端较小的 - int min = Math.min(max_left, max_right); - //只有较小的一段大于当前列的高度才会有水,其他情况不会有水 - if (min > height[i]) { - sum = sum + (min - height[i]); - } - } - return sum; -} -``` - -时间复杂度:O(n²),遍历每一列需要 n,找出左边最高和右边最高的墙加起来刚好又是一个 n,所以是 n²。 - -空间复杂度:O(1)。 - -# 解法三 动态规划 - -我们注意到,解法二中。对于每一列,我们求它左边最高的墙和右边最高的墙,都是重新遍历一遍所有高度,这里我们可以优化一下。 - -首先用两个数组,max_left [ i ] 代表第 i 列左边最高的墙的高度,max_right [ i ] 代表第 i 列右边最高的墙的高度。(一定要注意下,第 i 列左(右)边最高的墙,是不包括自身的,和 leetcode 上边的讲的有些不同) - -对于 max_left 我们其实可以这样求。 - -max_left [ i ] = Max ( max_left [ i - 1] , height [ i - 1]) 。它前边的墙的左边的最高高度和它前边的墙的高度选一个较大的,就是当前列左边最高的墙了。 - -对于 max_right我们可以这样求。 - -max_right[ i ] = Max ( max_right[ i + 1] , height [ i + 1]) 。它后边的墙的右边的最高高度和它后边的墙的高度选一个较大的,就是当前列右边最高的墙了。 - -这样,我们再利用解法二的算法,就不用在 for 循环里每次重新遍历一次求 max_left 和 max_right 了。 - -```java -public int trap(int[] height) { - int sum = 0; - int[] max_left = new int[height.length]; - int[] max_right = new int[height.length]; - - for (int i = 1; i < height.length - 1; i++) { - max_left[i] = Math.max(max_left[i - 1], height[i - 1]); - } - for (int i = height.length - 2; i >= 0; i--) { - max_right[i] = Math.max(max_right[i + 1], height[i + 1]); - } - for (int i = 1; i < height.length - 1; i++) { - int min = Math.min(max_left[i], max_right[i]); - if (min > height[i]) { - sum = sum + (min - height[i]); - } - } - return sum; -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(n),用来保存每一列左边最高的墙和右边最高的墙。 - -# 解法四 双指针 - -动态规划中,我们常常可以对空间复杂度进行进一步的优化。 - -例如这道题中,可以看到,max_left [ i ] 和 max_right [ i ] 数组中的元素我们其实只用一次,然后就再也不会用到了。所以我们可以不用数组,只用一个元素就行了。我们先改造下 max_left。 - -```java -public int trap(int[] height) { - int sum = 0; - int max_left = 0; - int[] max_right = new int[height.length]; - for (int i = height.length - 2; i >= 0; i--) { - max_right[i] = Math.max(max_right[i + 1], height[i + 1]); - } - for (int i = 1; i < height.length - 1; i++) { - max_left = Math.max(max_left, height[i - 1]); - int min = Math.min(max_left, max_right[i]); - if (min > height[i]) { - sum = sum + (min - height[i]); - } - } - return sum; -} -``` - -我们成功将 max_left 数组去掉了。但是会发现我们不能同时把 max_right 的数组去掉,因为最后的 for 循环是从左到右遍历的,而 max_right 的更新是从右向左的。 - -所以这里要用到两个指针,left 和 right,从两个方向去遍历。 - -那么什么时候从左到右,什么时候从右到左呢?根据下边的代码的更新规则,我们可以知道 - -```java -max_left = Math.max(max_left, height[i - 1]); -``` - -height [ left - 1] 是可能成为 max_left 的变量, 同理,height [ right + 1 ] 是可能成为 right_max 的变量。 - -只要保证 height [ left - 1 ] < height [ right + 1 ] ,那么 max_left 就一定小于 max_right。 - -因为 max_left 是由 height [ left - 1] 更新过来的,而 height [ left - 1 ] 是小于 height [ right + 1] 的,而 height [ right + 1 ] 会更新 max_right,所以间接的得出 max_left 一定小于 max_right。 - -反之,我们就从右到左更。 - -```java -public int trap(int[] height) { - int sum = 0; - int max_left = 0; - int max_right = 0; - int left = 1; - int right = height.length - 2; // 加右指针进去 - for (int i = 1; i < height.length - 1; i++) { - //从左到右更 - if (height[left - 1] < height[right + 1]) { - max_left = Math.max(max_left, height[left - 1]); - int min = max_left; - if (min > height[left]) { - sum = sum + (min - height[left]); - } - left++; - //从右到左更 - } else { - max_right = Math.max(max_right, height[right + 1]); - int min = max_right; - if (min > height[right]) { - sum = sum + (min - height[right]); - } - right--; - } - } - return sum; -} -``` - -时间复杂度: O(n)。 - -空间复杂度: O(1)。 - - # 解法五 栈 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/42_10.jpg) - -说到栈,我们肯定会想到括号匹配了。我们仔细观察蓝色的部分,可以和括号匹配类比下。每次匹配出一对括号(找到对应的一堵墙),就计算这两堵墙中的水。 - -我们用栈保存每堵墙。 - -当遍历墙的高度的时候,如果当前高度小于栈顶的墙高度,说明这里会有积水,我们将墙的高度的下标入栈。 - -如果当前高度大于栈顶的墙的高度,说明之前的积水到这里停下,我们可以计算下有多少积水了。计算完,就把当前的墙继续入栈,作为新的积水的墙。 - -总体的原则就是, - -1. 当前高度小于等于栈顶高度,入栈,指针后移。 - -2. 当前高度大于栈顶高度,出栈,计算出当前墙和栈顶的墙之间水的多少,然后计算当前的高度和新栈的高度的关系,重复第 2 步。直到当前墙的高度不大于栈顶高度或者栈空,然后把当前墙入栈,指针后移。 - -我们看具体的例子。 - -* 首先将 height [ 0 ] 入栈。然后 current 指向的高度大于栈顶高度,所以把栈顶 height [ 0 ] 出栈,然后栈空了,再把 height [ 1 ] 入栈。current 后移。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/42_11.jpg) - -* 然后 current 指向的高度小于栈顶高度,height [ 2 ] 入栈,current 后移。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/42_12.jpg) - -* 然后 current 指向的高度大于栈顶高度,栈顶 height [ 2 ] 出栈。计算 height [ 3 ] 和新的栈顶之间的水。计算完之后继续判断 current 和新的栈顶的关系。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/42_13.jpg) - -* current 指向的高度大于栈顶高度,栈顶 height [ 1 ] 出栈,栈空。所以把 height [ 3 ] 入栈。 currtent 后移。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/42_14.jpg) - -* 然后 current 指向的高度小于栈顶 height [ 3 ] 的高度,height [ 4 ] 入栈。current 后移。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/42_15.jpg) - -* 然后 current 指向的高度小于栈顶 height [ 4 ] 的高度,height [ 5 ] 入栈。current 后移。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/42_16.jpg) - -* 然后 current 指向的高度大于栈顶 height [ 5 ] 的高度,将栈顶 height [ 5 ] 出栈,然后计算 current 指向的墙和新栈顶 height [ 4 ] 之间的水。计算完之后继续判断 current 的指向和新栈顶的关系。此时 height [ 6 ] 不大于栈顶 height [ 4 ] ,所以将 height [ 6 ] 入栈。 current 后移。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/42_17.jpg) - -* 然后 current 指向的高度大于栈顶高度,将栈顶 height [ 6 ] 出栈。计算和新的栈顶 height [ 4 ] 组成两个边界中的水。然后判断 current 和新的栈顶 height [ 4 ] 的关系,依旧是大于,所以把 height [ 4 ] 出栈。计算current 和 新的栈顶 height [ 3 ] 之间的水。然后判断 current 和新的栈顶 height [ 3 ] 的关系,依旧是大于,所以把 height [ 3 ] 出栈,栈空。将 current 指向的 height [ 7 ] 入栈。current 后移。 - - 其实不停的出栈,可以看做是在找与 7 匹配的墙,也就是 3 。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/42_18.jpg) - -而对于计算 current 指向墙和新的栈顶之间的水,根据图的关系,我们可以直接把这两个墙当做之前解法三的 max_left 和 max_right,然后之前弹出的栈顶当做每次遍历的 height [ i ] 。水量就是 Min ( max _ left ,max _ right ) - height [ i ],只不过这里需要乘上两个墙之间的距离。可以看下代码继续理解下。 - -```java -public int trap6(int[] height) { - int sum = 0; - Stack stack = new Stack<>(); - int current = 0; - while (current < height.length) { - //如果栈不空并且当前指向的高度大于栈顶高度就一直循环 - while (!stack.empty() && height[current] > height[stack.peek()]) { - int h = height[stack.peek()]; //取出要出栈的元素 - stack.pop(); //出栈 - if (stack.empty()) { // 栈空就出去 - break; - } - int distance = current - stack.peek() - 1; //两堵墙之前的距离。 - int min = Math.min(height[stack.peek()], height[current]); - sum = sum + distance * (min - h); - } - stack.push(current); //当前指向的墙入栈 - current++; //指针后移 - } - return sum; -} -``` - -时间复杂度:虽然 while 循环里套了一个 while 循环,但是考虑到每个元素最多访问两次,入栈一次和出栈一次,所以时间复杂度是 O(n)。 - -空间复杂度:O(n)。栈的空间。 - -# 总 - -解法二到解法三,利用动态规划,空间换时间,解法三到解法四,优化动态规划的空间,这一系列下来,让人心旷神怡。 - - - - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/42.jpg) + +黑色的看成墙,蓝色的看成水,宽度一样,给定一个数组,每个数代表从左到右墙的高度,求出能装多少单位的水。也就是图中蓝色正方形的个数。 + +# 解法一 按行求 + +这是我最开始想到的一个解法,提交后直接 AC 了,自己都震惊了。就是先求高度为 1 的水,再求高度为 2 的水,再求高度为 3 的水。 + +整个思路就是,求第 i 层的水,遍历每个位置,如果当前的高度小于 i,并且两边有高度大于等于 i 的,说明这个地方一定有水,水就可以加 1。 + +如果求高度为 i 的水,首先用一个变量 temp 保存当前累积的水,初始化为 0 。从左到右遍历墙的高度,遇到高度大于等于 i 的时候,开始更新 temp。更新原则是遇到高度小于 i 的就把 temp 加 1,遇到高度大于等于 i 的,就把 temp 加到最终的答案 ans 里,并且 temp 置零,然后继续循环。 + +我们就以题目的例子讲一下。 + +先求第 1 行的水。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/42_2.jpg) + +也就是红色区域中的水,数组是 height = [ 0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1 ] 。 + +原则是高度小于 1,temp ++,高度大于等于 1,ans = ans + temp,temp = 0。 + +temp 初始化为 0 ,ans = 0 + +height [ 0 ] 等于 0 < 1,不更新。 + +height [ 1 ] 等于 1 >= 1,开始更新 temp。 + +height [ 2 ] 等于 0 < 1, temp = temp + 1 = 1。 + +height [ 3 ] 等于 2 >= 1, ans = ans + temp = 1,temp = 0。 + +height [ 4 ] 等于 1 >= 1,ans = ans + temp = 1,temp = 0。 + +height [ 5 ] 等于 0 < 1, temp = temp + 1 = 1。 + +height [ 6 ] 等于 1 >= 1,ans = ans + temp = 2,temp = 0。 + +剩下的 height [ 7 ] 到最后,高度都大于等于 1,更新 ans = ans + temp = 2,temp = 0。而其实 temp 一直都是 0 ,所以 ans 没有变化。 + +再求第 2 行的水。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/42_3.jpg) + +也就是红色区域中的水, + +数组是 height = [ 0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1 ] 。 + +原则是高度小于 2,temp ++,高度大于等于 2,ans = ans + temp,temp = 0。 + +temp 初始化为 0 ,ans 此时等于 2。 + +height [ 0 ] 等于 0 < 2,不更新。 + +height [ 1 ] 等于 1 < 2,不更新。 + +height [ 2 ] 等于 0 < 2, 不更新。 + +height [ 3 ] 等于 2 >= 2, 开始更新 + +height [ 4 ] 等于 1 < 2,temp = temp + 1 = 1。 + +height [ 5 ] 等于 0 < 2, temp = temp + 1 = 2。 + +height [ 6 ] 等于 1 < 2, temp = temp + 1 = 3。 + +height [ 7 ] 等于 3 >= 2, ans = ans + temp = 5,temp = 0。 + +height [ 8 ] 等于 2 >= 2, ans = ans + temp = 3,temp = 0。 + +height [ 9 ] 等于 1 < 2, temp = temp + 1 = 1。 + +height [ 10 ] 等于 2 >= 2, ans = ans + temp = 6,temp = 0。 + +height [ 11 ] 等于 1 < 2, temp = temp + 1 = 1。 + +然后结束循环,此时的 ans 就是 6。 + +再看第 3 层。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/42_4.jpg) + +按照之前的算法,之前的都是小于 3 的,不更新 temp,然后到 height [ 7 ] 等于 3,开始更新 temp,但是后边没有 height 大于等于 3 了,所以 ans 没有更新。 + +所以最终的 ans 就是 6。 + +看下代码吧。 + +```java +public int trap(int[] height) { + int sum = 0; + int max = getMax(height);//找到最大的高度,以便遍历。 + for (int i = 1; i <= max; i++) { + boolean isStart = false; //标记是否开始更新 temp + int temp_sum = 0; + for (int j = 0; j < height.length; j++) { + if (isStart && height[j] < i) { + temp_sum++; + } + if (height[j] >= i) { + sum = sum + temp_sum; + temp_sum = 0; + isStart = true; + } + } + } + return sum; +} + +private int getMax(int[] height) { + int max = 0; + for (int i = 0; i < height.length; i++) { + if (height[i] > max) { + max = height[i]; + } + } + return max; +} +``` + +时间复杂度:如果最大的数是 m,个数是 n,那么就是 O(m * n)。 + +空间复杂度: O (1)。 + +经过他人提醒,这个解法现在 AC 不了了,会报超时,但还是放在这里吧。 下边讲一下, leetcode [solution](https://leetcode.com/problems/trapping-rain-water/solution/) 提供的 4 个算法。 + +# 解法二 按列求 + +求每一列的水,我们只需要关注当前列,以及左边最高的墙,右边最高的墙就够了。 + +装水的多少,当然根据木桶效应,我们只需要看左边最高的墙和右边最高的墙中较矮的一个就够了。 + +所以,根据较矮的那个墙和当前列的墙的高度可以分为三种情况。 + +* 较矮的墙的高度大于当前列的墙的高度 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/42_5.jpg) + + 把正在求的列左边最高的墙和右边最高的墙确定后,然后为了方便理解,我们把无关的墙去掉。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/42_6.jpg) + + 这样就很清楚了,现在想象一下,往两边最高的墙之间注水。正在求的列会有多少水? + + 很明显,较矮的一边,也就是左边的墙的高度,减去当前列的高度就可以了,也就是 2 - 1 = 1,可以存一个单位的水。 + +* 较矮的墙的高度小于当前列的墙的高度 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/42_7.jpg) + + 同样的,我们把其他无关的列去掉。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/42_8.jpg) + + 想象下,往两边最高的墙之间注水。正在求的列会有多少水? + + 正在求的列不会有水,因为它大于了两边较矮的墙。 + +* 较矮的墙的高度等于当前列的墙的高度。 + + 和上一种情况是一样的,不会有水。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/42_9.jpg) + +明白了这三种情况,程序就很好写了,遍历每一列,然后分别求出这一列两边最高的墙。找出较矮的一端,和当前列的高度比较,结果就是上边的三种情况。 + +```java +public int trap(int[] height) { + int sum = 0; + //最两端的列不用考虑,因为一定不会有水。所以下标从 1 到 length - 2 + for (int i = 1; i < height.length - 1; i++) { + int max_left = 0; + //找出左边最高 + for (int j = i - 1; j >= 0; j--) { + if (height[j] > max_left) { + max_left = height[j]; + } + } + int max_right = 0; + //找出右边最高 + for (int j = i + 1; j < height.length; j++) { + if (height[j] > max_right) { + max_right = height[j]; + } + } + //找出两端较小的 + int min = Math.min(max_left, max_right); + //只有较小的一段大于当前列的高度才会有水,其他情况不会有水 + if (min > height[i]) { + sum = sum + (min - height[i]); + } + } + return sum; +} +``` + +时间复杂度:O(n²),遍历每一列需要 n,找出左边最高和右边最高的墙加起来刚好又是一个 n,所以是 n²。 + +空间复杂度:O(1)。 + +# 解法三 动态规划 + +我们注意到,解法二中。对于每一列,我们求它左边最高的墙和右边最高的墙,都是重新遍历一遍所有高度,这里我们可以优化一下。 + +首先用两个数组,max_left [ i ] 代表第 i 列左边最高的墙的高度,max_right [ i ] 代表第 i 列右边最高的墙的高度。(一定要注意下,第 i 列左(右)边最高的墙,是不包括自身的,和 leetcode 上边的讲的有些不同) + +对于 max_left 我们其实可以这样求。 + +max_left [ i ] = Max ( max_left [ i - 1] , height [ i - 1]) 。它前边的墙的左边的最高高度和它前边的墙的高度选一个较大的,就是当前列左边最高的墙了。 + +对于 max_right我们可以这样求。 + +max_right[ i ] = Max ( max_right[ i + 1] , height [ i + 1]) 。它后边的墙的右边的最高高度和它后边的墙的高度选一个较大的,就是当前列右边最高的墙了。 + +这样,我们再利用解法二的算法,就不用在 for 循环里每次重新遍历一次求 max_left 和 max_right 了。 + +```java +public int trap(int[] height) { + int sum = 0; + int[] max_left = new int[height.length]; + int[] max_right = new int[height.length]; + + for (int i = 1; i < height.length - 1; i++) { + max_left[i] = Math.max(max_left[i - 1], height[i - 1]); + } + for (int i = height.length - 2; i >= 0; i--) { + max_right[i] = Math.max(max_right[i + 1], height[i + 1]); + } + for (int i = 1; i < height.length - 1; i++) { + int min = Math.min(max_left[i], max_right[i]); + if (min > height[i]) { + sum = sum + (min - height[i]); + } + } + return sum; +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(n),用来保存每一列左边最高的墙和右边最高的墙。 + +# 解法四 双指针 + +动态规划中,我们常常可以对空间复杂度进行进一步的优化。 + +例如这道题中,可以看到,max_left [ i ] 和 max_right [ i ] 数组中的元素我们其实只用一次,然后就再也不会用到了。所以我们可以不用数组,只用一个元素就行了。我们先改造下 max_left。 + +```java +public int trap(int[] height) { + int sum = 0; + int max_left = 0; + int[] max_right = new int[height.length]; + for (int i = height.length - 2; i >= 0; i--) { + max_right[i] = Math.max(max_right[i + 1], height[i + 1]); + } + for (int i = 1; i < height.length - 1; i++) { + max_left = Math.max(max_left, height[i - 1]); + int min = Math.min(max_left, max_right[i]); + if (min > height[i]) { + sum = sum + (min - height[i]); + } + } + return sum; +} +``` + +我们成功将 max_left 数组去掉了。但是会发现我们不能同时把 max_right 的数组去掉,因为最后的 for 循环是从左到右遍历的,而 max_right 的更新是从右向左的。 + +所以这里要用到两个指针,left 和 right,从两个方向去遍历。 + +那么什么时候从左到右,什么时候从右到左呢?根据下边的代码的更新规则,我们可以知道 + +```java +max_left = Math.max(max_left, height[i - 1]); +``` + +height [ left - 1] 是可能成为 max_left 的变量, 同理,height [ right + 1 ] 是可能成为 right_max 的变量。 + +只要保证 height [ left - 1 ] < height [ right + 1 ] ,那么 max_left 就一定小于 max_right。 + +因为 max_left 是由 height [ left - 1] 更新过来的,而 height [ left - 1 ] 是小于 height [ right + 1] 的,而 height [ right + 1 ] 会更新 max_right,所以间接的得出 max_left 一定小于 max_right。 + +反之,我们就从右到左更。 + +```java +public int trap(int[] height) { + int sum = 0; + int max_left = 0; + int max_right = 0; + int left = 1; + int right = height.length - 2; // 加右指针进去 + for (int i = 1; i < height.length - 1; i++) { + //从左到右更 + if (height[left - 1] < height[right + 1]) { + max_left = Math.max(max_left, height[left - 1]); + int min = max_left; + if (min > height[left]) { + sum = sum + (min - height[left]); + } + left++; + //从右到左更 + } else { + max_right = Math.max(max_right, height[right + 1]); + int min = max_right; + if (min > height[right]) { + sum = sum + (min - height[right]); + } + right--; + } + } + return sum; +} +``` + +时间复杂度: O(n)。 + +空间复杂度: O(1)。 + + # 解法五 栈 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/42_10.jpg) + +说到栈,我们肯定会想到括号匹配了。我们仔细观察蓝色的部分,可以和括号匹配类比下。每次匹配出一对括号(找到对应的一堵墙),就计算这两堵墙中的水。 + +我们用栈保存每堵墙。 + +当遍历墙的高度的时候,如果当前高度小于栈顶的墙高度,说明这里会有积水,我们将墙的高度的下标入栈。 + +如果当前高度大于栈顶的墙的高度,说明之前的积水到这里停下,我们可以计算下有多少积水了。计算完,就把当前的墙继续入栈,作为新的积水的墙。 + +总体的原则就是, + +1. 当前高度小于等于栈顶高度,入栈,指针后移。 + +2. 当前高度大于栈顶高度,出栈,计算出当前墙和栈顶的墙之间水的多少,然后计算当前的高度和新栈的高度的关系,重复第 2 步。直到当前墙的高度不大于栈顶高度或者栈空,然后把当前墙入栈,指针后移。 + +我们看具体的例子。 + +* 首先将 height [ 0 ] 入栈。然后 current 指向的高度大于栈顶高度,所以把栈顶 height [ 0 ] 出栈,然后栈空了,再把 height [ 1 ] 入栈。current 后移。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/42_11.jpg) + +* 然后 current 指向的高度小于栈顶高度,height [ 2 ] 入栈,current 后移。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/42_12.jpg) + +* 然后 current 指向的高度大于栈顶高度,栈顶 height [ 2 ] 出栈。计算 height [ 3 ] 和新的栈顶之间的水。计算完之后继续判断 current 和新的栈顶的关系。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/42_13.jpg) + +* current 指向的高度大于栈顶高度,栈顶 height [ 1 ] 出栈,栈空。所以把 height [ 3 ] 入栈。 currtent 后移。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/42_14.jpg) + +* 然后 current 指向的高度小于栈顶 height [ 3 ] 的高度,height [ 4 ] 入栈。current 后移。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/42_15.jpg) + +* 然后 current 指向的高度小于栈顶 height [ 4 ] 的高度,height [ 5 ] 入栈。current 后移。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/42_16.jpg) + +* 然后 current 指向的高度大于栈顶 height [ 5 ] 的高度,将栈顶 height [ 5 ] 出栈,然后计算 current 指向的墙和新栈顶 height [ 4 ] 之间的水。计算完之后继续判断 current 的指向和新栈顶的关系。此时 height [ 6 ] 不大于栈顶 height [ 4 ] ,所以将 height [ 6 ] 入栈。 current 后移。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/42_17.jpg) + +* 然后 current 指向的高度大于栈顶高度,将栈顶 height [ 6 ] 出栈。计算和新的栈顶 height [ 4 ] 组成两个边界中的水。然后判断 current 和新的栈顶 height [ 4 ] 的关系,依旧是大于,所以把 height [ 4 ] 出栈。计算current 和 新的栈顶 height [ 3 ] 之间的水。然后判断 current 和新的栈顶 height [ 3 ] 的关系,依旧是大于,所以把 height [ 3 ] 出栈,栈空。将 current 指向的 height [ 7 ] 入栈。current 后移。 + + 其实不停的出栈,可以看做是在找与 7 匹配的墙,也就是 3 。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/42_18.jpg) + +而对于计算 current 指向墙和新的栈顶之间的水,根据图的关系,我们可以直接把这两个墙当做之前解法三的 max_left 和 max_right,然后之前弹出的栈顶当做每次遍历的 height [ i ] 。水量就是 Min ( max _ left ,max _ right ) - height [ i ],只不过这里需要乘上两个墙之间的距离。可以看下代码继续理解下。 + +```java +public int trap6(int[] height) { + int sum = 0; + Stack stack = new Stack<>(); + int current = 0; + while (current < height.length) { + //如果栈不空并且当前指向的高度大于栈顶高度就一直循环 + while (!stack.empty() && height[current] > height[stack.peek()]) { + int h = height[stack.peek()]; //取出要出栈的元素 + stack.pop(); //出栈 + if (stack.empty()) { // 栈空就出去 + break; + } + int distance = current - stack.peek() - 1; //两堵墙之前的距离。 + int min = Math.min(height[stack.peek()], height[current]); + sum = sum + distance * (min - h); + } + stack.push(current); //当前指向的墙入栈 + current++; //指针后移 + } + return sum; +} +``` + +时间复杂度:虽然 while 循环里套了一个 while 循环,但是考虑到每个元素最多访问两次,入栈一次和出栈一次,所以时间复杂度是 O(n)。 + +空间复杂度:O(n)。栈的空间。 + +# 总 + +解法二到解法三,利用动态规划,空间换时间,解法三到解法四,优化动态规划的空间,这一系列下来,让人心旷神怡。 + + + + diff --git a/leetCode-43-Multiply-Strings.md b/leetCode-43-Multiply-Strings.md index 66ba9101b..d41bd6aae 100644 --- a/leetCode-43-Multiply-Strings.md +++ b/leetCode-43-Multiply-Strings.md @@ -1,128 +1,128 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/43.jpg) - -就是两个数相乘,输出结果,只不过数字很大很大,都是用 String 存储的。也就是传说中的大数相乘。 - -# 解法一 - -我们就模仿我们在纸上做乘法的过程写出一个算法。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/43_2.jpg) - -个位乘个位,得出一个数,然后个位乘十位,全部乘完以后,就再用十位乘以各个位。然后百位乘以各个位,最后将每次得出的数相加。十位的结果要补 1 个 0 ,百位的结果要补两个 0 。相加的话我们可以直接用之前的[大数相加](https://leetcode.windliang.cc/leetCode-2-Add-Two-Numbers.html)。直接看代码吧。 - -```java -public String multiply(String num1, String num2) { - if (num1.equals("0") || num2.equals("0")) { - return "0"; - } - String ans = "0"; - int index = 0; //记录当前是哪一位,便于后边补 0 - for (int i = num2.length() - 1; i >= 0; i--) { - int carry = 0; //保存进位 - String ans_part = ""; //直接用字符串保存每位乘出来的数 - int m = num2.charAt(i) - '0'; - //乘上每一位 - for (int j = num1.length() - 1; j >= 0; j--) { - int n = num1.charAt(j) - '0'; - int mul = m * n + carry; - ans_part = mul % 10 + "" + ans_part; - carry = mul / 10; - } - if (carry > 0) { - ans_part = carry + "" + ans_part; - } - //补 0 - for (int k = 0; k < index; k++) { - ans_part = ans_part + "0"; - } - index++; - //和之前的结果相加 - ans = sumString(ans, ans_part); - } - return ans; -} -//大数相加 -private String sumString(String num1, String num2) { - int carry = 0; - int num1_index = num1.length() - 1; - int num2_index = num2.length() - 1; - String ans = ""; - while (num1_index >= 0 || num2_index >= 0) { - int n1 = num1_index >= 0 ? num1.charAt(num1_index) - '0' : 0; - int n2 = num2_index >= 0 ? num2.charAt(num2_index) - '0' : 0; - int sum = n1 + n2 + carry; - carry = sum / 10; - ans = sum % 10 + "" + ans; - num1_index--; - num2_index--; - } - if (carry > 0) { - ans = carry + "" + ans; - } - return ans; -} -``` - -时间复杂度:O(m * n)。m,n 是两个字符串的长度。 - -空间复杂度:O(1)。 - -# 解法二 - -参考[这里](https://leetcode.com/problems/multiply-strings/discuss/17605/Easiest-JAVA-Solution-with-Graph-Explanation)。 - -上边的解法非常简单粗暴,但是不够优雅。我们看一下从未见过的一种竖式计算。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/43_3.jpg) - -我们把进位先不算,写到对应的位置。最后再统一更新 pos 中的每一位。 - -而对于运算中的每个结果,可以观察出一个结论。 - -num1 的第 i 位乘上 num2 的第 j 位,结果会分别对应 pos 的第 i + j 位和第 i + j + 1 位。 - -例如图中的红色部分,num1 的第 1 位乘上 num2 的第 0 位,结果就对应 pos 的第 1 + 0 = 1 和 1 + 0 + 1 = 2 位。 - -有了这一点,我们就可以遍历求出每一个结果,然后更新 pos 上的值就够了。 - -```java -public String multiply(String num1, String num2) { - if (num1.equals("0") || num2.equals("0")) { - return "0"; - } - int n1 = num1.length(); - int n2 = num2.length(); - int[] pos = new int[n1 + n2]; //保存最后的结果 - for (int i = n1 - 1; i >= 0; i--) { - for (int j = n2 - 1; j >= 0; j--) { - //相乘的结果 - int mul = (num1.charAt(i) - '0') * (num2.charAt(j) - '0'); - //加上 pos[i+j+1] 之前已经累加的结果 - int sum = mul + pos[i + j + 1]; - //更新 pos[i + j] - pos[i + j] += sum / 10; - //更新 pos[i + j + 1] - pos[i + j + 1] = sum % 10; - } - } - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < pos.length; i++) { - //判断最高位是不是 0 - if (i == 0 && pos[i] == 0) { - continue; - } - sb.append(pos[i]); - } - return sb.toString(); -} -``` - -时间复杂度:O(m * n)。m,n 是两个字符串的长度。 - -空间复杂度:O(m + n)。m,n 是两个字符串的长度。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/43.jpg) + +就是两个数相乘,输出结果,只不过数字很大很大,都是用 String 存储的。也就是传说中的大数相乘。 + +# 解法一 + +我们就模仿我们在纸上做乘法的过程写出一个算法。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/43_2.jpg) + +个位乘个位,得出一个数,然后个位乘十位,全部乘完以后,就再用十位乘以各个位。然后百位乘以各个位,最后将每次得出的数相加。十位的结果要补 1 个 0 ,百位的结果要补两个 0 。相加的话我们可以直接用之前的[大数相加](https://leetcode.windliang.cc/leetCode-2-Add-Two-Numbers.html)。直接看代码吧。 + +```java +public String multiply(String num1, String num2) { + if (num1.equals("0") || num2.equals("0")) { + return "0"; + } + String ans = "0"; + int index = 0; //记录当前是哪一位,便于后边补 0 + for (int i = num2.length() - 1; i >= 0; i--) { + int carry = 0; //保存进位 + String ans_part = ""; //直接用字符串保存每位乘出来的数 + int m = num2.charAt(i) - '0'; + //乘上每一位 + for (int j = num1.length() - 1; j >= 0; j--) { + int n = num1.charAt(j) - '0'; + int mul = m * n + carry; + ans_part = mul % 10 + "" + ans_part; + carry = mul / 10; + } + if (carry > 0) { + ans_part = carry + "" + ans_part; + } + //补 0 + for (int k = 0; k < index; k++) { + ans_part = ans_part + "0"; + } + index++; + //和之前的结果相加 + ans = sumString(ans, ans_part); + } + return ans; +} +//大数相加 +private String sumString(String num1, String num2) { + int carry = 0; + int num1_index = num1.length() - 1; + int num2_index = num2.length() - 1; + String ans = ""; + while (num1_index >= 0 || num2_index >= 0) { + int n1 = num1_index >= 0 ? num1.charAt(num1_index) - '0' : 0; + int n2 = num2_index >= 0 ? num2.charAt(num2_index) - '0' : 0; + int sum = n1 + n2 + carry; + carry = sum / 10; + ans = sum % 10 + "" + ans; + num1_index--; + num2_index--; + } + if (carry > 0) { + ans = carry + "" + ans; + } + return ans; +} +``` + +时间复杂度:O(m * n)。m,n 是两个字符串的长度。 + +空间复杂度:O(1)。 + +# 解法二 + +参考[这里](https://leetcode.com/problems/multiply-strings/discuss/17605/Easiest-JAVA-Solution-with-Graph-Explanation)。 + +上边的解法非常简单粗暴,但是不够优雅。我们看一下从未见过的一种竖式计算。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/43_3.jpg) + +我们把进位先不算,写到对应的位置。最后再统一更新 pos 中的每一位。 + +而对于运算中的每个结果,可以观察出一个结论。 + +num1 的第 i 位乘上 num2 的第 j 位,结果会分别对应 pos 的第 i + j 位和第 i + j + 1 位。 + +例如图中的红色部分,num1 的第 1 位乘上 num2 的第 0 位,结果就对应 pos 的第 1 + 0 = 1 和 1 + 0 + 1 = 2 位。 + +有了这一点,我们就可以遍历求出每一个结果,然后更新 pos 上的值就够了。 + +```java +public String multiply(String num1, String num2) { + if (num1.equals("0") || num2.equals("0")) { + return "0"; + } + int n1 = num1.length(); + int n2 = num2.length(); + int[] pos = new int[n1 + n2]; //保存最后的结果 + for (int i = n1 - 1; i >= 0; i--) { + for (int j = n2 - 1; j >= 0; j--) { + //相乘的结果 + int mul = (num1.charAt(i) - '0') * (num2.charAt(j) - '0'); + //加上 pos[i+j+1] 之前已经累加的结果 + int sum = mul + pos[i + j + 1]; + //更新 pos[i + j] + pos[i + j] += sum / 10; + //更新 pos[i + j + 1] + pos[i + j + 1] = sum % 10; + } + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < pos.length; i++) { + //判断最高位是不是 0 + if (i == 0 && pos[i] == 0) { + continue; + } + sb.append(pos[i]); + } + return sb.toString(); +} +``` + +时间复杂度:O(m * n)。m,n 是两个字符串的长度。 + +空间复杂度:O(m + n)。m,n 是两个字符串的长度。 + +# 总 + 如果按普通的思路写,这道题也不难。新的竖式的计算,让人眼前一亮,代码优雅了很多。 \ No newline at end of file diff --git a/leetCode-44-Wildcard-Matching.md b/leetCode-44-Wildcard-Matching.md index 44a8af1fb..d44c22e6a 100644 --- a/leetCode-44-Wildcard-Matching.md +++ b/leetCode-44-Wildcard-Matching.md @@ -1,130 +1,292 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/44.png) - -字符串匹配,? 匹配单个任意字符,* 匹配任意长度字符串,包括空串。和[第 10 题](https://leetcode.windliang.cc/leetCode-10-Regular-Expression-Matching.html)有些类似。 - -# 解法一 动态规划 - -直接按照之前[第 10 题](https://leetcode.windliang.cc/leetCode-10-Regular-Expression-Matching.html),修改一下就可以了。 - -同样是用 dp\[i\]\[j\] 表示所有的情况,然后一层一层的根据递推关系求出来。 - -```java -public boolean isMatch(String text, String pattern) { - // 多一维的空间,因为求 dp[len - 1][j] 的时候需要知道 dp[len][j] 的情况, - // 多一维的话,就可以把 对 dp[len - 1][j] 也写进循环了 - boolean[][] dp = new boolean[text.length() + 1][pattern.length() + 1]; - // dp[len][len] 代表两个空串是否匹配了,"" 和 "" ,当然是 true 了。 - dp[text.length()][pattern.length()] = true; - - // 从 len 开始减少 - for (int i = text.length(); i >= 0; i--) { - for (int j = pattern.length(); j >= 0; j--) { - // dp[text.length()][pattern.length()] 已经进行了初始化 - if (i == text.length() && j == pattern.length()) - continue; - //相比之前增加了判断是否等于 * - boolean first_match = (i < text.length() && j < pattern.length() && (pattern.charAt(j) == text.charAt(i) || pattern.charAt(j) == '?' || pattern.charAt(j) == '*')); - if (j < pattern.length() && pattern.charAt(j) == '*') { - //将 * 跳过 和将字符匹配一个并且 pattern 不变两种情况 - dp[i][j] = dp[i][j + 1] || first_match && dp[i + 1][j]; - } else { - dp[i][j] = first_match && dp[i + 1][j + 1]; - } - } - } - return dp[0][0]; - } -``` - -时间复杂度:text 长度是 T,pattern 长度是 P,那么就是 O(TP)。 - -空间复杂度:O(TP)。 - -同样的,和[第10题](https://leetcode.windliang.cc/leetCode-10-Regular-Expression-Matching.html)一样,可以优化空间复杂度。 - -```java -public boolean isMatch(String text, String pattern) { - // 多一维的空间,因为求 dp[len - 1][j] 的时候需要知道 dp[len][j] 的情况, - // 多一维的话,就可以把 对 dp[len - 1][j] 也写进循环了 - boolean[][] dp = new boolean[2][pattern.length() + 1]; - dp[text.length() % 2][pattern.length()] = true; - - // 从 len 开始减少 - for (int i = text.length(); i >= 0; i--) { - for (int j = pattern.length(); j >= 0; j--) { - if (i == text.length() && j == pattern.length()) - continue; - boolean first_match = (i < text.length() && j < pattern.length() && (pattern.charAt(j) == text.charAt(i) - || pattern.charAt(j) == '?' || pattern.charAt(j) == '*')); - if (j < pattern.length() && pattern.charAt(j) == '*') { - dp[i % 2][j] = dp[i % 2][j + 1] || first_match && dp[(i + 1) % 2][j]; - } else { - dp[i % 2][j] = first_match && dp[(i + 1) % 2][j + 1]; - } - } - } - return dp[0][0]; -} -``` - -时间复杂度:text 长度是 T,pattern 长度是 P,那么就是 O(TP)。 - -空间复杂度:O(P)。 - -# 解法二 迭代 - -参考[这里](https://leetcode.com/problems/wildcard-matching/discuss/17810/Linear-runtime-and-constant-space-solution?orderBy=most_votes),也比较好理解,利用两个指针进行遍历。 - -```java -boolean isMatch(String str, String pattern) { - int s = 0, p = 0, match = 0, starIdx = -1; - //遍历整个字符串 - while (s < str.length()){ - // 一对一匹配,两指针同时后移。 - if (p < pattern.length() && (pattern.charAt(p) == '?' || str.charAt(s) == pattern.charAt(p))){ - s++; - p++; - } - // 碰到 *,假设它匹配空串,并且用 startIdx 记录 * 的位置,记录当前字符串的位置,p 后移 - else if (p < pattern.length() && pattern.charAt(p) == '*'){ - starIdx = p; - match = s; - p++; - } - // 当前字符不匹配,并且也没有 *,回退 - // p 回到 * 的下一个位置 - // match 更新到下一个位置 - // s 回到更新后的 match - // 这步代表用 * 匹配了一个字符 - else if (starIdx != -1){ - p = starIdx + 1; - match++; - s = match; - } - //字符不匹配,也没有 *,返回 false - else return false; - } - - //将末尾多余的 * 直接匹配空串 例如 text = ab, pattern = a******* - while (p < pattern.length() && pattern.charAt(p) == '*') - p++; - - return p == pattern.length(); -} -``` - -时间复杂度:如果 str 长度是 T,pattern 长度是 P,虽然只有一个 while 循环,但是 s 并不是每次都加 1,所以最坏的时候时间复杂度会达到 O(TP),例如 str = "bbbbbbbbbb",pattern = "*bbbb"。每次 pattern 到最后时,又会重新开始到开头。 - -空间复杂度:O(1)。 - -# 递归 - -在[第10题](https://leetcode.windliang.cc/leetCode-10-Regular-Expression-Matching.html)中还有递归的解法,但这题中如果按照第 10 题的递归的思路去解决,会导致超时,目前没想到怎么在第 10 题的基础上去改,有好的想法大家可以和我交流。 - -如果非要用递归的话,可以按照动态规划那个思路,先压栈,然后出栈过程其实就是动态规划那样了。所以其实不如直接动态规划。 - -# 总 - -动态规划的应用,理清递推的公式就可以。另外迭代的方法,也让人眼前一亮。 \ No newline at end of file +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/44.png) + +字符串匹配,? 匹配单个任意字符,* 匹配任意长度字符串,包括空串。和[第 10 题](https://leetcode.windliang.cc/leetCode-10-Regular-Expression-Matching.html)有些类似。 + +# 解法一 动态规划 + +直接按照之前[第 10 题](https://leetcode.windliang.cc/leetCode-10-Regular-Expression-Matching.html),修改一下就可以了。 + +同样是用 dp\[i\]\[j\] 表示所有的情况,然后一层一层的根据递推关系求出来。 + +```java +public boolean isMatch(String text, String pattern) { + // 多一维的空间,因为求 dp[len - 1][j] 的时候需要知道 dp[len][j] 的情况, + // 多一维的话,就可以把 对 dp[len - 1][j] 也写进循环了 + boolean[][] dp = new boolean[text.length() + 1][pattern.length() + 1]; + // dp[len][len] 代表两个空串是否匹配了,"" 和 "" ,当然是 true 了。 + dp[text.length()][pattern.length()] = true; + + // 从 len 开始减少 + for (int i = text.length(); i >= 0; i--) { + for (int j = pattern.length(); j >= 0; j--) { + // dp[text.length()][pattern.length()] 已经进行了初始化 + if (i == text.length() && j == pattern.length()) + continue; + //相比之前增加了判断是否等于 * + boolean first_match = (i < text.length() && j < pattern.length() && (pattern.charAt(j) == text.charAt(i) || pattern.charAt(j) == '?' || pattern.charAt(j) == '*')); + if (j < pattern.length() && pattern.charAt(j) == '*') { + //将 * 跳过 和将字符匹配一个并且 pattern 不变两种情况 + dp[i][j] = dp[i][j + 1] || first_match && dp[i + 1][j]; + } else { + dp[i][j] = first_match && dp[i + 1][j + 1]; + } + } + } + return dp[0][0]; + } +``` + +时间复杂度:text 长度是 T,pattern 长度是 P,那么就是 O(TP)。 + +空间复杂度:O(TP)。 + +同样的,和[第10题](https://leetcode.windliang.cc/leetCode-10-Regular-Expression-Matching.html)一样,可以优化空间复杂度。 + +```java +public boolean isMatch(String text, String pattern) { + // 多一维的空间,因为求 dp[len - 1][j] 的时候需要知道 dp[len][j] 的情况, + // 多一维的话,就可以把 对 dp[len - 1][j] 也写进循环了 + boolean[][] dp = new boolean[2][pattern.length() + 1]; + dp[text.length() % 2][pattern.length()] = true; + + // 从 len 开始减少 + for (int i = text.length(); i >= 0; i--) { + for (int j = pattern.length(); j >= 0; j--) { + if (i == text.length() && j == pattern.length()) + continue; + boolean first_match = (i < text.length() && j < pattern.length() && (pattern.charAt(j) == text.charAt(i) + || pattern.charAt(j) == '?' || pattern.charAt(j) == '*')); + if (j < pattern.length() && pattern.charAt(j) == '*') { + dp[i % 2][j] = dp[i % 2][j + 1] || first_match && dp[(i + 1) % 2][j]; + } else { + dp[i % 2][j] = first_match && dp[(i + 1) % 2][j + 1]; + } + } + } + return dp[0][0]; +} +``` + +时间复杂度:text 长度是 T,pattern 长度是 P,那么就是 O(TP)。 + +空间复杂度:O(P)。 + +# 解法二 迭代 + +参考[这里](https://leetcode.com/problems/wildcard-matching/discuss/17810/Linear-runtime-and-constant-space-solution?orderBy=most_votes),也比较好理解,利用两个指针进行遍历。 + +```java +boolean isMatch(String str, String pattern) { + int s = 0, p = 0, match = 0, starIdx = -1; + //遍历整个字符串 + while (s < str.length()){ + // 一对一匹配,两指针同时后移。 + if (p < pattern.length() && (pattern.charAt(p) == '?' || str.charAt(s) == pattern.charAt(p))){ + s++; + p++; + } + // 碰到 *,假设它匹配空串,并且用 startIdx 记录 * 的位置,记录当前字符串的位置,p 后移 + else if (p < pattern.length() && pattern.charAt(p) == '*'){ + starIdx = p; + match = s; + p++; + } + // 当前字符不匹配,并且也没有 *,回退 + // p 回到 * 的下一个位置 + // match 更新到下一个位置 + // s 回到更新后的 match + // 这步代表用 * 匹配了一个字符 + else if (starIdx != -1){ + p = starIdx + 1; + match++; + s = match; + } + //字符不匹配,也没有 *,返回 false + else return false; + } + + //将末尾多余的 * 直接匹配空串 例如 text = ab, pattern = a******* + while (p < pattern.length() && pattern.charAt(p) == '*') + p++; + + return p == pattern.length(); +} +``` + +时间复杂度:如果 str 长度是 T,pattern 长度是 P,虽然只有一个 while 循环,但是 s 并不是每次都加 1,所以最坏的时候时间复杂度会达到 O(TP),例如 str = "bbbbbbbbbb",pattern = "*bbbb"。每次 pattern 到最后时,又会重新开始到开头。 + +空间复杂度:O(1)。 + +# 递归 + +在[第10题](https://leetcode.windliang.cc/leetCode-10-Regular-Expression-Matching.html)中还有递归的解法,但这题中如果按照第 10 题的递归的思路去解决,会导致超时,目前没想到怎么在第 10 题的基础上去改,有好的想法大家可以和我交流。 + +如果非要用递归的话,可以按照动态规划那个思路,先压栈,然后出栈过程其实就是动态规划那样了。所以其实不如直接动态规划。 + +# 更新 + +`2021.7.7` 日更新。(太久没写 `java` 代码了,由于换了电脑 `eclipes` 也没有,在 `vscode` 里写 `java` 竟然不会写了,习惯了写 `js` ,分号不加,类型不管,写 `java` 有点不适应了,哈哈) + +上边说到当时按 [第 10 题](https://leetcode.wang/leetCode-10-Regular-Expression-Matching.html) 的递归思路超时了,代码如下: + +```java +class Solution { + public boolean isMatch(String text, String pattern) { + if (pattern.isEmpty()) + return text.isEmpty(); + if (text.isEmpty()) + return pattern.isEmpty() || isStars(pattern); + + boolean first_match = (!text.isEmpty() && (pattern.charAt(0) == text.charAt(0) || pattern.charAt(0) == '?')); + if (pattern.charAt(0) == '*') { + return (isMatch(text.substring(1), pattern) || (isMatch(text.substring(1), pattern.substring(1)))) + || (isMatch(text, pattern.substring(1))); + } else { + return first_match && isMatch(text.substring(1), pattern.substring(1)); + } + } + + private boolean isStars(String pattern) { + // TODO Auto-generated method stub + for (int i = 0; i < pattern.length(); i++) { + if (pattern.charAt(i) != '*') { + return false; + } + } + return true; + } +} +``` + +代码很好理解,这里就不多说了,可以参考 [第 10 题](https://leetcode.wang/leetCode-10-Regular-Expression-Matching.html) 的分析,但有个问题就是会超时。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/leetcode44n1.jpg) + +前几天 [@xuyuntian](https://xuyuntian.gitee.io/) 加了微信告诉我他写出了一个递归的写法,[代码](https://gitee.com/xuyuntian/leetcode/blob/master/src/_44.java) 如下: + +```java +class Solution { + public boolean isMatch(String s, String p) { + return dfs(new Boolean[s.length()][p.length()], s.toCharArray(), p.toCharArray(), 0, 0); + } + private boolean dfs(Boolean[][] dp, char[] s, char[] p, int i, int j) { + if (i == s.length && j == p.length) return true; + if (i > s.length || (i < s.length && j == p.length)) return false; + if (i < s.length) { + if (dp[i][j] != null) return dp[i][j]; + if (p[j] == '?' || p[j] == s[i]) { + return dp[i][j] = dfs(dp, s, p, i + 1, j + 1); + } + } + boolean res = false; + if (p[j] == '*') { + res = dfs(dp, s, p, i + 1, j + 1) || dfs(dp, s, p, i + 1, j) || dfs(dp, s, p, i, j + 1); + } + if (i < s.length) dp[i][j] = res; + return res; + } +} +``` + +看完以后突然就悟了,对啊,`memoization` 技术啊,把递归过程中的结果存起来呀! + +于是我把自己的递归代码用 `HashMap` 改良了一版,把所有结果都用 `HashMap` 存起来。 + +```java +class Solution { + public boolean isMatch(String text, String pattern) { + HashMap map=new HashMap<>(); + return isMatchHelper(text, pattern, map); + } + public boolean isMatchHelper(String text, String pattern, HashMap map) { + if (pattern.isEmpty()) + return text.isEmpty(); + if (text.isEmpty()) + return pattern.isEmpty() || isStars(pattern); + String key = text + '@' + pattern; + if(map.containsKey(key)) { + return map.get(key); + } + boolean first_match = (!text.isEmpty() && (pattern.charAt(0) == text.charAt(0) || pattern.charAt(0) == '?')); + if (pattern.charAt(0) == '*') { + boolean res = (isMatchHelper(text.substring(1), pattern, map) || (isMatchHelper(text.substring(1), pattern.substring(1), map))) + || (isMatchHelper(text, pattern.substring(1), map)); + map.put(key, res); + return res; + } else { + boolean res = first_match && isMatchHelper(text.substring(1), pattern.substring(1), map); + map.put(key, res); + return res; + } + } + + private boolean isStars(String pattern) { + // TODO Auto-generated method stub + for (int i = 0; i < pattern.length(); i++) { + if (pattern.charAt(i) != '*') { + return false; + } + } + return true; + } +} +``` + +遗憾的是竟然超内存了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/leetcode44n2.jpg) + +又看了下 [@xuyuntian](https://xuyuntian.gitee.io/) 的代码,原因只能是 `HashMap` 太占内存了,于是我也改成了用数组缓存结果。同样的,需要将下标在递归中传递。 + +```java +class Solution { + public boolean isMatch(String text, String pattern) { + boolean res = isMatchHelper(text, 0, pattern, 0, new Boolean[text.length()][pattern.length()]); + return res; + } + + public boolean isMatchHelper(String textOrigin, int textStart, String patternOrigin, int patternStart, Boolean[][] map) { + String text = textOrigin.substring(textStart); + String pattern = patternOrigin.substring(patternStart); + if (pattern.isEmpty()) + return text.isEmpty(); + if (text.isEmpty()) + return pattern.isEmpty() || isStars(pattern); + if(map[textStart][patternStart] != null) { + return map[textStart][patternStart] ; + } + boolean first_match = (!text.isEmpty() && (pattern.charAt(0) == text.charAt(0) || pattern.charAt(0) == '?')); + if (pattern.charAt(0) == '*') { + boolean res = (isMatchHelper(textOrigin, textStart + 1,patternOrigin, patternStart, map) || (isMatchHelper(textOrigin, textStart + 1 ,patternOrigin, patternStart + 1, map))) + || (isMatchHelper(textOrigin, textStart, patternOrigin, patternStart + 1, map)); + map[textStart][patternStart] = res; + return res; + } else { + boolean res = first_match && isMatchHelper(textOrigin, textStart + 1 ,patternOrigin, patternStart + 1, map); + map[textStart][patternStart] = res; + return res; + } + } + + private boolean isStars(String pattern) { + // TODO Auto-generated method stub + for (int i = 0; i < pattern.length(); i++) { + if (pattern.charAt(i) != '*') { + return false; + } + } + return true; + } +} +``` + +终于 `AC` 了! + +![](https://windliang.oss-cn-beijing.aliyuncs.com/leetcode44n3.jpg) + +# 总 + +动态规划的应用,理清递推的公式就可以。另外迭代的方法,也让人眼前一亮。 + diff --git a/leetCode-45-Jump-Game-II.md b/leetCode-45-Jump-Game-II.md index 3362f3d2f..1ad9fca25 100644 --- a/leetCode-45-Jump-Game-II.md +++ b/leetCode-45-Jump-Game-II.md @@ -1,87 +1,87 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/45.jpg) - -从数组的第 0 个位置开始跳,跳的距离小于等于数组上对应的数。求出跳到最后个位置需要的最短步数。比如上图中的第 0 个位置是 2,那么可以跳 1 个距离,或者 2 个距离,我们选择跳 1 个距离,就跳到了第 1 个位置,也就是 3 上。然后我们可以跳 1,2,3 个距离,我们选择跳 3 个距离,就直接到最后了。所以总共需要 2 步。 - -# 解法一 顺藤摸瓜 - -参考[这里](https://leetcode.com/problems/jump-game-ii/discuss/18023/Single-loop-simple-java-solution?orderBy=most_votes),leetCode 讨论里,大部分都是这个思路,贪婪算法,我们每次在可跳范围内选择可以使得跳的更远的位置。 - -如下图,开始的位置是 2,可跳的范围是橙色的。然后因为 3 可以跳的更远,所以跳到 3 的位置。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/45_2.jpg) - -如下图,然后现在的位置就是 3 了,能跳的范围是橙色的,然后因为 4 可以跳的更远,所以下次跳到 4 的位置。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/45_3.jpg) - -写代码的话,我们用 end 表示当前能跳的边界,对于上边第一个图的橙色 1,第二个图中就是橙色的 4,遍历数组的时候,到了边界,我们就重新更新新的边界。 - -```java -public int jump(int[] nums) { - int end = 0; - int maxPosition = 0; - int steps = 0; - for(int i = 0; i < nums.length - 1; i++){ - //找能跳的最远的 - maxPosition = Math.max(maxPosition, nums[i] + i); - if( i == end){ //遇到边界,就更新边界,并且步数加一 - end = maxPosition; - steps++; - } - } - return steps; -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(1)。 - -这里要注意一个细节,就是 for 循环中,i < nums.length - 1,少了末尾。因为开始的时候边界是第 0 个位置,steps 已经加 1 了。如下图,如果最后一步刚好跳到了末尾,此时 steps 其实不用加 1 了。如果是 i < nums.length,i 遍历到最后的时候,会进入 if 语句中,steps 会多加 1 。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/45_4.jpg) - -# 解法二 顺瓜摸藤 - -我们知道最终要到达最后一个位置,然后我们找前一个位置,遍历数组,找到能到达它的位置,离它最远的就是要找的位置。然后继续找上上个位置,最后到了第 0 个位置就结束了。 - -至于离它最远的位置,其实我们从左到右遍历数组,第一个满足的位置就是我们要找的。 - -```java -public int jump(int[] nums) { - int position = nums.length - 1; //要找的位置 - int steps = 0; - while (position != 0) { //是否到了第 0 个位置 - for (int i = 0; i < position; i++) { - if (nums[i] >= position - i) { - position = i; //更新要找的位置 - steps++; - break; - } - } - } - return steps; -} -``` - -时间复杂度:O(n²),因为最坏的情况比如 1 1 1 1 1 1,position 会从 5 更新到 0 ,并且每次更新都会经历一个 for 循环。 - -空间复杂度:O(1)。 - -这种想法看起来更简单了,为什么奏效呢?我们可以这样想。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/45_5.jpg) - -从左到右跳的话,2 -> 3 -> 4 -> 1。 - -从右到左的话,我们找能跳到 1 的最左边的位置,我们找的只能是 4 或者是 4 左边的。 - -找到 4 的话,不用说,刚好完美。 - -如果是中间范围 3 和 4 之间的第 2 个 1 变成了 3,那么这个位置也可以跳到末尾的 1,按我们的算法我们就找到了这个 3,也就是 4 左边的位置。但其实并不影响我们的 steps,因为这个数字是 3 到 4 中间范围的数,左边界 3 也可以到这个数,所以下次找的话,会找到边界 3 ,或者边界 3 左边的数。 会不会直接找到 上个边界 2 呢?不会的,如果找到了上一个边界 2,那么意味着从 2 直接跳到 3 和 4 之间的那个数,再从这个数跳到末尾就只需 2 步了,但是其实是需要 3 步的。 - -# 总 - -刷这么多题,第一次遇到了贪心算法,每次找局部最优,最后达到全局最优,完美! +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/45.jpg) + +从数组的第 0 个位置开始跳,跳的距离小于等于数组上对应的数。求出跳到最后个位置需要的最短步数。比如上图中的第 0 个位置是 2,那么可以跳 1 个距离,或者 2 个距离,我们选择跳 1 个距离,就跳到了第 1 个位置,也就是 3 上。然后我们可以跳 1,2,3 个距离,我们选择跳 3 个距离,就直接到最后了。所以总共需要 2 步。 + +# 解法一 顺藤摸瓜 + +参考[这里](https://leetcode.com/problems/jump-game-ii/discuss/18023/Single-loop-simple-java-solution?orderBy=most_votes),leetCode 讨论里,大部分都是这个思路,贪婪算法,我们每次在可跳范围内选择可以使得跳的更远的位置。 + +如下图,开始的位置是 2,可跳的范围是橙色的。然后因为 3 可以跳的更远,所以跳到 3 的位置。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/45_2.jpg) + +如下图,然后现在的位置就是 3 了,能跳的范围是橙色的,然后因为 4 可以跳的更远,所以下次跳到 4 的位置。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/45_3.jpg) + +写代码的话,我们用 end 表示当前能跳的边界,对于上边第一个图的橙色 1,第二个图中就是橙色的 4,遍历数组的时候,到了边界,我们就重新更新新的边界。 + +```java +public int jump(int[] nums) { + int end = 0; + int maxPosition = 0; + int steps = 0; + for(int i = 0; i < nums.length - 1; i++){ + //找能跳的最远的 + maxPosition = Math.max(maxPosition, nums[i] + i); + if( i == end){ //遇到边界,就更新边界,并且步数加一 + end = maxPosition; + steps++; + } + } + return steps; +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(1)。 + +这里要注意一个细节,就是 for 循环中,i < nums.length - 1,少了末尾。因为开始的时候边界是第 0 个位置,steps 已经加 1 了。如下图,如果最后一步刚好跳到了末尾,此时 steps 其实不用加 1 了。如果是 i < nums.length,i 遍历到最后的时候,会进入 if 语句中,steps 会多加 1 。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/45_4.jpg) + +# 解法二 顺瓜摸藤 + +我们知道最终要到达最后一个位置,然后我们找前一个位置,遍历数组,找到能到达它的位置,离它最远的就是要找的位置。然后继续找上上个位置,最后到了第 0 个位置就结束了。 + +至于离它最远的位置,其实我们从左到右遍历数组,第一个满足的位置就是我们要找的。 + +```java +public int jump(int[] nums) { + int position = nums.length - 1; //要找的位置 + int steps = 0; + while (position != 0) { //是否到了第 0 个位置 + for (int i = 0; i < position; i++) { + if (nums[i] >= position - i) { + position = i; //更新要找的位置 + steps++; + break; + } + } + } + return steps; +} +``` + +时间复杂度:O(n²),因为最坏的情况比如 1 1 1 1 1 1,position 会从 5 更新到 0 ,并且每次更新都会经历一个 for 循环。 + +空间复杂度:O(1)。 + +这种想法看起来更简单了,为什么奏效呢?我们可以这样想。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/45_5.jpg) + +从左到右跳的话,2 -> 3 -> 4 -> 1。 + +从右到左的话,我们找能跳到 1 的最左边的位置,我们找的只能是 4 或者是 4 左边的。 + +找到 4 的话,不用说,刚好完美。 + +如果是中间范围 3 和 4 之间的第 2 个 1 变成了 3,那么这个位置也可以跳到末尾的 1,按我们的算法我们就找到了这个 3,也就是 4 左边的位置。但其实并不影响我们的 steps,因为这个数字是 3 到 4 中间范围的数,左边界 3 也可以到这个数,所以下次找的话,会找到边界 3 ,或者边界 3 左边的数。 会不会直接找到 上个边界 2 呢?不会的,如果找到了上一个边界 2,那么意味着从 2 直接跳到 3 和 4 之间的那个数,再从这个数跳到末尾就只需 2 步了,但是其实是需要 3 步的。 + +# 总 + +刷这么多题,第一次遇到了贪心算法,每次找局部最优,最后达到全局最优,完美! diff --git a/leetCode-46-Permutations.md b/leetCode-46-Permutations.md index 900579ca4..c79e00a5b 100644 --- a/leetCode-46-Permutations.md +++ b/leetCode-46-Permutations.md @@ -1,199 +1,199 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/46.jpg) - -描述的很简单,就是给定几个数,然后输出他们所有排列的可能。 - -# 解法一 插入 - -这是自己开始想到的一个方法,考虑的思路是,先考虑小问题怎么解决,然后再利用小问题去解决大问题。没错,就是递归的思路。比如说, - -如果只有 1 个数字 [ 1 ],那么很简单,直接返回 [ [ 1 ] ] 就 OK 了。 - -如果加了 1 个数字 2, [ 1 2 ] 该怎么办呢?我们只需要在上边的情况里,在 1 的空隙,也就是左边右边插入 2 就够了。变成 [ [ **2** 1 ], [ 1 **2** ] ]。 - -如果再加 1 个数字 3,[ 1 2 3 ] 该怎么办呢?同样的,我们只需要在上边的所有情况里的空隙里插入数字 3 就行啦。例如 [ 2 1 ] 在左边,中间,右边插入 3 ,变成 **3** 2 1,2 **3** 1,2 1 **3**。同理,1 2 在左边,中间,右边插入 3,变成 **3** 1 2,1 **3** 2,1 2 **3**,所以最后的结果就是 [ [ 3 2 1],[ 2 3 1],[ 2 1 3 ], [ 3 1 2 ],[ 1 3 2 ],[ 1 2 3 ] ]。 - -如果再加数字也是同样的道理,只需要在之前的情况里,数字的空隙插入新的数字就够了。 - -思路有了,直接看代码吧。 - -```java -public List> permute(int[] nums) { - return permute_end(nums,nums.length-1); -} -// end 表示当前新增的数字的位置 -private List> permute_end(int[] nums, int end) { - // 只有一个数字的时候 - if(end == 0){ - List> all = new ArrayList<>(); - List temp = new ArrayList<>(); - temp.add(nums[0]); - all.add(temp); - return all; - } - //得到上次所有的结果 - List> all_end = permute_end(nums,end-1); - int current_size = all_end.size(); - //遍历每一种情况 - for (int j = 0; j < current_size; j++) { - //在数字的缝隙插入新的数字 - for (int k = 0; k <= end; k++) { - List temp = new ArrayList<>(all_end.get(j)); - temp.add(k, nums[end]); - //添加到结果中 - all_end.add(temp); - }; - - } - //由于 all_end 此时既保存了之前的结果,和添加完的结果,所以把之前的结果要删除 - for (int j = 0; j < current_size; j++) { - all_end.remove(0); - } - return all_end; -} -``` - -既然有递归的过程,我们也可以直接改成迭代的,可以把递归开始不停压栈的过程省略了。 - -```java -public List> permute(int[] nums) { - List> all = new ArrayList<>(); - all.add(new ArrayList<>()); - //在上边的基础上只加上最外层的 for 循环就够了,代表每次新添加的数字 - for (int i = 0; i < nums.length; i++) { - int current_size = all.size(); - for (int j = 0; j < current_size; j++) { - for (int k = 0; k <= i; k++) { - List temp = new ArrayList<>(all.get(j)); - temp.add(k, nums[i]); - all.add(temp); - } - } - for (int j = 0; j < current_size; j++) { - all.remove(0); - } - } - return all; -} -``` - -时间复杂度,如果只分析代码的话挺复杂的。如果从最后的结果来说,应该是 n! 个结果,所以时间复杂度应该是 O(n!)。 - -空间复杂度:O(1)。 - -# 解法二 回溯 - -这个开始没想到,参考[这里](https://leetcode.com/problems/permutations/discuss/18239/A-general-approach-to-backtracking-questions-in-Java-(Subsets-Permutations-Combination-Sum-Palindrome-Partioning))。 - -其实也算是蛮典型的回溯,利用递归每次向 temp 里添加一个数字,数字添加够以后再回来进行回溯,再向后添加新的解。 - -可以理解成一层一层的添加,每一层都是一个 for 循环。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/46_2.jpg) - -每调用一层就进入一个 for 循环,相当于列出了所有解,然后挑选了我们需要的。其实本质上就是深度优先遍历 DFS。 - -```java -public List> permute(int[] nums) { - List> list = new ArrayList<>(); - backtrack(list, new ArrayList<>(), nums); - return list; -} - -private void backtrack(List> list, List tempList, int [] nums){ - if(tempList.size() == nums.length){ - list.add(new ArrayList<>(tempList)); - } else{ - for(int i = 0; i < nums.length; i++){ - if(tempList.contains(nums[i])) continue; // 已经存在的元素,跳过 - tempList.add(nums[i]); //将当前元素加入 - backtrack(list, tempList, nums); //向后继续添加 - tempList.remove(tempList.size() - 1); //将 tempList 刚添加的元素,去掉,尝试新的元素 - } - } -} -``` - -时间复杂度: - -空间复杂度: - -# 解法三 交换 - -参考[这里](https://leetcode.com/problems/permutations/discuss/18247/My-elegant-recursive-C++-solution-with-inline-explanation?orderBy=most_votes)。 - -这个想法就很 cool 了,之前第一个解法的递归,有点儿动态规划的意思,把 1 个数字的解,2 个数字的解,3 个数字的解,一环套一环的求了出来。 - -假设有一个函数,可以实现题目的要求,即产生 nums 的所有的组合,并且加入到 all 数组中。不过它多了一个参数,begin,即只指定从 nums [ begin ] 开始的数字,前边的数字固定不变。 - -```java -upset(int[] nums, int begin, List> all) -``` - -如果有这样的函数,那么一切就都简单了。 - -如果 begin 等于 nums 的长度,那么就表示 begin 前的数字都不变,也就是全部数字不变,我们只需要把它加到 all 中就行了。 - -```java -if (begin == nums.length) { - ArrayList temp = new ArrayList(); - for (int i = 0; i < nums.length; i++) { - temp.add(nums[i]); - } - all.add(new ArrayList(temp)); - return; -} -``` - -如果是其它的情况,我们其实只需要用一个 for 循环,把每一个数字都放到 begin 一次,然后再变化后边的数字就够了,也就是调用 upset 函数,从 begin + 1 开始的所有组合。 - -```java -for (int i = begin; i < nums.length; i++) { - swap(nums, i, begin); - upset(nums, begin + 1, all); - swap(nums, i, begin); -} -``` - -总体就是这样了。 - -```java -public List> permute(int[] nums) { - List> all = new ArrayList<>(); - //从下标 0 开始的所有组合 - upset(nums, 0, all); - return all; -} - -private void upset(int[] nums, int begin, List> all) { - if (begin == nums.length) { - ArrayList temp = new ArrayList(); - for (int i = 0; i < nums.length; i++) { - temp.add(nums[i]); - } - all.add(new ArrayList(temp)); - return; - } - for (int i = begin; i < nums.length; i++) { - swap(nums, i, begin); - upset(nums, begin + 1, all); - swap(nums, i, begin); - } - -} - -private void swap(int[] nums, int i, int begin) { - int temp = nums[i]; - nums[i] = nums[begin]; - nums[begin] = temp; -} -``` - -时间复杂度: - -空间复杂度: - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/46.jpg) + +描述的很简单,就是给定几个数,然后输出他们所有排列的可能。 + +# 解法一 插入 + +这是自己开始想到的一个方法,考虑的思路是,先考虑小问题怎么解决,然后再利用小问题去解决大问题。没错,就是递归的思路。比如说, + +如果只有 1 个数字 [ 1 ],那么很简单,直接返回 [ [ 1 ] ] 就 OK 了。 + +如果加了 1 个数字 2, [ 1 2 ] 该怎么办呢?我们只需要在上边的情况里,在 1 的空隙,也就是左边右边插入 2 就够了。变成 [ [ **2** 1 ], [ 1 **2** ] ]。 + +如果再加 1 个数字 3,[ 1 2 3 ] 该怎么办呢?同样的,我们只需要在上边的所有情况里的空隙里插入数字 3 就行啦。例如 [ 2 1 ] 在左边,中间,右边插入 3 ,变成 **3** 2 1,2 **3** 1,2 1 **3**。同理,1 2 在左边,中间,右边插入 3,变成 **3** 1 2,1 **3** 2,1 2 **3**,所以最后的结果就是 [ [ 3 2 1],[ 2 3 1],[ 2 1 3 ], [ 3 1 2 ],[ 1 3 2 ],[ 1 2 3 ] ]。 + +如果再加数字也是同样的道理,只需要在之前的情况里,数字的空隙插入新的数字就够了。 + +思路有了,直接看代码吧。 + +```java +public List> permute(int[] nums) { + return permute_end(nums,nums.length-1); +} +// end 表示当前新增的数字的位置 +private List> permute_end(int[] nums, int end) { + // 只有一个数字的时候 + if(end == 0){ + List> all = new ArrayList<>(); + List temp = new ArrayList<>(); + temp.add(nums[0]); + all.add(temp); + return all; + } + //得到上次所有的结果 + List> all_end = permute_end(nums,end-1); + int current_size = all_end.size(); + //遍历每一种情况 + for (int j = 0; j < current_size; j++) { + //在数字的缝隙插入新的数字 + for (int k = 0; k <= end; k++) { + List temp = new ArrayList<>(all_end.get(j)); + temp.add(k, nums[end]); + //添加到结果中 + all_end.add(temp); + }; + + } + //由于 all_end 此时既保存了之前的结果,和添加完的结果,所以把之前的结果要删除 + for (int j = 0; j < current_size; j++) { + all_end.remove(0); + } + return all_end; +} +``` + +既然有递归的过程,我们也可以直接改成迭代的,可以把递归开始不停压栈的过程省略了。 + +```java +public List> permute(int[] nums) { + List> all = new ArrayList<>(); + all.add(new ArrayList<>()); + //在上边的基础上只加上最外层的 for 循环就够了,代表每次新添加的数字 + for (int i = 0; i < nums.length; i++) { + int current_size = all.size(); + for (int j = 0; j < current_size; j++) { + for (int k = 0; k <= i; k++) { + List temp = new ArrayList<>(all.get(j)); + temp.add(k, nums[i]); + all.add(temp); + } + } + for (int j = 0; j < current_size; j++) { + all.remove(0); + } + } + return all; +} +``` + +时间复杂度,如果只分析代码的话挺复杂的。如果从最后的结果来说,应该是 n! 个结果,所以时间复杂度应该是 O(n!)。 + +空间复杂度:O(1)。 + +# 解法二 回溯 + +这个开始没想到,参考[这里](https://leetcode.com/problems/permutations/discuss/18239/A-general-approach-to-backtracking-questions-in-Java-(Subsets-Permutations-Combination-Sum-Palindrome-Partioning))。 + +其实也算是蛮典型的回溯,利用递归每次向 temp 里添加一个数字,数字添加够以后再回来进行回溯,再向后添加新的解。 + +可以理解成一层一层的添加,每一层都是一个 for 循环。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/46_2.jpg) + +每调用一层就进入一个 for 循环,相当于列出了所有解,然后挑选了我们需要的。其实本质上就是深度优先遍历 DFS。 + +```java +public List> permute(int[] nums) { + List> list = new ArrayList<>(); + backtrack(list, new ArrayList<>(), nums); + return list; +} + +private void backtrack(List> list, List tempList, int [] nums){ + if(tempList.size() == nums.length){ + list.add(new ArrayList<>(tempList)); + } else{ + for(int i = 0; i < nums.length; i++){ + if(tempList.contains(nums[i])) continue; // 已经存在的元素,跳过 + tempList.add(nums[i]); //将当前元素加入 + backtrack(list, tempList, nums); //向后继续添加 + tempList.remove(tempList.size() - 1); //将 tempList 刚添加的元素,去掉,尝试新的元素 + } + } +} +``` + +时间复杂度: + +空间复杂度: + +# 解法三 交换 + +参考[这里](https://leetcode.com/problems/permutations/discuss/18247/My-elegant-recursive-C++-solution-with-inline-explanation?orderBy=most_votes)。 + +这个想法就很 cool 了,之前第一个解法的递归,有点儿动态规划的意思,把 1 个数字的解,2 个数字的解,3 个数字的解,一环套一环的求了出来。 + +假设有一个函数,可以实现题目的要求,即产生 nums 的所有的组合,并且加入到 all 数组中。不过它多了一个参数,begin,即只指定从 nums [ begin ] 开始的数字,前边的数字固定不变。 + +```java +upset(int[] nums, int begin, List> all) +``` + +如果有这样的函数,那么一切就都简单了。 + +如果 begin 等于 nums 的长度,那么就表示 begin 前的数字都不变,也就是全部数字不变,我们只需要把它加到 all 中就行了。 + +```java +if (begin == nums.length) { + ArrayList temp = new ArrayList(); + for (int i = 0; i < nums.length; i++) { + temp.add(nums[i]); + } + all.add(new ArrayList(temp)); + return; +} +``` + +如果是其它的情况,我们其实只需要用一个 for 循环,把每一个数字都放到 begin 一次,然后再变化后边的数字就够了,也就是调用 upset 函数,从 begin + 1 开始的所有组合。 + +```java +for (int i = begin; i < nums.length; i++) { + swap(nums, i, begin); + upset(nums, begin + 1, all); + swap(nums, i, begin); +} +``` + +总体就是这样了。 + +```java +public List> permute(int[] nums) { + List> all = new ArrayList<>(); + //从下标 0 开始的所有组合 + upset(nums, 0, all); + return all; +} + +private void upset(int[] nums, int begin, List> all) { + if (begin == nums.length) { + ArrayList temp = new ArrayList(); + for (int i = 0; i < nums.length; i++) { + temp.add(nums[i]); + } + all.add(new ArrayList(temp)); + return; + } + for (int i = begin; i < nums.length; i++) { + swap(nums, i, begin); + upset(nums, begin + 1, all); + swap(nums, i, begin); + } + +} + +private void swap(int[] nums, int i, int begin) { + int temp = nums[i]; + nums[i] = nums[begin]; + nums[begin] = temp; +} +``` + +时间复杂度: + +空间复杂度: + +# 总 + 这道题很经典了,用动态规划,回溯,递归各实现了一遍,当然解法一强行递归了一下,和解法三相比真是相形见绌,解法三才是原汁原味的递归,简洁优雅。 \ No newline at end of file diff --git a/leetCode-47-Permutations-II.md b/leetCode-47-Permutations-II.md index a8dff1b22..51521fe9e 100644 --- a/leetCode-47-Permutations-II.md +++ b/leetCode-47-Permutations-II.md @@ -1,227 +1,227 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/47.jpg) - -和[上一道题](https://leetcode.windliang.cc/leetCode-46-Permutations.html)类似,不同之处就是给定的数字中会有重复的,这样的话用之前的算法会产出重复的序列。例如,[ 1 1 ],用之前的算法,产生的结果肯定是 [ \[ 1 1 \], \[ 1 1 \] ],也就是产生了重复的序列。但我们可以在上一题的解法中进行修改从而解决这道题。 - -# 解法一 插入 - -这个没想到怎么在原基础上改,可以直接了当些,在它产生的结果里,对结果去重再返回。对于去重的话,一般的方法肯定就是写两个 for 循环,然后一个一个互相比较,然后找到重复的去掉。这里,我们用 [39题](https://leetcode.windliang.cc/leetCode-39-Combination-Sum.html?h=remove) 解法二中提到的一种去重的方法。 - -```java -public List> permuteUnique(int[] nums) { - List> all = new ArrayList<>(); - List temp = new ArrayList<>(); - temp.add(nums[0]); - all.add(temp); - for (int i = 1; i < nums.length; i++) { - int current_size = all.size(); - for (int j = 0; j < current_size; j++) { - List last = all.get(j); - for (int k = 0; k <= i; k++) { - if (k < i && nums[i] == last.get(k)) { - continue; - } - temp = new ArrayList<>(last); - temp.add(k, nums[i]); - all.add(temp); - } - } - for (int j = 0; j < current_size; j++) { - all.remove(0); - } - } - return removeDuplicate(all); -} - -private List> removeDuplicate(List> list) { - Map ans = new HashMap(); - for (int i = 0; i < list.size(); i++) { - List l = list.get(i); - String key = ""; - // [ 2 3 4 ] 转为 "2,3,4" - for (int j = 0; j < l.size() - 1; j++) { - key = key + l.get(j) + ","; - } - key = key + l.get(l.size() - 1); - ans.put(key, ""); - } - // 根据逗号还原 List - List> ans_list = new ArrayList>(); - for (String k : ans.keySet()) { - String[] l = k.split(","); - List temp = new ArrayList(); - for (int i = 0; i < l.length; i++) { - int c = Integer.parseInt(l[i]); - temp.add(c); - } - ans_list.add(temp); - } - return ans_list; -} -``` - -# 解法二 回溯 - -看下之前的算法 - -```java -public List> permute(int[] nums) { - List> list = new ArrayList<>(); - backtrack(list, new ArrayList<>(), nums); - return list; -} - -private void backtrack(List> list, List tempList, int [] nums){ - if(tempList.size() == nums.length){ - list.add(new ArrayList<>(tempList)); - } else{ - for(int i = 0; i < nums.length; i++){ - if(tempList.contains(nums[i])) continue; // 已经存在的元素,跳过 - tempList.add(nums[i]); //将当前元素加入 - backtrack(list, tempList, nums); //向后继续添加 - tempList.remove(tempList.size() - 1); //将 tempList 刚添加的元素,去掉,尝试新的元素 - } - } -} -``` - -假如给定的数组是 [ 1 1 3 ],我们来看一下遍历的这个图。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/47_2.jpg) - -第一个要解决的就是这句代码 - -```java -if(tempList.contains(nums[i])) continue; // 已经存在的元素,跳过 -``` - -之前没有重复的元素,所以可以直接在 templist 判断有没有当前元素,有的话就跳过。但这里的话,因为给定的有重复的元素,这个方法明显不可以了。 - -换个思路,我们可以再用一个 list 保存当前 templist 中已经有的元素的下标,然后添加新元素的时候去判断下标就可以了。 - -第二个问题就是,可以看到有重复元素的时候,上边第 1 个图和第 2 个图产生的是完全一样的序列。所以第 2 个遍历是没有必要的。 - -解决的方案就是把数组首先排下顺序,然后判断一下上一个添加的元素和当前元素是不是相等,相等的话就跳过,继续下一个元素。 - -```java -public List> permuteUnique(int[] nums) { - List> list = new ArrayList<>(); - Arrays.sort(nums); - List old = new ArrayList<>(); - backtrack(list, new ArrayList<>(), nums, old); - return list; -} - -private void backtrack(List> list, List tempList, int[] nums, List old) { - if (tempList.size() == nums.length) { - list.add(new ArrayList<>(tempList)); - } else { - for (int i = 0; i < nums.length; i++) { - //解决第一个问题 - if (old.contains(i)) { - continue; - } - //解决第二个问题 !old.contains(i - 1) 很关键,下边解释下 - if (i > 0 && !old.contains(i - 1) && nums[i - 1] == nums[i]) { - continue; - } - old.add(i);//添加下标 - tempList.add(nums[i]); // 将当前元素加入 - backtrack(list, tempList, nums, old); // 向后继续添加 - old.remove(old.size() - 1); - tempList.remove(tempList.size() - 1); - } - } -} -``` - -解决第二个问题 !old.contains(i - 1) 很关键 -因为上边 old.contains(i) 代码会使得一些元素跳过没有加到 templist 上,所以我们要判断 nums[ i - 1 ] 是不是被跳过的那个元素,如果 old.contains ( i ) 返回 true , 即使 nums [ i - 1 ] == nums [ i ] 也不能跳过当前元素。因为上一个元素 nums [ i - 1 ] 并没有被添加到 templist。可能比较绕,但是可以参照上边的图,走一下流程就懂了。如果不加 !old.contains ( i - 1 ),那么图中的第 2 行的第 2 个 1 本来应该加到 tempList,但是会被跳过。因为第 2 行第 1 个元素也是 1。 - -对于解决第一个问题,我们用了一个 list 来保存下标来解决。需要一个 O ( n ) 的空间。有一种方法,我们可以用 O(1)的空间。不过前提是,我们需要对问题的样例了解,也就是给定的输入所包含的数字。我们需要找到一个样例中一定不包含的数字来解决我们的问题。 - -首先,我们假设输入的所有的数字中没有 -100 这个数字。 - -然后,我们就可以递归前将当前数字先保存起来,然后置为 -100 隐藏起来,递归结束后还原即可。 - -```java -public List> permuteUnique(int[] nums) { - List> list = new ArrayList<>(); - Arrays.sort(nums); - backtrack(list, new ArrayList<>(), nums); - return list; -} - -private void backtrack(List> list, List tempList, int[] nums) { - if (tempList.size() == nums.length) { - list.add(new ArrayList<>(tempList)); - } else { - for (int i = 0; i < nums.length; i++) { - //解决第一个问题 - if (nums[i] == -100) { - continue; - } - //解决第二个问题 !old.contains(i - 1) 很关键 - if (i > 0 && nums[i-1] != -100 && nums[i - 1] == nums[i]) { - continue; - } - tempList.add(nums[i]); // 将当前元素加入 - int temp = nums[i]; //保存 - nums[i] = -100; // 隐藏 - backtrack(list, tempList, nums); // 向后继续添加 - nums[i] = temp; //还原 - tempList.remove(tempList.size() - 1); - } - } -} -``` - - - -当然这个想法局限性很大,但是如果对解决的问题很熟悉,一般是可以找到这样一个不会输入的数字,然后可以优化空间复杂度。 - -# 解法三 交换 - -这个改起来相对容易些,之前的想法就是在每一个位置,让每个数字轮流交换过去一下。这里的话,我们其实只要把当前位置已经有哪些数字来过保存起来,如果有重复的话,我们不让他交换,直接换下一个数字就可以了。 - -```java -public List> permuteUnique(int[] nums) { - List> all = new ArrayList<>(); - Arrays.sort(nums); - upset(nums, 0, all); - return all; -} - -private void upset(int[] nums, int begin, List> all) { - if (begin == nums.length) { - ArrayList temp = new ArrayList(); - for (int i = 0; i < nums.length; i++) { - temp.add(nums[i]); - } - all.add(new ArrayList(temp)); - return; - } - HashSet set = new HashSet<>(); //保存当前要交换的位置已经有过哪些数字了 - for (int i = begin; i < nums.length; i++) { - if (set.contains(nums[i])) { //如果存在了就跳过,不去交换 - continue; - } - set.add(nums[i]); - swap(nums, i, begin); - upset(nums, begin + 1, all); - swap(nums, i, begin); - } - -} - -private void swap(int[] nums, int i, int begin) { - int temp = nums[i]; - nums[i] = nums[begin]; - nums[begin] = temp; -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/47.jpg) + +和[上一道题](https://leetcode.windliang.cc/leetCode-46-Permutations.html)类似,不同之处就是给定的数字中会有重复的,这样的话用之前的算法会产出重复的序列。例如,[ 1 1 ],用之前的算法,产生的结果肯定是 [ \[ 1 1 \], \[ 1 1 \] ],也就是产生了重复的序列。但我们可以在上一题的解法中进行修改从而解决这道题。 + +# 解法一 插入 + +这个没想到怎么在原基础上改,可以直接了当些,在它产生的结果里,对结果去重再返回。对于去重的话,一般的方法肯定就是写两个 for 循环,然后一个一个互相比较,然后找到重复的去掉。这里,我们用 [39题](https://leetcode.windliang.cc/leetCode-39-Combination-Sum.html?h=remove) 解法二中提到的一种去重的方法。 + +```java +public List> permuteUnique(int[] nums) { + List> all = new ArrayList<>(); + List temp = new ArrayList<>(); + temp.add(nums[0]); + all.add(temp); + for (int i = 1; i < nums.length; i++) { + int current_size = all.size(); + for (int j = 0; j < current_size; j++) { + List last = all.get(j); + for (int k = 0; k <= i; k++) { + if (k < i && nums[i] == last.get(k)) { + continue; + } + temp = new ArrayList<>(last); + temp.add(k, nums[i]); + all.add(temp); + } + } + for (int j = 0; j < current_size; j++) { + all.remove(0); + } + } + return removeDuplicate(all); +} + +private List> removeDuplicate(List> list) { + Map ans = new HashMap(); + for (int i = 0; i < list.size(); i++) { + List l = list.get(i); + String key = ""; + // [ 2 3 4 ] 转为 "2,3,4" + for (int j = 0; j < l.size() - 1; j++) { + key = key + l.get(j) + ","; + } + key = key + l.get(l.size() - 1); + ans.put(key, ""); + } + // 根据逗号还原 List + List> ans_list = new ArrayList>(); + for (String k : ans.keySet()) { + String[] l = k.split(","); + List temp = new ArrayList(); + for (int i = 0; i < l.length; i++) { + int c = Integer.parseInt(l[i]); + temp.add(c); + } + ans_list.add(temp); + } + return ans_list; +} +``` + +# 解法二 回溯 + +看下之前的算法 + +```java +public List> permute(int[] nums) { + List> list = new ArrayList<>(); + backtrack(list, new ArrayList<>(), nums); + return list; +} + +private void backtrack(List> list, List tempList, int [] nums){ + if(tempList.size() == nums.length){ + list.add(new ArrayList<>(tempList)); + } else{ + for(int i = 0; i < nums.length; i++){ + if(tempList.contains(nums[i])) continue; // 已经存在的元素,跳过 + tempList.add(nums[i]); //将当前元素加入 + backtrack(list, tempList, nums); //向后继续添加 + tempList.remove(tempList.size() - 1); //将 tempList 刚添加的元素,去掉,尝试新的元素 + } + } +} +``` + +假如给定的数组是 [ 1 1 3 ],我们来看一下遍历的这个图。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/47_2.jpg) + +第一个要解决的就是这句代码 + +```java +if(tempList.contains(nums[i])) continue; // 已经存在的元素,跳过 +``` + +之前没有重复的元素,所以可以直接在 templist 判断有没有当前元素,有的话就跳过。但这里的话,因为给定的有重复的元素,这个方法明显不可以了。 + +换个思路,我们可以再用一个 list 保存当前 templist 中已经有的元素的下标,然后添加新元素的时候去判断下标就可以了。 + +第二个问题就是,可以看到有重复元素的时候,上边第 1 个图和第 2 个图产生的是完全一样的序列。所以第 2 个遍历是没有必要的。 + +解决的方案就是把数组首先排下顺序,然后判断一下上一个添加的元素和当前元素是不是相等,相等的话就跳过,继续下一个元素。 + +```java +public List> permuteUnique(int[] nums) { + List> list = new ArrayList<>(); + Arrays.sort(nums); + List old = new ArrayList<>(); + backtrack(list, new ArrayList<>(), nums, old); + return list; +} + +private void backtrack(List> list, List tempList, int[] nums, List old) { + if (tempList.size() == nums.length) { + list.add(new ArrayList<>(tempList)); + } else { + for (int i = 0; i < nums.length; i++) { + //解决第一个问题 + if (old.contains(i)) { + continue; + } + //解决第二个问题 !old.contains(i - 1) 很关键,下边解释下 + if (i > 0 && !old.contains(i - 1) && nums[i - 1] == nums[i]) { + continue; + } + old.add(i);//添加下标 + tempList.add(nums[i]); // 将当前元素加入 + backtrack(list, tempList, nums, old); // 向后继续添加 + old.remove(old.size() - 1); + tempList.remove(tempList.size() - 1); + } + } +} +``` + +解决第二个问题 !old.contains(i - 1) 很关键 +因为上边 old.contains(i) 代码会使得一些元素跳过没有加到 templist 上,所以我们要判断 nums[ i - 1 ] 是不是被跳过的那个元素,如果 old.contains ( i ) 返回 true , 即使 nums [ i - 1 ] == nums [ i ] 也不能跳过当前元素。因为上一个元素 nums [ i - 1 ] 并没有被添加到 templist。可能比较绕,但是可以参照上边的图,走一下流程就懂了。如果不加 !old.contains ( i - 1 ),那么图中的第 2 行的第 2 个 1 本来应该加到 tempList,但是会被跳过。因为第 2 行第 1 个元素也是 1。 + +对于解决第一个问题,我们用了一个 list 来保存下标来解决。需要一个 O ( n ) 的空间。有一种方法,我们可以用 O(1)的空间。不过前提是,我们需要对问题的样例了解,也就是给定的输入所包含的数字。我们需要找到一个样例中一定不包含的数字来解决我们的问题。 + +首先,我们假设输入的所有的数字中没有 -100 这个数字。 + +然后,我们就可以递归前将当前数字先保存起来,然后置为 -100 隐藏起来,递归结束后还原即可。 + +```java +public List> permuteUnique(int[] nums) { + List> list = new ArrayList<>(); + Arrays.sort(nums); + backtrack(list, new ArrayList<>(), nums); + return list; +} + +private void backtrack(List> list, List tempList, int[] nums) { + if (tempList.size() == nums.length) { + list.add(new ArrayList<>(tempList)); + } else { + for (int i = 0; i < nums.length; i++) { + //解决第一个问题 + if (nums[i] == -100) { + continue; + } + //解决第二个问题 !old.contains(i - 1) 很关键 + if (i > 0 && nums[i-1] != -100 && nums[i - 1] == nums[i]) { + continue; + } + tempList.add(nums[i]); // 将当前元素加入 + int temp = nums[i]; //保存 + nums[i] = -100; // 隐藏 + backtrack(list, tempList, nums); // 向后继续添加 + nums[i] = temp; //还原 + tempList.remove(tempList.size() - 1); + } + } +} +``` + + + +当然这个想法局限性很大,但是如果对解决的问题很熟悉,一般是可以找到这样一个不会输入的数字,然后可以优化空间复杂度。 + +# 解法三 交换 + +这个改起来相对容易些,之前的想法就是在每一个位置,让每个数字轮流交换过去一下。这里的话,我们其实只要把当前位置已经有哪些数字来过保存起来,如果有重复的话,我们不让他交换,直接换下一个数字就可以了。 + +```java +public List> permuteUnique(int[] nums) { + List> all = new ArrayList<>(); + Arrays.sort(nums); + upset(nums, 0, all); + return all; +} + +private void upset(int[] nums, int begin, List> all) { + if (begin == nums.length) { + ArrayList temp = new ArrayList(); + for (int i = 0; i < nums.length; i++) { + temp.add(nums[i]); + } + all.add(new ArrayList(temp)); + return; + } + HashSet set = new HashSet<>(); //保存当前要交换的位置已经有过哪些数字了 + for (int i = begin; i < nums.length; i++) { + if (set.contains(nums[i])) { //如果存在了就跳过,不去交换 + continue; + } + set.add(nums[i]); + swap(nums, i, begin); + upset(nums, begin + 1, all); + swap(nums, i, begin); + } + +} + +private void swap(int[] nums, int i, int begin) { + int temp = nums[i]; + nums[i] = nums[begin]; + nums[begin] = temp; +} +``` + +# 总 + 基本上都是在上道题的基础上改出来了,一些技巧也是经常遇到,比如先排序,然后判断和前一个是否重复。利用 Hash 去重的功能。利用原来的存储空间隐藏掉数据,然后再想办法还原。 \ No newline at end of file diff --git a/leetCode-48-Rotate-Image.md b/leetCode-48-Rotate-Image.md index fa4c87663..7ae554864 100644 --- a/leetCode-48-Rotate-Image.md +++ b/leetCode-48-Rotate-Image.md @@ -1,74 +1,74 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/48.jpg) - -将一个矩阵顺时针旋转 90 度,并且不使用额外的空间。大概属于找规律的题,没有什么一般的思路,观察就可以了。 - -# 解法一 - -可以先转置,然后把每列对称交换交换一下。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/48_2.jpg) - -```java -public void rotate(int[][] matrix) { - //以对角线为轴交换 - for (int i = 0; i < matrix.length; i++) { - for (int j = 0; j <=i; j++) { - if (i == j) { - continue; - } - int temp = matrix[i][j]; - matrix[i][j] = matrix[j][i]; - matrix[j][i] = temp; - } - } - //交换列 - for (int i = 0, j = matrix.length - 1; i < matrix.length / 2; i++, j--) { - for (int k = 0; k < matrix.length; k++) { - int temp = matrix[k][i]; - matrix[k][i] = matrix[k][j]; - matrix[k][j] = temp; - } - } - -} -``` - -时间复杂度:O(n²)。 - -空间复杂度:O(1)。 - -也可以先以横向的中轴线为轴,对称的行进行交换,然后再以对角线交换。 - -# 解法二 - -我把这个[链接](https://leetcode.com/problems/rotate-image/discuss/18895/Clear-Java-solution)的思路贴过来,里边评论有张图也都顺道贴过来吧,写的很好。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/48_3.jpg) - -一圈一圈的循环交换,很妙! - - - -```java -public void rotate(int[][] matrix) { - int n=matrix.length; - for (int i=0; i> groupAnagrams(String[] strs) { - List> ans = new ArrayList<>(); - boolean[] used = new boolean[strs.length]; - for (int i = 0; i < strs.length; i++) { - List temp = null; - if (!used[i]) { - temp = new ArrayList(); - temp.add(strs[i]); - //两两比较判断字符串是否符合 - for (int j = i + 1; j < strs.length; j++) { - if (equals(strs[i], strs[j])) { - used[j] = true; - temp.add(strs[j]); - } - } - } - if (temp != null) { - ans.add(temp); - - } - } - return ans; - -} - -private boolean equals(String string1, String string2) { - Map hash = new HashMap<>(); - //记录第一个字符串每个字符出现的次数,进行累加 - for (int i = 0; i < string1.length(); i++) { - if (hash.containsKey(string1.charAt(i))) { - hash.put(string1.charAt(i), hash.get(string1.charAt(i)) + 1); - } else { - hash.put(string1.charAt(i), 1); - } - } - //记录第一个字符串每个字符出现的次数,将之前的每次减 1 - for (int i = 0; i < string2.length(); i++) { - if (hash.containsKey(string2.charAt(i))) { - hash.put(string2.charAt(i), hash.get(string2.charAt(i)) - 1); - } else { - return false; - } - } - //判断每个字符的次数是不是 0 ,不是的话直接返回 false - Set set = hash.keySet(); - for (char c : set) { - if (hash.get(c) != 0) { - return false; - } - } - return true; -} -``` - -时间复杂度:两层 for 循环,再加上比较字符串,如果字符串最长为 K,总的时间复杂度就是 O(n²K)。 - -空间复杂度:O(NK),用来存储结果。 - -解法一算是比较通用的解法,不管字符串里边是大写字母,小写字母,数字,都可以用这个算法解决。这道题的话,题目告诉我们字符串中只有小写字母,针对这个限制,我们可以再用一些针对性强的算法。 - -下边的算法本质是,我们只要把一类的字符串用某一种方法唯一的映射到同一个位置就可以。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/49_2.jpg) - -# 解法二 - -参考官方给的[解法](https://leetcode.com/problems/group-anagrams/solution/)。 - -我们将每个字符串按照字母顺序排序,这样的话就可以把 eat,tea,ate 都映射到 aet。其他的类似。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/49_3.jpg) - -```java -public List> groupAnagrams(String[] strs) { - HashMap> hash = new HashMap<>(); - for (int i = 0; i < strs.length; i++) { - char[] s_arr = strs[i].toCharArray(); - //排序 - Arrays.sort(s_arr); - //映射到 key - String key = String.valueOf(s_arr); - //添加到对应的类中 - if (hash.containsKey(key)) { - hash.get(key).add(strs[i]); - } else { - List temp = new ArrayList(); - temp.add(strs[i]); - hash.put(key, temp); - } - - } - return new ArrayList>(hash.values()); -} -``` - -时间复杂度:排序的话算作 O(K log(K)),最外层的 for 循环,所以就是 O(n K log(K))。 - -空间复杂度:O(NK),用来存储结果。 - -# 解法三 - -参考[这里](https://leetcode.com/problems/group-anagrams/discuss/19183/Java-beat-100!!!-use-prime-number),利用[算术基本定理](https://zh.wikipedia.org/wiki/%E7%AE%97%E6%9C%AF%E5%9F%BA%E6%9C%AC%E5%AE%9A%E7%90%86)。 - -> **算术基本定理**,又称为**正整数的唯一分解定理**,即:每个大于1的[自然数](https://zh.wikipedia.org/wiki/%E8%87%AA%E7%84%B6%E6%95%B0),要么本身就是[质数](https://zh.wikipedia.org/wiki/%E8%B4%A8%E6%95%B0),要么可以写为2个以上的质数的[积](https://zh.wikipedia.org/wiki/%E7%A7%AF),而且这些质因子按大小排列之后,写法仅有一种方式。 - -利用这个,我们把每个字符串都映射到一个正数上。 - -用一个数组存储质数 prime = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103}。 - -然后每个字符串的字符减去 ' a ' ,然后取到 prime 中对应的质数。把它们累乘。 - -例如 abc ,就对应 'a' - 'a', 'b' - 'a', 'c' - 'a',即 0, 1, 2,也就是对应素数 2 3 5,然后相乘 2 * 3 * 5 = 30,就把 "abc" 映射到了 30。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/49_4.jpg) - -```java -public List> groupAnagrams(String[] strs) { - HashMap> hash = new HashMap<>(); - //每个字母对应一个质数 - int[] prime = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103 }; - for (int i = 0; i < strs.length; i++) { - int key = 1; - //累乘得到 key - for (int j = 0; j < strs[i].length(); j++) { - key *= prime[strs[i].charAt(j) - 'a']; - } - if (hash.containsKey(key)) { - hash.get(key).add(strs[i]); - } else { - List temp = new ArrayList(); - temp.add(strs[i]); - hash.put(key, temp); - } - - } - return new ArrayList>(hash.values()); -} - -``` - -时间复杂度:O(n * K),K 是字符串的最长长度。 - -空间复杂度:O(NK),用来存储结果。 - -这个解法时间复杂度,较解法二有提升,但是有一定的局限性,因为求 key 的时候用的是累乘,可能会造成溢出,超出 int 所能表示的数字。 - -# 解法四 - -参考[这里](https://leetcode.com/problems/group-anagrams/solution/),记录字符串的每个字符出现的次数从而完成映射。因为有 26 个字母,不好解释,我们假设只有 5 个字母,来看一下怎么完成映射。 - -首先初始化 key = "0#0#0#0#0#",数字分别代表 abcde 出现的次数,# 用来分割。 - -这样的话,"abb" 就映射到了 "1#2#0#0#0"。 - -"cdc" 就映射到了 "0#0#2#1#0"。 - -"dcc" 就映射到了 "0#0#2#1#0"。 - -```java -public List> groupAnagrams(String[] strs) { - HashMap> hash = new HashMap<>(); - for (int i = 0; i < strs.length; i++) { - int[] num = new int[26]; - //记录每个字符的次数 - for (int j = 0; j < strs[i].length(); j++) { - num[strs[i].charAt(j) - 'a']++; - } - //转成 0#2#2# 类似的形式 - String key = ""; - for (int j = 0; j < num.length; j++) { - key = key + num[j] + '#'; - } - if (hash.containsKey(key)) { - hash.get(key).add(strs[i]); - } else { - List temp = new ArrayList(); - temp.add(strs[i]); - hash.put(key, temp); - } - - } - return new ArrayList>(hash.values()); -} -``` - -时间复杂度: O(nK)。 - -空间复杂度:O(NK),用来存储结果。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/49.jpg) + +给定多个字符串,然后把它们分类。只要字符串所包含的字符完全一样就算作一类,不考虑顺序。 + +# 解法一 + +最通用的一种解法,对于每个字符串,比较它们的每个字符出现的个数是否相等,相等的话就把它们放在一个 list 中去,作为一个类别。最外层写一个 for 循环然后一一比较就可以,还可以用一个等大的布尔型数组来记录当前字符串是否已经加入的了 list 。比较两个字符串的字符出现的次数可以用一个 HashMap,具体看代码吧。 + +``` java +public List> groupAnagrams(String[] strs) { + List> ans = new ArrayList<>(); + boolean[] used = new boolean[strs.length]; + for (int i = 0; i < strs.length; i++) { + List temp = null; + if (!used[i]) { + temp = new ArrayList(); + temp.add(strs[i]); + //两两比较判断字符串是否符合 + for (int j = i + 1; j < strs.length; j++) { + if (equals(strs[i], strs[j])) { + used[j] = true; + temp.add(strs[j]); + } + } + } + if (temp != null) { + ans.add(temp); + + } + } + return ans; + +} + +private boolean equals(String string1, String string2) { + Map hash = new HashMap<>(); + //记录第一个字符串每个字符出现的次数,进行累加 + for (int i = 0; i < string1.length(); i++) { + if (hash.containsKey(string1.charAt(i))) { + hash.put(string1.charAt(i), hash.get(string1.charAt(i)) + 1); + } else { + hash.put(string1.charAt(i), 1); + } + } + //记录第一个字符串每个字符出现的次数,将之前的每次减 1 + for (int i = 0; i < string2.length(); i++) { + if (hash.containsKey(string2.charAt(i))) { + hash.put(string2.charAt(i), hash.get(string2.charAt(i)) - 1); + } else { + return false; + } + } + //判断每个字符的次数是不是 0 ,不是的话直接返回 false + Set set = hash.keySet(); + for (char c : set) { + if (hash.get(c) != 0) { + return false; + } + } + return true; +} +``` + +时间复杂度:两层 for 循环,再加上比较字符串,如果字符串最长为 K,总的时间复杂度就是 O(n²K)。 + +空间复杂度:O(NK),用来存储结果。 + +解法一算是比较通用的解法,不管字符串里边是大写字母,小写字母,数字,都可以用这个算法解决。这道题的话,题目告诉我们字符串中只有小写字母,针对这个限制,我们可以再用一些针对性强的算法。 + +下边的算法本质是,我们只要把一类的字符串用某一种方法唯一的映射到同一个位置就可以。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/49_2.jpg) + +# 解法二 + +参考官方给的[解法](https://leetcode.com/problems/group-anagrams/solution/)。 + +我们将每个字符串按照字母顺序排序,这样的话就可以把 eat,tea,ate 都映射到 aet。其他的类似。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/49_3.jpg) + +```java +public List> groupAnagrams(String[] strs) { + HashMap> hash = new HashMap<>(); + for (int i = 0; i < strs.length; i++) { + char[] s_arr = strs[i].toCharArray(); + //排序 + Arrays.sort(s_arr); + //映射到 key + String key = String.valueOf(s_arr); + //添加到对应的类中 + if (hash.containsKey(key)) { + hash.get(key).add(strs[i]); + } else { + List temp = new ArrayList(); + temp.add(strs[i]); + hash.put(key, temp); + } + + } + return new ArrayList>(hash.values()); +} +``` + +时间复杂度:排序的话算作 O(K log(K)),最外层的 for 循环,所以就是 O(n K log(K))。 + +空间复杂度:O(NK),用来存储结果。 + +# 解法三 + +参考[这里](https://leetcode.com/problems/group-anagrams/discuss/19183/Java-beat-100!!!-use-prime-number),利用[算术基本定理](https://zh.wikipedia.org/wiki/%E7%AE%97%E6%9C%AF%E5%9F%BA%E6%9C%AC%E5%AE%9A%E7%90%86)。 + +> **算术基本定理**,又称为**正整数的唯一分解定理**,即:每个大于1的[自然数](https://zh.wikipedia.org/wiki/%E8%87%AA%E7%84%B6%E6%95%B0),要么本身就是[质数](https://zh.wikipedia.org/wiki/%E8%B4%A8%E6%95%B0),要么可以写为2个以上的质数的[积](https://zh.wikipedia.org/wiki/%E7%A7%AF),而且这些质因子按大小排列之后,写法仅有一种方式。 + +利用这个,我们把每个字符串都映射到一个正数上。 + +用一个数组存储质数 prime = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103}。 + +然后每个字符串的字符减去 ' a ' ,然后取到 prime 中对应的质数。把它们累乘。 + +例如 abc ,就对应 'a' - 'a', 'b' - 'a', 'c' - 'a',即 0, 1, 2,也就是对应素数 2 3 5,然后相乘 2 * 3 * 5 = 30,就把 "abc" 映射到了 30。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/49_4.jpg) + +```java +public List> groupAnagrams(String[] strs) { + HashMap> hash = new HashMap<>(); + //每个字母对应一个质数 + int[] prime = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103 }; + for (int i = 0; i < strs.length; i++) { + int key = 1; + //累乘得到 key + for (int j = 0; j < strs[i].length(); j++) { + key *= prime[strs[i].charAt(j) - 'a']; + } + if (hash.containsKey(key)) { + hash.get(key).add(strs[i]); + } else { + List temp = new ArrayList(); + temp.add(strs[i]); + hash.put(key, temp); + } + + } + return new ArrayList>(hash.values()); +} + +``` + +时间复杂度:O(n * K),K 是字符串的最长长度。 + +空间复杂度:O(NK),用来存储结果。 + +这个解法时间复杂度,较解法二有提升,但是有一定的局限性,因为求 key 的时候用的是累乘,可能会造成溢出,超出 int 所能表示的数字。 + +# 解法四 + +参考[这里](https://leetcode.com/problems/group-anagrams/solution/),记录字符串的每个字符出现的次数从而完成映射。因为有 26 个字母,不好解释,我们假设只有 5 个字母,来看一下怎么完成映射。 + +首先初始化 key = "0#0#0#0#0#",数字分别代表 abcde 出现的次数,# 用来分割。 + +这样的话,"abb" 就映射到了 "1#2#0#0#0"。 + +"cdc" 就映射到了 "0#0#2#1#0"。 + +"dcc" 就映射到了 "0#0#2#1#0"。 + +```java +public List> groupAnagrams(String[] strs) { + HashMap> hash = new HashMap<>(); + for (int i = 0; i < strs.length; i++) { + int[] num = new int[26]; + //记录每个字符的次数 + for (int j = 0; j < strs[i].length(); j++) { + num[strs[i].charAt(j) - 'a']++; + } + //转成 0#2#2# 类似的形式 + String key = ""; + for (int j = 0; j < num.length; j++) { + key = key + num[j] + '#'; + } + if (hash.containsKey(key)) { + hash.get(key).add(strs[i]); + } else { + List temp = new ArrayList(); + temp.add(strs[i]); + hash.put(key, temp); + } + + } + return new ArrayList>(hash.values()); +} +``` + +时间复杂度: O(nK)。 + +空间复杂度:O(NK),用来存储结果。 + +# 总 + 利用 HashMap 去记录字符的次数之前也有遇到过,很常用。解法三中利用质数相乘,是真的太强了。 \ No newline at end of file diff --git a/leetCode-5-Longest-Palindromic-Substring.md b/leetCode-5-Longest-Palindromic-Substring.md index 2e0acb8d6..6dcf4021f 100644 --- a/leetCode-5-Longest-Palindromic-Substring.md +++ b/leetCode-5-Longest-Palindromic-Substring.md @@ -1,475 +1,475 @@ -## 题目描述(中等难度) - -![](http://windliang.oss-cn-beijing.aliyuncs.com/5_palindromic.jpg) - -给定一个字符串,输出最长的回文子串。回文串指的是正的读和反的读是一样的字符串,例如 "aba","ccbbcc"。 - -## 解法一 暴力破解 - -暴力求解,列举所有的子串,判断是否为回文串,保存最长的回文串。 - -```java -public boolean isPalindromic(String s) { - int len = s.length(); - for (int i = 0; i < len / 2; i++) { - if (s.charAt(i) != s.charAt(len - i - 1)) { - return false; - } - } - return true; - } - -// 暴力解法 -public String longestPalindrome(String s) { - String ans = ""; - int max = 0; - int len = s.length(); - for (int i = 0; i < len; i++) - for (int j = i + 1; j <= len; j++) { - String test = s.substring(i, j); - if (isPalindromic(test) && test.length() > max) { - ans = s.substring(i, j); - max = Math.max(max, ans.length()); - } - } - return ans; -} -``` - -时间复杂度:两层 for 循环 O(n²),for 循环里边判断是否为回文,O(n),所以时间复杂度为 O(n³)。 - -空间复杂度:O(1),常数个变量。 - -## 解法二 最长公共子串 - -根据回文串的定义,正着和反着读一样,那我们是不是把原来的字符串倒置了,然后找最长的公共子串就可以了。例如,S = " caba",S' = " abac",最长公共子串是 "aba",所以原字符串的最长回文串就是 "aba"。 - -关于求最长公共子串(不是公共子序列),有很多方法,这里用动态规划的方法,可以先阅读下边的链接。 - -https://blog.csdn.net/u010397369/article/details/38979077 - -https://www.kancloud.cn/digest/pieces-algorithm/163624 - -整体思想就是,申请一个二维的数组初始化为 0,然后判断对应的字符是否相等,相等的话 - -arr [ i ]\[ j ] = arr [ i - 1 ]\[ j - 1] + 1 。 - -当 i = 0 或者 j = 0 的时候单独分析,字符相等的话 arr [ i ]\[ j ] 就赋为 1 。 - -arr [ i ]\[ j ] 保存的就是公共子串的长度。 - -```java -public String longestPalindrome(String s) { - if (s.equals("")) - return ""; - String origin = s; - String reverse = new StringBuffer(s).reverse().toString(); //字符串倒置 - int length = s.length(); - int[][] arr = new int[length][length]; - int maxLen = 0; - int maxEnd = 0; - for (int i = 0; i < length; i++) - for (int j = 0; j < length; j++) { - if (origin.charAt(i) == reverse.charAt(j)) { - if (i == 0 || j == 0) { - arr[i][j] = 1; - } else { - arr[i][j] = arr[i - 1][j - 1] + 1; - } - } - if (arr[i][j] > maxLen) { - maxLen = arr[i][j]; - maxEnd = i; //以 i 位置结尾的字符 - } - - } - } - return s.substring(maxEnd - maxLen + 1, maxEnd + 1); -} -``` - -再看一个例子,S = "abc435cba",S’ = "abc534cba" ,最长公共子串是 "abc" 和 "cba" ,但很明显这两个字符串都不是回文串。 - -所以我们求出最长公共子串后,并不一定是回文串,我们还需要判断该字符串倒置前的下标和当前的字符串下标是不是匹配。 - -比如 S = " caba ",S' = " abac " ,S’ 中 aba 的下标是 0 1 2 ,倒置前是 3 2 1,和 S 中 aba 的下标符合,所以 aba 就是我们需要找的。当然我们不需要每个字符都判断,我们只需要判断末尾字符就可以。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/5_1.jpg) - -首先 i ,j 始终指向子串的末尾字符。所以 j 指向的红色的 a 倒置前的下标是 beforeRev = length - 1 - j = 4 - 1 - 2 = 1,对应的是字符串首位的下标,我们还需要加上字符串的长度才是末尾字符的下标,也就是 beforeRev + arr\[ i ] [ j ] - 1 = 1 + 3 - 1 = 3,因为 arr\[ i ] [ j ] 保存的就是当前子串的长度,也就是图中的数字 3 。此时再和它与 i 比较,如果相等,则说明它是我们要找的回文串。 - -之前的 S = "abc435cba",S' = "abc534cba" ,可以看一下图示,为什么不符合。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/5_2.jpg) - -当前 j 指向的 c ,倒置前的下标是 beforeRev = length - 1 - j = 9 - 1 - 2 = 6,对应的末尾下标是 beforeRev + arr\[ i ] [ j ] - 1 = 6 + 3 - 1 = 8 ,而此时 i = 2 ,所以当前的子串不是回文串。 - -代码的话,在上边的基础上,保存 maxLen 前判断一下下标匹不匹配就可以了。 - -``` java -public String longestPalindrome(String s) { - if (s.equals("")) - return ""; - String origin = s; - String reverse = new StringBuffer(s).reverse().toString(); - int length = s.length(); - int[][] arr = new int[length][length]; - int maxLen = 0; - int maxEnd = 0; - for (int i = 0; i < length; i++) - for (int j = 0; j < length; j++) { - if (origin.charAt(i) == reverse.charAt(j)) { - if (i == 0 || j == 0) { - arr[i][j] = 1; - } else { - arr[i][j] = arr[i - 1][j - 1] + 1; - } - } - /**********修改的地方*******************/ - if (arr[i][j] > maxLen) { - int beforeRev = length - 1 - j; - if (beforeRev + arr[i][j] - 1 == i) { //判断下标是否对应 - maxLen = arr[i][j]; - maxEnd = i; - } - /*************************************/ - } - } - return s.substring(maxEnd - maxLen + 1, maxEnd + 1); -} -``` - -时间复杂度:两层循环,O(n²)。 - -空间复杂度:一个二维数组,O(n²)。 - -空间复杂度其实可以再优化一下。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/5_2.jpg) - -我们分析一下循环,i = 0 ,j = 0,1,2 ... 8 更新一列,然后 i = 1 ,再更新一列,而更新的时候我们其实只需要上一列的信息,更新第 3 列的时候,第 1 列的信息是没有用的。所以我们只需要一个一维数组就可以了。但是更新 arr [ i ] 的时候我们需要 arr [ i - 1 ] 的信息,假设 a [ 3 ] = a [ 2 ] + 1,更新 a [ 4 ] 的时候, 我们需要 a [ 3 ] 的信息,但是 a [ 3 ] 在之前已经被更新了,所以 j 不能从 0 到 8 ,应该倒过来,a [ 8 ] = a [ 7 ] + 1,a [ 7 ] = a [ 6 ] + 1 , 这样更新 a [ 8 ] 的时候用 a [ 7 ] ,用完后才去更新 a [ 7 ],保证了不会出错。 - -```java -public String longestPalindrome(String s) { - if (s.equals("")) - return ""; - String origin = s; - String reverse = new StringBuffer(s).reverse().toString(); - int length = s.length(); - int[] arr = new int[length]; - int maxLen = 0; - int maxEnd = 0; - for (int i = 0; i < length; i++) - /**************修改的地方***************************/ - for (int j = length - 1; j >= 0; j--) { - /**************************************************/ - if (origin.charAt(i) == reverse.charAt(j)) { - if (i == 0 || j == 0) { - arr[j] = 1; - } else { - arr[j] = arr[j - 1] + 1; - } - /**************修改的地方***************************/ - //之前二维数组,每次用的是不同的列,所以不用置 0 。 - } else { - arr[j] = 0; - } - /**************************************************/ - if (arr[j] > maxLen) { - int beforeRev = length - 1 - j; - if (beforeRev + arr[j] - 1 == i) { - maxLen = arr[j]; - maxEnd = i; - } - - } - } - return s.substring(maxEnd - maxLen + 1, maxEnd + 1); -} -``` - -时间复杂度:O(n²)。 - -空间复杂度:降为 O(n)。 - -## 解法三 暴力破解优化 - -解法一的暴力解法时间复杂度太高,在 leetCode 上并不能 AC 。我们可以考虑,去掉一些暴力解法中重复的判断。我们可以基于下边的发现,进行改进。 - -首先定义 P(i,j)。 - -$$P(i,j)=\begin{cases}true& \text{s[i,j]是回文串} \\\\false& \text{s[i,j]不是回文串}\end{cases}$$ - -接下来 - -$$P(i,j)=(P(i+1,j-1)\&\&S[i]==S[j])$$ - -所以如果我们想知道 P(i,j)的情况,不需要调用判断回文串的函数了,只需要知道 P(i + 1,j - 1)的情况就可以了,这样时间复杂度就少了 O(n)。因此我们可以用动态规划的方法,空间换时间,把已经求出的 P(i,j)存储起来。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/5_3.jpg) - -如果 $$S[i+1,j-1]$$ 是回文串,那么只要 S [ i ] == S [ j ] ,就可以确定 S [ i , j ] 也是回文串了。 - -求 长度为 1 和长度为 2 的 P ( i , j ) 时不能用上边的公式,因为我们代入公式后会遇到 $$P[i][j]$$ 中 i > j 的情况,比如求 $$P[1][2]$$ 的话,我们需要知道 $$P[1+1][2-1]=P[2][1]$$ ,而 $$P[2][1]$$ 代表着 $$S[2,1]$$ 是不是回文串,显然是不对的,所以我们需要单独判断。 - -所以我们先初始化长度是 1 的回文串的 P [ i , j ],这样利用上边提出的公式 $$P(i,j)=(P(i+1,j-1)\&\&S[i]==S[j])$$,然后两边向外各扩充一个字符,长度为 3 的,为 5 的,所有奇数长度的就都求出来了。 - -同理,初始化长度是 2 的回文串 P [ i , i + 1 ],利用公式,长度为 4 的,6 的所有偶数长度的就都求出来了。 - -```JAVA -public String longestPalindrome(String s) { - int length = s.length(); - boolean[][] P = new boolean[length][length]; - int maxLen = 0; - String maxPal = ""; - for (int len = 1; len <= length; len++) //遍历所有的长度 - for (int start = 0; start < length; start++) { - int end = start + len - 1; - if (end >= length) //下标已经越界,结束本次循环 - break; - P[start][end] = (len == 1 || len == 2 || P[start + 1][end - 1]) && s.charAt(start) == s.charAt(end); //长度为 1 和 2 的单独判断下 - if (P[start][end] && len > maxLen) { - maxPal = s.substring(start, end + 1); - } - } - return maxPal; -} -``` - -时间复杂度:两层循环,O(n²)。 - -空间复杂度:用二维数组 P 保存每个子串的情况,O(n²)。 - -我们分析下每次循环用到的 P(i,j),看一看能不能向解法二一样优化一下空间复杂度。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/5_4.jpg) - -当我们求长度为 6 和 5 的子串的情况时,其实只用到了 4 , 3 长度的情况,而长度为 1 和 2 的子串情况其实已经不需要了。但是由于我们并不是用 P 数组的下标进行的循环,暂时没有想到优化的方法。 - -之后看到了另一种动态规划的思路 - -https://leetcode.com/problems/longest-palindromic-substring/discuss/2921/Share-my-Java-solution-using-dynamic-programming 。 - -公式还是这个不变 - -首先定义 P(i,j)。 - -$$P(i,j)=\begin{cases}true& \text{s[i,j]是回文串}\\\\false& \text{s[i,j]不是回文串}\end{cases}$$ - -接下来 - -$$P(i,j)=(P(i+1,j-1)\&\&S[i]==S[j])$$ - -递推公式中我们可以看到,我们首先知道了 i +1 才会知道 i ,所以我们只需要倒着遍历就行了。 - -```java -public String longestPalindrome(String s) { - int n = s.length(); - String res = ""; - boolean[][] dp = new boolean[n][n]; - for (int i = n - 1; i >= 0; i--) { - for (int j = i; j < n; j++) { - dp[i][j] = s.charAt(i) == s.charAt(j) && (j - i < 2 || dp[i + 1][j - 1]); //j - i 代表长度减去 1 - if (dp[i][j] && j - i + 1 > res.length()) { - res = s.substring(i, j + 1); - } - } - } - return res; -} -``` - -时间复杂度和空间复杂和之前都没有变化,我们来看看可不可以优化空间复杂度。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/5_5.jpg) - -当求第 i 行的时候我们只需要第 i + 1 行的信息,并且 j 的话需要 j - 1 的信息,所以和之前一样 j 也需要倒叙。 - -```java -public String longestPalindrome7(String s) { - int n = s.length(); - String res = ""; - boolean[] P = new boolean[n]; - for (int i = n - 1; i >= 0; i--) { - for (int j = n - 1; j >= i; j--) { - P[j] = s.charAt(i) == s.charAt(j) && (j - i < 3 || P[j - 1]); - if (P[j] && j - i + 1 > res.length()) { - res = s.substring(i, j + 1); - } - } - } - return res; - } -``` - -时间复杂度:不变,O(n²)。 - -空间复杂度:降为 O(n ) 。 - -## 解法四 扩展中心 - -我们知道回文串一定是对称的,所以我们可以每次循环选择一个中心,进行左右扩展,判断左右字符是否相等即可。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/5_6.jpg) - -由于存在奇数的字符串和偶数的字符串,所以我们需要从一个字符开始扩展,或者从两个字符之间开始扩展,所以总共有 n + n - 1 个中心。 - -```java -public String longestPalindrome(String s) { - if (s == null || s.length() < 1) return ""; - int start = 0, end = 0; - for (int i = 0; i < s.length(); i++) { - int len1 = expandAroundCenter(s, i, i); - int len2 = expandAroundCenter(s, i, i + 1); - int len = Math.max(len1, len2); - if (len > end - start) { - start = i - (len - 1) / 2; - end = i + len / 2; - } - } - return s.substring(start, end + 1); -} - -private int expandAroundCenter(String s, int left, int right) { - int L = left, R = right; - while (L >= 0 && R < s.length() && s.charAt(L) == s.charAt(R)) { - L--; - R++; - } - return R - L - 1; -} -``` - -时间复杂度:O(n²)。 - -空间复杂度:O(1)。 - -## 解法五 Manacher's Algorithm 马拉车算法。 - -> 马拉车算法 Manacher‘s Algorithm 是用来查找一个字符串的最长回文子串的线性方法,由一个叫Manacher的人在1975年发明的,这个方法的最大贡献是在于将时间复杂度提升到了线性。 - -主要参考了下边链接进行讲解。 - -https://segmentfault.com/a/1190000008484167 - -https://blog.crimx.com/2017/07/06/manachers-algorithm/ - -http://ju.outofmemory.cn/entry/130005 - -https://articles.leetcode.com/longest-palindromic-substring-part-ii/ - -首先我们解决下奇数和偶数的问题,在每个字符间插入"#",并且为了使得扩展的过程中,到边界后自动结束,在两端分别插入 "^" 和 "$",两个不可能在字符串中出现的字符,这样中心扩展的时候,判断两端字符是否相等的时候,如果到了边界就一定会不相等,从而出了循环。经过处理,字符串的长度永远都是奇数了。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/5_7.jpg) - -首先我们用一个数组 P 保存从中心扩展的最大个数,而它刚好也是去掉 "#" 的原字符串的总长度。例如下图中下标是 6 的地方。可以看到 P[ 6 ] 等于 5,所以它是从左边扩展 5 个字符,相应的右边也是扩展 5 个字符,也就是 "#c#b#c#b#c#"。而去掉 # 恢复到原来的字符串,变成 "cbcbc",它的长度刚好也就是 5。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/5_8.jpg) - -## 求原字符串下标 - -用 P 的下标 i 减去 P [ i ],再除以 2 ,就是原字符串的开头下标了。 - -例如我们找到 P[ i ] 的最大值为 5 ,也就是回文串的最大长度是 5 ,对应的下标是 6 ,所以原字符串的开头下标是 (6 - 5 )/ 2 = 0 。所以我们只需要返回原字符串的第 0 到 第 (5 - 1)位就可以了。 - -## 求每个 P [ i ] - -接下来是算法的关键了,它充分利用了回文串的对称性。 - -我们用 C 表示回文串的中心,用 R 表示回文串的右边半径坐标,所以 R = C + P[ C ] 。C 和 R 所对应的回文串是当前循环中 R 最靠右的回文串。 - -让我们考虑求 P [ i ] 的时候,如下图。 - -用 i_mirror 表示当前需要求的第 i 个字符关于 C 对应的下标。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/5_9.jpg) - -我们现在要求 P [ i ], 如果是用中心扩展法,那就向两边扩展比对就行了。但是我们其实可以利用回文串 C 的对称性。i 关于 C 的对称点是 i_mirror ,P [ i_mirror ] = 3,所以 P [ i ] 也等于 3 。 - -但是有三种情况将会造成直接赋值为 P [ i_mirror ] 是不正确的,下边一一讨论。 - -### 1. 超出了 R - -![](http://windliang.oss-cn-beijing.aliyuncs.com/5_10.jpg) - -当我们要求 P [ i ] 的时候,P [ mirror ] = 7,而此时 P [ i ] 并不等于 7 ,为什么呢,因为我们从 i 开始往后数 7 个,等于 22 ,已经超过了最右的 R ,此时不能利用对称性了,但我们一定可以扩展到 R 的,所以 P [ i ] 至少等于 R - i = 20 - 15 = 5,会不会更大呢,我们只需要比较 T [ R+1 ] 和 T [ R+1 ]关于 i 的对称点就行了,就像中心扩展法一样一个个扩展。 - -### 2. P [ i_mirror ] 遇到了原字符串的左边界 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/5_12.jpg) - -此时P [ i_mirror ] = 1,但是 P [ i ] 赋值成 1 是不正确的,出现这种情况的原因是 P [ i_mirror ] 在扩展的时候首先是 "#" == "#" ,之后遇到了 "^"和另一个字符比较,也就是到了边界,才终止循环的。而 P [ i ] 并没有遇到边界,所以我们可以继续通过中心扩展法一步一步向两边扩展就行了。 - -### 3. i 等于了 R - -此时我们先把 P [ i ] 赋值为 0 ,然后通过中心扩展法一步一步扩展就行了。 - -## 考虑 C 和 R 的更新 - -就这样一步一步的求出每个 P [ i ],当求出的 P [ i ] 的右边界大于当前的 R 时,我们就需要更新 C 和 R 为当前的回文串了。因为我们必须保证 i 在 R 里面,所以一旦有更右边的 R 就要更新 R。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/5_12.jpg) - -此时的 P [ i ] 求出来将会是 3 ,P [ i ] 对应的右边界将是 10 + 3 = 13,所以大于当前的 R ,我们需要把 C 更新成 i 的值,也就是 10 ,R 更新成 13。继续下边的循环。 - -```java -public String preProcess(String s) { - int n = s.length(); - if (n == 0) { - return "^$"; - } - String ret = "^"; - for (int i = 0; i < n; i++) - ret += "#" + s.charAt(i); - ret += "#$"; - return ret; -} - -// 马拉车算法 -public String longestPalindrome(String s) { - String T = preProcess(s); - int n = T.length(); - int[] P = new int[n]; - int C = 0, R = 0; - for (int i = 1; i < n - 1; i++) { - int i_mirror = 2 * C - i; - if (R > i) { - P[i] = Math.min(R - i, P[i_mirror]);// 防止超出 R - } else { - P[i] = 0;// 等于 R 的情况 - } - - // 碰到之前讲的三种情况时候,需要利用中心扩展法 - while (T.charAt(i + 1 + P[i]) == T.charAt(i - 1 - P[i])) { - P[i]++; - } - - // 判断是否需要更新 R - if (i + P[i] > R) { - C = i; - R = i + P[i]; - } - - } - - // 找出 P 的最大值 - int maxLen = 0; - int centerIndex = 0; - for (int i = 1; i < n - 1; i++) { - if (P[i] > maxLen) { - maxLen = P[i]; - centerIndex = i; - } - } - int start = (centerIndex - maxLen) / 2; //最开始讲的求原字符串下标 - return s.substring(start, start + maxLen); -} -``` - -时间复杂度:for 循环里边套了一层 while 循环,难道不是 O ( n² )?不!其实是 O ( n )。不严谨的想一下,因为 while 循环访问 R 右边的数字用来扩展,也就是那些还未求出的节点,然后不断扩展,而期间访问的节点下次就不会再进入 while 了,可以利用对称得到自己的解,所以每个节点访问都是常数次,所以是 O ( n )。 - -空间复杂度:O(n)。 - -## 总结 - +## 题目描述(中等难度) + +![](http://windliang.oss-cn-beijing.aliyuncs.com/5_palindromic.jpg) + +给定一个字符串,输出最长的回文子串。回文串指的是正的读和反的读是一样的字符串,例如 "aba","ccbbcc"。 + +## 解法一 暴力破解 + +暴力求解,列举所有的子串,判断是否为回文串,保存最长的回文串。 + +```java +public boolean isPalindromic(String s) { + int len = s.length(); + for (int i = 0; i < len / 2; i++) { + if (s.charAt(i) != s.charAt(len - i - 1)) { + return false; + } + } + return true; + } + +// 暴力解法 +public String longestPalindrome(String s) { + String ans = ""; + int max = 0; + int len = s.length(); + for (int i = 0; i < len; i++) + for (int j = i + 1; j <= len; j++) { + String test = s.substring(i, j); + if (isPalindromic(test) && test.length() > max) { + ans = s.substring(i, j); + max = Math.max(max, ans.length()); + } + } + return ans; +} +``` + +时间复杂度:两层 for 循环 O(n²),for 循环里边判断是否为回文,O(n),所以时间复杂度为 O(n³)。 + +空间复杂度:O(1),常数个变量。 + +## 解法二 最长公共子串 + +根据回文串的定义,正着和反着读一样,那我们是不是把原来的字符串倒置了,然后找最长的公共子串就可以了。例如,S = " caba",S' = " abac",最长公共子串是 "aba",所以原字符串的最长回文串就是 "aba"。 + +关于求最长公共子串(不是公共子序列),有很多方法,这里用动态规划的方法,可以先阅读下边的链接。 + +https://blog.csdn.net/u010397369/article/details/38979077 + +https://www.kancloud.cn/digest/pieces-algorithm/163624 + +整体思想就是,申请一个二维的数组初始化为 0,然后判断对应的字符是否相等,相等的话 + +arr [ i ]\[ j ] = arr [ i - 1 ]\[ j - 1] + 1 。 + +当 i = 0 或者 j = 0 的时候单独分析,字符相等的话 arr [ i ]\[ j ] 就赋为 1 。 + +arr [ i ]\[ j ] 保存的就是公共子串的长度。 + +```java +public String longestPalindrome(String s) { + if (s.equals("")) + return ""; + String origin = s; + String reverse = new StringBuffer(s).reverse().toString(); //字符串倒置 + int length = s.length(); + int[][] arr = new int[length][length]; + int maxLen = 0; + int maxEnd = 0; + for (int i = 0; i < length; i++) + for (int j = 0; j < length; j++) { + if (origin.charAt(i) == reverse.charAt(j)) { + if (i == 0 || j == 0) { + arr[i][j] = 1; + } else { + arr[i][j] = arr[i - 1][j - 1] + 1; + } + } + if (arr[i][j] > maxLen) { + maxLen = arr[i][j]; + maxEnd = i; //以 i 位置结尾的字符 + } + + } + } + return s.substring(maxEnd - maxLen + 1, maxEnd + 1); +} +``` + +再看一个例子,S = "abc435cba",S’ = "abc534cba" ,最长公共子串是 "abc" 和 "cba" ,但很明显这两个字符串都不是回文串。 + +所以我们求出最长公共子串后,并不一定是回文串,我们还需要判断该字符串倒置前的下标和当前的字符串下标是不是匹配。 + +比如 S = " caba ",S' = " abac " ,S’ 中 aba 的下标是 0 1 2 ,倒置前是 3 2 1,和 S 中 aba 的下标符合,所以 aba 就是我们需要找的。当然我们不需要每个字符都判断,我们只需要判断末尾字符就可以。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/5_1.jpg) + +首先 i ,j 始终指向子串的末尾字符。所以 j 指向的红色的 a 倒置前的下标是 beforeRev = length - 1 - j = 4 - 1 - 2 = 1,对应的是字符串首位的下标,我们还需要加上字符串的长度才是末尾字符的下标,也就是 beforeRev + arr\[ i ] [ j ] - 1 = 1 + 3 - 1 = 3,因为 arr\[ i ] [ j ] 保存的就是当前子串的长度,也就是图中的数字 3 。此时再和它与 i 比较,如果相等,则说明它是我们要找的回文串。 + +之前的 S = "abc435cba",S' = "abc534cba" ,可以看一下图示,为什么不符合。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/5_2.jpg) + +当前 j 指向的 c ,倒置前的下标是 beforeRev = length - 1 - j = 9 - 1 - 2 = 6,对应的末尾下标是 beforeRev + arr\[ i ] [ j ] - 1 = 6 + 3 - 1 = 8 ,而此时 i = 2 ,所以当前的子串不是回文串。 + +代码的话,在上边的基础上,保存 maxLen 前判断一下下标匹不匹配就可以了。 + +``` java +public String longestPalindrome(String s) { + if (s.equals("")) + return ""; + String origin = s; + String reverse = new StringBuffer(s).reverse().toString(); + int length = s.length(); + int[][] arr = new int[length][length]; + int maxLen = 0; + int maxEnd = 0; + for (int i = 0; i < length; i++) + for (int j = 0; j < length; j++) { + if (origin.charAt(i) == reverse.charAt(j)) { + if (i == 0 || j == 0) { + arr[i][j] = 1; + } else { + arr[i][j] = arr[i - 1][j - 1] + 1; + } + } + /**********修改的地方*******************/ + if (arr[i][j] > maxLen) { + int beforeRev = length - 1 - j; + if (beforeRev + arr[i][j] - 1 == i) { //判断下标是否对应 + maxLen = arr[i][j]; + maxEnd = i; + } + /*************************************/ + } + } + return s.substring(maxEnd - maxLen + 1, maxEnd + 1); +} +``` + +时间复杂度:两层循环,O(n²)。 + +空间复杂度:一个二维数组,O(n²)。 + +空间复杂度其实可以再优化一下。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/5_2.jpg) + +我们分析一下循环,i = 0 ,j = 0,1,2 ... 8 更新一列,然后 i = 1 ,再更新一列,而更新的时候我们其实只需要上一列的信息,更新第 3 列的时候,第 1 列的信息是没有用的。所以我们只需要一个一维数组就可以了。但是更新 arr [ i ] 的时候我们需要 arr [ i - 1 ] 的信息,假设 a [ 3 ] = a [ 2 ] + 1,更新 a [ 4 ] 的时候, 我们需要 a [ 3 ] 的信息,但是 a [ 3 ] 在之前已经被更新了,所以 j 不能从 0 到 8 ,应该倒过来,a [ 8 ] = a [ 7 ] + 1,a [ 7 ] = a [ 6 ] + 1 , 这样更新 a [ 8 ] 的时候用 a [ 7 ] ,用完后才去更新 a [ 7 ],保证了不会出错。 + +```java +public String longestPalindrome(String s) { + if (s.equals("")) + return ""; + String origin = s; + String reverse = new StringBuffer(s).reverse().toString(); + int length = s.length(); + int[] arr = new int[length]; + int maxLen = 0; + int maxEnd = 0; + for (int i = 0; i < length; i++) + /**************修改的地方***************************/ + for (int j = length - 1; j >= 0; j--) { + /**************************************************/ + if (origin.charAt(i) == reverse.charAt(j)) { + if (i == 0 || j == 0) { + arr[j] = 1; + } else { + arr[j] = arr[j - 1] + 1; + } + /**************修改的地方***************************/ + //之前二维数组,每次用的是不同的列,所以不用置 0 。 + } else { + arr[j] = 0; + } + /**************************************************/ + if (arr[j] > maxLen) { + int beforeRev = length - 1 - j; + if (beforeRev + arr[j] - 1 == i) { + maxLen = arr[j]; + maxEnd = i; + } + + } + } + return s.substring(maxEnd - maxLen + 1, maxEnd + 1); +} +``` + +时间复杂度:O(n²)。 + +空间复杂度:降为 O(n)。 + +## 解法三 暴力破解优化 + +解法一的暴力解法时间复杂度太高,在 leetCode 上并不能 AC 。我们可以考虑,去掉一些暴力解法中重复的判断。我们可以基于下边的发现,进行改进。 + +首先定义 P(i,j)。 + +$$P(i,j)=\begin{cases}true& \text{s[i,j]是回文串} \\\\false& \text{s[i,j]不是回文串}\end{cases}$$ + +接下来 + +$$P(i,j)=(P(i+1,j-1)\&\&S[i]==S[j])$$ + +所以如果我们想知道 P(i,j)的情况,不需要调用判断回文串的函数了,只需要知道 P(i + 1,j - 1)的情况就可以了,这样时间复杂度就少了 O(n)。因此我们可以用动态规划的方法,空间换时间,把已经求出的 P(i,j)存储起来。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/5_3.jpg) + +如果 $$S[i+1,j-1]$$ 是回文串,那么只要 S [ i ] == S [ j ] ,就可以确定 S [ i , j ] 也是回文串了。 + +求 长度为 1 和长度为 2 的 P ( i , j ) 时不能用上边的公式,因为我们代入公式后会遇到 $$P[i][j]$$ 中 i > j 的情况,比如求 $$P[1][2]$$ 的话,我们需要知道 $$P[1+1][2-1]=P[2][1]$$ ,而 $$P[2][1]$$ 代表着 $$S[2,1]$$ 是不是回文串,显然是不对的,所以我们需要单独判断。 + +所以我们先初始化长度是 1 的回文串的 P [ i , j ],这样利用上边提出的公式 $$P(i,j)=(P(i+1,j-1)\&\&S[i]==S[j])$$,然后两边向外各扩充一个字符,长度为 3 的,为 5 的,所有奇数长度的就都求出来了。 + +同理,初始化长度是 2 的回文串 P [ i , i + 1 ],利用公式,长度为 4 的,6 的所有偶数长度的就都求出来了。 + +```JAVA +public String longestPalindrome(String s) { + int length = s.length(); + boolean[][] P = new boolean[length][length]; + int maxLen = 0; + String maxPal = ""; + for (int len = 1; len <= length; len++) //遍历所有的长度 + for (int start = 0; start < length; start++) { + int end = start + len - 1; + if (end >= length) //下标已经越界,结束本次循环 + break; + P[start][end] = (len == 1 || len == 2 || P[start + 1][end - 1]) && s.charAt(start) == s.charAt(end); //长度为 1 和 2 的单独判断下 + if (P[start][end] && len > maxLen) { + maxPal = s.substring(start, end + 1); + } + } + return maxPal; +} +``` + +时间复杂度:两层循环,O(n²)。 + +空间复杂度:用二维数组 P 保存每个子串的情况,O(n²)。 + +我们分析下每次循环用到的 P(i,j),看一看能不能向解法二一样优化一下空间复杂度。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/5_4.jpg) + +当我们求长度为 6 和 5 的子串的情况时,其实只用到了 4 , 3 长度的情况,而长度为 1 和 2 的子串情况其实已经不需要了。但是由于我们并不是用 P 数组的下标进行的循环,暂时没有想到优化的方法。 + +之后看到了另一种动态规划的思路 + +https://leetcode.com/problems/longest-palindromic-substring/discuss/2921/Share-my-Java-solution-using-dynamic-programming 。 + +公式还是这个不变 + +首先定义 P(i,j)。 + +$$P(i,j)=\begin{cases}true& \text{s[i,j]是回文串}\\\\false& \text{s[i,j]不是回文串}\end{cases}$$ + +接下来 + +$$P(i,j)=(P(i+1,j-1)\&\&S[i]==S[j])$$ + +递推公式中我们可以看到,我们首先知道了 i +1 才会知道 i ,所以我们只需要倒着遍历就行了。 + +```java +public String longestPalindrome(String s) { + int n = s.length(); + String res = ""; + boolean[][] dp = new boolean[n][n]; + for (int i = n - 1; i >= 0; i--) { + for (int j = i; j < n; j++) { + dp[i][j] = s.charAt(i) == s.charAt(j) && (j - i < 2 || dp[i + 1][j - 1]); //j - i 代表长度减去 1 + if (dp[i][j] && j - i + 1 > res.length()) { + res = s.substring(i, j + 1); + } + } + } + return res; +} +``` + +时间复杂度和空间复杂和之前都没有变化,我们来看看可不可以优化空间复杂度。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/5_5.jpg) + +当求第 i 行的时候我们只需要第 i + 1 行的信息,并且 j 的话需要 j - 1 的信息,所以和之前一样 j 也需要倒叙。 + +```java +public String longestPalindrome7(String s) { + int n = s.length(); + String res = ""; + boolean[] P = new boolean[n]; + for (int i = n - 1; i >= 0; i--) { + for (int j = n - 1; j >= i; j--) { + P[j] = s.charAt(i) == s.charAt(j) && (j - i < 3 || P[j - 1]); + if (P[j] && j - i + 1 > res.length()) { + res = s.substring(i, j + 1); + } + } + } + return res; + } +``` + +时间复杂度:不变,O(n²)。 + +空间复杂度:降为 O(n ) 。 + +## 解法四 扩展中心 + +我们知道回文串一定是对称的,所以我们可以每次循环选择一个中心,进行左右扩展,判断左右字符是否相等即可。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/5_6.jpg) + +由于存在奇数的字符串和偶数的字符串,所以我们需要从一个字符开始扩展,或者从两个字符之间开始扩展,所以总共有 n + n - 1 个中心。 + +```java +public String longestPalindrome(String s) { + if (s == null || s.length() < 1) return ""; + int start = 0, end = 0; + for (int i = 0; i < s.length(); i++) { + int len1 = expandAroundCenter(s, i, i); + int len2 = expandAroundCenter(s, i, i + 1); + int len = Math.max(len1, len2); + if (len > end - start) { + start = i - (len - 1) / 2; + end = i + len / 2; + } + } + return s.substring(start, end + 1); +} + +private int expandAroundCenter(String s, int left, int right) { + int L = left, R = right; + while (L >= 0 && R < s.length() && s.charAt(L) == s.charAt(R)) { + L--; + R++; + } + return R - L - 1; +} +``` + +时间复杂度:O(n²)。 + +空间复杂度:O(1)。 + +## 解法五 Manacher's Algorithm 马拉车算法。 + +> 马拉车算法 Manacher‘s Algorithm 是用来查找一个字符串的最长回文子串的线性方法,由一个叫Manacher的人在1975年发明的,这个方法的最大贡献是在于将时间复杂度提升到了线性。 + +主要参考了下边链接进行讲解。 + +https://segmentfault.com/a/1190000008484167 + +https://blog.crimx.com/2017/07/06/manachers-algorithm/ + +http://ju.outofmemory.cn/entry/130005 + +https://articles.leetcode.com/longest-palindromic-substring-part-ii/ + +首先我们解决下奇数和偶数的问题,在每个字符间插入"#",并且为了使得扩展的过程中,到边界后自动结束,在两端分别插入 "^" 和 "$",两个不可能在字符串中出现的字符,这样中心扩展的时候,判断两端字符是否相等的时候,如果到了边界就一定会不相等,从而出了循环。经过处理,字符串的长度永远都是奇数了。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/5_7.jpg) + +首先我们用一个数组 P 保存从中心扩展的最大个数,而它刚好也是去掉 "#" 的原字符串的总长度。例如下图中下标是 6 的地方。可以看到 P[ 6 ] 等于 5,所以它是从左边扩展 5 个字符,相应的右边也是扩展 5 个字符,也就是 "#c#b#c#b#c#"。而去掉 # 恢复到原来的字符串,变成 "cbcbc",它的长度刚好也就是 5。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/5_8.jpg) + +## 求原字符串下标 + +用 P 的下标 i 减去 P [ i ],再除以 2 ,就是原字符串的开头下标了。 + +例如我们找到 P[ i ] 的最大值为 5 ,也就是回文串的最大长度是 5 ,对应的下标是 6 ,所以原字符串的开头下标是 (6 - 5 )/ 2 = 0 。所以我们只需要返回原字符串的第 0 到 第 (5 - 1)位就可以了。 + +## 求每个 P [ i ] + +接下来是算法的关键了,它充分利用了回文串的对称性。 + +我们用 C 表示回文串的中心,用 R 表示回文串的右边半径坐标,所以 R = C + P[ C ] 。C 和 R 所对应的回文串是当前循环中 R 最靠右的回文串。 + +让我们考虑求 P [ i ] 的时候,如下图。 + +用 i_mirror 表示当前需要求的第 i 个字符关于 C 对应的下标。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/5_9.jpg) + +我们现在要求 P [ i ], 如果是用中心扩展法,那就向两边扩展比对就行了。但是我们其实可以利用回文串 C 的对称性。i 关于 C 的对称点是 i_mirror ,P [ i_mirror ] = 3,所以 P [ i ] 也等于 3 。 + +但是有三种情况将会造成直接赋值为 P [ i_mirror ] 是不正确的,下边一一讨论。 + +### 1. 超出了 R + +![](http://windliang.oss-cn-beijing.aliyuncs.com/5_10.jpg) + +当我们要求 P [ i ] 的时候,P [ mirror ] = 7,而此时 P [ i ] 并不等于 7 ,为什么呢,因为我们从 i 开始往后数 7 个,等于 22 ,已经超过了最右的 R ,此时不能利用对称性了,但我们一定可以扩展到 R 的,所以 P [ i ] 至少等于 R - i = 20 - 15 = 5,会不会更大呢,我们只需要比较 T [ R+1 ] 和 T [ R+1 ]关于 i 的对称点就行了,就像中心扩展法一样一个个扩展。 + +### 2. P [ i_mirror ] 遇到了原字符串的左边界 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/5_12.jpg) + +此时P [ i_mirror ] = 1,但是 P [ i ] 赋值成 1 是不正确的,出现这种情况的原因是 P [ i_mirror ] 在扩展的时候首先是 "#" == "#" ,之后遇到了 "^"和另一个字符比较,也就是到了边界,才终止循环的。而 P [ i ] 并没有遇到边界,所以我们可以继续通过中心扩展法一步一步向两边扩展就行了。 + +### 3. i 等于了 R + +此时我们先把 P [ i ] 赋值为 0 ,然后通过中心扩展法一步一步扩展就行了。 + +## 考虑 C 和 R 的更新 + +就这样一步一步的求出每个 P [ i ],当求出的 P [ i ] 的右边界大于当前的 R 时,我们就需要更新 C 和 R 为当前的回文串了。因为我们必须保证 i 在 R 里面,所以一旦有更右边的 R 就要更新 R。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/5_12.jpg) + +此时的 P [ i ] 求出来将会是 3 ,P [ i ] 对应的右边界将是 10 + 3 = 13,所以大于当前的 R ,我们需要把 C 更新成 i 的值,也就是 10 ,R 更新成 13。继续下边的循环。 + +```java +public String preProcess(String s) { + int n = s.length(); + if (n == 0) { + return "^$"; + } + String ret = "^"; + for (int i = 0; i < n; i++) + ret += "#" + s.charAt(i); + ret += "#$"; + return ret; +} + +// 马拉车算法 +public String longestPalindrome(String s) { + String T = preProcess(s); + int n = T.length(); + int[] P = new int[n]; + int C = 0, R = 0; + for (int i = 1; i < n - 1; i++) { + int i_mirror = 2 * C - i; + if (R > i) { + P[i] = Math.min(R - i, P[i_mirror]);// 防止超出 R + } else { + P[i] = 0;// 等于 R 的情况 + } + + // 碰到之前讲的三种情况时候,需要利用中心扩展法 + while (T.charAt(i + 1 + P[i]) == T.charAt(i - 1 - P[i])) { + P[i]++; + } + + // 判断是否需要更新 R + if (i + P[i] > R) { + C = i; + R = i + P[i]; + } + + } + + // 找出 P 的最大值 + int maxLen = 0; + int centerIndex = 0; + for (int i = 1; i < n - 1; i++) { + if (P[i] > maxLen) { + maxLen = P[i]; + centerIndex = i; + } + } + int start = (centerIndex - maxLen) / 2; //最开始讲的求原字符串下标 + return s.substring(start, start + maxLen); +} +``` + +时间复杂度:for 循环里边套了一层 while 循环,难道不是 O ( n² )?不!其实是 O ( n )。不严谨的想一下,因为 while 循环访问 R 右边的数字用来扩展,也就是那些还未求出的节点,然后不断扩展,而期间访问的节点下次就不会再进入 while 了,可以利用对称得到自己的解,所以每个节点访问都是常数次,所以是 O ( n )。 + +空间复杂度:O(n)。 + +## 总结 + 时间复杂度从三次方降到了一次,美妙!这里两次用到了动态规划去求解,初步认识了动态规划,就是将之前求的值保存起来,方便后边的计算,使得一些多余的计算消失了。并且在动态规划中,通过观察数组的利用情况,从而降低了空间复杂度。而 Manacher 算法对回文串对称性的充分利用,不得不让人叹服,自己加油啦! \ No newline at end of file diff --git a/leetCode-50-Pow.md b/leetCode-50-Pow.md index c4779a26d..b8b2be614 100644 --- a/leetCode-50-Pow.md +++ b/leetCode-50-Pow.md @@ -1,374 +1,374 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/50.jpg) - -就是求幂次方。 - -# 解法一 - -求幂次方,用最简单的想法,就是写一个 for 循环累乘。 - -至于求负幂次方,比如 $$2^{-10}$$,可以先求出 $$2^{10}$$,然后取倒数,$$1/2^{10}$$ ,就可以了。 - -```java -double mul = 1; -if (n > 0) { - for (int i = 0; i < n; i++) { - mul *= x; - } -} else { - n = -n; - for (int i = 0; i < n; i++) { - mul *= x; - } - mul = 1 / mul; -} -``` - -但这样的话会出问题,之前在[29题](https://leetcode.windliang.cc/leetCode-29-Divide-Two-Integers.html)讨论过,问题出在 n = - n 上,因为最小负数 $$-2^{31}$$取相反数的话,按照计算机的规则,依旧是$$-2^{31}$$,所以这种情况需要单独讨论一下。 - -```java -if (n == -2147483648) { - return 0; -} -``` - -当然,这样做的话 -1 ,和 1 也需要单独讨论下,因为他们的任意次方都是 1 或者 -1 。 - -```java -if (x == -1) { - if ((n & 1) != 0) { //按位与不等于 0 ,说明是奇数 - return -1; - } else { - return 1; - } -} -if (x == 1.0) - return 1; -``` - -综上,代码就出来了。 - -```java -public double myPow(double x, int n) { - if (x == -1) { - if ((n & 1) != 0) { - return -1; - } else { - return 1; - } - } - if (x == 1.0) - return 1; - - if (n == -2147483648) { - return 0; - } - double mul = 1; - if (n > 0) { - for (int i = 0; i < n; i++) { - mul *= x; - } - } else { - n = -n; - for (int i = 0; i < n; i++) { - mul *= x; - } - mul = 1 / mul; - } - return mul; -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(1)。 - -# 解法二 递归 - -对于上边的解法,太慢了。可以优化下,类似于[29题](https://leetcode.windliang.cc/leetCode-29-Divide-Two-Integers.html)的思路。乘法的话,我们不用一次一次的相乘,得到 2 次方后,我们可以直接把 2 次方的结果相乘,就可以得到 4 次方,得到 4 次方的结果再相乘,就是 8 次方了,这样的话就会快很多了。 - -直接利用递归吧 - -对于 n 是偶数的情况,$$x^n=x^{n/2}*x^{n/2}$$。 - -对于 n 是奇数的情况,$$x^n=x^{n/2}*x^{n/2}*x$$。 - -```java -public double powRecursion(double x, int n) { - if (n == 0) { - return 1; - } - //偶数的情况 - if ((n & 1) == 0) { - double temp = powRecursion(x, n / 2); - return temp * temp; - } else { //奇数的情况 - double temp = powRecursion(x, n / 2); - return temp * temp * x; - } -} - -public double myPow(double x, int n) { - if (x == -1) { - if ((n & 1) != 0) { - return -1; - } else { - return 1; - } - } - if (x == 1.0f) - return 1; - - if (n == -2147483648) { - return 0; - } - double mul = 1; - if (n > 0) { - mul = powRecursion(x, n); - } else { - n = -n; - mul = powRecursion(x, n); - mul = 1 / mul; - } - return mul; -} -``` - -时间复杂度:O(log(n))。 - -空间复杂度: - -当然对于这种递归的解法的话,还有一些其他的思路,参考[这里](https://leetcode.com/problems/powx-n/discuss/19546/Short-and-easy-to-understand-solution)。 - -递归思路是下边的样子 - -$$x^n=(x*x)^{n/2}$$ , 对于 n 是偶数的情况。 - -$$x^n=(x*x)^{n/2}*x$$,对于 n 是奇数的情况, - -代码就很好写了。 - -```java -public double powRecursion(double x, int n) { - if (n == 0) { - return 1; - } - //偶数的情况 - if ((n & 1) == 0) { - return powRecursion(x * x, n / 2); - } else { //奇数的情况 - return powRecursion(x * x, n / 2) * x; - } -} - -public double myPow(double x, int n) { - if (x == -1) { - if ((n & 1) != 0) { - return -1; - } else { - return 1; - } - } - if (x == 1.0f) - return 1; - - if (n == -2147483648) { - return 0; - } - double mul = 1; - if (n > 0) { - mul = powRecursion(x, n); - } else { - n = -n; - mul = powRecursion(x, n); - mul = 1 / mul; - } - return mul; -} -``` - -时间复杂度:O(log(n))。 - -空间复杂度: - -# 解法三 迭代 - -这里介绍种全新的解法,开始的时候受前边思路的影响,一直没理解。下午问同学,同学立刻想到了自己在《编程之美》看到的解法,这里分享下。 - -以 x 的 10 次方举例。10 的 2 进制是 1010,然后用 2 进制转 10 进制的方法把它展成 2 的幂次的和。 - -$$x^{10}=x^{(1010)_2}=x^{1*2^3+0*2^2+1*2^1+0*2^0}=x^{1*2^3}*x^{0*2^2}x^{1*2^1}*x^{0*2^0}$$ - -这样话,再看一下下边的图,它们之间的对应关系就出来了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/50_2.jpg) - -2 进制对应 1 0 1 0,我们把对应 1 的项进行累乘就可以了,而要进行累乘的项也是很有规律,前一项是后一项的自乘。$$x^8=x^4*x^4$$。我们可以从最右边一位,开始迭代。看下代码吧。 - -```java -public double myPow(double x, int n) { - if (x == -1) { - if ((n & 1) != 0) { - return -1; - } else { - return 1; - } - } - if (x == 1.0f) - return 1; - - if (n == -2147483648) { - return 0; - } - double mul = 1; - if (n > 0) { - mul = powIteration(x, n); - } else { - n = -n; - mul = powIteration(x, n); - mul = 1 / mul; - } - return mul; -} - -public double powIteration(double x, int n) { - double ans = 1; - //遍历每一位 - while (n > 0) { - //最后一位是 1,加到累乘结果里 - if ((n & 1) == 1) { - ans = ans * x; - } - //更新 x - x = x * x; - //n 右移一位 - n = n >> 1; - } - return ans; -} -``` - -时间复杂度:log(n)。 - -空间复杂度:O(1)。 - -# 更新 - -2020.3.16 更新。感谢 [@为爱卖小菜](https://leetcode-cn.com/u/wei-ai-mai-xiao-cai/) 指出,上边的解法虽然都能 `AC`,但是以上全错,少考虑了一种情况。 - -前边我们分析到 ` -2147483648` 需要单独讨论。 - -> 但这样的话会出问题,之前在 [29题](https://leetcode.windliang.cc/leetCode-29-Divide-Two-Integers.html) 讨论过,问题出在 n = - n 上,因为最小负数 $$-2^{31}$$取相反数的话,按照计算机的规则,依旧是$$-2^{31}$$,所以这种情况需要单独讨论一下。 -> -> ```java -> if (n == -2147483648) { -> return 0; -> } -> ``` - -但当 `n = -2147483648` 个时候,并不是所有的 $$x^n$$ 结果都是 `0`。 - -当 `x` 等于 `-1` 或者 `1` 的时候结果是 `1` 。前边的解法也考虑到了。 - -下边 `x` 等于 `-1` 的时候我们顺便考虑了 `n` 是其他数的情况,所以没直接返回 `1`。 - -```java -if (x == -1) { - if ((n & 1) != 0) { - return -1; - } else { - return 1; - } -} -if (x == 1.0f) - return 1; -``` - -但其实 `x` 是浮点数,我们还少考虑了 `-1` 到 `0` 和 `0` 到 `1` 之间的数,此时的 $$x^n$$ 的结果应该是正无穷。 - -此外 `x == 0` 的话,数学上是不能算的,这里的话也输出正无穷。 - -综上,我们的前置条件如下 - -```java -if (x == -1) { - if ((n & 1) != 0) { - return -1; - } else { - return 1; - } -} -if (x == 1.0f){ - return 1; -} - -if(n == -2147483648){ - if(x > -1 && x < 1 ){ - return Double.POSITIVE_INFINITY; - }else{ - return 0; - } -} -``` - -上边就是当 `n = -2147483648` 的所有情况了。对于 $$x^n$$,`x` 分成了四种情况。 - -当 `x == -1` 结果是 `1`,上边的代码我们顺便把 `n` 是其它数的情况也顺便考虑了。 - -当 `x == 1` 结果是 `1`。 - -当 `-1 < x < 1` ,结果是正无穷。 - -当 `x < -1` 或者 `x > 1` ,结果是 `0`。 - - [@为爱卖小菜](https://leetcode-cn.com/u/wei-ai-mai-xiao-cai/) 也提供了一个新方法,可以把上边的所有情况统一起来。 - -因为当 `n = -2147483648` 的时候我们无法正确计算,我们可以把 $$x^{-2147483648}$$ 分解成 $$x^{-2147483647} * x^{-1}$$ 。这样的话两部分都可以成功求解了。 - -对于解法三,可以改写成下边的样子。其他解法也类似。 - -```java -public double myPow(double x, int n) { - double mul = 1; - if (n > 0) { - mul = powIteration(x, n); - } else { - //单独考虑 n = -2147483648 - if (n == -2147483648) { - return myPow(x, -2147483647) * (1 / x); - } - n = -n; - mul *= powIteration(x, n); - mul = 1 / mul; - } - return mul; -} - -public double powIteration(double x, int n) { - double ans = 1; - //遍历每一位 - while (n > 0) { - //最后一位是 1,加到累乘结果里 - if ((n & 1) == 1) { - ans = ans * x; - } - //更新 x - x = x * x; - //n 右移一位 - n = n >> 1; - } - return ans; -} -``` - - - -# 总 - -从一般的方法,到递归,最后的解法,直接从 2 进制考虑,每一个数字,都可以转换成 2 的幂次的和,从而实现了最终的解法。 - - - - - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/50.jpg) + +就是求幂次方。 + +# 解法一 + +求幂次方,用最简单的想法,就是写一个 for 循环累乘。 + +至于求负幂次方,比如 $$2^{-10}$$,可以先求出 $$2^{10}$$,然后取倒数,$$1/2^{10}$$ ,就可以了。 + +```java +double mul = 1; +if (n > 0) { + for (int i = 0; i < n; i++) { + mul *= x; + } +} else { + n = -n; + for (int i = 0; i < n; i++) { + mul *= x; + } + mul = 1 / mul; +} +``` + +但这样的话会出问题,之前在[29题](https://leetcode.windliang.cc/leetCode-29-Divide-Two-Integers.html)讨论过,问题出在 n = - n 上,因为最小负数 $$-2^{31}$$取相反数的话,按照计算机的规则,依旧是$$-2^{31}$$,所以这种情况需要单独讨论一下。 + +```java +if (n == -2147483648) { + return 0; +} +``` + +当然,这样做的话 -1 ,和 1 也需要单独讨论下,因为他们的任意次方都是 1 或者 -1 。 + +```java +if (x == -1) { + if ((n & 1) != 0) { //按位与不等于 0 ,说明是奇数 + return -1; + } else { + return 1; + } +} +if (x == 1.0) + return 1; +``` + +综上,代码就出来了。 + +```java +public double myPow(double x, int n) { + if (x == -1) { + if ((n & 1) != 0) { + return -1; + } else { + return 1; + } + } + if (x == 1.0) + return 1; + + if (n == -2147483648) { + return 0; + } + double mul = 1; + if (n > 0) { + for (int i = 0; i < n; i++) { + mul *= x; + } + } else { + n = -n; + for (int i = 0; i < n; i++) { + mul *= x; + } + mul = 1 / mul; + } + return mul; +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(1)。 + +# 解法二 递归 + +对于上边的解法,太慢了。可以优化下,类似于[29题](https://leetcode.windliang.cc/leetCode-29-Divide-Two-Integers.html)的思路。乘法的话,我们不用一次一次的相乘,得到 2 次方后,我们可以直接把 2 次方的结果相乘,就可以得到 4 次方,得到 4 次方的结果再相乘,就是 8 次方了,这样的话就会快很多了。 + +直接利用递归吧 + +对于 n 是偶数的情况,$$x^n=x^{n/2}*x^{n/2}$$。 + +对于 n 是奇数的情况,$$x^n=x^{n/2}*x^{n/2}*x$$。 + +```java +public double powRecursion(double x, int n) { + if (n == 0) { + return 1; + } + //偶数的情况 + if ((n & 1) == 0) { + double temp = powRecursion(x, n / 2); + return temp * temp; + } else { //奇数的情况 + double temp = powRecursion(x, n / 2); + return temp * temp * x; + } +} + +public double myPow(double x, int n) { + if (x == -1) { + if ((n & 1) != 0) { + return -1; + } else { + return 1; + } + } + if (x == 1.0f) + return 1; + + if (n == -2147483648) { + return 0; + } + double mul = 1; + if (n > 0) { + mul = powRecursion(x, n); + } else { + n = -n; + mul = powRecursion(x, n); + mul = 1 / mul; + } + return mul; +} +``` + +时间复杂度:O(log(n))。 + +空间复杂度: + +当然对于这种递归的解法的话,还有一些其他的思路,参考[这里](https://leetcode.com/problems/powx-n/discuss/19546/Short-and-easy-to-understand-solution)。 + +递归思路是下边的样子 + +$$x^n=(x*x)^{n/2}$$ , 对于 n 是偶数的情况。 + +$$x^n=(x*x)^{n/2}*x$$,对于 n 是奇数的情况, + +代码就很好写了。 + +```java +public double powRecursion(double x, int n) { + if (n == 0) { + return 1; + } + //偶数的情况 + if ((n & 1) == 0) { + return powRecursion(x * x, n / 2); + } else { //奇数的情况 + return powRecursion(x * x, n / 2) * x; + } +} + +public double myPow(double x, int n) { + if (x == -1) { + if ((n & 1) != 0) { + return -1; + } else { + return 1; + } + } + if (x == 1.0f) + return 1; + + if (n == -2147483648) { + return 0; + } + double mul = 1; + if (n > 0) { + mul = powRecursion(x, n); + } else { + n = -n; + mul = powRecursion(x, n); + mul = 1 / mul; + } + return mul; +} +``` + +时间复杂度:O(log(n))。 + +空间复杂度: + +# 解法三 迭代 + +这里介绍种全新的解法,开始的时候受前边思路的影响,一直没理解。下午问同学,同学立刻想到了自己在《编程之美》看到的解法,这里分享下。 + +以 x 的 10 次方举例。10 的 2 进制是 1010,然后用 2 进制转 10 进制的方法把它展成 2 的幂次的和。 + +$$x^{10}=x^{(1010)_2}=x^{1*2^3+0*2^2+1*2^1+0*2^0}=x^{1*2^3}*x^{0*2^2}x^{1*2^1}*x^{0*2^0}$$ + +这样话,再看一下下边的图,它们之间的对应关系就出来了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/50_2.jpg) + +2 进制对应 1 0 1 0,我们把对应 1 的项进行累乘就可以了,而要进行累乘的项也是很有规律,前一项是后一项的自乘。$$x^8=x^4*x^4$$。我们可以从最右边一位,开始迭代。看下代码吧。 + +```java +public double myPow(double x, int n) { + if (x == -1) { + if ((n & 1) != 0) { + return -1; + } else { + return 1; + } + } + if (x == 1.0f) + return 1; + + if (n == -2147483648) { + return 0; + } + double mul = 1; + if (n > 0) { + mul = powIteration(x, n); + } else { + n = -n; + mul = powIteration(x, n); + mul = 1 / mul; + } + return mul; +} + +public double powIteration(double x, int n) { + double ans = 1; + //遍历每一位 + while (n > 0) { + //最后一位是 1,加到累乘结果里 + if ((n & 1) == 1) { + ans = ans * x; + } + //更新 x + x = x * x; + //n 右移一位 + n = n >> 1; + } + return ans; +} +``` + +时间复杂度:log(n)。 + +空间复杂度:O(1)。 + +# 更新 + +2020.3.16 更新。感谢 [@为爱卖小菜](https://leetcode-cn.com/u/wei-ai-mai-xiao-cai/) 指出,上边的解法虽然都能 `AC`,但是以上全错,少考虑了一种情况。 + +前边我们分析到 ` -2147483648` 需要单独讨论。 + +> 但这样的话会出问题,之前在 [29题](https://leetcode.windliang.cc/leetCode-29-Divide-Two-Integers.html) 讨论过,问题出在 n = - n 上,因为最小负数 $$-2^{31}$$取相反数的话,按照计算机的规则,依旧是$$-2^{31}$$,所以这种情况需要单独讨论一下。 +> +> ```java +> if (n == -2147483648) { +> return 0; +> } +> ``` + +但当 `n = -2147483648` 个时候,并不是所有的 $$x^n$$ 结果都是 `0`。 + +当 `x` 等于 `-1` 或者 `1` 的时候结果是 `1` 。前边的解法也考虑到了。 + +下边 `x` 等于 `-1` 的时候我们顺便考虑了 `n` 是其他数的情况,所以没直接返回 `1`。 + +```java +if (x == -1) { + if ((n & 1) != 0) { + return -1; + } else { + return 1; + } +} +if (x == 1.0f) + return 1; +``` + +但其实 `x` 是浮点数,我们还少考虑了 `-1` 到 `0` 和 `0` 到 `1` 之间的数,此时的 $$x^n$$ 的结果应该是正无穷。 + +此外 `x == 0` 的话,数学上是不能算的,这里的话也输出正无穷。 + +综上,我们的前置条件如下 + +```java +if (x == -1) { + if ((n & 1) != 0) { + return -1; + } else { + return 1; + } +} +if (x == 1.0f){ + return 1; +} + +if(n == -2147483648){ + if(x > -1 && x < 1 ){ + return Double.POSITIVE_INFINITY; + }else{ + return 0; + } +} +``` + +上边就是当 `n = -2147483648` 的所有情况了。对于 $$x^n$$,`x` 分成了四种情况。 + +当 `x == -1` 结果是 `1`,上边的代码我们顺便把 `n` 是其它数的情况也顺便考虑了。 + +当 `x == 1` 结果是 `1`。 + +当 `-1 < x < 1` ,结果是正无穷。 + +当 `x < -1` 或者 `x > 1` ,结果是 `0`。 + + [@为爱卖小菜](https://leetcode-cn.com/u/wei-ai-mai-xiao-cai/) 也提供了一个新方法,可以把上边的所有情况统一起来。 + +因为当 `n = -2147483648` 的时候我们无法正确计算,我们可以把 $$x^{-2147483648}$$ 分解成 $$x^{-2147483647} * x^{-1}$$ 。这样的话两部分都可以成功求解了。 + +对于解法三,可以改写成下边的样子。其他解法也类似。 + +```java +public double myPow(double x, int n) { + double mul = 1; + if (n > 0) { + mul = powIteration(x, n); + } else { + //单独考虑 n = -2147483648 + if (n == -2147483648) { + return myPow(x, -2147483647) * (1 / x); + } + n = -n; + mul *= powIteration(x, n); + mul = 1 / mul; + } + return mul; +} + +public double powIteration(double x, int n) { + double ans = 1; + //遍历每一位 + while (n > 0) { + //最后一位是 1,加到累乘结果里 + if ((n & 1) == 1) { + ans = ans * x; + } + //更新 x + x = x * x; + //n 右移一位 + n = n >> 1; + } + return ans; +} +``` + + + +# 总 + +从一般的方法,到递归,最后的解法,直接从 2 进制考虑,每一个数字,都可以转换成 2 的幂次的和,从而实现了最终的解法。 + + + + + diff --git a/leetCode-51-N-Queens.md b/leetCode-51-N-Queens.md index c7ef4de8d..277362d92 100644 --- a/leetCode-51-N-Queens.md +++ b/leetCode-51-N-Queens.md @@ -1,79 +1,79 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/51.jpg) - -经典的 N 皇后问题。意思就是摆皇后的位置,每行每列以及对角线只能出现 1 个皇后。输出所有的情况。 - -# 解法一 回溯法 - -比较经典的回溯问题了,我们需要做的就是先在第一行放一个皇后,然后进入回溯,放下一行皇后的位置,一直走下去,如果已经放的皇后的数目等于 n 了,就加到最后的结果中。然后再回到上一行,变化皇后的位置,然后去找其他的解。 - -期间如果遇到当前行所有的位置都不能放皇后了,就再回到上一行,然后变化皇后的位置。再返回到下一行。 - -说起来可能还费力些,直接看代码吧。 - -```java -public List> solveNQueens(int n) { - List> ans = new ArrayList<>(); - backtrack(new ArrayList(), ans, n); - return ans; -} - -private void backtrack(List currentQueen, List> ans, int n) { - // 当前皇后的个数是否等于 n 了,等于的话就加到结果中 - if (currentQueen.size() == n) { - List temp = new ArrayList<>(); - for (int i = 0; i < n; i++) { - char[] t = new char[n]; - Arrays.fill(t, '.'); - t[currentQueen.get(i)] = 'Q'; - temp.add(new String(t)); - } - ans.add(temp); - return; - } - //尝试每一列 - for (int col = 0; col < n; col++) { - //当前列是否冲突 - if (!currentQueen.contains(col)) { - //判断对角线是否冲突 - if (isDiagonalAttack(currentQueen, col)) { - continue; - } - //将当前列的皇后加入 - currentQueen.add(col); - //去考虑下一行的情况 - backtrack(currentQueen, ans, n); - //将当前列的皇后移除,去判断下一列 - //进入这一步就是两种情况,下边的行走不通了回到这里或者就是已经拿到了一个解回到这里 - currentQueen.remove(currentQueen.size() - 1); - } - - } - -} - -private boolean isDiagonalAttack(List currentQueen, int i) { - // TODO Auto-generated method stub - int current_row = currentQueen.size(); - int current_col = i; - //判断每一行的皇后的情况 - for (int row = 0; row < currentQueen.size(); row++) { - //左上角的对角线和右上角的对角线,差要么相等,要么互为相反数,直接写成了绝对值 - if (Math.abs(current_row - row) == Math.abs(current_col - currentQueen.get(row))) { - return true; - } - } - return false; -} -``` - -时间复杂度: - -空间复杂度: - -上边我们只判断了列冲突和对角线冲突,至于行冲突,由于我们采取一行一行加皇后,所以一行只会有一个皇后,不会产生冲突。 - -# 总 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/51.jpg) + +经典的 N 皇后问题。意思就是摆皇后的位置,每行每列以及对角线只能出现 1 个皇后。输出所有的情况。 + +# 解法一 回溯法 + +比较经典的回溯问题了,我们需要做的就是先在第一行放一个皇后,然后进入回溯,放下一行皇后的位置,一直走下去,如果已经放的皇后的数目等于 n 了,就加到最后的结果中。然后再回到上一行,变化皇后的位置,然后去找其他的解。 + +期间如果遇到当前行所有的位置都不能放皇后了,就再回到上一行,然后变化皇后的位置。再返回到下一行。 + +说起来可能还费力些,直接看代码吧。 + +```java +public List> solveNQueens(int n) { + List> ans = new ArrayList<>(); + backtrack(new ArrayList(), ans, n); + return ans; +} + +private void backtrack(List currentQueen, List> ans, int n) { + // 当前皇后的个数是否等于 n 了,等于的话就加到结果中 + if (currentQueen.size() == n) { + List temp = new ArrayList<>(); + for (int i = 0; i < n; i++) { + char[] t = new char[n]; + Arrays.fill(t, '.'); + t[currentQueen.get(i)] = 'Q'; + temp.add(new String(t)); + } + ans.add(temp); + return; + } + //尝试每一列 + for (int col = 0; col < n; col++) { + //当前列是否冲突 + if (!currentQueen.contains(col)) { + //判断对角线是否冲突 + if (isDiagonalAttack(currentQueen, col)) { + continue; + } + //将当前列的皇后加入 + currentQueen.add(col); + //去考虑下一行的情况 + backtrack(currentQueen, ans, n); + //将当前列的皇后移除,去判断下一列 + //进入这一步就是两种情况,下边的行走不通了回到这里或者就是已经拿到了一个解回到这里 + currentQueen.remove(currentQueen.size() - 1); + } + + } + +} + +private boolean isDiagonalAttack(List currentQueen, int i) { + // TODO Auto-generated method stub + int current_row = currentQueen.size(); + int current_col = i; + //判断每一行的皇后的情况 + for (int row = 0; row < currentQueen.size(); row++) { + //左上角的对角线和右上角的对角线,差要么相等,要么互为相反数,直接写成了绝对值 + if (Math.abs(current_row - row) == Math.abs(current_col - currentQueen.get(row))) { + return true; + } + } + return false; +} +``` + +时间复杂度: + +空间复杂度: + +上边我们只判断了列冲突和对角线冲突,至于行冲突,由于我们采取一行一行加皇后,所以一行只会有一个皇后,不会产生冲突。 + +# 总 + 最早接触的一类问题了,学回溯法的话,一般就会以这个为例,所以思路上不会遇到什么困难。 \ No newline at end of file diff --git a/leetCode-52-N-QueensII.md b/leetCode-52-N-QueensII.md index 71a23e294..124e94c13 100644 --- a/leetCode-52-N-QueensII.md +++ b/leetCode-52-N-QueensII.md @@ -1,110 +1,110 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/52.jpg) - -和[上一题](https://leetcode.windliang.cc/leetCode-51-N-Queens.html)一样,只不过这次不需要返回所有结果,只需要返回有多少个解就可以。 - -# 解法一 - -我们直接把上道题的 ans 的 size 返回就可以了,此外 currentQueen.size ( ) == n 的时候,也不用去生成一个解了,直接加一个数字占位。 - -```java -public int totalNQueens(int n) { - List ans = new ArrayList<>(); - backtrack(new ArrayList(), ans, n); - return ans.size(); -} - -private void backtrack(List currentQueen, List ans, int n) { - if (currentQueen.size() == n) { - ans.add(1); - return; - } - for (int col = 0; col < n; col++) { - if (!currentQueen.contains(col)) { - if (isDiagonalAttack(currentQueen, col)) { - continue; - } - currentQueen.add(col); - backtrack(currentQueen, ans, n); - currentQueen.remove(currentQueen.size() - 1); - } - - } - -} - -private boolean isDiagonalAttack(List currentQueen, int i) { - int current_row = currentQueen.size(); - int current_col = i; - for (int row = 0; row < currentQueen.size(); row++) { - if (Math.abs(current_row - row) == Math.abs(current_col - currentQueen.get(row))) { - return true; - } - } - return false; -} -``` - -时间复杂度: - -空间复杂度: - -# 解法二 - -参考[这里](https://leetcode.com/problems/n-queens-ii/discuss/20048/Easiest-Java-Solution-(1ms-98.22))。 - -既然不用返回所有解,那么我们就不需要 currentQueen 来保存当前已加入皇后的位置。只需要一个 bool 型数组,来标记列是否被占有就可以了。 - -由于没有了 currentQueen,所有不能再用之前 isDiagonalAttack 判断对角线冲突的方法了。我们可以观察下,对角线元素的情况。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/52_2.jpg) - -可以发现对于同一条副对角线,row + col 的值是相等的。 - -对于同一条主对角线,row - col 的值是相等的。 - -我们同样可以用一个 bool 型数组,来保存当前对角线是否有元素,把它们相加相减的值作为下标。 - -对于 row - col ,由于出现了负数,所以可以加 1 个 n,由 [ - 3, 3 ] 转换为 [ 1 , 7 ] 。 - -```java -public int totalNQueens(int n) { - List ans = new ArrayList<>(); - boolean[] cols = new boolean[n]; // 列 - boolean[] d1 = new boolean[2 * n]; // 主对角线 - boolean[] d2 = new boolean[2 * n]; // 副对角线 - return backtrack(0, cols, d1, d2, n, 0); -} - -private int backtrack(int row, boolean[] cols, boolean[] d1, boolean[] d2, int n, int count) { - if (row == n) { - count++; - } else { - for (int col = 0; col < n; col++) { - int id1 = row - col + n; //主对角线加 n - int id2 = row + col; - if (cols[col] || d1[id1] || d2[id2]) - continue; - cols[col] = true; - d1[id1] = true; - d2[id2] = true; - count = backtrack(row + 1, cols, d1, d2, n, count); - cols[col] = false; - d1[id1] = false; - d2[id2] = false; - } - - } - return count; -} - -``` - -时间复杂度: - -空间复杂度: - -# 总 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/52.jpg) + +和[上一题](https://leetcode.windliang.cc/leetCode-51-N-Queens.html)一样,只不过这次不需要返回所有结果,只需要返回有多少个解就可以。 + +# 解法一 + +我们直接把上道题的 ans 的 size 返回就可以了,此外 currentQueen.size ( ) == n 的时候,也不用去生成一个解了,直接加一个数字占位。 + +```java +public int totalNQueens(int n) { + List ans = new ArrayList<>(); + backtrack(new ArrayList(), ans, n); + return ans.size(); +} + +private void backtrack(List currentQueen, List ans, int n) { + if (currentQueen.size() == n) { + ans.add(1); + return; + } + for (int col = 0; col < n; col++) { + if (!currentQueen.contains(col)) { + if (isDiagonalAttack(currentQueen, col)) { + continue; + } + currentQueen.add(col); + backtrack(currentQueen, ans, n); + currentQueen.remove(currentQueen.size() - 1); + } + + } + +} + +private boolean isDiagonalAttack(List currentQueen, int i) { + int current_row = currentQueen.size(); + int current_col = i; + for (int row = 0; row < currentQueen.size(); row++) { + if (Math.abs(current_row - row) == Math.abs(current_col - currentQueen.get(row))) { + return true; + } + } + return false; +} +``` + +时间复杂度: + +空间复杂度: + +# 解法二 + +参考[这里](https://leetcode.com/problems/n-queens-ii/discuss/20048/Easiest-Java-Solution-(1ms-98.22))。 + +既然不用返回所有解,那么我们就不需要 currentQueen 来保存当前已加入皇后的位置。只需要一个 bool 型数组,来标记列是否被占有就可以了。 + +由于没有了 currentQueen,所有不能再用之前 isDiagonalAttack 判断对角线冲突的方法了。我们可以观察下,对角线元素的情况。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/52_2.jpg) + +可以发现对于同一条副对角线,row + col 的值是相等的。 + +对于同一条主对角线,row - col 的值是相等的。 + +我们同样可以用一个 bool 型数组,来保存当前对角线是否有元素,把它们相加相减的值作为下标。 + +对于 row - col ,由于出现了负数,所以可以加 1 个 n,由 [ - 3, 3 ] 转换为 [ 1 , 7 ] 。 + +```java +public int totalNQueens(int n) { + List ans = new ArrayList<>(); + boolean[] cols = new boolean[n]; // 列 + boolean[] d1 = new boolean[2 * n]; // 主对角线 + boolean[] d2 = new boolean[2 * n]; // 副对角线 + return backtrack(0, cols, d1, d2, n, 0); +} + +private int backtrack(int row, boolean[] cols, boolean[] d1, boolean[] d2, int n, int count) { + if (row == n) { + count++; + } else { + for (int col = 0; col < n; col++) { + int id1 = row - col + n; //主对角线加 n + int id2 = row + col; + if (cols[col] || d1[id1] || d2[id2]) + continue; + cols[col] = true; + d1[id1] = true; + d2[id2] = true; + count = backtrack(row + 1, cols, d1, d2, n, count); + cols[col] = false; + d1[id1] = false; + d2[id2] = false; + } + + } + return count; +} + +``` + +时间复杂度: + +空间复杂度: + +# 总 + 和上一题相比,通过三个 bool 型数组来标记是否占有,不存储具体的位置,从而解决了这道题。 \ No newline at end of file diff --git a/leetCode-53-Maximum-Subarray.md b/leetCode-53-Maximum-Subarray.md index eff40df7f..4b74b9c06 100644 --- a/leetCode-53-Maximum-Subarray.md +++ b/leetCode-53-Maximum-Subarray.md @@ -1,263 +1,263 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/53.jpg) - -给一个数组,找出一个连续的子数组,长度任意,和最大。 - -# 解法一 动态规划思路一 - -用一个二维数组 dp\[ i \] \[ len \] 表示从下标 i 开始,长度为 len 的子数组的元素和。 - -这样长度是 len + 1 的子数组就可以通过长度是 len 的子数组去求,也就是下边的递推式, - -dp \[ i \] \[ len + 1 \] = dp\[ i \] \[ len \] + nums [ i + len - 1 ]。 - -当然,和[第 5 题](https://leetcode.windliang.cc/leetCode-5-Longest-Palindromic-Substring.html)一样,考虑到求 i + 1 的情况的时候,我们只需要 i 时候的情况,所有我们其实没必要用一个二维数组,直接用一维数组就可以了。 - -```java -public int maxSubArray(int[] nums) { - int n = nums.length; - int[] dp = new int[n]; - int max = Integer.MIN_VALUE; - for (int len = 1; len <= n; len++) { - for (int i = 0; i <= n - len; i++) { - //直接覆盖掉前边对应的情况就行 - dp[i] = dp[i] + nums[i + len - 1]; - //更新 max - if (dp[i] > max) { - max = dp[i]; - } - } - } - return max; -} -``` - -时间复杂度:O(n²)。 - -空间复杂度:O(n)。 - -# 解法二 动态规划思路二 - -参考[这里](https://leetcode.com/problems/maximum-subarray/discuss/20193/DP-solution-and-some-thoughts)。 - -用一个一维数组 dp [ i ] 表示以下标 i 结尾的子数组的元素的最大的和,也就是这个子数组最后一个元素是下边为 i 的元素,并且这个子数组是所有以 i 结尾的子数组中,和最大的。 - -这样的话就有两种情况, - -* 如果 dp [ i - 1 ] < 0,那么 dp [ i ] = nums [ i ]。 -* 如果 dp [ i - 1 ] >= 0,那么 dp [ i ] = dp [ i - 1 ] + nums [ i ]。 - -```java -public int maxSubArray(int[] nums) { - int n = nums.length; - int[] dp = new int[n]; - int max = nums[0]; - dp[0] = nums[0]; - for (int i = 1; i < n; i++) { - //两种情况更新 dp[i] - if (dp[i - 1] < 0) { - dp[i] = nums[i]; - } else { - dp[i] = dp[i - 1] + nums[i]; - } - //更新 max - max = Math.max(max, dp[i]); - } - return max; -} -``` - -时间复杂度: O(n)。 - -空间复杂度:O(n)。 - -当然,和以前一样,我们注意到更新 i 的情况的时候只用到 i - 1 的时候,所以我们不需要数组,只需要两个变量。 - -```java -public int maxSubArray(int[] nums) { - int n = nums.length; - //两个变量即可 - int[] dp = new int[2]; - int max = nums[0]; - dp[0] = nums[0]; - for (int i = 1; i < n; i++) { - //利用求余,轮换两个变量 - if (dp[(i - 1) % 2] < 0) { - dp[i % 2] = nums[i]; - } else { - dp[i % 2] = dp[(i - 1) % 2] + nums[i]; - } - max = Math.max(max, dp[i % 2]); - } - return max; -} -``` - -时间复杂度: O(n)。 - -空间复杂度:O(1)。 - - 再粗暴点,直接用一个变量就可以了。 - -```java -public int maxSubArray(int[] nums) { - int n = nums.length; - int dp = nums[0]; - int max = nums[0]; - for (int i = 1; i < n; i++) { - if (dp < 0) { - dp = nums[i]; - } else { - dp= dp + nums[i]; - } - max = Math.max(max, dp); - } - return max; -} -``` - -而对于 - -```java -if (dp < 0) { - dp = nums[i]; -} else { - dp= dp + nums[i]; -} -``` - -其实也可以这样理解, - -```java -dp= Math.max(dp + nums[i],nums[i]); -``` - -然后就变成了[这里](https://leetcode.com/problems/maximum-subarray/discuss/20211/Accepted-O(n)-solution-in-java)提到的算法。 - -# 解法三 折半 - -题目最后说 - -> If you have figured out the O(*n*) solution, try coding another solution using the divide and conquer approach, which is more subtle. - -[这里](If you have figured out the O(*n*) solution, try coding another solution using the divide and conquer approach, which is more subtle.)找到了种解法,分享下。 - -假设我们有了一个函数 int getSubMax(int start, int end, int[] nums) ,可以得到 num [ start, end ) (左包右不包) 中子数组最大值。 - -如果, start == end,那么 getSubMax 直接返回 nums [ start ] 就可以了。 - -```java -if (start == end) { - return nums[start]; -} -``` - -然后对问题进行分解。 - -先找一个 mid , mid = ( start + end ) / 2。 - -然后,对于我们要找的和最大的子数组有两种情况。 - -* mid 不在我们要找的子数组中 - - 这样的话,子数组的最大值要么是 mid 左半部分数组的子数组产生,要么是右边的产生,最大值的可以利用 getSubMax 求出来。 - - ```java - int leftMax = getSubMax(start, mid, nums); - int rightMax = getSubMax(mid + 1, end, nums); - ``` - -* mid 在我们要找的子数组中 - - 这样的话,我们可以分别从 mid 左边扩展,和右边扩展,找出两边和最大的时候,然后加起来就可以了。当然如果,左边或者右边最大的都小于 0 ,我们就不加了。 - - ```java - int containsMidMax = getContainMidMax(start, end, mid, nums); - private int getContainMidMax(int start, int end, int mid, int[] nums) { - int containsMidLeftMax = 0; //初始化为 0 ,防止最大的值也小于 0 - //找左边最大 - if (mid > 0) { - int sum = 0; - for (int i = mid - 1; i >= 0; i--) { - sum += nums[i]; - if (sum > containsMidLeftMax) { - containsMidLeftMax = sum; - } - } - - } - int containsMidRightMax = 0; - //找右边最大 - if (mid < end) { - int sum = 0; - for (int i = mid + 1; i <= end; i++) { - sum += nums[i]; - if (sum > containsMidRightMax) { - containsMidRightMax = sum; - } - } - } - return containsMidLeftMax + nums[mid] + containsMidRightMax; - } - ``` - - 最后,我们只需要返回这三个中最大的值就可以了。 - -综上,递归出口,问题分解就都有了。 - -```java -public int maxSubArray(int[] nums) { - return getSubMax(0, nums.length - 1, nums); -} - -private int getSubMax(int start, int end, int[] nums) { - //递归出口 - if (start == end) { - return nums[start]; - } - int mid = (start + end) / 2; - //要找的数组不包含 mid,然后得到左边和右边最大的值 - int leftMax = getSubMax(start, mid, nums); - int rightMax = getSubMax(mid + 1, end, nums); - //要找的数组包含 mid - int containsMidMax = getContainMidMax(start, end, mid, nums); - //返回它们 3 个中最大的 - return Math.max(containsMidMax, Math.max(leftMax, rightMax)); -} - -private int getContainMidMax(int start, int end, int mid, int[] nums) { - int containsMidLeftMax = 0; //初始化为 0 ,防止最大的值也小于 0 - //找左边最大 - if (mid > 0) { - int sum = 0; - for (int i = mid - 1; i >= 0; i--) { - sum += nums[i]; - if (sum > containsMidLeftMax) { - containsMidLeftMax = sum; - } - } - - } - int containsMidRightMax = 0; - //找右边最大 - if (mid < end) { - int sum = 0; - for (int i = mid + 1; i <= end; i++) { - sum += nums[i]; - if (sum > containsMidRightMax) { - containsMidRightMax = sum; - } - } - } - return containsMidLeftMax + nums[mid] + containsMidRightMax; -} -``` - -时间复杂度:O(n log ( n ))。由于 getContainMidMax 这个函数耗费了 O(n)。所以时间复杂度反而相比之前的算法变大了。 - -空间复杂度: - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/53.jpg) + +给一个数组,找出一个连续的子数组,长度任意,和最大。 + +# 解法一 动态规划思路一 + +用一个二维数组 dp\[ i \] \[ len \] 表示从下标 i 开始,长度为 len 的子数组的元素和。 + +这样长度是 len + 1 的子数组就可以通过长度是 len 的子数组去求,也就是下边的递推式, + +dp \[ i \] \[ len + 1 \] = dp\[ i \] \[ len \] + nums [ i + len - 1 ]。 + +当然,和[第 5 题](https://leetcode.windliang.cc/leetCode-5-Longest-Palindromic-Substring.html)一样,考虑到求 i + 1 的情况的时候,我们只需要 i 时候的情况,所有我们其实没必要用一个二维数组,直接用一维数组就可以了。 + +```java +public int maxSubArray(int[] nums) { + int n = nums.length; + int[] dp = new int[n]; + int max = Integer.MIN_VALUE; + for (int len = 1; len <= n; len++) { + for (int i = 0; i <= n - len; i++) { + //直接覆盖掉前边对应的情况就行 + dp[i] = dp[i] + nums[i + len - 1]; + //更新 max + if (dp[i] > max) { + max = dp[i]; + } + } + } + return max; +} +``` + +时间复杂度:O(n²)。 + +空间复杂度:O(n)。 + +# 解法二 动态规划思路二 + +参考[这里](https://leetcode.com/problems/maximum-subarray/discuss/20193/DP-solution-and-some-thoughts)。 + +用一个一维数组 dp [ i ] 表示以下标 i 结尾的子数组的元素的最大的和,也就是这个子数组最后一个元素是下边为 i 的元素,并且这个子数组是所有以 i 结尾的子数组中,和最大的。 + +这样的话就有两种情况, + +* 如果 dp [ i - 1 ] < 0,那么 dp [ i ] = nums [ i ]。 +* 如果 dp [ i - 1 ] >= 0,那么 dp [ i ] = dp [ i - 1 ] + nums [ i ]。 + +```java +public int maxSubArray(int[] nums) { + int n = nums.length; + int[] dp = new int[n]; + int max = nums[0]; + dp[0] = nums[0]; + for (int i = 1; i < n; i++) { + //两种情况更新 dp[i] + if (dp[i - 1] < 0) { + dp[i] = nums[i]; + } else { + dp[i] = dp[i - 1] + nums[i]; + } + //更新 max + max = Math.max(max, dp[i]); + } + return max; +} +``` + +时间复杂度: O(n)。 + +空间复杂度:O(n)。 + +当然,和以前一样,我们注意到更新 i 的情况的时候只用到 i - 1 的时候,所以我们不需要数组,只需要两个变量。 + +```java +public int maxSubArray(int[] nums) { + int n = nums.length; + //两个变量即可 + int[] dp = new int[2]; + int max = nums[0]; + dp[0] = nums[0]; + for (int i = 1; i < n; i++) { + //利用求余,轮换两个变量 + if (dp[(i - 1) % 2] < 0) { + dp[i % 2] = nums[i]; + } else { + dp[i % 2] = dp[(i - 1) % 2] + nums[i]; + } + max = Math.max(max, dp[i % 2]); + } + return max; +} +``` + +时间复杂度: O(n)。 + +空间复杂度:O(1)。 + + 再粗暴点,直接用一个变量就可以了。 + +```java +public int maxSubArray(int[] nums) { + int n = nums.length; + int dp = nums[0]; + int max = nums[0]; + for (int i = 1; i < n; i++) { + if (dp < 0) { + dp = nums[i]; + } else { + dp= dp + nums[i]; + } + max = Math.max(max, dp); + } + return max; +} +``` + +而对于 + +```java +if (dp < 0) { + dp = nums[i]; +} else { + dp= dp + nums[i]; +} +``` + +其实也可以这样理解, + +```java +dp= Math.max(dp + nums[i],nums[i]); +``` + +然后就变成了[这里](https://leetcode.com/problems/maximum-subarray/discuss/20211/Accepted-O(n)-solution-in-java)提到的算法。 + +# 解法三 折半 + +题目最后说 + +> If you have figured out the O(*n*) solution, try coding another solution using the divide and conquer approach, which is more subtle. + +[这里](If you have figured out the O(*n*) solution, try coding another solution using the divide and conquer approach, which is more subtle.)找到了种解法,分享下。 + +假设我们有了一个函数 int getSubMax(int start, int end, int[] nums) ,可以得到 num [ start, end ) (左包右不包) 中子数组最大值。 + +如果, start == end,那么 getSubMax 直接返回 nums [ start ] 就可以了。 + +```java +if (start == end) { + return nums[start]; +} +``` + +然后对问题进行分解。 + +先找一个 mid , mid = ( start + end ) / 2。 + +然后,对于我们要找的和最大的子数组有两种情况。 + +* mid 不在我们要找的子数组中 + + 这样的话,子数组的最大值要么是 mid 左半部分数组的子数组产生,要么是右边的产生,最大值的可以利用 getSubMax 求出来。 + + ```java + int leftMax = getSubMax(start, mid, nums); + int rightMax = getSubMax(mid + 1, end, nums); + ``` + +* mid 在我们要找的子数组中 + + 这样的话,我们可以分别从 mid 左边扩展,和右边扩展,找出两边和最大的时候,然后加起来就可以了。当然如果,左边或者右边最大的都小于 0 ,我们就不加了。 + + ```java + int containsMidMax = getContainMidMax(start, end, mid, nums); + private int getContainMidMax(int start, int end, int mid, int[] nums) { + int containsMidLeftMax = 0; //初始化为 0 ,防止最大的值也小于 0 + //找左边最大 + if (mid > 0) { + int sum = 0; + for (int i = mid - 1; i >= 0; i--) { + sum += nums[i]; + if (sum > containsMidLeftMax) { + containsMidLeftMax = sum; + } + } + + } + int containsMidRightMax = 0; + //找右边最大 + if (mid < end) { + int sum = 0; + for (int i = mid + 1; i <= end; i++) { + sum += nums[i]; + if (sum > containsMidRightMax) { + containsMidRightMax = sum; + } + } + } + return containsMidLeftMax + nums[mid] + containsMidRightMax; + } + ``` + + 最后,我们只需要返回这三个中最大的值就可以了。 + +综上,递归出口,问题分解就都有了。 + +```java +public int maxSubArray(int[] nums) { + return getSubMax(0, nums.length - 1, nums); +} + +private int getSubMax(int start, int end, int[] nums) { + //递归出口 + if (start == end) { + return nums[start]; + } + int mid = (start + end) / 2; + //要找的数组不包含 mid,然后得到左边和右边最大的值 + int leftMax = getSubMax(start, mid, nums); + int rightMax = getSubMax(mid + 1, end, nums); + //要找的数组包含 mid + int containsMidMax = getContainMidMax(start, end, mid, nums); + //返回它们 3 个中最大的 + return Math.max(containsMidMax, Math.max(leftMax, rightMax)); +} + +private int getContainMidMax(int start, int end, int mid, int[] nums) { + int containsMidLeftMax = 0; //初始化为 0 ,防止最大的值也小于 0 + //找左边最大 + if (mid > 0) { + int sum = 0; + for (int i = mid - 1; i >= 0; i--) { + sum += nums[i]; + if (sum > containsMidLeftMax) { + containsMidLeftMax = sum; + } + } + + } + int containsMidRightMax = 0; + //找右边最大 + if (mid < end) { + int sum = 0; + for (int i = mid + 1; i <= end; i++) { + sum += nums[i]; + if (sum > containsMidRightMax) { + containsMidRightMax = sum; + } + } + } + return containsMidLeftMax + nums[mid] + containsMidRightMax; +} +``` + +时间复杂度:O(n log ( n ))。由于 getContainMidMax 这个函数耗费了 O(n)。所以时间复杂度反而相比之前的算法变大了。 + +空间复杂度: + +# 总 + 解法一和解法二的动态规划,只是在定义的时候一个表示以 i 开头的子数组,一个表示以 i 结尾的子数组,却造成了时间复杂度的差异。问题就是解法一中求出了太多的没必要的和,不如解法二直接,只保存最大的和。解法三,一半一半的求,从而使问题分解,也是经常遇到的思想。 \ No newline at end of file diff --git a/leetCode-54-Spiral-Matrix.md b/leetCode-54-Spiral-Matrix.md index ff13f925e..1a8c15c43 100644 --- a/leetCode-54-Spiral-Matrix.md +++ b/leetCode-54-Spiral-Matrix.md @@ -1,91 +1,91 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/54.jpg) - -从第一个位置开始,螺旋状遍历二维矩阵。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/54_2.jpg) - -# 解法一 - -可以理解成贪吃蛇,从第一个位置开始沿着边界走,遇到边界就转换方向接着走,直到走完所有位置。 - -```java -/* - * direction 0 代表向右, 1 代表向下, 2 代表向左, 3 代表向上 -*/ -public List spiralOrder(int[][] matrix) { - List ans = new ArrayList<>(); - if(matrix.length == 0){ - return ans; - } - int start_x = 0, - start_y = 0, - direction = 0, - top_border = -1, //上边界 - right_border = matrix[0].length, //右边界 - bottom_border = matrix.length, //下边界 - left_border = -1; //左边界 - while(true){ - //全部遍历完结束 - if (ans.size() == matrix.length * matrix[0].length) { - return ans; - } - //注意 y 方向写在前边,x 方向写在后边 - ans.add(matrix[start_y][start_x]); - switch (direction) { - //当前向右 - case 0: - //继续向右是否到达边界 - //到达边界就改变方向,并且更新上边界 - if (start_x + 1 == right_border) { - direction = 1; - start_y += 1; - top_border += 1; - } else { - start_x += 1; - } - break; - //当前向下 - case 1: - //继续向下是否到达边界 - //到达边界就改变方向,并且更新右边界 - if (start_y + 1 == bottom_border) { - direction = 2; - start_x -= 1; - right_border -= 1; - } else { - start_y += 1; - } - break; - case 2: - if (start_x - 1 == left_border) { - direction = 3; - start_y -= 1; - bottom_border -= 1; - } else { - start_x -= 1; - } - break; - case 3: - if (start_y - 1 == top_border) { - direction = 0; - start_x += 1; - left_border += 1; - } else { - start_y -= 1; - } - break; - } - } - -} -``` - -时间复杂度:O(m * n),m 和 n 是数组的长宽。 - -空间复杂度:O(1)。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/54.jpg) + +从第一个位置开始,螺旋状遍历二维矩阵。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/54_2.jpg) + +# 解法一 + +可以理解成贪吃蛇,从第一个位置开始沿着边界走,遇到边界就转换方向接着走,直到走完所有位置。 + +```java +/* + * direction 0 代表向右, 1 代表向下, 2 代表向左, 3 代表向上 +*/ +public List spiralOrder(int[][] matrix) { + List ans = new ArrayList<>(); + if(matrix.length == 0){ + return ans; + } + int start_x = 0, + start_y = 0, + direction = 0, + top_border = -1, //上边界 + right_border = matrix[0].length, //右边界 + bottom_border = matrix.length, //下边界 + left_border = -1; //左边界 + while(true){ + //全部遍历完结束 + if (ans.size() == matrix.length * matrix[0].length) { + return ans; + } + //注意 y 方向写在前边,x 方向写在后边 + ans.add(matrix[start_y][start_x]); + switch (direction) { + //当前向右 + case 0: + //继续向右是否到达边界 + //到达边界就改变方向,并且更新上边界 + if (start_x + 1 == right_border) { + direction = 1; + start_y += 1; + top_border += 1; + } else { + start_x += 1; + } + break; + //当前向下 + case 1: + //继续向下是否到达边界 + //到达边界就改变方向,并且更新右边界 + if (start_y + 1 == bottom_border) { + direction = 2; + start_x -= 1; + right_border -= 1; + } else { + start_y += 1; + } + break; + case 2: + if (start_x - 1 == left_border) { + direction = 3; + start_y -= 1; + bottom_border -= 1; + } else { + start_x -= 1; + } + break; + case 3: + if (start_y - 1 == top_border) { + direction = 0; + start_x += 1; + left_border += 1; + } else { + start_y -= 1; + } + break; + } + } + +} +``` + +时间复杂度:O(m * n),m 和 n 是数组的长宽。 + +空间复杂度:O(1)。 + +# 总 + 在 leetcode 的 solution 和 discuss 看了下,基本就是这个思路了,只是实现上有些不同,怎么用来标记是否走过,当前方向,怎么遍历,实现有些不同,但本质上是一样的。就是充分理解题意,然后模仿遍历的过程。 \ No newline at end of file diff --git a/leetCode-55-Jump-Game.md b/leetCode-55-Jump-Game.md index c7bbebfa4..a467e0355 100644 --- a/leetCode-55-Jump-Game.md +++ b/leetCode-55-Jump-Game.md @@ -1,179 +1,179 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/55.jpg) - -[45题](https://leetcode.windliang.cc/leetCode-45-Jump-Game-II.html)的时候已经见过这道题了,只不过之前是返回从第 0 个位置能跳到最后一个位置的最小步数,这道题是返回是否能跳过去。 - -leetCode [Solution](https://leetcode.com/problems/jump-game/solution/) 中给出的是动态规划的解法,进行了一步一步的优化,但都也比较慢。不过,思路还是值得参考的,上边说的比较详细,这里就不啰嗦了。这里,由于受到 45 题的影响,自己对 45 题的解法改写了一下,从而解决了这个问题。 - -下边的解法都是基于[45题](https://leetcode.windliang.cc/leetCode-45-Jump-Game-II.html) 的想法,大家可以先过去看一下,懂了之后再回到下边来看。 - -# 解法一 顺藤摸瓜 - -45 题的代码。 - -```java -public int jump(int[] nums) { - int end = 0; - int maxPosition = 0; - int steps = 0; - for(int i = 0; i < nums.length - 1; i++){ - //找能跳的最远的 - maxPosition = Math.max(maxPosition, nums[i] + i); - if( i == end){ //遇到边界,就更新边界,并且步数加一 - end = maxPosition; - steps++; - } - } - return steps; -} -``` - -这里的话,我们完全可以把 step 去掉,并且考虑下当前更新的 i 是不是已经超过了边界。 - -```java -public boolean canJump(int[] nums) { - int end = 0; - int maxPosition = 0; - for(int i = 0; i < nums.length - 1; i++){ - //当前更新超过了边界,那么意味着出现了 0 ,直接返回 false - if(end < i){ - return false; - } - //找能跳的最远的 - maxPosition = Math.max(maxPosition, nums[i] + i); - - if( i == end){ //遇到边界,就更新边界,并且步数加一 - end = maxPosition; - } - } - //最远的距离是否到答末尾 - return maxPosition>=nums.length-1; -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(1)。 - -# 解法二 顺瓜摸藤 - -每次找最左边的能跳到当前位置的下标,之前的代码如下。 - -```java -public int jump(int[] nums) { - int position = nums.length - 1; //要找的位置 - int steps = 0; - while (position != 0) { //是否到了第 0 个位置 - for (int i = 0; i < position; i++) { - if (nums[i] >= position - i) { - position = i; //更新要找的位置 - steps++; - break; - } - } - } - return steps; -} - -``` - -这里修改的话,只需要判断最后回没回到 0 ,并且如果 while 里的 for 循环没有进入 if ,就意味着一个位置都没找到,就要返回 false。 - -```java -public boolean canJump(int[] nums) { - int position = nums.length - 1; //要找的位置 - boolean isUpdate = false; - while (position != 0) { //是否到了第 0 个位置 - isUpdate = false; - for (int i = 0; i < position; i++) { - if (nums[i] >= position - i) { - position = i; //更新要找的位置 - isUpdate = true; - break; - } - } - //如果没有进入 for 循环中的 if 语句,就返回 false - if(!isUpdate){ - return false; - } - } - return true; -} -``` - -时间复杂度:O(n²)。 - -空间复杂度:O(1)。 - -# 解法三 - -让我们直击问题的本质,与 45 题不同,我们并不需要知道最小的步数,所以我们对跳的过程并不感兴趣。并且如果数组里边没有 0,那么无论怎么跳,一定可以从第 0 个跳到最后一个位置。 - -所以我们只需要看 0 的位置,如果有 0 的话,我们只需要看 0 前边的位置,能不能跳过当前的 0 ,如果 0 前边的位置都不能跳过当前 0,那么直接返回 false。如果能的话,就看后边的 0 的情况。 - -```java -public boolean canJump(int[] nums) { - for (int i = 0; i < nums.length - 1; i++) { - //找到 0 的位置 - if (nums[i] == 0) { - int j = i - 1; - boolean isCanSkipZero = false; - while (j >= 0) { - //判断 0 前边的元素能否跳过 0 - if (j + nums[j] > i) { - isCanSkipZero = true; - break; - } - j--; - } - if (!isCanSkipZero) { - return false; - } - } - } - return true; -} -``` - -但这样时间复杂度没有提高, 在 @Zhengwen 的提醒下,可以用下边的方法。 - -我们判断 0 前边的元素能否跳过 0 ,不需要每次都向前查找,我们只需要用一个变量保存当前能跳的最远的距离,然后判断最远距离和当前 0 的位置就可以了。 - -```java -public boolean canJump(int[] nums) { - int max = 0; - for (int i = 0; i < nums.length - 1; i++) { - if (nums[i] == 0 && i >= max) { - return false; - } - max = Math.max(max, nums[i] + i); - } - return true; -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(1)。 - -参考[这里](),我们甚至不需要考虑 0 的位置,只需要判断最大距离有没有超过当前的 i 。 - -```java -public boolean canJump(int[] nums) { - int max = 0; - for (int i = 0; i < nums.length; i++) { - if (i > max) { - return false; - } - max = Math.max(max, nums[i] + i); - } - return true; -} -``` - - - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/55.jpg) + +[45题](https://leetcode.windliang.cc/leetCode-45-Jump-Game-II.html)的时候已经见过这道题了,只不过之前是返回从第 0 个位置能跳到最后一个位置的最小步数,这道题是返回是否能跳过去。 + +leetCode [Solution](https://leetcode.com/problems/jump-game/solution/) 中给出的是动态规划的解法,进行了一步一步的优化,但都也比较慢。不过,思路还是值得参考的,上边说的比较详细,这里就不啰嗦了。这里,由于受到 45 题的影响,自己对 45 题的解法改写了一下,从而解决了这个问题。 + +下边的解法都是基于[45题](https://leetcode.windliang.cc/leetCode-45-Jump-Game-II.html) 的想法,大家可以先过去看一下,懂了之后再回到下边来看。 + +# 解法一 顺藤摸瓜 + +45 题的代码。 + +```java +public int jump(int[] nums) { + int end = 0; + int maxPosition = 0; + int steps = 0; + for(int i = 0; i < nums.length - 1; i++){ + //找能跳的最远的 + maxPosition = Math.max(maxPosition, nums[i] + i); + if( i == end){ //遇到边界,就更新边界,并且步数加一 + end = maxPosition; + steps++; + } + } + return steps; +} +``` + +这里的话,我们完全可以把 step 去掉,并且考虑下当前更新的 i 是不是已经超过了边界。 + +```java +public boolean canJump(int[] nums) { + int end = 0; + int maxPosition = 0; + for(int i = 0; i < nums.length - 1; i++){ + //当前更新超过了边界,那么意味着出现了 0 ,直接返回 false + if(end < i){ + return false; + } + //找能跳的最远的 + maxPosition = Math.max(maxPosition, nums[i] + i); + + if( i == end){ //遇到边界,就更新边界,并且步数加一 + end = maxPosition; + } + } + //最远的距离是否到答末尾 + return maxPosition>=nums.length-1; +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(1)。 + +# 解法二 顺瓜摸藤 + +每次找最左边的能跳到当前位置的下标,之前的代码如下。 + +```java +public int jump(int[] nums) { + int position = nums.length - 1; //要找的位置 + int steps = 0; + while (position != 0) { //是否到了第 0 个位置 + for (int i = 0; i < position; i++) { + if (nums[i] >= position - i) { + position = i; //更新要找的位置 + steps++; + break; + } + } + } + return steps; +} + +``` + +这里修改的话,只需要判断最后回没回到 0 ,并且如果 while 里的 for 循环没有进入 if ,就意味着一个位置都没找到,就要返回 false。 + +```java +public boolean canJump(int[] nums) { + int position = nums.length - 1; //要找的位置 + boolean isUpdate = false; + while (position != 0) { //是否到了第 0 个位置 + isUpdate = false; + for (int i = 0; i < position; i++) { + if (nums[i] >= position - i) { + position = i; //更新要找的位置 + isUpdate = true; + break; + } + } + //如果没有进入 for 循环中的 if 语句,就返回 false + if(!isUpdate){ + return false; + } + } + return true; +} +``` + +时间复杂度:O(n²)。 + +空间复杂度:O(1)。 + +# 解法三 + +让我们直击问题的本质,与 45 题不同,我们并不需要知道最小的步数,所以我们对跳的过程并不感兴趣。并且如果数组里边没有 0,那么无论怎么跳,一定可以从第 0 个跳到最后一个位置。 + +所以我们只需要看 0 的位置,如果有 0 的话,我们只需要看 0 前边的位置,能不能跳过当前的 0 ,如果 0 前边的位置都不能跳过当前 0,那么直接返回 false。如果能的话,就看后边的 0 的情况。 + +```java +public boolean canJump(int[] nums) { + for (int i = 0; i < nums.length - 1; i++) { + //找到 0 的位置 + if (nums[i] == 0) { + int j = i - 1; + boolean isCanSkipZero = false; + while (j >= 0) { + //判断 0 前边的元素能否跳过 0 + if (j + nums[j] > i) { + isCanSkipZero = true; + break; + } + j--; + } + if (!isCanSkipZero) { + return false; + } + } + } + return true; +} +``` + +但这样时间复杂度没有提高, 在 @Zhengwen 的提醒下,可以用下边的方法。 + +我们判断 0 前边的元素能否跳过 0 ,不需要每次都向前查找,我们只需要用一个变量保存当前能跳的最远的距离,然后判断最远距离和当前 0 的位置就可以了。 + +```java +public boolean canJump(int[] nums) { + int max = 0; + for (int i = 0; i < nums.length - 1; i++) { + if (nums[i] == 0 && i >= max) { + return false; + } + max = Math.max(max, nums[i] + i); + } + return true; +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(1)。 + +参考[这里](),我们甚至不需要考虑 0 的位置,只需要判断最大距离有没有超过当前的 i 。 + +```java +public boolean canJump(int[] nums) { + int max = 0; + for (int i = 0; i < nums.length; i++) { + if (i > max) { + return false; + } + max = Math.max(max, nums[i] + i); + } + return true; +} +``` + + + +# 总 + 当自己按照 45 题的思路写完的时候,看 Solution 的时候都懵逼了,这道题竟然这么复杂?不过 Solution 把问题抽象成动态规划的思想,以及优化的过程还是非常值得学习的。 \ No newline at end of file diff --git a/leetCode-56-Merge-Intervals.md b/leetCode-56-Merge-Intervals.md index f54e99109..6c1372499 100644 --- a/leetCode-56-Merge-Intervals.md +++ b/leetCode-56-Merge-Intervals.md @@ -1,318 +1,318 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/56.jpg) - -给定一个列表,将有重叠部分的合并。例如\[ [ 1 3 ] \[ 2 6 \] ] 合并成 [ 1 6 ] 。 - -# 解法一 - -常规的思想,将大问题化解成小问题去解决。 - -假设给了一个大小为 n 的列表,然后我们假设 n - 1 个元素的列表已经完成了全部合并,我们现在要解决的就是剩下的 1 个,怎么加到已经合并完的 n -1 个元素中。 - -这样的话分下边几种情况, 我们把每个范围叫做一个节点,节点包括左端点和右端点。 - -1. 如下图,新加入的节点左端点和右端点,分别在两个节点之间。这样,我们只要删除这两个节点,并且使用左边节点的左端点,右边的节点的右端点作为一个新节点插入即可。也就是删除 [ 1 6 ] 和 [ 8 12 ] ,加入 [ 1 12 ] 到合并好的列表中。 - - ![ ](https://windliang.oss-cn-beijing.aliyuncs.com/56_2.jpg) - -2. 如下图,新加入的节点只有左端点在之前的一个节点之内,这样的话将这个节点删除,使用删除的节点的左端点,新加入的节点的右端点,作为新的节点插入即可。也就是删除 [ 1 6 ],加入 [ 1 7 ] 到合并好的列表中。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/56_3.jpg) - -3. 如下图,新加入的节点只有右端点在之前的一个节点之内,这样的话将这个节点删除,使用删除的节点的右端点,新加入的节点的左端点,作为新的节点插入即可。也就是删除 [ 8 12 ],加入 [ 7 12 ] 到合并好的列表中。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/56_4.jpg) - -4. 如下图,新加入的节点的左端点和右端点在之前的一个节点之内,这样的话新加入的节点舍弃就可以了。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/56_5.jpg) - -5. 如下图,新加入的节点没有在任何一个节点之内,那么将它直接作为新的节点加入到合并好的节点之内就可以了。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/56_6.jpg) - -6. 如下图,还有一种情况,就是新加入的节点两个端点,将之前的节点囊括其中,这种的话,我们只需要将囊括的节点删除,把新节点加入即可。把 [ 8 12 ] 删除,将 7 13 加入即可。并且,新加入的节点可能会囊括多个旧节点,比如新加入的节点是 [ 1 100 ],那么下边的三个节点就都包括了,就需要都删除掉。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/56_7.jpg) - -以上就是所有的情况了,可以开始写代码了。 - -```java -public class Interval { - int start; - int end; - - Interval() { - start = 0; - end = 0; - } - - Interval(int s, int e) { - start = s; - end = e; - } -} - -public List merge(List intervals) { - List ans = new ArrayList<>(); - if (intervals.size() == 0) - return ans; - //将第一个节点加入,作为合并好的节点列表 - ans.add(new Interval(intervals.get(0).start, intervals.get(0).end)); - //遍历其他的每一个节点 - for (int i = 1; i < intervals.size(); i++) { - Interval start = null; - Interval end = null; - //新加入节点的左端点 - int i_start = intervals.get(i).start; - //新加入节点的右端点 - int i_end = intervals.get(i).end; - int size = ans.size(); - //情况 6,保存囊括的节点,便于删除 - List in = new ArrayList<>(); - //遍历合并好的每一个节点 - for (int j = 0; j < size; j++) { - //找到左端点在哪个节点内 - if (i_start >= ans.get(j).start && i_start <= ans.get(j).end) { - start = ans.get(j); - } - //找到右端点在哪个节点内 - if (i_end >= ans.get(j).start && i_end <= ans.get(j).end) { - end = ans.get(j); - } - //判断新加入的节点是否囊括当前旧节点,对应情况 6 - if (i_start < ans.get(j).start && i_end >ans.get(j).end) { - in.add(ans.get(j)); - } - - } - //删除囊括的节点 - if (in.size() != 0) { - for (int index = 0; index < in.size(); index++) { - ans.remove(in.get(index)); - } - } - //equals 函数作用是在 start 和 end 有且只有一个 null,或者 start 和 end 是同一个节点返回 true,相当于情况 2 3 4 中的一种 - if (equals(start, end)) { - //如果 start 和 end 都不等于 null 就代表情况 4 - - // start 等于 null 的话相当于情况 3 - int s = start == null ? i_start : start.start; - // end 等于 null 的话相当于情况 2 - int e = end == null ? i_end : end.end; - ans.add(new Interval(s, e)); - // start 和 end 不是同一个节点,相当于情况 1 - } else if (start!= null && end!=null) { - ans.add(new Interval(start.start, end.end)); - // start 和 end 都为 null,相当于情况 5 和 情况 6 ,加入新节点 - }else if (start == null) { - ans.add(new Interval(i_start, i_end)); - } - //将旧节点删除 - if (start != null) { - ans.remove(start); - } - if (end != null) { - ans.remove(end); - } - - } - return ans; -} - -private boolean equals(Interval start, Interval end) { - if (start == null && end == null) { - return false; - } - if (start == null || end == null) { - return true; - } - if (start.start == end.start && start.end == end.end) { - return true; - } - return false; -} - -``` - -时间复杂度:O(n²)。 - -空间复杂度:O (n),用来存储结果。 - -# 解法二 - -参考[这里](https://leetcode.com/articles/merge-intervals/)的解法二。 - -在解法一中,我们每次对于新加入的节点,都用一个 for 循环去遍历已经合并好的列表。如果我们把之前的列表,按照左端点进行从小到大排序了。这样的话,每次添加新节点的话,我们只需要和合并好的列表最后一个节点对比就可以了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/56_8.jpg) - -排好序后我们只需要把新加入的节点和最后一个节点比较就够了。 - -情况 1,如果新加入的节点的左端点大于合并好的节点列表的最后一个节点的右端点,那么我们只需要把新节点直接加入就可以了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/56_9.jpg) - -情况 2 ,如果新加入的节点的左端点不大于合并好的节点列表的最后一个节点的右端点,那么只需要判断新加入的节点的右端点和最后一个节点的右端点哪个大,然后更新最后一个节点的右端点就可以了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/56_10.jpg) - -```java -private class IntervalComparator implements Comparator { - @Override - public int compare(Interval a, Interval b) { - return a.start < b.start ? -1 : a.start == b.start ? 0 : 1; - } -} - -public List merge(List intervals) { - Collections.sort(intervals, new IntervalComparator()); - - LinkedList merged = new LinkedList(); - for (Interval interval : intervals) { - //最开始是空的,直接加入 - //然后对应情况 1,新加入的节点的左端点大于最后一个节点的右端点 - if (merged.isEmpty() || merged.getLast().end < interval.start) { - merged.add(interval); - } - //对于情况 2 ,更新最后一个节点的右端点 - else { - merged.getLast().end = Math.max(merged.getLast().end, interval.end); - } - } - - return merged; -} -``` - -时间复杂度:O(n log(n)),排序算法。 - -空间复杂度:O(n),存储结果。另外排序算法也可能需要。 - -# 解法三 - -参考[这里](https://leetcode.com/articles/merge-intervals/)的解法 1。 - -刷这么多题,第一次利用图去解决问题,这里分享下作者的思路。 - -如果每个节点如果有重叠部分,就用一条边相连。 - - - -![](https://windliang.oss-cn-beijing.aliyuncs.com/56_11.jpg) - -我们用一个 HashMap,用邻接表的结构来实现图,类似于下边的样子。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/56_12.jpg) - -图存起来以后,可以发现,最后有几个连通图,最后合并后的列表就有几个。我们需要把每个连通图保存起来,然后在每个连通图中找最小和最大的端点作为一个节点加入到合并后的列表中就可以了。最后,我们把每个连通图就转换成下边的图了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/56_13.jpg) - -```java -class Solution { - private Map > graph; //存储图 - private Map > nodesInComp; ///存储每个有向图 - private Set visited; - - //主函数 - public List merge(List intervals) { - buildGraph(intervals); //建立图 - buildComponents(intervals); //单独保存每个有向图 - - - List merged = new LinkedList<>(); - //遍历每个有向图,将有向图中最小最大的节点加入到列表中 - for (int comp = 0; comp < nodesInComp.size(); comp++) { - merged.add(mergeNodes(nodesInComp.get(comp))); - } - - return merged; - } - - // 判断两个节点是否有重叠部分 - private boolean overlap(Interval a, Interval b) { - return a.start <= b.end && b.start <= a.end; - } - - //利用邻接表建立图 - private void buildGraph(List intervals) { - graph = new HashMap<>(); - for (Interval interval : intervals) { - graph.put(interval, new LinkedList<>()); - } - - for (Interval interval1 : intervals) { - for (Interval interval2 : intervals) { - if (overlap(interval1, interval2)) { - graph.get(interval1).add(interval2); - graph.get(interval2).add(interval1); - } - } - } - } - - // 将每个连接图单独存起来 - private void buildComponents(List intervals) { - nodesInComp = new HashMap(); - visited = new HashSet(); - int compNumber = 0; - - for (Interval interval : intervals) { - if (!visited.contains(interval)) { - markComponentDFS(interval, compNumber); - compNumber++; - } - } - } - - //利用深度优先遍历去找到所有互相相连的边 - private void markComponentDFS(Interval start, int compNumber) { - Stack stack = new Stack<>(); - stack.add(start); - - while (!stack.isEmpty()) { - Interval node = stack.pop(); - if (!visited.contains(node)) { - visited.add(node); - - if (nodesInComp.get(compNumber) == null) { - nodesInComp.put(compNumber, new LinkedList<>()); - } - nodesInComp.get(compNumber).add(node); - - for (Interval child : graph.get(node)) { - stack.add(child); - } - } - } - } - - - // 找出每个有向图中最小和最大的端点 - private Interval mergeNodes(List nodes) { - int minStart = nodes.get(0).start; - for (Interval node : nodes) { - minStart = Math.min(minStart, node.start); - } - - int maxEnd = nodes.get(0).end; - for (Interval node : nodes) { - maxEnd= Math.max(maxEnd, node.end); - } - - return new Interval(minStart, maxEnd); - } -} -``` - -时间复杂度: - -空间复杂度:O(n²),最坏的情况,每个节点都互相重合,这样每个都与其他节点相连,就会是 n² 的空间存储图。 - -可惜的是,这种解法在 leetcode 会遇到超时错误。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/56.jpg) + +给定一个列表,将有重叠部分的合并。例如\[ [ 1 3 ] \[ 2 6 \] ] 合并成 [ 1 6 ] 。 + +# 解法一 + +常规的思想,将大问题化解成小问题去解决。 + +假设给了一个大小为 n 的列表,然后我们假设 n - 1 个元素的列表已经完成了全部合并,我们现在要解决的就是剩下的 1 个,怎么加到已经合并完的 n -1 个元素中。 + +这样的话分下边几种情况, 我们把每个范围叫做一个节点,节点包括左端点和右端点。 + +1. 如下图,新加入的节点左端点和右端点,分别在两个节点之间。这样,我们只要删除这两个节点,并且使用左边节点的左端点,右边的节点的右端点作为一个新节点插入即可。也就是删除 [ 1 6 ] 和 [ 8 12 ] ,加入 [ 1 12 ] 到合并好的列表中。 + + ![ ](https://windliang.oss-cn-beijing.aliyuncs.com/56_2.jpg) + +2. 如下图,新加入的节点只有左端点在之前的一个节点之内,这样的话将这个节点删除,使用删除的节点的左端点,新加入的节点的右端点,作为新的节点插入即可。也就是删除 [ 1 6 ],加入 [ 1 7 ] 到合并好的列表中。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/56_3.jpg) + +3. 如下图,新加入的节点只有右端点在之前的一个节点之内,这样的话将这个节点删除,使用删除的节点的右端点,新加入的节点的左端点,作为新的节点插入即可。也就是删除 [ 8 12 ],加入 [ 7 12 ] 到合并好的列表中。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/56_4.jpg) + +4. 如下图,新加入的节点的左端点和右端点在之前的一个节点之内,这样的话新加入的节点舍弃就可以了。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/56_5.jpg) + +5. 如下图,新加入的节点没有在任何一个节点之内,那么将它直接作为新的节点加入到合并好的节点之内就可以了。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/56_6.jpg) + +6. 如下图,还有一种情况,就是新加入的节点两个端点,将之前的节点囊括其中,这种的话,我们只需要将囊括的节点删除,把新节点加入即可。把 [ 8 12 ] 删除,将 7 13 加入即可。并且,新加入的节点可能会囊括多个旧节点,比如新加入的节点是 [ 1 100 ],那么下边的三个节点就都包括了,就需要都删除掉。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/56_7.jpg) + +以上就是所有的情况了,可以开始写代码了。 + +```java +public class Interval { + int start; + int end; + + Interval() { + start = 0; + end = 0; + } + + Interval(int s, int e) { + start = s; + end = e; + } +} + +public List merge(List intervals) { + List ans = new ArrayList<>(); + if (intervals.size() == 0) + return ans; + //将第一个节点加入,作为合并好的节点列表 + ans.add(new Interval(intervals.get(0).start, intervals.get(0).end)); + //遍历其他的每一个节点 + for (int i = 1; i < intervals.size(); i++) { + Interval start = null; + Interval end = null; + //新加入节点的左端点 + int i_start = intervals.get(i).start; + //新加入节点的右端点 + int i_end = intervals.get(i).end; + int size = ans.size(); + //情况 6,保存囊括的节点,便于删除 + List in = new ArrayList<>(); + //遍历合并好的每一个节点 + for (int j = 0; j < size; j++) { + //找到左端点在哪个节点内 + if (i_start >= ans.get(j).start && i_start <= ans.get(j).end) { + start = ans.get(j); + } + //找到右端点在哪个节点内 + if (i_end >= ans.get(j).start && i_end <= ans.get(j).end) { + end = ans.get(j); + } + //判断新加入的节点是否囊括当前旧节点,对应情况 6 + if (i_start < ans.get(j).start && i_end >ans.get(j).end) { + in.add(ans.get(j)); + } + + } + //删除囊括的节点 + if (in.size() != 0) { + for (int index = 0; index < in.size(); index++) { + ans.remove(in.get(index)); + } + } + //equals 函数作用是在 start 和 end 有且只有一个 null,或者 start 和 end 是同一个节点返回 true,相当于情况 2 3 4 中的一种 + if (equals(start, end)) { + //如果 start 和 end 都不等于 null 就代表情况 4 + + // start 等于 null 的话相当于情况 3 + int s = start == null ? i_start : start.start; + // end 等于 null 的话相当于情况 2 + int e = end == null ? i_end : end.end; + ans.add(new Interval(s, e)); + // start 和 end 不是同一个节点,相当于情况 1 + } else if (start!= null && end!=null) { + ans.add(new Interval(start.start, end.end)); + // start 和 end 都为 null,相当于情况 5 和 情况 6 ,加入新节点 + }else if (start == null) { + ans.add(new Interval(i_start, i_end)); + } + //将旧节点删除 + if (start != null) { + ans.remove(start); + } + if (end != null) { + ans.remove(end); + } + + } + return ans; +} + +private boolean equals(Interval start, Interval end) { + if (start == null && end == null) { + return false; + } + if (start == null || end == null) { + return true; + } + if (start.start == end.start && start.end == end.end) { + return true; + } + return false; +} + +``` + +时间复杂度:O(n²)。 + +空间复杂度:O (n),用来存储结果。 + +# 解法二 + +参考[这里](https://leetcode.com/articles/merge-intervals/)的解法二。 + +在解法一中,我们每次对于新加入的节点,都用一个 for 循环去遍历已经合并好的列表。如果我们把之前的列表,按照左端点进行从小到大排序了。这样的话,每次添加新节点的话,我们只需要和合并好的列表最后一个节点对比就可以了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/56_8.jpg) + +排好序后我们只需要把新加入的节点和最后一个节点比较就够了。 + +情况 1,如果新加入的节点的左端点大于合并好的节点列表的最后一个节点的右端点,那么我们只需要把新节点直接加入就可以了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/56_9.jpg) + +情况 2 ,如果新加入的节点的左端点不大于合并好的节点列表的最后一个节点的右端点,那么只需要判断新加入的节点的右端点和最后一个节点的右端点哪个大,然后更新最后一个节点的右端点就可以了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/56_10.jpg) + +```java +private class IntervalComparator implements Comparator { + @Override + public int compare(Interval a, Interval b) { + return a.start < b.start ? -1 : a.start == b.start ? 0 : 1; + } +} + +public List merge(List intervals) { + Collections.sort(intervals, new IntervalComparator()); + + LinkedList merged = new LinkedList(); + for (Interval interval : intervals) { + //最开始是空的,直接加入 + //然后对应情况 1,新加入的节点的左端点大于最后一个节点的右端点 + if (merged.isEmpty() || merged.getLast().end < interval.start) { + merged.add(interval); + } + //对于情况 2 ,更新最后一个节点的右端点 + else { + merged.getLast().end = Math.max(merged.getLast().end, interval.end); + } + } + + return merged; +} +``` + +时间复杂度:O(n log(n)),排序算法。 + +空间复杂度:O(n),存储结果。另外排序算法也可能需要。 + +# 解法三 + +参考[这里](https://leetcode.com/articles/merge-intervals/)的解法 1。 + +刷这么多题,第一次利用图去解决问题,这里分享下作者的思路。 + +如果每个节点如果有重叠部分,就用一条边相连。 + + + +![](https://windliang.oss-cn-beijing.aliyuncs.com/56_11.jpg) + +我们用一个 HashMap,用邻接表的结构来实现图,类似于下边的样子。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/56_12.jpg) + +图存起来以后,可以发现,最后有几个连通图,最后合并后的列表就有几个。我们需要把每个连通图保存起来,然后在每个连通图中找最小和最大的端点作为一个节点加入到合并后的列表中就可以了。最后,我们把每个连通图就转换成下边的图了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/56_13.jpg) + +```java +class Solution { + private Map > graph; //存储图 + private Map > nodesInComp; ///存储每个有向图 + private Set visited; + + //主函数 + public List merge(List intervals) { + buildGraph(intervals); //建立图 + buildComponents(intervals); //单独保存每个有向图 + + + List merged = new LinkedList<>(); + //遍历每个有向图,将有向图中最小最大的节点加入到列表中 + for (int comp = 0; comp < nodesInComp.size(); comp++) { + merged.add(mergeNodes(nodesInComp.get(comp))); + } + + return merged; + } + + // 判断两个节点是否有重叠部分 + private boolean overlap(Interval a, Interval b) { + return a.start <= b.end && b.start <= a.end; + } + + //利用邻接表建立图 + private void buildGraph(List intervals) { + graph = new HashMap<>(); + for (Interval interval : intervals) { + graph.put(interval, new LinkedList<>()); + } + + for (Interval interval1 : intervals) { + for (Interval interval2 : intervals) { + if (overlap(interval1, interval2)) { + graph.get(interval1).add(interval2); + graph.get(interval2).add(interval1); + } + } + } + } + + // 将每个连接图单独存起来 + private void buildComponents(List intervals) { + nodesInComp = new HashMap(); + visited = new HashSet(); + int compNumber = 0; + + for (Interval interval : intervals) { + if (!visited.contains(interval)) { + markComponentDFS(interval, compNumber); + compNumber++; + } + } + } + + //利用深度优先遍历去找到所有互相相连的边 + private void markComponentDFS(Interval start, int compNumber) { + Stack stack = new Stack<>(); + stack.add(start); + + while (!stack.isEmpty()) { + Interval node = stack.pop(); + if (!visited.contains(node)) { + visited.add(node); + + if (nodesInComp.get(compNumber) == null) { + nodesInComp.put(compNumber, new LinkedList<>()); + } + nodesInComp.get(compNumber).add(node); + + for (Interval child : graph.get(node)) { + stack.add(child); + } + } + } + } + + + // 找出每个有向图中最小和最大的端点 + private Interval mergeNodes(List nodes) { + int minStart = nodes.get(0).start; + for (Interval node : nodes) { + minStart = Math.min(minStart, node.start); + } + + int maxEnd = nodes.get(0).end; + for (Interval node : nodes) { + maxEnd= Math.max(maxEnd, node.end); + } + + return new Interval(minStart, maxEnd); + } +} +``` + +时间复杂度: + +空间复杂度:O(n²),最坏的情况,每个节点都互相重合,这样每个都与其他节点相连,就会是 n² 的空间存储图。 + +可惜的是,这种解法在 leetcode 会遇到超时错误。 + +# 总 + 开始的时候,使用最常用的思路,将大问题化解为小问题,然后用递归或者直接用迭代实现。解法二中,先对列表进行排序,从而优化了时间复杂度,也不是第一次看到了。解法三中,利用图解决问题很新颖,是我刷题第一次遇到的,又多了一种解题思路。 \ No newline at end of file diff --git a/leetCode-57-Insert-Interval.md b/leetCode-57-Insert-Interval.md index fa3ca830e..201c197c6 100644 --- a/leetCode-57-Insert-Interval.md +++ b/leetCode-57-Insert-Interval.md @@ -1,212 +1,212 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/57.jpg) - -和[上一道](https://leetcode.windliang.cc/leetCode-56-Merge-Intervals.html)可以说是一个问题,只不过这个是给一个已经合并好的列表,然后给一个新的节点依据规则加入到合并好的列表。 - -# 解法一 - -对应 [56 题](https://leetcode.windliang.cc/leetCode-56-Merge-Intervals.html)的解法一,没看的话,可以先过去看一下。这个问题其实就是我们解法中的一个子问题,所以直接加过来就行了。 - -```java -public List insert(List intervals, Interval newInterval) { - Interval start = null; - Interval end = null; - int i_start = newInterval.start; - int i_end =newInterval.end; - int size = intervals.size(); - List in = new ArrayList<>(); - //遍历合并好的列表 - for (int j = 0; j < size; j++) { - if (i_start >= intervals.get(j).start && i_start <= intervals.get(j).end) { - start = intervals.get(j); - } - if (i_end >= intervals.get(j).start && i_end <= intervals.get(j).end) { - end = intervals.get(j); - } - if (i_start < intervals.get(j).start && i_end >intervals.get(j).end) { - in.add(intervals.get(j)); - } - - } - if (in.size() != 0) { - for (int index = 0; index < in.size(); index++) { - intervals.remove(in.get(index)); - } - } - Interval interval = null; - //根据不同的情况,生成新的节点 - if (equals(start, end)) { - int s = start == null ? i_start : start.start; - int e = end == null ? i_end : end.end; - interval = new Interval(s, e); - } else if (start!= null && end!=null) { - interval = new Interval(start.start, end.end); - }else if (start == null) { - interval = new Interval(i_start, i_end); - } - if (start != null) { - intervals.remove(start); - } - if (end != null) { - intervals.remove(end); - } - //不同之处是生成的节点,要遍历原节点,根据 start 插入到对应的位置,虽然题目没说,但这里如果不按顺序插入的话,leetcode 过不了。 - for(int i = 0;iinterval.start){ - intervals.add(i,interval); - return intervals; - } - } - intervals.add(interval); - return intervals; - } -private boolean equals(Interval start, Interval end) { - if (start == null && end == null) { - return false; - } - if (start == null || end == null) { - return true; - } - if (start.start == end.start && start.end == end.end) { - return true; - } - return false; -} -``` - -时间复杂度:O(n)。 - -空间复杂度: O(n), 里边的 in 变量用来存储囊括的节点时候耗费的。 - -我们其实可以利用迭代器,一边遍历,一边删除,这样就不需要 in 变量了。 - -```java -public List insert(List intervals, Interval newInterval) { - Interval start = null; - Interval end = null; - int i_start = newInterval.start; - int i_end = newInterval.end; - //利用迭代器遍历 - for (Iterator it = intervals.iterator(); it.hasNext();) { - Interval inter = it.next(); - if (i_start >= inter.start && i_start <= inter.end) { - start = inter; - } - if (i_end >= inter.start && i_end <= inter.end) { - end = inter; - } - if (i_start < inter.start && i_end > inter.end) { - - it.remove(); - } - } - Interval interval = null; - if (equals(start, end)) { - int s = start == null ? i_start : start.start; - int e = end == null ? i_end : end.end; - interval = new Interval(s, e); - } else if (start != null && end != null) { - interval = new Interval(start.start, end.end); - } else if (start == null) { - interval = new Interval(i_start, i_end); - } - if (start != null) { - intervals.remove(start); - } - if (end != null) { - intervals.remove(end); - } - for (int i = 0; i < intervals.size(); i++) { - if (intervals.get(i).start > interval.start) { - intervals.add(i, interval); - return intervals; - } - } - intervals.add(interval); - return intervals; - } - -private boolean equals(Interval start, Interval end) { - if (start == null && end == null) { - return false; - } - if (start == null || end == null) { - return true; - } - if (start.start == end.start && start.end == end.end) { - return true; - } - return false; -} -``` - -时间复杂度:O(n)。 - -空间复杂度: O(1)。 - -# 解法二 - -对应 [56 题](https://leetcode.windliang.cc/leetCode-56-Merge-Intervals.html)的解法二,考虑到它给定的合并的列表是有序的,和解法二是一个思想。只不过这里不能直接从末尾添加,而是根据新节点的 start 来找到它应该在的位置,然后再利用之前的想法就够了。 - -这里把 leetcode 里的两种写法,贴过来,大家可以参考一下。 - -[第一种](https://leetcode.com/problems/insert-interval/discuss/21602/Short-and-straight-forward-Java-solution)。 - -```java -public List insert(List intervals, Interval newInterval) { - List result = new LinkedList<>(); - int i = 0; - // 将新节点之前的节点加到结果中 - while (i < intervals.size() && intervals.get(i).end < newInterval.start) - result.add(intervals.get(i++)); - // 和新节点判断是否重叠,更新新节点 - while (i < intervals.size() && intervals.get(i).start <= newInterval.end) { - newInterval = new Interval( - Math.min(newInterval.start, intervals.get(i).start), - Math.max(newInterval.end, intervals.get(i).end)); - i++; - } - //将新节点加入 - result.add(newInterval); - ///剩下的全部加进来 - while (i < intervals.size()) result.add(intervals.get(i++)); - return result; -} -``` - -[第二种](https://leetcode.com/problems/insert-interval/discuss/21600/Short-java-code)。和之前是一样的思想,只不过更加的简洁,可以参考一下。 - -```java -public List insert(List intervals, Interval newInterval) { - List result = new ArrayList(); - for (Interval i : intervals) { - //新加的入的节点在当前节点后边 - if (newInterval == null || i.end < newInterval.start) - result.add(i); - //新加入的节点在当前节点的前边 - else if (i.start > newInterval.end) { - result.add(newInterval); - result.add(i); - newInterval = null; - //新加入的节点和当前节点有重合,更新节点 - } else { - newInterval.start = Math.min(newInterval.start, i.start); - newInterval.end = Math.max(newInterval.end, i.end); - } - } - if (newInterval != null) - result.add(newInterval); - return result; -} -``` - -总的来说,上边两个写法本质是一样的,就是依据他们是有序的,先把新节点前边的节点加入,然后开始判断是否重合,当前节点加入后,把后边的加入就可以了。 - -时间复杂度:O(n)。 - -空间复杂度:O(n),存储最后的结果。 - -# 总 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/57.jpg) + +和[上一道](https://leetcode.windliang.cc/leetCode-56-Merge-Intervals.html)可以说是一个问题,只不过这个是给一个已经合并好的列表,然后给一个新的节点依据规则加入到合并好的列表。 + +# 解法一 + +对应 [56 题](https://leetcode.windliang.cc/leetCode-56-Merge-Intervals.html)的解法一,没看的话,可以先过去看一下。这个问题其实就是我们解法中的一个子问题,所以直接加过来就行了。 + +```java +public List insert(List intervals, Interval newInterval) { + Interval start = null; + Interval end = null; + int i_start = newInterval.start; + int i_end =newInterval.end; + int size = intervals.size(); + List in = new ArrayList<>(); + //遍历合并好的列表 + for (int j = 0; j < size; j++) { + if (i_start >= intervals.get(j).start && i_start <= intervals.get(j).end) { + start = intervals.get(j); + } + if (i_end >= intervals.get(j).start && i_end <= intervals.get(j).end) { + end = intervals.get(j); + } + if (i_start < intervals.get(j).start && i_end >intervals.get(j).end) { + in.add(intervals.get(j)); + } + + } + if (in.size() != 0) { + for (int index = 0; index < in.size(); index++) { + intervals.remove(in.get(index)); + } + } + Interval interval = null; + //根据不同的情况,生成新的节点 + if (equals(start, end)) { + int s = start == null ? i_start : start.start; + int e = end == null ? i_end : end.end; + interval = new Interval(s, e); + } else if (start!= null && end!=null) { + interval = new Interval(start.start, end.end); + }else if (start == null) { + interval = new Interval(i_start, i_end); + } + if (start != null) { + intervals.remove(start); + } + if (end != null) { + intervals.remove(end); + } + //不同之处是生成的节点,要遍历原节点,根据 start 插入到对应的位置,虽然题目没说,但这里如果不按顺序插入的话,leetcode 过不了。 + for(int i = 0;iinterval.start){ + intervals.add(i,interval); + return intervals; + } + } + intervals.add(interval); + return intervals; + } +private boolean equals(Interval start, Interval end) { + if (start == null && end == null) { + return false; + } + if (start == null || end == null) { + return true; + } + if (start.start == end.start && start.end == end.end) { + return true; + } + return false; +} +``` + +时间复杂度:O(n)。 + +空间复杂度: O(n), 里边的 in 变量用来存储囊括的节点时候耗费的。 + +我们其实可以利用迭代器,一边遍历,一边删除,这样就不需要 in 变量了。 + +```java +public List insert(List intervals, Interval newInterval) { + Interval start = null; + Interval end = null; + int i_start = newInterval.start; + int i_end = newInterval.end; + //利用迭代器遍历 + for (Iterator it = intervals.iterator(); it.hasNext();) { + Interval inter = it.next(); + if (i_start >= inter.start && i_start <= inter.end) { + start = inter; + } + if (i_end >= inter.start && i_end <= inter.end) { + end = inter; + } + if (i_start < inter.start && i_end > inter.end) { + + it.remove(); + } + } + Interval interval = null; + if (equals(start, end)) { + int s = start == null ? i_start : start.start; + int e = end == null ? i_end : end.end; + interval = new Interval(s, e); + } else if (start != null && end != null) { + interval = new Interval(start.start, end.end); + } else if (start == null) { + interval = new Interval(i_start, i_end); + } + if (start != null) { + intervals.remove(start); + } + if (end != null) { + intervals.remove(end); + } + for (int i = 0; i < intervals.size(); i++) { + if (intervals.get(i).start > interval.start) { + intervals.add(i, interval); + return intervals; + } + } + intervals.add(interval); + return intervals; + } + +private boolean equals(Interval start, Interval end) { + if (start == null && end == null) { + return false; + } + if (start == null || end == null) { + return true; + } + if (start.start == end.start && start.end == end.end) { + return true; + } + return false; +} +``` + +时间复杂度:O(n)。 + +空间复杂度: O(1)。 + +# 解法二 + +对应 [56 题](https://leetcode.windliang.cc/leetCode-56-Merge-Intervals.html)的解法二,考虑到它给定的合并的列表是有序的,和解法二是一个思想。只不过这里不能直接从末尾添加,而是根据新节点的 start 来找到它应该在的位置,然后再利用之前的想法就够了。 + +这里把 leetcode 里的两种写法,贴过来,大家可以参考一下。 + +[第一种](https://leetcode.com/problems/insert-interval/discuss/21602/Short-and-straight-forward-Java-solution)。 + +```java +public List insert(List intervals, Interval newInterval) { + List result = new LinkedList<>(); + int i = 0; + // 将新节点之前的节点加到结果中 + while (i < intervals.size() && intervals.get(i).end < newInterval.start) + result.add(intervals.get(i++)); + // 和新节点判断是否重叠,更新新节点 + while (i < intervals.size() && intervals.get(i).start <= newInterval.end) { + newInterval = new Interval( + Math.min(newInterval.start, intervals.get(i).start), + Math.max(newInterval.end, intervals.get(i).end)); + i++; + } + //将新节点加入 + result.add(newInterval); + ///剩下的全部加进来 + while (i < intervals.size()) result.add(intervals.get(i++)); + return result; +} +``` + +[第二种](https://leetcode.com/problems/insert-interval/discuss/21600/Short-java-code)。和之前是一样的思想,只不过更加的简洁,可以参考一下。 + +```java +public List insert(List intervals, Interval newInterval) { + List result = new ArrayList(); + for (Interval i : intervals) { + //新加的入的节点在当前节点后边 + if (newInterval == null || i.end < newInterval.start) + result.add(i); + //新加入的节点在当前节点的前边 + else if (i.start > newInterval.end) { + result.add(newInterval); + result.add(i); + newInterval = null; + //新加入的节点和当前节点有重合,更新节点 + } else { + newInterval.start = Math.min(newInterval.start, i.start); + newInterval.end = Math.max(newInterval.end, i.end); + } + } + if (newInterval != null) + result.add(newInterval); + return result; +} +``` + +总的来说,上边两个写法本质是一样的,就是依据他们是有序的,先把新节点前边的节点加入,然后开始判断是否重合,当前节点加入后,把后边的加入就可以了。 + +时间复杂度:O(n)。 + +空间复杂度:O(n),存储最后的结果。 + +# 总 + 总的来说,这道题可以看做上道题的一些变形,本质上是一样的。由于用 for 循环不能一边遍历列表,一边删除某个元素,所以利用迭代器实现边遍历,边删除,自己也是第一次用。此外,解法一更加通用些,它不要求给定的列表有序。 \ No newline at end of file diff --git a/leetCode-58-Length-of-Last-Word.md b/leetCode-58-Length-of-Last-Word.md index aeb772ced..3da0bfbc0 100644 --- a/leetCode-58-Length-of-Last-Word.md +++ b/leetCode-58-Length-of-Last-Word.md @@ -1,38 +1,38 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/58.jpg) - -输出最后一个单词的长度。 - -# 解法一 - -直接从最后一个字符往前遍历,遇到空格停止就可以了。不过在此之前要过滤到末尾的空格。 - -```java -public int lengthOfLastWord(String s) { - int count = 0; - int index = s.length() - 1; - //过滤空格 - while (true) { - if (index < 0 || s.charAt(index) != ' ') - break; - index--; - } - //计算最后一个单词的长度 - for (int i = index; i >= 0; i--) { - if (s.charAt(i) == ' ') { - break; - } - count++; - } - return count; -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(1)。 - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/58.jpg) + +输出最后一个单词的长度。 + +# 解法一 + +直接从最后一个字符往前遍历,遇到空格停止就可以了。不过在此之前要过滤到末尾的空格。 + +```java +public int lengthOfLastWord(String s) { + int count = 0; + int index = s.length() - 1; + //过滤空格 + while (true) { + if (index < 0 || s.charAt(index) != ' ') + break; + index--; + } + //计算最后一个单词的长度 + for (int i = index; i >= 0; i--) { + if (s.charAt(i) == ' ') { + break; + } + count++; + } + return count; +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(1)。 + +# 总 + 时隔多天,又遇到了一个简单的题,没什么好说的,就是遍历一遍,没有 get 到考点。 \ No newline at end of file diff --git a/leetCode-59-Spiral-MatrixII.md b/leetCode-59-Spiral-MatrixII.md index 31a7d2de3..8ec268466 100644 --- a/leetCode-59-Spiral-MatrixII.md +++ b/leetCode-59-Spiral-MatrixII.md @@ -1,98 +1,98 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/59.jpg) - -和 [54题](https://leetcode.windliang.cc/leetCode-54-Spiral-Matrix.html) 差不多,54 题按照螺旋状遍历,这个是按照螺旋状生成二维数组。 - -# 解法一 - -直接按照 [54题](https://leetcode.windliang.cc/leetCode-54-Spiral-Matrix.html),贪吃蛇的走法来写,如果没做过可以看一下。 - -```java -/* - * direction 0 代表向右, 1 代表向下, 2 代表向左, 3 代表向上 - */ -public int[][] generateMatrix(int n) { - int[][] ans = new int[n][n]; - int start_x = 0, start_y = 0, direction = 0, top_border = -1, // 上边界 - right_border = n, // 右边界 - bottom_border = n, // 下边界 - left_border = -1; // 左边界 - int count = 1; - while (true) { - // 全部遍历完结束 - if (count == n * n + 1) { - return ans; - } - // 注意 y 方向写在前边,x 方向写在后边 - ans[start_y][start_x] = count; - count++; - switch (direction) { - // 当前向右 - case 0: - // 继续向右是否到达边界 - // 到达边界就改变方向,并且更新上边界 - if (start_x + 1 == right_border) { - direction = 1; - start_y += 1; - top_border += 1; - } else { - start_x += 1; - } - break; - // 当前向下 - case 1: - // 继续向下是否到达边界 - // 到达边界就改变方向,并且更新右边界 - if (start_y + 1 == bottom_border) { - direction = 2; - start_x -= 1; - right_border -= 1; - } else { - start_y += 1; - } - break; - case 2: - if (start_x - 1 == left_border) { - direction = 3; - start_y -= 1; - bottom_border -= 1; - } else { - start_x -= 1; - } - break; - case 3: - if (start_y - 1 == top_border) { - direction = 0; - start_x += 1; - left_border += 1; - } else { - start_y -= 1; - } - break; - } - } - -} -``` - -时间复杂度:O(n²)。 - -空间复杂度:O(1)。 - -# 解法二 - -[这里](https://leetcode.com/problems/spiral-matrix-ii/discuss/22282/4-9-lines-Python-solutions)看到了一个与众不同的想法,分享一下。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/59_2.jpg) - -矩阵先添加 1 个元素,然后顺时针旋转矩阵,然后再在矩阵第一行添加元素,再顺时针旋转矩阵,再在第一行添加元素,直到变成 n * n 的矩阵。 - -之前在 [48题](https://leetcode.windliang.cc/leetCode-48-Rotate-Image.html) 做过旋转矩阵的算法,但是当时是 n \* n,这个 n \* m 就更复杂些了,然后由于 JAVA 的矩阵定义的时候就固定死了,每次添加新的一行又得 new 新的数组,这样整个过程就会很浪费空间,综上,用 JAVA 不适合去实现这个算法,就不实现了,哈哈哈哈哈,看一下[作者](https://leetcode.com/problems/spiral-matrix-ii/discuss/22282/4-9-lines-Python-solutions)的 python 代码吧。 - -# 总 - -基本上和 [54题](https://leetcode.windliang.cc/leetCode-54-Spiral-Matrix.html) 差不多,依旧是理解题意,然后模仿遍历过程就可以了。 - - - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/59.jpg) + +和 [54题](https://leetcode.windliang.cc/leetCode-54-Spiral-Matrix.html) 差不多,54 题按照螺旋状遍历,这个是按照螺旋状生成二维数组。 + +# 解法一 + +直接按照 [54题](https://leetcode.windliang.cc/leetCode-54-Spiral-Matrix.html),贪吃蛇的走法来写,如果没做过可以看一下。 + +```java +/* + * direction 0 代表向右, 1 代表向下, 2 代表向左, 3 代表向上 + */ +public int[][] generateMatrix(int n) { + int[][] ans = new int[n][n]; + int start_x = 0, start_y = 0, direction = 0, top_border = -1, // 上边界 + right_border = n, // 右边界 + bottom_border = n, // 下边界 + left_border = -1; // 左边界 + int count = 1; + while (true) { + // 全部遍历完结束 + if (count == n * n + 1) { + return ans; + } + // 注意 y 方向写在前边,x 方向写在后边 + ans[start_y][start_x] = count; + count++; + switch (direction) { + // 当前向右 + case 0: + // 继续向右是否到达边界 + // 到达边界就改变方向,并且更新上边界 + if (start_x + 1 == right_border) { + direction = 1; + start_y += 1; + top_border += 1; + } else { + start_x += 1; + } + break; + // 当前向下 + case 1: + // 继续向下是否到达边界 + // 到达边界就改变方向,并且更新右边界 + if (start_y + 1 == bottom_border) { + direction = 2; + start_x -= 1; + right_border -= 1; + } else { + start_y += 1; + } + break; + case 2: + if (start_x - 1 == left_border) { + direction = 3; + start_y -= 1; + bottom_border -= 1; + } else { + start_x -= 1; + } + break; + case 3: + if (start_y - 1 == top_border) { + direction = 0; + start_x += 1; + left_border += 1; + } else { + start_y -= 1; + } + break; + } + } + +} +``` + +时间复杂度:O(n²)。 + +空间复杂度:O(1)。 + +# 解法二 + +[这里](https://leetcode.com/problems/spiral-matrix-ii/discuss/22282/4-9-lines-Python-solutions)看到了一个与众不同的想法,分享一下。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/59_2.jpg) + +矩阵先添加 1 个元素,然后顺时针旋转矩阵,然后再在矩阵第一行添加元素,再顺时针旋转矩阵,再在第一行添加元素,直到变成 n * n 的矩阵。 + +之前在 [48题](https://leetcode.windliang.cc/leetCode-48-Rotate-Image.html) 做过旋转矩阵的算法,但是当时是 n \* n,这个 n \* m 就更复杂些了,然后由于 JAVA 的矩阵定义的时候就固定死了,每次添加新的一行又得 new 新的数组,这样整个过程就会很浪费空间,综上,用 JAVA 不适合去实现这个算法,就不实现了,哈哈哈哈哈,看一下[作者](https://leetcode.com/problems/spiral-matrix-ii/discuss/22282/4-9-lines-Python-solutions)的 python 代码吧。 + +# 总 + +基本上和 [54题](https://leetcode.windliang.cc/leetCode-54-Spiral-Matrix.html) 差不多,依旧是理解题意,然后模仿遍历过程就可以了。 + + + diff --git a/leetCode-6-ZigZag-Conversion.md b/leetCode-6-ZigZag-Conversion.md index 1cf07e290..a69c520d5 100644 --- a/leetCode-6-ZigZag-Conversion.md +++ b/leetCode-6-ZigZag-Conversion.md @@ -1,96 +1,96 @@ -## 题目描述(中等难度) - -![](http://windliang.oss-cn-beijing.aliyuncs.com/6_zig.jpg) - -就是给定一个字符串,然后按写竖着的 「z」的方式排列字符,就是下边的样子。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/6_1.jpg) - -然后按行的方式输出每个字符,第 0 行,第 1 行,第 2 行 .... - -## 解法一 - -按照写 Z 的过程,遍历每个字符,然后将字符存到对应的行中。用 goingDown 保存当前的遍历方向,如果遍历到两端,就改变方向。 - -```java - public String convert(String s, int numRows) { - - if (numRows == 1) return s; - - List rows = new ArrayList<>(); - for (int i = 0; i < Math.min(numRows, s.length()); i++) - rows.add(new StringBuilder()); - - int curRow = 0; - boolean goingDown = false; - - for (char c : s.toCharArray()) { - rows.get(curRow).append(c); - if (curRow == 0 || curRow == numRows - 1) goingDown = !goingDown; //遍历到两端,改变方向 - curRow += goingDown ? 1 : -1; - } - - StringBuilder ret = new StringBuilder(); - for (StringBuilder row : rows) ret.append(row); - return ret.toString(); - } -``` - -时间复杂度:O(n),n 是字符串的长度。 - -空间复杂度:O(n),保存每个字符需要的空间。 - -## 解法二 - -找出按 Z 形排列后字符的规律,然后直接保存起来。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/6_3.jpg) - -我们可以看到,图形其实是有周期的,0,1,2 ... 7 总过 8 个,然后就又开始重复相同的路径。周期的计算就是 cycleLen = 2 × numRows - 2 = 2 × 5 - 2 = 8 个。 - -我们发现第 0 行和最后一行一个周期内有一个字符,所以第一个字符下标是 0 ,第二个字符下标是 0 + cycleLen = 8,第三个字符下标是 8 + cycleLen = 16 。 - -其他行都是两个字符。 - -第 1 个字符和第 0 行的规律是一样的。 - -第 2 个字符其实就是下一个周期的第 0 行的下标减去当前行。什么意思呢? - -我们求一下第 1 行第 1 个周期内的第 2 个字符,下一个周期的第 0 行的下标是 8 ,减去当前行 1 ,就是 7 了。 - -我们求一下第 1 行第 2 个而周期内的第 2 个字符,下一个周期的第 0 行的下标是 16 ,减去当前行 1 ,就是 15 了。 - -我们求一下第 2 行第 1 个周期内的第 2 个字符,下一个周期的第 0 行的下标是 8 ,减去当前行 2 ,就是 6 了。 - -当然期间一定要保证下标小于 n ,防止越界。 - -可以写代码了。 - -```java -public String convert(String s, int numRows) { - - if (numRows == 1) - return s; - - StringBuilder ret = new StringBuilder(); - int n = s.length(); - int cycleLen = 2 * numRows - 2; - - for (int i = 0; i < numRows; i++) { - for (int j = 0; j + i < n; j += cycleLen) { //每次加一个周期 - ret.append(s.charAt(j + i)); - if (i != 0 && i != numRows - 1 && j + cycleLen - i < n) //除去第 0 行和最后一行 - ret.append(s.charAt(j + cycleLen - i)); - } - } - return ret.toString(); -} -``` - -时间复杂度:O(n),虽然是两层循环,但第二次循环每次加的是 cycleLen ,无非是把每个字符遍历了 1 次,所以两层循环内执行的次数肯定是字符串的长度。 - -空间复杂度:O(n),保存字符串。 - -## 总结 - +## 题目描述(中等难度) + +![](http://windliang.oss-cn-beijing.aliyuncs.com/6_zig.jpg) + +就是给定一个字符串,然后按写竖着的 「z」的方式排列字符,就是下边的样子。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/6_1.jpg) + +然后按行的方式输出每个字符,第 0 行,第 1 行,第 2 行 .... + +## 解法一 + +按照写 Z 的过程,遍历每个字符,然后将字符存到对应的行中。用 goingDown 保存当前的遍历方向,如果遍历到两端,就改变方向。 + +```java + public String convert(String s, int numRows) { + + if (numRows == 1) return s; + + List rows = new ArrayList<>(); + for (int i = 0; i < Math.min(numRows, s.length()); i++) + rows.add(new StringBuilder()); + + int curRow = 0; + boolean goingDown = false; + + for (char c : s.toCharArray()) { + rows.get(curRow).append(c); + if (curRow == 0 || curRow == numRows - 1) goingDown = !goingDown; //遍历到两端,改变方向 + curRow += goingDown ? 1 : -1; + } + + StringBuilder ret = new StringBuilder(); + for (StringBuilder row : rows) ret.append(row); + return ret.toString(); + } +``` + +时间复杂度:O(n),n 是字符串的长度。 + +空间复杂度:O(n),保存每个字符需要的空间。 + +## 解法二 + +找出按 Z 形排列后字符的规律,然后直接保存起来。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/6_3.jpg) + +我们可以看到,图形其实是有周期的,0,1,2 ... 7 总过 8 个,然后就又开始重复相同的路径。周期的计算就是 cycleLen = 2 × numRows - 2 = 2 × 5 - 2 = 8 个。 + +我们发现第 0 行和最后一行一个周期内有一个字符,所以第一个字符下标是 0 ,第二个字符下标是 0 + cycleLen = 8,第三个字符下标是 8 + cycleLen = 16 。 + +其他行都是两个字符。 + +第 1 个字符和第 0 行的规律是一样的。 + +第 2 个字符其实就是下一个周期的第 0 行的下标减去当前行。什么意思呢? + +我们求一下第 1 行第 1 个周期内的第 2 个字符,下一个周期的第 0 行的下标是 8 ,减去当前行 1 ,就是 7 了。 + +我们求一下第 1 行第 2 个而周期内的第 2 个字符,下一个周期的第 0 行的下标是 16 ,减去当前行 1 ,就是 15 了。 + +我们求一下第 2 行第 1 个周期内的第 2 个字符,下一个周期的第 0 行的下标是 8 ,减去当前行 2 ,就是 6 了。 + +当然期间一定要保证下标小于 n ,防止越界。 + +可以写代码了。 + +```java +public String convert(String s, int numRows) { + + if (numRows == 1) + return s; + + StringBuilder ret = new StringBuilder(); + int n = s.length(); + int cycleLen = 2 * numRows - 2; + + for (int i = 0; i < numRows; i++) { + for (int j = 0; j + i < n; j += cycleLen) { //每次加一个周期 + ret.append(s.charAt(j + i)); + if (i != 0 && i != numRows - 1 && j + cycleLen - i < n) //除去第 0 行和最后一行 + ret.append(s.charAt(j + cycleLen - i)); + } + } + return ret.toString(); +} +``` + +时间复杂度:O(n),虽然是两层循环,但第二次循环每次加的是 cycleLen ,无非是把每个字符遍历了 1 次,所以两层循环内执行的次数肯定是字符串的长度。 + +空间复杂度:O(n),保存字符串。 + +## 总结 + 这次算是总结起来最轻松的了,这道题有些找规律的意思。解法一顺着排列的方式遍历,解法二直接从答案入口找出下标的规律。 \ No newline at end of file diff --git a/leetCode-60-Permutation-Sequence.md b/leetCode-60-Permutation-Sequence.md index 020ff54e0..8f3e918ff 100644 --- a/leetCode-60-Permutation-Sequence.md +++ b/leetCode-60-Permutation-Sequence.md @@ -1,133 +1,133 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/60.jpg) - -又是一道全排列的题,之前在[31题](https://leetcode.windliang.cc/leetCode-31-Next-Permutation.html?h=permu),[46题](https://leetcode.windliang.cc/leetCode-46-Permutations.html),也讨论过全排列问题的一些解法。这道题的话,是给一个 n,不是输出它的全排列,而是把所有组合从从小到大排列后,输出第 k 个。 - -# 解法一 - -以 n = 4 为例,可以结合下图看一下。因为是从小到大排列,那么最高位一定是从 1 到 4。然后可以看成一组一组的,我们只需要求出组数,就知道最高位是多少了。而每组的个数就是 n - 1 的阶乘,也就是 3 的阶乘 6。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/60_2.jpg) - -算组数的时候, 1 到 5 除以 6 是 0,6 除以 6 是 1,而 6 是属于第 0 组的,所有要把 k 减去 1。这样做除法结果就都是 0 了。 - -```java -int perGroupNum = factorial(n - 1); -int groupNum = (k - 1) / perGroupNum; -``` - -当然,还有一个问题下次 k 是多少了。求组数用的除法,余数就是下次的 k 了。因为 k 是从 1 计数的,所以如果 k 刚好等于了 perGroupNum 的倍数,此时得到的余数是 0 ,而其实由于我们求 groupNum 的时候减 1 了,所以此时 k 应该更新为 perGroupNum。 - -```java -k = k % perGroupNum; -k = k == 0 ? perGroupNum : k; -``` - -举个例子,如果 k = 6,那么 groupNum = ( k - 1 ) / 6 = 0, k % perGroupNum = 6 % 6 = 0,而下次的 k ,可以结合上图,很明显是 perGroupNum ,依旧是 6。 - -结合下图,确定了最高位属于第 0 组,下边就和上边的情况一样了。唯一不同的地方是最高位是 2 3 4,没有了 1。所有得到 groupNum 怎么得到最高位需要考虑下。 - -我们可以用一个 list 从小到大保存 1 到 n,每次选到一个就去掉,这样就可以得到 groupNum 对应的数字了。 - -```java -List nums = new ArrayList(); -for (int i = 0; i < n; i++) { - nums.add(i + 1); -} -int perGroupNum = factorial(n - 1); -int groupNum = (k - 1) / perGroupNum; -int num = nums.get(groupNum); //根据 groupNum 得到当前位 -nums.remove(groupNum);//去掉当前数字 -``` - - - -![](https://windliang.oss-cn-beijing.aliyuncs.com/60_3.jpg) - -综上,我们把它们整合在一起。 - -```java -public String getPermutation(int n, int k) { - List nums = new ArrayList(); - for (int i = 0; i < n; i++) { - nums.add(i + 1); - } - return getAns(nums, n, k); -} - -private String getAns(List nums, int n, int k) { - if (n == 1) { - //把剩下的最后一个数字返回就可以了 - return nums.get(0) + ""; - } - int perGroupNum = factorial(n - 1); //每组的个数 - int groupNum = (k - 1) / perGroupNum; - int num = nums.get(groupNum); - nums.remove(groupNum); - k = k % perGroupNum; //更新下次的 k - k = k == 0 ? perGroupNum : k; - return num + getAns(nums, n - 1, k); -} -public int factorial(int number) { - if (number <= 1) - return 1; - else - return number * factorial(number - 1); -} -``` - -时间复杂度: - -空间复杂度: - -这是最开始自己的想法,有 3 点可以改进一下。 - -第 1 点,更新 k 的时候,有一句 - -```java -k = k % perGroupNum; //更新下次的 k -k = k == 0 ? perGroupNum : k; -``` - -很不优雅了,问题的根源就在于问题给定的 k 是从 1 编码的。我们只要把 k - 1 % perGroupNum,这样得到的结果就是 k 从 0 编码的了。然后求 groupNum = (k - 1) / perGroupNum; 这里 k 也不用减 1 了。 - -第 2 点,这个算法很容易改成改成迭代的写法,只需要把递归的函数参数, 在每次迭代更新就够了。 - -第 3 点,我们求 perGroupNum 的时候,每次都调用了求迭代的函数,其实没有必要的,我们只需要一次循环求出 n 的阶乘。然后在每次迭代中除以 nums 的剩余个数就够了。 - -综上,看一下优化过的代码吧。 - -```java -public String getPermutation(int n, int k) { - List nums = new ArrayList(); - int factorial = 1; - for (int i = 0; i < n; i++) { - nums.add(i + 1); - if (i != 0) { - factorial *= i; - } - } - factorial *= n; //先求出 n 的阶乘 - StringBuilder ans = new StringBuilder(); - k = k - 1; // k 变为 k - 1 - for (int i = n; i > 0; i--) { - factorial /= (nums.size()); //更新为 n - 1 的阶乘 - int groupNum = k / factorial; - int num = nums.get(groupNum); - nums.remove(groupNum); - k = k % factorial; - ans.append(num); - - } - return ans.toString(); -} -``` - -时间复杂度:O(n),当然如果 remove 函数的时间是复杂度是 O(n),那么整体上就是 O(n²)。 - -空间复杂度:O(1)。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/60.jpg) + +又是一道全排列的题,之前在[31题](https://leetcode.windliang.cc/leetCode-31-Next-Permutation.html?h=permu),[46题](https://leetcode.windliang.cc/leetCode-46-Permutations.html),也讨论过全排列问题的一些解法。这道题的话,是给一个 n,不是输出它的全排列,而是把所有组合从从小到大排列后,输出第 k 个。 + +# 解法一 + +以 n = 4 为例,可以结合下图看一下。因为是从小到大排列,那么最高位一定是从 1 到 4。然后可以看成一组一组的,我们只需要求出组数,就知道最高位是多少了。而每组的个数就是 n - 1 的阶乘,也就是 3 的阶乘 6。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/60_2.jpg) + +算组数的时候, 1 到 5 除以 6 是 0,6 除以 6 是 1,而 6 是属于第 0 组的,所有要把 k 减去 1。这样做除法结果就都是 0 了。 + +```java +int perGroupNum = factorial(n - 1); +int groupNum = (k - 1) / perGroupNum; +``` + +当然,还有一个问题下次 k 是多少了。求组数用的除法,余数就是下次的 k 了。因为 k 是从 1 计数的,所以如果 k 刚好等于了 perGroupNum 的倍数,此时得到的余数是 0 ,而其实由于我们求 groupNum 的时候减 1 了,所以此时 k 应该更新为 perGroupNum。 + +```java +k = k % perGroupNum; +k = k == 0 ? perGroupNum : k; +``` + +举个例子,如果 k = 6,那么 groupNum = ( k - 1 ) / 6 = 0, k % perGroupNum = 6 % 6 = 0,而下次的 k ,可以结合上图,很明显是 perGroupNum ,依旧是 6。 + +结合下图,确定了最高位属于第 0 组,下边就和上边的情况一样了。唯一不同的地方是最高位是 2 3 4,没有了 1。所有得到 groupNum 怎么得到最高位需要考虑下。 + +我们可以用一个 list 从小到大保存 1 到 n,每次选到一个就去掉,这样就可以得到 groupNum 对应的数字了。 + +```java +List nums = new ArrayList(); +for (int i = 0; i < n; i++) { + nums.add(i + 1); +} +int perGroupNum = factorial(n - 1); +int groupNum = (k - 1) / perGroupNum; +int num = nums.get(groupNum); //根据 groupNum 得到当前位 +nums.remove(groupNum);//去掉当前数字 +``` + + + +![](https://windliang.oss-cn-beijing.aliyuncs.com/60_3.jpg) + +综上,我们把它们整合在一起。 + +```java +public String getPermutation(int n, int k) { + List nums = new ArrayList(); + for (int i = 0; i < n; i++) { + nums.add(i + 1); + } + return getAns(nums, n, k); +} + +private String getAns(List nums, int n, int k) { + if (n == 1) { + //把剩下的最后一个数字返回就可以了 + return nums.get(0) + ""; + } + int perGroupNum = factorial(n - 1); //每组的个数 + int groupNum = (k - 1) / perGroupNum; + int num = nums.get(groupNum); + nums.remove(groupNum); + k = k % perGroupNum; //更新下次的 k + k = k == 0 ? perGroupNum : k; + return num + getAns(nums, n - 1, k); +} +public int factorial(int number) { + if (number <= 1) + return 1; + else + return number * factorial(number - 1); +} +``` + +时间复杂度: + +空间复杂度: + +这是最开始自己的想法,有 3 点可以改进一下。 + +第 1 点,更新 k 的时候,有一句 + +```java +k = k % perGroupNum; //更新下次的 k +k = k == 0 ? perGroupNum : k; +``` + +很不优雅了,问题的根源就在于问题给定的 k 是从 1 编码的。我们只要把 k - 1 % perGroupNum,这样得到的结果就是 k 从 0 编码的了。然后求 groupNum = (k - 1) / perGroupNum; 这里 k 也不用减 1 了。 + +第 2 点,这个算法很容易改成改成迭代的写法,只需要把递归的函数参数, 在每次迭代更新就够了。 + +第 3 点,我们求 perGroupNum 的时候,每次都调用了求迭代的函数,其实没有必要的,我们只需要一次循环求出 n 的阶乘。然后在每次迭代中除以 nums 的剩余个数就够了。 + +综上,看一下优化过的代码吧。 + +```java +public String getPermutation(int n, int k) { + List nums = new ArrayList(); + int factorial = 1; + for (int i = 0; i < n; i++) { + nums.add(i + 1); + if (i != 0) { + factorial *= i; + } + } + factorial *= n; //先求出 n 的阶乘 + StringBuilder ans = new StringBuilder(); + k = k - 1; // k 变为 k - 1 + for (int i = n; i > 0; i--) { + factorial /= (nums.size()); //更新为 n - 1 的阶乘 + int groupNum = k / factorial; + int num = nums.get(groupNum); + nums.remove(groupNum); + k = k % factorial; + ans.append(num); + + } + return ans.toString(); +} +``` + +时间复杂度:O(n),当然如果 remove 函数的时间是复杂度是 O(n),那么整体上就是 O(n²)。 + +空间复杂度:O(1)。 + +# 总 + 这道题其实如果写出来,也不算难,优化的思路可以了解一下。 \ No newline at end of file diff --git a/leetCode-61-Rotate-List.md b/leetCode-61-Rotate-List.md index 503e73731..92bbf95ff 100644 --- a/leetCode-61-Rotate-List.md +++ b/leetCode-61-Rotate-List.md @@ -1,71 +1,71 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/61.jpg) - -将最后一个链表节点移到最前边,然后重复这个过程 k 次。 - -# 解法一 - -很明显我们不需要真的一个一个移,如果链表长度是 len, n = k % len,我们只需要将末尾 n 个链表节点整体移动到最前边就可以了。可以结合下边的图看一下,我们只需要找到倒数 n + 1 个节点的指针把它指向 null,以及末尾的指针指向头结点就可以了。找倒数 n 个结点,让我想到了 [19题](https://leetcode.windliang.cc/leetCode-19-Remov-Nth-Node-From-End-of-List.html),利用快慢指针。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/61_2.jpg) - -```java -public ListNode rotateRight(ListNode head, int k) { - if (head == null || k == 0) { - return head; - } - int len = 0; - ListNode h = head; - ListNode tail = null; - //求出链表长度,保存尾指针 - while (h != null) { - h = h.next; - len++; - if (h != null) { - tail = h; - } - } - //求出需要整体移动多少个节点 - int n = k % len; - if (n == 0) { - return head; - } - - //利用快慢指针找出倒数 n + 1 个节点的指针,用 slow 保存 - ListNode fast = head; - while (n >= 0) { - fast = fast.next; - n--; - } - ListNode slow = head; - while (fast != null) { - slow = slow.next; - fast = fast.next; - } - //尾指针指向头结点 - tail.next = head; - //头指针更新为倒数第 n 个节点 - head = slow.next; - //尾指针置为 null - slow.next = null; - return head; -} -``` - -时间复杂度:O ( n ) 。 - -空间复杂度:O(1)。 - -这里我们用到的快慢指针其实没有必要,快慢指针的一个优点是,不需要知道链表长度就可以找到倒数第 n 个节点。而这个算法中,我们在之前已经求出了 len ,所以我们其实可以直接找倒数第 n + 1 个节点。 - -```java -ListNode slow = head; -for (int i = 1; i < len - n; i++) { - slow = slow.next; -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/61.jpg) + +将最后一个链表节点移到最前边,然后重复这个过程 k 次。 + +# 解法一 + +很明显我们不需要真的一个一个移,如果链表长度是 len, n = k % len,我们只需要将末尾 n 个链表节点整体移动到最前边就可以了。可以结合下边的图看一下,我们只需要找到倒数 n + 1 个节点的指针把它指向 null,以及末尾的指针指向头结点就可以了。找倒数 n 个结点,让我想到了 [19题](https://leetcode.windliang.cc/leetCode-19-Remov-Nth-Node-From-End-of-List.html),利用快慢指针。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/61_2.jpg) + +```java +public ListNode rotateRight(ListNode head, int k) { + if (head == null || k == 0) { + return head; + } + int len = 0; + ListNode h = head; + ListNode tail = null; + //求出链表长度,保存尾指针 + while (h != null) { + h = h.next; + len++; + if (h != null) { + tail = h; + } + } + //求出需要整体移动多少个节点 + int n = k % len; + if (n == 0) { + return head; + } + + //利用快慢指针找出倒数 n + 1 个节点的指针,用 slow 保存 + ListNode fast = head; + while (n >= 0) { + fast = fast.next; + n--; + } + ListNode slow = head; + while (fast != null) { + slow = slow.next; + fast = fast.next; + } + //尾指针指向头结点 + tail.next = head; + //头指针更新为倒数第 n 个节点 + head = slow.next; + //尾指针置为 null + slow.next = null; + return head; +} +``` + +时间复杂度:O ( n ) 。 + +空间复杂度:O(1)。 + +这里我们用到的快慢指针其实没有必要,快慢指针的一个优点是,不需要知道链表长度就可以找到倒数第 n 个节点。而这个算法中,我们在之前已经求出了 len ,所以我们其实可以直接找倒数第 n + 1 个节点。 + +```java +ListNode slow = head; +for (int i = 1; i < len - n; i++) { + slow = slow.next; +} +``` + +# 总 + 这道题也没有什么技巧,只要对链表很熟,把题理解了,很快就解出来了。 \ No newline at end of file diff --git a/leetCode-62-Unique-Paths.md b/leetCode-62-Unique-Paths.md index 28bb7f72b..a86233697 100644 --- a/leetCode-62-Unique-Paths.md +++ b/leetCode-62-Unique-Paths.md @@ -1,200 +1,200 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/62.jpg) - -机器人从左上角走到右下角,只能向右或者向下走。输出总共有多少种走法。 - -# 解法一 递归 - -求 ( 0 , 0 ) 点到( m - 1 , n - 1) 点的走法。 - -(0,0)点到(m - 1 , n - 1) 点的走法等于(0,0)点右边的点 (1,0)到(m - 1 , n - 1)的走法加上(0,0)点下边的点(0,1)到(m - 1 , n - 1)的走法。 - -而左边的点(1,0)点到(m - 1 , n - 1) 点的走法等于(2,0) 点到(m - 1 , n - 1)的走法加上(1,1)点到(m - 1 , n - 1)的走法。 - -下边的点(0,1)点到(m - 1 , n - 1) 点的走法等于(1,1)点到(m - 1 , n - 1)的走法加上(0,2)点到(m - 1 , n - 1)的走法。 - -然后一直递归下去,直到 (m - 1 , n - 1) 点到(m - 1 , n - 1) ,返回 1。 - -```java -public int uniquePaths(int m, int n) { - HashMap visited = new HashMap<>(); - return getAns(0, 0, m - 1, n - 1, 0); - -} - -private int getAns(int x, int y, int m, int n, int num) { - if (x == m && y == n) { - return 1; - } - int n1 = 0; - int n2 = 0; - //向右探索的所有解 - if (x + 1 <= m) { - n1 = getAns(x + 1, y, m, n, num); - } - //向左探索的所有解 - if (y + 1 <= n) { - n2 = getAns(x, y + 1, m, n, num); - } - //加起来 - return n1 + n2; -} -``` - -时间复杂度: - -空间复杂度: - -遗憾的是,这个算法在 LeetCode 上超时了。我们可以优化下,问题出在当我们求点 (x,y)到(m - 1 , n - 1) 点的走法的时候,递归求了点 (x,y)点右边的点 (x + 1,0)到(m - 1 , n - 1)的走法和(x,y)下边的点(x,y + 1)到(m - 1 , n - 1)的走法。而没有考虑到(x + 1,0)到(m - 1 , n - 1)的走法和点(x,y + 1)到(m - 1 , n - 1)的走法是否是之前已经求过了。事实上,很多点求的时候后边的的点已经求过了,所以再进行递归是没有必要的。基于此,我们可以用 visited 保存已经求过的点。 - -```java -public int uniquePaths(int m, int n) { - HashMap visited = new HashMap<>(); - return getAns(0, 0, m - 1, n - 1, 0, visited); - -} -private int getAns(int x, int y, int m, int n, int num, HashMap visited) { - if (x == m && y == n) { - return 1; - } - int n1 = 0; - int n2 = 0; - String key = x + 1 + "@" + y; - //判断当前点是否已经求过了 - if (!visited.containsKey(key)) { - if (x + 1 <= m) { - n1 = getAns(x + 1, y, m, n, num, visited); - } - } else { - n1 = visited.get(key); - } - key = x + "@" + (y + 1); - if (!visited.containsKey(key)) { - if (y + 1 <= n) { - n2 = getAns(x, y + 1, m, n, num, visited); - } - } else { - n2 = visited.get(key); - } - //将当前点加入 visited 中 - key = x + "@" + y; - visited.put(key, n1+n2); - return n1 + n2; -} -``` - -时间复杂度: - -空间复杂度: - -# 解法二 动态规划 - -解法一是基于递归的,压栈浪费了很多时间。我们来分析一下,压栈的过程,然后我们其实完全可以省略压栈的过程,直接用迭代去实现。 - -如下图,如果是递归的话,根据上边的代码,从 (0,0)点向右压栈,向右压栈,到最右边后,就向下压栈,向下压栈,到最下边以后,就开始出栈。出栈过程就是橙色部分。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/62_2.jpg) - -然后根据代码,继续压栈前一列,下图的橙色部分,然后到最下边后,然后开始出栈,根据它的右边的点和下边的点计算当前的点的走法。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/62_3.jpg) - -接下来两步同理,压栈,出栈。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/62_4.jpg) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/62_5.jpg) - -我们现在要做的就是要省略压栈的过程,直接出栈。很明显可以做到的,只需要初始化最后一列为 1 ,然后 1 列,1 列的向前更新就可以了。有一些动态规划的思想了。 - -```java -public int uniquePaths(int m, int n) { - int[] dp = new int[m]; - //初始化最后一列 - for (int i = 0; i < m; i++) { - dp[i] = 1; - } - //从右向左更新所有列 - for (int i = n - 2; i >= 0; i--) { - //最后一行永远是 1,所以从倒数第 2 行开始 - //从下向上更新所有行 - for (int j = m - 2; j >= 0; j--) { - //右边的和下边的更新当前元素 - dp[j] = dp[j] + dp[j + 1]; - } - } - return dp[0]; -} -``` - -时间复杂度:O(m * n)。 - -空间复杂度:O(m)。 - -[这里](https://leetcode.com/problems/unique-paths/discuss/22954/C%2B%2B-DP)也有一个类似的想法。不过他是正向考虑的,和上边的想法刚好相反。如果把 dp \[ i \] [ j \] 表示为从点 (0,0)到点 ( i,j)的走法。 - -上边解法公式就是 dp \[ i \] [ j \] = dp \[ i + 1 \] [ j \] + dp \[ i \] [ j +1 \]。 - -[这里](https://leetcode.com/problems/unique-paths/discuss/22954/C%2B%2B-DP)的话就是 dp \[ i \] [ j \] = dp \[ i - 1 \] [ j \] + dp \[ i \] [ j - 1 \]。就是用它左边的和上边的更新,可以结合下图。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/62_6.jpg) - -这样的话,就是从左向右,从上到下一行一行更新(当前也可以一列一列更新)。 - -```java -public int uniquePaths(int m, int n) { - int[] dp = new int[n]; - for (int i = 0; i < n; i++) { - dp[i] = 1; - } - - for (int i = 1; i < m; i++) { - for (int j = 1; j < n; j++) { - dp[j] = dp[j] + dp[j - 1]; - } - } - return dp[(n - 1)]; -} -``` - -时间复杂度:O(m * n)。 - -空间复杂度:O(n)。 - -# 解法三 公式 - -参考[这里](https://leetcode.com/problems/unique-paths/discuss/22981/My-AC-solution-using-formula)。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/62_7.jpg) - -我们用 R 表示向右,D 表示向下,然后把所有路线写出来,就会发现神奇的事情了。 - -R R R D D - -R R D D R - -R D R D R - -…… - -从左上角,到右下角,总会是 3 个 R,2 个 D,只是出现的顺序不一样。所以求解法,本质上是求了组合数,N = m + n - 2,也就是总共走的步数。 k = m - 1,也就是向下的步数,D 的个数。所以总共的解就是 $$C^k_n = n!/(k!(n-k)!) = (n*(n-1)*(n-2)*...(n-k+1))/k!$$。 - -```java -public int uniquePaths(int m, int n) { - int N = n + m - 2; - int k = m - 1; - long res = 1; - for (int i = 1; i <= k; i++) - res = res * (N - k + i) / i; - return (int) res; -} -``` - -时间复杂度:O(m)。 - -空间复杂度:O(1)。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/62.jpg) + +机器人从左上角走到右下角,只能向右或者向下走。输出总共有多少种走法。 + +# 解法一 递归 + +求 ( 0 , 0 ) 点到( m - 1 , n - 1) 点的走法。 + +(0,0)点到(m - 1 , n - 1) 点的走法等于(0,0)点右边的点 (1,0)到(m - 1 , n - 1)的走法加上(0,0)点下边的点(0,1)到(m - 1 , n - 1)的走法。 + +而左边的点(1,0)点到(m - 1 , n - 1) 点的走法等于(2,0) 点到(m - 1 , n - 1)的走法加上(1,1)点到(m - 1 , n - 1)的走法。 + +下边的点(0,1)点到(m - 1 , n - 1) 点的走法等于(1,1)点到(m - 1 , n - 1)的走法加上(0,2)点到(m - 1 , n - 1)的走法。 + +然后一直递归下去,直到 (m - 1 , n - 1) 点到(m - 1 , n - 1) ,返回 1。 + +```java +public int uniquePaths(int m, int n) { + HashMap visited = new HashMap<>(); + return getAns(0, 0, m - 1, n - 1, 0); + +} + +private int getAns(int x, int y, int m, int n, int num) { + if (x == m && y == n) { + return 1; + } + int n1 = 0; + int n2 = 0; + //向右探索的所有解 + if (x + 1 <= m) { + n1 = getAns(x + 1, y, m, n, num); + } + //向左探索的所有解 + if (y + 1 <= n) { + n2 = getAns(x, y + 1, m, n, num); + } + //加起来 + return n1 + n2; +} +``` + +时间复杂度: + +空间复杂度: + +遗憾的是,这个算法在 LeetCode 上超时了。我们可以优化下,问题出在当我们求点 (x,y)到(m - 1 , n - 1) 点的走法的时候,递归求了点 (x,y)点右边的点 (x + 1,0)到(m - 1 , n - 1)的走法和(x,y)下边的点(x,y + 1)到(m - 1 , n - 1)的走法。而没有考虑到(x + 1,0)到(m - 1 , n - 1)的走法和点(x,y + 1)到(m - 1 , n - 1)的走法是否是之前已经求过了。事实上,很多点求的时候后边的的点已经求过了,所以再进行递归是没有必要的。基于此,我们可以用 visited 保存已经求过的点。 + +```java +public int uniquePaths(int m, int n) { + HashMap visited = new HashMap<>(); + return getAns(0, 0, m - 1, n - 1, 0, visited); + +} +private int getAns(int x, int y, int m, int n, int num, HashMap visited) { + if (x == m && y == n) { + return 1; + } + int n1 = 0; + int n2 = 0; + String key = x + 1 + "@" + y; + //判断当前点是否已经求过了 + if (!visited.containsKey(key)) { + if (x + 1 <= m) { + n1 = getAns(x + 1, y, m, n, num, visited); + } + } else { + n1 = visited.get(key); + } + key = x + "@" + (y + 1); + if (!visited.containsKey(key)) { + if (y + 1 <= n) { + n2 = getAns(x, y + 1, m, n, num, visited); + } + } else { + n2 = visited.get(key); + } + //将当前点加入 visited 中 + key = x + "@" + y; + visited.put(key, n1+n2); + return n1 + n2; +} +``` + +时间复杂度: + +空间复杂度: + +# 解法二 动态规划 + +解法一是基于递归的,压栈浪费了很多时间。我们来分析一下,压栈的过程,然后我们其实完全可以省略压栈的过程,直接用迭代去实现。 + +如下图,如果是递归的话,根据上边的代码,从 (0,0)点向右压栈,向右压栈,到最右边后,就向下压栈,向下压栈,到最下边以后,就开始出栈。出栈过程就是橙色部分。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/62_2.jpg) + +然后根据代码,继续压栈前一列,下图的橙色部分,然后到最下边后,然后开始出栈,根据它的右边的点和下边的点计算当前的点的走法。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/62_3.jpg) + +接下来两步同理,压栈,出栈。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/62_4.jpg) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/62_5.jpg) + +我们现在要做的就是要省略压栈的过程,直接出栈。很明显可以做到的,只需要初始化最后一列为 1 ,然后 1 列,1 列的向前更新就可以了。有一些动态规划的思想了。 + +```java +public int uniquePaths(int m, int n) { + int[] dp = new int[m]; + //初始化最后一列 + for (int i = 0; i < m; i++) { + dp[i] = 1; + } + //从右向左更新所有列 + for (int i = n - 2; i >= 0; i--) { + //最后一行永远是 1,所以从倒数第 2 行开始 + //从下向上更新所有行 + for (int j = m - 2; j >= 0; j--) { + //右边的和下边的更新当前元素 + dp[j] = dp[j] + dp[j + 1]; + } + } + return dp[0]; +} +``` + +时间复杂度:O(m * n)。 + +空间复杂度:O(m)。 + +[这里](https://leetcode.com/problems/unique-paths/discuss/22954/C%2B%2B-DP)也有一个类似的想法。不过他是正向考虑的,和上边的想法刚好相反。如果把 dp \[ i \] [ j \] 表示为从点 (0,0)到点 ( i,j)的走法。 + +上边解法公式就是 dp \[ i \] [ j \] = dp \[ i + 1 \] [ j \] + dp \[ i \] [ j +1 \]。 + +[这里](https://leetcode.com/problems/unique-paths/discuss/22954/C%2B%2B-DP)的话就是 dp \[ i \] [ j \] = dp \[ i - 1 \] [ j \] + dp \[ i \] [ j - 1 \]。就是用它左边的和上边的更新,可以结合下图。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/62_6.jpg) + +这样的话,就是从左向右,从上到下一行一行更新(当前也可以一列一列更新)。 + +```java +public int uniquePaths(int m, int n) { + int[] dp = new int[n]; + for (int i = 0; i < n; i++) { + dp[i] = 1; + } + + for (int i = 1; i < m; i++) { + for (int j = 1; j < n; j++) { + dp[j] = dp[j] + dp[j - 1]; + } + } + return dp[(n - 1)]; +} +``` + +时间复杂度:O(m * n)。 + +空间复杂度:O(n)。 + +# 解法三 公式 + +参考[这里](https://leetcode.com/problems/unique-paths/discuss/22981/My-AC-solution-using-formula)。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/62_7.jpg) + +我们用 R 表示向右,D 表示向下,然后把所有路线写出来,就会发现神奇的事情了。 + +R R R D D + +R R D D R + +R D R D R + +…… + +从左上角,到右下角,总会是 3 个 R,2 个 D,只是出现的顺序不一样。所以求解法,本质上是求了组合数,N = m + n - 2,也就是总共走的步数。 k = m - 1,也就是向下的步数,D 的个数。所以总共的解就是 $$C^k_n = n!/(k!(n-k)!) = (n*(n-1)*(n-2)*...(n-k+1))/k!$$。 + +```java +public int uniquePaths(int m, int n) { + int N = n + m - 2; + int k = m - 1; + long res = 1; + for (int i = 1; i <= k; i++) + res = res * (N - k + i) / i; + return (int) res; +} +``` + +时间复杂度:O(m)。 + +空间复杂度:O(1)。 + +# 总 + 从递归,到递归改迭代,之前的题也都遇到过了,本质上就是省去压栈的过程。解法三的公式法,直接到问题的本质,很厉害。 \ No newline at end of file diff --git a/leetCode-63-Unique-PathsII.md b/leetCode-63-Unique-PathsII.md index 9e5981255..e047492bd 100644 --- a/leetCode-63-Unique-PathsII.md +++ b/leetCode-63-Unique-PathsII.md @@ -1,108 +1,108 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/63.jpg) - -对[62题](https://leetcode.windliang.cc/leetCode-62-Unique-Paths.html)的变体,增加了一些不能走的格子,用 1 表示。还是输出从左上角到右下角总共有多少种走法。 - -没做过[62题](https://leetcode.windliang.cc/leetCode-62-Unique-Paths.html)的话可以先看一下,62 题总结的很详细了,我直接在 62 题的基础上改了。 - -# 解法一 递归 - -```java -public int uniquePathsWithObstacles(int[][] obstacleGrid) { - int m = obstacleGrid.length; - int n = obstacleGrid[0].length; - HashMap visited = new HashMap<>(); - //起点是障碍,直接返回 0 - if (obstacleGrid[0][0] == 1) - return 0; - return getAns(0, 0, m - 1, n - 1, 0, visited, obstacleGrid); -} - -private int getAns(int x, int y, int m, int n, int num, HashMap visited, int[][] obstacleGrid) { - // TODO Auto-generated method stub - if (x == m && y == n) { - return 1; - } - int n1 = 0; - int n2 = 0; - String key = x + 1 + "@" + y; - if (!visited.containsKey(key)) { - //与 62 题不同的地方,增加了判断是否是障碍 - if (x + 1 <= m && obstacleGrid[x + 1][y] == 0) { - n1 = getAns(x + 1, y, m, n, num, visited, obstacleGrid); - } - } else { - n1 = visited.get(key); - } - key = x + "@" + (y + 1); - if (!visited.containsKey(key)) { - //与 62 题不同的地方,增加了判断是否是障碍 - if (y + 1 <= n && obstacleGrid[x][y + 1] == 0) { - n2 = getAns(x, y + 1, m, n, num, visited, obstacleGrid); - } - } else { - n2 = visited.get(key); - } - //将当前点加入 visited 中 - key = x + "@" + y; - visited.put(key, n1+n2); - return n1 + n2; -} -``` - -时间复杂度: - -空间复杂度: - -# 解法二 动态规划 - -在[62题](https://leetcode.windliang.cc/leetCode-62-Unique-Paths.html)解法二最后个想法上改。 - -```java -public int uniquePathsWithObstacles(int[][] obstacleGrid) { - int m = obstacleGrid.length; - int n = obstacleGrid[0].length; - //起点是障碍,直接返回 0 - if (obstacleGrid[0][0] == 1) - return 0; - int[] dp = new int[n]; - int i = 0; - //初始化第一行和 62 题不一样了 - //这里的话不是全部初始化 1 了,因为如果有障碍的话当前列和后边的列就都走不过了,初始化为 0 - for (; i < n; i++) { - if (obstacleGrid[0][i] == 1) { - dp[i] = 0; - break; - } else { - dp[i] = 1; - } - } - //障碍后边的列全部初始化为 0 - for (; i < n; i++) { - dp[i] = 0; - } - for (i = 1; i < m; i++) { - //这里改为从 0 列开始了,因为 62 题中从 1 开始是因为第 0 列永远是 1 不需要更新 - //这里的话,如果第 0 列是障碍的话,需要更新为 0 - for (int j = 0; j < n; j++) { - if (obstacleGrid[i][j] == 1) { - dp[j] = 0; - } else { - //由于从第 0 列开始了,更新公式有 j - 1,所以 j = 0 的时候不更新 - if (j != 0) - dp[j] = dp[j] + dp[j - 1]; - } - } - } - return dp[(n - 1)]; -} -``` - -时间复杂度:O(m * n)。 - -空间复杂度:O(n)。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/63.jpg) + +对[62题](https://leetcode.windliang.cc/leetCode-62-Unique-Paths.html)的变体,增加了一些不能走的格子,用 1 表示。还是输出从左上角到右下角总共有多少种走法。 + +没做过[62题](https://leetcode.windliang.cc/leetCode-62-Unique-Paths.html)的话可以先看一下,62 题总结的很详细了,我直接在 62 题的基础上改了。 + +# 解法一 递归 + +```java +public int uniquePathsWithObstacles(int[][] obstacleGrid) { + int m = obstacleGrid.length; + int n = obstacleGrid[0].length; + HashMap visited = new HashMap<>(); + //起点是障碍,直接返回 0 + if (obstacleGrid[0][0] == 1) + return 0; + return getAns(0, 0, m - 1, n - 1, 0, visited, obstacleGrid); +} + +private int getAns(int x, int y, int m, int n, int num, HashMap visited, int[][] obstacleGrid) { + // TODO Auto-generated method stub + if (x == m && y == n) { + return 1; + } + int n1 = 0; + int n2 = 0; + String key = x + 1 + "@" + y; + if (!visited.containsKey(key)) { + //与 62 题不同的地方,增加了判断是否是障碍 + if (x + 1 <= m && obstacleGrid[x + 1][y] == 0) { + n1 = getAns(x + 1, y, m, n, num, visited, obstacleGrid); + } + } else { + n1 = visited.get(key); + } + key = x + "@" + (y + 1); + if (!visited.containsKey(key)) { + //与 62 题不同的地方,增加了判断是否是障碍 + if (y + 1 <= n && obstacleGrid[x][y + 1] == 0) { + n2 = getAns(x, y + 1, m, n, num, visited, obstacleGrid); + } + } else { + n2 = visited.get(key); + } + //将当前点加入 visited 中 + key = x + "@" + y; + visited.put(key, n1+n2); + return n1 + n2; +} +``` + +时间复杂度: + +空间复杂度: + +# 解法二 动态规划 + +在[62题](https://leetcode.windliang.cc/leetCode-62-Unique-Paths.html)解法二最后个想法上改。 + +```java +public int uniquePathsWithObstacles(int[][] obstacleGrid) { + int m = obstacleGrid.length; + int n = obstacleGrid[0].length; + //起点是障碍,直接返回 0 + if (obstacleGrid[0][0] == 1) + return 0; + int[] dp = new int[n]; + int i = 0; + //初始化第一行和 62 题不一样了 + //这里的话不是全部初始化 1 了,因为如果有障碍的话当前列和后边的列就都走不过了,初始化为 0 + for (; i < n; i++) { + if (obstacleGrid[0][i] == 1) { + dp[i] = 0; + break; + } else { + dp[i] = 1; + } + } + //障碍后边的列全部初始化为 0 + for (; i < n; i++) { + dp[i] = 0; + } + for (i = 1; i < m; i++) { + //这里改为从 0 列开始了,因为 62 题中从 1 开始是因为第 0 列永远是 1 不需要更新 + //这里的话,如果第 0 列是障碍的话,需要更新为 0 + for (int j = 0; j < n; j++) { + if (obstacleGrid[i][j] == 1) { + dp[j] = 0; + } else { + //由于从第 0 列开始了,更新公式有 j - 1,所以 j = 0 的时候不更新 + if (j != 0) + dp[j] = dp[j] + dp[j - 1]; + } + } + } + return dp[(n - 1)]; +} +``` + +时间复杂度:O(m * n)。 + +空间复杂度:O(n)。 + +# 总 + 和 62 题改动不大,就是在障碍的地方,更新的时候需要注意一下。 \ No newline at end of file diff --git a/leetCode-64-Minimum-PathSum.md b/leetCode-64-Minimum-PathSum.md index 3da60a479..e6f848461 100644 --- a/leetCode-64-Minimum-PathSum.md +++ b/leetCode-64-Minimum-PathSum.md @@ -1,94 +1,94 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/64.jpg) - -依旧是[62题](https://leetcode.windliang.cc/leetCode-62-Unique-Paths.html)的扩展,这个是输出从左上角到右下角,路径的数字加起来和最小是多少。 - -依旧在[62题](https://leetcode.windliang.cc/leetCode-62-Unique-Paths.html)代码的基础上改,大家可以先看下 62 題。 - -# 解法一 递归 - -62 题中我们把递归 getAns 定义为,输出 (x,y)到 (m ,n ) 的路径数,如果记做 dp[x\][y\]。 - -那么递推式就是 dp[x\][y\] = dp[x\][y+1\] + dp[x+1\][y\]。 - -这道题的话,把递归 getAns 定义为,输出 (x,y)到 (m,n ) 的路径和最小是多少。同样如果记做 dp[x\][y\]。这样的话, dp[x\][y\] = Min(dp[x\][y+1\] + dp[x+1\][y\])+ grid[x\][y\]。很好理解,就是当前点的右边和下边取一个和较小的,然后加上当前点的权值。 - -```java -public int minPathSum(int[][] grid) { - int m = grid.length; - int n = grid[0].length; - HashMap visited = new HashMap<>(); - return getAns(0, 0, m - 1, n - 1, 0, visited, grid); -} - -private int getAns(int x, int y, int m, int n, int num, HashMap visited, int[][] grid) { - // 到了终点,返回终点的权值 - if (x == m && y == n) { - return grid[m][n]; - } - int n1 = Integer.MAX_VALUE; - int n2 = Integer.MAX_VALUE; - String key = x + 1 + "@" + y; - if (!visited.containsKey(key)) { - if (x + 1 <= m) { - n1 = getAns(x + 1, y, m, n, num, visited, grid); - } - } else { - n1 = visited.get(key); - } - key = x + "@" + (y + 1); - if (!visited.containsKey(key)) { - if (y + 1 <= n) { - n2 = getAns(x, y + 1, m, n, num, visited, grid); - } - } else { - n2 = visited.get(key); - } - // 将当前点加入 visited 中 - key = x + "@" + y; - visited.put(key, Math.min(n1, n2) + grid[x][y]); - //返回两个之间较小的,并且加上当前权值 - return Math.min(n1, n2) + grid[x][y]; -} -``` - -时间复杂度: - -空间复杂度: - -# 解法二 - -这里我们直接用 grid 覆盖存,不去 new 一个 n 的空间了。 - -```java -public int minPathSum(int[][] grid) { - int m = grid.length; - int n = grid[0].length; - //由于第一行和第一列不能用我们的递推式,所以单独更新 - //更新第一行的权值 - for (int i = 1; i < n; i++) { - grid[0][i] = grid[0][i - 1] + grid[0][i]; - } - //更新第一列的权值 - for (int i = 1; i < m; i++) { - grid[i][0] = grid[i - 1][0] + grid[i][0]; - } - //利用递推式更新其它的 - for (int i = 1; i < m; i++) { - for (int j = 1; j < n; j++) { - grid[i][j] = Math.min(grid[i][j - 1], grid[i - 1][j]) + grid[i][j]; - - } - } - return grid[m - 1][n - 1]; -} -``` - -时间复杂度:O(m * n)。 - -空间复杂度:O(1)。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/64.jpg) + +依旧是[62题](https://leetcode.windliang.cc/leetCode-62-Unique-Paths.html)的扩展,这个是输出从左上角到右下角,路径的数字加起来和最小是多少。 + +依旧在[62题](https://leetcode.windliang.cc/leetCode-62-Unique-Paths.html)代码的基础上改,大家可以先看下 62 題。 + +# 解法一 递归 + +62 题中我们把递归 getAns 定义为,输出 (x,y)到 (m ,n ) 的路径数,如果记做 dp[x\][y\]。 + +那么递推式就是 dp[x\][y\] = dp[x\][y+1\] + dp[x+1\][y\]。 + +这道题的话,把递归 getAns 定义为,输出 (x,y)到 (m,n ) 的路径和最小是多少。同样如果记做 dp[x\][y\]。这样的话, dp[x\][y\] = Min(dp[x\][y+1\] + dp[x+1\][y\])+ grid[x\][y\]。很好理解,就是当前点的右边和下边取一个和较小的,然后加上当前点的权值。 + +```java +public int minPathSum(int[][] grid) { + int m = grid.length; + int n = grid[0].length; + HashMap visited = new HashMap<>(); + return getAns(0, 0, m - 1, n - 1, 0, visited, grid); +} + +private int getAns(int x, int y, int m, int n, int num, HashMap visited, int[][] grid) { + // 到了终点,返回终点的权值 + if (x == m && y == n) { + return grid[m][n]; + } + int n1 = Integer.MAX_VALUE; + int n2 = Integer.MAX_VALUE; + String key = x + 1 + "@" + y; + if (!visited.containsKey(key)) { + if (x + 1 <= m) { + n1 = getAns(x + 1, y, m, n, num, visited, grid); + } + } else { + n1 = visited.get(key); + } + key = x + "@" + (y + 1); + if (!visited.containsKey(key)) { + if (y + 1 <= n) { + n2 = getAns(x, y + 1, m, n, num, visited, grid); + } + } else { + n2 = visited.get(key); + } + // 将当前点加入 visited 中 + key = x + "@" + y; + visited.put(key, Math.min(n1, n2) + grid[x][y]); + //返回两个之间较小的,并且加上当前权值 + return Math.min(n1, n2) + grid[x][y]; +} +``` + +时间复杂度: + +空间复杂度: + +# 解法二 + +这里我们直接用 grid 覆盖存,不去 new 一个 n 的空间了。 + +```java +public int minPathSum(int[][] grid) { + int m = grid.length; + int n = grid[0].length; + //由于第一行和第一列不能用我们的递推式,所以单独更新 + //更新第一行的权值 + for (int i = 1; i < n; i++) { + grid[0][i] = grid[0][i - 1] + grid[0][i]; + } + //更新第一列的权值 + for (int i = 1; i < m; i++) { + grid[i][0] = grid[i - 1][0] + grid[i][0]; + } + //利用递推式更新其它的 + for (int i = 1; i < m; i++) { + for (int j = 1; j < n; j++) { + grid[i][j] = Math.min(grid[i][j - 1], grid[i - 1][j]) + grid[i][j]; + + } + } + return grid[m - 1][n - 1]; +} +``` + +时间复杂度:O(m * n)。 + +空间复杂度:O(1)。 + +# 总 + 依旧是[62题](https://leetcode.windliang.cc/leetCode-62-Unique-Paths.html)的扩展,理解了 62 题的话,很快就写出来了。 \ No newline at end of file diff --git a/leetCode-65-Valid-Number.md b/leetCode-65-Valid-Number.md index 29a9053f4..9cd2bc393 100644 --- a/leetCode-65-Valid-Number.md +++ b/leetCode-65-Valid-Number.md @@ -1,500 +1,500 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/65.jpg) - -给定一个字符串,判断它是否代表合法数字,当然题目描述的样例不够多,会使得设计算法中出现很多遗漏的地方,这里直接参考[评论区](https://leetcode.com/problems/valid-number/discuss/23741/The-worst-problem-i-have-ever-met-in-this-oj)@[yeelan0319](https://leetcode.com/yeelan0319)给出的更多测试样例。 - -```java -test(1, "123", true); -test(2, " 123 ", true); -test(3, "0", true); -test(4, "0123", true); //Cannot agree -test(5, "00", true); //Cannot agree -test(6, "-10", true); -test(7, "-0", true); -test(8, "123.5", true); -test(9, "123.000000", true); -test(10, "-500.777", true); -test(11, "0.0000001", true); -test(12, "0.00000", true); -test(13, "0.", true); //Cannot be more disagree!!! -test(14, "00.5", true); //Strongly cannot agree -test(15, "123e1", true); -test(16, "1.23e10", true); -test(17, "0.5e-10", true); -test(18, "1.0e4.5", false); -test(19, "0.5e04", true); -test(20, "12 3", false); -test(21, "1a3", false); -test(22, "", false); -test(23, " ", false); -test(24, null, false); -test(25, ".1", true); //Ok, if you say so -test(26, ".", false); -test(27, "2e0", true); //Really?! -test(28, "+.8", true); -test(29, " 005047e+6", true); //Damn = =||| -``` - -# 解法一 直接法 - -什么叫直接法呢,就是没有什么通用的方法,直接分析题目,然后写代码,直接贴两个 leetcode Disscuss 的代码吧,供参考。 - -[想法一](https://leetcode.com/problems/valid-number/discuss/23738/Clear-Java-solution-with-ifs)。 - -把当前的输入分成几类,再用几个标志位来判断当前是否合法。 - -```java -public boolean isNumber(String s) { - s = s.trim(); - - boolean pointSeen = false; - boolean eSeen = false; - boolean numberSeen = false; - boolean numberAfterE = true; - for(int i=0; i='0') || s[i]=='.'; i++) - s[i] == '.' ? n_pt++:n_nm++; - if(n_pt>1 || n_nm<1) // no more than one point, at least one digit - return false; - - // check the exponent if exist - if(s[i] == 'e') { - i++; - if(s[i] == '+' || s[i] == '-') i++; // skip the sign - - int n_nm = 0; - for(; s[i]>='0' && s[i]<='9'; i++, n_nm++) {} - if(n_nm<1) - return false; - } - - // skip the trailing whitespaces - for(; s[i] == ' '; i++) {} - - return s[i]==0; // must reach the ending 0 of the string -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(1)。 - -# 解法二 自动机 - -自己最开始想到的就是这个,编译原理时候在学到的自动机,就是一些状态转移。这一块内容很多,自己也很多东西都忘了,但不影响我们写算法,主要参考[这里](https://leetcode.com/problems/valid-number/discuss/23725/C%2B%2B-My-thought-with-DFA)。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/65_2.jpg) - -如上图,从 0 开始总共有 9 个状态,橙色代表可接受状态,也就是表示此时是合法数字。总共有四大类输入,数字,小数点,e 和 正负号。我们只需要将这个图实现就够了。 - -```java -public boolean isNumber(String s) { - int state = 0; - s = s.trim();//去除头尾的空格 - //遍历所有字符,当做输入 - for (int i = 0; i < s.length(); i++) { - switch (s.charAt(i)) { - //输入正负号 - case '+': - case '-': - if (state == 0) { - state = 1; - } else if (state == 4) { - state = 6; - } else { - return false; - } - break; - //输入数字 - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - //根据当前状态去跳转 - switch (state) { - case 0: - case 1: - case 2: - state = 2; - break; - case 3: - state = 3; - break; - case 4: - case 5: - case 6: - state = 5; - break; - case 7: - state = 8; - break; - case 8: - state = 8; - break; - default: - return false; - } - break; - //小数点 - case '.': - switch (state) { - case 0: - case 1: - state = 7; - break; - case 2: - state = 3; - break; - default: - return false; - } - break; - //e - case 'e': - switch (state) { - case 2: - case 3: - case 8: - state = 4; - break; - default: - return false; - } - break; - default: - return false; - - } - } - //橙色部分的状态代表合法数字 - return state == 2 || state == 3 || state == 5 || state == 8; -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(1)。 - -# 解法三 责任链模式 - -解法二看起来已经很清晰明了了,只需要把状态图画出来,然后实现代码就很简单了。但是缺点是,如果状态图少考虑了东西,再改起来就会很麻烦。 - -[这里](https://leetcode.com/problems/valid-number/discuss/23977/A-clean-design-solution-By-using-design-pattern)作者提出来,利用责任链的设计模式,会使得写出的算法扩展性以及维护性更高。这里用到的思想就是,每个类只判断一种类型。比如判断是否是正数的类,判断是否是小数的类,判断是否是科学计数法的类,这样每个类只关心自己的部分,出了问题很好排查,而且互不影响。 - -```java -//每个类都实现这个接口 -interface NumberValidate { - boolean validate(String s); -} -//定义一个抽象类,用来检查一些基础的操作,是否为空,去掉首尾空格,去掉 +/- -//doValidate 交给子类自己去实现 -abstract class NumberValidateTemplate implements NumberValidate{ - - public boolean validate(String s) - { - if (checkStringEmpty(s)) - { - return false; - } - - s = checkAndProcessHeader(s); - - if (s.length() == 0) - { - return false; - } - - return doValidate(s); - } - - private boolean checkStringEmpty(String s) - { - if (s.equals("")) - { - return true; - } - - return false; - } - - private String checkAndProcessHeader(String value) - { - value = value.trim(); - - if (value.startsWith("+") || value.startsWith("-")) - { - value = value.substring(1); - } - - - return value; - } - - - - protected abstract boolean doValidate(String s); -} - -//实现 doValidate 判断是否是整数 -class IntegerValidate extends NumberValidateTemplate{ - - protected boolean doValidate(String integer) - { - for (int i = 0; i < integer.length(); i++) - { - if(Character.isDigit(integer.charAt(i)) == false) - { - return false; - } - } - - return true; - } -} - -//实现 doValidate 判断是否是科学计数法 -class SienceFormatValidate extends NumberValidateTemplate{ - - protected boolean doValidate(String s) - { - s = s.toLowerCase(); - int pos = s.indexOf("e"); - if (pos == -1) - { - return false; - } - - if (s.length() == 1) - { - return false; - } - - String first = s.substring(0, pos); - String second = s.substring(pos+1, s.length()); - - if (validatePartBeforeE(first) == false || validatePartAfterE(second) == false) - { - return false; - } - - - return true; - } - - private boolean validatePartBeforeE(String first) - { - if (first.equals("") == true) - { - return false; - } - - if (checkHeadAndEndForSpace(first) == false) - { - return false; - } - - NumberValidate integerValidate = new IntegerValidate(); - NumberValidate floatValidate = new FloatValidate(); - if (integerValidate.validate(first) == false && floatValidate.validate(first) == false) - { - return false; - } - - return true; - } - - private boolean checkHeadAndEndForSpace(String part) - { - - if (part.startsWith(" ") || - part.endsWith(" ")) - { - return false; - } - - return true; - } - - private boolean validatePartAfterE(String second) - { - if (second.equals("") == true) - { - return false; - } - - if (checkHeadAndEndForSpace(second) == false) - { - return false; - } - - NumberValidate integerValidate = new IntegerValidate(); - if (integerValidate.validate(second) == false) - { - return false; - } - - return true; - } -} -//实现 doValidate 判断是否是小数 -class FloatValidate extends NumberValidateTemplate{ - - protected boolean doValidate(String floatVal) - { - int pos = floatVal.indexOf("."); - if (pos == -1) - { - return false; - } - - if (floatVal.length() == 1) - { - return false; - } - - NumberValidate nv = new IntegerValidate(); - String first = floatVal.substring(0, pos); - String second = floatVal.substring(pos + 1, floatVal.length()); - - if (checkFirstPart(first) == true && checkFirstPart(second) == true) - { - return true; - } - - return false; - } - - private boolean checkFirstPart(String first) - { - if (first.equals("") == false && checkPart(first) == false) - { - return false; - } - - return true; - } - - private boolean checkPart(String part) - { - if (Character.isDigit(part.charAt(0)) == false || - Character.isDigit(part.charAt(part.length() - 1)) == false) - { - return false; - } - - NumberValidate nv = new IntegerValidate(); - if (nv.validate(part) == false) - { - return false; - } - - return true; - } -} -//定义一个执行者,我们把之前实现的各个类加到一个数组里,然后依次调用 -class NumberValidator implements NumberValidate { - - private ArrayList validators = new ArrayList(); - - public NumberValidator() - { - addValidators(); - } - - private void addValidators() - { - NumberValidate nv = new IntegerValidate(); - validators.add(nv); - - nv = new FloatValidate(); - validators.add(nv); - - nv = new HexValidate(); - validators.add(nv); - - nv = new SienceFormatValidate(); - validators.add(nv); - } - - @Override - public boolean validate(String s) - { - for (NumberValidate nv : validators) - { - if (nv.validate(s) == true) - { - return true; - } - } - - return false; - } - - -} -public boolean isNumber(String s) { - NumberValidate nv = new NumberValidator(); - return nv.validate(s); -} - -``` - -时间复杂度: - -空间复杂度: - -# 总 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/65.jpg) + +给定一个字符串,判断它是否代表合法数字,当然题目描述的样例不够多,会使得设计算法中出现很多遗漏的地方,这里直接参考[评论区](https://leetcode.com/problems/valid-number/discuss/23741/The-worst-problem-i-have-ever-met-in-this-oj)@[yeelan0319](https://leetcode.com/yeelan0319)给出的更多测试样例。 + +```java +test(1, "123", true); +test(2, " 123 ", true); +test(3, "0", true); +test(4, "0123", true); //Cannot agree +test(5, "00", true); //Cannot agree +test(6, "-10", true); +test(7, "-0", true); +test(8, "123.5", true); +test(9, "123.000000", true); +test(10, "-500.777", true); +test(11, "0.0000001", true); +test(12, "0.00000", true); +test(13, "0.", true); //Cannot be more disagree!!! +test(14, "00.5", true); //Strongly cannot agree +test(15, "123e1", true); +test(16, "1.23e10", true); +test(17, "0.5e-10", true); +test(18, "1.0e4.5", false); +test(19, "0.5e04", true); +test(20, "12 3", false); +test(21, "1a3", false); +test(22, "", false); +test(23, " ", false); +test(24, null, false); +test(25, ".1", true); //Ok, if you say so +test(26, ".", false); +test(27, "2e0", true); //Really?! +test(28, "+.8", true); +test(29, " 005047e+6", true); //Damn = =||| +``` + +# 解法一 直接法 + +什么叫直接法呢,就是没有什么通用的方法,直接分析题目,然后写代码,直接贴两个 leetcode Disscuss 的代码吧,供参考。 + +[想法一](https://leetcode.com/problems/valid-number/discuss/23738/Clear-Java-solution-with-ifs)。 + +把当前的输入分成几类,再用几个标志位来判断当前是否合法。 + +```java +public boolean isNumber(String s) { + s = s.trim(); + + boolean pointSeen = false; + boolean eSeen = false; + boolean numberSeen = false; + boolean numberAfterE = true; + for(int i=0; i='0') || s[i]=='.'; i++) + s[i] == '.' ? n_pt++:n_nm++; + if(n_pt>1 || n_nm<1) // no more than one point, at least one digit + return false; + + // check the exponent if exist + if(s[i] == 'e') { + i++; + if(s[i] == '+' || s[i] == '-') i++; // skip the sign + + int n_nm = 0; + for(; s[i]>='0' && s[i]<='9'; i++, n_nm++) {} + if(n_nm<1) + return false; + } + + // skip the trailing whitespaces + for(; s[i] == ' '; i++) {} + + return s[i]==0; // must reach the ending 0 of the string +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(1)。 + +# 解法二 自动机 + +自己最开始想到的就是这个,编译原理时候在学到的自动机,就是一些状态转移。这一块内容很多,自己也很多东西都忘了,但不影响我们写算法,主要参考[这里](https://leetcode.com/problems/valid-number/discuss/23725/C%2B%2B-My-thought-with-DFA)。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/65_2.jpg) + +如上图,从 0 开始总共有 9 个状态,橙色代表可接受状态,也就是表示此时是合法数字。总共有四大类输入,数字,小数点,e 和 正负号。我们只需要将这个图实现就够了。 + +```java +public boolean isNumber(String s) { + int state = 0; + s = s.trim();//去除头尾的空格 + //遍历所有字符,当做输入 + for (int i = 0; i < s.length(); i++) { + switch (s.charAt(i)) { + //输入正负号 + case '+': + case '-': + if (state == 0) { + state = 1; + } else if (state == 4) { + state = 6; + } else { + return false; + } + break; + //输入数字 + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + //根据当前状态去跳转 + switch (state) { + case 0: + case 1: + case 2: + state = 2; + break; + case 3: + state = 3; + break; + case 4: + case 5: + case 6: + state = 5; + break; + case 7: + state = 8; + break; + case 8: + state = 8; + break; + default: + return false; + } + break; + //小数点 + case '.': + switch (state) { + case 0: + case 1: + state = 7; + break; + case 2: + state = 3; + break; + default: + return false; + } + break; + //e + case 'e': + switch (state) { + case 2: + case 3: + case 8: + state = 4; + break; + default: + return false; + } + break; + default: + return false; + + } + } + //橙色部分的状态代表合法数字 + return state == 2 || state == 3 || state == 5 || state == 8; +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(1)。 + +# 解法三 责任链模式 + +解法二看起来已经很清晰明了了,只需要把状态图画出来,然后实现代码就很简单了。但是缺点是,如果状态图少考虑了东西,再改起来就会很麻烦。 + +[这里](https://leetcode.com/problems/valid-number/discuss/23977/A-clean-design-solution-By-using-design-pattern)作者提出来,利用责任链的设计模式,会使得写出的算法扩展性以及维护性更高。这里用到的思想就是,每个类只判断一种类型。比如判断是否是正数的类,判断是否是小数的类,判断是否是科学计数法的类,这样每个类只关心自己的部分,出了问题很好排查,而且互不影响。 + +```java +//每个类都实现这个接口 +interface NumberValidate { + boolean validate(String s); +} +//定义一个抽象类,用来检查一些基础的操作,是否为空,去掉首尾空格,去掉 +/- +//doValidate 交给子类自己去实现 +abstract class NumberValidateTemplate implements NumberValidate{ + + public boolean validate(String s) + { + if (checkStringEmpty(s)) + { + return false; + } + + s = checkAndProcessHeader(s); + + if (s.length() == 0) + { + return false; + } + + return doValidate(s); + } + + private boolean checkStringEmpty(String s) + { + if (s.equals("")) + { + return true; + } + + return false; + } + + private String checkAndProcessHeader(String value) + { + value = value.trim(); + + if (value.startsWith("+") || value.startsWith("-")) + { + value = value.substring(1); + } + + + return value; + } + + + + protected abstract boolean doValidate(String s); +} + +//实现 doValidate 判断是否是整数 +class IntegerValidate extends NumberValidateTemplate{ + + protected boolean doValidate(String integer) + { + for (int i = 0; i < integer.length(); i++) + { + if(Character.isDigit(integer.charAt(i)) == false) + { + return false; + } + } + + return true; + } +} + +//实现 doValidate 判断是否是科学计数法 +class SienceFormatValidate extends NumberValidateTemplate{ + + protected boolean doValidate(String s) + { + s = s.toLowerCase(); + int pos = s.indexOf("e"); + if (pos == -1) + { + return false; + } + + if (s.length() == 1) + { + return false; + } + + String first = s.substring(0, pos); + String second = s.substring(pos+1, s.length()); + + if (validatePartBeforeE(first) == false || validatePartAfterE(second) == false) + { + return false; + } + + + return true; + } + + private boolean validatePartBeforeE(String first) + { + if (first.equals("") == true) + { + return false; + } + + if (checkHeadAndEndForSpace(first) == false) + { + return false; + } + + NumberValidate integerValidate = new IntegerValidate(); + NumberValidate floatValidate = new FloatValidate(); + if (integerValidate.validate(first) == false && floatValidate.validate(first) == false) + { + return false; + } + + return true; + } + + private boolean checkHeadAndEndForSpace(String part) + { + + if (part.startsWith(" ") || + part.endsWith(" ")) + { + return false; + } + + return true; + } + + private boolean validatePartAfterE(String second) + { + if (second.equals("") == true) + { + return false; + } + + if (checkHeadAndEndForSpace(second) == false) + { + return false; + } + + NumberValidate integerValidate = new IntegerValidate(); + if (integerValidate.validate(second) == false) + { + return false; + } + + return true; + } +} +//实现 doValidate 判断是否是小数 +class FloatValidate extends NumberValidateTemplate{ + + protected boolean doValidate(String floatVal) + { + int pos = floatVal.indexOf("."); + if (pos == -1) + { + return false; + } + + if (floatVal.length() == 1) + { + return false; + } + + NumberValidate nv = new IntegerValidate(); + String first = floatVal.substring(0, pos); + String second = floatVal.substring(pos + 1, floatVal.length()); + + if (checkFirstPart(first) == true && checkFirstPart(second) == true) + { + return true; + } + + return false; + } + + private boolean checkFirstPart(String first) + { + if (first.equals("") == false && checkPart(first) == false) + { + return false; + } + + return true; + } + + private boolean checkPart(String part) + { + if (Character.isDigit(part.charAt(0)) == false || + Character.isDigit(part.charAt(part.length() - 1)) == false) + { + return false; + } + + NumberValidate nv = new IntegerValidate(); + if (nv.validate(part) == false) + { + return false; + } + + return true; + } +} +//定义一个执行者,我们把之前实现的各个类加到一个数组里,然后依次调用 +class NumberValidator implements NumberValidate { + + private ArrayList validators = new ArrayList(); + + public NumberValidator() + { + addValidators(); + } + + private void addValidators() + { + NumberValidate nv = new IntegerValidate(); + validators.add(nv); + + nv = new FloatValidate(); + validators.add(nv); + + nv = new HexValidate(); + validators.add(nv); + + nv = new SienceFormatValidate(); + validators.add(nv); + } + + @Override + public boolean validate(String s) + { + for (NumberValidate nv : validators) + { + if (nv.validate(s) == true) + { + return true; + } + } + + return false; + } + + +} +public boolean isNumber(String s) { + NumberValidate nv = new NumberValidator(); + return nv.validate(s); +} + +``` + +时间复杂度: + +空间复杂度: + +# 总 + 解法二中自动机的应用,会使得自己的思路更清晰。而解法三中,作者提出的对设计模式的应用,使自己眼前一亮,虽然代码变多了,但是维护性,扩展性变的很强了。比如,题目新增了一种情况,"0x123" 16 进制也算是合法数字。这样的话,解法一和解法二就没什么用了,完全得重新设计。但对于解法三,我们只需要新增一个类,专门判断这种情况,然后加到执行者的数组里就够了,很棒! \ No newline at end of file diff --git a/leetCode-66-Plus-One.md b/leetCode-66-Plus-One.md index 3c73d6710..144a9bbc4 100644 --- a/leetCode-66-Plus-One.md +++ b/leetCode-66-Plus-One.md @@ -1,75 +1,75 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/66.jpg) - -数组代表一个数字,[ 1, 2, 3 ] 就代表 123,然后给它加上 1,输出新的数组。数组每个位置只保存 1 位,也就是 0 到 9。 - -# 解法一 递归 - -先用递归,好理解一些。 - -```java -public int[] plusOne(int[] digits) { - return plusOneAtIndex(digits, digits.length - 1); -} - -private int[] plusOneAtIndex(int[] digits, int index) { - //说明每一位都是 9 - if (index < 0) { - //新建一个更大的数组,最高位赋值为 1 - int[] ans = new int[digits.length + 1]; - ans[0] = 1; - //其他位赋值为 0,因为 java 里默认是 0,所以不需要管了 - return ans; - } - //如果当前位小于 9,直接加 1 返回 - if (digits[index] < 9) { - digits[index] += 1; - return digits; - } - - //否则的话当前位置为 0, - digits[index] = 0; - //考虑给前一位加 1 - return plusOneAtIndex(digits, index - 1); - -} -``` - -时间复杂度:O(n)。 - -空间复杂度: - -# 解法二 迭代 - -上边的递归,属于[尾递归](https://www.zhihu.com/question/20761771),完全没必要压栈,直接改成迭代的形式吧。 - -```java -public int[] plusOne(int[] digits) { - //从最低位遍历 - for (int i = digits.length - 1; i >= 0; i--) { - //小于 9 的话,直接加 1,结束循环 - if (digits[i] < 9) { - digits[i] += 1; - break; - } - //否则的话置为 0 - digits[i] = 0; - } - //最高位如果置为 0 了,说明最高位产生了进位 - if (digits[0] == 0) { - int[] ans = new int[digits.length + 1]; - ans[0] = 1; - digits = ans; - } - return digits; -} -``` - -时间复杂度:O(n)。 - -空间复杂度: - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/66.jpg) + +数组代表一个数字,[ 1, 2, 3 ] 就代表 123,然后给它加上 1,输出新的数组。数组每个位置只保存 1 位,也就是 0 到 9。 + +# 解法一 递归 + +先用递归,好理解一些。 + +```java +public int[] plusOne(int[] digits) { + return plusOneAtIndex(digits, digits.length - 1); +} + +private int[] plusOneAtIndex(int[] digits, int index) { + //说明每一位都是 9 + if (index < 0) { + //新建一个更大的数组,最高位赋值为 1 + int[] ans = new int[digits.length + 1]; + ans[0] = 1; + //其他位赋值为 0,因为 java 里默认是 0,所以不需要管了 + return ans; + } + //如果当前位小于 9,直接加 1 返回 + if (digits[index] < 9) { + digits[index] += 1; + return digits; + } + + //否则的话当前位置为 0, + digits[index] = 0; + //考虑给前一位加 1 + return plusOneAtIndex(digits, index - 1); + +} +``` + +时间复杂度:O(n)。 + +空间复杂度: + +# 解法二 迭代 + +上边的递归,属于[尾递归](https://www.zhihu.com/question/20761771),完全没必要压栈,直接改成迭代的形式吧。 + +```java +public int[] plusOne(int[] digits) { + //从最低位遍历 + for (int i = digits.length - 1; i >= 0; i--) { + //小于 9 的话,直接加 1,结束循环 + if (digits[i] < 9) { + digits[i] += 1; + break; + } + //否则的话置为 0 + digits[i] = 0; + } + //最高位如果置为 0 了,说明最高位产生了进位 + if (digits[0] == 0) { + int[] ans = new int[digits.length + 1]; + ans[0] = 1; + digits = ans; + } + return digits; +} +``` + +时间复杂度:O(n)。 + +空间复杂度: + +# 总 + 很简单的一道题,理解题意就可以了。有的编译器不进行尾递归优化,他遇到这种尾递归还是不停压栈压栈压栈,所以这种简单的我们直接用迭代就行。 \ No newline at end of file diff --git a/leetCode-67-Add Binary.md b/leetCode-67-Add Binary.md index c8a70323f..7d26c6394 100644 --- a/leetCode-67-Add Binary.md +++ b/leetCode-67-Add Binary.md @@ -1,97 +1,97 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/67.jpg) - -两个二进制数相加,返回结果,要注意到字符串的最低位代表着数字的最高位。例如 "100" 最高位(十进制中的百位的位置)是 1,但是对应的字符串的下标是 0。 - -# 解法一 - -开始的时候以为会有什么特殊的方法,然后想着不管了,先按[第二题](https://leetcode.windliang.cc/leetCode-2-Add-Two-Numbers.html)两个十进制数相加的想法写吧。 - -```java -public String addBinary(String a, String b) { - StringBuilder ans = new StringBuilder(); - int i = a.length() - 1; - int j = b.length() - 1; - int carry = 0; - while (i >= 0 || j >= 0) { - int num1 = i >= 0 ? a.charAt(i) - 48 : 0; - int num2 = j >= 0 ? b.charAt(j) - 48 : 0; - int sum = num1 + num2 + carry; - carry = 0; - if (sum >= 2) { - sum = sum % 2; - carry = 1; - } - ans.insert(0, sum); - i--; - j--; - - } - if (carry == 1) { - ans.insert(0, 1); - } - return ans.toString(); -} -``` - -时间复杂度:O(max (m,n))。m 和 n 分别是字符串 a 和 b 的长度。 - -空间复杂度:O(1)。 - -然后写完以后,在 Discuss 里逛了逛,找找其他的解法。发现基本都是这个思路,但是奇怪的是我的解法,时间上只超过了 60% 的人。然后,点开了超过 100% 的人的解法。 - -```java -public String addBinary2(String a, String b) { - char[] charsA = a.toCharArray(), charsB = b.toCharArray(); - char[] sum = new char[Math.max(a.length(), b.length()) + 1]; - int carry = 0, index = sum.length - 1; - for (int i = charsA.length - 1, j = charsB.length - 1; i >= 0 || j >= 0; i--, j--) { - int aNum = i < 0 ? 0 : charsA[i] - '0'; - int bNum = j < 0 ? 0 : charsB[j] - '0'; - - int s = aNum + bNum + carry; - sum[index--] = (char) (s % 2 + '0'); - carry = s / 2; - } - sum[index] = (char) ('0' + carry); - return carry == 0 ? new String(sum, 1, sum.length - 1) : new String(sum); -} -``` - -和我的思路是一样的,区别在于它提前申请了 sum 的空间,然后直接 index 从最后向 0 依次赋值。 - -因为 String .charAt ( 0 ) 代表的是数字的最高位,而我们计算是从最低位开始的,也就是 lenght - 1开始的,所以在之前的算法中每次得到一个结果我们用的是 ans.insert(0, sum) ,在 0 位置插入新的数。我猜测是这里耗费了很多时间,因为插入的话,会导致数组的后移。 - -我们如果把 insert 换成 append ,然后再最后的结果中再倒置,就会快一些了。 - -```java -public String3 addBinary(String a, String b) { - StringBuilder ans = new StringBuilder(); - int i = a.length() - 1; - int j = b.length() - 1; - int carry = 0; - while (i >= 0 || j >= 0) { - int num1 = i >= 0 ? a.charAt(i) - 48 : 0; - int num2 = j >= 0 ? b.charAt(j) - 48 : 0; - int sum = num1 + num2 + carry; - carry = 0; - if (sum >= 2) { - sum = sum % 2; - carry = 1; - } - ans.append(sum); - i--; - j--; - - } - if (carry == 1) { - ans.append(1); - } - return ans.reverse().toString(); -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/67.jpg) + +两个二进制数相加,返回结果,要注意到字符串的最低位代表着数字的最高位。例如 "100" 最高位(十进制中的百位的位置)是 1,但是对应的字符串的下标是 0。 + +# 解法一 + +开始的时候以为会有什么特殊的方法,然后想着不管了,先按[第二题](https://leetcode.windliang.cc/leetCode-2-Add-Two-Numbers.html)两个十进制数相加的想法写吧。 + +```java +public String addBinary(String a, String b) { + StringBuilder ans = new StringBuilder(); + int i = a.length() - 1; + int j = b.length() - 1; + int carry = 0; + while (i >= 0 || j >= 0) { + int num1 = i >= 0 ? a.charAt(i) - 48 : 0; + int num2 = j >= 0 ? b.charAt(j) - 48 : 0; + int sum = num1 + num2 + carry; + carry = 0; + if (sum >= 2) { + sum = sum % 2; + carry = 1; + } + ans.insert(0, sum); + i--; + j--; + + } + if (carry == 1) { + ans.insert(0, 1); + } + return ans.toString(); +} +``` + +时间复杂度:O(max (m,n))。m 和 n 分别是字符串 a 和 b 的长度。 + +空间复杂度:O(1)。 + +然后写完以后,在 Discuss 里逛了逛,找找其他的解法。发现基本都是这个思路,但是奇怪的是我的解法,时间上只超过了 60% 的人。然后,点开了超过 100% 的人的解法。 + +```java +public String addBinary2(String a, String b) { + char[] charsA = a.toCharArray(), charsB = b.toCharArray(); + char[] sum = new char[Math.max(a.length(), b.length()) + 1]; + int carry = 0, index = sum.length - 1; + for (int i = charsA.length - 1, j = charsB.length - 1; i >= 0 || j >= 0; i--, j--) { + int aNum = i < 0 ? 0 : charsA[i] - '0'; + int bNum = j < 0 ? 0 : charsB[j] - '0'; + + int s = aNum + bNum + carry; + sum[index--] = (char) (s % 2 + '0'); + carry = s / 2; + } + sum[index] = (char) ('0' + carry); + return carry == 0 ? new String(sum, 1, sum.length - 1) : new String(sum); +} +``` + +和我的思路是一样的,区别在于它提前申请了 sum 的空间,然后直接 index 从最后向 0 依次赋值。 + +因为 String .charAt ( 0 ) 代表的是数字的最高位,而我们计算是从最低位开始的,也就是 lenght - 1开始的,所以在之前的算法中每次得到一个结果我们用的是 ans.insert(0, sum) ,在 0 位置插入新的数。我猜测是这里耗费了很多时间,因为插入的话,会导致数组的后移。 + +我们如果把 insert 换成 append ,然后再最后的结果中再倒置,就会快一些了。 + +```java +public String3 addBinary(String a, String b) { + StringBuilder ans = new StringBuilder(); + int i = a.length() - 1; + int j = b.length() - 1; + int carry = 0; + while (i >= 0 || j >= 0) { + int num1 = i >= 0 ? a.charAt(i) - 48 : 0; + int num2 = j >= 0 ? b.charAt(j) - 48 : 0; + int sum = num1 + num2 + carry; + carry = 0; + if (sum >= 2) { + sum = sum % 2; + carry = 1; + } + ans.append(sum); + i--; + j--; + + } + if (carry == 1) { + ans.append(1); + } + return ans.reverse().toString(); +} +``` + +# 总 + 这里看出来多次 insert 会很耗费时间,不如最后直接 reverse。另外提前申请空间,直接根据下标赋值,省去了倒置的时间,很 cool。 \ No newline at end of file diff --git a/leetCode-68-Text-Justification.md b/leetCode-68-Text-Justification.md index 16d1404c3..0adf28db9 100644 --- a/leetCode-68-Text-Justification.md +++ b/leetCode-68-Text-Justification.md @@ -1,239 +1,239 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/68.png) - -一个句子,和一个长度表示一行最长的长度,然后对齐文本,有下边几个规则。 - -1. 同一个单词只能出现在一行中,不能拆分 -2. 一行如果只能放下一个单词,该单词放在最左边,然后空格补齐,例如 "acknowledgement#####",这里只是我为了直观,# 表示空格,题目并没有要求。 -3. 一行如果有多个单词,最左边和最右边不能有空格,每个单词间隙尽量平均,如果无法平均,把剩余的空隙从左边开始分配。例如,"enough###to###explain##to",3 个间隙,每个 2 个空格的话,剩下 2 个空格,从左边依次添加一个空格。 -4. 最后一行执行左对齐,单词间一个空格,末尾用空格补齐。 - -# 解法一 - -这道题关键就是理解题目,然后就是一些细节的把控了,我主要是下边的想法。 - -一行一行计算该行可以放多少个单词,然后计算单词间的空隙是多少,然后把它添加到结果中。 - -```java -public List fullJustify(String[] words, int maxWidth) { - List ans = new ArrayList<>(); - //当前行单词已经占用的长度 - int currentLen = 0; - //保存当前行的单词 - List row = new ArrayList<>(); - //遍历每个单词 - for (int i = 0; i < words.length;) { - //判断加入该单词是否超过最长长度 - //分了两种情况,一种情况是加入第一个单词,不需要多加 1 - //已经有单词的话,再加入单词,需要多加个空格,所以多加了 1 - if (currentLen == 0 && currentLen + words[i].length() <= maxWidth - || currentLen > 0 && currentLen + 1 + words[i].length() <= maxWidth) { - row.add(words[i]); - if (currentLen == 0) { - currentLen = currentLen + words[i].length(); - } else { - currentLen = currentLen + 1 + words[i].length(); - } - i++; - //超过的最长长度,对 row 里边的单词进行处理 - } else { - //计算有多少剩余,也就是总共的空格数,因为之前计算 currentLen 多算了一个空格,这里加回来 - int sub = maxWidth - currentLen + row.size() - 1; - //如果只有一个单词,那么就直接单词加空格就可以 - if (row.size() == 1) { - String blank = getStringBlank(sub); - ans.add(row.get(0) + blank); - } else { - //用来保存当前行的结果 - StringBuilder temp = new StringBuilder(); - //将第一个单词加进来 - temp.append(row.get(0)); - //计算平均空格数 - int averageBlank = sub / (row.size() - 1); - //如果除不尽,计算剩余空格数 - int missing = sub - averageBlank * (row.size() - 1); - //前 missing 的空格数比平均空格数多 1 - String blank = getStringBlank(averageBlank + 1); - int k = 1; - for (int j = 0; j < missing; j++) { - temp.append(blank + row.get(k)); - k++; - } - //剩下的空格数就是求得的平均空格数 - blank = getStringBlank(averageBlank); - for (; k < row.size(); k++) { - temp.append(blank + row.get(k)); - } - //将当前结果加入 - ans.add(temp.toString()); - - } - //清空以及置零 - row = new ArrayList<>(); - currentLen = 0; - - } - } - //单独考虑最后一行,左对齐 - StringBuilder temp = new StringBuilder(); - temp.append(row.get(0)); - for (int i = 1; i < row.size(); i++) { - temp.append(" " + row.get(i)); - } - //剩余部分用空格补齐 - temp.append(getStringBlank(maxWidth - currentLen)); - //最后一行加入到结果中 - ans.add(temp.toString()); - return ans; -} -//得到 n 个空白 -private String getStringBlank(int n) { - StringBuilder str = new StringBuilder(); - for (int i = 0; i < n; i++) { - str.append(" "); - } - return str.toString(); -} -``` - -时间复杂度: - -空间复杂度: - -但是这个算法,在 leetcode 跑,速度只打败了 30% 多的人,1 ms。然后在 discuss 里转了一圈寻求原因,发现大家思路都是这样子,然后找了一个人的跑了下,[链接](https://leetcode.com/problems/text-justification/discuss/24902/Java-easy-to-understand-broken-into-several-functions)。 - -```java -public List fullJustify(String[] words, int maxWidth) { - int left = 0; List result = new ArrayList<>(); - - while (left < words.length) { - int right = findRight(left, words, maxWidth); - result.add(justify(left, right, words, maxWidth)); - left = right + 1; - } - - return result; -} - -//找到当前行最右边的单词下标 -private int findRight(int left, String[] words, int maxWidth) { - int right = left; - int sum = words[right++].length(); - - while (right < words.length && (sum + 1 + words[right].length()) <= maxWidth) - sum += 1 + words[right++].length(); - - return right - 1; -} - -//根据不同的情况添加不同的空格 -private String justify(int left, int right, String[] words, int maxWidth) { - if (right - left == 0) return padResult(words[left], maxWidth); - - boolean isLastLine = right == words.length - 1; - int numSpaces = right - left; - int totalSpace = maxWidth - wordsLength(left, right, words); - - String space = isLastLine ? " " : blank(totalSpace / numSpaces); - int remainder = isLastLine ? 0 : totalSpace % numSpaces; - - StringBuilder result = new StringBuilder(); - for (int i = left; i <= right; i++) - result.append(words[i]) - .append(space) - .append(remainder-- > 0 ? " " : ""); - - return padResult(result.toString().trim(), maxWidth); -} - -//当前单词的长度 -private int wordsLength(int left, int right, String[] words) { - int wordsLength = 0; - for (int i = left; i <= right; i++) wordsLength += words[i].length(); - return wordsLength; -} - -private String padResult(String result, int maxWidth) { - return result + blank(maxWidth - result.length()); -} - -private String blank(int length) { - return new String(new char[length]).replace('\0', ' '); -} -``` - -看了下,发现思想和自己也是一样的。但是这个速度却打败了 100% ,0 ms。考虑了下,差别应该在我的算法里使用了一个叫做 row 的 list 用来保存当前行的单词,用了很多 row.get ( index ),而上边的算法只记录了 left 和 right 下标,取单词直接用的 words 数组。然后尝试着在我之前的算法上改了一下,去掉 row,用两个变量 start 和 end 保存当前行的单词范围。主要是 ( end - start ) 代替了之前的 row.size ( ), words [ start + k ] 代替了之前的 row.get ( k )。 - -```java -public List fullJustify2(String[] words, int maxWidth) { - List ans = new ArrayList<>(); - int currentLen = 0; - int start = 0; - int end = 0; - for (int i = 0; i < words.length;) { - //判断加入该单词是否超过最长长度 - //分了两种情况,一种情况是加入第一个单词,不需要多加 1 - //已经有单词的话,再加入单词,需要多加个空格,所以多加了 1 - if (currentLen == 0 && currentLen + words[i].length() <= maxWidth - || currentLen > 0 && currentLen + 1 + words[i].length() <= maxWidth) { - end++; - if (currentLen == 0) { - currentLen = currentLen + words[i].length(); - } else { - currentLen = currentLen + 1 + words[i].length(); - } - i++; - } else { - int sub = maxWidth - currentLen + (end - start) - 1; - if (end - start == 1) { - String blank = getStringBlank(sub); - ans.add(words[start] + blank); - } else { - StringBuilder temp = new StringBuilder(); - temp.append(words[start]); - int averageBlank = sub / ((end - start) - 1); - //如果除不尽,计算剩余空格数 - int missing = sub - averageBlank * ((end - start) - 1); - String blank = getStringBlank(averageBlank + 1); - int k = 1; - for (int j = 0; j < missing; j++) { - temp.append(blank + words[start+k]); - k++; - } - blank = getStringBlank(averageBlank); - for (; k <(end - start); k++) { - temp.append(blank + words[start+k]); - } - ans.add(temp.toString()); - - } - start = end; - currentLen = 0; - - } - } - StringBuilder temp = new StringBuilder(); - temp.append(words[start]); - for (int i = 1; i < (end - start); i++) { - temp.append(" " + words[start+i]); - } - temp.append(getStringBlank(maxWidth - currentLen)); - ans.add(temp.toString()); - return ans; -} -//得到 n 个空白 -private String getStringBlank(int n) { - StringBuilder str = new StringBuilder(); - for (int i = 0; i < n; i++) { - str.append(" "); - } - return str.toString(); -} -``` - -果然,速度也到了打败 100%,0 ms。 - -# 总 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/68.png) + +一个句子,和一个长度表示一行最长的长度,然后对齐文本,有下边几个规则。 + +1. 同一个单词只能出现在一行中,不能拆分 +2. 一行如果只能放下一个单词,该单词放在最左边,然后空格补齐,例如 "acknowledgement#####",这里只是我为了直观,# 表示空格,题目并没有要求。 +3. 一行如果有多个单词,最左边和最右边不能有空格,每个单词间隙尽量平均,如果无法平均,把剩余的空隙从左边开始分配。例如,"enough###to###explain##to",3 个间隙,每个 2 个空格的话,剩下 2 个空格,从左边依次添加一个空格。 +4. 最后一行执行左对齐,单词间一个空格,末尾用空格补齐。 + +# 解法一 + +这道题关键就是理解题目,然后就是一些细节的把控了,我主要是下边的想法。 + +一行一行计算该行可以放多少个单词,然后计算单词间的空隙是多少,然后把它添加到结果中。 + +```java +public List fullJustify(String[] words, int maxWidth) { + List ans = new ArrayList<>(); + //当前行单词已经占用的长度 + int currentLen = 0; + //保存当前行的单词 + List row = new ArrayList<>(); + //遍历每个单词 + for (int i = 0; i < words.length;) { + //判断加入该单词是否超过最长长度 + //分了两种情况,一种情况是加入第一个单词,不需要多加 1 + //已经有单词的话,再加入单词,需要多加个空格,所以多加了 1 + if (currentLen == 0 && currentLen + words[i].length() <= maxWidth + || currentLen > 0 && currentLen + 1 + words[i].length() <= maxWidth) { + row.add(words[i]); + if (currentLen == 0) { + currentLen = currentLen + words[i].length(); + } else { + currentLen = currentLen + 1 + words[i].length(); + } + i++; + //超过的最长长度,对 row 里边的单词进行处理 + } else { + //计算有多少剩余,也就是总共的空格数,因为之前计算 currentLen 多算了一个空格,这里加回来 + int sub = maxWidth - currentLen + row.size() - 1; + //如果只有一个单词,那么就直接单词加空格就可以 + if (row.size() == 1) { + String blank = getStringBlank(sub); + ans.add(row.get(0) + blank); + } else { + //用来保存当前行的结果 + StringBuilder temp = new StringBuilder(); + //将第一个单词加进来 + temp.append(row.get(0)); + //计算平均空格数 + int averageBlank = sub / (row.size() - 1); + //如果除不尽,计算剩余空格数 + int missing = sub - averageBlank * (row.size() - 1); + //前 missing 的空格数比平均空格数多 1 + String blank = getStringBlank(averageBlank + 1); + int k = 1; + for (int j = 0; j < missing; j++) { + temp.append(blank + row.get(k)); + k++; + } + //剩下的空格数就是求得的平均空格数 + blank = getStringBlank(averageBlank); + for (; k < row.size(); k++) { + temp.append(blank + row.get(k)); + } + //将当前结果加入 + ans.add(temp.toString()); + + } + //清空以及置零 + row = new ArrayList<>(); + currentLen = 0; + + } + } + //单独考虑最后一行,左对齐 + StringBuilder temp = new StringBuilder(); + temp.append(row.get(0)); + for (int i = 1; i < row.size(); i++) { + temp.append(" " + row.get(i)); + } + //剩余部分用空格补齐 + temp.append(getStringBlank(maxWidth - currentLen)); + //最后一行加入到结果中 + ans.add(temp.toString()); + return ans; +} +//得到 n 个空白 +private String getStringBlank(int n) { + StringBuilder str = new StringBuilder(); + for (int i = 0; i < n; i++) { + str.append(" "); + } + return str.toString(); +} +``` + +时间复杂度: + +空间复杂度: + +但是这个算法,在 leetcode 跑,速度只打败了 30% 多的人,1 ms。然后在 discuss 里转了一圈寻求原因,发现大家思路都是这样子,然后找了一个人的跑了下,[链接](https://leetcode.com/problems/text-justification/discuss/24902/Java-easy-to-understand-broken-into-several-functions)。 + +```java +public List fullJustify(String[] words, int maxWidth) { + int left = 0; List result = new ArrayList<>(); + + while (left < words.length) { + int right = findRight(left, words, maxWidth); + result.add(justify(left, right, words, maxWidth)); + left = right + 1; + } + + return result; +} + +//找到当前行最右边的单词下标 +private int findRight(int left, String[] words, int maxWidth) { + int right = left; + int sum = words[right++].length(); + + while (right < words.length && (sum + 1 + words[right].length()) <= maxWidth) + sum += 1 + words[right++].length(); + + return right - 1; +} + +//根据不同的情况添加不同的空格 +private String justify(int left, int right, String[] words, int maxWidth) { + if (right - left == 0) return padResult(words[left], maxWidth); + + boolean isLastLine = right == words.length - 1; + int numSpaces = right - left; + int totalSpace = maxWidth - wordsLength(left, right, words); + + String space = isLastLine ? " " : blank(totalSpace / numSpaces); + int remainder = isLastLine ? 0 : totalSpace % numSpaces; + + StringBuilder result = new StringBuilder(); + for (int i = left; i <= right; i++) + result.append(words[i]) + .append(space) + .append(remainder-- > 0 ? " " : ""); + + return padResult(result.toString().trim(), maxWidth); +} + +//当前单词的长度 +private int wordsLength(int left, int right, String[] words) { + int wordsLength = 0; + for (int i = left; i <= right; i++) wordsLength += words[i].length(); + return wordsLength; +} + +private String padResult(String result, int maxWidth) { + return result + blank(maxWidth - result.length()); +} + +private String blank(int length) { + return new String(new char[length]).replace('\0', ' '); +} +``` + +看了下,发现思想和自己也是一样的。但是这个速度却打败了 100% ,0 ms。考虑了下,差别应该在我的算法里使用了一个叫做 row 的 list 用来保存当前行的单词,用了很多 row.get ( index ),而上边的算法只记录了 left 和 right 下标,取单词直接用的 words 数组。然后尝试着在我之前的算法上改了一下,去掉 row,用两个变量 start 和 end 保存当前行的单词范围。主要是 ( end - start ) 代替了之前的 row.size ( ), words [ start + k ] 代替了之前的 row.get ( k )。 + +```java +public List fullJustify2(String[] words, int maxWidth) { + List ans = new ArrayList<>(); + int currentLen = 0; + int start = 0; + int end = 0; + for (int i = 0; i < words.length;) { + //判断加入该单词是否超过最长长度 + //分了两种情况,一种情况是加入第一个单词,不需要多加 1 + //已经有单词的话,再加入单词,需要多加个空格,所以多加了 1 + if (currentLen == 0 && currentLen + words[i].length() <= maxWidth + || currentLen > 0 && currentLen + 1 + words[i].length() <= maxWidth) { + end++; + if (currentLen == 0) { + currentLen = currentLen + words[i].length(); + } else { + currentLen = currentLen + 1 + words[i].length(); + } + i++; + } else { + int sub = maxWidth - currentLen + (end - start) - 1; + if (end - start == 1) { + String blank = getStringBlank(sub); + ans.add(words[start] + blank); + } else { + StringBuilder temp = new StringBuilder(); + temp.append(words[start]); + int averageBlank = sub / ((end - start) - 1); + //如果除不尽,计算剩余空格数 + int missing = sub - averageBlank * ((end - start) - 1); + String blank = getStringBlank(averageBlank + 1); + int k = 1; + for (int j = 0; j < missing; j++) { + temp.append(blank + words[start+k]); + k++; + } + blank = getStringBlank(averageBlank); + for (; k <(end - start); k++) { + temp.append(blank + words[start+k]); + } + ans.add(temp.toString()); + + } + start = end; + currentLen = 0; + + } + } + StringBuilder temp = new StringBuilder(); + temp.append(words[start]); + for (int i = 1; i < (end - start); i++) { + temp.append(" " + words[start+i]); + } + temp.append(getStringBlank(maxWidth - currentLen)); + ans.add(temp.toString()); + return ans; +} +//得到 n 个空白 +private String getStringBlank(int n) { + StringBuilder str = new StringBuilder(); + for (int i = 0; i < n; i++) { + str.append(" "); + } + return str.toString(); +} +``` + +果然,速度也到了打败 100%,0 ms。 + +# 总 + 充分说明 list 的读取还是没有数组的直接读取快呀,还有就是要向上边的作者学习,多封装几个函数,思路会更加清晰,代码也会简明。 \ No newline at end of file diff --git a/leetCode-69-Sqrtx.md b/leetCode-69-Sqrtx.md index aea8959b5..19b28d61a 100644 --- a/leetCode-69-Sqrtx.md +++ b/leetCode-69-Sqrtx.md @@ -1,211 +1,211 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/69.jpg) - -求一个数的平方根,不要求近似解,只需要整数部分。 - -# 解法一 二分法 - -本科的时候上计算方法的时候,讲过这个题的几个解法,二分法, 牛顿法,牛顿下山法,不同之处是之前是求近似解,类似误差是 0.0001 这样的。而这道题,只要求整数部分,所以先忘掉之前的解法,重新考虑一下。 - -求 n 的平方根的整数部分,所以平方根一定是 1,2,3 ... n 中的一个数。从一个有序序列中找一个数,像极了二分查找。先取中点 mid,然后判断 mid * mid 是否等于 n,小于 n 的话取左半部分,大于 n 的话取右半部分,等于 n 的话 mid 就是我们要找的了。 - -```java -public int mySqrt(int x) { - int L = 1, R = x; - while (L <= R) { - int mid = (L + R) / 2; - int square = mid * mid; - if (square == x) { - return mid; - } else if (square < x) { - L = mid + 1; - } else { - R = mid - 1; - } - } - return ?; -} -``` - -正常的 2 分法,如果最后没有找到就返回 -1。但这里肯定是不行的,那应该返回什么呢? - -对于平方数 4 9 16... 肯定会进入 square == x 然后结束掉。但是非平方数呢?例如 15。我们知道 15 的根,一定是 3 点几。因为 15 在 9 和 16 之间,9 的根是 3,16 的根是 4。所以对于 15,3 在这里就是我们要找的。 3 * 3 < 15,所以在上边算法中,最后的解是流向 square < x 的,所以我们用一个变量保存它,最后返回就可以了。 - -```java -public int mySqrt(int x) { - int L = 1, R = x; - int ans = 0; //保存最后的解 - while (L <= R) { - int mid = (L + R) / 2; - int square = mid * mid; - if (square == x) { - return mid; - } else if (square < x) { - ans = mid; //存起来以便返回 - L = mid + 1; - } else { - R = mid - 1; - } - } - return ans; -} -``` - -看起来很完美了,但如果 x = Integer.MAX_VALUE 的话,下边两句代码是会溢出的。 - -```java -int mid = (L + R) / 2; -int square = mid * mid; -``` - -当然,我们把变量用 long 存就解决了,这里有一个更优雅的解法。利用数学的变形。 - -```java -int mid = L + (R - L) / 2; -int div = x / mid; -``` - -当然相应的 if 语句也需要改变。 - -```java -else if (square < x) -mid * mid < x -mid < x / mid -mid < div -``` - -全部加进去就可以了。 - -```java -public int mySqrt(int x) { - int L = 1, R = x; - int ans = 0; - while (L <= R) { - int mid = L + (R - L) / 2; - int div = x / mid; - if (div == mid) { - return mid; - } else if (mid < div) { - ans = mid; - L = mid + 1; - } else { - R = mid - 1; - } - } - return ans; -} -``` - -时间复杂度:O(log ( x))。 - -空间复杂度:O(1)。 - -# 解法二 二分法求精确解 - -把求根转换为求函数的零点,求 n 的平方根,也就是求函数 f ( x ) = x² - n 的零点。这是一个二次曲线,与 x 轴有两个交点,我们要找的是那个正值。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/69_2.jpg) - -这里基于零点定理,去写算法。 - -> 如果函数 y = f ( x ) 在区间 [ a, b ] 上的图像是连续不断的一条曲线,并且有f ( a ) · f ( b ) < 0, 那么,函数y = f ( x ) 在区间 ( a , b ) 内有零点,即存在 c ∈ ( a , b ) , 使得 f ( c ) = 0 ,这个 c 也就是方程 f ( x ) = 0 的根。 - -简单的说,如果曲线上两点的值正负号相反,其间必定存在一个根。 - -这样我们就可以用二分法,找出中点,然后保留与中点的函数值符号相反的一段,丢弃另一段,然后继续找中点,直到符合我们的误差。 - -由于题目要求的是整数部分,所以我们需要想办法把我们的精确解转成整数。 - -四舍五入?由于我们求的是近似解,利用我们的算法我们求出的 8 的立方根会是 2.8125,直接四舍五入就是 3 了,很明显这里 8 的平方根应该是 2。 - -直接舍弃小数?由于我们是近似解,所有 9 的平方根可能会是 2.999, 舍弃小数变成 2 ,很明显也是不对的。 - -这里我想到一个解法。 - -我们先采取四舍五入变成 ans,然后判断 ans * ans 是否大于 n,如果大于 n 了,ans 减 1。如果小于等于,那么 ans 不变。这样的话,求 8 的平方根的例子就被我们解决了。 - -```java -int ans = (int) Math.round(mid); //先采取四舍五入 -if ((long) ans * ans > n) { - ans--; -} -// 可以不用 long -if (ans > n / ans) { - ans--; -} -``` - -合起来就可以了。 - -```java -//计算 x² - n -public double fx(double x, double n) { - return x * x - n; -} - -public int mySqrt(int n) { - double low = 0; - double high = n; - double mid = (low + high) / 2; - //函数值小于 0.1 的时候结束 - while (Math.abs(fx(mid, n)) > 0.1) { - //左端点的函数值 - double low_f = fx(low, n); - //中间节点的函数值 - double low_mid = fx(mid, n); - //判断哪一段的函数值是异号的 - if (low_f * low_mid < 0) { - high = mid; - } else { - low = mid; - } - mid = (low + high) / 2; - } - //先进行四舍五入 - int ans = (int) Math.round(mid); - if (ans != 0 && ans > n / ans) { - ans--; - } - return ans; -} -``` - -时间复杂度: - -空间复杂度:O(1)。 - -# 解法三 牛顿法 - -具体解释可以参考下[这篇文章](https://matongxue.com/madocs/205.html),或者搜一下, 有很多讲解的,代码的话根据下边的迭代式进行写。 - -$$x_{k+1}=x_k- f(x_k)/f^{'}(x_k)$$。 - -这里的话,$$f(x_n) = x^2-n$$ - -$$x_{k+1}=x_k-(x_k^2-n)/2x_k=(x_k^2+n)/2x_k = (x_k + n /x_k)/2$$。 - -```java -public int mySqrt(int n) { - double t = n; // 赋一个初值 - while (Math.abs(t * t - n) > 0.1) { - t = (n / t + t) / 2.0; - } - //先进行四舍五入 - int ans = (int) Math.round(t); - //判断是否超出 - if ((long) ans * ans > n) { - ans--; - } - return ans; -} - -``` - -时间复杂度: - -空间复杂度:O(1)。 - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/69.jpg) + +求一个数的平方根,不要求近似解,只需要整数部分。 + +# 解法一 二分法 + +本科的时候上计算方法的时候,讲过这个题的几个解法,二分法, 牛顿法,牛顿下山法,不同之处是之前是求近似解,类似误差是 0.0001 这样的。而这道题,只要求整数部分,所以先忘掉之前的解法,重新考虑一下。 + +求 n 的平方根的整数部分,所以平方根一定是 1,2,3 ... n 中的一个数。从一个有序序列中找一个数,像极了二分查找。先取中点 mid,然后判断 mid * mid 是否等于 n,小于 n 的话取左半部分,大于 n 的话取右半部分,等于 n 的话 mid 就是我们要找的了。 + +```java +public int mySqrt(int x) { + int L = 1, R = x; + while (L <= R) { + int mid = (L + R) / 2; + int square = mid * mid; + if (square == x) { + return mid; + } else if (square < x) { + L = mid + 1; + } else { + R = mid - 1; + } + } + return ?; +} +``` + +正常的 2 分法,如果最后没有找到就返回 -1。但这里肯定是不行的,那应该返回什么呢? + +对于平方数 4 9 16... 肯定会进入 square == x 然后结束掉。但是非平方数呢?例如 15。我们知道 15 的根,一定是 3 点几。因为 15 在 9 和 16 之间,9 的根是 3,16 的根是 4。所以对于 15,3 在这里就是我们要找的。 3 * 3 < 15,所以在上边算法中,最后的解是流向 square < x 的,所以我们用一个变量保存它,最后返回就可以了。 + +```java +public int mySqrt(int x) { + int L = 1, R = x; + int ans = 0; //保存最后的解 + while (L <= R) { + int mid = (L + R) / 2; + int square = mid * mid; + if (square == x) { + return mid; + } else if (square < x) { + ans = mid; //存起来以便返回 + L = mid + 1; + } else { + R = mid - 1; + } + } + return ans; +} +``` + +看起来很完美了,但如果 x = Integer.MAX_VALUE 的话,下边两句代码是会溢出的。 + +```java +int mid = (L + R) / 2; +int square = mid * mid; +``` + +当然,我们把变量用 long 存就解决了,这里有一个更优雅的解法。利用数学的变形。 + +```java +int mid = L + (R - L) / 2; +int div = x / mid; +``` + +当然相应的 if 语句也需要改变。 + +```java +else if (square < x) +mid * mid < x +mid < x / mid +mid < div +``` + +全部加进去就可以了。 + +```java +public int mySqrt(int x) { + int L = 1, R = x; + int ans = 0; + while (L <= R) { + int mid = L + (R - L) / 2; + int div = x / mid; + if (div == mid) { + return mid; + } else if (mid < div) { + ans = mid; + L = mid + 1; + } else { + R = mid - 1; + } + } + return ans; +} +``` + +时间复杂度:O(log ( x))。 + +空间复杂度:O(1)。 + +# 解法二 二分法求精确解 + +把求根转换为求函数的零点,求 n 的平方根,也就是求函数 f ( x ) = x² - n 的零点。这是一个二次曲线,与 x 轴有两个交点,我们要找的是那个正值。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/69_2.jpg) + +这里基于零点定理,去写算法。 + +> 如果函数 y = f ( x ) 在区间 [ a, b ] 上的图像是连续不断的一条曲线,并且有f ( a ) · f ( b ) < 0, 那么,函数y = f ( x ) 在区间 ( a , b ) 内有零点,即存在 c ∈ ( a , b ) , 使得 f ( c ) = 0 ,这个 c 也就是方程 f ( x ) = 0 的根。 + +简单的说,如果曲线上两点的值正负号相反,其间必定存在一个根。 + +这样我们就可以用二分法,找出中点,然后保留与中点的函数值符号相反的一段,丢弃另一段,然后继续找中点,直到符合我们的误差。 + +由于题目要求的是整数部分,所以我们需要想办法把我们的精确解转成整数。 + +四舍五入?由于我们求的是近似解,利用我们的算法我们求出的 8 的立方根会是 2.8125,直接四舍五入就是 3 了,很明显这里 8 的平方根应该是 2。 + +直接舍弃小数?由于我们是近似解,所有 9 的平方根可能会是 2.999, 舍弃小数变成 2 ,很明显也是不对的。 + +这里我想到一个解法。 + +我们先采取四舍五入变成 ans,然后判断 ans * ans 是否大于 n,如果大于 n 了,ans 减 1。如果小于等于,那么 ans 不变。这样的话,求 8 的平方根的例子就被我们解决了。 + +```java +int ans = (int) Math.round(mid); //先采取四舍五入 +if ((long) ans * ans > n) { + ans--; +} +// 可以不用 long +if (ans > n / ans) { + ans--; +} +``` + +合起来就可以了。 + +```java +//计算 x² - n +public double fx(double x, double n) { + return x * x - n; +} + +public int mySqrt(int n) { + double low = 0; + double high = n; + double mid = (low + high) / 2; + //函数值小于 0.1 的时候结束 + while (Math.abs(fx(mid, n)) > 0.1) { + //左端点的函数值 + double low_f = fx(low, n); + //中间节点的函数值 + double low_mid = fx(mid, n); + //判断哪一段的函数值是异号的 + if (low_f * low_mid < 0) { + high = mid; + } else { + low = mid; + } + mid = (low + high) / 2; + } + //先进行四舍五入 + int ans = (int) Math.round(mid); + if (ans != 0 && ans > n / ans) { + ans--; + } + return ans; +} +``` + +时间复杂度: + +空间复杂度:O(1)。 + +# 解法三 牛顿法 + +具体解释可以参考下[这篇文章](https://matongxue.com/madocs/205.html),或者搜一下, 有很多讲解的,代码的话根据下边的迭代式进行写。 + +$$x_{k+1}=x_k- f(x_k)/f^{'}(x_k)$$。 + +这里的话,$$f(x_n) = x^2-n$$ + +$$x_{k+1}=x_k-(x_k^2-n)/2x_k=(x_k^2+n)/2x_k = (x_k + n /x_k)/2$$。 + +```java +public int mySqrt(int n) { + double t = n; // 赋一个初值 + while (Math.abs(t * t - n) > 0.1) { + t = (n / t + t) / 2.0; + } + //先进行四舍五入 + int ans = (int) Math.round(t); + //判断是否超出 + if ((long) ans * ans > n) { + ans--; + } + return ans; +} + +``` + +时间复杂度: + +空间复杂度:O(1)。 + +# 总 + 首先用了正常的二分法,求出整数解。然后用常规的二分法、牛顿法求近似根,然后利用一个技巧转换为整数解。 \ No newline at end of file diff --git a/leetCode-7-Reverse-Integer.md b/leetCode-7-Reverse-Integer.md index 336ec7569..ad4d3e994 100644 --- a/leetCode-7-Reverse-Integer.md +++ b/leetCode-7-Reverse-Integer.md @@ -1,83 +1,83 @@ -## 题目描述(简单难度) - -![](http://windliang.oss-cn-beijing.aliyuncs.com/7_rev.jpg) - -很简单,就是输入整数,输出它的倒置。 - -第一反应就是, 取余得到个位数,然后除以 10 去掉个位数,然后用一个变量保存倒置的数。 - -```java -public int reverse(int x) { - int rev = 0; - while (x != 0) { - int pop = x % 10; - x /= 10; - rev = rev * 10 + pop; - } - return rev; -} -``` - -然后似乎不是那么理想。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/7_1.jpg) - -为什么呢?倒置过来不应该是 9646324351 吗。其实题目里讲了,int 的范围是 $$[-2^{31} ,2^{31}-1]$$ 也就是 $$[-2147483648,2147483647] $$ 。明显 9646324351 超出了范围,造成了溢出。所以我们需要在输出前,判断是否溢出。 - -问题的关键就是下边的一句了。 - - rev = rev * 10 + pop; - -为了区分两个 rev ,更好的说明,我们引入 temp 。 - -temp = rev * 10 + pop; - -rev = temp; - -我们对 temp = rev * 10 + pop; 进行讨论。intMAX = 2147483647 , intMin = - 2147483648 。 - -对于大于 intMax 的讨论,此时 x 一定是正数,pop 也是正数。 - -* 如果 rev > intMax / 10 ,那么没的说,此时肯定溢出了。 -* 如果 rev == intMax / 10 = 2147483647 / 10 = 214748364 ,此时 rev * 10 就是 2147483640 如果 pop 大于 7 ,那么就一定溢出了。但是!如果假设 pop 等于 8,那么意味着原数 x 是 8463847412 了,输入的是 int ,而此时是溢出的状态,所以不可能输入,所以意味着 pop 不可能大于 7 ,也就意味着 rev == intMax / 10 时不会造成溢出。 -* 如果 rev < intMax / 10 ,意味着 rev 最大是 214748363 , rev * 10 就是 2147483630 , 此时再加上 pop ,一定不会溢出。 - -对于小于 intMin 的讨论同理。 - -```java -public int reverse(int x) { - int rev = 0; - while (x != 0) { - int pop = x % 10; - x /= 10; - if (rev > Integer.MAX_VALUE/10 ) return 0; - if (rev < Integer.MIN_VALUE/10 ) return 0; - rev = rev * 10 + pop; - } - return rev; -} -``` - -时间复杂度:循环多少次呢?数字有多少位,就循环多少次,也就是 $$log_{10}(x) + 1$$ 次,所以时间复杂度是 O(log(x))。 - -空间复杂度:O(1)。 - -当然我们可以不用思考那么多,用一种偷懒的方式 AC ,我们直接把 rev 定义成 long ,然后输出前判断 rev 是不是在范围内,不在的话直接输出 0 。 - -```java -public int reverse(int x) { - long rev = 0; - while (x != 0) { - int pop = x % 10; - x /= 10; - rev = rev * 10 + pop; - } - if (rev > Integer.MAX_VALUE || rev < Integer.MIN_VALUE ) return 0; - return (int)rev; -} -``` - -## 总结 - -比较简单的一道题,主要是在考判断是不是溢出,又是轻松的一天! - +## 题目描述(简单难度) + +![](http://windliang.oss-cn-beijing.aliyuncs.com/7_rev.jpg) + +很简单,就是输入整数,输出它的倒置。 + +第一反应就是, 取余得到个位数,然后除以 10 去掉个位数,然后用一个变量保存倒置的数。 + +```java +public int reverse(int x) { + int rev = 0; + while (x != 0) { + int pop = x % 10; + x /= 10; + rev = rev * 10 + pop; + } + return rev; +} +``` + +然后似乎不是那么理想。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/7_1.jpg) + +为什么呢?倒置过来不应该是 9646324351 吗。其实题目里讲了,int 的范围是 $$[-2^{31} ,2^{31}-1]$$ 也就是 $$[-2147483648,2147483647] $$ 。明显 9646324351 超出了范围,造成了溢出。所以我们需要在输出前,判断是否溢出。 + +问题的关键就是下边的一句了。 + + rev = rev * 10 + pop; + +为了区分两个 rev ,更好的说明,我们引入 temp 。 + +temp = rev * 10 + pop; + +rev = temp; + +我们对 temp = rev * 10 + pop; 进行讨论。intMAX = 2147483647 , intMin = - 2147483648 。 + +对于大于 intMax 的讨论,此时 x 一定是正数,pop 也是正数。 + +* 如果 rev > intMax / 10 ,那么没的说,此时肯定溢出了。 +* 如果 rev == intMax / 10 = 2147483647 / 10 = 214748364 ,此时 rev * 10 就是 2147483640 如果 pop 大于 7 ,那么就一定溢出了。但是!如果假设 pop 等于 8,那么意味着原数 x 是 8463847412 了,输入的是 int ,而此时是溢出的状态,所以不可能输入,所以意味着 pop 不可能大于 7 ,也就意味着 rev == intMax / 10 时不会造成溢出。 +* 如果 rev < intMax / 10 ,意味着 rev 最大是 214748363 , rev * 10 就是 2147483630 , 此时再加上 pop ,一定不会溢出。 + +对于小于 intMin 的讨论同理。 + +```java +public int reverse(int x) { + int rev = 0; + while (x != 0) { + int pop = x % 10; + x /= 10; + if (rev > Integer.MAX_VALUE/10 ) return 0; + if (rev < Integer.MIN_VALUE/10 ) return 0; + rev = rev * 10 + pop; + } + return rev; +} +``` + +时间复杂度:循环多少次呢?数字有多少位,就循环多少次,也就是 $$log_{10}(x) + 1$$ 次,所以时间复杂度是 O(log(x))。 + +空间复杂度:O(1)。 + +当然我们可以不用思考那么多,用一种偷懒的方式 AC ,我们直接把 rev 定义成 long ,然后输出前判断 rev 是不是在范围内,不在的话直接输出 0 。 + +```java +public int reverse(int x) { + long rev = 0; + while (x != 0) { + int pop = x % 10; + x /= 10; + rev = rev * 10 + pop; + } + if (rev > Integer.MAX_VALUE || rev < Integer.MIN_VALUE ) return 0; + return (int)rev; +} +``` + +## 总结 + +比较简单的一道题,主要是在考判断是不是溢出,又是轻松的一天! + diff --git a/leetCode-70-Climbing-Stairs.md b/leetCode-70-Climbing-Stairs.md index 6aae26f16..d83eb31ea 100644 --- a/leetCode-70-Climbing-Stairs.md +++ b/leetCode-70-Climbing-Stairs.md @@ -1,206 +1,206 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/70.jpg) - -爬楼梯,每次走 1 个或 2 个台阶,n 层的台阶,总共有多少种走法。 - -# 解法一 暴力解法 - -用递归的思路想一下,要求 n 层的台阶的走法,由于一次走 1 或 2 个台阶,所以上到第 n 个台阶之前,一定是停留在第 n - 1 个台阶上,或者 n - 2 个台阶上。所以如果用 f ( n ) 代表 n 个台阶的走法。那么, - -f ( n ) = f ( n - 1) + f ( n - 2 )。 - -f ( 1 ) = 1,f ( 2 ) = 2 。 - -发现个神奇的事情,这就是斐波那契数列(Fibonacci sequence)。 - -直接暴力一点,利用递归写出来。 - -```java -public int climbStairs(int n) { - return climbStairsN(n); -} - -private int climbStairsN(int n) { - if (n == 1) { - return 1; - } - if (n == 2) { - return 2; - } - return climbStairsN(n - 1) + climbStairsN(n - 2); -} - -``` - -时间复杂度:是一个树状图,$$O(2^n)$$。 - -空间复杂度: - -# 解法二 暴力解法优化 - -解法一很慢,leetcode 上报了超时,原因就是先求 climbStairsN ( n - 1 ),然后求 climbStairsN ( n - 2 ) 的时候,其实很多解已经有了,但是它依旧进入了递归。优化方法就是把求出的解都存起来,后边求的时候直接使用,不用再进入递归了。叫做 memoization 技术。 - -```java -public int climbStairs(int n) { - return climbStairsN(n, new HashMap()); -} - -private int climbStairsN(int n, HashMap hashMap) { - if (n == 1) { - return 1; - } - if (n == 2) { - return 2; - } - int n1 = 0; - if (!hashMap.containsKey(n - 1)) { - n1 = climbStairsN(n - 1, hashMap); - hashMap.put(n - 1, n1); - } else { - n1 = hashMap.get(n - 1); - } - int n2 = 0; - if (!hashMap.containsKey(n - 2)) { - n2 = climbStairsN(n - 2, hashMap); - hashMap.put(n - 2, n1); - } else { - n2 = hashMap.get(n - 2); - } - return n1 + n2; -} -``` - -时间复杂度: - -空间复杂度: - -当然由于 key 都是整数,我们完全可以用一个数组去存储,不需要 Hash。 - -```java -public int climbStairs(int n) { - int memo[] = new int[n + 1]; - return climbStairsN(n, memo); -} -private int climbStairsN(int n, int[] memo) { - if (n == 1) { - return 1; - } - if (n == 2) { - return 2; - } - int n1 = 0; - //数组的默认值是 0 - if (memo[n - 1] == 0) { - n1 = climbStairsN(n - 1, memo); - memo[n - 1] = n1; - } else { - n1 = memo[n - 1]; - } - int n2 = 0; - if (memo[n - 2] == 0) { - n2 = climbStairsN(n - 2, memo); - memo[n - 2] = n2; - - } else { - n2 = memo[n - 2]; - } - return n1 + n2; -} -``` - -# 解法三 迭代 - -当然递归可以解决,我们可以直接迭代,省去递归压栈的过程。初始值 f ( 1 ) 和 f ( 2 ),然后可以求出 f ( 3 ),然后求出 f ( 4 ) ... 直到 f ( n ),一个循环就够了。其实就是动态规划的思想了。 - -```java -public int climbStairs(int n) { - int n1 = 1; - int n2 = 2; - if (n == 1) { - return n1; - } - if (n == 2) { - return n2; - } - //n1、n2 都后移一个位置 - for (int i = 3; i <= n; i++) { - int temp = n2; - n2 = n1 + n2; - n1 = temp; - } - return n2; -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(1)。 - -以上都是比较常规的方法,下边分享一下 [Solution](https://leetcode.com/problems/climbing-stairs/solution/) 里给出的其他解法。 - -# 解法四 矩阵相乘 - -[Solution5](https://leetcode.com/problems/climbing-stairs/solution/)叫做 Binets Method,它利用数学归纳法证明了一下,这里就直接用了,至于怎么想出来的,我也不清楚了。 - -定义一个矩阵 $$Q = \begin{matrix} 1 & 1 \\ 1 & 0 \end{matrix} $$ ,然后求 f ( n ) 话,我们先让 Q 矩阵求幂,然后取第一行第一列的元素就可以了,也就是 $$f(n)=Q^n[0][0]$$。 - -至于怎么更快的求幂,可以看 [50 题]()的解法三。 - -```java -public int climbStairs(int n) { - int[][] Q = {{1, 1}, {1, 0}}; - int[][] res = pow(Q, n); - return res[0][0]; -} -public int[][] pow(int[][] a, int n) { - int[][] ret = {{1, 0}, {0, 1}}; - while (n > 0) { - //最后一位是 1,加到累乘结果里 - if ((n & 1) == 1) { - ret = multiply(ret, a); - } - //n 右移一位 - n >>= 1; - //更新 a - a = multiply(a, a); - } - return ret; -} -public int[][] multiply(int[][] a, int[][] b) { - int[][] c = new int[2][2]; - for (int i = 0; i < 2; i++) { - for (int j = 0; j < 2; j++) { - c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j]; - } - } - return c; -} -``` - -时间复杂度:O(log (n))。 - -空间复杂度:O(1)。 - -# 解法五 公式法 - -直接套用公式 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/70_2.jpg) - -```java -public int climbStairs(int n) { - double sqrt5=Math.sqrt(5); - double fibn=Math.pow((1+sqrt5)/2,n+1)-Math.pow((1-sqrt5)/2,n+1); - return (int)(fibn/sqrt5); -} -``` - -时间复杂度:耗在了求幂的时候,O(log(n))。 - -空间复杂度:O(1)。 - -# 总 - -这道题把递归,动态规划的思想都用到了,很经典。此外,矩阵相乘的解法是真的强,直接将时间复杂度优化到 log 层面。 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/70.jpg) + +爬楼梯,每次走 1 个或 2 个台阶,n 层的台阶,总共有多少种走法。 + +# 解法一 暴力解法 + +用递归的思路想一下,要求 n 层的台阶的走法,由于一次走 1 或 2 个台阶,所以上到第 n 个台阶之前,一定是停留在第 n - 1 个台阶上,或者 n - 2 个台阶上。所以如果用 f ( n ) 代表 n 个台阶的走法。那么, + +f ( n ) = f ( n - 1) + f ( n - 2 )。 + +f ( 1 ) = 1,f ( 2 ) = 2 。 + +发现个神奇的事情,这就是斐波那契数列(Fibonacci sequence)。 + +直接暴力一点,利用递归写出来。 + +```java +public int climbStairs(int n) { + return climbStairsN(n); +} + +private int climbStairsN(int n) { + if (n == 1) { + return 1; + } + if (n == 2) { + return 2; + } + return climbStairsN(n - 1) + climbStairsN(n - 2); +} + +``` + +时间复杂度:是一个树状图,$$O(2^n)$$。 + +空间复杂度: + +# 解法二 暴力解法优化 + +解法一很慢,leetcode 上报了超时,原因就是先求 climbStairsN ( n - 1 ),然后求 climbStairsN ( n - 2 ) 的时候,其实很多解已经有了,但是它依旧进入了递归。优化方法就是把求出的解都存起来,后边求的时候直接使用,不用再进入递归了。叫做 memoization 技术。 + +```java +public int climbStairs(int n) { + return climbStairsN(n, new HashMap()); +} + +private int climbStairsN(int n, HashMap hashMap) { + if (n == 1) { + return 1; + } + if (n == 2) { + return 2; + } + int n1 = 0; + if (!hashMap.containsKey(n - 1)) { + n1 = climbStairsN(n - 1, hashMap); + hashMap.put(n - 1, n1); + } else { + n1 = hashMap.get(n - 1); + } + int n2 = 0; + if (!hashMap.containsKey(n - 2)) { + n2 = climbStairsN(n - 2, hashMap); + hashMap.put(n - 2, n1); + } else { + n2 = hashMap.get(n - 2); + } + return n1 + n2; +} +``` + +时间复杂度: + +空间复杂度: + +当然由于 key 都是整数,我们完全可以用一个数组去存储,不需要 Hash。 + +```java +public int climbStairs(int n) { + int memo[] = new int[n + 1]; + return climbStairsN(n, memo); +} +private int climbStairsN(int n, int[] memo) { + if (n == 1) { + return 1; + } + if (n == 2) { + return 2; + } + int n1 = 0; + //数组的默认值是 0 + if (memo[n - 1] == 0) { + n1 = climbStairsN(n - 1, memo); + memo[n - 1] = n1; + } else { + n1 = memo[n - 1]; + } + int n2 = 0; + if (memo[n - 2] == 0) { + n2 = climbStairsN(n - 2, memo); + memo[n - 2] = n2; + + } else { + n2 = memo[n - 2]; + } + return n1 + n2; +} +``` + +# 解法三 迭代 + +当然递归可以解决,我们可以直接迭代,省去递归压栈的过程。初始值 f ( 1 ) 和 f ( 2 ),然后可以求出 f ( 3 ),然后求出 f ( 4 ) ... 直到 f ( n ),一个循环就够了。其实就是动态规划的思想了。 + +```java +public int climbStairs(int n) { + int n1 = 1; + int n2 = 2; + if (n == 1) { + return n1; + } + if (n == 2) { + return n2; + } + //n1、n2 都后移一个位置 + for (int i = 3; i <= n; i++) { + int temp = n2; + n2 = n1 + n2; + n1 = temp; + } + return n2; +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(1)。 + +以上都是比较常规的方法,下边分享一下 [Solution](https://leetcode.com/problems/climbing-stairs/solution/) 里给出的其他解法。 + +# 解法四 矩阵相乘 + +[Solution5](https://leetcode.com/problems/climbing-stairs/solution/)叫做 Binets Method,它利用数学归纳法证明了一下,这里就直接用了,至于怎么想出来的,我也不清楚了。 + +定义一个矩阵 $$Q = \begin{matrix} 1 & 1 \\ 1 & 0 \end{matrix} $$ ,然后求 f ( n ) 话,我们先让 Q 矩阵求幂,然后取第一行第一列的元素就可以了,也就是 $$f(n)=Q^n[0][0]$$。 + +至于怎么更快的求幂,可以看 [50 题]()的解法三。 + +```java +public int climbStairs(int n) { + int[][] Q = {{1, 1}, {1, 0}}; + int[][] res = pow(Q, n); + return res[0][0]; +} +public int[][] pow(int[][] a, int n) { + int[][] ret = {{1, 0}, {0, 1}}; + while (n > 0) { + //最后一位是 1,加到累乘结果里 + if ((n & 1) == 1) { + ret = multiply(ret, a); + } + //n 右移一位 + n >>= 1; + //更新 a + a = multiply(a, a); + } + return ret; +} +public int[][] multiply(int[][] a, int[][] b) { + int[][] c = new int[2][2]; + for (int i = 0; i < 2; i++) { + for (int j = 0; j < 2; j++) { + c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j]; + } + } + return c; +} +``` + +时间复杂度:O(log (n))。 + +空间复杂度:O(1)。 + +# 解法五 公式法 + +直接套用公式 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/70_2.jpg) + +```java +public int climbStairs(int n) { + double sqrt5=Math.sqrt(5); + double fibn=Math.pow((1+sqrt5)/2,n+1)-Math.pow((1-sqrt5)/2,n+1); + return (int)(fibn/sqrt5); +} +``` + +时间复杂度:耗在了求幂的时候,O(log(n))。 + +空间复杂度:O(1)。 + +# 总 + +这道题把递归,动态规划的思想都用到了,很经典。此外,矩阵相乘的解法是真的强,直接将时间复杂度优化到 log 层面。 + diff --git a/leetCode-71-Simplify-Path.md b/leetCode-71-Simplify-Path.md index 6e5cd69e4..7119efad1 100644 --- a/leetCode-71-Simplify-Path.md +++ b/leetCode-71-Simplify-Path.md @@ -1,52 +1,52 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/71.jpg) - -生成一个绝对路径,把相对路径中 ".." 和 "." 都转换为实际的路径,此外,"///" 多余的 "/" 要去掉,开头要有一个 "/",末尾不要 "/"。 - -# 解法一 - -这道题,只要理解了题意,然后理一下就出来了。下面代码就不考虑空间复杂度了,多创建几个数组,代码会简洁一些。 - -```java -public String simplifyPath(String path) { - //先利用 "/" 将字符串分割成一个一个单词 - String[] wordArr = path.split("/"); - //将空字符串(由类似这种"/a//c"的字符串产生)和 "." ("."代表当前目录不影响路径)去掉,保存到 wordList - ArrayList wordList = new ArrayList(); - for (int i = 0; i < wordArr.length; i++) { - if (wordArr[i].isEmpty() || wordArr[i].equals(".")) { - continue; - } - wordList.add(wordArr[i]); - } - //wordListSim 保存简化后的路径 - ArrayList wordListSim = new ArrayList(); - //遍历 wordList - for (int i = 0; i < wordList.size(); i++) { - //如果遇到 "..",wordListSim 就删除末尾的单词 - if (wordList.get(i).equals("..")) { - if (!wordListSim.isEmpty()) { - wordListSim.remove(wordListSim.size() - 1); - } - //否则的话就加入 wordListSim - } else { - wordListSim.add(wordList.get(i)); - } - } - //将单词用 "/" 连接 - String ans = String.join("/", wordListSim); - //开头补上 "/" - ans = "/" + ans; - return ans; - -} -``` - -时间复杂度: - -空间复杂度: - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/71.jpg) + +生成一个绝对路径,把相对路径中 ".." 和 "." 都转换为实际的路径,此外,"///" 多余的 "/" 要去掉,开头要有一个 "/",末尾不要 "/"。 + +# 解法一 + +这道题,只要理解了题意,然后理一下就出来了。下面代码就不考虑空间复杂度了,多创建几个数组,代码会简洁一些。 + +```java +public String simplifyPath(String path) { + //先利用 "/" 将字符串分割成一个一个单词 + String[] wordArr = path.split("/"); + //将空字符串(由类似这种"/a//c"的字符串产生)和 "." ("."代表当前目录不影响路径)去掉,保存到 wordList + ArrayList wordList = new ArrayList(); + for (int i = 0; i < wordArr.length; i++) { + if (wordArr[i].isEmpty() || wordArr[i].equals(".")) { + continue; + } + wordList.add(wordArr[i]); + } + //wordListSim 保存简化后的路径 + ArrayList wordListSim = new ArrayList(); + //遍历 wordList + for (int i = 0; i < wordList.size(); i++) { + //如果遇到 "..",wordListSim 就删除末尾的单词 + if (wordList.get(i).equals("..")) { + if (!wordListSim.isEmpty()) { + wordListSim.remove(wordListSim.size() - 1); + } + //否则的话就加入 wordListSim + } else { + wordListSim.add(wordList.get(i)); + } + } + //将单词用 "/" 连接 + String ans = String.join("/", wordListSim); + //开头补上 "/" + ans = "/" + ans; + return ans; + +} +``` + +时间复杂度: + +空间复杂度: + +# 总 + 这道题就是理清思路就可以,没有用到什么技巧。 \ No newline at end of file diff --git a/leetCode-72-Edit-Distance.md b/leetCode-72-Edit-Distance.md index 0ca0e0e55..6bafb4fd0 100644 --- a/leetCode-72-Edit-Distance.md +++ b/leetCode-72-Edit-Distance.md @@ -1,202 +1,202 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/72.jpg) - -由一个字符串变为另一个字符串的最少操作次数,可以删除一个字符,替换一个字符,插入一个字符,也叫做最小编辑距离。 - -# 解法一 递归 - -我们可以发现删除一个字符和插入一个字符是等效的,对于变换次数并没有影响。例如 "a" 和 "ab" ,既可以 "a" 加上一个字符 "b" 变成 "ab",也可以是 "ab" 去掉一个字符 "b" 变成 "a"。所以下边的算法可以只考虑删除和替换。 - -首先,以递归的思想去考虑问题,思考如何将大问题化解为小问题。例如 horse 变为 ros,其实我们有三种可选方案。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/72_2.jpg) - -第一种,先把 horse 变为 ro ,求出它的最短编辑距离,假如是 x,然后 hosre 变成 ros 的编辑距离就可以是 x + 1。因为 horse 已经变成了 ro,然后我们可以把 ros 的 s 去掉,两个字符串就一样了,也就是再进行一次删除操作,所以加 1。 - -第二种,先把 hors 变为 ros,求出它的最短编辑距离,假如是 y,然后 hosre 变成 ros 的编辑距离就可以是 y + 1。因为 hors 变成了 ros,然后我们可以把 horse 的 e 去掉,两个字符串就一样了,也就是再进行一次删除操作,所以加 1。 - -第三种,先把 hors 变为 ro,求出它的最短编辑距离,假如是 z,然后我们再把 e 换成 s,两个字符串就一样了,hosre 变成 ros 的编辑距离就可以是 z + 1。当然,如果是其它的例子,最后一个字符是一样的,比如是 hosrs 和 ros ,此时我们直接取 z 作为编辑距离就可以了。 - -最后,我们从上边所有可选的编辑距离中,选一个最小的就可以了。 - -上边的第一种情况,假设了 horse 变为 ro 的最短编辑距离是 x,但其实我们并不知道 x 是多少,这个怎么求呢?类似的思路,也分为三种情况,然后选最小的就可以了!当然,上边的第二种,第三种情况也是类似的。然后一直递归下去。 - -最后,字符串长度不断地减少,直到出现了空串,这也是我们的递归出口了,如果是一个空串,一个不是空串,假如它的长度是 l,那么这两个字符串的最小编辑距离就是 l。如果是两个空串,那么最小编辑距离当然就是 0 了。 - -上边的分析,很容易就写出递归的写法了。 - -```java -public int minDistance(String word1, String word2) { - if (word1.length() == 0 && word2.length() == 0) { - return 0; - } - if (word1.length() == 0) { - return word2.length(); - } - if (word2.length() == 0) { - return word1.length(); - } - int x = minDistance(word1, word2.substring(0, word2.length() - 1)) + 1; - int y = minDistance(word1.substring(0, word1.length() - 1), word2) + 1; - int z = minDistance(word1.substring(0, word1.length() - 1), word2.substring(0, word2.length() - 1)); - if(word1.charAt(word1.length()-1)!=word2.charAt(word2.length()-1)){ - z++; - } - return Math.min(Math.min(x, y), z); -} -``` - -# 解法二 动态规划 - -上边的算法缺点很明显,先进行了压栈,浪费了很多时间,其次很多字符串的最小编辑距离都进行了重复计算。对于这种,很容易想到动态规划的思想去优化。 - -假设两个字符串是 word1 和 word2。 - -ans\[i\]\[j\] 来表示字符串 word1[ 0, i ) (word1 的第 0 到 第 i - 1个字符)和 word2[ 0, j - 1) 的最小编辑距离。然后状态转移方程就出来了。 - -if ( word1[m] == word2[n] ) - -​ ans\[m\]\[n\] = Math.min ( ans[m]\[n-1\] + 1, ans[m-1]\[n\] + 1, ans[m-1]\[n-1\]) - -if ( word1[m] != word2[n] ) - -​ ans\[m\]\[n\] = Math.min ( ans[m]\[n-1\] + 1, ans[m-1]\[n\] + 1, ans[m-1]\[n-1\] + 1) - -然后两层 for 循环,直接一层一层的更新数组就够了。 - -```java -public int minDistance(String word1, String word2) { - if (word1.length() == 0 && word2.length() == 0) { - return 0; - } - if (word1.length() == 0) { - return word2.length(); - } - if (word2.length() == 0) { - return word1.length(); - } - int[][] ans = new int[word1.length() + 1][word2.length() + 1]; - - //把有空串的情况更新了 - for (int i = 0; i <= word1.length(); i++) { - ans[i][0] = i; - } - for (int i = 0; i <= word2.length(); i++) { - ans[0][i] = i; - } - int n1 = word1.length(); - int n2 = word2.length(); - //从 1 开始遍历,从 0 开始的话,按照下边的算法取了 i - 1 会越界 - for (int i = 1; i <= n1; i++) { - for (int j = 1; j <= n2; j++) { - int min_delete = Math.min(ans[i - 1][j], ans[i][j - 1]) + 1; - int replace = ans[i - 1][j - 1]; - if (word1.charAt(i - 1) != word2.charAt(j - 1)) { - replace++; - } - ans[i][j] = Math.min(min_delete, replace); - } - } - return ans[n1][n2]; -} -``` - -时间复杂度:O(mn)。 - -空间复杂度:O(mn)。 - -如果你是顺序刷题的话,做到这里,一定会想到空间复杂度的优化,例如[5题](),[10题](),[53题]()等等。主要想法是,看上边的算法,我们再求 ans[i]\[\*\] 的时候,我们只用到 ans[i - 1]\[\*\] 的情况,所以我们完全只用两个数组就够了。 - -```java -public int minDistance(String word1, String word2) { - if (word1.length() == 0 && word2.length() == 0) { - return 0; - } - if (word1.length() == 0) { - return word2.length(); - } - if (word2.length() == 0) { - return word1.length(); - } - int[][] ans = new int[2][word2.length() + 1]; - - for (int i = 0; i <= word2.length(); i++) { - ans[0][i] = i; - } - int n1 = word1.length(); - int n2 = word2.length(); - for (int i = 1; i <= n1; i++) { - //由于只用了两个数组,所以不能向以前一样一次性初始化空串,在这里提前更新 j = 0 的情况 - ans[i % 2][0] = ans[(i - 1) % 2][0] + 1; - for (int j = 1; j <= n2; j++) { - int min_delete = Math.min(ans[(i - 1) % 2][j], ans[i % 2][j - 1]) + 1; - int replace = ans[(i - 1) % 2][j - 1]; - if (word1.charAt(i - 1) != word2.charAt(j - 1)) { - replace++; - } - ans[i % 2][j] = Math.min(min_delete, replace); - } - } - return ans[n1 % 2][n2]; -} -``` - -时间复杂度:O(mn)。 - -空间复杂度:O(n)。 - -再直接点,其实连两个数组我们都不需要,只需要一个数组。改写这个可能有些不好理解,可以结合一下图示。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/72_3.jpg) - -在更新二维数组的时候,我们都是一列一列的更新。在更新 ? 位置的时候,我们需要橙色位置的信息,也就是当前列的上一个位置,和上一列的当前位置,和上一列的上一个位置。如果我们用一个数组,当前列的上一个位置已经把上一列的上一个位置的数据覆盖掉了,所以我们要用一个变量提前保存上一列的上一个位置以便使用。 - -```java -public int minDistance(String word1, String word2) { - if (word1.length() == 0 && word2.length() == 0) { - return 0; - } - if (word1.length() == 0) { - return word2.length(); - } - if (word2.length() == 0) { - return word1.length(); - } - int[] ans = new int[word2.length() + 1]; - - for (int i = 0; i <= word2.length(); i++) { - ans[i] = i; - } - int n1 = word1.length(); - int n2 = word2.length(); - for (int i = 1; i <= n1; i++) { - int temp = ans[0]; - ans[0] = ans[0] + 1; - for (int j = 1; j <= n2; j++) { - int min_delete = Math.min(ans[j], ans[j - 1]) + 1; - //上一列的上一个位置,直接用 temp - int replace = temp; - if (word1.charAt(i - 1) != word2.charAt(j - 1)) { - replace++; - } - //保存当前列的信息 - temp = ans[j]; - //再进行更新 - ans[j] = Math.min(min_delete, replace); - } - } - return ans[n2]; -} -``` - -时间复杂度:O(mn)。 - -空间复杂度:O(n)。 - -# 总 - -动态规划的一系列操作,先递归,利用动态规划省略压栈的过程,然后空间复杂度的优化,很经典了。此外,对于动态规划数组的含义的定义也是很重要,开始的时候自己将 ans\[i\]\[j\] 表示为 字符串 word1[ 0, i ](word1 的第 0 到 第 i 个字符)和 word2[ 0, j - 1] 的最小编辑距离。和上边解法的区别只是包含了末尾的字符。这造成了初始化 ans\[0\]\[\*\] 和 ans\[\*\]\[0\] 的时候,会比较复杂,看到了[这里]()的解法,才有一种柳暗花明的感觉,思路是一样的,但更新ans\[0\]\[\*\] 和 ans\[\*\]\[0\] 却简单了很多。 - - - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/72.jpg) + +由一个字符串变为另一个字符串的最少操作次数,可以删除一个字符,替换一个字符,插入一个字符,也叫做最小编辑距离。 + +# 解法一 递归 + +我们可以发现删除一个字符和插入一个字符是等效的,对于变换次数并没有影响。例如 "a" 和 "ab" ,既可以 "a" 加上一个字符 "b" 变成 "ab",也可以是 "ab" 去掉一个字符 "b" 变成 "a"。所以下边的算法可以只考虑删除和替换。 + +首先,以递归的思想去考虑问题,思考如何将大问题化解为小问题。例如 horse 变为 ros,其实我们有三种可选方案。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/72_2.jpg) + +第一种,先把 horse 变为 ro ,求出它的最短编辑距离,假如是 x,然后 hosre 变成 ros 的编辑距离就可以是 x + 1。因为 horse 已经变成了 ro,然后我们可以把 ros 的 s 去掉,两个字符串就一样了,也就是再进行一次删除操作,所以加 1。 + +第二种,先把 hors 变为 ros,求出它的最短编辑距离,假如是 y,然后 hosre 变成 ros 的编辑距离就可以是 y + 1。因为 hors 变成了 ros,然后我们可以把 horse 的 e 去掉,两个字符串就一样了,也就是再进行一次删除操作,所以加 1。 + +第三种,先把 hors 变为 ro,求出它的最短编辑距离,假如是 z,然后我们再把 e 换成 s,两个字符串就一样了,hosre 变成 ros 的编辑距离就可以是 z + 1。当然,如果是其它的例子,最后一个字符是一样的,比如是 hosrs 和 ros ,此时我们直接取 z 作为编辑距离就可以了。 + +最后,我们从上边所有可选的编辑距离中,选一个最小的就可以了。 + +上边的第一种情况,假设了 horse 变为 ro 的最短编辑距离是 x,但其实我们并不知道 x 是多少,这个怎么求呢?类似的思路,也分为三种情况,然后选最小的就可以了!当然,上边的第二种,第三种情况也是类似的。然后一直递归下去。 + +最后,字符串长度不断地减少,直到出现了空串,这也是我们的递归出口了,如果是一个空串,一个不是空串,假如它的长度是 l,那么这两个字符串的最小编辑距离就是 l。如果是两个空串,那么最小编辑距离当然就是 0 了。 + +上边的分析,很容易就写出递归的写法了。 + +```java +public int minDistance(String word1, String word2) { + if (word1.length() == 0 && word2.length() == 0) { + return 0; + } + if (word1.length() == 0) { + return word2.length(); + } + if (word2.length() == 0) { + return word1.length(); + } + int x = minDistance(word1, word2.substring(0, word2.length() - 1)) + 1; + int y = minDistance(word1.substring(0, word1.length() - 1), word2) + 1; + int z = minDistance(word1.substring(0, word1.length() - 1), word2.substring(0, word2.length() - 1)); + if(word1.charAt(word1.length()-1)!=word2.charAt(word2.length()-1)){ + z++; + } + return Math.min(Math.min(x, y), z); +} +``` + +# 解法二 动态规划 + +上边的算法缺点很明显,先进行了压栈,浪费了很多时间,其次很多字符串的最小编辑距离都进行了重复计算。对于这种,很容易想到动态规划的思想去优化。 + +假设两个字符串是 word1 和 word2。 + +ans\[i\]\[j\] 来表示字符串 word1[ 0, i ) (word1 的第 0 到 第 i - 1个字符)和 word2[ 0, j - 1) 的最小编辑距离。然后状态转移方程就出来了。 + +if ( word1[m] == word2[n] ) + +​ ans\[m\]\[n\] = Math.min ( ans[m]\[n-1\] + 1, ans[m-1]\[n\] + 1, ans[m-1]\[n-1\]) + +if ( word1[m] != word2[n] ) + +​ ans\[m\]\[n\] = Math.min ( ans[m]\[n-1\] + 1, ans[m-1]\[n\] + 1, ans[m-1]\[n-1\] + 1) + +然后两层 for 循环,直接一层一层的更新数组就够了。 + +```java +public int minDistance(String word1, String word2) { + if (word1.length() == 0 && word2.length() == 0) { + return 0; + } + if (word1.length() == 0) { + return word2.length(); + } + if (word2.length() == 0) { + return word1.length(); + } + int[][] ans = new int[word1.length() + 1][word2.length() + 1]; + + //把有空串的情况更新了 + for (int i = 0; i <= word1.length(); i++) { + ans[i][0] = i; + } + for (int i = 0; i <= word2.length(); i++) { + ans[0][i] = i; + } + int n1 = word1.length(); + int n2 = word2.length(); + //从 1 开始遍历,从 0 开始的话,按照下边的算法取了 i - 1 会越界 + for (int i = 1; i <= n1; i++) { + for (int j = 1; j <= n2; j++) { + int min_delete = Math.min(ans[i - 1][j], ans[i][j - 1]) + 1; + int replace = ans[i - 1][j - 1]; + if (word1.charAt(i - 1) != word2.charAt(j - 1)) { + replace++; + } + ans[i][j] = Math.min(min_delete, replace); + } + } + return ans[n1][n2]; +} +``` + +时间复杂度:O(mn)。 + +空间复杂度:O(mn)。 + +如果你是顺序刷题的话,做到这里,一定会想到空间复杂度的优化,例如[5题](),[10题](),[53题]()等等。主要想法是,看上边的算法,我们再求 ans[i]\[\*\] 的时候,我们只用到 ans[i - 1]\[\*\] 的情况,所以我们完全只用两个数组就够了。 + +```java +public int minDistance(String word1, String word2) { + if (word1.length() == 0 && word2.length() == 0) { + return 0; + } + if (word1.length() == 0) { + return word2.length(); + } + if (word2.length() == 0) { + return word1.length(); + } + int[][] ans = new int[2][word2.length() + 1]; + + for (int i = 0; i <= word2.length(); i++) { + ans[0][i] = i; + } + int n1 = word1.length(); + int n2 = word2.length(); + for (int i = 1; i <= n1; i++) { + //由于只用了两个数组,所以不能向以前一样一次性初始化空串,在这里提前更新 j = 0 的情况 + ans[i % 2][0] = ans[(i - 1) % 2][0] + 1; + for (int j = 1; j <= n2; j++) { + int min_delete = Math.min(ans[(i - 1) % 2][j], ans[i % 2][j - 1]) + 1; + int replace = ans[(i - 1) % 2][j - 1]; + if (word1.charAt(i - 1) != word2.charAt(j - 1)) { + replace++; + } + ans[i % 2][j] = Math.min(min_delete, replace); + } + } + return ans[n1 % 2][n2]; +} +``` + +时间复杂度:O(mn)。 + +空间复杂度:O(n)。 + +再直接点,其实连两个数组我们都不需要,只需要一个数组。改写这个可能有些不好理解,可以结合一下图示。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/72_3.jpg) + +在更新二维数组的时候,我们都是一列一列的更新。在更新 ? 位置的时候,我们需要橙色位置的信息,也就是当前列的上一个位置,和上一列的当前位置,和上一列的上一个位置。如果我们用一个数组,当前列的上一个位置已经把上一列的上一个位置的数据覆盖掉了,所以我们要用一个变量提前保存上一列的上一个位置以便使用。 + +```java +public int minDistance(String word1, String word2) { + if (word1.length() == 0 && word2.length() == 0) { + return 0; + } + if (word1.length() == 0) { + return word2.length(); + } + if (word2.length() == 0) { + return word1.length(); + } + int[] ans = new int[word2.length() + 1]; + + for (int i = 0; i <= word2.length(); i++) { + ans[i] = i; + } + int n1 = word1.length(); + int n2 = word2.length(); + for (int i = 1; i <= n1; i++) { + int temp = ans[0]; + ans[0] = ans[0] + 1; + for (int j = 1; j <= n2; j++) { + int min_delete = Math.min(ans[j], ans[j - 1]) + 1; + //上一列的上一个位置,直接用 temp + int replace = temp; + if (word1.charAt(i - 1) != word2.charAt(j - 1)) { + replace++; + } + //保存当前列的信息 + temp = ans[j]; + //再进行更新 + ans[j] = Math.min(min_delete, replace); + } + } + return ans[n2]; +} +``` + +时间复杂度:O(mn)。 + +空间复杂度:O(n)。 + +# 总 + +动态规划的一系列操作,先递归,利用动态规划省略压栈的过程,然后空间复杂度的优化,很经典了。此外,对于动态规划数组的含义的定义也是很重要,开始的时候自己将 ans\[i\]\[j\] 表示为 字符串 word1[ 0, i ](word1 的第 0 到 第 i 个字符)和 word2[ 0, j - 1] 的最小编辑距离。和上边解法的区别只是包含了末尾的字符。这造成了初始化 ans\[0\]\[\*\] 和 ans\[\*\]\[0\] 的时候,会比较复杂,看到了[这里]()的解法,才有一种柳暗花明的感觉,思路是一样的,但更新ans\[0\]\[\*\] 和 ans\[\*\]\[0\] 却简单了很多。 + + + diff --git a/leetCode-74-Search-a-2D-Matrix.md b/leetCode-74-Search-a-2D-Matrix.md index c9899cce5..e2414c38d 100644 --- a/leetCode-74-Search-a-2D-Matrix.md +++ b/leetCode-74-Search-a-2D-Matrix.md @@ -1,41 +1,41 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/74.jpg) - -判断一个矩阵中是否存在某个数,矩阵是有序的。 - -# 解法一 二分法 - -看到了有序序列,啥都不用想直接二分,只需要考虑到怎么把二分时候的下标转换为矩阵的行、列下标就可以了,很简单,用除法和求余就够了。 - -```java -public boolean searchMatrix(int[][] matrix, int target) { - int rows = matrix.length; - if (rows == 0) { - return false; - } - int cols = matrix[0].length; - int left = 0; - int right = rows * cols - 1; - while (left <= right) { - int mid = (left + right) / 2; - int temp = matrix[mid / cols][mid % cols]; - if (temp == target) { - return true; - } else if (temp < target) { - left = mid + 1; - } else { - right = mid - 1; - } - } - return false; -} -``` - -时间复杂度:O ( log ( n ) )。 - -空间复杂度:O ( 1 )。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/74.jpg) + +判断一个矩阵中是否存在某个数,矩阵是有序的。 + +# 解法一 二分法 + +看到了有序序列,啥都不用想直接二分,只需要考虑到怎么把二分时候的下标转换为矩阵的行、列下标就可以了,很简单,用除法和求余就够了。 + +```java +public boolean searchMatrix(int[][] matrix, int target) { + int rows = matrix.length; + if (rows == 0) { + return false; + } + int cols = matrix[0].length; + int left = 0; + int right = rows * cols - 1; + while (left <= right) { + int mid = (left + right) / 2; + int temp = matrix[mid / cols][mid % cols]; + if (temp == target) { + return true; + } else if (temp < target) { + left = mid + 1; + } else { + right = mid - 1; + } + } + return false; +} +``` + +时间复杂度:O ( log ( n ) )。 + +空间复杂度:O ( 1 )。 + +# 总 + 这道题的二分法,比较简单,大家可以看下[33题](),相信对二分法会有一个更深刻的理解。 \ No newline at end of file diff --git a/leetCode-75-Sort-Colors.md b/leetCode-75-Sort-Colors.md index 46ca64616..4199fdee9 100644 --- a/leetCode-75-Sort-Colors.md +++ b/leetCode-75-Sort-Colors.md @@ -1,195 +1,195 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/75.jpg) - -给一个数组,含有的数只可能 0,1,2 中的一个,然后把这些数字从小到大排序。 - -# 解法一 - -题目下边的 Follow up 提到了一个解法,遍历一次数组,统计 0 出现的次数,1 出现的次数,2 出现的次数,然后再遍历数组,根据次数,把数组的元素改成相应的值。当然我们只需要记录 0 的次数,和 1 的次数,剩下的就是 2 的次数了。 - -``` java -public void sortColors(int[] nums) { - int zero_count = 0; - int one_count = 0; - for (int i = 0; i < nums.length; i++) { - if (nums[i] == 0) { - zero_count++; - } - if (nums[i] == 1) { - one_count++; - } - } - for (int i = 0; i < nums.length; i++) { - if (zero_count > 0) { - nums[i] = 0; - zero_count--; - } else if (one_count > 0) { - nums[i] = 1; - one_count--; - } else { - nums[i] = 2; - } - } -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(1)。 - -# 解法二 - -上边的算法,我们遍历了两次数组,让我们想一想只遍历一次的方法。我们假设一种简单的情况,如果只含有两个数 0 和 1,该怎么做呢? - -假设原数组是 1 0 1 1 0,我们可以用一个指针,zero_position,含义是该指针指向的位置,前边的位置全部存 0 。然后再用一个指针 i 遍历这个数组,找到 0 就把 0 放到当前 zero_position 指向的位置, zero_position 后移。用 Z 代表 zero_position,看下边的遍历过程。 - -``` -1 0 1 1 0 初始化 Z,i 指向第 0 个位置,i 后移 -^ -Z -i - -1 0 1 1 0 发现 0,把 Z 的位置置为 0,并且把 Z 的位置的数字交换过来,Z 后移一位 -^ ^ -Z i - -0 1 1 1 0 i 后移一位 - ^ - i - Z - -0 1 1 1 0 i 继续后移 - ^ ^ - Z i - -0 1 1 1 0 i 继续后移 - ^ ^ - Z i - -0 1 1 1 0 遇到 0,把 Z 的位置置为 0,并且把 Z 的位置的数字交换过来,Z 后移一位 - ^ ^ - Z i - -0 0 1 1 1 遍历结束 - ^ ^ - Z i -``` - -回到我们当前这道题,我们有 3 个数字,那我们可以用两个指针,一个是 zero_position,和之前一样,它前边的位置全部存 0。再来一个指针,two_position,注意这里是,它**后边**的位置全部存 2。然后遍历整个数组就行了。 - -下边画一个遍历过程中的图,理解一下,Z 前边全存 0,T 后边全存 2。 - -``` -0 1 0 2 1 2 2 2 - ^ ^ ^ - Z i T -``` - - - -```java -public void sortColors(int[] nums) { - int zero_position = 0; - int two_position = nums.length - 1; - for (int i = 0; i <= two_position; i++) { - if (nums[i] == 0) { - //将当前位置的数字保存 - int temp = nums[zero_position]; - //把 0 存过来 - nums[zero_position] = 0; - //把之前的数换过来 - nums[i] = temp; - //当前指针后移 - zero_position++; - } else if (nums[i] == 2) { - //将当前位置的数字保存 - int temp = nums[two_position]; - //把 2 存过来 - nums[two_position] = 2; - //把之前的数换过来 - nums[i] = temp; - //当前指针前移 - two_position--; - //这里一定要注意,因为我们把后边的数字换到了第 i 个位置, - //这个数字我们还没有判断它是多少,外层的 for 循环会使得 i++ 导致跳过这个元素 - //所以要 i-- - //而对于上边 zero_position 的更新不需要考虑,因为它是从前边换过来的数字 - //在之前已经都判断过了 - i--; - } - } -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(1)。 - -# 解法三 - -解法二中总共有三种数,然后很自然可以分成三部分,用两个指针作为间隔,但是,如果有 5 种数呢,解法二恐怕就不适用了。在 leetcode 发现另一种解法,参考[这里]()的解法二,用了大问题化小的思想。 - -我们用三个指针 n0,n1,n2,分别代表已排好序的数组当前 0 的末尾,1 的末尾,2 的末尾。 - -```java -0 0 1 2 2 2 0 2 1 - ^ ^ ^ ^ - n0 n1 n2 i -``` - -然后当前遍历到 i 的位置,等于 0,我们只需要把 n2 指针后移并且将当前数字置为 2,将 n1 指针后移并且将当前数字置为 1,将 n0 指针后移并且将当前数字置为 0。 - -```java -0 0 1 2 2 2 2 2 1 n2 后移后的情况 - ^ ^ ^ - n0 n1 i - n2 - -0 0 1 1 2 2 2 2 1 n1 后移后的情况 - ^ ^ ^ - n0 n1 i - n2 - -0 0 0 1 2 2 2 2 1 n0 后移后的情况 - ^ ^ ^ - n0 n1 i - n2 -``` - -然后就达到了将 i 指向的 0 插入到当前排好序的 0 的位置的末尾。 - -原因的话,由于前边插入了新的数字,势必造成数字的覆盖,指针后移后要把对应的指针位置置为对应的数,n2 指针后移后置为 2,n1 指针后移后置为 1,例如,假如之前有 3 个 2,由于前边插入一个数字,所以会导致 1 个 2 被覆盖掉,所以要加 1 个 2。 - -```java -public void sortColors(int[] nums) { - int n0 = -1, n1 = -1, n2 = -1; - int n = nums.length; - for (int i = 0; i < n; i++) { - if (nums[i] == 0) { - n2++; - nums[n2] = 2; - n1++; - nums[n1] = 1; - n0++; - nums[n0] = 0; - } else if (nums[i] == 1) { - n2++; - nums[n2] = 2; - n1++; - nums[n1] = 1; - } else if (nums[i] == 2) { - n2++; - nums[n2] = 2; - } - } -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(1)。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/75.jpg) + +给一个数组,含有的数只可能 0,1,2 中的一个,然后把这些数字从小到大排序。 + +# 解法一 + +题目下边的 Follow up 提到了一个解法,遍历一次数组,统计 0 出现的次数,1 出现的次数,2 出现的次数,然后再遍历数组,根据次数,把数组的元素改成相应的值。当然我们只需要记录 0 的次数,和 1 的次数,剩下的就是 2 的次数了。 + +``` java +public void sortColors(int[] nums) { + int zero_count = 0; + int one_count = 0; + for (int i = 0; i < nums.length; i++) { + if (nums[i] == 0) { + zero_count++; + } + if (nums[i] == 1) { + one_count++; + } + } + for (int i = 0; i < nums.length; i++) { + if (zero_count > 0) { + nums[i] = 0; + zero_count--; + } else if (one_count > 0) { + nums[i] = 1; + one_count--; + } else { + nums[i] = 2; + } + } +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(1)。 + +# 解法二 + +上边的算法,我们遍历了两次数组,让我们想一想只遍历一次的方法。我们假设一种简单的情况,如果只含有两个数 0 和 1,该怎么做呢? + +假设原数组是 1 0 1 1 0,我们可以用一个指针,zero_position,含义是该指针指向的位置,前边的位置全部存 0 。然后再用一个指针 i 遍历这个数组,找到 0 就把 0 放到当前 zero_position 指向的位置, zero_position 后移。用 Z 代表 zero_position,看下边的遍历过程。 + +``` +1 0 1 1 0 初始化 Z,i 指向第 0 个位置,i 后移 +^ +Z +i + +1 0 1 1 0 发现 0,把 Z 的位置置为 0,并且把 Z 的位置的数字交换过来,Z 后移一位 +^ ^ +Z i + +0 1 1 1 0 i 后移一位 + ^ + i + Z + +0 1 1 1 0 i 继续后移 + ^ ^ + Z i + +0 1 1 1 0 i 继续后移 + ^ ^ + Z i + +0 1 1 1 0 遇到 0,把 Z 的位置置为 0,并且把 Z 的位置的数字交换过来,Z 后移一位 + ^ ^ + Z i + +0 0 1 1 1 遍历结束 + ^ ^ + Z i +``` + +回到我们当前这道题,我们有 3 个数字,那我们可以用两个指针,一个是 zero_position,和之前一样,它前边的位置全部存 0。再来一个指针,two_position,注意这里是,它**后边**的位置全部存 2。然后遍历整个数组就行了。 + +下边画一个遍历过程中的图,理解一下,Z 前边全存 0,T 后边全存 2。 + +``` +0 1 0 2 1 2 2 2 + ^ ^ ^ + Z i T +``` + + + +```java +public void sortColors(int[] nums) { + int zero_position = 0; + int two_position = nums.length - 1; + for (int i = 0; i <= two_position; i++) { + if (nums[i] == 0) { + //将当前位置的数字保存 + int temp = nums[zero_position]; + //把 0 存过来 + nums[zero_position] = 0; + //把之前的数换过来 + nums[i] = temp; + //当前指针后移 + zero_position++; + } else if (nums[i] == 2) { + //将当前位置的数字保存 + int temp = nums[two_position]; + //把 2 存过来 + nums[two_position] = 2; + //把之前的数换过来 + nums[i] = temp; + //当前指针前移 + two_position--; + //这里一定要注意,因为我们把后边的数字换到了第 i 个位置, + //这个数字我们还没有判断它是多少,外层的 for 循环会使得 i++ 导致跳过这个元素 + //所以要 i-- + //而对于上边 zero_position 的更新不需要考虑,因为它是从前边换过来的数字 + //在之前已经都判断过了 + i--; + } + } +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(1)。 + +# 解法三 + +解法二中总共有三种数,然后很自然可以分成三部分,用两个指针作为间隔,但是,如果有 5 种数呢,解法二恐怕就不适用了。在 leetcode 发现另一种解法,参考[这里]()的解法二,用了大问题化小的思想。 + +我们用三个指针 n0,n1,n2,分别代表已排好序的数组当前 0 的末尾,1 的末尾,2 的末尾。 + +```java +0 0 1 2 2 2 0 2 1 + ^ ^ ^ ^ + n0 n1 n2 i +``` + +然后当前遍历到 i 的位置,等于 0,我们只需要把 n2 指针后移并且将当前数字置为 2,将 n1 指针后移并且将当前数字置为 1,将 n0 指针后移并且将当前数字置为 0。 + +```java +0 0 1 2 2 2 2 2 1 n2 后移后的情况 + ^ ^ ^ + n0 n1 i + n2 + +0 0 1 1 2 2 2 2 1 n1 后移后的情况 + ^ ^ ^ + n0 n1 i + n2 + +0 0 0 1 2 2 2 2 1 n0 后移后的情况 + ^ ^ ^ + n0 n1 i + n2 +``` + +然后就达到了将 i 指向的 0 插入到当前排好序的 0 的位置的末尾。 + +原因的话,由于前边插入了新的数字,势必造成数字的覆盖,指针后移后要把对应的指针位置置为对应的数,n2 指针后移后置为 2,n1 指针后移后置为 1,例如,假如之前有 3 个 2,由于前边插入一个数字,所以会导致 1 个 2 被覆盖掉,所以要加 1 个 2。 + +```java +public void sortColors(int[] nums) { + int n0 = -1, n1 = -1, n2 = -1; + int n = nums.length; + for (int i = 0; i < n; i++) { + if (nums[i] == 0) { + n2++; + nums[n2] = 2; + n1++; + nums[n1] = 1; + n0++; + nums[n0] = 0; + } else if (nums[i] == 1) { + n2++; + nums[n2] = 2; + n1++; + nums[n1] = 1; + } else if (nums[i] == 2) { + n2++; + nums[n2] = 2; + } + } +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(1)。 + +# 总 + 解法二利用指针,在原来的空间存东西很经典。解法三,其实本质是我们常用的递归思想,先假设一个小问题解决了,然后假如再来一个数该怎么操作。 \ No newline at end of file diff --git a/leetCode-76-Minimum-Window-Substring.md b/leetCode-76-Minimum-Window-Substring.md index b7271d551..d760796e1 100644 --- a/leetCode-76-Minimum-Window-Substring.md +++ b/leetCode-76-Minimum-Window-Substring.md @@ -1,198 +1,198 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/76.jpg) - -给两个字符串,S 和 T,在 S 中找出包含 T 中所有字母的最短字符串,不考虑顺序。 - -# 解法一 滑动窗口 - -没有想出来,直接看来了[题解](),这里总结一下。 - -用双指针 left 和 right 表示一个窗口。 - -1. right 向右移增大窗口,直到窗口包含了所有要求的字母。进行第二步。 -2. 记录此时的长度,left 向右移动,开始减少长度,每减少一次,就更新最小长度。直到当前窗口不包含所有字母,回到第 1 步。 - -```java -S = "ADOBECODEBANC", T = "ABC" -A D O B E C O D E B A N C //l 和 r 初始化为 0 -^ -l -r - -A D O B E C O D E B A N C //向后移动 r,扩大窗口 -^ ^ -l r - -A D O B E C O D E B A N C //向后移动 r,扩大窗口 -^ ^ -l r - -A D O B E C O D E B A N C //向后移动 r,扩大窗口 -^ ^ -l r - -A D O B E C O D E B A N C //向后移动 r,扩大窗口 -^ ^ -l r - - -//此时窗口中包含了所有字母(ABC),停止移动 r,记录此时的 l 和 r,然后开始移动 l -A D O B E C O D E B A N C -^ ^ -l r - -//向后移动 l,减小窗口,此时窗口中没有包含所有字母(ABC),重新开始移动 r,扩大窗口 -A D O B E C O D E B A N C - ^ ^ - l r - -//移动 r 直到窗口包含了所有字母(ABC), -//和之前的长度进行比较,如果窗口更小,则更新 l 和 r -//然后开始移动 l,开始缩小窗口 -A D O B E C O D E B A N C - ^ ^ - l r - -//此时窗口内依旧包含所有字母 -//和之前的长度进行比较,如果窗口更小,则更新 l 和 r -//继续移动 l,继续缩小窗口 -A D O B E C O D E B A N C - ^ ^ - l r - -//此时窗口内依旧包含所有字母 -//和之前的长度进行比较,如果窗口更小,则更新 l 和 r -//继续移动 l,继续缩小窗口 -A D O B E C O D E B A N C - ^ ^ - l r - -//继续减小 l,直到窗口中不再包含所有字母,然后开始移动 r,不停的重复上边的过程,直到全部遍历完 -``` - -思想有了,其实这里需要解决的问题只有一个,怎么来判断当前窗口包含了所有字母。 - -判断字符串相等,并且不要求顺序,之前已经用过很多次了,利用 HashMap,对于两个字符串 S = "ADOBECODEBANC", T = "ABCB",用 map 统计 T 的每个字母的出现次数,然后遍历 S,遇到相应的字母,就将相应字母的次数减 1,如果此时 map 中所有字母的次数都小于等于 0,那么此时的窗口一定包含了所有字母。 - -```java -public String minWindow(String s, String t) { - Map map = new HashMap<>(); - //遍历字符串 t,初始化每个字母的次数 - for (int i = 0; i < t.length(); i++) { - char char_i = t.charAt(i); - map.put(char_i, map.getOrDefault(char_i, 0) + 1); - } - int left = 0; //左指针 - int right = 0; //右指针 - int ans_left = 0; //保存最小窗口的左边界 - int ans_right = -1; //保存最小窗口的右边界 - int ans_len = Integer.MAX_VALUE; //当前最小窗口的长度 - //遍历字符串 s - while (right < s.length()) { - char char_right = s.charAt(right); - //判断 map 中是否含有当前字母 - if (map.containsKey(char_right)) { - //当前的字母次数减一 - map.put(char_right, map.get(char_right) - 1); - //开始移动左指针,减小窗口 - while (match(map)) { //如果当前窗口包含所有字母,就进入循环 - //当前窗口大小 - int temp_len = right - left + 1; - //如果当前窗口更小,则更新相应变量 - if (temp_len < ans_len) { - ans_left = left; - ans_right = right; - ans_len = temp_len; - } - //得到左指针的字母 - char key = s.charAt(left); - //判断 map 中是否有当前字母 - if (map.containsKey(key)) { - //因为要把当前字母移除,所有相应次数要加 1 - map.put(key, map.get(key) + 1); - } - left++; //左指针右移 - } - } - //右指针右移扩大窗口 - right++; - } - return s.substring(ans_left, ans_right+1); -} - -//判断所有的 value 是否为 0 -private boolean match(Map map) { - for (Integer value : map.values()) { - if (value > 0) { - return false; - } - } - return true; -} -``` - -时间复杂度:O(nm),n 是 S 的长度,match 函数消耗 O(m)。 - -空间复杂度:O(m),m 是 T 的长度。 - -参考[这里](),由于字符串中只有字母,我们其实可以不用 hashmap,可以直接用一个 int 数组,字母的 ascii 码值作为下标,保存每个字母的次数。 - -此外,判断当前窗口是否含有所有字母,我们除了可以判断所有字母的次数是否小于等于 0,还可以用一个计数变量 count,把 count 初始化为 t 的长度,然后每次找到一个满足条件的字母,count 就减 1,如果 count 等于了 0,就代表包含了所有字母。这样的话,可以把之前的 match(map) 优化到 O(1)。 - -```java -public String minWindow(String s, String t) { - int[] map = new int[128]; - // 遍历字符串 t,初始化每个字母的次数 - for (int i = 0; i < t.length(); i++) { - char char_i = t.charAt(i); - map[char_i]++; - } - int left = 0; // 左指针 - int right = 0; // 右指针 - int ans_left = 0; // 保存最小窗口的左边界 - int ans_right = -1; // 保存最小窗口的右边界 - int ans_len = Integer.MAX_VALUE; // 当前最小窗口的长度 - int count = t.length(); - // 遍历字符串 s - while (right < s.length()) { - char char_right = s.charAt(right); - - // 当前的字母次数减一 - map[char_right]--; - - //代表当前符合了一个字母 - if (map[char_right] >= 0) { - count--; - } - // 开始移动左指针,减小窗口 - while (count == 0) { // 如果当前窗口包含所有字母,就进入循环 - // 当前窗口大小 - int temp_len = right - left + 1; - // 如果当前窗口更小,则更新相应变量 - if (temp_len < ans_len) { - ans_left = left; - ans_right = right; - ans_len = temp_len; - } - // 得到左指针的字母 - char key = s.charAt(left); - // 因为要把当前字母移除,所有相应次数要加 1 - map[key]++; - //此时的 map[key] 大于 0 了,表示缺少当前字母了,count++ - if (map[key] > 0) { - count++; - } - left++; // 左指针右移 - } - // 右指针右移扩大窗口 - right++; - } - return s.substring(ans_left, ans_right + 1); -} -``` - -# 总 - -开始自己的思路偏了,一直往递归,动态规划的思想走,导致没想出来。对滑动窗口的应用的少,这次加深了印象。 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/76.jpg) + +给两个字符串,S 和 T,在 S 中找出包含 T 中所有字母的最短字符串,不考虑顺序。 + +# 解法一 滑动窗口 + +没有想出来,直接看来了[题解](),这里总结一下。 + +用双指针 left 和 right 表示一个窗口。 + +1. right 向右移增大窗口,直到窗口包含了所有要求的字母。进行第二步。 +2. 记录此时的长度,left 向右移动,开始减少长度,每减少一次,就更新最小长度。直到当前窗口不包含所有字母,回到第 1 步。 + +```java +S = "ADOBECODEBANC", T = "ABC" +A D O B E C O D E B A N C //l 和 r 初始化为 0 +^ +l +r + +A D O B E C O D E B A N C //向后移动 r,扩大窗口 +^ ^ +l r + +A D O B E C O D E B A N C //向后移动 r,扩大窗口 +^ ^ +l r + +A D O B E C O D E B A N C //向后移动 r,扩大窗口 +^ ^ +l r + +A D O B E C O D E B A N C //向后移动 r,扩大窗口 +^ ^ +l r + + +//此时窗口中包含了所有字母(ABC),停止移动 r,记录此时的 l 和 r,然后开始移动 l +A D O B E C O D E B A N C +^ ^ +l r + +//向后移动 l,减小窗口,此时窗口中没有包含所有字母(ABC),重新开始移动 r,扩大窗口 +A D O B E C O D E B A N C + ^ ^ + l r + +//移动 r 直到窗口包含了所有字母(ABC), +//和之前的长度进行比较,如果窗口更小,则更新 l 和 r +//然后开始移动 l,开始缩小窗口 +A D O B E C O D E B A N C + ^ ^ + l r + +//此时窗口内依旧包含所有字母 +//和之前的长度进行比较,如果窗口更小,则更新 l 和 r +//继续移动 l,继续缩小窗口 +A D O B E C O D E B A N C + ^ ^ + l r + +//此时窗口内依旧包含所有字母 +//和之前的长度进行比较,如果窗口更小,则更新 l 和 r +//继续移动 l,继续缩小窗口 +A D O B E C O D E B A N C + ^ ^ + l r + +//继续减小 l,直到窗口中不再包含所有字母,然后开始移动 r,不停的重复上边的过程,直到全部遍历完 +``` + +思想有了,其实这里需要解决的问题只有一个,怎么来判断当前窗口包含了所有字母。 + +判断字符串相等,并且不要求顺序,之前已经用过很多次了,利用 HashMap,对于两个字符串 S = "ADOBECODEBANC", T = "ABCB",用 map 统计 T 的每个字母的出现次数,然后遍历 S,遇到相应的字母,就将相应字母的次数减 1,如果此时 map 中所有字母的次数都小于等于 0,那么此时的窗口一定包含了所有字母。 + +```java +public String minWindow(String s, String t) { + Map map = new HashMap<>(); + //遍历字符串 t,初始化每个字母的次数 + for (int i = 0; i < t.length(); i++) { + char char_i = t.charAt(i); + map.put(char_i, map.getOrDefault(char_i, 0) + 1); + } + int left = 0; //左指针 + int right = 0; //右指针 + int ans_left = 0; //保存最小窗口的左边界 + int ans_right = -1; //保存最小窗口的右边界 + int ans_len = Integer.MAX_VALUE; //当前最小窗口的长度 + //遍历字符串 s + while (right < s.length()) { + char char_right = s.charAt(right); + //判断 map 中是否含有当前字母 + if (map.containsKey(char_right)) { + //当前的字母次数减一 + map.put(char_right, map.get(char_right) - 1); + //开始移动左指针,减小窗口 + while (match(map)) { //如果当前窗口包含所有字母,就进入循环 + //当前窗口大小 + int temp_len = right - left + 1; + //如果当前窗口更小,则更新相应变量 + if (temp_len < ans_len) { + ans_left = left; + ans_right = right; + ans_len = temp_len; + } + //得到左指针的字母 + char key = s.charAt(left); + //判断 map 中是否有当前字母 + if (map.containsKey(key)) { + //因为要把当前字母移除,所有相应次数要加 1 + map.put(key, map.get(key) + 1); + } + left++; //左指针右移 + } + } + //右指针右移扩大窗口 + right++; + } + return s.substring(ans_left, ans_right+1); +} + +//判断所有的 value 是否为 0 +private boolean match(Map map) { + for (Integer value : map.values()) { + if (value > 0) { + return false; + } + } + return true; +} +``` + +时间复杂度:O(nm),n 是 S 的长度,match 函数消耗 O(m)。 + +空间复杂度:O(m),m 是 T 的长度。 + +参考[这里](),由于字符串中只有字母,我们其实可以不用 hashmap,可以直接用一个 int 数组,字母的 ascii 码值作为下标,保存每个字母的次数。 + +此外,判断当前窗口是否含有所有字母,我们除了可以判断所有字母的次数是否小于等于 0,还可以用一个计数变量 count,把 count 初始化为 t 的长度,然后每次找到一个满足条件的字母,count 就减 1,如果 count 等于了 0,就代表包含了所有字母。这样的话,可以把之前的 match(map) 优化到 O(1)。 + +```java +public String minWindow(String s, String t) { + int[] map = new int[128]; + // 遍历字符串 t,初始化每个字母的次数 + for (int i = 0; i < t.length(); i++) { + char char_i = t.charAt(i); + map[char_i]++; + } + int left = 0; // 左指针 + int right = 0; // 右指针 + int ans_left = 0; // 保存最小窗口的左边界 + int ans_right = -1; // 保存最小窗口的右边界 + int ans_len = Integer.MAX_VALUE; // 当前最小窗口的长度 + int count = t.length(); + // 遍历字符串 s + while (right < s.length()) { + char char_right = s.charAt(right); + + // 当前的字母次数减一 + map[char_right]--; + + //代表当前符合了一个字母 + if (map[char_right] >= 0) { + count--; + } + // 开始移动左指针,减小窗口 + while (count == 0) { // 如果当前窗口包含所有字母,就进入循环 + // 当前窗口大小 + int temp_len = right - left + 1; + // 如果当前窗口更小,则更新相应变量 + if (temp_len < ans_len) { + ans_left = left; + ans_right = right; + ans_len = temp_len; + } + // 得到左指针的字母 + char key = s.charAt(left); + // 因为要把当前字母移除,所有相应次数要加 1 + map[key]++; + //此时的 map[key] 大于 0 了,表示缺少当前字母了,count++ + if (map[key] > 0) { + count++; + } + left++; // 左指针右移 + } + // 右指针右移扩大窗口 + right++; + } + return s.substring(ans_left, ans_right + 1); +} +``` + +# 总 + +开始自己的思路偏了,一直往递归,动态规划的思想走,导致没想出来。对滑动窗口的应用的少,这次加深了印象。 + diff --git a/leetCode-77-Combinations.md b/leetCode-77-Combinations.md index 059445cf7..a105303d4 100644 --- a/leetCode-77-Combinations.md +++ b/leetCode-77-Combinations.md @@ -1,278 +1,278 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/77.jpg) - -给定 n ,k ,表示从 { 1, 2, 3 ... n } 中选 k 个数,输出所有可能,并且选出数字从小到大排列,每个数字只能用一次。 - -# 解法一 回溯法 - -这种选数字很经典的回溯法问题了,先选一个数字,然后进入递归继续选,满足条件后加到结果中,然后回溯到上一步,继续递归。直接看代码吧,很好理解。 - -```java -public List> combine(int n, int k) { - List> ans = new ArrayList<>(); - getAns(1,n, k, new ArrayList(), ans); - return ans; -} - -private void getAns(int start, int n, int k, ArrayList temp,List> ans) { - //如果 temp 里的数字够了 k 个,就把它加入到结果中 - if(temp.size() == k){ - ans.add(new ArrayList(temp)); - return; - } - //从 start 到 n - for (int i = start; i <= n; i++) { - //将当前数字加入 temp - temp.add(i); - //进入递归 - getAns(i+1, n, k, temp, ans); - //将当前数字删除,进入下次 for 循环 - temp.remove(temp.size() - 1); - } -} -``` - -一个 for 循环,添加,递归,删除,很经典的回溯框架了。在[这里]()发现了一个优化方法。for 循环里 i 从 start 到 n,其实没必要到 n。比如,n = 5,k = 4,temp.size( ) == 1,此时代表我们还需要(4 - 1 = 3)个数字,如果 i = 4 的话,以后最多把 4 和 5 加入到 temp 中,而此时 temp.size() 才等于 1 + 2 = 3,不够 4 个,所以 i 没必要等于 4,i 循环到 3 就足够了。 - -所以 for 循环的结束条件可以改成, i <= n - ( k - temp.size ( ) ) + 1,k - temp.size ( ) 代表我们还需要的数字个数。因为我们最后取到了 n,所以还要加 1。 - -```java -public List> combine(int n, int k) { - List> ans = new ArrayList<>(); - getAns(1,n, k, new ArrayList(), ans); - return ans; -} - -private void getAns(int start, int n, int k, ArrayList temp, List> ans) { - if(temp.size() == k){ - ans.add(new ArrayList(temp)); - return; - } - for (int i = start; i <= n - (k -temp.size()) + 1; i++) { - temp.add(i); - getAns(i+1, n, k, temp, ans); - temp.remove(temp.size() - 1); - } -} -``` - -虽然只改了一句代码,速度却快了很多。 - -# 解法二 迭代 - -参考[这里](),完全按照解法一回溯的思想改成迭代。我们思考一下,回溯其实有三个过程。 - -* for 循环结束,也就是 i == n + 1,然后回到上一层的 for 循环 -* temp.size() == k,也就是所需要的数字够了,然后把它加入到结果中。 -* 每个 for 循环里边,进入递归,添加下一个数字 - -```java -public List> combine(int n, int k) { - List> ans = new ArrayList<>(); - List temp = new ArrayList<>(); - for(int i = 0;i= 0) { - temp.set(i, temp.get(i)+ 1); //当前数字加 1 - //当前数字大于 n,对应回溯法的 i == n + 1,然后回到上一层 - if (temp.get(i) > n) { - i--; - // 当前数字个数够了 - } else if (i == k - 1) { - ans.add(new ArrayList<>(temp)); - //进入更新下一个数字 - }else { - i++; - //把下一个数字置为上一个数字,类似于回溯法中的 start - temp.set(i, temp.get(i-1)); - } - } - return ans; -} -``` - -# 解法三 迭代法2 - -解法二的迭代法是基于回溯的思想,还有一种思想,参考[这里]()。类似于[46题]()的解法一,找 k 个数,我们可以先找出 1 个的所有结果,然后在 1 个的所有结果再添加 1 个数,变成 2 个,然后依次迭代,直到有 k 个数。 - -比如 n = 5, k = 3 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/77_2.jpg) - -第 1 次循环,我们找出所有 1 个数的可能 [ 1 ],[ 2 ],[ 3 ]。4 和 5 不可能,解法一分析过了,因为总共需要 3 个数,4,5 全加上才 2 个数。 - -第 2 次循环,在每个 list 添加 1 个数, [ 1 ] 扩展为 [ 1 , 2 ],[ 1 , 3 ],[ 1 , 4 ]。[ 1 , 5 ] 不可能,因为 5 后边没有数字了。 [ 2 ] 扩展为 [ 2 , 3 ],[ 2 , 4 ]。[ 3 ] 扩展为 [ 3 , 4 ]; - -第 3 次循环,在每个 list 添加 1 个数, [ 1,2 ] 扩展为[ 1,2,3], [ 1,2,4], [ 1,2,5];[ 1,3 ] 扩展为 [ 1,3,4], [ 1,3,5];[ 1,4 ] 扩展为 [ 1,4,5];[ 2,3 ] 扩展为 [ 2,3,4], [ 2,3,5];[ 2,4 ] 扩展为 [ 2,4,5];[ 3,4 ] 扩展为 [ 3,4,5]; - -最后结果就是,\[[ 1,2,3], [ 1,2,4], [ 1,2,5],[ 1,3,4], [ 1,3,5], [ 1,4,5], [ 2,3,4], [ 2,3,5],[ 2,4,5], [ 3,4,5]\]。 - -上边分析很明显了,三个循环,第一层循环是 1 到 k ,代表当前有多少个数。第二层循环就是遍历之前的所有结果。第三次循环就是将当前结果扩展为多个。 - -```java -public List> combine(int n, int k) { - if (n == 0 || k == 0 || k > n) return Collections.emptyList(); - List> res = new ArrayList>(); - //个数为 1 的所有可能 - for (int i = 1; i <= n + 1 - k; i++) res.add(Arrays.asList(i)); - //第一层循环,从 2 到 k - for (int i = 2; i <= k; i++) { - List> tmp = new ArrayList>(); - //第二层循环,遍历之前所有的结果 - for (List list : res) { - //第三次循环,对每个结果进行扩展 - //从最后一个元素加 1 开始,然后不是到 n ,而是和解法一的优化一样 - //(k - (i - 1) 代表当前已经有的个数,最后再加 1 是因为取了 n - for (int m = list.get(list.size() - 1) + 1; m <= n - (k - (i - 1)) + 1; m++) { - List newList = new ArrayList(list); - newList.add(m); - tmp.add(newList); - } - } - res = tmp; - } - return res; -} -``` - -# 解法四 递归 - -参考[这里]()。基于这个公式 C ( n, k ) = C ( n - 1, k - 1) + C ( n - 1, k ) 所用的思想,这个思想之前刷题也用过,但忘记是哪道了。 - -从 n 个数字选 k 个,我们把所有结果分为两种,包含第 n 个数和不包含第 n 个数。这样的话,就可以把问题转换成 - -* 从 n - 1 里边选 k - 1 个,然后每个结果加上 n -* 从 n - 1 个里边直接选 k 个。 - -把上边两个的结果合起来就可以了。 - - -```java -public List> combine(int n, int k) { - if (k == n || k == 0) { - List row = new LinkedList<>(); - for (int i = 1; i <= k; ++i) { - row.add(i); - } - return new LinkedList<>(Arrays.asList(row)); - } - // n - 1 里边选 k - 1 个 - List> result = combine(n - 1, k - 1); - //每个结果加上 n - result.forEach(e -> e.add(n)); - //把 n - 1 个选 k 个的结果也加入 - result.addAll(combine(n - 1, k)); - return result; -} -``` - -# 解法五 动态规划 - -参考[这里](),既然有了解法四的递归,那么一定可以有动态规划。递归就是压栈压栈压栈,然后到了递归出口,开始出栈出栈出栈。而动态规划一个好处就是省略了出栈的过程,我们直接从递归出口网上走。 - -```java -public List> combine(int n, int k) { - List>[][] dp = new List[n + 1][k + 1]; - //更新 k = 0 的所有情况 - for (int i = 0; i <= n; i++) { - dp[i][0] = new ArrayList<>(); - dp[i][0].add(new ArrayList()); - } - // i 从 1 到 n - for (int i = 1; i <= n; i++) { - // j 从 1 到 i 或者 k - for (int j = 1; j <= i && j <= k; j++) { - dp[i][j] = new ArrayList<>(); - //判断是否可以从 i - 1 里边选 j 个 - if (i > j){ - dp[i][j].addAll(dp[i - 1][j]); - } - //把 i - 1 里边选 j - 1 个的每个结果加上 i - for (List list: dp[i - 1][j - 1]) { - List tmpList = new ArrayList<>(list); - tmpList.add(i); - dp[i][j].add(tmpList); - } - } - } - return dp[n][k]; -} -``` - -这里遇到个神奇的问题,提一下,开始的的时候,最里边的 for 循环是这样写的 - -```java -for (List list: dp[i - 1][j - 1]) { - List tmpList = new LinkedList<>(list); - tmpList.add(i); - dp[i][j].add(tmpList); -} -``` - -就是 List 用的 Linked,而不是 Array,看起来没什么大问题,在 leetcode 上竟然报了超时。看了下 java 的源码。 - -```java -//ArrayList -public boolean add(E e) { - ensureCapacityInternal(size + 1); // Increments modCount!! - elementData[size++] = e; - return true; -} -//LinkedList -public boolean add(E e) { - linkLast(e); - return true; -} -void linkLast(E e) { - final Node l = last; - final Node newNode = new Node<>(l, e, null); - last = newNode; - if (l == null) - first = newNode; - else - l.next = newNode; - size++; - modCount++; -} -``` - -猜测原因可能是因为 linked 每次 add 的时候,都需要 new 一个节点对象,而我们进行了很多次 add,所以这里造成了时间的耗费,导致了超时。所以刷题的时候还是优先用 ArrayList 吧。 - -接下来就是动态规划的常规操作了,空间复杂度的优化,我们注意到更新 dp [ i \] \[ \* \] 的时候,只用到dp [ i - 1 \] \[ \* \] 的情况,所以我们可以只用一个一维数组就够了。和[72题]()解法二,以及[5题](),[10题](),[53题](> combine(int n, int k) { - List>[] dp = new ArrayList[k + 1]; - // i 从 1 到 n - dp[0] = new ArrayList<>(); - dp[0].add(new ArrayList()); - for (int i = 1; i <= n; i++) { - // j 从 1 到 i 或者 k - List> temp = new ArrayList<>(dp[0]); - for (int j = 1; j <= i && j <= k; j++) { - List> last = temp; - if(dp[j]!=null){ - temp = new ArrayList<>(dp[j]); - } - // 判断是否可以从 i - 1 里边选 j 个 - if (i <= j) { - dp[j] = new ArrayList<>(); - } - // 把 i - 1 里边选 j - 1 个的每个结果加上 i - for (List list : last) { - List tmpList = new ArrayList<>(list); - tmpList.add(i); - dp[j].add(tmpList); - } - } - } - return dp[k]; -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/77.jpg) + +给定 n ,k ,表示从 { 1, 2, 3 ... n } 中选 k 个数,输出所有可能,并且选出数字从小到大排列,每个数字只能用一次。 + +# 解法一 回溯法 + +这种选数字很经典的回溯法问题了,先选一个数字,然后进入递归继续选,满足条件后加到结果中,然后回溯到上一步,继续递归。直接看代码吧,很好理解。 + +```java +public List> combine(int n, int k) { + List> ans = new ArrayList<>(); + getAns(1,n, k, new ArrayList(), ans); + return ans; +} + +private void getAns(int start, int n, int k, ArrayList temp,List> ans) { + //如果 temp 里的数字够了 k 个,就把它加入到结果中 + if(temp.size() == k){ + ans.add(new ArrayList(temp)); + return; + } + //从 start 到 n + for (int i = start; i <= n; i++) { + //将当前数字加入 temp + temp.add(i); + //进入递归 + getAns(i+1, n, k, temp, ans); + //将当前数字删除,进入下次 for 循环 + temp.remove(temp.size() - 1); + } +} +``` + +一个 for 循环,添加,递归,删除,很经典的回溯框架了。在[这里]()发现了一个优化方法。for 循环里 i 从 start 到 n,其实没必要到 n。比如,n = 5,k = 4,temp.size( ) == 1,此时代表我们还需要(4 - 1 = 3)个数字,如果 i = 4 的话,以后最多把 4 和 5 加入到 temp 中,而此时 temp.size() 才等于 1 + 2 = 3,不够 4 个,所以 i 没必要等于 4,i 循环到 3 就足够了。 + +所以 for 循环的结束条件可以改成, i <= n - ( k - temp.size ( ) ) + 1,k - temp.size ( ) 代表我们还需要的数字个数。因为我们最后取到了 n,所以还要加 1。 + +```java +public List> combine(int n, int k) { + List> ans = new ArrayList<>(); + getAns(1,n, k, new ArrayList(), ans); + return ans; +} + +private void getAns(int start, int n, int k, ArrayList temp, List> ans) { + if(temp.size() == k){ + ans.add(new ArrayList(temp)); + return; + } + for (int i = start; i <= n - (k -temp.size()) + 1; i++) { + temp.add(i); + getAns(i+1, n, k, temp, ans); + temp.remove(temp.size() - 1); + } +} +``` + +虽然只改了一句代码,速度却快了很多。 + +# 解法二 迭代 + +参考[这里](),完全按照解法一回溯的思想改成迭代。我们思考一下,回溯其实有三个过程。 + +* for 循环结束,也就是 i == n + 1,然后回到上一层的 for 循环 +* temp.size() == k,也就是所需要的数字够了,然后把它加入到结果中。 +* 每个 for 循环里边,进入递归,添加下一个数字 + +```java +public List> combine(int n, int k) { + List> ans = new ArrayList<>(); + List temp = new ArrayList<>(); + for(int i = 0;i= 0) { + temp.set(i, temp.get(i)+ 1); //当前数字加 1 + //当前数字大于 n,对应回溯法的 i == n + 1,然后回到上一层 + if (temp.get(i) > n) { + i--; + // 当前数字个数够了 + } else if (i == k - 1) { + ans.add(new ArrayList<>(temp)); + //进入更新下一个数字 + }else { + i++; + //把下一个数字置为上一个数字,类似于回溯法中的 start + temp.set(i, temp.get(i-1)); + } + } + return ans; +} +``` + +# 解法三 迭代法2 + +解法二的迭代法是基于回溯的思想,还有一种思想,参考[这里]()。类似于[46题]()的解法一,找 k 个数,我们可以先找出 1 个的所有结果,然后在 1 个的所有结果再添加 1 个数,变成 2 个,然后依次迭代,直到有 k 个数。 + +比如 n = 5, k = 3 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/77_2.jpg) + +第 1 次循环,我们找出所有 1 个数的可能 [ 1 ],[ 2 ],[ 3 ]。4 和 5 不可能,解法一分析过了,因为总共需要 3 个数,4,5 全加上才 2 个数。 + +第 2 次循环,在每个 list 添加 1 个数, [ 1 ] 扩展为 [ 1 , 2 ],[ 1 , 3 ],[ 1 , 4 ]。[ 1 , 5 ] 不可能,因为 5 后边没有数字了。 [ 2 ] 扩展为 [ 2 , 3 ],[ 2 , 4 ]。[ 3 ] 扩展为 [ 3 , 4 ]; + +第 3 次循环,在每个 list 添加 1 个数, [ 1,2 ] 扩展为[ 1,2,3], [ 1,2,4], [ 1,2,5];[ 1,3 ] 扩展为 [ 1,3,4], [ 1,3,5];[ 1,4 ] 扩展为 [ 1,4,5];[ 2,3 ] 扩展为 [ 2,3,4], [ 2,3,5];[ 2,4 ] 扩展为 [ 2,4,5];[ 3,4 ] 扩展为 [ 3,4,5]; + +最后结果就是,\[[ 1,2,3], [ 1,2,4], [ 1,2,5],[ 1,3,4], [ 1,3,5], [ 1,4,5], [ 2,3,4], [ 2,3,5],[ 2,4,5], [ 3,4,5]\]。 + +上边分析很明显了,三个循环,第一层循环是 1 到 k ,代表当前有多少个数。第二层循环就是遍历之前的所有结果。第三次循环就是将当前结果扩展为多个。 + +```java +public List> combine(int n, int k) { + if (n == 0 || k == 0 || k > n) return Collections.emptyList(); + List> res = new ArrayList>(); + //个数为 1 的所有可能 + for (int i = 1; i <= n + 1 - k; i++) res.add(Arrays.asList(i)); + //第一层循环,从 2 到 k + for (int i = 2; i <= k; i++) { + List> tmp = new ArrayList>(); + //第二层循环,遍历之前所有的结果 + for (List list : res) { + //第三次循环,对每个结果进行扩展 + //从最后一个元素加 1 开始,然后不是到 n ,而是和解法一的优化一样 + //(k - (i - 1) 代表当前已经有的个数,最后再加 1 是因为取了 n + for (int m = list.get(list.size() - 1) + 1; m <= n - (k - (i - 1)) + 1; m++) { + List newList = new ArrayList(list); + newList.add(m); + tmp.add(newList); + } + } + res = tmp; + } + return res; +} +``` + +# 解法四 递归 + +参考[这里]()。基于这个公式 C ( n, k ) = C ( n - 1, k - 1) + C ( n - 1, k ) 所用的思想,这个思想之前刷题也用过,但忘记是哪道了。 + +从 n 个数字选 k 个,我们把所有结果分为两种,包含第 n 个数和不包含第 n 个数。这样的话,就可以把问题转换成 + +* 从 n - 1 里边选 k - 1 个,然后每个结果加上 n +* 从 n - 1 个里边直接选 k 个。 + +把上边两个的结果合起来就可以了。 + + +```java +public List> combine(int n, int k) { + if (k == n || k == 0) { + List row = new LinkedList<>(); + for (int i = 1; i <= k; ++i) { + row.add(i); + } + return new LinkedList<>(Arrays.asList(row)); + } + // n - 1 里边选 k - 1 个 + List> result = combine(n - 1, k - 1); + //每个结果加上 n + result.forEach(e -> e.add(n)); + //把 n - 1 个选 k 个的结果也加入 + result.addAll(combine(n - 1, k)); + return result; +} +``` + +# 解法五 动态规划 + +参考[这里](),既然有了解法四的递归,那么一定可以有动态规划。递归就是压栈压栈压栈,然后到了递归出口,开始出栈出栈出栈。而动态规划一个好处就是省略了出栈的过程,我们直接从递归出口网上走。 + +```java +public List> combine(int n, int k) { + List>[][] dp = new List[n + 1][k + 1]; + //更新 k = 0 的所有情况 + for (int i = 0; i <= n; i++) { + dp[i][0] = new ArrayList<>(); + dp[i][0].add(new ArrayList()); + } + // i 从 1 到 n + for (int i = 1; i <= n; i++) { + // j 从 1 到 i 或者 k + for (int j = 1; j <= i && j <= k; j++) { + dp[i][j] = new ArrayList<>(); + //判断是否可以从 i - 1 里边选 j 个 + if (i > j){ + dp[i][j].addAll(dp[i - 1][j]); + } + //把 i - 1 里边选 j - 1 个的每个结果加上 i + for (List list: dp[i - 1][j - 1]) { + List tmpList = new ArrayList<>(list); + tmpList.add(i); + dp[i][j].add(tmpList); + } + } + } + return dp[n][k]; +} +``` + +这里遇到个神奇的问题,提一下,开始的的时候,最里边的 for 循环是这样写的 + +```java +for (List list: dp[i - 1][j - 1]) { + List tmpList = new LinkedList<>(list); + tmpList.add(i); + dp[i][j].add(tmpList); +} +``` + +就是 List 用的 Linked,而不是 Array,看起来没什么大问题,在 leetcode 上竟然报了超时。看了下 java 的源码。 + +```java +//ArrayList +public boolean add(E e) { + ensureCapacityInternal(size + 1); // Increments modCount!! + elementData[size++] = e; + return true; +} +//LinkedList +public boolean add(E e) { + linkLast(e); + return true; +} +void linkLast(E e) { + final Node l = last; + final Node newNode = new Node<>(l, e, null); + last = newNode; + if (l == null) + first = newNode; + else + l.next = newNode; + size++; + modCount++; +} +``` + +猜测原因可能是因为 linked 每次 add 的时候,都需要 new 一个节点对象,而我们进行了很多次 add,所以这里造成了时间的耗费,导致了超时。所以刷题的时候还是优先用 ArrayList 吧。 + +接下来就是动态规划的常规操作了,空间复杂度的优化,我们注意到更新 dp [ i \] \[ \* \] 的时候,只用到dp [ i - 1 \] \[ \* \] 的情况,所以我们可以只用一个一维数组就够了。和[72题]()解法二,以及[5题](),[10题](),[53题](> combine(int n, int k) { + List>[] dp = new ArrayList[k + 1]; + // i 从 1 到 n + dp[0] = new ArrayList<>(); + dp[0].add(new ArrayList()); + for (int i = 1; i <= n; i++) { + // j 从 1 到 i 或者 k + List> temp = new ArrayList<>(dp[0]); + for (int j = 1; j <= i && j <= k; j++) { + List> last = temp; + if(dp[j]!=null){ + temp = new ArrayList<>(dp[j]); + } + // 判断是否可以从 i - 1 里边选 j 个 + if (i <= j) { + dp[j] = new ArrayList<>(); + } + // 把 i - 1 里边选 j - 1 个的每个结果加上 i + for (List list : last) { + List tmpList = new ArrayList<>(list); + tmpList.add(i); + dp[j].add(tmpList); + } + } + } + return dp[k]; +} +``` + +# 总 + 开始的时候直接用了动态规划,然后翻了一些 Discuss 感觉发现了新世界,把目前为止常用的思路都用到了,回溯,递归,迭代,动态规划,这道题也太经典了!值得细细回味。 \ No newline at end of file diff --git a/leetCode-78-Subsets.md b/leetCode-78-Subsets.md index 07882c582..deae262e7 100644 --- a/leetCode-78-Subsets.md +++ b/leetCode-78-Subsets.md @@ -1,142 +1,142 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/78.jpg) - -给一个数组,输出这个数组的所有子数组。 - -# 解法一 迭代一 - -和 [77 题]()解法三一个思想,想找出数组长度 1 的所有解,然后再在长度为 1 的所有解上加 1 个数字变成长度为 2 的所有解,同样的直到 n。 - -假如 nums = [ 1, 2, 3 ],参照下图。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/78_2.jpg) - -```java -public List> subsets(int[] nums) { - List> res = new ArrayList>(); - List> ans = new ArrayList>(); - ans.add(new ArrayList()); - res.add(new ArrayList()); - int n = nums.length; - // 第一层循环,子数组长度从 1 到 n - for (int i = 1; i <= n; i++) { - // 第二层循环,遍历上次的所有结果 - List> tmp = new ArrayList>(); - for (List list : res) { - // 第三次循环,对每个结果进行扩展 - for (int m = 0; m < n; m++) { - //只添加比末尾数字大的数字,防止重复 - if (list.size() > 0 && list.get(list.size() - 1) >= nums[m]) - continue; - List newList = new ArrayList(list); - newList.add(nums[m]); - tmp.add(newList); - ans.add(newList); - } - } - res = tmp; - } - return ans; -} - -``` - -# 解法二 迭代法2 - -参照[这里]()。解法一的迭代法,是直接从结果上进行分类,将子数组的长度分为长度是 1 的,2 的 .... n 的。我们还可以从条件上入手,先只考虑给定数组的 1 个元素的所有子数组,然后再考虑数组的 2 个元素的所有子数组 ... 最后再考虑数组的 n 个元素的所有子数组。求 k 个元素的所有子数组,只需要在 k - 1 个元素的所有子数组里边加上 nums [ k ] 即可。 - -例如 nums [1 , 2, 3] 的遍历过程。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/78_3.jpg) - -```java -public List> subsets(int[] nums) { - List> ans = new ArrayList<>(); - ans.add(new ArrayList<>());//初始化空数组 - for(int i = 0;i> ans_tmp = new ArrayList<>(); - //遍历之前的所有结果 - for(List list : ans){ - List tmp = new ArrayList<>(list); - tmp.add(nums[i]); //加入新增数字 - ans_tmp.add(tmp); - } - ans.addAll(ans_tmp); - } - return ans; -} -``` - -# 解法三 回溯法 - -参考[这里]()。同样是很经典的回溯法例子,添加一个数,递归,删除之前的数,下次循环。 - -```java -public List> subsets(int[] nums) { - List> ans = new ArrayList<>(); - getAns(nums, 0, new ArrayList<>(), ans); - return ans; -} - -private void getAns(int[] nums, int start, ArrayList temp, List> ans) { - ans.add(new ArrayList<>(temp)); - for (int i = start; i < nums.length; i++) { - temp.add(nums[i]); - getAns(nums, i + 1, temp, ans); - temp.remove(temp.size() - 1); - } -} -``` - -# 解法四 位操作 - -前方高能!!!!这个方法真的是太太太牛了。参考[这里]()。 - -数组的每个元素,可以有两个状态,**在**子数组中和**不在**子数组中,所有状态的组合就是所有子数组了。 - -例如,nums = [ 1, 2 , 3 ]。1 代表在,0 代表不在。 - -```java -1 2 3 -0 0 0 -> [ ] -0 0 1 -> [ 3] -0 1 0 -> [ 2 ] -0 1 1 -> [ 2 3] -1 0 0 -> [1 ] -1 0 1 -> [1 3] -1 1 0 -> [1 2 ] -1 1 1 -> [1 2 3] -``` - -所以我们只需要遍历 0 0 0 到 1 1 1,也就是 0 到 7,然后判断每个比特位是否是 1,是 1 的话将对应数字加入即可。如果数组长度是 n,那么每个比特位是 2 个状态,所有总共就是 2 的 n 次方个子数组。遍历 00 ... 0 到 11 ... 1 即可。 - -```java -public List> subsets(int[] nums) { - List> ans = new ArrayList<>(); - int bit_nums = nums.length; - int ans_nums = 1 << bit_nums; //执行 2 的 n 次方 - for (int i = 0; i < ans_nums; i++) { - List tmp = new ArrayList<>(); - int count = 0; //记录当前对应数组的哪一位 - int i_copy = i; //用来移位 - while (i_copy != 0) { - if ((i_copy & 1) == 1) { //判断当前位是否是 1 - tmp.add(nums[count]); - } - count++; - i_copy = i_copy >> 1;//右移一位 - } - ans.add(tmp); - - } - return ans; -} -``` - -# 总 - -同样是很经典的一道题,回溯,迭代,最后的位操作真的是太强了,每次遇到关于位操作的解法就很惊叹。 - - - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/78.jpg) + +给一个数组,输出这个数组的所有子数组。 + +# 解法一 迭代一 + +和 [77 题]()解法三一个思想,想找出数组长度 1 的所有解,然后再在长度为 1 的所有解上加 1 个数字变成长度为 2 的所有解,同样的直到 n。 + +假如 nums = [ 1, 2, 3 ],参照下图。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/78_2.jpg) + +```java +public List> subsets(int[] nums) { + List> res = new ArrayList>(); + List> ans = new ArrayList>(); + ans.add(new ArrayList()); + res.add(new ArrayList()); + int n = nums.length; + // 第一层循环,子数组长度从 1 到 n + for (int i = 1; i <= n; i++) { + // 第二层循环,遍历上次的所有结果 + List> tmp = new ArrayList>(); + for (List list : res) { + // 第三次循环,对每个结果进行扩展 + for (int m = 0; m < n; m++) { + //只添加比末尾数字大的数字,防止重复 + if (list.size() > 0 && list.get(list.size() - 1) >= nums[m]) + continue; + List newList = new ArrayList(list); + newList.add(nums[m]); + tmp.add(newList); + ans.add(newList); + } + } + res = tmp; + } + return ans; +} + +``` + +# 解法二 迭代法2 + +参照[这里]()。解法一的迭代法,是直接从结果上进行分类,将子数组的长度分为长度是 1 的,2 的 .... n 的。我们还可以从条件上入手,先只考虑给定数组的 1 个元素的所有子数组,然后再考虑数组的 2 个元素的所有子数组 ... 最后再考虑数组的 n 个元素的所有子数组。求 k 个元素的所有子数组,只需要在 k - 1 个元素的所有子数组里边加上 nums [ k ] 即可。 + +例如 nums [1 , 2, 3] 的遍历过程。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/78_3.jpg) + +```java +public List> subsets(int[] nums) { + List> ans = new ArrayList<>(); + ans.add(new ArrayList<>());//初始化空数组 + for(int i = 0;i> ans_tmp = new ArrayList<>(); + //遍历之前的所有结果 + for(List list : ans){ + List tmp = new ArrayList<>(list); + tmp.add(nums[i]); //加入新增数字 + ans_tmp.add(tmp); + } + ans.addAll(ans_tmp); + } + return ans; +} +``` + +# 解法三 回溯法 + +参考[这里]()。同样是很经典的回溯法例子,添加一个数,递归,删除之前的数,下次循环。 + +```java +public List> subsets(int[] nums) { + List> ans = new ArrayList<>(); + getAns(nums, 0, new ArrayList<>(), ans); + return ans; +} + +private void getAns(int[] nums, int start, ArrayList temp, List> ans) { + ans.add(new ArrayList<>(temp)); + for (int i = start; i < nums.length; i++) { + temp.add(nums[i]); + getAns(nums, i + 1, temp, ans); + temp.remove(temp.size() - 1); + } +} +``` + +# 解法四 位操作 + +前方高能!!!!这个方法真的是太太太牛了。参考[这里]()。 + +数组的每个元素,可以有两个状态,**在**子数组中和**不在**子数组中,所有状态的组合就是所有子数组了。 + +例如,nums = [ 1, 2 , 3 ]。1 代表在,0 代表不在。 + +```java +1 2 3 +0 0 0 -> [ ] +0 0 1 -> [ 3] +0 1 0 -> [ 2 ] +0 1 1 -> [ 2 3] +1 0 0 -> [1 ] +1 0 1 -> [1 3] +1 1 0 -> [1 2 ] +1 1 1 -> [1 2 3] +``` + +所以我们只需要遍历 0 0 0 到 1 1 1,也就是 0 到 7,然后判断每个比特位是否是 1,是 1 的话将对应数字加入即可。如果数组长度是 n,那么每个比特位是 2 个状态,所有总共就是 2 的 n 次方个子数组。遍历 00 ... 0 到 11 ... 1 即可。 + +```java +public List> subsets(int[] nums) { + List> ans = new ArrayList<>(); + int bit_nums = nums.length; + int ans_nums = 1 << bit_nums; //执行 2 的 n 次方 + for (int i = 0; i < ans_nums; i++) { + List tmp = new ArrayList<>(); + int count = 0; //记录当前对应数组的哪一位 + int i_copy = i; //用来移位 + while (i_copy != 0) { + if ((i_copy & 1) == 1) { //判断当前位是否是 1 + tmp.add(nums[count]); + } + count++; + i_copy = i_copy >> 1;//右移一位 + } + ans.add(tmp); + + } + return ans; +} +``` + +# 总 + +同样是很经典的一道题,回溯,迭代,最后的位操作真的是太强了,每次遇到关于位操作的解法就很惊叹。 + + + diff --git a/leetCode-79-Word-Search.md b/leetCode-79-Word-Search.md index 2f4a358a7..a9c8bee0d 100644 --- a/leetCode-79-Word-Search.md +++ b/leetCode-79-Word-Search.md @@ -1,162 +1,162 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/79.jpg) - -意思就是从某个字符出发,然后它可以向左向右向上向下移动,走过的路径构成一个字符串,判断是否能走出给定字符串的 word ,还有一个条件就是走过的字符不能够走第二次。 - -比如 SEE,从第二行最后一列的 S 出发,向下移动,再向左移动,就走出了 SEE。 - -ABCB,从第一行第一列的 A 出发,向右移动,再向右移动,到达 C 以后,不能向左移动回到 B ,并且也没有其他的路径走出 ABCB 所以返回 false。 - -# 解法一 DFS - -我们可以把矩阵看做一个图,然后利用图的深度优先遍历 DFS 的思想就可以了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/79_2.jpg) - -我们需要做的就是,在深度优先遍历过程中,判断当前遍历元素是否对应 word 元素,如果不匹配就结束当前的遍历,返回上一次的元素,尝试其他路径。当然,和普通的 dfs 一样,我们需要一个 visited 数组标记元素是否访问过。 - -```java -public boolean exist(char[][] board, String word) { - int rows = board.length; - if (rows == 0) { - return false; - } - int cols = board[0].length; - boolean[][] visited = new boolean[rows][cols]; - //从不同位置开始 - for (int i = 0; i < rows; i++) { - for (int j = 0; j < cols; j++) { - //从当前位置开始符合就返回 true - if (existRecursive(board, i, j, word, 0, visited)) { - return true; - } - } - } - return false; -} - -private boolean existRecursive(char[][] board, int row, int col, String word, int index, boolean[][] visited) { - //判断是否越界 - if (row < 0 || row >= board.length || col < 0 || col >= board[0].length) { - return false; - } - //当前元素访问过或者和当前 word 不匹配返回 false - if (visited[row][col] || board[row][col] != word.charAt(index)) { - return false; - } - //已经匹配到了最后一个字母,返回 true - if (index == word.length() - 1) { - return true; - } - //将当前位置标记位已访问 - visited[row][col] = true; - //对四个位置分别进行尝试 - boolean up = existRecursive(board, row - 1, col, word, index + 1, visited); - if (up) { - return true; - } - boolean down = existRecursive(board, row + 1, col, word, index + 1, visited); - if (down) { - return true; - } - boolean left = existRecursive(board, row, col - 1, word, index + 1, visited); - if (left) { - return true; - } - boolean right = existRecursive(board, row, col + 1, word, index + 1, visited); - if (right) { - return true; - } - //当前位置没有选进来,恢复标记为 false - visited[row][col] = false; - return false; -} -``` - -我们可以优化一下空间复杂度,我们之前是用了一个等大的二维数组来标记是否访问过。其实我们完全可以用之前的 board,我们把当前访问的元素置为 "$" ,也就是一个在 board 中不会出现的字符。然后当上下左右全部尝试完之后,我们再把当前元素还原就可以了。 - -```java -public boolean exist(char[][] board, String word) { - int rows = board.length; - if (rows == 0) { - return false; - } - int cols = board[0].length; - for (int i = 0; i < rows; i++) { - for (int j = 0; j < cols; j++) { - if (existRecursive(board, i, j, word, 0)) { - return true; - } - } - } - return false; -} - -private boolean existRecursive(char[][] board, int row, int col, String word, int index) { - if (row < 0 || row >= board.length || col < 0 || col >= board[0].length) { - return false; - } - if (board[row][col] != word.charAt(index)) { - return false; - } - if (index == word.length() - 1) { - return true; - } - /*********************改变的地方****************************************/ - char temp = board[row][col]; - board[row][col] = '$'; - /*********************************************************************/ - boolean up = existRecursive(board, row - 1, col, word, index + 1); - if (up) { - return true; - } - boolean down = existRecursive(board, row + 1, col, word, index + 1); - if (down) { - return true; - } - boolean left = existRecursive(board, row, col - 1, word, index + 1); - if (left) { - return true; - } - boolean right = existRecursive(board, row, col + 1, word, index + 1); - if (right) { - return true; - } - /*********************改变的地方****************************************/ - board[row][col] = temp; - /*********************************************************************/ - return false; -} -``` - -在[这里](),看到另外一种标记和还原的方法。异或。 - -```java -/*********************之前的做法****************************************/ -char temp = board[row][col]; -board[row][col] = '$'; -/*********************************************************************/ - -/*********************利用异或****************************************/ -board[row][col] ^= 128; -/*********************************************************************/ - -//还原 -/********************之前的做法****************************************/ -board[row][col] = temp; -/*********************************************************************/ - -/*********************利用异或****************************************/ -board[row][col] ^= 128; -/*********************************************************************/ - -``` - -至于原理,因为 ASCII 码值的范围是 0 - 127,二进制的话就是 0000 0000 - 0111 1111,我们把它和 128 做异或,也就是和 1000 0000 。这样,如果想还原原来的数字只需要再异或 128 就可以了。 - -其实原理是一样的,都是把之前的数字变成当前 board 不存在的字符,然后再变回来。只不过这里考虑它的二进制编码,在保留原有信息的基础上做改变,不再需要 temp 变量。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/79.jpg) + +意思就是从某个字符出发,然后它可以向左向右向上向下移动,走过的路径构成一个字符串,判断是否能走出给定字符串的 word ,还有一个条件就是走过的字符不能够走第二次。 + +比如 SEE,从第二行最后一列的 S 出发,向下移动,再向左移动,就走出了 SEE。 + +ABCB,从第一行第一列的 A 出发,向右移动,再向右移动,到达 C 以后,不能向左移动回到 B ,并且也没有其他的路径走出 ABCB 所以返回 false。 + +# 解法一 DFS + +我们可以把矩阵看做一个图,然后利用图的深度优先遍历 DFS 的思想就可以了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/79_2.jpg) + +我们需要做的就是,在深度优先遍历过程中,判断当前遍历元素是否对应 word 元素,如果不匹配就结束当前的遍历,返回上一次的元素,尝试其他路径。当然,和普通的 dfs 一样,我们需要一个 visited 数组标记元素是否访问过。 + +```java +public boolean exist(char[][] board, String word) { + int rows = board.length; + if (rows == 0) { + return false; + } + int cols = board[0].length; + boolean[][] visited = new boolean[rows][cols]; + //从不同位置开始 + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + //从当前位置开始符合就返回 true + if (existRecursive(board, i, j, word, 0, visited)) { + return true; + } + } + } + return false; +} + +private boolean existRecursive(char[][] board, int row, int col, String word, int index, boolean[][] visited) { + //判断是否越界 + if (row < 0 || row >= board.length || col < 0 || col >= board[0].length) { + return false; + } + //当前元素访问过或者和当前 word 不匹配返回 false + if (visited[row][col] || board[row][col] != word.charAt(index)) { + return false; + } + //已经匹配到了最后一个字母,返回 true + if (index == word.length() - 1) { + return true; + } + //将当前位置标记位已访问 + visited[row][col] = true; + //对四个位置分别进行尝试 + boolean up = existRecursive(board, row - 1, col, word, index + 1, visited); + if (up) { + return true; + } + boolean down = existRecursive(board, row + 1, col, word, index + 1, visited); + if (down) { + return true; + } + boolean left = existRecursive(board, row, col - 1, word, index + 1, visited); + if (left) { + return true; + } + boolean right = existRecursive(board, row, col + 1, word, index + 1, visited); + if (right) { + return true; + } + //当前位置没有选进来,恢复标记为 false + visited[row][col] = false; + return false; +} +``` + +我们可以优化一下空间复杂度,我们之前是用了一个等大的二维数组来标记是否访问过。其实我们完全可以用之前的 board,我们把当前访问的元素置为 "$" ,也就是一个在 board 中不会出现的字符。然后当上下左右全部尝试完之后,我们再把当前元素还原就可以了。 + +```java +public boolean exist(char[][] board, String word) { + int rows = board.length; + if (rows == 0) { + return false; + } + int cols = board[0].length; + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + if (existRecursive(board, i, j, word, 0)) { + return true; + } + } + } + return false; +} + +private boolean existRecursive(char[][] board, int row, int col, String word, int index) { + if (row < 0 || row >= board.length || col < 0 || col >= board[0].length) { + return false; + } + if (board[row][col] != word.charAt(index)) { + return false; + } + if (index == word.length() - 1) { + return true; + } + /*********************改变的地方****************************************/ + char temp = board[row][col]; + board[row][col] = '$'; + /*********************************************************************/ + boolean up = existRecursive(board, row - 1, col, word, index + 1); + if (up) { + return true; + } + boolean down = existRecursive(board, row + 1, col, word, index + 1); + if (down) { + return true; + } + boolean left = existRecursive(board, row, col - 1, word, index + 1); + if (left) { + return true; + } + boolean right = existRecursive(board, row, col + 1, word, index + 1); + if (right) { + return true; + } + /*********************改变的地方****************************************/ + board[row][col] = temp; + /*********************************************************************/ + return false; +} +``` + +在[这里](),看到另外一种标记和还原的方法。异或。 + +```java +/*********************之前的做法****************************************/ +char temp = board[row][col]; +board[row][col] = '$'; +/*********************************************************************/ + +/*********************利用异或****************************************/ +board[row][col] ^= 128; +/*********************************************************************/ + +//还原 +/********************之前的做法****************************************/ +board[row][col] = temp; +/*********************************************************************/ + +/*********************利用异或****************************************/ +board[row][col] ^= 128; +/*********************************************************************/ + +``` + +至于原理,因为 ASCII 码值的范围是 0 - 127,二进制的话就是 0000 0000 - 0111 1111,我们把它和 128 做异或,也就是和 1000 0000 。这样,如果想还原原来的数字只需要再异或 128 就可以了。 + +其实原理是一样的,都是把之前的数字变成当前 board 不存在的字符,然后再变回来。只不过这里考虑它的二进制编码,在保留原有信息的基础上做改变,不再需要 temp 变量。 + +# 总 + 关键是对题目的理解,抽象到 DFS,题目就迎刃而解了。异或的应用很强。 \ No newline at end of file diff --git a/leetCode-8-String-to-Integer.md b/leetCode-8-String-to-Integer.md index 74f597637..ff31e0a6d 100644 --- a/leetCode-8-String-to-Integer.md +++ b/leetCode-8-String-to-Integer.md @@ -1,61 +1,61 @@ -# 题目描述(中等难度) - -![](http://windliang.oss-cn-beijing.aliyuncs.com/8_atoi.png) - -将一个字符串转为整型。 - -这道题,难度其实不大,和[上道题](http://windliang.cc/2018/08/13/leetCode-7-Reverse-Integer/)有很多重合的地方。整体的思路就是遍历字符串,然后依次取出一个字符就可以了。无非是考虑一些特殊情况,还有就是理解题目意思。 - -经过多次试错,题目的意思是这样的。 - -从左遍历字符串,可以遇到空格,直到遇到 ' + ' 或者数字或者 ' - ' 就表示要转换的数字开始,如果之后遇到除了数字的其他字符(包括空格)就结束遍历,输出结果,不管后边有没有数字了,例如 " - 32332ada2323" 就输出 "- 32332"。 - -如果遇到空格或者 ' + ' 或者数字或者 ' - ' 之前遇到了其他字符,就直接输出 0 ,例如 " we1332"。 - -如果转换的数字超出了 int ,就返回 intMax 或者 intMin。 - -```java -public int myAtoi(String str) { - int sign = 1; - int ans = 0, pop = 0; - boolean hasSign = false; //代表是否开始转换数字 - for (int i = 0; i < str.length(); i++) { - if (str.charAt(i) == '-' && !hasSign) { - sign = -1; - hasSign = true; - continue; - } - if (str.charAt(i) == '+' && !hasSign) { - sign = 1; - hasSign = true; - continue; - } - if (str.charAt(i) == ' ' && !hasSign) { - continue; - } - - if (str.charAt(i) >= '0' && str.charAt(i) <= '9') { - hasSign = true; - pop = str.charAt(i) - '0'; - //和上道题判断出界一个意思只不过记得乘上 sign 。 - if (ans * sign > Integer.MAX_VALUE / 10 || (ans * sign == Integer.MAX_VALUE / 10 && pop * sign > 7)) - return 2147483647; - if (ans * sign < Integer.MIN_VALUE / 10 || (ans * sign == Integer.MIN_VALUE / 10 && pop * sign < -8)) - return -2147483648; - ans = ans * 10 + pop; - } else { - return ans * sign; - } - } - return ans * sign; - } -``` - -时间复杂度:O(n),n 是字符串的长度。 - -空间复杂度:O(1)。 - -# 总结 - -这道题让自己有点感到莫名其妙,好像没有 get 到出题人的点??? - +# 题目描述(中等难度) + +![](http://windliang.oss-cn-beijing.aliyuncs.com/8_atoi.png) + +将一个字符串转为整型。 + +这道题,难度其实不大,和[上道题](http://windliang.cc/2018/08/13/leetCode-7-Reverse-Integer/)有很多重合的地方。整体的思路就是遍历字符串,然后依次取出一个字符就可以了。无非是考虑一些特殊情况,还有就是理解题目意思。 + +经过多次试错,题目的意思是这样的。 + +从左遍历字符串,可以遇到空格,直到遇到 ' + ' 或者数字或者 ' - ' 就表示要转换的数字开始,如果之后遇到除了数字的其他字符(包括空格)就结束遍历,输出结果,不管后边有没有数字了,例如 " - 32332ada2323" 就输出 "- 32332"。 + +如果遇到空格或者 ' + ' 或者数字或者 ' - ' 之前遇到了其他字符,就直接输出 0 ,例如 " we1332"。 + +如果转换的数字超出了 int ,就返回 intMax 或者 intMin。 + +```java +public int myAtoi(String str) { + int sign = 1; + int ans = 0, pop = 0; + boolean hasSign = false; //代表是否开始转换数字 + for (int i = 0; i < str.length(); i++) { + if (str.charAt(i) == '-' && !hasSign) { + sign = -1; + hasSign = true; + continue; + } + if (str.charAt(i) == '+' && !hasSign) { + sign = 1; + hasSign = true; + continue; + } + if (str.charAt(i) == ' ' && !hasSign) { + continue; + } + + if (str.charAt(i) >= '0' && str.charAt(i) <= '9') { + hasSign = true; + pop = str.charAt(i) - '0'; + //和上道题判断出界一个意思只不过记得乘上 sign 。 + if (ans * sign > Integer.MAX_VALUE / 10 || (ans * sign == Integer.MAX_VALUE / 10 && pop * sign > 7)) + return 2147483647; + if (ans * sign < Integer.MIN_VALUE / 10 || (ans * sign == Integer.MIN_VALUE / 10 && pop * sign < -8)) + return -2147483648; + ans = ans * 10 + pop; + } else { + return ans * sign; + } + } + return ans * sign; + } +``` + +时间复杂度:O(n),n 是字符串的长度。 + +空间复杂度:O(1)。 + +# 总结 + +这道题让自己有点感到莫名其妙,好像没有 get 到出题人的点??? + diff --git a/leetCode-80-Remove-Duplicates-from-Sorted-ArrayII.md b/leetCode-80-Remove-Duplicates-from-Sorted-ArrayII.md index fef7d7270..f5286a668 100644 --- a/leetCode-80-Remove-Duplicates-from-Sorted-ArrayII.md +++ b/leetCode-80-Remove-Duplicates-from-Sorted-ArrayII.md @@ -1,79 +1,79 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/80.jpg) - -[26 题]()的升级版,给定一个数组,每个数字只允许出现 2 次,将满足条件的数字全部移到前边,并且返回有多少数字。例如 [ 1, 1, 1, 2, 2, 3, 4, 4, 4, 4 ],要变为 [ 1, 1, 2, 2, 3, 4, 4 ...] 剩余部分的数字不要求。 - -# 解法一 快慢指针 - -利用[26 题]()的思想,慢指针指向满足条件的数字的末尾,快指针遍历原数组。并且用一个变量记录当前末尾数字出现了几次,防止超过两次。 - -```java -public int removeDuplicates(int[] nums) { - int slow = 0; - int fast = 1; - int count = 1; - int max = 2; - for (; fast < nums.length; fast++) { - //当前遍历的数字和慢指针末尾数字不同,就加到慢指针的末尾 - if (nums[fast] != nums[slow]) { - slow++; - nums[slow] = nums[fast]; - count = 1; //当前数字置为 1 个 - //和末尾数字相同,考虑当前数字的个数,小于 2 的话,就加到慢指针的末尾 - } else { - if (count < max) { - slow++; - nums[slow] = nums[fast]; - count++; //当前数字加 1 - } - } - } - return slow + 1; -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(1)。 - -# 解法二 - -参考[这里](),解法一中,我们用一个变量 count 记录了末尾数字出现了几次。而由于给定的数组是有序的,我们可以更直接。将当前快指针遍历的数字和慢指针指向的数字的前一个数字比较(也就是满足条件的倒数第 2 个数),如果相等,因为有序,所有倒数第 1 个数字和倒数第 2 个数字都等于当前数字,再添加就超过 2 个了,所有不添加,如果不相等,那么就添加。s 代表 slow,f 代表 fast。 - -```java -//相等的情况 -1 1 1 1 1 2 2 3 - ^ ^ - s f -//不相等的情况 -1 1 1 1 1 2 2 3 - ^ ^ - s f -``` - - - -```java -public int removeDuplicates2(int[] nums) { - int slow = 1; - int fast = 2; - int max = 2; - for (; fast < nums.length; fast++) { - //不相等的话就添加 - if (nums[fast] != nums[slow - max + 1]) { - slow++; - nums[slow] = nums[fast]; - } - } - return slow + 1; -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(1)。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/80.jpg) + +[26 题]()的升级版,给定一个数组,每个数字只允许出现 2 次,将满足条件的数字全部移到前边,并且返回有多少数字。例如 [ 1, 1, 1, 2, 2, 3, 4, 4, 4, 4 ],要变为 [ 1, 1, 2, 2, 3, 4, 4 ...] 剩余部分的数字不要求。 + +# 解法一 快慢指针 + +利用[26 题]()的思想,慢指针指向满足条件的数字的末尾,快指针遍历原数组。并且用一个变量记录当前末尾数字出现了几次,防止超过两次。 + +```java +public int removeDuplicates(int[] nums) { + int slow = 0; + int fast = 1; + int count = 1; + int max = 2; + for (; fast < nums.length; fast++) { + //当前遍历的数字和慢指针末尾数字不同,就加到慢指针的末尾 + if (nums[fast] != nums[slow]) { + slow++; + nums[slow] = nums[fast]; + count = 1; //当前数字置为 1 个 + //和末尾数字相同,考虑当前数字的个数,小于 2 的话,就加到慢指针的末尾 + } else { + if (count < max) { + slow++; + nums[slow] = nums[fast]; + count++; //当前数字加 1 + } + } + } + return slow + 1; +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(1)。 + +# 解法二 + +参考[这里](),解法一中,我们用一个变量 count 记录了末尾数字出现了几次。而由于给定的数组是有序的,我们可以更直接。将当前快指针遍历的数字和慢指针指向的数字的前一个数字比较(也就是满足条件的倒数第 2 个数),如果相等,因为有序,所有倒数第 1 个数字和倒数第 2 个数字都等于当前数字,再添加就超过 2 个了,所有不添加,如果不相等,那么就添加。s 代表 slow,f 代表 fast。 + +```java +//相等的情况 +1 1 1 1 1 2 2 3 + ^ ^ + s f +//不相等的情况 +1 1 1 1 1 2 2 3 + ^ ^ + s f +``` + + + +```java +public int removeDuplicates2(int[] nums) { + int slow = 1; + int fast = 2; + int max = 2; + for (; fast < nums.length; fast++) { + //不相等的话就添加 + if (nums[fast] != nums[slow - max + 1]) { + slow++; + nums[slow] = nums[fast]; + } + } + return slow + 1; +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(1)。 + +# 总 + 快慢指针是个好东西,解法二直接利用有序,和倒数第 n 个比,从而保证末尾的数字不超过 n 个,太强了。 \ No newline at end of file diff --git a/leetCode-81-Search-in-Rotated-Sorted-ArrayII.md b/leetCode-81-Search-in-Rotated-Sorted-ArrayII.md index 203fbd573..ebf96fbe5 100644 --- a/leetCode-81-Search-in-Rotated-Sorted-ArrayII.md +++ b/leetCode-81-Search-in-Rotated-Sorted-ArrayII.md @@ -1,90 +1,90 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/81.jpg) - -[33题]()的升级版,数组的操作没有变,所谓的旋转数组,就是把有序数组前边若干个数字移动到末尾。区别在于这道题出现了重复的数字,同样是找 target。 - -# 解法一 - -把数组遍历一遍,然后依次判断数字是否相等的解法,当然就不用说了。这里直接在[33 题]()解法三的基础上去修改。33 题算法基于一个事实,数组从任意位置劈开后,至少有一半是有序的,什么意思呢? - -比如 [ 4 5 6 7 1 2 3] ,从 7 劈开,左边是 [ 4 5 6 7] 右边是 [ 7 1 2 3],左边是有序的。 - -基于这个事实。 - -我们可以先找到哪一段是有序的 (只要判断端点即可),知道了哪一段有序,我们只需要用正常的二分法就够了,只需要看 target 在不在这一段里,如果在,那么就把另一半丢弃。如果不在,那么就把这一段丢弃。 - -```java -public int search(int[] nums, int target) { - int start = 0; - int end = nums.length - 1; - while (start <= end) { - int mid = (start + end) / 2; - if (target == nums[mid]) { - return mid; - } - //左半段是有序的 - if (nums[start] <= nums[mid]) { - //target 在这段里 - if (target >= nums[start] && target < nums[mid]) { - end = mid - 1; - //target 在另一段里 - } else { - start = mid + 1; - } - //右半段是有序的 - } else { - if (target > nums[mid] && target <= nums[end]) { - start = mid + 1; - } else { - end = mid - 1; - } - } - - } - return -1; -} -``` - -如果不加修改,直接放到 leetcode 上跑,发现 nums = [ 1, 3, 1, 1, 1 ] ,target = 3,返回了 false,当然是不对的了。原因就出现在了,我们在判断哪段有序的时候,当 nums [ start ] <= nums [ mid ] 是认为左半段有序。而由于这道题出现了重复数字,此时的 nums [ start ] = 1, nums [ mid ] = 1,但此时左半段 [ 1, 3, 1 ] 并不是有序的,所以造成我们的算法错误。 - -所以 nums[start] == nums[mid] 需要我们单独考虑了。操作也很简单,参考[这里](= nums[start] && target < nums[mid]) { - end = mid - 1; - } else { - start = mid + 1; - } - } else if(nums[start] == nums[mid]){ - start++; - //右半段有序 - }else{ - if (target > nums[mid] && target <= nums[end]) { - start = mid + 1; - } else { - end = mid - 1; - } - } - } - return false; -} -``` - -时间复杂度:最好的情况,如果没有遇到 nums [ start ] == nums [ mid ],还是 O(log(n))。当然最差的情况,如果是类似于这种 [ 1, 1, 1, 1, 2, 1 ] ,target = 2,就是 O ( n ) 了。 - -空间复杂度:O ( 1 )。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/81.jpg) + +[33题]()的升级版,数组的操作没有变,所谓的旋转数组,就是把有序数组前边若干个数字移动到末尾。区别在于这道题出现了重复的数字,同样是找 target。 + +# 解法一 + +把数组遍历一遍,然后依次判断数字是否相等的解法,当然就不用说了。这里直接在[33 题]()解法三的基础上去修改。33 题算法基于一个事实,数组从任意位置劈开后,至少有一半是有序的,什么意思呢? + +比如 [ 4 5 6 7 1 2 3] ,从 7 劈开,左边是 [ 4 5 6 7] 右边是 [ 7 1 2 3],左边是有序的。 + +基于这个事实。 + +我们可以先找到哪一段是有序的 (只要判断端点即可),知道了哪一段有序,我们只需要用正常的二分法就够了,只需要看 target 在不在这一段里,如果在,那么就把另一半丢弃。如果不在,那么就把这一段丢弃。 + +```java +public int search(int[] nums, int target) { + int start = 0; + int end = nums.length - 1; + while (start <= end) { + int mid = (start + end) / 2; + if (target == nums[mid]) { + return mid; + } + //左半段是有序的 + if (nums[start] <= nums[mid]) { + //target 在这段里 + if (target >= nums[start] && target < nums[mid]) { + end = mid - 1; + //target 在另一段里 + } else { + start = mid + 1; + } + //右半段是有序的 + } else { + if (target > nums[mid] && target <= nums[end]) { + start = mid + 1; + } else { + end = mid - 1; + } + } + + } + return -1; +} +``` + +如果不加修改,直接放到 leetcode 上跑,发现 nums = [ 1, 3, 1, 1, 1 ] ,target = 3,返回了 false,当然是不对的了。原因就出现在了,我们在判断哪段有序的时候,当 nums [ start ] <= nums [ mid ] 是认为左半段有序。而由于这道题出现了重复数字,此时的 nums [ start ] = 1, nums [ mid ] = 1,但此时左半段 [ 1, 3, 1 ] 并不是有序的,所以造成我们的算法错误。 + +所以 nums[start] == nums[mid] 需要我们单独考虑了。操作也很简单,参考[这里](= nums[start] && target < nums[mid]) { + end = mid - 1; + } else { + start = mid + 1; + } + } else if(nums[start] == nums[mid]){ + start++; + //右半段有序 + }else{ + if (target > nums[mid] && target <= nums[end]) { + start = mid + 1; + } else { + end = mid - 1; + } + } + } + return false; +} +``` + +时间复杂度:最好的情况,如果没有遇到 nums [ start ] == nums [ mid ],还是 O(log(n))。当然最差的情况,如果是类似于这种 [ 1, 1, 1, 1, 2, 1 ] ,target = 2,就是 O ( n ) 了。 + +空间复杂度:O ( 1 )。 + +# 总 + 基于之前的算法,找出问题所在,然后思考解决方案。开始自己一直纠结于怎么保持时间复杂度还是 log ( n ),也没想出解决方案,看了 discuss,发现似乎只能权衡一下。另外 [33题]() 的另外两种解法,好像对于这道题完全失效了,如果大家发现怎么修改,欢迎和我交流。 \ No newline at end of file diff --git a/leetCode-82-Remove-Duplicates-from-Sorted-ListII.md b/leetCode-82-Remove-Duplicates-from-Sorted-ListII.md index 5ad5d65fb..f216b24f1 100644 --- a/leetCode-82-Remove-Duplicates-from-Sorted-ListII.md +++ b/leetCode-82-Remove-Duplicates-from-Sorted-ListII.md @@ -1,104 +1,104 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/82.jpg) - -给一个链表,如果一个数属于重复数字,就把这个数删除,一个都不留。 - -# 解法一 迭代 - -只需要两个指针,一个指针 pre 代表重复数字的前边的一个指针,另一个指针 cur 用来遍历链表。d 代表哨兵节点,用来简化边界条件,初始化为 head 指针的前一个节点。p 代表 pre,c 代表 cur。 - -```java -d 1 2 3 3 3 4 cur 和 cur.next 不相等,pre 移到 cur 的地方,cur后移 -^ ^ -p c - -d 1 2 3 3 3 4 cur 和 cur.next 不相等,pre 移到 cur 的地方,cur后移 - ^ ^ - p c - -d 1 2 3 3 3 4 5 cur 和 cur.next 相等, pre 保持不变,cur 后移 - ^ ^ - p c - -d 1 2 3 3 3 4 5 cur 和 cur.next 相等, pre 保持不变,cur 后移 - ^ ^ - p c - -d 1 2 3 3 3 4 5 cur 和 cur.next 不相等, pre.next 直接指向 cur.next, 删除所有 3,cur 后移 - ^ ^ - p c - -d 1 2 4 5 cur 和 cur.next 不相等,pre 移到 cur 的地方,cur后移 - ^ ^ - p c - -d 1 2 4 5 遍历结束 - ^ ^ - p c -``` - - - -```java -public ListNode deleteDuplicates(ListNode head) { - ListNode pre = new ListNode(0); - ListNode dummy = pre; - pre.next = head; - ListNode cur = head; - while(cur!=null && cur.next!=null){ - boolean equal = false; - //cur 和 cur.next 相等,cur 不停后移 - while(cur.next!=null && cur.val == cur.next.val){ - cur = cur.next; - equal = true; - } - //发生了相等的情况 - // pre.next 直接指向 cur.next 删除所有重复数字 - if(equal){ - pre.next = cur.next; - equal = false; - //没有发生相等的情况 - //pre 移到 cur 的地方 - }else{ - pre = cur; - } - //cur 后移 - cur = cur.next; - } - return dummy.next; -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(1)。 - -# 解法二 递归 - -[这里]()看到了一种递归的解法,分享一下。 主要分了两种情况,头结点和后边的节点相等,头结点和后边的节点不相等。 - -```java -public ListNode deleteDuplicates(ListNode head) { - if (head == null) return null; - //如果头结点和后边的节点相等 - if (head.next != null && head.val == head.next.val) { - //跳过所有重复数字 - while (head.next != null && head.val == head.next.val) { - head = head.next; - } - //将所有重复数字去掉后,进入递归 - return deleteDuplicates(head.next); - //头结点和后边的节点不相等 - } else { - //保留头结点,后边的所有节点进入递归 - head.next = deleteDuplicates(head.next); - } - //返回头结点 - return head; -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/82.jpg) + +给一个链表,如果一个数属于重复数字,就把这个数删除,一个都不留。 + +# 解法一 迭代 + +只需要两个指针,一个指针 pre 代表重复数字的前边的一个指针,另一个指针 cur 用来遍历链表。d 代表哨兵节点,用来简化边界条件,初始化为 head 指针的前一个节点。p 代表 pre,c 代表 cur。 + +```java +d 1 2 3 3 3 4 cur 和 cur.next 不相等,pre 移到 cur 的地方,cur后移 +^ ^ +p c + +d 1 2 3 3 3 4 cur 和 cur.next 不相等,pre 移到 cur 的地方,cur后移 + ^ ^ + p c + +d 1 2 3 3 3 4 5 cur 和 cur.next 相等, pre 保持不变,cur 后移 + ^ ^ + p c + +d 1 2 3 3 3 4 5 cur 和 cur.next 相等, pre 保持不变,cur 后移 + ^ ^ + p c + +d 1 2 3 3 3 4 5 cur 和 cur.next 不相等, pre.next 直接指向 cur.next, 删除所有 3,cur 后移 + ^ ^ + p c + +d 1 2 4 5 cur 和 cur.next 不相等,pre 移到 cur 的地方,cur后移 + ^ ^ + p c + +d 1 2 4 5 遍历结束 + ^ ^ + p c +``` + + + +```java +public ListNode deleteDuplicates(ListNode head) { + ListNode pre = new ListNode(0); + ListNode dummy = pre; + pre.next = head; + ListNode cur = head; + while(cur!=null && cur.next!=null){ + boolean equal = false; + //cur 和 cur.next 相等,cur 不停后移 + while(cur.next!=null && cur.val == cur.next.val){ + cur = cur.next; + equal = true; + } + //发生了相等的情况 + // pre.next 直接指向 cur.next 删除所有重复数字 + if(equal){ + pre.next = cur.next; + equal = false; + //没有发生相等的情况 + //pre 移到 cur 的地方 + }else{ + pre = cur; + } + //cur 后移 + cur = cur.next; + } + return dummy.next; +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(1)。 + +# 解法二 递归 + +[这里]()看到了一种递归的解法,分享一下。 主要分了两种情况,头结点和后边的节点相等,头结点和后边的节点不相等。 + +```java +public ListNode deleteDuplicates(ListNode head) { + if (head == null) return null; + //如果头结点和后边的节点相等 + if (head.next != null && head.val == head.next.val) { + //跳过所有重复数字 + while (head.next != null && head.val == head.next.val) { + head = head.next; + } + //将所有重复数字去掉后,进入递归 + return deleteDuplicates(head.next); + //头结点和后边的节点不相等 + } else { + //保留头结点,后边的所有节点进入递归 + head.next = deleteDuplicates(head.next); + } + //返回头结点 + return head; +} +``` + +# 总 + 主要还是对链表的理解,然后就是指来指去就好了。 \ No newline at end of file diff --git a/leetCode-83-Remove-Duplicates-from-Sorted-List.md b/leetCode-83-Remove-Duplicates-from-Sorted-List.md index 093566e9e..55ce0c83a 100644 --- a/leetCode-83-Remove-Duplicates-from-Sorted-List.md +++ b/leetCode-83-Remove-Duplicates-from-Sorted-List.md @@ -1,115 +1,115 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/83.jpg) - -给定一个链表,去重,每个数字只保留一个。 - -# 解法一 修改 - -按偷懒的方法,直接在 [82 题]()的基础上改,如果没做过可以先去看一下。之前是重复的数字一个都不保留,这道题的话要留一个,所以代码也很好改。 - -## 迭代法 - -```java -public ListNode deleteDuplicates(ListNode head) { - ListNode pre = new ListNode(0); - ListNode dummy = pre; - pre.next = head; - ListNode cur = head; - while(cur!=null && cur.next!=null){ - boolean equal = false; - while(cur.next!=null && cur.val == cur.next.val){ - cur = cur.next; - equal = true; - } - - if(equal){ - /*************修改的地方*****************/ - //pre.next 指向 cur,不再跳过当前数字 - pre.next = cur; - pre = cur; - /**************************************/ - equal = false; - }else{ - pre = cur; - } - cur = cur.next; - } - return dummy.next; -} -``` - -## 递归 - -```java -public ListNode deleteDuplicates(ListNode head) { - if (head == null) return null; - //如果头结点和后边的节点相等 - if (head.next != null && head.val == head.next.val) { - //跳过所有重复数字 - while (head.next != null && head.val == head.next.val) { - head = head.next; - } - /*************修改的地方*****************/ - //将 head 也包含,进入递归 - return deleteDuplicates(head); - /**************************************/ - //头结点和后边的节点不相等 - } else { - //保留头结点,后边的所有节点进入递归 - head.next = deleteDuplicates(head.next); - } - //返回头结点 - return head; -} -``` - -# 解法二 迭代 - - [82 题]()由于我们要把所有重复的数字都要删除,所有要有一个 pre 指针,指向所有重复数字的最前边。而这道题,我们最终要保留一个数字,所以完全不需要 pre 指针。还有就是,我们不用一次性找到所有重复的数字,我们只需要找到一个,删除一个就够了。所以代码看起来更加简单了。 - -```java -public ListNode deleteDuplicates(ListNode head) { - ListNode cur = head; - while(cur!=null && cur.next!=null){ - //相等的话就删除下一个节点 - if(cur.val == cur.next.val){ - cur.next = cur.next.next; - //不相等就后移 - }else{ - cur = cur.next; - } - } - return head; -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(1)。 - -# 解法三 递归 - -同样的,递归也会更简单些。 - -```java -public ListNode deleteDuplicates(ListNode head) { - if(head == null || head.next == null){ - return head; - } - //头结点和后一个时候相等 - if(head.val == head.next.val){ - //去掉头结点 - return deleteDuplicates(head.next); - }else{ - //加上头结点 - head.next = deleteDuplicates(head.next); - return head; - } -} -``` - -# 总 - -如果 [82 题]()会做的话,这道题就水到渠成了。 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/83.jpg) + +给定一个链表,去重,每个数字只保留一个。 + +# 解法一 修改 + +按偷懒的方法,直接在 [82 题]()的基础上改,如果没做过可以先去看一下。之前是重复的数字一个都不保留,这道题的话要留一个,所以代码也很好改。 + +## 迭代法 + +```java +public ListNode deleteDuplicates(ListNode head) { + ListNode pre = new ListNode(0); + ListNode dummy = pre; + pre.next = head; + ListNode cur = head; + while(cur!=null && cur.next!=null){ + boolean equal = false; + while(cur.next!=null && cur.val == cur.next.val){ + cur = cur.next; + equal = true; + } + + if(equal){ + /*************修改的地方*****************/ + //pre.next 指向 cur,不再跳过当前数字 + pre.next = cur; + pre = cur; + /**************************************/ + equal = false; + }else{ + pre = cur; + } + cur = cur.next; + } + return dummy.next; +} +``` + +## 递归 + +```java +public ListNode deleteDuplicates(ListNode head) { + if (head == null) return null; + //如果头结点和后边的节点相等 + if (head.next != null && head.val == head.next.val) { + //跳过所有重复数字 + while (head.next != null && head.val == head.next.val) { + head = head.next; + } + /*************修改的地方*****************/ + //将 head 也包含,进入递归 + return deleteDuplicates(head); + /**************************************/ + //头结点和后边的节点不相等 + } else { + //保留头结点,后边的所有节点进入递归 + head.next = deleteDuplicates(head.next); + } + //返回头结点 + return head; +} +``` + +# 解法二 迭代 + + [82 题]()由于我们要把所有重复的数字都要删除,所有要有一个 pre 指针,指向所有重复数字的最前边。而这道题,我们最终要保留一个数字,所以完全不需要 pre 指针。还有就是,我们不用一次性找到所有重复的数字,我们只需要找到一个,删除一个就够了。所以代码看起来更加简单了。 + +```java +public ListNode deleteDuplicates(ListNode head) { + ListNode cur = head; + while(cur!=null && cur.next!=null){ + //相等的话就删除下一个节点 + if(cur.val == cur.next.val){ + cur.next = cur.next.next; + //不相等就后移 + }else{ + cur = cur.next; + } + } + return head; +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(1)。 + +# 解法三 递归 + +同样的,递归也会更简单些。 + +```java +public ListNode deleteDuplicates(ListNode head) { + if(head == null || head.next == null){ + return head; + } + //头结点和后一个时候相等 + if(head.val == head.next.val){ + //去掉头结点 + return deleteDuplicates(head.next); + }else{ + //加上头结点 + head.next = deleteDuplicates(head.next); + return head; + } +} +``` + +# 总 + +如果 [82 题]()会做的话,这道题就水到渠成了。 + diff --git a/leetCode-84-Largest-Rectangle-in-Histogram.md b/leetCode-84-Largest-Rectangle-in-Histogram.md index bde9c0cab..5a5c66666 100644 --- a/leetCode-84-Largest-Rectangle-in-Histogram.md +++ b/leetCode-84-Largest-Rectangle-in-Histogram.md @@ -1,428 +1,428 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/84.png) - -给一个柱状图,输出一个矩形区域的最大面积。 - -# 解法一 暴力破解 - -以题目给出的例子为例,柱形图高度有 1, 2, 3, 5, 6,我们只需要找出每一个高度对应的最大的面积,选出最大的即可。如果求高度为 3 的面积最大的,我们只需要遍历每一个高度,然后看连续的大于等于 3 的柱形有几个,如果是 n 个,那么此时的面积就是 3 * n。所以高度确定的话,我们只需要找连续的大于等于 3 的柱形个数,也就是高度。 - -```java -public int largestRectangleArea(int[] heights) { - HashSet heightsSet = new HashSet(); - //得到所有的高度,也就是去重。 - for (int i = 0; i < heights.length; i++) { - heightsSet.add(heights[i]); - } - int maxArea = 0; - //遍历每一个高度 - for (int h : heightsSet) { - int width = 0; - int maxWidth = 1; - //找出连续的大于等于当前高度的柱形个数的最大值 - for (int i = 0; i < heights.length; i++) { - if (heights[i] >= h) { - width++; - //出现小于当前高度的就归零,并且更新最大宽度 - } else { - maxWidth = Math.max(width, maxWidth); - width = 0; - } - } - maxWidth = Math.max(width, maxWidth); - //更新最大区域的面积 - maxArea = Math.max(maxArea, h * maxWidth); - } - return maxArea; -} -``` - -时间复杂度:O(n²)。 - -空间复杂度:O ( n)。存所有高度。 - -# 解法二 - -参考[这里]()。有一些快排的影子,大家不妨先去回顾一下快排。快排中,我们找了一个基准点,把数组分成了小于基准点的数,和大于基准点的数。然后递归的完成了排序。 - -类似的,这里我们也可以找一个柱子。然后把所有柱子分成左半区域和右半区域。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/84_1.jpg) - -这样要找的最大矩形区域就是三种情况了。 - -1. 最大矩形区域在不包含选定柱子的左半区域当中。 -2. 最大矩形区域在不包含选定柱子的右半区域当中。 -3. 最大矩形区域包含选定柱子的区域。 - -对于 1、2 两种情况,我们只需要递归的去求就行了。而对于第 3 种情况,我们找一个特殊的柱子作为分界点以方便计算,哪一个柱子呢?最矮的那个!有什么好处呢?这样包含该柱子的最大区域,一定是涵盖了当前所有柱子。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/84_2.jpg) - -所以面积当然是当前选定柱子的高度乘以当前的最大宽度了。 - -对于当前的时间复杂度,如果每次选定的柱子都可以把区域一分为二,递推式就是 - -T(n) = 2 * T(n / 2 ) + n。 - -上边多加的 n 是找最小柱子耗费,因为需要遍历一遍柱子。然后和快排一样,这样递归下去,时间复杂度就是 O(n log(n))。当然和快排一样的问题,最坏的情况,如果最小柱子每次都出现在末尾,这样会使得只有左半区域,右半区域是 0。递推式就变成了 - -T(n) = T(n - 1 ) + n。 - -时间复杂度就变成 O(n²)了,怎么优化呢? - -重点就在上边找最小柱子多加的 n 上了,如果我们找最小柱子时间复杂度优化成 log(n)。那么在最坏情况下,递推式变成 - -T(n) = T(n - 1 ) + log(n)。 - -最坏的情况,递推式代入,依旧是 O(n log(n))。而找最小柱子,就可以抽象成,在一个数组区间内找最小值问题,而这个问题前人已经提出了一个数据结构,可以使得时间复杂度是 log(n),完美!名字叫做线段树,可以参考[这里]()和[线段树空间复杂度](),我就不重复讲了。主要思想就是利用二叉树,使得查找时间复杂度变成了 O(log(n))。 - -我们以序列 { 5, 9, 7, 4 ,6, 1} 为例,线段树长下边的样子。节点的值代表当前区间内的最小值。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/84_3.jpg) - - - -```java -class Node// 节点 -{ - int start;// 区间的左端点 - int end;// 区间的右端点 - int data;// 该区间的最小值 - int index; // 该节点最小值对应数组的下标 - - public Node(int start, int end)// 构造方法中传入左端点和右端点 - { - this.start = start; - this.end = end; - } - -} - -class SegmentTree { - private int[] base;;// 给定数组 - Node[] nodes;// 存储线段树的数组 - - public SegmentTree(int[] nums) { - base = new int[nums.length]; - for (int i = 0; i < nums.length; i++) { - base[i] = nums[i]; - } - //需要 4n 的空间,上边链接给出了原因 - nodes = new Node[base.length * 4]; - } - - public void build(int index)// 构造一颗线段树,传入下标 - { - Node node = nodes[index];// 取出该下标下的节点 - if (node == null) {// 根节点需要手动创建 - nodes[index] = new Node(0, this.base.length - 1); - node = nodes[index]; - } - if (node.start == node.end) {// 如果这个线段的左端点等于右端点则这个点是叶子节点 - node.data = base[node.start]; - node.index = node.start; - } else { // 否则递归构造左右子树 - int mid = (node.start + node.end) >> 1;// 现在这个线段的中点 - nodes[(index << 1) + 1] = new Node(node.start, mid);// 左孩子线段 - nodes[(index << 1) + 2] = new Node(mid + 1, node.end);// 右孩子线段 - build((index << 1) + 1);// 构造左孩子 - build((index << 1) + 2);// 构造右孩子 - if (nodes[(index << 1) + 1].data <= nodes[(index << 1) + 2].data) { - node.data = nodes[(index << 1) + 1].data; - node.index = nodes[(index << 1) + 1].index; - } else { - node.data = nodes[(index << 1) + 2].data; - node.index = nodes[(index << 1) + 2].index; - } - } - } - - public Node query(int index, int start, int end) { - Node node = nodes[index]; - if (node.start > end || node.end < start) - return null;// 当前区间和待查询区间没有交集,那么返回一个极大值 - if (node.start >= start && node.end <= end) - return node;// 如果当前的区间被包含在待查询的区间内则返回当前区间的最小值 - Node left = query((index << 1) + 1, start, end); - int dataLeft = left == null ? Integer.MAX_VALUE : left.data; - Node right = query((index << 1) + 2, start, end); - int dataRight = right == null ? Integer.MAX_VALUE : right.data; - return dataLeft <= dataRight ? left : right; - - } -} -class Solution { - public int largestRectangleArea(int[] heights) { - if (heights.length == 0) { - return 0; - } - //构造线段树 - SegmentTree tree = new SegmentTree(heights); - tree.build(0); - return getMaxArea(tree, 0, heights.length - 1, heights); - } - /** - * 查询某个区间的最小值 - * @param tree 构造好的线段树 - * @param start 待查询的区间的左端点 - * @param end 待查询的区间的右端点 - * @param heights 给定的数组 - * @return 返回当前区间的矩形区域的最大值 - */ - private int getMaxArea(SegmentTree tree, int start, int end, int[] heights) { - if (start == end) { - return heights[start]; - } - //非法情况,返回一个最小值,防止影响正确的最大值 - if (start > end) { - return Integer.MIN_VALUE; - } - //找出最小的柱子的下标 - int min = tree.query(0, start, end).index; - //最大矩形区域包含选定柱子的区域。 - int area1 = heights[min] * (end - start + 1); - //最大矩形区域在不包含选定柱子的左半区域。 - int area2 = getMaxArea(tree, start, min - 1, heights); - //最大矩形区域在不包含选定柱子的右半区域。 - int area3 = getMaxArea(tree, min + 1, end, heights); - //返回最大的情况 - return Math.max(Math.max(area1, area2), area3); - } -} -``` - -时间复杂度:O(n log(n))。 - -空间复杂度:O(n),用来存储线段树。 - -# 解法三 - -参考[这里](),思考下解法二中遇到的问题,利用了类似快排的思想,最好情况的递推式是 - -T(n) = 2 * T(n / 2 ) + n。 - -就是 n log(n),但是由于分界点的柱子的选择,并不能总保证两部分的柱子数量均分。所以如果这个问题解决,那么我们就可以保证时间复杂度是 n log(n)了。如何让它均分呢?我们强行把它分成 3 部分呗。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/84_4.jpg) - -1. 左半区域,含有一半柱子,当然如果总数是奇数个,这里会多一个。 - -2. 右半区域,含有一半柱子,当然如果总数是奇数个,这里会少一个。 -3. 包含最中间柱子的部分,最大区域一定会包含橙色部分,这样才会横跨两个区域。 - -情况 1、2 的最大区域面积可以用递归来解决,情况 3 的话,我们只要保证是 O(n),就满足了我们的递推式,从而保证时间复杂度是O(n log(n))。怎么做呢? - -贪婪的思想,每次选两边较高的柱子扩展柱子。然后其实就是求出了 2 个柱子,3 个柱子,4 个柱子,5 个柱子...每种情况的最大值,然后选最大的就可以了。 - -1. 初始的时候是两个柱子,记录此时的面积。 - -2. 然后加 1 个柱子,选取两边较高的柱子,然后计算此时的面积,更新最大区域面积。 -3. 不停的重复过程 2 ,直到所有柱子遍历完 - -```java -public int largestRectangleArea(int[] heights) { - if (heights.length == 0) { - return 0; - } - return getMaxArea(heights, 0, heights.length - 1); -} - -private int getMaxArea(int[] heights, int left, int right) { - if (left == right) { - return heights[left]; - } - int mid = left + (right - left) / 2; - //左半部分 - int area1 = getMaxArea(heights, left, mid); - //右半部分 - int area2 = getMaxArea(heights, mid + 1, right); - //中间区域 - int area3 = getMidArea(heights, left, mid, right); - //选择最大的 - return Math.max(Math.max(area1, area2), area3); -} - -private int getMidArea(int[] heights, int left, int mid, int right) { - int i = mid; - int j = mid + 1; - int minH = Math.min(heights[i], heights[j]); - int area = minH * 2; - //向两端扩展 - while (i >= left && j <= right) { - minH = Math.min(minH, Math.min(heights[i], heights[j])); - //更新最大面积 - area = Math.max(area, minH * (j - i + 1)); - if (i == left) { - j++; - } else if (j == right) { - i--; - //选择较高的柱子 - } else if (heights[i - 1] >= heights[j + 1]) { - i--; - } else { - j++; - - } - } - return area; -} -``` - -时间复杂度:O(n log(n))。 - -空间复杂度:O(log(n)),压栈的空间。 - -# 解法四 - -参考[这里]()。解法一暴力破解中,我们把所有矩形区域按高度依次求出来,选出了最大的。这里我们想另外一个分类方法。分别求出包含每个柱子的矩形区域的最大面积,然后选最大的。要包含这个柱子,也就是这个柱子是当前矩形区域的高度。也就是,这个柱子是当前矩形区域中柱子最高的。如下图中包含橙色柱子的矩形区域的最大面积。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/84_5.jpg) - -求当前的矩形区域,我们只需要知道比当前柱子到左边第一个小的 leftLessMin 和到右边第一个小的 rightLessMin 两个柱子下标,就可以求出矩形的面积为 (rightLessMin - leftLessMin - 1) * 当前柱子高度。然后遍历每个柱子,按同样的方法求出矩形面积,选最大的就可以了。 - -现在的问题就是,怎么知道 rightLessMin 和 leftLessMin 。 - -我们可以用一个数组,leftLessMin[ ] 保存各自的左边第一个小的柱子。 - -```java -leftLessMin[0] = -1;//第一个柱子前边没有柱子,所以赋值为 -1,便于计算面积 -for (int i = 1; i < heights.length; i++) { - int p = i - 1; - //p 一直减少,找到第一个比当前高度小的柱子就停止 - while (p >= 0 && height[p] >= height[i]) { - p--; - } - leftLessMin[i] = p; -} -``` - -上边的时间复杂度是 O(n²),我们可以进行优化。参考下边的图,当前柱子 i 比上一个柱子小的时候,因为我们是找比当前柱子矮的,之前我们进行减 1,判断上上个。但是我们之前已经求出了上一个柱子的 leftLessMin[ i - 1],也就是第一个比上个柱子小的柱子,所以其实我们可以直接跳到 leftLessMin[ i - 1] 比较。因为从 leftLessMin[ i - 1] + 1到 i - 1 的柱子一定是比当前柱子 i 高的,所以可以跳过。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/84_6.jpg) - -这样的话时间复杂度达到了O(n)。开始自己不理解,问了一下同学。其实证明的话,可以结合解法五,我们寻找 leftLessMin 其实可以看做压栈出栈的过程,每个元素只会被访问两次。 - -```java -int[] leftLessMin = new int[heights.length]; -leftLessMin[0] = -1; -for (int i = 1; i < heights.length; i++) { - int l = i - 1; - //当前柱子更小一些,进行左移 - while (l >= 0 && heights[l] >= heights[i]) { - l = leftLessMin[l]; - } - leftLessMin[i] = l; -} -``` - -求到右边第一个小的柱子同理,下边是完整的代码。 - -```java -public int largestRectangleArea(int[] heights) { - if (heights.length == 0) { - return 0; - } - //求每个柱子的左边第一个小的柱子的下标 - int[] leftLessMin = new int[heights.length]; - leftLessMin[0] = -1; - for (int i = 1; i < heights.length; i++) { - int l = i - 1; - while (l >= 0 && heights[l] >= heights[i]) { - l = leftLessMin[l]; - } - leftLessMin[i] = l; - } - - //求每个柱子的右边第一个小的柱子的下标 - int[] rightLessMin = new int[heights.length]; - rightLessMin[heights.length - 1] = heights.length; - for (int i = heights.length - 2; i >= 0; i--) { - int r = i + 1; - while (r <= heights.length - 1 && heights[r] >= heights[i]) { - r = rightLessMin[r]; - } - rightLessMin[i] = r; - } - - //求包含每个柱子的矩形区域的最大面积,选出最大的 - int maxArea = 0; - for (int i = 0; i < heights.length; i++) { - int area = (rightLessMin[i] - leftLessMin[i] - 1) * heights[i]; - maxArea = Math.max(area, maxArea); - } - return maxArea; -} - -``` - -时间复杂度:O(n),取决于找 leftLessMin [ i ] 的复杂度。 - -空间复杂度:O(n),保存每个柱子左边右边第一个小的柱子下标。 - -# 解法五 栈 - -参考[这里]()。之前也遇到利用栈巧妙解题的,例如[42题]()的解法五,和这道题的共同点就是配对问题。思路的话,本质上和解法四是一样的,可以先看下解法四,左边第一个小于当前柱子和右边第一个小于当前柱子是一对。通过它俩可以求出当前柱子的最大矩形区域。那么具体怎么操作呢? - -遍历每个柱子,然后分下边几种情况。 - -* 如果当前栈空,或者当前柱子大于栈顶柱子的高度,就将当前柱子的下标入栈 -* 当前柱子的高度小于栈顶柱子的高度。那么就把栈顶柱子出栈,当做解法四中的当前要求面积的柱子。而右边第一个小于当前柱子的下标就是当前在遍历的柱子,左边第一个小于当前柱子的下标就是当前新的栈顶。 - -遍历完成后,如果栈没有空。就依次出栈,出栈元素当做解法四中的要求面积的柱子,然后依次计算矩形区域面积。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/84_7.jpg) - -结合图可以看一下,从头开始遍历,遍历柱子开始的时候都大于前一个柱子高度,所以依次入栈。直到遍历到橙色部分,栈顶元素出栈,计算包含栈顶柱子的矩形区域。而左边第一个小于要求柱子的就是新栈顶,右边第一个小于要求柱子的就是当前遍历柱子。 - -```java -public int largestRectangleArea(int[] heights) { - int maxArea = 0; - Stack stack = new Stack<>(); - int p = 0; - while (p < heights.length) { - //栈空入栈 - if (stack.isEmpty()) { - stack.push(p); - p++; - } else { - int top = stack.peek(); - //当前高度大于栈顶,入栈 - if (heights[p] >= heights[top]) { - stack.push(p); - p++; - } else { - //保存栈顶高度 - int height = heights[stack.pop()]; - //左边第一个小于当前柱子的下标 - int leftLessMin = stack.isEmpty() ? -1 : stack.peek(); - //右边第一个小于当前柱子的下标 - int RightLessMin = p; - //计算面积 - int area = (RightLessMin - leftLessMin - 1) * height; - maxArea = Math.max(area, maxArea); - } - } - } - while (!stack.isEmpty()) { - //保存栈顶高度 - int height = heights[stack.pop()]; - //左边第一个小于当前柱子的下标 - int leftLessMin = stack.isEmpty() ? -1 : stack.peek(); - //右边没有小于当前高度的柱子,所以赋值为数组的长度便于计算 - int RightLessMin = heights.length; - int area = (RightLessMin - leftLessMin - 1) * height; - maxArea = Math.max(area, maxArea); - } - return maxArea; -} -``` - -时间复杂度:O(n),因为对于每个柱子只会经历入栈出栈,所以最多 2n 次。 - -空间复杂度:O(n),栈的大小。 - -# 总 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/84.png) + +给一个柱状图,输出一个矩形区域的最大面积。 + +# 解法一 暴力破解 + +以题目给出的例子为例,柱形图高度有 1, 2, 3, 5, 6,我们只需要找出每一个高度对应的最大的面积,选出最大的即可。如果求高度为 3 的面积最大的,我们只需要遍历每一个高度,然后看连续的大于等于 3 的柱形有几个,如果是 n 个,那么此时的面积就是 3 * n。所以高度确定的话,我们只需要找连续的大于等于 3 的柱形个数,也就是高度。 + +```java +public int largestRectangleArea(int[] heights) { + HashSet heightsSet = new HashSet(); + //得到所有的高度,也就是去重。 + for (int i = 0; i < heights.length; i++) { + heightsSet.add(heights[i]); + } + int maxArea = 0; + //遍历每一个高度 + for (int h : heightsSet) { + int width = 0; + int maxWidth = 1; + //找出连续的大于等于当前高度的柱形个数的最大值 + for (int i = 0; i < heights.length; i++) { + if (heights[i] >= h) { + width++; + //出现小于当前高度的就归零,并且更新最大宽度 + } else { + maxWidth = Math.max(width, maxWidth); + width = 0; + } + } + maxWidth = Math.max(width, maxWidth); + //更新最大区域的面积 + maxArea = Math.max(maxArea, h * maxWidth); + } + return maxArea; +} +``` + +时间复杂度:O(n²)。 + +空间复杂度:O ( n)。存所有高度。 + +# 解法二 + +参考[这里]()。有一些快排的影子,大家不妨先去回顾一下快排。快排中,我们找了一个基准点,把数组分成了小于基准点的数,和大于基准点的数。然后递归的完成了排序。 + +类似的,这里我们也可以找一个柱子。然后把所有柱子分成左半区域和右半区域。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/84_1.jpg) + +这样要找的最大矩形区域就是三种情况了。 + +1. 最大矩形区域在不包含选定柱子的左半区域当中。 +2. 最大矩形区域在不包含选定柱子的右半区域当中。 +3. 最大矩形区域包含选定柱子的区域。 + +对于 1、2 两种情况,我们只需要递归的去求就行了。而对于第 3 种情况,我们找一个特殊的柱子作为分界点以方便计算,哪一个柱子呢?最矮的那个!有什么好处呢?这样包含该柱子的最大区域,一定是涵盖了当前所有柱子。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/84_2.jpg) + +所以面积当然是当前选定柱子的高度乘以当前的最大宽度了。 + +对于当前的时间复杂度,如果每次选定的柱子都可以把区域一分为二,递推式就是 + +T(n) = 2 * T(n / 2 ) + n。 + +上边多加的 n 是找最小柱子耗费,因为需要遍历一遍柱子。然后和快排一样,这样递归下去,时间复杂度就是 O(n log(n))。当然和快排一样的问题,最坏的情况,如果最小柱子每次都出现在末尾,这样会使得只有左半区域,右半区域是 0。递推式就变成了 + +T(n) = T(n - 1 ) + n。 + +时间复杂度就变成 O(n²)了,怎么优化呢? + +重点就在上边找最小柱子多加的 n 上了,如果我们找最小柱子时间复杂度优化成 log(n)。那么在最坏情况下,递推式变成 + +T(n) = T(n - 1 ) + log(n)。 + +最坏的情况,递推式代入,依旧是 O(n log(n))。而找最小柱子,就可以抽象成,在一个数组区间内找最小值问题,而这个问题前人已经提出了一个数据结构,可以使得时间复杂度是 log(n),完美!名字叫做线段树,可以参考[这里]()和[线段树空间复杂度](),我就不重复讲了。主要思想就是利用二叉树,使得查找时间复杂度变成了 O(log(n))。 + +我们以序列 { 5, 9, 7, 4 ,6, 1} 为例,线段树长下边的样子。节点的值代表当前区间内的最小值。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/84_3.jpg) + + + +```java +class Node// 节点 +{ + int start;// 区间的左端点 + int end;// 区间的右端点 + int data;// 该区间的最小值 + int index; // 该节点最小值对应数组的下标 + + public Node(int start, int end)// 构造方法中传入左端点和右端点 + { + this.start = start; + this.end = end; + } + +} + +class SegmentTree { + private int[] base;;// 给定数组 + Node[] nodes;// 存储线段树的数组 + + public SegmentTree(int[] nums) { + base = new int[nums.length]; + for (int i = 0; i < nums.length; i++) { + base[i] = nums[i]; + } + //需要 4n 的空间,上边链接给出了原因 + nodes = new Node[base.length * 4]; + } + + public void build(int index)// 构造一颗线段树,传入下标 + { + Node node = nodes[index];// 取出该下标下的节点 + if (node == null) {// 根节点需要手动创建 + nodes[index] = new Node(0, this.base.length - 1); + node = nodes[index]; + } + if (node.start == node.end) {// 如果这个线段的左端点等于右端点则这个点是叶子节点 + node.data = base[node.start]; + node.index = node.start; + } else { // 否则递归构造左右子树 + int mid = (node.start + node.end) >> 1;// 现在这个线段的中点 + nodes[(index << 1) + 1] = new Node(node.start, mid);// 左孩子线段 + nodes[(index << 1) + 2] = new Node(mid + 1, node.end);// 右孩子线段 + build((index << 1) + 1);// 构造左孩子 + build((index << 1) + 2);// 构造右孩子 + if (nodes[(index << 1) + 1].data <= nodes[(index << 1) + 2].data) { + node.data = nodes[(index << 1) + 1].data; + node.index = nodes[(index << 1) + 1].index; + } else { + node.data = nodes[(index << 1) + 2].data; + node.index = nodes[(index << 1) + 2].index; + } + } + } + + public Node query(int index, int start, int end) { + Node node = nodes[index]; + if (node.start > end || node.end < start) + return null;// 当前区间和待查询区间没有交集,那么返回一个极大值 + if (node.start >= start && node.end <= end) + return node;// 如果当前的区间被包含在待查询的区间内则返回当前区间的最小值 + Node left = query((index << 1) + 1, start, end); + int dataLeft = left == null ? Integer.MAX_VALUE : left.data; + Node right = query((index << 1) + 2, start, end); + int dataRight = right == null ? Integer.MAX_VALUE : right.data; + return dataLeft <= dataRight ? left : right; + + } +} +class Solution { + public int largestRectangleArea(int[] heights) { + if (heights.length == 0) { + return 0; + } + //构造线段树 + SegmentTree tree = new SegmentTree(heights); + tree.build(0); + return getMaxArea(tree, 0, heights.length - 1, heights); + } + /** + * 查询某个区间的最小值 + * @param tree 构造好的线段树 + * @param start 待查询的区间的左端点 + * @param end 待查询的区间的右端点 + * @param heights 给定的数组 + * @return 返回当前区间的矩形区域的最大值 + */ + private int getMaxArea(SegmentTree tree, int start, int end, int[] heights) { + if (start == end) { + return heights[start]; + } + //非法情况,返回一个最小值,防止影响正确的最大值 + if (start > end) { + return Integer.MIN_VALUE; + } + //找出最小的柱子的下标 + int min = tree.query(0, start, end).index; + //最大矩形区域包含选定柱子的区域。 + int area1 = heights[min] * (end - start + 1); + //最大矩形区域在不包含选定柱子的左半区域。 + int area2 = getMaxArea(tree, start, min - 1, heights); + //最大矩形区域在不包含选定柱子的右半区域。 + int area3 = getMaxArea(tree, min + 1, end, heights); + //返回最大的情况 + return Math.max(Math.max(area1, area2), area3); + } +} +``` + +时间复杂度:O(n log(n))。 + +空间复杂度:O(n),用来存储线段树。 + +# 解法三 + +参考[这里](),思考下解法二中遇到的问题,利用了类似快排的思想,最好情况的递推式是 + +T(n) = 2 * T(n / 2 ) + n。 + +就是 n log(n),但是由于分界点的柱子的选择,并不能总保证两部分的柱子数量均分。所以如果这个问题解决,那么我们就可以保证时间复杂度是 n log(n)了。如何让它均分呢?我们强行把它分成 3 部分呗。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/84_4.jpg) + +1. 左半区域,含有一半柱子,当然如果总数是奇数个,这里会多一个。 + +2. 右半区域,含有一半柱子,当然如果总数是奇数个,这里会少一个。 +3. 包含最中间柱子的部分,最大区域一定会包含橙色部分,这样才会横跨两个区域。 + +情况 1、2 的最大区域面积可以用递归来解决,情况 3 的话,我们只要保证是 O(n),就满足了我们的递推式,从而保证时间复杂度是O(n log(n))。怎么做呢? + +贪婪的思想,每次选两边较高的柱子扩展柱子。然后其实就是求出了 2 个柱子,3 个柱子,4 个柱子,5 个柱子...每种情况的最大值,然后选最大的就可以了。 + +1. 初始的时候是两个柱子,记录此时的面积。 + +2. 然后加 1 个柱子,选取两边较高的柱子,然后计算此时的面积,更新最大区域面积。 +3. 不停的重复过程 2 ,直到所有柱子遍历完 + +```java +public int largestRectangleArea(int[] heights) { + if (heights.length == 0) { + return 0; + } + return getMaxArea(heights, 0, heights.length - 1); +} + +private int getMaxArea(int[] heights, int left, int right) { + if (left == right) { + return heights[left]; + } + int mid = left + (right - left) / 2; + //左半部分 + int area1 = getMaxArea(heights, left, mid); + //右半部分 + int area2 = getMaxArea(heights, mid + 1, right); + //中间区域 + int area3 = getMidArea(heights, left, mid, right); + //选择最大的 + return Math.max(Math.max(area1, area2), area3); +} + +private int getMidArea(int[] heights, int left, int mid, int right) { + int i = mid; + int j = mid + 1; + int minH = Math.min(heights[i], heights[j]); + int area = minH * 2; + //向两端扩展 + while (i >= left && j <= right) { + minH = Math.min(minH, Math.min(heights[i], heights[j])); + //更新最大面积 + area = Math.max(area, minH * (j - i + 1)); + if (i == left) { + j++; + } else if (j == right) { + i--; + //选择较高的柱子 + } else if (heights[i - 1] >= heights[j + 1]) { + i--; + } else { + j++; + + } + } + return area; +} +``` + +时间复杂度:O(n log(n))。 + +空间复杂度:O(log(n)),压栈的空间。 + +# 解法四 + +参考[这里]()。解法一暴力破解中,我们把所有矩形区域按高度依次求出来,选出了最大的。这里我们想另外一个分类方法。分别求出包含每个柱子的矩形区域的最大面积,然后选最大的。要包含这个柱子,也就是这个柱子是当前矩形区域的高度。也就是,这个柱子是当前矩形区域中柱子最高的。如下图中包含橙色柱子的矩形区域的最大面积。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/84_5.jpg) + +求当前的矩形区域,我们只需要知道比当前柱子到左边第一个小的 leftLessMin 和到右边第一个小的 rightLessMin 两个柱子下标,就可以求出矩形的面积为 (rightLessMin - leftLessMin - 1) * 当前柱子高度。然后遍历每个柱子,按同样的方法求出矩形面积,选最大的就可以了。 + +现在的问题就是,怎么知道 rightLessMin 和 leftLessMin 。 + +我们可以用一个数组,leftLessMin[ ] 保存各自的左边第一个小的柱子。 + +```java +leftLessMin[0] = -1;//第一个柱子前边没有柱子,所以赋值为 -1,便于计算面积 +for (int i = 1; i < heights.length; i++) { + int p = i - 1; + //p 一直减少,找到第一个比当前高度小的柱子就停止 + while (p >= 0 && height[p] >= height[i]) { + p--; + } + leftLessMin[i] = p; +} +``` + +上边的时间复杂度是 O(n²),我们可以进行优化。参考下边的图,当前柱子 i 比上一个柱子小的时候,因为我们是找比当前柱子矮的,之前我们进行减 1,判断上上个。但是我们之前已经求出了上一个柱子的 leftLessMin[ i - 1],也就是第一个比上个柱子小的柱子,所以其实我们可以直接跳到 leftLessMin[ i - 1] 比较。因为从 leftLessMin[ i - 1] + 1到 i - 1 的柱子一定是比当前柱子 i 高的,所以可以跳过。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/84_6.jpg) + +这样的话时间复杂度达到了O(n)。开始自己不理解,问了一下同学。其实证明的话,可以结合解法五,我们寻找 leftLessMin 其实可以看做压栈出栈的过程,每个元素只会被访问两次。 + +```java +int[] leftLessMin = new int[heights.length]; +leftLessMin[0] = -1; +for (int i = 1; i < heights.length; i++) { + int l = i - 1; + //当前柱子更小一些,进行左移 + while (l >= 0 && heights[l] >= heights[i]) { + l = leftLessMin[l]; + } + leftLessMin[i] = l; +} +``` + +求到右边第一个小的柱子同理,下边是完整的代码。 + +```java +public int largestRectangleArea(int[] heights) { + if (heights.length == 0) { + return 0; + } + //求每个柱子的左边第一个小的柱子的下标 + int[] leftLessMin = new int[heights.length]; + leftLessMin[0] = -1; + for (int i = 1; i < heights.length; i++) { + int l = i - 1; + while (l >= 0 && heights[l] >= heights[i]) { + l = leftLessMin[l]; + } + leftLessMin[i] = l; + } + + //求每个柱子的右边第一个小的柱子的下标 + int[] rightLessMin = new int[heights.length]; + rightLessMin[heights.length - 1] = heights.length; + for (int i = heights.length - 2; i >= 0; i--) { + int r = i + 1; + while (r <= heights.length - 1 && heights[r] >= heights[i]) { + r = rightLessMin[r]; + } + rightLessMin[i] = r; + } + + //求包含每个柱子的矩形区域的最大面积,选出最大的 + int maxArea = 0; + for (int i = 0; i < heights.length; i++) { + int area = (rightLessMin[i] - leftLessMin[i] - 1) * heights[i]; + maxArea = Math.max(area, maxArea); + } + return maxArea; +} + +``` + +时间复杂度:O(n),取决于找 leftLessMin [ i ] 的复杂度。 + +空间复杂度:O(n),保存每个柱子左边右边第一个小的柱子下标。 + +# 解法五 栈 + +参考[这里]()。之前也遇到利用栈巧妙解题的,例如[42题]()的解法五,和这道题的共同点就是配对问题。思路的话,本质上和解法四是一样的,可以先看下解法四,左边第一个小于当前柱子和右边第一个小于当前柱子是一对。通过它俩可以求出当前柱子的最大矩形区域。那么具体怎么操作呢? + +遍历每个柱子,然后分下边几种情况。 + +* 如果当前栈空,或者当前柱子大于栈顶柱子的高度,就将当前柱子的下标入栈 +* 当前柱子的高度小于栈顶柱子的高度。那么就把栈顶柱子出栈,当做解法四中的当前要求面积的柱子。而右边第一个小于当前柱子的下标就是当前在遍历的柱子,左边第一个小于当前柱子的下标就是当前新的栈顶。 + +遍历完成后,如果栈没有空。就依次出栈,出栈元素当做解法四中的要求面积的柱子,然后依次计算矩形区域面积。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/84_7.jpg) + +结合图可以看一下,从头开始遍历,遍历柱子开始的时候都大于前一个柱子高度,所以依次入栈。直到遍历到橙色部分,栈顶元素出栈,计算包含栈顶柱子的矩形区域。而左边第一个小于要求柱子的就是新栈顶,右边第一个小于要求柱子的就是当前遍历柱子。 + +```java +public int largestRectangleArea(int[] heights) { + int maxArea = 0; + Stack stack = new Stack<>(); + int p = 0; + while (p < heights.length) { + //栈空入栈 + if (stack.isEmpty()) { + stack.push(p); + p++; + } else { + int top = stack.peek(); + //当前高度大于栈顶,入栈 + if (heights[p] >= heights[top]) { + stack.push(p); + p++; + } else { + //保存栈顶高度 + int height = heights[stack.pop()]; + //左边第一个小于当前柱子的下标 + int leftLessMin = stack.isEmpty() ? -1 : stack.peek(); + //右边第一个小于当前柱子的下标 + int RightLessMin = p; + //计算面积 + int area = (RightLessMin - leftLessMin - 1) * height; + maxArea = Math.max(area, maxArea); + } + } + } + while (!stack.isEmpty()) { + //保存栈顶高度 + int height = heights[stack.pop()]; + //左边第一个小于当前柱子的下标 + int leftLessMin = stack.isEmpty() ? -1 : stack.peek(); + //右边没有小于当前高度的柱子,所以赋值为数组的长度便于计算 + int RightLessMin = heights.length; + int area = (RightLessMin - leftLessMin - 1) * height; + maxArea = Math.max(area, maxArea); + } + return maxArea; +} +``` + +时间复杂度:O(n),因为对于每个柱子只会经历入栈出栈,所以最多 2n 次。 + +空间复杂度:O(n),栈的大小。 + +# 总 + 这道题经典呀,第一次用快排的思想去解决问题,太优雅了。另外通过对问题的挖掘,时间复杂度优化到 O(n),也是惊叹。 \ No newline at end of file diff --git a/leetCode-85-Maximal-Rectangle.md b/leetCode-85-Maximal-Rectangle.md index 704da6d24..2ab5a3e8c 100644 --- a/leetCode-85-Maximal-Rectangle.md +++ b/leetCode-85-Maximal-Rectangle.md @@ -1,363 +1,363 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/85.jpg) - -给一个只有 0 和 1 的矩阵,输出一个最大的矩形的面积,这个矩形里边只含有 1。 - -# 解法一 暴力破解 - -参考[这里](),遍历每个点,求以这个点为矩阵右下角的所有矩阵面积。如下图的两个例子,橙色是当前遍历的点,然后虚线框圈出的矩阵是其中一个矩阵。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/85_2.jpg) - -怎么找出这样的矩阵呢?如下图,如果我们知道了以这个点结尾的连续 1 的个数的话,问题就变得简单了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/85_3.jpg) - -1. 首先求出高度是 1 的矩形面积,也就是它自身的数,也就是上图以橙色的 4 结尾的 「1234」的那个矩形,面积就是 4。 - -2. 然后向上扩展一行,高度增加一,选出当前列最小的数字,作为矩阵的宽,如上图,当前列中有 `2` 和 `4` ,那么,就将 `2` 作为矩形的宽,求出面积,对应上图的矩形圈出的部分。 - -3. 然后继续向上扩展,重复步骤 2。 - -按照上边的方法,遍历所有的点,以当前点为矩阵的右下角,求出所有的矩阵就可以了。下图是某一个点的过程。 - -以橙色的点为右下角,高度为 1。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/85_4.jpg) - -高度为 2。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/85_5.jpg) - -高度为 3。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/85_6.jpg) - -代码的话,把求每个点累计的连续 `1` 的个数用 `width` 保存,同时把求最大矩形的面积和求 `width`融合到同一个循环中。 - -```java -public int maximalRectangle(char[][] matrix) { - if (matrix.length == 0) { - return 0; - } - //保存以当前数字结尾的连续 1 的个数 - int[][] width = new int[matrix.length][matrix[0].length]; - int maxArea = 0; - //遍历每一行 - for (int row = 0; row < matrix.length; row++) { - for (int col = 0; col < matrix[0].length; col++) { - //更新 width - if (matrix[row][col] == '1') { - if (col == 0) { - width[row][col] = 1; - } else { - width[row][col] = width[row][col - 1] + 1; - } - } else { - width[row][col] = 0; - } - //记录所有行中最小的数 - int minWidth = width[row][col]; - //向上扩展行 - for (int up_row = row; up_row >= 0; up_row--) { - int height = row - up_row + 1; - //找最小的数作为矩阵的宽 - minWidth = Math.min(minWidth, width[up_row][col]); - //更新面积 - maxArea = Math.max(maxArea, height * minWidth); - } - } - } - return maxArea; -} -``` - -时间复杂度:O(m²n)。 - -空间复杂度:O(mn)。 - -# 解法二 - -参考[这里](),接下来的解法,会让这道题变得异常简单。还记得 [84 题]()吗?求一个直方图矩形的最大面积。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/84.png) - -大家可以先做 [84 题](),然后回来考虑这道题。 - -再想一下这个题,看下边的橙色的部分,这完全就是上一道题呀! - -![](https://windliang.oss-cn-beijing.aliyuncs.com/85_7.jpg) - -算法有了,就是求出每一层的 heights[] 然后传给上一题的函数就可以了。 - -利用上一题的栈解法。 - -```java -public int maximalRectangle(char[][] matrix) { - if (matrix.length == 0) { - return 0; - } - int[] heights = new int[matrix[0].length]; - int maxArea = 0; - for (int row = 0; row < matrix.length; row++) { - //遍历每一列,更新高度 - for (int col = 0; col < matrix[0].length; col++) { - if (matrix[row][col] == '1') { - heights[col] += 1; - } else { - heights[col] = 0; - } - } - //调用上一题的解法,更新函数 - maxArea = Math.max(maxArea, largestRectangleArea(heights)); - } - return maxArea; -} - -public int largestRectangleArea(int[] heights) { - int maxArea = 0; - Stack stack = new Stack<>(); - int p = 0; - while (p < heights.length) { - //栈空入栈 - if (stack.isEmpty()) { - stack.push(p); - p++; - } else { - int top = stack.peek(); - //当前高度大于栈顶,入栈 - if (heights[p] >= heights[top]) { - stack.push(p); - p++; - } else { - //保存栈顶高度 - int height = heights[stack.pop()]; - //左边第一个小于当前柱子的下标 - int leftLessMin = stack.isEmpty() ? -1 : stack.peek(); - //右边第一个小于当前柱子的下标 - int RightLessMin = p; - //计算面积 - int area = (RightLessMin - leftLessMin - 1) * height; - maxArea = Math.max(area, maxArea); - } - } - } - while (!stack.isEmpty()) { - //保存栈顶高度 - int height = heights[stack.pop()]; - //左边第一个小于当前柱子的下标 - int leftLessMin = stack.isEmpty() ? -1 : stack.peek(); - //右边没有小于当前高度的柱子,所以赋值为数组的长度便于计算 - int RightLessMin = heights.length; - int area = (RightLessMin - leftLessMin - 1) * height; - maxArea = Math.max(area, maxArea); - } - return maxArea; -} -``` - -时间复杂度:O(mn)。 - -空间复杂度:O(n)。 - -利用上一题的解法四。 - -```java -public int maximalRectangle(char[][] matrix) { - if (matrix.length == 0) { - return 0; - } - int[] heights = new int[matrix[0].length]; - int maxArea = 0; - for (int row = 0; row < matrix.length; row++) { - //遍历每一列,更新高度 - for (int col = 0; col < matrix[0].length; col++) { - if (matrix[row][col] == '1') { - heights[col] += 1; - } else { - heights[col] = 0; - } - } - //调用上一题的解法,更新函数 - maxArea = Math.max(maxArea, largestRectangleArea(heights)); - } - return maxArea; -} - -public int largestRectangleArea(int[] heights) { - if (heights.length == 0) { - return 0; - } - int[] leftLessMin = new int[heights.length]; - leftLessMin[0] = -1; - for (int i = 1; i < heights.length; i++) { - int l = i - 1; - while (l >= 0 && heights[l] >= heights[i]) { - l = leftLessMin[l]; - } - leftLessMin[i] = l; - } - - int[] rightLessMin = new int[heights.length]; - rightLessMin[heights.length - 1] = heights.length; - for (int i = heights.length - 2; i >= 0; i--) { - int r = i + 1; - while (r <= heights.length - 1 && heights[r] >= heights[i]) { - r = rightLessMin[r]; - } - rightLessMin[i] = r; - } - int maxArea = 0; - for (int i = 0; i < heights.length; i++) { - int area = (rightLessMin[i] - leftLessMin[i] - 1) * heights[i]; - maxArea = Math.max(area, maxArea); - } - return maxArea; -} -``` - -时间复杂度:O(mn)。 - -空间复杂度:O(n)。 - -# 解法三 - -解法二中套用的栈的解法,我们其实可以不用调用函数,而是把栈糅合到原来求 heights 中。因为栈的话并不是一次性需要所有的高度,所以可以求出一个高度,然后就操作栈。 - -```java -public int maximalRectangle(char[][] matrix) { - if (matrix.length == 0) { - return 0; - } - int[] heights = new int[matrix[0].length + 1]; //小技巧后边讲 - int maxArea = 0; - for (int row = 0; row < matrix.length; row++) { - Stack stack = new Stack(); - heights[matrix[0].length] = 0; - //每求一个高度就进行栈的操作 - for (int col = 0; col <= matrix[0].length; col++) { - if (col < matrix[0].length) { //多申请了 1 个元素,所以要判断 - if (matrix[row][col] == '1') { - heights[col] += 1; - } else { - heights[col] = 0; - } - } - if (stack.isEmpty() || heights[col] >= heights[stack.peek()]) { - stack.push(col); - } else { - //每次要判断新的栈顶是否高于当前元素 - while (!stack.isEmpty() && heights[col] < heights[stack.peek()]) { - int height = heights[stack.pop()]; - int leftLessMin = stack.isEmpty() ? -1 : stack.peek(); - int RightLessMin = col; - int area = (RightLessMin - leftLessMin - 1) * height; - maxArea = Math.max(area, maxArea); - } - stack.push(col); - } - } - - } - return maxArea; -} -``` - -时间复杂度:O(mn)。 - -空间复杂度:O(n)。 - -里边有一个小技巧,[84 题]() 的栈解法中,我们用了两个 while 循环,第二个 while 循环用来解决遍历完元素栈不空的情况。其实,我们注意到两个 while 循环的逻辑完全一样的。所以我们可以通过一些操作,使得遍历结束后,依旧进第一个 while 循环,从而剩下了第 2 个 while 循环,代码看起来会更简洁。 - -那就是 heights 多申请一个元素,赋值为 0。这样最后一次遍历的时候,栈顶肯定会大于当前元素,所以就进入了第一个 while 循环。 - -# 解法四 动态规划 - -参考[这里](),这是 leetcode Solution 中投票最高的,但比较难理解,但如果结合 84 题去想的话就很容易了。 - -解法二中,用了 84 题的两个解法,解法三中我们把栈糅合进了原算法,那么另一种可以一样的思路吗?不行!因为栈不要求所有的高度,可以边更新,边处理。而另一种,是利用两个数组, leftLessMin [ ] 和 rightLessMin [ ]。而这两个数组的更新,是需要所有高度的。 - -解法二中,我们更新一次 heights,就利用之前的算法,求一遍 leftLessMin [ ] 和 rightLessMin [ ],然后更新面积。而其实,我们求 leftLessMin [ ] 和 rightLessMin [ ] 可以利用之前的 leftLessMin [ ] 和 rightLessMin [ ] 来更新本次的。 - -我们回想一下 leftLessMin [ ] 和 rightLessMin [ ] 的含义, leftLessMin [ i ] 代表左边第一个比当前柱子矮的下标,如下图橙色柱子时当前遍历的柱子。rightLessMin [ ] 时右边第一个。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/84_5.jpg) - -left 和 right 是对称关系,下边只考虑 left 的求法。 - -如下图,如果当前新增的层全部是 1,当然这时最完美的情况,那么 leftLessMin [ ] 根本不需要改变。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/85_8.jpg) - -然而事实是残酷的,一定会有 0 的出现。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/85_9.jpg) - -我们考虑最后一个柱子的更新。上一层的 leftLessMin = 1,也就是蓝色 0 的位置是第一个比它低的柱子。但是在当前层,由于中间出现了 0。所以不再是之前的 leftLessMin ,而是和上次出现 0 的位置进行比较(因为 0 一定比当前柱子小),谁的下标大,更接近当前柱子,就选择谁。上图中出现 0 的位置是 2,之前的 leftLessMin 是 1,选一个较大的,那就是 2 了。 - -```java -public int maximalRectangle4(char[][] matrix) { - if (matrix.length == 0) { - return 0; - } - int maxArea = 0; - int cols = matrix[0].length; - int[] leftLessMin = new int[cols]; - int[] rightLessMin = new int[cols]; - Arrays.fill(leftLessMin, -1); //初始化为 -1,也就是最左边 - Arrays.fill(rightLessMin, cols); //初始化为 cols,也就是最右边 - int[] heights = new int[cols]; - for (int row = 0; row < matrix.length; row++) { - //更新所有高度 - for (int col = 0; col < cols; col++) { - if (matrix[row][col] == '1') { - heights[col] += 1; - } else { - heights[col] = 0; - } - } - //更新所有leftLessMin - int boundary = -1; //记录上次出现 0 的位置 - for (int col = 0; col < cols; col++) { - if (matrix[row][col] == '1') { - //和上次出现 0 的位置比较 - leftLessMin[col] = Math.max(leftLessMin[col], boundary); - } else { - //当前是 0 代表当前高度是 0,所以初始化为 -1,防止对下次循环的影响 - leftLessMin[col] = -1; - //更新 0 的位置 - boundary = col; - } - } - //右边同理 - boundary = cols; - for (int col = cols - 1; col >= 0; col--) { - if (matrix[row][col] == '1') { - rightLessMin[col] = Math.min(rightLessMin[col], boundary); - } else { - rightLessMin[col] = cols; - boundary = col; - } - } - - //更新所有面积 - for (int col = cols - 1; col >= 0; col--) { - int area = (rightLessMin[col] - leftLessMin[col] - 1) * heights[col]; - maxArea = Math.max(area, maxArea); - } - - } - return maxArea; - -} - -``` - -时间复杂度:O(mn)。 - -空间复杂度:O(n)。 - -# 总 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/85.jpg) + +给一个只有 0 和 1 的矩阵,输出一个最大的矩形的面积,这个矩形里边只含有 1。 + +# 解法一 暴力破解 + +参考[这里](),遍历每个点,求以这个点为矩阵右下角的所有矩阵面积。如下图的两个例子,橙色是当前遍历的点,然后虚线框圈出的矩阵是其中一个矩阵。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/85_2.jpg) + +怎么找出这样的矩阵呢?如下图,如果我们知道了以这个点结尾的连续 1 的个数的话,问题就变得简单了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/85_3.jpg) + +1. 首先求出高度是 1 的矩形面积,也就是它自身的数,也就是上图以橙色的 4 结尾的 「1234」的那个矩形,面积就是 4。 + +2. 然后向上扩展一行,高度增加一,选出当前列最小的数字,作为矩阵的宽,如上图,当前列中有 `2` 和 `4` ,那么,就将 `2` 作为矩形的宽,求出面积,对应上图的矩形圈出的部分。 + +3. 然后继续向上扩展,重复步骤 2。 + +按照上边的方法,遍历所有的点,以当前点为矩阵的右下角,求出所有的矩阵就可以了。下图是某一个点的过程。 + +以橙色的点为右下角,高度为 1。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/85_4.jpg) + +高度为 2。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/85_5.jpg) + +高度为 3。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/85_6.jpg) + +代码的话,把求每个点累计的连续 `1` 的个数用 `width` 保存,同时把求最大矩形的面积和求 `width`融合到同一个循环中。 + +```java +public int maximalRectangle(char[][] matrix) { + if (matrix.length == 0) { + return 0; + } + //保存以当前数字结尾的连续 1 的个数 + int[][] width = new int[matrix.length][matrix[0].length]; + int maxArea = 0; + //遍历每一行 + for (int row = 0; row < matrix.length; row++) { + for (int col = 0; col < matrix[0].length; col++) { + //更新 width + if (matrix[row][col] == '1') { + if (col == 0) { + width[row][col] = 1; + } else { + width[row][col] = width[row][col - 1] + 1; + } + } else { + width[row][col] = 0; + } + //记录所有行中最小的数 + int minWidth = width[row][col]; + //向上扩展行 + for (int up_row = row; up_row >= 0; up_row--) { + int height = row - up_row + 1; + //找最小的数作为矩阵的宽 + minWidth = Math.min(minWidth, width[up_row][col]); + //更新面积 + maxArea = Math.max(maxArea, height * minWidth); + } + } + } + return maxArea; +} +``` + +时间复杂度:O(m²n)。 + +空间复杂度:O(mn)。 + +# 解法二 + +参考[这里](),接下来的解法,会让这道题变得异常简单。还记得 [84 题]()吗?求一个直方图矩形的最大面积。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/84.png) + +大家可以先做 [84 题](),然后回来考虑这道题。 + +再想一下这个题,看下边的橙色的部分,这完全就是上一道题呀! + +![](https://windliang.oss-cn-beijing.aliyuncs.com/85_7.jpg) + +算法有了,就是求出每一层的 heights[] 然后传给上一题的函数就可以了。 + +利用上一题的栈解法。 + +```java +public int maximalRectangle(char[][] matrix) { + if (matrix.length == 0) { + return 0; + } + int[] heights = new int[matrix[0].length]; + int maxArea = 0; + for (int row = 0; row < matrix.length; row++) { + //遍历每一列,更新高度 + for (int col = 0; col < matrix[0].length; col++) { + if (matrix[row][col] == '1') { + heights[col] += 1; + } else { + heights[col] = 0; + } + } + //调用上一题的解法,更新函数 + maxArea = Math.max(maxArea, largestRectangleArea(heights)); + } + return maxArea; +} + +public int largestRectangleArea(int[] heights) { + int maxArea = 0; + Stack stack = new Stack<>(); + int p = 0; + while (p < heights.length) { + //栈空入栈 + if (stack.isEmpty()) { + stack.push(p); + p++; + } else { + int top = stack.peek(); + //当前高度大于栈顶,入栈 + if (heights[p] >= heights[top]) { + stack.push(p); + p++; + } else { + //保存栈顶高度 + int height = heights[stack.pop()]; + //左边第一个小于当前柱子的下标 + int leftLessMin = stack.isEmpty() ? -1 : stack.peek(); + //右边第一个小于当前柱子的下标 + int RightLessMin = p; + //计算面积 + int area = (RightLessMin - leftLessMin - 1) * height; + maxArea = Math.max(area, maxArea); + } + } + } + while (!stack.isEmpty()) { + //保存栈顶高度 + int height = heights[stack.pop()]; + //左边第一个小于当前柱子的下标 + int leftLessMin = stack.isEmpty() ? -1 : stack.peek(); + //右边没有小于当前高度的柱子,所以赋值为数组的长度便于计算 + int RightLessMin = heights.length; + int area = (RightLessMin - leftLessMin - 1) * height; + maxArea = Math.max(area, maxArea); + } + return maxArea; +} +``` + +时间复杂度:O(mn)。 + +空间复杂度:O(n)。 + +利用上一题的解法四。 + +```java +public int maximalRectangle(char[][] matrix) { + if (matrix.length == 0) { + return 0; + } + int[] heights = new int[matrix[0].length]; + int maxArea = 0; + for (int row = 0; row < matrix.length; row++) { + //遍历每一列,更新高度 + for (int col = 0; col < matrix[0].length; col++) { + if (matrix[row][col] == '1') { + heights[col] += 1; + } else { + heights[col] = 0; + } + } + //调用上一题的解法,更新函数 + maxArea = Math.max(maxArea, largestRectangleArea(heights)); + } + return maxArea; +} + +public int largestRectangleArea(int[] heights) { + if (heights.length == 0) { + return 0; + } + int[] leftLessMin = new int[heights.length]; + leftLessMin[0] = -1; + for (int i = 1; i < heights.length; i++) { + int l = i - 1; + while (l >= 0 && heights[l] >= heights[i]) { + l = leftLessMin[l]; + } + leftLessMin[i] = l; + } + + int[] rightLessMin = new int[heights.length]; + rightLessMin[heights.length - 1] = heights.length; + for (int i = heights.length - 2; i >= 0; i--) { + int r = i + 1; + while (r <= heights.length - 1 && heights[r] >= heights[i]) { + r = rightLessMin[r]; + } + rightLessMin[i] = r; + } + int maxArea = 0; + for (int i = 0; i < heights.length; i++) { + int area = (rightLessMin[i] - leftLessMin[i] - 1) * heights[i]; + maxArea = Math.max(area, maxArea); + } + return maxArea; +} +``` + +时间复杂度:O(mn)。 + +空间复杂度:O(n)。 + +# 解法三 + +解法二中套用的栈的解法,我们其实可以不用调用函数,而是把栈糅合到原来求 heights 中。因为栈的话并不是一次性需要所有的高度,所以可以求出一个高度,然后就操作栈。 + +```java +public int maximalRectangle(char[][] matrix) { + if (matrix.length == 0) { + return 0; + } + int[] heights = new int[matrix[0].length + 1]; //小技巧后边讲 + int maxArea = 0; + for (int row = 0; row < matrix.length; row++) { + Stack stack = new Stack(); + heights[matrix[0].length] = 0; + //每求一个高度就进行栈的操作 + for (int col = 0; col <= matrix[0].length; col++) { + if (col < matrix[0].length) { //多申请了 1 个元素,所以要判断 + if (matrix[row][col] == '1') { + heights[col] += 1; + } else { + heights[col] = 0; + } + } + if (stack.isEmpty() || heights[col] >= heights[stack.peek()]) { + stack.push(col); + } else { + //每次要判断新的栈顶是否高于当前元素 + while (!stack.isEmpty() && heights[col] < heights[stack.peek()]) { + int height = heights[stack.pop()]; + int leftLessMin = stack.isEmpty() ? -1 : stack.peek(); + int RightLessMin = col; + int area = (RightLessMin - leftLessMin - 1) * height; + maxArea = Math.max(area, maxArea); + } + stack.push(col); + } + } + + } + return maxArea; +} +``` + +时间复杂度:O(mn)。 + +空间复杂度:O(n)。 + +里边有一个小技巧,[84 题]() 的栈解法中,我们用了两个 while 循环,第二个 while 循环用来解决遍历完元素栈不空的情况。其实,我们注意到两个 while 循环的逻辑完全一样的。所以我们可以通过一些操作,使得遍历结束后,依旧进第一个 while 循环,从而剩下了第 2 个 while 循环,代码看起来会更简洁。 + +那就是 heights 多申请一个元素,赋值为 0。这样最后一次遍历的时候,栈顶肯定会大于当前元素,所以就进入了第一个 while 循环。 + +# 解法四 动态规划 + +参考[这里](),这是 leetcode Solution 中投票最高的,但比较难理解,但如果结合 84 题去想的话就很容易了。 + +解法二中,用了 84 题的两个解法,解法三中我们把栈糅合进了原算法,那么另一种可以一样的思路吗?不行!因为栈不要求所有的高度,可以边更新,边处理。而另一种,是利用两个数组, leftLessMin [ ] 和 rightLessMin [ ]。而这两个数组的更新,是需要所有高度的。 + +解法二中,我们更新一次 heights,就利用之前的算法,求一遍 leftLessMin [ ] 和 rightLessMin [ ],然后更新面积。而其实,我们求 leftLessMin [ ] 和 rightLessMin [ ] 可以利用之前的 leftLessMin [ ] 和 rightLessMin [ ] 来更新本次的。 + +我们回想一下 leftLessMin [ ] 和 rightLessMin [ ] 的含义, leftLessMin [ i ] 代表左边第一个比当前柱子矮的下标,如下图橙色柱子时当前遍历的柱子。rightLessMin [ ] 时右边第一个。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/84_5.jpg) + +left 和 right 是对称关系,下边只考虑 left 的求法。 + +如下图,如果当前新增的层全部是 1,当然这时最完美的情况,那么 leftLessMin [ ] 根本不需要改变。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/85_8.jpg) + +然而事实是残酷的,一定会有 0 的出现。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/85_9.jpg) + +我们考虑最后一个柱子的更新。上一层的 leftLessMin = 1,也就是蓝色 0 的位置是第一个比它低的柱子。但是在当前层,由于中间出现了 0。所以不再是之前的 leftLessMin ,而是和上次出现 0 的位置进行比较(因为 0 一定比当前柱子小),谁的下标大,更接近当前柱子,就选择谁。上图中出现 0 的位置是 2,之前的 leftLessMin 是 1,选一个较大的,那就是 2 了。 + +```java +public int maximalRectangle4(char[][] matrix) { + if (matrix.length == 0) { + return 0; + } + int maxArea = 0; + int cols = matrix[0].length; + int[] leftLessMin = new int[cols]; + int[] rightLessMin = new int[cols]; + Arrays.fill(leftLessMin, -1); //初始化为 -1,也就是最左边 + Arrays.fill(rightLessMin, cols); //初始化为 cols,也就是最右边 + int[] heights = new int[cols]; + for (int row = 0; row < matrix.length; row++) { + //更新所有高度 + for (int col = 0; col < cols; col++) { + if (matrix[row][col] == '1') { + heights[col] += 1; + } else { + heights[col] = 0; + } + } + //更新所有leftLessMin + int boundary = -1; //记录上次出现 0 的位置 + for (int col = 0; col < cols; col++) { + if (matrix[row][col] == '1') { + //和上次出现 0 的位置比较 + leftLessMin[col] = Math.max(leftLessMin[col], boundary); + } else { + //当前是 0 代表当前高度是 0,所以初始化为 -1,防止对下次循环的影响 + leftLessMin[col] = -1; + //更新 0 的位置 + boundary = col; + } + } + //右边同理 + boundary = cols; + for (int col = cols - 1; col >= 0; col--) { + if (matrix[row][col] == '1') { + rightLessMin[col] = Math.min(rightLessMin[col], boundary); + } else { + rightLessMin[col] = cols; + boundary = col; + } + } + + //更新所有面积 + for (int col = cols - 1; col >= 0; col--) { + int area = (rightLessMin[col] - leftLessMin[col] - 1) * heights[col]; + maxArea = Math.max(area, maxArea); + } + + } + return maxArea; + +} + +``` + +时间复杂度:O(mn)。 + +空间复杂度:O(n)。 + +# 总 + 有时候,如果可以把题抽象到已解决的问题当中去,可以大大的简化问题,很酷! \ No newline at end of file diff --git a/leetCode-86-Partition-List.md b/leetCode-86-Partition-List.md index e31c5a1db..2684e9439 100644 --- a/leetCode-86-Partition-List.md +++ b/leetCode-86-Partition-List.md @@ -1,121 +1,121 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/86.jpg) - -题目描述的很难理解,其实回想一下快排就很好理解了。就是快排的分区,将链表分成了两部分,一部分的数字全部小于分区点 x,另一部分全部大于等于分区点 x。最后就是 1 2 2 和 4 3 5 两部分。 - -# 解法一 - -回顾下快排的解法,快排中我们分区用了两个指针,一个指针表示该指针前边的数都小于分区点。另一个指针遍历数组。 - -``` -1 4 3 2 5 2 x = 3 - ^ ^ - i j -i 表示前边的数都小于分区点 3, j 表示当前遍历正在遍历的点 -如果 j 当前指向的数小于分区点,就和 i 指向的点交换位置,i 后移 - -1 2 3 4 5 2 x = 3 - ^ ^ - i j - -然后继续遍历就可以了。 -``` - -这道题无非是换成了链表,而且题目要求不能改变数字的相对位置。所以我们肯定不能用交换的策略了,更何况链表交换也比较麻烦,其实我们直接用插入就可以了。 - -同样的,用一个指针记录当前小于分区点的链表的末尾,用另一个指针遍历链表,每次遇到小于分区点的数,就把它插入到记录的链表末尾,并且更新末尾指针。dummy 哨兵节点,减少边界情况的判断。 - -```java -public ListNode partition(ListNode head, int x) { - ListNode dummy = new ListNode(0); - dummy.next = head; - ListNode tail = null; - head = dummy; - //找到第一个大于等于分区点的节点,tail 指向它的前边 - while (head.next != null) { - if (head.next.val >= x) { - tail = head; - head = head.next; - break; - }else { - head = head.next; - } - } - while (head.next != null) { - //如果当前节点小于分区点,就把它插入到 tail 的后边 - if (head.next.val < x) { - //拿出要插入的节点 - ListNode move = head.next; - //将要插入的结点移除 - head.next = move.next; - //将 move 插入到 tail 后边 - move.next = tail.next; - tail.next = move; - //更新 tail - tail = move; - }else{ - head = head.next; - } - - } - return dummy.next; -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(1)。 - -# 解法二 - -[官方]()给出的 solution 也许更好理解一些。 - -我们知道,快排中之所以用相对不好理解的双指针,就是为了减少空间复杂度,让我们想一下最直接的方法。new 两个数组,一个数组保存小于分区点的数,另一个数组保存大于等于分区点的数,然后把两个数组结合在一起就可以了。 - -```java -1 4 3 2 5 2 x = 3 -min = {1 2 2} -max = {4 3 5} -接在一起 -ans = {1 2 2 4 3 5} -``` - -数组由于需要多浪费空间,而没有采取这种思路,但是链表就不一样了呀,它并不需要开辟新的空间,而只改变指针就可以了。 - -```java -public ListNode partition(ListNode head, int x) { - //小于分区点的链表 - ListNode min_head = new ListNode(0); - ListNode min = min_head; - //大于等于分区点的链表 - ListNode max_head = new ListNode(0); - ListNode max = max_head; - - //遍历整个链表 - while (head != null) { - if (head.val < x) { - min.next = head; - min = min.next; - } else { - max.next = head; - max = max.next; - } - - head = head.next; - } - max.next = null; //这步不要忘记,不然链表就出现环了 - //两个链表接起来 - min.next = max_head.next; - - return min_head.next; -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(1)。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/86.jpg) + +题目描述的很难理解,其实回想一下快排就很好理解了。就是快排的分区,将链表分成了两部分,一部分的数字全部小于分区点 x,另一部分全部大于等于分区点 x。最后就是 1 2 2 和 4 3 5 两部分。 + +# 解法一 + +回顾下快排的解法,快排中我们分区用了两个指针,一个指针表示该指针前边的数都小于分区点。另一个指针遍历数组。 + +``` +1 4 3 2 5 2 x = 3 + ^ ^ + i j +i 表示前边的数都小于分区点 3, j 表示当前遍历正在遍历的点 +如果 j 当前指向的数小于分区点,就和 i 指向的点交换位置,i 后移 + +1 2 3 4 5 2 x = 3 + ^ ^ + i j + +然后继续遍历就可以了。 +``` + +这道题无非是换成了链表,而且题目要求不能改变数字的相对位置。所以我们肯定不能用交换的策略了,更何况链表交换也比较麻烦,其实我们直接用插入就可以了。 + +同样的,用一个指针记录当前小于分区点的链表的末尾,用另一个指针遍历链表,每次遇到小于分区点的数,就把它插入到记录的链表末尾,并且更新末尾指针。dummy 哨兵节点,减少边界情况的判断。 + +```java +public ListNode partition(ListNode head, int x) { + ListNode dummy = new ListNode(0); + dummy.next = head; + ListNode tail = null; + head = dummy; + //找到第一个大于等于分区点的节点,tail 指向它的前边 + while (head.next != null) { + if (head.next.val >= x) { + tail = head; + head = head.next; + break; + }else { + head = head.next; + } + } + while (head.next != null) { + //如果当前节点小于分区点,就把它插入到 tail 的后边 + if (head.next.val < x) { + //拿出要插入的节点 + ListNode move = head.next; + //将要插入的结点移除 + head.next = move.next; + //将 move 插入到 tail 后边 + move.next = tail.next; + tail.next = move; + //更新 tail + tail = move; + }else{ + head = head.next; + } + + } + return dummy.next; +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(1)。 + +# 解法二 + +[官方]()给出的 solution 也许更好理解一些。 + +我们知道,快排中之所以用相对不好理解的双指针,就是为了减少空间复杂度,让我们想一下最直接的方法。new 两个数组,一个数组保存小于分区点的数,另一个数组保存大于等于分区点的数,然后把两个数组结合在一起就可以了。 + +```java +1 4 3 2 5 2 x = 3 +min = {1 2 2} +max = {4 3 5} +接在一起 +ans = {1 2 2 4 3 5} +``` + +数组由于需要多浪费空间,而没有采取这种思路,但是链表就不一样了呀,它并不需要开辟新的空间,而只改变指针就可以了。 + +```java +public ListNode partition(ListNode head, int x) { + //小于分区点的链表 + ListNode min_head = new ListNode(0); + ListNode min = min_head; + //大于等于分区点的链表 + ListNode max_head = new ListNode(0); + ListNode max = max_head; + + //遍历整个链表 + while (head != null) { + if (head.val < x) { + min.next = head; + min = min.next; + } else { + max.next = head; + max = max.next; + } + + head = head.next; + } + max.next = null; //这步不要忘记,不然链表就出现环了 + //两个链表接起来 + min.next = max_head.next; + + return min_head.next; +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(1)。 + +# 总 + 做完了 84、85 连续两个 hard 后,终于可以做个链表压压惊了。本质上就是快排的分区,而在当时被抛弃的用两个数组分别保存,最 naive 的方法,用到链表这里却显示出了它的简洁。 \ No newline at end of file diff --git a/leetCode-87-Scramble-String.md b/leetCode-87-Scramble-String.md index f473976dd..587a60e4b 100644 --- a/leetCode-87-Scramble-String.md +++ b/leetCode-87-Scramble-String.md @@ -1,190 +1,190 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/87.png) - -把一个字符串按照树的形状,分成两部分,分成两部分...直到达到叶子节点。并且可以多次交换非叶子节点的两个子树,最后从左到右读取叶子节点,记为生成的字符串。题目是给两个字符串 S1 和 S2,然后问 S2 是否是 S1 经过上述方式生成的。 - -# 解法一 递归 - -开始的时候,由于给出的图示很巧都是平均分的,我以为只能平均分字符串,看了[这里](),明白其实可以任意位置把字符串分成两部分,这里需要注意一下。 - -这道题很容易想到用递归的思想去解,假如两个字符串 great 和 rgeat。考虑其中的一种切割方式。 - -第 1 种情况:S1 切割为两部分,然后进行若干步切割交换,最后判断两个子树分别是否能变成 S2 的两部分。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/87_2.jpg) - -第 2 种情况:S1 切割并且交换为两部分,然后进行若干步切割交换,最后判断两个子树是否能变成 S2 的两部分。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/87_3.jpg) - -上边是一种切割方式,我们只需要遍历所有的切割点即可。 - -```java -public boolean isScramble(String s1, String s2) { - if (s1.length() != s2.length()) { - return false; - } - if (s1.equals(s2)) { - return true; - } - - //判断两个字符串每个字母出现的次数是否一致 - int[] letters = new int[26]; - for (int i = 0; i < s1.length(); i++) { - letters[s1.charAt(i) - 'a']++; - letters[s2.charAt(i) - 'a']--; - } - //如果两个字符串的字母出现不一致直接返回 false - for (int i = 0; i < 26; i++) { - if (letters[i] != 0) { - return false; - } - } - - //遍历每个切割位置 - for (int i = 1; i < s1.length(); i++) { - //对应情况 1 ,判断 S1 的子树能否变为 S2 相应部分 - if (isScramble(s1.substring(0, i), s2.substring(0, i)) && isScramble(s1.substring(i), s2.substring(i))) { - return true; - } - //对应情况 2 ,S1 两个子树先进行了交换,然后判断 S1 的子树能否变为 S2 相应部分 - if (isScramble(s1.substring(i), s2.substring(0, s2.length() - i)) && - isScramble(s1.substring(0, i), s2.substring(s2.length() - i)) ) { - return true; - } - } - return false; -} -``` - -时间复杂度: - -空间复杂度: - -当然,我们可以用 memoization 技术,把递归过程中的结果存储起来,如果第二次递归过来,直接返回结果即可,无需重复递归。 - -```java -public boolean isScramble(String s1, String s2) { - HashMap memoization = new HashMap<>(); - return isScrambleRecursion(s1, s2, memoization); -} - -public boolean isScrambleRecursion(String s1, String s2, HashMap memoization) { - //判断之前是否已经有了结果 - int ret = memoization.getOrDefault(s1 + "#" + s2, -1); - if (ret == 1) { - return true; - } else if (ret == 0) { - return false; - } - if (s1.length() != s2.length()) { - memoization.put(s1 + "#" + s2, 0); - return false; - } - if (s1.equals(s2)) { - memoization.put(s1 + "#" + s2, 1); - return true; - } - - int[] letters = new int[26]; - for (int i = 0; i < s1.length(); i++) { - letters[s1.charAt(i) - 'a']++; - letters[s2.charAt(i) - 'a']--; - } - for (int i = 0; i < 26; i++) - if (letters[i] != 0) { - memoization.put(s1 + "#" + s2, 0); - return false; - } - - for (int i = 1; i < s1.length(); i++) { - if (isScramble(s1.substring(0, i), s2.substring(0, i)) && isScramble(s1.substring(i), s2.substring(i))) { - memoization.put(s1 + "#" + s2, 1); - return true; - } - if (isScramble(s1.substring(0, i), s2.substring(s2.length() - i)) - && isScramble(s1.substring(i), s2.substring(0, s2.length() - i))) { - memoization.put(s1 + "#" + s2, 1); - return true; - } - } - memoization.put(s1 + "#" + s2, 0); - return false; - } - -``` - - - -# 解法二 动态规划 - -既然是递归,压栈压栈压栈,出栈出栈出栈,我们可以利用动态规划的思想,省略压栈的过程,直接从底部往上走。 - -我们用 dp [ len ]\[ i \] \[ j \] 来表示 s1[ i, i + len ) 和 s2 [ j, j + len ) 两个字符串是否满足条件。换句话说,就是 s1 从 i 开始的 len 个字符是否能转换为 S2 从 j 开始的 len 个字符。那么解法一的两种情况,递归式可以写作。 - -第 1 种情况,参考下图: 假设左半部分长度是 q,dp [ len ]\[ i \] \[ j \] = dp [ q ]\[ i \] \[ j \] && dp [ len - q ]\[ i + q \] \[ j + q \] 。也就是 S1 的左半部分和 S2 的左半部分以及 S1 的右半部分和 S2 的右半部分。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/87_4.jpg) - -第 2 种情况,参考下图: 假设左半部分长度是 q,那么 dp [ len ]\[ i \] \[ j \] = dp [ q ]\[ i \] \[ j + len - q \] && dp [ len - q ]\[ i + q \] \[ j \] 。也就是 S1 的右半部分和 S2 的左半部分以及 S1 的左半部分和 S2 的右半部分。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/87_5.jpg) - -```java -public boolean isScramble4(String s1, String s2) { - if (s1.length() != s2.length()) { - return false; - } - if (s1.equals(s2)) { - return true; - } - - int[] letters = new int[26]; - for (int i = 0; i < s1.length(); i++) { - letters[s1.charAt(i) - 'a']++; - letters[s2.charAt(i) - 'a']--; - } - for (int i = 0; i < 26; i++) { - if (letters[i] != 0) { - return false; - } - } - - int length = s1.length(); - boolean[][][] dp = new boolean[length + 1][length][length]; - //遍历所有的字符串长度 - for (int len = 1; len <= length; len++) { - //S1 开始的地方 - for (int i = 0; i + len <= length; i++) { - //S2 开始的地方 - for (int j = 0; j + len <= length; j++) { - //长度是 1 无需切割 - if (len == 1) { - dp[len][i][j] = s1.charAt(i) == s2.charAt(j); - } else { - //遍历切割后的左半部分长度 - for (int q = 1; q < len; q++) { - dp[len][i][j] = dp[q][i][j] && dp[len - q][i + q][j + q] - || dp[q][i][j + len - q] && dp[len - q][i + q][j]; - //如果当前是 true 就 break,防止被覆盖为 false - if (dp[len][i][j]) { - break; - } - } - } - } - } - } - return dp[length][0][0]; -} -``` - -时间复杂度:$$O(n^4)$$。 - -空间复杂度:$$O(n^3)$$。 - -# 总 - -有时候太惯性思维了,二分查找做多了,看见树就想二分,这一点需要注意。这里还遇到一个问题时,解法一的 memoization 和解法二的动态规划理论上都会比解法一原始解法快一些,但是在 leetcode 上反而最开始的解法是最快的,这里有些想不通,大家有什么想法可以和我交流下。 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/87.png) + +把一个字符串按照树的形状,分成两部分,分成两部分...直到达到叶子节点。并且可以多次交换非叶子节点的两个子树,最后从左到右读取叶子节点,记为生成的字符串。题目是给两个字符串 S1 和 S2,然后问 S2 是否是 S1 经过上述方式生成的。 + +# 解法一 递归 + +开始的时候,由于给出的图示很巧都是平均分的,我以为只能平均分字符串,看了[这里](),明白其实可以任意位置把字符串分成两部分,这里需要注意一下。 + +这道题很容易想到用递归的思想去解,假如两个字符串 great 和 rgeat。考虑其中的一种切割方式。 + +第 1 种情况:S1 切割为两部分,然后进行若干步切割交换,最后判断两个子树分别是否能变成 S2 的两部分。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/87_2.jpg) + +第 2 种情况:S1 切割并且交换为两部分,然后进行若干步切割交换,最后判断两个子树是否能变成 S2 的两部分。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/87_3.jpg) + +上边是一种切割方式,我们只需要遍历所有的切割点即可。 + +```java +public boolean isScramble(String s1, String s2) { + if (s1.length() != s2.length()) { + return false; + } + if (s1.equals(s2)) { + return true; + } + + //判断两个字符串每个字母出现的次数是否一致 + int[] letters = new int[26]; + for (int i = 0; i < s1.length(); i++) { + letters[s1.charAt(i) - 'a']++; + letters[s2.charAt(i) - 'a']--; + } + //如果两个字符串的字母出现不一致直接返回 false + for (int i = 0; i < 26; i++) { + if (letters[i] != 0) { + return false; + } + } + + //遍历每个切割位置 + for (int i = 1; i < s1.length(); i++) { + //对应情况 1 ,判断 S1 的子树能否变为 S2 相应部分 + if (isScramble(s1.substring(0, i), s2.substring(0, i)) && isScramble(s1.substring(i), s2.substring(i))) { + return true; + } + //对应情况 2 ,S1 两个子树先进行了交换,然后判断 S1 的子树能否变为 S2 相应部分 + if (isScramble(s1.substring(i), s2.substring(0, s2.length() - i)) && + isScramble(s1.substring(0, i), s2.substring(s2.length() - i)) ) { + return true; + } + } + return false; +} +``` + +时间复杂度: + +空间复杂度: + +当然,我们可以用 memoization 技术,把递归过程中的结果存储起来,如果第二次递归过来,直接返回结果即可,无需重复递归。 + +```java +public boolean isScramble(String s1, String s2) { + HashMap memoization = new HashMap<>(); + return isScrambleRecursion(s1, s2, memoization); +} + +public boolean isScrambleRecursion(String s1, String s2, HashMap memoization) { + //判断之前是否已经有了结果 + int ret = memoization.getOrDefault(s1 + "#" + s2, -1); + if (ret == 1) { + return true; + } else if (ret == 0) { + return false; + } + if (s1.length() != s2.length()) { + memoization.put(s1 + "#" + s2, 0); + return false; + } + if (s1.equals(s2)) { + memoization.put(s1 + "#" + s2, 1); + return true; + } + + int[] letters = new int[26]; + for (int i = 0; i < s1.length(); i++) { + letters[s1.charAt(i) - 'a']++; + letters[s2.charAt(i) - 'a']--; + } + for (int i = 0; i < 26; i++) + if (letters[i] != 0) { + memoization.put(s1 + "#" + s2, 0); + return false; + } + + for (int i = 1; i < s1.length(); i++) { + if (isScramble(s1.substring(0, i), s2.substring(0, i)) && isScramble(s1.substring(i), s2.substring(i))) { + memoization.put(s1 + "#" + s2, 1); + return true; + } + if (isScramble(s1.substring(0, i), s2.substring(s2.length() - i)) + && isScramble(s1.substring(i), s2.substring(0, s2.length() - i))) { + memoization.put(s1 + "#" + s2, 1); + return true; + } + } + memoization.put(s1 + "#" + s2, 0); + return false; + } + +``` + + + +# 解法二 动态规划 + +既然是递归,压栈压栈压栈,出栈出栈出栈,我们可以利用动态规划的思想,省略压栈的过程,直接从底部往上走。 + +我们用 dp [ len ]\[ i \] \[ j \] 来表示 s1[ i, i + len ) 和 s2 [ j, j + len ) 两个字符串是否满足条件。换句话说,就是 s1 从 i 开始的 len 个字符是否能转换为 S2 从 j 开始的 len 个字符。那么解法一的两种情况,递归式可以写作。 + +第 1 种情况,参考下图: 假设左半部分长度是 q,dp [ len ]\[ i \] \[ j \] = dp [ q ]\[ i \] \[ j \] && dp [ len - q ]\[ i + q \] \[ j + q \] 。也就是 S1 的左半部分和 S2 的左半部分以及 S1 的右半部分和 S2 的右半部分。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/87_4.jpg) + +第 2 种情况,参考下图: 假设左半部分长度是 q,那么 dp [ len ]\[ i \] \[ j \] = dp [ q ]\[ i \] \[ j + len - q \] && dp [ len - q ]\[ i + q \] \[ j \] 。也就是 S1 的右半部分和 S2 的左半部分以及 S1 的左半部分和 S2 的右半部分。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/87_5.jpg) + +```java +public boolean isScramble4(String s1, String s2) { + if (s1.length() != s2.length()) { + return false; + } + if (s1.equals(s2)) { + return true; + } + + int[] letters = new int[26]; + for (int i = 0; i < s1.length(); i++) { + letters[s1.charAt(i) - 'a']++; + letters[s2.charAt(i) - 'a']--; + } + for (int i = 0; i < 26; i++) { + if (letters[i] != 0) { + return false; + } + } + + int length = s1.length(); + boolean[][][] dp = new boolean[length + 1][length][length]; + //遍历所有的字符串长度 + for (int len = 1; len <= length; len++) { + //S1 开始的地方 + for (int i = 0; i + len <= length; i++) { + //S2 开始的地方 + for (int j = 0; j + len <= length; j++) { + //长度是 1 无需切割 + if (len == 1) { + dp[len][i][j] = s1.charAt(i) == s2.charAt(j); + } else { + //遍历切割后的左半部分长度 + for (int q = 1; q < len; q++) { + dp[len][i][j] = dp[q][i][j] && dp[len - q][i + q][j + q] + || dp[q][i][j + len - q] && dp[len - q][i + q][j]; + //如果当前是 true 就 break,防止被覆盖为 false + if (dp[len][i][j]) { + break; + } + } + } + } + } + } + return dp[length][0][0]; +} +``` + +时间复杂度:$$O(n^4)$$。 + +空间复杂度:$$O(n^3)$$。 + +# 总 + +有时候太惯性思维了,二分查找做多了,看见树就想二分,这一点需要注意。这里还遇到一个问题时,解法一的 memoization 和解法二的动态规划理论上都会比解法一原始解法快一些,但是在 leetcode 上反而最开始的解法是最快的,这里有些想不通,大家有什么想法可以和我交流下。 + diff --git a/leetCode-88-Merge-Sorted-Array.md b/leetCode-88-Merge-Sorted-Array.md index cc6244294..26ef223a5 100644 --- a/leetCode-88-Merge-Sorted-Array.md +++ b/leetCode-88-Merge-Sorted-Array.md @@ -1,190 +1,190 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/88.jpg) - -给两个有序数组,把第二个数组合并到第一个数组中,保持有序。可以注意到第一个数组已经为我们多开辟了第二个数组所需要的空间。 - -# 解法一 直接法 - -简单粗暴,nums1 作为被插入的数组,然后遍历 nums2。用两个指针 i 和 j ,i 指向 nums1 当前判断的数字,j 指向 num2 当前遍历的数字。如果 j 指向的数字小于 i 指向的数字,那么就做插入操作。否则的话后移 i ,找到需要插入的位置 。 - -```java -1 2 3 0 0 0 | 2 5 6 //当前 j 指向的数字不小于 i 指向的数字,无需插入,后移 i -^ ^ -i j - -1 2 3 0 0 0 | 2 5 6 //当前 j 指向的数字不小于 i 指向的数字,无需插入后移 i - ^ ^ - i j - -1 2 3 0 0 0 | 2 5 6 //当前 j 指向的数字小于 i 指向的数字,将当前数字插入到 nums1 中 - ^ ^ - i j - -1 2 2 3 0 0 | 2 5 6 //插入完成后,j 进行后移,同时由于在 i 前边插入了数字,i 后移回到原来的数字 - ^ ^ - i j - -1 2 2 3 0 0 | 2 5 6 //当前 j 指向的数字不小于 i 指向的数字,无需插入后移 i - ^ ^ - i j - -1 2 2 3 0 0 | 2 5 6 //由于 nums1 完成遍历,将剩余的 nums2 直接插入 - ^ ^ - i j - -1 2 2 3 5 6 | 2 5 6 - ^ ^ - i j -``` - - - -```java -public void merge(int[] nums1, int m, int[] nums2, int n) { - int i = 0, j = 0; - //遍历 nums2 - while (j < n) { - //判断 nums1 是否遍历完 - //(nums1 原有的数和当前已经插入的数相加)和 i 进行比较 - if (i == m + j) { - //将剩余的 nums2 插入 - while (j < n) { - nums1[m + j] = nums2[j]; - j++; - } - return; - } - //判断当前 nums2 是否小于 nums1 - if (nums2[j] < nums1[i]) { - //nums1 后移数组,空出位置以便插入 - for (int k = m + j; k > i; k--) { - nums1[k] = nums1[k - 1]; - } - nums1[i] = nums2[j]; - //i 和 j 后移 - j++; - i++; - //当前 nums2 不小于 nums1, i 后移 - }else{ - i++; - } - } -} -``` - -时间复杂度:极端情况下,如果每次都需要插入,那么是 O(n²)。 - -空间复杂度:O(1)。 - -# 解法二 - -两个有序数组的合并,其实就是归并排序中的一个步骤。回想下,我们当时怎么做的。 - -我们当时是新开辟一个和 nums1 + nums2 等大的空间,然后用两个指针遍历 nums1 和 nums2,依次选择小的把它放到新的数组中。 - -这道题中,nums1 其实就是我们最后合并好的大数组,但是如果 nums1 当作上述新开辟的空间,那原来的 nums1 的数字放到哪里呢?放到 nums1 的末尾。这样我们就可以完全按照归并排序中的思路了,用三个指针就可以了。 - -```java -1 2 3 0 0 0 0 | 2 5 6 7 //将 nums1 的数据放到 nums1 的末尾 - -1 2 3 0 1 2 3 | 2 5 6 7 //i 和 j 分别指向两组数据的开头,k 指向代插入位置 -^ ^ ^ -k i j - -1 2 3 0 1 2 3 | 2 5 6 7 //此时 i 指向的数据小,把它插入,然后 i 后移,k 后移 -^ ^ ^ -k i j - -1 2 3 0 1 2 3 | 2 5 6 7 //此时 i 指向的数据小,把它插入,然后 i 后移,k 后移 - ^ ^ ^ - k i j - -1 2 3 0 1 2 3 | 2 5 6 7 //此时 j 指向的数据小,把它插入,然后 j 后移,k 后移 - ^ ^ ^ - k i j - -1 2 2 0 1 2 3 | 2 5 6 7 //此时 i 指向的数据小,把它插入,然后 i 后移,k 后移 - ^ ^ ^ - k i j - -1 2 2 3 1 2 3 | 2 5 6 7 //此时 i 遍历完,把 nums2 全部加入 - ^ ^ ^ - k i j - -1 2 2 3 5 6 7 | 2 5 6 7 - -``` - -```java -public void merge(int[] nums1, int m, int[] nums2, int n) { - //将 nums1 的数字全部移动到末尾 - for (int count = 1; count <= m; count++) { - nums1[m + n - count] = nums1[m - count]; - } - int i = n; //i 从 n 开始 - int j = 0; - int k = 0; - //遍历 nums2 - while (j < n) { - //如果 nums1 遍历结束,将 nums2 直接加入 - if (i == m + n) { - while (j < n) { - nums1[k++] = nums2[j++]; - } - return; - } - //哪个数小就对应的添加哪个数 - if (nums2[j] < nums1[i]) { - nums1[k] = nums2[j++]; - } else { - nums1[k] = nums1[i++]; - } - k++; - } -} -``` - -时间复杂度: O(n)。 - -空间复杂度:O(1)。 - -可以注意到,我们只考虑如果 nums1 遍历结束,将 nums2 直接加入。为什么不考虑如果 nums2 遍历结束,将 nums1 直接加入呢?因为我们最开始的时候已经把 nums1 全部放到了末尾,所以不需要再赋值了。 - -# 解法三 - -本以为自己的解法二已经很机智了,直到看到了[这里](),发现了一个神仙操作。 - -解法二中我们的思路是,把 nums1 当作合并后的大数组,依次从两个序列中选较小的数,此外,为了防止 nums1 原有的数字被覆盖,首先先把他放到了末尾。 - -那么,我们为什么不从 nums1 的末尾开始,依次选两个序列末尾较大的数插入呢?同样是 3 个指针,只不过变成哪个数大就对应的添加哪个数。 - -```java -public void merge3(int[] nums1, int m, int[] nums2, int n) { - int i = m - 1; //从末尾开始 - int j = n - 1; //从末尾开始 - int k = m + n - 1; //从末尾开始 - while (j >= 0) { - if (i < 0) { - while (j >= 0) { - nums1[k--] = nums2[j--]; - } - return; - } - //哪个数大就对应的添加哪个数。 - if (nums1[i] > nums2[j]) { - nums1[k--] = nums1[i--]; - } else { - nums1[k--] = nums2[j--]; - } - } -} -``` - -时间复杂度: O(n)。 - -空间复杂度:O(1)。 - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/88.jpg) + +给两个有序数组,把第二个数组合并到第一个数组中,保持有序。可以注意到第一个数组已经为我们多开辟了第二个数组所需要的空间。 + +# 解法一 直接法 + +简单粗暴,nums1 作为被插入的数组,然后遍历 nums2。用两个指针 i 和 j ,i 指向 nums1 当前判断的数字,j 指向 num2 当前遍历的数字。如果 j 指向的数字小于 i 指向的数字,那么就做插入操作。否则的话后移 i ,找到需要插入的位置 。 + +```java +1 2 3 0 0 0 | 2 5 6 //当前 j 指向的数字不小于 i 指向的数字,无需插入,后移 i +^ ^ +i j + +1 2 3 0 0 0 | 2 5 6 //当前 j 指向的数字不小于 i 指向的数字,无需插入后移 i + ^ ^ + i j + +1 2 3 0 0 0 | 2 5 6 //当前 j 指向的数字小于 i 指向的数字,将当前数字插入到 nums1 中 + ^ ^ + i j + +1 2 2 3 0 0 | 2 5 6 //插入完成后,j 进行后移,同时由于在 i 前边插入了数字,i 后移回到原来的数字 + ^ ^ + i j + +1 2 2 3 0 0 | 2 5 6 //当前 j 指向的数字不小于 i 指向的数字,无需插入后移 i + ^ ^ + i j + +1 2 2 3 0 0 | 2 5 6 //由于 nums1 完成遍历,将剩余的 nums2 直接插入 + ^ ^ + i j + +1 2 2 3 5 6 | 2 5 6 + ^ ^ + i j +``` + + + +```java +public void merge(int[] nums1, int m, int[] nums2, int n) { + int i = 0, j = 0; + //遍历 nums2 + while (j < n) { + //判断 nums1 是否遍历完 + //(nums1 原有的数和当前已经插入的数相加)和 i 进行比较 + if (i == m + j) { + //将剩余的 nums2 插入 + while (j < n) { + nums1[m + j] = nums2[j]; + j++; + } + return; + } + //判断当前 nums2 是否小于 nums1 + if (nums2[j] < nums1[i]) { + //nums1 后移数组,空出位置以便插入 + for (int k = m + j; k > i; k--) { + nums1[k] = nums1[k - 1]; + } + nums1[i] = nums2[j]; + //i 和 j 后移 + j++; + i++; + //当前 nums2 不小于 nums1, i 后移 + }else{ + i++; + } + } +} +``` + +时间复杂度:极端情况下,如果每次都需要插入,那么是 O(n²)。 + +空间复杂度:O(1)。 + +# 解法二 + +两个有序数组的合并,其实就是归并排序中的一个步骤。回想下,我们当时怎么做的。 + +我们当时是新开辟一个和 nums1 + nums2 等大的空间,然后用两个指针遍历 nums1 和 nums2,依次选择小的把它放到新的数组中。 + +这道题中,nums1 其实就是我们最后合并好的大数组,但是如果 nums1 当作上述新开辟的空间,那原来的 nums1 的数字放到哪里呢?放到 nums1 的末尾。这样我们就可以完全按照归并排序中的思路了,用三个指针就可以了。 + +```java +1 2 3 0 0 0 0 | 2 5 6 7 //将 nums1 的数据放到 nums1 的末尾 + +1 2 3 0 1 2 3 | 2 5 6 7 //i 和 j 分别指向两组数据的开头,k 指向代插入位置 +^ ^ ^ +k i j + +1 2 3 0 1 2 3 | 2 5 6 7 //此时 i 指向的数据小,把它插入,然后 i 后移,k 后移 +^ ^ ^ +k i j + +1 2 3 0 1 2 3 | 2 5 6 7 //此时 i 指向的数据小,把它插入,然后 i 后移,k 后移 + ^ ^ ^ + k i j + +1 2 3 0 1 2 3 | 2 5 6 7 //此时 j 指向的数据小,把它插入,然后 j 后移,k 后移 + ^ ^ ^ + k i j + +1 2 2 0 1 2 3 | 2 5 6 7 //此时 i 指向的数据小,把它插入,然后 i 后移,k 后移 + ^ ^ ^ + k i j + +1 2 2 3 1 2 3 | 2 5 6 7 //此时 i 遍历完,把 nums2 全部加入 + ^ ^ ^ + k i j + +1 2 2 3 5 6 7 | 2 5 6 7 + +``` + +```java +public void merge(int[] nums1, int m, int[] nums2, int n) { + //将 nums1 的数字全部移动到末尾 + for (int count = 1; count <= m; count++) { + nums1[m + n - count] = nums1[m - count]; + } + int i = n; //i 从 n 开始 + int j = 0; + int k = 0; + //遍历 nums2 + while (j < n) { + //如果 nums1 遍历结束,将 nums2 直接加入 + if (i == m + n) { + while (j < n) { + nums1[k++] = nums2[j++]; + } + return; + } + //哪个数小就对应的添加哪个数 + if (nums2[j] < nums1[i]) { + nums1[k] = nums2[j++]; + } else { + nums1[k] = nums1[i++]; + } + k++; + } +} +``` + +时间复杂度: O(n)。 + +空间复杂度:O(1)。 + +可以注意到,我们只考虑如果 nums1 遍历结束,将 nums2 直接加入。为什么不考虑如果 nums2 遍历结束,将 nums1 直接加入呢?因为我们最开始的时候已经把 nums1 全部放到了末尾,所以不需要再赋值了。 + +# 解法三 + +本以为自己的解法二已经很机智了,直到看到了[这里](),发现了一个神仙操作。 + +解法二中我们的思路是,把 nums1 当作合并后的大数组,依次从两个序列中选较小的数,此外,为了防止 nums1 原有的数字被覆盖,首先先把他放到了末尾。 + +那么,我们为什么不从 nums1 的末尾开始,依次选两个序列末尾较大的数插入呢?同样是 3 个指针,只不过变成哪个数大就对应的添加哪个数。 + +```java +public void merge3(int[] nums1, int m, int[] nums2, int n) { + int i = m - 1; //从末尾开始 + int j = n - 1; //从末尾开始 + int k = m + n - 1; //从末尾开始 + while (j >= 0) { + if (i < 0) { + while (j >= 0) { + nums1[k--] = nums2[j--]; + } + return; + } + //哪个数大就对应的添加哪个数。 + if (nums1[i] > nums2[j]) { + nums1[k--] = nums1[i--]; + } else { + nums1[k--] = nums2[j--]; + } + } +} +``` + +时间复杂度: O(n)。 + +空间复杂度:O(1)。 + +# 总 + 这道题看起来简单,但用到的思想很经典了。解法二中充分利用已有空间的思想,以及解法三中逆转我们的惯性思维,给定的数组从小到大,然后惯性上习惯从小到大,但如果逆转过来,从大的选,简直是神仙操作了! \ No newline at end of file diff --git a/leetCode-89-Gray-Code.md b/leetCode-89-Gray-Code.md index 0fdbb14b5..11f9530f8 100644 --- a/leetCode-89-Gray-Code.md +++ b/leetCode-89-Gray-Code.md @@ -1,161 +1,161 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/89.jpg) - -生成 n 位格雷码,所谓格雷码,就是连续的两个数字,只有一个 bit 位不同。 - -# 解法一 动态规划 - -按照动态规划或者说递归的思路去想,也就是解决了小问题,怎么解决大问题。 - -我们假设我们有了 n = 2 的解,然后考虑怎么得到 n = 3 的解。 - -```java -n = 2 的解 -00 - 0 -10 - 2 -11 - 3 -01 - 1 -``` - -如果再增加一位,无非是在最高位增加 0 或者 1,考虑先增加 0。由于加的是 0,其实数值并没有变化。 - -```java -n = 3 的解,最高位是 0 -000 - 0 -010 - 2 -011 - 3 -001 - 1 -``` - -再考虑增加 1,在 n = 2 的解基础上在最高位把 1 丢过去? - -```java -n = 3 的解 -000 - 0 -010 - 2 -011 - 3 -001 - 1 -------------- 下面的是新增的 -100 - 4 -110 - 6 -111 - 7 -101 - 5 -``` - -似乎没这么简单哈哈,第 4 行 001 和新增的第 5 行 100,有 3 个 bit 位不同了,当然不可以了。怎么解决呢? - -很简单,第 5 行新增的数据最高位由之前的第 4 行的 0 变成了 1,所以其它位就不要变化了,直接把第 4 行的其它位拉过来,也就是 101。 - -接下来,为了使得第 6 行和第 5 行只有一位不同,由于第 5 行拉的第 4 行的低位,而第 4 行和第 3 行只有一位不同。所以第 6 行可以把第 3 行的低位拿过来。其他行同理,如下图。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/89_2.jpg) - -蓝色部分由于最高位加的是 0 ,所以它的数值和 n = 2 的所有解的情况一样。而橙色部分由于最高位加了 1,所以值的话,就是在其对应的值上加 4,也就是 $$2^2$$,即$$2^{3-1}$$,也就是 1 << ( n - 1) 。所以我们的算法可以用迭代求出来了。 - -所以如果知道了 n = 2 的解的话,如果是 { 0, 1, 3, 2},那么 n = 3 的解就是 { 0, 1, 3, 2, 2 + 4, 3 + 4, 1 + 4, 0 + 4 },即 { 0 1 3 2 6 7 5 4 }。之前的解直接照搬过来,然后倒序把每个数加上 1 << ( n - 1) 添加到结果中即可。 - -```java -public List grayCode(int n) { - List gray = new ArrayList(); - gray.add(0); //初始化 n = 0 的解 - for (int i = 0; i < n; i++) { - int add = 1 << i; //要加的数 - //倒序遍历,并且加上一个值添加到结果中 - for (int j = gray.size() - 1; j >= 0; j--) { - gray.add(gray.get(j) + add); - } - } - return gray; -} -``` - -时间复杂度:$$O(2^n)$$,因为有这么多的结果。 - -空间复杂度:O(1)。 - -# 解法二 直接推导 - -解法一我觉得,在不了解格雷码的情况下,还是可以想到的,下边的话,应该是之前了解过格雷码才写出来的。看下[维基百科]()提供的一个生成格雷码的思路。 - -> 以二进制为 0 值的格雷码为第零项,第一项改变最右边的位元,第二项改变右起第一个为1的位元的左边位元,第三、四项方法同第一、二项,如此反复,即可排列出n个位元的格雷码。 - -以 n = 3 为例。 - -0 0 0 第零项初始化为 0。 - -0 0 **1** 第一项改变上一项最右边的位元 - -0 **1** 1 第二项改变上一项右起第一个为 1 的位元的左边位 - -0 1 **0** 第三项同第一项,改变上一项最右边的位元 - -**1** 1 0 第四项同第二项,改变最上一项右起第一个为 1 的位元的左边位 - -1 1 **1** 第五项同第一项,改变上一项最右边的位元 - -1 **0** 1 第六项同第二项,改变最上一项右起第一个为 1 的位元的左边位 - -1 0 **0** 第七项同第一项,改变上一项最右边的位元 - -思路有了,代码自然也就出来了。 - -```java -public List grayCode2(int n) { - List gray = new ArrayList(); - gray.add(0); //初始化第零项 - for (int i = 1; i < 1 << n; i++) { - //得到上一个的值 - int previous = gray.get(i - 1); - //同第一项的情况 - if (i % 2 == 1) { - previous ^= 1; //和 0000001 做异或,使得最右边一位取反 - gray.add(previous); - //同第二项的情况 - } else { - int temp = previous; - //寻找右边起第第一个为 1 的位元 - for (int j = 0; j < n; j++) { - if ((temp & 1) == 1) { - //和 00001000000 类似这样的数做异或,使得相应位取反 - previous = previous ^ (1 << (j + 1)); - gray.add(previous); - break; - } - temp = temp >> 1; - } - } - } - return gray; -} -``` - -时间复杂度:由于每添加两个数需要找第一个为 1 的位元,需要 O(n),所以$$O(n2^n)$$。 - -空间复杂度:O(1)。 - -# 解法三 公式 - -二进制转成格雷码有一个公式。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/89_3.jpg) - -所以我们遍历 0 到 $$2^n-1$$,然后利用公式转换即可。即最高位保留,其它位是当前位和它的高一位进行异或操作。 - -```java -public List grayCode(int n) { - List gray = new ArrayList(); - for(int binary = 0;binary < 1 << n; binary++){ - gray.add(binary ^ binary >> 1); - } - return gray; -} -``` - -时间复杂度:$$O(2^n)$$,因为有这么多的结果。 - -空间复杂度:O(1)。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/89.jpg) + +生成 n 位格雷码,所谓格雷码,就是连续的两个数字,只有一个 bit 位不同。 + +# 解法一 动态规划 + +按照动态规划或者说递归的思路去想,也就是解决了小问题,怎么解决大问题。 + +我们假设我们有了 n = 2 的解,然后考虑怎么得到 n = 3 的解。 + +```java +n = 2 的解 +00 - 0 +10 - 2 +11 - 3 +01 - 1 +``` + +如果再增加一位,无非是在最高位增加 0 或者 1,考虑先增加 0。由于加的是 0,其实数值并没有变化。 + +```java +n = 3 的解,最高位是 0 +000 - 0 +010 - 2 +011 - 3 +001 - 1 +``` + +再考虑增加 1,在 n = 2 的解基础上在最高位把 1 丢过去? + +```java +n = 3 的解 +000 - 0 +010 - 2 +011 - 3 +001 - 1 +------------- 下面的是新增的 +100 - 4 +110 - 6 +111 - 7 +101 - 5 +``` + +似乎没这么简单哈哈,第 4 行 001 和新增的第 5 行 100,有 3 个 bit 位不同了,当然不可以了。怎么解决呢? + +很简单,第 5 行新增的数据最高位由之前的第 4 行的 0 变成了 1,所以其它位就不要变化了,直接把第 4 行的其它位拉过来,也就是 101。 + +接下来,为了使得第 6 行和第 5 行只有一位不同,由于第 5 行拉的第 4 行的低位,而第 4 行和第 3 行只有一位不同。所以第 6 行可以把第 3 行的低位拿过来。其他行同理,如下图。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/89_2.jpg) + +蓝色部分由于最高位加的是 0 ,所以它的数值和 n = 2 的所有解的情况一样。而橙色部分由于最高位加了 1,所以值的话,就是在其对应的值上加 4,也就是 $$2^2$$,即$$2^{3-1}$$,也就是 1 << ( n - 1) 。所以我们的算法可以用迭代求出来了。 + +所以如果知道了 n = 2 的解的话,如果是 { 0, 1, 3, 2},那么 n = 3 的解就是 { 0, 1, 3, 2, 2 + 4, 3 + 4, 1 + 4, 0 + 4 },即 { 0 1 3 2 6 7 5 4 }。之前的解直接照搬过来,然后倒序把每个数加上 1 << ( n - 1) 添加到结果中即可。 + +```java +public List grayCode(int n) { + List gray = new ArrayList(); + gray.add(0); //初始化 n = 0 的解 + for (int i = 0; i < n; i++) { + int add = 1 << i; //要加的数 + //倒序遍历,并且加上一个值添加到结果中 + for (int j = gray.size() - 1; j >= 0; j--) { + gray.add(gray.get(j) + add); + } + } + return gray; +} +``` + +时间复杂度:$$O(2^n)$$,因为有这么多的结果。 + +空间复杂度:O(1)。 + +# 解法二 直接推导 + +解法一我觉得,在不了解格雷码的情况下,还是可以想到的,下边的话,应该是之前了解过格雷码才写出来的。看下[维基百科]()提供的一个生成格雷码的思路。 + +> 以二进制为 0 值的格雷码为第零项,第一项改变最右边的位元,第二项改变右起第一个为1的位元的左边位元,第三、四项方法同第一、二项,如此反复,即可排列出n个位元的格雷码。 + +以 n = 3 为例。 + +0 0 0 第零项初始化为 0。 + +0 0 **1** 第一项改变上一项最右边的位元 + +0 **1** 1 第二项改变上一项右起第一个为 1 的位元的左边位 + +0 1 **0** 第三项同第一项,改变上一项最右边的位元 + +**1** 1 0 第四项同第二项,改变最上一项右起第一个为 1 的位元的左边位 + +1 1 **1** 第五项同第一项,改变上一项最右边的位元 + +1 **0** 1 第六项同第二项,改变最上一项右起第一个为 1 的位元的左边位 + +1 0 **0** 第七项同第一项,改变上一项最右边的位元 + +思路有了,代码自然也就出来了。 + +```java +public List grayCode2(int n) { + List gray = new ArrayList(); + gray.add(0); //初始化第零项 + for (int i = 1; i < 1 << n; i++) { + //得到上一个的值 + int previous = gray.get(i - 1); + //同第一项的情况 + if (i % 2 == 1) { + previous ^= 1; //和 0000001 做异或,使得最右边一位取反 + gray.add(previous); + //同第二项的情况 + } else { + int temp = previous; + //寻找右边起第第一个为 1 的位元 + for (int j = 0; j < n; j++) { + if ((temp & 1) == 1) { + //和 00001000000 类似这样的数做异或,使得相应位取反 + previous = previous ^ (1 << (j + 1)); + gray.add(previous); + break; + } + temp = temp >> 1; + } + } + } + return gray; +} +``` + +时间复杂度:由于每添加两个数需要找第一个为 1 的位元,需要 O(n),所以$$O(n2^n)$$。 + +空间复杂度:O(1)。 + +# 解法三 公式 + +二进制转成格雷码有一个公式。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/89_3.jpg) + +所以我们遍历 0 到 $$2^n-1$$,然后利用公式转换即可。即最高位保留,其它位是当前位和它的高一位进行异或操作。 + +```java +public List grayCode(int n) { + List gray = new ArrayList(); + for(int binary = 0;binary < 1 << n; binary++){ + gray.add(binary ^ binary >> 1); + } + return gray; +} +``` + +时间复杂度:$$O(2^n)$$,因为有这么多的结果。 + +空间复杂度:O(1)。 + +# 总 + 解法一通过利用大问题化小问题的思路,解决了问题。解法二和解法三需要对格雷码有一定的了解才可以。此外,通过格雷码还可以去解[汉诺塔]()和[九连环]()的问题,大家有兴趣可以搜一下。 \ No newline at end of file diff --git a/leetCode-9-Palindrome-Number.md b/leetCode-9-Palindrome-Number.md index 5631fd234..86d06d4da 100644 --- a/leetCode-9-Palindrome-Number.md +++ b/leetCode-9-Palindrome-Number.md @@ -1,100 +1,100 @@ -# 题目描述(简单难度) - -![](http://windliang.oss-cn-beijing.aliyuncs.com/9_1.jpg) - -判断是不是回文数,负数不是回文数。 - -# 解法一 - -把 int 转成字符串,然后判断是否是回文串做就可以了,缺点是需要额外的空间存储字符串,当然题目也告诉了不能这样,所以 pass 。 - -# 解法二 - -在[第 7 道题](https://leetcode.windliang.cc/leetCode-7-Reverse-Integer.html)我们写了倒置 int 的算法,这里当然可以用到了,只需要判断倒置前后相不相等就可以了。 - -记不记得,当倒置后的数字超出 int 的范围时,我们返回的是 0 ,那么它一定不等于原数,此时一定返回 false 了,这正不正确呢? - -我们只需证明,如果倒置后超出 int 的范围,那么它一定不是回文数字就好了。 - -反证法,我们假设存在这么一个数,倒置后是超出 int 范围的,并且它是回文数字。 - -int 最大为 2147483647 , - -![](http://windliang.oss-cn-beijing.aliyuncs.com/9_2.jpg) - -让我们来讨论这个数可能是多少。 - -有没有可能是最高位大于 2 导致的溢出,比如最高位是 3 ,因为是回文串,所以最低位是 3 ,这就将导致转置前最高位也会是 3 ,所以不可能是这种情况。 - -有没有可能是第 2 高位大于 1 导致的溢出,此时保持最高位不变,假如第 2 高位是 2,因为是回文串,所以个位是 2,十位是 2 ,同样的会导致倒置前超出了 int 的最大值,所以也不可能是这种情况。 - -同理,第 3 高位,第 4,第 5,直线左边的都是上述的情况,所以不可能是前边的位数过大。 - -为了保证这个数是溢出的,前边 5 位必须固定不变了,因为它是回文串,所以直线后的灰色数字就一定是 4 ,而此时不管后边的数字取多少,都不可能是溢出的了。 - -综上,不存在这样一个数,所以可以安心的写代码了。 - -```java -public int reverse(int x) { - int rev = 0; - while (x != 0) { - int pop = x % 10; - x /= 10; - if (rev > Integer.MAX_VALUE / 10) - return 0; - if (rev < Integer.MIN_VALUE / 10) - return 0; - rev = rev * 10 + pop; - } - return rev; -} - -public boolean isPalindrome(int x) { - if (x < 0) { - return false; - } - int rev = reverse(x); - return x == rev; -} -``` - -时间复杂度:和求转置一样,x 有多少位,就循环多少次,所以是 O(log(x)) 。 - -空间复杂度:O(1)。 - -# 解法三 - -其实,我们只需要将右半部分倒置然后和左半部比较就可以了。比如 1221 ,把 21 转置和 12 比较就行了。 - -```java -public boolean isPalindrome(int x) { - if (x < 0) { - return false; - } - int digit = (int) (Math.log(x) / Math.log(10) + 1); //总位数 - int revert = 0; - int pop = 0; - //倒置右半部分 - for (int i = 0; i < digit / 2; i++) { - pop = x % 10; - revert = revert * 10 + pop; - x /= 10; - } - if (digit % 2 == 0 && x == revert) { - return true; - } - //奇数情况 x 除以 10 去除 1 位 - if (digit % 2 != 0 && x / 10 == revert) { - return true; - } - return false; -} -``` - -时间复杂度:循环 x 的总位数的一半次,所以时间复杂度依旧是 O(log(x))。 - -空间复杂度:O(1),常数个变量。 - -# 总结 - +# 题目描述(简单难度) + +![](http://windliang.oss-cn-beijing.aliyuncs.com/9_1.jpg) + +判断是不是回文数,负数不是回文数。 + +# 解法一 + +把 int 转成字符串,然后判断是否是回文串做就可以了,缺点是需要额外的空间存储字符串,当然题目也告诉了不能这样,所以 pass 。 + +# 解法二 + +在[第 7 道题](https://leetcode.windliang.cc/leetCode-7-Reverse-Integer.html)我们写了倒置 int 的算法,这里当然可以用到了,只需要判断倒置前后相不相等就可以了。 + +记不记得,当倒置后的数字超出 int 的范围时,我们返回的是 0 ,那么它一定不等于原数,此时一定返回 false 了,这正不正确呢? + +我们只需证明,如果倒置后超出 int 的范围,那么它一定不是回文数字就好了。 + +反证法,我们假设存在这么一个数,倒置后是超出 int 范围的,并且它是回文数字。 + +int 最大为 2147483647 , + +![](http://windliang.oss-cn-beijing.aliyuncs.com/9_2.jpg) + +让我们来讨论这个数可能是多少。 + +有没有可能是最高位大于 2 导致的溢出,比如最高位是 3 ,因为是回文串,所以最低位是 3 ,这就将导致转置前最高位也会是 3 ,所以不可能是这种情况。 + +有没有可能是第 2 高位大于 1 导致的溢出,此时保持最高位不变,假如第 2 高位是 2,因为是回文串,所以个位是 2,十位是 2 ,同样的会导致倒置前超出了 int 的最大值,所以也不可能是这种情况。 + +同理,第 3 高位,第 4,第 5,直线左边的都是上述的情况,所以不可能是前边的位数过大。 + +为了保证这个数是溢出的,前边 5 位必须固定不变了,因为它是回文串,所以直线后的灰色数字就一定是 4 ,而此时不管后边的数字取多少,都不可能是溢出的了。 + +综上,不存在这样一个数,所以可以安心的写代码了。 + +```java +public int reverse(int x) { + int rev = 0; + while (x != 0) { + int pop = x % 10; + x /= 10; + if (rev > Integer.MAX_VALUE / 10) + return 0; + if (rev < Integer.MIN_VALUE / 10) + return 0; + rev = rev * 10 + pop; + } + return rev; +} + +public boolean isPalindrome(int x) { + if (x < 0) { + return false; + } + int rev = reverse(x); + return x == rev; +} +``` + +时间复杂度:和求转置一样,x 有多少位,就循环多少次,所以是 O(log(x)) 。 + +空间复杂度:O(1)。 + +# 解法三 + +其实,我们只需要将右半部分倒置然后和左半部比较就可以了。比如 1221 ,把 21 转置和 12 比较就行了。 + +```java +public boolean isPalindrome(int x) { + if (x < 0) { + return false; + } + int digit = (int) (Math.log(x) / Math.log(10) + 1); //总位数 + int revert = 0; + int pop = 0; + //倒置右半部分 + for (int i = 0; i < digit / 2; i++) { + pop = x % 10; + revert = revert * 10 + pop; + x /= 10; + } + if (digit % 2 == 0 && x == revert) { + return true; + } + //奇数情况 x 除以 10 去除 1 位 + if (digit % 2 != 0 && x / 10 == revert) { + return true; + } + return false; +} +``` + +时间复杂度:循环 x 的总位数的一半次,所以时间复杂度依旧是 O(log(x))。 + +空间复杂度:O(1),常数个变量。 + +# 总结 + 这几天都比较简单,加油加油加油!。 \ No newline at end of file diff --git a/leetCode-90-SubsetsII.md b/leetCode-90-SubsetsII.md index b1cf1e125..b02ceebcb 100644 --- a/leetCode-90-SubsetsII.md +++ b/leetCode-90-SubsetsII.md @@ -1,264 +1,264 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/90.jpg) - -[78题]()升级版,大家可以先做 78 题。给定一个数组,输出所有它的子数组。区别在于,这道题给定的数组中,出现了重复的数字。 - -直接根据 78 题的思路去做。 - -# 解法一 回溯法 - -这个比较好改,我们只需要判断当前数字和上一个数字是否相同,相同的话跳过即可。当然,要把数字首先进行排序。 - -```java -public List> subsetsWithDup(int[] nums) { - List> ans = new ArrayList<>(); - Arrays.sort(nums); //排序 - getAns(nums, 0, new ArrayList<>(), ans); - return ans; -} - -private void getAns(int[] nums, int start, ArrayList temp, List> ans) { - ans.add(new ArrayList<>(temp)); - for (int i = start; i < nums.length; i++) { - //和上个数字相等就跳过 - if (i > start && nums[i] == nums[i - 1]) { - continue; - } - temp.add(nums[i]); - getAns(nums, i + 1, temp, ans); - temp.remove(temp.size() - 1); - } -} -``` - -时间复杂度: - -空间复杂度: - -# 解法二 迭代法 - -根据[78题]()解法二修改。我们看一下如果直接按照 78 题的思路会出什么问题。之前的思路是,先考虑 0 个数字的所有子串,再考虑 1 个的所有子串,再考虑 2 个的所有子串。而求 n 个的所有子串,就是 【n - 1 的所有子串】和 【n - 1 的所有子串加上 n】。例如, - -```java -数组 [ 1 2 3 ] -[ ]的所有子串 [ ] -[ 1 ] 个的所有子串 [ ] [ 1 ] -[ 1 2 ] 个的所有子串 [ ] [ 1 ] [ 2 ][ 1 2 ] -[ 1 2 3 ] 个的所有子串 [ ] [ 1 ] [ 2 ] [ 1 2 ] [ 3 ] [ 1 3 ] [ 2 3 ] [ 1 2 3 ] -``` - -但是如果有重复的数字,会出现什么问题呢 - -```java -数组 [ 1 2 2 ] -[ ] 的所有子串 [ ] -[ 1 ] 的所有子串 [ ] [ 1 ] -[ 1 2 ] 的所有子串 [ ] [ 1 ] [ 2 ][ 1 2 ] -[ 1 2 2 ] 的所有子串 [ ] [ 1 ] [ 2 ] [ 1 2 ] [ 2 ] [ 1 2 ] [ 2 2 ] [ 1 2 2 ] -``` - -我们发现出现了重复的数组,那么我们可不可以像解法一那样,遇到重复的就跳过这个数字呢?答案是否定的,如果最后一步 [ 1 2 2 ] 增加了 2 ,跳过后,最终答案会缺少 [ 2 2 ]、[ 1 2 2 ] 这两个解。我们仔细观察这两个解是怎么产生的。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/90_2.jpg) - -我们看到第 4 行黑色的部分,重复了,是怎么造成的呢? - -第 4 行新添加的 2 要加到第 3 行的所有解中,而第 3 行的一部分解是旧解,一部分是新解。可以看到,我们黑色部分是由第 3 行的旧解产生的,橙色部分是由新解产生的。 - -而第 1 行到第 2 行,已经在旧解中加入了 2 产生了第 2 行的橙色部分,所以这里如果再在旧解中加 2 产生黑色部分就造成了重复。 - -所以当有重复数字的时候,我们只考虑上一步的新解,算法中用一个指针保存每一步的新解开始的位置即可。 - -```java -public List> subsetsWithDup(int[] nums) { - List> ans = new ArrayList<>(); - ans.add(new ArrayList<>());// 初始化空数组 - Arrays.sort(nums); - int start = 1; //保存新解的开始位置 - for (int i = 0; i < nums.length; i++) { - List> ans_tmp = new ArrayList<>(); - // 遍历之前的所有结果 - for (int j = 0; j < ans.size(); j++) { - List list = ans.get(j); - //如果出现重复数字,就跳过所有旧解 - if (i > 0 && nums[i] == nums[i - 1] && j < start) { - continue; - } - List tmp = new ArrayList<>(list); - tmp.add(nums[i]); // 加入新增数字 - ans_tmp.add(tmp); - } - - start = ans.size(); //更新新解的开始位置 - ans.addAll(ans_tmp); - } - return ans; -} -``` - -时间复杂度: - -空间复杂度:O(1)。 - -还有一种思路,参考[这里](),当有重复数字出现的时候我们不再按照之前的思路走,而是单独考虑这种情况。 - -当有 n 个重复数字出现,其实就是在出现重复数字之前的所有解中,分别加 1 个重复数字, 2 个重复数字,3 个重复数字 ... 什么意思呢,看一个例子。 - -```java -数组 [ 1 2 2 2 ] -[ ]的所有子串 [ ] -[ 1 ] 个的所有子串 [ ] [ 1 ] -然后出现了重复数字 2,那么我们记录重复的次数。然后遍历之前每个解即可 -对于 [ ] 这个解, -加 1 个 2,变成 [ 2 ] -加 2 个 2,变成 [ 2 2 ] -加 3 个 2,变成 [ 2 2 2 ] -对于 [ 1 ] 这个解 -加 1 个 2,变成 [ 1 2 ] -加 2 个 2,变成 [ 1 2 2 ] -加 3 个 2,变成 [ 1 2 2 2 ] -``` - -代码的话,就很好写了。 - -```java -public List> subsetsWithDup(int[] num) { - List> result = new ArrayList>(); - List empty = new ArrayList(); - result.add(empty); - Arrays.sort(num); - - for (int i = 0; i < num.length; i++) { - int dupCount = 0; - //判断当前是否是重复数字,并且记录重复的次数 - while( ((i+1) < num.length) && num[i+1] == num[i]) { - dupCount++; - i++; - } - int prevNum = result.size(); - //遍历之前几个结果的每个解 - for (int j = 0; j < prevNum; j++) { - List element = new ArrayList(result.get(j)); - //每次在上次的结果中多加 1 个重复数字 - for (int t = 0; t <= dupCount; t++) { - element.add(num[i]); //加入当前重复的数字 - result.add(new ArrayList(element)); - } - } - } - return result; -} -``` - -# 解法三 位操作 - -本以为这个思路想不出来怎么去改了,然后看到了[这里]()。 - -回顾一下,这个题的思想就是每一个数字,考虑它的二进制表示。 - -例如,nums = [ 1, 2 , 3 ]。用 1 代表在,0 代表不在。 - -```java -1 2 3 -0 0 0 -> [ ] -0 0 1 -> [ 3] -0 1 0 -> [ 2 ] -0 1 1 -> [ 2 3] -1 0 0 -> [1 ] -1 0 1 -> [1 3] -1 1 0 -> [1 2 ] -1 1 1 -> [1 2 3] -``` - -但是如果有了重复数字,很明显就行不通了。例如对于 nums = [ 1 2 2 2 3 ]。 - -```java -1 2 2 2 3 -0 1 1 0 0 -> [ 2 2 ] -0 1 0 1 0 -> [ 2 2 ] -0 0 1 1 0 -> [ 2 2 ] -``` - -上边三个数产生的数组重复的了。三个中我们只取其中 1 个,取哪个呢?取从重复数字的开头连续的数字。什么意思呢?就是下边的情况是我们所保留的。 - -```java -2 2 2 2 2 -1 0 0 0 0 -> [ 2 ] -1 1 0 0 0 -> [ 2 2 ] -1 1 1 0 0 -> [ 2 2 2 ] -1 1 1 1 0 -> [ 2 2 2 2 ] -1 1 1 1 1 -> [ 2 2 2 2 2 ] -``` - -而对于 [ 2 2 ] 来说,除了 1 1 0 0 0 可以产生,下边的几种情况,都是产生的 [ 2 2 ] - -```java -2 2 2 2 2 -1 1 0 0 0 -> [ 2 2 ] -1 0 1 0 0 -> [ 2 2 ] -0 1 1 0 0 -> [ 2 2 ] -0 1 0 1 0 -> [ 2 2 ] -0 0 0 1 1 -> [ 2 2 ] -...... -``` - -怎么把 1 1 0 0 0 和上边的那么多种情况区分开来呢?我们来看一下出现了重复数字,并且当前是 1 的前一个的二进位。 - -对于 **1** 1 0 0 0 ,是 1。 - -对于 1 **0** 1 0 0 , 是 0。 - -对于 **0** 1 1 0 0 ,是 0。 - -对于 **0** 1 0 1 0 ,是 0。 - -对于 0 0 **0** 1 1 ,是 0。 - -...... - -可以看到只有第一种情况对应的是 1 ,其他情况都是 0。其实除去从开头是连续的 1 的话,就是两种情况。 - -第一种就是,占据了开头,类似于这种 1**0**...1.... - -第二种就是,没有占据开头,类似于这种 **0**...1... - -这两种情况,除了第一位,其他位的 1 的前边一定是 0。所以的话,我们的条件是看出现了重复数字,并且当前位是 1 的前一个的二进位。 - -所以可以改代码了。 - -```java -public List> subsetsWithDup(int[] num) { - Arrays.sort(num); - List> lists = new ArrayList<>(); - int subsetNum = 1< list = new ArrayList<>(); - boolean illegal=false; - for(int j=0;j>j&1)==1){ - //当前是重复数字,并且前一位是 0,跳过这种情况 - if(j>0&&num[j]==num[j-1]&&(i>>(j-1)&1)==0){ - illegal=true; - break; - }else{ - list.add(num[j]); - } - } - } - if(!illegal){ - lists.add(list); - } - - } - return lists; -} -``` - -# 总 - -解法一和解法二怎么改,分析一下比较容易想到。解法三就比较难了,突破口就是选一个特殊的结构做代表,和其他情况区分出来。而从头开始的连续 1 可能就会是我们第一个想到的数,然后分析一下,发现果然可以和其他所有情况区分开来。 - - - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/90.jpg) + +[78题]()升级版,大家可以先做 78 题。给定一个数组,输出所有它的子数组。区别在于,这道题给定的数组中,出现了重复的数字。 + +直接根据 78 题的思路去做。 + +# 解法一 回溯法 + +这个比较好改,我们只需要判断当前数字和上一个数字是否相同,相同的话跳过即可。当然,要把数字首先进行排序。 + +```java +public List> subsetsWithDup(int[] nums) { + List> ans = new ArrayList<>(); + Arrays.sort(nums); //排序 + getAns(nums, 0, new ArrayList<>(), ans); + return ans; +} + +private void getAns(int[] nums, int start, ArrayList temp, List> ans) { + ans.add(new ArrayList<>(temp)); + for (int i = start; i < nums.length; i++) { + //和上个数字相等就跳过 + if (i > start && nums[i] == nums[i - 1]) { + continue; + } + temp.add(nums[i]); + getAns(nums, i + 1, temp, ans); + temp.remove(temp.size() - 1); + } +} +``` + +时间复杂度: + +空间复杂度: + +# 解法二 迭代法 + +根据[78题]()解法二修改。我们看一下如果直接按照 78 题的思路会出什么问题。之前的思路是,先考虑 0 个数字的所有子串,再考虑 1 个的所有子串,再考虑 2 个的所有子串。而求 n 个的所有子串,就是 【n - 1 的所有子串】和 【n - 1 的所有子串加上 n】。例如, + +```java +数组 [ 1 2 3 ] +[ ]的所有子串 [ ] +[ 1 ] 个的所有子串 [ ] [ 1 ] +[ 1 2 ] 个的所有子串 [ ] [ 1 ] [ 2 ][ 1 2 ] +[ 1 2 3 ] 个的所有子串 [ ] [ 1 ] [ 2 ] [ 1 2 ] [ 3 ] [ 1 3 ] [ 2 3 ] [ 1 2 3 ] +``` + +但是如果有重复的数字,会出现什么问题呢 + +```java +数组 [ 1 2 2 ] +[ ] 的所有子串 [ ] +[ 1 ] 的所有子串 [ ] [ 1 ] +[ 1 2 ] 的所有子串 [ ] [ 1 ] [ 2 ][ 1 2 ] +[ 1 2 2 ] 的所有子串 [ ] [ 1 ] [ 2 ] [ 1 2 ] [ 2 ] [ 1 2 ] [ 2 2 ] [ 1 2 2 ] +``` + +我们发现出现了重复的数组,那么我们可不可以像解法一那样,遇到重复的就跳过这个数字呢?答案是否定的,如果最后一步 [ 1 2 2 ] 增加了 2 ,跳过后,最终答案会缺少 [ 2 2 ]、[ 1 2 2 ] 这两个解。我们仔细观察这两个解是怎么产生的。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/90_2.jpg) + +我们看到第 4 行黑色的部分,重复了,是怎么造成的呢? + +第 4 行新添加的 2 要加到第 3 行的所有解中,而第 3 行的一部分解是旧解,一部分是新解。可以看到,我们黑色部分是由第 3 行的旧解产生的,橙色部分是由新解产生的。 + +而第 1 行到第 2 行,已经在旧解中加入了 2 产生了第 2 行的橙色部分,所以这里如果再在旧解中加 2 产生黑色部分就造成了重复。 + +所以当有重复数字的时候,我们只考虑上一步的新解,算法中用一个指针保存每一步的新解开始的位置即可。 + +```java +public List> subsetsWithDup(int[] nums) { + List> ans = new ArrayList<>(); + ans.add(new ArrayList<>());// 初始化空数组 + Arrays.sort(nums); + int start = 1; //保存新解的开始位置 + for (int i = 0; i < nums.length; i++) { + List> ans_tmp = new ArrayList<>(); + // 遍历之前的所有结果 + for (int j = 0; j < ans.size(); j++) { + List list = ans.get(j); + //如果出现重复数字,就跳过所有旧解 + if (i > 0 && nums[i] == nums[i - 1] && j < start) { + continue; + } + List tmp = new ArrayList<>(list); + tmp.add(nums[i]); // 加入新增数字 + ans_tmp.add(tmp); + } + + start = ans.size(); //更新新解的开始位置 + ans.addAll(ans_tmp); + } + return ans; +} +``` + +时间复杂度: + +空间复杂度:O(1)。 + +还有一种思路,参考[这里](),当有重复数字出现的时候我们不再按照之前的思路走,而是单独考虑这种情况。 + +当有 n 个重复数字出现,其实就是在出现重复数字之前的所有解中,分别加 1 个重复数字, 2 个重复数字,3 个重复数字 ... 什么意思呢,看一个例子。 + +```java +数组 [ 1 2 2 2 ] +[ ]的所有子串 [ ] +[ 1 ] 个的所有子串 [ ] [ 1 ] +然后出现了重复数字 2,那么我们记录重复的次数。然后遍历之前每个解即可 +对于 [ ] 这个解, +加 1 个 2,变成 [ 2 ] +加 2 个 2,变成 [ 2 2 ] +加 3 个 2,变成 [ 2 2 2 ] +对于 [ 1 ] 这个解 +加 1 个 2,变成 [ 1 2 ] +加 2 个 2,变成 [ 1 2 2 ] +加 3 个 2,变成 [ 1 2 2 2 ] +``` + +代码的话,就很好写了。 + +```java +public List> subsetsWithDup(int[] num) { + List> result = new ArrayList>(); + List empty = new ArrayList(); + result.add(empty); + Arrays.sort(num); + + for (int i = 0; i < num.length; i++) { + int dupCount = 0; + //判断当前是否是重复数字,并且记录重复的次数 + while( ((i+1) < num.length) && num[i+1] == num[i]) { + dupCount++; + i++; + } + int prevNum = result.size(); + //遍历之前几个结果的每个解 + for (int j = 0; j < prevNum; j++) { + List element = new ArrayList(result.get(j)); + //每次在上次的结果中多加 1 个重复数字 + for (int t = 0; t <= dupCount; t++) { + element.add(num[i]); //加入当前重复的数字 + result.add(new ArrayList(element)); + } + } + } + return result; +} +``` + +# 解法三 位操作 + +本以为这个思路想不出来怎么去改了,然后看到了[这里]()。 + +回顾一下,这个题的思想就是每一个数字,考虑它的二进制表示。 + +例如,nums = [ 1, 2 , 3 ]。用 1 代表在,0 代表不在。 + +```java +1 2 3 +0 0 0 -> [ ] +0 0 1 -> [ 3] +0 1 0 -> [ 2 ] +0 1 1 -> [ 2 3] +1 0 0 -> [1 ] +1 0 1 -> [1 3] +1 1 0 -> [1 2 ] +1 1 1 -> [1 2 3] +``` + +但是如果有了重复数字,很明显就行不通了。例如对于 nums = [ 1 2 2 2 3 ]。 + +```java +1 2 2 2 3 +0 1 1 0 0 -> [ 2 2 ] +0 1 0 1 0 -> [ 2 2 ] +0 0 1 1 0 -> [ 2 2 ] +``` + +上边三个数产生的数组重复的了。三个中我们只取其中 1 个,取哪个呢?取从重复数字的开头连续的数字。什么意思呢?就是下边的情况是我们所保留的。 + +```java +2 2 2 2 2 +1 0 0 0 0 -> [ 2 ] +1 1 0 0 0 -> [ 2 2 ] +1 1 1 0 0 -> [ 2 2 2 ] +1 1 1 1 0 -> [ 2 2 2 2 ] +1 1 1 1 1 -> [ 2 2 2 2 2 ] +``` + +而对于 [ 2 2 ] 来说,除了 1 1 0 0 0 可以产生,下边的几种情况,都是产生的 [ 2 2 ] + +```java +2 2 2 2 2 +1 1 0 0 0 -> [ 2 2 ] +1 0 1 0 0 -> [ 2 2 ] +0 1 1 0 0 -> [ 2 2 ] +0 1 0 1 0 -> [ 2 2 ] +0 0 0 1 1 -> [ 2 2 ] +...... +``` + +怎么把 1 1 0 0 0 和上边的那么多种情况区分开来呢?我们来看一下出现了重复数字,并且当前是 1 的前一个的二进位。 + +对于 **1** 1 0 0 0 ,是 1。 + +对于 1 **0** 1 0 0 , 是 0。 + +对于 **0** 1 1 0 0 ,是 0。 + +对于 **0** 1 0 1 0 ,是 0。 + +对于 0 0 **0** 1 1 ,是 0。 + +...... + +可以看到只有第一种情况对应的是 1 ,其他情况都是 0。其实除去从开头是连续的 1 的话,就是两种情况。 + +第一种就是,占据了开头,类似于这种 1**0**...1.... + +第二种就是,没有占据开头,类似于这种 **0**...1... + +这两种情况,除了第一位,其他位的 1 的前边一定是 0。所以的话,我们的条件是看出现了重复数字,并且当前位是 1 的前一个的二进位。 + +所以可以改代码了。 + +```java +public List> subsetsWithDup(int[] num) { + Arrays.sort(num); + List> lists = new ArrayList<>(); + int subsetNum = 1< list = new ArrayList<>(); + boolean illegal=false; + for(int j=0;j>j&1)==1){ + //当前是重复数字,并且前一位是 0,跳过这种情况 + if(j>0&&num[j]==num[j-1]&&(i>>(j-1)&1)==0){ + illegal=true; + break; + }else{ + list.add(num[j]); + } + } + } + if(!illegal){ + lists.add(list); + } + + } + return lists; +} +``` + +# 总 + +解法一和解法二怎么改,分析一下比较容易想到。解法三就比较难了,突破口就是选一个特殊的结构做代表,和其他情况区分出来。而从头开始的连续 1 可能就会是我们第一个想到的数,然后分析一下,发现果然可以和其他所有情况区分开来。 + + + diff --git a/leetCode-92-Reverse-Linked-ListII.md b/leetCode-92-Reverse-Linked-ListII.md index 61b8eb465..6517249ba 100644 --- a/leetCode-92-Reverse-Linked-ListII.md +++ b/leetCode-92-Reverse-Linked-ListII.md @@ -1,105 +1,105 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/92.jpg) - -给定链表的一个范围,将这个范围内的链表倒置。 - -# 解法一 - -首先找到 m 的位置,记录两端的节点 left1 和 left2 。 - -然后每遍历一个节点,就倒置一个节点。 - -到 n 的位置后,利用之前的 left1 和 left2 完成连接。 - -为了完成链表的倒置需要两个指针 pre 和 head。为了少考虑边界条件,例如 m = 1 的倒置。加一个哨兵节点 dummy。 - -```java -m = 2, n = 4 - -1 2 3 4 5 加入哨兵节点 d,pre 简写 p,head 简写 h - -0 1 2 3 4 5 往后遍历 -^ ^ -d h -p - -0 1 2 3 4 5 此时 h 指向 m 的位置,记录 p 和 h 为 l1 和 l2 -^ ^ ^ -d p h - -0 1 2 3 4 5 然后继续遍历 -^ ^ ^ -d p h - l1 l2 - -0 1 2 3 4 5 开始倒置链表,使得 h 指向 p -^ ^ ^ ^ -d l1 p h - l2 -``` - -当前状态用图形描述 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/92_2.jpg) - -倒转链表,将 h 的 next 指向 p,并且后移 p 和 h。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/92_3.jpg) - -然后上边一步会重复多次,直到 h 到达 n 的位置。当然这道题比较特殊,上图 h 已经到达了 n 的位置。 - -此时,我们需要将 h 指向 p,同时将 l1 指向 h,l2 指向 h.next,使得链表接起来。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/92_4.jpg) - -操作完成,将 dummy.next 返回即可。 - -```java -public ListNode reverseBetween(ListNode head, int m, int n) { - if (m == n) { - return head; - } - ListNode dummy = new ListNode(0); - dummy.next = head; - int count = 0; - ListNode left1 = null; - ListNode left2 = null; - ListNode pre = dummy; - while (head != null) { - count++; - //到达 m,保存 l1 和 l2 - if (count == m) { - left1 = pre; - left2 = head; - } - // m 和 n 之间,倒转链表 - if (count > m && count < n) { - ListNode temp = head.next; - head.next = pre; - pre = head; - head = temp; - continue; - } - //到达 n - if (count == n) { - left2.next = head.next; - head.next = pre; - left1.next = head; - break; - } - //两个指针后移 - head = head.next; - pre = pre.next; - } - return dummy.next; -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(1)。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/92.jpg) + +给定链表的一个范围,将这个范围内的链表倒置。 + +# 解法一 + +首先找到 m 的位置,记录两端的节点 left1 和 left2 。 + +然后每遍历一个节点,就倒置一个节点。 + +到 n 的位置后,利用之前的 left1 和 left2 完成连接。 + +为了完成链表的倒置需要两个指针 pre 和 head。为了少考虑边界条件,例如 m = 1 的倒置。加一个哨兵节点 dummy。 + +```java +m = 2, n = 4 + +1 2 3 4 5 加入哨兵节点 d,pre 简写 p,head 简写 h + +0 1 2 3 4 5 往后遍历 +^ ^ +d h +p + +0 1 2 3 4 5 此时 h 指向 m 的位置,记录 p 和 h 为 l1 和 l2 +^ ^ ^ +d p h + +0 1 2 3 4 5 然后继续遍历 +^ ^ ^ +d p h + l1 l2 + +0 1 2 3 4 5 开始倒置链表,使得 h 指向 p +^ ^ ^ ^ +d l1 p h + l2 +``` + +当前状态用图形描述 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/92_2.jpg) + +倒转链表,将 h 的 next 指向 p,并且后移 p 和 h。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/92_3.jpg) + +然后上边一步会重复多次,直到 h 到达 n 的位置。当然这道题比较特殊,上图 h 已经到达了 n 的位置。 + +此时,我们需要将 h 指向 p,同时将 l1 指向 h,l2 指向 h.next,使得链表接起来。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/92_4.jpg) + +操作完成,将 dummy.next 返回即可。 + +```java +public ListNode reverseBetween(ListNode head, int m, int n) { + if (m == n) { + return head; + } + ListNode dummy = new ListNode(0); + dummy.next = head; + int count = 0; + ListNode left1 = null; + ListNode left2 = null; + ListNode pre = dummy; + while (head != null) { + count++; + //到达 m,保存 l1 和 l2 + if (count == m) { + left1 = pre; + left2 = head; + } + // m 和 n 之间,倒转链表 + if (count > m && count < n) { + ListNode temp = head.next; + head.next = pre; + pre = head; + head = temp; + continue; + } + //到达 n + if (count == n) { + left2.next = head.next; + head.next = pre; + left1.next = head; + break; + } + //两个指针后移 + head = head.next; + pre = pre.next; + } + return dummy.next; +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(1)。 + +# 总 + 考察链表知识,如果对链表很熟悉,在纸上画一画,理清楚怎么指向,很快可以写出来。 \ No newline at end of file diff --git a/leetCode-93-Restore-IP-Addresses.md b/leetCode-93-Restore-IP-Addresses.md index 7fe0df000..dd69748c0 100644 --- a/leetCode-93-Restore-IP-Addresses.md +++ b/leetCode-93-Restore-IP-Addresses.md @@ -1,117 +1,117 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/93.jpg) - -给一个字符串,输出所有的可能的 ip 地址,注意一下,01.1.001.1 类似这种 0 开头的是非法字符串。 - -# 解法一 回溯 递归 DFS - -很类似于刚做过的 [91 题](),对字符串进行划分。这个其实也是划分,划分的次数已经确定了,那就是分为 4 部分。那么就直接用回溯的思想,第一部分可能是 1 位数,然后进入递归。第一部分可能是 2 位数,然后进入递归。第一部分可能是 3 位数,然后进入递归。很好理解,直接看代码理解吧。 - -````java -public List restoreIpAddresses(String s) { - List ans = new ArrayList<>(); //保存最终的所有结果 - getAns(s, 0, new StringBuilder(), ans, 0); - return ans; -} - -/** -* @param: start 字符串开始部分 -* @param: temp 已经划分的部分 -* @param: ans 保存所有的解 -* @param: count 当前已经加入了几部分 -*/ -private void getAns(String s, int start, StringBuilder temp, List ans, int count) { - //如果剩余的长度大于剩下的部分都取 3 位数的长度,那么直接结束 - //例如 s = 121231312312, length = 12 - //当前 start = 1,count 等于 1 - //剩余字符串长度 11,剩余部分 4 - count = 3 部分,最多 3 * 3 是 9 - //所以一定是非法的,直接结束 - if (s.length() - start > 3 * (4 - count)) { - return; - } - //当前刚好到达了末尾 - if (start == s.length()) { - //当前刚好是 4 部分,将结果加入 - if (count == 4) { - ans.add(new String(temp.substring(0, temp.length() - 1))); - } - return; - } - //当前超过末位,或者已经到达了 4 部分结束掉 - if (start > s.length() || count == 4) { - return; - } - //保存的当前的解 - StringBuilder before = new StringBuilder(temp); - - //加入 1 位数 - temp.append(s.charAt(start) + "" + '.'); - getAns(s, start + 1, temp, ans, count + 1); - - //如果开头是 0,直接结束 - if (s.charAt(start) == '0') - return; - - //加入 2 位数 - if (start + 1 < s.length()) { - temp = new StringBuilder(before);//恢复为之前的解 - temp.append(s.substring(start, start + 2) + "" + '.'); - getAns(s, start + 2, temp, ans, count + 1); - } - - //加入 3 位数 - if (start + 2 < s.length()) { - temp = new StringBuilder(before); - int num = Integer.parseInt(s.substring(start, start + 3)); - if (num >= 0 && num <= 255) { - temp.append(s.substring(start, start + 3) + "" + '.'); - getAns(s, start + 3, temp, ans, count + 1); - } - } - -} -```` - -# 解法二 迭代 - -参考[这里](),相当暴力直接。因为我们知道了,需要划分为 4 部分,所以我们直接用利用三个指针将字符串强行分为四部分,遍历所有的划分,然后选取合法的解。 - -```java -public List restoreIpAddresses(String s) { - List res = new ArrayList(); - int len = s.length(); - //i < 4 保证第一部分不超过 3 位数 - //i < len - 2 保证剩余的字符串还能分成 3 部分 - for(int i = 1; i<4 && i3 || s.length()==0 || (s.charAt(0)=='0' && s.length()>1) || Integer.parseInt(s)>255) - return false; - return true; -} -``` - -时间复杂度:如果不考虑我们调用的内部函数,Integer.parseInt,s.substring,那么就是 O(1)。因为每一层循环最多遍历 4 次。考虑的话每次调用的时间复杂度是 O(n),常数次调用,所以是 O(n)。 - -空间复杂度:O(1)。 - -# 总 - -回溯或者说深度优先遍历,经常遇到了。但是解法二的暴力方法竟然通过了,有些意外。另外分享下 discuss 里有趣的评论,哈哈哈哈。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/93_2.png) - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/93.jpg) + +给一个字符串,输出所有的可能的 ip 地址,注意一下,01.1.001.1 类似这种 0 开头的是非法字符串。 + +# 解法一 回溯 递归 DFS + +很类似于刚做过的 [91 题](),对字符串进行划分。这个其实也是划分,划分的次数已经确定了,那就是分为 4 部分。那么就直接用回溯的思想,第一部分可能是 1 位数,然后进入递归。第一部分可能是 2 位数,然后进入递归。第一部分可能是 3 位数,然后进入递归。很好理解,直接看代码理解吧。 + +````java +public List restoreIpAddresses(String s) { + List ans = new ArrayList<>(); //保存最终的所有结果 + getAns(s, 0, new StringBuilder(), ans, 0); + return ans; +} + +/** +* @param: start 字符串开始部分 +* @param: temp 已经划分的部分 +* @param: ans 保存所有的解 +* @param: count 当前已经加入了几部分 +*/ +private void getAns(String s, int start, StringBuilder temp, List ans, int count) { + //如果剩余的长度大于剩下的部分都取 3 位数的长度,那么直接结束 + //例如 s = 121231312312, length = 12 + //当前 start = 1,count 等于 1 + //剩余字符串长度 11,剩余部分 4 - count = 3 部分,最多 3 * 3 是 9 + //所以一定是非法的,直接结束 + if (s.length() - start > 3 * (4 - count)) { + return; + } + //当前刚好到达了末尾 + if (start == s.length()) { + //当前刚好是 4 部分,将结果加入 + if (count == 4) { + ans.add(new String(temp.substring(0, temp.length() - 1))); + } + return; + } + //当前超过末位,或者已经到达了 4 部分结束掉 + if (start > s.length() || count == 4) { + return; + } + //保存的当前的解 + StringBuilder before = new StringBuilder(temp); + + //加入 1 位数 + temp.append(s.charAt(start) + "" + '.'); + getAns(s, start + 1, temp, ans, count + 1); + + //如果开头是 0,直接结束 + if (s.charAt(start) == '0') + return; + + //加入 2 位数 + if (start + 1 < s.length()) { + temp = new StringBuilder(before);//恢复为之前的解 + temp.append(s.substring(start, start + 2) + "" + '.'); + getAns(s, start + 2, temp, ans, count + 1); + } + + //加入 3 位数 + if (start + 2 < s.length()) { + temp = new StringBuilder(before); + int num = Integer.parseInt(s.substring(start, start + 3)); + if (num >= 0 && num <= 255) { + temp.append(s.substring(start, start + 3) + "" + '.'); + getAns(s, start + 3, temp, ans, count + 1); + } + } + +} +```` + +# 解法二 迭代 + +参考[这里](),相当暴力直接。因为我们知道了,需要划分为 4 部分,所以我们直接用利用三个指针将字符串强行分为四部分,遍历所有的划分,然后选取合法的解。 + +```java +public List restoreIpAddresses(String s) { + List res = new ArrayList(); + int len = s.length(); + //i < 4 保证第一部分不超过 3 位数 + //i < len - 2 保证剩余的字符串还能分成 3 部分 + for(int i = 1; i<4 && i3 || s.length()==0 || (s.charAt(0)=='0' && s.length()>1) || Integer.parseInt(s)>255) + return false; + return true; +} +``` + +时间复杂度:如果不考虑我们调用的内部函数,Integer.parseInt,s.substring,那么就是 O(1)。因为每一层循环最多遍历 4 次。考虑的话每次调用的时间复杂度是 O(n),常数次调用,所以是 O(n)。 + +空间复杂度:O(1)。 + +# 总 + +回溯或者说深度优先遍历,经常遇到了。但是解法二的暴力方法竟然通过了,有些意外。另外分享下 discuss 里有趣的评论,哈哈哈哈。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/93_2.png) + ![](https://windliang.oss-cn-beijing.aliyuncs.com/93_3.jpg) \ No newline at end of file diff --git a/leetCode-94-Binary-Tree-Inorder-Traversal.md b/leetCode-94-Binary-Tree-Inorder-Traversal.md index 782bea0cc..6e8533685 100644 --- a/leetCode-94-Binary-Tree-Inorder-Traversal.md +++ b/leetCode-94-Binary-Tree-Inorder-Traversal.md @@ -1,223 +1,223 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/94.jpg) - -二叉树的中序遍历。 - -# 解法一 递归 - -学二叉树的时候,必学的算法。用递归写简洁明了,就不多说了。 - -```java -public List inorderTraversal(TreeNode root) { - List ans = new ArrayList<>(); - getAns(root, ans); - return ans; -} - -private void getAns(TreeNode node, List ans) { - if (node == null) { - return; - } - getAns(node.left, ans); - ans.add(node.val); - getAns(node.right, ans); -} -``` - -时间复杂度:O(n),遍历每个节点。 - -空间复杂度:O(h),压栈消耗,h 是二叉树的高度。 - -官方[解法]()中还提供了两种解法,这里总结下。 - -# 解法二 栈 - -利用栈,去模拟递归。递归压栈的过程,就是保存现场,就是保存当前的变量,而解法一中当前有用的变量就是 node,所以我们用栈把每次的 node 保存起来即可。 - -模拟下递归的过程,只考虑 node 的压栈。 - -```java -//当前节点为空,出栈 -if (node == null) { - return; -} -//当前节点不为空 -getAns(node.left, ans); //压栈 -ans.add(node.val); //出栈后添加 -getAns(node.right, ans); //压栈 -//左右子树遍历完,出栈 -``` - -看一个具体的例子,想象一下吧。 - -```java - - 1 - / \ - 2 3 - / \ / - 4 5 6 - - push push push pop pop push pop pop -| | | | |_4_| | | | | | | | | | | -| | |_2_| |_2_| |_2_| | | |_5_| | | | | -|_1_| |_1_| |_1_| |_1_| |_1_| |_1_| |_1_| | | -ans add 4 add 2 add 5 add 1 -[] [4] [4 2] [4 2 5] [4 2 5 1] - push push pop pop -| | | | | | | | -| | |_6_| | | | | -|_3_| |_3_| |_3_| | | - add 6 add 3 - [4 2 5 1 6] [4 2 5 1 6 3] -``` - -结合代码。 - -```java -public List inorderTraversal(TreeNode root) { - List ans = new ArrayList<>(); - Stack stack = new Stack<>(); - TreeNode cur = root; - while (cur != null || !stack.isEmpty()) { - //节点不为空一直压栈 - while (cur != null) { - stack.push(cur); - cur = cur.left; //考虑左子树 - } - //节点为空,就出栈 - cur = stack.pop(); - //当前值加入 - ans.add(cur.val); - //考虑右子树 - cur = cur.right; - } - return ans; -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(h),栈消耗,h 是二叉树的高度。 - -# 解法三 Morris Traversal - -解法一和解法二本质上是一致的,都需要 O(h)的空间来保存上一层的信息。而我们注意到中序遍历,就是遍历完左子树,然后遍历根节点。如果我们把当前根节点存起来,然后遍历左子树,左子树遍历完以后回到当前根节点就可以了,怎么做到呢? - -我们知道,左子树最后遍历的节点一定是一个叶子节点,它的左右孩子都是 null,我们把它右孩子指向当前根节点存起来,这样的话我们就不需要额外空间了。这样做,遍历完当前左子树,就可以回到根节点了。 - -当然如果当前根节点左子树为空,那么我们只需要保存根节点的值,然后考虑右子树即可。 - -所以总体思想就是:记当前遍历的节点为 cur。 - -1、cur.left 为 null,保存 cur 的值,更新 cur = cur.right - -2、cur.left 不为 null,找到 cur.left 这颗子树最右边的节点记做 last - -**2.1** last.right 为 null,那么将 last.right = cur,更新 cur = cur.left - -**2.2** last.right 不为 null,说明之前已经访问过,第二次来到这里,表明当前子树遍历完成,保存 cur 的值,更新 cur = cur.right - -结合图示: - -![](https://windliang.oss-cn-beijing.aliyuncs.com/94_1.jpg) - -如上图,cur 指向根节点。 当前属于 2.1 的情况,cur.left 不为 null,cur 的左子树最右边的节点的右孩子为 null,那么我们把最右边的节点的右孩子指向 cur。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/94_2.jpg) - -接着,更新 cur = cur.left。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/94_3.jpg) - -如上图,当前属于 2.1 的情况,cur.left 不为 null,cur 的左子树最右边的节点的右孩子为 null,那么我们把最右边的节点的右孩子指向 cur。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/94_4.jpg) - -更新 cur = cur.left。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/94_5.jpg) - -如上图,当前属于情况 1,cur.left 为 null,保存 cur 的值,更新 cur = cur.right。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/94_6.jpg) - -如上图,当前属于 2.2 的情况,cur.left 不为 null,cur 的左子树最右边的节点的右孩子已经指向 cur,保存 cur 的值,更新 cur = cur.right。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/94_7.jpg) - -如上图,当前属于情况 1,cur.left 为 null,保存 cur 的值,更新 cur = cur.right。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/94_8.jpg) - -如上图,当前属于 2.2 的情况,cur.left 不为 null,cur 的左子树最右边的节点的右孩子已经指向 cur,保存 cur 的值,更新 cur = cur.right。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/94_9.jpg) - -当前属于情况 1,cur.left 为 null,保存 cur 的值,更新 cur = cur.right。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/94_10.jpg) - -cur 指向 null,结束遍历。 - -根据这个关系,写代码 - -记当前遍历的节点为 cur。 - -1、cur.left 为 null,保存 cur 的值,更新 cur = cur.right - -2、cur.left 不为 null,找到 cur.left 这颗子树最右边的节点记做 last - -**2.1** last.right 为 null,那么将 last.right = cur,更新 cur = cur.left - -**2.2** last.right 不为 null,说明之前已经访问过,第二次来到这里,表明当前子树遍历完成,保存 cur 的值,更新 cur = cur.right - -```java -public List inorderTraversal(TreeNode root) { - List ans = new ArrayList<>(); - TreeNode cur = root; - while (cur != null) { - //情况 1 - if (cur.left == null) { - ans.add(cur.val); - cur = cur.right; - } else { - //找左子树最右边的节点 - TreeNode pre = cur.left; - while (pre.right != null && pre.right != cur) { - pre = pre.right; - } - //情况 2.1 - if (pre.right == null) { - pre.right = cur; - cur = cur.left; - } - //情况 2.2 - if (pre.right == cur) { - pre.right = null; //这里可以恢复为 null - ans.add(cur.val); - cur = cur.right; - } - } - } - return ans; -} -``` - -时间复杂度:O(n)。每个节点遍历常数次。 - -空间复杂度:O(1)。 - -# 总 - -解法三是自己第一次见到,充分利用原来的空间的遍历,太强了。这么好的算法,当时上课的时候为什么没有讲,可惜了。 - - - - - - - - - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/94.jpg) + +二叉树的中序遍历。 + +# 解法一 递归 + +学二叉树的时候,必学的算法。用递归写简洁明了,就不多说了。 + +```java +public List inorderTraversal(TreeNode root) { + List ans = new ArrayList<>(); + getAns(root, ans); + return ans; +} + +private void getAns(TreeNode node, List ans) { + if (node == null) { + return; + } + getAns(node.left, ans); + ans.add(node.val); + getAns(node.right, ans); +} +``` + +时间复杂度:O(n),遍历每个节点。 + +空间复杂度:O(h),压栈消耗,h 是二叉树的高度。 + +官方[解法]()中还提供了两种解法,这里总结下。 + +# 解法二 栈 + +利用栈,去模拟递归。递归压栈的过程,就是保存现场,就是保存当前的变量,而解法一中当前有用的变量就是 node,所以我们用栈把每次的 node 保存起来即可。 + +模拟下递归的过程,只考虑 node 的压栈。 + +```java +//当前节点为空,出栈 +if (node == null) { + return; +} +//当前节点不为空 +getAns(node.left, ans); //压栈 +ans.add(node.val); //出栈后添加 +getAns(node.right, ans); //压栈 +//左右子树遍历完,出栈 +``` + +看一个具体的例子,想象一下吧。 + +```java + + 1 + / \ + 2 3 + / \ / + 4 5 6 + + push push push pop pop push pop pop +| | | | |_4_| | | | | | | | | | | +| | |_2_| |_2_| |_2_| | | |_5_| | | | | +|_1_| |_1_| |_1_| |_1_| |_1_| |_1_| |_1_| | | +ans add 4 add 2 add 5 add 1 +[] [4] [4 2] [4 2 5] [4 2 5 1] + push push pop pop +| | | | | | | | +| | |_6_| | | | | +|_3_| |_3_| |_3_| | | + add 6 add 3 + [4 2 5 1 6] [4 2 5 1 6 3] +``` + +结合代码。 + +```java +public List inorderTraversal(TreeNode root) { + List ans = new ArrayList<>(); + Stack stack = new Stack<>(); + TreeNode cur = root; + while (cur != null || !stack.isEmpty()) { + //节点不为空一直压栈 + while (cur != null) { + stack.push(cur); + cur = cur.left; //考虑左子树 + } + //节点为空,就出栈 + cur = stack.pop(); + //当前值加入 + ans.add(cur.val); + //考虑右子树 + cur = cur.right; + } + return ans; +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(h),栈消耗,h 是二叉树的高度。 + +# 解法三 Morris Traversal + +解法一和解法二本质上是一致的,都需要 O(h)的空间来保存上一层的信息。而我们注意到中序遍历,就是遍历完左子树,然后遍历根节点。如果我们把当前根节点存起来,然后遍历左子树,左子树遍历完以后回到当前根节点就可以了,怎么做到呢? + +我们知道,左子树最后遍历的节点一定是一个叶子节点,它的左右孩子都是 null,我们把它右孩子指向当前根节点存起来,这样的话我们就不需要额外空间了。这样做,遍历完当前左子树,就可以回到根节点了。 + +当然如果当前根节点左子树为空,那么我们只需要保存根节点的值,然后考虑右子树即可。 + +所以总体思想就是:记当前遍历的节点为 cur。 + +1、cur.left 为 null,保存 cur 的值,更新 cur = cur.right + +2、cur.left 不为 null,找到 cur.left 这颗子树最右边的节点记做 last + +**2.1** last.right 为 null,那么将 last.right = cur,更新 cur = cur.left + +**2.2** last.right 不为 null,说明之前已经访问过,第二次来到这里,表明当前子树遍历完成,保存 cur 的值,更新 cur = cur.right + +结合图示: + +![](https://windliang.oss-cn-beijing.aliyuncs.com/94_1.jpg) + +如上图,cur 指向根节点。 当前属于 2.1 的情况,cur.left 不为 null,cur 的左子树最右边的节点的右孩子为 null,那么我们把最右边的节点的右孩子指向 cur。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/94_2.jpg) + +接着,更新 cur = cur.left。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/94_3.jpg) + +如上图,当前属于 2.1 的情况,cur.left 不为 null,cur 的左子树最右边的节点的右孩子为 null,那么我们把最右边的节点的右孩子指向 cur。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/94_4.jpg) + +更新 cur = cur.left。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/94_5.jpg) + +如上图,当前属于情况 1,cur.left 为 null,保存 cur 的值,更新 cur = cur.right。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/94_6.jpg) + +如上图,当前属于 2.2 的情况,cur.left 不为 null,cur 的左子树最右边的节点的右孩子已经指向 cur,保存 cur 的值,更新 cur = cur.right。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/94_7.jpg) + +如上图,当前属于情况 1,cur.left 为 null,保存 cur 的值,更新 cur = cur.right。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/94_8.jpg) + +如上图,当前属于 2.2 的情况,cur.left 不为 null,cur 的左子树最右边的节点的右孩子已经指向 cur,保存 cur 的值,更新 cur = cur.right。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/94_9.jpg) + +当前属于情况 1,cur.left 为 null,保存 cur 的值,更新 cur = cur.right。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/94_10.jpg) + +cur 指向 null,结束遍历。 + +根据这个关系,写代码 + +记当前遍历的节点为 cur。 + +1、cur.left 为 null,保存 cur 的值,更新 cur = cur.right + +2、cur.left 不为 null,找到 cur.left 这颗子树最右边的节点记做 last + +**2.1** last.right 为 null,那么将 last.right = cur,更新 cur = cur.left + +**2.2** last.right 不为 null,说明之前已经访问过,第二次来到这里,表明当前子树遍历完成,保存 cur 的值,更新 cur = cur.right + +```java +public List inorderTraversal(TreeNode root) { + List ans = new ArrayList<>(); + TreeNode cur = root; + while (cur != null) { + //情况 1 + if (cur.left == null) { + ans.add(cur.val); + cur = cur.right; + } else { + //找左子树最右边的节点 + TreeNode pre = cur.left; + while (pre.right != null && pre.right != cur) { + pre = pre.right; + } + //情况 2.1 + if (pre.right == null) { + pre.right = cur; + cur = cur.left; + } + //情况 2.2 + if (pre.right == cur) { + pre.right = null; //这里可以恢复为 null + ans.add(cur.val); + cur = cur.right; + } + } + } + return ans; +} +``` + +时间复杂度:O(n)。每个节点遍历常数次。 + +空间复杂度:O(1)。 + +# 总 + +解法三是自己第一次见到,充分利用原来的空间的遍历,太强了。这么好的算法,当时上课的时候为什么没有讲,可惜了。 + + + + + + + + + diff --git a/leetCode-95-Unique-Binary-Search-TreesII.md b/leetCode-95-Unique-Binary-Search-TreesII.md index 168e3b219..3edef1821 100644 --- a/leetCode-95-Unique-Binary-Search-TreesII.md +++ b/leetCode-95-Unique-Binary-Search-TreesII.md @@ -1,548 +1,548 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/95.jpg) - -给一个 n,用1...n 这些数字生成所有可能的二分查找树。所谓二分查找树,定义如下: - -> 1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值; -> 2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值; -> 3. 任意节点的左、右子树也分别为二叉查找树; -> 4. 没有键值相等的节点。 - -# 解法一 回溯法 - -这是自己最早想到的一个思路。常规的回溯思想,就是普通的一个 for 循环,尝试插入 1, 2 ... n,然后进入递归,在原来的基础上继续尝试插入 1, 2... n。直到树包含了所有的数字。就是差不多下边这样的框架。 - -```java -递归{ - 递归出口; - for(int i = 1;i<=n;i++){ - add(i); - 进入递归; - remove(i); - } -} - -``` - -看一下详细的代码。 - -```java -public List generateTrees(int n) { - List ans = new ArrayList(); - if (n == 0) { - return ans; - } - TreeNode root = new TreeNode(0); //作为一个哨兵节点 - getAns(n, ans, root, 0); - return ans; -} - -private void getAns(int n, List ans, TreeNode root, int count) { - if (count == n) { - //复制当前树并且加到结果中 - TreeNode newRoot = treeCopy(root); - ans.add(newRoot.right); - return; - } - TreeNode root_copy = root; - //尝试插入每个数 - for (int i = 1; i <= n; i++) { - root = root_copy; - //寻找要插入的位置 - while (root != null) { - //在左子树中插入 - if (i < root.val) { - //到了最左边 - if (root.left==null) { - //插入当前数字 - root.left = new TreeNode(i); - //进入递归 - getAns(n, ans, root_copy, count + 1); - //还原为 null,尝试插入下个数字 - root.left = null; - break; - } - root = root.left; - //在右子树中插入 - } else if (i > root.val){ - //到了最右边 - if (root.right == null){ - //插入当前数字 - root.right = new TreeNode(i); - //进入递归 - getAns(n, ans, root_copy, count + 1); - //还原为 null,尝试插入下个数字 - root.right = null; - break; - } - root = root.right; - //如果找到了相等的数字,就结束,尝试下一个数字 - } else { - break; - } - } - } -} - -``` - -然而,理想很美丽,现实很骨感,出错了,就是回溯经常遇到的问题,出现了重复解。 - -```java -//第一种情况 - -第一次循环添加 2 - 2 - -第二次循环添加 1 - 2 - / - 1 - -第三次循环添加 3 - 2 - / \ - 1 3 - -//第二种情况 - -第一次循环添加 2 - 2 - -第二次循环添加 3 - 2 - \ - 3 - -第三次循环添加 1 - 2 - / \ - 1 3 -``` - -是的,因为每次循环都尝试了所有数字,所以造成了重复。所以接下来就是解决避免重复数字的发生,然而经过种种努力,都失败了,所以这种思路就此结束,如果大家想出了避免重复的方法,欢迎和我交流。 - -# 解法二 递归 - -解法一完全没有用到查找二叉树的性质,暴力尝试了所有可能从而造成了重复。我们可以利用一下查找二叉树的性质。左子树的所有值小于根节点,右子树的所有值大于根节点。 - -所以如果求 1...n 的所有可能。 - -我们只需要把 1 作为根节点,[ ] 空作为左子树,[ 2 ... n ] 的所有可能作为右子树。 - -2 作为根节点,[ 1 ] 作为左子树,[ 3...n ] 的所有可能作为右子树。 - -3 作为根节点,[ 1 2 ] 的所有可能作为左子树,[ 4 ... n ] 的所有可能作为右子树,然后左子树和右子树两两组合。 - -4 作为根节点,[ 1 2 3 ] 的所有可能作为左子树,[ 5 ... n ] 的所有可能作为右子树,然后左子树和右子树两两组合。 - -... - -n 作为根节点,[ 1... n ] 的所有可能作为左子树,[ ] 作为右子树。 - -至于,[ 2 ... n ] 的所有可能以及 [ 4 ... n ] 以及其他情况的所有可能,可以利用上边的方法,把每个数字作为根节点,然后把所有可能的左子树和右子树组合起来即可。 - -如果只有一个数字,那么所有可能就是一种情况,把该数字作为一棵树。而如果是 [ ],那就返回 null。 - -```java -public List generateTrees(int n) { - List ans = new ArrayList(); - if (n == 0) { - return ans; - } - return getAns(1, n); - -} - -private List getAns(int start, int end) { - List ans = new ArrayList(); - //此时没有数字,将 null 加入结果中 - if (start > end) { - ans.add(null); - return ans; - } - //只有一个数字,当前数字作为一棵树加入结果中 - if (start == end) { - TreeNode tree = new TreeNode(start); - ans.add(tree); - return ans; - } - //尝试每个数字作为根节点 - for (int i = start; i <= end; i++) { - //得到所有可能的左子树 - List leftTrees = getAns(start, i - 1); - //得到所有可能的右子树 - List rightTrees = getAns(i + 1, end); - //左子树右子树两两组合 - for (TreeNode leftTree : leftTrees) { - for (TreeNode rightTree : rightTrees) { - TreeNode root = new TreeNode(i); - root.left = leftTree; - root.right = rightTree; - //加入到最终结果中 - ans.add(root); - } - } - } - return ans; -} -``` - -# 解法三 动态规划 - -大多数递归都可以用动态规划的思想重写,这道也不例外。从底部往上走,参考[这里]()。 - -举个例子,n = 3 - -```java -数字个数是 0 的所有解 -null -数字个数是 1 的所有解 -1 -2 -3 -数字个数是 2 的所有解,我们只需要考虑连续数字 -[ 1 2 ] - 1 - \ - 2 - 2 - / - 1 - -[ 2 3 ] - 2 - \ - 3 - 3 - / - 2 -如果求 3 个数字的所有情况。 -[ 1 2 3 ] -利用解法二递归的思路,就是分别把每个数字作为根节点,然后考虑左子树和右子树的可能 -1 作为根节点,左子树是 [] 的所有可能,右子树是 [ 2 3 ] 的所有可能,利用之前求出的结果进行组合。 - 1 - / \ -null 2 - \ - 3 - - 1 - / \ -null 3 - / - 2 - -2 作为根节点,左子树是 [ 1 ] 的所有可能,右子树是 [ 3 ] 的所有可能,利用之前求出的结果进行组合。 - 2 - / \ - 1 3 - -3 作为根节点,左子树是 [ 1 2 ] 的所有可能,右子树是 [] 的所有可能,利用之前求出的结果进行组合。 - 3 - / \ - 1 null - \ - 2 - - 3 - / \ - 2 null - / - 1 -``` - -然后利用上边的思路基本上可以写代码了,就是求出长度为 1 的所有可能,长度为 2 的所有可能 ... 直到 n。 - -但是我们注意到,求长度为 2 的所有可能的时候,我们需要求 [ 1 2 ] 的所有可能,[ 2 3 ] 的所有可能,这只是 n = 3 的情况。如果 n 等于 100,我们需要求的更多了 [ 1 2 ] , [ 2 3 ] , [ 3 4 ] ... [ 99 100 ] 太多了。能不能优化呢? - -仔细观察,我们可以发现长度是为 2 的所有可能其实只有两种结构。 - -``` - x - / -y - -y - \ - x -``` - -看之前推导的 [ 1 2 ] 和 [ 2 3 ],只是数字不一样,结构是完全一样的。 - -``` -[ 1 2 ] - 1 - \ - 2 - 2 - / - 1 - -[ 2 3 ] - 2 - \ - 3 - 3 - / - 2 -``` - -所以我们 n = 100 的时候,求长度是 2 的所有情况的时候,我们没必要把 [ 1 2 ] , [ 2 3 ] , [ 3 4 ] ... [ 99 100 ] 所有的情况都求出来,只需要求出 [ 1 2 ] 的所有情况即可。 - -推广到任意长度 len,我们其实只需要求 [ 1 2 ... len ] 的所有情况就可以了。下一个问题随之而来,这些 [ 2 3 ] , [ 3 4 ] ... [ 99 100 ] 没求的怎么办呢? - -举个例子。n = 100,此时我们求把 98 作为根节点的所有情况,根据之前的推导,我们需要长度是 97 的 [ 1 2 ... 97 ] 的所有情况作为左子树,长度是 2 的 [ 99 100 ] 的所有情况作为右子树。 - -[ 1 2 ... 97 ] 的所有情况刚好是 [ 1 2 ... len ] ,已经求出来了。但 [ 99 100 ] 怎么办呢?我们只求了 [ 1 2 ] 的所有情况。答案很明显了,在 [ 1 2 ] 的所有情况每个数字加一个偏差 98,即加上根节点的值就可以了。 - -```java -[ 1 2 ] - 1 - \ - 2 - 2 - / - 1 - -[ 99 100 ] - 1 + 98 - \ - 2 + 98 - 2 + 98 - / - 1 + 98 - -即 - 99 - \ - 100 - 100 - / - 99 -``` - -所以我们需要一个函数,实现树的复制并且加上偏差。 - -```java -private TreeNode clone(TreeNode n, int offset) { - if (n == null) { - return null; - } - TreeNode node = new TreeNode(n.val + offset); - node.left = clone(n.left, offset); - node.right = clone(n.right, offset); - return node; -} -``` - -通过上边的所有分析,代码可以写了,总体思想就是求长度为 2 的所有情况,求长度为 3 的所有情况直到 n。而求长度为 len 的所有情况,我们只需要求出一个代表 [ 1 2 ... len ] 的所有情况,其他的话加上一个偏差,加上当前根节点即可。 - -```java -public List generateTrees(int n) { - ArrayList[] dp = new ArrayList[n + 1]; - dp[0] = new ArrayList(); - if (n == 0) { - return dp[0]; - } - dp[0].add(null); - //长度为 1 到 n - for (int len = 1; len <= n; len++) { - dp[len] = new ArrayList(); - //将不同的数字作为根节点,只需要考虑到 len - for (int root = 1; root <= len; root++) { - int left = root - 1; //左子树的长度 - int right = len - root; //右子树的长度 - for (TreeNode leftTree : dp[left]) { - for (TreeNode rightTree : dp[right]) { - TreeNode treeRoot = new TreeNode(root); - treeRoot.left = leftTree; - //克隆右子树并且加上偏差 - treeRoot.right = clone(rightTree, root); - dp[len].add(treeRoot); - } - } - } - } - return dp[n]; -} - -private TreeNode clone(TreeNode n, int offset) { - if (n == null) { - return null; - } - TreeNode node = new TreeNode(n.val + offset); - node.left = clone(n.left, offset); - node.right = clone(n.right, offset); - return node; -} -``` - -值得注意的是,所有的左子树我们没有 clone ,也就是很多子树被共享了,在内存中就会是下边的样子。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/95_2.jpg) - -也就是左子树用的都是之前的子树,没有开辟新的空间。 - -# 解法四 动态规划 2 - -解法三的动态规划完全是模仿了解法二递归的思想,这里再介绍另一种思想,会更好理解一些。参考[这里]()。 - -```java -考虑 [] 的所有解 -null - -考虑 [ 1 ] 的所有解 -1 - -考虑 [ 1 2 ] 的所有解 - 2 - / -1 - - 1 - \ - 2 - -考虑 [ 1 2 3 ] 的所有解 - 3 - / - 2 - / -1 - - 2 - / \ - 1 3 - - 3 - / - 1 - \ - 2 - - 1 - \ - 3 - / - 2 - - 1 - \ - 2 - \ - 3 -``` - -仔细分析,可以发现一个规律。首先我们每次新增加的数字大于之前的所有数字,所以新增加的数字出现的位置只可能是根节点或者是根节点的右孩子,右孩子的右孩子,右孩子的右孩子的右孩子等等,总之一定是右边。其次,新数字所在位置原来的子树,改为当前插入数字的左孩子即可,因为插入数字是最大的。 - -```java -对于下边的解 - 2 - / -1 - -然后增加 3 -1.把 3 放到根节点 - 3 - / - 2 - / -1 - -2. 把 3 放到根节点的右孩子 - 2 - / \ - 1 3 - -对于下边的解 - 1 - \ - 2 - -然后增加 3 -1.把 3 放到根节点 - 3 - / - 1 - \ - 2 - -2. 把 3 放到根节点的右孩子,原来的子树作为 3 的左孩子 - 1 - \ - 3 - / - 2 - -3. 把 3 放到根节点的右孩子的右孩子 - 1 - \ - 2 - \ - 3 -``` - -以上就是根据 [ 1 2 ] 推出 [ 1 2 3 ] 的所有过程,可以写代码了。由于求当前的所有解只需要上一次的解,所有我们只需要两个 list,pre 保存上一次的所有解, cur 计算当前的所有解。 - -```java -public List generateTrees(int n) { - List pre = new ArrayList(); - if (n == 0) { - return pre; - } - pre.add(null); - //每次增加一个数字 - for (int i = 1; i <= n; i++) { - List cur = new ArrayList(); - //遍历之前的所有解 - for (TreeNode root : pre) { - //插入到根节点 - TreeNode insert = new TreeNode(i); - insert.left = root; - cur.add(insert); - //插入到右孩子,右孩子的右孩子...最多找 n 次孩子 - for (int j = 0; j <= n; j++) { - TreeNode root_copy = treeCopy(root); //复制当前的树 - TreeNode right = root_copy; //找到要插入右孩子的位置 - int k = 0; - //遍历 j 次找右孩子 - for (; k < j; k++) { - if (right == null) - break; - right = right.right; - } - //到达 null 提前结束 - if (right == null) - break; - //保存当前右孩子的位置的子树作为插入节点的左孩子 - TreeNode rightTree = right.right; - insert = new TreeNode(i); - right.right = insert; //右孩子是插入的节点 - insert.left = rightTree; //插入节点的左孩子更新为插入位置之前的子树 - //加入结果中 - cur.add(root_copy); - } - } - pre = cur; - - } - return pre; -} - - -private TreeNode treeCopy(TreeNode root) { - if (root == null) { - return root; - } - TreeNode newRoot = new TreeNode(root.val); - newRoot.left = treeCopy(root.left); - newRoot.right = treeCopy(root.right); - return newRoot; -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/95.jpg) + +给一个 n,用1...n 这些数字生成所有可能的二分查找树。所谓二分查找树,定义如下: + +> 1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值; +> 2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值; +> 3. 任意节点的左、右子树也分别为二叉查找树; +> 4. 没有键值相等的节点。 + +# 解法一 回溯法 + +这是自己最早想到的一个思路。常规的回溯思想,就是普通的一个 for 循环,尝试插入 1, 2 ... n,然后进入递归,在原来的基础上继续尝试插入 1, 2... n。直到树包含了所有的数字。就是差不多下边这样的框架。 + +```java +递归{ + 递归出口; + for(int i = 1;i<=n;i++){ + add(i); + 进入递归; + remove(i); + } +} + +``` + +看一下详细的代码。 + +```java +public List generateTrees(int n) { + List ans = new ArrayList(); + if (n == 0) { + return ans; + } + TreeNode root = new TreeNode(0); //作为一个哨兵节点 + getAns(n, ans, root, 0); + return ans; +} + +private void getAns(int n, List ans, TreeNode root, int count) { + if (count == n) { + //复制当前树并且加到结果中 + TreeNode newRoot = treeCopy(root); + ans.add(newRoot.right); + return; + } + TreeNode root_copy = root; + //尝试插入每个数 + for (int i = 1; i <= n; i++) { + root = root_copy; + //寻找要插入的位置 + while (root != null) { + //在左子树中插入 + if (i < root.val) { + //到了最左边 + if (root.left==null) { + //插入当前数字 + root.left = new TreeNode(i); + //进入递归 + getAns(n, ans, root_copy, count + 1); + //还原为 null,尝试插入下个数字 + root.left = null; + break; + } + root = root.left; + //在右子树中插入 + } else if (i > root.val){ + //到了最右边 + if (root.right == null){ + //插入当前数字 + root.right = new TreeNode(i); + //进入递归 + getAns(n, ans, root_copy, count + 1); + //还原为 null,尝试插入下个数字 + root.right = null; + break; + } + root = root.right; + //如果找到了相等的数字,就结束,尝试下一个数字 + } else { + break; + } + } + } +} + +``` + +然而,理想很美丽,现实很骨感,出错了,就是回溯经常遇到的问题,出现了重复解。 + +```java +//第一种情况 + +第一次循环添加 2 + 2 + +第二次循环添加 1 + 2 + / + 1 + +第三次循环添加 3 + 2 + / \ + 1 3 + +//第二种情况 + +第一次循环添加 2 + 2 + +第二次循环添加 3 + 2 + \ + 3 + +第三次循环添加 1 + 2 + / \ + 1 3 +``` + +是的,因为每次循环都尝试了所有数字,所以造成了重复。所以接下来就是解决避免重复数字的发生,然而经过种种努力,都失败了,所以这种思路就此结束,如果大家想出了避免重复的方法,欢迎和我交流。 + +# 解法二 递归 + +解法一完全没有用到查找二叉树的性质,暴力尝试了所有可能从而造成了重复。我们可以利用一下查找二叉树的性质。左子树的所有值小于根节点,右子树的所有值大于根节点。 + +所以如果求 1...n 的所有可能。 + +我们只需要把 1 作为根节点,[ ] 空作为左子树,[ 2 ... n ] 的所有可能作为右子树。 + +2 作为根节点,[ 1 ] 作为左子树,[ 3...n ] 的所有可能作为右子树。 + +3 作为根节点,[ 1 2 ] 的所有可能作为左子树,[ 4 ... n ] 的所有可能作为右子树,然后左子树和右子树两两组合。 + +4 作为根节点,[ 1 2 3 ] 的所有可能作为左子树,[ 5 ... n ] 的所有可能作为右子树,然后左子树和右子树两两组合。 + +... + +n 作为根节点,[ 1... n ] 的所有可能作为左子树,[ ] 作为右子树。 + +至于,[ 2 ... n ] 的所有可能以及 [ 4 ... n ] 以及其他情况的所有可能,可以利用上边的方法,把每个数字作为根节点,然后把所有可能的左子树和右子树组合起来即可。 + +如果只有一个数字,那么所有可能就是一种情况,把该数字作为一棵树。而如果是 [ ],那就返回 null。 + +```java +public List generateTrees(int n) { + List ans = new ArrayList(); + if (n == 0) { + return ans; + } + return getAns(1, n); + +} + +private List getAns(int start, int end) { + List ans = new ArrayList(); + //此时没有数字,将 null 加入结果中 + if (start > end) { + ans.add(null); + return ans; + } + //只有一个数字,当前数字作为一棵树加入结果中 + if (start == end) { + TreeNode tree = new TreeNode(start); + ans.add(tree); + return ans; + } + //尝试每个数字作为根节点 + for (int i = start; i <= end; i++) { + //得到所有可能的左子树 + List leftTrees = getAns(start, i - 1); + //得到所有可能的右子树 + List rightTrees = getAns(i + 1, end); + //左子树右子树两两组合 + for (TreeNode leftTree : leftTrees) { + for (TreeNode rightTree : rightTrees) { + TreeNode root = new TreeNode(i); + root.left = leftTree; + root.right = rightTree; + //加入到最终结果中 + ans.add(root); + } + } + } + return ans; +} +``` + +# 解法三 动态规划 + +大多数递归都可以用动态规划的思想重写,这道也不例外。从底部往上走,参考[这里]()。 + +举个例子,n = 3 + +```java +数字个数是 0 的所有解 +null +数字个数是 1 的所有解 +1 +2 +3 +数字个数是 2 的所有解,我们只需要考虑连续数字 +[ 1 2 ] + 1 + \ + 2 + 2 + / + 1 + +[ 2 3 ] + 2 + \ + 3 + 3 + / + 2 +如果求 3 个数字的所有情况。 +[ 1 2 3 ] +利用解法二递归的思路,就是分别把每个数字作为根节点,然后考虑左子树和右子树的可能 +1 作为根节点,左子树是 [] 的所有可能,右子树是 [ 2 3 ] 的所有可能,利用之前求出的结果进行组合。 + 1 + / \ +null 2 + \ + 3 + + 1 + / \ +null 3 + / + 2 + +2 作为根节点,左子树是 [ 1 ] 的所有可能,右子树是 [ 3 ] 的所有可能,利用之前求出的结果进行组合。 + 2 + / \ + 1 3 + +3 作为根节点,左子树是 [ 1 2 ] 的所有可能,右子树是 [] 的所有可能,利用之前求出的结果进行组合。 + 3 + / \ + 1 null + \ + 2 + + 3 + / \ + 2 null + / + 1 +``` + +然后利用上边的思路基本上可以写代码了,就是求出长度为 1 的所有可能,长度为 2 的所有可能 ... 直到 n。 + +但是我们注意到,求长度为 2 的所有可能的时候,我们需要求 [ 1 2 ] 的所有可能,[ 2 3 ] 的所有可能,这只是 n = 3 的情况。如果 n 等于 100,我们需要求的更多了 [ 1 2 ] , [ 2 3 ] , [ 3 4 ] ... [ 99 100 ] 太多了。能不能优化呢? + +仔细观察,我们可以发现长度是为 2 的所有可能其实只有两种结构。 + +``` + x + / +y + +y + \ + x +``` + +看之前推导的 [ 1 2 ] 和 [ 2 3 ],只是数字不一样,结构是完全一样的。 + +``` +[ 1 2 ] + 1 + \ + 2 + 2 + / + 1 + +[ 2 3 ] + 2 + \ + 3 + 3 + / + 2 +``` + +所以我们 n = 100 的时候,求长度是 2 的所有情况的时候,我们没必要把 [ 1 2 ] , [ 2 3 ] , [ 3 4 ] ... [ 99 100 ] 所有的情况都求出来,只需要求出 [ 1 2 ] 的所有情况即可。 + +推广到任意长度 len,我们其实只需要求 [ 1 2 ... len ] 的所有情况就可以了。下一个问题随之而来,这些 [ 2 3 ] , [ 3 4 ] ... [ 99 100 ] 没求的怎么办呢? + +举个例子。n = 100,此时我们求把 98 作为根节点的所有情况,根据之前的推导,我们需要长度是 97 的 [ 1 2 ... 97 ] 的所有情况作为左子树,长度是 2 的 [ 99 100 ] 的所有情况作为右子树。 + +[ 1 2 ... 97 ] 的所有情况刚好是 [ 1 2 ... len ] ,已经求出来了。但 [ 99 100 ] 怎么办呢?我们只求了 [ 1 2 ] 的所有情况。答案很明显了,在 [ 1 2 ] 的所有情况每个数字加一个偏差 98,即加上根节点的值就可以了。 + +```java +[ 1 2 ] + 1 + \ + 2 + 2 + / + 1 + +[ 99 100 ] + 1 + 98 + \ + 2 + 98 + 2 + 98 + / + 1 + 98 + +即 + 99 + \ + 100 + 100 + / + 99 +``` + +所以我们需要一个函数,实现树的复制并且加上偏差。 + +```java +private TreeNode clone(TreeNode n, int offset) { + if (n == null) { + return null; + } + TreeNode node = new TreeNode(n.val + offset); + node.left = clone(n.left, offset); + node.right = clone(n.right, offset); + return node; +} +``` + +通过上边的所有分析,代码可以写了,总体思想就是求长度为 2 的所有情况,求长度为 3 的所有情况直到 n。而求长度为 len 的所有情况,我们只需要求出一个代表 [ 1 2 ... len ] 的所有情况,其他的话加上一个偏差,加上当前根节点即可。 + +```java +public List generateTrees(int n) { + ArrayList[] dp = new ArrayList[n + 1]; + dp[0] = new ArrayList(); + if (n == 0) { + return dp[0]; + } + dp[0].add(null); + //长度为 1 到 n + for (int len = 1; len <= n; len++) { + dp[len] = new ArrayList(); + //将不同的数字作为根节点,只需要考虑到 len + for (int root = 1; root <= len; root++) { + int left = root - 1; //左子树的长度 + int right = len - root; //右子树的长度 + for (TreeNode leftTree : dp[left]) { + for (TreeNode rightTree : dp[right]) { + TreeNode treeRoot = new TreeNode(root); + treeRoot.left = leftTree; + //克隆右子树并且加上偏差 + treeRoot.right = clone(rightTree, root); + dp[len].add(treeRoot); + } + } + } + } + return dp[n]; +} + +private TreeNode clone(TreeNode n, int offset) { + if (n == null) { + return null; + } + TreeNode node = new TreeNode(n.val + offset); + node.left = clone(n.left, offset); + node.right = clone(n.right, offset); + return node; +} +``` + +值得注意的是,所有的左子树我们没有 clone ,也就是很多子树被共享了,在内存中就会是下边的样子。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/95_2.jpg) + +也就是左子树用的都是之前的子树,没有开辟新的空间。 + +# 解法四 动态规划 2 + +解法三的动态规划完全是模仿了解法二递归的思想,这里再介绍另一种思想,会更好理解一些。参考[这里]()。 + +```java +考虑 [] 的所有解 +null + +考虑 [ 1 ] 的所有解 +1 + +考虑 [ 1 2 ] 的所有解 + 2 + / +1 + + 1 + \ + 2 + +考虑 [ 1 2 3 ] 的所有解 + 3 + / + 2 + / +1 + + 2 + / \ + 1 3 + + 3 + / + 1 + \ + 2 + + 1 + \ + 3 + / + 2 + + 1 + \ + 2 + \ + 3 +``` + +仔细分析,可以发现一个规律。首先我们每次新增加的数字大于之前的所有数字,所以新增加的数字出现的位置只可能是根节点或者是根节点的右孩子,右孩子的右孩子,右孩子的右孩子的右孩子等等,总之一定是右边。其次,新数字所在位置原来的子树,改为当前插入数字的左孩子即可,因为插入数字是最大的。 + +```java +对于下边的解 + 2 + / +1 + +然后增加 3 +1.把 3 放到根节点 + 3 + / + 2 + / +1 + +2. 把 3 放到根节点的右孩子 + 2 + / \ + 1 3 + +对于下边的解 + 1 + \ + 2 + +然后增加 3 +1.把 3 放到根节点 + 3 + / + 1 + \ + 2 + +2. 把 3 放到根节点的右孩子,原来的子树作为 3 的左孩子 + 1 + \ + 3 + / + 2 + +3. 把 3 放到根节点的右孩子的右孩子 + 1 + \ + 2 + \ + 3 +``` + +以上就是根据 [ 1 2 ] 推出 [ 1 2 3 ] 的所有过程,可以写代码了。由于求当前的所有解只需要上一次的解,所有我们只需要两个 list,pre 保存上一次的所有解, cur 计算当前的所有解。 + +```java +public List generateTrees(int n) { + List pre = new ArrayList(); + if (n == 0) { + return pre; + } + pre.add(null); + //每次增加一个数字 + for (int i = 1; i <= n; i++) { + List cur = new ArrayList(); + //遍历之前的所有解 + for (TreeNode root : pre) { + //插入到根节点 + TreeNode insert = new TreeNode(i); + insert.left = root; + cur.add(insert); + //插入到右孩子,右孩子的右孩子...最多找 n 次孩子 + for (int j = 0; j <= n; j++) { + TreeNode root_copy = treeCopy(root); //复制当前的树 + TreeNode right = root_copy; //找到要插入右孩子的位置 + int k = 0; + //遍历 j 次找右孩子 + for (; k < j; k++) { + if (right == null) + break; + right = right.right; + } + //到达 null 提前结束 + if (right == null) + break; + //保存当前右孩子的位置的子树作为插入节点的左孩子 + TreeNode rightTree = right.right; + insert = new TreeNode(i); + right.right = insert; //右孩子是插入的节点 + insert.left = rightTree; //插入节点的左孩子更新为插入位置之前的子树 + //加入结果中 + cur.add(root_copy); + } + } + pre = cur; + + } + return pre; +} + + +private TreeNode treeCopy(TreeNode root) { + if (root == null) { + return root; + } + TreeNode newRoot = new TreeNode(root.val); + newRoot.left = treeCopy(root.left); + newRoot.right = treeCopy(root.right); + return newRoot; +} +``` + +# 总 + 解法二和解法四算作常规的思路,比较容易想到。解法三,发现同构的操作真的是神仙操作了,服! \ No newline at end of file diff --git a/leetCode-96-Unique-Binary-Search-Trees.md b/leetCode-96-Unique-Binary-Search-Trees.md index 6d4ee797e..1abd08f46 100644 --- a/leetCode-96-Unique-Binary-Search-Trees.md +++ b/leetCode-96-Unique-Binary-Search-Trees.md @@ -1,257 +1,257 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/96.jpg) - -和 [95 题]()一样,只不过这道题不需要输出所有的树,只需要输出所有可能的二分查找树的数量。所以完全按照 95 题思路写,大家可以先到 [95 题]()看一看。 - -# 解法一 递归 - -下边是 95 题的分析。 - -> 我们可以利用一下查找二叉树的性质。左子树的所有值小于根节点,右子树的所有值大于根节点。 -> -> 所以如果求 1...n 的所有可能。 -> -> 我们只需要把 1 作为根节点,[ ] 空作为左子树,[ 2 ... n ] 的所有可能作为右子树。 -> -> 2 作为根节点,[ 1 ] 作为左子树,[ 3...n ] 的所有可能作为右子树。 -> -> 3 作为根节点,[ 1 2 ] 的所有可能作为左子树,[ 4 ... n ] 的所有可能作为右子树,然后左子树和右子树两两组合。 -> -> 4 作为根节点,[ 1 2 3 ] 的所有可能作为左子树,[ 5 ... n ] 的所有可能作为右子树,然后左子树和右子树两两组合。 -> -> ... -> -> n 作为根节点,[ 1... n ] 的所有可能作为左子树,[ ] 作为右子树。 -> -> 至于,[ 2 ... n ] 的所有可能以及 [ 4 ... n ] 以及其他情况的所有可能,可以利用上边的方法,把每个数字作为根节点,然后把所有可能的左子树和右子树组合起来即可。 -> -> 如果只有一个数字,那么所有可能就是一种情况,把该数字作为一棵树。而如果是 [ ],那就返回 null。 - -对于这道题,我们会更简单些,只需要返回树的数量即可。求当前根的数量,只需要左子树的数量乘上右子树。 - -```java -public int numTrees(int n) { - if (n == 0) { - return 0; - } - return getAns(1, n); - -} -private int getAns(int start, int end) { - int ans = 0; - //此时没有数字,只有一个数字,返回 1 - if (start >= end) { - return 1; - } - //尝试每个数字作为根节点 - for (int i = start; i <= end; i++) { - //得到所有可能的左子树 - int leftTreesNum = getAns(start, i - 1); - //得到所有可能的右子树 - int rightTreesNum = getAns(i + 1, end); - //左子树右子树两两组合 - ans+=leftTreesNum * rightTreesNum; - } - return ans; -} -``` - -受到[这里]()的启发,我们甚至可以改写的更简单些。因为 95 题要把每颗树返回,所有传的参数是 start 和 end。这里的话,我们只关心数量,所以不需要具体的范围,而是传树的节点的数量即可。 - -```java -public int numTrees(int n) { - if (n == 0) { - return 0; - } - return getAns(n); - -} - -private int getAns(int n) { - int ans = 0; - //此时没有数字或者只有一个数字,返回 1 - if (n==0 ||n==1) { - return 1; - } - //尝试每个数字作为根节点 - for (int i = 1; i <= n; i++) { - //得到所有可能的左子树 - // i - 1 代表左子树节点的数量 - int leftTreesNum = getAns(i-1); - //得到所有可能的右子树 - //n - i 代表左子树节点的数量 - int rightTreesNum = getAns(n-i); - //左子树右子树两两组合 - ans+=leftTreesNum * rightTreesNum; - } - return ans; -} -``` - -然后,由于递归的分叉,所以会导致很多重复解的计算,所以使用 memoization 技术,把递归过程中求出的解保存起来,第二次需要的时候直接拿即可。 - -```java -public int numTrees(int n) { - if (n == 0) { - return 0; - } - HashMap memoization = new HashMap<>(); - return getAns(n,memoization); - -} - -private int getAns(int n, HashMap memoization) { - if(memoization.containsKey(n)){ - return memoization.get(n); - } - int ans = 0; - //此时没有数字,只有一个数字,返回 1 - if (n==0 ||n==1) { - return 1; - } - //尝试每个数字作为根节点 - for (int i = 1; i <= n; i++) { - //得到所有可能的左子树 - int leftTreesNum = getAns(i-1,memoization); - //得到所有可能的右子树 - int rightTreesNum = getAns(n-i,memoization); - //左子树右子树两两组合 - ans+=leftTreesNum * rightTreesNum; - } - memoization.put(n, ans); - return ans; -} -``` - -# 解法二 动态规划 - -直接利用[95题]()解法三的思路,讲解比较长就不贴过来了,可以过去看一下。 - -或者直接从这里的解法一的思路考虑,因为递归是从顶层往下走,压栈压栈压栈,到了长度是 0 或者是 1 就出栈出栈出栈。我们可以利用动态规划的思想,直接从底部往上走。求出长度是 0,长度是 1,长度是 2....长度是 n 的解。用一个数组 dp 把这些结果全部保存起来。 - -```java -public int numTrees(int n) { - int[] dp = new int[n + 1]; - dp[0] = 1; - if (n == 0) { - return 0; - } - // 长度为 1 到 n - for (int len = 1; len <= n; len++) { - // 将不同的数字作为根节点,只需要考虑到 len - for (int root = 1; root <= len; root++) { - int left = root - 1; // 左子树的长度 - int right = len - root; // 右子树的长度 - dp[len] += dp[left] * dp[right]; - } - } - return dp[n]; -} -``` - -参考[这里]()还有优化的空间。 - -利用对称性,可以使得循环减少一些。 - -* n 是偶数的时候 - 1 2 | 3 4 ,for 循环中我们以每个数字为根求出每个的解。我们其实可以只求一半,根据对称性我们可以知道 1 和 4,2 和 3 求出的解分别是相等的。 - -* n 是奇数的时候 - - 1 2 | 3 | 4 5,和偶数同理,只求一半,此外最中间的 3 的解也要加上。 - -```java -public int numTrees6(int n) { - if (n == 0) { - return 0; - } - int[] dp = new int[n + 1]; - dp[0] = 1; - dp[1] = 1; - // 长度为 1 到 n - for (int len = 2; len <= n; len++) { - // 将不同的数字作为根节点,只需要考虑到 len - for (int root = 1; root <= len / 2; root++) { - int left = root - 1; // 左子树的长度 - int right = len - root; // 右子树的长度 - dp[len] += dp[left] * dp[right]; - } - dp[len] *= 2;// 利用对称性乘 2 - // 考虑奇数的情况 - if ((len & 1) == 1) { - int root = (len >> 1) + 1; - int left = root - 1; // 左子树的长度 - int right = len - root; // 右子树的长度 - dp[len] += dp[left] * dp[right]; - } - } - return dp[n]; -} -``` - -# 解法三 公式法 - -参考[这里]()。其实利用的是卡塔兰数列,这是第二次遇到了,之前是第 [22 题]() ,生成合法的括号序列。 - -这道题,为什么和卡塔兰数列联系起来呢? - -看一下卡塔兰树数列的定义: - -> 令h ( 0 ) = 1,catalan 数满足递推式: -> -> **h ( n ) = h ( 0 ) \* h ( n - 1 ) + h ( 1 ) \* h ( n - 2 ) + ... + h ( n - 1 ) \* h ( 0 ) ( n >=1 )** -> -> 例如:h ( 2 ) = h ( 0 ) \* h ( 1 ) + h ( 1 ) \* h ( 0 ) = 1 \* 1 + 1 * 1 = 2 -> -> h ( 3 ) = h ( 0 ) \* h ( 2 ) + h ( 1 ) \* h ( 1 ) + h ( 2 ) \* h ( 0 ) = 1 \* 2 + 1 \* 1 + 2 \* 1 = 5 - -再看看解法二的算法 - -```java -public int numTrees(int n) { - int[] dp = new int[n + 1]; - dp[0] = 1; - if (n == 0) { - return 0; - } - // 长度为 1 到 n - for (int len = 1; len <= n; len++) { - // 将不同的数字作为根节点,只需要考虑到 len - for (int root = 1; root <= len; root++) { - int left = root - 1; // 左子树的长度 - int right = len - root; // 右子树的长度 - dp[len] += dp[left] * dp[right]; - } - } - return dp[n]; -} -``` - -完美符合,而卡塔兰数有一个通项公式。 - - - -![](https://windliang.oss-cn-beijing.aliyuncs.com/96_2.png) - -注:$$\binom{2n}{n}$$ 代表 $$C^n_{2n}$$ - -化简一下上边的公式 - -$$C_n = (2n)!/(n+1)!n! = (2n)*(2n-1)*...*(n+1)/(n+1)!$$ - -所以用一个循环即可。 - -```java -int numTrees(int n) { - long ans = 1, i; - for (i = 1; i <= n; i++) - ans = ans * (i + n) / i; - return (int) (ans / i); -} - -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/96.jpg) + +和 [95 题]()一样,只不过这道题不需要输出所有的树,只需要输出所有可能的二分查找树的数量。所以完全按照 95 题思路写,大家可以先到 [95 题]()看一看。 + +# 解法一 递归 + +下边是 95 题的分析。 + +> 我们可以利用一下查找二叉树的性质。左子树的所有值小于根节点,右子树的所有值大于根节点。 +> +> 所以如果求 1...n 的所有可能。 +> +> 我们只需要把 1 作为根节点,[ ] 空作为左子树,[ 2 ... n ] 的所有可能作为右子树。 +> +> 2 作为根节点,[ 1 ] 作为左子树,[ 3...n ] 的所有可能作为右子树。 +> +> 3 作为根节点,[ 1 2 ] 的所有可能作为左子树,[ 4 ... n ] 的所有可能作为右子树,然后左子树和右子树两两组合。 +> +> 4 作为根节点,[ 1 2 3 ] 的所有可能作为左子树,[ 5 ... n ] 的所有可能作为右子树,然后左子树和右子树两两组合。 +> +> ... +> +> n 作为根节点,[ 1... n ] 的所有可能作为左子树,[ ] 作为右子树。 +> +> 至于,[ 2 ... n ] 的所有可能以及 [ 4 ... n ] 以及其他情况的所有可能,可以利用上边的方法,把每个数字作为根节点,然后把所有可能的左子树和右子树组合起来即可。 +> +> 如果只有一个数字,那么所有可能就是一种情况,把该数字作为一棵树。而如果是 [ ],那就返回 null。 + +对于这道题,我们会更简单些,只需要返回树的数量即可。求当前根的数量,只需要左子树的数量乘上右子树。 + +```java +public int numTrees(int n) { + if (n == 0) { + return 0; + } + return getAns(1, n); + +} +private int getAns(int start, int end) { + int ans = 0; + //此时没有数字,只有一个数字,返回 1 + if (start >= end) { + return 1; + } + //尝试每个数字作为根节点 + for (int i = start; i <= end; i++) { + //得到所有可能的左子树 + int leftTreesNum = getAns(start, i - 1); + //得到所有可能的右子树 + int rightTreesNum = getAns(i + 1, end); + //左子树右子树两两组合 + ans+=leftTreesNum * rightTreesNum; + } + return ans; +} +``` + +受到[这里]()的启发,我们甚至可以改写的更简单些。因为 95 题要把每颗树返回,所有传的参数是 start 和 end。这里的话,我们只关心数量,所以不需要具体的范围,而是传树的节点的数量即可。 + +```java +public int numTrees(int n) { + if (n == 0) { + return 0; + } + return getAns(n); + +} + +private int getAns(int n) { + int ans = 0; + //此时没有数字或者只有一个数字,返回 1 + if (n==0 ||n==1) { + return 1; + } + //尝试每个数字作为根节点 + for (int i = 1; i <= n; i++) { + //得到所有可能的左子树 + // i - 1 代表左子树节点的数量 + int leftTreesNum = getAns(i-1); + //得到所有可能的右子树 + //n - i 代表左子树节点的数量 + int rightTreesNum = getAns(n-i); + //左子树右子树两两组合 + ans+=leftTreesNum * rightTreesNum; + } + return ans; +} +``` + +然后,由于递归的分叉,所以会导致很多重复解的计算,所以使用 memoization 技术,把递归过程中求出的解保存起来,第二次需要的时候直接拿即可。 + +```java +public int numTrees(int n) { + if (n == 0) { + return 0; + } + HashMap memoization = new HashMap<>(); + return getAns(n,memoization); + +} + +private int getAns(int n, HashMap memoization) { + if(memoization.containsKey(n)){ + return memoization.get(n); + } + int ans = 0; + //此时没有数字,只有一个数字,返回 1 + if (n==0 ||n==1) { + return 1; + } + //尝试每个数字作为根节点 + for (int i = 1; i <= n; i++) { + //得到所有可能的左子树 + int leftTreesNum = getAns(i-1,memoization); + //得到所有可能的右子树 + int rightTreesNum = getAns(n-i,memoization); + //左子树右子树两两组合 + ans+=leftTreesNum * rightTreesNum; + } + memoization.put(n, ans); + return ans; +} +``` + +# 解法二 动态规划 + +直接利用[95题]()解法三的思路,讲解比较长就不贴过来了,可以过去看一下。 + +或者直接从这里的解法一的思路考虑,因为递归是从顶层往下走,压栈压栈压栈,到了长度是 0 或者是 1 就出栈出栈出栈。我们可以利用动态规划的思想,直接从底部往上走。求出长度是 0,长度是 1,长度是 2....长度是 n 的解。用一个数组 dp 把这些结果全部保存起来。 + +```java +public int numTrees(int n) { + int[] dp = new int[n + 1]; + dp[0] = 1; + if (n == 0) { + return 0; + } + // 长度为 1 到 n + for (int len = 1; len <= n; len++) { + // 将不同的数字作为根节点,只需要考虑到 len + for (int root = 1; root <= len; root++) { + int left = root - 1; // 左子树的长度 + int right = len - root; // 右子树的长度 + dp[len] += dp[left] * dp[right]; + } + } + return dp[n]; +} +``` + +参考[这里]()还有优化的空间。 + +利用对称性,可以使得循环减少一些。 + +* n 是偶数的时候 + 1 2 | 3 4 ,for 循环中我们以每个数字为根求出每个的解。我们其实可以只求一半,根据对称性我们可以知道 1 和 4,2 和 3 求出的解分别是相等的。 + +* n 是奇数的时候 + + 1 2 | 3 | 4 5,和偶数同理,只求一半,此外最中间的 3 的解也要加上。 + +```java +public int numTrees6(int n) { + if (n == 0) { + return 0; + } + int[] dp = new int[n + 1]; + dp[0] = 1; + dp[1] = 1; + // 长度为 1 到 n + for (int len = 2; len <= n; len++) { + // 将不同的数字作为根节点,只需要考虑到 len + for (int root = 1; root <= len / 2; root++) { + int left = root - 1; // 左子树的长度 + int right = len - root; // 右子树的长度 + dp[len] += dp[left] * dp[right]; + } + dp[len] *= 2;// 利用对称性乘 2 + // 考虑奇数的情况 + if ((len & 1) == 1) { + int root = (len >> 1) + 1; + int left = root - 1; // 左子树的长度 + int right = len - root; // 右子树的长度 + dp[len] += dp[left] * dp[right]; + } + } + return dp[n]; +} +``` + +# 解法三 公式法 + +参考[这里]()。其实利用的是卡塔兰数列,这是第二次遇到了,之前是第 [22 题]() ,生成合法的括号序列。 + +这道题,为什么和卡塔兰数列联系起来呢? + +看一下卡塔兰树数列的定义: + +> 令h ( 0 ) = 1,catalan 数满足递推式: +> +> **h ( n ) = h ( 0 ) \* h ( n - 1 ) + h ( 1 ) \* h ( n - 2 ) + ... + h ( n - 1 ) \* h ( 0 ) ( n >=1 )** +> +> 例如:h ( 2 ) = h ( 0 ) \* h ( 1 ) + h ( 1 ) \* h ( 0 ) = 1 \* 1 + 1 * 1 = 2 +> +> h ( 3 ) = h ( 0 ) \* h ( 2 ) + h ( 1 ) \* h ( 1 ) + h ( 2 ) \* h ( 0 ) = 1 \* 2 + 1 \* 1 + 2 \* 1 = 5 + +再看看解法二的算法 + +```java +public int numTrees(int n) { + int[] dp = new int[n + 1]; + dp[0] = 1; + if (n == 0) { + return 0; + } + // 长度为 1 到 n + for (int len = 1; len <= n; len++) { + // 将不同的数字作为根节点,只需要考虑到 len + for (int root = 1; root <= len; root++) { + int left = root - 1; // 左子树的长度 + int right = len - root; // 右子树的长度 + dp[len] += dp[left] * dp[right]; + } + } + return dp[n]; +} +``` + +完美符合,而卡塔兰数有一个通项公式。 + + + +![](https://windliang.oss-cn-beijing.aliyuncs.com/96_2.png) + +注:$$\binom{2n}{n}$$ 代表 $$C^n_{2n}$$ + +化简一下上边的公式 + +$$C_n = (2n)!/(n+1)!n! = (2n)*(2n-1)*...*(n+1)/(n+1)!$$ + +所以用一个循环即可。 + +```java +int numTrees(int n) { + long ans = 1, i; + for (i = 1; i <= n; i++) + ans = ans * (i + n) / i; + return (int) (ans / i); +} + +``` + +# 总 + 上道题会了以后,这道题很好写。解法二中利用对称的优化,解法三的公式太强了。 \ No newline at end of file diff --git a/leetCode-97-Interleaving-String.md b/leetCode-97-Interleaving-String.md index 2ec616a4e..746eb2a26 100644 --- a/leetCode-97-Interleaving-String.md +++ b/leetCode-97-Interleaving-String.md @@ -1,334 +1,334 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/97.jpg) - -在两个字符串 s1 和 s2 中依次取字母,问是否可以组成 S3。什么意思呢?比如 s1 = abc , s2 = de,s3 = abdce。 - -s1 取 1 个 字母得到 a,s1 再取个字母得到 ab,s2 取个字母得到 abd, s1 取 1 个 字母得到 abdc, s2 取 1 个 字母得到 abdce,然后就得到了 s3,所以返回 true。 - -# 解法一 回溯法 - -如果我们简化下问题,如果 s1 和 s2 中不含有重复的字母,比如 s1 = abc,s2 = de,s3 = abdce。 - -这样是不是就简单多了。我们只需要三个指针,依次遍历字符串。 - -```java -i 和 k 的指的字母相等,所以 i 后移,k 后移 -a b c -^ -i - -d e -^ -j - -a b d c e -^ -k - -i 和 k 的指的字母相等,所以 i 后移,k 后移 -a b c - ^ - i - -d e -^ -j - -a b d c e - ^ - k - -j 和 k 的指的字母相等,所以 j 后移,k 后移 -a b c - ^ - i - -d e -^ -j - -a b d c e - ^ - k - -就这样比较下去,如果 i,j,k 都成功移动到了末尾即成功。 -``` - -但是这道题 s1 和 s2 中会有重复的字符出现,比如下边的情况 - -```java -a d c - ^ - i - -d e -^ -j - -a d c e - ^ - k -``` - -此时 i 和 j 指向的字母都和 k 相等,此时该怎么办呢? - -回溯法!是的,我们先尝试 i 和 k 后移,然后看能不能成功。不行的话我们再回溯回来,把 j 和 k 后移。 - -```java -public boolean isInterleave(String s1, String s2, String s3) { - return getAns(s1, 0, s2, 0, s3, 0); -} - -private boolean getAns(String s1, int i, String s2, int j, String s3, int k) { - //长度不匹配直接返回 false - if (s1.length() + s2.length() != s3.length()) { - return false; - } - // i、j、k 全部达到了末尾就返回 true - if (i == s1.length() && j == s2.length() && k == s3.length()) { - return true; - } - // i 到达了末尾,直接移动 j 和 k 不停比较 - if (i == s1.length()) { - while (j < s2.length()) { - if (s2.charAt(j) != s3.charAt(k)) { - return false; - } - j++; - k++; - } - return true; - } - // j 到达了末尾,直接移动 i 和 k 不停比较 - if (j == s2.length()) { - while (i < s1.length()) { - if (s1.charAt(i) != s3.charAt(k)) { - return false; - } - i++; - k++; - } - return true; - } - //判断 i 和 k 指向的字符是否相等 - if (s1.charAt(i) == s3.charAt(k)) { - //后移 i 和 k 继续判断,如果成功了直接返回 true - if (getAns(s1, i + 1, s2, j, s3, k + 1)) { - return true; - } - } - //移动 i 和 k 失败,尝试移动 j 和 k - if (s2.charAt(j) == s3.charAt(k)) { - if (getAns(s1, i, s2, j + 1, s3, k + 1)) { - return true; - } - } - //移动 i 和 j 都失败,返回 false - return false; -} -``` -让我们优化一下,由于递归的分支,所以会造成很多重复情况的判断,所以我们用 memoization 技术,把求出的结果用 hashmap 保存起来,第二次过来的时候直接返回结果以免再次进入递归。 - -用 1 表示 true,0 表示 false,-1 代表还未赋值。 - -hashmap key 的话用字符串 i + "@" + j ,之所以中间加 "@",是为了防止 i = 1 和 j = 22。以及 i = 12,j = 2。这样的两种情况产生的就都是 122。加上 "@" 可以区分开来。 - -```java -public boolean isInterleave(String s1, String s2, String s3) { - HashMap memoization = new HashMap<>(); - return getAns(s1, 0, s2, 0, s3, 0, memoization); -} - -private boolean getAns(String s1, int i, String s2, int j, String s3, int k, HashMap memoization) { - if (s1.length() + s2.length() != s3.length()) { - return false; - } - String key = i + "@" + j; - if (memoization.containsKey(key)) { - return memoization.getOrDefault(key, -1) == 1; - } - if (i == s1.length() && j == s2.length() && k == s3.length()) { - memoization.put(key, 1); - return true; - } - if (i == s1.length()) { - while (j < s2.length()) { - if (s2.charAt(j) != s3.charAt(k)) { - memoization.put(key, 0); - return false; - } - j++; - k++; - } - memoization.put(key, 1); - return true; - } - - if (j == s2.length()) { - while (i < s1.length()) { - if (s1.charAt(i) != s3.charAt(k)) { - memoization.put(key, 0); - return false; - } - i++; - k++; - } - memoization.put(key, 1); - return true; - } - if (s1.charAt(i) == s3.charAt(k)) { - if (getAns(s1, i + 1, s2, j, s3, k + 1, memoization)) { - memoization.put(key, 1); - return true; - } - } - if (s2.charAt(j) == s3.charAt(k)) { - if (getAns(s1, i, s2, j + 1, s3, k + 1, memoization)) { - memoization.put(key, 1); - return true; - } - } - memoization.put(key, 0); - return false; -} -``` - -# 解法二 动态规划 - -参考[这里]()。 - -其实和递归本质上是一样的,解法一中压栈到末尾最后一个字符的时候,再次压栈,就会进入 if (i == s1.length() && j == s2.length() && k == s3.length()) 这里,然后就开始一系列的出栈过程。 - -而动态规划就是利用一个 dp 数组去省去压栈,所谓空间换时间。这里的话,我们也不模仿递归从尾部开始了,我们直接从开头开始,思想是一样的。 - -我们定义一个 boolean 二维数组 dp \[ i \] \[ j \] 来表示 s1[ 0, i ) 和 s2 [ 0, j ) 组合后能否构成 s3 [ 0, i + j ),注意不包括右边界,主要是为了考虑开始的时候如果只取 s1,那么 s2 就是空串,这样的话 dp \[ i \] \[ 0 \] 就能表示 s2 取空串。 - -状态转换方程也很好写了,如果要求 dp \[ i \] \[ j \] 。 - -如果 dp \[ i - 1 \] \[ j \] == true,并且 s1 [ i - 1 ] == s3 [ i + j - 1], dp \[ i \] \[ j \] = true 。 - -如果 dp \[ i \] \[ j - 1 \] == true,并且 s2 [ j - 1 ] == s3 [ i + j - 1], dp \[ i \] \[ j \] = true 。 - -否则的话,就更新为 dp \[ i \] \[ j \] = false。 - -如果 i 为 0,或者 j 为 0,那直接判断 s2 和 s3 对应的字母或者 s1 和 s3 对应的字母即可。 - -```java -public boolean isInterleave(String s1, String s2, String s3) { - if (s1.length() + s2.length() != s3.length()) { - return false; - } - boolean[][] dp = new boolean[s1.length() + 1][s2.length() + 1]; - for (int i = 0; i <= s1.length(); i++) { - for (int j = 0; j <= s2.length(); j++) { - if (i == 0 && j == 0) { - dp[i][j] = true; - } else if (i == 0) { - dp[i][j] = dp[i][j - 1] && s2.charAt(j - 1) == s3.charAt(j - 1); - } else if (j == 0) { - dp[i][j] = dp[i - 1][j] && s1.charAt(i - 1) == s3.charAt(i - 1); - } else { - dp[i][j] = dp[i - 1][j] && s1.charAt(i - 1) == s3.charAt(i + j - 1) - || dp[i][j - 1] && s2.charAt(j - 1) == s3.charAt(i + j - 1); - } - } - } - return dp[s1.length()][s2.length()]; -} -``` - -然后就是老规矩了,空间复杂度的优化,例如[5题](),[10题](),[53题](),[72题]()等等都是同样的思路。都是注意到一个特点,当更新到 dp \[ i \] \[ j \] 的时候,我们只用到 dp \[ i - 1 \] \[ j \] ,即上一层的数据,再之前的数据就没有用了。所以我们不需要二维数组,只需要一个一维数组就够了。 - -```java -public boolean isInterleave(String s1, String s2, String s3) { - if (s1.length() + s2.length() != s3.length()) { - return false; - } - boolean[] dp = new boolean[s2.length() + 1]; - for (int i = 0; i <= s1.length(); i++) { - for (int j = 0; j <= s2.length(); j++) { - if (i == 0 && j == 0) { - dp[j] = true; - } else if (i == 0) { - dp[j] = dp[j - 1] && s2.charAt(j - 1) == s3.charAt(j - 1); - } else if (j == 0) { - dp[j] = dp[j] && s1.charAt(i - 1) == s3.charAt(i - 1); - } else { - dp[j] = dp[j] && s1.charAt(i - 1) == s3.charAt(i + j - 1) - || dp[j - 1] && s2.charAt(j - 1) == s3.charAt(i + j - 1); - } - } - } - return dp[s2.length()]; -} -``` - -# 解法三 广度优先遍历 BFS - -参考[这里]()。我们把问题抽象一下。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/97_2.jpg) - -从左上角到达右下角,遍历过程加上边对应的字符,最后就可以产生 S3 了。回想一下,解法一递归的遍历过程,其实就是图的深度遍历,从 0 位置出发,一致尝试向右,不行的话就回溯,再尝试向下,然后再开始尝试向右,直到右下角。像一只贪婪的蛇,认准目标直奔而去。 - -而解法一开始没有优化前讲到说会有很多重复的解,结合上边的图也刚好理解了。因为开始尝试了条路后,回退回退回退,然后再向前的时候就可能回到原来的路上了。 - -这里的话,既然都已经抽象出一个图了,所以除了 DFS,当然还有 BFS。尝试遍历整个图,如果到达了右下角就返回 true。 - -当然任意两个节点并不是都可以到达的,只有当前要遍历的 S1 或者 S2 对应的字母和 S3 相应的字母相等我们才可以遍历。 - -用一个队列保存可以遍历的节点,然后不停的从队列里取元素,然后把可以到达的新的节点加到队列中。 - -```java -class Point { - int x; - int y; - - Point(int x, int y) { - this.x = x; - this.y = y; - } -} -class Solution { - public boolean isInterleave(String s1, String s2, String s3) { - if (s1.length() + s2.length() != s3.length()) { - return false; - } - Queue queue = new LinkedList(); - queue.add(new Point(0, 0)); - //判断是否已经遍历过 - boolean[][] visited = new boolean[s1.length() + 1][s2.length() + 1]; - while (!queue.isEmpty()) { - Point cur = queue.poll(); - //到达右下角就返回 true - if (cur.x == s1.length() && cur.y == s2.length()) { - return true; - } - // 尝试是否能向右走 - int right = cur.x + 1; - if (right <= s1.length() && s1.charAt(right - 1) == s3.charAt(right + cur.y - 1)) { - if (!visited[right][cur.y]) { - visited[right][cur.y] = true; - queue.offer(new Point(right, cur.y)); - } - } - - // 尝试是否能向下走 - int down = cur.y + 1; - if (down <= s2.length() && s2.charAt(down - 1) == s3.charAt(down + cur.x - 1)) { - if (!visited[cur.x][down]) { - visited[cur.x][down] = true; - queue.offer(new Point(cur.x, down)); - } - } - - } - return false; -} -} -``` - -# 总 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/97.jpg) + +在两个字符串 s1 和 s2 中依次取字母,问是否可以组成 S3。什么意思呢?比如 s1 = abc , s2 = de,s3 = abdce。 + +s1 取 1 个 字母得到 a,s1 再取个字母得到 ab,s2 取个字母得到 abd, s1 取 1 个 字母得到 abdc, s2 取 1 个 字母得到 abdce,然后就得到了 s3,所以返回 true。 + +# 解法一 回溯法 + +如果我们简化下问题,如果 s1 和 s2 中不含有重复的字母,比如 s1 = abc,s2 = de,s3 = abdce。 + +这样是不是就简单多了。我们只需要三个指针,依次遍历字符串。 + +```java +i 和 k 的指的字母相等,所以 i 后移,k 后移 +a b c +^ +i + +d e +^ +j + +a b d c e +^ +k + +i 和 k 的指的字母相等,所以 i 后移,k 后移 +a b c + ^ + i + +d e +^ +j + +a b d c e + ^ + k + +j 和 k 的指的字母相等,所以 j 后移,k 后移 +a b c + ^ + i + +d e +^ +j + +a b d c e + ^ + k + +就这样比较下去,如果 i,j,k 都成功移动到了末尾即成功。 +``` + +但是这道题 s1 和 s2 中会有重复的字符出现,比如下边的情况 + +```java +a d c + ^ + i + +d e +^ +j + +a d c e + ^ + k +``` + +此时 i 和 j 指向的字母都和 k 相等,此时该怎么办呢? + +回溯法!是的,我们先尝试 i 和 k 后移,然后看能不能成功。不行的话我们再回溯回来,把 j 和 k 后移。 + +```java +public boolean isInterleave(String s1, String s2, String s3) { + return getAns(s1, 0, s2, 0, s3, 0); +} + +private boolean getAns(String s1, int i, String s2, int j, String s3, int k) { + //长度不匹配直接返回 false + if (s1.length() + s2.length() != s3.length()) { + return false; + } + // i、j、k 全部达到了末尾就返回 true + if (i == s1.length() && j == s2.length() && k == s3.length()) { + return true; + } + // i 到达了末尾,直接移动 j 和 k 不停比较 + if (i == s1.length()) { + while (j < s2.length()) { + if (s2.charAt(j) != s3.charAt(k)) { + return false; + } + j++; + k++; + } + return true; + } + // j 到达了末尾,直接移动 i 和 k 不停比较 + if (j == s2.length()) { + while (i < s1.length()) { + if (s1.charAt(i) != s3.charAt(k)) { + return false; + } + i++; + k++; + } + return true; + } + //判断 i 和 k 指向的字符是否相等 + if (s1.charAt(i) == s3.charAt(k)) { + //后移 i 和 k 继续判断,如果成功了直接返回 true + if (getAns(s1, i + 1, s2, j, s3, k + 1)) { + return true; + } + } + //移动 i 和 k 失败,尝试移动 j 和 k + if (s2.charAt(j) == s3.charAt(k)) { + if (getAns(s1, i, s2, j + 1, s3, k + 1)) { + return true; + } + } + //移动 i 和 j 都失败,返回 false + return false; +} +``` +让我们优化一下,由于递归的分支,所以会造成很多重复情况的判断,所以我们用 memoization 技术,把求出的结果用 hashmap 保存起来,第二次过来的时候直接返回结果以免再次进入递归。 + +用 1 表示 true,0 表示 false,-1 代表还未赋值。 + +hashmap key 的话用字符串 i + "@" + j ,之所以中间加 "@",是为了防止 i = 1 和 j = 22。以及 i = 12,j = 2。这样的两种情况产生的就都是 122。加上 "@" 可以区分开来。 + +```java +public boolean isInterleave(String s1, String s2, String s3) { + HashMap memoization = new HashMap<>(); + return getAns(s1, 0, s2, 0, s3, 0, memoization); +} + +private boolean getAns(String s1, int i, String s2, int j, String s3, int k, HashMap memoization) { + if (s1.length() + s2.length() != s3.length()) { + return false; + } + String key = i + "@" + j; + if (memoization.containsKey(key)) { + return memoization.getOrDefault(key, -1) == 1; + } + if (i == s1.length() && j == s2.length() && k == s3.length()) { + memoization.put(key, 1); + return true; + } + if (i == s1.length()) { + while (j < s2.length()) { + if (s2.charAt(j) != s3.charAt(k)) { + memoization.put(key, 0); + return false; + } + j++; + k++; + } + memoization.put(key, 1); + return true; + } + + if (j == s2.length()) { + while (i < s1.length()) { + if (s1.charAt(i) != s3.charAt(k)) { + memoization.put(key, 0); + return false; + } + i++; + k++; + } + memoization.put(key, 1); + return true; + } + if (s1.charAt(i) == s3.charAt(k)) { + if (getAns(s1, i + 1, s2, j, s3, k + 1, memoization)) { + memoization.put(key, 1); + return true; + } + } + if (s2.charAt(j) == s3.charAt(k)) { + if (getAns(s1, i, s2, j + 1, s3, k + 1, memoization)) { + memoization.put(key, 1); + return true; + } + } + memoization.put(key, 0); + return false; +} +``` + +# 解法二 动态规划 + +参考[这里]()。 + +其实和递归本质上是一样的,解法一中压栈到末尾最后一个字符的时候,再次压栈,就会进入 if (i == s1.length() && j == s2.length() && k == s3.length()) 这里,然后就开始一系列的出栈过程。 + +而动态规划就是利用一个 dp 数组去省去压栈,所谓空间换时间。这里的话,我们也不模仿递归从尾部开始了,我们直接从开头开始,思想是一样的。 + +我们定义一个 boolean 二维数组 dp \[ i \] \[ j \] 来表示 s1[ 0, i ) 和 s2 [ 0, j ) 组合后能否构成 s3 [ 0, i + j ),注意不包括右边界,主要是为了考虑开始的时候如果只取 s1,那么 s2 就是空串,这样的话 dp \[ i \] \[ 0 \] 就能表示 s2 取空串。 + +状态转换方程也很好写了,如果要求 dp \[ i \] \[ j \] 。 + +如果 dp \[ i - 1 \] \[ j \] == true,并且 s1 [ i - 1 ] == s3 [ i + j - 1], dp \[ i \] \[ j \] = true 。 + +如果 dp \[ i \] \[ j - 1 \] == true,并且 s2 [ j - 1 ] == s3 [ i + j - 1], dp \[ i \] \[ j \] = true 。 + +否则的话,就更新为 dp \[ i \] \[ j \] = false。 + +如果 i 为 0,或者 j 为 0,那直接判断 s2 和 s3 对应的字母或者 s1 和 s3 对应的字母即可。 + +```java +public boolean isInterleave(String s1, String s2, String s3) { + if (s1.length() + s2.length() != s3.length()) { + return false; + } + boolean[][] dp = new boolean[s1.length() + 1][s2.length() + 1]; + for (int i = 0; i <= s1.length(); i++) { + for (int j = 0; j <= s2.length(); j++) { + if (i == 0 && j == 0) { + dp[i][j] = true; + } else if (i == 0) { + dp[i][j] = dp[i][j - 1] && s2.charAt(j - 1) == s3.charAt(j - 1); + } else if (j == 0) { + dp[i][j] = dp[i - 1][j] && s1.charAt(i - 1) == s3.charAt(i - 1); + } else { + dp[i][j] = dp[i - 1][j] && s1.charAt(i - 1) == s3.charAt(i + j - 1) + || dp[i][j - 1] && s2.charAt(j - 1) == s3.charAt(i + j - 1); + } + } + } + return dp[s1.length()][s2.length()]; +} +``` + +然后就是老规矩了,空间复杂度的优化,例如[5题](),[10题](),[53题](),[72题]()等等都是同样的思路。都是注意到一个特点,当更新到 dp \[ i \] \[ j \] 的时候,我们只用到 dp \[ i - 1 \] \[ j \] ,即上一层的数据,再之前的数据就没有用了。所以我们不需要二维数组,只需要一个一维数组就够了。 + +```java +public boolean isInterleave(String s1, String s2, String s3) { + if (s1.length() + s2.length() != s3.length()) { + return false; + } + boolean[] dp = new boolean[s2.length() + 1]; + for (int i = 0; i <= s1.length(); i++) { + for (int j = 0; j <= s2.length(); j++) { + if (i == 0 && j == 0) { + dp[j] = true; + } else if (i == 0) { + dp[j] = dp[j - 1] && s2.charAt(j - 1) == s3.charAt(j - 1); + } else if (j == 0) { + dp[j] = dp[j] && s1.charAt(i - 1) == s3.charAt(i - 1); + } else { + dp[j] = dp[j] && s1.charAt(i - 1) == s3.charAt(i + j - 1) + || dp[j - 1] && s2.charAt(j - 1) == s3.charAt(i + j - 1); + } + } + } + return dp[s2.length()]; +} +``` + +# 解法三 广度优先遍历 BFS + +参考[这里]()。我们把问题抽象一下。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/97_2.jpg) + +从左上角到达右下角,遍历过程加上边对应的字符,最后就可以产生 S3 了。回想一下,解法一递归的遍历过程,其实就是图的深度遍历,从 0 位置出发,一致尝试向右,不行的话就回溯,再尝试向下,然后再开始尝试向右,直到右下角。像一只贪婪的蛇,认准目标直奔而去。 + +而解法一开始没有优化前讲到说会有很多重复的解,结合上边的图也刚好理解了。因为开始尝试了条路后,回退回退回退,然后再向前的时候就可能回到原来的路上了。 + +这里的话,既然都已经抽象出一个图了,所以除了 DFS,当然还有 BFS。尝试遍历整个图,如果到达了右下角就返回 true。 + +当然任意两个节点并不是都可以到达的,只有当前要遍历的 S1 或者 S2 对应的字母和 S3 相应的字母相等我们才可以遍历。 + +用一个队列保存可以遍历的节点,然后不停的从队列里取元素,然后把可以到达的新的节点加到队列中。 + +```java +class Point { + int x; + int y; + + Point(int x, int y) { + this.x = x; + this.y = y; + } +} +class Solution { + public boolean isInterleave(String s1, String s2, String s3) { + if (s1.length() + s2.length() != s3.length()) { + return false; + } + Queue queue = new LinkedList(); + queue.add(new Point(0, 0)); + //判断是否已经遍历过 + boolean[][] visited = new boolean[s1.length() + 1][s2.length() + 1]; + while (!queue.isEmpty()) { + Point cur = queue.poll(); + //到达右下角就返回 true + if (cur.x == s1.length() && cur.y == s2.length()) { + return true; + } + // 尝试是否能向右走 + int right = cur.x + 1; + if (right <= s1.length() && s1.charAt(right - 1) == s3.charAt(right + cur.y - 1)) { + if (!visited[right][cur.y]) { + visited[right][cur.y] = true; + queue.offer(new Point(right, cur.y)); + } + } + + // 尝试是否能向下走 + int down = cur.y + 1; + if (down <= s2.length() && s2.charAt(down - 1) == s3.charAt(down + cur.x - 1)) { + if (!visited[cur.x][down]) { + visited[cur.x][down] = true; + queue.offer(new Point(cur.x, down)); + } + } + + } + return false; +} +} +``` + +# 总 + 很经典的一道题了,第一次用到了 BFS,之前都是 DFS。最后的图,其实把所有的解法的本质都揭露了出来。 \ No newline at end of file diff --git a/leetCode-98-Validate-Binary-Search-Tree.md b/leetCode-98-Validate-Binary-Search-Tree.md index e851ee0ae..9f60092d3 100644 --- a/leetCode-98-Validate-Binary-Search-Tree.md +++ b/leetCode-98-Validate-Binary-Search-Tree.md @@ -1,345 +1,345 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/98.jpg) - -输入一个树,判断该树是否是合法二分查找树,[95]()题做过生成二分查找树。二分查找树定义如下: - -> 1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值; -> 2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值; -> 3. 任意节点的左、右子树也分别为二叉查找树; -> 4. 没有键值相等的节点。 - -# 解法一 - -开始的时候以为可以很简单的用递归写出来。想法是,左子树是合法二分查找树,右子树是合法二分查找树,并且根节点大于左孩子,小于右孩子,那么当前树就是合法二分查找树。代码如下: - -```java -public boolean isValidBST(TreeNode root) { - if (root == null) { - return true; - } - boolean leftVailid = true; - boolean rightVaild = true; - if (root.left != null) { - //大于左孩子并且左子树是合法二分查找树 - leftVailid = root.val > root.left.val && isValidBST(root.left); - } - if (!leftVailid) { - return false; - } - if (root.right != null) { - //小于右孩子并且右子树是合法二分查找树 - rightVaild = root.val < root.right.val && isValidBST(root.right); - } - return rightVaild; -} -``` - -当然,这个解法没有通过。对于下面的解,结果利用上边的解法是错误的。 - -```java - 10 - / \ - 5 15 - / \ - 6 20 -``` - -虽然满足左子树是合法二分查找树,右子树是合法二分查找树,并且根节点大于左孩子,小于右孩子,但这个树不是合法的二分查找树。因为右子树中的 6 小于当前根节点 10。所以我们不应该判断「根节点大于左孩子,小于右孩子」,而是判断「根节点大于左子树中最大的数,小于右子树中最小的数」。 - -```java -public boolean isValidBST(TreeNode root) { - if (root == null || root.left == null && root.right == null) { - return true; - } - //左子树是否合法 - if (isValidBST(root.left)) { - if (root.left != null) { - int max = getMaxOfBST(root.left);//得到左子树中最大的数 - if (root.val <= max) { //相等的情况,代表有重复的数字 - return false; - } - } - - } else { - return false; - } - - //右子树是否合法 - if (isValidBST(root.right)) { - if (root.right != null) { - int min = getMinOfBST(root.right);//得到右子树中最小的数 - if (root.val >= min) { //相等的情况,代表有重复的数字 - return false; - } - } - - } else { - return false; - } - return true; -} - -private int getMinOfBST(TreeNode root) { - int min = root.val; - while (root != null) { - if (root.val <= min) { - min = root.val; - } - root = root.left; - } - return min; -} - -private int getMaxOfBST(TreeNode root) { - int max = root.val; - while (root != null) { - if (root.val >= max) { - max = root.val; - } - root = root.right; - } - return max; -} -``` - -# 解法二 - -来利用另一种思路,参考[官方题解]()。 - -解法一中,我们是判断根节点是否合法,找到了左子树中最大的数,右子树中最小的数。 由左子树和右子树决定当前根节点是否合法。 - -但如果正常的来讲,明明先有的根节点,按理说根节点是任何数都行,而不是由左子树和右子树限定。相反,根节点反而决定了左孩子和右孩子的合法取值范围。 - -所以,我们可以从根节点进行 DFS,然后计算每个节点应该的取值范围,如果当前节点不符合就返回 false。 - -```java - 10 - / \ - 5 15 - / \ / - 3 6 7 - - 考虑 10 的范围 - 10(-inf,+inf) - - 考虑 5 的范围 - 10(-inf,+inf) - / - 5(-inf,10) - - 考虑 3 的范围 - 10(-inf,+inf) - / - 5(-inf,10) - / - 3(-inf,5) - - 考虑 6 的范围 - 10(-inf,+inf) - / - 5(-inf,10) - / \ - 3(-inf,5) 6(5,10) - - 考虑 15 的范围 - 10(-inf,+inf) - / \ - 5(-inf,10) 15(10,+inf) - / \ - 3(-inf,5) 6(5,10) - - 考虑 7 的范围,出现不符合返回 false - 10(-inf,+inf) - / \ -5(-inf,10) 15(10,+inf) - / \ / -3(-inf,5) 6(5,10) 7(10,15) - - -``` - -可以观察到,左孩子的范围是 (父结点左边界,父节点的值),右孩子的范围是(父节点的值,父节点的右边界)。 - -还有个问题,java 里边没有提供负无穷和正无穷,用什么数来表示呢? - -方案一,假设我们的题目的数值都是 Integer 范围的,那么我们用不在 Integer 范围的数字来表示负无穷和正无穷。用 long 去存储。 - -```java -public boolean isValidBST(TreeNode root) { - long maxValue = (long)Integer.MAX_VALUE + 1; - long minValue = (long)Integer.MIN_VALUE - 1; - return getAns(root, minValue, maxValue); -} - -private boolean getAns(TreeNode node, long minVal, long maxVal) { - if (node == null) { - return true; - } - if (node.val <= minVal) { - return false; - } - if (node.val >= maxVal) { - return false; - } - return getAns(node.left, minVal, node.val) && getAns(node.right, node.val, maxVal); -} -``` - - - -方案二:传入 Integer 对象,然后 null 表示负无穷和正无穷。然后利用 JAVA 的自动装箱拆箱,数值的比较可以直接用不等号。 - -```java -public boolean isValidBST(TreeNode root) { - return getAns(root, null, null); -} - -private boolean getAns(TreeNode node, Integer minValue, Integer maxValue) { - if (node == null) { - return true; - } - if (minValue != null && node.val <= minValue) { - return false; - } - if (maxValue != null && node.val >= maxValue) { - return false; - } - return getAns(node.left, minValue, node.val) && getAns(node.right, node.val, maxValue); -} -``` -# 解法三 DFS BFS - -解法二其实就是树的 DFS,也就是二叉树的先序遍历,然后在遍历过程中,判断当前的值是是否在区间中。所以我们可以用栈来模拟递归过程。 - -```java -public boolean isValidBST(TreeNode root) { - if (root == null || root.left == null && root.right == null) { - return true; - } - //利用三个栈来保存对应的节点和区间 - LinkedList stack = new LinkedList<>(); - LinkedList minValues = new LinkedList<>(); - LinkedList maxValues = new LinkedList<>(); - //头结点入栈 - TreeNode pNode = root; - stack.push(pNode); - minValues.push(null); - maxValues.push(null); - while (pNode != null || !stack.isEmpty()) { - if (pNode != null) { - //判断栈顶元素是否符合 - Integer minValue = minValues.peek(); - Integer maxValue = maxValues.peek(); - TreeNode node = stack.peek(); - if (minValue != null && node.val <= minValue) { - return false; - } - if (maxValue != null && node.val >= maxValue) { - return false; - } - //将左孩子加入到栈 - if(pNode.left!=null){ - stack.push(pNode.left); - minValues.push(minValue); - maxValues.push(pNode.val); - } - - pNode = pNode.left; - } else { // pNode == null && !stack.isEmpty() - //出栈,将右孩子加入栈中 - TreeNode node = stack.pop(); - minValues.pop(); - Integer maxValue = maxValues.pop(); - if(node.right!=null){ - stack.push(node.right); - minValues.push(node.val); - maxValues.push(maxValue); - } - pNode = node.right; - } - } - return true; -} -``` - -上边的 DFS 可以看出来一个缺点,就是我们判断完当前元素后并没有出栈,后续还会回来得到右孩子后才会出栈。所以其实我们可以用 BFS,利用一个队列,一层一层的遍历,遍历完一个就删除一个。 - -```java -public boolean isValidBST(TreeNode root) { - if (root == null || root.left == null && root.right == null) { - return true; - } - //利用三个队列来保存对应的节点和区间 - Queue queue = new LinkedList<>(); - Queue minValues = new LinkedList<>(); - Queue maxValues = new LinkedList<>(); - //头结点入队列 - TreeNode pNode = root; - queue.offer(pNode); - minValues.offer(null); - maxValues.offer(null); - while (!queue.isEmpty()) { - //判断队列的头元素是否符合条件并且出队列 - Integer minValue = minValues.poll(); - Integer maxValue = maxValues.poll(); - pNode = queue.poll(); - if (minValue != null && pNode.val <= minValue) { - return false; - } - if (maxValue != null && pNode.val >= maxValue) { - return false; - } - //左孩子入队列 - if(pNode.left!=null){ - queue.offer(pNode.left); - minValues.offer(minValue); - maxValues.offer(pNode.val); - } - //右孩子入队列 - if(pNode.right!=null){ - queue.offer(pNode.right); - minValues.offer(pNode.val); - maxValues.offer(maxValue); - } - } - return true; -} -``` - -# 解法四 中序遍历 - -参考[这里]()。 - -解法三中我们用了先序遍历 和 BFS,现在来考虑中序遍历。中序遍历在 [94]() 题中已经考虑过了。那么中序遍历在这里有什么好处呢? - -中序遍历顺序会是左孩子,根节点,右孩子。二分查找树的性质,左孩子小于根节点,根节点小于右孩子。 - -是的,如果我们将中序遍历的结果输出,那么将会到的一个从小到大排列的序列。 - -所以我们只需要进行一次中序遍历,将遍历结果保存,然后判断该数组是否是从小到大排列的即可。 - -更近一步,由于我们只需要临近的两个数的相对关系,所以我们只需要在遍历过程中,把当前遍历的结果和上一个结果比较即可。 - -```java -public boolean isValidBST(TreeNode root) { - if (root == null) return true; - Stack stack = new Stack<>(); - TreeNode pre = null; - while (root != null || !stack.isEmpty()) { - while (root != null) { - stack.push(root); - root = root.left; - } - root = stack.pop(); - if(pre != null && root.val <= pre.val) return false; - pre = root; - root = root.right; - } - return true; -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/98.jpg) + +输入一个树,判断该树是否是合法二分查找树,[95]()题做过生成二分查找树。二分查找树定义如下: + +> 1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值; +> 2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值; +> 3. 任意节点的左、右子树也分别为二叉查找树; +> 4. 没有键值相等的节点。 + +# 解法一 + +开始的时候以为可以很简单的用递归写出来。想法是,左子树是合法二分查找树,右子树是合法二分查找树,并且根节点大于左孩子,小于右孩子,那么当前树就是合法二分查找树。代码如下: + +```java +public boolean isValidBST(TreeNode root) { + if (root == null) { + return true; + } + boolean leftVailid = true; + boolean rightVaild = true; + if (root.left != null) { + //大于左孩子并且左子树是合法二分查找树 + leftVailid = root.val > root.left.val && isValidBST(root.left); + } + if (!leftVailid) { + return false; + } + if (root.right != null) { + //小于右孩子并且右子树是合法二分查找树 + rightVaild = root.val < root.right.val && isValidBST(root.right); + } + return rightVaild; +} +``` + +当然,这个解法没有通过。对于下面的解,结果利用上边的解法是错误的。 + +```java + 10 + / \ + 5 15 + / \ + 6 20 +``` + +虽然满足左子树是合法二分查找树,右子树是合法二分查找树,并且根节点大于左孩子,小于右孩子,但这个树不是合法的二分查找树。因为右子树中的 6 小于当前根节点 10。所以我们不应该判断「根节点大于左孩子,小于右孩子」,而是判断「根节点大于左子树中最大的数,小于右子树中最小的数」。 + +```java +public boolean isValidBST(TreeNode root) { + if (root == null || root.left == null && root.right == null) { + return true; + } + //左子树是否合法 + if (isValidBST(root.left)) { + if (root.left != null) { + int max = getMaxOfBST(root.left);//得到左子树中最大的数 + if (root.val <= max) { //相等的情况,代表有重复的数字 + return false; + } + } + + } else { + return false; + } + + //右子树是否合法 + if (isValidBST(root.right)) { + if (root.right != null) { + int min = getMinOfBST(root.right);//得到右子树中最小的数 + if (root.val >= min) { //相等的情况,代表有重复的数字 + return false; + } + } + + } else { + return false; + } + return true; +} + +private int getMinOfBST(TreeNode root) { + int min = root.val; + while (root != null) { + if (root.val <= min) { + min = root.val; + } + root = root.left; + } + return min; +} + +private int getMaxOfBST(TreeNode root) { + int max = root.val; + while (root != null) { + if (root.val >= max) { + max = root.val; + } + root = root.right; + } + return max; +} +``` + +# 解法二 + +来利用另一种思路,参考[官方题解]()。 + +解法一中,我们是判断根节点是否合法,找到了左子树中最大的数,右子树中最小的数。 由左子树和右子树决定当前根节点是否合法。 + +但如果正常的来讲,明明先有的根节点,按理说根节点是任何数都行,而不是由左子树和右子树限定。相反,根节点反而决定了左孩子和右孩子的合法取值范围。 + +所以,我们可以从根节点进行 DFS,然后计算每个节点应该的取值范围,如果当前节点不符合就返回 false。 + +```java + 10 + / \ + 5 15 + / \ / + 3 6 7 + + 考虑 10 的范围 + 10(-inf,+inf) + + 考虑 5 的范围 + 10(-inf,+inf) + / + 5(-inf,10) + + 考虑 3 的范围 + 10(-inf,+inf) + / + 5(-inf,10) + / + 3(-inf,5) + + 考虑 6 的范围 + 10(-inf,+inf) + / + 5(-inf,10) + / \ + 3(-inf,5) 6(5,10) + + 考虑 15 的范围 + 10(-inf,+inf) + / \ + 5(-inf,10) 15(10,+inf) + / \ + 3(-inf,5) 6(5,10) + + 考虑 7 的范围,出现不符合返回 false + 10(-inf,+inf) + / \ +5(-inf,10) 15(10,+inf) + / \ / +3(-inf,5) 6(5,10) 7(10,15) + + +``` + +可以观察到,左孩子的范围是 (父结点左边界,父节点的值),右孩子的范围是(父节点的值,父节点的右边界)。 + +还有个问题,java 里边没有提供负无穷和正无穷,用什么数来表示呢? + +方案一,假设我们的题目的数值都是 Integer 范围的,那么我们用不在 Integer 范围的数字来表示负无穷和正无穷。用 long 去存储。 + +```java +public boolean isValidBST(TreeNode root) { + long maxValue = (long)Integer.MAX_VALUE + 1; + long minValue = (long)Integer.MIN_VALUE - 1; + return getAns(root, minValue, maxValue); +} + +private boolean getAns(TreeNode node, long minVal, long maxVal) { + if (node == null) { + return true; + } + if (node.val <= minVal) { + return false; + } + if (node.val >= maxVal) { + return false; + } + return getAns(node.left, minVal, node.val) && getAns(node.right, node.val, maxVal); +} +``` + + + +方案二:传入 Integer 对象,然后 null 表示负无穷和正无穷。然后利用 JAVA 的自动装箱拆箱,数值的比较可以直接用不等号。 + +```java +public boolean isValidBST(TreeNode root) { + return getAns(root, null, null); +} + +private boolean getAns(TreeNode node, Integer minValue, Integer maxValue) { + if (node == null) { + return true; + } + if (minValue != null && node.val <= minValue) { + return false; + } + if (maxValue != null && node.val >= maxValue) { + return false; + } + return getAns(node.left, minValue, node.val) && getAns(node.right, node.val, maxValue); +} +``` +# 解法三 DFS BFS + +解法二其实就是树的 DFS,也就是二叉树的先序遍历,然后在遍历过程中,判断当前的值是是否在区间中。所以我们可以用栈来模拟递归过程。 + +```java +public boolean isValidBST(TreeNode root) { + if (root == null || root.left == null && root.right == null) { + return true; + } + //利用三个栈来保存对应的节点和区间 + LinkedList stack = new LinkedList<>(); + LinkedList minValues = new LinkedList<>(); + LinkedList maxValues = new LinkedList<>(); + //头结点入栈 + TreeNode pNode = root; + stack.push(pNode); + minValues.push(null); + maxValues.push(null); + while (pNode != null || !stack.isEmpty()) { + if (pNode != null) { + //判断栈顶元素是否符合 + Integer minValue = minValues.peek(); + Integer maxValue = maxValues.peek(); + TreeNode node = stack.peek(); + if (minValue != null && node.val <= minValue) { + return false; + } + if (maxValue != null && node.val >= maxValue) { + return false; + } + //将左孩子加入到栈 + if(pNode.left!=null){ + stack.push(pNode.left); + minValues.push(minValue); + maxValues.push(pNode.val); + } + + pNode = pNode.left; + } else { // pNode == null && !stack.isEmpty() + //出栈,将右孩子加入栈中 + TreeNode node = stack.pop(); + minValues.pop(); + Integer maxValue = maxValues.pop(); + if(node.right!=null){ + stack.push(node.right); + minValues.push(node.val); + maxValues.push(maxValue); + } + pNode = node.right; + } + } + return true; +} +``` + +上边的 DFS 可以看出来一个缺点,就是我们判断完当前元素后并没有出栈,后续还会回来得到右孩子后才会出栈。所以其实我们可以用 BFS,利用一个队列,一层一层的遍历,遍历完一个就删除一个。 + +```java +public boolean isValidBST(TreeNode root) { + if (root == null || root.left == null && root.right == null) { + return true; + } + //利用三个队列来保存对应的节点和区间 + Queue queue = new LinkedList<>(); + Queue minValues = new LinkedList<>(); + Queue maxValues = new LinkedList<>(); + //头结点入队列 + TreeNode pNode = root; + queue.offer(pNode); + minValues.offer(null); + maxValues.offer(null); + while (!queue.isEmpty()) { + //判断队列的头元素是否符合条件并且出队列 + Integer minValue = minValues.poll(); + Integer maxValue = maxValues.poll(); + pNode = queue.poll(); + if (minValue != null && pNode.val <= minValue) { + return false; + } + if (maxValue != null && pNode.val >= maxValue) { + return false; + } + //左孩子入队列 + if(pNode.left!=null){ + queue.offer(pNode.left); + minValues.offer(minValue); + maxValues.offer(pNode.val); + } + //右孩子入队列 + if(pNode.right!=null){ + queue.offer(pNode.right); + minValues.offer(pNode.val); + maxValues.offer(maxValue); + } + } + return true; +} +``` + +# 解法四 中序遍历 + +参考[这里]()。 + +解法三中我们用了先序遍历 和 BFS,现在来考虑中序遍历。中序遍历在 [94]() 题中已经考虑过了。那么中序遍历在这里有什么好处呢? + +中序遍历顺序会是左孩子,根节点,右孩子。二分查找树的性质,左孩子小于根节点,根节点小于右孩子。 + +是的,如果我们将中序遍历的结果输出,那么将会到的一个从小到大排列的序列。 + +所以我们只需要进行一次中序遍历,将遍历结果保存,然后判断该数组是否是从小到大排列的即可。 + +更近一步,由于我们只需要临近的两个数的相对关系,所以我们只需要在遍历过程中,把当前遍历的结果和上一个结果比较即可。 + +```java +public boolean isValidBST(TreeNode root) { + if (root == null) return true; + Stack stack = new Stack<>(); + TreeNode pre = null; + while (root != null || !stack.isEmpty()) { + while (root != null) { + stack.push(root); + root = root.left; + } + root = stack.pop(); + if(pre != null && root.val <= pre.val) return false; + pre = root; + root = root.right; + } + return true; +} +``` + +# 总 + 这几天都是二叉树的相关题,主要是对前序遍历,中序遍历的理解,以及 DFS,如果再用好递归,利用栈模拟递归,题目就很好解了。 \ No newline at end of file diff --git a/leetcode-100-Same-Tree.md b/leetcode-100-Same-Tree.md index cc4df3075..4b81d8e85 100644 --- a/leetcode-100-Same-Tree.md +++ b/leetcode-100-Same-Tree.md @@ -1,65 +1,65 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/100.jpg) - -判断两个二叉树是否相同。 - -# 解法一 - -这道题就很简单了,只要把两个树同时遍历一下,遍历过程中判断数值是否相等或者同时为 null 即可。而遍历的方法,当然可以选择 DFS 里的先序遍历,中序遍历,后序遍历,或者 BFS。 - -当然实现的话,可以用递归,用栈,或者中序遍历提到的 Morris。也可以参照 [98 题]() 、[ 94 题 ](),对二叉树的遍历讨论了很多。 - -这里的话,由于最近几题对中序遍历用的多,所以就直接用中序遍历了。 - -```java -public boolean isSameTree(TreeNode p, TreeNode q) { - return inorderTraversal(p,q); -} -private boolean inorderTraversal(TreeNode p, TreeNode q) { - if(p==null&&q==null){ - return true; - }else if(p==null || q==null){ - return false; - } - //考虑左子树是否符合 - if(!inorderTraversal(p.left,q.left)){ - return false; - } - //考虑当前节点是否符合 - if(p.val!=q.val){ - return false; - } - //考虑右子树是否符合 - if(!inorderTraversal(p.right,q.right)){ - return false; - } - return true; -} -``` - -时间复杂度:O(N)。对每个节点进行了访问。 - -空间复杂度:O(h),h 是树的高度,也就是压栈所耗费的空间。当然 h 最小为 log(N),最大就等于 N。 - -```java -最好情况例子 - 1 - / \ - 2 3 - / \ / \ - 4 5 6 7 - -最差情况例子 - 1 - \ - 2 - \ - 3 - \ - 4 -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/100.jpg) + +判断两个二叉树是否相同。 + +# 解法一 + +这道题就很简单了,只要把两个树同时遍历一下,遍历过程中判断数值是否相等或者同时为 null 即可。而遍历的方法,当然可以选择 DFS 里的先序遍历,中序遍历,后序遍历,或者 BFS。 + +当然实现的话,可以用递归,用栈,或者中序遍历提到的 Morris。也可以参照 [98 题]() 、[ 94 题 ](),对二叉树的遍历讨论了很多。 + +这里的话,由于最近几题对中序遍历用的多,所以就直接用中序遍历了。 + +```java +public boolean isSameTree(TreeNode p, TreeNode q) { + return inorderTraversal(p,q); +} +private boolean inorderTraversal(TreeNode p, TreeNode q) { + if(p==null&&q==null){ + return true; + }else if(p==null || q==null){ + return false; + } + //考虑左子树是否符合 + if(!inorderTraversal(p.left,q.left)){ + return false; + } + //考虑当前节点是否符合 + if(p.val!=q.val){ + return false; + } + //考虑右子树是否符合 + if(!inorderTraversal(p.right,q.right)){ + return false; + } + return true; +} +``` + +时间复杂度:O(N)。对每个节点进行了访问。 + +空间复杂度:O(h),h 是树的高度,也就是压栈所耗费的空间。当然 h 最小为 log(N),最大就等于 N。 + +```java +最好情况例子 + 1 + / \ + 2 3 + / \ / \ + 4 5 6 7 + +最差情况例子 + 1 + \ + 2 + \ + 3 + \ + 4 +``` + +# 总 + 这道题比较简单,本质上考察的就是二叉树的遍历。 \ No newline at end of file diff --git a/leetcode-101-200.md b/leetcode-101-200.md index f72770ede..106d5d784 100644 --- a/leetcode-101-200.md +++ b/leetcode-101-200.md @@ -1,151 +1,151 @@ -# leetcode 101 到 200 题 - -101. Symmetric Tree - -102. Binary Tree Level Order Traversal - -103. Binary Tree Zigzag Level Order Traversal - -105. Construct Binary Tree from Preorder and Inorder Traversal - -106. Construct Binary Tree from Inorder and Postorder Traversal - -107. Binary Tree Level Order Traversal II - -108. Convert Sorted Array to Binary Search Tree - -109. Convert Sorted List to Binary Search Tree - -110. Balanced Binary Tree - -111. Minimum Depth of Binary Tree - -112. Path Sum - -113. Path Sum II - -114. Flatten Binary Tree to Linked List - -115. Distinct Subsequences - -116. Populating Next Right Pointers in Each Node - -117. Populating Next Right Pointers in Each Node II - -118. Pascal's Triangle - -119. Pascal's Triangle II - -120. Triangle - -121. Best Time to Buy and Sell Stock - -122. Best Time to Buy and Sell Stock II - -123. Best Time to Buy and Sell Stock III - -124. Binary Tree Maximum Path Sum - -125. Valid Palindrome - -126. Word Ladder II - -127. Word Ladder - -128. Longest Consecutive Sequence - -129. Sum Root to Leaf Numbers - -130. Surrounded Regions - -131. Palindrome Partitioning - -132. Palindrome Partitioning II - -133. Clone Graph - -134. Gas Station - -135. Candy - -136. Single Number - -137. Single Number II - -138. Copy List with Random Pointer - -139. Word Break - -140. Word Break II - -141. Linked List Cycle - -142. Linked List Cycle II - -143. Reorder List - -144. Binary Tree Preorder Traversal - -145. Binary Tree Postorder Traversal - -146. LRU Cache - -147. Insertion Sort List - -148. Sort List - -149. Max Points on a Line - -150. Evaluate Reverse Polish Notation - -151. Reverse Words in a String - -152. Maximum Product Subarray - -153. Find Minimum in Rotated Sorted Array - -154. Find Minimum in Rotated Sorted Array II - -155. Min Stack - -160. Intersection of Two Linked Lists - -162. Find Peak Element - -164. Maximum Gap - -165. Compare Version Numbers - -166. Fraction to Recurring Decimal - -167. Two Sum II - Input array is sorted - -168. Excel Sheet Column Title - -169. Majority Element - -171. Excel Sheet Column Number - -172. Factorial Trailing Zeroes - -173. Binary Search Tree Iterator - -174. Dungeon Game - -179. Largest Number - -187. Repeated DNA Sequences - -188. Best Time to Buy and Sell Stock IV - -189. Rotate Array - -190. Reverse Bits - -191. Number of 1 Bits - -198. House Robber - -199. Binary Tree Right Side View - +# leetcode 101 到 200 题 + +101. Symmetric Tree + +102. Binary Tree Level Order Traversal + +103. Binary Tree Zigzag Level Order Traversal + +105. Construct Binary Tree from Preorder and Inorder Traversal + +106. Construct Binary Tree from Inorder and Postorder Traversal + +107. Binary Tree Level Order Traversal II + +108. Convert Sorted Array to Binary Search Tree + +109. Convert Sorted List to Binary Search Tree + +110. Balanced Binary Tree + +111. Minimum Depth of Binary Tree + +112. Path Sum + +113. Path Sum II + +114. Flatten Binary Tree to Linked List + +115. Distinct Subsequences + +116. Populating Next Right Pointers in Each Node + +117. Populating Next Right Pointers in Each Node II + +118. Pascal's Triangle + +119. Pascal's Triangle II + +120. Triangle + +121. Best Time to Buy and Sell Stock + +122. Best Time to Buy and Sell Stock II + +123. Best Time to Buy and Sell Stock III + +124. Binary Tree Maximum Path Sum + +125. Valid Palindrome + +126. Word Ladder II + +127. Word Ladder + +128. Longest Consecutive Sequence + +129. Sum Root to Leaf Numbers + +130. Surrounded Regions + +131. Palindrome Partitioning + +132. Palindrome Partitioning II + +133. Clone Graph + +134. Gas Station + +135. Candy + +136. Single Number + +137. Single Number II + +138. Copy List with Random Pointer + +139. Word Break + +140. Word Break II + +141. Linked List Cycle + +142. Linked List Cycle II + +143. Reorder List + +144. Binary Tree Preorder Traversal + +145. Binary Tree Postorder Traversal + +146. LRU Cache + +147. Insertion Sort List + +148. Sort List + +149. Max Points on a Line + +150. Evaluate Reverse Polish Notation + +151. Reverse Words in a String + +152. Maximum Product Subarray + +153. Find Minimum in Rotated Sorted Array + +154. Find Minimum in Rotated Sorted Array II + +155. Min Stack + +160. Intersection of Two Linked Lists + +162. Find Peak Element + +164. Maximum Gap + +165. Compare Version Numbers + +166. Fraction to Recurring Decimal + +167. Two Sum II - Input array is sorted + +168. Excel Sheet Column Title + +169. Majority Element + +171. Excel Sheet Column Number + +172. Factorial Trailing Zeroes + +173. Binary Search Tree Iterator + +174. Dungeon Game + +179. Largest Number + +187. Repeated DNA Sequences + +188. Best Time to Buy and Sell Stock IV + +189. Rotate Array + +190. Reverse Bits + +191. Number of 1 Bits + +198. House Robber + +199. Binary Tree Right Side View + 200. Number of Islands \ No newline at end of file diff --git a/leetcode-101-Symmetric-Tree.md b/leetcode-101-Symmetric-Tree.md index 0a7c890c2..ebf5b4126 100644 --- a/leetcode-101-Symmetric-Tree.md +++ b/leetcode-101-Symmetric-Tree.md @@ -1,138 +1,138 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/101.jpg) - -判断一个二叉树是否关于中心轴对称。 - -# 解法一 - -和 [100 题]() 判断两个二叉树是否相等其实是一样的思路,都是用某种遍历方法来同时遍历**两个树**,然后看是否**对应相等**。 - -这里的需要遍历的两个树就是左子树和右子树了。 - -这里的对应相等的话,因为判断左子树 A 和右子树 B 是否对称,需要判断两点。 - -* A 的根节点和 B 的根节点是否相等 -* A 的左子树和 B 的右子树是否相等,同时 A 的右子树和左子树是否相等。 - -上边两点都满足,就表示是对称的。所以代码就出来了。 - -```java -public boolean isSymmetric5(TreeNode root) { - if (root == null) { - return true; - } - return isSymmetricHelper(root.left, root.right); -} - -private boolean isSymmetricHelper(TreeNode left, TreeNode right) { - //有且仅有一个为 null ,直接返回 false - if (left == null && right != null || left != null && right == null) { - return false; - } - if (left != null && right != null) - //A 的根节点和 B 的根节点是否相等 - if (left.val != right.val) { - return false; - } - //A 的左子树和 B 的右子树是否相等,同时 A 的右子树和左子树是否相等。 - return isSymmetricHelper(left.left, right.right) && isSymmetricHelper(left.right, right.left); - } - //都为 null,返回 true - return true; -} -``` - -# 解法二 DFS 栈 - -解法一其实就是类似于 DFS 的先序遍历。不同之处是对于 left 子树是正常的先序遍历 根节点 -> 左子树 -> 右子树 的顺序,对于 right 子树的话是 根节点 -> 右子树 -> 左子树 的顺序。 - -所以我们可以用栈,把递归改写为迭代的形式。 - -```java -public boolean isSymmetric(TreeNode root) { - if (root == null) { - return true; - } - Stack stackLeft = new Stack<>(); - Stack stackRight = new Stack<>(); - TreeNode curLeft = root.left; - TreeNode curRight = root.right; - while (curLeft != null || !stackLeft.isEmpty() || curRight!=null || !stackRight.isEmpty()) { - // 节点不为空一直压栈 - while (curLeft != null) { - stackLeft.push(curLeft); - curLeft = curLeft.left; // 考虑左子树 - } - while (curRight != null) { - stackRight.push(curRight); - curRight = curRight.right; // 考虑右子树 - } - //长度不同就返回 false - if (stackLeft.size() != stackRight.size()) { - return false; - } - // 节点为空,就出栈 - curLeft = stackLeft.pop(); - curRight = stackRight.pop(); - - // 当前值判断 - if (curLeft.val != curRight.val) { - return false; - } - // 考虑右子树 - curLeft = curLeft.right; - curRight = curRight.left; - } - return true; -} -``` - -当然我们也可以使用中序遍历或者后序遍历,是一样的道理。 - -# 解法三 BFS 队列 - -DFS 考虑完了,当然还有 BFS,一层一层的遍历两个树,然后判断**对应**的节点是否相等即可。 - -利用两个队列来保存下一次遍历的节点即可。 - -```java -public boolean isSymmetric6(TreeNode root) { - if (root == null) { - return true; - } - Queue leftTree = new LinkedList<>(); - Queue rightTree = new LinkedList<>(); - //两个树的根节点分别加入 - leftTree.offer(root.left); - rightTree.offer(root.right); - while (!leftTree.isEmpty() && !rightTree.isEmpty()) { - TreeNode curLeft = leftTree.poll(); - TreeNode curRight = rightTree.poll(); - if (curLeft == null && curRight != null || curLeft != null && curRight == null) { - return false; - } - if (curLeft != null && curRight != null) { - if (curLeft.val != curRight.val) { - return false; - } - //先加入左子树后加入右子树 - leftTree.offer(curLeft.left); - leftTree.offer(curLeft.right); - - //先加入右子树后加入左子树 - rightTree.offer(curRight.right); - rightTree.offer(curRight.left); - } - - } - if (!leftTree.isEmpty() || !rightTree.isEmpty()) { - return false; - } - return true; -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/101.jpg) + +判断一个二叉树是否关于中心轴对称。 + +# 解法一 + +和 [100 题]() 判断两个二叉树是否相等其实是一样的思路,都是用某种遍历方法来同时遍历**两个树**,然后看是否**对应相等**。 + +这里的需要遍历的两个树就是左子树和右子树了。 + +这里的对应相等的话,因为判断左子树 A 和右子树 B 是否对称,需要判断两点。 + +* A 的根节点和 B 的根节点是否相等 +* A 的左子树和 B 的右子树是否相等,同时 A 的右子树和左子树是否相等。 + +上边两点都满足,就表示是对称的。所以代码就出来了。 + +```java +public boolean isSymmetric5(TreeNode root) { + if (root == null) { + return true; + } + return isSymmetricHelper(root.left, root.right); +} + +private boolean isSymmetricHelper(TreeNode left, TreeNode right) { + //有且仅有一个为 null ,直接返回 false + if (left == null && right != null || left != null && right == null) { + return false; + } + if (left != null && right != null) + //A 的根节点和 B 的根节点是否相等 + if (left.val != right.val) { + return false; + } + //A 的左子树和 B 的右子树是否相等,同时 A 的右子树和左子树是否相等。 + return isSymmetricHelper(left.left, right.right) && isSymmetricHelper(left.right, right.left); + } + //都为 null,返回 true + return true; +} +``` + +# 解法二 DFS 栈 + +解法一其实就是类似于 DFS 的先序遍历。不同之处是对于 left 子树是正常的先序遍历 根节点 -> 左子树 -> 右子树 的顺序,对于 right 子树的话是 根节点 -> 右子树 -> 左子树 的顺序。 + +所以我们可以用栈,把递归改写为迭代的形式。 + +```java +public boolean isSymmetric(TreeNode root) { + if (root == null) { + return true; + } + Stack stackLeft = new Stack<>(); + Stack stackRight = new Stack<>(); + TreeNode curLeft = root.left; + TreeNode curRight = root.right; + while (curLeft != null || !stackLeft.isEmpty() || curRight!=null || !stackRight.isEmpty()) { + // 节点不为空一直压栈 + while (curLeft != null) { + stackLeft.push(curLeft); + curLeft = curLeft.left; // 考虑左子树 + } + while (curRight != null) { + stackRight.push(curRight); + curRight = curRight.right; // 考虑右子树 + } + //长度不同就返回 false + if (stackLeft.size() != stackRight.size()) { + return false; + } + // 节点为空,就出栈 + curLeft = stackLeft.pop(); + curRight = stackRight.pop(); + + // 当前值判断 + if (curLeft.val != curRight.val) { + return false; + } + // 考虑右子树 + curLeft = curLeft.right; + curRight = curRight.left; + } + return true; +} +``` + +当然我们也可以使用中序遍历或者后序遍历,是一样的道理。 + +# 解法三 BFS 队列 + +DFS 考虑完了,当然还有 BFS,一层一层的遍历两个树,然后判断**对应**的节点是否相等即可。 + +利用两个队列来保存下一次遍历的节点即可。 + +```java +public boolean isSymmetric6(TreeNode root) { + if (root == null) { + return true; + } + Queue leftTree = new LinkedList<>(); + Queue rightTree = new LinkedList<>(); + //两个树的根节点分别加入 + leftTree.offer(root.left); + rightTree.offer(root.right); + while (!leftTree.isEmpty() && !rightTree.isEmpty()) { + TreeNode curLeft = leftTree.poll(); + TreeNode curRight = rightTree.poll(); + if (curLeft == null && curRight != null || curLeft != null && curRight == null) { + return false; + } + if (curLeft != null && curRight != null) { + if (curLeft.val != curRight.val) { + return false; + } + //先加入左子树后加入右子树 + leftTree.offer(curLeft.left); + leftTree.offer(curLeft.right); + + //先加入右子树后加入左子树 + rightTree.offer(curRight.right); + rightTree.offer(curRight.left); + } + + } + if (!leftTree.isEmpty() || !rightTree.isEmpty()) { + return false; + } + return true; +} +``` + +# 总 + 总体上来说和 [100 题]() 是一样的,只不过这里的两棵树对应相等,是左对右,右对左。 \ No newline at end of file diff --git a/leetcode-102-Binary-Tree-Level-Order-Traversal.md b/leetcode-102-Binary-Tree-Level-Order-Traversal.md index 8cc5309c6..d7c9d69e3 100644 --- a/leetcode-102-Binary-Tree-Level-Order-Traversal.md +++ b/leetcode-102-Binary-Tree-Level-Order-Traversal.md @@ -1,114 +1,114 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/102.jpg) - -二叉树的层次遍历,输出一个 list 的 list。 - -# 解法一 DFS - -这道题考的就是 BFS,我们可以通过 DFS 实现。只需要在递归过程中将当前 level 传入即可。 - -```java -public List> levelOrder(TreeNode root) { - List> ans = new ArrayList<>(); - DFS(root, 0, ans); - return ans; -} - -private void DFS(TreeNode root, int level, List> ans) { - if(root == null){ - return; - } - //当前层数还没有元素,先 new 一个空的列表 - if(ans.size()<=level){ - ans.add(new ArrayList<>()); - } - //当前值加入 - ans.get(level).add(root.val); - - DFS(root.left,level+1,ans); - DFS(root.right,level+1,ans); -} -``` - -# 解法二 BFS 队列 - -如果是顺序刷题,前边的 [97 题](),[ 98 题](),[101 题](),都用到了 BFS ,应该很熟悉了。 - -之前我们用一个 while 循环,不停的从队列中拿一个节点,并且在循环中将当前取出来的节点的左孩子和右孩子也加入到队列中。 - -相比于这道题,我们要解决的问题是,怎么知道当前节点的 level 。 - -## 第一种方案 - -定义一个新的 class,class 里边两个成员 node 和 level,将我们新定义的 class 每次加入到队列中。或者用一个新的队列和之前的节点队列同步入队出队,新的队列存储 level。 - -下边的代码实现后一种。 - -```java -public List> levelOrder(TreeNode root) { - List> ans = new ArrayList<>(); - if (root == null) { - return ans; - } - Queue treeNode = new LinkedList<>(); - Queue nodeLevel = new LinkedList<>(); - treeNode.offer(root); - int level = 0; - nodeLevel.offer(level); - while (!treeNode.isEmpty()) { - TreeNode curNode = treeNode.poll(); - int curLevel = nodeLevel.poll(); - if (curNode != null) { - if (ans.size() <= curLevel) { - ans.add(new ArrayList<>()); - } - ans.get(curLevel).add(curNode.val); - level = curLevel + 1; - treeNode.offer(curNode.left); - nodeLevel.offer(level); - treeNode.offer(curNode.right); - nodeLevel.offer(level); - } - } - return ans; -} -``` - -# 方案二 - -参考[这里]()。 - -我们在 while 循环中加一个 for 循环,循环次数是循环前的队列中的元素个数即可,使得每次的 while 循环出队的元素都是同一层的元素。 - -for 循环结束也就意味着当前层结束了,而此时的队列存储的元素就是下一层的所有元素了。 - -```java -public List> levelOrder(TreeNode root) { - Queue queue = new LinkedList(); - List> ans = new LinkedList>(); - if (root == null) - return ans; - queue.offer(root); - while (!queue.isEmpty()) { - int levelNum = queue.size(); // 当前层元素的个数 - List subList = new LinkedList(); - for (int i = 0; i < levelNum; i++) { - TreeNode curNode = queue.poll(); - if (curNode != null) { - subList.add(curNode.val); - queue.offer(curNode.left); - queue.offer(curNode.right); - } - } - if(subList.size()>0){ - ans.add(subList); - } - } - return ans; -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/102.jpg) + +二叉树的层次遍历,输出一个 list 的 list。 + +# 解法一 DFS + +这道题考的就是 BFS,我们可以通过 DFS 实现。只需要在递归过程中将当前 level 传入即可。 + +```java +public List> levelOrder(TreeNode root) { + List> ans = new ArrayList<>(); + DFS(root, 0, ans); + return ans; +} + +private void DFS(TreeNode root, int level, List> ans) { + if(root == null){ + return; + } + //当前层数还没有元素,先 new 一个空的列表 + if(ans.size()<=level){ + ans.add(new ArrayList<>()); + } + //当前值加入 + ans.get(level).add(root.val); + + DFS(root.left,level+1,ans); + DFS(root.right,level+1,ans); +} +``` + +# 解法二 BFS 队列 + +如果是顺序刷题,前边的 [97 题](),[ 98 题](),[101 题](),都用到了 BFS ,应该很熟悉了。 + +之前我们用一个 while 循环,不停的从队列中拿一个节点,并且在循环中将当前取出来的节点的左孩子和右孩子也加入到队列中。 + +相比于这道题,我们要解决的问题是,怎么知道当前节点的 level 。 + +## 第一种方案 + +定义一个新的 class,class 里边两个成员 node 和 level,将我们新定义的 class 每次加入到队列中。或者用一个新的队列和之前的节点队列同步入队出队,新的队列存储 level。 + +下边的代码实现后一种。 + +```java +public List> levelOrder(TreeNode root) { + List> ans = new ArrayList<>(); + if (root == null) { + return ans; + } + Queue treeNode = new LinkedList<>(); + Queue nodeLevel = new LinkedList<>(); + treeNode.offer(root); + int level = 0; + nodeLevel.offer(level); + while (!treeNode.isEmpty()) { + TreeNode curNode = treeNode.poll(); + int curLevel = nodeLevel.poll(); + if (curNode != null) { + if (ans.size() <= curLevel) { + ans.add(new ArrayList<>()); + } + ans.get(curLevel).add(curNode.val); + level = curLevel + 1; + treeNode.offer(curNode.left); + nodeLevel.offer(level); + treeNode.offer(curNode.right); + nodeLevel.offer(level); + } + } + return ans; +} +``` + +# 方案二 + +参考[这里]()。 + +我们在 while 循环中加一个 for 循环,循环次数是循环前的队列中的元素个数即可,使得每次的 while 循环出队的元素都是同一层的元素。 + +for 循环结束也就意味着当前层结束了,而此时的队列存储的元素就是下一层的所有元素了。 + +```java +public List> levelOrder(TreeNode root) { + Queue queue = new LinkedList(); + List> ans = new LinkedList>(); + if (root == null) + return ans; + queue.offer(root); + while (!queue.isEmpty()) { + int levelNum = queue.size(); // 当前层元素的个数 + List subList = new LinkedList(); + for (int i = 0; i < levelNum; i++) { + TreeNode curNode = queue.poll(); + if (curNode != null) { + subList.add(curNode.val); + queue.offer(curNode.left); + queue.offer(curNode.right); + } + } + if(subList.size()>0){ + ans.add(subList); + } + } + return ans; +} +``` + +# 总 + 考察的知识点就是二叉树的 BFS,解法二的方案二是自己不曾想到的, while 循环中加入一个 for 循环,很妙! \ No newline at end of file diff --git a/leetcode-103-Binary-Tree-Zigzag-Level-Order-Traversal.md b/leetcode-103-Binary-Tree-Zigzag-Level-Order-Traversal.md index 577f91742..931a793e0 100644 --- a/leetcode-103-Binary-Tree-Zigzag-Level-Order-Traversal.md +++ b/leetcode-103-Binary-Tree-Zigzag-Level-Order-Traversal.md @@ -1,175 +1,175 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/103.jpg) - -和 [102 题]() 类似,二叉树的层次遍历。只不过这题要求,第 1 层从左到右,第 2 层从右到左,第 3 层从左到右,第 4 层从右到左,交替进行。 - -# 思路分析 - -大家可以先做下 [102 题]() 吧,直接在 102 题的基础上进行修改即可。从左到右和从右到左交替,所以我们只需要判断当前的 `level`,层数从 0 开始的话,偶数就把元素添加到当前层的末尾,奇数的话,每次把新元素添加到头部,这样就实现了从右到左的遍历。 - -# 解法一 DFS - -判断 level 是偶数还是奇数即可。 - -```java -public List> zigzagLevelOrder(TreeNode root) { - List> ans = new ArrayList<>(); - DFS(root, 0, ans); - return ans; -} - -private void DFS(TreeNode root, int level, List> ans) { - if (root == null) { - return; - } - if (ans.size() <= level) { - ans.add(new ArrayList<>()); - } - if ((level % 2) == 0) { - ans.get(level).add(root.val); //添加到末尾 - } else { - ans.get(level).add(0, root.val); //添加到头部 - } - - DFS(root.left, level + 1, ans); - DFS(root.right, level + 1, ans); -} -``` - -# 解法二 BFS 队列 - -如果是顺序刷题,前边的 [97 题](https://leetcode.wang/leetCode-97-Interleaving-String.html#%E8%A7%A3%E6%B3%95%E4%B8%89-%E5%B9%BF%E5%BA%A6%E4%BC%98%E5%85%88%E9%81%8D%E5%8E%86-bfs),[ 98 题](https://leetcode.wang/leetCode-98-Validate-Binary-Search-Tree.html#%E8%A7%A3%E6%B3%95%E4%B8%89-dfs-bfs),[101 题](https://leetcode.wang/leetcode-101-Symmetric-Tree.html#%E8%A7%A3%E6%B3%95%E4%B8%89-bfs-%E9%98%9F%E5%88%97),都用到了 BFS ,应该很熟悉了。 - -之前我们用一个 `while` 循环,不停的从队列中拿一个节点,并且在循环中将当前取出来的节点的左孩子和右孩子也加入到队列中。 - -相比于这道题,我们要解决的问题是,怎么知道当前节点的 `level` 。 - -## 第一种方案 - -定义一个新的 class,class 里边两个成员 node 和 level,将我们新定义的 class 每次加入到队列中。或者用一个新的队列和之前的节点队列同步入队出队,新的队列存储 level。 - -下边的代码实现后一种,并且对 level 进行判断。 - -```java -public List> zigzagLevelOrder(TreeNode root) { - List> ans = new ArrayList<>(); - if (root == null) { - return ans; - } - Queue treeNode = new LinkedList<>(); - Queue nodeLevel = new LinkedList<>(); - treeNode.offer(root); - int level = 0; - nodeLevel.offer(level); - while (!treeNode.isEmpty()) { - TreeNode curNode = treeNode.poll(); - int curLevel = nodeLevel.poll(); - if (curNode != null) { - if (ans.size() <= curLevel) { - ans.add(new ArrayList<>()); - } - if ((curLevel % 2) == 0) { - ans.get(curLevel).add(curNode.val); - } else { - ans.get(curLevel).add(0, curNode.val); - } - level = curLevel + 1; - treeNode.offer(curNode.left); - nodeLevel.offer(level); - treeNode.offer(curNode.right); - nodeLevel.offer(level); - } - } - return ans; -} -``` - -# 第二种方案 - -把 [102 题]() 的解释贴过来。 - -> 我们在 while 循环中加一个 for 循环,循环次数是循环前的队列中的元素个数即可,使得每次的 while 循环出队的元素都是同一层的元素。 -> -> for 循环结束也就意味着当前层结束了,而此时的队列存储的元素就是下一层的所有元素了。 - -这道题我们要知道当前应该是从左到右还是从右到左,最直接的方案当然是增加一个 `level` 变量,和上边的解法一样,来判断 `level` 是奇数还是偶数即可。 - -```java -public List> zigzagLevelOrder(TreeNode root) { - Queue queue = new LinkedList(); - List> ans = new LinkedList>(); - if (root == null) - return ans; - queue.offer(root); - int level = 0; - while (!queue.isEmpty()) { - int levelNum = queue.size(); // 当前层元素的个数 - List subList = new LinkedList(); - for (int i = 0; i < levelNum; i++) { - TreeNode curNode = queue.poll(); - if (curNode != null) { - if ((level % 2) == 0) { - subList.add(curNode.val); - } else { - subList.add(0, curNode.val); - } - queue.offer(curNode.left); - queue.offer(curNode.right); - } - } - //因为上边 queue.offer(curNode.left) 没有判断是否是 null - //所以要判断当前是否有元素 - if (subList.size() > 0) { - ans.add(subList); - } - level++; - } - return ans; -} -``` - -除了增加 `level` 变量外,我们还可以增加一个 `boolean` 变量来区别当前从左还是从右。此外 [这里]() 的评论里,看到了另外一种想法,不用添加新的变量。我们直接判断当前 `ans` 的大小,如果大小是 n 代表当前在添加第 n 层。 - -# 解法三 - -[这里]() 看到一个有趣的想法,分享一下。 - -我们直接用两个栈(或者队列)轮换着添加元素,一个栈从左到右添加元素,一个栈从右到左添加元素。 - -```java -public List> zigzagLevelOrder(TreeNode root) { - TreeNode c=root; - List> ans =new ArrayList>(); - if(c==null) return ans; - Stack s1=new Stack(); - Stack s2=new Stack(); - s1.push(root); - while(!s1.isEmpty()||!s2.isEmpty()) - { - List tmp=new ArrayList(); - while(!s1.isEmpty()) - { - c=s1.pop(); - tmp.add(c.val); - if(c.left!=null) s2.push(c.left); - if(c.right!=null) s2.push(c.right); - } - ans.add(tmp); - tmp=new ArrayList(); - while(!s2.isEmpty()) - { - c=s2.pop(); - tmp.add(c.val); - if(c.right!=null)s1.push(c.right); - if(c.left!=null)s1.push(c.left); - } - if(!tmp.isEmpty()) ans.add(tmp); - } - return ans; -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/103.jpg) + +和 [102 题]() 类似,二叉树的层次遍历。只不过这题要求,第 1 层从左到右,第 2 层从右到左,第 3 层从左到右,第 4 层从右到左,交替进行。 + +# 思路分析 + +大家可以先做下 [102 题]() 吧,直接在 102 题的基础上进行修改即可。从左到右和从右到左交替,所以我们只需要判断当前的 `level`,层数从 0 开始的话,偶数就把元素添加到当前层的末尾,奇数的话,每次把新元素添加到头部,这样就实现了从右到左的遍历。 + +# 解法一 DFS + +判断 level 是偶数还是奇数即可。 + +```java +public List> zigzagLevelOrder(TreeNode root) { + List> ans = new ArrayList<>(); + DFS(root, 0, ans); + return ans; +} + +private void DFS(TreeNode root, int level, List> ans) { + if (root == null) { + return; + } + if (ans.size() <= level) { + ans.add(new ArrayList<>()); + } + if ((level % 2) == 0) { + ans.get(level).add(root.val); //添加到末尾 + } else { + ans.get(level).add(0, root.val); //添加到头部 + } + + DFS(root.left, level + 1, ans); + DFS(root.right, level + 1, ans); +} +``` + +# 解法二 BFS 队列 + +如果是顺序刷题,前边的 [97 题](https://leetcode.wang/leetCode-97-Interleaving-String.html#%E8%A7%A3%E6%B3%95%E4%B8%89-%E5%B9%BF%E5%BA%A6%E4%BC%98%E5%85%88%E9%81%8D%E5%8E%86-bfs),[ 98 题](https://leetcode.wang/leetCode-98-Validate-Binary-Search-Tree.html#%E8%A7%A3%E6%B3%95%E4%B8%89-dfs-bfs),[101 题](https://leetcode.wang/leetcode-101-Symmetric-Tree.html#%E8%A7%A3%E6%B3%95%E4%B8%89-bfs-%E9%98%9F%E5%88%97),都用到了 BFS ,应该很熟悉了。 + +之前我们用一个 `while` 循环,不停的从队列中拿一个节点,并且在循环中将当前取出来的节点的左孩子和右孩子也加入到队列中。 + +相比于这道题,我们要解决的问题是,怎么知道当前节点的 `level` 。 + +## 第一种方案 + +定义一个新的 class,class 里边两个成员 node 和 level,将我们新定义的 class 每次加入到队列中。或者用一个新的队列和之前的节点队列同步入队出队,新的队列存储 level。 + +下边的代码实现后一种,并且对 level 进行判断。 + +```java +public List> zigzagLevelOrder(TreeNode root) { + List> ans = new ArrayList<>(); + if (root == null) { + return ans; + } + Queue treeNode = new LinkedList<>(); + Queue nodeLevel = new LinkedList<>(); + treeNode.offer(root); + int level = 0; + nodeLevel.offer(level); + while (!treeNode.isEmpty()) { + TreeNode curNode = treeNode.poll(); + int curLevel = nodeLevel.poll(); + if (curNode != null) { + if (ans.size() <= curLevel) { + ans.add(new ArrayList<>()); + } + if ((curLevel % 2) == 0) { + ans.get(curLevel).add(curNode.val); + } else { + ans.get(curLevel).add(0, curNode.val); + } + level = curLevel + 1; + treeNode.offer(curNode.left); + nodeLevel.offer(level); + treeNode.offer(curNode.right); + nodeLevel.offer(level); + } + } + return ans; +} +``` + +# 第二种方案 + +把 [102 题]() 的解释贴过来。 + +> 我们在 while 循环中加一个 for 循环,循环次数是循环前的队列中的元素个数即可,使得每次的 while 循环出队的元素都是同一层的元素。 +> +> for 循环结束也就意味着当前层结束了,而此时的队列存储的元素就是下一层的所有元素了。 + +这道题我们要知道当前应该是从左到右还是从右到左,最直接的方案当然是增加一个 `level` 变量,和上边的解法一样,来判断 `level` 是奇数还是偶数即可。 + +```java +public List> zigzagLevelOrder(TreeNode root) { + Queue queue = new LinkedList(); + List> ans = new LinkedList>(); + if (root == null) + return ans; + queue.offer(root); + int level = 0; + while (!queue.isEmpty()) { + int levelNum = queue.size(); // 当前层元素的个数 + List subList = new LinkedList(); + for (int i = 0; i < levelNum; i++) { + TreeNode curNode = queue.poll(); + if (curNode != null) { + if ((level % 2) == 0) { + subList.add(curNode.val); + } else { + subList.add(0, curNode.val); + } + queue.offer(curNode.left); + queue.offer(curNode.right); + } + } + //因为上边 queue.offer(curNode.left) 没有判断是否是 null + //所以要判断当前是否有元素 + if (subList.size() > 0) { + ans.add(subList); + } + level++; + } + return ans; +} +``` + +除了增加 `level` 变量外,我们还可以增加一个 `boolean` 变量来区别当前从左还是从右。此外 [这里]() 的评论里,看到了另外一种想法,不用添加新的变量。我们直接判断当前 `ans` 的大小,如果大小是 n 代表当前在添加第 n 层。 + +# 解法三 + +[这里]() 看到一个有趣的想法,分享一下。 + +我们直接用两个栈(或者队列)轮换着添加元素,一个栈从左到右添加元素,一个栈从右到左添加元素。 + +```java +public List> zigzagLevelOrder(TreeNode root) { + TreeNode c=root; + List> ans =new ArrayList>(); + if(c==null) return ans; + Stack s1=new Stack(); + Stack s2=new Stack(); + s1.push(root); + while(!s1.isEmpty()||!s2.isEmpty()) + { + List tmp=new ArrayList(); + while(!s1.isEmpty()) + { + c=s1.pop(); + tmp.add(c.val); + if(c.left!=null) s2.push(c.left); + if(c.right!=null) s2.push(c.right); + } + ans.add(tmp); + tmp=new ArrayList(); + while(!s2.isEmpty()) + { + c=s2.pop(); + tmp.add(c.val); + if(c.right!=null)s1.push(c.right); + if(c.left!=null)s1.push(c.left); + } + if(!tmp.isEmpty()) ans.add(tmp); + } + return ans; +} +``` + +# 总 + 这道题和 [102 题]() 区别不大,只需要对当前层进行判断即可。解法三用两个栈还是蛮有意思的。 \ No newline at end of file diff --git a/leetcode-104-Maximum-Depth-of-Binary-Tree.md b/leetcode-104-Maximum-Depth-of-Binary-Tree.md index 09ecbc7d8..1e958efbb 100644 --- a/leetcode-104-Maximum-Depth-of-Binary-Tree.md +++ b/leetcode-104-Maximum-Depth-of-Binary-Tree.md @@ -1,53 +1,53 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/104.jpg) - -输出二叉树的深度。 - -# 解法一 DFS - -依旧是考的二叉树的遍历。最简单的思路就是用递归进行 DFS 即可。 - -```java -public int maxDepth(TreeNode root) { - if (root == null) { - return 0; - } - return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1; -} -``` - -# 解法二 BFS - -可以直接仿照 [103 题](),利用一个队列,进行 BFS 即可。代码可以直接搬过来。 - -```java -public int maxDepth(TreeNode root) { - Queue queue = new LinkedList(); - List> ans = new LinkedList>(); - if (root == null) - return 0; - queue.offer(root); - int level = 0; - while (!queue.isEmpty()) { - int levelNum = queue.size(); // 当前层元素的个数 - for (int i = 0; i < levelNum; i++) { - TreeNode curNode = queue.poll(); - if (curNode != null) { - if (curNode.left != null) { - queue.offer(curNode.left); - } - if (curNode.right != null) { - queue.offer(curNode.right); - } - } - } - level++; - } - return level; -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/104.jpg) + +输出二叉树的深度。 + +# 解法一 DFS + +依旧是考的二叉树的遍历。最简单的思路就是用递归进行 DFS 即可。 + +```java +public int maxDepth(TreeNode root) { + if (root == null) { + return 0; + } + return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1; +} +``` + +# 解法二 BFS + +可以直接仿照 [103 题](),利用一个队列,进行 BFS 即可。代码可以直接搬过来。 + +```java +public int maxDepth(TreeNode root) { + Queue queue = new LinkedList(); + List> ans = new LinkedList>(); + if (root == null) + return 0; + queue.offer(root); + int level = 0; + while (!queue.isEmpty()) { + int levelNum = queue.size(); // 当前层元素的个数 + for (int i = 0; i < levelNum; i++) { + TreeNode curNode = queue.poll(); + if (curNode != null) { + if (curNode.left != null) { + queue.offer(curNode.left); + } + if (curNode.right != null) { + queue.offer(curNode.right); + } + } + } + level++; + } + return level; +} +``` + +# 总 + 依旧考的是二叉树的遍历方式,没有什么难点。 \ No newline at end of file diff --git a/leetcode-105-Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal.md b/leetcode-105-Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal.md index 03bcf7526..dcedf9d14 100644 --- a/leetcode-105-Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal.md +++ b/leetcode-105-Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal.md @@ -1,326 +1,326 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/105.jpg) - -根据二叉树的先序遍历和中序遍历还原二叉树。 - -# 解法一 递归 - -先序遍历的顺序是根节点,左子树,右子树。中序遍历的顺序是左子树,根节点,右子树。 - -所以我们只需要根据先序遍历得到根节点,然后在中序遍历中找到根节点的位置,它的左边就是左子树的节点,右边就是右子树的节点。 - -生成左子树和右子树就可以递归的进行了。 - -比如上图的例子,我们来分析一下。 - -```java -preorder = [3,9,20,15,7] -inorder = [9,3,15,20,7] -首先根据 preorder 找到根节点是 3 - -然后根据根节点将 inorder 分成左子树和右子树 -左子树 -inorder [9] - -右子树 -inorder [15,20,7] - -把相应的前序遍历的数组也加进来 -左子树 -preorder[9] -inorder [9] - -右子树 -preorder[20 15 7] -inorder [15,20,7] - -现在我们只需要构造左子树和右子树即可,成功把大问题化成了小问题 -然后重复上边的步骤继续划分,直到 preorder 和 inorder 都为空,返回 null 即可 -``` - -事实上,我们不需要真的把 `preorder` 和 `inorder` 切分了,只需要用分别用两个指针指向开头和结束位置即可。注意下边的两个指针指向的数组范围是包括左边界,不包括右边界。 - -对于下边的树的合成。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/105_3.jpg) - -左子树 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/105_4.jpg) - -右子树 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/105_5.jpg) - -```java -public TreeNode buildTree(int[] preorder, int[] inorder) { - return buildTreeHelper(preorder, 0, preorder.length, inorder, 0, inorder.length); -} - -private TreeNode buildTreeHelper(int[] preorder, int p_start, int p_end, int[] inorder, int i_start, int i_end) { - // preorder 为空,直接返回 null - if (p_start == p_end) { - return null; - } - int root_val = preorder[p_start]; - TreeNode root = new TreeNode(root_val); - //在中序遍历中找到根节点的位置 - int i_root_index = 0; - for (int i = i_start; i < i_end; i++) { - if (root_val == inorder[i]) { - i_root_index = i; - break; - } - } - int leftNum = i_root_index - i_start; - //递归的构造左子树 - root.left = buildTreeHelper(preorder, p_start + 1, p_start + leftNum + 1, inorder, i_start, i_root_index); - //递归的构造右子树 - root.right = buildTreeHelper(preorder, p_start + leftNum + 1, p_end, inorder, i_root_index + 1, i_end); - return root; -} -``` - -上边的代码很好理解,但存在一个问题,在中序遍历中找到根节点的位置每次都得遍历中序遍历的数组去寻找,参考[这里]() ,我们可以用一个`HashMap`把中序遍历数组的每个元素的值和下标存起来,这样寻找根节点的位置就可以直接得到了。 - -```java -public TreeNode buildTree(int[] preorder, int[] inorder) { - HashMap map = new HashMap<>(); - for (int i = 0; i < inorder.length; i++) { - map.put(inorder[i], i); - } - return buildTreeHelper(preorder, 0, preorder.length, inorder, 0, inorder.length, map); -} - -private TreeNode buildTreeHelper(int[] preorder, int p_start, int p_end, int[] inorder, int i_start, int i_end, - HashMap map) { - if (p_start == p_end) { - return null; - } - int root_val = preorder[p_start]; - TreeNode root = new TreeNode(root_val); - int i_root_index = map.get(root_val); - int leftNum = i_root_index - i_start; - root.left = buildTreeHelper(preorder, p_start + 1, p_start + leftNum + 1, inorder, i_start, i_root_index, map); - root.right = buildTreeHelper(preorder, p_start + leftNum + 1, p_end, inorder, i_root_index + 1, i_end, map); - return root; -} -``` - -本以为已经完美了,在 [这里]() 又看到了令人眼前一亮的思路,就是 StefanPochmann 大神,经常逛 Discuss 一定会注意到他,拥有 3 万多的赞。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/105_2.jpg) - -他也发现了每次都得遍历一次去找中序遍历数组中的根节点的麻烦,但他没有用 `HashMap`就解决了这个问题,下边来说一下。 - -用`pre`变量保存当前要构造的树的根节点,从根节点开始递归的构造左子树和右子树,`in`变量指向当前根节点可用数字的开头,然后对于当前`pre`有一个停止点`stop`,从`in`到`stop`表示要构造的树当前的数字范围。 - -```java -public TreeNode buildTree(int[] preorder, int[] inorder) { - return buildTreeHelper(preorder, inorder, (long)Integer.MAX_VALUE + 1); -} -int pre = 0; -int in = 0; -private TreeNode buildTreeHelper(int[] preorder, int[] inorder, long stop) { - //到达末尾返回 null - if(pre == preorder.length){ - return null; - } - //到达停止点返回 null - //当前停止点已经用了,in 后移 - if (inorder[in] == stop) { - in++; - return null; - } - int root_val = preorder[pre++]; - TreeNode root = new TreeNode(root_val); - //左子树的停止点是当前的根节点 - root.left = buildTreeHelper(preorder, inorder, root_val); - //右子树的停止点是当前树的停止点 - root.right = buildTreeHelper(preorder, inorder, stop); - return root; -} -``` - -代码很简洁,但如果细想起来真的很难理解了。 - -把他的原话也贴过来吧。 - -> Consider the example again. Instead of finding the `1` in `inorder`, splitting the arrays into parts and recursing on them, just recurse on the full remaining arrays and **stop** when you come across the `1` in `inorder`. That's what my above solution does. Each recursive call gets told where to stop, and it tells its subcalls where to stop. It gives its own root value as stopper to its left subcall and its parent`s stopper as stopper to its right subcall. - -本来很想讲清楚这个算法,但是各种画图,还是太难说清楚了。这里就画几个过程中的图,大家也只能按照上边的代码走一遍,理解一下了。 - -```java - 3 - / \ - 9 7 - / \ - 20 15 - -前序遍历数组和中序遍历数组 -preorder = [ 3, 9, 20, 15, 7 ] -inorder = [ 20, 9, 15, 3, 7 ] -p 代表 pre,i 代表 in,s 代表 stop - -首先构造根节点为 3 的树,可用数字是 i 到 s -s 初始化一个树中所有的数字都不会相等的数,所以代码中用了一个 long 来表示 -3, 9, 20, 15, 7 -^ -p -20, 9, 15, 3, 7 -^ ^ -i s - -考虑根节点为 3 的左子树, 考虑根节点为 3 的树的右子树, -stop 值是当前根节点的值 3 只知道 stop 值是上次的 s -新的根节点是 9,可用数字是 i 到 s -不包括 s -3, 9, 20, 15, 7 3, 9, 20, 15, 7 - ^ - p -20, 9, 15, 3, 7 20, 9, 15, 3, 7 -^ ^ ^ -i s s - -递归出口的情况 -3, 9, 20, 15, 7 - ^ - p -20, 9, 15, 3, 7 -^ -i -s -此时 in 和 stop 相等,表明没有可用的数字,所以返回 null,并且表明此时到达了某个树的根节点,所以 i 后移。 -``` - -总之他的思想就是,不再从中序遍历中寻找根节点的位置,而是直接把值传过去,表明当前子树的结束点。不过总感觉还是没有 get 到他的点,`in` 和 `stop` 变量的含义也是我赋予的,对于整个算法也只是勉强说通,大家有好的想法可以和我交流。 - -# 解法二 迭代 栈 - -参考 [这里](),我们可以利用一个栈,用迭代实现。 - -假设我们要还原的树是下图 - -```java - 3 - / \ - 9 7 - / \ - 20 15 -``` - -首先假设我们只有先序遍历的数组,如果还原一颗树,会遇到什么问题。 - -```java -preorder = [3, 9, 20, 15, 7 ] -``` - -首先我们把 `3` 作为根节点,然后到了 `9` ,就出现一个问题,`9` 是左子树还是右子树呢? - -所以需要再加上中序遍历的数组来确定。 - -```java -inorder = [ 20, 9, 15, 3, 7 ] -``` - -我们知道中序遍历,首先遍历左子树,然后是根节点,最后是右子树。这里第一个遍历的是 `20` ,说明先序遍历的 `9` 一定是左子树,利用反证法证明。 - -假如 `9` 是右子树,根据先序遍历 `preorder = [ 3, 9, 20, 15, 7 ]`,说明根节点 `3` 的左子树是空的, - -左子树为空,那么中序遍历就会先遍历根节点 `3`,而此时是 `20`,假设不成立,说明 `9` 是左子树。 - -接下来的 `20` 同理,所以可以目前构建出来的树如下。 - -```java - 3 - / - 9 - / - 20 -``` - -同时,还注意到此时先序遍历的 `20` 和中序遍历 `20` 相等了,说明什么呢? - -说明中序遍历的下一个数 `15` 不是左子树了,如果是左子树,那么中序遍历的第一个数就不会是 `20`。 - -所以 `15` 一定是右子树了,现在还有个问题,它是 `20` 的右子树,还是 `9` 的右子树,还是 `3` 的右子树? - -我们来假设几种情况,来想一下。 - -1. 如果是 `3` 的右子树, `20` 和 `9` 的右子树为空,那么中序遍历就是`20 9 3 15`。 - -2. 如果是 `9` 的右子树,`20` 的右子树为空,那么中序遍历就是`20 9 15`。 - -3. 如果是 `20` 的右子树,那么中序遍历就是`20 15`。 - -之前已经遍历的根节点是 `3 9 20`,**把它倒过来,即`20 9 3`**,然后和上边的三种中序遍历比较,会发现 `15` 就是**最后一次相等**的节点的右子树。 - -第 1 种情况,中序遍历是`20 9 3 15`,和`20 9 3` 都相等,所以 `15` 是`3` 的右子树。 - -第 2 种情况,中序遍历是`20 9 15`,只有`20 9` 相等,所以 `15` 是 `9` 的右子树。 - -第 3 种情况,中序遍历就是`20 15`,只有`20` 相等,所以 `20` 是 `15` 的右子树。 - -而此时我们的中序遍历数组是`inorder = [ 20, 9 ,15, 3, 7 ]`,`20` 匹配,`9`匹配,最后一次匹配是 `9`,所以 `15` 是 `9`的右子树。 - -```java - 3 - / - 9 - / \ - 20 15 -``` - -综上所述,我们用一个栈保存已经遍历过的节点,遍历前序遍历的数组,一直作为当前根节点的左子树,直到当前节点和中序遍历的数组的节点相等了,那么我们正序遍历中序遍历的数组,倒着遍历已经遍历过的根节点(用栈的 pop 实现),找到最后一次相等的位置,把它作为该节点的右子树。 - -上边的分析就是迭代总体的思想,代码的话还有一些细节注意一下。用一个栈保存已经遍历的节点,用 curRoot 保存当前正在遍历的节点。 - -```java -public TreeNode buildTree(int[] preorder, int[] inorder) { - if (preorder.length == 0) { - return null; - } - Stack roots = new Stack(); - int pre = 0; - int in = 0; - //先序遍历第一个值作为根节点 - TreeNode curRoot = new TreeNode(preorder[pre]); - TreeNode root = curRoot; - roots.push(curRoot); - pre++; - //遍历前序遍历的数组 - while (pre < preorder.length) { - //出现了当前节点的值和中序遍历数组的值相等,寻找是谁的右子树 - if (curRoot.val == inorder[in]) { - //每次进行出栈,实现倒着遍历 - while (!roots.isEmpty() && roots.peek().val == inorder[in]) { - curRoot = roots.peek(); - roots.pop(); - in++; - } - //设为当前的右孩子 - curRoot.right = new TreeNode(preorder[pre]); - //更新 curRoot - curRoot = curRoot.right; - roots.push(curRoot); - pre++; - } else { - //否则的话就一直作为左子树 - curRoot.left = new TreeNode(preorder[pre]); - curRoot = curRoot.left; - roots.push(curRoot); - pre++; - } - } - return root; -} - -``` - -# 总 - -用常规的递归和 HashMap 做的话这道题是不难的,用 `stop` 变量省去 HashMap 的思想以及解法二的迭代可以了解一下吧,不是很容易想到。 - - - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/105.jpg) + +根据二叉树的先序遍历和中序遍历还原二叉树。 + +# 解法一 递归 + +先序遍历的顺序是根节点,左子树,右子树。中序遍历的顺序是左子树,根节点,右子树。 + +所以我们只需要根据先序遍历得到根节点,然后在中序遍历中找到根节点的位置,它的左边就是左子树的节点,右边就是右子树的节点。 + +生成左子树和右子树就可以递归的进行了。 + +比如上图的例子,我们来分析一下。 + +```java +preorder = [3,9,20,15,7] +inorder = [9,3,15,20,7] +首先根据 preorder 找到根节点是 3 + +然后根据根节点将 inorder 分成左子树和右子树 +左子树 +inorder [9] + +右子树 +inorder [15,20,7] + +把相应的前序遍历的数组也加进来 +左子树 +preorder[9] +inorder [9] + +右子树 +preorder[20 15 7] +inorder [15,20,7] + +现在我们只需要构造左子树和右子树即可,成功把大问题化成了小问题 +然后重复上边的步骤继续划分,直到 preorder 和 inorder 都为空,返回 null 即可 +``` + +事实上,我们不需要真的把 `preorder` 和 `inorder` 切分了,只需要用分别用两个指针指向开头和结束位置即可。注意下边的两个指针指向的数组范围是包括左边界,不包括右边界。 + +对于下边的树的合成。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/105_3.jpg) + +左子树 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/105_4.jpg) + +右子树 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/105_5.jpg) + +```java +public TreeNode buildTree(int[] preorder, int[] inorder) { + return buildTreeHelper(preorder, 0, preorder.length, inorder, 0, inorder.length); +} + +private TreeNode buildTreeHelper(int[] preorder, int p_start, int p_end, int[] inorder, int i_start, int i_end) { + // preorder 为空,直接返回 null + if (p_start == p_end) { + return null; + } + int root_val = preorder[p_start]; + TreeNode root = new TreeNode(root_val); + //在中序遍历中找到根节点的位置 + int i_root_index = 0; + for (int i = i_start; i < i_end; i++) { + if (root_val == inorder[i]) { + i_root_index = i; + break; + } + } + int leftNum = i_root_index - i_start; + //递归的构造左子树 + root.left = buildTreeHelper(preorder, p_start + 1, p_start + leftNum + 1, inorder, i_start, i_root_index); + //递归的构造右子树 + root.right = buildTreeHelper(preorder, p_start + leftNum + 1, p_end, inorder, i_root_index + 1, i_end); + return root; +} +``` + +上边的代码很好理解,但存在一个问题,在中序遍历中找到根节点的位置每次都得遍历中序遍历的数组去寻找,参考[这里]() ,我们可以用一个`HashMap`把中序遍历数组的每个元素的值和下标存起来,这样寻找根节点的位置就可以直接得到了。 + +```java +public TreeNode buildTree(int[] preorder, int[] inorder) { + HashMap map = new HashMap<>(); + for (int i = 0; i < inorder.length; i++) { + map.put(inorder[i], i); + } + return buildTreeHelper(preorder, 0, preorder.length, inorder, 0, inorder.length, map); +} + +private TreeNode buildTreeHelper(int[] preorder, int p_start, int p_end, int[] inorder, int i_start, int i_end, + HashMap map) { + if (p_start == p_end) { + return null; + } + int root_val = preorder[p_start]; + TreeNode root = new TreeNode(root_val); + int i_root_index = map.get(root_val); + int leftNum = i_root_index - i_start; + root.left = buildTreeHelper(preorder, p_start + 1, p_start + leftNum + 1, inorder, i_start, i_root_index, map); + root.right = buildTreeHelper(preorder, p_start + leftNum + 1, p_end, inorder, i_root_index + 1, i_end, map); + return root; +} +``` + +本以为已经完美了,在 [这里]() 又看到了令人眼前一亮的思路,就是 StefanPochmann 大神,经常逛 Discuss 一定会注意到他,拥有 3 万多的赞。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/105_2.jpg) + +他也发现了每次都得遍历一次去找中序遍历数组中的根节点的麻烦,但他没有用 `HashMap`就解决了这个问题,下边来说一下。 + +用`pre`变量保存当前要构造的树的根节点,从根节点开始递归的构造左子树和右子树,`in`变量指向当前根节点可用数字的开头,然后对于当前`pre`有一个停止点`stop`,从`in`到`stop`表示要构造的树当前的数字范围。 + +```java +public TreeNode buildTree(int[] preorder, int[] inorder) { + return buildTreeHelper(preorder, inorder, (long)Integer.MAX_VALUE + 1); +} +int pre = 0; +int in = 0; +private TreeNode buildTreeHelper(int[] preorder, int[] inorder, long stop) { + //到达末尾返回 null + if(pre == preorder.length){ + return null; + } + //到达停止点返回 null + //当前停止点已经用了,in 后移 + if (inorder[in] == stop) { + in++; + return null; + } + int root_val = preorder[pre++]; + TreeNode root = new TreeNode(root_val); + //左子树的停止点是当前的根节点 + root.left = buildTreeHelper(preorder, inorder, root_val); + //右子树的停止点是当前树的停止点 + root.right = buildTreeHelper(preorder, inorder, stop); + return root; +} +``` + +代码很简洁,但如果细想起来真的很难理解了。 + +把他的原话也贴过来吧。 + +> Consider the example again. Instead of finding the `1` in `inorder`, splitting the arrays into parts and recursing on them, just recurse on the full remaining arrays and **stop** when you come across the `1` in `inorder`. That's what my above solution does. Each recursive call gets told where to stop, and it tells its subcalls where to stop. It gives its own root value as stopper to its left subcall and its parent`s stopper as stopper to its right subcall. + +本来很想讲清楚这个算法,但是各种画图,还是太难说清楚了。这里就画几个过程中的图,大家也只能按照上边的代码走一遍,理解一下了。 + +```java + 3 + / \ + 9 7 + / \ + 20 15 + +前序遍历数组和中序遍历数组 +preorder = [ 3, 9, 20, 15, 7 ] +inorder = [ 20, 9, 15, 3, 7 ] +p 代表 pre,i 代表 in,s 代表 stop + +首先构造根节点为 3 的树,可用数字是 i 到 s +s 初始化一个树中所有的数字都不会相等的数,所以代码中用了一个 long 来表示 +3, 9, 20, 15, 7 +^ +p +20, 9, 15, 3, 7 +^ ^ +i s + +考虑根节点为 3 的左子树, 考虑根节点为 3 的树的右子树, +stop 值是当前根节点的值 3 只知道 stop 值是上次的 s +新的根节点是 9,可用数字是 i 到 s +不包括 s +3, 9, 20, 15, 7 3, 9, 20, 15, 7 + ^ + p +20, 9, 15, 3, 7 20, 9, 15, 3, 7 +^ ^ ^ +i s s + +递归出口的情况 +3, 9, 20, 15, 7 + ^ + p +20, 9, 15, 3, 7 +^ +i +s +此时 in 和 stop 相等,表明没有可用的数字,所以返回 null,并且表明此时到达了某个树的根节点,所以 i 后移。 +``` + +总之他的思想就是,不再从中序遍历中寻找根节点的位置,而是直接把值传过去,表明当前子树的结束点。不过总感觉还是没有 get 到他的点,`in` 和 `stop` 变量的含义也是我赋予的,对于整个算法也只是勉强说通,大家有好的想法可以和我交流。 + +# 解法二 迭代 栈 + +参考 [这里](),我们可以利用一个栈,用迭代实现。 + +假设我们要还原的树是下图 + +```java + 3 + / \ + 9 7 + / \ + 20 15 +``` + +首先假设我们只有先序遍历的数组,如果还原一颗树,会遇到什么问题。 + +```java +preorder = [3, 9, 20, 15, 7 ] +``` + +首先我们把 `3` 作为根节点,然后到了 `9` ,就出现一个问题,`9` 是左子树还是右子树呢? + +所以需要再加上中序遍历的数组来确定。 + +```java +inorder = [ 20, 9, 15, 3, 7 ] +``` + +我们知道中序遍历,首先遍历左子树,然后是根节点,最后是右子树。这里第一个遍历的是 `20` ,说明先序遍历的 `9` 一定是左子树,利用反证法证明。 + +假如 `9` 是右子树,根据先序遍历 `preorder = [ 3, 9, 20, 15, 7 ]`,说明根节点 `3` 的左子树是空的, + +左子树为空,那么中序遍历就会先遍历根节点 `3`,而此时是 `20`,假设不成立,说明 `9` 是左子树。 + +接下来的 `20` 同理,所以可以目前构建出来的树如下。 + +```java + 3 + / + 9 + / + 20 +``` + +同时,还注意到此时先序遍历的 `20` 和中序遍历 `20` 相等了,说明什么呢? + +说明中序遍历的下一个数 `15` 不是左子树了,如果是左子树,那么中序遍历的第一个数就不会是 `20`。 + +所以 `15` 一定是右子树了,现在还有个问题,它是 `20` 的右子树,还是 `9` 的右子树,还是 `3` 的右子树? + +我们来假设几种情况,来想一下。 + +1. 如果是 `3` 的右子树, `20` 和 `9` 的右子树为空,那么中序遍历就是`20 9 3 15`。 + +2. 如果是 `9` 的右子树,`20` 的右子树为空,那么中序遍历就是`20 9 15`。 + +3. 如果是 `20` 的右子树,那么中序遍历就是`20 15`。 + +之前已经遍历的根节点是 `3 9 20`,**把它倒过来,即`20 9 3`**,然后和上边的三种中序遍历比较,会发现 `15` 就是**最后一次相等**的节点的右子树。 + +第 1 种情况,中序遍历是`20 9 3 15`,和`20 9 3` 都相等,所以 `15` 是`3` 的右子树。 + +第 2 种情况,中序遍历是`20 9 15`,只有`20 9` 相等,所以 `15` 是 `9` 的右子树。 + +第 3 种情况,中序遍历就是`20 15`,只有`20` 相等,所以 `20` 是 `15` 的右子树。 + +而此时我们的中序遍历数组是`inorder = [ 20, 9 ,15, 3, 7 ]`,`20` 匹配,`9`匹配,最后一次匹配是 `9`,所以 `15` 是 `9`的右子树。 + +```java + 3 + / + 9 + / \ + 20 15 +``` + +综上所述,我们用一个栈保存已经遍历过的节点,遍历前序遍历的数组,一直作为当前根节点的左子树,直到当前节点和中序遍历的数组的节点相等了,那么我们正序遍历中序遍历的数组,倒着遍历已经遍历过的根节点(用栈的 pop 实现),找到最后一次相等的位置,把它作为该节点的右子树。 + +上边的分析就是迭代总体的思想,代码的话还有一些细节注意一下。用一个栈保存已经遍历的节点,用 curRoot 保存当前正在遍历的节点。 + +```java +public TreeNode buildTree(int[] preorder, int[] inorder) { + if (preorder.length == 0) { + return null; + } + Stack roots = new Stack(); + int pre = 0; + int in = 0; + //先序遍历第一个值作为根节点 + TreeNode curRoot = new TreeNode(preorder[pre]); + TreeNode root = curRoot; + roots.push(curRoot); + pre++; + //遍历前序遍历的数组 + while (pre < preorder.length) { + //出现了当前节点的值和中序遍历数组的值相等,寻找是谁的右子树 + if (curRoot.val == inorder[in]) { + //每次进行出栈,实现倒着遍历 + while (!roots.isEmpty() && roots.peek().val == inorder[in]) { + curRoot = roots.peek(); + roots.pop(); + in++; + } + //设为当前的右孩子 + curRoot.right = new TreeNode(preorder[pre]); + //更新 curRoot + curRoot = curRoot.right; + roots.push(curRoot); + pre++; + } else { + //否则的话就一直作为左子树 + curRoot.left = new TreeNode(preorder[pre]); + curRoot = curRoot.left; + roots.push(curRoot); + pre++; + } + } + return root; +} + +``` + +# 总 + +用常规的递归和 HashMap 做的话这道题是不难的,用 `stop` 变量省去 HashMap 的思想以及解法二的迭代可以了解一下吧,不是很容易想到。 + + + diff --git a/leetcode-106-Construct-Binary-Tree-from-Inorder-and-Postorder-Traversal.md b/leetcode-106-Construct-Binary-Tree-from-Inorder-and-Postorder-Traversal.md index 223362a84..f8d38fc54 100644 --- a/leetcode-106-Construct-Binary-Tree-from-Inorder-and-Postorder-Traversal.md +++ b/leetcode-106-Construct-Binary-Tree-from-Inorder-and-Postorder-Traversal.md @@ -1,148 +1,148 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/106.jpg) - -根据二叉树的中序遍历和后序遍历还原二叉树。 - -# 思路分析 - -可以先看一下 [105 题](),直接在 105 题的基础上改了,大家也可以先根据 105 题改一改。 - -105 题给的是先序遍历和中序遍历,这里把先序遍历换成了后序遍历。 - -区别在于先序遍历的顺序是 根节点 -> 左子树 -> 右子树。 - -后序遍历的顺序是 左子树 -> 右子树 -> 根节点。 - -我们当然还是先确定根节点,然后在中序遍历中找根节点的位置,然后分出左子树和右子树。 - -对于之前的解法一,传数组的两个边界,影响不大,只要重新计算边界就可以了。 - -但是对于另外两种解法,利用 stop 和栈的算法,之前都是通过遍历前序遍历的数组实现的。所以构造过程是根节点,左子树,右子树。 - -但这里如果是后序遍历,我们先找根节点,所以相当于从右往左遍历,这样的顺序的话就成了,根节点 -> 右子树 -> 左子树,所以我们会先生成右子树,再生成左子树。 - -# 解法一 - -常规解法,利用递归,传递左子树和右子树的数组范围即可。 - -```java -public TreeNode buildTree(int[] inorder, int[] postorder) { - HashMap map = new HashMap<>(); - for (int i = 0; i < inorder.length; i++) { - map.put(inorder[i], i); - } - return buildTreeHelper(inorder, 0, inorder.length, postorder, 0, postorder.length, map); -} - -private TreeNode buildTreeHelper(int[] inorder, int i_start, int i_end, int[] postorder, int p_start, int p_end, - HashMap map) { - if (p_start == p_end) { - return null; - } - int root_val = postorder[p_end - 1]; - TreeNode root = new TreeNode(root_val); - int i_root_index = map.get(root_val); - int leftNum = i_root_index - i_start; - root.left = buildTreeHelper(inorder, i_start, i_root_index, postorder, p_start, p_start + leftNum, map); - root.right = buildTreeHelper(inorder, i_root_index + 1, i_end, postorder, p_start + leftNum, p_end - 1, - map); - return root; -} -``` - -# 解法二 stop 值 - -这里的话,之前说了,递归的话得先构造右子树再构造左子树,此外各种指针,也应该从末尾向零走。 - -视线从右往左看。 - -```java - 3 - / \ - 9 20 - / \ - 15 7 - -s 初始化一个树中所有的数字都不会相等的数,所以代码中用了一个 long 来表示 -<------------------ -中序 - 9, 3, 15, 20, 7 -^ ^ -s i - -后序 -9, 15, 7, 20, 3 - ^ - p -<------------------- -``` - -`p` 和 `i` 都从右往左进行遍历,所以 `p` 开始产生的每次都是右子树的根节点。之前代码里的`++`要相应的改成`--`。 - -```java -int post; -int in; -public TreeNode buildTree(int[] inorder, int[] postorder) { - post = postorder.length - 1; - in = inorder.length - 1; - return buildTreeHelper(inorder, postorder, (long) Integer.MIN_VALUE - 1); -} - -private TreeNode buildTreeHelper(int[] inorder, int[] postorder, long stop) { - if (post == -1) { - return null; - } - if (inorder[in] == stop) { - in--; - return null; - } - int root_val = postorder[post--]; - TreeNode root = new TreeNode(root_val); - root.right = buildTreeHelper(inorder, postorder, root_val); - root.left = buildTreeHelper(inorder, postorder, stop); - return root; -} -``` - -# 解法三 栈 - -之前解法是构造左子树、左子树、左子树,出现相等,构造一颗右子树。这里相应的要改成构造右子树、右子树、右子树,出现相等,构造一颗左子树。和解法二一样,两个指针的话也是从末尾到头部进行。 - -```java -public TreeNode buildTree(int[] inorder, int[] postorder) { - if (postorder.length == 0) { - return null; - } - Stack roots = new Stack(); - int post = postorder.length - 1; - int in = inorder.length - 1; - TreeNode curRoot = new TreeNode(postorder[post]); - TreeNode root = curRoot; - roots.push(curRoot); - post--; - while (post >= 0) { - if (curRoot.val == inorder[in]) { - while (!roots.isEmpty() && roots.peek().val == inorder[in]) { - curRoot = roots.peek(); - roots.pop(); - in--; - } - curRoot.left = new TreeNode(postorder[post]); - curRoot = curRoot.left; - roots.push(curRoot); - post--; - } else { - curRoot.right = new TreeNode(postorder[post]); - curRoot = curRoot.right; - roots.push(curRoot); - post--; - } - } - return root; -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/106.jpg) + +根据二叉树的中序遍历和后序遍历还原二叉树。 + +# 思路分析 + +可以先看一下 [105 题](),直接在 105 题的基础上改了,大家也可以先根据 105 题改一改。 + +105 题给的是先序遍历和中序遍历,这里把先序遍历换成了后序遍历。 + +区别在于先序遍历的顺序是 根节点 -> 左子树 -> 右子树。 + +后序遍历的顺序是 左子树 -> 右子树 -> 根节点。 + +我们当然还是先确定根节点,然后在中序遍历中找根节点的位置,然后分出左子树和右子树。 + +对于之前的解法一,传数组的两个边界,影响不大,只要重新计算边界就可以了。 + +但是对于另外两种解法,利用 stop 和栈的算法,之前都是通过遍历前序遍历的数组实现的。所以构造过程是根节点,左子树,右子树。 + +但这里如果是后序遍历,我们先找根节点,所以相当于从右往左遍历,这样的顺序的话就成了,根节点 -> 右子树 -> 左子树,所以我们会先生成右子树,再生成左子树。 + +# 解法一 + +常规解法,利用递归,传递左子树和右子树的数组范围即可。 + +```java +public TreeNode buildTree(int[] inorder, int[] postorder) { + HashMap map = new HashMap<>(); + for (int i = 0; i < inorder.length; i++) { + map.put(inorder[i], i); + } + return buildTreeHelper(inorder, 0, inorder.length, postorder, 0, postorder.length, map); +} + +private TreeNode buildTreeHelper(int[] inorder, int i_start, int i_end, int[] postorder, int p_start, int p_end, + HashMap map) { + if (p_start == p_end) { + return null; + } + int root_val = postorder[p_end - 1]; + TreeNode root = new TreeNode(root_val); + int i_root_index = map.get(root_val); + int leftNum = i_root_index - i_start; + root.left = buildTreeHelper(inorder, i_start, i_root_index, postorder, p_start, p_start + leftNum, map); + root.right = buildTreeHelper(inorder, i_root_index + 1, i_end, postorder, p_start + leftNum, p_end - 1, + map); + return root; +} +``` + +# 解法二 stop 值 + +这里的话,之前说了,递归的话得先构造右子树再构造左子树,此外各种指针,也应该从末尾向零走。 + +视线从右往左看。 + +```java + 3 + / \ + 9 20 + / \ + 15 7 + +s 初始化一个树中所有的数字都不会相等的数,所以代码中用了一个 long 来表示 +<------------------ +中序 + 9, 3, 15, 20, 7 +^ ^ +s i + +后序 +9, 15, 7, 20, 3 + ^ + p +<------------------- +``` + +`p` 和 `i` 都从右往左进行遍历,所以 `p` 开始产生的每次都是右子树的根节点。之前代码里的`++`要相应的改成`--`。 + +```java +int post; +int in; +public TreeNode buildTree(int[] inorder, int[] postorder) { + post = postorder.length - 1; + in = inorder.length - 1; + return buildTreeHelper(inorder, postorder, (long) Integer.MIN_VALUE - 1); +} + +private TreeNode buildTreeHelper(int[] inorder, int[] postorder, long stop) { + if (post == -1) { + return null; + } + if (inorder[in] == stop) { + in--; + return null; + } + int root_val = postorder[post--]; + TreeNode root = new TreeNode(root_val); + root.right = buildTreeHelper(inorder, postorder, root_val); + root.left = buildTreeHelper(inorder, postorder, stop); + return root; +} +``` + +# 解法三 栈 + +之前解法是构造左子树、左子树、左子树,出现相等,构造一颗右子树。这里相应的要改成构造右子树、右子树、右子树,出现相等,构造一颗左子树。和解法二一样,两个指针的话也是从末尾到头部进行。 + +```java +public TreeNode buildTree(int[] inorder, int[] postorder) { + if (postorder.length == 0) { + return null; + } + Stack roots = new Stack(); + int post = postorder.length - 1; + int in = inorder.length - 1; + TreeNode curRoot = new TreeNode(postorder[post]); + TreeNode root = curRoot; + roots.push(curRoot); + post--; + while (post >= 0) { + if (curRoot.val == inorder[in]) { + while (!roots.isEmpty() && roots.peek().val == inorder[in]) { + curRoot = roots.peek(); + roots.pop(); + in--; + } + curRoot.left = new TreeNode(postorder[post]); + curRoot = curRoot.left; + roots.push(curRoot); + post--; + } else { + curRoot.right = new TreeNode(postorder[post]); + curRoot = curRoot.right; + roots.push(curRoot); + post--; + } + } + return root; +} +``` + +# 总 + 理解了 [105 题]() 的话,这道题很快就出来了,完全是 105 题的逆向思考。 \ No newline at end of file diff --git a/leetcode-107-Binary-Tree-Level-Order-TraversalII.md b/leetcode-107-Binary-Tree-Level-Order-TraversalII.md index a738e28c1..fa73e57e3 100644 --- a/leetcode-107-Binary-Tree-Level-Order-TraversalII.md +++ b/leetcode-107-Binary-Tree-Level-Order-TraversalII.md @@ -1,173 +1,173 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/107.jpg) - -树的层次遍历,和 [102 题]() 的不同之处是,之前输出的数组顺序是从根部一层一层的输出,现在是从底部,一层一层的输出。 - -# 解法一 DFS - -把 [102 题]() 的`DFS`贴过来看一下。 - -```java -public List> levelOrder(TreeNode root) { - List> ans = new ArrayList<>(); - DFS(root, 0, ans); - return ans; -} - -private void DFS(TreeNode root, int level, List> ans) { - if(root == null){ - return; - } - //当前层数还没有元素,先 new 一个空的列表 - if(ans.size()<=level){ - ans.add(new ArrayList<>()); - } - //当前值加入 - ans.get(level).add(root.val); - - DFS(root.left,level+1,ans); - DFS(root.right,level+1,ans); -} -``` - -之前我们根据 level 得到数组的位置,然后添加。 - -```java -ans.get(level).add(root.val); - -ans [] [] [] [] []. -index 0 1 2 3 4 -level 0 1 2 3 4 - ------------> -index = 0 + level - -现在 level 是逆过来存的 -ans [] [] [] [] []. -index 0 1 2 3 4 -level 4 3 2 1 0 - <------------ -index = 4 - level - -4 就是 ans 的末尾下标,就是 ans.size() - 1 -所以代码变为 -ans.get(ans.size() - 1 - level).add(root.val); -``` - -此外还有句代码要改。 - -```java -if(ans.size()<=level){ - ans.add(new ArrayList<>()); -} -在添加当前 level 的第一个元素的时候,首先添加一个空列表到 ans 中 -假设当前 level = 2,ans 中只添加了 level 是 0 和 1 的元素 -ans [3] [9] -index 0 1 -level 1 0 -因为 level 是从右往左增加的,所以空列表要到 ans 的头部 -ans [] [3] [9] -index 0 1 2 -level 2 1 0 -所以代码改成下边的样子 - ans.add(0,new ArrayList<>()); -``` - -综上,只要改了这两处就可以了。 - -```java -public List> levelOrderBottom(TreeNode root) { - List> ans = new ArrayList<>(); - DFS(root, 0, ans); - return ans; -} - -private void DFS(TreeNode root, int level, List> ans) { - if (root == null) { - return; - } - // 当前层数还没有元素,先 new 一个空的列表 - if (ans.size() <= level) { - ans.add(0, new ArrayList<>()); - } - // 当前值加入 - ans.get(ans.size() - 1 - level).add(root.val); - - DFS(root.left, level + 1, ans); - DFS(root.right, level + 1, ans); -} -``` - -# 解法二 BFS - - [102 题]() 从根节点往下走的代码贴过来。 - -```java -public List> levelOrder(TreeNode root) { - Queue queue = new LinkedList(); - List> ans = new LinkedList>(); - if (root == null) - return ans; - queue.offer(root); - while (!queue.isEmpty()) { - int levelNum = queue.size(); // 当前层元素的个数 - List subList = new LinkedList(); - for (int i = 0; i < levelNum; i++) { - TreeNode curNode = queue.poll(); - if (curNode != null) { - subList.add(curNode.val); - queue.offer(curNode.left); - queue.offer(curNode.right); - } - } - if(subList.size()>0){ - ans.add(subList); - } - } - return ans; -} -``` - -`BFS`相比于`DFS`要简单些,因为`BFS`是一次性把当前层的元素都添加到`ans`中,所以我们只需要改一句代码。 - -```java -ans.add(subList); -``` - -改成添加到头部即可。 - -```java -ans.add(0,subList); -``` - -再改个函数名字, 总体代码就是 - -```java -public List> levelOrderBottom(TreeNode root) { - Queue queue = new LinkedList(); - List> ans = new LinkedList>(); - if (root == null) - return ans; - queue.offer(root); - while (!queue.isEmpty()) { - int levelNum = queue.size(); // 当前层元素的个数 - List subList = new LinkedList(); - for (int i = 0; i < levelNum; i++) { - TreeNode curNode = queue.poll(); - if (curNode != null) { - subList.add(curNode.val); - queue.offer(curNode.left); - queue.offer(curNode.right); - } - } - if (subList.size() > 0) { - ans.add(0, subList); - } - } - return ans; -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/107.jpg) + +树的层次遍历,和 [102 题]() 的不同之处是,之前输出的数组顺序是从根部一层一层的输出,现在是从底部,一层一层的输出。 + +# 解法一 DFS + +把 [102 题]() 的`DFS`贴过来看一下。 + +```java +public List> levelOrder(TreeNode root) { + List> ans = new ArrayList<>(); + DFS(root, 0, ans); + return ans; +} + +private void DFS(TreeNode root, int level, List> ans) { + if(root == null){ + return; + } + //当前层数还没有元素,先 new 一个空的列表 + if(ans.size()<=level){ + ans.add(new ArrayList<>()); + } + //当前值加入 + ans.get(level).add(root.val); + + DFS(root.left,level+1,ans); + DFS(root.right,level+1,ans); +} +``` + +之前我们根据 level 得到数组的位置,然后添加。 + +```java +ans.get(level).add(root.val); + +ans [] [] [] [] []. +index 0 1 2 3 4 +level 0 1 2 3 4 + ------------> +index = 0 + level + +现在 level 是逆过来存的 +ans [] [] [] [] []. +index 0 1 2 3 4 +level 4 3 2 1 0 + <------------ +index = 4 - level + +4 就是 ans 的末尾下标,就是 ans.size() - 1 +所以代码变为 +ans.get(ans.size() - 1 - level).add(root.val); +``` + +此外还有句代码要改。 + +```java +if(ans.size()<=level){ + ans.add(new ArrayList<>()); +} +在添加当前 level 的第一个元素的时候,首先添加一个空列表到 ans 中 +假设当前 level = 2,ans 中只添加了 level 是 0 和 1 的元素 +ans [3] [9] +index 0 1 +level 1 0 +因为 level 是从右往左增加的,所以空列表要到 ans 的头部 +ans [] [3] [9] +index 0 1 2 +level 2 1 0 +所以代码改成下边的样子 + ans.add(0,new ArrayList<>()); +``` + +综上,只要改了这两处就可以了。 + +```java +public List> levelOrderBottom(TreeNode root) { + List> ans = new ArrayList<>(); + DFS(root, 0, ans); + return ans; +} + +private void DFS(TreeNode root, int level, List> ans) { + if (root == null) { + return; + } + // 当前层数还没有元素,先 new 一个空的列表 + if (ans.size() <= level) { + ans.add(0, new ArrayList<>()); + } + // 当前值加入 + ans.get(ans.size() - 1 - level).add(root.val); + + DFS(root.left, level + 1, ans); + DFS(root.right, level + 1, ans); +} +``` + +# 解法二 BFS + + [102 题]() 从根节点往下走的代码贴过来。 + +```java +public List> levelOrder(TreeNode root) { + Queue queue = new LinkedList(); + List> ans = new LinkedList>(); + if (root == null) + return ans; + queue.offer(root); + while (!queue.isEmpty()) { + int levelNum = queue.size(); // 当前层元素的个数 + List subList = new LinkedList(); + for (int i = 0; i < levelNum; i++) { + TreeNode curNode = queue.poll(); + if (curNode != null) { + subList.add(curNode.val); + queue.offer(curNode.left); + queue.offer(curNode.right); + } + } + if(subList.size()>0){ + ans.add(subList); + } + } + return ans; +} +``` + +`BFS`相比于`DFS`要简单些,因为`BFS`是一次性把当前层的元素都添加到`ans`中,所以我们只需要改一句代码。 + +```java +ans.add(subList); +``` + +改成添加到头部即可。 + +```java +ans.add(0,subList); +``` + +再改个函数名字, 总体代码就是 + +```java +public List> levelOrderBottom(TreeNode root) { + Queue queue = new LinkedList(); + List> ans = new LinkedList>(); + if (root == null) + return ans; + queue.offer(root); + while (!queue.isEmpty()) { + int levelNum = queue.size(); // 当前层元素的个数 + List subList = new LinkedList(); + for (int i = 0; i < levelNum; i++) { + TreeNode curNode = queue.poll(); + if (curNode != null) { + subList.add(curNode.val); + queue.offer(curNode.left); + queue.offer(curNode.right); + } + } + if (subList.size() > 0) { + ans.add(0, subList); + } + } + return ans; +} +``` + +# 总 + 这道题依旧考层次遍历,只需要在 [102 题]() 的基础上,找到 `level` 和 `index` 的对应关系即可。此外,因为我们在头部添加元素,所以用链表会好一些。如果数组的话,还得整体后移才能添加新的元素。 \ No newline at end of file diff --git a/leetcode-108-Convert-Sorted-Array-to-Binary-Search-Tree.md b/leetcode-108-Convert-Sorted-Array-to-Binary-Search-Tree.md index 62cb38e8a..c906becbe 100644 --- a/leetcode-108-Convert-Sorted-Array-to-Binary-Search-Tree.md +++ b/leetcode-108-Convert-Sorted-Array-to-Binary-Search-Tree.md @@ -1,256 +1,256 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/108.jpg) - -给一个升序数组,生成一个平衡二叉搜索树。平衡二叉树定义如下: - -> 它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。 - -二叉搜索树定义如下: - -> 1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值; -> 2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值; -> 3. 任意节点的左、右子树也分别为二叉查找树; -> 4. 没有键值相等的节点。 - -# 解法一 递归 - -如果做了 [98 题]() 和 [99 题](),那么又看到这里的升序数组,然后应该会想到一个点,二叉搜索树的中序遍历刚好可以输出一个升序数组。 - -所以题目给出的升序数组就是二叉搜索树的中序遍历。 - -根据中序遍历还原一颗树,又想到了 [105 题]() 和 [106 题](),通过中序遍历加前序遍历或者中序遍历加后序遍历来还原一棵树。前序(后序)遍历的作用呢?提供根节点!然后根据根节点,就可以递归的生成左右子树。 - -这里的话怎么知道根节点呢?平衡二叉树,既然要做到平衡,我们只要把根节点选为数组的中点即可。 - -综上,和之前一样,找到了根节点,然后把数组一分为二,进入递归即可。注意这里的边界情况,包括左边界,不包括右边界。 - -```java -public TreeNode sortedArrayToBST(int[] nums) { - return sortedArrayToBST(nums, 0, nums.length); -} - -private TreeNode sortedArrayToBST(int[] nums, int start, int end) { - if (start == end) { - return null; - } - int mid = (start + end) >>> 1; - TreeNode root = new TreeNode(nums[mid]); - root.left = sortedArrayToBST(nums, start, mid); - root.right = sortedArrayToBST(nums, mid + 1, end); - - return root; -} -``` - -# 解法二 栈 DFS - -递归都可以转为迭代的形式。 - -一部分递归算法,可以转成动态规划,实现空间换时间,例如 [5题](),[10题](),[53题](),[72题](),从自顶向下再向顶改为了自底向上。 - -一部分递归算法,只是可以用栈去模仿递归的过程,对于时间或空间的复杂度没有任何好处,比如这道题,唯一好处可能就是能让我们更清楚的了解递归的过程吧。 - -自己之前对于这种完全模仿递归思路写成迭代,一直也没写过,今天也就试试吧。 - -思路的话,我们本质上就是在模拟递归,递归其实就是压栈出栈的过程,我们需要用一个栈去把递归的参数存起来。这里的话,就是函数的参数 `start`,`end`,以及内部定义的 `root`。为了方便,我们就定义一个类。 - -```java -class MyTreeNode { - TreeNode root; - int start; - int end - MyTreeNode(TreeNode r, int s, int e) { - this.root = r; - this.start = s; - this.end = e; - } -} -``` - -第一步,我们把根节点存起来。 - -```java -Stack rootStack = new Stack<>(); -int start = 0; -int end = nums.length; -int mid = (start + end) >>> 1; -TreeNode root = new TreeNode(nums[mid]); -TreeNode curRoot = root; -rootStack.push(new MyTreeNode(root, start, end)); -``` - -然后开始递归的过程,就是不停的生成左子树。因为要生成左子树,`end - start` 表示当前树的可用数字的个数,因为根节点已经用去 1 个了,所以为了生成左子树,个数肯定需要大于 1。 - -```java -while (end - start > 1) { - mid = (start + end) >>> 1; //当前根节点 - end = mid;//左子树的结尾 - mid = (start + end) >>> 1;//左子树的中点 - curRoot.left = new TreeNode(nums[mid]); - curRoot = curRoot.left; - rootStack.push(new MyTreeNode(curRoot, start, end)); -} -``` - -在递归中,返回 `null` 以后,开始生成右子树。这里的话,当 `end - start <= 1` ,也就是无法生成左子树了,我们就可以出栈,来生成右子树。 - -```java -MyTreeNode myNode = rootStack.pop(); -//当前作为根节点的 start end 以及 mid -start = myNode.start; -end = myNode.end; -mid = (start + end) >>> 1; -start = mid + 1; //右子树的 start -curRoot = myNode.root; //当前根节点 -if (start < end) { //判断当前范围内是否有数 - mid = (start + end) >>> 1; //右子树的 mid - curRoot.right = new TreeNode(nums[mid]); - curRoot = curRoot.right; - rootStack.push(new MyTreeNode(curRoot, start, end)); -} -``` - -然后把上边几块内容组合起来就可以了。 - -```java -class MyTreeNode { - TreeNode root; - int start; - int end; - - MyTreeNode(TreeNode r, int s, int e) { - this.root = r; - this.start = s; - this.end = e; - } -} -public TreeNode sortedArrayToBST(int[] nums) { - if (nums.length == 0) { - return null; - } - Stack rootStack = new Stack<>(); - int start = 0; - int end = nums.length; - int mid = (start + end) >>> 1; - TreeNode root = new TreeNode(nums[mid]); - TreeNode curRoot = root; - rootStack.push(new MyTreeNode(root, start, end)); - while (end - start > 1 || !rootStack.isEmpty()) { - //考虑左子树 - while (end - start > 1) { - mid = (start + end) >>> 1; //当前根节点 - end = mid;//左子树的结尾 - mid = (start + end) >>> 1;//左子树的中点 - curRoot.left = new TreeNode(nums[mid]); - curRoot = curRoot.left; - rootStack.push(new MyTreeNode(curRoot, start, end)); - } - //出栈考虑右子树 - MyTreeNode myNode = rootStack.pop(); - //当前作为根节点的 start end 以及 mid - start = myNode.start; - end = myNode.end; - mid = (start + end) >>> 1; - start = mid + 1; //右子树的 start - curRoot = myNode.root; //当前根节点 - if (start < end) { //判断当前范围内是否有数 - mid = (start + end) >>> 1; //右子树的 mid - curRoot.right = new TreeNode(nums[mid]); - curRoot = curRoot.right; - rootStack.push(new MyTreeNode(curRoot, start, end)); - } - - } - - return root; -} -``` - -# 解法三 队列 BFS - -参考 [这里]()。 和递归的思路基本一样,不停的划分范围。 - -```java -class MyTreeNode { - TreeNode root; - int start; - int end; - - MyTreeNode(TreeNode r, int s, int e) { - this.root = r; - this.start = s; - this.end = e; - } -} -public TreeNode sortedArrayToBST3(int[] nums) { - if (nums.length == 0) { - return null; - } - Queue rootQueue = new LinkedList<>(); - TreeNode root = new TreeNode(0); - rootQueue.offer(new MyTreeNode(root, 0, nums.length)); - while (!rootQueue.isEmpty()) { - MyTreeNode myRoot = rootQueue.poll(); - int start = myRoot.start; - int end = myRoot.end; - int mid = (start + end) >>> 1; - TreeNode curRoot = myRoot.root; - curRoot.val = nums[mid]; - if (start < mid) { - curRoot.left = new TreeNode(0); - rootQueue.offer(new MyTreeNode(curRoot.left, start, mid)); - } - if (mid + 1 < end) { - curRoot.right = new TreeNode(0); - rootQueue.offer(new MyTreeNode(curRoot.right, mid + 1, end)); - } - } - - return root; -} -``` - -最巧妙的地方是它先生成 `left` 和 `right` 但不进行赋值,只是把范围传过去,然后出队的时候再进行赋值。这样最开始的根节点也无需单独考虑了。 - -# 扩展 求中点 - -前几天和同学发现个有趣的事情,分享一下。 - -首先假设我们的变量都是 `int` 值。 - -二分查找中我们需要根据 `start` 和 `end` 求中点,正常情况下加起来除以 2 即可。 - -```java -int mid = (start + end) / 2 -``` - -但这样有一个缺点,我们知道`int`的最大值是 `Integer.MAX_VALUE` ,也就是`2147483647`。那么有一个问题,如果 `start = 2147483645`,`end = 2147483645`,虽然 `start` 和 `end`都没有超出最大值,但是如果利用上边的公式,加起来的话就会造成溢出,从而导致`mid`计算错误。 - -解决的一个方案就是利用数学上的技巧,我们可以加一个 `start` 再减一个 `start` 将公式变形。 - -```java -(start + end) / 2 = (start + end + start - start) / 2 = start + (end - start) / 2 -``` - -这样的话,就解决了上边的问题。 - -然后当时和同学看到`jdk`源码中,求`mid`的方法如下 - -```java -int mid = (start + end) >>> 1 -``` - -它通过移位实现了除以 2,但。。。这样难道不会导致溢出吗? - -首先大家可以补一下 [补码](https://mp.weixin.qq.com/s/uvcQHJi6AXhPDJL-6JWUkw) 的知识。 - -其实问题的关键就是这里了`>>>` ,我们知道还有一种右移是`>>`。区别在于`>>`为有符号右移,右移以后最高位保持原来的最高位。而 `>>>` 这个右移的话最高位补 0。 - -所以这里其实利用到了整数的补码形式,最高位其实是符号位,所以当 `start + end` 溢出的时候,其实本质上只是符号位收到了进位,而`>>>`这个右移不仅可以把符号位右移,同时最高位只是补零,不会对数字的大小造成影响。 - -但`>>`有符号右移就会出现问题了,事实上 JDK6 之前都用的`>>`,这个 BUG 在 java 里竟然隐藏了十年之久。 - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/108.jpg) + +给一个升序数组,生成一个平衡二叉搜索树。平衡二叉树定义如下: + +> 它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。 + +二叉搜索树定义如下: + +> 1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值; +> 2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值; +> 3. 任意节点的左、右子树也分别为二叉查找树; +> 4. 没有键值相等的节点。 + +# 解法一 递归 + +如果做了 [98 题]() 和 [99 题](),那么又看到这里的升序数组,然后应该会想到一个点,二叉搜索树的中序遍历刚好可以输出一个升序数组。 + +所以题目给出的升序数组就是二叉搜索树的中序遍历。 + +根据中序遍历还原一颗树,又想到了 [105 题]() 和 [106 题](),通过中序遍历加前序遍历或者中序遍历加后序遍历来还原一棵树。前序(后序)遍历的作用呢?提供根节点!然后根据根节点,就可以递归的生成左右子树。 + +这里的话怎么知道根节点呢?平衡二叉树,既然要做到平衡,我们只要把根节点选为数组的中点即可。 + +综上,和之前一样,找到了根节点,然后把数组一分为二,进入递归即可。注意这里的边界情况,包括左边界,不包括右边界。 + +```java +public TreeNode sortedArrayToBST(int[] nums) { + return sortedArrayToBST(nums, 0, nums.length); +} + +private TreeNode sortedArrayToBST(int[] nums, int start, int end) { + if (start == end) { + return null; + } + int mid = (start + end) >>> 1; + TreeNode root = new TreeNode(nums[mid]); + root.left = sortedArrayToBST(nums, start, mid); + root.right = sortedArrayToBST(nums, mid + 1, end); + + return root; +} +``` + +# 解法二 栈 DFS + +递归都可以转为迭代的形式。 + +一部分递归算法,可以转成动态规划,实现空间换时间,例如 [5题](),[10题](),[53题](),[72题](),从自顶向下再向顶改为了自底向上。 + +一部分递归算法,只是可以用栈去模仿递归的过程,对于时间或空间的复杂度没有任何好处,比如这道题,唯一好处可能就是能让我们更清楚的了解递归的过程吧。 + +自己之前对于这种完全模仿递归思路写成迭代,一直也没写过,今天也就试试吧。 + +思路的话,我们本质上就是在模拟递归,递归其实就是压栈出栈的过程,我们需要用一个栈去把递归的参数存起来。这里的话,就是函数的参数 `start`,`end`,以及内部定义的 `root`。为了方便,我们就定义一个类。 + +```java +class MyTreeNode { + TreeNode root; + int start; + int end + MyTreeNode(TreeNode r, int s, int e) { + this.root = r; + this.start = s; + this.end = e; + } +} +``` + +第一步,我们把根节点存起来。 + +```java +Stack rootStack = new Stack<>(); +int start = 0; +int end = nums.length; +int mid = (start + end) >>> 1; +TreeNode root = new TreeNode(nums[mid]); +TreeNode curRoot = root; +rootStack.push(new MyTreeNode(root, start, end)); +``` + +然后开始递归的过程,就是不停的生成左子树。因为要生成左子树,`end - start` 表示当前树的可用数字的个数,因为根节点已经用去 1 个了,所以为了生成左子树,个数肯定需要大于 1。 + +```java +while (end - start > 1) { + mid = (start + end) >>> 1; //当前根节点 + end = mid;//左子树的结尾 + mid = (start + end) >>> 1;//左子树的中点 + curRoot.left = new TreeNode(nums[mid]); + curRoot = curRoot.left; + rootStack.push(new MyTreeNode(curRoot, start, end)); +} +``` + +在递归中,返回 `null` 以后,开始生成右子树。这里的话,当 `end - start <= 1` ,也就是无法生成左子树了,我们就可以出栈,来生成右子树。 + +```java +MyTreeNode myNode = rootStack.pop(); +//当前作为根节点的 start end 以及 mid +start = myNode.start; +end = myNode.end; +mid = (start + end) >>> 1; +start = mid + 1; //右子树的 start +curRoot = myNode.root; //当前根节点 +if (start < end) { //判断当前范围内是否有数 + mid = (start + end) >>> 1; //右子树的 mid + curRoot.right = new TreeNode(nums[mid]); + curRoot = curRoot.right; + rootStack.push(new MyTreeNode(curRoot, start, end)); +} +``` + +然后把上边几块内容组合起来就可以了。 + +```java +class MyTreeNode { + TreeNode root; + int start; + int end; + + MyTreeNode(TreeNode r, int s, int e) { + this.root = r; + this.start = s; + this.end = e; + } +} +public TreeNode sortedArrayToBST(int[] nums) { + if (nums.length == 0) { + return null; + } + Stack rootStack = new Stack<>(); + int start = 0; + int end = nums.length; + int mid = (start + end) >>> 1; + TreeNode root = new TreeNode(nums[mid]); + TreeNode curRoot = root; + rootStack.push(new MyTreeNode(root, start, end)); + while (end - start > 1 || !rootStack.isEmpty()) { + //考虑左子树 + while (end - start > 1) { + mid = (start + end) >>> 1; //当前根节点 + end = mid;//左子树的结尾 + mid = (start + end) >>> 1;//左子树的中点 + curRoot.left = new TreeNode(nums[mid]); + curRoot = curRoot.left; + rootStack.push(new MyTreeNode(curRoot, start, end)); + } + //出栈考虑右子树 + MyTreeNode myNode = rootStack.pop(); + //当前作为根节点的 start end 以及 mid + start = myNode.start; + end = myNode.end; + mid = (start + end) >>> 1; + start = mid + 1; //右子树的 start + curRoot = myNode.root; //当前根节点 + if (start < end) { //判断当前范围内是否有数 + mid = (start + end) >>> 1; //右子树的 mid + curRoot.right = new TreeNode(nums[mid]); + curRoot = curRoot.right; + rootStack.push(new MyTreeNode(curRoot, start, end)); + } + + } + + return root; +} +``` + +# 解法三 队列 BFS + +参考 [这里]()。 和递归的思路基本一样,不停的划分范围。 + +```java +class MyTreeNode { + TreeNode root; + int start; + int end; + + MyTreeNode(TreeNode r, int s, int e) { + this.root = r; + this.start = s; + this.end = e; + } +} +public TreeNode sortedArrayToBST3(int[] nums) { + if (nums.length == 0) { + return null; + } + Queue rootQueue = new LinkedList<>(); + TreeNode root = new TreeNode(0); + rootQueue.offer(new MyTreeNode(root, 0, nums.length)); + while (!rootQueue.isEmpty()) { + MyTreeNode myRoot = rootQueue.poll(); + int start = myRoot.start; + int end = myRoot.end; + int mid = (start + end) >>> 1; + TreeNode curRoot = myRoot.root; + curRoot.val = nums[mid]; + if (start < mid) { + curRoot.left = new TreeNode(0); + rootQueue.offer(new MyTreeNode(curRoot.left, start, mid)); + } + if (mid + 1 < end) { + curRoot.right = new TreeNode(0); + rootQueue.offer(new MyTreeNode(curRoot.right, mid + 1, end)); + } + } + + return root; +} +``` + +最巧妙的地方是它先生成 `left` 和 `right` 但不进行赋值,只是把范围传过去,然后出队的时候再进行赋值。这样最开始的根节点也无需单独考虑了。 + +# 扩展 求中点 + +前几天和同学发现个有趣的事情,分享一下。 + +首先假设我们的变量都是 `int` 值。 + +二分查找中我们需要根据 `start` 和 `end` 求中点,正常情况下加起来除以 2 即可。 + +```java +int mid = (start + end) / 2 +``` + +但这样有一个缺点,我们知道`int`的最大值是 `Integer.MAX_VALUE` ,也就是`2147483647`。那么有一个问题,如果 `start = 2147483645`,`end = 2147483645`,虽然 `start` 和 `end`都没有超出最大值,但是如果利用上边的公式,加起来的话就会造成溢出,从而导致`mid`计算错误。 + +解决的一个方案就是利用数学上的技巧,我们可以加一个 `start` 再减一个 `start` 将公式变形。 + +```java +(start + end) / 2 = (start + end + start - start) / 2 = start + (end - start) / 2 +``` + +这样的话,就解决了上边的问题。 + +然后当时和同学看到`jdk`源码中,求`mid`的方法如下 + +```java +int mid = (start + end) >>> 1 +``` + +它通过移位实现了除以 2,但。。。这样难道不会导致溢出吗? + +首先大家可以补一下 [补码](https://mp.weixin.qq.com/s/uvcQHJi6AXhPDJL-6JWUkw) 的知识。 + +其实问题的关键就是这里了`>>>` ,我们知道还有一种右移是`>>`。区别在于`>>`为有符号右移,右移以后最高位保持原来的最高位。而 `>>>` 这个右移的话最高位补 0。 + +所以这里其实利用到了整数的补码形式,最高位其实是符号位,所以当 `start + end` 溢出的时候,其实本质上只是符号位收到了进位,而`>>>`这个右移不仅可以把符号位右移,同时最高位只是补零,不会对数字的大小造成影响。 + +但`>>`有符号右移就会出现问题了,事实上 JDK6 之前都用的`>>`,这个 BUG 在 java 里竟然隐藏了十年之久。 + +# 总 + 经过这么多的分析,大家估计体会到了递归的魅力了吧,简洁而优雅。另外的两种迭代的实现,可以让我们更清楚的了解递归到底发生了什么。关于求中点,大家以后就用`>>>`吧,比`start + (end - start) / 2`简洁不少,还能给别人科普一下补码的知识。 \ No newline at end of file diff --git a/leetcode-109-Convert-Sorted-List-to-Binary-Search-Tree.md b/leetcode-109-Convert-Sorted-List-to-Binary-Search-Tree.md index 8102db96e..2495eadd0 100644 --- a/leetcode-109-Convert-Sorted-List-to-Binary-Search-Tree.md +++ b/leetcode-109-Convert-Sorted-List-to-Binary-Search-Tree.md @@ -1,150 +1,150 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/109.jpg) - -和 [108 题]() 是一样的,都是给定一个升序序列,然后生成二分平衡查找树。区别在于 108 题给定的是数组,这里给的是链表。 - -# 解法一 - -大家先看一下 [108 题]() 吧,算法的关键是取到中间的数据做为根节点。而这里链表的话,由于不支持随机访问,所以会麻烦些。最简单的思路就是我们把链表先用线性表存起来,然后题目就转换成 108 题了。 - -为了方便,把上一道题的数组参数改为`List` 。 - -```java -public TreeNode sortedListToBST(ListNode head) { - ArrayList nums = new ArrayList<>(); - while (head != null) { - nums.add(head.val); - head = head.next; - } - return sortedArrayToBST(nums); -} - -public TreeNode sortedArrayToBST(ArrayList nums) { - return sortedArrayToBST(nums, 0, nums.size()); -} - -private TreeNode sortedArrayToBST(ArrayList nums, int start, int end) { - if (start == end) { - return null; - } - int mid = (start + end) >>> 1; - TreeNode root = new TreeNode(nums.get(mid)); - root.left = sortedArrayToBST(nums, start, mid); - root.right = sortedArrayToBST(nums, mid + 1, end); - return root; -} -``` - -时间复杂度:`O(n)`。 - -空间复杂度:数组进行辅助,`O(n)`。 - -# 解法二 - -参考 [这里]()。 - -有没有一种方案,不用数组的辅助呢?那么我们需要解决怎么得到 mid 的值的问题。 - -最直接的思路就是根据 start 和 end,求出 mid,然后从 head 遍历 mid - start 次,就到达了 mid 值。但最开始的 end,我们还得遍历一遍链表才能得到,总体来说就是太复杂了。 - -这里有一个求中点节点值的技巧,利用快慢指针。 - -快指针和慢指针同时从头部开始遍历,快指针每次走两步,慢指针每次走一步,当快指针走到链表尾部,此时慢指针就指向了中间位置。 - -除了求中点节点的值不一样,基本架构和 [108 题]() 是一样的。 - -```java -public TreeNode sortedListToBST(ListNode head) { - return sortedArrayToBST(head, null); -} - -private TreeNode sortedArrayToBST(ListNode head, ListNode tail) { - if (head == tail) { - return null; - } - ListNode fast = head; - ListNode slow = head; - while (fast != tail && fast.next != tail) { - slow = slow.next; - fast = fast.next.next; - } - - TreeNode root = new TreeNode(slow.val); - root.left = sortedArrayToBST(head, slow); - root.right = sortedArrayToBST(slow.next, tail); - return root; -} -``` - -时间复杂度:根据递归式可知,`T(n) = 2 * T(n / 2 ) + n`,`O(nlog(n))`。 - -空间复杂度:`O(log(n))`。 - -# 解法三 - -解法二虽然没有借助数组,优化了空间复杂度,但是时间复杂度增加了,那么有没有一种两全其美的方法,时间复杂度是解法一,空间复杂度是解法二。还真有,参考 [这里]()。 - -主要思想是,因为我们知道题目给定的升序数组,其实就是二叉搜索树的中序遍历。那么我们完全可以按照这个顺序去为每个节点赋值。 - -实现的话,我们套用中序遍历的递归过程,并且将 `start` 和 `end` 作为递归参数,当 `start == end` 的时候,就返回 `null`。 - -先回想一下中序遍历的算法。 - -```java -public List inorderTraversal(TreeNode root) { - List ans = new ArrayList<>(); - getAns(root, ans); - return ans; -} - -private void getAns(TreeNode node, List ans) { - if (node == null) { - return; - } - getAns(node.left, ans); - ans.add(node.val); - getAns(node.right, ans); -} -``` - -之前是将 `node.val` 进行保存,这里的话我们是给当前节点进行赋值,为了依次赋值,我们需要一个`cur`指针指向给定的数列,每赋一个值就进行后移。 - -```java -ListNode cur = null; - -public TreeNode sortedListToBST(ListNode head) { - cur = head; - int end = 0; - while (head != null) { - end++; - head = head.next; - } - return sortedArrayToBSTHelper(0, end); -} - -private TreeNode sortedArrayToBSTHelper(int start, int end) { - if (start == end) { - return null; - } - int mid = (start + end) >>> 1; - //遍历左子树并且将根节点返回 - TreeNode left = sortedArrayToBSTHelper(start, mid); - //遍历当前根节点并进行赋值 - TreeNode root = new TreeNode(cur.val); - root.left = left; - cur = cur.next; //指针后移,进行下一次的赋值 - //遍历右子树并且将根节点返回 - TreeNode right = sortedArrayToBSTHelper(mid + 1, end); - root.right = right; - return root; -} -``` - -时间复杂度:`O(n)`,主要是得到开始的 end,需要遍历一遍链表。 - -空间复杂度:`O(log(n))`,递归压栈的消耗。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/109.jpg) + +和 [108 题]() 是一样的,都是给定一个升序序列,然后生成二分平衡查找树。区别在于 108 题给定的是数组,这里给的是链表。 + +# 解法一 + +大家先看一下 [108 题]() 吧,算法的关键是取到中间的数据做为根节点。而这里链表的话,由于不支持随机访问,所以会麻烦些。最简单的思路就是我们把链表先用线性表存起来,然后题目就转换成 108 题了。 + +为了方便,把上一道题的数组参数改为`List` 。 + +```java +public TreeNode sortedListToBST(ListNode head) { + ArrayList nums = new ArrayList<>(); + while (head != null) { + nums.add(head.val); + head = head.next; + } + return sortedArrayToBST(nums); +} + +public TreeNode sortedArrayToBST(ArrayList nums) { + return sortedArrayToBST(nums, 0, nums.size()); +} + +private TreeNode sortedArrayToBST(ArrayList nums, int start, int end) { + if (start == end) { + return null; + } + int mid = (start + end) >>> 1; + TreeNode root = new TreeNode(nums.get(mid)); + root.left = sortedArrayToBST(nums, start, mid); + root.right = sortedArrayToBST(nums, mid + 1, end); + return root; +} +``` + +时间复杂度:`O(n)`。 + +空间复杂度:数组进行辅助,`O(n)`。 + +# 解法二 + +参考 [这里]()。 + +有没有一种方案,不用数组的辅助呢?那么我们需要解决怎么得到 mid 的值的问题。 + +最直接的思路就是根据 start 和 end,求出 mid,然后从 head 遍历 mid - start 次,就到达了 mid 值。但最开始的 end,我们还得遍历一遍链表才能得到,总体来说就是太复杂了。 + +这里有一个求中点节点值的技巧,利用快慢指针。 + +快指针和慢指针同时从头部开始遍历,快指针每次走两步,慢指针每次走一步,当快指针走到链表尾部,此时慢指针就指向了中间位置。 + +除了求中点节点的值不一样,基本架构和 [108 题]() 是一样的。 + +```java +public TreeNode sortedListToBST(ListNode head) { + return sortedArrayToBST(head, null); +} + +private TreeNode sortedArrayToBST(ListNode head, ListNode tail) { + if (head == tail) { + return null; + } + ListNode fast = head; + ListNode slow = head; + while (fast != tail && fast.next != tail) { + slow = slow.next; + fast = fast.next.next; + } + + TreeNode root = new TreeNode(slow.val); + root.left = sortedArrayToBST(head, slow); + root.right = sortedArrayToBST(slow.next, tail); + return root; +} +``` + +时间复杂度:根据递归式可知,`T(n) = 2 * T(n / 2 ) + n`,`O(nlog(n))`。 + +空间复杂度:`O(log(n))`。 + +# 解法三 + +解法二虽然没有借助数组,优化了空间复杂度,但是时间复杂度增加了,那么有没有一种两全其美的方法,时间复杂度是解法一,空间复杂度是解法二。还真有,参考 [这里]()。 + +主要思想是,因为我们知道题目给定的升序数组,其实就是二叉搜索树的中序遍历。那么我们完全可以按照这个顺序去为每个节点赋值。 + +实现的话,我们套用中序遍历的递归过程,并且将 `start` 和 `end` 作为递归参数,当 `start == end` 的时候,就返回 `null`。 + +先回想一下中序遍历的算法。 + +```java +public List inorderTraversal(TreeNode root) { + List ans = new ArrayList<>(); + getAns(root, ans); + return ans; +} + +private void getAns(TreeNode node, List ans) { + if (node == null) { + return; + } + getAns(node.left, ans); + ans.add(node.val); + getAns(node.right, ans); +} +``` + +之前是将 `node.val` 进行保存,这里的话我们是给当前节点进行赋值,为了依次赋值,我们需要一个`cur`指针指向给定的数列,每赋一个值就进行后移。 + +```java +ListNode cur = null; + +public TreeNode sortedListToBST(ListNode head) { + cur = head; + int end = 0; + while (head != null) { + end++; + head = head.next; + } + return sortedArrayToBSTHelper(0, end); +} + +private TreeNode sortedArrayToBSTHelper(int start, int end) { + if (start == end) { + return null; + } + int mid = (start + end) >>> 1; + //遍历左子树并且将根节点返回 + TreeNode left = sortedArrayToBSTHelper(start, mid); + //遍历当前根节点并进行赋值 + TreeNode root = new TreeNode(cur.val); + root.left = left; + cur = cur.next; //指针后移,进行下一次的赋值 + //遍历右子树并且将根节点返回 + TreeNode right = sortedArrayToBSTHelper(mid + 1, end); + root.right = right; + return root; +} +``` + +时间复杂度:`O(n)`,主要是得到开始的 end,需要遍历一遍链表。 + +空间复杂度:`O(log(n))`,递归压栈的消耗。 + +# 总 + 快慢指针求链表的中间值,这个技巧不错。此外,解法三的模仿中序遍历的过程,然后把给定的数组依次赋值过去,太强了。 \ No newline at end of file diff --git a/leetcode-110-Balanced-Binary-Tree.md b/leetcode-110-Balanced-Binary-Tree.md index 0b3d265ab..51697d013 100644 --- a/leetcode-110-Balanced-Binary-Tree.md +++ b/leetcode-110-Balanced-Binary-Tree.md @@ -1,141 +1,141 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/110.jpg) - -判断一棵树是否是平衡二叉树,平衡二叉树定义如下: - -> 它是一棵空树或它的左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树。 - -# 解法一 - -直接按照定义来吧,并且多定义一个求高度的函数,之前在 [104 题]() 做过。 - -```java -public boolean isBalanced(TreeNode root) { - //它是一棵空树 - if (root == null) { - return true; - } - //它的左右两个子树的高度差的绝对值不超过1 - int leftDepth = getTreeDepth(root.left); - int rightDepth = getTreeDepth(root.right); - if (Math.abs(leftDepth - rightDepth) > 1) { - return false; - } - //左右两个子树都是一棵平衡二叉树 - return isBalanced(root.left) && isBalanced(root.right); - -} - -private int getTreeDepth(TreeNode root) { - if (root == null) { - return 0; - } - int leftDepth = getTreeDepth(root.left); - int rightDepth = getTreeDepth(root.right); - return Math.max(leftDepth, rightDepth) + 1; -} -``` - -# 解法二 - -大家觉不觉得解法一怪怪的,有一种少了些什么的感觉,自己写之前就有这种感觉,写完以后仔细分析了一下。 - -当我们求左子树的高度时,同样是利用了递归去求它的左子树的高度和右子树的高度。 - -当代码执行到 - -```java -isBalanced(root.left) && isBalanced(root.right) -``` - -递归的判断左子树和右子树是否是平衡二叉树的时候,我们又会继续求高度,求高度再次进入 `getTreeDepth` 函数的时候,我们会发现,其实在上一次这些高度都已经求过了。 - -第二个不好的地方在于, `getTreeDepth` 递归的求高度的时候,也是求了左子树的高度,右子树的高度,此时完全可以判断当前树是否是平衡二叉树了,而不是再继续求高度。 - -综上,我们其实只需要求一次高度,并且在求左子树和右子树的高度的同时,判断一下当前是否是平衡二叉树。 - -考虑到 `getTreeDepth` 函数返回的是`int`值,同时高度不可能为负数,那么如果求高度过程中我们发现了当前不是平衡二叉树,就返回`-1`。 - -```java -private int getTreeDepth(TreeNode root) { - if (root == null) { - return 0; - } - int leftDepth = getTreeDepth(root.left); - int rightDepth = getTreeDepth(root.right); - if (Math.abs(leftDepth - rightDepth) > 1) { - return -1; - } - return Math.max(leftDepth, rightDepth) + 1; -} -``` - -上边的代码还是有问题的, - -```java -int leftDepth = getTreeDepth(root.left); -int rightDepth = getTreeDepth(root.right); -``` - -如果左右子树都不是平衡二叉树,此时都返回了`-1`,那么再执行下边的代码。 - -```java -if (Math.abs(leftDepth - rightDepth) > 1) { - return -1; -} -``` - -它们的差会是 0,不会进入`if`中,但是本来应该进入 `if` 返回 `-1` 的。 - -所以当发现 `leftDepth`返回 `-1` 的时候,我们需要提前返回 `-1`。`rightDepth`也会有同样的问题,所以也需要提前返回 `-1`。 - -```java -private int getTreeDepth(TreeNode root) { - if (root == null) { - return 0; - } - int leftDepth = getTreeDepth(root.left); - if (leftDepth == -1) { - return -1; - } - int rightDepth = getTreeDepth(root.right); - if (rightDepth == -1) { - return -1; - } - if (Math.abs(leftDepth - rightDepth) > 1) { - return -1; - } - return Math.max(leftDepth, rightDepth) + 1; -} -``` - -对于我们要写的 `isBalanced`函数,修改的话就简单了,只需要调用一次 `getTreeDepth`函数,然后判断返回值是不是`-1`就可以了。 - -```java -public boolean isBalanced(TreeNode root) { - return getTreeDepth(root) != -1; -} - -private int getTreeDepth(TreeNode root) { - if (root == null) { - return 0; - } - int leftDepth = getTreeDepth(root.left); - if (leftDepth == -1) { - return -1; - } - int rightDepth = getTreeDepth(root.right); - if (rightDepth == -1) { - return -1; - } - if (Math.abs(leftDepth - rightDepth) > 1) { - return -1; - } - return Math.max(leftDepth, rightDepth) + 1; -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/110.jpg) + +判断一棵树是否是平衡二叉树,平衡二叉树定义如下: + +> 它是一棵空树或它的左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树。 + +# 解法一 + +直接按照定义来吧,并且多定义一个求高度的函数,之前在 [104 题]() 做过。 + +```java +public boolean isBalanced(TreeNode root) { + //它是一棵空树 + if (root == null) { + return true; + } + //它的左右两个子树的高度差的绝对值不超过1 + int leftDepth = getTreeDepth(root.left); + int rightDepth = getTreeDepth(root.right); + if (Math.abs(leftDepth - rightDepth) > 1) { + return false; + } + //左右两个子树都是一棵平衡二叉树 + return isBalanced(root.left) && isBalanced(root.right); + +} + +private int getTreeDepth(TreeNode root) { + if (root == null) { + return 0; + } + int leftDepth = getTreeDepth(root.left); + int rightDepth = getTreeDepth(root.right); + return Math.max(leftDepth, rightDepth) + 1; +} +``` + +# 解法二 + +大家觉不觉得解法一怪怪的,有一种少了些什么的感觉,自己写之前就有这种感觉,写完以后仔细分析了一下。 + +当我们求左子树的高度时,同样是利用了递归去求它的左子树的高度和右子树的高度。 + +当代码执行到 + +```java +isBalanced(root.left) && isBalanced(root.right) +``` + +递归的判断左子树和右子树是否是平衡二叉树的时候,我们又会继续求高度,求高度再次进入 `getTreeDepth` 函数的时候,我们会发现,其实在上一次这些高度都已经求过了。 + +第二个不好的地方在于, `getTreeDepth` 递归的求高度的时候,也是求了左子树的高度,右子树的高度,此时完全可以判断当前树是否是平衡二叉树了,而不是再继续求高度。 + +综上,我们其实只需要求一次高度,并且在求左子树和右子树的高度的同时,判断一下当前是否是平衡二叉树。 + +考虑到 `getTreeDepth` 函数返回的是`int`值,同时高度不可能为负数,那么如果求高度过程中我们发现了当前不是平衡二叉树,就返回`-1`。 + +```java +private int getTreeDepth(TreeNode root) { + if (root == null) { + return 0; + } + int leftDepth = getTreeDepth(root.left); + int rightDepth = getTreeDepth(root.right); + if (Math.abs(leftDepth - rightDepth) > 1) { + return -1; + } + return Math.max(leftDepth, rightDepth) + 1; +} +``` + +上边的代码还是有问题的, + +```java +int leftDepth = getTreeDepth(root.left); +int rightDepth = getTreeDepth(root.right); +``` + +如果左右子树都不是平衡二叉树,此时都返回了`-1`,那么再执行下边的代码。 + +```java +if (Math.abs(leftDepth - rightDepth) > 1) { + return -1; +} +``` + +它们的差会是 0,不会进入`if`中,但是本来应该进入 `if` 返回 `-1` 的。 + +所以当发现 `leftDepth`返回 `-1` 的时候,我们需要提前返回 `-1`。`rightDepth`也会有同样的问题,所以也需要提前返回 `-1`。 + +```java +private int getTreeDepth(TreeNode root) { + if (root == null) { + return 0; + } + int leftDepth = getTreeDepth(root.left); + if (leftDepth == -1) { + return -1; + } + int rightDepth = getTreeDepth(root.right); + if (rightDepth == -1) { + return -1; + } + if (Math.abs(leftDepth - rightDepth) > 1) { + return -1; + } + return Math.max(leftDepth, rightDepth) + 1; +} +``` + +对于我们要写的 `isBalanced`函数,修改的话就简单了,只需要调用一次 `getTreeDepth`函数,然后判断返回值是不是`-1`就可以了。 + +```java +public boolean isBalanced(TreeNode root) { + return getTreeDepth(root) != -1; +} + +private int getTreeDepth(TreeNode root) { + if (root == null) { + return 0; + } + int leftDepth = getTreeDepth(root.left); + if (leftDepth == -1) { + return -1; + } + int rightDepth = getTreeDepth(root.right); + if (rightDepth == -1) { + return -1; + } + if (Math.abs(leftDepth - rightDepth) > 1) { + return -1; + } + return Math.max(leftDepth, rightDepth) + 1; +} +``` + +# 总 + 还是比较简单的,有时候可能一下子想不到最优的思路,所以可以先把常规的想法先写出来以便理清思路,然后尝试着去优化。 \ No newline at end of file diff --git a/leetcode-111-Minimum-Depth-of-Binary-Tree.md b/leetcode-111-Minimum-Depth-of-Binary-Tree.md index 2822fdfb4..1830043f3 100644 --- a/leetcode-111-Minimum-Depth-of-Binary-Tree.md +++ b/leetcode-111-Minimum-Depth-of-Binary-Tree.md @@ -1,196 +1,196 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/111.jpg) - -返回从根节点到叶子节点最小深度。 - -# 解法一 递归 - -和 [104 题]() 有些像,当时是返回根节点到叶子节点的最大深度。记得当时的代码很简单。 - -```java -public int maxDepth(TreeNode root) { - if (root == null) { - return 0; - } - return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1; -} -``` - -这道题是不是只要把`Math.max`,改成`Math.min`就够了。 - -```java -public int minDepth(TreeNode root) { - if (root == null) { - return 0; - } - return Math.min(minDepth(root.left), minDepth(root.right)) + 1; -} -``` - -粗略的想一下,似乎很完美,比如题目给的例子 - -```java - 3 - / \ - 9 20 - / \ - 15 7 -``` - -根据代码走一遍,`root.left `返回 `1`,`root.right`返回 `2`,选较小的`1`,加上 `1` 返回结果`2`,完美符合结果。 - -但如果是下边的样子呢? - -```java - 3 - / \ - 9 20 - / / \ - 8 15 7 -``` - -区别在于有一个子树的拥有一个孩子,另一个孩子为空。 - -这样利用上边的算法,当考虑`9`这个子树的时候,左孩子会返回`1`,由于它的右孩子为`null`,右孩子会返回`0`,选较小的`0`,加上 `1` 返回结果`1`给上一层。 - -也就是最顶层的`root.left `依旧得到了 `1`,但明显是不对的,对于左子树,应该是从 `9` 到 `8`,深度应该是 `2`。 - -所以代码上需要修正这个算法,再想想题目要求是从根节点到叶节点,所以如果有一个子树的左孩子或者右孩子为`null`了,那就意味着这个方向不可能到达叶子节点了,所以就不要再用`Min`函数去判断了。 - -我对代码的修正如下: - -```java -public int minDepth(TreeNode root) { - if (root == null) { - return 0; - } - return minDepthHelper(root); - -} - -private int minDepthHelper(TreeNode root) { - //到达叶子节点就返回 1 - if (root.left == null && root.right == null) { - return 1; - } - //左孩子为空,只考虑右孩子的方向 - if (root.left == null) { - return minDepthHelper(root.right) + 1; - } - //右孩子为空,只考虑左孩子的方向 - if (root.right == null) { - return minDepthHelper(root.left) + 1; - } - //既有左孩子又有右孩子,那么就选一个较小的 - return Math.min(minDepthHelper(root.left), minDepthHelper(root.right)) + 1; -} -``` - -其实也是可以把两个函数合在一起的,参考[这里]()。 - -```java -public int minDepth(TreeNode root) { - if (root == null){ - return 0; - } - // 左孩子为空,只考虑右孩子的方向 - if (root.left == null) { - return minDepth(root.right) + 1; - } - // 右孩子为空,只考虑左孩子的方向 - if (root.right == null) { - return minDepth(root.left) + 1; - } - return Math.min(minDepth(root.left),minDepth(root.right)) + 1; -} -``` - -此外,还有一个想法,觉得不错,大家可以看看,参考[这里]()。 - -```java -public int minDepth(TreeNode root) { - if (root == null) { - return 0; - } - if (root.left != null && root.right != null) { - return Math.min(minDepth(root.left), minDepth(root.right)) + 1; - } else { - return Math.max(minDepth(root.left), minDepth(root.right)) + 1; - } -} -``` - -当左孩子为空或者右孩子为空的时候,它就直接去选一个较大深度的,因为较小深度一定是为空的那个孩子,是我们不考虑的。 - -上边三个算法本质上其实是一样的,就是解决了一个孩子为空另一个不为空的问题,而对于[104 题]() 没有出现这个问题,是因为我们选的是`max`,所以不用在意是否有一个为空。 - -# 解法二 BFS - -[104 题]() 也提供了`BFS`的方案,利用一个队列进行层次遍历,用一个 `level` 变量保存当前的深度,代码如下: - -```java -public int maxDepth(TreeNode root) { - Queue queue = new LinkedList(); - if (root == null) - return 0; - queue.offer(root); - int level = 0; - while (!queue.isEmpty()) { - int levelNum = queue.size(); // 当前层元素的个数 - for (int i = 0; i < levelNum; i++) { - TreeNode curNode = queue.poll(); - if (curNode != null) { - if (curNode.left != null) { - queue.offer(curNode.left); - } - if (curNode.right != null) { - queue.offer(curNode.right); - } - } - } - level++; - } - return level; -} - -``` - -对于这道题就比较容易修改了,只要在 `for` 循环中判断当前是不是叶子节点,如果是的话,返回当前的 level 就可以了。此外要把`level`初始化改为`1`,因为如果只有一个根节点,它就是叶子节点,而在代码中,level 是在 `for`循环以后才`++`的,如果被提前结束的话,此时应该返回`1`。 - -```java -public int minDepth(TreeNode root) { - Queue queue = new LinkedList(); - if (root == null) - return 0; - queue.offer(root); - /**********修改的地方*****************/ - int level = 1; - /***********************************/ - while (!queue.isEmpty()) { - int levelNum = queue.size(); // 当前层元素的个数 - for (int i = 0; i < levelNum; i++) { - TreeNode curNode = queue.poll(); - if (curNode != null) { - /**********修改的地方*****************/ - if (curNode.left == null && curNode.right == null) { - return level; - } - /***********************************/ - if (curNode.left != null) { - queue.offer(curNode.left); - } - if (curNode.right != null) { - queue.offer(curNode.right); - } - } - } - level++; - } - return level; -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/111.jpg) + +返回从根节点到叶子节点最小深度。 + +# 解法一 递归 + +和 [104 题]() 有些像,当时是返回根节点到叶子节点的最大深度。记得当时的代码很简单。 + +```java +public int maxDepth(TreeNode root) { + if (root == null) { + return 0; + } + return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1; +} +``` + +这道题是不是只要把`Math.max`,改成`Math.min`就够了。 + +```java +public int minDepth(TreeNode root) { + if (root == null) { + return 0; + } + return Math.min(minDepth(root.left), minDepth(root.right)) + 1; +} +``` + +粗略的想一下,似乎很完美,比如题目给的例子 + +```java + 3 + / \ + 9 20 + / \ + 15 7 +``` + +根据代码走一遍,`root.left `返回 `1`,`root.right`返回 `2`,选较小的`1`,加上 `1` 返回结果`2`,完美符合结果。 + +但如果是下边的样子呢? + +```java + 3 + / \ + 9 20 + / / \ + 8 15 7 +``` + +区别在于有一个子树的拥有一个孩子,另一个孩子为空。 + +这样利用上边的算法,当考虑`9`这个子树的时候,左孩子会返回`1`,由于它的右孩子为`null`,右孩子会返回`0`,选较小的`0`,加上 `1` 返回结果`1`给上一层。 + +也就是最顶层的`root.left `依旧得到了 `1`,但明显是不对的,对于左子树,应该是从 `9` 到 `8`,深度应该是 `2`。 + +所以代码上需要修正这个算法,再想想题目要求是从根节点到叶节点,所以如果有一个子树的左孩子或者右孩子为`null`了,那就意味着这个方向不可能到达叶子节点了,所以就不要再用`Min`函数去判断了。 + +我对代码的修正如下: + +```java +public int minDepth(TreeNode root) { + if (root == null) { + return 0; + } + return minDepthHelper(root); + +} + +private int minDepthHelper(TreeNode root) { + //到达叶子节点就返回 1 + if (root.left == null && root.right == null) { + return 1; + } + //左孩子为空,只考虑右孩子的方向 + if (root.left == null) { + return minDepthHelper(root.right) + 1; + } + //右孩子为空,只考虑左孩子的方向 + if (root.right == null) { + return minDepthHelper(root.left) + 1; + } + //既有左孩子又有右孩子,那么就选一个较小的 + return Math.min(minDepthHelper(root.left), minDepthHelper(root.right)) + 1; +} +``` + +其实也是可以把两个函数合在一起的,参考[这里]()。 + +```java +public int minDepth(TreeNode root) { + if (root == null){ + return 0; + } + // 左孩子为空,只考虑右孩子的方向 + if (root.left == null) { + return minDepth(root.right) + 1; + } + // 右孩子为空,只考虑左孩子的方向 + if (root.right == null) { + return minDepth(root.left) + 1; + } + return Math.min(minDepth(root.left),minDepth(root.right)) + 1; +} +``` + +此外,还有一个想法,觉得不错,大家可以看看,参考[这里]()。 + +```java +public int minDepth(TreeNode root) { + if (root == null) { + return 0; + } + if (root.left != null && root.right != null) { + return Math.min(minDepth(root.left), minDepth(root.right)) + 1; + } else { + return Math.max(minDepth(root.left), minDepth(root.right)) + 1; + } +} +``` + +当左孩子为空或者右孩子为空的时候,它就直接去选一个较大深度的,因为较小深度一定是为空的那个孩子,是我们不考虑的。 + +上边三个算法本质上其实是一样的,就是解决了一个孩子为空另一个不为空的问题,而对于[104 题]() 没有出现这个问题,是因为我们选的是`max`,所以不用在意是否有一个为空。 + +# 解法二 BFS + +[104 题]() 也提供了`BFS`的方案,利用一个队列进行层次遍历,用一个 `level` 变量保存当前的深度,代码如下: + +```java +public int maxDepth(TreeNode root) { + Queue queue = new LinkedList(); + if (root == null) + return 0; + queue.offer(root); + int level = 0; + while (!queue.isEmpty()) { + int levelNum = queue.size(); // 当前层元素的个数 + for (int i = 0; i < levelNum; i++) { + TreeNode curNode = queue.poll(); + if (curNode != null) { + if (curNode.left != null) { + queue.offer(curNode.left); + } + if (curNode.right != null) { + queue.offer(curNode.right); + } + } + } + level++; + } + return level; +} + +``` + +对于这道题就比较容易修改了,只要在 `for` 循环中判断当前是不是叶子节点,如果是的话,返回当前的 level 就可以了。此外要把`level`初始化改为`1`,因为如果只有一个根节点,它就是叶子节点,而在代码中,level 是在 `for`循环以后才`++`的,如果被提前结束的话,此时应该返回`1`。 + +```java +public int minDepth(TreeNode root) { + Queue queue = new LinkedList(); + if (root == null) + return 0; + queue.offer(root); + /**********修改的地方*****************/ + int level = 1; + /***********************************/ + while (!queue.isEmpty()) { + int levelNum = queue.size(); // 当前层元素的个数 + for (int i = 0; i < levelNum; i++) { + TreeNode curNode = queue.poll(); + if (curNode != null) { + /**********修改的地方*****************/ + if (curNode.left == null && curNode.right == null) { + return level; + } + /***********************************/ + if (curNode.left != null) { + queue.offer(curNode.left); + } + if (curNode.right != null) { + queue.offer(curNode.right); + } + } + } + level++; + } + return level; +} +``` + +# 总 + 和 [104 题]() 题对比着考虑的话,只要找到这道题的不同之处,代码就很好写了。 \ No newline at end of file diff --git a/leetcode-112-Path-Sum.md b/leetcode-112-Path-Sum.md index dc4cf4a07..d6a70309b 100644 --- a/leetcode-112-Path-Sum.md +++ b/leetcode-112-Path-Sum.md @@ -1,253 +1,253 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/112.jpg) - -给定一个`sum`,判断是否有一条从根节点到叶子节点的路径,该路径上所有数字的和等于`sum`。 - -# 解法一 递归 - -这道题其实和 [111 题]() 是一样的,大家可以先看 [111 题]() 的分析,这道题无非是把 [111 题]() 递归传递的`depth`改为了`sum`的传递。 - -如果不仔细分析题目,代码可能会写成下边的样子。 - -```java -public boolean hasPathSum(TreeNode root, int sum) { - if (root == null) { - return false; - } - return hasPathSumHelper(root, sum); -} - -private boolean hasPathSumHelper(TreeNode root, int sum) { - if (root == null) { - return sum == 0; - } - return hasPathSumHelper(root.left, sum - root.val) || hasPathSumHelper(root.right, sum - root.val); -} -``` - -看起来没什么问题,并且对于题目给的样例也是没问题的。但是对于下边的样例: - -```java - 3 - / \ - 9 20 - / / \ - 8 15 7 - -sum = 12 -``` - -当某个子树只有一个孩子的时候,就会出问题了,可以看 [111 题]() 的分析。 - -所以代码需要写成下边的样子。 - -```java -public boolean hasPathSum(TreeNode root, int sum) { - if (root == null) { - return false; - } - return hasPathSumHelper(root, sum); -} - -private boolean hasPathSumHelper(TreeNode root, int sum) { - //到达叶子节点 - if (root.left == null && root.right == null) { - return root.val == sum; - } - //左孩子为 null - if (root.left == null) { - return hasPathSumHelper(root.right, sum - root.val); - } - //右孩子为 null - if (root.right == null) { - return hasPathSumHelper(root.left, sum - root.val); - } - return hasPathSumHelper(root.left, sum - root.val) || hasPathSumHelper(root.right, sum - root.val); -} -``` - -# 解法二 BFS - -同样的,我们可以利用一个队列对二叉树进行层次遍历。同时还需要一个队列,保存当前从根节点到当前节点已经累加的和。`BFS`的基本框架不用改变,参考 [102 题]()。只需要多一个队列,进行细微的改变即可。 - -```java -public boolean hasPathSum(TreeNode root, int sum) { - Queue queue = new LinkedList(); - Queue queueSum = new LinkedList(); - if (root == null) - return false; - queue.offer(root); - queueSum.offer(root.val); - while (!queue.isEmpty()) { - int levelNum = queue.size(); // 当前层元素的个数 - for (int i = 0; i < levelNum; i++) { - TreeNode curNode = queue.poll(); - int curSum = queueSum.poll(); - if (curNode != null) { - //判断叶子节点是否满足了条件 - if (curNode.left == null && curNode.right == null && curSum == sum) { - return true; - } - //当前节点和累计的和加入队列 - if (curNode.left != null) { - queue.offer(curNode.left); - queueSum.offer(curSum + curNode.left.val); - } - if (curNode.right != null) { - queue.offer(curNode.right); - queueSum.offer(curSum + curNode.right.val); - } - } - } - } - return false; -} -``` - -# 解法三 DFS - -解法一其实本质上就是做了`DFS`,我们知道`DFS`可以用栈去模拟。对于这道题,我们可以像解法二的`BFS`一样,再增加一个栈,去保存从根节点到当前节点累计的和就可以了。 - -这里的话,用`DFS`里的中序遍历,参考 [94 题]()。 - -```java -public boolean hasPathSum(TreeNode root, int sum) { - Stack stack = new Stack<>(); - Stack stackSum = new Stack<>(); - TreeNode cur = root; - int curSum = 0; - while (cur != null || !stack.isEmpty()) { - // 节点不为空一直压栈 - while (cur != null) { - stack.push(cur); - curSum += cur.val; - stackSum.push(curSum); - cur = cur.left; // 考虑左子树 - } - // 节点为空,就出栈 - cur = stack.pop(); - curSum = stackSum.pop(); - //判断是否满足条件 - if (curSum == sum && cur.left == null && cur.right == null) { - return true; - } - // 考虑右子树 - cur = cur.right; - } - return false; -} -``` - -但是之前讲了,对于这种利用栈完全模拟递归的思路,对时间复杂度和空间复杂度并没有什么提高。只是把递归传递的参数`root`和`sum`,本该由计算机自动的压栈出栈,由我们手动去压栈出栈了。 - -所以我们能不能提高一下,比如省去`sum`这个栈?让我们来分析以下。参考 [这里]() 。 - -我们如果只用一个变量`curSum`来记录根节点到当前节点累计的和,有节点入栈就加上节点的值,有节点出栈就减去节点的值。 - -比如对于下边的树,我们进行中序遍历。 - -```java - 3 - / \ - 9 20 - / \ - 8 15 - -curSum = 0 -3 入栈, curSum = 3,3 -9 入栈, curSum = 12,3 -> 9 -8 入栈, curSum = 20, 3 -> 9 -> 8 -8 出栈, curSum = 12, 3 -> 9 -9 出栈, curSum = 3, -15 入栈, curSum = 18, 3 -> 9 -> 15 -``` - -此时路径是 `3 -> 9 -> 15`,和应该是 `27`。但我们得到的是 `18`,少加了 `9 `。 - -原因就是我们进行的是中序遍历,当我们还没访问右边的节点的时候,根节点已经出栈了,再访问右边节点的时候,`curSum`就会少一个根节点的值。 - -所以,我们可以用后序遍历,先访问左子树,再访问右子树,最后访问根节点。再看一下上边的问题。 - -```java - 3 - / \ - 9 20 - / \ - 8 15 - -curSum = 0 -3 入栈, curSum = 3,3 -9 入栈, curSum = 12,3 -> 9 -8 入栈, curSum = 20, 3 -> 9 -> 8 -8 出栈, curSum = 12, 3 -> 9 -15 入栈, curSum = 27, 3 -> 9 -> 15 -``` - -此时路径 `3 -> 9 -> 15` 对应的 `curSum` 就是正确的了。 - -用栈实现后序遍历,比中序遍历要复杂一些。当访问到根节点的时候,它的右子树可能访问过了,那就把根节点输出。它的右子树可能没访问过,我们需要去遍历它的右子树。所以我们要用一个变量`pre`保存上一次遍历的节点,用来判断当前根节点的右子树是否已经遍历完成。 - -```java -public List postorderTraversal(TreeNode root) { - List result = new LinkedList<>(); - Stack toVisit = new Stack<>(); - TreeNode cur = root; - TreeNode pre = null; - - while (cur != null || !toVisit.isEmpty()) { - while (cur != null) { - toVisit.push(cur); // 添加根节点 - cur = cur.left; // 递归添加左节点 - } - cur = toVisit.peek(); // 已经访问到最左的节点了 - // 在不存在右节点或者右节点已经访问过的情况下,访问根节点 - if (cur.right == null || cur.right == pre) { - toVisit.pop(); - result.add(cur.val); - pre = cur; - cur = null; - } else { - cur = cur.right; // 右节点还没有访问过就先访问右节点 - } - } - return result; -} -``` - -有了上边的后序遍历,对于这道题,代码就很好改了。 - -```java -public boolean hasPathSum(TreeNode root, int sum) { - Stack toVisit = new Stack<>(); - TreeNode cur = root; - TreeNode pre = null; - int curSum = 0; //记录当前的累计的和 - while (cur != null || !toVisit.isEmpty()) { - while (cur != null) { - toVisit.push(cur); // 添加根节点 - curSum += cur.val; - cur = cur.left; // 递归添加左节点 - } - cur = toVisit.peek(); // 已经访问到最左的节点了 - //判断是否满足条件 - if (curSum == sum && cur.left == null && cur.right == null) { - return true; - } - // 在不存在右节点或者右节点已经访问过的情况下,访问根节点 - if (cur.right == null || cur.right == pre) { - TreeNode pop = toVisit.pop(); - curSum -= pop.val; //减去出栈的值 - pre = cur; - cur = null; - } else { - cur = cur.right; // 右节点还没有访问过就先访问右节点 - } - } - return false; -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/112.jpg) + +给定一个`sum`,判断是否有一条从根节点到叶子节点的路径,该路径上所有数字的和等于`sum`。 + +# 解法一 递归 + +这道题其实和 [111 题]() 是一样的,大家可以先看 [111 题]() 的分析,这道题无非是把 [111 题]() 递归传递的`depth`改为了`sum`的传递。 + +如果不仔细分析题目,代码可能会写成下边的样子。 + +```java +public boolean hasPathSum(TreeNode root, int sum) { + if (root == null) { + return false; + } + return hasPathSumHelper(root, sum); +} + +private boolean hasPathSumHelper(TreeNode root, int sum) { + if (root == null) { + return sum == 0; + } + return hasPathSumHelper(root.left, sum - root.val) || hasPathSumHelper(root.right, sum - root.val); +} +``` + +看起来没什么问题,并且对于题目给的样例也是没问题的。但是对于下边的样例: + +```java + 3 + / \ + 9 20 + / / \ + 8 15 7 + +sum = 12 +``` + +当某个子树只有一个孩子的时候,就会出问题了,可以看 [111 题]() 的分析。 + +所以代码需要写成下边的样子。 + +```java +public boolean hasPathSum(TreeNode root, int sum) { + if (root == null) { + return false; + } + return hasPathSumHelper(root, sum); +} + +private boolean hasPathSumHelper(TreeNode root, int sum) { + //到达叶子节点 + if (root.left == null && root.right == null) { + return root.val == sum; + } + //左孩子为 null + if (root.left == null) { + return hasPathSumHelper(root.right, sum - root.val); + } + //右孩子为 null + if (root.right == null) { + return hasPathSumHelper(root.left, sum - root.val); + } + return hasPathSumHelper(root.left, sum - root.val) || hasPathSumHelper(root.right, sum - root.val); +} +``` + +# 解法二 BFS + +同样的,我们可以利用一个队列对二叉树进行层次遍历。同时还需要一个队列,保存当前从根节点到当前节点已经累加的和。`BFS`的基本框架不用改变,参考 [102 题]()。只需要多一个队列,进行细微的改变即可。 + +```java +public boolean hasPathSum(TreeNode root, int sum) { + Queue queue = new LinkedList(); + Queue queueSum = new LinkedList(); + if (root == null) + return false; + queue.offer(root); + queueSum.offer(root.val); + while (!queue.isEmpty()) { + int levelNum = queue.size(); // 当前层元素的个数 + for (int i = 0; i < levelNum; i++) { + TreeNode curNode = queue.poll(); + int curSum = queueSum.poll(); + if (curNode != null) { + //判断叶子节点是否满足了条件 + if (curNode.left == null && curNode.right == null && curSum == sum) { + return true; + } + //当前节点和累计的和加入队列 + if (curNode.left != null) { + queue.offer(curNode.left); + queueSum.offer(curSum + curNode.left.val); + } + if (curNode.right != null) { + queue.offer(curNode.right); + queueSum.offer(curSum + curNode.right.val); + } + } + } + } + return false; +} +``` + +# 解法三 DFS + +解法一其实本质上就是做了`DFS`,我们知道`DFS`可以用栈去模拟。对于这道题,我们可以像解法二的`BFS`一样,再增加一个栈,去保存从根节点到当前节点累计的和就可以了。 + +这里的话,用`DFS`里的中序遍历,参考 [94 题]()。 + +```java +public boolean hasPathSum(TreeNode root, int sum) { + Stack stack = new Stack<>(); + Stack stackSum = new Stack<>(); + TreeNode cur = root; + int curSum = 0; + while (cur != null || !stack.isEmpty()) { + // 节点不为空一直压栈 + while (cur != null) { + stack.push(cur); + curSum += cur.val; + stackSum.push(curSum); + cur = cur.left; // 考虑左子树 + } + // 节点为空,就出栈 + cur = stack.pop(); + curSum = stackSum.pop(); + //判断是否满足条件 + if (curSum == sum && cur.left == null && cur.right == null) { + return true; + } + // 考虑右子树 + cur = cur.right; + } + return false; +} +``` + +但是之前讲了,对于这种利用栈完全模拟递归的思路,对时间复杂度和空间复杂度并没有什么提高。只是把递归传递的参数`root`和`sum`,本该由计算机自动的压栈出栈,由我们手动去压栈出栈了。 + +所以我们能不能提高一下,比如省去`sum`这个栈?让我们来分析以下。参考 [这里]() 。 + +我们如果只用一个变量`curSum`来记录根节点到当前节点累计的和,有节点入栈就加上节点的值,有节点出栈就减去节点的值。 + +比如对于下边的树,我们进行中序遍历。 + +```java + 3 + / \ + 9 20 + / \ + 8 15 + +curSum = 0 +3 入栈, curSum = 3,3 +9 入栈, curSum = 12,3 -> 9 +8 入栈, curSum = 20, 3 -> 9 -> 8 +8 出栈, curSum = 12, 3 -> 9 +9 出栈, curSum = 3, +15 入栈, curSum = 18, 3 -> 9 -> 15 +``` + +此时路径是 `3 -> 9 -> 15`,和应该是 `27`。但我们得到的是 `18`,少加了 `9 `。 + +原因就是我们进行的是中序遍历,当我们还没访问右边的节点的时候,根节点已经出栈了,再访问右边节点的时候,`curSum`就会少一个根节点的值。 + +所以,我们可以用后序遍历,先访问左子树,再访问右子树,最后访问根节点。再看一下上边的问题。 + +```java + 3 + / \ + 9 20 + / \ + 8 15 + +curSum = 0 +3 入栈, curSum = 3,3 +9 入栈, curSum = 12,3 -> 9 +8 入栈, curSum = 20, 3 -> 9 -> 8 +8 出栈, curSum = 12, 3 -> 9 +15 入栈, curSum = 27, 3 -> 9 -> 15 +``` + +此时路径 `3 -> 9 -> 15` 对应的 `curSum` 就是正确的了。 + +用栈实现后序遍历,比中序遍历要复杂一些。当访问到根节点的时候,它的右子树可能访问过了,那就把根节点输出。它的右子树可能没访问过,我们需要去遍历它的右子树。所以我们要用一个变量`pre`保存上一次遍历的节点,用来判断当前根节点的右子树是否已经遍历完成。 + +```java +public List postorderTraversal(TreeNode root) { + List result = new LinkedList<>(); + Stack toVisit = new Stack<>(); + TreeNode cur = root; + TreeNode pre = null; + + while (cur != null || !toVisit.isEmpty()) { + while (cur != null) { + toVisit.push(cur); // 添加根节点 + cur = cur.left; // 递归添加左节点 + } + cur = toVisit.peek(); // 已经访问到最左的节点了 + // 在不存在右节点或者右节点已经访问过的情况下,访问根节点 + if (cur.right == null || cur.right == pre) { + toVisit.pop(); + result.add(cur.val); + pre = cur; + cur = null; + } else { + cur = cur.right; // 右节点还没有访问过就先访问右节点 + } + } + return result; +} +``` + +有了上边的后序遍历,对于这道题,代码就很好改了。 + +```java +public boolean hasPathSum(TreeNode root, int sum) { + Stack toVisit = new Stack<>(); + TreeNode cur = root; + TreeNode pre = null; + int curSum = 0; //记录当前的累计的和 + while (cur != null || !toVisit.isEmpty()) { + while (cur != null) { + toVisit.push(cur); // 添加根节点 + curSum += cur.val; + cur = cur.left; // 递归添加左节点 + } + cur = toVisit.peek(); // 已经访问到最左的节点了 + //判断是否满足条件 + if (curSum == sum && cur.left == null && cur.right == null) { + return true; + } + // 在不存在右节点或者右节点已经访问过的情况下,访问根节点 + if (cur.right == null || cur.right == pre) { + TreeNode pop = toVisit.pop(); + curSum -= pop.val; //减去出栈的值 + pre = cur; + cur = null; + } else { + cur = cur.right; // 右节点还没有访问过就先访问右节点 + } + } + return false; +} +``` + +# 总 + 这道题还是在考二叉树的遍历,`DFS`,`BFS`。解法三通过后序遍历节省了`sum`栈,蛮有意思的。 \ No newline at end of file diff --git a/leetcode-113-Path-SumII.md b/leetcode-113-Path-SumII.md index eb9ad1222..ea4aedaaa 100644 --- a/leetcode-113-Path-SumII.md +++ b/leetcode-113-Path-SumII.md @@ -1,170 +1,170 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/113.jpg) - -[112 题]() 的升级版,给定一个`sum`,输出从根节点开始到叶子节点,和为`sum` 的所有路径可能。 - -直接在 [112 题]() 的基础上改了,解法没有新内容,大家可以过去看一看。 - -# 解法一 递归 - -[112 题]() 的解法是下边的样子。 - -```java -public boolean hasPathSum(TreeNode root, int sum) { - if (root == null) { - return false; - } - return hasPathSumHelper(root, sum); -} - -private boolean hasPathSumHelper(TreeNode root, int sum) { - //到达叶子节点 - if (root.left == null && root.right == null) { - return root.val == sum; - } - //左孩子为 null - if (root.left == null) { - return hasPathSumHelper(root.right, sum - root.val); - } - //右孩子为 null - if (root.right == null) { - return hasPathSumHelper(root.left, sum - root.val); - } - return hasPathSumHelper(root.left, sum - root.val) || hasPathSumHelper(root.right, sum - root.val); -} -``` - -这里的话我们需要一个`ans`变量来保存所有结果。一个`temp`变量来保存遍历的路径。需要注意的地方就是,`java`中的`list`传递的是引用,所以递归结束后,要把之前加入的元素删除,不要影响到其他分支的`temp`。 - -```java -public List> pathSum(TreeNode root, int sum) { - - List> ans = new ArrayList<>(); - if (root == null) { - return ans; - } - hasPathSumHelper(root, sum, new ArrayList(), ans); - return ans; -} - -private void hasPathSumHelper(TreeNode root, int sum, ArrayList temp, List> ans) { - // 到达叶子节点 - if (root.left == null && root.right == null) { - if (root.val == sum) { - temp.add(root.val); - ans.add(new ArrayList<>(temp)); - temp.remove(temp.size() - 1); - } - return; - } - // 左孩子为 null - if (root.left == null) { - temp.add(root.val); - hasPathSumHelper(root.right, sum - root.val, temp, ans); - temp.remove(temp.size() - 1); - return; - } - // 右孩子为 null - if (root.right == null) { - temp.add(root.val); - hasPathSumHelper(root.left, sum - root.val, temp, ans); - temp.remove(temp.size() - 1); - return; - } - temp.add(root.val); - hasPathSumHelper(root.right, sum - root.val, temp, ans); - temp.remove(temp.size() - 1); - - temp.add(root.val); - hasPathSumHelper(root.left, sum - root.val, temp, ans); - temp.remove(temp.size() - 1); -} -``` - -# 解法二 DFS 栈 - -[112 题]() 中解法二讲的是`BFS`,但是对于这道题由于我们要保存一条一条的路径,而`BFS`是一层一层的进行的,到最后一层一次性会得到很多条路径。这就导致遍历过程中,我们需要很多`list`来保存不同的路径,对于这道题是不划算的。 - -所以这里我们看 [112 题]() 利用栈实现的`DFS`。 - -看一下之前用后序遍历实现的代码。 - -```java -public boolean hasPathSum(TreeNode root, int sum) { - List result = new LinkedList<>(); - Stack toVisit = new Stack<>(); - TreeNode cur = root; - TreeNode pre = null; - int curSum = 0; //记录当前的累计的和 - while (cur != null || !toVisit.isEmpty()) { - while (cur != null) { - toVisit.push(cur); // 添加根节点 - curSum += cur.val; - cur = cur.left; // 递归添加左节点 - } - cur = toVisit.peek(); // 已经访问到最左的节点了 - //判断是否满足条件 - if (curSum == sum && cur.left == null && cur.right == null) { - return true; - } - // 在不存在右节点或者右节点已经访问过的情况下,访问根节点 - if (cur.right == null || cur.right == pre) { - TreeNode pop = toVisit.pop(); - curSum -= pop.val; //减去出栈的值 - pre = cur; - cur = null; - } else { - cur = cur.right; // 右节点还没有访问过就先访问右节点 - } - } - return false; -} -``` - -和解法一一样,我们需要`ans`变量和`temp`变量,同样需要注意`temp`是对象,是引用传递。 - -```java -public List> pathSum(TreeNode root, int sum) { - Stack toVisit = new Stack<>(); - List> ans = new ArrayList<>(); - List temp = new ArrayList<>(); - TreeNode cur = root; - TreeNode pre = null; - int curSum = 0; // 记录当前的累计的和 - while (cur != null || !toVisit.isEmpty()) { - while (cur != null) { - toVisit.push(cur); // 添加根节点 - curSum += cur.val; - /************修改的地方******************/ - temp.add(cur.val); - /**************************************/ - cur = cur.left; // 递归添加左节点 - } - cur = toVisit.peek(); // 已经访问到最左的节点了 - // 判断是否满足条件 - if (curSum == sum && cur.left == null && cur.right == null) { - /************修改的地方******************/ - ans.add(new ArrayList<>(temp)); - /**************************************/ - } - // 在不存在右节点或者右节点已经访问过的情况下,访问根节点 - if (cur.right == null || cur.right == pre) { - TreeNode pop = toVisit.pop(); - curSum -= pop.val; // 减去出栈的值 - /************修改的地方******************/ - temp.remove(temp.size() - 1); - /**************************************/ - pre = cur; - cur = null; - } else { - cur = cur.right; // 右节点还没有访问过就先访问右节点 - } - } - return ans; -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/113.jpg) + +[112 题]() 的升级版,给定一个`sum`,输出从根节点开始到叶子节点,和为`sum` 的所有路径可能。 + +直接在 [112 题]() 的基础上改了,解法没有新内容,大家可以过去看一看。 + +# 解法一 递归 + +[112 题]() 的解法是下边的样子。 + +```java +public boolean hasPathSum(TreeNode root, int sum) { + if (root == null) { + return false; + } + return hasPathSumHelper(root, sum); +} + +private boolean hasPathSumHelper(TreeNode root, int sum) { + //到达叶子节点 + if (root.left == null && root.right == null) { + return root.val == sum; + } + //左孩子为 null + if (root.left == null) { + return hasPathSumHelper(root.right, sum - root.val); + } + //右孩子为 null + if (root.right == null) { + return hasPathSumHelper(root.left, sum - root.val); + } + return hasPathSumHelper(root.left, sum - root.val) || hasPathSumHelper(root.right, sum - root.val); +} +``` + +这里的话我们需要一个`ans`变量来保存所有结果。一个`temp`变量来保存遍历的路径。需要注意的地方就是,`java`中的`list`传递的是引用,所以递归结束后,要把之前加入的元素删除,不要影响到其他分支的`temp`。 + +```java +public List> pathSum(TreeNode root, int sum) { + + List> ans = new ArrayList<>(); + if (root == null) { + return ans; + } + hasPathSumHelper(root, sum, new ArrayList(), ans); + return ans; +} + +private void hasPathSumHelper(TreeNode root, int sum, ArrayList temp, List> ans) { + // 到达叶子节点 + if (root.left == null && root.right == null) { + if (root.val == sum) { + temp.add(root.val); + ans.add(new ArrayList<>(temp)); + temp.remove(temp.size() - 1); + } + return; + } + // 左孩子为 null + if (root.left == null) { + temp.add(root.val); + hasPathSumHelper(root.right, sum - root.val, temp, ans); + temp.remove(temp.size() - 1); + return; + } + // 右孩子为 null + if (root.right == null) { + temp.add(root.val); + hasPathSumHelper(root.left, sum - root.val, temp, ans); + temp.remove(temp.size() - 1); + return; + } + temp.add(root.val); + hasPathSumHelper(root.right, sum - root.val, temp, ans); + temp.remove(temp.size() - 1); + + temp.add(root.val); + hasPathSumHelper(root.left, sum - root.val, temp, ans); + temp.remove(temp.size() - 1); +} +``` + +# 解法二 DFS 栈 + +[112 题]() 中解法二讲的是`BFS`,但是对于这道题由于我们要保存一条一条的路径,而`BFS`是一层一层的进行的,到最后一层一次性会得到很多条路径。这就导致遍历过程中,我们需要很多`list`来保存不同的路径,对于这道题是不划算的。 + +所以这里我们看 [112 题]() 利用栈实现的`DFS`。 + +看一下之前用后序遍历实现的代码。 + +```java +public boolean hasPathSum(TreeNode root, int sum) { + List result = new LinkedList<>(); + Stack toVisit = new Stack<>(); + TreeNode cur = root; + TreeNode pre = null; + int curSum = 0; //记录当前的累计的和 + while (cur != null || !toVisit.isEmpty()) { + while (cur != null) { + toVisit.push(cur); // 添加根节点 + curSum += cur.val; + cur = cur.left; // 递归添加左节点 + } + cur = toVisit.peek(); // 已经访问到最左的节点了 + //判断是否满足条件 + if (curSum == sum && cur.left == null && cur.right == null) { + return true; + } + // 在不存在右节点或者右节点已经访问过的情况下,访问根节点 + if (cur.right == null || cur.right == pre) { + TreeNode pop = toVisit.pop(); + curSum -= pop.val; //减去出栈的值 + pre = cur; + cur = null; + } else { + cur = cur.right; // 右节点还没有访问过就先访问右节点 + } + } + return false; +} +``` + +和解法一一样,我们需要`ans`变量和`temp`变量,同样需要注意`temp`是对象,是引用传递。 + +```java +public List> pathSum(TreeNode root, int sum) { + Stack toVisit = new Stack<>(); + List> ans = new ArrayList<>(); + List temp = new ArrayList<>(); + TreeNode cur = root; + TreeNode pre = null; + int curSum = 0; // 记录当前的累计的和 + while (cur != null || !toVisit.isEmpty()) { + while (cur != null) { + toVisit.push(cur); // 添加根节点 + curSum += cur.val; + /************修改的地方******************/ + temp.add(cur.val); + /**************************************/ + cur = cur.left; // 递归添加左节点 + } + cur = toVisit.peek(); // 已经访问到最左的节点了 + // 判断是否满足条件 + if (curSum == sum && cur.left == null && cur.right == null) { + /************修改的地方******************/ + ans.add(new ArrayList<>(temp)); + /**************************************/ + } + // 在不存在右节点或者右节点已经访问过的情况下,访问根节点 + if (cur.right == null || cur.right == pre) { + TreeNode pop = toVisit.pop(); + curSum -= pop.val; // 减去出栈的值 + /************修改的地方******************/ + temp.remove(temp.size() - 1); + /**************************************/ + pre = cur; + cur = null; + } else { + cur = cur.right; // 右节点还没有访问过就先访问右节点 + } + } + return ans; +} +``` + +# 总 + 和 [112 题]() 没什么区别,主要是注意函数传对象的时候,我们传的不是对象的副本,只是传了一个引用。 \ No newline at end of file diff --git a/leetcode-114-Flatten-Binary-Tree-to-Linked-List.md b/leetcode-114-Flatten-Binary-Tree-to-Linked-List.md index 785d4a32f..4b108ff0f 100644 --- a/leetcode-114-Flatten-Binary-Tree-to-Linked-List.md +++ b/leetcode-114-Flatten-Binary-Tree-to-Linked-List.md @@ -1,311 +1,311 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/114.jpg) - -把一个二叉树展开成一个链表,展开顺序如图所示。 - -# 解法一 - -可以发现展开的顺序其实就是二叉树的先序遍历。算法和 [94 题]() 中序遍历的 Morris 算法有些神似,我们需要两步完成这道题。 - -1. 将左子树插入到右子树的地方 -2. 将原来的右子树接到左子树的最右边节点 -3. 考虑新的右子树的根节点,一直重复上边的过程,直到新的右子树为 null - -可以看图理解下这个过程。 - -```java - 1 - / \ - 2 5 - / \ \ -3 4 6 - -//将 1 的左子树插入到右子树的地方 - 1 - \ - 2 5 - / \ \ - 3 4 6 -//将原来的右子树接到左子树的最右边节点 - 1 - \ - 2 - / \ - 3 4 - \ - 5 - \ - 6 - - //将 2 的左子树插入到右子树的地方 - 1 - \ - 2 - \ - 3 4 - \ - 5 - \ - 6 - - //将原来的右子树接到左子树的最右边节点 - 1 - \ - 2 - \ - 3 - \ - 4 - \ - 5 - \ - 6 - - ...... -``` - -代码的话也很好写,首先我们需要找出左子树最右边的节点以便把右子树接过来。 - -```java -public void flatten(TreeNode root) { - while (root != null) { - //左子树为 null,直接考虑下一个节点 - if (root.left == null) { - root = root.right; - } else { - // 找左子树最右边的节点 - TreeNode pre = root.left; - while (pre.right != null) { - pre = pre.right; - } - //将原来的右子树接到左子树的最右边节点 - pre.right = root.right; - // 将左子树插入到右子树的地方 - root.right = root.left; - root.left = null; - // 考虑下一个节点 - root = root.right; - } - } -} -``` - -# 解法二 - -题目中,要求说是`in-place`,之前一直以为这个意思就是要求空间复杂度是`O(1)`。偶然看见评论区 [StefanPochmann](https://leetcode.com/stefanpochmann) 大神的解释。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/114_2.jpg) - -也就是说`in-place` 的意思可能更多说的是直接在原来的节点上改变指向,空间复杂度并没有要求。所以这道题也可以用递归解一下,参考 [这里]() 。 - -```java - 1 - / \ - 2 5 - / \ \ -3 4 6 -``` - -利用递归的话,可能比解法一难理解一些。 - -题目其实就是将二叉树通过右指针,组成一个链表。 - -```java -1 -> 2 -> 3 -> 4 -> 5 -> 6 -``` - -我们知道题目给定的遍历顺序其实就是先序遍历的顺序,所以我们能不能利用先序遍历的代码,每遍历一个节点,就将上一个节点的右指针更新为当前节点。 - -先序遍历的顺序是`1 2 3 4 5 6`。 - -遍历到`2`,把`1`的右指针指向`2`。`1 -> 2 3 4 5 6`。 - -遍历到`3`,把`2`的右指针指向`3`。`1 -> 2 -> 3 4 5 6`。 - -... ... - -一直进行下去似乎就解决了这个问题。但现实是残酷的,原因就是我们把`1`的右指针指向`2`,那么`1`的原本的右孩子就丢失了,也就是`5` 就找不到了。 - -解决方法的话,我们可以逆过来进行。 - -我们依次遍历`6 5 4 3 2 1`,然后每遍历一个节点就将当前节点的右指针更新为上一个节点。 - -遍历到`5`,把`5`的右指针指向`6`。`6 <- 5 4 3 2 1`。 - -遍历到`4`,把`4`的右指针指向`5`。`6 <- 5 <- 4 3 2 1`。 - -... ... - -```java - 1 - / \ - 2 5 - / \ \ -3 4 6 -``` - -这样就不会有丢失孩子的问题了,因为更新当前的右指针的时候,当前节点的右孩子已经访问过了。 - -而`6 5 4 3 2 1`的遍历顺序其实变形的后序遍历,遍历顺序是右子树->左子树->根节点。 - -先回想一下变形的后序遍历的代码 - -```java -public void PrintBinaryTreeBacRecur(TreeNode root){ - if (root == null) - return; - - PrintBinaryTreeBacRecur(root.right); - PrintBinaryTreeBacRecur(root.left); - System.out.print(root.data); - -} -``` - -这里的话,我们不再是打印根节点,而是利用一个全局变量`pre`,更新当前根节点的右指针为`pre`,左指针为`null`。 - -```java -private TreeNode pre = null; - -public void flatten(TreeNode root) { - if (root == null) - return; - flatten(root.right); - flatten(root.left); - root.right = pre; - root.left = null; - pre = root; -} -``` - -相应的左孩子也要置为`null`,同样的也不用担心左孩子丢失,因为是后序遍历,左孩子已经遍历过了。和 [112 题]() 一样,都巧妙的利用了后序遍历。 - -既然后序遍历这么有用,利用 [112 题]() 介绍的后序遍历的迭代方法,把这道题也改一下吧。 - -```java -public void flatten(TreeNode root) { - Stack toVisit = new Stack<>(); - TreeNode cur = root; - TreeNode pre = null; - - while (cur != null || !toVisit.isEmpty()) { - while (cur != null) { - toVisit.push(cur); // 添加根节点 - cur = cur.right; // 递归添加右节点 - } - cur = toVisit.peek(); // 已经访问到最右的节点了 - // 在不存在左节点或者右节点已经访问过的情况下,访问根节点 - if (cur.left == null || cur.left == pre) { - toVisit.pop(); - /**************修改的地方***************/ - cur.right = pre; - cur.left = null; - /*************************************/ - pre = cur; - cur = null; - } else { - cur = cur.left; // 左节点还没有访问过就先访问左节点 - } - } -} -``` - -# 解法三 - -解法二中提到如果用先序遍历的话,会丢失掉右孩子,除了用后序遍历,还有没有其他的方法避免这个问题。在`Discuss`又发现了一种解法,参考 [这里]()。 - -为了更好的控制算法,所以我们用先序遍历迭代的形式,正常的先序遍历代码如下, - -```java -public static void preOrderStack(TreeNode root) { - if (root == null) { - return; - } - Stack s = new Stack(); - while (root != null || !s.isEmpty()) { - while (root != null) { - System.out.println(root.val); - s.push(root); - root = root.left; - } - root = s.pop(); - root = root.right; - } -} -``` - -还有一种特殊的先序遍历,提前将右孩子保存到栈中,我们利用这种遍历方式就可以防止右孩子的丢失了。由于栈是先进后出,所以我们先将右节点入栈。 - -```java -public static void preOrderStack(TreeNode root) { - if (root == null){ - return; - } - Stack s = new Stack(); - s.push(root); - while (!s.isEmpty()) { - TreeNode temp = s.pop(); - System.out.println(temp.val); - if (temp.right != null){ - s.push(temp.right); - } - if (temp.left != null){ - s.push(temp.left); - } - } -} -``` - -之前我们的思路如下: - -题目其实就是将二叉树通过右指针,组成一个链表。 - -```java -1 -> 2 -> 3 -> 4 -> 5 -> 6 -``` - -我们知道题目给定的遍历顺序其实就是先序遍历的顺序,所以我们可以利用先序遍历的代码,每遍历一个节点,就将上一个节点的右指针更新为当前节点。 - -先序遍历的顺序是`1 2 3 4 5 6`。 - -遍历到`2`,把`1`的右指针指向`2`。`1 -> 2 3 4 5 6`。 - -遍历到`3`,把`2`的右指针指向`3`。`1 -> 2 -> 3 4 5 6`。 - -... ... - -因为我们用栈保存了右孩子,所以不需要担心右孩子丢失了。用一个`pre`变量保存上次遍历的节点。修改的代码如下: - -```java -public void flatten(TreeNode root) { - if (root == null){ - return; - } - Stack s = new Stack(); - s.push(root); - TreeNode pre = null; - while (!s.isEmpty()) { - TreeNode temp = s.pop(); - /***********修改的地方*************/ - if(pre!=null){ - pre.right = temp; - pre.left = null; - } - /********************************/ - if (temp.right != null){ - s.push(temp.right); - } - if (temp.left != null){ - s.push(temp.left); - } - /***********修改的地方*************/ - pre = temp; - /********************************/ - } -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/114.jpg) + +把一个二叉树展开成一个链表,展开顺序如图所示。 + +# 解法一 + +可以发现展开的顺序其实就是二叉树的先序遍历。算法和 [94 题]() 中序遍历的 Morris 算法有些神似,我们需要两步完成这道题。 + +1. 将左子树插入到右子树的地方 +2. 将原来的右子树接到左子树的最右边节点 +3. 考虑新的右子树的根节点,一直重复上边的过程,直到新的右子树为 null + +可以看图理解下这个过程。 + +```java + 1 + / \ + 2 5 + / \ \ +3 4 6 + +//将 1 的左子树插入到右子树的地方 + 1 + \ + 2 5 + / \ \ + 3 4 6 +//将原来的右子树接到左子树的最右边节点 + 1 + \ + 2 + / \ + 3 4 + \ + 5 + \ + 6 + + //将 2 的左子树插入到右子树的地方 + 1 + \ + 2 + \ + 3 4 + \ + 5 + \ + 6 + + //将原来的右子树接到左子树的最右边节点 + 1 + \ + 2 + \ + 3 + \ + 4 + \ + 5 + \ + 6 + + ...... +``` + +代码的话也很好写,首先我们需要找出左子树最右边的节点以便把右子树接过来。 + +```java +public void flatten(TreeNode root) { + while (root != null) { + //左子树为 null,直接考虑下一个节点 + if (root.left == null) { + root = root.right; + } else { + // 找左子树最右边的节点 + TreeNode pre = root.left; + while (pre.right != null) { + pre = pre.right; + } + //将原来的右子树接到左子树的最右边节点 + pre.right = root.right; + // 将左子树插入到右子树的地方 + root.right = root.left; + root.left = null; + // 考虑下一个节点 + root = root.right; + } + } +} +``` + +# 解法二 + +题目中,要求说是`in-place`,之前一直以为这个意思就是要求空间复杂度是`O(1)`。偶然看见评论区 [StefanPochmann](https://leetcode.com/stefanpochmann) 大神的解释。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/114_2.jpg) + +也就是说`in-place` 的意思可能更多说的是直接在原来的节点上改变指向,空间复杂度并没有要求。所以这道题也可以用递归解一下,参考 [这里]() 。 + +```java + 1 + / \ + 2 5 + / \ \ +3 4 6 +``` + +利用递归的话,可能比解法一难理解一些。 + +题目其实就是将二叉树通过右指针,组成一个链表。 + +```java +1 -> 2 -> 3 -> 4 -> 5 -> 6 +``` + +我们知道题目给定的遍历顺序其实就是先序遍历的顺序,所以我们能不能利用先序遍历的代码,每遍历一个节点,就将上一个节点的右指针更新为当前节点。 + +先序遍历的顺序是`1 2 3 4 5 6`。 + +遍历到`2`,把`1`的右指针指向`2`。`1 -> 2 3 4 5 6`。 + +遍历到`3`,把`2`的右指针指向`3`。`1 -> 2 -> 3 4 5 6`。 + +... ... + +一直进行下去似乎就解决了这个问题。但现实是残酷的,原因就是我们把`1`的右指针指向`2`,那么`1`的原本的右孩子就丢失了,也就是`5` 就找不到了。 + +解决方法的话,我们可以逆过来进行。 + +我们依次遍历`6 5 4 3 2 1`,然后每遍历一个节点就将当前节点的右指针更新为上一个节点。 + +遍历到`5`,把`5`的右指针指向`6`。`6 <- 5 4 3 2 1`。 + +遍历到`4`,把`4`的右指针指向`5`。`6 <- 5 <- 4 3 2 1`。 + +... ... + +```java + 1 + / \ + 2 5 + / \ \ +3 4 6 +``` + +这样就不会有丢失孩子的问题了,因为更新当前的右指针的时候,当前节点的右孩子已经访问过了。 + +而`6 5 4 3 2 1`的遍历顺序其实变形的后序遍历,遍历顺序是右子树->左子树->根节点。 + +先回想一下变形的后序遍历的代码 + +```java +public void PrintBinaryTreeBacRecur(TreeNode root){ + if (root == null) + return; + + PrintBinaryTreeBacRecur(root.right); + PrintBinaryTreeBacRecur(root.left); + System.out.print(root.data); + +} +``` + +这里的话,我们不再是打印根节点,而是利用一个全局变量`pre`,更新当前根节点的右指针为`pre`,左指针为`null`。 + +```java +private TreeNode pre = null; + +public void flatten(TreeNode root) { + if (root == null) + return; + flatten(root.right); + flatten(root.left); + root.right = pre; + root.left = null; + pre = root; +} +``` + +相应的左孩子也要置为`null`,同样的也不用担心左孩子丢失,因为是后序遍历,左孩子已经遍历过了。和 [112 题]() 一样,都巧妙的利用了后序遍历。 + +既然后序遍历这么有用,利用 [112 题]() 介绍的后序遍历的迭代方法,把这道题也改一下吧。 + +```java +public void flatten(TreeNode root) { + Stack toVisit = new Stack<>(); + TreeNode cur = root; + TreeNode pre = null; + + while (cur != null || !toVisit.isEmpty()) { + while (cur != null) { + toVisit.push(cur); // 添加根节点 + cur = cur.right; // 递归添加右节点 + } + cur = toVisit.peek(); // 已经访问到最右的节点了 + // 在不存在左节点或者右节点已经访问过的情况下,访问根节点 + if (cur.left == null || cur.left == pre) { + toVisit.pop(); + /**************修改的地方***************/ + cur.right = pre; + cur.left = null; + /*************************************/ + pre = cur; + cur = null; + } else { + cur = cur.left; // 左节点还没有访问过就先访问左节点 + } + } +} +``` + +# 解法三 + +解法二中提到如果用先序遍历的话,会丢失掉右孩子,除了用后序遍历,还有没有其他的方法避免这个问题。在`Discuss`又发现了一种解法,参考 [这里]()。 + +为了更好的控制算法,所以我们用先序遍历迭代的形式,正常的先序遍历代码如下, + +```java +public static void preOrderStack(TreeNode root) { + if (root == null) { + return; + } + Stack s = new Stack(); + while (root != null || !s.isEmpty()) { + while (root != null) { + System.out.println(root.val); + s.push(root); + root = root.left; + } + root = s.pop(); + root = root.right; + } +} +``` + +还有一种特殊的先序遍历,提前将右孩子保存到栈中,我们利用这种遍历方式就可以防止右孩子的丢失了。由于栈是先进后出,所以我们先将右节点入栈。 + +```java +public static void preOrderStack(TreeNode root) { + if (root == null){ + return; + } + Stack s = new Stack(); + s.push(root); + while (!s.isEmpty()) { + TreeNode temp = s.pop(); + System.out.println(temp.val); + if (temp.right != null){ + s.push(temp.right); + } + if (temp.left != null){ + s.push(temp.left); + } + } +} +``` + +之前我们的思路如下: + +题目其实就是将二叉树通过右指针,组成一个链表。 + +```java +1 -> 2 -> 3 -> 4 -> 5 -> 6 +``` + +我们知道题目给定的遍历顺序其实就是先序遍历的顺序,所以我们可以利用先序遍历的代码,每遍历一个节点,就将上一个节点的右指针更新为当前节点。 + +先序遍历的顺序是`1 2 3 4 5 6`。 + +遍历到`2`,把`1`的右指针指向`2`。`1 -> 2 3 4 5 6`。 + +遍历到`3`,把`2`的右指针指向`3`。`1 -> 2 -> 3 4 5 6`。 + +... ... + +因为我们用栈保存了右孩子,所以不需要担心右孩子丢失了。用一个`pre`变量保存上次遍历的节点。修改的代码如下: + +```java +public void flatten(TreeNode root) { + if (root == null){ + return; + } + Stack s = new Stack(); + s.push(root); + TreeNode pre = null; + while (!s.isEmpty()) { + TreeNode temp = s.pop(); + /***********修改的地方*************/ + if(pre!=null){ + pre.right = temp; + pre.left = null; + } + /********************************/ + if (temp.right != null){ + s.push(temp.right); + } + if (temp.left != null){ + s.push(temp.left); + } + /***********修改的地方*************/ + pre = temp; + /********************************/ + } +} +``` + +# 总 + 解法一和解法三可以看作自顶向下的解决问题,解法二可以看作自底向上。以前觉得后序遍历比较麻烦,没想到竟然连续遇到了后序遍历的应用。先序遍历的两种方式自己也是第一次意识到,之前都是用的第一种正常的方式。 \ No newline at end of file diff --git a/leetcode-115-Distinct-Subsequences.md b/leetcode-115-Distinct-Subsequences.md index 729ddb580..9dd1285a3 100644 --- a/leetcode-115-Distinct-Subsequences.md +++ b/leetcode-115-Distinct-Subsequences.md @@ -1,431 +1,431 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/115.png) - -给定两个字符串 `S` 和`T`,从 `S` 中选择字母,使得刚好和 `T` 相等,有多少种选法。 - -# 解法一 递归之分治 - -S 中的每个字母就是两种可能选他或者不选他。我们用递归的常规思路,将大问题化成小问题,也就是分治的思想。 - -如果我们求 `S[0,S_len - 1]` 中能选出多少个 `T[0,T_len - 1]`,个数记为 `n`。那么分两种情况, - -* `S[0] == T[0]`,需要知道两种情况 - - * 从 `S` 中选择当前的字母,此时 `S` 跳过这个字母, `T` 也跳过一个字母。 - - 去求 `S[1,S_len - 1]` 中能选出多少个 `T[1,T_len - 1]`,个数记为 `n1` - - * `S` 不选当前的字母,此时`S`跳过这个字母,` T` 不跳过字母。 - - 去求`S[1,S_len - 1]` 中能选出多少个 `T[0,T_len - 1]`,个数记为 `n2` - -* `S[0] != T[0]` - - `S` 只能不选当前的字母,此时`S`跳过这个字母, `T` 不跳过字母。 - - 去求`S[1,S_len - 1]` 中能选出多少个 `T[0,T_len - 1]`,个数记为 `n1` - -也就是说如果求 `S[0,S_len - 1]` 中能选出多少个 `T[0,T_len - 1]`,个数记为 n。转换为数学式就是 - -```java -if(S[0] == T[0]){ - n = n1 + n2; -}else{ - n = n1; -} -``` - -推广到一般情况,我们可以先写出递归的部分代码。 - -```java -public int numDistinct(String s, String t) { - return numDistinctHelper(s, 0, t, 0); -} - -private int numDistinctHelper(String s, int s_start, String t, int t_start) { - int count = 0; - //当前字母相等 - if (s.charAt(s_start) == t.charAt(t_start)) { - //从 S 选择当前的字母,此时 S 跳过这个字母, T 也跳过一个字母。 - count = numDistinctHelper(s, s_start + 1, t, t_start + 1, map) - //S 不选当前的字母,此时 S 跳过这个字母,T 不跳过字母。 - + numDistinctHelper(s, s_start + 1, t, t_start, map); - //当前字母不相等 - }else{ - //S 只能不选当前的字母,此时 S 跳过这个字母, T 不跳过字母。 - count = numDistinctHelper(s, s_start + 1, t, t_start, map); - } - return count; -} -``` - -递归出口的话,因为我们的`S`和`T`的开始下标都是增长的。 - -如果`S[s_start, S_len - 1]`中, `s_start` 等于了 `S_len` ,意味着`S`是空串,从空串中选字符串`T`,那结果肯定是`0`。 - -如果`T[t_start, T_len - 1]`中,` t_start `等于了 `T_len`,意味着`T`是空串,从`S`中选择空字符串`T`,只需要不选择 `S` 中的所有字母,所以选法是`1`。 - -综上,代码总体就是下边的样子 - -```java -public int numDistinct(String s, String t) { - return numDistinctHelper(s, 0, t, 0); -} - -private int numDistinctHelper(String s, int s_start, String t, int t_start) { - //T 是空串,选法就是 1 种 - if (t_start == t.length()) { - return 1; - } - //S 是空串,选法是 0 种 - if (s_start == s.length()) { - return 0; - } - int count = 0; - //当前字母相等 - if (s.charAt(s_start) == t.charAt(t_start)) { - //从 S 选择当前的字母,此时 S 跳过这个字母, T 也跳过一个字母。 - count = numDistinctHelper(s, s_start + 1, t, t_start + 1) - //S 不选当前的字母,此时 S 跳过这个字母,T 不跳过字母。 - + numDistinctHelper(s, s_start + 1, t, t_start); - //当前字母不相等 - }else{ - //S 只能不选当前的字母,此时 S 跳过这个字母, T 不跳过字母。 - count = numDistinctHelper(s, s_start + 1, t, t_start); - } - return count; -} -``` - -遗憾的是,这个解法对于如果`S`太长的 `case` 会超时。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/115_2.jpg) - -原因就是因为递归函数中,我们多次调用了递归函数,这会使得我们重复递归很多的过程,解决方案就很简单了,`Memoization ` 技术,把每次的结果利用一个`map`保存起来,在求之前,先看`map`中有没有,有的话直接拿出来就可以了。 - -`map`的`key`的话就标识当前的递归,`s_start` 和 `t_start` 联合表示,利用字符串 `s_start + '@' + t_start`。 - -`value`的话就保存这次递归返回的`count`。 - -```java -public int numDistinct(String s, String t) { - HashMap map = new HashMap<>(); - return numDistinctHelper(s, 0, t, 0, map); -} - -private int numDistinctHelper(String s, int s_start, String t, int t_start, HashMap map) { - //T 是空串,选法就是 1 种 - if (t_start == t.length()) { - return 1; - } - //S 是空串,选法是 0 种 - if (s_start == s.length()) { - return 0; - } - String key = s_start + "@" + t_start; - //先判断之前有没有求过这个解 - if (map.containsKey(key)) { - return map.get(key); - } - int count = 0; - //当前字母相等 - if (s.charAt(s_start) == t.charAt(t_start)) { - //从 S 选择当前的字母,此时 S 跳过这个字母, T 也跳过一个字母。 - count = numDistinctHelper(s, s_start + 1, t, t_start + 1, map) - //S 不选当前的字母,此时 S 跳过这个字母,T 不跳过字母。 - + numDistinctHelper(s, s_start + 1, t, t_start, map); - //当前字母不相等 - }else{ - //S 只能不选当前的字母,此时 S 跳过这个字母, T 不跳过字母。 - count = numDistinctHelper(s, s_start + 1, t, t_start, map); - } - //将当前解放到 map 中 - map.put(key, count); - return count; -} -``` - -# 解法二 递归之回溯 - -回溯的思想就是朝着一个方向找到一个解,然后再回到之前的状态,改变当前状态,继续尝试得到新的解。可以类比于二叉树的`DFS`,一路走到底,然后回到之前的节点继续递归。 - -对于这道题,和二叉树的`DFS`很像了,每次有两个可选的状态,选择`S`串的当前字母和不选择当前字母。 - -当`S`串的当前字母和`T`串的当前字母相等,我们就可以选择`S`的当前字母,进入递归。 - -递归出来以后,继续尝试不选择`S`的当前字母,进入递归。 - -代码可以是下边这样。 - -```java -public int numDistinct3(String s, String t) { - numDistinctHelper(s, 0, t, 0); -} - -private void numDistinctHelper(String s, int s_start, String t, int t_start) { - //当前字母相等,选中当前 S 的字母,s_start 后移一个 - //选中当前 S 的字母,意味着和 T 的当前字母匹配,所以 t_start 后移一个 - if (s.charAt(s_start) == t.charAt(t_start)) { - numDistinctHelper(s, s_start + 1, t, t_start + 1); - } - //出来以后,继续尝试不选择当前字母,s_start 后移一个,t_start 不后移 - numDistinctHelper(s, s_start + 1, t, t_start); -} -``` - -递归出口的话,就是两种了。 - -* 当`t_start == T_len`,那么就意味着当前从`S`中选择的字母组成了`T`,此时就代表一种选法。我们可以用一个全局变量`count`,`count`计数此时就加一。然后`return`,返回到上一层继续寻求解。 - -* 当`s_start == S_len`,此时`S`到达了结尾,直接 return。 - -```java -int count = 0; -public int numDistinct(String s, String t) { - numDistinctHelper(s, 0, t, 0); - return count; -} - -private void numDistinctHelper(String s, int s_start, String t, int t_start) { - if (t_start == t.length()) { - count++; - return; - } - if (s_start == s.length()) { - return; - } - //当前字母相等,s_start 后移一个,t_start 后移一个 - if (s.charAt(s_start) == t.charAt(t_start)) { - numDistinctHelper(s, s_start + 1, t, t_start + 1); - } - //出来以后,继续尝试不选择当前字母,s_start 后移一个,t_start 不后移 - numDistinctHelper(s, s_start + 1, t, t_start); -} -``` - -![](https://windliang.oss-cn-beijing.aliyuncs.com/115_2.jpg) - -好吧,这个熟悉的错误又出现了,同样是递归中调用了两次递归,会重复计算一些解。怎么办呢?`Memoization ` 技术。 - -`map`的`key`和之前一样,标识当前的递归,`s_start` 和 `t_start` 联合表示,利用字符串 `s_start + '@' + t_start`。 - -`map`的`value`的话?存什么呢。区别于解法一,我们每次都得到了当前条件下的`count`,然后存起来了。而现在我们只有一个全局变量,该怎么办呢?存全局变量`count`吗? - -如果递归过程中 - -```java -if (map.containsKey(key)) { - ... ... -} -``` - -遇到了已经求过的解该怎么办呢? - -我们每次得到一个解后增加全局变量`count`,所以我们`map`的`value`存两次递归后 `count` 的增量。这样的话,第二次遇到同样的情况的时候,就不用递归了,把当前增量加上就可以了。 - -```java -if (map.containsKey(key)) { - count += map.get(key); - return; -} -``` - -综上,代码就出来了 - -```java -int count = 0; -public int numDistinct(String s, String t) { - HashMap map = new HashMap<>(); - numDistinctHelper(s, 0, t, 0, map); - return count; -} - -private void numDistinctHelper(String s, int s_start, String t, int t_start, - HashMap map) { - if (t_start == t.length()) { - count++; - return; - } - if (s_start == s.length()) { - return; - } - String key = s_start + "@" + t_start; - if (map.containsKey(key)) { - count += map.get(key); - return; - } - int count_pre = count; - //当前字母相等,s_start 后移一个,t_start 后移一个 - if (s.charAt(s_start) == t.charAt(t_start)) { - numDistinctHelper(s, s_start + 1, t, t_start + 1, map); - } - //出来以后,继续尝试不选择当前字母,s_start 后移一个,t_start 不后移 - numDistinctHelper(s, s_start + 1, t, t_start, map); - - //将增量存起来 - int count_increment = count - count_pre; - map.put(key, count_increment); -} -``` - -# 解法三 动态规划 - -让我们来回想一下解法一做了什么。`s_start` 和 `t_start` 不停的增加,一直压栈,压栈,直到 - -```java -//T 是空串,选法就是 1 种 -if (t_start == t.length()) { - return 1; -} -//S 是空串,选法是 0 种 -if (s_start == s.length()) { - return 0; -} -``` - -`T` 是空串或者 `S` 是空串,我们就直接可以返回结果了,接下来就是不停的出栈出栈,然后把结果通过递推关系取得。 - -递归的过程就是由顶到底再回到顶。 - -动态规划要做的就是去省略压栈的过程,直接由底向顶。 - -这里我们用一个二维数组 `dp[m][n]` 对应于从 `S[m,S_len)` 中能选出多少个 `T[n,T_len)`。 - -当 `m == S_len`,意味着`S`是空串,此时`dp[S_len][n]`,n 取 0 到 `T_len - 1`的值都为 `0`。 - -当 ` n == T_len`,意味着`T`是空串,此时`dp[m][T_len]`,m 取 0 到 `S_len`的值都为 `1`。 - -然后状态转移的话和解法一分析的一样。如果求`dp[s][t]`。 - -* `S[s] == T[t]`,当前字符相等,那就对应两种情况,选择`S`的当前字母和不选择`S`的当前字母 - - `dp[s][t] = dp[s+1][t+1] + dp[s+1][t]` - -* `S[s] != T[t]`,只有一种情况,不选择`S`的当前字母 - - `dp[s][t] = dp[s+1][t]` - -代码就可以写了。 - -```java -public int numDistinct(String s, String t) { - int s_len = s.length(); - int t_len = t.length(); - int[][] dp = new int[s_len + 1][t_len + 1]; - //当 T 为空串时,所有的 s 对应于 1 - for (int i = 0; i <= s_len; i++) { - dp[i][t_len] = 1; - } - - //倒着进行,T 每次增加一个字母 - for (int t_i = t_len - 1; t_i >= 0; t_i--) { - dp[s_len][t_i] = 0; // 这句可以省去,因为默认值是 0 - //倒着进行,S 每次增加一个字母 - for (int s_i = s_len - 1; s_i >= 0; s_i--) { - //如果当前字母相等 - if (t.charAt(t_i) == s.charAt(s_i)) { - //对应于两种情况,选择当前字母和不选择当前字母 - dp[s_i][t_i] = dp[s_i + 1][t_i + 1] + dp[s_i + 1][t_i]; - //如果当前字母不相等 - } else { - dp[s_i][t_i] = dp[s_i + 1][t_i]; - } - } - } - return dp[0][0]; -} -``` - -对比于解法一和解法二,如果`Memoization ` 技术我们不用`hash`,而是用一个二维数组,会发现其实我们的递归过程,其实就是在更新下图中的二维表,只不过更新的顺序没有动态规划这么归整。这也是不用`Memoization ` 技术会超时的原因,如果把递归的更新路线画出来,会发现很多路线重合了,意味着我们进行了很多没有必要的递归,从而造成了超时。 - -我们画一下动态规划的过程。 - -`S = "babgbag", T = "bag"` - -T 为空串时,所有的 s 对应于 1。 -S 为空串时,所有的 t 对应于 0。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/115_3.jpg) - -此时我们从 `dp[6][2]` 开始求。根据公式,因为当前字母相等,所以 `dp[6][2] = dp[7][3] + dp[7][2] = 1 + 0 = 1 。` - -接着求`dp[5][2]`,当前字母不相等,`dp[5][2] = dp[6][2] = 1`。 - -一直求下去。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/115_4.jpg) - -求当前问号的地方的值的时候,我们只需要它的上一个值和斜对角的值。 - -换句话讲,求当前列的时候,我们只需要上一列的信息。比如当前求第`1`列,第`3`列的值就不会用到了。 - -所以我们可以优化算法的空间复杂度,不需要二维数组,需要一维数组就够了。 - -此时需要解决一个问题,就是当求上图的`dp[1][1]`的时候,需要`dp[2][1]`和`dp[2][2]`的信息。但是如果我们是一维数组,`dp[2][1]`之前已经把`dp[2][2]`的信息覆盖掉了。所以我们需要一个`pre`变量保存之前的值。 - -```java -public int numDistinct(String s, String t) { - int s_len = s.length(); - int t_len = t.length(); - int[]dp = new int[s_len + 1]; - for (int i = 0; i <= s_len; i++) { - dp[i] = 1; - } - //倒着进行,T 每次增加一个字母 - for (int t_i = t_len - 1; t_i >= 0; t_i--) { - int pre = dp[s_len]; - dp[s_len] = 0; - //倒着进行,S 每次增加一个字母 - for (int s_i = s_len - 1; s_i >= 0; s_i--) { - int temp = dp[s_i]; - if (t.charAt(t_i) == s.charAt(s_i)) { - dp[s_i] = dp[s_i + 1] + pre; - } else { - dp[s_i] = dp[s_i + 1]; - } - pre = temp; - } - } - return dp[0]; -} -``` - -利用`temp`和`pre`两个变量实现了保存之前的值。 - -其实动态规划优化空间复杂度的思想,在 [5题](),[10题](),[53题](),[72题 ]()等等都已经用了,是非常经典的。 - -上边的动态规划是从字符串末尾倒着进行的,其实我们只要改变`dp`数组的含义,用`dp[m][n]`表示`S[0,m)`和`T[0,n)`,然后两层循环我们就可以从 `1` 往末尾进行了,思想是类似的,`leetcode` 高票答案也都是这样的,如果理解了上边的思想,代码其实也很好写。这里只分享下代码吧。 - -```java -public int numDistinct(String s, String t) { - int s_len = s.length(); - int t_len = t.length(); - int[] dp = new int[s_len + 1]; - for (int i = 0; i <= s_len; i++) { - dp[i] = 1; - } - for (int t_i = 1; t_i <= t_len; t_i++) { - int pre = dp[0]; - dp[0] = 0; - for (int s_i = 1; s_i <= s_len; s_i++) { - int temp = dp[s_i]; - if (t.charAt(t_i - 1) == s.charAt(s_i - 1)) { - dp[s_i] = dp[s_i - 1] + pre; - } else { - dp[s_i] = dp[s_i - 1]; - } - pre = temp; - } - } - return dp[s_len]; -} -``` - -# 总 - -这道题太经典了,从递归实现回溯,递归实现分治,`Memoization ` 技术对递归的优化,从递归转为动态规划再到动态规划空间复杂度的优化,一切都是理所当然,不需要什么特殊技巧,一切都是这么优雅,太棒了。 - -自己一开始是想到回溯的方法,然后卡到了超时的问题上,看了[这篇]() 和 [这篇]() 的题解后才恍然大悟,一切才都联通了,解法一、解法二、解法三其实本质都是在填充那个二维矩阵,最终殊途同归,不知为什么脑海中有宇宙大爆炸,然后万物产生联系的画面,2333。 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/115.png) + +给定两个字符串 `S` 和`T`,从 `S` 中选择字母,使得刚好和 `T` 相等,有多少种选法。 + +# 解法一 递归之分治 + +S 中的每个字母就是两种可能选他或者不选他。我们用递归的常规思路,将大问题化成小问题,也就是分治的思想。 + +如果我们求 `S[0,S_len - 1]` 中能选出多少个 `T[0,T_len - 1]`,个数记为 `n`。那么分两种情况, + +* `S[0] == T[0]`,需要知道两种情况 + + * 从 `S` 中选择当前的字母,此时 `S` 跳过这个字母, `T` 也跳过一个字母。 + + 去求 `S[1,S_len - 1]` 中能选出多少个 `T[1,T_len - 1]`,个数记为 `n1` + + * `S` 不选当前的字母,此时`S`跳过这个字母,` T` 不跳过字母。 + + 去求`S[1,S_len - 1]` 中能选出多少个 `T[0,T_len - 1]`,个数记为 `n2` + +* `S[0] != T[0]` + + `S` 只能不选当前的字母,此时`S`跳过这个字母, `T` 不跳过字母。 + + 去求`S[1,S_len - 1]` 中能选出多少个 `T[0,T_len - 1]`,个数记为 `n1` + +也就是说如果求 `S[0,S_len - 1]` 中能选出多少个 `T[0,T_len - 1]`,个数记为 n。转换为数学式就是 + +```java +if(S[0] == T[0]){ + n = n1 + n2; +}else{ + n = n1; +} +``` + +推广到一般情况,我们可以先写出递归的部分代码。 + +```java +public int numDistinct(String s, String t) { + return numDistinctHelper(s, 0, t, 0); +} + +private int numDistinctHelper(String s, int s_start, String t, int t_start) { + int count = 0; + //当前字母相等 + if (s.charAt(s_start) == t.charAt(t_start)) { + //从 S 选择当前的字母,此时 S 跳过这个字母, T 也跳过一个字母。 + count = numDistinctHelper(s, s_start + 1, t, t_start + 1, map) + //S 不选当前的字母,此时 S 跳过这个字母,T 不跳过字母。 + + numDistinctHelper(s, s_start + 1, t, t_start, map); + //当前字母不相等 + }else{ + //S 只能不选当前的字母,此时 S 跳过这个字母, T 不跳过字母。 + count = numDistinctHelper(s, s_start + 1, t, t_start, map); + } + return count; +} +``` + +递归出口的话,因为我们的`S`和`T`的开始下标都是增长的。 + +如果`S[s_start, S_len - 1]`中, `s_start` 等于了 `S_len` ,意味着`S`是空串,从空串中选字符串`T`,那结果肯定是`0`。 + +如果`T[t_start, T_len - 1]`中,` t_start `等于了 `T_len`,意味着`T`是空串,从`S`中选择空字符串`T`,只需要不选择 `S` 中的所有字母,所以选法是`1`。 + +综上,代码总体就是下边的样子 + +```java +public int numDistinct(String s, String t) { + return numDistinctHelper(s, 0, t, 0); +} + +private int numDistinctHelper(String s, int s_start, String t, int t_start) { + //T 是空串,选法就是 1 种 + if (t_start == t.length()) { + return 1; + } + //S 是空串,选法是 0 种 + if (s_start == s.length()) { + return 0; + } + int count = 0; + //当前字母相等 + if (s.charAt(s_start) == t.charAt(t_start)) { + //从 S 选择当前的字母,此时 S 跳过这个字母, T 也跳过一个字母。 + count = numDistinctHelper(s, s_start + 1, t, t_start + 1) + //S 不选当前的字母,此时 S 跳过这个字母,T 不跳过字母。 + + numDistinctHelper(s, s_start + 1, t, t_start); + //当前字母不相等 + }else{ + //S 只能不选当前的字母,此时 S 跳过这个字母, T 不跳过字母。 + count = numDistinctHelper(s, s_start + 1, t, t_start); + } + return count; +} +``` + +遗憾的是,这个解法对于如果`S`太长的 `case` 会超时。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/115_2.jpg) + +原因就是因为递归函数中,我们多次调用了递归函数,这会使得我们重复递归很多的过程,解决方案就很简单了,`Memoization ` 技术,把每次的结果利用一个`map`保存起来,在求之前,先看`map`中有没有,有的话直接拿出来就可以了。 + +`map`的`key`的话就标识当前的递归,`s_start` 和 `t_start` 联合表示,利用字符串 `s_start + '@' + t_start`。 + +`value`的话就保存这次递归返回的`count`。 + +```java +public int numDistinct(String s, String t) { + HashMap map = new HashMap<>(); + return numDistinctHelper(s, 0, t, 0, map); +} + +private int numDistinctHelper(String s, int s_start, String t, int t_start, HashMap map) { + //T 是空串,选法就是 1 种 + if (t_start == t.length()) { + return 1; + } + //S 是空串,选法是 0 种 + if (s_start == s.length()) { + return 0; + } + String key = s_start + "@" + t_start; + //先判断之前有没有求过这个解 + if (map.containsKey(key)) { + return map.get(key); + } + int count = 0; + //当前字母相等 + if (s.charAt(s_start) == t.charAt(t_start)) { + //从 S 选择当前的字母,此时 S 跳过这个字母, T 也跳过一个字母。 + count = numDistinctHelper(s, s_start + 1, t, t_start + 1, map) + //S 不选当前的字母,此时 S 跳过这个字母,T 不跳过字母。 + + numDistinctHelper(s, s_start + 1, t, t_start, map); + //当前字母不相等 + }else{ + //S 只能不选当前的字母,此时 S 跳过这个字母, T 不跳过字母。 + count = numDistinctHelper(s, s_start + 1, t, t_start, map); + } + //将当前解放到 map 中 + map.put(key, count); + return count; +} +``` + +# 解法二 递归之回溯 + +回溯的思想就是朝着一个方向找到一个解,然后再回到之前的状态,改变当前状态,继续尝试得到新的解。可以类比于二叉树的`DFS`,一路走到底,然后回到之前的节点继续递归。 + +对于这道题,和二叉树的`DFS`很像了,每次有两个可选的状态,选择`S`串的当前字母和不选择当前字母。 + +当`S`串的当前字母和`T`串的当前字母相等,我们就可以选择`S`的当前字母,进入递归。 + +递归出来以后,继续尝试不选择`S`的当前字母,进入递归。 + +代码可以是下边这样。 + +```java +public int numDistinct3(String s, String t) { + numDistinctHelper(s, 0, t, 0); +} + +private void numDistinctHelper(String s, int s_start, String t, int t_start) { + //当前字母相等,选中当前 S 的字母,s_start 后移一个 + //选中当前 S 的字母,意味着和 T 的当前字母匹配,所以 t_start 后移一个 + if (s.charAt(s_start) == t.charAt(t_start)) { + numDistinctHelper(s, s_start + 1, t, t_start + 1); + } + //出来以后,继续尝试不选择当前字母,s_start 后移一个,t_start 不后移 + numDistinctHelper(s, s_start + 1, t, t_start); +} +``` + +递归出口的话,就是两种了。 + +* 当`t_start == T_len`,那么就意味着当前从`S`中选择的字母组成了`T`,此时就代表一种选法。我们可以用一个全局变量`count`,`count`计数此时就加一。然后`return`,返回到上一层继续寻求解。 + +* 当`s_start == S_len`,此时`S`到达了结尾,直接 return。 + +```java +int count = 0; +public int numDistinct(String s, String t) { + numDistinctHelper(s, 0, t, 0); + return count; +} + +private void numDistinctHelper(String s, int s_start, String t, int t_start) { + if (t_start == t.length()) { + count++; + return; + } + if (s_start == s.length()) { + return; + } + //当前字母相等,s_start 后移一个,t_start 后移一个 + if (s.charAt(s_start) == t.charAt(t_start)) { + numDistinctHelper(s, s_start + 1, t, t_start + 1); + } + //出来以后,继续尝试不选择当前字母,s_start 后移一个,t_start 不后移 + numDistinctHelper(s, s_start + 1, t, t_start); +} +``` + +![](https://windliang.oss-cn-beijing.aliyuncs.com/115_2.jpg) + +好吧,这个熟悉的错误又出现了,同样是递归中调用了两次递归,会重复计算一些解。怎么办呢?`Memoization ` 技术。 + +`map`的`key`和之前一样,标识当前的递归,`s_start` 和 `t_start` 联合表示,利用字符串 `s_start + '@' + t_start`。 + +`map`的`value`的话?存什么呢。区别于解法一,我们每次都得到了当前条件下的`count`,然后存起来了。而现在我们只有一个全局变量,该怎么办呢?存全局变量`count`吗? + +如果递归过程中 + +```java +if (map.containsKey(key)) { + ... ... +} +``` + +遇到了已经求过的解该怎么办呢? + +我们每次得到一个解后增加全局变量`count`,所以我们`map`的`value`存两次递归后 `count` 的增量。这样的话,第二次遇到同样的情况的时候,就不用递归了,把当前增量加上就可以了。 + +```java +if (map.containsKey(key)) { + count += map.get(key); + return; +} +``` + +综上,代码就出来了 + +```java +int count = 0; +public int numDistinct(String s, String t) { + HashMap map = new HashMap<>(); + numDistinctHelper(s, 0, t, 0, map); + return count; +} + +private void numDistinctHelper(String s, int s_start, String t, int t_start, + HashMap map) { + if (t_start == t.length()) { + count++; + return; + } + if (s_start == s.length()) { + return; + } + String key = s_start + "@" + t_start; + if (map.containsKey(key)) { + count += map.get(key); + return; + } + int count_pre = count; + //当前字母相等,s_start 后移一个,t_start 后移一个 + if (s.charAt(s_start) == t.charAt(t_start)) { + numDistinctHelper(s, s_start + 1, t, t_start + 1, map); + } + //出来以后,继续尝试不选择当前字母,s_start 后移一个,t_start 不后移 + numDistinctHelper(s, s_start + 1, t, t_start, map); + + //将增量存起来 + int count_increment = count - count_pre; + map.put(key, count_increment); +} +``` + +# 解法三 动态规划 + +让我们来回想一下解法一做了什么。`s_start` 和 `t_start` 不停的增加,一直压栈,压栈,直到 + +```java +//T 是空串,选法就是 1 种 +if (t_start == t.length()) { + return 1; +} +//S 是空串,选法是 0 种 +if (s_start == s.length()) { + return 0; +} +``` + +`T` 是空串或者 `S` 是空串,我们就直接可以返回结果了,接下来就是不停的出栈出栈,然后把结果通过递推关系取得。 + +递归的过程就是由顶到底再回到顶。 + +动态规划要做的就是去省略压栈的过程,直接由底向顶。 + +这里我们用一个二维数组 `dp[m][n]` 对应于从 `S[m,S_len)` 中能选出多少个 `T[n,T_len)`。 + +当 `m == S_len`,意味着`S`是空串,此时`dp[S_len][n]`,n 取 0 到 `T_len - 1`的值都为 `0`。 + +当 ` n == T_len`,意味着`T`是空串,此时`dp[m][T_len]`,m 取 0 到 `S_len`的值都为 `1`。 + +然后状态转移的话和解法一分析的一样。如果求`dp[s][t]`。 + +* `S[s] == T[t]`,当前字符相等,那就对应两种情况,选择`S`的当前字母和不选择`S`的当前字母 + + `dp[s][t] = dp[s+1][t+1] + dp[s+1][t]` + +* `S[s] != T[t]`,只有一种情况,不选择`S`的当前字母 + + `dp[s][t] = dp[s+1][t]` + +代码就可以写了。 + +```java +public int numDistinct(String s, String t) { + int s_len = s.length(); + int t_len = t.length(); + int[][] dp = new int[s_len + 1][t_len + 1]; + //当 T 为空串时,所有的 s 对应于 1 + for (int i = 0; i <= s_len; i++) { + dp[i][t_len] = 1; + } + + //倒着进行,T 每次增加一个字母 + for (int t_i = t_len - 1; t_i >= 0; t_i--) { + dp[s_len][t_i] = 0; // 这句可以省去,因为默认值是 0 + //倒着进行,S 每次增加一个字母 + for (int s_i = s_len - 1; s_i >= 0; s_i--) { + //如果当前字母相等 + if (t.charAt(t_i) == s.charAt(s_i)) { + //对应于两种情况,选择当前字母和不选择当前字母 + dp[s_i][t_i] = dp[s_i + 1][t_i + 1] + dp[s_i + 1][t_i]; + //如果当前字母不相等 + } else { + dp[s_i][t_i] = dp[s_i + 1][t_i]; + } + } + } + return dp[0][0]; +} +``` + +对比于解法一和解法二,如果`Memoization ` 技术我们不用`hash`,而是用一个二维数组,会发现其实我们的递归过程,其实就是在更新下图中的二维表,只不过更新的顺序没有动态规划这么归整。这也是不用`Memoization ` 技术会超时的原因,如果把递归的更新路线画出来,会发现很多路线重合了,意味着我们进行了很多没有必要的递归,从而造成了超时。 + +我们画一下动态规划的过程。 + +`S = "babgbag", T = "bag"` + +T 为空串时,所有的 s 对应于 1。 +S 为空串时,所有的 t 对应于 0。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/115_3.jpg) + +此时我们从 `dp[6][2]` 开始求。根据公式,因为当前字母相等,所以 `dp[6][2] = dp[7][3] + dp[7][2] = 1 + 0 = 1 。` + +接着求`dp[5][2]`,当前字母不相等,`dp[5][2] = dp[6][2] = 1`。 + +一直求下去。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/115_4.jpg) + +求当前问号的地方的值的时候,我们只需要它的上一个值和斜对角的值。 + +换句话讲,求当前列的时候,我们只需要上一列的信息。比如当前求第`1`列,第`3`列的值就不会用到了。 + +所以我们可以优化算法的空间复杂度,不需要二维数组,需要一维数组就够了。 + +此时需要解决一个问题,就是当求上图的`dp[1][1]`的时候,需要`dp[2][1]`和`dp[2][2]`的信息。但是如果我们是一维数组,`dp[2][1]`之前已经把`dp[2][2]`的信息覆盖掉了。所以我们需要一个`pre`变量保存之前的值。 + +```java +public int numDistinct(String s, String t) { + int s_len = s.length(); + int t_len = t.length(); + int[]dp = new int[s_len + 1]; + for (int i = 0; i <= s_len; i++) { + dp[i] = 1; + } + //倒着进行,T 每次增加一个字母 + for (int t_i = t_len - 1; t_i >= 0; t_i--) { + int pre = dp[s_len]; + dp[s_len] = 0; + //倒着进行,S 每次增加一个字母 + for (int s_i = s_len - 1; s_i >= 0; s_i--) { + int temp = dp[s_i]; + if (t.charAt(t_i) == s.charAt(s_i)) { + dp[s_i] = dp[s_i + 1] + pre; + } else { + dp[s_i] = dp[s_i + 1]; + } + pre = temp; + } + } + return dp[0]; +} +``` + +利用`temp`和`pre`两个变量实现了保存之前的值。 + +其实动态规划优化空间复杂度的思想,在 [5题](),[10题](),[53题](),[72题 ]()等等都已经用了,是非常经典的。 + +上边的动态规划是从字符串末尾倒着进行的,其实我们只要改变`dp`数组的含义,用`dp[m][n]`表示`S[0,m)`和`T[0,n)`,然后两层循环我们就可以从 `1` 往末尾进行了,思想是类似的,`leetcode` 高票答案也都是这样的,如果理解了上边的思想,代码其实也很好写。这里只分享下代码吧。 + +```java +public int numDistinct(String s, String t) { + int s_len = s.length(); + int t_len = t.length(); + int[] dp = new int[s_len + 1]; + for (int i = 0; i <= s_len; i++) { + dp[i] = 1; + } + for (int t_i = 1; t_i <= t_len; t_i++) { + int pre = dp[0]; + dp[0] = 0; + for (int s_i = 1; s_i <= s_len; s_i++) { + int temp = dp[s_i]; + if (t.charAt(t_i - 1) == s.charAt(s_i - 1)) { + dp[s_i] = dp[s_i - 1] + pre; + } else { + dp[s_i] = dp[s_i - 1]; + } + pre = temp; + } + } + return dp[s_len]; +} +``` + +# 总 + +这道题太经典了,从递归实现回溯,递归实现分治,`Memoization ` 技术对递归的优化,从递归转为动态规划再到动态规划空间复杂度的优化,一切都是理所当然,不需要什么特殊技巧,一切都是这么优雅,太棒了。 + +自己一开始是想到回溯的方法,然后卡到了超时的问题上,看了[这篇]() 和 [这篇]() 的题解后才恍然大悟,一切才都联通了,解法一、解法二、解法三其实本质都是在填充那个二维矩阵,最终殊途同归,不知为什么脑海中有宇宙大爆炸,然后万物产生联系的画面,2333。 + 这里自己需要吸取下教训,自己开始在回溯卡住了以后,思考了动态规划的方法,`dp`数组的含义已经定义出来了,想状态转移方程的时候在脑海里一直想,又卡住了。所以对于这种稍微复杂的动态规划还是拿纸出来画一画比较好。 \ No newline at end of file diff --git a/leetcode-116-Populating-Next-Right-Pointers-in-Each-Node.md b/leetcode-116-Populating-Next-Right-Pointers-in-Each-Node.md index ef191cfed..4695b16fb 100644 --- a/leetcode-116-Populating-Next-Right-Pointers-in-Each-Node.md +++ b/leetcode-116-Populating-Next-Right-Pointers-in-Each-Node.md @@ -1,138 +1,138 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/116.jpg) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/116_2.jpg) - -给定一个满二叉树,每个节点多了一个`next`指针,然后将所有的`next`指针指向它的右边的节点。并且要求空间复杂度是`O(1)`。 - -# 解法一 BFS - -如果没有要求空间复杂度这道题就简单多了,我们只需要用一个队列做`BFS`,`BFS`参见 [102 题]()。然后按顺序将每个节点连起来就可以了。 - -```java -public Node connect(Node root) { - if (root == null) { - return root; - } - Queue queue = new LinkedList(); - queue.offer(root); - while (!queue.isEmpty()) { - int size = queue.size(); - Node pre = null; - for (int i = 0; i < size; i++) { - Node cur = queue.poll(); - //从第二个节点开始,将前一个节点的 pre 指向当前节点 - if (i > 0) { - pre.next = cur; - } - pre = cur; - if (cur.left != null) { - queue.offer(cur.left); - } - if (cur.right != null) { - queue.offer(cur.right); - } - - } - } - return root; -} -``` - -# 解法二 迭代 - -当然既然题目要求了空间复杂度,那么我们来考虑下不用队列该怎么处理。只需要解决三个问题就够了。 - -* 每一层怎么遍历? - - 之前是用队列将下一层的节点保存了起来。 - - 这里的话,其实只需要提前把下一层的`next`构造完成,到了下一层的时候就可以遍历了。 - -* 什么时候进入下一层? - - 之前是得到当前队列的元素个数,然后遍历那么多次。 - - 这里的话,注意到最右边的节点的`next`为`null`,所以可以判断当前遍历的节点是不是`null`。 - -* 怎么得到每层开头节点? - - 之前队列把当前层的所以节点存了起来,得到开头节点当然很容易。 - - 这里的话,我们额外需要一个变量把它存起来。 - -三个问题都解决了,就可以写代码了。利用三个指针,`start` 指向每层的开始节点,`cur`指向当前遍历的节点,`pre`指向当前遍历的节点的前一个节点。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/116_3.jpg) - -如上图,我们需要把 `pre` 的左孩子的 `next` 指向右孩子,`pre` 的右孩子的`next`指向`cur`的左孩子。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/116_4.jpg) - -如上图,当 `cur` 指向 `null` 以后,我们只需要把 `pre` 的左孩子的 `next` 指向右孩子。 - -```java -public Node connect(Node root) { - if (root == null) { - return root; - } - Node pre = root; - Node cur = null; - Node start = pre; - while (pre.left != null) { - //遍历到了最右边的节点,要将 pre 和 cur 更新到下一层,并且用 start 记录 - if (cur == null) { - //我们只需要把 pre 的左孩子的 next 指向右孩子。 - pre.left.next = pre.right; - - pre = start.left; - cur = start.right; - start = pre; - //将下一层的 next 连起来,同时 pre、next 后移 - } else { - //把 pre 的左孩子的 next 指向右孩子 - pre.left.next = pre.right; - //pre 的右孩子的 next 指向 cur 的左孩子。 - pre.right.next = cur.left; - - pre = pre.next; - cur = cur.next; - } - } - return root; -} -``` - -分享下 `leetcode` 的高票回答的代码,看起来更简洁一些,`C++` 写的。 - -```C++ -void connect(TreeLinkNode *root) { - if (root == NULL) return; - TreeLinkNode *pre = root; - TreeLinkNode *cur = NULL; - while(pre->left) { - cur = pre; - while(cur) { - cur->left->next = cur->right; - if(cur->next) cur->right->next = cur->next->left; - cur = cur->next; - } - pre = pre->left; - } -} -``` - -我的代码里的变量和他的变量对应关系如下。 - -```java -我的 start pre cur - | | | -他的 pre cur cur.next -``` - -除了变量名不一样,算法本质还是一样的。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/116.jpg) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/116_2.jpg) + +给定一个满二叉树,每个节点多了一个`next`指针,然后将所有的`next`指针指向它的右边的节点。并且要求空间复杂度是`O(1)`。 + +# 解法一 BFS + +如果没有要求空间复杂度这道题就简单多了,我们只需要用一个队列做`BFS`,`BFS`参见 [102 题]()。然后按顺序将每个节点连起来就可以了。 + +```java +public Node connect(Node root) { + if (root == null) { + return root; + } + Queue queue = new LinkedList(); + queue.offer(root); + while (!queue.isEmpty()) { + int size = queue.size(); + Node pre = null; + for (int i = 0; i < size; i++) { + Node cur = queue.poll(); + //从第二个节点开始,将前一个节点的 pre 指向当前节点 + if (i > 0) { + pre.next = cur; + } + pre = cur; + if (cur.left != null) { + queue.offer(cur.left); + } + if (cur.right != null) { + queue.offer(cur.right); + } + + } + } + return root; +} +``` + +# 解法二 迭代 + +当然既然题目要求了空间复杂度,那么我们来考虑下不用队列该怎么处理。只需要解决三个问题就够了。 + +* 每一层怎么遍历? + + 之前是用队列将下一层的节点保存了起来。 + + 这里的话,其实只需要提前把下一层的`next`构造完成,到了下一层的时候就可以遍历了。 + +* 什么时候进入下一层? + + 之前是得到当前队列的元素个数,然后遍历那么多次。 + + 这里的话,注意到最右边的节点的`next`为`null`,所以可以判断当前遍历的节点是不是`null`。 + +* 怎么得到每层开头节点? + + 之前队列把当前层的所以节点存了起来,得到开头节点当然很容易。 + + 这里的话,我们额外需要一个变量把它存起来。 + +三个问题都解决了,就可以写代码了。利用三个指针,`start` 指向每层的开始节点,`cur`指向当前遍历的节点,`pre`指向当前遍历的节点的前一个节点。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/116_3.jpg) + +如上图,我们需要把 `pre` 的左孩子的 `next` 指向右孩子,`pre` 的右孩子的`next`指向`cur`的左孩子。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/116_4.jpg) + +如上图,当 `cur` 指向 `null` 以后,我们只需要把 `pre` 的左孩子的 `next` 指向右孩子。 + +```java +public Node connect(Node root) { + if (root == null) { + return root; + } + Node pre = root; + Node cur = null; + Node start = pre; + while (pre.left != null) { + //遍历到了最右边的节点,要将 pre 和 cur 更新到下一层,并且用 start 记录 + if (cur == null) { + //我们只需要把 pre 的左孩子的 next 指向右孩子。 + pre.left.next = pre.right; + + pre = start.left; + cur = start.right; + start = pre; + //将下一层的 next 连起来,同时 pre、next 后移 + } else { + //把 pre 的左孩子的 next 指向右孩子 + pre.left.next = pre.right; + //pre 的右孩子的 next 指向 cur 的左孩子。 + pre.right.next = cur.left; + + pre = pre.next; + cur = cur.next; + } + } + return root; +} +``` + +分享下 `leetcode` 的高票回答的代码,看起来更简洁一些,`C++` 写的。 + +```C++ +void connect(TreeLinkNode *root) { + if (root == NULL) return; + TreeLinkNode *pre = root; + TreeLinkNode *cur = NULL; + while(pre->left) { + cur = pre; + while(cur) { + cur->left->next = cur->right; + if(cur->next) cur->right->next = cur->next->left; + cur = cur->next; + } + pre = pre->left; + } +} +``` + +我的代码里的变量和他的变量对应关系如下。 + +```java +我的 start pre cur + | | | +他的 pre cur cur.next +``` + +除了变量名不一样,算法本质还是一样的。 + +# 总 + 题目让我们初始化 `next` 指针,初始化过程中我们又利用到了`next`指针,很巧妙了。 \ No newline at end of file diff --git a/leetcode-117-Populating-Next-Right-Pointers-in-Each-NodeII.md b/leetcode-117-Populating-Next-Right-Pointers-in-Each-NodeII.md index 244976edb..c175a3648 100644 --- a/leetcode-117-Populating-Next-Right-Pointers-in-Each-NodeII.md +++ b/leetcode-117-Populating-Next-Right-Pointers-in-Each-NodeII.md @@ -1,204 +1,204 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/117.jpg) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/117_2.jpg) - -给定一个二叉树,然后每个节点有一个 `next` 指针,将它指向它右边的节点。和 [116 题]() 基本一样,区别在于之前是满二叉树。 - -# 解法一 BFS - -直接把 [116 题]() 题的代码复制过来就好,一句也不用改。 - -利用一个栈将下一层的节点保存。通过`pre`指针把栈里的元素一个一个接起来。 - -```java -public Node connect(Node root) { - if (root == null) { - return root; - } - Queue queue = new LinkedList(); - queue.offer(root); - while (!queue.isEmpty()) { - int size = queue.size(); - Node pre = null; - for (int i = 0; i < size; i++) { - Node cur = queue.poll(); - if (i > 0) { - pre.next = cur; - } - pre = cur; - if (cur.left != null) { - queue.offer(cur.left); - } - if (cur.right != null) { - queue.offer(cur.right); - } - - } - } - return root; -} -``` - -# 解法二 - -当然题目要求了空间复杂度,可以先到 [116 题]() 看一下思路,这里在上边的基础上改一下。 - -我们用第二种简洁的代码,相对会好改一些。 - -```java -Node connect(Node root) { - if (root == null) - return root; - Node pre = root; - Node cur = null; - while (pre.left != null) { - cur = pre; - while (cur != null) { - cur.left.next = cur.right; - if (cur.next != null) { - cur.right.next = cur.next.left; - } - cur = cur.next; - } - pre = pre.left; - } - - return root; -} -``` - -需要解决的问题还是挺多的。 - -```java -cur.left.next = cur.right; -cur.right.next = cur.next.left; -``` - -之前的关键代码就是上边两句,但是在这道题中我们无法保证`cur.left` 或者 `cur.right` 或者 `cur.next.left`或者`cur.next.right` 是否为`null`。所以我们需要用一个`while`循环来保证当前节点至少有一个孩子。 - -```java -while (cur.left == null && cur.right == null) { - cur = cur.next; -} -``` - -这样的话保证了当前节点至少有一个孩子,然后如果一个孩子为 `null`,那么就可以保证另一个一定不为 `null` 了。 - -整体的话,就用了上边介绍的技巧,代码比较长,可以结合的看一下。 - -```java -Node connect(Node root) { - if (root == null) - return root; - Node pre = root; - Node cur = null; - while (true) { - cur = pre; - while (cur != null) { - //找到至少有一个孩子的节点 - if (cur.left == null && cur.right == null) { - cur = cur.next; - continue; - } - //找到当前节点的下一个至少有一个孩子的节点 - Node next = cur.next; - while (next != null && next.left == null && next.right == null) { - next = next.next; - if (next == null) { - break; - } - } - //当前节点的左右孩子都不为空,就将 left.next 指向 right - if (cur.left != null && cur.right != null) { - cur.left.next = cur.right; - } - //要接上 next 的节点的孩子,所以用 temp 处理当前节点 right 为 null 的情况 - Node temp = cur.right == null ? cur.left : cur.right; - - if (next != null) { - //next 左孩子不为 null,就接上左孩子。 - if (next.left != null) { - temp.next = next.left; - //next 左孩子为 null,就接上右孩子。 - } else { - temp.next = next.right; - } - } - - cur = cur.next; - } - //找到拥有孩子的节点 - while (pre.left == null && pre.right == null) { - pre = pre.next; - //都没有孩子说明已经是最后一层了 - if (pre == null) { - return root; - } - } - //进入下一层 - pre = pre.left != null ? pre.left : pre.right; - } -} -``` - -# 解法三 - -参考 [这里]()。 - -利用解法一的思想,我们利用 `pre` 指针,然后一个一个取节点,把它连起来。解法一为什么没有像解法二那样考虑当前节点为 `null` 呢?因为我们没有添加为 `null` 的节点,就是下边的代码的作用。 - -```java -if (cur.left != null) { - queue.offer(cur.left); -} -if (cur.right != null) { - queue.offer(cur.right); -} -``` - -所以这里是一样的,如果当前节点为`null`不处理就可以了。 - -第二个问题,怎么得到每次的开头的节点呢?我们用一个`dummy`指针,当连接第一个节点的时候,就将`dummy`指针指向他。此外,之前用的`pre`指针,把它当成`tail`指针可能会更好理解。如下图所示: - -![](https://windliang.oss-cn-beijing.aliyuncs.com/117_3.jpg) - -`cur` 指针利用 `next` 不停的遍历当前层。 - -如果 `cur` 的孩子不为 `null` 就将它接到 `tail` 后边,然后更新`tail`。 - -当 `cur` 为 `null` 的时候,再利用 `dummy` 指针得到新的一层的开始节点。 - -`dummy` 指针在链表中经常用到,他只是为了处理头结点的情况,它并不属于当前链表。 - -代码就异常的简单了。 - -```java -Node connect(Node root) { - Node cur = root; - while (cur != null) { - Node dummy = new Node(); - Node tail = dummy; - //遍历 cur 的当前层 - while (cur != null) { - if (cur.left != null) { - tail.next = cur.left; - tail = tail.next; - } - if (cur.right != null) { - tail.next = cur.right; - tail = tail.next; - } - cur = cur.next; - } - //更新 cur 到下一层 - cur = dummy.next; - } - return root; -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/117.jpg) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/117_2.jpg) + +给定一个二叉树,然后每个节点有一个 `next` 指针,将它指向它右边的节点。和 [116 题]() 基本一样,区别在于之前是满二叉树。 + +# 解法一 BFS + +直接把 [116 题]() 题的代码复制过来就好,一句也不用改。 + +利用一个栈将下一层的节点保存。通过`pre`指针把栈里的元素一个一个接起来。 + +```java +public Node connect(Node root) { + if (root == null) { + return root; + } + Queue queue = new LinkedList(); + queue.offer(root); + while (!queue.isEmpty()) { + int size = queue.size(); + Node pre = null; + for (int i = 0; i < size; i++) { + Node cur = queue.poll(); + if (i > 0) { + pre.next = cur; + } + pre = cur; + if (cur.left != null) { + queue.offer(cur.left); + } + if (cur.right != null) { + queue.offer(cur.right); + } + + } + } + return root; +} +``` + +# 解法二 + +当然题目要求了空间复杂度,可以先到 [116 题]() 看一下思路,这里在上边的基础上改一下。 + +我们用第二种简洁的代码,相对会好改一些。 + +```java +Node connect(Node root) { + if (root == null) + return root; + Node pre = root; + Node cur = null; + while (pre.left != null) { + cur = pre; + while (cur != null) { + cur.left.next = cur.right; + if (cur.next != null) { + cur.right.next = cur.next.left; + } + cur = cur.next; + } + pre = pre.left; + } + + return root; +} +``` + +需要解决的问题还是挺多的。 + +```java +cur.left.next = cur.right; +cur.right.next = cur.next.left; +``` + +之前的关键代码就是上边两句,但是在这道题中我们无法保证`cur.left` 或者 `cur.right` 或者 `cur.next.left`或者`cur.next.right` 是否为`null`。所以我们需要用一个`while`循环来保证当前节点至少有一个孩子。 + +```java +while (cur.left == null && cur.right == null) { + cur = cur.next; +} +``` + +这样的话保证了当前节点至少有一个孩子,然后如果一个孩子为 `null`,那么就可以保证另一个一定不为 `null` 了。 + +整体的话,就用了上边介绍的技巧,代码比较长,可以结合的看一下。 + +```java +Node connect(Node root) { + if (root == null) + return root; + Node pre = root; + Node cur = null; + while (true) { + cur = pre; + while (cur != null) { + //找到至少有一个孩子的节点 + if (cur.left == null && cur.right == null) { + cur = cur.next; + continue; + } + //找到当前节点的下一个至少有一个孩子的节点 + Node next = cur.next; + while (next != null && next.left == null && next.right == null) { + next = next.next; + if (next == null) { + break; + } + } + //当前节点的左右孩子都不为空,就将 left.next 指向 right + if (cur.left != null && cur.right != null) { + cur.left.next = cur.right; + } + //要接上 next 的节点的孩子,所以用 temp 处理当前节点 right 为 null 的情况 + Node temp = cur.right == null ? cur.left : cur.right; + + if (next != null) { + //next 左孩子不为 null,就接上左孩子。 + if (next.left != null) { + temp.next = next.left; + //next 左孩子为 null,就接上右孩子。 + } else { + temp.next = next.right; + } + } + + cur = cur.next; + } + //找到拥有孩子的节点 + while (pre.left == null && pre.right == null) { + pre = pre.next; + //都没有孩子说明已经是最后一层了 + if (pre == null) { + return root; + } + } + //进入下一层 + pre = pre.left != null ? pre.left : pre.right; + } +} +``` + +# 解法三 + +参考 [这里]()。 + +利用解法一的思想,我们利用 `pre` 指针,然后一个一个取节点,把它连起来。解法一为什么没有像解法二那样考虑当前节点为 `null` 呢?因为我们没有添加为 `null` 的节点,就是下边的代码的作用。 + +```java +if (cur.left != null) { + queue.offer(cur.left); +} +if (cur.right != null) { + queue.offer(cur.right); +} +``` + +所以这里是一样的,如果当前节点为`null`不处理就可以了。 + +第二个问题,怎么得到每次的开头的节点呢?我们用一个`dummy`指针,当连接第一个节点的时候,就将`dummy`指针指向他。此外,之前用的`pre`指针,把它当成`tail`指针可能会更好理解。如下图所示: + +![](https://windliang.oss-cn-beijing.aliyuncs.com/117_3.jpg) + +`cur` 指针利用 `next` 不停的遍历当前层。 + +如果 `cur` 的孩子不为 `null` 就将它接到 `tail` 后边,然后更新`tail`。 + +当 `cur` 为 `null` 的时候,再利用 `dummy` 指针得到新的一层的开始节点。 + +`dummy` 指针在链表中经常用到,他只是为了处理头结点的情况,它并不属于当前链表。 + +代码就异常的简单了。 + +```java +Node connect(Node root) { + Node cur = root; + while (cur != null) { + Node dummy = new Node(); + Node tail = dummy; + //遍历 cur 的当前层 + while (cur != null) { + if (cur.left != null) { + tail.next = cur.left; + tail = tail.next; + } + if (cur.right != null) { + tail.next = cur.right; + tail = tail.next; + } + cur = cur.next; + } + //更新 cur 到下一层 + cur = dummy.next; + } + return root; +} +``` + +# 总 + 本来为了图方便,在 [116 题]() 的基础上把解法二改了出来,还搞了蛮久,因为为 `null` 的情况太多了,不停的报空指针异常,最后终于理清了思路。但和解法三比起来实在是相形见绌了,解法三太优雅了,但其实这才是正常的思路,从解法一的做法产生灵感,利用 `tail` 指针将它们连起来。 \ No newline at end of file diff --git a/leetcode-118-Pascal's-Triangle.md b/leetcode-118-Pascal's-Triangle.md index f667f5cff..044920d73 100644 --- a/leetcode-118-Pascal's-Triangle.md +++ b/leetcode-118-Pascal's-Triangle.md @@ -1,33 +1,33 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/118.jpg) - -其实就是杨辉三角,当前元素等于上一层的两个元素的和。 - -# 解法一 - -用两层循环,注意一下我们下标是从 0 开始还是从 1 开始,然后就可以写出来了。 - -```java -public List> generate(int numRows) { - List> ans = new ArrayList<>(); - for (int i = 0; i < numRows; i++) { - List sub = new ArrayList<>(); - for (int j = 0; j <= i; j++) { - if (j == 0 || j == i) { - sub.add(1); - } else { - List last = ans.get(i - 1); - sub.add(last.get(j - 1) + last.get(j)); - } - - } - ans.add(sub); - } - return ans; -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/118.jpg) + +其实就是杨辉三角,当前元素等于上一层的两个元素的和。 + +# 解法一 + +用两层循环,注意一下我们下标是从 0 开始还是从 1 开始,然后就可以写出来了。 + +```java +public List> generate(int numRows) { + List> ans = new ArrayList<>(); + for (int i = 0; i < numRows; i++) { + List sub = new ArrayList<>(); + for (int j = 0; j <= i; j++) { + if (j == 0 || j == i) { + sub.add(1); + } else { + List last = ans.get(i - 1); + sub.add(last.get(j - 1) + last.get(j)); + } + + } + ans.add(sub); + } + return ans; +} +``` + +# 总 + 好像有一段时间没有碰到简单题了,哈哈。 \ No newline at end of file diff --git a/leetcode-119-Pascal's-TriangleII.md b/leetcode-119-Pascal's-TriangleII.md index 49740c91d..d45a561c2 100644 --- a/leetcode-119-Pascal's-TriangleII.md +++ b/leetcode-119-Pascal's-TriangleII.md @@ -1,129 +1,129 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/119.jpg) - -和 [118 题]() 一样,依旧是杨辉三角。区别在于之前是输出所有层的数,这道题只需要输出第 `k` 层的数。 - -# 解法一 - -和 [118 题]() 一样,我们只需要一层一层的求。但是不需要把每一层的结果都保存起来,只需要保存上一层的结果,就可以求出当前层的结果了。 - -```java -public List getRow(int rowIndex) { - List pre = new ArrayList<>(); - List cur = new ArrayList<>(); - for (int i = 0; i <= rowIndex; i++) { - cur = new ArrayList<>(); - for (int j = 0; j <= i; j++) { - if (j == 0 || j == i) { - cur.add(1); - } else { - cur.add(pre.get(j - 1) + pre.get(j)); - } - } - pre = cur; - } - return cur; -} -``` - -参考 [这里](),其实我们可以优化一下,我们可以把 `pre` 的 `List` 省去。 - -这样的话,`cur`每次不去新建 `List`,而是把`cur`当作`pre`。 - -又因为更新当前`j`的时候,就把之前`j`的信息覆盖掉了。而更新 `j + 1` 的时候又需要之前`j`的信息,所以在更新前,我们需要一个变量把之前`j`的信息保存起来。 - -```java -public List getRow(int rowIndex) { - int pre = 1; - List cur = new ArrayList<>(); - cur.add(1); - for (int i = 1; i <= rowIndex; i++) { - for (int j = 1; j < i; j++) { - int temp = cur.get(j); - cur.set(j, pre + cur.get(j)); - pre = temp; - } - cur.add(1); - } - return cur; -} -``` - -区别在于我们用了 `set` 函数来修改值,由于当前层比上一层多一个元素,所以对于最后一层的元素如果用 `set` 方法的话会造成越界。此外,每层的第一个元素始终为`1`。基于这两点,我们把之前`j == 0 || j == i`的情况移到了`for`循环外进行处理。 - -除了上边优化的思路,还有一种想法,那就是倒着进行,这样就不会存在覆盖的情况了。 - -因为更新完`j`的信息后,虽然把`j`之前的信息覆盖掉了。但是下一次我们更新的是`j - 1`,需要的是`j - 1`和`j - 2` 的信息,`j`信息覆盖就不会造成影响了。 - -```java -public List getRow(int rowIndex) { - int pre = 1; - List cur = new ArrayList<>(); - cur.add(1); - for (int i = 1; i <= rowIndex; i++) { - for (int j = i - 1; j > 0; j--) { - cur.set(j, cur.get(j - 1) + cur.get(j)); - } - cur.add(1);//补上每层的最后一个 1 - } - return cur; -} -``` - -# 解法二 公式法 - -如果熟悉杨辉三角,应该记得杨辉三角其实可以看做由组合数构成。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/119_2.jpg) - -根据组合数的公式,将`(n-k)!`约掉,化简就是下边的结果。 - -$$C^k_n = n!/(k!(n-k)!) = (n*(n-1)*(n-2)*...(n-k+1))/k!$$ - -然后我们就可以利用组合数解决这道题。 - -```java -public List getRow(int rowIndex) { - List ans = new ArrayList<>(); - int N = rowIndex; - for (int k = 0; k <= N; k++) { - ans.add(Combination(N, k)); - } - return ans; -} - -private int Combination(int N, int k) { - long res = 1; - for (int i = 1; i <= k; i++) - res = res * (N - k + i) / i; - return (int) res; -} -``` - -参考 [这里](),我们可以优化一下。 - -上边的算法对于每个组合数我们都重新求了一遍,但事实上前后的组合数其实是有联系的。 - -$$C_n^k=C_n^{k-1}\times(n-k+1)/k $$ - -代码的话,我们只需要用`pre`变量保存上一次的组合数结果。计算过程中,可能越界,所以用到了`long`。 - -```java -public List getRow(int rowIndex) { - List ans = new ArrayList<>(); - int N = rowIndex; - long pre = 1; - ans.add(1); - for (int k = 1; k <= N; k++) { - long cur = pre * (N - k + 1) / k; - ans.add((int) cur); - pre = cur; - } - return ans; -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/119.jpg) + +和 [118 题]() 一样,依旧是杨辉三角。区别在于之前是输出所有层的数,这道题只需要输出第 `k` 层的数。 + +# 解法一 + +和 [118 题]() 一样,我们只需要一层一层的求。但是不需要把每一层的结果都保存起来,只需要保存上一层的结果,就可以求出当前层的结果了。 + +```java +public List getRow(int rowIndex) { + List pre = new ArrayList<>(); + List cur = new ArrayList<>(); + for (int i = 0; i <= rowIndex; i++) { + cur = new ArrayList<>(); + for (int j = 0; j <= i; j++) { + if (j == 0 || j == i) { + cur.add(1); + } else { + cur.add(pre.get(j - 1) + pre.get(j)); + } + } + pre = cur; + } + return cur; +} +``` + +参考 [这里](),其实我们可以优化一下,我们可以把 `pre` 的 `List` 省去。 + +这样的话,`cur`每次不去新建 `List`,而是把`cur`当作`pre`。 + +又因为更新当前`j`的时候,就把之前`j`的信息覆盖掉了。而更新 `j + 1` 的时候又需要之前`j`的信息,所以在更新前,我们需要一个变量把之前`j`的信息保存起来。 + +```java +public List getRow(int rowIndex) { + int pre = 1; + List cur = new ArrayList<>(); + cur.add(1); + for (int i = 1; i <= rowIndex; i++) { + for (int j = 1; j < i; j++) { + int temp = cur.get(j); + cur.set(j, pre + cur.get(j)); + pre = temp; + } + cur.add(1); + } + return cur; +} +``` + +区别在于我们用了 `set` 函数来修改值,由于当前层比上一层多一个元素,所以对于最后一层的元素如果用 `set` 方法的话会造成越界。此外,每层的第一个元素始终为`1`。基于这两点,我们把之前`j == 0 || j == i`的情况移到了`for`循环外进行处理。 + +除了上边优化的思路,还有一种想法,那就是倒着进行,这样就不会存在覆盖的情况了。 + +因为更新完`j`的信息后,虽然把`j`之前的信息覆盖掉了。但是下一次我们更新的是`j - 1`,需要的是`j - 1`和`j - 2` 的信息,`j`信息覆盖就不会造成影响了。 + +```java +public List getRow(int rowIndex) { + int pre = 1; + List cur = new ArrayList<>(); + cur.add(1); + for (int i = 1; i <= rowIndex; i++) { + for (int j = i - 1; j > 0; j--) { + cur.set(j, cur.get(j - 1) + cur.get(j)); + } + cur.add(1);//补上每层的最后一个 1 + } + return cur; +} +``` + +# 解法二 公式法 + +如果熟悉杨辉三角,应该记得杨辉三角其实可以看做由组合数构成。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/119_2.jpg) + +根据组合数的公式,将`(n-k)!`约掉,化简就是下边的结果。 + +$$C^k_n = n!/(k!(n-k)!) = (n*(n-1)*(n-2)*...(n-k+1))/k!$$ + +然后我们就可以利用组合数解决这道题。 + +```java +public List getRow(int rowIndex) { + List ans = new ArrayList<>(); + int N = rowIndex; + for (int k = 0; k <= N; k++) { + ans.add(Combination(N, k)); + } + return ans; +} + +private int Combination(int N, int k) { + long res = 1; + for (int i = 1; i <= k; i++) + res = res * (N - k + i) / i; + return (int) res; +} +``` + +参考 [这里](),我们可以优化一下。 + +上边的算法对于每个组合数我们都重新求了一遍,但事实上前后的组合数其实是有联系的。 + +$$C_n^k=C_n^{k-1}\times(n-k+1)/k $$ + +代码的话,我们只需要用`pre`变量保存上一次的组合数结果。计算过程中,可能越界,所以用到了`long`。 + +```java +public List getRow(int rowIndex) { + List ans = new ArrayList<>(); + int N = rowIndex; + long pre = 1; + ans.add(1); + for (int k = 1; k <= N; k++) { + long cur = pre * (N - k + 1) / k; + ans.add((int) cur); + pre = cur; + } + return ans; +} +``` + +# 总 + 这道题其实还是比较简单的,只是优化的两种方法是比较常用的,一种就是用`pre`变量将要被覆盖的变量存起来,另一种就是倒着进行。另外求组合数的时候,要防止`int`的溢出。 \ No newline at end of file diff --git a/leetcode-120-Triangle.md b/leetcode-120-Triangle.md index 26ed51979..01daef1fb 100644 --- a/leetcode-120-Triangle.md +++ b/leetcode-120-Triangle.md @@ -1,144 +1,144 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/120.jpg) - -当前层只能选择下一层相邻的两个元素走,比如第 `3` 层的 `5` 只能选择第`4`层的 `1` 和 `8` ,从最上边开始,走一条路径,走到最底层最小的和是多少。 - -# 题目解析 - -先看一下 [115 题]() 吧,和这道题思路方法是完全完全一样的。此外,[119 题]() 倒着循环优化空间复杂度也可以看一下。 - -这道题本质上就是动态规划,再本质一些就是更新一张二维表。 - - [115 题]() 已经进行了详细介绍,这里就粗略的记录了。 - -# 解法一 递归之分治 - -求第 `0` 层到第 `n` 层的和最小,就是第`0`层的数字加上第`1`层到第`n`层的的最小和。 - -递归出口就是,第`n`层到第`n`层最小的和,就是该层的数字本身。 - -```java -public int minimumTotal(List> triangle) { - return minimumTotalHelper(0, 0, triangle); -} - -private int minimumTotalHelper(int row, int col, List> triangle) { - if (row == triangle.size()) { - return 0; - } - int min = Integer.MAX_VALUE; - List cur = triangle.get(row); - min = Math.min(min, cur.get(col) + minimumTotalHelper(row + 1, col, triangle)); - if (col + 1 < cur.size()) { - min = Math.min(min, cur.get(col + 1) + minimumTotalHelper(row + 1, col + 1, triangle)); - } - return min; -} -``` - -因为函数里边调用了两次自己,所以导致进行了很多重复的搜索,所以肯定会导致超时。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/120_2.jpg) - -优化的话,就是 `Memoization` 技术,把每次的结果存起来,进入递归前先判断当前解有没有求出来。我们可以用 `HashMap` 存,也可以用二维数组存。 - -用 `HashMap` 的话,`key` 存字符串 `row + "@" + col`,中间之所以加一个分隔符,就是防止`row = 1,col = 23` 和 `row = 12, col = 3`,这两种情况的混淆。 - -```java -public int minimumTotal(List> triangle) { - HashMap map = new HashMap<>(); - return minimumTotalHelper(0, 0, triangle, map); -} - -private int minimumTotalHelper(int row, int col, List> triangle, HashMap map) { - if (row == triangle.size()) { - return 0; - } - String key = row + "@" + col; - if (map.containsKey(key)) { - return map.get(key); - } - int min = Integer.MAX_VALUE; - List cur = triangle.get(row); - min = Math.min(min, cur.get(col) + minimumTotalHelper(row + 1, col, triangle, map)); - if (col + 1 < cur.size()) { - min = Math.min(min, cur.get(col + 1) + minimumTotalHelper(row + 1, col + 1, triangle, map)); - } - map.put(key, min); - return min; -} -``` - -# 动态规划 - -动态规划可以自顶向下,也可以自底向上, [115 题]() 主要写的是自底向上,这里写个自顶向下吧。 - -用一个数组 `dp[row][col]` 表示从顶部到当前位置,即第 `row` 行第 `col` 列,的最小和。 - -状态转移方程也很好写了。 - -`dp[row][col] = Min(dp[row - 1][col - 1],dp[row-1][col]), triangle[row][col] ` - -到当前位置有两种选择,选一个较小的,然后加上当前位置的数字即可。 - -```java -public int minimumTotal(List> triangle) { - int rows = triangle.size(); - int cols = triangle.get(rows - 1).size(); - int[][] dp = new int[rows][cols]; - dp[0][0] = triangle.get(0).get(0); - for (int row = 1; row < rows; row++) { - List curRow = triangle.get(row); - int col = 0; - dp[row][col] = dp[row - 1][col] + curRow.get(col); - col++; - for (; col < curRow.size() - 1; col++) { - dp[row][col] = Math.min(dp[row - 1][col - 1], dp[row - 1][col]) + curRow.get(col); - } - dp[row][col] = dp[row - 1][col - 1] + curRow.get(col); - } - int min = Integer.MAX_VALUE; - for (int col = 0; col < cols; col++) { - min = Math.min(min, dp[rows - 1][col]); - } - return min; -} -``` - -注意的地方就是把左边界和右边界的情况单独考虑,因为到达左边界和右边界只有一个位置可选。 - -接下来,注意到我们是一层一层的更新,更新当前层只需要上一层的信息,所以我们不需要二维数组,只需要一维数组就可以了。 - -另外,和 [119 题]() 题一样,更新`col`列的时候,会把之前`col`列的信息覆盖。当更新 `col + 1` 列的时候,旧的 `col` 列的信息已经没有了,所以我们可以采取倒着更新 `col` 的方法。 - -```java -public int minimumTotal(List> triangle) { - int rows = triangle.size(); - int cols = triangle.get(rows - 1).size(); - int[] dp = new int[cols]; - dp[0] = triangle.get(0).get(0); - for (int row = 1; row < rows; row++) { - List curRow = triangle.get(row); - int col = curRow.size() - 1; - dp[col] = dp[col - 1] + curRow.get(col); - col--; - for (; col > 0; col--) { - dp[col] = Math.min(dp[col - 1], dp[col]) + curRow.get(col); - } - - dp[col] = dp[col] + curRow.get(col); - } - int min = Integer.MAX_VALUE; - for (int col = 0; col < cols; col++) { - min = Math.min(min, dp[col]); - } - return min; -} -``` - -另外,大家可以试一试自底向上的方法,写起来还相对简单些。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/120.jpg) + +当前层只能选择下一层相邻的两个元素走,比如第 `3` 层的 `5` 只能选择第`4`层的 `1` 和 `8` ,从最上边开始,走一条路径,走到最底层最小的和是多少。 + +# 题目解析 + +先看一下 [115 题]() 吧,和这道题思路方法是完全完全一样的。此外,[119 题]() 倒着循环优化空间复杂度也可以看一下。 + +这道题本质上就是动态规划,再本质一些就是更新一张二维表。 + + [115 题]() 已经进行了详细介绍,这里就粗略的记录了。 + +# 解法一 递归之分治 + +求第 `0` 层到第 `n` 层的和最小,就是第`0`层的数字加上第`1`层到第`n`层的的最小和。 + +递归出口就是,第`n`层到第`n`层最小的和,就是该层的数字本身。 + +```java +public int minimumTotal(List> triangle) { + return minimumTotalHelper(0, 0, triangle); +} + +private int minimumTotalHelper(int row, int col, List> triangle) { + if (row == triangle.size()) { + return 0; + } + int min = Integer.MAX_VALUE; + List cur = triangle.get(row); + min = Math.min(min, cur.get(col) + minimumTotalHelper(row + 1, col, triangle)); + if (col + 1 < cur.size()) { + min = Math.min(min, cur.get(col + 1) + minimumTotalHelper(row + 1, col + 1, triangle)); + } + return min; +} +``` + +因为函数里边调用了两次自己,所以导致进行了很多重复的搜索,所以肯定会导致超时。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/120_2.jpg) + +优化的话,就是 `Memoization` 技术,把每次的结果存起来,进入递归前先判断当前解有没有求出来。我们可以用 `HashMap` 存,也可以用二维数组存。 + +用 `HashMap` 的话,`key` 存字符串 `row + "@" + col`,中间之所以加一个分隔符,就是防止`row = 1,col = 23` 和 `row = 12, col = 3`,这两种情况的混淆。 + +```java +public int minimumTotal(List> triangle) { + HashMap map = new HashMap<>(); + return minimumTotalHelper(0, 0, triangle, map); +} + +private int minimumTotalHelper(int row, int col, List> triangle, HashMap map) { + if (row == triangle.size()) { + return 0; + } + String key = row + "@" + col; + if (map.containsKey(key)) { + return map.get(key); + } + int min = Integer.MAX_VALUE; + List cur = triangle.get(row); + min = Math.min(min, cur.get(col) + minimumTotalHelper(row + 1, col, triangle, map)); + if (col + 1 < cur.size()) { + min = Math.min(min, cur.get(col + 1) + minimumTotalHelper(row + 1, col + 1, triangle, map)); + } + map.put(key, min); + return min; +} +``` + +# 动态规划 + +动态规划可以自顶向下,也可以自底向上, [115 题]() 主要写的是自底向上,这里写个自顶向下吧。 + +用一个数组 `dp[row][col]` 表示从顶部到当前位置,即第 `row` 行第 `col` 列,的最小和。 + +状态转移方程也很好写了。 + +`dp[row][col] = Min(dp[row - 1][col - 1],dp[row-1][col]), triangle[row][col] ` + +到当前位置有两种选择,选一个较小的,然后加上当前位置的数字即可。 + +```java +public int minimumTotal(List> triangle) { + int rows = triangle.size(); + int cols = triangle.get(rows - 1).size(); + int[][] dp = new int[rows][cols]; + dp[0][0] = triangle.get(0).get(0); + for (int row = 1; row < rows; row++) { + List curRow = triangle.get(row); + int col = 0; + dp[row][col] = dp[row - 1][col] + curRow.get(col); + col++; + for (; col < curRow.size() - 1; col++) { + dp[row][col] = Math.min(dp[row - 1][col - 1], dp[row - 1][col]) + curRow.get(col); + } + dp[row][col] = dp[row - 1][col - 1] + curRow.get(col); + } + int min = Integer.MAX_VALUE; + for (int col = 0; col < cols; col++) { + min = Math.min(min, dp[rows - 1][col]); + } + return min; +} +``` + +注意的地方就是把左边界和右边界的情况单独考虑,因为到达左边界和右边界只有一个位置可选。 + +接下来,注意到我们是一层一层的更新,更新当前层只需要上一层的信息,所以我们不需要二维数组,只需要一维数组就可以了。 + +另外,和 [119 题]() 题一样,更新`col`列的时候,会把之前`col`列的信息覆盖。当更新 `col + 1` 列的时候,旧的 `col` 列的信息已经没有了,所以我们可以采取倒着更新 `col` 的方法。 + +```java +public int minimumTotal(List> triangle) { + int rows = triangle.size(); + int cols = triangle.get(rows - 1).size(); + int[] dp = new int[cols]; + dp[0] = triangle.get(0).get(0); + for (int row = 1; row < rows; row++) { + List curRow = triangle.get(row); + int col = curRow.size() - 1; + dp[col] = dp[col - 1] + curRow.get(col); + col--; + for (; col > 0; col--) { + dp[col] = Math.min(dp[col - 1], dp[col]) + curRow.get(col); + } + + dp[col] = dp[col] + curRow.get(col); + } + int min = Integer.MAX_VALUE; + for (int col = 0; col < cols; col++) { + min = Math.min(min, dp[col]); + } + return min; +} +``` + +另外,大家可以试一试自底向上的方法,写起来还相对简单些。 + +# 总 + 就是 [115 题]() 的变形了,没有新东西,如果理解了 [115 题]() ,那么这道题直接套算法就行,基本不用思考了。 \ No newline at end of file diff --git a/leetcode-121-Best-Time-to-Buy-and-Sell-Stock.md b/leetcode-121-Best-Time-to-Buy-and-Sell-Stock.md index 10ba631c3..08e74c510 100644 --- a/leetcode-121-Best-Time-to-Buy-and-Sell-Stock.md +++ b/leetcode-121-Best-Time-to-Buy-and-Sell-Stock.md @@ -1,166 +1,166 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/121.jpg) - -给一个数组,看作每天股票的价格,然后某一天买入,某一天卖出,最大收益可以是多少。可以不操作,收入就是 `0`。 - -# 解法一 暴力破解 - -先写个暴力的,看看对题目的理解对不对。用两个循环,外层循环表示买入时候的价格,内层循环表示卖出时候的价格,遍历所有的情况,期间更新最大的收益。 - -```java -public int maxProfit(int[] prices) { - int maxProfit = 0; - for (int i = 0; i < prices.length; i++) { - for (int j = i + 1; j < prices.length; j++) { - maxProfit = Math.max(maxProfit, prices[j] - prices[i]); - } - } - return maxProfit; -} -``` - -# 解法二 双指针 - -这种数组优化,经常就是考虑双指针的方法,从而使得两层循环变成一层。思考一下怎么定义指针的含义。 - -```java -用两个指针, buy 表示第几天买入,sell 表示第几天卖出 -开始 buy,sell 都指向 0,表示不操作 -3 6 7 2 9 -^ -b -^ -s - -sell 后移表示这天卖出,计算收益是 6 - 3 = 3 -3 6 7 2 9 -^ ^ -b s - - -sell 后移表示这天卖出,计算收益是 7 - 3 = 4 -3 6 7 2 9 -^ ^ -b s - -sell 后移表示这天卖出,计算收益是 2 - 3 = -1 -3 6 7 2 9 12 -^ ^ -b s - -此外,如上图,当前 sell 指向的价格小于了我们买入的价格,所以我们要把 buy 指向当前 sell 才会获得更大的收益 -原因很简单,收益的价格等于 prices[sell] - prices[buy],buy 指向 sell 会使得减数更小, -所以肯定要选更小的 buy -3 6 7 2 9 12 - ^ - s - ^ - b - - -sell 后移表示这天卖出,计算收益是 9 - 2 = 7 -这里也可以看出来减数从之前的 3 变成了 2,所以收益会更大 -3 6 7 2 9 12 - ^ ^ - b s - -sell 后移表示这天卖出,计算收益是 12 - 2 = 10 -3 6 7 2 9 12 - ^ ^ - b s - -然后在这些价格里选最大的就可以了。 -``` - -代码的话就很好写了。 - -```java -public int maxProfit(int[] prices) { - int maxProfit = 0; - int buy = 0; - int sell = 0; - for (; sell < prices.length; sell++) { - //当前价格更小了,更新 buy - if (prices[sell] < prices[buy]) { - buy = sell; - } else { - maxProfit = Math.max(maxProfit, prices[sell] - prices[buy]); - - } - } - return maxProfit; -} -``` - -# 解法三 - -参考下边的链接。 - -https://leetcode.com/problems/best-time-to-buy-and-sell-stock/discuss/39038/Kadane's-Algorithm-Since-no-one-has-mentioned-about-this-so-far-%3A)-(In-case-if-interviewer-twists-the-input) - -一个很新的角度,先回忆一下 [53 题](),求子序列最大的和。 - -![img](https://windliang.oss-cn-beijing.aliyuncs.com/53.jpg) - -当时的解法二,用动态规划, - -用一个一维数组 `dp [ i ]` 表示以下标 `i` 结尾的子数组的元素的最大的和,也就是这个子数组最后一个元素是下边为 `i` 的元素,并且这个子数组是所有以 `i `结尾的子数组中,和最大的。 - -这样的话就有两种情况, - -- 如果 `dp [ i - 1 ] < 0`,那么 `dp [ i ] = nums [ i ]`。 -- 如果 `dp [ i - 1 ] >= 0`,那么 `dp [ i ] = dp [ i - 1 ] + nums [ i ]`。 - -直接放一下最后经过优化后的代码,具体的可以过去 [看一下]()。 - -```java -public int maxSubArray(int[] nums) { - int n = nums.length; - int dp = nums[0]; - int max = nums[0]; - for (int i = 1; i < n; i++) { - dp= Math.max(dp + nums[i],nums[i]); - max = Math.max(max, dp); - } - return max; -} -``` - -而对于这道题我们可以转换成上边的问题。 - -对于数组 ` 1 6 2 8`,代表股票每天的价格。 - -定义一下转换规则,当前天的价格减去前一天的价格,第一天由于没有前一天,规定为 `0`,用来代表不操作。 - -数组就转换为 `0 6-1 2-6 8-2`,也就是 `0 5 -4 6`。现在的数组的含义就变成了股票相对于前一天的变化了。 - -现在我们只需要找出连续的和最大是多少就可以了,也就是变成了 `53` 题。 - -连续的和比如对应第 3 到 第 6 天加起来的和,那对应的买入卖出其实就是第 `2` 天买入,第 `6` 天卖出。 - -换句话讲,买入卖出和连续的和形成了互相映射,所以问题转换成功。 - -代码在上边的基础上改一下就可以了。 - -```java -public int maxProfit(int[] prices) { - int n = prices.length; - int dp = 0; - int max = 0; - for (int i = 1; i < n; i++) { - int num = prices[i] - prices[i - 1]; - dp = Math.max(dp + num, num); - max = Math.max(max, dp); - } - return max; -} -``` - -而这个算法其实叫做 `Kadane` 算法,如果序列中含有负数,并且可以不选择任何一个数,那么最小的和也肯定是 `0`,也就是上边的情况,这也是把我们把第一天的浮动当作是 `0` 的原因。所以 `max `初始化成了 `0`。 - -更多`Kadane` 算法的介绍可以参考 [维基百科]()。 - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/121.jpg) + +给一个数组,看作每天股票的价格,然后某一天买入,某一天卖出,最大收益可以是多少。可以不操作,收入就是 `0`。 + +# 解法一 暴力破解 + +先写个暴力的,看看对题目的理解对不对。用两个循环,外层循环表示买入时候的价格,内层循环表示卖出时候的价格,遍历所有的情况,期间更新最大的收益。 + +```java +public int maxProfit(int[] prices) { + int maxProfit = 0; + for (int i = 0; i < prices.length; i++) { + for (int j = i + 1; j < prices.length; j++) { + maxProfit = Math.max(maxProfit, prices[j] - prices[i]); + } + } + return maxProfit; +} +``` + +# 解法二 双指针 + +这种数组优化,经常就是考虑双指针的方法,从而使得两层循环变成一层。思考一下怎么定义指针的含义。 + +```java +用两个指针, buy 表示第几天买入,sell 表示第几天卖出 +开始 buy,sell 都指向 0,表示不操作 +3 6 7 2 9 +^ +b +^ +s + +sell 后移表示这天卖出,计算收益是 6 - 3 = 3 +3 6 7 2 9 +^ ^ +b s + + +sell 后移表示这天卖出,计算收益是 7 - 3 = 4 +3 6 7 2 9 +^ ^ +b s + +sell 后移表示这天卖出,计算收益是 2 - 3 = -1 +3 6 7 2 9 12 +^ ^ +b s + +此外,如上图,当前 sell 指向的价格小于了我们买入的价格,所以我们要把 buy 指向当前 sell 才会获得更大的收益 +原因很简单,收益的价格等于 prices[sell] - prices[buy],buy 指向 sell 会使得减数更小, +所以肯定要选更小的 buy +3 6 7 2 9 12 + ^ + s + ^ + b + + +sell 后移表示这天卖出,计算收益是 9 - 2 = 7 +这里也可以看出来减数从之前的 3 变成了 2,所以收益会更大 +3 6 7 2 9 12 + ^ ^ + b s + +sell 后移表示这天卖出,计算收益是 12 - 2 = 10 +3 6 7 2 9 12 + ^ ^ + b s + +然后在这些价格里选最大的就可以了。 +``` + +代码的话就很好写了。 + +```java +public int maxProfit(int[] prices) { + int maxProfit = 0; + int buy = 0; + int sell = 0; + for (; sell < prices.length; sell++) { + //当前价格更小了,更新 buy + if (prices[sell] < prices[buy]) { + buy = sell; + } else { + maxProfit = Math.max(maxProfit, prices[sell] - prices[buy]); + + } + } + return maxProfit; +} +``` + +# 解法三 + +参考下边的链接。 + +https://leetcode.com/problems/best-time-to-buy-and-sell-stock/discuss/39038/Kadane's-Algorithm-Since-no-one-has-mentioned-about-this-so-far-%3A)-(In-case-if-interviewer-twists-the-input) + +一个很新的角度,先回忆一下 [53 题](),求子序列最大的和。 + +![img](https://windliang.oss-cn-beijing.aliyuncs.com/53.jpg) + +当时的解法二,用动态规划, + +用一个一维数组 `dp [ i ]` 表示以下标 `i` 结尾的子数组的元素的最大的和,也就是这个子数组最后一个元素是下边为 `i` 的元素,并且这个子数组是所有以 `i `结尾的子数组中,和最大的。 + +这样的话就有两种情况, + +- 如果 `dp [ i - 1 ] < 0`,那么 `dp [ i ] = nums [ i ]`。 +- 如果 `dp [ i - 1 ] >= 0`,那么 `dp [ i ] = dp [ i - 1 ] + nums [ i ]`。 + +直接放一下最后经过优化后的代码,具体的可以过去 [看一下]()。 + +```java +public int maxSubArray(int[] nums) { + int n = nums.length; + int dp = nums[0]; + int max = nums[0]; + for (int i = 1; i < n; i++) { + dp= Math.max(dp + nums[i],nums[i]); + max = Math.max(max, dp); + } + return max; +} +``` + +而对于这道题我们可以转换成上边的问题。 + +对于数组 ` 1 6 2 8`,代表股票每天的价格。 + +定义一下转换规则,当前天的价格减去前一天的价格,第一天由于没有前一天,规定为 `0`,用来代表不操作。 + +数组就转换为 `0 6-1 2-6 8-2`,也就是 `0 5 -4 6`。现在的数组的含义就变成了股票相对于前一天的变化了。 + +现在我们只需要找出连续的和最大是多少就可以了,也就是变成了 `53` 题。 + +连续的和比如对应第 3 到 第 6 天加起来的和,那对应的买入卖出其实就是第 `2` 天买入,第 `6` 天卖出。 + +换句话讲,买入卖出和连续的和形成了互相映射,所以问题转换成功。 + +代码在上边的基础上改一下就可以了。 + +```java +public int maxProfit(int[] prices) { + int n = prices.length; + int dp = 0; + int max = 0; + for (int i = 1; i < n; i++) { + int num = prices[i] - prices[i - 1]; + dp = Math.max(dp + num, num); + max = Math.max(max, dp); + } + return max; +} +``` + +而这个算法其实叫做 `Kadane` 算法,如果序列中含有负数,并且可以不选择任何一个数,那么最小的和也肯定是 `0`,也就是上边的情况,这也是把我们把第一天的浮动当作是 `0` 的原因。所以 `max `初始化成了 `0`。 + +更多`Kadane` 算法的介绍可以参考 [维基百科]()。 + +# 总 + 这道题虽然是比较简单的,但是双指针的用法还是经常见的。另外解法三对问题的转换是真的佩服了。 \ No newline at end of file diff --git a/leetcode-122-Best-Time-to-Buy-and-Sell-StockII.md b/leetcode-122-Best-Time-to-Buy-and-Sell-StockII.md index f33d826dc..8d255dcd4 100644 --- a/leetcode-122-Best-Time-to-Buy-and-Sell-StockII.md +++ b/leetcode-122-Best-Time-to-Buy-and-Sell-StockII.md @@ -1,102 +1,102 @@ - # 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/122.jpg) - -和 [121 题]() 一样,给定一个数组,代表每天的价格。区别在于 `121` 题只能进行一次买入卖出。但是这道题可以不停的买入、卖出,但是只有卖出了才能继续买入。 - -# 解法一 - -就用最简单的思想,我们穿越回去了过去,知道了未来每天的股票价格,要怎么操作呢? - -跌了的前一天卖出,例如下边的例子 - -```java -1 2 3 4 天 -2 7 8 5 - -第 4 天下跌,我们可以在前一天卖出,下跌当天再次买入,后边出现下跌,前一天继续卖出 -``` - -需要考虑两种特殊情况 - -一直上涨,没有下跌 - -```java -1 3 5 9 - -那么我们在最后一天卖出就可以 -``` - -第二天下跌 - -```java -8 7 9 10 - -下跌的时候我们本应该在前一天卖出,然而第一天只能买入并不能卖出,所以这种情况并不会带来收益 -``` - -考虑了上边的所有情况,就可以写代码了。 - -```java -public int maxProfit(int[] prices) { - int profit = 0; - int buy = 0; - int sell = 1; - - for (; sell < prices.length; sell++) { - //出现下跌 - if (prices[sell] < prices[sell - 1]) { - //不是第 2 天下跌,就前一天卖出,累计收益 - if (sell != 1) { - profit += prices[sell - 1] - prices[buy]; - } - //下跌当天再次买入 - buy = sell; - - //到最后一天是上涨,那就在最后一天卖出 - } else if (sell == prices.length - 1) { - profit += prices[sell] - prices[buy]; - } - } - return profit; -} -``` - -还有一种持续下跌的情况 - -```java -9 8 7 3 2 - -但是对于我们的代码,持续下跌的话,buy 和 sell - 1 就相等了,所以每次累计就是 0,不影响结果 -``` - -# 解法二 - -其实不用考虑那么多,再直接点,只要当前天相对于前一天上涨了,我们就前一天买入,当前天卖出。 - -```java -public int maxProfit(int[] prices) { - int profit = 0; - for (int i = 1; i < prices.length; i++) { - int sub = prices[i] - prices[i - 1]; - if (sub > 0) { - profit += sub; - } - } - return profit; -} -``` - -# 总 - -上边两种解法都是从实际情况出发,来考虑怎么盈利最大。[官方]() 给出的理解方式也很好,这里分享一下。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/122_2.jpg) - -两种解法其实都可以抽象到上边的图中。 - -解法一,其实每次就是找了波谷和波峰做了差,然后把所有的差进行累计。 - -解法二,找的是上升的折线段,把所有上升的折线段的高度进行了累计。 - + # 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/122.jpg) + +和 [121 题]() 一样,给定一个数组,代表每天的价格。区别在于 `121` 题只能进行一次买入卖出。但是这道题可以不停的买入、卖出,但是只有卖出了才能继续买入。 + +# 解法一 + +就用最简单的思想,我们穿越回去了过去,知道了未来每天的股票价格,要怎么操作呢? + +跌了的前一天卖出,例如下边的例子 + +```java +1 2 3 4 天 +2 7 8 5 + +第 4 天下跌,我们可以在前一天卖出,下跌当天再次买入,后边出现下跌,前一天继续卖出 +``` + +需要考虑两种特殊情况 + +一直上涨,没有下跌 + +```java +1 3 5 9 + +那么我们在最后一天卖出就可以 +``` + +第二天下跌 + +```java +8 7 9 10 + +下跌的时候我们本应该在前一天卖出,然而第一天只能买入并不能卖出,所以这种情况并不会带来收益 +``` + +考虑了上边的所有情况,就可以写代码了。 + +```java +public int maxProfit(int[] prices) { + int profit = 0; + int buy = 0; + int sell = 1; + + for (; sell < prices.length; sell++) { + //出现下跌 + if (prices[sell] < prices[sell - 1]) { + //不是第 2 天下跌,就前一天卖出,累计收益 + if (sell != 1) { + profit += prices[sell - 1] - prices[buy]; + } + //下跌当天再次买入 + buy = sell; + + //到最后一天是上涨,那就在最后一天卖出 + } else if (sell == prices.length - 1) { + profit += prices[sell] - prices[buy]; + } + } + return profit; +} +``` + +还有一种持续下跌的情况 + +```java +9 8 7 3 2 + +但是对于我们的代码,持续下跌的话,buy 和 sell - 1 就相等了,所以每次累计就是 0,不影响结果 +``` + +# 解法二 + +其实不用考虑那么多,再直接点,只要当前天相对于前一天上涨了,我们就前一天买入,当前天卖出。 + +```java +public int maxProfit(int[] prices) { + int profit = 0; + for (int i = 1; i < prices.length; i++) { + int sub = prices[i] - prices[i - 1]; + if (sub > 0) { + profit += sub; + } + } + return profit; +} +``` + +# 总 + +上边两种解法都是从实际情况出发,来考虑怎么盈利最大。[官方]() 给出的理解方式也很好,这里分享一下。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/122_2.jpg) + +两种解法其实都可以抽象到上边的图中。 + +解法一,其实每次就是找了波谷和波峰做了差,然后把所有的差进行累计。 + +解法二,找的是上升的折线段,把所有上升的折线段的高度进行了累计。 + 所以一些题,可能代码是一样的,但是理解的含义并不相同。 \ No newline at end of file diff --git a/leetcode-123-Best-Time-to-Buy-and-Sell-StockIII.md b/leetcode-123-Best-Time-to-Buy-and-Sell-StockIII.md index cb38e70b9..0eff42bb9 100644 --- a/leetcode-123-Best-Time-to-Buy-and-Sell-StockIII.md +++ b/leetcode-123-Best-Time-to-Buy-and-Sell-StockIII.md @@ -1,228 +1,228 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/123.jpg) - -依旧是买卖股票的延伸,但比 [121 题]() , [122 题]() 难度高了不少。这道题的意思是,给一个数组代表股票每天的价格。你最多可以买入卖出两次,但只有卖出了才可以再次买入,求出最大的收益是多少。 - -# 解法一 - -参考 [这里]()。 - -开始的想法是求出收益第一高和第二高的两次买卖,然后加起来。对于普通的情况是可以解决的,但是对于下边的情况 - -```java -1 5 2 8 3 10 -``` - -第一天买第二天卖,第三天买第四天卖,第五天买第六天卖,三次收益分别是 `4`,`6`,`7`,最高的两次就是 `6 + 7 = 13` 了,但是我们第二天其实可以不卖出,第四天再卖出,那么收益是 `8 - 1 = 7`,再加上第五天买入第六天卖出的收益就是 `7 + 7 = 14`了。 - -所以当达到了一个高点时不一定要卖出,所以需要考虑的情况就很多了,不能像 [121 题]() , [122 题]() 那样简单的考虑了。那只能朝着动态规划思路想了。 - -动态规划关键就是数组定义和状态转移方程了。 - -按最简单的动态规划的思路想,用 `dp[i]`表示前`i`天的最高收益,那么 `dp[i+1]` 怎么根据 `dp[i]` 求出来呢?发现并不能求出来。 - -我们注意到我们题目是求那么多天最多交易两次的最高收益,还有一个**最多交易次数**的变量,我们把它加到数组中再试一试。 - -用 `dp[i][k]` 表示前`i`天最多交易`k`次的最高收益,那么 `dp[i][k]` 怎么通过之前的解求出来呢? - -首先第 `i` 天可以什么都不操作,今天的最高收益就等于昨天的最高收益 - -`dp[i][k] = dp[i-1][k]` - -此外,为了获得更大收益我们第 `i` 天也可以选择卖出,既然选择卖出,那么在`0`到 `i-1` 天就要选择一天买入。多选择了一次买入,那在买入之前已经进行了 `k-1` 次买卖。 - -在第 `0` 天买入,收益就是 ` prices[i] - prices[0] ` - -在第 `1` 天买入,收益就是 `prices[i] - prices[1] + dp[0][k-1]`,多加了前一天的最大收益 - -在第 `2` 天买入,收益就是 `prices[i] - prices[2] + dp[1][k-1]`,多加了前一天的最大收益 - -... - -在第 `j` 天买入,收益就是 `prices[i] - prices[j] + dp[j-1][k-1]`,多加了前一天的最大收益 - -上边的每一种可能选择一个最大的,然后与第`i`天什么都不操作比较,就是`dp[i][k]`的值了。 - -当然上边的推导已经可以写代码了,但为了最后的代码更加简洁(写完代码后发现的),我们可以再换一下状态转移方程。真的只是为了简洁,时间复杂度和空间复杂度上不会有影响。 - -> 为了获得更大收益我们第 `i` 天也可以选择卖出,既然选择卖出,那么在`0`到 `i-1` 天就要选择一天买入。 - -我们也可以选择`0`到`i`天中选择一天买入,因为第 `i` 天买入,第 `i`天卖出对最后的收益是没有影响的。 - ->在第 `j` 天买入,收益就是 `prices[i] - prices[j] + dp[j-1][k-1]`,多加了前一天的最大收益 - -我们多加了前一天的最大收益,我们也可以改成加当前天的最大收益。 - -在第 `j` 天买入,收益就是 `prices[i] - prices[j] + dp[j][k-1]` - -不严谨的想一下,如果第 `j` 天就是最后我们要选择的买入点,它使得最后的收益最高,`dp[j][k-1]` 和 `dp[j-1][k-1]` 一定是相等的。因为第 `j` 天一定是一个低点而第 `j - 1` 天是个高点,第 `j` 天为了得到更高收益肯定选择不操作,所以和第 `j - 1` 天的收益是一样的,所以改了状态转移方程,最后求出的最高解还是一致的。 - -综上,最后的状态转移方程就是 - -`dp[i][k] = Max(dp[i-1][k],(prices[i] - prices[0] + dp[0][k-1]),(prices[i] - prices[1] + dp[1][k-1])...(prices[i] - prices[i] + dp[i][k-1]))` - -也就是 - -`dp[i][k] = Max(dp[i-1][k],prices[i] - prices[j] + dp[j][k-1])`,`j` 取 `0 - i`。 - -而 `prices[i] - prices[j] + dp[j][k-1]` 也可以看做, `prices[i] - (prices[j] - dp[j][k-1])` ,为了求这个表达式的最大值,我们可以找`prices[j] - dp[j][k-1]`的最小值。 - -而初始条件对于`k` 等于 `0` 的情况,收益就是 `0` 了。 - -还有前 `0` 天的最大收益也是 0 ,也就是`dp[0][k]`是 0 。由于下标是从`0`开始的,这里的前`0`天其实就是第一天。 - -因为初始条件的结果都是`0`,数组初始化后就是 `0` ,所以不需要特殊处理。 - -```java -public int maxProfit(int[] prices) { - if (prices.length == 0) { - return 0; - } - int K = 2; - int[][] dp = new int[prices.length][K + 1]; - for (int k = 1; k <= K; k++) { - for (int i = 1; i < prices.length; i++) { - int min = Integer.MAX_VALUE; - //找出第 0 天到第 i 天 prices[buy] - dp[buy][k - 1] 的最小值 - for (int buy = 0; buy <= i; buy++) { - min = Math.min(prices[buy] - dp[buy][k - 1], min); - } - //比较不操作和选择一天买入的哪个值更大 - dp[i][k] = Math.max(dp[i - 1][k], prices[i] - min); - } - } - return dp[prices.length - 1][K]; -} -``` - -找第 `j` 天`prices[buy] - dp[buy][k - 1] `的最小值的时候,我们考虑了 `prices[0] - dp[0][k - 1] `、 `prices[1] - dp[1][k - 1] `、 `prices[2] - dp[2][k - 1] `...,找第 `j + 1` 天`prices[buy] - dp[buy][k - 1] `的最小值的时候,我们又会从头考虑 `prices[0] - dp[0][k - 1] `、 `prices[1] - dp[1][k - 1] `、 `prices[2] - dp[2][k - 1] `...,所以其实没必要每次从头考虑,我们只需要把之前的结果保存起来,然后再和新加入的 `prices[j+1] - dp[j+1][k - 1] ` 比较就可以了。 - -```java -public int maxProfit(int[] prices) { - if (prices.length == 0) { - return 0; - } - int K = 2; - int[][] dp = new int[prices.length][K + 1]; - for (int k = 1; k <= K; k++) { - int min = prices[0]; - for (int i = 1; i < prices.length; i++) { - //找出第 1 天到第 i 天 prices[buy] - dp[buy][k - 1] 的最小值 - min = Math.min(prices[i] - dp[i][k - 1], min); - //比较不操作和选择一天买入的哪个值更大 - dp[i][k] = Math.max(dp[i - 1][k], prices[i] - min); - } - } - return dp[prices.length - 1][K]; -} -``` - -此时按照动态规划的套路,结合代码和下边的图。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/123_2.jpg) - -根据代码,我们是固定 `k` 然后一列一列更新 `dp`。而更新当前列只需要前一列的信息,所以不需要二维数组,只需要一个一维数组。但是注意到最外层的 `for` 循环是一个常数次,所以我们可以把两层循环内外颠倒下,可以更好的进行空间复杂度的优化。 - -```java -public int maxProfit(int[] prices) { - if (prices.length == 0) { - return 0; - } - int K = 2; - int[][] dp = new int[prices.length][K + 1]; - int min[] = new int[K + 1]; - for (int i = 1; i <= K; i++) { - min[i] = prices[0]; - } - for (int i = 1; i < prices.length; i++) { - for (int k = 1; k <= K; k++) { - min[k] = Math.min(prices[i] - dp[i][k - 1], min[k]); - dp[i][k] = Math.max(dp[i - 1][k], prices[i] - min[k]); - } - } - return dp[prices.length - 1][K]; -} - -``` - -![](https://windliang.oss-cn-beijing.aliyuncs.com/123_2.jpg) - -再结合图看,此时我们就是一行一行的更新了,对于每一列都有一个 `min` 所以我们多了 `min` 数组。现在让我们将二维数组 `dp` 改成一维数组。 - -```java -public int maxProfit(int[] prices) { - if (prices.length == 0) { - return 0; - } - int K = 2; - int[] dp = new int[K + 1]; - int min[] = new int[K + 1]; - for (int i = 1; i <= K; i++) { - min[i] = prices[0]; - } - for (int i = 1; i < prices.length; i++) { - for (int k = 1; k <= K; k++) { - min[k] = Math.min(prices[i] - dp[k - 1], min[k]); - dp[k] = Math.max(dp[k], prices[i] - min[k]); - } - } - return dp[K]; -} -``` - -由于 `K` 是一个常数,所以我们的 `min` 数组和 `dp` 数组都可以分别当成两个变量。 - -```java -public int maxProfit(int[] prices) { - if (prices.length == 0) { - return 0; - } - int dp1 = 0; - int dp2 = 0; - int min1 = prices[0]; - int min2 = prices[0]; - for (int i = 1; i < prices.length; i++) { - min1 = Math.min(prices[i] - 0, min1); - dp1 = Math.max(dp1, prices[i] - min1); - - min2 = Math.min(prices[i] - dp1, min2); - dp2 = Math.max(dp2, prices[i] - min2); - } - return dp2; -} -``` - -如果结合一步一步的优化,最后这个代码也就很好的能解释通了。 - -# 解法二 - -再分享个利用状态机的 [解法](),虽然不容易想到,但真的太强了,上次用状态机还是 [65 题]()。 - -每天我们其实是有四个状态,买入当前价格的股票,以当前价格的股票卖出。第二次买入股票,第二次卖出股票。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/123_3.jpg) - -`s0`代表初始状态,初始时钱是 `0`。`s1`代表第一次买入后当前的钱,`s2`代表第一次卖出后当前的前,`s3`代表第二次买入后当前的钱,`s4`代表第二次卖出后当前的钱。 - -然后我们只需要更新每天的这四个状态即可。 - -```java -int maxProfit(vector& prices) { - if(prices.empty()) return 0; - //进行初始化,第一天 s1 将股票买入,其他状态全部初始化为最小值 - int s1=-prices[0],s2=INT_MIN,s3=INT_MIN,s4=INT_MIN; - - for(int i=1;i) , [122 题]() 难度高了不少。这道题的意思是,给一个数组代表股票每天的价格。你最多可以买入卖出两次,但只有卖出了才可以再次买入,求出最大的收益是多少。 + +# 解法一 + +参考 [这里]()。 + +开始的想法是求出收益第一高和第二高的两次买卖,然后加起来。对于普通的情况是可以解决的,但是对于下边的情况 + +```java +1 5 2 8 3 10 +``` + +第一天买第二天卖,第三天买第四天卖,第五天买第六天卖,三次收益分别是 `4`,`6`,`7`,最高的两次就是 `6 + 7 = 13` 了,但是我们第二天其实可以不卖出,第四天再卖出,那么收益是 `8 - 1 = 7`,再加上第五天买入第六天卖出的收益就是 `7 + 7 = 14`了。 + +所以当达到了一个高点时不一定要卖出,所以需要考虑的情况就很多了,不能像 [121 题]() , [122 题]() 那样简单的考虑了。那只能朝着动态规划思路想了。 + +动态规划关键就是数组定义和状态转移方程了。 + +按最简单的动态规划的思路想,用 `dp[i]`表示前`i`天的最高收益,那么 `dp[i+1]` 怎么根据 `dp[i]` 求出来呢?发现并不能求出来。 + +我们注意到我们题目是求那么多天最多交易两次的最高收益,还有一个**最多交易次数**的变量,我们把它加到数组中再试一试。 + +用 `dp[i][k]` 表示前`i`天最多交易`k`次的最高收益,那么 `dp[i][k]` 怎么通过之前的解求出来呢? + +首先第 `i` 天可以什么都不操作,今天的最高收益就等于昨天的最高收益 + +`dp[i][k] = dp[i-1][k]` + +此外,为了获得更大收益我们第 `i` 天也可以选择卖出,既然选择卖出,那么在`0`到 `i-1` 天就要选择一天买入。多选择了一次买入,那在买入之前已经进行了 `k-1` 次买卖。 + +在第 `0` 天买入,收益就是 ` prices[i] - prices[0] ` + +在第 `1` 天买入,收益就是 `prices[i] - prices[1] + dp[0][k-1]`,多加了前一天的最大收益 + +在第 `2` 天买入,收益就是 `prices[i] - prices[2] + dp[1][k-1]`,多加了前一天的最大收益 + +... + +在第 `j` 天买入,收益就是 `prices[i] - prices[j] + dp[j-1][k-1]`,多加了前一天的最大收益 + +上边的每一种可能选择一个最大的,然后与第`i`天什么都不操作比较,就是`dp[i][k]`的值了。 + +当然上边的推导已经可以写代码了,但为了最后的代码更加简洁(写完代码后发现的),我们可以再换一下状态转移方程。真的只是为了简洁,时间复杂度和空间复杂度上不会有影响。 + +> 为了获得更大收益我们第 `i` 天也可以选择卖出,既然选择卖出,那么在`0`到 `i-1` 天就要选择一天买入。 + +我们也可以选择`0`到`i`天中选择一天买入,因为第 `i` 天买入,第 `i`天卖出对最后的收益是没有影响的。 + +>在第 `j` 天买入,收益就是 `prices[i] - prices[j] + dp[j-1][k-1]`,多加了前一天的最大收益 + +我们多加了前一天的最大收益,我们也可以改成加当前天的最大收益。 + +在第 `j` 天买入,收益就是 `prices[i] - prices[j] + dp[j][k-1]` + +不严谨的想一下,如果第 `j` 天就是最后我们要选择的买入点,它使得最后的收益最高,`dp[j][k-1]` 和 `dp[j-1][k-1]` 一定是相等的。因为第 `j` 天一定是一个低点而第 `j - 1` 天是个高点,第 `j` 天为了得到更高收益肯定选择不操作,所以和第 `j - 1` 天的收益是一样的,所以改了状态转移方程,最后求出的最高解还是一致的。 + +综上,最后的状态转移方程就是 + +`dp[i][k] = Max(dp[i-1][k],(prices[i] - prices[0] + dp[0][k-1]),(prices[i] - prices[1] + dp[1][k-1])...(prices[i] - prices[i] + dp[i][k-1]))` + +也就是 + +`dp[i][k] = Max(dp[i-1][k],prices[i] - prices[j] + dp[j][k-1])`,`j` 取 `0 - i`。 + +而 `prices[i] - prices[j] + dp[j][k-1]` 也可以看做, `prices[i] - (prices[j] - dp[j][k-1])` ,为了求这个表达式的最大值,我们可以找`prices[j] - dp[j][k-1]`的最小值。 + +而初始条件对于`k` 等于 `0` 的情况,收益就是 `0` 了。 + +还有前 `0` 天的最大收益也是 0 ,也就是`dp[0][k]`是 0 。由于下标是从`0`开始的,这里的前`0`天其实就是第一天。 + +因为初始条件的结果都是`0`,数组初始化后就是 `0` ,所以不需要特殊处理。 + +```java +public int maxProfit(int[] prices) { + if (prices.length == 0) { + return 0; + } + int K = 2; + int[][] dp = new int[prices.length][K + 1]; + for (int k = 1; k <= K; k++) { + for (int i = 1; i < prices.length; i++) { + int min = Integer.MAX_VALUE; + //找出第 0 天到第 i 天 prices[buy] - dp[buy][k - 1] 的最小值 + for (int buy = 0; buy <= i; buy++) { + min = Math.min(prices[buy] - dp[buy][k - 1], min); + } + //比较不操作和选择一天买入的哪个值更大 + dp[i][k] = Math.max(dp[i - 1][k], prices[i] - min); + } + } + return dp[prices.length - 1][K]; +} +``` + +找第 `j` 天`prices[buy] - dp[buy][k - 1] `的最小值的时候,我们考虑了 `prices[0] - dp[0][k - 1] `、 `prices[1] - dp[1][k - 1] `、 `prices[2] - dp[2][k - 1] `...,找第 `j + 1` 天`prices[buy] - dp[buy][k - 1] `的最小值的时候,我们又会从头考虑 `prices[0] - dp[0][k - 1] `、 `prices[1] - dp[1][k - 1] `、 `prices[2] - dp[2][k - 1] `...,所以其实没必要每次从头考虑,我们只需要把之前的结果保存起来,然后再和新加入的 `prices[j+1] - dp[j+1][k - 1] ` 比较就可以了。 + +```java +public int maxProfit(int[] prices) { + if (prices.length == 0) { + return 0; + } + int K = 2; + int[][] dp = new int[prices.length][K + 1]; + for (int k = 1; k <= K; k++) { + int min = prices[0]; + for (int i = 1; i < prices.length; i++) { + //找出第 1 天到第 i 天 prices[buy] - dp[buy][k - 1] 的最小值 + min = Math.min(prices[i] - dp[i][k - 1], min); + //比较不操作和选择一天买入的哪个值更大 + dp[i][k] = Math.max(dp[i - 1][k], prices[i] - min); + } + } + return dp[prices.length - 1][K]; +} +``` + +此时按照动态规划的套路,结合代码和下边的图。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/123_2.jpg) + +根据代码,我们是固定 `k` 然后一列一列更新 `dp`。而更新当前列只需要前一列的信息,所以不需要二维数组,只需要一个一维数组。但是注意到最外层的 `for` 循环是一个常数次,所以我们可以把两层循环内外颠倒下,可以更好的进行空间复杂度的优化。 + +```java +public int maxProfit(int[] prices) { + if (prices.length == 0) { + return 0; + } + int K = 2; + int[][] dp = new int[prices.length][K + 1]; + int min[] = new int[K + 1]; + for (int i = 1; i <= K; i++) { + min[i] = prices[0]; + } + for (int i = 1; i < prices.length; i++) { + for (int k = 1; k <= K; k++) { + min[k] = Math.min(prices[i] - dp[i][k - 1], min[k]); + dp[i][k] = Math.max(dp[i - 1][k], prices[i] - min[k]); + } + } + return dp[prices.length - 1][K]; +} + +``` + +![](https://windliang.oss-cn-beijing.aliyuncs.com/123_2.jpg) + +再结合图看,此时我们就是一行一行的更新了,对于每一列都有一个 `min` 所以我们多了 `min` 数组。现在让我们将二维数组 `dp` 改成一维数组。 + +```java +public int maxProfit(int[] prices) { + if (prices.length == 0) { + return 0; + } + int K = 2; + int[] dp = new int[K + 1]; + int min[] = new int[K + 1]; + for (int i = 1; i <= K; i++) { + min[i] = prices[0]; + } + for (int i = 1; i < prices.length; i++) { + for (int k = 1; k <= K; k++) { + min[k] = Math.min(prices[i] - dp[k - 1], min[k]); + dp[k] = Math.max(dp[k], prices[i] - min[k]); + } + } + return dp[K]; +} +``` + +由于 `K` 是一个常数,所以我们的 `min` 数组和 `dp` 数组都可以分别当成两个变量。 + +```java +public int maxProfit(int[] prices) { + if (prices.length == 0) { + return 0; + } + int dp1 = 0; + int dp2 = 0; + int min1 = prices[0]; + int min2 = prices[0]; + for (int i = 1; i < prices.length; i++) { + min1 = Math.min(prices[i] - 0, min1); + dp1 = Math.max(dp1, prices[i] - min1); + + min2 = Math.min(prices[i] - dp1, min2); + dp2 = Math.max(dp2, prices[i] - min2); + } + return dp2; +} +``` + +如果结合一步一步的优化,最后这个代码也就很好的能解释通了。 + +# 解法二 + +再分享个利用状态机的 [解法](),虽然不容易想到,但真的太强了,上次用状态机还是 [65 题]()。 + +每天我们其实是有四个状态,买入当前价格的股票,以当前价格的股票卖出。第二次买入股票,第二次卖出股票。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/123_3.jpg) + +`s0`代表初始状态,初始时钱是 `0`。`s1`代表第一次买入后当前的钱,`s2`代表第一次卖出后当前的前,`s3`代表第二次买入后当前的钱,`s4`代表第二次卖出后当前的钱。 + +然后我们只需要更新每天的这四个状态即可。 + +```java +int maxProfit(vector& prices) { + if(prices.empty()) return 0; + //进行初始化,第一天 s1 将股票买入,其他状态全部初始化为最小值 + int s1=-prices[0],s2=INT_MIN,s3=INT_MIN,s4=INT_MIN; + + for(int i=1;i)。 - -首先看到二叉树的题,肯定就是想递归了。递归常规的思路,肯定是递归考虑左子树的最大值,递归考虑右子树的最大值。 - -```java -public int maxPathSum(TreeNode root) { - if (root == null) { - return Integer.MIN_VALUE; - } - //左子树的最大值 - int left = maxPathSum(root.left); - //右子树的最大值 - int right = maxPathSum(root.right); - //再考虑包含根节点的最大值 - int all = ....; - return Math.max(Math.max(left, right), all); -} -``` - -问题就来了,怎么考虑包含根节点的最大路径等于多少?因为我们递归求出来的最大 `left` 可能不包含根节点的左孩子,例如下边的情况。 - -```java - 8 - / \ - -3 7 - / \ -1 4 -``` - -左子树的最大值 `left` 肯定就是 `4` 了,然而此时的根节点 `8` 并不能直接和 `4` 去相连。所以考虑包含根节点的路径的最大值时,并不能单纯的用 `root.val + left + right`。 - -所以如果考虑包含当前根节点的 `8` 的最大路径,首先必须包含左右孩子,其次每次遇到一个分叉,就要选择能产生更大的值的路径。例如下边的例子: - -```java - 8 - / \ - -3 7 - / \ -1 4 - \ / \ - 3 2 6 - -考虑左子树 -3 的路径的时候,我们有左子树 1 和右子树 4 的选择,但我们不能同时选择 -如果同时选了,路径就是 ... -> 1 -> -3 -> 4 -> ... 就无法通过根节点 8 了 -所以我们只能去求左子树能返回的最大值,右子树能返回的最大值,选一个较大的 -``` -假设我们只考虑通过根节点 `8` 的最大路径是多少,那么代码就可以写出来了。 - -```java - -public int maxPathSum(TreeNode root) { - //如果最大值是负数,我们选择不选 - int left = Math.max(helper(root.left), 0); - int right = Math.max(helper(root.right), 0); - return root.val + left + right; -} - -int helper(TreeNode root) { - if (root == null) return 0; - int left = Math.max(helper(root.left), 0); - int right = Math.max(helper(root.right), 0); - //选择左子树和右子树产生的值较大的一个 - return root.val + Math.max(left, right); -} - -``` - -接下来我觉得就是这道题最精彩的地方了,现在我们只考虑了包含最初根节点 `8` 的路径。那如果不包含当前根节点,而是其他的路径呢? - -可以发现在 `helper` 函数中,我们每次都求了当前给定的节点的左子树和右子树的最大值,和我们 `maxPathSum` 函数的逻辑是一样的。所以我们利用一个全局变量,在考虑 `helper` 函数中当前 `root` 的时候,同时去判断一下包含当前 `root` 的路径的最大值。 - -这样在递归过程中就考虑了所有包含当前节点的情况。 - -```java -int max = Integer.MIN_VALUE; - -public int maxPathSum(TreeNode root) { - helper(root); - return max; -} -int helper(TreeNode root) { - if (root == null) return 0; - - int left = Math.max(helper(root.left), 0); - int right = Math.max(helper(root.right), 0); - - //求的过程中考虑包含当前根节点的最大路径 - max = Math.max(max, root.val + left + right); - - //只返回包含当前根节点和左子树或者右子树的路径 - return root.val + Math.max(left, right); -} -``` - -# 总 - -这道题最妙的地方就是在递归中利用全局变量,来更新最大路径的值,太强了。前边遇到过和全局变量结合的递归,例如 [106 题](),当递归和全局变量结合有时候确实会难理解些。而在 [ 110 题]() 中也应用了和这个题一样的思想,就是发现递归过程和主函数有一样的逻辑,此时可以在递归过程中就可以进行求解。 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/124.jpg) + +考虑一条路径,可以从任意节点开始,每个节点最多经过一次,问经过的节点的和最大是多少。 + +# 解法一 递归 + +参考了 [这里]()。 + +首先看到二叉树的题,肯定就是想递归了。递归常规的思路,肯定是递归考虑左子树的最大值,递归考虑右子树的最大值。 + +```java +public int maxPathSum(TreeNode root) { + if (root == null) { + return Integer.MIN_VALUE; + } + //左子树的最大值 + int left = maxPathSum(root.left); + //右子树的最大值 + int right = maxPathSum(root.right); + //再考虑包含根节点的最大值 + int all = ....; + return Math.max(Math.max(left, right), all); +} +``` + +问题就来了,怎么考虑包含根节点的最大路径等于多少?因为我们递归求出来的最大 `left` 可能不包含根节点的左孩子,例如下边的情况。 + +```java + 8 + / \ + -3 7 + / \ +1 4 +``` + +左子树的最大值 `left` 肯定就是 `4` 了,然而此时的根节点 `8` 并不能直接和 `4` 去相连。所以考虑包含根节点的路径的最大值时,并不能单纯的用 `root.val + left + right`。 + +所以如果考虑包含当前根节点的 `8` 的最大路径,首先必须包含左右孩子,其次每次遇到一个分叉,就要选择能产生更大的值的路径。例如下边的例子: + +```java + 8 + / \ + -3 7 + / \ +1 4 + \ / \ + 3 2 6 + +考虑左子树 -3 的路径的时候,我们有左子树 1 和右子树 4 的选择,但我们不能同时选择 +如果同时选了,路径就是 ... -> 1 -> -3 -> 4 -> ... 就无法通过根节点 8 了 +所以我们只能去求左子树能返回的最大值,右子树能返回的最大值,选一个较大的 +``` +假设我们只考虑通过根节点 `8` 的最大路径是多少,那么代码就可以写出来了。 + +```java + +public int maxPathSum(TreeNode root) { + //如果最大值是负数,我们选择不选 + int left = Math.max(helper(root.left), 0); + int right = Math.max(helper(root.right), 0); + return root.val + left + right; +} + +int helper(TreeNode root) { + if (root == null) return 0; + int left = Math.max(helper(root.left), 0); + int right = Math.max(helper(root.right), 0); + //选择左子树和右子树产生的值较大的一个 + return root.val + Math.max(left, right); +} + +``` + +接下来我觉得就是这道题最精彩的地方了,现在我们只考虑了包含最初根节点 `8` 的路径。那如果不包含当前根节点,而是其他的路径呢? + +可以发现在 `helper` 函数中,我们每次都求了当前给定的节点的左子树和右子树的最大值,和我们 `maxPathSum` 函数的逻辑是一样的。所以我们利用一个全局变量,在考虑 `helper` 函数中当前 `root` 的时候,同时去判断一下包含当前 `root` 的路径的最大值。 + +这样在递归过程中就考虑了所有包含当前节点的情况。 + +```java +int max = Integer.MIN_VALUE; + +public int maxPathSum(TreeNode root) { + helper(root); + return max; +} +int helper(TreeNode root) { + if (root == null) return 0; + + int left = Math.max(helper(root.left), 0); + int right = Math.max(helper(root.right), 0); + + //求的过程中考虑包含当前根节点的最大路径 + max = Math.max(max, root.val + left + right); + + //只返回包含当前根节点和左子树或者右子树的路径 + return root.val + Math.max(left, right); +} +``` + +# 总 + +这道题最妙的地方就是在递归中利用全局变量,来更新最大路径的值,太强了。前边遇到过和全局变量结合的递归,例如 [106 题](),当递归和全局变量结合有时候确实会难理解些。而在 [ 110 题]() 中也应用了和这个题一样的思想,就是发现递归过程和主函数有一样的逻辑,此时可以在递归过程中就可以进行求解。 + diff --git a/leetcode-125-Valid-Palindrome.md b/leetcode-125-Valid-Palindrome.md index b399e10d9..b4021cc35 100644 --- a/leetcode-125-Valid-Palindrome.md +++ b/leetcode-125-Valid-Palindrome.md @@ -1,97 +1,97 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/125.jpg) - -判断一个字符串是否是回文串,忽略掉除了字母和数字外的字符。 - -# 解法一 - -两个指针,一个指针从头进行,一个指针从尾部进行。依次判断两个指针的字符是否相等,同时要跳过非法字符。需要注意的是,两个指针不用从头到尾遍历,当两个指针相遇的时候就意味着这个字符串是回文串了。 - -还需要注意的是 `'A' == 'a'` ,也就是大写字母和小写字母是相同的。 - -```java -public boolean isPalindrome(String s) { - int len = s.length(); - s = s.toLowerCase(); //统一转为小写 - int i = 0, j = len - 1; - while (i < j) { - //跳过非法字符 - while (!isAlphanumeric(s.charAt(i))) { - i++; - //匹配 " " 这样的空白字符串防止越界 - if (i == len) { - return true; - } - } - while (!isAlphanumeric(s.charAt(j))) { - j--; - } - if (s.charAt(i) != s.charAt(j)) { - return false; - } - i++; - j--; - } - return true; -} - -private boolean isAlphanumeric(char c) { - if ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9') { - return true; - } - return false; -} -``` - -# 解法二 - -上边的是常规的思路,这里分享另一个 [思路]() 。 - -上边为了处理大小写字母的问题,首先全部统一为了小写。为了找出非法字符,每次都需要判断一下该字符是否在合法范围内。 - -这里用一个技巧,把 `'0'` 到 `'9'` 映射到 `1` 到 `10`,`'a'` 到 `'z'` 映射到 `11` 到 `36` ,`'A'` 到 `'Z'` 也映射到 `11` 到 `36` 。然后把所有数字和字母用一个 `char` 数组存起来,没存的字符就默认映射到 `0` 了。 - -这样只需要判断字符串中每个字母映射过去的数字是否相等,如果是 `0` 就意味着它是非法字符。 - -```java -private static final char[] charMap = new char[256]; - -static { - // 映射 '0' 到 '9' - for (int i = 0; i < 10; i++) { - charMap[i + '0'] = (char) (1 + i); // numeric - } - // 映射 'a' 到 'z' 和 映射 'A' 到 'Z' - for (int i = 0; i < 26; i++) { - charMap[i + 'a'] = charMap[i + 'A'] = (char) (11 + i); - } -} - -public boolean isPalindrome(String s) { - char[] pChars = s.toCharArray(); - int start = 0, end = pChars.length - 1; - char cS, cE; - while (start < end) { - // 得到映射后的数字 - cS = charMap[pChars[start]]; - cE = charMap[pChars[end]]; - if (cS != 0 && cE != 0) { - if (cS != cE) - return false; - start++; - end--; - } else { - if (cS == 0) - start++; - if (cE == 0) - end--; - } - } - return true; -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/125.jpg) + +判断一个字符串是否是回文串,忽略掉除了字母和数字外的字符。 + +# 解法一 + +两个指针,一个指针从头进行,一个指针从尾部进行。依次判断两个指针的字符是否相等,同时要跳过非法字符。需要注意的是,两个指针不用从头到尾遍历,当两个指针相遇的时候就意味着这个字符串是回文串了。 + +还需要注意的是 `'A' == 'a'` ,也就是大写字母和小写字母是相同的。 + +```java +public boolean isPalindrome(String s) { + int len = s.length(); + s = s.toLowerCase(); //统一转为小写 + int i = 0, j = len - 1; + while (i < j) { + //跳过非法字符 + while (!isAlphanumeric(s.charAt(i))) { + i++; + //匹配 " " 这样的空白字符串防止越界 + if (i == len) { + return true; + } + } + while (!isAlphanumeric(s.charAt(j))) { + j--; + } + if (s.charAt(i) != s.charAt(j)) { + return false; + } + i++; + j--; + } + return true; +} + +private boolean isAlphanumeric(char c) { + if ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9') { + return true; + } + return false; +} +``` + +# 解法二 + +上边的是常规的思路,这里分享另一个 [思路]() 。 + +上边为了处理大小写字母的问题,首先全部统一为了小写。为了找出非法字符,每次都需要判断一下该字符是否在合法范围内。 + +这里用一个技巧,把 `'0'` 到 `'9'` 映射到 `1` 到 `10`,`'a'` 到 `'z'` 映射到 `11` 到 `36` ,`'A'` 到 `'Z'` 也映射到 `11` 到 `36` 。然后把所有数字和字母用一个 `char` 数组存起来,没存的字符就默认映射到 `0` 了。 + +这样只需要判断字符串中每个字母映射过去的数字是否相等,如果是 `0` 就意味着它是非法字符。 + +```java +private static final char[] charMap = new char[256]; + +static { + // 映射 '0' 到 '9' + for (int i = 0; i < 10; i++) { + charMap[i + '0'] = (char) (1 + i); // numeric + } + // 映射 'a' 到 'z' 和 映射 'A' 到 'Z' + for (int i = 0; i < 26; i++) { + charMap[i + 'a'] = charMap[i + 'A'] = (char) (11 + i); + } +} + +public boolean isPalindrome(String s) { + char[] pChars = s.toCharArray(); + int start = 0, end = pChars.length - 1; + char cS, cE; + while (start < end) { + // 得到映射后的数字 + cS = charMap[pChars[start]]; + cE = charMap[pChars[end]]; + if (cS != 0 && cE != 0) { + if (cS != cE) + return false; + start++; + end--; + } else { + if (cS == 0) + start++; + if (cE == 0) + end--; + } + } + return true; +} +``` + +# 总 + 很简单的一道题了,值得注意的就是解法二将所有字母进行映射,同时将大小写字母映射到同一个数字的想法,省了很多事,速度会提升一些。也可以做一下 [第 5 题]() ,给定一个字符串,找出最长的回文子串,里边的介绍的马拉车算法是真的太强了。 \ No newline at end of file diff --git a/leetcode-128-Longest-Consecutive-Sequence.md b/leetcode-128-Longest-Consecutive-Sequence.md index 7e769d292..f96f54034 100644 --- a/leetcode-128-Longest-Consecutive-Sequence.md +++ b/leetcode-128-Longest-Consecutive-Sequence.md @@ -1,134 +1,134 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/128.jpg) - -给一个数组,求出连续的数字最多有多少个,时间复杂度要求是 `O(n)`。 - -# 解法一 - -首先想一下最直接的暴力破解。我们可以用一个 `HashSet` 把给的数组保存起来。然后再考虑数组的每个数,比如这个数是 `n`,然后看 `n + 1` 在不在 `HashSet` 中,然后再看 `n + 2` 在不在,接下来 `n + 3`、`n + 4` 直到在 `HashSet` 中找不到,记录当前的长度。然后继续考虑下一个数,并且更新最长的长度。 - -```java -public int longestConsecutive(int[] nums) { - HashSet set = new HashSet<>(); - for (int i = 0; i < nums.length; i++) { - set.add(nums[i]); - } - int max = 0; - for (int i = 0; i < nums.length; i++) { - int num = nums[i]; - int count = 0; - while (set.contains(num)) { - count++; - num += 1; - } - max = Math.max(max, count); - } - return max; -} -``` - -当然时间复杂度不符合题意了,我们想一下优化方案。 - -上边的暴力破解有一个问题就是做了很多没必要的计算,因为我们要找最长的连续数字。所以如果是数组 `54367`,当我们遇到 `5` 的时候计算一遍 `567`。遇到 `4` 又计算一遍 `4567`。遇到 `3` 又计算一遍 `34567`。很明显从 `3` 开始才是我们想要的序列。 - -换句话讲,我们只考虑从序列最小的数开始即可。实现的话,当考虑 `n` 的时候,我们先看一看 `n - 1` 是否存在,如果不存在,那么从 `n` 开始就是我们需要考虑的序列了。否则的话,直接跳过。 - -```java -public int longestConsecutive(int[] nums) { - HashSet set = new HashSet<>(); - for (int i = 0; i < nums.length; i++) { - set.add(nums[i]); - } - int max = 0; - for (int i = 0; i < nums.length; i++) { - int num = nums[i]; - //n - 1 是否存在 - if (!set.contains(num - 1)) { - int count = 0; - while (set.contains(num)) { - count++; - num += 1; - } - max = Math.max(max, count); - } - } - return max; -} -``` - -这个时间复杂度的话就是 `O(n)` 了。虽然 `for` 循环里套了 `while` 循环,但每个元素其实最多也就是被访问两次。比如极端情况 `987654` ,`98765` 循环的时候都不会进入 `while` 循环,只有到 `4` 的时候才进入了 `while` 循环。所以总共的话, `98765` 也只会被访问两次,所以时间复杂度就是 `O(n)` 了。 - -# 解法二 - -参考 [这里]() ,虽然不容易直接想到,但还是有迹可循的。 - -本质上就是把连续的序列进行合并,思路就是考虑我们先解决了小问题,然后大问题怎么解决。 - -```java -假如我们已经了有连续的序列,123 和 56,并且序列的边界保存了当前序列的长度。 -1 2 3 -3 3 <- 序列长度 - -5 6 -2 2 <- 序列长度 - -此时来了一个数字 4 -我们只需要考虑 4 - 1 = 3,以 3 结尾的序列的长度 -以及 4 + 1 = 5,以 5 开头的序列的长度 -所以当前就会得到一个包含 4 的,长度为 3 + 1 + 2 = 6 的序列 -1 2 3 4 5 6 -3 3 2 2 <- 序列长度 - -此时把两个边界的长度进行更新 -1 2 3 4 5 6 -6 3 2 6 <- 序列长度 - -此时如果又来了 7 -我们只需要考虑 7 - 1 = 6,以 6 结尾的序列的长度 -以及 7 + 1 = 8,以 8 开头的序列的长度,但是不存在以 8 开头的序列,所以这个长度是 0 -所以当前就会得到一个包含 7 的,长度为 6 + 1 + 0 = 7 的序列 -1 2 3 4 5 6 7 -6 3 2 6 <- 序列长度 - -此时把两个边界的长度进行更新 -1 2 3 4 5 6 7 -7 3 2 6 7 <- 序列长度 -``` - -实现的话,我们可以用一个 `HashMap` ,存储以当前 `key` 为边界的连续序列的长度。可以再结合代码理解一下。 - -```java -public int longestConsecutive(int[] nums) { - HashMap map = new HashMap<>(); - int max = 0; - for (int i = 0; i < nums.length; i++) { - int num = nums[i]; - //已经考虑过的数字就跳过,必须跳过,不然会出错 - //比如 [1 2 1] - //最后的 1 如果不跳过,因为之前的 2 的最长长度已经更新成 2 了,所以会出错 - if(map.containsKey(num)) { - continue; - } - //找到以左边数字结尾的最长序列,默认为 0 - int left = map.getOrDefault(num - 1, 0); - //找到以右边数开头的最长序列,默认为 0 - int right = map.getOrDefault(num + 1, 0); - int sum = left + 1 + right; - max = Math.max(max, sum); - - //将当前数字放到 map 中,防止重复考虑数字,value 可以随便给一个值 - map.put(num, -1); - //更新左边界长度 - map.put(num - left, sum); - //更新右边界长度 - map.put(num + right, sum); - } - return max; -} -``` - -# 总 - -两种思路其实都是正常的操作,仔细想的话还是可以想出来的。 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/128.jpg) + +给一个数组,求出连续的数字最多有多少个,时间复杂度要求是 `O(n)`。 + +# 解法一 + +首先想一下最直接的暴力破解。我们可以用一个 `HashSet` 把给的数组保存起来。然后再考虑数组的每个数,比如这个数是 `n`,然后看 `n + 1` 在不在 `HashSet` 中,然后再看 `n + 2` 在不在,接下来 `n + 3`、`n + 4` 直到在 `HashSet` 中找不到,记录当前的长度。然后继续考虑下一个数,并且更新最长的长度。 + +```java +public int longestConsecutive(int[] nums) { + HashSet set = new HashSet<>(); + for (int i = 0; i < nums.length; i++) { + set.add(nums[i]); + } + int max = 0; + for (int i = 0; i < nums.length; i++) { + int num = nums[i]; + int count = 0; + while (set.contains(num)) { + count++; + num += 1; + } + max = Math.max(max, count); + } + return max; +} +``` + +当然时间复杂度不符合题意了,我们想一下优化方案。 + +上边的暴力破解有一个问题就是做了很多没必要的计算,因为我们要找最长的连续数字。所以如果是数组 `54367`,当我们遇到 `5` 的时候计算一遍 `567`。遇到 `4` 又计算一遍 `4567`。遇到 `3` 又计算一遍 `34567`。很明显从 `3` 开始才是我们想要的序列。 + +换句话讲,我们只考虑从序列最小的数开始即可。实现的话,当考虑 `n` 的时候,我们先看一看 `n - 1` 是否存在,如果不存在,那么从 `n` 开始就是我们需要考虑的序列了。否则的话,直接跳过。 + +```java +public int longestConsecutive(int[] nums) { + HashSet set = new HashSet<>(); + for (int i = 0; i < nums.length; i++) { + set.add(nums[i]); + } + int max = 0; + for (int i = 0; i < nums.length; i++) { + int num = nums[i]; + //n - 1 是否存在 + if (!set.contains(num - 1)) { + int count = 0; + while (set.contains(num)) { + count++; + num += 1; + } + max = Math.max(max, count); + } + } + return max; +} +``` + +这个时间复杂度的话就是 `O(n)` 了。虽然 `for` 循环里套了 `while` 循环,但每个元素其实最多也就是被访问两次。比如极端情况 `987654` ,`98765` 循环的时候都不会进入 `while` 循环,只有到 `4` 的时候才进入了 `while` 循环。所以总共的话, `98765` 也只会被访问两次,所以时间复杂度就是 `O(n)` 了。 + +# 解法二 + +参考 [这里]() ,虽然不容易直接想到,但还是有迹可循的。 + +本质上就是把连续的序列进行合并,思路就是考虑我们先解决了小问题,然后大问题怎么解决。 + +```java +假如我们已经了有连续的序列,123 和 56,并且序列的边界保存了当前序列的长度。 +1 2 3 +3 3 <- 序列长度 + +5 6 +2 2 <- 序列长度 + +此时来了一个数字 4 +我们只需要考虑 4 - 1 = 3,以 3 结尾的序列的长度 +以及 4 + 1 = 5,以 5 开头的序列的长度 +所以当前就会得到一个包含 4 的,长度为 3 + 1 + 2 = 6 的序列 +1 2 3 4 5 6 +3 3 2 2 <- 序列长度 + +此时把两个边界的长度进行更新 +1 2 3 4 5 6 +6 3 2 6 <- 序列长度 + +此时如果又来了 7 +我们只需要考虑 7 - 1 = 6,以 6 结尾的序列的长度 +以及 7 + 1 = 8,以 8 开头的序列的长度,但是不存在以 8 开头的序列,所以这个长度是 0 +所以当前就会得到一个包含 7 的,长度为 6 + 1 + 0 = 7 的序列 +1 2 3 4 5 6 7 +6 3 2 6 <- 序列长度 + +此时把两个边界的长度进行更新 +1 2 3 4 5 6 7 +7 3 2 6 7 <- 序列长度 +``` + +实现的话,我们可以用一个 `HashMap` ,存储以当前 `key` 为边界的连续序列的长度。可以再结合代码理解一下。 + +```java +public int longestConsecutive(int[] nums) { + HashMap map = new HashMap<>(); + int max = 0; + for (int i = 0; i < nums.length; i++) { + int num = nums[i]; + //已经考虑过的数字就跳过,必须跳过,不然会出错 + //比如 [1 2 1] + //最后的 1 如果不跳过,因为之前的 2 的最长长度已经更新成 2 了,所以会出错 + if(map.containsKey(num)) { + continue; + } + //找到以左边数字结尾的最长序列,默认为 0 + int left = map.getOrDefault(num - 1, 0); + //找到以右边数开头的最长序列,默认为 0 + int right = map.getOrDefault(num + 1, 0); + int sum = left + 1 + right; + max = Math.max(max, sum); + + //将当前数字放到 map 中,防止重复考虑数字,value 可以随便给一个值 + map.put(num, -1); + //更新左边界长度 + map.put(num - left, sum); + //更新右边界长度 + map.put(num + right, sum); + } + return max; +} +``` + +# 总 + +两种思路其实都是正常的操作,仔细想的话还是可以想出来的。 + diff --git a/leetcode-129-Sum-Root-to-Leaf-Numbers.md b/leetcode-129-Sum-Root-to-Leaf-Numbers.md index 04b900a21..b8ee1a2c4 100644 --- a/leetcode-129-Sum-Root-to-Leaf-Numbers.md +++ b/leetcode-129-Sum-Root-to-Leaf-Numbers.md @@ -1,137 +1,137 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/129.jpg) - -从根节点到叶子节点的路径组成一个数字,计算所有的数字和。 - -# 思路分析 - -和 [112 题]() 有些像,112 题是给出一个 `sum`,然后去找这条路径。但本质上都一样的,只需要对二叉树进行遍历,遍历过程中记录当前路径的和就可以。说到遍历,无非就是 `BFS` 和 `DFS`,如果进行 `BFS`,过程中我们需要维护多条路径的和,所以我们选择 `DFS` 。 - -说到 `DFS` 的话,可以用递归,也可以用栈去实现,递归会好理解一些,所以这里就只介绍递归吧,栈的话前边也用过很多了,可以看下 [112 题]() 。 - -说到递归,既可以利用回溯的思想,也可以用分治的思想,这里就用这两种方式写一下,关于回溯、分治,可以看一下 [115 题](),会有一个深刻的理解。 - -# 解法一 回溯法 - -回溯的思想就是一直进行深度遍历,直到得到一个解后,记录当前解。然后再回到之前的状态继续进行深度遍历。 - -所以我们需要定义一个函数来得到这个解。 - -```java -void dfs(TreeNode root, int cursum) -``` - -这个函数表示从根节点走到 `root` 节点的时候,路径累积的和是 `cursum`。 - -这里我们用一个全局变量 `sum` 来保存每条路径的和。 - -所以回溯的出口就是,当我们到达叶子节点,保存当前累计的路径和。 - -```java -private void dfs(TreeNode root, int cursum) { - if (root.left == null && root.right == null) { - sum += cursum; - return; - } -``` - -然后就是分别去尝试左子树和右子树就可以。把所有的代码合起来。 - -```java -public int sumNumbers(TreeNode root) { - if (root == null) { - return 0; - } - dfs(root, root.val); - return sum; -} - -int sum = 0; - -private void dfs(TreeNode root, int cursum) { - //到达叶子节点 - if (root.left == null && root.right == null) { - sum += cursum; - return; - } - //尝试左子树 - if(root.left!=null){ - dfs(root.left, cursum * 10 + root.left.val); - } - //尝试右子树 - if(root.right!=null){ - dfs(root.right, cursum * 10 + root.right.val); - - } - -} - -``` - -# 解法二 分治法 - -分支法的思想就是,解决子问题,通过子问题解决最终问题。 - -要求一个树所有的路径和,我们只需要知道从根节点出发经过左子树的所有路径和和从根节点出发经过右子树的所有路径和,加起来就可以了。 - -所以我们需要定义一个函数。 - -```java -int sumNumbersHelper(TreeNode root, int sum) -``` - -参数含义是经过当前 `root` 节点之前,已经累计的和是 `sum`,函数返回从最初根节点经过当前 `root` 节点达到叶子节点的和。(明确函数的定义很重要,这样才可以保证正确的写出递归) - -所以如果经过当前节点,那么当前已有路径的和就是 - -```java -int cursum = sum * 10 + root.val; -``` - -然后我们需要考虑经过当前 `root` 节点后,再经过它的左孩子到叶子节点的所有路径和。 - -```java -int ans1 = sumNumbersHelper(root.left,cursum) -``` - -再考虑经过当前 `root` 节点后,再经过它的右孩子到叶子节点的路径和。 - -```java -int ans2 = sumNumbersHelper(root.right,cursum) -``` - -两个都算出来以后,加起来就是从最初根节点经过当前 `root` 节点到达叶子节点的所有路径和了。 - -```java -public int sumNumbers(TreeNode root) { - if (root == null) { - return 0; - } - return sumNumbersHelper(root, 0); -} - -private int sumNumbersHelper(TreeNode root, int sum) { - //已经累计的和 - int cursum = sum * 10 + root.val; - if (root.left == null && root.right == null) { - return cursum; - } - int ans = 0; - //从最开始经过当前 root 再经过左孩子到达叶子节点所有的路径和 - if (root.left != null) { - ans += sumNumbersHelper(root.left, cursum); - } - //从最开始经过当前 root 再经过右孩子到达叶子节点所有的路径和 - if (root.right != null) { - ans += sumNumbersHelper(root.right, cursum); - } - //返回从最开始经过当前 root 然后到达叶子节点所有的路径和 - return ans; -} - -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/129.jpg) + +从根节点到叶子节点的路径组成一个数字,计算所有的数字和。 + +# 思路分析 + +和 [112 题]() 有些像,112 题是给出一个 `sum`,然后去找这条路径。但本质上都一样的,只需要对二叉树进行遍历,遍历过程中记录当前路径的和就可以。说到遍历,无非就是 `BFS` 和 `DFS`,如果进行 `BFS`,过程中我们需要维护多条路径的和,所以我们选择 `DFS` 。 + +说到 `DFS` 的话,可以用递归,也可以用栈去实现,递归会好理解一些,所以这里就只介绍递归吧,栈的话前边也用过很多了,可以看下 [112 题]() 。 + +说到递归,既可以利用回溯的思想,也可以用分治的思想,这里就用这两种方式写一下,关于回溯、分治,可以看一下 [115 题](),会有一个深刻的理解。 + +# 解法一 回溯法 + +回溯的思想就是一直进行深度遍历,直到得到一个解后,记录当前解。然后再回到之前的状态继续进行深度遍历。 + +所以我们需要定义一个函数来得到这个解。 + +```java +void dfs(TreeNode root, int cursum) +``` + +这个函数表示从根节点走到 `root` 节点的时候,路径累积的和是 `cursum`。 + +这里我们用一个全局变量 `sum` 来保存每条路径的和。 + +所以回溯的出口就是,当我们到达叶子节点,保存当前累计的路径和。 + +```java +private void dfs(TreeNode root, int cursum) { + if (root.left == null && root.right == null) { + sum += cursum; + return; + } +``` + +然后就是分别去尝试左子树和右子树就可以。把所有的代码合起来。 + +```java +public int sumNumbers(TreeNode root) { + if (root == null) { + return 0; + } + dfs(root, root.val); + return sum; +} + +int sum = 0; + +private void dfs(TreeNode root, int cursum) { + //到达叶子节点 + if (root.left == null && root.right == null) { + sum += cursum; + return; + } + //尝试左子树 + if(root.left!=null){ + dfs(root.left, cursum * 10 + root.left.val); + } + //尝试右子树 + if(root.right!=null){ + dfs(root.right, cursum * 10 + root.right.val); + + } + +} + +``` + +# 解法二 分治法 + +分支法的思想就是,解决子问题,通过子问题解决最终问题。 + +要求一个树所有的路径和,我们只需要知道从根节点出发经过左子树的所有路径和和从根节点出发经过右子树的所有路径和,加起来就可以了。 + +所以我们需要定义一个函数。 + +```java +int sumNumbersHelper(TreeNode root, int sum) +``` + +参数含义是经过当前 `root` 节点之前,已经累计的和是 `sum`,函数返回从最初根节点经过当前 `root` 节点达到叶子节点的和。(明确函数的定义很重要,这样才可以保证正确的写出递归) + +所以如果经过当前节点,那么当前已有路径的和就是 + +```java +int cursum = sum * 10 + root.val; +``` + +然后我们需要考虑经过当前 `root` 节点后,再经过它的左孩子到叶子节点的所有路径和。 + +```java +int ans1 = sumNumbersHelper(root.left,cursum) +``` + +再考虑经过当前 `root` 节点后,再经过它的右孩子到叶子节点的路径和。 + +```java +int ans2 = sumNumbersHelper(root.right,cursum) +``` + +两个都算出来以后,加起来就是从最初根节点经过当前 `root` 节点到达叶子节点的所有路径和了。 + +```java +public int sumNumbers(TreeNode root) { + if (root == null) { + return 0; + } + return sumNumbersHelper(root, 0); +} + +private int sumNumbersHelper(TreeNode root, int sum) { + //已经累计的和 + int cursum = sum * 10 + root.val; + if (root.left == null && root.right == null) { + return cursum; + } + int ans = 0; + //从最开始经过当前 root 再经过左孩子到达叶子节点所有的路径和 + if (root.left != null) { + ans += sumNumbersHelper(root.left, cursum); + } + //从最开始经过当前 root 再经过右孩子到达叶子节点所有的路径和 + if (root.right != null) { + ans += sumNumbersHelper(root.right, cursum); + } + //返回从最开始经过当前 root 然后到达叶子节点所有的路径和 + return ans; +} + +``` + +# 总 + 这道题本质上还是在考二叉树的遍历,回溯和分治的思想的区别也可以对比考虑一下。 \ No newline at end of file diff --git a/leetcode-130-Longest-Increasing-Subsequence.md b/leetcode-130-Longest-Increasing-Subsequence.md index b1562f810..9306815c5 100644 --- a/leetcode-130-Longest-Increasing-Subsequence.md +++ b/leetcode-130-Longest-Increasing-Subsequence.md @@ -1,252 +1,252 @@ -# 题目描述(中等难度) - -300、Longest Increasing Subsequence - -Given an unsorted array of integers, find the length of longest increasing subsequence. - -**Example:** - -``` -Input: [10,9,2,5,3,7,101,18] -Output: 4 -Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4. -``` - -**Note:** - -- There may be more than one LIS combination, it is only necessary for you to return the length. -- Your algorithm should run in O(*n2*) complexity. - -**Follow up:** Could you improve it to O(*n* log *n*) time complexity? - -最长上升子序列的长度。 - -# 解法一 - -比较经典的一道题,之前笔试也遇到过。最直接的方法就是动态规划了。 - -`dp[i]`表示以第 `i` 个数字**为结尾**的最长上升子序列的长度。 - -求 `dp[i]` 的时候,如果前边的某个数 `nums[j] < nums[i]` ,那么我们可以将第 `i` 个数接到第 `j` 个数字的后边作为一个新的上升子序列,此时对应的上升子序列的长度就是 `dp[j] + 1`。 - -可以从下边情况中选择最大的。 - -如果 `nums[0] < nums[i]`,`dp[0] + 1` 就是 `dp[i]` 的一个备选解。 - -如果 `nums[1] < nums[i]`,`dp[1] + 1` 就是 `dp[i]` 的一个备选解。 - -如果 `nums[2] < nums[i]`,`dp[2] + 1` 就是 `dp[i]` 的一个备选解。 - -... - -如果 `nums[i-1] < nums[i]`,`dp[i-1] + 1` 就是 `dp[i]` 的一个备选解。 - -从上边的备选解中选择最大的就是 `dp[i]` 的值。 - -```java -public int lengthOfLIS(int[] nums) { - int n = nums.length; - if (n == 0) { - return 0; - } - int dp[] = new int[n]; - int max = 1; - for (int i = 0; i < n; i++) { - dp[i] = 1; - for (int j = 0; j < i; j++) { - if (nums[j] < nums[i]) { - dp[i] = Math.max(dp[i], dp[j] + 1); - } - } - max = Math.max(max, dp[i]); - } - return max; -} -``` - -时间复杂度:`O(n²)`。 - -空间复杂度:`O(1)`。 - -# 解法二 - -还有一种很巧妙的方法,最开始知道这个方法的时候就觉得很巧妙,但还是把它忘记了,又看了一遍 [这里](https://leetcode.com/problems/longest-increasing-subsequence/discuss/74824/JavaPython-Binary-search-O(nlogn)-time-with-explanation) 才想起来。 - -不同之处在于 `dp` 数组的定义。 - -`dp[i]` 表示长度为 `i + 1` 的所有上升子序列的末尾的最小值。 - -比较绕,举个例子。 - -```java -nums = [4,5,6,3] -len = 1 : [4], [5], [6], [3] => tails[0] = 3 -长度为 1 的上升子序列有 4 个,末尾最小的值就是 4 - -len = 2 : [4, 5], [5, 6] => tails[1] = 5 -长度为 2 的上升子序列有 2 个,末尾最小的值就是 5 - -len = 3 : [4, 5, 6] => tails[2] = 6 -长度为 3 的上升子序列有 1 个,末尾最小的值就是 6 -``` - -有了上边的定义,我们可以依次考虑每个数字,举个例子。 - -```java -nums = [10,9,2,5,3,7,101,18] - -开始没有数字 -dp = [] - -1---------------------------- -10 9 2 5 3 7 101 18 -^ - -先考虑 10, 只有 1 个数字, 此时长度为 1 的最长上升子序列末尾的值就是 10 -len 1 -dp = [10] - -2---------------------------- -10 9 2 5 3 7 101 18 - ^ -考虑 9, 9 比之前长度为 1 的最长上升子序列末尾的最小值 10 小, 更新长度为 1 的最长上升子序列末尾的值为 9 -len 1 -dp = [9] - -3---------------------------- -10 9 2 5 3 7 101 18 - ^ -考虑 2, 2 比之前长度为 1 的最长上升子序列末尾的最小值 9 小, 更新长度为 1 的最长上升子序列末尾的值为 2 -len 1 -dp = [2] - -4---------------------------- -10 9 2 5 3 7 101 18 - ^ -考虑 5, -5 比之前长度为 1 的最长上升子序列末尾的最小值 2 大, -此时可以扩展长度, 更新长度为 2 的最长上升子序列末尾的值为 5 -len 1 2 -dp = [2 5] - -5---------------------------- -10 9 2 5 3 7 101 18 - ^ -考虑 3, -3 比之前长度为 1 的最长上升子序列末尾的最小值 2 大, 向后考虑 -3 比之前长度为 2 的最长上升子序列末尾的最小值 5 小, 更新长度为 2 的最长上升子序列末尾的值为 3 -len 1 2 -dp = [2 3] - -6---------------------------- -10 9 2 5 3 7 101 18 - ^ -考虑 7, -7 比之前长度为 1 的最长上升子序列末尾的最小值 2 大, 向后考虑 -7 比之前长度为 2 的最长上升子序列末尾的最小值 3 大, 向后考虑 -此时可以扩展长度, 更新长度为 3 的最长上升子序列末尾的值为 7 -len 1 2 3 -dp = [2 3 7] - -7---------------------------- -10 9 2 5 3 7 101 18 - ^ -考虑 101, -101 比之前长度为 1 的最长上升子序列末尾的最小值 2 大, 向后考虑 -101 比之前长度为 2 的最长上升子序列末尾的最小值 3 大, 向后考虑 -101 比之前长度为 3 的最长上升子序列末尾的最小值 7 大, 向后考虑 -此时可以扩展长度, 更新长度为 4 的最长上升子序列末尾的值为 101 -len 1 2 3 4 -dp = [2 3 7 101] - -8---------------------------- -10 9 2 5 3 7 101 18 - ^ -考虑 18, -18 比之前长度为 1 的最长上升子序列末尾的最小值 2 大, 向后考虑 -18 比之前长度为 2 的最长上升子序列末尾的最小值 3 大, 向后考虑 -18 比之前长度为 3 的最长上升子序列末尾的最小值 7 大, 向后考虑 -3 比之前长度为 4 的最长上升子序列末尾的最小值 101 小, 更新长度为 4 的最长上升子序列末尾的值为 18 -len 1 2 3 4 -dp = [2 3 7 18] - -遍历完成,所以数字都考虑了,此时 dp 的长度就是最长上升子序列的长度 -``` - -总结上边的规律,新来一个数字以后,我们去寻找 `dp` 中第一个比它大的值,然后将当前值更新为新来的数字。 - -如果 `dp` 中没有比新来的数字大的数,那么就扩展长度,将新来的值放到最后。 - -写代码的话,因为 `dp` 是一个动态扩容的过程,我们可以用一个 `list` 。但由于比较简单,我们知道 `dp` 最大的长度也就是 `nums` 的长度,我们可以直接用数组,然后自己记录当前数组的长度即可。 - -```java -public int lengthOfLIS(int[] nums) { - int n = nums.length; - if (n == 0) { - return 0; - } - int dp[] = new int[n]; - int len = 0; - for (int i = 0; i < n; i++) { - int j = 0; - // 寻找 dp 中第一个大于等于新来的数的位置 - for (j = 0; j < len; j++) { - if (nums[i] <= dp[j]) { - break; - } - } - // 更新当前值 - dp[j] = nums[i]; - // 是否更新长度 - if (j == len) { - len++; - } - } - return len; -} -``` - -上边花了一大段话讲这个解法,但是上边的时间复杂度依旧是 `O(n²)`,当然不能满足。 - -这个解法巧妙的地方在于,通过上边 `dp` 的定义,`dp` 一定是有序的。我们要从一个有序数组中寻找第一个大于等于新来数的位置,此时就可以通过二分查找了。 - -```java -public int lengthOfLIS(int[] nums) { - int n = nums.length; - if (n == 0) { - return 0; - } - int dp[] = new int[n]; - int len = 0; - for (int i = 0; i < n; i++) { - int start = 0; - int end = len; - while (start < end) { - int mid = (start + end) >>> 1; - if (dp[mid] < nums[i]) { - start = mid + 1; - } else { - end = mid; - } - } - dp[start] = nums[i]; - if (start == len) { - len++; - } - } - return len; -} -``` - -这样的话时间复杂度就是 `O(nlog(n))` 了。 - -上边需要注意的一点是,在二分查找中我们将 `end` 初始化为 `len`, 平常我们习惯上初始化为 `len - 1`。 - -赋值为 `len` 的目的是当 `dp` 中没有数字比当前数字大的时候,最后 `start` 刚好就是 `len`, 方便扩展数组。 - -# 总 - -解法一比较常规,比较容易想到。 - -解法二的话就很巧妙了,关键就是 `dp` 的定义使得 `dp` 是一个有序数组了。这种也不容易记住,半年前笔试做过这道题,但现在还是忘记了,不过还是可以欣赏一下的,哈哈。 - +# 题目描述(中等难度) + +300、Longest Increasing Subsequence + +Given an unsorted array of integers, find the length of longest increasing subsequence. + +**Example:** + +``` +Input: [10,9,2,5,3,7,101,18] +Output: 4 +Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4. +``` + +**Note:** + +- There may be more than one LIS combination, it is only necessary for you to return the length. +- Your algorithm should run in O(*n2*) complexity. + +**Follow up:** Could you improve it to O(*n* log *n*) time complexity? + +最长上升子序列的长度。 + +# 解法一 + +比较经典的一道题,之前笔试也遇到过。最直接的方法就是动态规划了。 + +`dp[i]`表示以第 `i` 个数字**为结尾**的最长上升子序列的长度。 + +求 `dp[i]` 的时候,如果前边的某个数 `nums[j] < nums[i]` ,那么我们可以将第 `i` 个数接到第 `j` 个数字的后边作为一个新的上升子序列,此时对应的上升子序列的长度就是 `dp[j] + 1`。 + +可以从下边情况中选择最大的。 + +如果 `nums[0] < nums[i]`,`dp[0] + 1` 就是 `dp[i]` 的一个备选解。 + +如果 `nums[1] < nums[i]`,`dp[1] + 1` 就是 `dp[i]` 的一个备选解。 + +如果 `nums[2] < nums[i]`,`dp[2] + 1` 就是 `dp[i]` 的一个备选解。 + +... + +如果 `nums[i-1] < nums[i]`,`dp[i-1] + 1` 就是 `dp[i]` 的一个备选解。 + +从上边的备选解中选择最大的就是 `dp[i]` 的值。 + +```java +public int lengthOfLIS(int[] nums) { + int n = nums.length; + if (n == 0) { + return 0; + } + int dp[] = new int[n]; + int max = 1; + for (int i = 0; i < n; i++) { + dp[i] = 1; + for (int j = 0; j < i; j++) { + if (nums[j] < nums[i]) { + dp[i] = Math.max(dp[i], dp[j] + 1); + } + } + max = Math.max(max, dp[i]); + } + return max; +} +``` + +时间复杂度:`O(n²)`。 + +空间复杂度:`O(1)`。 + +# 解法二 + +还有一种很巧妙的方法,最开始知道这个方法的时候就觉得很巧妙,但还是把它忘记了,又看了一遍 [这里](https://leetcode.com/problems/longest-increasing-subsequence/discuss/74824/JavaPython-Binary-search-O(nlogn)-time-with-explanation) 才想起来。 + +不同之处在于 `dp` 数组的定义。 + +`dp[i]` 表示长度为 `i + 1` 的所有上升子序列的末尾的最小值。 + +比较绕,举个例子。 + +```java +nums = [4,5,6,3] +len = 1 : [4], [5], [6], [3] => tails[0] = 3 +长度为 1 的上升子序列有 4 个,末尾最小的值就是 4 + +len = 2 : [4, 5], [5, 6] => tails[1] = 5 +长度为 2 的上升子序列有 2 个,末尾最小的值就是 5 + +len = 3 : [4, 5, 6] => tails[2] = 6 +长度为 3 的上升子序列有 1 个,末尾最小的值就是 6 +``` + +有了上边的定义,我们可以依次考虑每个数字,举个例子。 + +```java +nums = [10,9,2,5,3,7,101,18] + +开始没有数字 +dp = [] + +1---------------------------- +10 9 2 5 3 7 101 18 +^ + +先考虑 10, 只有 1 个数字, 此时长度为 1 的最长上升子序列末尾的值就是 10 +len 1 +dp = [10] + +2---------------------------- +10 9 2 5 3 7 101 18 + ^ +考虑 9, 9 比之前长度为 1 的最长上升子序列末尾的最小值 10 小, 更新长度为 1 的最长上升子序列末尾的值为 9 +len 1 +dp = [9] + +3---------------------------- +10 9 2 5 3 7 101 18 + ^ +考虑 2, 2 比之前长度为 1 的最长上升子序列末尾的最小值 9 小, 更新长度为 1 的最长上升子序列末尾的值为 2 +len 1 +dp = [2] + +4---------------------------- +10 9 2 5 3 7 101 18 + ^ +考虑 5, +5 比之前长度为 1 的最长上升子序列末尾的最小值 2 大, +此时可以扩展长度, 更新长度为 2 的最长上升子序列末尾的值为 5 +len 1 2 +dp = [2 5] + +5---------------------------- +10 9 2 5 3 7 101 18 + ^ +考虑 3, +3 比之前长度为 1 的最长上升子序列末尾的最小值 2 大, 向后考虑 +3 比之前长度为 2 的最长上升子序列末尾的最小值 5 小, 更新长度为 2 的最长上升子序列末尾的值为 3 +len 1 2 +dp = [2 3] + +6---------------------------- +10 9 2 5 3 7 101 18 + ^ +考虑 7, +7 比之前长度为 1 的最长上升子序列末尾的最小值 2 大, 向后考虑 +7 比之前长度为 2 的最长上升子序列末尾的最小值 3 大, 向后考虑 +此时可以扩展长度, 更新长度为 3 的最长上升子序列末尾的值为 7 +len 1 2 3 +dp = [2 3 7] + +7---------------------------- +10 9 2 5 3 7 101 18 + ^ +考虑 101, +101 比之前长度为 1 的最长上升子序列末尾的最小值 2 大, 向后考虑 +101 比之前长度为 2 的最长上升子序列末尾的最小值 3 大, 向后考虑 +101 比之前长度为 3 的最长上升子序列末尾的最小值 7 大, 向后考虑 +此时可以扩展长度, 更新长度为 4 的最长上升子序列末尾的值为 101 +len 1 2 3 4 +dp = [2 3 7 101] + +8---------------------------- +10 9 2 5 3 7 101 18 + ^ +考虑 18, +18 比之前长度为 1 的最长上升子序列末尾的最小值 2 大, 向后考虑 +18 比之前长度为 2 的最长上升子序列末尾的最小值 3 大, 向后考虑 +18 比之前长度为 3 的最长上升子序列末尾的最小值 7 大, 向后考虑 +3 比之前长度为 4 的最长上升子序列末尾的最小值 101 小, 更新长度为 4 的最长上升子序列末尾的值为 18 +len 1 2 3 4 +dp = [2 3 7 18] + +遍历完成,所以数字都考虑了,此时 dp 的长度就是最长上升子序列的长度 +``` + +总结上边的规律,新来一个数字以后,我们去寻找 `dp` 中第一个比它大的值,然后将当前值更新为新来的数字。 + +如果 `dp` 中没有比新来的数字大的数,那么就扩展长度,将新来的值放到最后。 + +写代码的话,因为 `dp` 是一个动态扩容的过程,我们可以用一个 `list` 。但由于比较简单,我们知道 `dp` 最大的长度也就是 `nums` 的长度,我们可以直接用数组,然后自己记录当前数组的长度即可。 + +```java +public int lengthOfLIS(int[] nums) { + int n = nums.length; + if (n == 0) { + return 0; + } + int dp[] = new int[n]; + int len = 0; + for (int i = 0; i < n; i++) { + int j = 0; + // 寻找 dp 中第一个大于等于新来的数的位置 + for (j = 0; j < len; j++) { + if (nums[i] <= dp[j]) { + break; + } + } + // 更新当前值 + dp[j] = nums[i]; + // 是否更新长度 + if (j == len) { + len++; + } + } + return len; +} +``` + +上边花了一大段话讲这个解法,但是上边的时间复杂度依旧是 `O(n²)`,当然不能满足。 + +这个解法巧妙的地方在于,通过上边 `dp` 的定义,`dp` 一定是有序的。我们要从一个有序数组中寻找第一个大于等于新来数的位置,此时就可以通过二分查找了。 + +```java +public int lengthOfLIS(int[] nums) { + int n = nums.length; + if (n == 0) { + return 0; + } + int dp[] = new int[n]; + int len = 0; + for (int i = 0; i < n; i++) { + int start = 0; + int end = len; + while (start < end) { + int mid = (start + end) >>> 1; + if (dp[mid] < nums[i]) { + start = mid + 1; + } else { + end = mid; + } + } + dp[start] = nums[i]; + if (start == len) { + len++; + } + } + return len; +} +``` + +这样的话时间复杂度就是 `O(nlog(n))` 了。 + +上边需要注意的一点是,在二分查找中我们将 `end` 初始化为 `len`, 平常我们习惯上初始化为 `len - 1`。 + +赋值为 `len` 的目的是当 `dp` 中没有数字比当前数字大的时候,最后 `start` 刚好就是 `len`, 方便扩展数组。 + +# 总 + +解法一比较常规,比较容易想到。 + +解法二的话就很巧妙了,关键就是 `dp` 的定义使得 `dp` 是一个有序数组了。这种也不容易记住,半年前笔试做过这道题,但现在还是忘记了,不过还是可以欣赏一下的,哈哈。 + diff --git a/leetcode-130-Surrounded-Regions.md b/leetcode-130-Surrounded-Regions.md index f004c0e6a..d85003e32 100644 --- a/leetcode-130-Surrounded-Regions.md +++ b/leetcode-130-Surrounded-Regions.md @@ -1,517 +1,517 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/130.jpg) - -有一点点像围棋,把被 `X` 围起来的 `O` 变成 `X`,边界的 `O` 一定不会被围起来。如果 `O` 和边界的 `O` 连通起来,那么这些 `O` 就都算作不被围起来,比如下边的例子。 - -```java -X X X X X -O O O X X -X X X O X -X O X X X -``` - -上边的例子就只需要变化 `1` 个 `O` 。 - -```java -X X X X X -O O O X X -X X X X X -X O X X X -``` - -# 解法一 - -把相邻的`O` 看作是连通的图,然后从每一个 `O` 开始做 `DFS`。 - -如果遍历完成后没有到达边界的 `O` ,我们就把当前 `O` 改成 `X`。 - -如果遍历过程中到达了边界的 `O` ,直接结束 `DFS`,当前的 `O` 就不用操作。 - -然后继续考虑下一个 `O`,继续做一次 `DFS`。 - -```java -public void solve(char[][] board) { - int rows = board.length; - if (rows == 0) { - return; - } - int cols = board[0].length; - //考虑除去边界以外的所有 O - for (int i = 1; i < rows - 1; i++) { - for (int j = 1; j < cols - 1; j++) { - if (board[i][j] == 'O') { - //visited 用于记录 DFS 过程中已经访问过的节点 - HashSet visited = new HashSet<>(); - //如果没有到达边界,就把当前 O 置为 X - if (!solveHelper(i, j, board, visited)) { - board[i][j] = 'X'; - } - } - } - } -} - -private boolean solveHelper(int row, int col, char[][] board, HashSet visited) { - //是否访问过 - if (visited.contains(row + "@" + col)) { - return false; - } - visited.add(row + "@" + col); - - //到达了 X 直接返回 false - if (board[row][col] == 'X') { - return false; - } - - if (row == 0 || row == board.length - 1 || col == 0 || col == board[0].length - 1) { - return true; - } - - //分别尝试四个方向 - if (solveHelper(row - 1, col, board, visited) - || solveHelper(row, col - 1, board, visited) - || solveHelper(row + 1, col, board, visited) - || solveHelper(row, col + 1, board, visited)) { - return true; - } else { - return false; - } -} -``` - -遗憾的是,到最后两个 `test` 的时候超时了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/130_2.jpg) - -优化的的话,我尝试了在每次 `DFS` 过程中,返回 `true` 之前,把当前的 `row` 和 `col` 记录下来,然后第二次遇到这些点的时候,就直接跳过 。 - -```java -public void solve(char[][] board) { - int rows = board.length; - if (rows == 0) { - return; - } - //记录可以连通到边界的 O - HashSet memoization = new HashSet<>(); - int cols = board[0].length; - for (int i = 1; i < rows - 1; i++) { - for (int j = 1; j < cols - 1; j++) { - if (board[i][j] == 'O') { - //如果当前位置的 O 被记录过了,直接跳过 - if (memoization.contains(i + "@" + j)) { - continue; - } - HashSet visited = new HashSet<>(); - if (!solveHelper(i, j, board, visited, memoization)) { - board[i][j] = 'X'; - } - } - } - } -} - -private boolean solveHelper(int row, int col, char[][] board, HashSet visited, - HashSet memoization) { - if (visited.contains(row + "@" + col)) { - return false; - } - visited.add(row + "@" + col); - - if (board[row][col] == 'X') { - return false; - } - //当前位置可以连通到边界,返回 true - if (memoization.contains(row + "@" +col)) { - return true; - } - if (row == 0 || row == board.length - 1 || col == 0 || col == board[0].length - 1) { - //当前位置可以连通道边界,记录下来 - memoization.add(row + "@" + col); - return true; - } - - if (solveHelper(row - 1, col, board, visited, memoization) - || solveHelper(row, col - 1, board, visited, memoization) - || solveHelper(row + 1, col, board, visited, memoization) - || solveHelper(row, col + 1, board, visited, memoization)) { - //当前位置可以连通道边界,记录下来 - memoization.add(row + "@" + col); - return true; - } else { - return false; - } - -} -``` - -但没什么效果,依旧还是超时。 - -之前还考虑过能不能在**遍历过程中**,返回 `false` 之前,直接把 `O` 改成 `X`。最后发现是不可以的,比如下边的例子。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/130_3.jpg) - -如果我们从橙色的 `O` 做 `DFS`,然后沿着箭头方向,到达最后一个 `O` 的时候,它的左边上边右边都是 `X` ,根据代码它就返回 `false`,此外它下边是访问过的节点也会返回 `false`,所以四个方向都返回 `false`,如果把它改成 `X`明显是不对的。 - -# 解法二 - -解法一是从当前节点做 `DFS` ,然后看它是否能到达边界的 `O`。那么我们能不能把思路逆转过来呢? - -从边界的 `O` 做 `DFS`,然后把遇到的 `O` 都标记一下,这些 `O` 就是可以连通到边界的。然后把边界的所有的 `O` 都做一次 `DFS` ,把 `DFS` 过程的中的 `O` 做一下标记。最后我们只需要遍历节点,把没有标记过的 `O` 改成 `X` 就可以了。 - -标记的话,我们可以用一个 `visited` 二维数组,把访问过的置为 `true` 。 - -```java -public void solve(char[][] board) { - int rows = board.length; - if (rows == 0) { - return; - } - int cols = board[0].length; - boolean[][] visited = new boolean[rows][cols]; - for (int i = 0; i < cols; i++) { - //最上边一行的所有 O 做 DFS - if (board[0][i] == 'O') { - dfs(0, i, board, visited); - } - //最下边一行的所有 O 做 DFS - if (board[board.length - 1][i] == 'O') { - dfs(board.length - 1, i, board, visited); - } - - } - for (int i = 1; i < rows - 1; i++) { - //最左边一列的所有 O 做 DFS - if (board[i][0] == 'O') { - dfs(i, 0, board, visited); - } - //最右边一列的所有 O 做 DFS - if (board[i][board[0].length - 1] == 'O') { - dfs(i, board[0].length - 1, board, visited); - } - } - //把所有没有标记过的 O 改为 X - for (int i = 0; i < rows; i++) { - for (int j = 0; j < cols; j++) { - //省略了对 X 的判断,把 X 变成 X 不影响结果 - if (!visited[i][j]) { - board[i][j] = 'X'; - } - } - } -} - -private void dfs(int i, int j, char[][] board, boolean[][] visited) { - if (i < 0 || j < 0 || i == board.length || j == board[0].length) { - return; - } - if (visited[i][j]) { - return; - } - if (board[i][j] == 'O') { - visited[i][j] = true; - dfs(i + 1, j, board, visited); - dfs(i, j + 1, board, visited); - dfs(i, j - 1, board, visited); - dfs(i - 1, j, board, visited); - } - -} -``` - -然后这个解法 `AC` 了,但空间复杂度可以优化一下,这个思想很多题用过了,比如 [79 题]()。 - -这里的 `visited` 的二维数组我们可以不需要。我们可以先把连通的 `O` 改成 `*`,或者其他的字符。最后遍历整个 `board`,遇到 `*` 就把它还原到 `O` 。遇到 `O`,因为它没有被修改成`*`,也就意味着它没有连到边界,就把它改成 `X`。 - -```java -public void solve(char[][] board) { - int rows = board.length; - if (rows == 0) { - return; - } - int cols = board[0].length; - for (int i = 0; i < cols; i++) { - //最上边一行的所有 O 做 DFS - if (board[0][i] == 'O') { - dfs(0, i, board); - } - //最下边一行的所有 O 做 DFS - if (board[board.length - 1][i] == 'O') { - dfs(board.length - 1, i, board); - } - - } - for (int i = 1; i < rows - 1; i++) { - //最左边一列的所有 O 做 DFS - if (board[i][0] == 'O') { - dfs(i, 0, board); - } - //最右边一列的所有 O 做 DFS - if (board[i][board[0].length - 1] == 'O') { - dfs(i, board[0].length - 1, board); - } - } - //把所有没有标记过的 O 改为 X,标记过的还原 - for (int i = 0; i < rows; i++) { - for (int j = 0; j < cols; j++) { - if (board[i][j] == '*') { - board[i][j] = 'O'; - }else if(board[i][j] == 'O'){ - board[i][j] = 'X'; - } - } - } -} - -private void dfs(int i, int j, char[][] board) { - if (i < 0 || j < 0 || i == board.length || j == board[0].length) { - return; - } - if (board[i][j] == '*') { - return; - } - if (board[i][j] == 'O') { - board[i][j] = '*'; - dfs(i + 1, j, board); - dfs(i, j + 1, board); - dfs(i, j - 1, board); - dfs(i - 1, j, board); - } - -} -``` - - - -但是在逛 `Disscuss` 的时候发现有人提出来说,`DFS` 的解法可能导致栈溢出。 - -这个 [解法]() 下的第一个评论,我把原文贴过来。 - -> This is a DFS solution, but it may causes a stack overflow issue. -> -> When you use DFS, it is tricky to use: -> -> ``` -> if(i>1) -> check(vec,i-1,j,row,col); -> if(j>1) -> check(vec,i,j-1,row,col); -> ``` -> -> because it is more common to write like this: -> -> ``` -> if(i>=1) -> check(vec,i-1,j,row,col); -> if(j>=1) -> check(vec,i,j-1,row,col); -> ``` -> -> Then I'll explain it. -> -> There is a test case like this: -> -> ``` -> OOOOOOOOOO -> XXXXXXXXXO -> OOOOOOOOOO -> OXXXXXXXXX -> OOOOOOOOOO -> XXXXXXXXXO -> OOOOOOOOOO -> OXXXXXXXXX -> OOOOOOOOOO -> XXXXXXXXXO -> ``` -> -> To make it clear, I draw a 10x10 board, but it is actually a 250x250 board like this one. -> -> When dfs function visit `board[0][0]`, it ought to visit every grid marked 'O', thus lead to stack overflow(runtime error). -> -> After you change "if(j>=1)" to "if(j>1)", the DFS function won't check `board[i][0]` (0<=i<=row-1), and then not all the grids marked 'O' will be visited when you dfs(`board[0][0]`). -> Your code won't cause stack overflow in this test case, but if we change the test case a little, it won't work well. -> -> Consider a test case like this: -> -> ``` -> OOOOOOOOOOOX -> XXXXXXXXXXOX -> XOOOOOOOOOOX -> XOXXXXXXXXXX -> XOOOOOOOOOOX -> XXXXXXXXXXOX -> XOOOOOOOOOOX -> XOXXXXXXXXXX -> XOOOOOOOOOOX -> XXXXXXXXXXOX -> ``` -> -> I draw a 10x12 board, but it may be as large as the 250x250 board. -> -> I believe that your code will get "runtime error" in this test case when tested in Leetcode system. - -他的意思就是说,比如下边的例子类型,假如是 `250 × 250` 大小的话,因为我们做的是 `DFS`,一直压栈的话就会造成溢出。 - -```java -OOOOOOOOOOOX -XXXXXXXXXXOX -XOOOOOOOOOOX -XOXXXXXXXXXX -XOOOOOOOOOOX -XXXXXXXXXXOX -XOOOOOOOOOOX -XOXXXXXXXXXX -XOOOOOOOOOOX -XXXXXXXXXXOX -``` - -但是我的代码已经通过了呀,一个可能的原因就是 `leetcode` 升级了,因为这是 `2015` 年的评论,现在是 `2019` 年,压栈的大小足够大了,只要有递归出口,就不用担心压栈放不下了。我就好奇的想测一下 `leetcode` 的压栈到底有多大。写了一个简单的递归代码。 - -```java -public void solve(char[][] board) { - dfs(2677574); -} - -private int dfs(int count) { - if (count == 0) { - return 1; - } - return dfs(count - 1); - -} -``` - -然后一开始传一个较大的数字,然后利用二分法,开始不停试探那个溢出的临界点是多少。经过多次尝试,发现 `2677574` 的话就会造成溢出。`2677573 ` 就不会造成溢出。本以为这样就结束了,然后准备截图总结的时候发现。取 `2677574` 竟然不溢出了,`2677573 ` 反而溢出了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/130_4.jpg) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/130_5.jpg) - -同一个数字,一会儿溢出一会儿不溢出,那就没办法得出结论了。那可能栈的大小和它服务器当前的承载的能力有关了,不过一般情况的栈的大小肯定足够解决题目了。 - -那么退一步讲,如果它的栈的限定很小,这里的 `DFS` 行不通,我们有什么解决方案吗? - -这里我想到两种,一种就是用栈去模拟递归,这里的栈当然就是对象了,存在堆里,就不用担心函数栈溢出了。 - -另一种,利用一个队列,去实现 `BFS`,首先把四个边界的 `O` 加到队列中,然后按照正常的 `BFS` 和之前一样访问连通的 `O` 并且进行标记。最后再把没有标记的 `O` 改成 `X` 就可以了。 - -# 解法三 - -这里再介绍另外一种思想,参考 [这里](),就是并查集,其实本质上和上边的解法是一样的,只是抽象出了一种数据结构,在很多地方都有应用。 - -看下维基百科对 [并查集]() 的定义。 - -> 在[计算机科学](https://zh.wikipedia.org/wiki/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6)中,**并查集**是一种树型的[数据结构](https://zh.wikipedia.org/wiki/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84),用于处理一些[不交集](https://zh.wikipedia.org/wiki/%E4%B8%8D%E4%BA%A4%E9%9B%86)(Disjoint Sets)的合并及查询问题。有一个**联合-查找算法**(**union-find algorithm**)定义了两个用于此数据结构的操作: -> -> - Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。 -> - Union:将两个子集合并成同一个集合。 -> -> 由于支持这两种操作,一个不相交集也常被称为联合-查找数据结构(union-find data structure)或合并-查找集合(merge-find set)。其他的重要方法,MakeSet,用于创建单元素集合。有了这些方法,许多经典的[划分问题](https://zh.wikipedia.org/w/index.php?title=%E5%88%92%E5%88%86%E9%97%AE%E9%A2%98&action=edit&redlink=1)可以被解决。 -> -> 为了更加精确的定义这些方法,需要定义如何表示集合。一种常用的策略是为每个集合选定一个固定的元素,称为代表,以表示整个集合。接着,Find(x) 返回 x 所属集合的代表,而 Union 使用两个集合的代表作为参数。 - -网上很多讲并查集的文章了,这里推荐 [一篇]()。 - -知道了并查集,下边就很好解决了,因为你会发现,我们做的就是分类的问题,`O` 其实最终就是两大类,一种能连通到边界,一种不能连通到边界。 - -首先我们把每个节点各作为一类,用它的行数和列数生成一个 `id` 标识该类。 - -```java -int node(int i, int j) { - return i * cols + j; -} -``` - -然后遍历每个 `O `节点,和它上下左右的节点进行合并即可。 - -如果当前节点是边界的 `O`,就把它和 `dummy` 节点(一个在所有节点外的节点)合并。最后就会把所有连通到边界的 `o` 节点和 `dummy` 节点合为了一类。 - -最后我们只需要判断,每一个 `o` 节点是否和 `dummy` 节点是不是一类即可。 - -```java -public class Solution { - int rows, cols; - - public void solve(char[][] board) { - if(board == null || board.length == 0) return; - - rows = board.length; - cols = board[0].length; - - //多申请一个空间 - UnionFind uf = new UnionFind(rows * cols + 1); - //所有边界的 O 节点都和 dummy 节点合并 - int dummyNode = rows * cols; - - for(int i = 0; i < rows; i++) { - for(int j = 0; j < cols; j++) { - if(board[i][j] == 'O') { - //当前节点在边界就和 dummy 合并 - if(i == 0 || i == rows-1 || j == 0 || j == cols-1) { - uf.union( dummyNode,node(i,j)); - } - else { - //将上下左右的 O 节点和当前节点合并 - if(board[i-1][j] == 'O') uf.union(node(i,j), node(i-1,j)); - if(board[i+1][j] == 'O') uf.union(node(i,j), node(i+1,j)); - if(board[i][j-1] == 'O') uf.union(node(i,j), node(i, j-1)); - if( board[i][j+1] == 'O') uf.union(node(i,j), node(i, j+1)); - } - } - } - } - - for(int i = 0; i < rows; i++) { - for(int j = 0; j < cols; j++) { - //判断是否和 dummy 节点是一类 - if(uf.isConnected(node(i,j), dummyNode)) { - board[i][j] = 'O'; - } - else { - board[i][j] = 'X'; - } - } - } - } - - int node(int i, int j) { - return i * cols + j; - } -} - -class UnionFind { - int [] parents; - public UnionFind(int totalNodes) { - parents = new int[totalNodes]; - for(int i = 0; i < totalNodes; i++) { - parents[i] = i; - } - } - - void union(int node1, int node2) { - int root1 = find(node1); - int root2 = find(node2); - if(root1 != root2) { - parents[root2] = root1; - } - } - - int find(int node) { - while(parents[node] != node) { - parents[node] = parents[parents[node]]; - node = parents[node]; - } - return node; - } - - boolean isConnected(int node1, int node2) { - return find(node1) == find(node2); - } -} -``` - -# 总 - -解法一到解法二仅仅是思路的一个逆转,速度却带来了质的提升。所以有时候走到了死胡同,可以试试往回走。 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/130.jpg) + +有一点点像围棋,把被 `X` 围起来的 `O` 变成 `X`,边界的 `O` 一定不会被围起来。如果 `O` 和边界的 `O` 连通起来,那么这些 `O` 就都算作不被围起来,比如下边的例子。 + +```java +X X X X X +O O O X X +X X X O X +X O X X X +``` + +上边的例子就只需要变化 `1` 个 `O` 。 + +```java +X X X X X +O O O X X +X X X X X +X O X X X +``` + +# 解法一 + +把相邻的`O` 看作是连通的图,然后从每一个 `O` 开始做 `DFS`。 + +如果遍历完成后没有到达边界的 `O` ,我们就把当前 `O` 改成 `X`。 + +如果遍历过程中到达了边界的 `O` ,直接结束 `DFS`,当前的 `O` 就不用操作。 + +然后继续考虑下一个 `O`,继续做一次 `DFS`。 + +```java +public void solve(char[][] board) { + int rows = board.length; + if (rows == 0) { + return; + } + int cols = board[0].length; + //考虑除去边界以外的所有 O + for (int i = 1; i < rows - 1; i++) { + for (int j = 1; j < cols - 1; j++) { + if (board[i][j] == 'O') { + //visited 用于记录 DFS 过程中已经访问过的节点 + HashSet visited = new HashSet<>(); + //如果没有到达边界,就把当前 O 置为 X + if (!solveHelper(i, j, board, visited)) { + board[i][j] = 'X'; + } + } + } + } +} + +private boolean solveHelper(int row, int col, char[][] board, HashSet visited) { + //是否访问过 + if (visited.contains(row + "@" + col)) { + return false; + } + visited.add(row + "@" + col); + + //到达了 X 直接返回 false + if (board[row][col] == 'X') { + return false; + } + + if (row == 0 || row == board.length - 1 || col == 0 || col == board[0].length - 1) { + return true; + } + + //分别尝试四个方向 + if (solveHelper(row - 1, col, board, visited) + || solveHelper(row, col - 1, board, visited) + || solveHelper(row + 1, col, board, visited) + || solveHelper(row, col + 1, board, visited)) { + return true; + } else { + return false; + } +} +``` + +遗憾的是,到最后两个 `test` 的时候超时了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/130_2.jpg) + +优化的的话,我尝试了在每次 `DFS` 过程中,返回 `true` 之前,把当前的 `row` 和 `col` 记录下来,然后第二次遇到这些点的时候,就直接跳过 。 + +```java +public void solve(char[][] board) { + int rows = board.length; + if (rows == 0) { + return; + } + //记录可以连通到边界的 O + HashSet memoization = new HashSet<>(); + int cols = board[0].length; + for (int i = 1; i < rows - 1; i++) { + for (int j = 1; j < cols - 1; j++) { + if (board[i][j] == 'O') { + //如果当前位置的 O 被记录过了,直接跳过 + if (memoization.contains(i + "@" + j)) { + continue; + } + HashSet visited = new HashSet<>(); + if (!solveHelper(i, j, board, visited, memoization)) { + board[i][j] = 'X'; + } + } + } + } +} + +private boolean solveHelper(int row, int col, char[][] board, HashSet visited, + HashSet memoization) { + if (visited.contains(row + "@" + col)) { + return false; + } + visited.add(row + "@" + col); + + if (board[row][col] == 'X') { + return false; + } + //当前位置可以连通到边界,返回 true + if (memoization.contains(row + "@" +col)) { + return true; + } + if (row == 0 || row == board.length - 1 || col == 0 || col == board[0].length - 1) { + //当前位置可以连通道边界,记录下来 + memoization.add(row + "@" + col); + return true; + } + + if (solveHelper(row - 1, col, board, visited, memoization) + || solveHelper(row, col - 1, board, visited, memoization) + || solveHelper(row + 1, col, board, visited, memoization) + || solveHelper(row, col + 1, board, visited, memoization)) { + //当前位置可以连通道边界,记录下来 + memoization.add(row + "@" + col); + return true; + } else { + return false; + } + +} +``` + +但没什么效果,依旧还是超时。 + +之前还考虑过能不能在**遍历过程中**,返回 `false` 之前,直接把 `O` 改成 `X`。最后发现是不可以的,比如下边的例子。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/130_3.jpg) + +如果我们从橙色的 `O` 做 `DFS`,然后沿着箭头方向,到达最后一个 `O` 的时候,它的左边上边右边都是 `X` ,根据代码它就返回 `false`,此外它下边是访问过的节点也会返回 `false`,所以四个方向都返回 `false`,如果把它改成 `X`明显是不对的。 + +# 解法二 + +解法一是从当前节点做 `DFS` ,然后看它是否能到达边界的 `O`。那么我们能不能把思路逆转过来呢? + +从边界的 `O` 做 `DFS`,然后把遇到的 `O` 都标记一下,这些 `O` 就是可以连通到边界的。然后把边界的所有的 `O` 都做一次 `DFS` ,把 `DFS` 过程的中的 `O` 做一下标记。最后我们只需要遍历节点,把没有标记过的 `O` 改成 `X` 就可以了。 + +标记的话,我们可以用一个 `visited` 二维数组,把访问过的置为 `true` 。 + +```java +public void solve(char[][] board) { + int rows = board.length; + if (rows == 0) { + return; + } + int cols = board[0].length; + boolean[][] visited = new boolean[rows][cols]; + for (int i = 0; i < cols; i++) { + //最上边一行的所有 O 做 DFS + if (board[0][i] == 'O') { + dfs(0, i, board, visited); + } + //最下边一行的所有 O 做 DFS + if (board[board.length - 1][i] == 'O') { + dfs(board.length - 1, i, board, visited); + } + + } + for (int i = 1; i < rows - 1; i++) { + //最左边一列的所有 O 做 DFS + if (board[i][0] == 'O') { + dfs(i, 0, board, visited); + } + //最右边一列的所有 O 做 DFS + if (board[i][board[0].length - 1] == 'O') { + dfs(i, board[0].length - 1, board, visited); + } + } + //把所有没有标记过的 O 改为 X + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + //省略了对 X 的判断,把 X 变成 X 不影响结果 + if (!visited[i][j]) { + board[i][j] = 'X'; + } + } + } +} + +private void dfs(int i, int j, char[][] board, boolean[][] visited) { + if (i < 0 || j < 0 || i == board.length || j == board[0].length) { + return; + } + if (visited[i][j]) { + return; + } + if (board[i][j] == 'O') { + visited[i][j] = true; + dfs(i + 1, j, board, visited); + dfs(i, j + 1, board, visited); + dfs(i, j - 1, board, visited); + dfs(i - 1, j, board, visited); + } + +} +``` + +然后这个解法 `AC` 了,但空间复杂度可以优化一下,这个思想很多题用过了,比如 [79 题]()。 + +这里的 `visited` 的二维数组我们可以不需要。我们可以先把连通的 `O` 改成 `*`,或者其他的字符。最后遍历整个 `board`,遇到 `*` 就把它还原到 `O` 。遇到 `O`,因为它没有被修改成`*`,也就意味着它没有连到边界,就把它改成 `X`。 + +```java +public void solve(char[][] board) { + int rows = board.length; + if (rows == 0) { + return; + } + int cols = board[0].length; + for (int i = 0; i < cols; i++) { + //最上边一行的所有 O 做 DFS + if (board[0][i] == 'O') { + dfs(0, i, board); + } + //最下边一行的所有 O 做 DFS + if (board[board.length - 1][i] == 'O') { + dfs(board.length - 1, i, board); + } + + } + for (int i = 1; i < rows - 1; i++) { + //最左边一列的所有 O 做 DFS + if (board[i][0] == 'O') { + dfs(i, 0, board); + } + //最右边一列的所有 O 做 DFS + if (board[i][board[0].length - 1] == 'O') { + dfs(i, board[0].length - 1, board); + } + } + //把所有没有标记过的 O 改为 X,标记过的还原 + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + if (board[i][j] == '*') { + board[i][j] = 'O'; + }else if(board[i][j] == 'O'){ + board[i][j] = 'X'; + } + } + } +} + +private void dfs(int i, int j, char[][] board) { + if (i < 0 || j < 0 || i == board.length || j == board[0].length) { + return; + } + if (board[i][j] == '*') { + return; + } + if (board[i][j] == 'O') { + board[i][j] = '*'; + dfs(i + 1, j, board); + dfs(i, j + 1, board); + dfs(i, j - 1, board); + dfs(i - 1, j, board); + } + +} +``` + + + +但是在逛 `Disscuss` 的时候发现有人提出来说,`DFS` 的解法可能导致栈溢出。 + +这个 [解法]() 下的第一个评论,我把原文贴过来。 + +> This is a DFS solution, but it may causes a stack overflow issue. +> +> When you use DFS, it is tricky to use: +> +> ``` +> if(i>1) +> check(vec,i-1,j,row,col); +> if(j>1) +> check(vec,i,j-1,row,col); +> ``` +> +> because it is more common to write like this: +> +> ``` +> if(i>=1) +> check(vec,i-1,j,row,col); +> if(j>=1) +> check(vec,i,j-1,row,col); +> ``` +> +> Then I'll explain it. +> +> There is a test case like this: +> +> ``` +> OOOOOOOOOO +> XXXXXXXXXO +> OOOOOOOOOO +> OXXXXXXXXX +> OOOOOOOOOO +> XXXXXXXXXO +> OOOOOOOOOO +> OXXXXXXXXX +> OOOOOOOOOO +> XXXXXXXXXO +> ``` +> +> To make it clear, I draw a 10x10 board, but it is actually a 250x250 board like this one. +> +> When dfs function visit `board[0][0]`, it ought to visit every grid marked 'O', thus lead to stack overflow(runtime error). +> +> After you change "if(j>=1)" to "if(j>1)", the DFS function won't check `board[i][0]` (0<=i<=row-1), and then not all the grids marked 'O' will be visited when you dfs(`board[0][0]`). +> Your code won't cause stack overflow in this test case, but if we change the test case a little, it won't work well. +> +> Consider a test case like this: +> +> ``` +> OOOOOOOOOOOX +> XXXXXXXXXXOX +> XOOOOOOOOOOX +> XOXXXXXXXXXX +> XOOOOOOOOOOX +> XXXXXXXXXXOX +> XOOOOOOOOOOX +> XOXXXXXXXXXX +> XOOOOOOOOOOX +> XXXXXXXXXXOX +> ``` +> +> I draw a 10x12 board, but it may be as large as the 250x250 board. +> +> I believe that your code will get "runtime error" in this test case when tested in Leetcode system. + +他的意思就是说,比如下边的例子类型,假如是 `250 × 250` 大小的话,因为我们做的是 `DFS`,一直压栈的话就会造成溢出。 + +```java +OOOOOOOOOOOX +XXXXXXXXXXOX +XOOOOOOOOOOX +XOXXXXXXXXXX +XOOOOOOOOOOX +XXXXXXXXXXOX +XOOOOOOOOOOX +XOXXXXXXXXXX +XOOOOOOOOOOX +XXXXXXXXXXOX +``` + +但是我的代码已经通过了呀,一个可能的原因就是 `leetcode` 升级了,因为这是 `2015` 年的评论,现在是 `2019` 年,压栈的大小足够大了,只要有递归出口,就不用担心压栈放不下了。我就好奇的想测一下 `leetcode` 的压栈到底有多大。写了一个简单的递归代码。 + +```java +public void solve(char[][] board) { + dfs(2677574); +} + +private int dfs(int count) { + if (count == 0) { + return 1; + } + return dfs(count - 1); + +} +``` + +然后一开始传一个较大的数字,然后利用二分法,开始不停试探那个溢出的临界点是多少。经过多次尝试,发现 `2677574` 的话就会造成溢出。`2677573 ` 就不会造成溢出。本以为这样就结束了,然后准备截图总结的时候发现。取 `2677574` 竟然不溢出了,`2677573 ` 反而溢出了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/130_4.jpg) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/130_5.jpg) + +同一个数字,一会儿溢出一会儿不溢出,那就没办法得出结论了。那可能栈的大小和它服务器当前的承载的能力有关了,不过一般情况的栈的大小肯定足够解决题目了。 + +那么退一步讲,如果它的栈的限定很小,这里的 `DFS` 行不通,我们有什么解决方案吗? + +这里我想到两种,一种就是用栈去模拟递归,这里的栈当然就是对象了,存在堆里,就不用担心函数栈溢出了。 + +另一种,利用一个队列,去实现 `BFS`,首先把四个边界的 `O` 加到队列中,然后按照正常的 `BFS` 和之前一样访问连通的 `O` 并且进行标记。最后再把没有标记的 `O` 改成 `X` 就可以了。 + +# 解法三 + +这里再介绍另外一种思想,参考 [这里](),就是并查集,其实本质上和上边的解法是一样的,只是抽象出了一种数据结构,在很多地方都有应用。 + +看下维基百科对 [并查集]() 的定义。 + +> 在[计算机科学](https://zh.wikipedia.org/wiki/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6)中,**并查集**是一种树型的[数据结构](https://zh.wikipedia.org/wiki/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84),用于处理一些[不交集](https://zh.wikipedia.org/wiki/%E4%B8%8D%E4%BA%A4%E9%9B%86)(Disjoint Sets)的合并及查询问题。有一个**联合-查找算法**(**union-find algorithm**)定义了两个用于此数据结构的操作: +> +> - Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。 +> - Union:将两个子集合并成同一个集合。 +> +> 由于支持这两种操作,一个不相交集也常被称为联合-查找数据结构(union-find data structure)或合并-查找集合(merge-find set)。其他的重要方法,MakeSet,用于创建单元素集合。有了这些方法,许多经典的[划分问题](https://zh.wikipedia.org/w/index.php?title=%E5%88%92%E5%88%86%E9%97%AE%E9%A2%98&action=edit&redlink=1)可以被解决。 +> +> 为了更加精确的定义这些方法,需要定义如何表示集合。一种常用的策略是为每个集合选定一个固定的元素,称为代表,以表示整个集合。接着,Find(x) 返回 x 所属集合的代表,而 Union 使用两个集合的代表作为参数。 + +网上很多讲并查集的文章了,这里推荐 [一篇]()。 + +知道了并查集,下边就很好解决了,因为你会发现,我们做的就是分类的问题,`O` 其实最终就是两大类,一种能连通到边界,一种不能连通到边界。 + +首先我们把每个节点各作为一类,用它的行数和列数生成一个 `id` 标识该类。 + +```java +int node(int i, int j) { + return i * cols + j; +} +``` + +然后遍历每个 `O `节点,和它上下左右的节点进行合并即可。 + +如果当前节点是边界的 `O`,就把它和 `dummy` 节点(一个在所有节点外的节点)合并。最后就会把所有连通到边界的 `o` 节点和 `dummy` 节点合为了一类。 + +最后我们只需要判断,每一个 `o` 节点是否和 `dummy` 节点是不是一类即可。 + +```java +public class Solution { + int rows, cols; + + public void solve(char[][] board) { + if(board == null || board.length == 0) return; + + rows = board.length; + cols = board[0].length; + + //多申请一个空间 + UnionFind uf = new UnionFind(rows * cols + 1); + //所有边界的 O 节点都和 dummy 节点合并 + int dummyNode = rows * cols; + + for(int i = 0; i < rows; i++) { + for(int j = 0; j < cols; j++) { + if(board[i][j] == 'O') { + //当前节点在边界就和 dummy 合并 + if(i == 0 || i == rows-1 || j == 0 || j == cols-1) { + uf.union( dummyNode,node(i,j)); + } + else { + //将上下左右的 O 节点和当前节点合并 + if(board[i-1][j] == 'O') uf.union(node(i,j), node(i-1,j)); + if(board[i+1][j] == 'O') uf.union(node(i,j), node(i+1,j)); + if(board[i][j-1] == 'O') uf.union(node(i,j), node(i, j-1)); + if( board[i][j+1] == 'O') uf.union(node(i,j), node(i, j+1)); + } + } + } + } + + for(int i = 0; i < rows; i++) { + for(int j = 0; j < cols; j++) { + //判断是否和 dummy 节点是一类 + if(uf.isConnected(node(i,j), dummyNode)) { + board[i][j] = 'O'; + } + else { + board[i][j] = 'X'; + } + } + } + } + + int node(int i, int j) { + return i * cols + j; + } +} + +class UnionFind { + int [] parents; + public UnionFind(int totalNodes) { + parents = new int[totalNodes]; + for(int i = 0; i < totalNodes; i++) { + parents[i] = i; + } + } + + void union(int node1, int node2) { + int root1 = find(node1); + int root2 = find(node2); + if(root1 != root2) { + parents[root2] = root1; + } + } + + int find(int node) { + while(parents[node] != node) { + parents[node] = parents[parents[node]]; + node = parents[node]; + } + return node; + } + + boolean isConnected(int node1, int node2) { + return find(node1) == find(node2); + } +} +``` + +# 总 + +解法一到解法二仅仅是思路的一个逆转,速度却带来了质的提升。所以有时候走到了死胡同,可以试试往回走。 + 刷这么多题第一次应用到了并查集,并查集说简单点,就是每一类选一个代表,然后类中的其他元素最终都可以找到这个代表。然后遍历其他元素,将它合并到某个类中。 \ No newline at end of file diff --git a/leetcode-131-Palindrome-Partitioning.md b/leetcode-131-Palindrome-Partitioning.md index e96bc503f..1d3901d46 100644 --- a/leetcode-131-Palindrome-Partitioning.md +++ b/leetcode-131-Palindrome-Partitioning.md @@ -1,241 +1,241 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/131.jpg) - -给一个字符串,然后在任意位置切割若干次,保证切割后的每个字符串都是回文串。输出所有满足要求的切割结果。 - -# 解法一 分治 - -将大问题分解为小问题,利用小问题的结果,解决当前大问题。 - -这道题的话,举个例子。 - -```java -aabb -先考虑在第 1 个位置切割,a | abb -这样我们只需要知道 abb 的所有结果,然后在所有结果的头部把 a 加入 -abb 的所有结果就是 [a b b] [a bb] -每个结果头部加入 a,就是 [a a b b] [a a bb] - -aabb -再考虑在第 2 个位置切割,aa | bb -这样我们只需要知道 bb 的所有结果,然后在所有结果的头部把 aa 加入 -bb 的所有结果就是 [b b] [bb] -每个结果头部加入 aa,就是 [aa b b] [aa bb] - -aabb -再考虑在第 3 个位置切割,aab|b -因为 aab 不是回文串,所有直接跳过 - -aabb -再考虑在第 4 个位置切割,aabb | -因为 aabb 不是回文串,所有直接跳过 - -最后所有的结果就是所有的加起来 -[a a b b] [a a bb] [aa b b] [aa bb] -``` - -然后中间的过程求 `abb` 的所有结果,求 `aab` 的所有结果等等,就可以递归的去求。递归出口的话,就是空串的所有子串就是一个空的`list` 即可。 - -```java -public List> partition(String s) { - return partitionHelper(s, 0); -} - -private List> partitionHelper(String s, int start) { - //递归出口,空字符串 - if (start == s.length()) { - List list = new ArrayList<>(); - List> ans = new ArrayList<>(); - ans.add(list); - return ans; - } - List> ans = new ArrayList<>(); - for (int i = start; i < s.length(); i++) { - //当前切割后是回文串才考虑 - if (isPalindrome(s.substring(start, i + 1))) { - String left = s.substring(start, i + 1); - //遍历后边字符串的所有结果,将当前的字符串加到头部 - for (List l : partitionHelper(s, i + 1)) { - l.add(0, left); - ans.add(l); - } - } - - } - return ans; -} - -private boolean isPalindrome(String s) { - int i = 0; - int j = s.length() - 1; - while (i < j) { - if (s.charAt(i) != s.charAt(j)) { - return false; - } - i++; - j--; - } - return true; -} -``` - -分治的话,一般情况下都可以利用动态规划的思想改为迭代的形式。递归就是压栈压栈,然后到达出口就出栈出栈出栈。动态规划就可以把压栈的过程省去,直接从递归出口往回考虑。之前做过很多题了,可以参考 [77题]()、[91 题]()、[115 题]() 等等,都是一样的思想。这道题修改的话,看完解法二的优化后可以参考 [这里]() 的代码。 - -# 解法二 分治优化 - -每次判断一个字符串是否是回文串的时候,我们都会调用 `isPalindrome` 判断。这就会造成一个问题,比如字符串 `abbbba`,期间我们肯定会判断 `bbbb` 是不是回文串,也会判断 `abbbba` 是不是回文串。判断 `abbbba` 是不是回文串的时候,在 `isPalindrome` 中依旧会判断中间的 `bbbb` 部分。而其实如果我们已经知道了 `bbbb` 是回文串,只需要判断 `abbbba` 的开头和末尾字符是否相等即可。 - -所以我们为了避免一些重复判断,可以用动态规划的方法,把所有字符是否是回文串提前存起来。 - -对于字符串 `s`。 - -用 `dp[i][j]` 表示 `s[i,j]` 是否是回文串。 - -然后有 `dp[i][j] = s[i] == s[j] && dp[i+1][j-1]` 。 - -我们只需要两层 `for` 循环,从每个下标开始,考虑所有长度的子串即可。 - -```java -boolean[][] dp = new boolean[s.length()][s.length()]; -int length = s.length(); -//考虑所有长度的子串 -for (int len = 1; len <= length; len++) { - //从每个下标开始 - for (int i = 0; i <= s.length() - len; i++) { - int j = i + len - 1; - dp[i][j] = s.charAt(i) == s.charAt(j) && (len < 3 || dp[i + 1][j - 1]); - } -} -``` - -因为要保证 `dp[i + 1][j - 1]` 中 `i + 1 <= j - 1`, - -```java -i + 1 <= j - 1 -把 j = i + len - 1 代入上式 -i + 1 <= i + len - 1 - 1 -化简得 -len >= 3 -``` - -所以为了保证正确,多加了 `len < 3` 的条件。也就意味着长度是 `1` 和 `2` 的时候,我们只需要判断 `s[i] == s[j]`。 - -然后把 `dp` 传入到递归函数中即可。 - -```java -public List> partition(String s) { - boolean[][] dp = new boolean[s.length()][s.length()]; - int length = s.length(); - for (int len = 1; len <= length; len++) { - for (int i = 0; i <= s.length() - len; i++) { - int j = i + len - 1; - dp[i][j] = s.charAt(i) == s.charAt(j) && (len < 3 || dp[i + 1][j - 1]); - } - } - return partitionHelper(s, 0, dp); -} - -private List> partitionHelper(String s, int start, boolean[][] dp) { - if (start == s.length()) { - List list = new ArrayList<>(); - List> ans = new ArrayList<>(); - ans.add(list); - return ans; - } - List> ans = new ArrayList<>(); - for (int i = start; i < s.length(); i++) { - if (dp[start][i]) { - String left = s.substring(start, i + 1); - for (List l : partitionHelper(s, i + 1, dp)) { - l.add(0, left); - ans.add(l); - } - } - - } - return ans; -} -``` - -# 解法三 回溯 - -[115 题]() 中考虑了分治、回溯、动态规划,这道题同样可以用回溯法。 - -回溯法其实就是一个 `dfs` 的过程,同样举个例子。 - -```java -aabb -先考虑在第 1 个位置切割,a | abb -把 a 加入到结果中 [a] - -然后考虑 abb -先考虑在第 1 个位置切割,a | bb -把 a 加入到结果中 [a a] - -然后考虑 bb -先考虑在第 1 个位置切割,b | b -把 b 加入到结果中 [a a b] - -然后考虑 b -先考虑在第 1 个位置切割,b | -把 b 加入到结果中 [a a b b] - -然后考虑空串 -把结果加到最终结果中 [[a a b b]] - -回溯到上一层 -考虑 bb -考虑在第 2 个位置切割,bb | -把 bb 加入到结果中 [a a bb] - -然后考虑 空串 -把结果加到最终结果中 [[a a b b] [a a bb]] - -然后继续回溯 -``` - -可以看做下边的图做 `dfs` ,而每一层其实就是当前字符串所有可能的回文子串。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/131_2.jpg) - -就是很经典的回溯法,一个 `for` 循环,添加元素,递归,删除元素。这里判断是否是回文串,我们就直接用 `dp` 数组。 - -```java -public List> partition(String s) { - boolean[][] dp = new boolean[s.length()][s.length()]; - int length = s.length(); - for (int len = 1; len <= length; len++) { - for (int i = 0; i <= s.length() - len; i++) { - dp[i][i + len - 1] = s.charAt(i) == s.charAt(i + len - 1) && (len < 3 || dp[i + 1][i + len - 2]); - } - } - List> ans = new ArrayList<>(); - partitionHelper(s, 0, dp, new ArrayList<>(), ans); - return ans; -} - -private void partitionHelper(String s, int start, boolean[][] dp, List temp, List> res) { - //到了空串就加到最终的结果中 - if (start == s.length()) { - res.add(new ArrayList<>(temp)); - } - //在不同位置切割 - for (int i = start; i < s.length(); i++) { - //如果是回文串就加到结果中 - if (dp[start][i]) { - String left = s.substring(start, i + 1); - temp.add(left); - partitionHelper(s, i + 1, dp, temp, res); - temp.remove(temp.size() - 1); - } - - } -} - -``` - -# 总 - -这道题没有什么新内容了,就是分治、回溯、动态规划,很常规的题目了。 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/131.jpg) + +给一个字符串,然后在任意位置切割若干次,保证切割后的每个字符串都是回文串。输出所有满足要求的切割结果。 + +# 解法一 分治 + +将大问题分解为小问题,利用小问题的结果,解决当前大问题。 + +这道题的话,举个例子。 + +```java +aabb +先考虑在第 1 个位置切割,a | abb +这样我们只需要知道 abb 的所有结果,然后在所有结果的头部把 a 加入 +abb 的所有结果就是 [a b b] [a bb] +每个结果头部加入 a,就是 [a a b b] [a a bb] + +aabb +再考虑在第 2 个位置切割,aa | bb +这样我们只需要知道 bb 的所有结果,然后在所有结果的头部把 aa 加入 +bb 的所有结果就是 [b b] [bb] +每个结果头部加入 aa,就是 [aa b b] [aa bb] + +aabb +再考虑在第 3 个位置切割,aab|b +因为 aab 不是回文串,所有直接跳过 + +aabb +再考虑在第 4 个位置切割,aabb | +因为 aabb 不是回文串,所有直接跳过 + +最后所有的结果就是所有的加起来 +[a a b b] [a a bb] [aa b b] [aa bb] +``` + +然后中间的过程求 `abb` 的所有结果,求 `aab` 的所有结果等等,就可以递归的去求。递归出口的话,就是空串的所有子串就是一个空的`list` 即可。 + +```java +public List> partition(String s) { + return partitionHelper(s, 0); +} + +private List> partitionHelper(String s, int start) { + //递归出口,空字符串 + if (start == s.length()) { + List list = new ArrayList<>(); + List> ans = new ArrayList<>(); + ans.add(list); + return ans; + } + List> ans = new ArrayList<>(); + for (int i = start; i < s.length(); i++) { + //当前切割后是回文串才考虑 + if (isPalindrome(s.substring(start, i + 1))) { + String left = s.substring(start, i + 1); + //遍历后边字符串的所有结果,将当前的字符串加到头部 + for (List l : partitionHelper(s, i + 1)) { + l.add(0, left); + ans.add(l); + } + } + + } + return ans; +} + +private boolean isPalindrome(String s) { + int i = 0; + int j = s.length() - 1; + while (i < j) { + if (s.charAt(i) != s.charAt(j)) { + return false; + } + i++; + j--; + } + return true; +} +``` + +分治的话,一般情况下都可以利用动态规划的思想改为迭代的形式。递归就是压栈压栈,然后到达出口就出栈出栈出栈。动态规划就可以把压栈的过程省去,直接从递归出口往回考虑。之前做过很多题了,可以参考 [77题]()、[91 题]()、[115 题]() 等等,都是一样的思想。这道题修改的话,看完解法二的优化后可以参考 [这里]() 的代码。 + +# 解法二 分治优化 + +每次判断一个字符串是否是回文串的时候,我们都会调用 `isPalindrome` 判断。这就会造成一个问题,比如字符串 `abbbba`,期间我们肯定会判断 `bbbb` 是不是回文串,也会判断 `abbbba` 是不是回文串。判断 `abbbba` 是不是回文串的时候,在 `isPalindrome` 中依旧会判断中间的 `bbbb` 部分。而其实如果我们已经知道了 `bbbb` 是回文串,只需要判断 `abbbba` 的开头和末尾字符是否相等即可。 + +所以我们为了避免一些重复判断,可以用动态规划的方法,把所有字符是否是回文串提前存起来。 + +对于字符串 `s`。 + +用 `dp[i][j]` 表示 `s[i,j]` 是否是回文串。 + +然后有 `dp[i][j] = s[i] == s[j] && dp[i+1][j-1]` 。 + +我们只需要两层 `for` 循环,从每个下标开始,考虑所有长度的子串即可。 + +```java +boolean[][] dp = new boolean[s.length()][s.length()]; +int length = s.length(); +//考虑所有长度的子串 +for (int len = 1; len <= length; len++) { + //从每个下标开始 + for (int i = 0; i <= s.length() - len; i++) { + int j = i + len - 1; + dp[i][j] = s.charAt(i) == s.charAt(j) && (len < 3 || dp[i + 1][j - 1]); + } +} +``` + +因为要保证 `dp[i + 1][j - 1]` 中 `i + 1 <= j - 1`, + +```java +i + 1 <= j - 1 +把 j = i + len - 1 代入上式 +i + 1 <= i + len - 1 - 1 +化简得 +len >= 3 +``` + +所以为了保证正确,多加了 `len < 3` 的条件。也就意味着长度是 `1` 和 `2` 的时候,我们只需要判断 `s[i] == s[j]`。 + +然后把 `dp` 传入到递归函数中即可。 + +```java +public List> partition(String s) { + boolean[][] dp = new boolean[s.length()][s.length()]; + int length = s.length(); + for (int len = 1; len <= length; len++) { + for (int i = 0; i <= s.length() - len; i++) { + int j = i + len - 1; + dp[i][j] = s.charAt(i) == s.charAt(j) && (len < 3 || dp[i + 1][j - 1]); + } + } + return partitionHelper(s, 0, dp); +} + +private List> partitionHelper(String s, int start, boolean[][] dp) { + if (start == s.length()) { + List list = new ArrayList<>(); + List> ans = new ArrayList<>(); + ans.add(list); + return ans; + } + List> ans = new ArrayList<>(); + for (int i = start; i < s.length(); i++) { + if (dp[start][i]) { + String left = s.substring(start, i + 1); + for (List l : partitionHelper(s, i + 1, dp)) { + l.add(0, left); + ans.add(l); + } + } + + } + return ans; +} +``` + +# 解法三 回溯 + +[115 题]() 中考虑了分治、回溯、动态规划,这道题同样可以用回溯法。 + +回溯法其实就是一个 `dfs` 的过程,同样举个例子。 + +```java +aabb +先考虑在第 1 个位置切割,a | abb +把 a 加入到结果中 [a] + +然后考虑 abb +先考虑在第 1 个位置切割,a | bb +把 a 加入到结果中 [a a] + +然后考虑 bb +先考虑在第 1 个位置切割,b | b +把 b 加入到结果中 [a a b] + +然后考虑 b +先考虑在第 1 个位置切割,b | +把 b 加入到结果中 [a a b b] + +然后考虑空串 +把结果加到最终结果中 [[a a b b]] + +回溯到上一层 +考虑 bb +考虑在第 2 个位置切割,bb | +把 bb 加入到结果中 [a a bb] + +然后考虑 空串 +把结果加到最终结果中 [[a a b b] [a a bb]] + +然后继续回溯 +``` + +可以看做下边的图做 `dfs` ,而每一层其实就是当前字符串所有可能的回文子串。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/131_2.jpg) + +就是很经典的回溯法,一个 `for` 循环,添加元素,递归,删除元素。这里判断是否是回文串,我们就直接用 `dp` 数组。 + +```java +public List> partition(String s) { + boolean[][] dp = new boolean[s.length()][s.length()]; + int length = s.length(); + for (int len = 1; len <= length; len++) { + for (int i = 0; i <= s.length() - len; i++) { + dp[i][i + len - 1] = s.charAt(i) == s.charAt(i + len - 1) && (len < 3 || dp[i + 1][i + len - 2]); + } + } + List> ans = new ArrayList<>(); + partitionHelper(s, 0, dp, new ArrayList<>(), ans); + return ans; +} + +private void partitionHelper(String s, int start, boolean[][] dp, List temp, List> res) { + //到了空串就加到最终的结果中 + if (start == s.length()) { + res.add(new ArrayList<>(temp)); + } + //在不同位置切割 + for (int i = start; i < s.length(); i++) { + //如果是回文串就加到结果中 + if (dp[start][i]) { + String left = s.substring(start, i + 1); + temp.add(left); + partitionHelper(s, i + 1, dp, temp, res); + temp.remove(temp.size() - 1); + } + + } +} + +``` + +# 总 + +这道题没有什么新内容了,就是分治、回溯、动态规划,很常规的题目了。 + diff --git a/leetcode-132-Palindrome-PartitioningII.md b/leetcode-132-Palindrome-PartitioningII.md index d23a9ed0d..911ddf3c4 100644 --- a/leetcode-132-Palindrome-PartitioningII.md +++ b/leetcode-132-Palindrome-PartitioningII.md @@ -1,411 +1,411 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/132.jpg) - -和 [131 题]() 一样,可以在任意位置切割字符串,需要保证切割后的每个子串都是回文串。问最少需要切割几次。 - -和 [131 题]() 用相同的分析方法即可。 - -# 解法一 分治 - -大问题化小问题,利用小问题的结果,解决当前大问题。 - -举个例子。 - -```java -aabb -先考虑在第 1 个位置切割,a | abb -这样我们只需要知道 abb 的最小切割次数,然后加 1,记为 m1 - -aabb -再考虑在第 2 个位置切割,aa | bb -这样我们只需要知道 bb 的所有结果,然后加 1,记为 m2 - - -aabb -再考虑在第 3 个位置切割,aab|b -因为 aab 不是回文串,所有直接跳过 - -aabb -再考虑在第 4 个位置切割,aabb | -因为 aabb 不是回文串,所有直接跳过 - -此时只需要比较 m1 和 m2 的大小,选一个较小的即可。 -``` - -然后中间的过程求 `abb` 的最小切割次数,求 `aab` 的最小切割次数等等,就可以递归的去求。递归出口的话,如果字符串的长度为 `1`,那么它就是回文串了,最小切割次数就是 `0` 。 - -此外,和 [131 题]() 一样,我们用一个 `dp` 把每个子串是否是回文串,提前存起来。 - -```java -public int minCut(String s) { - boolean[][] dp = new boolean[s.length()][s.length()]; - int length = s.length(); - for (int len = 1; len <= length; len++) { - for (int i = 0; i <= s.length() - len; i++) { - int j = i + len - 1; - dp[i][j] = s.charAt(i) == s.charAt(j) && (len < 3 || dp[i + 1][j - 1]); - } - } - return minCutHelper(s, 0, dp); - -} - -private int minCutHelper(String s, int start, boolean[][] dp) { - //长度是 1 ,最小切割次数就是 0 - if (dp[start][s.length() - 1]) { - return 0; - } - int min = Integer.MAX_VALUE; - for (int i = start; i < s.length(); i++) { - //只考虑回文串 - if (dp[start][i]) { - //和之前的值比较选一个较小的 - min = Math.min(min, 1 + minCutHelper(s, i + 1, dp)); - } - } - return min; -} -``` - -意料之中,超时了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/132_2.jpg) - -优化方法的话,`memoization` 技术,前边很多题都用到了,比如 [87 题](),[91 题]() 等等。就是为了解决递归过程中重复解的计算,典型例子比如斐波那契数列。用一个 `map` ,把递归过程中的结果存储起来。 - -```java -public int minCut(String s) { - boolean[][] dp = new boolean[s.length()][s.length()]; - int length = s.length(); - HashMap map = new HashMap<>(); - for (int len = 1; len <= length; len++) { - for (int i = 0; i <= s.length() - len; i++) { - int j = i + len - 1; - dp[i][j] = s.charAt(i) == s.charAt(j) && (len < 3 || dp[i + 1][j - 1]); - } - } - return minCutHelper(s, 0, dp, map); - -} - -private int minCutHelper(String s, int start, boolean[][] dp, HashMap map) { - - if (map.containsKey(start)) { - return map.get(start); - } - if (dp[start][s.length() - 1]) { - return 0; - } - int min = Integer.MAX_VALUE; - for (int i = start; i < s.length(); i++) { - if (dp[start][i]) { - min = Math.min(min, 1 + minCutHelper(s, i + 1, dp, map)); - } - } - map.put(start, min); - return min; -} -``` - -接下来还是一样的讨论,既然用到了 `memoization` 技术,一定就可以把它改写为动态规划,让我们理一下递归的思路。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/132_3.jpg) - -如上图,图中 `a,b,c,d` 表示括起来的字符串的最小切割次数。此时需要求问号处括起来的字符串的最小切割次数。 - -对应于代码中的下边这一部分了。 - -```java -int min = Integer.MAX_VALUE; -for (int i = start; i < s.length(); i++) { - if (dp[start][i]) { - min = Math.min(min, 1 + minCutHelper(s, i + 1, dp, map)); - } -} -``` - -如下图,先判断 `start` 到 `i` 是否是回文串,如果是的话,就用 `1 + d` 和之前的 `min` 比较。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/132_4.jpg) - -如下图,`i` 后移,继续判断 `start` 到 `i` 是否是回文串,如果是的话,就用 `1 + c` 和之前的 `min` 比较。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/132_5.jpg) - -然后 `i` 继续后移重复上边的过程。每次选一个较小的切割次数,最后问号处就求出来了。 - -接着 `start` 继续前移,重复上边的过程,直到求出 `start` 等于 `0` 的最小切割次数就是我们要找的了。 - -仔细考虑下上边的状态,其实状态转移方程也就出来了。 - -用 `dp[i]` 表示字符串 `s[i,s.lenght-1]`,也就是从 `i` 开始到末尾的字符串的最小切割次数。 - -求 `dp[i]` 的话,假设 `s[i,j]` 是回文串。 - -那么 `dp[i] = Min(min,dp[j + 1])`. - -然后考虑所有的 `j`,其中 `j > i` ,找出最小的即可。 - -当然上边的动态规划和递归的方向是一样的,也没什么毛病。不过我们也可以逆过来,从左往右求。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/132_6.jpg) - -这样的话,用 `dp[i]` 表示字符串 `s[0,i]`,也就是从开头到 `i` 的字符串的最小切割次数。 - -求 `dp[i]` 的话,假设 `s[j,i]` 是回文串。 - -那么 `dp[i] = Min(min,dp[j - 1])`. - -然后考虑所有的 `j`,也就是 `j = i, j = i - 1, j = i - 2, j = i - 3....` ,其中 `j < i` ,找出最小的即可。 - -之前代码用过 `dp` 变量了,所以用 `min` 变量表示上边的 `dp`。 - -```java -public int minCut(String s) { - boolean[][] dp = new boolean[s.length()][s.length()]; - int length = s.length(); - for (int len = 1; len <= length; len++) { - for (int i = 0; i <= s.length() - len; i++) { - int j = i + len - 1; - dp[i][j] = s.charAt(i) == s.charAt(j) && (len < 3 || dp[i + 1][j - 1]); - } - } - int[] min = new int[s.length()]; - min[0] = 0; - for (int i = 1; i < s.length(); i++) { - int temp = Integer.MAX_VALUE; //找出最小的 - for (int j = 0; j <= i; j++) { - if (dp[j][i]) { - //从开头就匹配,不需要切割 - if (j == 0) { - temp = 0; - break; - //正常的情况 - } else { - temp = Math.min(temp, min[j - 1] + 1); - } - } - } - min[i] = temp; - - } - return min[s.length() - 1]; -} -``` - -当然我们可以优化一下,注意到求 `dp` 和 求 `min` 的时候都用到了两个 `for` 循环,同样都是根据前边的状态更新当前的状态,所以我们可以把他们糅合在一起。 - -```java -public int minCut(String s) { - boolean[][] dp = new boolean[s.length()][s.length()]; - int[] min = new int[s.length()]; - min[0] = 0; - for (int i = 1; i < s.length(); i++) { - int temp = Integer.MAX_VALUE; - for (int j = 0; j <= i; j++) { - if (s.charAt(j) == s.charAt(i) && (j + 1 > i - 1 || dp[j + 1][i - 1])) { - dp[j][i] = true; - if (j == 0) { - temp = 0; - } else { - temp = Math.min(temp, min[j - 1] + 1); - } - } - } - min[i] = temp; - - } - return min[s.length() - 1]; - -} -``` - -# 解法二 回溯 - -回溯法其实就是一个 `dfs` 的过程。在当前字符串找到第一个回文串的位置,然后切割。剩余的字符串进入递归,继续找回文串的位置,然后切割。直到剩余的字符串本身已经是一个回文串了,就记录已经切过的次数。 - -可以用一个全局变量,保存已经切过的次数,然后到最后更新。 - -```java -public int minCut(String s) { - boolean[][] dp = new boolean[s.length()][s.length()]; - int length = s.length(); - - for (int len = 1; len <= length; len++) { - for (int i = 0; i <= s.length() - len; i++) { - int j = i + len - 1; - dp[i][j] = s.charAt(i) == s.charAt(j) && (len < 3 || dp[i + 1][j - 1]); - } - } - minCutHelper(s, 0, dp, 0); - return min; - -} - -int min = Integer.MAX_VALUE; -//num 记录已经切割的次数 -private void minCutHelper(String s, int start, boolean[][] dp, int num) { - if (dp[start][s.length() - 1]) { - min = Math.min(min, num); - return; - } - //尝试当前字符串所有的切割位置 - for (int i = start; i < s.length() - 1; i++) { - if (dp[start][i]) { - minCutHelper(s, i + 1, dp, num + 1); - } - } -} - -``` - -同样出现了超时的问题。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/132_2.jpg) - -我们可以像解法一一样优化一下,用一个 `map` 存一下递归过程的中的解。那么问题来了,解法一是把返回值存了起来,但是这个解法并没有返回值,那么我们存什么呢?和 [115 题]() 一样,存增量。什么意思呢? - -我们知道 `minCutHelper`函数是计算了从 `start` 开始的字符串,全部切割完成后还需要切割的次数,并且当前已经切割了 `num` 次。也就是执行完下边的 `for` 循环后,如果全局变量`min` 更新了,那么多切割的次数就是 `min - num`,我们把它存起来就可以了。如果 `min` 没更新,那就不用管了。 - -```java -for (int i = start; i < s.length() - 1; i++) { - if (dp[start][i]) { - minCutHelper(s, i + 1, dp, num + 1); - } -} -``` - -这样只需要在进入递归前,判断之前有没有算过从 `start` 开始的字符串所带来的增量即可。 - -```java -public int minCut(String s) { - boolean[][] dp = new boolean[s.length()][s.length()]; - int length = s.length(); - - for (int len = 1; len <= length; len++) { - for (int i = 0; i <= s.length() - len; i++) { - int j = i + len - 1; - dp[i][j] = s.charAt(i) == s.charAt(j) && (len < 3 || dp[i + 1][j - 1]); - } - } - HashMap map = new HashMap<>(); - minCutHelper(s, 0, dp, 0, map); - return min; - -} - -int min = Integer.MAX_VALUE; - -private void minCutHelper(String s, int start, boolean[][] dp, int num, HashMap map) { - //直接利用之前存的增量 - if (map.containsKey(start)) { - min = Math.min(min, num + map.get(start)); - return; - } - - if (dp[start][s.length() - 1]) { - min = Math.min(min, num); - return; - } - for (int i = start; i < s.length() - 1; i++) { - if (dp[start][i]) { - minCutHelper(s, i + 1, dp, num + 1, map); - - } - } - // min 是否更新了 - if (min > num) { - map.put(start, min - num); - } -} -``` - -# 解法三 - -上边的解法是一些通用的思考方式,针对这道题还有一种解法,在 [这里]() 看到的,也分享一下吧。 - -同样也是动态规划的思路,用 `dp[i]` 表示字符串 `s[0,i]`,也就是从开头到 `i` 的字符串的最小切割次数。相比于之前更新 `dp` 的方式,这里的话把之前存储每个子串是否是回文串的空间省去了。 - -基本思想就是遍历每个字符,以当前字符为中心向两边扩展,判断扩展出来的是否回文串,比如下边的例子。 - -```java -0 1 2 3 4 5 6 -c f d a d f e - ^ - c -现在以 a 为中心向两边扩展,此时第 2 个和第 4 个字符相等,我们就可以更新 -dp[4] = Min(dp[4],dp[1] + 1); -也就是在当前回文串前边切一刀 - -然后以 a 为中心继续向两边扩展,此时第 1 个和第 5 个字符相等,我们就可以更新 -dp[5] = Min(dp[5],dp[0] + 1); -也就是在当前回文串前边切一刀 - -然后继续扩展,直到当前不再是回文串,把中心往后移动,考虑以 d 为中心,继续更新 dp -``` - -当然上边是回文串为奇数的情况,我们还需要考虑以当前字符为中心的偶数的情况,是一样的道理。 - -可以参考下边的代码。 - -```java -public int minCut(String s) { - int[] dp = new int[s.length()]; - int n = s.length(); - //假设没有任何的回文串,初始化 dp - for (int i = 0; i < n; i++) { - dp[i] = i; - } - - // 考虑每个中心 - for (int i = 0; i < s.length(); i++) { - // j 表示某一个方向扩展的个数 - int j = 0; - // 考虑奇数的情况 - while (true) { - if (i - j < 0 || i + j > n - 1) { - break; - } - if (s.charAt(i - j) == s.charAt(i + j)) { - if (i - j == 0) { - dp[i + j] = 0; - } else { - dp[i + j] = Math.min(dp[i + j], dp[i - j - 1] + 1); - } - - } else { - break; - } - j++; - } - - // j 表示某一个方向扩展的个数 - j = 1; - // 考虑偶数的情况 - while (true) { - if (i - j + 1 < 0 || i + j > n - 1) { - break; - } - if (s.charAt(i - j + 1) == s.charAt(i + j)) { - if (i - j + 1 == 0) { - dp[i + j] = 0; - } else { - dp[i + j] = Math.min(dp[i + j], dp[i - j + 1 - 1] + 1); - } - - } else { - break; - } - j++; - } - - } - return dp[n - 1]; -} -``` - -# 总 - -前边的解法还是很常规,从递归到动态规划,利用分治或者回溯,以及 `memoization` 技术,经常用到了。最后一个解法,边找回文串边更新 `dp` ,从而降低了空间复杂度,也是很妙了。 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/132.jpg) + +和 [131 题]() 一样,可以在任意位置切割字符串,需要保证切割后的每个子串都是回文串。问最少需要切割几次。 + +和 [131 题]() 用相同的分析方法即可。 + +# 解法一 分治 + +大问题化小问题,利用小问题的结果,解决当前大问题。 + +举个例子。 + +```java +aabb +先考虑在第 1 个位置切割,a | abb +这样我们只需要知道 abb 的最小切割次数,然后加 1,记为 m1 + +aabb +再考虑在第 2 个位置切割,aa | bb +这样我们只需要知道 bb 的所有结果,然后加 1,记为 m2 + + +aabb +再考虑在第 3 个位置切割,aab|b +因为 aab 不是回文串,所有直接跳过 + +aabb +再考虑在第 4 个位置切割,aabb | +因为 aabb 不是回文串,所有直接跳过 + +此时只需要比较 m1 和 m2 的大小,选一个较小的即可。 +``` + +然后中间的过程求 `abb` 的最小切割次数,求 `aab` 的最小切割次数等等,就可以递归的去求。递归出口的话,如果字符串的长度为 `1`,那么它就是回文串了,最小切割次数就是 `0` 。 + +此外,和 [131 题]() 一样,我们用一个 `dp` 把每个子串是否是回文串,提前存起来。 + +```java +public int minCut(String s) { + boolean[][] dp = new boolean[s.length()][s.length()]; + int length = s.length(); + for (int len = 1; len <= length; len++) { + for (int i = 0; i <= s.length() - len; i++) { + int j = i + len - 1; + dp[i][j] = s.charAt(i) == s.charAt(j) && (len < 3 || dp[i + 1][j - 1]); + } + } + return minCutHelper(s, 0, dp); + +} + +private int minCutHelper(String s, int start, boolean[][] dp) { + //长度是 1 ,最小切割次数就是 0 + if (dp[start][s.length() - 1]) { + return 0; + } + int min = Integer.MAX_VALUE; + for (int i = start; i < s.length(); i++) { + //只考虑回文串 + if (dp[start][i]) { + //和之前的值比较选一个较小的 + min = Math.min(min, 1 + minCutHelper(s, i + 1, dp)); + } + } + return min; +} +``` + +意料之中,超时了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/132_2.jpg) + +优化方法的话,`memoization` 技术,前边很多题都用到了,比如 [87 题](),[91 题]() 等等。就是为了解决递归过程中重复解的计算,典型例子比如斐波那契数列。用一个 `map` ,把递归过程中的结果存储起来。 + +```java +public int minCut(String s) { + boolean[][] dp = new boolean[s.length()][s.length()]; + int length = s.length(); + HashMap map = new HashMap<>(); + for (int len = 1; len <= length; len++) { + for (int i = 0; i <= s.length() - len; i++) { + int j = i + len - 1; + dp[i][j] = s.charAt(i) == s.charAt(j) && (len < 3 || dp[i + 1][j - 1]); + } + } + return minCutHelper(s, 0, dp, map); + +} + +private int minCutHelper(String s, int start, boolean[][] dp, HashMap map) { + + if (map.containsKey(start)) { + return map.get(start); + } + if (dp[start][s.length() - 1]) { + return 0; + } + int min = Integer.MAX_VALUE; + for (int i = start; i < s.length(); i++) { + if (dp[start][i]) { + min = Math.min(min, 1 + minCutHelper(s, i + 1, dp, map)); + } + } + map.put(start, min); + return min; +} +``` + +接下来还是一样的讨论,既然用到了 `memoization` 技术,一定就可以把它改写为动态规划,让我们理一下递归的思路。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/132_3.jpg) + +如上图,图中 `a,b,c,d` 表示括起来的字符串的最小切割次数。此时需要求问号处括起来的字符串的最小切割次数。 + +对应于代码中的下边这一部分了。 + +```java +int min = Integer.MAX_VALUE; +for (int i = start; i < s.length(); i++) { + if (dp[start][i]) { + min = Math.min(min, 1 + minCutHelper(s, i + 1, dp, map)); + } +} +``` + +如下图,先判断 `start` 到 `i` 是否是回文串,如果是的话,就用 `1 + d` 和之前的 `min` 比较。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/132_4.jpg) + +如下图,`i` 后移,继续判断 `start` 到 `i` 是否是回文串,如果是的话,就用 `1 + c` 和之前的 `min` 比较。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/132_5.jpg) + +然后 `i` 继续后移重复上边的过程。每次选一个较小的切割次数,最后问号处就求出来了。 + +接着 `start` 继续前移,重复上边的过程,直到求出 `start` 等于 `0` 的最小切割次数就是我们要找的了。 + +仔细考虑下上边的状态,其实状态转移方程也就出来了。 + +用 `dp[i]` 表示字符串 `s[i,s.lenght-1]`,也就是从 `i` 开始到末尾的字符串的最小切割次数。 + +求 `dp[i]` 的话,假设 `s[i,j]` 是回文串。 + +那么 `dp[i] = Min(min,dp[j + 1])`. + +然后考虑所有的 `j`,其中 `j > i` ,找出最小的即可。 + +当然上边的动态规划和递归的方向是一样的,也没什么毛病。不过我们也可以逆过来,从左往右求。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/132_6.jpg) + +这样的话,用 `dp[i]` 表示字符串 `s[0,i]`,也就是从开头到 `i` 的字符串的最小切割次数。 + +求 `dp[i]` 的话,假设 `s[j,i]` 是回文串。 + +那么 `dp[i] = Min(min,dp[j - 1])`. + +然后考虑所有的 `j`,也就是 `j = i, j = i - 1, j = i - 2, j = i - 3....` ,其中 `j < i` ,找出最小的即可。 + +之前代码用过 `dp` 变量了,所以用 `min` 变量表示上边的 `dp`。 + +```java +public int minCut(String s) { + boolean[][] dp = new boolean[s.length()][s.length()]; + int length = s.length(); + for (int len = 1; len <= length; len++) { + for (int i = 0; i <= s.length() - len; i++) { + int j = i + len - 1; + dp[i][j] = s.charAt(i) == s.charAt(j) && (len < 3 || dp[i + 1][j - 1]); + } + } + int[] min = new int[s.length()]; + min[0] = 0; + for (int i = 1; i < s.length(); i++) { + int temp = Integer.MAX_VALUE; //找出最小的 + for (int j = 0; j <= i; j++) { + if (dp[j][i]) { + //从开头就匹配,不需要切割 + if (j == 0) { + temp = 0; + break; + //正常的情况 + } else { + temp = Math.min(temp, min[j - 1] + 1); + } + } + } + min[i] = temp; + + } + return min[s.length() - 1]; +} +``` + +当然我们可以优化一下,注意到求 `dp` 和 求 `min` 的时候都用到了两个 `for` 循环,同样都是根据前边的状态更新当前的状态,所以我们可以把他们糅合在一起。 + +```java +public int minCut(String s) { + boolean[][] dp = new boolean[s.length()][s.length()]; + int[] min = new int[s.length()]; + min[0] = 0; + for (int i = 1; i < s.length(); i++) { + int temp = Integer.MAX_VALUE; + for (int j = 0; j <= i; j++) { + if (s.charAt(j) == s.charAt(i) && (j + 1 > i - 1 || dp[j + 1][i - 1])) { + dp[j][i] = true; + if (j == 0) { + temp = 0; + } else { + temp = Math.min(temp, min[j - 1] + 1); + } + } + } + min[i] = temp; + + } + return min[s.length() - 1]; + +} +``` + +# 解法二 回溯 + +回溯法其实就是一个 `dfs` 的过程。在当前字符串找到第一个回文串的位置,然后切割。剩余的字符串进入递归,继续找回文串的位置,然后切割。直到剩余的字符串本身已经是一个回文串了,就记录已经切过的次数。 + +可以用一个全局变量,保存已经切过的次数,然后到最后更新。 + +```java +public int minCut(String s) { + boolean[][] dp = new boolean[s.length()][s.length()]; + int length = s.length(); + + for (int len = 1; len <= length; len++) { + for (int i = 0; i <= s.length() - len; i++) { + int j = i + len - 1; + dp[i][j] = s.charAt(i) == s.charAt(j) && (len < 3 || dp[i + 1][j - 1]); + } + } + minCutHelper(s, 0, dp, 0); + return min; + +} + +int min = Integer.MAX_VALUE; +//num 记录已经切割的次数 +private void minCutHelper(String s, int start, boolean[][] dp, int num) { + if (dp[start][s.length() - 1]) { + min = Math.min(min, num); + return; + } + //尝试当前字符串所有的切割位置 + for (int i = start; i < s.length() - 1; i++) { + if (dp[start][i]) { + minCutHelper(s, i + 1, dp, num + 1); + } + } +} + +``` + +同样出现了超时的问题。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/132_2.jpg) + +我们可以像解法一一样优化一下,用一个 `map` 存一下递归过程的中的解。那么问题来了,解法一是把返回值存了起来,但是这个解法并没有返回值,那么我们存什么呢?和 [115 题]() 一样,存增量。什么意思呢? + +我们知道 `minCutHelper`函数是计算了从 `start` 开始的字符串,全部切割完成后还需要切割的次数,并且当前已经切割了 `num` 次。也就是执行完下边的 `for` 循环后,如果全局变量`min` 更新了,那么多切割的次数就是 `min - num`,我们把它存起来就可以了。如果 `min` 没更新,那就不用管了。 + +```java +for (int i = start; i < s.length() - 1; i++) { + if (dp[start][i]) { + minCutHelper(s, i + 1, dp, num + 1); + } +} +``` + +这样只需要在进入递归前,判断之前有没有算过从 `start` 开始的字符串所带来的增量即可。 + +```java +public int minCut(String s) { + boolean[][] dp = new boolean[s.length()][s.length()]; + int length = s.length(); + + for (int len = 1; len <= length; len++) { + for (int i = 0; i <= s.length() - len; i++) { + int j = i + len - 1; + dp[i][j] = s.charAt(i) == s.charAt(j) && (len < 3 || dp[i + 1][j - 1]); + } + } + HashMap map = new HashMap<>(); + minCutHelper(s, 0, dp, 0, map); + return min; + +} + +int min = Integer.MAX_VALUE; + +private void minCutHelper(String s, int start, boolean[][] dp, int num, HashMap map) { + //直接利用之前存的增量 + if (map.containsKey(start)) { + min = Math.min(min, num + map.get(start)); + return; + } + + if (dp[start][s.length() - 1]) { + min = Math.min(min, num); + return; + } + for (int i = start; i < s.length() - 1; i++) { + if (dp[start][i]) { + minCutHelper(s, i + 1, dp, num + 1, map); + + } + } + // min 是否更新了 + if (min > num) { + map.put(start, min - num); + } +} +``` + +# 解法三 + +上边的解法是一些通用的思考方式,针对这道题还有一种解法,在 [这里]() 看到的,也分享一下吧。 + +同样也是动态规划的思路,用 `dp[i]` 表示字符串 `s[0,i]`,也就是从开头到 `i` 的字符串的最小切割次数。相比于之前更新 `dp` 的方式,这里的话把之前存储每个子串是否是回文串的空间省去了。 + +基本思想就是遍历每个字符,以当前字符为中心向两边扩展,判断扩展出来的是否回文串,比如下边的例子。 + +```java +0 1 2 3 4 5 6 +c f d a d f e + ^ + c +现在以 a 为中心向两边扩展,此时第 2 个和第 4 个字符相等,我们就可以更新 +dp[4] = Min(dp[4],dp[1] + 1); +也就是在当前回文串前边切一刀 + +然后以 a 为中心继续向两边扩展,此时第 1 个和第 5 个字符相等,我们就可以更新 +dp[5] = Min(dp[5],dp[0] + 1); +也就是在当前回文串前边切一刀 + +然后继续扩展,直到当前不再是回文串,把中心往后移动,考虑以 d 为中心,继续更新 dp +``` + +当然上边是回文串为奇数的情况,我们还需要考虑以当前字符为中心的偶数的情况,是一样的道理。 + +可以参考下边的代码。 + +```java +public int minCut(String s) { + int[] dp = new int[s.length()]; + int n = s.length(); + //假设没有任何的回文串,初始化 dp + for (int i = 0; i < n; i++) { + dp[i] = i; + } + + // 考虑每个中心 + for (int i = 0; i < s.length(); i++) { + // j 表示某一个方向扩展的个数 + int j = 0; + // 考虑奇数的情况 + while (true) { + if (i - j < 0 || i + j > n - 1) { + break; + } + if (s.charAt(i - j) == s.charAt(i + j)) { + if (i - j == 0) { + dp[i + j] = 0; + } else { + dp[i + j] = Math.min(dp[i + j], dp[i - j - 1] + 1); + } + + } else { + break; + } + j++; + } + + // j 表示某一个方向扩展的个数 + j = 1; + // 考虑偶数的情况 + while (true) { + if (i - j + 1 < 0 || i + j > n - 1) { + break; + } + if (s.charAt(i - j + 1) == s.charAt(i + j)) { + if (i - j + 1 == 0) { + dp[i + j] = 0; + } else { + dp[i + j] = Math.min(dp[i + j], dp[i - j + 1 - 1] + 1); + } + + } else { + break; + } + j++; + } + + } + return dp[n - 1]; +} +``` + +# 总 + +前边的解法还是很常规,从递归到动态规划,利用分治或者回溯,以及 `memoization` 技术,经常用到了。最后一个解法,边找回文串边更新 `dp` ,从而降低了空间复杂度,也是很妙了。 + diff --git a/leetcode-133-Clone-Graph.md b/leetcode-133-Clone-Graph.md index 6ee351178..10f9eaf6d 100644 --- a/leetcode-133-Clone-Graph.md +++ b/leetcode-133-Clone-Graph.md @@ -1,155 +1,155 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/133.jpg) - -复制一个图,图的节点定义如下。 - -```java -class Node { - public int val; - public List neighbors; - - public Node() {} - - public Node(int _val,List _neighbors) { - val = _val; - neighbors = _neighbors; - } -}; -``` - -`neighbors` 是一个装 `Node` 的 `list` ,因为对象的话,`java` 变量都存储的是引用,所以复制的话要新 `new` 一个 `Node` 放到 `neighbors`。 - -# 思路分析 - -这个题其实就是对图进行一个遍历,通过 `BFS` 或者 `DFS`。需要解决的问题就是怎么添加当前节点的 `neighbors`,因为遍历当前节点的时候,它的邻居节点可能还没有生成。 - -# 解法一 BFS - -先来一个简单粗暴的想法。 - -首先对图进行一个 `BFS`,把所有节点 `new` 出来,不处理 `neighbors` ,并且把所有的节点存到 `map` 中。 - -然后再对图做一个 `BFS`,因为此时所有的节点已经创建了,只需要更新所有节点的 `neighbors`。 - -```java -public Node cloneGraph(Node node) { - if (node == null) { - return node; - } - //第一次 BFS - Queue queue = new LinkedList<>(); - Map map = new HashMap<>(); - Set visited = new HashSet<>(); - queue.offer(node); - visited.add(node.val); - while (!queue.isEmpty()) { - Node cur = queue.poll(); - //生成每一个节点 - Node n = new Node(); - n.val = cur.val; - n.neighbors = new ArrayList(); - map.put(n.val, n); - for (Node temp : cur.neighbors) { - if (visited.contains(temp.val)) { - continue; - } - queue.offer(temp); - visited.add(temp.val); - } - } - - //第二次 BFS 更新所有节点的 neightbors - queue = new LinkedList<>(); - queue.offer(node); - visited = new HashSet<>(); - visited.add(node.val); - while (!queue.isEmpty()) { - Node cur = queue.poll(); - for (Node temp : cur.neighbors) { - map.get(cur.val).neighbors.add(map.get(temp.val)); - } - for (Node temp : cur.neighbors) { - if (visited.contains(temp.val)) { - continue; - } - queue.offer(temp); - visited.add(temp.val); - } - } - return map.get(node.val); -} - -``` - -当然再仔细思考一下,其实我们不需要两次 `BFS`。 - -我们要解决的问题是遍历当前节点的时候,邻居节点没有生成,那么我们可以一边遍历一边生成邻居节点,就可以同时更新 `neighbors `了。 - -同样需要一个 `map` 记录已经生成的节点。 - -```java -public Node cloneGraph(Node node) { - if (node == null) { - return node; - } - Queue queue = new LinkedList<>(); - Map map = new HashMap<>(); - queue.offer(node); - //先生成第一个节点 - Node n = new Node(); - n.val = node.val; - n.neighbors = new ArrayList(); - map.put(n.val, n); - while (!queue.isEmpty()) { - Node cur = queue.poll(); - for (Node temp : cur.neighbors) { - //没有生成的节点就生成 - if (!map.containsKey(temp.val)) { - n = new Node(); - n.val = temp.val; - n.neighbors = new ArrayList(); - map.put(n.val, n); - queue.offer(temp); - } - map.get(cur.val).neighbors.add(map.get(temp.val)); - } - } - - return map.get(node.val); -} -``` - -# 解法二 DFS - -`DFS` 的话用递归即可,也用一个 `map` 记录已经生成的节点。 - -```java -public Node cloneGraph(Node node) { - if (node == null) { - return node; - } - Map map = new HashMap<>(); - return cloneGrapthHelper(node, map); -} - -private Node cloneGrapthHelper(Node node, Map map) { - if (map.containsKey(node.val)) { - return map.get(node.val); - } - //生成当前节点 - Node n = new Node(); - n.val = node.val; - n.neighbors = new ArrayList(); - map.put(node.val, n); - //添加它的所有邻居节点 - for (Node temp : node.neighbors) { - n.neighbors.add(cloneGrapthHelper(temp, map)); - } - return n; -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/133.jpg) + +复制一个图,图的节点定义如下。 + +```java +class Node { + public int val; + public List neighbors; + + public Node() {} + + public Node(int _val,List _neighbors) { + val = _val; + neighbors = _neighbors; + } +}; +``` + +`neighbors` 是一个装 `Node` 的 `list` ,因为对象的话,`java` 变量都存储的是引用,所以复制的话要新 `new` 一个 `Node` 放到 `neighbors`。 + +# 思路分析 + +这个题其实就是对图进行一个遍历,通过 `BFS` 或者 `DFS`。需要解决的问题就是怎么添加当前节点的 `neighbors`,因为遍历当前节点的时候,它的邻居节点可能还没有生成。 + +# 解法一 BFS + +先来一个简单粗暴的想法。 + +首先对图进行一个 `BFS`,把所有节点 `new` 出来,不处理 `neighbors` ,并且把所有的节点存到 `map` 中。 + +然后再对图做一个 `BFS`,因为此时所有的节点已经创建了,只需要更新所有节点的 `neighbors`。 + +```java +public Node cloneGraph(Node node) { + if (node == null) { + return node; + } + //第一次 BFS + Queue queue = new LinkedList<>(); + Map map = new HashMap<>(); + Set visited = new HashSet<>(); + queue.offer(node); + visited.add(node.val); + while (!queue.isEmpty()) { + Node cur = queue.poll(); + //生成每一个节点 + Node n = new Node(); + n.val = cur.val; + n.neighbors = new ArrayList(); + map.put(n.val, n); + for (Node temp : cur.neighbors) { + if (visited.contains(temp.val)) { + continue; + } + queue.offer(temp); + visited.add(temp.val); + } + } + + //第二次 BFS 更新所有节点的 neightbors + queue = new LinkedList<>(); + queue.offer(node); + visited = new HashSet<>(); + visited.add(node.val); + while (!queue.isEmpty()) { + Node cur = queue.poll(); + for (Node temp : cur.neighbors) { + map.get(cur.val).neighbors.add(map.get(temp.val)); + } + for (Node temp : cur.neighbors) { + if (visited.contains(temp.val)) { + continue; + } + queue.offer(temp); + visited.add(temp.val); + } + } + return map.get(node.val); +} + +``` + +当然再仔细思考一下,其实我们不需要两次 `BFS`。 + +我们要解决的问题是遍历当前节点的时候,邻居节点没有生成,那么我们可以一边遍历一边生成邻居节点,就可以同时更新 `neighbors `了。 + +同样需要一个 `map` 记录已经生成的节点。 + +```java +public Node cloneGraph(Node node) { + if (node == null) { + return node; + } + Queue queue = new LinkedList<>(); + Map map = new HashMap<>(); + queue.offer(node); + //先生成第一个节点 + Node n = new Node(); + n.val = node.val; + n.neighbors = new ArrayList(); + map.put(n.val, n); + while (!queue.isEmpty()) { + Node cur = queue.poll(); + for (Node temp : cur.neighbors) { + //没有生成的节点就生成 + if (!map.containsKey(temp.val)) { + n = new Node(); + n.val = temp.val; + n.neighbors = new ArrayList(); + map.put(n.val, n); + queue.offer(temp); + } + map.get(cur.val).neighbors.add(map.get(temp.val)); + } + } + + return map.get(node.val); +} +``` + +# 解法二 DFS + +`DFS` 的话用递归即可,也用一个 `map` 记录已经生成的节点。 + +```java +public Node cloneGraph(Node node) { + if (node == null) { + return node; + } + Map map = new HashMap<>(); + return cloneGrapthHelper(node, map); +} + +private Node cloneGrapthHelper(Node node, Map map) { + if (map.containsKey(node.val)) { + return map.get(node.val); + } + //生成当前节点 + Node n = new Node(); + n.val = node.val; + n.neighbors = new ArrayList(); + map.put(node.val, n); + //添加它的所有邻居节点 + for (Node temp : node.neighbors) { + n.neighbors.add(cloneGrapthHelper(temp, map)); + } + return n; +} +``` + +# 总 + 这道题本质上就是对图的遍历,只要想到用 `map` 去存储已经生成的节点,题目基本上就解决了。 \ No newline at end of file diff --git a/leetcode-134-Gas-Station.md b/leetcode-134-Gas-Station.md index 27b0369c5..661f9e08e 100644 --- a/leetcode-134-Gas-Station.md +++ b/leetcode-134-Gas-Station.md @@ -1,195 +1,195 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/134.png) - -把这个题理解成下边的图就可以。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/134_2.jpg) - -每个节点表示添加的油量,每条边表示消耗的油量。题目的意思就是问我们从哪个节点出发,还可以回到该节点。只能顺时针方向走。 - -# 解法一 暴力解法 - -考虑暴力破解,一方面是验证下自己对题目的理解是否正确,另一方面后续的优化也可以从这里入手。 - -考虑从第 `0` 个点出发,能否回到第 `0` 个点。 - -考虑从第 `1` 个点出发,能否回到第 1 个点。 - -考虑从第 `2` 个点出发,能否回到第 `2` 个点。 - -... ... - -考虑从第 `n` 个点出发,能否回到第 `n` 个点。 - -由于是个圆,得到下一个点的时候我们需要取余数。 - -```java -public int canCompleteCircuit(int[] gas, int[] cost) { - int n = gas.length; - //考虑从每一个点出发 - for (int i = 0; i < n; i++) { - int j = i; - int remain = gas[i]; - //当前剩余的油能否到达下一个点 - while (remain - cost[j] >= 0) { - //减去花费的加上新的点的补给 - remain = remain - cost[j] + gas[(j + 1) % n]; - j = (j + 1) % n; - //j 回到了 i - if (j == i) { - return i; - } - } - } - //任何点都不可以 - return -1; -} -``` - -# 解法二 优化尝试一 - -暴力破解慢的原因就是会进行很多重复的计算。比如下边的情况: - -```java -假设当前在考虑 i,先初始化 j = i -* * * * * * - ^ - i - ^ - j - -随后 j 会进行后移 -* * * * * * - ^ ^ - i j - -继续后移 -* * * * * * - ^ ^ - i j - -继续后移 -* * * * * * -^ ^ -j i - -此时 j 又回到了第 0 个位置,我们在之前已经考虑过了这个位置。 -如果之前考虑第 0 个位置的时候,最远到了第 2 个位置。 -那么此时 j 就可以直接跳到第 2 个位置,同时加上当时的剩余汽油,继续考虑 -* * * * * * - ^ ^ - j i -``` - -利用上边的思想我们可以进行一个优化,就是每考虑一个点,就将当前点能够到达的最远距离记录下来,同时到达最远距离时候的剩余汽油也要记下来。 - -```java -public int canCompleteCircuit(int[] gas, int[] cost) { - int n = gas.length; - //记录能到的最远距离 - int[] farIndex = new int[n]; - for (int i = 0; i < farIndex.length; i++) { - farIndex[i] = -1; - } - //记录到达最远距离时候剩余的汽油 - int[] farIndexRemain = new int[n]; - for (int i = 0; i < n; i++) { - int j = i; - int remain = gas[i]; - while (remain - cost[j] >= 0) { - //到达下个点后的剩余 - remain = remain - cost[j]; - j = (j + 1) % n; - //判断之前有没有考虑过这个点 - if (farIndex[j] != -1) { - //加上当时剩余的汽油 - remain = remain + farIndexRemain[j]; - //j 进行跳跃 - j = farIndex[j]; - } else { - //加上当前点的补给 - remain = remain + gas[j]; - } - if (j == i) { - return i; - } - } - //记录当前点最远到达哪里 - farIndex[i] = j; - //记录当前点的剩余 - farIndexRemain[i] = remain; - } - return -1; - -} -``` - -遗憾的是,这个想法针对 `leetcode` 的测试集速度上没有带来很明显的提升。不过记录已经求出来的解进行优化,这个思想还是经常用的,也就是空间换时间。 - -让我们换个思路继续优化。 - -# 解法三 优化尝试二 - -我们考虑一下下边的情况。 - -```java -* * * * * * -^ ^ -i j -``` - -当考虑 `i` 能到达的最远的时候,假设是 `j`。 - -那么 `i + 1` 到 `j` 之间的节点是不是就都不可能绕一圈了? - -假设 `i + 1` 的节点能绕一圈,那么就意味着从 `i + 1` 开始一定能到达 `j + 1`。 - -又因为从 `i` 能到达 `i + 1`,所以从 ` i ` 也能到达 `j + 1`。 - -但事实上,`i` 最远到达 `j` 。产生矛盾,所以 `i + 1` 的节点一定不能绕一圈。同理,其他的也是一样的证明。 - -所以下一次的 `i` 我们不需要从 `i + 1` 开始考虑,直接从 `j + 1` 开始考虑即可。 - -还有一种情况,就是因为到达末尾的时候,会回到 `0`。 - -如果对于下边的情况。 - -```java -* * * * * * - ^ ^ - j i -``` - -如果 `i` 最远能够到达 `j` ,根据上边的结论 `i + 1` 到 `j` 之间的节点都不可能绕一圈了。想象成一个圆,所以 `i` 后边的节点就都不需要考虑了,直接返回 `-1` 即可。 - -```java -public int canCompleteCircuit(int[] gas, int[] cost) { - int n = gas.length; - for (int i = 0; i < n; i++) { - int j = i; - int remain = gas[i]; - while (remain - cost[j] >= 0) { - //减去花费的加上新的点的补给 - remain = remain - cost[j] + gas[(j + 1) % n]; - j = (j + 1) % n; - //j 回到了 i - if (j == i) { - return i; - } - } - //最远距离绕到了之前,所以 i 后边的都不可能绕一圈了 - if (j < i) { - return -1; - } - //i 直接跳到 j,外层 for 循环执行 i++,相当于从 j + 1 开始考虑 - i = j; - - } - return -1; -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/134.png) + +把这个题理解成下边的图就可以。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/134_2.jpg) + +每个节点表示添加的油量,每条边表示消耗的油量。题目的意思就是问我们从哪个节点出发,还可以回到该节点。只能顺时针方向走。 + +# 解法一 暴力解法 + +考虑暴力破解,一方面是验证下自己对题目的理解是否正确,另一方面后续的优化也可以从这里入手。 + +考虑从第 `0` 个点出发,能否回到第 `0` 个点。 + +考虑从第 `1` 个点出发,能否回到第 1 个点。 + +考虑从第 `2` 个点出发,能否回到第 `2` 个点。 + +... ... + +考虑从第 `n` 个点出发,能否回到第 `n` 个点。 + +由于是个圆,得到下一个点的时候我们需要取余数。 + +```java +public int canCompleteCircuit(int[] gas, int[] cost) { + int n = gas.length; + //考虑从每一个点出发 + for (int i = 0; i < n; i++) { + int j = i; + int remain = gas[i]; + //当前剩余的油能否到达下一个点 + while (remain - cost[j] >= 0) { + //减去花费的加上新的点的补给 + remain = remain - cost[j] + gas[(j + 1) % n]; + j = (j + 1) % n; + //j 回到了 i + if (j == i) { + return i; + } + } + } + //任何点都不可以 + return -1; +} +``` + +# 解法二 优化尝试一 + +暴力破解慢的原因就是会进行很多重复的计算。比如下边的情况: + +```java +假设当前在考虑 i,先初始化 j = i +* * * * * * + ^ + i + ^ + j + +随后 j 会进行后移 +* * * * * * + ^ ^ + i j + +继续后移 +* * * * * * + ^ ^ + i j + +继续后移 +* * * * * * +^ ^ +j i + +此时 j 又回到了第 0 个位置,我们在之前已经考虑过了这个位置。 +如果之前考虑第 0 个位置的时候,最远到了第 2 个位置。 +那么此时 j 就可以直接跳到第 2 个位置,同时加上当时的剩余汽油,继续考虑 +* * * * * * + ^ ^ + j i +``` + +利用上边的思想我们可以进行一个优化,就是每考虑一个点,就将当前点能够到达的最远距离记录下来,同时到达最远距离时候的剩余汽油也要记下来。 + +```java +public int canCompleteCircuit(int[] gas, int[] cost) { + int n = gas.length; + //记录能到的最远距离 + int[] farIndex = new int[n]; + for (int i = 0; i < farIndex.length; i++) { + farIndex[i] = -1; + } + //记录到达最远距离时候剩余的汽油 + int[] farIndexRemain = new int[n]; + for (int i = 0; i < n; i++) { + int j = i; + int remain = gas[i]; + while (remain - cost[j] >= 0) { + //到达下个点后的剩余 + remain = remain - cost[j]; + j = (j + 1) % n; + //判断之前有没有考虑过这个点 + if (farIndex[j] != -1) { + //加上当时剩余的汽油 + remain = remain + farIndexRemain[j]; + //j 进行跳跃 + j = farIndex[j]; + } else { + //加上当前点的补给 + remain = remain + gas[j]; + } + if (j == i) { + return i; + } + } + //记录当前点最远到达哪里 + farIndex[i] = j; + //记录当前点的剩余 + farIndexRemain[i] = remain; + } + return -1; + +} +``` + +遗憾的是,这个想法针对 `leetcode` 的测试集速度上没有带来很明显的提升。不过记录已经求出来的解进行优化,这个思想还是经常用的,也就是空间换时间。 + +让我们换个思路继续优化。 + +# 解法三 优化尝试二 + +我们考虑一下下边的情况。 + +```java +* * * * * * +^ ^ +i j +``` + +当考虑 `i` 能到达的最远的时候,假设是 `j`。 + +那么 `i + 1` 到 `j` 之间的节点是不是就都不可能绕一圈了? + +假设 `i + 1` 的节点能绕一圈,那么就意味着从 `i + 1` 开始一定能到达 `j + 1`。 + +又因为从 `i` 能到达 `i + 1`,所以从 ` i ` 也能到达 `j + 1`。 + +但事实上,`i` 最远到达 `j` 。产生矛盾,所以 `i + 1` 的节点一定不能绕一圈。同理,其他的也是一样的证明。 + +所以下一次的 `i` 我们不需要从 `i + 1` 开始考虑,直接从 `j + 1` 开始考虑即可。 + +还有一种情况,就是因为到达末尾的时候,会回到 `0`。 + +如果对于下边的情况。 + +```java +* * * * * * + ^ ^ + j i +``` + +如果 `i` 最远能够到达 `j` ,根据上边的结论 `i + 1` 到 `j` 之间的节点都不可能绕一圈了。想象成一个圆,所以 `i` 后边的节点就都不需要考虑了,直接返回 `-1` 即可。 + +```java +public int canCompleteCircuit(int[] gas, int[] cost) { + int n = gas.length; + for (int i = 0; i < n; i++) { + int j = i; + int remain = gas[i]; + while (remain - cost[j] >= 0) { + //减去花费的加上新的点的补给 + remain = remain - cost[j] + gas[(j + 1) % n]; + j = (j + 1) % n; + //j 回到了 i + if (j == i) { + return i; + } + } + //最远距离绕到了之前,所以 i 后边的都不可能绕一圈了 + if (j < i) { + return -1; + } + //i 直接跳到 j,外层 for 循环执行 i++,相当于从 j + 1 开始考虑 + i = j; + + } + return -1; +} +``` + +# 总 + 写题的时候先写出暴力的解法,然后再考虑优化,有时候是一种不错的选择。 \ No newline at end of file diff --git a/leetcode-135-Candy.md b/leetcode-135-Candy.md index 1d8107945..b1c256779 100644 --- a/leetcode-135-Candy.md +++ b/leetcode-135-Candy.md @@ -1,264 +1,264 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/135.jpg) - -给 N 个小朋友分糖,每个人至少有一颗糖。并且有一个 `rating` 数组,如果小朋友的 `rating`比它旁边小朋友的 `rating` 大(不包括等于),那么他必须要比对应小朋友的糖多。问至少需要分配多少颗糖。 - -用 `-` 表示糖,举几个例子。 - -```java -1 0 2 -- - - -- - -总共就需要 5 颗糖。 - -1 2 2 -- - - - - -总共就需要 4 颗糖。 -``` - -# 解法一 - -根据题目,首先每个小朋友会至少有一个糖。 - -如果当前小朋友的 `rating` 比后一个小朋友的小,那么后一个小朋友的糖肯定是当前小朋友的糖加 `1`。 - -比如 `ration = [ 5, 6, 7]` ,那么三个小朋友的糖就依次是 `1 2 3 `。 - -如果当前小朋友的 `rating` 比后一个小朋友的大,那么理论上当前小朋友的糖要比后一个的小朋友的糖多,但此时后一个小朋友的糖还没有确定,怎么办呢? - -参考 [32题]() 的解法五,利用正着遍历,再倒着遍历的思想。 - -首先我们正着遍历一次,只考虑当前小朋友的 `rating` 比后一个小朋友的小的情况。 - -接着再倒着遍历依次,继续考虑当前小朋友的 `rating` 比后一个小朋友的小的情况。因为之前已经更新过一次糖果了,此时后一个小朋友的糖如果已经比当前小朋友的糖多了,就不需要进行更新了。 - -举个例子 - -```java -初始化每人一个糖 -1 2 3 2 1 4 -- - - - - -. - -只考虑当前小朋友的 rating 比后一个小朋友的小的情况,后一个小朋友的糖是当前小朋友的糖加 1。 -1 < 2 -1 2 3 2 1 4 -- - - - - - - - - -2 < 3 -1 2 3 2 1 4 -- - - - - - - - - - - - -3 > 2 不考虑 - -2 > 1 不考虑 - -1 < 4 -1 2 3 2 1 4 -- - - - - - - - - - - - - -倒过来重新进行 -继续考虑当前小朋友的 rating 比后一个小朋友的小的情况。此时后一个小朋友的糖如果已经比当前小朋友的糖多了,就不需要进行更新。 -4 1 2 3 2 1 -- - - - - - -- - - - - - -4 > 1 不考虑 - -1 < 2 -4 1 2 3 2 1 -- - - - - - -- - - - - - - -2 < 3,3 的糖果已经比 2 的多了,不需要考虑 - -3 > 2,不考虑 - -2 > 1,不考虑 - -所以最终的糖的数量就是上边的 - 的和。 -``` - -代码的话,我们用一个 `candies` 数组保存当前的分配情况。 - -```java -public int candy(int[] ratings) { - int n = ratings.length; - int[] candies = new int[n]; - //每人发一个糖 - for (int i = 0; i < n; i++) { - candies[i] = 1; - } - //正着进行 - for (int i = 0; i < n - 1; i++) { - //当前小朋友的 rating 比后一个小朋友的小,后一个小朋友的糖是当前小朋友的糖加 1。 - if (ratings[i] < ratings[i + 1]) { - candies[i + 1] = candies[i] + 1; - } - } - //倒着进行 - //下标顺序就变成了 i i-1 i-2 i-3 ... 0 - //当前就是第 i 个,后一个就是第 i - 1 个 - for (int i = n - 1; i > 0; i--) { - //当前小朋友的 rating 比后一个小朋友的小 - if (ratings[i] < ratings[i - 1]) { - //后一个小朋友的糖果树没有前一个的多,就更新后一个等于前一个加 1 - if (candies[i - 1] <= candies[i]) { - candies[i - 1] = candies[i] + 1; - } - - } - } - //计算糖果总和 - int sum = 0; - for (int i = 0; i < n; i++) { - sum += candies[i]; - } - return sum; -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(n)。 - -# 解法二 - -参考 [这里]()。 - -解法一中,考虑到 - -> 如果当前小朋友的 `rating` 比后一个小朋友的大,那么理论上当前小朋友的糖要比后一个的小朋友的糖多,但此时后一个小朋友的糖还没有确定,怎么办呢? - -之前采用了倒着遍历一次的方式进行了解决,这里再考虑另外一种解法。 - -考虑下边的情况。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/135_2.jpg) - -对于第 `2` 个 `rating 4`,它比后一个 `rating` 要大,所以要取决于再后边的 `rating`,一直走到 `2`,也就是山底,此时对应的糖果数是 `1`,然后往后走,走回山顶,糖果数一次加 `1`,也就是到 `rating 4` 时,糖果数就是 `3` 了。 - -再一般化,山顶的糖果数就等于从左边的山底或右边的山底依次加 `1` 。 - -所以我们的算法只需要记录山顶,然后再记录下坡的高度,下坡的高度刚好是一个等差序列可以直接用公式求和。而山顶的糖果数,取决于左边山底到山顶和右边山底到山顶的哪个高度大。 - -而产生山底可以有两种情况,一种是 `rating` 产生了增加,如上图。还有一种就是 `rating` 不再降低,而是持平。 - -知道了上边的想法,基本上就可以写代码了,每个人写出来的应该都不一样,在 `discuss` 区也看到了很多不同的写法,下边说一下我的思路。 - -抽象出四种情况,这里的高度不是 `rating` 进行相减,而是从山底的 `rating` 到山顶的 `rating` 经过的次数。 - -1. 左边山底到山顶的高度大,并且右边山底后继续增加。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/135_3.jpg) - -2. 左边山底到山顶的高度大,并且右边山底是平坡。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/135_4.jpg) - -3. 右边山底到山顶的高度大,并且右边山底后继续增加。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/135_2.jpg) - -4. 右边山底到山顶的高度大,并且右边山底是平坡。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/135_5.jpg) - - - -有了这四种情况就可以写代码了。 - -我们用 `total` 变量记录糖果总和, `pre` 变量记录前一个小朋友的糖果数。如果当前的 `rating` 比前一个的 `rating` 大,那么说明在走上坡,可以把前一个小朋友的糖果数加到 `total` 中,并且更新 `pre` 为当前小朋友的糖果数。 - -如果当前的 `rating` 比前一个的 `rating` 小,说明开始走下坡,用 `down` 变量记录连续多少次下降,此时的 `pre` 记录的就是从左边山底到山底的高度。当出现平坡或上坡的时候,将所有的下坡的糖果数利用等差公式计算。此外根据 `pre` 和 `down` 决定山顶的糖果数。 - -根据当前是上坡还是平坡,来更新 `pre`。 - -大框架就是上边的想法了,还有一些边界需要考虑一下,看一下代码。 - -```java -public int candy(int[] ratings) { - int n = ratings.length; - int total = 0; - int down = 0; - int pre = 1; - for (int i = 1; i < n; i++) { - //当前是在上坡或者平坡 - if (ratings[i] >= ratings[i - 1]) { - //之前出现过了下坡 - if (down > 0) { - //山顶的糖果数大于下降的高度,对应情况 1 - //将下降的糖果数利用等差公式计算,单独加上山顶 - if (pre > down) { - total += count(down); - total += pre; - //山顶的糖果数小于下降的高度,对应情况 3, - //将山顶也按照等差公式直接计算进去累加 - } else { - total += count(down + 1); - } - - //当前是上坡,对应情况 1 或者 3 - //更新 pre 等于 2 - if (ratings[i] > ratings[i - 1]) { - pre = 2; - - //当前是平坡,对应情况 2 或者 4 - //更新 pre 等于 1 - } else { - pre = 1; - } - down = 0; - //之前没有出现过下坡 - } else { - //将前一个小朋友的糖果数相加 - total += pre; - //如果是上坡更新当前糖果数是上一个的加 1 - if (ratings[i] > ratings[i - 1]) { - pre = pre + 1; - //如果是平坡,更新当前糖果数为 1 - } else { - pre = 1; - } - - } - } else { - down++; - } - } - //判断是否有下坡 - if (down > 0) { - //和之前的逻辑一样进行相加 - if (pre > down) { - total += count(down); - total += pre; - } else { - total += count(down + 1); - } - //将最后一个小朋友的糖果计算 - } else { - total += pre; - } - return total; -} - -//等差数列求和 -private int count(int n) { - return (1 + n) * n / 2; -} - -``` - -这个算法相对于解法一的好处就是将空间复杂度从 `O(n)` 优化到了 `O(1)`。 - -# 总 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/135.jpg) + +给 N 个小朋友分糖,每个人至少有一颗糖。并且有一个 `rating` 数组,如果小朋友的 `rating`比它旁边小朋友的 `rating` 大(不包括等于),那么他必须要比对应小朋友的糖多。问至少需要分配多少颗糖。 + +用 `-` 表示糖,举几个例子。 + +```java +1 0 2 +- - - +- - +总共就需要 5 颗糖。 + +1 2 2 +- - - + - +总共就需要 4 颗糖。 +``` + +# 解法一 + +根据题目,首先每个小朋友会至少有一个糖。 + +如果当前小朋友的 `rating` 比后一个小朋友的小,那么后一个小朋友的糖肯定是当前小朋友的糖加 `1`。 + +比如 `ration = [ 5, 6, 7]` ,那么三个小朋友的糖就依次是 `1 2 3 `。 + +如果当前小朋友的 `rating` 比后一个小朋友的大,那么理论上当前小朋友的糖要比后一个的小朋友的糖多,但此时后一个小朋友的糖还没有确定,怎么办呢? + +参考 [32题]() 的解法五,利用正着遍历,再倒着遍历的思想。 + +首先我们正着遍历一次,只考虑当前小朋友的 `rating` 比后一个小朋友的小的情况。 + +接着再倒着遍历依次,继续考虑当前小朋友的 `rating` 比后一个小朋友的小的情况。因为之前已经更新过一次糖果了,此时后一个小朋友的糖如果已经比当前小朋友的糖多了,就不需要进行更新了。 + +举个例子 + +```java +初始化每人一个糖 +1 2 3 2 1 4 +- - - - - -. + +只考虑当前小朋友的 rating 比后一个小朋友的小的情况,后一个小朋友的糖是当前小朋友的糖加 1。 +1 < 2 +1 2 3 2 1 4 +- - - - - - + - + +2 < 3 +1 2 3 2 1 4 +- - - - - - + - - + - + +3 > 2 不考虑 + +2 > 1 不考虑 + +1 < 4 +1 2 3 2 1 4 +- - - - - - + - - - + - + +倒过来重新进行 +继续考虑当前小朋友的 rating 比后一个小朋友的小的情况。此时后一个小朋友的糖如果已经比当前小朋友的糖多了,就不需要进行更新。 +4 1 2 3 2 1 +- - - - - - +- - - + - + +4 > 1 不考虑 + +1 < 2 +4 1 2 3 2 1 +- - - - - - +- - - - + - + +2 < 3,3 的糖果已经比 2 的多了,不需要考虑 + +3 > 2,不考虑 + +2 > 1,不考虑 + +所以最终的糖的数量就是上边的 - 的和。 +``` + +代码的话,我们用一个 `candies` 数组保存当前的分配情况。 + +```java +public int candy(int[] ratings) { + int n = ratings.length; + int[] candies = new int[n]; + //每人发一个糖 + for (int i = 0; i < n; i++) { + candies[i] = 1; + } + //正着进行 + for (int i = 0; i < n - 1; i++) { + //当前小朋友的 rating 比后一个小朋友的小,后一个小朋友的糖是当前小朋友的糖加 1。 + if (ratings[i] < ratings[i + 1]) { + candies[i + 1] = candies[i] + 1; + } + } + //倒着进行 + //下标顺序就变成了 i i-1 i-2 i-3 ... 0 + //当前就是第 i 个,后一个就是第 i - 1 个 + for (int i = n - 1; i > 0; i--) { + //当前小朋友的 rating 比后一个小朋友的小 + if (ratings[i] < ratings[i - 1]) { + //后一个小朋友的糖果树没有前一个的多,就更新后一个等于前一个加 1 + if (candies[i - 1] <= candies[i]) { + candies[i - 1] = candies[i] + 1; + } + + } + } + //计算糖果总和 + int sum = 0; + for (int i = 0; i < n; i++) { + sum += candies[i]; + } + return sum; +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(n)。 + +# 解法二 + +参考 [这里]()。 + +解法一中,考虑到 + +> 如果当前小朋友的 `rating` 比后一个小朋友的大,那么理论上当前小朋友的糖要比后一个的小朋友的糖多,但此时后一个小朋友的糖还没有确定,怎么办呢? + +之前采用了倒着遍历一次的方式进行了解决,这里再考虑另外一种解法。 + +考虑下边的情况。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/135_2.jpg) + +对于第 `2` 个 `rating 4`,它比后一个 `rating` 要大,所以要取决于再后边的 `rating`,一直走到 `2`,也就是山底,此时对应的糖果数是 `1`,然后往后走,走回山顶,糖果数一次加 `1`,也就是到 `rating 4` 时,糖果数就是 `3` 了。 + +再一般化,山顶的糖果数就等于从左边的山底或右边的山底依次加 `1` 。 + +所以我们的算法只需要记录山顶,然后再记录下坡的高度,下坡的高度刚好是一个等差序列可以直接用公式求和。而山顶的糖果数,取决于左边山底到山顶和右边山底到山顶的哪个高度大。 + +而产生山底可以有两种情况,一种是 `rating` 产生了增加,如上图。还有一种就是 `rating` 不再降低,而是持平。 + +知道了上边的想法,基本上就可以写代码了,每个人写出来的应该都不一样,在 `discuss` 区也看到了很多不同的写法,下边说一下我的思路。 + +抽象出四种情况,这里的高度不是 `rating` 进行相减,而是从山底的 `rating` 到山顶的 `rating` 经过的次数。 + +1. 左边山底到山顶的高度大,并且右边山底后继续增加。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/135_3.jpg) + +2. 左边山底到山顶的高度大,并且右边山底是平坡。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/135_4.jpg) + +3. 右边山底到山顶的高度大,并且右边山底后继续增加。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/135_2.jpg) + +4. 右边山底到山顶的高度大,并且右边山底是平坡。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/135_5.jpg) + + + +有了这四种情况就可以写代码了。 + +我们用 `total` 变量记录糖果总和, `pre` 变量记录前一个小朋友的糖果数。如果当前的 `rating` 比前一个的 `rating` 大,那么说明在走上坡,可以把前一个小朋友的糖果数加到 `total` 中,并且更新 `pre` 为当前小朋友的糖果数。 + +如果当前的 `rating` 比前一个的 `rating` 小,说明开始走下坡,用 `down` 变量记录连续多少次下降,此时的 `pre` 记录的就是从左边山底到山底的高度。当出现平坡或上坡的时候,将所有的下坡的糖果数利用等差公式计算。此外根据 `pre` 和 `down` 决定山顶的糖果数。 + +根据当前是上坡还是平坡,来更新 `pre`。 + +大框架就是上边的想法了,还有一些边界需要考虑一下,看一下代码。 + +```java +public int candy(int[] ratings) { + int n = ratings.length; + int total = 0; + int down = 0; + int pre = 1; + for (int i = 1; i < n; i++) { + //当前是在上坡或者平坡 + if (ratings[i] >= ratings[i - 1]) { + //之前出现过了下坡 + if (down > 0) { + //山顶的糖果数大于下降的高度,对应情况 1 + //将下降的糖果数利用等差公式计算,单独加上山顶 + if (pre > down) { + total += count(down); + total += pre; + //山顶的糖果数小于下降的高度,对应情况 3, + //将山顶也按照等差公式直接计算进去累加 + } else { + total += count(down + 1); + } + + //当前是上坡,对应情况 1 或者 3 + //更新 pre 等于 2 + if (ratings[i] > ratings[i - 1]) { + pre = 2; + + //当前是平坡,对应情况 2 或者 4 + //更新 pre 等于 1 + } else { + pre = 1; + } + down = 0; + //之前没有出现过下坡 + } else { + //将前一个小朋友的糖果数相加 + total += pre; + //如果是上坡更新当前糖果数是上一个的加 1 + if (ratings[i] > ratings[i - 1]) { + pre = pre + 1; + //如果是平坡,更新当前糖果数为 1 + } else { + pre = 1; + } + + } + } else { + down++; + } + } + //判断是否有下坡 + if (down > 0) { + //和之前的逻辑一样进行相加 + if (pre > down) { + total += count(down); + total += pre; + } else { + total += count(down + 1); + } + //将最后一个小朋友的糖果计算 + } else { + total += pre; + } + return total; +} + +//等差数列求和 +private int count(int n) { + return (1 + n) * n / 2; +} + +``` + +这个算法相对于解法一的好处就是将空间复杂度从 `O(n)` 优化到了 `O(1)`。 + +# 总 + 解法一虽然空间复杂度大一些,但是很好理解,正着遍历,倒着遍历的思想,每次遇到都印象深刻。解法二主要是对问题进行深入考虑,虽然麻烦些,但空间复杂度确实优化了。 \ No newline at end of file diff --git a/leetcode-136-Single-Number.md b/leetcode-136-Single-Number.md index 7381a4379..d4a7d5192 100644 --- a/leetcode-136-Single-Number.md +++ b/leetcode-136-Single-Number.md @@ -1,120 +1,120 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/136.jpg) - -所有数字都是成对出现的,只有一个数字是落单的,找出这个落单的数字。 - -# 解法一 - -题目要求线性复杂度内实现,并且要求没有额外空间。首先我们考虑假如没有空间复杂度的限制。 - -这其实就只需要统计每个数字出现的次数,很容易想到去用 `HashMap` 。 - -遍历一次数组,第一次遇到就将对应的 `key` 置为 `1`。第二次遇到就拿到 `key` 对应的 `value` 然后进行加 `1` 再存入。最后只需要寻找 `value` 是 `1` 的 `key` 就可以了。 - -利用 `HashMap` 统计字符个数已经用过很多次了,比如 [30 题](https://leetcode.wang/leetCode-30-Substring-with-Concatenation-of-All-Words.html)、[49 题](https://leetcode.wang/leetCode-49-Group-Anagrams.html) 等等,最重要的好处就是可以在 `O(1)` 下取得之前的元素,从而使得题目的时间复杂度达到 `O(n)`。 - -当然,注意到这个题目每个数字出现的次数要么是 `1` 次,要么是 `2` 次,所以我们也可以用一个 `HashSet` ,在第一次遇到就加到 `Set` 中,第二次遇到就把当前元素从 `Set` 中移除。这样遍历一遍后,`Set` 中剩下的元素就是我们要找的那个落单的元素了。 - -```java -public int singleNumber(int[] nums) { - HashSet set = new HashSet<>(); - for (int i = 0; i < nums.length; i++) { - if (!set.contains(nums[i])) { - set.add(nums[i]); - } else { - set.remove(nums[i]); - } - } - return set.iterator().next(); -} -``` - -当然,上边的解法空间复杂度是 `O(n)`,怎么用 `O(1)` 的空间复杂度解决上边的问题呢? - -想了很久,双指针,利用已确定元素的空间,等等的思想都考虑了,始终想不到解法,然后看了官方的 [Solution](https://leetcode.com/problems/single-number/solution/) ,下边分享一下。 - -# 解法二 数学推导 - -假设我们的数字是 `a b a b c c d` - -怎么求出 `d` 呢? - -只需要把出现过的数字加起来乘以 `2` ,然后减去之前的数字和就可以了。 - -什么意思呢? - -上边的例子出现过的数字就是 `a b c d` ,加起来乘以二就是 `2 * ( a + b + c + d)`,之前的数字和就是 `a + b + a + b + c + c + d` 。 - -`2 * ( a + b + c + d) - (a + b + a + b + c + c + d)`,然后结果是不是就是 `d` 了。。。。。。 - -看完这个解法我只能说 `tql`。。。 - -找出现过什么数字,我们只需要一个 `Set` 去重就可以了。 - -```java -public int singleNumber(int[] nums) { - HashSet set = new HashSet<>(); - int sum = 0;//之前的数字和 - for (int i = 0; i < nums.length; i++) { - set.add(nums[i]); - sum += nums[i]; - } - int sumMul = 0;//出现过的数字和 - for (int n : set) { - sumMul += n; - } - sumMul = sumMul * 2; - return sumMul - sum; -} -``` - -但上边的解法还是需要 `O(n)` 的空间复杂度,下边的解法让我彻底跪了。 - -# 解法三 异或 - -还记得位操作中的异或吗?计算规则如下。 - -> 0 ⊕ 0 = 0 -> -> 1 ⊕ 1 = 0 -> -> 0 ⊕ 1 = 1 -> -> 1 ⊕ 0 = 1 - -总结起来就是相同为零,不同为一。 - -根据上边的规则,可以推导出一些性质 - -* 0 ⊕ a = a -* a ⊕ a = 0 - -此外异或满足交换律以及结合律。 - -所以对于之前的例子 `a b a b c c d` ,如果我们把给定的数字相互异或会发生什么呢? - -```java - a ⊕ b ⊕ a ⊕ b ⊕ c ⊕ c ⊕ d -= ( a ⊕ a ) ⊕ ( b ⊕ b ) ⊕ ( c ⊕ c ) ⊕ d -= 0 ⊕ 0 ⊕ 0 ⊕ d -= d -``` - -是的,答案就这样出来了,我妈妈问我为什么要跪着。。。 - -`java` 里的异或是 `^` 操作符,初始值可以给一个 `0`。 - -```java -public int singleNumber(int[] nums) { - int ans = 0; - for (int i = 0; i < nums.length; i++) { - ans ^= nums[i]; - } - return ans; -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/136.jpg) + +所有数字都是成对出现的,只有一个数字是落单的,找出这个落单的数字。 + +# 解法一 + +题目要求线性复杂度内实现,并且要求没有额外空间。首先我们考虑假如没有空间复杂度的限制。 + +这其实就只需要统计每个数字出现的次数,很容易想到去用 `HashMap` 。 + +遍历一次数组,第一次遇到就将对应的 `key` 置为 `1`。第二次遇到就拿到 `key` 对应的 `value` 然后进行加 `1` 再存入。最后只需要寻找 `value` 是 `1` 的 `key` 就可以了。 + +利用 `HashMap` 统计字符个数已经用过很多次了,比如 [30 题](https://leetcode.wang/leetCode-30-Substring-with-Concatenation-of-All-Words.html)、[49 题](https://leetcode.wang/leetCode-49-Group-Anagrams.html) 等等,最重要的好处就是可以在 `O(1)` 下取得之前的元素,从而使得题目的时间复杂度达到 `O(n)`。 + +当然,注意到这个题目每个数字出现的次数要么是 `1` 次,要么是 `2` 次,所以我们也可以用一个 `HashSet` ,在第一次遇到就加到 `Set` 中,第二次遇到就把当前元素从 `Set` 中移除。这样遍历一遍后,`Set` 中剩下的元素就是我们要找的那个落单的元素了。 + +```java +public int singleNumber(int[] nums) { + HashSet set = new HashSet<>(); + for (int i = 0; i < nums.length; i++) { + if (!set.contains(nums[i])) { + set.add(nums[i]); + } else { + set.remove(nums[i]); + } + } + return set.iterator().next(); +} +``` + +当然,上边的解法空间复杂度是 `O(n)`,怎么用 `O(1)` 的空间复杂度解决上边的问题呢? + +想了很久,双指针,利用已确定元素的空间,等等的思想都考虑了,始终想不到解法,然后看了官方的 [Solution](https://leetcode.com/problems/single-number/solution/) ,下边分享一下。 + +# 解法二 数学推导 + +假设我们的数字是 `a b a b c c d` + +怎么求出 `d` 呢? + +只需要把出现过的数字加起来乘以 `2` ,然后减去之前的数字和就可以了。 + +什么意思呢? + +上边的例子出现过的数字就是 `a b c d` ,加起来乘以二就是 `2 * ( a + b + c + d)`,之前的数字和就是 `a + b + a + b + c + c + d` 。 + +`2 * ( a + b + c + d) - (a + b + a + b + c + c + d)`,然后结果是不是就是 `d` 了。。。。。。 + +看完这个解法我只能说 `tql`。。。 + +找出现过什么数字,我们只需要一个 `Set` 去重就可以了。 + +```java +public int singleNumber(int[] nums) { + HashSet set = new HashSet<>(); + int sum = 0;//之前的数字和 + for (int i = 0; i < nums.length; i++) { + set.add(nums[i]); + sum += nums[i]; + } + int sumMul = 0;//出现过的数字和 + for (int n : set) { + sumMul += n; + } + sumMul = sumMul * 2; + return sumMul - sum; +} +``` + +但上边的解法还是需要 `O(n)` 的空间复杂度,下边的解法让我彻底跪了。 + +# 解法三 异或 + +还记得位操作中的异或吗?计算规则如下。 + +> 0 ⊕ 0 = 0 +> +> 1 ⊕ 1 = 0 +> +> 0 ⊕ 1 = 1 +> +> 1 ⊕ 0 = 1 + +总结起来就是相同为零,不同为一。 + +根据上边的规则,可以推导出一些性质 + +* 0 ⊕ a = a +* a ⊕ a = 0 + +此外异或满足交换律以及结合律。 + +所以对于之前的例子 `a b a b c c d` ,如果我们把给定的数字相互异或会发生什么呢? + +```java + a ⊕ b ⊕ a ⊕ b ⊕ c ⊕ c ⊕ d += ( a ⊕ a ) ⊕ ( b ⊕ b ) ⊕ ( c ⊕ c ) ⊕ d += 0 ⊕ 0 ⊕ 0 ⊕ d += d +``` + +是的,答案就这样出来了,我妈妈问我为什么要跪着。。。 + +`java` 里的异或是 `^` 操作符,初始值可以给一个 `0`。 + +```java +public int singleNumber(int[] nums) { + int ans = 0; + for (int i = 0; i < nums.length; i++) { + ans ^= nums[i]; + } + return ans; +} +``` + +# 总 + 解法一利用 `HashMap` 计数算是一个很常用的思想了。解法二的数学推导理论上还能想到,解法三的异或操作真的是太神仙操作了,自愧不如。 \ No newline at end of file diff --git a/leetcode-137-Single-NumberII.md b/leetcode-137-Single-NumberII.md index e7ba07e66..e38d9ffcb 100644 --- a/leetcode-137-Single-NumberII.md +++ b/leetcode-137-Single-NumberII.md @@ -1,390 +1,390 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/137.jpg) - -[136 题](https://leetcode.wang/leetcode-136-Single-Number.html) 的升级版,这个题的话意思是,每个数字都出现了 3 次,只有一个数字出现了 1 次,找出这个数字。同样要求时间复杂度为 O(n),空间复杂度为 O(1)。 - -大家可以先看一下 [136 题](https://leetcode.wang/leetcode-136-Single-Number.html) ,完全按 [136 题](https://leetcode.wang/leetcode-136-Single-Number.html) 的每个解法去考虑一下。 - -# 解法一 - -先不考虑空间复杂度,用最常规的方法。 - -可以用一个 `HashMap` 对每个数字进行计数,然后返回数量为 `1` 的数字就可以了。 - -```java -public int singleNumber(int[] nums) { - HashMap map = new HashMap<>(); - for (int i = 0; i < nums.length; i++) { - if (map.containsKey(nums[i])) { - map.put(nums[i], map.get(nums[i]) + 1); - } else { - map.put(nums[i], 1); - } - } - for (Integer key : map.keySet()) { - if (map.get(key) == 1) { - return key; - } - - } - return -1; // 这句不会执行 -} -``` - -时间复杂度:O(n)。 - -空间复杂度:O(n)。 - -# 解法二 数学推导 - -回想一下 [136 题](https://leetcode.wang/leetcode-136-Single-Number.html) 中,每个数字都出现两次,只有一个数字出现 `1` 次是怎么做的。 - -> 假设我们的数字是 `a b a b c c d` -> -> 怎么求出 `d` 呢? -> -> 只需要把出现过的数字加起来乘以 `2` ,然后减去之前的数字和就可以了。 -> -> 什么意思呢? -> -> 上边的例子出现过的数字就是 `a b c d` ,加起来乘以二就是 `2 * ( a + b + c + d)`,之前的数字和就是 `a + b + a + b + c + c + d` 。 -> -> `2 * ( a + b + c + d) - (a + b + a + b + c + c + d)`,然后结果是不是就是 `d` 了。。。。。。 -> -> 看完这个解法我只能说 `tql`。。。 -> -> 找出现过什么数字,我们只需要一个 `Set` 去重就可以了。 - -这里的话每个数字出现了 `3` 次,所以我们可以加起来乘以 `3` 然后减去之前所有的数字和。这样得到的差就是只出现过一次的那个数字的 `2` 倍。 - -```java -public int singleNumber(int[] nums) { - HashSet set = new HashSet<>(); - int sum = 0; - for (int i = 0; i < nums.length; i++) { - set.add(nums[i]); - sum += nums[i]; - } - int sumMul = 0; - for (int n : set) { - sumMul += n; - } - sumMul = sumMul * 3; - return (sumMul - sum) / 2; -} -``` - -然而并没有通过 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/137_2.jpg) - -原因就是 `int` 是 `32` 位整数,计算机中是以补码表示的,详细的参考 [趣谈补码](https://mp.weixin.qq.com/s/uvcQHJi6AXhPDJL-6JWUkw) 。 -问题的根本就出现在,如果 `2a = c` ,那么对于 `a` 的取值有两种情况。在没有溢出的情况下,`a = c/2` 是没有问题的。但如果 `a` 是很大的数,加起来溢出了,此时 `a = c >>> 1`。 - -举个具体的例子, - -如果给定的数组是 `[1 1 1 Integer.MaxValue]`。如果按上边的解法最后得到的就是 - -`(1 + Ingeger.MaxValue) * 3 - (1 + 1 + 1 + Integer.MaxValue) = 2 * Integer.MaxValue` - -由于产生了溢出 - -`2 * Integer.MaxValue = -2`,最后我们返回的结果就是 `-2 / 2 = -1`。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/137_3.jpg) - -所以这个思路行不通了,因为无法知道是不是会溢出。 - -# 解法三 位操作 - -136 题通过异或解决了问题,这道题明显不能用异或了,参考 [这里](https://leetcode.com/problems/single-number-ii/discuss/43297/Java-O(n)-easy-to-understand-solution-easily-extended-to-any-times-of-occurance) 的一个解法。 - -我们把数字放眼到二进制形式 - -```java -假如例子是 1 2 6 1 1 2 2 3 3 3, 3 个 1, 3 个 2, 3 个 3,1 个 6 -1 0 0 1 -2 0 1 0 -6 1 1 0 -1 0 0 1 -1 0 0 1 -2 0 1 0 -2 0 1 0 -3 0 1 1 -3 0 1 1 -3 0 1 1 -看最右边的一列 1001100111 有 6 个 1 -再往前看一列 0110011111 有 7 个 1 -再往前看一列 0010000 有 1 个 1 -我们只需要把是 3 的倍数的对应列写 0,不是 3 的倍数的对应列写 1 -也就是 1 1 0,也就是 6。 -``` - -原因的话,其实很容易想明白。如果所有数字都出现了 `3` 次,那么每一列的 `1` 的个数就一定是 `3` 的倍数。之所以有的列不是 `3` 的倍数,就是因为只出现了 `1` 次的数贡献出了 `1`。所以所有不是 `3` 的倍数的列写 `1`,其他列写 `0` ,就找到了这个出现 `1` 次的数。 - -```java -public int singleNumber(int[] nums) { - int ans = 0; - //考虑每一位 - for (int i = 0; i < 32; i++) { - int count = 0; - //考虑每一个数 - for (int j = 0; j < nums.length; j++) { - //当前位是否是 1 - if ((nums[j] >>> i & 1) == 1) { - count++; - } - } - //1 的个数是否是 3 的倍数 - if (count % 3 != 0) { - ans = ans | 1 << i; - } - } - return ans; -} - -``` - -时间复杂度:O(n)。 - -空间复杂度:O(1)。 - -# 解法四 通用方法 - -参考 [这里](https://leetcode.com/problems/single-number-ii/discuss/43295/Detailed-explanation-and-generalization-of-the-bitwise-operation-method-for-single-numbers)。 - -解法三中,我们将数字转为二进制,统计了每一位的 `1` 的个数。我们使用了一个 `32位` 的 `int` 来统计。事实上,我们只需要看它是不是 `3` 的倍数,所以我们只需要两个 `bit` 位就够了。初始化为 `00`,遇到第一个 `1` 变为 `01`,遇到第二个 `1` 变为 `10`,遇到第三个 `1` 变回 `00` 。接下来就需要考虑怎么做到。 - -本来想按自己理解的思路写一遍,但 [这里](https://leetcode.com/problems/single-number-ii/discuss/43295/Detailed-explanation-and-generalization-of-the-bitwise-operation-method-for-single-numbers) 写的很好了,主要还是翻译下吧。 - -## 将问题一般化 - -给一个数组,每个元素都出现 `k ( k > 1)` 次,除了一个数字只出现 `p` 次`(p >= 1, p % k !=0)`,找到出现 `p` 次的那个数。 - -## 考虑其中的一个 bit - -为了计数 `k` 次,我们必须要 `m` 个比特,其中 $$2^m >=k$$ ,也就是 `m >= logk`。 - -假设我们 `m` 个比特依次是 $$x_mx_{m-1}...x_2x_1$$ 。 - -开始全部初始化为 `0`。`00...00`。 - -然后扫描所有数字的当前 `bit` 位,用 `i` 表示当前的 `bit`。 - -也就是解法三的例子中的某一列。 - -```java -假如例子是 1 2 6 1 1 2 2 3 3 3, 3 个 1, 3 个 2, 3 个 3,1 个 6 -1 0 0 1 -2 0 1 0 -6 1 1 0 -1 0 0 1 -1 0 0 1 -2 0 1 0 -2 0 1 0 -3 0 1 1 -3 0 1 1 -3 0 1 1 -``` - -初始 状态 `00...00`。 - -第一次遇到 `1` , `m` 个比特依次是 `00...01`。 - -第二次遇到 `1` , `m` 个比特依次是 `00...10`。 - -第三次遇到 `1` , `m` 个比特依次是 `00...11`。 - -第四次遇到 `1` , `m` 个比特依次是 `00..100`。 - -`x1` 的变化规律就是遇到 `1` 变成 `1` ,再遇到 `1` 变回 `0`。遇到 `0` 的话就不变。 - -所以 `x1 = x1 ^ i`,可以用异或来求出 `x1` 。 - -那么 `x2...xm` 怎么办呢? - -`x2` 的话,当遇到 `1` 的时候,如果之前 `x1` 是 `0`,`x2` 就不变。如果之前 `x1` 是 `1`,对应于上边的第二次遇到 `1` 和第四次遇到 `1`。 `x2` 从 `0` 变成 `1` 和 从 `1` 变成 `0`。 - -所以 `x2` 的变化规律就是遇到 `1` 同时 `x1` 是 `1` 就变成 `1`,再遇到 `1` 同时 `x1` 是 `1` 就变回 `0`。遇到 `0` 的话就不变。和 `x1` 的变化规律很像,所以同样可以使用异或。 - -`x2 = x2 ^ (i & x1)`,多判断了 `x1` 是不是 `1`。 - -`x3,x4 ... xm` 就是同理了,`xm = xm ^ (xm-1 & ... & x1 & i)` 。 - -再说直接点,上边其实就是模拟了每次加 `1` 的时候,各个比特位的变化。所以高位 `xm` 只有当低位全部为 `1` 的时候才会得到进位 `1` 。 - -`00 -> 01 -> 10 -> 11 -> 00` - -上边有个问题,假设我们的 `k = 3`,那么我们应该在 `10` 之后就变成 `00`,而不是到 `11`。 - -所以我们需要一个 `mask` ,当没有到达 `k` 的时候和 `mask`进行与操作是它本身,当到达 `k` 的时候和 `mask` 相与就回到 `00...000`。 - -根据上边的要求构造 `mask`,假设 `k` 写成二进制以后是 `km...k2k1`。 - -`mask = ~(y1 & y2 & ... & ym)`, - -如果`kj = 1`,那么`yj = xj` - -如果 `kj = 0`,`yj = ~xj` 。 - -举两个例子。 - -`k = 3: 写成二进制,k1 = 1, k2 = 1, mask = ~(x1 & x2)`; - -`k = 5: 写成二进制,k1 = 1, k2 = 0, k3 = 1, mask = ~(x1 & ~x2 & x3)`; - -很容易想明白,当 `x1x2...xm` 达到 `k1k2...km` 的时候因为我们要把 `x1x2...xm` 归零。我们只需要用 `0` 和每一位进行与操作就回到了 `0`。 - -所以我们只需要把等于 `0` 的比特位取反,然后再和其他所有位相与就得到 `1` ,然后再取反就是 `0` 了。 - -如果 `x1x2...xm` 没有达到 `k1k2...km` ,那么求出来的结果一定是 `1`,这样和原来的 `bit` 位进行与操作的话就保持了原来的数。 - -总之,最后我们的代码就是下边的框架。 - -```java -for (int i : nums) { - xm ^= (xm-1 & ... & x1 & i); - xm-1 ^= (xm-2 & ... & x1 & i); - ..... - x1 ^= i; - - mask = ~(y1 & y2 & ... & ym) where yj = xj if kj = 1, and yj = ~xj if kj = 0 (j = 1 to m). - - xm &= mask; - ...... - x1 &= mask; -} -``` - -# 考虑全部 bit - -```java -假如例子是 1 2 6 1 1 2 2 3 3 3, 3 个 1, 3 个 2, 3 个 3,1 个 6 -1 0 0 1 -2 0 1 0 -6 1 1 0 -1 0 0 1 -1 0 0 1 -2 0 1 0 -2 0 1 0 -3 0 1 1 -3 0 1 1 -3 0 1 1 -``` - -之前是完成了一个 `bit` 位,也就是每一列的操作。因为我们给的数是 `int` 类型,所以有 `32` 位。所以我们需要对每一位都进行计数。有了上边的分析,我们不需要再向解法三那样依次考虑每一位,我们可以同时对 `32` 位进行计数。 - -对于 `k` 等于 `3` ,也就是这道题。我们可以用两个 `int`,`x1` 和 `x2`。`x1` 表示对于 `32` 位每一位计数的低位,`x2` 表示对于 `32` 位每一位计数的高位。通过之前的公式,我们利用位操作就可以同时完成计数了。 - -```java -int x1 = 0, x2 = 0, mask = 0; - -for (int i : nums) { - x2 ^= x1 & i; - x1 ^= i; - mask = ~(x1 & x2); - x2 &= mask; - x1 &= mask; -} -``` - -## 返回什么 - -最后一个问题,我们需要返回什么? - -解法三中,我们看 `1` 出现的个数是不是 `3` 的倍数,不是 `3` 的倍数就将对应位置 `1`。 - -这里的话一样的道理,因为所有的数字都出现了 `k` 次,只有一个数字出现了 `p` 次。 - -因为 `xm...x2x1` 组合起来就是对于每一列 `1` 的计数。举个例子 - -```java -假如例子是 1 2 6 1 1 2 2 3 3 3, 3 个 1, 3 个 2, 3 个 3,1 个 6 -1 0 0 1 -2 0 1 0 -6 1 1 0 -1 0 0 1 -1 0 0 1 -2 0 1 0 -2 0 1 0 -3 0 1 1 -3 0 1 1 -3 0 1 1 - -看最右边的一列 1001100111 有 6 个 1, 也就是 110 -再往前看一列 0110011111 有 7 个 1, 也就是 111 -再往前看一列 0010000 有 1 个 1, 也就是 001 -再对应到 x1, x2, x3 就是 -x1 1 1 0 -x2 0 1 1 -x3 0 1 1 -``` - -如果 `p = 1`,那么如果出现一次的数字的某一位是 `1` ,一定会使得 `x1` ,也就是计数的最低位置的对应位为 `1`,所以我们把 `x1` 返回即可。对于上边的例子,就是 `110` ,所以返回 `6`。 - -如果 `p = 2`,二进制就是 `10`,那么如果出现 `2`次的数字的某一位是 `1` ,一定会使得 `x2` 的对应位变为 `1`,所以我们把 `x2` 返回即可。 - -如果 `p = 3`,二进制就是 `11`,那么如果出现 `3`次的数字的某一位是 `1` ,一定会使得 `x1` 和`x2`的对应位都变为`1`,所以我们把 `x1` 或者 `x2` 返回即可。 - -所以这道题的代码就出来了 - -```java -public int singleNumber(int[] nums) { - int x1 = 0, x2 = 0, mask = 0; - for (int i : nums) { - x2 ^= x1 & i; - x1 ^= i; - mask = ~(x1 & x2); - x2 &= mask; - x1 &= mask; - } - return x1; -} -``` - -至于为什么先对 `x2` 异或再对 `x1` 异或,就是因为 `x2` 的变化依赖于 `x1` 之前的状态。颠倒过来明显就不对了。 - -再扩展一下题目,对于 `k = 5, p = 3` 怎么做,也就是每个数字出现了`5` 次,只有一个数字出现了 `3` 次。 - -首先根据 `k = 5`,所以我们至少需要 `3` 个比特位。因为 `2` 个比特位最多计数四次。 - -然后根据 `k` 的二进制形式是 `101`,所以 `mask = ~(x1 & ~x2 & x3)`。 - -根据 `p` 的二进制是 `011`,所以我们最后可以把 `x1` 返回。 - -```java -public int singleNumber(int[] nums) { - int x1 = 0, x2 = 0, x3 = 0, mask = 0; - for (int i : nums) { - x3 ^= x2 & x1 & i; - x2 ^= x1 & i; - x1 ^= i; - mask = ~(x1 & ~x2 & x3); - x3 &= mask; - x2 &= mask; - x1 &= mask; - } - return x1; -} -``` - -而 [136 题](https://leetcode.wang/leetcode-136-Single-Number.html) 中,`k = 2, p = 1` ,其实也是这个类型。只不过因为 `k = 2`,而我们用一个比特位计数的时候,等于 `2` 的时候就自动归零了,所以不需要 `mask`,相对来说就更简单了。 - -```java -public int singleNumber(int[] nums) { - int x1 = 0; - - for (int i : nums) { - x1 ^= i; - } - - return x1; -} -``` - -这个解法真是太强了,完全回到二进制的操作,五体投地了,推荐再看一下英文的 [原文](https://leetcode.com/problems/single-number-ii/discuss/43295/Detailed-explanation-and-generalization-of-the-bitwise-operation-method-for-single-numbers) 分析,太强了。 - -# 总 - -解法一利用 `HashMap` 计数很常规,解法二通过数学公式虽然没有通过,但溢出的问题也就我们经常需要考虑的。解法三把数字放眼到二进制,统计 `1` 的个数已经很强了。解法四直接利用 `bit` 位来计数,真的是大开眼界了,神仙操作。 +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/137.jpg) + +[136 题](https://leetcode.wang/leetcode-136-Single-Number.html) 的升级版,这个题的话意思是,每个数字都出现了 3 次,只有一个数字出现了 1 次,找出这个数字。同样要求时间复杂度为 O(n),空间复杂度为 O(1)。 + +大家可以先看一下 [136 题](https://leetcode.wang/leetcode-136-Single-Number.html) ,完全按 [136 题](https://leetcode.wang/leetcode-136-Single-Number.html) 的每个解法去考虑一下。 + +# 解法一 + +先不考虑空间复杂度,用最常规的方法。 + +可以用一个 `HashMap` 对每个数字进行计数,然后返回数量为 `1` 的数字就可以了。 + +```java +public int singleNumber(int[] nums) { + HashMap map = new HashMap<>(); + for (int i = 0; i < nums.length; i++) { + if (map.containsKey(nums[i])) { + map.put(nums[i], map.get(nums[i]) + 1); + } else { + map.put(nums[i], 1); + } + } + for (Integer key : map.keySet()) { + if (map.get(key) == 1) { + return key; + } + + } + return -1; // 这句不会执行 +} +``` + +时间复杂度:O(n)。 + +空间复杂度:O(n)。 + +# 解法二 数学推导 + +回想一下 [136 题](https://leetcode.wang/leetcode-136-Single-Number.html) 中,每个数字都出现两次,只有一个数字出现 `1` 次是怎么做的。 + +> 假设我们的数字是 `a b a b c c d` +> +> 怎么求出 `d` 呢? +> +> 只需要把出现过的数字加起来乘以 `2` ,然后减去之前的数字和就可以了。 +> +> 什么意思呢? +> +> 上边的例子出现过的数字就是 `a b c d` ,加起来乘以二就是 `2 * ( a + b + c + d)`,之前的数字和就是 `a + b + a + b + c + c + d` 。 +> +> `2 * ( a + b + c + d) - (a + b + a + b + c + c + d)`,然后结果是不是就是 `d` 了。。。。。。 +> +> 看完这个解法我只能说 `tql`。。。 +> +> 找出现过什么数字,我们只需要一个 `Set` 去重就可以了。 + +这里的话每个数字出现了 `3` 次,所以我们可以加起来乘以 `3` 然后减去之前所有的数字和。这样得到的差就是只出现过一次的那个数字的 `2` 倍。 + +```java +public int singleNumber(int[] nums) { + HashSet set = new HashSet<>(); + int sum = 0; + for (int i = 0; i < nums.length; i++) { + set.add(nums[i]); + sum += nums[i]; + } + int sumMul = 0; + for (int n : set) { + sumMul += n; + } + sumMul = sumMul * 3; + return (sumMul - sum) / 2; +} +``` + +然而并没有通过 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/137_2.jpg) + +原因就是 `int` 是 `32` 位整数,计算机中是以补码表示的,详细的参考 [趣谈补码](https://mp.weixin.qq.com/s/uvcQHJi6AXhPDJL-6JWUkw) 。 +问题的根本就出现在,如果 `2a = c` ,那么对于 `a` 的取值有两种情况。在没有溢出的情况下,`a = c/2` 是没有问题的。但如果 `a` 是很大的数,加起来溢出了,此时 `a = c >>> 1`。 + +举个具体的例子, + +如果给定的数组是 `[1 1 1 Integer.MaxValue]`。如果按上边的解法最后得到的就是 + +`(1 + Ingeger.MaxValue) * 3 - (1 + 1 + 1 + Integer.MaxValue) = 2 * Integer.MaxValue` + +由于产生了溢出 + +`2 * Integer.MaxValue = -2`,最后我们返回的结果就是 `-2 / 2 = -1`。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/137_3.jpg) + +所以这个思路行不通了,因为无法知道是不是会溢出。 + +# 解法三 位操作 + +136 题通过异或解决了问题,这道题明显不能用异或了,参考 [这里](https://leetcode.com/problems/single-number-ii/discuss/43297/Java-O(n)-easy-to-understand-solution-easily-extended-to-any-times-of-occurance) 的一个解法。 + +我们把数字放眼到二进制形式 + +```java +假如例子是 1 2 6 1 1 2 2 3 3 3, 3 个 1, 3 个 2, 3 个 3,1 个 6 +1 0 0 1 +2 0 1 0 +6 1 1 0 +1 0 0 1 +1 0 0 1 +2 0 1 0 +2 0 1 0 +3 0 1 1 +3 0 1 1 +3 0 1 1 +看最右边的一列 1001100111 有 6 个 1 +再往前看一列 0110011111 有 7 个 1 +再往前看一列 0010000 有 1 个 1 +我们只需要把是 3 的倍数的对应列写 0,不是 3 的倍数的对应列写 1 +也就是 1 1 0,也就是 6。 +``` + +原因的话,其实很容易想明白。如果所有数字都出现了 `3` 次,那么每一列的 `1` 的个数就一定是 `3` 的倍数。之所以有的列不是 `3` 的倍数,就是因为只出现了 `1` 次的数贡献出了 `1`。所以所有不是 `3` 的倍数的列写 `1`,其他列写 `0` ,就找到了这个出现 `1` 次的数。 + +```java +public int singleNumber(int[] nums) { + int ans = 0; + //考虑每一位 + for (int i = 0; i < 32; i++) { + int count = 0; + //考虑每一个数 + for (int j = 0; j < nums.length; j++) { + //当前位是否是 1 + if ((nums[j] >>> i & 1) == 1) { + count++; + } + } + //1 的个数是否是 3 的倍数 + if (count % 3 != 0) { + ans = ans | 1 << i; + } + } + return ans; +} + +``` + +时间复杂度:O(n)。 + +空间复杂度:O(1)。 + +# 解法四 通用方法 + +参考 [这里](https://leetcode.com/problems/single-number-ii/discuss/43295/Detailed-explanation-and-generalization-of-the-bitwise-operation-method-for-single-numbers)。 + +解法三中,我们将数字转为二进制,统计了每一位的 `1` 的个数。我们使用了一个 `32位` 的 `int` 来统计。事实上,我们只需要看它是不是 `3` 的倍数,所以我们只需要两个 `bit` 位就够了。初始化为 `00`,遇到第一个 `1` 变为 `01`,遇到第二个 `1` 变为 `10`,遇到第三个 `1` 变回 `00` 。接下来就需要考虑怎么做到。 + +本来想按自己理解的思路写一遍,但 [这里](https://leetcode.com/problems/single-number-ii/discuss/43295/Detailed-explanation-and-generalization-of-the-bitwise-operation-method-for-single-numbers) 写的很好了,主要还是翻译下吧。 + +## 将问题一般化 + +给一个数组,每个元素都出现 `k ( k > 1)` 次,除了一个数字只出现 `p` 次`(p >= 1, p % k !=0)`,找到出现 `p` 次的那个数。 + +## 考虑其中的一个 bit + +为了计数 `k` 次,我们必须要 `m` 个比特,其中 $$2^m >=k$$ ,也就是 `m >= logk`。 + +假设我们 `m` 个比特依次是 $$x_mx_{m-1}...x_2x_1$$ 。 + +开始全部初始化为 `0`。`00...00`。 + +然后扫描所有数字的当前 `bit` 位,用 `i` 表示当前的 `bit`。 + +也就是解法三的例子中的某一列。 + +```java +假如例子是 1 2 6 1 1 2 2 3 3 3, 3 个 1, 3 个 2, 3 个 3,1 个 6 +1 0 0 1 +2 0 1 0 +6 1 1 0 +1 0 0 1 +1 0 0 1 +2 0 1 0 +2 0 1 0 +3 0 1 1 +3 0 1 1 +3 0 1 1 +``` + +初始 状态 `00...00`。 + +第一次遇到 `1` , `m` 个比特依次是 `00...01`。 + +第二次遇到 `1` , `m` 个比特依次是 `00...10`。 + +第三次遇到 `1` , `m` 个比特依次是 `00...11`。 + +第四次遇到 `1` , `m` 个比特依次是 `00..100`。 + +`x1` 的变化规律就是遇到 `1` 变成 `1` ,再遇到 `1` 变回 `0`。遇到 `0` 的话就不变。 + +所以 `x1 = x1 ^ i`,可以用异或来求出 `x1` 。 + +那么 `x2...xm` 怎么办呢? + +`x2` 的话,当遇到 `1` 的时候,如果之前 `x1` 是 `0`,`x2` 就不变。如果之前 `x1` 是 `1`,对应于上边的第二次遇到 `1` 和第四次遇到 `1`。 `x2` 从 `0` 变成 `1` 和 从 `1` 变成 `0`。 + +所以 `x2` 的变化规律就是遇到 `1` 同时 `x1` 是 `1` 就变成 `1`,再遇到 `1` 同时 `x1` 是 `1` 就变回 `0`。遇到 `0` 的话就不变。和 `x1` 的变化规律很像,所以同样可以使用异或。 + +`x2 = x2 ^ (i & x1)`,多判断了 `x1` 是不是 `1`。 + +`x3,x4 ... xm` 就是同理了,`xm = xm ^ (xm-1 & ... & x1 & i)` 。 + +再说直接点,上边其实就是模拟了每次加 `1` 的时候,各个比特位的变化。所以高位 `xm` 只有当低位全部为 `1` 的时候才会得到进位 `1` 。 + +`00 -> 01 -> 10 -> 11 -> 00` + +上边有个问题,假设我们的 `k = 3`,那么我们应该在 `10` 之后就变成 `00`,而不是到 `11`。 + +所以我们需要一个 `mask` ,当没有到达 `k` 的时候和 `mask`进行与操作是它本身,当到达 `k` 的时候和 `mask` 相与就回到 `00...000`。 + +根据上边的要求构造 `mask`,假设 `k` 写成二进制以后是 `km...k2k1`。 + +`mask = ~(y1 & y2 & ... & ym)`, + +如果`kj = 1`,那么`yj = xj` + +如果 `kj = 0`,`yj = ~xj` 。 + +举两个例子。 + +`k = 3: 写成二进制,k1 = 1, k2 = 1, mask = ~(x1 & x2)`; + +`k = 5: 写成二进制,k1 = 1, k2 = 0, k3 = 1, mask = ~(x1 & ~x2 & x3)`; + +很容易想明白,当 `x1x2...xm` 达到 `k1k2...km` 的时候因为我们要把 `x1x2...xm` 归零。我们只需要用 `0` 和每一位进行与操作就回到了 `0`。 + +所以我们只需要把等于 `0` 的比特位取反,然后再和其他所有位相与就得到 `1` ,然后再取反就是 `0` 了。 + +如果 `x1x2...xm` 没有达到 `k1k2...km` ,那么求出来的结果一定是 `1`,这样和原来的 `bit` 位进行与操作的话就保持了原来的数。 + +总之,最后我们的代码就是下边的框架。 + +```java +for (int i : nums) { + xm ^= (xm-1 & ... & x1 & i); + xm-1 ^= (xm-2 & ... & x1 & i); + ..... + x1 ^= i; + + mask = ~(y1 & y2 & ... & ym) where yj = xj if kj = 1, and yj = ~xj if kj = 0 (j = 1 to m). + + xm &= mask; + ...... + x1 &= mask; +} +``` + +# 考虑全部 bit + +```java +假如例子是 1 2 6 1 1 2 2 3 3 3, 3 个 1, 3 个 2, 3 个 3,1 个 6 +1 0 0 1 +2 0 1 0 +6 1 1 0 +1 0 0 1 +1 0 0 1 +2 0 1 0 +2 0 1 0 +3 0 1 1 +3 0 1 1 +3 0 1 1 +``` + +之前是完成了一个 `bit` 位,也就是每一列的操作。因为我们给的数是 `int` 类型,所以有 `32` 位。所以我们需要对每一位都进行计数。有了上边的分析,我们不需要再向解法三那样依次考虑每一位,我们可以同时对 `32` 位进行计数。 + +对于 `k` 等于 `3` ,也就是这道题。我们可以用两个 `int`,`x1` 和 `x2`。`x1` 表示对于 `32` 位每一位计数的低位,`x2` 表示对于 `32` 位每一位计数的高位。通过之前的公式,我们利用位操作就可以同时完成计数了。 + +```java +int x1 = 0, x2 = 0, mask = 0; + +for (int i : nums) { + x2 ^= x1 & i; + x1 ^= i; + mask = ~(x1 & x2); + x2 &= mask; + x1 &= mask; +} +``` + +## 返回什么 + +最后一个问题,我们需要返回什么? + +解法三中,我们看 `1` 出现的个数是不是 `3` 的倍数,不是 `3` 的倍数就将对应位置 `1`。 + +这里的话一样的道理,因为所有的数字都出现了 `k` 次,只有一个数字出现了 `p` 次。 + +因为 `xm...x2x1` 组合起来就是对于每一列 `1` 的计数。举个例子 + +```java +假如例子是 1 2 6 1 1 2 2 3 3 3, 3 个 1, 3 个 2, 3 个 3,1 个 6 +1 0 0 1 +2 0 1 0 +6 1 1 0 +1 0 0 1 +1 0 0 1 +2 0 1 0 +2 0 1 0 +3 0 1 1 +3 0 1 1 +3 0 1 1 + +看最右边的一列 1001100111 有 6 个 1, 也就是 110 +再往前看一列 0110011111 有 7 个 1, 也就是 111 +再往前看一列 0010000 有 1 个 1, 也就是 001 +再对应到 x1, x2, x3 就是 +x1 1 1 0 +x2 0 1 1 +x3 0 1 1 +``` + +如果 `p = 1`,那么如果出现一次的数字的某一位是 `1` ,一定会使得 `x1` ,也就是计数的最低位置的对应位为 `1`,所以我们把 `x1` 返回即可。对于上边的例子,就是 `110` ,所以返回 `6`。 + +如果 `p = 2`,二进制就是 `10`,那么如果出现 `2`次的数字的某一位是 `1` ,一定会使得 `x2` 的对应位变为 `1`,所以我们把 `x2` 返回即可。 + +如果 `p = 3`,二进制就是 `11`,那么如果出现 `3`次的数字的某一位是 `1` ,一定会使得 `x1` 和`x2`的对应位都变为`1`,所以我们把 `x1` 或者 `x2` 返回即可。 + +所以这道题的代码就出来了 + +```java +public int singleNumber(int[] nums) { + int x1 = 0, x2 = 0, mask = 0; + for (int i : nums) { + x2 ^= x1 & i; + x1 ^= i; + mask = ~(x1 & x2); + x2 &= mask; + x1 &= mask; + } + return x1; +} +``` + +至于为什么先对 `x2` 异或再对 `x1` 异或,就是因为 `x2` 的变化依赖于 `x1` 之前的状态。颠倒过来明显就不对了。 + +再扩展一下题目,对于 `k = 5, p = 3` 怎么做,也就是每个数字出现了`5` 次,只有一个数字出现了 `3` 次。 + +首先根据 `k = 5`,所以我们至少需要 `3` 个比特位。因为 `2` 个比特位最多计数四次。 + +然后根据 `k` 的二进制形式是 `101`,所以 `mask = ~(x1 & ~x2 & x3)`。 + +根据 `p` 的二进制是 `011`,所以我们最后可以把 `x1` 返回。 + +```java +public int singleNumber(int[] nums) { + int x1 = 0, x2 = 0, x3 = 0, mask = 0; + for (int i : nums) { + x3 ^= x2 & x1 & i; + x2 ^= x1 & i; + x1 ^= i; + mask = ~(x1 & ~x2 & x3); + x3 &= mask; + x2 &= mask; + x1 &= mask; + } + return x1; +} +``` + +而 [136 题](https://leetcode.wang/leetcode-136-Single-Number.html) 中,`k = 2, p = 1` ,其实也是这个类型。只不过因为 `k = 2`,而我们用一个比特位计数的时候,等于 `2` 的时候就自动归零了,所以不需要 `mask`,相对来说就更简单了。 + +```java +public int singleNumber(int[] nums) { + int x1 = 0; + + for (int i : nums) { + x1 ^= i; + } + + return x1; +} +``` + +这个解法真是太强了,完全回到二进制的操作,五体投地了,推荐再看一下英文的 [原文](https://leetcode.com/problems/single-number-ii/discuss/43295/Detailed-explanation-and-generalization-of-the-bitwise-operation-method-for-single-numbers) 分析,太强了。 + +# 总 + +解法一利用 `HashMap` 计数很常规,解法二通过数学公式虽然没有通过,但溢出的问题也就我们经常需要考虑的。解法三把数字放眼到二进制,统计 `1` 的个数已经很强了。解法四直接利用 `bit` 位来计数,真的是大开眼界了,神仙操作。 diff --git a/leetcode-138-Copy-List-with-Random-Pointer.md b/leetcode-138-Copy-List-with-Random-Pointer.md index be993cb31..e0acee176 100644 --- a/leetcode-138-Copy-List-with-Random-Pointer.md +++ b/leetcode-138-Copy-List-with-Random-Pointer.md @@ -1,225 +1,225 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/138.jpg) - -给一个链表,返回复制后的链表。链表节点相对于普通的多了一个 `random` 指针,会随机指向链表内的任意节点或者指向 `null`。 - -# 思路分析 - -这道题其实和 [133 题](https://leetcode.wang/leetcode-133-Clone-Graph.html) 复制一个图很类似,这里的话就是要解决的问题就是,当更新当前节点的 `random` 指针的时候,如果 `random` 指向的是很后边的节点,但此时后边的节点还没有生成,那么我们该如何处理。 - -和 [133 题](https://leetcode.wang/leetcode-133-Clone-Graph.html) 一样,我们可以利用 `HashMap` 将节点提前生成并且保存起来,第二次遍历到他的时候直接从 `HashMap` 里边拿即可。 - -这里的话就有两种思路,一种需要遍历两边链表,一种只需要遍历一遍。 - -> 2020.3.3 更新,leetcode 增加了样例,之前没有重复的数字所以 `key` 存的 `val` ,现在有了重复数字,将 `key` 修改为 `Node`。此外 `Node` 的无参的构造函数也被去掉了,也需要修改。 - -# 解法一 - -首先利用 `HashMap` 来一个不用思考的代码。 - -遍历第一遍链表,我们不考虑链表之间的相互关系,仅仅生成所有节点,然后把它存到 `HashMap` 中,`val` 作为 `key`,`Node` 作为 `value`。 - -遍历第二遍链表,将之前生成的节点取出来,更新它们的 `next` 和 `random` 指针。 - -```java -public Node copyRandomList(Node head) { - if (head == null) { - return null; - } - HashMap map = new HashMap<>(); - Node h = head; - while (h != null) { - Node t = new Node(h.val); - map.put(h, t); - h = h.next; - } - h = head; - while (h != null) { - if (h.next != null) { - map.get(h).next = map.get(h.next); - } - if (h.random != null) { - map.get(h).random = map.get(h.random); - } - h = h.next; - } - return map.get(head); -} -``` - -# 解法二 - -解法一虽然简单易懂,但还是有可以优化的地方的。我们可以只遍历一次链表。 - -核心思想就是延迟更新它的 `next`。 - -```java -1 -> 2 -> 3 - -用 cur 指向已经生成的节点的末尾 -1 -> 2 - ^ - c - -然后将 3 构造完成 - -最后将 2 的 next 指向 3 -1 -> 2 -> 3 - ^ - c - -期间已经生成的节点存到 HashMap 中,第二次遇到的时候直接从 HashMap 中拿 -``` - -看下代码理解一下含义吧 - -```java -public Node copyRandomList(Node head) { - if (head == null) { - return null; - } - HashMap map = new HashMap<>(); - Node h = head; - Node cur = new Node(-1); //空结点,dummy 节点,为了方便头结点计算 - while (h != null) { - //判断当前节点是否已经产生过 - if (!map.containsKey(h)) { - Node t = new Node(h.val); - map.put(h, t); - } - //得到当前节点去更新它的 random 指针 - Node next = map.get(h); - if (h.random != null) { - //判断当前节点是否已经产生过 - if (!map.containsKey(h.random)) { - next.random = new Node(h.random.val); - map.put(h.random, next.random); - } else { - next.random = map.get(h.random); - } - - } - //将当前生成的节点接到 cur 的后边 - cur.next = next; - cur = cur.next; - h = h.next; - } - return map.get(head); -} -``` - -# 解法三 - -上边的两种解法都用到了 `HashMap` ,所以额外需要 `O(n)` 的空间复杂度。现在考虑不需要额外空间的方法。 - -主要参考了[这里](https://leetcode.com/problems/copy-list-with-random-pointer/discuss/43491/A-solution-with-constant-space-complexity-O(1)-and-linear-time-complexity-O(N))。主要解决的问题就是我们生成节点以后,当更新它的 `random` 的时候,怎么找到之前生成的节点,前两种解法用了 `HashMap` 全部存起来,这里的话可以利用原来的链表的指针域。 - - 主要需要三步。 - -1. 生成所有的节点,并且分别插入到原有节点的后边 -2. 更新插入节点的 `random` -3. 将新旧节点分离开来 - -一图胜千言,大家看一下下边的图吧。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/138_2.jpg) - -代码对应如下。 - -```java -public Node copyRandomList(Node head) { - if (head == null) { - return null; - } - Node l1 = head; - Node l2 = null; - //生成所有的节点,并且分别插入到原有节点的后边 - while (l1 != null) { - l2 = new Node(l1.val); - l2.next = l1.next; - l1.next = l2; - l1 = l1.next.next; - } - //更新插入节点的 random - l1 = head; - while (l1 != null) { - if (l1.random != null) { - l1.next.random = l1.random.next; - } - l1 = l1.next.next; - } - - l1 = head; - Node l2_head = l1.next; - //将新旧节点分离开来 - while (l1 != null) { - l2 = l1.next; - l1.next = l2.next; - if (l2.next != null) { - l2.next = l2.next.next; - } - l1 = l1.next; - } - return l2_head; -} -``` - -# 解法四 - -不利用额外的空间复杂度还有一种思路,参考 [这里](https://leetcode.com/problems/copy-list-with-random-pointer/discuss/43497/2-clean-C%2B%2B-algorithms-without-using-extra-arrayhash-table.-Algorithms-are-explained-step-by-step.)。 - -解法三利用原链表的 `next` 域把新生成的节点保存了起来。类似的,我们还可以利用原链表的 `random` 域把新生成的节点保存起来。 - -主要还是三个步骤。 - -1. 生成所有的节点,将它们保存到原链表的 `random` 域,同时利用新生成的节点的 `next` 域保存原链表的 `random`。 -2. 更新新生成节点的 `random` 指针。 -3. 恢复原链表的 `random` 指针,同时更新新生成节点的 `next` 指针。 - -一图胜千言。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/138_3.jpg) - -相应的代码如下。 - -```java -public Node copyRandomList(Node head) { - if (head == null) { - return null; - } - Node l1 = head; - Node l2 = null; - //生成所有的节点,讲它们保存到原链表的 random 域, - //同时利用新生成的节点的 next 域保存原链表的 random。 - while (l1 != null) { - l2 = new Node(l1.val); - l2.next = l1.random; - l1.random = l2; - l1 = l1.next; - } - l1 = head; - //更新新生成节点的 random 指针。 - while (l1 != null) { - l2 = l1.random; - l2.random = l2.next != null ? l2.next.random : null; - l1 = l1.next; - } - - l1 = head; - Node l2_head = l1.random; - //恢复原链表的 random 指针,同时更新新生成节点的 next 指针。 - while (l1 != null) { - l2 = l1.random; - l1.random = l2.next; - l2.next = l1.next != null ? l1.next.random : null; - l1 = l1.next; - } - return l2_head; -} -``` - -# 总 - -解法一、解法二是比较直接的想法,直接利用 `HashMap` 存储之前的节点。解法三、解法四利用原有链表的指针,通过指来指去完成了赋值。链表操作的核心思想就是,在改变某一个节点的指针域的时候,一定要把该节点的指针指向的节点用另一个指针保存起来,以免造成丢失。 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/138.jpg) + +给一个链表,返回复制后的链表。链表节点相对于普通的多了一个 `random` 指针,会随机指向链表内的任意节点或者指向 `null`。 + +# 思路分析 + +这道题其实和 [133 题](https://leetcode.wang/leetcode-133-Clone-Graph.html) 复制一个图很类似,这里的话就是要解决的问题就是,当更新当前节点的 `random` 指针的时候,如果 `random` 指向的是很后边的节点,但此时后边的节点还没有生成,那么我们该如何处理。 + +和 [133 题](https://leetcode.wang/leetcode-133-Clone-Graph.html) 一样,我们可以利用 `HashMap` 将节点提前生成并且保存起来,第二次遍历到他的时候直接从 `HashMap` 里边拿即可。 + +这里的话就有两种思路,一种需要遍历两边链表,一种只需要遍历一遍。 + +> 2020.3.3 更新,leetcode 增加了样例,之前没有重复的数字所以 `key` 存的 `val` ,现在有了重复数字,将 `key` 修改为 `Node`。此外 `Node` 的无参的构造函数也被去掉了,也需要修改。 + +# 解法一 + +首先利用 `HashMap` 来一个不用思考的代码。 + +遍历第一遍链表,我们不考虑链表之间的相互关系,仅仅生成所有节点,然后把它存到 `HashMap` 中,`val` 作为 `key`,`Node` 作为 `value`。 + +遍历第二遍链表,将之前生成的节点取出来,更新它们的 `next` 和 `random` 指针。 + +```java +public Node copyRandomList(Node head) { + if (head == null) { + return null; + } + HashMap map = new HashMap<>(); + Node h = head; + while (h != null) { + Node t = new Node(h.val); + map.put(h, t); + h = h.next; + } + h = head; + while (h != null) { + if (h.next != null) { + map.get(h).next = map.get(h.next); + } + if (h.random != null) { + map.get(h).random = map.get(h.random); + } + h = h.next; + } + return map.get(head); +} +``` + +# 解法二 + +解法一虽然简单易懂,但还是有可以优化的地方的。我们可以只遍历一次链表。 + +核心思想就是延迟更新它的 `next`。 + +```java +1 -> 2 -> 3 + +用 cur 指向已经生成的节点的末尾 +1 -> 2 + ^ + c + +然后将 3 构造完成 + +最后将 2 的 next 指向 3 +1 -> 2 -> 3 + ^ + c + +期间已经生成的节点存到 HashMap 中,第二次遇到的时候直接从 HashMap 中拿 +``` + +看下代码理解一下含义吧 + +```java +public Node copyRandomList(Node head) { + if (head == null) { + return null; + } + HashMap map = new HashMap<>(); + Node h = head; + Node cur = new Node(-1); //空结点,dummy 节点,为了方便头结点计算 + while (h != null) { + //判断当前节点是否已经产生过 + if (!map.containsKey(h)) { + Node t = new Node(h.val); + map.put(h, t); + } + //得到当前节点去更新它的 random 指针 + Node next = map.get(h); + if (h.random != null) { + //判断当前节点是否已经产生过 + if (!map.containsKey(h.random)) { + next.random = new Node(h.random.val); + map.put(h.random, next.random); + } else { + next.random = map.get(h.random); + } + + } + //将当前生成的节点接到 cur 的后边 + cur.next = next; + cur = cur.next; + h = h.next; + } + return map.get(head); +} +``` + +# 解法三 + +上边的两种解法都用到了 `HashMap` ,所以额外需要 `O(n)` 的空间复杂度。现在考虑不需要额外空间的方法。 + +主要参考了[这里](https://leetcode.com/problems/copy-list-with-random-pointer/discuss/43491/A-solution-with-constant-space-complexity-O(1)-and-linear-time-complexity-O(N))。主要解决的问题就是我们生成节点以后,当更新它的 `random` 的时候,怎么找到之前生成的节点,前两种解法用了 `HashMap` 全部存起来,这里的话可以利用原来的链表的指针域。 + + 主要需要三步。 + +1. 生成所有的节点,并且分别插入到原有节点的后边 +2. 更新插入节点的 `random` +3. 将新旧节点分离开来 + +一图胜千言,大家看一下下边的图吧。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/138_2.jpg) + +代码对应如下。 + +```java +public Node copyRandomList(Node head) { + if (head == null) { + return null; + } + Node l1 = head; + Node l2 = null; + //生成所有的节点,并且分别插入到原有节点的后边 + while (l1 != null) { + l2 = new Node(l1.val); + l2.next = l1.next; + l1.next = l2; + l1 = l1.next.next; + } + //更新插入节点的 random + l1 = head; + while (l1 != null) { + if (l1.random != null) { + l1.next.random = l1.random.next; + } + l1 = l1.next.next; + } + + l1 = head; + Node l2_head = l1.next; + //将新旧节点分离开来 + while (l1 != null) { + l2 = l1.next; + l1.next = l2.next; + if (l2.next != null) { + l2.next = l2.next.next; + } + l1 = l1.next; + } + return l2_head; +} +``` + +# 解法四 + +不利用额外的空间复杂度还有一种思路,参考 [这里](https://leetcode.com/problems/copy-list-with-random-pointer/discuss/43497/2-clean-C%2B%2B-algorithms-without-using-extra-arrayhash-table.-Algorithms-are-explained-step-by-step.)。 + +解法三利用原链表的 `next` 域把新生成的节点保存了起来。类似的,我们还可以利用原链表的 `random` 域把新生成的节点保存起来。 + +主要还是三个步骤。 + +1. 生成所有的节点,将它们保存到原链表的 `random` 域,同时利用新生成的节点的 `next` 域保存原链表的 `random`。 +2. 更新新生成节点的 `random` 指针。 +3. 恢复原链表的 `random` 指针,同时更新新生成节点的 `next` 指针。 + +一图胜千言。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/138_3.jpg) + +相应的代码如下。 + +```java +public Node copyRandomList(Node head) { + if (head == null) { + return null; + } + Node l1 = head; + Node l2 = null; + //生成所有的节点,讲它们保存到原链表的 random 域, + //同时利用新生成的节点的 next 域保存原链表的 random。 + while (l1 != null) { + l2 = new Node(l1.val); + l2.next = l1.random; + l1.random = l2; + l1 = l1.next; + } + l1 = head; + //更新新生成节点的 random 指针。 + while (l1 != null) { + l2 = l1.random; + l2.random = l2.next != null ? l2.next.random : null; + l1 = l1.next; + } + + l1 = head; + Node l2_head = l1.random; + //恢复原链表的 random 指针,同时更新新生成节点的 next 指针。 + while (l1 != null) { + l2 = l1.random; + l1.random = l2.next; + l2.next = l1.next != null ? l1.next.random : null; + l1 = l1.next; + } + return l2_head; +} +``` + +# 总 + +解法一、解法二是比较直接的想法,直接利用 `HashMap` 存储之前的节点。解法三、解法四利用原有链表的指针,通过指来指去完成了赋值。链表操作的核心思想就是,在改变某一个节点的指针域的时候,一定要把该节点的指针指向的节点用另一个指针保存起来,以免造成丢失。 + diff --git a/leetcode-139-Word-Break.md b/leetcode-139-Word-Break.md index 0cffb2006..3730df246 100644 --- a/leetcode-139-Word-Break.md +++ b/leetcode-139-Word-Break.md @@ -1,306 +1,306 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/139.jpg) - -给一个字符串,和一些单词,问字符串能不能由这些单词构成。每个单词可以用多次,也可以不用。 - -# 解法一 回溯 - -来一个简单粗暴的方法,利用回溯法,用 `wordDict` 去生成所有可能的字符串。期间如果出现了目标字符串 `s`,就返回 `true`。 - -```java -public boolean wordBreak(String s, List wordDict) { - return wordBreakHelper(s,wordDict,""); -} -//temp 是当前生成的字符串 -private boolean wordBreakHelper(String s, List wordDict, String temp) { - //如果此时生成的字符串长度够了,就判断和目标字符日是否相等 - if(temp.length() == s.length()){ - if(temp.equals(s)){ - return true; - }else{ - return false; - } - } - //长度超了,就返回 false - if(temp.length() > s.length()){ - return false; - } - //考虑每个单词 - for(int i = 0;i < wordDict.size(); i++){ - if(wordBreakHelper(s,wordDict,temp + wordDict.get(i))){ - return true; - } - } - return false; -} -``` - -意料之中,超时了 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/139_2.jpg) - -让我们考虑优化的方法。 - -在递归出口的地方优化一下。 - -之前是在长度相等的时候,开始判断字符串是否相等。 - -很明显,字符串长度相等之前我们其实就可以判断当前是不是符合了。 - -例如 `temp = "abc"`,如果 `s = "dddefg"`,虽然此时 `temp` 和 `s` 的长度不相等。但因为前缀已经不同,所以后边无论是什么都不可以了。此时就可以返回 `false` 了。 - -所以递归出口可以从头判断每个字符是否相等,不相等就直接返回 `false`。 - -```java -for (int i = 0; i < temp.length(); i++) { - if (s.charAt(i) != temp.charAt(i)) { - return false; - } -} -``` - -然后代码就是下边的样子。 - -```java -public boolean wordBreak(String s, List wordDict) { - return wordBreakHelper(s, wordDict, ""); -} - -private boolean wordBreakHelper(String s, List wordDict, String temp) { - if (temp.length() > s.length()) { - return false; - } - //判断此时对应的字符是否全部相等 - for (int i = 0; i < temp.length(); i++) { - if (s.charAt(i) != temp.charAt(i)) { - return false; - } - } - if (s.length() == temp.length()) { - return true; - } - for (int i = 0; i < wordDict.size(); i++) { - if (wordBreakHelper(s, wordDict, temp + wordDict.get(i))) { - return true; - } - } - return false; -} -``` - -遗憾的是,依旧是超时 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/139_3.jpg) - -发现上边的例子答案很明显是 `false`,因为 `s` 中的 `b` 字母在 `wordDict` 中并没有出现。 - -所以我们可以先遍历一遍 `s` 和 `wordDict` ,从而确定 `s` 中的字符是否在 `wordDict` 中存在,如果不存在可以提前返回 `false` 。 - -所以代码可以继续优化。 - -```java -public boolean wordBreak(String s, List wordDict) { - HashSet set = new HashSet<>(); - //将 wordDict 的每个字母放到 set 中 - for (int i = 0; i < wordDict.size(); i++) { - String t = wordDict.get(i); - for (int j = 0; j < t.length(); j++) { - set.add(t.charAt(j)); - } - } - //判断 s 的每个字母在 set 中是否存在 - for (int i = 0; i < s.length(); i++) { - if (!set.contains(s.charAt(i))) { - return false; - } - } - return wordBreakHelper(s, wordDict, ""); -} - -private boolean wordBreakHelper(String s, List wordDict, String temp) { - if (temp.length() > s.length()) { - return false; - } - for (int i = 0; i < temp.length(); i++) { - if (s.charAt(i) != temp.charAt(i)) { - return false; - } - } - if (s.length() == temp.length()) { - return true; - } - for (int i = 0; i < wordDict.size(); i++) { - if (wordBreakHelper(s, wordDict, temp + wordDict.get(i))) { - return true; - } - } - return false; -} -``` - -令人悲伤的是 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/139_4.jpg) - -还有 `5` 个 `test` 没有通过。还有什么可以优化的地方呢? - -是时候拿出绝招了,在前边的题已经用过很多很多次,`memoization` 技术。思想就是把回溯中已经考虑过的解存起来,第二次回溯过来的时候可以直接使用。 - -这里的话,我们可以用一个 `HashMap`,`key` 的话就存 `temp`,`value` 的话就代表以当前 `temp` 开始的字符串,经过后边的尝试是否能达到目标字符串 `s`。 - -```java -public boolean wordBreak(String s, List wordDict) { - HashSet set = new HashSet<>(); - for (int i = 0; i < wordDict.size(); i++) { - String t = wordDict.get(i); - for (int j = 0; j < t.length(); j++) { - set.add(t.charAt(j)); - } - } - for (int i = 0; i < s.length(); i++) { - if (!set.contains(s.charAt(i))) { - return false; - } - } - return wordBreakHelper(s, wordDict, "", new HashMap()); -} - -private boolean wordBreakHelper(String s, List wordDict, String temp, HashMap hashMap) { - if (temp.length() > s.length()) { - return false; - } - //之前是否存过 - if(hashMap.containsKey(temp)){ - return hashMap.get(temp); - } - for (int i = 0; i < temp.length(); i++) { - if (s.charAt(i) != temp.charAt(i)) { - return false; - } - } - if (s.length() == temp.length()) { - return true; - } - for (int i = 0; i < wordDict.size(); i++) { - if (wordBreakHelper(s, wordDict, temp + wordDict.get(i), hashMap)) { - //结果放入 hashMap - hashMap.put(temp, true); - return true; - } - } - //结果放入 hashMap - hashMap.put(temp, false); - return false; -} -``` - -这次就成功通过了。 - -# 解法二 分治 - -换一种思想,分治,也就是大问题转换为小问题,通过小问题来解决。 - -这个想法前边已经做过很多很多题了,大家可以参考 [97 题](https://leetcode.wang/leetCode-97-Interleaving-String.html) 、[115 题](https://leetcode.wang/leetcode-115-Distinct-Subsequences.html) 等等。 - -我们现在要判断目标串 `s` 是否能由 `wordDict` 构成。 - -我们用 `dp[i,j)`,表示从 `s` 的第 `i` 个字符开始,到第 `j` 个字符的前一个结束的字符串是否能由 `wordDict` 构成。 - -假如我们知道了 `dp[0,1) dp[0,2) dp[0,3)...dp[0,len - 1) ` ,也就是除 `s` 本身的所有子串是否能由 `wordDict` 构成。 - -那么我们就可以知道 - -```java -dp[0,len) = dp[0,1) && wordDict.contains(s[i,len)) - || dp[0,2) && wordDict.contains(s[2,len)) - || dp[0,3) && wordDict.contains(s[3,len)) - ... - || dp[0,len - 1) && wordDict.contains(s[len - 1,len)) -``` - -`dp[0,len)` 就代表着 `s` 是否能由 `wordDict` 构成。有了上边的转移方程,就可以用递归写出来了。 - -```java -public boolean wordBreak(String s, List wordDict) { - HashSet set = new HashSet<>(); - for (int i = 0; i < wordDict.size(); i++) { - set.add(wordDict.get(i)); - } - return wordBreakHelper(s, set); -} - -private boolean wordBreakHelper(String s, HashSet set) { - if (s.length() == 0) { - return true; - } - for (int i = 0; i < s.length(); i++) { - if (set.contains(s.substring(i, s.length())) && wordBreakHelper(s.substring(0, i), set)) { - return true; - } - } - return false; -} -``` - -如果不做任何处理,依旧会得到超时。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/139_5.jpg) - -所有,`memoization` 又来了,和之前一样将中间结果存储起来。 - -```java -public boolean wordBreak(String s, List wordDict) { - HashSet set = new HashSet<>(); - for (int i = 0; i < wordDict.size(); i++) { - set.add(wordDict.get(i)); - } - return wordBreakHelper(s, set, new HashMap()); -} - -private boolean wordBreakHelper(String s, HashSet set, HashMap map) { - if (s.length() == 0) { - return true; - } - if (map.containsKey(s)) { - return map.get(s); - } - for (int i = 0; i < s.length(); i++) { - if (set.contains(s.substring(i, s.length())) && wordBreakHelper(s.substring(0, i), set, map)) { - map.put(s, true); - return true; - } - } - map.put(s, false); - return false; -} -``` - -当然除了递归中存储,我们也可以直接用动态规划的思想,求一个结果就保存一个结果。 - -用 `dp[i]` 表示字符串 `s[0,i)` 能否由 `wordDict` 构成。 - -```java -public boolean wordBreak(String s, List wordDict) { - HashSet set = new HashSet<>(); - for (int i = 0; i < wordDict.size(); i++) { - set.add(wordDict.get(i)); - } - boolean[] dp = new boolean[s.length() + 1]; - dp[0] = true; - for (int i = 1; i <= s.length(); i++) { - for (int j = 0; j < i; j++) { - dp[i] = dp[j] && set.contains(s.substring(j, i)); - if (dp[i]) { - break; - } - } - } - return dp[s.length()]; -} -``` - -# 总 - -解法一的回溯优化主要就是剪枝,让一些提前知道结果的解直接结束,不进入递归。解法二的想法,就太常用了,从递归到 `memoization` 再到动态规划,其实本质都是一样的。 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/139.jpg) + +给一个字符串,和一些单词,问字符串能不能由这些单词构成。每个单词可以用多次,也可以不用。 + +# 解法一 回溯 + +来一个简单粗暴的方法,利用回溯法,用 `wordDict` 去生成所有可能的字符串。期间如果出现了目标字符串 `s`,就返回 `true`。 + +```java +public boolean wordBreak(String s, List wordDict) { + return wordBreakHelper(s,wordDict,""); +} +//temp 是当前生成的字符串 +private boolean wordBreakHelper(String s, List wordDict, String temp) { + //如果此时生成的字符串长度够了,就判断和目标字符日是否相等 + if(temp.length() == s.length()){ + if(temp.equals(s)){ + return true; + }else{ + return false; + } + } + //长度超了,就返回 false + if(temp.length() > s.length()){ + return false; + } + //考虑每个单词 + for(int i = 0;i < wordDict.size(); i++){ + if(wordBreakHelper(s,wordDict,temp + wordDict.get(i))){ + return true; + } + } + return false; +} +``` + +意料之中,超时了 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/139_2.jpg) + +让我们考虑优化的方法。 + +在递归出口的地方优化一下。 + +之前是在长度相等的时候,开始判断字符串是否相等。 + +很明显,字符串长度相等之前我们其实就可以判断当前是不是符合了。 + +例如 `temp = "abc"`,如果 `s = "dddefg"`,虽然此时 `temp` 和 `s` 的长度不相等。但因为前缀已经不同,所以后边无论是什么都不可以了。此时就可以返回 `false` 了。 + +所以递归出口可以从头判断每个字符是否相等,不相等就直接返回 `false`。 + +```java +for (int i = 0; i < temp.length(); i++) { + if (s.charAt(i) != temp.charAt(i)) { + return false; + } +} +``` + +然后代码就是下边的样子。 + +```java +public boolean wordBreak(String s, List wordDict) { + return wordBreakHelper(s, wordDict, ""); +} + +private boolean wordBreakHelper(String s, List wordDict, String temp) { + if (temp.length() > s.length()) { + return false; + } + //判断此时对应的字符是否全部相等 + for (int i = 0; i < temp.length(); i++) { + if (s.charAt(i) != temp.charAt(i)) { + return false; + } + } + if (s.length() == temp.length()) { + return true; + } + for (int i = 0; i < wordDict.size(); i++) { + if (wordBreakHelper(s, wordDict, temp + wordDict.get(i))) { + return true; + } + } + return false; +} +``` + +遗憾的是,依旧是超时 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/139_3.jpg) + +发现上边的例子答案很明显是 `false`,因为 `s` 中的 `b` 字母在 `wordDict` 中并没有出现。 + +所以我们可以先遍历一遍 `s` 和 `wordDict` ,从而确定 `s` 中的字符是否在 `wordDict` 中存在,如果不存在可以提前返回 `false` 。 + +所以代码可以继续优化。 + +```java +public boolean wordBreak(String s, List wordDict) { + HashSet set = new HashSet<>(); + //将 wordDict 的每个字母放到 set 中 + for (int i = 0; i < wordDict.size(); i++) { + String t = wordDict.get(i); + for (int j = 0; j < t.length(); j++) { + set.add(t.charAt(j)); + } + } + //判断 s 的每个字母在 set 中是否存在 + for (int i = 0; i < s.length(); i++) { + if (!set.contains(s.charAt(i))) { + return false; + } + } + return wordBreakHelper(s, wordDict, ""); +} + +private boolean wordBreakHelper(String s, List wordDict, String temp) { + if (temp.length() > s.length()) { + return false; + } + for (int i = 0; i < temp.length(); i++) { + if (s.charAt(i) != temp.charAt(i)) { + return false; + } + } + if (s.length() == temp.length()) { + return true; + } + for (int i = 0; i < wordDict.size(); i++) { + if (wordBreakHelper(s, wordDict, temp + wordDict.get(i))) { + return true; + } + } + return false; +} +``` + +令人悲伤的是 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/139_4.jpg) + +还有 `5` 个 `test` 没有通过。还有什么可以优化的地方呢? + +是时候拿出绝招了,在前边的题已经用过很多很多次,`memoization` 技术。思想就是把回溯中已经考虑过的解存起来,第二次回溯过来的时候可以直接使用。 + +这里的话,我们可以用一个 `HashMap`,`key` 的话就存 `temp`,`value` 的话就代表以当前 `temp` 开始的字符串,经过后边的尝试是否能达到目标字符串 `s`。 + +```java +public boolean wordBreak(String s, List wordDict) { + HashSet set = new HashSet<>(); + for (int i = 0; i < wordDict.size(); i++) { + String t = wordDict.get(i); + for (int j = 0; j < t.length(); j++) { + set.add(t.charAt(j)); + } + } + for (int i = 0; i < s.length(); i++) { + if (!set.contains(s.charAt(i))) { + return false; + } + } + return wordBreakHelper(s, wordDict, "", new HashMap()); +} + +private boolean wordBreakHelper(String s, List wordDict, String temp, HashMap hashMap) { + if (temp.length() > s.length()) { + return false; + } + //之前是否存过 + if(hashMap.containsKey(temp)){ + return hashMap.get(temp); + } + for (int i = 0; i < temp.length(); i++) { + if (s.charAt(i) != temp.charAt(i)) { + return false; + } + } + if (s.length() == temp.length()) { + return true; + } + for (int i = 0; i < wordDict.size(); i++) { + if (wordBreakHelper(s, wordDict, temp + wordDict.get(i), hashMap)) { + //结果放入 hashMap + hashMap.put(temp, true); + return true; + } + } + //结果放入 hashMap + hashMap.put(temp, false); + return false; +} +``` + +这次就成功通过了。 + +# 解法二 分治 + +换一种思想,分治,也就是大问题转换为小问题,通过小问题来解决。 + +这个想法前边已经做过很多很多题了,大家可以参考 [97 题](https://leetcode.wang/leetCode-97-Interleaving-String.html) 、[115 题](https://leetcode.wang/leetcode-115-Distinct-Subsequences.html) 等等。 + +我们现在要判断目标串 `s` 是否能由 `wordDict` 构成。 + +我们用 `dp[i,j)`,表示从 `s` 的第 `i` 个字符开始,到第 `j` 个字符的前一个结束的字符串是否能由 `wordDict` 构成。 + +假如我们知道了 `dp[0,1) dp[0,2) dp[0,3)...dp[0,len - 1) ` ,也就是除 `s` 本身的所有子串是否能由 `wordDict` 构成。 + +那么我们就可以知道 + +```java +dp[0,len) = dp[0,1) && wordDict.contains(s[i,len)) + || dp[0,2) && wordDict.contains(s[2,len)) + || dp[0,3) && wordDict.contains(s[3,len)) + ... + || dp[0,len - 1) && wordDict.contains(s[len - 1,len)) +``` + +`dp[0,len)` 就代表着 `s` 是否能由 `wordDict` 构成。有了上边的转移方程,就可以用递归写出来了。 + +```java +public boolean wordBreak(String s, List wordDict) { + HashSet set = new HashSet<>(); + for (int i = 0; i < wordDict.size(); i++) { + set.add(wordDict.get(i)); + } + return wordBreakHelper(s, set); +} + +private boolean wordBreakHelper(String s, HashSet set) { + if (s.length() == 0) { + return true; + } + for (int i = 0; i < s.length(); i++) { + if (set.contains(s.substring(i, s.length())) && wordBreakHelper(s.substring(0, i), set)) { + return true; + } + } + return false; +} +``` + +如果不做任何处理,依旧会得到超时。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/139_5.jpg) + +所有,`memoization` 又来了,和之前一样将中间结果存储起来。 + +```java +public boolean wordBreak(String s, List wordDict) { + HashSet set = new HashSet<>(); + for (int i = 0; i < wordDict.size(); i++) { + set.add(wordDict.get(i)); + } + return wordBreakHelper(s, set, new HashMap()); +} + +private boolean wordBreakHelper(String s, HashSet set, HashMap map) { + if (s.length() == 0) { + return true; + } + if (map.containsKey(s)) { + return map.get(s); + } + for (int i = 0; i < s.length(); i++) { + if (set.contains(s.substring(i, s.length())) && wordBreakHelper(s.substring(0, i), set, map)) { + map.put(s, true); + return true; + } + } + map.put(s, false); + return false; +} +``` + +当然除了递归中存储,我们也可以直接用动态规划的思想,求一个结果就保存一个结果。 + +用 `dp[i]` 表示字符串 `s[0,i)` 能否由 `wordDict` 构成。 + +```java +public boolean wordBreak(String s, List wordDict) { + HashSet set = new HashSet<>(); + for (int i = 0; i < wordDict.size(); i++) { + set.add(wordDict.get(i)); + } + boolean[] dp = new boolean[s.length() + 1]; + dp[0] = true; + for (int i = 1; i <= s.length(); i++) { + for (int j = 0; j < i; j++) { + dp[i] = dp[j] && set.contains(s.substring(j, i)); + if (dp[i]) { + break; + } + } + } + return dp[s.length()]; +} +``` + +# 总 + +解法一的回溯优化主要就是剪枝,让一些提前知道结果的解直接结束,不进入递归。解法二的想法,就太常用了,从递归到 `memoization` 再到动态规划,其实本质都是一样的。 + diff --git a/leetcode-140-Word-BreakII.md b/leetcode-140-Word-BreakII.md index 2a4cdde15..7cf67b319 100644 --- a/leetcode-140-Word-BreakII.md +++ b/leetcode-140-Word-BreakII.md @@ -1,204 +1,204 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/140.png) - -[139 题](https://leetcode.wang/leetcode-139-Word-Break.html) 的升级版。给一个字符串,和一些单词,找出由这些单词组成该字符串的所有可能,每个单词可以用多次,也可以不用。 - -完全按照 [139 题](https://leetcode.wang/leetcode-139-Word-Break.html) 的思路做了,大家可以先过去看一下。 - -# 解法一 动态规划 - -先考虑 `139 题` 最后一个解法,动态规划,看起来也比较简单粗暴。 - -> 用 `dp[i]` 表示字符串 `s[0,i)` 能否由 `wordDict` 构成。 -> -> ```java -> public boolean wordBreak(String s, List wordDict) { -> HashSet set = new HashSet<>(); -> for (int i = 0; i < wordDict.size(); i++) { -> set.add(wordDict.get(i)); -> } -> boolean[] dp = new boolean[s.length() + 1]; -> dp[0] = true; -> for (int i = 1; i <= s.length(); i++) { -> for (int j = 0; j < i; j++) { -> dp[i] = dp[j] && set.contains(s.substring(j, i)); -> if (dp[i]) { -> break; -> } -> } -> } -> return dp[s.length()]; -> } -> ``` - -这里修改的话,我们只需要用 `dp[i]` 表示字符串 `s[0,i)` 由 `wordDict` 构成的所有情况。 - -总体思想还是和之前一样的。 - -```java -X X X X X X -^ ^ ^ -0 j i -先判断 j 到 i 的字符串在没在 wordDict 中 -然后把 0 到 j 的字符串由 wordDict 构成所有情况后边加空格再加上 j 到 i 的字符串即可 -``` - -结合上边的思想,然后把它放到循环中,考虑所有情况即可。 - -```java -public List wordBreak(String s, List wordDict) { - HashSet set = new HashSet<>(); - for (int i = 0; i < wordDict.size(); i++) { - set.add(wordDict.get(i)); - } - List> dp = new ArrayList<>(); - List temp = new ArrayList<>(); - //空串的情况 - temp.add(""); - dp.add(temp); - for (int i = 1; i <= s.length(); i++) { - temp = new ArrayList<>(); - for (int j = 0; j < i; j++) { - if (set.contains(s.substring(j, i))) { - //得到前半部分的所有情况然后和当前单词相加 - for (int k = 0; k < dp.get(j).size(); k++) { - String t = dp.get(j).get(k); - //空串的时候不加空格,也就是 j = 0 的时候 - if (t.equals("")) { - temp.add(s.substring(j, i)); - } else { - temp.add(t + " " + s.substring(j, i)); - } - - } - - } - } - dp.add(temp); - } - return dp.get(s.length()); -} -``` - -遗憾的是,熟悉的问题又来了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/140_2.jpg) - -由于 `s` 中的 `b` 字母在 `wordDict` 中并没有出现,所以其实我们并不需要做那么多循环,直接返回空列表即可。 - -和之前一样,所以我们可以先遍历一遍 `s` 和 `wordDict` ,从而确定 `s` 中的字符是否在 `wordDict` 中存在,如果不存在可以提前返回空列表。 - -```java -public List wordBreak(String s, List wordDict) { - //提前进行一次判断 - HashSet set2 = new HashSet<>(); - for (int i = 0; i < wordDict.size(); i++) { - String t = wordDict.get(i); - for (int j = 0; j < t.length(); j++) { - set2.add(t.charAt(j)); - } - } - for (int i = 0; i < s.length(); i++) { - if (!set2.contains(s.charAt(i))) { - return new ArrayList<>(); - } - } - - HashSet set = new HashSet<>(); - for (int i = 0; i < wordDict.size(); i++) { - set.add(wordDict.get(i)); - } - List> dp = new ArrayList<>(); - List temp = new ArrayList<>(); - temp.add(""); - dp.add(temp); - for (int i = 1; i <= s.length(); i++) { - temp = new ArrayList<>(); - for (int j = 0; j < i; j++) { - if (set.contains(s.substring(j, i))) { - for (int k = 0; k < dp.get(j).size(); k++) { - String t = dp.get(j).get(k); - if (t.equals("")) { - temp.add(s.substring(j, i)); - } else { - temp.add(t + " " + s.substring(j, i)); - } - - } - - } - } - dp.add(temp); - } - return dp.get(s.length()); -} -``` - -遗憾的是,刚刚那个例子通过了,又出现了新的问题。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/140_3.jpg) - -由于 `wordDict` 有 `b` 字母了,所以并没有提前结束,而是进了 `for` 循环。 - -再优化也想不到方法了,是我们的算法出问题了。因为 `139` 题中找到一个解以后就 `break` 了,而这里我们要考虑所有子串,所有的解,极端情况下时间复杂度达到了 `O(n³)`。还有一点致命的是,我们之前求的解最后可能并不需要。举个例子。 - -```java -"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabad" -["aa","aaa","aaaa","aaaaa","aaaaaa","aaaaaaa","aaaaaaaa","aaaaaaaaa","aaaaaaaaaa","b","ba","de"] - -我们之前求了 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 的所有组成可能,但由于剩余字符串 "bad" 不在 wordDict 中,所有之前求出来并没有用 - -又比如,我们之前求了 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab" 的所有组成可能,但由于剩余字符串 "ad" 不在 wordDict 中,所有之前求出来也并没有用 -``` - -针对这个问题,我们可以优化一下,也就是下边的解法二 - -# 解法二 - -我们直接用递归的方法,先判断当前字符串在不在 `wordDict` 中,如果在的话就递归的去求剩余字符串的所有组成可能。此外,吸取之前的教训,直接使用 `memoization` 技术,将递归过程中求出来的解缓存起来,便于之后直接用。 - -```java -public List wordBreak(String s, List wordDict) { - HashSet set = new HashSet<>(); - for (int i = 0; i < wordDict.size(); i++) { - set.add(wordDict.get(i)); - } - return wordBreakHelper(s, set, new HashMap>()); -} - -private List wordBreakHelper(String s, HashSet set, HashMap> map) { - if (s.length() == 0) { - return new ArrayList<>(); - } - if (map.containsKey(s)) { - return map.get(s); - } - List res = new ArrayList<>(); - for (int j = 0; j < s.length(); j++) { - //判断当前字符串是否存在 - if (set.contains(s.substring(j, s.length()))) { - //空串的情况,直接加入 - if (j == 0) { - res.add(s.substring(j, s.length())); - } else { - //递归得到剩余字符串的所有组成可能,然后和当前字符串分别用空格连起来加到结果中 - List temp = wordBreakHelper(s.substring(0, j), set, map); - for (int k = 0; k < temp.size(); k++) { - String t = temp.get(k); - res.add(t + " " + s.substring(j, s.length())); - } - } - - } - } - //缓存结果 - map.put(s, res); - return res; -} -``` - -# 总 - -按理说其实可以直接就想到解法二,但受之前写的题的影响,这种分治的问题,都最终能转成动态规划,所以先写了动态规划的思路,想直接一步到位,没想到反而遇到了问题,很有意思,哈哈。原因就是你求子问题的代价很大,而动态规划就是要求所有的子问题。而解决最终问题的时候,一些子问题其实是没有必要的。 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/140.png) + +[139 题](https://leetcode.wang/leetcode-139-Word-Break.html) 的升级版。给一个字符串,和一些单词,找出由这些单词组成该字符串的所有可能,每个单词可以用多次,也可以不用。 + +完全按照 [139 题](https://leetcode.wang/leetcode-139-Word-Break.html) 的思路做了,大家可以先过去看一下。 + +# 解法一 动态规划 + +先考虑 `139 题` 最后一个解法,动态规划,看起来也比较简单粗暴。 + +> 用 `dp[i]` 表示字符串 `s[0,i)` 能否由 `wordDict` 构成。 +> +> ```java +> public boolean wordBreak(String s, List wordDict) { +> HashSet set = new HashSet<>(); +> for (int i = 0; i < wordDict.size(); i++) { +> set.add(wordDict.get(i)); +> } +> boolean[] dp = new boolean[s.length() + 1]; +> dp[0] = true; +> for (int i = 1; i <= s.length(); i++) { +> for (int j = 0; j < i; j++) { +> dp[i] = dp[j] && set.contains(s.substring(j, i)); +> if (dp[i]) { +> break; +> } +> } +> } +> return dp[s.length()]; +> } +> ``` + +这里修改的话,我们只需要用 `dp[i]` 表示字符串 `s[0,i)` 由 `wordDict` 构成的所有情况。 + +总体思想还是和之前一样的。 + +```java +X X X X X X +^ ^ ^ +0 j i +先判断 j 到 i 的字符串在没在 wordDict 中 +然后把 0 到 j 的字符串由 wordDict 构成所有情况后边加空格再加上 j 到 i 的字符串即可 +``` + +结合上边的思想,然后把它放到循环中,考虑所有情况即可。 + +```java +public List wordBreak(String s, List wordDict) { + HashSet set = new HashSet<>(); + for (int i = 0; i < wordDict.size(); i++) { + set.add(wordDict.get(i)); + } + List> dp = new ArrayList<>(); + List temp = new ArrayList<>(); + //空串的情况 + temp.add(""); + dp.add(temp); + for (int i = 1; i <= s.length(); i++) { + temp = new ArrayList<>(); + for (int j = 0; j < i; j++) { + if (set.contains(s.substring(j, i))) { + //得到前半部分的所有情况然后和当前单词相加 + for (int k = 0; k < dp.get(j).size(); k++) { + String t = dp.get(j).get(k); + //空串的时候不加空格,也就是 j = 0 的时候 + if (t.equals("")) { + temp.add(s.substring(j, i)); + } else { + temp.add(t + " " + s.substring(j, i)); + } + + } + + } + } + dp.add(temp); + } + return dp.get(s.length()); +} +``` + +遗憾的是,熟悉的问题又来了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/140_2.jpg) + +由于 `s` 中的 `b` 字母在 `wordDict` 中并没有出现,所以其实我们并不需要做那么多循环,直接返回空列表即可。 + +和之前一样,所以我们可以先遍历一遍 `s` 和 `wordDict` ,从而确定 `s` 中的字符是否在 `wordDict` 中存在,如果不存在可以提前返回空列表。 + +```java +public List wordBreak(String s, List wordDict) { + //提前进行一次判断 + HashSet set2 = new HashSet<>(); + for (int i = 0; i < wordDict.size(); i++) { + String t = wordDict.get(i); + for (int j = 0; j < t.length(); j++) { + set2.add(t.charAt(j)); + } + } + for (int i = 0; i < s.length(); i++) { + if (!set2.contains(s.charAt(i))) { + return new ArrayList<>(); + } + } + + HashSet set = new HashSet<>(); + for (int i = 0; i < wordDict.size(); i++) { + set.add(wordDict.get(i)); + } + List> dp = new ArrayList<>(); + List temp = new ArrayList<>(); + temp.add(""); + dp.add(temp); + for (int i = 1; i <= s.length(); i++) { + temp = new ArrayList<>(); + for (int j = 0; j < i; j++) { + if (set.contains(s.substring(j, i))) { + for (int k = 0; k < dp.get(j).size(); k++) { + String t = dp.get(j).get(k); + if (t.equals("")) { + temp.add(s.substring(j, i)); + } else { + temp.add(t + " " + s.substring(j, i)); + } + + } + + } + } + dp.add(temp); + } + return dp.get(s.length()); +} +``` + +遗憾的是,刚刚那个例子通过了,又出现了新的问题。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/140_3.jpg) + +由于 `wordDict` 有 `b` 字母了,所以并没有提前结束,而是进了 `for` 循环。 + +再优化也想不到方法了,是我们的算法出问题了。因为 `139` 题中找到一个解以后就 `break` 了,而这里我们要考虑所有子串,所有的解,极端情况下时间复杂度达到了 `O(n³)`。还有一点致命的是,我们之前求的解最后可能并不需要。举个例子。 + +```java +"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabad" +["aa","aaa","aaaa","aaaaa","aaaaaa","aaaaaaa","aaaaaaaa","aaaaaaaaa","aaaaaaaaaa","b","ba","de"] + +我们之前求了 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 的所有组成可能,但由于剩余字符串 "bad" 不在 wordDict 中,所有之前求出来并没有用 + +又比如,我们之前求了 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab" 的所有组成可能,但由于剩余字符串 "ad" 不在 wordDict 中,所有之前求出来也并没有用 +``` + +针对这个问题,我们可以优化一下,也就是下边的解法二 + +# 解法二 + +我们直接用递归的方法,先判断当前字符串在不在 `wordDict` 中,如果在的话就递归的去求剩余字符串的所有组成可能。此外,吸取之前的教训,直接使用 `memoization` 技术,将递归过程中求出来的解缓存起来,便于之后直接用。 + +```java +public List wordBreak(String s, List wordDict) { + HashSet set = new HashSet<>(); + for (int i = 0; i < wordDict.size(); i++) { + set.add(wordDict.get(i)); + } + return wordBreakHelper(s, set, new HashMap>()); +} + +private List wordBreakHelper(String s, HashSet set, HashMap> map) { + if (s.length() == 0) { + return new ArrayList<>(); + } + if (map.containsKey(s)) { + return map.get(s); + } + List res = new ArrayList<>(); + for (int j = 0; j < s.length(); j++) { + //判断当前字符串是否存在 + if (set.contains(s.substring(j, s.length()))) { + //空串的情况,直接加入 + if (j == 0) { + res.add(s.substring(j, s.length())); + } else { + //递归得到剩余字符串的所有组成可能,然后和当前字符串分别用空格连起来加到结果中 + List temp = wordBreakHelper(s.substring(0, j), set, map); + for (int k = 0; k < temp.size(); k++) { + String t = temp.get(k); + res.add(t + " " + s.substring(j, s.length())); + } + } + + } + } + //缓存结果 + map.put(s, res); + return res; +} +``` + +# 总 + +按理说其实可以直接就想到解法二,但受之前写的题的影响,这种分治的问题,都最终能转成动态规划,所以先写了动态规划的思路,想直接一步到位,没想到反而遇到了问题,很有意思,哈哈。原因就是你求子问题的代价很大,而动态规划就是要求所有的子问题。而解决最终问题的时候,一些子问题其实是没有必要的。 + diff --git a/leetcode-141-Linked-List-Cycle.md b/leetcode-141-Linked-List-Cycle.md index 77a946c6d..db07ed4c7 100644 --- a/leetcode-141-Linked-List-Cycle.md +++ b/leetcode-141-Linked-List-Cycle.md @@ -1,53 +1,53 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/141.png) - -判断一个链表是否有环。 - -# 解法一 - -最直接的方法,遍历链表,并且把遍历过的节点用 `HashSet` 存起来,如果遍历过程中又遇到了之前的节点就说明有环。如果到达了 `null` 就说明没有环。 - -```java -public boolean hasCycle(ListNode head) { - HashSet set = new HashSet<>(); - while (head != null) { - set.add(head); - head = head.next; - if (set.contains(head)) { - return true; - } - } - return false; -} -``` - -# 解法二 - -学数据结构课程的时候,应该都用过这个方法,很巧妙,快慢指针。 - -原理也很好理解,想象一下圆形跑道,两个人跑步,如果一个人跑的快,一个人跑的慢,那么不管两个人从哪个位置出发,跑的过程中两人一定会相遇。 - -所以这里我们用两个指针 `fast` 和 `slow`。`fast` 每次走两步,`slow` 每次走一步,如果 `fast` 到达了 `null` 就说明没有环。如果 `fast` 和 `slow` 相遇了就说明有环。 - -```java -public boolean hasCycle(ListNode head) { - ListNode slow = head; - ListNode fast = head; - while (fast != null) { - if (fast.next == null) { - return false; - } - slow = slow.next; - fast = fast.next.next; - if (fast == slow) { - return true; - } - } - return false; -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/141.png) + +判断一个链表是否有环。 + +# 解法一 + +最直接的方法,遍历链表,并且把遍历过的节点用 `HashSet` 存起来,如果遍历过程中又遇到了之前的节点就说明有环。如果到达了 `null` 就说明没有环。 + +```java +public boolean hasCycle(ListNode head) { + HashSet set = new HashSet<>(); + while (head != null) { + set.add(head); + head = head.next; + if (set.contains(head)) { + return true; + } + } + return false; +} +``` + +# 解法二 + +学数据结构课程的时候,应该都用过这个方法,很巧妙,快慢指针。 + +原理也很好理解,想象一下圆形跑道,两个人跑步,如果一个人跑的快,一个人跑的慢,那么不管两个人从哪个位置出发,跑的过程中两人一定会相遇。 + +所以这里我们用两个指针 `fast` 和 `slow`。`fast` 每次走两步,`slow` 每次走一步,如果 `fast` 到达了 `null` 就说明没有环。如果 `fast` 和 `slow` 相遇了就说明有环。 + +```java +public boolean hasCycle(ListNode head) { + ListNode slow = head; + ListNode fast = head; + while (fast != null) { + if (fast.next == null) { + return false; + } + slow = slow.next; + fast = fast.next.next; + if (fast == slow) { + return true; + } + } + return false; +} +``` + +# 总 + 比较简单的一道题了,快慢指针的思想,也比较常用,很巧妙。 \ No newline at end of file diff --git a/leetcode-142-Linked-List-CycleII.md b/leetcode-142-Linked-List-CycleII.md index 414a0a627..44bd9e3ab 100644 --- a/leetcode-142-Linked-List-CycleII.md +++ b/leetcode-142-Linked-List-CycleII.md @@ -1,102 +1,102 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/142.png) - -[141 题](https://leetcode.wang/leetcode-141-Linked-List-Cycle.html) 的升级版,之前只需要判断是否有环,现在需要把环的入口点找到,也就是直线和圆的交接点。思路的话,还是之前的两种思路。 - -# 解法一 HashMap - -遍历链表,并且把遍历过的节点用 `HashSet` 存起来,如果遍历过程中又遇到了之前的节点,说明这个节点就是我们要找到入口点。如果到达了 `null` 就说明没有环。 - -```java -public ListNode detectCycle(ListNode head) { - HashSet set = new HashSet<>(); - while (head != null) { - set.add(head); - head = head.next; - if (set.contains(head)) { - return head; - } - } - return null; -} -``` - -# 解法二 快慢指针 - -还是之前的思想, - -> 学数据结构课程的时候,应该都用过这个方法,很巧妙,快慢指针。 -> -> 原理也很好理解,想象一下圆形跑道,两个人跑步,如果一个人跑的快,一个人跑的慢,那么不管两个人从哪个位置出发,跑的过程中两人一定会相遇。 -> -> 所以这里我们用两个指针 `fast` 和 `slow`。`fast` 每次走两步,`slow` 每次走一步,如果 `fast` 到达了 `null` 就说明没有环。如果 `fast` 和 `slow` 相遇了就说明有环。 - -但是这道题,我们需要找到入口点,而快慢指针相遇的点可能并不是入口点,而是环中的某一个点,所以需要一些数学上的推导,参考了 [这里](https://leetcode.com/problems/linked-list-cycle-ii/discuss/44793/O(n)-solution-by-using-two-pointers-without-change-anything) 。 - -如下图,我们明确几个位置。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/142_2.jpg) - -从 `head` 到入口点的距离设为 `x`,入口点到相遇点的距离设为 `y`,环的的长度设为 `n`。 - -假设 `slow` 指针走过的距离为 `t`,那么 `fast` 指针走过的一定是 `slow` 指针的 `2` 倍,也就是 `2t`。 - -`slow` 指针从 `head` 出发走了 `x` 的距离到达入口点,然后可能走了 `k1` 圈,然后再次回到入口点,再走了 `y` 的距离到达相遇点和 `fast` 指针相遇。 - -`t = x + k1 * n + y` - -`fast` 指针同理,`fast` 指针从 `head` 出发走了 `x` 的距离到达入口点,然后可能走了 `k2` 圈,然后再次回到入口点,再走了 `y` 的距离到达相遇点和 `slow` 指针相遇。 - -`2t = x + k2 * n + y` - -上边两个等式做一个差,可以得到 - -`t = (k2 - k1) * n` - -设 `k = k2 - k1` ,那么 `t = k * n`。 - -把 `t = k * n` 代入到第一个式子 `t = x + k1 * n + y` 中。 - -`k * n = x + k1 * n + y` - -移项,`x = (k - k1) * n - y` - -取出一个 `n` 和 `y` 结合,`x = (k - k1 - 1) * n + (n - y)` - -左边的含义就是从 `head` 到达入口点。 - -右边的含义, `n - y` 就是从相遇点到入口点的距离,`(k - k1 - 1) * n` 就是转 `(k - k1 - 1)` 圈。 - -左边右边的含义结合起来就是,从相遇点走到入口点,然后转 `(k - k1 - 1)` 圈后再次回到入口点的这段时间,刚好就等于从 `head` 走向入口点的时间。 - -所以代码的话,我们只需要 `meet` 指针从相遇点出发的同时,让 `head` 指针也出发, `head` 指针和 `meet` 指针相遇的位置就是入口点了。 - -```java -public ListNode detectCycle(ListNode head) { - ListNode slow = head; - ListNode fast = head; - ListNode meet = null; - while (fast != null) { - if (fast.next == null) { - return null; - } - slow = slow.next; - fast = fast.next.next; - //到达相遇点 - if (fast == slow) { - meet = fast; - while (head != meet) { - head = head.next; - meet = meet.next; - } - return head; - } - } - return null; -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/142.png) + +[141 题](https://leetcode.wang/leetcode-141-Linked-List-Cycle.html) 的升级版,之前只需要判断是否有环,现在需要把环的入口点找到,也就是直线和圆的交接点。思路的话,还是之前的两种思路。 + +# 解法一 HashMap + +遍历链表,并且把遍历过的节点用 `HashSet` 存起来,如果遍历过程中又遇到了之前的节点,说明这个节点就是我们要找到入口点。如果到达了 `null` 就说明没有环。 + +```java +public ListNode detectCycle(ListNode head) { + HashSet set = new HashSet<>(); + while (head != null) { + set.add(head); + head = head.next; + if (set.contains(head)) { + return head; + } + } + return null; +} +``` + +# 解法二 快慢指针 + +还是之前的思想, + +> 学数据结构课程的时候,应该都用过这个方法,很巧妙,快慢指针。 +> +> 原理也很好理解,想象一下圆形跑道,两个人跑步,如果一个人跑的快,一个人跑的慢,那么不管两个人从哪个位置出发,跑的过程中两人一定会相遇。 +> +> 所以这里我们用两个指针 `fast` 和 `slow`。`fast` 每次走两步,`slow` 每次走一步,如果 `fast` 到达了 `null` 就说明没有环。如果 `fast` 和 `slow` 相遇了就说明有环。 + +但是这道题,我们需要找到入口点,而快慢指针相遇的点可能并不是入口点,而是环中的某一个点,所以需要一些数学上的推导,参考了 [这里](https://leetcode.com/problems/linked-list-cycle-ii/discuss/44793/O(n)-solution-by-using-two-pointers-without-change-anything) 。 + +如下图,我们明确几个位置。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/142_2.jpg) + +从 `head` 到入口点的距离设为 `x`,入口点到相遇点的距离设为 `y`,环的的长度设为 `n`。 + +假设 `slow` 指针走过的距离为 `t`,那么 `fast` 指针走过的一定是 `slow` 指针的 `2` 倍,也就是 `2t`。 + +`slow` 指针从 `head` 出发走了 `x` 的距离到达入口点,然后可能走了 `k1` 圈,然后再次回到入口点,再走了 `y` 的距离到达相遇点和 `fast` 指针相遇。 + +`t = x + k1 * n + y` + +`fast` 指针同理,`fast` 指针从 `head` 出发走了 `x` 的距离到达入口点,然后可能走了 `k2` 圈,然后再次回到入口点,再走了 `y` 的距离到达相遇点和 `slow` 指针相遇。 + +`2t = x + k2 * n + y` + +上边两个等式做一个差,可以得到 + +`t = (k2 - k1) * n` + +设 `k = k2 - k1` ,那么 `t = k * n`。 + +把 `t = k * n` 代入到第一个式子 `t = x + k1 * n + y` 中。 + +`k * n = x + k1 * n + y` + +移项,`x = (k - k1) * n - y` + +取出一个 `n` 和 `y` 结合,`x = (k - k1 - 1) * n + (n - y)` + +左边的含义就是从 `head` 到达入口点。 + +右边的含义, `n - y` 就是从相遇点到入口点的距离,`(k - k1 - 1) * n` 就是转 `(k - k1 - 1)` 圈。 + +左边右边的含义结合起来就是,从相遇点走到入口点,然后转 `(k - k1 - 1)` 圈后再次回到入口点的这段时间,刚好就等于从 `head` 走向入口点的时间。 + +所以代码的话,我们只需要 `meet` 指针从相遇点出发的同时,让 `head` 指针也出发, `head` 指针和 `meet` 指针相遇的位置就是入口点了。 + +```java +public ListNode detectCycle(ListNode head) { + ListNode slow = head; + ListNode fast = head; + ListNode meet = null; + while (fast != null) { + if (fast.next == null) { + return null; + } + slow = slow.next; + fast = fast.next.next; + //到达相遇点 + if (fast == slow) { + meet = fast; + while (head != meet) { + head = head.next; + meet = meet.next; + } + return head; + } + } + return null; +} +``` + +# 总 + 解法一很直接,但是多用了空间。解法二自己推导时候,只差了最后一步的变形,没有明确我们要求的变量 `x`。然后看了别人的题解才恍然大悟。 \ No newline at end of file diff --git a/leetcode-143-Reorder-List.md b/leetcode-143-Reorder-List.md index da7fb7891..934ffca85 100644 --- a/leetcode-143-Reorder-List.md +++ b/leetcode-143-Reorder-List.md @@ -1,195 +1,195 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/143.jpg) - -给一个链表,然后依次头尾头尾头尾取元素,组成新的链表。 - -# 解法一 存储 - -链表的缺点就是不能随机存储,当我们想取末尾元素的时候,只能从头遍历一遍,很耗费时间。第二次取末尾元素的时候,又得遍历一遍。 - -所以先来个简单粗暴的想法,把链表存储到线性表中,然后用双指针依次从头尾取元素即可。 - -```java -public void reorderList(ListNode head) { - if (head == null) { - return; - } - //存到 list 中去 - List list = new ArrayList<>(); - while (head != null) { - list.add(head); - head = head.next; - } - //头尾指针依次取元素 - int i = 0, j = list.size() - 1; - while (i < j) { - list.get(i).next = list.get(j); - i++; - //偶数个节点的情况,会提前相遇 - if (i == j) { - break; - } - list.get(j).next = list.get(i); - j--; - } - list.get(i).next = null; -} -``` - -# 解法二 递归 - -参考 [这里](https://leetcode.com/problems/reorder-list/discuss/45113/Share-a-consise-recursive-solution-in-C%2B%2B)。 - -解法一中也说到了,我们的问题就是取尾元素的时候,需要遍历一遍链表。 - -如果我们的递归函数能够返回当前头元素对应的尾元素,并且将头元素和尾元素之间的链表按要求完成,那就变得简单了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/143_2.jpg) - -如上图,我们只需要将 `head` 指向 `tail`,`tail` 指向处理完的链表头即可。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/143_3.jpg) - -然后我们把之前的 `tail.next` 返回就是外层 `head` 对应的 `tail` 了。 - -递归出口的话,如果只有一个节点,那么我们只需要将 `head.next` 返回。 - -```java -if (len == 1) { - ListNode outTail = head.next; - head.next = null; - return outTail; -} -``` - -如果是两个节点,我们需要将 `head.next.next` 返回。 - -```java -if (len == 2) { - ListNode outTail = head.next.next; - head.next.next = null; - return outTail; -} -``` - -然后总体的代码就是下边的样子 - -```java -public void reorderList(ListNode head) { - - if (head == null || head.next == null || head.next.next == null) { - return; - } - int len = 0; - ListNode h = head; - //求出节点数 - while (h != null) { - len++; - h = h.next; - } - - reorderListHelper(head, len); -} - -private ListNode reorderListHelper(ListNode head, int len) { - if (len == 1) { - ListNode outTail = head.next; - head.next = null; - return outTail; - } - if (len == 2) { - ListNode outTail = head.next.next; - head.next.next = null; - return outTail; - } - //得到对应的尾节点,并且将头结点和尾节点之间的链表通过递归处理 - ListNode tail = reorderListHelper(head.next, len - 2); - ListNode subHead = head.next;//中间链表的头结点 - head.next = tail; - ListNode outTail = tail.next; //上一层 head 对应的 tail - tail.next = subHead; - return outTail; -} -``` - -# 解法三 - -参考 [这里](https://leetcode.com/problems/reorder-list/discuss/44992/Java-solution-with-3-steps),主要是利用到一头一尾取元素的特性。 - -主要是三步,举个例子。 - -```java -1 -> 2 -> 3 -> 4 -> 5 -> 6 -第一步,将链表平均分成两半 -1 -> 2 -> 3 -4 -> 5 -> 6 - -第二步,将第二个链表逆序 -1 -> 2 -> 3 -6 -> 5 -> 4 - -第三步,依次连接两个链表 -1 -> 6 -> 2 -> 5 -> 3 -> 4 -``` - -第一步找中点的话,可以应用 [19 题](https://leetcode.wang/leetCode-19-Remov-Nth-Node-From-End-of-List.html) 的方法,快慢指针。快指针一次走两步,慢指针一次走一步,当快指针走到终点的话,慢指针会刚好到中点。如果节点个数是偶数的话,`slow` 走到的是左端点,利用这一点,我们可以把奇数和偶数的情况合并,不需要分开考虑。 - -第二步链表逆序的话,在 [第 2 题](https://leetcode.wang/leetCode-2-Add-Two-Numbers.html) 讨论过了,有迭代和递归的两种方式,迭代的话主要利用两个指针,依次逆转。 - -第三步的话就很简单了,两个指针分别向后移动就可以。 - -```java -public void reorderList(ListNode head) { - if (head == null || head.next == null || head.next.next == null) { - return; - } - //找中点,链表分成两个 - ListNode slow = head; - ListNode fast = head; - while (fast.next != null && fast.next.next != null) { - slow = slow.next; - fast = fast.next.next; - } - - ListNode newHead = slow.next; - slow.next = null; - - //第二个链表倒置 - newHead = reverseList(newHead); - - //链表节点依次连接 - while (newHead != null) { - ListNode temp = newHead.next; - newHead.next = head.next; - head.next = newHead; - head = newHead.next; - newHead = temp; - } - -} - -private ListNode reverseList(ListNode head) { - if (head == null) { - return null; - } - ListNode tail = head; - head = head.next; - - tail.next = null; - - while (head != null) { - ListNode temp = head.next; - head.next = tail; - tail = head; - head = temp; - } - - return tail; -} -``` - -# 总 - -解法一利用空间去存储就很简单了,解法二递归的思想也很经典,自己也想了很久,看到作者的思路才恍然大悟,判断当前 `length` 定义递归出口很巧妙。解法三主要就是对题目的理解,关键就是利用一头一尾取元素的特性。 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/143.jpg) + +给一个链表,然后依次头尾头尾头尾取元素,组成新的链表。 + +# 解法一 存储 + +链表的缺点就是不能随机存储,当我们想取末尾元素的时候,只能从头遍历一遍,很耗费时间。第二次取末尾元素的时候,又得遍历一遍。 + +所以先来个简单粗暴的想法,把链表存储到线性表中,然后用双指针依次从头尾取元素即可。 + +```java +public void reorderList(ListNode head) { + if (head == null) { + return; + } + //存到 list 中去 + List list = new ArrayList<>(); + while (head != null) { + list.add(head); + head = head.next; + } + //头尾指针依次取元素 + int i = 0, j = list.size() - 1; + while (i < j) { + list.get(i).next = list.get(j); + i++; + //偶数个节点的情况,会提前相遇 + if (i == j) { + break; + } + list.get(j).next = list.get(i); + j--; + } + list.get(i).next = null; +} +``` + +# 解法二 递归 + +参考 [这里](https://leetcode.com/problems/reorder-list/discuss/45113/Share-a-consise-recursive-solution-in-C%2B%2B)。 + +解法一中也说到了,我们的问题就是取尾元素的时候,需要遍历一遍链表。 + +如果我们的递归函数能够返回当前头元素对应的尾元素,并且将头元素和尾元素之间的链表按要求完成,那就变得简单了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/143_2.jpg) + +如上图,我们只需要将 `head` 指向 `tail`,`tail` 指向处理完的链表头即可。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/143_3.jpg) + +然后我们把之前的 `tail.next` 返回就是外层 `head` 对应的 `tail` 了。 + +递归出口的话,如果只有一个节点,那么我们只需要将 `head.next` 返回。 + +```java +if (len == 1) { + ListNode outTail = head.next; + head.next = null; + return outTail; +} +``` + +如果是两个节点,我们需要将 `head.next.next` 返回。 + +```java +if (len == 2) { + ListNode outTail = head.next.next; + head.next.next = null; + return outTail; +} +``` + +然后总体的代码就是下边的样子 + +```java +public void reorderList(ListNode head) { + + if (head == null || head.next == null || head.next.next == null) { + return; + } + int len = 0; + ListNode h = head; + //求出节点数 + while (h != null) { + len++; + h = h.next; + } + + reorderListHelper(head, len); +} + +private ListNode reorderListHelper(ListNode head, int len) { + if (len == 1) { + ListNode outTail = head.next; + head.next = null; + return outTail; + } + if (len == 2) { + ListNode outTail = head.next.next; + head.next.next = null; + return outTail; + } + //得到对应的尾节点,并且将头结点和尾节点之间的链表通过递归处理 + ListNode tail = reorderListHelper(head.next, len - 2); + ListNode subHead = head.next;//中间链表的头结点 + head.next = tail; + ListNode outTail = tail.next; //上一层 head 对应的 tail + tail.next = subHead; + return outTail; +} +``` + +# 解法三 + +参考 [这里](https://leetcode.com/problems/reorder-list/discuss/44992/Java-solution-with-3-steps),主要是利用到一头一尾取元素的特性。 + +主要是三步,举个例子。 + +```java +1 -> 2 -> 3 -> 4 -> 5 -> 6 +第一步,将链表平均分成两半 +1 -> 2 -> 3 +4 -> 5 -> 6 + +第二步,将第二个链表逆序 +1 -> 2 -> 3 +6 -> 5 -> 4 + +第三步,依次连接两个链表 +1 -> 6 -> 2 -> 5 -> 3 -> 4 +``` + +第一步找中点的话,可以应用 [19 题](https://leetcode.wang/leetCode-19-Remov-Nth-Node-From-End-of-List.html) 的方法,快慢指针。快指针一次走两步,慢指针一次走一步,当快指针走到终点的话,慢指针会刚好到中点。如果节点个数是偶数的话,`slow` 走到的是左端点,利用这一点,我们可以把奇数和偶数的情况合并,不需要分开考虑。 + +第二步链表逆序的话,在 [第 2 题](https://leetcode.wang/leetCode-2-Add-Two-Numbers.html) 讨论过了,有迭代和递归的两种方式,迭代的话主要利用两个指针,依次逆转。 + +第三步的话就很简单了,两个指针分别向后移动就可以。 + +```java +public void reorderList(ListNode head) { + if (head == null || head.next == null || head.next.next == null) { + return; + } + //找中点,链表分成两个 + ListNode slow = head; + ListNode fast = head; + while (fast.next != null && fast.next.next != null) { + slow = slow.next; + fast = fast.next.next; + } + + ListNode newHead = slow.next; + slow.next = null; + + //第二个链表倒置 + newHead = reverseList(newHead); + + //链表节点依次连接 + while (newHead != null) { + ListNode temp = newHead.next; + newHead.next = head.next; + head.next = newHead; + head = newHead.next; + newHead = temp; + } + +} + +private ListNode reverseList(ListNode head) { + if (head == null) { + return null; + } + ListNode tail = head; + head = head.next; + + tail.next = null; + + while (head != null) { + ListNode temp = head.next; + head.next = tail; + tail = head; + head = temp; + } + + return tail; +} +``` + +# 总 + +解法一利用空间去存储就很简单了,解法二递归的思想也很经典,自己也想了很久,看到作者的思路才恍然大悟,判断当前 `length` 定义递归出口很巧妙。解法三主要就是对题目的理解,关键就是利用一头一尾取元素的特性。 + diff --git a/leetcode-144-Binary-Tree-Preorder-Traversal.md b/leetcode-144-Binary-Tree-Preorder-Traversal.md index e1cacdd8a..9c586ab28 100644 --- a/leetcode-144-Binary-Tree-Preorder-Traversal.md +++ b/leetcode-144-Binary-Tree-Preorder-Traversal.md @@ -1,120 +1,120 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/144.jpg) - -二叉树的先序遍历。 - -# 思路分析 - -之前做过 [94 题](https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html) 的中序遍历,先序遍历的话代码可以直接拿过来用,只需要改一改 `list.add` 的位置。 - -# 解法一 递归 - -递归很好理解了,代码也是最简洁的。 - -```java -public List preorderTraversal(TreeNode root) { - List list = new ArrayList<>(); - preorderTraversalHelper(root, list); - return list; -} - -private void preorderTraversalHelper(TreeNode root, List list) { - if (root == null) { - return; - } - list.add(root.val); - preorderTraversalHelper(root.left, list); - preorderTraversalHelper(root.right, list); -} -``` - -# 解法二 栈 - -第一种思路就是利用栈去模拟上边的递归。 - -```java -public List preorderTraversal(TreeNode root) { - List list = new ArrayList<>(); - Stack stack = new Stack<>(); - TreeNode cur = root; - while (cur != null || !stack.isEmpty()) { - if (cur != null) { - list.add(cur.val); - stack.push(cur); - cur = cur.left; //考虑左子树 - }else { - //节点为空,就出栈 - cur = stack.pop(); - //考虑右子树 - cur = cur.right; - } - } - return list; -} -``` - -第二种思路的话,我们还可以将左右子树分别压栈,然后每次从栈里取元素。需要注意的是,因为我们应该先访问左子树,而栈的话是先进后出,所以我们压栈先压右子树。 - -```java -public List preorderTraversal(TreeNode root) { - List list = new ArrayList<>(); - if (root == null) { - return list; - } - Stack stack = new Stack<>(); - stack.push(root); - while (!stack.isEmpty()) { - TreeNode cur = stack.pop(); - if (cur == null) { - continue; - } - list.add(cur.val); - stack.push(cur.right); - stack.push(cur.left); - } - return list; -} -``` - -# 解法三 Morris Traversal - -上边的两种解法,空间复杂度都是 `O(n)`,利用 Morris Traversal 可以使得空间复杂度变为 `O(1)`。 - -它的主要思想就是利用叶子节点的左右子树是 `null` ,所以我们可以利用这个空间去存我们需要的节点,详细的可以参考 [94 题](https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html) 中序遍历。 - -```java -public List preorderTraversal(TreeNode root) { - List list = new ArrayList<>(); - TreeNode cur = root; - while (cur != null) { - //情况 1 - if (cur.left == null) { - list.add(cur.val); - cur = cur.right; - } else { - //找左子树最右边的节点 - TreeNode pre = cur.left; - while (pre.right != null && pre.right != cur) { - pre = pre.right; - } - //情况 2.1 - if (pre.right == null) { - list.add(cur.val); - pre.right = cur; - cur = cur.left; - } - //情况 2.2 - if (pre.right == cur) { - pre.right = null; //这里可以恢复为 null - cur = cur.right; - } - } - } - return list; -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/144.jpg) + +二叉树的先序遍历。 + +# 思路分析 + +之前做过 [94 题](https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html) 的中序遍历,先序遍历的话代码可以直接拿过来用,只需要改一改 `list.add` 的位置。 + +# 解法一 递归 + +递归很好理解了,代码也是最简洁的。 + +```java +public List preorderTraversal(TreeNode root) { + List list = new ArrayList<>(); + preorderTraversalHelper(root, list); + return list; +} + +private void preorderTraversalHelper(TreeNode root, List list) { + if (root == null) { + return; + } + list.add(root.val); + preorderTraversalHelper(root.left, list); + preorderTraversalHelper(root.right, list); +} +``` + +# 解法二 栈 + +第一种思路就是利用栈去模拟上边的递归。 + +```java +public List preorderTraversal(TreeNode root) { + List list = new ArrayList<>(); + Stack stack = new Stack<>(); + TreeNode cur = root; + while (cur != null || !stack.isEmpty()) { + if (cur != null) { + list.add(cur.val); + stack.push(cur); + cur = cur.left; //考虑左子树 + }else { + //节点为空,就出栈 + cur = stack.pop(); + //考虑右子树 + cur = cur.right; + } + } + return list; +} +``` + +第二种思路的话,我们还可以将左右子树分别压栈,然后每次从栈里取元素。需要注意的是,因为我们应该先访问左子树,而栈的话是先进后出,所以我们压栈先压右子树。 + +```java +public List preorderTraversal(TreeNode root) { + List list = new ArrayList<>(); + if (root == null) { + return list; + } + Stack stack = new Stack<>(); + stack.push(root); + while (!stack.isEmpty()) { + TreeNode cur = stack.pop(); + if (cur == null) { + continue; + } + list.add(cur.val); + stack.push(cur.right); + stack.push(cur.left); + } + return list; +} +``` + +# 解法三 Morris Traversal + +上边的两种解法,空间复杂度都是 `O(n)`,利用 Morris Traversal 可以使得空间复杂度变为 `O(1)`。 + +它的主要思想就是利用叶子节点的左右子树是 `null` ,所以我们可以利用这个空间去存我们需要的节点,详细的可以参考 [94 题](https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html) 中序遍历。 + +```java +public List preorderTraversal(TreeNode root) { + List list = new ArrayList<>(); + TreeNode cur = root; + while (cur != null) { + //情况 1 + if (cur.left == null) { + list.add(cur.val); + cur = cur.right; + } else { + //找左子树最右边的节点 + TreeNode pre = cur.left; + while (pre.right != null && pre.right != cur) { + pre = pre.right; + } + //情况 2.1 + if (pre.right == null) { + list.add(cur.val); + pre.right = cur; + cur = cur.left; + } + //情况 2.2 + if (pre.right == cur) { + pre.right = null; //这里可以恢复为 null + cur = cur.right; + } + } + } + return list; +} +``` + +# 总 + 和 [94 题](https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html) 没什么差别,解法三利用已有空间去存东西,从而降低空间复杂度的思想经常用到。 \ No newline at end of file diff --git a/leetcode-145-Binary-Tree-Postorder-Traversal.md b/leetcode-145-Binary-Tree-Postorder-Traversal.md index 1e5c7c5b1..46422d909 100644 --- a/leetcode-145-Binary-Tree-Postorder-Traversal.md +++ b/leetcode-145-Binary-Tree-Postorder-Traversal.md @@ -1,584 +1,584 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/145.jpg) - -二叉树的后序遍历,会用到之前 [94 题](https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html) 中序遍历和 [144 题](https://leetcode.wang/leetcode-144-Binary-Tree-Preorder-Traversal.html) 先序遍历的一些思想。 - -# 解法一 递归 - -和之前的中序遍历和先序遍历没什么大的改变,只需要改变一下 `list.add` 的位置。 - -```java -public List postorderTraversal(TreeNode root) { - List list = new ArrayList<>(); - postorderTraversalHelper(root, list); - return list; -} - -private void postorderTraversalHelper(TreeNode root, List list) { - if (root == null) { - return; - } - postorderTraversalHelper(root.left, list); - postorderTraversalHelper(root.right, list); - list.add(root.val); -} -``` - -# 解法二 栈 - -主要就是用栈要模拟递归的过程,区别于之前 [94 题](https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html) 中序遍历和 [144 题](https://leetcode.wang/leetcode-144-Binary-Tree-Preorder-Traversal.html) 先序遍历,后序遍历的非递归形式会相对难一些。 - -原因就是,当遍历完某个根节点的左子树,回到根节点的时候,对于中序遍历和先序遍历可以把当前根节点从栈里弹出,然后转到右子树。举个例子, - -```java - 1 - / \ - 2 3 - / \ - 4 5 -``` - -当遍历完 `2,4,5` 的时候,回到 `1` 之后我们就可以把 `1` 弹出,然后通过 `1` 到达右子树继续遍历。 - -而对于后序遍历,当我们到达 `1` 的时候并不能立刻把 `1` 弹出,因为遍历完右子树,我们还需要将这个根节点加入到 `list` 中。 - -所以我们就需要判断是从左子树到的根节点,还是右子树到的根节点。 - -如果是从左子树到的根节点,此时应该转到右子树。如果是从右子树到的根节点,那么就可以把当前节点弹出,并且加入到 `list` 中。 - -当然,如果是从左子树到的根节点,此时如果根节点的右子树为 `null`, 此时也可以把当前节点弹出,并且加入到 `list` 中。 - -基于上边的思想,可以写出一些不同的代码。 - -## 思想一 - -可以先看一下中序遍历的实现。 - -```java -public List inorderTraversal(TreeNode root) { - List ans = new ArrayList<>(); - Stack stack = new Stack<>(); - TreeNode cur = root; - while (cur != null || !stack.isEmpty()) { - //节点不为空一直压栈 - while (cur != null) { - stack.push(cur); - cur = cur.left; //考虑左子树 - } - //节点为空,就出栈 - cur = stack.pop(); - //当前值加入 - ans.add(cur.val); - //考虑右子树 - cur = cur.right; - } - return ans; -} -``` - -这里后序遍历的话,和中序遍历有些像。 - -开始的话,也是不停的往左子树走,然后直到为 `null` 。不同之处是,之前直接把节点 `pop` 并且加入到 `list` 中,然后直接转到右子树。 - -这里的话,我们应该把节点 `peek` 出来,然后判断一下当前根节点的右子树是否为空或者是否是从右子树回到的根节点。 - -判断是否是从右子树回到的根节点,这里我用了一个 `set` ,当从左子树到根节点的时候,我把根节点加入到 `set` 中,之后我们就可以判断当前节点在不在 `set` 中,如果在的话就表明当前是第二次回来,也就意味着是从右子树到的根节点。 - -```java -public List postorderTraversal(TreeNode root) { - List list = new ArrayList<>(); - Stack stack = new Stack<>(); - Set set = new HashSet<>(); - TreeNode cur = root; - while (cur != null || !stack.isEmpty()) { - while (cur != null && !set.contains(cur)) { - stack.push(cur); - cur = cur.left; - } - cur = stack.peek(); - //右子树为空或者第二次来到这里 - if (cur.right == null || set.contains(cur)) { - list.add(cur.val); - set.add(cur); - stack.pop();//将当前节点弹出 - if (stack.isEmpty()) { - return list; - } - //转到右子树,这种情况对应于右子树为空的情况 - cur = stack.peek(); - cur = cur.right; - //从左子树过来,加到 set 中,转到右子树 - } else { - set.add(cur); - cur = cur.right; - } - } - return list; -} -``` - -上边的代码把一些情况其实做了合并,并不是很好理解,下边分享一下 `solution` 里的一些简洁的解法。 - -## 思想二 - -上边的解法在判断当前是从左子树到的根节点还是右子树到的根节点用了 `set` ,[这里](https://leetcode.com/problems/binary-tree-postorder-traversal/discuss/45550/C%2B%2B-Iterative-Recursive-and-Morris-Traversal) 还有一个更直接的方法,通过记录上一次遍历的节点。 - -如果当前节点的右节点和上一次遍历的节点相同,那就表明当前是从右节点过来的了。 - -```java -public List postorderTraversal(TreeNode root) { - List list = new ArrayList<>(); - Stack stack = new Stack<>(); - TreeNode cur = root; - TreeNode last = null; - while (cur != null || !stack.isEmpty()) { - if (cur != null) { - stack.push(cur); - cur = cur.left; - } else { - TreeNode temp = stack.peek(); - //是否变到右子树 - if (temp.right != null && temp.right != last) { - cur = temp.right; - } else { - list.add(temp.val); - last = temp; - stack.pop(); - } - } - } - return list; -} -``` - -## 思想三 - -在 [这里](https://leetcode.com/problems/binary-tree-postorder-traversal/discuss/45582/A-real-Postorder-Traversal-.without-reverse-or-insert-4ms) 看到另一种想法,还是基于上边分析的入口点,不过解决方案真的是太优雅了。 - -先看一下 [144 题](https://leetcode.wang/leetcode-144-Binary-Tree-Preorder-Traversal.html) 前序遍历的代码。 - -> 我们还可以将左右子树分别压栈,然后每次从栈里取元素。需要注意的是,因为我们应该先访问左子树,而栈的话是先进后出,所以我们压栈先压右子树。 - -```java -public List preorderTraversal(TreeNode root) { - List list = new ArrayList<>(); - if (root == null) { - return list; - } - Stack stack = new Stack<>(); - stack.push(root); - while (!stack.isEmpty()) { - TreeNode cur = stack.pop(); - if (cur == null) { - continue; - } - list.add(cur.val); - stack.push(cur.right); - stack.push(cur.left); - } - return list; -} -``` - -后序遍历遇到的问题就是到根节点的时候不能直接 `pop` ,因为后边还需要回来。 - -上边的作者,提出只需要把每个节点 `push` 两次,然后判断当前 `pop` 节点和栈顶节点是否相同。 - -相同的话,就意味着是从左子树到的根节点。 - -不同的话,就意味着是从右子树到的根节点,此时就可以把节点加入到 `list` 中。 - -```java -public List postorderTraversal(TreeNode root) { - List list = new ArrayList<>(); - if (root == null) { - return list; - } - Stack stack = new Stack<>(); - stack.push(root); - stack.push(root); - while (!stack.isEmpty()) { - TreeNode cur = stack.pop(); - if (cur == null) { - continue; - } - if (!stack.isEmpty() && cur == stack.peek()) { - stack.push(cur.right); - stack.push(cur.right); - stack.push(cur.left); - stack.push(cur.left); - } else { - list.add(cur.val); - } - } - return list; -} -``` - -# 解法三 转换问题 - -首先我们知道前序遍历的非递归形式会比后序遍历好理解些,那么我们能实现`后序遍历 -> 前序遍历`的转换吗? - -后序遍历的顺序是 `左 -> 右 -> 根`。 - -前序遍历的顺序是 `根 -> 左 -> 右`,左右其实是等价的,所以我们也可以轻松的写出 `根 -> 右 -> 左` 的代码。 - -然后把 `根 -> 右 -> 左` 逆序,就是 `左 -> 右 -> 根`,也就是后序遍历了。 - -让我们改一下之前 [144 题](https://leetcode.wang/leetcode-144-Binary-Tree-Preorder-Traversal.html) 先序遍历的代码。 - -改之前的代码。 - -```java -public List preorderTraversal(TreeNode root) { - List list = new ArrayList<>(); - Stack stack = new Stack<>(); - TreeNode cur = root; - while (cur != null || !stack.isEmpty()) { - if (cur != null) { - list.add(cur.val); - stack.push(cur); - cur = cur.left; //考虑左子树 - }else { - //节点为空,就出栈 - cur = stack.pop(); - //考虑右子树 - cur = cur.right; - } - } - return list; -} -``` - -然后我们只需要把上边的 `left` 改成 `right`,`right` 改成 `left` 就可以了。最后倒置即可。 - -```java -public List postorderTraversal2(TreeNode root) { - List list = new ArrayList<>(); - Stack stack = new Stack<>(); - TreeNode cur = root; - while (cur != null || !stack.isEmpty()) { - if (cur != null) { - list.add(cur.val); - stack.push(cur); - cur = cur.right; // 考虑左子树 - } else { - // 节点为空,就出栈 - cur = stack.pop(); - // 考虑右子树 - cur = cur.left; - } - } - Collections.reverse(list); - return list; -} -``` - -同样的,之前先序遍历的 `Morris Traversal` ,不需要额外空间的解法。 - -```java -public List preorderTraversal(TreeNode root) { - List list = new ArrayList<>(); - TreeNode cur = root; - while (cur != null) { - //情况 1 - if (cur.left == null) { - list.add(cur.val); - cur = cur.right; - } else { - //找左子树最右边的节点 - TreeNode pre = cur.left; - while (pre.right != null && pre.right != cur) { - pre = pre.right; - } - //情况 2.1 - if (pre.right == null) { - list.add(cur.val); - pre.right = cur; - cur = cur.left; - } - //情况 2.2 - if (pre.right == cur) { - pre.right = null; //这里可以恢复为 null - cur = cur.right; - } - } - } - return list; -} - -``` - -同样的处理,把上边的 `left` 改成 `right`,`right` 改成 `left` 就可以了。最后倒置即可。 - -```java -public List postorderTraversal(TreeNode root) { - List list = new ArrayList<>(); - TreeNode cur = root; - while (cur != null) { - if (cur.right == null) { - list.add(cur.val); - cur = cur.left; - } else { - TreeNode pre = cur.right; - while (pre.left != null && pre.left != cur) { - pre = pre.left; - } - if (pre.left == null) { - list.add(cur.val); - pre.left = cur; - cur = cur.right; - } - if (pre.left == cur) { - pre.left = null; // 这里可以恢复为 null - cur = cur.left; - } - } - } - Collections.reverse(list); - - return list; -} -``` - -上边的话由于我们用的是 `ArrayList` ,所以倒置的话其实是比较麻烦的,可能需要更多的时间或空间。 - -所以我们可以用 `LinkedList` , 这样倒置链表就只需要遍历一遍,也不需要额外的空间了。 - -更近一步,我们在调用 `list.add` 的时候,其实可以直接 `list.addFirst` ,每次都插入到链表头,这样做的话,最后也不需要逆转链表了。 - -# 解法四 Morris Traversal - -上边已经成功改写 `Morris Traversal` 了,但是是一种取巧的方式,通过变形的前序遍历做的。同学介绍了另一种写法,这里也分享下。 - -[94 题](https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html) 中序遍历中对 `Morris` 遍历有详细的介绍,我先贴过来。 - -我们知道,左子树最后遍历的节点一定是一个叶子节点,它的左右孩子都是 `null`,我们把它右孩子指向当前根节点,这样的话我们就不需要额外空间了。这样做,遍历完当前左子树,就可以回到根节点了。 - -当然如果当前根节点左子树为空,那么我们只需要保存根节点的值,然后考虑右子树即可。 - -所以总体思想就是:记当前遍历的节点为 `cur`。 - -1、`cur.left` 为 `null`,保存 `cur` 的值,更新 `cur = cur.right` - -2、`cur.left` 不为 `null`,找到 `cur.left` 这颗子树最右边的节点记做 `last` - -**2.1** `last.right` 为 `null`,那么将 `last.right = cur`,更新 `cur = cur.left` - -**2.2** `last.right` 不为 `null`,说明之前已经访问过,第二次来到这里,表明当前子树遍历完成,保存 `cur` 的值,更新 `cur = cur.right` - -结合图示: - -![](https://windliang.oss-cn-beijing.aliyuncs.com/94_1.jpg) - -如上图,`cur` 指向根节点。 当前属于 `2.1` 的情况,`cur.left` 不为 `null`,`cur` 的左子树最右边的节点的右孩子为 `null`,那么我们把最右边的节点的右孩子指向 `cur`。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/94_2.jpg) - -接着,更新 `cur = cur.left`。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/94_3.jpg) - -如上图,当前属于 `2.1` 的情况,`cur.left` 不为 `null`,cur 的左子树最右边的节点的右孩子为 `null`,那么我们把最右边的节点的右孩子指向 `cur`。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/94_4.jpg) - -更新 `cur = cur.left`。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/94_5.jpg) - -如上图,当前属于情况 1,`cur.left` 为 `null`,保存 `cur` 的值,更新 `cur = cur.right`。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/94_6.jpg) - -如上图,当前属于 `2.2` 的情况,`cur.left` 不为 `null`,`cur` 的左子树最右边的节点的右孩子已经指向 `cur`,保存 `cur` 的值,更新 `cur = cur.right`。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/94_7.jpg) - -如上图,当前属于情况 1,`cur.left` 为 `null`,保存 `cur` 的值,更新 `cur = cur.right`。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/94_8.jpg) - -如上图,当前属于 `2.2` 的情况,`cur.left` 不为 `null`,`cur` 的左子树最右边的节点的右孩子已经指向 `cur`,保存 `cur` 的值,更新 `cur = cur.right`。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/94_9.jpg) - -当前属于情况 1,`cur.left` 为 `null`,保存 `cur` 的值,更新 `cur = cur.right`。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/94_10.jpg) - -`cur` 指向 `null`,结束遍历。 - -根据这个关系,写代码 - -记当前遍历的节点为 `cur`。 - -1、`cur.left` 为 `null`,保存 `cur` 的值,更新 `cur = cur.right` - -2、`cur.left` 不为 `null`,找到 `cur.left` 这颗子树最右边的节点记做 `last` - -**2.1** `last.right` 为 `null`,那么将 `last.right = cur`,更新 `cur = cur.left` - -**2.2** `last.right` 不为 `null`,说明之前已经访问过,第二次来到这里,表明当前子树遍历完成,保存 `cur` 的值,更新 `cur = cur.right` - -```java -public List inorderTraversal(TreeNode root) { - List ans = new ArrayList<>(); - TreeNode cur = root; - while (cur != null) { - //情况 1 - if (cur.left == null) { - ans.add(cur.val); - cur = cur.right; - } else { - //找左子树最右边的节点 - TreeNode pre = cur.left; - while (pre.right != null && pre.right != cur) { - pre = pre.right; - } - //情况 2.1 - if (pre.right == null) { - pre.right = cur; - cur = cur.left; - } - //情况 2.2 - if (pre.right == cur) { - pre.right = null; //这里可以恢复为 null - ans.add(cur.val); - cur = cur.right; - } - } - } - return ans; -} -``` - -根据上边的关系,我们会发现除了叶子节点只访问一次,其他节点都会访问两次,结合下图。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/145_2.jpg) - -当第二次访问某个节点的时候,我们只需要将它的左节点,以及左节点的右节点,左节点的右节点的右节点... 逆序添加到 `list` 中即可。比如上边的例子。 - -上边的遍历顺序其实就是按照深度优先的方式。 - -```java -先访问 15, 7, 3, 1 然后往回走 -3 第二次访问,将它的左节点逆序加入到 list 中 -list = [1] - -继续访问 2, 然后往回走 -7 第二次访问,将它的左节点,左节点的右节点逆序加入到 list 中 -list = [1 2 3] - -继续访问 6 4, 然后往回走 -6 第二次访问, 将它的左节点逆序加入到 list 中 -list = [1 2 3 4] - -继续访问 5, 然后往回走 -15 第二次访问, 将它的左节点, 左节点的右节点, 左节点的右节点的右节点逆序加入到 list 中 -list = [1 2 3 4 5 6 7] - -然后访问 14 10 8, 然后往回走 -10 第二次访问,将它的左节点逆序加入到 list 中 -list = [1 2 3 4 5 6 7 8] - -继续访问 9, 然后往回走 -14 第二次访问,将它的左节点,左节点的右节点逆序加入到 list 中 -list = [1 2 3 4 5 6 7 8 9 10] - -继续访问 13 11, 然后往回走 -13 第二次访问, 将它的左节点逆序加入到 list 中 -list = [1 2 3 4 5 6 7 8 9 10 11] - -继续遍历 12,结束遍历 - -然后单独把根节点,以及根节点的右节点,右节点的右节点,右节点的右节点的右节点逆序加入到 list 中 -list = [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15] - -得到 list 就刚好是后序遍历 -``` - -如下图,问题就转换成了 `9` 组单链表的逆序问题。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/145_3.jpg) - -当遇到第二次访问的节点,我们将单链表逆序,然后加入到 `list` 并且还原即可。单链表逆序已经在 [第 2 题](https://leetcode.wang/leetCode-2-Add-Two-Numbers.html) 讨论过了,直接拿过来用,只需要把 `node.next` 改为 `node.right` 。 - -```java -public List postorderTraversal(TreeNode root) { - List list = new ArrayList<>(); - TreeNode cur = root; - while (cur != null) { - // 情况 1 - if (cur.left == null) { - cur = cur.right; - } else { - // 找左子树最右边的节点 - TreeNode pre = cur.left; - while (pre.right != null && pre.right != cur) { - pre = pre.right; - } - // 情况 2.1 - if (pre.right == null) { - pre.right = cur; - cur = cur.left; - } - // 情况 2.2,第二次遍历节点 - if (pre.right == cur) { - pre.right = null; // 这里可以恢复为 null - //逆序 - TreeNode head = reversList(cur.left); - //加入到 list 中,并且把逆序还原 - reversList(head, list); - cur = cur.right; - } - } - } - TreeNode head = reversList(root); - reversList(head, list); - return list; -} - -private TreeNode reversList(TreeNode head) { - if (head == null) { - return null; - } - TreeNode tail = head; - head = head.right; - - tail.right = null; - - while (head != null) { - TreeNode temp = head.right; - head.right = tail; - tail = head; - head = temp; - } - - return tail; -} - -private TreeNode reversList(TreeNode head, List list) { - if (head == null) { - return null; - } - TreeNode tail = head; - head = head.right; - list.add(tail.val); - tail.right = null; - - while (head != null) { - TreeNode temp = head.right; - head.right = tail; - tail = head; - list.add(tail.val); - head = temp; - } - return tail; -} -``` - -# 总 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/145.jpg) + +二叉树的后序遍历,会用到之前 [94 题](https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html) 中序遍历和 [144 题](https://leetcode.wang/leetcode-144-Binary-Tree-Preorder-Traversal.html) 先序遍历的一些思想。 + +# 解法一 递归 + +和之前的中序遍历和先序遍历没什么大的改变,只需要改变一下 `list.add` 的位置。 + +```java +public List postorderTraversal(TreeNode root) { + List list = new ArrayList<>(); + postorderTraversalHelper(root, list); + return list; +} + +private void postorderTraversalHelper(TreeNode root, List list) { + if (root == null) { + return; + } + postorderTraversalHelper(root.left, list); + postorderTraversalHelper(root.right, list); + list.add(root.val); +} +``` + +# 解法二 栈 + +主要就是用栈要模拟递归的过程,区别于之前 [94 题](https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html) 中序遍历和 [144 题](https://leetcode.wang/leetcode-144-Binary-Tree-Preorder-Traversal.html) 先序遍历,后序遍历的非递归形式会相对难一些。 + +原因就是,当遍历完某个根节点的左子树,回到根节点的时候,对于中序遍历和先序遍历可以把当前根节点从栈里弹出,然后转到右子树。举个例子, + +```java + 1 + / \ + 2 3 + / \ + 4 5 +``` + +当遍历完 `2,4,5` 的时候,回到 `1` 之后我们就可以把 `1` 弹出,然后通过 `1` 到达右子树继续遍历。 + +而对于后序遍历,当我们到达 `1` 的时候并不能立刻把 `1` 弹出,因为遍历完右子树,我们还需要将这个根节点加入到 `list` 中。 + +所以我们就需要判断是从左子树到的根节点,还是右子树到的根节点。 + +如果是从左子树到的根节点,此时应该转到右子树。如果是从右子树到的根节点,那么就可以把当前节点弹出,并且加入到 `list` 中。 + +当然,如果是从左子树到的根节点,此时如果根节点的右子树为 `null`, 此时也可以把当前节点弹出,并且加入到 `list` 中。 + +基于上边的思想,可以写出一些不同的代码。 + +## 思想一 + +可以先看一下中序遍历的实现。 + +```java +public List inorderTraversal(TreeNode root) { + List ans = new ArrayList<>(); + Stack stack = new Stack<>(); + TreeNode cur = root; + while (cur != null || !stack.isEmpty()) { + //节点不为空一直压栈 + while (cur != null) { + stack.push(cur); + cur = cur.left; //考虑左子树 + } + //节点为空,就出栈 + cur = stack.pop(); + //当前值加入 + ans.add(cur.val); + //考虑右子树 + cur = cur.right; + } + return ans; +} +``` + +这里后序遍历的话,和中序遍历有些像。 + +开始的话,也是不停的往左子树走,然后直到为 `null` 。不同之处是,之前直接把节点 `pop` 并且加入到 `list` 中,然后直接转到右子树。 + +这里的话,我们应该把节点 `peek` 出来,然后判断一下当前根节点的右子树是否为空或者是否是从右子树回到的根节点。 + +判断是否是从右子树回到的根节点,这里我用了一个 `set` ,当从左子树到根节点的时候,我把根节点加入到 `set` 中,之后我们就可以判断当前节点在不在 `set` 中,如果在的话就表明当前是第二次回来,也就意味着是从右子树到的根节点。 + +```java +public List postorderTraversal(TreeNode root) { + List list = new ArrayList<>(); + Stack stack = new Stack<>(); + Set set = new HashSet<>(); + TreeNode cur = root; + while (cur != null || !stack.isEmpty()) { + while (cur != null && !set.contains(cur)) { + stack.push(cur); + cur = cur.left; + } + cur = stack.peek(); + //右子树为空或者第二次来到这里 + if (cur.right == null || set.contains(cur)) { + list.add(cur.val); + set.add(cur); + stack.pop();//将当前节点弹出 + if (stack.isEmpty()) { + return list; + } + //转到右子树,这种情况对应于右子树为空的情况 + cur = stack.peek(); + cur = cur.right; + //从左子树过来,加到 set 中,转到右子树 + } else { + set.add(cur); + cur = cur.right; + } + } + return list; +} +``` + +上边的代码把一些情况其实做了合并,并不是很好理解,下边分享一下 `solution` 里的一些简洁的解法。 + +## 思想二 + +上边的解法在判断当前是从左子树到的根节点还是右子树到的根节点用了 `set` ,[这里](https://leetcode.com/problems/binary-tree-postorder-traversal/discuss/45550/C%2B%2B-Iterative-Recursive-and-Morris-Traversal) 还有一个更直接的方法,通过记录上一次遍历的节点。 + +如果当前节点的右节点和上一次遍历的节点相同,那就表明当前是从右节点过来的了。 + +```java +public List postorderTraversal(TreeNode root) { + List list = new ArrayList<>(); + Stack stack = new Stack<>(); + TreeNode cur = root; + TreeNode last = null; + while (cur != null || !stack.isEmpty()) { + if (cur != null) { + stack.push(cur); + cur = cur.left; + } else { + TreeNode temp = stack.peek(); + //是否变到右子树 + if (temp.right != null && temp.right != last) { + cur = temp.right; + } else { + list.add(temp.val); + last = temp; + stack.pop(); + } + } + } + return list; +} +``` + +## 思想三 + +在 [这里](https://leetcode.com/problems/binary-tree-postorder-traversal/discuss/45582/A-real-Postorder-Traversal-.without-reverse-or-insert-4ms) 看到另一种想法,还是基于上边分析的入口点,不过解决方案真的是太优雅了。 + +先看一下 [144 题](https://leetcode.wang/leetcode-144-Binary-Tree-Preorder-Traversal.html) 前序遍历的代码。 + +> 我们还可以将左右子树分别压栈,然后每次从栈里取元素。需要注意的是,因为我们应该先访问左子树,而栈的话是先进后出,所以我们压栈先压右子树。 + +```java +public List preorderTraversal(TreeNode root) { + List list = new ArrayList<>(); + if (root == null) { + return list; + } + Stack stack = new Stack<>(); + stack.push(root); + while (!stack.isEmpty()) { + TreeNode cur = stack.pop(); + if (cur == null) { + continue; + } + list.add(cur.val); + stack.push(cur.right); + stack.push(cur.left); + } + return list; +} +``` + +后序遍历遇到的问题就是到根节点的时候不能直接 `pop` ,因为后边还需要回来。 + +上边的作者,提出只需要把每个节点 `push` 两次,然后判断当前 `pop` 节点和栈顶节点是否相同。 + +相同的话,就意味着是从左子树到的根节点。 + +不同的话,就意味着是从右子树到的根节点,此时就可以把节点加入到 `list` 中。 + +```java +public List postorderTraversal(TreeNode root) { + List list = new ArrayList<>(); + if (root == null) { + return list; + } + Stack stack = new Stack<>(); + stack.push(root); + stack.push(root); + while (!stack.isEmpty()) { + TreeNode cur = stack.pop(); + if (cur == null) { + continue; + } + if (!stack.isEmpty() && cur == stack.peek()) { + stack.push(cur.right); + stack.push(cur.right); + stack.push(cur.left); + stack.push(cur.left); + } else { + list.add(cur.val); + } + } + return list; +} +``` + +# 解法三 转换问题 + +首先我们知道前序遍历的非递归形式会比后序遍历好理解些,那么我们能实现`后序遍历 -> 前序遍历`的转换吗? + +后序遍历的顺序是 `左 -> 右 -> 根`。 + +前序遍历的顺序是 `根 -> 左 -> 右`,左右其实是等价的,所以我们也可以轻松的写出 `根 -> 右 -> 左` 的代码。 + +然后把 `根 -> 右 -> 左` 逆序,就是 `左 -> 右 -> 根`,也就是后序遍历了。 + +让我们改一下之前 [144 题](https://leetcode.wang/leetcode-144-Binary-Tree-Preorder-Traversal.html) 先序遍历的代码。 + +改之前的代码。 + +```java +public List preorderTraversal(TreeNode root) { + List list = new ArrayList<>(); + Stack stack = new Stack<>(); + TreeNode cur = root; + while (cur != null || !stack.isEmpty()) { + if (cur != null) { + list.add(cur.val); + stack.push(cur); + cur = cur.left; //考虑左子树 + }else { + //节点为空,就出栈 + cur = stack.pop(); + //考虑右子树 + cur = cur.right; + } + } + return list; +} +``` + +然后我们只需要把上边的 `left` 改成 `right`,`right` 改成 `left` 就可以了。最后倒置即可。 + +```java +public List postorderTraversal2(TreeNode root) { + List list = new ArrayList<>(); + Stack stack = new Stack<>(); + TreeNode cur = root; + while (cur != null || !stack.isEmpty()) { + if (cur != null) { + list.add(cur.val); + stack.push(cur); + cur = cur.right; // 考虑左子树 + } else { + // 节点为空,就出栈 + cur = stack.pop(); + // 考虑右子树 + cur = cur.left; + } + } + Collections.reverse(list); + return list; +} +``` + +同样的,之前先序遍历的 `Morris Traversal` ,不需要额外空间的解法。 + +```java +public List preorderTraversal(TreeNode root) { + List list = new ArrayList<>(); + TreeNode cur = root; + while (cur != null) { + //情况 1 + if (cur.left == null) { + list.add(cur.val); + cur = cur.right; + } else { + //找左子树最右边的节点 + TreeNode pre = cur.left; + while (pre.right != null && pre.right != cur) { + pre = pre.right; + } + //情况 2.1 + if (pre.right == null) { + list.add(cur.val); + pre.right = cur; + cur = cur.left; + } + //情况 2.2 + if (pre.right == cur) { + pre.right = null; //这里可以恢复为 null + cur = cur.right; + } + } + } + return list; +} + +``` + +同样的处理,把上边的 `left` 改成 `right`,`right` 改成 `left` 就可以了。最后倒置即可。 + +```java +public List postorderTraversal(TreeNode root) { + List list = new ArrayList<>(); + TreeNode cur = root; + while (cur != null) { + if (cur.right == null) { + list.add(cur.val); + cur = cur.left; + } else { + TreeNode pre = cur.right; + while (pre.left != null && pre.left != cur) { + pre = pre.left; + } + if (pre.left == null) { + list.add(cur.val); + pre.left = cur; + cur = cur.right; + } + if (pre.left == cur) { + pre.left = null; // 这里可以恢复为 null + cur = cur.left; + } + } + } + Collections.reverse(list); + + return list; +} +``` + +上边的话由于我们用的是 `ArrayList` ,所以倒置的话其实是比较麻烦的,可能需要更多的时间或空间。 + +所以我们可以用 `LinkedList` , 这样倒置链表就只需要遍历一遍,也不需要额外的空间了。 + +更近一步,我们在调用 `list.add` 的时候,其实可以直接 `list.addFirst` ,每次都插入到链表头,这样做的话,最后也不需要逆转链表了。 + +# 解法四 Morris Traversal + +上边已经成功改写 `Morris Traversal` 了,但是是一种取巧的方式,通过变形的前序遍历做的。同学介绍了另一种写法,这里也分享下。 + +[94 题](https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html) 中序遍历中对 `Morris` 遍历有详细的介绍,我先贴过来。 + +我们知道,左子树最后遍历的节点一定是一个叶子节点,它的左右孩子都是 `null`,我们把它右孩子指向当前根节点,这样的话我们就不需要额外空间了。这样做,遍历完当前左子树,就可以回到根节点了。 + +当然如果当前根节点左子树为空,那么我们只需要保存根节点的值,然后考虑右子树即可。 + +所以总体思想就是:记当前遍历的节点为 `cur`。 + +1、`cur.left` 为 `null`,保存 `cur` 的值,更新 `cur = cur.right` + +2、`cur.left` 不为 `null`,找到 `cur.left` 这颗子树最右边的节点记做 `last` + +**2.1** `last.right` 为 `null`,那么将 `last.right = cur`,更新 `cur = cur.left` + +**2.2** `last.right` 不为 `null`,说明之前已经访问过,第二次来到这里,表明当前子树遍历完成,保存 `cur` 的值,更新 `cur = cur.right` + +结合图示: + +![](https://windliang.oss-cn-beijing.aliyuncs.com/94_1.jpg) + +如上图,`cur` 指向根节点。 当前属于 `2.1` 的情况,`cur.left` 不为 `null`,`cur` 的左子树最右边的节点的右孩子为 `null`,那么我们把最右边的节点的右孩子指向 `cur`。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/94_2.jpg) + +接着,更新 `cur = cur.left`。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/94_3.jpg) + +如上图,当前属于 `2.1` 的情况,`cur.left` 不为 `null`,cur 的左子树最右边的节点的右孩子为 `null`,那么我们把最右边的节点的右孩子指向 `cur`。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/94_4.jpg) + +更新 `cur = cur.left`。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/94_5.jpg) + +如上图,当前属于情况 1,`cur.left` 为 `null`,保存 `cur` 的值,更新 `cur = cur.right`。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/94_6.jpg) + +如上图,当前属于 `2.2` 的情况,`cur.left` 不为 `null`,`cur` 的左子树最右边的节点的右孩子已经指向 `cur`,保存 `cur` 的值,更新 `cur = cur.right`。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/94_7.jpg) + +如上图,当前属于情况 1,`cur.left` 为 `null`,保存 `cur` 的值,更新 `cur = cur.right`。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/94_8.jpg) + +如上图,当前属于 `2.2` 的情况,`cur.left` 不为 `null`,`cur` 的左子树最右边的节点的右孩子已经指向 `cur`,保存 `cur` 的值,更新 `cur = cur.right`。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/94_9.jpg) + +当前属于情况 1,`cur.left` 为 `null`,保存 `cur` 的值,更新 `cur = cur.right`。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/94_10.jpg) + +`cur` 指向 `null`,结束遍历。 + +根据这个关系,写代码 + +记当前遍历的节点为 `cur`。 + +1、`cur.left` 为 `null`,保存 `cur` 的值,更新 `cur = cur.right` + +2、`cur.left` 不为 `null`,找到 `cur.left` 这颗子树最右边的节点记做 `last` + +**2.1** `last.right` 为 `null`,那么将 `last.right = cur`,更新 `cur = cur.left` + +**2.2** `last.right` 不为 `null`,说明之前已经访问过,第二次来到这里,表明当前子树遍历完成,保存 `cur` 的值,更新 `cur = cur.right` + +```java +public List inorderTraversal(TreeNode root) { + List ans = new ArrayList<>(); + TreeNode cur = root; + while (cur != null) { + //情况 1 + if (cur.left == null) { + ans.add(cur.val); + cur = cur.right; + } else { + //找左子树最右边的节点 + TreeNode pre = cur.left; + while (pre.right != null && pre.right != cur) { + pre = pre.right; + } + //情况 2.1 + if (pre.right == null) { + pre.right = cur; + cur = cur.left; + } + //情况 2.2 + if (pre.right == cur) { + pre.right = null; //这里可以恢复为 null + ans.add(cur.val); + cur = cur.right; + } + } + } + return ans; +} +``` + +根据上边的关系,我们会发现除了叶子节点只访问一次,其他节点都会访问两次,结合下图。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/145_2.jpg) + +当第二次访问某个节点的时候,我们只需要将它的左节点,以及左节点的右节点,左节点的右节点的右节点... 逆序添加到 `list` 中即可。比如上边的例子。 + +上边的遍历顺序其实就是按照深度优先的方式。 + +```java +先访问 15, 7, 3, 1 然后往回走 +3 第二次访问,将它的左节点逆序加入到 list 中 +list = [1] + +继续访问 2, 然后往回走 +7 第二次访问,将它的左节点,左节点的右节点逆序加入到 list 中 +list = [1 2 3] + +继续访问 6 4, 然后往回走 +6 第二次访问, 将它的左节点逆序加入到 list 中 +list = [1 2 3 4] + +继续访问 5, 然后往回走 +15 第二次访问, 将它的左节点, 左节点的右节点, 左节点的右节点的右节点逆序加入到 list 中 +list = [1 2 3 4 5 6 7] + +然后访问 14 10 8, 然后往回走 +10 第二次访问,将它的左节点逆序加入到 list 中 +list = [1 2 3 4 5 6 7 8] + +继续访问 9, 然后往回走 +14 第二次访问,将它的左节点,左节点的右节点逆序加入到 list 中 +list = [1 2 3 4 5 6 7 8 9 10] + +继续访问 13 11, 然后往回走 +13 第二次访问, 将它的左节点逆序加入到 list 中 +list = [1 2 3 4 5 6 7 8 9 10 11] + +继续遍历 12,结束遍历 + +然后单独把根节点,以及根节点的右节点,右节点的右节点,右节点的右节点的右节点逆序加入到 list 中 +list = [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15] + +得到 list 就刚好是后序遍历 +``` + +如下图,问题就转换成了 `9` 组单链表的逆序问题。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/145_3.jpg) + +当遇到第二次访问的节点,我们将单链表逆序,然后加入到 `list` 并且还原即可。单链表逆序已经在 [第 2 题](https://leetcode.wang/leetCode-2-Add-Two-Numbers.html) 讨论过了,直接拿过来用,只需要把 `node.next` 改为 `node.right` 。 + +```java +public List postorderTraversal(TreeNode root) { + List list = new ArrayList<>(); + TreeNode cur = root; + while (cur != null) { + // 情况 1 + if (cur.left == null) { + cur = cur.right; + } else { + // 找左子树最右边的节点 + TreeNode pre = cur.left; + while (pre.right != null && pre.right != cur) { + pre = pre.right; + } + // 情况 2.1 + if (pre.right == null) { + pre.right = cur; + cur = cur.left; + } + // 情况 2.2,第二次遍历节点 + if (pre.right == cur) { + pre.right = null; // 这里可以恢复为 null + //逆序 + TreeNode head = reversList(cur.left); + //加入到 list 中,并且把逆序还原 + reversList(head, list); + cur = cur.right; + } + } + } + TreeNode head = reversList(root); + reversList(head, list); + return list; +} + +private TreeNode reversList(TreeNode head) { + if (head == null) { + return null; + } + TreeNode tail = head; + head = head.right; + + tail.right = null; + + while (head != null) { + TreeNode temp = head.right; + head.right = tail; + tail = head; + head = temp; + } + + return tail; +} + +private TreeNode reversList(TreeNode head, List list) { + if (head == null) { + return null; + } + TreeNode tail = head; + head = head.right; + list.add(tail.val); + tail.right = null; + + while (head != null) { + TreeNode temp = head.right; + head.right = tail; + tail = head; + list.add(tail.val); + head = temp; + } + return tail; +} +``` + +# 总 + 当初学后序遍历的时候,就觉得用递归就好了,简洁而优雅,对非递归的解法一直也没有深入总结。没想到一总结竟然这么多东西。和上一个节点比较,重复加入,转换问题的思想,单链表的逆序都也是比较经典的。 \ No newline at end of file diff --git a/leetcode-146-LRU-Cache.md b/leetcode-146-LRU-Cache.md index f7328dfbd..09fdee34e 100644 --- a/leetcode-146-LRU-Cache.md +++ b/leetcode-146-LRU-Cache.md @@ -1,235 +1,235 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/146.jpg) - -`LRU` 缓存。存储空间有限,当存满的时候的一种淘汰策略。`LRU` 选择删除最远一次操作过的元素,操作包括`get` 或者 `put` 。换句话讲,最开始 `put` 进去的如果没有进行过 `get`,那存满的时候就先删它。 - -# 思路分析 - -看到 `O(1)` 的时间复杂度首先想到的就是用 `HashMap` 去存储。 - -之后的问题就是怎么实现,当存满的时候删除最远一次操作过的元素。 - -可以用一个链表,每 `put` 一个 元素,就把它加到链表尾部。如果 `get` 某个元素,就把这个元素移动到链表尾部。当存满的时候,就把链表头的元素删除。 - -接下来还有一个问题就是,移动某个元素的时候,我们可以通过 `HashMap` 直接得到这个元素,但对于链表,如果想移动一个元素,肯定需要知道它的前一个节点才能操作。 - -而找到前一个元素,最直接的方法就是遍历一遍,但是这就使得算法的时间复杂度就不再是 `O(1)` 了。 - -另一种思路,就是使用双向链表,这样就可以直接得到它的前一个元素,从而实现移动操作。 - -综上,`HashMap` 加上双向链表即可解这道题了。 - -# 解法一 - -有了上边的思路,接下来就是实现上的细节了,最后的参考图如下。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/146_2.jpg) - -首先定义节点。 - -```java -class MyNode { - Object key; - Object value; - MyNode prev = null; - MyNode next = null; - MyNode(Object k, Object v) { - key = k; - value = v; - } -} -``` - -定义双向链表类。 - -这里用了一个 `dummyHead` ,也就是哨兵节点,不存数据,可以把链表头结点等效于其他的节点,从而简化一些操作。 - -```java -class DoubleLinkedList { - private MyNode dummyHead = new MyNode(null, null); // 头节点 - private MyNode tail = dummyHead; - //添加节点到末尾 - public void add(MyNode myNode) { - tail.next = myNode; - myNode.prev = tail; - tail = myNode; - } - - //得到头结点 - public MyNode getHead() { - return dummyHead.next; - } - - //移除当前节点 - public void removeMyNode(MyNode myNode) { - myNode.prev.next = myNode.next; - //判断删除的是否是尾节点 - if (myNode.next != null) { - myNode.next.prev = myNode.prev; - } else { - tail = myNode.prev; - } - //全部指向 null - myNode.prev = null; - myNode.next = null; - } - - //移动当前节点到末尾 - public void moveToTail(MyNode myNode) { - removeMyNode(myNode); - add(myNode); - } -} -``` - -接下来就是我们的 `LRU` 类。 - -```java -public class LRUCache { - private int capacity = 0; - private HashMap map = new HashMap<>(); - private DoubleLinkedList list = new DoubleLinkedList(); - - public LRUCache(int capacity) { - this.capacity = capacity; - } - - //get 的同时要把当前节点移动到末尾 - public int get(int key) { - if (map.containsKey(key)) { - MyNode myNode = map.get(key); - list.moveToTail(myNode); - return (int) myNode.value; - } else { - return -1; - } - } - - //对于之前存在的节点单独考虑 - public void put(int key, int value) { - if (map.containsKey(key)) { - MyNode myNode = map.get(key); - myNode.value = value; - list.moveToTail(myNode); - } else { - //判断是否存满 - if (map.size() == capacity) { - //从 map 和 list 中都删除头结点 - MyNode head = list.getHead(); - map.remove((int) head.key); - list.removeMyNode(head); - //插入当前元素 - MyNode myNode = new MyNode(key, value); - list.add(myNode); - map.put(key, myNode); - } else { - MyNode myNode = new MyNode(key, value); - list.add(myNode); - map.put(key, myNode); - } - } - } -} -``` - -接下来把上边的代码放在一起就可以了。 - -```java -class MyNode { - Object key; - Object value; - MyNode prev = null; - MyNode next = null; - MyNode(Object k, Object v) { - key = k; - value = v; - } -} - -class DoubleLinkedList { - private MyNode dummyHead = new MyNode(null, null); // 头节点 - private MyNode tail = dummyHead; - //添加节点到末尾 - public void add(MyNode myNode) { - tail.next = myNode; - myNode.prev = tail; - tail = myNode; - } - - //得到头结点 - public MyNode getHead() { - return dummyHead.next; - } - - //移除当前节点 - public void removeMyNode(MyNode myNode) { - myNode.prev.next = myNode.next; - //判断删除的是否是尾节点 - if (myNode.next != null) { - myNode.next.prev = myNode.prev; - } else { - tail = myNode.prev; - } - //全部指向 null - myNode.prev = null; - myNode.next = null; - } - - //移动当前节点到末尾 - public void moveToTail(MyNode myNode) { - removeMyNode(myNode); - add(myNode); - } -} - -public class LRUCache { - private int capacity = 0; - private HashMap map = new HashMap<>(); - private DoubleLinkedList list = new DoubleLinkedList(); - - public LRUCache(int capacity) { - this.capacity = capacity; - } - - //get 的同时要把当前节点移动到末尾 - public int get(int key) { - if (map.containsKey(key)) { - MyNode myNode = map.get(key); - list.moveToTail(myNode); - return (int) myNode.value; - } else { - return -1; - } - } - - //对于之前存在的节点单独考虑 - public void put(int key, int value) { - if (map.containsKey(key)) { - MyNode myNode = map.get(key); - myNode.value = value; - list.moveToTail(myNode); - } else { - //判断是否存满 - if (map.size() == capacity) { - //从 map 和 list 中都删除头结点 - MyNode head = list.getHead(); - map.remove((int) head.key); - list.removeMyNode(head); - //插入当前元素 - MyNode myNode = new MyNode(key, value); - list.add(myNode); - map.put(key, myNode); - } else { - MyNode myNode = new MyNode(key, value); - list.add(myNode); - map.put(key, myNode); - } - } - } -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/146.jpg) + +`LRU` 缓存。存储空间有限,当存满的时候的一种淘汰策略。`LRU` 选择删除最远一次操作过的元素,操作包括`get` 或者 `put` 。换句话讲,最开始 `put` 进去的如果没有进行过 `get`,那存满的时候就先删它。 + +# 思路分析 + +看到 `O(1)` 的时间复杂度首先想到的就是用 `HashMap` 去存储。 + +之后的问题就是怎么实现,当存满的时候删除最远一次操作过的元素。 + +可以用一个链表,每 `put` 一个 元素,就把它加到链表尾部。如果 `get` 某个元素,就把这个元素移动到链表尾部。当存满的时候,就把链表头的元素删除。 + +接下来还有一个问题就是,移动某个元素的时候,我们可以通过 `HashMap` 直接得到这个元素,但对于链表,如果想移动一个元素,肯定需要知道它的前一个节点才能操作。 + +而找到前一个元素,最直接的方法就是遍历一遍,但是这就使得算法的时间复杂度就不再是 `O(1)` 了。 + +另一种思路,就是使用双向链表,这样就可以直接得到它的前一个元素,从而实现移动操作。 + +综上,`HashMap` 加上双向链表即可解这道题了。 + +# 解法一 + +有了上边的思路,接下来就是实现上的细节了,最后的参考图如下。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/146_2.jpg) + +首先定义节点。 + +```java +class MyNode { + Object key; + Object value; + MyNode prev = null; + MyNode next = null; + MyNode(Object k, Object v) { + key = k; + value = v; + } +} +``` + +定义双向链表类。 + +这里用了一个 `dummyHead` ,也就是哨兵节点,不存数据,可以把链表头结点等效于其他的节点,从而简化一些操作。 + +```java +class DoubleLinkedList { + private MyNode dummyHead = new MyNode(null, null); // 头节点 + private MyNode tail = dummyHead; + //添加节点到末尾 + public void add(MyNode myNode) { + tail.next = myNode; + myNode.prev = tail; + tail = myNode; + } + + //得到头结点 + public MyNode getHead() { + return dummyHead.next; + } + + //移除当前节点 + public void removeMyNode(MyNode myNode) { + myNode.prev.next = myNode.next; + //判断删除的是否是尾节点 + if (myNode.next != null) { + myNode.next.prev = myNode.prev; + } else { + tail = myNode.prev; + } + //全部指向 null + myNode.prev = null; + myNode.next = null; + } + + //移动当前节点到末尾 + public void moveToTail(MyNode myNode) { + removeMyNode(myNode); + add(myNode); + } +} +``` + +接下来就是我们的 `LRU` 类。 + +```java +public class LRUCache { + private int capacity = 0; + private HashMap map = new HashMap<>(); + private DoubleLinkedList list = new DoubleLinkedList(); + + public LRUCache(int capacity) { + this.capacity = capacity; + } + + //get 的同时要把当前节点移动到末尾 + public int get(int key) { + if (map.containsKey(key)) { + MyNode myNode = map.get(key); + list.moveToTail(myNode); + return (int) myNode.value; + } else { + return -1; + } + } + + //对于之前存在的节点单独考虑 + public void put(int key, int value) { + if (map.containsKey(key)) { + MyNode myNode = map.get(key); + myNode.value = value; + list.moveToTail(myNode); + } else { + //判断是否存满 + if (map.size() == capacity) { + //从 map 和 list 中都删除头结点 + MyNode head = list.getHead(); + map.remove((int) head.key); + list.removeMyNode(head); + //插入当前元素 + MyNode myNode = new MyNode(key, value); + list.add(myNode); + map.put(key, myNode); + } else { + MyNode myNode = new MyNode(key, value); + list.add(myNode); + map.put(key, myNode); + } + } + } +} +``` + +接下来把上边的代码放在一起就可以了。 + +```java +class MyNode { + Object key; + Object value; + MyNode prev = null; + MyNode next = null; + MyNode(Object k, Object v) { + key = k; + value = v; + } +} + +class DoubleLinkedList { + private MyNode dummyHead = new MyNode(null, null); // 头节点 + private MyNode tail = dummyHead; + //添加节点到末尾 + public void add(MyNode myNode) { + tail.next = myNode; + myNode.prev = tail; + tail = myNode; + } + + //得到头结点 + public MyNode getHead() { + return dummyHead.next; + } + + //移除当前节点 + public void removeMyNode(MyNode myNode) { + myNode.prev.next = myNode.next; + //判断删除的是否是尾节点 + if (myNode.next != null) { + myNode.next.prev = myNode.prev; + } else { + tail = myNode.prev; + } + //全部指向 null + myNode.prev = null; + myNode.next = null; + } + + //移动当前节点到末尾 + public void moveToTail(MyNode myNode) { + removeMyNode(myNode); + add(myNode); + } +} + +public class LRUCache { + private int capacity = 0; + private HashMap map = new HashMap<>(); + private DoubleLinkedList list = new DoubleLinkedList(); + + public LRUCache(int capacity) { + this.capacity = capacity; + } + + //get 的同时要把当前节点移动到末尾 + public int get(int key) { + if (map.containsKey(key)) { + MyNode myNode = map.get(key); + list.moveToTail(myNode); + return (int) myNode.value; + } else { + return -1; + } + } + + //对于之前存在的节点单独考虑 + public void put(int key, int value) { + if (map.containsKey(key)) { + MyNode myNode = map.get(key); + myNode.value = value; + list.moveToTail(myNode); + } else { + //判断是否存满 + if (map.size() == capacity) { + //从 map 和 list 中都删除头结点 + MyNode head = list.getHead(); + map.remove((int) head.key); + list.removeMyNode(head); + //插入当前元素 + MyNode myNode = new MyNode(key, value); + list.add(myNode); + map.put(key, myNode); + } else { + MyNode myNode = new MyNode(key, value); + list.add(myNode); + map.put(key, myNode); + } + } + } +} +``` + +# 总 + 最关键的其实就是双向链表的应用了,想到这个点其他的话就水到渠成了。 \ No newline at end of file diff --git a/leetcode-147-Insertion-Sort-List.md b/leetcode-147-Insertion-Sort-List.md index c7db4cdb0..ed8bbc78b 100644 --- a/leetcode-147-Insertion-Sort-List.md +++ b/leetcode-147-Insertion-Sort-List.md @@ -1,98 +1,98 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/147.jpg) - -实现基于链表的插入排序。 - -# 解法一 - -所谓的插入排序,就是一次拿一个数把它插入到正确的位置。 - -举个例子。 - -```java -4 2 1 3 -res = [] - -拿出 4 -res = [4] - -拿出 2 -res = [2 4] - -拿出 1 -res = [1 2 4] - -拿出 3 -res = [1 2 3 4] -``` - -用代码的实现的话,因为要拿出一个要插入到已经排好序的链表中,首先肯定是依次遍历链表,找到第一个比要插入元素大的位置,把它插到前边。 - -至于插入的话,我们需要知道插入位置的前一个节点,所以我们可以用 `node.next` 和要插入的节点比较,`node` 就是插入位置的前一个节点了。 - -而 `head` 指针已经是最前了,所以我们可以用一个 `dummy` 指针,来将头指针的情况统一。 - -```java -public ListNode insertionSortList(ListNode head) { - if (head == null) { - return null; - } - ListNode dummy = new ListNode(0); - //拿出的节点 - while (head != null) { - ListNode tempH = dummy; - ListNode headNext = head.next; - head.next = null; - while (tempH.next != null) { - //找到大于要插入的节点的位置 - if (tempH.next.val > head.val) { - head.next = tempH.next; - tempH.next = head; - break; - } - tempH = tempH.next; - } - //没有执行插入,将当前节点加到末尾 - if (tempH.next == null) { - tempH.next = head; - } - head = headNext; - } - return dummy.next; -} -``` - -[这里](https://leetcode.com/problems/insertion-sort-list/discuss/46420/An-easy-and-clear-way-to-sort-(-O(1)-space-) 还有另一种写法,分享一下。 - -```java -public ListNode insertionSortList(ListNode head) { - if( head == null ){ - return head; - } - - ListNode helper = new ListNode(0); //new starter of the sorted list - ListNode cur = head; //the node will be inserted - ListNode pre = helper; //insert node between pre and pre.next - ListNode next = null; //the next node will be inserted - //not the end of input list - while( cur != null ){ - next = cur.next; - //find the right place to insert - while( pre.next != null && pre.next.val < cur.val ){ - pre = pre.next; - } - //insert between pre and pre.next - cur.next = pre.next; - pre.next = cur; - pre = helper; - cur = next; - } - - return helper.next; -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/147.jpg) + +实现基于链表的插入排序。 + +# 解法一 + +所谓的插入排序,就是一次拿一个数把它插入到正确的位置。 + +举个例子。 + +```java +4 2 1 3 +res = [] + +拿出 4 +res = [4] + +拿出 2 +res = [2 4] + +拿出 1 +res = [1 2 4] + +拿出 3 +res = [1 2 3 4] +``` + +用代码的实现的话,因为要拿出一个要插入到已经排好序的链表中,首先肯定是依次遍历链表,找到第一个比要插入元素大的位置,把它插到前边。 + +至于插入的话,我们需要知道插入位置的前一个节点,所以我们可以用 `node.next` 和要插入的节点比较,`node` 就是插入位置的前一个节点了。 + +而 `head` 指针已经是最前了,所以我们可以用一个 `dummy` 指针,来将头指针的情况统一。 + +```java +public ListNode insertionSortList(ListNode head) { + if (head == null) { + return null; + } + ListNode dummy = new ListNode(0); + //拿出的节点 + while (head != null) { + ListNode tempH = dummy; + ListNode headNext = head.next; + head.next = null; + while (tempH.next != null) { + //找到大于要插入的节点的位置 + if (tempH.next.val > head.val) { + head.next = tempH.next; + tempH.next = head; + break; + } + tempH = tempH.next; + } + //没有执行插入,将当前节点加到末尾 + if (tempH.next == null) { + tempH.next = head; + } + head = headNext; + } + return dummy.next; +} +``` + +[这里](https://leetcode.com/problems/insertion-sort-list/discuss/46420/An-easy-and-clear-way-to-sort-(-O(1)-space-) 还有另一种写法,分享一下。 + +```java +public ListNode insertionSortList(ListNode head) { + if( head == null ){ + return head; + } + + ListNode helper = new ListNode(0); //new starter of the sorted list + ListNode cur = head; //the node will be inserted + ListNode pre = helper; //insert node between pre and pre.next + ListNode next = null; //the next node will be inserted + //not the end of input list + while( cur != null ){ + next = cur.next; + //find the right place to insert + while( pre.next != null && pre.next.val < cur.val ){ + pre = pre.next; + } + //insert between pre and pre.next + cur.next = pre.next; + pre.next = cur; + pre = helper; + cur = next; + } + + return helper.next; +} +``` + +# 总 + 基本就是按照插入排序的定义来了,注意的就是链表节点的指向了,还有就是 `dummy` 节点的应用,就不需要单独处理头结点了。 \ No newline at end of file diff --git a/leetcode-148-Sort-List.md b/leetcode-148-Sort-List.md index cc8249e72..3295590a7 100644 --- a/leetcode-148-Sort-List.md +++ b/leetcode-148-Sort-List.md @@ -1,113 +1,113 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/148.jpg) - -要求时间复杂度为 `O(nlogn)`,最常用的就是归并排序了。 - -# 解法一 - -归并排序需要一个辅助方法,也就是对两个有序链表进行合并,在 [21 题](https://leetcode.wang/leetCode-21-Merge-Two-Sorted-Lists.html) 已经讨论过。 - -至于归并排序的思想,这里就不多讲了,本科的时候用 `Scratch` 做过一个演示视频,感兴趣的可以参考 [这里](https://zhuanlan.zhihu.com/p/71647786),哈哈。 - -那就直接放代码了。因为归并排序是一半一半的进行,所以需要找到中点。最常用的方法就是快慢指针去找中点了。 - -```java -ListNode dummy = new ListNode(0); -dummy.next = head; -ListNode fast = dummy; -ListNode slow = dummy; -while (fast != null && fast.next != null) { - slow = slow.next; - fast = fast.next.next; -} -``` - -上边的代码我加了一个 `dummy` 指针,就是想当节点个数是偶数的时候,让 `slow` 刚好指向前边一半节点的最后一个节点,也就是下边的状态。 - -```java -1 2 3 4 - ^ ^ - slow fast -``` - -如果 `slow` 和 `fast` 都从 `head` 开始走,那么当 `fast` 结束的时候,`slow` 就会走到后边一半节点的开头了。 - -当然除了上边的方法,在 [这里](https://leetcode.com/problems/sort-list/discuss/46714/Java-merge-sort-solution) 看到,还可以加一个 `pre` 指针,让它一直指向 `slow` 的前一个即可。 - -```java -// step 1. cut the list to two halves -ListNode prev = null, slow = head, fast = head; - -while (fast != null && fast.next != null) { - prev = slow; - slow = slow.next; - fast = fast.next.next; -} -``` - -他们的目的都是一样的,就是为了方便的把两个链表平均分开。 - -```java -public ListNode sortList(ListNode head) { - return mergeSort(head); -} - -private ListNode mergeSort(ListNode head) { - if (head == null || head.next == null) { - return head; - } - ListNode dummy = new ListNode(0); - dummy.next = head; - ListNode fast = dummy; - ListNode slow = dummy; - //快慢指针找中点 - while (fast != null && fast.next != null) { - slow = slow.next; - fast = fast.next.next; - } - - ListNode head2 = slow.next; - slow.next = null; - head = mergeSort(head); - head2 = mergeSort(head2); - return merge(head, head2); - -} - -private ListNode merge(ListNode head1, ListNode head2) { - ListNode dummy = new ListNode(0); - ListNode tail = dummy; - while (head1 != null && head2 != null) { - if (head1.val < head2.val) { - tail.next = head1; - tail = tail.next; - head1 = head1.next; - } else { - tail.next = head2; - tail = tail.next; - head2 = head2.next; - } - - } - if (head1 != null) { - tail.next = head1; - } - - if (head2 != null) { - tail.next = head2; - } - - return dummy.next; - -} - -``` - -当然严格的说,上边的解法空间复杂度并不是 `O(1)`,因为递归过程中压栈是需要消耗空间的,每次取一半,所以空间复杂度是 `O(log(n))`。 - -递归可以去改写成迭代的形式,也就是自底向上的走,就可以省去压栈的空间,空间复杂度从而达到 `O(1)`,详细的可以参考 [这里](https://leetcode.com/problems/sort-list/discuss/46712/Bottom-to-up(not-recurring)-with-o(1)-space-complextity-and-o(nlgn)-time-complextity) 。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/148.jpg) + +要求时间复杂度为 `O(nlogn)`,最常用的就是归并排序了。 + +# 解法一 + +归并排序需要一个辅助方法,也就是对两个有序链表进行合并,在 [21 题](https://leetcode.wang/leetCode-21-Merge-Two-Sorted-Lists.html) 已经讨论过。 + +至于归并排序的思想,这里就不多讲了,本科的时候用 `Scratch` 做过一个演示视频,感兴趣的可以参考 [这里](https://zhuanlan.zhihu.com/p/71647786),哈哈。 + +那就直接放代码了。因为归并排序是一半一半的进行,所以需要找到中点。最常用的方法就是快慢指针去找中点了。 + +```java +ListNode dummy = new ListNode(0); +dummy.next = head; +ListNode fast = dummy; +ListNode slow = dummy; +while (fast != null && fast.next != null) { + slow = slow.next; + fast = fast.next.next; +} +``` + +上边的代码我加了一个 `dummy` 指针,就是想当节点个数是偶数的时候,让 `slow` 刚好指向前边一半节点的最后一个节点,也就是下边的状态。 + +```java +1 2 3 4 + ^ ^ + slow fast +``` + +如果 `slow` 和 `fast` 都从 `head` 开始走,那么当 `fast` 结束的时候,`slow` 就会走到后边一半节点的开头了。 + +当然除了上边的方法,在 [这里](https://leetcode.com/problems/sort-list/discuss/46714/Java-merge-sort-solution) 看到,还可以加一个 `pre` 指针,让它一直指向 `slow` 的前一个即可。 + +```java +// step 1. cut the list to two halves +ListNode prev = null, slow = head, fast = head; + +while (fast != null && fast.next != null) { + prev = slow; + slow = slow.next; + fast = fast.next.next; +} +``` + +他们的目的都是一样的,就是为了方便的把两个链表平均分开。 + +```java +public ListNode sortList(ListNode head) { + return mergeSort(head); +} + +private ListNode mergeSort(ListNode head) { + if (head == null || head.next == null) { + return head; + } + ListNode dummy = new ListNode(0); + dummy.next = head; + ListNode fast = dummy; + ListNode slow = dummy; + //快慢指针找中点 + while (fast != null && fast.next != null) { + slow = slow.next; + fast = fast.next.next; + } + + ListNode head2 = slow.next; + slow.next = null; + head = mergeSort(head); + head2 = mergeSort(head2); + return merge(head, head2); + +} + +private ListNode merge(ListNode head1, ListNode head2) { + ListNode dummy = new ListNode(0); + ListNode tail = dummy; + while (head1 != null && head2 != null) { + if (head1.val < head2.val) { + tail.next = head1; + tail = tail.next; + head1 = head1.next; + } else { + tail.next = head2; + tail = tail.next; + head2 = head2.next; + } + + } + if (head1 != null) { + tail.next = head1; + } + + if (head2 != null) { + tail.next = head2; + } + + return dummy.next; + +} + +``` + +当然严格的说,上边的解法空间复杂度并不是 `O(1)`,因为递归过程中压栈是需要消耗空间的,每次取一半,所以空间复杂度是 `O(log(n))`。 + +递归可以去改写成迭代的形式,也就是自底向上的走,就可以省去压栈的空间,空间复杂度从而达到 `O(1)`,详细的可以参考 [这里](https://leetcode.com/problems/sort-list/discuss/46712/Bottom-to-up(not-recurring)-with-o(1)-space-complextity-and-o(nlgn)-time-complextity) 。 + +# 总 + 和 [147 题](https://leetcode.wang/leetcode-147-Insertion-Sort-List.html) 一样,主要还是考察对链表的理解和排序算法的实现。 \ No newline at end of file diff --git a/leetcode-149-Max-Points-on-a-Line.md b/leetcode-149-Max-Points-on-a-Line.md index bab3e8422..466313375 100644 --- a/leetcode-149-Max-Points-on-a-Line.md +++ b/leetcode-149-Max-Points-on-a-Line.md @@ -1,371 +1,371 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/149.jpg) - -平面上有很多点,找出经过某一条直线最多有多少个点。 - -# 解法一 暴力破解 - -两点确定一条直线,最简单的方式考虑任意两点组成一条直线,然后判断其他点在不在这条直线上。 - -两点确定一条直线,直线方程可以表示成下边的样子。 - -$$\frac{y_2-y_1}{x_2-x_1}=\frac{y-y_2}{x-x_2}$$ - -所以当来了一个点 `(x,y)` 的时候,理论上,我们只需要代入到上边的方程进行判断即可。 - -但是在计算机中,算上边的除法的时候,结果可能是小数,计算机中用浮点数存储,但小数并不能精确表示,可以参考这篇 [浮点数](https://zhuanlan.zhihu.com/p/75581822) 的讲解。所以我们不能直接去判断等式两边是否相等。 - -第一个想法是,等式两边分子乘分母,转换为乘法的形式。 - -$$(y_2-y_1)*(x-x_2)=(y-y_2)*(x_2-x_1)$$ - -所以我们可以写一个 `test` 函数来判断点 `(x,y)` 是否在由点 `(x1,y1)` 和 `(x2,y2)` 组成的直线上。 - -```java -private boolean test(int x1, int y1, int x2, int y2, int x, int y) { - return (y2 - y1) * (x - x2) == (y - y2) * (x2 - x1); -} -``` - -上边看起来没问题,但如果乘积过大的话就可能造成溢出,从而产生问题。 - -最直接的解决方案就是不用 `int` 存,改用 `long` 存,可以暂时解决上边的问题。 - -```java -private boolean test(int x1, int y1, int x2, int y2, int x, int y) { - return (long)(y2 - y1) * (x - x2) == (long)(y - y2) * (x2 - x1); -} -``` - -但如果数据过大,依旧可能造成溢出,再直接的方法就是用 `java` 提供的 `BigInteger` 类处理。记得 `import java.math.BigInteger;`。 - -```java -private boolean test(int x1, int y1, int x2, int y2, int x, int y) { - BigInteger x11 = BigInteger.valueOf(x1); - BigInteger x22 = BigInteger.valueOf(x2); - BigInteger y11 = BigInteger.valueOf(y1); - BigInteger y22 = BigInteger.valueOf(y2); - BigInteger x0 = BigInteger.valueOf(x); - BigInteger y0 = BigInteger.valueOf(y); - return y22.subtract(y11).multiply(x0.subtract(x22)).equals(y0.subtract(y22).multiply(x22.subtract(x11))); -} -``` - -此外,还有一个方案。 - -对于下边的等式, - -$$\frac{y_2-y_1}{x_2-x_1}=\frac{y-y_2}{x-x_2}$$ - -还可以理解成判断两个分数相等,回到数学上,我们只需要将两个分数约分到最简,然后分别判断分子和分母是否相等即可。 - -所以,我们需要求分子和分母的最大公约数,直接用辗转相除法即可。 - -```java -private int gcd(int a, int b) { - while (b != 0) { - int temp = a % b; - a = b; - b = temp; - } - return a; -} -``` - -然后 `test` 函数就可以写成下边的样子。需要注意的是,我们求了`y - y2` 和 `x - x2` 最大公约数,所以要保证他俩都不是 `0` ,防止除零错误。 - -```java -private boolean test(int x1, int y1, int x2, int y2, int x, int y) { - int g1 = gcd(y2 - y1, x2 - x1); - if(y == y2 && x == x2){ - return true; - } - int g2 = gcd(y - y2, x - x2); - return (y2 - y1) / g1 == (y - y2) / g2 && (x2 - x1) / g1 == (x - x2) / g2; -} -``` - -有了 `test` 函数,接下来,我们只需要三层遍历。前两层遍历选择两个点的所有组合构成一条直线,第三层遍历其他所有点,来判断当前点在不在之前两个点组成的直线上。 - -需要注意的是,因为我们两点组成一条直线,必须保证这两个点不重合。所以我们进入第三层循环之前,如果两个点相等就可以直接跳过。 - -```java -if (points[i][0] == points[j][0] && points[i][1] == points[j][1]) { - continue; -} -``` - -此外,我们还需要考虑所有点都相等的情况,这样就可以看做所有点都在一条直线上。 - -```java -int i = 0; -for (; i < points.length - 1; i++) { - if (points[i][0] != points[i + 1][0] || points[i][1] != points[i + 1][1]) { - break; - } -} -if (i == points.length - 1) { - return points.length; -} -``` - -还有就是点的数量只有两个,或者一个,零个的时候,直接返回点的数量即可。 - -```java -if (points.length < 3) { - return points.length; -} -``` - -综上所述,代码就出来了,其中 `test` 函数有三种写法。 - -```java -public int maxPoints(int[][] points) { - if (points.length < 3) { - return points.length; - } - int i = 0; - for (; i < points.length - 1; i++) { - if (points[i][0] != points[i + 1][0] || points[i][1] != points[i + 1][1]) { - break; - } - - } - if (i == points.length - 1) { - return points.length; - } - int max = 0; - for (i = 0; i < points.length; i++) { - for (int j = i + 1; j < points.length; j++) { - if (points[i][0] == points[j][0] && points[i][1] == points[j][1]) { - continue; - } - int tempMax = 0; - for (int k = 0; k < points.length; k++) { - if (k != i && k != j) { - if (test(points[i][0], points[i][1], points[j][0], points[j][1], points[k][0], points[k][1])) { - tempMax++; - } - } - - } - if (tempMax > max) { - max = tempMax; - } - } - } - //加上直线本身的两个点 - return max + 2; -} -/*private boolean test(int x1, int y1, int x2, int y2, int x, int y) { - return (long)(y2 - y1) * (x - x2) == (long)(y - y2) * (x2 - x1); -}*/ - -/*private boolean test(int x1, int y1, int x2, int y2, int x, int y) { - BigInteger x11 = BigInteger.valueOf(x1); - BigInteger x22 = BigInteger.valueOf(x2); - BigInteger y11 = BigInteger.valueOf(y1); - BigInteger y22 = BigInteger.valueOf(y2); - BigInteger x0 = BigInteger.valueOf(x); - BigInteger y0 = BigInteger.valueOf(y); - return y22.subtract(y11).multiply(x0.subtract(x22)).equals(y0.subtract(y22).multiply(x22.subtract(x11))); -}*/ - -private boolean test(int x1, int y1, int x2, int y2, int x, int y) { - int g1 = gcd(y2 - y1, x2 - x1); - if(y == y2 && x == x2){ - return true; - } - int g2 = gcd(y - y2, x - x2); - return (y2 - y1) / g1 == (y - y2) / g2 && (x2 - x1) / g1 == (x - x2) / g2; -} - -private int gcd(int a, int b) { - while (b != 0) { - int temp = a % b; - a = b; - b = temp; - } - return a; -} -``` - -# 解法二 - -解法一很暴力,我们考虑在其基础上进行优化。 - -注意到,如果是下边的情况。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/149_2.jpg) - -对于解法一的算法,我们会经过下边的流程。 - -我们先考虑 `1,2` 组成的直线,看 `3,4,5,6`在不在 `1,2` 的直线上。 - -再考虑 `1,3` 组成的直线,看 `2,4,5,6`在不在 `1,3` 的直线上。 - -再考虑 `1,4` 组成的直线,看 `2,3,5,6`在不在 `1,4` 的直线上。 - -.... - -上边的问题很明显了,对于 `1,2`,`1,3 `,`1,4` ... 组成的直线,其实是同一条,我们只需要判断一次就可以了。 - -所以我们需要做的是,怎么保证在判断完 `1,2` 构成的直线后,把 `1,3`,`1,4`... 这些在 `1,2` 直线上的点直接跳过。 - -回到数学上,给定两个点可以唯一的确定一条直线,表达式为 `y = kx + b`。 - -对于 `1,2`,`1,3 `,`1,4` 这些点求出来的表达式都是唯一确定的。 - -所以我们当考虑 `1,2` 两个点的时候,我们可以求出 `k` 和 `b` 把它存到 `HashSet` 中,然后当考虑 `1,3` 以及后边的点的时候,先求出 `k` 和 `b`,然后从 `HashSet` 中看是否存在即可。 - -当然存的时候,我们可以用一个技巧,`key` 存一个 `String` ,也就是 `k + "@" + b` 。 - -```java -public int maxPoints(int[][] points) { - if(points.length < 3){ - return points.length; - } - int i = 0; - //判断所有点是否都相同的特殊情况 - for (; i < points.length - 1; i++) { - if (points[i][0] != points[i + 1][0] || points[i][1] != points[i + 1][1]) { - break; - } - - } - if (i == points.length - 1) { - return points.length; - } - HashSet set = new HashSet<>(); - int max = 0; - for (i = 0; i < points.length; i++) { - for (int j = i + 1; j < points.length; j++) { - if (points[i][0] == points[j][0] && points[i][1] == points[j][1]) { - continue; - } - String key = getK(points[i][0], points[i][1], points[j][0], points[j][1]) - + "@" - + getB(points[i][0], points[i][1], points[j][0], points[j][1]); - if (set.contains(key)) { - continue; - } - int tempMax = 0; - for (int k = 0; k < points.length; k++) { - if (k != i && k != j) { - if (test(points[i][0], points[i][1], points[j][0], points[j][1], points[k][0], points[k][1])) { - tempMax++; - } - } - - } - if (tempMax > max) { - max = tempMax; - } - set.add(key); - } - } - return max + 2; -} - -private double getB(int x1, int y1, int x2, int y2) { - if (y2 == y1) { - return Double.POSITIVE_INFINITY; - } - return (double) (x2 - x1) * (-y1) / (y2 - y1) + x1; -} - -private double getK(int x1, int y1, int x2, int y2) { - if (x2 - x1 == 0) { - return Double.POSITIVE_INFINITY; - } - return (double) (y2 - y1) / (x2 - x1); -} - -private boolean test(int x1, int y1, int x2, int y2, int x, int y) { - return (long)(y2 - y1) * (x - x2) == (long)(y - y2) * (x2 - x1); -} -``` - -上边的算法虽然能 `AC`,但如果严格来说其实是有问题的。还是因为之前说的浮点数的问题,计算机并不能精确表示小数。这就造成不同的直线可能会求出相同的 `k` 和 `b`。 - -如果要修改的话,我们可以用分数表示小数,同时必须进行约分,使得分数化成最简。 - -对于上边的算法,有两个变量都需要用小数表示,所以可能会复杂些,可以看一下解法三的思路。 - -# 解法三 - -解法二中,我们相当于是对直线的分类,一条直线一条直线的考虑,去求直线上的点。 - -[这里](https://leetcode.com/problems/max-points-on-a-line/discuss/47113/A-java-solution-with-notes) 看到另一种想法,分享一下。 - -灵感应该来自于直线方程的另一种表示方式,「点斜式」,换句话,一个点加一个斜率即可唯一的确定一条直线。 - -所以我们可以对「点」进行分类然后去求,问题转换成,经过某个点的直线,哪条直线上的点最多。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/149_3.jpg) - -当确定一个点后,平面上的其他点都和这个点可以求出一个斜率,斜率相同的点就意味着在同一条直线上。 - -所以我们可以用 `HashMap` 去计数,斜率作为 `key`,然后遍历平面上的其他点,相同的 `key` 意味着在同一条直线上。 - -上边的思想解决了「经过某个点的直线,哪条直线上的点最多」的问题。接下来只需要换一个点,然后用同样的方法考虑完所有的点即可。 - -当然还有一个问题就是斜率是小数,怎么办。 - -之前提到过了,我们用分数去表示,求分子分母的最大公约数,然后约分,最后将 「分子 + "@" + "分母"」作为 `key` 即可。 - -最后还有一个细节就是,当确定某个点的时候,平面内如果有和这个重叠的点,如果按照正常的算法约分的话,会出现除 `0` 的情况,所以我们需要单独用一个变量记录重复点的个数,而重复点一定是过当前点的直线的。 - -```java -public int maxPoints(int[][] points) { - if (points.length < 3) { - return points.length; - } - int res = 0; - //遍历每个点 - for (int i = 0; i < points.length; i++) { - int duplicate = 0; - int max = 0;//保存经过当前点的直线中,最多的点 - HashMap map = new HashMap<>(); - for (int j = i + 1; j < points.length; j++) { - //求出分子分母 - int x = points[j][0] - points[i][0]; - int y = points[j][1] - points[i][1]; - if (x == 0 && y == 0) { - duplicate++; - continue; - - } - //进行约分 - int gcd = gcd(x, y); - x = x / gcd; - y = y / gcd; - String key = x + "@" + y; - map.put(key, map.getOrDefault(key, 0) + 1); - max = Math.max(max, map.get(key)); - } - //1 代表当前考虑的点,duplicate 代表和当前的点重复的点 - res = Math.max(res, max + duplicate + 1); - } - return res; -} - -private int gcd(int a, int b) { - while (b != 0) { - int temp = a % b; - a = b; - b = temp; - } - return a; -} -``` - -# 总 - -这道题首先还是去想暴力的想法,然后去考虑重复的情况,对情况进行分类从而优化时间复杂度。同样解法三其实也是一种分类的思想,会减少很多不必要情况的讨论。 - - - - - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/149.jpg) + +平面上有很多点,找出经过某一条直线最多有多少个点。 + +# 解法一 暴力破解 + +两点确定一条直线,最简单的方式考虑任意两点组成一条直线,然后判断其他点在不在这条直线上。 + +两点确定一条直线,直线方程可以表示成下边的样子。 + +$$\frac{y_2-y_1}{x_2-x_1}=\frac{y-y_2}{x-x_2}$$ + +所以当来了一个点 `(x,y)` 的时候,理论上,我们只需要代入到上边的方程进行判断即可。 + +但是在计算机中,算上边的除法的时候,结果可能是小数,计算机中用浮点数存储,但小数并不能精确表示,可以参考这篇 [浮点数](https://zhuanlan.zhihu.com/p/75581822) 的讲解。所以我们不能直接去判断等式两边是否相等。 + +第一个想法是,等式两边分子乘分母,转换为乘法的形式。 + +$$(y_2-y_1)*(x-x_2)=(y-y_2)*(x_2-x_1)$$ + +所以我们可以写一个 `test` 函数来判断点 `(x,y)` 是否在由点 `(x1,y1)` 和 `(x2,y2)` 组成的直线上。 + +```java +private boolean test(int x1, int y1, int x2, int y2, int x, int y) { + return (y2 - y1) * (x - x2) == (y - y2) * (x2 - x1); +} +``` + +上边看起来没问题,但如果乘积过大的话就可能造成溢出,从而产生问题。 + +最直接的解决方案就是不用 `int` 存,改用 `long` 存,可以暂时解决上边的问题。 + +```java +private boolean test(int x1, int y1, int x2, int y2, int x, int y) { + return (long)(y2 - y1) * (x - x2) == (long)(y - y2) * (x2 - x1); +} +``` + +但如果数据过大,依旧可能造成溢出,再直接的方法就是用 `java` 提供的 `BigInteger` 类处理。记得 `import java.math.BigInteger;`。 + +```java +private boolean test(int x1, int y1, int x2, int y2, int x, int y) { + BigInteger x11 = BigInteger.valueOf(x1); + BigInteger x22 = BigInteger.valueOf(x2); + BigInteger y11 = BigInteger.valueOf(y1); + BigInteger y22 = BigInteger.valueOf(y2); + BigInteger x0 = BigInteger.valueOf(x); + BigInteger y0 = BigInteger.valueOf(y); + return y22.subtract(y11).multiply(x0.subtract(x22)).equals(y0.subtract(y22).multiply(x22.subtract(x11))); +} +``` + +此外,还有一个方案。 + +对于下边的等式, + +$$\frac{y_2-y_1}{x_2-x_1}=\frac{y-y_2}{x-x_2}$$ + +还可以理解成判断两个分数相等,回到数学上,我们只需要将两个分数约分到最简,然后分别判断分子和分母是否相等即可。 + +所以,我们需要求分子和分母的最大公约数,直接用辗转相除法即可。 + +```java +private int gcd(int a, int b) { + while (b != 0) { + int temp = a % b; + a = b; + b = temp; + } + return a; +} +``` + +然后 `test` 函数就可以写成下边的样子。需要注意的是,我们求了`y - y2` 和 `x - x2` 最大公约数,所以要保证他俩都不是 `0` ,防止除零错误。 + +```java +private boolean test(int x1, int y1, int x2, int y2, int x, int y) { + int g1 = gcd(y2 - y1, x2 - x1); + if(y == y2 && x == x2){ + return true; + } + int g2 = gcd(y - y2, x - x2); + return (y2 - y1) / g1 == (y - y2) / g2 && (x2 - x1) / g1 == (x - x2) / g2; +} +``` + +有了 `test` 函数,接下来,我们只需要三层遍历。前两层遍历选择两个点的所有组合构成一条直线,第三层遍历其他所有点,来判断当前点在不在之前两个点组成的直线上。 + +需要注意的是,因为我们两点组成一条直线,必须保证这两个点不重合。所以我们进入第三层循环之前,如果两个点相等就可以直接跳过。 + +```java +if (points[i][0] == points[j][0] && points[i][1] == points[j][1]) { + continue; +} +``` + +此外,我们还需要考虑所有点都相等的情况,这样就可以看做所有点都在一条直线上。 + +```java +int i = 0; +for (; i < points.length - 1; i++) { + if (points[i][0] != points[i + 1][0] || points[i][1] != points[i + 1][1]) { + break; + } +} +if (i == points.length - 1) { + return points.length; +} +``` + +还有就是点的数量只有两个,或者一个,零个的时候,直接返回点的数量即可。 + +```java +if (points.length < 3) { + return points.length; +} +``` + +综上所述,代码就出来了,其中 `test` 函数有三种写法。 + +```java +public int maxPoints(int[][] points) { + if (points.length < 3) { + return points.length; + } + int i = 0; + for (; i < points.length - 1; i++) { + if (points[i][0] != points[i + 1][0] || points[i][1] != points[i + 1][1]) { + break; + } + + } + if (i == points.length - 1) { + return points.length; + } + int max = 0; + for (i = 0; i < points.length; i++) { + for (int j = i + 1; j < points.length; j++) { + if (points[i][0] == points[j][0] && points[i][1] == points[j][1]) { + continue; + } + int tempMax = 0; + for (int k = 0; k < points.length; k++) { + if (k != i && k != j) { + if (test(points[i][0], points[i][1], points[j][0], points[j][1], points[k][0], points[k][1])) { + tempMax++; + } + } + + } + if (tempMax > max) { + max = tempMax; + } + } + } + //加上直线本身的两个点 + return max + 2; +} +/*private boolean test(int x1, int y1, int x2, int y2, int x, int y) { + return (long)(y2 - y1) * (x - x2) == (long)(y - y2) * (x2 - x1); +}*/ + +/*private boolean test(int x1, int y1, int x2, int y2, int x, int y) { + BigInteger x11 = BigInteger.valueOf(x1); + BigInteger x22 = BigInteger.valueOf(x2); + BigInteger y11 = BigInteger.valueOf(y1); + BigInteger y22 = BigInteger.valueOf(y2); + BigInteger x0 = BigInteger.valueOf(x); + BigInteger y0 = BigInteger.valueOf(y); + return y22.subtract(y11).multiply(x0.subtract(x22)).equals(y0.subtract(y22).multiply(x22.subtract(x11))); +}*/ + +private boolean test(int x1, int y1, int x2, int y2, int x, int y) { + int g1 = gcd(y2 - y1, x2 - x1); + if(y == y2 && x == x2){ + return true; + } + int g2 = gcd(y - y2, x - x2); + return (y2 - y1) / g1 == (y - y2) / g2 && (x2 - x1) / g1 == (x - x2) / g2; +} + +private int gcd(int a, int b) { + while (b != 0) { + int temp = a % b; + a = b; + b = temp; + } + return a; +} +``` + +# 解法二 + +解法一很暴力,我们考虑在其基础上进行优化。 + +注意到,如果是下边的情况。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/149_2.jpg) + +对于解法一的算法,我们会经过下边的流程。 + +我们先考虑 `1,2` 组成的直线,看 `3,4,5,6`在不在 `1,2` 的直线上。 + +再考虑 `1,3` 组成的直线,看 `2,4,5,6`在不在 `1,3` 的直线上。 + +再考虑 `1,4` 组成的直线,看 `2,3,5,6`在不在 `1,4` 的直线上。 + +.... + +上边的问题很明显了,对于 `1,2`,`1,3 `,`1,4` ... 组成的直线,其实是同一条,我们只需要判断一次就可以了。 + +所以我们需要做的是,怎么保证在判断完 `1,2` 构成的直线后,把 `1,3`,`1,4`... 这些在 `1,2` 直线上的点直接跳过。 + +回到数学上,给定两个点可以唯一的确定一条直线,表达式为 `y = kx + b`。 + +对于 `1,2`,`1,3 `,`1,4` 这些点求出来的表达式都是唯一确定的。 + +所以我们当考虑 `1,2` 两个点的时候,我们可以求出 `k` 和 `b` 把它存到 `HashSet` 中,然后当考虑 `1,3` 以及后边的点的时候,先求出 `k` 和 `b`,然后从 `HashSet` 中看是否存在即可。 + +当然存的时候,我们可以用一个技巧,`key` 存一个 `String` ,也就是 `k + "@" + b` 。 + +```java +public int maxPoints(int[][] points) { + if(points.length < 3){ + return points.length; + } + int i = 0; + //判断所有点是否都相同的特殊情况 + for (; i < points.length - 1; i++) { + if (points[i][0] != points[i + 1][0] || points[i][1] != points[i + 1][1]) { + break; + } + + } + if (i == points.length - 1) { + return points.length; + } + HashSet set = new HashSet<>(); + int max = 0; + for (i = 0; i < points.length; i++) { + for (int j = i + 1; j < points.length; j++) { + if (points[i][0] == points[j][0] && points[i][1] == points[j][1]) { + continue; + } + String key = getK(points[i][0], points[i][1], points[j][0], points[j][1]) + + "@" + + getB(points[i][0], points[i][1], points[j][0], points[j][1]); + if (set.contains(key)) { + continue; + } + int tempMax = 0; + for (int k = 0; k < points.length; k++) { + if (k != i && k != j) { + if (test(points[i][0], points[i][1], points[j][0], points[j][1], points[k][0], points[k][1])) { + tempMax++; + } + } + + } + if (tempMax > max) { + max = tempMax; + } + set.add(key); + } + } + return max + 2; +} + +private double getB(int x1, int y1, int x2, int y2) { + if (y2 == y1) { + return Double.POSITIVE_INFINITY; + } + return (double) (x2 - x1) * (-y1) / (y2 - y1) + x1; +} + +private double getK(int x1, int y1, int x2, int y2) { + if (x2 - x1 == 0) { + return Double.POSITIVE_INFINITY; + } + return (double) (y2 - y1) / (x2 - x1); +} + +private boolean test(int x1, int y1, int x2, int y2, int x, int y) { + return (long)(y2 - y1) * (x - x2) == (long)(y - y2) * (x2 - x1); +} +``` + +上边的算法虽然能 `AC`,但如果严格来说其实是有问题的。还是因为之前说的浮点数的问题,计算机并不能精确表示小数。这就造成不同的直线可能会求出相同的 `k` 和 `b`。 + +如果要修改的话,我们可以用分数表示小数,同时必须进行约分,使得分数化成最简。 + +对于上边的算法,有两个变量都需要用小数表示,所以可能会复杂些,可以看一下解法三的思路。 + +# 解法三 + +解法二中,我们相当于是对直线的分类,一条直线一条直线的考虑,去求直线上的点。 + +[这里](https://leetcode.com/problems/max-points-on-a-line/discuss/47113/A-java-solution-with-notes) 看到另一种想法,分享一下。 + +灵感应该来自于直线方程的另一种表示方式,「点斜式」,换句话,一个点加一个斜率即可唯一的确定一条直线。 + +所以我们可以对「点」进行分类然后去求,问题转换成,经过某个点的直线,哪条直线上的点最多。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/149_3.jpg) + +当确定一个点后,平面上的其他点都和这个点可以求出一个斜率,斜率相同的点就意味着在同一条直线上。 + +所以我们可以用 `HashMap` 去计数,斜率作为 `key`,然后遍历平面上的其他点,相同的 `key` 意味着在同一条直线上。 + +上边的思想解决了「经过某个点的直线,哪条直线上的点最多」的问题。接下来只需要换一个点,然后用同样的方法考虑完所有的点即可。 + +当然还有一个问题就是斜率是小数,怎么办。 + +之前提到过了,我们用分数去表示,求分子分母的最大公约数,然后约分,最后将 「分子 + "@" + "分母"」作为 `key` 即可。 + +最后还有一个细节就是,当确定某个点的时候,平面内如果有和这个重叠的点,如果按照正常的算法约分的话,会出现除 `0` 的情况,所以我们需要单独用一个变量记录重复点的个数,而重复点一定是过当前点的直线的。 + +```java +public int maxPoints(int[][] points) { + if (points.length < 3) { + return points.length; + } + int res = 0; + //遍历每个点 + for (int i = 0; i < points.length; i++) { + int duplicate = 0; + int max = 0;//保存经过当前点的直线中,最多的点 + HashMap map = new HashMap<>(); + for (int j = i + 1; j < points.length; j++) { + //求出分子分母 + int x = points[j][0] - points[i][0]; + int y = points[j][1] - points[i][1]; + if (x == 0 && y == 0) { + duplicate++; + continue; + + } + //进行约分 + int gcd = gcd(x, y); + x = x / gcd; + y = y / gcd; + String key = x + "@" + y; + map.put(key, map.getOrDefault(key, 0) + 1); + max = Math.max(max, map.get(key)); + } + //1 代表当前考虑的点,duplicate 代表和当前的点重复的点 + res = Math.max(res, max + duplicate + 1); + } + return res; +} + +private int gcd(int a, int b) { + while (b != 0) { + int temp = a % b; + a = b; + b = temp; + } + return a; +} +``` + +# 总 + +这道题首先还是去想暴力的想法,然后去考虑重复的情况,对情况进行分类从而优化时间复杂度。同样解法三其实也是一种分类的思想,会减少很多不必要情况的讨论。 + + + + + diff --git a/leetcode-150-Evaluate-Reverse-Polish-Notation.md b/leetcode-150-Evaluate-Reverse-Polish-Notation.md index c4a5fba2d..e5ca7ff2a 100644 --- a/leetcode-150-Evaluate-Reverse-Polish-Notation.md +++ b/leetcode-150-Evaluate-Reverse-Polish-Notation.md @@ -1,67 +1,67 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/150.png) - -我们平常用的是中缀表达式,也就是上边 Explanation 中解释的。题目中的是逆波兰式,一个好处就是只需要运算符,不需要括号,不会产生歧义。 - -计算法则就是,每次找到运算符位置的前两个数字,然后进行计算。 - -# 解法一 - -学栈的时候,应该就知道这个逆波兰式了,栈的典型应用。 - -遇到操作数就入栈,遇到操作符就将栈顶的两个元素弹出进行操作,将结果继续入栈即可。 - -```java -public int evalRPN(String[] tokens) { - Stack stack = new Stack<>(); - for (String t : tokens) { - if (isOperation(t)) { - int a = stringToNumber(stack.pop()); - int b = stringToNumber(stack.pop()); - int ans = eval(b, a, t.charAt(0)); - stack.push(ans + ""); - } else { - stack.push(t); - } - } - return stringToNumber(stack.pop()); -} - -private int eval(int a, int b, char op) { - switch (op) { - case '+': - return a + b; - case '-': - return a - b; - case '*': - return a * b; - case '/': - return a / b; - } - return 0; -} - -private int stringToNumber(String s) { - int sign = 1; - int start = 0; - if (s.charAt(0) == '-') { - sign = -1; - start = 1; - } - int res = 0; - for (int i = start; i < s.length(); i++) { - res = res * 10 + s.charAt(i) - '0'; - } - return res * sign; -} - -private boolean isOperation(String t) { - return t.equals("+") || t.equals("-") || t.equals("*") || t.equals("/"); -} - -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/150.png) + +我们平常用的是中缀表达式,也就是上边 Explanation 中解释的。题目中的是逆波兰式,一个好处就是只需要运算符,不需要括号,不会产生歧义。 + +计算法则就是,每次找到运算符位置的前两个数字,然后进行计算。 + +# 解法一 + +学栈的时候,应该就知道这个逆波兰式了,栈的典型应用。 + +遇到操作数就入栈,遇到操作符就将栈顶的两个元素弹出进行操作,将结果继续入栈即可。 + +```java +public int evalRPN(String[] tokens) { + Stack stack = new Stack<>(); + for (String t : tokens) { + if (isOperation(t)) { + int a = stringToNumber(stack.pop()); + int b = stringToNumber(stack.pop()); + int ans = eval(b, a, t.charAt(0)); + stack.push(ans + ""); + } else { + stack.push(t); + } + } + return stringToNumber(stack.pop()); +} + +private int eval(int a, int b, char op) { + switch (op) { + case '+': + return a + b; + case '-': + return a - b; + case '*': + return a * b; + case '/': + return a / b; + } + return 0; +} + +private int stringToNumber(String s) { + int sign = 1; + int start = 0; + if (s.charAt(0) == '-') { + sign = -1; + start = 1; + } + int res = 0; + for (int i = start; i < s.length(); i++) { + res = res * 10 + s.charAt(i) - '0'; + } + return res * sign; +} + +private boolean isOperation(String t) { + return t.equals("+") || t.equals("-") || t.equals("*") || t.equals("/"); +} + +``` + +# 总 + 主要就是栈的应用,比较简单。 \ No newline at end of file diff --git a/leetcode-151-Reverse-Words-in-a-String.md b/leetcode-151-Reverse-Words-in-a-String.md index 6e8f76961..e516bff3c 100644 --- a/leetcode-151-Reverse-Words-in-a-String.md +++ b/leetcode-151-Reverse-Words-in-a-String.md @@ -1,122 +1,122 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/151.png) - -将字符串的每个单词反过来,单词内部不需要逆转,注意可能会有多余的空格。 - -# 解法一 - -题目很直观,做法也会很直观,哈哈。遍历原字符串,遇到字母就加到一个 `temp` 变量中,遇到空格,如果 `temp` 变量不为空,就把 `temp` 组成的单词加到一个栈中,然后清空 `temp` 继续遍历。 - -最后,将栈中的每个单词依次拿出来拼接即可。 - -有一个技巧可以用,就是最后一个单词后边可能没有空格,为了统一,我们可以人为的在字符串后边加入一个空格。 - -```java -public String reverseWords(String s) { - Stack stack = new Stack<>(); - StringBuilder temp = new StringBuilder(); - s += " "; - for (int i = 0; i < s.length(); i++) { - if (s.charAt(i) == ' ') { - if (temp.length() != 0) { - stack.push(temp.toString()); - temp = new StringBuilder(); - } - } else { - temp.append(s.charAt(i)); - } - } - if (stack.isEmpty()) { - return ""; - } - StringBuilder res = new StringBuilder(); - res.append(stack.pop()); - while (!stack.isEmpty()) { - res.append(" "); - res.append(stack.pop()); - } - return res.toString(); -} -``` - -# 解法二 - -可以看下题目中的 `Follow up`。 - -> For C programmers, try to solve it *in-place* in *O*(1) extra space. - -如果用 C 语言,试着不用额外空间解决这个问题。 - -我们一直用的是 java,而 java 中的 `String` 变量是不可更改的,如果对它修改其实又会去重新创建新的内存空间。 - -而 C 语言不同,C 语言中的 `string` 本质上其实是 `char` 数组,所以我们可以在给定的 `string` 上直接进行修改而不使用额外空间。 - -为了曲线救国,继续用 java 实现,我们先将 `String` 转为 `char` 数组,所有的操作都在 `char` 数组上进行。 - -```java - char[] a = s.toCharArray(); -``` - -至于算法的话,参考了 [这里](https://leetcode.com/problems/reverse-words-in-a-string/discuss/47720/Clean-Java-two-pointers-solution-(no-trim(-)-no-split(-)-no-StringBuilder)。 - -主要是三个步骤即可。 - -1. 原地逆转 `char` 数组,这会导致每个单词内部也被逆转,接下来进行第二步 -2. 原地逆转每个单词 -3. 去除多余的空格 - -具体代码的话就直接从 [这里](https://leetcode.com/problems/reverse-words-in-a-string/discuss/47720/Clean-Java-two-pointers-solution-(no-trim(-)-no-split(-)-no-StringBuilder) 粘贴过来了,写的很简洁。几个封装的函数,关键就是去解决怎么原地完成。 - -```java -public String reverseWords(String s) { - if (s == null) return null; - - char[] a = s.toCharArray(); - int n = a.length; - - // step 1. reverse the whole string - reverse(a, 0, n - 1); - // step 2. reverse each word - reverseWords(a, n); - // step 3. clean up spaces - return cleanSpaces(a, n); -} - -void reverseWords(char[] a, int n) { - int i = 0, j = 0; - - while (i < n) { - while (i < j || i < n && a[i] == ' ') i++; // skip spaces - while (j < i || j < n && a[j] != ' ') j++; // skip non spaces - reverse(a, i, j - 1); // reverse the word - } -} - -// trim leading, trailing and multiple spaces -String cleanSpaces(char[] a, int n) { - int i = 0, j = 0; - - while (j < n) { - while (j < n && a[j] == ' ') j++; // skip spaces - while (j < n && a[j] != ' ') a[i++] = a[j++]; // keep non spaces - while (j < n && a[j] == ' ') j++; // skip spaces - if (j < n) a[i++] = ' '; // keep only one space - } - - return new String(a).substring(0, i); -} - -// reverse a[] from a[i] to a[j] -private void reverse(char[] a, int i, int j) { - while (i < j) { - char t = a[i]; - a[i++] = a[j]; - a[j--] = t; - } -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/151.png) + +将字符串的每个单词反过来,单词内部不需要逆转,注意可能会有多余的空格。 + +# 解法一 + +题目很直观,做法也会很直观,哈哈。遍历原字符串,遇到字母就加到一个 `temp` 变量中,遇到空格,如果 `temp` 变量不为空,就把 `temp` 组成的单词加到一个栈中,然后清空 `temp` 继续遍历。 + +最后,将栈中的每个单词依次拿出来拼接即可。 + +有一个技巧可以用,就是最后一个单词后边可能没有空格,为了统一,我们可以人为的在字符串后边加入一个空格。 + +```java +public String reverseWords(String s) { + Stack stack = new Stack<>(); + StringBuilder temp = new StringBuilder(); + s += " "; + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) == ' ') { + if (temp.length() != 0) { + stack.push(temp.toString()); + temp = new StringBuilder(); + } + } else { + temp.append(s.charAt(i)); + } + } + if (stack.isEmpty()) { + return ""; + } + StringBuilder res = new StringBuilder(); + res.append(stack.pop()); + while (!stack.isEmpty()) { + res.append(" "); + res.append(stack.pop()); + } + return res.toString(); +} +``` + +# 解法二 + +可以看下题目中的 `Follow up`。 + +> For C programmers, try to solve it *in-place* in *O*(1) extra space. + +如果用 C 语言,试着不用额外空间解决这个问题。 + +我们一直用的是 java,而 java 中的 `String` 变量是不可更改的,如果对它修改其实又会去重新创建新的内存空间。 + +而 C 语言不同,C 语言中的 `string` 本质上其实是 `char` 数组,所以我们可以在给定的 `string` 上直接进行修改而不使用额外空间。 + +为了曲线救国,继续用 java 实现,我们先将 `String` 转为 `char` 数组,所有的操作都在 `char` 数组上进行。 + +```java + char[] a = s.toCharArray(); +``` + +至于算法的话,参考了 [这里](https://leetcode.com/problems/reverse-words-in-a-string/discuss/47720/Clean-Java-two-pointers-solution-(no-trim(-)-no-split(-)-no-StringBuilder)。 + +主要是三个步骤即可。 + +1. 原地逆转 `char` 数组,这会导致每个单词内部也被逆转,接下来进行第二步 +2. 原地逆转每个单词 +3. 去除多余的空格 + +具体代码的话就直接从 [这里](https://leetcode.com/problems/reverse-words-in-a-string/discuss/47720/Clean-Java-two-pointers-solution-(no-trim(-)-no-split(-)-no-StringBuilder) 粘贴过来了,写的很简洁。几个封装的函数,关键就是去解决怎么原地完成。 + +```java +public String reverseWords(String s) { + if (s == null) return null; + + char[] a = s.toCharArray(); + int n = a.length; + + // step 1. reverse the whole string + reverse(a, 0, n - 1); + // step 2. reverse each word + reverseWords(a, n); + // step 3. clean up spaces + return cleanSpaces(a, n); +} + +void reverseWords(char[] a, int n) { + int i = 0, j = 0; + + while (i < n) { + while (i < j || i < n && a[i] == ' ') i++; // skip spaces + while (j < i || j < n && a[j] != ' ') j++; // skip non spaces + reverse(a, i, j - 1); // reverse the word + } +} + +// trim leading, trailing and multiple spaces +String cleanSpaces(char[] a, int n) { + int i = 0, j = 0; + + while (j < n) { + while (j < n && a[j] == ' ') j++; // skip spaces + while (j < n && a[j] != ' ') a[i++] = a[j++]; // keep non spaces + while (j < n && a[j] == ' ') j++; // skip spaces + if (j < n) a[i++] = ' '; // keep only one space + } + + return new String(a).substring(0, i); +} + +// reverse a[] from a[i] to a[j] +private void reverse(char[] a, int i, int j) { + while (i < j) { + char t = a[i]; + a[i++] = a[j]; + a[j--] = t; + } +} +``` + +# 总 + 一开始没有 `get` 到题目要求空间复杂度为 `O(1)` 的想法,后来在 `discuss` 中才突然明白。 \ No newline at end of file diff --git a/leetcode-152-Maximum-Product-Subarray.md b/leetcode-152-Maximum-Product-Subarray.md index e928f30fc..7a0b8eaff 100644 --- a/leetcode-152-Maximum-Product-Subarray.md +++ b/leetcode-152-Maximum-Product-Subarray.md @@ -1,251 +1,251 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/152.jpg) - -找一个连续的子数组,使得连乘起来最大。 - -# 解法一 动态规划 - -开始没有往这方面想,直接想到了解法二,一会儿讲。看到 [这里](https://leetcode.com/problems/maximum-product-subarray/discuss/48230/Possibly-simplest-solution-with-O(n)-time-complexity),才想起来直接用动态规划解就可以,和 [53 题](https://leetcode.wang/leetCode-53-Maximum-Subarray.html) 子数组最大的和思路差不多。 - -我们先定义一个数组 `dpMax`,用 `dpMax[i]` 表示以第 `i` 个元素的结尾的子数组,乘积最大的值,也就是这个数组必须包含第 `i` 个元素。 - -那么 `dpMax[i]` 的话有几种取值。 - -* 当 `nums[i] >= 0` 并且`dpMax[i-1] > 0`,`dpMax[i] = dpMax[i-1] * nums[i]` -* 当 `nums[i] >= 0` 并且`dpMax[i-1] < 0`,此时如果和前边的数累乘的话,会变成负数,所以`dpMax[i] = nums[i]` -* 当 `nums[i] < 0`,此时如果前边累乘结果是一个很大的负数,和当前负数累乘的话就会变成一个更大的数。所以我们还需要一个数组 `dpMin` 来记录以第 `i` 个元素的结尾的子数组,乘积最小的值。 - * 当`dpMin[i-1] < 0`,`dpMax[i] = dpMin[i-1] * nums[i]` - * 当`dpMin[i-1] >= 0`,`dpMax[i] = nums[i]` - -当然,上边引入了 `dpMin` 数组,怎么求 `dpMin` 其实和上边求 `dpMax` 的过程其实是一样的。 - -按上边的分析,我们就需要加很多的 `if else`来判断不同的情况,这里可以用个技巧。 - -我们注意到上边`dpMax[i]` 的取值无非就是三种,`dpMax[i-1] * nums[i]`、`dpMin[i-1] * nums[i]` 以及 `nums[i]`。 - -所以我们更新的时候,无需去区分当前是哪种情况,只需要从三个取值中选一个最大的即可。 - -```java -dpMax[i] = max(dpMax[i-1] * nums[i], dpMin[i-1] * nums[i], nums[i]); -``` - -求 `dpMin[i]` 同理。 - -```java -dpMin[i] = min(dpMax[i-1] * nums[i], dpMin[i-1] * nums[i], nums[i]); -``` - -更新过程中,我们可以用一个变量 `max` 去保存当前得到的最大值。 - -```java -public int maxProduct(int[] nums) { - int n = nums.length; - if (n == 0) { - return 0; - } - - int[] dpMax = new int[n]; - dpMax[0] = nums[0]; - int[] dpMin = new int[n]; - dpMin[0] = nums[0]; - int max = nums[0]; - for (int i = 1; i < n; i++) { - dpMax[i] = Math.max(dpMin[i - 1] * nums[i], Math.max(dpMax[i - 1] * nums[i], nums[i])); - dpMin[i] = Math.min(dpMin[i - 1] * nums[i], Math.min(dpMax[i - 1] * nums[i], nums[i])); - max = Math.max(max, dpMax[i]); - } - return max; -} -``` - -当然,动态规划的老问题,我们注意到更新 `dp[i]` 的时候,我们只用到 `dp[i-1]` 的信息,再之前的信息就用不到了。所以我们完全不需要一个数组,只需要一个变量去重复覆盖更新即可。 - -```java -public int maxProduct(int[] nums) { - int n = nums.length; - if (n == 0) { - return 0; - } - int dpMax = nums[0]; - int dpMin = nums[0]; - int max = nums[0]; - for (int i = 1; i < n; i++) { - //更新 dpMin 的时候需要 dpMax 之前的信息,所以先保存起来 - int preMax = dpMax; - dpMax = Math.max(dpMin * nums[i], Math.max(dpMax * nums[i], nums[i])); - dpMin = Math.min(dpMin * nums[i], Math.min(preMax * nums[i], nums[i])); - max = Math.max(max, dpMax); - } - return max; -} -``` - -# 解法二 - -仔细想一个这个题在考什么,我们先把题目简单化,以方便理清思路。 - -如果给定的数组全部都是正数,那么子数组最大的乘积是多少呢?很简单,把所有的数字相乘即可。 - -但如果给定的数组存在负数呢,似乎这就变得麻烦些了。 - -我们继续简化问题,如果出现了偶数个负数呢?此时最大乘积又变成了,把所有的数字相乘即可。 - -所以,其实我们需要解决的问题就是,当出现奇数个负数的时候该怎么办。 - -乘积理论上乘的数越多越好,但前提是必须保证负数是偶数个。 - -那么对于一个有奇数个负数的数组,基于上边的原则,最大数的取值情况就是两种。 - -第一种,如下图,不包含最后一个负数的子数组。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/152_2.jpg) - -第二种,如下图,不包含第一个负数的子数组。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/152_3.jpg) - -综上所述,最大值要么是全部数字相乘,要么是上边的两种情况。 - -写代码的话,我们如果考虑当前负数是偶数个还是奇数个,第几次遇到负数,当前是否要累乘,就会变得很复杂很复杂,比如下边的代码(就不要理解下边的代码了)。 - -```java -public int maxProduct(int[] nums) { - if (nums.length == 0) { - return 0; - } - if (nums.length == 1) { - return nums[0]; - } - int max_even = 1; - boolean flag = false; - boolean update = false; - int max = 0; - int max_odd = 1; - for (int i = 0; i < nums.length; i++) { - max_even *= nums[i]; - max = Math.max(max, max_even); - if (nums[i] == 0) { - - if (update) { - max = Math.max(max, max_odd); - } - max_even = 1; - max_odd = 1; - flag = false; - update = false; - continue; - } - if (flag) { - max_odd *= nums[i]; - update = true; - continue; - } - if (nums[i] < 0) { - flag = true; - } - } - if (update) { - - max = Math.max(max, max_odd); - } - flag = false; - update = false; - max_odd = 1; - for (int i = nums.length - 1; i >= 0; i--) { - if (nums[i] == 0) { - if (update) { - max = Math.max(max, max_odd); - } - max_odd = 1; - flag = false; - update = false; - continue; - } - if (flag) { - max_odd *= nums[i]; - update = true; - continue; - } - if (nums[i] < 0) { - flag = true; - } - } - if (update) { - max = Math.max(max, max_odd); - } - - return max; -} -``` - -事实上,和解法一一样,我们只要保证计算过程中包含了上边讨论的三种情况即可。 - -对于负数是奇数个的情况,我们采用正着遍历,倒着遍历的技巧即可。 - -```java -public int maxProduct(int[] nums) { - if (nums.length == 0) { - return 0; - } - int max = 1; - int res = nums[0]; - //包含了所有数相乘的情况 - //奇数个负数的情况一 - for (int i = 0; i < nums.length; i++) { - max *= nums[i]; - res = Math.max(res, max); - } - max = 1; - //奇数个负数的情况二 - for (int i = nums.length - 1; i >= 0; i--) { - max *= nums[i]; - res = Math.max(res, max); - } - - return res; -} -``` - -不过代码还没有结束,我们只考虑了负数和正数,没有考虑 `0`。如果有 `0` 存在的话,会使得上边的代码到 `0` 的位置之后 `max` 就一直变成 `0` 了。 - -修正这个问题可以用一个直接的方式,把数组看成下边的样子。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/152_4.jpg) - -把数组看成几个都不含有 `0` 的子数组进行解决即可。 - -代码中,我们只需要在遇到零的时候,把 `max` 再初始化为 `1` 即可。 - -```java -public int maxProduct(int[] nums) { - if (nums.length == 0) { - return 0; - } - - int max = 1; - int res = nums[0]; - for (int i = 0; i < nums.length; i++) { - max *= nums[i]; - res = Math.max(res, max); - if (max == 0) { - max = 1; - } - - } - max = 1; - for (int i = nums.length - 1; i >= 0; i--) { - max *= nums[i]; - res = Math.max(res, max); - if (max == 0) { - max = 1; - } - } - - return res; -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/152.jpg) + +找一个连续的子数组,使得连乘起来最大。 + +# 解法一 动态规划 + +开始没有往这方面想,直接想到了解法二,一会儿讲。看到 [这里](https://leetcode.com/problems/maximum-product-subarray/discuss/48230/Possibly-simplest-solution-with-O(n)-time-complexity),才想起来直接用动态规划解就可以,和 [53 题](https://leetcode.wang/leetCode-53-Maximum-Subarray.html) 子数组最大的和思路差不多。 + +我们先定义一个数组 `dpMax`,用 `dpMax[i]` 表示以第 `i` 个元素的结尾的子数组,乘积最大的值,也就是这个数组必须包含第 `i` 个元素。 + +那么 `dpMax[i]` 的话有几种取值。 + +* 当 `nums[i] >= 0` 并且`dpMax[i-1] > 0`,`dpMax[i] = dpMax[i-1] * nums[i]` +* 当 `nums[i] >= 0` 并且`dpMax[i-1] < 0`,此时如果和前边的数累乘的话,会变成负数,所以`dpMax[i] = nums[i]` +* 当 `nums[i] < 0`,此时如果前边累乘结果是一个很大的负数,和当前负数累乘的话就会变成一个更大的数。所以我们还需要一个数组 `dpMin` 来记录以第 `i` 个元素的结尾的子数组,乘积最小的值。 + * 当`dpMin[i-1] < 0`,`dpMax[i] = dpMin[i-1] * nums[i]` + * 当`dpMin[i-1] >= 0`,`dpMax[i] = nums[i]` + +当然,上边引入了 `dpMin` 数组,怎么求 `dpMin` 其实和上边求 `dpMax` 的过程其实是一样的。 + +按上边的分析,我们就需要加很多的 `if else`来判断不同的情况,这里可以用个技巧。 + +我们注意到上边`dpMax[i]` 的取值无非就是三种,`dpMax[i-1] * nums[i]`、`dpMin[i-1] * nums[i]` 以及 `nums[i]`。 + +所以我们更新的时候,无需去区分当前是哪种情况,只需要从三个取值中选一个最大的即可。 + +```java +dpMax[i] = max(dpMax[i-1] * nums[i], dpMin[i-1] * nums[i], nums[i]); +``` + +求 `dpMin[i]` 同理。 + +```java +dpMin[i] = min(dpMax[i-1] * nums[i], dpMin[i-1] * nums[i], nums[i]); +``` + +更新过程中,我们可以用一个变量 `max` 去保存当前得到的最大值。 + +```java +public int maxProduct(int[] nums) { + int n = nums.length; + if (n == 0) { + return 0; + } + + int[] dpMax = new int[n]; + dpMax[0] = nums[0]; + int[] dpMin = new int[n]; + dpMin[0] = nums[0]; + int max = nums[0]; + for (int i = 1; i < n; i++) { + dpMax[i] = Math.max(dpMin[i - 1] * nums[i], Math.max(dpMax[i - 1] * nums[i], nums[i])); + dpMin[i] = Math.min(dpMin[i - 1] * nums[i], Math.min(dpMax[i - 1] * nums[i], nums[i])); + max = Math.max(max, dpMax[i]); + } + return max; +} +``` + +当然,动态规划的老问题,我们注意到更新 `dp[i]` 的时候,我们只用到 `dp[i-1]` 的信息,再之前的信息就用不到了。所以我们完全不需要一个数组,只需要一个变量去重复覆盖更新即可。 + +```java +public int maxProduct(int[] nums) { + int n = nums.length; + if (n == 0) { + return 0; + } + int dpMax = nums[0]; + int dpMin = nums[0]; + int max = nums[0]; + for (int i = 1; i < n; i++) { + //更新 dpMin 的时候需要 dpMax 之前的信息,所以先保存起来 + int preMax = dpMax; + dpMax = Math.max(dpMin * nums[i], Math.max(dpMax * nums[i], nums[i])); + dpMin = Math.min(dpMin * nums[i], Math.min(preMax * nums[i], nums[i])); + max = Math.max(max, dpMax); + } + return max; +} +``` + +# 解法二 + +仔细想一个这个题在考什么,我们先把题目简单化,以方便理清思路。 + +如果给定的数组全部都是正数,那么子数组最大的乘积是多少呢?很简单,把所有的数字相乘即可。 + +但如果给定的数组存在负数呢,似乎这就变得麻烦些了。 + +我们继续简化问题,如果出现了偶数个负数呢?此时最大乘积又变成了,把所有的数字相乘即可。 + +所以,其实我们需要解决的问题就是,当出现奇数个负数的时候该怎么办。 + +乘积理论上乘的数越多越好,但前提是必须保证负数是偶数个。 + +那么对于一个有奇数个负数的数组,基于上边的原则,最大数的取值情况就是两种。 + +第一种,如下图,不包含最后一个负数的子数组。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/152_2.jpg) + +第二种,如下图,不包含第一个负数的子数组。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/152_3.jpg) + +综上所述,最大值要么是全部数字相乘,要么是上边的两种情况。 + +写代码的话,我们如果考虑当前负数是偶数个还是奇数个,第几次遇到负数,当前是否要累乘,就会变得很复杂很复杂,比如下边的代码(就不要理解下边的代码了)。 + +```java +public int maxProduct(int[] nums) { + if (nums.length == 0) { + return 0; + } + if (nums.length == 1) { + return nums[0]; + } + int max_even = 1; + boolean flag = false; + boolean update = false; + int max = 0; + int max_odd = 1; + for (int i = 0; i < nums.length; i++) { + max_even *= nums[i]; + max = Math.max(max, max_even); + if (nums[i] == 0) { + + if (update) { + max = Math.max(max, max_odd); + } + max_even = 1; + max_odd = 1; + flag = false; + update = false; + continue; + } + if (flag) { + max_odd *= nums[i]; + update = true; + continue; + } + if (nums[i] < 0) { + flag = true; + } + } + if (update) { + + max = Math.max(max, max_odd); + } + flag = false; + update = false; + max_odd = 1; + for (int i = nums.length - 1; i >= 0; i--) { + if (nums[i] == 0) { + if (update) { + max = Math.max(max, max_odd); + } + max_odd = 1; + flag = false; + update = false; + continue; + } + if (flag) { + max_odd *= nums[i]; + update = true; + continue; + } + if (nums[i] < 0) { + flag = true; + } + } + if (update) { + max = Math.max(max, max_odd); + } + + return max; +} +``` + +事实上,和解法一一样,我们只要保证计算过程中包含了上边讨论的三种情况即可。 + +对于负数是奇数个的情况,我们采用正着遍历,倒着遍历的技巧即可。 + +```java +public int maxProduct(int[] nums) { + if (nums.length == 0) { + return 0; + } + int max = 1; + int res = nums[0]; + //包含了所有数相乘的情况 + //奇数个负数的情况一 + for (int i = 0; i < nums.length; i++) { + max *= nums[i]; + res = Math.max(res, max); + } + max = 1; + //奇数个负数的情况二 + for (int i = nums.length - 1; i >= 0; i--) { + max *= nums[i]; + res = Math.max(res, max); + } + + return res; +} +``` + +不过代码还没有结束,我们只考虑了负数和正数,没有考虑 `0`。如果有 `0` 存在的话,会使得上边的代码到 `0` 的位置之后 `max` 就一直变成 `0` 了。 + +修正这个问题可以用一个直接的方式,把数组看成下边的样子。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/152_4.jpg) + +把数组看成几个都不含有 `0` 的子数组进行解决即可。 + +代码中,我们只需要在遇到零的时候,把 `max` 再初始化为 `1` 即可。 + +```java +public int maxProduct(int[] nums) { + if (nums.length == 0) { + return 0; + } + + int max = 1; + int res = nums[0]; + for (int i = 0; i < nums.length; i++) { + max *= nums[i]; + res = Math.max(res, max); + if (max == 0) { + max = 1; + } + + } + max = 1; + for (int i = nums.length - 1; i >= 0; i--) { + max *= nums[i]; + res = Math.max(res, max); + if (max == 0) { + max = 1; + } + } + + return res; +} +``` + +# 总 + 解法二其实是对问题本质的深挖,正常情况下,我们其实用动态规划的思想去直接求解即可。 \ No newline at end of file diff --git a/leetcode-153-Find-Minimum-in-Rotated-Sorted-Array.md b/leetcode-153-Find-Minimum-in-Rotated-Sorted-Array.md index dc0ee99a3..b31789c8a 100644 --- a/leetcode-153-Find-Minimum-in-Rotated-Sorted-Array.md +++ b/leetcode-153-Find-Minimum-in-Rotated-Sorted-Array.md @@ -1,120 +1,120 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/153.jpg) - -给定一个特殊升序数组,即一个排序好的数组,把前边的若干的个数,一起移动到末尾,找出最小的数字。 - -# 解法一 - -其实之前已经在 [33 题](https://leetcode.wang/leetCode-33-Search-in-Rotated-Sorted-Array.html) 解法一中写过这个解法了,这里直接贴过来。 - -求最小值,遍历一遍当然可以,不过这里提到了有序数组,所以我们可以采取二分的方法去找,二分的方法就要保证每次比较后,去掉一半的元素。 - -这里我们去比较中点和端点值的情况,那么是根据中点和起点比较,还是中点和终点比较呢?我们来分析下。 - -- `mid` 和 `start` 比较 - - `mid > start` : 最小值在左半部分。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/33_5.jpg) - - `mid < start`: 最小值在左半部分。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/33_6.jpg) - - 无论大于还是小于,最小值都在左半部分,所以 `mid` 和 `start` 比较是不可取的。 - -- `mid` 和 `end` 比较 - - `mid` < `end`:最小值在左半部分。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/33_5.jpg) - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/33_6.jpg) - - `mid` > `end`:最小值在右半部分。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/33_7.jpg) - - 所以我们只需要把 `mid` 和 `end` 比较,`mid < end` 丢弃右半部分(更新 `end = mid`),`mid > end` 丢弃左半部分(更新 `start = mid`)。直到 `end` 等于 `start` 时候结束就可以了。 - -但这样会有一个问题的,对于下边的例子,就会遇到死循环了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/33_8.jpg) - -问题出在,当数组剩两个的时候,`mid = (start + end)/ 2`,`mid` 取的就是 `start`。上图的例子, `mid > end`, 更新 `start = mid`,`start` 位置并不会变化。那么下一次 `mid` 的值也不会变,就死循环了。所以,我们要更新 `start = mid + 1`,同时也使得 `start` 指向了最小值。 - -综上,找最小值的代码就出来了。 - -```java -public int findMin(int[] nums) { - int start = 0; - int end = nums.length - 1; - while (start < end) { - int mid = (start + end) >>> 1; - if (nums[mid] > nums[end]) { - start = mid + 1; - } else { - end = mid; - } - } - return nums[start]; -} -``` - -# 解法二 - -解法一中我们把 `mid` 和 `end` 进行比较,那么我们能不能把 `mid` 和 `start` 比较解决问题呢? - -看一下之前的分析。 - -`mid` 和 `start` 比较 - -`mid > start` : 最小值在左半部分或者右半部分。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/33_5.jpg) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/33_7.jpg) - -`mid < start`: 最小值在左半部分。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/33_6.jpg) - -上边的问题就出在了 `mid > start` 中出现了两种情况,如果数组是有序的最小值出现在了左半部分,和`mid < start` 出现了同样的情况。所以我们其实只需要在更新 `start` 和 `end` 之前,判断数组是否已经有序,把这种情况单独考虑。有序的话,直接返回第一个元素即可。 - -```java -public int findMin(int[] nums) { - int start = 0; - int end = nums.length - 1; - while (start < end) { - if (nums[start] < nums[end]) { - return nums[start]; - } - int mid = (start + end) >>> 1; - //必须是大于等于,比如 nums=[9,8],mid 和 start 都指向了 9 - if (nums[mid] >= nums[start]) { - start = mid + 1; - } else { - end = mid; - } - } - return nums[start]; -} -``` - -本质上其实和解法一是一样的。 - -此外还可以思考一个问题,如果给定的数组经过了一次变化,也就是给定的不是有序的,那么我们是不是就不用在过程中判断当前是不是有序数组了? - -答案肯定是否定的了,比如 `nums = [4,5,6,1,2,3]`,经过一次更新,`start` 会指向 `1`,`end` 会指向 `3`,此时就变成有序了,所以在过程中我们必须判断数组是否有序。而解法一的好处就是,即使是有序的,也不影响我们的判断。 - -# 总 - -二分的方法,主要就是要确定丢弃哪一半。 - - - - - - - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/153.jpg) + +给定一个特殊升序数组,即一个排序好的数组,把前边的若干的个数,一起移动到末尾,找出最小的数字。 + +# 解法一 + +其实之前已经在 [33 题](https://leetcode.wang/leetCode-33-Search-in-Rotated-Sorted-Array.html) 解法一中写过这个解法了,这里直接贴过来。 + +求最小值,遍历一遍当然可以,不过这里提到了有序数组,所以我们可以采取二分的方法去找,二分的方法就要保证每次比较后,去掉一半的元素。 + +这里我们去比较中点和端点值的情况,那么是根据中点和起点比较,还是中点和终点比较呢?我们来分析下。 + +- `mid` 和 `start` 比较 + + `mid > start` : 最小值在左半部分。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/33_5.jpg) + + `mid < start`: 最小值在左半部分。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/33_6.jpg) + + 无论大于还是小于,最小值都在左半部分,所以 `mid` 和 `start` 比较是不可取的。 + +- `mid` 和 `end` 比较 + + `mid` < `end`:最小值在左半部分。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/33_5.jpg) + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/33_6.jpg) + + `mid` > `end`:最小值在右半部分。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/33_7.jpg) + + 所以我们只需要把 `mid` 和 `end` 比较,`mid < end` 丢弃右半部分(更新 `end = mid`),`mid > end` 丢弃左半部分(更新 `start = mid`)。直到 `end` 等于 `start` 时候结束就可以了。 + +但这样会有一个问题的,对于下边的例子,就会遇到死循环了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/33_8.jpg) + +问题出在,当数组剩两个的时候,`mid = (start + end)/ 2`,`mid` 取的就是 `start`。上图的例子, `mid > end`, 更新 `start = mid`,`start` 位置并不会变化。那么下一次 `mid` 的值也不会变,就死循环了。所以,我们要更新 `start = mid + 1`,同时也使得 `start` 指向了最小值。 + +综上,找最小值的代码就出来了。 + +```java +public int findMin(int[] nums) { + int start = 0; + int end = nums.length - 1; + while (start < end) { + int mid = (start + end) >>> 1; + if (nums[mid] > nums[end]) { + start = mid + 1; + } else { + end = mid; + } + } + return nums[start]; +} +``` + +# 解法二 + +解法一中我们把 `mid` 和 `end` 进行比较,那么我们能不能把 `mid` 和 `start` 比较解决问题呢? + +看一下之前的分析。 + +`mid` 和 `start` 比较 + +`mid > start` : 最小值在左半部分或者右半部分。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/33_5.jpg) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/33_7.jpg) + +`mid < start`: 最小值在左半部分。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/33_6.jpg) + +上边的问题就出在了 `mid > start` 中出现了两种情况,如果数组是有序的最小值出现在了左半部分,和`mid < start` 出现了同样的情况。所以我们其实只需要在更新 `start` 和 `end` 之前,判断数组是否已经有序,把这种情况单独考虑。有序的话,直接返回第一个元素即可。 + +```java +public int findMin(int[] nums) { + int start = 0; + int end = nums.length - 1; + while (start < end) { + if (nums[start] < nums[end]) { + return nums[start]; + } + int mid = (start + end) >>> 1; + //必须是大于等于,比如 nums=[9,8],mid 和 start 都指向了 9 + if (nums[mid] >= nums[start]) { + start = mid + 1; + } else { + end = mid; + } + } + return nums[start]; +} +``` + +本质上其实和解法一是一样的。 + +此外还可以思考一个问题,如果给定的数组经过了一次变化,也就是给定的不是有序的,那么我们是不是就不用在过程中判断当前是不是有序数组了? + +答案肯定是否定的了,比如 `nums = [4,5,6,1,2,3]`,经过一次更新,`start` 会指向 `1`,`end` 会指向 `3`,此时就变成有序了,所以在过程中我们必须判断数组是否有序。而解法一的好处就是,即使是有序的,也不影响我们的判断。 + +# 总 + +二分的方法,主要就是要确定丢弃哪一半。 + + + + + + + diff --git a/leetcode-154-Find-Minimum-in-Rotated-Sorted-ArrayII.md b/leetcode-154-Find-Minimum-in-Rotated-Sorted-ArrayII.md index a93d104f9..5e38566b0 100644 --- a/leetcode-154-Find-Minimum-in-Rotated-Sorted-ArrayII.md +++ b/leetcode-154-Find-Minimum-in-Rotated-Sorted-ArrayII.md @@ -1,230 +1,230 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/154.jpg) - -[153 题](https://leetcode.wang/leetcode-153-Find-Minimum-in-Rotated-Sorted-Array.html) 升级版,依旧是旋转过的数组,一个排序好的数组,把前边的若干的个数,一起移动到末尾,找出最小的数字。只不过这道题中的数字可能有重复的。 - -# 思路分析 - -想一下 [153 题](https://leetcode.wang/leetcode-153-Find-Minimum-in-Rotated-Sorted-Array.html) 我们怎么做的。 - -`mid` 和 `end` 比较。 - -`mid` < `end`:最小值在左半部分。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/33_5.jpg) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/33_6.jpg) - -`mid` > `end`:最小值在右半部分。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/33_7.jpg) - -所以我们只需要把 `mid` 和 `end` 比较,`mid < end` 丢弃右半部分(更新 `end = mid`),`mid > end` 丢弃左半部分(更新 `start = mid + 1`)。直到 `end` 等于 `start` 时候结束就可以了。 - -之前没有重复的数字,所以 `mid` 要么大于 `end` ,要么小于 `end`。这里的话,就需要考虑如果 `mid == end` 的话,我们该怎么做。代码框架也就出来了。 - -```java -public int findMin(int[] nums) { - int start = 0; - int end = nums.length - 1; - while (start < end) { - int mid = (start + end) >>> 1; - if (nums[mid] > nums[end]) { - start = mid + 1; - } else if (nums[mid] < nums[end]) { - end = mid; - } else { - //添加代码 - } - } - return nums[start]; -} -``` - -可以想几个例子考虑下。 - -```java -3 3 1 2 3 3 3 3 3 - ^ ^ - mid end - -上边的情况, mid == end,此时最小值在 mid 左边 - -3 3 3 3 3 2 3 - ^ ^ - mid end - -上边的情况, mid == end,此时最小值在 mid 右边 - -2 3 3 1 1 1 1 1 1 - ^ ^ - mid end - -上边的情况, mid == end,此时最小值在 mid 左边 -``` - -当相等的时候,最小值可能在左边,也可能在右边,问题的关键就是去解决这个问题。 - -# 想法一 - -我想到的一种解决方案是如果遇到了相等的情况,将 `mid` 指针不停的向左滑动,直到当前的值不再等于 `end`。然后就会有三种情况。 - -* 情况一,移动结束的值小于了 `end`,此时最小值一定在 `mid` 的左边 - - ```java - 3 3 1 2 3 3 3 3 3 - ^ ^ - mid end - ``` - -* 情况二,移动结束的值大于了 `end`,此时最小值一定是结束位置的后一个值 - - ```java - 2 3 3 1 1 1 1 1 1 - ^ ^ - mid end - ``` - -* 情况三,移动到头后依旧是等于 `end`,此时最小值一定在 `mid` 的右边 - - ```java - 3 3 3 3 3 2 3 - ^ ^ - mid end - ``` - -大于小于等于也只有上边的三种情况了,所以代码也就出来了。 - -```java -public int findMin(int[] nums) { - int start = 0; - int end = nums.length - 1; - while (start < end) { - int mid = (start + end) >>> 1; - if (nums[mid] > nums[end]) { - start = mid + 1; - } else if (nums[mid] < nums[end]) { - end = mid; - } else { - int temp = mid; - //不停前移 - while (temp >= 0) { - if (nums[temp] == nums[end]) { - temp--; - //情况一 - } else if (nums[temp] < nums[end]) { - end = mid; - break; - //情况二 - } else { - return nums[temp + 1]; - } - } - //情况三 - if (temp == -1) { - start = mid + 1; - } - } - } - return nums[start]; -} -``` - -# 想法二 - -参考 [这里](https://leetcode.com/problems/find-minimum-in-rotated-sorted-array-ii/discuss/48808/My-pretty-simple-code-to-solve-it) ,非常简洁优雅。 - -主要思想就是把问题转换回 [153 题](https://leetcode.wang/leetcode-153-Find-Minimum-in-Rotated-Sorted-Array.html) ,同样也是解决相等的时候怎么办。 - -只做一件事,`end--`。因为 `mid` 和 `end` 相等,所以我们直接把 `end` 抛弃一定不会影响结果的。 - -```java -public int findMin(int[] nums) { - int start = 0; - int end = nums.length - 1; - while (start < end) { - int mid = (start + end) >>> 1; - if (nums[mid] > nums[end]) { - start = mid + 1; - } else if (nums[mid] < nums[end]) { - end = mid; - } else { - end--; - } - } - return nums[start]; -} -``` - -# 想法三 - -参考 [这里](https://leetcode.com/problems/find-minimum-in-rotated-sorted-array-ii/discuss/48815/Only-two-more-lines-code-on-top-of-the-solution-for-Part-I) 。 - -首先根据题目的意思,数组是被分成了两段。 - -当有重复数字的时候,之所以麻烦,就是因为有一个重复数字可能会被切割在两段里,比如下边的例子。 - -```java -3 3 1 2 3 3 3 3 3 - ^ ^ - mid end - -分成了 3 3 和 1 2 3 3 3 3 两段,3 同时处于两段 - -3 3 3 3 3 2 3 - ^ ^ - mid end - -分成了 3 3 3 3 和 2 3 两段,3 同时处于两段 -``` - -而如果所有重复的数字只处于一段里,其实并不复杂,比如下边的例子 - -```java -1 2 3 3 3 3 - ^ ^ - mid end - -只有一段 1 2 3 3 3 3 - -7 7 6 6 6 - ^ ^ - mid end -分成了 7 7 和 6 6 6 两段,没有数字处于两段里 -``` - -对于上边的情况,其实我们可以确定,当 `mid` 和 `end` 相等的时候,最小值一定在 `start` 到 `mid` 之间。也就是和 `mid < end` 的情况一致的。 - -所以我们可以做一个预处理,保证所有重复数字不在两段里出现即可,再简单化,也就是保证切割的位置不要是重复数字。也就是比较 `start` 和 `end` 是否相同,相同的话 `end--` 即可。 - -```java -while (nums[end] == nums[start] && end > start) { - end--; -} -``` - -最后只要把 [153 题](https://leetcode.wang/leetcode-153-Find-Minimum-in-Rotated-Sorted-Array.html) 的代码拿过来即可。 - -```java -public int findMin(int[] nums) { - int start = 0; - int end = nums.length - 1; - while (nums[end] == nums[start] && end > start) { - end--; - } - while (start < end) { - int mid = (start + end) >>> 1; - if (nums[mid] > nums[end]) { - start = mid + 1; - } else{ - end = mid; - } - } - return nums[start]; -} -``` - -# 总 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/154.jpg) + +[153 题](https://leetcode.wang/leetcode-153-Find-Minimum-in-Rotated-Sorted-Array.html) 升级版,依旧是旋转过的数组,一个排序好的数组,把前边的若干的个数,一起移动到末尾,找出最小的数字。只不过这道题中的数字可能有重复的。 + +# 思路分析 + +想一下 [153 题](https://leetcode.wang/leetcode-153-Find-Minimum-in-Rotated-Sorted-Array.html) 我们怎么做的。 + +`mid` 和 `end` 比较。 + +`mid` < `end`:最小值在左半部分。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/33_5.jpg) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/33_6.jpg) + +`mid` > `end`:最小值在右半部分。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/33_7.jpg) + +所以我们只需要把 `mid` 和 `end` 比较,`mid < end` 丢弃右半部分(更新 `end = mid`),`mid > end` 丢弃左半部分(更新 `start = mid + 1`)。直到 `end` 等于 `start` 时候结束就可以了。 + +之前没有重复的数字,所以 `mid` 要么大于 `end` ,要么小于 `end`。这里的话,就需要考虑如果 `mid == end` 的话,我们该怎么做。代码框架也就出来了。 + +```java +public int findMin(int[] nums) { + int start = 0; + int end = nums.length - 1; + while (start < end) { + int mid = (start + end) >>> 1; + if (nums[mid] > nums[end]) { + start = mid + 1; + } else if (nums[mid] < nums[end]) { + end = mid; + } else { + //添加代码 + } + } + return nums[start]; +} +``` + +可以想几个例子考虑下。 + +```java +3 3 1 2 3 3 3 3 3 + ^ ^ + mid end + +上边的情况, mid == end,此时最小值在 mid 左边 + +3 3 3 3 3 2 3 + ^ ^ + mid end + +上边的情况, mid == end,此时最小值在 mid 右边 + +2 3 3 1 1 1 1 1 1 + ^ ^ + mid end + +上边的情况, mid == end,此时最小值在 mid 左边 +``` + +当相等的时候,最小值可能在左边,也可能在右边,问题的关键就是去解决这个问题。 + +# 想法一 + +我想到的一种解决方案是如果遇到了相等的情况,将 `mid` 指针不停的向左滑动,直到当前的值不再等于 `end`。然后就会有三种情况。 + +* 情况一,移动结束的值小于了 `end`,此时最小值一定在 `mid` 的左边 + + ```java + 3 3 1 2 3 3 3 3 3 + ^ ^ + mid end + ``` + +* 情况二,移动结束的值大于了 `end`,此时最小值一定是结束位置的后一个值 + + ```java + 2 3 3 1 1 1 1 1 1 + ^ ^ + mid end + ``` + +* 情况三,移动到头后依旧是等于 `end`,此时最小值一定在 `mid` 的右边 + + ```java + 3 3 3 3 3 2 3 + ^ ^ + mid end + ``` + +大于小于等于也只有上边的三种情况了,所以代码也就出来了。 + +```java +public int findMin(int[] nums) { + int start = 0; + int end = nums.length - 1; + while (start < end) { + int mid = (start + end) >>> 1; + if (nums[mid] > nums[end]) { + start = mid + 1; + } else if (nums[mid] < nums[end]) { + end = mid; + } else { + int temp = mid; + //不停前移 + while (temp >= 0) { + if (nums[temp] == nums[end]) { + temp--; + //情况一 + } else if (nums[temp] < nums[end]) { + end = mid; + break; + //情况二 + } else { + return nums[temp + 1]; + } + } + //情况三 + if (temp == -1) { + start = mid + 1; + } + } + } + return nums[start]; +} +``` + +# 想法二 + +参考 [这里](https://leetcode.com/problems/find-minimum-in-rotated-sorted-array-ii/discuss/48808/My-pretty-simple-code-to-solve-it) ,非常简洁优雅。 + +主要思想就是把问题转换回 [153 题](https://leetcode.wang/leetcode-153-Find-Minimum-in-Rotated-Sorted-Array.html) ,同样也是解决相等的时候怎么办。 + +只做一件事,`end--`。因为 `mid` 和 `end` 相等,所以我们直接把 `end` 抛弃一定不会影响结果的。 + +```java +public int findMin(int[] nums) { + int start = 0; + int end = nums.length - 1; + while (start < end) { + int mid = (start + end) >>> 1; + if (nums[mid] > nums[end]) { + start = mid + 1; + } else if (nums[mid] < nums[end]) { + end = mid; + } else { + end--; + } + } + return nums[start]; +} +``` + +# 想法三 + +参考 [这里](https://leetcode.com/problems/find-minimum-in-rotated-sorted-array-ii/discuss/48815/Only-two-more-lines-code-on-top-of-the-solution-for-Part-I) 。 + +首先根据题目的意思,数组是被分成了两段。 + +当有重复数字的时候,之所以麻烦,就是因为有一个重复数字可能会被切割在两段里,比如下边的例子。 + +```java +3 3 1 2 3 3 3 3 3 + ^ ^ + mid end + +分成了 3 3 和 1 2 3 3 3 3 两段,3 同时处于两段 + +3 3 3 3 3 2 3 + ^ ^ + mid end + +分成了 3 3 3 3 和 2 3 两段,3 同时处于两段 +``` + +而如果所有重复的数字只处于一段里,其实并不复杂,比如下边的例子 + +```java +1 2 3 3 3 3 + ^ ^ + mid end + +只有一段 1 2 3 3 3 3 + +7 7 6 6 6 + ^ ^ + mid end +分成了 7 7 和 6 6 6 两段,没有数字处于两段里 +``` + +对于上边的情况,其实我们可以确定,当 `mid` 和 `end` 相等的时候,最小值一定在 `start` 到 `mid` 之间。也就是和 `mid < end` 的情况一致的。 + +所以我们可以做一个预处理,保证所有重复数字不在两段里出现即可,再简单化,也就是保证切割的位置不要是重复数字。也就是比较 `start` 和 `end` 是否相同,相同的话 `end--` 即可。 + +```java +while (nums[end] == nums[start] && end > start) { + end--; +} +``` + +最后只要把 [153 题](https://leetcode.wang/leetcode-153-Find-Minimum-in-Rotated-Sorted-Array.html) 的代码拿过来即可。 + +```java +public int findMin(int[] nums) { + int start = 0; + int end = nums.length - 1; + while (nums[end] == nums[start] && end > start) { + end--; + } + while (start < end) { + int mid = (start + end) >>> 1; + if (nums[mid] > nums[end]) { + start = mid + 1; + } else{ + end = mid; + } + } + return nums[start]; +} +``` + +# 总 + 思路一主要是对新出现的情况进行细分来解决问题,思路二和思路三是把问题向原先的问题靠拢,从而尽可能少的变动了代码。不过这道题由于一些情况很特殊,所以虽然用了二分,最坏时间复杂度也降到了 `O(n)`,不再是 `log` 级的。 \ No newline at end of file diff --git a/leetcode-155-Min-Stack.md b/leetcode-155-Min-Stack.md index ec7962a82..ef3fec7c3 100644 --- a/leetcode-155-Min-Stack.md +++ b/leetcode-155-Min-Stack.md @@ -1,399 +1,399 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/155.jpg) - -设计数据结构的题,设计一个栈,除了栈特有的功能,入栈、出栈、查看栈顶元素,还需要增加一个功能,得到当前栈里边最小的元素。 - -# 解法一 - -要实现一个 `stack`,那么我们还能用 `java` 自带的 `stack` 吗?也不用纠结,这道题的关键其实是实现「得到最小值这个功能」,所以为了代码简洁些,我们就直接使用系统自带的 `stack` 了。 - -这道题最直接的解法就是我们可以用两个栈,一个栈去保存正常的入栈出栈的值,另一个栈去存最小值,也就是用栈顶保存当前所有元素的最小值。存最小值的栈的具体操作流程如下: - -将第一个元素入栈。 - -新加入的元素如果大于栈顶元素,那么新加入的元素就不处理。 - -新加入的元素如果小于等于栈顶元素,那么就将新元素入栈。 - -出栈元素不等于栈顶元素,不操作。 - -出栈元素等于栈顶元素,那么就将栈顶元素出栈。 - -举个例子。 - -```java -入栈 3 -| | | | -| | | | -|_3_| |_3_| -stack minStack - -入栈 5 , 5 大于 minStack 栈顶,不处理 -| | | | -| 5 | | | -|_3_| |_3_| -stack minStack - -入栈 2 ,此时右边的 minStack 栈顶就保存了当前最小值 2 -| 2 | | | -| 5 | | 2 | -|_3_| |_3_| -stack minStack - -出栈 2,此时右边的 minStack 栈顶就保存了当前最小值 3 -| | | | -| 5 | | | -|_3_| |_3_| -stack minStack - -出栈 5,右边 minStack 不处理 -| | | | -| | | | -|_3_| |_3_| -stack minStack - -出栈 3 -| | | | -| | | | -|_ _| |_ _| -stack minStack -``` - -代码的话就很好写了。 - -```java -class MinStack { - /** initialize your data structure here. */ - private Stack stack; - private Stack minStack; - - public MinStack() { - stack = new Stack<>(); - minStack = new Stack<>(); - } - - public void push(int x) { - stack.push(x); - if (!minStack.isEmpty()) { - int top = minStack.peek(); - //小于的时候才入栈 - if (x <= top) { - minStack.push(x); - } - }else{ - minStack.push(x); - } - } - - public void pop() { - int pop = stack.pop(); - - int top = minStack.peek(); - //等于的时候再出栈 - if (pop == top) { - minStack.pop(); - } - - } - - public int top() { - return stack.peek(); - } - - public int getMin() { - return minStack.peek(); - } -} -``` - -# 解法二 - -解法一中用了两个栈去实现,那么我们能不能用一个栈去实现呢? - -参考了 [这里](https://leetcode.com/problems/min-stack/discuss/49014/Java-accepted-solution-using-one-stack)。 - -解法一中单独用了一个栈去保存所有最小值,那么我们能不能只用一个变量去保存最小值呢? - -再看一下上边的例子。 - -```java -入栈 3 -| | min = 3 -| | -|_3_| -stack - -入栈 5 -| | min = 3 -| 5 | -|_3_| -stack - -入栈 2 -| 2 | min = 2? -| 5 | -|_3_| -stack -``` - -如果只用一个变量就会遇到一个问题,如果把 `min` 更新为 `2`,那么之前的最小值 `3` 就丢失了。 - -怎么把 `3` 保存起来呢?把它在 `2` 之前压入栈中即可。 - -```java -入栈 2 ,同时将之前的 min 值 3 入栈,再把 2 入栈,同时更新 min = 2 -| 2 | min = 2 -| 3 | -| 5 | -|_3_| -stack - -入栈 6 -| 6 | min = 2 -| 2 | -| 3 | -| 5 | -|_3_| -stack - -出栈 6 -| 2 | min = 2 -| 3 | -| 5 | -|_3_| -stack - -出栈 2 -| 2 | min = 2 -| 3 | -| 5 | -|_3_| -stack -``` - -上边的最后一个状态,当出栈元素是最小元素我们该如何处理呢? - -我们只需要把 `2` 出栈,然后再出栈一次,把 `3` 赋值给 `min` 即可。 - -```java -出栈 2 -| | min = 3 -| 5 | -|_3_| -stack -``` - -通过上边的方式,我们就只需要一个栈了。当有更小的值来的时候,我们只需要把之前的最小值入栈,当前更小的值再入栈即可。当这个最小值要出栈的时候,下一个值便是之前的最小值了。 - -```java -class MinStack { - int min = Integer.MAX_VALUE; - Stack stack = new Stack(); - public void push(int x) { - //当前值更小 - if(x <= min){ - //将之前的最小值保存 - stack.push(min); - //更新最小值 - min=x; - } - stack.push(x); - } - - public void pop() { - //如果弹出的值是最小值,那么将下一个元素更新为最小值 - if(stack.pop() == min) { - min=stack.pop(); - } - } - - public int top() { - return stack.peek(); - } - - public int getMin() { - return min; - } -} -``` - -# 解法三 - -参考 [这里](https://leetcode.com/problems/min-stack/discuss/49031/Share-my-Java-solution-with-ONLY-ONE-stack),再分享利用一个栈的另一种思路。 - -通过解法二的分析,我们关键要解决的问题就是当有新的更小值的时候,之前的最小值该怎么办? - -解法二中通过把之前的最小值入栈解决问题。 - -这里的话,用了另一种思路。同样是用一个 `min` 变量保存最小值。只不过栈里边我们不去保存原来的值,而是去存储入栈的值和最小值的差值。然后得到之前的最小值的话,我们就可以通过 `min` 值和栈顶元素得到,举个例子。 - -```java -入栈 3,存入 3 - 3 = 0 -| | min = 3 -| | -|_0_| -stack - -入栈 5,存入 5 - 3 = 2 -| | min = 3 -| 2 | -|_0_| -stack - -入栈 2,因为出现了更小的数,所以我们会存入一个负数,这里很关键 -也就是存入 2 - 3 = -1, 并且更新 min = 2 -对于之前的 min 值 3, 我们只需要用更新后的 min - 栈顶元素 -1 就可以得到 -| -1| min = 2 -| 5 | -|_3_| -stack - -入栈 6,存入 6 - 2 = 4 -| 4 | min = 2 -| -1| -| 5 | -|_3_| -stack - -出栈,返回的值就是栈顶元素 4 加上 min,就是 6 -| | min = 2 -| -1| -| 5 | -|_3_| -stack - -出栈,此时栈顶元素是负数,说明之前对 min 值进行了更新。 -入栈元素 - min = 栈顶元素,入栈元素其实就是当前的 min 值 2 -所以更新前的 min 就等于入栈元素 2 - 栈顶元素(-1) = 3 -| | min = 3 -| 5 | -|_3_| -stack -``` - -再理一下上边的思路,我们每次存入的是 `原来值 - 当前最小值`。 - -当原来值大于等于当前最小值的时候,我们存入的肯定就是非负数,所以出栈的时候就是 `栈中的值 + 当前最小值` 。 - -当原来值小于当前最小值的时候,我们存入的肯定就是负值,此时的值我们不入栈,用 `min` 保存起来,同时将差值入栈。 - -当后续如果出栈元素是负数的时候,那么要出栈的元素其实就是 `min`。此外之前的 `min` 值,我们可以通过栈顶的值和当前 `min` 值进行还原,就是用 `min` 减去栈顶元素即可。 - -```java -public class MinStack { - long min; - Stack stack; - - public MinStack(){ - stack=new Stack<>(); - } - - public void push(int x) { - if (stack.isEmpty()) { - min = x; - stack.push(x - min); - } else { - stack.push(x - min); - if (x < min){ - min = x; // 更新最小值 - } - - } - } - - public void pop() { - if (stack.isEmpty()) - return; - - long pop = stack.pop(); - - //弹出的是负值,要更新 min - if (pop < 0) { - min = min - pop; - } - - } - - public int top() { - long top = stack.peek(); - //负数的话,出栈的值保存在 min 中 - if (top < 0) { - return (int) (min); - //出栈元素加上最小值即可 - } else { - return (int) (top + min); - } - } - - public int getMin() { - return (int) min; - } -} -``` - -上边的解法的一个缺点就是由于我们保存的是差值,所以可能造成溢出,所以我们用了数据范围更大的 `long` 类型。 - -此外相对于解法二,最小值需要更新的时候,我们并没有将之前的最小值存起来,我们每次都是通过当前最小值和栈顶元素推出了之前的最小值,所以会省一些空间。 - -# 解法四 - -再分享一个有趣的解法,参考 [这里](https://leetcode.com/problems/min-stack/discuss/49217/6ms-Java-Solution-using-Linked-List.-Clean-self-explanatory-and-efficient.) 。 - -回到最初的疑虑,我们要不要用 `java` 提供的 `stack` 。如果不用的话,可以怎么做的? - -直接用一个链表即可实现栈的基本功能,那么最小值该怎么得到呢?我们可以在 `Node` 节点中增加一个 `min` 字段,这样的话每次加入一个节点的时候,我们同时只要确定它的 `min` 值即可。 - -代码很简洁,我直接把代码贴过来吧。 - -```java -class MinStack { - class Node{ - int value; - int min; - Node next; - - Node(int x, int min){ - this.value=x; - this.min=min; - next = null; - } - } - Node head; - //每次加入的节点放到头部 - public void push(int x) { - if(null==head){ - head = new Node(x,x); - }else{ - //当前值和之前头结点的最小值较小的做为当前的 min - Node n = new Node(x, Math.min(x,head.min)); - n.next=head; - head=n; - } - } - - public void pop() { - if(head!=null) - head =head.next; - } - - public int top() { - if(head!=null) - return head.value; - return -1; - } - - public int getMin() { - if(null!=head) - return head.min; - return -1; - } -} -``` - -# 总 - -虽然题目比较简单,但解法二和解法三真的让人耳目一新,一个通过存储,一个通过差值解决了「保存之前值」的问题,思路很值得借鉴。解法四更像降维打击一样,回到改底层数据结构,从而更加简洁的解决了问题。 - - - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/155.jpg) + +设计数据结构的题,设计一个栈,除了栈特有的功能,入栈、出栈、查看栈顶元素,还需要增加一个功能,得到当前栈里边最小的元素。 + +# 解法一 + +要实现一个 `stack`,那么我们还能用 `java` 自带的 `stack` 吗?也不用纠结,这道题的关键其实是实现「得到最小值这个功能」,所以为了代码简洁些,我们就直接使用系统自带的 `stack` 了。 + +这道题最直接的解法就是我们可以用两个栈,一个栈去保存正常的入栈出栈的值,另一个栈去存最小值,也就是用栈顶保存当前所有元素的最小值。存最小值的栈的具体操作流程如下: + +将第一个元素入栈。 + +新加入的元素如果大于栈顶元素,那么新加入的元素就不处理。 + +新加入的元素如果小于等于栈顶元素,那么就将新元素入栈。 + +出栈元素不等于栈顶元素,不操作。 + +出栈元素等于栈顶元素,那么就将栈顶元素出栈。 + +举个例子。 + +```java +入栈 3 +| | | | +| | | | +|_3_| |_3_| +stack minStack + +入栈 5 , 5 大于 minStack 栈顶,不处理 +| | | | +| 5 | | | +|_3_| |_3_| +stack minStack + +入栈 2 ,此时右边的 minStack 栈顶就保存了当前最小值 2 +| 2 | | | +| 5 | | 2 | +|_3_| |_3_| +stack minStack + +出栈 2,此时右边的 minStack 栈顶就保存了当前最小值 3 +| | | | +| 5 | | | +|_3_| |_3_| +stack minStack + +出栈 5,右边 minStack 不处理 +| | | | +| | | | +|_3_| |_3_| +stack minStack + +出栈 3 +| | | | +| | | | +|_ _| |_ _| +stack minStack +``` + +代码的话就很好写了。 + +```java +class MinStack { + /** initialize your data structure here. */ + private Stack stack; + private Stack minStack; + + public MinStack() { + stack = new Stack<>(); + minStack = new Stack<>(); + } + + public void push(int x) { + stack.push(x); + if (!minStack.isEmpty()) { + int top = minStack.peek(); + //小于的时候才入栈 + if (x <= top) { + minStack.push(x); + } + }else{ + minStack.push(x); + } + } + + public void pop() { + int pop = stack.pop(); + + int top = minStack.peek(); + //等于的时候再出栈 + if (pop == top) { + minStack.pop(); + } + + } + + public int top() { + return stack.peek(); + } + + public int getMin() { + return minStack.peek(); + } +} +``` + +# 解法二 + +解法一中用了两个栈去实现,那么我们能不能用一个栈去实现呢? + +参考了 [这里](https://leetcode.com/problems/min-stack/discuss/49014/Java-accepted-solution-using-one-stack)。 + +解法一中单独用了一个栈去保存所有最小值,那么我们能不能只用一个变量去保存最小值呢? + +再看一下上边的例子。 + +```java +入栈 3 +| | min = 3 +| | +|_3_| +stack + +入栈 5 +| | min = 3 +| 5 | +|_3_| +stack + +入栈 2 +| 2 | min = 2? +| 5 | +|_3_| +stack +``` + +如果只用一个变量就会遇到一个问题,如果把 `min` 更新为 `2`,那么之前的最小值 `3` 就丢失了。 + +怎么把 `3` 保存起来呢?把它在 `2` 之前压入栈中即可。 + +```java +入栈 2 ,同时将之前的 min 值 3 入栈,再把 2 入栈,同时更新 min = 2 +| 2 | min = 2 +| 3 | +| 5 | +|_3_| +stack + +入栈 6 +| 6 | min = 2 +| 2 | +| 3 | +| 5 | +|_3_| +stack + +出栈 6 +| 2 | min = 2 +| 3 | +| 5 | +|_3_| +stack + +出栈 2 +| 2 | min = 2 +| 3 | +| 5 | +|_3_| +stack +``` + +上边的最后一个状态,当出栈元素是最小元素我们该如何处理呢? + +我们只需要把 `2` 出栈,然后再出栈一次,把 `3` 赋值给 `min` 即可。 + +```java +出栈 2 +| | min = 3 +| 5 | +|_3_| +stack +``` + +通过上边的方式,我们就只需要一个栈了。当有更小的值来的时候,我们只需要把之前的最小值入栈,当前更小的值再入栈即可。当这个最小值要出栈的时候,下一个值便是之前的最小值了。 + +```java +class MinStack { + int min = Integer.MAX_VALUE; + Stack stack = new Stack(); + public void push(int x) { + //当前值更小 + if(x <= min){ + //将之前的最小值保存 + stack.push(min); + //更新最小值 + min=x; + } + stack.push(x); + } + + public void pop() { + //如果弹出的值是最小值,那么将下一个元素更新为最小值 + if(stack.pop() == min) { + min=stack.pop(); + } + } + + public int top() { + return stack.peek(); + } + + public int getMin() { + return min; + } +} +``` + +# 解法三 + +参考 [这里](https://leetcode.com/problems/min-stack/discuss/49031/Share-my-Java-solution-with-ONLY-ONE-stack),再分享利用一个栈的另一种思路。 + +通过解法二的分析,我们关键要解决的问题就是当有新的更小值的时候,之前的最小值该怎么办? + +解法二中通过把之前的最小值入栈解决问题。 + +这里的话,用了另一种思路。同样是用一个 `min` 变量保存最小值。只不过栈里边我们不去保存原来的值,而是去存储入栈的值和最小值的差值。然后得到之前的最小值的话,我们就可以通过 `min` 值和栈顶元素得到,举个例子。 + +```java +入栈 3,存入 3 - 3 = 0 +| | min = 3 +| | +|_0_| +stack + +入栈 5,存入 5 - 3 = 2 +| | min = 3 +| 2 | +|_0_| +stack + +入栈 2,因为出现了更小的数,所以我们会存入一个负数,这里很关键 +也就是存入 2 - 3 = -1, 并且更新 min = 2 +对于之前的 min 值 3, 我们只需要用更新后的 min - 栈顶元素 -1 就可以得到 +| -1| min = 2 +| 5 | +|_3_| +stack + +入栈 6,存入 6 - 2 = 4 +| 4 | min = 2 +| -1| +| 5 | +|_3_| +stack + +出栈,返回的值就是栈顶元素 4 加上 min,就是 6 +| | min = 2 +| -1| +| 5 | +|_3_| +stack + +出栈,此时栈顶元素是负数,说明之前对 min 值进行了更新。 +入栈元素 - min = 栈顶元素,入栈元素其实就是当前的 min 值 2 +所以更新前的 min 就等于入栈元素 2 - 栈顶元素(-1) = 3 +| | min = 3 +| 5 | +|_3_| +stack +``` + +再理一下上边的思路,我们每次存入的是 `原来值 - 当前最小值`。 + +当原来值大于等于当前最小值的时候,我们存入的肯定就是非负数,所以出栈的时候就是 `栈中的值 + 当前最小值` 。 + +当原来值小于当前最小值的时候,我们存入的肯定就是负值,此时的值我们不入栈,用 `min` 保存起来,同时将差值入栈。 + +当后续如果出栈元素是负数的时候,那么要出栈的元素其实就是 `min`。此外之前的 `min` 值,我们可以通过栈顶的值和当前 `min` 值进行还原,就是用 `min` 减去栈顶元素即可。 + +```java +public class MinStack { + long min; + Stack stack; + + public MinStack(){ + stack=new Stack<>(); + } + + public void push(int x) { + if (stack.isEmpty()) { + min = x; + stack.push(x - min); + } else { + stack.push(x - min); + if (x < min){ + min = x; // 更新最小值 + } + + } + } + + public void pop() { + if (stack.isEmpty()) + return; + + long pop = stack.pop(); + + //弹出的是负值,要更新 min + if (pop < 0) { + min = min - pop; + } + + } + + public int top() { + long top = stack.peek(); + //负数的话,出栈的值保存在 min 中 + if (top < 0) { + return (int) (min); + //出栈元素加上最小值即可 + } else { + return (int) (top + min); + } + } + + public int getMin() { + return (int) min; + } +} +``` + +上边的解法的一个缺点就是由于我们保存的是差值,所以可能造成溢出,所以我们用了数据范围更大的 `long` 类型。 + +此外相对于解法二,最小值需要更新的时候,我们并没有将之前的最小值存起来,我们每次都是通过当前最小值和栈顶元素推出了之前的最小值,所以会省一些空间。 + +# 解法四 + +再分享一个有趣的解法,参考 [这里](https://leetcode.com/problems/min-stack/discuss/49217/6ms-Java-Solution-using-Linked-List.-Clean-self-explanatory-and-efficient.) 。 + +回到最初的疑虑,我们要不要用 `java` 提供的 `stack` 。如果不用的话,可以怎么做的? + +直接用一个链表即可实现栈的基本功能,那么最小值该怎么得到呢?我们可以在 `Node` 节点中增加一个 `min` 字段,这样的话每次加入一个节点的时候,我们同时只要确定它的 `min` 值即可。 + +代码很简洁,我直接把代码贴过来吧。 + +```java +class MinStack { + class Node{ + int value; + int min; + Node next; + + Node(int x, int min){ + this.value=x; + this.min=min; + next = null; + } + } + Node head; + //每次加入的节点放到头部 + public void push(int x) { + if(null==head){ + head = new Node(x,x); + }else{ + //当前值和之前头结点的最小值较小的做为当前的 min + Node n = new Node(x, Math.min(x,head.min)); + n.next=head; + head=n; + } + } + + public void pop() { + if(head!=null) + head =head.next; + } + + public int top() { + if(head!=null) + return head.value; + return -1; + } + + public int getMin() { + if(null!=head) + return head.min; + return -1; + } +} +``` + +# 总 + +虽然题目比较简单,但解法二和解法三真的让人耳目一新,一个通过存储,一个通过差值解决了「保存之前值」的问题,思路很值得借鉴。解法四更像降维打击一样,回到改底层数据结构,从而更加简洁的解决了问题。 + + + diff --git a/leetcode-160-Intersection-of-Two-Linked-Lists.md b/leetcode-160-Intersection-of-Two-Linked-Lists.md index 3c3ad5c14..520c0bffc 100644 --- a/leetcode-160-Intersection-of-Two-Linked-Lists.md +++ b/leetcode-160-Intersection-of-Two-Linked-Lists.md @@ -1,195 +1,195 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/160.png) - -两个链表,如果有重合的部分,把相遇点返回。如果没有重合的部分,就返回 `null`。 - -# 思路分析 - -最暴力的方法就是选择链表 A 的每个节点,然后考虑链表 B 能否到达,但时间复杂度会达到 `O(mn)` 。 - -再进行优化的话,可以利用一个 `HashMap`,将链表 A 中所有的节点存入,然后遍历链表 B 的每一个节点,看 `HashMap` 中是否存在即可。时间复杂度优化到了 `O(n)`,但同时需要 `O(n)` 的空间。 - -接下来我们只考虑时间复杂度 `O(n)`,空间复杂度为 `O(1)` 的解法。 - -# 解法一 - -有一些 [142 题](https://leetcode.wang/leetcode-142-Linked-List-CycleII.html)(找出链表环的入口点)解法的影子。我们需要两个指针,分别从两个链表的某个位置开始遍历,当两个指针相遇的时候,刚好停在两个链表的相遇点问题也就解决了。 - -所以最关键的问题就是从链表的某个位置开始遍历,那么从哪个位置呢? - -如果把问题简单化,假如两个链表有重合部分,并且两个链表的总长度相等。那么我们只需要让两个指针分别从链表头遍历即可,也就是下边的例子,指针 `A` 从 `1` 开始遍历,指针 `B` 从 `4` 同时遍历,那么两个指针就会在 `7` 相遇,就是我们要找的位置。 - -```java -1 -> 2 -> 3 - -> 7 -> 8 -> 9 -4 -> 5 -> 6 -``` - -如果两个链表长度不相等呢,比如下边的例子。 - -```java -1 -> 2 -> 3 - -> 7 -> 8 -> 9 -4 -> 5 -> 6 -> 7 -> 8 -``` - -此时短的链表还是从链表头 `1` 开始,但是长的链表就应该先多走 `2 `步,从 `6` 开始。 - -为什么多走 `2` 步呢?很简单,因为没有重合的链表部分 `1 2 3` 和 `4 5 6 7 8`,长度差了 `2`。怎么算出这个长度呢?我们只需要用两个链表的总长度做差即可(原因:重合部分相减为 `0` ,最终结果就相当于不重合部分的差),也就是 `8 - 6 = 2` 。 - -综上,我们只需要算出两个链表的长度,让长的的链表提前走几步,然后再同时开始遍历,相遇点就是我们要找的位置。 - -```java -public ListNode getIntersectionNode(ListNode headA, ListNode headB) { - if (headA == null || headB == null) { - return null; - } - ListNode tailA = headA; - int lenA = 0; - //链表 A 的长度 - while (tailA.next != null) { - tailA = tailA.next; - lenA++; - } - ListNode tailB = headB; - int lenB = 0; - //链表 B 的长度 - while (tailB.next != null) { - tailB = tailB.next; - lenB++; - } - //没有重合部分,直接结束 - if (tailA != tailB) { - return null; - } - //让长的链表提前走 - if (lenA > lenB) { - int sub = lenA - lenB; - while (sub > 0) { - headA = headA.next; - sub--; - } - } else { - int sub = lenB - lenA; - while (sub > 0) { - headB = headB.next; - sub--; - } - } - - //依次遍历,找到相遇点 - while (headA != headB) { - headA = headA.next; - headB = headB.next; - } - return headA; -} -``` - -[这里](https://leetcode.com/problems/intersection-of-two-linked-lists/discuss/49785/Java-solution-without-knowing-the-difference-in-len!) 看到另一种写法,但本质上和上边是一样的,分享一下。 - -```java -public ListNode getIntersectionNode(ListNode headA, ListNode headB) { - if(headA == null || headB == null) return null; - - ListNode a = headA; - ListNode b = headB; - - while( a != b){ - a = a == null? headB : a.next; - b = b == null? headA : b.next; - } - - return a; -} -``` - -上边的代码简洁了很多,它没有去分别求两个链表的长度,而是把所有的情况都合并了起来。 - -* 如果没有重合部分,那么 `a` 和 `b` 在某一时间点 一定会同时走到 `null`,从而结束循环。 - -* 如果有重合部分,分两种情况。 - * 长度相同的话, `a` 和 `b` 一定是同时到达相遇点,然后返回。 - * 长度不同的话,较短的链表先到达结尾,然后指针转向较长的链表。此刻,较长的链表继续向末尾走,多走的距离刚好就是最开始介绍的解法,链表的长度差,走完之后指针转向较短的链表。然后继续走的话,相遇的位置就刚好是相遇点了。 - -综上,代码巧妙的把所有情况合并了起来。 - -# 解法二 - -最开始考虑这道题的时候,我还想了另一种思路。就是遍历某一个链表,把这个链表的每个节点进行标记,这里的话当然就是对 `val` 进行特殊标记。然后再遍历另一个链表,发现了这个标记也就找到了相遇点了。当然,这个标记一定得是可逆的,完成任务后我们要把原来链表的 `val` 进行还原。 - -常用的标记方法,比如取它相反数,取绝对值,异或,找一个不可能存在的数赋值过去等等,但对于这道题都无效。然后自己到这里思路也就断了,后来就想到了上边的解法一。 - -[这里](https://leetcode.com/problems/intersection-of-two-linked-lists/discuss/50030/My-C%2B%2B-Accepted-Solution-with-O(n)-time-and-O(1)-memory-(72ms)) 看到了类似于标记的方法,蛮有意思,分享一下,分下边几步。 - -1. 统计链表 `A` 的所有节点 `val` 的和,记为 `sumA`,同时记录长度 `lenA`。 -2. 把链表 `B` 的所有节点的 `val` 都进行加 `1`。 -3. 再次统计链表 `A` 的所有节点 `val` 的和,记为 `sumA2`。 -4. 将链表 `B` 的所有节点的 `val` 都进行减 `1`,相当于还原。 - -有了上边的几个数据就可以知道。 - -* `sumA == sumA2` 的话,就表明两个链表没有重合部分。 -* `sumA != sumA2` 的话,`sub = sumA2 - sumA`,由于我们对重合部分的 `val` 进行了加 `1`,所以前后的差 `sub` 就刚好表示了重合节点的个数。同时我们知道链表 A 的总长度 `lenA`,所以我们只需要对链表 A 遍历 `lenA - sub` 次,就刚好会走到重合部分的开头了,也就是我们要找的相遇点。 - -代码如下。 - -```java -public ListNode getIntersectionNode(ListNode headA, ListNode headB) { - if (headA == null || headB == null) { - return null; - } - - //步骤 1 - ListNode tailA = headA; - int lenA = 0; - int sumA = 0; - - while (tailA != null) { - sumA += tailA.val; - tailA = tailA.next; - lenA++; - } - - //步骤 2 - ListNode tailB = headB; - while (tailB != null) { - tailB.val = tailB.val + 1; - tailB = tailB.next; - } - - //步骤 3 - tailA = headA; - int sumA2 = 0; - while (tailA != null) { - sumA2 += tailA.val; - tailA = tailA.next; - } - - //步骤 4 - tailB = headB; - while (tailB != null) { - tailB.val = tailB.val - 1; - tailB = tailB.next; - } - - - if (sumA == sumA2) { - return null; - } else { - for (int i = 0; i < lenA - (sumA2 - sumA); i++) { - headA = headA.next; - } - return headA; - } - -} -``` - -上边算法的缺点就是,由于进行了加 `1` 操作,对于过大的数可能会引起溢出,此外求和也很有可能引起溢出。但思想还是很有趣的。 - -# 总 - -遇到题以后,可以对不同的情况分析,回想之前的一些思想,从而找到突破口。 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/160.png) + +两个链表,如果有重合的部分,把相遇点返回。如果没有重合的部分,就返回 `null`。 + +# 思路分析 + +最暴力的方法就是选择链表 A 的每个节点,然后考虑链表 B 能否到达,但时间复杂度会达到 `O(mn)` 。 + +再进行优化的话,可以利用一个 `HashMap`,将链表 A 中所有的节点存入,然后遍历链表 B 的每一个节点,看 `HashMap` 中是否存在即可。时间复杂度优化到了 `O(n)`,但同时需要 `O(n)` 的空间。 + +接下来我们只考虑时间复杂度 `O(n)`,空间复杂度为 `O(1)` 的解法。 + +# 解法一 + +有一些 [142 题](https://leetcode.wang/leetcode-142-Linked-List-CycleII.html)(找出链表环的入口点)解法的影子。我们需要两个指针,分别从两个链表的某个位置开始遍历,当两个指针相遇的时候,刚好停在两个链表的相遇点问题也就解决了。 + +所以最关键的问题就是从链表的某个位置开始遍历,那么从哪个位置呢? + +如果把问题简单化,假如两个链表有重合部分,并且两个链表的总长度相等。那么我们只需要让两个指针分别从链表头遍历即可,也就是下边的例子,指针 `A` 从 `1` 开始遍历,指针 `B` 从 `4` 同时遍历,那么两个指针就会在 `7` 相遇,就是我们要找的位置。 + +```java +1 -> 2 -> 3 + -> 7 -> 8 -> 9 +4 -> 5 -> 6 +``` + +如果两个链表长度不相等呢,比如下边的例子。 + +```java +1 -> 2 -> 3 + -> 7 -> 8 -> 9 +4 -> 5 -> 6 -> 7 -> 8 +``` + +此时短的链表还是从链表头 `1` 开始,但是长的链表就应该先多走 `2 `步,从 `6` 开始。 + +为什么多走 `2` 步呢?很简单,因为没有重合的链表部分 `1 2 3` 和 `4 5 6 7 8`,长度差了 `2`。怎么算出这个长度呢?我们只需要用两个链表的总长度做差即可(原因:重合部分相减为 `0` ,最终结果就相当于不重合部分的差),也就是 `8 - 6 = 2` 。 + +综上,我们只需要算出两个链表的长度,让长的的链表提前走几步,然后再同时开始遍历,相遇点就是我们要找的位置。 + +```java +public ListNode getIntersectionNode(ListNode headA, ListNode headB) { + if (headA == null || headB == null) { + return null; + } + ListNode tailA = headA; + int lenA = 0; + //链表 A 的长度 + while (tailA.next != null) { + tailA = tailA.next; + lenA++; + } + ListNode tailB = headB; + int lenB = 0; + //链表 B 的长度 + while (tailB.next != null) { + tailB = tailB.next; + lenB++; + } + //没有重合部分,直接结束 + if (tailA != tailB) { + return null; + } + //让长的链表提前走 + if (lenA > lenB) { + int sub = lenA - lenB; + while (sub > 0) { + headA = headA.next; + sub--; + } + } else { + int sub = lenB - lenA; + while (sub > 0) { + headB = headB.next; + sub--; + } + } + + //依次遍历,找到相遇点 + while (headA != headB) { + headA = headA.next; + headB = headB.next; + } + return headA; +} +``` + +[这里](https://leetcode.com/problems/intersection-of-two-linked-lists/discuss/49785/Java-solution-without-knowing-the-difference-in-len!) 看到另一种写法,但本质上和上边是一样的,分享一下。 + +```java +public ListNode getIntersectionNode(ListNode headA, ListNode headB) { + if(headA == null || headB == null) return null; + + ListNode a = headA; + ListNode b = headB; + + while( a != b){ + a = a == null? headB : a.next; + b = b == null? headA : b.next; + } + + return a; +} +``` + +上边的代码简洁了很多,它没有去分别求两个链表的长度,而是把所有的情况都合并了起来。 + +* 如果没有重合部分,那么 `a` 和 `b` 在某一时间点 一定会同时走到 `null`,从而结束循环。 + +* 如果有重合部分,分两种情况。 + * 长度相同的话, `a` 和 `b` 一定是同时到达相遇点,然后返回。 + * 长度不同的话,较短的链表先到达结尾,然后指针转向较长的链表。此刻,较长的链表继续向末尾走,多走的距离刚好就是最开始介绍的解法,链表的长度差,走完之后指针转向较短的链表。然后继续走的话,相遇的位置就刚好是相遇点了。 + +综上,代码巧妙的把所有情况合并了起来。 + +# 解法二 + +最开始考虑这道题的时候,我还想了另一种思路。就是遍历某一个链表,把这个链表的每个节点进行标记,这里的话当然就是对 `val` 进行特殊标记。然后再遍历另一个链表,发现了这个标记也就找到了相遇点了。当然,这个标记一定得是可逆的,完成任务后我们要把原来链表的 `val` 进行还原。 + +常用的标记方法,比如取它相反数,取绝对值,异或,找一个不可能存在的数赋值过去等等,但对于这道题都无效。然后自己到这里思路也就断了,后来就想到了上边的解法一。 + +[这里](https://leetcode.com/problems/intersection-of-two-linked-lists/discuss/50030/My-C%2B%2B-Accepted-Solution-with-O(n)-time-and-O(1)-memory-(72ms)) 看到了类似于标记的方法,蛮有意思,分享一下,分下边几步。 + +1. 统计链表 `A` 的所有节点 `val` 的和,记为 `sumA`,同时记录长度 `lenA`。 +2. 把链表 `B` 的所有节点的 `val` 都进行加 `1`。 +3. 再次统计链表 `A` 的所有节点 `val` 的和,记为 `sumA2`。 +4. 将链表 `B` 的所有节点的 `val` 都进行减 `1`,相当于还原。 + +有了上边的几个数据就可以知道。 + +* `sumA == sumA2` 的话,就表明两个链表没有重合部分。 +* `sumA != sumA2` 的话,`sub = sumA2 - sumA`,由于我们对重合部分的 `val` 进行了加 `1`,所以前后的差 `sub` 就刚好表示了重合节点的个数。同时我们知道链表 A 的总长度 `lenA`,所以我们只需要对链表 A 遍历 `lenA - sub` 次,就刚好会走到重合部分的开头了,也就是我们要找的相遇点。 + +代码如下。 + +```java +public ListNode getIntersectionNode(ListNode headA, ListNode headB) { + if (headA == null || headB == null) { + return null; + } + + //步骤 1 + ListNode tailA = headA; + int lenA = 0; + int sumA = 0; + + while (tailA != null) { + sumA += tailA.val; + tailA = tailA.next; + lenA++; + } + + //步骤 2 + ListNode tailB = headB; + while (tailB != null) { + tailB.val = tailB.val + 1; + tailB = tailB.next; + } + + //步骤 3 + tailA = headA; + int sumA2 = 0; + while (tailA != null) { + sumA2 += tailA.val; + tailA = tailA.next; + } + + //步骤 4 + tailB = headB; + while (tailB != null) { + tailB.val = tailB.val - 1; + tailB = tailB.next; + } + + + if (sumA == sumA2) { + return null; + } else { + for (int i = 0; i < lenA - (sumA2 - sumA); i++) { + headA = headA.next; + } + return headA; + } + +} +``` + +上边算法的缺点就是,由于进行了加 `1` 操作,对于过大的数可能会引起溢出,此外求和也很有可能引起溢出。但思想还是很有趣的。 + +# 总 + +遇到题以后,可以对不同的情况分析,回想之前的一些思想,从而找到突破口。 + diff --git a/leetcode-162-Find-Peak-Element.md b/leetcode-162-Find-Peak-Element.md index 965815396..ad72cb620 100644 --- a/leetcode-162-Find-Peak-Element.md +++ b/leetcode-162-Find-Peak-Element.md @@ -1,87 +1,87 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/162.png) - -给一个数组,找出任意一个峰顶。找出的这个峰顶的特点就是,比它的左邻居和右邻居都大。 - -# 解法一 线性扫描 - -因为 `nums[-1]` 看做负无穷,所以从第 `0` 个元素开始,它一定是上升的趋势,由于我们要找峰顶,所以当它第一次出现下降,下降前的值就是我们要找的了。 - -如果它一直上升到最后一个值,又因为 `nums[n]` 看做负无穷,所以最后一个值就可以看做一个峰顶。 - -````java -public int findPeakElement(int[] nums) { - for (int i = 0; i < nums.length - 1; i++) { - //第一次下降 - if (nums[i] > nums[i + 1]) { - return i; - } - } - //一直上升 - return nums.length - 1; -} -```` - -# 解法二 二分法 - -要不是题目下边提示时间复杂度可以达到 `log` 级别,还真不敢往二分的方面想。因为二分法,我们一般用在有序数组上,那么这个题为什么可以用二分呢? - -不管什么情况,之所以能用二分,是因为我们可以根据某个条件,直接抛弃一半的元素,从而使得时间复杂度降到 `log` 级别。 - -至于这道题,因为题目告诉我们可以返回数组中的任意一个峰顶。所以我们只要确定某一半至少存在一个峰顶,那么另一半就可以抛弃掉。 - -我们只需要把 `nums[mid]` 和 `nums[mid + 1]` 比较。 - -先考虑第一次二分的时候,`start = 0`,`end = nums.length - 1`。 - -如果 `nums[mid] < nums[mid + 1]`,此时在上升阶段,因为 `nums[n]` 看做负无穷,也就是最终一定会下降,所以 `mid + 1` 到 `end` 之间至少会存在一个峰顶,可以把左半部分抛弃。 - -如果 `nums[mid] > nums[mid + 1]`,此时在下降阶段,因为 `nums[0]` 看做负无穷,最初一定是上升阶段,所以 `start` 到 `mid` 之间至少会存在一个峰顶,可以把右半部分抛弃。 - -通过上边的切割,我们就保证了后续左边界一定是在上升,右边界一定是在下降,所以第二次、第三次... 的二分就和上边一个道理了。 - -代码的话就可以有两种形式了,一种递归,一种迭代。 - -递归的代码如下: - -```java -public int findPeakElement(int[] nums) { - return findPeakElementHelper(nums, 0, nums.length - 1); -} - -private int findPeakElementHelper(int[] nums, int start, int end) { - if (start == end) { - return start; - } - int mid = (start + end) >>> 1; - if (nums[mid] < nums[mid + 1]) { - return findPeakElementHelper(nums, mid + 1, end); - } else { - return findPeakElementHelper(nums, start, mid); - } -} -``` - -由于递归形式比较简单,所以我们最好用迭代去实现,因为递归的话需要压栈的空间。虽然上边的递归是尾递归的形式,不需要压栈,但这需要编译器的支持。 - -```java -public int findPeakElement(int[] nums) { - int start = 0; - int end = nums.length - 1; - - while(start!=end) { - int mid = (start + end) >>> 1; - if(nums[mid] < nums[mid + 1]) { - start = mid + 1; - }else { - end = mid; - } - } - return start; -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/162.png) + +给一个数组,找出任意一个峰顶。找出的这个峰顶的特点就是,比它的左邻居和右邻居都大。 + +# 解法一 线性扫描 + +因为 `nums[-1]` 看做负无穷,所以从第 `0` 个元素开始,它一定是上升的趋势,由于我们要找峰顶,所以当它第一次出现下降,下降前的值就是我们要找的了。 + +如果它一直上升到最后一个值,又因为 `nums[n]` 看做负无穷,所以最后一个值就可以看做一个峰顶。 + +````java +public int findPeakElement(int[] nums) { + for (int i = 0; i < nums.length - 1; i++) { + //第一次下降 + if (nums[i] > nums[i + 1]) { + return i; + } + } + //一直上升 + return nums.length - 1; +} +```` + +# 解法二 二分法 + +要不是题目下边提示时间复杂度可以达到 `log` 级别,还真不敢往二分的方面想。因为二分法,我们一般用在有序数组上,那么这个题为什么可以用二分呢? + +不管什么情况,之所以能用二分,是因为我们可以根据某个条件,直接抛弃一半的元素,从而使得时间复杂度降到 `log` 级别。 + +至于这道题,因为题目告诉我们可以返回数组中的任意一个峰顶。所以我们只要确定某一半至少存在一个峰顶,那么另一半就可以抛弃掉。 + +我们只需要把 `nums[mid]` 和 `nums[mid + 1]` 比较。 + +先考虑第一次二分的时候,`start = 0`,`end = nums.length - 1`。 + +如果 `nums[mid] < nums[mid + 1]`,此时在上升阶段,因为 `nums[n]` 看做负无穷,也就是最终一定会下降,所以 `mid + 1` 到 `end` 之间至少会存在一个峰顶,可以把左半部分抛弃。 + +如果 `nums[mid] > nums[mid + 1]`,此时在下降阶段,因为 `nums[0]` 看做负无穷,最初一定是上升阶段,所以 `start` 到 `mid` 之间至少会存在一个峰顶,可以把右半部分抛弃。 + +通过上边的切割,我们就保证了后续左边界一定是在上升,右边界一定是在下降,所以第二次、第三次... 的二分就和上边一个道理了。 + +代码的话就可以有两种形式了,一种递归,一种迭代。 + +递归的代码如下: + +```java +public int findPeakElement(int[] nums) { + return findPeakElementHelper(nums, 0, nums.length - 1); +} + +private int findPeakElementHelper(int[] nums, int start, int end) { + if (start == end) { + return start; + } + int mid = (start + end) >>> 1; + if (nums[mid] < nums[mid + 1]) { + return findPeakElementHelper(nums, mid + 1, end); + } else { + return findPeakElementHelper(nums, start, mid); + } +} +``` + +由于递归形式比较简单,所以我们最好用迭代去实现,因为递归的话需要压栈的空间。虽然上边的递归是尾递归的形式,不需要压栈,但这需要编译器的支持。 + +```java +public int findPeakElement(int[] nums) { + int start = 0; + int end = nums.length - 1; + + while(start!=end) { + int mid = (start + end) >>> 1; + if(nums[mid] < nums[mid + 1]) { + start = mid + 1; + }else { + end = mid; + } + } + return start; +} +``` + +# 总 + 第一次遇到不需要有序数组也可以二分的题,蛮有意思。 \ No newline at end of file diff --git a/leetcode-164-Maximum-Gap.md b/leetcode-164-Maximum-Gap.md index 1530f44f0..a559c3e15 100644 --- a/leetcode-164-Maximum-Gap.md +++ b/leetcode-164-Maximum-Gap.md @@ -1,250 +1,250 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/164.jpg) - -给一个乱序的数组,求出数组排序以后的相邻数字的差最大是多少。 - -# 解法一 - -先来个直接的,就按题目的意思,先排序,再搜索。 - -```java -public int maximumGap(int[] nums) { - Arrays.sort(nums); - int max = 0; - for (int i = 0; i < nums.length - 1; i++) { - if (nums[i + 1] - nums[i] > max) { - max = nums[i + 1] - nums[i]; - } - } - return max; -} -``` - -但是正常的排序算法时间复杂度会是 `O(nlog(n))`,题目要求我们在 `O(n)` 的复杂度下去求解。 - -自己想了各种思路,但想不出怎么降到 `O(n)`,把 [官方](https://leetcode.com/problems/maximum-gap/solution/) 给的题解分享一下吧。 - -我们一般排序算法多用快速排序,平均时间复杂度是 `O(nlog(n))`,其实还有一种排序算法,时间复杂度是 `O(kn)`,`k` 是最大数字的位数,当 `k` 远小于 `n` 的时候,时间复杂度可以近似看成 `O(n)`。这种排序算法就是基数排序,下边讲一下具体思想。 - -比如这样一个数列排序: `342 58 576 356`, 我们来看一下怎么排序: - -```java -不足的位数看做是 0 -342 058 576 356 -按照个位将数字依次放到不同的位置 -0: -1: -2: 342 -3: -4: -5: -6: 576, 356 -7: -8: 058 -9: - -把上边的数字依次拿出来,组成新的序列 342 576 356 058,然后按十位继续放到不同的位置。 -0: -1: -2: -3: -4: 342 -5: 356 058 -6: -7: 576 -8: -9: - -把上边的数字依次拿出来,组成新的序列 342 356 058 576,然后按百位继续装到不同的位置。 -0: 058 -1: -2: -3: 342 356 -4: -5: 576 -6: -7: -8: -9: - -把数字依次拿出来,最终结果就是 58 342 356 576 -``` - -为了代码更好理解, 我们可以直接用 `10` 个 `list` 去存放每一组的数字,官方题解是直接用一维数组实现的。 - -对于取各个位的的数字,我们通过对数字除以 `1`, `10`, `100`... 然后再对 `10` 取余来实现。 - -```java -public int maximumGap(int[] nums) { - if (nums.length <= 1) { - return 0; - } - List> lists = new ArrayList<>(); - for (int i = 0; i < 10; i++) { - lists.add(new ArrayList<>()); - } - int n = nums.length; - int max = nums[0]; - //找出最大的数字 - for (int i = 1; i < n; i++) { - if (max < nums[i]) { - max = nums[i]; - } - } - int m = max; - int exp = 1; - //一位一位的进行 - while (max > 0) { - //将之前的元素清空 - for (int i = 0; i < 10; i++) { - lists.set(i, new ArrayList<>()); - } - //将数字放入对应的位置 - for (int i = 0; i < n; i++) { - lists.get(nums[i] / exp % 10).add(nums[i]); - } - - //将数字依次拿出来 - int index = 0; - for (int i = 0; i < 10; i++) { - for (int j = 0; j < lists.get(i).size(); j++) { - nums[index] = lists.get(i).get(j); - index++; - } - - } - max /= 10; - exp *= 10; - } - - int maxGap = 0; - for (int i = 0; i < nums.length - 1; i++) { - if (nums[i + 1] - nums[i] > maxGap) { - maxGap = nums[i + 1] - nums[i]; - } - } - return maxGap; -} -``` - -# 解法二 - -上边的解法还不是真正的 `O(n)`,下边继续介绍另一种解法,参考了 [这里](https://leetcode.com/problems/maximum-gap/discuss/50643/bucket-sort-JAVA-solution-with-explanation-O(N)-time-and-space) ,评论区对自己帮助很多。 - -我们知道如果是有序数组的话,我们就可以通过计算两两数字之间差即可解决问题。 - -那么如果是更宏观上的有序呢? - -```java -我们把 0 3 4 6 23 28 29 33 38 依次装到三个箱子中 - 0 1 2 3 - ------- ------- ------- ------- -| 3 4 | | | | 29 | | 33 | -| 6 | | | | 23 | | | -| 0 | | | | 28 | | 38 | - ------- ------- ------- ------- - 0 - 9 10 - 19 20 - 29 30 - 39 -我们把每个箱子的最大值和最小值表示出来 - min max min max min max min max - 0 6 - - 23 29 33 38 -``` - -我们可以只计算相邻箱子 `min` 和 `max` 的差值来解决问题吗?空箱子直接跳过。 - -第 `2` 个箱子的 `min` 减第 `0` 个箱子的 `max`, `23 - 6 = 17` - -第 `3` 个箱子的 `min` 减第 `2` 个箱子的 `max`, `33 - 29 = 4` - -看起来没什么问题,但这样做一定需要一个前提,因为我们只计算了相邻箱子的差值,没有计算箱子内数字的情况,所以我们需要保证每个箱子里边的数字一定不会产生最大 `gap`。 - -我们把箱子能放的的数字个数记为 `interval`,给定的数字中最小的是 `min`,最大的是 `max`。那么箱子划分的范围就是 `min ~ (min + 1 * interval - 1)`、`(min + 1 * interval) ~ (min + 2 * interval - 1)`、`(min + 2 * interval) ~ (min + 3 * interval - 1)`...,上边举的例子中, `interval` 我们其实取了 `10`。 - -划定了箱子范围后,我们其实很容易把数字放到箱子中,通过 `(nums[i] - min) / interval` 即可得到当前数字应该放到的箱子编号。那么最主要的问题其实就是怎么去确定 `interval`。 - -`interval` 过小的话,需要更多的箱子去存储,很费空间,此外箱子增多了,比较的次数也会增多,不划算。 - -`interval` 过大的话,箱子内部的数字可能产生题目要求的最大 `gap`,所以肯定不行。 - -所以我们要找到那个保证箱子内部的数字不会产生最大 `gap`,并且尽量大的 `interval`。 - -继续看上边的例子,`0 3 4 6 23 28 29 33 38`,数组中的最小值 `0` 和最大值 `38` ,并没有参与到 `interval` 的计算中,所以它俩可以不放到箱子中,还剩下 `n - 2` 个数字。 - -像上边的例子,如果我们保证至少有一个空箱子,那么我们就可以断言,箱子内部一定不会产生最大 `gap`。 - -因为在我们的某次计算中,会跳过一个空箱子,那么得到的 `gap` 一定会大于 `interval`,而箱子中的数字最大的 `gap` 是 `interval - 1`。 - -接下来的问题,怎么保证至少有一个空箱子呢? - -鸽巢原理的变形,有 `n - 2` 个数字,如果箱子数多于 `n - 2` ,那么一定会出现空箱子。总范围是 `max - min`,那么 `interval = (max - min) / 箱子数`,为了使得 `interval` 尽量大,箱子数取最小即可,也就是 `n - 1`。 - -所以 `interval = (max - min) / n - 1` 。这里如果除不尽的话,我们 `interval` 可以向上取整。因为我们给定的数字都是整数,这里向上取整的话对于最大 `gap` 是没有影响的。比如原来范围是 `[0,5.5)`,那么内部产生的最大 `gap` 是 `5 - 0 = 5`。现在向上取整,范围变成`[0,6)`,但是内部产生的最大 `gap` 依旧是 `5 - 0 = 5`。 - -所有问题都解决了,可以安心写代码了。 - -```java -public int maximumGap(int[] nums) { - if (nums.length <= 1) { - return 0; - } - int n = nums.length; - int min = nums[0]; - int max = nums[0]; - //找出最大值、最小值 - for (int i = 1; i < n; i++) { - min = Math.min(nums[i], min); - max = Math.max(nums[i], max); - } - if(max - min == 0) { - return 0; - } - - //算出每个箱子的范围 - int interval = (int) Math.ceil((double)(max - min) / (n - 1)); - - //每个箱子里数字的最小值和最大值 - int[] bucketMin = new int[n - 1]; - int[] bucketMax = new int[n - 1]; - - //最小值初始为 Integer.MAX_VALUE - Arrays.fill(bucketMin, Integer.MAX_VALUE); - //最小值初始化为 -1,因为题目告诉我们所有数字是非负数 - Arrays.fill(bucketMax, -1); - - //考虑每个数字 - for (int i = 0; i < nums.length; i++) { - //当前数字所在箱子编号 - int index = (nums[i] - min) / interval; - //最大数和最小数不需要考虑 - if(nums[i] == min || nums[i] == max) { - continue; - } - //更新当前数字所在箱子的最小值和最大值 - bucketMin[index] = Math.min(nums[i], bucketMin[index]); - bucketMax[index] = Math.max(nums[i], bucketMax[index]); - } - - int maxGap = 0; - //min 看做第 -1 个箱子的最大值 - int previousMax = min; - //从第 0 个箱子开始计算 - for (int i = 0; i < n - 1; i++) { - //最大值是 -1 说明箱子中没有数字,直接跳过 - if (bucketMax[i] == -1) { - continue; - } - - //当前箱子的最小值减去前一个箱子的最大值 - maxGap = Math.max(bucketMin[i] - previousMax, maxGap); - previousMax = bucketMax[i]; - } - //最大值可能处于边界,不在箱子中,需要单独考虑 - maxGap = Math.max(max - previousMax, maxGap); - return maxGap; - -} -``` - -# 总 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/164.jpg) + +给一个乱序的数组,求出数组排序以后的相邻数字的差最大是多少。 + +# 解法一 + +先来个直接的,就按题目的意思,先排序,再搜索。 + +```java +public int maximumGap(int[] nums) { + Arrays.sort(nums); + int max = 0; + for (int i = 0; i < nums.length - 1; i++) { + if (nums[i + 1] - nums[i] > max) { + max = nums[i + 1] - nums[i]; + } + } + return max; +} +``` + +但是正常的排序算法时间复杂度会是 `O(nlog(n))`,题目要求我们在 `O(n)` 的复杂度下去求解。 + +自己想了各种思路,但想不出怎么降到 `O(n)`,把 [官方](https://leetcode.com/problems/maximum-gap/solution/) 给的题解分享一下吧。 + +我们一般排序算法多用快速排序,平均时间复杂度是 `O(nlog(n))`,其实还有一种排序算法,时间复杂度是 `O(kn)`,`k` 是最大数字的位数,当 `k` 远小于 `n` 的时候,时间复杂度可以近似看成 `O(n)`。这种排序算法就是基数排序,下边讲一下具体思想。 + +比如这样一个数列排序: `342 58 576 356`, 我们来看一下怎么排序: + +```java +不足的位数看做是 0 +342 058 576 356 +按照个位将数字依次放到不同的位置 +0: +1: +2: 342 +3: +4: +5: +6: 576, 356 +7: +8: 058 +9: + +把上边的数字依次拿出来,组成新的序列 342 576 356 058,然后按十位继续放到不同的位置。 +0: +1: +2: +3: +4: 342 +5: 356 058 +6: +7: 576 +8: +9: + +把上边的数字依次拿出来,组成新的序列 342 356 058 576,然后按百位继续装到不同的位置。 +0: 058 +1: +2: +3: 342 356 +4: +5: 576 +6: +7: +8: +9: + +把数字依次拿出来,最终结果就是 58 342 356 576 +``` + +为了代码更好理解, 我们可以直接用 `10` 个 `list` 去存放每一组的数字,官方题解是直接用一维数组实现的。 + +对于取各个位的的数字,我们通过对数字除以 `1`, `10`, `100`... 然后再对 `10` 取余来实现。 + +```java +public int maximumGap(int[] nums) { + if (nums.length <= 1) { + return 0; + } + List> lists = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + lists.add(new ArrayList<>()); + } + int n = nums.length; + int max = nums[0]; + //找出最大的数字 + for (int i = 1; i < n; i++) { + if (max < nums[i]) { + max = nums[i]; + } + } + int m = max; + int exp = 1; + //一位一位的进行 + while (max > 0) { + //将之前的元素清空 + for (int i = 0; i < 10; i++) { + lists.set(i, new ArrayList<>()); + } + //将数字放入对应的位置 + for (int i = 0; i < n; i++) { + lists.get(nums[i] / exp % 10).add(nums[i]); + } + + //将数字依次拿出来 + int index = 0; + for (int i = 0; i < 10; i++) { + for (int j = 0; j < lists.get(i).size(); j++) { + nums[index] = lists.get(i).get(j); + index++; + } + + } + max /= 10; + exp *= 10; + } + + int maxGap = 0; + for (int i = 0; i < nums.length - 1; i++) { + if (nums[i + 1] - nums[i] > maxGap) { + maxGap = nums[i + 1] - nums[i]; + } + } + return maxGap; +} +``` + +# 解法二 + +上边的解法还不是真正的 `O(n)`,下边继续介绍另一种解法,参考了 [这里](https://leetcode.com/problems/maximum-gap/discuss/50643/bucket-sort-JAVA-solution-with-explanation-O(N)-time-and-space) ,评论区对自己帮助很多。 + +我们知道如果是有序数组的话,我们就可以通过计算两两数字之间差即可解决问题。 + +那么如果是更宏观上的有序呢? + +```java +我们把 0 3 4 6 23 28 29 33 38 依次装到三个箱子中 + 0 1 2 3 + ------- ------- ------- ------- +| 3 4 | | | | 29 | | 33 | +| 6 | | | | 23 | | | +| 0 | | | | 28 | | 38 | + ------- ------- ------- ------- + 0 - 9 10 - 19 20 - 29 30 - 39 +我们把每个箱子的最大值和最小值表示出来 + min max min max min max min max + 0 6 - - 23 29 33 38 +``` + +我们可以只计算相邻箱子 `min` 和 `max` 的差值来解决问题吗?空箱子直接跳过。 + +第 `2` 个箱子的 `min` 减第 `0` 个箱子的 `max`, `23 - 6 = 17` + +第 `3` 个箱子的 `min` 减第 `2` 个箱子的 `max`, `33 - 29 = 4` + +看起来没什么问题,但这样做一定需要一个前提,因为我们只计算了相邻箱子的差值,没有计算箱子内数字的情况,所以我们需要保证每个箱子里边的数字一定不会产生最大 `gap`。 + +我们把箱子能放的的数字个数记为 `interval`,给定的数字中最小的是 `min`,最大的是 `max`。那么箱子划分的范围就是 `min ~ (min + 1 * interval - 1)`、`(min + 1 * interval) ~ (min + 2 * interval - 1)`、`(min + 2 * interval) ~ (min + 3 * interval - 1)`...,上边举的例子中, `interval` 我们其实取了 `10`。 + +划定了箱子范围后,我们其实很容易把数字放到箱子中,通过 `(nums[i] - min) / interval` 即可得到当前数字应该放到的箱子编号。那么最主要的问题其实就是怎么去确定 `interval`。 + +`interval` 过小的话,需要更多的箱子去存储,很费空间,此外箱子增多了,比较的次数也会增多,不划算。 + +`interval` 过大的话,箱子内部的数字可能产生题目要求的最大 `gap`,所以肯定不行。 + +所以我们要找到那个保证箱子内部的数字不会产生最大 `gap`,并且尽量大的 `interval`。 + +继续看上边的例子,`0 3 4 6 23 28 29 33 38`,数组中的最小值 `0` 和最大值 `38` ,并没有参与到 `interval` 的计算中,所以它俩可以不放到箱子中,还剩下 `n - 2` 个数字。 + +像上边的例子,如果我们保证至少有一个空箱子,那么我们就可以断言,箱子内部一定不会产生最大 `gap`。 + +因为在我们的某次计算中,会跳过一个空箱子,那么得到的 `gap` 一定会大于 `interval`,而箱子中的数字最大的 `gap` 是 `interval - 1`。 + +接下来的问题,怎么保证至少有一个空箱子呢? + +鸽巢原理的变形,有 `n - 2` 个数字,如果箱子数多于 `n - 2` ,那么一定会出现空箱子。总范围是 `max - min`,那么 `interval = (max - min) / 箱子数`,为了使得 `interval` 尽量大,箱子数取最小即可,也就是 `n - 1`。 + +所以 `interval = (max - min) / n - 1` 。这里如果除不尽的话,我们 `interval` 可以向上取整。因为我们给定的数字都是整数,这里向上取整的话对于最大 `gap` 是没有影响的。比如原来范围是 `[0,5.5)`,那么内部产生的最大 `gap` 是 `5 - 0 = 5`。现在向上取整,范围变成`[0,6)`,但是内部产生的最大 `gap` 依旧是 `5 - 0 = 5`。 + +所有问题都解决了,可以安心写代码了。 + +```java +public int maximumGap(int[] nums) { + if (nums.length <= 1) { + return 0; + } + int n = nums.length; + int min = nums[0]; + int max = nums[0]; + //找出最大值、最小值 + for (int i = 1; i < n; i++) { + min = Math.min(nums[i], min); + max = Math.max(nums[i], max); + } + if(max - min == 0) { + return 0; + } + + //算出每个箱子的范围 + int interval = (int) Math.ceil((double)(max - min) / (n - 1)); + + //每个箱子里数字的最小值和最大值 + int[] bucketMin = new int[n - 1]; + int[] bucketMax = new int[n - 1]; + + //最小值初始为 Integer.MAX_VALUE + Arrays.fill(bucketMin, Integer.MAX_VALUE); + //最小值初始化为 -1,因为题目告诉我们所有数字是非负数 + Arrays.fill(bucketMax, -1); + + //考虑每个数字 + for (int i = 0; i < nums.length; i++) { + //当前数字所在箱子编号 + int index = (nums[i] - min) / interval; + //最大数和最小数不需要考虑 + if(nums[i] == min || nums[i] == max) { + continue; + } + //更新当前数字所在箱子的最小值和最大值 + bucketMin[index] = Math.min(nums[i], bucketMin[index]); + bucketMax[index] = Math.max(nums[i], bucketMax[index]); + } + + int maxGap = 0; + //min 看做第 -1 个箱子的最大值 + int previousMax = min; + //从第 0 个箱子开始计算 + for (int i = 0; i < n - 1; i++) { + //最大值是 -1 说明箱子中没有数字,直接跳过 + if (bucketMax[i] == -1) { + continue; + } + + //当前箱子的最小值减去前一个箱子的最大值 + maxGap = Math.max(bucketMin[i] - previousMax, maxGap); + previousMax = bucketMax[i]; + } + //最大值可能处于边界,不在箱子中,需要单独考虑 + maxGap = Math.max(max - previousMax, maxGap); + return maxGap; + +} +``` + +# 总 + 这道题主要是对排序算法的了解,第一次见到了基数排序的应用,解法二其实是桶排序的步骤。 \ No newline at end of file diff --git a/leetcode-165-Compare-Version-Numbers.md b/leetcode-165-Compare-Version-Numbers.md index 3cd699982..9728d97e2 100644 --- a/leetcode-165-Compare-Version-Numbers.md +++ b/leetcode-165-Compare-Version-Numbers.md @@ -1,109 +1,109 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/165.png) - -比较两个版本号,`version1` 大于 `version2` 就返回 `1`,相等返回 `0`,小于就返回 `-1`。比较的时候先比较最左边的数字,相等的话再比较后一个,以此类推。 - -# 解法一 - -这道题今年笔试的时候遇到好几次了,没想到竟然是 `leetcode` 的原题。思路很简单,按照「点」对版本号进行切割,然后依次比较每个数字即可。 - -切割的话涉及到 `java` 语言的一个特性,`.` 在正则里有特殊含义,所以我们需要进行转义。 - -这里切割出来的是字符串,所以我们需要把字符串转为数字,因为字符串转数字不是这道题的重点,所以直接调用系统提供的 `Integer.parseInt` 即可。 - -```java -public int compareVersion(String version1, String version2) { - String[] nums1 = version1.split("\\."); - String[] nums2 = version2.split("\\."); - int i = 0, j = 0; - while (i < nums1.length || j < nums2.length) { - //这个技巧经常用到,当一个已经遍历结束的话,我们将其赋值为 0 - String num1 = i < nums1.length ? nums1[i] : "0"; - String num2 = j < nums2.length ? nums2[j] : "0"; - int res = compare(num1, num2); - if (res == 0) { - i++; - j++; - } else { - return res; - } - } - return 0; -} - -private int compare(String num1, String num2) { - int n1 = Integer.parseInt(num1); - int n2 = Integer.parseInt(num2); - if (n1 > n2) { - return 1; - } else if (n1 < n2) { - return -1; - } else { - return 0; - } -} -``` - -# 解法二 - -上边的解法可以成功 `AC`,但是如果数字过大的话,`int` 是无法保存的。所以我们可以不把字符串转为数字,而是直接用字符串比较。 - -```java -public int compareVersion(String version1, String version2) { - String[] nums1 = version1.split("\\."); - String[] nums2 = version2.split("\\."); - int i = 0, j = 0; - while (i < nums1.length || j < nums2.length) { - String num1 = i < nums1.length ? nums1[i] : "0"; - String num2 = j < nums2.length ? nums2[j] : "0"; - int res = compare(num1, num2); - if (res == 0) { - i++; - j++; - } else { - return res; - } - } - return 0; -} - -private int compare(String num1, String num2) { - //将高位的 0 去掉 - num1 = removeFrontZero(num1); - num2 = removeFrontZero(num2); - //先根据长度进行判断 - if (num1.length() > num2.length()) { - return 1; - } else if (num1.length() < num2.length()) { - return -1; - } else { - //长度相等的时候 - for (int i = 0; i < num1.length(); i++) { - if (num1.charAt(i) - num2.charAt(i) > 0) { - return 1; - } else if (num1.charAt(i) - num2.charAt(i) < 0) { - return -1; - } - } - return 0; - } -} - -private String removeFrontZero(String num) { - int start = 0; - for (int i = 0; i < num.length(); i++) { - if (num.charAt(i) == '0') { - start++; - } else { - break; - } - } - return num.substring(start); -} -``` - -# 总 - -题目其实是比较简单的,` String num1 = i < nums1.length ? nums1[i] : "0";` 这个技巧在 [第 2 题](https://leetcode.wang/leetCode-2-Add-Two-Numbers.html) 用到过,会使得代码很清晰,逻辑上也会简单些。解法二直接对字符串进行操作,这也是处理大数运算的时候的方法。 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/165.png) + +比较两个版本号,`version1` 大于 `version2` 就返回 `1`,相等返回 `0`,小于就返回 `-1`。比较的时候先比较最左边的数字,相等的话再比较后一个,以此类推。 + +# 解法一 + +这道题今年笔试的时候遇到好几次了,没想到竟然是 `leetcode` 的原题。思路很简单,按照「点」对版本号进行切割,然后依次比较每个数字即可。 + +切割的话涉及到 `java` 语言的一个特性,`.` 在正则里有特殊含义,所以我们需要进行转义。 + +这里切割出来的是字符串,所以我们需要把字符串转为数字,因为字符串转数字不是这道题的重点,所以直接调用系统提供的 `Integer.parseInt` 即可。 + +```java +public int compareVersion(String version1, String version2) { + String[] nums1 = version1.split("\\."); + String[] nums2 = version2.split("\\."); + int i = 0, j = 0; + while (i < nums1.length || j < nums2.length) { + //这个技巧经常用到,当一个已经遍历结束的话,我们将其赋值为 0 + String num1 = i < nums1.length ? nums1[i] : "0"; + String num2 = j < nums2.length ? nums2[j] : "0"; + int res = compare(num1, num2); + if (res == 0) { + i++; + j++; + } else { + return res; + } + } + return 0; +} + +private int compare(String num1, String num2) { + int n1 = Integer.parseInt(num1); + int n2 = Integer.parseInt(num2); + if (n1 > n2) { + return 1; + } else if (n1 < n2) { + return -1; + } else { + return 0; + } +} +``` + +# 解法二 + +上边的解法可以成功 `AC`,但是如果数字过大的话,`int` 是无法保存的。所以我们可以不把字符串转为数字,而是直接用字符串比较。 + +```java +public int compareVersion(String version1, String version2) { + String[] nums1 = version1.split("\\."); + String[] nums2 = version2.split("\\."); + int i = 0, j = 0; + while (i < nums1.length || j < nums2.length) { + String num1 = i < nums1.length ? nums1[i] : "0"; + String num2 = j < nums2.length ? nums2[j] : "0"; + int res = compare(num1, num2); + if (res == 0) { + i++; + j++; + } else { + return res; + } + } + return 0; +} + +private int compare(String num1, String num2) { + //将高位的 0 去掉 + num1 = removeFrontZero(num1); + num2 = removeFrontZero(num2); + //先根据长度进行判断 + if (num1.length() > num2.length()) { + return 1; + } else if (num1.length() < num2.length()) { + return -1; + } else { + //长度相等的时候 + for (int i = 0; i < num1.length(); i++) { + if (num1.charAt(i) - num2.charAt(i) > 0) { + return 1; + } else if (num1.charAt(i) - num2.charAt(i) < 0) { + return -1; + } + } + return 0; + } +} + +private String removeFrontZero(String num) { + int start = 0; + for (int i = 0; i < num.length(); i++) { + if (num.charAt(i) == '0') { + start++; + } else { + break; + } + } + return num.substring(start); +} +``` + +# 总 + +题目其实是比较简单的,` String num1 = i < nums1.length ? nums1[i] : "0";` 这个技巧在 [第 2 题](https://leetcode.wang/leetCode-2-Add-Two-Numbers.html) 用到过,会使得代码很清晰,逻辑上也会简单些。解法二直接对字符串进行操作,这也是处理大数运算的时候的方法。 + diff --git a/leetcode-166-Fraction-to-Recurring-Decimal.md b/leetcode-166-Fraction-to-Recurring-Decimal.md index 07517c53c..e727d5a05 100644 --- a/leetcode-166-Fraction-to-Recurring-Decimal.md +++ b/leetcode-166-Fraction-to-Recurring-Decimal.md @@ -1,132 +1,132 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/166.jpg) - -算除法,如果是循环小数,要把循环的部分用括号括起来。 - -# 解法一 - -这道题说简单的话,其实就是模拟下我们算除法的过程。 - -说难的话,有很多坑的地方要注意下,自己也是提交了好几次,才 `AC` 的,需要考虑很多东西。 - -首先说一下我们要模拟一下什么过程,以 `20/11` 为例。 - -第一次得到的商,就是我们的整数部分,`int` 间运算就可以直接取到整数部分了,记为 `integer`。 - -也就是 `integer = 20 / 11 = 1`。 - -然后回想一下我们用竖式计算的过程。 - -如下图,首先得到了商是 `1`,余数是 `9`。在程序中得到余数的话,可以用 `被除数 - 商 * 除数`。 - -也就是 `20 - 1 * 11 = 9`。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/166_2.jpg) - -如下图,接下来我们将余数乘以 `10` 做为新的被除数,继续把 `11` 当做除数。然后得到商和新的余数。 - -也就是计算 `90 / 11`。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/166_3.jpg) - -如下图,接下来重复上边的过程,用余数乘以 `10` 做为新的被除数,继续把 `11` 当做除数。然后得到商和新的余数。 - -也就是计算 `20 / 11`。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/166_4.jpg) - -如下图,接下来继续重复上边的过程,用余数乘以 `10` 做为新的被除数,继续把 `11` 当做除数。然后得到商和新的余数。 - -也就是计算 `90 / 11`。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/166_5.jpg) - -那么什么时候结束呢? - -* 第一种情况,余数为 `0`,说明没有循环小数。 - -* 第二种情况,一开始这里爬坑了。开始觉得只要商里边出现重复的数字(不考虑整数部分的数字,也就是上边例子的第一个 `1`),就可以认为出现了循环小数。 - - 比如上边的例子,`8` 第二次出现,所以到这里不再计算。而循环小数部分就是和当前数字重复的位置到当前位置的前一个,也就是 `81`。所以最终结果就是 `1.(81)`。 - - 但提交的时候,出现了一个反例,如下图。 - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/166_6.jpg) - - 虽然出现了重复的 `8`,但最终结果并不是 `8` 循环。很明显下次是 `40 / 17`,需要商 `2`。至于原因就是两次商 `8` 所对应的被除数并不一样,第一次是 `150` ,第二次是 `140`。 - - 所以为了判断是否出现循环小数,我们不应该判断是否出现了重复的商,而是应该判断是否出现了重复的被除数。 - -经过上边的分析,循环也很明显了。被除数除以除数,记录商。然后余数乘以 `10` 做为新的被除数继续除以除数。直到余数为 `0` 或者出现重复的被除数。 - -记录商的话,我们将整数部分和小数部分单独记录。因为小数部分要累积记录,一开始我用的是一个 `int` 去保存小数部分。比如第一个商是 `1`,第二个商是 `2`,我把之前的商乘以 `10` 再加上新的商。也就是 `1 * 10 + 2 = 12`,当第三个商 `5` 来的时候,就是 `12 * 10 + 5 = 125`,看起来很完美。 - -但比如上边的例子 `1/17` ,小数部分第一个商是 `0`,第二个商是 `5`,如果按上边的记录方法,记录的就是 `5`,而不是 `05`。另外,如果商的部分数字过多的话,还会产生溢出,所以最终用 `String` 记录了商,每次将新的商加到 `String` 中即可。 - -还有一个问题就是怎么判断是否出现了重复的商? - -很简单,用一个 `HashMap`,`key` 记录出现过的被除数,`value` 记录商出现的位置,这样当出现重复被除数的时候,通过 `value` 立刻知道循环的小数部分是多少。 - -最后一个问题,我们只考虑了正数除以正数的例子,对于正数除以负数或者负数除以负数呢?和我们在纸上算一样,先确定商的符号,然后将被除数和除数都转为正数即可。 - -上边的操作会带来一个问题,对于 `java` 而言,`int` 类型的话,负数的最小值是 `-2147483648`,正数的最大值是 `2147483647`,并不能把`-2147483648` 转成正数,至于原因的话可以参考这篇文章,[补码](https://zhuanlan.zhihu.com/p/67227136)。 - -溢出这个问题其实不是这个题的关键,所以我们直接用数据范围更大的 `long` 去存数字就可以了。 - -```java -public String fractionToDecimal(int numerator, int denominator) { - long num = numerator; - long den = denominator; - String sign = ""; - //确定符号 - if (num > 0 && den < 0 || num < 0 && den > 0) { - sign = "-"; - } - //转为正数 - num = Math.abs(num); - den = Math.abs(den); - //记录整数部分 - long integer = num / den; - //计算余数 - num = num - integer * den; - HashMap map = new HashMap<>(); - int index = 0; - String decimal = "";//记录小数部分 - int repeatIndex = -1;//保存重复的位置 - while (num != 0) { - num *= 10;//余数乘以 10 作为新的被除数 - if (map.containsKey(num)) { - repeatIndex = map.get(num); - break; - } - //保存被除数 - map.put(num, index); - //保存当前的商 - long decimalPlace = num / den; - //加到所有的商中 - decimal = decimal + decimalPlace; - //计算新的余数 - num = num - decimalPlace * den; - index++; - } - //是否存在循环小数 - if (repeatIndex != -1) { - String dec = decimal; - return sign + integer + "." + dec.substring(0, repeatIndex) + "(" + dec.substring(repeatIndex) + ")"; - } else { - if (decimal == "") { - return sign + integer; - } else { - return sign + integer + "." + decimal; - } - } -} -``` - -有人可能会问,如果数字很大,又超过了 `long` 怎么办,一种方案是之前写过的 [29 题](https://leetcode.wang/leetCode-29-Divide-Two-Integers.html),因为负数存的数更多,所以我们可以把负数当做正数,正数当做负数,所有的计算都在负数范围内计算。另一种方案的话, `java` 其实已经提供了大数类 `BigInteger` 供我们使用,就不存在溢出的问题了。至于原理的话,应该和第 [2 题](https://leetcode.wang/leetCode-2-Add-Two-Numbers.html) 大数相加一样,把数字用链表去存储,这样多大的数字都能进行存储了,然后把运算法则都封装成方法即可。 - -# 总 - -这道题其实就是模拟我们平时在纸上竖式计算的过程,其中一些问题要注意下,溢出的问题,正负数的问题等等。`HashMap` 来处理重复值的技巧,经常用到了。 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/166.jpg) + +算除法,如果是循环小数,要把循环的部分用括号括起来。 + +# 解法一 + +这道题说简单的话,其实就是模拟下我们算除法的过程。 + +说难的话,有很多坑的地方要注意下,自己也是提交了好几次,才 `AC` 的,需要考虑很多东西。 + +首先说一下我们要模拟一下什么过程,以 `20/11` 为例。 + +第一次得到的商,就是我们的整数部分,`int` 间运算就可以直接取到整数部分了,记为 `integer`。 + +也就是 `integer = 20 / 11 = 1`。 + +然后回想一下我们用竖式计算的过程。 + +如下图,首先得到了商是 `1`,余数是 `9`。在程序中得到余数的话,可以用 `被除数 - 商 * 除数`。 + +也就是 `20 - 1 * 11 = 9`。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/166_2.jpg) + +如下图,接下来我们将余数乘以 `10` 做为新的被除数,继续把 `11` 当做除数。然后得到商和新的余数。 + +也就是计算 `90 / 11`。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/166_3.jpg) + +如下图,接下来重复上边的过程,用余数乘以 `10` 做为新的被除数,继续把 `11` 当做除数。然后得到商和新的余数。 + +也就是计算 `20 / 11`。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/166_4.jpg) + +如下图,接下来继续重复上边的过程,用余数乘以 `10` 做为新的被除数,继续把 `11` 当做除数。然后得到商和新的余数。 + +也就是计算 `90 / 11`。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/166_5.jpg) + +那么什么时候结束呢? + +* 第一种情况,余数为 `0`,说明没有循环小数。 + +* 第二种情况,一开始这里爬坑了。开始觉得只要商里边出现重复的数字(不考虑整数部分的数字,也就是上边例子的第一个 `1`),就可以认为出现了循环小数。 + + 比如上边的例子,`8` 第二次出现,所以到这里不再计算。而循环小数部分就是和当前数字重复的位置到当前位置的前一个,也就是 `81`。所以最终结果就是 `1.(81)`。 + + 但提交的时候,出现了一个反例,如下图。 + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/166_6.jpg) + + 虽然出现了重复的 `8`,但最终结果并不是 `8` 循环。很明显下次是 `40 / 17`,需要商 `2`。至于原因就是两次商 `8` 所对应的被除数并不一样,第一次是 `150` ,第二次是 `140`。 + + 所以为了判断是否出现循环小数,我们不应该判断是否出现了重复的商,而是应该判断是否出现了重复的被除数。 + +经过上边的分析,循环也很明显了。被除数除以除数,记录商。然后余数乘以 `10` 做为新的被除数继续除以除数。直到余数为 `0` 或者出现重复的被除数。 + +记录商的话,我们将整数部分和小数部分单独记录。因为小数部分要累积记录,一开始我用的是一个 `int` 去保存小数部分。比如第一个商是 `1`,第二个商是 `2`,我把之前的商乘以 `10` 再加上新的商。也就是 `1 * 10 + 2 = 12`,当第三个商 `5` 来的时候,就是 `12 * 10 + 5 = 125`,看起来很完美。 + +但比如上边的例子 `1/17` ,小数部分第一个商是 `0`,第二个商是 `5`,如果按上边的记录方法,记录的就是 `5`,而不是 `05`。另外,如果商的部分数字过多的话,还会产生溢出,所以最终用 `String` 记录了商,每次将新的商加到 `String` 中即可。 + +还有一个问题就是怎么判断是否出现了重复的商? + +很简单,用一个 `HashMap`,`key` 记录出现过的被除数,`value` 记录商出现的位置,这样当出现重复被除数的时候,通过 `value` 立刻知道循环的小数部分是多少。 + +最后一个问题,我们只考虑了正数除以正数的例子,对于正数除以负数或者负数除以负数呢?和我们在纸上算一样,先确定商的符号,然后将被除数和除数都转为正数即可。 + +上边的操作会带来一个问题,对于 `java` 而言,`int` 类型的话,负数的最小值是 `-2147483648`,正数的最大值是 `2147483647`,并不能把`-2147483648` 转成正数,至于原因的话可以参考这篇文章,[补码](https://zhuanlan.zhihu.com/p/67227136)。 + +溢出这个问题其实不是这个题的关键,所以我们直接用数据范围更大的 `long` 去存数字就可以了。 + +```java +public String fractionToDecimal(int numerator, int denominator) { + long num = numerator; + long den = denominator; + String sign = ""; + //确定符号 + if (num > 0 && den < 0 || num < 0 && den > 0) { + sign = "-"; + } + //转为正数 + num = Math.abs(num); + den = Math.abs(den); + //记录整数部分 + long integer = num / den; + //计算余数 + num = num - integer * den; + HashMap map = new HashMap<>(); + int index = 0; + String decimal = "";//记录小数部分 + int repeatIndex = -1;//保存重复的位置 + while (num != 0) { + num *= 10;//余数乘以 10 作为新的被除数 + if (map.containsKey(num)) { + repeatIndex = map.get(num); + break; + } + //保存被除数 + map.put(num, index); + //保存当前的商 + long decimalPlace = num / den; + //加到所有的商中 + decimal = decimal + decimalPlace; + //计算新的余数 + num = num - decimalPlace * den; + index++; + } + //是否存在循环小数 + if (repeatIndex != -1) { + String dec = decimal; + return sign + integer + "." + dec.substring(0, repeatIndex) + "(" + dec.substring(repeatIndex) + ")"; + } else { + if (decimal == "") { + return sign + integer; + } else { + return sign + integer + "." + decimal; + } + } +} +``` + +有人可能会问,如果数字很大,又超过了 `long` 怎么办,一种方案是之前写过的 [29 题](https://leetcode.wang/leetCode-29-Divide-Two-Integers.html),因为负数存的数更多,所以我们可以把负数当做正数,正数当做负数,所有的计算都在负数范围内计算。另一种方案的话, `java` 其实已经提供了大数类 `BigInteger` 供我们使用,就不存在溢出的问题了。至于原理的话,应该和第 [2 题](https://leetcode.wang/leetCode-2-Add-Two-Numbers.html) 大数相加一样,把数字用链表去存储,这样多大的数字都能进行存储了,然后把运算法则都封装成方法即可。 + +# 总 + +这道题其实就是模拟我们平时在纸上竖式计算的过程,其中一些问题要注意下,溢出的问题,正负数的问题等等。`HashMap` 来处理重复值的技巧,经常用到了。 + diff --git a/leetcode-167-Two-SumII-Input-array-is-sorted.md b/leetcode-167-Two-SumII-Input-array-is-sorted.md index 695619f25..7ea0a3ef1 100644 --- a/leetcode-167-Two-SumII-Input-array-is-sorted.md +++ b/leetcode-167-Two-SumII-Input-array-is-sorted.md @@ -1,33 +1,33 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/167.jpg) - -给一个有序数组和一个目标值,找出两个数使其和为目标值,返回这两个数的位置(数组下标加 1)。 - -# 解法一 - -[第 1 题](https://leetcode.wang/leetCode-1-Two-Sum.html) 做过无序数组找两个数,里边的解法当然也可以用到这道题,利用了 `HashMap`,可以过去看一下。 - -[第 15 题](https://leetcode.wang/leetCode-15-3Sum.html) 找出三个数,使其和为目标值的题目中的解法中,其实我们将问题转换到了现在这道题,也可以过去看一下。具体的话,其实我们只需要首尾两个指针进行遍历即可。 - -```java -public int[] twoSum(int[] numbers, int target) { - int i = 0; - int j = numbers.length - 1; - while (i < j) { - if (numbers[i] + numbers[j] == target) { - return new int[] { i + 1, j + 1 }; - } else if (numbers[i] + numbers[j] < target) { - i++; - } else { - j--; - } - } - //因为题目告诉我们一定有解,所以这里随便返回了 - return new int[] { -1, -1 }; -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/167.jpg) + +给一个有序数组和一个目标值,找出两个数使其和为目标值,返回这两个数的位置(数组下标加 1)。 + +# 解法一 + +[第 1 题](https://leetcode.wang/leetCode-1-Two-Sum.html) 做过无序数组找两个数,里边的解法当然也可以用到这道题,利用了 `HashMap`,可以过去看一下。 + +[第 15 题](https://leetcode.wang/leetCode-15-3Sum.html) 找出三个数,使其和为目标值的题目中的解法中,其实我们将问题转换到了现在这道题,也可以过去看一下。具体的话,其实我们只需要首尾两个指针进行遍历即可。 + +```java +public int[] twoSum(int[] numbers, int target) { + int i = 0; + int j = numbers.length - 1; + while (i < j) { + if (numbers[i] + numbers[j] == target) { + return new int[] { i + 1, j + 1 }; + } else if (numbers[i] + numbers[j] < target) { + i++; + } else { + j--; + } + } + //因为题目告诉我们一定有解,所以这里随便返回了 + return new int[] { -1, -1 }; +} +``` + +# 总 + 这道题没有新东西,双指针的技巧经常用到。这可能是篇幅最少的一个题解了,哈哈。 \ No newline at end of file diff --git a/leetcode-168-Excel-Sheet-Column-Title.md b/leetcode-168-Excel-Sheet-Column-Title.md index e9ee02c2d..a9a41ce73 100644 --- a/leetcode-168-Excel-Sheet-Column-Title.md +++ b/leetcode-168-Excel-Sheet-Column-Title.md @@ -1,101 +1,101 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/168.jpg) - -把数字根据对应规则转为字母。 - -# 解法一 - -这道题的话本质上其实就是进制转换,把 `10` 进制转为 `26` 进制表示,可以看一下 [这篇文章](https://zhuanlan.zhihu.com/p/75006709) 对进制转换有个更深的了解。 - -先讨论一下我们熟悉的 `10` 进制和 `16` 进制的转换。 - -`16` 进制中,`0` 到 `9` 还是正常的数字,然后增加字母 `A` 表示 `10`,字母 `B` 表示 `11`... 以此类推,直到 `F` 表示 `15`,所以我们各个位的取值范围是 `0 - 15` 。 - -假如 `16` 的进制的 `A1F` 转换为 `10` 进制的话就用下边的式子,其中 `A` 表示 `10`, `F` 表示 `15`。 - -$$10\times16^2+1\times16^1+15\times16^0=2591$$ - -那么假如我们知道的是 `10` 进制的 `2591`,怎么转为 `16` 进制呢? - -我们把上边的等式一般化,设我们要求的每一位分别是 `x1,x2,x3...`,事先我们并不知道有多少位。 - -$$...x_4\times16^3+x_3\times16^2+x_2\times16^1+x_1\times16^0=2591$$ - -我们可以在等式两边同时模上 `16`,等式就变成 - -$$x_1=2591mod16=15$$ - -这样我们就求出来了 `x1`,接下来我们在等式两边同时除以 `16`。等式左边由于 `x1` 的范围是 `0 - 15`,所以在整数间运算不管 `x1` 是多少,`x1/16` 都等于 `0`,所以等式就变成了下边的样子 - -$$...x_4\times16^2+x_3\times16^1+x_2\times16^0=2591/16=161$$ - -和之前对比, `16` 的幂都小了 `1`,同时 `x1` 消失了。 - -接下来就可以重复上边的两个步骤,模 `16` 和除以 `16`,就可以依次求出 `x2`、`x3` ...了。 - -直到除以 `16` 后变成 `0` ,就可以结束了。 - -对于 `10` 进制转 `26` 进制的话是一样的道理,只不过每次采用模 `26` 和除以 `26`。 - -所以代码的话,可以直接写出来。 - -```java -public String convertToTitle(int n) { - StringBuilder sb = new StringBuilder(); - while (n > 0) { - int c = n % 26; - sb.insert(0, (char) ('A' + c - 1)); - n /= 26; - } - return sb.toString(); -} -``` - -上边 `(char) ('A' + c - 1)` 这里的话,算是一个技巧,我们是通过对 `A` 的偏移,强制将整数转为了字符。 - -比如 `c = 3`,那么 `'A' + c - 1`,会把 `A` 根据 ASCII 码值转为 `65`,然后计算 `65 + c - 1 = 65 + 3 - 1 = 67`,然后 `(char)67`,根据 ASCII 码值就刚好是字符 `C`。 - -但是上边的算法还是有问题的,因为题目中每个位的范围是 `1-26`,而不是`0-25`。 - -所以取余的话对于 `1-25` 的数字结果都是正确的,但如果当前的位是 `26`,余数求出来就是 `0`,此时我们需要把它修正为 `26`。 - -此外,我们每次除以 `26` 是为了把最低位去掉,回忆下之前的等式 - -$$...x_4\times26^3+x_3\times26^2+x_2\times26^1+x_1\times26^0=2591$$ - -因为 `x1` 的范围正常情况下是 `0-25` ,所以除以 `26` 可以把 `x1` 去掉,但这道题的范围是 `1-26`,当 `x1` 是 `26` 的时候,除以 `26` 后等式会变成下边的样子 - -$$...x_4\times26^2+x_3\times26^1+x_2\times26^0+1=2591/26$$ - -所以当我们再模 `26` 去求 `x2` 的话就会发生错误。 - -修改的话,当我们发现当前位是 `26` 的时候,我们应该在等式两边减去一个 `1` 。 - -$$...x_4\times26^3+x_3\times26^2+x_2\times26^1+(x_1-1)\times26^0=2591 - 1$$ - -这样两边再同时除以 `26` 的时候,就可以把 `x1` 去掉了。 - -代码修正后如下。 - -```java -public String convertToTitle(int n) { - StringBuilder sb = new StringBuilder(); - while (n > 0) { - int c = n % 26; - if(c == 0){ - c = 26; - n -= 1; - } - sb.insert(0, (char) ('A' + c - 1)); - n /= 26; - } - return sb.toString(); -} -``` - -# 总 - -这道题看做是进制转换问题的变形,只要知道进制转换的原理,改这道题的话也不难。 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/168.jpg) + +把数字根据对应规则转为字母。 + +# 解法一 + +这道题的话本质上其实就是进制转换,把 `10` 进制转为 `26` 进制表示,可以看一下 [这篇文章](https://zhuanlan.zhihu.com/p/75006709) 对进制转换有个更深的了解。 + +先讨论一下我们熟悉的 `10` 进制和 `16` 进制的转换。 + +`16` 进制中,`0` 到 `9` 还是正常的数字,然后增加字母 `A` 表示 `10`,字母 `B` 表示 `11`... 以此类推,直到 `F` 表示 `15`,所以我们各个位的取值范围是 `0 - 15` 。 + +假如 `16` 的进制的 `A1F` 转换为 `10` 进制的话就用下边的式子,其中 `A` 表示 `10`, `F` 表示 `15`。 + +$$10\times16^2+1\times16^1+15\times16^0=2591$$ + +那么假如我们知道的是 `10` 进制的 `2591`,怎么转为 `16` 进制呢? + +我们把上边的等式一般化,设我们要求的每一位分别是 `x1,x2,x3...`,事先我们并不知道有多少位。 + +$$...x_4\times16^3+x_3\times16^2+x_2\times16^1+x_1\times16^0=2591$$ + +我们可以在等式两边同时模上 `16`,等式就变成 + +$$x_1=2591mod16=15$$ + +这样我们就求出来了 `x1`,接下来我们在等式两边同时除以 `16`。等式左边由于 `x1` 的范围是 `0 - 15`,所以在整数间运算不管 `x1` 是多少,`x1/16` 都等于 `0`,所以等式就变成了下边的样子 + +$$...x_4\times16^2+x_3\times16^1+x_2\times16^0=2591/16=161$$ + +和之前对比, `16` 的幂都小了 `1`,同时 `x1` 消失了。 + +接下来就可以重复上边的两个步骤,模 `16` 和除以 `16`,就可以依次求出 `x2`、`x3` ...了。 + +直到除以 `16` 后变成 `0` ,就可以结束了。 + +对于 `10` 进制转 `26` 进制的话是一样的道理,只不过每次采用模 `26` 和除以 `26`。 + +所以代码的话,可以直接写出来。 + +```java +public String convertToTitle(int n) { + StringBuilder sb = new StringBuilder(); + while (n > 0) { + int c = n % 26; + sb.insert(0, (char) ('A' + c - 1)); + n /= 26; + } + return sb.toString(); +} +``` + +上边 `(char) ('A' + c - 1)` 这里的话,算是一个技巧,我们是通过对 `A` 的偏移,强制将整数转为了字符。 + +比如 `c = 3`,那么 `'A' + c - 1`,会把 `A` 根据 ASCII 码值转为 `65`,然后计算 `65 + c - 1 = 65 + 3 - 1 = 67`,然后 `(char)67`,根据 ASCII 码值就刚好是字符 `C`。 + +但是上边的算法还是有问题的,因为题目中每个位的范围是 `1-26`,而不是`0-25`。 + +所以取余的话对于 `1-25` 的数字结果都是正确的,但如果当前的位是 `26`,余数求出来就是 `0`,此时我们需要把它修正为 `26`。 + +此外,我们每次除以 `26` 是为了把最低位去掉,回忆下之前的等式 + +$$...x_4\times26^3+x_3\times26^2+x_2\times26^1+x_1\times26^0=2591$$ + +因为 `x1` 的范围正常情况下是 `0-25` ,所以除以 `26` 可以把 `x1` 去掉,但这道题的范围是 `1-26`,当 `x1` 是 `26` 的时候,除以 `26` 后等式会变成下边的样子 + +$$...x_4\times26^2+x_3\times26^1+x_2\times26^0+1=2591/26$$ + +所以当我们再模 `26` 去求 `x2` 的话就会发生错误。 + +修改的话,当我们发现当前位是 `26` 的时候,我们应该在等式两边减去一个 `1` 。 + +$$...x_4\times26^3+x_3\times26^2+x_2\times26^1+(x_1-1)\times26^0=2591 - 1$$ + +这样两边再同时除以 `26` 的时候,就可以把 `x1` 去掉了。 + +代码修正后如下。 + +```java +public String convertToTitle(int n) { + StringBuilder sb = new StringBuilder(); + while (n > 0) { + int c = n % 26; + if(c == 0){ + c = 26; + n -= 1; + } + sb.insert(0, (char) ('A' + c - 1)); + n /= 26; + } + return sb.toString(); +} +``` + +# 总 + +这道题看做是进制转换问题的变形,只要知道进制转换的原理,改这道题的话也不难。 + 区别就在于题目规定的数字中没有 `0` ,换句话讲,正常的 `26` 进制本应该满 `26` 进 `1`,然后低位补 `0`,但是这里满 `26` 的话就用 `26` 表示。满 `27` 的时候才会向前进 `1`,然后低位补 `1`。所以 `Z(26)` 的下一个数字就是 `A(1)A(1)`,即 `27` 对应 `AA`。 \ No newline at end of file diff --git a/leetcode-169-Majority-Element.md b/leetcode-169-Majority-Element.md index 2e738ebfc..3767915b2 100644 --- a/leetcode-169-Majority-Element.md +++ b/leetcode-169-Majority-Element.md @@ -1,115 +1,115 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/169.jpg) - -给一个数组,存在一个数字超过了半数,找出这个数。 - -# 解法一 - -这种计数问题,直接就会想到 `HashMap`,遍历过程中统计每个数字出现的个数即可。可以确定的是,超过半数的数字一定有且只有一个。所以在计数过程中如果出现了超过半数的数字,我们可以立刻返回。 - -```java -public int majorityElement(int[] nums) { - HashMap map = new HashMap<>(); - int n = nums.length; - for (int i = 0; i < nums.length; i++) { - int before = map.getOrDefault(nums[i], 0); - if (before == n / 2) { - return nums[i]; - } - map.put(nums[i], before + 1); - } - //随便返回一个 - return -1; -} -``` - -上边的解法时间复杂度是 `O(n)`,同时也需要 `O(n)` 的空间复杂度。所以下边讨论在保证时间复杂度不变的情况下,空间复杂度为 `O(1)` 的解法。 - -# 解法二 位运算 - -看到 [这里](https://leetcode.com/problems/majority-element/discuss/51612/C%2B%2B-6-Solutions) 介绍的。 - -[137 题](https://leetcode.wang/leetcode-137-Single-NumberII.html) 解法三中已经用过这个思想了,就是把数字放眼到二进制的形式,举个例子。 - -```java -5 5 2 1 2 2 2 都写成 2 进制 -1 0 1 -1 0 1 -0 1 0 -0 0 1 -0 1 0 -0 1 0 -0 1 0 -``` - -由于 `2` 是超过半数的数,它的二进制是 `010`,所以对于从右边数第一列一定是 `0` 超过半数,从右边数第二列一定是 `1` 超过半数,从右边数第三列一定是 `0` 超过半数。然后每一列超过半数的 `0,1,0` 用 `10`进制表示就是 `2` 。 - -所以我们只需要统计每一列超过半数的数,`0` 或者 `1`,然后这些超过半数的二进制位组成一个数字,就是我们要找的数。 - -当然,我们可以只统计 `1` 的个数,让每一位开始默认为 `0`,如果发现某一列的 `1` 的个数超过半数,就将当前位改为 `1`。 - -具体算法通过按位与和按位或实现。 - -```java -public int majorityElement(int[] nums) { - int majority = 0; - int n = nums.length; - //判断每一位 - for (int i = 0, mask = 1; i < 32; i++, mask <<= 1) { - int bits = 0; - //记录当前列 1 的个数 - for (int j = 0; j < n; j++) { - if ((mask & nums[j]) == mask) { - bits++; - } - } - //当前列 1 的个数是否超过半数 - if (bits > n / 2) { - majority |= mask; - } - } - return majority; -} -``` - -# 解法三 摩尔投票法 - -1980 年由 Boyer 和 Moore 两个人提出来的算法,英文是 [Boyer-Moore Majority Vote Algorithm](http://www.cs.utexas.edu/~moore/best-ideas/mjrty/])。 - -算法思想很简单,但第一个想出来的人是真的强。 - -我们假设这样一个场景,在一个游戏中,分了若干个队伍,有一个队伍的人数超过了半数。所有人的战力都相同,不同队伍的两个人遇到就是同归于尽,同一个队伍的人遇到当然互不伤害。 - -这样经过充分时间的游戏后,最后的结果是确定的,一定是超过半数的那个队伍留在了最后。 - -而对于这道题,我们只需要利用上边的思想,把数组的每个数都看做队伍编号,然后模拟游戏过程即可。 - -`group` 记录当前队伍的人数,`count` 记录当前队伍剩余的人数。如果当前队伍剩余人数为 `0`,记录下次遇到的人的所在队伍号。 - -```java -public int majorityElement(int[] nums) { - int count = 1; - int group = nums[0]; - for (int i = 1; i < nums.length; i++) { - //当前队伍人数为零,记录现在遇到的人的队伍号 - if (count == 0) { - count = 1; - group = nums[i]; - continue; - } - //现在遇到的人和当前队伍同组,人数加 1 - if (nums[i] == group) { - count++; - //遇到了其他队伍的人,人数减 1 - } else { - count--; - } - } - return group; - } -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/169.jpg) + +给一个数组,存在一个数字超过了半数,找出这个数。 + +# 解法一 + +这种计数问题,直接就会想到 `HashMap`,遍历过程中统计每个数字出现的个数即可。可以确定的是,超过半数的数字一定有且只有一个。所以在计数过程中如果出现了超过半数的数字,我们可以立刻返回。 + +```java +public int majorityElement(int[] nums) { + HashMap map = new HashMap<>(); + int n = nums.length; + for (int i = 0; i < nums.length; i++) { + int before = map.getOrDefault(nums[i], 0); + if (before == n / 2) { + return nums[i]; + } + map.put(nums[i], before + 1); + } + //随便返回一个 + return -1; +} +``` + +上边的解法时间复杂度是 `O(n)`,同时也需要 `O(n)` 的空间复杂度。所以下边讨论在保证时间复杂度不变的情况下,空间复杂度为 `O(1)` 的解法。 + +# 解法二 位运算 + +看到 [这里](https://leetcode.com/problems/majority-element/discuss/51612/C%2B%2B-6-Solutions) 介绍的。 + +[137 题](https://leetcode.wang/leetcode-137-Single-NumberII.html) 解法三中已经用过这个思想了,就是把数字放眼到二进制的形式,举个例子。 + +```java +5 5 2 1 2 2 2 都写成 2 进制 +1 0 1 +1 0 1 +0 1 0 +0 0 1 +0 1 0 +0 1 0 +0 1 0 +``` + +由于 `2` 是超过半数的数,它的二进制是 `010`,所以对于从右边数第一列一定是 `0` 超过半数,从右边数第二列一定是 `1` 超过半数,从右边数第三列一定是 `0` 超过半数。然后每一列超过半数的 `0,1,0` 用 `10`进制表示就是 `2` 。 + +所以我们只需要统计每一列超过半数的数,`0` 或者 `1`,然后这些超过半数的二进制位组成一个数字,就是我们要找的数。 + +当然,我们可以只统计 `1` 的个数,让每一位开始默认为 `0`,如果发现某一列的 `1` 的个数超过半数,就将当前位改为 `1`。 + +具体算法通过按位与和按位或实现。 + +```java +public int majorityElement(int[] nums) { + int majority = 0; + int n = nums.length; + //判断每一位 + for (int i = 0, mask = 1; i < 32; i++, mask <<= 1) { + int bits = 0; + //记录当前列 1 的个数 + for (int j = 0; j < n; j++) { + if ((mask & nums[j]) == mask) { + bits++; + } + } + //当前列 1 的个数是否超过半数 + if (bits > n / 2) { + majority |= mask; + } + } + return majority; +} +``` + +# 解法三 摩尔投票法 + +1980 年由 Boyer 和 Moore 两个人提出来的算法,英文是 [Boyer-Moore Majority Vote Algorithm](http://www.cs.utexas.edu/~moore/best-ideas/mjrty/])。 + +算法思想很简单,但第一个想出来的人是真的强。 + +我们假设这样一个场景,在一个游戏中,分了若干个队伍,有一个队伍的人数超过了半数。所有人的战力都相同,不同队伍的两个人遇到就是同归于尽,同一个队伍的人遇到当然互不伤害。 + +这样经过充分时间的游戏后,最后的结果是确定的,一定是超过半数的那个队伍留在了最后。 + +而对于这道题,我们只需要利用上边的思想,把数组的每个数都看做队伍编号,然后模拟游戏过程即可。 + +`group` 记录当前队伍的人数,`count` 记录当前队伍剩余的人数。如果当前队伍剩余人数为 `0`,记录下次遇到的人的所在队伍号。 + +```java +public int majorityElement(int[] nums) { + int count = 1; + int group = nums[0]; + for (int i = 1; i < nums.length; i++) { + //当前队伍人数为零,记录现在遇到的人的队伍号 + if (count == 0) { + count = 1; + group = nums[i]; + continue; + } + //现在遇到的人和当前队伍同组,人数加 1 + if (nums[i] == group) { + count++; + //遇到了其他队伍的人,人数减 1 + } else { + count--; + } + } + return group; + } +``` + +# 总 + 解法一用 `HashMap` 计数的方法经常用到,很容易想到。解法二把数字放眼到二进制的世界,也算是经常用到了。解法三只能说 666 了,太强了,神仙操作。 \ No newline at end of file diff --git a/leetcode-171-Excel-Sheet-Column-Number.md b/leetcode-171-Excel-Sheet-Column-Number.md index d24da4463..b6eaaefce 100644 --- a/leetcode-171-Excel-Sheet-Column-Number.md +++ b/leetcode-171-Excel-Sheet-Column-Number.md @@ -1,85 +1,85 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/171.jpg) - -根据对应规则,将字符串转为对应的数字。 - -# 解法一 - -这道题就是 [168](https://leetcode.wang/leetcode-168-Excel-Sheet-Column-Title.html) 题的逆过程,其实之前已经讲过怎么转换了,可以先过去看一下。 - -类比于我们最熟悉的十进制,对于 `2019` 可以看成下边的样子。 - -$$2\times10^3+0\times10^2+1\times10^1+9\times10^0=2019$$ - -这道题本质上其实就是一个稍微有些不一样的 `26` 进制,具体为什么在 [168](https://leetcode.wang/leetcode-168-Excel-Sheet-Column-Title.html) 题中已经分析过了。 - -转换的话,其实只需要把上边基数 `10` 换成 `26` 即可。 - -$$...x_4\times26^3+x_3\times26^2+x_2\times26^1+x_1\times26^0$$ - -所以给定一个数的时候,我们可以从右往左算,依次乘 `26` 的 `0,1,2...` 次幂,再累加即可。 - -```java -public int titleToNumber(String s) { - char[] c = s.toCharArray(); - int res = 0; - int mul = 1; - for (int i = c.length - 1; i >= 0; i--) { - res = res + mul * (c[i] - 'A' + 1); - mul *= 26; - } - return res; -} -``` - -`c[i] - 'A' + 1` 这里字符做差,就相当于 ASCII 码对应的数字做差,从而算出当前字母对应的数字。 - -# 解法二 - -上边是比较直接的解法,在 [这里](https://leetcode.com/problems/excel-sheet-column-number/discuss/52091/Here-is-my-java-solution) 又看到另外一种解法。 - -上边的解法我们是倒着遍历的,那么我们能不能正着遍历呢?换言之,如果先给你高位的数,再给你低位的数,你怎么进行累加呢。 - -其实在十进制运算中我们经常使用的,比如要还原的数字是 `2019`,依次给你数字 `2,0,1,9`。就可以用下边的算法。 - -```java -int res = 0; -res = res * 10 + 2; //2 -res = res * 10 + 0; //20 -res = res * 10 + 1; //201 -res = res * 10 + 9; //2019 -``` - -直观上,我们每次乘 `10` 就相当于把每一位左移了一位,然后再把当前位加到低位。 - -那么具体上是为什么呢?还是要回到我们的等式 - -$$2\times10^3+0\times10^2+1\times10^1+9\times10^0=2019$$ - -将所有的 `10` 提取出来 。 - -$$10\times(2\times10^2+0\times10^1+1\times10^0)+9=2019$$ - -$$10\times(10\times(2\times10^1+0\times10^0)+1)+9=2019$$ - -$$10\times(10\times(10\times(10\times0 + 2)+0)+1)+9=2019$$ - -然后我们就会发现,我们每次做的就是将结果乘以 `10`,然后加上给定的数字。 - -而对于 `26` 进制是一样的道理,只需要把 `10` 改成 `26` 即可。 - -```java -public int titleToNumber(String s) { - char[] c = s.toCharArray(); - int res = 0; - for (int i = 0; i < c.length; i++) { - res = res * 26 + (c[i] - 'A' + 1); - } - return res; -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/171.jpg) + +根据对应规则,将字符串转为对应的数字。 + +# 解法一 + +这道题就是 [168](https://leetcode.wang/leetcode-168-Excel-Sheet-Column-Title.html) 题的逆过程,其实之前已经讲过怎么转换了,可以先过去看一下。 + +类比于我们最熟悉的十进制,对于 `2019` 可以看成下边的样子。 + +$$2\times10^3+0\times10^2+1\times10^1+9\times10^0=2019$$ + +这道题本质上其实就是一个稍微有些不一样的 `26` 进制,具体为什么在 [168](https://leetcode.wang/leetcode-168-Excel-Sheet-Column-Title.html) 题中已经分析过了。 + +转换的话,其实只需要把上边基数 `10` 换成 `26` 即可。 + +$$...x_4\times26^3+x_3\times26^2+x_2\times26^1+x_1\times26^0$$ + +所以给定一个数的时候,我们可以从右往左算,依次乘 `26` 的 `0,1,2...` 次幂,再累加即可。 + +```java +public int titleToNumber(String s) { + char[] c = s.toCharArray(); + int res = 0; + int mul = 1; + for (int i = c.length - 1; i >= 0; i--) { + res = res + mul * (c[i] - 'A' + 1); + mul *= 26; + } + return res; +} +``` + +`c[i] - 'A' + 1` 这里字符做差,就相当于 ASCII 码对应的数字做差,从而算出当前字母对应的数字。 + +# 解法二 + +上边是比较直接的解法,在 [这里](https://leetcode.com/problems/excel-sheet-column-number/discuss/52091/Here-is-my-java-solution) 又看到另外一种解法。 + +上边的解法我们是倒着遍历的,那么我们能不能正着遍历呢?换言之,如果先给你高位的数,再给你低位的数,你怎么进行累加呢。 + +其实在十进制运算中我们经常使用的,比如要还原的数字是 `2019`,依次给你数字 `2,0,1,9`。就可以用下边的算法。 + +```java +int res = 0; +res = res * 10 + 2; //2 +res = res * 10 + 0; //20 +res = res * 10 + 1; //201 +res = res * 10 + 9; //2019 +``` + +直观上,我们每次乘 `10` 就相当于把每一位左移了一位,然后再把当前位加到低位。 + +那么具体上是为什么呢?还是要回到我们的等式 + +$$2\times10^3+0\times10^2+1\times10^1+9\times10^0=2019$$ + +将所有的 `10` 提取出来 。 + +$$10\times(2\times10^2+0\times10^1+1\times10^0)+9=2019$$ + +$$10\times(10\times(2\times10^1+0\times10^0)+1)+9=2019$$ + +$$10\times(10\times(10\times(10\times0 + 2)+0)+1)+9=2019$$ + +然后我们就会发现,我们每次做的就是将结果乘以 `10`,然后加上给定的数字。 + +而对于 `26` 进制是一样的道理,只需要把 `10` 改成 `26` 即可。 + +```java +public int titleToNumber(String s) { + char[] c = s.toCharArray(); + int res = 0; + for (int i = 0; i < c.length; i++) { + res = res * 26 + (c[i] - 'A' + 1); + } + return res; +} +``` + +# 总 + 这道题依旧是进制转换。 \ No newline at end of file diff --git a/leetcode-172-Factorial-Trailing-Zeroes.md b/leetcode-172-Factorial-Trailing-Zeroes.md index a6647a6cb..396f8295c 100644 --- a/leetcode-172-Factorial-Trailing-Zeroes.md +++ b/leetcode-172-Factorial-Trailing-Zeroes.md @@ -1,87 +1,87 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/172.jpg) - -给定一个数,求出一个数的阶乘末尾有多少个 0。 - -# 解法一 - -之前小红书面试的时候碰到的一道题,没想到又是 leetcode 的原题。这种没有通用解法的题,完全依靠于对题目的分析理解了,自己当时也是在面试官的提示下慢慢出来的,要是想不到题目的点,还是比较难做的。 - -首先肯定不能依赖于把阶乘算出来再去判断有多少个零了,因为阶乘很容易就溢出了,所以先一步一步理一下思路吧。 - -首先末尾有多少个 `0` ,只需要给当前数乘以一个 `10` 就可以加一个 `0`。 - -再具体对于 `5!`,也就是 `5 * 4 * 3 * 2 * 1 = 120`,我们发现结果会有一个 `0`,原因就是 `2` 和 `5` 相乘构成了一个 `10`。而对于 `10` 的话,其实也只有 `2 * 5` 可以构成,所以我们只需要找有多少对 `2/5`。 - -我们把每个乘数再稍微分解下,看一个例子。 - -`11! = 11 * 10 * 9 * 8 * 7 * 6 * 5 * 4 * 3 * 2 * 1 = 11 * (2 * 5) * 9 * (4 * 2) * 7 * (3 * 2) * (1 * 5) * (2 * 2) * 3 * (1 * 2) * 1 ` - -对于含有 `2` 的因子的话是 `1 * 2, 2 * 2, 3 * 2, 4 * 2 ...` - -对于含有 `5` 的因子的话是 `1 * 5, 2 * 5...` - -含有 `2` 的因子每两个出现一次,含有 `5` 的因子每 `5` 个出现一次,所有 `2` 出现的个数远远多于 `5`,换言之找到一个 `5`,一定能找到一个 `2` 与之配对。所以我们只需要找有多少个 `5`。 - -直接的,我们只需要判断每个累乘的数有多少个 `5` 的因子即可。 - -```java -public int trailingZeroes(int n) { - int count = 0; - for (int i = 1; i <= n; i++) { - int N = i; - while (N > 0) { - if (N % 5 == 0) { - count++; - N /= 5; - } else { - break; - } - } - } - return count; - -} -``` - -![](https://windliang.oss-cn-beijing.aliyuncs.com/172_2.jpg) - -但发生了超时,我们继续分析。 - -对于一个数的阶乘,就如之前分析的,`5` 的因子一定是每隔 `5` 个数出现一次,也就是下边的样子。 - -`n! = 1 * 2 * 3 * 4 * (1 * 5) * ... * (2 * 5) * ... * (3 * 5) *... * n` - -因为每隔 `5` 个数出现一个 `5`,所以计算出现了多少个 `5`,我们只需要用 `n/5` 就可以算出来。 - -但还没有结束,继续分析。 - -`... * (1 * 5) * ... * (1 * 5 * 5) * ... * (2 * 5 * 5) * ... * (3 * 5 * 5) * ... * n` - -每隔 `25` 个数字,出现的是两个 `5`,所以除了每隔 `5` 个数算作一个 `5`,每隔 `25` 个数,还需要多算一个 `5`。 - -也就是我们需要再加上 `n / 25` 个 `5`。 - -同理我们还会发现每隔 `5 * 5 * 5 = 125 ` 个数字,会出现 `3` 个 `5`,所以我们还需要再加上 `n / 125` 。 - -综上,规律就是每隔 `5` 个数,出现一个 `5`,每隔 `25` 个数,出现 `2` 个 `5`,每隔 `125` 个数,出现 `3` 个 `5`... 以此类推。 - -最终 `5` 的个数就是 `n / 5 + n / 25 + n / 125 ...` - -写程序的话,如果直接按照上边的式子计算,分母可能会造成溢出。所以算 `n / 25` 的时候,我们先把 `n` 更新,`n = n / 5`,然后再计算 `n / 5` 即可。后边的同理。 - -```java -public int trailingZeroes(int n) { - int count = 0; - while (n > 0) { - count += n / 5; - n = n / 5; - } - return count; -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/172.jpg) + +给定一个数,求出一个数的阶乘末尾有多少个 0。 + +# 解法一 + +之前小红书面试的时候碰到的一道题,没想到又是 leetcode 的原题。这种没有通用解法的题,完全依靠于对题目的分析理解了,自己当时也是在面试官的提示下慢慢出来的,要是想不到题目的点,还是比较难做的。 + +首先肯定不能依赖于把阶乘算出来再去判断有多少个零了,因为阶乘很容易就溢出了,所以先一步一步理一下思路吧。 + +首先末尾有多少个 `0` ,只需要给当前数乘以一个 `10` 就可以加一个 `0`。 + +再具体对于 `5!`,也就是 `5 * 4 * 3 * 2 * 1 = 120`,我们发现结果会有一个 `0`,原因就是 `2` 和 `5` 相乘构成了一个 `10`。而对于 `10` 的话,其实也只有 `2 * 5` 可以构成,所以我们只需要找有多少对 `2/5`。 + +我们把每个乘数再稍微分解下,看一个例子。 + +`11! = 11 * 10 * 9 * 8 * 7 * 6 * 5 * 4 * 3 * 2 * 1 = 11 * (2 * 5) * 9 * (4 * 2) * 7 * (3 * 2) * (1 * 5) * (2 * 2) * 3 * (1 * 2) * 1 ` + +对于含有 `2` 的因子的话是 `1 * 2, 2 * 2, 3 * 2, 4 * 2 ...` + +对于含有 `5` 的因子的话是 `1 * 5, 2 * 5...` + +含有 `2` 的因子每两个出现一次,含有 `5` 的因子每 `5` 个出现一次,所有 `2` 出现的个数远远多于 `5`,换言之找到一个 `5`,一定能找到一个 `2` 与之配对。所以我们只需要找有多少个 `5`。 + +直接的,我们只需要判断每个累乘的数有多少个 `5` 的因子即可。 + +```java +public int trailingZeroes(int n) { + int count = 0; + for (int i = 1; i <= n; i++) { + int N = i; + while (N > 0) { + if (N % 5 == 0) { + count++; + N /= 5; + } else { + break; + } + } + } + return count; + +} +``` + +![](https://windliang.oss-cn-beijing.aliyuncs.com/172_2.jpg) + +但发生了超时,我们继续分析。 + +对于一个数的阶乘,就如之前分析的,`5` 的因子一定是每隔 `5` 个数出现一次,也就是下边的样子。 + +`n! = 1 * 2 * 3 * 4 * (1 * 5) * ... * (2 * 5) * ... * (3 * 5) *... * n` + +因为每隔 `5` 个数出现一个 `5`,所以计算出现了多少个 `5`,我们只需要用 `n/5` 就可以算出来。 + +但还没有结束,继续分析。 + +`... * (1 * 5) * ... * (1 * 5 * 5) * ... * (2 * 5 * 5) * ... * (3 * 5 * 5) * ... * n` + +每隔 `25` 个数字,出现的是两个 `5`,所以除了每隔 `5` 个数算作一个 `5`,每隔 `25` 个数,还需要多算一个 `5`。 + +也就是我们需要再加上 `n / 25` 个 `5`。 + +同理我们还会发现每隔 `5 * 5 * 5 = 125 ` 个数字,会出现 `3` 个 `5`,所以我们还需要再加上 `n / 125` 。 + +综上,规律就是每隔 `5` 个数,出现一个 `5`,每隔 `25` 个数,出现 `2` 个 `5`,每隔 `125` 个数,出现 `3` 个 `5`... 以此类推。 + +最终 `5` 的个数就是 `n / 5 + n / 25 + n / 125 ...` + +写程序的话,如果直接按照上边的式子计算,分母可能会造成溢出。所以算 `n / 25` 的时候,我们先把 `n` 更新,`n = n / 5`,然后再计算 `n / 5` 即可。后边的同理。 + +```java +public int trailingZeroes(int n) { + int count = 0; + while (n > 0) { + count += n / 5; + n = n / 5; + } + return count; +} +``` + +# 总 + 更偏向于数学题,主要是对问题的归纳总结。 \ No newline at end of file diff --git a/leetcode-173-Binary-Search-Tree-Iterator.md b/leetcode-173-Binary-Search-Tree-Iterator.md index 5dda980f4..c467b2085 100644 --- a/leetcode-173-Binary-Search-Tree-Iterator.md +++ b/leetcode-173-Binary-Search-Tree-Iterator.md @@ -1,132 +1,132 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/173.png) - -一个二叉查找树,实现一个迭代器。`next` 依次返回树中最小的值,`hasNext` 返回树中是否还有未返回的元素。 - -> 二叉查找树是指一棵空树或者具有下列性质的二叉树: -> -> 1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值; -> 2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值; -> 3. 任意节点的左、右子树也分别为二叉查找树; -> 4. 没有键值相等的节点。 - -# 思路分析 - -如果做过 [108 题](https://leetcode.wang/leetcode-108-Convert-Sorted-Array-to-Binary-Search-Tree.html) 和 [109 题](https://leetcode.wang/leetcode-109-Convert-Sorted-List-to-Binary-Search-Tree.html) ,这里看到二分查找树,一定会想到它的一个性质。「二分查找树的中序遍历依次得到的元素刚好是一个升序数组」,所以这道题无非就是把中序遍历的结果依次输出即可。 - -至于中序遍历,在 [94 题](https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html) 已经做过了,里边介绍了三种解法,下边的解法也是依赖于之前中序遍历的代码,大家可以先过去看一下。 - -# 解法一 - -先不考虑题目 `Note` 中要求的空间复杂度和时间复杂度,简单粗暴一些。在构造函数中,对二叉树进行中序遍历,把结果保存到一个队列中,然后 `next` 方法直接执行出队操作即可。至于 `hasNext` 方法的话,判断队列是否为空即可。 - -```java -class BSTIterator { - - Queue queue = new LinkedList<>(); - - public BSTIterator(TreeNode root) { - inorderTraversal(root); - } - - private void inorderTraversal(TreeNode root) { - if (root == null) { - return; - } - inorderTraversal(root.left); - queue.offer(root.val); - inorderTraversal(root.right); - } - - /** @return the next smallest number */ - public int next() { - return queue.poll(); - } - - /** @return whether we have a next smallest number */ - public boolean hasNext() { - return !queue.isEmpty(); - } -} -``` - -时间复杂度的话,构造函数因为遍历了一遍二叉树,所以是 `O(n)`,对于 `next` 和 `hasNext` 方法都是 `O(1)`。 - -空间复杂度,用队列保存了所有的节点值,所以是 `O(n)`,此外中序遍历递归压栈的过程也需要 `O(h)` 的空间。 - -# 解法二 - -解法一中我们把所有节点都保存了起来,其实没必要一次性保存所有节点,而是需要一个输出一个即可。 - -所以我们要控制中序遍历的进程,不要让它一次性结束,如果用解法一递归的方法去遍历那就很难控制了,所以自然而然的会想到用栈模拟递归的过程。 - -下边是 [94 题](https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html) 解法二的代码。 - -```java -public List inorderTraversal(TreeNode root) { - List ans = new ArrayList<>(); - Stack stack = new Stack<>(); - TreeNode cur = root; - while (cur != null || !stack.isEmpty()) { - //节点不为空一直压栈 - while (cur != null) { - stack.push(cur); - cur = cur.left; //考虑左子树 - } - //节点为空,就出栈 - cur = stack.pop(); - //当前值加入 - ans.add(cur.val); - //考虑右子树 - cur = cur.right; - } - return ans; -} -``` - -和这道题糅合一起也很简单了,只需要把 `stack` 和 `cur` 作为成员变量,然后每次调用 `next` 就执行一次 `while` 循环,并且要记录当前值,结束掉本次循环。 - -```java -class BSTIterator { - Stack stack = new Stack<>(); - TreeNode cur = null; - - public BSTIterator(TreeNode root) { - cur = root; - } - - /** @return the next smallest number */ - public int next() { - int res = -1; - while (cur != null || !stack.isEmpty()) { - // 节点不为空一直压栈 - while (cur != null) { - stack.push(cur); - cur = cur.left; // 考虑左子树 - } - // 节点为空,就出栈 - cur = stack.pop(); - res = cur.val; - // 考虑右子树 - cur = cur.right; - break; - } - - return res; - } - - /** @return whether we have a next smallest number */ - public boolean hasNext() { - return cur != null || !stack.isEmpty(); - } -} -``` - -时间复杂度的话,对于 `next` 方法,大多数时候是 `O(1)`,但最坏情况因为最里边的 `while` 循环,其实有可能达到 `O(n)`。但如果算均摊时间复杂度的话,其实还是 `O(1)`,因为每个节点最多也就经过两次就出栈了。 - -空间复杂度,这里只需要消耗栈的空间,也就是 `O(h)`。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/173.png) + +一个二叉查找树,实现一个迭代器。`next` 依次返回树中最小的值,`hasNext` 返回树中是否还有未返回的元素。 + +> 二叉查找树是指一棵空树或者具有下列性质的二叉树: +> +> 1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值; +> 2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值; +> 3. 任意节点的左、右子树也分别为二叉查找树; +> 4. 没有键值相等的节点。 + +# 思路分析 + +如果做过 [108 题](https://leetcode.wang/leetcode-108-Convert-Sorted-Array-to-Binary-Search-Tree.html) 和 [109 题](https://leetcode.wang/leetcode-109-Convert-Sorted-List-to-Binary-Search-Tree.html) ,这里看到二分查找树,一定会想到它的一个性质。「二分查找树的中序遍历依次得到的元素刚好是一个升序数组」,所以这道题无非就是把中序遍历的结果依次输出即可。 + +至于中序遍历,在 [94 题](https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html) 已经做过了,里边介绍了三种解法,下边的解法也是依赖于之前中序遍历的代码,大家可以先过去看一下。 + +# 解法一 + +先不考虑题目 `Note` 中要求的空间复杂度和时间复杂度,简单粗暴一些。在构造函数中,对二叉树进行中序遍历,把结果保存到一个队列中,然后 `next` 方法直接执行出队操作即可。至于 `hasNext` 方法的话,判断队列是否为空即可。 + +```java +class BSTIterator { + + Queue queue = new LinkedList<>(); + + public BSTIterator(TreeNode root) { + inorderTraversal(root); + } + + private void inorderTraversal(TreeNode root) { + if (root == null) { + return; + } + inorderTraversal(root.left); + queue.offer(root.val); + inorderTraversal(root.right); + } + + /** @return the next smallest number */ + public int next() { + return queue.poll(); + } + + /** @return whether we have a next smallest number */ + public boolean hasNext() { + return !queue.isEmpty(); + } +} +``` + +时间复杂度的话,构造函数因为遍历了一遍二叉树,所以是 `O(n)`,对于 `next` 和 `hasNext` 方法都是 `O(1)`。 + +空间复杂度,用队列保存了所有的节点值,所以是 `O(n)`,此外中序遍历递归压栈的过程也需要 `O(h)` 的空间。 + +# 解法二 + +解法一中我们把所有节点都保存了起来,其实没必要一次性保存所有节点,而是需要一个输出一个即可。 + +所以我们要控制中序遍历的进程,不要让它一次性结束,如果用解法一递归的方法去遍历那就很难控制了,所以自然而然的会想到用栈模拟递归的过程。 + +下边是 [94 题](https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html) 解法二的代码。 + +```java +public List inorderTraversal(TreeNode root) { + List ans = new ArrayList<>(); + Stack stack = new Stack<>(); + TreeNode cur = root; + while (cur != null || !stack.isEmpty()) { + //节点不为空一直压栈 + while (cur != null) { + stack.push(cur); + cur = cur.left; //考虑左子树 + } + //节点为空,就出栈 + cur = stack.pop(); + //当前值加入 + ans.add(cur.val); + //考虑右子树 + cur = cur.right; + } + return ans; +} +``` + +和这道题糅合一起也很简单了,只需要把 `stack` 和 `cur` 作为成员变量,然后每次调用 `next` 就执行一次 `while` 循环,并且要记录当前值,结束掉本次循环。 + +```java +class BSTIterator { + Stack stack = new Stack<>(); + TreeNode cur = null; + + public BSTIterator(TreeNode root) { + cur = root; + } + + /** @return the next smallest number */ + public int next() { + int res = -1; + while (cur != null || !stack.isEmpty()) { + // 节点不为空一直压栈 + while (cur != null) { + stack.push(cur); + cur = cur.left; // 考虑左子树 + } + // 节点为空,就出栈 + cur = stack.pop(); + res = cur.val; + // 考虑右子树 + cur = cur.right; + break; + } + + return res; + } + + /** @return whether we have a next smallest number */ + public boolean hasNext() { + return cur != null || !stack.isEmpty(); + } +} +``` + +时间复杂度的话,对于 `next` 方法,大多数时候是 `O(1)`,但最坏情况因为最里边的 `while` 循环,其实有可能达到 `O(n)`。但如果算均摊时间复杂度的话,其实还是 `O(1)`,因为每个节点最多也就经过两次就出栈了。 + +空间复杂度,这里只需要消耗栈的空间,也就是 `O(h)`。 + +# 总 + 这道题关键就是要知道二叉搜寻树的中序遍历是升序序列,然后问题其实就转移到怎么进行中序遍历了。 \ No newline at end of file diff --git a/leetcode-174-Dungeon-Game.md b/leetcode-174-Dungeon-Game.md index aff72b1ce..6ba5bd478 100644 --- a/leetcode-174-Dungeon-Game.md +++ b/leetcode-174-Dungeon-Game.md @@ -1,351 +1,351 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/174.png) - -题目描述,任务是从左上角(K)走到右下角(P),初始的时候有一个生命值 `HP`。只能向右和向下走,格子上边的数值代表增加 `HP` 和减少 `HP`,一旦变为 `0`,就立刻结束,问初始的 `HP` 最小可以取多少,才能从 `K` 走到 `P`。注意如果 `P` 点是负值,也要保证到达 `P` 点后将 `P` 点的值减去后, `HP` 的值依旧大于 `0`。 - -# 解法一 回溯法 - -最直接暴力的方法就是做搜索了,在每个位置无非就是向右向下两种可能,然后去尝试所有的解,然后找到最小的即可,也就是做一个 `DFS` 或者说是回溯法。 - -```java -//全局变量去保存最小值 -int minHealth = Integer.MAX_VALUE; - -public int calculateMinimumHP(int[][] dungeon) { - //calculateMinimumHPHelper 四个参数 - //int x, int y, int health, int addHealth, int[][] dungeon - //x, y 代表要准备到的位置,x 代表是哪一列,y 代表是哪一行 - //health 代表当前的生命值 - //addHealth 代表当前已经增加的生命值 - //初始的时候给加 1 点血,addHealth 和 health 都是 1 - calculateMinimumHPHelper(0, 0, 1, 1, dungeon); - return minHealth; -} - -private void calculateMinimumHPHelper(int x, int y, int health, int addHealth, int[][] dungeon) { - //加上当前位置的奖励或惩罚 - health = health + dungeon[y][x]; - //此时是否需要加血,加血的话就将 health 加到 1 - if (health <= 0) { - addHealth = addHealth + Math.abs(health) + 1; - } - - //是否到了终点 - if (x == dungeon[0].length - 1 && y == dungeon.length - 1) { - minHealth = Math.min(addHealth, minHealth); - return; - } - - //是否加过血 - if (health <= 0) { - //加过血的话,health 就变为 1 - if (x < dungeon[0].length - 1) { - calculateMinimumHPHelper(x + 1, y, 1, addHealth, dungeon); - } - if (y < dungeon.length - 1) { - calculateMinimumHPHelper(x, y + 1, 1, addHealth, dungeon); - } - } else { - //没加过血的话,health 就是当前的 health - if (x < dungeon[0].length - 1) { - calculateMinimumHPHelper(x + 1, y, health, addHealth, dungeon); - } - if (y < dungeon.length - 1) { - calculateMinimumHPHelper(x, y + 1, health, addHealth, dungeon); - } - } - -} -``` - -然后结果是意料之中的,会超时。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/174_2.jpg) - -然后我们就需要剪枝,将一些情况提前结束掉,最容易想到的就是,如果当前加的血已经超过了全局最小值,那就可以直接结束,不用进后边的递归。 - -```java -if (addHealth > minHealth) { - return; -} -``` - -然后发现对于给定的 `test case` 并没有什么影响。 - -之所以超时,就是因为我们会经过很多重复的位置,比如 - -```java -0 1 2 -3 4 5 -6 7 8 -如果按 DFS,第一条路径就是 0 -> 1 -> 2 -> 5 -> 8 -然后通过回溯,第二次判断的路径就会是 0 -> 1 -> 4 -> 5 -> ... -我们会发现它又会来到 5 这个位置 -其他的也类似,如果表格很大的话,不停的回溯,一些位置会经过很多次 -``` - -接下来,就会想到用 `map` 去缓冲我们过程中求出的解,`key` 话当然是 `x` 和 `y` 了,`value` 呢?存当前的 `health` 和 `addhealth`?那第二次来到这个位置的时候,我们并不能做什么,比如举个例子。 - -第一次来到 `(3,5)` 的时候,`health` 是 `5`,`addhealth` 是 `6`。 - -第二次来到 `(3,5)` 的时候,`health` 是 `4`,`addhealth` 是 `7`,我们什么也做不了,我们并不知道未来它会走什么路。 - -因为走的路是由 `health` 和 `addhealth` 共同决定的,此时来到相同的位置,由于 `health` 和 `addhealth` 都不一样,所以未来的路也很有可能变化,所以我们并不能通过缓冲结果来剪枝。 - -我们最多能判断当 `x`、`y`、`health` 和 `addhealth` 全部相同的时候提前结束,但这种情况也很少,所以并不能有效的加快搜索速度。 - -这条路看起来到死路了,我们换个思路,去用动态规划。 - -动态规划的关键就是去定义我们的状态了,这里直接将要解决的问题定义为我们的状态。 - -用 `dp[i][j]` 存储从起点 `(0, 0)` 到达 `(i, j)` 时候所需要的最小初始生命值。 - -到达 `(i,j)` 有两个点,`(i-1, j)` 和 `(i, j-1)`。 - -接下来就需要去推导状态转移方程了。 - -```java -* * 8 * -* 7 ! ? -? ? ? ? -``` - -假如我们要求上图中 `!` 位置的 `dp`,假设之前的 `dp` 已经都求出来了。 - -那么 `dp` 是等于感叹号上边的 `dp` 还是等于它左边的 `dp` 呢?选较小的吗? - -但如果 `8` 对应的当时的 `health` 是 `100`,而 `7` 对应的是 `5`,此时更好的选择应该是 `8`。 - -那就选 `health` 大的呗,那 `dp` 不管了吗?极端的例子,假如此时的位置已经是终点了,很明显我们应该选择从左边过来,也就是 `7` 的位置过来,之前的 `health` 并不重要了。 - -所以推到这里会发现,因为我们有两个不确定的变量,一个是 `dp` ,也就是从起点 `(0, 0)` 到达 `(i, j)` 时候所需要的最小初始生命值,还有一个就是当时剩下的生命值。 - -当更新 `dp` 的时候我们并不知道它应该是从上边下来,还是从左边过来有利于到达终点的时候所需的初始生命值最小。 - -换句话讲,依赖过去的状态,并不能指导我们当前的选择,因为还需要未来的信息。 - -所以到这里,我再次走到了死胡同,就去看 `Discuss` 了,这里分享下别人的做法。 - -# 解法二 递归 - -看到 [这里](https://leetcode.com/problems/dungeon-game/discuss/52790/My-AC-Java-Version-Suggestions-are-welcome) 评论区的一个解法。 - -所需要做的就是将上边动态规划的思路逆转一下。 - -``` - ↓ -→ * -``` - -之前我们考虑的是当前这个位置,它应该是从上边下来还是左边过来会更好些,然后发现并不能确定。 - -现在的话,看下边的图。 - -```java -* → x -↓ -y -``` - -我们现在考虑从当前位置,应该是向右走还是向下走,这样我们是可以确定的。 - -如果我们知道右边的位置到终点的需要的最小生命值是 `x`,下边位置到终点需要的最小生命值是 `y`。 - -很明显我们应该选择所需生命值较小的方向。 - -如果 `x < y`,我们就向右走。 - -如果 `x > y`,我们就向下走。 - -知道方向以后,当前位置到终点的最小生命值 `need` 就等于 `x` 和 `y` 中较小的值减去当前位置上边的值。 - -如果算出来 `need` 大于 `0`,那就说明我们需要 `need` 的生命值到达终点。 - -如果算出来 `need` 小于等于 `0`,那就说明当前位置增加的生命值很大,所以当前位置我们只需要给一个最小值 `1`,就足以走到终点。 - -举个具体的例子就明白了。 - -如果右边的位置到终点的需要的最小生命值是 `5`,下边位置到终点需要的最小生命值是 `8`。 - -所以我们选择向右走。 - -如果当前位置的值是 `2`,然后 `need = 5 - 2 = 3`,所以当前位置的初始值应该是 `3`。 - -如果当前位置的值是 `-3`,然后 `need = 5 - (-3) = 8`,所以当前位置的初始值应该是 `8`。 - -如果当前位置的值是 `10`,说明增加的生命值很多,`need = 5 - 10 = -5`,此时我们只需要将当前位置的生命值初始为 `1` 即可。 - -然后每个位置都这样考虑,递归也就出来了。 - -递归出口也很好考虑, 那就是最后求终点到终点需要的最小生命值。 - -如果终点位置的值是正的,那么所需要的最小生命值就是 `1`。 - -如果终点位置的值是负的,那么所需要的最小生命值就是负值的绝对值加 `1`。 - -```java -public int calculateMinimumHP(int[][] dungeon) { - return calculateMinimumHPHelper(0, 0, dungeon); -} - -private int calculateMinimumHPHelper(int i, int j, int[][] dungeon) { - //是否到达终点 - if (i == dungeon.length - 1 && j == dungeon[0].length - 1) { - if (dungeon[i][j] > 0) { - return 1; - } else { - return -dungeon[i][j] + 1; - } - } - //右边位置到达终点所需要的最小值,如果已经在右边界,不能往右走了,赋值为最大值 - int right = j < dungeon[0].length - 1 ? calculateMinimumHPHelper(i, j + 1, dungeon) : Integer.MAX_VALUE; - //下边位置到达终点需要的最小值,如果已经在下边界,不能往下走了,赋值为最大值 - int down = i < dungeon.length - 1 ? calculateMinimumHPHelper(i + 1, j, dungeon) : Integer.MAX_VALUE; - //当前位置到终点还需要的生命值 - int need = right < down ? right - dungeon[i][j] : down - dungeon[i][j]; - if (need <= 0) { - return 1; - } else { - return need; - } -} -``` - -当然还是意料之中的超时了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/174_2.jpg) - -不过不要慌,还是之前的思想,我们利用 `map` 去缓冲中间过程的值,也就是 `memoization` 技术。 - -这个 `map` 的 `key` 和 `value` 就显而易见了,`key` 是坐标 `i,j`,`value` 的话就存当最后求出来的当前位置到终点所需的最小生命值,也就是 `return` 前同时存进 `map` 中。 - -```java -public int calculateMinimumHP(int[][] dungeon) { - return calculateMinimumHPHelper(0, 0, dungeon, new HashMap()); -} - -private int calculateMinimumHPHelper(int i, int j, int[][] dungeon, HashMap map) { - if (i == dungeon.length - 1 && j == dungeon[0].length - 1) { - if (dungeon[i][j] > 0) { - return 1; - } else { - return -dungeon[i][j] + 1; - } - } - String key = i + "@" + j; - if (map.containsKey(key)) { - return map.get(key); - } - int right = j < dungeon[0].length - 1 ? calculateMinimumHPHelper(i, j + 1, dungeon, map) : Integer.MAX_VALUE; - int down = i < dungeon.length - 1 ? calculateMinimumHPHelper(i + 1, j, dungeon, map) : Integer.MAX_VALUE; - int need = right < down ? right - dungeon[i][j] : down - dungeon[i][j]; - if (need <= 0) { - map.put(key, 1); - return 1; - } else { - map.put(key, need); - return need; - } -} -``` - -# 解法三 动态规划 - -其实解法二递归写完以后,很快就能想到动态规划怎么去解了。虽然它俩本质是一样的,但用动态规划可以节省递归压栈的时间,直接从底部往上走。 - -我们的状态就定义成解法二递归中返回的值,用 `dp[i][j]` 表示从 `(i, j)` 到达终点所需要的最小生命值。 - -状态转移方程的话和递归也一模一样,只需要把函数调用改成取直接取数组的值。 - -因为对于边界的情况,我们需要赋值为最大值,所以数组的话我们也扩充一行一列将其初始化为最大值,比如 - -```java -奖惩数组 -1 -3 3 -0 -2 0 --3 -3 -3 - -dp 数组 -终点位置就是递归出口时候返回的值,边界扩展一下 -用 M 表示 Integer.MAXVALUE -0 0 0 M -0 0 0 M -0 0 4 M -M M M M - -然后就可以一行一行或者一列一列的去更新 dp 数组,当然要倒着更新 -因为更新 dp[i][j] 的时候我们需要 dp[i+1][j] 和 dp[i][j+1] 的值 -``` -然后代码就出来了,可以和递归代码做个对比。 - -```java -public int calculateMinimumHP(int[][] dungeon) { - int row = dungeon.length; - int col = dungeon[0].length; - int[][] dp = new int[row + 1][col + 1]; - //终点所需要的值 - dp[row - 1][col - 1] = dungeon[row - 1][col - 1] > 0 ? 1 : -dungeon[row - 1][col - 1] + 1; - //扩充的边界更新为最大值 - for (int i = 0; i <= col; i++) { - dp[row][i] = Integer.MAX_VALUE; - } - for (int i = 0; i <= row; i++) { - dp[i][col] = Integer.MAX_VALUE; - } - - //逆过来更新 - for (int i = row - 1; i >= 0; i--) { - for (int j = col - 1; j >= 0; j--) { - if (i == row - 1 && j == col - 1) { - continue; - } - //选择向右走还是向下走 - dp[i][j] = Math.min(dp[i + 1][j], dp[i][j + 1]) - dungeon[i][j]; - if (dp[i][j] <= 0) { - dp[i][j] = 1; - } - } - } - return dp[0][0]; -} -``` - -如果动态规划做的多的话,必不可少的一步就是空间复杂度可以进行优化,比如 [5题](),[10题](),[53题](),[72题 ](),[115 题](https://leetcode.wang/leetcode-115-Distinct-Subsequences.html) 等等都已经用过了。 - -因为我们的 `dp` 数组在更新第 `i` 行的时候,我们只需要第 `i+1` 行的信息,而 `i+2`,`i+3` 行的信息我们就不再需要了,我们我们其实不需要二维数组,只需要一个一维数组就足够了。 - -```java -public int calculateMinimumHP(int[][] dungeon) { - int row = dungeon.length; - int col = dungeon[0].length; - int[] dp = new int[col + 1]; - - for (int i = 0; i <= col; i++) { - dp[i] = Integer.MAX_VALUE; - } - dp[col - 1] = dungeon[row - 1][col - 1] > 0 ? 1 : -dungeon[row - 1][col - 1] + 1; - for (int i = row - 1; i >= 0; i--) { - for (int j = col - 1; j >= 0; j--) { - if (i == row - 1 && j == col - 1) { - continue; - } - dp[j] = Math.min(dp[j], dp[j + 1]) - dungeon[i][j]; - if (dp[j] <= 0) { - dp[j] = 1; - } - } - } - return dp[0]; -} -``` - -# 总 - -回过来看这道题,其实有时候只是一个思维的逆转,就可以把问题解决了。 - -开始的时候,想求出从起点出发到任点的所需的最小生命值,然后发现走到了死胡同,因为根据当前的信息无法指导未来的方向。而思维逆转过来,从未来往回走,去求出任一点到终点所需要的最小生命值,问题瞬间得到了解决。 - -第一次遇到这样的动态规划题目,之前的动态规划无论从左上角到右下角,还是从右下角到左上角都是可以做的。而这个题由于有两个变量,所以只允许一个方向才能解题,很有意思。所以,最根本的原因就是终点到起点和起点到终点所需要的最小生命值并不一定是相同的。 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/174.png) + +题目描述,任务是从左上角(K)走到右下角(P),初始的时候有一个生命值 `HP`。只能向右和向下走,格子上边的数值代表增加 `HP` 和减少 `HP`,一旦变为 `0`,就立刻结束,问初始的 `HP` 最小可以取多少,才能从 `K` 走到 `P`。注意如果 `P` 点是负值,也要保证到达 `P` 点后将 `P` 点的值减去后, `HP` 的值依旧大于 `0`。 + +# 解法一 回溯法 + +最直接暴力的方法就是做搜索了,在每个位置无非就是向右向下两种可能,然后去尝试所有的解,然后找到最小的即可,也就是做一个 `DFS` 或者说是回溯法。 + +```java +//全局变量去保存最小值 +int minHealth = Integer.MAX_VALUE; + +public int calculateMinimumHP(int[][] dungeon) { + //calculateMinimumHPHelper 四个参数 + //int x, int y, int health, int addHealth, int[][] dungeon + //x, y 代表要准备到的位置,x 代表是哪一列,y 代表是哪一行 + //health 代表当前的生命值 + //addHealth 代表当前已经增加的生命值 + //初始的时候给加 1 点血,addHealth 和 health 都是 1 + calculateMinimumHPHelper(0, 0, 1, 1, dungeon); + return minHealth; +} + +private void calculateMinimumHPHelper(int x, int y, int health, int addHealth, int[][] dungeon) { + //加上当前位置的奖励或惩罚 + health = health + dungeon[y][x]; + //此时是否需要加血,加血的话就将 health 加到 1 + if (health <= 0) { + addHealth = addHealth + Math.abs(health) + 1; + } + + //是否到了终点 + if (x == dungeon[0].length - 1 && y == dungeon.length - 1) { + minHealth = Math.min(addHealth, minHealth); + return; + } + + //是否加过血 + if (health <= 0) { + //加过血的话,health 就变为 1 + if (x < dungeon[0].length - 1) { + calculateMinimumHPHelper(x + 1, y, 1, addHealth, dungeon); + } + if (y < dungeon.length - 1) { + calculateMinimumHPHelper(x, y + 1, 1, addHealth, dungeon); + } + } else { + //没加过血的话,health 就是当前的 health + if (x < dungeon[0].length - 1) { + calculateMinimumHPHelper(x + 1, y, health, addHealth, dungeon); + } + if (y < dungeon.length - 1) { + calculateMinimumHPHelper(x, y + 1, health, addHealth, dungeon); + } + } + +} +``` + +然后结果是意料之中的,会超时。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/174_2.jpg) + +然后我们就需要剪枝,将一些情况提前结束掉,最容易想到的就是,如果当前加的血已经超过了全局最小值,那就可以直接结束,不用进后边的递归。 + +```java +if (addHealth > minHealth) { + return; +} +``` + +然后发现对于给定的 `test case` 并没有什么影响。 + +之所以超时,就是因为我们会经过很多重复的位置,比如 + +```java +0 1 2 +3 4 5 +6 7 8 +如果按 DFS,第一条路径就是 0 -> 1 -> 2 -> 5 -> 8 +然后通过回溯,第二次判断的路径就会是 0 -> 1 -> 4 -> 5 -> ... +我们会发现它又会来到 5 这个位置 +其他的也类似,如果表格很大的话,不停的回溯,一些位置会经过很多次 +``` + +接下来,就会想到用 `map` 去缓冲我们过程中求出的解,`key` 话当然是 `x` 和 `y` 了,`value` 呢?存当前的 `health` 和 `addhealth`?那第二次来到这个位置的时候,我们并不能做什么,比如举个例子。 + +第一次来到 `(3,5)` 的时候,`health` 是 `5`,`addhealth` 是 `6`。 + +第二次来到 `(3,5)` 的时候,`health` 是 `4`,`addhealth` 是 `7`,我们什么也做不了,我们并不知道未来它会走什么路。 + +因为走的路是由 `health` 和 `addhealth` 共同决定的,此时来到相同的位置,由于 `health` 和 `addhealth` 都不一样,所以未来的路也很有可能变化,所以我们并不能通过缓冲结果来剪枝。 + +我们最多能判断当 `x`、`y`、`health` 和 `addhealth` 全部相同的时候提前结束,但这种情况也很少,所以并不能有效的加快搜索速度。 + +这条路看起来到死路了,我们换个思路,去用动态规划。 + +动态规划的关键就是去定义我们的状态了,这里直接将要解决的问题定义为我们的状态。 + +用 `dp[i][j]` 存储从起点 `(0, 0)` 到达 `(i, j)` 时候所需要的最小初始生命值。 + +到达 `(i,j)` 有两个点,`(i-1, j)` 和 `(i, j-1)`。 + +接下来就需要去推导状态转移方程了。 + +```java +* * 8 * +* 7 ! ? +? ? ? ? +``` + +假如我们要求上图中 `!` 位置的 `dp`,假设之前的 `dp` 已经都求出来了。 + +那么 `dp` 是等于感叹号上边的 `dp` 还是等于它左边的 `dp` 呢?选较小的吗? + +但如果 `8` 对应的当时的 `health` 是 `100`,而 `7` 对应的是 `5`,此时更好的选择应该是 `8`。 + +那就选 `health` 大的呗,那 `dp` 不管了吗?极端的例子,假如此时的位置已经是终点了,很明显我们应该选择从左边过来,也就是 `7` 的位置过来,之前的 `health` 并不重要了。 + +所以推到这里会发现,因为我们有两个不确定的变量,一个是 `dp` ,也就是从起点 `(0, 0)` 到达 `(i, j)` 时候所需要的最小初始生命值,还有一个就是当时剩下的生命值。 + +当更新 `dp` 的时候我们并不知道它应该是从上边下来,还是从左边过来有利于到达终点的时候所需的初始生命值最小。 + +换句话讲,依赖过去的状态,并不能指导我们当前的选择,因为还需要未来的信息。 + +所以到这里,我再次走到了死胡同,就去看 `Discuss` 了,这里分享下别人的做法。 + +# 解法二 递归 + +看到 [这里](https://leetcode.com/problems/dungeon-game/discuss/52790/My-AC-Java-Version-Suggestions-are-welcome) 评论区的一个解法。 + +所需要做的就是将上边动态规划的思路逆转一下。 + +``` + ↓ +→ * +``` + +之前我们考虑的是当前这个位置,它应该是从上边下来还是左边过来会更好些,然后发现并不能确定。 + +现在的话,看下边的图。 + +```java +* → x +↓ +y +``` + +我们现在考虑从当前位置,应该是向右走还是向下走,这样我们是可以确定的。 + +如果我们知道右边的位置到终点的需要的最小生命值是 `x`,下边位置到终点需要的最小生命值是 `y`。 + +很明显我们应该选择所需生命值较小的方向。 + +如果 `x < y`,我们就向右走。 + +如果 `x > y`,我们就向下走。 + +知道方向以后,当前位置到终点的最小生命值 `need` 就等于 `x` 和 `y` 中较小的值减去当前位置上边的值。 + +如果算出来 `need` 大于 `0`,那就说明我们需要 `need` 的生命值到达终点。 + +如果算出来 `need` 小于等于 `0`,那就说明当前位置增加的生命值很大,所以当前位置我们只需要给一个最小值 `1`,就足以走到终点。 + +举个具体的例子就明白了。 + +如果右边的位置到终点的需要的最小生命值是 `5`,下边位置到终点需要的最小生命值是 `8`。 + +所以我们选择向右走。 + +如果当前位置的值是 `2`,然后 `need = 5 - 2 = 3`,所以当前位置的初始值应该是 `3`。 + +如果当前位置的值是 `-3`,然后 `need = 5 - (-3) = 8`,所以当前位置的初始值应该是 `8`。 + +如果当前位置的值是 `10`,说明增加的生命值很多,`need = 5 - 10 = -5`,此时我们只需要将当前位置的生命值初始为 `1` 即可。 + +然后每个位置都这样考虑,递归也就出来了。 + +递归出口也很好考虑, 那就是最后求终点到终点需要的最小生命值。 + +如果终点位置的值是正的,那么所需要的最小生命值就是 `1`。 + +如果终点位置的值是负的,那么所需要的最小生命值就是负值的绝对值加 `1`。 + +```java +public int calculateMinimumHP(int[][] dungeon) { + return calculateMinimumHPHelper(0, 0, dungeon); +} + +private int calculateMinimumHPHelper(int i, int j, int[][] dungeon) { + //是否到达终点 + if (i == dungeon.length - 1 && j == dungeon[0].length - 1) { + if (dungeon[i][j] > 0) { + return 1; + } else { + return -dungeon[i][j] + 1; + } + } + //右边位置到达终点所需要的最小值,如果已经在右边界,不能往右走了,赋值为最大值 + int right = j < dungeon[0].length - 1 ? calculateMinimumHPHelper(i, j + 1, dungeon) : Integer.MAX_VALUE; + //下边位置到达终点需要的最小值,如果已经在下边界,不能往下走了,赋值为最大值 + int down = i < dungeon.length - 1 ? calculateMinimumHPHelper(i + 1, j, dungeon) : Integer.MAX_VALUE; + //当前位置到终点还需要的生命值 + int need = right < down ? right - dungeon[i][j] : down - dungeon[i][j]; + if (need <= 0) { + return 1; + } else { + return need; + } +} +``` + +当然还是意料之中的超时了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/174_2.jpg) + +不过不要慌,还是之前的思想,我们利用 `map` 去缓冲中间过程的值,也就是 `memoization` 技术。 + +这个 `map` 的 `key` 和 `value` 就显而易见了,`key` 是坐标 `i,j`,`value` 的话就存当最后求出来的当前位置到终点所需的最小生命值,也就是 `return` 前同时存进 `map` 中。 + +```java +public int calculateMinimumHP(int[][] dungeon) { + return calculateMinimumHPHelper(0, 0, dungeon, new HashMap()); +} + +private int calculateMinimumHPHelper(int i, int j, int[][] dungeon, HashMap map) { + if (i == dungeon.length - 1 && j == dungeon[0].length - 1) { + if (dungeon[i][j] > 0) { + return 1; + } else { + return -dungeon[i][j] + 1; + } + } + String key = i + "@" + j; + if (map.containsKey(key)) { + return map.get(key); + } + int right = j < dungeon[0].length - 1 ? calculateMinimumHPHelper(i, j + 1, dungeon, map) : Integer.MAX_VALUE; + int down = i < dungeon.length - 1 ? calculateMinimumHPHelper(i + 1, j, dungeon, map) : Integer.MAX_VALUE; + int need = right < down ? right - dungeon[i][j] : down - dungeon[i][j]; + if (need <= 0) { + map.put(key, 1); + return 1; + } else { + map.put(key, need); + return need; + } +} +``` + +# 解法三 动态规划 + +其实解法二递归写完以后,很快就能想到动态规划怎么去解了。虽然它俩本质是一样的,但用动态规划可以节省递归压栈的时间,直接从底部往上走。 + +我们的状态就定义成解法二递归中返回的值,用 `dp[i][j]` 表示从 `(i, j)` 到达终点所需要的最小生命值。 + +状态转移方程的话和递归也一模一样,只需要把函数调用改成取直接取数组的值。 + +因为对于边界的情况,我们需要赋值为最大值,所以数组的话我们也扩充一行一列将其初始化为最大值,比如 + +```java +奖惩数组 +1 -3 3 +0 -2 0 +-3 -3 -3 + +dp 数组 +终点位置就是递归出口时候返回的值,边界扩展一下 +用 M 表示 Integer.MAXVALUE +0 0 0 M +0 0 0 M +0 0 4 M +M M M M + +然后就可以一行一行或者一列一列的去更新 dp 数组,当然要倒着更新 +因为更新 dp[i][j] 的时候我们需要 dp[i+1][j] 和 dp[i][j+1] 的值 +``` +然后代码就出来了,可以和递归代码做个对比。 + +```java +public int calculateMinimumHP(int[][] dungeon) { + int row = dungeon.length; + int col = dungeon[0].length; + int[][] dp = new int[row + 1][col + 1]; + //终点所需要的值 + dp[row - 1][col - 1] = dungeon[row - 1][col - 1] > 0 ? 1 : -dungeon[row - 1][col - 1] + 1; + //扩充的边界更新为最大值 + for (int i = 0; i <= col; i++) { + dp[row][i] = Integer.MAX_VALUE; + } + for (int i = 0; i <= row; i++) { + dp[i][col] = Integer.MAX_VALUE; + } + + //逆过来更新 + for (int i = row - 1; i >= 0; i--) { + for (int j = col - 1; j >= 0; j--) { + if (i == row - 1 && j == col - 1) { + continue; + } + //选择向右走还是向下走 + dp[i][j] = Math.min(dp[i + 1][j], dp[i][j + 1]) - dungeon[i][j]; + if (dp[i][j] <= 0) { + dp[i][j] = 1; + } + } + } + return dp[0][0]; +} +``` + +如果动态规划做的多的话,必不可少的一步就是空间复杂度可以进行优化,比如 [5题](),[10题](),[53题](),[72题 ](),[115 题](https://leetcode.wang/leetcode-115-Distinct-Subsequences.html) 等等都已经用过了。 + +因为我们的 `dp` 数组在更新第 `i` 行的时候,我们只需要第 `i+1` 行的信息,而 `i+2`,`i+3` 行的信息我们就不再需要了,我们我们其实不需要二维数组,只需要一个一维数组就足够了。 + +```java +public int calculateMinimumHP(int[][] dungeon) { + int row = dungeon.length; + int col = dungeon[0].length; + int[] dp = new int[col + 1]; + + for (int i = 0; i <= col; i++) { + dp[i] = Integer.MAX_VALUE; + } + dp[col - 1] = dungeon[row - 1][col - 1] > 0 ? 1 : -dungeon[row - 1][col - 1] + 1; + for (int i = row - 1; i >= 0; i--) { + for (int j = col - 1; j >= 0; j--) { + if (i == row - 1 && j == col - 1) { + continue; + } + dp[j] = Math.min(dp[j], dp[j + 1]) - dungeon[i][j]; + if (dp[j] <= 0) { + dp[j] = 1; + } + } + } + return dp[0]; +} +``` + +# 总 + +回过来看这道题,其实有时候只是一个思维的逆转,就可以把问题解决了。 + +开始的时候,想求出从起点出发到任点的所需的最小生命值,然后发现走到了死胡同,因为根据当前的信息无法指导未来的方向。而思维逆转过来,从未来往回走,去求出任一点到终点所需要的最小生命值,问题瞬间得到了解决。 + +第一次遇到这样的动态规划题目,之前的动态规划无论从左上角到右下角,还是从右下角到左上角都是可以做的。而这个题由于有两个变量,所以只允许一个方向才能解题,很有意思。所以,最根本的原因就是终点到起点和起点到终点所需要的最小生命值并不一定是相同的。 + 遇到问题到了死胡同,不如逆过来去解决问题,太妙了! \ No newline at end of file diff --git a/leetcode-179-Largest-Number.md b/leetcode-179-Largest-Number.md index 8d0182a62..66a659e77 100644 --- a/leetcode-179-Largest-Number.md +++ b/leetcode-179-Largest-Number.md @@ -1,304 +1,304 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/179.jpg) - -给一个数组,将这些数任意排列,组成一个值最大的数。 - -# 解法一 - -直觉上,我们应该先选最高位大的数。有 `9` 开头的数字先选 `9`,然后选 `8` 开头的数,再选 `7` 开头的... 以此类推。所以我们需要一个函数来判断最高位是多少。 - -```java -private int getHighestPosition(int num) { - while (num / 10 > 0) { - num /= 10; - } - return num; -} -``` - -举个例子,比如对于 `5,914,67`,先选 `914`,再选 `67`,再选 `5`,组成 `914675`,因为数字的高位越大越好,每次尽量选高位的大的数从而保证了最后构成的数一定是最大的。 - -接下来的一个问题,如果最高位的数字一样呢?又分成两种情况。 - -如果两个数字长度相等,比如 `34` 和 `30` , 那么很明显,选择较大的即可。 - -如果两个数字长度不相等,比如 `3` 和 `30`,此时先选择 `3` 还是先选择 `30` 呢? - -我们只需要把它两拼接在一起直接去比较,也就是比较 `330` 和 `303`,很明显是 `330` 大,所以我们先选择 `3`。 - -所以我们可以封装一个比较函数。 - -```java -private int compare(int n1, int n2) { - int len1 = (n1 + "").length(); - int len2 = (n2 + "").length(); - //长度相等的情况 - if (len1 == len2) { - if (n1 > n2) { - return 1; - } else if (n1 < n2) { - return -1; - } else { - return 0; - } - } - //长度不等的情况 - int combination1 = (int) (n1 * Math.pow(10, len2)) + n2; - int combination2 =(int) (n2 * Math.pow(10, len1)) + n1; - - if (combination1 > combination2) { - return 1; - } else if (combination1 < combination2) { - return -1; - } else { - return 0; - } - -} -``` - -通过上边的分析,我们可以利用 `HashMap` 去存每一组数字,`key` 就是 `9,8,7...0` 分别代表开头数字是多少。而 `value` 就去存链表,每个链表的数字根据上边分析的规则将它们「从大到小」排列即可。 - -所以我们还需要一个插入元素到链表的方法。对于链表,我们的头结点不去存储值,而 `head.next` 才是我们第一个存储的值。 - -```java -//将 node 通过插入排序的思想,找到第一个比他小的节点然后插入到它的前边 -private void insert(MyNode head, MyNode node) { - while (head != null && head.next != null) { - int cur = head.next.val; - int insert = node.val; - if (compare(cur, insert) == -1) { - node.next = head.next; - head.next = node; - return; - } - head = head.next; - } - head.next = node; -} -``` - -然后对于 `3,30,34,5,9`,我们就会有下边的结构。 - -```java -9: head -> 9 -8: -7: -6: -5: head -> 5 -4: -3: head -> 34 -> 3 -> 30 -2: -1: -0: -``` - -然后我们只需要依次遍历这些数字组成一个字符串即可,即`9534330`。 - -然后把上边所有的代码合起来即可。 - -```java -class MyNode { - int val; - MyNode next; - MyNode(int val) { - this.val = val; - } -} - -public String largestNumber(int[] nums) { - HashMap map = new HashMap<>(); - for (int i = 9; i >= 0; i--) { - map.put(i, new MyNode(-1)); - } - //依次插入每一个数 - for (int i = 0; i < nums.length; i++) { - int key = getHighestPosition(nums[i]); - //得到头指针 - MyNode head = map.get(key); - MyNode MyNode = new MyNode(nums[i]); - //插入到当前链表的相应位置 - insert(head, MyNode); - } - //遍历所有值 - StringBuilder sb = new StringBuilder(); - for (int i = 9; i >= 0; i--) { - MyNode head = map.get(i).next; - while (head != null) { - sb.append(head.val); - head = head.next; - } - } - String res = sb.toString(); - //考虑 "000" 只有 0 的特殊情况 - return res.charAt(0) == '0' ? "0" : res; -} - -private void insert(MyNode head, MyNode node) { - while (head != null && head.next != null) { - int cur = head.next.val; - int insert = node.val; - if (compare(cur, insert) == -1) { - node.next = head.next; - head.next = node; - return; - } - head = head.next; - } - head.next = node; -} - -private int compare(int n1, int n2) { - int len1 = (n1 + "").length(); - int len2 = (n2 + "").length(); - if (len1 == len2) { - if (n1 > n2) { - return 1; - } else if (n1 < n2) { - return -1; - } else { - return 0; - } - } - int combination1 = (int) (n1 * Math.pow(10, len2)) + n2; - int combination2 = (int) (n2 * Math.pow(10, len1)) + n1; - - if (combination1 > combination2) { - return 1; - } else if (combination1 < combination2) { - return -1; - } else { - return 0; - } - -} - -private int getHighestPosition(int num) { - while (num / 10 > 0) { - num /= 10; - } - return num; -} -``` - -# 解法二 - -仔细想一下上边的想法,我们通过每个数字的最高位人为的把所有数字分成 `10` 类,然后每一类做了一个插入排序。其实我们也可以不进行分类,直接对所有数字进行排序。 - -我们直接调用系统的排序方法,传一个我们自定义的比较器即可。看一下我们之前的比较函数是否可以用。 - -```java -private int compare(int n1, int n2) { - int len1 = (n1 + "").length(); - int len2 = (n2 + "").length(); - if (len1 == len2) { - if (n1 > n2) { - return 1; - } else if (n1 < n2) { - return -1; - } else { - return 0; - } - } - int combination1 = (int) (n1 * Math.pow(10, len2)) + n2; - int combination2 = (int) (n2 * Math.pow(10, len1)) + n1; - - if (combination1 > combination2) { - return 1; - } else if (combination1 < combination2) { - return -1; - } else { - return 0; - } - -} -``` - -之前我们只考虑了最高位相等的情况,如果最高位不同的话检查一下上边的代码是否还可以用。比如对于 `93,234`,然后根据上边代码我们会比较 `93234` 和 `23493`,然后我们会选择 `93`,发现代码不需要修改。 - -此外,因为我们要从大到小排列,所以前一个数字大于后一个数字的时候,我们应该返回 `-1`。 - -```java -public String largestNumber(int[] nums) { - //自带的比较器不能使用 int 类型,所以我们把它转为 Integer 类型 - Integer[] n = new Integer[nums.length]; - for (int i = 0; i < nums.length; i++) { - n[i] = nums[i]; - } - Arrays.sort(n, new Comparator() { - @Override - public int compare(Integer n1, Integer n2) { - int len1 = (n1 + "").length(); - int len2 = (n2 + "").length(); - if (len1 == len2) { - if (n1 > n2) { - return -1; - } else if (n1 < n2) { - return 1; - } else { - return 0; - } - } - int combination1 = (int) (n1 * Math.pow(10, len2)) + n2; - int combination2 = (int) (n2 * Math.pow(10, len1)) + n1; - - if (combination1 > combination2) { - return -1; - } else if (combination1 < combination2) { - return 1; - } else { - return 0; - } - } - }); - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < nums.length; i++) { - sb.append(n[i]); - } - String res = sb.toString(); - return res.charAt(0) == '0' ? "0" : res; -} - -``` - -分析一下时间复杂度,首先取决于我们使用的排序算法,如果是解法一的插入排序,那么就是 `O(n²)`,如果是快速排序,那么就是 `O(nlog(n)`,此外我们的比较函数因为要求出每个数字的长度,我们需要遍历一遍数字,记做 `O(k)`,所以总的时间复杂度对于快排的话就是 `O(nklon(n))`。 - -# 解法三 - -上边的解法严格来说其实还是有些问题的。在比较两个数字大小的时候,当长度不相等的时候,我们把两个数字合并起来。如果数字特别大,强行把它们合并起来是会溢出的。 - -所以我们可以把数字转为 `String` ,把字符串合并起来,然后对字符串进行比较。 - -此外,在比较函数中我们单独分别判断了数字长度相等和不相等的情况,其实长度相等的情况也是可以合并到长度不相等的情况中去的。 - -```java -public String largestNumber(int[] nums) { - Integer[] n = new Integer[nums.length]; - for (int i = 0; i < nums.length; i++) { - n[i] = nums[i]; - } - Arrays.sort(n, new Comparator() { - @Override - public int compare(Integer n1, Integer n2) { - String s1 = n1 + "" + n2; - String s2 = n2 + "" + n1; - //compareTo 方法 - //如果参数是一个按字典顺序排列等于该字符串的字符串,则返回值为0; - //如果参数是按字典顺序大于此字符串的字符串,则返回值小于0; - //如果参数是按字典顺序小于此字符串的字符串,则返回值大于0。 - return s2.compareTo(s1); - } - }); - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < nums.length; i++) { - sb.append(n[i]); - } - String res = sb.toString(); - return res.charAt(0) == '0' ? "0" : res; -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/179.jpg) + +给一个数组,将这些数任意排列,组成一个值最大的数。 + +# 解法一 + +直觉上,我们应该先选最高位大的数。有 `9` 开头的数字先选 `9`,然后选 `8` 开头的数,再选 `7` 开头的... 以此类推。所以我们需要一个函数来判断最高位是多少。 + +```java +private int getHighestPosition(int num) { + while (num / 10 > 0) { + num /= 10; + } + return num; +} +``` + +举个例子,比如对于 `5,914,67`,先选 `914`,再选 `67`,再选 `5`,组成 `914675`,因为数字的高位越大越好,每次尽量选高位的大的数从而保证了最后构成的数一定是最大的。 + +接下来的一个问题,如果最高位的数字一样呢?又分成两种情况。 + +如果两个数字长度相等,比如 `34` 和 `30` , 那么很明显,选择较大的即可。 + +如果两个数字长度不相等,比如 `3` 和 `30`,此时先选择 `3` 还是先选择 `30` 呢? + +我们只需要把它两拼接在一起直接去比较,也就是比较 `330` 和 `303`,很明显是 `330` 大,所以我们先选择 `3`。 + +所以我们可以封装一个比较函数。 + +```java +private int compare(int n1, int n2) { + int len1 = (n1 + "").length(); + int len2 = (n2 + "").length(); + //长度相等的情况 + if (len1 == len2) { + if (n1 > n2) { + return 1; + } else if (n1 < n2) { + return -1; + } else { + return 0; + } + } + //长度不等的情况 + int combination1 = (int) (n1 * Math.pow(10, len2)) + n2; + int combination2 =(int) (n2 * Math.pow(10, len1)) + n1; + + if (combination1 > combination2) { + return 1; + } else if (combination1 < combination2) { + return -1; + } else { + return 0; + } + +} +``` + +通过上边的分析,我们可以利用 `HashMap` 去存每一组数字,`key` 就是 `9,8,7...0` 分别代表开头数字是多少。而 `value` 就去存链表,每个链表的数字根据上边分析的规则将它们「从大到小」排列即可。 + +所以我们还需要一个插入元素到链表的方法。对于链表,我们的头结点不去存储值,而 `head.next` 才是我们第一个存储的值。 + +```java +//将 node 通过插入排序的思想,找到第一个比他小的节点然后插入到它的前边 +private void insert(MyNode head, MyNode node) { + while (head != null && head.next != null) { + int cur = head.next.val; + int insert = node.val; + if (compare(cur, insert) == -1) { + node.next = head.next; + head.next = node; + return; + } + head = head.next; + } + head.next = node; +} +``` + +然后对于 `3,30,34,5,9`,我们就会有下边的结构。 + +```java +9: head -> 9 +8: +7: +6: +5: head -> 5 +4: +3: head -> 34 -> 3 -> 30 +2: +1: +0: +``` + +然后我们只需要依次遍历这些数字组成一个字符串即可,即`9534330`。 + +然后把上边所有的代码合起来即可。 + +```java +class MyNode { + int val; + MyNode next; + MyNode(int val) { + this.val = val; + } +} + +public String largestNumber(int[] nums) { + HashMap map = new HashMap<>(); + for (int i = 9; i >= 0; i--) { + map.put(i, new MyNode(-1)); + } + //依次插入每一个数 + for (int i = 0; i < nums.length; i++) { + int key = getHighestPosition(nums[i]); + //得到头指针 + MyNode head = map.get(key); + MyNode MyNode = new MyNode(nums[i]); + //插入到当前链表的相应位置 + insert(head, MyNode); + } + //遍历所有值 + StringBuilder sb = new StringBuilder(); + for (int i = 9; i >= 0; i--) { + MyNode head = map.get(i).next; + while (head != null) { + sb.append(head.val); + head = head.next; + } + } + String res = sb.toString(); + //考虑 "000" 只有 0 的特殊情况 + return res.charAt(0) == '0' ? "0" : res; +} + +private void insert(MyNode head, MyNode node) { + while (head != null && head.next != null) { + int cur = head.next.val; + int insert = node.val; + if (compare(cur, insert) == -1) { + node.next = head.next; + head.next = node; + return; + } + head = head.next; + } + head.next = node; +} + +private int compare(int n1, int n2) { + int len1 = (n1 + "").length(); + int len2 = (n2 + "").length(); + if (len1 == len2) { + if (n1 > n2) { + return 1; + } else if (n1 < n2) { + return -1; + } else { + return 0; + } + } + int combination1 = (int) (n1 * Math.pow(10, len2)) + n2; + int combination2 = (int) (n2 * Math.pow(10, len1)) + n1; + + if (combination1 > combination2) { + return 1; + } else if (combination1 < combination2) { + return -1; + } else { + return 0; + } + +} + +private int getHighestPosition(int num) { + while (num / 10 > 0) { + num /= 10; + } + return num; +} +``` + +# 解法二 + +仔细想一下上边的想法,我们通过每个数字的最高位人为的把所有数字分成 `10` 类,然后每一类做了一个插入排序。其实我们也可以不进行分类,直接对所有数字进行排序。 + +我们直接调用系统的排序方法,传一个我们自定义的比较器即可。看一下我们之前的比较函数是否可以用。 + +```java +private int compare(int n1, int n2) { + int len1 = (n1 + "").length(); + int len2 = (n2 + "").length(); + if (len1 == len2) { + if (n1 > n2) { + return 1; + } else if (n1 < n2) { + return -1; + } else { + return 0; + } + } + int combination1 = (int) (n1 * Math.pow(10, len2)) + n2; + int combination2 = (int) (n2 * Math.pow(10, len1)) + n1; + + if (combination1 > combination2) { + return 1; + } else if (combination1 < combination2) { + return -1; + } else { + return 0; + } + +} +``` + +之前我们只考虑了最高位相等的情况,如果最高位不同的话检查一下上边的代码是否还可以用。比如对于 `93,234`,然后根据上边代码我们会比较 `93234` 和 `23493`,然后我们会选择 `93`,发现代码不需要修改。 + +此外,因为我们要从大到小排列,所以前一个数字大于后一个数字的时候,我们应该返回 `-1`。 + +```java +public String largestNumber(int[] nums) { + //自带的比较器不能使用 int 类型,所以我们把它转为 Integer 类型 + Integer[] n = new Integer[nums.length]; + for (int i = 0; i < nums.length; i++) { + n[i] = nums[i]; + } + Arrays.sort(n, new Comparator() { + @Override + public int compare(Integer n1, Integer n2) { + int len1 = (n1 + "").length(); + int len2 = (n2 + "").length(); + if (len1 == len2) { + if (n1 > n2) { + return -1; + } else if (n1 < n2) { + return 1; + } else { + return 0; + } + } + int combination1 = (int) (n1 * Math.pow(10, len2)) + n2; + int combination2 = (int) (n2 * Math.pow(10, len1)) + n1; + + if (combination1 > combination2) { + return -1; + } else if (combination1 < combination2) { + return 1; + } else { + return 0; + } + } + }); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < nums.length; i++) { + sb.append(n[i]); + } + String res = sb.toString(); + return res.charAt(0) == '0' ? "0" : res; +} + +``` + +分析一下时间复杂度,首先取决于我们使用的排序算法,如果是解法一的插入排序,那么就是 `O(n²)`,如果是快速排序,那么就是 `O(nlog(n)`,此外我们的比较函数因为要求出每个数字的长度,我们需要遍历一遍数字,记做 `O(k)`,所以总的时间复杂度对于快排的话就是 `O(nklon(n))`。 + +# 解法三 + +上边的解法严格来说其实还是有些问题的。在比较两个数字大小的时候,当长度不相等的时候,我们把两个数字合并起来。如果数字特别大,强行把它们合并起来是会溢出的。 + +所以我们可以把数字转为 `String` ,把字符串合并起来,然后对字符串进行比较。 + +此外,在比较函数中我们单独分别判断了数字长度相等和不相等的情况,其实长度相等的情况也是可以合并到长度不相等的情况中去的。 + +```java +public String largestNumber(int[] nums) { + Integer[] n = new Integer[nums.length]; + for (int i = 0; i < nums.length; i++) { + n[i] = nums[i]; + } + Arrays.sort(n, new Comparator() { + @Override + public int compare(Integer n1, Integer n2) { + String s1 = n1 + "" + n2; + String s2 = n2 + "" + n1; + //compareTo 方法 + //如果参数是一个按字典顺序排列等于该字符串的字符串,则返回值为0; + //如果参数是按字典顺序大于此字符串的字符串,则返回值小于0; + //如果参数是按字典顺序小于此字符串的字符串,则返回值大于0。 + return s2.compareTo(s1); + } + }); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < nums.length; i++) { + sb.append(n[i]); + } + String res = sb.toString(); + return res.charAt(0) == '0' ? "0" : res; +} +``` + +# 总 + 只要找到了选取数字的原则,这道题也就转换成一道排序题了。 \ No newline at end of file diff --git a/leetcode-187-Repeated-DNA-Sequences.md b/leetcode-187-Repeated-DNA-Sequences.md index bccd3869b..47db7faa6 100644 --- a/leetcode-187-Repeated-DNA-Sequences.md +++ b/leetcode-187-Repeated-DNA-Sequences.md @@ -1,210 +1,210 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/187.jpg) - -一个 `DNA` 序列,从任意位置开始的连续 `10` 个字母当做一组,将重复的组输出。 - -# 解法一 - -先来个暴力的方法,双层循环,选取一组然后和后边的所有组进行比较,如果发现重复的组就把它加入到结果中。为了防止加入重复的结果,我们用 `set` 进行存储。 - -```java -public List findRepeatedDnaSequences(String s) { - int len = s.length(); - Set res = new HashSet<>(); - for (int i = 0; i <= len - 10; i++) { - for (int j = i + 1; j <= len - 10; j++) { - if (s.substring(i, i + 10).equals(s.substring(j, j + 10))) { - res.add(s.substring(i, i + 10)); - break; - } - } - } - return new ArrayList<>(res); -} -``` - -意料之中,超时了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/187_2.jpg) - -由于每一组都遍历了两次,所以造成了时间的浪费。我们可以利用一个 `HashSet` ,每遍历一组就将其放入,在加入之前判断 `HashSet` 中是否存在,如果存在就说明和之前的发生重复,就把它加到结果中。从而我们可以减少一层循环。 - -```java -public List findRepeatedDnaSequences(String s) { - int len = s.length(); - Set res = new HashSet<>(); - Set set = new HashSet<>(); - for (int i = 0; i <= len - 10; i++) { - String key = s.substring(i, i + 10); - //之前是否存在 - if (set.contains(key)) { - res.add(key); - } else { - set.add(key); - } - - } - return new ArrayList<>(res); -} - -``` - -# 解法二 - -正常情况下到解法一就可以结束了,然后在 `Discuss` 中逛了一下,其实上边的算法还有优化的地方,下边分享一下,参考了 [这里](https://leetcode.com/problems/repeated-dna-sequences/discuss/53867/Clean-Java-solution-(hashmap-%2B-bits-manipulation))。 - -通过这句代码 `String key = s.substring(i, i + 10);`,我们每次截取字符串作为 `key` 然后存放到 `HashSet` 中。 - -```java -对于 Input: s = "AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT"。 -当 i 等于 0 的时候,key = AAAAACCCCC -当 i 等于 1 的时候,key = AAAACCCCCA -当 i 等于 2 的时候,key = AAACCCCCAA -``` - -我们会发现,递增过程中,每次的字符串相对于之前都是少一个字母,多一个字母,而剩下的 `9` 个字母是没有变化的。 - -但是我们的代码中,并没有考虑之前已经得到的字符串,每次都是一股脑的重新从 `i` 取到 `i+9`,`String key = s.substring(i, i + 10);`。那么怎么利用之前的信息呢? - -把字符串编码为数字序列,然后通过移位保留之前的信息,具体的看下边的介绍。 - -我们把字母映射到二进制位, `A -> 00, C -> 01, G -> 10, T -> 11`,我们可以用一个 `HashMap` 去存这些对应关系,但因为我们只需要从存 `4` 个值,我们可以直接用一个 `char` 数组完成字母到数字的映射。 - -```java -//因为有 26 个字母,然后我们减去'A'以后,不管字母是什么,下标最大也就是 25 -char map[] = new char[26]; -map['A' - 'A'] = 0; //二进制 00 -map['C' - 'A'] = 1; //二进制 01 -map['G' - 'A'] = 2; //二进制 10 -map['T' - 'A'] = 3; //二进制 11 -``` - -有了这个对应关系我们就可以把字符串映射为二进制序列。 - -```java -对于 Input: s = "AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT"。 -就可以看做是 0000000000010101010100000000000010101010100000000000101010111111 -当 i 等于 0 的时候,key = AAAAACCCCC -当 i 等于 1 的时候,key = AAAACCCCCA -当 i 等于 2 的时候,key = AAACCCCCAA - -就可以看做是 -当 i 等于 0 的时候,key = 00000000000101010101 -当 i 等于 1 的时候,key = 00000000010101010100 -当 i 等于 2 的时候,key = 00000001010101010000 -``` - -`i = 0` 时候的 `key` 只需要左移两位,把最高位两位去掉,低位腾出两位,然后加上新加入的字母 `A`,也就是 `00`,就到了 `i = 1` 时候的 `key`。 - -此外,如果我们的 `key` 用 `int` 存储,一般情况下是 `32` 位的,但我们是 `10` 个字母,每个字母对应两位,所以我们只需要 `20` 位,我们需要把 `key` 和 `11111111111111111111(0xfffff)` 进行按位与,只保留低 `20` 位,所以更新 `key` 的话需要三个步骤,左移两位 -> 加上当前的字母 -> 按位与操作。 - -```java -key <<= 2; -key |= map[array[i] - 'A']; -key &= 0xfffff; -``` - -代码的话,除了求 `key` 的地方不同,整个框架和解法一也是一样的。 - -```java -public List findRepeatedDnaSequences(String s) { - int len = s.length(); - if (len == 0 || len < 10) { - return new ArrayList<>(); - } - Set res = new HashSet<>(); - Set set = new HashSet<>(); - char map[] = new char[26]; - map['A' - 'A'] = 0; - map['C' - 'A'] = 1; - map['G' - 'A'] = 2; - map['T' - 'A'] = 3; - int key = 0; - char[] array = s.toCharArray(); - //第一组单独初始化出来 - for (int i = 0; i < 10; i++) { - key = key << 2 | map[array[i] - 'A']; - } - set.add(key); - for (int i = 10; i < len; i++) { - key <<= 2; - key |= map[array[i] - 'A']; - key &= 0xfffff; - if (set.contains(key)) { - res.add(s.substring(i - 9, i + 1)); - } else { - set.add(key); - } - - } - return new ArrayList<>(res); - } -``` - -至于求 `key` 的话,我们单独用了一个 `map` 进行了映射,那么能不能不用 `map` 呢?可以的,参考 [这里](https://leetcode.com/problems/repeated-dna-sequences/discuss/53877/I-did-it-in-10-lines-of-C%2B%2B)。 - -我们知道每个字母本质上就是一个数字,至于对应关系就是 ASCII 码值。 - -```java -A -> 65 1000001 -C -> 65 1000011 -G -> 65 1000111 -T -> 65 1010100 -``` - -所以每个字母天然的就映射到了一个序列,我们并不需要 `map` 人为的转换。此时一个字母映射到了 `7` 个二进制位,但观察上边 `4` 个数字我们其实只用低三位就可以区分这四个字母了。 - -```java -A -> 001 -C -> 011 -G -> 111 -T -> 100 -``` - -所以对应规则就出来了,相对于之前的改变的地方,此时我们每次需要移 `3`位,并且按位与的话,因为每个字母对应三位,`10` 个字母总共需要 `30` 位,所以我们需要把 `key` 和`111111111111111111111111111111(0x3fffffff)` 也就是 `30` 个 `1` 进行按位与。 - -至于把字母转为 `key` ,我们只需要把低三位和 `111` 也就是十进制的 `7` 按位与一下即可。 - -```java -key <<= 3; -key |= (array[i] & 7); -key &= 0x3fffffff; -``` - -然后其他的地方,和上边通过 `map` 得到 `key` 的解法也没什么区别了。 - -```java -public List findRepeatedDnaSequences(String s) { - int len = s.length(); - if (len == 0 || len < 10) { - return new ArrayList<>(); - } - Set res = new HashSet<>(); - Set set = new HashSet<>(); - int key = 0; - char[] array = s.toCharArray(); - for (int i = 0; i < 10; i++) { - key <<= 3; - key |= (array[i] & 7); - } - set.add(key); - for (int i = 10; i < len; i++) { - key <<= 3; - key |= (array[i] & 7); - key &= 0x3fffffff; - if (set.contains(key)) { - res.add(s.substring(i - 9, i + 1)); - } else { - set.add(key); - } - - } - return new ArrayList<>(res); -} -``` - -# 总 - -解法一的话是很常规的思路。解法二的话,通过对 `key` 的选取,依次从时间和空间上对解法一进行了轻微的优化,这里需要对二进制有较深的理解。 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/187.jpg) + +一个 `DNA` 序列,从任意位置开始的连续 `10` 个字母当做一组,将重复的组输出。 + +# 解法一 + +先来个暴力的方法,双层循环,选取一组然后和后边的所有组进行比较,如果发现重复的组就把它加入到结果中。为了防止加入重复的结果,我们用 `set` 进行存储。 + +```java +public List findRepeatedDnaSequences(String s) { + int len = s.length(); + Set res = new HashSet<>(); + for (int i = 0; i <= len - 10; i++) { + for (int j = i + 1; j <= len - 10; j++) { + if (s.substring(i, i + 10).equals(s.substring(j, j + 10))) { + res.add(s.substring(i, i + 10)); + break; + } + } + } + return new ArrayList<>(res); +} +``` + +意料之中,超时了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/187_2.jpg) + +由于每一组都遍历了两次,所以造成了时间的浪费。我们可以利用一个 `HashSet` ,每遍历一组就将其放入,在加入之前判断 `HashSet` 中是否存在,如果存在就说明和之前的发生重复,就把它加到结果中。从而我们可以减少一层循环。 + +```java +public List findRepeatedDnaSequences(String s) { + int len = s.length(); + Set res = new HashSet<>(); + Set set = new HashSet<>(); + for (int i = 0; i <= len - 10; i++) { + String key = s.substring(i, i + 10); + //之前是否存在 + if (set.contains(key)) { + res.add(key); + } else { + set.add(key); + } + + } + return new ArrayList<>(res); +} + +``` + +# 解法二 + +正常情况下到解法一就可以结束了,然后在 `Discuss` 中逛了一下,其实上边的算法还有优化的地方,下边分享一下,参考了 [这里](https://leetcode.com/problems/repeated-dna-sequences/discuss/53867/Clean-Java-solution-(hashmap-%2B-bits-manipulation))。 + +通过这句代码 `String key = s.substring(i, i + 10);`,我们每次截取字符串作为 `key` 然后存放到 `HashSet` 中。 + +```java +对于 Input: s = "AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT"。 +当 i 等于 0 的时候,key = AAAAACCCCC +当 i 等于 1 的时候,key = AAAACCCCCA +当 i 等于 2 的时候,key = AAACCCCCAA +``` + +我们会发现,递增过程中,每次的字符串相对于之前都是少一个字母,多一个字母,而剩下的 `9` 个字母是没有变化的。 + +但是我们的代码中,并没有考虑之前已经得到的字符串,每次都是一股脑的重新从 `i` 取到 `i+9`,`String key = s.substring(i, i + 10);`。那么怎么利用之前的信息呢? + +把字符串编码为数字序列,然后通过移位保留之前的信息,具体的看下边的介绍。 + +我们把字母映射到二进制位, `A -> 00, C -> 01, G -> 10, T -> 11`,我们可以用一个 `HashMap` 去存这些对应关系,但因为我们只需要从存 `4` 个值,我们可以直接用一个 `char` 数组完成字母到数字的映射。 + +```java +//因为有 26 个字母,然后我们减去'A'以后,不管字母是什么,下标最大也就是 25 +char map[] = new char[26]; +map['A' - 'A'] = 0; //二进制 00 +map['C' - 'A'] = 1; //二进制 01 +map['G' - 'A'] = 2; //二进制 10 +map['T' - 'A'] = 3; //二进制 11 +``` + +有了这个对应关系我们就可以把字符串映射为二进制序列。 + +```java +对于 Input: s = "AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT"。 +就可以看做是 0000000000010101010100000000000010101010100000000000101010111111 +当 i 等于 0 的时候,key = AAAAACCCCC +当 i 等于 1 的时候,key = AAAACCCCCA +当 i 等于 2 的时候,key = AAACCCCCAA + +就可以看做是 +当 i 等于 0 的时候,key = 00000000000101010101 +当 i 等于 1 的时候,key = 00000000010101010100 +当 i 等于 2 的时候,key = 00000001010101010000 +``` + +`i = 0` 时候的 `key` 只需要左移两位,把最高位两位去掉,低位腾出两位,然后加上新加入的字母 `A`,也就是 `00`,就到了 `i = 1` 时候的 `key`。 + +此外,如果我们的 `key` 用 `int` 存储,一般情况下是 `32` 位的,但我们是 `10` 个字母,每个字母对应两位,所以我们只需要 `20` 位,我们需要把 `key` 和 `11111111111111111111(0xfffff)` 进行按位与,只保留低 `20` 位,所以更新 `key` 的话需要三个步骤,左移两位 -> 加上当前的字母 -> 按位与操作。 + +```java +key <<= 2; +key |= map[array[i] - 'A']; +key &= 0xfffff; +``` + +代码的话,除了求 `key` 的地方不同,整个框架和解法一也是一样的。 + +```java +public List findRepeatedDnaSequences(String s) { + int len = s.length(); + if (len == 0 || len < 10) { + return new ArrayList<>(); + } + Set res = new HashSet<>(); + Set set = new HashSet<>(); + char map[] = new char[26]; + map['A' - 'A'] = 0; + map['C' - 'A'] = 1; + map['G' - 'A'] = 2; + map['T' - 'A'] = 3; + int key = 0; + char[] array = s.toCharArray(); + //第一组单独初始化出来 + for (int i = 0; i < 10; i++) { + key = key << 2 | map[array[i] - 'A']; + } + set.add(key); + for (int i = 10; i < len; i++) { + key <<= 2; + key |= map[array[i] - 'A']; + key &= 0xfffff; + if (set.contains(key)) { + res.add(s.substring(i - 9, i + 1)); + } else { + set.add(key); + } + + } + return new ArrayList<>(res); + } +``` + +至于求 `key` 的话,我们单独用了一个 `map` 进行了映射,那么能不能不用 `map` 呢?可以的,参考 [这里](https://leetcode.com/problems/repeated-dna-sequences/discuss/53877/I-did-it-in-10-lines-of-C%2B%2B)。 + +我们知道每个字母本质上就是一个数字,至于对应关系就是 ASCII 码值。 + +```java +A -> 65 1000001 +C -> 65 1000011 +G -> 65 1000111 +T -> 65 1010100 +``` + +所以每个字母天然的就映射到了一个序列,我们并不需要 `map` 人为的转换。此时一个字母映射到了 `7` 个二进制位,但观察上边 `4` 个数字我们其实只用低三位就可以区分这四个字母了。 + +```java +A -> 001 +C -> 011 +G -> 111 +T -> 100 +``` + +所以对应规则就出来了,相对于之前的改变的地方,此时我们每次需要移 `3`位,并且按位与的话,因为每个字母对应三位,`10` 个字母总共需要 `30` 位,所以我们需要把 `key` 和`111111111111111111111111111111(0x3fffffff)` 也就是 `30` 个 `1` 进行按位与。 + +至于把字母转为 `key` ,我们只需要把低三位和 `111` 也就是十进制的 `7` 按位与一下即可。 + +```java +key <<= 3; +key |= (array[i] & 7); +key &= 0x3fffffff; +``` + +然后其他的地方,和上边通过 `map` 得到 `key` 的解法也没什么区别了。 + +```java +public List findRepeatedDnaSequences(String s) { + int len = s.length(); + if (len == 0 || len < 10) { + return new ArrayList<>(); + } + Set res = new HashSet<>(); + Set set = new HashSet<>(); + int key = 0; + char[] array = s.toCharArray(); + for (int i = 0; i < 10; i++) { + key <<= 3; + key |= (array[i] & 7); + } + set.add(key); + for (int i = 10; i < len; i++) { + key <<= 3; + key |= (array[i] & 7); + key &= 0x3fffffff; + if (set.contains(key)) { + res.add(s.substring(i - 9, i + 1)); + } else { + set.add(key); + } + + } + return new ArrayList<>(res); +} +``` + +# 总 + +解法一的话是很常规的思路。解法二的话,通过对 `key` 的选取,依次从时间和空间上对解法一进行了轻微的优化,这里需要对二进制有较深的理解。 + diff --git a/leetcode-188-Best-Time-to-Buy-and-Sell-StockIV.md b/leetcode-188-Best-Time-to-Buy-and-Sell-StockIV.md index fa83a2423..062e4b653 100644 --- a/leetcode-188-Best-Time-to-Buy-and-Sell-StockIV.md +++ b/leetcode-188-Best-Time-to-Buy-and-Sell-StockIV.md @@ -1,386 +1,386 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/188.jpg) - -买卖股票续集,前边是 [121 题](), [122 题]() ,[123 题](https://leetcode.wang/leetcode-123-Best-Time-to-Buy-and-Sell-StockIII.html) ,这道题的意思是,给一个数组代表股票每天的价格。你最多可以买入卖出 `K` 次,但只有卖出了才可以再次买入,求出最大的收益是多少。 - -# 解法一 - -直接按照前边题推出来的动态规划的方法做了,大家可以先到 [121 题](), [122 题]() ,[123 题](https://leetcode.wang/leetcode-123-Best-Time-to-Buy-and-Sell-StockIII.html) 看一下。 - -[123 题](https://leetcode.wang/leetcode-123-Best-Time-to-Buy-and-Sell-StockIII.html) 要求最多买卖两次,最终优化的出的代码如下。 - -```java -public int maxProfit(int[] prices) { - if (prices.length == 0) { - return 0; - } - int K = 2; - int[] dp = new int[K + 1]; - int min[] = new int[K + 1]; - for (int i = 1; i <= K; i++) { - min[i] = prices[0]; - } - for (int i = 1; i < prices.length; i++) { - for (int k = 1; k <= K; k++) { - min[k] = Math.min(prices[i] - dp[k - 1], min[k]); - dp[k] = Math.max(dp[k], prices[i] - min[k]); - } - } - return dp[K]; -} -``` - -之前我们已经抽象出了一个变量 `K` 代表最多买卖 `K` 次,所以这里的话我们只需要把函数传过来的参数赋值给 `K` 即可。 - -```java -public int maxProfit(int k, int[] prices) { - if (prices.length == 0) { - return 0; - } - int K = k; - int[] dp = new int[K + 1]; - int min[] = new int[K + 1]; - for (int i = 1; i <= K; i++) { - min[i] = prices[0]; - } - for (int i = 1; i < prices.length; i++) { - for (int kk = 1; kk <= K; kk++) { - min[kk] = Math.min(prices[i] - dp[kk - 1], min[kk]); - dp[kk] = Math.max(dp[kk], prices[i] - min[kk]); - } - } - return dp[K]; -} -``` - -但事情果然没有这么简单,内存超限了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/188_2.jpg) - -分析一下原因,我们申请了两个 `K+1` 大的数组,当 `K` 太大的时候就超出了内存的限制。 - -第一反应是因为数组需要消耗连续的内存空间,然后我又把数组改成链表进行了尝试,虽然没报超内存的错误,但直接报了超时的错误,原因也很简单,就是因为我们很多的随机存取,链表的话不易操作。现在再想想,有点儿傻,因为这个内存超限应该是 `leetcode` 的限制,而不是物理内存上真的不够了,所以数组改链表不会解决这个问题的。 - -怎么减小 `K` 的大小呢?为什么 `K` 会那么大那么大。仔细想想,其实 `K` 太大是没有意义的,如果我们数组的大小是 `n`,然后一天买,一天卖,我们最多就是 `n/2` 次交易(买入然后卖出算一次交易)。所以当 `K` 大于 `n/2` 的时候是没有意义的,所以我们可以再给 `K` 赋值的时候和 `n/2` 比较,选择较小的值赋值给 `K`。 - -```java -public int maxProfit(int k, int[] prices) { - if (prices.length == 0) { - return 0; - } - int K = Math.min(k, prices.length / 2); - int[] dp = new int[K + 1]; - int min[] = new int[K + 1]; - for (int i = 1; i <= K; i++) { - min[i] = prices[0]; - } - for (int i = 1; i < prices.length; i++) { - for (int kk = 1; kk <= K; kk++) { - min[kk] = Math.min(prices[i] - dp[kk - 1], min[kk]); - dp[kk] = Math.max(dp[kk], prices[i] - min[kk]); - } - } - return dp[K]; -} -``` - -然后去逛 `Discuss` 的时候突然想到,如果我们最多交易 `K` 次,而 `K` 又达到了 `n/2`,也就是最多的交易次数,那不就代表着我们可以交易任意次吗,交易任意次这不就是 [122 题]() 讨论的吗。所以代码可以再优化一下。 - -```java -public int maxProfit(int k, int[] prices) { - if (prices.length == 0) { - return 0; - } - //K 看做任意次,转到 122 题 - if (k >= prices.length / 2) { - return maxProfit(prices); - } - int K = k; - int[] dp = new int[K + 1]; - int min[] = new int[K + 1]; - for (int i = 1; i <= K; i++) { - min[i] = prices[0]; - } - for (int i = 1; i < prices.length; i++) { - for (int kk = 1; kk <= K; kk++) { - min[kk] = Math.min(prices[i] - dp[kk - 1], min[kk]); - dp[kk] = Math.max(dp[kk], prices[i] - min[kk]); - } - } - return dp[K]; -} - -//122 题代码 -public int maxProfit(int[] prices) { - int profit = 0; - for (int i = 1; i < prices.length; i++) { - int sub = prices[i] - prices[i - 1]; - if (sub > 0) { - profit += sub; - } - } - return profit; -} -``` - -时间复杂度:`O(nk)`。 - -空间复杂度:`O(k)`。 - -# 解法二 - -原本以为这道题也就结束了,但 `Discuss` 区总是人外有人,天外有天,有人提出了崭新的解法,并且时间复杂度上进行了优化。参考 [这里](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iv/discuss/54145/O(n)-time-8ms-Accepted-Solution-with-Detailed-Explanation-(C%2B%2B)) ,分享一下。 - -为了得到最高的收益,我们肯定会选择在波谷买入,然后在波峰卖出。第 `v` 天买入,第 `p` 天卖出,我们记做 `(v,p)`。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/188_3.jpg) - -所以我们所有可能的交易就是选取波谷、波峰,如上图,也就是 `(0,1)`,`(2,4)`,`(5,6)`。然后我们把这些交易所得的收益 `prices[p] - prices[v]` 依次放入数组中。把收益降序排序,选取前 `k` 个加起来,就是题目让我们所求的了,即最多进行 `k` 次交易时的最大收入。 - -```java -//定义一个结构,存储一次交易的买入卖出时间 -class Transaction { - int valley; - int peek; - - Transaction(int v, int p) { - valley = v; - peek = p; - } -} - -public int maxProfit(int k, int[] prices) { - if(k == 0){ - return 0; - } - Stack stack = new Stack<>(); - List profit = new ArrayList<>(); - int v; - int p = -1; - int n = prices.length; - while (true) { - v = p + 1; - //寻找波谷 - while (v + 1 < n && prices[v] > prices[v + 1]) { - v++; - } - p = v; - //寻找波峰 - while (p + 1 < n && prices[p] <= prices[p + 1]) { - p++; - } - - //到达最后,结束寻找 - if (p == v) { - break; - } - - //将这次的波谷、波峰存入 - stack.push(new Transaction(v, p)); - } - - //遍历所有的买入、卖出,计算其收益 - while (!stack.isEmpty()) { - Transaction pop = stack.pop(); - profit.add(prices[pop.peek] - prices[pop.valley]); - } - int ret = 0; - //如果能够进行的交易数 K 大于我们存的交易数,就把所有收益累加 - if (k >= profit.size()) { - for (int i = 0; i < profit.size(); i++) { - ret += profit.get(i); - } - } else { - //将收益从大到小排序 - Collections.sort(profit, new Comparator() { - @Override - public int compare(Integer n1, Integer n2) { - return n2 - n1; - } - }); - - //选取前 k 个 - for (int i = 0; i < k; i++) { - ret += profit.get(i); - } - } - return ret; -} -``` - -当然事情并不会这么简单,上边的情况是最理想的,在每次的波谷买入、波峰卖出,但对于下边的情况就会有些特殊了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/188_4.jpg) - -如果按照上边的算法,我们会将 `(0,1)` 和 `(2,3)` 存入。如果进行两次交易,当然也是没有问题的,刚好就是这两次的收益相加。但如果进行一次交易呢? - -很明显我们应该在第 `0` 天买入,在第 `3` 天卖出,所以我们应该将 `(0,3)` 存入。也就是当有新的交易 `(2,3)` 来的时候我们要和栈顶的交易 `(0,1)` 的波峰波谷进行比较,如果波谷大于之前的波谷,并且波峰也大于之前的波峰,两次交易就需要合并为 `(0,3)`。 - -接下来的问题就是我们栈中只存了 `(0,3)` 这一次交易,那么算收益的时候,如果可以进行两次交易,那该怎么办呢?这也是这个解法最巧妙的地方了。 - -假如两次交易的时间分别是 `(v1,p1)` 和 `(v2,p2)` ,那么 - -如果最多进行一次交易,那么最大收益就是 `prices[p2] - prices[v1]` - -如果最多进行两次交易,那么最大收益就是 `prices[p1] - prices[v1] + prices[p2] - prices[v2]`,进行一下变换 `(prices[p2] - prices[v1]) + (prices[p1] - prices[v2])`,第一个括号括起来的就是进行一次交易的最大收益,所以相对于只进行一次交易,我们的收益增加了第二个括号括起来的 `prices[p1] - prices[v2]`,所以我们只需要在合并两次交易的时候,把 `prices[p1] - prices[v2]` 存到 `profit` 数组中即可。 - -举个具体的例子,假如股票价格数组是 `1,4,2,6`,然后我们有一个 `stack` 去存每次的交易,`profit` 去存每次交易的收入。 - -我们会把 `6 - 1 = 5` 和 `4 - 2 = 2`存入`profit` 中。 - -这样如果最多进行一次交易,从 `profit` 中选取最大的收益,我们刚好得到就是 `5`。 - -如果最多进行两次交易,从 `profit` 中选取前二名的收益,我们就得到 `5 + 2 = 7`,刚好等价于 `(4 - 1) + (6 - 2) = 7`。 - -```java -while (!stack.isEmpty() && prices[p] >= prices[stack.peek().peek && prices[v] >= prices[stack.peek().valley]) { - Transaction pop = stack.pop(); - //加入 prices[p1] - prices[v2] 的收益 - profit.add(prices[pop.peek] - prices[v]); - //买入点更新为前一次的买入点 - v = pop.valley; -} -``` - -至于为什么要用 `while` 循环,因为和之前的合并之后,完全可能继续合并,比如下边的例子。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/188_5.jpg) - - 一开始 `(2,3)` 不能和 `(0,1)` 合并,但当 `(4,5)` 来时候,先和 `(2,3)` 合并为 `(2,5)`,再和 `(0,1)`合并为 `(0,5)`。 - -还有一种情况,如果新加入的交易的买入点低于栈顶交易的买入点,我们要把栈顶元素出栈。比如下图的例子。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/188_6.jpg) - -首先是 `(0,1)` 入栈,然后是 `(2,3)` 入栈。接着 `(4,5)` 入栈,此时我们应该将 `(2,3)` 出栈,原因有两点。 - -第一,因为新来的交易买入点更低,未来如果有交易可以和 `(2,3)` 合并,那么一定可以和 `(4,5)` 合并。并且和 `(4,5)`合并后的收益会更大。 - -第二,因为栈顶的元素是已经不能合并的交易,而每次我们是和栈顶进行合并,所以新来的交易完全可能会和栈顶之前的元素进行合并交易,因此我们要把旧的栈顶元素出栈。就比如上图的中例子,把 `(2,3)` 出栈以后,我们可以把 `(4,5)`和 `(0,1)` 进行合并。 - -```java -//当前的买入点比栈顶的低 -while (!stack.isEmpty() && prices[v] <= prices[stack.peek().valley]) { - Transaction pop = stack.pop(); - profit.add(prices[pop.peek] - prices[pop.valley]); -} -``` - -至于为什么要用 `while` 循环,因为有可能需要连续出栈,比如下图的例子。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/188_7.jpg) - -`(6,7)`来的时候,要把 `(4,5)`、`(2,3)` 依次出栈。 - -综上所述,我们要把新的交易的买入点和栈顶的买入点比较,如果当前的买入点更低,要把栈顶的元素出栈。然后再判断,卖出点是否高于栈顶元素的卖出点,如果更高的话,要把当前交易和栈顶的交易合并。 - -代码的话,整体都不需要变化,只需要在新的交易入栈之前执行一些出栈和合并交易的操作。 - -```java -class Transaction { - int valley; - int peek; - - Transaction(int v, int p) { - valley = v; - peek = p; - } -} - -public int maxProfit(int k, int[] prices) { - if(k == 0){ - return 0; - } - Stack stack = new Stack<>(); - List profit = new ArrayList<>(); - int v; - int p = -1; - int n = prices.length; - while (true) { - v = p + 1; - while (v + 1 < n && prices[v] > prices[v + 1]) { - v++; - } - p = v; - while (p + 1 < n && prices[p] <= prices[p + 1]) { - p++; - } - - if (p == v) { - break; - } - - //新的交易的买入点更低,要把栈顶的元素出栈 - while (!stack.isEmpty() && prices[v] <= prices[stack.peek().valley]) { - Transaction pop = stack.pop(); - profit.add(prices[pop.peek] - prices[pop.valley]); - } - - //当前交易和栈顶交易是否能合并 - while (!stack.isEmpty() && prices[p] >= prices[stack.peek().peek]) { - Transaction pop = stack.pop(); - profit.add(prices[pop.peek] - prices[v]); - v = pop.valley; - } - - stack.push(new Transaction(v, p)); - } - - while (!stack.isEmpty()) { - Transaction pop = stack.pop(); - profit.add(prices[pop.peek] - prices[pop.valley]); - } - int ret = 0; - if (k >= profit.size()) { - for (int i = 0; i < profit.size(); i++) { - ret += profit.get(i); - } - } else { - Collections.sort(profit, new Comparator() { - @Override - public int compare(Integer n1, Integer n2) { - return n2 - n1; - } - }); - - for (int i = 0; i < k; i++) { - ret += profit.get(i); - } - } - return ret; -} -``` - -时间复杂度的话,计算交易的时候需要 `O(n)` ,然后找出前 `k` 笔最大的交易的话用到了排序,如果是快速排序,那么就是 `O(nlog(n))`,所以总的来说就是 `O(nlog(n))`。 - -时间复杂度上我们还是可以优化的,在求前 `k` 笔最大的交易,我们可以用大小为 `k` 的优先队列存储,优先队列在 [23 题](https://leetcode.wang/leetCode-23-Merge-k-Sorted-Lists.html) 的时候也有用过。 - -```java -//相当于最小堆,队列头始终队列中是最小的元素 -PriorityQueue queue = new PriorityQueue(); - -for (int i = 0; i < profit.size(); i++) { - if (i < k) { - queue.add(profit.get(i)); - } else { - int peek = queue.peek(); - //当前收益大于栈顶元素,将栈顶元素弹出,然后将当前元素加入队列 - if (profit.get(i) > peek) { - queue.poll(); - queue.add(profit.get(i)); - } - } - -} - -while (!queue.isEmpty()) { - ret += queue.poll(); -} -``` - -优先队列的出栈入栈时间复杂度都是 `O(log(k))`,我们遍历了收益数组,这样的话时间复杂度就是 `O(nlog(k))` 了。 - -# 总 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/188.jpg) + +买卖股票续集,前边是 [121 题](), [122 题]() ,[123 题](https://leetcode.wang/leetcode-123-Best-Time-to-Buy-and-Sell-StockIII.html) ,这道题的意思是,给一个数组代表股票每天的价格。你最多可以买入卖出 `K` 次,但只有卖出了才可以再次买入,求出最大的收益是多少。 + +# 解法一 + +直接按照前边题推出来的动态规划的方法做了,大家可以先到 [121 题](), [122 题]() ,[123 题](https://leetcode.wang/leetcode-123-Best-Time-to-Buy-and-Sell-StockIII.html) 看一下。 + +[123 题](https://leetcode.wang/leetcode-123-Best-Time-to-Buy-and-Sell-StockIII.html) 要求最多买卖两次,最终优化的出的代码如下。 + +```java +public int maxProfit(int[] prices) { + if (prices.length == 0) { + return 0; + } + int K = 2; + int[] dp = new int[K + 1]; + int min[] = new int[K + 1]; + for (int i = 1; i <= K; i++) { + min[i] = prices[0]; + } + for (int i = 1; i < prices.length; i++) { + for (int k = 1; k <= K; k++) { + min[k] = Math.min(prices[i] - dp[k - 1], min[k]); + dp[k] = Math.max(dp[k], prices[i] - min[k]); + } + } + return dp[K]; +} +``` + +之前我们已经抽象出了一个变量 `K` 代表最多买卖 `K` 次,所以这里的话我们只需要把函数传过来的参数赋值给 `K` 即可。 + +```java +public int maxProfit(int k, int[] prices) { + if (prices.length == 0) { + return 0; + } + int K = k; + int[] dp = new int[K + 1]; + int min[] = new int[K + 1]; + for (int i = 1; i <= K; i++) { + min[i] = prices[0]; + } + for (int i = 1; i < prices.length; i++) { + for (int kk = 1; kk <= K; kk++) { + min[kk] = Math.min(prices[i] - dp[kk - 1], min[kk]); + dp[kk] = Math.max(dp[kk], prices[i] - min[kk]); + } + } + return dp[K]; +} +``` + +但事情果然没有这么简单,内存超限了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/188_2.jpg) + +分析一下原因,我们申请了两个 `K+1` 大的数组,当 `K` 太大的时候就超出了内存的限制。 + +第一反应是因为数组需要消耗连续的内存空间,然后我又把数组改成链表进行了尝试,虽然没报超内存的错误,但直接报了超时的错误,原因也很简单,就是因为我们很多的随机存取,链表的话不易操作。现在再想想,有点儿傻,因为这个内存超限应该是 `leetcode` 的限制,而不是物理内存上真的不够了,所以数组改链表不会解决这个问题的。 + +怎么减小 `K` 的大小呢?为什么 `K` 会那么大那么大。仔细想想,其实 `K` 太大是没有意义的,如果我们数组的大小是 `n`,然后一天买,一天卖,我们最多就是 `n/2` 次交易(买入然后卖出算一次交易)。所以当 `K` 大于 `n/2` 的时候是没有意义的,所以我们可以再给 `K` 赋值的时候和 `n/2` 比较,选择较小的值赋值给 `K`。 + +```java +public int maxProfit(int k, int[] prices) { + if (prices.length == 0) { + return 0; + } + int K = Math.min(k, prices.length / 2); + int[] dp = new int[K + 1]; + int min[] = new int[K + 1]; + for (int i = 1; i <= K; i++) { + min[i] = prices[0]; + } + for (int i = 1; i < prices.length; i++) { + for (int kk = 1; kk <= K; kk++) { + min[kk] = Math.min(prices[i] - dp[kk - 1], min[kk]); + dp[kk] = Math.max(dp[kk], prices[i] - min[kk]); + } + } + return dp[K]; +} +``` + +然后去逛 `Discuss` 的时候突然想到,如果我们最多交易 `K` 次,而 `K` 又达到了 `n/2`,也就是最多的交易次数,那不就代表着我们可以交易任意次吗,交易任意次这不就是 [122 题]() 讨论的吗。所以代码可以再优化一下。 + +```java +public int maxProfit(int k, int[] prices) { + if (prices.length == 0) { + return 0; + } + //K 看做任意次,转到 122 题 + if (k >= prices.length / 2) { + return maxProfit(prices); + } + int K = k; + int[] dp = new int[K + 1]; + int min[] = new int[K + 1]; + for (int i = 1; i <= K; i++) { + min[i] = prices[0]; + } + for (int i = 1; i < prices.length; i++) { + for (int kk = 1; kk <= K; kk++) { + min[kk] = Math.min(prices[i] - dp[kk - 1], min[kk]); + dp[kk] = Math.max(dp[kk], prices[i] - min[kk]); + } + } + return dp[K]; +} + +//122 题代码 +public int maxProfit(int[] prices) { + int profit = 0; + for (int i = 1; i < prices.length; i++) { + int sub = prices[i] - prices[i - 1]; + if (sub > 0) { + profit += sub; + } + } + return profit; +} +``` + +时间复杂度:`O(nk)`。 + +空间复杂度:`O(k)`。 + +# 解法二 + +原本以为这道题也就结束了,但 `Discuss` 区总是人外有人,天外有天,有人提出了崭新的解法,并且时间复杂度上进行了优化。参考 [这里](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iv/discuss/54145/O(n)-time-8ms-Accepted-Solution-with-Detailed-Explanation-(C%2B%2B)) ,分享一下。 + +为了得到最高的收益,我们肯定会选择在波谷买入,然后在波峰卖出。第 `v` 天买入,第 `p` 天卖出,我们记做 `(v,p)`。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/188_3.jpg) + +所以我们所有可能的交易就是选取波谷、波峰,如上图,也就是 `(0,1)`,`(2,4)`,`(5,6)`。然后我们把这些交易所得的收益 `prices[p] - prices[v]` 依次放入数组中。把收益降序排序,选取前 `k` 个加起来,就是题目让我们所求的了,即最多进行 `k` 次交易时的最大收入。 + +```java +//定义一个结构,存储一次交易的买入卖出时间 +class Transaction { + int valley; + int peek; + + Transaction(int v, int p) { + valley = v; + peek = p; + } +} + +public int maxProfit(int k, int[] prices) { + if(k == 0){ + return 0; + } + Stack stack = new Stack<>(); + List profit = new ArrayList<>(); + int v; + int p = -1; + int n = prices.length; + while (true) { + v = p + 1; + //寻找波谷 + while (v + 1 < n && prices[v] > prices[v + 1]) { + v++; + } + p = v; + //寻找波峰 + while (p + 1 < n && prices[p] <= prices[p + 1]) { + p++; + } + + //到达最后,结束寻找 + if (p == v) { + break; + } + + //将这次的波谷、波峰存入 + stack.push(new Transaction(v, p)); + } + + //遍历所有的买入、卖出,计算其收益 + while (!stack.isEmpty()) { + Transaction pop = stack.pop(); + profit.add(prices[pop.peek] - prices[pop.valley]); + } + int ret = 0; + //如果能够进行的交易数 K 大于我们存的交易数,就把所有收益累加 + if (k >= profit.size()) { + for (int i = 0; i < profit.size(); i++) { + ret += profit.get(i); + } + } else { + //将收益从大到小排序 + Collections.sort(profit, new Comparator() { + @Override + public int compare(Integer n1, Integer n2) { + return n2 - n1; + } + }); + + //选取前 k 个 + for (int i = 0; i < k; i++) { + ret += profit.get(i); + } + } + return ret; +} +``` + +当然事情并不会这么简单,上边的情况是最理想的,在每次的波谷买入、波峰卖出,但对于下边的情况就会有些特殊了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/188_4.jpg) + +如果按照上边的算法,我们会将 `(0,1)` 和 `(2,3)` 存入。如果进行两次交易,当然也是没有问题的,刚好就是这两次的收益相加。但如果进行一次交易呢? + +很明显我们应该在第 `0` 天买入,在第 `3` 天卖出,所以我们应该将 `(0,3)` 存入。也就是当有新的交易 `(2,3)` 来的时候我们要和栈顶的交易 `(0,1)` 的波峰波谷进行比较,如果波谷大于之前的波谷,并且波峰也大于之前的波峰,两次交易就需要合并为 `(0,3)`。 + +接下来的问题就是我们栈中只存了 `(0,3)` 这一次交易,那么算收益的时候,如果可以进行两次交易,那该怎么办呢?这也是这个解法最巧妙的地方了。 + +假如两次交易的时间分别是 `(v1,p1)` 和 `(v2,p2)` ,那么 + +如果最多进行一次交易,那么最大收益就是 `prices[p2] - prices[v1]` + +如果最多进行两次交易,那么最大收益就是 `prices[p1] - prices[v1] + prices[p2] - prices[v2]`,进行一下变换 `(prices[p2] - prices[v1]) + (prices[p1] - prices[v2])`,第一个括号括起来的就是进行一次交易的最大收益,所以相对于只进行一次交易,我们的收益增加了第二个括号括起来的 `prices[p1] - prices[v2]`,所以我们只需要在合并两次交易的时候,把 `prices[p1] - prices[v2]` 存到 `profit` 数组中即可。 + +举个具体的例子,假如股票价格数组是 `1,4,2,6`,然后我们有一个 `stack` 去存每次的交易,`profit` 去存每次交易的收入。 + +我们会把 `6 - 1 = 5` 和 `4 - 2 = 2`存入`profit` 中。 + +这样如果最多进行一次交易,从 `profit` 中选取最大的收益,我们刚好得到就是 `5`。 + +如果最多进行两次交易,从 `profit` 中选取前二名的收益,我们就得到 `5 + 2 = 7`,刚好等价于 `(4 - 1) + (6 - 2) = 7`。 + +```java +while (!stack.isEmpty() && prices[p] >= prices[stack.peek().peek && prices[v] >= prices[stack.peek().valley]) { + Transaction pop = stack.pop(); + //加入 prices[p1] - prices[v2] 的收益 + profit.add(prices[pop.peek] - prices[v]); + //买入点更新为前一次的买入点 + v = pop.valley; +} +``` + +至于为什么要用 `while` 循环,因为和之前的合并之后,完全可能继续合并,比如下边的例子。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/188_5.jpg) + + 一开始 `(2,3)` 不能和 `(0,1)` 合并,但当 `(4,5)` 来时候,先和 `(2,3)` 合并为 `(2,5)`,再和 `(0,1)`合并为 `(0,5)`。 + +还有一种情况,如果新加入的交易的买入点低于栈顶交易的买入点,我们要把栈顶元素出栈。比如下图的例子。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/188_6.jpg) + +首先是 `(0,1)` 入栈,然后是 `(2,3)` 入栈。接着 `(4,5)` 入栈,此时我们应该将 `(2,3)` 出栈,原因有两点。 + +第一,因为新来的交易买入点更低,未来如果有交易可以和 `(2,3)` 合并,那么一定可以和 `(4,5)` 合并。并且和 `(4,5)`合并后的收益会更大。 + +第二,因为栈顶的元素是已经不能合并的交易,而每次我们是和栈顶进行合并,所以新来的交易完全可能会和栈顶之前的元素进行合并交易,因此我们要把旧的栈顶元素出栈。就比如上图的中例子,把 `(2,3)` 出栈以后,我们可以把 `(4,5)`和 `(0,1)` 进行合并。 + +```java +//当前的买入点比栈顶的低 +while (!stack.isEmpty() && prices[v] <= prices[stack.peek().valley]) { + Transaction pop = stack.pop(); + profit.add(prices[pop.peek] - prices[pop.valley]); +} +``` + +至于为什么要用 `while` 循环,因为有可能需要连续出栈,比如下图的例子。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/188_7.jpg) + +`(6,7)`来的时候,要把 `(4,5)`、`(2,3)` 依次出栈。 + +综上所述,我们要把新的交易的买入点和栈顶的买入点比较,如果当前的买入点更低,要把栈顶的元素出栈。然后再判断,卖出点是否高于栈顶元素的卖出点,如果更高的话,要把当前交易和栈顶的交易合并。 + +代码的话,整体都不需要变化,只需要在新的交易入栈之前执行一些出栈和合并交易的操作。 + +```java +class Transaction { + int valley; + int peek; + + Transaction(int v, int p) { + valley = v; + peek = p; + } +} + +public int maxProfit(int k, int[] prices) { + if(k == 0){ + return 0; + } + Stack stack = new Stack<>(); + List profit = new ArrayList<>(); + int v; + int p = -1; + int n = prices.length; + while (true) { + v = p + 1; + while (v + 1 < n && prices[v] > prices[v + 1]) { + v++; + } + p = v; + while (p + 1 < n && prices[p] <= prices[p + 1]) { + p++; + } + + if (p == v) { + break; + } + + //新的交易的买入点更低,要把栈顶的元素出栈 + while (!stack.isEmpty() && prices[v] <= prices[stack.peek().valley]) { + Transaction pop = stack.pop(); + profit.add(prices[pop.peek] - prices[pop.valley]); + } + + //当前交易和栈顶交易是否能合并 + while (!stack.isEmpty() && prices[p] >= prices[stack.peek().peek]) { + Transaction pop = stack.pop(); + profit.add(prices[pop.peek] - prices[v]); + v = pop.valley; + } + + stack.push(new Transaction(v, p)); + } + + while (!stack.isEmpty()) { + Transaction pop = stack.pop(); + profit.add(prices[pop.peek] - prices[pop.valley]); + } + int ret = 0; + if (k >= profit.size()) { + for (int i = 0; i < profit.size(); i++) { + ret += profit.get(i); + } + } else { + Collections.sort(profit, new Comparator() { + @Override + public int compare(Integer n1, Integer n2) { + return n2 - n1; + } + }); + + for (int i = 0; i < k; i++) { + ret += profit.get(i); + } + } + return ret; +} +``` + +时间复杂度的话,计算交易的时候需要 `O(n)` ,然后找出前 `k` 笔最大的交易的话用到了排序,如果是快速排序,那么就是 `O(nlog(n))`,所以总的来说就是 `O(nlog(n))`。 + +时间复杂度上我们还是可以优化的,在求前 `k` 笔最大的交易,我们可以用大小为 `k` 的优先队列存储,优先队列在 [23 题](https://leetcode.wang/leetCode-23-Merge-k-Sorted-Lists.html) 的时候也有用过。 + +```java +//相当于最小堆,队列头始终队列中是最小的元素 +PriorityQueue queue = new PriorityQueue(); + +for (int i = 0; i < profit.size(); i++) { + if (i < k) { + queue.add(profit.get(i)); + } else { + int peek = queue.peek(); + //当前收益大于栈顶元素,将栈顶元素弹出,然后将当前元素加入队列 + if (profit.get(i) > peek) { + queue.poll(); + queue.add(profit.get(i)); + } + } + +} + +while (!queue.isEmpty()) { + ret += queue.poll(); +} +``` + +优先队列的出栈入栈时间复杂度都是 `O(log(k))`,我们遍历了收益数组,这样的话时间复杂度就是 `O(nlog(k))` 了。 + +# 总 + 对于解法一,其实就是之前买卖股票的解法。但解法二是真的太强了,不容易想到,但想到的人真是太厉害了。 \ No newline at end of file diff --git a/leetcode-189-Rotate-Array.md b/leetcode-189-Rotate-Array.md index ed386c099..41e2b8249 100644 --- a/leetcode-189-Rotate-Array.md +++ b/leetcode-189-Rotate-Array.md @@ -1,160 +1,160 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/189.jpg) - -转动数组,将数组的最后一个元素移动到开头,重复操作 `k` 次。 - -# 解法一 - -完全按照题目的意思,每次把末尾的元素移动到开头,当然移动前需要把所有元素后移一位,把第一个位置腾出来。 - -此外,如果 `k` 大于数组的长度,`k` 是等效于 `k % n` 的。举个例子,`nums = [1 2 3]`,`k = 4`,操作 `4` 次和操作 `4 % 3 = 1` 次是一样的结果。 - -```java -public void rotate(int[] nums, int k) { - int n = nums.length; - k = k % n; - for (int i = 0; i < k; i++) { - int temp = nums[n - 1]; - for (int j = n - 1; j > 0; j--) { - nums[j] = nums[j - 1]; - } - nums[0] = temp; - } -} -``` - -时间复杂度:`O(kn)`。 - -空间复杂度:`O(1)`。 - -# 解法二 - -空间换时间,解法一中每个元素都需要移动 `k` 次,因为最后一个元素移到第一个位置的话,就进行了整体后移。不然的话,第一个位置原来的数就会被覆盖掉。 - -我们可以申请一个和原数组等大的数组,复制之前所有的值。这样的话,我们就可以随心所欲的在原数组上赋值了,不需要考虑值的丢失。 - -```java -public void rotate(int[] nums, int k) { - int n = nums.length; - k = k % n; - int[] numsCopy = new int[n]; - for (int i = 0; i < n; i++) { - numsCopy[i] = nums[i]; - } - - //末尾的 k 个数复制过来 - for (int i = 0; i < k; i++) { - nums[i] = numsCopy[n - k + i]; - } - - //剩下的数复制过来 - for (int i = k; i < n; i++) { - nums[i] = numsCopy[i - k]; - } -} -``` - -时间复杂度:`O(n)`。 - -空间复杂度:`O(n)`。 - -# 解法三 - -上边的解法都是直接可以想到的,写完之后看了 [官方](https://leetcode.com/problems/rotate-array/solution/) 提供的解法,下边分享一下。 - -换一种题目的理解方式。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/189_2.jpg) - -把数组看成一个圆环,而 `k` 的含义其实就是所有数字顺时针移动 `k` 个位置。 - -如果 `k = 2`,那么含义就是 `1` 放到 `3` 的位置,`3` 放到 `5` 的位置... - -当然程序上,如果 `1` 放到 `3` 的位置,`3` 就会被覆盖了,我们应该用一个变量 `pre` 存储当前位置被占用的数字。 - -思想就是上边的了,代码的话可能会有不同的写法,下边的供参考。 - -```java -public void rotate(int[] nums, int k) { - int n = nums.length; - k = k % n; - if (k == 0) { - return; - } - int count = 0; //记录搬移了多少个数字 - int start = 0; - int current = start; - int pre = nums[current]; - while (true) { - do { - //要移动过去的位置 - int next = (current + k) % n; - //数字做缓存 - int temp = nums[next]; - //将数字搬过来 - nums[next] = pre; - pre = temp; - //考虑下一个位置 - current = next; - count++; - //全部数字搬移完就结束 - if (count == n) { - return; - } - } while (start != current); - //这里是防止死循环,因为搬移的位置可能会回到最开始的位置, 所以我们 start++, 继续搬移其他组 - start++; - current = start; - pre = nums[current]; - } -} -``` - -时间复杂度:`O(n)`,每个数字仅搬移一次。 - -空间复杂度:`O(1)`。 - -# 解法四 - -依旧是参考 [官方](https://leetcode.com/problems/rotate-array/solution/) 题解。 - -看具体的例子,`1 2 3 4 5`,`k = 2`。 - -转换后最终变成 ` 4 5 1 2 3`。 - -其实可以分三步完成。 - -整体逆序 `5 4 3 2 1` 。 - -前 `k` 个再逆序 `4 5 3 2 1`。 - -后边的再逆序 `4 5 1 2 3`。 - -```java -public void rotate(int[] nums, int k) { - int n = nums.length; - k = k % n; - reverse(nums, 0, n - 1); - reverse(nums, 0, k - 1); - reverse(nums, k, n - 1); -} - -private void reverse(int[] nums, int start, int end) { - while (start < end) { - int temp = nums[start]; - nums[start] = nums[end]; - nums[end] = temp; - start++; - end--; - } -} -``` - -时间复杂度:`O(n)`。 - -空间复杂度:`O(1)`。 - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/189.jpg) + +转动数组,将数组的最后一个元素移动到开头,重复操作 `k` 次。 + +# 解法一 + +完全按照题目的意思,每次把末尾的元素移动到开头,当然移动前需要把所有元素后移一位,把第一个位置腾出来。 + +此外,如果 `k` 大于数组的长度,`k` 是等效于 `k % n` 的。举个例子,`nums = [1 2 3]`,`k = 4`,操作 `4` 次和操作 `4 % 3 = 1` 次是一样的结果。 + +```java +public void rotate(int[] nums, int k) { + int n = nums.length; + k = k % n; + for (int i = 0; i < k; i++) { + int temp = nums[n - 1]; + for (int j = n - 1; j > 0; j--) { + nums[j] = nums[j - 1]; + } + nums[0] = temp; + } +} +``` + +时间复杂度:`O(kn)`。 + +空间复杂度:`O(1)`。 + +# 解法二 + +空间换时间,解法一中每个元素都需要移动 `k` 次,因为最后一个元素移到第一个位置的话,就进行了整体后移。不然的话,第一个位置原来的数就会被覆盖掉。 + +我们可以申请一个和原数组等大的数组,复制之前所有的值。这样的话,我们就可以随心所欲的在原数组上赋值了,不需要考虑值的丢失。 + +```java +public void rotate(int[] nums, int k) { + int n = nums.length; + k = k % n; + int[] numsCopy = new int[n]; + for (int i = 0; i < n; i++) { + numsCopy[i] = nums[i]; + } + + //末尾的 k 个数复制过来 + for (int i = 0; i < k; i++) { + nums[i] = numsCopy[n - k + i]; + } + + //剩下的数复制过来 + for (int i = k; i < n; i++) { + nums[i] = numsCopy[i - k]; + } +} +``` + +时间复杂度:`O(n)`。 + +空间复杂度:`O(n)`。 + +# 解法三 + +上边的解法都是直接可以想到的,写完之后看了 [官方](https://leetcode.com/problems/rotate-array/solution/) 提供的解法,下边分享一下。 + +换一种题目的理解方式。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/189_2.jpg) + +把数组看成一个圆环,而 `k` 的含义其实就是所有数字顺时针移动 `k` 个位置。 + +如果 `k = 2`,那么含义就是 `1` 放到 `3` 的位置,`3` 放到 `5` 的位置... + +当然程序上,如果 `1` 放到 `3` 的位置,`3` 就会被覆盖了,我们应该用一个变量 `pre` 存储当前位置被占用的数字。 + +思想就是上边的了,代码的话可能会有不同的写法,下边的供参考。 + +```java +public void rotate(int[] nums, int k) { + int n = nums.length; + k = k % n; + if (k == 0) { + return; + } + int count = 0; //记录搬移了多少个数字 + int start = 0; + int current = start; + int pre = nums[current]; + while (true) { + do { + //要移动过去的位置 + int next = (current + k) % n; + //数字做缓存 + int temp = nums[next]; + //将数字搬过来 + nums[next] = pre; + pre = temp; + //考虑下一个位置 + current = next; + count++; + //全部数字搬移完就结束 + if (count == n) { + return; + } + } while (start != current); + //这里是防止死循环,因为搬移的位置可能会回到最开始的位置, 所以我们 start++, 继续搬移其他组 + start++; + current = start; + pre = nums[current]; + } +} +``` + +时间复杂度:`O(n)`,每个数字仅搬移一次。 + +空间复杂度:`O(1)`。 + +# 解法四 + +依旧是参考 [官方](https://leetcode.com/problems/rotate-array/solution/) 题解。 + +看具体的例子,`1 2 3 4 5`,`k = 2`。 + +转换后最终变成 ` 4 5 1 2 3`。 + +其实可以分三步完成。 + +整体逆序 `5 4 3 2 1` 。 + +前 `k` 个再逆序 `4 5 3 2 1`。 + +后边的再逆序 `4 5 1 2 3`。 + +```java +public void rotate(int[] nums, int k) { + int n = nums.length; + k = k % n; + reverse(nums, 0, n - 1); + reverse(nums, 0, k - 1); + reverse(nums, k, n - 1); +} + +private void reverse(int[] nums, int start, int end) { + while (start < end) { + int temp = nums[start]; + nums[start] = nums[end]; + nums[end] = temp; + start++; + end--; + } +} +``` + +时间复杂度:`O(n)`。 + +空间复杂度:`O(1)`。 + +# 总 + 解法一、解法二就是对题目最简单的理解,解法三和解法四是进一步对题目的剖析,很厉害。 \ No newline at end of file diff --git a/leetcode-190-Reverse-Bits.md b/leetcode-190-Reverse-Bits.md index 7df392c5f..10a76f0b8 100644 --- a/leetcode-190-Reverse-Bits.md +++ b/leetcode-190-Reverse-Bits.md @@ -1,84 +1,84 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/190.jpg) - -将一个 `int` 类型的数字,`32` 个 `bit`,进行倒置。 - -# 解法一 - -用一个变量 `res` 去存储结果,依次得到要转换数字的低位,然后依次保存到 `res` 中。`res` 每得到一位后进行左移腾出位置保存下一位。举个具体的例子。 - -```java -原数字 1011 ,res = 0 - -res 左移一位,res = 0, -得到 1011 的最低位 1 加过来, res = 1 -1011 右移一位变为 101 - -res = 1 左移一位,res = 10, -得到 101 的最低位 1 加过来, res = 11 -101 右移一位变为 10 - -res = 11 左移一位,res = 110, -得到 10 的最低位 0 加过来, res = 110 -10 右移一位变为 1 - -res = 110 左移一位,res = 1100, -得到 1 的最低位 1 加过来, res = 1101 -1 右移一位变为 0, 结束 -``` - -至于怎么得到最低位,和把最低位加过来,我们可以通过位操作完成。 - -```java -public int reverseBits(int n) { - int res = 0; - int count = 0; - while (count < 32) { - res <<= 1; //res 左移一位空出位置 - res |= (n & 1); //得到的最低位加过来 - n >>= 1;//原数字右移一位去掉已经处理过的最低位 - count++; - } - return res; -} -``` - -# 解法二 - -另一种想法,参考 [这里](https://leetcode.com/problems/reverse-bits/discuss/54741/O(1)-bit-operation-C%2B%2B-solution-(8ms))。 - -如果是两位数字怎么逆序呢?比如 `2 4`,我们只需要**交换**两个数字的位置,变成 `4 2`。 - -如果是四位数字怎么逆序呢?比如 `1 2 3 4`,同样的我们只需要交换两部分 `1 2` 和`3 4` 的数字,变成 `3 4 1 2`,接下来只需要分别将两部分 `3 4` 和 `1 2 ` 分别逆序,两位数的逆序已经讨论过。 - -如果是八位数字怎么逆序呢?比如 `1 2 3 4 5 6 7 8`,同样的我们只需要交换两部分`1 2 3 4` 和 `5 6 7 8` 的数字,变成 `5 6 7 8 1 2 3 4`,接下来只需要分别将两部分 `5 6 7 8` 和 `1 2 3 4` 分别逆序,四位数的逆序已经讨论过。 - -这道题也可以用这个思想去解决,`32` 位的数字左半部分何右半部分交换,得到两个 `16` 位的数字,然后两部分再交换,得到两个 `8` 位的数字... - -在二进制中交换两部分,可以用一个技巧,举个例子,对于 `x = 1101` 交换两部分,我们只需要 - -`(1100) & x >>> 2 | (0011) & x <<< 2 = (0011)|(0100)= 0111 `,然后就完成了 `11` 和 `01` 的交换。 - -```java -public int reverseBits(int n) { - n = ((n & 0xffff0000) >>> 16) | ((n & 0x0000ffff) << 16); - n = ((n & 0xff00ff00) >>> 8) | ((n & 0x00ff00ff) << 8); - n = ((n & 0xf0f0f0f0) >>> 4) | ((n & 0x0f0f0f0f) << 4); - n = ((n & 0xcccccccc) >>> 2) | ((n & 0x33333333) << 2); - n = ((n & 0xaaaaaaaa) >>> 1) | ((n & 0x55555555) << 1); - return n; -} -``` - -上边的写成 `16` 进制可能一下子不能理解,写成 `2` 进制就明白了。 - -比如 ` n = ((n & 0xcccccccc) >>> 2) | ((n & 0x33333333) << 2);` 也就是之前讨论的两位和两位交换。 - -我们需要分别和 `1100` 和 `0011` 进行与操作,写成 `16` 进制就是 `c` 和 `3`,因为我们要同时对 `32` 位的数字操作,每四个算一组,也就是八组,也就是 `8`和 `c` 和 `8` 个 `3` 了。 - -另外需要注意的是一定要是逻辑右移,也就是三个大于号 `>>>`,不能去考虑符号位,具体就是涉及到 [补码](https://zhuanlan.zhihu.com/p/67227136) 的知识了。 - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/190.jpg) + +将一个 `int` 类型的数字,`32` 个 `bit`,进行倒置。 + +# 解法一 + +用一个变量 `res` 去存储结果,依次得到要转换数字的低位,然后依次保存到 `res` 中。`res` 每得到一位后进行左移腾出位置保存下一位。举个具体的例子。 + +```java +原数字 1011 ,res = 0 + +res 左移一位,res = 0, +得到 1011 的最低位 1 加过来, res = 1 +1011 右移一位变为 101 + +res = 1 左移一位,res = 10, +得到 101 的最低位 1 加过来, res = 11 +101 右移一位变为 10 + +res = 11 左移一位,res = 110, +得到 10 的最低位 0 加过来, res = 110 +10 右移一位变为 1 + +res = 110 左移一位,res = 1100, +得到 1 的最低位 1 加过来, res = 1101 +1 右移一位变为 0, 结束 +``` + +至于怎么得到最低位,和把最低位加过来,我们可以通过位操作完成。 + +```java +public int reverseBits(int n) { + int res = 0; + int count = 0; + while (count < 32) { + res <<= 1; //res 左移一位空出位置 + res |= (n & 1); //得到的最低位加过来 + n >>= 1;//原数字右移一位去掉已经处理过的最低位 + count++; + } + return res; +} +``` + +# 解法二 + +另一种想法,参考 [这里](https://leetcode.com/problems/reverse-bits/discuss/54741/O(1)-bit-operation-C%2B%2B-solution-(8ms))。 + +如果是两位数字怎么逆序呢?比如 `2 4`,我们只需要**交换**两个数字的位置,变成 `4 2`。 + +如果是四位数字怎么逆序呢?比如 `1 2 3 4`,同样的我们只需要交换两部分 `1 2` 和`3 4` 的数字,变成 `3 4 1 2`,接下来只需要分别将两部分 `3 4` 和 `1 2 ` 分别逆序,两位数的逆序已经讨论过。 + +如果是八位数字怎么逆序呢?比如 `1 2 3 4 5 6 7 8`,同样的我们只需要交换两部分`1 2 3 4` 和 `5 6 7 8` 的数字,变成 `5 6 7 8 1 2 3 4`,接下来只需要分别将两部分 `5 6 7 8` 和 `1 2 3 4` 分别逆序,四位数的逆序已经讨论过。 + +这道题也可以用这个思想去解决,`32` 位的数字左半部分何右半部分交换,得到两个 `16` 位的数字,然后两部分再交换,得到两个 `8` 位的数字... + +在二进制中交换两部分,可以用一个技巧,举个例子,对于 `x = 1101` 交换两部分,我们只需要 + +`(1100) & x >>> 2 | (0011) & x <<< 2 = (0011)|(0100)= 0111 `,然后就完成了 `11` 和 `01` 的交换。 + +```java +public int reverseBits(int n) { + n = ((n & 0xffff0000) >>> 16) | ((n & 0x0000ffff) << 16); + n = ((n & 0xff00ff00) >>> 8) | ((n & 0x00ff00ff) << 8); + n = ((n & 0xf0f0f0f0) >>> 4) | ((n & 0x0f0f0f0f) << 4); + n = ((n & 0xcccccccc) >>> 2) | ((n & 0x33333333) << 2); + n = ((n & 0xaaaaaaaa) >>> 1) | ((n & 0x55555555) << 1); + return n; +} +``` + +上边的写成 `16` 进制可能一下子不能理解,写成 `2` 进制就明白了。 + +比如 ` n = ((n & 0xcccccccc) >>> 2) | ((n & 0x33333333) << 2);` 也就是之前讨论的两位和两位交换。 + +我们需要分别和 `1100` 和 `0011` 进行与操作,写成 `16` 进制就是 `c` 和 `3`,因为我们要同时对 `32` 位的数字操作,每四个算一组,也就是八组,也就是 `8`和 `c` 和 `8` 个 `3` 了。 + +另外需要注意的是一定要是逻辑右移,也就是三个大于号 `>>>`,不能去考虑符号位,具体就是涉及到 [补码](https://zhuanlan.zhihu.com/p/67227136) 的知识了。 + +# 总 + 重点是对于二进制的理解,计算机中的数字都是以二进制的形式存储,按位与,按位或,右移左移一些位操作需要熟悉。 \ No newline at end of file diff --git a/leetcode-191-Number-of-1-Bits.md b/leetcode-191-Number-of-1-Bits.md index 198fc398d..92f34526d 100644 --- a/leetcode-191-Number-of-1-Bits.md +++ b/leetcode-191-Number-of-1-Bits.md @@ -1,110 +1,110 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/191.jpg) - -给一个数字,求出其二进制形式中 `1` 的个数。 - -# 解法一 - -简单粗暴些,依次判断最低位是否是 `1`,然后把它加入到结果中。判断最低位是否是 `1`,我们只需要把原数字和 `000000..001` 相与,也就是和 `1` 相与即可。 - -```java -public int hammingWeight(int n) { - int count = 0; - while (n != 0) { - count += n & 1; - n >>>= 1; - } - return count; -} -``` - -# 解法二 - -比较 `trick` 的方法,[官方](https://leetcode.com/problems/number-of-1-bits/solution/) 题解提供的,分享一下。 - -有一个方法,可以把最右边的 `1` 置为 `0`,举个具体的例子。 - -比如十进制的 `10`,二进制形式是 `1010`,然后我们只需要把它和 `9` 进行按位与操作,也就是 `10 & 9 = (1010) & (1001) = 1000`,也就是把 `1010` 最右边的 `1` 置为 `0`。 - -规律就是对于任意一个数 `n`,然后 `n & (n-1)` 的结果就是把 `n` 的最右边的 `1` 置为 `0` 。 - -也比较好理解,当我们对一个数减 `1` 的话,比如原来的数是 `...1010000`,然后减一就会向前借位,直到遇到最右边的第一个 `1`,变成 `...1001111`,然后我们把它和原数按位与,就会把从原数最右边 `1` 开始的位置全部置零了 `...10000000`。 - -有了这个技巧,我们只需要把原数依次将最右边的 `1` 置为 `0`,直到原数变成 `0`,记录总共操作了几次即可。 - -```java -public int hammingWeight(int n) { - int count = 0; - while (n != 0) { - n &= (n - 1); - count += 1; - } - return count; -} -``` - -# 解法三 - -有点类似于 [190 题](https://leetcode.wang/leetcode-190-Reverse-Bits.html) 的解法二,通过整体的位操作解决问题,参考 [这里](https://leetcode.com/problems/number-of-1-bits/discuss/55120/Short-code-of-C%2B%2B-O(m)-by-time-m-is-the-count-of-1's-and-another-several-method-of-O(1)-time) ,也是比较 `trick` 的,不容易想到,但还是很有意思的。 - -本质思想就是用本身的比特位去记录对应位数的比特位 `1` 的个数,举个具体的例子吧。为了简洁,求一下 `8` 比特的数字中 `1` 的个数。 - -```java -统计数代表对应括号内 1 的个数 -1 1 0 1 0 0 1 1 -首先把它看做 8 组,统计每组 1 的个数 -原数字:(1) (1) (0) (1) (0) (0) (1) (1) -统计数:(1) (1) (0) (1) (0) (0) (1) (1) -每个数字本身,就天然的代表了当前组 1 的个数。 - -接下来看做 4 组,相邻两组进行合并,统计数其实就是上边相邻组统计数相加即可。 -原数字:(1 1) (0 1) (0 0) (1 1) -统计数:(1 0) (0 1) (0 0) (1 0) -十进制: 2 1 0 2 - -接下来看做 2 组,相邻两组进行合并,统计数变成上边相邻组统计数的和。 -原数字:(1 1 0 1) (0 0 1 1) -统计数:(0 0 1 1) (0 0 1 0) -十进制: 3 2 - -接下来看做 1 组,相邻两组进行合并,统计数变成上边相邻组统计数的和。 -原数字:(1 1 0 1 0 0 1 1) -统计数:(0 0 0 0 0 1 0 1) -十进制: 5 -``` - -看一下 「统计数」的变化,也就是统计的 `1` 的个数。 - -看下二进制形式的变化,两两相加。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/191_2.jpg) - -看下十进制形式的变化,两两相加。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/191_3.jpg) - -最后我们就的得到了 `1` 的个数是 `5`。 - -所以问题的关键就是怎么实现每次合并相邻统计数,我们可以通过位操作实现,举个例子。 - -比如上边 `4` 组到 `2` 组中的前两组合成一组的变化。要把 `(1 0) (0 1)` 两组相加,变成 `(0 0 1 1)` 。其实我们只需要把 `1001` 和 `0011` 相与得到低两位,然后把 `1001` 右移两位再和 `0011` 相与得到高两位,最后将两数相加即可。也就是`(1001) & (0011) + (1001) >>> 2 & (0011)= 0011`。 - -扩展到任意情况,两组合并成一组,如果合并前每组的个数是 `n`,合并前的数字是 `x`,那么合并后的数字就是 `x & (000...111...) + x >>> n & (000...111...) `,其中 `0` 和 `1` 的个数是 `n`。 - -```java -public int hammingWeight(int n) { - n = (n & 0x55555555) + ((n >>> 1) & 0x55555555); // 32 组向 16 组合并,合并前每组 1 个数 - n = (n & 0x33333333) + ((n >>> 2) & 0x33333333); // 16 组向 8 组合并,合并前每组 2 个数 - n = (n & 0x0f0f0f0f) + ((n >>> 4) & 0x0f0f0f0f); // 8 组向 4 组合并,合并前每组 4 个数 - n = (n & 0x00ff00ff)+ ((n >>> 8) & 0x00ff00ff); // 4 组向 2 组合并,合并前每组 8 个数 - n = (n & 0x0000ffff) + ((n >>> 16) & 0x0000ffff); // 2 组向 1 组合并,合并前每组 16 个数 - return n; -} -``` - -写成 `16` 进制可能不好理解,我们拿16 组向 8 组合并举例,合并前每组 2 个数。也就是上边我们推导的,我们要把 `(1 0) (0 1)` 两组合并,需要和 `0011` 按位与,写成 `16` 进制就是 `3`,因为合并完是 `8` 组,所以就是 `8` 个 `3`,即 `0x33333333`。 - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/191.jpg) + +给一个数字,求出其二进制形式中 `1` 的个数。 + +# 解法一 + +简单粗暴些,依次判断最低位是否是 `1`,然后把它加入到结果中。判断最低位是否是 `1`,我们只需要把原数字和 `000000..001` 相与,也就是和 `1` 相与即可。 + +```java +public int hammingWeight(int n) { + int count = 0; + while (n != 0) { + count += n & 1; + n >>>= 1; + } + return count; +} +``` + +# 解法二 + +比较 `trick` 的方法,[官方](https://leetcode.com/problems/number-of-1-bits/solution/) 题解提供的,分享一下。 + +有一个方法,可以把最右边的 `1` 置为 `0`,举个具体的例子。 + +比如十进制的 `10`,二进制形式是 `1010`,然后我们只需要把它和 `9` 进行按位与操作,也就是 `10 & 9 = (1010) & (1001) = 1000`,也就是把 `1010` 最右边的 `1` 置为 `0`。 + +规律就是对于任意一个数 `n`,然后 `n & (n-1)` 的结果就是把 `n` 的最右边的 `1` 置为 `0` 。 + +也比较好理解,当我们对一个数减 `1` 的话,比如原来的数是 `...1010000`,然后减一就会向前借位,直到遇到最右边的第一个 `1`,变成 `...1001111`,然后我们把它和原数按位与,就会把从原数最右边 `1` 开始的位置全部置零了 `...10000000`。 + +有了这个技巧,我们只需要把原数依次将最右边的 `1` 置为 `0`,直到原数变成 `0`,记录总共操作了几次即可。 + +```java +public int hammingWeight(int n) { + int count = 0; + while (n != 0) { + n &= (n - 1); + count += 1; + } + return count; +} +``` + +# 解法三 + +有点类似于 [190 题](https://leetcode.wang/leetcode-190-Reverse-Bits.html) 的解法二,通过整体的位操作解决问题,参考 [这里](https://leetcode.com/problems/number-of-1-bits/discuss/55120/Short-code-of-C%2B%2B-O(m)-by-time-m-is-the-count-of-1's-and-another-several-method-of-O(1)-time) ,也是比较 `trick` 的,不容易想到,但还是很有意思的。 + +本质思想就是用本身的比特位去记录对应位数的比特位 `1` 的个数,举个具体的例子吧。为了简洁,求一下 `8` 比特的数字中 `1` 的个数。 + +```java +统计数代表对应括号内 1 的个数 +1 1 0 1 0 0 1 1 +首先把它看做 8 组,统计每组 1 的个数 +原数字:(1) (1) (0) (1) (0) (0) (1) (1) +统计数:(1) (1) (0) (1) (0) (0) (1) (1) +每个数字本身,就天然的代表了当前组 1 的个数。 + +接下来看做 4 组,相邻两组进行合并,统计数其实就是上边相邻组统计数相加即可。 +原数字:(1 1) (0 1) (0 0) (1 1) +统计数:(1 0) (0 1) (0 0) (1 0) +十进制: 2 1 0 2 + +接下来看做 2 组,相邻两组进行合并,统计数变成上边相邻组统计数的和。 +原数字:(1 1 0 1) (0 0 1 1) +统计数:(0 0 1 1) (0 0 1 0) +十进制: 3 2 + +接下来看做 1 组,相邻两组进行合并,统计数变成上边相邻组统计数的和。 +原数字:(1 1 0 1 0 0 1 1) +统计数:(0 0 0 0 0 1 0 1) +十进制: 5 +``` + +看一下 「统计数」的变化,也就是统计的 `1` 的个数。 + +看下二进制形式的变化,两两相加。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/191_2.jpg) + +看下十进制形式的变化,两两相加。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/191_3.jpg) + +最后我们就的得到了 `1` 的个数是 `5`。 + +所以问题的关键就是怎么实现每次合并相邻统计数,我们可以通过位操作实现,举个例子。 + +比如上边 `4` 组到 `2` 组中的前两组合成一组的变化。要把 `(1 0) (0 1)` 两组相加,变成 `(0 0 1 1)` 。其实我们只需要把 `1001` 和 `0011` 相与得到低两位,然后把 `1001` 右移两位再和 `0011` 相与得到高两位,最后将两数相加即可。也就是`(1001) & (0011) + (1001) >>> 2 & (0011)= 0011`。 + +扩展到任意情况,两组合并成一组,如果合并前每组的个数是 `n`,合并前的数字是 `x`,那么合并后的数字就是 `x & (000...111...) + x >>> n & (000...111...) `,其中 `0` 和 `1` 的个数是 `n`。 + +```java +public int hammingWeight(int n) { + n = (n & 0x55555555) + ((n >>> 1) & 0x55555555); // 32 组向 16 组合并,合并前每组 1 个数 + n = (n & 0x33333333) + ((n >>> 2) & 0x33333333); // 16 组向 8 组合并,合并前每组 2 个数 + n = (n & 0x0f0f0f0f) + ((n >>> 4) & 0x0f0f0f0f); // 8 组向 4 组合并,合并前每组 4 个数 + n = (n & 0x00ff00ff)+ ((n >>> 8) & 0x00ff00ff); // 4 组向 2 组合并,合并前每组 8 个数 + n = (n & 0x0000ffff) + ((n >>> 16) & 0x0000ffff); // 2 组向 1 组合并,合并前每组 16 个数 + return n; +} +``` + +写成 `16` 进制可能不好理解,我们拿16 组向 8 组合并举例,合并前每组 2 个数。也就是上边我们推导的,我们要把 `(1 0) (0 1)` 两组合并,需要和 `0011` 按位与,写成 `16` 进制就是 `3`,因为合并完是 `8` 组,所以就是 `8` 个 `3`,即 `0x33333333`。 + +# 总 + 解法一比较常规,解法二很技巧,解法三就更加技巧了,但想法很强。这几天都是操作二进制数,只要对一些位操作了解,常规解法只要按照题目意思还原过程即可。 \ No newline at end of file diff --git a/leetcode-198-House-Robber.md b/leetcode-198-House-Robber.md index eaa1649d3..4c4e3bbdd 100644 --- a/leetcode-198-House-Robber.md +++ b/leetcode-198-House-Robber.md @@ -1,135 +1,135 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/198.jpg) - -一个数组,每个元素代表商店的存款,一个小偷晚上去偷商店,问最多能偷多少钱。有一个前提,不能偷相邻的商店,不然警报会响起。 - -# 思路分析 - -一道很典型的通过子问题去解决原问题的题目,所以可以通过递归以及动态规划解决。 - -如果我们需要求前 `n` 家商店最多能偷多少钱,并且知道了前 `n - 1` 家店最多能偷的钱数,前 `n - 2` 家店最多能偷的钱数。 - -对于第 `n` 家店,我们只能选择偷或者不偷。 - -如果偷的话,那么前 `n` 家商店最多能偷的钱数就是「前 `n - 2` 家店最多能偷的钱数」加上「第 `n` 家店的钱数」。因为选择偷第 `n` 家商店,第 `n - 1` 家商店就不可以偷了。 - -如果不偷的话,那么前 `n` 家商店最多能偷的钱数就是「前 `n - 1` 家店最多能偷的钱数」。 - -最终前 `n` 家商店最多能偷的钱数就是上边两种情况选择较大的值。 - -接下来就是递归出口或者说初始条件。 - -当 `n = 0`,也就没有商店,那么能偷的最大钱数当然是 `0` 了。 - -当 `n = 1`,也就是只有一家店的时候,能偷的最大钱数就是当前店的钱数。 - -# 解法一 递归 - -通过上边的分析,代码也就直接出来了。 - -```java -public int rob(int[] nums) { - return robHelpler(nums, nums.length); -} - -private int robHelpler(int[] nums, int n) { - if (n == 0) { - return 0; - } - if (n == 1) { - return nums[0]; - } - return Math.max(robHelpler(nums, n - 2) + nums[n - 1], robHelpler(nums, n - 1)); -} -``` - -然后就会意料之中的超时。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/198_2.jpg) - -原因很简单,因为递归中会计算很多重复的解,解法方案的话我们就是把递归过程中的解存起来,第二次遇到的话直接返回即可。 - -```java -public int rob(int[] nums) { - int[] map = new int[nums.length + 1]; - Arrays.fill(map, -1); - return robHelpler(nums, nums.length, map); -} - -private int robHelpler(int[] nums, int n, int[] map) { - if (n == 0) { - return 0; - } - if (n == 1) { - return nums[0]; - } - if (map[n] != -1) { - return map[n]; - } - int res = Math.max(robHelpler(nums, n - 2, map) + nums[n - 1], robHelpler(nums, n - 1, map)); - map[n] = res; - return res; -} -``` - -# 解法二 动态规划 - -有了上边的递归和之前的分析,其实动态规划也可以直接出来了。 - -用 `dp[n]` 数组表示前 `n` 天能够带来的最大收益。 - -`dp[n] = Math.max(dp[n - 2] + nums[n - 1], dp[n - 1] )`。 - -初始条件的话, - -`dp[0] = 0` 以及 `dp[1] = nums[0]`。 - -```java -public int rob(int[] nums) { - int n = nums.length; - if (n == 0) { - return 0; - } - if (n == 1) { - return nums[0]; - } - - int[] dp = new int[n + 1]; - dp[0] = 0; - dp[1] = nums[0]; - for (int i = 2; i <= n; i++) { - dp[i] = Math.max(dp[i - 2] + nums[i - 1], dp[i - 1]); - } - return dp[n]; -} -``` - -接下来就是动态规划空间复杂度上的优化,比如 [5题](),[10题](),[53题](),[72题 ](),[115 题](https://leetcode.wang/leetcode-115-Distinct-Subsequences.html) 等等都已经用过了。 - -原因就是我们更新 `dp[i]` 的时候,只需要 `dp[i - 1]` 以及 `dp[i - 2]` 的信息,再之前的信息就不需要了,所以我们不需要数组,只需要几个变量就可以了。 - -```java -public int rob(int[] nums) { - int n = nums.length; - if (n == 0) { - return 0; - } - if (n == 1) { - return nums[0]; - } - - int pre = 0; //代替上边代码中的 dp[i - 2] - int cur = nums[0]; //代替上边代码中的 dp[i - 1] 和 dp[i] - for (int i = 2; i <= n; i++) { - int temp = cur; - cur = Math.max(pre + nums[i - 1], cur); - pre = temp; - } - return cur; -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/198.jpg) + +一个数组,每个元素代表商店的存款,一个小偷晚上去偷商店,问最多能偷多少钱。有一个前提,不能偷相邻的商店,不然警报会响起。 + +# 思路分析 + +一道很典型的通过子问题去解决原问题的题目,所以可以通过递归以及动态规划解决。 + +如果我们需要求前 `n` 家商店最多能偷多少钱,并且知道了前 `n - 1` 家店最多能偷的钱数,前 `n - 2` 家店最多能偷的钱数。 + +对于第 `n` 家店,我们只能选择偷或者不偷。 + +如果偷的话,那么前 `n` 家商店最多能偷的钱数就是「前 `n - 2` 家店最多能偷的钱数」加上「第 `n` 家店的钱数」。因为选择偷第 `n` 家商店,第 `n - 1` 家商店就不可以偷了。 + +如果不偷的话,那么前 `n` 家商店最多能偷的钱数就是「前 `n - 1` 家店最多能偷的钱数」。 + +最终前 `n` 家商店最多能偷的钱数就是上边两种情况选择较大的值。 + +接下来就是递归出口或者说初始条件。 + +当 `n = 0`,也就没有商店,那么能偷的最大钱数当然是 `0` 了。 + +当 `n = 1`,也就是只有一家店的时候,能偷的最大钱数就是当前店的钱数。 + +# 解法一 递归 + +通过上边的分析,代码也就直接出来了。 + +```java +public int rob(int[] nums) { + return robHelpler(nums, nums.length); +} + +private int robHelpler(int[] nums, int n) { + if (n == 0) { + return 0; + } + if (n == 1) { + return nums[0]; + } + return Math.max(robHelpler(nums, n - 2) + nums[n - 1], robHelpler(nums, n - 1)); +} +``` + +然后就会意料之中的超时。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/198_2.jpg) + +原因很简单,因为递归中会计算很多重复的解,解法方案的话我们就是把递归过程中的解存起来,第二次遇到的话直接返回即可。 + +```java +public int rob(int[] nums) { + int[] map = new int[nums.length + 1]; + Arrays.fill(map, -1); + return robHelpler(nums, nums.length, map); +} + +private int robHelpler(int[] nums, int n, int[] map) { + if (n == 0) { + return 0; + } + if (n == 1) { + return nums[0]; + } + if (map[n] != -1) { + return map[n]; + } + int res = Math.max(robHelpler(nums, n - 2, map) + nums[n - 1], robHelpler(nums, n - 1, map)); + map[n] = res; + return res; +} +``` + +# 解法二 动态规划 + +有了上边的递归和之前的分析,其实动态规划也可以直接出来了。 + +用 `dp[n]` 数组表示前 `n` 天能够带来的最大收益。 + +`dp[n] = Math.max(dp[n - 2] + nums[n - 1], dp[n - 1] )`。 + +初始条件的话, + +`dp[0] = 0` 以及 `dp[1] = nums[0]`。 + +```java +public int rob(int[] nums) { + int n = nums.length; + if (n == 0) { + return 0; + } + if (n == 1) { + return nums[0]; + } + + int[] dp = new int[n + 1]; + dp[0] = 0; + dp[1] = nums[0]; + for (int i = 2; i <= n; i++) { + dp[i] = Math.max(dp[i - 2] + nums[i - 1], dp[i - 1]); + } + return dp[n]; +} +``` + +接下来就是动态规划空间复杂度上的优化,比如 [5题](),[10题](),[53题](),[72题 ](),[115 题](https://leetcode.wang/leetcode-115-Distinct-Subsequences.html) 等等都已经用过了。 + +原因就是我们更新 `dp[i]` 的时候,只需要 `dp[i - 1]` 以及 `dp[i - 2]` 的信息,再之前的信息就不需要了,所以我们不需要数组,只需要几个变量就可以了。 + +```java +public int rob(int[] nums) { + int n = nums.length; + if (n == 0) { + return 0; + } + if (n == 1) { + return nums[0]; + } + + int pre = 0; //代替上边代码中的 dp[i - 2] + int cur = nums[0]; //代替上边代码中的 dp[i - 1] 和 dp[i] + for (int i = 2; i <= n; i++) { + int temp = cur; + cur = Math.max(pre + nums[i - 1], cur); + pre = temp; + } + return cur; +} +``` + +# 总 + 一道很典型的题,可以体会从递归 -> 递归加缓冲 -> 动态规划 -> 动态规划空间复杂度优化这一系列的过程,如果题目做的多了,最开始就可以直接想到最后的方法了。 \ No newline at end of file diff --git a/leetcode-199-Binary-Tree-Right-Side-View.md b/leetcode-199-Binary-Tree-Right-Side-View.md index bcd29b31c..cf12fc2ac 100644 --- a/leetcode-199-Binary-Tree-Right-Side-View.md +++ b/leetcode-199-Binary-Tree-Right-Side-View.md @@ -1,80 +1,80 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/199.jpg) - -给一个二叉树,然后想象自己站在二叉树右边向左边看过去,返回从上到下看到的数字序列。 - -# 解法一 - -题目意思再说的直白一些,就是依次输出二叉树每层最右边的元素。 - -每层最右边,可以想到二叉树的层次遍历,我们只需要保存每层遍历的最后一个元素即可。 - -二叉树的层次遍历在 [102 题](https://leetcode.wang/leetcode-102-Binary-Tree-Level-Order-Traversal.html) 已经做过了,代码拿过来用就可以。 - -我们只需要用一个队列,每次保存下层的元素即可。 - -```java -public List rightSideView(TreeNode root) { - Queue queue = new LinkedList(); - List res = new LinkedList<>(); - if (root == null) - return res; - queue.offer(root); - while (!queue.isEmpty()) { - int levelNum = queue.size(); // 当前层元素的个数 - for (int i = 0; i < levelNum; i++) { - TreeNode curNode = queue.poll(); - //只保存当前层的最后一个元素 - if (i == levelNum - 1) { - res.add(curNode.val); - } - if (curNode.left != null) { - queue.offer(curNode.left); - } - if (curNode.right != null) { - queue.offer(curNode.right); - } - - } - } - return res; -} -``` - -# 解法二 - -解法一的层次遍历是最直接的想法。我们也可以用深度优先遍历,在 [这里](https://leetcode.com/problems/binary-tree-right-side-view/discuss/56012/My-simple-accepted-solution(JAVA)) 看到的。 - -二叉树的深度优先遍历在之前也讨论过了, [94 题](https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html) 的中序遍历、 [144 题](https://leetcode.wang/leetcode-144-Binary-Tree-Preorder-Traversal.html) 的先序遍历以及 [145 题](https://leetcode.wang/leetcode-145-Binary-Tree-Postorder-Traversal.html) 的后序遍历。 - -这里采用最简单的递归写法,并且优先从右子树开始遍历。 - -用一个变量记录当前层数,每次保存第一次到达该层的元素。 - -```java -public List rightSideView(TreeNode root) { - List res = new LinkedList<>(); - rightSideViewHelper(root, 0, res); - return res; -} - -private void rightSideViewHelper(TreeNode root, int level, List res) { - if (root == null) { - return; - } - //res.size() 的值理解成当前在等待的层级数 - //res.size() == 0, 在等待 level = 0 的第一个数 - //res.size() == 1, 在等待 level = 1 的第一个数 - //res.size() == 2, 在等待 level = 2 的第一个数 - if (level == res.size()) { - res.add(root.val); - } - rightSideViewHelper(root.right, level + 1, res); - rightSideViewHelper(root.left, level + 1, res); -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/199.jpg) + +给一个二叉树,然后想象自己站在二叉树右边向左边看过去,返回从上到下看到的数字序列。 + +# 解法一 + +题目意思再说的直白一些,就是依次输出二叉树每层最右边的元素。 + +每层最右边,可以想到二叉树的层次遍历,我们只需要保存每层遍历的最后一个元素即可。 + +二叉树的层次遍历在 [102 题](https://leetcode.wang/leetcode-102-Binary-Tree-Level-Order-Traversal.html) 已经做过了,代码拿过来用就可以。 + +我们只需要用一个队列,每次保存下层的元素即可。 + +```java +public List rightSideView(TreeNode root) { + Queue queue = new LinkedList(); + List res = new LinkedList<>(); + if (root == null) + return res; + queue.offer(root); + while (!queue.isEmpty()) { + int levelNum = queue.size(); // 当前层元素的个数 + for (int i = 0; i < levelNum; i++) { + TreeNode curNode = queue.poll(); + //只保存当前层的最后一个元素 + if (i == levelNum - 1) { + res.add(curNode.val); + } + if (curNode.left != null) { + queue.offer(curNode.left); + } + if (curNode.right != null) { + queue.offer(curNode.right); + } + + } + } + return res; +} +``` + +# 解法二 + +解法一的层次遍历是最直接的想法。我们也可以用深度优先遍历,在 [这里](https://leetcode.com/problems/binary-tree-right-side-view/discuss/56012/My-simple-accepted-solution(JAVA)) 看到的。 + +二叉树的深度优先遍历在之前也讨论过了, [94 题](https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html) 的中序遍历、 [144 题](https://leetcode.wang/leetcode-144-Binary-Tree-Preorder-Traversal.html) 的先序遍历以及 [145 题](https://leetcode.wang/leetcode-145-Binary-Tree-Postorder-Traversal.html) 的后序遍历。 + +这里采用最简单的递归写法,并且优先从右子树开始遍历。 + +用一个变量记录当前层数,每次保存第一次到达该层的元素。 + +```java +public List rightSideView(TreeNode root) { + List res = new LinkedList<>(); + rightSideViewHelper(root, 0, res); + return res; +} + +private void rightSideViewHelper(TreeNode root, int level, List res) { + if (root == null) { + return; + } + //res.size() 的值理解成当前在等待的层级数 + //res.size() == 0, 在等待 level = 0 的第一个数 + //res.size() == 1, 在等待 level = 1 的第一个数 + //res.size() == 2, 在等待 level = 2 的第一个数 + if (level == res.size()) { + res.add(root.val); + } + rightSideViewHelper(root.right, level + 1, res); + rightSideViewHelper(root.left, level + 1, res); +} +``` + +# 总 + 这道题其实本质上就是考了二叉树的层次遍历和深度优先遍历。 \ No newline at end of file diff --git a/leetcode-200-Number-of-Islands.md b/leetcode-200-Number-of-Islands.md index e625708f0..482c78bbe 100644 --- a/leetcode-200-Number-of-Islands.md +++ b/leetcode-200-Number-of-Islands.md @@ -1,256 +1,256 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/200.jpg) - -一个二维数组,把 `1` 看做陆地,把 `0` 看做大海,陆地相连组成一个岛屿。把数组以外的区域也看做是大海,问总共有多少个岛屿。 - -# 解法一 - -想法很简单,我们只需要遍历二维数组,然后遇到 `1` 的时候,把当前的 `1` 以及它周围的所有 `1` 都标记成一个字符,这里直接标记成 `2`。然后记录遇到了几次 `1`,就代表有几个岛屿。看下边的例子。 - -```java -[1] 1 0 0 0 - 1 1 0 0 0 - 0 0 1 0 0 - 0 0 0 1 1 -当前遇到了 1, count = 1; -把当前的 1 和它周围的 1 标记为 2 -2 2 0 0 0 -2 2 0 0 0 -0 0 1 0 0 -0 0 0 1 1 - -2 2 0 0 0 -2 2 0 0 0 -0 0 [1] 0 0 -0 0 0 1 1 -遇到下一个 1, count = 2; -把当前的 1 和它周围的 1 标记为 2 -2 2 0 0 0 -2 2 0 0 0 -0 0 2 0 0 -0 0 0 1 1 - -2 2 0 0 0 -2 2 0 0 0 -0 0 2 0 0 -0 0 0 [1] 1 -遇到下一个 1, count = 3; -把当前的 1 和它周围的 1 标记为 2 -2 2 0 0 0 -2 2 0 0 0 -0 0 2 0 0 -0 0 0 2 2 - -没有 1 了,所以岛屿数是 count = 3 个。 -``` - -还有一个问题就是怎么标记与当前 `1` 相邻的 `1`。也很直接,我们直接把和当前 `1` 连通的位置看做一个图,然后做一个遍历即可。可以直接用递归写一个 `DFS`,即深度优先遍历。 - -```java -public int numIslands(char[][] grid) { - int count = 0; - int rows = grid.length; - if (rows == 0) { - return 0; - } - int cols = grid[0].length; - for (int r = 0; r < rows; r++) { - for (int c = 0; c < cols; c++) { - if (grid[r][c] == '1') { - count++; - marked(r, c, rows, cols, grid); - } - } - } - return count; -} - -private void marked(int r, int c, int rows, int cols, char[][] grid) { - if (r == -1 || c == -1 || r == rows || c == cols || grid[r][c] != '1') { - return; - } - //当前 1 标记为 2 - grid[r][c] = '2'; - - //向上下左右扩展 - marked(r + 1, c, rows, cols, grid); - marked(r, c + 1, rows, cols, grid); - marked(r - 1, c, rows, cols, grid); - marked(r, c - 1, rows, cols, grid); - -} -``` - -当然做遍历的话,我们也可以采用 `BFS`,广度优先遍历。图的广度优先遍历和二叉树的 [层次遍历](https://leetcode.wang/leetcode-102-Binary-Tree-Level-Order-Traversal.html) 类似,只需要借助一个队列即可。 - -和上边的区别不大,改一下标记函数即可。 - -此外入队列的时候,我们把二维坐标转为了一维,就省去了再创建一个类表示坐标。 - -```java -public int numIslands(char[][] grid) { - int count = 0; - int rows = grid.length; - if (rows == 0) { - return 0; - } - int cols = grid[0].length; - for (int r = 0; r < rows; r++) { - for (int c = 0; c < cols; c++) { - if (grid[r][c] == '1') { - count++; - bfs(r, c, rows, cols, grid); - } - } - } - return count; - } - private void bfs(int r, int c, int rows, int cols, char[][] grid) { - Queue queue = new LinkedList(); - queue.offer(r * cols + c); - while (!queue.isEmpty()) { - int cur = queue.poll(); - int row = cur / cols; - int col = cur % cols; - //已经标记过就结束,这句很关键,不然会把一些节点重复加入 - if(grid[row][col] == '2'){ - continue; - } - grid[row][col] = '2'; - //将上下左右连通的 1 加入队列 - if (row != (rows - 1) && grid[row + 1][col] == '1') { - queue.offer((row + 1) * cols + col); - } - if (col != (cols - 1) && grid[row][col + 1] == '1') { - queue.offer(row * cols + col + 1); - } - if (row != 0 && grid[row - 1][col] == '1') { - queue.offer((row - 1) * cols + col); - } - if (col != 0 && grid[row][col - 1] == '1') { - queue.offer(row * cols + col - 1); - } - - } - } -``` - -# 解法二 并查集 - -一开始看到这道题,我其实想到的是并查集,然后想了想感觉有些复杂,复杂度可能会高一些,就换了下思路想到了解法一。逛了一下 `Discuss` 发现也有人用并查集实现了,那这里也再总结下。 - -并查集在 [130 题](https://leetcode.wang/leetcode-130-Surrounded-Regions.html) 中用过一次,把当时的介绍在粘过来。 - -看下维基百科对 [并查集]() 的定义。 - -> 在[计算机科学](https://zh.wikipedia.org/wiki/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6)中,**并查集**是一种树型的[数据结构](https://zh.wikipedia.org/wiki/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84),用于处理一些[不交集](https://zh.wikipedia.org/wiki/%E4%B8%8D%E4%BA%A4%E9%9B%86)(Disjoint Sets)的合并及查询问题。有一个**联合-查找算法**(**union-find algorithm**)定义了两个用于此数据结构的操作: -> -> - Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。 -> - Union:将两个子集合并成同一个集合。 -> -> 由于支持这两种操作,一个不相交集也常被称为联合-查找数据结构(union-find data structure)或合并-查找集合(merge-find set)。其他的重要方法,MakeSet,用于创建单元素集合。有了这些方法,许多经典的[划分问题](https://zh.wikipedia.org/w/index.php?title=%E5%88%92%E5%88%86%E9%97%AE%E9%A2%98&action=edit&redlink=1)可以被解决。 -> -> 为了更加精确的定义这些方法,需要定义如何表示集合。一种常用的策略是为每个集合选定一个固定的元素,称为代表,以表示整个集合。接着,Find(x) 返回 x 所属集合的代表,而 Union 使用两个集合的代表作为参数。 - -网上很多讲并查集的文章了,这里推荐 [一篇](),大家可以先去看一下。 - -知道了并查集,下边就很好解决了,因为你会发现,我们做的就是分类的问题,把相邻的 `1` 都分成一类。 - -首先我们把每个节点各作为一类,用它的行数和列数生成一个 `id` 标识该类。 - -```java -int node(int i, int j) { - return i * cols + j; -} -``` - -用 `nums` 来记录当前有多少个岛屿,初始化的时候每个 `1` 都认为是一个岛屿,然后开始合并。 - -遍历每个为 `1 ` 的节点,将它的右边和下边的 `1` 和当前节点合并(这里算作一个优化,不需要像解法一那样上下左右)。每进行一次合并,我们就将 `nums` 减 `1` 。 - -最后返回 `nums` 即可。 - -```java -class UnionFind { - int[] parents; - int nums; - - public UnionFind(char[][] grid, int rows, int cols) { - nums = 0; - // 记录 1 的个数 - for (int i = 0; i < rows; i++) { - for (int j = 0; j < cols; j++) { - if (grid[i][j] == '1') { - nums++; - } - } - } - - //每一个类初始化为它本身 - int totalNodes = rows * cols; - parents = new int[totalNodes]; - for (int i = 0; i < totalNodes; i++) { - parents[i] = i; - } - } - - void union(int node1, int node2) { - int root1 = find(node1); - int root2 = find(node2); - //发生合并,nums-- - if (root1 != root2) { - parents[root2] = root1; - nums--; - } - } - - int find(int node) { - while (parents[node] != node) { - parents[node] = parents[parents[node]]; - node = parents[node]; - } - return node; - } - - int getNums() { - return nums; - } -} - -int rows; -int cols; - -public int numIslands(char[][] grid) { - if (grid.length == 0) - return 0; - - rows = grid.length; - cols = grid[0].length; - UnionFind uf = new UnionFind(grid, rows, cols); - - for (int row = 0; row < rows; row++) { - for (int col = 0; col < cols; col++) { - if (grid[row][col] == '1') { - // 将下边右边的 1 节点和当前节点合并 - if (row != (rows - 1) && grid[row + 1][col] == '1') { - uf.union(node(row, col), node(row + 1, col)); - } - if (col != (cols - 1) && grid[row][col + 1] == '1') { - uf.union(node(row, col), node(row, col + 1)); - } - } - } - } - return uf.getNums(); - -} - -int node(int i, int j) { - return i * cols + j; -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/200.jpg) + +一个二维数组,把 `1` 看做陆地,把 `0` 看做大海,陆地相连组成一个岛屿。把数组以外的区域也看做是大海,问总共有多少个岛屿。 + +# 解法一 + +想法很简单,我们只需要遍历二维数组,然后遇到 `1` 的时候,把当前的 `1` 以及它周围的所有 `1` 都标记成一个字符,这里直接标记成 `2`。然后记录遇到了几次 `1`,就代表有几个岛屿。看下边的例子。 + +```java +[1] 1 0 0 0 + 1 1 0 0 0 + 0 0 1 0 0 + 0 0 0 1 1 +当前遇到了 1, count = 1; +把当前的 1 和它周围的 1 标记为 2 +2 2 0 0 0 +2 2 0 0 0 +0 0 1 0 0 +0 0 0 1 1 + +2 2 0 0 0 +2 2 0 0 0 +0 0 [1] 0 0 +0 0 0 1 1 +遇到下一个 1, count = 2; +把当前的 1 和它周围的 1 标记为 2 +2 2 0 0 0 +2 2 0 0 0 +0 0 2 0 0 +0 0 0 1 1 + +2 2 0 0 0 +2 2 0 0 0 +0 0 2 0 0 +0 0 0 [1] 1 +遇到下一个 1, count = 3; +把当前的 1 和它周围的 1 标记为 2 +2 2 0 0 0 +2 2 0 0 0 +0 0 2 0 0 +0 0 0 2 2 + +没有 1 了,所以岛屿数是 count = 3 个。 +``` + +还有一个问题就是怎么标记与当前 `1` 相邻的 `1`。也很直接,我们直接把和当前 `1` 连通的位置看做一个图,然后做一个遍历即可。可以直接用递归写一个 `DFS`,即深度优先遍历。 + +```java +public int numIslands(char[][] grid) { + int count = 0; + int rows = grid.length; + if (rows == 0) { + return 0; + } + int cols = grid[0].length; + for (int r = 0; r < rows; r++) { + for (int c = 0; c < cols; c++) { + if (grid[r][c] == '1') { + count++; + marked(r, c, rows, cols, grid); + } + } + } + return count; +} + +private void marked(int r, int c, int rows, int cols, char[][] grid) { + if (r == -1 || c == -1 || r == rows || c == cols || grid[r][c] != '1') { + return; + } + //当前 1 标记为 2 + grid[r][c] = '2'; + + //向上下左右扩展 + marked(r + 1, c, rows, cols, grid); + marked(r, c + 1, rows, cols, grid); + marked(r - 1, c, rows, cols, grid); + marked(r, c - 1, rows, cols, grid); + +} +``` + +当然做遍历的话,我们也可以采用 `BFS`,广度优先遍历。图的广度优先遍历和二叉树的 [层次遍历](https://leetcode.wang/leetcode-102-Binary-Tree-Level-Order-Traversal.html) 类似,只需要借助一个队列即可。 + +和上边的区别不大,改一下标记函数即可。 + +此外入队列的时候,我们把二维坐标转为了一维,就省去了再创建一个类表示坐标。 + +```java +public int numIslands(char[][] grid) { + int count = 0; + int rows = grid.length; + if (rows == 0) { + return 0; + } + int cols = grid[0].length; + for (int r = 0; r < rows; r++) { + for (int c = 0; c < cols; c++) { + if (grid[r][c] == '1') { + count++; + bfs(r, c, rows, cols, grid); + } + } + } + return count; + } + private void bfs(int r, int c, int rows, int cols, char[][] grid) { + Queue queue = new LinkedList(); + queue.offer(r * cols + c); + while (!queue.isEmpty()) { + int cur = queue.poll(); + int row = cur / cols; + int col = cur % cols; + //已经标记过就结束,这句很关键,不然会把一些节点重复加入 + if(grid[row][col] == '2'){ + continue; + } + grid[row][col] = '2'; + //将上下左右连通的 1 加入队列 + if (row != (rows - 1) && grid[row + 1][col] == '1') { + queue.offer((row + 1) * cols + col); + } + if (col != (cols - 1) && grid[row][col + 1] == '1') { + queue.offer(row * cols + col + 1); + } + if (row != 0 && grid[row - 1][col] == '1') { + queue.offer((row - 1) * cols + col); + } + if (col != 0 && grid[row][col - 1] == '1') { + queue.offer(row * cols + col - 1); + } + + } + } +``` + +# 解法二 并查集 + +一开始看到这道题,我其实想到的是并查集,然后想了想感觉有些复杂,复杂度可能会高一些,就换了下思路想到了解法一。逛了一下 `Discuss` 发现也有人用并查集实现了,那这里也再总结下。 + +并查集在 [130 题](https://leetcode.wang/leetcode-130-Surrounded-Regions.html) 中用过一次,把当时的介绍在粘过来。 + +看下维基百科对 [并查集]() 的定义。 + +> 在[计算机科学](https://zh.wikipedia.org/wiki/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6)中,**并查集**是一种树型的[数据结构](https://zh.wikipedia.org/wiki/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84),用于处理一些[不交集](https://zh.wikipedia.org/wiki/%E4%B8%8D%E4%BA%A4%E9%9B%86)(Disjoint Sets)的合并及查询问题。有一个**联合-查找算法**(**union-find algorithm**)定义了两个用于此数据结构的操作: +> +> - Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。 +> - Union:将两个子集合并成同一个集合。 +> +> 由于支持这两种操作,一个不相交集也常被称为联合-查找数据结构(union-find data structure)或合并-查找集合(merge-find set)。其他的重要方法,MakeSet,用于创建单元素集合。有了这些方法,许多经典的[划分问题](https://zh.wikipedia.org/w/index.php?title=%E5%88%92%E5%88%86%E9%97%AE%E9%A2%98&action=edit&redlink=1)可以被解决。 +> +> 为了更加精确的定义这些方法,需要定义如何表示集合。一种常用的策略是为每个集合选定一个固定的元素,称为代表,以表示整个集合。接着,Find(x) 返回 x 所属集合的代表,而 Union 使用两个集合的代表作为参数。 + +网上很多讲并查集的文章了,这里推荐 [一篇](),大家可以先去看一下。 + +知道了并查集,下边就很好解决了,因为你会发现,我们做的就是分类的问题,把相邻的 `1` 都分成一类。 + +首先我们把每个节点各作为一类,用它的行数和列数生成一个 `id` 标识该类。 + +```java +int node(int i, int j) { + return i * cols + j; +} +``` + +用 `nums` 来记录当前有多少个岛屿,初始化的时候每个 `1` 都认为是一个岛屿,然后开始合并。 + +遍历每个为 `1 ` 的节点,将它的右边和下边的 `1` 和当前节点合并(这里算作一个优化,不需要像解法一那样上下左右)。每进行一次合并,我们就将 `nums` 减 `1` 。 + +最后返回 `nums` 即可。 + +```java +class UnionFind { + int[] parents; + int nums; + + public UnionFind(char[][] grid, int rows, int cols) { + nums = 0; + // 记录 1 的个数 + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + if (grid[i][j] == '1') { + nums++; + } + } + } + + //每一个类初始化为它本身 + int totalNodes = rows * cols; + parents = new int[totalNodes]; + for (int i = 0; i < totalNodes; i++) { + parents[i] = i; + } + } + + void union(int node1, int node2) { + int root1 = find(node1); + int root2 = find(node2); + //发生合并,nums-- + if (root1 != root2) { + parents[root2] = root1; + nums--; + } + } + + int find(int node) { + while (parents[node] != node) { + parents[node] = parents[parents[node]]; + node = parents[node]; + } + return node; + } + + int getNums() { + return nums; + } +} + +int rows; +int cols; + +public int numIslands(char[][] grid) { + if (grid.length == 0) + return 0; + + rows = grid.length; + cols = grid[0].length; + UnionFind uf = new UnionFind(grid, rows, cols); + + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++) { + if (grid[row][col] == '1') { + // 将下边右边的 1 节点和当前节点合并 + if (row != (rows - 1) && grid[row + 1][col] == '1') { + uf.union(node(row, col), node(row + 1, col)); + } + if (col != (cols - 1) && grid[row][col + 1] == '1') { + uf.union(node(row, col), node(row, col + 1)); + } + } + } + } + return uf.getNums(); + +} + +int node(int i, int j) { + return i * cols + j; +} +``` + +# 总 + 解法一标记的思想前边的题目也遇到过好多次了,解法二的话算作一个通用的解法,当发现题目是分类相关的,可以考虑并查集。 \ No newline at end of file diff --git a/leetcode-201-300.md b/leetcode-201-300.md index 3f5972578..3ebc70692 100644 --- a/leetcode-201-300.md +++ b/leetcode-201-300.md @@ -1,73 +1,73 @@ -# leetcode 201 到 300 题 - -201. Bitwise AND of Numbers Range - -202. Happy Number - -203. Remove Linked List Elements - -204. Count Primes - -205. Isomorphic Strings - -206. Reverse Linked List - -207. Course Schedule - -208. Implement Trie, Prefix Tree - -209. Minimum Size Subarray Sum - -210. Course Schedule II - -211. Add and Search Word - Data structure design - -212. Word Search II - -213. House Robber II - -214. Shortest Palindrome - -215. Kth Largest Element in an Array - -216. Combination Sum III - -217. Contains Duplicate - -218. The Skyline Problem - -219. Contains Duplicate II - -220. Contains Duplicate III - -221. Maximal Square - -222. Count Complete Tree Nodes - -223. Rectangle Area - -224. Basic Calculator - -225. Implement Stack using Queues - -226. Invert Binary Tree - -227. Basic Calculator II - -228. Summary Ranges - -229. Majority Element II - -230. Kth Smallest Element in a BST - -231. Power of Two - -232. Implement Queue using Stacks - -233. Number of Digit One - -234. Palindrome Linked List - -235. Lowest Common Ancestor of a Binary Search Tree - +# leetcode 201 到 300 题 + +201. Bitwise AND of Numbers Range + +202. Happy Number + +203. Remove Linked List Elements + +204. Count Primes + +205. Isomorphic Strings + +206. Reverse Linked List + +207. Course Schedule + +208. Implement Trie, Prefix Tree + +209. Minimum Size Subarray Sum + +210. Course Schedule II + +211. Add and Search Word - Data structure design + +212. Word Search II + +213. House Robber II + +214. Shortest Palindrome + +215. Kth Largest Element in an Array + +216. Combination Sum III + +217. Contains Duplicate + +218. The Skyline Problem + +219. Contains Duplicate II + +220. Contains Duplicate III + +221. Maximal Square + +222. Count Complete Tree Nodes + +223. Rectangle Area + +224. Basic Calculator + +225. Implement Stack using Queues + +226. Invert Binary Tree + +227. Basic Calculator II + +228. Summary Ranges + +229. Majority Element II + +230. Kth Smallest Element in a BST + +231. Power of Two + +232. Implement Queue using Stacks + +233. Number of Digit One + +234. Palindrome Linked List + +235. Lowest Common Ancestor of a Binary Search Tree +

...

\ No newline at end of file diff --git a/leetcode-201-Bitwise-AND-of-Numbers-Range.md b/leetcode-201-Bitwise-AND-of-Numbers-Range.md index 315655f2b..1f5e0c1b2 100644 --- a/leetcode-201-Bitwise-AND-of-Numbers-Range.md +++ b/leetcode-201-Bitwise-AND-of-Numbers-Range.md @@ -1,347 +1,347 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/201.jpg) - -给一个闭区间的范围,将这个范围内的所有数字相与,返回结果。例如 `[5, 7]` 就返回 `5 & 6 & 7`。 - -# 解法一 暴力 - -写一个 `for` 循环,依次相与即可。 - -```java -public int rangeBitwiseAnd(int m, int n) { - int res = m; - for (int i = m + 1; i <= n; i++) { - res &= i; - } - return res; -} -``` - -然后会发现时间超时了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/201_2.jpg) - -当范围太大的话会造成超时,这里优化的话想法也很很简单。我们只需要在 `res == 0` 的时候提前出 `for` 循环即可。 - -```java -public int rangeBitwiseAnd(int m, int n) { - int res = m; - for (int i = m + 1; i <= n; i++) { - res &= i; - if(res == 0){ - return 0; - } - } - return res; -} -``` - -但接下来遇到了 `wrong answer` 。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/201_3.jpg) - -把这个样例再根据代码理一遍,就会发现大问题了,根本原因就是补码的原因,可以看一下 [趣谈计算机补码](https://zhuanlan.zhihu.com/p/67227136)。 - -右边界 `n` 是 `2147483647`,也就是 `Integer` 中最大的正数,二进制形式是 `01111...1`,其中有 `31` 个 `1`。在代码中当 `i` 等于 `n` 的时候依旧会进入循环。出循环执行 `i++`,我们期望它变成 `2147483647 + 1 = 2147483648`,然后跳出 `for` 循环。事实上,对 `2147483647` 加 `1`,也就是 `01111...1` 加 `1`,变成了 `1000..000`,其中有 `31` 个 `1`。而这个二进制在补码中表示的是 `-2147483648`。因此我们依旧会进入 `for` 循环,以此往复,直到结果是 `0` 才出了 `for` 循环。。 - -知道了这个,我们只需要判断 `i == 2147483647` 的话,就跳出 `for` 循环即可。 - -```java -public int rangeBitwiseAnd(int m, int n) { - //m 要赋值给 i,所以提前判断一下 - if(m == Integer.MAX_VALUE){ - return m; - } - int res = m; - for (int i = m + 1; i <= n; i++) { - res &= i; - if(res == 0 || i == Integer.MAX_VALUE){ - break; - } - } - return res; -} -``` - -上边的解法就是我能想到的了,然后就去逛 `Discuss` 了,简直大开眼界。下边分享一下,主要是两种思路。 - -# 解法二 - -参考 [这里](https://leetcode.com/problems/bitwise-and-of-numbers-range/discuss/56729/Bit-operation-solution(JAVA)) 。 - -我们只需要一个经常用的一个思想,去考虑子问题。我们现在要做的是把从 `m` 到 `n` 的所有数字的 `32` 个比特位依次相与。直接肯定不能出结果,如果要是只考虑 `31` 个比特位呢,还是不能出结果。然后依次降低规模,`30`、`29` ... `3`,`2` 直到 `1`。如果让你说出从 `m` 到 `n` 的数字全部相与,最低位的结果是多少呢? - -最低位会有很多数相与,要么是 `0` ,要么是 `1`,而出现了 `0` 的话相与的结果一定会是 `0`。 - -只看所有数的最低位变化情况,`m` 到 `n` 的话,要么从 `0` 开始递增,`01010101...`,要么从 `1` 开始递增 `10101010...`。 - -因此,参与相与的数中最低位要么在第一个数字第一次出现 `0` ,要么在第二个数字出现第一次出现 `0` 。 - -如果 `m < n`,也就是参与相与的数字的个数至少有两个,所以一定会有 `0` 的出现,所以相与结果一定是 `0`。 - -看具体的例子,`[5,7]`。 - -```java -最低位序列从 1 开始递增, 也就是最右边的一列 101 -m 5 1 0 1 - 6 1 1 0 -n 7 1 1 1 - 0 -``` - -此时 `m < n`,所以至少会有两个数字,所以最低位相与结果一定是 `0`。 - -解决了最低位的问题,我们只需要把 `m` 和 `n` 同时右移一位。然后继续按照上边的思路考虑新的最低位的结果即可。 - -而当 `m == n` 的时候,很明显,结果就是 `m` 了。 - -代码中,我们需要用一个变量 `zero` 记录我们右移的次数,也就是最低位 `0` 的个数。 - -```java -public int rangeBitwiseAnd(int m, int n) { - int zeros = 0; - while (n > m) { - zeros++; - m >>>= 1; - n >>>= 1; - } - //将 0 的个数空出来 - return m << zeros; -} -``` - -然后还有一个优化的手段,在 [191 题](https://leetcode.wang/leetcode-191-Number-of-1-Bits.html) 介绍过一个把二进制最右边 `1` 置为 `0` 的方法,在这道题中也可以用到。 - -> 有一个方法,可以把最右边的 `1` 置为 `0`,举个具体的例子。 -> -> 比如十进制的 `10`,二进制形式是 `1010`,然后我们只需要把它和 `9` 进行按位与操作,也就是 `10 & 9 = (1010) & (1001) = 1000`,也就是把 `1010` 最右边的 `1` 置为 `0`。 -> -> 规律就是对于任意一个数 `n`,然后 `n & (n-1)` 的结果就是把 `n` 的最右边的 `1` 置为 `0` 。 -> -> 也比较好理解,当我们对一个数减 `1` 的话,比如原来的数是 `...1010000`,然后减一就会向前借位,直到遇到最右边的第一个 `1`,变成 `...1001111`,然后我们把它和原数按位与,就会把从原数最右边 `1` 开始的位置全部置零了 `...10000000`。 - -这里的话我们考虑一种可以优化的情况,我们直接用 `n` 这个变量去保存最终的结果,只需要考虑 `n` 的低位的 `1` 是否需要置为 `0`。 - -```java -m X X X X X X X X - ... -n X X X X 1 0 0 0 - -此时 m < n,上边的解法中然后我们会依次进行右移,我们考虑把 n 低位的 0 移光直到 1 移动到最低位 - -m2 X X X X X - ... -n2 X X X X 1 - -此时如果 m2 < n2,那么我们就可以确定最低位相与的结果一定是 0 - -回到开头 , n 的低位都是 0, 所以从 m < n 一定可以推出 m2 < n2, 所以最终结果的当前位一定是 0 - -因此,如果 m < n ,我们只需要把 n ,也就是 X X X X 1 0 0 0 的最右边的 1 置 0, 然后继续考虑。 -``` - -代码的话,用前边介绍的 `n & (n - 1)`。 - -```java -public int rangeBitwiseAnd(int m, int n) { - int zeros = 0; - while (n > m) { - n = n & (n - 1); - } - return n; -} -``` - -# 解法三 - -参考了 [这篇](https://leetcode.com/problems/bitwise-and-of-numbers-range/discuss/56827/Fast-three-line-C%2B%2B-solution-and-explanation-with-no-loops-or-recursion-and-one-extra-variable) 和 [这篇](https://leetcode.com/problems/bitwise-and-of-numbers-range/discuss/56735/Java-8-ms-one-liner-O(log(32))-no-loop-no-explicit-log)。 - -解法的关键就是去考虑这样一个问题,一个数大于一个数意味着什么?或者说,怎么判断一个数大于一个数? - -在十进制中,我们只需要从高位向低位看去,直到某一位不相同,大小也就判断了出来。 - -比如 `6489...` 和 `6486...`,由于 `9 > 6`,所以不管后边的位是什么 `6489...` 一定会大于 ``6486...`` 。 - -那么对于二进制呢? - -一样的道理,但因为二进制只有两个数 `0` 和 `1`,所以当出现某一位大于另一位的时候,一定是 `1 > 0`。 - -所以对于 `[m n]`,如果 `m < n`,那么一定是下边的形式。 - -```java -m S S S 0 X X X X -n S S S 1 X X X X -``` - -前边的若干位都相同,然后从某一位开始从 `0` 变成 `1`。 - -所有数字相与的结果,结合解法一的结论,如果 `n > m`,最低位相与后是 `0`。最后一定是 `S S S 0 0 0 0 0` 的形式。 - -因为高位保证了 `m` 和 `n` 同时右移以后,依旧是 `n > m`。 - -```java -m S S S 0 X X X X -n S S S 1 X X X X - -此时 n > m, 所以最低位结果是 0 - -然后 m 和 n 同时右移 - -m S S S 0 X X X -n S S S 1 X X X -依旧是 n > m, 所以最低位结果是 0 -``` - -因此相与结果最低位一直是 `0`,一直到 `S S S` 。所以最终结果就是 `S S S 0 0 0 0 0`。 - -其实和解法一的第二种思想有些类似,解法一中我们是从右往左依次将 `1` 置为 `0`。而在这里,我们从左往右看,找到第一个 `0` 和 `1`,就保证了移位过程中一定是 `n > m`。 - -知道了这个结论,我们只需要把 `m` 和 `11..1X0..00` 相与即可。上边例子中,我们只需要把 `S S S 0 X X X` 和 `1 1 1 X 0 0 0` 相与即可。 - -那么怎么得到 `1 1 1 X 0 0 0` 呢? - -再观察一下,`m` 和 `n`。 - -```java -m S S S 0 X X X X -n S S S 1 X X X X -``` - -我们如果把 `m` 和 `n` 进行异或操作,结果就是 `0 0 0 1 X X X X`。 - -对比一下异或后的结果和最后我们需要的结果。 - -```java -当前结果 0 0 0 1 X X X X -最后结果 1 1 1 X 0 0 0 0 -``` - -首先我们需要将低位全部变成 `0`。 - -```java -当前结果 0 0 0 1 0 0 0 0 -最后结果 1 1 1 X 0 0 0 0 -``` - -`java` 中有个方法可以实现,`Integer.highestOneBit`,可以实现保留最高位的 `1` ,然后将其它位全部置为 `0`。即,把 `0 0 0 1 X X X X` 变成 `0 0 0 1 0 0 0 0` 。 - -继续看上边的对比,接下来我们要把高位的 `0` 变为 `1`,通过取反操作,变成下边的结果。 - -```java -当前结果 1 1 1 0 1 1 1 1 -最后结果 1 1 1 X 0 0 0 0 -``` - -然后再在当前结果加 `1`,就实现了我们的转换。 - -```java -当前结果 1 1 1 1 0 0 0 0 -最后结果 1 1 1 X 0 0 0 0 -``` - -把最终得到的结果和 `m` 相与即可,`m == n` 的情况单独考虑。 - -```java -public int rangeBitwiseAnd(int m, int n) { - if (m == n) { - return m; - } - return m & ~Integer.highestOneBit(m ^ n) + 1; -} -``` - -结合 [补码](https://zhuanlan.zhihu.com/p/67227136) 的知识,「按位取反,末尾加 1」其实相当于取了一个相反数,[29 题](https://leetcode.wang/leetCode-29-Divide-Two-Integers.htmlhttps://leetcode.wang/leetCode-29-Divide-Two-Integers.html) 中我们也运用过这个结论。所以代码可以写的更简洁一些。 - -```java -public int rangeBitwiseAnd(int m, int n) { - return m == n ? m : m & -Integer.highestOneBit(m ^ n); -} -``` - -我们调用了库函数 `Integer.highestOneBit`,我们去看一下它的实现。 - -```java -/** - * Returns an {@code int} value with at most a single one-bit, in the - * position of the highest-order ("leftmost") one-bit in the specified - * {@code int} value. Returns zero if the specified value has no - * one-bits in its two's complement binary representation, that is, if it - * is equal to zero. - * - * @param i the value whose highest one bit is to be computed - * @return an {@code int} value with a single one-bit, in the position - * of the highest-order one-bit in the specified value, or zero if - * the specified value is itself equal to zero. - * @since 1.5 - */ -public static int highestOneBit(int i) { - // HD, Figure 3-1 - i |= (i >> 1); - i |= (i >> 2); - i |= (i >> 4); - i |= (i >> 8); - i |= (i >> 16); - return i - (i >>> 1); -} -``` - -它做了什么事情呢? - -对于 `0 0 0 1 X X X X` ,最终会变成 `0 0 0 1 1 1 1 1`,记做 `i` 。把 `i` 再右移一位变成 `0 0 0 0 1 1 1 1`,然后两数做差。 - -```java -i 0 0 0 1 1 1 1 1 -i >>> 1 0 0 0 0 1 1 1 1 - 0 0 0 1 0 0 0 0 -``` - -就得到了这个函数最后返回的结果了。 - -将 `0 0 0 1 X X X X` 变成 `0 0 0 1 1 1 1 1`,可以通过复制实现。 - -第一步,将首位的 `1` 赋值给它的旁边。 - -```java -i |= (i >> 1); -0 0 0 1 X X X X -> 0 0 0 1 1 X X X - -现在首位有两个 1 了,所以就将这两个 1 看做一个整体,继续把 1 赋值给它的旁边。 -i |= (i >> 2); -0 0 0 1 1 X X X -> 0 0 0 1 1 1 1 X - -现在首位有 4 个 1 了,所以就将这 4 个 1 看做一个整体,继续把 1 赋值给它的旁边。 -i |= (i >> 4); -0 0 0 1 1 1 1 X -> 0 0 0 1 1 1 1 1 - -其实到这里已经结束了,但函数中是考虑最坏的情况,类似于这种 1000000...00, 首位是 1, 有 31 个 0 -``` - -通过移位变成了 `0 0 0 1 1 1 1 1`,回想一下我们之前分析的,我们需要 `1 1 1 X 0 0 0` 的结果,和当前移位后的结果对比,我们只需要取反就可以得到了,最后和 `m` 相与即可。 - -```java -public int rangeBitwiseAnd(int m, int n) { - if (m == n) { - return m; - } - int i = m ^ n; - i |= (i >>> 1); - i |= (i >>> 2); - i |= (i >>> 4); - i |= (i >>> 8); - i |= (i >>> 16); - return m & ~i; -} -``` - -# 总 - -解法一只要注意溢出的问题即可。 - -解法二考虑的时候是从右往左考虑,解法三是从左往右考虑,但是殊途同归,本质上,两种解法都是求了两个数字的最长相同前缀,然后低位补零。 - -解法二中,我们不停的右移或者将右边的 `1` 置为 `0`,就是把不是相同前缀的部分置为 `0`,直到二者相等,也就是只剩下了相同前缀。 - -解法三中,通过异或,直接把相同前缀部分置为了 `0`。然后通过某种方法把相同前缀对应部分置为 `1` 来提取相同前缀。 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/201.jpg) + +给一个闭区间的范围,将这个范围内的所有数字相与,返回结果。例如 `[5, 7]` 就返回 `5 & 6 & 7`。 + +# 解法一 暴力 + +写一个 `for` 循环,依次相与即可。 + +```java +public int rangeBitwiseAnd(int m, int n) { + int res = m; + for (int i = m + 1; i <= n; i++) { + res &= i; + } + return res; +} +``` + +然后会发现时间超时了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/201_2.jpg) + +当范围太大的话会造成超时,这里优化的话想法也很很简单。我们只需要在 `res == 0` 的时候提前出 `for` 循环即可。 + +```java +public int rangeBitwiseAnd(int m, int n) { + int res = m; + for (int i = m + 1; i <= n; i++) { + res &= i; + if(res == 0){ + return 0; + } + } + return res; +} +``` + +但接下来遇到了 `wrong answer` 。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/201_3.jpg) + +把这个样例再根据代码理一遍,就会发现大问题了,根本原因就是补码的原因,可以看一下 [趣谈计算机补码](https://zhuanlan.zhihu.com/p/67227136)。 + +右边界 `n` 是 `2147483647`,也就是 `Integer` 中最大的正数,二进制形式是 `01111...1`,其中有 `31` 个 `1`。在代码中当 `i` 等于 `n` 的时候依旧会进入循环。出循环执行 `i++`,我们期望它变成 `2147483647 + 1 = 2147483648`,然后跳出 `for` 循环。事实上,对 `2147483647` 加 `1`,也就是 `01111...1` 加 `1`,变成了 `1000..000`,其中有 `31` 个 `1`。而这个二进制在补码中表示的是 `-2147483648`。因此我们依旧会进入 `for` 循环,以此往复,直到结果是 `0` 才出了 `for` 循环。。 + +知道了这个,我们只需要判断 `i == 2147483647` 的话,就跳出 `for` 循环即可。 + +```java +public int rangeBitwiseAnd(int m, int n) { + //m 要赋值给 i,所以提前判断一下 + if(m == Integer.MAX_VALUE){ + return m; + } + int res = m; + for (int i = m + 1; i <= n; i++) { + res &= i; + if(res == 0 || i == Integer.MAX_VALUE){ + break; + } + } + return res; +} +``` + +上边的解法就是我能想到的了,然后就去逛 `Discuss` 了,简直大开眼界。下边分享一下,主要是两种思路。 + +# 解法二 + +参考 [这里](https://leetcode.com/problems/bitwise-and-of-numbers-range/discuss/56729/Bit-operation-solution(JAVA)) 。 + +我们只需要一个经常用的一个思想,去考虑子问题。我们现在要做的是把从 `m` 到 `n` 的所有数字的 `32` 个比特位依次相与。直接肯定不能出结果,如果要是只考虑 `31` 个比特位呢,还是不能出结果。然后依次降低规模,`30`、`29` ... `3`,`2` 直到 `1`。如果让你说出从 `m` 到 `n` 的数字全部相与,最低位的结果是多少呢? + +最低位会有很多数相与,要么是 `0` ,要么是 `1`,而出现了 `0` 的话相与的结果一定会是 `0`。 + +只看所有数的最低位变化情况,`m` 到 `n` 的话,要么从 `0` 开始递增,`01010101...`,要么从 `1` 开始递增 `10101010...`。 + +因此,参与相与的数中最低位要么在第一个数字第一次出现 `0` ,要么在第二个数字出现第一次出现 `0` 。 + +如果 `m < n`,也就是参与相与的数字的个数至少有两个,所以一定会有 `0` 的出现,所以相与结果一定是 `0`。 + +看具体的例子,`[5,7]`。 + +```java +最低位序列从 1 开始递增, 也就是最右边的一列 101 +m 5 1 0 1 + 6 1 1 0 +n 7 1 1 1 + 0 +``` + +此时 `m < n`,所以至少会有两个数字,所以最低位相与结果一定是 `0`。 + +解决了最低位的问题,我们只需要把 `m` 和 `n` 同时右移一位。然后继续按照上边的思路考虑新的最低位的结果即可。 + +而当 `m == n` 的时候,很明显,结果就是 `m` 了。 + +代码中,我们需要用一个变量 `zero` 记录我们右移的次数,也就是最低位 `0` 的个数。 + +```java +public int rangeBitwiseAnd(int m, int n) { + int zeros = 0; + while (n > m) { + zeros++; + m >>>= 1; + n >>>= 1; + } + //将 0 的个数空出来 + return m << zeros; +} +``` + +然后还有一个优化的手段,在 [191 题](https://leetcode.wang/leetcode-191-Number-of-1-Bits.html) 介绍过一个把二进制最右边 `1` 置为 `0` 的方法,在这道题中也可以用到。 + +> 有一个方法,可以把最右边的 `1` 置为 `0`,举个具体的例子。 +> +> 比如十进制的 `10`,二进制形式是 `1010`,然后我们只需要把它和 `9` 进行按位与操作,也就是 `10 & 9 = (1010) & (1001) = 1000`,也就是把 `1010` 最右边的 `1` 置为 `0`。 +> +> 规律就是对于任意一个数 `n`,然后 `n & (n-1)` 的结果就是把 `n` 的最右边的 `1` 置为 `0` 。 +> +> 也比较好理解,当我们对一个数减 `1` 的话,比如原来的数是 `...1010000`,然后减一就会向前借位,直到遇到最右边的第一个 `1`,变成 `...1001111`,然后我们把它和原数按位与,就会把从原数最右边 `1` 开始的位置全部置零了 `...10000000`。 + +这里的话我们考虑一种可以优化的情况,我们直接用 `n` 这个变量去保存最终的结果,只需要考虑 `n` 的低位的 `1` 是否需要置为 `0`。 + +```java +m X X X X X X X X + ... +n X X X X 1 0 0 0 + +此时 m < n,上边的解法中然后我们会依次进行右移,我们考虑把 n 低位的 0 移光直到 1 移动到最低位 + +m2 X X X X X + ... +n2 X X X X 1 + +此时如果 m2 < n2,那么我们就可以确定最低位相与的结果一定是 0 + +回到开头 , n 的低位都是 0, 所以从 m < n 一定可以推出 m2 < n2, 所以最终结果的当前位一定是 0 + +因此,如果 m < n ,我们只需要把 n ,也就是 X X X X 1 0 0 0 的最右边的 1 置 0, 然后继续考虑。 +``` + +代码的话,用前边介绍的 `n & (n - 1)`。 + +```java +public int rangeBitwiseAnd(int m, int n) { + int zeros = 0; + while (n > m) { + n = n & (n - 1); + } + return n; +} +``` + +# 解法三 + +参考了 [这篇](https://leetcode.com/problems/bitwise-and-of-numbers-range/discuss/56827/Fast-three-line-C%2B%2B-solution-and-explanation-with-no-loops-or-recursion-and-one-extra-variable) 和 [这篇](https://leetcode.com/problems/bitwise-and-of-numbers-range/discuss/56735/Java-8-ms-one-liner-O(log(32))-no-loop-no-explicit-log)。 + +解法的关键就是去考虑这样一个问题,一个数大于一个数意味着什么?或者说,怎么判断一个数大于一个数? + +在十进制中,我们只需要从高位向低位看去,直到某一位不相同,大小也就判断了出来。 + +比如 `6489...` 和 `6486...`,由于 `9 > 6`,所以不管后边的位是什么 `6489...` 一定会大于 ``6486...`` 。 + +那么对于二进制呢? + +一样的道理,但因为二进制只有两个数 `0` 和 `1`,所以当出现某一位大于另一位的时候,一定是 `1 > 0`。 + +所以对于 `[m n]`,如果 `m < n`,那么一定是下边的形式。 + +```java +m S S S 0 X X X X +n S S S 1 X X X X +``` + +前边的若干位都相同,然后从某一位开始从 `0` 变成 `1`。 + +所有数字相与的结果,结合解法一的结论,如果 `n > m`,最低位相与后是 `0`。最后一定是 `S S S 0 0 0 0 0` 的形式。 + +因为高位保证了 `m` 和 `n` 同时右移以后,依旧是 `n > m`。 + +```java +m S S S 0 X X X X +n S S S 1 X X X X + +此时 n > m, 所以最低位结果是 0 + +然后 m 和 n 同时右移 + +m S S S 0 X X X +n S S S 1 X X X +依旧是 n > m, 所以最低位结果是 0 +``` + +因此相与结果最低位一直是 `0`,一直到 `S S S` 。所以最终结果就是 `S S S 0 0 0 0 0`。 + +其实和解法一的第二种思想有些类似,解法一中我们是从右往左依次将 `1` 置为 `0`。而在这里,我们从左往右看,找到第一个 `0` 和 `1`,就保证了移位过程中一定是 `n > m`。 + +知道了这个结论,我们只需要把 `m` 和 `11..1X0..00` 相与即可。上边例子中,我们只需要把 `S S S 0 X X X` 和 `1 1 1 X 0 0 0` 相与即可。 + +那么怎么得到 `1 1 1 X 0 0 0` 呢? + +再观察一下,`m` 和 `n`。 + +```java +m S S S 0 X X X X +n S S S 1 X X X X +``` + +我们如果把 `m` 和 `n` 进行异或操作,结果就是 `0 0 0 1 X X X X`。 + +对比一下异或后的结果和最后我们需要的结果。 + +```java +当前结果 0 0 0 1 X X X X +最后结果 1 1 1 X 0 0 0 0 +``` + +首先我们需要将低位全部变成 `0`。 + +```java +当前结果 0 0 0 1 0 0 0 0 +最后结果 1 1 1 X 0 0 0 0 +``` + +`java` 中有个方法可以实现,`Integer.highestOneBit`,可以实现保留最高位的 `1` ,然后将其它位全部置为 `0`。即,把 `0 0 0 1 X X X X` 变成 `0 0 0 1 0 0 0 0` 。 + +继续看上边的对比,接下来我们要把高位的 `0` 变为 `1`,通过取反操作,变成下边的结果。 + +```java +当前结果 1 1 1 0 1 1 1 1 +最后结果 1 1 1 X 0 0 0 0 +``` + +然后再在当前结果加 `1`,就实现了我们的转换。 + +```java +当前结果 1 1 1 1 0 0 0 0 +最后结果 1 1 1 X 0 0 0 0 +``` + +把最终得到的结果和 `m` 相与即可,`m == n` 的情况单独考虑。 + +```java +public int rangeBitwiseAnd(int m, int n) { + if (m == n) { + return m; + } + return m & ~Integer.highestOneBit(m ^ n) + 1; +} +``` + +结合 [补码](https://zhuanlan.zhihu.com/p/67227136) 的知识,「按位取反,末尾加 1」其实相当于取了一个相反数,[29 题](https://leetcode.wang/leetCode-29-Divide-Two-Integers.htmlhttps://leetcode.wang/leetCode-29-Divide-Two-Integers.html) 中我们也运用过这个结论。所以代码可以写的更简洁一些。 + +```java +public int rangeBitwiseAnd(int m, int n) { + return m == n ? m : m & -Integer.highestOneBit(m ^ n); +} +``` + +我们调用了库函数 `Integer.highestOneBit`,我们去看一下它的实现。 + +```java +/** + * Returns an {@code int} value with at most a single one-bit, in the + * position of the highest-order ("leftmost") one-bit in the specified + * {@code int} value. Returns zero if the specified value has no + * one-bits in its two's complement binary representation, that is, if it + * is equal to zero. + * + * @param i the value whose highest one bit is to be computed + * @return an {@code int} value with a single one-bit, in the position + * of the highest-order one-bit in the specified value, or zero if + * the specified value is itself equal to zero. + * @since 1.5 + */ +public static int highestOneBit(int i) { + // HD, Figure 3-1 + i |= (i >> 1); + i |= (i >> 2); + i |= (i >> 4); + i |= (i >> 8); + i |= (i >> 16); + return i - (i >>> 1); +} +``` + +它做了什么事情呢? + +对于 `0 0 0 1 X X X X` ,最终会变成 `0 0 0 1 1 1 1 1`,记做 `i` 。把 `i` 再右移一位变成 `0 0 0 0 1 1 1 1`,然后两数做差。 + +```java +i 0 0 0 1 1 1 1 1 +i >>> 1 0 0 0 0 1 1 1 1 + 0 0 0 1 0 0 0 0 +``` + +就得到了这个函数最后返回的结果了。 + +将 `0 0 0 1 X X X X` 变成 `0 0 0 1 1 1 1 1`,可以通过复制实现。 + +第一步,将首位的 `1` 赋值给它的旁边。 + +```java +i |= (i >> 1); +0 0 0 1 X X X X -> 0 0 0 1 1 X X X + +现在首位有两个 1 了,所以就将这两个 1 看做一个整体,继续把 1 赋值给它的旁边。 +i |= (i >> 2); +0 0 0 1 1 X X X -> 0 0 0 1 1 1 1 X + +现在首位有 4 个 1 了,所以就将这 4 个 1 看做一个整体,继续把 1 赋值给它的旁边。 +i |= (i >> 4); +0 0 0 1 1 1 1 X -> 0 0 0 1 1 1 1 1 + +其实到这里已经结束了,但函数中是考虑最坏的情况,类似于这种 1000000...00, 首位是 1, 有 31 个 0 +``` + +通过移位变成了 `0 0 0 1 1 1 1 1`,回想一下我们之前分析的,我们需要 `1 1 1 X 0 0 0` 的结果,和当前移位后的结果对比,我们只需要取反就可以得到了,最后和 `m` 相与即可。 + +```java +public int rangeBitwiseAnd(int m, int n) { + if (m == n) { + return m; + } + int i = m ^ n; + i |= (i >>> 1); + i |= (i >>> 2); + i |= (i >>> 4); + i |= (i >>> 8); + i |= (i >>> 16); + return m & ~i; +} +``` + +# 总 + +解法一只要注意溢出的问题即可。 + +解法二考虑的时候是从右往左考虑,解法三是从左往右考虑,但是殊途同归,本质上,两种解法都是求了两个数字的最长相同前缀,然后低位补零。 + +解法二中,我们不停的右移或者将右边的 `1` 置为 `0`,就是把不是相同前缀的部分置为 `0`,直到二者相等,也就是只剩下了相同前缀。 + +解法三中,通过异或,直接把相同前缀部分置为了 `0`。然后通过某种方法把相同前缀对应部分置为 `1` 来提取相同前缀。 + 这个题,太神奇了,太妙了! \ No newline at end of file diff --git a/leetcode-202-Happy-Number.md b/leetcode-202-Happy-Number.md index b64c9f975..00a492ab8 100644 --- a/leetcode-202-Happy-Number.md +++ b/leetcode-202-Happy-Number.md @@ -1,95 +1,95 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/202.jpg) - -给一个数字,将这个数字的各个位取平方然后相加,得到新的数字再重复这个过程。如果得到了 `1` 就返回 `true`,如果得不到 `1` 就返回 `false` 。 - -# 解法一 - -之前秋招的一道笔试题,当时想法也很简单。如果过程中得到了 `1` 直接返回 `true` 。 - -什么时候得不到 `1` 呢?产生了循环,也就是出现的数字在之前出现过,那么 `1` 一定不会得到了,此时返回 `false`。 - -在代码中,我们只需要用 `HashSet` 去记录已经得到的数字即可。 - -```java -public boolean isHappy(int n) { - HashSet set = new HashSet<>(); - set.add(n); - while (true) { - int next = getNext(n); - if (next == 1) { - return true; - } - if (set.contains(next)) { - return false; - } else { - set.add(next); - n = next; - } - } -} - -//计算各个位的平方和 -private int getNext(int n) { - int next = 0; - while (n > 0) { - int t = n % 10; - next += t * t; - n /= 10; - } - return next; -} -``` - -还有一个问题,代码中我们用了 `while` 循环,那么有没有可能永远不产生 `1` 并且不产生重复的数字,然后使得代码变成死循环呢? - -不需要担心,因为根据我们的算法,产生的数字一定是有限的。即使产生的数字不是有限的,因为我们用的是 `int` 来保存数字,`int` 所表示的数字个数是有限的。因此,如果产生的数字是 `n` 个,如果我们循环到第 `n + 1` 次,根据鸽巢原理,此时一定会产生一个重复数字了,从而跳出 `while` 循环。 - -# 解法二 - -参考 [这里](https://leetcode.com/problems/happy-number/discuss/56917/My-solution-in-C(-O(1)-space-and-no-magic-math-property-involved-)),优化了空间复杂度到 `O(1)`。 - -回想一下 [141 题](https://leetcode.wang/leetcode-141-Linked-List-Cycle.html),判断一个链表是否有环。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/141.png) - -而这道题,其实本质上就是判断链表是否有环,当出现重复的数字也就是产生了环。 - -所以我们可以用快慢指针的方法,或者叫 Floyd Cycle detection algorithm。 - -原理也很好理解,想象一下圆形跑道,两个人跑步,如果一个人跑的快,一个人跑的慢,那么不管两个人从哪个位置出发,跑的过程中两人一定会相遇。 - -所以这里我们用两个指针 `fast` 和 `slow`。`fast` 每次走两步,`slow` 每次走一步。 - -如果有重复的数字的话,`fast` 和 `slow` 就一定会相遇。 - -没有重复数字的话,当 `fast` 经过 `1` 的时候,就会停下来了。然后 `slow` 最终也会走向 `1`,所以也会相遇。 - -因此,代码的话,当 `fast` 和 `slow` 相遇的时候只需要判断当前是否是 `1` 即可。 - -```java -public boolean isHappy(int n) { - int slow = n; - int fast = n; - do { - slow = getNext(slow); - fast = getNext(getNext(fast)); - } while (slow != fast); - return slow == 1; -} - -private int getNext(int n) { - int next = 0; - while (n > 0) { - int t = n % 10; - next += t * t; - n /= 10; - } - return next; -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/202.jpg) + +给一个数字,将这个数字的各个位取平方然后相加,得到新的数字再重复这个过程。如果得到了 `1` 就返回 `true`,如果得不到 `1` 就返回 `false` 。 + +# 解法一 + +之前秋招的一道笔试题,当时想法也很简单。如果过程中得到了 `1` 直接返回 `true` 。 + +什么时候得不到 `1` 呢?产生了循环,也就是出现的数字在之前出现过,那么 `1` 一定不会得到了,此时返回 `false`。 + +在代码中,我们只需要用 `HashSet` 去记录已经得到的数字即可。 + +```java +public boolean isHappy(int n) { + HashSet set = new HashSet<>(); + set.add(n); + while (true) { + int next = getNext(n); + if (next == 1) { + return true; + } + if (set.contains(next)) { + return false; + } else { + set.add(next); + n = next; + } + } +} + +//计算各个位的平方和 +private int getNext(int n) { + int next = 0; + while (n > 0) { + int t = n % 10; + next += t * t; + n /= 10; + } + return next; +} +``` + +还有一个问题,代码中我们用了 `while` 循环,那么有没有可能永远不产生 `1` 并且不产生重复的数字,然后使得代码变成死循环呢? + +不需要担心,因为根据我们的算法,产生的数字一定是有限的。即使产生的数字不是有限的,因为我们用的是 `int` 来保存数字,`int` 所表示的数字个数是有限的。因此,如果产生的数字是 `n` 个,如果我们循环到第 `n + 1` 次,根据鸽巢原理,此时一定会产生一个重复数字了,从而跳出 `while` 循环。 + +# 解法二 + +参考 [这里](https://leetcode.com/problems/happy-number/discuss/56917/My-solution-in-C(-O(1)-space-and-no-magic-math-property-involved-)),优化了空间复杂度到 `O(1)`。 + +回想一下 [141 题](https://leetcode.wang/leetcode-141-Linked-List-Cycle.html),判断一个链表是否有环。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/141.png) + +而这道题,其实本质上就是判断链表是否有环,当出现重复的数字也就是产生了环。 + +所以我们可以用快慢指针的方法,或者叫 Floyd Cycle detection algorithm。 + +原理也很好理解,想象一下圆形跑道,两个人跑步,如果一个人跑的快,一个人跑的慢,那么不管两个人从哪个位置出发,跑的过程中两人一定会相遇。 + +所以这里我们用两个指针 `fast` 和 `slow`。`fast` 每次走两步,`slow` 每次走一步。 + +如果有重复的数字的话,`fast` 和 `slow` 就一定会相遇。 + +没有重复数字的话,当 `fast` 经过 `1` 的时候,就会停下来了。然后 `slow` 最终也会走向 `1`,所以也会相遇。 + +因此,代码的话,当 `fast` 和 `slow` 相遇的时候只需要判断当前是否是 `1` 即可。 + +```java +public boolean isHappy(int n) { + int slow = n; + int fast = n; + do { + slow = getNext(slow); + fast = getNext(getNext(fast)); + } while (slow != fast); + return slow == 1; +} + +private int getNext(int n) { + int next = 0; + while (n > 0) { + int t = n % 10; + next += t * t; + n /= 10; + } + return next; +} +``` + +# 总 + 解法一很常规,解法二的话将模型归结到有环链表太厉害了,自愧不如。 \ No newline at end of file diff --git a/leetcode-203-Remove-Linked-List-Elements.md b/leetcode-203-Remove-Linked-List-Elements.md index c61b59681..459de46a4 100644 --- a/leetcode-203-Remove-Linked-List-Elements.md +++ b/leetcode-203-Remove-Linked-List-Elements.md @@ -1,52 +1,52 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/203.jpg) - -给一个链表,删除链表中的给定值。 - -# 解法一 - -遍历一遍去找目标值,将找到的所有节点删除即可。 - -为了方便考虑头结点,我们加一个 `dummy` 指针,`next` 指向头结点,这个技巧在链表中经常用到。在 [19 题](https://leetcode.wang/leetCode-19-Remov-Nth-Node-From-End-of-List.html) 中应该是第一次用到。 - -```java -public ListNode removeElements(ListNode head, int val) { - ListNode dummyHead = new ListNode(0); - dummyHead.next = head; - ListNode newHead = dummyHead; - //newHead 始终指向要考虑的节点的前一个位置 - while (newHead.next != null) { - ListNode next = newHead.next; - if (next.val == val) { - newHead.next = next.next; - } else { - newHead = newHead.next; - } - - } - return dummyHead.next; -} -``` - -# 解法二 递归 - -也可以用递归,会更好理解一些。但是递归需要压栈,需要消耗一定的空间。 - -```java -public ListNode removeElements(ListNode head, int val) { - if (head == null) { - return head; - } - if (head.val == val) { - return removeElements(head.next, val); - } else { - head.next = removeElements(head.next, val); - return head; - } -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/203.jpg) + +给一个链表,删除链表中的给定值。 + +# 解法一 + +遍历一遍去找目标值,将找到的所有节点删除即可。 + +为了方便考虑头结点,我们加一个 `dummy` 指针,`next` 指向头结点,这个技巧在链表中经常用到。在 [19 题](https://leetcode.wang/leetCode-19-Remov-Nth-Node-From-End-of-List.html) 中应该是第一次用到。 + +```java +public ListNode removeElements(ListNode head, int val) { + ListNode dummyHead = new ListNode(0); + dummyHead.next = head; + ListNode newHead = dummyHead; + //newHead 始终指向要考虑的节点的前一个位置 + while (newHead.next != null) { + ListNode next = newHead.next; + if (next.val == val) { + newHead.next = next.next; + } else { + newHead = newHead.next; + } + + } + return dummyHead.next; +} +``` + +# 解法二 递归 + +也可以用递归,会更好理解一些。但是递归需要压栈,需要消耗一定的空间。 + +```java +public ListNode removeElements(ListNode head, int val) { + if (head == null) { + return head; + } + if (head.val == val) { + return removeElements(head.next, val); + } else { + head.next = removeElements(head.next, val); + return head; + } +} +``` + +# 总 + 主要就是对链表的删除,还有 `dummy` 指针的使用。 \ No newline at end of file diff --git a/leetcode-204-Count-Primes.md b/leetcode-204-Count-Primes.md index a0aacd30b..2b7be9567 100644 --- a/leetcode-204-Count-Primes.md +++ b/leetcode-204-Count-Primes.md @@ -1,70 +1,70 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/204.jpg) - -求出小于 `n` 的素数个数。 - -# 解法一 - -遍历 `2` 到 `n - 1` ,依次判断当前数是否是素数。 - -判断 `n` 是否是素数,只需要判断 `2` 到 `n - 1` 是否是 `n` 的因子,如果有一个是,那就表明 `n` 不是素数。 - -判断素数可以做一个优化,那就是不需要判断 `2` 到 `n - 1`,只需要判断`2` 到 `sqrt(n)` 也就是根号 `n` 即可。因为如果存在超过根号 `n` 的因子,那么一定存在小于根号 `n` 的数与之对应。 - -```java -public int countPrimes(int n) { - int count = 0; - for (int i = 2; i < n; i++) { - if (isPrime(i)) { - count++; - } - } - return count; -} - -private boolean isPrime(int n) { - int sqrt = (int) Math.sqrt(n); - for (int i = 2; i <= sqrt; i++) { - if (n % i == 0) { - return false; - } - } - return true; -} -``` - -# 解法二 - -空间换时间,参考 [这里](https://leetcode.com/problems/count-primes/discuss/57588/My-simple-Java-solution)。 - -用一个数组表示当前数是否是素数。 - -然后从 `2` 开始,将 `2` 的倍数,`4`、`6`、`8`、`10` ...依次标记为非素数。 - -下个素数 `3`,将 `3` 的倍数,`6`、`9`、`12`、`15` ...依次标记为非素数。 - -下个素数 `7`,将 `7` 的倍数,`14`、`21`、`28`、`35` ...依次标记为非素数。 - -在代码中,因为数组默认值是 `false` ,所以用 `false` 代表当前数是素数,用 `true` 代表当前数是非素数。 - -```java -public int countPrimes(int n) { - boolean[] notPrime = new boolean[n]; - int count = 0; - for (int i = 2; i < n; i++) { - if (!notPrime[i]) { - count++; - //将当前素数的倍数依次标记为非素数 - for (int j = 2; j * i < n; j++) { - notPrime[j * i] = true; - } - } - } - return count; -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/204.jpg) + +求出小于 `n` 的素数个数。 + +# 解法一 + +遍历 `2` 到 `n - 1` ,依次判断当前数是否是素数。 + +判断 `n` 是否是素数,只需要判断 `2` 到 `n - 1` 是否是 `n` 的因子,如果有一个是,那就表明 `n` 不是素数。 + +判断素数可以做一个优化,那就是不需要判断 `2` 到 `n - 1`,只需要判断`2` 到 `sqrt(n)` 也就是根号 `n` 即可。因为如果存在超过根号 `n` 的因子,那么一定存在小于根号 `n` 的数与之对应。 + +```java +public int countPrimes(int n) { + int count = 0; + for (int i = 2; i < n; i++) { + if (isPrime(i)) { + count++; + } + } + return count; +} + +private boolean isPrime(int n) { + int sqrt = (int) Math.sqrt(n); + for (int i = 2; i <= sqrt; i++) { + if (n % i == 0) { + return false; + } + } + return true; +} +``` + +# 解法二 + +空间换时间,参考 [这里](https://leetcode.com/problems/count-primes/discuss/57588/My-simple-Java-solution)。 + +用一个数组表示当前数是否是素数。 + +然后从 `2` 开始,将 `2` 的倍数,`4`、`6`、`8`、`10` ...依次标记为非素数。 + +下个素数 `3`,将 `3` 的倍数,`6`、`9`、`12`、`15` ...依次标记为非素数。 + +下个素数 `7`,将 `7` 的倍数,`14`、`21`、`28`、`35` ...依次标记为非素数。 + +在代码中,因为数组默认值是 `false` ,所以用 `false` 代表当前数是素数,用 `true` 代表当前数是非素数。 + +```java +public int countPrimes(int n) { + boolean[] notPrime = new boolean[n]; + int count = 0; + for (int i = 2; i < n; i++) { + if (!notPrime[i]) { + count++; + //将当前素数的倍数依次标记为非素数 + for (int j = 2; j * i < n; j++) { + notPrime[j * i] = true; + } + } + } + return count; +} +``` + +# 总 + 解法二中空间换时间的思想在编程中经常用到。 \ No newline at end of file diff --git a/leetcode-205-Isomorphic-Strings.md b/leetcode-205-Isomorphic-Strings.md index 9b24ccd59..9ee5f7726 100644 --- a/leetcode-205-Isomorphic-Strings.md +++ b/leetcode-205-Isomorphic-Strings.md @@ -1,235 +1,235 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/205.jpg) - -判断两个字符串是否是同构的。 - -# 解法一 - -题目描述中已经很详细了,两个字符串同构的含义就是字符串 `s` 可以唯一的映射到 `t` ,同时 `t` 也可以唯一的映射到 `s` 。 - -举个具体的例子。 - -```java -egg 和 add 同构,就意味着下边的映射成立 -e -> a -g -> d -也就是将 egg 的 e 换成 a, g 换成 d, 就变成了 add - -同时倒过来也成立 -a -> e -d -> g -也就是将 add 的 a 换成 e, d 换成 g, 就变成了 egg - -foo 和 bar 不同构,原因就是映射不唯一 -o -> a -o -> r -其中 o 映射到了两个字母 -``` - -我们可以利用一个 `map` 来处理映射。对于 `s` 到 `t` 的映射,我们同时遍历 `s` 和 `t` ,假设当前遇到的字母分别是 `c1` 和 `c2` 。 - -如果 `map[c1]` 不存在,那么就将 `c1` 映射到 `c2` ,即 `map[c1] = c2`。 - -如果 `map[c1]` 存在,那么就判断 `map[c1]` 是否等于 `c2`,也就是验证之前的映射和当前的字母是否相同。 - -```java -private boolean isIsomorphicHelper(String s, String t) { - int n = s.length(); - HashMap map = new HashMap<>(); - for (int i = 0; i < n; i++) { - char c1 = s.charAt(i); - char c2 = s.charAt(i); - if (map.containsKey(c1)) { - if (map.get(c1) != c2) { - return false; - } - } else { - map.put(c1, c2); - } - } - return true; -} -``` - -对于这道题,我们只需要验证 `s - > t` 和 `t -> s` 两个方向即可。如果验证一个方向,是不可以的。 - -举个例子,`s = ab, t = cc`,如果单看 `s -> t` ,那么 `a -> c, b -> c` 是没有问题的。 - -必须再验证 `t -> s`,此时,`c -> a, c -> b`,一个字母对应了多个字母,所以不是同构的。 - -代码的话,只需要调用两次上边的代码即可。 - -```java -public boolean isIsomorphic(String s, String t) { - return isIsomorphicHelper(s, t) && isIsomorphicHelper(t, s); - -} - -private boolean isIsomorphicHelper(String s, String t) { - int n = s.length(); - HashMap map = new HashMap<>(); - for (int i = 0; i < n; i++) { - char c1 = s.charAt(i); - char c2 = t.charAt(i); - if (map.containsKey(c1)) { - if (map.get(c1) != c2) { - return false; - } - } else { - map.put(c1, c2); - } - } - return true; -} -``` - -# 解法二 - -另一种思想,参考 [这里](https://leetcode.com/problems/isomorphic-strings/discuss/57796/My-6-lines-solution) 。 - -解法一中,我们判断 `s` 和 `t` 是否一一对应,通过对两个方向分别考虑来解决的。 - -这里的话,我们找一个第三方来解决,即,按照字母出现的顺序,把两个字符串都映射到另一个集合中。 - -举个现实生活中的例子,一个人说中文,一个人说法语,怎么判断他们说的是一个意思呢?把中文翻译成英语,把法语也翻译成英语,然后看最后的英语是否相同即可。 - -```java -将第一个出现的字母映射成 1,第二个出现的字母映射成 2 - -对于 egg -e -> 1 -g -> 2 -也就是将 egg 的 e 换成 1, g 换成 2, 就变成了 122 - -对于 add -a -> 1 -d -> 2 -也就是将 add 的 a 换成 1, d 换成 2, 就变成了 122 - -egg -> 122, add -> 122 -都变成了 122,所以两个字符串异构。 -``` - -代码的话,只需要将两个字符串分别翻译成第三种类型即可。我们可以定义一个变量 `count = 1`,映射给出现的字母,然后进行自增。 - -```java -public boolean isIsomorphic(String s, String t) { - return isIsomorphicHelper(s).equals(isIsomorphicHelper(t)); -} - -private String isIsomorphicHelper(String s) { - int[] map = new int[128]; - int n = s.length(); - StringBuilder sb = new StringBuilder(); - int count = 1; - for (int i = 0; i < n; i++) { - char c = s.charAt(i); - //当前字母第一次出现,赋值 - if (map[c] == 0) { - map[c] = count; - count++; - } - sb.append(map[c]); - } - return sb.toString(); -} -``` - -为了方便,我们也可以将当前字母直接映射为当前字母的下标加 `1`。因为 `0` 是我们的默认值,所以不能直接赋值为下标,而是「下标加 `1`」。 - -```java -public boolean isIsomorphic(String s, String t) { - return isIsomorphicHelper(s).equals(isIsomorphicHelper(t)); -} - -private String isIsomorphicHelper(String s) { - int[] map = new int[128]; - int n = s.length(); - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < n; i++) { - char c = s.charAt(i); - //当前字母第一次出现,赋值 - if (map[c] == 0) { - map[c] = i + 1; - } - sb.append(map[c]); - } - return sb.toString(); -} -``` - -上边的 `isIsomorphicHelper` 中我们通过 `map` 记录了当前字母要映射到哪个数字,然后最终返回了整个转换后的字符串。 - -其实我们不需要将字符串完全转换,我们可以用两个 `map` 分别记录两个字符串每个字母的映射。将所有字母初始都映射到 `0`。记录过程中,如果发现了当前映射不一致,就可以立即返回 `false` 了。 - -举个例子。 - -```java -对 abaddee 和 cdbccff - -a b a d d e e -c d b c c f f -^ - -当前 -a -> 0 -c -> 0 - -修改映射 -a -> 1 -c -> 1 - -a b a d d e e -c d b c c f f - ^ - -当前 -b -> 0 -d -> 0 - -修改映射 -b -> 2 -d -> 2 - - -a b a d d e e -c d b c c f f - ^ -当前 -a -> 1 (之前被修改过) -b -> 0 - -出现不一致,所以两个字符串不异构 -``` - -代码的话,用两个 `map` 记录映射即可。 - -```java -public boolean isIsomorphic(String s, String t) { - int n = s.length(); - int[] mapS = new int[128]; - int[] mapT = new int[128]; - for (int i = 0; i < n; i++) { - char c1 = s.charAt(i); - char c2 = t.charAt(i); - //当前的映射值是否相同 - if (mapS[c1] != mapT[c2]) { - return false; - } else { - //是否已经修改过,修改过就不需要再处理 - if (mapS[c1] == 0) { - mapS[c1] = i + 1; - mapT[c2] = i + 1; - } - } - } - return true; -} -``` - -# 总 - -解法一就是我们比较常规的思路,解法二通过一个第三方的集合,将代码大大简化了,太巧妙了! - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/205.jpg) + +判断两个字符串是否是同构的。 + +# 解法一 + +题目描述中已经很详细了,两个字符串同构的含义就是字符串 `s` 可以唯一的映射到 `t` ,同时 `t` 也可以唯一的映射到 `s` 。 + +举个具体的例子。 + +```java +egg 和 add 同构,就意味着下边的映射成立 +e -> a +g -> d +也就是将 egg 的 e 换成 a, g 换成 d, 就变成了 add + +同时倒过来也成立 +a -> e +d -> g +也就是将 add 的 a 换成 e, d 换成 g, 就变成了 egg + +foo 和 bar 不同构,原因就是映射不唯一 +o -> a +o -> r +其中 o 映射到了两个字母 +``` + +我们可以利用一个 `map` 来处理映射。对于 `s` 到 `t` 的映射,我们同时遍历 `s` 和 `t` ,假设当前遇到的字母分别是 `c1` 和 `c2` 。 + +如果 `map[c1]` 不存在,那么就将 `c1` 映射到 `c2` ,即 `map[c1] = c2`。 + +如果 `map[c1]` 存在,那么就判断 `map[c1]` 是否等于 `c2`,也就是验证之前的映射和当前的字母是否相同。 + +```java +private boolean isIsomorphicHelper(String s, String t) { + int n = s.length(); + HashMap map = new HashMap<>(); + for (int i = 0; i < n; i++) { + char c1 = s.charAt(i); + char c2 = s.charAt(i); + if (map.containsKey(c1)) { + if (map.get(c1) != c2) { + return false; + } + } else { + map.put(c1, c2); + } + } + return true; +} +``` + +对于这道题,我们只需要验证 `s - > t` 和 `t -> s` 两个方向即可。如果验证一个方向,是不可以的。 + +举个例子,`s = ab, t = cc`,如果单看 `s -> t` ,那么 `a -> c, b -> c` 是没有问题的。 + +必须再验证 `t -> s`,此时,`c -> a, c -> b`,一个字母对应了多个字母,所以不是同构的。 + +代码的话,只需要调用两次上边的代码即可。 + +```java +public boolean isIsomorphic(String s, String t) { + return isIsomorphicHelper(s, t) && isIsomorphicHelper(t, s); + +} + +private boolean isIsomorphicHelper(String s, String t) { + int n = s.length(); + HashMap map = new HashMap<>(); + for (int i = 0; i < n; i++) { + char c1 = s.charAt(i); + char c2 = t.charAt(i); + if (map.containsKey(c1)) { + if (map.get(c1) != c2) { + return false; + } + } else { + map.put(c1, c2); + } + } + return true; +} +``` + +# 解法二 + +另一种思想,参考 [这里](https://leetcode.com/problems/isomorphic-strings/discuss/57796/My-6-lines-solution) 。 + +解法一中,我们判断 `s` 和 `t` 是否一一对应,通过对两个方向分别考虑来解决的。 + +这里的话,我们找一个第三方来解决,即,按照字母出现的顺序,把两个字符串都映射到另一个集合中。 + +举个现实生活中的例子,一个人说中文,一个人说法语,怎么判断他们说的是一个意思呢?把中文翻译成英语,把法语也翻译成英语,然后看最后的英语是否相同即可。 + +```java +将第一个出现的字母映射成 1,第二个出现的字母映射成 2 + +对于 egg +e -> 1 +g -> 2 +也就是将 egg 的 e 换成 1, g 换成 2, 就变成了 122 + +对于 add +a -> 1 +d -> 2 +也就是将 add 的 a 换成 1, d 换成 2, 就变成了 122 + +egg -> 122, add -> 122 +都变成了 122,所以两个字符串异构。 +``` + +代码的话,只需要将两个字符串分别翻译成第三种类型即可。我们可以定义一个变量 `count = 1`,映射给出现的字母,然后进行自增。 + +```java +public boolean isIsomorphic(String s, String t) { + return isIsomorphicHelper(s).equals(isIsomorphicHelper(t)); +} + +private String isIsomorphicHelper(String s) { + int[] map = new int[128]; + int n = s.length(); + StringBuilder sb = new StringBuilder(); + int count = 1; + for (int i = 0; i < n; i++) { + char c = s.charAt(i); + //当前字母第一次出现,赋值 + if (map[c] == 0) { + map[c] = count; + count++; + } + sb.append(map[c]); + } + return sb.toString(); +} +``` + +为了方便,我们也可以将当前字母直接映射为当前字母的下标加 `1`。因为 `0` 是我们的默认值,所以不能直接赋值为下标,而是「下标加 `1`」。 + +```java +public boolean isIsomorphic(String s, String t) { + return isIsomorphicHelper(s).equals(isIsomorphicHelper(t)); +} + +private String isIsomorphicHelper(String s) { + int[] map = new int[128]; + int n = s.length(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < n; i++) { + char c = s.charAt(i); + //当前字母第一次出现,赋值 + if (map[c] == 0) { + map[c] = i + 1; + } + sb.append(map[c]); + } + return sb.toString(); +} +``` + +上边的 `isIsomorphicHelper` 中我们通过 `map` 记录了当前字母要映射到哪个数字,然后最终返回了整个转换后的字符串。 + +其实我们不需要将字符串完全转换,我们可以用两个 `map` 分别记录两个字符串每个字母的映射。将所有字母初始都映射到 `0`。记录过程中,如果发现了当前映射不一致,就可以立即返回 `false` 了。 + +举个例子。 + +```java +对 abaddee 和 cdbccff + +a b a d d e e +c d b c c f f +^ + +当前 +a -> 0 +c -> 0 + +修改映射 +a -> 1 +c -> 1 + +a b a d d e e +c d b c c f f + ^ + +当前 +b -> 0 +d -> 0 + +修改映射 +b -> 2 +d -> 2 + + +a b a d d e e +c d b c c f f + ^ +当前 +a -> 1 (之前被修改过) +b -> 0 + +出现不一致,所以两个字符串不异构 +``` + +代码的话,用两个 `map` 记录映射即可。 + +```java +public boolean isIsomorphic(String s, String t) { + int n = s.length(); + int[] mapS = new int[128]; + int[] mapT = new int[128]; + for (int i = 0; i < n; i++) { + char c1 = s.charAt(i); + char c2 = t.charAt(i); + //当前的映射值是否相同 + if (mapS[c1] != mapT[c2]) { + return false; + } else { + //是否已经修改过,修改过就不需要再处理 + if (mapS[c1] == 0) { + mapS[c1] = i + 1; + mapT[c2] = i + 1; + } + } + } + return true; +} +``` + +# 总 + +解法一就是我们比较常规的思路,解法二通过一个第三方的集合,将代码大大简化了,太巧妙了! + 题目其实有点像映射的知识,两个字符串为两个集合,然后判断当前映射是否为单射。 \ No newline at end of file diff --git a/leetcode-206-Reverse-Linked-List.md b/leetcode-206-Reverse-Linked-List.md index d6927d177..acd0d7055 100644 --- a/leetcode-206-Reverse-Linked-List.md +++ b/leetcode-206-Reverse-Linked-List.md @@ -1,118 +1,118 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/206.jpg) - -单链表倒置。 - -之前在 [第 2 题](https://leetcode.wang/leetCode-2-Add-Two-Numbers.html) 大数相加的时候已经分享过了,这里直接贴过来。 - -# 解法一迭代 - -首先看一下原链表。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/l0.jpg) - -总共需要添加两个指针,`pre` 和 `next`。 - -初始化 `pre` 指向 `NULL` 。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/l00.jpg) - -然后就是迭代的步骤,总共四步,顺序一步都不能错。 - -- `next` 指向 `head` 的 `next` ,防止原链表丢失 - - ![](http://windliang.oss-cn-beijing.aliyuncs.com/l1.jpg) - -- `head` 的 `next` 从原来链表脱离,指向 `pre` 。 - - ![](http://windliang.oss-cn-beijing.aliyuncs.com/l2.jpg) - -- `pre` 指向 `head` - - ![](http://windliang.oss-cn-beijing.aliyuncs.com/l3.jpg) - -- `head` 指向 `next` - - ![](http://windliang.oss-cn-beijing.aliyuncs.com/l4.jpg) - -一次迭代就完成了,如果再进行一次迭代就变成下边的样子。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/l5.jpg) - -可以看到整个过程无非是把旧链表的 `head` 取下来,添加的新链表头部。代码怎么写呢? - -```java -next = head -> next; //保存 head 的 next , 以防取下 head 后丢失 -head -> next = pre; //将 head 从原链表取下来,添加到新链表上 -pre = head;// pre 右移 -head = next; // head 右移 -``` - -接下来就是停止条件了,我们再进行一次循环。 - -![](http://windliang.oss-cn-beijing.aliyuncs.com/l6.jpg) - -可以发现当 `head` 或者 `next` 指向 `null` 的时候,我们就可以停止了。此时将 `pre` 返回,便是逆序了的链表了。 - -```java -public ListNode reverseList(ListNode head) { - if (head == null) - return null; - ListNode pre = null; - ListNode next; - while (head != null) { - next = head.next; - head.next = pre; - pre = head; - head = next; - } - return pre; -} -``` - -# 解法二递归 - -- 首先假设我们实现了将单链表逆序的函数,`ListNode reverseListRecursion(ListNode head)` ,传入链表头,返回逆序后的链表头。 - -- 接着我们确定如何把问题一步一步的化小,我们可以这样想。 - - 把 `head` 结点拿出来,剩下的部分我们调用函数 `reverseListRecursion` ,这样剩下的部分就逆序了,接着我们把 `head` 结点放到新链表的尾部就可以了。这就是整个递归的思想了。 - - - - ![](http://windliang.oss-cn-beijing.aliyuncs.com/ll0.jpg) - - - head 结点拿出来 - - ![](http://windliang.oss-cn-beijing.aliyuncs.com/ll1.jpg) - - - 剩余部分调用逆序函数 `reverseListRecursion` ,并得到了 `newhead` - - ![](http://windliang.oss-cn-beijing.aliyuncs.com/ll2.jpg) - - - 将 2 指向 1 ,1 指向 `null`,将 `newhead` 返回即可。 - - ![](http://windliang.oss-cn-beijing.aliyuncs.com/ll3.jpg) - -- 找到递归出口 - - 当然就是如果结点的个数是一个,那么逆序的话还是它本身,直接 return 就够了。怎么判断结点个数是不是一个呢?它的 `next` 等于 `null` 就说明是一个了。但如果传进来的本身就是 `null`,那么直接找它的 `next` 会报错,所以先判断传进来的是不是 `null` ,如果是,也是直接返回就可以了。 - -```java -public ListNode reverseList(ListNode head) { - ListNode newHead; - if (head == null || head.next == null) { - return head; - } - newHead = reverseList(head.next); // head.next 作为剩余部分的头指针 - // head.next 代表新链表的尾,将它的 next 置为 head,就是将 head 加到末尾了。 - head.next.next = head; - head.next = null; - return newHead; -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/206.jpg) + +单链表倒置。 + +之前在 [第 2 题](https://leetcode.wang/leetCode-2-Add-Two-Numbers.html) 大数相加的时候已经分享过了,这里直接贴过来。 + +# 解法一迭代 + +首先看一下原链表。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/l0.jpg) + +总共需要添加两个指针,`pre` 和 `next`。 + +初始化 `pre` 指向 `NULL` 。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/l00.jpg) + +然后就是迭代的步骤,总共四步,顺序一步都不能错。 + +- `next` 指向 `head` 的 `next` ,防止原链表丢失 + + ![](http://windliang.oss-cn-beijing.aliyuncs.com/l1.jpg) + +- `head` 的 `next` 从原来链表脱离,指向 `pre` 。 + + ![](http://windliang.oss-cn-beijing.aliyuncs.com/l2.jpg) + +- `pre` 指向 `head` + + ![](http://windliang.oss-cn-beijing.aliyuncs.com/l3.jpg) + +- `head` 指向 `next` + + ![](http://windliang.oss-cn-beijing.aliyuncs.com/l4.jpg) + +一次迭代就完成了,如果再进行一次迭代就变成下边的样子。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/l5.jpg) + +可以看到整个过程无非是把旧链表的 `head` 取下来,添加的新链表头部。代码怎么写呢? + +```java +next = head -> next; //保存 head 的 next , 以防取下 head 后丢失 +head -> next = pre; //将 head 从原链表取下来,添加到新链表上 +pre = head;// pre 右移 +head = next; // head 右移 +``` + +接下来就是停止条件了,我们再进行一次循环。 + +![](http://windliang.oss-cn-beijing.aliyuncs.com/l6.jpg) + +可以发现当 `head` 或者 `next` 指向 `null` 的时候,我们就可以停止了。此时将 `pre` 返回,便是逆序了的链表了。 + +```java +public ListNode reverseList(ListNode head) { + if (head == null) + return null; + ListNode pre = null; + ListNode next; + while (head != null) { + next = head.next; + head.next = pre; + pre = head; + head = next; + } + return pre; +} +``` + +# 解法二递归 + +- 首先假设我们实现了将单链表逆序的函数,`ListNode reverseListRecursion(ListNode head)` ,传入链表头,返回逆序后的链表头。 + +- 接着我们确定如何把问题一步一步的化小,我们可以这样想。 + + 把 `head` 结点拿出来,剩下的部分我们调用函数 `reverseListRecursion` ,这样剩下的部分就逆序了,接着我们把 `head` 结点放到新链表的尾部就可以了。这就是整个递归的思想了。 + + + + ![](http://windliang.oss-cn-beijing.aliyuncs.com/ll0.jpg) + + - head 结点拿出来 + + ![](http://windliang.oss-cn-beijing.aliyuncs.com/ll1.jpg) + + - 剩余部分调用逆序函数 `reverseListRecursion` ,并得到了 `newhead` + + ![](http://windliang.oss-cn-beijing.aliyuncs.com/ll2.jpg) + + - 将 2 指向 1 ,1 指向 `null`,将 `newhead` 返回即可。 + + ![](http://windliang.oss-cn-beijing.aliyuncs.com/ll3.jpg) + +- 找到递归出口 + + 当然就是如果结点的个数是一个,那么逆序的话还是它本身,直接 return 就够了。怎么判断结点个数是不是一个呢?它的 `next` 等于 `null` 就说明是一个了。但如果传进来的本身就是 `null`,那么直接找它的 `next` 会报错,所以先判断传进来的是不是 `null` ,如果是,也是直接返回就可以了。 + +```java +public ListNode reverseList(ListNode head) { + ListNode newHead; + if (head == null || head.next == null) { + return head; + } + newHead = reverseList(head.next); // head.next 作为剩余部分的头指针 + // head.next 代表新链表的尾,将它的 next 置为 head,就是将 head 加到末尾了。 + head.next.next = head; + head.next = null; + return newHead; +} +``` + +# 总 + 关于链表的题,记住更改指向的时候要保存之前的节点,不然会丢失节点。 \ No newline at end of file diff --git a/leetcode-207-Course-Schedule.md b/leetcode-207-Course-Schedule.md index f19a032bc..25ef07a77 100644 --- a/leetcode-207-Course-Schedule.md +++ b/leetcode-207-Course-Schedule.md @@ -1,192 +1,192 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/207.jpg) - -给定 `n` 组先修课的关系,`[m,n]` 代表在上 `m` 这门课之前必须先上 `n` 这门课。输出能否成功上完所有课。 - -# 解法一 - -把所有的关系可以看做图的边,所有的边构成了一个有向图。 - -对于`[[1,3],[1,4],[2,4],[3,5],[3,6],[4,6]]` 就可以看做下边的图,箭头指向的是需要先上的课。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/207_2.jpg) - -想法很简单,要想上完所有的课,一定会有一些课没有先修课,比如上图的 `5`、`6`。然后我们可以把 `5` 和 `6` 节点删去。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/207_3.jpg) - -然后 `3` 和 `4` 就可以上了,同样的道理再把 `3` 和 `4` 删去。 - -接下来就可以去学 `1` 和 `2` 了。因此可以完成所有的课。 - -代码的话,用邻接表表示图。此外,我们不需要真的去删除节点,我们可以用 `outNum` 变量记录所有节点的先修课门数。当删除一个节点的时候,就将相应节点的先修课个数减一即可。 - -最后只需要判断所有的节点的先修课门数是否全部是 `0` 即可。 - -```java -public boolean canFinish(int numCourses, int[][] prerequisites) { - //保存每个节点的先修课个数,也就是出度 - HashMap outNum = new HashMap<>(); - //保存以 key 为先修课的列表,也就是入度的节点 - HashMap> inNodes = new HashMap<>(); - //保存所有节点 - HashSet set = new HashSet<>(); - int rows = prerequisites.length; - for (int i = 0; i < rows; i++) { - int key = prerequisites[i][0]; - int value = prerequisites[i][1]; - set.add(key); - set.add(value); - if (!outNum.containsKey(key)) { - outNum.put(key, 0); - } - if (!outNum.containsKey(value)) { - outNum.put(value, 0); - } - //当前节点先修课个数加一 - int num = outNum.get(key); - outNum.put(key, num + 1); - - if (!inNodes.containsKey(value)) { - inNodes.put(value, new ArrayList<>()); - } - //更新以 value 为先修课的列表 - ArrayList list = inNodes.get(value); - list.add(key); - } - - //将当前先修课个数为 0 的课加入到队列中 - Queue queue = new LinkedList<>(); - for (int k : set) { - if (outNum.get(k) == 0) { - queue.offer(k); - } - } - while (!queue.isEmpty()) { - //队列拿出来的课代表要删除的节点 - //要删除的节点的 list 中所有课的先修课个数减一 - int v = queue.poll(); - ArrayList list = inNodes.getOrDefault(v, new ArrayList<>()); - - for (int k : list) { - int num = outNum.get(k); - //当前课的先修课要变成 0, 加入队列 - if (num == 1) { - queue.offer(k); - } - //当前课的先修课个数减一 - outNum.put(k, num - 1); - } - } - - //判断所有课的先修课的个数是否为 0 - for (int k : set) { - if (outNum.get(k) != 0) { - return false; - } - } - return true; -} -``` - -# 解法二 - -还有另一种思路,我们只需要一门课一门课的判断。 - -从某门课开始遍历,我们通过 `DFS` 一条路径一条路径的判断,保证过程中没有遇到环。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/207_2.jpg) - -深度优先遍历 `1`,相当于 `3` 条路径 - -`1 -> 3 -> 5`,`1 -> 3 -> 6`,`1 -> 4 -> 6`。 - -深度优先遍历 `2`,相当于 `1` 条路径 - -`2 -> 4 -> 6`。 - -深度优先遍历 `3`,相当于 `2` 条路径 - -`3 -> 5`,`3 -> 6`。 - -深度优先遍历 `4`,相当于 `1` 条路径 - -`4 -> 6`。 - -深度优先遍历 `5`,相当于 `1` 条路径 - -`5`。 - -深度优先遍历 `6`,相当于 `1` 条路径 - -`6`。 - -什么情况下不能完成所有课程呢?某条路径出现了环,如下图。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/207_4.jpg) - -出现了 `1 -> 3 -> 6 -> 3`。所以不能学完所有课程。 - -代码的话,用邻接表表示图。通过递归实现 `DFS` ,用 `visited` 存储当前路径上的节点。 - -同时用 `visitedFinish` 表示可以学完的课程,起到优化算法的作用。 - -```java -public boolean canFinish(int numCourses, int[][] prerequisites) { - HashMap> outNodes = new HashMap<>(); - HashSet set = new HashSet<>(); - int rows = prerequisites.length; - for (int i = 0; i < rows; i++) { - int key = prerequisites[i][0]; - int value = prerequisites[i][1]; - set.add(key); - if (!outNodes.containsKey(key)) { - outNodes.put(key, new ArrayList<>()); - } - //存储当前节点的所有先修课程 - ArrayList list = outNodes.get(key); - list.add(value); - } - - HashSet visitedFinish = new HashSet<>(); - //判断每一门课 - for (int k : set) { - if (!dfs(k, outNodes, new HashSet<>(), visitedFinish)) { - return false; - } - visitedFinish.add(k); - } - return true; -} - -private boolean dfs(int start, HashMap> outNodes, HashSet visited, - HashSet visitedFinish) { - //已经处理过或者到了叶子节点 - if (visitedFinish.contains(start) || !outNodes.containsKey(start)) { - return true; - } - //出现了环 - if (visited.contains(start)) { - return false; - } - //将当前节点加入路径 - visited.add(start); - ArrayList list = outNodes.get(start); - for (int k : list) { - if(!dfs(k, outNodes, visited, visitedFinish)){ - return false; - } - } - visited.remove(start); - return true; -} -``` - -# 总 - -这道题本质上其实就是图的遍历。解法一是 `BFS` ,解法二是 `DFS` 。 - -解法一其实就是图的拓扑排序,解法二是判断图中是否有环的方法。 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/207.jpg) + +给定 `n` 组先修课的关系,`[m,n]` 代表在上 `m` 这门课之前必须先上 `n` 这门课。输出能否成功上完所有课。 + +# 解法一 + +把所有的关系可以看做图的边,所有的边构成了一个有向图。 + +对于`[[1,3],[1,4],[2,4],[3,5],[3,6],[4,6]]` 就可以看做下边的图,箭头指向的是需要先上的课。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/207_2.jpg) + +想法很简单,要想上完所有的课,一定会有一些课没有先修课,比如上图的 `5`、`6`。然后我们可以把 `5` 和 `6` 节点删去。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/207_3.jpg) + +然后 `3` 和 `4` 就可以上了,同样的道理再把 `3` 和 `4` 删去。 + +接下来就可以去学 `1` 和 `2` 了。因此可以完成所有的课。 + +代码的话,用邻接表表示图。此外,我们不需要真的去删除节点,我们可以用 `outNum` 变量记录所有节点的先修课门数。当删除一个节点的时候,就将相应节点的先修课个数减一即可。 + +最后只需要判断所有的节点的先修课门数是否全部是 `0` 即可。 + +```java +public boolean canFinish(int numCourses, int[][] prerequisites) { + //保存每个节点的先修课个数,也就是出度 + HashMap outNum = new HashMap<>(); + //保存以 key 为先修课的列表,也就是入度的节点 + HashMap> inNodes = new HashMap<>(); + //保存所有节点 + HashSet set = new HashSet<>(); + int rows = prerequisites.length; + for (int i = 0; i < rows; i++) { + int key = prerequisites[i][0]; + int value = prerequisites[i][1]; + set.add(key); + set.add(value); + if (!outNum.containsKey(key)) { + outNum.put(key, 0); + } + if (!outNum.containsKey(value)) { + outNum.put(value, 0); + } + //当前节点先修课个数加一 + int num = outNum.get(key); + outNum.put(key, num + 1); + + if (!inNodes.containsKey(value)) { + inNodes.put(value, new ArrayList<>()); + } + //更新以 value 为先修课的列表 + ArrayList list = inNodes.get(value); + list.add(key); + } + + //将当前先修课个数为 0 的课加入到队列中 + Queue queue = new LinkedList<>(); + for (int k : set) { + if (outNum.get(k) == 0) { + queue.offer(k); + } + } + while (!queue.isEmpty()) { + //队列拿出来的课代表要删除的节点 + //要删除的节点的 list 中所有课的先修课个数减一 + int v = queue.poll(); + ArrayList list = inNodes.getOrDefault(v, new ArrayList<>()); + + for (int k : list) { + int num = outNum.get(k); + //当前课的先修课要变成 0, 加入队列 + if (num == 1) { + queue.offer(k); + } + //当前课的先修课个数减一 + outNum.put(k, num - 1); + } + } + + //判断所有课的先修课的个数是否为 0 + for (int k : set) { + if (outNum.get(k) != 0) { + return false; + } + } + return true; +} +``` + +# 解法二 + +还有另一种思路,我们只需要一门课一门课的判断。 + +从某门课开始遍历,我们通过 `DFS` 一条路径一条路径的判断,保证过程中没有遇到环。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/207_2.jpg) + +深度优先遍历 `1`,相当于 `3` 条路径 + +`1 -> 3 -> 5`,`1 -> 3 -> 6`,`1 -> 4 -> 6`。 + +深度优先遍历 `2`,相当于 `1` 条路径 + +`2 -> 4 -> 6`。 + +深度优先遍历 `3`,相当于 `2` 条路径 + +`3 -> 5`,`3 -> 6`。 + +深度优先遍历 `4`,相当于 `1` 条路径 + +`4 -> 6`。 + +深度优先遍历 `5`,相当于 `1` 条路径 + +`5`。 + +深度优先遍历 `6`,相当于 `1` 条路径 + +`6`。 + +什么情况下不能完成所有课程呢?某条路径出现了环,如下图。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/207_4.jpg) + +出现了 `1 -> 3 -> 6 -> 3`。所以不能学完所有课程。 + +代码的话,用邻接表表示图。通过递归实现 `DFS` ,用 `visited` 存储当前路径上的节点。 + +同时用 `visitedFinish` 表示可以学完的课程,起到优化算法的作用。 + +```java +public boolean canFinish(int numCourses, int[][] prerequisites) { + HashMap> outNodes = new HashMap<>(); + HashSet set = new HashSet<>(); + int rows = prerequisites.length; + for (int i = 0; i < rows; i++) { + int key = prerequisites[i][0]; + int value = prerequisites[i][1]; + set.add(key); + if (!outNodes.containsKey(key)) { + outNodes.put(key, new ArrayList<>()); + } + //存储当前节点的所有先修课程 + ArrayList list = outNodes.get(key); + list.add(value); + } + + HashSet visitedFinish = new HashSet<>(); + //判断每一门课 + for (int k : set) { + if (!dfs(k, outNodes, new HashSet<>(), visitedFinish)) { + return false; + } + visitedFinish.add(k); + } + return true; +} + +private boolean dfs(int start, HashMap> outNodes, HashSet visited, + HashSet visitedFinish) { + //已经处理过或者到了叶子节点 + if (visitedFinish.contains(start) || !outNodes.containsKey(start)) { + return true; + } + //出现了环 + if (visited.contains(start)) { + return false; + } + //将当前节点加入路径 + visited.add(start); + ArrayList list = outNodes.get(start); + for (int k : list) { + if(!dfs(k, outNodes, visited, visitedFinish)){ + return false; + } + } + visited.remove(start); + return true; +} +``` + +# 总 + +这道题本质上其实就是图的遍历。解法一是 `BFS` ,解法二是 `DFS` 。 + +解法一其实就是图的拓扑排序,解法二是判断图中是否有环的方法。 + 另外,图在代码中有两种表示形式,邻接表和邻接矩阵,上边的解法都采用的是邻接表。 \ No newline at end of file diff --git a/leetcode-208-Implement-Trie-Prefix-Tree.md b/leetcode-208-Implement-Trie-Prefix-Tree.md index 3d62668bb..cd0182b6c 100644 --- a/leetcode-208-Implement-Trie-Prefix-Tree.md +++ b/leetcode-208-Implement-Trie-Prefix-Tree.md @@ -1,131 +1,131 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/208.jpg) - -实现 `Trie` 数,即前缀树。`trie`的发明者 Edward Fredkin 把它读作 `/ˈtriː/ "tree"`。但是,其他作者把它读作`/traɪ/"try"`。 - -# 解法一 - -算作一个高级的数据结构,实现方式可以通过 `26` 叉树。每个节点存一个字母,根节点不存字母。 - -```java -"app" "as" "cat" "yes" "year" "you" - root - / | \ - a c y - / \ | / \ - p s a e o - / | / \ \ - p t s a u - | - r -上图中省略了 null 节点,对于第一层画完整了其实是下边的样子, 图示中用 0 代表 null - root - / | | | | | | | | | | | | | | | | | | | | | | | | \ - a 0 c 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 y 0 -其它层同理 -``` - -代码的话,我们定义一个节点,每个节点包含一个节点数组,代表 `26` 个孩子。此外,还需要一个 `flag` 变量来标记当前节点是否是某个单词的结束。 - -```java -class TrieNode { - TrieNode[] children; - boolean flag; - public TrieNode() { - children = new TrieNode[26]; - flag = false; - //节点初始化为 null - for (int i = 0; i < 26; i++) { - children[i] = null; - } - } -} -``` - -然后只需要实现题目中所需要的三个函数即可。其中 `children[0]` 存 `a`, `children[1]` 存 `b`, `children[2]` 存 `c`... 依次类推。所以存的时候我们用当前字符减去 `a` ,从而得到相应的 `children` 下标。 - -```java -class Trie { - class TrieNode { - TrieNode[] children; - boolean flag; - public TrieNode() { - children = new TrieNode[26]; - flag = false; - for (int i = 0; i < 26; i++) { - children[i] = null; - } - } - } - - TrieNode root; - - /** Initialize your data structure here. */ - public Trie() { - root = new TrieNode(); - } - - /** Inserts a word into the trie. */ - public void insert(String word) { - char[] array = word.toCharArray(); - TrieNode cur = root; - for (int i = 0; i < array.length; i++) { - //当前孩子是否存在 - if (cur.children[array[i] - 'a'] == null) { - cur.children[array[i] - 'a'] = new TrieNode(); - } - cur = cur.children[array[i] - 'a']; - } - //当前节点代表结束 - cur.flag = true; - } - - /** Returns if the word is in the trie. */ - public boolean search(String word) { - char[] array = word.toCharArray(); - TrieNode cur = root; - for (int i = 0; i < array.length; i++) { - //不含有当前节点 - if (cur.children[array[i] - 'a'] == null) { - return false; - } - cur = cur.children[array[i] - 'a']; - } - //当前节点是否为是某个单词的结束 - return cur.flag; - } - - /** - * Returns if there is any word in the trie that starts with the given - * prefix. - */ - public boolean startsWith(String prefix) { - char[] array = prefix.toCharArray(); - TrieNode cur = root; - for (int i = 0; i < array.length; i++) { - if (cur.children[array[i] - 'a'] == null) { - return false; - } - cur = cur.children[array[i] - 'a']; - } - return true; - } - -}; -``` - -# 总 - -只要知道每个节点存字母,路径代表单词,代码就很好写出来了。 - -前缀树适用于两个场景。 - -* 统计每个单词出现的次数,代码的话只需要将上边的 `flag` 改成 `int` 类型,然后每次插入的时候计数即可。 - - 当然,我们用 `HashMap` 也可以做到,`key` 是单词,`value` 存这个单词出现的次数即可。但缺点是,当单词很多很多的时候,受到 `Hash` 函数的影响,`hash` 值会经常出现冲突,算法可能退化为 `O(n)`,`n` 是 `key` 的总数。 - - 而对于前缀树,我们查找一个单词出现的次数,始终是 `O(m)`,`m` 为单词的长度。 - -* 查找某个前缀的单词,最常见的比如搜索引擎的提示、拼写检查、ip 路由等。 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/208.jpg) + +实现 `Trie` 数,即前缀树。`trie`的发明者 Edward Fredkin 把它读作 `/ˈtriː/ "tree"`。但是,其他作者把它读作`/traɪ/"try"`。 + +# 解法一 + +算作一个高级的数据结构,实现方式可以通过 `26` 叉树。每个节点存一个字母,根节点不存字母。 + +```java +"app" "as" "cat" "yes" "year" "you" + root + / | \ + a c y + / \ | / \ + p s a e o + / | / \ \ + p t s a u + | + r +上图中省略了 null 节点,对于第一层画完整了其实是下边的样子, 图示中用 0 代表 null + root + / | | | | | | | | | | | | | | | | | | | | | | | | \ + a 0 c 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 y 0 +其它层同理 +``` + +代码的话,我们定义一个节点,每个节点包含一个节点数组,代表 `26` 个孩子。此外,还需要一个 `flag` 变量来标记当前节点是否是某个单词的结束。 + +```java +class TrieNode { + TrieNode[] children; + boolean flag; + public TrieNode() { + children = new TrieNode[26]; + flag = false; + //节点初始化为 null + for (int i = 0; i < 26; i++) { + children[i] = null; + } + } +} +``` + +然后只需要实现题目中所需要的三个函数即可。其中 `children[0]` 存 `a`, `children[1]` 存 `b`, `children[2]` 存 `c`... 依次类推。所以存的时候我们用当前字符减去 `a` ,从而得到相应的 `children` 下标。 + +```java +class Trie { + class TrieNode { + TrieNode[] children; + boolean flag; + public TrieNode() { + children = new TrieNode[26]; + flag = false; + for (int i = 0; i < 26; i++) { + children[i] = null; + } + } + } + + TrieNode root; + + /** Initialize your data structure here. */ + public Trie() { + root = new TrieNode(); + } + + /** Inserts a word into the trie. */ + public void insert(String word) { + char[] array = word.toCharArray(); + TrieNode cur = root; + for (int i = 0; i < array.length; i++) { + //当前孩子是否存在 + if (cur.children[array[i] - 'a'] == null) { + cur.children[array[i] - 'a'] = new TrieNode(); + } + cur = cur.children[array[i] - 'a']; + } + //当前节点代表结束 + cur.flag = true; + } + + /** Returns if the word is in the trie. */ + public boolean search(String word) { + char[] array = word.toCharArray(); + TrieNode cur = root; + for (int i = 0; i < array.length; i++) { + //不含有当前节点 + if (cur.children[array[i] - 'a'] == null) { + return false; + } + cur = cur.children[array[i] - 'a']; + } + //当前节点是否为是某个单词的结束 + return cur.flag; + } + + /** + * Returns if there is any word in the trie that starts with the given + * prefix. + */ + public boolean startsWith(String prefix) { + char[] array = prefix.toCharArray(); + TrieNode cur = root; + for (int i = 0; i < array.length; i++) { + if (cur.children[array[i] - 'a'] == null) { + return false; + } + cur = cur.children[array[i] - 'a']; + } + return true; + } + +}; +``` + +# 总 + +只要知道每个节点存字母,路径代表单词,代码就很好写出来了。 + +前缀树适用于两个场景。 + +* 统计每个单词出现的次数,代码的话只需要将上边的 `flag` 改成 `int` 类型,然后每次插入的时候计数即可。 + + 当然,我们用 `HashMap` 也可以做到,`key` 是单词,`value` 存这个单词出现的次数即可。但缺点是,当单词很多很多的时候,受到 `Hash` 函数的影响,`hash` 值会经常出现冲突,算法可能退化为 `O(n)`,`n` 是 `key` 的总数。 + + 而对于前缀树,我们查找一个单词出现的次数,始终是 `O(m)`,`m` 为单词的长度。 + +* 查找某个前缀的单词,最常见的比如搜索引擎的提示、拼写检查、ip 路由等。 + diff --git a/leetcode-209-Minimum-Size-Subarray-Sum.md b/leetcode-209-Minimum-Size-Subarray-Sum.md index 33775dbd9..89d4a5483 100644 --- a/leetcode-209-Minimum-Size-Subarray-Sum.md +++ b/leetcode-209-Minimum-Size-Subarray-Sum.md @@ -1,368 +1,368 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/209.jpg) - -找出最小的连续子数组,使得子数组的和大于等于 `s`。 - -# 解法一 暴力破解 - -从第 `0` 个数字开始,依次添加数字,记录当总和大于等于 `s` 时的长度。 - -从第 `1` 个数字开始,依次添加数字,记录当总和大于等于 `s` 时的长度。 - -从第 `2` 个数字开始,依次添加数字,记录当总和大于等于 `s` 时的长度。 - -... - -从最后个数字开始,依次添加数字,记录当总和大于等于 `s` 时的长度。 - -从上边得到的长度中选择最小的即可。 - -```java -public int minSubArrayLen(int s, int[] nums) { - int min = Integer.MAX_VALUE; - int n = nums.length; - for (int i = 0; i < n; i++) { - int start = i; - int sum = 0; - while (start < n) { - sum += nums[start]; - start++; - //当前和大于等于 s 的时候结束 - if (sum >= s) { - min = Math.min(min, start - i); - break; - } - } - } - //min 是否更新,如果没有更新说明数组所有的数字和小于 s, 没有满足条件的解, 返回 0 - return min == Integer.MAX_VALUE ? 0 : min; -} -``` - -时间复杂度:`O(n²)`。 - -# 解法二 双指针 - -受到 [76 题](https://leetcode.wang/leetCode-76-Minimum-Window-Substring.html) Minimum Window Substring 的启示,找一个范围使得其值满足某个条件,然后就会想到滑动窗口,也就是用双指针的方法。和这道题本质是一样的。 - -用双指针 left 和 right 表示一个窗口。 - -1. right 向右移增大窗口,直到窗口内的数字和大于等于了 `s`。进行第 `2` 步。 -2. 记录此时的长度,left 向右移动,开始减少长度,每减少一次,就更新最小长度。直到当前窗口内的数字和小于了 `s`,回到第 1 步。 - -举个例子,模拟下滑动窗口的过程吧。 - -```java -s = 7, nums = [2,3,1,2,4,3] - -2 3 1 2 4 3 -^ -l -r -上边的窗口内所有数字的和 2 小于 7, r 右移 - -2 3 1 2 4 3 -^ ^ -l r -上边的窗口内所有数字的和 2 + 3 小于 7, r 右移 - -2 3 1 2 4 3 -^ ^ -l r -上边的窗口内所有数字的和 2 + 3 + 1 小于 7, r 右移 - -2 3 1 2 4 3 -^ ^ -l r -上边的窗口内所有数字的和 2 + 3 + 1 + 2 大于等于了 7, 记录此时的长度 min = 4, l 右移 - -2 3 1 2 4 3 - ^ ^ - l r -上边的窗口内所有数字的和 3 + 1 + 2 小于 7, r 右移 - -2 3 1 2 4 3 - ^ ^ - l r -上边的窗口内所有数字的和 3 + 1 + 2 + 4 大于等于了 7, 更新此时的长度 min = 4, l 右移 - -2 3 1 2 4 3 - ^ ^ - l r -上边的窗口内所有数字的和 1 + 2 + 4 大于等于了 7, 更新此时的长度 min = 3, l 右移 - -2 3 1 2 4 3 - ^ ^ - l r -上边的窗口内所有数字的和 2 + 4 小于 7, r 右移 - -2 3 1 2 4 3 - ^ ^ - l r -上边的窗口内所有数字的和 2 + 4 + 3 大于等于了 7, 更新此时的长度 min = 3, l 右移 - -2 3 1 2 4 3 - ^ ^ - l r -上边的窗口内所有数字的和 4 + 3 大于等于了 7, 更新此时的长度 min = 2, l 右移 - -2 3 1 2 4 3 - ^ - r - l -上边的窗口内所有数字的和 3 小于 7, r 右移,结束 -``` - -代码的话,只需要还原上边的过程即可。 - -```java -public int minSubArrayLen(int s, int[] nums) { - int n = nums.length; - if (n == 0) { - return 0; - } - int left = 0; - int right = 0; - int sum = 0; - int min = Integer.MAX_VALUE; - while (right < n) { - sum += nums[right]; - right++; - while (sum >= s) { - min = Math.min(min, right - left); - sum -= nums[left]; - left++; - } - } - return min == Integer.MAX_VALUE ? 0 : min; -} -``` - -时间复杂度:`O(n)`。 - -# 解法三 二分查找 - -正常想的话,到解法二按理说已经结束了,但题目里让提出一个 `O(nlog(n))` 的解法,这里自己也没想出来,分享下 [这里](https://leetcode.com/problems/minimum-size-subarray-sum/discuss/59123/O(N)O(NLogN)-solutions-both-O(1)-space) 的想法。 - -看到 `log` 就会想到二分查找,接着就会想到有序数组,最后,有序数组在哪里呢? - -定义一个新的数组,`sums[i]` ,代表从 `0` 到 `i` 的累积和,这样就得到了一个有序数组。 - -这样做有个好处,那就是通过 `sums` 数组,如果要求 `i` 到 `j` 的所有子数组的和的话,就等于 `sums[j] - sums[i - 1]`。也就是前 `j` 个数字的和减去前 `i - 1 ` 个数字的和。 - -然后求解这道题的话,算法和解法一的暴力破解还是一样的,也就是 - -求出从第 `0` 个数字开始,总和大于等于 `s` 时的长度。 - -求出从第 `1` 个数字开始,总和大于等于 `s` 时的长度。 - -求出从第 `2` 个数字开始,总和大于等于 `s` 时的长度。 - -... - -不同之处在于这里求总和时候,可以利用 `sums` 数组,不再需要累加了。 - -比如求从第 `i` 个数字开始,总和大于等于 `s` 时的长度,我们只需要找从第 `i + 1` 个数字到第几个数字的和大于等于 `s - nums[i]` 即可。求 `i + 1` 到 `j` 的所有数字的和的话,前边已经说明过了,也就是 `sums[j] - sums[i]`。 - -```java -public int minSubArrayLen(int s, int[] nums) { - int n = nums.length; - if (n == 0) { - return 0; - } - int[] sums = new int[n]; - sums[0] = nums[0]; - for (int i = 1; i < n; i++) { - sums[i] = nums[i] + sums[i - 1]; - } - int min = Integer.MAX_VALUE; - for (int i = 0; i < n; i++) { - int s2 = s - nums[i]; //除去当前数字 - for (int j = i; j < n; j++) { - //i + 1 到 j 的所有数字和 - if (sums[j] - sums[i] >= s2) { - min = Math.min(min, j - i + 1); - } - } - } - return min == Integer.MAX_VALUE ? 0 : min; -} -``` - -至于二分查找,我们只需要修改内层的 `for` 循环。对于 `sums[j] - sums[i] >= s2`,通过移项,也就是 `sums[j] >= s2 + sums[i] ` ,含义就是寻找一个 `sums[j]`,使得其刚好大于等于 `s2 + sums[i]`。因为 `sums` 是个有序数组,所有找 `sum[j]` 可以采取二分的方法。 - -```java -public int minSubArrayLen(int s, int[] nums) { - int n = nums.length; - if (n == 0) { - return 0; - } - int[] sums = new int[n]; - sums[0] = nums[0]; - for (int i = 1; i < n; i++) { - sums[i] = nums[i] + sums[i - 1]; - } - int min = Integer.MAX_VALUE; - for (int i = 0; i < n; i++) { - int s2 = s - nums[i]; - //二分查找,目标值是 s2 + sums[i] - int k = binarySearch(i, n - 1, sums, s2 + sums[i]); - if (k != -1) { - min = Math.min(min, k - i + 1); - } - - } - return min == Integer.MAX_VALUE ? 0 : min; -} - -//寻求刚好大于 target 的 sums 的下标,也就是大于等于 target 所有 sums 中最小的那个 -private int binarySearch(int start, int end, int[] sums, int target) { - int mid = -1; - while (start <= end) { - mid = (start + end) >>> 1; - if (sums[mid] == target) { - return mid; - } else if (sums[mid] < target) { - start = mid + 1; - } else { - end = mid - 1; - } - } - //是否找到,没有找到返回 -1 - return sums[mid] > target ? mid : -1; -} -``` - -时间复杂度:`O(nlog(n))`。 - -`2020.5.30` 更新,感谢 @Yolo 指出,上边的代码虽然 `AC` 了,但二分查找是有瑕疵的。 - -当 `sums[mid] > target` 我们不能直接更新 `end = mid - 1` ,因为此时的 `mid` 可能是我们要找的解,所以应该改成 `end = mid` 。 - -自己构造了一个反例 - -```java -59 -[10, 50, 5] -``` - -这个 `case` 在 `leetcode` 是过不了的。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/209_2.jpg) - -代码改成下边的样子就可以了。 - -```java -public int minSubArrayLen(int s, int[] nums) { - int n = nums.length; - if (n == 0) { - return 0; - } - int[] sums = new int[n]; - sums[0] = nums[0]; - for (int i = 1; i < n; i++) { - sums[i] = nums[i] + sums[i - 1]; - } - int min = Integer.MAX_VALUE; - for (int i = 0; i < n; i++) { - int s2 = s - nums[i]; - //二分查找,目标值是 s2 + sums[i] - int k = binarySearch(i, n - 1, sums, s2 + sums[i]); - if (k != -1) { - min = Math.min(min, k - i + 1); - } - - } - return min == Integer.MAX_VALUE ? 0 : min; -} - -//寻求刚好大于 target 的 sums 的下标,也就是大于等于 target 所有 sums 中最小的那个 -private int binarySearch(int start, int end, int[] sums, int target) { - int mid = -1; - while (start < end) { - mid = (start + end) >>> 1; - if (sums[mid] >= target) { - end = mid; - } else { - start = mid + 1; - } - } - //是否找到,没有找到返回 -1 - return sums[start] >= target ? start : -1; -} -``` - -上边还要注意的一点是,我们要找的是大于等于 `target` 中**最小**的下标,所以当 `sums[mid] == target` 的时候不能立刻停止,需要继续查找。 - -当然这里其实不用管,因为这里的数组是累和数组,并且都是整数,是严格递增的,当找到相等的时候,前一个一定是小于 `target` 的。 - -# 解法四 二分查找 - -解法三的二分查找的关键在于 `sums` 数组的定义,一般情况下也不会往那方面想。还是在 [这里](https://leetcode.com/problems/minimum-size-subarray-sum/discuss/59123/O(N)O(NLogN)-solutions-both-O(1)-space) 看到的解法,另外一种二分的思路,蛮有意思,分享一下。 - -题目中,我们要寻找连续的数字和大于等于 `s` 的最小长度。那么,我们可以对这个长度采取二分的方法去寻找吗? - -答案是肯定的,原因就是长度为 `1` 的所有连续数字中最大的和、长度为 `2` 的所有连续数字中最大的和、长度为 `3` 的所有连续数字中最大的和 ... 长度为 `n` 的所有连续数字中最大的和,同样是一个升序数组。 - -算法的话就是对长度进行二分,寻求满足条件的最小长度。 - -对于长度为 `n` 的数组,我们先去判断长度为 `n/2` 的连续数字中最大的和是否大于等于 `s`。 - -* 如果大于等于 `s` ,那么我们需要减少长度,继续判断所有长度为 `n/4` 的连续数字 -* 如果小于 `s`,我们需要增加长度,我们继续判断所有长度为 `(n/2 + n) / 2`,也就是 `3n/4` 的连续数字。 - -可以再结合下边的代码看一下。 - -```java -public int minSubArrayLen(int s, int[] nums) { - int n = nums.length; - if (n == 0) { - return 0; - } - int minLen = 0, maxLen = n; - int midLen; - int min = -1; - while (minLen <= maxLen) { - //取中间的长度 - midLen = (minLen + maxLen) >>> 1; - //判断当前长度的最大和是否大于等于 s - if (getMaxSum(midLen, nums) >= s) { - maxLen = midLen - 1; //减小长度 - min = midLen; //更新最小值 - } else { - minLen = midLen + 1; //增大长度 - } - } - return min == -1 ? 0 : min; -} - -private int getMaxSum(int len, int[] nums) { - int n = nums.length; - int sum = 0; - int maxSum = 0; - // 达到长度 - for (int i = 0; i < len; i++) { - sum += nums[i]; - } - maxSum = sum; // 初始化 maxSum - - for (int i = len; i < n; i++) { - // 加一个数字减一个数字,保持长度不变 - sum += nums[i]; - sum = sum - nums[i - len]; - // 更新 maxSum - maxSum = Math.max(maxSum, sum); - } - return maxSum; -} -``` - -时间复杂度:`O(nlog(n))`。 - -# 总 - -这道题的话,通过前边刷题的经验,第一反应就想到了解法二中的双指针。如果不知道双指针的话,应该会想到解法一的暴力破解。 - -但对于二分查找的解法三和解法四,还是比较难想到的。不过回味完,其实二分查找的关键就是那个递增的有序数列,从而可以每次抛弃一半的可选解。 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/209.jpg) + +找出最小的连续子数组,使得子数组的和大于等于 `s`。 + +# 解法一 暴力破解 + +从第 `0` 个数字开始,依次添加数字,记录当总和大于等于 `s` 时的长度。 + +从第 `1` 个数字开始,依次添加数字,记录当总和大于等于 `s` 时的长度。 + +从第 `2` 个数字开始,依次添加数字,记录当总和大于等于 `s` 时的长度。 + +... + +从最后个数字开始,依次添加数字,记录当总和大于等于 `s` 时的长度。 + +从上边得到的长度中选择最小的即可。 + +```java +public int minSubArrayLen(int s, int[] nums) { + int min = Integer.MAX_VALUE; + int n = nums.length; + for (int i = 0; i < n; i++) { + int start = i; + int sum = 0; + while (start < n) { + sum += nums[start]; + start++; + //当前和大于等于 s 的时候结束 + if (sum >= s) { + min = Math.min(min, start - i); + break; + } + } + } + //min 是否更新,如果没有更新说明数组所有的数字和小于 s, 没有满足条件的解, 返回 0 + return min == Integer.MAX_VALUE ? 0 : min; +} +``` + +时间复杂度:`O(n²)`。 + +# 解法二 双指针 + +受到 [76 题](https://leetcode.wang/leetCode-76-Minimum-Window-Substring.html) Minimum Window Substring 的启示,找一个范围使得其值满足某个条件,然后就会想到滑动窗口,也就是用双指针的方法。和这道题本质是一样的。 + +用双指针 left 和 right 表示一个窗口。 + +1. right 向右移增大窗口,直到窗口内的数字和大于等于了 `s`。进行第 `2` 步。 +2. 记录此时的长度,left 向右移动,开始减少长度,每减少一次,就更新最小长度。直到当前窗口内的数字和小于了 `s`,回到第 1 步。 + +举个例子,模拟下滑动窗口的过程吧。 + +```java +s = 7, nums = [2,3,1,2,4,3] + +2 3 1 2 4 3 +^ +l +r +上边的窗口内所有数字的和 2 小于 7, r 右移 + +2 3 1 2 4 3 +^ ^ +l r +上边的窗口内所有数字的和 2 + 3 小于 7, r 右移 + +2 3 1 2 4 3 +^ ^ +l r +上边的窗口内所有数字的和 2 + 3 + 1 小于 7, r 右移 + +2 3 1 2 4 3 +^ ^ +l r +上边的窗口内所有数字的和 2 + 3 + 1 + 2 大于等于了 7, 记录此时的长度 min = 4, l 右移 + +2 3 1 2 4 3 + ^ ^ + l r +上边的窗口内所有数字的和 3 + 1 + 2 小于 7, r 右移 + +2 3 1 2 4 3 + ^ ^ + l r +上边的窗口内所有数字的和 3 + 1 + 2 + 4 大于等于了 7, 更新此时的长度 min = 4, l 右移 + +2 3 1 2 4 3 + ^ ^ + l r +上边的窗口内所有数字的和 1 + 2 + 4 大于等于了 7, 更新此时的长度 min = 3, l 右移 + +2 3 1 2 4 3 + ^ ^ + l r +上边的窗口内所有数字的和 2 + 4 小于 7, r 右移 + +2 3 1 2 4 3 + ^ ^ + l r +上边的窗口内所有数字的和 2 + 4 + 3 大于等于了 7, 更新此时的长度 min = 3, l 右移 + +2 3 1 2 4 3 + ^ ^ + l r +上边的窗口内所有数字的和 4 + 3 大于等于了 7, 更新此时的长度 min = 2, l 右移 + +2 3 1 2 4 3 + ^ + r + l +上边的窗口内所有数字的和 3 小于 7, r 右移,结束 +``` + +代码的话,只需要还原上边的过程即可。 + +```java +public int minSubArrayLen(int s, int[] nums) { + int n = nums.length; + if (n == 0) { + return 0; + } + int left = 0; + int right = 0; + int sum = 0; + int min = Integer.MAX_VALUE; + while (right < n) { + sum += nums[right]; + right++; + while (sum >= s) { + min = Math.min(min, right - left); + sum -= nums[left]; + left++; + } + } + return min == Integer.MAX_VALUE ? 0 : min; +} +``` + +时间复杂度:`O(n)`。 + +# 解法三 二分查找 + +正常想的话,到解法二按理说已经结束了,但题目里让提出一个 `O(nlog(n))` 的解法,这里自己也没想出来,分享下 [这里](https://leetcode.com/problems/minimum-size-subarray-sum/discuss/59123/O(N)O(NLogN)-solutions-both-O(1)-space) 的想法。 + +看到 `log` 就会想到二分查找,接着就会想到有序数组,最后,有序数组在哪里呢? + +定义一个新的数组,`sums[i]` ,代表从 `0` 到 `i` 的累积和,这样就得到了一个有序数组。 + +这样做有个好处,那就是通过 `sums` 数组,如果要求 `i` 到 `j` 的所有子数组的和的话,就等于 `sums[j] - sums[i - 1]`。也就是前 `j` 个数字的和减去前 `i - 1 ` 个数字的和。 + +然后求解这道题的话,算法和解法一的暴力破解还是一样的,也就是 + +求出从第 `0` 个数字开始,总和大于等于 `s` 时的长度。 + +求出从第 `1` 个数字开始,总和大于等于 `s` 时的长度。 + +求出从第 `2` 个数字开始,总和大于等于 `s` 时的长度。 + +... + +不同之处在于这里求总和时候,可以利用 `sums` 数组,不再需要累加了。 + +比如求从第 `i` 个数字开始,总和大于等于 `s` 时的长度,我们只需要找从第 `i + 1` 个数字到第几个数字的和大于等于 `s - nums[i]` 即可。求 `i + 1` 到 `j` 的所有数字的和的话,前边已经说明过了,也就是 `sums[j] - sums[i]`。 + +```java +public int minSubArrayLen(int s, int[] nums) { + int n = nums.length; + if (n == 0) { + return 0; + } + int[] sums = new int[n]; + sums[0] = nums[0]; + for (int i = 1; i < n; i++) { + sums[i] = nums[i] + sums[i - 1]; + } + int min = Integer.MAX_VALUE; + for (int i = 0; i < n; i++) { + int s2 = s - nums[i]; //除去当前数字 + for (int j = i; j < n; j++) { + //i + 1 到 j 的所有数字和 + if (sums[j] - sums[i] >= s2) { + min = Math.min(min, j - i + 1); + } + } + } + return min == Integer.MAX_VALUE ? 0 : min; +} +``` + +至于二分查找,我们只需要修改内层的 `for` 循环。对于 `sums[j] - sums[i] >= s2`,通过移项,也就是 `sums[j] >= s2 + sums[i] ` ,含义就是寻找一个 `sums[j]`,使得其刚好大于等于 `s2 + sums[i]`。因为 `sums` 是个有序数组,所有找 `sum[j]` 可以采取二分的方法。 + +```java +public int minSubArrayLen(int s, int[] nums) { + int n = nums.length; + if (n == 0) { + return 0; + } + int[] sums = new int[n]; + sums[0] = nums[0]; + for (int i = 1; i < n; i++) { + sums[i] = nums[i] + sums[i - 1]; + } + int min = Integer.MAX_VALUE; + for (int i = 0; i < n; i++) { + int s2 = s - nums[i]; + //二分查找,目标值是 s2 + sums[i] + int k = binarySearch(i, n - 1, sums, s2 + sums[i]); + if (k != -1) { + min = Math.min(min, k - i + 1); + } + + } + return min == Integer.MAX_VALUE ? 0 : min; +} + +//寻求刚好大于 target 的 sums 的下标,也就是大于等于 target 所有 sums 中最小的那个 +private int binarySearch(int start, int end, int[] sums, int target) { + int mid = -1; + while (start <= end) { + mid = (start + end) >>> 1; + if (sums[mid] == target) { + return mid; + } else if (sums[mid] < target) { + start = mid + 1; + } else { + end = mid - 1; + } + } + //是否找到,没有找到返回 -1 + return sums[mid] > target ? mid : -1; +} +``` + +时间复杂度:`O(nlog(n))`。 + +`2020.5.30` 更新,感谢 @Yolo 指出,上边的代码虽然 `AC` 了,但二分查找是有瑕疵的。 + +当 `sums[mid] > target` 我们不能直接更新 `end = mid - 1` ,因为此时的 `mid` 可能是我们要找的解,所以应该改成 `end = mid` 。 + +自己构造了一个反例 + +```java +59 +[10, 50, 5] +``` + +这个 `case` 在 `leetcode` 是过不了的。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/209_2.jpg) + +代码改成下边的样子就可以了。 + +```java +public int minSubArrayLen(int s, int[] nums) { + int n = nums.length; + if (n == 0) { + return 0; + } + int[] sums = new int[n]; + sums[0] = nums[0]; + for (int i = 1; i < n; i++) { + sums[i] = nums[i] + sums[i - 1]; + } + int min = Integer.MAX_VALUE; + for (int i = 0; i < n; i++) { + int s2 = s - nums[i]; + //二分查找,目标值是 s2 + sums[i] + int k = binarySearch(i, n - 1, sums, s2 + sums[i]); + if (k != -1) { + min = Math.min(min, k - i + 1); + } + + } + return min == Integer.MAX_VALUE ? 0 : min; +} + +//寻求刚好大于 target 的 sums 的下标,也就是大于等于 target 所有 sums 中最小的那个 +private int binarySearch(int start, int end, int[] sums, int target) { + int mid = -1; + while (start < end) { + mid = (start + end) >>> 1; + if (sums[mid] >= target) { + end = mid; + } else { + start = mid + 1; + } + } + //是否找到,没有找到返回 -1 + return sums[start] >= target ? start : -1; +} +``` + +上边还要注意的一点是,我们要找的是大于等于 `target` 中**最小**的下标,所以当 `sums[mid] == target` 的时候不能立刻停止,需要继续查找。 + +当然这里其实不用管,因为这里的数组是累和数组,并且都是整数,是严格递增的,当找到相等的时候,前一个一定是小于 `target` 的。 + +# 解法四 二分查找 + +解法三的二分查找的关键在于 `sums` 数组的定义,一般情况下也不会往那方面想。还是在 [这里](https://leetcode.com/problems/minimum-size-subarray-sum/discuss/59123/O(N)O(NLogN)-solutions-both-O(1)-space) 看到的解法,另外一种二分的思路,蛮有意思,分享一下。 + +题目中,我们要寻找连续的数字和大于等于 `s` 的最小长度。那么,我们可以对这个长度采取二分的方法去寻找吗? + +答案是肯定的,原因就是长度为 `1` 的所有连续数字中最大的和、长度为 `2` 的所有连续数字中最大的和、长度为 `3` 的所有连续数字中最大的和 ... 长度为 `n` 的所有连续数字中最大的和,同样是一个升序数组。 + +算法的话就是对长度进行二分,寻求满足条件的最小长度。 + +对于长度为 `n` 的数组,我们先去判断长度为 `n/2` 的连续数字中最大的和是否大于等于 `s`。 + +* 如果大于等于 `s` ,那么我们需要减少长度,继续判断所有长度为 `n/4` 的连续数字 +* 如果小于 `s`,我们需要增加长度,我们继续判断所有长度为 `(n/2 + n) / 2`,也就是 `3n/4` 的连续数字。 + +可以再结合下边的代码看一下。 + +```java +public int minSubArrayLen(int s, int[] nums) { + int n = nums.length; + if (n == 0) { + return 0; + } + int minLen = 0, maxLen = n; + int midLen; + int min = -1; + while (minLen <= maxLen) { + //取中间的长度 + midLen = (minLen + maxLen) >>> 1; + //判断当前长度的最大和是否大于等于 s + if (getMaxSum(midLen, nums) >= s) { + maxLen = midLen - 1; //减小长度 + min = midLen; //更新最小值 + } else { + minLen = midLen + 1; //增大长度 + } + } + return min == -1 ? 0 : min; +} + +private int getMaxSum(int len, int[] nums) { + int n = nums.length; + int sum = 0; + int maxSum = 0; + // 达到长度 + for (int i = 0; i < len; i++) { + sum += nums[i]; + } + maxSum = sum; // 初始化 maxSum + + for (int i = len; i < n; i++) { + // 加一个数字减一个数字,保持长度不变 + sum += nums[i]; + sum = sum - nums[i - len]; + // 更新 maxSum + maxSum = Math.max(maxSum, sum); + } + return maxSum; +} +``` + +时间复杂度:`O(nlog(n))`。 + +# 总 + +这道题的话,通过前边刷题的经验,第一反应就想到了解法二中的双指针。如果不知道双指针的话,应该会想到解法一的暴力破解。 + +但对于二分查找的解法三和解法四,还是比较难想到的。不过回味完,其实二分查找的关键就是那个递增的有序数列,从而可以每次抛弃一半的可选解。 + diff --git a/leetcode-210-Course-ScheduleII.md b/leetcode-210-Course-ScheduleII.md index 400959ac5..bde2e5054 100644 --- a/leetcode-210-Course-ScheduleII.md +++ b/leetcode-210-Course-ScheduleII.md @@ -1,246 +1,246 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/210.jpg) - -[207 题](https://leetcode.wang/leetcode-207-Course-Schedule.html) Course Schedule 的延伸,给定 `n` 组先修课的关系,`[m,n]` 代表在上 `m` 这门课之前必须先上 `n` 这门课。输出一个上课序列。 - -# 思路分析 - -[207 题](https://leetcode.wang/leetcode-207-Course-Schedule.html) 考虑是否存在一个序列上完所有课,这里的话,换汤不换药,完全可以按照 [207 题](https://leetcode.wang/leetcode-207-Course-Schedule.html) 的解法改出来,大家可以先去看一下。主要是两种思路,`BFS` 和 `DFS` ,题目就是在考拓扑排序。 - -# 解法一 - -先把 [207 题](https://leetcode.wang/leetcode-207-Course-Schedule.html) 的思路贴过来。 - -把所有的关系可以看做图的边,所有的边构成了一个有向图。 - -对于`[[1,3],[1,4],[2,4],[3,5],[3,6],[4,6]]` 就可以看做下边的图,箭头指向的是需要先上的课。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/207_2.jpg) - -想法很简单,要想上完所有的课,一定会有一些课没有先修课,比如上图的 `5`、`6`。然后我们可以把 `5` 和 `6` 节点删去。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/207_3.jpg) - -然后 `3` 和 `4` 就可以上了,同样的道理再把 `3` 和 `4` 删去。 - -接下来就可以去学 `1` 和 `2` 了。因此可以完成所有的课。 - -代码的话,用邻接表表示图。此外,我们不需要真的去删除节点,我们可以用 `outNum` 变量记录所有节点的先修课门数。当删除一个节点的时候,就将相应节点的先修课个数减一即可。 - -最后只需要判断所有的节点的先修课门数是否全部是 `0` 即可。 - -在这道题的话,改之前的代码也很简单,只需要把每次出队的元素保存起来即可。 - -```java -public int[] findOrder(int numCourses, int[][] prerequisites) { - // 保存每个节点的先修课个数,也就是出度 - HashMap outNum = new HashMap<>(); - // 保存以 key 为先修课的列表,也就是入度的节点 - HashMap> inNodes = new HashMap<>(); - // 保存所有节点 - HashSet set = new HashSet<>(); - int rows = prerequisites.length; - for (int i = 0; i < rows; i++) { - int key = prerequisites[i][0]; - int value = prerequisites[i][1]; - set.add(key); - set.add(value); - if (!outNum.containsKey(key)) { - outNum.put(key, 0); - } - if (!outNum.containsKey(value)) { - outNum.put(value, 0); - } - // 当前节点先修课个数加一 - int num = outNum.get(key); - outNum.put(key, num + 1); - - if (!inNodes.containsKey(value)) { - inNodes.put(value, new ArrayList<>()); - } - // 更新以 value 为先修课的列表 - ArrayList list = inNodes.get(value); - list.add(key); - } - - // 将当前先修课个数为 0 的课加入到队列中 - Queue queue = new LinkedList<>(); - for (int k : set) { - if (outNum.get(k) == 0) { - queue.offer(k); - } - } - int[] res = new int[numCourses]; - int count = 0; - while (!queue.isEmpty()) { - // 队列拿出来的课代表要删除的节点 - // 要删除的节点的 list 中所有课的先修课个数减一 - int v = queue.poll(); - //**************主要修改的地方********************// - res[count++] = v; - //**********************************************// - ArrayList list = inNodes.getOrDefault(v, new ArrayList<>()); - - for (int k : list) { - int num = outNum.get(k); - // 当前课的先修课要变成 0, 加入队列 - if (num == 1) { - queue.offer(k); - } - // 当前课的先修课个数减一 - outNum.put(k, num - 1); - } - } - for (int k : set) { - if (outNum.get(k) != 0) { - //有课没有完成,返回空数组 - return new int[0]; - } - } - //**************主要修改的地方********************// - HashSet resSet = new HashSet<>(); - for (int i = 0; i < count; i++) { - resSet.add(res[i]); - } - //有些课是独立存在的,这些课可以随时上,添加进来 - for (int i = 0; i < numCourses; i++) { - if (!resSet.contains(i)) { - res[count++] = i; - } - } - //**********************************************// - return res; -} -``` - -上边的代码就是要注意一些课,既没有先修课,也不是别的课的先修课,所以这些课什么时候上都可以,在最后加进来即可。 - -# 解法二 - -同样的,先把 [207 题](https://leetcode.wang/leetcode-207-Course-Schedule.html) 的思路贴过来。 - -还有另一种思路,我们只需要一门课一门课的判断。 - -从某门课开始遍历,我们通过 `DFS` 一条路径一条路径的判断,保证过程中没有遇到环。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/207_2.jpg) - -深度优先遍历 `1`,相当于 `3` 条路径 - -`1 -> 3 -> 5`,`1 -> 3 -> 6`,`1 -> 4 -> 6`。 - -深度优先遍历 `2`,相当于 `1` 条路径 - -`2 -> 4 -> 6`。 - -深度优先遍历 `3`,相当于 `2` 条路径 - -`3 -> 5`,`3 -> 6`。 - -深度优先遍历 `4`,相当于 `1` 条路径 - -`4 -> 6`。 - -深度优先遍历 `5`,相当于 `1` 条路径 - -`5`。 - -深度优先遍历 `6`,相当于 `1` 条路径 - -`6`。 - -什么情况下不能完成所有课程呢?某条路径出现了环,如下图。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/207_4.jpg) - -出现了 `1 -> 3 -> 6 -> 3`。所以不能学完所有课程。 - -代码的话,用邻接表表示图。通过递归实现 `DFS` ,用 `visited` 存储当前路径上的节点。 - -同时用 `visitedFinish` 表示可以学完的课程,起到优化算法的作用。 - -在这道题的话,我们只需要在 `dfs` 中把叶子节点加入,并且如果当前节点的所有先修课已经完成,也将其加入。在代码中就体现在完成了 `for` 循环的时候。 - -```java -public int[] findOrder(int numCourses, int[][] prerequisites) { - HashMap> outNodes = new HashMap<>(); - HashSet set = new HashSet<>(); - int rows = prerequisites.length; - for (int i = 0; i < rows; i++) { - int key = prerequisites[i][0]; - int value = prerequisites[i][1]; - set.add(key); - if (!outNodes.containsKey(key)) { - outNodes.put(key, new ArrayList<>()); - } - // 存储当前节点的所有先修课程 - ArrayList list = outNodes.get(key); - list.add(value); - } - - int[] res = new int[numCourses]; - HashSet resSet = new HashSet<>(); //防止重复的节点加入 - HashSet visitedFinish = new HashSet<>(); - // 判断每一门课 - for (int k : set) { - if (!dfs(k, outNodes, new HashSet<>(), visitedFinish, res, resSet)) { - return new int[0]; - } - visitedFinish.add(k); - } - //和之前一样,把独立的课加入 - for (int i = 0; i < numCourses; i++) { - if (!resSet.contains(i)) { - res[count++] = i; - } - } - return res; -} - -int count = 0; - -private boolean dfs(int start, HashMap> outNodes, HashSet visited, - HashSet visitedFinish, int[] res, HashSet resSet) { - // 已经处理过 - if (visitedFinish.contains(start)) { - return true; - } - //**************主要修改的地方********************// - // 到了叶子节点 - if (!outNodes.containsKey(start)) { - if (!resSet.contains(start)) { - resSet.add(start); - res[count++] = start; - } - return true; - } - //**********************************************// - // 出现了环 - if (visited.contains(start)) { - return false; - } - // 将当前节点加入路径 - visited.add(start); - ArrayList list = outNodes.get(start); - for (int k : list) { - if (!dfs(k, outNodes, visited, visitedFinish, res, resSet)) { - return false; - } - } - //**************主要修改的地方********************// - if (!resSet.contains(start)) { - resSet.add(start); - res[count++] = start; - } - //**********************************************// - visited.remove(start); - return true; -} -``` - -我们分别用数组 `res` 和集合 `resSet` 存储最终的结果,因为 `DFS` 中可能经过重复的节点,`resSet` 可以保证我们不添加重复的节点。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/210.jpg) + +[207 题](https://leetcode.wang/leetcode-207-Course-Schedule.html) Course Schedule 的延伸,给定 `n` 组先修课的关系,`[m,n]` 代表在上 `m` 这门课之前必须先上 `n` 这门课。输出一个上课序列。 + +# 思路分析 + +[207 题](https://leetcode.wang/leetcode-207-Course-Schedule.html) 考虑是否存在一个序列上完所有课,这里的话,换汤不换药,完全可以按照 [207 题](https://leetcode.wang/leetcode-207-Course-Schedule.html) 的解法改出来,大家可以先去看一下。主要是两种思路,`BFS` 和 `DFS` ,题目就是在考拓扑排序。 + +# 解法一 + +先把 [207 题](https://leetcode.wang/leetcode-207-Course-Schedule.html) 的思路贴过来。 + +把所有的关系可以看做图的边,所有的边构成了一个有向图。 + +对于`[[1,3],[1,4],[2,4],[3,5],[3,6],[4,6]]` 就可以看做下边的图,箭头指向的是需要先上的课。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/207_2.jpg) + +想法很简单,要想上完所有的课,一定会有一些课没有先修课,比如上图的 `5`、`6`。然后我们可以把 `5` 和 `6` 节点删去。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/207_3.jpg) + +然后 `3` 和 `4` 就可以上了,同样的道理再把 `3` 和 `4` 删去。 + +接下来就可以去学 `1` 和 `2` 了。因此可以完成所有的课。 + +代码的话,用邻接表表示图。此外,我们不需要真的去删除节点,我们可以用 `outNum` 变量记录所有节点的先修课门数。当删除一个节点的时候,就将相应节点的先修课个数减一即可。 + +最后只需要判断所有的节点的先修课门数是否全部是 `0` 即可。 + +在这道题的话,改之前的代码也很简单,只需要把每次出队的元素保存起来即可。 + +```java +public int[] findOrder(int numCourses, int[][] prerequisites) { + // 保存每个节点的先修课个数,也就是出度 + HashMap outNum = new HashMap<>(); + // 保存以 key 为先修课的列表,也就是入度的节点 + HashMap> inNodes = new HashMap<>(); + // 保存所有节点 + HashSet set = new HashSet<>(); + int rows = prerequisites.length; + for (int i = 0; i < rows; i++) { + int key = prerequisites[i][0]; + int value = prerequisites[i][1]; + set.add(key); + set.add(value); + if (!outNum.containsKey(key)) { + outNum.put(key, 0); + } + if (!outNum.containsKey(value)) { + outNum.put(value, 0); + } + // 当前节点先修课个数加一 + int num = outNum.get(key); + outNum.put(key, num + 1); + + if (!inNodes.containsKey(value)) { + inNodes.put(value, new ArrayList<>()); + } + // 更新以 value 为先修课的列表 + ArrayList list = inNodes.get(value); + list.add(key); + } + + // 将当前先修课个数为 0 的课加入到队列中 + Queue queue = new LinkedList<>(); + for (int k : set) { + if (outNum.get(k) == 0) { + queue.offer(k); + } + } + int[] res = new int[numCourses]; + int count = 0; + while (!queue.isEmpty()) { + // 队列拿出来的课代表要删除的节点 + // 要删除的节点的 list 中所有课的先修课个数减一 + int v = queue.poll(); + //**************主要修改的地方********************// + res[count++] = v; + //**********************************************// + ArrayList list = inNodes.getOrDefault(v, new ArrayList<>()); + + for (int k : list) { + int num = outNum.get(k); + // 当前课的先修课要变成 0, 加入队列 + if (num == 1) { + queue.offer(k); + } + // 当前课的先修课个数减一 + outNum.put(k, num - 1); + } + } + for (int k : set) { + if (outNum.get(k) != 0) { + //有课没有完成,返回空数组 + return new int[0]; + } + } + //**************主要修改的地方********************// + HashSet resSet = new HashSet<>(); + for (int i = 0; i < count; i++) { + resSet.add(res[i]); + } + //有些课是独立存在的,这些课可以随时上,添加进来 + for (int i = 0; i < numCourses; i++) { + if (!resSet.contains(i)) { + res[count++] = i; + } + } + //**********************************************// + return res; +} +``` + +上边的代码就是要注意一些课,既没有先修课,也不是别的课的先修课,所以这些课什么时候上都可以,在最后加进来即可。 + +# 解法二 + +同样的,先把 [207 题](https://leetcode.wang/leetcode-207-Course-Schedule.html) 的思路贴过来。 + +还有另一种思路,我们只需要一门课一门课的判断。 + +从某门课开始遍历,我们通过 `DFS` 一条路径一条路径的判断,保证过程中没有遇到环。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/207_2.jpg) + +深度优先遍历 `1`,相当于 `3` 条路径 + +`1 -> 3 -> 5`,`1 -> 3 -> 6`,`1 -> 4 -> 6`。 + +深度优先遍历 `2`,相当于 `1` 条路径 + +`2 -> 4 -> 6`。 + +深度优先遍历 `3`,相当于 `2` 条路径 + +`3 -> 5`,`3 -> 6`。 + +深度优先遍历 `4`,相当于 `1` 条路径 + +`4 -> 6`。 + +深度优先遍历 `5`,相当于 `1` 条路径 + +`5`。 + +深度优先遍历 `6`,相当于 `1` 条路径 + +`6`。 + +什么情况下不能完成所有课程呢?某条路径出现了环,如下图。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/207_4.jpg) + +出现了 `1 -> 3 -> 6 -> 3`。所以不能学完所有课程。 + +代码的话,用邻接表表示图。通过递归实现 `DFS` ,用 `visited` 存储当前路径上的节点。 + +同时用 `visitedFinish` 表示可以学完的课程,起到优化算法的作用。 + +在这道题的话,我们只需要在 `dfs` 中把叶子节点加入,并且如果当前节点的所有先修课已经完成,也将其加入。在代码中就体现在完成了 `for` 循环的时候。 + +```java +public int[] findOrder(int numCourses, int[][] prerequisites) { + HashMap> outNodes = new HashMap<>(); + HashSet set = new HashSet<>(); + int rows = prerequisites.length; + for (int i = 0; i < rows; i++) { + int key = prerequisites[i][0]; + int value = prerequisites[i][1]; + set.add(key); + if (!outNodes.containsKey(key)) { + outNodes.put(key, new ArrayList<>()); + } + // 存储当前节点的所有先修课程 + ArrayList list = outNodes.get(key); + list.add(value); + } + + int[] res = new int[numCourses]; + HashSet resSet = new HashSet<>(); //防止重复的节点加入 + HashSet visitedFinish = new HashSet<>(); + // 判断每一门课 + for (int k : set) { + if (!dfs(k, outNodes, new HashSet<>(), visitedFinish, res, resSet)) { + return new int[0]; + } + visitedFinish.add(k); + } + //和之前一样,把独立的课加入 + for (int i = 0; i < numCourses; i++) { + if (!resSet.contains(i)) { + res[count++] = i; + } + } + return res; +} + +int count = 0; + +private boolean dfs(int start, HashMap> outNodes, HashSet visited, + HashSet visitedFinish, int[] res, HashSet resSet) { + // 已经处理过 + if (visitedFinish.contains(start)) { + return true; + } + //**************主要修改的地方********************// + // 到了叶子节点 + if (!outNodes.containsKey(start)) { + if (!resSet.contains(start)) { + resSet.add(start); + res[count++] = start; + } + return true; + } + //**********************************************// + // 出现了环 + if (visited.contains(start)) { + return false; + } + // 将当前节点加入路径 + visited.add(start); + ArrayList list = outNodes.get(start); + for (int k : list) { + if (!dfs(k, outNodes, visited, visitedFinish, res, resSet)) { + return false; + } + } + //**************主要修改的地方********************// + if (!resSet.contains(start)) { + resSet.add(start); + res[count++] = start; + } + //**********************************************// + visited.remove(start); + return true; +} +``` + +我们分别用数组 `res` 和集合 `resSet` 存储最终的结果,因为 `DFS` 中可能经过重复的节点,`resSet` 可以保证我们不添加重复的节点。 + +# 总 + 总体上和 [207 题](https://leetcode.wang/leetcode-207-Course-Schedule.html) 是一样的,一些细节的地方注意到了即可。当然上边的代码因为是在 207 题的基础上改的,所以可能不够简洁,仅供参考,总体思想就是 `BFS` 和 `DFS` 。 \ No newline at end of file diff --git a/leetcode-211-Add-And-Search-Word-Data-structure-design.md b/leetcode-211-Add-And-Search-Word-Data-structure-design.md index edae6e2db..86707694d 100644 --- a/leetcode-211-Add-And-Search-Word-Data-structure-design.md +++ b/leetcode-211-Add-And-Search-Word-Data-structure-design.md @@ -1,255 +1,255 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/211.jpg) - -设计一个数据结构,实现 `add` 方法添加字符串,`search` 查找字符串,所查找的字符串可能含有 `.` ,代表任意的字符。 - -# 解法一 暴力 - -来个暴力的方法,用 `HashSet` 存入所有的字符串。当查找字符串的时候,我们首先判断 `set` 中是否存在,如果存在的话直接返回 `true` 。不存在的话,因为要查找的字符串中可能含有 `.` ,接下来我们需要遍历 `set` ,一个一个的进行匹配。 - -```java -class WordDictionary { - HashSet set; - - /** Initialize your data structure here. */ - public WordDictionary() { - set = new HashSet<>(); - } - - /** Adds a word into the data structure. */ - public void addWord(String word) { - set.add(word); - } - - /** - * Returns if the word is in the data structure. A word could contain the - * dot character '.' to represent any one letter. - */ - public boolean search(String word) { - if (set.contains(word)) { - return true; - } - for (String s : set) { - if (equal(s, word)) { - return true; - } - } - return false; - } - - private boolean equal(String s, String word) { - char[] c1 = s.toCharArray(); - char[] c2 = word.toCharArray(); - int n1 = s.length(); - int n2 = word.length(); - if (n1 != n2) { - return false; - } - for (int i = 0; i < n1; i++) { - //. 代表任意字符,跳过 - if (c1[i] != c2[i] && c2[i] != '.') { - return false; - } - } - return true; - } -} -``` - -当然,由于有些暴力,出现了超时。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/211_2.jpg) - -至于优化的话,我们不要将加入的字符串一股脑的放入一个 `set` 中,可以通过长度进行分类,将长度相同的放到一个 `set` 中。这样一个一个匹配的时候,规模会减小一些。 - -```java -class WordDictionary { - HashMap> map; - - /** Initialize your data structure here. */ - public WordDictionary() { - map = new HashMap<>(); - } - - /** Adds a word into the data structure. */ - public void addWord(String word) { - int n = word.length(); - //将字符串加入对应长度的 set 中 - if (map.containsKey(n)) { - HashSet set = map.get(n); - set.add(word); - } else { - HashSet set = new HashSet(); - set.add(word); - map.put(n, set); - } - } - - /** - * Returns if the word is in the data structure. A word could contain the - * dot character '.' to represent any one letter. - */ - public boolean search(String word) { - HashSet set = map.getOrDefault(word.length(), new HashSet()); - if (set.contains(word)) { - return true; - } - for (String s : set) { - if (equal(s, word)) { - return true; - } - } - return false; - } - - private boolean equal(String s, String word) { - char[] c1 = s.toCharArray(); - char[] c2 = word.toCharArray(); - int n1 = s.length(); - int n2 = word.length(); - if (n1 != n2) { - return false; - } - for (int i = 0; i < n1; i++) { - if (c1[i] != c2[i] && c2[i] != '.') { - return false; - } - } - return true; - } -} -``` - -虽然上边的解法在 leetcode 中 AC 了,但其实很大程度上取决于 test cases 中所有字符串长度的分布,如果字符串长度全部集中于某个值,上边的解法的优化其实是无能为力的。 - -上边是按长度分类进行添加的,同样的我们还可以按照字符串的开头字母进行分类。当然,算法的速度同样也依赖于数据的分布,适用于数据分布均匀的情况。 - -# 解法二 前缀树 - -前几天在 [208 题](https://leetcode.wang/leetcode-208-Implement-Trie-Prefix-Tree.html) 刚做了前缀树,这里的话我们也可以通过前缀树进行存储,这样查找字符串就不用依赖于字符串的数量了。 - -代码的话在前缀树的基础上改一下就可以,大家可以先看一下 [208 题](https://leetcode.wang/leetcode-208-Implement-Trie-Prefix-Tree.html)。对于字符串中的 `.` ,我们通过递归去查找。 - -```java -class WordDictionary { - class TrieNode { - TrieNode[] children; - boolean flag; - - public TrieNode() { - children = new TrieNode[26]; - flag = false; - for (int i = 0; i < 26; i++) { - children[i] = null; - } - } - - } - TrieNode root; - - /** Initialize your data structure here. */ - public WordDictionary() { - root = new TrieNode(); - } - - /** Adds a word into the data structure. */ - public void addWord(String word) { - char[] array = word.toCharArray(); - TrieNode cur = root; - for (int i = 0; i < array.length; i++) { - // 当前孩子是否存在 - if (cur.children[array[i] - 'a'] == null) { - cur.children[array[i] - 'a'] = new TrieNode(); - } - cur = cur.children[array[i] - 'a']; - } - // 当前节点代表结束 - cur.flag = true; - } - - /** - * Returns if the word is in the data structure. A word could contain the - * dot character '.' to represent any one letter. - */ - public boolean search(String word) { - return searchHelp(word, root); - } - - private boolean searchHelp(String word, TrieNode root) { - char[] array = word.toCharArray(); - TrieNode cur = root; - for (int i = 0; i < array.length; i++) { - // 对于 . , 递归的判断所有不为空的孩子 - if(array[i] == '.'){ - for(int j = 0;j < 26; j++){ - if(cur.children[j] != null){ - if(searchHelp(word.substring(i + 1),cur.children[j])){ - return true; - } - } - } - return false; - } - // 不含有当前节点 - if (cur.children[array[i] - 'a'] == null) { - return false; - } - cur = cur.children[array[i] - 'a']; - } - // 当前节点是否为是某个单词的结束 - return cur.flag; - } -} -``` - -# 解法三 - -再分享一个 leetcode 上边的大神 [@StefanPochmann](https://leetcode.com/stefanpochmann) 的一个想法,直接利用正则表达式。 - -我就不细讲了,直接看代码吧,很简洁。 - -```python -import re -class WordDictionary: - - def __init__(self): - self.words = '#' - - def addWord(self, word): - self.words += word + '#' - - def search(self, word): - return bool(re.search('#' + word + '#', self.words)) -``` - -我用 `java` 改写了一下。 - -```java -import java.util.regex.*; -class WordDictionary { - StringBuilder sb; - public WordDictionary() { - sb = new StringBuilder(); - sb.append('#'); - } - public void addWord(String word) { - sb.append(word); - sb.append('#'); - } - - public boolean search(String word) { - Pattern p = Pattern.compile('#' + word + '#'); - Matcher m = p.matcher(sb.toString()); - return m.find(); - } -} -``` - -不过遗憾的是,`java` 在 `leetcode` 上这个解法会超时,`python` 没什么问题。当然优化的话,我们可以再像解法一那样对字符串进行分类,这里就不再写了。 - -上边的解法的关键就是,用 `#` 分割不同单词,以及查找的时候查找 `# + word + #` ,很妙。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/211.jpg) + +设计一个数据结构,实现 `add` 方法添加字符串,`search` 查找字符串,所查找的字符串可能含有 `.` ,代表任意的字符。 + +# 解法一 暴力 + +来个暴力的方法,用 `HashSet` 存入所有的字符串。当查找字符串的时候,我们首先判断 `set` 中是否存在,如果存在的话直接返回 `true` 。不存在的话,因为要查找的字符串中可能含有 `.` ,接下来我们需要遍历 `set` ,一个一个的进行匹配。 + +```java +class WordDictionary { + HashSet set; + + /** Initialize your data structure here. */ + public WordDictionary() { + set = new HashSet<>(); + } + + /** Adds a word into the data structure. */ + public void addWord(String word) { + set.add(word); + } + + /** + * Returns if the word is in the data structure. A word could contain the + * dot character '.' to represent any one letter. + */ + public boolean search(String word) { + if (set.contains(word)) { + return true; + } + for (String s : set) { + if (equal(s, word)) { + return true; + } + } + return false; + } + + private boolean equal(String s, String word) { + char[] c1 = s.toCharArray(); + char[] c2 = word.toCharArray(); + int n1 = s.length(); + int n2 = word.length(); + if (n1 != n2) { + return false; + } + for (int i = 0; i < n1; i++) { + //. 代表任意字符,跳过 + if (c1[i] != c2[i] && c2[i] != '.') { + return false; + } + } + return true; + } +} +``` + +当然,由于有些暴力,出现了超时。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/211_2.jpg) + +至于优化的话,我们不要将加入的字符串一股脑的放入一个 `set` 中,可以通过长度进行分类,将长度相同的放到一个 `set` 中。这样一个一个匹配的时候,规模会减小一些。 + +```java +class WordDictionary { + HashMap> map; + + /** Initialize your data structure here. */ + public WordDictionary() { + map = new HashMap<>(); + } + + /** Adds a word into the data structure. */ + public void addWord(String word) { + int n = word.length(); + //将字符串加入对应长度的 set 中 + if (map.containsKey(n)) { + HashSet set = map.get(n); + set.add(word); + } else { + HashSet set = new HashSet(); + set.add(word); + map.put(n, set); + } + } + + /** + * Returns if the word is in the data structure. A word could contain the + * dot character '.' to represent any one letter. + */ + public boolean search(String word) { + HashSet set = map.getOrDefault(word.length(), new HashSet()); + if (set.contains(word)) { + return true; + } + for (String s : set) { + if (equal(s, word)) { + return true; + } + } + return false; + } + + private boolean equal(String s, String word) { + char[] c1 = s.toCharArray(); + char[] c2 = word.toCharArray(); + int n1 = s.length(); + int n2 = word.length(); + if (n1 != n2) { + return false; + } + for (int i = 0; i < n1; i++) { + if (c1[i] != c2[i] && c2[i] != '.') { + return false; + } + } + return true; + } +} +``` + +虽然上边的解法在 leetcode 中 AC 了,但其实很大程度上取决于 test cases 中所有字符串长度的分布,如果字符串长度全部集中于某个值,上边的解法的优化其实是无能为力的。 + +上边是按长度分类进行添加的,同样的我们还可以按照字符串的开头字母进行分类。当然,算法的速度同样也依赖于数据的分布,适用于数据分布均匀的情况。 + +# 解法二 前缀树 + +前几天在 [208 题](https://leetcode.wang/leetcode-208-Implement-Trie-Prefix-Tree.html) 刚做了前缀树,这里的话我们也可以通过前缀树进行存储,这样查找字符串就不用依赖于字符串的数量了。 + +代码的话在前缀树的基础上改一下就可以,大家可以先看一下 [208 题](https://leetcode.wang/leetcode-208-Implement-Trie-Prefix-Tree.html)。对于字符串中的 `.` ,我们通过递归去查找。 + +```java +class WordDictionary { + class TrieNode { + TrieNode[] children; + boolean flag; + + public TrieNode() { + children = new TrieNode[26]; + flag = false; + for (int i = 0; i < 26; i++) { + children[i] = null; + } + } + + } + TrieNode root; + + /** Initialize your data structure here. */ + public WordDictionary() { + root = new TrieNode(); + } + + /** Adds a word into the data structure. */ + public void addWord(String word) { + char[] array = word.toCharArray(); + TrieNode cur = root; + for (int i = 0; i < array.length; i++) { + // 当前孩子是否存在 + if (cur.children[array[i] - 'a'] == null) { + cur.children[array[i] - 'a'] = new TrieNode(); + } + cur = cur.children[array[i] - 'a']; + } + // 当前节点代表结束 + cur.flag = true; + } + + /** + * Returns if the word is in the data structure. A word could contain the + * dot character '.' to represent any one letter. + */ + public boolean search(String word) { + return searchHelp(word, root); + } + + private boolean searchHelp(String word, TrieNode root) { + char[] array = word.toCharArray(); + TrieNode cur = root; + for (int i = 0; i < array.length; i++) { + // 对于 . , 递归的判断所有不为空的孩子 + if(array[i] == '.'){ + for(int j = 0;j < 26; j++){ + if(cur.children[j] != null){ + if(searchHelp(word.substring(i + 1),cur.children[j])){ + return true; + } + } + } + return false; + } + // 不含有当前节点 + if (cur.children[array[i] - 'a'] == null) { + return false; + } + cur = cur.children[array[i] - 'a']; + } + // 当前节点是否为是某个单词的结束 + return cur.flag; + } +} +``` + +# 解法三 + +再分享一个 leetcode 上边的大神 [@StefanPochmann](https://leetcode.com/stefanpochmann) 的一个想法,直接利用正则表达式。 + +我就不细讲了,直接看代码吧,很简洁。 + +```python +import re +class WordDictionary: + + def __init__(self): + self.words = '#' + + def addWord(self, word): + self.words += word + '#' + + def search(self, word): + return bool(re.search('#' + word + '#', self.words)) +``` + +我用 `java` 改写了一下。 + +```java +import java.util.regex.*; +class WordDictionary { + StringBuilder sb; + public WordDictionary() { + sb = new StringBuilder(); + sb.append('#'); + } + public void addWord(String word) { + sb.append(word); + sb.append('#'); + } + + public boolean search(String word) { + Pattern p = Pattern.compile('#' + word + '#'); + Matcher m = p.matcher(sb.toString()); + return m.find(); + } +} +``` + +不过遗憾的是,`java` 在 `leetcode` 上这个解法会超时,`python` 没什么问题。当然优化的话,我们可以再像解法一那样对字符串进行分类,这里就不再写了。 + +上边的解法的关键就是,用 `#` 分割不同单词,以及查找的时候查找 `# + word + #` ,很妙。 + +# 总 + 解法一是直觉上的解法,分类的思想也经常用到。解法二的话,需要数据结构的积累,刚好前几题实现了前缀树,空间换时间,这里也就直接想到了。解法三的话,大概只有大神能想到了,一种完全不同的视角,很棒。 \ No newline at end of file diff --git a/leetcode-212-Word-SearchII.md b/leetcode-212-Word-SearchII.md index 7486e06f8..629b32d2a 100644 --- a/leetcode-212-Word-SearchII.md +++ b/leetcode-212-Word-SearchII.md @@ -1,352 +1,352 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/212.jpg) - -[79 题](https://leetcode.wang/leetCode-79-Word-Search.html) Word Search 的延续。 - -意思就是从某个字符出发,然后它可以向左向右向上向下移动,走过的路径构成一个字符串,判断是否能走出给定字符串的 word ,还有一个条件就是走过的字符不能够走第二次。 - -比如 eat,从第二行最后一列的 e 出发,向左移动,再向左移动,就走出了 eat。 - -题目中给一个 word 列表,要求找出哪些单词可以由 `board` 生成。 - -# 解法一 - -直接利用 [79 题](https://leetcode.wang/leetCode-79-Word-Search.html) 的代码 ,79 题是判断某个单词能否在 board 中生成。这里的话,很简单,直接遍历 `words` 数组,然后利用 79 题的代码去依次判断即可。 - -[79 题 ](https://leetcode.wang/leetCode-79-Word-Search.html)使用的时候采用 `dfs` ,没做过的话大家可以先过去看一下。 - -```java -public List findWords(char[][] board, String[] words) { - List res = new ArrayList<>(); - //判断每个单词 - for (String word : words) { - if (exist(board, word)) { - res.add(word); - } - } - return res; -} -//下边是 79 题的代码 -public boolean exist(char[][] board, String word) { - int rows = board.length; - if (rows == 0) { - return false; - } - int cols = board[0].length; - for (int i = 0; i < rows; i++) { - for (int j = 0; j < cols; j++) { - if (existRecursive(board, i, j, word, 0)) { - return true; - } - } - } - return false; -} - -private boolean existRecursive(char[][] board, int row, int col, String word, int index) { - if (row < 0 || row >= board.length || col < 0 || col >= board[0].length) { - return false; - } - if (board[row][col] != word.charAt(index)) { - return false; - } - if (index == word.length() - 1) { - return true; - } - char temp = board[row][col]; - board[row][col] = '$'; - boolean up = existRecursive(board, row - 1, col, word, index + 1); - if (up) { - board[row][col] = temp; //将 board 还原, 79 题中的代码没有还原,这里必须还原 - return true; - } - boolean down = existRecursive(board, row + 1, col, word, index + 1); - if (down) { - board[row][col] = temp;//将 board 还原, 79 题中的代码没有还原,这里必须还原 - return true; - } - boolean left = existRecursive(board, row, col - 1, word, index + 1); - if (left) { - board[row][col] = temp;//将 board 还原, 79 题中的代码没有还原,这里必须还原 - return true; - } - boolean right = existRecursive(board, row, col + 1, word, index + 1); - if (right) { - board[row][col] = temp;//将 board 还原, 79 题中的代码没有还原,这里必须还原 - return true; - } - board[row][col] = temp; - return false; -} -``` - -然后它竟然过了,竟然过了。。。我还以为这么暴力一定会暗藏玄机。不过为了尊重它是一道 hard 的题目,我就继续思考能不能优化下。 - -顺着上边的思路想,首先 [79 题 ](https://leetcode.wang/leetCode-79-Word-Search.html) 中判断单个单词是否能生成肯定不能优化了,不然之前肯定会写优化方法。那么继续优化的话,就只能去寻求不同 `word` 的之间的联系了。 - -什么意思呢? - -就是如果知道了某个单词能生成,那么对于后边将要判断的单词能不能提供些帮助呢? - -或者知道了某个单词不能生成,对于后边将要判断的单词能不能提供些帮助呢? - -想了想,只想到了一种情况。比如我们已经知道了 `basketboard` 能够在二维数组 `board` 中生成。那么它的所有前缀一定也能生成,比如 `basket` 一定能够生成。 - -说到前缀,自然而然的想到了之前的前缀树,这几天出现的频率也比较高,刚做的 [211 题](https://leetcode.wang/leetcode-211-Add-And-Search-Word-Data-structure-design.html) 也用到了。我们可以把能生成的 `word` 加入到前缀树中,然后再判断后边的单词前,先判断它是不是前缀树中某个单词的前缀。 - -当然如果单词 `A` 是 `B` 的前缀,那么 `A` 的长度肯定短一些,所以我们必须先判断了较长的单词 `B`,才能产生优化的效果。所以我们首先要把 `words` 按照单词的长度从大到小排序。 - -小弟不才,只想到了这一种联系,下边是代码,前缀树直接搬 [208 题](https://leetcode.wang/leetcode-208-Implement-Trie-Prefix-Tree.html) 的代码即可。 - -```java -//208 题前缀树代码 -class Trie { - class TrieNode { - TrieNode[] children; - boolean flag; - - public TrieNode() { - children = new TrieNode[26]; - flag = false; - for (int i = 0; i < 26; i++) { - children[i] = null; - } - } - } - - TrieNode root; - - /** Initialize your data structure here. */ - public Trie() { - root = new TrieNode(); - } - - /** Inserts a word into the trie. */ - public void insert(String word) { - char[] array = word.toCharArray(); - TrieNode cur = root; - for (int i = 0; i < array.length; i++) { - // 当前孩子是否存在 - if (cur.children[array[i] - 'a'] == null) { - cur.children[array[i] - 'a'] = new TrieNode(); - } - cur = cur.children[array[i] - 'a']; - } - // 当前节点代表结束 - cur.flag = true; - } - - /** - * Returns if there is any word in the trie that starts with the given - * prefix. - */ - public boolean startsWith(String prefix) { - char[] array = prefix.toCharArray(); - TrieNode cur = root; - for (int i = 0; i < array.length; i++) { - if (cur.children[array[i] - 'a'] == null) { - return false; - } - cur = cur.children[array[i] - 'a']; - } - return true; - } - -}; -//本题代码 -public List findWords(char[][] board, String[] words) { - //将单词的长度从大到小排序 - Arrays.sort(words, new Comparator() { - @Override - public int compare(String o1, String o2) { - return o2.length() - o1.length(); - } - - }); - Trie trie = new Trie(); - List res = new ArrayList<>(); - for (String word : words) { - //判断当前单词是否是已经完成的单词的前缀 - if (trie.startsWith(word)) { - res.add(word); - continue; - } - if (exist(board, word)) { - res.add(word); - //加入到前缀树中 - trie.insert(word); - } - } - return res; -} -//下边是 79 题的代码 -public boolean exist(char[][] board, String word) { - int rows = board.length; - if (rows == 0) { - return false; - } - int cols = board[0].length; - for (int i = 0; i < rows; i++) { - for (int j = 0; j < cols; j++) { - if (existRecursive(board, i, j, word, 0)) { - return true; - } - } - } - return false; -} - - -private boolean existRecursive(char[][] board, int row, int col, String word, int index) { - if (row < 0 || row >= board.length || col < 0 || col >= board[0].length) { - return false; - } - if (board[row][col] != word.charAt(index)) { - return false; - } - if (index == word.length() - 1) { - return true; - } - char temp = board[row][col]; - board[row][col] = '$'; - boolean up = existRecursive(board, row - 1, col, word, index + 1); - if (up) { - board[row][col] = temp; - return true; - } - boolean down = existRecursive(board, row + 1, col, word, index + 1); - if (down) { - board[row][col] = temp; - return true; - } - boolean left = existRecursive(board, row, col - 1, word, index + 1); - if (left) { - board[row][col] = temp; - return true; - } - boolean right = existRecursive(board, row, col + 1, word, index + 1); - if (right) { - board[row][col] = temp; - return true; - } - board[row][col] = temp; - return false; -} -``` - -然而事实是残酷的,对于 `leetcode` 的 `test cases` ,这个想法并没有带来速度上的提升。于是就去逛 `discuss` 了,也就是下边的解法二。 - -# 解法二 - -参考 [这里](https://leetcode.com/problems/word-search-ii/discuss/59780/Java-15ms-Easiest-Solution-(100.00))。 - -解法一中的想法是,`从 words 中依次选定一个单词` -> `从图中的每个位置出发,看能否找到这个单词` - -我们其实可以倒过来。`从图中的每个位置出发` -> `看遍历过程中是否遇到了 words 中的某个单词` - -遍历过程中判断是否遇到了某个单词,我们可以事先把所有单词存到前缀树中。这样的话,如果当前走的路径不是前缀树的前缀,我们就可以提前结束了。如果是前缀树的中的单词,我们就将其存到结果中。 - -至于实现的话,我们可以在遍历过程中,将当前路径的单词传进函数,然后判断当前路径构成的单词是否是在前缀树中出现即可。 - -这个想法可行,但不够好,因为每次都从前缀树中判断当前路径的单词,会带来重复的判断。比如先判断了 `an` 存在于前缀树中,接下来假如路径变成 `ang` ,判断它在不在前缀中,又需要判断一遍 `an` 。 - -因此,我们可以将前缀树融合到我们的算法中,递归中去传递前缀树的节点,判断当前节点的孩子是否为 `null`,如果是 `null` 说明当前前缀不存在,可以提前结束。如果不是 `null`,再判断当前节点是否是单词的结尾,如果是结尾直接将当前单词加入。 - -由于递归过程中没有加路径,所以我们改造一下前缀树的节点,将单词直接存入节点,这样的话就可以直接取到了。 - -干巴巴的文字可能不好理解,看一下下边的代码应该就明白了。 - -```java -//改造前缀树节点 -class TrieNode { - public TrieNode[] children; - public String word; //节点直接存当前的单词 - - public TrieNode() { - children = new TrieNode[26]; - word = null; - for (int i = 0; i < 26; i++) { - children[i] = null; - } - } -} -class Trie { - TrieNode root; - /** Initialize your data structure here. */ - public Trie() { - root = new TrieNode(); - } - - /** Inserts a word into the trie. */ - public void insert(String word) { - char[] array = word.toCharArray(); - TrieNode cur = root; - for (int i = 0; i < array.length; i++) { - // 当前孩子是否存在 - if (cur.children[array[i] - 'a'] == null) { - cur.children[array[i] - 'a'] = new TrieNode(); - } - cur = cur.children[array[i] - 'a']; - } - // 当前节点结束,存入当前单词 - cur.word = word; - } -}; - -class Solution { - public List findWords(char[][] board, String[] words) { - Trie trie = new Trie(); - //将所有单词存入前缀树中 - List res = new ArrayList<>(); - for (String word : words) { - trie.insert(word); - } - int rows = board.length; - if (rows == 0) { - return res; - } - int cols = board[0].length; - //从每个位置开始遍历 - for (int i = 0; i < rows; i++) { - for (int j = 0; j < cols; j++) { - existRecursive(board, i, j, trie.root, res); - } - } - return res; - } - - private void existRecursive(char[][] board, int row, int col, TrieNode node, List res) { - if (row < 0 || row >= board.length || col < 0 || col >= board[0].length) { - return; - } - char cur = board[row][col];//将要遍历的字母 - //当前节点遍历过或者将要遍历的字母在前缀树中不存在 - if (cur == '$' || node.children[cur - 'a'] == null) { - return; - } - node = node.children[cur - 'a']; - //判断当前节点是否是一个单词的结束 - if (node.word != null) { - //加入到结果中 - res.add(node.word); - //将当前单词置为 null,防止重复加入 - node.word = null; - } - char temp = board[row][col]; - //上下左右去遍历 - board[row][col] = '$'; - existRecursive(board, row - 1, col, node, res); - existRecursive(board, row + 1, col, node, res); - existRecursive(board, row, col - 1, node, res); - existRecursive(board, row, col + 1, node, res); - board[row][col] = temp; - } -} -``` - -结合代码就很好懂了,就是从每个位置对图做深度优先搜索,然后路径生成的字符串如果没有在前缀树中出现就提前结束,如果到了前缀树中某个单词的结束,就将当前单词加入即可。 - -# 总 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/212.jpg) + +[79 题](https://leetcode.wang/leetCode-79-Word-Search.html) Word Search 的延续。 + +意思就是从某个字符出发,然后它可以向左向右向上向下移动,走过的路径构成一个字符串,判断是否能走出给定字符串的 word ,还有一个条件就是走过的字符不能够走第二次。 + +比如 eat,从第二行最后一列的 e 出发,向左移动,再向左移动,就走出了 eat。 + +题目中给一个 word 列表,要求找出哪些单词可以由 `board` 生成。 + +# 解法一 + +直接利用 [79 题](https://leetcode.wang/leetCode-79-Word-Search.html) 的代码 ,79 题是判断某个单词能否在 board 中生成。这里的话,很简单,直接遍历 `words` 数组,然后利用 79 题的代码去依次判断即可。 + +[79 题 ](https://leetcode.wang/leetCode-79-Word-Search.html)使用的时候采用 `dfs` ,没做过的话大家可以先过去看一下。 + +```java +public List findWords(char[][] board, String[] words) { + List res = new ArrayList<>(); + //判断每个单词 + for (String word : words) { + if (exist(board, word)) { + res.add(word); + } + } + return res; +} +//下边是 79 题的代码 +public boolean exist(char[][] board, String word) { + int rows = board.length; + if (rows == 0) { + return false; + } + int cols = board[0].length; + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + if (existRecursive(board, i, j, word, 0)) { + return true; + } + } + } + return false; +} + +private boolean existRecursive(char[][] board, int row, int col, String word, int index) { + if (row < 0 || row >= board.length || col < 0 || col >= board[0].length) { + return false; + } + if (board[row][col] != word.charAt(index)) { + return false; + } + if (index == word.length() - 1) { + return true; + } + char temp = board[row][col]; + board[row][col] = '$'; + boolean up = existRecursive(board, row - 1, col, word, index + 1); + if (up) { + board[row][col] = temp; //将 board 还原, 79 题中的代码没有还原,这里必须还原 + return true; + } + boolean down = existRecursive(board, row + 1, col, word, index + 1); + if (down) { + board[row][col] = temp;//将 board 还原, 79 题中的代码没有还原,这里必须还原 + return true; + } + boolean left = existRecursive(board, row, col - 1, word, index + 1); + if (left) { + board[row][col] = temp;//将 board 还原, 79 题中的代码没有还原,这里必须还原 + return true; + } + boolean right = existRecursive(board, row, col + 1, word, index + 1); + if (right) { + board[row][col] = temp;//将 board 还原, 79 题中的代码没有还原,这里必须还原 + return true; + } + board[row][col] = temp; + return false; +} +``` + +然后它竟然过了,竟然过了。。。我还以为这么暴力一定会暗藏玄机。不过为了尊重它是一道 hard 的题目,我就继续思考能不能优化下。 + +顺着上边的思路想,首先 [79 题 ](https://leetcode.wang/leetCode-79-Word-Search.html) 中判断单个单词是否能生成肯定不能优化了,不然之前肯定会写优化方法。那么继续优化的话,就只能去寻求不同 `word` 的之间的联系了。 + +什么意思呢? + +就是如果知道了某个单词能生成,那么对于后边将要判断的单词能不能提供些帮助呢? + +或者知道了某个单词不能生成,对于后边将要判断的单词能不能提供些帮助呢? + +想了想,只想到了一种情况。比如我们已经知道了 `basketboard` 能够在二维数组 `board` 中生成。那么它的所有前缀一定也能生成,比如 `basket` 一定能够生成。 + +说到前缀,自然而然的想到了之前的前缀树,这几天出现的频率也比较高,刚做的 [211 题](https://leetcode.wang/leetcode-211-Add-And-Search-Word-Data-structure-design.html) 也用到了。我们可以把能生成的 `word` 加入到前缀树中,然后再判断后边的单词前,先判断它是不是前缀树中某个单词的前缀。 + +当然如果单词 `A` 是 `B` 的前缀,那么 `A` 的长度肯定短一些,所以我们必须先判断了较长的单词 `B`,才能产生优化的效果。所以我们首先要把 `words` 按照单词的长度从大到小排序。 + +小弟不才,只想到了这一种联系,下边是代码,前缀树直接搬 [208 题](https://leetcode.wang/leetcode-208-Implement-Trie-Prefix-Tree.html) 的代码即可。 + +```java +//208 题前缀树代码 +class Trie { + class TrieNode { + TrieNode[] children; + boolean flag; + + public TrieNode() { + children = new TrieNode[26]; + flag = false; + for (int i = 0; i < 26; i++) { + children[i] = null; + } + } + } + + TrieNode root; + + /** Initialize your data structure here. */ + public Trie() { + root = new TrieNode(); + } + + /** Inserts a word into the trie. */ + public void insert(String word) { + char[] array = word.toCharArray(); + TrieNode cur = root; + for (int i = 0; i < array.length; i++) { + // 当前孩子是否存在 + if (cur.children[array[i] - 'a'] == null) { + cur.children[array[i] - 'a'] = new TrieNode(); + } + cur = cur.children[array[i] - 'a']; + } + // 当前节点代表结束 + cur.flag = true; + } + + /** + * Returns if there is any word in the trie that starts with the given + * prefix. + */ + public boolean startsWith(String prefix) { + char[] array = prefix.toCharArray(); + TrieNode cur = root; + for (int i = 0; i < array.length; i++) { + if (cur.children[array[i] - 'a'] == null) { + return false; + } + cur = cur.children[array[i] - 'a']; + } + return true; + } + +}; +//本题代码 +public List findWords(char[][] board, String[] words) { + //将单词的长度从大到小排序 + Arrays.sort(words, new Comparator() { + @Override + public int compare(String o1, String o2) { + return o2.length() - o1.length(); + } + + }); + Trie trie = new Trie(); + List res = new ArrayList<>(); + for (String word : words) { + //判断当前单词是否是已经完成的单词的前缀 + if (trie.startsWith(word)) { + res.add(word); + continue; + } + if (exist(board, word)) { + res.add(word); + //加入到前缀树中 + trie.insert(word); + } + } + return res; +} +//下边是 79 题的代码 +public boolean exist(char[][] board, String word) { + int rows = board.length; + if (rows == 0) { + return false; + } + int cols = board[0].length; + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + if (existRecursive(board, i, j, word, 0)) { + return true; + } + } + } + return false; +} + + +private boolean existRecursive(char[][] board, int row, int col, String word, int index) { + if (row < 0 || row >= board.length || col < 0 || col >= board[0].length) { + return false; + } + if (board[row][col] != word.charAt(index)) { + return false; + } + if (index == word.length() - 1) { + return true; + } + char temp = board[row][col]; + board[row][col] = '$'; + boolean up = existRecursive(board, row - 1, col, word, index + 1); + if (up) { + board[row][col] = temp; + return true; + } + boolean down = existRecursive(board, row + 1, col, word, index + 1); + if (down) { + board[row][col] = temp; + return true; + } + boolean left = existRecursive(board, row, col - 1, word, index + 1); + if (left) { + board[row][col] = temp; + return true; + } + boolean right = existRecursive(board, row, col + 1, word, index + 1); + if (right) { + board[row][col] = temp; + return true; + } + board[row][col] = temp; + return false; +} +``` + +然而事实是残酷的,对于 `leetcode` 的 `test cases` ,这个想法并没有带来速度上的提升。于是就去逛 `discuss` 了,也就是下边的解法二。 + +# 解法二 + +参考 [这里](https://leetcode.com/problems/word-search-ii/discuss/59780/Java-15ms-Easiest-Solution-(100.00))。 + +解法一中的想法是,`从 words 中依次选定一个单词` -> `从图中的每个位置出发,看能否找到这个单词` + +我们其实可以倒过来。`从图中的每个位置出发` -> `看遍历过程中是否遇到了 words 中的某个单词` + +遍历过程中判断是否遇到了某个单词,我们可以事先把所有单词存到前缀树中。这样的话,如果当前走的路径不是前缀树的前缀,我们就可以提前结束了。如果是前缀树的中的单词,我们就将其存到结果中。 + +至于实现的话,我们可以在遍历过程中,将当前路径的单词传进函数,然后判断当前路径构成的单词是否是在前缀树中出现即可。 + +这个想法可行,但不够好,因为每次都从前缀树中判断当前路径的单词,会带来重复的判断。比如先判断了 `an` 存在于前缀树中,接下来假如路径变成 `ang` ,判断它在不在前缀中,又需要判断一遍 `an` 。 + +因此,我们可以将前缀树融合到我们的算法中,递归中去传递前缀树的节点,判断当前节点的孩子是否为 `null`,如果是 `null` 说明当前前缀不存在,可以提前结束。如果不是 `null`,再判断当前节点是否是单词的结尾,如果是结尾直接将当前单词加入。 + +由于递归过程中没有加路径,所以我们改造一下前缀树的节点,将单词直接存入节点,这样的话就可以直接取到了。 + +干巴巴的文字可能不好理解,看一下下边的代码应该就明白了。 + +```java +//改造前缀树节点 +class TrieNode { + public TrieNode[] children; + public String word; //节点直接存当前的单词 + + public TrieNode() { + children = new TrieNode[26]; + word = null; + for (int i = 0; i < 26; i++) { + children[i] = null; + } + } +} +class Trie { + TrieNode root; + /** Initialize your data structure here. */ + public Trie() { + root = new TrieNode(); + } + + /** Inserts a word into the trie. */ + public void insert(String word) { + char[] array = word.toCharArray(); + TrieNode cur = root; + for (int i = 0; i < array.length; i++) { + // 当前孩子是否存在 + if (cur.children[array[i] - 'a'] == null) { + cur.children[array[i] - 'a'] = new TrieNode(); + } + cur = cur.children[array[i] - 'a']; + } + // 当前节点结束,存入当前单词 + cur.word = word; + } +}; + +class Solution { + public List findWords(char[][] board, String[] words) { + Trie trie = new Trie(); + //将所有单词存入前缀树中 + List res = new ArrayList<>(); + for (String word : words) { + trie.insert(word); + } + int rows = board.length; + if (rows == 0) { + return res; + } + int cols = board[0].length; + //从每个位置开始遍历 + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + existRecursive(board, i, j, trie.root, res); + } + } + return res; + } + + private void existRecursive(char[][] board, int row, int col, TrieNode node, List res) { + if (row < 0 || row >= board.length || col < 0 || col >= board[0].length) { + return; + } + char cur = board[row][col];//将要遍历的字母 + //当前节点遍历过或者将要遍历的字母在前缀树中不存在 + if (cur == '$' || node.children[cur - 'a'] == null) { + return; + } + node = node.children[cur - 'a']; + //判断当前节点是否是一个单词的结束 + if (node.word != null) { + //加入到结果中 + res.add(node.word); + //将当前单词置为 null,防止重复加入 + node.word = null; + } + char temp = board[row][col]; + //上下左右去遍历 + board[row][col] = '$'; + existRecursive(board, row - 1, col, node, res); + existRecursive(board, row + 1, col, node, res); + existRecursive(board, row, col - 1, node, res); + existRecursive(board, row, col + 1, node, res); + board[row][col] = temp; + } +} +``` + +结合代码就很好懂了,就是从每个位置对图做深度优先搜索,然后路径生成的字符串如果没有在前缀树中出现就提前结束,如果到了前缀树中某个单词的结束,就将当前单词加入即可。 + +# 总 + 受到前边的题目思维的限制,只想到了解法一,优化的话也没有很成功。其实把思路倒过来,解法二也就可以出来了,很有意思。 \ No newline at end of file diff --git a/leetcode-213-House-RobberII.md b/leetcode-213-House-RobberII.md index b68bbe17a..3dab3eb7b 100644 --- a/leetcode-213-House-RobberII.md +++ b/leetcode-213-House-RobberII.md @@ -1,108 +1,108 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/213.jpg) - -[198 题](https://leetcode.wang/leetcode-198-House-Robber.html) House Robber 的延续。一个数组,每个元素代表商店的存款,一个小偷晚上去偷商店,问最多能偷多少钱。有一个前提,不能偷相邻的商店,不然警报会响起。这道题的不同之处在于,商店是环形分布,所以第一家和最后一家也算作相邻商店。 - -# 解法一 - -这道题和 [198 题](https://leetcode.wang/leetcode-198-House-Robber.html) 的区别在题目描述中也指出来了,即偷了第一家就不能偷最后一家。 - -所以顺理成章,偷不偷第一家我们单独考虑一下即可。 - -偷第一家,也就是求出在前 `n - 1` 家中偷的最大收益,也就是不考虑最后一家的最大收益。 - -不偷第一家,也就是求第 `2` 家到最后一家中偷的最大收益,也就是不考虑第一家的最大收益。 - -然后只需要返回上边两个最大收益中的较大的即可。 - -图示的话就是下边的两种范围。 - -```java -X X X X X X -^ ^ - -X X X X X X - ^ ^ -``` - -我们看一下之前求全部商店的最大收益的代码,把最后优化的代码直接贴过来了,大家可以到 [198 题](https://leetcode.wang/leetcode-198-House-Robber.html) 看详细的。 - -```java -public int rob(int[] nums) { - int n = nums.length; - if (n == 0) { - return 0; - } - if (n == 1) { - return nums[0]; - } - int pre = 0; - int cur = nums[0]; - for (int i = 2; i <= n; i++) { - int temp = cur; - cur = Math.max(pre + nums[i - 1], cur); - pre = temp; - } - return cur; -} - -``` - -为了适应这道题的算法,我们可以对上边的代码进行改造。增加所偷的商店的范围的参数。 - -```java -public int robHelper(int[] nums, int start, int end) { - int n = nums.length; - if (n == 0) { - return 0; - } - if (n == 1) { - return nums[0]; - } - - int pre = 0; - int cur = nums[start]; - for (int i = start + 2; i <= end; i++) { - int temp = cur; - cur = Math.max(pre + nums[i - 1], cur); - pre = temp; - } - return cur; -} -``` - -有了上边的代码,这道题就非常好写了。 - -```java -public int rob(int[] nums) { - //考虑第一家 - int max1 = robHelper(nums, 0, nums.length - 1); - //不考虑第一家 - int max2 = robHelper(nums, 1, nums.length); - return Math.max(max1, max2); -} - -public int robHelper(int[] nums, int start, int end) { - int n = nums.length; - if (n == 0) { - return 0; - } - if (n == 1) { - return nums[0]; - } - - int pre = 0; - int cur = nums[start]; - for (int i = start + 2; i <= end; i++) { - int temp = cur; - cur = Math.max(pre + nums[i - 1], cur); - pre = temp; - } - return cur; -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/213.jpg) + +[198 题](https://leetcode.wang/leetcode-198-House-Robber.html) House Robber 的延续。一个数组,每个元素代表商店的存款,一个小偷晚上去偷商店,问最多能偷多少钱。有一个前提,不能偷相邻的商店,不然警报会响起。这道题的不同之处在于,商店是环形分布,所以第一家和最后一家也算作相邻商店。 + +# 解法一 + +这道题和 [198 题](https://leetcode.wang/leetcode-198-House-Robber.html) 的区别在题目描述中也指出来了,即偷了第一家就不能偷最后一家。 + +所以顺理成章,偷不偷第一家我们单独考虑一下即可。 + +偷第一家,也就是求出在前 `n - 1` 家中偷的最大收益,也就是不考虑最后一家的最大收益。 + +不偷第一家,也就是求第 `2` 家到最后一家中偷的最大收益,也就是不考虑第一家的最大收益。 + +然后只需要返回上边两个最大收益中的较大的即可。 + +图示的话就是下边的两种范围。 + +```java +X X X X X X +^ ^ + +X X X X X X + ^ ^ +``` + +我们看一下之前求全部商店的最大收益的代码,把最后优化的代码直接贴过来了,大家可以到 [198 题](https://leetcode.wang/leetcode-198-House-Robber.html) 看详细的。 + +```java +public int rob(int[] nums) { + int n = nums.length; + if (n == 0) { + return 0; + } + if (n == 1) { + return nums[0]; + } + int pre = 0; + int cur = nums[0]; + for (int i = 2; i <= n; i++) { + int temp = cur; + cur = Math.max(pre + nums[i - 1], cur); + pre = temp; + } + return cur; +} + +``` + +为了适应这道题的算法,我们可以对上边的代码进行改造。增加所偷的商店的范围的参数。 + +```java +public int robHelper(int[] nums, int start, int end) { + int n = nums.length; + if (n == 0) { + return 0; + } + if (n == 1) { + return nums[0]; + } + + int pre = 0; + int cur = nums[start]; + for (int i = start + 2; i <= end; i++) { + int temp = cur; + cur = Math.max(pre + nums[i - 1], cur); + pre = temp; + } + return cur; +} +``` + +有了上边的代码,这道题就非常好写了。 + +```java +public int rob(int[] nums) { + //考虑第一家 + int max1 = robHelper(nums, 0, nums.length - 1); + //不考虑第一家 + int max2 = robHelper(nums, 1, nums.length); + return Math.max(max1, max2); +} + +public int robHelper(int[] nums, int start, int end) { + int n = nums.length; + if (n == 0) { + return 0; + } + if (n == 1) { + return nums[0]; + } + + int pre = 0; + int cur = nums[start]; + for (int i = start + 2; i <= end; i++) { + int temp = cur; + cur = Math.max(pre + nums[i - 1], cur); + pre = temp; + } + return cur; +} +``` + +# 总 + 这道题通过分类的思想,成功将新问题化解到了已求解问题上,这个思想也经常遇到。 \ No newline at end of file diff --git a/leetcode-214-Shortest-Palindrome.md b/leetcode-214-Shortest-Palindrome.md index 2a1516a2d..1c755ab7e 100644 --- a/leetcode-214-Shortest-Palindrome.md +++ b/leetcode-214-Shortest-Palindrome.md @@ -1,633 +1,633 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/214.jpg) - -在字符串开头补充最少的字符,使得当前字符串成为回文串。 - -# 解法一 暴力 - -先判断整个字符串是不是回文串,如果是的话,就直接将当前字符串返回。不是的话,进行下一步。 - -判断去掉末尾 `1` 个字符的字符串是不是回文串,如果是的话,就将末尾的 `1` 个字符加到原字符串的头部返回。不是的话,进行下一步。 - -判断去掉末尾 `2` 个字符的字符串是不是回文串,如果是的话,就将末尾的 `2` 个字符倒置后加到原字符串的头部返回。不是的话,进行下一步。 - -判断去掉末尾 `3` 个字符的字符串是不是回文串,如果是的话,就将末尾的 `3` 个字符倒置后加到原字符串的头部返回。不是的话,进行下一步。 - -... - -直到判断去掉末尾的 `n - 1` 个字符,整个字符串剩下一个字符,把末尾的 `n - 1` 个字符倒置后加到原字符串的头部返回。 - -举个例子,比如字符串 `abbacd`。 - -```java -原字符串 abbacd -先判断 abbacd 是不是回文串, 发现不是, 执行下一步 -判断 abbac 是不是回文串, 发现不是, 执行下一步 -判断 abba 是不是回文串, 发现是,将末尾的 2 个字符 cd 倒置后加到原字符串的头部, -即 dcabbacd -``` - -代码的话,判断是否是回文串的话可以用 [125 题](https://leetcode.wang/leetcode-125-Valid-Palindrome.html) 的思想,利用双指针法。 - -```java -//判断是否是回文串, 传入字符串的范围 -public boolean isPalindromic(String s, int start, int end) { - char[] c = s.toCharArray(); - while (start < end) { - if (c[start] != c[end]) { - return false; - } - start++; - end--; - } - return true; -} - -public String shortestPalindrome(String s) { - int end = s.length() - 1; - //找到回文串的结尾, 用 end 标记 - for (; end > 0; end--) { - if (isPalindromic(s, 0, end)) { - break; - } - } - //将末尾的几个倒置然后加到原字符串开头 - return new StringBuilder(s.substring(end + 1)).reverse() + s; -} -``` - -遗憾的是超时了(前几天这个方法还能是过的,今天突然就超时了,官方应该是增加了 case)。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/214_2.jpg) - -上边字符串的长度达到了四万多,类似于这样的 `aaaaaaaaaaaaaacdaaaaaaaaaaaaaa`,每次调用判断字符串是否是回文串的时候,都需要判断很多次,最终时间复杂度达到了 `O(n²)`,造成了超时。 - -# 解法二 - -参考 [这里](https://leetcode.com/problems/shortest-palindrome/discuss/60098/My-7-lines-recursive-Java-solution)。 - -根据解法一,我们其实就是在**寻找从开头开始的最长回文串**(这个很关键,后边所有的解法都是基于这个了),然后将末尾的除去最长回文串部分的几个字符倒置后加到原字符串开头即可。 - -我们只需要两个指针, `i` 和 `j`,`i` 初始化为 `0`,`j` 初始化为字符串长度减 `1`。然后依次判断 `s[i]` 和 `s[j]` 是否相同,相同的话, `i` 就进行加 `1`,`j` 进行减 `1`。 `s[i]` 和 `s[j]` 不同的话,只将 `j` 进行减 `1`。 - -看几个例子。 - -```java -abbacde -a b b a c d e -^ ^ -i j -如上所示, s[i] != s[j], j-- - -a b b a c d e -^ ^ -i j -如上所示, s[i] != s[j], j-- - -a b b a c d e -^ ^ -i j -如上所示, s[i] != s[j], j-- - -a b b a c d e -^ ^ -i j -如上所示, s[i] == s[j], i++, j-- - -a b b a c d e - ^ ^ - i j -如上所示, s[i] == s[j], i++, j-- - -a b b a c d e - ^ ^ - j i -如上所示, s[i] == s[j], i++, j-- - -a b b a c d e -^ ^ -j i -如上所示, s[i] == s[j], i++, j-- - - a b b a c d e -^ ^ -j i -如上所示, j < 0, 结束循环。 -此时 i 指向最长回文串的下一个字符串,我们只需要把 i 到 最后的字符倒置加到开头即可。 -``` - -当然,上边是最理想的情况,如果 `j` 在最长回文串外提前出现了和 `i` 相同的字符会有影响吗? - -```java -abbacba -a b b a c b a -^ ^ -i j -如上所示, s[i] == s[j], i++, j-- - -a b b a c b a - ^ ^ - i j -如上所示, s[i] == s[j], i++, j-- - -a b b a c b a - ^ ^ - i j -如上所示, s[i] != s[j], j-- - -a b b a c b a - ^ ^ - i j -如上所示, s[i] != s[j], j-- - -a b b a c b a - ^ - i - j -如上所示, s[i] == s[j], i++, j-- - -a b b a c b a - ^ ^ - j i -如上所示, s[i] != s[j], j-- - -a b b a c b a -^ ^ -j i -如上所示, s[i] == s[j], i++, j-- - - a b b a c d e -^ ^ -j i -如上所示, j < 0, 结束循环。 -会发现此时 i 和之前一样, 依旧指向最长回文串的下一个字符,我们只需要把 i 到最后的字符倒置加到开头即可。 -``` - -可以看到上边的两种情况,只要 `j` 进入了最长回文子串,一定会使得 `i` 走出最长回文子串。所以我们可以利用双指针写一下代码了。 - -```java -public String shortestPalindrome(String s) { - int i = 0, j = s.length() - 1; - char[] c = s.toCharArray(); - while (j >= 0) { - if (i == j){ - continue; - } - if (c[i] == c[j]) { - i++; - } - j--; - } - //此时代表整个字符串是回文串 - if (i == s.length()) { - return s; - } - //后缀 - String suffix = s.substring(i); - //后缀倒置 - String reverse = new StringBuilder(suffix).reverse().toString(); - //加到开头 - return reverse + s; -} -``` - -看起来没什么问题,但还有一种情况,那就是 `i` 提前走出了最长回文子串,看下边的例子。 - -```java -ababbcefbbaba -a b a b b c e f b b a b a -^ ^ -i j - -i 和 j 同时移动, 一直是相等, 直到下边的情况 - -a b a b b c e f b b a b a - ^ ^ - i j - -然后继续移动, 最后就变成了下边的样子 - - a b a b b c e f b b a b a -^ ^ -j i - -会发现此时 0 到 i - 1 并不是一个回文串, 所以我们需要递归的去解决这个问题 -``` - -此时我们并没有找到最长回文串,但是我们可以肯定最长回文串一定在 `0` 到 `i` 之间,所以我们只需要递归的从`s[0, i)` 中继续寻找最长回文串即可。 - -因为上边的所有情况,都保证了 `i` 一定可以走出最长回文串,只不过可能超出一部分,所以用递归解决即可。代码的整体框架不需要改变。 - -```java -public String shortestPalindrome(String s) { - int i = 0, j = s.length() - 1; - char[] c = s.toCharArray(); - while (j >= 0) { - if (c[i] == c[j]) { - i++; - } - j--; - } - //此时代表整个字符串是回文串 - if (i == s.length()) { - return s; - } - //后缀 - String suffix = s.substring(i); - //后缀倒置 - String reverse = new StringBuilder(suffix).reverse().toString(); - //递归 s[0,i),寻找开头开始的最长回文串,将其余部分加到开头和结尾 - return reverse + shortestPalindrome(s.substring(0, i)) + suffix; -} -``` - -这个解法相对解法一会好一些,但对于某些极端情况,时间复杂度依旧会达到 `O(n²)`。比如下边的例子。 - -```java -aababababa -a a b a b a b a b a -^ ^ -i j -如上所示, s[i] == s[j], i++, j-- - -a a b a b a b a b a - ^ ^ - i j -如上所示, s[i] != s[j], j-- - -a a b a b a b a b a - ^ ^ - i j -如上所示, 此时 i 和 j 之间是一个回文串,所以 i 和 j 最终会变成下边的样子 - - a a b a b a b a b a -^ ^ -j i - -结合上边的代码,接下来去掉末尾字符,将对下边的字符串进行递归 - -a a b a b a b a - -此时会发现和最开始的结构一样,最终结果是去掉末尾的两个字符,继续对下边的字符串递归 - -a a b a b a - -此时会发现和最开始的结构一样,最终结果是去掉末尾的两个字符,继续对下边的字符串递归 - -a a b a - -此时会发现和最开始的结构一样,最终结果是去掉末尾的两个字符,继续对下边的字符串递归 - -a a - -此时是回文串了,递归结束 -``` - -所以每次递归只会减少两个字符,递归路径如下 - -```java -a a b a b a b a b a -a a b a b a b a -a a b a b a -a a b a -a a -``` - -如果初始字符串是上边的结构,即 `aaba...ba...ba...ba`,有几万个 `ba` 的话,和解法一一样会造成超时。由于 `leetcode` 中没有这种 `case` ,所以这个解法也就 `AC` 了。 - -# 解法三 - -寻找开头开始的最长回文串,我们回到更暴力的方法。 - -将原始字符串逆序,然后比较对应的子串即可判断是否是回文串。举个例子。 - -```java -abbacd - -原s: abbacd, 长度记为 n -逆r: dcabba, 长度记为 n - -判断 s[0,n) 和 r[0,n) -abbacd != dcabba - -判断 s[0,n - 1) 和 r[1,n) -abbac != cabba - -判断 s[0,n - 2) 和 r[2,n) -abba == abba - -从开头开始的最长回文串也就找到了, 接下来只需要使用之前的方法。 -将末尾不是回文串的部分倒置加到原字符串开头即可。 -``` - -代码的话,也很好写了。 - -```java -public String shortestPalindrome(String s) { - String r = new StringBuilder(s).reverse().toString(); - int n = s.length(); - int i = 0; - for (; i < n; i++) { - if (s.substring(0, n - i).equals(r.substring(i))) { - break; - } - } - return new StringBuilder(s.substring(n - i)).reverse() + s; -} -``` - -然后它竟然 `AC` 了,当然这个时间复杂度是 `O(n²)`,之所以通过了,还是取决于 `test cases` 。 - -# 解法四 - -在解法三倒置的基础上进行一下优化,参考 [这里](https://leetcode.com/problems/shortest-palindrome/discuss/60153/8-line-O(n)-method-using-Rabin-Karp-rolling-hash)。 - -用到了字符串匹配算法 RK 算法的思想,也就是滚动哈希。 - -解法三中,每次比较两个字符串是否相等都需要一个字符一个字符比较,如果我们把字符串通过 `hash` 算法映射到数字,就可以只判断数字是否相等即可。 - -而 `hash` 算法,这里的话,我们将 `a` 看做 `1`,`b` 看做 `2` ... 以此类推,然后把字符串看做是 `26` 进制的一个数字,将其转为十进制后的值作为 `hash` 值。 - -也许需要一些进制转换的知识,可以参考 [这里](https://www.zhihu.com/question/357414448/answer/949086536)。 - -举个例子,对于 `abcd`。 - -```java - a b c d - 1 2 3 4 -26^3 26^2 26 1 -``` - -那么 `abcd` 的 `hash` 值就是 $$4+3*26+2*26^2+1*26^3$$。 - -这样做的好处是,我们可以通过前一个字符串的 `hash` 值,算出当前字符串的 `hash` 值。 - -举个例子。 - -对于字符串 `abb` ,如果我们知道了它的 `hash` 值是 `x` ,那么对于 `abba` 的 `hash` 值,因为新增加的数字 `a` 对应 `1`,所以 `abba` 的 `hash` 值就是 `(x * 26 + 1)`。 - -所以代码可以写成下边的样子。 - -```java -public String shortestPalindrome(String s) { - int n = s.length(), pos = -1; - int b = 26; // 基数 - int pow = 1; // 为了方便计算倒置字符串的 hash 值 - char[] c = s.toCharArray(); - int hash1 = 0, hash2 = 0; - for (int i = 0; i < n; i++, pow = pow * b) { - hash1 = hash1 * b + (c[i] - 'a' + 1); - // 倒置字符串的 hash 值, 新增的字符要放到最高位 - hash2 = hash2 + (c[i] - 'a' + 1) * pow; - if (hash1 == hash2) { - pos = i; - } - } - return new StringBuilder(s.substring(pos + 1)).reverse() + s; -} -``` - -理论上,上边的代码是可行的,但会发现出现了 `wrong answer`。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/214_3.jpg) - -我猜测下原因,不是十分确定。 - -最直接的问题肯定是由于我们用 `int` 存储 `hash` 值,所以一定会出现溢出的情况。溢出以后,接着带来了 `hash` 冲突,从而使得相同的 `hash` 值,但是字符串并不相同。 - -基于上边的分析,我们可以在 `pos = i` 之前判断一下当前是否是回文串。 - -```java -public boolean isPalindromic(String s, int start, int end) { - char[] c = s.toCharArray(); - while (start < end) { - if (c[start] != c[end]) { - return false; - } - start++; - end--; - } - return true; -} -public String shortestPalindrome(String s) { - int n = s.length(), pos = -1; - int b = 26; // 基数 - int pow = 1; // 为了方便计算倒置字符串的 hash 值 - char[] c = s.toCharArray(); - int hash1 = 0, hash2 = 0; - for (int i = 0; i < n; i++, pow = pow * b) { - hash1 = hash1 * b + (c[i] - 'a' + 1); - // 倒置字符串的 hash 值, 新增的字符要放到最高位 - hash2 = hash2 + (c[i] - 'a' + 1) * pow; - if (hash1 == hash2) { - //确认下当前是否是回文串 - if (isPalindromic(s,0,i)) { - pos = i; - } - } - } - return new StringBuilder(s.substring(pos + 1)).reverse() + s; -} -``` - -但是超时了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/214_2.jpg) - -然后就是换 `hash` 算法,我们可以把每次的结果取模,这样就不会溢出了。 - -```java -public String shortestPalindrome(String s) { - int n = s.length(), pos = -1; - int b = 26; // 基数 - int pow = 1; // 为了方便计算倒置字符串的 hash 值 - char[] c = s.toCharArray(); - int hash1 = 0, hash2 = 0; - int mod = 1000000; - for (int i = 0; i < n; i++, pow = (pow * b) % mod) { - hash1 = (hash1 * b + (c[i] - 'a' + 1)) % mod; - // 倒置字符串的 hash 值, 新增的字符要放到最高位 - hash2 = (hash2 + (c[i] - 'a' + 1) * pow)% mod; - if (hash1 == hash2) { - pos = i; - } - } - return new StringBuilder(s.substring(pos + 1)).reverse() + s; -} -``` - -虽然这种方法 `AC` 了,但我觉得是侥幸的,我觉得即使每次取模,并不能保证不会出现 `hash` 冲突,只是当前的 `test case` 没有出现 `hash` 冲突。当然这是我的想法,并不是很确定,大家有其他想法欢迎和我交流。 - -感谢 @[franklinqin0](https://www.zhihu.com/people/franklinqin7) 指出,上边确认当前是否是回文串的时候,我们调用了 `isPalindromic` ,但超时了,这里的话我们还可以和它的逆置字符串进行比较。 - -```java -public String shortestPalindrome(String s) { - int n = s.length(), pos = -1; - int b = 26; // 基数 - int pow = 1; // 为了方便计算倒置字符串的 hash 值 - char[] c = s.toCharArray(); - String rev = new StringBuilder(s).reverse().toString(); - int hash1 = 0, hash2 = 0; - for (int i = 0; i < n; i++, pow = pow * b) { - hash1 = hash1 * b + (c[i] - 'a' + 1); - // 倒置字符串的 hash 值, 新增的字符要放到最高位 - hash2 = hash2 + (c[i] - 'a' + 1) * pow; - if (hash1 == hash2) { - if (s.substring(0, i + 1).equals(rev.substring(n - i - 1))) { - pos = i; - } - } - } - return new StringBuilder(s.substring(pos + 1)).reverse() + s; -} -``` - -这样做的话就不会超时了,但如果分析时间复杂度的话其实是一样的,很神奇。 - -# 解法五 - -参考 [这里](https://leetcode.com/problems/shortest-palindrome/discuss/60113/Clean-KMP-solution-with-super-detailed-explanation)。 - -这个解法的前提是你熟悉另一种字符串匹配算法,即 KMP 算法。推荐两个链接,大家可以先学习一下,我就不多说了。KMP 算法代码简单,但理解求 `next` 数组的话,确实有些麻烦。 - -[http://jakeboxer.com/blog/2009/12/13/the-knuth-morris-pratt-algorithm-in-my-own-words/](http://jakeboxer.com/blog/2009/12/13/the-knuth-morris-pratt-algorithm-in-my-own-words/) - -[https://learnku.com/articles/10622/introduction-of-kmp-algorithm-and-derivation-of-next-array](https://learnku.com/articles/10622/introduction-of-kmp-algorithm-and-derivation-of-next-array) - -如果熟悉了 KMP 算法,下边就简单了。 - -再回想一下解法三,倒置字符串的思路,依次比较对应子串。 - -```java -abbacd - -原s: abbacd, 长度记为 n -逆r: dcabba, 长度记为 n - -我们把两个字符串写在一起 -abbacd dcabba - -判断 abbacd 和 dcabba 是否相等 -判断 abbac 和 cabba 是否相等 -判断 abba 和 abba 是否相等 -``` - -如果我们把 `abbacd dcabba`看成一个字符串,中间加上一个分隔符 `#`,`abbacd#dcabba`。 - -回味一下上边的三条判断,判断 XXX 和 XXX 是否相等,按列看一下。 - -左半部分 `abbacd`,`abbac` , `abba` 其实就是 `abbacd#dcabba` 的一些前缀。 - -右半部分`dcabba`,`cabba`,`abba` 其实就是 `abbacd#dcabba` 的一些后缀。 - -寻找前缀和后缀相等。 - -想一想 `KMP` 算法,这不就是 `next` 数组做的事情吗。 - -而我们中间加了分隔符,也就保证了前缀和后缀相等时,前缀一定在 `abbacd` 中。 - -换句话说,我们如果求出了 `abbacd#dcabba` 的 `next` 数组,因为我们构造的字符串后缀就是原字符串的倒置,前缀后缀相等时,也就意味着当前前缀是一个回文串,而 `next` 数组是寻求最长的前缀,我们也就找到了开头开始的最长回文串。 - -因为 `next` 数组的含义并不统一,但 `KMP` 算法本质上都是一样的,所以下边的代码仅供参考。 - -我的 `next` 数组 `next[i]` 所考虑的对应字符串不包含 `s[i]`。 - -```java -public String shortestPalindrome(String s) { - String ss = s + '#' + new StringBuilder(s).reverse(); - int max = getLastNext(ss); - return new StringBuilder(s.substring(max)).reverse() + s; -} - -//返回 next 数组的最后一个值 -public int getLastNext(String s) { - int n = s.length(); - char[] c = s.toCharArray(); - int[] next = new int[n + 1]; - next[0] = -1; - next[1] = 0; - int k = 0; - int i = 2; - while (i <= n) { - if (k == -1 || c[i - 1] == c[k]) { - next[i] = k + 1; - k++; - i++; - } else { - k = next[k]; - } - } - return next[n]; -} -``` - -# 解法六 - -参考 [这里](https://leetcode.com/problems/shortest-palindrome/discuss/60188/My-C%2B%2B-O(n)-solution-based-on-Manacher's-algorithm) 。 - -大家还记得 [第 5 题](https://leetcode.wang/leetCode-5-Longest-Palindromic-Substring.html) 吗?求最长回文子串。 - -这里我们已经把题目转换成了求开头开始的最长回文子串,很明显这个问题只是第 5 题的子问题了。但这道题时间复杂度差不多只有 `O(n)` 才会通过。这就必须使用 [第 5 题](https://leetcode.wang/leetCode-5-Longest-Palindromic-Substring.html) 介绍的马拉车算法了。 - -直接把马拉车算法粘贴过来即可,然后在最后稍微修改一下即可。大家不熟悉的话,可以参考 [一文让你彻底明白马拉车算法](https://zhuanlan.zhihu.com/p/70532099)。 - -```java -public String preProcess(String s) { - int n = s.length(); - if (n == 0) { - return "^$"; - } - String ret = "^"; - for (int i = 0; i < n; i++) - ret += "#" + s.charAt(i); - ret += "#$"; - return ret; -} - -// 马拉车算法 -public String shortestPalindrome(String s) { - String T = preProcess(s); - int n = T.length(); - int[] P = new int[n]; - int C = 0, R = 0; - for (int i = 1; i < n - 1; i++) { - int i_mirror = 2 * C - i; - if (R > i) { - P[i] = Math.min(R - i, P[i_mirror]);// 防止超出 R - } else { - P[i] = 0;// 等于 R 的情况 - } - - // 碰到之前讲的三种情况时候,需要利用中心扩展法 - while (T.charAt(i + 1 + P[i]) == T.charAt(i - 1 - P[i])) { - P[i]++; - } - - // 判断是否需要更新 R - if (i + P[i] > R) { - C = i; - R = i + P[i]; - } - - } - - //这里的话需要修改 - int maxLen = 0; - int centerIndex = 0; - for (int i = 1; i < n - 1; i++) { - int start = (i - P[i]) / 2; - //我们要判断当前回文串是不是开头是不是从 0 开始的 - if (start == 0) { - maxLen = P[i] > maxLen ? P[i] : maxLen; - } - } - return new StringBuilder(s.substring(maxLen)).reverse() + s; -} -``` - -# 总 - -这道题太强了,六种解法,各有特色。把这道题捋下来着实不易,涉及到很多算法。但懂了之后确实心旷神怡。 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/214.jpg) + +在字符串开头补充最少的字符,使得当前字符串成为回文串。 + +# 解法一 暴力 + +先判断整个字符串是不是回文串,如果是的话,就直接将当前字符串返回。不是的话,进行下一步。 + +判断去掉末尾 `1` 个字符的字符串是不是回文串,如果是的话,就将末尾的 `1` 个字符加到原字符串的头部返回。不是的话,进行下一步。 + +判断去掉末尾 `2` 个字符的字符串是不是回文串,如果是的话,就将末尾的 `2` 个字符倒置后加到原字符串的头部返回。不是的话,进行下一步。 + +判断去掉末尾 `3` 个字符的字符串是不是回文串,如果是的话,就将末尾的 `3` 个字符倒置后加到原字符串的头部返回。不是的话,进行下一步。 + +... + +直到判断去掉末尾的 `n - 1` 个字符,整个字符串剩下一个字符,把末尾的 `n - 1` 个字符倒置后加到原字符串的头部返回。 + +举个例子,比如字符串 `abbacd`。 + +```java +原字符串 abbacd +先判断 abbacd 是不是回文串, 发现不是, 执行下一步 +判断 abbac 是不是回文串, 发现不是, 执行下一步 +判断 abba 是不是回文串, 发现是,将末尾的 2 个字符 cd 倒置后加到原字符串的头部, +即 dcabbacd +``` + +代码的话,判断是否是回文串的话可以用 [125 题](https://leetcode.wang/leetcode-125-Valid-Palindrome.html) 的思想,利用双指针法。 + +```java +//判断是否是回文串, 传入字符串的范围 +public boolean isPalindromic(String s, int start, int end) { + char[] c = s.toCharArray(); + while (start < end) { + if (c[start] != c[end]) { + return false; + } + start++; + end--; + } + return true; +} + +public String shortestPalindrome(String s) { + int end = s.length() - 1; + //找到回文串的结尾, 用 end 标记 + for (; end > 0; end--) { + if (isPalindromic(s, 0, end)) { + break; + } + } + //将末尾的几个倒置然后加到原字符串开头 + return new StringBuilder(s.substring(end + 1)).reverse() + s; +} +``` + +遗憾的是超时了(前几天这个方法还能是过的,今天突然就超时了,官方应该是增加了 case)。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/214_2.jpg) + +上边字符串的长度达到了四万多,类似于这样的 `aaaaaaaaaaaaaacdaaaaaaaaaaaaaa`,每次调用判断字符串是否是回文串的时候,都需要判断很多次,最终时间复杂度达到了 `O(n²)`,造成了超时。 + +# 解法二 + +参考 [这里](https://leetcode.com/problems/shortest-palindrome/discuss/60098/My-7-lines-recursive-Java-solution)。 + +根据解法一,我们其实就是在**寻找从开头开始的最长回文串**(这个很关键,后边所有的解法都是基于这个了),然后将末尾的除去最长回文串部分的几个字符倒置后加到原字符串开头即可。 + +我们只需要两个指针, `i` 和 `j`,`i` 初始化为 `0`,`j` 初始化为字符串长度减 `1`。然后依次判断 `s[i]` 和 `s[j]` 是否相同,相同的话, `i` 就进行加 `1`,`j` 进行减 `1`。 `s[i]` 和 `s[j]` 不同的话,只将 `j` 进行减 `1`。 + +看几个例子。 + +```java +abbacde +a b b a c d e +^ ^ +i j +如上所示, s[i] != s[j], j-- + +a b b a c d e +^ ^ +i j +如上所示, s[i] != s[j], j-- + +a b b a c d e +^ ^ +i j +如上所示, s[i] != s[j], j-- + +a b b a c d e +^ ^ +i j +如上所示, s[i] == s[j], i++, j-- + +a b b a c d e + ^ ^ + i j +如上所示, s[i] == s[j], i++, j-- + +a b b a c d e + ^ ^ + j i +如上所示, s[i] == s[j], i++, j-- + +a b b a c d e +^ ^ +j i +如上所示, s[i] == s[j], i++, j-- + + a b b a c d e +^ ^ +j i +如上所示, j < 0, 结束循环。 +此时 i 指向最长回文串的下一个字符串,我们只需要把 i 到 最后的字符倒置加到开头即可。 +``` + +当然,上边是最理想的情况,如果 `j` 在最长回文串外提前出现了和 `i` 相同的字符会有影响吗? + +```java +abbacba +a b b a c b a +^ ^ +i j +如上所示, s[i] == s[j], i++, j-- + +a b b a c b a + ^ ^ + i j +如上所示, s[i] == s[j], i++, j-- + +a b b a c b a + ^ ^ + i j +如上所示, s[i] != s[j], j-- + +a b b a c b a + ^ ^ + i j +如上所示, s[i] != s[j], j-- + +a b b a c b a + ^ + i + j +如上所示, s[i] == s[j], i++, j-- + +a b b a c b a + ^ ^ + j i +如上所示, s[i] != s[j], j-- + +a b b a c b a +^ ^ +j i +如上所示, s[i] == s[j], i++, j-- + + a b b a c d e +^ ^ +j i +如上所示, j < 0, 结束循环。 +会发现此时 i 和之前一样, 依旧指向最长回文串的下一个字符,我们只需要把 i 到最后的字符倒置加到开头即可。 +``` + +可以看到上边的两种情况,只要 `j` 进入了最长回文子串,一定会使得 `i` 走出最长回文子串。所以我们可以利用双指针写一下代码了。 + +```java +public String shortestPalindrome(String s) { + int i = 0, j = s.length() - 1; + char[] c = s.toCharArray(); + while (j >= 0) { + if (i == j){ + continue; + } + if (c[i] == c[j]) { + i++; + } + j--; + } + //此时代表整个字符串是回文串 + if (i == s.length()) { + return s; + } + //后缀 + String suffix = s.substring(i); + //后缀倒置 + String reverse = new StringBuilder(suffix).reverse().toString(); + //加到开头 + return reverse + s; +} +``` + +看起来没什么问题,但还有一种情况,那就是 `i` 提前走出了最长回文子串,看下边的例子。 + +```java +ababbcefbbaba +a b a b b c e f b b a b a +^ ^ +i j + +i 和 j 同时移动, 一直是相等, 直到下边的情况 + +a b a b b c e f b b a b a + ^ ^ + i j + +然后继续移动, 最后就变成了下边的样子 + + a b a b b c e f b b a b a +^ ^ +j i + +会发现此时 0 到 i - 1 并不是一个回文串, 所以我们需要递归的去解决这个问题 +``` + +此时我们并没有找到最长回文串,但是我们可以肯定最长回文串一定在 `0` 到 `i` 之间,所以我们只需要递归的从`s[0, i)` 中继续寻找最长回文串即可。 + +因为上边的所有情况,都保证了 `i` 一定可以走出最长回文串,只不过可能超出一部分,所以用递归解决即可。代码的整体框架不需要改变。 + +```java +public String shortestPalindrome(String s) { + int i = 0, j = s.length() - 1; + char[] c = s.toCharArray(); + while (j >= 0) { + if (c[i] == c[j]) { + i++; + } + j--; + } + //此时代表整个字符串是回文串 + if (i == s.length()) { + return s; + } + //后缀 + String suffix = s.substring(i); + //后缀倒置 + String reverse = new StringBuilder(suffix).reverse().toString(); + //递归 s[0,i),寻找开头开始的最长回文串,将其余部分加到开头和结尾 + return reverse + shortestPalindrome(s.substring(0, i)) + suffix; +} +``` + +这个解法相对解法一会好一些,但对于某些极端情况,时间复杂度依旧会达到 `O(n²)`。比如下边的例子。 + +```java +aababababa +a a b a b a b a b a +^ ^ +i j +如上所示, s[i] == s[j], i++, j-- + +a a b a b a b a b a + ^ ^ + i j +如上所示, s[i] != s[j], j-- + +a a b a b a b a b a + ^ ^ + i j +如上所示, 此时 i 和 j 之间是一个回文串,所以 i 和 j 最终会变成下边的样子 + + a a b a b a b a b a +^ ^ +j i + +结合上边的代码,接下来去掉末尾字符,将对下边的字符串进行递归 + +a a b a b a b a + +此时会发现和最开始的结构一样,最终结果是去掉末尾的两个字符,继续对下边的字符串递归 + +a a b a b a + +此时会发现和最开始的结构一样,最终结果是去掉末尾的两个字符,继续对下边的字符串递归 + +a a b a + +此时会发现和最开始的结构一样,最终结果是去掉末尾的两个字符,继续对下边的字符串递归 + +a a + +此时是回文串了,递归结束 +``` + +所以每次递归只会减少两个字符,递归路径如下 + +```java +a a b a b a b a b a +a a b a b a b a +a a b a b a +a a b a +a a +``` + +如果初始字符串是上边的结构,即 `aaba...ba...ba...ba`,有几万个 `ba` 的话,和解法一一样会造成超时。由于 `leetcode` 中没有这种 `case` ,所以这个解法也就 `AC` 了。 + +# 解法三 + +寻找开头开始的最长回文串,我们回到更暴力的方法。 + +将原始字符串逆序,然后比较对应的子串即可判断是否是回文串。举个例子。 + +```java +abbacd + +原s: abbacd, 长度记为 n +逆r: dcabba, 长度记为 n + +判断 s[0,n) 和 r[0,n) +abbacd != dcabba + +判断 s[0,n - 1) 和 r[1,n) +abbac != cabba + +判断 s[0,n - 2) 和 r[2,n) +abba == abba + +从开头开始的最长回文串也就找到了, 接下来只需要使用之前的方法。 +将末尾不是回文串的部分倒置加到原字符串开头即可。 +``` + +代码的话,也很好写了。 + +```java +public String shortestPalindrome(String s) { + String r = new StringBuilder(s).reverse().toString(); + int n = s.length(); + int i = 0; + for (; i < n; i++) { + if (s.substring(0, n - i).equals(r.substring(i))) { + break; + } + } + return new StringBuilder(s.substring(n - i)).reverse() + s; +} +``` + +然后它竟然 `AC` 了,当然这个时间复杂度是 `O(n²)`,之所以通过了,还是取决于 `test cases` 。 + +# 解法四 + +在解法三倒置的基础上进行一下优化,参考 [这里](https://leetcode.com/problems/shortest-palindrome/discuss/60153/8-line-O(n)-method-using-Rabin-Karp-rolling-hash)。 + +用到了字符串匹配算法 RK 算法的思想,也就是滚动哈希。 + +解法三中,每次比较两个字符串是否相等都需要一个字符一个字符比较,如果我们把字符串通过 `hash` 算法映射到数字,就可以只判断数字是否相等即可。 + +而 `hash` 算法,这里的话,我们将 `a` 看做 `1`,`b` 看做 `2` ... 以此类推,然后把字符串看做是 `26` 进制的一个数字,将其转为十进制后的值作为 `hash` 值。 + +也许需要一些进制转换的知识,可以参考 [这里](https://www.zhihu.com/question/357414448/answer/949086536)。 + +举个例子,对于 `abcd`。 + +```java + a b c d + 1 2 3 4 +26^3 26^2 26 1 +``` + +那么 `abcd` 的 `hash` 值就是 $$4+3*26+2*26^2+1*26^3$$。 + +这样做的好处是,我们可以通过前一个字符串的 `hash` 值,算出当前字符串的 `hash` 值。 + +举个例子。 + +对于字符串 `abb` ,如果我们知道了它的 `hash` 值是 `x` ,那么对于 `abba` 的 `hash` 值,因为新增加的数字 `a` 对应 `1`,所以 `abba` 的 `hash` 值就是 `(x * 26 + 1)`。 + +所以代码可以写成下边的样子。 + +```java +public String shortestPalindrome(String s) { + int n = s.length(), pos = -1; + int b = 26; // 基数 + int pow = 1; // 为了方便计算倒置字符串的 hash 值 + char[] c = s.toCharArray(); + int hash1 = 0, hash2 = 0; + for (int i = 0; i < n; i++, pow = pow * b) { + hash1 = hash1 * b + (c[i] - 'a' + 1); + // 倒置字符串的 hash 值, 新增的字符要放到最高位 + hash2 = hash2 + (c[i] - 'a' + 1) * pow; + if (hash1 == hash2) { + pos = i; + } + } + return new StringBuilder(s.substring(pos + 1)).reverse() + s; +} +``` + +理论上,上边的代码是可行的,但会发现出现了 `wrong answer`。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/214_3.jpg) + +我猜测下原因,不是十分确定。 + +最直接的问题肯定是由于我们用 `int` 存储 `hash` 值,所以一定会出现溢出的情况。溢出以后,接着带来了 `hash` 冲突,从而使得相同的 `hash` 值,但是字符串并不相同。 + +基于上边的分析,我们可以在 `pos = i` 之前判断一下当前是否是回文串。 + +```java +public boolean isPalindromic(String s, int start, int end) { + char[] c = s.toCharArray(); + while (start < end) { + if (c[start] != c[end]) { + return false; + } + start++; + end--; + } + return true; +} +public String shortestPalindrome(String s) { + int n = s.length(), pos = -1; + int b = 26; // 基数 + int pow = 1; // 为了方便计算倒置字符串的 hash 值 + char[] c = s.toCharArray(); + int hash1 = 0, hash2 = 0; + for (int i = 0; i < n; i++, pow = pow * b) { + hash1 = hash1 * b + (c[i] - 'a' + 1); + // 倒置字符串的 hash 值, 新增的字符要放到最高位 + hash2 = hash2 + (c[i] - 'a' + 1) * pow; + if (hash1 == hash2) { + //确认下当前是否是回文串 + if (isPalindromic(s,0,i)) { + pos = i; + } + } + } + return new StringBuilder(s.substring(pos + 1)).reverse() + s; +} +``` + +但是超时了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/214_2.jpg) + +然后就是换 `hash` 算法,我们可以把每次的结果取模,这样就不会溢出了。 + +```java +public String shortestPalindrome(String s) { + int n = s.length(), pos = -1; + int b = 26; // 基数 + int pow = 1; // 为了方便计算倒置字符串的 hash 值 + char[] c = s.toCharArray(); + int hash1 = 0, hash2 = 0; + int mod = 1000000; + for (int i = 0; i < n; i++, pow = (pow * b) % mod) { + hash1 = (hash1 * b + (c[i] - 'a' + 1)) % mod; + // 倒置字符串的 hash 值, 新增的字符要放到最高位 + hash2 = (hash2 + (c[i] - 'a' + 1) * pow)% mod; + if (hash1 == hash2) { + pos = i; + } + } + return new StringBuilder(s.substring(pos + 1)).reverse() + s; +} +``` + +虽然这种方法 `AC` 了,但我觉得是侥幸的,我觉得即使每次取模,并不能保证不会出现 `hash` 冲突,只是当前的 `test case` 没有出现 `hash` 冲突。当然这是我的想法,并不是很确定,大家有其他想法欢迎和我交流。 + +感谢 @[franklinqin0](https://www.zhihu.com/people/franklinqin7) 指出,上边确认当前是否是回文串的时候,我们调用了 `isPalindromic` ,但超时了,这里的话我们还可以和它的逆置字符串进行比较。 + +```java +public String shortestPalindrome(String s) { + int n = s.length(), pos = -1; + int b = 26; // 基数 + int pow = 1; // 为了方便计算倒置字符串的 hash 值 + char[] c = s.toCharArray(); + String rev = new StringBuilder(s).reverse().toString(); + int hash1 = 0, hash2 = 0; + for (int i = 0; i < n; i++, pow = pow * b) { + hash1 = hash1 * b + (c[i] - 'a' + 1); + // 倒置字符串的 hash 值, 新增的字符要放到最高位 + hash2 = hash2 + (c[i] - 'a' + 1) * pow; + if (hash1 == hash2) { + if (s.substring(0, i + 1).equals(rev.substring(n - i - 1))) { + pos = i; + } + } + } + return new StringBuilder(s.substring(pos + 1)).reverse() + s; +} +``` + +这样做的话就不会超时了,但如果分析时间复杂度的话其实是一样的,很神奇。 + +# 解法五 + +参考 [这里](https://leetcode.com/problems/shortest-palindrome/discuss/60113/Clean-KMP-solution-with-super-detailed-explanation)。 + +这个解法的前提是你熟悉另一种字符串匹配算法,即 KMP 算法。推荐两个链接,大家可以先学习一下,我就不多说了。KMP 算法代码简单,但理解求 `next` 数组的话,确实有些麻烦。 + +[http://jakeboxer.com/blog/2009/12/13/the-knuth-morris-pratt-algorithm-in-my-own-words/](http://jakeboxer.com/blog/2009/12/13/the-knuth-morris-pratt-algorithm-in-my-own-words/) + +[https://learnku.com/articles/10622/introduction-of-kmp-algorithm-and-derivation-of-next-array](https://learnku.com/articles/10622/introduction-of-kmp-algorithm-and-derivation-of-next-array) + +如果熟悉了 KMP 算法,下边就简单了。 + +再回想一下解法三,倒置字符串的思路,依次比较对应子串。 + +```java +abbacd + +原s: abbacd, 长度记为 n +逆r: dcabba, 长度记为 n + +我们把两个字符串写在一起 +abbacd dcabba + +判断 abbacd 和 dcabba 是否相等 +判断 abbac 和 cabba 是否相等 +判断 abba 和 abba 是否相等 +``` + +如果我们把 `abbacd dcabba`看成一个字符串,中间加上一个分隔符 `#`,`abbacd#dcabba`。 + +回味一下上边的三条判断,判断 XXX 和 XXX 是否相等,按列看一下。 + +左半部分 `abbacd`,`abbac` , `abba` 其实就是 `abbacd#dcabba` 的一些前缀。 + +右半部分`dcabba`,`cabba`,`abba` 其实就是 `abbacd#dcabba` 的一些后缀。 + +寻找前缀和后缀相等。 + +想一想 `KMP` 算法,这不就是 `next` 数组做的事情吗。 + +而我们中间加了分隔符,也就保证了前缀和后缀相等时,前缀一定在 `abbacd` 中。 + +换句话说,我们如果求出了 `abbacd#dcabba` 的 `next` 数组,因为我们构造的字符串后缀就是原字符串的倒置,前缀后缀相等时,也就意味着当前前缀是一个回文串,而 `next` 数组是寻求最长的前缀,我们也就找到了开头开始的最长回文串。 + +因为 `next` 数组的含义并不统一,但 `KMP` 算法本质上都是一样的,所以下边的代码仅供参考。 + +我的 `next` 数组 `next[i]` 所考虑的对应字符串不包含 `s[i]`。 + +```java +public String shortestPalindrome(String s) { + String ss = s + '#' + new StringBuilder(s).reverse(); + int max = getLastNext(ss); + return new StringBuilder(s.substring(max)).reverse() + s; +} + +//返回 next 数组的最后一个值 +public int getLastNext(String s) { + int n = s.length(); + char[] c = s.toCharArray(); + int[] next = new int[n + 1]; + next[0] = -1; + next[1] = 0; + int k = 0; + int i = 2; + while (i <= n) { + if (k == -1 || c[i - 1] == c[k]) { + next[i] = k + 1; + k++; + i++; + } else { + k = next[k]; + } + } + return next[n]; +} +``` + +# 解法六 + +参考 [这里](https://leetcode.com/problems/shortest-palindrome/discuss/60188/My-C%2B%2B-O(n)-solution-based-on-Manacher's-algorithm) 。 + +大家还记得 [第 5 题](https://leetcode.wang/leetCode-5-Longest-Palindromic-Substring.html) 吗?求最长回文子串。 + +这里我们已经把题目转换成了求开头开始的最长回文子串,很明显这个问题只是第 5 题的子问题了。但这道题时间复杂度差不多只有 `O(n)` 才会通过。这就必须使用 [第 5 题](https://leetcode.wang/leetCode-5-Longest-Palindromic-Substring.html) 介绍的马拉车算法了。 + +直接把马拉车算法粘贴过来即可,然后在最后稍微修改一下即可。大家不熟悉的话,可以参考 [一文让你彻底明白马拉车算法](https://zhuanlan.zhihu.com/p/70532099)。 + +```java +public String preProcess(String s) { + int n = s.length(); + if (n == 0) { + return "^$"; + } + String ret = "^"; + for (int i = 0; i < n; i++) + ret += "#" + s.charAt(i); + ret += "#$"; + return ret; +} + +// 马拉车算法 +public String shortestPalindrome(String s) { + String T = preProcess(s); + int n = T.length(); + int[] P = new int[n]; + int C = 0, R = 0; + for (int i = 1; i < n - 1; i++) { + int i_mirror = 2 * C - i; + if (R > i) { + P[i] = Math.min(R - i, P[i_mirror]);// 防止超出 R + } else { + P[i] = 0;// 等于 R 的情况 + } + + // 碰到之前讲的三种情况时候,需要利用中心扩展法 + while (T.charAt(i + 1 + P[i]) == T.charAt(i - 1 - P[i])) { + P[i]++; + } + + // 判断是否需要更新 R + if (i + P[i] > R) { + C = i; + R = i + P[i]; + } + + } + + //这里的话需要修改 + int maxLen = 0; + int centerIndex = 0; + for (int i = 1; i < n - 1; i++) { + int start = (i - P[i]) / 2; + //我们要判断当前回文串是不是开头是不是从 0 开始的 + if (start == 0) { + maxLen = P[i] > maxLen ? P[i] : maxLen; + } + } + return new StringBuilder(s.substring(maxLen)).reverse() + s; +} +``` + +# 总 + +这道题太强了,六种解法,各有特色。把这道题捋下来着实不易,涉及到很多算法。但懂了之后确实心旷神怡。 + 花时间最多的地方其实在 `KMP` 那里,求 `next` 数组确实难理解一些。然后递归解法,看起来简单,其实理解起来的话也没那么容易。最后没想到又回到了马拉车算法,不得不再佩服一下这个解法,神仙操作,直接将时间复杂度优化到了 `O(n)`。 \ No newline at end of file diff --git a/leetcode-215-Kth-Largest-Element-in-an-Array.md b/leetcode-215-Kth-Largest-Element-in-an-Array.md index 0cc1e8ca5..b7faee7c9 100644 --- a/leetcode-215-Kth-Largest-Element-in-an-Array.md +++ b/leetcode-215-Kth-Largest-Element-in-an-Array.md @@ -1,136 +1,136 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/215.jpg) - -找出第 `k` 大的数。 - -# 解法一 暴力 - -使用快排从大到小排序,将第 `k` 个数返回即可。 - -我们直接使用 `java` 提供的排序算法,又因为默认是从小到大排序,所以将倒数第 `k` 个数返回即可。 - -```java -public int findKthLargest(int[] nums, int k) { - Arrays.sort(nums); - return nums[nums.length - k]; -} -``` - -# 解法二 - -我们没必要把所有数字正确排序,我们可以借鉴快排中分区的思想,这里不细讲了,大家可以去回顾一下快排。 - -随机选择一个分区点,左边都是大于分区点的数,右边都是小于分区点的数。左部分的个数记做 `m`。 - -如果 `k == m + 1`,我们把分区点返回即可。 - -如果 `k > m + 1`,说明第 `k` 大数在右边,我们在右边去寻找第 `k - m - 1` 大的数即可。 - -如果 `k < m + 1`,说明第 `k` 大数在左边,我们在左边去寻找第 `k` 大的数即可。 - -左边和右边寻找在代码中采取递归即可。 - -分区达到的效果就是下边的样子。 - -```java -原数组 3 7 6 1 5 - -如果把 5 作为分区点,那么数组最后就会变成下边的样子, i 指向最终的分区点 -7 6 5 1 3 - ^ - i -``` - -代码的话,分区可以采取双指针,`i` 前边始终存比分区点大的元素。 - -```java -public int findKthLargest(int[] nums, int k) { - return findKthLargestHelper(nums, 0, nums.length - 1, k); -} - -private int findKthLargestHelper(int[] nums, int start, int end, int k) { - int i = start; - int pivot = nums[end];//分区点 - //将 i 的左半部分存比分区点大的数 - //将 i 的右半部分存比分区点小的数 - for (int j = start; j < end; j++) { - if (nums[j] >= pivot) { - int temp = nums[i]; - nums[i] = nums[j]; - nums[j] = temp; - i++; - } - } - //分区点放到 i 的位置 - int temp = nums[i]; - nums[i] = pivot; - nums[end] = temp; - //左边的数量加上 1 - int count = i - start + 1; - if (count == k) { - return nums[i]; - //从右边去继续寻找 - } else if (count < k) { - return findKthLargestHelper(nums, i + 1, end, k - count); - //从左边去继续寻找 - } else { - return findKthLargestHelper(nums, start, i - 1, k); - } -} -``` - -# 解法三 - -我们可以使用优先队列,建一个最大堆,然后依次弹出元素,弹出的第 `k` 个元素就是我们要找的。 - -优先队列的使用也不是第一次了,之前在 [23 题](https://leetcode.wang/leetCode-23-Merge-k-Sorted-Lists.html) 和 [188 题](https://leetcode.wang/leetcode-188-Best-Time-to-Buy-and-Sell-StockIV.html) 也用过,原理可以参考 [这里 ](http://blog.51cto.com/ahalei/1425314?source=dra)和 [这里](http://blog.51cto.com/ahalei/1427156)。 - -这里我们直接使用 `java` 提供的优先队列了。 - -```java -public int findKthLargest(int[] nums, int k) { - Comparator cmp; - cmp = new Comparator() { - @Override - public int compare(Integer i1, Integer i2) { - // TODO Auto-generated method stub - return i2 - i1; - } - }; - - // 建立最大堆 - Queue q = new PriorityQueue(cmp); - for (int i = 0; i < nums.length; i++) { - q.offer(nums[i]); - } - - for (int i = 0; i < k - 1; i++) { - q.poll(); - } - return q.poll(); -} -``` - -`java` 默认的是建最小堆,所以我们需要一个比较器来改变优先级。 - -如果使用最小堆也可以解决这个问题,只需要保证队列中一直是 `k` 个元素即可。当队列超出 `k` 个元素后,把队列中最小的去掉即可,这就保证了最后队列中的元素一定是前 `k` 大的元素。 - -```java -public int findKthLargest(int[] nums, int k) { - // 建立最小堆 - Queue q = new PriorityQueue(); - for (int i = 0; i < nums.length; i++) { - q.offer(nums[i]); - if (q.size() > k) { - q.poll(); - } - } - return q.poll(); -} -``` - -# 总 - -这道题不是很难,只要掌握了快排的思想,解法二也能很快写出来。解法三的话,就得事先了解优先队列了。 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/215.jpg) + +找出第 `k` 大的数。 + +# 解法一 暴力 + +使用快排从大到小排序,将第 `k` 个数返回即可。 + +我们直接使用 `java` 提供的排序算法,又因为默认是从小到大排序,所以将倒数第 `k` 个数返回即可。 + +```java +public int findKthLargest(int[] nums, int k) { + Arrays.sort(nums); + return nums[nums.length - k]; +} +``` + +# 解法二 + +我们没必要把所有数字正确排序,我们可以借鉴快排中分区的思想,这里不细讲了,大家可以去回顾一下快排。 + +随机选择一个分区点,左边都是大于分区点的数,右边都是小于分区点的数。左部分的个数记做 `m`。 + +如果 `k == m + 1`,我们把分区点返回即可。 + +如果 `k > m + 1`,说明第 `k` 大数在右边,我们在右边去寻找第 `k - m - 1` 大的数即可。 + +如果 `k < m + 1`,说明第 `k` 大数在左边,我们在左边去寻找第 `k` 大的数即可。 + +左边和右边寻找在代码中采取递归即可。 + +分区达到的效果就是下边的样子。 + +```java +原数组 3 7 6 1 5 + +如果把 5 作为分区点,那么数组最后就会变成下边的样子, i 指向最终的分区点 +7 6 5 1 3 + ^ + i +``` + +代码的话,分区可以采取双指针,`i` 前边始终存比分区点大的元素。 + +```java +public int findKthLargest(int[] nums, int k) { + return findKthLargestHelper(nums, 0, nums.length - 1, k); +} + +private int findKthLargestHelper(int[] nums, int start, int end, int k) { + int i = start; + int pivot = nums[end];//分区点 + //将 i 的左半部分存比分区点大的数 + //将 i 的右半部分存比分区点小的数 + for (int j = start; j < end; j++) { + if (nums[j] >= pivot) { + int temp = nums[i]; + nums[i] = nums[j]; + nums[j] = temp; + i++; + } + } + //分区点放到 i 的位置 + int temp = nums[i]; + nums[i] = pivot; + nums[end] = temp; + //左边的数量加上 1 + int count = i - start + 1; + if (count == k) { + return nums[i]; + //从右边去继续寻找 + } else if (count < k) { + return findKthLargestHelper(nums, i + 1, end, k - count); + //从左边去继续寻找 + } else { + return findKthLargestHelper(nums, start, i - 1, k); + } +} +``` + +# 解法三 + +我们可以使用优先队列,建一个最大堆,然后依次弹出元素,弹出的第 `k` 个元素就是我们要找的。 + +优先队列的使用也不是第一次了,之前在 [23 题](https://leetcode.wang/leetCode-23-Merge-k-Sorted-Lists.html) 和 [188 题](https://leetcode.wang/leetcode-188-Best-Time-to-Buy-and-Sell-StockIV.html) 也用过,原理可以参考 [这里 ](http://blog.51cto.com/ahalei/1425314?source=dra)和 [这里](http://blog.51cto.com/ahalei/1427156)。 + +这里我们直接使用 `java` 提供的优先队列了。 + +```java +public int findKthLargest(int[] nums, int k) { + Comparator cmp; + cmp = new Comparator() { + @Override + public int compare(Integer i1, Integer i2) { + // TODO Auto-generated method stub + return i2 - i1; + } + }; + + // 建立最大堆 + Queue q = new PriorityQueue(cmp); + for (int i = 0; i < nums.length; i++) { + q.offer(nums[i]); + } + + for (int i = 0; i < k - 1; i++) { + q.poll(); + } + return q.poll(); +} +``` + +`java` 默认的是建最小堆,所以我们需要一个比较器来改变优先级。 + +如果使用最小堆也可以解决这个问题,只需要保证队列中一直是 `k` 个元素即可。当队列超出 `k` 个元素后,把队列中最小的去掉即可,这就保证了最后队列中的元素一定是前 `k` 大的元素。 + +```java +public int findKthLargest(int[] nums, int k) { + // 建立最小堆 + Queue q = new PriorityQueue(); + for (int i = 0; i < nums.length; i++) { + q.offer(nums[i]); + if (q.size() > k) { + q.poll(); + } + } + return q.poll(); +} +``` + +# 总 + +这道题不是很难,只要掌握了快排的思想,解法二也能很快写出来。解法三的话,就得事先了解优先队列了。 + diff --git a/leetcode-216-Combination-SumIII.md b/leetcode-216-Combination-SumIII.md index ef240d13a..fe3c59430 100644 --- a/leetcode-216-Combination-SumIII.md +++ b/leetcode-216-Combination-SumIII.md @@ -1,41 +1,41 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/216.jpg) - -返回所有目标和的组合。`k` 代表每个组合能选取的个数,`n` 代表目标和,可选取的数字是 `1` 到 `9`,每种组合中每个数字只能选择一次。 - -# 思路分析 - -很典型的回溯法应用了,或者说是 `DFS`。约等于暴力求解,去考虑所有情况,然后依次判断即可。之前也做过很多回溯的题了,这里不细讲了,如果对回溯法不熟悉,大家可以在 [https://leetcode.wang/](https://leetcode.wang/) 左上角搜索「回溯」,多做一些题就有感觉了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/216_2.jpg) - -# 解法一 回溯法 - -回溯法完全可以看做一个模版,整体框架就是一个大的 for 循环,然后先 add,接着利用递归进行遍历,然后再 remove ,继续循环。 - -```java -public List> combinationSum3(int k, int n) { - List> res = new ArrayList<>(); - getAnswer(res, new ArrayList<>(), k, n, 1); - return res; -} - -private void getAnswer(List> res, ArrayList temp, int k, int n, int start) { - if (temp.size() == k) { - if (n == 0) { - res.add(new ArrayList<>(temp)); - } - return; - } - for (int i = start; i < 10; i++) { - temp.add(i); - getAnswer(res, temp, k, n - i, i + 1); - temp.remove(temp.size() - 1); - } -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/216.jpg) + +返回所有目标和的组合。`k` 代表每个组合能选取的个数,`n` 代表目标和,可选取的数字是 `1` 到 `9`,每种组合中每个数字只能选择一次。 + +# 思路分析 + +很典型的回溯法应用了,或者说是 `DFS`。约等于暴力求解,去考虑所有情况,然后依次判断即可。之前也做过很多回溯的题了,这里不细讲了,如果对回溯法不熟悉,大家可以在 [https://leetcode.wang/](https://leetcode.wang/) 左上角搜索「回溯」,多做一些题就有感觉了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/216_2.jpg) + +# 解法一 回溯法 + +回溯法完全可以看做一个模版,整体框架就是一个大的 for 循环,然后先 add,接着利用递归进行遍历,然后再 remove ,继续循环。 + +```java +public List> combinationSum3(int k, int n) { + List> res = new ArrayList<>(); + getAnswer(res, new ArrayList<>(), k, n, 1); + return res; +} + +private void getAnswer(List> res, ArrayList temp, int k, int n, int start) { + if (temp.size() == k) { + if (n == 0) { + res.add(new ArrayList<>(temp)); + } + return; + } + for (int i = start; i < 10; i++) { + temp.add(i); + getAnswer(res, temp, k, n - i, i + 1); + temp.remove(temp.size() - 1); + } +} +``` + +# 总 + 这道题没什么难点,主要就是回溯法的应用。 \ No newline at end of file diff --git a/leetcode-217-Contains-Duplicate.md b/leetcode-217-Contains-Duplicate.md index f4318fe94..b0f0de797 100644 --- a/leetcode-217-Contains-Duplicate.md +++ b/leetcode-217-Contains-Duplicate.md @@ -1,36 +1,36 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/217.jpg) - -判断是否有重复数字。 - -# 思路分析 - -这种题目直接就想到利用 `HashMap` 或者 `HashSet`,将数字依次存入其中。这样做的好处就是,判断新加入的数字是否已经存在,时间复杂度可以是 `O(1)`。 - -[官方题解](https://leetcode.com/problems/contains-duplicate/solution/) 也介绍了另外两种解法,就不细讲了。 - -一种是纯暴力方法,两层 `for` 循环,两两判断即可。 - -一种是先将原数组排序,然后判断是否有前后两个数字相同即可。 - -# 解法一 - -这里只给出利用 `HashSet` 的方法了,空间换时间,比较常用。 - -```java -public boolean containsDuplicate(int[] nums) { - HashSet set = new HashSet<>(); - for (int i = 0; i < nums.length; i++) { - if (set.contains(nums[i])) { - return true; - } - set.add(nums[i]); - } - return false; -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/217.jpg) + +判断是否有重复数字。 + +# 思路分析 + +这种题目直接就想到利用 `HashMap` 或者 `HashSet`,将数字依次存入其中。这样做的好处就是,判断新加入的数字是否已经存在,时间复杂度可以是 `O(1)`。 + +[官方题解](https://leetcode.com/problems/contains-duplicate/solution/) 也介绍了另外两种解法,就不细讲了。 + +一种是纯暴力方法,两层 `for` 循环,两两判断即可。 + +一种是先将原数组排序,然后判断是否有前后两个数字相同即可。 + +# 解法一 + +这里只给出利用 `HashSet` 的方法了,空间换时间,比较常用。 + +```java +public boolean containsDuplicate(int[] nums) { + HashSet set = new HashSet<>(); + for (int i = 0; i < nums.length; i++) { + if (set.contains(nums[i])) { + return true; + } + set.add(nums[i]); + } + return false; +} +``` + +# 总 + 一道比较简单的题目,利用 `HashMap` 可以判重以及计数,比如 [30 题](https://leetcode.wang/leetCode-30-Substring-with-Concatenation-of-All-Words.html)、[49 题](https://leetcode.wang/leetCode-49-Group-Anagrams.html)、[136 题](https://leetcode.wang/leetcode-136-Single-Number.html)、[137 题](https://leetcode.wang/leetcode-137-Single-NumberII.html)。 \ No newline at end of file diff --git a/leetcode-218-The-Skyline-Problem.md b/leetcode-218-The-Skyline-Problem.md index d9a48ff52..0b93e0c42 100644 --- a/leetcode-218-The-Skyline-Problem.md +++ b/leetcode-218-The-Skyline-Problem.md @@ -1,485 +1,485 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/218.png) - -给定几个矩形,矩形以坐标形式表示,`[x1, x2, h]`,分别代表矩形与 `x` 轴左右交点的 `x` 坐标以及矩形的高度。 - -输出所有矩形组成的轮廓,只输出所有关键点即可。关键点用坐标 `[x,y]` 的形式。 - -# 思路分析? - -自己也没有想出来解法,主要参考了下边的几个链接。 - -[https://www.youtube.com/watch?v=GSBLe8cKu0s](https://www.youtube.com/watch?v=GSBLe8cKu0s) - -[https://www.geeksforgeeks.org/the-skyline-problem-using-divide-and-conquer-algorithm/](https://www.geeksforgeeks.org/the-skyline-problem-using-divide-and-conquer-algorithm/) - -[https://leetcode.com/problems/the-skyline-problem/discuss/61281/Java-divide-and-conquer-solution-beats-96](https://leetcode.com/problems/the-skyline-problem/discuss/61281/Java-divide-and-conquer-solution-beats-96) - -虽然明白了上边作者的解法,但并没有像以往一样理出作者是怎么想出解法的,或者说推导有点儿太马后炮了,并不能说服自己,所以下边介绍的解法只讲方法,没有一步一步的递进的过程了。 - -首先讲一下为什么题目要求我们输出那些关键点,换句话说,知道那些关键点我们怎么画出轮廓。 - -我们只需要从原点向右出发,沿着水平方向一直画线。 - -如果在正上方或者正下方遇到关键点,就拐向关键点。 - -到达关键点后继续向右水平画线,重复上边的过程即可。 - -可以结合下边的图看一下。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/218_2.jpg) - -# 解法一 - -有些类似归并排序的思想,divide and conquer 。 - -首先考虑,如果只给一个建筑 `[x, y, h]`,那么答案是多少? - -很明显输出的解将会是 `[[x, h], [y, 0]]`,也就是左上角和右下角坐标。 - -接下来考虑,如果有建筑 A B C D E,我们知道了建筑 A B C 输出的解和 D E 输出的解,那么怎么把这两组解合并,得到 A B C D E 输出的解。 - -合并方法采用归并排序中双指针的方法,将两个指针分别指向两组解的开头,然后进行比对。具体的,看下边的例子。 - -每次选取 `x` 坐标较小的点,然后再根据一定规则算出高度,具体的看下边的过程。 - -```java -Skyline1 = {(1, 11), (3, 13), (9, 0), (12, 7), (16, 0)} -Skyline2 = {(14, 3), (19, 18), (22, 3), (23, 13), (29, 0)} - -Skyline1 存储第一组的解。 -Skyline2 存储第二组的解。 - -Result 存储合并后的解, Result = {} - -h1 表示将 Skyline1 中的某个关键点加入 Result 中时, 当前关键点的高度 -h2 表示将 Skyline2 中的某个关键点加入 Result 中时, 当前关键点的高度 - -h1 = 0, h2 = 0 -i = 0, j = 0 - -(1, 11), (3, 13), (9, 0), (12, 7), (16, 0) - ^ - i -(14, 3), (19, 18), (22, 3), (23, 13), (29, 0) - ^ - j -比较 (1, 11) 和 (14, 3) -比较 x 坐标, 1 < 14, 所以考虑 (1, 11) -x 取 1, 接下来更新 height -h1 = 11, height = max(h1, h2) = max(11, 0) = 11 -将 (1, 11) 加入到 Result 中 -Result = {(1, 11)} -i 后移, i = i + 1 = 2 - -(1, 11), (3, 13), (9, 0), (12, 7), (16, 0) - ^ - i -(14, 3), (19, 18), (22, 3), (23, 13), (29, 0) - ^ - j -比较 (3, 13) 和 (14, 3) -比较 x 坐标, 3 < 14, 所以考虑 (3, 13) -x 取 3, 接下来更新 height -h1 = 13, height = max(h1, h2) = max(13, 0) = 13 -将 (3, 13) 加入到 Result 中 -Result = {(1, 11), (3, 13)} -i 后移, i = i + 1 = 3 - -(9, 0) 和 (12, 7) 同理 -此时 h1 = 7 -Result 为 {(1, 11), (3, 13), (9, 0), (12, 7)} -i = 4 - -(1, 11), (3, 13), (9, 0), (12, 7), (16, 0) - ^ - i -(14, 3), (19, 18), (22, 3), (23, 13), (29, 0) - ^ - j -比较 (16, 0) 和 (14, 3) -比较 x 坐标, 14 < 16, 所以考虑 (14, 3) -x 取 14, 接下来更新 height -h2 = 3, height = max(h1, h2) = max(7, 3) = 7 -将 (14, 7) 加入到 Result 中 -Result = {(1, 11), (3, 13), (9, 0), (12, 7), (14, 7)} -j 后移, j = j + 1 = 2 - -(1, 11), (3, 13), (9, 0), (12, 7), (16, 0) - ^ - i -(14, 3), (19, 18), (22, 3), (23, 13), (29, 0) - ^ - j -比较 (16, 0) 和 (19, 18) -比较 x 坐标, 16 < 19, 所以考虑 (16, 0) -x 取 16, 接下来更新 height -h1 = 0, height = max(h1, h2) = max(0, 3) = 3 -将 (16, 3) 加入到 Result 中 -Result = {(1, 11), (3, 13), (9, 0), (12, 7), (14, 7), (16, 3)} -i 后移, i = i + 1 = 5 - -因为 Skyline1 没有更多的解了,所以只需要将 Skyline2 剩下的解按照上边 height 的更新方式继续加入到 Result 中即可 -Result = {(1, 11), (3, 13), (9, 0), (12, 7), (14, 7), (16, 3), - (19, 18), (22, 3), (23, 13), (29, 0)} - -我们会发现上边多了一些解, (14, 7) 并不是我们需要的, 因为之前已经有了 (12, 7), 所以我们需要将其去掉。 -Result = {(1, 11), (3, 13), (9, 0), (12, 7), (16, 3), (19, 18), - (22, 3), (23, 13), (29, 0)} -``` - -代码的话,模仿归并排序,我们每次将 `buildings` 对半分,然后进入递归,将得到的两组解按照上边的方式合并即可。 - -```java -public List> getSkyline(int[][] buildings) { - if(buildings.length == 0){ - return new ArrayList<>(); - } - return merge(buildings, 0, buildings.length - 1); -} - -private List> merge(int[][] buildings, int start, int end) { - - List> res = new ArrayList<>(); - //只有一个建筑, 将 [x, h], [y, 0] 加入结果 - if (start == end) { - List temp = new ArrayList<>(); - temp.add(buildings[start][0]); - temp.add(buildings[start][2]); - res.add(temp); - - temp = new ArrayList<>(); - temp.add(buildings[start][1]); - temp.add(00); - res.add(temp); - return res; - } - int mid = (start + end) >>> 1; - //第一组解 - List> Skyline1 = merge(buildings, start, mid); - //第二组解 - List> Skyline2 = merge(buildings, mid + 1, end); - //下边将两组解合并 - int h1 = 0; - int h2 = 0; - int i = 0; - int j = 0; - while (i < Skyline1 .size() || j < Skyline2 .size()) { - long x1 = i < Skyline1 .size() ? Skyline1 .get(i).get(0) : Long.MAX_VALUE; - long x2 = j < Skyline2 .size() ? Skyline2 .get(j).get(0) : Long.MAX_VALUE; - long x = 0; - //比较两个坐标 - if (x1 < x2) { - h1 = Skyline1 .get(i).get(1); - x = x1; - i++; - } else if (x1 > x2) { - h2 = Skyline2 .get(j).get(1); - x = x2; - j++; - } else { - h1 = Skyline1 .get(i).get(1); - h2 = Skyline2 .get(j).get(1); - x = x1; - i++; - j++; - } - //更新 height - int height = Math.max(h1, h2); - //重复的解不要加入 - if (res.isEmpty() || height != res.get(res.size() - 1).get(1)) { - List temp = new ArrayList<>(); - temp.add((int) x); - temp.add(height); - res.add(temp); - } - } - return res; -} -``` - -上边有两个技巧需要注意,技巧只是为了让算法更简洁一些,不用也是可以的,但可能会麻烦些。 - -一个就是下边的部分 - -```java -long x1 = i < Skyline1 .size() ? Skyline1 .get(i).get(0) : Long.MAX_VALUE; -long x2 = j < Skyline2 .size() ? Skyline2 .get(j).get(0) : Long.MAX_VALUE; -``` - -当 `Skyline1` 或者 `Skyline2` 遍历完的时候,我们给他赋值为一个很大的数,这样的话我们可以在一个 `while` 循环中完成我们的算法,不用再单独考虑当一个遍历完的处理。 - -这里需要注意的是,我们将 `x1` 和 `x2` 定义为 `long`,算是一个 `trick`,可以保证我们给 `x1` 或者 `x2` 赋的 `Long.MAX_VALUE` 这个值,后续不会出现 `x1 == x2`。因为原始数据都是 `int` 范围的。 - -当然也可以有其他的处理方式,比如当遍历完的时候,给 `x1` 或者 `x2` 赋值成负数,不过这样的话就需要更改后续的 `if` 判断条件,不细说了。 - -另外一个技巧就是下边的部分。 - -```java - if (res.isEmpty() || height != res.get(res.size() - 1).get(1)) { -``` - -我们在将当前结果加入的 `res` 中时,判断一下当前的高度是不是 `res` 中最后一个的高度,可以提前防止加入重复的点。 - -# 解法二 - -直接讲解法,比较好理解。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/218_3.jpg) - -只考虑每个 building 的左上角和右上角坐标,将所有点按 `x` 坐标排序,然后开始遍历。 - -需要一个优先队列来存储遍历坐标的高度,也就是 y 轴坐标。 - -对于左上角坐标和右上角坐标有不同的处理方式。 - -遇到左上角坐标,将其 y 坐标加入到优先队列中。 - -遇到右上角坐标,将其 y 坐标从优先队列中删除,也就是删除了其对应的左上角坐标的 y 值。 - -最后判断优先队列中的最高高度相对于之前是否更新,如果更新了的话,就将当前的 `x` 以及更新后的最高高度作为一个坐标加入到最终结果中。 - -```java -buildings [2 9 10], [3 7 15], [5 12 12], [15 20 10], [19 24 8] -根据 buildings 求出每个 building 的左上角和右上角坐标 -将所有坐标按照 x 排序, 并标记当前坐标是左上角坐标还是右上角坐标 -l(2,10) l(3,15) l(5,12) r(7,15) r(9,10) r(12,12) -l(15,10) l(19,8) r(20,10) r(24,8) -PriorityQueue = {0}, preMax = 0 - -l(2,10) 将 10 加入优先队列 -preMax = 0, PriorityQueue = {0 10} -当前 PriorityQueue 的 max = 10, 相对于 preMax 更新了 -将 (2,10) 加入到 res, res = {(2,10)} -更新 preMax = 10 - -l(3,15) 将 15 加入优先队列 -preMax = 10, PriorityQueue = {0 10 15} -当前 PriorityQueue 的 max = 15, 相对于 preMax 更新了 -将 (3,15) 加入到 res, res = {(2,10) (3,15)} -更新 preMax = 15 - -l(5,12) 将 12 加入优先队列 -preMax = 15, PriorityQueue = {0 10 15 12} -当前 PriorityQueue 的 max = 15, 相对于 preMax 没有更新 -res 不变 - -r(7,15) , 遇到右上角坐标, 将 15 从优先队列删除 -preMax = 15, PriorityQueue = {0 10 12} -当前 PriorityQueue 的 max = 12, 相对于 preMax 更新了 -将 (7,max) 即 (7,12) 加入到 res, res = {(2,10) (3,15) (7,12)} -更新 preMax = 12 - -r(9,10) , 遇到右上角坐标, 将 10 从优先队列删除 -preMax = 12, PriorityQueue = {0 12} -当前 PriorityQueue 的 max = 12, 相对于 preMax 没有更新 -res 不变 - -r(12,12) , 遇到右上角坐标, 将 12 从优先队列删除 -preMax = 12, PriorityQueue = {0} -当前 PriorityQueue 的 max = 0, 相对于 preMax 更新了 -将 (12,max) 即 (7,0) 加入到 res, res = {(2,10) (3,15) (7,12) (12,0)} -更新 preMax = 0 - -后边的同理,就不进行下去了。 -``` - -然后再考虑一些边界情况,开始给坐标排序的时候我们是根据 `x` 坐标大小,当 `x` 坐标相等的时候怎么办呢? - -考虑两个坐标比较的时候,`x` 坐标相等会有三种情况。 - -1. 当两个坐标都是左上角坐标,我们要将高度高的排在前边 -2. 当两个坐标都是右上角坐标,我们要将高度低的排在前边 -3. 当两个坐标一个是左上角坐标,一个是右上角坐标,我们需要将左上角坐标排在前边 - -上边的三条规则也是根据三种情况归纳总结出来的,大家可以举例子来判断。 - -有了这三个规则,然后写代码的话就会很繁琐,这里有个技巧。存左上角坐标的时候, 将高度(y)存为负数。存右上角坐标的时候,将高度(y)存为正数。 - -这么做有两个作用。 - -一个作用就是可以根据高度的正负数区分当前是左上角坐标还是右上角坐标。 - -另一个作用就是可以通过一个比较器,就实现上边的三条比较规则。 - -```java -public int compare(List p1, List p2) { - int x1 = p1.get(0); - int y1 = p1.get(1); - int x2 = p2.get(0); - int y2 = p2.get(1); - //不相等时候,按照 x 从小到大排序 - if (x1 != x2) { - return x1 - x2; - //相等时候,只需要将高度相减就满足了上边的三条规则,可以尝试验证一下 - } else { - return y1 - y2; - } -} -``` - -另一个技巧在举例子的时候已经用到了,就是优先队列初始的时候将 `0` 加入。 - -然后其他部分代码的话按照上边举的例子写就可以了。 - -```java -public List> getSkyline(int[][] buildings) { - List> points = new ArrayList<>(); - List> results = new ArrayList<>(); - int n = buildings.length; - //求出左上角和右上角坐标, 左上角坐标的 y 存负数 - for (int[] b : buildings) { - List p1 = new ArrayList<>(); - p1.add(b[0]); - p1.add(-b[2]); - points.add(p1); - - List p2 = new ArrayList<>(); - p2.add(b[1]); - p2.add(b[2]); - points.add(p2); - } - //将所有坐标排序 - Collections.sort(points, new Comparator>() { - @Override - public int compare(List p1, List p2) { - int x1 = p1.get(0); - int y1 = p1.get(1); - int x2 = p2.get(0); - int y2 = p2.get(1); - if (x1 != x2) { - return x1 - x2; - } else { - return y1 - y2; - } - } - - }); - //默认的优先队列是最小堆,我们需要最大堆,每次需要得到队列中最大的元素 - Queue queue = new PriorityQueue<>(new Comparator() { - @Override - public int compare(Integer i1, Integer i2) { - return i2 - i1; - } - }); - queue.offer(0); - int preMax = 0; - - for (List p : points) { - int x = p.get(0); - int y = p.get(1); - //左上角坐标 - if (y < 0) { - queue.offer(-y); - //右上角坐标 - } else { - queue.remove(y); - } - int curMax = queue.peek(); - //最大值更新了, 将当前结果加入 - if (curMax != preMax) { - List temp = new ArrayList<>(); - temp.add(x); - temp.add(curMax); - results.add(temp); - preMax = curMax; - } - } - return results; -} - -``` - -代码的话还能优化一下,上边代码中最常出现的三种操作。 - -添加高度,时间复杂度 `O(log(n))`。 - -删除高度,时间复杂度 `O(n)`。 - -查看最大高度,时间复杂度 `O(1)`。 - -有一个操作是 `O(n)`,加上外层的遍历,所以会使得最终的时间复杂度成为 `O(n²)` 。 - -之所以是上边的时间复杂度,因为我们使用的是优先队列。我们还可以使用 `TreeMap`,这样上边的三种操作时间复杂度就都是 `O(log(n))` 了,最终的时间复杂度就变为 `O(nlog(n))` - -`TreeMap` 的话 `key` 当然就是存高度了,因为可能添加重复的高度,所有`value` 的话存高度出现的次数即可。 - -代码的话,整体思想不需要改变,只需要改变添加高度、删除高度、查看最大高度的部分。 - -```java -public List> getSkyline(int[][] buildings) { - List> points = new ArrayList<>(); - List> results = new ArrayList<>(); - int n = buildings.length; - //求出将左上角和右上角坐标, 左上角坐标的 y 存负数 - for (int[] b : buildings) { - List p1 = new ArrayList<>(); - p1.add(b[0]); - p1.add(-b[2]); - points.add(p1); - - List p2 = new ArrayList<>(); - p2.add(b[1]); - p2.add(b[2]); - points.add(p2); - } - //将所有坐标排序 - Collections.sort(points, new Comparator>() { - @Override - public int compare(List p1, List p2) { - int x1 = p1.get(0); - int y1 = p1.get(1); - int x2 = p2.get(0); - int y2 = p2.get(1); - if (x1 != x2) { - return x1 - x2; - } else { - return y1 - y2; - } - } - - }); - TreeMap treeMap = new TreeMap<>(new Comparator() { - @Override - public int compare(Integer i1, Integer i2) { - return i2 - i1; - } - }); - treeMap.put(0, 1); - int preMax = 0; - - for (List p : points) { - int x = p.get(0); - int y = p.get(1); - if (y < 0) { - Integer v = treeMap.get(-y); - if (v == null) { - treeMap.put(-y, 1); - } else { - treeMap.put(-y, v + 1); - } - } else { - Integer v = treeMap.get(y); - if (v == 1) { - treeMap.remove(y); - } else { - treeMap.put(y, v - 1); - } - } - int curMax = treeMap.firstKey(); - if (curMax != preMax) { - List temp = new ArrayList<>(); - temp.add(x); - temp.add(curMax); - results.add(temp); - preMax = curMax; - } - } - return results; -} -``` - -# 总 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/218.png) + +给定几个矩形,矩形以坐标形式表示,`[x1, x2, h]`,分别代表矩形与 `x` 轴左右交点的 `x` 坐标以及矩形的高度。 + +输出所有矩形组成的轮廓,只输出所有关键点即可。关键点用坐标 `[x,y]` 的形式。 + +# 思路分析? + +自己也没有想出来解法,主要参考了下边的几个链接。 + +[https://www.youtube.com/watch?v=GSBLe8cKu0s](https://www.youtube.com/watch?v=GSBLe8cKu0s) + +[https://www.geeksforgeeks.org/the-skyline-problem-using-divide-and-conquer-algorithm/](https://www.geeksforgeeks.org/the-skyline-problem-using-divide-and-conquer-algorithm/) + +[https://leetcode.com/problems/the-skyline-problem/discuss/61281/Java-divide-and-conquer-solution-beats-96](https://leetcode.com/problems/the-skyline-problem/discuss/61281/Java-divide-and-conquer-solution-beats-96) + +虽然明白了上边作者的解法,但并没有像以往一样理出作者是怎么想出解法的,或者说推导有点儿太马后炮了,并不能说服自己,所以下边介绍的解法只讲方法,没有一步一步的递进的过程了。 + +首先讲一下为什么题目要求我们输出那些关键点,换句话说,知道那些关键点我们怎么画出轮廓。 + +我们只需要从原点向右出发,沿着水平方向一直画线。 + +如果在正上方或者正下方遇到关键点,就拐向关键点。 + +到达关键点后继续向右水平画线,重复上边的过程即可。 + +可以结合下边的图看一下。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/218_2.jpg) + +# 解法一 + +有些类似归并排序的思想,divide and conquer 。 + +首先考虑,如果只给一个建筑 `[x, y, h]`,那么答案是多少? + +很明显输出的解将会是 `[[x, h], [y, 0]]`,也就是左上角和右下角坐标。 + +接下来考虑,如果有建筑 A B C D E,我们知道了建筑 A B C 输出的解和 D E 输出的解,那么怎么把这两组解合并,得到 A B C D E 输出的解。 + +合并方法采用归并排序中双指针的方法,将两个指针分别指向两组解的开头,然后进行比对。具体的,看下边的例子。 + +每次选取 `x` 坐标较小的点,然后再根据一定规则算出高度,具体的看下边的过程。 + +```java +Skyline1 = {(1, 11), (3, 13), (9, 0), (12, 7), (16, 0)} +Skyline2 = {(14, 3), (19, 18), (22, 3), (23, 13), (29, 0)} + +Skyline1 存储第一组的解。 +Skyline2 存储第二组的解。 + +Result 存储合并后的解, Result = {} + +h1 表示将 Skyline1 中的某个关键点加入 Result 中时, 当前关键点的高度 +h2 表示将 Skyline2 中的某个关键点加入 Result 中时, 当前关键点的高度 + +h1 = 0, h2 = 0 +i = 0, j = 0 + +(1, 11), (3, 13), (9, 0), (12, 7), (16, 0) + ^ + i +(14, 3), (19, 18), (22, 3), (23, 13), (29, 0) + ^ + j +比较 (1, 11) 和 (14, 3) +比较 x 坐标, 1 < 14, 所以考虑 (1, 11) +x 取 1, 接下来更新 height +h1 = 11, height = max(h1, h2) = max(11, 0) = 11 +将 (1, 11) 加入到 Result 中 +Result = {(1, 11)} +i 后移, i = i + 1 = 2 + +(1, 11), (3, 13), (9, 0), (12, 7), (16, 0) + ^ + i +(14, 3), (19, 18), (22, 3), (23, 13), (29, 0) + ^ + j +比较 (3, 13) 和 (14, 3) +比较 x 坐标, 3 < 14, 所以考虑 (3, 13) +x 取 3, 接下来更新 height +h1 = 13, height = max(h1, h2) = max(13, 0) = 13 +将 (3, 13) 加入到 Result 中 +Result = {(1, 11), (3, 13)} +i 后移, i = i + 1 = 3 + +(9, 0) 和 (12, 7) 同理 +此时 h1 = 7 +Result 为 {(1, 11), (3, 13), (9, 0), (12, 7)} +i = 4 + +(1, 11), (3, 13), (9, 0), (12, 7), (16, 0) + ^ + i +(14, 3), (19, 18), (22, 3), (23, 13), (29, 0) + ^ + j +比较 (16, 0) 和 (14, 3) +比较 x 坐标, 14 < 16, 所以考虑 (14, 3) +x 取 14, 接下来更新 height +h2 = 3, height = max(h1, h2) = max(7, 3) = 7 +将 (14, 7) 加入到 Result 中 +Result = {(1, 11), (3, 13), (9, 0), (12, 7), (14, 7)} +j 后移, j = j + 1 = 2 + +(1, 11), (3, 13), (9, 0), (12, 7), (16, 0) + ^ + i +(14, 3), (19, 18), (22, 3), (23, 13), (29, 0) + ^ + j +比较 (16, 0) 和 (19, 18) +比较 x 坐标, 16 < 19, 所以考虑 (16, 0) +x 取 16, 接下来更新 height +h1 = 0, height = max(h1, h2) = max(0, 3) = 3 +将 (16, 3) 加入到 Result 中 +Result = {(1, 11), (3, 13), (9, 0), (12, 7), (14, 7), (16, 3)} +i 后移, i = i + 1 = 5 + +因为 Skyline1 没有更多的解了,所以只需要将 Skyline2 剩下的解按照上边 height 的更新方式继续加入到 Result 中即可 +Result = {(1, 11), (3, 13), (9, 0), (12, 7), (14, 7), (16, 3), + (19, 18), (22, 3), (23, 13), (29, 0)} + +我们会发现上边多了一些解, (14, 7) 并不是我们需要的, 因为之前已经有了 (12, 7), 所以我们需要将其去掉。 +Result = {(1, 11), (3, 13), (9, 0), (12, 7), (16, 3), (19, 18), + (22, 3), (23, 13), (29, 0)} +``` + +代码的话,模仿归并排序,我们每次将 `buildings` 对半分,然后进入递归,将得到的两组解按照上边的方式合并即可。 + +```java +public List> getSkyline(int[][] buildings) { + if(buildings.length == 0){ + return new ArrayList<>(); + } + return merge(buildings, 0, buildings.length - 1); +} + +private List> merge(int[][] buildings, int start, int end) { + + List> res = new ArrayList<>(); + //只有一个建筑, 将 [x, h], [y, 0] 加入结果 + if (start == end) { + List temp = new ArrayList<>(); + temp.add(buildings[start][0]); + temp.add(buildings[start][2]); + res.add(temp); + + temp = new ArrayList<>(); + temp.add(buildings[start][1]); + temp.add(00); + res.add(temp); + return res; + } + int mid = (start + end) >>> 1; + //第一组解 + List> Skyline1 = merge(buildings, start, mid); + //第二组解 + List> Skyline2 = merge(buildings, mid + 1, end); + //下边将两组解合并 + int h1 = 0; + int h2 = 0; + int i = 0; + int j = 0; + while (i < Skyline1 .size() || j < Skyline2 .size()) { + long x1 = i < Skyline1 .size() ? Skyline1 .get(i).get(0) : Long.MAX_VALUE; + long x2 = j < Skyline2 .size() ? Skyline2 .get(j).get(0) : Long.MAX_VALUE; + long x = 0; + //比较两个坐标 + if (x1 < x2) { + h1 = Skyline1 .get(i).get(1); + x = x1; + i++; + } else if (x1 > x2) { + h2 = Skyline2 .get(j).get(1); + x = x2; + j++; + } else { + h1 = Skyline1 .get(i).get(1); + h2 = Skyline2 .get(j).get(1); + x = x1; + i++; + j++; + } + //更新 height + int height = Math.max(h1, h2); + //重复的解不要加入 + if (res.isEmpty() || height != res.get(res.size() - 1).get(1)) { + List temp = new ArrayList<>(); + temp.add((int) x); + temp.add(height); + res.add(temp); + } + } + return res; +} +``` + +上边有两个技巧需要注意,技巧只是为了让算法更简洁一些,不用也是可以的,但可能会麻烦些。 + +一个就是下边的部分 + +```java +long x1 = i < Skyline1 .size() ? Skyline1 .get(i).get(0) : Long.MAX_VALUE; +long x2 = j < Skyline2 .size() ? Skyline2 .get(j).get(0) : Long.MAX_VALUE; +``` + +当 `Skyline1` 或者 `Skyline2` 遍历完的时候,我们给他赋值为一个很大的数,这样的话我们可以在一个 `while` 循环中完成我们的算法,不用再单独考虑当一个遍历完的处理。 + +这里需要注意的是,我们将 `x1` 和 `x2` 定义为 `long`,算是一个 `trick`,可以保证我们给 `x1` 或者 `x2` 赋的 `Long.MAX_VALUE` 这个值,后续不会出现 `x1 == x2`。因为原始数据都是 `int` 范围的。 + +当然也可以有其他的处理方式,比如当遍历完的时候,给 `x1` 或者 `x2` 赋值成负数,不过这样的话就需要更改后续的 `if` 判断条件,不细说了。 + +另外一个技巧就是下边的部分。 + +```java + if (res.isEmpty() || height != res.get(res.size() - 1).get(1)) { +``` + +我们在将当前结果加入的 `res` 中时,判断一下当前的高度是不是 `res` 中最后一个的高度,可以提前防止加入重复的点。 + +# 解法二 + +直接讲解法,比较好理解。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/218_3.jpg) + +只考虑每个 building 的左上角和右上角坐标,将所有点按 `x` 坐标排序,然后开始遍历。 + +需要一个优先队列来存储遍历坐标的高度,也就是 y 轴坐标。 + +对于左上角坐标和右上角坐标有不同的处理方式。 + +遇到左上角坐标,将其 y 坐标加入到优先队列中。 + +遇到右上角坐标,将其 y 坐标从优先队列中删除,也就是删除了其对应的左上角坐标的 y 值。 + +最后判断优先队列中的最高高度相对于之前是否更新,如果更新了的话,就将当前的 `x` 以及更新后的最高高度作为一个坐标加入到最终结果中。 + +```java +buildings [2 9 10], [3 7 15], [5 12 12], [15 20 10], [19 24 8] +根据 buildings 求出每个 building 的左上角和右上角坐标 +将所有坐标按照 x 排序, 并标记当前坐标是左上角坐标还是右上角坐标 +l(2,10) l(3,15) l(5,12) r(7,15) r(9,10) r(12,12) +l(15,10) l(19,8) r(20,10) r(24,8) +PriorityQueue = {0}, preMax = 0 + +l(2,10) 将 10 加入优先队列 +preMax = 0, PriorityQueue = {0 10} +当前 PriorityQueue 的 max = 10, 相对于 preMax 更新了 +将 (2,10) 加入到 res, res = {(2,10)} +更新 preMax = 10 + +l(3,15) 将 15 加入优先队列 +preMax = 10, PriorityQueue = {0 10 15} +当前 PriorityQueue 的 max = 15, 相对于 preMax 更新了 +将 (3,15) 加入到 res, res = {(2,10) (3,15)} +更新 preMax = 15 + +l(5,12) 将 12 加入优先队列 +preMax = 15, PriorityQueue = {0 10 15 12} +当前 PriorityQueue 的 max = 15, 相对于 preMax 没有更新 +res 不变 + +r(7,15) , 遇到右上角坐标, 将 15 从优先队列删除 +preMax = 15, PriorityQueue = {0 10 12} +当前 PriorityQueue 的 max = 12, 相对于 preMax 更新了 +将 (7,max) 即 (7,12) 加入到 res, res = {(2,10) (3,15) (7,12)} +更新 preMax = 12 + +r(9,10) , 遇到右上角坐标, 将 10 从优先队列删除 +preMax = 12, PriorityQueue = {0 12} +当前 PriorityQueue 的 max = 12, 相对于 preMax 没有更新 +res 不变 + +r(12,12) , 遇到右上角坐标, 将 12 从优先队列删除 +preMax = 12, PriorityQueue = {0} +当前 PriorityQueue 的 max = 0, 相对于 preMax 更新了 +将 (12,max) 即 (7,0) 加入到 res, res = {(2,10) (3,15) (7,12) (12,0)} +更新 preMax = 0 + +后边的同理,就不进行下去了。 +``` + +然后再考虑一些边界情况,开始给坐标排序的时候我们是根据 `x` 坐标大小,当 `x` 坐标相等的时候怎么办呢? + +考虑两个坐标比较的时候,`x` 坐标相等会有三种情况。 + +1. 当两个坐标都是左上角坐标,我们要将高度高的排在前边 +2. 当两个坐标都是右上角坐标,我们要将高度低的排在前边 +3. 当两个坐标一个是左上角坐标,一个是右上角坐标,我们需要将左上角坐标排在前边 + +上边的三条规则也是根据三种情况归纳总结出来的,大家可以举例子来判断。 + +有了这三个规则,然后写代码的话就会很繁琐,这里有个技巧。存左上角坐标的时候, 将高度(y)存为负数。存右上角坐标的时候,将高度(y)存为正数。 + +这么做有两个作用。 + +一个作用就是可以根据高度的正负数区分当前是左上角坐标还是右上角坐标。 + +另一个作用就是可以通过一个比较器,就实现上边的三条比较规则。 + +```java +public int compare(List p1, List p2) { + int x1 = p1.get(0); + int y1 = p1.get(1); + int x2 = p2.get(0); + int y2 = p2.get(1); + //不相等时候,按照 x 从小到大排序 + if (x1 != x2) { + return x1 - x2; + //相等时候,只需要将高度相减就满足了上边的三条规则,可以尝试验证一下 + } else { + return y1 - y2; + } +} +``` + +另一个技巧在举例子的时候已经用到了,就是优先队列初始的时候将 `0` 加入。 + +然后其他部分代码的话按照上边举的例子写就可以了。 + +```java +public List> getSkyline(int[][] buildings) { + List> points = new ArrayList<>(); + List> results = new ArrayList<>(); + int n = buildings.length; + //求出左上角和右上角坐标, 左上角坐标的 y 存负数 + for (int[] b : buildings) { + List p1 = new ArrayList<>(); + p1.add(b[0]); + p1.add(-b[2]); + points.add(p1); + + List p2 = new ArrayList<>(); + p2.add(b[1]); + p2.add(b[2]); + points.add(p2); + } + //将所有坐标排序 + Collections.sort(points, new Comparator>() { + @Override + public int compare(List p1, List p2) { + int x1 = p1.get(0); + int y1 = p1.get(1); + int x2 = p2.get(0); + int y2 = p2.get(1); + if (x1 != x2) { + return x1 - x2; + } else { + return y1 - y2; + } + } + + }); + //默认的优先队列是最小堆,我们需要最大堆,每次需要得到队列中最大的元素 + Queue queue = new PriorityQueue<>(new Comparator() { + @Override + public int compare(Integer i1, Integer i2) { + return i2 - i1; + } + }); + queue.offer(0); + int preMax = 0; + + for (List p : points) { + int x = p.get(0); + int y = p.get(1); + //左上角坐标 + if (y < 0) { + queue.offer(-y); + //右上角坐标 + } else { + queue.remove(y); + } + int curMax = queue.peek(); + //最大值更新了, 将当前结果加入 + if (curMax != preMax) { + List temp = new ArrayList<>(); + temp.add(x); + temp.add(curMax); + results.add(temp); + preMax = curMax; + } + } + return results; +} + +``` + +代码的话还能优化一下,上边代码中最常出现的三种操作。 + +添加高度,时间复杂度 `O(log(n))`。 + +删除高度,时间复杂度 `O(n)`。 + +查看最大高度,时间复杂度 `O(1)`。 + +有一个操作是 `O(n)`,加上外层的遍历,所以会使得最终的时间复杂度成为 `O(n²)` 。 + +之所以是上边的时间复杂度,因为我们使用的是优先队列。我们还可以使用 `TreeMap`,这样上边的三种操作时间复杂度就都是 `O(log(n))` 了,最终的时间复杂度就变为 `O(nlog(n))` + +`TreeMap` 的话 `key` 当然就是存高度了,因为可能添加重复的高度,所有`value` 的话存高度出现的次数即可。 + +代码的话,整体思想不需要改变,只需要改变添加高度、删除高度、查看最大高度的部分。 + +```java +public List> getSkyline(int[][] buildings) { + List> points = new ArrayList<>(); + List> results = new ArrayList<>(); + int n = buildings.length; + //求出将左上角和右上角坐标, 左上角坐标的 y 存负数 + for (int[] b : buildings) { + List p1 = new ArrayList<>(); + p1.add(b[0]); + p1.add(-b[2]); + points.add(p1); + + List p2 = new ArrayList<>(); + p2.add(b[1]); + p2.add(b[2]); + points.add(p2); + } + //将所有坐标排序 + Collections.sort(points, new Comparator>() { + @Override + public int compare(List p1, List p2) { + int x1 = p1.get(0); + int y1 = p1.get(1); + int x2 = p2.get(0); + int y2 = p2.get(1); + if (x1 != x2) { + return x1 - x2; + } else { + return y1 - y2; + } + } + + }); + TreeMap treeMap = new TreeMap<>(new Comparator() { + @Override + public int compare(Integer i1, Integer i2) { + return i2 - i1; + } + }); + treeMap.put(0, 1); + int preMax = 0; + + for (List p : points) { + int x = p.get(0); + int y = p.get(1); + if (y < 0) { + Integer v = treeMap.get(-y); + if (v == null) { + treeMap.put(-y, 1); + } else { + treeMap.put(-y, v + 1); + } + } else { + Integer v = treeMap.get(y); + if (v == 1) { + treeMap.remove(y); + } else { + treeMap.put(y, v - 1); + } + } + int curMax = treeMap.firstKey(); + if (curMax != preMax) { + List temp = new ArrayList<>(); + temp.add(x); + temp.add(curMax); + results.add(temp); + preMax = curMax; + } + } + return results; +} +``` + +# 总 + 这道题确实很难想,虽然知道解法了,但还是想不到大神们是怎么想出来的,革命尚未成功,同志仍需努力,继续加油吧。 \ No newline at end of file diff --git a/leetcode-219-ContainsDuplicateII.md b/leetcode-219-ContainsDuplicateII.md index f2e19a644..602214f20 100644 --- a/leetcode-219-ContainsDuplicateII.md +++ b/leetcode-219-ContainsDuplicateII.md @@ -1,105 +1,105 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/219.jpg) - -判断是否有重复的数字出现,并且两个数字最多相隔 `k`。 - -# 解法一 暴力 - -两层循环,判断当前数字后的 `k` 个数字是否有重复数字。 - -```java -public boolean containsNearbyDuplicate(int[] nums, int k) { - int n = nums.length; - for (int i = 0; i < n; i++) { - for (int j = 1; j <= k; j++) { - if (i + j >= n) { - break; - } - if (nums[i] == nums[i + j]) { - return true; - } - } - } - return false; -} -``` - -# 解法二 map - -利用 `hashmap`,`key` 存储值,`value` 存储下标。遍历数组,如果出现重复的值,判断下标的关系。 - -```java -public boolean containsNearbyDuplicate(int[] nums, int k) { - HashMap map = new HashMap<>(); - int n = nums.length; - for (int i = 0; i < n; i++) { - if (map.containsKey(nums[i])) { - int index = map.get(nums[i]); - if (i - index <= k) { - return true; - } - } - //更新当前值的下标 - map.put(nums[i], i); - } - return false; -} -``` - -# 解法三 set - -没想到用 `set` 也能做,参考 [这里](https://leetcode.com/problems/contains-duplicate-ii/discuss/61372/Simple-Java-solution)。 - -因为下标相差不能超过 `k`,所以我们 `set` 中只存储 `k+1` 个连续的数,超过以后就将 `set` 中的第一个数删除。 - -```java -public boolean containsNearbyDuplicate(int[] nums, int k) { - HashSet set = new HashSet<>(); - int n = nums.length; - int i = 0; - //将 k + 1 个数存入 set - for (; i <= k && i < n; i++) { - if (set.contains(nums[i])) { - return true; - } - set.add(nums[i]); - } - for (; i < n; i++) { - //移除 set 中第一个数 - set.remove(nums[i - k - 1]); - //判断 set 中是否有当前数 - if (set.contains(nums[i])) { - return true; - } - //将当前数加入 - set.add(nums[i]); - } - return false; -} -``` - -当然上边的代码,可以利用 `set.add` 的返回值来简化代码。 - -如果要加入的数在 `set` 中已经存在,那么 `add` 就会返回 `false`。我们就不需要调用 `set.contains` 了。 - -```java -public boolean containsNearbyDuplicate(int[] nums, int k) { - HashSet set = new HashSet<>(); - int n = nums.length; - for (int i = 0; i < n; i++) { - if (i > k) { - set.remove(nums[i - k - 1]); - } - if (!set.add(nums[i])) { - return true; - } - } - return false; -} -``` - -# 总 - -前两种解法都比较容易想到,解法三通过一个滑动窗口解决问题,很巧妙。 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/219.jpg) + +判断是否有重复的数字出现,并且两个数字最多相隔 `k`。 + +# 解法一 暴力 + +两层循环,判断当前数字后的 `k` 个数字是否有重复数字。 + +```java +public boolean containsNearbyDuplicate(int[] nums, int k) { + int n = nums.length; + for (int i = 0; i < n; i++) { + for (int j = 1; j <= k; j++) { + if (i + j >= n) { + break; + } + if (nums[i] == nums[i + j]) { + return true; + } + } + } + return false; +} +``` + +# 解法二 map + +利用 `hashmap`,`key` 存储值,`value` 存储下标。遍历数组,如果出现重复的值,判断下标的关系。 + +```java +public boolean containsNearbyDuplicate(int[] nums, int k) { + HashMap map = new HashMap<>(); + int n = nums.length; + for (int i = 0; i < n; i++) { + if (map.containsKey(nums[i])) { + int index = map.get(nums[i]); + if (i - index <= k) { + return true; + } + } + //更新当前值的下标 + map.put(nums[i], i); + } + return false; +} +``` + +# 解法三 set + +没想到用 `set` 也能做,参考 [这里](https://leetcode.com/problems/contains-duplicate-ii/discuss/61372/Simple-Java-solution)。 + +因为下标相差不能超过 `k`,所以我们 `set` 中只存储 `k+1` 个连续的数,超过以后就将 `set` 中的第一个数删除。 + +```java +public boolean containsNearbyDuplicate(int[] nums, int k) { + HashSet set = new HashSet<>(); + int n = nums.length; + int i = 0; + //将 k + 1 个数存入 set + for (; i <= k && i < n; i++) { + if (set.contains(nums[i])) { + return true; + } + set.add(nums[i]); + } + for (; i < n; i++) { + //移除 set 中第一个数 + set.remove(nums[i - k - 1]); + //判断 set 中是否有当前数 + if (set.contains(nums[i])) { + return true; + } + //将当前数加入 + set.add(nums[i]); + } + return false; +} +``` + +当然上边的代码,可以利用 `set.add` 的返回值来简化代码。 + +如果要加入的数在 `set` 中已经存在,那么 `add` 就会返回 `false`。我们就不需要调用 `set.contains` 了。 + +```java +public boolean containsNearbyDuplicate(int[] nums, int k) { + HashSet set = new HashSet<>(); + int n = nums.length; + for (int i = 0; i < n; i++) { + if (i > k) { + set.remove(nums[i - k - 1]); + } + if (!set.add(nums[i])) { + return true; + } + } + return false; +} +``` + +# 总 + +前两种解法都比较容易想到,解法三通过一个滑动窗口解决问题,很巧妙。 + diff --git a/leetcode-220-Contains-DuplicateIII.md b/leetcode-220-Contains-DuplicateIII.md index 02fa5aa9f..32af41ee3 100644 --- a/leetcode-220-Contains-DuplicateIII.md +++ b/leetcode-220-Contains-DuplicateIII.md @@ -1,356 +1,356 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/220.jpg) - -判断是否存在两个数,下标之间相差不超过 `k`,并且两数相差不超过 `t`。 - -先做一下 [219. Contains Duplicate II](https://leetcode.wang/leetcode-219-ContainsDuplicateII.html) ,再做这个题可能更有感觉。 - -# 解法一 暴力 - -两层循环,判断当前数字和下标相距它 `k` 内的数是否存在数值相差不超过 `t` 的。 - -```java -public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) { - int n = nums.length; - for (int i = 0; i < n; i++) { - for (int j = 1; j <= k; j++) { - if (i + j >= n) { - break; - } - if (Math.abs(nums[i] - nums[i + j]) <= t) { - return true; - } - } - } - return false; -} -``` - -上边的看似没有什么问题,但对于一些数相减可能会产生溢出,比如 `Integer.MAX_VALUE - (-2)` 就会产生溢出了,一些溢出可能导致我们最终的结果出问题。关于为什么产生溢出可以参考 [趣谈补码](https://zhuanlan.zhihu.com/p/67227136)。 - -最直接的解决方案,也是最偷懒的方案,题目中给我们的数据是 `int`,我们强制转为 `long` 进行运算,就不会出错了。 - -```java -public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) { - int n = nums.length; - for (int i = 0; i < n; i++) { - for (int j = 1; j <= k; j++) { - if (i + j >= n) { - break; - } - if (Math.abs((long) nums[i] - nums[i + j]) <= t) { - return true; - } - } - } - return false; -} -``` - -题目到这里已经解决了,我们再多思考一下,不借助 `long` 可以做吗? - -两数相减然后取绝对值出现了溢出,也就意味着绝对值的结果大于了 `Integer.MAX_VALUE`,而 `t <= Integer.MAX_VALUE`,也就意味着此时两数相差不满足小于等于 `t` 的条件。 - -换句话讲,如果两数相减取绝对值产生了溢出,那么此时结果一定是大于 `t` 的,可以直接跳过。 - -因此,接下来只需要解决怎么判断是否产生溢出即可。 - -两数相减有四种情况。 - -* 正数减正数,不会产生溢出。 - -* 正数减负数,结果一定是正数,如果此时结果是负数,说明出现了溢出。 - -* 负数减正数,结果一定是负数,如果此时结果是正数,说明出现了溢出。这里还有一种特殊情况需要考虑,我们知道负数比正数多一个。最小的负数是 `-2147483648`,最大的正数是 `2147483647`。 - - 如果我们计算 `-2147483647 - 1 = -2147483648`,虽然结果没有溢出,但如果取绝对值,由于正数不能表示 `2147483648` ,所以这种情况也需要看成溢出。 - -* 负数减负数,不会产生溢出。 - -代码的话,考虑可能产生溢出的两种情况即可。 - -```java -public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) { - int n = nums.length; - for (int i = 0; i < n; i++) { - for (int j = 1; j <= k; j++) { - if (i + j >= n) { - break; - } - int sub = nums[i] - nums[i + j]; - //正数减负数, 0 算作正数,因为 0 - (-2147483648) 会出现溢出 - if (nums[i] >= 0 && nums[i + j] < 0) { - if (sub < 0) { - continue; - } - } - //负数减正数 - if (nums[i] < 0 && nums[i + j] >= 0) { - if (sub > 0 || sub == Integer.MIN_VALUE) { - continue; - } - } - if (Math.abs(sub) <= t) { - return true; - } - } - } - return false; -} -``` - -但是这个解法竟然超时了,百思不得其解,,,大家谁知道可以告诉我为什么。 - -# 解法二 - -考虑下算法的优化,受 [219 题](https://leetcode.wang/leetcode-219-ContainsDuplicateII.html) 的启发,利用一个滑动窗口,我们只考虑当前数前边的窗口内情况。那么问题来了,比较窗口内的什么呢? - -如果我们知道窗口内的最大值和最小值,那就可以优化一下算法,举个例子。 - -```java -k = 3, t = 2, 窗口内 3 个数,当前考虑 x -2 6 3 x 5 -^ ^ - -窗口内的数是 2 6 3,最大数是 max, 最小数是 min -我们把 x 和最大数和最小数比较 -如果 x >= max, 那么如果 x 和 max 的差大于了 t, 那么和窗口内的其他数的差肯定都大于 t, 也就不需要判断了 -如果 x <= min, 那么如果 min 和 x 的差大于了 t, 那么窗口内的其他数和它的差肯定都大于 t, 也就不需要判断了 -如果 min < x < max, 这样的话就只能按照解法一暴力的方式,一个一个判断了 - -如果没有找到符合条件的解, 那么就将窗口中的第一个数删除, 将 x 加入窗口后继续判断 - -2 6 3 x 5 - ^ ^ -继续考虑 5 和窗口内最大数和最小数的情况。 -``` - -上边的分析,我们需要得到最大数和最小数,以及从窗口内删除数。受 [218 题](https://leetcode.wang/leetcode-218-The-Skyline-Problem.html) 启发,我们可以用两个 `TreeMap` ,这样得到最大数或者最小数时间复杂度是 `O(log(n))`,以及删除一个数字也是 `O(log(n))` 。 - -```java -public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) { - //为了得到窗口内的最大数 - TreeMap maxTreeMap = new TreeMap<>(new Comparator() { - @Override - public int compare(Integer i1, Integer i2) { - return i2 - i1; - } - }); - //为了得到窗口内的最小数 - TreeMap minTreeMap = new TreeMap<>(); - int n = nums.length; - if (n == 0) { - return false; - } - maxTreeMap.put(nums[0], 1); - minTreeMap.put(nums[0], 1); - for (int i = 1; i < n; i++) { - //将窗口内的第一个数删除 - if (i > k) { - remove(maxTreeMap, nums[i - k - 1]); - remove(minTreeMap, nums[i - k - 1]); - } - if (maxTreeMap.size() == 0) { - continue; - } - long max = maxTreeMap.firstKey(); - long min = minTreeMap.firstKey(); - //和最大数以及最小数进行比较 - if (nums[i] >= max) { - if (nums[i] - max <= t) { - return true; - } - } else if (nums[i] <= min) { - if (min - nums[i] <= t) { - return true; - } - } else { - for (int j = 1; j <= k; j++) { - if (i - j < 0) { - break; - } - if (Math.abs((long) nums[i] - nums[i - j]) <= t) { - return true; - } - } - } - //当前数加入窗口 - add(maxTreeMap, nums[i]); - add(minTreeMap, nums[i]); - } - return false; -} - -private void add(TreeMap treeMap, int num) { - // TODO Auto-generated method stub - Integer v = treeMap.get(num); - if (v == null) { - treeMap.put(num, 1); - } else { - treeMap.put(num, v + 1); - } -} - -private void remove(TreeMap treeMap, int num) { - // TODO Auto-generated method stub - Integer v = treeMap.get(num); - if (v == 1) { - treeMap.remove(num); - } else { - treeMap.put(num, v - 1); - } -} -``` - -遗憾的是,对于 leetcode 的 test cases,这个解法并没有带来时间上的提升,时间甚至比解法一的暴力还慢。在中国站返回了超时错误,美国站可以 AC 。 - -# 解法三 set - -参考 [这里](https://leetcode.com/problems/contains-duplicate-iii/discuss/61641/C%2B%2B-using-set-(less-10-lines)-with-simple-explanation.) 。 - -这个方法的前提是对 `TreeSet` 这个数据结构要了解。其中有一个方法 `public E ceiling(E e)` ,返回 `treeSet` 中大于等于 `e` 的元素中最小的元素,如果没有大于等于 `e` 的元素就返回 `null`。 - -还有一个对应的方法,`public E floor(E e)`,返回 `treeSet` 中小于等于 `e` 的元素中最大的元素,如果没有小于等于 `e` 的元素就返回 `null`。 - -并且两个方法的时间复杂度都是 `O(log(n))`。 - -知道了这个就好说了,我们依旧是解法二那样的滑动窗口,举个例子。 - -```java -k = 3, t = 2, 窗口内 3 个数用 TreeSet 存储, 当前考虑 x -2 6 3 x 5 -^ ^ -``` - -此时我们去寻找窗口中是否存在 `x - t ~ x + t` 的元素。 - -如果我们调用 `ceilling(x - t)` 返回了 `c`,`c` 是窗口内大于等于 `x - t` 中最小的数。 -只要 `c` 不大于 `x + t`, 那么 `c` 一定是我们要找的了。否则的话,窗口就继续右移。 - -代码的话,由于溢出的问题,运算的时候我们直接用 `long` 强转。 - -```java -public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) { - TreeSet set = new TreeSet<>(); - int n = nums.length; - for (int i = 0; i < n; i++) { - if (i > k) { - set.remove((long)nums[i - k - 1]); - } - Long low = set.ceiling((long) nums[i] - t); - //是否找到了符合条件的数 - if (low != null && low <= (long)nums[i] + t) { - return true; - } - set.add((long) nums[i]); - } - return false; -} -``` - -# 解法四 map - -参考 [这里](https://leetcode.com/problems/contains-duplicate-iii/discuss/61639/JavaPython-one-pass-solution-O(n)-time-O(n)-space-using-buckets)。 - -运用到了桶排序的思想,在 [164 题](https://leetcode.wang/leetcode-164-Maximum-Gap.html) 也使用过桶排序的思想。 - -首先还是滑动窗口的思想,一个窗口一个窗口考虑。 - -不同之处在于,我们把窗口内的数字存在不同编号的桶中。每个桶内存的数字范围是 `t + 1` 个数,这样做的好处是,桶内任意两个数之间的差一定是小于等于 `t` 的。 - -```java -t = 2, 每个桶内的数字范围如下 -编号 ... -2 -1 0 1 ... - ------- ------- ------- ------- -桶内数字范围 | -6 ~ -4 | | -3 ~ -1 | | 0 ~ 2 | | 3 ~ 5 | - ------- ------- ------- ------- -``` - -有了上边的桶,再结合滑动窗口就简单多了,同样的举个例子。 - -```java -k = 3, t = 2, 窗口内 3 个数用上边的桶存储, 当前考虑 x -2 6 3 x 5 -^ ^ -桶中的情况 - 0 1 2 - ------- ------- ------- -| 2 | | 3 | | 6 | - ------- ------- ------- -``` - -接下来我们只需要算出来 `x` 在哪个桶中。 - -如果 `x` 所在桶已经有数字了,那就说明存在和 `x` 相差小于等于 `t` 的数。 - -如果 `x` 所在桶没有数字,因为与 `x` 所在桶不相邻的桶中的数字与 `x` 的差一定大于 `t`,所以只需要考虑与 x 所在桶**相邻的两个桶**中的数字与 `x`的差是否小于等于 `t`。 - -如果没有找到和 `x` 相差小于等于 `t` 的数, 那么窗口右移。从桶中将窗口中第一个数删除, 并且将 `x` 加入桶中 - -接下来需要解决怎么求出一个数所在桶的编号。 - -```java -//w 表示桶中的存储数字范围的个数 -private long getId(long num, long w) { - if (num >= 0) { - return num / w; - } else { - //num 加 1, 把负数移动到从 0 开始, 这样算出来标号最小是 0, 已经用过了, 所以要再减 1 - return (num + 1) / w - 1; - } -} -``` - -「桶」放到代码中我们要什么数据结构存储呢?我们注意到,桶中其实最多就只会有一个数字(如果有两个数字,说明我们已经找到了相差小于等于 `t` 的数,直接结束)。所以我们完全可以用一个 `map` ,`key` 表示桶编号,`value` 表示桶中当前的数字。 - -同样的,为了防止溢出,所有数字我们都用成了 `long`。 - -```java -public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) { - if (t < 0) { - return false; - } - HashMap map = new HashMap<>(); - int n = nums.length; - long w = t + 1; // 一个桶里边数字范围的个数是 t + 1 - for (int i = 0; i < n; i++) { - //删除窗口中第一个数字 - if (i > k) { - map.remove(getId(nums[i - k - 1], w)); - } - //得到当前数的桶编号 - long id = getId(nums[i], w); - if (map.containsKey(id)) { - return true; - } - if (map.containsKey(id + 1) && map.get(id + 1) - nums[i] < w) { - return true; - } - - if (map.containsKey(id - 1) && nums[i] - map.get(id - 1) < w) { - return true; - } - map.put(id, (long) nums[i]); - } - return false; -} - -private long getId(long num, long w) { - if (num >= 0) { - return num / w; - } else { - return (num + 1) / w - 1; - } -} -``` - -# 总 - -解法一暴力比较常规,解法二我应该是在潜意识中受到 [164 题](https://leetcode.wang/leetcode-164-Maximum-Gap.html) 的启发,用到了最大值和最小值,但对当前题并没有起到决定性的优化作用。 - -解法三的话,知道 `treeSet` 中 `ceiling` 方法很关键。并且思想也很棒,我们并没有去判断窗口中的数是否满足和当前数相差小于等于 `t`。而是反过来,去寻找满足条件的数字在窗口中是否存在。这种思维的逆转,在解题中也经常用到。 - -解法四的话,通过对数字的映射,从而将一部分数映射到一个 `id` ,进而通过 `map` 解决问题,很厉害。 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/220.jpg) + +判断是否存在两个数,下标之间相差不超过 `k`,并且两数相差不超过 `t`。 + +先做一下 [219. Contains Duplicate II](https://leetcode.wang/leetcode-219-ContainsDuplicateII.html) ,再做这个题可能更有感觉。 + +# 解法一 暴力 + +两层循环,判断当前数字和下标相距它 `k` 内的数是否存在数值相差不超过 `t` 的。 + +```java +public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) { + int n = nums.length; + for (int i = 0; i < n; i++) { + for (int j = 1; j <= k; j++) { + if (i + j >= n) { + break; + } + if (Math.abs(nums[i] - nums[i + j]) <= t) { + return true; + } + } + } + return false; +} +``` + +上边的看似没有什么问题,但对于一些数相减可能会产生溢出,比如 `Integer.MAX_VALUE - (-2)` 就会产生溢出了,一些溢出可能导致我们最终的结果出问题。关于为什么产生溢出可以参考 [趣谈补码](https://zhuanlan.zhihu.com/p/67227136)。 + +最直接的解决方案,也是最偷懒的方案,题目中给我们的数据是 `int`,我们强制转为 `long` 进行运算,就不会出错了。 + +```java +public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) { + int n = nums.length; + for (int i = 0; i < n; i++) { + for (int j = 1; j <= k; j++) { + if (i + j >= n) { + break; + } + if (Math.abs((long) nums[i] - nums[i + j]) <= t) { + return true; + } + } + } + return false; +} +``` + +题目到这里已经解决了,我们再多思考一下,不借助 `long` 可以做吗? + +两数相减然后取绝对值出现了溢出,也就意味着绝对值的结果大于了 `Integer.MAX_VALUE`,而 `t <= Integer.MAX_VALUE`,也就意味着此时两数相差不满足小于等于 `t` 的条件。 + +换句话讲,如果两数相减取绝对值产生了溢出,那么此时结果一定是大于 `t` 的,可以直接跳过。 + +因此,接下来只需要解决怎么判断是否产生溢出即可。 + +两数相减有四种情况。 + +* 正数减正数,不会产生溢出。 + +* 正数减负数,结果一定是正数,如果此时结果是负数,说明出现了溢出。 + +* 负数减正数,结果一定是负数,如果此时结果是正数,说明出现了溢出。这里还有一种特殊情况需要考虑,我们知道负数比正数多一个。最小的负数是 `-2147483648`,最大的正数是 `2147483647`。 + + 如果我们计算 `-2147483647 - 1 = -2147483648`,虽然结果没有溢出,但如果取绝对值,由于正数不能表示 `2147483648` ,所以这种情况也需要看成溢出。 + +* 负数减负数,不会产生溢出。 + +代码的话,考虑可能产生溢出的两种情况即可。 + +```java +public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) { + int n = nums.length; + for (int i = 0; i < n; i++) { + for (int j = 1; j <= k; j++) { + if (i + j >= n) { + break; + } + int sub = nums[i] - nums[i + j]; + //正数减负数, 0 算作正数,因为 0 - (-2147483648) 会出现溢出 + if (nums[i] >= 0 && nums[i + j] < 0) { + if (sub < 0) { + continue; + } + } + //负数减正数 + if (nums[i] < 0 && nums[i + j] >= 0) { + if (sub > 0 || sub == Integer.MIN_VALUE) { + continue; + } + } + if (Math.abs(sub) <= t) { + return true; + } + } + } + return false; +} +``` + +但是这个解法竟然超时了,百思不得其解,,,大家谁知道可以告诉我为什么。 + +# 解法二 + +考虑下算法的优化,受 [219 题](https://leetcode.wang/leetcode-219-ContainsDuplicateII.html) 的启发,利用一个滑动窗口,我们只考虑当前数前边的窗口内情况。那么问题来了,比较窗口内的什么呢? + +如果我们知道窗口内的最大值和最小值,那就可以优化一下算法,举个例子。 + +```java +k = 3, t = 2, 窗口内 3 个数,当前考虑 x +2 6 3 x 5 +^ ^ + +窗口内的数是 2 6 3,最大数是 max, 最小数是 min +我们把 x 和最大数和最小数比较 +如果 x >= max, 那么如果 x 和 max 的差大于了 t, 那么和窗口内的其他数的差肯定都大于 t, 也就不需要判断了 +如果 x <= min, 那么如果 min 和 x 的差大于了 t, 那么窗口内的其他数和它的差肯定都大于 t, 也就不需要判断了 +如果 min < x < max, 这样的话就只能按照解法一暴力的方式,一个一个判断了 + +如果没有找到符合条件的解, 那么就将窗口中的第一个数删除, 将 x 加入窗口后继续判断 + +2 6 3 x 5 + ^ ^ +继续考虑 5 和窗口内最大数和最小数的情况。 +``` + +上边的分析,我们需要得到最大数和最小数,以及从窗口内删除数。受 [218 题](https://leetcode.wang/leetcode-218-The-Skyline-Problem.html) 启发,我们可以用两个 `TreeMap` ,这样得到最大数或者最小数时间复杂度是 `O(log(n))`,以及删除一个数字也是 `O(log(n))` 。 + +```java +public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) { + //为了得到窗口内的最大数 + TreeMap maxTreeMap = new TreeMap<>(new Comparator() { + @Override + public int compare(Integer i1, Integer i2) { + return i2 - i1; + } + }); + //为了得到窗口内的最小数 + TreeMap minTreeMap = new TreeMap<>(); + int n = nums.length; + if (n == 0) { + return false; + } + maxTreeMap.put(nums[0], 1); + minTreeMap.put(nums[0], 1); + for (int i = 1; i < n; i++) { + //将窗口内的第一个数删除 + if (i > k) { + remove(maxTreeMap, nums[i - k - 1]); + remove(minTreeMap, nums[i - k - 1]); + } + if (maxTreeMap.size() == 0) { + continue; + } + long max = maxTreeMap.firstKey(); + long min = minTreeMap.firstKey(); + //和最大数以及最小数进行比较 + if (nums[i] >= max) { + if (nums[i] - max <= t) { + return true; + } + } else if (nums[i] <= min) { + if (min - nums[i] <= t) { + return true; + } + } else { + for (int j = 1; j <= k; j++) { + if (i - j < 0) { + break; + } + if (Math.abs((long) nums[i] - nums[i - j]) <= t) { + return true; + } + } + } + //当前数加入窗口 + add(maxTreeMap, nums[i]); + add(minTreeMap, nums[i]); + } + return false; +} + +private void add(TreeMap treeMap, int num) { + // TODO Auto-generated method stub + Integer v = treeMap.get(num); + if (v == null) { + treeMap.put(num, 1); + } else { + treeMap.put(num, v + 1); + } +} + +private void remove(TreeMap treeMap, int num) { + // TODO Auto-generated method stub + Integer v = treeMap.get(num); + if (v == 1) { + treeMap.remove(num); + } else { + treeMap.put(num, v - 1); + } +} +``` + +遗憾的是,对于 leetcode 的 test cases,这个解法并没有带来时间上的提升,时间甚至比解法一的暴力还慢。在中国站返回了超时错误,美国站可以 AC 。 + +# 解法三 set + +参考 [这里](https://leetcode.com/problems/contains-duplicate-iii/discuss/61641/C%2B%2B-using-set-(less-10-lines)-with-simple-explanation.) 。 + +这个方法的前提是对 `TreeSet` 这个数据结构要了解。其中有一个方法 `public E ceiling(E e)` ,返回 `treeSet` 中大于等于 `e` 的元素中最小的元素,如果没有大于等于 `e` 的元素就返回 `null`。 + +还有一个对应的方法,`public E floor(E e)`,返回 `treeSet` 中小于等于 `e` 的元素中最大的元素,如果没有小于等于 `e` 的元素就返回 `null`。 + +并且两个方法的时间复杂度都是 `O(log(n))`。 + +知道了这个就好说了,我们依旧是解法二那样的滑动窗口,举个例子。 + +```java +k = 3, t = 2, 窗口内 3 个数用 TreeSet 存储, 当前考虑 x +2 6 3 x 5 +^ ^ +``` + +此时我们去寻找窗口中是否存在 `x - t ~ x + t` 的元素。 + +如果我们调用 `ceilling(x - t)` 返回了 `c`,`c` 是窗口内大于等于 `x - t` 中最小的数。 +只要 `c` 不大于 `x + t`, 那么 `c` 一定是我们要找的了。否则的话,窗口就继续右移。 + +代码的话,由于溢出的问题,运算的时候我们直接用 `long` 强转。 + +```java +public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) { + TreeSet set = new TreeSet<>(); + int n = nums.length; + for (int i = 0; i < n; i++) { + if (i > k) { + set.remove((long)nums[i - k - 1]); + } + Long low = set.ceiling((long) nums[i] - t); + //是否找到了符合条件的数 + if (low != null && low <= (long)nums[i] + t) { + return true; + } + set.add((long) nums[i]); + } + return false; +} +``` + +# 解法四 map + +参考 [这里](https://leetcode.com/problems/contains-duplicate-iii/discuss/61639/JavaPython-one-pass-solution-O(n)-time-O(n)-space-using-buckets)。 + +运用到了桶排序的思想,在 [164 题](https://leetcode.wang/leetcode-164-Maximum-Gap.html) 也使用过桶排序的思想。 + +首先还是滑动窗口的思想,一个窗口一个窗口考虑。 + +不同之处在于,我们把窗口内的数字存在不同编号的桶中。每个桶内存的数字范围是 `t + 1` 个数,这样做的好处是,桶内任意两个数之间的差一定是小于等于 `t` 的。 + +```java +t = 2, 每个桶内的数字范围如下 +编号 ... -2 -1 0 1 ... + ------- ------- ------- ------- +桶内数字范围 | -6 ~ -4 | | -3 ~ -1 | | 0 ~ 2 | | 3 ~ 5 | + ------- ------- ------- ------- +``` + +有了上边的桶,再结合滑动窗口就简单多了,同样的举个例子。 + +```java +k = 3, t = 2, 窗口内 3 个数用上边的桶存储, 当前考虑 x +2 6 3 x 5 +^ ^ +桶中的情况 + 0 1 2 + ------- ------- ------- +| 2 | | 3 | | 6 | + ------- ------- ------- +``` + +接下来我们只需要算出来 `x` 在哪个桶中。 + +如果 `x` 所在桶已经有数字了,那就说明存在和 `x` 相差小于等于 `t` 的数。 + +如果 `x` 所在桶没有数字,因为与 `x` 所在桶不相邻的桶中的数字与 `x` 的差一定大于 `t`,所以只需要考虑与 x 所在桶**相邻的两个桶**中的数字与 `x`的差是否小于等于 `t`。 + +如果没有找到和 `x` 相差小于等于 `t` 的数, 那么窗口右移。从桶中将窗口中第一个数删除, 并且将 `x` 加入桶中 + +接下来需要解决怎么求出一个数所在桶的编号。 + +```java +//w 表示桶中的存储数字范围的个数 +private long getId(long num, long w) { + if (num >= 0) { + return num / w; + } else { + //num 加 1, 把负数移动到从 0 开始, 这样算出来标号最小是 0, 已经用过了, 所以要再减 1 + return (num + 1) / w - 1; + } +} +``` + +「桶」放到代码中我们要什么数据结构存储呢?我们注意到,桶中其实最多就只会有一个数字(如果有两个数字,说明我们已经找到了相差小于等于 `t` 的数,直接结束)。所以我们完全可以用一个 `map` ,`key` 表示桶编号,`value` 表示桶中当前的数字。 + +同样的,为了防止溢出,所有数字我们都用成了 `long`。 + +```java +public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) { + if (t < 0) { + return false; + } + HashMap map = new HashMap<>(); + int n = nums.length; + long w = t + 1; // 一个桶里边数字范围的个数是 t + 1 + for (int i = 0; i < n; i++) { + //删除窗口中第一个数字 + if (i > k) { + map.remove(getId(nums[i - k - 1], w)); + } + //得到当前数的桶编号 + long id = getId(nums[i], w); + if (map.containsKey(id)) { + return true; + } + if (map.containsKey(id + 1) && map.get(id + 1) - nums[i] < w) { + return true; + } + + if (map.containsKey(id - 1) && nums[i] - map.get(id - 1) < w) { + return true; + } + map.put(id, (long) nums[i]); + } + return false; +} + +private long getId(long num, long w) { + if (num >= 0) { + return num / w; + } else { + return (num + 1) / w - 1; + } +} +``` + +# 总 + +解法一暴力比较常规,解法二我应该是在潜意识中受到 [164 题](https://leetcode.wang/leetcode-164-Maximum-Gap.html) 的启发,用到了最大值和最小值,但对当前题并没有起到决定性的优化作用。 + +解法三的话,知道 `treeSet` 中 `ceiling` 方法很关键。并且思想也很棒,我们并没有去判断窗口中的数是否满足和当前数相差小于等于 `t`。而是反过来,去寻找满足条件的数字在窗口中是否存在。这种思维的逆转,在解题中也经常用到。 + +解法四的话,通过对数字的映射,从而将一部分数映射到一个 `id` ,进而通过 `map` 解决问题,很厉害。 + 后三种方法其实都是和滑动窗口有关,通过滑动窗口,我们保证了两个数字的下标一定是小于等于 `k` 的。 \ No newline at end of file diff --git a/leetcode-221-Maximal-Square.md b/leetcode-221-Maximal-Square.md index dd9c8ebf6..71e6e56a1 100644 --- a/leetcode-221-Maximal-Square.md +++ b/leetcode-221-Maximal-Square.md @@ -1,277 +1,277 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/221.jpg) - -输出包含 `1` 的最大正方形面积。 - -# 解法一 暴力 - -参考 [85 题](https://leetcode.wang/leetCode-85-Maximal-Rectangle.html) 解法一,85 题是求包含 `1` 的最大矩形,这道题明显只是 85 题的一个子问题了,85 题的解法稍加修改就能写出这道题了,下边讲一下 [85 题](https://leetcode.wang/leetCode-85-Maximal-Rectangle.html) 的思路。 - -参考[这里](),遍历每个点,求以这个点为矩阵右下角的所有矩阵面积。如下图的两个例子,橙色是当前遍历的点,然后虚线框圈出的矩阵是其中一个矩阵。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/85_2.jpg) - -怎么找出这样的矩阵呢?如下图,如果我们知道了以这个点结尾的连续 1 的个数的话,问题就变得简单了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/85_3.jpg) - -1. 首先求出高度是 1 的矩形面积,也就是它自身的数,也就是上图以橙色的 4 结尾的 「1234」的那个矩形,面积就是 4。 -2. 然后向上扩展一行,高度增加一,选出当前列最小的数字,作为矩阵的宽,如上图,当前列中有 `2` 和 `4` ,那么就将 `2` 作为矩形的宽,求出面积,对应上图的矩形圈出的部分。 -3. 然后继续向上扩展,重复步骤 2。 - -按照上边的方法,遍历所有的点,以当前点为矩阵的右下角,求出所有的矩阵就可以了。下图是某一个点的过程。 - -以橙色的点为右下角,高度为 1。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/85_4.jpg) - -高度为 2。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/85_5.jpg) - -高度为 3。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/85_6.jpg) - -代码的话,把求每个点累计的连续 `1` 的个数用 `width` 保存,同时把求最大矩形的面积和求 `width`融合到同一个循环中。 - -下边是 [85 题](https://leetcode.wang/leetCode-85-Maximal-Rectangle.html) 的代码。 - -```java -public int maximalRectangle(char[][] matrix) { - if (matrix.length == 0) { - return 0; - } - //保存以当前数字结尾的连续 1 的个数 - int[][] width = new int[matrix.length][matrix[0].length]; - int maxArea = 0; - //遍历每一行 - for (int row = 0; row < matrix.length; row++) { - for (int col = 0; col < matrix[0].length; col++) { - //更新 width - if (matrix[row][col] == '1') { - if (col == 0) { - width[row][col] = 1; - } else { - width[row][col] = width[row][col - 1] + 1; - } - } else { - width[row][col] = 0; - } - //记录所有行中最小的数 - int minWidth = width[row][col]; - //向上扩展行 - for (int up_row = row; up_row >= 0; up_row--) { - int height = row - up_row + 1; - //找最小的数作为矩阵的宽 - minWidth = Math.min(minWidth, width[up_row][col]); - //更新面积 - maxArea = Math.max(maxArea, height * minWidth); - } - } - } - return maxArea; -} -``` - -我们先在上边的代码基础上,把这道题做出来,我把修改的地方标记出来了。下边的代码一定程度上已经做了一些优化,把能提前结束的地方提前结束了。 - -```java -public int maximalSquare(char[][] matrix) { - if (matrix.length == 0) { - return 0; - } - //保存以当前数字结尾的连续 1 的个数 - int[][] width = new int[matrix.length][matrix[0].length]; - int maxArea = 0; - /************修改的地方*****************/ - int maxHeight = 0; //记录当前正方形的最大边长 - /*************************************/ - - //遍历每一行 - for (int row = 0; row < matrix.length; row++) { - for (int col = 0; col < matrix[0].length; col++) { - // 更新 width - if (matrix[row][col] == '1') { - if (col == 0) { - width[row][col] = 1; - } else { - width[row][col] = width[row][col - 1] + 1; - } - } else { - width[row][col] = 0; - } - // 记录所有行中最小的数 - int minWidth = width[row][col]; - - /************修改的地方*****************/ - if(minWidth <= maxHeight){ - continue; - } - /*************************************/ - - // 向上扩展行 - for (int up_row = row; up_row >= 0; up_row--) { - int height = row - up_row + 1; - // 找最小的数作为矩阵的宽 - minWidth = Math.min(minWidth, width[up_row][col]); - - /************修改的地方*****************/ - //因为我们找正方形,当前高度大于了最小宽度,可以提前结束 - if(height > minWidth){ - break; - } - // 只有是正方形的时候才更新面积 - if (height == minWidth) { - maxArea = Math.max(maxArea, height * minWidth); - maxHeight = Math.max(maxHeight, height); - break; - } - /*************************************/ - } - } - } - return maxArea; -} -``` - -当然因为我们只考虑正方形,我们可以抛开原来的代码,只参照之前的思路写一个新的代码。 - -首先因为正方形的面积是边长乘边长,所以上边的 `maxArea` 是没有意义的,我们只记录最大边长即可。然后是其它细节的修改,让代码更简洁,代码如下。 - -```java -public int maximalSquare(char[][] matrix) { - if (matrix.length == 0) { - return 0; - } - // 保存以当前数字结尾的连续 1 的个数 - int[][] width = new int[matrix.length][matrix[0].length]; - // 记录最大边长 - int maxSide = 0; - // 遍历每一行 - for (int row = 0; row < matrix.length; row++) { - for (int col = 0; col < matrix[0].length; col++) { - // 更新 width - if (matrix[row][col] == '1') { - if (col == 0) { - width[row][col] = 1; - } else { - width[row][col] = width[row][col - 1] + 1; - } - } else { - width[row][col] = 0; - } - // 当前点作为正方形的右下角进行扩展 - int curWidth = width[row][col]; - // 向上扩展行 - for (int up_row = row; up_row >= 0; up_row--) { - int height = row - up_row + 1; - if (width[up_row][col] <= maxSide || height > curWidth) { - break; - } - maxSide = Math.max(height, maxSide); - } - } - } - return maxSide * maxSide; -} -``` - -# 解法二 动态规划 - -写出解法一,也没有往别的地方想了,参考 [这里](https://leetcode.com/problems/maximal-square/discuss/61803/C%2B%2B-space-optimized-DP),很典型的动态规划的问题了。 - -解法一中我们求每个点的最大边长时,没有考虑到之前的解,事实上之前的解完全可以充分利用。 - -用 `dp[i][j]` 表示以 `matrix[i][j]` 为右下角正方形的最大边长。那么递推式如下。 - -初始条件,那就是第一行和第一列的 `dp[i][j] = matrix[i][j] - '0'`,也就意味着 `dp[i][j]` 要么是 `0` 要么是 `1`。 - -然后就是递推式。 - -`dp[i][j] = Min(dp[i-1][j],dp[i][j-1],dp[i-1][j-1]) + 1`。 - -也就是当前点的左边,上边,左上角的三个点中选一个最小值,然后加 `1`。 - -首先要明确 `dp[i][j]` 表示以 `matrix[i][j]` 为右下角的正方形的最大边长。 - -然后我们从当前点向左和向上扩展,可以参考下边的图。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/221_3.jpg) - -向左最多能扩展多少呢?`dp[i][j-1]` 和 `dp[i-1][j-1]`,当前点左边和左上角选一个较小的。也就是它左边最大的正方形和它左上角最大的正方形的,边长选较小的。 - -向上能能扩展多少呢?`dp[i-1][j]` 和 `dp[i-1][j-1]`,当前点上边和左上角选一个较小的。也就是它上边最大的正方形和它左上角最大的正方形,边长选较小的。 - -然后向左扩展和向上扩展两个最小值中再选一个较小的,最后加上 `1` 就是最终的边长了。 - -最终其实是从三个正方形中最小的边长。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/221_4.jpg) - -代码的话,使用个技巧,那就是行和列多申请一行,这样的话第一行和第一列的情况就不需要单独考虑了。 - -```java -public int maximalSquare(char[][] matrix) { - int rows = matrix.length; - if (rows == 0) { - return 0; - } - int cols = matrix[0].length; - int[][] dp = new int[rows + 1][cols + 1]; - int maxSide = 0; - for (int i = 1; i <= rows; i++) { - for (int j = 1; j <= cols; j++) { - //因为多申请了一行一列,所以这里下标要减 1 - if (matrix[i - 1][j - 1] == '0') { - dp[i][j] = 0; - } else { - dp[i][j] = Math.min(dp[i - 1][j], Math.min(dp[i][j - 1], dp[i - 1][j - 1])) + 1; - maxSide = Math.max(dp[i][j], maxSide); - } - } - } - return maxSide * maxSide; -} - -``` - -然后又是动态规划的经典操作了,空间复杂度的优化,之前也遇到很多了,这里不细讲了。因为更新当前行的时候,只用到前一行的信息,之前的行就没有再用到了,所以我们可以用一维数组,不需要二维矩阵。 - -把图画出来就可以理解出来各个变量的关系了,这里偷懒就不画了。第一次遇到空间复杂度的优化是 [第 5 题](https://leetcode.wang/leetCode-5-Longest-Palindromic-Substring.htm) ,写的比较详细,大家可以看看。后边基本上遇到动态规划,就会考虑空间复杂度的优化,很多很多了。可以在 [https://leetcode.wang/](https://leetcode.wang/) 搜索动态规划做一做。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/221_2.jpg) - -下边是空间复杂度优化的代码,最关键的是用 `pre` 保存了左上角的值。 - -```java -public int maximalSquare(char[][] matrix) { - int rows = matrix.length; - if (rows == 0) { - return 0; - } - int cols = matrix[0].length; - int[] dp = new int[cols + 1]; - int maxSide = 0; - int pre = 0; - for (int i = 1; i <= rows; i++) { - for (int j = 1; j <= cols; j++) { - int temp = dp[j]; - if (matrix[i - 1][j - 1] == '0') { - dp[j] = 0; - } else { - dp[j] = Math.min(dp[j - 1], Math.min(dp[j], pre)) + 1; - maxSide = Math.max(dp[j], maxSide); - } - pre = temp; - } - } - return maxSide * maxSide; -} - -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/221.jpg) + +输出包含 `1` 的最大正方形面积。 + +# 解法一 暴力 + +参考 [85 题](https://leetcode.wang/leetCode-85-Maximal-Rectangle.html) 解法一,85 题是求包含 `1` 的最大矩形,这道题明显只是 85 题的一个子问题了,85 题的解法稍加修改就能写出这道题了,下边讲一下 [85 题](https://leetcode.wang/leetCode-85-Maximal-Rectangle.html) 的思路。 + +参考[这里](),遍历每个点,求以这个点为矩阵右下角的所有矩阵面积。如下图的两个例子,橙色是当前遍历的点,然后虚线框圈出的矩阵是其中一个矩阵。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/85_2.jpg) + +怎么找出这样的矩阵呢?如下图,如果我们知道了以这个点结尾的连续 1 的个数的话,问题就变得简单了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/85_3.jpg) + +1. 首先求出高度是 1 的矩形面积,也就是它自身的数,也就是上图以橙色的 4 结尾的 「1234」的那个矩形,面积就是 4。 +2. 然后向上扩展一行,高度增加一,选出当前列最小的数字,作为矩阵的宽,如上图,当前列中有 `2` 和 `4` ,那么就将 `2` 作为矩形的宽,求出面积,对应上图的矩形圈出的部分。 +3. 然后继续向上扩展,重复步骤 2。 + +按照上边的方法,遍历所有的点,以当前点为矩阵的右下角,求出所有的矩阵就可以了。下图是某一个点的过程。 + +以橙色的点为右下角,高度为 1。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/85_4.jpg) + +高度为 2。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/85_5.jpg) + +高度为 3。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/85_6.jpg) + +代码的话,把求每个点累计的连续 `1` 的个数用 `width` 保存,同时把求最大矩形的面积和求 `width`融合到同一个循环中。 + +下边是 [85 题](https://leetcode.wang/leetCode-85-Maximal-Rectangle.html) 的代码。 + +```java +public int maximalRectangle(char[][] matrix) { + if (matrix.length == 0) { + return 0; + } + //保存以当前数字结尾的连续 1 的个数 + int[][] width = new int[matrix.length][matrix[0].length]; + int maxArea = 0; + //遍历每一行 + for (int row = 0; row < matrix.length; row++) { + for (int col = 0; col < matrix[0].length; col++) { + //更新 width + if (matrix[row][col] == '1') { + if (col == 0) { + width[row][col] = 1; + } else { + width[row][col] = width[row][col - 1] + 1; + } + } else { + width[row][col] = 0; + } + //记录所有行中最小的数 + int minWidth = width[row][col]; + //向上扩展行 + for (int up_row = row; up_row >= 0; up_row--) { + int height = row - up_row + 1; + //找最小的数作为矩阵的宽 + minWidth = Math.min(minWidth, width[up_row][col]); + //更新面积 + maxArea = Math.max(maxArea, height * minWidth); + } + } + } + return maxArea; +} +``` + +我们先在上边的代码基础上,把这道题做出来,我把修改的地方标记出来了。下边的代码一定程度上已经做了一些优化,把能提前结束的地方提前结束了。 + +```java +public int maximalSquare(char[][] matrix) { + if (matrix.length == 0) { + return 0; + } + //保存以当前数字结尾的连续 1 的个数 + int[][] width = new int[matrix.length][matrix[0].length]; + int maxArea = 0; + /************修改的地方*****************/ + int maxHeight = 0; //记录当前正方形的最大边长 + /*************************************/ + + //遍历每一行 + for (int row = 0; row < matrix.length; row++) { + for (int col = 0; col < matrix[0].length; col++) { + // 更新 width + if (matrix[row][col] == '1') { + if (col == 0) { + width[row][col] = 1; + } else { + width[row][col] = width[row][col - 1] + 1; + } + } else { + width[row][col] = 0; + } + // 记录所有行中最小的数 + int minWidth = width[row][col]; + + /************修改的地方*****************/ + if(minWidth <= maxHeight){ + continue; + } + /*************************************/ + + // 向上扩展行 + for (int up_row = row; up_row >= 0; up_row--) { + int height = row - up_row + 1; + // 找最小的数作为矩阵的宽 + minWidth = Math.min(minWidth, width[up_row][col]); + + /************修改的地方*****************/ + //因为我们找正方形,当前高度大于了最小宽度,可以提前结束 + if(height > minWidth){ + break; + } + // 只有是正方形的时候才更新面积 + if (height == minWidth) { + maxArea = Math.max(maxArea, height * minWidth); + maxHeight = Math.max(maxHeight, height); + break; + } + /*************************************/ + } + } + } + return maxArea; +} +``` + +当然因为我们只考虑正方形,我们可以抛开原来的代码,只参照之前的思路写一个新的代码。 + +首先因为正方形的面积是边长乘边长,所以上边的 `maxArea` 是没有意义的,我们只记录最大边长即可。然后是其它细节的修改,让代码更简洁,代码如下。 + +```java +public int maximalSquare(char[][] matrix) { + if (matrix.length == 0) { + return 0; + } + // 保存以当前数字结尾的连续 1 的个数 + int[][] width = new int[matrix.length][matrix[0].length]; + // 记录最大边长 + int maxSide = 0; + // 遍历每一行 + for (int row = 0; row < matrix.length; row++) { + for (int col = 0; col < matrix[0].length; col++) { + // 更新 width + if (matrix[row][col] == '1') { + if (col == 0) { + width[row][col] = 1; + } else { + width[row][col] = width[row][col - 1] + 1; + } + } else { + width[row][col] = 0; + } + // 当前点作为正方形的右下角进行扩展 + int curWidth = width[row][col]; + // 向上扩展行 + for (int up_row = row; up_row >= 0; up_row--) { + int height = row - up_row + 1; + if (width[up_row][col] <= maxSide || height > curWidth) { + break; + } + maxSide = Math.max(height, maxSide); + } + } + } + return maxSide * maxSide; +} +``` + +# 解法二 动态规划 + +写出解法一,也没有往别的地方想了,参考 [这里](https://leetcode.com/problems/maximal-square/discuss/61803/C%2B%2B-space-optimized-DP),很典型的动态规划的问题了。 + +解法一中我们求每个点的最大边长时,没有考虑到之前的解,事实上之前的解完全可以充分利用。 + +用 `dp[i][j]` 表示以 `matrix[i][j]` 为右下角正方形的最大边长。那么递推式如下。 + +初始条件,那就是第一行和第一列的 `dp[i][j] = matrix[i][j] - '0'`,也就意味着 `dp[i][j]` 要么是 `0` 要么是 `1`。 + +然后就是递推式。 + +`dp[i][j] = Min(dp[i-1][j],dp[i][j-1],dp[i-1][j-1]) + 1`。 + +也就是当前点的左边,上边,左上角的三个点中选一个最小值,然后加 `1`。 + +首先要明确 `dp[i][j]` 表示以 `matrix[i][j]` 为右下角的正方形的最大边长。 + +然后我们从当前点向左和向上扩展,可以参考下边的图。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/221_3.jpg) + +向左最多能扩展多少呢?`dp[i][j-1]` 和 `dp[i-1][j-1]`,当前点左边和左上角选一个较小的。也就是它左边最大的正方形和它左上角最大的正方形的,边长选较小的。 + +向上能能扩展多少呢?`dp[i-1][j]` 和 `dp[i-1][j-1]`,当前点上边和左上角选一个较小的。也就是它上边最大的正方形和它左上角最大的正方形,边长选较小的。 + +然后向左扩展和向上扩展两个最小值中再选一个较小的,最后加上 `1` 就是最终的边长了。 + +最终其实是从三个正方形中最小的边长。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/221_4.jpg) + +代码的话,使用个技巧,那就是行和列多申请一行,这样的话第一行和第一列的情况就不需要单独考虑了。 + +```java +public int maximalSquare(char[][] matrix) { + int rows = matrix.length; + if (rows == 0) { + return 0; + } + int cols = matrix[0].length; + int[][] dp = new int[rows + 1][cols + 1]; + int maxSide = 0; + for (int i = 1; i <= rows; i++) { + for (int j = 1; j <= cols; j++) { + //因为多申请了一行一列,所以这里下标要减 1 + if (matrix[i - 1][j - 1] == '0') { + dp[i][j] = 0; + } else { + dp[i][j] = Math.min(dp[i - 1][j], Math.min(dp[i][j - 1], dp[i - 1][j - 1])) + 1; + maxSide = Math.max(dp[i][j], maxSide); + } + } + } + return maxSide * maxSide; +} + +``` + +然后又是动态规划的经典操作了,空间复杂度的优化,之前也遇到很多了,这里不细讲了。因为更新当前行的时候,只用到前一行的信息,之前的行就没有再用到了,所以我们可以用一维数组,不需要二维矩阵。 + +把图画出来就可以理解出来各个变量的关系了,这里偷懒就不画了。第一次遇到空间复杂度的优化是 [第 5 题](https://leetcode.wang/leetCode-5-Longest-Palindromic-Substring.htm) ,写的比较详细,大家可以看看。后边基本上遇到动态规划,就会考虑空间复杂度的优化,很多很多了。可以在 [https://leetcode.wang/](https://leetcode.wang/) 搜索动态规划做一做。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/221_2.jpg) + +下边是空间复杂度优化的代码,最关键的是用 `pre` 保存了左上角的值。 + +```java +public int maximalSquare(char[][] matrix) { + int rows = matrix.length; + if (rows == 0) { + return 0; + } + int cols = matrix[0].length; + int[] dp = new int[cols + 1]; + int maxSide = 0; + int pre = 0; + for (int i = 1; i <= rows; i++) { + for (int j = 1; j <= cols; j++) { + int temp = dp[j]; + if (matrix[i - 1][j - 1] == '0') { + dp[j] = 0; + } else { + dp[j] = Math.min(dp[j - 1], Math.min(dp[j], pre)) + 1; + maxSide = Math.max(dp[j], maxSide); + } + pre = temp; + } + } + return maxSide * maxSide; +} + +``` + +# 总 + 解法一的话是受之前解法的启发,解法二的话算是动态规划的经典应用了,通过之前的解更新当前的解。这里的空间复杂度优化需要多加一个变量来辅助,算是比较难的了。 \ No newline at end of file diff --git a/leetcode-222-Count-Complete-Tree-Nodes.md b/leetcode-222-Count-Complete-Tree-Nodes.md index fc669744f..007f837fb 100644 --- a/leetcode-222-Count-Complete-Tree-Nodes.md +++ b/leetcode-222-Count-Complete-Tree-Nodes.md @@ -1,182 +1,182 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/222.jpg) - -给一个完全二叉树,输出它的节点个数。 - -# 解法之前 - -因为中文翻译的原因,对一些二叉树的概念大家可能不一致,这里我们统一一下。 - -* full binary tree - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/222_2.jpg) - - 下边是维基百科的定义。 - - > A **full** binary tree (sometimes referred to as a **proper**[[15\]](https://en.wikipedia.org/wiki/Binary_tree#cite_note-15) or **plane** binary tree)[[16\]](https://en.wikipedia.org/wiki/Binary_tree#cite_note-16)[[17\]](https://en.wikipedia.org/wiki/Binary_tree#cite_note-17) is a tree in which every node has either 0 or 2 children. Another way of defining a full binary tree is a [recursive definition](https://en.wikipedia.org/wiki/Recursive_definition). A full binary tree is either: - > - A single vertex. - > - A tree whose root node has two subtrees, both of which are full binary trees. - - 每个节点有 `0` 或 `2` 个子节点。 - -* perfect binary tree - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/222_4.jpg) - - 下边是维基百科的定义。 - - > A **perfect** binary tree is a binary tree in which all interior nodes have two children *and* all leaves have the same *depth* or same *level*.An example of a perfect binary tree is the (non-incestuous) [ancestry chart](https://en.wikipedia.org/wiki/Ancestry_chart) of a person to a given depth, as each person has exactly two biological parents (one mother and one father). Provided the ancestry chart always displays the mother and the father on the same side for a given node, their sex can be seen as an analogy of left and right children, *children* being understood here as an algorithmic term. A perfect tree is therefore always complete but a complete tree is not necessarily perfect. - - 除了叶子节点外,所有节点都有两个子节点,并且所有叶子节点拥有相同的高度。 - -* complete binary tree - - ![](https://windliang.oss-cn-beijing.aliyuncs.com/222_3.jpg) - - 下边是维基百科的定义。 - - > In a **complete** binary tree every level, *except possibly the last*, is completely filled, and all nodes in the last level are as far left as possible. It can have between 1 and 2*h* nodes at the last level *h*. An alternative definition is a perfect tree whose rightmost leaves (perhaps all) have been removed. Some authors use the term **complete** to refer instead to a perfect binary tree as defined below, in which case they call this type of tree (with a possibly not filled last level) an **almost complete** binary tree or **nearly complete** binary tree. A complete binary tree can be efficiently represented using an array. - - 除去最后一层后就是一个 perfect binary tree,并且最后一层的节点从左到右依次排列。 - -此外,对于 perfect binary tree,总节点数就是一个等比数列相加。 - -第`1`层 `1` 个节点,第`2`层 `2` 个节点,第`3`层 `4` 个节点,...,第`h`层 $$2^{h - 1}$$ 个节点。 - -相加的话,通过等比数列求和的公式。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/222_5.jpg) - -这里的话,首项 `a1` 是 `1`,公比 `q` 是 `2`,项数 `n` 是 `h`,代入上边的公式,就可以得到节点总数是 $$2^h - 1$$ 。 - -# 解法一 - -首先我们考虑普通的二叉树,怎么求总结数。只需要一个递归即可。 - -```java -public int countNodes(TreeNode root) { - if (root == null) { - return 0; - } - return countNodes(root.left) + countNodes(root.right) + 1; -} -``` - -接下来考虑优化,参考 [这里](https://leetcode.com/problems/count-complete-tree-nodes/discuss/61953/Easy-short-c%2B%2B-recursive-solution) 。 - -上边不管当前是什么二叉树,就直接进入递归了。但如果当前二叉树是一个 perfect binary tree,我们完全可以用公式算出当前二叉树的总节点数。 - -```java -public int countNodes(TreeNode root) { - if (root == null) { - return 0; - } - //因为当前树是 complete binary tree - //所以可以通过从最左边和从最右边得到的高度判断当前是否是 perfect binary tree - TreeNode left = root; - int h1 = 0; - while (left != null) { - h1++; - left = left.left; - } - TreeNode right = root; - int h2 = 0; - while (right != null) { - h2++; - right = right.right; - } - //如果是 perfect binary tree 就套用公式求解 - if (h1 == h2) { - return (1 << h1) - 1; - } else { - return countNodes(root.left) + countNodes(root.right) + 1; - } -} -``` - -上边用了位运算,`1 << h1` 等价于 $$2^{h1}$$ ,记得**加上括号**,因为 `<<` 运算的优先级比加减还要低。 - -时间复杂度的话,分析主函数部分,主要是两部分相加。 - -```java -public int countNodes(TreeNode root) { - if (root == null) { - return 0; - } - return countNodes(root.left) + countNodes(root.right) + 1; -} -``` - -首先 complete binary tree 的左子树和右子树中肯定会有一个 perfect binary tree。 - -假如 `countNodes` 的时间消耗是 `T(n)`。那么对于不是 perfect binary tree 的子树,时间消耗就是 `T(n/2)`,perfect binary tree 那部分因为计算了树的高度,就是 `clog(n)`。 - -```java -T(n) = T(n/2) + c1 lgn - = T(n/4) + c1 lgn + c2 (lgn - 1) - = ... - = T(1) + c [lgn + (lgn-1) + (lgn-2) + ... + 1] - = O(lgn*lgn) -``` - -所以时间复杂度就是 `O(log²(n))`。 - -# 解法二 - -参考 [这里](https://leetcode.com/problems/count-complete-tree-nodes/discuss/61958/Concise-Java-solutions-O(log(n)2)。 - -解法一中,我们注意到对于 complete binary tree ,左子树和右子树中一定存在 perfect binary tree,而 perfect binary tree 的总结点数可以通过公式计算。所以代码也可以按照下边的思路写。 - -通过判断整个树的高度和右子树的高度的关系,从而推断出左子树是 perfect binary tree 还是右子树是 perfect binary tree。 - -如果右子树的高度等于整个树的高度减 `1`,说明左边都填满了,所以左子树是 perfect binary tree ,如下图。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/222_6.jpg) - -否则的话,右子树是 perfect binary tree ,如下图。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/222_7.jpg) - -代码的话,因为是 complete binary tree,所以求高度的时候,可以一直向左遍历。 - -```java -private int getHeight(TreeNode root) { - if (root == null) { - return 0; - } else { - return getHeight(root.left) + 1; - } -} - -public int countNodes(TreeNode root) { - if (root == null) { - return 0; - } - int height = getHeight(root); - int rightHeight = getHeight(root.right); - // 左子树是 perfect binary tree - if (rightHeight == height - 1) { - // 左子树高度和右子树高度相等 - // 左子树加右子树加根节点 - //return (1 << rightHeight) - 1 + countNodes(root.right) + 1; - return (1 << rightHeight) + countNodes(root.right); - // 右子树是 perfect binary tree - } else { - // 左子树加右子树加根节点 - //return countNodes(root.left) + (1 << rightHeight) - 1 + 1; - return countNodes(root.left) + (1 << rightHeight); - } -} -``` - -时间复杂度的话,因为使用了类似二分的思想,每次都去掉了二叉树一半的节点,所以总共会进行 `O(log(n))` 次。每次求高度消耗 `O(log(n))` 。因此总的时间复杂度也是 `O(log²(n))`。 - -# 总 - -解法一相对更容易想到。不过两种解法都抓住了一个本质,对于 complete binary tree ,左子树和右子树中一定存在 perfect binary tree 。根据这条规则实现了两个算法。 - - - - - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/222.jpg) + +给一个完全二叉树,输出它的节点个数。 + +# 解法之前 + +因为中文翻译的原因,对一些二叉树的概念大家可能不一致,这里我们统一一下。 + +* full binary tree + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/222_2.jpg) + + 下边是维基百科的定义。 + + > A **full** binary tree (sometimes referred to as a **proper**[[15\]](https://en.wikipedia.org/wiki/Binary_tree#cite_note-15) or **plane** binary tree)[[16\]](https://en.wikipedia.org/wiki/Binary_tree#cite_note-16)[[17\]](https://en.wikipedia.org/wiki/Binary_tree#cite_note-17) is a tree in which every node has either 0 or 2 children. Another way of defining a full binary tree is a [recursive definition](https://en.wikipedia.org/wiki/Recursive_definition). A full binary tree is either: + > - A single vertex. + > - A tree whose root node has two subtrees, both of which are full binary trees. + + 每个节点有 `0` 或 `2` 个子节点。 + +* perfect binary tree + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/222_4.jpg) + + 下边是维基百科的定义。 + + > A **perfect** binary tree is a binary tree in which all interior nodes have two children *and* all leaves have the same *depth* or same *level*.An example of a perfect binary tree is the (non-incestuous) [ancestry chart](https://en.wikipedia.org/wiki/Ancestry_chart) of a person to a given depth, as each person has exactly two biological parents (one mother and one father). Provided the ancestry chart always displays the mother and the father on the same side for a given node, their sex can be seen as an analogy of left and right children, *children* being understood here as an algorithmic term. A perfect tree is therefore always complete but a complete tree is not necessarily perfect. + + 除了叶子节点外,所有节点都有两个子节点,并且所有叶子节点拥有相同的高度。 + +* complete binary tree + + ![](https://windliang.oss-cn-beijing.aliyuncs.com/222_3.jpg) + + 下边是维基百科的定义。 + + > In a **complete** binary tree every level, *except possibly the last*, is completely filled, and all nodes in the last level are as far left as possible. It can have between 1 and 2*h* nodes at the last level *h*. An alternative definition is a perfect tree whose rightmost leaves (perhaps all) have been removed. Some authors use the term **complete** to refer instead to a perfect binary tree as defined below, in which case they call this type of tree (with a possibly not filled last level) an **almost complete** binary tree or **nearly complete** binary tree. A complete binary tree can be efficiently represented using an array. + + 除去最后一层后就是一个 perfect binary tree,并且最后一层的节点从左到右依次排列。 + +此外,对于 perfect binary tree,总节点数就是一个等比数列相加。 + +第`1`层 `1` 个节点,第`2`层 `2` 个节点,第`3`层 `4` 个节点,...,第`h`层 $$2^{h - 1}$$ 个节点。 + +相加的话,通过等比数列求和的公式。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/222_5.jpg) + +这里的话,首项 `a1` 是 `1`,公比 `q` 是 `2`,项数 `n` 是 `h`,代入上边的公式,就可以得到节点总数是 $$2^h - 1$$ 。 + +# 解法一 + +首先我们考虑普通的二叉树,怎么求总结数。只需要一个递归即可。 + +```java +public int countNodes(TreeNode root) { + if (root == null) { + return 0; + } + return countNodes(root.left) + countNodes(root.right) + 1; +} +``` + +接下来考虑优化,参考 [这里](https://leetcode.com/problems/count-complete-tree-nodes/discuss/61953/Easy-short-c%2B%2B-recursive-solution) 。 + +上边不管当前是什么二叉树,就直接进入递归了。但如果当前二叉树是一个 perfect binary tree,我们完全可以用公式算出当前二叉树的总节点数。 + +```java +public int countNodes(TreeNode root) { + if (root == null) { + return 0; + } + //因为当前树是 complete binary tree + //所以可以通过从最左边和从最右边得到的高度判断当前是否是 perfect binary tree + TreeNode left = root; + int h1 = 0; + while (left != null) { + h1++; + left = left.left; + } + TreeNode right = root; + int h2 = 0; + while (right != null) { + h2++; + right = right.right; + } + //如果是 perfect binary tree 就套用公式求解 + if (h1 == h2) { + return (1 << h1) - 1; + } else { + return countNodes(root.left) + countNodes(root.right) + 1; + } +} +``` + +上边用了位运算,`1 << h1` 等价于 $$2^{h1}$$ ,记得**加上括号**,因为 `<<` 运算的优先级比加减还要低。 + +时间复杂度的话,分析主函数部分,主要是两部分相加。 + +```java +public int countNodes(TreeNode root) { + if (root == null) { + return 0; + } + return countNodes(root.left) + countNodes(root.right) + 1; +} +``` + +首先 complete binary tree 的左子树和右子树中肯定会有一个 perfect binary tree。 + +假如 `countNodes` 的时间消耗是 `T(n)`。那么对于不是 perfect binary tree 的子树,时间消耗就是 `T(n/2)`,perfect binary tree 那部分因为计算了树的高度,就是 `clog(n)`。 + +```java +T(n) = T(n/2) + c1 lgn + = T(n/4) + c1 lgn + c2 (lgn - 1) + = ... + = T(1) + c [lgn + (lgn-1) + (lgn-2) + ... + 1] + = O(lgn*lgn) +``` + +所以时间复杂度就是 `O(log²(n))`。 + +# 解法二 + +参考 [这里](https://leetcode.com/problems/count-complete-tree-nodes/discuss/61958/Concise-Java-solutions-O(log(n)2)。 + +解法一中,我们注意到对于 complete binary tree ,左子树和右子树中一定存在 perfect binary tree,而 perfect binary tree 的总结点数可以通过公式计算。所以代码也可以按照下边的思路写。 + +通过判断整个树的高度和右子树的高度的关系,从而推断出左子树是 perfect binary tree 还是右子树是 perfect binary tree。 + +如果右子树的高度等于整个树的高度减 `1`,说明左边都填满了,所以左子树是 perfect binary tree ,如下图。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/222_6.jpg) + +否则的话,右子树是 perfect binary tree ,如下图。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/222_7.jpg) + +代码的话,因为是 complete binary tree,所以求高度的时候,可以一直向左遍历。 + +```java +private int getHeight(TreeNode root) { + if (root == null) { + return 0; + } else { + return getHeight(root.left) + 1; + } +} + +public int countNodes(TreeNode root) { + if (root == null) { + return 0; + } + int height = getHeight(root); + int rightHeight = getHeight(root.right); + // 左子树是 perfect binary tree + if (rightHeight == height - 1) { + // 左子树高度和右子树高度相等 + // 左子树加右子树加根节点 + //return (1 << rightHeight) - 1 + countNodes(root.right) + 1; + return (1 << rightHeight) + countNodes(root.right); + // 右子树是 perfect binary tree + } else { + // 左子树加右子树加根节点 + //return countNodes(root.left) + (1 << rightHeight) - 1 + 1; + return countNodes(root.left) + (1 << rightHeight); + } +} +``` + +时间复杂度的话,因为使用了类似二分的思想,每次都去掉了二叉树一半的节点,所以总共会进行 `O(log(n))` 次。每次求高度消耗 `O(log(n))` 。因此总的时间复杂度也是 `O(log²(n))`。 + +# 总 + +解法一相对更容易想到。不过两种解法都抓住了一个本质,对于 complete binary tree ,左子树和右子树中一定存在 perfect binary tree 。根据这条规则实现了两个算法。 + + + + + diff --git a/leetcode-223-Rectangle-Area.md b/leetcode-223-Rectangle-Area.md index 7bcb752bf..506ba7199 100644 --- a/leetcode-223-Rectangle-Area.md +++ b/leetcode-223-Rectangle-Area.md @@ -1,71 +1,71 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/223.png) - -求出被两个矩形覆盖的面积。 - -# 解法一 - -这道题没有特殊的算法,就是对题目的分析,下边是我的思路。 - -首先将问题简单化,考虑如果没有重叠区域呢? - -把两个矩形叫做 A 和 B,不重叠就有四种情况,A 在 B 左边,A 在 B 右边,A 在 B 上边,A 在 B 下边。 - -判断上边的四种情况也很简单,比如判断 A 是否在 B 左边,只需要判断 A 的最右边的坐标是否小于 B 的最左边的坐标即可。其他情况类似。 - -此时矩形覆盖的面积就是两个矩形的面积和。 - -接下来考虑有重叠的情况。 - -此时我们只要求出重叠形成的矩形的面积,然后用两个矩形的面积减去重叠矩形的面积就是两个矩形覆盖的面积了。 - -而求重叠矩形的面积也很简单,我们只需要确认重叠矩形的四条边即可,可以结合题目的图想。 - -左边只需选择两个矩形的两条左边靠右的那条。 - -上边只需选择两个矩形的两条上边靠下的那条。 - -右边只需选择两个矩形的两条右边靠左的那条。 - -下边只需选择两个矩形的两条下边靠上的那条。 - -确定以后,重叠的矩形的面积也就可以算出来了。 - -```java -public int computeArea(int A, int B, int C, int D, int E, int F, int G, int H) { - //求第一个矩形的面积 - int length1 = C - A; - int width1 = D - B; - int area1 = length1 * width1; - - //求第二个矩形的面积 - int length2 = G - E; - int width2 = H - F; - int area2 = length2 * width2; - - // 没有重叠的情况 - if (E >= C || G <= A || F >= D || H <= B) { - return area1 + area2; - } - - //确定右边 - int x1 = Math.min(C, G); - //确定左边 - int x2 = Math.max(E, A); - int length3 = x1 - x2; - - //确定上边 - int y1 = Math.min(D, H); - //确定下边 - int y2 = Math.max(F, B); - int width3 = y1 - y2; - int area3 = length3 * width3; - - return area1 + area2 - area3; -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/223.png) + +求出被两个矩形覆盖的面积。 + +# 解法一 + +这道题没有特殊的算法,就是对题目的分析,下边是我的思路。 + +首先将问题简单化,考虑如果没有重叠区域呢? + +把两个矩形叫做 A 和 B,不重叠就有四种情况,A 在 B 左边,A 在 B 右边,A 在 B 上边,A 在 B 下边。 + +判断上边的四种情况也很简单,比如判断 A 是否在 B 左边,只需要判断 A 的最右边的坐标是否小于 B 的最左边的坐标即可。其他情况类似。 + +此时矩形覆盖的面积就是两个矩形的面积和。 + +接下来考虑有重叠的情况。 + +此时我们只要求出重叠形成的矩形的面积,然后用两个矩形的面积减去重叠矩形的面积就是两个矩形覆盖的面积了。 + +而求重叠矩形的面积也很简单,我们只需要确认重叠矩形的四条边即可,可以结合题目的图想。 + +左边只需选择两个矩形的两条左边靠右的那条。 + +上边只需选择两个矩形的两条上边靠下的那条。 + +右边只需选择两个矩形的两条右边靠左的那条。 + +下边只需选择两个矩形的两条下边靠上的那条。 + +确定以后,重叠的矩形的面积也就可以算出来了。 + +```java +public int computeArea(int A, int B, int C, int D, int E, int F, int G, int H) { + //求第一个矩形的面积 + int length1 = C - A; + int width1 = D - B; + int area1 = length1 * width1; + + //求第二个矩形的面积 + int length2 = G - E; + int width2 = H - F; + int area2 = length2 * width2; + + // 没有重叠的情况 + if (E >= C || G <= A || F >= D || H <= B) { + return area1 + area2; + } + + //确定右边 + int x1 = Math.min(C, G); + //确定左边 + int x2 = Math.max(E, A); + int length3 = x1 - x2; + + //确定上边 + int y1 = Math.min(D, H); + //确定下边 + int y2 = Math.max(F, B); + int width3 = y1 - y2; + int area3 = length3 * width3; + + return area1 + area2 - area3; +} +``` + +# 总 + 这道题没有什么算法,只需要分析题目,适当的分类将题目简单化,然后一一攻破即可。 \ No newline at end of file diff --git a/leetcode-224-Basic-Calculator.md b/leetcode-224-Basic-Calculator.md index 80e442ff0..b4f339f93 100644 --- a/leetcode-224-Basic-Calculator.md +++ b/leetcode-224-Basic-Calculator.md @@ -1,668 +1,668 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/224.png) - -简单计算器,只有加法和减法以及括号,并且参与运算的数字都是非负数。 - -# 思路分析 - -科学计算器的话,学栈的时候当时一定会遇到的一个练手项目了。记得当时自己写了黑框的计算器,QT 版的计算器,安卓版的计算器,难点就是处理优先级、括号、正负数的问题,几年过去自己也只记得大体框架了,当时用了两个栈,然后遇到操作数怎么办,遇到操作符怎么办,遇到括号怎么办,总之有一个通用的方法,下边的思路也没有细讲了,直接网上搜到了压栈出栈的过程,然后写了相应的代码供参考。解法三的话算作对这道题专门的解法。 - -# 解法一 逆波兰式 - -[150 题](https://leetcode.wang/leetcode-150-Evaluate-Reverse-Polish-Notation.html) 的时候我们做了逆波兰数。 - -![img](https://windliang.oss-cn-beijing.aliyuncs.com/150.png) - -我们平常用的是中缀表达式,也就是上边 Explanation 中解释的。题目中的是逆波兰式,也叫后缀表达式,一个好处就是只需要运算符,不需要括号,不会产生歧义。 - -计算法则就是,每次找到运算符位置的前两个数字,然后进行计算。 - -然后当时直接用栈写了代码,遇到操作数就入栈,遇到操作符就将栈顶的两个元素弹出进行操作,将结果继续入栈即可。 - -```java -public int evalRPN(String[] tokens) { - Stack stack = new Stack<>(); - for (String t : tokens) { - if (isOperation(t)) { - int a = stringToNumber(stack.pop()); - int b = stringToNumber(stack.pop()); - int ans = eval(b, a, t.charAt(0)); - stack.push(ans + ""); - } else { - stack.push(t); - } - } - return stringToNumber(stack.pop()); -} - -private int eval(int a, int b, char op) { - switch (op) { - case '+': - return a + b; - case '-': - return a - b; - case '*': - return a * b; - case '/': - return a / b; - } - return 0; -} - -private int stringToNumber(String s) { - int sign = 1; - int start = 0; - if (s.charAt(0) == '-') { - sign = -1; - start = 1; - } - int res = 0; - for (int i = start; i < s.length(); i++) { - res = res * 10 + s.charAt(i) - '0'; - } - return res * sign; -} - -private boolean isOperation(String t) { - return t.equals("+") || t.equals("-") || t.equals("*") || t.equals("/"); -} - -``` - -有了上边的代码,我们只需要把题目给的中缀表达式转成后缀表达式,直接调用上边计算逆波兰式就可以了。 - -中缀表达式转后缀表达式也有一个通用的方法,我直接复制 [这里](https://blog.csdn.net/sgbfblog/article/details/8001651) 的规则过来。 - -1)如果遇到操作数,我们就直接将其加入到后缀表达式。 - -2)如果遇到左括号,则我们将其放入到栈中。 - -3)如果遇到一个右括号,则将栈元素弹出,将弹出的操作符加入到后缀表达式直到遇到左括号为止,接着将左括号弹出,但不加入到结果中。 - -4)如果遇到其他的操作符,如(“+”, “-”)等,从栈中弹出元素将其加入到后缀表达式,直到栈顶的元素优先级比当前的优先级低(或者遇到左括号或者栈为空)为止。弹出完这些元素后,最后将当前遇到的操作符压入到栈中。 - -5)如果我们读到了输入的末尾,则将栈中所有元素依次弹出。 - -这里的话注意一下第四条规则,因为题目中只有加法和减法,加法和减法是同优先级的,所以一定不会遇到更低优先级的元素,所以「直到栈顶的元素优先级比当前的优先级低(或者遇到左括号或者栈为空)为止。」这句话可以改成「直到遇到左括号或者栈为空为止」。 - -然后就是对数字的处理,因为数字可能并不只有一位,所以遇到数字的时候要不停的累加。 - -当遇到运算符或者括号的时候就将累加的数字加到后缀表达式中。 - -```java -public int calculate(String s) { - String[] polish = getPolish(s); //转后缀表达式 - return evalRPN(polish); -} - -//中缀表达式转后缀表达式 -private String[] getPolish(String s) { - List res = new ArrayList<>(); - Stack stack = new Stack<>(); - char[] array = s.toCharArray(); - int n = array.length; - int temp = -1; //累加数字,-1 表示当前没有数字 - for (int i = 0; i < n; i++) { - if (array[i] == ' ') { - continue; - } - //遇到数字 - if (isNumber(array[i])) { - //进行数字的累加 - if (temp == -1) { - temp = array[i] - '0'; - } else { - temp = temp * 10 + array[i] - '0'; - } - } else { - //遇到其它操作符,将数字加入到结果中 - if (temp != -1) { - res.add(temp + ""); - temp = -1; - } - if (isOperation(array[i] + "")) { - //遇到操作符将栈中的操作符加入到结果中 - while (!stack.isEmpty()) { - //遇到左括号结束 - if (stack.peek().equals("(")) { - break; - } - res.add(stack.pop()); - } - //当前操作符入栈 - stack.push(array[i] + ""); - } else { - //遇到左括号,直接入栈 - if (array[i] == '(') { - stack.push(array[i] + ""); - } - //遇到右括号,将出栈元素加入到结果中,直到遇到左括号 - if (array[i] == ')') { - while (!stack.peek().equals("(")) { - res.add(stack.pop()); - } - //左括号出栈 - stack.pop(); - } - - } - } - } - //如果有数字,将数字加入到结果 - if (temp != -1) { - res.add(temp + ""); - } - //栈中的其他元素加入到结果 - while (!stack.isEmpty()) { - res.add(stack.pop()); - } - String[] sArray = new String[res.size()]; - //List 转为 数组 - for (int i = 0; i < res.size(); i++) { - sArray[i] = res.get(i); - } - return sArray; -} - -// 下边是 150 题的代码,求后缀表达式的值 -public int evalRPN(String[] tokens) { - Stack stack = new Stack<>(); - for (String t : tokens) { - if (isOperation(t)) { - int a = stringToNumber(stack.pop()); - int b = stringToNumber(stack.pop()); - int ans = eval(b, a, t.charAt(0)); - stack.push(ans + ""); - } else { - stack.push(t); - } - } - return stringToNumber(stack.pop()); -} - -private int eval(int a, int b, char op) { - switch (op) { - case '+': - return a + b; - case '-': - return a - b; - case '*': - return a * b; - case '/': - return a / b; - } - return 0; -} - -private int stringToNumber(String s) { - int sign = 1; - int start = 0; - if (s.charAt(0) == '-') { - sign = -1; - start = 1; - } - int res = 0; - for (int i = start; i < s.length(); i++) { - res = res * 10 + s.charAt(i) - '0'; - } - return res * sign; -} - -private boolean isNumber(char c) { - return c >= '0' && c <= '9'; -} - -private boolean isOperation(String t) { - return t.equals("+") || t.equals("-") || t.equals("*") || t.equals("/"); -} - -``` - -# 解法二 双栈 - -解法一经过了一个中间过程,先转为了后缀表达式然后进行求值。我们其实可以直接利用两个栈,边遍历边进行的,这个方法是我当时上课学的方法。从 [这里](https://www.yanbinghu.com/2019/03/24/57779.html) 把过程贴到下边,和解法一其实有些类似的。 - -1. 使用两个栈,`stack0` 用于存储操作数,`stack1` 用于存储操作符 -2. 从左往右扫描,遇到操作数入栈 `stack0` -3. 遇到操作符时,如果当前优先级低于或等于栈顶操作符优先级,则从 `stack0` 弹出两个元素,从 `stack1` 弹出一个操作符,进行计算,将结果并压入`stack0`,继续与栈顶操作符的比较优先级。 -4. 如果遇到操作符高于栈顶操作符优先级,则直接入栈 `stack1` -5. 遇到左括号,直接入栈 `stack1`。 -6. 遇到右括号,则从 `stack0` 弹出两个元素,从 `stack1` 弹出一个操作符进行计算,并将结果加入到 `stack0` 中,重复这步直到遇到左括号 - -和解法一一样,因为我们只有加法和减法,所以这个流程可以简化一下。 - -第 3 条改成「遇到操作符时,则从 `stack0` 弹出两个元素进行计算,并压入`stack0`,直到栈空或者遇到左括号,最后将当前操作符压入 `stack1` 」 - -第 4 条去掉,已经和第 3 条合并了。 - -整体框架和解法一其实差不多,数字的话同样也需要累加,然后当遇到运算符或者括号的时候就将数字入栈。 - -```java -public int calculate(String s) { - char[] array = s.toCharArray(); - int n = array.length; - Stack num = new Stack<>(); - Stack op = new Stack<>(); - int temp = -1; - for (int i = 0; i < n; i++) { - if (array[i] == ' ') { - continue; - } - // 数字进行累加 - if (isNumber(array[i])) { - if (temp == -1) { - temp = array[i] - '0'; - } else { - temp = temp * 10 + array[i] - '0'; - } - } else { - //将数字入栈 - if (temp != -1) { - num.push(temp); - temp = -1; - } - //遇到操作符 - if (isOperation(array[i] + "")) { - while (!op.isEmpty()) { - if (op.peek() == '(') { - break; - } - //不停的出栈,进行运算,并将结果再次压入栈中 - int num1 = num.pop(); - int num2 = num.pop(); - if (op.pop() == '+') { - num.push(num1 + num2); - } else { - num.push(num2 - num1); - } - - } - //当前运算符入栈 - op.push(array[i]); - } else { - //遇到左括号,直接入栈 - if (array[i] == '(') { - op.push(array[i]); - } - //遇到右括号,不停的进行运算,直到遇到左括号 - if (array[i] == ')') { - while (op.peek() != '(') { - int num1 = num.pop(); - int num2 = num.pop(); - if (op.pop() == '+') { - num.push(num1 + num2); - } else { - num.push(num2 - num1); - } - } - op.pop(); - } - - } - } - } - if (temp != -1) { - num.push(temp); - } - //将栈中的其他元素继续运算 - while (!op.isEmpty()) { - int num1 = num.pop(); - int num2 = num.pop(); - if (op.pop() == '+') { - num.push(num1 + num2); - } else { - num.push(num2 - num1); - } - } - return num.pop(); -} - -private boolean isNumber(char c) { - return c >= '0' && c <= '9'; -} - -private boolean isOperation(String t) { - return t.equals("+") || t.equals("-") || t.equals("*") || t.equals("/"); -} -``` - -有一点需要注意,就是算减法的时候,是 `num2 - num1`,因为我们最初压栈的时候,被减数先压入栈中,然后减数再压栈。出栈的时候,先出来的是减数,然后才是被减数。 - -# 解法三 - -当然,因为只有加法和减法,所以可以不用上边通用的的方法,可以单独分析一下。 - -首先,将问题简单化,如果没有括号的话,该怎么做? - -`1 + 2 - 3 + 5` - -我们把式子看成下边的样子。 - -`+ 1 + 2 - 3 + 5` - -用一个变量 `op` 记录数字前的运算,初始化为 `+`。然后用 `res` 进行累加结果,初始化为 `0`。用 `num` 保存当前的操作数。 - -从上边第二个加号开始,每次遇到操作符的时候,根据之前保存的 `op` 进行累加结果 `res = res op num`,然后 `op` 更新为当前操作符。 - -结合代码理解一下。 - -```java -public int calculateWithOutParentheses(String s) { - char[] array = s.toCharArray(); - int n = array.length; - int res = 0; - int num = 0; - char op = '+'; - for (int i = 0; i < n; i++) { - if (array[i] == ' ') { - continue; - } - if (array[i] >= '0' && array[i] <= '9') { - num = num * 10 + array[i] - '0'; - } else { - if (op == '+') { - res = res + num; - } - if (op == '-') { - res = res - num; - } - num = 0; - op = array[i]; - } - } - if (op == '+') { - res = res + num; - } - if (op == '-') { - res = res - num; - } - return res; -} -``` - -下边考虑包含括号的问题。 - -可能是这样 `1 - (2 + 4) + 1`,可能括号里包含括号 `2 + (1 - (2 + 4)) - 2` - -做法也很简单,当遇到左括号的时候,我们只需要将当前累计的结果,以及当前的 `op` 进行压栈保存,然后各个参数恢复为初始状态,继续进行正常的扫描计算。 - -当遇到右括号的时候,将栈中保存的结果和 `op` 与当前结果进行计算,计算完成后将各个参数恢复为初始状态,然后继续进行正常的扫描计算。 - -举个例子,对于 `2 + 1 - (2 + 4) + 1`,遇到左括号的时候,我们就将已经累加的结果 `3` 和左括号前的 `-` 放入栈中。也就是 `3 - (...) + 1`。 - -接着如果遇到了右括号,括号里边 `2 + 4` 的结果是 `6`,已经算出来了,接着我们从栈里边把 `3` 和 `-` 取出来,也就是再计算 `3 - 6 + 1` 就可以了。 - -结合代码再看一下。 - -```java -public int calculate(String s) { - char[] array = s.toCharArray(); - int n = array.length; - int res = 0; - int num = 0; - Stack opStack = new Stack<>(); - Stack resStack = new Stack<>(); - char op = '+'; - for (int i = 0; i < n; i++) { - if (array[i] == ' ') { - continue; - } - if (array[i] >= '0' && array[i] <= '9') { - num = num * 10 + array[i] - '0'; - } else if (array[i] == '+' || array[i] == '-') { - if (op == '+') { - res = res + num; - } - if (op == '-') { - res = res - num; - } - num = 0; - op = array[i]; - //遇到左括号,将结果和括号前的运算保存,然后将参数重置 - } else if (array[i] == '(') { - resStack.push(res); - opStack.push(op); - - //将参数重置 - op = '+'; - res = 0; - } else if (array[i] == ')') { - //将右括号前的当前运算结束 - //比如 (3 + 4 - 5), 当遇到右括号的时候, - 5 还没有运算 - //(因为我们只有遇到操作符才会进行计算) - if (op == '+') { - res = res + num; - } - if (op == '-') { - res = res - num; - } - - //将之前的结果和操作取出来和当前结果进行运算 - char opBefore = opStack.pop(); - int resBefore = resStack.pop(); - if (opBefore == '+') { - res = resBefore + res; - } - if (opBefore == '-') { - res = resBefore - res; - } - - //将参数重置 - op = '+'; - num = 0; - } - } - if (op == '+') { - res = res + num; - } - if (op == '-') { - res = res - num; - } - return res; -} -``` - -参考 [这里](https://leetcode.com/problems/basic-calculator/discuss/62361/Iterative-Java-solution-with-stack),我们可以将代码简化一些。上边计算的时候,每次都判断当前 `op` 是加号还是减号,比较麻烦。我们可以将两者统一起来。用一个变量 `sign` 代替 `op`。如果是 `+`,`sign` 就等于 `1`。如果是 `-`,`sign` 就等于 `-1`。 - -这样做的好处就是,更新 `res` 的时候,两种情况可以合为一种, `res = res + sign * num`。 - -另外一个好处就是,我们不再需要两个栈。因为此时的 `sign` 也是 `int` 类型,所以可以把它和 `res` 放到同一个栈中。 - -```java -public int calculate(String s) { - char[] array = s.toCharArray(); - int n = array.length; - int res = 0; - int num = 0; - Stack stack = new Stack<>(); - int sign = 1; - for (int i = 0; i < n; i++) { - if (array[i] == ' ') { - continue; - } - if (array[i] >= '0' && array[i] <= '9') { - num = num * 10 + array[i] - '0'; - } else if (array[i] == '+' || array[i] == '-') { - res = res + sign * num; - - //将参数重置 - num = 0; - sign = array[i] == '+' ? 1 : -1; - // 遇到左括号,将结果和括号前的运算保存,然后将参数重置 - } else if (array[i] == '(') { - stack.push(res); - stack.push(sign); - sign = 1; - res = 0; - } else if (array[i] == ')') { - // 将右括号前的运算结束 - res = res + sign * num; - - // 将之前的结果和操作取出来和当前结果进行运算 - int signBefore = stack.pop(); - int resBefore = stack.pop(); - res = resBefore + signBefore * res; - - // 将参数重置 - sign = 1; - num = 0; - } - } - res = res + sign * num; - return res; -} - -``` - -# 解法四 - -[官方题解](https://leetcode.com/problems/basic-calculator/solution/) 中还介绍了另外一种思路,这里也分享一下。 - -这道题的关键就是怎么处理括号的问题,如果我们把括号中的结果全算出来,然后再计算整个表达式也就不难了。 - -比如 `2 - (6 + 5 + 2) + 4`,把括号中的结果得到,然后计算 `2 - 13 + 4` 就很简单了。 - -括号匹配问题的话,自然会想到栈,比如 [20 题](https://leetcode.wang/leetCode-20-Valid Parentheses.html) 的括号匹配。 - -这里的话,我们当然也使用栈,当出现匹配的括号的时候,就计算当前栈中所匹配的括号里的表达式。 - -换句话讲,遍历表达式一直将元素入栈,直到我们遇到右括号就开始出栈,一直出栈直到栈顶是左括号,这期间出栈的元素就是当前括号中的表达式。 - -举个例子。 - -````java -2 - (6 + 5 + 2) + 4 - -遇到右括号前一直入栈 -stack = [ 2, -, (, 6, +, 5, +, 2 ] - -接着我们遇到了右括号,开始出栈,并且边出栈边计算 -将 res 初始化为出栈的第一个元素,res = 2 -stack = [ 2, -, (, 6, +, 5, + ] - -接下来出栈的话,出栈元素依次就是运算符, 操作数, 运算符, 操作数... -我们只需要根据操作符, 然后和 res 累加即可 - -res = res + 5 = 7 - -stack = [ 2, -, (, 6, + ] - -res = res + 6 = 13 - -stack = [ 2, -, ( ] - -stack 遇到了左括号,停止计算,将左括号弹出,然后将 res 中压入栈中 - -stack = [ 2, -, 13 ] - -然后继续遍历原表达式 -stack = [ 2, -, 13, +, 4 ] - -原表达式遍历完成, 然后将 stack 中的元素边出栈边计算 - -将 res 初始化为出栈的第一个元素,res = 4 - -stack = [ 2, -, 13, + ] - -接下来出栈的话,出栈元素依次就是运算符, 操作数, 运算符, 操作数... -我们只需要根据操作符, 然后和 res 累加即可 - -res = res + 13 = 17 - -stack = [ 2, - ] - -res = res - 2 = 15 - -stack = [] - -栈空, 结束运算 -```` - -遗憾的时候,会发现我们计算结果是错误的。原因就是减法不满足交换律,由于我们使用了栈,所以会使得计算倒过来。`A + B` 变成 `B + A` 没什么问题,但是 `A - B` 变成 `B - A` 就会出问题了。 - -解决这个问题也很简单,我们只需要倒着遍历原表达式就可以了,相当于先把 `A - B` 变成了 `B - A` ,通过栈运算的话,我们就是计算 `A - B` 了。 - -然后因为是倒着遍历,所以我们先会遇到右括号,然后是左括号。所以算法变成了遇到左括号后开始出栈进行表达式的计算。 - -还有个问题需要解决,因为我们是倒着遍历,对于有好几位的数字,我们先得到的是数字的低位,最后得到的是数字的高位,所以数字的更新方式和之前的算法都不同。 - -举个例子,对于 `123`,初始化 `num = 0`。 - -由于是倒着遍历,我们先会得到 `3`,此时 `num = 3 * 1 + num = 3`。 - -然后得到 `2`,此时 `num = 2 * 10 + num = 23`。 - -然后得到 `1`,此时 `num = 1 * 100 + num = 123`。 - -也就是每次得到数要依次乘 `1`、`10`、`100` ... 之后再与原结果累加。 - -```java -public int calculate(String s) { - char[] array = s.toCharArray(); - int n = array.length; - Stack stack = new Stack<>(); - int num = 0; - int pow = 1; - for (int i = n - 1; i >= 0; i--) { - if (array[i] == ' ') { - continue; - } - if (array[i] >= '0' && array[i] <= '9') { - num = (array[i] - '0') * pow + num; - pow *= 10; - } else { - //当前是否有数字 - if (pow != 1) { - stack.push(num); - num = 0; - pow = 1; - } - //使用解法三的技巧, 加号用 1 表示, 减号用 -1 表示 - if (array[i] == '+' || array[i] == '-') { - stack.push(array[i] == '+' ? 1 : -1); - //遇到左括号开始计算栈中元素 - } else if (array[i] == '(') { - int res = evaluateExpr(stack); - //右括号出栈 - stack.pop(); - //括号内计算的结果入栈 - stack.push(res); - - } else if (array[i] == ')') { - // 将右括号入栈,用 -2 表示 - stack.push(-2); - } - } - } - //当前是否有数字 - if (pow != 1) { - stack.push(num); - } - //计算去除完括号以后栈中表达式的值 - return evaluateExpr(stack); - -} - -private int evaluateExpr(Stack stack) { - //第一个数作为初始结果 - int res = stack.pop(); - //栈不空,并且没有遇到右括号 - while (!stack.isEmpty() && stack.peek() != -2) { - //第一个出栈的元素是操作符,第二个出栈的元素是操作数 - res = res + stack.pop() * stack.pop(); - } - return res; -} -``` - - - -# 总 - -解法一和解法二算是通用的方法,也就是加上乘除运算以后方法依旧通用。 - -解法三的话,就是针对这道题进行的一个简化的算法,最关键的就是括号的处理,栈的应用很关键。然后就是一个技巧,通过 `sign` 将加减统一起来很漂亮。 - -解法四的话很巧妙,但不容易想到,通过栈找到匹配的括号,然后先计算括号中的元素,最终等效于先把所有括号去掉再统一计算主表达式,相当于主表达式延迟了计算,倒是很符合我们平常计算带括号的表达式的思维。「有括号的,先计算括号里边的」,想起了小学时候的口诀,哈哈。 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/224.png) + +简单计算器,只有加法和减法以及括号,并且参与运算的数字都是非负数。 + +# 思路分析 + +科学计算器的话,学栈的时候当时一定会遇到的一个练手项目了。记得当时自己写了黑框的计算器,QT 版的计算器,安卓版的计算器,难点就是处理优先级、括号、正负数的问题,几年过去自己也只记得大体框架了,当时用了两个栈,然后遇到操作数怎么办,遇到操作符怎么办,遇到括号怎么办,总之有一个通用的方法,下边的思路也没有细讲了,直接网上搜到了压栈出栈的过程,然后写了相应的代码供参考。解法三的话算作对这道题专门的解法。 + +# 解法一 逆波兰式 + +[150 题](https://leetcode.wang/leetcode-150-Evaluate-Reverse-Polish-Notation.html) 的时候我们做了逆波兰数。 + +![img](https://windliang.oss-cn-beijing.aliyuncs.com/150.png) + +我们平常用的是中缀表达式,也就是上边 Explanation 中解释的。题目中的是逆波兰式,也叫后缀表达式,一个好处就是只需要运算符,不需要括号,不会产生歧义。 + +计算法则就是,每次找到运算符位置的前两个数字,然后进行计算。 + +然后当时直接用栈写了代码,遇到操作数就入栈,遇到操作符就将栈顶的两个元素弹出进行操作,将结果继续入栈即可。 + +```java +public int evalRPN(String[] tokens) { + Stack stack = new Stack<>(); + for (String t : tokens) { + if (isOperation(t)) { + int a = stringToNumber(stack.pop()); + int b = stringToNumber(stack.pop()); + int ans = eval(b, a, t.charAt(0)); + stack.push(ans + ""); + } else { + stack.push(t); + } + } + return stringToNumber(stack.pop()); +} + +private int eval(int a, int b, char op) { + switch (op) { + case '+': + return a + b; + case '-': + return a - b; + case '*': + return a * b; + case '/': + return a / b; + } + return 0; +} + +private int stringToNumber(String s) { + int sign = 1; + int start = 0; + if (s.charAt(0) == '-') { + sign = -1; + start = 1; + } + int res = 0; + for (int i = start; i < s.length(); i++) { + res = res * 10 + s.charAt(i) - '0'; + } + return res * sign; +} + +private boolean isOperation(String t) { + return t.equals("+") || t.equals("-") || t.equals("*") || t.equals("/"); +} + +``` + +有了上边的代码,我们只需要把题目给的中缀表达式转成后缀表达式,直接调用上边计算逆波兰式就可以了。 + +中缀表达式转后缀表达式也有一个通用的方法,我直接复制 [这里](https://blog.csdn.net/sgbfblog/article/details/8001651) 的规则过来。 + +1)如果遇到操作数,我们就直接将其加入到后缀表达式。 + +2)如果遇到左括号,则我们将其放入到栈中。 + +3)如果遇到一个右括号,则将栈元素弹出,将弹出的操作符加入到后缀表达式直到遇到左括号为止,接着将左括号弹出,但不加入到结果中。 + +4)如果遇到其他的操作符,如(“+”, “-”)等,从栈中弹出元素将其加入到后缀表达式,直到栈顶的元素优先级比当前的优先级低(或者遇到左括号或者栈为空)为止。弹出完这些元素后,最后将当前遇到的操作符压入到栈中。 + +5)如果我们读到了输入的末尾,则将栈中所有元素依次弹出。 + +这里的话注意一下第四条规则,因为题目中只有加法和减法,加法和减法是同优先级的,所以一定不会遇到更低优先级的元素,所以「直到栈顶的元素优先级比当前的优先级低(或者遇到左括号或者栈为空)为止。」这句话可以改成「直到遇到左括号或者栈为空为止」。 + +然后就是对数字的处理,因为数字可能并不只有一位,所以遇到数字的时候要不停的累加。 + +当遇到运算符或者括号的时候就将累加的数字加到后缀表达式中。 + +```java +public int calculate(String s) { + String[] polish = getPolish(s); //转后缀表达式 + return evalRPN(polish); +} + +//中缀表达式转后缀表达式 +private String[] getPolish(String s) { + List res = new ArrayList<>(); + Stack stack = new Stack<>(); + char[] array = s.toCharArray(); + int n = array.length; + int temp = -1; //累加数字,-1 表示当前没有数字 + for (int i = 0; i < n; i++) { + if (array[i] == ' ') { + continue; + } + //遇到数字 + if (isNumber(array[i])) { + //进行数字的累加 + if (temp == -1) { + temp = array[i] - '0'; + } else { + temp = temp * 10 + array[i] - '0'; + } + } else { + //遇到其它操作符,将数字加入到结果中 + if (temp != -1) { + res.add(temp + ""); + temp = -1; + } + if (isOperation(array[i] + "")) { + //遇到操作符将栈中的操作符加入到结果中 + while (!stack.isEmpty()) { + //遇到左括号结束 + if (stack.peek().equals("(")) { + break; + } + res.add(stack.pop()); + } + //当前操作符入栈 + stack.push(array[i] + ""); + } else { + //遇到左括号,直接入栈 + if (array[i] == '(') { + stack.push(array[i] + ""); + } + //遇到右括号,将出栈元素加入到结果中,直到遇到左括号 + if (array[i] == ')') { + while (!stack.peek().equals("(")) { + res.add(stack.pop()); + } + //左括号出栈 + stack.pop(); + } + + } + } + } + //如果有数字,将数字加入到结果 + if (temp != -1) { + res.add(temp + ""); + } + //栈中的其他元素加入到结果 + while (!stack.isEmpty()) { + res.add(stack.pop()); + } + String[] sArray = new String[res.size()]; + //List 转为 数组 + for (int i = 0; i < res.size(); i++) { + sArray[i] = res.get(i); + } + return sArray; +} + +// 下边是 150 题的代码,求后缀表达式的值 +public int evalRPN(String[] tokens) { + Stack stack = new Stack<>(); + for (String t : tokens) { + if (isOperation(t)) { + int a = stringToNumber(stack.pop()); + int b = stringToNumber(stack.pop()); + int ans = eval(b, a, t.charAt(0)); + stack.push(ans + ""); + } else { + stack.push(t); + } + } + return stringToNumber(stack.pop()); +} + +private int eval(int a, int b, char op) { + switch (op) { + case '+': + return a + b; + case '-': + return a - b; + case '*': + return a * b; + case '/': + return a / b; + } + return 0; +} + +private int stringToNumber(String s) { + int sign = 1; + int start = 0; + if (s.charAt(0) == '-') { + sign = -1; + start = 1; + } + int res = 0; + for (int i = start; i < s.length(); i++) { + res = res * 10 + s.charAt(i) - '0'; + } + return res * sign; +} + +private boolean isNumber(char c) { + return c >= '0' && c <= '9'; +} + +private boolean isOperation(String t) { + return t.equals("+") || t.equals("-") || t.equals("*") || t.equals("/"); +} + +``` + +# 解法二 双栈 + +解法一经过了一个中间过程,先转为了后缀表达式然后进行求值。我们其实可以直接利用两个栈,边遍历边进行的,这个方法是我当时上课学的方法。从 [这里](https://www.yanbinghu.com/2019/03/24/57779.html) 把过程贴到下边,和解法一其实有些类似的。 + +1. 使用两个栈,`stack0` 用于存储操作数,`stack1` 用于存储操作符 +2. 从左往右扫描,遇到操作数入栈 `stack0` +3. 遇到操作符时,如果当前优先级低于或等于栈顶操作符优先级,则从 `stack0` 弹出两个元素,从 `stack1` 弹出一个操作符,进行计算,将结果并压入`stack0`,继续与栈顶操作符的比较优先级。 +4. 如果遇到操作符高于栈顶操作符优先级,则直接入栈 `stack1` +5. 遇到左括号,直接入栈 `stack1`。 +6. 遇到右括号,则从 `stack0` 弹出两个元素,从 `stack1` 弹出一个操作符进行计算,并将结果加入到 `stack0` 中,重复这步直到遇到左括号 + +和解法一一样,因为我们只有加法和减法,所以这个流程可以简化一下。 + +第 3 条改成「遇到操作符时,则从 `stack0` 弹出两个元素进行计算,并压入`stack0`,直到栈空或者遇到左括号,最后将当前操作符压入 `stack1` 」 + +第 4 条去掉,已经和第 3 条合并了。 + +整体框架和解法一其实差不多,数字的话同样也需要累加,然后当遇到运算符或者括号的时候就将数字入栈。 + +```java +public int calculate(String s) { + char[] array = s.toCharArray(); + int n = array.length; + Stack num = new Stack<>(); + Stack op = new Stack<>(); + int temp = -1; + for (int i = 0; i < n; i++) { + if (array[i] == ' ') { + continue; + } + // 数字进行累加 + if (isNumber(array[i])) { + if (temp == -1) { + temp = array[i] - '0'; + } else { + temp = temp * 10 + array[i] - '0'; + } + } else { + //将数字入栈 + if (temp != -1) { + num.push(temp); + temp = -1; + } + //遇到操作符 + if (isOperation(array[i] + "")) { + while (!op.isEmpty()) { + if (op.peek() == '(') { + break; + } + //不停的出栈,进行运算,并将结果再次压入栈中 + int num1 = num.pop(); + int num2 = num.pop(); + if (op.pop() == '+') { + num.push(num1 + num2); + } else { + num.push(num2 - num1); + } + + } + //当前运算符入栈 + op.push(array[i]); + } else { + //遇到左括号,直接入栈 + if (array[i] == '(') { + op.push(array[i]); + } + //遇到右括号,不停的进行运算,直到遇到左括号 + if (array[i] == ')') { + while (op.peek() != '(') { + int num1 = num.pop(); + int num2 = num.pop(); + if (op.pop() == '+') { + num.push(num1 + num2); + } else { + num.push(num2 - num1); + } + } + op.pop(); + } + + } + } + } + if (temp != -1) { + num.push(temp); + } + //将栈中的其他元素继续运算 + while (!op.isEmpty()) { + int num1 = num.pop(); + int num2 = num.pop(); + if (op.pop() == '+') { + num.push(num1 + num2); + } else { + num.push(num2 - num1); + } + } + return num.pop(); +} + +private boolean isNumber(char c) { + return c >= '0' && c <= '9'; +} + +private boolean isOperation(String t) { + return t.equals("+") || t.equals("-") || t.equals("*") || t.equals("/"); +} +``` + +有一点需要注意,就是算减法的时候,是 `num2 - num1`,因为我们最初压栈的时候,被减数先压入栈中,然后减数再压栈。出栈的时候,先出来的是减数,然后才是被减数。 + +# 解法三 + +当然,因为只有加法和减法,所以可以不用上边通用的的方法,可以单独分析一下。 + +首先,将问题简单化,如果没有括号的话,该怎么做? + +`1 + 2 - 3 + 5` + +我们把式子看成下边的样子。 + +`+ 1 + 2 - 3 + 5` + +用一个变量 `op` 记录数字前的运算,初始化为 `+`。然后用 `res` 进行累加结果,初始化为 `0`。用 `num` 保存当前的操作数。 + +从上边第二个加号开始,每次遇到操作符的时候,根据之前保存的 `op` 进行累加结果 `res = res op num`,然后 `op` 更新为当前操作符。 + +结合代码理解一下。 + +```java +public int calculateWithOutParentheses(String s) { + char[] array = s.toCharArray(); + int n = array.length; + int res = 0; + int num = 0; + char op = '+'; + for (int i = 0; i < n; i++) { + if (array[i] == ' ') { + continue; + } + if (array[i] >= '0' && array[i] <= '9') { + num = num * 10 + array[i] - '0'; + } else { + if (op == '+') { + res = res + num; + } + if (op == '-') { + res = res - num; + } + num = 0; + op = array[i]; + } + } + if (op == '+') { + res = res + num; + } + if (op == '-') { + res = res - num; + } + return res; +} +``` + +下边考虑包含括号的问题。 + +可能是这样 `1 - (2 + 4) + 1`,可能括号里包含括号 `2 + (1 - (2 + 4)) - 2` + +做法也很简单,当遇到左括号的时候,我们只需要将当前累计的结果,以及当前的 `op` 进行压栈保存,然后各个参数恢复为初始状态,继续进行正常的扫描计算。 + +当遇到右括号的时候,将栈中保存的结果和 `op` 与当前结果进行计算,计算完成后将各个参数恢复为初始状态,然后继续进行正常的扫描计算。 + +举个例子,对于 `2 + 1 - (2 + 4) + 1`,遇到左括号的时候,我们就将已经累加的结果 `3` 和左括号前的 `-` 放入栈中。也就是 `3 - (...) + 1`。 + +接着如果遇到了右括号,括号里边 `2 + 4` 的结果是 `6`,已经算出来了,接着我们从栈里边把 `3` 和 `-` 取出来,也就是再计算 `3 - 6 + 1` 就可以了。 + +结合代码再看一下。 + +```java +public int calculate(String s) { + char[] array = s.toCharArray(); + int n = array.length; + int res = 0; + int num = 0; + Stack opStack = new Stack<>(); + Stack resStack = new Stack<>(); + char op = '+'; + for (int i = 0; i < n; i++) { + if (array[i] == ' ') { + continue; + } + if (array[i] >= '0' && array[i] <= '9') { + num = num * 10 + array[i] - '0'; + } else if (array[i] == '+' || array[i] == '-') { + if (op == '+') { + res = res + num; + } + if (op == '-') { + res = res - num; + } + num = 0; + op = array[i]; + //遇到左括号,将结果和括号前的运算保存,然后将参数重置 + } else if (array[i] == '(') { + resStack.push(res); + opStack.push(op); + + //将参数重置 + op = '+'; + res = 0; + } else if (array[i] == ')') { + //将右括号前的当前运算结束 + //比如 (3 + 4 - 5), 当遇到右括号的时候, - 5 还没有运算 + //(因为我们只有遇到操作符才会进行计算) + if (op == '+') { + res = res + num; + } + if (op == '-') { + res = res - num; + } + + //将之前的结果和操作取出来和当前结果进行运算 + char opBefore = opStack.pop(); + int resBefore = resStack.pop(); + if (opBefore == '+') { + res = resBefore + res; + } + if (opBefore == '-') { + res = resBefore - res; + } + + //将参数重置 + op = '+'; + num = 0; + } + } + if (op == '+') { + res = res + num; + } + if (op == '-') { + res = res - num; + } + return res; +} +``` + +参考 [这里](https://leetcode.com/problems/basic-calculator/discuss/62361/Iterative-Java-solution-with-stack),我们可以将代码简化一些。上边计算的时候,每次都判断当前 `op` 是加号还是减号,比较麻烦。我们可以将两者统一起来。用一个变量 `sign` 代替 `op`。如果是 `+`,`sign` 就等于 `1`。如果是 `-`,`sign` 就等于 `-1`。 + +这样做的好处就是,更新 `res` 的时候,两种情况可以合为一种, `res = res + sign * num`。 + +另外一个好处就是,我们不再需要两个栈。因为此时的 `sign` 也是 `int` 类型,所以可以把它和 `res` 放到同一个栈中。 + +```java +public int calculate(String s) { + char[] array = s.toCharArray(); + int n = array.length; + int res = 0; + int num = 0; + Stack stack = new Stack<>(); + int sign = 1; + for (int i = 0; i < n; i++) { + if (array[i] == ' ') { + continue; + } + if (array[i] >= '0' && array[i] <= '9') { + num = num * 10 + array[i] - '0'; + } else if (array[i] == '+' || array[i] == '-') { + res = res + sign * num; + + //将参数重置 + num = 0; + sign = array[i] == '+' ? 1 : -1; + // 遇到左括号,将结果和括号前的运算保存,然后将参数重置 + } else if (array[i] == '(') { + stack.push(res); + stack.push(sign); + sign = 1; + res = 0; + } else if (array[i] == ')') { + // 将右括号前的运算结束 + res = res + sign * num; + + // 将之前的结果和操作取出来和当前结果进行运算 + int signBefore = stack.pop(); + int resBefore = stack.pop(); + res = resBefore + signBefore * res; + + // 将参数重置 + sign = 1; + num = 0; + } + } + res = res + sign * num; + return res; +} + +``` + +# 解法四 + +[官方题解](https://leetcode.com/problems/basic-calculator/solution/) 中还介绍了另外一种思路,这里也分享一下。 + +这道题的关键就是怎么处理括号的问题,如果我们把括号中的结果全算出来,然后再计算整个表达式也就不难了。 + +比如 `2 - (6 + 5 + 2) + 4`,把括号中的结果得到,然后计算 `2 - 13 + 4` 就很简单了。 + +括号匹配问题的话,自然会想到栈,比如 [20 题](https://leetcode.wang/leetCode-20-Valid Parentheses.html) 的括号匹配。 + +这里的话,我们当然也使用栈,当出现匹配的括号的时候,就计算当前栈中所匹配的括号里的表达式。 + +换句话讲,遍历表达式一直将元素入栈,直到我们遇到右括号就开始出栈,一直出栈直到栈顶是左括号,这期间出栈的元素就是当前括号中的表达式。 + +举个例子。 + +````java +2 - (6 + 5 + 2) + 4 + +遇到右括号前一直入栈 +stack = [ 2, -, (, 6, +, 5, +, 2 ] + +接着我们遇到了右括号,开始出栈,并且边出栈边计算 +将 res 初始化为出栈的第一个元素,res = 2 +stack = [ 2, -, (, 6, +, 5, + ] + +接下来出栈的话,出栈元素依次就是运算符, 操作数, 运算符, 操作数... +我们只需要根据操作符, 然后和 res 累加即可 + +res = res + 5 = 7 + +stack = [ 2, -, (, 6, + ] + +res = res + 6 = 13 + +stack = [ 2, -, ( ] + +stack 遇到了左括号,停止计算,将左括号弹出,然后将 res 中压入栈中 + +stack = [ 2, -, 13 ] + +然后继续遍历原表达式 +stack = [ 2, -, 13, +, 4 ] + +原表达式遍历完成, 然后将 stack 中的元素边出栈边计算 + +将 res 初始化为出栈的第一个元素,res = 4 + +stack = [ 2, -, 13, + ] + +接下来出栈的话,出栈元素依次就是运算符, 操作数, 运算符, 操作数... +我们只需要根据操作符, 然后和 res 累加即可 + +res = res + 13 = 17 + +stack = [ 2, - ] + +res = res - 2 = 15 + +stack = [] + +栈空, 结束运算 +```` + +遗憾的时候,会发现我们计算结果是错误的。原因就是减法不满足交换律,由于我们使用了栈,所以会使得计算倒过来。`A + B` 变成 `B + A` 没什么问题,但是 `A - B` 变成 `B - A` 就会出问题了。 + +解决这个问题也很简单,我们只需要倒着遍历原表达式就可以了,相当于先把 `A - B` 变成了 `B - A` ,通过栈运算的话,我们就是计算 `A - B` 了。 + +然后因为是倒着遍历,所以我们先会遇到右括号,然后是左括号。所以算法变成了遇到左括号后开始出栈进行表达式的计算。 + +还有个问题需要解决,因为我们是倒着遍历,对于有好几位的数字,我们先得到的是数字的低位,最后得到的是数字的高位,所以数字的更新方式和之前的算法都不同。 + +举个例子,对于 `123`,初始化 `num = 0`。 + +由于是倒着遍历,我们先会得到 `3`,此时 `num = 3 * 1 + num = 3`。 + +然后得到 `2`,此时 `num = 2 * 10 + num = 23`。 + +然后得到 `1`,此时 `num = 1 * 100 + num = 123`。 + +也就是每次得到数要依次乘 `1`、`10`、`100` ... 之后再与原结果累加。 + +```java +public int calculate(String s) { + char[] array = s.toCharArray(); + int n = array.length; + Stack stack = new Stack<>(); + int num = 0; + int pow = 1; + for (int i = n - 1; i >= 0; i--) { + if (array[i] == ' ') { + continue; + } + if (array[i] >= '0' && array[i] <= '9') { + num = (array[i] - '0') * pow + num; + pow *= 10; + } else { + //当前是否有数字 + if (pow != 1) { + stack.push(num); + num = 0; + pow = 1; + } + //使用解法三的技巧, 加号用 1 表示, 减号用 -1 表示 + if (array[i] == '+' || array[i] == '-') { + stack.push(array[i] == '+' ? 1 : -1); + //遇到左括号开始计算栈中元素 + } else if (array[i] == '(') { + int res = evaluateExpr(stack); + //右括号出栈 + stack.pop(); + //括号内计算的结果入栈 + stack.push(res); + + } else if (array[i] == ')') { + // 将右括号入栈,用 -2 表示 + stack.push(-2); + } + } + } + //当前是否有数字 + if (pow != 1) { + stack.push(num); + } + //计算去除完括号以后栈中表达式的值 + return evaluateExpr(stack); + +} + +private int evaluateExpr(Stack stack) { + //第一个数作为初始结果 + int res = stack.pop(); + //栈不空,并且没有遇到右括号 + while (!stack.isEmpty() && stack.peek() != -2) { + //第一个出栈的元素是操作符,第二个出栈的元素是操作数 + res = res + stack.pop() * stack.pop(); + } + return res; +} +``` + + + +# 总 + +解法一和解法二算是通用的方法,也就是加上乘除运算以后方法依旧通用。 + +解法三的话,就是针对这道题进行的一个简化的算法,最关键的就是括号的处理,栈的应用很关键。然后就是一个技巧,通过 `sign` 将加减统一起来很漂亮。 + +解法四的话很巧妙,但不容易想到,通过栈找到匹配的括号,然后先计算括号中的元素,最终等效于先把所有括号去掉再统一计算主表达式,相当于主表达式延迟了计算,倒是很符合我们平常计算带括号的表达式的思维。「有括号的,先计算括号里边的」,想起了小学时候的口诀,哈哈。 + diff --git a/leetcode-225-Implement-Stack-using-Queues.md b/leetcode-225-Implement-Stack-using-Queues.md index a5db472b6..8e8e2f483 100644 --- a/leetcode-225-Implement-Stack-using-Queues.md +++ b/leetcode-225-Implement-Stack-using-Queues.md @@ -1,189 +1,189 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/225.png) - -用队列实现栈的功能,队列我们只能调用 `push to back`, `peek/pop from front`, `size`, and `is empty` 的操作。 - -# 解法一 - -来一个简单粗暴的方法,粗暴到我开始怀疑我理解错了题意。 - -首先肯定是用 `queue` 去保存我们的数据,`push` 的话正常的加到队列。 - -至于 `pop` 的话,因为队列是先进先出,栈是先进后出,所以此时我们应该将队列最后一个元素出队列。我们只需要将队列中除去最后一个元素,其他元素全部出队列,剩下的最后一个就是我们要弹出的。然后把之前出了队列的元素再保存起来即可。 - -然后 `top` 的话和 `pop` 同理。 - -```java -class MyStack { - - Queue queue; - - /** Initialize your data structure here. */ - public MyStack() { - queue = new LinkedList<>(); - } - - /** Push element x onto stack. */ - public void push(int x) { - queue.offer(x); - } - - /** Removes the element on top of the stack and returns that element. */ - public int pop() { - Queue temp = new LinkedList<>(); - //只剩下最后一个元素 - while (queue.size() > 1) { - temp.offer(queue.poll()); - } - //去除最后一个元素 - int remove = queue.poll(); - //原来的元素还原 - while (!temp.isEmpty()) { - queue.offer(temp.poll()); - } - return remove; - } - - /** Get the top element. */ - public int top() { - Queue temp = new LinkedList<>(); - while (queue.size() > 1) { - temp.offer(queue.poll()); - } - int top = queue.poll(); - temp.offer(top); - while (!temp.isEmpty()) { - queue.offer(temp.poll()); - } - return top; - } - - /** Returns whether the stack is empty. */ - public boolean empty() { - return queue.isEmpty(); - } -} - -/** - * Your MyStack object will be instantiated and called as such: - * MyStack obj = new MyStack(); - * obj.push(x); - * int param_2 = obj.pop(); - * int param_3 = obj.top(); - * boolean param_4 = obj.empty(); - */ -``` - -上边代码的受到 [这里](https://leetcode.com/problems/implement-stack-using-queues/discuss/62621/One-Queue-Java-Solution) 的启发,可以稍微优化一下,去掉 `temp`。我们可以边删除边添加。 - -```java -class MyStack { - - Queue queue; - - /** Initialize your data structure here. */ - public MyStack() { - queue = new LinkedList<>(); - } - - /** Push element x onto stack. */ - public void push(int x) { - queue.offer(x); - } - - /** Removes the element on top of the stack and returns that element. */ - public int pop() { - int size = queue.size(); - while (size > 1) { - queue.offer(queue.poll()); - size--; - } - return queue.poll(); - } - - /** Get the top element. */ - public int top() { - int size = queue.size(); - while (size > 1) { - queue.offer(queue.poll()); - size--; - } - int top = queue.poll(); - queue.offer(top); - return top; - } - - /** Returns whether the stack is empty. */ - public boolean empty() { - return queue.isEmpty(); - } -} - -/** - * Your MyStack object will be instantiated and called as such: - * MyStack obj = new MyStack(); - * obj.push(x); - * int param_2 = obj.pop(); - * int param_3 = obj.top(); - * boolean param_4 = obj.empty(); - */ -``` - -# 解法二 - -参考 [这里](https://leetcode.com/problems/implement-stack-using-queues/discuss/62527/A-simple-C%2B%2B-solution),一个非常巧妙优雅的方法。只针对 `push` 做特殊化处理,其他函数直接返回就可以。 - -每次 `push` 一个新元素之后,我们把队列中其他的元素重新排到新元素的后边。 - -```java -class MyStack { - - Queue queue; - - /** Initialize your data structure here. */ - public MyStack() { - queue = new LinkedList<>(); - } - - /** Push element x onto stack. */ - public void push(int x) { - queue.offer(x); - int size = queue.size(); - while (size > 1) { - queue.offer(queue.poll()); - size--; - } - } - - /** Removes the element on top of the stack and returns that element. */ - public int pop() { - return queue.poll(); - } - - /** Get the top element. */ - public int top() { - return queue.peek(); - } - - /** Returns whether the stack is empty. */ - public boolean empty() { - return queue.isEmpty(); - } -} - -/** - * Your MyStack object will be instantiated and called as such: - * MyStack obj = new MyStack(); - * obj.push(x); - * int param_2 = obj.pop(); - * int param_3 = obj.top(); - * boolean param_4 = obj.empty(); - */ -``` - -# 总 - -这道题的话最大的作用就是去理解队列和栈的特性吧,实际中没必要用队列去实现栈,何必呢。 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/225.png) + +用队列实现栈的功能,队列我们只能调用 `push to back`, `peek/pop from front`, `size`, and `is empty` 的操作。 + +# 解法一 + +来一个简单粗暴的方法,粗暴到我开始怀疑我理解错了题意。 + +首先肯定是用 `queue` 去保存我们的数据,`push` 的话正常的加到队列。 + +至于 `pop` 的话,因为队列是先进先出,栈是先进后出,所以此时我们应该将队列最后一个元素出队列。我们只需要将队列中除去最后一个元素,其他元素全部出队列,剩下的最后一个就是我们要弹出的。然后把之前出了队列的元素再保存起来即可。 + +然后 `top` 的话和 `pop` 同理。 + +```java +class MyStack { + + Queue queue; + + /** Initialize your data structure here. */ + public MyStack() { + queue = new LinkedList<>(); + } + + /** Push element x onto stack. */ + public void push(int x) { + queue.offer(x); + } + + /** Removes the element on top of the stack and returns that element. */ + public int pop() { + Queue temp = new LinkedList<>(); + //只剩下最后一个元素 + while (queue.size() > 1) { + temp.offer(queue.poll()); + } + //去除最后一个元素 + int remove = queue.poll(); + //原来的元素还原 + while (!temp.isEmpty()) { + queue.offer(temp.poll()); + } + return remove; + } + + /** Get the top element. */ + public int top() { + Queue temp = new LinkedList<>(); + while (queue.size() > 1) { + temp.offer(queue.poll()); + } + int top = queue.poll(); + temp.offer(top); + while (!temp.isEmpty()) { + queue.offer(temp.poll()); + } + return top; + } + + /** Returns whether the stack is empty. */ + public boolean empty() { + return queue.isEmpty(); + } +} + +/** + * Your MyStack object will be instantiated and called as such: + * MyStack obj = new MyStack(); + * obj.push(x); + * int param_2 = obj.pop(); + * int param_3 = obj.top(); + * boolean param_4 = obj.empty(); + */ +``` + +上边代码的受到 [这里](https://leetcode.com/problems/implement-stack-using-queues/discuss/62621/One-Queue-Java-Solution) 的启发,可以稍微优化一下,去掉 `temp`。我们可以边删除边添加。 + +```java +class MyStack { + + Queue queue; + + /** Initialize your data structure here. */ + public MyStack() { + queue = new LinkedList<>(); + } + + /** Push element x onto stack. */ + public void push(int x) { + queue.offer(x); + } + + /** Removes the element on top of the stack and returns that element. */ + public int pop() { + int size = queue.size(); + while (size > 1) { + queue.offer(queue.poll()); + size--; + } + return queue.poll(); + } + + /** Get the top element. */ + public int top() { + int size = queue.size(); + while (size > 1) { + queue.offer(queue.poll()); + size--; + } + int top = queue.poll(); + queue.offer(top); + return top; + } + + /** Returns whether the stack is empty. */ + public boolean empty() { + return queue.isEmpty(); + } +} + +/** + * Your MyStack object will be instantiated and called as such: + * MyStack obj = new MyStack(); + * obj.push(x); + * int param_2 = obj.pop(); + * int param_3 = obj.top(); + * boolean param_4 = obj.empty(); + */ +``` + +# 解法二 + +参考 [这里](https://leetcode.com/problems/implement-stack-using-queues/discuss/62527/A-simple-C%2B%2B-solution),一个非常巧妙优雅的方法。只针对 `push` 做特殊化处理,其他函数直接返回就可以。 + +每次 `push` 一个新元素之后,我们把队列中其他的元素重新排到新元素的后边。 + +```java +class MyStack { + + Queue queue; + + /** Initialize your data structure here. */ + public MyStack() { + queue = new LinkedList<>(); + } + + /** Push element x onto stack. */ + public void push(int x) { + queue.offer(x); + int size = queue.size(); + while (size > 1) { + queue.offer(queue.poll()); + size--; + } + } + + /** Removes the element on top of the stack and returns that element. */ + public int pop() { + return queue.poll(); + } + + /** Get the top element. */ + public int top() { + return queue.peek(); + } + + /** Returns whether the stack is empty. */ + public boolean empty() { + return queue.isEmpty(); + } +} + +/** + * Your MyStack object will be instantiated and called as such: + * MyStack obj = new MyStack(); + * obj.push(x); + * int param_2 = obj.pop(); + * int param_3 = obj.top(); + * boolean param_4 = obj.empty(); + */ +``` + +# 总 + +这道题的话最大的作用就是去理解队列和栈的特性吧,实际中没必要用队列去实现栈,何必呢。 + `leetcode` 上还有很多其他的解法,这里也就不介绍了,基本上看了作者的代码就能明白作者的想法了。解法二应该就是相对来说最完美的解法了。 \ No newline at end of file diff --git a/leetcode-226-Invert-Binary-Tree.md b/leetcode-226-Invert-Binary-Tree.md index 7d1985991..ec02ace96 100644 --- a/leetcode-226-Invert-Binary-Tree.md +++ b/leetcode-226-Invert-Binary-Tree.md @@ -1,81 +1,81 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/226.png) - -反转二叉树,将二叉树所有的节点的左右两个孩子交换。 - -# 解法一 递归 - -对于二叉树的问题,用递归写的话就会异常简单了。交换左右节点,然后左右节点交给递归即可。 - -```java -public TreeNode invertTree(TreeNode root) { - if (root == null) { - return root; - } - - TreeNode temp = root.left; - root.left = root.right; - root.right = temp; - - invertTree(root.left); - invertTree(root.right); - return root; -} -``` - -# 解法二 DFS 栈 - -当然递归都可以用栈模拟,因为解法一的递归比较简单,所以改写也比较容易。 - -```java -public TreeNode invertTree(TreeNode root) { - Stack stack = new Stack<>(); - stack.push(root); - while (!stack.isEmpty()) { - TreeNode cur = stack.pop(); - if (cur == null) { - continue; - } - TreeNode temp = cur.left; - cur.left = cur.right; - cur.right = temp; - - stack.push(cur.right); - stack.push(cur.left); - } - return root; -} -``` - -# 解法三 BFS 队列 - -既然可以 DFS,那么也可以 BFS,只需要将解法二的栈改成队列即可。代码不用怎么变,但二叉树的遍历顺序完全改变了。 - -```java -public TreeNode invertTree(TreeNode root) { - Queue queue = new LinkedList<>(); - queue.offer(root); - while (!queue.isEmpty()) { - TreeNode cur = queue.poll(); - if (cur == null) { - continue; - } - TreeNode temp = cur.left; - cur.left = cur.right; - cur.right = temp; - - queue.offer(cur.left); - queue.offer(cur.right); - } - return root; -} -``` - -# 总 - -一道比较简单的题,用递归很快就可以解决。之前一直认为,递归改写成解法二或者解法三的迭代那样会更好一些,因为可以防止递归的堆栈溢出。虽然也有缺点,那就是代码会相对更复杂些,可读性有些降低。 - -刚才看到 [王垠](https://www.yinwang.org/) 大神的一个不一样的观点,分享一下。 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/226.png) + +反转二叉树,将二叉树所有的节点的左右两个孩子交换。 + +# 解法一 递归 + +对于二叉树的问题,用递归写的话就会异常简单了。交换左右节点,然后左右节点交给递归即可。 + +```java +public TreeNode invertTree(TreeNode root) { + if (root == null) { + return root; + } + + TreeNode temp = root.left; + root.left = root.right; + root.right = temp; + + invertTree(root.left); + invertTree(root.right); + return root; +} +``` + +# 解法二 DFS 栈 + +当然递归都可以用栈模拟,因为解法一的递归比较简单,所以改写也比较容易。 + +```java +public TreeNode invertTree(TreeNode root) { + Stack stack = new Stack<>(); + stack.push(root); + while (!stack.isEmpty()) { + TreeNode cur = stack.pop(); + if (cur == null) { + continue; + } + TreeNode temp = cur.left; + cur.left = cur.right; + cur.right = temp; + + stack.push(cur.right); + stack.push(cur.left); + } + return root; +} +``` + +# 解法三 BFS 队列 + +既然可以 DFS,那么也可以 BFS,只需要将解法二的栈改成队列即可。代码不用怎么变,但二叉树的遍历顺序完全改变了。 + +```java +public TreeNode invertTree(TreeNode root) { + Queue queue = new LinkedList<>(); + queue.offer(root); + while (!queue.isEmpty()) { + TreeNode cur = queue.poll(); + if (cur == null) { + continue; + } + TreeNode temp = cur.left; + cur.left = cur.right; + cur.right = temp; + + queue.offer(cur.left); + queue.offer(cur.right); + } + return root; +} +``` + +# 总 + +一道比较简单的题,用递归很快就可以解决。之前一直认为,递归改写成解法二或者解法三的迭代那样会更好一些,因为可以防止递归的堆栈溢出。虽然也有缺点,那就是代码会相对更复杂些,可读性有些降低。 + +刚才看到 [王垠](https://www.yinwang.org/) 大神的一个不一样的观点,分享一下。 + ![](https://windliang.oss-cn-beijing.aliyuncs.com/226_2.jpg) \ No newline at end of file diff --git a/leetcode-227-Basic-CalculatorII.md b/leetcode-227-Basic-CalculatorII.md index a1d03a287..2345af290 100644 --- a/leetcode-227-Basic-CalculatorII.md +++ b/leetcode-227-Basic-CalculatorII.md @@ -1,296 +1,296 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/227.png) - -基础计算器,只有加减乘除,正数,整数。 - -# 思路分析 - -[224 题](https://leetcode.wang/leetcode-224-Basic-Calculator.html) 已经介绍了两种通用的计算器的解法,一种是利用后缀表达式,一种是双栈,这里就直接在 [224 题](https://leetcode.wang/leetcode-224-Basic-Calculator.html) 的基础上改了,大家可以先去做一下。 - -# 解法一 后缀表达式 - -[150 题](https://leetcode.wang/leetcode-150-Evaluate-Reverse-Polish-Notation.html) 已经写了后缀表达式的求值,这里的话我们主要是写中缀表达式转后缀表达式,下边是规则。 - -1)如果遇到操作数,我们就直接将其加入到后缀表达式。 - -2)如果遇到左括号,则我们将其放入到栈中。 - -3)如果遇到一个右括号,则将栈元素弹出,将弹出的操作符加入到后缀表达式直到遇到左括号为止,接着将左括号弹出,但不加入到结果中。 - -4)如果遇到其他的操作符,如(“+”, “-”)等,从栈中弹出元素将其加入到后缀表达式,直到栈顶的元素优先级比当前的优先级低(或者遇到左括号或者栈为空)为止。弹出完这些元素后,最后将当前遇到的操作符压入到栈中。 - -5)如果我们读到了输入的末尾,则将栈中所有元素依次弹出。 - -这道题比较简单,不用考虑括号,只需要判断当前操作符和栈顶操作符的优先级。 - -```java -//op1 > op2 的时候返回 true, 其他情况都返回 false -private boolean compare(String op1, String op2) { - if (op1.equals("*") || op1.equals("/")) { - return op2.equals("+") || op2.equals("-"); - } - return false; -} -``` - -下边是整体的代码,供参考。 - -```java -public int calculate(String s) { - String[] polish = getPolish(s); // 转后缀表达式 - return evalRPN(polish); -} - -// 中缀表达式转后缀表达式 -private String[] getPolish(String s) { - List res = new ArrayList<>(); - Stack stack = new Stack<>(); - char[] array = s.toCharArray(); - int n = array.length; - int temp = -1; // 累加数字,-1 表示当前没有数字 - for (int i = 0; i < n; i++) { - if (array[i] == ' ') { - continue; - } - // 遇到数字 - if (isNumber(array[i])) { - // 进行数字的累加 - if (temp == -1) { - temp = array[i] - '0'; - } else { - temp = temp * 10 + array[i] - '0'; - } - } else { - // 遇到其它操作符,将数字加入到结果中 - if (temp != -1) { - res.add(temp + ""); - temp = -1; - } - // 遇到操作符将栈中的操作符加入到结果中 - while (!stack.isEmpty()) { - // 栈顶优先级更低就结束 - if (compare(array[i]+"",stack.peek())) { - break; - } - res.add(stack.pop()); - } - // 当前操作符入栈 - stack.push(array[i] + ""); - - } - } - // 如果有数字,将数字加入到结果 - if (temp != -1) { - res.add(temp + ""); - } - // 栈中的其他元素加入到结果 - while (!stack.isEmpty()) { - res.add(stack.pop()); - } - String[] sArray = new String[res.size()]; - // List 转为 数组 - for (int i = 0; i < res.size(); i++) { - sArray[i] = res.get(i); - } - return sArray; -} - -private boolean compare(String op1, String op2) { - if (op1.equals("*") || op1.equals("/")) { - return op2.equals("+") || op2.equals("-"); - } - return false; -} - -// 下边是 150 题的代码,求后缀表达式的值 -public int evalRPN(String[] tokens) { - Stack stack = new Stack<>(); - for (String t : tokens) { - if (isOperation(t)) { - int a = stringToNumber(stack.pop()); - int b = stringToNumber(stack.pop()); - int ans = eval(b, a, t.charAt(0)); - stack.push(ans + ""); - } else { - stack.push(t); - } - } - return stringToNumber(stack.pop()); -} - -private int eval(int a, int b, char op) { - switch (op) { - case '+': - return a + b; - case '-': - return a - b; - case '*': - return a * b; - case '/': - return a / b; - } - return 0; -} - -private int stringToNumber(String s) { - int sign = 1; - int start = 0; - if (s.charAt(0) == '-') { - sign = -1; - start = 1; - } - int res = 0; - for (int i = start; i < s.length(); i++) { - res = res * 10 + s.charAt(i) - '0'; - } - return res * sign; -} - -private boolean isNumber(char c) { - return c >= '0' && c <= '9'; -} - -private boolean isOperation(String t) { - return t.equals("+") || t.equals("-") || t.equals("*") || t.equals("/"); -} -``` - -# 解法二 双栈 - -规则如下。 - -1. 使用两个栈,`stack0` 用于存储操作数,`stack1` 用于存储操作符 -2. 从左往右扫描,遇到操作数入栈 `stack0` -3. 遇到操作符时,如果当前优先级低于或等于栈顶操作符优先级,则从 `stack0` 弹出两个元素,从 `stack1` 弹出一个操作符,进行计算,将结果并压入`stack0`,继续与栈顶操作符的比较优先级。 -4. 如果遇到操作符高于栈顶操作符优先级,则直接入栈 `stack1` -5. 遇到左括号,直接入栈 `stack1`。 -6. 遇到右括号,则从 `stack0` 弹出两个元素,从 `stack1` 弹出一个操作符进行计算,并将结果加入到 `stack0` 中,重复这步直到遇到左括号 - -同样的不需要考虑括号,会变得更简单一些。 - -```java -public int calculate(String s) { - char[] array = s.toCharArray(); - int n = array.length; - Stack num = new Stack<>(); - Stack op = new Stack<>(); - int temp = -1; - for (int i = 0; i < n; i++) { - if (array[i] == ' ') { - continue; - } - // 数字进行累加 - if (isNumber(array[i])) { - if (temp == -1) { - temp = array[i] - '0'; - } else { - temp = temp * 10 + array[i] - '0'; - } - } else { - // 将数字入栈 - if (temp != -1) { - num.push(temp); - temp = -1; - } - // 遇到操作符 - while (!op.isEmpty()) { - //遇到更低优先级的话就结束 - if (compare(array[i], op.peek())) { - break; - } - // 不停的出栈,进行运算,并将结果再次压入栈中 - int num1 = num.pop(); - int num2 = num.pop(); - num.push(eval(num1, num2, op.pop())); - } - // 当前运算符入栈 - op.push(array[i]); - - } - } - if (temp != -1) { - num.push(temp); - } - // 将栈中的其他元素继续运算 - while (!op.isEmpty()) { - int num1 = num.pop(); - int num2 = num.pop(); - num.push(eval(num1, num2, op.pop())); - } - return num.pop(); -} - -private boolean compare(char op1, char op2) { - if(op1 == '*' || op1 == '/'){ - return op2 == '+' || op2 == '-'; - } - return false; -} - -private boolean isNumber(char c) { - return c >= '0' && c <= '9'; -} -private int eval(int a, int b, char op) { - switch (op) { - case '+': - return a + b; - case '-': - return b - a; - case '*': - return a * b; - case '/': - return b / a; - } - return 0; -} -``` - -和 [224 题](https://leetcode.wang/leetcode-224-Basic-Calculator.html) 一样需要注意减法和除法,由于使用了栈,所以算的时候两个数字要反一下。 - -# 解法三 - -分享一下 [这里](https://leetcode.com/problems/basic-calculator-ii/discuss/63003/Share-my-java-solution) 的解法,属于专门针对这道题的解法。 - -把减法、乘法、除法在遍历过程中将结果计算出来,最后将所有结果累加。 - -```java -public int calculate(String s) { - int len; - if(s==null || (len = s.length())==0) return 0; - Stack stack = new Stack(); - int num = 0; - char sign = '+'; - for(int i=0;i op2 的时候返回 true, 其他情况都返回 false +private boolean compare(String op1, String op2) { + if (op1.equals("*") || op1.equals("/")) { + return op2.equals("+") || op2.equals("-"); + } + return false; +} +``` + +下边是整体的代码,供参考。 + +```java +public int calculate(String s) { + String[] polish = getPolish(s); // 转后缀表达式 + return evalRPN(polish); +} + +// 中缀表达式转后缀表达式 +private String[] getPolish(String s) { + List res = new ArrayList<>(); + Stack stack = new Stack<>(); + char[] array = s.toCharArray(); + int n = array.length; + int temp = -1; // 累加数字,-1 表示当前没有数字 + for (int i = 0; i < n; i++) { + if (array[i] == ' ') { + continue; + } + // 遇到数字 + if (isNumber(array[i])) { + // 进行数字的累加 + if (temp == -1) { + temp = array[i] - '0'; + } else { + temp = temp * 10 + array[i] - '0'; + } + } else { + // 遇到其它操作符,将数字加入到结果中 + if (temp != -1) { + res.add(temp + ""); + temp = -1; + } + // 遇到操作符将栈中的操作符加入到结果中 + while (!stack.isEmpty()) { + // 栈顶优先级更低就结束 + if (compare(array[i]+"",stack.peek())) { + break; + } + res.add(stack.pop()); + } + // 当前操作符入栈 + stack.push(array[i] + ""); + + } + } + // 如果有数字,将数字加入到结果 + if (temp != -1) { + res.add(temp + ""); + } + // 栈中的其他元素加入到结果 + while (!stack.isEmpty()) { + res.add(stack.pop()); + } + String[] sArray = new String[res.size()]; + // List 转为 数组 + for (int i = 0; i < res.size(); i++) { + sArray[i] = res.get(i); + } + return sArray; +} + +private boolean compare(String op1, String op2) { + if (op1.equals("*") || op1.equals("/")) { + return op2.equals("+") || op2.equals("-"); + } + return false; +} + +// 下边是 150 题的代码,求后缀表达式的值 +public int evalRPN(String[] tokens) { + Stack stack = new Stack<>(); + for (String t : tokens) { + if (isOperation(t)) { + int a = stringToNumber(stack.pop()); + int b = stringToNumber(stack.pop()); + int ans = eval(b, a, t.charAt(0)); + stack.push(ans + ""); + } else { + stack.push(t); + } + } + return stringToNumber(stack.pop()); +} + +private int eval(int a, int b, char op) { + switch (op) { + case '+': + return a + b; + case '-': + return a - b; + case '*': + return a * b; + case '/': + return a / b; + } + return 0; +} + +private int stringToNumber(String s) { + int sign = 1; + int start = 0; + if (s.charAt(0) == '-') { + sign = -1; + start = 1; + } + int res = 0; + for (int i = start; i < s.length(); i++) { + res = res * 10 + s.charAt(i) - '0'; + } + return res * sign; +} + +private boolean isNumber(char c) { + return c >= '0' && c <= '9'; +} + +private boolean isOperation(String t) { + return t.equals("+") || t.equals("-") || t.equals("*") || t.equals("/"); +} +``` + +# 解法二 双栈 + +规则如下。 + +1. 使用两个栈,`stack0` 用于存储操作数,`stack1` 用于存储操作符 +2. 从左往右扫描,遇到操作数入栈 `stack0` +3. 遇到操作符时,如果当前优先级低于或等于栈顶操作符优先级,则从 `stack0` 弹出两个元素,从 `stack1` 弹出一个操作符,进行计算,将结果并压入`stack0`,继续与栈顶操作符的比较优先级。 +4. 如果遇到操作符高于栈顶操作符优先级,则直接入栈 `stack1` +5. 遇到左括号,直接入栈 `stack1`。 +6. 遇到右括号,则从 `stack0` 弹出两个元素,从 `stack1` 弹出一个操作符进行计算,并将结果加入到 `stack0` 中,重复这步直到遇到左括号 + +同样的不需要考虑括号,会变得更简单一些。 + +```java +public int calculate(String s) { + char[] array = s.toCharArray(); + int n = array.length; + Stack num = new Stack<>(); + Stack op = new Stack<>(); + int temp = -1; + for (int i = 0; i < n; i++) { + if (array[i] == ' ') { + continue; + } + // 数字进行累加 + if (isNumber(array[i])) { + if (temp == -1) { + temp = array[i] - '0'; + } else { + temp = temp * 10 + array[i] - '0'; + } + } else { + // 将数字入栈 + if (temp != -1) { + num.push(temp); + temp = -1; + } + // 遇到操作符 + while (!op.isEmpty()) { + //遇到更低优先级的话就结束 + if (compare(array[i], op.peek())) { + break; + } + // 不停的出栈,进行运算,并将结果再次压入栈中 + int num1 = num.pop(); + int num2 = num.pop(); + num.push(eval(num1, num2, op.pop())); + } + // 当前运算符入栈 + op.push(array[i]); + + } + } + if (temp != -1) { + num.push(temp); + } + // 将栈中的其他元素继续运算 + while (!op.isEmpty()) { + int num1 = num.pop(); + int num2 = num.pop(); + num.push(eval(num1, num2, op.pop())); + } + return num.pop(); +} + +private boolean compare(char op1, char op2) { + if(op1 == '*' || op1 == '/'){ + return op2 == '+' || op2 == '-'; + } + return false; +} + +private boolean isNumber(char c) { + return c >= '0' && c <= '9'; +} +private int eval(int a, int b, char op) { + switch (op) { + case '+': + return a + b; + case '-': + return b - a; + case '*': + return a * b; + case '/': + return b / a; + } + return 0; +} +``` + +和 [224 题](https://leetcode.wang/leetcode-224-Basic-Calculator.html) 一样需要注意减法和除法,由于使用了栈,所以算的时候两个数字要反一下。 + +# 解法三 + +分享一下 [这里](https://leetcode.com/problems/basic-calculator-ii/discuss/63003/Share-my-java-solution) 的解法,属于专门针对这道题的解法。 + +把减法、乘法、除法在遍历过程中将结果计算出来,最后将所有结果累加。 + +```java +public int calculate(String s) { + int len; + if(s==null || (len = s.length())==0) return 0; + Stack stack = new Stack(); + int num = 0; + char sign = '+'; + for(int i=0;iy` 的形式。 - -# 解法一 - -直接按照题目意思遍历一遍就可以。判断是否连续只需要判断当前数字和后一个数字是否相差 `1` 即可。发生不连续的时候,将当前范围保存起来。 - -```java -public List summaryRanges(int[] nums) { - int n = nums.length; - if (n == 0) { - return new ArrayList<>(); - } - int start = nums[0]; - int end = nums[0]; - List res = new ArrayList<>(); - for (int i = 0; i < n - 1; i++) { - if (nums[i] + 1 != nums[i + 1]) { - //发生了不连续,当前数字是范围的结束 - end = nums[i]; - if (start != end) { - res.add(start + "->" + end); - } else { - res.add(start + ""); - } - //下一个数字作为范围的开头 - start = nums[i + 1]; - } - } - //上边循环只遍历到了 n - 2, 所以最后一个数字单独考虑一下 - end = nums[n - 1]; - if (start != end) { - res.add(start + "->" + end); - } else { - res.add(start + ""); - } - return res; -} -``` - -# 解法二 - -上边解法不管最好还是最坏,时间复杂度都是 `O(n)`,分享 [这里](https://leetcode.com/problems/summary-ranges/discuss/63212/Using-binary-search-but-worst-case-O(n)) 的一个解法,可以对某些情况进行优化。 - -我们可以一半一半的考虑,比如 `1 2 3 4 5 7`。先考虑左半部 `1 2 3` 是否连续,只需要判断下标之差和数字之差是否相等。` 2 - 0 == 3 - 1`,所以左半部分是连续的数字,得到一个范围 `1 -> 3`,而不需要向解法一那样一个一个数字的遍历。 - -这里带来一个问题,判断右半部分的时候,我们知道 `4 -> 5`,但是它应该和左半部连接起来变成 `1 -> 5`。这里的话,我们需要定义一个 `Range` 类,当加入新的范围的时候,判断一下两个范围是否相连即可。 - -```java -class Range { - int start; - int end; - - Range(int s, int e) { - start = s; - end = e; - } -} - -public List summaryRanges(int[] nums) { - - List resStr = new ArrayList<>(); - - if (nums.length == 0) { - return resStr; - } - - List res = new ArrayList<>(); - helper(nums, 0, nums.length - 1, res); - - for (Range r : res) { - if (r.start == r.end) { - resStr.add(Integer.toString(r.start)); - } else { - resStr.add(r.start + "->" + r.end); - } - } - - return resStr; -} - -private void helper(int[] nums, int i, int j, List res) { - if (i == j || nums[j] - nums[i] == j - i) { - add2res(nums[i], nums[j], res); - return; - } - - int m = (i + j) / 2; - //一半一半的考虑 - helper(nums, i, m, res); - helper(nums, m + 1, j, res); -} - -private void add2res(int a, int b, List res) { - //判断新加入的范围和之前最后一个范围是否相连 - if (res.isEmpty() || res.get(res.size() - 1).end + 1 != a) { - res.add(new Range(a, b)); - } else { - res.get(res.size() - 1).end = b; - } -} -``` - -虽然最坏的时间复杂度依旧是 `O(n)`(比如所有的数字全部不相连),但对于某些情况带来了很大的提升。 - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/228.jpg) + +给一个数组,把连续的数字写成 `x->y` 的形式。 + +# 解法一 + +直接按照题目意思遍历一遍就可以。判断是否连续只需要判断当前数字和后一个数字是否相差 `1` 即可。发生不连续的时候,将当前范围保存起来。 + +```java +public List summaryRanges(int[] nums) { + int n = nums.length; + if (n == 0) { + return new ArrayList<>(); + } + int start = nums[0]; + int end = nums[0]; + List res = new ArrayList<>(); + for (int i = 0; i < n - 1; i++) { + if (nums[i] + 1 != nums[i + 1]) { + //发生了不连续,当前数字是范围的结束 + end = nums[i]; + if (start != end) { + res.add(start + "->" + end); + } else { + res.add(start + ""); + } + //下一个数字作为范围的开头 + start = nums[i + 1]; + } + } + //上边循环只遍历到了 n - 2, 所以最后一个数字单独考虑一下 + end = nums[n - 1]; + if (start != end) { + res.add(start + "->" + end); + } else { + res.add(start + ""); + } + return res; +} +``` + +# 解法二 + +上边解法不管最好还是最坏,时间复杂度都是 `O(n)`,分享 [这里](https://leetcode.com/problems/summary-ranges/discuss/63212/Using-binary-search-but-worst-case-O(n)) 的一个解法,可以对某些情况进行优化。 + +我们可以一半一半的考虑,比如 `1 2 3 4 5 7`。先考虑左半部 `1 2 3` 是否连续,只需要判断下标之差和数字之差是否相等。` 2 - 0 == 3 - 1`,所以左半部分是连续的数字,得到一个范围 `1 -> 3`,而不需要向解法一那样一个一个数字的遍历。 + +这里带来一个问题,判断右半部分的时候,我们知道 `4 -> 5`,但是它应该和左半部连接起来变成 `1 -> 5`。这里的话,我们需要定义一个 `Range` 类,当加入新的范围的时候,判断一下两个范围是否相连即可。 + +```java +class Range { + int start; + int end; + + Range(int s, int e) { + start = s; + end = e; + } +} + +public List summaryRanges(int[] nums) { + + List resStr = new ArrayList<>(); + + if (nums.length == 0) { + return resStr; + } + + List res = new ArrayList<>(); + helper(nums, 0, nums.length - 1, res); + + for (Range r : res) { + if (r.start == r.end) { + resStr.add(Integer.toString(r.start)); + } else { + resStr.add(r.start + "->" + r.end); + } + } + + return resStr; +} + +private void helper(int[] nums, int i, int j, List res) { + if (i == j || nums[j] - nums[i] == j - i) { + add2res(nums[i], nums[j], res); + return; + } + + int m = (i + j) / 2; + //一半一半的考虑 + helper(nums, i, m, res); + helper(nums, m + 1, j, res); +} + +private void add2res(int a, int b, List res) { + //判断新加入的范围和之前最后一个范围是否相连 + if (res.isEmpty() || res.get(res.size() - 1).end + 1 != a) { + res.add(new Range(a, b)); + } else { + res.get(res.size() - 1).end = b; + } +} +``` + +虽然最坏的时间复杂度依旧是 `O(n)`(比如所有的数字全部不相连),但对于某些情况带来了很大的提升。 + +# 总 + 解法一就是根据题意写出的一个解法,解法二的话通过二分的方式对解法带来了一定程度上的优化。 \ No newline at end of file diff --git a/leetcode-229-Majority-ElementII.md b/leetcode-229-Majority-ElementII.md index 444881b1c..b964a05e4 100644 --- a/leetcode-229-Majority-ElementII.md +++ b/leetcode-229-Majority-ElementII.md @@ -1,169 +1,169 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/229.jpg) - -找出数组中数量超过 `n/3` 的数字,`n` 是数组的长度。 - -# 解法一 - -题目要求是 `O(1)` 的空间复杂度,我们先用 `map` 写一下,看看对题意的理解对不对。 - -`map` 的话 `key` 存数字,`value` 存数字出现的个数。如果数字出现的次数等于了 `n/3 + 1` 就把它加到结果中。 - -```java -public List majorityElement(int[] nums) { - int n = nums.length; - HashMap map = new HashMap<>(); - List res = new ArrayList<>(); - for (int i = 0; i < n; i++) { - int count = map.getOrDefault(nums[i], 0); - //之前的数量已经是 n/3, 当前数量就是 n/3 + 1 了 - if (count == n / 3) { - res.add(nums[i]); - } - //可以提前结束 - if (count == 2 * n / 3 || res.size() == 2) { - return res; - } - map.put(nums[i], count + 1); - - } - return res; -} -``` - -# 解法二 - -[169 题](https://leetcode.wang/leetcode-169-Majority-Element.html) 我们做过找出数组的中超过 `n/2` 数量的数字,其中介绍了摩尔投票法,这里的话可以改写一下,参考 [这里](https://leetcode.com/problems/majority-element-ii/discuss/63520/Boyer-Moore-Majority-Vote-algorithm-and-my-elaboration)。 - -首先看一下 [169 题](https://leetcode.wang/leetcode-169-Majority-Element.html) 我们是怎么做的。 - -> 我们假设这样一个场景,在一个游戏中,分了若干个队伍,有一个队伍的人数超过了半数。所有人的战力都相同,不同队伍的两个人遇到就是同归于尽,同一个队伍的人遇到当然互不伤害。 -> -> 这样经过充分时间的游戏后,最后的结果是确定的,一定是超过半数的那个队伍留在了最后。 -> -> 而对于这道题,我们只需要利用上边的思想,把数组的每个数都看做队伍编号,然后模拟游戏过程即可。 -> -> `group` 记录当前队伍的人数,`count` 记录当前队伍剩余的人数。如果当前队伍剩余人数为 `0`,记录下次遇到的人的所在队伍号。 - -对于这道题的话,超过 `n/3` 的队伍可能有两个,首先我们用 `group1` 和 `group2` 记录这两个队伍,`count1` 和 `count2` 分别记录两个队伍的数量,然后遵循下边的游戏规则。 - -将数组中的每一个数字看成队伍编号。 - -`group1` 和 `group2` 首先初始化为不可能和当前数字相等的两个数,将这两个队伍看成同盟,它俩不互相伤害。 - -然后遍历数组中的其他数字,如果遇到的数字属于其中的一个队伍,就将当前队伍的数量加 `1`。 - -如果某个队伍的数量变成了 `0`,就把这个队伍编号更新为当前的数字。 - -否则的话,将两个队伍的数量都减 `1`。 - -```java -public List majorityElement(int[] nums) { - int n = nums.length; - long group1 = (long)Integer.MAX_VALUE + 1; - int count1 = 0; - long group2 = (long)Integer.MAX_VALUE + 1; - int count2 = 0; - for (int i = 0; i < n; i++) { - if (nums[i] == group1) { - count1++; - } else if (nums[i] == group2) { - count2++; - } else if (count1 == 0) { - group1 = nums[i]; - count1 = 1; - } else if (count2 == 0) { - group2 = nums[i]; - count2 = 1; - } else { - count1--; - count2--; - } - } - - //计算两个队伍的数量,因为可能只存在一个数字的数量超过了 n/3 - count1 = 0; - count2 = 0; - for (int i = 0; i < n; i++) { - if (nums[i] == group1) { - count1++; - } - if (nums[i] == group2) { - count2++; - } - } - //只保存数量大于 n/3 的队伍 - List res = new ArrayList<>(); - if (count1 > n / 3) { - res.add((int) group1); - } - - if (count2 > n / 3) { - res.add((int) group2); - } - return res; -} -``` - -上边有个技巧就是先将 `group` 初始化为一个大于 `int` 最大值的 `long` 值,这样可以保证后边的 `if` 条件判断中,数组中一定不会有数字和 `group`相等,从而进入后边的更新队伍编号的分支中。除了用 `long` 值,我们还可以用包装对象 `Integer`,将 `group` 初始化为 `null` 可以达到同样的效果。 - -当然,不用上边的技巧也是可以的,我们可以先在 `nums` 里找到两个不同的值分别赋值给 `group1` 和 `group2` 中即可,只不过代码上不会有上边的简洁。 - -`2020.5.27` 更新,[@Frankie](http://yaoyichen.cn/algorithm/2020/05/27/leetcode-229.html) 提醒,其实不用上边分析的那么麻烦,只需要给 `group1` 和 `group2` 随便赋两个不相等的值即可。 - -因为如果数组中前两个数有和 `group1` 或者 `group2` 相等的元素,就进入前两个 `if` 语句中的某一个,逻辑上也没问题。 - -如果数组中前两个数没有和 `group1` 或者 `group2` 相等的元素,那么就和使用 `long` 一个性质了。 - -```java -public List majorityElement(int[] nums) { - int n = nums.length; - int group1 = 0; - int count1 = 0; - int group2 = 1; - int count2 = 0; - for (int i = 0; i < n; i++) { - if (nums[i] == group1) { - count1++; - } else if (nums[i] == group2) { - count2++; - } else if (count1 == 0) { - group1 = nums[i]; - count1 = 1; - } else if (count2 == 0) { - group2 = nums[i]; - count2 = 1; - } else { - count1--; - count2--; - } - } - - //计算两个队伍的数量,因为可能只存在一个数字的数量超过了 n/3 - count1 = 0; - count2 = 0; - for (int i = 0; i < n; i++) { - if (nums[i] == group1) { - count1++; - } - if (nums[i] == group2) { - count2++; - } - } - //只保存数量大于 n/3 的队伍 - List res = new ArrayList<>(); - if (count1 > n / 3) { - res.add( group1); - } - - if (count2 > n / 3) { - res.add(group2); - } - return res; -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/229.jpg) + +找出数组中数量超过 `n/3` 的数字,`n` 是数组的长度。 + +# 解法一 + +题目要求是 `O(1)` 的空间复杂度,我们先用 `map` 写一下,看看对题意的理解对不对。 + +`map` 的话 `key` 存数字,`value` 存数字出现的个数。如果数字出现的次数等于了 `n/3 + 1` 就把它加到结果中。 + +```java +public List majorityElement(int[] nums) { + int n = nums.length; + HashMap map = new HashMap<>(); + List res = new ArrayList<>(); + for (int i = 0; i < n; i++) { + int count = map.getOrDefault(nums[i], 0); + //之前的数量已经是 n/3, 当前数量就是 n/3 + 1 了 + if (count == n / 3) { + res.add(nums[i]); + } + //可以提前结束 + if (count == 2 * n / 3 || res.size() == 2) { + return res; + } + map.put(nums[i], count + 1); + + } + return res; +} +``` + +# 解法二 + +[169 题](https://leetcode.wang/leetcode-169-Majority-Element.html) 我们做过找出数组的中超过 `n/2` 数量的数字,其中介绍了摩尔投票法,这里的话可以改写一下,参考 [这里](https://leetcode.com/problems/majority-element-ii/discuss/63520/Boyer-Moore-Majority-Vote-algorithm-and-my-elaboration)。 + +首先看一下 [169 题](https://leetcode.wang/leetcode-169-Majority-Element.html) 我们是怎么做的。 + +> 我们假设这样一个场景,在一个游戏中,分了若干个队伍,有一个队伍的人数超过了半数。所有人的战力都相同,不同队伍的两个人遇到就是同归于尽,同一个队伍的人遇到当然互不伤害。 +> +> 这样经过充分时间的游戏后,最后的结果是确定的,一定是超过半数的那个队伍留在了最后。 +> +> 而对于这道题,我们只需要利用上边的思想,把数组的每个数都看做队伍编号,然后模拟游戏过程即可。 +> +> `group` 记录当前队伍的人数,`count` 记录当前队伍剩余的人数。如果当前队伍剩余人数为 `0`,记录下次遇到的人的所在队伍号。 + +对于这道题的话,超过 `n/3` 的队伍可能有两个,首先我们用 `group1` 和 `group2` 记录这两个队伍,`count1` 和 `count2` 分别记录两个队伍的数量,然后遵循下边的游戏规则。 + +将数组中的每一个数字看成队伍编号。 + +`group1` 和 `group2` 首先初始化为不可能和当前数字相等的两个数,将这两个队伍看成同盟,它俩不互相伤害。 + +然后遍历数组中的其他数字,如果遇到的数字属于其中的一个队伍,就将当前队伍的数量加 `1`。 + +如果某个队伍的数量变成了 `0`,就把这个队伍编号更新为当前的数字。 + +否则的话,将两个队伍的数量都减 `1`。 + +```java +public List majorityElement(int[] nums) { + int n = nums.length; + long group1 = (long)Integer.MAX_VALUE + 1; + int count1 = 0; + long group2 = (long)Integer.MAX_VALUE + 1; + int count2 = 0; + for (int i = 0; i < n; i++) { + if (nums[i] == group1) { + count1++; + } else if (nums[i] == group2) { + count2++; + } else if (count1 == 0) { + group1 = nums[i]; + count1 = 1; + } else if (count2 == 0) { + group2 = nums[i]; + count2 = 1; + } else { + count1--; + count2--; + } + } + + //计算两个队伍的数量,因为可能只存在一个数字的数量超过了 n/3 + count1 = 0; + count2 = 0; + for (int i = 0; i < n; i++) { + if (nums[i] == group1) { + count1++; + } + if (nums[i] == group2) { + count2++; + } + } + //只保存数量大于 n/3 的队伍 + List res = new ArrayList<>(); + if (count1 > n / 3) { + res.add((int) group1); + } + + if (count2 > n / 3) { + res.add((int) group2); + } + return res; +} +``` + +上边有个技巧就是先将 `group` 初始化为一个大于 `int` 最大值的 `long` 值,这样可以保证后边的 `if` 条件判断中,数组中一定不会有数字和 `group`相等,从而进入后边的更新队伍编号的分支中。除了用 `long` 值,我们还可以用包装对象 `Integer`,将 `group` 初始化为 `null` 可以达到同样的效果。 + +当然,不用上边的技巧也是可以的,我们可以先在 `nums` 里找到两个不同的值分别赋值给 `group1` 和 `group2` 中即可,只不过代码上不会有上边的简洁。 + +`2020.5.27` 更新,[@Frankie](http://yaoyichen.cn/algorithm/2020/05/27/leetcode-229.html) 提醒,其实不用上边分析的那么麻烦,只需要给 `group1` 和 `group2` 随便赋两个不相等的值即可。 + +因为如果数组中前两个数有和 `group1` 或者 `group2` 相等的元素,就进入前两个 `if` 语句中的某一个,逻辑上也没问题。 + +如果数组中前两个数没有和 `group1` 或者 `group2` 相等的元素,那么就和使用 `long` 一个性质了。 + +```java +public List majorityElement(int[] nums) { + int n = nums.length; + int group1 = 0; + int count1 = 0; + int group2 = 1; + int count2 = 0; + for (int i = 0; i < n; i++) { + if (nums[i] == group1) { + count1++; + } else if (nums[i] == group2) { + count2++; + } else if (count1 == 0) { + group1 = nums[i]; + count1 = 1; + } else if (count2 == 0) { + group2 = nums[i]; + count2 = 1; + } else { + count1--; + count2--; + } + } + + //计算两个队伍的数量,因为可能只存在一个数字的数量超过了 n/3 + count1 = 0; + count2 = 0; + for (int i = 0; i < n; i++) { + if (nums[i] == group1) { + count1++; + } + if (nums[i] == group2) { + count2++; + } + } + //只保存数量大于 n/3 的队伍 + List res = new ArrayList<>(); + if (count1 > n / 3) { + res.add( group1); + } + + if (count2 > n / 3) { + res.add(group2); + } + return res; +} +``` + +# 总 + 解法一算是通用的解法,解法二的话看起来比较容易,但如果只看上边的解析,然后自己写代码的话还是会遇到很多问题的,其中 `if` 分支的顺序很重要。 \ No newline at end of file diff --git a/leetcode-230-Kth-Smallest-Element-in-a-BST.md b/leetcode-230-Kth-Smallest-Element-in-a-BST.md index f99a684ba..b92a128d9 100644 --- a/leetcode-230-Kth-Smallest-Element-in-a-BST.md +++ b/leetcode-230-Kth-Smallest-Element-in-a-BST.md @@ -1,158 +1,158 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/230.png) - -给一个二叉搜索树,找到树中第 `k` 小的树。二叉搜索树的定义如下: - -1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值; -2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值; -3. 任意节点的左、右子树也分别为二叉查找树; -4. 没有键值相等的节点。 - -# 思路分析 - -通过前边 [98 题](https://leetcode.wang/leetCode-98-Validate-Binary-Search-Tree.html) 、[99 题](https://leetcode.wang/leetcode-99-Recover-Binary-Search-Tree.html) 以及 [108 题](https://leetcode.wang/leetcode-108-Convert-Sorted-Array-to-Binary-Search-Tree.html) 的洗礼,看到二叉搜索树,应该会立刻想到它的一个性质,它的中序遍历输出的是一个升序数组。知道了这个,这道题就很简单了,只需要把中序遍历的第 `k` 个元素返回即可。 - -# 解法一 中序遍历 - -说到中序遍历,[94 题](https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html) 已经讨论过了,总共介绍了三种解法,大家可以过去看一下,这里的话,直接在之前的基础上做修改了。 - -总体上,我们只需要增加两个变量 `num` 和 `res`。`num` 记录中序遍历已经输出的元素个数,当 `num == k` 的时候,我们只需要将当前元素保存到 `res` 中,然后返回即可。 - -下边分享下三种遍历方式的解法,供参考。 - -递归法。 - -```java -int num = 0; -int res; - -public int kthSmallest(TreeNode root, int k) { - inorderTraversal(root, k); - return res; -} - -private void inorderTraversal(TreeNode node, int k) { - if (node == null) { - return; - } - inorderTraversal(node.left, k); - num++; - if (num == k) { - res = node.val; - return; - } - inorderTraversal(node.right, k); -} -``` - -递归改写,压栈法。 - -```java -public int kthSmallest(TreeNode root, int k) { - Stack stack = new Stack<>(); - int num = 0; - int res = -1; - TreeNode cur = root; - while (cur != null || !stack.isEmpty()) { - // 节点不为空一直压栈 - while (cur != null) { - stack.push(cur); - cur = cur.left; // 考虑左子树 - } - // 节点为空,就出栈 - cur = stack.pop(); - // 当前值加入 - num++; - if (num == k) { - res = cur.val; - break; - } - // 考虑右子树 - cur = cur.right; - } - return res; -} -``` - -常数空间复杂度的 Morris 遍历,[94 题](https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html) 对 Morris 遍历有详细的解释。 - -```java -public int kthSmallest(TreeNode root, int k) { - TreeNode cur = root; - int num = 0; - int res = -1; - while (cur != null) { - // 情况 1 - if (cur.left == null) { - num++; - if (num == k) { - res = cur.val; - break; - } - cur = cur.right; - } else { - // 找左子树最右边的节点 - TreeNode pre = cur.left; - while (pre.right != null && pre.right != cur) { - pre = pre.right; - } - // 情况 2.1 - if (pre.right == null) { - pre.right = cur; - cur = cur.left; - } - // 情况 2.2 - if (pre.right == cur) { - pre.right = null; // 这里可以恢复为 null - num++; - if (num == k) { - res = cur.val; - break; - } - cur = cur.right; - } - } - - } - return res; -} -``` - -可以看到,三种解法都是一样的,我们只是在中序遍历输出的时候,记录了已经输出的个数而已。 - -# 解法二 分治法 - -如果不知道解法一中二叉搜索树的性质,用分治法也可以做,分享 [这里](https://leetcode.com/problems/kth-smallest-element-in-a-bst/discuss/63743/Java-divide-and-conquer-solution-considering-augmenting-tree-structure-for-the-follow-up) 的解法。 - -我们只需要先计算左子树的节点个数,记为 `n`,然后有三种情况。 - -`n` 加 `1` 等于 `k`,那就说明当前根节点就是我们要找的。 - -`n `加 `1` 小于 `k`,那就说明第 `k` 小的数一定在右子树中,我们只需要递归的在右子树中寻找第 `k - n - 1` 小的数即可。 - -`n` 加 `1` 大于 `k`,那就说明第 `k` 小个数一定在左子树中,我们只需要递归的在左子树中寻找第 `k` 小的数即可。 - -```java -public int kthSmallest(TreeNode root, int k) { - int n = nodeCount(root.left); - if(n + 1 == k) { - return root.val; - } else if (n + 1 < k) { - return kthSmallest(root.right, k - n - 1); - } else { - return kthSmallest(root.left, k); - } -} - -private int nodeCount(TreeNode root) { - if(root == null) { - return 0; - } - return 1 + nodeCount(root.left) + nodeCount(root.right); -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/230.png) + +给一个二叉搜索树,找到树中第 `k` 小的树。二叉搜索树的定义如下: + +1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值; +2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值; +3. 任意节点的左、右子树也分别为二叉查找树; +4. 没有键值相等的节点。 + +# 思路分析 + +通过前边 [98 题](https://leetcode.wang/leetCode-98-Validate-Binary-Search-Tree.html) 、[99 题](https://leetcode.wang/leetcode-99-Recover-Binary-Search-Tree.html) 以及 [108 题](https://leetcode.wang/leetcode-108-Convert-Sorted-Array-to-Binary-Search-Tree.html) 的洗礼,看到二叉搜索树,应该会立刻想到它的一个性质,它的中序遍历输出的是一个升序数组。知道了这个,这道题就很简单了,只需要把中序遍历的第 `k` 个元素返回即可。 + +# 解法一 中序遍历 + +说到中序遍历,[94 题](https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html) 已经讨论过了,总共介绍了三种解法,大家可以过去看一下,这里的话,直接在之前的基础上做修改了。 + +总体上,我们只需要增加两个变量 `num` 和 `res`。`num` 记录中序遍历已经输出的元素个数,当 `num == k` 的时候,我们只需要将当前元素保存到 `res` 中,然后返回即可。 + +下边分享下三种遍历方式的解法,供参考。 + +递归法。 + +```java +int num = 0; +int res; + +public int kthSmallest(TreeNode root, int k) { + inorderTraversal(root, k); + return res; +} + +private void inorderTraversal(TreeNode node, int k) { + if (node == null) { + return; + } + inorderTraversal(node.left, k); + num++; + if (num == k) { + res = node.val; + return; + } + inorderTraversal(node.right, k); +} +``` + +递归改写,压栈法。 + +```java +public int kthSmallest(TreeNode root, int k) { + Stack stack = new Stack<>(); + int num = 0; + int res = -1; + TreeNode cur = root; + while (cur != null || !stack.isEmpty()) { + // 节点不为空一直压栈 + while (cur != null) { + stack.push(cur); + cur = cur.left; // 考虑左子树 + } + // 节点为空,就出栈 + cur = stack.pop(); + // 当前值加入 + num++; + if (num == k) { + res = cur.val; + break; + } + // 考虑右子树 + cur = cur.right; + } + return res; +} +``` + +常数空间复杂度的 Morris 遍历,[94 题](https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html) 对 Morris 遍历有详细的解释。 + +```java +public int kthSmallest(TreeNode root, int k) { + TreeNode cur = root; + int num = 0; + int res = -1; + while (cur != null) { + // 情况 1 + if (cur.left == null) { + num++; + if (num == k) { + res = cur.val; + break; + } + cur = cur.right; + } else { + // 找左子树最右边的节点 + TreeNode pre = cur.left; + while (pre.right != null && pre.right != cur) { + pre = pre.right; + } + // 情况 2.1 + if (pre.right == null) { + pre.right = cur; + cur = cur.left; + } + // 情况 2.2 + if (pre.right == cur) { + pre.right = null; // 这里可以恢复为 null + num++; + if (num == k) { + res = cur.val; + break; + } + cur = cur.right; + } + } + + } + return res; +} +``` + +可以看到,三种解法都是一样的,我们只是在中序遍历输出的时候,记录了已经输出的个数而已。 + +# 解法二 分治法 + +如果不知道解法一中二叉搜索树的性质,用分治法也可以做,分享 [这里](https://leetcode.com/problems/kth-smallest-element-in-a-bst/discuss/63743/Java-divide-and-conquer-solution-considering-augmenting-tree-structure-for-the-follow-up) 的解法。 + +我们只需要先计算左子树的节点个数,记为 `n`,然后有三种情况。 + +`n` 加 `1` 等于 `k`,那就说明当前根节点就是我们要找的。 + +`n `加 `1` 小于 `k`,那就说明第 `k` 小的数一定在右子树中,我们只需要递归的在右子树中寻找第 `k - n - 1` 小的数即可。 + +`n` 加 `1` 大于 `k`,那就说明第 `k` 小个数一定在左子树中,我们只需要递归的在左子树中寻找第 `k` 小的数即可。 + +```java +public int kthSmallest(TreeNode root, int k) { + int n = nodeCount(root.left); + if(n + 1 == k) { + return root.val; + } else if (n + 1 < k) { + return kthSmallest(root.right, k - n - 1); + } else { + return kthSmallest(root.left, k); + } +} + +private int nodeCount(TreeNode root) { + if(root == null) { + return 0; + } + return 1 + nodeCount(root.left) + nodeCount(root.right); +} +``` + +# 总 + 解法一的前提就是需要知道二分查找树的中序遍历是升序数组,问题就转换成中序遍历求解了。解法二的话,属于通用的解法,分治法,思路很棒。 \ No newline at end of file diff --git a/leetcode-231-Power-of-Two.md b/leetcode-231-Power-of-Two.md index a947124e3..19ad4ea64 100644 --- a/leetcode-231-Power-of-Two.md +++ b/leetcode-231-Power-of-Two.md @@ -1,365 +1,365 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/231.jpg) - -判断一个数是不是 `2` 的幂次。 - -# 思路分析 - -题目比较简单,有很多解法,看了其他人的解法,各种秀操作,哈哈。解法一和解法二是我开始想到的,后边的解法是其他人的也总结到这里。 - -# 解法一 - -介绍一种暴力的方法,判断 `1` 和当前数是否相等,再判断 `2` 和当前数是否相等,再判断 `4` 和当前数是否相等...直到所枚举的数超过了当前数,那么就返回 `false`。至于枚举的数 `1 2 4 8 16...`,可以通过移位得到。 - -```java -public boolean isPowerOfTwo(int n) { - int power = 1; - while (power <= n) { - if (power == n) { - return true; - } - power = power << 1; - // if (power == Integer.MIN_VALUE) - if (power == -2147483648) { - break; - } - } - return false; -} -``` - -当然有一点需要注意,需要了解一些补码的知识,参考 [趣谈补码](https://zhuanlan.zhihu.com/p/67227136)。 - -对于 `int` 类型,最大的 `2` 的幂次是 2 的 30 次方,即`1073741824`,二进制形式是 `0100000...00`。 - -最大的负数是 `-2147483648`,二进制形式是 `1000000...00`。 - -可以发现前者左移一位刚好变成了后者,这也是代码中判断是否是 `-2147483648` 的原因,不然的话会造成死循环的。 - -# 解法二 - -我们把数字放眼到二进制形式,列举一下 `2` 的幂次。 - -```java -1 1 -2 10 -4 100 -8 1000 -16 10000 -... -``` - -可以发现发现都是 `100...` 的形式,如果做过 [201 题](https://leetcode.wang/leetcode-201-Bitwise-AND-of-Numbers-Range.html),对 `Integer.highestOneBit` 方法可能还记得。可以实现保留最高位的 `1` ,然后将其它位全部置为 `0`。即,把 `0 0 0 1 X X X X` 变成 `0 0 0 1 0 0 0 0` 。 - -如果我们对给定的数 `n` 调用这个方法,如果 `n` 是 `2` 的幂次,那么它还是它本身。如果是其他数,由于其它位被置 `0` 了,所以它一定不等于它本身了。 - -```java -public boolean isPowerOfTwo(int n) { - if (n == 0 || n == -2147483648) { - return false; - } - return Integer.highestOneBit(n) == n; -} -``` - - `0` 和 `-2147483648` 不符合上边的规则,单独考虑,我们可以更干脆一些,小于等于 `0` 的数直接不考虑。 - -```java -public boolean isPowerOfTwo(int n) { - if (n <= 0) { - return false; - } - return Integer.highestOneBit(n) == n; -} -``` - -[201 题](https://leetcode.wang/leetcode-201-Bitwise-AND-of-Numbers-Range.html#解法三) 的解法三对 `highestOneBit` 的源码进行了分析,下边把之前的解析贴过来。 - -我们调用了库函数 `Integer.highestOneBit`,我们去看一下它的实现。 - -```java -/** - * Returns an {@code int} value with at most a single one-bit, in the - * position of the highest-order ("leftmost") one-bit in the specified - * {@code int} value. Returns zero if the specified value has no - * one-bits in its two's complement binary representation, that is, if it - * is equal to zero. - * - * @param i the value whose highest one bit is to be computed - * @return an {@code int} value with a single one-bit, in the position - * of the highest-order one-bit in the specified value, or zero if - * the specified value is itself equal to zero. - * @since 1.5 - */ -public static int highestOneBit(int i) { - // HD, Figure 3-1 - i |= (i >> 1); - i |= (i >> 2); - i |= (i >> 4); - i |= (i >> 8); - i |= (i >> 16); - return i - (i >>> 1); -} -``` - -它做了什么事情呢?我们从 `return` 入手。 - -对于 `0 0 0 1 X X X X` ,最终会变成 `0 0 0 1 1 1 1 1`,记做 `i` 。把 `i` 再右移一位变成 `0 0 0 0 1 1 1 1`,然后两数做差。 - -```java -i 0 0 0 1 1 1 1 1 -i >>> 1 0 0 0 0 1 1 1 1 - 0 0 0 1 0 0 0 0 -``` - -就得到了这个函数最后返回的结果了。 - -将 `0 0 0 1 X X X X` 变成 `0 0 0 1 1 1 1 1`,可以通过复制实现。 - -第一步,将首位的 `1` 赋值给它的旁边。 - -```java -i |= (i >> 1); -0 0 0 1 X X X X -> 0 0 0 1 1 X X X - -现在首位有两个 1 了,所以就将这两个 1 看做一个整体,继续把 1 赋值给它的旁边。 -i |= (i >> 2); -0 0 0 1 1 X X X -> 0 0 0 1 1 1 1 X - -现在首位有 4 个 1 了,所以就将这 4 个 1 看做一个整体,继续把 1 赋值给它的旁边。 -i |= (i >> 4); -0 0 0 1 1 1 1 X -> 0 0 0 1 1 1 1 1 - -其实到这里已经结束了,但函数中是考虑最坏的情况,类似于这种 1000000...00, 首位是 1, 有 31 个 0 -``` - -我们可以把上边的源码直接放过来。 - -```java -public boolean isPowerOfTwo(int n) { - if (n <= 0) { - return false; - } - int i = n; - i |= (i >> 1); - i |= (i >> 2); - i |= (i >> 4); - i |= (i >> 8); - i |= (i >> 16); - i = i - (i >>> 1); - return i == n; -} -``` - -上边的解法是我开始想到的,下边的解法全部来自 [这里](https://leetcode.com/problems/power-of-two/discuss/63966/4-different-ways-to-solve-Iterative-Recursive-Bit-operation-Math),分享一下。 - -# 解法三 - -一直进行除以 `2`,直到不是 `2` 的倍数。 - -```java -public boolean isPowerOfTwo(int n) { - if (n == 0) return false; - while (n % 2 == 0) { - n /= 2; - } - return n == 1; -} -``` - -也可以改写成递归的形式。 - -```java -public boolean isPowerOfTwo(int n) { - return n > 0 && (n == 1 || (n%2 == 0 && isPowerOfTwo(n/2))); -} -``` - -# 解法四 - -做过 [191 题](https://leetcode.wang/leetcode-191-Number-of-1-Bits.html) 的话,对一个 `trick` 应该有印象。 - -有一个方法,可以把最右边的 `1` 置为 `0`,举个具体的例子。 - -比如十进制的 `10`,二进制形式是 `1010`,然后我们只需要把它和 `9` 进行按位与操作,也就是 `10 & 9 = (1010) & (1001) = 1000`,也就是把 `1010` 最右边的 `1` 置为 `0`。 - -规律就是对于任意一个数 `n`,然后 `n & (n-1)` 的结果就是把 `n` 的最右边的 `1` 置为 `0` 。 - -也比较好理解,当我们对一个数减 `1` 的话,比如原来的数是 `...1010000`,然后减一就会向前借位,直到遇到最右边的第一个 `1`,变成 `...1001111`,然后我们把它和原数按位与,就会把从原数最右边 `1` 开始的位置全部置零了 `...10000000`。 - -有了这个知识,我们看一下解法二列举的 `2` 的幂次,只有一个 `1`,如果通过 `n&(n-1)`,那么就会变成 `0` 了。 - -同样的 `0` 和 `-2147483648` 不符合上边的规则,需要单独考虑。又因为所有负数一定不是 `2` 的幂次,所以代码可以写成下边的样子。 - -```java -public boolean isPowerOfTwo(int n) { - if (n <= 0) { - return false; - } - return (n & (n - 1)) == 0; -} -``` - -# 解法五 - -`java` 中还有一个方法 `Integer.bitCount(n)`,返回 `n` 的二进制形式的 `1` 的个数。 - -```java -public boolean isPowerOfTwo(int n) { - if (n <= 0) { - return false; - } - return Integer.bitCount(n) == 1; -} -``` - -同样的,我们学习一下 `bitCount` 的源码。 - -```java -/** - * Returns the number of one-bits in the two's complement binary - * representation of the specified {@code int} value. This function is - * sometimes referred to as the population count. - * - * @param i the value whose bits are to be counted - * @return the number of one-bits in the two's complement binary - * representation of the specified {@code int} value. - * @since 1.5 - */ -public static int bitCount(int i) { - // HD, Figure 5-2 - i = i - ((i >>> 1) & 0x55555555); - i = (i & 0x33333333) + ((i >>> 2) & 0x33333333); - i = (i + (i >>> 4)) & 0x0f0f0f0f; - i = i + (i >>> 8); - i = i + (i >>> 16); - return i & 0x3f; -} -``` - -[191 题](https://leetcode.wang/leetcode-191-Number-of-1-Bits.html) 题目就是求二进制 `1` 的个数,其中解法三介绍的其实就是上边的解法,我把解释贴过来。 - -有点类似于 [190 题](https://leetcode.wang/leetcode-190-Reverse-Bits.html) 的解法二,通过整体的位操作解决问题,参考 [这里](https://leetcode.com/problems/number-of-1-bits/discuss/55120/Short-code-of-C%2B%2B-O(m)-by-time-m-is-the-count-of-1's-and-another-several-method-of-O(1)-time) ,也是比较 `trick` 的,不容易想到,但还是很有意思的。 - -本质思想就是用本身的比特位去记录对应位数的比特位 `1` 的个数,举个具体的例子吧。为了简洁,求一下 `8` 比特的数字中 `1` 的个数。 - -```java -统计数代表对应括号内 1 的个数 -1 1 0 1 0 0 1 1 -首先把它看做 8 组,统计每组 1 的个数 -原数字:(1) (1) (0) (1) (0) (0) (1) (1) -统计数:(1) (1) (0) (1) (0) (0) (1) (1) -每个数字本身,就天然的代表了当前组 1 的个数。 - -接下来看做 4 组,相邻两组进行合并,统计数其实就是上边相邻组统计数相加即可。 -原数字:(1 1) (0 1) (0 0) (1 1) -统计数:(1 0) (0 1) (0 0) (1 0) -十进制: 2 1 0 2 - -接下来看做 2 组,相邻两组进行合并,统计数变成上边相邻组统计数的和。 -原数字:(1 1 0 1) (0 0 1 1) -统计数:(0 0 1 1) (0 0 1 0) -十进制: 3 2 - -接下来看做 1 组,相邻两组进行合并,统计数变成上边相邻组统计数的和。 -原数字:(1 1 0 1 0 0 1 1) -统计数:(0 0 0 0 0 1 0 1) -十进制: 5 -``` - -看一下 「统计数」的变化,也就是统计的 `1` 的个数。 - -看下二进制形式的变化,两两相加。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/191_2.jpg) - -看下十进制形式的变化,两两相加。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/191_3.jpg) - -最后我们就的得到了 `1` 的个数是 `5`。 - -所以问题的关键就是怎么实现每次合并相邻统计数,我们可以通过位操作实现,举个例子。 - -比如上边 `4` 组到 `2` 组中的前两组合成一组的变化。要把 `(1 0) (0 1)` 两组相加,变成 `(0 0 1 1)` 。其实我们只需要把 `1001` 和 `0011` 相与得到低两位,然后把 `1001` 右移两位再和 `0011` 相与得到高两位,最后将两数相加即可。也就是`(1001) & (0011) + (1001) >>> 2 & (0011)= 0011`。 - -扩展到任意情况,两组合并成一组,如果合并前每组的个数是 `n`,合并前的数字是 `x`,那么合并后的数字就是 `x & (000...111...) + x >>> n & (000...111...) `,其中 `0` 和 `1` 的个数是 `n`。 - -```java -public int hammingWeight(int n) { - n = (n & 0x55555555) + ((n >>> 1) & 0x55555555); // 32 组向 16 组合并,合并前每组 1 个数 - n = (n & 0x33333333) + ((n >>> 2) & 0x33333333); // 16 组向 8 组合并,合并前每组 2 个数 - n = (n & 0x0f0f0f0f) + ((n >>> 4) & 0x0f0f0f0f); // 8 组向 4 组合并,合并前每组 4 个数 - n = (n & 0x00ff00ff)+ ((n >>> 8) & 0x00ff00ff); // 4 组向 2 组合并,合并前每组 8 个数 - n = (n & 0x0000ffff) + ((n >>> 16) & 0x0000ffff); // 2 组向 1 组合并,合并前每组 16 个数 - return n; -} -``` - -写成 `16` 进制可能不好理解,我们拿16 组向 8 组合并举例,合并前每组 2 个数。也就是上边我们推导的,我们要把 `(1 0) (0 1)` 两组合并,需要和 `0011` 按位与,写成 `16` 进制就是 `3`,因为合并完是 `8` 组,所以就是 `8` 个 `3`,即 `0x33333333`。 - -再回到 `java` 的源码。 - -```java -public static int bitCount(int i) { - // HD, Figure 5-2 - i = i - ((i >>> 1) & 0x55555555); - i = (i & 0x33333333) + ((i >>> 2) & 0x33333333); - i = (i + (i >>> 4)) & 0x0f0f0f0f; - i = i + (i >>> 8); - i = i + (i >>> 16); - return i & 0x3f; -} -``` - -第一步写法不一样,也很好理解。结合下边的图看, - -![](https://windliang.oss-cn-beijing.aliyuncs.com/191_2.jpg) - -第一步要做的就是把 `11 -> 10`,`01 - > 01`,`00 -> 00`,`10 -> 01`。 - -其实就是原来的数减去高位的数,`11 - 1 = 10`, `01 - 0 = 01`,`00 - 0 = 00`,`10 - 1 = 01`。 - -第二步一致。 - -第三步,没有与完相加,而是先相加再相与,之所以可以这么做,是因为四位的话最多就是 `4` 个 `1`,也就是 `0100` 和 `0100` 合并,结果最多也就是 `4` 位,所以只需要最后和 `0f` 相与,将高四位置零即可。 - -第四步和第五步没有相与,是因为最多 `32` 个 `1`,用二进制表示就是 `100000`,所以只需要低 `6` 位的结果,高 `8` 位是什么已经不重要了,也就不需要通过相与置零了,最后只需要和 `0x3f(111111)` 相与取得我们的结果即可。 - -# 解法六 - -前边提到对于 `int` 类型,最大的 `2` 的幂次是 2 的 30 次方,即`1073741824`。对于 `n` 分两种情况讨论。 - -* 如果 `n` 是 `2` 的幂次,那么 $$n=2^k$$ - - $$2^{30}=2^k*2^{30-k}$$,所以 $$2^{30} \% 2^k==0$$。 - -* 如果 `n` 不是 `2` 的幂次,那么 $$n = j * 2^k$$,其中 `j` 是一个奇数,因为 `n` 一直进行除以 `2` 可以得到 `k`,直到不能被 `2` 整除,此时一定是奇数,也就是公式中的 `j`。 - - $$2^{30} \% (j * 2^k) = 2^{30-k} \% j!=0$$。 - -综上所述,通过是否能被 `2` 的 `30` 次方,即`1073741824` 整除,即可解决我们的问题。 - - ```java -public boolean isPowerOfTwo(int n) { - if (n <= 0) { - return false; - } - return 1073741824 % n == 0; -} - ``` - -# 解法七 - -超级暴力打表法,因为 `int` 范围内 `2` 的幂次也只有 `31` 个数。 - -```java -public boolean isPowerOfTwo(int n) { - return new HashSet<>(Arrays.asList(1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576, 2097152, 4194304, 8388608,16777216, 33554432, 67108864, 134217728, 268435456, 536870912, 1073741824)).contains(n); -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/231.jpg) + +判断一个数是不是 `2` 的幂次。 + +# 思路分析 + +题目比较简单,有很多解法,看了其他人的解法,各种秀操作,哈哈。解法一和解法二是我开始想到的,后边的解法是其他人的也总结到这里。 + +# 解法一 + +介绍一种暴力的方法,判断 `1` 和当前数是否相等,再判断 `2` 和当前数是否相等,再判断 `4` 和当前数是否相等...直到所枚举的数超过了当前数,那么就返回 `false`。至于枚举的数 `1 2 4 8 16...`,可以通过移位得到。 + +```java +public boolean isPowerOfTwo(int n) { + int power = 1; + while (power <= n) { + if (power == n) { + return true; + } + power = power << 1; + // if (power == Integer.MIN_VALUE) + if (power == -2147483648) { + break; + } + } + return false; +} +``` + +当然有一点需要注意,需要了解一些补码的知识,参考 [趣谈补码](https://zhuanlan.zhihu.com/p/67227136)。 + +对于 `int` 类型,最大的 `2` 的幂次是 2 的 30 次方,即`1073741824`,二进制形式是 `0100000...00`。 + +最大的负数是 `-2147483648`,二进制形式是 `1000000...00`。 + +可以发现前者左移一位刚好变成了后者,这也是代码中判断是否是 `-2147483648` 的原因,不然的话会造成死循环的。 + +# 解法二 + +我们把数字放眼到二进制形式,列举一下 `2` 的幂次。 + +```java +1 1 +2 10 +4 100 +8 1000 +16 10000 +... +``` + +可以发现发现都是 `100...` 的形式,如果做过 [201 题](https://leetcode.wang/leetcode-201-Bitwise-AND-of-Numbers-Range.html),对 `Integer.highestOneBit` 方法可能还记得。可以实现保留最高位的 `1` ,然后将其它位全部置为 `0`。即,把 `0 0 0 1 X X X X` 变成 `0 0 0 1 0 0 0 0` 。 + +如果我们对给定的数 `n` 调用这个方法,如果 `n` 是 `2` 的幂次,那么它还是它本身。如果是其他数,由于其它位被置 `0` 了,所以它一定不等于它本身了。 + +```java +public boolean isPowerOfTwo(int n) { + if (n == 0 || n == -2147483648) { + return false; + } + return Integer.highestOneBit(n) == n; +} +``` + + `0` 和 `-2147483648` 不符合上边的规则,单独考虑,我们可以更干脆一些,小于等于 `0` 的数直接不考虑。 + +```java +public boolean isPowerOfTwo(int n) { + if (n <= 0) { + return false; + } + return Integer.highestOneBit(n) == n; +} +``` + +[201 题](https://leetcode.wang/leetcode-201-Bitwise-AND-of-Numbers-Range.html#解法三) 的解法三对 `highestOneBit` 的源码进行了分析,下边把之前的解析贴过来。 + +我们调用了库函数 `Integer.highestOneBit`,我们去看一下它的实现。 + +```java +/** + * Returns an {@code int} value with at most a single one-bit, in the + * position of the highest-order ("leftmost") one-bit in the specified + * {@code int} value. Returns zero if the specified value has no + * one-bits in its two's complement binary representation, that is, if it + * is equal to zero. + * + * @param i the value whose highest one bit is to be computed + * @return an {@code int} value with a single one-bit, in the position + * of the highest-order one-bit in the specified value, or zero if + * the specified value is itself equal to zero. + * @since 1.5 + */ +public static int highestOneBit(int i) { + // HD, Figure 3-1 + i |= (i >> 1); + i |= (i >> 2); + i |= (i >> 4); + i |= (i >> 8); + i |= (i >> 16); + return i - (i >>> 1); +} +``` + +它做了什么事情呢?我们从 `return` 入手。 + +对于 `0 0 0 1 X X X X` ,最终会变成 `0 0 0 1 1 1 1 1`,记做 `i` 。把 `i` 再右移一位变成 `0 0 0 0 1 1 1 1`,然后两数做差。 + +```java +i 0 0 0 1 1 1 1 1 +i >>> 1 0 0 0 0 1 1 1 1 + 0 0 0 1 0 0 0 0 +``` + +就得到了这个函数最后返回的结果了。 + +将 `0 0 0 1 X X X X` 变成 `0 0 0 1 1 1 1 1`,可以通过复制实现。 + +第一步,将首位的 `1` 赋值给它的旁边。 + +```java +i |= (i >> 1); +0 0 0 1 X X X X -> 0 0 0 1 1 X X X + +现在首位有两个 1 了,所以就将这两个 1 看做一个整体,继续把 1 赋值给它的旁边。 +i |= (i >> 2); +0 0 0 1 1 X X X -> 0 0 0 1 1 1 1 X + +现在首位有 4 个 1 了,所以就将这 4 个 1 看做一个整体,继续把 1 赋值给它的旁边。 +i |= (i >> 4); +0 0 0 1 1 1 1 X -> 0 0 0 1 1 1 1 1 + +其实到这里已经结束了,但函数中是考虑最坏的情况,类似于这种 1000000...00, 首位是 1, 有 31 个 0 +``` + +我们可以把上边的源码直接放过来。 + +```java +public boolean isPowerOfTwo(int n) { + if (n <= 0) { + return false; + } + int i = n; + i |= (i >> 1); + i |= (i >> 2); + i |= (i >> 4); + i |= (i >> 8); + i |= (i >> 16); + i = i - (i >>> 1); + return i == n; +} +``` + +上边的解法是我开始想到的,下边的解法全部来自 [这里](https://leetcode.com/problems/power-of-two/discuss/63966/4-different-ways-to-solve-Iterative-Recursive-Bit-operation-Math),分享一下。 + +# 解法三 + +一直进行除以 `2`,直到不是 `2` 的倍数。 + +```java +public boolean isPowerOfTwo(int n) { + if (n == 0) return false; + while (n % 2 == 0) { + n /= 2; + } + return n == 1; +} +``` + +也可以改写成递归的形式。 + +```java +public boolean isPowerOfTwo(int n) { + return n > 0 && (n == 1 || (n%2 == 0 && isPowerOfTwo(n/2))); +} +``` + +# 解法四 + +做过 [191 题](https://leetcode.wang/leetcode-191-Number-of-1-Bits.html) 的话,对一个 `trick` 应该有印象。 + +有一个方法,可以把最右边的 `1` 置为 `0`,举个具体的例子。 + +比如十进制的 `10`,二进制形式是 `1010`,然后我们只需要把它和 `9` 进行按位与操作,也就是 `10 & 9 = (1010) & (1001) = 1000`,也就是把 `1010` 最右边的 `1` 置为 `0`。 + +规律就是对于任意一个数 `n`,然后 `n & (n-1)` 的结果就是把 `n` 的最右边的 `1` 置为 `0` 。 + +也比较好理解,当我们对一个数减 `1` 的话,比如原来的数是 `...1010000`,然后减一就会向前借位,直到遇到最右边的第一个 `1`,变成 `...1001111`,然后我们把它和原数按位与,就会把从原数最右边 `1` 开始的位置全部置零了 `...10000000`。 + +有了这个知识,我们看一下解法二列举的 `2` 的幂次,只有一个 `1`,如果通过 `n&(n-1)`,那么就会变成 `0` 了。 + +同样的 `0` 和 `-2147483648` 不符合上边的规则,需要单独考虑。又因为所有负数一定不是 `2` 的幂次,所以代码可以写成下边的样子。 + +```java +public boolean isPowerOfTwo(int n) { + if (n <= 0) { + return false; + } + return (n & (n - 1)) == 0; +} +``` + +# 解法五 + +`java` 中还有一个方法 `Integer.bitCount(n)`,返回 `n` 的二进制形式的 `1` 的个数。 + +```java +public boolean isPowerOfTwo(int n) { + if (n <= 0) { + return false; + } + return Integer.bitCount(n) == 1; +} +``` + +同样的,我们学习一下 `bitCount` 的源码。 + +```java +/** + * Returns the number of one-bits in the two's complement binary + * representation of the specified {@code int} value. This function is + * sometimes referred to as the population count. + * + * @param i the value whose bits are to be counted + * @return the number of one-bits in the two's complement binary + * representation of the specified {@code int} value. + * @since 1.5 + */ +public static int bitCount(int i) { + // HD, Figure 5-2 + i = i - ((i >>> 1) & 0x55555555); + i = (i & 0x33333333) + ((i >>> 2) & 0x33333333); + i = (i + (i >>> 4)) & 0x0f0f0f0f; + i = i + (i >>> 8); + i = i + (i >>> 16); + return i & 0x3f; +} +``` + +[191 题](https://leetcode.wang/leetcode-191-Number-of-1-Bits.html) 题目就是求二进制 `1` 的个数,其中解法三介绍的其实就是上边的解法,我把解释贴过来。 + +有点类似于 [190 题](https://leetcode.wang/leetcode-190-Reverse-Bits.html) 的解法二,通过整体的位操作解决问题,参考 [这里](https://leetcode.com/problems/number-of-1-bits/discuss/55120/Short-code-of-C%2B%2B-O(m)-by-time-m-is-the-count-of-1's-and-another-several-method-of-O(1)-time) ,也是比较 `trick` 的,不容易想到,但还是很有意思的。 + +本质思想就是用本身的比特位去记录对应位数的比特位 `1` 的个数,举个具体的例子吧。为了简洁,求一下 `8` 比特的数字中 `1` 的个数。 + +```java +统计数代表对应括号内 1 的个数 +1 1 0 1 0 0 1 1 +首先把它看做 8 组,统计每组 1 的个数 +原数字:(1) (1) (0) (1) (0) (0) (1) (1) +统计数:(1) (1) (0) (1) (0) (0) (1) (1) +每个数字本身,就天然的代表了当前组 1 的个数。 + +接下来看做 4 组,相邻两组进行合并,统计数其实就是上边相邻组统计数相加即可。 +原数字:(1 1) (0 1) (0 0) (1 1) +统计数:(1 0) (0 1) (0 0) (1 0) +十进制: 2 1 0 2 + +接下来看做 2 组,相邻两组进行合并,统计数变成上边相邻组统计数的和。 +原数字:(1 1 0 1) (0 0 1 1) +统计数:(0 0 1 1) (0 0 1 0) +十进制: 3 2 + +接下来看做 1 组,相邻两组进行合并,统计数变成上边相邻组统计数的和。 +原数字:(1 1 0 1 0 0 1 1) +统计数:(0 0 0 0 0 1 0 1) +十进制: 5 +``` + +看一下 「统计数」的变化,也就是统计的 `1` 的个数。 + +看下二进制形式的变化,两两相加。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/191_2.jpg) + +看下十进制形式的变化,两两相加。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/191_3.jpg) + +最后我们就的得到了 `1` 的个数是 `5`。 + +所以问题的关键就是怎么实现每次合并相邻统计数,我们可以通过位操作实现,举个例子。 + +比如上边 `4` 组到 `2` 组中的前两组合成一组的变化。要把 `(1 0) (0 1)` 两组相加,变成 `(0 0 1 1)` 。其实我们只需要把 `1001` 和 `0011` 相与得到低两位,然后把 `1001` 右移两位再和 `0011` 相与得到高两位,最后将两数相加即可。也就是`(1001) & (0011) + (1001) >>> 2 & (0011)= 0011`。 + +扩展到任意情况,两组合并成一组,如果合并前每组的个数是 `n`,合并前的数字是 `x`,那么合并后的数字就是 `x & (000...111...) + x >>> n & (000...111...) `,其中 `0` 和 `1` 的个数是 `n`。 + +```java +public int hammingWeight(int n) { + n = (n & 0x55555555) + ((n >>> 1) & 0x55555555); // 32 组向 16 组合并,合并前每组 1 个数 + n = (n & 0x33333333) + ((n >>> 2) & 0x33333333); // 16 组向 8 组合并,合并前每组 2 个数 + n = (n & 0x0f0f0f0f) + ((n >>> 4) & 0x0f0f0f0f); // 8 组向 4 组合并,合并前每组 4 个数 + n = (n & 0x00ff00ff)+ ((n >>> 8) & 0x00ff00ff); // 4 组向 2 组合并,合并前每组 8 个数 + n = (n & 0x0000ffff) + ((n >>> 16) & 0x0000ffff); // 2 组向 1 组合并,合并前每组 16 个数 + return n; +} +``` + +写成 `16` 进制可能不好理解,我们拿16 组向 8 组合并举例,合并前每组 2 个数。也就是上边我们推导的,我们要把 `(1 0) (0 1)` 两组合并,需要和 `0011` 按位与,写成 `16` 进制就是 `3`,因为合并完是 `8` 组,所以就是 `8` 个 `3`,即 `0x33333333`。 + +再回到 `java` 的源码。 + +```java +public static int bitCount(int i) { + // HD, Figure 5-2 + i = i - ((i >>> 1) & 0x55555555); + i = (i & 0x33333333) + ((i >>> 2) & 0x33333333); + i = (i + (i >>> 4)) & 0x0f0f0f0f; + i = i + (i >>> 8); + i = i + (i >>> 16); + return i & 0x3f; +} +``` + +第一步写法不一样,也很好理解。结合下边的图看, + +![](https://windliang.oss-cn-beijing.aliyuncs.com/191_2.jpg) + +第一步要做的就是把 `11 -> 10`,`01 - > 01`,`00 -> 00`,`10 -> 01`。 + +其实就是原来的数减去高位的数,`11 - 1 = 10`, `01 - 0 = 01`,`00 - 0 = 00`,`10 - 1 = 01`。 + +第二步一致。 + +第三步,没有与完相加,而是先相加再相与,之所以可以这么做,是因为四位的话最多就是 `4` 个 `1`,也就是 `0100` 和 `0100` 合并,结果最多也就是 `4` 位,所以只需要最后和 `0f` 相与,将高四位置零即可。 + +第四步和第五步没有相与,是因为最多 `32` 个 `1`,用二进制表示就是 `100000`,所以只需要低 `6` 位的结果,高 `8` 位是什么已经不重要了,也就不需要通过相与置零了,最后只需要和 `0x3f(111111)` 相与取得我们的结果即可。 + +# 解法六 + +前边提到对于 `int` 类型,最大的 `2` 的幂次是 2 的 30 次方,即`1073741824`。对于 `n` 分两种情况讨论。 + +* 如果 `n` 是 `2` 的幂次,那么 $$n=2^k$$ + + $$2^{30}=2^k*2^{30-k}$$,所以 $$2^{30} \% 2^k==0$$。 + +* 如果 `n` 不是 `2` 的幂次,那么 $$n = j * 2^k$$,其中 `j` 是一个奇数,因为 `n` 一直进行除以 `2` 可以得到 `k`,直到不能被 `2` 整除,此时一定是奇数,也就是公式中的 `j`。 + + $$2^{30} \% (j * 2^k) = 2^{30-k} \% j!=0$$。 + +综上所述,通过是否能被 `2` 的 `30` 次方,即`1073741824` 整除,即可解决我们的问题。 + + ```java +public boolean isPowerOfTwo(int n) { + if (n <= 0) { + return false; + } + return 1073741824 % n == 0; +} + ``` + +# 解法七 + +超级暴力打表法,因为 `int` 范围内 `2` 的幂次也只有 `31` 个数。 + +```java +public boolean isPowerOfTwo(int n) { + return new HashSet<>(Arrays.asList(1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576, 2097152, 4194304, 8388608,16777216, 33554432, 67108864, 134217728, 268435456, 536870912, 1073741824)).contains(n); +} +``` + +# 总 + 这道题的话使用了很多二进制的技巧,因为题目本身比较简单,所以就是各种大神秀操作了,哈哈,大概是解法最多的一道题了。 \ No newline at end of file diff --git a/leetcode-232-Implement-Queue-using-Stacks.md b/leetcode-232-Implement-Queue-using-Stacks.md index fcb2ad3c9..9110c7a3c 100644 --- a/leetcode-232-Implement-Queue-using-Stacks.md +++ b/leetcode-232-Implement-Queue-using-Stacks.md @@ -1,186 +1,186 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/232.png) - -使用栈来实现队列。 - -# 思路分析 - -[225 题](https://leetcode.wang/leetcode-225-Implement-Stack-using-Queues.html) 是使用队列来实现栈,其中介绍了两种解法,解法一通过一个临时队列来实现 `pop` 和 `peek`。解法二只修改 `push` 。下边的话,我们依旧借助之前的思想来解决这个问题。 - -# 解法一 - -通过一个临时栈,每次 `pop` 的时候将原来的元素都保存到临时栈中,只剩下最后一个元素,这个元素是第一个加入栈中的,对于队列就是第一个应该弹出的。然后再把原来的元素还原到栈中即可。 - -`peek` 的话是同理。 - -```java -class MyQueue { - - Stack stack; - - /** Initialize your data structure here. */ - public MyQueue() { - stack = new Stack<>(); - } - - /** Push element x to the back of queue. */ - public void push(int x) { - stack.push(x); - } - - /** Removes the element from in front of queue and returns that element. */ - public int pop() { - int size = stack.size(); - //保存到临时栈中 - Stack temp = new Stack<>(); - while (size > 0) { - temp.push(stack.pop()); - size--; - } - - int remove = temp.pop(); - - //还原 - size = temp.size(); - while (size > 0) { - stack.push(temp.pop()); - size--; - } - return remove; - } - - /** Get the front element. */ - public int peek() { - int size = stack.size(); - Stack temp = new Stack<>(); - while (size > 0) { - temp.push(stack.pop()); - size--; - } - int top = temp.peek(); - - size = temp.size(); - while (size > 0) { - stack.push(temp.pop()); - size--; - } - return top; - } - - /** Returns whether the queue is empty. */ - public boolean empty() { - return stack.isEmpty(); - } - -} - -/** - * Your MyQueue object will be instantiated and called as such: - * MyQueue obj = new MyQueue(); - * obj.push(x); - * int param_2 = obj.pop(); - * int param_3 = obj.peek(); - * boolean param_4 = obj.empty(); - */ -``` - -# 解法二 - -我们可以像 [225 题](https://leetcode.wang/leetcode-225-Implement-Stack-using-Queues.html) 一样,只修改 `push` 函数。我们只需要每次将新来的元素放到栈底,然后将其他元素还原。 - -```java -class MyQueue { - - Stack stack; - - /** Initialize your data structure here. */ - public MyQueue() { - stack = new Stack<>(); - } - - /** Push element x to the back of queue. */ - public void push(int x) { - Stack temp = new Stack<>(); - int size = stack.size(); - //把原来的保存起来 - while (size > 0) { - temp.push(stack.pop()); - size--; - } - //当前元素压到栈底 - stack.push(x); - size = temp.size(); - //将原来的还原回去 - while (size > 0) { - stack.push(temp.pop()); - size--; - } - } - - /** Removes the element from in front of queue and returns that element. */ - public int pop() { - return stack.pop(); - } - - /** Get the front element. */ - public int peek() { - return stack.peek(); - } - - /** Returns whether the queue is empty. */ - public boolean empty() { - return stack.isEmpty(); - } -} -/** - * Your MyQueue object will be instantiated and called as such: - * MyQueue obj = new MyQueue(); - * obj.push(x); - * int param_2 = obj.pop(); - * int param_3 = obj.peek(); - * boolean param_4 = obj.empty(); - */ -``` - -# 解法三 - -上边两种解法都是使用了临时栈,先弹出再还原,每个元素会遍历两次。 - -参考 [这里](https://leetcode.com/problems/implement-queue-using-stacks/discuss/64206/Short-O(1)-amortized-C%2B%2B-Java-Ruby) ,我们使用两个栈,一个栈输入,一个栈输出。当需要查看或者出队的时候,我们就将输入栈元素依次放入到输出栈中,此时的输出栈的输出顺序刚好和队列是相符的。 - -这样的话,每个元素只会遍历一次了。 - -可以看一下代码。 - -```java -class MyQueue { - - Stack input = new Stack(); - Stack output = new Stack(); - - public void push(int x) { - input.push(x); - } - - public int pop() { - peek(); - return output.pop(); - } - - public int peek() { - if (output.empty()) - while (!input.empty()) - output.push(input.pop()); - return output.peek(); - } - - public boolean empty() { - return input.empty() && output.empty(); - } -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/232.png) + +使用栈来实现队列。 + +# 思路分析 + +[225 题](https://leetcode.wang/leetcode-225-Implement-Stack-using-Queues.html) 是使用队列来实现栈,其中介绍了两种解法,解法一通过一个临时队列来实现 `pop` 和 `peek`。解法二只修改 `push` 。下边的话,我们依旧借助之前的思想来解决这个问题。 + +# 解法一 + +通过一个临时栈,每次 `pop` 的时候将原来的元素都保存到临时栈中,只剩下最后一个元素,这个元素是第一个加入栈中的,对于队列就是第一个应该弹出的。然后再把原来的元素还原到栈中即可。 + +`peek` 的话是同理。 + +```java +class MyQueue { + + Stack stack; + + /** Initialize your data structure here. */ + public MyQueue() { + stack = new Stack<>(); + } + + /** Push element x to the back of queue. */ + public void push(int x) { + stack.push(x); + } + + /** Removes the element from in front of queue and returns that element. */ + public int pop() { + int size = stack.size(); + //保存到临时栈中 + Stack temp = new Stack<>(); + while (size > 0) { + temp.push(stack.pop()); + size--; + } + + int remove = temp.pop(); + + //还原 + size = temp.size(); + while (size > 0) { + stack.push(temp.pop()); + size--; + } + return remove; + } + + /** Get the front element. */ + public int peek() { + int size = stack.size(); + Stack temp = new Stack<>(); + while (size > 0) { + temp.push(stack.pop()); + size--; + } + int top = temp.peek(); + + size = temp.size(); + while (size > 0) { + stack.push(temp.pop()); + size--; + } + return top; + } + + /** Returns whether the queue is empty. */ + public boolean empty() { + return stack.isEmpty(); + } + +} + +/** + * Your MyQueue object will be instantiated and called as such: + * MyQueue obj = new MyQueue(); + * obj.push(x); + * int param_2 = obj.pop(); + * int param_3 = obj.peek(); + * boolean param_4 = obj.empty(); + */ +``` + +# 解法二 + +我们可以像 [225 题](https://leetcode.wang/leetcode-225-Implement-Stack-using-Queues.html) 一样,只修改 `push` 函数。我们只需要每次将新来的元素放到栈底,然后将其他元素还原。 + +```java +class MyQueue { + + Stack stack; + + /** Initialize your data structure here. */ + public MyQueue() { + stack = new Stack<>(); + } + + /** Push element x to the back of queue. */ + public void push(int x) { + Stack temp = new Stack<>(); + int size = stack.size(); + //把原来的保存起来 + while (size > 0) { + temp.push(stack.pop()); + size--; + } + //当前元素压到栈底 + stack.push(x); + size = temp.size(); + //将原来的还原回去 + while (size > 0) { + stack.push(temp.pop()); + size--; + } + } + + /** Removes the element from in front of queue and returns that element. */ + public int pop() { + return stack.pop(); + } + + /** Get the front element. */ + public int peek() { + return stack.peek(); + } + + /** Returns whether the queue is empty. */ + public boolean empty() { + return stack.isEmpty(); + } +} +/** + * Your MyQueue object will be instantiated and called as such: + * MyQueue obj = new MyQueue(); + * obj.push(x); + * int param_2 = obj.pop(); + * int param_3 = obj.peek(); + * boolean param_4 = obj.empty(); + */ +``` + +# 解法三 + +上边两种解法都是使用了临时栈,先弹出再还原,每个元素会遍历两次。 + +参考 [这里](https://leetcode.com/problems/implement-queue-using-stacks/discuss/64206/Short-O(1)-amortized-C%2B%2B-Java-Ruby) ,我们使用两个栈,一个栈输入,一个栈输出。当需要查看或者出队的时候,我们就将输入栈元素依次放入到输出栈中,此时的输出栈的输出顺序刚好和队列是相符的。 + +这样的话,每个元素只会遍历一次了。 + +可以看一下代码。 + +```java +class MyQueue { + + Stack input = new Stack(); + Stack output = new Stack(); + + public void push(int x) { + input.push(x); + } + + public int pop() { + peek(); + return output.pop(); + } + + public int peek() { + if (output.empty()) + while (!input.empty()) + output.push(input.pop()); + return output.peek(); + } + + public boolean empty() { + return input.empty() && output.empty(); + } +} +``` + +# 总 + 解法一和解法二的话是完全按照 [225 题](https://leetcode.wang/leetcode-225-Implement-Stack-using-Queues.html) 的思想,解法三的话,相对解法一和解法二相对要好一些。 \ No newline at end of file diff --git a/leetcode-233-Number-of-Digit-One.md b/leetcode-233-Number-of-Digit-One.md index 824d70d5e..971ec4fa9 100644 --- a/leetcode-233-Number-of-Digit-One.md +++ b/leetcode-233-Number-of-Digit-One.md @@ -1,174 +1,174 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/233.jpg) - -给一个数 `n`,输出 `0 ~ n` 中的数字中 `1` 出现的个数。 - -# 解法一 暴力 - -直接想到的当然就是暴力的方法,一个数一个数的判断,一位一位的判断。 - -```java -public int countDigitOne(int n) { - int num = 0; - for (int i = 1; i <= n; i++) { - int temp = i; - while (temp > 0) { - if (temp % 10 == 1) { - num++; - } - temp /= 10; - } - } - return num; -} -``` - -但这个解法会超时。 - -# 解法二 - -自己也没想到别的方法,讲一下 [这里](https://leetcode.com/problems/number-of-digit-one/discuss/64382/JavaPython-one-pass-solution-easy-to-understand) 的思路。 - -总体思想就是分类,先求所有数中个位是 `1` 的个数,再求十位是 `1` 的个数,再求百位是 `1` 的个数... - -假设 `n = xyzdabc`,此时我们求千位是 `1` 的个数,也就是 `d` 所在的位置。 - -那么此时有三种情况, - -* `d == 0`,那么千位上 `1` 的个数就是 `xyz * 1000` -* `d == 1`,那么千位上 `1` 的个数就是 `xyz * 1000 + abc + 1` -* `d > 1`,那么千位上 `1` 的个数就是 `xyz * 1000 + 1000` - -为什么呢? - -当我们考虑千位是 `1` 的时候,我们将千位定为 `1`,也就是 `xyz1abc`。 - -对于 `xyz` 的话,可以取 `0,1,2...(xyz-1)`,也就是 `xyz` 种可能。 - -当 `xyz` 固定为上边其中的一个数的时候,`abc` 可以取 `0,1,2...999`,也就是 `1000` 种可能。 - -这样的话,总共就是 `xyz*1000` 种可能。 - -注意到,我们前三位只取到了 `xyz-1`,那么如果取 `xyz` 呢? - -此时就出现了上边的三种情况,取决于 `d` 的值。 - -`d == 1` 的时候,千位刚好是 `1`,此时 `abc` 可以取的值就是 `0` 到 `abc` ,所以多加了 `abc + 1`。 - -`d > 1` 的时候,`d` 如果取 `1`,那么 `abc` 就可以取 `0` 到 `999`,此时就多加了 `1000`。 - -再看一个具体的例子。 - -```java -如果n = 4560234 -让我们统计一下千位有多少个 1 -xyz 可以取 0 到 455, abc 可以取 0 到 999 -4551000 to 4551999 (1000) -4541000 to 4541999 (1000) -4531000 to 4531999 (1000) -... - 21000 to 21999 (1000) - 11000 to 11999 (1000) - 1000 to 1999 (1000) -总共就是 456 * 1000 - -如果 n = 4561234 -xyz 可以取 0 到 455, abc 可以取 0 到 999 -4551000 to 4551999 (1000) -4541000 to 4541999 (1000) -4531000 to 4531999 (1000) -... -1000 to 1999 (1000) -xyz 还可以取 456, abc 可以取 0 到 234 -4561000 to 4561234 (234 + 1) -总共就是 456 * 1000 + 234 + 1 - -如果 n = 4563234 -xyz 可以取 0 到 455, abc 可以取 0 到 999 -4551000 to 4551999 (1000) -4541000 to 4541999 (1000) -4531000 to 4531999 (1000) -... -1000 to 1999 (1000) -xyz 还可以取 456, abc 可以取 0 到 999 -4561000 to 4561999 (1000) -总共就是 456 * 1000 + 1000 -``` - -至于其它位的话是一样的道理。 - -代码的话就很好写了。 - -```java -public int countDigitOne(int n) { - int count = 0; - //依次考虑个位、十位、百位...是 1 - //k = 1000, 对应于上边举的例子 - for (int k = 1; k <= n; k *= 10) { - // xyzdabc - int abc = n % k; - int xyzd = n / k; - int d = xyzd % 10; - int xyz = xyzd / 10; - count += xyz * k; - if (d > 1) { - count += k; - } - if (d == 1) { - count += abc + 1; - } - //如果不加这句的话,虽然 k 一直乘以 10,但由于溢出的问题 - //k 本来要大于 n 的时候,却小于了 n 会再次进入循环 - //此时代表最高位是 1 的情况也考虑完成了 - if(xyz == 0){ - break; - } - } - return count; -} -``` - -然后代码的话,可以再简化一下,注意到 `d > 1` 的时候,我们多加了一个 `k`。 - -我们可以通过计算 `long xyz = xyzd / 10;` 的时候,给 `xyzd` 多加 `8`,从而使得当 `d > 1` 的时候,此时求出来的 `xyz` 会比之前大 `1`,这样计算 `xyz * k` 的时候就相当于多加了一个 `k`。 - -此外,上边 `k` 溢出的问题,我们可以通过 `long` 类型解决。 - -```java -public int countDigitOne(int n) { - int count = 0; - for (long k = 1; k <= n; k *= 10) { - // xyzdabc - int abc = n % k; - int xyzd = n / k; - int d = xyzd % 10; - int xyz = (xyzd + 8) / 10; - count += xyz * k; - if (d == 1) { - count += abc + 1; - } - } - return count; -} -``` - -而这个代码,其实和 Solution [高赞](https://leetcode.com/problems/number-of-digit-one/discuss/64381/4%2B-lines-O(log-n)-C%2B%2BJavaPython) 中的解法是一样的,只不过省去了 `xyz` 和 `d` 这两个变量。 - -```java -public int countDigitOne(int n) { - int count = 0; - - for (long k = 1; k <= n; k *= 10) { - long r = n / k, m = n % k; - // sum up the count of ones on every place k - count += (r + 8) / 10 * k + (r % 10 == 1 ? m + 1 : 0); - } - - return count; -} -``` - -# 总 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/233.jpg) + +给一个数 `n`,输出 `0 ~ n` 中的数字中 `1` 出现的个数。 + +# 解法一 暴力 + +直接想到的当然就是暴力的方法,一个数一个数的判断,一位一位的判断。 + +```java +public int countDigitOne(int n) { + int num = 0; + for (int i = 1; i <= n; i++) { + int temp = i; + while (temp > 0) { + if (temp % 10 == 1) { + num++; + } + temp /= 10; + } + } + return num; +} +``` + +但这个解法会超时。 + +# 解法二 + +自己也没想到别的方法,讲一下 [这里](https://leetcode.com/problems/number-of-digit-one/discuss/64382/JavaPython-one-pass-solution-easy-to-understand) 的思路。 + +总体思想就是分类,先求所有数中个位是 `1` 的个数,再求十位是 `1` 的个数,再求百位是 `1` 的个数... + +假设 `n = xyzdabc`,此时我们求千位是 `1` 的个数,也就是 `d` 所在的位置。 + +那么此时有三种情况, + +* `d == 0`,那么千位上 `1` 的个数就是 `xyz * 1000` +* `d == 1`,那么千位上 `1` 的个数就是 `xyz * 1000 + abc + 1` +* `d > 1`,那么千位上 `1` 的个数就是 `xyz * 1000 + 1000` + +为什么呢? + +当我们考虑千位是 `1` 的时候,我们将千位定为 `1`,也就是 `xyz1abc`。 + +对于 `xyz` 的话,可以取 `0,1,2...(xyz-1)`,也就是 `xyz` 种可能。 + +当 `xyz` 固定为上边其中的一个数的时候,`abc` 可以取 `0,1,2...999`,也就是 `1000` 种可能。 + +这样的话,总共就是 `xyz*1000` 种可能。 + +注意到,我们前三位只取到了 `xyz-1`,那么如果取 `xyz` 呢? + +此时就出现了上边的三种情况,取决于 `d` 的值。 + +`d == 1` 的时候,千位刚好是 `1`,此时 `abc` 可以取的值就是 `0` 到 `abc` ,所以多加了 `abc + 1`。 + +`d > 1` 的时候,`d` 如果取 `1`,那么 `abc` 就可以取 `0` 到 `999`,此时就多加了 `1000`。 + +再看一个具体的例子。 + +```java +如果n = 4560234 +让我们统计一下千位有多少个 1 +xyz 可以取 0 到 455, abc 可以取 0 到 999 +4551000 to 4551999 (1000) +4541000 to 4541999 (1000) +4531000 to 4531999 (1000) +... + 21000 to 21999 (1000) + 11000 to 11999 (1000) + 1000 to 1999 (1000) +总共就是 456 * 1000 + +如果 n = 4561234 +xyz 可以取 0 到 455, abc 可以取 0 到 999 +4551000 to 4551999 (1000) +4541000 to 4541999 (1000) +4531000 to 4531999 (1000) +... +1000 to 1999 (1000) +xyz 还可以取 456, abc 可以取 0 到 234 +4561000 to 4561234 (234 + 1) +总共就是 456 * 1000 + 234 + 1 + +如果 n = 4563234 +xyz 可以取 0 到 455, abc 可以取 0 到 999 +4551000 to 4551999 (1000) +4541000 to 4541999 (1000) +4531000 to 4531999 (1000) +... +1000 to 1999 (1000) +xyz 还可以取 456, abc 可以取 0 到 999 +4561000 to 4561999 (1000) +总共就是 456 * 1000 + 1000 +``` + +至于其它位的话是一样的道理。 + +代码的话就很好写了。 + +```java +public int countDigitOne(int n) { + int count = 0; + //依次考虑个位、十位、百位...是 1 + //k = 1000, 对应于上边举的例子 + for (int k = 1; k <= n; k *= 10) { + // xyzdabc + int abc = n % k; + int xyzd = n / k; + int d = xyzd % 10; + int xyz = xyzd / 10; + count += xyz * k; + if (d > 1) { + count += k; + } + if (d == 1) { + count += abc + 1; + } + //如果不加这句的话,虽然 k 一直乘以 10,但由于溢出的问题 + //k 本来要大于 n 的时候,却小于了 n 会再次进入循环 + //此时代表最高位是 1 的情况也考虑完成了 + if(xyz == 0){ + break; + } + } + return count; +} +``` + +然后代码的话,可以再简化一下,注意到 `d > 1` 的时候,我们多加了一个 `k`。 + +我们可以通过计算 `long xyz = xyzd / 10;` 的时候,给 `xyzd` 多加 `8`,从而使得当 `d > 1` 的时候,此时求出来的 `xyz` 会比之前大 `1`,这样计算 `xyz * k` 的时候就相当于多加了一个 `k`。 + +此外,上边 `k` 溢出的问题,我们可以通过 `long` 类型解决。 + +```java +public int countDigitOne(int n) { + int count = 0; + for (long k = 1; k <= n; k *= 10) { + // xyzdabc + int abc = n % k; + int xyzd = n / k; + int d = xyzd % 10; + int xyz = (xyzd + 8) / 10; + count += xyz * k; + if (d == 1) { + count += abc + 1; + } + } + return count; +} +``` + +而这个代码,其实和 Solution [高赞](https://leetcode.com/problems/number-of-digit-one/discuss/64381/4%2B-lines-O(log-n)-C%2B%2BJavaPython) 中的解法是一样的,只不过省去了 `xyz` 和 `d` 这两个变量。 + +```java +public int countDigitOne(int n) { + int count = 0; + + for (long k = 1; k <= n; k *= 10) { + long r = n / k, m = n % k; + // sum up the count of ones on every place k + count += (r + 8) / 10 * k + (r % 10 == 1 ? m + 1 : 0); + } + + return count; +} +``` + +# 总 + 这道题的话,就是数学的分类、找规律的题目了,和 [172 题](https://leetcode.wang/leetcode-172-Factorial-Trailing-Zeroes.html) 找阶乘中 `0` 的个数一样,没有一些通用的算法,完全靠分析能力,如果面试碰到很容易卡主。 \ No newline at end of file diff --git a/leetcode-234-Palindrome-Linked-List.md b/leetcode-234-Palindrome-Linked-List.md index ac9405a32..3bf57de83 100644 --- a/leetcode-234-Palindrome-Linked-List.md +++ b/leetcode-234-Palindrome-Linked-List.md @@ -1,105 +1,105 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/234.jpg) - -判断一个链表存储的数字是否是回文数字。 - -# 思路分析 - -这个题就难在链表不能随机读取,不能像数组那样直接首尾首尾的依次判断。如果不考虑额外的空间的话,我们只需要把链表中的数字存储到数组中然后判断即可。 - -如果不用额外空间的话,我们可以把链表分成两半,把后一半倒置,和前一半依次判断即可。 - -# 解法一 - -两个关键点。 - -* 链表分成两半。 - - 最直接的方法就是先遍历一遍链表得到链表的长度 `n`,然后再遍历 `n/2` 次就找到了中点。 - - 还有一个比较 trick 的方法,在 [143 题](https://leetcode.wang/leetcode-143-Reorder-List.html),[148 题](https://leetcode.wang/leetcode-148-Sort-List.html) 都用过了,也就是快慢指针,快指针一次走两步,慢指针一次走一步,当快指针走到终点的时候,慢指针此时就到了中点。 - - ```java - // 找中点,链表分成两个 - ListNode slow = head; - ListNode fast = head; - while (fast != null && fast.next != null) { - slow = slow.next; - fast = fast.next.next; - } - ``` - - 需要注意的是,当链表个数为偶数的时候,`slow` 指向第二个链表的开始。当链表个数为奇数的时候是,`slow` 指向最中间的位置。 - -* 链表倒置 - - 在 [第 2 题](https://leetcode.wang/leetCode-2-Add-Two-Numbers.html) 的时候已经介绍过链表倒置了。 - - ```java - private ListNode reverseList(ListNode head) { - if (head == null) { - return null; - } - ListNode tail = null; - while (head != null) { - ListNode temp = head.next; - head.next = tail; - tail = head; - head = temp; - } - - return tail; - } - ``` - -然后整体的代码就出来了。 - -```java -public boolean isPalindrome(ListNode head) { - if (head == null || head.next == null) { - return true; - } - // 找中点,链表分成两个 - ListNode slow = head; - ListNode fast = head; - while (fast != null && fast.next != null) { - slow = slow.next; - fast = fast.next.next; - } - - // 第二个链表倒置 - ListNode newHead = reverseList(slow); - - // 前一半和后一半依次比较 - while (newHead != null) { - if (head.val != newHead.val) { - return false; - } - head = head.next; - newHead = newHead.next; - } - return true; -} - -private ListNode reverseList(ListNode head) { - if (head == null) { - return null; - } - ListNode tail = null; - while (head != null) { - ListNode temp = head.next; - head.next = tail; - tail = head; - head = temp; - } - - return tail; -} -``` - -# 总 - -其实还有一个争议的地方,利用输入的数据,算不算空间复杂度是 `O(1)`,上边的解法我们改变了原来链表的结构,即使我们在 `return` 前可以再将链表还原,但如果较真的话,还是可以说它空间复杂度不是 `O(1)`,因为如果输入的数据只是可读的,我们的算法确实需要额外空间。 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/234.jpg) + +判断一个链表存储的数字是否是回文数字。 + +# 思路分析 + +这个题就难在链表不能随机读取,不能像数组那样直接首尾首尾的依次判断。如果不考虑额外的空间的话,我们只需要把链表中的数字存储到数组中然后判断即可。 + +如果不用额外空间的话,我们可以把链表分成两半,把后一半倒置,和前一半依次判断即可。 + +# 解法一 + +两个关键点。 + +* 链表分成两半。 + + 最直接的方法就是先遍历一遍链表得到链表的长度 `n`,然后再遍历 `n/2` 次就找到了中点。 + + 还有一个比较 trick 的方法,在 [143 题](https://leetcode.wang/leetcode-143-Reorder-List.html),[148 题](https://leetcode.wang/leetcode-148-Sort-List.html) 都用过了,也就是快慢指针,快指针一次走两步,慢指针一次走一步,当快指针走到终点的时候,慢指针此时就到了中点。 + + ```java + // 找中点,链表分成两个 + ListNode slow = head; + ListNode fast = head; + while (fast != null && fast.next != null) { + slow = slow.next; + fast = fast.next.next; + } + ``` + + 需要注意的是,当链表个数为偶数的时候,`slow` 指向第二个链表的开始。当链表个数为奇数的时候是,`slow` 指向最中间的位置。 + +* 链表倒置 + + 在 [第 2 题](https://leetcode.wang/leetCode-2-Add-Two-Numbers.html) 的时候已经介绍过链表倒置了。 + + ```java + private ListNode reverseList(ListNode head) { + if (head == null) { + return null; + } + ListNode tail = null; + while (head != null) { + ListNode temp = head.next; + head.next = tail; + tail = head; + head = temp; + } + + return tail; + } + ``` + +然后整体的代码就出来了。 + +```java +public boolean isPalindrome(ListNode head) { + if (head == null || head.next == null) { + return true; + } + // 找中点,链表分成两个 + ListNode slow = head; + ListNode fast = head; + while (fast != null && fast.next != null) { + slow = slow.next; + fast = fast.next.next; + } + + // 第二个链表倒置 + ListNode newHead = reverseList(slow); + + // 前一半和后一半依次比较 + while (newHead != null) { + if (head.val != newHead.val) { + return false; + } + head = head.next; + newHead = newHead.next; + } + return true; +} + +private ListNode reverseList(ListNode head) { + if (head == null) { + return null; + } + ListNode tail = null; + while (head != null) { + ListNode temp = head.next; + head.next = tail; + tail = head; + head = temp; + } + + return tail; +} +``` + +# 总 + +其实还有一个争议的地方,利用输入的数据,算不算空间复杂度是 `O(1)`,上边的解法我们改变了原来链表的结构,即使我们在 `return` 前可以再将链表还原,但如果较真的话,还是可以说它空间复杂度不是 `O(1)`,因为如果输入的数据只是可读的,我们的算法确实需要额外空间。 + 详见 [discuss](https://leetcode.com/problems/palindrome-linked-list/discuss/64493/Reversing-a-list-is-not-considered-"O(1)-space") 里大神们的讨论,我觉得不用纠结,因为这完全取决于定义,定义的话不也是人定的吗,达成共识即可,具体问题再具体分析。 \ No newline at end of file diff --git a/leetcode-235-Lowest-Common-Ancestor-of-a-Binary-Search-Tree.md b/leetcode-235-Lowest-Common-Ancestor-of-a-Binary-Search-Tree.md index b3073b2bc..d18c80529 100644 --- a/leetcode-235-Lowest-Common-Ancestor-of-a-Binary-Search-Tree.md +++ b/leetcode-235-Lowest-Common-Ancestor-of-a-Binary-Search-Tree.md @@ -1,81 +1,81 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/235.png) - -从二叉搜索树中,找出两个节点的最近的共同祖先。 - -二叉搜索树定义如下。 - -> 1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值; -> 2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值; -> 3. 任意节点的左、右子树也分别为二叉查找树; -> 4. 没有键值相等的节点。 - -# 解法一 递归 - -由于是二叉搜索树,所以找最近的共同祖先比较容易,总共就三种情况。 - -* 如果给定的两个节点的值都小于根节点的值,那么最近的共同祖先一定在左子树 -* 如果给定的两个节点的值都大于根节点的值,那么最近的共同祖先一定在右子树 -* 如果一个大于等于、一个小于等于根节点的值,那么当前根节点就是最近的共同祖先了 - -至于前两种情况用递归继续去解决即可。 - -代码的话,我们可以通过交换使得 `p.val <= q.val` ,这样就可以简化后边 `if` 语句的判断。 - -```java -public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { - - // 保持 p.val <= q.val - if (p.val > q.val) { - return lowestCommonAncestor(root, q, p); - } - //如果有一个是根节点就可以提前结束, 当然这个 if 不要也可以 - if (p.val == root.val || q.val == root.val) { - return root; - } - if (q.val < root.val) { - return lowestCommonAncestor(root.left, p, q); - } else if (p.val > root.val) { - return lowestCommonAncestor(root.right, p, q); - } else { - return root; - } - -} -``` - -# 解法二 迭代 - -上边的递归比较简单,可以直接改写成循环的形式。 - -```java -public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { - int pVal = p.val; - int qVal = q.val; - if (pVal == root.val || qVal == root.val) { - return root; - } - // 保持 p.val <= q.val - if (pVal > qVal) { - int temp = pVal; - pVal = qVal; - qVal = temp; - } - while (true) { - if (qVal < root.val) { - root = root.left; - } else if (pVal > root.val) { - root = root.right; - } else { - return root; - } - } -} -``` - -# 总 - -只要知道二叉搜索树的定义,这个题就很好解了。当然之前的题目都是用的二叉搜索树的另一个性质,「中序遍历输出的是一个升序数组」,比如刚做完的 [230 题](https://leetcode.wang/leetcode-230-Kth-Smallest-Element-in-a-BST.html)。 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/235.png) + +从二叉搜索树中,找出两个节点的最近的共同祖先。 + +二叉搜索树定义如下。 + +> 1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值; +> 2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值; +> 3. 任意节点的左、右子树也分别为二叉查找树; +> 4. 没有键值相等的节点。 + +# 解法一 递归 + +由于是二叉搜索树,所以找最近的共同祖先比较容易,总共就三种情况。 + +* 如果给定的两个节点的值都小于根节点的值,那么最近的共同祖先一定在左子树 +* 如果给定的两个节点的值都大于根节点的值,那么最近的共同祖先一定在右子树 +* 如果一个大于等于、一个小于等于根节点的值,那么当前根节点就是最近的共同祖先了 + +至于前两种情况用递归继续去解决即可。 + +代码的话,我们可以通过交换使得 `p.val <= q.val` ,这样就可以简化后边 `if` 语句的判断。 + +```java +public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { + + // 保持 p.val <= q.val + if (p.val > q.val) { + return lowestCommonAncestor(root, q, p); + } + //如果有一个是根节点就可以提前结束, 当然这个 if 不要也可以 + if (p.val == root.val || q.val == root.val) { + return root; + } + if (q.val < root.val) { + return lowestCommonAncestor(root.left, p, q); + } else if (p.val > root.val) { + return lowestCommonAncestor(root.right, p, q); + } else { + return root; + } + +} +``` + +# 解法二 迭代 + +上边的递归比较简单,可以直接改写成循环的形式。 + +```java +public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { + int pVal = p.val; + int qVal = q.val; + if (pVal == root.val || qVal == root.val) { + return root; + } + // 保持 p.val <= q.val + if (pVal > qVal) { + int temp = pVal; + pVal = qVal; + qVal = temp; + } + while (true) { + if (qVal < root.val) { + root = root.left; + } else if (pVal > root.val) { + root = root.right; + } else { + return root; + } + } +} +``` + +# 总 + +只要知道二叉搜索树的定义,这个题就很好解了。当然之前的题目都是用的二叉搜索树的另一个性质,「中序遍历输出的是一个升序数组」,比如刚做完的 [230 题](https://leetcode.wang/leetcode-230-Kth-Smallest-Element-in-a-BST.html)。 + 对于二叉树的题,开始可以用递归的思想去思考会比较简单。 \ No newline at end of file diff --git a/leetcode-236-Lowest-Common-Ancestor-of-a-Binary-Tree.md b/leetcode-236-Lowest-Common-Ancestor-of-a-Binary-Tree.md index 2a6308d42..b6c3b3da8 100644 --- a/leetcode-236-Lowest-Common-Ancestor-of-a-Binary-Tree.md +++ b/leetcode-236-Lowest-Common-Ancestor-of-a-Binary-Tree.md @@ -1,183 +1,183 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/236.png) - -给定二叉树的两个节点,找出两个节点的最近的共同祖先。 - -# 解法一 - -刚做的 [235 题](https://leetcode.wang/leetcode-235-Lowest-Common-Ancestor-of-a-Binary-Search-Tree.html) 是这个题的子问题, [235 题](https://leetcode.wang/leetcode-235-Lowest-Common-Ancestor-of-a-Binary-Search-Tree.html)是让我们在二叉搜索树中找两个节点的最近的共同祖先。当时分了三种情况。 - -- 如果给定的两个节点的值都小于根节点的值,那么最近的共同祖先一定在左子树 -- 如果给定的两个节点的值都大于根节点的值,那么最近的共同祖先一定在右子树 -- 如果一个大于等于、一个小于等于根节点的值,那么当前根节点就是最近的共同祖先了 - -通过确定两个节点的位置,然后再用递归去解决问题。 - -之前是二叉搜索树,所以通过和根节点的值进行比较就能知道节点的是在左子树和右子树了,这道题的话我们只有通过遍历去寻找给定的节点,从而确定节点是在左子树还是右子树了。 - -遍历采用 [94 题](https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html) 的中序遍历,栈的形式。 - -```java -public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { - if (root == p || root == q) { - return root; - } - Stack stack = new Stack<>(); - //中序遍历判断两个节点是否在左子树 - TreeNode cur = root.left; - boolean pLeft = false; - boolean qLeft = false; - while (cur != null || !stack.isEmpty()) { - // 节点不为空一直压栈 - while (cur != null) { - stack.push(cur); - cur = cur.left; // 考虑左子树 - } - // 节点为空,就出栈 - cur = stack.pop(); - // 判断是否等于 p 节点 - if (cur == p) { - pLeft = true; - } - // 判断是否等于 q 节点 - if (cur == q) { - qLeft = true; - } - - if(pLeft && qLeft){ - break; - } - // 考虑右子树 - cur = cur.right; - } - - //两个节点都在左子树 - if (pLeft && qLeft) { - return lowestCommonAncestor(root.left, p, q); - //两个节点都在右子树 - } else if (!pLeft && !qLeft) { - return lowestCommonAncestor(root.right, p, q); - } - //一左一右 - return root; -} -``` - -# 解法二 - -参考 [这里](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree/discuss/65225/4-lines-C%2B%2BJavaPythonRuby) 。 - -我们注意到如果两个节点在左子树中的最近共同祖先是 `r`,那么当前树的最近公共祖先也就是 `r`,所以我们可以用递归的方式去解决。 - -```java -public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { - if (root == null || root == p || root == q ) { - return root; - } - TreeNode leftCommonAncestor = lowestCommonAncestor(root.left, p, q); - TreeNode rightCommonAncestor = lowestCommonAncestor(root.right, p, q); - //在左子树中没有找到,那一定在右子树中 - if(leftCommonAncestor == null){ - return rightCommonAncestor; - } - //在右子树中没有找到,那一定在左子树中 - if(rightCommonAncestor == null){ - return leftCommonAncestor; - } - //不在左子树,也不在右子树,那说明是根节点 - return root; -} -``` - -对于 `lowestCommonAncestor` 这个函数的理解的话,它不一定可以返回最近的共同祖先,如果子树中只能找到 `p` 节点或者 `q` 节点,它最终返回其实就是 `p` 节点或者 `q` 节点。这其实对应于最后一种情况,也就是 `leftCommonAncestor` 和 `rightCommonAncestor` 都不为 `null`,说明 `p` 节点和 `q` 节点分处于两个子树中,直接 `return root`。 - -相对于解法一的话快了很多,因为不需要每次都遍历一遍二叉树,这个解法所有节点只会遍历一次。 - -# 解法三 - -参考 [这里](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree/discuss/65236/JavaPython-iterative-solution) ,也很有意思,分享一下。 - -`root` 节点一定是 `p` 节点和 `q` 节点的共同祖先,只不过这道题要找的是最近的共同祖先。 - -从 `root` 节点出发有一条唯一的路径到达 `p`。 - -从 `root` 节点出发也有一条唯一的路径到达 `q`。 - -可以抽象成下图的样子。 - -```java - root - | - | - | - r - / \ - / \ - / / - \ \ - p \ - q -``` - -事实上,我们要找的最近的共同祖先就是第一次出现分叉的时候,也就是上图中的 `r`。 - -那么怎么找到 `r` 呢? - -我们可以把从 `root` 到 `p` 和 `root` 到 `q` 的路径找到,比如说是 - -`root -> * -> * -> r -> x -> x -> p` - -`root -> * -> * -> r -> y -> y -> y -> y -> q` - -然后我们倒着遍历其中一条路径,然后看当前节点在不在另一条路径中,当第一次出现在的时候,这个节点就是我们要找到的最近的公共祖先了。 - -比如倒着遍历 `root` 到 `q` 的路径。 - -依次判断 `q` 在不在 `root` 到 `p` 的路径中,`y` 在不在? ... `r` 在不在? 发现 `r` 在,说明 `r` 就是我们要找到的节点。 - -代码实现的话,因为我们要倒着遍历某一条路径,所以可以用 `HashMap` 来保存每个节点的父节点,从而可以方便的倒着遍历。 - -另外我们要判断路径中有没有某个节点,所以我们要把这条路径的所有节点存到 `HashSet` 中方便判断。 - -寻找路径的话,我们一开始肯定不知道该向左还是向右,所以我们采取遍历整个树,直到找到了 `p` 和 `q` ,然后从 `p` 和 `q` 开始,通过 `hashMap` 存储的每个节点的父节点,就可以倒着遍历回去确定路径。 - -```java -public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { - Stack stack = new Stack<>(); - HashMap parent = new HashMap<>(); - stack.push(root); - parent.put(root, null); - //将遍历过程中每个节点的父节点保存起来 - while (!parent.containsKey(p) || !parent.containsKey(q)) { - TreeNode cur = stack.pop(); - if (cur.left != null) { - stack.push(cur.left); - parent.put(cur.left, cur); - } - if (cur.right != null) { - stack.push(cur.right); - parent.put(cur.right, cur); - } - } - HashSet path = new HashSet<>(); - // 倒着还原 p 的路径,并将每个节点加入到 set 中 - while (p != null) { - path.add(p); - p = parent.get(p); - } - - // 倒着遍历 q 的路径,判断是否在 p 的路径中 - while (q != null) { - if (path.contains(q)) { - break; - } - q = parent.get(q); - } - return q; -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/236.png) + +给定二叉树的两个节点,找出两个节点的最近的共同祖先。 + +# 解法一 + +刚做的 [235 题](https://leetcode.wang/leetcode-235-Lowest-Common-Ancestor-of-a-Binary-Search-Tree.html) 是这个题的子问题, [235 题](https://leetcode.wang/leetcode-235-Lowest-Common-Ancestor-of-a-Binary-Search-Tree.html)是让我们在二叉搜索树中找两个节点的最近的共同祖先。当时分了三种情况。 + +- 如果给定的两个节点的值都小于根节点的值,那么最近的共同祖先一定在左子树 +- 如果给定的两个节点的值都大于根节点的值,那么最近的共同祖先一定在右子树 +- 如果一个大于等于、一个小于等于根节点的值,那么当前根节点就是最近的共同祖先了 + +通过确定两个节点的位置,然后再用递归去解决问题。 + +之前是二叉搜索树,所以通过和根节点的值进行比较就能知道节点的是在左子树和右子树了,这道题的话我们只有通过遍历去寻找给定的节点,从而确定节点是在左子树还是右子树了。 + +遍历采用 [94 题](https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html) 的中序遍历,栈的形式。 + +```java +public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { + if (root == p || root == q) { + return root; + } + Stack stack = new Stack<>(); + //中序遍历判断两个节点是否在左子树 + TreeNode cur = root.left; + boolean pLeft = false; + boolean qLeft = false; + while (cur != null || !stack.isEmpty()) { + // 节点不为空一直压栈 + while (cur != null) { + stack.push(cur); + cur = cur.left; // 考虑左子树 + } + // 节点为空,就出栈 + cur = stack.pop(); + // 判断是否等于 p 节点 + if (cur == p) { + pLeft = true; + } + // 判断是否等于 q 节点 + if (cur == q) { + qLeft = true; + } + + if(pLeft && qLeft){ + break; + } + // 考虑右子树 + cur = cur.right; + } + + //两个节点都在左子树 + if (pLeft && qLeft) { + return lowestCommonAncestor(root.left, p, q); + //两个节点都在右子树 + } else if (!pLeft && !qLeft) { + return lowestCommonAncestor(root.right, p, q); + } + //一左一右 + return root; +} +``` + +# 解法二 + +参考 [这里](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree/discuss/65225/4-lines-C%2B%2BJavaPythonRuby) 。 + +我们注意到如果两个节点在左子树中的最近共同祖先是 `r`,那么当前树的最近公共祖先也就是 `r`,所以我们可以用递归的方式去解决。 + +```java +public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { + if (root == null || root == p || root == q ) { + return root; + } + TreeNode leftCommonAncestor = lowestCommonAncestor(root.left, p, q); + TreeNode rightCommonAncestor = lowestCommonAncestor(root.right, p, q); + //在左子树中没有找到,那一定在右子树中 + if(leftCommonAncestor == null){ + return rightCommonAncestor; + } + //在右子树中没有找到,那一定在左子树中 + if(rightCommonAncestor == null){ + return leftCommonAncestor; + } + //不在左子树,也不在右子树,那说明是根节点 + return root; +} +``` + +对于 `lowestCommonAncestor` 这个函数的理解的话,它不一定可以返回最近的共同祖先,如果子树中只能找到 `p` 节点或者 `q` 节点,它最终返回其实就是 `p` 节点或者 `q` 节点。这其实对应于最后一种情况,也就是 `leftCommonAncestor` 和 `rightCommonAncestor` 都不为 `null`,说明 `p` 节点和 `q` 节点分处于两个子树中,直接 `return root`。 + +相对于解法一的话快了很多,因为不需要每次都遍历一遍二叉树,这个解法所有节点只会遍历一次。 + +# 解法三 + +参考 [这里](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree/discuss/65236/JavaPython-iterative-solution) ,也很有意思,分享一下。 + +`root` 节点一定是 `p` 节点和 `q` 节点的共同祖先,只不过这道题要找的是最近的共同祖先。 + +从 `root` 节点出发有一条唯一的路径到达 `p`。 + +从 `root` 节点出发也有一条唯一的路径到达 `q`。 + +可以抽象成下图的样子。 + +```java + root + | + | + | + r + / \ + / \ + / / + \ \ + p \ + q +``` + +事实上,我们要找的最近的共同祖先就是第一次出现分叉的时候,也就是上图中的 `r`。 + +那么怎么找到 `r` 呢? + +我们可以把从 `root` 到 `p` 和 `root` 到 `q` 的路径找到,比如说是 + +`root -> * -> * -> r -> x -> x -> p` + +`root -> * -> * -> r -> y -> y -> y -> y -> q` + +然后我们倒着遍历其中一条路径,然后看当前节点在不在另一条路径中,当第一次出现在的时候,这个节点就是我们要找到的最近的公共祖先了。 + +比如倒着遍历 `root` 到 `q` 的路径。 + +依次判断 `q` 在不在 `root` 到 `p` 的路径中,`y` 在不在? ... `r` 在不在? 发现 `r` 在,说明 `r` 就是我们要找到的节点。 + +代码实现的话,因为我们要倒着遍历某一条路径,所以可以用 `HashMap` 来保存每个节点的父节点,从而可以方便的倒着遍历。 + +另外我们要判断路径中有没有某个节点,所以我们要把这条路径的所有节点存到 `HashSet` 中方便判断。 + +寻找路径的话,我们一开始肯定不知道该向左还是向右,所以我们采取遍历整个树,直到找到了 `p` 和 `q` ,然后从 `p` 和 `q` 开始,通过 `hashMap` 存储的每个节点的父节点,就可以倒着遍历回去确定路径。 + +```java +public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { + Stack stack = new Stack<>(); + HashMap parent = new HashMap<>(); + stack.push(root); + parent.put(root, null); + //将遍历过程中每个节点的父节点保存起来 + while (!parent.containsKey(p) || !parent.containsKey(q)) { + TreeNode cur = stack.pop(); + if (cur.left != null) { + stack.push(cur.left); + parent.put(cur.left, cur); + } + if (cur.right != null) { + stack.push(cur.right); + parent.put(cur.right, cur); + } + } + HashSet path = new HashSet<>(); + // 倒着还原 p 的路径,并将每个节点加入到 set 中 + while (p != null) { + path.add(p); + p = parent.get(p); + } + + // 倒着遍历 q 的路径,判断是否在 p 的路径中 + while (q != null) { + if (path.contains(q)) { + break; + } + q = parent.get(q); + } + return q; +} +``` + +# 总 + 解法一的话是受到上一题的影响,理论上应该可以直接想到解法二的,是一个很常规的递归的问题。解法三的话,想法很新颖。 \ No newline at end of file diff --git a/leetcode-237-Delete-Node-in-a-Linked-List.md b/leetcode-237-Delete-Node-in-a-Linked-List.md index 684698907..702961283 100644 --- a/leetcode-237-Delete-Node-in-a-Linked-List.md +++ b/leetcode-237-Delete-Node-in-a-Linked-List.md @@ -1,33 +1,33 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/237.png) - -删除链表的某个节点。 - -# 解法一 - -然后我以为就是一个简单的链表删除节点的题,但看到给的函数懵逼了。 - -```java -public void deleteNode(ListNode node) { - -} -``` - -???头结点呢?没有头结点怎么删除,函数给错了吧。 - -然后看了 [solution](https://leetcode.com/problems/delete-node-in-a-linked-list/solution/)。 - -```java -public void deleteNode(ListNode node) { - node.val = node.next.val; - node.next = node.next.next; -} -``` - -好吧,我佛了,感觉感情受到了欺骗,这算什么删除节点... - -# 总 - -感觉很无聊的一道题,没有什么意义,可以看一下 [203 题](https://leetcode.wang/leetcode-203-Remove-Linked-List-Elements.html),纯正的删除节点的题目。 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/237.png) + +删除链表的某个节点。 + +# 解法一 + +然后我以为就是一个简单的链表删除节点的题,但看到给的函数懵逼了。 + +```java +public void deleteNode(ListNode node) { + +} +``` + +???头结点呢?没有头结点怎么删除,函数给错了吧。 + +然后看了 [solution](https://leetcode.com/problems/delete-node-in-a-linked-list/solution/)。 + +```java +public void deleteNode(ListNode node) { + node.val = node.next.val; + node.next = node.next.next; +} +``` + +好吧,我佛了,感觉感情受到了欺骗,这算什么删除节点... + +# 总 + +感觉很无聊的一道题,没有什么意义,可以看一下 [203 题](https://leetcode.wang/leetcode-203-Remove-Linked-List-Elements.html),纯正的删除节点的题目。 + diff --git a/leetcode-238-Product-of-Array-Except-Self.md b/leetcode-238-Product-of-Array-Except-Self.md index 7853aba6e..42e4cbddb 100644 --- a/leetcode-238-Product-of-Array-Except-Self.md +++ b/leetcode-238-Product-of-Array-Except-Self.md @@ -1,188 +1,188 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/238.jpg) - -返回一个数组,第 `i` 个位置存储原数组除了第 `i` 个数以外的所有数的乘积。 - -# 解法一 - -最直接的想法就是先把所有的数乘起来,然后对于需要返回的数组的第 `i` 个位置,只需要将所有数的累乘结果除以第 `i` 个数即可。 - -如果所有数的累乘结果记为 `mul`,返回的数组用 `res` 存储,原数组用 `nums` 存储,那么 - - `res[i] = mul / nums[i]`。 - -除法的话需要考虑除零的问题,如果 `nums` 中有一个 `0`,那么 `res` 除了 `0` 的那个位置的结果是其余数累乘,其余位置的结果就都是 `0` 了。 - -如果 `nums` 中 `0`的个数超过一个,那么 `res` 所有结果就都是 `0` 了。 - -```java -public int[] productExceptSelf(int[] nums) { - int mul = 1; - int zeroNums = 0; - int zeroFirst = -1; - for (int i = 0; i < nums.length; i++) { - if (nums[i] == 0) { - zeroNums++; - if (zeroNums == 1) { - zeroFirst = i; - } - continue; - } - mul *= nums[i]; - } - int[] res = new int[nums.length]; - if (zeroNums > 1) { - return res; - }else if(zeroNums == 1){ - res[zeroFirst] = mul; - return res; - } - - for (int i = 0; i < nums.length; i++) { - res[i] = mul / nums[i]; - } - return res; -} -``` - -当然了题目中说不能用除法,恰巧在 [29 题](https://leetcode.wang/leetCode-29-Divide-Two-Integers.html) 我们用加减法实现过除法,这里的话就可以直接调用了。 - -```java -public int[] productExceptSelf(int[] nums) { - int mul = 1; - int zeroNums = 0; - int zeroFirst = -1; - for (int i = 0; i < nums.length; i++) { - if (nums[i] == 0) { - zeroNums++; - if (zeroNums == 1) { - zeroFirst = i; - } - continue; - } - mul *= nums[i]; - } - int[] res = new int[nums.length]; - if (zeroNums > 1) { - return res; - } else if (zeroNums == 1) { - res[zeroFirst] = mul; - return res; - } - for (int i = 0; i < nums.length; i++) { - res[i] = divide(mul, nums[i]); - } - return res; -} - -//下边是 29 题实现的除法 -public int divide(int dividend, int divisor) { - long ans = divide((long) dividend, (long) (divisor)); - long m = 2147483648L; - if (ans == m) { - return Integer.MAX_VALUE; - } else { - return (int) ans; - } -} - -public long divide(long dividend, long divisor) { - long ans = 1; - long sign = 1; - if (dividend < 0) { - sign = opposite(sign); - dividend = opposite(dividend); - } - if (divisor < 0) { - sign = opposite(sign); - divisor = opposite(divisor); - } - long origin_dividend = dividend; - long origin_divisor = divisor; - - if (dividend < divisor) { - return 0; - } - - dividend -= divisor; - while (divisor <= dividend) { - ans = ans + ans; - divisor += divisor; - dividend -= divisor; - } - long a = ans + divide(origin_dividend - divisor, origin_divisor); - return sign > 0 ? a : opposite(a); -} - -public long opposite(long x) { - return ~x + 1; -} -``` - -# 解法二 - -也没有想到其他的解法,分享一个 [官方](https://leetcode.com/problems/product-of-array-except-self/solution/) 给的解法。 - -只要看到这张图,应该就明白算法了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/238_2.png) - -白色部分表示当前准备求解的位置,其结果就是粉色部分数字的累乘再乘上黄色部分数字的累乘。换句话说,其实就是左右两部分的累乘结果再相乘 - -所以我们只需要把所有粉色结果和黄色结果提前保存起来,然后可以直接去计算结果了。 - -假设数组个数是 `n`。 - -我们用 `left[i]`存储下标是 `0 ~ i - 1` 的数累乘的结果。 - -用 `right[i]` 存储下标是 `i + 1 ~ n - 1` 的数累乘的结果。 - -那么 `res[i] = left[i] * right[i]`。 - -至于边界情况,我们把 `left[0]` 和 `right[n - 1]`初始化为 `1` 即可。 - -```java -public int[] productExceptSelf(int[] nums) { - int n = nums.length; - int left[] = new int[n]; - left[0] = 1; - for (int i = 1; i < n; i++) { - left[i] = left[i - 1] * nums[i - 1]; - } - int right[] = new int[n]; - right[n - 1] = 1; - for (int i = n - 2; i >= 0; i--) { - right[i] = right[i + 1] * nums[i + 1]; - } - - int res[] = new int[n]; - for (int i = 0; i < n; i++) { - res[i] = left[i] * right[i]; - } - return res; -} -``` - -当然,我们可以省去 `left` 和 `right` 数组,先用 `res` 存储 `left` 数组的结果,然后边更新 `right` ,边和 `res` 相乘。 - -```java -public int[] productExceptSelf(int[] nums) { - int n = nums.length; - int res[] = new int[n]; - res[0] = 1; - for (int i = 1; i < n; i++) { - res[i] = res[i - 1] * nums[i - 1]; - } - int right = 1; - for (int i = n - 2; i >= 0; i--) { - right = right * nums[i + 1]; - res[i] = res[i] * right; - } - return res; -} -``` - -# 总 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/238.jpg) + +返回一个数组,第 `i` 个位置存储原数组除了第 `i` 个数以外的所有数的乘积。 + +# 解法一 + +最直接的想法就是先把所有的数乘起来,然后对于需要返回的数组的第 `i` 个位置,只需要将所有数的累乘结果除以第 `i` 个数即可。 + +如果所有数的累乘结果记为 `mul`,返回的数组用 `res` 存储,原数组用 `nums` 存储,那么 + + `res[i] = mul / nums[i]`。 + +除法的话需要考虑除零的问题,如果 `nums` 中有一个 `0`,那么 `res` 除了 `0` 的那个位置的结果是其余数累乘,其余位置的结果就都是 `0` 了。 + +如果 `nums` 中 `0`的个数超过一个,那么 `res` 所有结果就都是 `0` 了。 + +```java +public int[] productExceptSelf(int[] nums) { + int mul = 1; + int zeroNums = 0; + int zeroFirst = -1; + for (int i = 0; i < nums.length; i++) { + if (nums[i] == 0) { + zeroNums++; + if (zeroNums == 1) { + zeroFirst = i; + } + continue; + } + mul *= nums[i]; + } + int[] res = new int[nums.length]; + if (zeroNums > 1) { + return res; + }else if(zeroNums == 1){ + res[zeroFirst] = mul; + return res; + } + + for (int i = 0; i < nums.length; i++) { + res[i] = mul / nums[i]; + } + return res; +} +``` + +当然了题目中说不能用除法,恰巧在 [29 题](https://leetcode.wang/leetCode-29-Divide-Two-Integers.html) 我们用加减法实现过除法,这里的话就可以直接调用了。 + +```java +public int[] productExceptSelf(int[] nums) { + int mul = 1; + int zeroNums = 0; + int zeroFirst = -1; + for (int i = 0; i < nums.length; i++) { + if (nums[i] == 0) { + zeroNums++; + if (zeroNums == 1) { + zeroFirst = i; + } + continue; + } + mul *= nums[i]; + } + int[] res = new int[nums.length]; + if (zeroNums > 1) { + return res; + } else if (zeroNums == 1) { + res[zeroFirst] = mul; + return res; + } + for (int i = 0; i < nums.length; i++) { + res[i] = divide(mul, nums[i]); + } + return res; +} + +//下边是 29 题实现的除法 +public int divide(int dividend, int divisor) { + long ans = divide((long) dividend, (long) (divisor)); + long m = 2147483648L; + if (ans == m) { + return Integer.MAX_VALUE; + } else { + return (int) ans; + } +} + +public long divide(long dividend, long divisor) { + long ans = 1; + long sign = 1; + if (dividend < 0) { + sign = opposite(sign); + dividend = opposite(dividend); + } + if (divisor < 0) { + sign = opposite(sign); + divisor = opposite(divisor); + } + long origin_dividend = dividend; + long origin_divisor = divisor; + + if (dividend < divisor) { + return 0; + } + + dividend -= divisor; + while (divisor <= dividend) { + ans = ans + ans; + divisor += divisor; + dividend -= divisor; + } + long a = ans + divide(origin_dividend - divisor, origin_divisor); + return sign > 0 ? a : opposite(a); +} + +public long opposite(long x) { + return ~x + 1; +} +``` + +# 解法二 + +也没有想到其他的解法,分享一个 [官方](https://leetcode.com/problems/product-of-array-except-self/solution/) 给的解法。 + +只要看到这张图,应该就明白算法了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/238_2.png) + +白色部分表示当前准备求解的位置,其结果就是粉色部分数字的累乘再乘上黄色部分数字的累乘。换句话说,其实就是左右两部分的累乘结果再相乘 + +所以我们只需要把所有粉色结果和黄色结果提前保存起来,然后可以直接去计算结果了。 + +假设数组个数是 `n`。 + +我们用 `left[i]`存储下标是 `0 ~ i - 1` 的数累乘的结果。 + +用 `right[i]` 存储下标是 `i + 1 ~ n - 1` 的数累乘的结果。 + +那么 `res[i] = left[i] * right[i]`。 + +至于边界情况,我们把 `left[0]` 和 `right[n - 1]`初始化为 `1` 即可。 + +```java +public int[] productExceptSelf(int[] nums) { + int n = nums.length; + int left[] = new int[n]; + left[0] = 1; + for (int i = 1; i < n; i++) { + left[i] = left[i - 1] * nums[i - 1]; + } + int right[] = new int[n]; + right[n - 1] = 1; + for (int i = n - 2; i >= 0; i--) { + right[i] = right[i + 1] * nums[i + 1]; + } + + int res[] = new int[n]; + for (int i = 0; i < n; i++) { + res[i] = left[i] * right[i]; + } + return res; +} +``` + +当然,我们可以省去 `left` 和 `right` 数组,先用 `res` 存储 `left` 数组的结果,然后边更新 `right` ,边和 `res` 相乘。 + +```java +public int[] productExceptSelf(int[] nums) { + int n = nums.length; + int res[] = new int[n]; + res[0] = 1; + for (int i = 1; i < n; i++) { + res[i] = res[i - 1] * nums[i - 1]; + } + int right = 1; + for (int i = n - 2; i >= 0; i--) { + right = right * nums[i + 1]; + res[i] = res[i] * right; + } + return res; +} +``` + +# 总 + 解法二应该是出题人的意思,关键就是认识到那个图,有种分类的意思,把其余的数分成了左半部分和右半部分。解法一的话有种作弊的感觉,哈哈。 \ No newline at end of file diff --git a/leetcode-239-Sliding-Window-Maximum.md b/leetcode-239-Sliding-Window-Maximum.md index db384c57b..a818b8e81 100644 --- a/leetcode-239-Sliding-Window-Maximum.md +++ b/leetcode-239-Sliding-Window-Maximum.md @@ -1,278 +1,278 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/239.png) - -输出每个窗口内的最大值。 - -# 解法一 暴力 - -两层 `for` 循环,每次都从窗口中找最大值即可。 - -```java -public int[] maxSlidingWindow(int[] nums, int k) { - int n = nums.length; - if (n == 0) { - return nums; - } - int result[] = new int[n - k + 1]; - for (int i = 0; i < result.length; i++) { - int max = Integer.MIN_VALUE; - for (int j = 0; j < k; j++) { - max = Math.max(max, nums[i + j]); - } - result[i] = max; - } - return result; -} -``` - -时间复杂度的话是 `O(nk)`。 - -# 解法二优先队列 - -注意到我们每次循环都寻找最大的值,自然的可以想到优先队列,这样得到最大值就是 `O(1)` 了。 - -当优先队列的数字等于窗口大小的时候,我们只需要将第一个元素删除,然后将新的数字加入。 - -```java -public int[] maxSlidingWindow(int[] nums, int k) { - // 建立最大堆 - Queue max = new PriorityQueue(new Comparator() { - @Override - public int compare(Integer i1, Integer i2) { - // TODO Auto-generated method stub - return i2 - i1; - } - }); - int n = nums.length; - if (n == 0) { - return nums; - } - int result[] = new int[n - k + 1]; - int index = 0; - for (int i = 0; i < n; i++) { - //移除第一个元素 - if (max.size() >= k) { - max.remove(nums[i - k]); - } - max.offer(nums[i]); - //更新 result - if (i >= k - 1) { - result[index++] = max.peek(); - } - } - return result; -} -``` - -时间复杂度的话,循环中主要是调用了 `remove` 函数和 `offer` 函数,虽然 `offer` 函数的时间复杂度是 `log` 级的,但是 `remove` 是 `O(k)` ,所以最终的时间复杂度依旧是 `O(nk)`。 - -和 [218 题](https://leetcode.wang/leetcode-218-The-Skyline-Problem.html) 一样,我们可以用 `TreeMap` 代替优先队列,这样删除的时间复杂度也就是 `log` 级了。 - -```java -public int[] maxSlidingWindow(int[] nums, int k) { - TreeMap treeMap = new TreeMap<>(new Comparator() { - @Override - public int compare(Integer i1, Integer i2) { - return i2 - i1; - } - }); - int n = nums.length; - if (n == 0) { - return nums; - } - int result[] = new int[n - k + 1]; - int index = 0; - for (int i = 0; i < n; i++) { - //此时不能用 treeMap 的大小和 k 比较, 因为 nums 中有相等的元素 - //当 i >= k 的时候每次都需要删除一个元素 - if (i >= k) { - Integer v = treeMap.get(nums[i - k]); - if (v == 1) { - treeMap.remove(nums[i - k]); - } else { - treeMap.put(nums[i - k], v - 1); - } - } - //添加元素 - Integer v = treeMap.get(nums[i]); - if (v == null) { - treeMap.put(nums[i], 1); - } else { - treeMap.put(nums[i], v + 1); - } - //更新 result - if (i >= k - 1) { - result[index++] = treeMap.firstKey(); - } - } - return result; -} -``` - -此时时间复杂度就是 `O(nlog(k))` 了。 - -# 解法三 单调队列 - -参考 [这里](https://leetcode.com/problems/sliding-window-maximum/discuss/65884/Java-O(n)-solution-using-deque-with-explanation)。 - -我们可以用一个单调递减队列。单调递减队列添加元素的算法如下。 - -如果当前元素比队列的最后一个元素大,那么就将最后一个元素出队,重复这步直到当前元素小于队列的最后一个元素或者队列为空。进行下一步。 - -如果当前元素小于等于队列的最后一个元素或者队列为空,那么就直接将当前元素入队。 - -按照上边的方法添加元素,队列中的元素就刚好是一个单调递减的序列,而最大值就刚好是队头的元素。 - -当队列的元素等于窗口的大小的时候,由于添加元素的时候我们进行了出队操作,所以我们不能像解法二那样每次都删除第一个元素,需要先判断一下队头元素是否是我们要删除的元素。 - -```java -public int[] maxSlidingWindow(int[] nums, int k) { - Deque max = new ArrayDeque<>(); - int n = nums.length; - if (n == 0) { - return nums; - } - int result[] = new int[n - k + 1]; - int index = 0; - for (int i = 0; i < n; i++) { - if (i >= k) { - if (max.peekFirst() == nums[i - k]) { - max.removeFirst(); - } - } - while (!max.isEmpty() && nums[i] > max.peekLast()) { - max.removeLast(); - } - - max.addLast(nums[i]); - if (i >= k - 1) { - result[index++] = max.peekFirst(); - } - } - return result; -} -``` - -时间复杂度就是 `O(n)`了,因为每个元素最多只会添加到队列和从队列删除两次操作。 - -# 解法四 - -参考 [这里](https://leetcode.com/problems/sliding-window-maximum/discuss/65881/O(n)-solution-in-Java-with-two-simple-pass-in-the-array) ,一种神奇的解法,有点 [238 题](https://leetcode.wang/leetcode-238-Product-of-Array-Except-Self.html) 解法二的影子。 - -我们把数组 `k` 个一组进行分组。 - -``` java -nums = [1,3,-1,-3,5,3,6,7], and k = 3 - -| 1 3 -1 | -5 4 3 | 5 7 | - -如果我们要求 result[1],也就是下边 i 到 j 范围内的数字的最大值 - -| 1 3 -1 | -5 4 3 | 5 7 | - ^ ^ - i j -i 到 j 范围内的数字被分割线分成了两部分 -``` - -如果我们知道了左半部的最大值和右半部分的最大值,那么两个选最大的即可。 - -左半部分的最大值,也就是当前数到它右边界范围内的最大值。 - -用 `rightMax[i]` 保存 `i` 到它的右边界范围内的最大值,只需要从右到左遍历一遍就可以求出来了。 - -每次到右边界的时候就开始重新计算 `max` 值。 - -```java -int rightMax[] = new int[n]; -max = Integer.MIN_VALUE; -for (int i = n - 1; i >= 0; i--) { - if (max < nums[i]) { - max = nums[i]; - } - rightMax[i] = Math.max(nums[i], max); - if (i % k == 0) { - max = Integer.MIN_VALUE; - } -} -``` - -同理,右半部分的最大值,也就是当前数到它左边界范围内的最大值。 - -用 `leftMax[i]` 保存 `i` 到它的左边界范围内的最大值,只需要从左到右遍历一遍就可以求出来。 - -每次到左边界的时候就开始重新计算 `max` 值。 - -```java -int leftMax[] = new int[n]; -int max = Integer.MIN_VALUE; -for (int i = 0; i < n; i++) { - if (i % k == 0) { - max = Integer.MIN_VALUE; - } - if (max < nums[i]) { - max = nums[i]; - } - leftMax[i] = Math.max(nums[i], max); -} -``` - -有了上边的两个数组,当前范围的两个边界 `i` 和 `j`,`rightMax[i]` 存储的就是左半部分(`i` 到右边界)的最大值,`leftMax[j]` 存储的就是右半部分(`j` 到左边界)的最大值。`result[i]` 的结果也就出来了。 - -```java -result[i] = Math.max(rightMax[i], leftMax[j]); -``` - -代码的话,把上边的部分结合起来即可。 - -```java -public int[] maxSlidingWindow(int[] nums, int k) { - int n = nums.length; - if (n == 0) { - return nums; - } - - //当前数到自己的左边界的最大值 - int leftMax[] = new int[n]; - int max = Integer.MIN_VALUE; - for (int i = 0; i < n; i++) { - if (i % k == 0) { - max = Integer.MIN_VALUE; - } - if (max < nums[i]) { - max = nums[i]; - } - leftMax[i] = Math.max(nums[i], max); - } - - //当前数到自己的右边界的最大值 - int rightMax[] = new int[n]; - max = Integer.MIN_VALUE; - for (int i = n - 1; i >= 0; i--) { - if (max < nums[i]) { - max = nums[i]; - } - rightMax[i] = Math.max(nums[i], max); - if (i % k == 0) { - max = Integer.MIN_VALUE; - } - } - - int result[] = new int[n - k + 1]; - for (int i = 0; i < result.length; i++) { - int j = i + k - 1; - result[i] = Math.max(rightMax[i], leftMax[j]); - } - return result; -} -``` - -时间复杂度和解法三一样是 `O(n)`。 - -# 总 - -解法一和解法二的话是正常的思路,一步一步水到渠成。 - -解法三的话直觉上其实也能意识到,我开始想到了单调栈,但一开始代码写错了,然后就换思路了,有点可惜,如果单调栈写对的话,应该可以写出解法三。 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/239.png) + +输出每个窗口内的最大值。 + +# 解法一 暴力 + +两层 `for` 循环,每次都从窗口中找最大值即可。 + +```java +public int[] maxSlidingWindow(int[] nums, int k) { + int n = nums.length; + if (n == 0) { + return nums; + } + int result[] = new int[n - k + 1]; + for (int i = 0; i < result.length; i++) { + int max = Integer.MIN_VALUE; + for (int j = 0; j < k; j++) { + max = Math.max(max, nums[i + j]); + } + result[i] = max; + } + return result; +} +``` + +时间复杂度的话是 `O(nk)`。 + +# 解法二优先队列 + +注意到我们每次循环都寻找最大的值,自然的可以想到优先队列,这样得到最大值就是 `O(1)` 了。 + +当优先队列的数字等于窗口大小的时候,我们只需要将第一个元素删除,然后将新的数字加入。 + +```java +public int[] maxSlidingWindow(int[] nums, int k) { + // 建立最大堆 + Queue max = new PriorityQueue(new Comparator() { + @Override + public int compare(Integer i1, Integer i2) { + // TODO Auto-generated method stub + return i2 - i1; + } + }); + int n = nums.length; + if (n == 0) { + return nums; + } + int result[] = new int[n - k + 1]; + int index = 0; + for (int i = 0; i < n; i++) { + //移除第一个元素 + if (max.size() >= k) { + max.remove(nums[i - k]); + } + max.offer(nums[i]); + //更新 result + if (i >= k - 1) { + result[index++] = max.peek(); + } + } + return result; +} +``` + +时间复杂度的话,循环中主要是调用了 `remove` 函数和 `offer` 函数,虽然 `offer` 函数的时间复杂度是 `log` 级的,但是 `remove` 是 `O(k)` ,所以最终的时间复杂度依旧是 `O(nk)`。 + +和 [218 题](https://leetcode.wang/leetcode-218-The-Skyline-Problem.html) 一样,我们可以用 `TreeMap` 代替优先队列,这样删除的时间复杂度也就是 `log` 级了。 + +```java +public int[] maxSlidingWindow(int[] nums, int k) { + TreeMap treeMap = new TreeMap<>(new Comparator() { + @Override + public int compare(Integer i1, Integer i2) { + return i2 - i1; + } + }); + int n = nums.length; + if (n == 0) { + return nums; + } + int result[] = new int[n - k + 1]; + int index = 0; + for (int i = 0; i < n; i++) { + //此时不能用 treeMap 的大小和 k 比较, 因为 nums 中有相等的元素 + //当 i >= k 的时候每次都需要删除一个元素 + if (i >= k) { + Integer v = treeMap.get(nums[i - k]); + if (v == 1) { + treeMap.remove(nums[i - k]); + } else { + treeMap.put(nums[i - k], v - 1); + } + } + //添加元素 + Integer v = treeMap.get(nums[i]); + if (v == null) { + treeMap.put(nums[i], 1); + } else { + treeMap.put(nums[i], v + 1); + } + //更新 result + if (i >= k - 1) { + result[index++] = treeMap.firstKey(); + } + } + return result; +} +``` + +此时时间复杂度就是 `O(nlog(k))` 了。 + +# 解法三 单调队列 + +参考 [这里](https://leetcode.com/problems/sliding-window-maximum/discuss/65884/Java-O(n)-solution-using-deque-with-explanation)。 + +我们可以用一个单调递减队列。单调递减队列添加元素的算法如下。 + +如果当前元素比队列的最后一个元素大,那么就将最后一个元素出队,重复这步直到当前元素小于队列的最后一个元素或者队列为空。进行下一步。 + +如果当前元素小于等于队列的最后一个元素或者队列为空,那么就直接将当前元素入队。 + +按照上边的方法添加元素,队列中的元素就刚好是一个单调递减的序列,而最大值就刚好是队头的元素。 + +当队列的元素等于窗口的大小的时候,由于添加元素的时候我们进行了出队操作,所以我们不能像解法二那样每次都删除第一个元素,需要先判断一下队头元素是否是我们要删除的元素。 + +```java +public int[] maxSlidingWindow(int[] nums, int k) { + Deque max = new ArrayDeque<>(); + int n = nums.length; + if (n == 0) { + return nums; + } + int result[] = new int[n - k + 1]; + int index = 0; + for (int i = 0; i < n; i++) { + if (i >= k) { + if (max.peekFirst() == nums[i - k]) { + max.removeFirst(); + } + } + while (!max.isEmpty() && nums[i] > max.peekLast()) { + max.removeLast(); + } + + max.addLast(nums[i]); + if (i >= k - 1) { + result[index++] = max.peekFirst(); + } + } + return result; +} +``` + +时间复杂度就是 `O(n)`了,因为每个元素最多只会添加到队列和从队列删除两次操作。 + +# 解法四 + +参考 [这里](https://leetcode.com/problems/sliding-window-maximum/discuss/65881/O(n)-solution-in-Java-with-two-simple-pass-in-the-array) ,一种神奇的解法,有点 [238 题](https://leetcode.wang/leetcode-238-Product-of-Array-Except-Self.html) 解法二的影子。 + +我们把数组 `k` 个一组进行分组。 + +``` java +nums = [1,3,-1,-3,5,3,6,7], and k = 3 + +| 1 3 -1 | -5 4 3 | 5 7 | + +如果我们要求 result[1],也就是下边 i 到 j 范围内的数字的最大值 + +| 1 3 -1 | -5 4 3 | 5 7 | + ^ ^ + i j +i 到 j 范围内的数字被分割线分成了两部分 +``` + +如果我们知道了左半部的最大值和右半部分的最大值,那么两个选最大的即可。 + +左半部分的最大值,也就是当前数到它右边界范围内的最大值。 + +用 `rightMax[i]` 保存 `i` 到它的右边界范围内的最大值,只需要从右到左遍历一遍就可以求出来了。 + +每次到右边界的时候就开始重新计算 `max` 值。 + +```java +int rightMax[] = new int[n]; +max = Integer.MIN_VALUE; +for (int i = n - 1; i >= 0; i--) { + if (max < nums[i]) { + max = nums[i]; + } + rightMax[i] = Math.max(nums[i], max); + if (i % k == 0) { + max = Integer.MIN_VALUE; + } +} +``` + +同理,右半部分的最大值,也就是当前数到它左边界范围内的最大值。 + +用 `leftMax[i]` 保存 `i` 到它的左边界范围内的最大值,只需要从左到右遍历一遍就可以求出来。 + +每次到左边界的时候就开始重新计算 `max` 值。 + +```java +int leftMax[] = new int[n]; +int max = Integer.MIN_VALUE; +for (int i = 0; i < n; i++) { + if (i % k == 0) { + max = Integer.MIN_VALUE; + } + if (max < nums[i]) { + max = nums[i]; + } + leftMax[i] = Math.max(nums[i], max); +} +``` + +有了上边的两个数组,当前范围的两个边界 `i` 和 `j`,`rightMax[i]` 存储的就是左半部分(`i` 到右边界)的最大值,`leftMax[j]` 存储的就是右半部分(`j` 到左边界)的最大值。`result[i]` 的结果也就出来了。 + +```java +result[i] = Math.max(rightMax[i], leftMax[j]); +``` + +代码的话,把上边的部分结合起来即可。 + +```java +public int[] maxSlidingWindow(int[] nums, int k) { + int n = nums.length; + if (n == 0) { + return nums; + } + + //当前数到自己的左边界的最大值 + int leftMax[] = new int[n]; + int max = Integer.MIN_VALUE; + for (int i = 0; i < n; i++) { + if (i % k == 0) { + max = Integer.MIN_VALUE; + } + if (max < nums[i]) { + max = nums[i]; + } + leftMax[i] = Math.max(nums[i], max); + } + + //当前数到自己的右边界的最大值 + int rightMax[] = new int[n]; + max = Integer.MIN_VALUE; + for (int i = n - 1; i >= 0; i--) { + if (max < nums[i]) { + max = nums[i]; + } + rightMax[i] = Math.max(nums[i], max); + if (i % k == 0) { + max = Integer.MIN_VALUE; + } + } + + int result[] = new int[n - k + 1]; + for (int i = 0; i < result.length; i++) { + int j = i + k - 1; + result[i] = Math.max(rightMax[i], leftMax[j]); + } + return result; +} +``` + +时间复杂度和解法三一样是 `O(n)`。 + +# 总 + +解法一和解法二的话是正常的思路,一步一步水到渠成。 + +解法三的话直觉上其实也能意识到,我开始想到了单调栈,但一开始代码写错了,然后就换思路了,有点可惜,如果单调栈写对的话,应该可以写出解法三。 + 解法四的话就很厉害了,一般情况下很少往那方面想,不过这种将解分割的思想也是常用的。 \ No newline at end of file diff --git a/leetcode-240-Search-a-2D-MatrixII.md b/leetcode-240-Search-a-2D-MatrixII.md index 8cbf21cf4..3dbc00c29 100644 --- a/leetcode-240-Search-a-2D-MatrixII.md +++ b/leetcode-240-Search-a-2D-MatrixII.md @@ -1,229 +1,229 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/240.jpg) - -矩阵的每行从左到右是升序, 每列从上到下也是升序,在矩阵中查找某个数。 - -# 解法一 - -看到有序,第一反应就是二分查找。最直接的做法,一行一行的进行二分查找即可。 - -此外,结合有序的性质,一些情况可以提前结束。 - -比如某一行的第一个元素大于了 `target` ,当前行和后边的所有行都不用考虑了,直接返回 `false`。 - -某一行的最后一个元素小于了 `target` ,当前行就不用考虑了,换下一行。 - -```java -public boolean searchMatrix(int[][] matrix, int target) { - if (matrix.length == 0 || matrix[0].length == 0) { - return false; - } - for (int i = 0; i < matrix.length; i++) { - if (matrix[i][0] > target) { - break; - } - if(matrix[i][matrix[i].length - 1] < target){ - continue; - } - int col = binarySearch(matrix[i], target); - if (col != -1) { - return true; - } - } - return false; -} - -//二分查找 -private int binarySearch(int[] nums, int target) { - int start = 0; - int end = nums.length - 1; - while (start <= end) { - int mid = (start + end) >>> 1; - if (nums[mid] == target) { - return mid; - } else if (nums[mid] < target) { - start = mid + 1; - } else { - end = mid - 1; - } - } - return -1; -} -``` - -时间复杂度的话,如果是 `m` 行 `n` 列,就是 `O(mlog(n))`。 - -# 解法二 - -参考 [这里](https://leetcode.com/problems/search-a-2d-matrix-ii/discuss/66140/My-concise-O(m%2Bn)-Java-solution),需要很敏锐的观察力了。 - -数组从左到右和从上到下都是升序的,如果从右上角出发开始遍历呢? - -会发现每次都是向左数字会变小,向下数字会变大,有点和二分查找树相似。二分查找树的话,是向左数字变小,向右数字变大。 - -所以我们可以把 `target` 和当前值比较。 - -* 如果 `target` 的值大于当前值,那么就向下走。 -* 如果 `target` 的值小于当前值,那么就向左走。 -* 如果相等的话,直接返回 `true` 。 - -也可以换个角度思考。 - -如果 `target` 的值小于当前值,也就意味着当前值所在的列肯定不会存在 `target` 了,可以把当前列去掉,从新的右上角的值开始遍历。 - -同理,如果 `target` 的值大于当前值,也就意味着当前值所在的行肯定不会存在 `target` 了,可以把当前行去掉,从新的右上角的值开始遍历。 - -看下边的例子。 - -```java -[1, 4, 7, 11, 15], -[2, 5, 8, 12, 19], -[3, 6, 9, 16, 22], -[10, 13, 14, 17, 24], -[18, 21, 23, 26, 30] - -如果 target = 9,如果我们从 15 开始遍历, cur = 15 - -target < 15, 去掉当前列, cur = 11 -[1, 4, 7, 11], -[2, 5, 8, 12], -[3, 6, 9, 16], -[10, 13, 14, 17], -[18, 21, 23, 26] - -target < 11, 去掉当前列, cur = 7 -[1, 4, 7], -[2, 5, 8], -[3, 6, 9], -[10, 13, 14], -[18, 21, 23] - -target > 7, 去掉当前行, cur = 8 -[2, 5, 8], -[3, 6, 9], -[10, 13, 14], -[18, 21, 23] - -target > 8, 去掉当前行, cur = 9, 遍历结束 -[3, 6, 9], -[10, 13, 14], -[18, 21, 23] -``` - -不管从哪种角度考虑,代码的话都是一样的。 - -```java -public boolean searchMatrix(int[][] matrix, int target) { - if (matrix.length == 0 || matrix[0].length == 0) { - return false; - } - int row = 0; - int col = matrix[0].length - 1; - while (row < matrix.length && col >= 0) { - if (target > matrix[row][col]) { - row++; - } else if (target < matrix[row][col]) { - col--; - } else { - return true; - } - } - return false; -} -``` - -时间复杂度就是每个节点最多遍历一遍了,`O(m + n)`。 - -# 解法三 - -参考 [这里](https://leetcode.com/problems/search-a-2d-matrix-ii/discuss/66147/*Java*-an-easy-to-understand-divide-and-conquer-method) ,还有一种解法。 - -我的理解的话,算是一种变形的二分法。 - -二分法的思想就是,目标值和中点值进行比较,然后可以丢弃一半的元素。 - -这道题的话是矩阵,如果我们找到矩阵的中心,然后和目标值比较看能不能丢弃一些元素。 - -```java -如下图,中心位置是 9 -[1, 4, 7, 11, 15], -[2, 5, 8, 12, 19], -[3, 6, /9/,16, 22], -[10, 13, 14, 17, 24], -[18, 21, 23, 26, 30] - -通过中心位置, 我们可以把原矩形分成四个矩形, 左上, 右上, 左下, 右下 -[1, 4, 7 [11, 15 - 2, 5, 8 12, 19 - 3, 6, /9/] 16, 22] - -[10, 13, 14 [17, 24 -[18, 21, 23] 26, 30] - -如果 target = 10, -此时中心值小于目标值,左上角矩形中所有的数都小于目标值,我们可以丢弃左上角的矩形,继续从剩下三个矩形中寻找 - -如果 target = 5, -此时中心值大于目标值,右下角矩形中所有的数都大于目标值,那么我们可以丢弃右下角的矩形,继续从剩下三个矩形中寻找 -``` - -我们找到了丢弃元素的原则,可以写代码了。 - -这里的话,矩形我们用左上角和右下角坐标的形式表示,下图是分割后矩形的坐标情况。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/240_2.jpg) - -我们可以用递归的形式去写,递归出口的话,当矩阵中只有一个元素,直接判断当前元素是不是目标值即可。 - -还有就是分割的时候可能越界,比如原矩阵只有一行,左下角和右下角的矩阵其实是不存在的,按照上边的坐标公式计算出来后,我们要判断一下是否越界。 - -```java -public boolean searchMatrix(int[][] matrix, int target) { - if (matrix.length == 0 || matrix[0].length == 0) { - return false; - } - return searchMatrixHelper(matrix, 0, 0, matrix[0].length - 1, matrix.length - 1, matrix[0].length - 1, matrix.length - 1, target); -} - -private boolean searchMatrixHelper(int[][] matrix, int x1, int y1, int x2, int y2, int xMax, int yMax, int target) { - //只需要判断左上角坐标即可 - if (x1 > xMax || y1 > yMax) { - return false; - } - - //x 轴代表的是列,y 轴代表的是行 - if(x1 == x2 && y1 == y2){ - return matrix[y1][x1] == target; - } - int m1 = (x1 + x2) >>> 1; - int m2 = (y1 + y2) >>> 1; - if (matrix[m2][m1] == target) { - return true; - } - if (matrix[m2][m1] < target) { - // 右上矩阵 - return searchMatrixHelper(matrix, m1 + 1, y1, x2, m2, x2, y2, target) || - // 左下矩阵 - searchMatrixHelper(matrix, x1, m2 + 1, m1, y2, x2, y2, target) || - // 右下矩阵 - searchMatrixHelper(matrix, m1 + 1, m2 + 1, x2, y2, x2, y2, target); - - } else { - // 右上矩阵 - return searchMatrixHelper(matrix, m1 + 1, y1, x2, m2, x2, y2, target) || - // 左下矩阵 - searchMatrixHelper(matrix, x1, m2 + 1, m1, y2, x2, y2, target) || - // 左上矩阵 - searchMatrixHelper(matrix, x1, y1, m1, m2, x2, y2, target); - } -} -``` - -# 总 - -看到有序数组第一反应就是二分了,也就是解法一。 - -解法二的话,从右上角开始遍历的想法很妙。 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/240.jpg) + +矩阵的每行从左到右是升序, 每列从上到下也是升序,在矩阵中查找某个数。 + +# 解法一 + +看到有序,第一反应就是二分查找。最直接的做法,一行一行的进行二分查找即可。 + +此外,结合有序的性质,一些情况可以提前结束。 + +比如某一行的第一个元素大于了 `target` ,当前行和后边的所有行都不用考虑了,直接返回 `false`。 + +某一行的最后一个元素小于了 `target` ,当前行就不用考虑了,换下一行。 + +```java +public boolean searchMatrix(int[][] matrix, int target) { + if (matrix.length == 0 || matrix[0].length == 0) { + return false; + } + for (int i = 0; i < matrix.length; i++) { + if (matrix[i][0] > target) { + break; + } + if(matrix[i][matrix[i].length - 1] < target){ + continue; + } + int col = binarySearch(matrix[i], target); + if (col != -1) { + return true; + } + } + return false; +} + +//二分查找 +private int binarySearch(int[] nums, int target) { + int start = 0; + int end = nums.length - 1; + while (start <= end) { + int mid = (start + end) >>> 1; + if (nums[mid] == target) { + return mid; + } else if (nums[mid] < target) { + start = mid + 1; + } else { + end = mid - 1; + } + } + return -1; +} +``` + +时间复杂度的话,如果是 `m` 行 `n` 列,就是 `O(mlog(n))`。 + +# 解法二 + +参考 [这里](https://leetcode.com/problems/search-a-2d-matrix-ii/discuss/66140/My-concise-O(m%2Bn)-Java-solution),需要很敏锐的观察力了。 + +数组从左到右和从上到下都是升序的,如果从右上角出发开始遍历呢? + +会发现每次都是向左数字会变小,向下数字会变大,有点和二分查找树相似。二分查找树的话,是向左数字变小,向右数字变大。 + +所以我们可以把 `target` 和当前值比较。 + +* 如果 `target` 的值大于当前值,那么就向下走。 +* 如果 `target` 的值小于当前值,那么就向左走。 +* 如果相等的话,直接返回 `true` 。 + +也可以换个角度思考。 + +如果 `target` 的值小于当前值,也就意味着当前值所在的列肯定不会存在 `target` 了,可以把当前列去掉,从新的右上角的值开始遍历。 + +同理,如果 `target` 的值大于当前值,也就意味着当前值所在的行肯定不会存在 `target` 了,可以把当前行去掉,从新的右上角的值开始遍历。 + +看下边的例子。 + +```java +[1, 4, 7, 11, 15], +[2, 5, 8, 12, 19], +[3, 6, 9, 16, 22], +[10, 13, 14, 17, 24], +[18, 21, 23, 26, 30] + +如果 target = 9,如果我们从 15 开始遍历, cur = 15 + +target < 15, 去掉当前列, cur = 11 +[1, 4, 7, 11], +[2, 5, 8, 12], +[3, 6, 9, 16], +[10, 13, 14, 17], +[18, 21, 23, 26] + +target < 11, 去掉当前列, cur = 7 +[1, 4, 7], +[2, 5, 8], +[3, 6, 9], +[10, 13, 14], +[18, 21, 23] + +target > 7, 去掉当前行, cur = 8 +[2, 5, 8], +[3, 6, 9], +[10, 13, 14], +[18, 21, 23] + +target > 8, 去掉当前行, cur = 9, 遍历结束 +[3, 6, 9], +[10, 13, 14], +[18, 21, 23] +``` + +不管从哪种角度考虑,代码的话都是一样的。 + +```java +public boolean searchMatrix(int[][] matrix, int target) { + if (matrix.length == 0 || matrix[0].length == 0) { + return false; + } + int row = 0; + int col = matrix[0].length - 1; + while (row < matrix.length && col >= 0) { + if (target > matrix[row][col]) { + row++; + } else if (target < matrix[row][col]) { + col--; + } else { + return true; + } + } + return false; +} +``` + +时间复杂度就是每个节点最多遍历一遍了,`O(m + n)`。 + +# 解法三 + +参考 [这里](https://leetcode.com/problems/search-a-2d-matrix-ii/discuss/66147/*Java*-an-easy-to-understand-divide-and-conquer-method) ,还有一种解法。 + +我的理解的话,算是一种变形的二分法。 + +二分法的思想就是,目标值和中点值进行比较,然后可以丢弃一半的元素。 + +这道题的话是矩阵,如果我们找到矩阵的中心,然后和目标值比较看能不能丢弃一些元素。 + +```java +如下图,中心位置是 9 +[1, 4, 7, 11, 15], +[2, 5, 8, 12, 19], +[3, 6, /9/,16, 22], +[10, 13, 14, 17, 24], +[18, 21, 23, 26, 30] + +通过中心位置, 我们可以把原矩形分成四个矩形, 左上, 右上, 左下, 右下 +[1, 4, 7 [11, 15 + 2, 5, 8 12, 19 + 3, 6, /9/] 16, 22] + +[10, 13, 14 [17, 24 +[18, 21, 23] 26, 30] + +如果 target = 10, +此时中心值小于目标值,左上角矩形中所有的数都小于目标值,我们可以丢弃左上角的矩形,继续从剩下三个矩形中寻找 + +如果 target = 5, +此时中心值大于目标值,右下角矩形中所有的数都大于目标值,那么我们可以丢弃右下角的矩形,继续从剩下三个矩形中寻找 +``` + +我们找到了丢弃元素的原则,可以写代码了。 + +这里的话,矩形我们用左上角和右下角坐标的形式表示,下图是分割后矩形的坐标情况。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/240_2.jpg) + +我们可以用递归的形式去写,递归出口的话,当矩阵中只有一个元素,直接判断当前元素是不是目标值即可。 + +还有就是分割的时候可能越界,比如原矩阵只有一行,左下角和右下角的矩阵其实是不存在的,按照上边的坐标公式计算出来后,我们要判断一下是否越界。 + +```java +public boolean searchMatrix(int[][] matrix, int target) { + if (matrix.length == 0 || matrix[0].length == 0) { + return false; + } + return searchMatrixHelper(matrix, 0, 0, matrix[0].length - 1, matrix.length - 1, matrix[0].length - 1, matrix.length - 1, target); +} + +private boolean searchMatrixHelper(int[][] matrix, int x1, int y1, int x2, int y2, int xMax, int yMax, int target) { + //只需要判断左上角坐标即可 + if (x1 > xMax || y1 > yMax) { + return false; + } + + //x 轴代表的是列,y 轴代表的是行 + if(x1 == x2 && y1 == y2){ + return matrix[y1][x1] == target; + } + int m1 = (x1 + x2) >>> 1; + int m2 = (y1 + y2) >>> 1; + if (matrix[m2][m1] == target) { + return true; + } + if (matrix[m2][m1] < target) { + // 右上矩阵 + return searchMatrixHelper(matrix, m1 + 1, y1, x2, m2, x2, y2, target) || + // 左下矩阵 + searchMatrixHelper(matrix, x1, m2 + 1, m1, y2, x2, y2, target) || + // 右下矩阵 + searchMatrixHelper(matrix, m1 + 1, m2 + 1, x2, y2, x2, y2, target); + + } else { + // 右上矩阵 + return searchMatrixHelper(matrix, m1 + 1, y1, x2, m2, x2, y2, target) || + // 左下矩阵 + searchMatrixHelper(matrix, x1, m2 + 1, m1, y2, x2, y2, target) || + // 左上矩阵 + searchMatrixHelper(matrix, x1, y1, m1, m2, x2, y2, target); + } +} +``` + +# 总 + +看到有序数组第一反应就是二分了,也就是解法一。 + +解法二的话,从右上角开始遍历的想法很妙。 + 解法三的话思想很简单,就是变形的二分法,每次抛弃一部分元素,但代码的话其实写出来不是很容易,相对于解法一和解法二来说是有些复杂度的。 \ No newline at end of file diff --git a/leetcode-241-Different-Ways-to-Add-Parentheses.md b/leetcode-241-Different-Ways-to-Add-Parentheses.md index dd4816d35..db0fc2c2a 100644 --- a/leetcode-241-Different-Ways-to-Add-Parentheses.md +++ b/leetcode-241-Different-Ways-to-Add-Parentheses.md @@ -1,288 +1,288 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/241.png) - -给一个表达式加括号,得到所有不同情况的解。 - -# 解法一 递归 - -一看到题就觉得有点复杂,可以考虑一下递归的方式,去寻找子问题和原问题解的关系。 - -可以通过运算符把整个式子分成两部分,两部分再利用递归解决。 - -以 `2 * 3 - 4 * 5` 为例。 - -`2` 和 `3 - 4 * 5` 两部分,中间是 `*` 号相连。 - -`2 * 3` 和 `4 * 5` 两部分,中间是 `-` 号相连。 - -`2 * 3 - 4` 和 ` 5` 两部分,中间是 `*` 号相连。 - -有了两部分的结果,然后再通过中间的符号两两计算加入到最终的结果中即可。 - -比如第一种情况,`2` 和 `3 - 4 * 5` 两部分,中间是 `*` 号相连。 - -`2` 的解就是 `[2]`,`3 - 4 * 5` 的解就是 `[-5, -17]`。 - -把两部分解通过 `*` 号计算,最终结果就是 `[-10, -34]`。 - -另外两种情况也类似。 - -然后还需要递归出口。 - -如果给定的字符串只有数字,没有运算符,那结果就是给定的字符串转为数字。 - -比如上边的第一种情况,`2` 的解就是 `[2]`。 - -```java -public List diffWaysToCompute(String input) { - if (input.length() == 0) { - return new ArrayList<>(); - } - List result = new ArrayList<>(); - int num = 0; - //考虑是全数字的情况 - int index = 0; - while (index < input.length() && !isOperation(input.charAt(index))) { - num = num * 10 + input.charAt(index) - '0'; - index++; - } - //将全数字的情况直接返回 - if (index == input.length()) { - result.add(num); - return result; - } - - for (int i = 0; i < input.length(); i++) { - //通过运算符将字符串分成两部分 - if (isOperation(input.charAt(i))) { - List result1 = diffWaysToCompute(input.substring(0, i)); - List result2 = diffWaysToCompute(input.substring(i + 1)); - //将两个结果依次运算 - for (int j = 0; j < result1.size(); j++) { - for (int k = 0; k < result2.size(); k++) { - char op = input.charAt(i); - result.add(caculate(result1.get(j), op, result2.get(k))); - } - } - } - } - return result; -} - -private int caculate(int num1, char c, int num2) { - switch (c) { - case '+': - return num1 + num2; - case '-': - return num1 - num2; - case '*': - return num1 * num2; - } - return -1; -} - -private boolean isOperation(char c) { - return c == '+' || c == '-' || c == '*'; -} -``` - -由于递归是两个分支,所以会有一些的解进行了重复计算,我们可以通过 `memoization` 技术,前边很多题都用过了,一种空间换时间的方法。 - -将递归过程中的解保存起来,如果第二次递归过来,直接返回结果即可,无需重复递归。 - -将解通过 `map` 存储,其中,`key` 存储函数入口参数的字符串,`value` 存储当前全部解的一个 `List` 。 - -```java -//添加一个 map -HashMap> map = new HashMap<>(); -public List diffWaysToCompute(String input) { - if (input.length() == 0) { - return new ArrayList<>(); - } - //如果已经有当前解了,直接返回 - if(map.containsKey(input)){ - return map.get(input); - } - List result = new ArrayList<>(); - int num = 0; - int index = 0; - while (index < input.length() && !isOperation(input.charAt(index))) { - num = num * 10 + input.charAt(index) - '0'; - index++; - } - if (index == input.length()) { - result.add(num); - //存到 map - map.put(input, result); - return result; - } - for (int i = 0; i < input.length(); i++) { - if (isOperation(input.charAt(i))) { - List result1 = diffWaysToCompute(input.substring(0, i)); - List result2 = diffWaysToCompute(input.substring(i + 1)); - for (int j = 0; j < result1.size(); j++) { - for (int k = 0; k < result2.size(); k++) { - char op = input.charAt(i); - result.add(caculate(result1.get(j), op, result2.get(k))); - } - } - } - } - //存到 map - map.put(input, result); - return result; -} - -private int caculate(int num1, char c, int num2) { - switch (c) { - case '+': - return num1 + num2; - case '-': - return num1 - num2; - case '*': - return num1 * num2; - } - return -1; -} - -private boolean isOperation(char c) { - return c == '+' || c == '-' || c == '*'; -} -``` - -# 解法二 动态规划 - -按理说写完递归、 写完 `memoization` ,接下来动态规划也能顺理成章的写出来了,比如经典的 [爬楼梯](https://leetcode.wang/leetCode-70-Climbing-Stairs.html) 问题。但这个如果什么都不处理,`dp` 数组的含义比较难定义,分享一下 [这里](https://leetcode.com/problems/different-ways-to-add-parentheses/discuss/66351/C%2B%2B-solution-using-dp-easy-understanding) 的处理吧。 - -最巧妙的地方就是做一个预处理,把每个数字提前转为 `int` 然后存起来,同时把运算符也都存起来。 - -这样的话我们就有了两个 `list`,一个保存了所有数字,一个保存了所有运算符。 - -```java -2 * 3 - 4 * 5 -存起来的数字是 numList = [2 3 4 5], -存起来的运算符是 opList = [*, -, *]。 -``` - -`dp[i][j]` 也比较好定义了,含义是第 `i` 到第 `j` 个数字(从 `0` 开始计数)范围内的表达式的所有解。 - -```java -举个例子,2 * 3 - 4 * 5 -dp[1][3] 就代表第一个数字 3 到第三个数字 5 范围内的表达式 3 - 4 * 5 的所有解。 -``` - -初始条件的话,也很简单了,就是范围内只有一个数字。 - -```java -2 * 3 - 4 * 5 -dp[0][0] = [2],dp[1][1] = [3],dp[2][2] = [4],dp[3][3] = [5]。 -``` - -有了一个数字的所有解,然后两个数字的所有解就可以求出来。 - -有了两个数字的所有解,然后三个数字的所有解就和解法一求法一样。 - -把三个数字分成两部分,将两部分的解两两组合起来即可。 - -两部分之间的运算符的话,因为表达式是一个数字一个运算符,所以运算符的下标就是左部分最后一个数字的下标。 -看下边的例子。 - -```java -2 * 3 - 4 * 5 -存起来的数字是 numList = [2 3 4 5], -存起来的运算符是 opList = [*, -, *]。 - -假设我们求 dp[1][3] -也就是计算 3 - 4 * 5 的解 -分成 3 和 4 * 5 两部分,3 对应的下标是 1 ,对应的运算符就是 opList[1] = '-' 。 -也就是计算 3 - 20 = -17 - -分成 3 - 4 和 5 两部分,4 的下标是 2 ,对应的运算符就是 opList[2] = '*'。 -也就是计算 -1 * 5 = -5 - -所以 dp[1][3] = [-17 -5] - -``` - -四个、五个... 都可以分成两部分,然后通过之前的解求出来。 - -直到包含了所有数字的解求出来,假设数字总个数是 `n`,`dp[0][n-1]` 就是最后返回的了。 - -```java -public List diffWaysToCompute(String input) { - List numList = new ArrayList<>(); - List opList = new ArrayList<>(); - char[] array = input.toCharArray(); - int num = 0; - for (int i = 0; i < array.length; i++) { - if (isOperation(array[i])) { - numList.add(num); - num = 0; - opList.add(array[i]); - continue; - } - num = num * 10 + array[i] - '0'; - } - numList.add(num); - int N = numList.size(); // 数字的个数 - - // 一个数字 - ArrayList[][] dp = (ArrayList[][]) new ArrayList[N][N]; - for (int i = 0; i < N; i++) { - ArrayList result = new ArrayList<>(); - result.add(numList.get(i)); - dp[i][i] = result; - } - // 2 个数字到 N 个数字 - for (int n = 2; n <= N; n++) { - // 开始下标 - for (int i = 0; i < N; i++) { - // 结束下标 - int j = i + n - 1; - if (j >= N) { - break; - } - ArrayList result = new ArrayList<>(); - // 分成 i ~ s 和 s+1 ~ j 两部分 - for (int s = i; s < j; s++) { - ArrayList result1 = dp[i][s]; - ArrayList result2 = dp[s + 1][j]; - for (int x = 0; x < result1.size(); x++) { - for (int y = 0; y < result2.size(); y++) { - // 第 s 个数字下标对应是第 s 个运算符 - char op = opList.get(s); - result.add(caculate(result1.get(x), op, result2.get(y))); - } - } - } - dp[i][j] = result; - - } - } - return dp[0][N-1]; -} - -private int caculate(int num1, char c, int num2) { - switch (c) { - case '+': - return num1 + num2; - case '-': - return num1 - num2; - case '*': - return num1 * num2; - } - return -1; -} - -private boolean isOperation(char c) { - return c == '+' || c == '-' || c == '*'; -} -``` - -# 总 - -解法一的话是比较直觉的方法,用递归可以将问题简化。 - -解法二的话,关键就在于字符串的预处理,将数字和运算符分别存起来,很巧妙。然后就能很明确的定义出 `dp` 的含义,代码就比较容易写出来了。 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/241.png) + +给一个表达式加括号,得到所有不同情况的解。 + +# 解法一 递归 + +一看到题就觉得有点复杂,可以考虑一下递归的方式,去寻找子问题和原问题解的关系。 + +可以通过运算符把整个式子分成两部分,两部分再利用递归解决。 + +以 `2 * 3 - 4 * 5` 为例。 + +`2` 和 `3 - 4 * 5` 两部分,中间是 `*` 号相连。 + +`2 * 3` 和 `4 * 5` 两部分,中间是 `-` 号相连。 + +`2 * 3 - 4` 和 ` 5` 两部分,中间是 `*` 号相连。 + +有了两部分的结果,然后再通过中间的符号两两计算加入到最终的结果中即可。 + +比如第一种情况,`2` 和 `3 - 4 * 5` 两部分,中间是 `*` 号相连。 + +`2` 的解就是 `[2]`,`3 - 4 * 5` 的解就是 `[-5, -17]`。 + +把两部分解通过 `*` 号计算,最终结果就是 `[-10, -34]`。 + +另外两种情况也类似。 + +然后还需要递归出口。 + +如果给定的字符串只有数字,没有运算符,那结果就是给定的字符串转为数字。 + +比如上边的第一种情况,`2` 的解就是 `[2]`。 + +```java +public List diffWaysToCompute(String input) { + if (input.length() == 0) { + return new ArrayList<>(); + } + List result = new ArrayList<>(); + int num = 0; + //考虑是全数字的情况 + int index = 0; + while (index < input.length() && !isOperation(input.charAt(index))) { + num = num * 10 + input.charAt(index) - '0'; + index++; + } + //将全数字的情况直接返回 + if (index == input.length()) { + result.add(num); + return result; + } + + for (int i = 0; i < input.length(); i++) { + //通过运算符将字符串分成两部分 + if (isOperation(input.charAt(i))) { + List result1 = diffWaysToCompute(input.substring(0, i)); + List result2 = diffWaysToCompute(input.substring(i + 1)); + //将两个结果依次运算 + for (int j = 0; j < result1.size(); j++) { + for (int k = 0; k < result2.size(); k++) { + char op = input.charAt(i); + result.add(caculate(result1.get(j), op, result2.get(k))); + } + } + } + } + return result; +} + +private int caculate(int num1, char c, int num2) { + switch (c) { + case '+': + return num1 + num2; + case '-': + return num1 - num2; + case '*': + return num1 * num2; + } + return -1; +} + +private boolean isOperation(char c) { + return c == '+' || c == '-' || c == '*'; +} +``` + +由于递归是两个分支,所以会有一些的解进行了重复计算,我们可以通过 `memoization` 技术,前边很多题都用过了,一种空间换时间的方法。 + +将递归过程中的解保存起来,如果第二次递归过来,直接返回结果即可,无需重复递归。 + +将解通过 `map` 存储,其中,`key` 存储函数入口参数的字符串,`value` 存储当前全部解的一个 `List` 。 + +```java +//添加一个 map +HashMap> map = new HashMap<>(); +public List diffWaysToCompute(String input) { + if (input.length() == 0) { + return new ArrayList<>(); + } + //如果已经有当前解了,直接返回 + if(map.containsKey(input)){ + return map.get(input); + } + List result = new ArrayList<>(); + int num = 0; + int index = 0; + while (index < input.length() && !isOperation(input.charAt(index))) { + num = num * 10 + input.charAt(index) - '0'; + index++; + } + if (index == input.length()) { + result.add(num); + //存到 map + map.put(input, result); + return result; + } + for (int i = 0; i < input.length(); i++) { + if (isOperation(input.charAt(i))) { + List result1 = diffWaysToCompute(input.substring(0, i)); + List result2 = diffWaysToCompute(input.substring(i + 1)); + for (int j = 0; j < result1.size(); j++) { + for (int k = 0; k < result2.size(); k++) { + char op = input.charAt(i); + result.add(caculate(result1.get(j), op, result2.get(k))); + } + } + } + } + //存到 map + map.put(input, result); + return result; +} + +private int caculate(int num1, char c, int num2) { + switch (c) { + case '+': + return num1 + num2; + case '-': + return num1 - num2; + case '*': + return num1 * num2; + } + return -1; +} + +private boolean isOperation(char c) { + return c == '+' || c == '-' || c == '*'; +} +``` + +# 解法二 动态规划 + +按理说写完递归、 写完 `memoization` ,接下来动态规划也能顺理成章的写出来了,比如经典的 [爬楼梯](https://leetcode.wang/leetCode-70-Climbing-Stairs.html) 问题。但这个如果什么都不处理,`dp` 数组的含义比较难定义,分享一下 [这里](https://leetcode.com/problems/different-ways-to-add-parentheses/discuss/66351/C%2B%2B-solution-using-dp-easy-understanding) 的处理吧。 + +最巧妙的地方就是做一个预处理,把每个数字提前转为 `int` 然后存起来,同时把运算符也都存起来。 + +这样的话我们就有了两个 `list`,一个保存了所有数字,一个保存了所有运算符。 + +```java +2 * 3 - 4 * 5 +存起来的数字是 numList = [2 3 4 5], +存起来的运算符是 opList = [*, -, *]。 +``` + +`dp[i][j]` 也比较好定义了,含义是第 `i` 到第 `j` 个数字(从 `0` 开始计数)范围内的表达式的所有解。 + +```java +举个例子,2 * 3 - 4 * 5 +dp[1][3] 就代表第一个数字 3 到第三个数字 5 范围内的表达式 3 - 4 * 5 的所有解。 +``` + +初始条件的话,也很简单了,就是范围内只有一个数字。 + +```java +2 * 3 - 4 * 5 +dp[0][0] = [2],dp[1][1] = [3],dp[2][2] = [4],dp[3][3] = [5]。 +``` + +有了一个数字的所有解,然后两个数字的所有解就可以求出来。 + +有了两个数字的所有解,然后三个数字的所有解就和解法一求法一样。 + +把三个数字分成两部分,将两部分的解两两组合起来即可。 + +两部分之间的运算符的话,因为表达式是一个数字一个运算符,所以运算符的下标就是左部分最后一个数字的下标。 +看下边的例子。 + +```java +2 * 3 - 4 * 5 +存起来的数字是 numList = [2 3 4 5], +存起来的运算符是 opList = [*, -, *]。 + +假设我们求 dp[1][3] +也就是计算 3 - 4 * 5 的解 +分成 3 和 4 * 5 两部分,3 对应的下标是 1 ,对应的运算符就是 opList[1] = '-' 。 +也就是计算 3 - 20 = -17 + +分成 3 - 4 和 5 两部分,4 的下标是 2 ,对应的运算符就是 opList[2] = '*'。 +也就是计算 -1 * 5 = -5 + +所以 dp[1][3] = [-17 -5] + +``` + +四个、五个... 都可以分成两部分,然后通过之前的解求出来。 + +直到包含了所有数字的解求出来,假设数字总个数是 `n`,`dp[0][n-1]` 就是最后返回的了。 + +```java +public List diffWaysToCompute(String input) { + List numList = new ArrayList<>(); + List opList = new ArrayList<>(); + char[] array = input.toCharArray(); + int num = 0; + for (int i = 0; i < array.length; i++) { + if (isOperation(array[i])) { + numList.add(num); + num = 0; + opList.add(array[i]); + continue; + } + num = num * 10 + array[i] - '0'; + } + numList.add(num); + int N = numList.size(); // 数字的个数 + + // 一个数字 + ArrayList[][] dp = (ArrayList[][]) new ArrayList[N][N]; + for (int i = 0; i < N; i++) { + ArrayList result = new ArrayList<>(); + result.add(numList.get(i)); + dp[i][i] = result; + } + // 2 个数字到 N 个数字 + for (int n = 2; n <= N; n++) { + // 开始下标 + for (int i = 0; i < N; i++) { + // 结束下标 + int j = i + n - 1; + if (j >= N) { + break; + } + ArrayList result = new ArrayList<>(); + // 分成 i ~ s 和 s+1 ~ j 两部分 + for (int s = i; s < j; s++) { + ArrayList result1 = dp[i][s]; + ArrayList result2 = dp[s + 1][j]; + for (int x = 0; x < result1.size(); x++) { + for (int y = 0; y < result2.size(); y++) { + // 第 s 个数字下标对应是第 s 个运算符 + char op = opList.get(s); + result.add(caculate(result1.get(x), op, result2.get(y))); + } + } + } + dp[i][j] = result; + + } + } + return dp[0][N-1]; +} + +private int caculate(int num1, char c, int num2) { + switch (c) { + case '+': + return num1 + num2; + case '-': + return num1 - num2; + case '*': + return num1 * num2; + } + return -1; +} + +private boolean isOperation(char c) { + return c == '+' || c == '-' || c == '*'; +} +``` + +# 总 + +解法一的话是比较直觉的方法,用递归可以将问题简化。 + +解法二的话,关键就在于字符串的预处理,将数字和运算符分别存起来,很巧妙。然后就能很明确的定义出 `dp` 的含义,代码就比较容易写出来了。 + diff --git a/leetcode-242-Valid-Anagram.md b/leetcode-242-Valid-Anagram.md index b7f6cab1a..59047f14b 100644 --- a/leetcode-242-Valid-Anagram.md +++ b/leetcode-242-Valid-Anagram.md @@ -1,188 +1,188 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/242.jpg) - -判断两个字符串是否是异构,也就是两个字符串包含的字母是完全相同的,只是顺序不一样。 - -# 思路分析 - -[49 题](https://leetcode.wang/leetCode-49-Group-Anagrams.html) 其实已经做过了,当时是给很多字符串,然后把异构的字符串放到一组里。介绍了四种解法,这里就不细讲了,直接写代码了。 - -# 解法一 HashMap - -最通用的解法,通过 `HashMap` 记录其中一个字符串每个字母的数量,然后再来判断和另一个字符串中每个字母的数量是否相同。 - -```java -public boolean isAnagram(String s, String t) { - HashMap map = new HashMap<>(); - char[] sArray = s.toCharArray(); - for (int i = 0; i < sArray.length; i++) { - int count = map.getOrDefault(sArray[i], 0); - map.put(sArray[i], count + 1); - } - - char[] tArray = t.toCharArray(); - for (int i = 0; i < tArray.length; i++) { - int count = map.getOrDefault(tArray[i], 0); - if (count == 0) { - return false; - } - map.put(tArray[i], count - 1); - } - - for (int value : map.values()) { - if (value != 0) { - return false; - } - } - return true; -} -``` - -我们最后判断了一下所有的 `value` 是否为 `0`,因为要考虑这种情况,`abc` 和 `ab`。 - -我们也可以在开头判断一下两个字符串长度是否相同,这样的话最后就不用判断所有的 `value` 是否为 `0` 了。 - -```java -public boolean isAnagram(String s, String t) { - if (s.length() != t.length()) { - return false; - } - HashMap map = new HashMap<>(); - char[] sArray = s.toCharArray(); - for (int i = 0; i < sArray.length; i++) { - int count = map.getOrDefault(sArray[i], 0); - map.put(sArray[i], count + 1); - } - - char[] tArray = t.toCharArray(); - for (int i = 0; i < tArray.length; i++) { - int count = map.getOrDefault(tArray[i], 0); - if (count == 0) { - return false; - } - map.put(tArray[i], count - 1); - } - - return true; -} -``` - - - -# 解法二 排序 - -把两个字符串按照字典序进行排序,排序完比较两个字符串是否相等。 - -```java -public boolean isAnagram(String s, String t) { - char[] sArray = s.toCharArray(); - Arrays.sort(sArray); - s = String.valueOf(sArray); - - char[] tArray = t.toCharArray(); - Arrays.sort(tArray); - t = String.valueOf(tArray); - - return s.equals(t); -} -``` - -# 解法三 素数相乘 - -把每个字母映射到一个素数上,然后把相应的素数相乘作为一个 `key` 。 - -```java -public boolean isAnagram(String s, String t) { - int[] prime = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, - 101 }; - char[] sArray = s.toCharArray(); - int sKey = 1; - for (int i = 0; i < sArray.length; i++) { - sKey = sKey * prime[sArray[i] - 'a']; - } - - char[] tArray = t.toCharArray(); - int tKey = 1; - for (int i = 0; i < tArray.length; i++) { - tKey = tKey * prime[tArray[i] - 'a']; - } - - return sKey == tKey; -} -``` - -[49 题](https://leetcode.wang/leetCode-49-Group-Anagrams.html) 这样写是可以的,但当时也指出了一个问题,相乘如果数过大的话会造成溢出,最后的 `key` 就不准确了,所以会出现错误。 - -这里的话用 `int` 就不可以了,改成 `long` 也不行。最后用了 `java` 提供的大数类。 - -```java -import java.math.BigInteger; -class Solution { - public boolean isAnagram(String s, String t) { - int[] prime = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, - 101 }; - char[] sArray = s.toCharArray(); - BigInteger sKey = BigInteger.valueOf(1); - for (int i = 0; i < sArray.length; i++) { - BigInteger temp = BigInteger.valueOf(prime[sArray[i] - 'a']); - sKey = sKey.multiply(temp); - } - - char[] tArray = t.toCharArray(); - BigInteger tKey = BigInteger.valueOf(1); - for (int i = 0; i < tArray.length; i++) { - BigInteger temp = BigInteger.valueOf(prime[tArray[i] - 'a']); - tKey = tKey.multiply(temp); - } - - return sKey.equals(tKey); - } -} -``` - -# 解法四 数组 - -这个本质上和解法一是一样的,因为字母只有 `26` 个,所以我们可以用一个大小为 `26` 的数组来统计每个字母的个数。 - -因为有 26 个字母,不好解释,我们假设只有 5 个字母,来看一下怎么完成映射。 - -首先初始化 `key = "0#0#0#0#0#"`,数字分别代表 `abcde` 出现的次数,`#` 用来分割。 - -这样的话,`"abb"` 就映射到了 `"1#2#0#0#0"`。 - -`"cdc"` 就映射到了 `"0#0#2#1#0"`。 - -`"dcc`" 就映射到了 `"0#0#2#1#0"`。 - -```java -public boolean isAnagram(String s, String t) { - int[] sNum = new int[26]; - // 记录每个字符的次数 - char[] sArray = s.toCharArray(); - for (int i = 0; i < sArray.length; i++) { - sNum[sArray[i] - 'a']++; - } - StringBuilder sKey = new StringBuilder(); - for (int i = 0; i < sNum.length; i++) { - sKey.append(sNum[i] + "#"); - } - - int[] tNum = new int[26]; - // 记录每个字符的次数 - char[] tArray = t.toCharArray(); - for (int i = 0; i < tArray.length; i++) { - tNum[tArray[i] - 'a']++; - } - StringBuilder tKey = new StringBuilder(); - for (int i = 0; i < tNum.length; i++) { - tKey.append(tNum[i] + "#"); - } - - return sKey.toString().equals(tKey.toString()); -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/242.jpg) + +判断两个字符串是否是异构,也就是两个字符串包含的字母是完全相同的,只是顺序不一样。 + +# 思路分析 + +[49 题](https://leetcode.wang/leetCode-49-Group-Anagrams.html) 其实已经做过了,当时是给很多字符串,然后把异构的字符串放到一组里。介绍了四种解法,这里就不细讲了,直接写代码了。 + +# 解法一 HashMap + +最通用的解法,通过 `HashMap` 记录其中一个字符串每个字母的数量,然后再来判断和另一个字符串中每个字母的数量是否相同。 + +```java +public boolean isAnagram(String s, String t) { + HashMap map = new HashMap<>(); + char[] sArray = s.toCharArray(); + for (int i = 0; i < sArray.length; i++) { + int count = map.getOrDefault(sArray[i], 0); + map.put(sArray[i], count + 1); + } + + char[] tArray = t.toCharArray(); + for (int i = 0; i < tArray.length; i++) { + int count = map.getOrDefault(tArray[i], 0); + if (count == 0) { + return false; + } + map.put(tArray[i], count - 1); + } + + for (int value : map.values()) { + if (value != 0) { + return false; + } + } + return true; +} +``` + +我们最后判断了一下所有的 `value` 是否为 `0`,因为要考虑这种情况,`abc` 和 `ab`。 + +我们也可以在开头判断一下两个字符串长度是否相同,这样的话最后就不用判断所有的 `value` 是否为 `0` 了。 + +```java +public boolean isAnagram(String s, String t) { + if (s.length() != t.length()) { + return false; + } + HashMap map = new HashMap<>(); + char[] sArray = s.toCharArray(); + for (int i = 0; i < sArray.length; i++) { + int count = map.getOrDefault(sArray[i], 0); + map.put(sArray[i], count + 1); + } + + char[] tArray = t.toCharArray(); + for (int i = 0; i < tArray.length; i++) { + int count = map.getOrDefault(tArray[i], 0); + if (count == 0) { + return false; + } + map.put(tArray[i], count - 1); + } + + return true; +} +``` + + + +# 解法二 排序 + +把两个字符串按照字典序进行排序,排序完比较两个字符串是否相等。 + +```java +public boolean isAnagram(String s, String t) { + char[] sArray = s.toCharArray(); + Arrays.sort(sArray); + s = String.valueOf(sArray); + + char[] tArray = t.toCharArray(); + Arrays.sort(tArray); + t = String.valueOf(tArray); + + return s.equals(t); +} +``` + +# 解法三 素数相乘 + +把每个字母映射到一个素数上,然后把相应的素数相乘作为一个 `key` 。 + +```java +public boolean isAnagram(String s, String t) { + int[] prime = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, + 101 }; + char[] sArray = s.toCharArray(); + int sKey = 1; + for (int i = 0; i < sArray.length; i++) { + sKey = sKey * prime[sArray[i] - 'a']; + } + + char[] tArray = t.toCharArray(); + int tKey = 1; + for (int i = 0; i < tArray.length; i++) { + tKey = tKey * prime[tArray[i] - 'a']; + } + + return sKey == tKey; +} +``` + +[49 题](https://leetcode.wang/leetCode-49-Group-Anagrams.html) 这样写是可以的,但当时也指出了一个问题,相乘如果数过大的话会造成溢出,最后的 `key` 就不准确了,所以会出现错误。 + +这里的话用 `int` 就不可以了,改成 `long` 也不行。最后用了 `java` 提供的大数类。 + +```java +import java.math.BigInteger; +class Solution { + public boolean isAnagram(String s, String t) { + int[] prime = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, + 101 }; + char[] sArray = s.toCharArray(); + BigInteger sKey = BigInteger.valueOf(1); + for (int i = 0; i < sArray.length; i++) { + BigInteger temp = BigInteger.valueOf(prime[sArray[i] - 'a']); + sKey = sKey.multiply(temp); + } + + char[] tArray = t.toCharArray(); + BigInteger tKey = BigInteger.valueOf(1); + for (int i = 0; i < tArray.length; i++) { + BigInteger temp = BigInteger.valueOf(prime[tArray[i] - 'a']); + tKey = tKey.multiply(temp); + } + + return sKey.equals(tKey); + } +} +``` + +# 解法四 数组 + +这个本质上和解法一是一样的,因为字母只有 `26` 个,所以我们可以用一个大小为 `26` 的数组来统计每个字母的个数。 + +因为有 26 个字母,不好解释,我们假设只有 5 个字母,来看一下怎么完成映射。 + +首先初始化 `key = "0#0#0#0#0#"`,数字分别代表 `abcde` 出现的次数,`#` 用来分割。 + +这样的话,`"abb"` 就映射到了 `"1#2#0#0#0"`。 + +`"cdc"` 就映射到了 `"0#0#2#1#0"`。 + +`"dcc`" 就映射到了 `"0#0#2#1#0"`。 + +```java +public boolean isAnagram(String s, String t) { + int[] sNum = new int[26]; + // 记录每个字符的次数 + char[] sArray = s.toCharArray(); + for (int i = 0; i < sArray.length; i++) { + sNum[sArray[i] - 'a']++; + } + StringBuilder sKey = new StringBuilder(); + for (int i = 0; i < sNum.length; i++) { + sKey.append(sNum[i] + "#"); + } + + int[] tNum = new int[26]; + // 记录每个字符的次数 + char[] tArray = t.toCharArray(); + for (int i = 0; i < tArray.length; i++) { + tNum[tArray[i] - 'a']++; + } + StringBuilder tKey = new StringBuilder(); + for (int i = 0; i < tNum.length; i++) { + tKey.append(tNum[i] + "#"); + } + + return sKey.toString().equals(tKey.toString()); +} +``` + +# 总 + 和 [49 题](https://leetcode.wang/leetCode-49-Group-Anagrams.html) 的解法完全一样,唯一不同的地方在于解法三,这里因为给的字符串比较长,所以素数相乘的方法不是很好了。 \ No newline at end of file diff --git a/leetcode-257-Binary-Tree-Paths.md b/leetcode-257-Binary-Tree-Paths.md index d91fda797..52bdca21e 100644 --- a/leetcode-257-Binary-Tree-Paths.md +++ b/leetcode-257-Binary-Tree-Paths.md @@ -1,46 +1,46 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/257.jpg) - -输出从根到叶子节点的所有路径。 - -# 思路分析 - -很明显是一个二叉树遍历的问题,我们可以用递归形式的 `DFS` ,使用栈形式的 `DFS`,使用队列形式的 `BFS`。 - -和 [112 题](https://leetcode.wang/leetcode-112-Path-Sum.html) 差不多,这里就不详细说了。 - -只给出 `DFS` 递归的代码了,其他代码的话可以参考 [这里](https://leetcode.com/problems/binary-tree-paths/discuss/68278/My-Java-solution-in-DFS-BFS-recursion)。 - -# 解法一 DFS - -用 `result` 保存所有解,到达叶子节点的时候就将结果保存起来。 - -```java -public List binaryTreePaths(TreeNode root) { - List result = new ArrayList<>(); - if(root == null){ - return result; - } - binaryTreePaths(root, "", result); - return result; -} - -private void binaryTreePaths(TreeNode root, String temp, List result) { - if (root.left == null && root.right == null) { - temp = temp + root.val; - result.add(temp); - return; - } - if (root.left != null) { - binaryTreePaths(root.left, temp + root.val + "->", result); - } - if (root.right != null) { - binaryTreePaths(root.right, temp + root.val + "->", result); - } -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/257.jpg) + +输出从根到叶子节点的所有路径。 + +# 思路分析 + +很明显是一个二叉树遍历的问题,我们可以用递归形式的 `DFS` ,使用栈形式的 `DFS`,使用队列形式的 `BFS`。 + +和 [112 题](https://leetcode.wang/leetcode-112-Path-Sum.html) 差不多,这里就不详细说了。 + +只给出 `DFS` 递归的代码了,其他代码的话可以参考 [这里](https://leetcode.com/problems/binary-tree-paths/discuss/68278/My-Java-solution-in-DFS-BFS-recursion)。 + +# 解法一 DFS + +用 `result` 保存所有解,到达叶子节点的时候就将结果保存起来。 + +```java +public List binaryTreePaths(TreeNode root) { + List result = new ArrayList<>(); + if(root == null){ + return result; + } + binaryTreePaths(root, "", result); + return result; +} + +private void binaryTreePaths(TreeNode root, String temp, List result) { + if (root.left == null && root.right == null) { + temp = temp + root.val; + result.add(temp); + return; + } + if (root.left != null) { + binaryTreePaths(root.left, temp + root.val + "->", result); + } + if (root.right != null) { + binaryTreePaths(root.right, temp + root.val + "->", result); + } +} +``` + +# 总 + 考察的就是二叉树的遍历,很基础的一道题。 \ No newline at end of file diff --git a/leetcode-258-Add-Digits.md b/leetcode-258-Add-Digits.md index b2e4009bb..8526f64a6 100644 --- a/leetcode-258-Add-Digits.md +++ b/leetcode-258-Add-Digits.md @@ -1,119 +1,119 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/258.jpg) - -将给定的数字的各个位相加得到新的数字,一直重复这个过程,直到这个数小于 `10`,将这个数输出。 - -# 解法一 - -开始有点不明所以,直接用递归或者循环按照题目的意思写不就行了吗,先用递归尝试了一下。 - -```java -public int addDigits(int num) { - if (num < 10) { - return num; - } - int next = 0; - while (num != 0) { - next = next + num % 10; - num /= 10; - } - return addDigits(next); -} -``` - -没想到直接通过了,上边的递归很简单可以直接写成迭代的形式。 - -```java -public int addDigits(int num) { - while (num >= 10) { - int next = 0; - while (num != 0) { - next = next + num % 10; - num /= 10; - } - num = next; - } - return num; -} -``` - -# 解法二 数学上 - -看了下 `Discuss` ,原来要求的数叫做数字根,看下 [维基百科](https://zh.wikipedia.org/wiki/數根) 的定义。 - -> 在[数学](https://zh.wikipedia.org/wiki/數學)中,**数根**(又称**位数根**或**数字根**Digital root)是[自然数](https://zh.wikipedia.org/wiki/自然數)的一种[性质](https://zh.wikipedia.org/w/index.php?title=性質&action=edit&redlink=1),换句话说,每个[自然数](https://zh.wikipedia.org/wiki/自然數)都有一个**数根**。 -> -> 数根是将一[正整数](https://zh.wikipedia.org/wiki/正整數)的各个[位数](https://zh.wikipedia.org/wiki/位數)相加(即横向相加),若加完后的值大于[10](https://zh.wikipedia.org/wiki/10)的话,则继续将各位数进行横向相加直到其值小于[十](https://zh.wikipedia.org/wiki/十)为止[[1\]](https://zh.wikipedia.org/wiki/數根#cite_note-數學的神祕奇趣-1),或是,将一数字重复做[数字和](https://zh.wikipedia.org/wiki/数字和),直到其值小于[十](https://zh.wikipedia.org/wiki/十)为止,则所得的值为该数的**数根**。 -> -> 例如54817的数根为[7](https://zh.wikipedia.org/wiki/7),因为[5](https://zh.wikipedia.org/wiki/5)+[4](https://zh.wikipedia.org/wiki/4)+[8](https://zh.wikipedia.org/wiki/8)+[1](https://zh.wikipedia.org/wiki/1)+[7](https://zh.wikipedia.org/wiki/7)=[25](https://zh.wikipedia.org/wiki/25),[25](https://zh.wikipedia.org/wiki/25)[大于](https://zh.wikipedia.org/wiki/大于)10则再[加](https://zh.wikipedia.org/wiki/加)一次,[2](https://zh.wikipedia.org/wiki/2)+[5](https://zh.wikipedia.org/wiki/5)=[7](https://zh.wikipedia.org/wiki/7),[7](https://zh.wikipedia.org/wiki/7)[小于](https://zh.wikipedia.org/wiki/小于)十,则7为54817的数根。 - -然后是它的用途。 - -> 数根可以计算[模运算](https://zh.wikipedia.org/wiki/模运算)的[同余](https://zh.wikipedia.org/wiki/同餘),对于非常大的数字的情况下可以节省很多[时间](https://zh.wikipedia.org/wiki/時間)。 -> -> 数字根可作为一种检验计算正确性的方法。例如,两数字的和的数根等于两数字分别的数根的和。 -> -> 另外,数根也可以用来判断数字的整除性,如果数根能被3或9整除,则原来的数也能被3或9整除。 - -接下来讨论我们怎么求出树根。 - -我们把 `1` 到 `30` 的树根列出来。 - -```java -原数: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 -数根: 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 -``` - -可以发现数根 `9` 个为一组, `1 - 9` 循环出现。我们需要做就是把原数映射到树根就可以,循环出现的话,想到的就是取余了。 - -结合上边的规律,对于给定的 `n` 有三种情况。 - -`n` 是 `0` ,数根就是 `0`。 - -`n` 不是 `9` 的倍数,数根就是 `n` 对 `9` 取余,即 `n mod 9`。 - -`n` 是 `9` 的倍数,数根就是 `9`。 - -我们可以把两种情况统一起来,我们将给定的数字减 `1`,相当于原数整体向左偏移了 `1`,然后再将得到的数字对 `9` 取余,最后将得到的结果加 `1` 即可。 - -原数是 `n`,树根就可以表示成 `(n-1) mod 9 + 1`,可以结合下边的过程理解。 - -```java -原数: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 -偏移: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 -取余: 0 1 2 3 4 5 6 7 8 0 1 2 3 4 5 6 7 8 0 1 2 3 4 5 6 7 8 0 1 2 -数根: 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 -``` - -所以代码的话其实一句就够了。 - -```java -public int addDigits(int num) { - return (num - 1) % 9 + 1; -} -``` - -当然上边是通过找规律得出的方法,我们需要证明一下。知乎的[最高赞](https://www.zhihu.com/question/30972581/answer/50203344) 讲的很清楚了,我再把推导和上边的公式一起说一下。 - -下边是作者的推导。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/258_2.jpg) - -上边证明了对原数做一个 `f` 操作,也就是各个位上的数相加,然后不停的做 `f` 操作,最终的结果对 `9` 取余和原数 `x` 对 `9` 取余是相等的。 - -不考虑 `0`这种特殊情况,不停的做 `f` 操作,最终得到的数就是 `1 - 9`,对 `9`取余的结果是 `1 - 8` 和 `0`。结果是 `0` 的话对应数根就是 `9`,其他情况的数根就是取余结果。 - -也就是我们之前讨论的。 - -> `n` 是 `0` ,数根就是 `0`。 -> -> `n` 不是 `9` 的倍数,数根就是 `n` 对 `9` 取余,即 `n mod 9`。 -> -> `n` 是 `9` 的倍数,数根就是 `9`。 - -同样的,我们可以通过 `(n-1) mod 9 + 1` 这个式子把上边的几种情况统一起来。 - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/258.jpg) + +将给定的数字的各个位相加得到新的数字,一直重复这个过程,直到这个数小于 `10`,将这个数输出。 + +# 解法一 + +开始有点不明所以,直接用递归或者循环按照题目的意思写不就行了吗,先用递归尝试了一下。 + +```java +public int addDigits(int num) { + if (num < 10) { + return num; + } + int next = 0; + while (num != 0) { + next = next + num % 10; + num /= 10; + } + return addDigits(next); +} +``` + +没想到直接通过了,上边的递归很简单可以直接写成迭代的形式。 + +```java +public int addDigits(int num) { + while (num >= 10) { + int next = 0; + while (num != 0) { + next = next + num % 10; + num /= 10; + } + num = next; + } + return num; +} +``` + +# 解法二 数学上 + +看了下 `Discuss` ,原来要求的数叫做数字根,看下 [维基百科](https://zh.wikipedia.org/wiki/數根) 的定义。 + +> 在[数学](https://zh.wikipedia.org/wiki/數學)中,**数根**(又称**位数根**或**数字根**Digital root)是[自然数](https://zh.wikipedia.org/wiki/自然數)的一种[性质](https://zh.wikipedia.org/w/index.php?title=性質&action=edit&redlink=1),换句话说,每个[自然数](https://zh.wikipedia.org/wiki/自然數)都有一个**数根**。 +> +> 数根是将一[正整数](https://zh.wikipedia.org/wiki/正整數)的各个[位数](https://zh.wikipedia.org/wiki/位數)相加(即横向相加),若加完后的值大于[10](https://zh.wikipedia.org/wiki/10)的话,则继续将各位数进行横向相加直到其值小于[十](https://zh.wikipedia.org/wiki/十)为止[[1\]](https://zh.wikipedia.org/wiki/數根#cite_note-數學的神祕奇趣-1),或是,将一数字重复做[数字和](https://zh.wikipedia.org/wiki/数字和),直到其值小于[十](https://zh.wikipedia.org/wiki/十)为止,则所得的值为该数的**数根**。 +> +> 例如54817的数根为[7](https://zh.wikipedia.org/wiki/7),因为[5](https://zh.wikipedia.org/wiki/5)+[4](https://zh.wikipedia.org/wiki/4)+[8](https://zh.wikipedia.org/wiki/8)+[1](https://zh.wikipedia.org/wiki/1)+[7](https://zh.wikipedia.org/wiki/7)=[25](https://zh.wikipedia.org/wiki/25),[25](https://zh.wikipedia.org/wiki/25)[大于](https://zh.wikipedia.org/wiki/大于)10则再[加](https://zh.wikipedia.org/wiki/加)一次,[2](https://zh.wikipedia.org/wiki/2)+[5](https://zh.wikipedia.org/wiki/5)=[7](https://zh.wikipedia.org/wiki/7),[7](https://zh.wikipedia.org/wiki/7)[小于](https://zh.wikipedia.org/wiki/小于)十,则7为54817的数根。 + +然后是它的用途。 + +> 数根可以计算[模运算](https://zh.wikipedia.org/wiki/模运算)的[同余](https://zh.wikipedia.org/wiki/同餘),对于非常大的数字的情况下可以节省很多[时间](https://zh.wikipedia.org/wiki/時間)。 +> +> 数字根可作为一种检验计算正确性的方法。例如,两数字的和的数根等于两数字分别的数根的和。 +> +> 另外,数根也可以用来判断数字的整除性,如果数根能被3或9整除,则原来的数也能被3或9整除。 + +接下来讨论我们怎么求出树根。 + +我们把 `1` 到 `30` 的树根列出来。 + +```java +原数: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 +数根: 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 +``` + +可以发现数根 `9` 个为一组, `1 - 9` 循环出现。我们需要做就是把原数映射到树根就可以,循环出现的话,想到的就是取余了。 + +结合上边的规律,对于给定的 `n` 有三种情况。 + +`n` 是 `0` ,数根就是 `0`。 + +`n` 不是 `9` 的倍数,数根就是 `n` 对 `9` 取余,即 `n mod 9`。 + +`n` 是 `9` 的倍数,数根就是 `9`。 + +我们可以把两种情况统一起来,我们将给定的数字减 `1`,相当于原数整体向左偏移了 `1`,然后再将得到的数字对 `9` 取余,最后将得到的结果加 `1` 即可。 + +原数是 `n`,树根就可以表示成 `(n-1) mod 9 + 1`,可以结合下边的过程理解。 + +```java +原数: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 +偏移: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 +取余: 0 1 2 3 4 5 6 7 8 0 1 2 3 4 5 6 7 8 0 1 2 3 4 5 6 7 8 0 1 2 +数根: 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 +``` + +所以代码的话其实一句就够了。 + +```java +public int addDigits(int num) { + return (num - 1) % 9 + 1; +} +``` + +当然上边是通过找规律得出的方法,我们需要证明一下。知乎的[最高赞](https://www.zhihu.com/question/30972581/answer/50203344) 讲的很清楚了,我再把推导和上边的公式一起说一下。 + +下边是作者的推导。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/258_2.jpg) + +上边证明了对原数做一个 `f` 操作,也就是各个位上的数相加,然后不停的做 `f` 操作,最终的结果对 `9` 取余和原数 `x` 对 `9` 取余是相等的。 + +不考虑 `0`这种特殊情况,不停的做 `f` 操作,最终得到的数就是 `1 - 9`,对 `9`取余的结果是 `1 - 8` 和 `0`。结果是 `0` 的话对应数根就是 `9`,其他情况的数根就是取余结果。 + +也就是我们之前讨论的。 + +> `n` 是 `0` ,数根就是 `0`。 +> +> `n` 不是 `9` 的倍数,数根就是 `n` 对 `9` 取余,即 `n mod 9`。 +> +> `n` 是 `9` 的倍数,数根就是 `9`。 + +同样的,我们可以通过 `(n-1) mod 9 + 1` 这个式子把上边的几种情况统一起来。 + +# 总 + 这道题的话如果用程序的话很好解决,就是不停的循环即可。解法二数学上的话就很神奇了,一般也不会往这方面想了。 \ No newline at end of file diff --git a/leetcode-260-Single-NumberIII.md b/leetcode-260-Single-NumberIII.md index bcbe159b1..1191e6045 100644 --- a/leetcode-260-Single-NumberIII.md +++ b/leetcode-260-Single-NumberIII.md @@ -1,217 +1,217 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/260.jpg) - -所有数字都出现了两次,只有两个数字都只出现了 `1` 次,找出这两个数字。 - -# 解法一 - -最直接的方法,统计每个数出现的次数。使用 `HashMap` 或者 `HashSet`,由于每个数字最多出现两次,我们可以使用 `HashSet`。 - -遍历数组,遇到的数如果 `HashSet` 中存在,就把这个数删除。如果不存在,就把它加入到 `HashSet` 中。最后 `HashSet` 中剩下的两个数就是我们要找的了。 - -```java -public int[] singleNumber(int[] nums) { - HashSet set = new HashSet<>(); - for (int n : nums) { - if (set.contains(n)) { - set.remove(n); - } else { - set.add(n); - } - } - int[] result = new int[2]; - int i = 0; - for (int n : set) { - result[i] = n; - i++; - } - return result; -} -``` - -# 解法二 - -我们之前做过 [136 题](https://leetcode.wang/leetcode-136-Single-Number.html) ,当时是所有数字都是成对出现的,只有一个数字是落单的,找出这个落单的数字。其中介绍了异或的方法,把之前的介绍先粘贴过来。 - -还记得位操作中的异或吗?计算规则如下。 - -> 0 ⊕ 0 = 0 -> -> 1 ⊕ 1 = 0 -> -> 0 ⊕ 1 = 1 -> -> 1 ⊕ 0 = 1 - -总结起来就是相同为零,不同为一。 - -根据上边的规则,可以推导出一些性质 - -- 0 ⊕ a = a -- a ⊕ a = 0 - -此外异或满足交换律以及结合律。 - -所以对于之前的例子 `a b a b c c d` ,如果我们把给定的数字相互异或会发生什么呢? - -```java - a ⊕ b ⊕ a ⊕ b ⊕ c ⊕ c ⊕ d -= ( a ⊕ a ) ⊕ ( b ⊕ b ) ⊕ ( c ⊕ c ) ⊕ d -= 0 ⊕ 0 ⊕ 0 ⊕ d -= d -``` - -然后我们就找出了只出现了一次的数字。 - -这道题的话,因为要寻找的是两个数字,全部异或后不是我们所要的结果。介绍一下 [这里](https://leetcode.com/problems/single-number-iii/discuss/68900/Accepted-C%2B%2BJava-O(n)-time-O(1)-space-Easy-Solution-with-Detail-Explanations) 的思路。 - -如果我们把原数组分成两组,只出现过一次的两个数字分别在两组里边,那么问题就转换成之前的老问题了,只需要这两组里的数字各自异或,答案就出来了。 - -那么通过什么把数组分成两组呢? - -放眼到二进制,我们要找的这两个数字是不同的,所以它俩至少有一位是不同的,所以我们可以根据这一位,把数组分成这一位都是 `1` 的一类和这一位都是 `0` 的一类,这样就把这两个数分到两组里了。 - -那么怎么知道那两个数字哪一位不同呢? - -回到我们异或的结果,如果把数组中的所有数字异或,最后异或的结果,其实就是我们要找的两个数字的异或。而异或结果如果某一位是 `1`,也就意味着当前位两个数字一个是 `1` ,一个是 `0`,也就找到了不同的一位。 - -思路就是上边的了,然后再考虑代码怎么写。 - -怎么把数字分类? - -我们构造一个数,把我们要找的那两个数字二进制不同的那一位写成 `1`,其它位都写 `0`,也就是 `0...0100...000` 的形式。 - -然后把构造出来的数和数组中的数字相与,如果结果是 `0`,那就意味着这个数属于当前位为 `0` 的一类。否则的话,就意味着这个数属于当前位为 `1` 的一类。 - -怎么构造 `0...0100...000` 这样的数。 - -由于我们异或得到的数可能不只一位是 `1`,可能是这样的 `0100110`,那么怎么只留一位是 `1` 呢? - -方法有很多了。 - -比如,[201 题](https://leetcode.wang/leetcode-201-Bitwise-AND-of-Numbers-Range.html) 解法三介绍的 `Integer.highestOneBit` 方法,它可以保留某个数的最高位的 `1`,其它位全部置 `0`,源码的话当时也介绍了,可以过去看一下。 - -最后,总结下我们的算法,我们通过要找的两个数字的某一位不同,将原数组分成两组,然后组内分别进行异或,最后要找的数字就是两组分别异或的结果。 - -然后举个具体的例子,来理解一下算法。 - -```java -[1,2,1,3,2,5] - -1 = 001 -2 = 010 -1 = 001 -3 = 011 -2 = 010 -5 = 101 - -把上边所有的数字异或,最后得到的结果就是 3 ^ 5 = 6 (110) - -然后对 110 调用 Integer.highestOneBit 方法就得到 100, 我们通过倒数第三位将原数组分类 - -倒数第三位为 0 的组 -1 = 001 -2 = 010 -1 = 001 -3 = 011 -2 = 010 - -倒数第三位为 1 的组 -5 = 101 - -最后组内数字依次异或即可。 -``` - -再结合代码,理解一下。 - -```java -public int[] singleNumber(int[] nums) { - int diff = 0; - for (int n : nums) { - diff ^= n; - } - diff = Integer.highestOneBit(diff); - int[] result = { 0, 0 }; - for (int n : nums) { - //当前位是 0 的组, 然后组内异或 - if ((diff & n) == 0) { - result[0] ^= n; - //当前位是 1 的组 - } else { - result[1] ^= n; - } - } - return result; -} -``` - -[这里](https://leetcode.com/problems/single-number-iii/discuss/69007/C-O(n)-time-O(1)-space-7-line-Solution-with-Detail-Explanation) 提出了一个小小的改进。 - -假如我们要找的数字是 `a` 和 `b`,一开始我们得到 `diff = a ^ b`。然后通过异或我们分别求出了 `a` 和 `b` 。 - -其实如果我们知道了 `a`,`b` 的话可以通过一次异或就能得到,`b = diff ^ a` 。 - -```java -public int[] singleNumber(int[] nums) { - int diff = 0; - for (int n : nums) { - diff ^= n; - } - int diff2 = Integer.highestOneBit(diff); - int[] result = { 0, 0 }; - for (int n : nums) { - //当前位是 0 的组, 然后组内异或 - if ((diff2 & n) == 0) { - result[0] ^= n; - } - } - result[1] = diff ^ result[0]; - return result; -} -``` - -得到只有一位 `1` 的数,除了 `Integer.highestOneBit` 的方法还有其他的做法。 - -[这里](https://leetcode.com/problems/single-number-iii/discuss/68900/Accepted-C%2B%2BJava-O(n)-time-O(1)-space-Easy-Solution-with-Detail-Explanations) 的做法。 - -```java - diff &= -diff; -``` - -取负号其实就是先取反,再加 `1`,需要 [补码](https://zhuanlan.zhihu.com/p/67227136) 的知识。最后再和原数相与就会保留最低位的 `1`。比如 `1010`,先取反是 `0101`,再加 `1`,就是 `0110`,再和 `1010` 相与,就是 `0010` 了。 - -还有 [这里](https://leetcode.com/problems/single-number-iii/discuss/68921/C%2B%2B-solution-O(n)-time-and-O(1)-space-easy-understaning-with-simple-explanation) 的做法。 - -```java - diff = (diff & (diff - 1)) ^ diff; -``` - -`n & (n - 1)` 的操作我们在 [191 题](https://leetcode.wang/leetcode-191-Number-of-1-Bits.html) 用过,它可以将最低位的 `1` 置为 `0`。比如 `1110`,先将最低位的 `1` 置为 `0` 就变成 `1100`,然后再和原数 `1110` 异或,就得到了 `0010` 。 - -还有 [这里](https://leetcode.com/problems/single-number-iii/discuss/68923/Bit-manipulation-beats-99.62) 的做法。 - -```java -diff = xor & ~(diff - 1); -``` - -先减 `1`,再取反,再相与。比如 `1010` 减 `1` 就是 `1001`,然后取反 `0110`,然后和原数 `1010` 相与,就是 `0010` 了。 - -还有 [这里](https://leetcode.com/problems/single-number-iii/discuss/342714/Best-Explanation-C%2B%2B) 的做法。 - -```java -int mask=1; -while((diff & mask)==0) -{ - mask<<=1; -} -//mask 就是我们要构造的了 -``` - -这个方法比较直接,依次判断哪一位是 `1`。 - -# 总 - -解法一的话经常用了,最容易想到的方法。 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/260.jpg) + +所有数字都出现了两次,只有两个数字都只出现了 `1` 次,找出这两个数字。 + +# 解法一 + +最直接的方法,统计每个数出现的次数。使用 `HashMap` 或者 `HashSet`,由于每个数字最多出现两次,我们可以使用 `HashSet`。 + +遍历数组,遇到的数如果 `HashSet` 中存在,就把这个数删除。如果不存在,就把它加入到 `HashSet` 中。最后 `HashSet` 中剩下的两个数就是我们要找的了。 + +```java +public int[] singleNumber(int[] nums) { + HashSet set = new HashSet<>(); + for (int n : nums) { + if (set.contains(n)) { + set.remove(n); + } else { + set.add(n); + } + } + int[] result = new int[2]; + int i = 0; + for (int n : set) { + result[i] = n; + i++; + } + return result; +} +``` + +# 解法二 + +我们之前做过 [136 题](https://leetcode.wang/leetcode-136-Single-Number.html) ,当时是所有数字都是成对出现的,只有一个数字是落单的,找出这个落单的数字。其中介绍了异或的方法,把之前的介绍先粘贴过来。 + +还记得位操作中的异或吗?计算规则如下。 + +> 0 ⊕ 0 = 0 +> +> 1 ⊕ 1 = 0 +> +> 0 ⊕ 1 = 1 +> +> 1 ⊕ 0 = 1 + +总结起来就是相同为零,不同为一。 + +根据上边的规则,可以推导出一些性质 + +- 0 ⊕ a = a +- a ⊕ a = 0 + +此外异或满足交换律以及结合律。 + +所以对于之前的例子 `a b a b c c d` ,如果我们把给定的数字相互异或会发生什么呢? + +```java + a ⊕ b ⊕ a ⊕ b ⊕ c ⊕ c ⊕ d += ( a ⊕ a ) ⊕ ( b ⊕ b ) ⊕ ( c ⊕ c ) ⊕ d += 0 ⊕ 0 ⊕ 0 ⊕ d += d +``` + +然后我们就找出了只出现了一次的数字。 + +这道题的话,因为要寻找的是两个数字,全部异或后不是我们所要的结果。介绍一下 [这里](https://leetcode.com/problems/single-number-iii/discuss/68900/Accepted-C%2B%2BJava-O(n)-time-O(1)-space-Easy-Solution-with-Detail-Explanations) 的思路。 + +如果我们把原数组分成两组,只出现过一次的两个数字分别在两组里边,那么问题就转换成之前的老问题了,只需要这两组里的数字各自异或,答案就出来了。 + +那么通过什么把数组分成两组呢? + +放眼到二进制,我们要找的这两个数字是不同的,所以它俩至少有一位是不同的,所以我们可以根据这一位,把数组分成这一位都是 `1` 的一类和这一位都是 `0` 的一类,这样就把这两个数分到两组里了。 + +那么怎么知道那两个数字哪一位不同呢? + +回到我们异或的结果,如果把数组中的所有数字异或,最后异或的结果,其实就是我们要找的两个数字的异或。而异或结果如果某一位是 `1`,也就意味着当前位两个数字一个是 `1` ,一个是 `0`,也就找到了不同的一位。 + +思路就是上边的了,然后再考虑代码怎么写。 + +怎么把数字分类? + +我们构造一个数,把我们要找的那两个数字二进制不同的那一位写成 `1`,其它位都写 `0`,也就是 `0...0100...000` 的形式。 + +然后把构造出来的数和数组中的数字相与,如果结果是 `0`,那就意味着这个数属于当前位为 `0` 的一类。否则的话,就意味着这个数属于当前位为 `1` 的一类。 + +怎么构造 `0...0100...000` 这样的数。 + +由于我们异或得到的数可能不只一位是 `1`,可能是这样的 `0100110`,那么怎么只留一位是 `1` 呢? + +方法有很多了。 + +比如,[201 题](https://leetcode.wang/leetcode-201-Bitwise-AND-of-Numbers-Range.html) 解法三介绍的 `Integer.highestOneBit` 方法,它可以保留某个数的最高位的 `1`,其它位全部置 `0`,源码的话当时也介绍了,可以过去看一下。 + +最后,总结下我们的算法,我们通过要找的两个数字的某一位不同,将原数组分成两组,然后组内分别进行异或,最后要找的数字就是两组分别异或的结果。 + +然后举个具体的例子,来理解一下算法。 + +```java +[1,2,1,3,2,5] + +1 = 001 +2 = 010 +1 = 001 +3 = 011 +2 = 010 +5 = 101 + +把上边所有的数字异或,最后得到的结果就是 3 ^ 5 = 6 (110) + +然后对 110 调用 Integer.highestOneBit 方法就得到 100, 我们通过倒数第三位将原数组分类 + +倒数第三位为 0 的组 +1 = 001 +2 = 010 +1 = 001 +3 = 011 +2 = 010 + +倒数第三位为 1 的组 +5 = 101 + +最后组内数字依次异或即可。 +``` + +再结合代码,理解一下。 + +```java +public int[] singleNumber(int[] nums) { + int diff = 0; + for (int n : nums) { + diff ^= n; + } + diff = Integer.highestOneBit(diff); + int[] result = { 0, 0 }; + for (int n : nums) { + //当前位是 0 的组, 然后组内异或 + if ((diff & n) == 0) { + result[0] ^= n; + //当前位是 1 的组 + } else { + result[1] ^= n; + } + } + return result; +} +``` + +[这里](https://leetcode.com/problems/single-number-iii/discuss/69007/C-O(n)-time-O(1)-space-7-line-Solution-with-Detail-Explanation) 提出了一个小小的改进。 + +假如我们要找的数字是 `a` 和 `b`,一开始我们得到 `diff = a ^ b`。然后通过异或我们分别求出了 `a` 和 `b` 。 + +其实如果我们知道了 `a`,`b` 的话可以通过一次异或就能得到,`b = diff ^ a` 。 + +```java +public int[] singleNumber(int[] nums) { + int diff = 0; + for (int n : nums) { + diff ^= n; + } + int diff2 = Integer.highestOneBit(diff); + int[] result = { 0, 0 }; + for (int n : nums) { + //当前位是 0 的组, 然后组内异或 + if ((diff2 & n) == 0) { + result[0] ^= n; + } + } + result[1] = diff ^ result[0]; + return result; +} +``` + +得到只有一位 `1` 的数,除了 `Integer.highestOneBit` 的方法还有其他的做法。 + +[这里](https://leetcode.com/problems/single-number-iii/discuss/68900/Accepted-C%2B%2BJava-O(n)-time-O(1)-space-Easy-Solution-with-Detail-Explanations) 的做法。 + +```java + diff &= -diff; +``` + +取负号其实就是先取反,再加 `1`,需要 [补码](https://zhuanlan.zhihu.com/p/67227136) 的知识。最后再和原数相与就会保留最低位的 `1`。比如 `1010`,先取反是 `0101`,再加 `1`,就是 `0110`,再和 `1010` 相与,就是 `0010` 了。 + +还有 [这里](https://leetcode.com/problems/single-number-iii/discuss/68921/C%2B%2B-solution-O(n)-time-and-O(1)-space-easy-understaning-with-simple-explanation) 的做法。 + +```java + diff = (diff & (diff - 1)) ^ diff; +``` + +`n & (n - 1)` 的操作我们在 [191 题](https://leetcode.wang/leetcode-191-Number-of-1-Bits.html) 用过,它可以将最低位的 `1` 置为 `0`。比如 `1110`,先将最低位的 `1` 置为 `0` 就变成 `1100`,然后再和原数 `1110` 异或,就得到了 `0010` 。 + +还有 [这里](https://leetcode.com/problems/single-number-iii/discuss/68923/Bit-manipulation-beats-99.62) 的做法。 + +```java +diff = xor & ~(diff - 1); +``` + +先减 `1`,再取反,再相与。比如 `1010` 减 `1` 就是 `1001`,然后取反 `0110`,然后和原数 `1010` 相与,就是 `0010` 了。 + +还有 [这里](https://leetcode.com/problems/single-number-iii/discuss/342714/Best-Explanation-C%2B%2B) 的做法。 + +```java +int mask=1; +while((diff & mask)==0) +{ + mask<<=1; +} +//mask 就是我们要构造的了 +``` + +这个方法比较直接,依次判断哪一位是 `1`。 + +# 总 + +解法一的话经常用了,最容易想到的方法。 + 解法二的话,将问题转换成基本问题,这个思想经常用到,但有时候也比较难想。后边总结的得到只包含一个 `1` 的二进制的各种骚操作比较有意思。 \ No newline at end of file diff --git a/leetcode-263-Ugly-Number.md b/leetcode-263-Ugly-Number.md index f5efff832..b81503875 100644 --- a/leetcode-263-Ugly-Number.md +++ b/leetcode-263-Ugly-Number.md @@ -1,46 +1,46 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/263.png) - -判断是否是丑数,丑数的质数因子中仅含有 `2, 3, 5`。 - -# 解法一 - -可以用递归的思想去写,判断能否被 `2, 3, 5` 整除,如果能整除的话,就去递归。 - -```java -public boolean isUgly(int num) { - if (num <= 0) { - return false; - } - if (num % 2 == 0) { - return isUgly(num / 2); - } - - if (num % 3 == 0) { - return isUgly(num / 3); - } - - if (num % 5 == 0) { - return isUgly(num / 5); - } - - return num == 1; -} -``` - -还可以直接用 `while` 循环,分享 [这里](https://leetcode.com/problems/ugly-number/discuss/69342/Simplest-java-solution) 的解法。 - -```java -public boolean isUgly(int num) { - if (num <= 0) return false; - while (num % 2 == 0) num /= 2; - while (num % 3 == 0) num /= 3; - while (num % 5 == 0) num /= 5; - return num == 1; -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/263.png) + +判断是否是丑数,丑数的质数因子中仅含有 `2, 3, 5`。 + +# 解法一 + +可以用递归的思想去写,判断能否被 `2, 3, 5` 整除,如果能整除的话,就去递归。 + +```java +public boolean isUgly(int num) { + if (num <= 0) { + return false; + } + if (num % 2 == 0) { + return isUgly(num / 2); + } + + if (num % 3 == 0) { + return isUgly(num / 3); + } + + if (num % 5 == 0) { + return isUgly(num / 5); + } + + return num == 1; +} +``` + +还可以直接用 `while` 循环,分享 [这里](https://leetcode.com/problems/ugly-number/discuss/69342/Simplest-java-solution) 的解法。 + +```java +public boolean isUgly(int num) { + if (num <= 0) return false; + while (num % 2 == 0) num /= 2; + while (num % 3 == 0) num /= 3; + while (num % 5 == 0) num /= 5; + return num == 1; +} +``` + +# 总 + emm,很简单的一道题。 \ No newline at end of file diff --git a/leetcode-264-Ugly-NumberII.md b/leetcode-264-Ugly-NumberII.md index ef731ef51..cf2f6382d 100644 --- a/leetcode-264-Ugly-NumberII.md +++ b/leetcode-264-Ugly-NumberII.md @@ -1,207 +1,207 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/264.jpg) - -输出第 `n` 个丑数。 - -# 解法一 暴力 - -判断每个数字是否是丑数,然后数到第 `n` 个。 - -```java -public int nthUglyNumber(int n) { - int count = 0; - int result = 1; - while (count < n) { - if (isUgly(result)) { - count++; - } - result++; - } - //result 多加了 1 - return result - 1; -} - -public boolean isUgly(int num) { - if (num <= 0) { - return false; - } - while (num % 2 == 0) { - num /= 2; - } - while (num % 3 == 0) { - num /= 3; - } - while (num % 5 == 0) { - num /= 5; - } - return num == 1; -} -``` - -不过题目没有那么简单,这样的话会超时。 - -受到 [204 题](https://leetcode.wang/leetcode-204-Count-Primes.html) 求小于 `n` 的素数个数的启发,我们这里考虑一下筛选法。先把当时的思路粘贴过来。 - -> 用一个数组表示当前数是否是素数。 -> -> 然后从 `2` 开始,将 `2` 的倍数,`4`、`6`、`8`、`10` ...依次标记为非素数。 -> -> 下个素数 `3`,将 `3` 的倍数,`6`、`9`、`12`、`15` ...依次标记为非素数。 -> -> 下个素数 `7`,将 `7` 的倍数,`14`、`21`、`28`、`35` ...依次标记为非素数。 -> -> 在代码中,因为数组默认值是 `false` ,所以用 `false` 代表当前数是素数,用 `true` 代表当前数是非素数。 - -下边是当时的代码。 - -```java -public int countPrimes(int n) { - boolean[] notPrime = new boolean[n]; - int count = 0; - for (int i = 2; i < n; i++) { - if (!notPrime[i]) { - count++; - //将当前素数的倍数依次标记为非素数 - for (int j = 2; j * i < n; j++) { - notPrime[j * i] = true; - } - } - } - return count; -} -``` - -这里的话,所有丑数都是之前的丑数乘以 `2, 3, 5` 生成的,所以我们也可以提前把后边的丑数标记出来。这样的话,就不用调用 `isUgly` 函数判断当前是否是丑数了。 - -```java -public int nthUglyNumber(int n) { - HashSet set = new HashSet<>(); - int count = 0; - set.add(1); - int result = 1; - while (count < n) { - if (set.contains(result)) { - count++; - set.add(result * 2); - set.add(result * 3); - set.add(result * 5); - } - result++; - } - return result - 1; -} -``` - -但尴尬的是,依旧是超时,悲伤。然后就去看题解了,分享一下别人的解法。 - -# 解法二 - -参考 [这里](https://leetcode.com/problems/ugly-number-ii/discuss/69372/Java-solution-using-PriorityQueue)。 - -看一下解法一中 `set` 的方法,我们递增 `result`,然后看 `set` 中是否含有。如果含有的话,就把当前数乘以 `2, 3, 5` 继续加到 `set` 中。 - -因为 `result` 是递增的,所以我们每次找到的其实是 `set` 中最小的元素。 - -所以我们不需要一直递增 `result` ,只需要每次找 `set` 中最小的元素。找最小的元素,就可以想到优先队列了。 - -还需要注意一点,当我们从 `set` 中拿到最小的元素后,要把这个元素以及和它相等的元素都删除。 - -```java -public int nthUglyNumber(int n) { - Queue queue = new PriorityQueue(); - int count = 0; - long result = 1; - queue.add(result); - while (count < n) { - result = queue.poll(); - // 删除重复的 - while (!queue.isEmpty() && result == queue.peek()) { - queue.poll(); - } - count++; - queue.offer(result * 2); - queue.offer(result * 3); - queue.offer(result * 5); - } - return (int) result; -} -``` - -这里的话要用 `long`,不然的话如果溢出,可能会将一个负数加到队列中,最终结果也就不会准确了。 - -我们还可以用是 `TreeSet` ,这样就不用考虑重复元素了。 - -```java -public int nthUglyNumber(int n) { - TreeSet set = new TreeSet(); - int count = 0; - long result = 1; - set.add(result); - while (count < n) { - result = set.pollFirst(); - count++; - set.add(result * 2); - set.add(result * 3); - set.add(result * 5); - } - return (int) result; -} -``` - -# 解法三 - -参考 [这里](https://leetcode.com/problems/ugly-number-ii/discuss/69362/O(n)-Java-solution)。 - -我们知道丑数序列是 `1, 2, 3, 4, 5, 6, 8, 9...`。 - -我们所有的丑数都是通过之前的丑数乘以 `2, 3, 5` 生成的,所以丑数序列可以看成下边的样子。 - - `1, 1×2, 1×3, 2×2, 1×5, 2×3, 2×4, 3×3...`。 - -我们可以把丑数分成三组,用丑数序列分别乘 `2, 3, 5` 。 - -```java -乘 2: 1×2, 2×2, 3×2, 4×2, 5×2, 6×2, 8×2,9×2,… -乘 3: 1×3, 2×3, 3×3, 4×3, 5×3, 6×3, 8×3,9×3,… -乘 5: 1×5, 2×5, 3×5, 4×5, 5×5, 6×5, 8×5,9×5,… -``` - -我们需要做的就是把上边三组按照顺序合并起来。 - -合并有序数组的话,可以通过归并排序的思想,利用三个指针,每次找到三组中最小的元素,然后指针后移。 - -当然,最初我们我们并不知道丑数序列,我们可以一边更新丑数序列,一边使用丑数序列。 - -```java -public int nthUglyNumber(int n) { - int[] ugly = new int[n]; - ugly[0] = 1; // 丑数序列 - int index2 = 0, index3 = 0, index5 = 0; //三个指针 - for (int i = 1; i < n; i++) { - // 三个中选择较小的 - int factor2 = 2 * ugly[index2]; - int factor3 = 3 * ugly[index3]; - int factor5 = 5 * ugly[index5]; - int min = Math.min(Math.min(factor2, factor3), factor5); - ugly[i] = min;//更新丑数序列 - if (factor2 == min) - index2++; - if (factor3 == min) - index3++; - if (factor5 == min) - index5++; - } - return ugly[n - 1]; -} -``` - -这里需要注意的是,归并排序中我们每次从两个数组中选一个较小的,所以用的是 `if...else...`。 - -这里的话,用的是并列的 `if` , 这样如果有多组的当前值都是 `min`,指针都需要后移,从而保证 `ugly` 数组中不会加入重复元素。 - -# 总 - -解法二的话自己其实差一步就可以想到了。 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/264.jpg) + +输出第 `n` 个丑数。 + +# 解法一 暴力 + +判断每个数字是否是丑数,然后数到第 `n` 个。 + +```java +public int nthUglyNumber(int n) { + int count = 0; + int result = 1; + while (count < n) { + if (isUgly(result)) { + count++; + } + result++; + } + //result 多加了 1 + return result - 1; +} + +public boolean isUgly(int num) { + if (num <= 0) { + return false; + } + while (num % 2 == 0) { + num /= 2; + } + while (num % 3 == 0) { + num /= 3; + } + while (num % 5 == 0) { + num /= 5; + } + return num == 1; +} +``` + +不过题目没有那么简单,这样的话会超时。 + +受到 [204 题](https://leetcode.wang/leetcode-204-Count-Primes.html) 求小于 `n` 的素数个数的启发,我们这里考虑一下筛选法。先把当时的思路粘贴过来。 + +> 用一个数组表示当前数是否是素数。 +> +> 然后从 `2` 开始,将 `2` 的倍数,`4`、`6`、`8`、`10` ...依次标记为非素数。 +> +> 下个素数 `3`,将 `3` 的倍数,`6`、`9`、`12`、`15` ...依次标记为非素数。 +> +> 下个素数 `7`,将 `7` 的倍数,`14`、`21`、`28`、`35` ...依次标记为非素数。 +> +> 在代码中,因为数组默认值是 `false` ,所以用 `false` 代表当前数是素数,用 `true` 代表当前数是非素数。 + +下边是当时的代码。 + +```java +public int countPrimes(int n) { + boolean[] notPrime = new boolean[n]; + int count = 0; + for (int i = 2; i < n; i++) { + if (!notPrime[i]) { + count++; + //将当前素数的倍数依次标记为非素数 + for (int j = 2; j * i < n; j++) { + notPrime[j * i] = true; + } + } + } + return count; +} +``` + +这里的话,所有丑数都是之前的丑数乘以 `2, 3, 5` 生成的,所以我们也可以提前把后边的丑数标记出来。这样的话,就不用调用 `isUgly` 函数判断当前是否是丑数了。 + +```java +public int nthUglyNumber(int n) { + HashSet set = new HashSet<>(); + int count = 0; + set.add(1); + int result = 1; + while (count < n) { + if (set.contains(result)) { + count++; + set.add(result * 2); + set.add(result * 3); + set.add(result * 5); + } + result++; + } + return result - 1; +} +``` + +但尴尬的是,依旧是超时,悲伤。然后就去看题解了,分享一下别人的解法。 + +# 解法二 + +参考 [这里](https://leetcode.com/problems/ugly-number-ii/discuss/69372/Java-solution-using-PriorityQueue)。 + +看一下解法一中 `set` 的方法,我们递增 `result`,然后看 `set` 中是否含有。如果含有的话,就把当前数乘以 `2, 3, 5` 继续加到 `set` 中。 + +因为 `result` 是递增的,所以我们每次找到的其实是 `set` 中最小的元素。 + +所以我们不需要一直递增 `result` ,只需要每次找 `set` 中最小的元素。找最小的元素,就可以想到优先队列了。 + +还需要注意一点,当我们从 `set` 中拿到最小的元素后,要把这个元素以及和它相等的元素都删除。 + +```java +public int nthUglyNumber(int n) { + Queue queue = new PriorityQueue(); + int count = 0; + long result = 1; + queue.add(result); + while (count < n) { + result = queue.poll(); + // 删除重复的 + while (!queue.isEmpty() && result == queue.peek()) { + queue.poll(); + } + count++; + queue.offer(result * 2); + queue.offer(result * 3); + queue.offer(result * 5); + } + return (int) result; +} +``` + +这里的话要用 `long`,不然的话如果溢出,可能会将一个负数加到队列中,最终结果也就不会准确了。 + +我们还可以用是 `TreeSet` ,这样就不用考虑重复元素了。 + +```java +public int nthUglyNumber(int n) { + TreeSet set = new TreeSet(); + int count = 0; + long result = 1; + set.add(result); + while (count < n) { + result = set.pollFirst(); + count++; + set.add(result * 2); + set.add(result * 3); + set.add(result * 5); + } + return (int) result; +} +``` + +# 解法三 + +参考 [这里](https://leetcode.com/problems/ugly-number-ii/discuss/69362/O(n)-Java-solution)。 + +我们知道丑数序列是 `1, 2, 3, 4, 5, 6, 8, 9...`。 + +我们所有的丑数都是通过之前的丑数乘以 `2, 3, 5` 生成的,所以丑数序列可以看成下边的样子。 + + `1, 1×2, 1×3, 2×2, 1×5, 2×3, 2×4, 3×3...`。 + +我们可以把丑数分成三组,用丑数序列分别乘 `2, 3, 5` 。 + +```java +乘 2: 1×2, 2×2, 3×2, 4×2, 5×2, 6×2, 8×2,9×2,… +乘 3: 1×3, 2×3, 3×3, 4×3, 5×3, 6×3, 8×3,9×3,… +乘 5: 1×5, 2×5, 3×5, 4×5, 5×5, 6×5, 8×5,9×5,… +``` + +我们需要做的就是把上边三组按照顺序合并起来。 + +合并有序数组的话,可以通过归并排序的思想,利用三个指针,每次找到三组中最小的元素,然后指针后移。 + +当然,最初我们我们并不知道丑数序列,我们可以一边更新丑数序列,一边使用丑数序列。 + +```java +public int nthUglyNumber(int n) { + int[] ugly = new int[n]; + ugly[0] = 1; // 丑数序列 + int index2 = 0, index3 = 0, index5 = 0; //三个指针 + for (int i = 1; i < n; i++) { + // 三个中选择较小的 + int factor2 = 2 * ugly[index2]; + int factor3 = 3 * ugly[index3]; + int factor5 = 5 * ugly[index5]; + int min = Math.min(Math.min(factor2, factor3), factor5); + ugly[i] = min;//更新丑数序列 + if (factor2 == min) + index2++; + if (factor3 == min) + index3++; + if (factor5 == min) + index5++; + } + return ugly[n - 1]; +} +``` + +这里需要注意的是,归并排序中我们每次从两个数组中选一个较小的,所以用的是 `if...else...`。 + +这里的话,用的是并列的 `if` , 这样如果有多组的当前值都是 `min`,指针都需要后移,从而保证 `ugly` 数组中不会加入重复元素。 + +# 总 + +解法二的话自己其实差一步就可以想到了。 + 解法三又是先通过分类,然后有一些动态规划的思想,用之前的解更新当前的解。 \ No newline at end of file diff --git a/leetcode-268-Missing-Number.md b/leetcode-268-Missing-Number.md index 49a12774e..07ac9f7cc 100644 --- a/leetcode-268-Missing-Number.md +++ b/leetcode-268-Missing-Number.md @@ -1,92 +1,92 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/268.jpg) - -从 `0` 到 `n` 中找到缺失的数字。 - -# 解法一 - -最直接的方法,把所有数字存到 `HashSet` 中,然后依次判断哪个数字不存在。 - -要注意的是数组的长度其实就等于题目中 `0, 1, ..., n` 中的 `n` 。 - -```java -public int missingNumber(int[] nums) { - HashSet set = new HashSet<>(); - for (int n : nums) { - set.add(n); - } - - //判断 0 到 n 中哪个数字缺失了 - for (int i = 0; i <= nums.length; i++) { - if (!set.contains(i)) { - return i; - } - } - return -1; -} -``` - -# 解法二 - -对 [136 题](https://leetcode.wang/leetcode-136-Single-Number.html) 的解法二求和做差的方法记忆深刻,这里的话也可以用求和做差。 - -求出 `0` 到 `n` 的和,然后再计算原数组的和,做一个差就是缺失的数字了。 - -```java -public int missingNumber(int[] nums) { - int sum1 = 0; - for (int n : nums) { - sum1 += n; - } - // 等差公式计算 1 到 n 的和 - int sum2 = (1 + nums.length) * nums.length / 2; - - return sum2 - sum1; -} -``` - -# 解法三 - -又到了神奇的异或的方法了,[这里](https://leetcode.com/problems/missing-number/discuss/69791/4-Line-Simple-Java-Bit-Manipulate-Solution-with-Explaination) 的解法。 - -[136 题](https://leetcode.wang/leetcode-136-Single-Number.html) 详细的介绍了异或的一个性质,`a ⊕ a = 0`,也就是相同数字异或等于 `0`。 - -这道题的话,相当于我们有两个序列。 - -一个完整的序列, `0` 到 `n`。 - -一个是 `0` 到 `n` 中缺少了一个数字的序列。 - -把这两个序列合在一起,其实就变成了[136 题](https://leetcode.wang/leetcode-136-Single-Number.html) 的题干——所有数字都出现了两次,只有一个数字出现了一次,找出这个数字。 - -假如合起来的数字序列是 `a b a b c c d` ,`d` 出现了一次,也就是我们缺失的数字。 - -如果我们把给定的数字相互异或会发生什么呢?因为异或满足交换律和结合律,所以结果如下。 - -```java - a ⊕ b ⊕ a ⊕ b ⊕ c ⊕ c ⊕ d -= ( a ⊕ a ) ⊕ ( b ⊕ b ) ⊕ ( c ⊕ c ) ⊕ d -= 0 ⊕ 0 ⊕ 0 ⊕ d -= d -``` - -这样我们就找了缺失的数字了。 - -代码的话,我们可以把下标当成上边所说的完整的序列。因为下标没有 `n`,所以初始化 `result = n`。 - -然后把两个序列的数字依次异或即可。 - -```java -public int missingNumber(int[] nums) { - int result = nums.length; - for (int i = 0; i < nums.length; i++) { - result = result ^ nums[i] ^ i; - } - return result; -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/268.jpg) + +从 `0` 到 `n` 中找到缺失的数字。 + +# 解法一 + +最直接的方法,把所有数字存到 `HashSet` 中,然后依次判断哪个数字不存在。 + +要注意的是数组的长度其实就等于题目中 `0, 1, ..., n` 中的 `n` 。 + +```java +public int missingNumber(int[] nums) { + HashSet set = new HashSet<>(); + for (int n : nums) { + set.add(n); + } + + //判断 0 到 n 中哪个数字缺失了 + for (int i = 0; i <= nums.length; i++) { + if (!set.contains(i)) { + return i; + } + } + return -1; +} +``` + +# 解法二 + +对 [136 题](https://leetcode.wang/leetcode-136-Single-Number.html) 的解法二求和做差的方法记忆深刻,这里的话也可以用求和做差。 + +求出 `0` 到 `n` 的和,然后再计算原数组的和,做一个差就是缺失的数字了。 + +```java +public int missingNumber(int[] nums) { + int sum1 = 0; + for (int n : nums) { + sum1 += n; + } + // 等差公式计算 1 到 n 的和 + int sum2 = (1 + nums.length) * nums.length / 2; + + return sum2 - sum1; +} +``` + +# 解法三 + +又到了神奇的异或的方法了,[这里](https://leetcode.com/problems/missing-number/discuss/69791/4-Line-Simple-Java-Bit-Manipulate-Solution-with-Explaination) 的解法。 + +[136 题](https://leetcode.wang/leetcode-136-Single-Number.html) 详细的介绍了异或的一个性质,`a ⊕ a = 0`,也就是相同数字异或等于 `0`。 + +这道题的话,相当于我们有两个序列。 + +一个完整的序列, `0` 到 `n`。 + +一个是 `0` 到 `n` 中缺少了一个数字的序列。 + +把这两个序列合在一起,其实就变成了[136 题](https://leetcode.wang/leetcode-136-Single-Number.html) 的题干——所有数字都出现了两次,只有一个数字出现了一次,找出这个数字。 + +假如合起来的数字序列是 `a b a b c c d` ,`d` 出现了一次,也就是我们缺失的数字。 + +如果我们把给定的数字相互异或会发生什么呢?因为异或满足交换律和结合律,所以结果如下。 + +```java + a ⊕ b ⊕ a ⊕ b ⊕ c ⊕ c ⊕ d += ( a ⊕ a ) ⊕ ( b ⊕ b ) ⊕ ( c ⊕ c ) ⊕ d += 0 ⊕ 0 ⊕ 0 ⊕ d += d +``` + +这样我们就找了缺失的数字了。 + +代码的话,我们可以把下标当成上边所说的完整的序列。因为下标没有 `n`,所以初始化 `result = n`。 + +然后把两个序列的数字依次异或即可。 + +```java +public int missingNumber(int[] nums) { + int result = nums.length; + for (int i = 0; i < nums.length; i++) { + result = result ^ nums[i] ^ i; + } + return result; +} +``` + +# 总 + 解法一和解法二的话都是可以直接想出来,解法三异或的方法其实也不难,但还是没形成惯性,没有往异或思考。 \ No newline at end of file diff --git a/leetcode-273-Intege-to-English-Words.md b/leetcode-273-Intege-to-English-Words.md index b77626da8..66c06bf48 100644 --- a/leetcode-273-Intege-to-English-Words.md +++ b/leetcode-273-Intege-to-English-Words.md @@ -1,75 +1,75 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/273.png) - -将数字用英文单词表示。 - -# 思路分析 - -没有什么特殊的方法,分析规律就可以了,主要有几个点。 - -* 每三位一组 -* 小于 `20` 的和大于 `20` 的分开考虑 -* 单词之间空格的处理 -* 每三位后边增加个单位,从右数除了第一组,以后每一组后边依次加单位, `Thousand", "Million", "Billion"` -* 我们从右到左遍历,是在倒着完善结果 - -# 解法一 - -空格的处理,在每个单词前加空格,最后返回结果的时候调用 `trim` 函数去掉头尾的空格。 - -倒着遍历的处理,利用 `insert` 函数,每次在 `0` 的位置插入单词。 - -下边的代码供参考,每个人的代码写出来应该都不同。 - -```java -public String numberToWords(int num) { - if (num == 0) { - return "Zero"; - } - //个位和十位 - String[] nums1 = { "", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", - "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen" }; - - //十位 - String[] nums2 = { "", "", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety" }; - - //单位 - String[] nums3 = { "", "Thousand", "Million", "Billion" }; - StringBuilder result = new StringBuilder(); - int count = 0; // 记录第几组,方便加单位 - while (num > 0) { - int threeNum = num % 1000; - //当前组大于 0 才加单位 - if (threeNum > 0) { - result.insert(0, " " + nums3[count]); - } - count++; - int twoNum = num % 100; - if (twoNum < 20) { - //小于 20 两位同时考虑 - if (twoNum > 0) { - result.insert(0, " " + nums1[twoNum]); - } - } else { - //个位 - if (twoNum % 10 > 0) { - result.insert(0, " " + nums1[twoNum % 10]); - } - //十位 - result.insert(0, " " + nums2[twoNum / 10]); - } - //百位 - if (threeNum >= 100) { - result.insert(0, " Hundred"); - result.insert(0, " " + nums1[threeNum / 100]); - } - num /= 1000; - } - return result.toString().trim(); -} -``` - -# 总 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/273.png) + +将数字用英文单词表示。 + +# 思路分析 + +没有什么特殊的方法,分析规律就可以了,主要有几个点。 + +* 每三位一组 +* 小于 `20` 的和大于 `20` 的分开考虑 +* 单词之间空格的处理 +* 每三位后边增加个单位,从右数除了第一组,以后每一组后边依次加单位, `Thousand", "Million", "Billion"` +* 我们从右到左遍历,是在倒着完善结果 + +# 解法一 + +空格的处理,在每个单词前加空格,最后返回结果的时候调用 `trim` 函数去掉头尾的空格。 + +倒着遍历的处理,利用 `insert` 函数,每次在 `0` 的位置插入单词。 + +下边的代码供参考,每个人的代码写出来应该都不同。 + +```java +public String numberToWords(int num) { + if (num == 0) { + return "Zero"; + } + //个位和十位 + String[] nums1 = { "", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", + "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen" }; + + //十位 + String[] nums2 = { "", "", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety" }; + + //单位 + String[] nums3 = { "", "Thousand", "Million", "Billion" }; + StringBuilder result = new StringBuilder(); + int count = 0; // 记录第几组,方便加单位 + while (num > 0) { + int threeNum = num % 1000; + //当前组大于 0 才加单位 + if (threeNum > 0) { + result.insert(0, " " + nums3[count]); + } + count++; + int twoNum = num % 100; + if (twoNum < 20) { + //小于 20 两位同时考虑 + if (twoNum > 0) { + result.insert(0, " " + nums1[twoNum]); + } + } else { + //个位 + if (twoNum % 10 > 0) { + result.insert(0, " " + nums1[twoNum % 10]); + } + //十位 + result.insert(0, " " + nums2[twoNum / 10]); + } + //百位 + if (threeNum >= 100) { + result.insert(0, " Hundred"); + result.insert(0, " " + nums1[threeNum / 100]); + } + num /= 1000; + } + return result.toString().trim(); +} +``` + +# 总 + 主要就是对问题的梳理,不是很难。 \ No newline at end of file diff --git a/leetcode-274-H-Index.md b/leetcode-274-H-Index.md index fd39d35ff..4574631f2 100644 --- a/leetcode-274-H-Index.md +++ b/leetcode-274-H-Index.md @@ -1,197 +1,197 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/274.jpg) - -求 `H` 指数,`H` 指数等于 `n`,代表该作者所发表的所有论文中至少有 `n` 篇论文的被引用次数大于等于 `n`。 - -# 解法一 - -第一次看到这个概念比较难理解,看一下 [维基百科]([https://zh.wikipedia.org/zh-hans/H%E6%8C%87%E6%95%B0](https://zh.wikipedia.org/zh-hans/H指数)) 的定义。 - -> H指数的计算基于其研究者的论文数量及其论文被引用的次数。赫希认为:一个人在其所有学术文章中有N篇论文分别被引用了至少N次,他的H指数就是N。如[美国](https://zh.wikipedia.org/wiki/美国)[耶鲁大学](https://zh.wikipedia.org/wiki/耶鲁大学)免疫学家[理查德·弗来沃](https://zh.wikipedia.org/w/index.php?title=理查德·弗来沃&action=edit&redlink=1)发表的900篇文章中,有107篇被引用了107次以上,他的H指数是107。 -> -> 可以按照如下方法确定某人的H指数: -> -> 1. 将其发表的所有[SCI](https://zh.wikipedia.org/wiki/科学引文索引)论文按被引次数从高到低排序; -> 2. 从前往后查找排序后的列表,直到某篇论文的序号大于该论文被引次数。所得序号减一即为H指数。 - -我们先按照上边提供的解法写一下代码。 - -```java -public int hIndex(int[] citations) { - Arrays.sort(citations); // 默认的是从小到大排序,所以后边要倒着遍历 - int n = 1; // 论文序号 - //倒着遍历就是从大到小遍历了 - for (int i = citations.length - 1; i >= 0; i--) { - // 论文序号大于该论文的被引次数 - if (n > citations[i]) { - break; - } - n++; - } - // 所得序号减一即为 H 指数。 - return n - 1; -} -``` - -我们结合下图理解一下上边的算法,把 `[3,0,6,1,5]` 从大到小排序,画到图中。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/274_2.jpg) - -上边的 `H-Index` 是 `3`,在图中表现的话就是有 `3` 个点在直线上方(包括在直线上),其他点在直线下方。 - -我们从大到小排序后,其实就是依次判断点是否在直线上方(包括在直线上),如果出现了点在直线下方,那么前一个点的横坐标就是我们要找的 `H-Index`。 - -我们也可以从小到大遍历,结合下图。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/274_3.jpg) - -我们从 `0` 开始遍历,依次判断点是否在直线下方,如果出现了点在直线上方(包括在直线上),那么当前点的垂线与直线交点的纵坐标就是 `H-Index` 了。 - -点的垂线与直线交点的纵坐标的求法是 `n - i`,`n` 是数组长度,`i` 是数组下标。 - -这就是 [这里](https://leetcode.com/problems/h-index/discuss/70897/Python-O(n-lgn)-time-with-sort-O(n)-time-with-O(n)-space) 代码的理解了。 - -```java -public int hIndex(int[] citations) { - Arrays.sort(citations); - int n = citations.length; - for (int i = 0; i < n; i++) { - // 点在直线上方 - if (citations[i] >= n - i) { - return n - i; - } - } - return 0; -} -``` - -# 解法二 - -参考 [这里](https://leetcode.com/problems/h-index/discuss/70768/Java-bucket-sort-O(n)-solution-with-detail-explanation) ,换一种思路理解。 - -首先如果数组的长度是 `n`,那么 `H-Index` 最大也就是 `n`。 - -我们先判断 `H-Index` 是不是 `n`,如果被引次数大于等于 `n` 的论文数大于等于 `n`,那么 `H-Index` 就是 `n`。 - -否则的话判断 `H-Index` 是不是 `n - 1`,如果被引次数大于等于 `n - 1` 的论文数大于等于 `n - 1`,那么 `H-Index` 就是 `n - 1`。 - -否则的话判断 `H-Index` 是不是 `n - 2`,如果被引次数大于等于 `n - 2` 的论文数大于等于 `n - 2`,那么 `H-Index` 就是 `n - 2`。 - -... ... - -否则的话判断 `H-Index` 是不是 `1`,如果被引次数大于等于 `1` 的论文数大于等于 `1`,那么 `H-Index` 就是 `1`。 - -否则的话判断 `H-Index` 是不是 `0`,如果被引次数大于等于 `0` 的论文数大于等于 `0`,那么 `H-Index` 就是 `0`。 - -接下来的话有用到 [计数排序](https://www.runoob.com/w3cnote/counting-sort.html) 的思想。 - -上边的算法中,我们每次想要知道「被引次数大于等于 `N` 的论文数」, `N = n, n - 1, n - 2 ... 0` 。 - -如果我们知道了被引次数等于 `0` 的论文数,被引次数等于 `1` 的论文数,被引次数等于 `2` 的论文数 ... 被引次数等于 `n - 1` 的论文数,那么通过累加,被引次数大于等于 `0` 到被引次数大于等于 `n - 1` 的论文数也就知道了。 - -因为我们只关心被引次数大于等于 `n` 的论文数,所以被引次数等于 `n` 的论文数,所以被引次数等于 `n + 1` 的论文数,所以被引次数等于 `n + 2` 的论文数... 都不是我们关心的,我们只需要记录被引次数大于等于 `n` 的论文数。 - -综上,我们需要一个额外空间,分别存储被引次数等于 `0` 的论文数,被引次数等于 `1` 的论文数,被引次数等于 `2` 的论文数 ... 被引次数等于 `n - 1` 的论文数以及被引次数大于等于 `n` 的论文数。 - -然后回到算法最开始,依次判断被引次数大于等于 `N` 的论文数是否大于等于 `N` 即可, `N = n, n - 1, n - 2 ... 0` 。 - -```java -public int hIndex(int[] citations) { - int n = citations.length; - int[] buckets = new int[n+1]; - //计数 - for(int c : citations) { - if(c >= n) { - buckets[n]++; - } else { - buckets[c]++; - } - } - int count = 0; - //依次判断被引次数大于等于 N 的论文数是否大于等于 N - for(int i = n; i >= 0; i--) { - count += buckets[i]; - if(count >= i) { - return i; - } - } - return 0; -} -``` - -参考 [这里](https://leetcode.com/problems/h-index/discuss/70823/O(N)-time-O(1)space-solution),我们还能进一步的优化,我们可以利用原有的数组 `citations` 计数,不再开辟新的空间 `buckets`。 - -用原有数组计数的话,假设 `citations[0] = 3`,那么我们应该将 `citations[3] = 1`。但如果我们遍历到了 `citations[3]` 的时候,此时它代表的是被引用次数等于 `3` 的论文数,而不是当前论文的被引用次数。 - -所以我们需要区分当前数字是在计数还是表示论文的被引用次数。 - -有一个 `trick`,注意到论文被引用次数都是非负数,所以我们可以用负数计数。用 `-1` 代表 `0`, `-2` 代表 `1`, `-3` 代表 `2`... 以此类推。这样的话,从负数到它的原本含义的映射就是「先取相反数,然后再减一」。 - -如果当前是 `-4` ,那么它代表 `-(-4) - 1 = 3`。 - -取相反数,在 [补码](https://zhuanlan.zhihu.com/p/67227136) 中讨论过,可以通过取反加一替代,代入原来的映射 「先取相反数,然后再减一」,就是 「先取反加一,然后再减一」,所以就是 「取反」即可。 - -还有个问题需要解决。 - -假设 `citations[0] = 3`,那么我们应该将 `citations[3] = -2`,如果直接这样做的话,`citations[3]` 之前的数就被替代了。所以替代前,我们还需要将 `citations[3]` 存起来,然后重复这步。 - -还有一些细节,可以结合代码看,文字有些难描述。 - -因为我们数组大小是 `n`,那么只能统计 `0` 的 `n - 1` 的情况,还需要一个变量单独记录被引用数大于等于 `n` 的论文数。 - -```java -public int hIndex(int[] citations) { - int n = citations.length; - int N = 0; // 记录引用数大于等于 n 的论文数 - for (int i = 0; i < n; i++) { - int count = citations[i]; - //已经记录过这个数 - if (count < 0) { - continue; - } - //初始化为 0 - citations[i] = -1; // -1 -> 0 - //大于等于 n 的情况,用 N 统计 - if (count >= n) { - N++; - continue; - } - //当前值之前是否被统计过 - while (citations[count] >= 0) { - //保存当前论文被引用次数 - int temp = citations[count]; - //统计当前数,初始化为 1 - citations[count] = -2; // -2 -> 1 - count = temp; - //大于等于 n 的情况 - if (count >= n) { - N++; - break; - } - } - //当前值之前已经被统计过,在原来的基础上减一(也就是计数加 1) - if (count < n && citations[count] < 0) { - citations[count]--; - } - } - - // 全部论文引用数大于等于 n - if (N == n) { - return n; - } - int count = N; - for (int i = n - 1; i >= 0; i--) { - count = count + (~citations[i]); - if (count >= i) { - return i; - } - } - return 0; -} -``` - -# 总 - -这道题的话,如果知道定义的话很好写。然后解法二利用原有空间的思想进行优化也经常用到。 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/274.jpg) + +求 `H` 指数,`H` 指数等于 `n`,代表该作者所发表的所有论文中至少有 `n` 篇论文的被引用次数大于等于 `n`。 + +# 解法一 + +第一次看到这个概念比较难理解,看一下 [维基百科]([https://zh.wikipedia.org/zh-hans/H%E6%8C%87%E6%95%B0](https://zh.wikipedia.org/zh-hans/H指数)) 的定义。 + +> H指数的计算基于其研究者的论文数量及其论文被引用的次数。赫希认为:一个人在其所有学术文章中有N篇论文分别被引用了至少N次,他的H指数就是N。如[美国](https://zh.wikipedia.org/wiki/美国)[耶鲁大学](https://zh.wikipedia.org/wiki/耶鲁大学)免疫学家[理查德·弗来沃](https://zh.wikipedia.org/w/index.php?title=理查德·弗来沃&action=edit&redlink=1)发表的900篇文章中,有107篇被引用了107次以上,他的H指数是107。 +> +> 可以按照如下方法确定某人的H指数: +> +> 1. 将其发表的所有[SCI](https://zh.wikipedia.org/wiki/科学引文索引)论文按被引次数从高到低排序; +> 2. 从前往后查找排序后的列表,直到某篇论文的序号大于该论文被引次数。所得序号减一即为H指数。 + +我们先按照上边提供的解法写一下代码。 + +```java +public int hIndex(int[] citations) { + Arrays.sort(citations); // 默认的是从小到大排序,所以后边要倒着遍历 + int n = 1; // 论文序号 + //倒着遍历就是从大到小遍历了 + for (int i = citations.length - 1; i >= 0; i--) { + // 论文序号大于该论文的被引次数 + if (n > citations[i]) { + break; + } + n++; + } + // 所得序号减一即为 H 指数。 + return n - 1; +} +``` + +我们结合下图理解一下上边的算法,把 `[3,0,6,1,5]` 从大到小排序,画到图中。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/274_2.jpg) + +上边的 `H-Index` 是 `3`,在图中表现的话就是有 `3` 个点在直线上方(包括在直线上),其他点在直线下方。 + +我们从大到小排序后,其实就是依次判断点是否在直线上方(包括在直线上),如果出现了点在直线下方,那么前一个点的横坐标就是我们要找的 `H-Index`。 + +我们也可以从小到大遍历,结合下图。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/274_3.jpg) + +我们从 `0` 开始遍历,依次判断点是否在直线下方,如果出现了点在直线上方(包括在直线上),那么当前点的垂线与直线交点的纵坐标就是 `H-Index` 了。 + +点的垂线与直线交点的纵坐标的求法是 `n - i`,`n` 是数组长度,`i` 是数组下标。 + +这就是 [这里](https://leetcode.com/problems/h-index/discuss/70897/Python-O(n-lgn)-time-with-sort-O(n)-time-with-O(n)-space) 代码的理解了。 + +```java +public int hIndex(int[] citations) { + Arrays.sort(citations); + int n = citations.length; + for (int i = 0; i < n; i++) { + // 点在直线上方 + if (citations[i] >= n - i) { + return n - i; + } + } + return 0; +} +``` + +# 解法二 + +参考 [这里](https://leetcode.com/problems/h-index/discuss/70768/Java-bucket-sort-O(n)-solution-with-detail-explanation) ,换一种思路理解。 + +首先如果数组的长度是 `n`,那么 `H-Index` 最大也就是 `n`。 + +我们先判断 `H-Index` 是不是 `n`,如果被引次数大于等于 `n` 的论文数大于等于 `n`,那么 `H-Index` 就是 `n`。 + +否则的话判断 `H-Index` 是不是 `n - 1`,如果被引次数大于等于 `n - 1` 的论文数大于等于 `n - 1`,那么 `H-Index` 就是 `n - 1`。 + +否则的话判断 `H-Index` 是不是 `n - 2`,如果被引次数大于等于 `n - 2` 的论文数大于等于 `n - 2`,那么 `H-Index` 就是 `n - 2`。 + +... ... + +否则的话判断 `H-Index` 是不是 `1`,如果被引次数大于等于 `1` 的论文数大于等于 `1`,那么 `H-Index` 就是 `1`。 + +否则的话判断 `H-Index` 是不是 `0`,如果被引次数大于等于 `0` 的论文数大于等于 `0`,那么 `H-Index` 就是 `0`。 + +接下来的话有用到 [计数排序](https://www.runoob.com/w3cnote/counting-sort.html) 的思想。 + +上边的算法中,我们每次想要知道「被引次数大于等于 `N` 的论文数」, `N = n, n - 1, n - 2 ... 0` 。 + +如果我们知道了被引次数等于 `0` 的论文数,被引次数等于 `1` 的论文数,被引次数等于 `2` 的论文数 ... 被引次数等于 `n - 1` 的论文数,那么通过累加,被引次数大于等于 `0` 到被引次数大于等于 `n - 1` 的论文数也就知道了。 + +因为我们只关心被引次数大于等于 `n` 的论文数,所以被引次数等于 `n` 的论文数,所以被引次数等于 `n + 1` 的论文数,所以被引次数等于 `n + 2` 的论文数... 都不是我们关心的,我们只需要记录被引次数大于等于 `n` 的论文数。 + +综上,我们需要一个额外空间,分别存储被引次数等于 `0` 的论文数,被引次数等于 `1` 的论文数,被引次数等于 `2` 的论文数 ... 被引次数等于 `n - 1` 的论文数以及被引次数大于等于 `n` 的论文数。 + +然后回到算法最开始,依次判断被引次数大于等于 `N` 的论文数是否大于等于 `N` 即可, `N = n, n - 1, n - 2 ... 0` 。 + +```java +public int hIndex(int[] citations) { + int n = citations.length; + int[] buckets = new int[n+1]; + //计数 + for(int c : citations) { + if(c >= n) { + buckets[n]++; + } else { + buckets[c]++; + } + } + int count = 0; + //依次判断被引次数大于等于 N 的论文数是否大于等于 N + for(int i = n; i >= 0; i--) { + count += buckets[i]; + if(count >= i) { + return i; + } + } + return 0; +} +``` + +参考 [这里](https://leetcode.com/problems/h-index/discuss/70823/O(N)-time-O(1)space-solution),我们还能进一步的优化,我们可以利用原有的数组 `citations` 计数,不再开辟新的空间 `buckets`。 + +用原有数组计数的话,假设 `citations[0] = 3`,那么我们应该将 `citations[3] = 1`。但如果我们遍历到了 `citations[3]` 的时候,此时它代表的是被引用次数等于 `3` 的论文数,而不是当前论文的被引用次数。 + +所以我们需要区分当前数字是在计数还是表示论文的被引用次数。 + +有一个 `trick`,注意到论文被引用次数都是非负数,所以我们可以用负数计数。用 `-1` 代表 `0`, `-2` 代表 `1`, `-3` 代表 `2`... 以此类推。这样的话,从负数到它的原本含义的映射就是「先取相反数,然后再减一」。 + +如果当前是 `-4` ,那么它代表 `-(-4) - 1 = 3`。 + +取相反数,在 [补码](https://zhuanlan.zhihu.com/p/67227136) 中讨论过,可以通过取反加一替代,代入原来的映射 「先取相反数,然后再减一」,就是 「先取反加一,然后再减一」,所以就是 「取反」即可。 + +还有个问题需要解决。 + +假设 `citations[0] = 3`,那么我们应该将 `citations[3] = -2`,如果直接这样做的话,`citations[3]` 之前的数就被替代了。所以替代前,我们还需要将 `citations[3]` 存起来,然后重复这步。 + +还有一些细节,可以结合代码看,文字有些难描述。 + +因为我们数组大小是 `n`,那么只能统计 `0` 的 `n - 1` 的情况,还需要一个变量单独记录被引用数大于等于 `n` 的论文数。 + +```java +public int hIndex(int[] citations) { + int n = citations.length; + int N = 0; // 记录引用数大于等于 n 的论文数 + for (int i = 0; i < n; i++) { + int count = citations[i]; + //已经记录过这个数 + if (count < 0) { + continue; + } + //初始化为 0 + citations[i] = -1; // -1 -> 0 + //大于等于 n 的情况,用 N 统计 + if (count >= n) { + N++; + continue; + } + //当前值之前是否被统计过 + while (citations[count] >= 0) { + //保存当前论文被引用次数 + int temp = citations[count]; + //统计当前数,初始化为 1 + citations[count] = -2; // -2 -> 1 + count = temp; + //大于等于 n 的情况 + if (count >= n) { + N++; + break; + } + } + //当前值之前已经被统计过,在原来的基础上减一(也就是计数加 1) + if (count < n && citations[count] < 0) { + citations[count]--; + } + } + + // 全部论文引用数大于等于 n + if (N == n) { + return n; + } + int count = N; + for (int i = n - 1; i >= 0; i--) { + count = count + (~citations[i]); + if (count >= i) { + return i; + } + } + return 0; +} +``` + +# 总 + +这道题的话,如果知道定义的话很好写。然后解法二利用原有空间的思想进行优化也经常用到。 + diff --git a/leetcode-275-H-IndexII.md b/leetcode-275-H-IndexII.md index 496fc258b..0a1f477cf 100644 --- a/leetcode-275-H-IndexII.md +++ b/leetcode-275-H-IndexII.md @@ -1,65 +1,65 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/275.png) - -求 `H-Index`,和 [上一道题](https://leetcode.wang/leetcode-274-H-Index.html) 一样,只不过这道题给定的数组是有序的,详细的可以先做一下上一题。 - -# 解法一 - - 先看一下之前的其中一个的做法。 - -![img](https://windliang.oss-cn-beijing.aliyuncs.com/274_3.jpg) - -我们从 `0` 开始遍历,依次判断点是否在直线下方,如果出现了点在直线上方(包括在直线上),那么当前点的垂线与直线交点的纵坐标就是 `H-Index` 了。 - -点的垂线与直线交点的纵坐标的求法是 `n - i`,`n` 是数组长度,`i` 是数组下标。 - -代码如下。 - -```java -public int hIndex(int[] citations) { - Arrays.sort(citations); - int n = citations.length; - for (int i = 0; i < n; i++) { - // 点在直线上方 - if (citations[i] >= n - i) { - return n - i; - } - } - return 0; -} - -``` - -说白了,我们是要寻找**第一个**在直线上方(包括在直线上)的点,给定数组是有序的,所以我们可以用二分查找。 - -```java -public int hIndex(int[] citations) { - int n = citations.length; - int low = 0; - int high = n - 1; - while (low <= high) { - int mid = (low + high) >>> 1; - //在直线上方 - if (citations[mid] >= n - mid) { - if (mid == 0) { - return n; - } - //前一个点是否在直线下方 - int before = mid - 1; - if (citations[before] < n - before) { - return n - mid; - } - - high = mid - 1; - } else { - low = mid + 1; - } - } - return 0; -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/275.png) + +求 `H-Index`,和 [上一道题](https://leetcode.wang/leetcode-274-H-Index.html) 一样,只不过这道题给定的数组是有序的,详细的可以先做一下上一题。 + +# 解法一 + + 先看一下之前的其中一个的做法。 + +![img](https://windliang.oss-cn-beijing.aliyuncs.com/274_3.jpg) + +我们从 `0` 开始遍历,依次判断点是否在直线下方,如果出现了点在直线上方(包括在直线上),那么当前点的垂线与直线交点的纵坐标就是 `H-Index` 了。 + +点的垂线与直线交点的纵坐标的求法是 `n - i`,`n` 是数组长度,`i` 是数组下标。 + +代码如下。 + +```java +public int hIndex(int[] citations) { + Arrays.sort(citations); + int n = citations.length; + for (int i = 0; i < n; i++) { + // 点在直线上方 + if (citations[i] >= n - i) { + return n - i; + } + } + return 0; +} + +``` + +说白了,我们是要寻找**第一个**在直线上方(包括在直线上)的点,给定数组是有序的,所以我们可以用二分查找。 + +```java +public int hIndex(int[] citations) { + int n = citations.length; + int low = 0; + int high = n - 1; + while (low <= high) { + int mid = (low + high) >>> 1; + //在直线上方 + if (citations[mid] >= n - mid) { + if (mid == 0) { + return n; + } + //前一个点是否在直线下方 + int before = mid - 1; + if (citations[before] < n - before) { + return n - mid; + } + + high = mid - 1; + } else { + low = mid + 1; + } + } + return 0; +} +``` + +# 总 + 主要就是二分查找的应用了。 \ No newline at end of file diff --git a/leetcode-278-First-Bad-Version.md b/leetcode-278-First-Bad-Version.md index 55b876b65..b21603159 100644 --- a/leetcode-278-First-Bad-Version.md +++ b/leetcode-278-First-Bad-Version.md @@ -1,152 +1,152 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/278.jpg) - -题目说的比较绕,其实就是在 `false false false true true` 这样的序列中找出第一次出现 `true` 的位置。 - -可以通过 `isBadVersion` 函数得到当前位置是 `false` 还是 `true`。 - -# 解法一 - -最直接的解法,从 `1` 开始遍历,依次判断是否是 `true`。 - -```java -public int firstBadVersion(int n) { - for (int i = 1; i < n; i++) { - if (isBadVersion(i)) { - return i; - } - } - return -1; -} -``` - -没想到这个解法竟然会超时。 - -# 解法二 - -把 `false false false true true` 可以想成有序数组,`0 0 0 1 1`,寻找第一次出现 `1` 的位置。 - -自然会想到二分查找了,和 [275 题](https://leetcode.wang/leetcode-275-H-IndexII.html) 解法是一样的,当时是找到第一个出现在直线上方的点。 - -```java -public int firstBadVersion(int n) { - int low = 1; - int high = n; - while (low <= high) { - int mid = (low + high) >>> 1; - if (isBadVersion(mid)) { - if (mid == 1) { - return 1; - } - //判断前一个是否是 false - if (!isBadVersion(mid - 1)) { - return mid; - } - high = mid - 1; - } else { - low = mid + 1; - } - } - return -1; -} -``` - -和 [275 题](https://leetcode.wang/leetcode-275-H-IndexII.html) 一样,我觉得上边的解法比较好理解也就没写其他的写法了,没想到又碰到这种题了,那顺便再说一下其他的写法吧。 - -上边是采取提前结束的方法,事实上,因为数组中一定会有一个 `true` ,所以我们确信一定会找到我们要寻找的值。 - -所以我们可以通过不断的缩小范围,直到数组中只剩下一个位置,那么这个位置就一定是我们要找的。 - -下边的解法保证每次循环我们要找到解都在 `low` 和 `high` 之间,从而当 `low == high` 的时候,此时剩下的最后一个数就是我们要找的了。 - -```java -public int firstBadVersion(int n) { - int low = 1; - int high = n; - //这里去除等于,只剩一个值的时候就跳出来 - while (low < high) { - int mid = (low + high) >>> 1; - if (isBadVersion(mid)) { - //这里不再是 mid - 1, 因为 mid 有可能是我们要找的值 - high = mid; - } else { - low = mid + 1; - } - } - return low; -} -``` - -还有一种写法,比较反直觉。 - -```java -public int firstBadVersion(int n) { - int low = 1; - int high = n;= - while (low <= high) { - int mid = (low + high) >>> 1; - if (isBadVersion(mid)) { - high = mid - 1; - } else { - low = mid + 1; - } - } - return low; -} -``` - -`high = mid - 1` ,看起来会把我们要找的值丢掉。其实是没有关系的,如果 `mid` 值是我们要找的,那么后续 `low` 会不断向 `high` 靠近,当 `low` 和 `high` 相等的时候,`low` 最后更新 `low = mid + 1` ,刚好又回到了我们要找的值。 - -还有一种情况,如果之前一直没有找到我们要找的值,直到最后一步 `low == high` 的时候才找到。此时会进入 `if` 语句,更新 `high = mid - 1` 错过我们要找的值。但没有关系,我们返回的是 `low` ,依旧是我们要找的值。 - -# 扩展 求中点 - -在 [108 题](https://leetcode.wang/leetcode-108-Convert-Sorted-Array-to-Binary-Search-Tree.html) 已经说过这个扩展了,由于经常用到,这里再贴过来,如果不清楚的话可以看一下。 - -前几天和同学发现个有趣的事情,分享一下。 - -首先假设我们的变量都是 `int` 值。 - -二分查找中我们需要根据 `start` 和 `end` 求中点,正常情况下加起来除以 2 即可。 - -```java -int mid = (start + end) / 2 -``` - -但这样有一个缺点,我们知道`int`的最大值是 `Integer.MAX_VALUE` ,也就是`2147483647`。那么有一个问题,如果 `start = 2147483645`,`end = 2147483645`,虽然 `start` 和 `end`都没有超出最大值,但是如果利用上边的公式,加起来的话就会造成溢出,从而导致`mid`计算错误。 - -解决的一个方案就是利用数学上的技巧,我们可以加一个 `start` 再减一个 `start` 将公式变形。 - -```java -(start + end) / 2 = (start + end + start - start) / 2 = start + (end - start) / 2 -``` - -这样的话,就解决了上边的问题。 - -然后当时和同学看到`jdk`源码中,求`mid`的方法如下 - -```java -int mid = (start + end) >>> 1 -``` - -它通过移位实现了除以 2,但。。。这样难道不会导致溢出吗? - -首先大家可以补一下 [补码](https://mp.weixin.qq.com/s/uvcQHJi6AXhPDJL-6JWUkw) 的知识。 - -其实问题的关键就是这里了`>>>` ,我们知道还有一种右移是`>>`。区别在于`>>`为有符号右移,右移以后最高位保持原来的最高位。而 `>>>` 这个右移的话最高位补 0。 - -所以这里其实利用到了整数的补码形式,最高位其实是符号位,所以当 `start + end` 溢出的时候,其实本质上只是符号位收到了进位,而`>>>`这个右移不仅可以把符号位右移,同时最高位只是补零,不会对数字的大小造成影响。 - -但 `>>` 有符号右移就会出现问题了,事实上 JDK6 之前都用的`>>`,这个 BUG 在 java 里竟然隐藏了十年之久。 - -# 总 - -还是典型的二分查找的应用。解法一就不说了,说一下解法二的三种写法。 - -我觉得第一种写法比较直观,每次判断一下我们是否找到了,可以提前结束。 - -第二种写法的话也经常用到,比如 [33 题](https://leetcode.wang/leetCode-33-Search-in-Rotated-Sorted-Array.html) 找最小值下标的时候,当时和 [@为爱卖小菜](https://leetcode-cn.com/u/wei-ai-mai-xiao-cai/) 讨论了很多,自己对二分理解也深刻了不少。这种写法用于一定可以找到解的时候,一定要注意的是 `low < high`,不能加等号,不然可能造成死循环。 - -第三种写法的话,这里就不推荐了。 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/278.jpg) + +题目说的比较绕,其实就是在 `false false false true true` 这样的序列中找出第一次出现 `true` 的位置。 + +可以通过 `isBadVersion` 函数得到当前位置是 `false` 还是 `true`。 + +# 解法一 + +最直接的解法,从 `1` 开始遍历,依次判断是否是 `true`。 + +```java +public int firstBadVersion(int n) { + for (int i = 1; i < n; i++) { + if (isBadVersion(i)) { + return i; + } + } + return -1; +} +``` + +没想到这个解法竟然会超时。 + +# 解法二 + +把 `false false false true true` 可以想成有序数组,`0 0 0 1 1`,寻找第一次出现 `1` 的位置。 + +自然会想到二分查找了,和 [275 题](https://leetcode.wang/leetcode-275-H-IndexII.html) 解法是一样的,当时是找到第一个出现在直线上方的点。 + +```java +public int firstBadVersion(int n) { + int low = 1; + int high = n; + while (low <= high) { + int mid = (low + high) >>> 1; + if (isBadVersion(mid)) { + if (mid == 1) { + return 1; + } + //判断前一个是否是 false + if (!isBadVersion(mid - 1)) { + return mid; + } + high = mid - 1; + } else { + low = mid + 1; + } + } + return -1; +} +``` + +和 [275 题](https://leetcode.wang/leetcode-275-H-IndexII.html) 一样,我觉得上边的解法比较好理解也就没写其他的写法了,没想到又碰到这种题了,那顺便再说一下其他的写法吧。 + +上边是采取提前结束的方法,事实上,因为数组中一定会有一个 `true` ,所以我们确信一定会找到我们要寻找的值。 + +所以我们可以通过不断的缩小范围,直到数组中只剩下一个位置,那么这个位置就一定是我们要找的。 + +下边的解法保证每次循环我们要找到解都在 `low` 和 `high` 之间,从而当 `low == high` 的时候,此时剩下的最后一个数就是我们要找的了。 + +```java +public int firstBadVersion(int n) { + int low = 1; + int high = n; + //这里去除等于,只剩一个值的时候就跳出来 + while (low < high) { + int mid = (low + high) >>> 1; + if (isBadVersion(mid)) { + //这里不再是 mid - 1, 因为 mid 有可能是我们要找的值 + high = mid; + } else { + low = mid + 1; + } + } + return low; +} +``` + +还有一种写法,比较反直觉。 + +```java +public int firstBadVersion(int n) { + int low = 1; + int high = n;= + while (low <= high) { + int mid = (low + high) >>> 1; + if (isBadVersion(mid)) { + high = mid - 1; + } else { + low = mid + 1; + } + } + return low; +} +``` + +`high = mid - 1` ,看起来会把我们要找的值丢掉。其实是没有关系的,如果 `mid` 值是我们要找的,那么后续 `low` 会不断向 `high` 靠近,当 `low` 和 `high` 相等的时候,`low` 最后更新 `low = mid + 1` ,刚好又回到了我们要找的值。 + +还有一种情况,如果之前一直没有找到我们要找的值,直到最后一步 `low == high` 的时候才找到。此时会进入 `if` 语句,更新 `high = mid - 1` 错过我们要找的值。但没有关系,我们返回的是 `low` ,依旧是我们要找的值。 + +# 扩展 求中点 + +在 [108 题](https://leetcode.wang/leetcode-108-Convert-Sorted-Array-to-Binary-Search-Tree.html) 已经说过这个扩展了,由于经常用到,这里再贴过来,如果不清楚的话可以看一下。 + +前几天和同学发现个有趣的事情,分享一下。 + +首先假设我们的变量都是 `int` 值。 + +二分查找中我们需要根据 `start` 和 `end` 求中点,正常情况下加起来除以 2 即可。 + +```java +int mid = (start + end) / 2 +``` + +但这样有一个缺点,我们知道`int`的最大值是 `Integer.MAX_VALUE` ,也就是`2147483647`。那么有一个问题,如果 `start = 2147483645`,`end = 2147483645`,虽然 `start` 和 `end`都没有超出最大值,但是如果利用上边的公式,加起来的话就会造成溢出,从而导致`mid`计算错误。 + +解决的一个方案就是利用数学上的技巧,我们可以加一个 `start` 再减一个 `start` 将公式变形。 + +```java +(start + end) / 2 = (start + end + start - start) / 2 = start + (end - start) / 2 +``` + +这样的话,就解决了上边的问题。 + +然后当时和同学看到`jdk`源码中,求`mid`的方法如下 + +```java +int mid = (start + end) >>> 1 +``` + +它通过移位实现了除以 2,但。。。这样难道不会导致溢出吗? + +首先大家可以补一下 [补码](https://mp.weixin.qq.com/s/uvcQHJi6AXhPDJL-6JWUkw) 的知识。 + +其实问题的关键就是这里了`>>>` ,我们知道还有一种右移是`>>`。区别在于`>>`为有符号右移,右移以后最高位保持原来的最高位。而 `>>>` 这个右移的话最高位补 0。 + +所以这里其实利用到了整数的补码形式,最高位其实是符号位,所以当 `start + end` 溢出的时候,其实本质上只是符号位收到了进位,而`>>>`这个右移不仅可以把符号位右移,同时最高位只是补零,不会对数字的大小造成影响。 + +但 `>>` 有符号右移就会出现问题了,事实上 JDK6 之前都用的`>>`,这个 BUG 在 java 里竟然隐藏了十年之久。 + +# 总 + +还是典型的二分查找的应用。解法一就不说了,说一下解法二的三种写法。 + +我觉得第一种写法比较直观,每次判断一下我们是否找到了,可以提前结束。 + +第二种写法的话也经常用到,比如 [33 题](https://leetcode.wang/leetCode-33-Search-in-Rotated-Sorted-Array.html) 找最小值下标的时候,当时和 [@为爱卖小菜](https://leetcode-cn.com/u/wei-ai-mai-xiao-cai/) 讨论了很多,自己对二分理解也深刻了不少。这种写法用于一定可以找到解的时候,一定要注意的是 `low < high`,不能加等号,不然可能造成死循环。 + +第三种写法的话,这里就不推荐了。 + diff --git a/leetcode-279-Perfect-Squares.md b/leetcode-279-Perfect-Squares.md index fa9651fce..89b9533df 100644 --- a/leetcode-279-Perfect-Squares.md +++ b/leetcode-279-Perfect-Squares.md @@ -1,279 +1,279 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/279.jpg) - -把一个数分解成若干个平方数的和,可能有多种方案,找到所需平方数的最少的方案,将最少个数返回。 - -# 解法一 回溯法 - -相当于一种暴力的方法,去考虑所有的分解方案,找出最小的解,举个例子。 - -```java -n = 12 -先把 n 减去一个平方数,然后求剩下的数分解成平方数和所需的最小个数 - -把 n 减去 1, 然后求出 11 分解成平方数和所需的最小个数,记做 n1 -那么当前方案总共需要 n1 + 1 个平方数 - -把 n 减去 4, 然后求出 8 分解成平方数和所需的最小个数,记做 n2 -那么当前方案总共需要 n2 + 1 个平方数 - -把 n 减去 9, 然后求出 3 分解成平方数和所需的最小个数,记做 n3 -那么当前方案总共需要 n3 + 1 个平方数 - -下一个平方数是 16, 大于 12, 不能再分了。 - -接下来我们只需要从 (n1 + 1), (n2 + 1), (n3 + 1) 三种方案中选择最小的个数, -此时就是 12 分解成平方数和所需的最小个数了 - -至于求 11、8、3 分解成最小平方数和所需的最小个数继续用上边的方法去求 - -直到如果求 0 分解成最小平方数的和的个数, 返回 0 即可 -``` - -代码的话,就是回溯的写法,或者说是 `DFS`。 - -```java -public int numSquares(int n) { - return numSquaresHelper(n); -} - -private int numSquaresHelper(int n) { - if (n == 0) { - return 0; - } - int count = Integer.MAX_VALUE; - //依次减去一个平方数 - for (int i = 1; i * i <= n; i++) { - //选最小的 - count = Math.min(count, numSquaresHelper(n - i * i) + 1); - } - return count; -} -``` - -当然上边的会造成超时,很多解会重复的计算,之前也遇到很多这种情况了。我们需要 `memoization` 技术,也就是把过程中的解利用 `HashMap` 全部保存起来即可。 - -```java -public int numSquares(int n) { - return numSquaresHelper(n, new HashMap()); -} - -private int numSquaresHelper(int n, HashMap map) { - if (map.containsKey(n)) { - return map.get(n); - } - if (n == 0) { - return 0; - } - int count = Integer.MAX_VALUE; - for (int i = 1; i * i <= n; i++) { - count = Math.min(count, numSquaresHelper(n - i * i, map) + 1); - } - map.put(n, count); - return count; -} -``` - -# 解法二 动态规划 - -理解了解法一的话,很容易改写成动态规划。递归相当于先压栈压栈然后出栈出栈,动态规划可以省去压栈的过程。 - -动态规划的转移方程就对应递归的过程,动态规划的初始条件就对应递归的出口。 - -```java -public int numSquares(int n) { - int dp[] = new int[n + 1]; - Arrays.fill(dp, Integer.MAX_VALUE); - dp[0] = 0; - //依次求出 1, 2... 直到 n 的解 - for (int i = 1; i <= n; i++) { - //依次减去一个平方数 - for (int j = 1; j * j <= i; j++) { - dp[i] = Math.min(dp[i], dp[i - j * j] + 1); - } - } - return dp[n]; -} -``` - -这里还提出来一种 `Static Dynamic Programming`,主要考虑到测试数据有多组,看一下 `leetcode` 全部代码的逻辑。 - -点击下图箭头的位置。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/279_2.jpg) - -然后会看到下边的代码。 - -```java -class Solution { - public int numSquares(int n) { - int dp[] = new int[n + 1]; - Arrays.fill(dp,Integer.MAX_VALUE); - dp[0] = 0; - for (int i = 1; i <= n; i++) { - for (int j = 1; j * j <= i; j++) { - dp[i] = Math.min(dp[i], dp[i - j * j] + 1); - } - } - return dp[n]; - } -} - -public class MainClass { - public static void main(String[] args) throws IOException { - BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); - String line; - while ((line = in.readLine()) != null) { - int n = Integer.parseInt(line); - - int ret = new Solution().numSquares(n); - - String out = String.valueOf(ret); - - System.out.print(out); - } - } -} -``` - -可以看到上边的逻辑,每次求 `n` 的时候都是创建新的对象然后调用方法。 - -这样会带来一个问题,假如第一次我们求了 `90` 的平方数和的最小个数,期间 `dp` 会求出 `1` 到 `89` 的所有的平方数和的最小个数。 - -第二次如果我们求 `50` 的平方数和的最小个数,其实第一次我们已经求过了,但实际上我们依旧会求一遍 `1` 到 `50` 的所有平方数和的最小个数。 - -我们可以通过声明 `dp` 是 `static` 变量,这样每次调用就不会重复计算了。所有对象将共享 `dp` 。 - -```java -static ArrayList dp = new ArrayList<>(); -public int numSquares(int n) { - //第一次进入将 0 加入 - if(dp.size() == 0){ - dp.add(0); - } - //之前是否计算过 n - if(dp.size() <= n){ - //接着之前最后一个值开始计算 - for (int i = dp.size(); i <= n; i++) { - int min = Integer.MAX_VALUE; - for (int j = 1; j * j <= i; j++) { - min = Math.min(min, dp.get(i - j * j) + 1); - } - dp.add(min); - } - } - return dp.get(n); -} -``` - -# 解法三 BFS - -参考 [这里](https://leetcode.com/problems/perfect-squares/discuss/71488/Summary-of-4-different-solutions-(BFS-DP-static-DP-and-mathematics))。 - -相对于解法一的 `DFS` ,当然也可以使用 `BFS` 。 - -`DFS` 是一直做减法,然后一直减一直减,直到减到 `0` 算作找到一个解。属于一个解一个解的寻找。 - -`BFS` 的话,我们可以一层一层的算。第一层依次减去一个平方数得到第二层,第二层依次减去一个平方数得到第三层。直到某一层出现了 `0`,此时的层数就是我们要找到平方数和的最小个数。 - -举个例子,`n = 12`,每层的话每个节点依次减 `1, 4, 9...`。如下图,灰色表示当前层重复的节点,不需要处理。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/279_3.jpg) - -如上图,当出现 `0` 的时候遍历就可以停止,此时是第 `3` 层(从 `0` 计数),所以最终答案就是 `3`。 - -实现的话当然离不开队列,此外我们需要一个 `set` 来记录重复的解。 - -```java -public int numSquares(int n) { - Queue queue = new LinkedList<>(); - HashSet visited = new HashSet<>(); - int level = 0; - queue.add(n); - while (!queue.isEmpty()) { - int size = queue.size(); - level++; // 开始生成下一层 - for (int i = 0; i < size; i++) { - int cur = queue.poll(); - //依次减 1, 4, 9... 生成下一层的节点 - for (int j = 1; j * j <= cur; j++) { - int next = cur - j * j; - if (next == 0) { - return level; - } - if (!visited.contains(next)) { - queue.offer(next); - visited.add(next); - } - } - } - } - return -1; -} -``` - -# 解法四 数学 - -参考 [这里](https://leetcode.com/problems/perfect-squares/discuss/71488/Summary-of-4-different-solutions-(BFS-DP-static-DP-and-mathematics))。 - -这个解法就不是编程的思想了,需要一些预备的数学知识。 - -[四平方和定理]([https://zh.wikipedia.org/wiki/%E5%9B%9B%E5%B9%B3%E6%96%B9%E5%92%8C%E5%AE%9A%E7%90%86](https://zh.wikipedia.org/wiki/四平方和定理)),意思是任何正整数都能表示成四个平方数的和。少于四个平方数的,像 `12` 这种,可以补一个 `0` 也可以看成四个平方数,`12 = 4 + 4 + 4 + 0`。知道了这个定理,对于题目要找的解,其实只可能是 `1, 2, 3, 4` 其中某个数。 - -[Legendre's three-square theorem](https://en.wikipedia.org/wiki/Legendre's_three-square_theorem) ,这个定理表明,如果正整数 `n` 被表示为三个平方数的和,那么 `n` 不等于 $$ 4^a*(8b+7)$$,`a` 和 `b` 都是非负整数。 - -换言之,如果 $$n == 4^a*(8b+7)$$,那么他一定不能表示为三个平方数的和,同时也说明不能表示为一个、两个平方数的和,因为如果能表示为两个平方数的和,那么补个 `0`,就能凑成三个平方数的和了。 - -一个、两个、三个都排除了,所以如果 $$n == 4^a*(8b+7)$$,那么 `n` 只能表示成四个平方数的和了。 - -所以代码的话,我们采取排除的方法。 - -首先考虑答案是不是 `1`,也就是判断当前数是不是一个平方数。 - -然后考虑答案是不是 `4`,也就是判断 `n` 是不是等于 $$ 4^a*(8b+7)$$。 - -然后考虑答案是不是 `2`,当前数依次减去一个平方数,判断得到的差是不是平方数。 - -以上情况都排除的话,答案就是 `3`。 - -```java -public int numSquares(int n) { - //判断是否是 1 - if (isSquare(n)) { - return 1; - } - - //判断是否是 4 - int temp = n; - while (temp % 4 == 0) { - temp /= 4; - } - if (temp % 8 == 7) { - return 4; - } - - //判断是否是 2 - for (int i = 1; i * i < n; i++) { - if (isSquare(n - i * i)) { - return 2; - } - } - - return 3; -} - -//判断是否是平方数 -private boolean isSquare(int n) { - int sqrt = (int) Math.sqrt(n); - return sqrt * sqrt == n; -} -``` - -# 总 - -解法一和解法二的话算比较常规的思想,我觉得可以看做暴力的思想,是最直接的思路。 - -解法三的话,只是改变了遍历的方式,本质上和解法一还是一致的。 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/279.jpg) + +把一个数分解成若干个平方数的和,可能有多种方案,找到所需平方数的最少的方案,将最少个数返回。 + +# 解法一 回溯法 + +相当于一种暴力的方法,去考虑所有的分解方案,找出最小的解,举个例子。 + +```java +n = 12 +先把 n 减去一个平方数,然后求剩下的数分解成平方数和所需的最小个数 + +把 n 减去 1, 然后求出 11 分解成平方数和所需的最小个数,记做 n1 +那么当前方案总共需要 n1 + 1 个平方数 + +把 n 减去 4, 然后求出 8 分解成平方数和所需的最小个数,记做 n2 +那么当前方案总共需要 n2 + 1 个平方数 + +把 n 减去 9, 然后求出 3 分解成平方数和所需的最小个数,记做 n3 +那么当前方案总共需要 n3 + 1 个平方数 + +下一个平方数是 16, 大于 12, 不能再分了。 + +接下来我们只需要从 (n1 + 1), (n2 + 1), (n3 + 1) 三种方案中选择最小的个数, +此时就是 12 分解成平方数和所需的最小个数了 + +至于求 11、8、3 分解成最小平方数和所需的最小个数继续用上边的方法去求 + +直到如果求 0 分解成最小平方数的和的个数, 返回 0 即可 +``` + +代码的话,就是回溯的写法,或者说是 `DFS`。 + +```java +public int numSquares(int n) { + return numSquaresHelper(n); +} + +private int numSquaresHelper(int n) { + if (n == 0) { + return 0; + } + int count = Integer.MAX_VALUE; + //依次减去一个平方数 + for (int i = 1; i * i <= n; i++) { + //选最小的 + count = Math.min(count, numSquaresHelper(n - i * i) + 1); + } + return count; +} +``` + +当然上边的会造成超时,很多解会重复的计算,之前也遇到很多这种情况了。我们需要 `memoization` 技术,也就是把过程中的解利用 `HashMap` 全部保存起来即可。 + +```java +public int numSquares(int n) { + return numSquaresHelper(n, new HashMap()); +} + +private int numSquaresHelper(int n, HashMap map) { + if (map.containsKey(n)) { + return map.get(n); + } + if (n == 0) { + return 0; + } + int count = Integer.MAX_VALUE; + for (int i = 1; i * i <= n; i++) { + count = Math.min(count, numSquaresHelper(n - i * i, map) + 1); + } + map.put(n, count); + return count; +} +``` + +# 解法二 动态规划 + +理解了解法一的话,很容易改写成动态规划。递归相当于先压栈压栈然后出栈出栈,动态规划可以省去压栈的过程。 + +动态规划的转移方程就对应递归的过程,动态规划的初始条件就对应递归的出口。 + +```java +public int numSquares(int n) { + int dp[] = new int[n + 1]; + Arrays.fill(dp, Integer.MAX_VALUE); + dp[0] = 0; + //依次求出 1, 2... 直到 n 的解 + for (int i = 1; i <= n; i++) { + //依次减去一个平方数 + for (int j = 1; j * j <= i; j++) { + dp[i] = Math.min(dp[i], dp[i - j * j] + 1); + } + } + return dp[n]; +} +``` + +这里还提出来一种 `Static Dynamic Programming`,主要考虑到测试数据有多组,看一下 `leetcode` 全部代码的逻辑。 + +点击下图箭头的位置。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/279_2.jpg) + +然后会看到下边的代码。 + +```java +class Solution { + public int numSquares(int n) { + int dp[] = new int[n + 1]; + Arrays.fill(dp,Integer.MAX_VALUE); + dp[0] = 0; + for (int i = 1; i <= n; i++) { + for (int j = 1; j * j <= i; j++) { + dp[i] = Math.min(dp[i], dp[i - j * j] + 1); + } + } + return dp[n]; + } +} + +public class MainClass { + public static void main(String[] args) throws IOException { + BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); + String line; + while ((line = in.readLine()) != null) { + int n = Integer.parseInt(line); + + int ret = new Solution().numSquares(n); + + String out = String.valueOf(ret); + + System.out.print(out); + } + } +} +``` + +可以看到上边的逻辑,每次求 `n` 的时候都是创建新的对象然后调用方法。 + +这样会带来一个问题,假如第一次我们求了 `90` 的平方数和的最小个数,期间 `dp` 会求出 `1` 到 `89` 的所有的平方数和的最小个数。 + +第二次如果我们求 `50` 的平方数和的最小个数,其实第一次我们已经求过了,但实际上我们依旧会求一遍 `1` 到 `50` 的所有平方数和的最小个数。 + +我们可以通过声明 `dp` 是 `static` 变量,这样每次调用就不会重复计算了。所有对象将共享 `dp` 。 + +```java +static ArrayList dp = new ArrayList<>(); +public int numSquares(int n) { + //第一次进入将 0 加入 + if(dp.size() == 0){ + dp.add(0); + } + //之前是否计算过 n + if(dp.size() <= n){ + //接着之前最后一个值开始计算 + for (int i = dp.size(); i <= n; i++) { + int min = Integer.MAX_VALUE; + for (int j = 1; j * j <= i; j++) { + min = Math.min(min, dp.get(i - j * j) + 1); + } + dp.add(min); + } + } + return dp.get(n); +} +``` + +# 解法三 BFS + +参考 [这里](https://leetcode.com/problems/perfect-squares/discuss/71488/Summary-of-4-different-solutions-(BFS-DP-static-DP-and-mathematics))。 + +相对于解法一的 `DFS` ,当然也可以使用 `BFS` 。 + +`DFS` 是一直做减法,然后一直减一直减,直到减到 `0` 算作找到一个解。属于一个解一个解的寻找。 + +`BFS` 的话,我们可以一层一层的算。第一层依次减去一个平方数得到第二层,第二层依次减去一个平方数得到第三层。直到某一层出现了 `0`,此时的层数就是我们要找到平方数和的最小个数。 + +举个例子,`n = 12`,每层的话每个节点依次减 `1, 4, 9...`。如下图,灰色表示当前层重复的节点,不需要处理。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/279_3.jpg) + +如上图,当出现 `0` 的时候遍历就可以停止,此时是第 `3` 层(从 `0` 计数),所以最终答案就是 `3`。 + +实现的话当然离不开队列,此外我们需要一个 `set` 来记录重复的解。 + +```java +public int numSquares(int n) { + Queue queue = new LinkedList<>(); + HashSet visited = new HashSet<>(); + int level = 0; + queue.add(n); + while (!queue.isEmpty()) { + int size = queue.size(); + level++; // 开始生成下一层 + for (int i = 0; i < size; i++) { + int cur = queue.poll(); + //依次减 1, 4, 9... 生成下一层的节点 + for (int j = 1; j * j <= cur; j++) { + int next = cur - j * j; + if (next == 0) { + return level; + } + if (!visited.contains(next)) { + queue.offer(next); + visited.add(next); + } + } + } + } + return -1; +} +``` + +# 解法四 数学 + +参考 [这里](https://leetcode.com/problems/perfect-squares/discuss/71488/Summary-of-4-different-solutions-(BFS-DP-static-DP-and-mathematics))。 + +这个解法就不是编程的思想了,需要一些预备的数学知识。 + +[四平方和定理]([https://zh.wikipedia.org/wiki/%E5%9B%9B%E5%B9%B3%E6%96%B9%E5%92%8C%E5%AE%9A%E7%90%86](https://zh.wikipedia.org/wiki/四平方和定理)),意思是任何正整数都能表示成四个平方数的和。少于四个平方数的,像 `12` 这种,可以补一个 `0` 也可以看成四个平方数,`12 = 4 + 4 + 4 + 0`。知道了这个定理,对于题目要找的解,其实只可能是 `1, 2, 3, 4` 其中某个数。 + +[Legendre's three-square theorem](https://en.wikipedia.org/wiki/Legendre's_three-square_theorem) ,这个定理表明,如果正整数 `n` 被表示为三个平方数的和,那么 `n` 不等于 $$ 4^a*(8b+7)$$,`a` 和 `b` 都是非负整数。 + +换言之,如果 $$n == 4^a*(8b+7)$$,那么他一定不能表示为三个平方数的和,同时也说明不能表示为一个、两个平方数的和,因为如果能表示为两个平方数的和,那么补个 `0`,就能凑成三个平方数的和了。 + +一个、两个、三个都排除了,所以如果 $$n == 4^a*(8b+7)$$,那么 `n` 只能表示成四个平方数的和了。 + +所以代码的话,我们采取排除的方法。 + +首先考虑答案是不是 `1`,也就是判断当前数是不是一个平方数。 + +然后考虑答案是不是 `4`,也就是判断 `n` 是不是等于 $$ 4^a*(8b+7)$$。 + +然后考虑答案是不是 `2`,当前数依次减去一个平方数,判断得到的差是不是平方数。 + +以上情况都排除的话,答案就是 `3`。 + +```java +public int numSquares(int n) { + //判断是否是 1 + if (isSquare(n)) { + return 1; + } + + //判断是否是 4 + int temp = n; + while (temp % 4 == 0) { + temp /= 4; + } + if (temp % 8 == 7) { + return 4; + } + + //判断是否是 2 + for (int i = 1; i * i < n; i++) { + if (isSquare(n - i * i)) { + return 2; + } + } + + return 3; +} + +//判断是否是平方数 +private boolean isSquare(int n) { + int sqrt = (int) Math.sqrt(n); + return sqrt * sqrt == n; +} +``` + +# 总 + +解法一和解法二的话算比较常规的思想,我觉得可以看做暴力的思想,是最直接的思路。 + +解法三的话,只是改变了遍历的方式,本质上和解法一还是一致的。 + 解法四就需要数学功底了,这里仅做了解,记住结论就可以了。 \ No newline at end of file diff --git a/leetcode-282-Expression-Add-Operators.md b/leetcode-282-Expression-Add-Operators.md index e361760c2..f18811248 100644 --- a/leetcode-282-Expression-Add-Operators.md +++ b/leetcode-282-Expression-Add-Operators.md @@ -1,209 +1,209 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/282.png) - -在数字中间添加加号、减号或者乘号,使得计算结果等于 `target`,返回所有的情况。 - -# 解法一 回溯法 - -自己开始想到了回溯法,但思路偏了,只能解决添加加号和减号,乘号不知道怎么处理。讲一下别人的思路,参考 [这里](https://leetcode.com/problems/expression-add-operators/discuss/71895/Java-Standard-Backtrace-AC-Solutoin-short-and-clear)。 - -思路就是添加一个符号,再添加一个数字,计算到目前为止表达式的结果。 - -当到达字符串末尾的时候,判断当前结果是否等于 `target` 。 - -我们先考虑只有加法和减法,举个例子。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/282_2.jpg) - -选取第一个数字的时候不加符号,然后剩下的都是符号加数字的形式。 - -如果我们对上边的图做深度优先遍历,会发现我们考虑了所有的表达式,下边就是深度优先遍历的结果。 - -```java -1 +2 +3 = 6 -1 +2 -3 = 0 -1 -2 +3 = 2 -1 -2 -3 = -4 -1 +23 = 24 -1 -23 = -22 -12 +3 = 15 -12 -3 = 9 -123 = 123 -``` - -看一下只考虑加法和减法的代码。 - -有些地方需要注意,我们将字符串转成数字的时候可能会超出 `int` 范围,以及计算目前为止表达式的结果,也就是上图的括号里的值,也可能超出 `int` 范围,所以这两个值我们采用 `long` 。 - -还有就是 "01" 这种以 `0` 开头的字符串我们不用考虑,可以直接跳过。 - -计算上图括号中的值的时候,我们只需要用它的父节点中括号的值,加上或者减去当前值。 - -```java -public List addOperators2(String num, int target) { - List result = new ArrayList<>(); - addOperatorsHelper(num, target, result, "", 0, 0, 0); - return result; -} - -// path: 目前为止的表达式 -// 字符串开始的位置 -// eval 目前为止计算的结果 -private void addOperatorsHelper(String num, int target, List result, String path, int start, long eval) { - if (start == num.length()) { - if (target == eval) { - result.add(path); - } - return; - - } - for (int i = start; i < num.length(); i++) { - // 数字不能以 0 开头 - if (num.charAt(start) == '0' && i > start) { - break; - } - long cur = Long.parseLong(num.substring(start, i + 1)); - // 选取第一个数不加符号 - if (start == 0) { - addOperatorsHelper(num, target, result, path + cur, i + 1, cur, cur); - } else { - // 加当前值 - addOperatorsHelper(num, target, result, path + "+" + cur, i + 1, eval + cur, cur); - // 减当前值 - addOperatorsHelper(num, target, result, path + "-" + cur, i + 1, eval - cur, -cur); - } - } -} -``` - -现在考虑乘法有什么不同。 - -还是以 `123` 为例。如果还是按照上边的代码的逻辑,如果添加乘法的话就是下边的样子。 - -```java - 1 - / - +2(3) - / - *3(9) -``` - -也就是 `1 +2 *3 = 9`,很明显是错的,原因就在于乘法的优先级较高,并不能直接将前边的结果乘上当前的数。而是用当前数乘以前一个操作数。 - -算的话,我们可以用之前的值(3)减去前一个操作数(2),然后再加上当前数(3)乘以前一个操作数(2)。 - -即,`3 - 2 + 3 * 2 = 7` - -所以代码的话,我们需要添加一个 `pre` 参数,用来记录上一个操作数。 - -对于加法和乘法,上一个操作数我们根据符号直接返回 `-2`,`3` 这种就可以了,但对于乘法有些不同。 - -如果是连乘的情况,比如对于 `2345`,假设之前已经进行到了 `2 +3 *4 (14)`,现在到 `5` 了。 - -如果我们想增加乘号,计算表达式 `2 +3 *4 *5` 的值。`5` 的前一个操作数是 `*4` ,我们应该记录的值是 `3 * 4 = 12` 。这样的话才能套用上边的讲的公式。 - -用之前的值(14)减去前一个操作数(12),然后再加上当前数(5)乘以前一个操作数(12)。 - -即,`14 - 12 + 5 * 12 = 62`。也就是 `2 +3 *4 *5 = 62`。 - -可以结合代码再看一下。 - -```java -public List addOperators(String num, int target) { - List result = new ArrayList<>(); - addOperatorsHelper(num, target, result, "", 0, 0, 0); - return result; -} - -private void addOperatorsHelper(String num, int target, List result, String path, int start, long eval, long pre) { - if (start == num.length()) { - if (target == eval) { - result.add(path); - } - return; - - } - for (int i = start; i < num.length(); i++) { - // 数字不能以 0 开头 - if (num.charAt(start) == '0' && i > start) { - break; - } - long cur = Long.parseLong(num.substring(start, i + 1)); - if (start == 0) { - addOperatorsHelper(num, target, result, path + cur, i + 1, cur, cur); - } else { - // 加当前值 - addOperatorsHelper(num, target, result, path + "+" + cur, i + 1, eval + cur, cur); - // 减当前值 - addOperatorsHelper(num, target, result, path + "-" + cur, i + 1, eval - cur, -cur); - - //乘法有两点不同 - - //当前表达式的值就是 先减去之前的值,然后加上 当前值和前边的操作数相乘 - //eval - pre + pre * cur - - //另外 addOperatorsHelper 函数传进 pre 参数需要是 pre * cur - //比如前边讲的 2+ 3 * 4 * 5 这种连乘的情况 - addOperatorsHelper(num, target, result, path + "*" + cur, i + 1, eval - pre + pre * cur, pre * cur); - } - } -} -``` - -上边的我们 `path` 参数采用的是 `String` 类型,`String` 类型进行相加的话会生成新的对象,比较慢。 - -我们可以用 `StringBuilder` 类型。如果用 `StringBuilder` 的话,每次调用完函数就需要将之前添加的东西删除掉,然后再调用新的函数。 - -[评论区](https://leetcode.com/problems/expression-add-operators/discuss/71895/Java-Standard-Backtrace-AC-Solutoin-short-and-clear) 介绍了 `StringBuilder` 删除之前添加的元素的方法,可以通过设置 `StringBuilder` 的长度。 - -```java -public List addOperators(String num, int target) { - List result = new ArrayList<>(); - addOperatorsHelper(num, target, result, new StringBuilder(), 0, 0, 0); - return result; -} - -private void addOperatorsHelper(String num, int target, List result, StringBuilder path, int start, long eval, long pre) { - if (start == num.length()) { - if (target == eval) { - result.add(path.toString()); - } - return; - - } - for (int i = start; i < num.length(); i++) { - // 数字不能以 0 开头 - if (num.charAt(start) == '0' && i > start) { - break; - } - long cur = Long.parseLong(num.substring(start, i + 1)); - int len = path.length(); - if (start == 0) { - addOperatorsHelper(num, target, result, path.append(cur), i + 1, cur, cur); - path.setLength(len); - } else { - - // 加当前值 - addOperatorsHelper(num, target, result, path.append("+").append(cur), i + 1, eval + cur, cur); - path.setLength(len); - // 减当前值 - addOperatorsHelper(num, target, result, path.append("-").append(cur), i + 1, eval - cur, -cur); - path.setLength(len); - // 乘当前值 - addOperatorsHelper(num, target, result, path.append("*").append(cur), i + 1, eval - pre + pre * cur, - pre * cur); - path.setLength(len); - } - } -} -``` - -# 总 - -如果思路对了,用回溯法可以很快写出来,乘法的情况需要单独考虑一下。 - -我开始的思路是加数字再加符号,和上边的解法刚好是反过来了,但我的思路解决不了乘法的问题。 - -这里继续吸取教训,走到死胡同的时候,试着转变思路。 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/282.png) + +在数字中间添加加号、减号或者乘号,使得计算结果等于 `target`,返回所有的情况。 + +# 解法一 回溯法 + +自己开始想到了回溯法,但思路偏了,只能解决添加加号和减号,乘号不知道怎么处理。讲一下别人的思路,参考 [这里](https://leetcode.com/problems/expression-add-operators/discuss/71895/Java-Standard-Backtrace-AC-Solutoin-short-and-clear)。 + +思路就是添加一个符号,再添加一个数字,计算到目前为止表达式的结果。 + +当到达字符串末尾的时候,判断当前结果是否等于 `target` 。 + +我们先考虑只有加法和减法,举个例子。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/282_2.jpg) + +选取第一个数字的时候不加符号,然后剩下的都是符号加数字的形式。 + +如果我们对上边的图做深度优先遍历,会发现我们考虑了所有的表达式,下边就是深度优先遍历的结果。 + +```java +1 +2 +3 = 6 +1 +2 -3 = 0 +1 -2 +3 = 2 +1 -2 -3 = -4 +1 +23 = 24 +1 -23 = -22 +12 +3 = 15 +12 -3 = 9 +123 = 123 +``` + +看一下只考虑加法和减法的代码。 + +有些地方需要注意,我们将字符串转成数字的时候可能会超出 `int` 范围,以及计算目前为止表达式的结果,也就是上图的括号里的值,也可能超出 `int` 范围,所以这两个值我们采用 `long` 。 + +还有就是 "01" 这种以 `0` 开头的字符串我们不用考虑,可以直接跳过。 + +计算上图括号中的值的时候,我们只需要用它的父节点中括号的值,加上或者减去当前值。 + +```java +public List addOperators2(String num, int target) { + List result = new ArrayList<>(); + addOperatorsHelper(num, target, result, "", 0, 0, 0); + return result; +} + +// path: 目前为止的表达式 +// 字符串开始的位置 +// eval 目前为止计算的结果 +private void addOperatorsHelper(String num, int target, List result, String path, int start, long eval) { + if (start == num.length()) { + if (target == eval) { + result.add(path); + } + return; + + } + for (int i = start; i < num.length(); i++) { + // 数字不能以 0 开头 + if (num.charAt(start) == '0' && i > start) { + break; + } + long cur = Long.parseLong(num.substring(start, i + 1)); + // 选取第一个数不加符号 + if (start == 0) { + addOperatorsHelper(num, target, result, path + cur, i + 1, cur, cur); + } else { + // 加当前值 + addOperatorsHelper(num, target, result, path + "+" + cur, i + 1, eval + cur, cur); + // 减当前值 + addOperatorsHelper(num, target, result, path + "-" + cur, i + 1, eval - cur, -cur); + } + } +} +``` + +现在考虑乘法有什么不同。 + +还是以 `123` 为例。如果还是按照上边的代码的逻辑,如果添加乘法的话就是下边的样子。 + +```java + 1 + / + +2(3) + / + *3(9) +``` + +也就是 `1 +2 *3 = 9`,很明显是错的,原因就在于乘法的优先级较高,并不能直接将前边的结果乘上当前的数。而是用当前数乘以前一个操作数。 + +算的话,我们可以用之前的值(3)减去前一个操作数(2),然后再加上当前数(3)乘以前一个操作数(2)。 + +即,`3 - 2 + 3 * 2 = 7` + +所以代码的话,我们需要添加一个 `pre` 参数,用来记录上一个操作数。 + +对于加法和乘法,上一个操作数我们根据符号直接返回 `-2`,`3` 这种就可以了,但对于乘法有些不同。 + +如果是连乘的情况,比如对于 `2345`,假设之前已经进行到了 `2 +3 *4 (14)`,现在到 `5` 了。 + +如果我们想增加乘号,计算表达式 `2 +3 *4 *5` 的值。`5` 的前一个操作数是 `*4` ,我们应该记录的值是 `3 * 4 = 12` 。这样的话才能套用上边的讲的公式。 + +用之前的值(14)减去前一个操作数(12),然后再加上当前数(5)乘以前一个操作数(12)。 + +即,`14 - 12 + 5 * 12 = 62`。也就是 `2 +3 *4 *5 = 62`。 + +可以结合代码再看一下。 + +```java +public List addOperators(String num, int target) { + List result = new ArrayList<>(); + addOperatorsHelper(num, target, result, "", 0, 0, 0); + return result; +} + +private void addOperatorsHelper(String num, int target, List result, String path, int start, long eval, long pre) { + if (start == num.length()) { + if (target == eval) { + result.add(path); + } + return; + + } + for (int i = start; i < num.length(); i++) { + // 数字不能以 0 开头 + if (num.charAt(start) == '0' && i > start) { + break; + } + long cur = Long.parseLong(num.substring(start, i + 1)); + if (start == 0) { + addOperatorsHelper(num, target, result, path + cur, i + 1, cur, cur); + } else { + // 加当前值 + addOperatorsHelper(num, target, result, path + "+" + cur, i + 1, eval + cur, cur); + // 减当前值 + addOperatorsHelper(num, target, result, path + "-" + cur, i + 1, eval - cur, -cur); + + //乘法有两点不同 + + //当前表达式的值就是 先减去之前的值,然后加上 当前值和前边的操作数相乘 + //eval - pre + pre * cur + + //另外 addOperatorsHelper 函数传进 pre 参数需要是 pre * cur + //比如前边讲的 2+ 3 * 4 * 5 这种连乘的情况 + addOperatorsHelper(num, target, result, path + "*" + cur, i + 1, eval - pre + pre * cur, pre * cur); + } + } +} +``` + +上边的我们 `path` 参数采用的是 `String` 类型,`String` 类型进行相加的话会生成新的对象,比较慢。 + +我们可以用 `StringBuilder` 类型。如果用 `StringBuilder` 的话,每次调用完函数就需要将之前添加的东西删除掉,然后再调用新的函数。 + +[评论区](https://leetcode.com/problems/expression-add-operators/discuss/71895/Java-Standard-Backtrace-AC-Solutoin-short-and-clear) 介绍了 `StringBuilder` 删除之前添加的元素的方法,可以通过设置 `StringBuilder` 的长度。 + +```java +public List addOperators(String num, int target) { + List result = new ArrayList<>(); + addOperatorsHelper(num, target, result, new StringBuilder(), 0, 0, 0); + return result; +} + +private void addOperatorsHelper(String num, int target, List result, StringBuilder path, int start, long eval, long pre) { + if (start == num.length()) { + if (target == eval) { + result.add(path.toString()); + } + return; + + } + for (int i = start; i < num.length(); i++) { + // 数字不能以 0 开头 + if (num.charAt(start) == '0' && i > start) { + break; + } + long cur = Long.parseLong(num.substring(start, i + 1)); + int len = path.length(); + if (start == 0) { + addOperatorsHelper(num, target, result, path.append(cur), i + 1, cur, cur); + path.setLength(len); + } else { + + // 加当前值 + addOperatorsHelper(num, target, result, path.append("+").append(cur), i + 1, eval + cur, cur); + path.setLength(len); + // 减当前值 + addOperatorsHelper(num, target, result, path.append("-").append(cur), i + 1, eval - cur, -cur); + path.setLength(len); + // 乘当前值 + addOperatorsHelper(num, target, result, path.append("*").append(cur), i + 1, eval - pre + pre * cur, + pre * cur); + path.setLength(len); + } + } +} +``` + +# 总 + +如果思路对了,用回溯法可以很快写出来,乘法的情况需要单独考虑一下。 + +我开始的思路是加数字再加符号,和上边的解法刚好是反过来了,但我的思路解决不了乘法的问题。 + +这里继续吸取教训,走到死胡同的时候,试着转变思路。 + diff --git a/leetcode-283-Move-Zeroes.md b/leetcode-283-Move-Zeroes.md index 28147e8a0..73ff05b55 100644 --- a/leetcode-283-Move-Zeroes.md +++ b/leetcode-283-Move-Zeroes.md @@ -1,72 +1,72 @@ -# 题目描述(简单难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/283.jpg) - -将所有的 `0` 移动到末尾,并且保持其他数字的相对顺序不变。 - -# 解法一 - -我的第一反应是利用两个指针,一个指针指向开头,一个指针指向末尾非零元素,然后从开头指针遍历,如果遇到 `0` 就和末尾指向的元素相交换,末尾指针向前移动到非零元素。 - -这就保证末尾指针后边元素全部是 `0`,当首尾指针相遇的时候结束。 - -但是上边的想法会使得其他数字的相对顺序改变了,我们可以逆转一下思路。不是将 `0` 放到末尾,而是将所有非零元素放到开头,这样就保证末尾剩下的都是 `0` 了。 - -同样利用双指针,指针 `i` 用于遍历数组,指针 `j` 开始指向开头,保证它前边的所有元素都是非 `0` 元素。 - -当 `i` 指针遇到非零元素就和 `j` 指针指向的元素交换,`j` 指针然后后移。 - -以 `0,1,0,3,12` 为例模拟一下过程。 - -```java -0,1,0,3,12 -^ -i -j - -nums[i] == 0, i 后移 -0,1,0,3,12 -^ ^ -j i - -nums[i] != 0, 交换和 j 指向的元素, i 后移, j 后移 -1,0,0,3,12 - ^ ^ - j i - -nums[i] == 0, i 后移 -1,0,0,3,12 - ^ ^ - j i - -nums[i] != 0, 交换和 j 指向的元素, i 后移, j 后移 -1,3,0,0,12 - ^ ^ - j i - -nums[i] != 0, 交换和 j 指向的元素, i 后移, j 后移, 遍历结束 -1,3,12,0,0 - ^ ^ - j i -``` - -可以注意到 `j` 前边的元素始终都是非零元素,可以结合代码再看下。 - -```java -public void moveZeroes(int[] nums) { - int j = 0; - for (int i = 0; i < nums.length; i++) { - //不等于 0 就交换 - if (nums[i] != 0) { - int temp = nums[j]; - nums[j] = nums[i]; - nums[i] = temp; - j++; - } - } -} -``` - -# 总 - +# 题目描述(简单难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/283.jpg) + +将所有的 `0` 移动到末尾,并且保持其他数字的相对顺序不变。 + +# 解法一 + +我的第一反应是利用两个指针,一个指针指向开头,一个指针指向末尾非零元素,然后从开头指针遍历,如果遇到 `0` 就和末尾指向的元素相交换,末尾指针向前移动到非零元素。 + +这就保证末尾指针后边元素全部是 `0`,当首尾指针相遇的时候结束。 + +但是上边的想法会使得其他数字的相对顺序改变了,我们可以逆转一下思路。不是将 `0` 放到末尾,而是将所有非零元素放到开头,这样就保证末尾剩下的都是 `0` 了。 + +同样利用双指针,指针 `i` 用于遍历数组,指针 `j` 开始指向开头,保证它前边的所有元素都是非 `0` 元素。 + +当 `i` 指针遇到非零元素就和 `j` 指针指向的元素交换,`j` 指针然后后移。 + +以 `0,1,0,3,12` 为例模拟一下过程。 + +```java +0,1,0,3,12 +^ +i +j + +nums[i] == 0, i 后移 +0,1,0,3,12 +^ ^ +j i + +nums[i] != 0, 交换和 j 指向的元素, i 后移, j 后移 +1,0,0,3,12 + ^ ^ + j i + +nums[i] == 0, i 后移 +1,0,0,3,12 + ^ ^ + j i + +nums[i] != 0, 交换和 j 指向的元素, i 后移, j 后移 +1,3,0,0,12 + ^ ^ + j i + +nums[i] != 0, 交换和 j 指向的元素, i 后移, j 后移, 遍历结束 +1,3,12,0,0 + ^ ^ + j i +``` + +可以注意到 `j` 前边的元素始终都是非零元素,可以结合代码再看下。 + +```java +public void moveZeroes(int[] nums) { + int j = 0; + for (int i = 0; i < nums.length; i++) { + //不等于 0 就交换 + if (nums[i] != 0) { + int temp = nums[j]; + nums[j] = nums[i]; + nums[i] = temp; + j++; + } + } +} +``` + +# 总 + 比较简单的一道题,双指针经常用到。用一个指针用来分割元素,使得它前边都是符合某种条件的元素。在快速排序中,也有用到这个思想。 \ No newline at end of file diff --git a/leetcode-284-Peeking-Iterator.md b/leetcode-284-Peeking-Iterator.md index efcbbecda..b3107ba9a 100644 --- a/leetcode-284-Peeking-Iterator.md +++ b/leetcode-284-Peeking-Iterator.md @@ -1,90 +1,90 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/284.jpg) - -给迭代器增加一个 `peek` 功能,也就是查看下一个元素,但是不从迭代器中弹出。 - -# 解法一 - -我第一反应是直接把迭代器的元素放到 `list` 中不就实现了吗? - -```java -class PeekingIterator implements Iterator { - List list; - int cur = 0; - - public PeekingIterator(Iterator iterator) { - // initialize any member here. - list = new ArrayList<>(); - while (iterator.hasNext()) - list.add(iterator.next()); - - } - - // Returns the next element in the iteration without advancing the iterator. - public Integer peek() { - return list.get(cur); - } - - // hasNext() and next() should behave the same as in the Iterator interface. - // Override them if needed. - @Override - public Integer next() { - return list.get(cur++); - } - - @Override - public boolean hasNext() { - return cur < list.size(); - } -} -``` - -# 解法二 - -解法一还真的通过了,觉得自己没有 get 题目的点,然后去逛 Discuss 了,原来题目想让我们这样做,分享 [这里](https://leetcode.com/problems/peeking-iterator/discuss/72558/Concise-Java-Solution) 的代码。 - -我们知道构造函数传进来的迭代器已经有了 `next` 和 `haseNext` 函数,我们需要增加 `peek` 函数。我们可以加一个缓冲变量,记录当前要返回的值。 - -`peek` 的话只需要将缓冲变量直接返回。 - -`next` 的话我们需要更新缓冲变量,然后将之前的缓冲变量返回即可。 - -```java -class PeekingIterator implements Iterator { - private Integer next = null;//缓冲变量 - private Iterator iter; - - public PeekingIterator(Iterator iterator) { - // initialize any member here. - iter = iterator; - if (iter.hasNext()){ - next = iter.next(); - } - - } - - // Returns the next element in the iteration without advancing the iterator. - public Integer peek() { - return next; - } - - // hasNext() and next() should behave the same as in the Iterator interface. - // Override them if needed. - @Override - public Integer next() { - Integer res = next; - next = iter.hasNext() ? iter.next() : null; - return res; - } - - @Override - public boolean hasNext() { - return next != null; - } -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/284.jpg) + +给迭代器增加一个 `peek` 功能,也就是查看下一个元素,但是不从迭代器中弹出。 + +# 解法一 + +我第一反应是直接把迭代器的元素放到 `list` 中不就实现了吗? + +```java +class PeekingIterator implements Iterator { + List list; + int cur = 0; + + public PeekingIterator(Iterator iterator) { + // initialize any member here. + list = new ArrayList<>(); + while (iterator.hasNext()) + list.add(iterator.next()); + + } + + // Returns the next element in the iteration without advancing the iterator. + public Integer peek() { + return list.get(cur); + } + + // hasNext() and next() should behave the same as in the Iterator interface. + // Override them if needed. + @Override + public Integer next() { + return list.get(cur++); + } + + @Override + public boolean hasNext() { + return cur < list.size(); + } +} +``` + +# 解法二 + +解法一还真的通过了,觉得自己没有 get 题目的点,然后去逛 Discuss 了,原来题目想让我们这样做,分享 [这里](https://leetcode.com/problems/peeking-iterator/discuss/72558/Concise-Java-Solution) 的代码。 + +我们知道构造函数传进来的迭代器已经有了 `next` 和 `haseNext` 函数,我们需要增加 `peek` 函数。我们可以加一个缓冲变量,记录当前要返回的值。 + +`peek` 的话只需要将缓冲变量直接返回。 + +`next` 的话我们需要更新缓冲变量,然后将之前的缓冲变量返回即可。 + +```java +class PeekingIterator implements Iterator { + private Integer next = null;//缓冲变量 + private Iterator iter; + + public PeekingIterator(Iterator iterator) { + // initialize any member here. + iter = iterator; + if (iter.hasNext()){ + next = iter.next(); + } + + } + + // Returns the next element in the iteration without advancing the iterator. + public Integer peek() { + return next; + } + + // hasNext() and next() should behave the same as in the Iterator interface. + // Override them if needed. + @Override + public Integer next() { + Integer res = next; + next = iter.hasNext() ? iter.next() : null; + return res; + } + + @Override + public boolean hasNext() { + return next != null; + } +} +``` + +# 总 + 其实是比较简单的一道题,用到的思想也比较简单,增加了一个缓冲变量来实现 `peek` 的功能。 \ No newline at end of file diff --git a/leetcode-287-Find-the-Duplicate-Number.md b/leetcode-287-Find-the-Duplicate-Number.md index d1a431db5..e770c2dd1 100644 --- a/leetcode-287-Find-the-Duplicate-Number.md +++ b/leetcode-287-Find-the-Duplicate-Number.md @@ -1,214 +1,214 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/287.jpg) - -将 `1` 到 `n` 范围内的某些数,放到大小为 `n + 1` 的数组中,数组要放满,所以一定会有一个重复的数字,找出这个重复的数字。比如 `[2,2,2,1]`。 - -假设重复的数字只有一个。 - -解法一和解法二先不考虑题目中 `Note` 的要求。 - -# 解法一 排序 - -最简单的,先排序,然后两两判断即可。 - -```java -public int findDuplicate(int[] nums) { - Arrays.sort(nums); - for (int i = 0; i < nums.length - 1; i++) { - if (nums[i] == nums[i + 1]) { - return nums[i]; - } - } - return -1; -} -``` - -# 解法二 HashSet - -判断重复数字,可以用 `HashSet`,这个方法经常用了。 - -```java -public int findDuplicate(int[] nums) { - HashSet set = new HashSet<>(); - for (int i = 0; i < nums.length; i++) { - if (set.contains(nums[i])) { - return nums[i]; - } - set.add(nums[i]); - } - return -1; -} -``` - -# 解法三 二分查找 - -参考 [这里](https://leetcode.com/problems/find-the-duplicate-number/discuss/72844/Two-Solutions-(with-explanation)%3A-O(nlog(n)) 。 - -我们知道二分查找要求有序,但是给定的数组不是有序的,那么怎么用二分查找呢? - -原数组不是有序,但是我们知道重复的那个数字肯定是 `1` 到 `n` 中的某一个,而 `1,2...,n` 就是一个有序序列。因此我们可以对 `1,2...,n` 进行二分查找。 - - `mid = (1 + n) / 2`,接下来判断最终答案是在 `[1, mid]` 中还是在 `[mid + 1, n]` 中。 - -我们只需要统计原数组中小于等于 `mid` 的个数,记为 `count`。 - -如果 `count > mid` ,鸽巢原理,在 `[1,mid]` 范围内的数字个数超过了 `mid` ,所以一定有一个重复数字。 - -否则的话,既然不在 `[1,mid]` ,那么最终答案一定在 `[mid + 1, n]` 中。 - -```java -public int findDuplicate(int[] nums) { - int n = nums.length - 1; - int low = 1; - int high = n; - while (low < high) { - int mid = (low + high) >>> 1; - int count = 0; - for (int i = 0; i < nums.length; i++) { - if (nums[i] <= mid) { - count++; - } - } - if (count > mid) { - high = mid; - } else { - low = mid + 1; - } - } - return low; -} - -``` - -# 解法四 二进制 - -参考 [这里](https://leetcode.com/problems/find-the-duplicate-number/discuss/72872/O(32*N)-solution-using-bit-manipulation-in-10-lines)。[137 题](https://leetcode.wang/leetcode-137-Single-NumberII.html#解法三-位操作) 以及 [169 题](https://leetcode.wang/leetcode-169-Majority-Element.html#解法二-位运算) 其实已经用过这个思想,但还是不容易往这方面想。 - -主要就是我们要把数字放眼到二进制。 - -然后依次统计数组中每一位 `1` 的个数,记为 `a[i]`。再依次统计 `1` 到 `n` 中每一位 `1` 的个数,记为 `b[i]`。`i` 代表的是哪一位,因为是 `int`,所以范围是 `0` 到 `32`。 - -记重复的数字是 `res`。 - -如果 `a[i] > b[i]` 也就意味着 `res` 当前位是 `1`。 - -否则的话,`res` 当前位就是 `0`。 - -举个例子吧,`1 3 4 2 2`。 - -```java -1 3 4 2 2 写成 2 进制 -1 [0 0 1] -3 [0 1 1] -4 [1 0 0] -2 [0 1 0] -2 [0 1 0] - -把 1 到 n,也就是 1 2 3 4 也写成 2 进制 -1 [0 0 1] -2 [0 1 0] -3 [0 1 1] -4 [1 0 0] - -依次统计每一列 1 的个数, res = XXX - -原数组最后一列 1 的个数是 2 -1 到 4 最后一列 1 的个数是 2 -2 不大于 2,所以当前位是 0, res = XX0 - -原数组倒数第二列 1 的个数是 3 -1 到 4 倒数第二列 1 的个数是 2 -3 大于 2,所以当前位是 1, res = X10 - -原数组倒数第三列 1 的个数是 1 -1 到 4 倒数第三列 1 的个数是 1 -1 不大于 1,所以当前位是 0, res = 010 - -所以 res = 010, 也就是 2 -``` - -上边是重复数字的重复次数是 `2` 的情况,如果重复次数大于 `2` 的话上边的结论依旧成立。 - -简单的想一下,`1 3 4 2 2` ,因为 `2` 的倒数第二位的二进制位是 `1`,所以原数组在倒数第二列中 `1 ` 的个数会比`1` 到 `4` 这个序列倒数第二列中 `1 ` 的个数多 `1` 个。如果原数组其他的数变成了 `2` 呢?也就`2` 的重复次数大于 `2`。 - -如果是 `1` 变成了 `2`,数组变成 `2 3 4 2 2` , 那么倒数第二列中 `1 ` 的个数又会增加 `1`。 - -如果是 `3` 变成了 `2`,数组变成 `1 2 4 2 2` , 那么倒数第二列中 `1 ` 的个数不会变化。 - -所以不管怎么样,如果重复数字的某一列是 `1`,那么当前列 `1` 的个数一定会比 `1` 到 `n` 序列中 `1` 的个数多。 - -```java -public int findDuplicate(int[] nums) { - int res = 0; - int n = nums.length; - //统计每一列 1 的个数 - for (int i = 0; i < 32; i++) { - int a = 0; - int b = 0; - int mask = (1 << i); - for (int j = 0; j < n; j++) { - //统计原数组当前列 1 的个数 - if ((nums[j] & mask) > 0) { - a++; - } - //统计 1 到 n 序列中当前列 1 的个数 - if ((j & mask) > 0) { - b++; - } - } - if (a > b) { - res = res | mask; - } - } - return res; -} -``` - -# 解法五 - -参考 [这里](https://leetcode.com/problems/find-the-duplicate-number/discuss/72846/My-easy-understood-solution-with-O(n)-time-and-O(1)-space-without-modifying-the-array.-With-clear-explanation.) ,一个神奇的解法了。 - -把数组的值看成 `next` 指针,数组的下标看成节点的索引。因为数组中至少有两个值一样,也说明有两个节点指向同一个位置,所以一定会出现环。 - -举个例子,`3 1 3 4 2` 可以看成下图的样子。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/287_2.jpg) - -```java -nums[0] = 3 -nums[3] = 4 -nums[4] = 2 -nums[2] = 3 -``` - -所以我们要做的就是找到上图中有环链表的入口点 `3`,也就是 [142 题](https://leetcode.wang/leetcode-142-Linked-List-CycleII.html) 。 - -具体证明不说了,只介绍方法,感兴趣的话可以到 [142 题](https://leetcode.wang/leetcode-142-Linked-List-CycleII.html) 看一下。 - -我们需要快慢指针,同时从起点出发,慢指针一次走一步,快指针一次走两步,然后记录快慢指针相遇的点。 - -之后再用两个指针,一个指针从起点出发,一个指针从相遇点出发,当他们再次相遇的时候就是入口点了。 - -```java -public int findDuplicate(int[] nums) { - int slow = nums[0]; - int fast = nums[nums[0]]; - //寻找相遇点 - while (slow != fast) { - slow = nums[slow]; - fast = nums[nums[fast]]; - } - //slow 从起点出发, fast 从相遇点出发, 一次走一步 - slow = 0; - while (slow != fast) { - slow = nums[slow]; - fast = nums[fast]; - } - return slow; -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/287.jpg) + +将 `1` 到 `n` 范围内的某些数,放到大小为 `n + 1` 的数组中,数组要放满,所以一定会有一个重复的数字,找出这个重复的数字。比如 `[2,2,2,1]`。 + +假设重复的数字只有一个。 + +解法一和解法二先不考虑题目中 `Note` 的要求。 + +# 解法一 排序 + +最简单的,先排序,然后两两判断即可。 + +```java +public int findDuplicate(int[] nums) { + Arrays.sort(nums); + for (int i = 0; i < nums.length - 1; i++) { + if (nums[i] == nums[i + 1]) { + return nums[i]; + } + } + return -1; +} +``` + +# 解法二 HashSet + +判断重复数字,可以用 `HashSet`,这个方法经常用了。 + +```java +public int findDuplicate(int[] nums) { + HashSet set = new HashSet<>(); + for (int i = 0; i < nums.length; i++) { + if (set.contains(nums[i])) { + return nums[i]; + } + set.add(nums[i]); + } + return -1; +} +``` + +# 解法三 二分查找 + +参考 [这里](https://leetcode.com/problems/find-the-duplicate-number/discuss/72844/Two-Solutions-(with-explanation)%3A-O(nlog(n)) 。 + +我们知道二分查找要求有序,但是给定的数组不是有序的,那么怎么用二分查找呢? + +原数组不是有序,但是我们知道重复的那个数字肯定是 `1` 到 `n` 中的某一个,而 `1,2...,n` 就是一个有序序列。因此我们可以对 `1,2...,n` 进行二分查找。 + + `mid = (1 + n) / 2`,接下来判断最终答案是在 `[1, mid]` 中还是在 `[mid + 1, n]` 中。 + +我们只需要统计原数组中小于等于 `mid` 的个数,记为 `count`。 + +如果 `count > mid` ,鸽巢原理,在 `[1,mid]` 范围内的数字个数超过了 `mid` ,所以一定有一个重复数字。 + +否则的话,既然不在 `[1,mid]` ,那么最终答案一定在 `[mid + 1, n]` 中。 + +```java +public int findDuplicate(int[] nums) { + int n = nums.length - 1; + int low = 1; + int high = n; + while (low < high) { + int mid = (low + high) >>> 1; + int count = 0; + for (int i = 0; i < nums.length; i++) { + if (nums[i] <= mid) { + count++; + } + } + if (count > mid) { + high = mid; + } else { + low = mid + 1; + } + } + return low; +} + +``` + +# 解法四 二进制 + +参考 [这里](https://leetcode.com/problems/find-the-duplicate-number/discuss/72872/O(32*N)-solution-using-bit-manipulation-in-10-lines)。[137 题](https://leetcode.wang/leetcode-137-Single-NumberII.html#解法三-位操作) 以及 [169 题](https://leetcode.wang/leetcode-169-Majority-Element.html#解法二-位运算) 其实已经用过这个思想,但还是不容易往这方面想。 + +主要就是我们要把数字放眼到二进制。 + +然后依次统计数组中每一位 `1` 的个数,记为 `a[i]`。再依次统计 `1` 到 `n` 中每一位 `1` 的个数,记为 `b[i]`。`i` 代表的是哪一位,因为是 `int`,所以范围是 `0` 到 `32`。 + +记重复的数字是 `res`。 + +如果 `a[i] > b[i]` 也就意味着 `res` 当前位是 `1`。 + +否则的话,`res` 当前位就是 `0`。 + +举个例子吧,`1 3 4 2 2`。 + +```java +1 3 4 2 2 写成 2 进制 +1 [0 0 1] +3 [0 1 1] +4 [1 0 0] +2 [0 1 0] +2 [0 1 0] + +把 1 到 n,也就是 1 2 3 4 也写成 2 进制 +1 [0 0 1] +2 [0 1 0] +3 [0 1 1] +4 [1 0 0] + +依次统计每一列 1 的个数, res = XXX + +原数组最后一列 1 的个数是 2 +1 到 4 最后一列 1 的个数是 2 +2 不大于 2,所以当前位是 0, res = XX0 + +原数组倒数第二列 1 的个数是 3 +1 到 4 倒数第二列 1 的个数是 2 +3 大于 2,所以当前位是 1, res = X10 + +原数组倒数第三列 1 的个数是 1 +1 到 4 倒数第三列 1 的个数是 1 +1 不大于 1,所以当前位是 0, res = 010 + +所以 res = 010, 也就是 2 +``` + +上边是重复数字的重复次数是 `2` 的情况,如果重复次数大于 `2` 的话上边的结论依旧成立。 + +简单的想一下,`1 3 4 2 2` ,因为 `2` 的倒数第二位的二进制位是 `1`,所以原数组在倒数第二列中 `1 ` 的个数会比`1` 到 `4` 这个序列倒数第二列中 `1 ` 的个数多 `1` 个。如果原数组其他的数变成了 `2` 呢?也就`2` 的重复次数大于 `2`。 + +如果是 `1` 变成了 `2`,数组变成 `2 3 4 2 2` , 那么倒数第二列中 `1 ` 的个数又会增加 `1`。 + +如果是 `3` 变成了 `2`,数组变成 `1 2 4 2 2` , 那么倒数第二列中 `1 ` 的个数不会变化。 + +所以不管怎么样,如果重复数字的某一列是 `1`,那么当前列 `1` 的个数一定会比 `1` 到 `n` 序列中 `1` 的个数多。 + +```java +public int findDuplicate(int[] nums) { + int res = 0; + int n = nums.length; + //统计每一列 1 的个数 + for (int i = 0; i < 32; i++) { + int a = 0; + int b = 0; + int mask = (1 << i); + for (int j = 0; j < n; j++) { + //统计原数组当前列 1 的个数 + if ((nums[j] & mask) > 0) { + a++; + } + //统计 1 到 n 序列中当前列 1 的个数 + if ((j & mask) > 0) { + b++; + } + } + if (a > b) { + res = res | mask; + } + } + return res; +} +``` + +# 解法五 + +参考 [这里](https://leetcode.com/problems/find-the-duplicate-number/discuss/72846/My-easy-understood-solution-with-O(n)-time-and-O(1)-space-without-modifying-the-array.-With-clear-explanation.) ,一个神奇的解法了。 + +把数组的值看成 `next` 指针,数组的下标看成节点的索引。因为数组中至少有两个值一样,也说明有两个节点指向同一个位置,所以一定会出现环。 + +举个例子,`3 1 3 4 2` 可以看成下图的样子。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/287_2.jpg) + +```java +nums[0] = 3 +nums[3] = 4 +nums[4] = 2 +nums[2] = 3 +``` + +所以我们要做的就是找到上图中有环链表的入口点 `3`,也就是 [142 题](https://leetcode.wang/leetcode-142-Linked-List-CycleII.html) 。 + +具体证明不说了,只介绍方法,感兴趣的话可以到 [142 题](https://leetcode.wang/leetcode-142-Linked-List-CycleII.html) 看一下。 + +我们需要快慢指针,同时从起点出发,慢指针一次走一步,快指针一次走两步,然后记录快慢指针相遇的点。 + +之后再用两个指针,一个指针从起点出发,一个指针从相遇点出发,当他们再次相遇的时候就是入口点了。 + +```java +public int findDuplicate(int[] nums) { + int slow = nums[0]; + int fast = nums[nums[0]]; + //寻找相遇点 + while (slow != fast) { + slow = nums[slow]; + fast = nums[nums[fast]]; + } + //slow 从起点出发, fast 从相遇点出发, 一次走一步 + slow = 0; + while (slow != fast) { + slow = nums[slow]; + fast = nums[fast]; + } + return slow; +} +``` + +# 总 + 看起来比较简单的一道题,思想用了不少。经典的二分,从二进制思考问题,以及最后将问题转换的思想,都很经典。 \ No newline at end of file diff --git a/leetcode-289-Game-of-Life.md b/leetcode-289-Game-of-Life.md index f5968a63f..ce58599b0 100644 --- a/leetcode-289-Game-of-Life.md +++ b/leetcode-289-Game-of-Life.md @@ -1,557 +1,557 @@ -# 题目描述(中等难度) - -289、Game of Life - -According to the [Wikipedia's article](https://en.wikipedia.org/wiki/Conway's_Game_of_Life): "The **Game of Life**, also known simply as **Life**, is a cellular automaton devised by the British mathematician John Horton Conway in 1970." - -Given a *board* with *m* by *n* cells, each cell has an initial state *live* (1) or *dead* (0). Each cell interacts with its [eight neighbors](https://en.wikipedia.org/wiki/Moore_neighborhood) (horizontal, vertical, diagonal) using the following four rules (taken from the above Wikipedia article): - -1. Any live cell with fewer than two live neighbors dies, as if caused by under-population. -2. Any live cell with two or three live neighbors lives on to the next generation. -3. Any live cell with more than three live neighbors dies, as if by over-population.. -4. Any dead cell with exactly three live neighbors becomes a live cell, as if by reproduction. - -Write a function to compute the next state (after one update) of the board given its current state. The next state is created by applying the above rules simultaneously to every cell in the current state, where births and deaths occur simultaneously. - -**Example:** - -``` -Input: -[ - [0,1,0], - [0,0,1], - [1,1,1], - [0,0,0] -] -Output: -[ - [0,0,0], - [1,0,1], - [0,1,1], - [0,1,0] -] -``` - -**Follow up**: - -1. Could you solve it in-place? Remember that the board needs to be updated at the same time: You cannot update some cells first and then use their updated values to update other cells. -2. In this question, we represent the board using a 2D array. In principle, the board is infinite, which would cause problems when the active area encroaches the border of the array. How would you address these problems? - -给一个二维矩阵,其中 `1` 代表活细胞,`0` 代表死细胞,然后去遵循下边的规则来更新。 - -* 如果活细胞周围八个位置的活细胞数少于两个,则该位置活细胞死亡; -* 如果活细胞周围八个位置有两个或三个活细胞,则该位置活细胞仍然存活; -* 如果活细胞周围八个位置有超过三个活细胞,则该位置活细胞死亡; -* 如果死细胞周围正好有三个活细胞,则该位置死细胞复活; - -更新的时候是整体更新,下一状态只取决于当前状态,和它将要变成什么无关。 - -# 解法一 - -最直接的想法,就是再用一个等大的矩阵,然后一个一个判断原矩阵每个元素的下一状态,然后存到新矩阵,最后用新矩阵覆盖原矩阵即可。 - -上边的想法虽然可行,但需要额外空间,我们考虑不需要额外空间的想法。 - -用一个常用的方法,我们可以将需要改变的数字先转成另一个数字,最后再将其还原。 - -具体的讲,如果某个数字需要由 `0` 变成 `1`,我们先把它变成 `-1`。 - -如果某个数字需要由 `1` 变成 `0`,我们先把它变成 `-2`。 - -这样做的话,在记录周围八个位置有多少 `1` 的时候,除了 `1` 以外,还要记录 `-2` 的个数。 - -最后再将所有的 `-1` 变成 `1`,`-2` 变成 `0` 即可。 - -看下代码。 - -```java -public void gameOfLife(int[][] board) { - int rows = board.length; - if (rows == 0) { - return; - } - int cols = board[0].length; - for (int r = 0; r < rows; r++) { - for (int c = 0; c < cols; c++) { - //周围八个位置有多少个 1 - int count = getOnes(r, c, rows, cols, board); - - //当前是 0, 周围有 3 个 1, 说明 0 需要变成 1, 记成 -1 - if (board[r][c] == 0) { - if (count == 3) { - board[r][c] = -1; - } - } - //当前是 1 - if (board[r][c] == 1) { - //当前 1 需要变成 0, 记成 -2 - if (count < 2 || count > 3) { - board[r][c] = -2; - } - } - - } - } - - //将所有数字还原 - for (int r = 0; r < rows; r++) { - for (int c = 0; c < cols; c++) { - if (board[r][c] == -1) { - board[r][c] = 1; - } - if (board[r][c] == -2) { - board[r][c] = 0; - } - } - } - -} -//需要统计周围八个位置 1 和 -2 的个数 -private int getOnes(int r, int c, int rows, int cols, int[][] board) { - int count = 0; - // 上 - if (r - 1 >= 0 && (board[r - 1][c] == 1 || board[r - 1][c] == -2)) { - count++; - } - // 下 - if (r + 1 < rows && (board[r + 1][c] == 1 || board[r + 1][c] == -2)) { - count++; - } - // 左 - if (c - 1 >= 0 && (board[r][c - 1] == 1 || board[r][c - 1] == -2)) { - count++; - } - // 右 - if (c + 1 < cols && (board[r][c + 1] == 1 || board[r][c + 1] == -2)) { - count++; - } - // 左上 - if (c - 1 >= 0 && r - 1 >= 0 && (board[r - 1][c - 1] == 1 || board[r - 1][c - 1] == -2)) { - count++; - } - // 左下 - if (c - 1 >= 0 && r + 1 < rows && (board[r + 1][c - 1] == 1 || board[r + 1][c - 1] == -2)) { - count++; - } - // 右上 - if (c + 1 < cols && r - 1 >= 0 && (board[r - 1][c + 1] == 1 || board[r - 1][c + 1] == -2)) { - count++; - } - // 右下 - if (c + 1 < cols && r + 1 < rows && (board[r + 1][c + 1] == 1 || board[r + 1][c + 1] == -2)) { - count++; - } - return count; -} -``` - -上边就是我直接想到的了,下边分享一下别人的技巧,使得上边的代码简洁些,但时间复杂度不会变化。 - -# 一些优化 - -主要是两方面,一方面是考虑在记录 `1` 变成 `0` 和 `0` 变成 `1` 时候要变成的数字,另一方面就是统计周围八个位置 `1` 的个数时候的写法。 - -分享 [@StefanPochmann](https://leetcode.com/problems/game-of-life/discuss/73230/C%2B%2B-O(1)-space-O(mn)-time) 的做法。 - -想法很简单,因为之前记录细胞生命是否活着的时候用的是 `0` 和 `1`,相当于只用了 `1` 个比特位来记录。把它们扩展一位,看成 `00` 和 `01`。 - -我们可以用新扩展的第二位来表示下次的状态,因为开始的时候倒数第二位默认是 `0`,所以在计算过程中我们只关心下一状态是 `1` 的时候,将自己本身的数(`0` 或者 `1` )通过和 `2` 进行异或即可。如果下一次状态是 `0` 就不需要管了。 - -这样做的好处就是在还原的时候,我们可以将其右移一位即可。 - -通过判断当前位置邻居中 `1` 的个数,然后通过下边的方式来更新。 - -```java -//count 代表当前位置邻居中 1 的个数 -//count == 3 的时候下一状态是 1, 或者 count == 2, 并且当前是 1 的时候下一状态是 1 -if(count == 3 || (board[r][c] == 1) && count == 2){ - board[r][c] |= 2; //2 的二进制是 10,相当于将第二位 置为 1 -} - - -//和下边的是等价的 -if(count == 3 || count + board[r][c] == 3){ - board[r][c] |= 2; //2 的二进制是 10,相当于将第二位 置为 1 -} -``` - -还有就是在统计周围八个位置 `1` 的个数时候,可以通过下边的方式,确定开始遍历的行和列,然后开始遍历。 - -```java -private int getOnes(int r, int c, int rows, int cols, int[][] board) { - int count = 0; - for (int i = Math.max(r - 1, 0); i <= Math.min(r + 1, rows - 1); i++) { - for (int j = Math.max(c - 1, 0); j <= Math.min(c + 1, cols - 1); j++) { - count += board[i][j] & 1; - } - } - //如果原来是 1,需要减去 - count -= board[i][j] & 1; - return count; -} -``` - -然后把代码综合起来。 - -```java -public void gameOfLife(int[][] board) { - int rows = board.length; - if (rows == 0) { - return; - } - int cols = board[0].length; - for (int r = 0; r < rows; r++) { - for (int c = 0; c < cols; c++) { - //周围八个位置有多少个 1 - int count = getOnes(r, c, rows, cols, board); - - //count == 3 的时候下一状态是 1, 或者 count == 2, 并且当前是 1 的时候下一状态是 1 - if(count == 3 || (board[r][c] == 1) && count == 2){ - board[r][c] |= 2; //2 的二进制是 10,相当于将第二位 置为 1 - } - - } - } - - //将所有数字还原 - for (int r = 0; r < rows; r++) { - for (int c = 0; c < cols; c++) { - //右移一位还原 - board[r][c] >>= 1; - } - } - -} -//需要统计周围八个位置 1 的个数 -private int getOnes(int r, int c, int rows, int cols, int[][] board) { - int count = 0; - for (int i = Math.max(r - 1, 0); i <= Math.min(r + 1, rows - 1); i++) { - for (int j = Math.max(c - 1, 0); j <= Math.min(c + 1, cols - 1); j++) { - count += board[i][j] & 1; - } - } - //如果原来是 1, 需要减去 1 - count -= board[r][c] & 1; - return count; -} -``` - -当然如果对二进制操作不熟,也可以使用 [这里](https://leetcode.com/problems/game-of-life/discuss/73223/Easiest-JAVA-solution-with-explanation) 的代码。 - -把上边代码的这一部分。 - -```java -//count == 3 的时候下一状态是 1, 或者 count == 2, 并且当前是 1 的时候下一状态是 1 -if(count == 3 || (board[r][c] == 1) && count == 2){ - board[r][c] |= 2; //2 的二进制是 10,相当于将第二位 置为 1 -} -``` - -按照文章开头的算法,更具体的分类。 - -```java -if (board[r][c] == 1 && (count == 2 || count == 3) { - board[r][c] = 3; // Make the 2nd bit 1: 01 ---> 11 -} -if (board[r][c] == 0 && count == 3) { - board[r][c] = 2; // Make the 2nd bit 1: 00 ---> 10 -} -``` - -还有 [这里](https://leetcode.com/problems/game-of-life/discuss/73366/Clean-O(1)-space-O(mn)-time-Java-Solution) 的一种想法。 - -如果是 `0` 变成 `1`,将赋值为 `3`。如果是 `1` 变成 `0` 就赋值成 `2` 。 - -这样做的好处就是,在还原的时候通过对 `2` 求余即可。 - -```java - board[i][j] %=2; -``` - -最后还有一种求周围八个位置 `1`的个数的思路。 - -参考 [这里](https://leetcode.com/problems/game-of-life/discuss/73252/C%2B%2B-AC-Code-O(1)-space-O(mn)-time)。我们可以先初始化一个数值对,然后通过和当前位置相加来得到相应的值。主要修改了 `getOnes` 函数。 - -```java -public void gameOfLife(int[][] board) { - int rows = board.length; - if (rows == 0) { - return; - } - int cols = board[0].length; - for (int r = 0; r < rows; r++) { - for (int c = 0; c < cols; c++) { - //周围八个位置有多少个 1 - int count = getOnes(r, c, rows, cols, board); - - //当前是 0, 周围有 3 个 1, 说明 0 需要变成 1, 记成 3 - if (board[r][c] == 0) { - if (count == 3) { - board[r][c] = 3; - } - } - //当前是 1 - if (board[r][c] == 1) { - //当前 1 需要变成 0, 记成 2 - if (count < 2 || count > 3) { - board[r][c] = 2; - } - } - - } - } - - //将所有数字还原 - for (int r = 0; r < rows; r++) { - for (int c = 0; c < cols; c++) { - board[r][c] %= 2; - } - } - -} - -//作为类的静态成员变量,仅初始化一次 -static int d[][] = {% raw %}{{% endraw %}{1,-1},{1,0},{1,1},{0,-1},{0,1},{-1,-1},{-1,0},{-1,1}}; -//需要统计周围八个位置 1 和 2 的个数 -private int getOnes(int r, int c, int rows, int cols, int[][] board) { - int count = 0; - for(int k = 0; k < 8; k++){ - int x = d[k][0] + r; - int y = d[k][1] + c; - if(x < 0 || x >= rows || y < 0 || y >= cols) { - continue; - } - if(board[x][y] == 1 || board[x][y] == 2) { - count++; - } - } - return count; -} -``` - -# 解法二 - -[这里](https://leetcode.com/problems/game-of-life/discuss/73335/C%2B%2B-O(mn)-time-O(1)-space-sol) 看到一个完全不同的思路,分享一下。 - -```java -Y Y Y -Y @ X -X X X -``` - -对于这道题,如果按照从上到下,从左到右的顺序遍历。那么当我们考虑 `@` 位置的时候,`Y` 位置已经全部更新了,但我们需要 `Y` 位置之前的信息,怎么办呢? - -因为 `@` 有八个邻居,是一个一般化的位置,所以我们讨论遍历到 `@` 的时候该怎么处理。 - -在更新 `@` 的时候,考虑它还没有进行更新的邻居,也就是 `X` 的位置。如果 `@` 位置是 `1`,那么就将所有 `X` 位置的值加上 `2`(也可以是其它值,但 `2` 是个不错的选择)。如果 `X` 位置原来的值是 `1` ,那么就将 `@` 的位置加上 `2` 。 - -这样的话,如果 `@` 原本是 `1`,它周围有 `2` 个或者 `3` 个 `1`,那么它就会被加成 `5` 或者 `7` 。如果原本是 `0` ,它周围有 `3` 个 `1`,那么就会被加成 `6`。也就是当 `@` 变成`5, 6, 7` 的时候,它的下一次状态是 `1`,否则话的就是 `0`。此时我们可以直接将 `@` 更新成 `0` 或者 `1`,因为如果 `@` 是 `1` 的话,已经将和它是邻居的所有位置进行了加 `2` 。 - -还需要解决一个问题,遍历 `@` 的时候,它可能已经被之前的邻居 `Y` 加了若干个 `2` 。那么此时怎么判断 `@` 原来的位置是 `1` 还是 `0` 呢?很简单,如果原来是 `1` ,因为每次加的是 `2`,所以它一定是个奇数。反之,它就是偶数。 - -再结合代码看一下。 - -```java -public void gameOfLife(int[][] board) { - int rows = board.length; - if (rows == 0) { - return; - } - int cols = board[0].length; - for (int r = 0; r < rows; r++) { - for (int c = 0; c < cols; c++) { - // 右 - check(r, c, r, c + 1, rows, cols, board); - // 右下 - check(r, c, r + 1, c + 1, rows, cols, board); - // 下 - check(r, c, r + 1, c, rows, cols, board); - // 左下 - check(r, c, r + 1, c - 1, rows, cols, board); - //5, 6, 7 代表下一状态是 1 - if (board[r][c] >= 5 && board[r][c] <= 7) { - board[r][c] = 1; - } else { - board[r][c] = 0; - } - - } - } -} - -private void check(int rCur, int cCur, int rNext, int cNext, int rows, int cols, int[][] board) { - if (rNext < 0 || cNext < 0 || rNext >= rows || cNext >= cols) { - return; - } - //如果是奇数说明之前是 1, 更新它之后的邻居 - if (board[rCur][cCur] % 2 == 1) { - board[rNext][cNext] += 2; - } - //如果是奇数说明之前是 1, 更新当前的位置值 - if (board[rNext][cNext] % 2 == 1) { - board[rCur][cCur] += 2; - } -} -``` - -# 扩展 - -题目中 `Follow up` 第二点指出,如果给定的 `board` 是无限的,我们该怎么处理呢?这是一个开放性的问题,讨论的点会有很多,每个人的想法可能都不一样,下边结合 [官方](https://leetcode.com/problems/game-of-life/solution/) 的讲解说一下。 - -首先在程序中,无限 `board` 肯定是不存在的,它只不过是一个很大很大的矩阵,大到无法直接将矩阵读到内存中。 - -第一个能想到的解决方案就是我们不需要直接将整个矩阵读到内存中,而是每次读出矩阵的三行,每次处理中间那行,然后把结果写入到文件。 - -第二个的话,如果是一个很大很大的矩阵,很有可能矩阵是一个稀疏矩阵,而我们只关心每个位置的八个邻居中 `1` 的个数,所以我们可以在内存中仅仅保存 `1` 的坐标。 - -如果题目给定的我们就是所有 `1` 的坐标,那么可以有下边的算法。 - -用一个 `HashMap` 去统计每个位置它的邻居的 `1` 的个数。只需要遍历所有 `1` 的坐标,然后将它八个邻居相应的 `HashMap` 的值进行加 `1`。 - -参考 [ruben3](https://leetcode.com/ruben3) 的 java 代码。 - -```java -//live 保存了所有是 1 的坐标, Coord 是坐标类 -private Set gameOfLife(Set live) { - Map neighbours = new HashMap<>(); - for (Coord cell : live) { - for (int i = cell.i-1; i newLive = new HashSet<>();//下一个状态的所有 1 的坐标 - for (Map.Entry cell : neighbours.entrySet()) { - //当前位置周围有 3 个活细胞,或者有两个活细胞, 并且当前位置是一个活细胞 - if (cell.getValue() == 3 || cell.getValue() == 2 && live.contains(cell.getKey())) { - newLive.add(cell.getKey()); - } - } - return newLive; -} - -//相当于一个坐标类 -private static class Coord { - int i; - int j; - private Coord(int i, int j) { - this.i = i; - this.j = j; - } - public boolean equals(Object o) { - return o instanceof Coord && ((Coord)o).i == i && ((Coord)o).j == j; - } - public int hashCode() { - int hashCode = 1; - hashCode = 31 * hashCode + i; - hashCode = 31 * hashCode + j; - return hashCode; - } -} - -//为了验证这个算法, 我们手动去求了 1 的所有坐标,并且调用上边的函数来验证我们的算法 -public void gameOfLife(int[][] board) { - Set live = new HashSet<>(); - int m = board.length; - int n = board[0].length; - for (int i = 0; i 3) { + board[r][c] = -2; + } + } + + } + } + + //将所有数字还原 + for (int r = 0; r < rows; r++) { + for (int c = 0; c < cols; c++) { + if (board[r][c] == -1) { + board[r][c] = 1; + } + if (board[r][c] == -2) { + board[r][c] = 0; + } + } + } + +} +//需要统计周围八个位置 1 和 -2 的个数 +private int getOnes(int r, int c, int rows, int cols, int[][] board) { + int count = 0; + // 上 + if (r - 1 >= 0 && (board[r - 1][c] == 1 || board[r - 1][c] == -2)) { + count++; + } + // 下 + if (r + 1 < rows && (board[r + 1][c] == 1 || board[r + 1][c] == -2)) { + count++; + } + // 左 + if (c - 1 >= 0 && (board[r][c - 1] == 1 || board[r][c - 1] == -2)) { + count++; + } + // 右 + if (c + 1 < cols && (board[r][c + 1] == 1 || board[r][c + 1] == -2)) { + count++; + } + // 左上 + if (c - 1 >= 0 && r - 1 >= 0 && (board[r - 1][c - 1] == 1 || board[r - 1][c - 1] == -2)) { + count++; + } + // 左下 + if (c - 1 >= 0 && r + 1 < rows && (board[r + 1][c - 1] == 1 || board[r + 1][c - 1] == -2)) { + count++; + } + // 右上 + if (c + 1 < cols && r - 1 >= 0 && (board[r - 1][c + 1] == 1 || board[r - 1][c + 1] == -2)) { + count++; + } + // 右下 + if (c + 1 < cols && r + 1 < rows && (board[r + 1][c + 1] == 1 || board[r + 1][c + 1] == -2)) { + count++; + } + return count; +} +``` + +上边就是我直接想到的了,下边分享一下别人的技巧,使得上边的代码简洁些,但时间复杂度不会变化。 + +# 一些优化 + +主要是两方面,一方面是考虑在记录 `1` 变成 `0` 和 `0` 变成 `1` 时候要变成的数字,另一方面就是统计周围八个位置 `1` 的个数时候的写法。 + +分享 [@StefanPochmann](https://leetcode.com/problems/game-of-life/discuss/73230/C%2B%2B-O(1)-space-O(mn)-time) 的做法。 + +想法很简单,因为之前记录细胞生命是否活着的时候用的是 `0` 和 `1`,相当于只用了 `1` 个比特位来记录。把它们扩展一位,看成 `00` 和 `01`。 + +我们可以用新扩展的第二位来表示下次的状态,因为开始的时候倒数第二位默认是 `0`,所以在计算过程中我们只关心下一状态是 `1` 的时候,将自己本身的数(`0` 或者 `1` )通过和 `2` 进行异或即可。如果下一次状态是 `0` 就不需要管了。 + +这样做的好处就是在还原的时候,我们可以将其右移一位即可。 + +通过判断当前位置邻居中 `1` 的个数,然后通过下边的方式来更新。 + +```java +//count 代表当前位置邻居中 1 的个数 +//count == 3 的时候下一状态是 1, 或者 count == 2, 并且当前是 1 的时候下一状态是 1 +if(count == 3 || (board[r][c] == 1) && count == 2){ + board[r][c] |= 2; //2 的二进制是 10,相当于将第二位 置为 1 +} + + +//和下边的是等价的 +if(count == 3 || count + board[r][c] == 3){ + board[r][c] |= 2; //2 的二进制是 10,相当于将第二位 置为 1 +} +``` + +还有就是在统计周围八个位置 `1` 的个数时候,可以通过下边的方式,确定开始遍历的行和列,然后开始遍历。 + +```java +private int getOnes(int r, int c, int rows, int cols, int[][] board) { + int count = 0; + for (int i = Math.max(r - 1, 0); i <= Math.min(r + 1, rows - 1); i++) { + for (int j = Math.max(c - 1, 0); j <= Math.min(c + 1, cols - 1); j++) { + count += board[i][j] & 1; + } + } + //如果原来是 1,需要减去 + count -= board[i][j] & 1; + return count; +} +``` + +然后把代码综合起来。 + +```java +public void gameOfLife(int[][] board) { + int rows = board.length; + if (rows == 0) { + return; + } + int cols = board[0].length; + for (int r = 0; r < rows; r++) { + for (int c = 0; c < cols; c++) { + //周围八个位置有多少个 1 + int count = getOnes(r, c, rows, cols, board); + + //count == 3 的时候下一状态是 1, 或者 count == 2, 并且当前是 1 的时候下一状态是 1 + if(count == 3 || (board[r][c] == 1) && count == 2){ + board[r][c] |= 2; //2 的二进制是 10,相当于将第二位 置为 1 + } + + } + } + + //将所有数字还原 + for (int r = 0; r < rows; r++) { + for (int c = 0; c < cols; c++) { + //右移一位还原 + board[r][c] >>= 1; + } + } + +} +//需要统计周围八个位置 1 的个数 +private int getOnes(int r, int c, int rows, int cols, int[][] board) { + int count = 0; + for (int i = Math.max(r - 1, 0); i <= Math.min(r + 1, rows - 1); i++) { + for (int j = Math.max(c - 1, 0); j <= Math.min(c + 1, cols - 1); j++) { + count += board[i][j] & 1; + } + } + //如果原来是 1, 需要减去 1 + count -= board[r][c] & 1; + return count; +} +``` + +当然如果对二进制操作不熟,也可以使用 [这里](https://leetcode.com/problems/game-of-life/discuss/73223/Easiest-JAVA-solution-with-explanation) 的代码。 + +把上边代码的这一部分。 + +```java +//count == 3 的时候下一状态是 1, 或者 count == 2, 并且当前是 1 的时候下一状态是 1 +if(count == 3 || (board[r][c] == 1) && count == 2){ + board[r][c] |= 2; //2 的二进制是 10,相当于将第二位 置为 1 +} +``` + +按照文章开头的算法,更具体的分类。 + +```java +if (board[r][c] == 1 && (count == 2 || count == 3) { + board[r][c] = 3; // Make the 2nd bit 1: 01 ---> 11 +} +if (board[r][c] == 0 && count == 3) { + board[r][c] = 2; // Make the 2nd bit 1: 00 ---> 10 +} +``` + +还有 [这里](https://leetcode.com/problems/game-of-life/discuss/73366/Clean-O(1)-space-O(mn)-time-Java-Solution) 的一种想法。 + +如果是 `0` 变成 `1`,将赋值为 `3`。如果是 `1` 变成 `0` 就赋值成 `2` 。 + +这样做的好处就是,在还原的时候通过对 `2` 求余即可。 + +```java + board[i][j] %=2; +``` + +最后还有一种求周围八个位置 `1`的个数的思路。 + +参考 [这里](https://leetcode.com/problems/game-of-life/discuss/73252/C%2B%2B-AC-Code-O(1)-space-O(mn)-time)。我们可以先初始化一个数值对,然后通过和当前位置相加来得到相应的值。主要修改了 `getOnes` 函数。 + +```java +public void gameOfLife(int[][] board) { + int rows = board.length; + if (rows == 0) { + return; + } + int cols = board[0].length; + for (int r = 0; r < rows; r++) { + for (int c = 0; c < cols; c++) { + //周围八个位置有多少个 1 + int count = getOnes(r, c, rows, cols, board); + + //当前是 0, 周围有 3 个 1, 说明 0 需要变成 1, 记成 3 + if (board[r][c] == 0) { + if (count == 3) { + board[r][c] = 3; + } + } + //当前是 1 + if (board[r][c] == 1) { + //当前 1 需要变成 0, 记成 2 + if (count < 2 || count > 3) { + board[r][c] = 2; + } + } + + } + } + + //将所有数字还原 + for (int r = 0; r < rows; r++) { + for (int c = 0; c < cols; c++) { + board[r][c] %= 2; + } + } + +} + +//作为类的静态成员变量,仅初始化一次 +static int d[][] = {% raw %}{{% endraw %}{1,-1},{1,0},{1,1},{0,-1},{0,1},{-1,-1},{-1,0},{-1,1}}; +//需要统计周围八个位置 1 和 2 的个数 +private int getOnes(int r, int c, int rows, int cols, int[][] board) { + int count = 0; + for(int k = 0; k < 8; k++){ + int x = d[k][0] + r; + int y = d[k][1] + c; + if(x < 0 || x >= rows || y < 0 || y >= cols) { + continue; + } + if(board[x][y] == 1 || board[x][y] == 2) { + count++; + } + } + return count; +} +``` + +# 解法二 + +[这里](https://leetcode.com/problems/game-of-life/discuss/73335/C%2B%2B-O(mn)-time-O(1)-space-sol) 看到一个完全不同的思路,分享一下。 + +```java +Y Y Y +Y @ X +X X X +``` + +对于这道题,如果按照从上到下,从左到右的顺序遍历。那么当我们考虑 `@` 位置的时候,`Y` 位置已经全部更新了,但我们需要 `Y` 位置之前的信息,怎么办呢? + +因为 `@` 有八个邻居,是一个一般化的位置,所以我们讨论遍历到 `@` 的时候该怎么处理。 + +在更新 `@` 的时候,考虑它还没有进行更新的邻居,也就是 `X` 的位置。如果 `@` 位置是 `1`,那么就将所有 `X` 位置的值加上 `2`(也可以是其它值,但 `2` 是个不错的选择)。如果 `X` 位置原来的值是 `1` ,那么就将 `@` 的位置加上 `2` 。 + +这样的话,如果 `@` 原本是 `1`,它周围有 `2` 个或者 `3` 个 `1`,那么它就会被加成 `5` 或者 `7` 。如果原本是 `0` ,它周围有 `3` 个 `1`,那么就会被加成 `6`。也就是当 `@` 变成`5, 6, 7` 的时候,它的下一次状态是 `1`,否则话的就是 `0`。此时我们可以直接将 `@` 更新成 `0` 或者 `1`,因为如果 `@` 是 `1` 的话,已经将和它是邻居的所有位置进行了加 `2` 。 + +还需要解决一个问题,遍历 `@` 的时候,它可能已经被之前的邻居 `Y` 加了若干个 `2` 。那么此时怎么判断 `@` 原来的位置是 `1` 还是 `0` 呢?很简单,如果原来是 `1` ,因为每次加的是 `2`,所以它一定是个奇数。反之,它就是偶数。 + +再结合代码看一下。 + +```java +public void gameOfLife(int[][] board) { + int rows = board.length; + if (rows == 0) { + return; + } + int cols = board[0].length; + for (int r = 0; r < rows; r++) { + for (int c = 0; c < cols; c++) { + // 右 + check(r, c, r, c + 1, rows, cols, board); + // 右下 + check(r, c, r + 1, c + 1, rows, cols, board); + // 下 + check(r, c, r + 1, c, rows, cols, board); + // 左下 + check(r, c, r + 1, c - 1, rows, cols, board); + //5, 6, 7 代表下一状态是 1 + if (board[r][c] >= 5 && board[r][c] <= 7) { + board[r][c] = 1; + } else { + board[r][c] = 0; + } + + } + } +} + +private void check(int rCur, int cCur, int rNext, int cNext, int rows, int cols, int[][] board) { + if (rNext < 0 || cNext < 0 || rNext >= rows || cNext >= cols) { + return; + } + //如果是奇数说明之前是 1, 更新它之后的邻居 + if (board[rCur][cCur] % 2 == 1) { + board[rNext][cNext] += 2; + } + //如果是奇数说明之前是 1, 更新当前的位置值 + if (board[rNext][cNext] % 2 == 1) { + board[rCur][cCur] += 2; + } +} +``` + +# 扩展 + +题目中 `Follow up` 第二点指出,如果给定的 `board` 是无限的,我们该怎么处理呢?这是一个开放性的问题,讨论的点会有很多,每个人的想法可能都不一样,下边结合 [官方](https://leetcode.com/problems/game-of-life/solution/) 的讲解说一下。 + +首先在程序中,无限 `board` 肯定是不存在的,它只不过是一个很大很大的矩阵,大到无法直接将矩阵读到内存中。 + +第一个能想到的解决方案就是我们不需要直接将整个矩阵读到内存中,而是每次读出矩阵的三行,每次处理中间那行,然后把结果写入到文件。 + +第二个的话,如果是一个很大很大的矩阵,很有可能矩阵是一个稀疏矩阵,而我们只关心每个位置的八个邻居中 `1` 的个数,所以我们可以在内存中仅仅保存 `1` 的坐标。 + +如果题目给定的我们就是所有 `1` 的坐标,那么可以有下边的算法。 + +用一个 `HashMap` 去统计每个位置它的邻居的 `1` 的个数。只需要遍历所有 `1` 的坐标,然后将它八个邻居相应的 `HashMap` 的值进行加 `1`。 + +参考 [ruben3](https://leetcode.com/ruben3) 的 java 代码。 + +```java +//live 保存了所有是 1 的坐标, Coord 是坐标类 +private Set gameOfLife(Set live) { + Map neighbours = new HashMap<>(); + for (Coord cell : live) { + for (int i = cell.i-1; i newLive = new HashSet<>();//下一个状态的所有 1 的坐标 + for (Map.Entry cell : neighbours.entrySet()) { + //当前位置周围有 3 个活细胞,或者有两个活细胞, 并且当前位置是一个活细胞 + if (cell.getValue() == 3 || cell.getValue() == 2 && live.contains(cell.getKey())) { + newLive.add(cell.getKey()); + } + } + return newLive; +} + +//相当于一个坐标类 +private static class Coord { + int i; + int j; + private Coord(int i, int j) { + this.i = i; + this.j = j; + } + public boolean equals(Object o) { + return o instanceof Coord && ((Coord)o).i == i && ((Coord)o).j == j; + } + public int hashCode() { + int hashCode = 1; + hashCode = 31 * hashCode + i; + hashCode = 31 * hashCode + j; + return hashCode; + } +} + +//为了验证这个算法, 我们手动去求了 1 的所有坐标,并且调用上边的函数来验证我们的算法 +public void gameOfLife(int[][] board) { + Set live = new HashSet<>(); + int m = board.length; + int n = board[0].length; + for (int i = 0; i map = new HashMap<>(); - String[] array = str.split(" "); - if (pattern.length() != array.length) { - return false; - } - for (int i = 0; i < pattern.length(); i++) { - char key = pattern.charAt(i); - //当前 key 是否存在 - if (map.containsKey(key)) { - if (!map.get(key).equals(array[i])) { - return false; - } - } else { - map.put(key, array[i]); - } - } - return true; -} -``` - -但上边的代码还是有问题的,我们只是完成了 `pattern` 到 `str` 的映射,如果对于下边的例子是有问题的。 - -```java -pattern = "abba" -str = "dog dog dog dog" -``` - -最直接的方法,在添加新的 `key` 的时候判断一下相应的 `value` 是否已经用过了。 - -```java -public boolean wordPattern(String pattern, String str) { - HashMap map = new HashMap<>(); - String[] array = str.split(" "); - if(pattern.length() != array.length){ - return false; - } - for(int i = 0; i < pattern.length();i++){ - char key = pattern.charAt(i); - if(map.containsKey(key)){ - if(!map.get(key).equals(array[i])){ - return false; - } - }else{ - //判断 value 中是否存在 - if(map.containsValue(array[i])){ - return false; - } - map.put(key, array[i]); - } - } - return true; -} -``` - -虽然可以 AC 了,但还有一个问题,`containsValue` 的话,需要遍历一遍 `value` ,会导致时间复杂度增加。最直接的解决方法,我们可以把 `HashMap` 中的 `value` 存到 `HashSet` 中。 - -```java -public boolean wordPattern(String pattern, String str) { - HashMap map = new HashMap<>(); - HashSet set = new HashSet<>(); - String[] array = str.split(" "); - if (pattern.length() != array.length) { - return false; - } - for (int i = 0; i < pattern.length(); i++) { - char key = pattern.charAt(i); - if (map.containsKey(key)) { - if (!map.get(key).equals(array[i])) { - return false; - } - } else { - // 判断 value 中是否存在 - if (set.contains(array[i])) { - return false; - } - map.put(key, array[i]); - set.add(array[i]); - } - } - return true; -} -``` - -当然还有另外一种思路,我们只判断了 `pattern` 到 `str` 的映射,我们只需要再判断一次 `str` 到 `pattern` 的映射就可以了,这样就保证了一一对应。 - -两次判断映射的逻辑是一样的,所以我们可以抽象出一个函数,但由于 `pattern` 只能看成 `char` 数组,这样的话会使得两次的 `HashMap` 不一样,一次是 `HashMap` ,一次是 `HashMap`。所以我们提前将 `pattern` 转成 `String` 数组。 - -```java -public boolean wordPattern(String pattern, String str) { - String[] array1 = str.split(" "); - if (array1.length != pattern.length()) { - return false; - } - String[] array2 = pattern.split(""); - //两个方向的映射 - return wordPatternHelper(array1, array2) && wordPatternHelper(array2, array1); -} - -//array1 到 array2 的映射 -private boolean wordPatternHelper(String[] array1, String[] array2) { - HashMap map = new HashMap<>(); - for (int i = 0; i < array1.length; i++) { - String key = array1[i]; - if (map.containsKey(key)) { - if (!map.get(key).equals(array2[i])) { - return false; - } - } else { - map.put(key, array2[i]); - } - } - return true; -} -``` - -# 解法二 - - [205 题](https://leetcode.wang/leetcode-205-Isomorphic-Strings.html) 还介绍了另一种思路。 - -解法一中,我们通过对两个方向分别考虑来解决的。 - -这里的话,我们找一个第三方来解决。即,按照单词出现的顺序,把两个字符串都映射到另一个集合中。 - -第一次出现的单词(字母)映射到 `1` ,第二次出现的单词(字母)映射到 `2`... 依次类推,这样就会得到一个新的字符串了。两个字符串都通过这样方法去映射,然后判断新得到的字符串是否相等 。 - -举个现实生活中的例子,一个人说中文,一个人说法语,怎么判断他们说的是一个意思呢?把中文翻译成英语,把法语也翻译成英语,然后看最后的英语是否相同即可。举个例子。 - -```java -pattern = "abba", str = "dog cat cat dog" - -对于 abba -a -> 1 -b -> 2 -最终的得到的字符串就是 1221 - -对于 dog cat cat dog -dog -> 1 -cat -> 2 -最终的得到的字符串就是 1221 - -最终两个字符串都映射到了 1221, 所以 str 符合 pattern -``` - -代码的话,我们同样封装一个函数,返回映射后的结果。 - -```java -public boolean wordPattern(String pattern, String str) { - String[] array = str.split(" "); - if(array.length!=pattern.length()){ - return false; - } - //判断映射后的结果是否相等 - return wordPatternHelper(pattern.split("")).equals(wordPatternHelper(array)); -} - -private String wordPatternHelper(String[] array) { - HashMap map = new HashMap<>(); - int count = 1; - //保存映射后的结果 - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < array.length; i++) { - //是否已经映射过 - if (map.containsKey(array[i])) { - sb.append(map.get(array[i])); - } else { - sb.append(count); - map.put(array[i], count); - count++; - } - } - return sb.toString(); -} -``` - -为了方便,我们也可以将当前单词(字母)直接映射为当前单词(字母)的下标,省去 `count` 变量。 - -```java -public boolean wordPattern(String pattern, String str) { - String[] array = str.split(" "); - if(array.length!=pattern.length()){ - return false; - } - //判断映射后的结果是否相等 - return wordPatternHelper(pattern.split("")).equals(wordPatternHelper(array)); -} - -private String wordPatternHelper(String[] array) { - HashMap map = new HashMap<>(); - //保存映射后的结果 - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < array.length; i++) { - //是否已经映射过 - if (map.containsKey(array[i])) { - sb.append(map.get(array[i])); - } else { - sb.append(i); - map.put(array[i], i); - } - } - return sb.toString(); -} -``` - -上边我们是调用了两次函数,将字符串整体转换后来判断。我们其实可以一个单词(字母)一个单词(字母)的判断。第一次遇到的单词(字母)就给它一个 `value` ,也就是把下标给它。如果第二次遇到,就判断它们的 `value` 是否一致。 - -怎么判断是否是第一次遇到,我们可以通过判断 `key` 是否存在,但这样判断起来会比较麻烦。为了统一,我们可以给还不存在的 `key` 一个默认的 `value`,这样代码写起来比较统一。 - -```java -public boolean wordPattern(String pattern, String str) { - String[] array1 = str.split(" "); - if (array1.length != pattern.length()) { - return false; - } - char[] array2 = pattern.toCharArray(); - HashMap map1 = new HashMap<>(); - HashMap map2 = new HashMap<>(); - for (int i = 0; i < array1.length; i++) { - String c1 = array1[i]; - char c2 = array2[i]; - // 当前的映射值是否相同 - int a = map1.getOrDefault(c1, -1); - int b = map2.getOrDefault(c2, -1); - if (a != b) { - return false; - } else { - map1.put(c1, i); - map2.put(c2, i); - } - } - return true; -} -``` - -同样的思路,然后看一下 [StefanPochmann](https://leetcode.com/stefanpochmann) 大神的 [代码](https://leetcode.com/problems/word-pattern/discuss/73402/8-lines-simple-Java)。 - -```java -public boolean wordPattern(String pattern, String str) { - String[] words = str.split(" "); - if (words.length != pattern.length()) - return false; - Map index = new HashMap(); - for (Integer i = 0; i < words.length; ++i) - if (index.put(pattern.charAt(i), i) != index.put(words[i], i)) - return false; - return true; -} -``` - -他利用了 `put` 的返回值,如果 `put` 的 `key` 不存在,那么就插入成功,返回 `null`。 - -如果 `put` 的 `key` 已经存在了,返回 `key` 是之前对应的 `value`。 - -所以第一次遇到的单词(字母)两者都会返回 `null`,不会进入 `return false`。 - -第二次遇到的单词(字母)就判断之前插入的 `value` 是否相等。也有可能其中一个之前还没插入值,那就是 `null` ,另一个之前已经插入了,会得到一个 `value`,两者一定不相等,就会返回 `false`。 - -还有一点需要注意,`for` 循环中我们使用的是 `Integer i`,而不是 `int i`。是因为 `map` 中的 `value` 只能是 `Integer` 。 - -如果我们用 `int i` 的话,`java` 会自动装箱,转成 `Integer`。这样就会带来一个问题,`put` 返回的是一个 `Integer` ,判断对象相等的话,相当于判断的是引用的地址,那么即使 `Integer` 包含的值相等,那么它俩也不会相等。我们可以改成 `int i` 后试一试。 - -```java -public boolean wordPattern(String pattern, String str) { - String[] words = str.split(" "); - if (words.length != pattern.length()) - return false; - Map index = new HashMap(); - for (int i = 0; i < words.length; ++i) - if (index.put(pattern.charAt(i), i) != index.put(words[i], i)) - return false; - return true; -} -``` - -改成 `int i` 以后,就不能 `AC` 了。但你会发现当 `pattern` 的长度比较小时,代码是没有问题的,这又是为什么呢? - -是因为当数字在 `[-128,127]` 的范围内时,对于同一个值,`Integer` 对象是共享的,举个例子。 - -```java -Integer a = 66; -Integer b = 66; -System.out.println(a == b); // ? - -Integer c = 166; -Integer d = 166; -System.out.println(c == d); // ? -``` - -大家觉得上边会返回什么? - -是的,是 `true` 和 `false`。当不在 `[-128,127]` 的范围内时,即使 `Integer` 包含的值相等,但由于是对象之间比较,依旧会返回 `false`。 - -回到之前的问题,如果你非常想用 `int` ,比较两个值的时候,你可以拆箱去比较。但返回的有可能是 `null`,所以需要多加几个判断条件。 - -```java -public boolean wordPattern(String pattern, String str) { - String[] words = str.split(" "); - if (words.length != pattern.length()) - return false; - Map index = new HashMap(); - for (int i = 0; i < words.length; ++i) { - Object a = index.put(pattern.charAt(i), i); - Object b = index.put(words[i], i); - if (a == null && b == null) - continue; - if (a == null || b == null) - return false; - if ((int) a != (int) b) { - return false; - } - } - return true; -} -``` - -也可以通过 `Object.equals` 来判断两个 `Integer` 是否相等。 - -```java -public boolean wordPattern(String pattern, String str) { - String[] words = str.split(" "); - if (words.length != pattern.length()) - return false; - Map index = new HashMap(); - for (int i=0; i map = new HashMap<>(); + String[] array = str.split(" "); + if (pattern.length() != array.length) { + return false; + } + for (int i = 0; i < pattern.length(); i++) { + char key = pattern.charAt(i); + //当前 key 是否存在 + if (map.containsKey(key)) { + if (!map.get(key).equals(array[i])) { + return false; + } + } else { + map.put(key, array[i]); + } + } + return true; +} +``` + +但上边的代码还是有问题的,我们只是完成了 `pattern` 到 `str` 的映射,如果对于下边的例子是有问题的。 + +```java +pattern = "abba" +str = "dog dog dog dog" +``` + +最直接的方法,在添加新的 `key` 的时候判断一下相应的 `value` 是否已经用过了。 + +```java +public boolean wordPattern(String pattern, String str) { + HashMap map = new HashMap<>(); + String[] array = str.split(" "); + if(pattern.length() != array.length){ + return false; + } + for(int i = 0; i < pattern.length();i++){ + char key = pattern.charAt(i); + if(map.containsKey(key)){ + if(!map.get(key).equals(array[i])){ + return false; + } + }else{ + //判断 value 中是否存在 + if(map.containsValue(array[i])){ + return false; + } + map.put(key, array[i]); + } + } + return true; +} +``` + +虽然可以 AC 了,但还有一个问题,`containsValue` 的话,需要遍历一遍 `value` ,会导致时间复杂度增加。最直接的解决方法,我们可以把 `HashMap` 中的 `value` 存到 `HashSet` 中。 + +```java +public boolean wordPattern(String pattern, String str) { + HashMap map = new HashMap<>(); + HashSet set = new HashSet<>(); + String[] array = str.split(" "); + if (pattern.length() != array.length) { + return false; + } + for (int i = 0; i < pattern.length(); i++) { + char key = pattern.charAt(i); + if (map.containsKey(key)) { + if (!map.get(key).equals(array[i])) { + return false; + } + } else { + // 判断 value 中是否存在 + if (set.contains(array[i])) { + return false; + } + map.put(key, array[i]); + set.add(array[i]); + } + } + return true; +} +``` + +当然还有另外一种思路,我们只判断了 `pattern` 到 `str` 的映射,我们只需要再判断一次 `str` 到 `pattern` 的映射就可以了,这样就保证了一一对应。 + +两次判断映射的逻辑是一样的,所以我们可以抽象出一个函数,但由于 `pattern` 只能看成 `char` 数组,这样的话会使得两次的 `HashMap` 不一样,一次是 `HashMap` ,一次是 `HashMap`。所以我们提前将 `pattern` 转成 `String` 数组。 + +```java +public boolean wordPattern(String pattern, String str) { + String[] array1 = str.split(" "); + if (array1.length != pattern.length()) { + return false; + } + String[] array2 = pattern.split(""); + //两个方向的映射 + return wordPatternHelper(array1, array2) && wordPatternHelper(array2, array1); +} + +//array1 到 array2 的映射 +private boolean wordPatternHelper(String[] array1, String[] array2) { + HashMap map = new HashMap<>(); + for (int i = 0; i < array1.length; i++) { + String key = array1[i]; + if (map.containsKey(key)) { + if (!map.get(key).equals(array2[i])) { + return false; + } + } else { + map.put(key, array2[i]); + } + } + return true; +} +``` + +# 解法二 + + [205 题](https://leetcode.wang/leetcode-205-Isomorphic-Strings.html) 还介绍了另一种思路。 + +解法一中,我们通过对两个方向分别考虑来解决的。 + +这里的话,我们找一个第三方来解决。即,按照单词出现的顺序,把两个字符串都映射到另一个集合中。 + +第一次出现的单词(字母)映射到 `1` ,第二次出现的单词(字母)映射到 `2`... 依次类推,这样就会得到一个新的字符串了。两个字符串都通过这样方法去映射,然后判断新得到的字符串是否相等 。 + +举个现实生活中的例子,一个人说中文,一个人说法语,怎么判断他们说的是一个意思呢?把中文翻译成英语,把法语也翻译成英语,然后看最后的英语是否相同即可。举个例子。 + +```java +pattern = "abba", str = "dog cat cat dog" + +对于 abba +a -> 1 +b -> 2 +最终的得到的字符串就是 1221 + +对于 dog cat cat dog +dog -> 1 +cat -> 2 +最终的得到的字符串就是 1221 + +最终两个字符串都映射到了 1221, 所以 str 符合 pattern +``` + +代码的话,我们同样封装一个函数,返回映射后的结果。 + +```java +public boolean wordPattern(String pattern, String str) { + String[] array = str.split(" "); + if(array.length!=pattern.length()){ + return false; + } + //判断映射后的结果是否相等 + return wordPatternHelper(pattern.split("")).equals(wordPatternHelper(array)); +} + +private String wordPatternHelper(String[] array) { + HashMap map = new HashMap<>(); + int count = 1; + //保存映射后的结果 + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < array.length; i++) { + //是否已经映射过 + if (map.containsKey(array[i])) { + sb.append(map.get(array[i])); + } else { + sb.append(count); + map.put(array[i], count); + count++; + } + } + return sb.toString(); +} +``` + +为了方便,我们也可以将当前单词(字母)直接映射为当前单词(字母)的下标,省去 `count` 变量。 + +```java +public boolean wordPattern(String pattern, String str) { + String[] array = str.split(" "); + if(array.length!=pattern.length()){ + return false; + } + //判断映射后的结果是否相等 + return wordPatternHelper(pattern.split("")).equals(wordPatternHelper(array)); +} + +private String wordPatternHelper(String[] array) { + HashMap map = new HashMap<>(); + //保存映射后的结果 + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < array.length; i++) { + //是否已经映射过 + if (map.containsKey(array[i])) { + sb.append(map.get(array[i])); + } else { + sb.append(i); + map.put(array[i], i); + } + } + return sb.toString(); +} +``` + +上边我们是调用了两次函数,将字符串整体转换后来判断。我们其实可以一个单词(字母)一个单词(字母)的判断。第一次遇到的单词(字母)就给它一个 `value` ,也就是把下标给它。如果第二次遇到,就判断它们的 `value` 是否一致。 + +怎么判断是否是第一次遇到,我们可以通过判断 `key` 是否存在,但这样判断起来会比较麻烦。为了统一,我们可以给还不存在的 `key` 一个默认的 `value`,这样代码写起来比较统一。 + +```java +public boolean wordPattern(String pattern, String str) { + String[] array1 = str.split(" "); + if (array1.length != pattern.length()) { + return false; + } + char[] array2 = pattern.toCharArray(); + HashMap map1 = new HashMap<>(); + HashMap map2 = new HashMap<>(); + for (int i = 0; i < array1.length; i++) { + String c1 = array1[i]; + char c2 = array2[i]; + // 当前的映射值是否相同 + int a = map1.getOrDefault(c1, -1); + int b = map2.getOrDefault(c2, -1); + if (a != b) { + return false; + } else { + map1.put(c1, i); + map2.put(c2, i); + } + } + return true; +} +``` + +同样的思路,然后看一下 [StefanPochmann](https://leetcode.com/stefanpochmann) 大神的 [代码](https://leetcode.com/problems/word-pattern/discuss/73402/8-lines-simple-Java)。 + +```java +public boolean wordPattern(String pattern, String str) { + String[] words = str.split(" "); + if (words.length != pattern.length()) + return false; + Map index = new HashMap(); + for (Integer i = 0; i < words.length; ++i) + if (index.put(pattern.charAt(i), i) != index.put(words[i], i)) + return false; + return true; +} +``` + +他利用了 `put` 的返回值,如果 `put` 的 `key` 不存在,那么就插入成功,返回 `null`。 + +如果 `put` 的 `key` 已经存在了,返回 `key` 是之前对应的 `value`。 + +所以第一次遇到的单词(字母)两者都会返回 `null`,不会进入 `return false`。 + +第二次遇到的单词(字母)就判断之前插入的 `value` 是否相等。也有可能其中一个之前还没插入值,那就是 `null` ,另一个之前已经插入了,会得到一个 `value`,两者一定不相等,就会返回 `false`。 + +还有一点需要注意,`for` 循环中我们使用的是 `Integer i`,而不是 `int i`。是因为 `map` 中的 `value` 只能是 `Integer` 。 + +如果我们用 `int i` 的话,`java` 会自动装箱,转成 `Integer`。这样就会带来一个问题,`put` 返回的是一个 `Integer` ,判断对象相等的话,相当于判断的是引用的地址,那么即使 `Integer` 包含的值相等,那么它俩也不会相等。我们可以改成 `int i` 后试一试。 + +```java +public boolean wordPattern(String pattern, String str) { + String[] words = str.split(" "); + if (words.length != pattern.length()) + return false; + Map index = new HashMap(); + for (int i = 0; i < words.length; ++i) + if (index.put(pattern.charAt(i), i) != index.put(words[i], i)) + return false; + return true; +} +``` + +改成 `int i` 以后,就不能 `AC` 了。但你会发现当 `pattern` 的长度比较小时,代码是没有问题的,这又是为什么呢? + +是因为当数字在 `[-128,127]` 的范围内时,对于同一个值,`Integer` 对象是共享的,举个例子。 + +```java +Integer a = 66; +Integer b = 66; +System.out.println(a == b); // ? + +Integer c = 166; +Integer d = 166; +System.out.println(c == d); // ? +``` + +大家觉得上边会返回什么? + +是的,是 `true` 和 `false`。当不在 `[-128,127]` 的范围内时,即使 `Integer` 包含的值相等,但由于是对象之间比较,依旧会返回 `false`。 + +回到之前的问题,如果你非常想用 `int` ,比较两个值的时候,你可以拆箱去比较。但返回的有可能是 `null`,所以需要多加几个判断条件。 + +```java +public boolean wordPattern(String pattern, String str) { + String[] words = str.split(" "); + if (words.length != pattern.length()) + return false; + Map index = new HashMap(); + for (int i = 0; i < words.length; ++i) { + Object a = index.put(pattern.charAt(i), i); + Object b = index.put(words[i], i); + if (a == null && b == null) + continue; + if (a == null || b == null) + return false; + if ((int) a != (int) b) { + return false; + } + } + return true; +} +``` + +也可以通过 `Object.equals` 来判断两个 `Integer` 是否相等。 + +```java +public boolean wordPattern(String pattern, String str) { + String[] words = str.split(" "); + if (words.length != pattern.length()) + return false; + Map index = new HashMap(); + for (int i=0; i map = new HashMap<>(); -public boolean canWinNim(int n) { - if (map.containsKey(n)) { - return map.get(n); - } - if (n <= 0) { - return false; - } - if (n < 4) { - return true; - } - for (int i = 1; i <= 3; i++) { - if (canWinNim(n - i - 1) && canWinNim(n - i - 2) && canWinNim(n - i - 3)) { - map.put(n, true); - return true; - } - } - map.put(n, false); - return false; -} -``` - -然后竟然遇到了 `Runtime Error`。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/292_2.jpg) - -虽然这个问题不常见,但依旧是问题不大,无非就是因为递归需要压栈,然后压的太多了,造成了栈溢出。那我们用动态规划呀,初始条件和状态转移方程都是现成的,和递归简直一模一样,不信你看下边的代码。 - -```java -public boolean canWinNim(int n) { - if(n == 0){ - return false; - } - if(n < 4){ - return true; - } - boolean[] dp = new boolean[n + 1]; - dp[0] = false; - dp[1] = true; - dp[2] = true; - dp[3] = true; - //从下往上走 - for (int num = 4; num <= n; num++) { - for (int i = 1; i <= 3; i++) { - if (dp[num - i - 1] && dp[num - i - 2] && dp[num - i - 3]) { - dp[num] = true; - break; - } - } - } - return dp[n]; -} -``` - -上边值得注意的地方是,我们给 `dp[num]` 只赋过 `true`。因为 `dp` 数组的默认值是 `false` ,所以如果它是 `false` 就不用管了。 - -但是竟然还是报错了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/292_3.jpg) - -超出内存限制???我觉得它在耍我,但事实确实如此。因为当 `n` 太大的时候,`dp` 一次性不能申请这么大的空间。 - -但是不要慌张,如果前边的题中动态规划做的很多的话,应该还记得动态规划的最后一步,空间复杂度的优化。 - -因为我们注意到,当求 `dp[n]` 的时候,我们最远也就用到 `dp[n - 6]`,换言之,我们只需要 `dp[n - 6]`, `dp[n - 5]`, `dp[n - 4]`, `dp[n - 3]`, `dp[n - 2]`, `dp[n - 1]`这 `6` 个数,再往前我们就不需要了。 - -所以我们并不需要大小为 `n` 的数组,大小为 `6` 的数组就足够了。此时数组的下标就是 `0` 到 `5` ,所以给数组更新的时候,我们只需要对 `6` 取余即可。 - -```java -public boolean canWinNim(int n) { - if (n == 0) { - return false; - } - if (n < 4) { - return true; - } - boolean[] dp = new boolean[6]; - dp[0] = false; - dp[1] = true; - dp[2] = true; - dp[3] = true; - for (int num = 4; num <= n; num++) { - int i = 1; - for (; i <= 3; i++) { - if (dp[(num - i - 1) % 6] && dp[(num - i - 2) % 6] && dp[(num - i - 3) % 6]){ - dp[num % 6] = true; - break; - } - } - if(i == 4){ - dp[num % 6] = false; - } - } - return dp[n % 6]; -} -``` - -有一点需要注意,之前提到「因为 `dp` 数组的默认值是 `false` ,所以如果它是 `false` 就不用管了」。但这里因为数组在循环使用,所以如果内层的 `for` 循环尝试了所有情况都不行的话,我们要将当前值置为 `false` ,因为它之前可能是 `true`。 - -当我准备收工的时候。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/292_4.jpg) - -又看到了这个熟悉的错误,此时我想到了一首诗。 - -假如生活欺骗了你, - -不要悲伤,不要心急! - -忧郁的日子里须要镇静: - -相信吧,快乐的日子将会来临! - -心儿永远向往着未来; - -现在却常是忧郁。 - -一切都是瞬息,一切都将会过去; - -而那过去了的,就会成为亲切的怀恋。 - -# 解法二 - -上边优化的已经到头了,但我们不能放弃。经过前边题的锤炼,直觉告诉我,最后的答案一定是有规律的,先输它 `100` 个试试。 - -```java -for (int i = 1; i <= 100; i++) { - System.out.print(canWinNim(i) + " "); -} -``` - -看一下结果。 - -```java -true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false -``` - -惊不惊喜,意不意外。果然是周期性的,`3` 个 `true` ,`1` 个 `false` ,由此往复。 - -代码就很好写了,如果是 `4` 的倍数就是 `false` ,否则就是 `true` 。 - -```java -public boolean canWinNim(int n) { - if (n % 4 == 0) { - return false; - } else { - return true; - } -} -``` - -或者更简洁些。 - -```java -public boolean canWinNim(int n) { - return n % 4 != 0; -} -``` - -或者利用二进制判断是不是 `4` 的倍数,只需要通过和 `3` (二进制 `11`)进行相与,如果是 `4` 的倍数,那么结果一定是 `0`。 - -为什么呢?[这里](https://www.cnblogs.com/superbi/archive/2013/02/28/2936334.html) 有个解释。 - -```java -算法如下: -  x&3==0,则是4的倍数。 -原理: -先来看一组数字的二进制表示 -  4    0100 -  8    1000 - 12 1100 -  16 10000 -  20 10100 -``` - - -由此可见 `4` 的倍数的二进制表示的后 `2` 为一定为 `0`。 - -从另外一个角度来看,`4` 的二进制表示是 `0100`,任何 `4` 的倍数一定是在此基础上增加 `n` 个 `0100` -由此也可得 `4` 的倍数的二进制表示的后 `2` 为一定为 `0`。 - -所以代码也可以这样写。 - -```java -public boolean canWinNim(int n) { - return (n & 3) != 0; -} -``` - -上边有很多写法,但我看到下边的输出时,第一反应并不是判断 `4` 的倍数。 - -```java -true true true false true true true false true true true false -``` - -当时我的第一反应是,肯定需要把 `n`对 `4` 求余。结果的话对应如下 - -```java -n 1 2 3 4 5 6 7 8 ... - true true true false true true true false ... -n%4 1 2 3 0 1 2 3 0 ... -``` - -此时余数如果是 `1` 到 `3` 那么结果就是 `true` 。为了方便,我先把 `n` 减 `1`,然后才求余。这样的话只要余数小于 `3` ,那么就是 `true`。 - -```java -n 1 2 3 4 5 6 7 8 ... -n-1 0 1 2 3 4 5 6 7 ... - true true true false true true true false ... -(n-1)%4 0 1 2 3 0 1 2 3 ... -``` - -代码的话就可以写成下边的样子。 - -```java -public boolean canWinNim(int n) { - if ((n - 1) % 4 < 3) { - return true; - } else { - return false; - } -} -``` - -看起来有点多余,我也不知道为什么第一反应是这个,可能受前边题的影响,对这个减 `1` 再求余的技巧记忆太深刻了吧,哈哈。 - -题目 `AC` 了,但是是为什么呢?为什么会有这个规律。 - -其实也不难理解,因为如果是 `4` 个石子,谁先手就谁输。因为你一次性最多拿 `3` 个,最后一个石子一定被对方拿走。 - -然后我们可以把石子,`4` 个,`4` 个分成一个个小堆。然后有 `4` 种情况。 - -全是 `4` 个一小堆。 - -```java -X X X X X X X X -X X X X X X X X -``` - -余下 `1` 个。 - -```java -X X X X X X X X X -X X X X X X X X -``` - -余下 `2` 个。 - -```java -X X X X X X X X X X -X X X X X X X X -``` - -余下 `3` 个。 - -```java -X X X X X X X X X X -X X X X X X X X X -``` - -只要有余下的,因为是你先手,你只需要把余下的全拿走。然后对方从每个小堆里拿石子,你只需要把每个小堆里剩下的拿走即可。最后一定是你拿走最后一个石子。 - -如果非要说,如果对方从多个小堆里拿石子呢?他拿完以后我们就把每个小堆再还原成 `4` 个,`4` 个的,然后把不是 `4` 个的那堆拿走。 - -其实上边只是一个抽象出的模型,实际上,当第一步我们把余下的拿走以后。之后如果对方拿 `x` 个,我们只需要拿 `4 - x` 个即可。 - -而如果没有余下的,那如果对方知道这个技巧的话,一定是对方赢了。 - - - -# 总 - -这个题,emm,有点意思。 - -解法一真的是把我的毕生所学都用上了,竟然没有 `AC`。 - -解法二的话,如果不把结果都输出然后找规律,其实也可能想到。关键点就是分堆,想到这个点,很快就能找到答案。为什么这么说呢? - -因为解法一尝试完所有可能后,备受打击,我就去睡觉了,醒来的时候又想了想,突然就想到了分堆。开始想了每堆分 `5` 个,发现不行,`3` 个呢?为什么不是 `2` 个呢,最后也大致推出了是 `4`个,然后起来就把结果输出来,验证了一下自己的想法。 - -总之,当正常的编程思路解不了的问题的时候,找找规律也算是一条路,哈哈。 - - - +# 题目描述(简单难度) + +292、Nim Game + +You are playing the following Nim Game with your friend: There is a heap of stones on the table, each time one of you take turns to remove 1 to 3 stones. The one who removes the last stone will be the winner. You will take the first turn to remove the stones. + +Both of you are very clever and have optimal strategies for the game. Write a function to determine whether you can win the game given the number of stones in the heap. + +**Example:** + +``` +Input: 4 +Output: false +Explanation: If there are 4 stones in the heap, then you will never win the game; + No matter 1, 2, or 3 stones you remove, the last stone will always be + removed by your friend. +``` + +有一堆石子,你和另一个人在玩游戏,每人轮流拿走 `1, 2` 或者 `3` 个石子,最后的石子被谁拿走,谁就是赢家。从你开始拿石子。 + +# 解法一 + +遇到这种题,想想就很复杂,直接递归来解。 + +只需要模拟拿石子过程,首先定义一些初始情况。 + +当没有石子的话,也就意味着最后的石子被对方拿走了,也就是你输了。 + +当石子数剩下 `1, 2, 3` 个的时候,你可以一次性都拿走,也就是你赢了。 + +```java +if (n == 0) { + return false; +} +if (n < 4) { + return true; +} +``` + +然后我们考虑所有的情况。 + +你拿走 `1` 个石子,然后不论对方从剩下的石子中拿走 `1` 个,`2` 个,还是 `3` 个,判断一下剩下的石子你是不是有稳赢的策略。 + +如果上边不行的话,你就拿走 `2` 个石子,然后再判断不论对方从剩下的石子拿走 `1` 个,`2` 个,还是`3` 个,剩下的石子你是不是都有稳赢的策略。 + +如果上边还不行的话,你就拿走 `3` 个石子,然后再判断不论对方从剩下的石子拿走 `1` 个,`2` 个,还是`3` 个,剩下的石子你是不是都有稳赢的策略。 + +如果上边通通不行,那就是你输了。 + +```java +public boolean canWinNim1(int n) { + if (n == 0) { + return false; + } + if (n < 4) { + return true; + } + //依次尝试拿走 1,2,3 个 + for (int i = 1; i <= 3; i++) { + //对方拿走 1 个,2 个,3 个, 你都有稳赢的策略 + if (canWinNim(n - i - 1) && canWinNim(n - i - 2) && canWinNim(n - i - 3)) { + return true; + } + } + //否则的话就是你输了 + return false; +} +``` + +但会发现超时了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/292.jpg) + +但是问题不大,之前的题已经遇到过无数次这种情况了,只需要通过 `memoization` 技术,利用 `map` 把过程中的解保存起来就可以了。 + +```java +HashMap map = new HashMap<>(); +public boolean canWinNim(int n) { + if (map.containsKey(n)) { + return map.get(n); + } + if (n <= 0) { + return false; + } + if (n < 4) { + return true; + } + for (int i = 1; i <= 3; i++) { + if (canWinNim(n - i - 1) && canWinNim(n - i - 2) && canWinNim(n - i - 3)) { + map.put(n, true); + return true; + } + } + map.put(n, false); + return false; +} +``` + +然后竟然遇到了 `Runtime Error`。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/292_2.jpg) + +虽然这个问题不常见,但依旧是问题不大,无非就是因为递归需要压栈,然后压的太多了,造成了栈溢出。那我们用动态规划呀,初始条件和状态转移方程都是现成的,和递归简直一模一样,不信你看下边的代码。 + +```java +public boolean canWinNim(int n) { + if(n == 0){ + return false; + } + if(n < 4){ + return true; + } + boolean[] dp = new boolean[n + 1]; + dp[0] = false; + dp[1] = true; + dp[2] = true; + dp[3] = true; + //从下往上走 + for (int num = 4; num <= n; num++) { + for (int i = 1; i <= 3; i++) { + if (dp[num - i - 1] && dp[num - i - 2] && dp[num - i - 3]) { + dp[num] = true; + break; + } + } + } + return dp[n]; +} +``` + +上边值得注意的地方是,我们给 `dp[num]` 只赋过 `true`。因为 `dp` 数组的默认值是 `false` ,所以如果它是 `false` 就不用管了。 + +但是竟然还是报错了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/292_3.jpg) + +超出内存限制???我觉得它在耍我,但事实确实如此。因为当 `n` 太大的时候,`dp` 一次性不能申请这么大的空间。 + +但是不要慌张,如果前边的题中动态规划做的很多的话,应该还记得动态规划的最后一步,空间复杂度的优化。 + +因为我们注意到,当求 `dp[n]` 的时候,我们最远也就用到 `dp[n - 6]`,换言之,我们只需要 `dp[n - 6]`, `dp[n - 5]`, `dp[n - 4]`, `dp[n - 3]`, `dp[n - 2]`, `dp[n - 1]`这 `6` 个数,再往前我们就不需要了。 + +所以我们并不需要大小为 `n` 的数组,大小为 `6` 的数组就足够了。此时数组的下标就是 `0` 到 `5` ,所以给数组更新的时候,我们只需要对 `6` 取余即可。 + +```java +public boolean canWinNim(int n) { + if (n == 0) { + return false; + } + if (n < 4) { + return true; + } + boolean[] dp = new boolean[6]; + dp[0] = false; + dp[1] = true; + dp[2] = true; + dp[3] = true; + for (int num = 4; num <= n; num++) { + int i = 1; + for (; i <= 3; i++) { + if (dp[(num - i - 1) % 6] && dp[(num - i - 2) % 6] && dp[(num - i - 3) % 6]){ + dp[num % 6] = true; + break; + } + } + if(i == 4){ + dp[num % 6] = false; + } + } + return dp[n % 6]; +} +``` + +有一点需要注意,之前提到「因为 `dp` 数组的默认值是 `false` ,所以如果它是 `false` 就不用管了」。但这里因为数组在循环使用,所以如果内层的 `for` 循环尝试了所有情况都不行的话,我们要将当前值置为 `false` ,因为它之前可能是 `true`。 + +当我准备收工的时候。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/292_4.jpg) + +又看到了这个熟悉的错误,此时我想到了一首诗。 + +假如生活欺骗了你, + +不要悲伤,不要心急! + +忧郁的日子里须要镇静: + +相信吧,快乐的日子将会来临! + +心儿永远向往着未来; + +现在却常是忧郁。 + +一切都是瞬息,一切都将会过去; + +而那过去了的,就会成为亲切的怀恋。 + +# 解法二 + +上边优化的已经到头了,但我们不能放弃。经过前边题的锤炼,直觉告诉我,最后的答案一定是有规律的,先输它 `100` 个试试。 + +```java +for (int i = 1; i <= 100; i++) { + System.out.print(canWinNim(i) + " "); +} +``` + +看一下结果。 + +```java +true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false true true true false +``` + +惊不惊喜,意不意外。果然是周期性的,`3` 个 `true` ,`1` 个 `false` ,由此往复。 + +代码就很好写了,如果是 `4` 的倍数就是 `false` ,否则就是 `true` 。 + +```java +public boolean canWinNim(int n) { + if (n % 4 == 0) { + return false; + } else { + return true; + } +} +``` + +或者更简洁些。 + +```java +public boolean canWinNim(int n) { + return n % 4 != 0; +} +``` + +或者利用二进制判断是不是 `4` 的倍数,只需要通过和 `3` (二进制 `11`)进行相与,如果是 `4` 的倍数,那么结果一定是 `0`。 + +为什么呢?[这里](https://www.cnblogs.com/superbi/archive/2013/02/28/2936334.html) 有个解释。 + +```java +算法如下: +  x&3==0,则是4的倍数。 +原理: +先来看一组数字的二进制表示 +  4    0100 +  8    1000 + 12 1100 +  16 10000 +  20 10100 +``` + + +由此可见 `4` 的倍数的二进制表示的后 `2` 为一定为 `0`。 + +从另外一个角度来看,`4` 的二进制表示是 `0100`,任何 `4` 的倍数一定是在此基础上增加 `n` 个 `0100` +由此也可得 `4` 的倍数的二进制表示的后 `2` 为一定为 `0`。 + +所以代码也可以这样写。 + +```java +public boolean canWinNim(int n) { + return (n & 3) != 0; +} +``` + +上边有很多写法,但我看到下边的输出时,第一反应并不是判断 `4` 的倍数。 + +```java +true true true false true true true false true true true false +``` + +当时我的第一反应是,肯定需要把 `n`对 `4` 求余。结果的话对应如下 + +```java +n 1 2 3 4 5 6 7 8 ... + true true true false true true true false ... +n%4 1 2 3 0 1 2 3 0 ... +``` + +此时余数如果是 `1` 到 `3` 那么结果就是 `true` 。为了方便,我先把 `n` 减 `1`,然后才求余。这样的话只要余数小于 `3` ,那么就是 `true`。 + +```java +n 1 2 3 4 5 6 7 8 ... +n-1 0 1 2 3 4 5 6 7 ... + true true true false true true true false ... +(n-1)%4 0 1 2 3 0 1 2 3 ... +``` + +代码的话就可以写成下边的样子。 + +```java +public boolean canWinNim(int n) { + if ((n - 1) % 4 < 3) { + return true; + } else { + return false; + } +} +``` + +看起来有点多余,我也不知道为什么第一反应是这个,可能受前边题的影响,对这个减 `1` 再求余的技巧记忆太深刻了吧,哈哈。 + +题目 `AC` 了,但是是为什么呢?为什么会有这个规律。 + +其实也不难理解,因为如果是 `4` 个石子,谁先手就谁输。因为你一次性最多拿 `3` 个,最后一个石子一定被对方拿走。 + +然后我们可以把石子,`4` 个,`4` 个分成一个个小堆。然后有 `4` 种情况。 + +全是 `4` 个一小堆。 + +```java +X X X X X X X X +X X X X X X X X +``` + +余下 `1` 个。 + +```java +X X X X X X X X X +X X X X X X X X +``` + +余下 `2` 个。 + +```java +X X X X X X X X X X +X X X X X X X X +``` + +余下 `3` 个。 + +```java +X X X X X X X X X X +X X X X X X X X X +``` + +只要有余下的,因为是你先手,你只需要把余下的全拿走。然后对方从每个小堆里拿石子,你只需要把每个小堆里剩下的拿走即可。最后一定是你拿走最后一个石子。 + +如果非要说,如果对方从多个小堆里拿石子呢?他拿完以后我们就把每个小堆再还原成 `4` 个,`4` 个的,然后把不是 `4` 个的那堆拿走。 + +其实上边只是一个抽象出的模型,实际上,当第一步我们把余下的拿走以后。之后如果对方拿 `x` 个,我们只需要拿 `4 - x` 个即可。 + +而如果没有余下的,那如果对方知道这个技巧的话,一定是对方赢了。 + + + +# 总 + +这个题,emm,有点意思。 + +解法一真的是把我的毕生所学都用上了,竟然没有 `AC`。 + +解法二的话,如果不把结果都输出然后找规律,其实也可能想到。关键点就是分堆,想到这个点,很快就能找到答案。为什么这么说呢? + +因为解法一尝试完所有可能后,备受打击,我就去睡觉了,醒来的时候又想了想,突然就想到了分堆。开始想了每堆分 `5` 个,发现不行,`3` 个呢?为什么不是 `2` 个呢,最后也大致推出了是 `4`个,然后起来就把结果输出来,验证了一下自己的想法。 + +总之,当正常的编程思路解不了的问题的时候,找找规律也算是一条路,哈哈。 + + + diff --git a/leetcode-295-Find-Median-from-Data-Stream.md b/leetcode-295-Find-Median-from-Data-Stream.md index 72ee104df..967fb554c 100644 --- a/leetcode-295-Find-Median-from-Data-Stream.md +++ b/leetcode-295-Find-Median-from-Data-Stream.md @@ -1,583 +1,583 @@ -# 题目描述(困难难度) - -295、Find Median from Data Stream - -Median is the middle value in an ordered integer list. If the size of the list is even, there is no middle value. So the median is the mean of the two middle value. - -For example, - -``` -[2,3,4]`, the median is `3 -[2,3]`, the median is `(2 + 3) / 2 = 2.5 -``` - -Design a data structure that supports the following two operations: - -- void addNum(int num) - Add a integer number from the data stream to the data structure. -- double findMedian() - Return the median of all elements so far. - - - -**Example:** - -``` -addNum(1) -addNum(2) -findMedian() -> 1.5 -addNum(3) -findMedian() -> 2 -``` - - - -**Follow up:** - -1. If all integer numbers from the stream are between 0 and 100, how would you optimize it? -2. If 99% of all integer numbers from the stream are between 0 and 100, how would you optimize it? - -设计一个数据结构,提过两个接口,添加数字和返回当前集合内的中位数。 - -# 解法一 - -先分享 [官方](https://leetcode.com/problems/find-median-from-data-stream/solution/) 给我们提供的两个最容易的解法。 - -把添加的数字放到 `list` 中,如果需要返回中位数,把 `list` 排序即可。 - -```java -class MedianFinder { - List list = new ArrayList<>(); - - /** initialize your data structure here. */ - public MedianFinder() { - - } - - public void addNum(int num) { - list.add(num); - } - - public double findMedian() { - Collections.sort(list); - int n = list.size(); - if ((n & 1) == 1) { - return list.get(n / 2); - } else { - return ((double) list.get(n / 2) + list.get(n / 2 - 1)) / 2; - } - } -} -``` - -简单明了。但是时间复杂度有点儿高,对于 `findMedian` 函数,因为每次都需要排序。如果是快速排序,那时间复杂度也是 `O(nlog(n))`。 - -这里可以做一个简单的优化。我们不需要每次返回中位数都去排序。我们可以将排序融入到 `addNum` 中,假设之前已经有序了,然后将添加的数字插入到相应的位置即可。也就是插入排序的思想。 - -```java -class MedianFinder { - List list = new ArrayList<>(); - - /** initialize your data structure here. */ - public MedianFinder() { - - } - - public void addNum(int num) { - int i = 0; - // 寻找第一个大于 num 的数的下标 - for (; i < list.size(); i++) { - if (num < list.get(i)) { - break; - } - } - // 将当前数插入 - list.add(i, num); - } - - public double findMedian() { - int n = list.size(); - if ((n & 1) == 1) { - return list.get(n / 2); - } else { - return ((double) list.get(n / 2) + list.get(n / 2 - 1)) / 2; - } - } -} -``` - -上边的话 `findMedian()` 就不需要排序了,时间复杂度就是 `O(1)`了。对于 `addNum()` 函数的话时间复杂度就是 `O(n)` 了。 - -`addNum()` 还可以做一点优化。因为我们要在有序数组中寻找第一个大于 `num` 的下标,提到有序数组找某个值,可以想到二分的方法。 - -```java -public void addNum(int num) { - int insert = -1; - int low = 0; - int high = list.size() - 1; - while (low <= high) { - int mid = (low + high) >>> 1; - if (num <= list.get(mid)) { - //判断 num 是否大于等于前边的数 - int pre = mid > 0 ? list.get(mid - 1) : Integer.MIN_VALUE; - if (num >= pre) { - insert = mid; - break; - } else { - high = mid - 1; - } - } else { - low = mid + 1; - } - } - if (insert == -1) { - insert = list.size(); - } - // 将当前数插入 - list.add(insert, num); -} -``` - -虽然我们使用了二分去查找要插入的位置,对应的时间复杂度是 `O(log(n))`,但是 `list.add(insert, num)` 的时间复杂度是 `O(n)`。所以整体上依旧是 `O(n)`。 - -参考 [这里](https://leetcode.com/problems/find-median-from-data-stream/discuss/74057/Tired-of-TWO-HEAPSET-solutions-See-this-segment-dividing-solution-(c%2B%2B)),还能继续优化,这个思想也比较常见,分享一下。 - -我们每次添加数字的时候,都需要从所有数字中寻找要插入的位置,如果数字太多的话,速度会很慢。 - -我们可以将数字分成若干个子序列,类似于下图。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/295_2.jpg) - -上边每一个长方形内数字都是有序的。添加数字的时候,分成两步。先找到数字应该加入的长方形,然后将数字添加到该长方形内。这样做的好处很明显,我们只需要将数字加入长方形内的有序数列中,长方形内的数字个数相对于整个序列会小很多。 - -我们可以设置每个长方形内最多存多少数字,如果超过了限制,就将长方形平均分成两个。 - -举个简单的例子,接着上图,假设每个长方形内最多存 `3` 个数字,现在添加数字 `9`。 - -我们首先找到 `9` 应该属于第 `2` 个长方形,然后将 `9` 插入。然后发现此时的数量超过了 `3` 个,此时我们就把该长方形平均分成两个,如下图。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/295_3.jpg) - -至于最多存多少,我们可以根据总共有多少个数字来自己设定。但不是很好控制,太小了的话,寻找长方形的时候比较耗时间,太大的话,加入到长方形里的时候比较耗时间。事先不知道数字有多少的话,就更麻烦了。 - -[StefanPochmann](https://leetcode.com/stefanpochmann) 大神提出了一个建议。我们可以将大小设置成一个动态的,每个长方形最多存多少根据当前数字的总个数实时改变。假设当前数字总量是 `n`。长方形里数字个数是 `len` ,如果 `len * len > n` ,那么当前长方形就分割为两个。也就是每个长方形最多存 `sqrt(n)` 个数字。 - -这里我就偷个懒了,直接分享下 [@mission4success](https://leetcode.com/mission4success) 的代码。主要就是找长方形以及找中位数那里判断的情况会多一些。 - -```java -public class MedianFinder { - private LinkedList> buckets; // store all ranges - private int total_size; - - MedianFinder() { - total_size = 0; - buckets = new LinkedList<>(); - buckets.add(new LinkedList<>()); - } - - void addNum(int num) { - List correctRange = new LinkedList<>(); - int targetIndex = 0; - - // find the correct range to insert given num - for (int i = 0; i < buckets.size(); i++) { - if (buckets.size() == 1 || - (i == 0 && num <= buckets.get(i).getLast()) || - (i == buckets.size() - 1 && num >= buckets.get(i).getFirst()) || - (buckets.get(i).getFirst() <= num && num <= buckets.get(i).getLast()) || - (num > buckets.get(i).getLast() && num < buckets.get(i+1).getFirst())) { - correctRange = buckets.get(i); - targetIndex = i; - break; - } - } - - // put num at back of correct range, and sort it to keep increasing sequence - total_size++; - correctRange.add(num); - Collections.sort(correctRange); - - // if currentRange's size > threshold, split it into two halves and add them back to buckets - int len = correctRange.size(); - //if (len > 10) { - if (len * len > total_size) { - LinkedList half1 = new LinkedList<>(correctRange.subList(0, (len) / 2)); - LinkedList half2 = new LinkedList<>(correctRange.subList((len) / 2, len)); - - buckets.set(targetIndex, half1); //replaces - buckets.add(targetIndex + 1, half2); //inserts - } - - } - - // iterate thru all ranges in buckets to find median value - double findMedian() { - if (total_size==0) - return 0; - - int mid1 = total_size/2; - int mid2 = mid1 + 1; - - int leftCount=0; - double first = 0.0, second = 0.0; - for (List bucket : buckets) { - if (leftCount 1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值; -> 2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值; -> 3. 任意节点的左、右子树也分别为二叉查找树; -> 4. 没有键值相等的节点。 - -如果我们将数字存到二分查找树中,当找中位数的时候有一个明显的好处。如果我们知道了左子树的数量 `leftNum` ,找假设把数据排序后的第 `k` 个数,`k` 从 `0` 计数。 - -如果 `leftNum == k` ,那么根节点就是我们要找的。 - -如果 `leftNum > k`,我们只需要再从左子树中找第 `k` 个数。 - -如果 `leftNum < k`,我们只需要从右子树中找第 `k - leftNum - 1` 个数。 - -代码的话,我们首先定义一个二分查找树。和普通的二分查找树不同的地方在于,节点多了一个成员变量,记录以当前节点为根节点的二叉树的总节点数量。 - -此外实现了 `find` 函数,来返回有序情况下第 `k` 个节点的值。 - -```java -class BST { - class Node { - int val; - int size; - Node left, right; - - Node(int v) { - val = v; - size = 1; - }; - }; - - private Node root; - - BST() { - - } - - public void add(int val) { - // 新增节点 - Node newNode = new Node(val); - // 当前节点 - Node current = root; - // 上个节点 - Node parent = null; - // 如果根节点为空 - if (current == null) { - root = newNode; - return; - } - while (true) { - parent = current; - //向左子树添加节点 - if (val < current.val) { - current = current.left; - parent.size++; - if (current == null) { - parent.left = newNode; - return; - } - //向右子树添加节点 - } else { - current = current.right; - parent.size++; - if (current == null) { - parent.right = newNode; - return; - } - } - } - - } - - public int find(int k) { - Node t = root; - while (true) { - int leftSize = t.left != null ? t.left.size : 0; - if (leftSize == k) - return t.val; - if (leftSize > k) { - t = t.left; - } else { - k = k - leftSize - 1; - t = t.right; - } - } - } - - public int size() { - return root.size; - } -}; - -class MedianFinder { - BST bst; - - MedianFinder() { - bst = new BST(); - } - - // Adds a number into the data structure. - public void addNum(int num) { - bst.add(num); - } - - public double findMedian() { - int num = bst.size(); - if (num % 2 == 0) { - return ((double)bst.find(num / 2) + bst.find(num / 2 - 1)) / 2; - } else { - return bst.find(num / 2); - } - - } -}; -``` - -时间复杂度的话,最好的情况 `addNum` 和 `findMedian` 都是 `O(log(n))`。但如果二叉树分布不均,类似于下边这种,那么时间复杂度就都是 `O(n)` 了。 - -```java - 1 - \ - 2 - \ - 3 - \ - 4 -``` - -# 解法三 - -分享一下我最开始的想法。 - -什么是中位数?如果是偶数个数字,我们把它分成两个集合。左边的集合的所有数字小于右边集合的所有数字。中位数就是左边集合最大数和右边集合最小的数取一个平均数。 - -想到上边这个点,会发现我们只关心集合的最大数和最小数,立马就会想到优先队列。 - -添加数字的时候,我们把数字放到两个优先队列中。始终保证两个优先队列的大小相等。如果总数是奇数,我们就让左边集合多一个数。 - -有了上边的想法可以写代码了,代码大家写出来应该都不一样,分享下我的代码。 - -```java -class MedianFinder { - //左边的队列,每次取最大值 - Queue leftQueue = new PriorityQueue<>(new Comparator() { - @Override - public int compare(Integer i1, Integer i2) { - return i2 - i1; - } - }); - //右边的队列,每次取最小值 - Queue rightQueue = new PriorityQueue<>(); - - double median = 0; //保存当前的中位数 - - /** initialize your data structure here. */ - public MedianFinder() { - } - - public void addNum(int num) { - int leftSize = leftQueue.size(); - int rightSize = rightQueue.size(); - //如果当前数量相等 - if (leftSize == rightSize) { - //当前没有数字,将将数字加到左半部分 - if (leftSize == 0) { - leftQueue.add(num); - return; - } - //当前数字小于等于右半部分最小的数字, num 属于左边 - if (num <= rightQueue.peek()) { - leftQueue.add(num); - //当前数字大于右半部分最小的数字, num 应该属于右边 - } else { - //维持两边平衡. 将右边拿出一个放到左边 - leftQueue.add(rightQueue.poll()); - //将 num 放到右边 - rightQueue.add(num); - } - //如果当前数量不等 - } else { - //num 大于等于左边最大的数字, num 属于右边 - if (num >= leftQueue.peek()) { - rightQueue.add(num); - //num 小于左边最大的数字, num 应该属于左边 - } else { - //维持两边平衡, 左边拿出一个放到右边 - rightQueue.add(leftQueue.poll()); - //左边将 num 放入 - leftQueue.add(num); - } - } - } - - public double findMedian() { - if (leftQueue.size() > rightQueue.size()){ - return leftQueue.peek(); - }else{ - return ((double)leftQueue.peek() + rightQueue.peek()) / 2; - } - } -} -``` - -上边代码由于使用了优先队列,`addNum()` 的时间复杂度就是 `O(log(n))`。`findMedian()`的时间复杂度是 `O(1)`。上边为了保持两边集合的数量关系,写的代码比较多。再看一下 [stefanpochmann 大神](https://leetcode.com/problems/find-median-from-data-stream/discuss/74062/Short-simple-JavaC%2B%2BPython-O(log-n)-%2B-O(1)) 同样思路下的代码。 - -```java -class MedianFinder { - private Queue left = new PriorityQueue(), - right = new PriorityQueue(); - - public void addNum(int num) { - left.add((long) num); - right.add(-left.poll()); - if (left.size() < right.size()) - left.add(-right.poll()); - } - - public double findMedian() { - return left.size() > right.size() - ? left.peek() - : (left.peek() - right.peek()) / 2.0; - } -}; -``` - -简洁而优雅,这大概就是艺术吧。 - -他首先将数字加入到左边,然后再拿一个数字加到右边。然后判断一下左边数量是否小于右边,如果是的话将从右边拿回一个放到左边。 - -当加入新的数字之前,不管两边集合数字的数量相等,还是左边比右边多一个。通过上边的代码,依旧可以保证添加完数字以后两边的数量相等或者左边比右边多一个。 - -还使用了一个技巧,会发现它是用了两个默认的优先队列。对于右边的优先队列添加元素的时候将原来的数字取了相反数。这样做的好处就是,不管默认的优先队列是取最大数,还是取最小数。由于其中一个添加元素使用的是相反数,最终实现的效果就是两个优先队列一定是相反的效果。如果其中一个是取最小数,另外一个就是取最大数。 - -因为使用了相反数,对于 `-Integer.MIN_VALUE` 会溢出,所以我们添加元素的时候强转成了 `long`。 - - - -# 扩展 - -If all integer numbers from the stream are between 0 and 100, how would you optimize it? - -分享 [这里](https://leetcode.com/problems/find-median-from-data-stream/discuss/286238/Java-Simple-Code-Follow-Up) 的思路。 - -这样的话,我们可以用一个数组,`num[i]`记录数字 `i` 的数量。此外用一个变量 `n` 统计当前数字的总数量。这样求中位数的时候,我们只需要找到第 `n/2+1`个数或者 `n/2,n/2+1`个数即可。注意因为这里计数是从`1` 开始的,所以和解法一看起来找到数字不一样,解法一找的是下标。 - -```java -class MedianFinder { - int[] backets = new int[101]; - int n = 0; - - public MedianFinder() { - - } - - public void addNum(int num) { - backets[num]++; - n++; - } - - public double findMedian() { - - int count = 0; - int right = 0; - //如果是 5 个数,就寻找第 5 / 2 + 1 = 3 个数 - while (true) { - count += backets[right]; - if (count >= n / 2 + 1) { - break; - } - right++; - } - //奇数的情况直接返回 - if ((n & 1) == 1) { - return right; - } - //如果是 4 个数, 之前找到了第 4/2+1=3 个数, 还需要前一个数 - int left; - //如果之前找的数只占一个, 向前寻找上一个数 - if (backets[right] == 1) { - int temp = right - 1; - while (backets[temp] == 0) { - temp--; - } - left = temp; - //如果之前找的数占多个, 前一个数等于当前数 - } else { - left = right; - } - return (left + right) / 2.0; - - } -} -``` - -If 99% of all integer numbers from the stream are between 0 and 100, how would you optimize it? - -这个也没有说清楚,我们假设一种简单的情况。当调用 `findMedian` 的时候,解一定落在 `0 - 100` 之中。那么接着上边的代码,我们只需要增加一个变量 `less`,记录小于 `0` 的个数。原来找第 `n / 2 + 1` 个数,现在找第`n/2 - less + 1`就可以了。 - -如果调用 `findMedian` 的时候解落在哪里不一定,那么我们就增加两个 `list` 分别来保存小于 `0` 和大于 `100` 的数即可。 - -# 再扩展 - -题目说的是从数据流中找到中位数。如果这个数据流很大,很大,无法全部加载到内存呢? - -分享 [这里](https://stackoverflow.com/questions/10657503/find-running-median-from-a-stream-of-integers/10693752#10693752) 的想法。 - -如果数据整体呈某种概率分布,比如正态分布。我们可以通过 `reservoir sampling ` 的方法。我们保存固定数量的数字,当存满的时候,就随机替代掉某个数字。伪代码如下: - -```c -int n = 0; // Running count of elements observed so far -#define SIZE 10000 -int reservoir[SIZE]; - -while(streamHasData()) -{ - int x = readNumberFromStream(); - - if (n < SIZE) - { - reservoir[n++] = x; - } - else - { - int p = random(++n); // Choose a random number 0 >= p < n - if (p < SIZE) - { - reservoir[p] = x; - } - } -} -``` - -相当于从一个大的数据集下进行了取样,然后找中位数的时候,把我们保存的数组排个序去找就可以了。 - -# 总 - -总结了好多,但 [Solution](https://leetcode.com/problems/find-median-from-data-stream/solution/) 里提到的还有一些没有介绍,主要就是涉及到一些新的数据结构,比如 `Multiset ` 、`Segment Trees ` 和 ` Order Statistic Trees`,之前也没怎么用过,这里先留坑吧。 - +# 题目描述(困难难度) + +295、Find Median from Data Stream + +Median is the middle value in an ordered integer list. If the size of the list is even, there is no middle value. So the median is the mean of the two middle value. + +For example, + +``` +[2,3,4]`, the median is `3 +[2,3]`, the median is `(2 + 3) / 2 = 2.5 +``` + +Design a data structure that supports the following two operations: + +- void addNum(int num) - Add a integer number from the data stream to the data structure. +- double findMedian() - Return the median of all elements so far. + + + +**Example:** + +``` +addNum(1) +addNum(2) +findMedian() -> 1.5 +addNum(3) +findMedian() -> 2 +``` + + + +**Follow up:** + +1. If all integer numbers from the stream are between 0 and 100, how would you optimize it? +2. If 99% of all integer numbers from the stream are between 0 and 100, how would you optimize it? + +设计一个数据结构,提过两个接口,添加数字和返回当前集合内的中位数。 + +# 解法一 + +先分享 [官方](https://leetcode.com/problems/find-median-from-data-stream/solution/) 给我们提供的两个最容易的解法。 + +把添加的数字放到 `list` 中,如果需要返回中位数,把 `list` 排序即可。 + +```java +class MedianFinder { + List list = new ArrayList<>(); + + /** initialize your data structure here. */ + public MedianFinder() { + + } + + public void addNum(int num) { + list.add(num); + } + + public double findMedian() { + Collections.sort(list); + int n = list.size(); + if ((n & 1) == 1) { + return list.get(n / 2); + } else { + return ((double) list.get(n / 2) + list.get(n / 2 - 1)) / 2; + } + } +} +``` + +简单明了。但是时间复杂度有点儿高,对于 `findMedian` 函数,因为每次都需要排序。如果是快速排序,那时间复杂度也是 `O(nlog(n))`。 + +这里可以做一个简单的优化。我们不需要每次返回中位数都去排序。我们可以将排序融入到 `addNum` 中,假设之前已经有序了,然后将添加的数字插入到相应的位置即可。也就是插入排序的思想。 + +```java +class MedianFinder { + List list = new ArrayList<>(); + + /** initialize your data structure here. */ + public MedianFinder() { + + } + + public void addNum(int num) { + int i = 0; + // 寻找第一个大于 num 的数的下标 + for (; i < list.size(); i++) { + if (num < list.get(i)) { + break; + } + } + // 将当前数插入 + list.add(i, num); + } + + public double findMedian() { + int n = list.size(); + if ((n & 1) == 1) { + return list.get(n / 2); + } else { + return ((double) list.get(n / 2) + list.get(n / 2 - 1)) / 2; + } + } +} +``` + +上边的话 `findMedian()` 就不需要排序了,时间复杂度就是 `O(1)`了。对于 `addNum()` 函数的话时间复杂度就是 `O(n)` 了。 + +`addNum()` 还可以做一点优化。因为我们要在有序数组中寻找第一个大于 `num` 的下标,提到有序数组找某个值,可以想到二分的方法。 + +```java +public void addNum(int num) { + int insert = -1; + int low = 0; + int high = list.size() - 1; + while (low <= high) { + int mid = (low + high) >>> 1; + if (num <= list.get(mid)) { + //判断 num 是否大于等于前边的数 + int pre = mid > 0 ? list.get(mid - 1) : Integer.MIN_VALUE; + if (num >= pre) { + insert = mid; + break; + } else { + high = mid - 1; + } + } else { + low = mid + 1; + } + } + if (insert == -1) { + insert = list.size(); + } + // 将当前数插入 + list.add(insert, num); +} +``` + +虽然我们使用了二分去查找要插入的位置,对应的时间复杂度是 `O(log(n))`,但是 `list.add(insert, num)` 的时间复杂度是 `O(n)`。所以整体上依旧是 `O(n)`。 + +参考 [这里](https://leetcode.com/problems/find-median-from-data-stream/discuss/74057/Tired-of-TWO-HEAPSET-solutions-See-this-segment-dividing-solution-(c%2B%2B)),还能继续优化,这个思想也比较常见,分享一下。 + +我们每次添加数字的时候,都需要从所有数字中寻找要插入的位置,如果数字太多的话,速度会很慢。 + +我们可以将数字分成若干个子序列,类似于下图。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/295_2.jpg) + +上边每一个长方形内数字都是有序的。添加数字的时候,分成两步。先找到数字应该加入的长方形,然后将数字添加到该长方形内。这样做的好处很明显,我们只需要将数字加入长方形内的有序数列中,长方形内的数字个数相对于整个序列会小很多。 + +我们可以设置每个长方形内最多存多少数字,如果超过了限制,就将长方形平均分成两个。 + +举个简单的例子,接着上图,假设每个长方形内最多存 `3` 个数字,现在添加数字 `9`。 + +我们首先找到 `9` 应该属于第 `2` 个长方形,然后将 `9` 插入。然后发现此时的数量超过了 `3` 个,此时我们就把该长方形平均分成两个,如下图。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/295_3.jpg) + +至于最多存多少,我们可以根据总共有多少个数字来自己设定。但不是很好控制,太小了的话,寻找长方形的时候比较耗时间,太大的话,加入到长方形里的时候比较耗时间。事先不知道数字有多少的话,就更麻烦了。 + +[StefanPochmann](https://leetcode.com/stefanpochmann) 大神提出了一个建议。我们可以将大小设置成一个动态的,每个长方形最多存多少根据当前数字的总个数实时改变。假设当前数字总量是 `n`。长方形里数字个数是 `len` ,如果 `len * len > n` ,那么当前长方形就分割为两个。也就是每个长方形最多存 `sqrt(n)` 个数字。 + +这里我就偷个懒了,直接分享下 [@mission4success](https://leetcode.com/mission4success) 的代码。主要就是找长方形以及找中位数那里判断的情况会多一些。 + +```java +public class MedianFinder { + private LinkedList> buckets; // store all ranges + private int total_size; + + MedianFinder() { + total_size = 0; + buckets = new LinkedList<>(); + buckets.add(new LinkedList<>()); + } + + void addNum(int num) { + List correctRange = new LinkedList<>(); + int targetIndex = 0; + + // find the correct range to insert given num + for (int i = 0; i < buckets.size(); i++) { + if (buckets.size() == 1 || + (i == 0 && num <= buckets.get(i).getLast()) || + (i == buckets.size() - 1 && num >= buckets.get(i).getFirst()) || + (buckets.get(i).getFirst() <= num && num <= buckets.get(i).getLast()) || + (num > buckets.get(i).getLast() && num < buckets.get(i+1).getFirst())) { + correctRange = buckets.get(i); + targetIndex = i; + break; + } + } + + // put num at back of correct range, and sort it to keep increasing sequence + total_size++; + correctRange.add(num); + Collections.sort(correctRange); + + // if currentRange's size > threshold, split it into two halves and add them back to buckets + int len = correctRange.size(); + //if (len > 10) { + if (len * len > total_size) { + LinkedList half1 = new LinkedList<>(correctRange.subList(0, (len) / 2)); + LinkedList half2 = new LinkedList<>(correctRange.subList((len) / 2, len)); + + buckets.set(targetIndex, half1); //replaces + buckets.add(targetIndex + 1, half2); //inserts + } + + } + + // iterate thru all ranges in buckets to find median value + double findMedian() { + if (total_size==0) + return 0; + + int mid1 = total_size/2; + int mid2 = mid1 + 1; + + int leftCount=0; + double first = 0.0, second = 0.0; + for (List bucket : buckets) { + if (leftCount 1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值; +> 2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值; +> 3. 任意节点的左、右子树也分别为二叉查找树; +> 4. 没有键值相等的节点。 + +如果我们将数字存到二分查找树中,当找中位数的时候有一个明显的好处。如果我们知道了左子树的数量 `leftNum` ,找假设把数据排序后的第 `k` 个数,`k` 从 `0` 计数。 + +如果 `leftNum == k` ,那么根节点就是我们要找的。 + +如果 `leftNum > k`,我们只需要再从左子树中找第 `k` 个数。 + +如果 `leftNum < k`,我们只需要从右子树中找第 `k - leftNum - 1` 个数。 + +代码的话,我们首先定义一个二分查找树。和普通的二分查找树不同的地方在于,节点多了一个成员变量,记录以当前节点为根节点的二叉树的总节点数量。 + +此外实现了 `find` 函数,来返回有序情况下第 `k` 个节点的值。 + +```java +class BST { + class Node { + int val; + int size; + Node left, right; + + Node(int v) { + val = v; + size = 1; + }; + }; + + private Node root; + + BST() { + + } + + public void add(int val) { + // 新增节点 + Node newNode = new Node(val); + // 当前节点 + Node current = root; + // 上个节点 + Node parent = null; + // 如果根节点为空 + if (current == null) { + root = newNode; + return; + } + while (true) { + parent = current; + //向左子树添加节点 + if (val < current.val) { + current = current.left; + parent.size++; + if (current == null) { + parent.left = newNode; + return; + } + //向右子树添加节点 + } else { + current = current.right; + parent.size++; + if (current == null) { + parent.right = newNode; + return; + } + } + } + + } + + public int find(int k) { + Node t = root; + while (true) { + int leftSize = t.left != null ? t.left.size : 0; + if (leftSize == k) + return t.val; + if (leftSize > k) { + t = t.left; + } else { + k = k - leftSize - 1; + t = t.right; + } + } + } + + public int size() { + return root.size; + } +}; + +class MedianFinder { + BST bst; + + MedianFinder() { + bst = new BST(); + } + + // Adds a number into the data structure. + public void addNum(int num) { + bst.add(num); + } + + public double findMedian() { + int num = bst.size(); + if (num % 2 == 0) { + return ((double)bst.find(num / 2) + bst.find(num / 2 - 1)) / 2; + } else { + return bst.find(num / 2); + } + + } +}; +``` + +时间复杂度的话,最好的情况 `addNum` 和 `findMedian` 都是 `O(log(n))`。但如果二叉树分布不均,类似于下边这种,那么时间复杂度就都是 `O(n)` 了。 + +```java + 1 + \ + 2 + \ + 3 + \ + 4 +``` + +# 解法三 + +分享一下我最开始的想法。 + +什么是中位数?如果是偶数个数字,我们把它分成两个集合。左边的集合的所有数字小于右边集合的所有数字。中位数就是左边集合最大数和右边集合最小的数取一个平均数。 + +想到上边这个点,会发现我们只关心集合的最大数和最小数,立马就会想到优先队列。 + +添加数字的时候,我们把数字放到两个优先队列中。始终保证两个优先队列的大小相等。如果总数是奇数,我们就让左边集合多一个数。 + +有了上边的想法可以写代码了,代码大家写出来应该都不一样,分享下我的代码。 + +```java +class MedianFinder { + //左边的队列,每次取最大值 + Queue leftQueue = new PriorityQueue<>(new Comparator() { + @Override + public int compare(Integer i1, Integer i2) { + return i2 - i1; + } + }); + //右边的队列,每次取最小值 + Queue rightQueue = new PriorityQueue<>(); + + double median = 0; //保存当前的中位数 + + /** initialize your data structure here. */ + public MedianFinder() { + } + + public void addNum(int num) { + int leftSize = leftQueue.size(); + int rightSize = rightQueue.size(); + //如果当前数量相等 + if (leftSize == rightSize) { + //当前没有数字,将将数字加到左半部分 + if (leftSize == 0) { + leftQueue.add(num); + return; + } + //当前数字小于等于右半部分最小的数字, num 属于左边 + if (num <= rightQueue.peek()) { + leftQueue.add(num); + //当前数字大于右半部分最小的数字, num 应该属于右边 + } else { + //维持两边平衡. 将右边拿出一个放到左边 + leftQueue.add(rightQueue.poll()); + //将 num 放到右边 + rightQueue.add(num); + } + //如果当前数量不等 + } else { + //num 大于等于左边最大的数字, num 属于右边 + if (num >= leftQueue.peek()) { + rightQueue.add(num); + //num 小于左边最大的数字, num 应该属于左边 + } else { + //维持两边平衡, 左边拿出一个放到右边 + rightQueue.add(leftQueue.poll()); + //左边将 num 放入 + leftQueue.add(num); + } + } + } + + public double findMedian() { + if (leftQueue.size() > rightQueue.size()){ + return leftQueue.peek(); + }else{ + return ((double)leftQueue.peek() + rightQueue.peek()) / 2; + } + } +} +``` + +上边代码由于使用了优先队列,`addNum()` 的时间复杂度就是 `O(log(n))`。`findMedian()`的时间复杂度是 `O(1)`。上边为了保持两边集合的数量关系,写的代码比较多。再看一下 [stefanpochmann 大神](https://leetcode.com/problems/find-median-from-data-stream/discuss/74062/Short-simple-JavaC%2B%2BPython-O(log-n)-%2B-O(1)) 同样思路下的代码。 + +```java +class MedianFinder { + private Queue left = new PriorityQueue(), + right = new PriorityQueue(); + + public void addNum(int num) { + left.add((long) num); + right.add(-left.poll()); + if (left.size() < right.size()) + left.add(-right.poll()); + } + + public double findMedian() { + return left.size() > right.size() + ? left.peek() + : (left.peek() - right.peek()) / 2.0; + } +}; +``` + +简洁而优雅,这大概就是艺术吧。 + +他首先将数字加入到左边,然后再拿一个数字加到右边。然后判断一下左边数量是否小于右边,如果是的话将从右边拿回一个放到左边。 + +当加入新的数字之前,不管两边集合数字的数量相等,还是左边比右边多一个。通过上边的代码,依旧可以保证添加完数字以后两边的数量相等或者左边比右边多一个。 + +还使用了一个技巧,会发现它是用了两个默认的优先队列。对于右边的优先队列添加元素的时候将原来的数字取了相反数。这样做的好处就是,不管默认的优先队列是取最大数,还是取最小数。由于其中一个添加元素使用的是相反数,最终实现的效果就是两个优先队列一定是相反的效果。如果其中一个是取最小数,另外一个就是取最大数。 + +因为使用了相反数,对于 `-Integer.MIN_VALUE` 会溢出,所以我们添加元素的时候强转成了 `long`。 + + + +# 扩展 + +If all integer numbers from the stream are between 0 and 100, how would you optimize it? + +分享 [这里](https://leetcode.com/problems/find-median-from-data-stream/discuss/286238/Java-Simple-Code-Follow-Up) 的思路。 + +这样的话,我们可以用一个数组,`num[i]`记录数字 `i` 的数量。此外用一个变量 `n` 统计当前数字的总数量。这样求中位数的时候,我们只需要找到第 `n/2+1`个数或者 `n/2,n/2+1`个数即可。注意因为这里计数是从`1` 开始的,所以和解法一看起来找到数字不一样,解法一找的是下标。 + +```java +class MedianFinder { + int[] backets = new int[101]; + int n = 0; + + public MedianFinder() { + + } + + public void addNum(int num) { + backets[num]++; + n++; + } + + public double findMedian() { + + int count = 0; + int right = 0; + //如果是 5 个数,就寻找第 5 / 2 + 1 = 3 个数 + while (true) { + count += backets[right]; + if (count >= n / 2 + 1) { + break; + } + right++; + } + //奇数的情况直接返回 + if ((n & 1) == 1) { + return right; + } + //如果是 4 个数, 之前找到了第 4/2+1=3 个数, 还需要前一个数 + int left; + //如果之前找的数只占一个, 向前寻找上一个数 + if (backets[right] == 1) { + int temp = right - 1; + while (backets[temp] == 0) { + temp--; + } + left = temp; + //如果之前找的数占多个, 前一个数等于当前数 + } else { + left = right; + } + return (left + right) / 2.0; + + } +} +``` + +If 99% of all integer numbers from the stream are between 0 and 100, how would you optimize it? + +这个也没有说清楚,我们假设一种简单的情况。当调用 `findMedian` 的时候,解一定落在 `0 - 100` 之中。那么接着上边的代码,我们只需要增加一个变量 `less`,记录小于 `0` 的个数。原来找第 `n / 2 + 1` 个数,现在找第`n/2 - less + 1`就可以了。 + +如果调用 `findMedian` 的时候解落在哪里不一定,那么我们就增加两个 `list` 分别来保存小于 `0` 和大于 `100` 的数即可。 + +# 再扩展 + +题目说的是从数据流中找到中位数。如果这个数据流很大,很大,无法全部加载到内存呢? + +分享 [这里](https://stackoverflow.com/questions/10657503/find-running-median-from-a-stream-of-integers/10693752#10693752) 的想法。 + +如果数据整体呈某种概率分布,比如正态分布。我们可以通过 `reservoir sampling ` 的方法。我们保存固定数量的数字,当存满的时候,就随机替代掉某个数字。伪代码如下: + +```c +int n = 0; // Running count of elements observed so far +#define SIZE 10000 +int reservoir[SIZE]; + +while(streamHasData()) +{ + int x = readNumberFromStream(); + + if (n < SIZE) + { + reservoir[n++] = x; + } + else + { + int p = random(++n); // Choose a random number 0 >= p < n + if (p < SIZE) + { + reservoir[p] = x; + } + } +} +``` + +相当于从一个大的数据集下进行了取样,然后找中位数的时候,把我们保存的数组排个序去找就可以了。 + +# 总 + +总结了好多,但 [Solution](https://leetcode.com/problems/find-median-from-data-stream/solution/) 里提到的还有一些没有介绍,主要就是涉及到一些新的数据结构,比如 `Multiset ` 、`Segment Trees ` 和 ` Order Statistic Trees`,之前也没怎么用过,这里先留坑吧。 + 针对这道题最优的话还是优先队列比较好,既简单,又容易想到。其他的解法可以当扩展思路了,其中用到的一些思想都很有意思。 \ No newline at end of file diff --git a/leetcode-297-Serialize-and-Deserialize-Binary-Tree.md b/leetcode-297-Serialize-and-Deserialize-Binary-Tree.md index cfaf85a7d..0af023255 100644 --- a/leetcode-297-Serialize-and-Deserialize-Binary-Tree.md +++ b/leetcode-297-Serialize-and-Deserialize-Binary-Tree.md @@ -1,360 +1,360 @@ -# 题目描述(简单难度) - -297、Serialize and Deserialize Binary Tree - -Serialization is the process of converting a data structure or object into a sequence of bits so that it can be stored in a file or memory buffer, or transmitted across a network connection link to be reconstructed later in the same or another computer environment. - -Design an algorithm to serialize and deserialize a binary tree. There is no restriction on how your serialization/deserialization algorithm should work. You just need to ensure that a binary tree can be serialized to a string and this string can be deserialized to the original tree structure. - -**Example:** - -``` -You may serialize the following tree: - - 1 - / \ - 2 3 - / \ - 4 5 - -as "[1,2,3,null,null,4,5]" -``` - -**Clarification:** The above format is the same as [how LeetCode serializes a binary tree](https://leetcode.com/faq/#binary-tree). You do not necessarily need to follow this format, so please be creative and come up with different approaches yourself. - -**Note:** Do not use class member/global/static variables to store states. Your serialize and deserialize algorithms should be stateless. - -提供两个方法,一个方法将二叉树序列化为一个字符串,另一个方法将序列化的字符串还原为二叉树。 - -# 解法一 - -来个偷懒的方法,我们知道通过先序遍历和中序遍历可以还原一个二叉树。[144 题](https://leetcode.wang/leetcode-144-Binary-Tree-Preorder-Traversal.html) 的先序遍历,[94 题](https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html) 的中序遍历,[105 题](https://leetcode.wang/leetcode-105-Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal.html) 的通过先序遍历和中序遍历还原二叉树。 - -上边主要的代码都有了,我们还需要将先序遍历和中序遍历的结果转为字符串,以及从字符串还原先序遍历和中序遍历的结果。 - -`list` 有 `toString` 方法,它会把 `list` 转成 `"[1, 2, 3, 5]"`这样类似的字符串。所以把这个字符串还原为 `list` 的时候,我们只需要去掉首尾的中括号,然后用逗号切割即可。 - -```java -// Encodes a tree to a single string. -public String serialize(TreeNode root) { - if (root == null) { - return ""; - } - List preOrder = preorderTraversal(root); - List inOrder = inorderTraversal(root); - //两个结果用 "@" 分割 - return preOrder + "@" + inOrder; -} - -// Decodes your encoded data to tree. -public TreeNode deserialize(String data) { - if (data.length() == 0) { - return null; - } - String[] split = data.split("@"); - //还原先序遍历的结果 - String[] preStr = split[0].substring(1, split[0].length() - 1).split(","); - int[] preorder = new int[preStr.length]; - for (int i = 0; i < preStr.length; i++) { - //trim 是为了去除首尾多余的空格 - preorder[i] = Integer.parseInt(preStr[i].trim()); - } - - //还原中序遍历的结果 - String[] inStr = split[1].substring(1, split[1].length() - 1).split(","); - int[] inorder = new int[inStr.length]; - for (int i = 0; i < inStr.length; i++) { - inorder[i] = Integer.parseInt(inStr[i].trim()); - } - - return buildTree(preorder, inorder); -} - -// 前序遍历 -public List preorderTraversal(TreeNode root) { - List list = new ArrayList<>(); - preorderTraversalHelper(root, list); - return list; -} - -private void preorderTraversalHelper(TreeNode root, List list) { - if (root == null) { - return; - } - list.add(root.val); - preorderTraversalHelper(root.left, list); - preorderTraversalHelper(root.right, list); -} - -// 中序遍历 -public List inorderTraversal(TreeNode root) { - List ans = new ArrayList<>(); - getAns(root, ans); - return ans; -} - -private void getAns(TreeNode node, List ans) { - if (node == null) { - return; - } - getAns(node.left, ans); - ans.add(node.val); - getAns(node.right, ans); -} - -//还原二叉树 -private TreeNode buildTree(int[] preorder, int[] inorder) { - return buildTreeHelper(preorder, 0, preorder.length, inorder, 0, inorder.length); -} - -private TreeNode buildTreeHelper(int[] preorder, int p_start, int p_end, int[] inorder, int i_start, int i_end) { - // preorder 为空,直接返回 null - if (p_start == p_end) { - return null; - } - int root_val = preorder[p_start]; - TreeNode root = new TreeNode(root_val); - // 在中序遍历中找到根节点的位置 - int i_root_index = 0; - for (int i = i_start; i < i_end; i++) { - if (root_val == inorder[i]) { - i_root_index = i; - break; - } - } - int leftNum = i_root_index - i_start; - // 递归的构造左子树 - root.left = buildTreeHelper(preorder, p_start + 1, p_start + leftNum + 1, inorder, i_start, i_root_index); - // 递归的构造右子树 - root.right = buildTreeHelper(preorder, p_start + leftNum + 1, p_end, inorder, i_root_index + 1, i_end); - return root; -} -``` - -但是竟然遇到了 `WA`。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/297_2.jpg) - -我开始看到的结果的时候真的小小的震惊了一下,哪里出了问题。用的都是之前的代码,只可能是字符串转换那里出问题了。然后调试了一下发现没有问题,甚至又回到之前的题重新提交了一下,也是没有问题的。 - -先序遍历和中序遍历唯一确定一个二叉树,这个定理错了???然后用上边的样例调试了一下,恍然大悟,这个定理的前提必须得是没有重复的元素。 - -# 解法二 - -好吧,看来不能偷懒。那我们就用 `leetcode` 所使用的方式吧,通过层次遍历来序列化和还原二叉树。 - -我们只需要将每一层的序列存到数组中,如果是 `null` 就存 `null`。可以结合 [102 题](https://leetcode.wang/leetcode-102-Binary-Tree-Level-Order-Traversal.html) 二叉树的层次遍历。 - -```java -// Encodes a tree to a single string. -public String serialize(TreeNode root) { - if (root == null) { - return ""; - } - Queue queue = new LinkedList(); - List res = new LinkedList(); - queue.offer(root); - //BFS - while (!queue.isEmpty()) { - TreeNode curNode = queue.poll(); - if (curNode != null) { - res.add(curNode.val); - queue.offer(curNode.left); - queue.offer(curNode.right); - } else { - res.add(null); - } - } - return res.toString(); -} - -// Decodes your encoded data to tree. -public TreeNode deserialize(String data) { - if (data.length() == 0) { - return null; - } - //将字符串还原为数组 - String[] preStr = data.substring(1, data.length() - 1).split(","); - Integer[] bfsOrder = new Integer[preStr.length]; - for (int i = 0; i < preStr.length; i++) { - if (preStr[i].trim().equals("null")) { - bfsOrder[i] = null; - } else { - bfsOrder[i] = Integer.parseInt(preStr[i].trim()); - } - } - - Queue queue = new LinkedList(); - TreeNode root = new TreeNode(bfsOrder[0]); - int cur = 1;//通过 cur 指针依次给节点赋值 - queue.offer(root); - while (!queue.isEmpty()) { - TreeNode curNode = queue.poll(); - if (bfsOrder[cur] != null) { - curNode.left = new TreeNode(bfsOrder[cur]); - queue.add(curNode.left); - } - cur++; - if (bfsOrder[cur] != null) { - curNode.right = new TreeNode(bfsOrder[cur]); - queue.add(curNode.right); - } - cur++; - } - return root; -} -``` - -上边的方法已经可以 `AC` 了,但还可以做一个小小的优化。 - -如果通过上边的代码,对于下边的二叉树。 - -```java - 1 - / \ - 2 3 - / -4 -``` - -序列化成字符串就是 `"[1, 2, 3, 4, null, null, null, null, null]"`。就是下边的样子。 - -```java -n 表示 null - 1 - / \ - 2 3 - / \ / \ - 4 n n n - / \ - n n -``` - -当我们一层一层的还原的时候,因为 `TreeNode` 的默认值就是 `null`。所以还原到 `4` 的时候后边其实就不需要管了。 - -因为末尾的 `null` 是没有必要的,所以在返回之前,我们可以把末尾的 `null` 去掉。此外,`deserialize()` 函数中,因为我们去掉了末尾的 `null`,所以当 `cur` 到达数组末尾的时候要提前结束循环。 - -```java -// Encodes a tree to a single string. -public String serialize(TreeNode root) { - if (root == null) { - return ""; - } - Queue queue = new LinkedList(); - List res = new LinkedList(); - queue.offer(root); - while (!queue.isEmpty()) { - TreeNode curNode = queue.poll(); - if (curNode != null) { - res.add(curNode.val); - queue.offer(curNode.left); - queue.offer(curNode.right); - } else { - res.add(null); - } - } - //去掉末尾的 null - while (true) { - if (res.get(res.size() - 1) == null) { - res.remove(res.size() - 1); - } else { - break; - } - } - return res.toString(); -} - -// Decodes your encoded data to tree. -public TreeNode deserialize(String data) { - if (data.length() == 0) { - return null; - } - String[] preStr = data.substring(1, data.length() - 1).split(","); - Integer[] bfsOrder = new Integer[preStr.length]; - for (int i = 0; i < preStr.length; i++) { - if (preStr[i].trim().equals("null")) { - bfsOrder[i] = null; - } else { - bfsOrder[i] = Integer.parseInt(preStr[i].trim()); - } - } - - Queue queue = new LinkedList(); - TreeNode root = new TreeNode(bfsOrder[0]); - int cur = 1; - queue.offer(root); - while (!queue.isEmpty()) { - if (cur == bfsOrder.length) { - break; - } - TreeNode curNode = queue.poll(); - if (bfsOrder[cur] != null) { - curNode.left = new TreeNode(bfsOrder[cur]); - queue.add(curNode.left); - } - cur++; - if (cur == bfsOrder.length) { - break; - } - if (bfsOrder[cur] != null) { - curNode.right = new TreeNode(bfsOrder[cur]); - queue.add(curNode.right); - } - cur++; - } - return root; -} -``` - -# 解法三 - -我们可以只用先序遍历。什么???只用先序遍历,是的,你没有听错。我开始也没往这方面想。直到看到 [这里](https://leetcode.com/problems/serialize-and-deserialize-binary-tree/discuss/74253/Easy-to-understand-Java-Solution) 的题解。 - -为什么可以只用先序遍历?因为我们先序遍历过程中把遇到的 `null` 也保存起来了。所以本质上和解法二的 `BFS` 是一样的。 - -此外,他没有套用之前先序遍历的代码,重写了先序遍历,在遍历过程中生成序列化的字符串。 - -```java -private static final String spliter = ","; -private static final String NN = "X"; //当做 null - -// Encodes a tree to a single string. -public String serialize(TreeNode root) { - StringBuilder sb = new StringBuilder(); - buildString(root, sb); - return sb.toString(); -} - -private void buildString(TreeNode node, StringBuilder sb) { - if (node == null) { - sb.append(NN).append(spliter); - } else { - sb.append(node.val).append(spliter); - buildString(node.left, sb); - buildString(node.right,sb); - } -} -// Decodes your encoded data to tree. -public TreeNode deserialize(String data) { - Deque nodes = new LinkedList<>(); - nodes.addAll(Arrays.asList(data.split(spliter))); - return buildTree(nodes); -} - -private TreeNode buildTree(Deque nodes) { - String val = nodes.remove(); - if (val.equals(NN)) return null; - else { - TreeNode node = new TreeNode(Integer.valueOf(val)); - node.left = buildTree(nodes); - node.right = buildTree(nodes); - return node; - } -} -``` - -# 总 - -这道题的话完善了自己脑子里的一些认识,先序遍历和中序遍历可以唯一的确定一个二叉树,前提是元素必须不一样。其实看通过先序遍历和中序遍历还原二叉树的代码也可以知道,因为我们需要找根节点的下标,如果有重复的值,肯定就不行了。 - +# 题目描述(简单难度) + +297、Serialize and Deserialize Binary Tree + +Serialization is the process of converting a data structure or object into a sequence of bits so that it can be stored in a file or memory buffer, or transmitted across a network connection link to be reconstructed later in the same or another computer environment. + +Design an algorithm to serialize and deserialize a binary tree. There is no restriction on how your serialization/deserialization algorithm should work. You just need to ensure that a binary tree can be serialized to a string and this string can be deserialized to the original tree structure. + +**Example:** + +``` +You may serialize the following tree: + + 1 + / \ + 2 3 + / \ + 4 5 + +as "[1,2,3,null,null,4,5]" +``` + +**Clarification:** The above format is the same as [how LeetCode serializes a binary tree](https://leetcode.com/faq/#binary-tree). You do not necessarily need to follow this format, so please be creative and come up with different approaches yourself. + +**Note:** Do not use class member/global/static variables to store states. Your serialize and deserialize algorithms should be stateless. + +提供两个方法,一个方法将二叉树序列化为一个字符串,另一个方法将序列化的字符串还原为二叉树。 + +# 解法一 + +来个偷懒的方法,我们知道通过先序遍历和中序遍历可以还原一个二叉树。[144 题](https://leetcode.wang/leetcode-144-Binary-Tree-Preorder-Traversal.html) 的先序遍历,[94 题](https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html) 的中序遍历,[105 题](https://leetcode.wang/leetcode-105-Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal.html) 的通过先序遍历和中序遍历还原二叉树。 + +上边主要的代码都有了,我们还需要将先序遍历和中序遍历的结果转为字符串,以及从字符串还原先序遍历和中序遍历的结果。 + +`list` 有 `toString` 方法,它会把 `list` 转成 `"[1, 2, 3, 5]"`这样类似的字符串。所以把这个字符串还原为 `list` 的时候,我们只需要去掉首尾的中括号,然后用逗号切割即可。 + +```java +// Encodes a tree to a single string. +public String serialize(TreeNode root) { + if (root == null) { + return ""; + } + List preOrder = preorderTraversal(root); + List inOrder = inorderTraversal(root); + //两个结果用 "@" 分割 + return preOrder + "@" + inOrder; +} + +// Decodes your encoded data to tree. +public TreeNode deserialize(String data) { + if (data.length() == 0) { + return null; + } + String[] split = data.split("@"); + //还原先序遍历的结果 + String[] preStr = split[0].substring(1, split[0].length() - 1).split(","); + int[] preorder = new int[preStr.length]; + for (int i = 0; i < preStr.length; i++) { + //trim 是为了去除首尾多余的空格 + preorder[i] = Integer.parseInt(preStr[i].trim()); + } + + //还原中序遍历的结果 + String[] inStr = split[1].substring(1, split[1].length() - 1).split(","); + int[] inorder = new int[inStr.length]; + for (int i = 0; i < inStr.length; i++) { + inorder[i] = Integer.parseInt(inStr[i].trim()); + } + + return buildTree(preorder, inorder); +} + +// 前序遍历 +public List preorderTraversal(TreeNode root) { + List list = new ArrayList<>(); + preorderTraversalHelper(root, list); + return list; +} + +private void preorderTraversalHelper(TreeNode root, List list) { + if (root == null) { + return; + } + list.add(root.val); + preorderTraversalHelper(root.left, list); + preorderTraversalHelper(root.right, list); +} + +// 中序遍历 +public List inorderTraversal(TreeNode root) { + List ans = new ArrayList<>(); + getAns(root, ans); + return ans; +} + +private void getAns(TreeNode node, List ans) { + if (node == null) { + return; + } + getAns(node.left, ans); + ans.add(node.val); + getAns(node.right, ans); +} + +//还原二叉树 +private TreeNode buildTree(int[] preorder, int[] inorder) { + return buildTreeHelper(preorder, 0, preorder.length, inorder, 0, inorder.length); +} + +private TreeNode buildTreeHelper(int[] preorder, int p_start, int p_end, int[] inorder, int i_start, int i_end) { + // preorder 为空,直接返回 null + if (p_start == p_end) { + return null; + } + int root_val = preorder[p_start]; + TreeNode root = new TreeNode(root_val); + // 在中序遍历中找到根节点的位置 + int i_root_index = 0; + for (int i = i_start; i < i_end; i++) { + if (root_val == inorder[i]) { + i_root_index = i; + break; + } + } + int leftNum = i_root_index - i_start; + // 递归的构造左子树 + root.left = buildTreeHelper(preorder, p_start + 1, p_start + leftNum + 1, inorder, i_start, i_root_index); + // 递归的构造右子树 + root.right = buildTreeHelper(preorder, p_start + leftNum + 1, p_end, inorder, i_root_index + 1, i_end); + return root; +} +``` + +但是竟然遇到了 `WA`。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/297_2.jpg) + +我开始看到的结果的时候真的小小的震惊了一下,哪里出了问题。用的都是之前的代码,只可能是字符串转换那里出问题了。然后调试了一下发现没有问题,甚至又回到之前的题重新提交了一下,也是没有问题的。 + +先序遍历和中序遍历唯一确定一个二叉树,这个定理错了???然后用上边的样例调试了一下,恍然大悟,这个定理的前提必须得是没有重复的元素。 + +# 解法二 + +好吧,看来不能偷懒。那我们就用 `leetcode` 所使用的方式吧,通过层次遍历来序列化和还原二叉树。 + +我们只需要将每一层的序列存到数组中,如果是 `null` 就存 `null`。可以结合 [102 题](https://leetcode.wang/leetcode-102-Binary-Tree-Level-Order-Traversal.html) 二叉树的层次遍历。 + +```java +// Encodes a tree to a single string. +public String serialize(TreeNode root) { + if (root == null) { + return ""; + } + Queue queue = new LinkedList(); + List res = new LinkedList(); + queue.offer(root); + //BFS + while (!queue.isEmpty()) { + TreeNode curNode = queue.poll(); + if (curNode != null) { + res.add(curNode.val); + queue.offer(curNode.left); + queue.offer(curNode.right); + } else { + res.add(null); + } + } + return res.toString(); +} + +// Decodes your encoded data to tree. +public TreeNode deserialize(String data) { + if (data.length() == 0) { + return null; + } + //将字符串还原为数组 + String[] preStr = data.substring(1, data.length() - 1).split(","); + Integer[] bfsOrder = new Integer[preStr.length]; + for (int i = 0; i < preStr.length; i++) { + if (preStr[i].trim().equals("null")) { + bfsOrder[i] = null; + } else { + bfsOrder[i] = Integer.parseInt(preStr[i].trim()); + } + } + + Queue queue = new LinkedList(); + TreeNode root = new TreeNode(bfsOrder[0]); + int cur = 1;//通过 cur 指针依次给节点赋值 + queue.offer(root); + while (!queue.isEmpty()) { + TreeNode curNode = queue.poll(); + if (bfsOrder[cur] != null) { + curNode.left = new TreeNode(bfsOrder[cur]); + queue.add(curNode.left); + } + cur++; + if (bfsOrder[cur] != null) { + curNode.right = new TreeNode(bfsOrder[cur]); + queue.add(curNode.right); + } + cur++; + } + return root; +} +``` + +上边的方法已经可以 `AC` 了,但还可以做一个小小的优化。 + +如果通过上边的代码,对于下边的二叉树。 + +```java + 1 + / \ + 2 3 + / +4 +``` + +序列化成字符串就是 `"[1, 2, 3, 4, null, null, null, null, null]"`。就是下边的样子。 + +```java +n 表示 null + 1 + / \ + 2 3 + / \ / \ + 4 n n n + / \ + n n +``` + +当我们一层一层的还原的时候,因为 `TreeNode` 的默认值就是 `null`。所以还原到 `4` 的时候后边其实就不需要管了。 + +因为末尾的 `null` 是没有必要的,所以在返回之前,我们可以把末尾的 `null` 去掉。此外,`deserialize()` 函数中,因为我们去掉了末尾的 `null`,所以当 `cur` 到达数组末尾的时候要提前结束循环。 + +```java +// Encodes a tree to a single string. +public String serialize(TreeNode root) { + if (root == null) { + return ""; + } + Queue queue = new LinkedList(); + List res = new LinkedList(); + queue.offer(root); + while (!queue.isEmpty()) { + TreeNode curNode = queue.poll(); + if (curNode != null) { + res.add(curNode.val); + queue.offer(curNode.left); + queue.offer(curNode.right); + } else { + res.add(null); + } + } + //去掉末尾的 null + while (true) { + if (res.get(res.size() - 1) == null) { + res.remove(res.size() - 1); + } else { + break; + } + } + return res.toString(); +} + +// Decodes your encoded data to tree. +public TreeNode deserialize(String data) { + if (data.length() == 0) { + return null; + } + String[] preStr = data.substring(1, data.length() - 1).split(","); + Integer[] bfsOrder = new Integer[preStr.length]; + for (int i = 0; i < preStr.length; i++) { + if (preStr[i].trim().equals("null")) { + bfsOrder[i] = null; + } else { + bfsOrder[i] = Integer.parseInt(preStr[i].trim()); + } + } + + Queue queue = new LinkedList(); + TreeNode root = new TreeNode(bfsOrder[0]); + int cur = 1; + queue.offer(root); + while (!queue.isEmpty()) { + if (cur == bfsOrder.length) { + break; + } + TreeNode curNode = queue.poll(); + if (bfsOrder[cur] != null) { + curNode.left = new TreeNode(bfsOrder[cur]); + queue.add(curNode.left); + } + cur++; + if (cur == bfsOrder.length) { + break; + } + if (bfsOrder[cur] != null) { + curNode.right = new TreeNode(bfsOrder[cur]); + queue.add(curNode.right); + } + cur++; + } + return root; +} +``` + +# 解法三 + +我们可以只用先序遍历。什么???只用先序遍历,是的,你没有听错。我开始也没往这方面想。直到看到 [这里](https://leetcode.com/problems/serialize-and-deserialize-binary-tree/discuss/74253/Easy-to-understand-Java-Solution) 的题解。 + +为什么可以只用先序遍历?因为我们先序遍历过程中把遇到的 `null` 也保存起来了。所以本质上和解法二的 `BFS` 是一样的。 + +此外,他没有套用之前先序遍历的代码,重写了先序遍历,在遍历过程中生成序列化的字符串。 + +```java +private static final String spliter = ","; +private static final String NN = "X"; //当做 null + +// Encodes a tree to a single string. +public String serialize(TreeNode root) { + StringBuilder sb = new StringBuilder(); + buildString(root, sb); + return sb.toString(); +} + +private void buildString(TreeNode node, StringBuilder sb) { + if (node == null) { + sb.append(NN).append(spliter); + } else { + sb.append(node.val).append(spliter); + buildString(node.left, sb); + buildString(node.right,sb); + } +} +// Decodes your encoded data to tree. +public TreeNode deserialize(String data) { + Deque nodes = new LinkedList<>(); + nodes.addAll(Arrays.asList(data.split(spliter))); + return buildTree(nodes); +} + +private TreeNode buildTree(Deque nodes) { + String val = nodes.remove(); + if (val.equals(NN)) return null; + else { + TreeNode node = new TreeNode(Integer.valueOf(val)); + node.left = buildTree(nodes); + node.right = buildTree(nodes); + return node; + } +} +``` + +# 总 + +这道题的话完善了自己脑子里的一些认识,先序遍历和中序遍历可以唯一的确定一个二叉树,前提是元素必须不一样。其实看通过先序遍历和中序遍历还原二叉树的代码也可以知道,因为我们需要找根节点的下标,如果有重复的值,肯定就不行了。 + 其次,如果二叉树的遍历考虑了 `null`,那么不管什么遍历我们都能把二叉树还原。 \ No newline at end of file diff --git a/leetcode-299-Bulls-and-Cows.md b/leetcode-299-Bulls-and-Cows.md index 210257e87..fae043e83 100644 --- a/leetcode-299-Bulls-and-Cows.md +++ b/leetcode-299-Bulls-and-Cows.md @@ -1,201 +1,201 @@ -# 题目描述(简单难度) - -299、Bulls and Cows - -You are playing the following [Bulls and Cows](https://en.wikipedia.org/wiki/Bulls_and_Cows) game with your friend: You write down a number and ask your friend to guess what the number is. Each time your friend makes a guess, you provide a hint that indicates how many digits in said guess match your secret number exactly in both digit and position (called "bulls") and how many digits match the secret number but locate in the wrong position (called "cows"). Your friend will use successive guesses and hints to eventually derive the secret number. - -Write a function to return a hint according to the secret number and friend's guess, use `A` to indicate the bulls and `B` to indicate the cows. - -Please note that both secret number and friend's guess may contain duplicate digits. - -**Example 1:** - -``` -Input: secret = "1807", guess = "7810" - -Output: "1A3B" - -Explanation: 1 bull and 3 cows. The bull is 8, the cows are 0, 1 and 7. -``` - -**Example 2:** - -``` -Input: secret = "1123", guess = "0111" - -Output: "1A1B" - -Explanation: The 1st 1 in friend's guess is a bull, the 2nd or 3rd 1 is a cow. -``` - -**Note:** You may assume that the secret number and your friend's guess only contain digits, and their lengths are always equal. - -说简单点就是统计两个字符串中数字和位置都对应相等的个数以及数字相等但位置不一样的个数。 - -# 解法一 - -只要理解了题意的话,这道题还是比较容易写的。分两步。 - -首先统计数字和位置都对应相等的个数,只要依次遍历两个数组看是否对应相等即可。 - -```java -int A = 0; -for (int i = 0; i < guess.length(); i++) { - if (secret.charAt(i) == guess.charAt(i)) { - A++; - } -} -``` - -然后我们可以通过两个 `HashMap` 统计相等的数字有多少个。我们只需要分别记录 `secret` 和 `guess` 中每个数字的个数,然后取两者较小的就是相等数字的个数了。 - -比如 `secret` 中有 `3` 个 `2`,`guess` 中有`4` 个 `2`,那么两者相等的数字就至少有 `3` 个。 - -```java -HashMap mapS = new HashMap<>(); -HashMap mapG = new HashMap<>(); -//统计每个数字的个数 -for (int i = 0; i < secret.length(); i++) { - mapS.put(secret.charAt(i), mapS.getOrDefault(secret.charAt(i), 0) + 1); - mapG.put(guess.charAt(i), mapG.getOrDefault(guess.charAt(i), 0) + 1); -} -int B = 0; -//两者取较小 -for (Character key : mapG.keySet()) { - int n1 = mapG.getOrDefault(key, 0); - int n2 = mapS.getOrDefault(key, 0); - B = B + Math.min(n1, n2); -} -``` - -题目中让我们求的是数字相等但位置不同的,上边的 `B` 把位置相同的也包括了,所以最终的结果应该是 `B - A`。 - -上边的两块代码结合起来即可。 - -```java -public String getHint(String secret, String guess) { - int A = 0; - for (int i = 0; i < guess.length(); i++) { - if (secret.charAt(i) == guess.charAt(i)) { - A++; - } - } - HashMap mapS = new HashMap<>(); - HashMap mapG = new HashMap<>(); - for (int i = 0; i < secret.length(); i++) { - mapS.put(secret.charAt(i), mapS.getOrDefault(secret.charAt(i), 0) + 1); - mapG.put(guess.charAt(i), mapG.getOrDefault(guess.charAt(i), 0) + 1); - } - int B = 0; - for (Character key : mapG.keySet()) { - int n1 = mapG.getOrDefault(key, 0); - int n2 = mapS.getOrDefault(key, 0); - B = B + Math.min(n1, n2); - } - return A + "A" + (B - A) + "B"; -} -``` - -分享 [这里](https://leetcode.com/problems/bulls-and-cows/discuss/74629/My-3ms-Java-solution-may-help-u) 的代码,我们还可以做两点优化。 - -第一点,因为 `map` 中的 `key` 只有 `0` 到 `9`,所以我们可以用数组取代 `map` 。 - -第二点,我们可以把第二步用 `map` 统计个数的代码放到第一个 `for` 循环中,当位置不相等的时候再统计个数,这样最后就不用 `B - A`了。 - -```java -public String getHint(String secret, String guess) { - int A = 0; - int[] mapS = new int[10]; - int[] mapG = new int[10]; - for (int i = 0; i < guess.length(); i++) { - if (secret.charAt(i) == guess.charAt(i)) { - A++; - } else { - mapS[secret.charAt(i) - '0']++; - mapG[guess.charAt(i) - '0']++; - } - } - int B = 0; - for (int i = 0; i < 10; i++) { - B += Math.min(mapS[i], mapG[i]); - } - return A + "A" + B + "B"; -} -``` - -# 解法二 - -解法一最后的代码依旧有两个 `for` 循环,分享 [这里](https://leetcode.com/problems/bulls-and-cows/discuss/74621/One-pass-Java-solution) 只有一个循环的代码。 - -比较巧妙,相对于解法一难理解些,可以结合下边的注释理解一下。 - -```java -public String getHint(String secret, String guess) { - int bulls = 0; - int cows = 0; - int[] numbers = new int[10]; - for (int i = 0; i 0) cows++; - //secret 中的数, 计数加 1 - numbers[s] ++; - //guess 中的数, 计数减 1 - numbers[g] --; - } - } - return bulls + "A" + cows + "B"; -} -``` - -理解不了的话,可以举个例子。 - -```java -"231" 和 "321" - -secret 2 3 1 -guess 3 2 1 - ^ - i - -当前遍历到第 1 个数, s = 2, g = 3 -numbers[s]++, 也就是 numbers[2] = 1 -numbers[g]--, 也就是 numbers[3] = -1 - -secret 2 3 2 -guess 3 2 1 - ^ - i - -numbers[2] = 1, numbers[3] = -1 - -当前遍历到第 2 个数, s = 3, g = 2 -此时 numbers[s], 也就是 numbers[3] < 0, cows++ -此时 numbers[g], 也就是 numbers[2] > 0, cows++ -cows = 2 - -继续执行 -numbers[s]++, 也就是 numbers[3] + 1 = -1 + 1 = 0 -numbers[g]--, 也就是 numbers[2] - 1 = 1 - 1 = 0 - -secret 2 3 2 -guess 3 2 1 - ^ - i -numbers[2] = 0, numbers[3] = 0 -当前遍历到第 3 个数, s = 2, g = 1 -此时 s 虽然又遇到了 2, 但是 number[2] = 0 了, 无法再匹配, 所以 cows 不会加 1 了 - -最终 cows 就是 2 了 -``` - - - -# 总 - +# 题目描述(简单难度) + +299、Bulls and Cows + +You are playing the following [Bulls and Cows](https://en.wikipedia.org/wiki/Bulls_and_Cows) game with your friend: You write down a number and ask your friend to guess what the number is. Each time your friend makes a guess, you provide a hint that indicates how many digits in said guess match your secret number exactly in both digit and position (called "bulls") and how many digits match the secret number but locate in the wrong position (called "cows"). Your friend will use successive guesses and hints to eventually derive the secret number. + +Write a function to return a hint according to the secret number and friend's guess, use `A` to indicate the bulls and `B` to indicate the cows. + +Please note that both secret number and friend's guess may contain duplicate digits. + +**Example 1:** + +``` +Input: secret = "1807", guess = "7810" + +Output: "1A3B" + +Explanation: 1 bull and 3 cows. The bull is 8, the cows are 0, 1 and 7. +``` + +**Example 2:** + +``` +Input: secret = "1123", guess = "0111" + +Output: "1A1B" + +Explanation: The 1st 1 in friend's guess is a bull, the 2nd or 3rd 1 is a cow. +``` + +**Note:** You may assume that the secret number and your friend's guess only contain digits, and their lengths are always equal. + +说简单点就是统计两个字符串中数字和位置都对应相等的个数以及数字相等但位置不一样的个数。 + +# 解法一 + +只要理解了题意的话,这道题还是比较容易写的。分两步。 + +首先统计数字和位置都对应相等的个数,只要依次遍历两个数组看是否对应相等即可。 + +```java +int A = 0; +for (int i = 0; i < guess.length(); i++) { + if (secret.charAt(i) == guess.charAt(i)) { + A++; + } +} +``` + +然后我们可以通过两个 `HashMap` 统计相等的数字有多少个。我们只需要分别记录 `secret` 和 `guess` 中每个数字的个数,然后取两者较小的就是相等数字的个数了。 + +比如 `secret` 中有 `3` 个 `2`,`guess` 中有`4` 个 `2`,那么两者相等的数字就至少有 `3` 个。 + +```java +HashMap mapS = new HashMap<>(); +HashMap mapG = new HashMap<>(); +//统计每个数字的个数 +for (int i = 0; i < secret.length(); i++) { + mapS.put(secret.charAt(i), mapS.getOrDefault(secret.charAt(i), 0) + 1); + mapG.put(guess.charAt(i), mapG.getOrDefault(guess.charAt(i), 0) + 1); +} +int B = 0; +//两者取较小 +for (Character key : mapG.keySet()) { + int n1 = mapG.getOrDefault(key, 0); + int n2 = mapS.getOrDefault(key, 0); + B = B + Math.min(n1, n2); +} +``` + +题目中让我们求的是数字相等但位置不同的,上边的 `B` 把位置相同的也包括了,所以最终的结果应该是 `B - A`。 + +上边的两块代码结合起来即可。 + +```java +public String getHint(String secret, String guess) { + int A = 0; + for (int i = 0; i < guess.length(); i++) { + if (secret.charAt(i) == guess.charAt(i)) { + A++; + } + } + HashMap mapS = new HashMap<>(); + HashMap mapG = new HashMap<>(); + for (int i = 0; i < secret.length(); i++) { + mapS.put(secret.charAt(i), mapS.getOrDefault(secret.charAt(i), 0) + 1); + mapG.put(guess.charAt(i), mapG.getOrDefault(guess.charAt(i), 0) + 1); + } + int B = 0; + for (Character key : mapG.keySet()) { + int n1 = mapG.getOrDefault(key, 0); + int n2 = mapS.getOrDefault(key, 0); + B = B + Math.min(n1, n2); + } + return A + "A" + (B - A) + "B"; +} +``` + +分享 [这里](https://leetcode.com/problems/bulls-and-cows/discuss/74629/My-3ms-Java-solution-may-help-u) 的代码,我们还可以做两点优化。 + +第一点,因为 `map` 中的 `key` 只有 `0` 到 `9`,所以我们可以用数组取代 `map` 。 + +第二点,我们可以把第二步用 `map` 统计个数的代码放到第一个 `for` 循环中,当位置不相等的时候再统计个数,这样最后就不用 `B - A`了。 + +```java +public String getHint(String secret, String guess) { + int A = 0; + int[] mapS = new int[10]; + int[] mapG = new int[10]; + for (int i = 0; i < guess.length(); i++) { + if (secret.charAt(i) == guess.charAt(i)) { + A++; + } else { + mapS[secret.charAt(i) - '0']++; + mapG[guess.charAt(i) - '0']++; + } + } + int B = 0; + for (int i = 0; i < 10; i++) { + B += Math.min(mapS[i], mapG[i]); + } + return A + "A" + B + "B"; +} +``` + +# 解法二 + +解法一最后的代码依旧有两个 `for` 循环,分享 [这里](https://leetcode.com/problems/bulls-and-cows/discuss/74621/One-pass-Java-solution) 只有一个循环的代码。 + +比较巧妙,相对于解法一难理解些,可以结合下边的注释理解一下。 + +```java +public String getHint(String secret, String guess) { + int bulls = 0; + int cows = 0; + int[] numbers = new int[10]; + for (int i = 0; i 0) cows++; + //secret 中的数, 计数加 1 + numbers[s] ++; + //guess 中的数, 计数减 1 + numbers[g] --; + } + } + return bulls + "A" + cows + "B"; +} +``` + +理解不了的话,可以举个例子。 + +```java +"231" 和 "321" + +secret 2 3 1 +guess 3 2 1 + ^ + i + +当前遍历到第 1 个数, s = 2, g = 3 +numbers[s]++, 也就是 numbers[2] = 1 +numbers[g]--, 也就是 numbers[3] = -1 + +secret 2 3 2 +guess 3 2 1 + ^ + i + +numbers[2] = 1, numbers[3] = -1 + +当前遍历到第 2 个数, s = 3, g = 2 +此时 numbers[s], 也就是 numbers[3] < 0, cows++ +此时 numbers[g], 也就是 numbers[2] > 0, cows++ +cows = 2 + +继续执行 +numbers[s]++, 也就是 numbers[3] + 1 = -1 + 1 = 0 +numbers[g]--, 也就是 numbers[2] - 1 = 1 - 1 = 0 + +secret 2 3 2 +guess 3 2 1 + ^ + i +numbers[2] = 0, numbers[3] = 0 +当前遍历到第 3 个数, s = 2, g = 1 +此时 s 虽然又遇到了 2, 但是 number[2] = 0 了, 无法再匹配, 所以 cows 不会加 1 了 + +最终 cows 就是 2 了 +``` + + + +# 总 + 算比较简单的一道题,主要是理解题意。解法二的话,遇到 `s` 加 `1`,遇到 `g` 减 `1`,真的很妙。 \ No newline at end of file diff --git a/leetcode-300-Longest-Increasing-Subsequence.md b/leetcode-300-Longest-Increasing-Subsequence.md index 7f39a1881..7501e3e9c 100644 --- a/leetcode-300-Longest-Increasing-Subsequence.md +++ b/leetcode-300-Longest-Increasing-Subsequence.md @@ -1,248 +1,248 @@ -# 题目描述(中等难度) - -300、Longest Increasing Subsequence - -Given an unsorted array of integers, find the length of longest increasing subsequence. - -**Example:** - -``` -Input: [10,9,2,5,3,7,101,18] -Output: 4 -Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4. -``` - -**Note:** - -- There may be more than one LIS combination, it is only necessary for you to return the length. -- Your algorithm should run in O(*n2*) complexity. - -**Follow up:** Could you improve it to O(*n* log *n*) time complexity? - -最长上升子序列的长度。 - -# 解法一 - -比较经典的一道题,之前笔试也遇到过。最直接的方法就是动态规划了。 - -`dp[i]`表示以第 `i` 个数字**为结尾**的最长上升子序列的长度。 - -求 `dp[i]` 的时候,如果前边的某个数 `nums[j] < nums[i]` ,那么我们可以将第 `i` 个数接到第 `j` 个数字的后边作为一个新的上升子序列,此时对应的上升子序列的长度就是 `dp[j] + 1`。 - -可以从下边情况中选择最大的。 - -如果 `nums[0] < nums[i]`,`dp[0] + 1` 就是 `dp[i]` 的一个备选解。 - -如果 `nums[1] < nums[i]`,`dp[1] + 1` 就是 `dp[i]` 的一个备选解。 - -如果 `nums[2] < nums[i]`,`dp[2] + 1` 就是 `dp[i]` 的一个备选解。 - -... - -如果 `nums[i-1] < nums[i]`,`dp[i-1] + 1` 就是 `dp[i]` 的一个备选解。 - -从上边的备选解中选择最大的就是 `dp[i]` 的值。 - -```java -public int lengthOfLIS(int[] nums) { - int n = nums.length; - if (n == 0) { - return 0; - } - int dp[] = new int[n]; - int max = 1; - for (int i = 0; i < n; i++) { - dp[i] = 1; - for (int j = 0; j < i; j++) { - if (nums[j] < nums[i]) { - dp[i] = Math.max(dp[i], dp[j] + 1); - } - } - max = Math.max(max, dp[i]); - } - return max; -} -``` - -时间复杂度:`O(n²)`。 - -空间复杂度:`O(1)`。 - -# 解法二 - -还有一种很巧妙的方法,最开始知道这个方法的时候就觉得很巧妙,但还是把它忘记了,又看了一遍 [这里](https://leetcode.com/problems/longest-increasing-subsequence/discuss/74824/JavaPython-Binary-search-O(nlogn)-time-with-explanation) 才想起来。 - -不同之处在于 `dp` 数组的定义。 - -`dp[i]` 表示长度为 `i + 1` 的所有上升子序列的末尾的最小值。 - -举个例子。 - -```java -nums = [4,5,6,3] -len = 1 : [4], [5], [6], [3] => tails[0] = 3 -长度为 1 的上升子序列有 4 个,末尾最小的值就是 3 - -len = 2 : [4, 5], [5, 6] => tails[1] = 5 -长度为 2 的上升子序列有 2 个,末尾最小的值就是 5 - -len = 3 : [4, 5, 6] => tails[2] = 6 -长度为 3 的上升子序列有 1 个,末尾最小的值就是 6 -``` - -有了上边的定义,我们可以依次考虑每个数字,举个例子。 - -```java -nums = [10,9,2,5,3,7,101,18] - -开始没有数字 -dp = [] - -1---------------------------- -10 9 2 5 3 7 101 18 -^ - -先考虑 10, 只有 1 个数字, 此时长度为 1 的最长上升子序列末尾的值就是 10 -len 1 -dp = [10] - -2---------------------------- -10 9 2 5 3 7 101 18 - ^ -考虑 9, 9 比之前长度为 1 的最长上升子序列末尾的最小值 10 小, 更新长度为 1 的最长上升子序列末尾的值为 9 -len 1 -dp = [9] - -3---------------------------- -10 9 2 5 3 7 101 18 - ^ -考虑 2, 2 比之前长度为 1 的最长上升子序列末尾的最小值 9 小, 更新长度为 1 的最长上升子序列末尾的值为 2 -len 1 -dp = [2] - -4---------------------------- -10 9 2 5 3 7 101 18 - ^ -考虑 5, -5 比之前长度为 1 的最长上升子序列末尾的最小值 2 大, -此时可以扩展长度, 更新长度为 2 的最长上升子序列末尾的值为 5 -len 1 2 -dp = [2 5] - -5---------------------------- -10 9 2 5 3 7 101 18 - ^ -考虑 3, -3 比之前长度为 1 的最长上升子序列末尾的最小值 2 大, 向后考虑 -3 比之前长度为 2 的最长上升子序列末尾的最小值 5 小, 更新长度为 2 的最长上升子序列末尾的值为 3 -len 1 2 -dp = [2 3] - -6---------------------------- -10 9 2 5 3 7 101 18 - ^ -考虑 7, -7 比之前长度为 1 的最长上升子序列末尾的最小值 2 大, 向后考虑 -7 比之前长度为 2 的最长上升子序列末尾的最小值 3 大, 向后考虑 -此时可以扩展长度, 更新长度为 3 的最长上升子序列末尾的值为 7 -len 1 2 3 -dp = [2 3 7] - -7---------------------------- -10 9 2 5 3 7 101 18 - ^ -考虑 101, -101 比之前长度为 1 的最长上升子序列末尾的最小值 2 大, 向后考虑 -101 比之前长度为 2 的最长上升子序列末尾的最小值 3 大, 向后考虑 -101 比之前长度为 3 的最长上升子序列末尾的最小值 7 大, 向后考虑 -此时可以扩展长度, 更新长度为 4 的最长上升子序列末尾的值为 101 -len 1 2 3 4 -dp = [2 3 7 101] - -8---------------------------- -10 9 2 5 3 7 101 18 - ^ -考虑 18, -18 比之前长度为 1 的最长上升子序列末尾的最小值 2 大, 向后考虑 -18 比之前长度为 2 的最长上升子序列末尾的最小值 3 大, 向后考虑 -18 比之前长度为 3 的最长上升子序列末尾的最小值 7 大, 向后考虑 -3 比之前长度为 4 的最长上升子序列末尾的最小值 101 小, 更新长度为 4 的最长上升子序列末尾的值为 18 -len 1 2 3 4 -dp = [2 3 7 18] - -遍历完成,所以数字都考虑了,此时 dp 的长度就是最长上升子序列的长度 -``` - -总结上边的规律,新来一个数字以后,我们去寻找 `dp` 中第一个比它大的值,然后将当前值更新为新来的数字。 - -如果 `dp` 中没有比新来的数字大的数,那么就扩展长度,将新来的值放到最后。 - -写代码的话,因为 `dp` 是一个动态扩容的过程,我们可以用一个 `list` 。但由于比较简单,我们知道 `dp` 最大的长度也就是 `nums` 的长度,我们可以直接用数组,然后自己记录当前数组的长度即可。 - -```java -public int lengthOfLIS(int[] nums) { - int n = nums.length; - if (n == 0) { - return 0; - } - int dp[] = new int[n]; - int len = 0; - for (int i = 0; i < n; i++) { - int j = 0; - // 寻找 dp 中第一个大于等于新来的数的位置 - for (j = 0; j < len; j++) { - if (nums[i] <= dp[j]) { - break; - } - } - // 更新当前值 - dp[j] = nums[i]; - // 是否更新长度 - if (j == len) { - len++; - } - } - return len; -} -``` - -上边花了一大段话讲这个解法,但是上边的时间复杂度依旧是 `O(n²)`,当然不能满足。 - -这个解法巧妙的地方在于,通过上边 `dp` 的定义,`dp` 一定是有序的。我们要从一个有序数组中寻找第一个大于等于新来数的位置,此时就可以通过二分查找了。 - -```java -public int lengthOfLIS(int[] nums) { - int n = nums.length; - if (n == 0) { - return 0; - } - int dp[] = new int[n]; - int len = 0; - for (int i = 0; i < n; i++) { - int start = 0; - int end = len; - while (start < end) { - int mid = (start + end) >>> 1; - if (dp[mid] < nums[i]) { - start = mid + 1; - } else { - end = mid; - } - } - dp[start] = nums[i]; - if (start == len) { - len++; - } - } - return len; -} -``` - -这样的话时间复杂度就是 `O(nlog(n))` 了。 - -# 总 - -解法一比较常规,比较容易想到。 - -解法二的话就很巧妙了,关键就是 `dp` 的定义使得 `dp` 是一个有序数组了。这种也不容易记住,半年前笔试做过这道题,但现在还是忘记了,但还是可以欣赏一下的,哈哈。 - +# 题目描述(中等难度) + +300、Longest Increasing Subsequence + +Given an unsorted array of integers, find the length of longest increasing subsequence. + +**Example:** + +``` +Input: [10,9,2,5,3,7,101,18] +Output: 4 +Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4. +``` + +**Note:** + +- There may be more than one LIS combination, it is only necessary for you to return the length. +- Your algorithm should run in O(*n2*) complexity. + +**Follow up:** Could you improve it to O(*n* log *n*) time complexity? + +最长上升子序列的长度。 + +# 解法一 + +比较经典的一道题,之前笔试也遇到过。最直接的方法就是动态规划了。 + +`dp[i]`表示以第 `i` 个数字**为结尾**的最长上升子序列的长度。 + +求 `dp[i]` 的时候,如果前边的某个数 `nums[j] < nums[i]` ,那么我们可以将第 `i` 个数接到第 `j` 个数字的后边作为一个新的上升子序列,此时对应的上升子序列的长度就是 `dp[j] + 1`。 + +可以从下边情况中选择最大的。 + +如果 `nums[0] < nums[i]`,`dp[0] + 1` 就是 `dp[i]` 的一个备选解。 + +如果 `nums[1] < nums[i]`,`dp[1] + 1` 就是 `dp[i]` 的一个备选解。 + +如果 `nums[2] < nums[i]`,`dp[2] + 1` 就是 `dp[i]` 的一个备选解。 + +... + +如果 `nums[i-1] < nums[i]`,`dp[i-1] + 1` 就是 `dp[i]` 的一个备选解。 + +从上边的备选解中选择最大的就是 `dp[i]` 的值。 + +```java +public int lengthOfLIS(int[] nums) { + int n = nums.length; + if (n == 0) { + return 0; + } + int dp[] = new int[n]; + int max = 1; + for (int i = 0; i < n; i++) { + dp[i] = 1; + for (int j = 0; j < i; j++) { + if (nums[j] < nums[i]) { + dp[i] = Math.max(dp[i], dp[j] + 1); + } + } + max = Math.max(max, dp[i]); + } + return max; +} +``` + +时间复杂度:`O(n²)`。 + +空间复杂度:`O(1)`。 + +# 解法二 + +还有一种很巧妙的方法,最开始知道这个方法的时候就觉得很巧妙,但还是把它忘记了,又看了一遍 [这里](https://leetcode.com/problems/longest-increasing-subsequence/discuss/74824/JavaPython-Binary-search-O(nlogn)-time-with-explanation) 才想起来。 + +不同之处在于 `dp` 数组的定义。 + +`dp[i]` 表示长度为 `i + 1` 的所有上升子序列的末尾的最小值。 + +举个例子。 + +```java +nums = [4,5,6,3] +len = 1 : [4], [5], [6], [3] => tails[0] = 3 +长度为 1 的上升子序列有 4 个,末尾最小的值就是 3 + +len = 2 : [4, 5], [5, 6] => tails[1] = 5 +长度为 2 的上升子序列有 2 个,末尾最小的值就是 5 + +len = 3 : [4, 5, 6] => tails[2] = 6 +长度为 3 的上升子序列有 1 个,末尾最小的值就是 6 +``` + +有了上边的定义,我们可以依次考虑每个数字,举个例子。 + +```java +nums = [10,9,2,5,3,7,101,18] + +开始没有数字 +dp = [] + +1---------------------------- +10 9 2 5 3 7 101 18 +^ + +先考虑 10, 只有 1 个数字, 此时长度为 1 的最长上升子序列末尾的值就是 10 +len 1 +dp = [10] + +2---------------------------- +10 9 2 5 3 7 101 18 + ^ +考虑 9, 9 比之前长度为 1 的最长上升子序列末尾的最小值 10 小, 更新长度为 1 的最长上升子序列末尾的值为 9 +len 1 +dp = [9] + +3---------------------------- +10 9 2 5 3 7 101 18 + ^ +考虑 2, 2 比之前长度为 1 的最长上升子序列末尾的最小值 9 小, 更新长度为 1 的最长上升子序列末尾的值为 2 +len 1 +dp = [2] + +4---------------------------- +10 9 2 5 3 7 101 18 + ^ +考虑 5, +5 比之前长度为 1 的最长上升子序列末尾的最小值 2 大, +此时可以扩展长度, 更新长度为 2 的最长上升子序列末尾的值为 5 +len 1 2 +dp = [2 5] + +5---------------------------- +10 9 2 5 3 7 101 18 + ^ +考虑 3, +3 比之前长度为 1 的最长上升子序列末尾的最小值 2 大, 向后考虑 +3 比之前长度为 2 的最长上升子序列末尾的最小值 5 小, 更新长度为 2 的最长上升子序列末尾的值为 3 +len 1 2 +dp = [2 3] + +6---------------------------- +10 9 2 5 3 7 101 18 + ^ +考虑 7, +7 比之前长度为 1 的最长上升子序列末尾的最小值 2 大, 向后考虑 +7 比之前长度为 2 的最长上升子序列末尾的最小值 3 大, 向后考虑 +此时可以扩展长度, 更新长度为 3 的最长上升子序列末尾的值为 7 +len 1 2 3 +dp = [2 3 7] + +7---------------------------- +10 9 2 5 3 7 101 18 + ^ +考虑 101, +101 比之前长度为 1 的最长上升子序列末尾的最小值 2 大, 向后考虑 +101 比之前长度为 2 的最长上升子序列末尾的最小值 3 大, 向后考虑 +101 比之前长度为 3 的最长上升子序列末尾的最小值 7 大, 向后考虑 +此时可以扩展长度, 更新长度为 4 的最长上升子序列末尾的值为 101 +len 1 2 3 4 +dp = [2 3 7 101] + +8---------------------------- +10 9 2 5 3 7 101 18 + ^ +考虑 18, +18 比之前长度为 1 的最长上升子序列末尾的最小值 2 大, 向后考虑 +18 比之前长度为 2 的最长上升子序列末尾的最小值 3 大, 向后考虑 +18 比之前长度为 3 的最长上升子序列末尾的最小值 7 大, 向后考虑 +3 比之前长度为 4 的最长上升子序列末尾的最小值 101 小, 更新长度为 4 的最长上升子序列末尾的值为 18 +len 1 2 3 4 +dp = [2 3 7 18] + +遍历完成,所以数字都考虑了,此时 dp 的长度就是最长上升子序列的长度 +``` + +总结上边的规律,新来一个数字以后,我们去寻找 `dp` 中第一个比它大的值,然后将当前值更新为新来的数字。 + +如果 `dp` 中没有比新来的数字大的数,那么就扩展长度,将新来的值放到最后。 + +写代码的话,因为 `dp` 是一个动态扩容的过程,我们可以用一个 `list` 。但由于比较简单,我们知道 `dp` 最大的长度也就是 `nums` 的长度,我们可以直接用数组,然后自己记录当前数组的长度即可。 + +```java +public int lengthOfLIS(int[] nums) { + int n = nums.length; + if (n == 0) { + return 0; + } + int dp[] = new int[n]; + int len = 0; + for (int i = 0; i < n; i++) { + int j = 0; + // 寻找 dp 中第一个大于等于新来的数的位置 + for (j = 0; j < len; j++) { + if (nums[i] <= dp[j]) { + break; + } + } + // 更新当前值 + dp[j] = nums[i]; + // 是否更新长度 + if (j == len) { + len++; + } + } + return len; +} +``` + +上边花了一大段话讲这个解法,但是上边的时间复杂度依旧是 `O(n²)`,当然不能满足。 + +这个解法巧妙的地方在于,通过上边 `dp` 的定义,`dp` 一定是有序的。我们要从一个有序数组中寻找第一个大于等于新来数的位置,此时就可以通过二分查找了。 + +```java +public int lengthOfLIS(int[] nums) { + int n = nums.length; + if (n == 0) { + return 0; + } + int dp[] = new int[n]; + int len = 0; + for (int i = 0; i < n; i++) { + int start = 0; + int end = len; + while (start < end) { + int mid = (start + end) >>> 1; + if (dp[mid] < nums[i]) { + start = mid + 1; + } else { + end = mid; + } + } + dp[start] = nums[i]; + if (start == len) { + len++; + } + } + return len; +} +``` + +这样的话时间复杂度就是 `O(nlog(n))` 了。 + +# 总 + +解法一比较常规,比较容易想到。 + +解法二的话就很巧妙了,关键就是 `dp` 的定义使得 `dp` 是一个有序数组了。这种也不容易记住,半年前笔试做过这道题,但现在还是忘记了,但还是可以欣赏一下的,哈哈。 + diff --git a/leetcode-301-Remove-Invalid-Parentheses.md b/leetcode-301-Remove-Invalid-Parentheses.md index a075efc95..3f1cb087b 100644 --- a/leetcode-301-Remove-Invalid-Parentheses.md +++ b/leetcode-301-Remove-Invalid-Parentheses.md @@ -1,662 +1,662 @@ -# 题目描述(困难难度) - -301、Remove Invalid Parentheses - -Remove the minimum number of invalid parentheses in order to make the input string valid. Return all possible results. - -**Note:** The input string may contain letters other than the parentheses `(` and `)`. - -**Example 1:** - -``` -Input: "()())()" -Output: ["()()()", "(())()"] -``` - -**Example 2:** - -``` -Input: "(a)())()" -Output: ["(a)()()", "(a())()"] -``` - -**Example 3:** - -``` -Input: ")(" -Output: [""] -``` - -移除最少的括号,使得整个字符串合法,也就是左右括号匹配,返回所有可能的结果。 - -# 解法一 回溯法/DFS/暴力 - -需要解决几个关键点。 - -第一,怎么判断括号是否匹配? - -[20 题](https://leetcode.wang/leetCode-20-Valid Parentheses.html) 的时候做过括号匹配的问题,除了使用栈,我们也可以用一个计数器 `count`,遇到左括号进行加 `1` ,遇到右括号进行减 `1`,如果最后计数器是 `0`,说明括号是匹配的。代码的话可以参考下边。 - -```javascript -function isVaild(s) { - let count = 0; - for (let i = 0; i < s.length; i++) { - if (s[i] === '(') { - count++; - } else if (s[i] === ')') { - count--; - } - if (count < 0) { - return false; - } - } - return count === 0; -} -``` - -第二,如果用暴力的方法,怎么列举所有情况? - -要列举所有的情况,每个括号无非是两种状态,在或者不在,字母的话就只有「在」一种情况。我们可以通过回溯法或者说是 `DFS` 。可以参考下边的图。 - -对于 `(a)())`, 如下图,蓝色表示在,橙色表示不在,下边是第一个字符在的情况。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/301_2.jpg) - -下边是第一个字符不在的情况。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/301_3.jpg) - -我们要做的就是从第一个字符开始,通过深度优先遍历的顺序,遍历到最后一个字符后判断当前路径上的字符串是否合法。 - -对于代码的话,我们可以一边遍历,一边记录当前 `count` 的情况,也就是左右括号的情况。到最后一个字符后,只需要判断 `count` 是否为 `0` 即可。 - -第三,怎么保证删掉最少的括号? - -这个方法很多,说一下我的。假设我们用 `res` 数组保存最终的结果,当新的字符串要加入的时候,我们判断一下新加入的字符串的长度和数组中第一个元素长度的关系。 - -如果新加入的字符串的长度大于数组中第一个元素的长度,我们就清空数组,然后再将新字符串加入。 - -如果新加入的字符串的长度小于数组中第一个元素的长度,那么当前字符串抛弃掉。 - -如果新加入的字符串的长度等于数组中第一个元素的长度,将新字符串加入到 `res` 中。 - -第四,重复的情况怎么办? - -简单粗暴一些,最后通过 `set` 去重即可。 - -上边四个问题解决后,就可以写代码了。 - -```javascript -/** - * @param {string} s - * @return {string[]} - */ -var removeInvalidParentheses = function (s) { - let res = ['']; - removeInvalidParenthesesHelper(s, 0, s.length, 0, '', res); - //去重 - return [...new Set(res)]; -}; - -/** - * - * @param {string 原字符串} s - * @param {number 当前考虑的字符下标} start - * @param {number s 的长度} end - * @param {number 记录左括号和右括号的情况} count - * @param {string 遍历的路径字符串} temp - * @param {string[] 保存最终的结果} res - */ -function removeInvalidParenthesesHelper(s, start, end, count, temp, res) { - //当前右括号多了, 后边无论是什么都不可能是合法字符串了, 直接结束 - if (count < 0) { - return; - } - //到达结尾 - if (start === end) { - if (count === 0) { - let max = res[0].length; - if (temp.length > max) { - //清空之前的 - res.length = 0; - //将当前的加入 - res.push(temp); - } else if (temp.length === max) { - res.push(temp); - } - } - return; - } - //添加当前字符 - if (s[start] === '(') { - removeInvalidParenthesesHelper( - s, - start + 1, - end, - count + 1, - temp + '(', - res - ); - } else if (s[start] === ')') { - removeInvalidParenthesesHelper( - s, - start + 1, - end, - count - 1, - temp + ')', - res - ); - } else { - removeInvalidParenthesesHelper( - s, - start + 1, - end, - count, - temp + s.charAt(start), - res - ); - } - - //不添加当前字符 - if (s[start] === '(' || s[start] === ')') { - removeInvalidParenthesesHelper(s, start + 1, end, count, temp, res); - } -} -``` - -上边的代码,剪枝的话只有 - -```java -if (count < 0) { - return; -} -``` - -我们可以通过记录更多的信息,来让更多的情况提前结束,参考 [这里](https://leetcode.com/problems/remove-invalid-parentheses/discuss/75038/Evolve-from-intuitive-solution-to-optimal-a-review-of-all-solutions)。 - -因为我们考虑的是删除最少的括号数,我们可以在深度优先遍历之前记录需要删除的左括号的个数和右括号的个数,遍历过程中如果删除的超过了需要删除的括号个数,就可以直接结束。 - -可以参考下边的代码,大部分代码没有改变,函数添加了两个参数来记录需要删除的左括号的个数和右括号的个数。 - -```javascript -/** - * @param {string} s - * @return {string[]} - */ -var removeInvalidParentheses = function (s) { - let res = []; - let rmLeft = 0; //需要删除的左括号个数 - let rmRight = 0; //需要删除的右括号个数 - for (let i = 0; i < s.length; i++) { - if (s[i] === '(') { - rmLeft++; - } else if (s[i] === ')') { - if (rmLeft > 0) { - rmLeft--; - } else { - rmRight++; - } - } - } - removeInvalidParenthesesHelper(s, 0, s.length, 0, '', res, rmLeft, rmRight); - //去重 - return [...new Set(res)]; -}; - -/** - * - * @param {string 原字符串} s - * @param {number 当前考虑的字符} start - * @param {number s 的长度} end - * @param {number 记录左括号和右括号的情况} count - * @param {string 遍历的路径字符串} temp - * @param {string[] 保存最终的结果} res - * @param {number 当前需要删除的左括号数量} rmLeft - * @param {number 当前需要删除的右括号数量} rmRight - */ -function removeInvalidParenthesesHelper( - s, - start, - end, - count, - temp, - res, - rmLeft, - rmRight -) { - if (count < 0 || rmLeft < 0 || rmRight < 0) { - return; - } - //到达结尾 - if (start === end) { - if (count === 0 && rmLeft === 0 && rmRight === 0) { - res.push(temp); - } - return; - } - //添加当前字符 - if (s[start] === '(') { - removeInvalidParenthesesHelper( - s, - start + 1, - end, - count + 1, - temp + '(', - res, - rmLeft, - rmRight - ); - } else if (s[start] === ')') { - removeInvalidParenthesesHelper( - s, - start + 1, - end, - count - 1, - temp + ')', - res, - rmLeft, - rmRight - ); - } else { - removeInvalidParenthesesHelper( - s, - start + 1, - end, - count, - temp + s.charAt(start), - res, - rmLeft, - rmRight - ); - } - - //删除当前字符, 更新 rmLeft 或者 rmRight - if (s[start] === '(') { - removeInvalidParenthesesHelper( - s, - start + 1, - end, - count, - temp, - res, - rmLeft - 1, - rmRight - ); - } else if (s[start] === ')') { - removeInvalidParenthesesHelper( - s, - start + 1, - end, - count, - temp, - res, - rmLeft, - rmRight - 1 - ); - } -} -``` - -# 解法二 BFS - -参考 [这里](https://leetcode.com/problems/remove-invalid-parentheses/discuss/75032/Share-my-Java-BFS-solution)。 - -解法一通过的是 `DFS`,我们也可以通过广度优先遍历。 - -思想很简单,先判断整个字符串是否合法, 如果合法的话就将其加入到结果中。否则的话,进行下一步。 - -只删掉 `1` 个括号,考虑所有的删除情况,然后判断剩下的字符串是否合法,如果合法的话就将其加入到结果中。否则的话,进行下一步。 - -只删掉 `2` 个括号,考虑所有的删除情况,然后判断剩下的字符串是否合法,如果合法的话就将其加入到结果中。否则的话,进行下一步。 - -只删掉 `3` 个括号,考虑所有的删除情况,然后判断剩下的字符串是否合法,如果合法的话就将其加入到结果中。否则的话,进行下一步。 - -... - -因为我们考虑删除最少的括号数,如果上边某一步出现了合法情况,后边的步骤就不用进行了。 - -同样要解决重复的问题,除了解法一在最后返回前用 `set` 去重。这里我们也可以在过程中使用一个 `set` ,在加入队列之前判断一下是否重复。 - -```javascript -/** - * @param {string} s - * @return {string[]} - */ -var removeInvalidParentheses = function (s) { - let res = []; - let queue = []; - let visited = new Set(); - - queue.push(s); - - while (true) { - let size = queue.length; - //考虑当前层 - for (let i = 0; i < size; i++) { - s = queue.shift(); - if (isVaild(s)) { - res.push(s); - } else if (res.length == 0) { - //生成下一层, 原来的基础上再多删除一个括号 - for (let removei = 0; removei < s.length; removei++) { - if (s[removei] == '(' || s[removei] === ')') { - let nexts = s.substring(0, removei) + s.substring(removei + 1); - //防止重复 - if (!visited.has(nexts)) { - queue.push(nexts); - visited.add(nexts); - } - } - } - } - } - //出现了合法字符串,终止循环 - if (res.length > 0) { - break; - } - } - return res; -}; - -function isVaild(s) { - let count = 0; - for (let i = 0; i < s.length; i++) { - if (s[i] === '(') { - count++; - } else if (s[i] === ')') { - count--; - } - if (count < 0) { - return false; - } - } - return count === 0; -} -``` - -上边使用了 `set` 来防止重复,下边考虑不用 `set` 怎么防止重复,参考 [这里](https://leetcode.com/problems/remove-invalid-parentheses/discuss/75038/Evolve-from-intuitive-solution-to-optimal-a-review-of-all-solutions)。 - -之所以产生重复,有两种情况。 - -第一种情况是有连续括号的时候,比如 `(()`,删除第一个括号和第二个括号都会产生 `()`。 - -解决方案的话,当出现连续括号的时候我们只删连续括号中的第一个。 - -第二种情况,当删除第 `i` 个括号后,假设后边删除了第 `j` 个括号。 - -也有可能开始的时候删了第 `j` 个括号,后边又去删了第 `i` 个括号。 - -举个例子 `(()(()`,删除路径可能是下边的两种情况。 - -`(()(()` -> `()(()` -> `()()` - -`(()(()` -> `(()()` -> `()()` - -两种删除路径出现了重复的情况,解法方案的话,我们可以规定删除括号的顺序。 - -我们记录一下删除括号的位置,第二次删除括号的位置必须是第一次删除括号的后边。 - -代码的话,只需要在加入队列中的元素中新增一个删除的位置。 - -```javascript -/** - * @param {string} s - * @return {string[]} - */ -var removeInvalidParentheses = function (s) { - let res = []; - let queue = []; - - queue.push([s, 0]); - - while (queue.length > 0) { - s = queue.shift(); - if (isVaild(s[0])) { - res.push(s[0]); - } else if (res.length == 0) { - let removei = s[1]; - s = s[0]; - for (; removei < s.length; removei++) { - if ( - //保证是连续括号的第一个 - (s[removei] == '(' || s[removei] === ')') && - (removei === 0 || s[removei - 1] != s[removei]) - ) { - let nexts = s.substring(0, removei) + s.substring(removei + 1); - //此时删除位置的下标 removei 就是下次删除位置的开始 - queue.push([nexts, removei]); - } - } - } - } - return res; -}; - -function isVaild(s) { - let count = 0; - for (let i = 0; i < s.length; i++) { - if (s[i] === '(') { - count++; - } else if (s[i] === ')') { - count--; - } - if (count < 0) { - return false; - } - } - return count === 0; -} -``` - -# 解法三 - -参考 [这里](https://leetcode.com/problems/remove-invalid-parentheses/discuss/75027/Easy-Short-Concise-and-Fast-Java-DFS-3-ms-solution),这个解法不属于通用的解法,更像是对问题的分析,和 `DFS` 和 `BFS` 都有一些像。 - -我们可以从头开始遍历,用 `count` 记录括号的情况,遇到左括号加 `1` ,遇到右括号减 `1`。如果 `count` 小于 `0` 了,说明此时右括号多了,此时我们可以从前边删除一个右括号。 - -删除以后,剩下的字符串再进入递归按照上边的方式继续考虑。 - -为了避免重复,删除的时候我们需要向解法二一样,当出现连续括号的时候我们只删第一个,第二次删除括号的位置必须是第一次删除括号的后边。 - -举个例子。 - -```java -s = ()())()) - -( ) ( ) ) ( ) ) - ^ - -此时右括号多了, 从前边找右括号去删除, 两种情况 - -1. -( ( ) ) ( ) ) - ^ -然后上边的字符串继续遍历去删除 - -2. -( ) ( ) ( ) ) - ^ -然后上边的字符串继续遍历去删除 -``` - -基于上边的想法,我们可以写出下边的代码,需要两个参数记录遍历的位置和删除的位置。 - -```javascript -var removeInvalidParentheses = function (s) { - let res = []; - removeInvalidParenthesesHelper(s, 0, 0, s.length, res); - return res; -}; - -function removeInvalidParenthesesHelper(s, istart, jstart, end, res) { - let count = 0; - for (let i = istart; i < end; i++) { - if (s[i] === '(') { - count++; - } else if (s[i] === ')') { - count--; - } - if (count < 0) { - //考虑前边所有可以删除的情况 - for (let j = jstart; j <= i; j++) { - if (s[j] === ')' && (j === 0 || s[j - 1] !== ')')) { - removeInvalidParenthesesHelper( - s.substring(0, j) + s.substring(j + 1), - i, - j, - end, - res - ); - } - } - //这里很重要, 考虑完所有删除的情况后结束即可 - return; - } - } - res.push(s); -} -``` - -但上边我们只考虑了右括号多的时候,左括号多的时候并没有考虑。 - -很简单,我们只需要倒着遍历,然后按照上边的解法继续判断一次即可。变成遇到右括号加 `1` ,遇到左括号减 `1`。如果 `count` 小于 `0` 了,说明此时左括号多了,此时我们可以从前边删除一个左括号。 - -为了更少的改动上边的代码,我们可以把字符串倒转一下作为参数。 - -```java -/** - * @param {string} s - * @return {string[]} - */ -var removeInvalidParentheses = function (s) { - let res = []; - removeInvalidParenthesesHelper(s, 0, 0, s.length, res); - return res; -}; - -function removeInvalidParenthesesHelper(s, istart, jstart, end, res) { - let count = 0; - for (let i = istart; i < end; i++) { - if (s[i] === '(') { - count++; - } else if (s[i] === ')') { - count--; - } - if (count < 0) { - for (let j = jstart; j <= i; j++) { - if (s[j] === ')' && (j === 0 || s[j - 1] !== ')')) { - removeInvalidParenthesesHelper( - s.substring(0, j) + s.substring(j + 1), - i, - j, - end, - res - ); - } - } - return; - } - } - s = s.split('').reverse().join(''); - //考虑删除左括号 - removeInvalidParenthesesHelper2(s, 0, 0, s.length, res); -} -function removeInvalidParenthesesHelper2(s, istart, jstart, end, res) { - let count = 0; - for (let i = istart; i < end; i++) { - if (s[i] === ')') { - count++; - } else if (s[i] === '(') { - count--; - } - if (count < 0) { - for (let j = jstart; j <= i; j++) { - if (s[j] === '(' && (j === 0 || s[j - 1] !== '(')) { - removeInvalidParenthesesHelper2( - s.substring(0, j) + s.substring(j + 1), - i, - j, - end, - res - ); - } - } - return; - } - } - //此时结果是倒着的, 逆转回来 - s = s.split('').reverse().join(''); - //加入到结果中 - res.push(s); -} -``` - -当然两个辅助函数非常像,只是判断左右括号那里不一样,完全可以合并起来,入口函数再添加两个参数即可。 - -```java -/** - * @param {string} s - * @return {string[]} - */ -var removeInvalidParentheses = function (s) { - let res = []; - removeInvalidParenthesesHelper(s, 0, 0, s.length, res, '(', ')'); - return res; -}; - -function removeInvalidParenthesesHelper( - s, - istart, - jstart, - end, - res, - left, - right -) { - let count = 0; - for (let i = istart; i < end; i++) { - if (s[i] === left) { - count++; - } else if (s[i] === right) { - count--; - } - if (count < 0) { - for (let j = jstart; j <= i; j++) { - if (s[j] === right && (j === 0 || s[j - 1] !== right)) { - removeInvalidParenthesesHelper( - s.substring(0, j) + s.substring(j + 1), - i, - j, - end, - res, - left, - right - ); - } - } - return; - } - } - s = s.split('').reverse().join(''); - //此时多余的右括号去除结束, 还要去除多余的左括号 - if (left === '(') { - removeInvalidParenthesesHelper(s, 0, 0, s.length, res, ')', '('); - //此时多余的左右括号都去除结束, 添加当前结果 - } else { - res.push(s); - } -} -``` - -# 总 - -解法一的回溯法虽然有些暴力,但确实是有效的,写完以后可以考虑一些剪枝,优化速度。 - -解法二的话,`BFS` 往往和 `DFS` 共存,能 `DFS` 一般就可以通过 `BFS` 去解决。 - -解法三的话,就是直接从问题入手,哪个括号多了就去删哪个,多余的删除完后,就是一个合法字符串了。如果先看到的解法三,可能不是很好理解。 - - - - - +# 题目描述(困难难度) + +301、Remove Invalid Parentheses + +Remove the minimum number of invalid parentheses in order to make the input string valid. Return all possible results. + +**Note:** The input string may contain letters other than the parentheses `(` and `)`. + +**Example 1:** + +``` +Input: "()())()" +Output: ["()()()", "(())()"] +``` + +**Example 2:** + +``` +Input: "(a)())()" +Output: ["(a)()()", "(a())()"] +``` + +**Example 3:** + +``` +Input: ")(" +Output: [""] +``` + +移除最少的括号,使得整个字符串合法,也就是左右括号匹配,返回所有可能的结果。 + +# 解法一 回溯法/DFS/暴力 + +需要解决几个关键点。 + +第一,怎么判断括号是否匹配? + +[20 题](https://leetcode.wang/leetCode-20-Valid Parentheses.html) 的时候做过括号匹配的问题,除了使用栈,我们也可以用一个计数器 `count`,遇到左括号进行加 `1` ,遇到右括号进行减 `1`,如果最后计数器是 `0`,说明括号是匹配的。代码的话可以参考下边。 + +```javascript +function isVaild(s) { + let count = 0; + for (let i = 0; i < s.length; i++) { + if (s[i] === '(') { + count++; + } else if (s[i] === ')') { + count--; + } + if (count < 0) { + return false; + } + } + return count === 0; +} +``` + +第二,如果用暴力的方法,怎么列举所有情况? + +要列举所有的情况,每个括号无非是两种状态,在或者不在,字母的话就只有「在」一种情况。我们可以通过回溯法或者说是 `DFS` 。可以参考下边的图。 + +对于 `(a)())`, 如下图,蓝色表示在,橙色表示不在,下边是第一个字符在的情况。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/301_2.jpg) + +下边是第一个字符不在的情况。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/301_3.jpg) + +我们要做的就是从第一个字符开始,通过深度优先遍历的顺序,遍历到最后一个字符后判断当前路径上的字符串是否合法。 + +对于代码的话,我们可以一边遍历,一边记录当前 `count` 的情况,也就是左右括号的情况。到最后一个字符后,只需要判断 `count` 是否为 `0` 即可。 + +第三,怎么保证删掉最少的括号? + +这个方法很多,说一下我的。假设我们用 `res` 数组保存最终的结果,当新的字符串要加入的时候,我们判断一下新加入的字符串的长度和数组中第一个元素长度的关系。 + +如果新加入的字符串的长度大于数组中第一个元素的长度,我们就清空数组,然后再将新字符串加入。 + +如果新加入的字符串的长度小于数组中第一个元素的长度,那么当前字符串抛弃掉。 + +如果新加入的字符串的长度等于数组中第一个元素的长度,将新字符串加入到 `res` 中。 + +第四,重复的情况怎么办? + +简单粗暴一些,最后通过 `set` 去重即可。 + +上边四个问题解决后,就可以写代码了。 + +```javascript +/** + * @param {string} s + * @return {string[]} + */ +var removeInvalidParentheses = function (s) { + let res = ['']; + removeInvalidParenthesesHelper(s, 0, s.length, 0, '', res); + //去重 + return [...new Set(res)]; +}; + +/** + * + * @param {string 原字符串} s + * @param {number 当前考虑的字符下标} start + * @param {number s 的长度} end + * @param {number 记录左括号和右括号的情况} count + * @param {string 遍历的路径字符串} temp + * @param {string[] 保存最终的结果} res + */ +function removeInvalidParenthesesHelper(s, start, end, count, temp, res) { + //当前右括号多了, 后边无论是什么都不可能是合法字符串了, 直接结束 + if (count < 0) { + return; + } + //到达结尾 + if (start === end) { + if (count === 0) { + let max = res[0].length; + if (temp.length > max) { + //清空之前的 + res.length = 0; + //将当前的加入 + res.push(temp); + } else if (temp.length === max) { + res.push(temp); + } + } + return; + } + //添加当前字符 + if (s[start] === '(') { + removeInvalidParenthesesHelper( + s, + start + 1, + end, + count + 1, + temp + '(', + res + ); + } else if (s[start] === ')') { + removeInvalidParenthesesHelper( + s, + start + 1, + end, + count - 1, + temp + ')', + res + ); + } else { + removeInvalidParenthesesHelper( + s, + start + 1, + end, + count, + temp + s.charAt(start), + res + ); + } + + //不添加当前字符 + if (s[start] === '(' || s[start] === ')') { + removeInvalidParenthesesHelper(s, start + 1, end, count, temp, res); + } +} +``` + +上边的代码,剪枝的话只有 + +```java +if (count < 0) { + return; +} +``` + +我们可以通过记录更多的信息,来让更多的情况提前结束,参考 [这里](https://leetcode.com/problems/remove-invalid-parentheses/discuss/75038/Evolve-from-intuitive-solution-to-optimal-a-review-of-all-solutions)。 + +因为我们考虑的是删除最少的括号数,我们可以在深度优先遍历之前记录需要删除的左括号的个数和右括号的个数,遍历过程中如果删除的超过了需要删除的括号个数,就可以直接结束。 + +可以参考下边的代码,大部分代码没有改变,函数添加了两个参数来记录需要删除的左括号的个数和右括号的个数。 + +```javascript +/** + * @param {string} s + * @return {string[]} + */ +var removeInvalidParentheses = function (s) { + let res = []; + let rmLeft = 0; //需要删除的左括号个数 + let rmRight = 0; //需要删除的右括号个数 + for (let i = 0; i < s.length; i++) { + if (s[i] === '(') { + rmLeft++; + } else if (s[i] === ')') { + if (rmLeft > 0) { + rmLeft--; + } else { + rmRight++; + } + } + } + removeInvalidParenthesesHelper(s, 0, s.length, 0, '', res, rmLeft, rmRight); + //去重 + return [...new Set(res)]; +}; + +/** + * + * @param {string 原字符串} s + * @param {number 当前考虑的字符} start + * @param {number s 的长度} end + * @param {number 记录左括号和右括号的情况} count + * @param {string 遍历的路径字符串} temp + * @param {string[] 保存最终的结果} res + * @param {number 当前需要删除的左括号数量} rmLeft + * @param {number 当前需要删除的右括号数量} rmRight + */ +function removeInvalidParenthesesHelper( + s, + start, + end, + count, + temp, + res, + rmLeft, + rmRight +) { + if (count < 0 || rmLeft < 0 || rmRight < 0) { + return; + } + //到达结尾 + if (start === end) { + if (count === 0 && rmLeft === 0 && rmRight === 0) { + res.push(temp); + } + return; + } + //添加当前字符 + if (s[start] === '(') { + removeInvalidParenthesesHelper( + s, + start + 1, + end, + count + 1, + temp + '(', + res, + rmLeft, + rmRight + ); + } else if (s[start] === ')') { + removeInvalidParenthesesHelper( + s, + start + 1, + end, + count - 1, + temp + ')', + res, + rmLeft, + rmRight + ); + } else { + removeInvalidParenthesesHelper( + s, + start + 1, + end, + count, + temp + s.charAt(start), + res, + rmLeft, + rmRight + ); + } + + //删除当前字符, 更新 rmLeft 或者 rmRight + if (s[start] === '(') { + removeInvalidParenthesesHelper( + s, + start + 1, + end, + count, + temp, + res, + rmLeft - 1, + rmRight + ); + } else if (s[start] === ')') { + removeInvalidParenthesesHelper( + s, + start + 1, + end, + count, + temp, + res, + rmLeft, + rmRight - 1 + ); + } +} +``` + +# 解法二 BFS + +参考 [这里](https://leetcode.com/problems/remove-invalid-parentheses/discuss/75032/Share-my-Java-BFS-solution)。 + +解法一通过的是 `DFS`,我们也可以通过广度优先遍历。 + +思想很简单,先判断整个字符串是否合法, 如果合法的话就将其加入到结果中。否则的话,进行下一步。 + +只删掉 `1` 个括号,考虑所有的删除情况,然后判断剩下的字符串是否合法,如果合法的话就将其加入到结果中。否则的话,进行下一步。 + +只删掉 `2` 个括号,考虑所有的删除情况,然后判断剩下的字符串是否合法,如果合法的话就将其加入到结果中。否则的话,进行下一步。 + +只删掉 `3` 个括号,考虑所有的删除情况,然后判断剩下的字符串是否合法,如果合法的话就将其加入到结果中。否则的话,进行下一步。 + +... + +因为我们考虑删除最少的括号数,如果上边某一步出现了合法情况,后边的步骤就不用进行了。 + +同样要解决重复的问题,除了解法一在最后返回前用 `set` 去重。这里我们也可以在过程中使用一个 `set` ,在加入队列之前判断一下是否重复。 + +```javascript +/** + * @param {string} s + * @return {string[]} + */ +var removeInvalidParentheses = function (s) { + let res = []; + let queue = []; + let visited = new Set(); + + queue.push(s); + + while (true) { + let size = queue.length; + //考虑当前层 + for (let i = 0; i < size; i++) { + s = queue.shift(); + if (isVaild(s)) { + res.push(s); + } else if (res.length == 0) { + //生成下一层, 原来的基础上再多删除一个括号 + for (let removei = 0; removei < s.length; removei++) { + if (s[removei] == '(' || s[removei] === ')') { + let nexts = s.substring(0, removei) + s.substring(removei + 1); + //防止重复 + if (!visited.has(nexts)) { + queue.push(nexts); + visited.add(nexts); + } + } + } + } + } + //出现了合法字符串,终止循环 + if (res.length > 0) { + break; + } + } + return res; +}; + +function isVaild(s) { + let count = 0; + for (let i = 0; i < s.length; i++) { + if (s[i] === '(') { + count++; + } else if (s[i] === ')') { + count--; + } + if (count < 0) { + return false; + } + } + return count === 0; +} +``` + +上边使用了 `set` 来防止重复,下边考虑不用 `set` 怎么防止重复,参考 [这里](https://leetcode.com/problems/remove-invalid-parentheses/discuss/75038/Evolve-from-intuitive-solution-to-optimal-a-review-of-all-solutions)。 + +之所以产生重复,有两种情况。 + +第一种情况是有连续括号的时候,比如 `(()`,删除第一个括号和第二个括号都会产生 `()`。 + +解决方案的话,当出现连续括号的时候我们只删连续括号中的第一个。 + +第二种情况,当删除第 `i` 个括号后,假设后边删除了第 `j` 个括号。 + +也有可能开始的时候删了第 `j` 个括号,后边又去删了第 `i` 个括号。 + +举个例子 `(()(()`,删除路径可能是下边的两种情况。 + +`(()(()` -> `()(()` -> `()()` + +`(()(()` -> `(()()` -> `()()` + +两种删除路径出现了重复的情况,解法方案的话,我们可以规定删除括号的顺序。 + +我们记录一下删除括号的位置,第二次删除括号的位置必须是第一次删除括号的后边。 + +代码的话,只需要在加入队列中的元素中新增一个删除的位置。 + +```javascript +/** + * @param {string} s + * @return {string[]} + */ +var removeInvalidParentheses = function (s) { + let res = []; + let queue = []; + + queue.push([s, 0]); + + while (queue.length > 0) { + s = queue.shift(); + if (isVaild(s[0])) { + res.push(s[0]); + } else if (res.length == 0) { + let removei = s[1]; + s = s[0]; + for (; removei < s.length; removei++) { + if ( + //保证是连续括号的第一个 + (s[removei] == '(' || s[removei] === ')') && + (removei === 0 || s[removei - 1] != s[removei]) + ) { + let nexts = s.substring(0, removei) + s.substring(removei + 1); + //此时删除位置的下标 removei 就是下次删除位置的开始 + queue.push([nexts, removei]); + } + } + } + } + return res; +}; + +function isVaild(s) { + let count = 0; + for (let i = 0; i < s.length; i++) { + if (s[i] === '(') { + count++; + } else if (s[i] === ')') { + count--; + } + if (count < 0) { + return false; + } + } + return count === 0; +} +``` + +# 解法三 + +参考 [这里](https://leetcode.com/problems/remove-invalid-parentheses/discuss/75027/Easy-Short-Concise-and-Fast-Java-DFS-3-ms-solution),这个解法不属于通用的解法,更像是对问题的分析,和 `DFS` 和 `BFS` 都有一些像。 + +我们可以从头开始遍历,用 `count` 记录括号的情况,遇到左括号加 `1` ,遇到右括号减 `1`。如果 `count` 小于 `0` 了,说明此时右括号多了,此时我们可以从前边删除一个右括号。 + +删除以后,剩下的字符串再进入递归按照上边的方式继续考虑。 + +为了避免重复,删除的时候我们需要向解法二一样,当出现连续括号的时候我们只删第一个,第二次删除括号的位置必须是第一次删除括号的后边。 + +举个例子。 + +```java +s = ()())()) + +( ) ( ) ) ( ) ) + ^ + +此时右括号多了, 从前边找右括号去删除, 两种情况 + +1. +( ( ) ) ( ) ) + ^ +然后上边的字符串继续遍历去删除 + +2. +( ) ( ) ( ) ) + ^ +然后上边的字符串继续遍历去删除 +``` + +基于上边的想法,我们可以写出下边的代码,需要两个参数记录遍历的位置和删除的位置。 + +```javascript +var removeInvalidParentheses = function (s) { + let res = []; + removeInvalidParenthesesHelper(s, 0, 0, s.length, res); + return res; +}; + +function removeInvalidParenthesesHelper(s, istart, jstart, end, res) { + let count = 0; + for (let i = istart; i < end; i++) { + if (s[i] === '(') { + count++; + } else if (s[i] === ')') { + count--; + } + if (count < 0) { + //考虑前边所有可以删除的情况 + for (let j = jstart; j <= i; j++) { + if (s[j] === ')' && (j === 0 || s[j - 1] !== ')')) { + removeInvalidParenthesesHelper( + s.substring(0, j) + s.substring(j + 1), + i, + j, + end, + res + ); + } + } + //这里很重要, 考虑完所有删除的情况后结束即可 + return; + } + } + res.push(s); +} +``` + +但上边我们只考虑了右括号多的时候,左括号多的时候并没有考虑。 + +很简单,我们只需要倒着遍历,然后按照上边的解法继续判断一次即可。变成遇到右括号加 `1` ,遇到左括号减 `1`。如果 `count` 小于 `0` 了,说明此时左括号多了,此时我们可以从前边删除一个左括号。 + +为了更少的改动上边的代码,我们可以把字符串倒转一下作为参数。 + +```java +/** + * @param {string} s + * @return {string[]} + */ +var removeInvalidParentheses = function (s) { + let res = []; + removeInvalidParenthesesHelper(s, 0, 0, s.length, res); + return res; +}; + +function removeInvalidParenthesesHelper(s, istart, jstart, end, res) { + let count = 0; + for (let i = istart; i < end; i++) { + if (s[i] === '(') { + count++; + } else if (s[i] === ')') { + count--; + } + if (count < 0) { + for (let j = jstart; j <= i; j++) { + if (s[j] === ')' && (j === 0 || s[j - 1] !== ')')) { + removeInvalidParenthesesHelper( + s.substring(0, j) + s.substring(j + 1), + i, + j, + end, + res + ); + } + } + return; + } + } + s = s.split('').reverse().join(''); + //考虑删除左括号 + removeInvalidParenthesesHelper2(s, 0, 0, s.length, res); +} +function removeInvalidParenthesesHelper2(s, istart, jstart, end, res) { + let count = 0; + for (let i = istart; i < end; i++) { + if (s[i] === ')') { + count++; + } else if (s[i] === '(') { + count--; + } + if (count < 0) { + for (let j = jstart; j <= i; j++) { + if (s[j] === '(' && (j === 0 || s[j - 1] !== '(')) { + removeInvalidParenthesesHelper2( + s.substring(0, j) + s.substring(j + 1), + i, + j, + end, + res + ); + } + } + return; + } + } + //此时结果是倒着的, 逆转回来 + s = s.split('').reverse().join(''); + //加入到结果中 + res.push(s); +} +``` + +当然两个辅助函数非常像,只是判断左右括号那里不一样,完全可以合并起来,入口函数再添加两个参数即可。 + +```java +/** + * @param {string} s + * @return {string[]} + */ +var removeInvalidParentheses = function (s) { + let res = []; + removeInvalidParenthesesHelper(s, 0, 0, s.length, res, '(', ')'); + return res; +}; + +function removeInvalidParenthesesHelper( + s, + istart, + jstart, + end, + res, + left, + right +) { + let count = 0; + for (let i = istart; i < end; i++) { + if (s[i] === left) { + count++; + } else if (s[i] === right) { + count--; + } + if (count < 0) { + for (let j = jstart; j <= i; j++) { + if (s[j] === right && (j === 0 || s[j - 1] !== right)) { + removeInvalidParenthesesHelper( + s.substring(0, j) + s.substring(j + 1), + i, + j, + end, + res, + left, + right + ); + } + } + return; + } + } + s = s.split('').reverse().join(''); + //此时多余的右括号去除结束, 还要去除多余的左括号 + if (left === '(') { + removeInvalidParenthesesHelper(s, 0, 0, s.length, res, ')', '('); + //此时多余的左右括号都去除结束, 添加当前结果 + } else { + res.push(s); + } +} +``` + +# 总 + +解法一的回溯法虽然有些暴力,但确实是有效的,写完以后可以考虑一些剪枝,优化速度。 + +解法二的话,`BFS` 往往和 `DFS` 共存,能 `DFS` 一般就可以通过 `BFS` 去解决。 + +解法三的话,就是直接从问题入手,哪个括号多了就去删哪个,多余的删除完后,就是一个合法字符串了。如果先看到的解法三,可能不是很好理解。 + + + + + diff --git a/leetcode-303-Range-Sum-Query-Immutable.md b/leetcode-303-Range-Sum-Query-Immutable.md index 28cc06d5a..332d110a2 100644 --- a/leetcode-303-Range-Sum-Query-Immutable.md +++ b/leetcode-303-Range-Sum-Query-Immutable.md @@ -1,141 +1,141 @@ -# 题目描述(简单难度) - -303、Range Sum Query - Immutable - -Given an integer array *nums*, find the sum of the elements between indices *i* and *j* (*i* ≤ *j*), inclusive. - -**Example:** - -``` -Given nums = [-2, 0, 3, -5, 2, -1] - -sumRange(0, 2) -> 1 -sumRange(2, 5) -> -1 -sumRange(0, 5) -> -3 -``` - - - -**Note:** - -1. You may assume that the array does not change. -2. There are many calls to *sumRange* function. - -设计一个类,返回数组的第 `i` 到 `j` 个元素的和。 - -# 解法一 暴力 - -题目是想让我们设计一种数据结构,我们先暴力写一下。 - -```javascript -/** - * @param {number[]} nums - */ -var NumArray = function(nums) { - this.nums = nums; -}; - -/** - * @param {number} i - * @param {number} j - * @return {number} - */ -NumArray.prototype.sumRange = function(i, j) { - let sum = 0; - for(; i <= j; i++){ - sum += this.nums[i]; - } - return sum; -}; - -/** - * Your NumArray object will be instantiated and called as such: - * var obj = new NumArray(nums) - * var param_1 = obj.sumRange(i,j) - */ -``` - -分享 [官方](https://leetcode.com/problems/range-sum-query-immutable/solution/) 提供的一个优化,因为是多次调用 `sumRange`,我们可以在每次调用后将结果保存起来,这样的话第二次调用就可以直接返回了。 - -```java -/** - * @param {number[]} nums - */ -var NumArray = function(nums) { - this.nums = nums; - this.map = {}; -}; - -/** - * @param {number} i - * @param {number} j - * @return {number} - */ -NumArray.prototype.sumRange = function(i, j) { - let key = i + '@' + j; - if(this.map.hasOwnProperty(key)){ - return this.map[key]; - } - - let sum = 0; - for(; i <= j; i++){ - sum += this.nums[i]; - } - - this.map[key] = sum; - return sum; -}; - -/** - * Your NumArray object will be instantiated and called as such: - * var obj = new NumArray(nums) - * var param_1 = obj.sumRange(i,j) - */ -``` - -# 解法二 - -和 [238 题](https://leetcode.wang/leetcode-238-Product-of-Array-Except-Self.html) 的解法二有一些像。 - -我们用一个数组保存累计的和,`numsAccumulate[i]` 存储 `0` 到 `i - 1` 累计的和。 - -如果我们想求 `i` 累积到 `j` 的和,只需要用 `numsAccumulate[j + 1]` 减去 `numsAccumulate[i]`。 - -结合下边的图应该很好理解,我们要求的是橙色部分,相当于 `B` 的部分减去 `A` 的部分。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/303_2.jpg) - -代码也就水到渠成了。 - -```javascript -/** - * @param {number[]} nums - */ -var NumArray = function (nums) { - this.numsAccumulate = [0]; - let sum = 0; - for (let i = 0; i < nums.length; i++) { - sum += nums[i]; - this.numsAccumulate.push(sum); - } -}; - -/** - * @param {number} i - * @param {number} j - * @return {number} - */ -NumArray.prototype.sumRange = function (i, j) { - return this.numsAccumulate[j + 1] - this.numsAccumulate[i]; -}; - -/** - * Your NumArray object will be instantiated and called as such: - * var obj = new NumArray(nums) - * var param_1 = obj.sumRange(i,j) - */ -``` - -# 总 - +# 题目描述(简单难度) + +303、Range Sum Query - Immutable + +Given an integer array *nums*, find the sum of the elements between indices *i* and *j* (*i* ≤ *j*), inclusive. + +**Example:** + +``` +Given nums = [-2, 0, 3, -5, 2, -1] + +sumRange(0, 2) -> 1 +sumRange(2, 5) -> -1 +sumRange(0, 5) -> -3 +``` + + + +**Note:** + +1. You may assume that the array does not change. +2. There are many calls to *sumRange* function. + +设计一个类,返回数组的第 `i` 到 `j` 个元素的和。 + +# 解法一 暴力 + +题目是想让我们设计一种数据结构,我们先暴力写一下。 + +```javascript +/** + * @param {number[]} nums + */ +var NumArray = function(nums) { + this.nums = nums; +}; + +/** + * @param {number} i + * @param {number} j + * @return {number} + */ +NumArray.prototype.sumRange = function(i, j) { + let sum = 0; + for(; i <= j; i++){ + sum += this.nums[i]; + } + return sum; +}; + +/** + * Your NumArray object will be instantiated and called as such: + * var obj = new NumArray(nums) + * var param_1 = obj.sumRange(i,j) + */ +``` + +分享 [官方](https://leetcode.com/problems/range-sum-query-immutable/solution/) 提供的一个优化,因为是多次调用 `sumRange`,我们可以在每次调用后将结果保存起来,这样的话第二次调用就可以直接返回了。 + +```java +/** + * @param {number[]} nums + */ +var NumArray = function(nums) { + this.nums = nums; + this.map = {}; +}; + +/** + * @param {number} i + * @param {number} j + * @return {number} + */ +NumArray.prototype.sumRange = function(i, j) { + let key = i + '@' + j; + if(this.map.hasOwnProperty(key)){ + return this.map[key]; + } + + let sum = 0; + for(; i <= j; i++){ + sum += this.nums[i]; + } + + this.map[key] = sum; + return sum; +}; + +/** + * Your NumArray object will be instantiated and called as such: + * var obj = new NumArray(nums) + * var param_1 = obj.sumRange(i,j) + */ +``` + +# 解法二 + +和 [238 题](https://leetcode.wang/leetcode-238-Product-of-Array-Except-Self.html) 的解法二有一些像。 + +我们用一个数组保存累计的和,`numsAccumulate[i]` 存储 `0` 到 `i - 1` 累计的和。 + +如果我们想求 `i` 累积到 `j` 的和,只需要用 `numsAccumulate[j + 1]` 减去 `numsAccumulate[i]`。 + +结合下边的图应该很好理解,我们要求的是橙色部分,相当于 `B` 的部分减去 `A` 的部分。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/303_2.jpg) + +代码也就水到渠成了。 + +```javascript +/** + * @param {number[]} nums + */ +var NumArray = function (nums) { + this.numsAccumulate = [0]; + let sum = 0; + for (let i = 0; i < nums.length; i++) { + sum += nums[i]; + this.numsAccumulate.push(sum); + } +}; + +/** + * @param {number} i + * @param {number} j + * @return {number} + */ +NumArray.prototype.sumRange = function (i, j) { + return this.numsAccumulate[j + 1] - this.numsAccumulate[i]; +}; + +/** + * Your NumArray object will be instantiated and called as such: + * var obj = new NumArray(nums) + * var param_1 = obj.sumRange(i,j) + */ +``` + +# 总 + 比较简单的一道题,解法一做缓存的思路比较常见。解法二的思路印象中也遇到过几次,看到题目我的第一反应想到的就是解法二。 \ No newline at end of file diff --git a/leetcode-304-Range-Sum-Query-2D-Immutable.md b/leetcode-304-Range-Sum-Query-2D-Immutable.md index c2d104d98..5dec0adca 100644 --- a/leetcode-304-Range-Sum-Query-2D-Immutable.md +++ b/leetcode-304-Range-Sum-Query-2D-Immutable.md @@ -1,209 +1,209 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/304.jpg) - -给定矩阵的左上角坐标和右下角坐标,返回矩阵内的数字累计的和。 - -# 解法一 - -和 [上一道题](https://leetcode.wang/leetcode-303-Range-Sum-Query-Immutable.html) 其实差不多,上一个题是一维空间的累计,这个是二维,没做过上一题,可以先看一下,这里用同样的思路了。 - -如果我们只看矩阵的某一行,那其实就变成上一题了。所以我们可以提前把每一行各自的累和求出来,然后求整个矩阵的累和的时候,一行一行求即可。 - -```javascript -/** - * @param {number[][]} matrix - */ -var NumMatrix = function (matrix) { - this.rowsAccumulate = []; - let rows = matrix.length; - if(rows === 0){ - return; - } - let cols = matrix[0].length; - for (let i = 0; i < rows; i++) { - let row = [0]; - let sum = 0; - for (let j = 0; j < cols; j++) { - sum += matrix[i][j]; - row.push(sum); - } - this.rowsAccumulate.push(row); - } -}; - -/** - * @param {number} row1 - * @param {number} col1 - * @param {number} row2 - * @param {number} col2 - * @return {number} - */ -NumMatrix.prototype.sumRegion = function (row1, col1, row2, col2) { - let sum = 0; - for (let i = row1; i <= row2; i++) { - sum = sum + this.rowsAccumulate[i][col2 + 1] - this.rowsAccumulate[i][col1]; - } - return sum; -}; - -/** - * Your NumMatrix object will be instantiated and called as such: - * var obj = new NumMatrix(matrix) - * var param_1 = obj.sumRegion(row1,col1,row2,col2) - */ -``` - -# 解法二 - -当然,我们也可以忘记上一道题的解法,重新分析,但思想还是上一题的思想。 - -我们可以用 `matrixAccumulate[i][j]` 来保存从 `(0, 0)` 到 `(i - 1, j - 1)` 矩阵内所有数累计的和。 - -`matrixAccumulate[0][*]` 和 `matrixAccumulate[*][0]` 都置为 `0` ,这样做的好处就是为了统一处理边界的情况,看完下边的解法,可以回过头来思考。 - -然后和上一道题一样,对于 `(row1, col1)` 和 `(row2, col2)` 这两个点组成的矩阵内数字的累计和可以表示为下边的式子。 - -```javascript -this.matrixAccumulate[row2 + 1][col2 + 1] - - this.matrixAccumulate[row1][col2 + 1] - - this.matrixAccumulate[row2 + 1][col1] + - this.matrixAccumulate[row1][col1] -``` - -至于为什么这样,可以结合下边的图。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/304_1.jpg) - -我们要求的是橙色部分的矩阵。只需要用 `(0, 0)` 到 `(row2, col2)` 的矩阵,减去 `(0, 0)` 到 `(row1, col2)` 的矩阵,再减去 `(0, 0)` 到 `(row2, col1)` 的矩阵,最后加上 `(0, 0)` 到 `(row1, col1)` 的矩阵。因为 `(0, 0)` 到 `(row1, col1)` 的矩阵多减了一次。 - -然后可以看看坐标的分布,就可以得出上边的式子了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/304_2.jpg) - -之所以出现,`row2 + 1` 、`co2 + 1 ` 这种坐标,是因为我们的 `matrixAccumulate[i][j]` 来保存从 `(0, 0)` 到 `(i - 1, j - 1)` ,有一个减 `1 ` 的操作。 - -至于 `matrixAccumulate` 怎么求,我们可以使用上边类似的方法,通过矩阵的加减实现。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/304_3.jpg) - -`O` 到 `A` 的累加,就等于 `A` 位置的值加上 `O` 的 `C` 的累加,加上 `O` 的 `B` 的累加,减去 `O` 到 `D` 的累加。代码的话,就是下边的样子。 - -```javascript -this.matrixAccumulate[i][j] = - matrix[i-1][j-1] + - this.matrixAccumulate[i - 1][j] + - this.matrixAccumulate[i][j - 1] - - this.matrixAccumulate[i - 1][j - 1]; - } -``` - -总代码就是下边的了。 - -```javascript -/** - * @param {number[][]} matrix - */ -var NumMatrix = function (matrix) { - this.matrixAccumulate = []; - let rows = matrix.length; - if (rows === 0) { - return; - } - let cols = matrix[0].length; - - for (let i = 0; i <= rows; i++) { - let row = []; - for (let j = 0; j <= cols; j++) { - row.push(0); - } - this.matrixAccumulate.push(row); - } - for (let i = 1; i <= rows; i++) { - for (let j = 1; j <= cols; j++) { - this.matrixAccumulate[i][j] = - matrix[i-1][j-1] + - this.matrixAccumulate[i - 1][j] + - this.matrixAccumulate[i][j - 1] - - this.matrixAccumulate[i - 1][j - 1]; - } - } -}; - -/** - * @param {number} row1 - * @param {number} col1 - * @param {number} row2 - * @param {number} col2 - * @return {number} - */ -NumMatrix.prototype.sumRegion = function (row1, col1, row2, col2) { - return ( - this.matrixAccumulate[row2 + 1][col2 + 1] - - this.matrixAccumulate[row1][col2 + 1] - - this.matrixAccumulate[row2 + 1][col1] + - this.matrixAccumulate[row1][col1] - ); -}; - -/** - * Your NumMatrix object will be instantiated and called as such: - * var obj = new NumMatrix(matrix) - * var param_1 = obj.sumRegion(row1,col1,row2,col2) - */ -``` - -再分享 [StefanPochmann](https://leetcode.com/problems/range-sum-query-2d-immutable/discuss/75381/C%2B%2B-with-helper) 大神的一个思路,上边我们用 `matrixAccumulate[i][j]` 来保存从 `(0, 0)` 到 `(i - 1, j - 1)` 矩阵内所有数累计的和,多了减一。虽然这种思路经常用到,就像字符串截取函数一样,一般都是包括左端点,不包括右端点,但看起来比较绕。 - -我们可以用 `matrixAccumulate[i][j]` 来保存从 `(0, 0)` 到 `(i, j)` 矩阵内所有数累计的和,这样的话,为了避免单独判断边界情况的麻烦,我们可以封装一个函数,对于下标小于 `0` 的边界情况直接返回 `0` ,参考下边的代码。 - -```java -/** - * @param {number[][]} matrix - */ -var NumMatrix = function (matrix) { - this.matrixAccumulate = matrix; - let rows = matrix.length; - if (rows === 0) { - return; - } - let cols = matrix[0].length; - - for (let i = 0; i < rows; i++) { - for (let j = 0; j < cols; j++) { - this.matrixAccumulate[i][j] += - this.f(i - 1, j) + this.f(i, j - 1) - this.f(i - 1, j - 1); - } - } -}; - -/** - * @param {number} row1 - * @param {number} col1 - * @param {number} row2 - * @param {number} col2 - * @return {number} - */ -NumMatrix.prototype.sumRegion = function (row1, col1, row2, col2) { - return ( - this.f(row2, col2) - - this.f(row1 - 1, col2) - - this.f(row2, col1 - 1) + - this.f(row1 - 1, col1 - 1) - ); -}; - -NumMatrix.prototype.f = function (i, j) { - return i >= 0 && j >= 0 ? this.matrixAccumulate[i][j] : 0; -}; - -/** - * Your NumMatrix object will be instantiated and called as such: - * var obj = new NumMatrix(matrix) - * var param_1 = obj.sumRegion(row1,col1,row2,col2) - */ -``` - -# 总 - -比较简单的一道题,基本上还是上一题的思路,想起来小学求矩形面积了,哈哈。解法二的话两种技巧都是处理边界情况的方法,将边界的逻辑和其他部分的逻辑统一了起来,前一种扩充 `0` 的技巧比较常用。 +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/304.jpg) + +给定矩阵的左上角坐标和右下角坐标,返回矩阵内的数字累计的和。 + +# 解法一 + +和 [上一道题](https://leetcode.wang/leetcode-303-Range-Sum-Query-Immutable.html) 其实差不多,上一个题是一维空间的累计,这个是二维,没做过上一题,可以先看一下,这里用同样的思路了。 + +如果我们只看矩阵的某一行,那其实就变成上一题了。所以我们可以提前把每一行各自的累和求出来,然后求整个矩阵的累和的时候,一行一行求即可。 + +```javascript +/** + * @param {number[][]} matrix + */ +var NumMatrix = function (matrix) { + this.rowsAccumulate = []; + let rows = matrix.length; + if(rows === 0){ + return; + } + let cols = matrix[0].length; + for (let i = 0; i < rows; i++) { + let row = [0]; + let sum = 0; + for (let j = 0; j < cols; j++) { + sum += matrix[i][j]; + row.push(sum); + } + this.rowsAccumulate.push(row); + } +}; + +/** + * @param {number} row1 + * @param {number} col1 + * @param {number} row2 + * @param {number} col2 + * @return {number} + */ +NumMatrix.prototype.sumRegion = function (row1, col1, row2, col2) { + let sum = 0; + for (let i = row1; i <= row2; i++) { + sum = sum + this.rowsAccumulate[i][col2 + 1] - this.rowsAccumulate[i][col1]; + } + return sum; +}; + +/** + * Your NumMatrix object will be instantiated and called as such: + * var obj = new NumMatrix(matrix) + * var param_1 = obj.sumRegion(row1,col1,row2,col2) + */ +``` + +# 解法二 + +当然,我们也可以忘记上一道题的解法,重新分析,但思想还是上一题的思想。 + +我们可以用 `matrixAccumulate[i][j]` 来保存从 `(0, 0)` 到 `(i - 1, j - 1)` 矩阵内所有数累计的和。 + +`matrixAccumulate[0][*]` 和 `matrixAccumulate[*][0]` 都置为 `0` ,这样做的好处就是为了统一处理边界的情况,看完下边的解法,可以回过头来思考。 + +然后和上一道题一样,对于 `(row1, col1)` 和 `(row2, col2)` 这两个点组成的矩阵内数字的累计和可以表示为下边的式子。 + +```javascript +this.matrixAccumulate[row2 + 1][col2 + 1] - + this.matrixAccumulate[row1][col2 + 1] - + this.matrixAccumulate[row2 + 1][col1] + + this.matrixAccumulate[row1][col1] +``` + +至于为什么这样,可以结合下边的图。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/304_1.jpg) + +我们要求的是橙色部分的矩阵。只需要用 `(0, 0)` 到 `(row2, col2)` 的矩阵,减去 `(0, 0)` 到 `(row1, col2)` 的矩阵,再减去 `(0, 0)` 到 `(row2, col1)` 的矩阵,最后加上 `(0, 0)` 到 `(row1, col1)` 的矩阵。因为 `(0, 0)` 到 `(row1, col1)` 的矩阵多减了一次。 + +然后可以看看坐标的分布,就可以得出上边的式子了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/304_2.jpg) + +之所以出现,`row2 + 1` 、`co2 + 1 ` 这种坐标,是因为我们的 `matrixAccumulate[i][j]` 来保存从 `(0, 0)` 到 `(i - 1, j - 1)` ,有一个减 `1 ` 的操作。 + +至于 `matrixAccumulate` 怎么求,我们可以使用上边类似的方法,通过矩阵的加减实现。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/304_3.jpg) + +`O` 到 `A` 的累加,就等于 `A` 位置的值加上 `O` 的 `C` 的累加,加上 `O` 的 `B` 的累加,减去 `O` 到 `D` 的累加。代码的话,就是下边的样子。 + +```javascript +this.matrixAccumulate[i][j] = + matrix[i-1][j-1] + + this.matrixAccumulate[i - 1][j] + + this.matrixAccumulate[i][j - 1] - + this.matrixAccumulate[i - 1][j - 1]; + } +``` + +总代码就是下边的了。 + +```javascript +/** + * @param {number[][]} matrix + */ +var NumMatrix = function (matrix) { + this.matrixAccumulate = []; + let rows = matrix.length; + if (rows === 0) { + return; + } + let cols = matrix[0].length; + + for (let i = 0; i <= rows; i++) { + let row = []; + for (let j = 0; j <= cols; j++) { + row.push(0); + } + this.matrixAccumulate.push(row); + } + for (let i = 1; i <= rows; i++) { + for (let j = 1; j <= cols; j++) { + this.matrixAccumulate[i][j] = + matrix[i-1][j-1] + + this.matrixAccumulate[i - 1][j] + + this.matrixAccumulate[i][j - 1] - + this.matrixAccumulate[i - 1][j - 1]; + } + } +}; + +/** + * @param {number} row1 + * @param {number} col1 + * @param {number} row2 + * @param {number} col2 + * @return {number} + */ +NumMatrix.prototype.sumRegion = function (row1, col1, row2, col2) { + return ( + this.matrixAccumulate[row2 + 1][col2 + 1] - + this.matrixAccumulate[row1][col2 + 1] - + this.matrixAccumulate[row2 + 1][col1] + + this.matrixAccumulate[row1][col1] + ); +}; + +/** + * Your NumMatrix object will be instantiated and called as such: + * var obj = new NumMatrix(matrix) + * var param_1 = obj.sumRegion(row1,col1,row2,col2) + */ +``` + +再分享 [StefanPochmann](https://leetcode.com/problems/range-sum-query-2d-immutable/discuss/75381/C%2B%2B-with-helper) 大神的一个思路,上边我们用 `matrixAccumulate[i][j]` 来保存从 `(0, 0)` 到 `(i - 1, j - 1)` 矩阵内所有数累计的和,多了减一。虽然这种思路经常用到,就像字符串截取函数一样,一般都是包括左端点,不包括右端点,但看起来比较绕。 + +我们可以用 `matrixAccumulate[i][j]` 来保存从 `(0, 0)` 到 `(i, j)` 矩阵内所有数累计的和,这样的话,为了避免单独判断边界情况的麻烦,我们可以封装一个函数,对于下标小于 `0` 的边界情况直接返回 `0` ,参考下边的代码。 + +```java +/** + * @param {number[][]} matrix + */ +var NumMatrix = function (matrix) { + this.matrixAccumulate = matrix; + let rows = matrix.length; + if (rows === 0) { + return; + } + let cols = matrix[0].length; + + for (let i = 0; i < rows; i++) { + for (let j = 0; j < cols; j++) { + this.matrixAccumulate[i][j] += + this.f(i - 1, j) + this.f(i, j - 1) - this.f(i - 1, j - 1); + } + } +}; + +/** + * @param {number} row1 + * @param {number} col1 + * @param {number} row2 + * @param {number} col2 + * @return {number} + */ +NumMatrix.prototype.sumRegion = function (row1, col1, row2, col2) { + return ( + this.f(row2, col2) - + this.f(row1 - 1, col2) - + this.f(row2, col1 - 1) + + this.f(row1 - 1, col1 - 1) + ); +}; + +NumMatrix.prototype.f = function (i, j) { + return i >= 0 && j >= 0 ? this.matrixAccumulate[i][j] : 0; +}; + +/** + * Your NumMatrix object will be instantiated and called as such: + * var obj = new NumMatrix(matrix) + * var param_1 = obj.sumRegion(row1,col1,row2,col2) + */ +``` + +# 总 + +比较简单的一道题,基本上还是上一题的思路,想起来小学求矩形面积了,哈哈。解法二的话两种技巧都是处理边界情况的方法,将边界的逻辑和其他部分的逻辑统一了起来,前一种扩充 `0` 的技巧比较常用。 diff --git a/leetcode-306-Additive-Number.md b/leetcode-306-Additive-Number.md index 0c45b7716..d8df13762 100644 --- a/leetcode-306-Additive-Number.md +++ b/leetcode-306-Additive-Number.md @@ -1,255 +1,255 @@ -# 题目描述(中等难度) - -306、Additive Number - -Additive number is a string whose digits can form additive sequence. - -A valid additive sequence should contain **at least** three numbers. Except for the first two numbers, each subsequent number in the sequence must be the sum of the preceding two. - -Given a string containing only digits `'0'-'9'`, write a function to determine if it's an additive number. - -**Note:** Numbers in the additive sequence **cannot** have leading zeros, so sequence `1, 2, 03` or `1, 02, 3` is invalid. - - - -**Example 1:** - -``` -Input: "112358" -Output: true -Explanation: The digits can form an additive sequence: 1, 1, 2, 3, 5, 8. - 1 + 1 = 2, 1 + 2 = 3, 2 + 3 = 5, 3 + 5 = 8 -``` - -**Example 2:** - -``` -Input: "199100199" -Output: true -Explanation: The additive sequence is: 1, 99, 100, 199. - 1 + 99 = 100, 99 + 100 = 199 -``` - - - -**Constraints:** - -- `num` consists only of digits `'0'-'9'`. -- `1 <= num.length <= 35` - -**Follow up:** -How would you handle overflow for very large input integers? - -给一个字符串,判断字符串符不符合某种形式。就是从开头选两个数,它俩的和刚好是下一个数,然后第 `2` 个数和第 `3` 数字的和又是下一个数字,以此类推。 - -# 解法一 - -理解了题意的话很简单,可以勉强的归到回溯法的问题中,我们从暴力求解的角度考虑。 - -首先需要两个数,我们可以用两层 `for` 循环依次列举。 - -```javascript -//i 表示第一个数字的结尾(不包括 i) -for (let i = 1; i < num.length; i++) { - //j 表示从 i 开始第二个数字的结尾(不包括 j) - for (let j = i + 1; j < num.length; j++) { - let num1 = Number(num.substring(0, i)); - let num2 = Number(num.substring(i, j)); - } -} -``` - -然后需要处理下特殊情况,将以 `0` 开头但不是`0`的数字去掉,比如 `023` 这种是不合法的, - -```java -//i 表示第一个数字的结尾(不包括 i) -for (let i = 1; i < num.length; i++) { - // 0 开头, 并且当前数字不是 0 - if (num[0] === '0' && i > 1) { - return false; - } - //j 表示从 i 开始第二个数字的结尾(不包括 j) - for (let j = i + 1; j < num.length; j++) { - // 0 开头, 并且当前数字不是 0 - if (num[i] === '0' && j - i > 1) { - break; - } - let num1 = Number(num.substring(0, i)); - let num2 = Number(num.substring(i, j)); - } -} -``` - -有了这两个数字,下边的就好说了。 - -只需要计算 `sum = num1 + num2` ,然后看 `sum` 在不在接下来的字符串中。 - -如果不在,那么就考虑下一个 `num1` 和 `num2` 。 - -如果在的话,`num1` 就更新为 `num2`,`num2` 更新为 `sum` ,有了新的 `num1`和 `num2` ,然后继续按照上边的步骤考虑。 - -举个例子, - -```java -1 2 2 1 4 16 - ^ ^ - i j - -num1 = 12 -num2 = 2 -sum = num1 + num2 = 14 - -接下来的数字刚好是 14, 那么就更新 num1 和 num2 - -num1 = num2 = 2 -num2 = sum = 14 - -然后继续判断即可。 -``` - -代码的话,我们可以写成递归的形式。 - -```javascript -/** - * @param {string} num - * @return {boolean} - */ -var isAdditiveNumber = function (num) { - if (num.length === 0) { - return true; - } - for (let i = 1; i < num.length; i++) { - if (num[0] === '0' && i > 1) { - return false; - } - for (let j = i + 1; j < num.length; j++) { - if (num[i] === '0' && j - i > 1) { - break; - } - let num1 = Number(num.substring(0, i)); - let num2 = Number(num.substring(i, j)); - if (isAdditiveNumberHelper(num.substring(j), num1, num2)) { - return true; - } - } - } - return false; -}; - -function isAdditiveNumberHelper(num, num1, num2) { - if (num.length === 0) { - return true; - } - //依次列举数字,判断是否等于 num1 + num2 - for (let i = 1; i <= num.length; i++) { - //不考虑以 0 开头的数字 - if (num[0] === '0' && i > 1) { - return false; - } - let sum = Number(num.substring(0, i)); - if (num1 + num2 === sum) { - //传递剩下的字符串以及新的 num1 和 num2 - return isAdditiveNumberHelper(num.substring(i), num2, sum); - //此时大于了 num1 + num2, 再往后遍历只会更大, 所以直接结束 - } else if (num1 + num2 < sum) { - break; - } - } - return false; -} -``` - -主要思想就是上边的了,看了 [这里](https://leetcode.com/problems/additive-number/discuss/75567/Java-Recursive-and-Iterative-Solutions) 的分析,代码的话,可以简单的优化下。 - -第一点,如果两个数的位数分别是 `a` 位和 `b` 位,`a >= b`,那么它俩相加得到的和至少是 `a` 位。比如 `100 + 99` 得到 `199`,依旧是三位数。 - -所以我们的 `num1` 和 `num2` 其中的一个数的位数一定不能大于等于字符串总长度的一半,不然的话剩下的字符串一定不等于 `num1` 和 `num2` 的和,因为剩下的长度不够了。 - -第二点,上边的代码中 `isAdditiveNumberHelper` 函数,略微写的复杂了些,我们可以直接利用 `String` 的 `startsWith` 函数,判断 `num1 + num2 ` 是不是字符串的开头即可。 - -基于上边两点,优化后的代码如下。 - -```javascript -/** - * @param {string} num - * @return {boolean} - */ -var isAdditiveNumber = function (num) { - if (num.length === 0) { - return true; - } - //这里取了等号,是因为长度是奇数的时候,除以二是向下取整 - for (let i = 1; i <= num.length / 2; i++) { - if (num[0] === '0' && i > 1) { - return false; - } - for (let j = i + 1; j < num.length; j++) { - if (num[i] === '0' && j - i > 1 || (j - i) > num.length / 2) { - break; - } - let num1 = Number(num.substring(0, i)); - let num2 = Number(num.substring(i, j)); - if (isAdditiveNumberHelper(num.substring(j), num1, num2)) { - return true; - } - } - } - return false; -}; - -function isAdditiveNumberHelper(num, num1, num2) { - if (num.length === 0) { - return true; - } - return num.startsWith(num1 + num2) && isAdditiveNumberHelper(num.substring((num1+num2+'').length), num2, num1 + num2); -} -``` - -当然,我们最初的分析很明显是迭代的形式,我们内层也可以直接写成循环,不需要递归函数。 - -```javascript -/** - * @param {string} num - * @return {boolean} - */ -var isAdditiveNumber = function (num) { - if (num.length === 0) { - return true; - } - for (let i = 1; i <= num.length / 2; i++) { - if (num[0] === '0' && i > 1) { - return false; - } - for (let j = i + 1; j < num.length; j++) { - if (num[i] === '0' && j - i > 1 || (j - i) > num.length / 2) { - break; - } - let num1 = Number(num.substring(0, i)); - let num2 = Number(num.substring(i, j)); - let temp = num.substring(j); - while(temp.startsWith(num1 + num2)) { - let n = num1; - num1 = num2; - num2 = num1 + n; - temp = temp.substring((num2 + '').length); - if (temp.length === 0) { - return true; - } - } - } - } - return false; -}; -``` - -还有一个扩展,`How would you handle overflow for very large input integers?` - -让我们考虑数字太大了怎么办,此时解决大数相加的问题即可,所有的操作在字符串层面上进行。参考 [第 2 题](https://leetcode.wang/leetCode-2-Add-Two-Numbers.html)。 - -# 总 - -这道题主要的想法就是列举所有情况,但总觉得先单独列举两个数字不够优雅,思考了下怎么把它合并到递归中,无果,悲伤。 - -上边其实是一种思路,只是在写法上可能有所不同,唯一的优化就是列举数字的时候考虑一半即可,但时间复杂度不会变化。 - +# 题目描述(中等难度) + +306、Additive Number + +Additive number is a string whose digits can form additive sequence. + +A valid additive sequence should contain **at least** three numbers. Except for the first two numbers, each subsequent number in the sequence must be the sum of the preceding two. + +Given a string containing only digits `'0'-'9'`, write a function to determine if it's an additive number. + +**Note:** Numbers in the additive sequence **cannot** have leading zeros, so sequence `1, 2, 03` or `1, 02, 3` is invalid. + + + +**Example 1:** + +``` +Input: "112358" +Output: true +Explanation: The digits can form an additive sequence: 1, 1, 2, 3, 5, 8. + 1 + 1 = 2, 1 + 2 = 3, 2 + 3 = 5, 3 + 5 = 8 +``` + +**Example 2:** + +``` +Input: "199100199" +Output: true +Explanation: The additive sequence is: 1, 99, 100, 199. + 1 + 99 = 100, 99 + 100 = 199 +``` + + + +**Constraints:** + +- `num` consists only of digits `'0'-'9'`. +- `1 <= num.length <= 35` + +**Follow up:** +How would you handle overflow for very large input integers? + +给一个字符串,判断字符串符不符合某种形式。就是从开头选两个数,它俩的和刚好是下一个数,然后第 `2` 个数和第 `3` 数字的和又是下一个数字,以此类推。 + +# 解法一 + +理解了题意的话很简单,可以勉强的归到回溯法的问题中,我们从暴力求解的角度考虑。 + +首先需要两个数,我们可以用两层 `for` 循环依次列举。 + +```javascript +//i 表示第一个数字的结尾(不包括 i) +for (let i = 1; i < num.length; i++) { + //j 表示从 i 开始第二个数字的结尾(不包括 j) + for (let j = i + 1; j < num.length; j++) { + let num1 = Number(num.substring(0, i)); + let num2 = Number(num.substring(i, j)); + } +} +``` + +然后需要处理下特殊情况,将以 `0` 开头但不是`0`的数字去掉,比如 `023` 这种是不合法的, + +```java +//i 表示第一个数字的结尾(不包括 i) +for (let i = 1; i < num.length; i++) { + // 0 开头, 并且当前数字不是 0 + if (num[0] === '0' && i > 1) { + return false; + } + //j 表示从 i 开始第二个数字的结尾(不包括 j) + for (let j = i + 1; j < num.length; j++) { + // 0 开头, 并且当前数字不是 0 + if (num[i] === '0' && j - i > 1) { + break; + } + let num1 = Number(num.substring(0, i)); + let num2 = Number(num.substring(i, j)); + } +} +``` + +有了这两个数字,下边的就好说了。 + +只需要计算 `sum = num1 + num2` ,然后看 `sum` 在不在接下来的字符串中。 + +如果不在,那么就考虑下一个 `num1` 和 `num2` 。 + +如果在的话,`num1` 就更新为 `num2`,`num2` 更新为 `sum` ,有了新的 `num1`和 `num2` ,然后继续按照上边的步骤考虑。 + +举个例子, + +```java +1 2 2 1 4 16 + ^ ^ + i j + +num1 = 12 +num2 = 2 +sum = num1 + num2 = 14 + +接下来的数字刚好是 14, 那么就更新 num1 和 num2 + +num1 = num2 = 2 +num2 = sum = 14 + +然后继续判断即可。 +``` + +代码的话,我们可以写成递归的形式。 + +```javascript +/** + * @param {string} num + * @return {boolean} + */ +var isAdditiveNumber = function (num) { + if (num.length === 0) { + return true; + } + for (let i = 1; i < num.length; i++) { + if (num[0] === '0' && i > 1) { + return false; + } + for (let j = i + 1; j < num.length; j++) { + if (num[i] === '0' && j - i > 1) { + break; + } + let num1 = Number(num.substring(0, i)); + let num2 = Number(num.substring(i, j)); + if (isAdditiveNumberHelper(num.substring(j), num1, num2)) { + return true; + } + } + } + return false; +}; + +function isAdditiveNumberHelper(num, num1, num2) { + if (num.length === 0) { + return true; + } + //依次列举数字,判断是否等于 num1 + num2 + for (let i = 1; i <= num.length; i++) { + //不考虑以 0 开头的数字 + if (num[0] === '0' && i > 1) { + return false; + } + let sum = Number(num.substring(0, i)); + if (num1 + num2 === sum) { + //传递剩下的字符串以及新的 num1 和 num2 + return isAdditiveNumberHelper(num.substring(i), num2, sum); + //此时大于了 num1 + num2, 再往后遍历只会更大, 所以直接结束 + } else if (num1 + num2 < sum) { + break; + } + } + return false; +} +``` + +主要思想就是上边的了,看了 [这里](https://leetcode.com/problems/additive-number/discuss/75567/Java-Recursive-and-Iterative-Solutions) 的分析,代码的话,可以简单的优化下。 + +第一点,如果两个数的位数分别是 `a` 位和 `b` 位,`a >= b`,那么它俩相加得到的和至少是 `a` 位。比如 `100 + 99` 得到 `199`,依旧是三位数。 + +所以我们的 `num1` 和 `num2` 其中的一个数的位数一定不能大于等于字符串总长度的一半,不然的话剩下的字符串一定不等于 `num1` 和 `num2` 的和,因为剩下的长度不够了。 + +第二点,上边的代码中 `isAdditiveNumberHelper` 函数,略微写的复杂了些,我们可以直接利用 `String` 的 `startsWith` 函数,判断 `num1 + num2 ` 是不是字符串的开头即可。 + +基于上边两点,优化后的代码如下。 + +```javascript +/** + * @param {string} num + * @return {boolean} + */ +var isAdditiveNumber = function (num) { + if (num.length === 0) { + return true; + } + //这里取了等号,是因为长度是奇数的时候,除以二是向下取整 + for (let i = 1; i <= num.length / 2; i++) { + if (num[0] === '0' && i > 1) { + return false; + } + for (let j = i + 1; j < num.length; j++) { + if (num[i] === '0' && j - i > 1 || (j - i) > num.length / 2) { + break; + } + let num1 = Number(num.substring(0, i)); + let num2 = Number(num.substring(i, j)); + if (isAdditiveNumberHelper(num.substring(j), num1, num2)) { + return true; + } + } + } + return false; +}; + +function isAdditiveNumberHelper(num, num1, num2) { + if (num.length === 0) { + return true; + } + return num.startsWith(num1 + num2) && isAdditiveNumberHelper(num.substring((num1+num2+'').length), num2, num1 + num2); +} +``` + +当然,我们最初的分析很明显是迭代的形式,我们内层也可以直接写成循环,不需要递归函数。 + +```javascript +/** + * @param {string} num + * @return {boolean} + */ +var isAdditiveNumber = function (num) { + if (num.length === 0) { + return true; + } + for (let i = 1; i <= num.length / 2; i++) { + if (num[0] === '0' && i > 1) { + return false; + } + for (let j = i + 1; j < num.length; j++) { + if (num[i] === '0' && j - i > 1 || (j - i) > num.length / 2) { + break; + } + let num1 = Number(num.substring(0, i)); + let num2 = Number(num.substring(i, j)); + let temp = num.substring(j); + while(temp.startsWith(num1 + num2)) { + let n = num1; + num1 = num2; + num2 = num1 + n; + temp = temp.substring((num2 + '').length); + if (temp.length === 0) { + return true; + } + } + } + } + return false; +}; +``` + +还有一个扩展,`How would you handle overflow for very large input integers?` + +让我们考虑数字太大了怎么办,此时解决大数相加的问题即可,所有的操作在字符串层面上进行。参考 [第 2 题](https://leetcode.wang/leetCode-2-Add-Two-Numbers.html)。 + +# 总 + +这道题主要的想法就是列举所有情况,但总觉得先单独列举两个数字不够优雅,思考了下怎么把它合并到递归中,无果,悲伤。 + +上边其实是一种思路,只是在写法上可能有所不同,唯一的优化就是列举数字的时候考虑一半即可,但时间复杂度不会变化。 + 之所以说勉强归到回溯法,是因为遍历路径的感觉是,先选两个数,然后一路到底,失败的话不是回到上一层,而是直接回到开头,然后重新选取两个数,继续一路到底,不是典型的回溯。 \ No newline at end of file diff --git a/leetcode-307-Range-Sum-Query-Mutable.md b/leetcode-307-Range-Sum-Query-Mutable.md index e95f145c7..277136c86 100644 --- a/leetcode-307-Range-Sum-Query-Mutable.md +++ b/leetcode-307-Range-Sum-Query-Mutable.md @@ -1,767 +1,767 @@ -# 题目描述(中等难度) - -307、Range Sum Query - Mutable - -Given an integer array *nums*, find the sum of the elements between indices *i* and *j* (*i* ≤ *j*), inclusive. - -The *update(i, val)* function modifies *nums* by updating the element at index *i* to *val*. - -**Example:** - -``` -Given nums = [1, 3, 5] - -sumRange(0, 2) -> 9 -update(1, 2) -sumRange(0, 2) -> 8 -``` - - - -**Constraints:** - -- The array is only modifiable by the *update* function. -- You may assume the number of calls to *update* and *sumRange* function is distributed evenly. -- `0 <= i <= j <= nums.length - 1` - -实现一个数据结构,支持数组的区间查询,更新数组的某个元素。 - - # 解法一 - -先来个暴力的看看对题意理解的对不对。不用技巧,`sumRange` 直接 `for` 循环算,`update` 直接更。 - -```javascript -/** - * @param {number[]} nums - */ -var NumArray = function (nums) { - this.nums = [...nums]; -}; - -/** - * @param {number} i - * @param {number} val - * @return {void} - */ -NumArray.prototype.update = function (i, val) { - this.nums[i] = val; -}; - -/** - * @param {number} i - * @param {number} j - * @return {number} - */ -NumArray.prototype.sumRange = function (i, j) { - let sum = 0; - for (let k = i; k <= j; k++) { - sum += this.nums[k]; - } - return sum; -}; - -/** - * Your NumArray object will be instantiated and called as such: - * var obj = new NumArray(nums) - * obj.update(i,val) - * var param_2 = obj.sumRange(i,j) - */ -``` - -时间复杂度: `update` 是 `O(1)`,`sumRange` 是 `O(n)`。 - -# 解法二 - -[303 题](https://leetcode.wang/leetcode-303-Range-Sum-Query-Immutable.html) 做过 `sumRange` 的优化,这里直接使用,可以过去看一下。提前把一些前缀和存起来,然后查询区间和的时候在可以通过差实现。 - -```javascript -/** - * @param {number[]} nums - */ -var NumArray = function (nums) { - this.nums = [...nums]; - this.numsAccumulate = [0]; - let sum = 0; - for (let i = 0; i < nums.length; i++) { - sum += nums[i]; - this.numsAccumulate.push(sum); - } -}; - -/** - * @param {number} i - * @param {number} val - * @return {void} - */ -NumArray.prototype.update = function (i, val) { - let sub = val - this.nums[i]; - this.nums[i] = val; - for (let k = i + 1; k < this.numsAccumulate.length; k++) { - this.numsAccumulate[k] += sub; - } -}; - -/** - * @param {number} i - * @param {number} j - * @return {number} - */ -NumArray.prototype.sumRange = function (i, j) { - return this.numsAccumulate[j + 1] - this.numsAccumulate[i]; -}; - -/** - * Your NumArray object will be instantiated and called as such: - * var obj = new NumArray(nums) - * obj.update(i,val) - * var param_2 = obj.sumRange(i,j) - */ -``` - -时间复杂度: `update` 是 `O(n)`,`sumRange` 是 `O(1)`。 - -虽然 `sumRange` 的时间复杂度优化了,但是 `update` 又变成了 `O(n)`。因为更新一个值的时候,这个值后边的累计和都需要更新。 - -分享 [这里](https://leetcode.com/problems/range-sum-query-mutable/discuss/75741/Segment-Tree-Binary-Indexed-Tree-and-the-simple-way-using-buffer-to-accelerate-in-C%2B%2B-all-quite-efficient) 的一个优化思路。通过一个 `buffer` ,`update` 进行延迟更新。 - -当更新某个值的时候,我们不立刻进行更新,而是仅仅将当前下标以及要更新的值与原来的值的差值存起来,可以用一个 `map` 作为 `buffer` ,`map[index]=sub`。 - -当求 `sumRange` 的时候,返回区间和之前,我们需要遍历我们的 `buffer` ,看一下区间内是否包含了 `buffer` 中存储的下标,然后进行相应的更新。 - -`buffer` 的大小可以根据实际情况去定,这里取 `300`。 - -```javascript -/** - * @param {number[]} nums - */ -var NumArray = function (nums) { - this.buffer = {}; - this.bufferSize = 0; - this.nums = [...nums]; - this.numsAccumulate = [0]; - let sum = 0; - for (let i = 0; i < nums.length; i++) { - sum += nums[i]; - this.numsAccumulate.push(sum); - } -}; - -/** - * @param {number} i - * @param {number} val - * @return {void} - */ -NumArray.prototype.update = function (i, val) { - let sub = val - this.nums[i]; - this.buffer[i] = sub; - this.bufferSize++; - if (this.bufferSize > 300) { - for (i in this.buffer) { - let index = Number(i); - sub = this.buffer[i]; - this.nums[index] += sub; - for (let k = index + 1; k < this.numsAccumulate.length; k++) { - this.numsAccumulate[k] += sub; - } - } - this.buffer = {}; - this.bufferSize = 0; - } -}; - -/** - * @param {number} i - * @param {number} j - * @return {number} - */ -NumArray.prototype.sumRange = function (i, j) { - let sum = this.numsAccumulate[j + 1] - this.numsAccumulate[i]; - for (let index in this.buffer) { - sub = this.buffer[index]; - if (index >= i && index <= j) { - sum += sub; - } - } - return sum; -}; - -/** - * Your NumArray object will be instantiated and called as such: - * var obj = new NumArray(nums) - * obj.update(i,val) - * var param_2 = obj.sumRange(i,j) - */ - -``` - -# 解法三 - -解法一和解法二写了不少,但时间复杂度两个方法始终一个是 `O(1)`,一个是 `O(n)`。这里再分享 [官方题解](https://leetcode.com/problems/range-sum-query-mutable/solution/) 提供的一个解法,可以优化查询区间的时间复杂度。 - -我们可以将原数据分成若干个组,然后提前计算这些组的和,举个例子。 - -```javascript -组号: 0 1 2 3 -数组: [2 4 5 6] [9 9 3 8] [1 2 3 4] [4 2 3 4] -下标: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -和: 17 29 10 13 -``` - -如果我们要计算 `sumRange(1,13)`,之前我们需要循环累加下标 `1` 到 `13` 的数字的和。 - -现在我们只需要循环累加 `1` 到 `3` 的和,加上循环累加 `12` 到 `13` 的和,再累加中间组提前算好的和,也就是第 `1` 组和第 `2` 组的和 `29` 和 `10` ,就是最终的结果了。 - -至于更新的话,我们也不需要像解法二那样更新那么多。我们只需要更新当前元素所在的组即可。 - -下一个问题,每组的大小定多少呢? - -如果定的小了,那么组数就会特别多。 - -如果定的大了,那么组内元素就会特别多。 - -组数和组内元素个数都会影响到 `sumRange` 的时间复杂度。 - -这里,我们在组数和组内元素个数之间取个平衡,假设数组大小是 `n`,那么组内元素个数取 $$\sqrt{n}$$ ,这样的话组数也是 $$\sqrt{n}$$ ,这样就可以保证我们查询的时间复杂度是 $$O(\sqrt{n})$$ 了。因为最坏的情况,无非是查询范围跨越整个数组,中间我们需要累加 $$\sqrt{n} - 2$$ 个组,第 `0` 组最多累加 $$\sqrt{n}$$ 次,最后一组也最多累加 $$\sqrt{n}$$ 次,整体上就是 $$O(\sqrt{n})$$ 了。 - -结合代码理解一下。 - -```javascript -/** - * @param {number[]} nums - */ -var NumArray = function (nums) { - this.nums = [...nums]; - this.groupSize = Math.floor(Math.sqrt(this.nums.length)); - this.group = []; - let sum = 0; - let i = 0; - for (i = 0; i < nums.length; i++) { - sum += nums[i]; - if ((i + 1) % this.groupSize === 0) { - this.group.push(sum); - sum = 0; - } - } - //有可能数组大小不能整除组的大小, 最后会遗漏下几个元素 - if (i % this.groupSize !== 0) { - this.group.push(sum); - } -}; - -/** - * @param {number} i - * @param {number} val - * @return {void} - */ -NumArray.prototype.update = function (i, val) { - let sub = val - this.nums[i]; - let groudId = Math.floor(i / this.groupSize); - this.group[groudId] += sub; - this.nums[i] = val; -}; - -/** - * @param {number} i - * @param {number} j - * @return {number} - */ -NumArray.prototype.sumRange = function (i, j) { - let groupI = Math.floor(i / this.groupSize); - let groupJ = Math.floor(j / this.groupSize); - let sum = 0; - //在同一组内, 直接累加 - if (groupI === groupJ) { - for (let k = i; k <= j; k++) { - sum += this.nums[k]; - } - } else { - //左边组的元素累加 - for (let k = i; k < (groupI + 1) * this.groupSize; k++) { - sum += this.nums[k]; - } - //累加中间所有的组 - for (let g = groupI + 1; g < groupJ; g++) { - sum += this.group[g]; - } - //右边组的元素累加 - for (let k = groupJ * this.groupSize; k <= j; k++) { - sum += this.nums[k]; - } - } - return sum; -}; - -/** - * Your NumArray object will be instantiated and called as such: - * var obj = new NumArray(nums) - * obj.update(i,val) - * var param_2 = obj.sumRange(i,j) - */ -``` - -时间复杂度: `update` 是 `O(1)`,`sumRange` 是 $$O(\sqrt{n})$$ 。 - -# 解法四 - -这个解法需要我们事先知道「线段树」这个数据结构,[84 题](https://leetcode.wang/leetCode-84-Largest-Rectangle-in-Histogram.htm) 也用过线段树。 - -线段树常用于区间统计问题,求区间和、区间最大值、最小值等,可以使得查询以及更新的时间复杂度都为 `O(log(n))`。 - -[84 题](https://leetcode.wang/leetCode-84-Largest-Rectangle-in-Histogram.htm) 我们底层是通过数组来存储的线段树,省空间,但写起来需要多思考一下下标之间的关系,相对复杂一些。这里我们通过在每个节点中加入左子树和右子树的指针来实现线段树的节点间的关系。 - -如果会写二叉树,其实线段树是同样的写法,唯一不同的地方在于二叉树只是存储当前节点的值。线段树的话需要存储当前区间的左右端点,对于这道题还要把当前区间的和存起来。明确了这一点,线段树的初始化就很好写了。 - -首先我们定义一个 `node` 节点。 - -```javascript -class TreeNode { - constructor() { - this.leftChild = null; - this.rightChild = null; - this.leftIndex = 0; - this.rightIndex = 0; - this.sum = 0; - } -} -``` - -然后通过递归的方式去建立线段树。 - -```javascript -class SegmentTree { - constructor(nums) { - this.root = this.buildTree(nums, 0, nums.length - 1); - } - buildTree(nums, start, end) { - let root = new TreeNode(); - root.leftIndex = start; - root.rightIndex = end; - if (start === end) { - root.sum = nums[start]; - return root; - } - const mid = Math.floor((start + end) / 2); - root.leftChild = this.buildTree(nums, start, mid); - root.rightChild = this.buildTree(nums, mid + 1, end); - root.sum = root.leftChild.sum + root.rightChild.sum; - return root; - } -} -``` - -和建立二叉树不同的点在于,赋的值比较多,左端点,右端点,`sum` 值还需要在左右子树建立完毕才去赋值。 - -左右子树的话我们每次从中间分隔区间,参考下图,橙色表示区间,蓝色当前当前区间和。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/306_1.jpg) - -如果我们要查区间 `[i,j]` 的和,分为两大类情况。 - -* 如果当前节点的的左端点 `leftIndex` 等于 `i`,右端点 `rightIndex` 等于 `j` ,那么当前节点就是我们要找的,直接返回当前节点的 `sum` 值即可。 -* 将区间的中点记做 `mid`。分为三种情况。 - * 查询区间在 `mid` 的左边,也就是 `j` 小于等于 `mid` ,此时我们只需要从左子树去查询区间 `[i,j]`的和即可。 - * 查询区间在 `mid` 的右边,也就是 `i` 大于 `mid`,此时我们只需要从右子树去查询区间 `[i, j]` 的和即可。 - * 否则的话,整个区间包含了 `mid`,也就是 `i <= mid < j`,此时我们需要从左子树查询区间 `[i, mid]` 的和再加上从右子树查询区间 `[mid + 1, j]` 的和。 - -举个例子,我们要查询 `[1,4]` 的和。参考上边的图,根节点的范围是 `[0, 5]`,`mid` 就是 `(0 + 5) / 2 = 2`。要查询的区间包含了 `mid` ,下一步我们从左子树查询 `[1, 2]`,从右子树查询 `[3, 4]`。然后接下来把左子树和右子树当做根节点继续查询即可。 - -代码的话就很好写了。 - -```javascript -NumArray.prototype.sumRangeHelper = function (node, i, j) { - const leftIndex = node.leftIndex; - const rightIndex = node.rightIndex; - if (leftIndex === i && rightIndex === j) { - return node.sum; - } - const mid = Math.floor((leftIndex + rightIndex) / 2); - let sum = 0; - if (j <= mid) { - sum = this.sumRangeHelper(node.leftChild, i, j); - } else if (i > mid) { - sum = this.sumRangeHelper(node.rightChild, i, j); - } else { - sum = - this.sumRangeHelper(node.leftChild, i, mid) + - this.sumRangeHelper(node.rightChild, mid + 1, j); - } - return sum; -}; -``` - -接下来考虑单点更新。 - -同样的我们可以通过递归来求解,只需要判断要更新的位置是在左子树还是右子树,然后更新相应的子树,最后更新当前根节点的值即可。参考下边的代码。 - -```javascript -NumArray.prototype.updateHelper = function (node, i, val) { - const leftIndex = node.leftIndex; - const rightIndex = node.rightIndex; - //当前节点只包含一个值,更新的一定是这个值 - if (leftIndex === rightIndex) { - node.sum = val; - return; - } - const mid = Math.floor((leftIndex + rightIndex) / 2); - if (i <= mid) { - this.updateHelper(node.leftChild, i, val); - } else { - this.updateHelper(node.rightChild, i, val); - } - node.sum = node.leftChild.sum + node.rightChild.sum; -}; -``` - -然后把上边所有的代码综合在一起即可。 - -```javascript -class TreeNode { - constructor() { - this.leftChild = null; - this.rightChild = null; - this.leftIndex = 0; - this.rightIndex = 0; - this.sum = 0; - } -} -class SegmentTree { - constructor(nums) { - this.root = this.buildTree(nums, 0, nums.length - 1); - } - buildTree(nums, start, end) { - let root = new TreeNode(); - root.leftIndex = start; - root.rightIndex = end; - if (start === end) { - root.sum = nums[start]; - return root; - } - const mid = Math.floor((start + end) / 2); - root.leftChild = this.buildTree(nums, start, mid); - root.rightChild = this.buildTree(nums, mid + 1, end); - root.sum = root.leftChild.sum + root.rightChild.sum; - return root; - } -} -/** - * @param {number[]} nums - */ -var NumArray = function (nums) { - if (nums.length === 0) { - return; - } - this.tree = new SegmentTree(nums); -}; - -/** - * @param {number} i - * @param {number} val - * @return {void} - */ -NumArray.prototype.update = function (i, val) { - this.updateHelper(this.tree.root, i, val); -}; - -NumArray.prototype.updateHelper = function (node, i, val) { - const leftIndex = node.leftIndex; - const rightIndex = node.rightIndex; - if (leftIndex === rightIndex) { - node.sum = val; - return; - } - const mid = Math.floor((leftIndex + rightIndex) / 2); - if (i <= mid) { - this.updateHelper(node.leftChild, i, val); - } else { - this.updateHelper(node.rightChild, i, val); - } - node.sum = node.leftChild.sum + node.rightChild.sum; -}; -/** - * @param {number} i - * @param {number} j - * @return {number} - */ -NumArray.prototype.sumRange = function (i, j) { - const sum = this.sumRangeHelper(this.tree.root, i, j); - return sum; -}; - -NumArray.prototype.sumRangeHelper = function (node, i, j) { - const leftIndex = node.leftIndex; - const rightIndex = node.rightIndex; - if (leftIndex === i && rightIndex === j) { - return node.sum; - } - const mid = Math.floor((leftIndex + rightIndex) / 2); - let sum = 0; - if (j <= mid) { - sum = this.sumRangeHelper(node.leftChild, i, j); - } else if (i > mid) { - sum = this.sumRangeHelper(node.rightChild, i, j); - } else { - sum = - this.sumRangeHelper(node.leftChild, i, mid) + - this.sumRangeHelper(node.rightChild, mid + 1, j); - } - return sum; -}; - -/** - * Your NumArray object will be instantiated and called as such: - * var obj = new NumArray(nums) - * obj.update(i,val) - * var param_2 = obj.sumRange(i,j) - */ - -``` - -# 解法五 - -这个解法写法很简单,但理解的话可能稍微难一些。我甚至去看了提出这个解法的 [论文](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.14.8917&rep=rep1&type=pdf)。这个解法叫 `Fenwick tree` 或者`binary indexed tree`,翻译过来的话叫做树状数组或者二叉索引树,但我觉得 `binary` 翻译成二进制更好,叫做二进制索引树更贴切些,二叉树容易引起误解。 - -回想一下解法三,我们预先求出了若干个区间和,然后查询的区间可以根据之前预先求出来的区间来求出。这里的话同样的思想,先预先求一些区间和,然后把要求的区间分解成若干个之前求好的区间和即可。相比于解法三,这里的分解会更加巧妙一些。 - -我们知道计算机中的数都是由二进制来表示的,任何一个数都可以分解成 `2` 的幂次的和,进制转换不熟的话可以参考 [再谈进制转换](https://zhuanlan.zhihu.com/p/114542440)。 - -举个例子 $$11 = 2^0 + 2^1 + 2^3 = 1 + 2 + 8$$,$$9=2^0+2^3=1+8$$ 等等。 - -接下来就是神奇的地方了,每一个数都可以拆成这样的 `x = a + b + c + ...` 的形式。 - -我们把等式左侧的数 `x` 看做是区间 `[1, x]`,等式右边看做从 `x` 开始每个区间的**长度**,也就变成了下边的样子。 - -`[1, x] = [x, x - a + 1] + [x - a, x - a - b + 1] + [x - a - b, x - a - b - c + 1] + ...`。 - -看起来有些复杂,举个具体的例子就简单多了。 - -以 $$11 = 2^0 + 2^1 + 2^3 = 1 + 2 + 8$$ 为例,可以转换为下边的等式。 - -`[1, 11] = [11, 11] + [10, 9] + [8, 1]`。 - -`[11, 11] `、`[10, 9]`、`[8, 1]` 长度分别是 `1`、`2`、`8`。 - -我们成功把一个大区间,分成了若干个小区间,这就是树状数组最核心的地方了,只要理解了上边讲的,下边就很简单了。 - -首先,因为数组的下标是从 `0` 开始的,上边的区间范围是从 `1` 开始的,所以我们在原数组开头补一个 `0` ,这样区间就是从 `1` 开始了。 - -因此我们可以通过分解快速的求出 `[1, x]` 任意前缀区间的和,知道了前缀区间的和,就回到了解法二,通过做差可以算出任意区间的和了。 - -最后,我们需要解决子区间该怎么求? - -`[1, 11] = [11, 11] + [10, 9] + [8, 1]` 我们用 `V` 表示子区间,用 `F` 表示某个区间。 - -`F[1,11] = V[11] + V[10] + V[8]` - -其中,`V[11] = F[11,11], V[10] = F[10,9], V[8]=F[8...1]`,为什么是这样? - -回到二进制,`F[0001,1011] = V[1011] + V[1010] + V[1000]` - -`1010 = 1011 - 0001`,`0001` 就是十进制的 `1`,所以 `V[1011]` 存 `1` 个数,所以 `V[11] = F[11,11]`。 - -`1000 = 1010 - 0010`,`0010` 就是十进制的 `2`,所以 `V[1010]` 存 `2` 个数,所以 ` V[10] = F[10,9]`。 - -`0000 = 1000 - 1000`,`1000` 就是十进制的 `8`,所以 `V[1000]` 存 `8` 个数,所以 ` V[8] = F[8...1]`。 - - `V[1011]` 存 `1` 个数, `V[1010]` 存 `2` 个数,看的是二进制最右边的一个 `1` 到末尾的大小。`1010` 就是 `10`,`1000` 就是 `1000` 。 - -怎么得到一个数最右边的 `1` 到末尾的大小,是二进制操作的一个技巧,会用到一些补码的知识,可以参考 [趣谈计算机补码](https://zhuanlan.zhihu.com/p/67227136)。 - -将原数取反,然后再加 `1` 得到的新数和原数按位相与就得到了最右边的 `1` 到末尾的数。 - -举个例子,对于 `101000` ,先取反得到 `010111`,再加 `1` 变成 `011000`,再和原数相与,`101000 & 011000`,刚好就得到了 `1000`。而取反再加一,根据补码的知识,可以通过取相反数得到。 - -所以对于 `i` 的话,`i & -i` 就得到了最右边的 `1` 到末尾的数,也就是 `V[i]` 这个区间存多少个数。 - -如果 `len = i & -i` ,那么 `V[i] = F[i,i-1,i-2, ... i-len+1]`。 - -参考下边的代码,`BIT` 就是我们上边要求的 `V` 数组。 - -```javascript -/** - * @param {number[]} nums - */ -var NumArray = function (nums) { - this.nums = [0, ...nums]; //补一个 0 - this.BIT = new Array(this.nums.length); - for (let i = 1; i < this.BIT.length; i++) { - let index = i - ( i & -i ) + 1; - this.BIT[i] = 0; - //累加 index 到 i 的和 - while (true) { - this.BIT[i] += this.nums[index]; - index += 1; - if (index > i) { - break; - } - } - } -}; -``` - -有了 `BIT` 这个数组,一切就都好说了。如果我们想求 `F[1, 11]` 也就是前 `11` 个数的和。 - -`F[1,11] = BIT[11] + BIT[10] + BIT[8]`,看下二进制 `BIT[0001,1011] = BIT[1011] + BIT[1010] + BIT[1000]` 。 - -`1011 -> 1010 -> 1000`,对于 `BIT` 每次的下标就是依次把当前数最右边的 `1 ` 变成 `0` 。 - -这里有两种做法,一种是我们求出当前数最右边的 `1` 到末尾的数,然后用原数减一下。 - -举个例子, `1010` 最右边的 `1` 到末尾的数是 `10` ,然后用 `1010 - 10` 就得到 `1000` 了。 - -另外一种做法,就是 `n & (n - 1)`,比如 `1010 & (1010 - 1)`,刚好就是 `1000` 了。 - -知道了这个,我们可以实现一个函数,用来求区间 `[1, n]` 的和。 - -```javascript -NumArray.prototype.range = function (index) { - let sum = 0; - while (index > 0) { - sum += this.BIT[index]; - index -= index & -index; - //index = index & (index - 1); //这样也可以 - } - return sum; -}; -``` - -有了 `range` 函数,题目中的 `sumRange` 也就很好实现了。 - -```javascript -NumArray.prototype.sumRange = function (i, j) { - //range 求的区间范围下标是从 1 开始的,所以这里的 j 需要加 1 - return this.range(j + 1) - this.range(i); -}; -``` - -接下来是更新函数怎么写。 - -更新函数的话,最关键的就是找出,当我们更新的数组第 `i` 个值,会影响到我们的哪些子区间,也就是代码中的 `BIT` 数组需要更新哪些。 - -我们来回忆下之前做了什么事情。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/306_2.jpg) - -这是论文中的一张图,含义就是我们之前分析的,`BIT[8]` 存的是 `F[1...8]` ,对应图中的就是从第 `8` 个位置到第 `1 ` 个位置的矩形。`BIT[6]` 存的是 `F[6,5]`, 对应图中的就是从第 `6` 个位置一直到第 `5 ` 个位置的矩形。 - -然后我们水平从某个数画一条线,比如从 `3` 那里画一条线。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/306_3.jpg) - -穿过了 `3` 对应的矩形,`4` 对应的矩形,`8` 对应的矩形。因此如果改变第 `3` 个数,`BIT[3]`,`BIT[4]` 以及 `BIT[8]` 就需要更新。通过这种方式我们把每个数会影响到哪个区间画出来,找一下规律。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/306_4.jpg) - -当改变了第 `5` 个元素的时候,会依次影响到 `BIT[5]`,`BIT[6]`,`BIT[8]`,`BIT[16]`。 - -`00101 -> 00110 -> 01000 -> 10000`。 - -`00101 + 1 = 00110`。 - -` 00110 + 10 = 01000` - -` 01000 + 1000 = 10000` - -可以看到每次都是加上当前数最右边的 `1` 到末尾的数,即 `next = current + (current & -current)`。 - -所以更新的代码也就出来了。 - -```javascript -/** - * @param {number} i - * @param {number} val - * @return {void} - */ -NumArray.prototype.update = function (i, val) { - i += 1;//对应的下标要进行加 1 - const sub = val - this.nums[i]; - this.nums[i] = val; - while (i < this.nums.length) { - this.BIT[i] += sub; - i += i & -i; - } -}; -``` - -综上,这道题就解决了,我们把代码合在一起。 - -```javascript -/** - * @param {number[]} nums - */ -var NumArray = function (nums) { - this.nums = [0, ...nums]; - this.BIT = new Array(this.nums.length); - for (let i = 1; i < this.BIT.length; i++) { - let index = i - ( i & -i ) + 1; - this.BIT[i] = 0; - while (true) { - this.BIT[i] += this.nums[index]; - index += 1; - if (index > i) { - break; - } - } - } -}; - -/** - * @param {number} i - * @param {number} val - * @return {void} - */ -NumArray.prototype.update = function (i, val) { - i += 1; - const sub = val - this.nums[i]; - this.nums[i] = val; - while (i < this.nums.length) { - this.BIT[i] += sub; - i += i & -i; - } -}; -/** - * @param {number} i - * @param {number} j - * @return {number} - */ -NumArray.prototype.sumRange = function (i, j) { - return this.range(j + 1) - this.range(i); -}; - -NumArray.prototype.range = function (index) { - let sum = 0; - while (index > 0) { - sum += this.BIT[index]; - // index -= index & -index; - index = index & (index - 1); //这样也可以 - } - return sum; -}; - -/** - * Your NumArray object will be instantiated and called as such: - * var obj = new NumArray(nums) - * obj.update(i,val) - * var param_2 = obj.sumRange(i,j) - */ -``` - -时间复杂度的话,初始化、更新、查询其实都和二进制的位数有关,以查询为例。每次将二进制的最后一位变成 `0`,最坏的情况就是初始值是全 `1`,即 `1111` 这种,执行次数就是 `4` 次,也就是二进制的位数。 - -如果是 `n` ,那么位数大约就是 `log(n)`,可以结合 [再谈进制转换](https://zhuanlan.zhihu.com/p/114542440) 理解。把一个数展开为 `2` 的幂次和,位数其实就是最高位的幂次加 `1`。比如 $$11 = 2^0 + 2^1 + 2^3$$ ,最高幂次是 `3` ,所以 `11` 的二进制`(1011)` 位数就是 `4`。如果要求的数是 `n`,最高的次幂是 `x` ,$$2^x + ... = n$$,近似一下 $$2^x=n$$,`x = log(n)`,位数就是 `log(n) + 1`。 - -所以 `update` 和 `sumRange` 的时间复杂度就是 `O(log(n))`。 - -对于初始化函数,因为要执行 `n` 次,所以就是 `O(nlog(n))`。当然我们也可以利用解法二,把前缀和都求出来,然后更新数组 `BIT` 的每个值,这样就是 `O(n)` 了。但不是很有必要,因为如果查询和更新的次数很多,远大于 `n` 次,那么初始化这里的时间复杂度也就无关紧要了。 - -# 总 - -看起来比较简单的一道题,涉及的东西还蛮多的。 - -解法二的通过缓存的方法一定程度优化了算法。 - -解法三的思想比较常用,将区间分成若干个子区间,然后通过子区间求解。 - -解法四的线段树属于通用的解法,除了求区间和,求区间最大值、最小值也是试用的。 - -解法五理解起来难一些,也不容易想到,但确实非常巧妙,代码相对线段树也会简单很多,不得不佩服作者。 - +# 题目描述(中等难度) + +307、Range Sum Query - Mutable + +Given an integer array *nums*, find the sum of the elements between indices *i* and *j* (*i* ≤ *j*), inclusive. + +The *update(i, val)* function modifies *nums* by updating the element at index *i* to *val*. + +**Example:** + +``` +Given nums = [1, 3, 5] + +sumRange(0, 2) -> 9 +update(1, 2) +sumRange(0, 2) -> 8 +``` + + + +**Constraints:** + +- The array is only modifiable by the *update* function. +- You may assume the number of calls to *update* and *sumRange* function is distributed evenly. +- `0 <= i <= j <= nums.length - 1` + +实现一个数据结构,支持数组的区间查询,更新数组的某个元素。 + + # 解法一 + +先来个暴力的看看对题意理解的对不对。不用技巧,`sumRange` 直接 `for` 循环算,`update` 直接更。 + +```javascript +/** + * @param {number[]} nums + */ +var NumArray = function (nums) { + this.nums = [...nums]; +}; + +/** + * @param {number} i + * @param {number} val + * @return {void} + */ +NumArray.prototype.update = function (i, val) { + this.nums[i] = val; +}; + +/** + * @param {number} i + * @param {number} j + * @return {number} + */ +NumArray.prototype.sumRange = function (i, j) { + let sum = 0; + for (let k = i; k <= j; k++) { + sum += this.nums[k]; + } + return sum; +}; + +/** + * Your NumArray object will be instantiated and called as such: + * var obj = new NumArray(nums) + * obj.update(i,val) + * var param_2 = obj.sumRange(i,j) + */ +``` + +时间复杂度: `update` 是 `O(1)`,`sumRange` 是 `O(n)`。 + +# 解法二 + +[303 题](https://leetcode.wang/leetcode-303-Range-Sum-Query-Immutable.html) 做过 `sumRange` 的优化,这里直接使用,可以过去看一下。提前把一些前缀和存起来,然后查询区间和的时候在可以通过差实现。 + +```javascript +/** + * @param {number[]} nums + */ +var NumArray = function (nums) { + this.nums = [...nums]; + this.numsAccumulate = [0]; + let sum = 0; + for (let i = 0; i < nums.length; i++) { + sum += nums[i]; + this.numsAccumulate.push(sum); + } +}; + +/** + * @param {number} i + * @param {number} val + * @return {void} + */ +NumArray.prototype.update = function (i, val) { + let sub = val - this.nums[i]; + this.nums[i] = val; + for (let k = i + 1; k < this.numsAccumulate.length; k++) { + this.numsAccumulate[k] += sub; + } +}; + +/** + * @param {number} i + * @param {number} j + * @return {number} + */ +NumArray.prototype.sumRange = function (i, j) { + return this.numsAccumulate[j + 1] - this.numsAccumulate[i]; +}; + +/** + * Your NumArray object will be instantiated and called as such: + * var obj = new NumArray(nums) + * obj.update(i,val) + * var param_2 = obj.sumRange(i,j) + */ +``` + +时间复杂度: `update` 是 `O(n)`,`sumRange` 是 `O(1)`。 + +虽然 `sumRange` 的时间复杂度优化了,但是 `update` 又变成了 `O(n)`。因为更新一个值的时候,这个值后边的累计和都需要更新。 + +分享 [这里](https://leetcode.com/problems/range-sum-query-mutable/discuss/75741/Segment-Tree-Binary-Indexed-Tree-and-the-simple-way-using-buffer-to-accelerate-in-C%2B%2B-all-quite-efficient) 的一个优化思路。通过一个 `buffer` ,`update` 进行延迟更新。 + +当更新某个值的时候,我们不立刻进行更新,而是仅仅将当前下标以及要更新的值与原来的值的差值存起来,可以用一个 `map` 作为 `buffer` ,`map[index]=sub`。 + +当求 `sumRange` 的时候,返回区间和之前,我们需要遍历我们的 `buffer` ,看一下区间内是否包含了 `buffer` 中存储的下标,然后进行相应的更新。 + +`buffer` 的大小可以根据实际情况去定,这里取 `300`。 + +```javascript +/** + * @param {number[]} nums + */ +var NumArray = function (nums) { + this.buffer = {}; + this.bufferSize = 0; + this.nums = [...nums]; + this.numsAccumulate = [0]; + let sum = 0; + for (let i = 0; i < nums.length; i++) { + sum += nums[i]; + this.numsAccumulate.push(sum); + } +}; + +/** + * @param {number} i + * @param {number} val + * @return {void} + */ +NumArray.prototype.update = function (i, val) { + let sub = val - this.nums[i]; + this.buffer[i] = sub; + this.bufferSize++; + if (this.bufferSize > 300) { + for (i in this.buffer) { + let index = Number(i); + sub = this.buffer[i]; + this.nums[index] += sub; + for (let k = index + 1; k < this.numsAccumulate.length; k++) { + this.numsAccumulate[k] += sub; + } + } + this.buffer = {}; + this.bufferSize = 0; + } +}; + +/** + * @param {number} i + * @param {number} j + * @return {number} + */ +NumArray.prototype.sumRange = function (i, j) { + let sum = this.numsAccumulate[j + 1] - this.numsAccumulate[i]; + for (let index in this.buffer) { + sub = this.buffer[index]; + if (index >= i && index <= j) { + sum += sub; + } + } + return sum; +}; + +/** + * Your NumArray object will be instantiated and called as such: + * var obj = new NumArray(nums) + * obj.update(i,val) + * var param_2 = obj.sumRange(i,j) + */ + +``` + +# 解法三 + +解法一和解法二写了不少,但时间复杂度两个方法始终一个是 `O(1)`,一个是 `O(n)`。这里再分享 [官方题解](https://leetcode.com/problems/range-sum-query-mutable/solution/) 提供的一个解法,可以优化查询区间的时间复杂度。 + +我们可以将原数据分成若干个组,然后提前计算这些组的和,举个例子。 + +```javascript +组号: 0 1 2 3 +数组: [2 4 5 6] [9 9 3 8] [1 2 3 4] [4 2 3 4] +下标: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +和: 17 29 10 13 +``` + +如果我们要计算 `sumRange(1,13)`,之前我们需要循环累加下标 `1` 到 `13` 的数字的和。 + +现在我们只需要循环累加 `1` 到 `3` 的和,加上循环累加 `12` 到 `13` 的和,再累加中间组提前算好的和,也就是第 `1` 组和第 `2` 组的和 `29` 和 `10` ,就是最终的结果了。 + +至于更新的话,我们也不需要像解法二那样更新那么多。我们只需要更新当前元素所在的组即可。 + +下一个问题,每组的大小定多少呢? + +如果定的小了,那么组数就会特别多。 + +如果定的大了,那么组内元素就会特别多。 + +组数和组内元素个数都会影响到 `sumRange` 的时间复杂度。 + +这里,我们在组数和组内元素个数之间取个平衡,假设数组大小是 `n`,那么组内元素个数取 $$\sqrt{n}$$ ,这样的话组数也是 $$\sqrt{n}$$ ,这样就可以保证我们查询的时间复杂度是 $$O(\sqrt{n})$$ 了。因为最坏的情况,无非是查询范围跨越整个数组,中间我们需要累加 $$\sqrt{n} - 2$$ 个组,第 `0` 组最多累加 $$\sqrt{n}$$ 次,最后一组也最多累加 $$\sqrt{n}$$ 次,整体上就是 $$O(\sqrt{n})$$ 了。 + +结合代码理解一下。 + +```javascript +/** + * @param {number[]} nums + */ +var NumArray = function (nums) { + this.nums = [...nums]; + this.groupSize = Math.floor(Math.sqrt(this.nums.length)); + this.group = []; + let sum = 0; + let i = 0; + for (i = 0; i < nums.length; i++) { + sum += nums[i]; + if ((i + 1) % this.groupSize === 0) { + this.group.push(sum); + sum = 0; + } + } + //有可能数组大小不能整除组的大小, 最后会遗漏下几个元素 + if (i % this.groupSize !== 0) { + this.group.push(sum); + } +}; + +/** + * @param {number} i + * @param {number} val + * @return {void} + */ +NumArray.prototype.update = function (i, val) { + let sub = val - this.nums[i]; + let groudId = Math.floor(i / this.groupSize); + this.group[groudId] += sub; + this.nums[i] = val; +}; + +/** + * @param {number} i + * @param {number} j + * @return {number} + */ +NumArray.prototype.sumRange = function (i, j) { + let groupI = Math.floor(i / this.groupSize); + let groupJ = Math.floor(j / this.groupSize); + let sum = 0; + //在同一组内, 直接累加 + if (groupI === groupJ) { + for (let k = i; k <= j; k++) { + sum += this.nums[k]; + } + } else { + //左边组的元素累加 + for (let k = i; k < (groupI + 1) * this.groupSize; k++) { + sum += this.nums[k]; + } + //累加中间所有的组 + for (let g = groupI + 1; g < groupJ; g++) { + sum += this.group[g]; + } + //右边组的元素累加 + for (let k = groupJ * this.groupSize; k <= j; k++) { + sum += this.nums[k]; + } + } + return sum; +}; + +/** + * Your NumArray object will be instantiated and called as such: + * var obj = new NumArray(nums) + * obj.update(i,val) + * var param_2 = obj.sumRange(i,j) + */ +``` + +时间复杂度: `update` 是 `O(1)`,`sumRange` 是 $$O(\sqrt{n})$$ 。 + +# 解法四 + +这个解法需要我们事先知道「线段树」这个数据结构,[84 题](https://leetcode.wang/leetCode-84-Largest-Rectangle-in-Histogram.htm) 也用过线段树。 + +线段树常用于区间统计问题,求区间和、区间最大值、最小值等,可以使得查询以及更新的时间复杂度都为 `O(log(n))`。 + +[84 题](https://leetcode.wang/leetCode-84-Largest-Rectangle-in-Histogram.htm) 我们底层是通过数组来存储的线段树,省空间,但写起来需要多思考一下下标之间的关系,相对复杂一些。这里我们通过在每个节点中加入左子树和右子树的指针来实现线段树的节点间的关系。 + +如果会写二叉树,其实线段树是同样的写法,唯一不同的地方在于二叉树只是存储当前节点的值。线段树的话需要存储当前区间的左右端点,对于这道题还要把当前区间的和存起来。明确了这一点,线段树的初始化就很好写了。 + +首先我们定义一个 `node` 节点。 + +```javascript +class TreeNode { + constructor() { + this.leftChild = null; + this.rightChild = null; + this.leftIndex = 0; + this.rightIndex = 0; + this.sum = 0; + } +} +``` + +然后通过递归的方式去建立线段树。 + +```javascript +class SegmentTree { + constructor(nums) { + this.root = this.buildTree(nums, 0, nums.length - 1); + } + buildTree(nums, start, end) { + let root = new TreeNode(); + root.leftIndex = start; + root.rightIndex = end; + if (start === end) { + root.sum = nums[start]; + return root; + } + const mid = Math.floor((start + end) / 2); + root.leftChild = this.buildTree(nums, start, mid); + root.rightChild = this.buildTree(nums, mid + 1, end); + root.sum = root.leftChild.sum + root.rightChild.sum; + return root; + } +} +``` + +和建立二叉树不同的点在于,赋的值比较多,左端点,右端点,`sum` 值还需要在左右子树建立完毕才去赋值。 + +左右子树的话我们每次从中间分隔区间,参考下图,橙色表示区间,蓝色当前当前区间和。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/306_1.jpg) + +如果我们要查区间 `[i,j]` 的和,分为两大类情况。 + +* 如果当前节点的的左端点 `leftIndex` 等于 `i`,右端点 `rightIndex` 等于 `j` ,那么当前节点就是我们要找的,直接返回当前节点的 `sum` 值即可。 +* 将区间的中点记做 `mid`。分为三种情况。 + * 查询区间在 `mid` 的左边,也就是 `j` 小于等于 `mid` ,此时我们只需要从左子树去查询区间 `[i,j]`的和即可。 + * 查询区间在 `mid` 的右边,也就是 `i` 大于 `mid`,此时我们只需要从右子树去查询区间 `[i, j]` 的和即可。 + * 否则的话,整个区间包含了 `mid`,也就是 `i <= mid < j`,此时我们需要从左子树查询区间 `[i, mid]` 的和再加上从右子树查询区间 `[mid + 1, j]` 的和。 + +举个例子,我们要查询 `[1,4]` 的和。参考上边的图,根节点的范围是 `[0, 5]`,`mid` 就是 `(0 + 5) / 2 = 2`。要查询的区间包含了 `mid` ,下一步我们从左子树查询 `[1, 2]`,从右子树查询 `[3, 4]`。然后接下来把左子树和右子树当做根节点继续查询即可。 + +代码的话就很好写了。 + +```javascript +NumArray.prototype.sumRangeHelper = function (node, i, j) { + const leftIndex = node.leftIndex; + const rightIndex = node.rightIndex; + if (leftIndex === i && rightIndex === j) { + return node.sum; + } + const mid = Math.floor((leftIndex + rightIndex) / 2); + let sum = 0; + if (j <= mid) { + sum = this.sumRangeHelper(node.leftChild, i, j); + } else if (i > mid) { + sum = this.sumRangeHelper(node.rightChild, i, j); + } else { + sum = + this.sumRangeHelper(node.leftChild, i, mid) + + this.sumRangeHelper(node.rightChild, mid + 1, j); + } + return sum; +}; +``` + +接下来考虑单点更新。 + +同样的我们可以通过递归来求解,只需要判断要更新的位置是在左子树还是右子树,然后更新相应的子树,最后更新当前根节点的值即可。参考下边的代码。 + +```javascript +NumArray.prototype.updateHelper = function (node, i, val) { + const leftIndex = node.leftIndex; + const rightIndex = node.rightIndex; + //当前节点只包含一个值,更新的一定是这个值 + if (leftIndex === rightIndex) { + node.sum = val; + return; + } + const mid = Math.floor((leftIndex + rightIndex) / 2); + if (i <= mid) { + this.updateHelper(node.leftChild, i, val); + } else { + this.updateHelper(node.rightChild, i, val); + } + node.sum = node.leftChild.sum + node.rightChild.sum; +}; +``` + +然后把上边所有的代码综合在一起即可。 + +```javascript +class TreeNode { + constructor() { + this.leftChild = null; + this.rightChild = null; + this.leftIndex = 0; + this.rightIndex = 0; + this.sum = 0; + } +} +class SegmentTree { + constructor(nums) { + this.root = this.buildTree(nums, 0, nums.length - 1); + } + buildTree(nums, start, end) { + let root = new TreeNode(); + root.leftIndex = start; + root.rightIndex = end; + if (start === end) { + root.sum = nums[start]; + return root; + } + const mid = Math.floor((start + end) / 2); + root.leftChild = this.buildTree(nums, start, mid); + root.rightChild = this.buildTree(nums, mid + 1, end); + root.sum = root.leftChild.sum + root.rightChild.sum; + return root; + } +} +/** + * @param {number[]} nums + */ +var NumArray = function (nums) { + if (nums.length === 0) { + return; + } + this.tree = new SegmentTree(nums); +}; + +/** + * @param {number} i + * @param {number} val + * @return {void} + */ +NumArray.prototype.update = function (i, val) { + this.updateHelper(this.tree.root, i, val); +}; + +NumArray.prototype.updateHelper = function (node, i, val) { + const leftIndex = node.leftIndex; + const rightIndex = node.rightIndex; + if (leftIndex === rightIndex) { + node.sum = val; + return; + } + const mid = Math.floor((leftIndex + rightIndex) / 2); + if (i <= mid) { + this.updateHelper(node.leftChild, i, val); + } else { + this.updateHelper(node.rightChild, i, val); + } + node.sum = node.leftChild.sum + node.rightChild.sum; +}; +/** + * @param {number} i + * @param {number} j + * @return {number} + */ +NumArray.prototype.sumRange = function (i, j) { + const sum = this.sumRangeHelper(this.tree.root, i, j); + return sum; +}; + +NumArray.prototype.sumRangeHelper = function (node, i, j) { + const leftIndex = node.leftIndex; + const rightIndex = node.rightIndex; + if (leftIndex === i && rightIndex === j) { + return node.sum; + } + const mid = Math.floor((leftIndex + rightIndex) / 2); + let sum = 0; + if (j <= mid) { + sum = this.sumRangeHelper(node.leftChild, i, j); + } else if (i > mid) { + sum = this.sumRangeHelper(node.rightChild, i, j); + } else { + sum = + this.sumRangeHelper(node.leftChild, i, mid) + + this.sumRangeHelper(node.rightChild, mid + 1, j); + } + return sum; +}; + +/** + * Your NumArray object will be instantiated and called as such: + * var obj = new NumArray(nums) + * obj.update(i,val) + * var param_2 = obj.sumRange(i,j) + */ + +``` + +# 解法五 + +这个解法写法很简单,但理解的话可能稍微难一些。我甚至去看了提出这个解法的 [论文](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.14.8917&rep=rep1&type=pdf)。这个解法叫 `Fenwick tree` 或者`binary indexed tree`,翻译过来的话叫做树状数组或者二叉索引树,但我觉得 `binary` 翻译成二进制更好,叫做二进制索引树更贴切些,二叉树容易引起误解。 + +回想一下解法三,我们预先求出了若干个区间和,然后查询的区间可以根据之前预先求出来的区间来求出。这里的话同样的思想,先预先求一些区间和,然后把要求的区间分解成若干个之前求好的区间和即可。相比于解法三,这里的分解会更加巧妙一些。 + +我们知道计算机中的数都是由二进制来表示的,任何一个数都可以分解成 `2` 的幂次的和,进制转换不熟的话可以参考 [再谈进制转换](https://zhuanlan.zhihu.com/p/114542440)。 + +举个例子 $$11 = 2^0 + 2^1 + 2^3 = 1 + 2 + 8$$,$$9=2^0+2^3=1+8$$ 等等。 + +接下来就是神奇的地方了,每一个数都可以拆成这样的 `x = a + b + c + ...` 的形式。 + +我们把等式左侧的数 `x` 看做是区间 `[1, x]`,等式右边看做从 `x` 开始每个区间的**长度**,也就变成了下边的样子。 + +`[1, x] = [x, x - a + 1] + [x - a, x - a - b + 1] + [x - a - b, x - a - b - c + 1] + ...`。 + +看起来有些复杂,举个具体的例子就简单多了。 + +以 $$11 = 2^0 + 2^1 + 2^3 = 1 + 2 + 8$$ 为例,可以转换为下边的等式。 + +`[1, 11] = [11, 11] + [10, 9] + [8, 1]`。 + +`[11, 11] `、`[10, 9]`、`[8, 1]` 长度分别是 `1`、`2`、`8`。 + +我们成功把一个大区间,分成了若干个小区间,这就是树状数组最核心的地方了,只要理解了上边讲的,下边就很简单了。 + +首先,因为数组的下标是从 `0` 开始的,上边的区间范围是从 `1` 开始的,所以我们在原数组开头补一个 `0` ,这样区间就是从 `1` 开始了。 + +因此我们可以通过分解快速的求出 `[1, x]` 任意前缀区间的和,知道了前缀区间的和,就回到了解法二,通过做差可以算出任意区间的和了。 + +最后,我们需要解决子区间该怎么求? + +`[1, 11] = [11, 11] + [10, 9] + [8, 1]` 我们用 `V` 表示子区间,用 `F` 表示某个区间。 + +`F[1,11] = V[11] + V[10] + V[8]` + +其中,`V[11] = F[11,11], V[10] = F[10,9], V[8]=F[8...1]`,为什么是这样? + +回到二进制,`F[0001,1011] = V[1011] + V[1010] + V[1000]` + +`1010 = 1011 - 0001`,`0001` 就是十进制的 `1`,所以 `V[1011]` 存 `1` 个数,所以 `V[11] = F[11,11]`。 + +`1000 = 1010 - 0010`,`0010` 就是十进制的 `2`,所以 `V[1010]` 存 `2` 个数,所以 ` V[10] = F[10,9]`。 + +`0000 = 1000 - 1000`,`1000` 就是十进制的 `8`,所以 `V[1000]` 存 `8` 个数,所以 ` V[8] = F[8...1]`。 + + `V[1011]` 存 `1` 个数, `V[1010]` 存 `2` 个数,看的是二进制最右边的一个 `1` 到末尾的大小。`1010` 就是 `10`,`1000` 就是 `1000` 。 + +怎么得到一个数最右边的 `1` 到末尾的大小,是二进制操作的一个技巧,会用到一些补码的知识,可以参考 [趣谈计算机补码](https://zhuanlan.zhihu.com/p/67227136)。 + +将原数取反,然后再加 `1` 得到的新数和原数按位相与就得到了最右边的 `1` 到末尾的数。 + +举个例子,对于 `101000` ,先取反得到 `010111`,再加 `1` 变成 `011000`,再和原数相与,`101000 & 011000`,刚好就得到了 `1000`。而取反再加一,根据补码的知识,可以通过取相反数得到。 + +所以对于 `i` 的话,`i & -i` 就得到了最右边的 `1` 到末尾的数,也就是 `V[i]` 这个区间存多少个数。 + +如果 `len = i & -i` ,那么 `V[i] = F[i,i-1,i-2, ... i-len+1]`。 + +参考下边的代码,`BIT` 就是我们上边要求的 `V` 数组。 + +```javascript +/** + * @param {number[]} nums + */ +var NumArray = function (nums) { + this.nums = [0, ...nums]; //补一个 0 + this.BIT = new Array(this.nums.length); + for (let i = 1; i < this.BIT.length; i++) { + let index = i - ( i & -i ) + 1; + this.BIT[i] = 0; + //累加 index 到 i 的和 + while (true) { + this.BIT[i] += this.nums[index]; + index += 1; + if (index > i) { + break; + } + } + } +}; +``` + +有了 `BIT` 这个数组,一切就都好说了。如果我们想求 `F[1, 11]` 也就是前 `11` 个数的和。 + +`F[1,11] = BIT[11] + BIT[10] + BIT[8]`,看下二进制 `BIT[0001,1011] = BIT[1011] + BIT[1010] + BIT[1000]` 。 + +`1011 -> 1010 -> 1000`,对于 `BIT` 每次的下标就是依次把当前数最右边的 `1 ` 变成 `0` 。 + +这里有两种做法,一种是我们求出当前数最右边的 `1` 到末尾的数,然后用原数减一下。 + +举个例子, `1010` 最右边的 `1` 到末尾的数是 `10` ,然后用 `1010 - 10` 就得到 `1000` 了。 + +另外一种做法,就是 `n & (n - 1)`,比如 `1010 & (1010 - 1)`,刚好就是 `1000` 了。 + +知道了这个,我们可以实现一个函数,用来求区间 `[1, n]` 的和。 + +```javascript +NumArray.prototype.range = function (index) { + let sum = 0; + while (index > 0) { + sum += this.BIT[index]; + index -= index & -index; + //index = index & (index - 1); //这样也可以 + } + return sum; +}; +``` + +有了 `range` 函数,题目中的 `sumRange` 也就很好实现了。 + +```javascript +NumArray.prototype.sumRange = function (i, j) { + //range 求的区间范围下标是从 1 开始的,所以这里的 j 需要加 1 + return this.range(j + 1) - this.range(i); +}; +``` + +接下来是更新函数怎么写。 + +更新函数的话,最关键的就是找出,当我们更新的数组第 `i` 个值,会影响到我们的哪些子区间,也就是代码中的 `BIT` 数组需要更新哪些。 + +我们来回忆下之前做了什么事情。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/306_2.jpg) + +这是论文中的一张图,含义就是我们之前分析的,`BIT[8]` 存的是 `F[1...8]` ,对应图中的就是从第 `8` 个位置到第 `1 ` 个位置的矩形。`BIT[6]` 存的是 `F[6,5]`, 对应图中的就是从第 `6` 个位置一直到第 `5 ` 个位置的矩形。 + +然后我们水平从某个数画一条线,比如从 `3` 那里画一条线。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/306_3.jpg) + +穿过了 `3` 对应的矩形,`4` 对应的矩形,`8` 对应的矩形。因此如果改变第 `3` 个数,`BIT[3]`,`BIT[4]` 以及 `BIT[8]` 就需要更新。通过这种方式我们把每个数会影响到哪个区间画出来,找一下规律。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/306_4.jpg) + +当改变了第 `5` 个元素的时候,会依次影响到 `BIT[5]`,`BIT[6]`,`BIT[8]`,`BIT[16]`。 + +`00101 -> 00110 -> 01000 -> 10000`。 + +`00101 + 1 = 00110`。 + +` 00110 + 10 = 01000` + +` 01000 + 1000 = 10000` + +可以看到每次都是加上当前数最右边的 `1` 到末尾的数,即 `next = current + (current & -current)`。 + +所以更新的代码也就出来了。 + +```javascript +/** + * @param {number} i + * @param {number} val + * @return {void} + */ +NumArray.prototype.update = function (i, val) { + i += 1;//对应的下标要进行加 1 + const sub = val - this.nums[i]; + this.nums[i] = val; + while (i < this.nums.length) { + this.BIT[i] += sub; + i += i & -i; + } +}; +``` + +综上,这道题就解决了,我们把代码合在一起。 + +```javascript +/** + * @param {number[]} nums + */ +var NumArray = function (nums) { + this.nums = [0, ...nums]; + this.BIT = new Array(this.nums.length); + for (let i = 1; i < this.BIT.length; i++) { + let index = i - ( i & -i ) + 1; + this.BIT[i] = 0; + while (true) { + this.BIT[i] += this.nums[index]; + index += 1; + if (index > i) { + break; + } + } + } +}; + +/** + * @param {number} i + * @param {number} val + * @return {void} + */ +NumArray.prototype.update = function (i, val) { + i += 1; + const sub = val - this.nums[i]; + this.nums[i] = val; + while (i < this.nums.length) { + this.BIT[i] += sub; + i += i & -i; + } +}; +/** + * @param {number} i + * @param {number} j + * @return {number} + */ +NumArray.prototype.sumRange = function (i, j) { + return this.range(j + 1) - this.range(i); +}; + +NumArray.prototype.range = function (index) { + let sum = 0; + while (index > 0) { + sum += this.BIT[index]; + // index -= index & -index; + index = index & (index - 1); //这样也可以 + } + return sum; +}; + +/** + * Your NumArray object will be instantiated and called as such: + * var obj = new NumArray(nums) + * obj.update(i,val) + * var param_2 = obj.sumRange(i,j) + */ +``` + +时间复杂度的话,初始化、更新、查询其实都和二进制的位数有关,以查询为例。每次将二进制的最后一位变成 `0`,最坏的情况就是初始值是全 `1`,即 `1111` 这种,执行次数就是 `4` 次,也就是二进制的位数。 + +如果是 `n` ,那么位数大约就是 `log(n)`,可以结合 [再谈进制转换](https://zhuanlan.zhihu.com/p/114542440) 理解。把一个数展开为 `2` 的幂次和,位数其实就是最高位的幂次加 `1`。比如 $$11 = 2^0 + 2^1 + 2^3$$ ,最高幂次是 `3` ,所以 `11` 的二进制`(1011)` 位数就是 `4`。如果要求的数是 `n`,最高的次幂是 `x` ,$$2^x + ... = n$$,近似一下 $$2^x=n$$,`x = log(n)`,位数就是 `log(n) + 1`。 + +所以 `update` 和 `sumRange` 的时间复杂度就是 `O(log(n))`。 + +对于初始化函数,因为要执行 `n` 次,所以就是 `O(nlog(n))`。当然我们也可以利用解法二,把前缀和都求出来,然后更新数组 `BIT` 的每个值,这样就是 `O(n)` 了。但不是很有必要,因为如果查询和更新的次数很多,远大于 `n` 次,那么初始化这里的时间复杂度也就无关紧要了。 + +# 总 + +看起来比较简单的一道题,涉及的东西还蛮多的。 + +解法二的通过缓存的方法一定程度优化了算法。 + +解法三的思想比较常用,将区间分成若干个子区间,然后通过子区间求解。 + +解法四的线段树属于通用的解法,除了求区间和,求区间最大值、最小值也是试用的。 + +解法五理解起来难一些,也不容易想到,但确实非常巧妙,代码相对线段树也会简单很多,不得不佩服作者。 + diff --git a/leetcode-73-Set-Matrix-Zeroes.md b/leetcode-73-Set-Matrix-Zeroes.md index 7fa4c55c5..f5739287f 100644 --- a/leetcode-73-Set-Matrix-Zeroes.md +++ b/leetcode-73-Set-Matrix-Zeroes.md @@ -1,332 +1,332 @@ -# 题目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/73.jpg) - -给定一个矩阵,然后找到所有含有 0 的地方,把该位置所在行所在列的元素全部变成 0。 - -# 解法一 - -暴力解法,用一个等大的空间把给定的矩阵存起来,然后遍历这个矩阵,遇到 0 就把原矩阵的当前行,当前列全部变作 0,然后继续遍历。 - -```java -public void setZeroes(int[][] matrix) { - int row = matrix.length; - int col = matrix[0].length; - int[][] matrix_copy = new int[row][col]; - //复制矩阵 - for (int i = 0; i < row; i++) { - for (int j = 0; j < col; j++) { - matrix_copy[i][j] = matrix[i][j]; - } - } - for (int i = 0; i < row; i++) { - for (int j = 0; j < col; j++) { - //找到 0 的位置 - if (matrix_copy[i][j] == 0) { - //将当前行,当前列置为 0 - setRowZeroes(matrix, i); - setColZeroes(matrix, j); - } - - } - } -} -//第 col 列全部置为 0 -private void setColZeroes(int[][] matrix, int col) { - for (int i = 0; i < matrix.length; i++) { - matrix[i][col] = 0; - } -} -//第 rol 行全部置为 0 -private void setRowZeroes(int[][] matrix, int row) { - for (int i = 0; i < matrix[row].length; i++) { - matrix[row][i] = 0; - } -} -``` - -时间复杂度:O ( mn )。 - -空间复杂度:O(mn)。m 和 n 分别是矩阵的行数和列数。 - -# 解法二 - -空间复杂度可以优化一下,我们可以把哪一行有 0 ,哪一列有 0 都记录下来,然后最后统一把这些行,这些列置为 0。 - -```java -public void setZeroes(int[][] matrix) { - int row = matrix.length; - int col = matrix[0].length; - //用两个 bool 数组标记当前行和当前列是否需要置为 0 - boolean[] row_zero = new boolean[row]; - boolean[] col_zero = new boolean[col]; - for (int i = 0; i < row; i++) { - for (int j = 0; j < col; j++) { - //找到 0 的位置 - if (matrix[i][j] == 0) { - row_zero[i] = true; - col_zero[j] = true; - } - } - } - //将行标记为 true 的行全部置为 0 - for (int i = 0; i < row; i++) { - if (row_zero[i]) { - setRowZeroes(matrix, i); - } - } - //将列标记为 false 的列全部置为 0 - for (int i = 0; i < col; i++) { - if (col_zero[i]) { - setColZeroes(matrix, i); - } - } -} -//第 col 列全部置为 0 -private void setColZeroes(int[][] matrix, int col) { - for (int i = 0; i < matrix.length; i++) { - matrix[i][col] = 0; - } -} -//第 rol 行全部置为 0 -private void setRowZeroes(int[][] matrix, int row) { - for (int i = 0; i < matrix[row].length; i++) { - matrix[row][i] = 0; - } -} -``` - -时间复杂度:O ( mn )。 - -空间复杂度:O(m + n)。m 和 n 分别是矩阵的行数和列数。 - -顺便说一下 [leetcode 解法一]()说的解法,思想是一样的,只不过它没有用 bool 数组去标记,而是用两个 set 去存行和列。 - -```java -class Solution { - public void setZeroes(int[][] matrix) { - int R = matrix.length; - int C = matrix[0].length; - Set rows = new HashSet(); - Set cols = new HashSet(); - - // 将元素为 0 的地方的行和列存起来 - for (int i = 0; i < R; i++) { - for (int j = 0; j < C; j++) { - if (matrix[i][j] == 0) { - rows.add(i); - cols.add(j); - } - } - } - - //将存储的 Set 拿出来,然后将当前行和列相应的元素置零 - for (int i = 0; i < R; i++) { - for (int j = 0; j < C; j++) { - if (rows.contains(i) || cols.contains(j)) { - matrix[i][j] = 0; - } - } - } - } -} -``` - -这里,有一个比自己巧妙的地方时,自己比较直接的用两个函数去将行和列分别置零,但很明显自己的算法会使得一些元素重复置零。而上边提供的算法,每个元素只遍历一次就够了,很棒。 - -# 解法三 - -继续优化空间复杂度,接下来用的思想之前也用过,例如[41题解法二]()和[47题解法二](),就是用给定的数组去存我们需要的数据,只要保证原来的数据不丢失就可以。 - -按 [47题解法二]() 的思路,就是假设我们对问题足够的了解,假设存在一个数,矩阵中永远不会存在,然后我们就可以把需要变成 0 的位置先变成这个数,也就是先标记一下,最后再统一把这个数变成 0。直接贴下[leetcode解法二]()的代码。 - -```java -class Solution { - public void setZeroes(int[][] matrix) { - int MODIFIED = -1000000; //假设这个数字不存在于矩阵中 - int R = matrix.length; - int C = matrix[0].length; - - for (int r = 0; r < R; r++) { - for (int c = 0; c < C; c++) { - //找到等于 0 的位置 - if (matrix[r][c] == 0) { - // 将需要变成 0 的行和列改为之前定义的数字 - // 如果是 0 不要管,因为我们要找 0 的位置 - for (int k = 0; k < C; k++) { - if (matrix[r][k] != 0) { - matrix[r][k] = MODIFIED; - } - } - for (int k = 0; k < R; k++) { - if (matrix[k][c] != 0) { - matrix[k][c] = MODIFIED; - } - } - } - } - } - - for (int r = 0; r < R; r++) { - for (int c = 0; c < C; c++) { - // 将是定义的数字的位置变成 0 - if (matrix[r][c] == MODIFIED) { - matrix[r][c] = 0; - } - } - } - } -} -``` - -时间复杂度:O ( mn )。 - -空间复杂度:O(1)。m 和 n 分别是矩阵的行数和列数。 - -当然,这个解法局限性很强,很依赖于样例的取值,我们继续想其他的方法。 - -回想一下解法二,我们用了两个 bool 数组去标记当前哪些行和那些列需要置零,我们能不能在矩阵中找点儿空间去存我们的标记呢? - -可以的!因为当我们找到第一个 0 的时候,这个 0 所在行和所在列就要全部更新成 0,所以它之前的数据是什么就不重要了,所以我们可以把这一行和这一列当做标记位,0 当做 false,1 当做 true,最后像解法二一样,统一更新就够了。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/73_2.jpg) - -如上图,找到第一个 0 出现的位置,把橙色当做解法二的列标志位,黄色当做解法二的行标志位。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/73_3.jpg) - -如上图,我们首先需要初始化为 0,并且遇到之前是 0 的位置我们需要把它置为 1,代表当前行(或者列)最终要值为 0。 - -![](https://windliang.oss-cn-beijing.aliyuncs.com/73_4.jpg) - -如上图,继续遍历找 0 的位置,找到后将对应的位置置为 1 即可。橙色部分的数字为 1 代表当前列要置为 0,黄色部分的数字为 1 代表当前行要置为 0。 - -看下代码吧。 - -```java -public void setZeroes(int[][] matrix) { - int row = matrix.length; - int col = matrix[0].length; - int free_row = -1; //记录第一个 0 出现的行 - int free_col = -1; //记录第一个 0 出现的列 - for (int i = 0; i < row; i++) { - for (int j = 0; j < col; j++) { - //如果是当前作为标记的列,就跳过 - if (j == free_col) { - continue; - } - if (matrix[i][j] == 0) { - //判断是否是第一个 0 - if (free_row == -1) { - free_row = i; - free_col = j; - //初始化行标记位为 0,如果之前是 0 就置为 1 - for (int k = 0; k < matrix.length; k++) { - if (matrix[k][free_col] == 0) { - matrix[k][free_col] = 1; - } else { - matrix[k][free_col] = 0; - } - - } - //初始化列标记位为 0,如果之前是 0 就置为 1 - for (int k = 0; k < matrix[free_row].length; k++) { - if (matrix[free_row][k] == 0) { - matrix[free_row][k] = 1; - } else { - matrix[free_row][k] = 0; - } - } - break; - //找 0 的位置,将相应的标志置 1 - } else { - matrix[i][free_col] = 1; - matrix[free_row][j] = 1; - } - } - - } - } - if (free_row != -1) { - //将标志位为 1 的所有列置为 0 - for (int i = 0; i < col; i++) { - if (matrix[free_row][i] == 1) { - setColZeroes(matrix, i); - } - } - //将标志位为 1 的所有行置为 0 - for (int i = 0; i < row; i++) { - if (matrix[i][free_col] == 1) { - setRowZeroes(matrix, i); - } - } - } -} - -private void setColZeroes(int[][] matrix, int col) { - for (int i = 0; i < matrix.length; i++) { - matrix[i][col] = 0; - } -} - -private void setRowZeroes(int[][] matrix, int row) { - for (int i = 0; i < matrix[row].length; i++) { - matrix[row][i] = 0; - } -} -``` - -时间复杂度:O ( mn )。 - -空间复杂度:O(1)。 - -[leetcode解法三]()和我的思想是一样的,它标记位直接用第一行和第一列,由于第一行和第一列不一定会被置为 0,所以需要用 isCol 变量来标记第一列是否需要置为 0,用 matrix[0\]\[0\] 标记第一行是否需要置为 0。它是将用 0 表示当前行(列)需要置 0,这一点也很巧妙,相比我上边的算法就不需要初始化标记位了。 - -```java -class Solution { - public void setZeroes(int[][] matrix) { - Boolean isCol = false; - int R = matrix.length; - int C = matrix[0].length; - for (int i = 0; i < R; i++) { - //判断第 1 列是否需要置为 0 - if (matrix[i][0] == 0) { - isCol = true; - } - //找 0 的位置,将相应标记置 0 - for (int j = 1; j < C; j++) { - if (matrix[i][j] == 0) { - matrix[0][j] = 0; - matrix[i][0] = 0; - } - } - } - //根据标志,将元素置 0 - for (int i = 1; i < R; i++) { - for (int j = 1; j < C; j++) { - if (matrix[i][0] == 0 || matrix[0][j] == 0) { - matrix[i][j] = 0; - } - } - } - - //判断第一行是否需要置 0 - if (matrix[0][0] == 0) { - for (int j = 0; j < C; j++) { - matrix[0][j] = 0; - } - } - - //判断第一列是否需要置 0 - if (isCol) { - for (int i = 0; i < R; i++) { - matrix[i][0] = 0; - } - } - } -} -``` - -# 总 - +# 题目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/73.jpg) + +给定一个矩阵,然后找到所有含有 0 的地方,把该位置所在行所在列的元素全部变成 0。 + +# 解法一 + +暴力解法,用一个等大的空间把给定的矩阵存起来,然后遍历这个矩阵,遇到 0 就把原矩阵的当前行,当前列全部变作 0,然后继续遍历。 + +```java +public void setZeroes(int[][] matrix) { + int row = matrix.length; + int col = matrix[0].length; + int[][] matrix_copy = new int[row][col]; + //复制矩阵 + for (int i = 0; i < row; i++) { + for (int j = 0; j < col; j++) { + matrix_copy[i][j] = matrix[i][j]; + } + } + for (int i = 0; i < row; i++) { + for (int j = 0; j < col; j++) { + //找到 0 的位置 + if (matrix_copy[i][j] == 0) { + //将当前行,当前列置为 0 + setRowZeroes(matrix, i); + setColZeroes(matrix, j); + } + + } + } +} +//第 col 列全部置为 0 +private void setColZeroes(int[][] matrix, int col) { + for (int i = 0; i < matrix.length; i++) { + matrix[i][col] = 0; + } +} +//第 rol 行全部置为 0 +private void setRowZeroes(int[][] matrix, int row) { + for (int i = 0; i < matrix[row].length; i++) { + matrix[row][i] = 0; + } +} +``` + +时间复杂度:O ( mn )。 + +空间复杂度:O(mn)。m 和 n 分别是矩阵的行数和列数。 + +# 解法二 + +空间复杂度可以优化一下,我们可以把哪一行有 0 ,哪一列有 0 都记录下来,然后最后统一把这些行,这些列置为 0。 + +```java +public void setZeroes(int[][] matrix) { + int row = matrix.length; + int col = matrix[0].length; + //用两个 bool 数组标记当前行和当前列是否需要置为 0 + boolean[] row_zero = new boolean[row]; + boolean[] col_zero = new boolean[col]; + for (int i = 0; i < row; i++) { + for (int j = 0; j < col; j++) { + //找到 0 的位置 + if (matrix[i][j] == 0) { + row_zero[i] = true; + col_zero[j] = true; + } + } + } + //将行标记为 true 的行全部置为 0 + for (int i = 0; i < row; i++) { + if (row_zero[i]) { + setRowZeroes(matrix, i); + } + } + //将列标记为 false 的列全部置为 0 + for (int i = 0; i < col; i++) { + if (col_zero[i]) { + setColZeroes(matrix, i); + } + } +} +//第 col 列全部置为 0 +private void setColZeroes(int[][] matrix, int col) { + for (int i = 0; i < matrix.length; i++) { + matrix[i][col] = 0; + } +} +//第 rol 行全部置为 0 +private void setRowZeroes(int[][] matrix, int row) { + for (int i = 0; i < matrix[row].length; i++) { + matrix[row][i] = 0; + } +} +``` + +时间复杂度:O ( mn )。 + +空间复杂度:O(m + n)。m 和 n 分别是矩阵的行数和列数。 + +顺便说一下 [leetcode 解法一]()说的解法,思想是一样的,只不过它没有用 bool 数组去标记,而是用两个 set 去存行和列。 + +```java +class Solution { + public void setZeroes(int[][] matrix) { + int R = matrix.length; + int C = matrix[0].length; + Set rows = new HashSet(); + Set cols = new HashSet(); + + // 将元素为 0 的地方的行和列存起来 + for (int i = 0; i < R; i++) { + for (int j = 0; j < C; j++) { + if (matrix[i][j] == 0) { + rows.add(i); + cols.add(j); + } + } + } + + //将存储的 Set 拿出来,然后将当前行和列相应的元素置零 + for (int i = 0; i < R; i++) { + for (int j = 0; j < C; j++) { + if (rows.contains(i) || cols.contains(j)) { + matrix[i][j] = 0; + } + } + } + } +} +``` + +这里,有一个比自己巧妙的地方时,自己比较直接的用两个函数去将行和列分别置零,但很明显自己的算法会使得一些元素重复置零。而上边提供的算法,每个元素只遍历一次就够了,很棒。 + +# 解法三 + +继续优化空间复杂度,接下来用的思想之前也用过,例如[41题解法二]()和[47题解法二](),就是用给定的数组去存我们需要的数据,只要保证原来的数据不丢失就可以。 + +按 [47题解法二]() 的思路,就是假设我们对问题足够的了解,假设存在一个数,矩阵中永远不会存在,然后我们就可以把需要变成 0 的位置先变成这个数,也就是先标记一下,最后再统一把这个数变成 0。直接贴下[leetcode解法二]()的代码。 + +```java +class Solution { + public void setZeroes(int[][] matrix) { + int MODIFIED = -1000000; //假设这个数字不存在于矩阵中 + int R = matrix.length; + int C = matrix[0].length; + + for (int r = 0; r < R; r++) { + for (int c = 0; c < C; c++) { + //找到等于 0 的位置 + if (matrix[r][c] == 0) { + // 将需要变成 0 的行和列改为之前定义的数字 + // 如果是 0 不要管,因为我们要找 0 的位置 + for (int k = 0; k < C; k++) { + if (matrix[r][k] != 0) { + matrix[r][k] = MODIFIED; + } + } + for (int k = 0; k < R; k++) { + if (matrix[k][c] != 0) { + matrix[k][c] = MODIFIED; + } + } + } + } + } + + for (int r = 0; r < R; r++) { + for (int c = 0; c < C; c++) { + // 将是定义的数字的位置变成 0 + if (matrix[r][c] == MODIFIED) { + matrix[r][c] = 0; + } + } + } + } +} +``` + +时间复杂度:O ( mn )。 + +空间复杂度:O(1)。m 和 n 分别是矩阵的行数和列数。 + +当然,这个解法局限性很强,很依赖于样例的取值,我们继续想其他的方法。 + +回想一下解法二,我们用了两个 bool 数组去标记当前哪些行和那些列需要置零,我们能不能在矩阵中找点儿空间去存我们的标记呢? + +可以的!因为当我们找到第一个 0 的时候,这个 0 所在行和所在列就要全部更新成 0,所以它之前的数据是什么就不重要了,所以我们可以把这一行和这一列当做标记位,0 当做 false,1 当做 true,最后像解法二一样,统一更新就够了。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/73_2.jpg) + +如上图,找到第一个 0 出现的位置,把橙色当做解法二的列标志位,黄色当做解法二的行标志位。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/73_3.jpg) + +如上图,我们首先需要初始化为 0,并且遇到之前是 0 的位置我们需要把它置为 1,代表当前行(或者列)最终要值为 0。 + +![](https://windliang.oss-cn-beijing.aliyuncs.com/73_4.jpg) + +如上图,继续遍历找 0 的位置,找到后将对应的位置置为 1 即可。橙色部分的数字为 1 代表当前列要置为 0,黄色部分的数字为 1 代表当前行要置为 0。 + +看下代码吧。 + +```java +public void setZeroes(int[][] matrix) { + int row = matrix.length; + int col = matrix[0].length; + int free_row = -1; //记录第一个 0 出现的行 + int free_col = -1; //记录第一个 0 出现的列 + for (int i = 0; i < row; i++) { + for (int j = 0; j < col; j++) { + //如果是当前作为标记的列,就跳过 + if (j == free_col) { + continue; + } + if (matrix[i][j] == 0) { + //判断是否是第一个 0 + if (free_row == -1) { + free_row = i; + free_col = j; + //初始化行标记位为 0,如果之前是 0 就置为 1 + for (int k = 0; k < matrix.length; k++) { + if (matrix[k][free_col] == 0) { + matrix[k][free_col] = 1; + } else { + matrix[k][free_col] = 0; + } + + } + //初始化列标记位为 0,如果之前是 0 就置为 1 + for (int k = 0; k < matrix[free_row].length; k++) { + if (matrix[free_row][k] == 0) { + matrix[free_row][k] = 1; + } else { + matrix[free_row][k] = 0; + } + } + break; + //找 0 的位置,将相应的标志置 1 + } else { + matrix[i][free_col] = 1; + matrix[free_row][j] = 1; + } + } + + } + } + if (free_row != -1) { + //将标志位为 1 的所有列置为 0 + for (int i = 0; i < col; i++) { + if (matrix[free_row][i] == 1) { + setColZeroes(matrix, i); + } + } + //将标志位为 1 的所有行置为 0 + for (int i = 0; i < row; i++) { + if (matrix[i][free_col] == 1) { + setRowZeroes(matrix, i); + } + } + } +} + +private void setColZeroes(int[][] matrix, int col) { + for (int i = 0; i < matrix.length; i++) { + matrix[i][col] = 0; + } +} + +private void setRowZeroes(int[][] matrix, int row) { + for (int i = 0; i < matrix[row].length; i++) { + matrix[row][i] = 0; + } +} +``` + +时间复杂度:O ( mn )。 + +空间复杂度:O(1)。 + +[leetcode解法三]()和我的思想是一样的,它标记位直接用第一行和第一列,由于第一行和第一列不一定会被置为 0,所以需要用 isCol 变量来标记第一列是否需要置为 0,用 matrix[0\]\[0\] 标记第一行是否需要置为 0。它是将用 0 表示当前行(列)需要置 0,这一点也很巧妙,相比我上边的算法就不需要初始化标记位了。 + +```java +class Solution { + public void setZeroes(int[][] matrix) { + Boolean isCol = false; + int R = matrix.length; + int C = matrix[0].length; + for (int i = 0; i < R; i++) { + //判断第 1 列是否需要置为 0 + if (matrix[i][0] == 0) { + isCol = true; + } + //找 0 的位置,将相应标记置 0 + for (int j = 1; j < C; j++) { + if (matrix[i][j] == 0) { + matrix[0][j] = 0; + matrix[i][0] = 0; + } + } + } + //根据标志,将元素置 0 + for (int i = 1; i < R; i++) { + for (int j = 1; j < C; j++) { + if (matrix[i][0] == 0 || matrix[0][j] == 0) { + matrix[i][j] = 0; + } + } + } + + //判断第一行是否需要置 0 + if (matrix[0][0] == 0) { + for (int j = 0; j < C; j++) { + matrix[0][j] = 0; + } + } + + //判断第一列是否需要置 0 + if (isCol) { + for (int i = 0; i < R; i++) { + matrix[i][0] = 0; + } + } + } +} +``` + +# 总 + 这道题如果对空间复杂度没有要求就很简单了,对于空间复杂度的优化,充分利用给定的空间的思想很经典了。 \ No newline at end of file diff --git a/leetcode-91-Decode-Ways.md b/leetcode-91-Decode-Ways.md index c5cb9b2ad..b23410e3a 100644 --- a/leetcode-91-Decode-Ways.md +++ b/leetcode-91-Decode-Ways.md @@ -1,197 +1,197 @@ -# 題目描述(中等难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/91.jpg) - -每个数字对应一个字母,给一串数字,问有几种解码方式。例如 226 可以有三种,2|2|6,22|6,2|26。 - -# 解法一 递归 - -很容易想到递归去解决,将大问题化作小问题。 - -比如 232232323232。 - -对于第一个字母我们有两种划分方式。 - -2|**32232323232** 和 23|**2232323232** - -所以,如果我们分别知道了上边划分的右半部分 32232323232 的解码方式是 ans1 种,2232323232 的解码方式是 ans2 种,那么整体 232232323232 的解码方式就是 ans1 + ans2 种。可能一下子,有些反应不过来,可以看一下下边的类比。 - -假如从深圳到北京可以经过**武汉**和**上海**两条路,而从**武汉**到北京有 8 条路,从**上海**到北京有 6 条路。那么从深圳到北京就有 8 + 6 = 14 条路。 - -```java -public int numDecodings(String s) { - return getAns(s, 0); -} - -private int getAns(String s, int start) { - //划分到了最后返回 1 - if (start == s.length()) { - return 1; - } - //开头是 0,0 不对应任何字母,直接返回 0 - if (s.charAt(start) == '0') { - return 0; - } - //得到第一种的划分的解码方式 - int ans1 = getAns(s, start + 1); - int ans2 = 0; - //判断前两个数字是不是小于等于 26 的 - if (start < s.length() - 1) { - int ten = (s.charAt(start) - '0') * 10; - int one = s.charAt(start + 1) - '0'; - if (ten + one <= 26) { - ans2 = getAns(s, start + 2); - } - } - return ans1 + ans2; -} -``` - -时间复杂度: - -空间复杂度: - -# 解法二 递归 memoization - -解法一的递归中,走完左子树,再走右子树会把一些已经算过的结果重新算,所以我们可以用 memoization 技术,就是算出一个结果很就保存,第二次算这个的时候直接拿出来就可以了。 - -```java -public int numDecodings(String s) { - HashMap memoization = new HashMap<>(); - return getAns(s, 0, memoization); -} - -private int getAns(String s, int start, HashMap memoization) { - if (start == s.length()) { - return 1; - } - if (s.charAt(start) == '0') { - return 0; - } - //判断之前是否计算过 - int m = memoization.getOrDefault(start, -1); - if (m != -1) { - return m; - } - int ans1 = getAns(s, start + 1, memoization); - int ans2 = 0; - if (start < s.length() - 1) { - int ten = (s.charAt(start) - '0') * 10; - int one = s.charAt(start + 1) - '0'; - if (ten + one <= 26) { - ans2 = getAns(s, start + 2, memoization); - } - } - //将结果保存 - memoization.put(start, ans1 + ans2); - return ans1 + ans2; -} -``` - -# 解法三 动态规划 - -同样的,递归就是压栈压栈压栈,出栈出栈出栈的过程,我们可以利用动态规划的思想,省略压栈的过程,直接从 bottom 到 top。 - -用一个 dp 数组, dp [ i ] 代表字符串 s [ i, s.len-1 ],也就是 s 从 i 开始到结尾的字符串的解码方式。 - -这样和递归完全一样的递推式。 - -如果 s [ i ] 和 s [ i + 1 ] 组成的数字小于等于 26,那么 - -dp [ i ] = dp[ i + 1 ] + dp [ i + 2 ] - -```java -public int numDecodings(String s) { - int len = s.length(); - int[] dp = new int[len + 1]; - dp[len] = 1; //将递归法的结束条件初始化为 1 - //最后一个数字不等于 0 就初始化为 1 - if (s.charAt(len - 1) != '0') { - dp[len - 1] = 1; - } - for (int i = len - 2; i >= 0; i--) { - //当前数字时 0 ,直接跳过,0 不代表任何字母 - if (s.charAt(i) == '0') { - continue; - } - int ans1 = dp[i + 1]; - //判断两个字母组成的数字是否小于等于 26 - int ans2 = 0; - int ten = (s.charAt(i) - '0') * 10; - int one = s.charAt(i + 1) - '0'; - if (ten + one <= 26) { - ans2 = dp[i + 2]; - } - dp[i] = ans1 + ans2; - - } - return dp[0]; -} -``` - -接下来就是,动态规划的空间优化了,例如[5题](),[10题](),[53题](),[72题]()等等都是同样的思路。都是注意到一个特点,当更新到 dp [ i ] 的时候,我们只用到 dp [ i + 1] 和 dp [ i + 2],之后的数据就没有用了。所以我们不需要 dp 开 len + 1 的空间。 - -简单的做法,我们只申请 3 个空间,然后把 dp 的下标对 3 求余就够了。 - -```java -public int numDecodings4(String s) { - int len = s.length(); - int[] dp = new int[3]; - dp[len % 3] = 1; - if (s.charAt(len - 1) != '0') { - dp[(len - 1) % 3] = 1; - } - for (int i = len - 2; i >= 0; i--) { - if (s.charAt(i) == '0') { - dp[i % 3] = 0; //这里很重要,因为空间复用了,不要忘记归零 - continue; - } - int ans1 = dp[(i + 1) % 3]; - int ans2 = 0; - int ten = (s.charAt(i) - '0') * 10; - int one = s.charAt(i + 1) - '0'; - if (ten + one <= 26) { - ans2 = dp[(i + 2) % 3]; - } - dp[i % 3] = ans1 + ans2; - - } - return dp[0]; -} -``` - -然后,如果多考虑以下,我们其实并不需要 3 个空间,我们只需要 2 个就够了,只需要更新的时候,指针移动一下,代码如下。 - -```java -public int numDecodings5(String s) { - int len = s.length(); - int end = 1; - int cur = 0; - if (s.charAt(len - 1) != '0') { - cur = 1; - } - for (int i = len - 2; i >= 0; i--) { - if (s.charAt(i) == '0') { - end = cur;//end 前移 - cur = 0; - continue; - } - int ans1 = cur; - int ans2 = 0; - int ten = (s.charAt(i) - '0') * 10; - int one = s.charAt(i + 1) - '0'; - if (ten + one <= 26) { - ans2 = end; - } - end = cur; //end 前移 - cur = ans1 + ans2; - - } - return cur; -} -``` - -# 总 - -从递归,到动态规划,到动态规划的空间复杂度优化,已经很多这样的题了,很经典。 - +# 題目描述(中等难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/91.jpg) + +每个数字对应一个字母,给一串数字,问有几种解码方式。例如 226 可以有三种,2|2|6,22|6,2|26。 + +# 解法一 递归 + +很容易想到递归去解决,将大问题化作小问题。 + +比如 232232323232。 + +对于第一个字母我们有两种划分方式。 + +2|**32232323232** 和 23|**2232323232** + +所以,如果我们分别知道了上边划分的右半部分 32232323232 的解码方式是 ans1 种,2232323232 的解码方式是 ans2 种,那么整体 232232323232 的解码方式就是 ans1 + ans2 种。可能一下子,有些反应不过来,可以看一下下边的类比。 + +假如从深圳到北京可以经过**武汉**和**上海**两条路,而从**武汉**到北京有 8 条路,从**上海**到北京有 6 条路。那么从深圳到北京就有 8 + 6 = 14 条路。 + +```java +public int numDecodings(String s) { + return getAns(s, 0); +} + +private int getAns(String s, int start) { + //划分到了最后返回 1 + if (start == s.length()) { + return 1; + } + //开头是 0,0 不对应任何字母,直接返回 0 + if (s.charAt(start) == '0') { + return 0; + } + //得到第一种的划分的解码方式 + int ans1 = getAns(s, start + 1); + int ans2 = 0; + //判断前两个数字是不是小于等于 26 的 + if (start < s.length() - 1) { + int ten = (s.charAt(start) - '0') * 10; + int one = s.charAt(start + 1) - '0'; + if (ten + one <= 26) { + ans2 = getAns(s, start + 2); + } + } + return ans1 + ans2; +} +``` + +时间复杂度: + +空间复杂度: + +# 解法二 递归 memoization + +解法一的递归中,走完左子树,再走右子树会把一些已经算过的结果重新算,所以我们可以用 memoization 技术,就是算出一个结果很就保存,第二次算这个的时候直接拿出来就可以了。 + +```java +public int numDecodings(String s) { + HashMap memoization = new HashMap<>(); + return getAns(s, 0, memoization); +} + +private int getAns(String s, int start, HashMap memoization) { + if (start == s.length()) { + return 1; + } + if (s.charAt(start) == '0') { + return 0; + } + //判断之前是否计算过 + int m = memoization.getOrDefault(start, -1); + if (m != -1) { + return m; + } + int ans1 = getAns(s, start + 1, memoization); + int ans2 = 0; + if (start < s.length() - 1) { + int ten = (s.charAt(start) - '0') * 10; + int one = s.charAt(start + 1) - '0'; + if (ten + one <= 26) { + ans2 = getAns(s, start + 2, memoization); + } + } + //将结果保存 + memoization.put(start, ans1 + ans2); + return ans1 + ans2; +} +``` + +# 解法三 动态规划 + +同样的,递归就是压栈压栈压栈,出栈出栈出栈的过程,我们可以利用动态规划的思想,省略压栈的过程,直接从 bottom 到 top。 + +用一个 dp 数组, dp [ i ] 代表字符串 s [ i, s.len-1 ],也就是 s 从 i 开始到结尾的字符串的解码方式。 + +这样和递归完全一样的递推式。 + +如果 s [ i ] 和 s [ i + 1 ] 组成的数字小于等于 26,那么 + +dp [ i ] = dp[ i + 1 ] + dp [ i + 2 ] + +```java +public int numDecodings(String s) { + int len = s.length(); + int[] dp = new int[len + 1]; + dp[len] = 1; //将递归法的结束条件初始化为 1 + //最后一个数字不等于 0 就初始化为 1 + if (s.charAt(len - 1) != '0') { + dp[len - 1] = 1; + } + for (int i = len - 2; i >= 0; i--) { + //当前数字时 0 ,直接跳过,0 不代表任何字母 + if (s.charAt(i) == '0') { + continue; + } + int ans1 = dp[i + 1]; + //判断两个字母组成的数字是否小于等于 26 + int ans2 = 0; + int ten = (s.charAt(i) - '0') * 10; + int one = s.charAt(i + 1) - '0'; + if (ten + one <= 26) { + ans2 = dp[i + 2]; + } + dp[i] = ans1 + ans2; + + } + return dp[0]; +} +``` + +接下来就是,动态规划的空间优化了,例如[5题](),[10题](),[53题](),[72题]()等等都是同样的思路。都是注意到一个特点,当更新到 dp [ i ] 的时候,我们只用到 dp [ i + 1] 和 dp [ i + 2],之后的数据就没有用了。所以我们不需要 dp 开 len + 1 的空间。 + +简单的做法,我们只申请 3 个空间,然后把 dp 的下标对 3 求余就够了。 + +```java +public int numDecodings4(String s) { + int len = s.length(); + int[] dp = new int[3]; + dp[len % 3] = 1; + if (s.charAt(len - 1) != '0') { + dp[(len - 1) % 3] = 1; + } + for (int i = len - 2; i >= 0; i--) { + if (s.charAt(i) == '0') { + dp[i % 3] = 0; //这里很重要,因为空间复用了,不要忘记归零 + continue; + } + int ans1 = dp[(i + 1) % 3]; + int ans2 = 0; + int ten = (s.charAt(i) - '0') * 10; + int one = s.charAt(i + 1) - '0'; + if (ten + one <= 26) { + ans2 = dp[(i + 2) % 3]; + } + dp[i % 3] = ans1 + ans2; + + } + return dp[0]; +} +``` + +然后,如果多考虑以下,我们其实并不需要 3 个空间,我们只需要 2 个就够了,只需要更新的时候,指针移动一下,代码如下。 + +```java +public int numDecodings5(String s) { + int len = s.length(); + int end = 1; + int cur = 0; + if (s.charAt(len - 1) != '0') { + cur = 1; + } + for (int i = len - 2; i >= 0; i--) { + if (s.charAt(i) == '0') { + end = cur;//end 前移 + cur = 0; + continue; + } + int ans1 = cur; + int ans2 = 0; + int ten = (s.charAt(i) - '0') * 10; + int one = s.charAt(i + 1) - '0'; + if (ten + one <= 26) { + ans2 = end; + } + end = cur; //end 前移 + cur = ans1 + ans2; + + } + return cur; +} +``` + +# 总 + +从递归,到动态规划,到动态规划的空间复杂度优化,已经很多这样的题了,很经典。 + diff --git a/leetcode-99-Recover-Binary-Search-Tree.md b/leetcode-99-Recover-Binary-Search-Tree.md index 2770324fc..fb3093ed2 100644 --- a/leetcode-99-Recover-Binary-Search-Tree.md +++ b/leetcode-99-Recover-Binary-Search-Tree.md @@ -1,267 +1,267 @@ -# 题目描述(困难难度) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/99.png) - -依旧是二分查找树的题,一个合法的二分查找树随机交换了两个数的位置,然后让我们恢复二分查找树。不能改变原来的结构,只是改变两个数的位置。二分查找树定义如下: - -> 1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值; -> 2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值; -> 3. 任意节点的左、右子树也分别为二叉查找树; -> 4. 没有键值相等的节点。 - -# 解法一 递归 - -和 [98 题]()有些像。这里的思路如下: - -让我们来考虑交换的位置的可能: - -1. 根节点和左子树的某个数字交换 -> 由于根节点大于左子树中的所有数,所以交换后我们只要找左子树中最大的那个数,就是所交换的那个数 - -2. 根节点和右子树的某个数字交换 -> 由于根节点小于右子树中的所有数,所以交换后我们只要在右子树中最小的那个数,就是所交换的那个数 -3. 左子树和右子树的两个数字交换 -> 找左子树中最大的数,右子树中最小的数,即对应两个交换的数 -4. 左子树中的两个数字交换 -5. 右子树中的两个数字交换 - -思想有了,代码很好写了。 - -```java -public void recoverTree2(TreeNode root) { - if (root == null) { - return; - } - //寻找左子树中最大的节点 - TreeNode maxLeft = getMaxOfBST(root.left); - //寻找右子树中最小的节点 - TreeNode minRight = getMinOfBST(root.right); - - if (minRight != null && maxLeft != null) { - //左边的大于根节点,右边的小于根节点,对应情况 3,左右子树中的两个数字交换 - if ( maxLeft.val > root.val && minRight.val < root.val) { - int temp = minRight.val; - minRight.val = maxLeft.val; - maxLeft.val = temp; - } - } - - if (maxLeft != null) { - //左边最大的大于根节点,对应情况 1,根节点和左子树的某个数做了交换 - if (maxLeft.val > root.val) { - int temp = maxLeft.val; - maxLeft.val = root.val; - root.val = temp; - } - } - - if (minRight != null) { - //右边最小的小于根节点,对应情况 2,根节点和右子树的某个数做了交换 - if (minRight.val < root.val) { - int temp = minRight.val; - minRight.val = root.val; - root.val = temp; - } - } - //对应情况 4,左子树中的两个数进行了交换 - recoverTree(root.left); - //对应情况 5,右子树中的两个数进行了交换 - recoverTree(root.right); - -} -//寻找树中最小的节点 -private TreeNode getMinOfBST(TreeNode root) { - if (root == null) { - return null; - } - TreeNode minLeft = getMinOfBST(root.left); - TreeNode minRight = getMinOfBST(root.right); - TreeNode min = root; - if (minLeft != null && min.val > minLeft.val) { - min = minLeft; - } - if (minRight != null && min.val > minRight.val) { - min = minRight; - } - return min; -} - -//寻找树中最大的节点 -private TreeNode getMaxOfBST(TreeNode root) { - if (root == null) { - return null; - } - TreeNode maxLeft = getMaxOfBST(root.left); - TreeNode maxRight = getMaxOfBST(root.right); - TreeNode max = root; - if (maxLeft != null && max.val < maxLeft.val) { - max = maxLeft; - } - if (maxRight != null && max.val < maxRight.val) { - max = maxRight; - } - return max; -} -``` - -# 解法二 - -参考 [这里]()。 - -如果记得 [98 题](),我们判断是否是一个合法的二分查找树是使用到了中序遍历。原因就是二分查找树的一个性质,左孩子小于根节点,根节点小于右孩子。所以做一次中序遍历,产生的序列就是从小到大排列的有序序列。 - -回到这道题,题目交换了两个数字,其实就是在有序序列中交换了两个数字。而我们只需要把它还原。 - -交换的位置的话就是两种情况。 - -* 相邻的两个数字交换 - - [ 1 2 3 4 5 ] 中 2 和 3 进行交换,[ 1 3 2 4 5 ],这样的话只产生一组逆序的数字(正常情况是从小到大排序,交换后产生了从大到小),3 2。 - - 我们只需要遍历数组,找到后,把这一组的两个数字进行交换即可。 - -* 不相邻的两个数字交换 - - [ 1 2 3 4 5 ] 中 2 和 5 进行交换,[ 1 5 3 4 2 ],这样的话其实就是产生了两组逆序的数字对。5 3 和 4 2。 - - 所以我们只需要遍历数组,然后找到这两组逆序对,然后把第一组前一个数字和第二组后一个数字进行交换即完成了还原。 - -所以在中序遍历中,只需要利用一个 pre 节点和当前节点比较,如果 pre 节点的值大于当前节点的值,那么就是我们要找的逆序的数字。分别用两个指针 first 和 second 保存即可。如果找到第二组逆序的数字,我们就把 second 更新为当前节点。最后把 first 和 second 两个的数字交换即可。 - -中序遍历,参考[ 94 题 ](),有三种方法,递归,栈,Morris 。这里的话,我们都改一下。 - -## 递归版中序遍历 - -```java -TreeNode first = null; -TreeNode second = null; -public void recoverTree(TreeNode root) { - inorderTraversal(root); - int temp = first.val; - first.val = second.val; - second.val = temp; -} -TreeNode pre = null; -private void inorderTraversal(TreeNode root) { - if (root == null) { - return; - } - inorderTraversal(root.left); - /*******************************************************/ - if(pre != null && root.val < pre.val) { - //第一次遇到逆序对 - if(first==null){ - first = pre; - second = root; - //第二次遇到逆序对 - }else{ - second = root; - } - } - pre = root; - /*******************************************************/ - inorderTraversal(root.right); -} -``` - -## 栈版中序遍历 - -```java -TreeNode first = null; -TreeNode second = null; - -public void recoverTree(TreeNode root) { - inorderTraversal(root); - int temp = first.val; - first.val = second.val; - second.val = temp; -} - -public void inorderTraversal(TreeNode root) { - if (root == null) - return; - Stack stack = new Stack<>(); - TreeNode pre = null; - while (root != null || !stack.isEmpty()) { - while (root != null) { - stack.push(root); - root = root.left; - } - root = stack.pop(); - /*******************************************************/ - if (pre != null && root.val < pre.val) { - if (first == null) { - first = pre; - second = root; - } else { - second = root; - } - } - pre = root; - /*******************************************************/ - root = root.right; - } -} -``` - -## Morris 版中序遍历 - -因为之前这个方法中用了 pre 变量,为了方便,这里也需要 pre 变量,我们用 pre_new 代替。具体 Morris 遍历算法参见[ 94 题 ]()。利用 Morris 的话,我们的空间复杂度终于达到了 O(1)。 - -```java -public void recoverTree(TreeNode root) { - TreeNode first = null; - TreeNode second = null; - TreeNode cur = root; - TreeNode pre_new = null; - while (cur != null) { - // 情况 1 - if (cur.left == null) { - /*******************************************************/ - if (pre_new != null && cur.val < pre_new.val) { - if (first == null) { - first = pre_new; - second = cur; - } else { - second = cur; - } - } - pre_new = cur; - /*******************************************************/ - cur = cur.right; - } else { - // 找左子树最右边的节点 - TreeNode pre = cur.left; - while (pre.right != null && pre.right != cur) { - pre = pre.right; - } - // 情况 2.1 - if (pre.right == null) { - pre.right = cur; - cur = cur.left; - } - // 情况 2.2 - if (pre.right == cur) { - pre.right = null; // 这里可以恢复为 null - /*******************************************************/ - if (pre_new != null && cur.val < pre_new.val) { - if (first == null) { - first = pre_new; - second = cur; - } else { - second = cur; - } - } - pre_new = cur; - /*******************************************************/ - cur = cur.right; - } - } - } - - int temp = first.val; - first.val = second.val; - second.val = temp; -} -``` - -# 总 - +# 题目描述(困难难度) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/99.png) + +依旧是二分查找树的题,一个合法的二分查找树随机交换了两个数的位置,然后让我们恢复二分查找树。不能改变原来的结构,只是改变两个数的位置。二分查找树定义如下: + +> 1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值; +> 2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值; +> 3. 任意节点的左、右子树也分别为二叉查找树; +> 4. 没有键值相等的节点。 + +# 解法一 递归 + +和 [98 题]()有些像。这里的思路如下: + +让我们来考虑交换的位置的可能: + +1. 根节点和左子树的某个数字交换 -> 由于根节点大于左子树中的所有数,所以交换后我们只要找左子树中最大的那个数,就是所交换的那个数 + +2. 根节点和右子树的某个数字交换 -> 由于根节点小于右子树中的所有数,所以交换后我们只要在右子树中最小的那个数,就是所交换的那个数 +3. 左子树和右子树的两个数字交换 -> 找左子树中最大的数,右子树中最小的数,即对应两个交换的数 +4. 左子树中的两个数字交换 +5. 右子树中的两个数字交换 + +思想有了,代码很好写了。 + +```java +public void recoverTree2(TreeNode root) { + if (root == null) { + return; + } + //寻找左子树中最大的节点 + TreeNode maxLeft = getMaxOfBST(root.left); + //寻找右子树中最小的节点 + TreeNode minRight = getMinOfBST(root.right); + + if (minRight != null && maxLeft != null) { + //左边的大于根节点,右边的小于根节点,对应情况 3,左右子树中的两个数字交换 + if ( maxLeft.val > root.val && minRight.val < root.val) { + int temp = minRight.val; + minRight.val = maxLeft.val; + maxLeft.val = temp; + } + } + + if (maxLeft != null) { + //左边最大的大于根节点,对应情况 1,根节点和左子树的某个数做了交换 + if (maxLeft.val > root.val) { + int temp = maxLeft.val; + maxLeft.val = root.val; + root.val = temp; + } + } + + if (minRight != null) { + //右边最小的小于根节点,对应情况 2,根节点和右子树的某个数做了交换 + if (minRight.val < root.val) { + int temp = minRight.val; + minRight.val = root.val; + root.val = temp; + } + } + //对应情况 4,左子树中的两个数进行了交换 + recoverTree(root.left); + //对应情况 5,右子树中的两个数进行了交换 + recoverTree(root.right); + +} +//寻找树中最小的节点 +private TreeNode getMinOfBST(TreeNode root) { + if (root == null) { + return null; + } + TreeNode minLeft = getMinOfBST(root.left); + TreeNode minRight = getMinOfBST(root.right); + TreeNode min = root; + if (minLeft != null && min.val > minLeft.val) { + min = minLeft; + } + if (minRight != null && min.val > minRight.val) { + min = minRight; + } + return min; +} + +//寻找树中最大的节点 +private TreeNode getMaxOfBST(TreeNode root) { + if (root == null) { + return null; + } + TreeNode maxLeft = getMaxOfBST(root.left); + TreeNode maxRight = getMaxOfBST(root.right); + TreeNode max = root; + if (maxLeft != null && max.val < maxLeft.val) { + max = maxLeft; + } + if (maxRight != null && max.val < maxRight.val) { + max = maxRight; + } + return max; +} +``` + +# 解法二 + +参考 [这里]()。 + +如果记得 [98 题](),我们判断是否是一个合法的二分查找树是使用到了中序遍历。原因就是二分查找树的一个性质,左孩子小于根节点,根节点小于右孩子。所以做一次中序遍历,产生的序列就是从小到大排列的有序序列。 + +回到这道题,题目交换了两个数字,其实就是在有序序列中交换了两个数字。而我们只需要把它还原。 + +交换的位置的话就是两种情况。 + +* 相邻的两个数字交换 + + [ 1 2 3 4 5 ] 中 2 和 3 进行交换,[ 1 3 2 4 5 ],这样的话只产生一组逆序的数字(正常情况是从小到大排序,交换后产生了从大到小),3 2。 + + 我们只需要遍历数组,找到后,把这一组的两个数字进行交换即可。 + +* 不相邻的两个数字交换 + + [ 1 2 3 4 5 ] 中 2 和 5 进行交换,[ 1 5 3 4 2 ],这样的话其实就是产生了两组逆序的数字对。5 3 和 4 2。 + + 所以我们只需要遍历数组,然后找到这两组逆序对,然后把第一组前一个数字和第二组后一个数字进行交换即完成了还原。 + +所以在中序遍历中,只需要利用一个 pre 节点和当前节点比较,如果 pre 节点的值大于当前节点的值,那么就是我们要找的逆序的数字。分别用两个指针 first 和 second 保存即可。如果找到第二组逆序的数字,我们就把 second 更新为当前节点。最后把 first 和 second 两个的数字交换即可。 + +中序遍历,参考[ 94 题 ](),有三种方法,递归,栈,Morris 。这里的话,我们都改一下。 + +## 递归版中序遍历 + +```java +TreeNode first = null; +TreeNode second = null; +public void recoverTree(TreeNode root) { + inorderTraversal(root); + int temp = first.val; + first.val = second.val; + second.val = temp; +} +TreeNode pre = null; +private void inorderTraversal(TreeNode root) { + if (root == null) { + return; + } + inorderTraversal(root.left); + /*******************************************************/ + if(pre != null && root.val < pre.val) { + //第一次遇到逆序对 + if(first==null){ + first = pre; + second = root; + //第二次遇到逆序对 + }else{ + second = root; + } + } + pre = root; + /*******************************************************/ + inorderTraversal(root.right); +} +``` + +## 栈版中序遍历 + +```java +TreeNode first = null; +TreeNode second = null; + +public void recoverTree(TreeNode root) { + inorderTraversal(root); + int temp = first.val; + first.val = second.val; + second.val = temp; +} + +public void inorderTraversal(TreeNode root) { + if (root == null) + return; + Stack stack = new Stack<>(); + TreeNode pre = null; + while (root != null || !stack.isEmpty()) { + while (root != null) { + stack.push(root); + root = root.left; + } + root = stack.pop(); + /*******************************************************/ + if (pre != null && root.val < pre.val) { + if (first == null) { + first = pre; + second = root; + } else { + second = root; + } + } + pre = root; + /*******************************************************/ + root = root.right; + } +} +``` + +## Morris 版中序遍历 + +因为之前这个方法中用了 pre 变量,为了方便,这里也需要 pre 变量,我们用 pre_new 代替。具体 Morris 遍历算法参见[ 94 题 ]()。利用 Morris 的话,我们的空间复杂度终于达到了 O(1)。 + +```java +public void recoverTree(TreeNode root) { + TreeNode first = null; + TreeNode second = null; + TreeNode cur = root; + TreeNode pre_new = null; + while (cur != null) { + // 情况 1 + if (cur.left == null) { + /*******************************************************/ + if (pre_new != null && cur.val < pre_new.val) { + if (first == null) { + first = pre_new; + second = cur; + } else { + second = cur; + } + } + pre_new = cur; + /*******************************************************/ + cur = cur.right; + } else { + // 找左子树最右边的节点 + TreeNode pre = cur.left; + while (pre.right != null && pre.right != cur) { + pre = pre.right; + } + // 情况 2.1 + if (pre.right == null) { + pre.right = cur; + cur = cur.left; + } + // 情况 2.2 + if (pre.right == cur) { + pre.right = null; // 这里可以恢复为 null + /*******************************************************/ + if (pre_new != null && cur.val < pre_new.val) { + if (first == null) { + first = pre_new; + second = cur; + } else { + second = cur; + } + } + pre_new = cur; + /*******************************************************/ + cur = cur.right; + } + } + } + + int temp = first.val; + first.val = second.val; + second.val = temp; +} +``` + +# 总 + 自己开始看到二分查找树,还是没有想到中序遍历,而是用了递归的思路去分析。可以看到如果想到中序遍历,题目会简单很多。 \ No newline at end of file diff --git "a/leetcode100\346\226\251\345\233\236\351\241\276.md" "b/leetcode100\346\226\251\345\233\236\351\241\276.md" index cff352c88..2adb46a3f 100644 --- "a/leetcode100\346\226\251\345\233\236\351\241\276.md" +++ "b/leetcode100\346\226\251\345\233\236\351\241\276.md" @@ -1,101 +1,101 @@ -leetcode 100 斩!从第 1 题开始,到现在也差不多快一年了,回顾纪念一下。 - -![](https://windliangblog.oss-cn-beijing.aliyuncs.com/submit.jpg) - -# 为什么开始刷题? - -从大一就知道了 leetcode,但刷题总是三天打鱼,两天晒网,会发现刷过的题,隔一段时间再看还是需要很久才能再想起来,于是就萌发了刷一题总结一题的想法。 - -另一方面,leetcode 上的 discuss 里一些解,有时候讲解的很少,甚至只丢一些代码,对于我等这种菜鸟有时候看的太废劲了,所以不如自己把各种解法都理清楚,然后详细的总结出来,也方便其他人更好的理解。 - -# 刚开始的感觉 - -大一的时候,听过 ACM,然后暑假也去学校的 ACM 集训试了试,但当时基础太差了,栈和队列都不知道是什么,所以也就没有走上 ACM 的道路。之后就各种学安卓、web、后端的应用开发的一些东西了。后来准备开始刷题是大四毕业的时候了吧。 - -当时对回溯、动态规划也都只是上课的时候学过,也并不熟练。开始几题的时候,也都很慢,很多都自己想不出来。然后就去看别人的题解。看完以后,就什么都不看,然后按自己的思路再写一遍代码。 - -尤其是[第 5 题](),求最长回文序列,现在都印象深刻,记得当时用了好几天才把所有解法总结了出来。 - -所以大家如果想刷题的话,也不用怕自己基础不好,大不了哪些名词不会就去查,一点点积累就可以,重要的是**开始**和**坚持**。 - -# 现在的感觉 - -从开始可能只是觉得该刷一刷题,到现在可能真的是爱上了刷题。 - -现在刷题基本可以想出一种思路,有时候甚至和最优解想到了一起,还会想出一些别人没有想到的解法,这种成就感可能就是打游戏超神的感觉吧,哈哈。 - -此外,看 discuss 的时候,每当看到令人拍案称奇的思路的时候,真的是让人心旷神怡,开心的不得了,就像中了彩票一样的开心,赶快去和同学分享。 - -有时候也会看到一些让人捧腹的评论,题目是输入一个字符串,输出所有可能的 ip 地址。 - -```java -Input: "25525511135" -Output: ["255.255.11.135","255.255.111.35"] -``` - -![](https://windliang.oss-cn-beijing.aliyuncs.com/93_2.png) - -![](https://windliang.oss-cn-beijing.aliyuncs.com/93_3.jpg) - -# 刷题的收获 - -在总结的过程中,因为力求给他人讲懂,在理清思路的动机的过程中,会发现之前的想法可能是错的,会总结着总结着就明白了另一种解法,或者产生新的想法,或者明白各个解法相互之间的联系,会比仅仅 AC 多出很多收获。 - -从理清他人的想法,再到自己写出代码,再到把各个解法用自己的理解串起来,会有一种「纸上得来终觉浅,绝知此事要躬行」的感觉。有时候虽然大的框架有了,但是小的细节方面还是需要自己去体会。为什么加这个 if?为什么是小于等于?每一句代码的产生都是有原因的,绝不会是可有可无的代码。 - -所以虽然一道题从看题,理解,自己考虑,看别人解法,到重新实现,再到总结出来,可能需要 3、4 个小时,甚至 5、6 个小时或者更多,但我觉得是值得的。 - -此外,也有很多人加自己的微信过来亦或是感谢自己,亦或是指出错误,亦或是询问问题,亦或是没说过话的,哈哈。有微软、谷歌、百度、阿里、腾讯的大佬,有各个大学的学生,甚至巧的是还能加上高中的校友,世界真小,哈哈。 - -![](https://windliangblog.oss-cn-beijing.aliyuncs.com/addchat1.jpg) - -![](https://windliangblog.oss-cn-beijing.aliyuncs.com/addchat2.jpg) - -![](https://windliangblog.oss-cn-beijing.aliyuncs.com/addchat3.jpg) - -![](https://windliangblog.oss-cn-beijing.aliyuncs.com/addchat4.jpg) - -上边是最近加的一些人,每次收到别人的称赞自己也会很开心。此外,博客是直接放在 github 上的,目前也有 280 stars 了,是自己 github 上 start 数最多的项目了,说来也惭愧,希望以后自己努力可以有一个好的开源项目。 - -# 刷题的理解 - -一些人可能会纠结用什么语言去刷,其实没必要纠结的。刷题需要考虑的是算法,而不是语言。算法就像是从家里到超市该怎么走?出门左拐,右拐直走....而语言是我们选择的交通工具,骑车?步行?开车?平衡车?每种交通工具都有自己的优点和缺点,语言也是如此。而好的算法可能更像是,我们偶然发现了一条近路,降低了我们的时间复杂度或者是空间复杂度。 - -刷了 100 道题了,我觉得必须要掌握的就是递归的思想了,利用这个思想可以解大部分的题了。计算机擅长的就是记忆以及速度,而递归可以把这两个优势发挥到极致。遇到问题以后,我们可以考虑如何把大问题分解成小问题,想出来以后,代码很容易就出来了。 - -此外,一些递归可以用动态规划的思想改写,从而优化递归压栈所消耗的时间,递归是顶部到底部再回到顶部,而动态规划通过存储,直接从底部到顶部解决问题。 - -最经典的例子就是斐波那契数列了,求第 n 项数列的值。 - -> 斐波那契数列,指的是这样一个数列:1、1、2、3、5、8、13、21、34 …… 在数学上,斐波纳契数列定义如下:F ( 0 ) = 0,F ( 1 ) = 1 , F ( n ) = F ( n - 1 ) + F ( n - 2 )(n >= 2,n ∈ N*); - -如果用递归的思想去写,代码简洁而优雅。 - -```java -long Fibonacci(int n){ - if (n == 0) - return 0; - else if (n == 1) - return 1; - else - return Fibonacci(n-1) + Fibonacci(n-2); -} -``` - -当然,这样的话太慢了,优化的话,就是把递归过程的结果保存起来,或者就是改写成动态规划,最强的是其实是有一个公式的,直接利用公式就可以。 - -此外,还有一些题目就是根据题目的理解去写代码了,没有什么特殊的技巧。 - -# 未来的打算 - -当然是继续刷下去了,很开心,每天不刷一刷题会不习惯的,希望大家也早日感受到刷题的乐趣,哈哈。 - -在线地址:[https://leetcode.wang](https://leetcode.wang),域名也比较好记,希望对大家会有帮助。 - -我是用 gitbook 搭建的,我觉得上边「搜索」的插件很好用,可以直接根据关键字搜出来自己想做的题。 - -![](https://windliangblog.oss-cn-beijing.aliyuncs.com/search.jpg) - -知乎专栏也会同步更新:[]()。 - -越努力,越幸运,共勉。 \ No newline at end of file +leetcode 100 斩!从第 1 题开始,到现在也差不多快一年了,回顾纪念一下。 + +![](https://windliangblog.oss-cn-beijing.aliyuncs.com/submit.jpg) + +# 为什么开始刷题? + +从大一就知道了 leetcode,但刷题总是三天打鱼,两天晒网,会发现刷过的题,隔一段时间再看还是需要很久才能再想起来,于是就萌发了刷一题总结一题的想法。 + +另一方面,leetcode 上的 discuss 里一些解,有时候讲解的很少,甚至只丢一些代码,对于我等这种菜鸟有时候看的太废劲了,所以不如自己把各种解法都理清楚,然后详细的总结出来,也方便其他人更好的理解。 + +# 刚开始的感觉 + +大一的时候,听过 ACM,然后暑假也去学校的 ACM 集训试了试,但当时基础太差了,栈和队列都不知道是什么,所以也就没有走上 ACM 的道路。之后就各种学安卓、web、后端的应用开发的一些东西了。后来准备开始刷题是大四毕业的时候了吧。 + +当时对回溯、动态规划也都只是上课的时候学过,也并不熟练。开始几题的时候,也都很慢,很多都自己想不出来。然后就去看别人的题解。看完以后,就什么都不看,然后按自己的思路再写一遍代码。 + +尤其是[第 5 题](),求最长回文序列,现在都印象深刻,记得当时用了好几天才把所有解法总结了出来。 + +所以大家如果想刷题的话,也不用怕自己基础不好,大不了哪些名词不会就去查,一点点积累就可以,重要的是**开始**和**坚持**。 + +# 现在的感觉 + +从开始可能只是觉得该刷一刷题,到现在可能真的是爱上了刷题。 + +现在刷题基本可以想出一种思路,有时候甚至和最优解想到了一起,还会想出一些别人没有想到的解法,这种成就感可能就是打游戏超神的感觉吧,哈哈。 + +此外,看 discuss 的时候,每当看到令人拍案称奇的思路的时候,真的是让人心旷神怡,开心的不得了,就像中了彩票一样的开心,赶快去和同学分享。 + +有时候也会看到一些让人捧腹的评论,题目是输入一个字符串,输出所有可能的 ip 地址。 + +```java +Input: "25525511135" +Output: ["255.255.11.135","255.255.111.35"] +``` + +![](https://windliang.oss-cn-beijing.aliyuncs.com/93_2.png) + +![](https://windliang.oss-cn-beijing.aliyuncs.com/93_3.jpg) + +# 刷题的收获 + +在总结的过程中,因为力求给他人讲懂,在理清思路的动机的过程中,会发现之前的想法可能是错的,会总结着总结着就明白了另一种解法,或者产生新的想法,或者明白各个解法相互之间的联系,会比仅仅 AC 多出很多收获。 + +从理清他人的想法,再到自己写出代码,再到把各个解法用自己的理解串起来,会有一种「纸上得来终觉浅,绝知此事要躬行」的感觉。有时候虽然大的框架有了,但是小的细节方面还是需要自己去体会。为什么加这个 if?为什么是小于等于?每一句代码的产生都是有原因的,绝不会是可有可无的代码。 + +所以虽然一道题从看题,理解,自己考虑,看别人解法,到重新实现,再到总结出来,可能需要 3、4 个小时,甚至 5、6 个小时或者更多,但我觉得是值得的。 + +此外,也有很多人加自己的微信过来亦或是感谢自己,亦或是指出错误,亦或是询问问题,亦或是没说过话的,哈哈。有微软、谷歌、百度、阿里、腾讯的大佬,有各个大学的学生,甚至巧的是还能加上高中的校友,世界真小,哈哈。 + +![](https://windliangblog.oss-cn-beijing.aliyuncs.com/addchat1.jpg) + +![](https://windliangblog.oss-cn-beijing.aliyuncs.com/addchat2.jpg) + +![](https://windliangblog.oss-cn-beijing.aliyuncs.com/addchat3.jpg) + +![](https://windliangblog.oss-cn-beijing.aliyuncs.com/addchat4.jpg) + +上边是最近加的一些人,每次收到别人的称赞自己也会很开心。此外,博客是直接放在 github 上的,目前也有 280 stars 了,是自己 github 上 star 数最多的项目了,说来也惭愧,希望以后自己努力可以有一个好的开源项目。 + +# 刷题的理解 + +一些人可能会纠结用什么语言去刷,其实没必要纠结的。刷题需要考虑的是算法,而不是语言。算法就像是从家里到超市该怎么走?出门左拐,右拐直走....而语言是我们选择的交通工具,骑车?步行?开车?平衡车?每种交通工具都有自己的优点和缺点,语言也是如此。而好的算法可能更像是,我们偶然发现了一条近路,降低了我们的时间复杂度或者是空间复杂度。 + +刷了 100 道题了,我觉得必须要掌握的就是递归的思想了,利用这个思想可以解大部分的题了。计算机擅长的就是记忆以及速度,而递归可以把这两个优势发挥到极致。遇到问题以后,我们可以考虑如何把大问题分解成小问题,想出来以后,代码很容易就出来了。 + +此外,一些递归可以用动态规划的思想改写,从而优化递归压栈所消耗的时间,递归是顶部到底部再回到顶部,而动态规划通过存储,直接从底部到顶部解决问题。 + +最经典的例子就是斐波那契数列了,求第 n 项数列的值。 + +> 斐波那契数列,指的是这样一个数列:1、1、2、3、5、8、13、21、34 …… 在数学上,斐波纳契数列定义如下:F ( 0 ) = 0,F ( 1 ) = 1 , F ( n ) = F ( n - 1 ) + F ( n - 2 )(n >= 2,n ∈ N*); + +如果用递归的思想去写,代码简洁而优雅。 + +```java +long Fibonacci(int n){ + if (n == 0) + return 0; + else if (n == 1) + return 1; + else + return Fibonacci(n-1) + Fibonacci(n-2); +} +``` + +当然,这样的话太慢了,优化的话,就是把递归过程的结果保存起来,或者就是改写成动态规划,最强的是其实是有一个公式的,直接利用公式就可以。 + +此外,还有一些题目就是根据题目的理解去写代码了,没有什么特殊的技巧。 + +# 未来的打算 + +当然是继续刷下去了,很开心,每天不刷一刷题会不习惯的,希望大家也早日感受到刷题的乐趣,哈哈。 + +在线地址:[https://leetcode.wang](https://leetcode.wang),域名也比较好记,希望对大家会有帮助。 + +我是用 gitbook 搭建的,我觉得上边「搜索」的插件很好用,可以直接根据关键字搜出来自己想做的题。 + +![](https://windliangblog.oss-cn-beijing.aliyuncs.com/search.jpg) + +知乎专栏也会同步更新:[]()。 + +越努力,越幸运,共勉。 diff --git "a/leetcode\345\212\233\346\211\243\345\210\267\351\242\2301\345\210\260300\347\232\204\346\204\237\345\217\227.md" "b/leetcode\345\212\233\346\211\243\345\210\267\351\242\2301\345\210\260300\347\232\204\346\204\237\345\217\227.md" index c5f4f517b..f6bd520cb 100644 --- "a/leetcode\345\212\233\346\211\243\345\210\267\351\242\2301\345\210\260300\347\232\204\346\204\237\345\217\227.md" +++ "b/leetcode\345\212\233\346\211\243\345\210\267\351\242\2301\345\210\260300\347\232\204\346\204\237\345\217\227.md" @@ -1,105 +1,105 @@ -# 回顾 - -自己也不是 `ACMer`,在大一暑假的时候学校组织过 `ACM` 集训,但无奈自己当时底子太差,连栈、队列这些基础的数据结构也不懂,觉得刷这些题很无聊,然后就不了了之了。如果你是大一,接触到了 `ACM` ,可以多试试,如果 `ACM` 拿些奖,找工作基本上是没问题了。 - -后来有了些编程的基础后,才慢慢体会到刷题的乐趣。第一道题是 `18` 年 `7` 月本科毕业那个暑假总结的,当时写在 [windliang.wang](https://windliang.wang/) 这个博客上。 - -![](https://windliangblog.oss-cn-beijing.aliyuncs.com/300leetcode1.jpg) - -期间边刷题边熟悉一些常用的技巧, `HashMap`、二进制操作、回溯法、分治法、`memoization` 、动态规划等等,逐渐有了刷题的感觉,渐渐的也爱上了刷题。差不多过了一年,有了这篇 [leetcode 100 斩!回顾](https://zhuanlan.zhihu.com/p/73146252)。 - -在博客总结了几道题以后,为了防止博客文章的刷屏,也为了更好的翻阅题目,自己通过 `gitbook` 这个框架重新整理了题解,使用了自己的二级域名 [leetcode.windliang.cc](http://leetcode.windliang.cc/),再后来为了方便统计等功能,买了新域名 [leetcode.wang](https://leetcode.wang/)。前段时间因为 `github` 的 `pages` 服务在国内不稳定,将博客迁移到了阿里云上,详细过程可以参考 [这里](https://zhuanlan.zhihu.com/p/108720935)。最终的博客就是下边的样子了。 - -![](https://windliangblog.oss-cn-beijing.aliyuncs.com/300leetcode2.jpg) - - - -现在差不多快两年了,从本科毕业到了研究生毕业,顺序刷到了 `300` 题,当然其中的付费题和 `SQL` 的题跳过了。每道题先自己写,写完以后会逛 `discuss` 区的第一页,学习别人的思路,然后再自己写一遍代码,最后按照自己的理解进行了详细的总结,这种刷题速度虽然慢,但我觉得有下边的好处。 - -# 总结的好处 - -第一个就是总结一遍会加深自己的印象,当用到一个之前用过的思路,结合一些关键词很快就能找到之前是哪道题,然后可以再比对这些题的异同点。同样,也可以方便自己以后的查找,更快的想起当时的思路。 - -第二个的话,可以对不同的算法之间的联系有更深的体会,从递归,到递归加 `memoization`,再到动态规划,最后进行动态规划空间复杂度的优化,用到的分治、回溯、动态规划会发现它们本质上其实是一样的,现在都对 [115 题](https://leetcode.wang/leetcode-115-Distinct-Subsequences.html) 印象深刻。 - -一些常见的问题也会帮助自己查漏补缺,比如二叉树的中序遍历,在 [94 题](https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html) 我才知道原来还有 `Morris Traversal`,可以使得中序遍历的空间复杂度降为 `O(1)`。还有一些大神们的解法,印象最深刻的就属 [第 5 题](https://leetcode.wang/leetCode-5-Longest-Palindromic-Substring.html) 的马拉车算法了。 - -第三个的话,因为你想让别人明白你的想法,你会不停的去思考自己的解法,力求每一步都是清晰的,有时候虽然已经是 `AC` 的解法,总结着总结着会发现自己的思路其实是错的,只是 `LeetCode` 的 `test cases` 没有覆盖而已。 - -第四个的话,就是可以和别人交流,在交流过程中你又会加深一些算法的理解。比如常见的二分,印象最深的就是和 [@为爱卖小菜](https://leetcode-cn.com/u/wei-ai-mai-xiao-cai/) 讨论的一个问题,「在二分查找的时候, `while` 里面的 `low` 和 `high` 的关系,为什么有时候取等号有时候又不取等号」,当时两个人为了这个问题讨论了好久。这个问题看起来好像没什么,但当你真正去思考的话,一定会收获良多。 - -另外,别人也会指出你解法的问题,和第三点一样,有时候 `AC` 了,但依旧可能存在问题。当然也有可能是 `LeetCode` 改了函数,所以之前的代码无法通过了。 - -第五个的话,就是成就感了,来源于两处。一个的话就是自己绞尽脑汁,几个小时甚至几天后彻底理解一个解法的那一刻,另一个就是很多人去称赞你、感谢你的时候。在力扣中国站自己的多篇文章都被标为了精选题解,最开始发的 [第 5 题](https://leetcode-cn.com/problems/longest-palindromic-substring/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-bao-gu/) 竟然已经有 `132k` 的浏览量了。 - -![](https://windliangblog.oss-cn-beijing.aliyuncs.com/300leetcode3.jpg) - -目前 [github](https://github.com/wind-liang/leetcode) 也有 `1.1k` 的 `stars`,知乎专栏 [LeetCode刷题](https://zhuanlan.zhihu.com/leetcode1024) 也有 `1.5k+ ` 的关注量。之前刷到两百题的时候发到曹大的星球还被曹大赞赏加精选,当时太激动了。曹大的公众号是「caoz的梦呓」,自己的偶像之一,大家可以关注一下。 - -这些正激励会让自己更有动力坚持下去。 - -# 开始刷题的疑惑 - -## 什么样的基础才能刷题? - -对于前 `90` 题的话,只需要了解一门语言,知道变量定义、判断语句,循环语句,定义函数,递归。了解基本的数据结构,顺序表、链表、栈、队列、哈希表,就可以开始刷题了。 - -到了 `94` 题出现了二叉树,需要知道深度优先遍历、广度优先遍历。后边个别题也会用到图,但不多。 - -期间很多题目也涉及到很多二进制的操作,也需要一些补码的知识,可以参考我之前总结的 [趣谈计算机补码](https://zhuanlan.zhihu.com/p/67227136)。 - -期间也会遇到很多自己之前不了解的数据结构,比如优先队列,`TreeMap`、线段树、并查集、前缀树等等,这些的话也不用急于了解,遇到的话再开始学习也不迟。 - -前 `300` 题的话,大致有三种类型。第一种只需要理解题目,然后模拟题目的过程就可以求解。第二种的话,可以用一些通用的思想求解,分治法、回溯法、动态规划等,贪心用的比较少。第三种的话,会涉及到一些数学的公式,能大大提高算法的性能,但如果之前不知道的话一般情况下是想不到的。 - -## 按照什么顺序刷题? - -如果刚接触编程,可以按照题目难度来,先多刷一些 `easy` 难度的,熟悉一下刷题的流程。也有人是通过专题刷的,比如动态规划专题,所有的题目都可以通过动态规划来解决。我觉得这样不是很好,因为这样的话失去了一个自己分析题目、选取方法的过程,遇到新题有时候还是不知道该怎么下手。 - -所以如果时间充足的话,可以随机刷题,或者像我一样顺序刷,这样对一些常用的思路会慢慢加深然后固化。 - -## 选哪门语言刷? - -不用纠结,不用纠结,不用纠结,随便一门都可以。之前的 [leetcode 100 斩!回顾](https://zhuanlan.zhihu.com/p/73146252) 这里也就讲过。 - -要想清楚语言和算法之间的关系。 - -算法就像是从家里到超市该怎么走?出门左拐,直走后右拐....起着指导性的作用。 - -语言是我们选择的交通工具,骑车?步行?开车?平衡车?每种交通工具都有自己的优点和缺点,语言也是如此。 - -好的算法可能更像是,我们偶然发现了一条近路,降低了我们的时间复杂度或者是空间复杂度。 - -所以其实并不需要纠结,选择自己熟悉的一门语言即可。更多关于语言之间的关系可以参考 [到底学哪一门编程语言](https://zhuanlan.zhihu.com/p/90440843)。 - -我选 `java` 的主要原因是,`java` 属于强类型语言,这样写出来的解法会更易读些。如果有其他语言的基础,`java` 基本不用学也能读懂个大概。 - -## 刷题和算法岗有关系吗? - -据我了解没啥关系,算法岗的话目前主要指的是深度学习,而刷题锻炼的是一种基础能力。可以增强你的逻辑能力和动手能力,当有一个想法的时候,可以快速通过编程实现的一种能力。 - -还有就是一些基础的数据结构和算法也必须是了解的,二叉树、图、广度优先遍历、深度优先遍历等等,在工程实践中会看到它们的影子。 - -## 只刷题能找到工作吗? - -在美国可能可以,在国内的话有点儿难。国内除了基本的刷题,还需要了解自己岗位(前端、后端、算法等)的相关知识,可以牛客网看看面经了解个大概,还有就是有一些自己做过的项目,面试官会从你做的项目中问一些相关知识点。 - -## 总结花费的时间 - -拿我个人来说,花费的时间取决于题目的难度。如果比较简单,`1` 到 `2` 个小时就可以完成一篇总结。如果遇到解法比较多的题目,有时候可能要花费七八个小时了,第一天把所有的解法理通,第二天把解法总结下来。 - -# 未来的计划 - -刷题总结已经快两年了,以后还会继续下去,但更新频率会降低了。 - -一方面自己马上毕业要进入工作了,供自己支配的时间会变少,总结确实需要花费不少时间,有的题目一篇文章下来甚至需要七八个小时,未来更多的精力会放在前端领域上。 - -另一方面,就是刷题带来的新鲜感没有前 `100` 题的时候那么频繁了,只会偶尔碰到几个新的思路,大部分的思路、技巧在之前的题目已经见过了。 - -之前都是用 `java` 写的代码,未来会改成 `JavaScript` 了,因为我的工作是前端,想不到吧,哈哈,好多人知道后都发出了疑问,之前也总结过一篇原因,参考 [面完腾讯阿里后对人生的思考](https://zhuanlan.zhihu.com/p/99181212)。`js` 会尽量多用 `ES6` 的语法,之前确实用的比较少。 - -另外,大家有问题的话可以和我一起探讨,最好是我总结过的题目,不然新题我可能也不会,哈哈。希望是那种你已经经过各种调试,网上各种搜寻还是解决不了的问题,这样一起讨论的话才更有意义些。不然的话,可能只是我帮你调试、谷歌,仅仅锻炼了我的能力。 - -刷题博客地址是 [leetcode.wang](https://leetcode.wang/),知乎专栏是 [LeetCode刷题](https://zhuanlan.zhihu.com/leetcode1024),欢迎 `star`、关注,哈哈。 - +# 回顾 + +自己也不是 `ACMer`,在大一暑假的时候学校组织过 `ACM` 集训,但无奈自己当时底子太差,连栈、队列这些基础的数据结构也不懂,觉得刷这些题很无聊,然后就不了了之了。如果你是大一,接触到了 `ACM` ,可以多试试,如果 `ACM` 拿些奖,找工作基本上是没问题了。 + +后来有了些编程的基础后,才慢慢体会到刷题的乐趣。第一道题是 `18` 年 `7` 月本科毕业那个暑假总结的,当时写在 [windliang.wang](https://windliang.wang/) 这个博客上。 + +![](https://windliangblog.oss-cn-beijing.aliyuncs.com/300leetcode1.jpg) + +期间边刷题边熟悉一些常用的技巧, `HashMap`、二进制操作、回溯法、分治法、`memoization` 、动态规划等等,逐渐有了刷题的感觉,渐渐的也爱上了刷题。差不多过了一年,有了这篇 [leetcode 100 斩!回顾](https://zhuanlan.zhihu.com/p/73146252)。 + +在博客总结了几道题以后,为了防止博客文章的刷屏,也为了更好的翻阅题目,自己通过 `gitbook` 这个框架重新整理了题解,使用了自己的二级域名 [leetcode.windliang.cc](http://leetcode.windliang.cc/),再后来为了方便统计等功能,买了新域名 [leetcode.wang](https://leetcode.wang/)。前段时间因为 `github` 的 `pages` 服务在国内不稳定,将博客迁移到了阿里云上,详细过程可以参考 [这里](https://zhuanlan.zhihu.com/p/108720935)。最终的博客就是下边的样子了。 + +![](https://windliangblog.oss-cn-beijing.aliyuncs.com/300leetcode2.jpg) + + + +现在差不多快两年了,从本科毕业到了研究生毕业,顺序刷到了 `300` 题,当然其中的付费题和 `SQL` 的题跳过了。每道题先自己写,写完以后会逛 `discuss` 区的第一页,学习别人的思路,然后再自己写一遍代码,最后按照自己的理解进行了详细的总结,这种刷题速度虽然慢,但我觉得有下边的好处。 + +# 总结的好处 + +第一个就是总结一遍会加深自己的印象,当用到一个之前用过的思路,结合一些关键词很快就能找到之前是哪道题,然后可以再比对这些题的异同点。同样,也可以方便自己以后的查找,更快的想起当时的思路。 + +第二个的话,可以对不同的算法之间的联系有更深的体会,从递归,到递归加 `memoization`,再到动态规划,最后进行动态规划空间复杂度的优化,用到的分治、回溯、动态规划会发现它们本质上其实是一样的,现在都对 [115 题](https://leetcode.wang/leetcode-115-Distinct-Subsequences.html) 印象深刻。 + +一些常见的问题也会帮助自己查漏补缺,比如二叉树的中序遍历,在 [94 题](https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html) 我才知道原来还有 `Morris Traversal`,可以使得中序遍历的空间复杂度降为 `O(1)`。还有一些大神们的解法,印象最深刻的就属 [第 5 题](https://leetcode.wang/leetCode-5-Longest-Palindromic-Substring.html) 的马拉车算法了。 + +第三个的话,因为你想让别人明白你的想法,你会不停的去思考自己的解法,力求每一步都是清晰的,有时候虽然已经是 `AC` 的解法,总结着总结着会发现自己的思路其实是错的,只是 `LeetCode` 的 `test cases` 没有覆盖而已。 + +第四个的话,就是可以和别人交流,在交流过程中你又会加深一些算法的理解。比如常见的二分,印象最深的就是和 [@为爱卖小菜](https://leetcode-cn.com/u/wei-ai-mai-xiao-cai/) 讨论的一个问题,「在二分查找的时候, `while` 里面的 `low` 和 `high` 的关系,为什么有时候取等号有时候又不取等号」,当时两个人为了这个问题讨论了好久。这个问题看起来好像没什么,但当你真正去思考的话,一定会收获良多。 + +另外,别人也会指出你解法的问题,和第三点一样,有时候 `AC` 了,但依旧可能存在问题。当然也有可能是 `LeetCode` 改了函数,所以之前的代码无法通过了。 + +第五个的话,就是成就感了,来源于两处。一个的话就是自己绞尽脑汁,几个小时甚至几天后彻底理解一个解法的那一刻,另一个就是很多人去称赞你、感谢你的时候。在力扣中国站自己的多篇文章都被标为了精选题解,最开始发的 [第 5 题](https://leetcode-cn.com/problems/longest-palindromic-substring/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-bao-gu/) 竟然已经有 `132k` 的浏览量了。 + +![](https://windliangblog.oss-cn-beijing.aliyuncs.com/300leetcode3.jpg) + +目前 [github](https://github.com/wind-liang/leetcode) 也有 `1.1k` 的 `stars`,知乎专栏 [LeetCode刷题](https://zhuanlan.zhihu.com/leetcode1024) 也有 `1.5k+ ` 的关注量。之前刷到两百题的时候发到曹大的星球还被曹大赞赏加精选,当时太激动了。曹大的公众号是「caoz的梦呓」,自己的偶像之一,大家可以关注一下。 + +这些正激励会让自己更有动力坚持下去。 + +# 开始刷题的疑惑 + +## 什么样的基础才能刷题? + +对于前 `90` 题的话,只需要了解一门语言,知道变量定义、判断语句,循环语句,定义函数,递归。了解基本的数据结构,顺序表、链表、栈、队列、哈希表,就可以开始刷题了。 + +到了 `94` 题出现了二叉树,需要知道深度优先遍历、广度优先遍历。后边个别题也会用到图,但不多。 + +期间很多题目也涉及到很多二进制的操作,也需要一些补码的知识,可以参考我之前总结的 [趣谈计算机补码](https://zhuanlan.zhihu.com/p/67227136)。 + +期间也会遇到很多自己之前不了解的数据结构,比如优先队列,`TreeMap`、线段树、并查集、前缀树等等,这些的话也不用急于了解,遇到的话再开始学习也不迟。 + +前 `300` 题的话,大致有三种类型。第一种只需要理解题目,然后模拟题目的过程就可以求解。第二种的话,可以用一些通用的思想求解,分治法、回溯法、动态规划等,贪心用的比较少。第三种的话,会涉及到一些数学的公式,能大大提高算法的性能,但如果之前不知道的话一般情况下是想不到的。 + +## 按照什么顺序刷题? + +如果刚接触编程,可以按照题目难度来,先多刷一些 `easy` 难度的,熟悉一下刷题的流程。也有人是通过专题刷的,比如动态规划专题,所有的题目都可以通过动态规划来解决。我觉得这样不是很好,因为这样的话失去了一个自己分析题目、选取方法的过程,遇到新题有时候还是不知道该怎么下手。 + +所以如果时间充足的话,可以随机刷题,或者像我一样顺序刷,这样对一些常用的思路会慢慢加深然后固化。 + +## 选哪门语言刷? + +不用纠结,不用纠结,不用纠结,随便一门都可以。之前的 [leetcode 100 斩!回顾](https://zhuanlan.zhihu.com/p/73146252) 这里也就讲过。 + +要想清楚语言和算法之间的关系。 + +算法就像是从家里到超市该怎么走?出门左拐,直走后右拐....起着指导性的作用。 + +语言是我们选择的交通工具,骑车?步行?开车?平衡车?每种交通工具都有自己的优点和缺点,语言也是如此。 + +好的算法可能更像是,我们偶然发现了一条近路,降低了我们的时间复杂度或者是空间复杂度。 + +所以其实并不需要纠结,选择自己熟悉的一门语言即可。更多关于语言之间的关系可以参考 [到底学哪一门编程语言](https://zhuanlan.zhihu.com/p/90440843)。 + +我选 `java` 的主要原因是,`java` 属于强类型语言,这样写出来的解法会更易读些。如果有其他语言的基础,`java` 基本不用学也能读懂个大概。 + +## 刷题和算法岗有关系吗? + +据我了解没啥关系,算法岗的话目前主要指的是深度学习,而刷题锻炼的是一种基础能力。可以增强你的逻辑能力和动手能力,当有一个想法的时候,可以快速通过编程实现的一种能力。 + +还有就是一些基础的数据结构和算法也必须是了解的,二叉树、图、广度优先遍历、深度优先遍历等等,在工程实践中会看到它们的影子。 + +## 只刷题能找到工作吗? + +在美国可能可以,在国内的话有点儿难。国内除了基本的刷题,还需要了解自己岗位(前端、后端、算法等)的相关知识,可以牛客网看看面经了解个大概,还有就是有一些自己做过的项目,面试官会从你做的项目中问一些相关知识点。 + +## 总结花费的时间 + +拿我个人来说,花费的时间取决于题目的难度。如果比较简单,`1` 到 `2` 个小时就可以完成一篇总结。如果遇到解法比较多的题目,有时候可能要花费七八个小时了,第一天把所有的解法理通,第二天把解法总结下来。 + +# 未来的计划 + +刷题总结已经快两年了,以后还会继续下去,但更新频率会降低了。 + +一方面自己马上毕业要进入工作了,供自己支配的时间会变少,总结确实需要花费不少时间,有的题目一篇文章下来甚至需要七八个小时,未来更多的精力会放在前端领域上。 + +另一方面,就是刷题带来的新鲜感没有前 `100` 题的时候那么频繁了,只会偶尔碰到几个新的思路,大部分的思路、技巧在之前的题目已经见过了。 + +之前都是用 `java` 写的代码,未来会改成 `JavaScript` 了,因为我的工作是前端,想不到吧,哈哈,好多人知道后都发出了疑问,之前也总结过一篇原因,参考 [面完腾讯阿里后对人生的思考](https://zhuanlan.zhihu.com/p/99181212)。`js` 会尽量多用 `ES6` 的语法,之前确实用的比较少。 + +另外,大家有问题的话可以和我一起探讨,最好是我总结过的题目,不然新题我可能也不会,哈哈。希望是那种你已经经过各种调试,网上各种搜寻还是解决不了的问题,这样一起讨论的话才更有意义些。不然的话,可能只是我帮你调试、谷歌,仅仅锻炼了我的能力。 + +刷题博客地址是 [leetcode.wang](https://leetcode.wang/),知乎专栏是 [LeetCode刷题](https://zhuanlan.zhihu.com/leetcode1024),欢迎 `star`、关注,哈哈。 + 最后,越努力,越幸运,共勉。 \ No newline at end of file diff --git a/more.md b/more.md index 1006ef030..20a9701df 100644 --- a/more.md +++ b/more.md @@ -1,15 +1,15 @@ -# 持续更新中 - -为什么刷题:https://leetcode.wang/leetcode100%E6%96%A9%E5%9B%9E%E9%A1%BE.html - -知乎开设了专栏,同步更新:[https://zhuanlan.zhihu.com/leetcode1024](https://zhuanlan.zhihu.com/leetcode1024),关注后可以及时收到更新,网站有时候可能出问题打不开,建议关注一下知乎专栏备用。 - -准备刷一道,总结一道,目前事情比较多,更新频率会低一些,感谢支持。 - -300 题以后的打算,参考 leetcode 力扣刷题 1 到 300 的感受。 - -可以加好友一起交流 - -微信: 17771420231 - +# 持续更新中 + +为什么刷题:https://leetcode.wang/leetcode100%E6%96%A9%E5%9B%9E%E9%A1%BE.html + +知乎开设了专栏,同步更新:[https://zhuanlan.zhihu.com/leetcode1024](https://zhuanlan.zhihu.com/leetcode1024),关注后可以及时收到更新,网站有时候可能出问题打不开,建议关注一下知乎专栏备用。 + +准备刷一道,总结一道,目前事情比较多,更新频率会低一些,感谢支持。 + +300 题以后的打算,参考 leetcode 力扣刷题 1 到 300 的感受。 + +可以加好友一起交流 + +微信: 17771420231 + 公众号: windliang,更新编程相关 \ No newline at end of file diff --git a/node_modules/.bin/katex b/node_modules/.bin/katex index 8ac118b1e..8d323e542 100644 --- a/node_modules/.bin/katex +++ b/node_modules/.bin/katex @@ -1,15 +1,15 @@ -#!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") - -case `uname` in - *CYGWIN*) basedir=`cygpath -w "$basedir"`;; -esac - -if [ -x "$basedir/node" ]; then - "$basedir/node" "$basedir/../katex/cli.js" "$@" - ret=$? -else - node "$basedir/../katex/cli.js" "$@" - ret=$? -fi -exit $ret +#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*) basedir=`cygpath -w "$basedir"`;; +esac + +if [ -x "$basedir/node" ]; then + "$basedir/node" "$basedir/../katex/cli.js" "$@" + ret=$? +else + node "$basedir/../katex/cli.js" "$@" + ret=$? +fi +exit $ret diff --git a/node_modules/.bin/katex.cmd b/node_modules/.bin/katex.cmd index 21ebd856b..d75b53f90 100644 --- a/node_modules/.bin/katex.cmd +++ b/node_modules/.bin/katex.cmd @@ -1,7 +1,7 @@ -@IF EXIST "%~dp0\node.exe" ( - "%~dp0\node.exe" "%~dp0\..\katex\cli.js" %* -) ELSE ( - @SETLOCAL - @SET PATHEXT=%PATHEXT:;.JS;=;% - node "%~dp0\..\katex\cli.js" %* +@IF EXIST "%~dp0\node.exe" ( + "%~dp0\node.exe" "%~dp0\..\katex\cli.js" %* +) ELSE ( + @SETLOCAL + @SET PATHEXT=%PATHEXT:;.JS;=;% + node "%~dp0\..\katex\cli.js" %* ) \ No newline at end of file diff --git a/node_modules/boolbase/README.md b/node_modules/boolbase/README.md index 85eefa5e5..8f2037723 100644 --- a/node_modules/boolbase/README.md +++ b/node_modules/boolbase/README.md @@ -1,10 +1,10 @@ -#boolbase -This very simple module provides two basic functions, one that always returns true (`trueFunc`) and one that always returns false (`falseFunc`). - -###WTF? - -By having only a single instance of these functions around, it's possible to do some nice optimizations. Eg. [`CSSselect`](https://github.com/fb55/CSSselect) uses these functions to determine whether a selector won't match any elements. If that's the case, the DOM doesn't even have to be touched. - -###And why is this a separate module? - +#boolbase +This very simple module provides two basic functions, one that always returns true (`trueFunc`) and one that always returns false (`falseFunc`). + +###WTF? + +By having only a single instance of these functions around, it's possible to do some nice optimizations. Eg. [`CSSselect`](https://github.com/fb55/CSSselect) uses these functions to determine whether a selector won't match any elements. If that's the case, the DOM doesn't even have to be touched. + +###And why is this a separate module? + I'm trying to modularize `CSSselect` and most modules depend on these functions. IMHO, having a separate module is the easiest solution to this problem. \ No newline at end of file diff --git a/node_modules/boolbase/index.js b/node_modules/boolbase/index.js index 8799fd95d..62a2e7bdf 100644 --- a/node_modules/boolbase/index.js +++ b/node_modules/boolbase/index.js @@ -1,8 +1,8 @@ -module.exports = { - trueFunc: function trueFunc(){ - return true; - }, - falseFunc: function falseFunc(){ - return false; - } +module.exports = { + trueFunc: function trueFunc(){ + return true; + }, + falseFunc: function falseFunc(){ + return false; + } }; \ No newline at end of file diff --git a/node_modules/boolbase/package.json b/node_modules/boolbase/package.json index d11208280..ee3a46aa7 100644 --- a/node_modules/boolbase/package.json +++ b/node_modules/boolbase/package.json @@ -1,82 +1,82 @@ -{ - "_args": [ - [ - { - "name": "boolbase", - "raw": "boolbase@~1.0.0", - "rawSpec": "~1.0.0", - "scope": null, - "spec": ">=1.0.0 <1.1.0", - "type": "range" - }, - "E:\\WEB\\leetcode\\node_modules\\css-select" - ] - ], - "_from": "boolbase@>=1.0.0 <1.1.0", - "_id": "boolbase@1.0.0", - "_inCache": true, - "_installable": true, - "_location": "/boolbase", - "_npmUser": { - "email": "me@feedic.com", - "name": "feedic" - }, - "_npmVersion": "1.4.2", - "_phantomChildren": {}, - "_requested": { - "name": "boolbase", - "raw": "boolbase@~1.0.0", - "rawSpec": "~1.0.0", - "scope": null, - "spec": ">=1.0.0 <1.1.0", - "type": "range" - }, - "_requiredBy": [ - "/css-select", - "/nth-check" - ], - "_resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "_shasum": "68dff5fbe60c51eb37725ea9e3ed310dcc1e776e", - "_shrinkwrap": null, - "_spec": "boolbase@~1.0.0", - "_where": "E:\\WEB\\leetcode\\node_modules\\css-select", - "author": { - "email": "me@feedic.com", - "name": "Felix Boehm" - }, - "bugs": { - "url": "https://github.com/fb55/boolbase/issues" - }, - "dependencies": {}, - "description": "two functions: One that returns true, one that returns false", - "devDependencies": {}, - "directories": {}, - "dist": { - "shasum": "68dff5fbe60c51eb37725ea9e3ed310dcc1e776e", - "tarball": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz" - }, - "homepage": "https://github.com/fb55/boolbase", - "keywords": [ - "boolean", - "function" - ], - "license": "ISC", - "main": "index.js", - "maintainers": [ - { - "email": "me@feedic.com", - "name": "feedic" - } - ], - "name": "boolbase", - "optionalDependencies": {}, - "readme": "ERROR: No README data found!", - "repository": { - "type": "git", - "url": "git+https://github.com/fb55/boolbase.git" - }, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "version": "1.0.0" -} +{ + "_args": [ + [ + { + "name": "boolbase", + "raw": "boolbase@~1.0.0", + "rawSpec": "~1.0.0", + "scope": null, + "spec": ">=1.0.0 <1.1.0", + "type": "range" + }, + "E:\\WEB\\leetcode\\node_modules\\css-select" + ] + ], + "_from": "boolbase@>=1.0.0 <1.1.0", + "_id": "boolbase@1.0.0", + "_inCache": true, + "_installable": true, + "_location": "/boolbase", + "_npmUser": { + "email": "me@feedic.com", + "name": "feedic" + }, + "_npmVersion": "1.4.2", + "_phantomChildren": {}, + "_requested": { + "name": "boolbase", + "raw": "boolbase@~1.0.0", + "rawSpec": "~1.0.0", + "scope": null, + "spec": ">=1.0.0 <1.1.0", + "type": "range" + }, + "_requiredBy": [ + "/css-select", + "/nth-check" + ], + "_resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "_shasum": "68dff5fbe60c51eb37725ea9e3ed310dcc1e776e", + "_shrinkwrap": null, + "_spec": "boolbase@~1.0.0", + "_where": "E:\\WEB\\leetcode\\node_modules\\css-select", + "author": { + "email": "me@feedic.com", + "name": "Felix Boehm" + }, + "bugs": { + "url": "https://github.com/fb55/boolbase/issues" + }, + "dependencies": {}, + "description": "two functions: One that returns true, one that returns false", + "devDependencies": {}, + "directories": {}, + "dist": { + "shasum": "68dff5fbe60c51eb37725ea9e3ed310dcc1e776e", + "tarball": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz" + }, + "homepage": "https://github.com/fb55/boolbase", + "keywords": [ + "boolean", + "function" + ], + "license": "ISC", + "main": "index.js", + "maintainers": [ + { + "email": "me@feedic.com", + "name": "feedic" + } + ], + "name": "boolbase", + "optionalDependencies": {}, + "readme": "ERROR: No README data found!", + "repository": { + "type": "git", + "url": "git+https://github.com/fb55/boolbase.git" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "version": "1.0.0" +} diff --git a/node_modules/cheerio/History.md b/node_modules/cheerio/History.md index c7e38e66a..b524d2cc4 100644 --- a/node_modules/cheerio/History.md +++ b/node_modules/cheerio/History.md @@ -1,576 +1,576 @@ - -0.22.0 / 2016-08-23 -================== - - * Return undefined in .prop if given an invalid element or tag (#880) - * Merge pull request #884 from cheeriojs/readme-cleanup - * readme updates - * Merge pull request #881 from piamancini/patch-1 - * Added backers and sponsors from OpenCollective - * Use jQuery from the jquery module in benchmarks (#871) - * Document, test, and extend static `$.text` method (#855) - * Fix typo on calling _.extend (#861) - * Update versions (#870) - * Use individual lodash functions (#864) - * Added `.serialize()` support. Fixes #69 (#827) - * Update Readme.md (#857) - * add extension for JSON require call - * remove gittask badge - * Merge pull request #672 from underdogio/dev/checkbox.radio.values.sqwished - * Added default value for checkboxes/radios - -0.20.0 / 2016-02-01 -================== - - * Add coveralls badge, remove link to old report (Felix Böhm) - * Update lodash dependeny to 4.1.0 (leif.hanack) - * Fix PR #726 adding 'appendTo()' and 'prependTo()' (Delgan) - * Added appendTo and prependTo with tests #641 (digihaven) - * Fix #780 by changing options context in '.find()' (Felix Böhm) - * Add an unit test checking the query of child (Delgan) - * fix #667: attr({foo: null}) removes attribute foo, like attr('foo', null) (Ray Waldin) - * Include reference to dedicated "Loading" section (Mike Pennisi) - * Added load method to $ (alanev) - * update css-select to 1.2.0 (Felix Böhm) - * Fixing Grammatical Error (Dan Corman) - * Test against node v0.12 --> v4.2 (Jason Kurian) - * Correct output in example (Felix Böhm) - * Fix npm files filter (Bogdan Chadkin) - * Enable setting data on all elements in selection (Mike Pennisi) - * Reinstate `$.fn.toArray` (Mike Pennisi) - * update css-select to 1.1.0 (Thomas Shafer) - * Complete implementation of `wrap` (Mike Pennisi) - * Correct name of unit test (Mike Pennisi) - * Correct grammar in test titles (Mike Pennisi) - * Normalize whitespace (Mike Pennisi) - * Insert omitted assertion (Mike Pennisi) - * Update invocation of `children` (Mike Pennisi) - * Begin implementation of `wrap` method (Dandlezzz) - * Update Readme.md (Sven Slootweg) - * fix document's mistake in Readme.md (exoticknight) - * Add tests for setting text and html as non-strings (Ryc O'Chet) - * Fix for passing non-string values to .html or .text (Ryc O'Chet) - * use a selector to filter form elements (fb55) - * fix README.md typo (Yutian Li) - * README: fix spelling (Chris Rebert) - * Added support for options without a `value` attribute. Fixes #633 (Todd Wolfson) - * responding to pull request feedback - remove item() method and related tests (Ray Waldin) - * add length property and item method to object returned by prop('style'), plus tests (Ray Waldin) - * Added .prop method to readme (Artem Burtsev) - * Added .prop method (Artem Burtsev) - * Added Gitter badge (The Gitter Badger) - -0.19.0 / 2015-03-21 -================== - - * fixed allignment (fb55) - * added test case for malformed json in data attributes (fb55) - * fix: handle some extreme cases like `data-custom="{{templatevar}}"`. There is possibility error while parsing json . (Harish.K) - * Add missing optional selector doc for {prev,next}{All,Until} (Jérémie Astori) - * update to dom-serializer@0.1.0 (Felix Böhm) - * Document `Cheerio#serialzeArray` (Mike Pennisi) - * Fixed up `serializeArray()` and added multiple support (Todd Wolfson) - * Implement serializeArray() (Jarno Leppänen) - * recognize options in $.xml() (fb55) - * lib/static.js: text(): rm errant space before ++ (Chris Rebert) - * Do not expose internal `children` array (Mike Pennisi) - * Change lodash dependencies to ^3.1.0 (Samy Pessé) - * Update lodash@3.1.0 (Samy Pessé) - * Updates Readme.md: .not(function (index, elem)) (Patrick Ward) - * update to css-select@1.0.0 (fb55) - * Allow failures in Node.js v0.11 (Mike Pennisi) - * Added: Gittask badge (Matthew Mueller) - * Isolate prototypes of functions created via `load` (Mike Pennisi) - * Updates Readme.md: adds JS syntax highlighting (frankcash) - * #608 -- Add support for insertBefore/insertAfter syntax. Supports target types of: $, [$], selector (both single and multiple results) (Ben Cochran) - * Clone input nodes when inserting over a set (Mike Pennisi) - * Move unit test files (Mike Pennisi) - * remove unnecessarily tricky code (David Chambers) - * pass options to $.html in toString (fb55) - * add license info to package.json (Chris Rebert) - * xyz@~0.5.0 (David Chambers) - * Remove unofficial signature of `children` (Mike Pennisi) - * Fix bug in `css` method (Mike Pennisi) - * Correct bug in implementation of `Cheerio#val` (Mike Pennisi) - -0.18.0 / 2014-11-06 -================== - - * bump htmlparser2 dependency to ~3.8.1 (Chris Rebert) - * Correct unit test titles (Mike Pennisi) - * Correct behavior of `after` and `before` (Mike Pennisi) - * implement jQuery's .has() (Chris Rebert) - * Update repository url (haqii) - * attr() should return undefined or name for booleans (Raoul Millais) - * Update Readme.md (Ryan Breen) - * Implement `Cheerio#not` (Mike Pennisi) - * Clone nodes according to original parsing options (Mike Pennisi) - * fix lint error (David Chambers) - * Add explicit tests for DOM level 1 API (Mike Pennisi) - * Expose DOM level 1 API for Node-like objects (Mike Pennisi) - * Correct error in documentation (Mike Pennisi) - * Return a fully-qualified Function from `$.load` (Mike Pennisi) - * Update tests to avoid duck typing (Mike Pennisi) - * Alter "loaded" functions to produce true instances (Mike Pennisi) - * Organize tests for `cheerio.load` (Mike Pennisi) - * Complete `$.prototype.find` (Mike Pennisi) - * Use JSHint's `extends` option (Mike Pennisi) - * Remove aliases for exported methods (Mike Pennisi) - * Disallow unused variables (Mike Pennisi) - * Remove unused internal variables (Mike Pennisi) - * Remove unused variables from unit tests (Mike Pennisi) - * Remove unused API method references (Mike Pennisi) - * Move tests for `contains` method (Mike Pennisi) - * xyz@0.4.0 (David Chambers) - * Created a wiki for companies using cheerio in production (Matthew Mueller) - * Implement `$.prototype.index` (Mike Pennisi) - * Implement `$.prototype.addBack` (Mike Pennisi) - * Added double quotes to radio attribute name to account for characters such as brackets (akant10) - * Update History.md (Gabriel Falkenberg) - * add 0.17.0 changelog (David Chambers) - * exit prepublish script if tag not found (David Chambers) - * alphabetize devDependencies (fb55) - * ignore coverage dir (fb55) - * submit coverage to coveralls (fb55) - * replace jscoverage with istanbul (fb55) - -0.17.0 / 2014-06-10 -================== - - * Fix bug in internal `uniqueSplice` function (Mike Pennisi) - * accept buffer argument to cheerio.load (David Chambers) - * Respect options on the element level (Alex Indigo) - * Change state definition to more readable (Artem Burtsev) - * added test (0xBADC0FFEE) - * add class only if doesn't exist (Artem Burtsev) - * Made it less insane. (Alex Indigo) - * Implement `Cheerio#add` (Mike Pennisi) - * Use "loaded" instance of Cheerio in unit tests (Mike Pennisi) - * Be more strict with object check. (Alex Indigo) - * Added options argument to .html() static method. (Alex Indigo) - * Fixed encoding mishaps. Adjusted tests. (Alex Indigo) - * use dom-serializer module (fb55) - * don't test on 0.8, don't ignore 0.11 (Felix Böhm) - * parse: rm unused variables (coderaiser) - * cheerio: rm unused variable (coderaiser) - * Fixed test (Avi Kohn) - * Added test (Avi Kohn) - * Changed == to === (Avi Kohn) - * Fixed a bug in removing type="hidden" attr (Avi Kohn) - * sorted (Alexey Raspopov) - * add `muted` attr to booleanAttributes (Alexey Raspopov) - * fixed context of `this` in .html (Felix Böhm) - * append new elements for each element in selection (fb55) - -0.16.0 / 2014-05-08 -================== - - * fix `make bench` (David Chambers) - * makefile: add release-* targets (David Chambers) - * alphabetize dependencies (David Chambers) - * Rewrite `data` internals with caching behavior (Mike Pennisi) - * Fence .val example as js (Kevin Sawicki) - * Fixed typos. Deleted trailing whitespace from test/render.js (Nattaphoom Ch) - * Fix manipulation APIs with removed elements (kpdecker) - * Perform manual string parsing for hasClass (kpdecker) - * Fix existing element removal (kpdecker) - * update render tests (Felix Böhm) - * fixed cheerio path (Felix Böhm) - * use `entities.escape` for attribute values (Felix Böhm) - * bump entities version (Felix Böhm) - * remove lowerCaseTags option from readme (Felix Böhm) - * added test case for .html in xmlMode (fb55) - * render xml in `html()` when `xmlMode: true` (fb55) - * use a map for booleanAttributes (fb55) - * update singleTags, use utils.isTag (fb55) - * update travis badge URL (Felix Böhm) - * use typeof instead of _.isString and _.isNumber (fb55) - * use Array.isArray instead of _.isArray (fb55) - * replace _.isFunction with typeof (fb55) - * removed unnecessary error message (fb55) - * decode entities in htmlparser2 (fb55) - * pass options object to CSSselect (fb55) - -0.15.0 / 2014-04-08 -================== - - * Update callbacks to pass element per docs (@kpdecker) - * preserve options (@fb55) - * Use SVG travis badge (@t3chnoboy) - * only use static requires (@fb55) - * Optimize manipulation methods (@kpdecker) - * Optimize add and remove class cases (@kpdecker) - * accept dom of DomHandler to cheerio.load (@nleush) - * added parentsUntil method (@finspin) - * Add performance optimization and bug fix `empty` method (@kpdecker) - -0.14.0 / 2014-04-01 -================== - - * call encodeXML and directly expose decodeHTML (@fb55) - * use latest htmlparser2 and entities versions (@fb55) - * Deprecate `$.fn.toArray` (@jugglinmike) - * Implement `$.fn.get` (@jugglinmike) - * .replaceWith now replaces all selected elements. (@xavi-) - * Correct arguments for 'replaceWith' callback (@jugglinmike) - * switch to lodash (@fb55) - * update to entities@0.5.0 (@fb55) - * Fix attr when $ collection contains text modules (@kpdecker) - * Update to latest version of expect.js (@jugglinmike) - * Remove nodes from their previous structures (@jugglinmike) - * Update render.js (@stevenvachon) - * CDATA test (@stevenvachon) - * only ever one child index for cdata (@stevenvachon) - * don't loop through cdata children array (@stevenvachon) - * proper rendering of CDATA (@stevenvachon) - * Add cheerio-only bench option (@kpdecker) - * Avoid delete operations (@kpdecker) - * Add independent html benchmark (@kpdecker) - * Cache tag check in render (@kpdecker) - * Simplify attribute rendering step (@kpdecker) - * Add html rendering bench case (@kpdecker) - * Remove unnecessary check from removeAttr (@kpdecker) - * Remove unnecessary encoding step for attrs (@kpdecker) - * Add test for removeAttr+attr on boolean attributes (@kpdecker) - * Add single element benchmark case (@kpdecker) - * Optimize filter with selector (@kpdecker) - * Fix passing context as dom node (@alfred-nsh) - * Fix bug in `nextUntil` (@jugglinmike) - * Fix bug in `nextAll` (@jugglinmike) - * Implement `selector` argument of `next` method (@jugglinmike) - * Fix bug in `prevUntil` (@jugglinmike) - * Implement `selector` argument of `prev` method (@jugglinmike) - * Fix bug in `prevAll` (@jugglinmike) - * Fix bug in `siblings` (@jugglinmike) - * Avoid unnecessary indexOf from toggleClass (@kpdecker) - * Use strict equality rather than falsy check in eq (@kpdecker) - * Add benchmark coverage for all $ APIs (@kpdecker) - * Optimize filter Cheerio intermediate creation (@kpdecker) - * Optimize siblings cheerio instance creation (@kpdecker) - * Optimize identity cases for first/last/eq (@kpdecker) - * Use domEach for traversal (@kpdecker) - * Inline children lookup in find (@kpdecker) - * Use domEach in data accessor (@kpdecker) - * Avoid cheerio creation in add/remove/toggleClass (@kpdecker) - * Implement getAttr local helper (@kpdecker) - -0.13.1 / 2014-01-07 -================== - - * Fix select with context in Cheerio function (@jugglinmike) - * Remove unecessary DOM maintenance logic (@jugglinmike) - * Deprecate support for node 0.6 - -0.13.0 / 2013-12-30 -================== - - * Remove "root" node (@jugglinmike) - * Fix bug in `prevAll`, `prev`, `nextAll`, `next`, `prevUntil`, `nextUntil` (@jugglinmike) - * Fix `replaceWith` method (@jugglinmike) - * added nextUntil() and prevUntil() (@finspin) - * Remove internal `connect` function (@jugglinmike) - * Rename `Cheerio#make` to document private status (@jugginmike) - * Remove extraneous call to `_.uniq` (@jugglinmike) - * Use CSSselect library directly (@jugglinmike) - * Run CI against Node v0.11 as an allowed failure (@jugginmike) - * Correct bug in `Cheerio#parents` (@jugglinmike) - * Implement `$.fn.end` (@jugginmike) - * Ignore colons inside of url(.*) when parsing css (@Meekohi) - * Introduce rudimentary benchmark suite (@jugglinmike) - * Update HtmlParser2 version (@jugglinmike) - * Correct inconsistency in `$.fn.map` (@jugglinmike) - * fixed traversing tests (@finspin) - * Simplify `make` method (@jugglinmike) - * Avoid shadowing instance methods from arrays (@jugglinmike) - -0.12.4 / 2013-11-12 -================== - - * Coerce JSON values returned by `data` (@jugglinmike) - * issue #284: when rendering HTML, use original data attributes (@Trott) - * Introduce JSHint for automated code linting (@jugglinmike) - * Prevent `find` from returning duplicate elements (@jugglinmike) - * Implement function signature of `replaceWith` (@jugglinmike) - * Implement function signature of `before` (@jugglinmike) - * Implement function signature of `after` (@jugglinmike) - * Implement function signature of `append`/`prepend` (@jugglinmike) - * Extend iteration methods to accept nodes (@jugglinmike) - * Improve `removeClass` (@jugglinmike) - * Complete function signature of `addClass` (@jugglinmike) - * Fix bug in `removeClass` (@jugglinmike) - * Improve contributing.md (@jugglinmike) - * Fix and document .css() (@jugglinmike) - -0.12.3 / 2013-10-04 -=================== - - * Add .toggleClass() function (@cyberthom) - * Add contributing guidelines (@jugglinmike) - * Fix bug in `siblings` (@jugglinmike) - * Correct the implementation `filter` and `is` (@jugglinmike) - * add .data() function (@andi-neck) - * add .css() (@yields) - * Implements contents() (@jlep) - -0.12.2 / 2013-09-04 -================== - - * Correct implementation of `$.fn.text` (@jugglinmike) - * Refactor Cheerio array creation (@jugglinmike) - * Extend manipulation methods to accept Arrays (@jugglinmike) - * support .attr(attributeName, function(index, attr)) (@xiaohwan) - -0.12.1 / 2013-07-30 -================== - - * Correct behavior of `Cheerio#parents` (@jugglinmike) - * Double quotes inside attributes kills HTML (@khoomeister) - * Making next({}) and prev({}) return empty object (@absentTelegraph) - * Implement $.parseHTML (@jugglinmike) - * Correct bug in jQuery.fn.closest (@jugglinmike) - * Correct behavior of $.fn.val on 'option' elements (@jugglinmike) - -0.12.0 / 2013-06-09 -=================== - - * Breaking Change: Changed context from parent to the actual passed one (@swissmanu) - * Fixed: jquery checkbox val behavior (@jhubble) - * Added: output xml with $.xml() (@Maciek416) - * Bumped: htmlparser2 to 3.1.1 - * Fixed: bug in attr(key, val) on empty objects (@farhadi) - * Added: prevAll, nextAll (@lessmind) - * Fixed: Safety check in parents and closest (@zero21xxx) - * Added: .is(sel) (@zero21xxx) - -0.11.0 / 2013-04-22 -================== - -* Added: .closest() (@jeremy-dentel) -* Added: .parents() (@zero21xxx) -* Added: .val() (@rschmukler & @leahciMic) -* Added: Travis support for node 0.10.0 (@jeremy-dentel) -* Fixed: .find() if no selector (@davidchambers) -* Fixed: Propagate syntax errors caused by invalid selectors (@davidchambers) - -0.10.8 / 2013-03-11 -================== - -* Add slice method (SBoudrias) - -0.10.7 / 2013-02-10 -================== - -* Code & doc cleanup (davidchambers) -* Fixed bug in filter (jugglinmike) - -0.10.6 / 2013-01-29 -================== - -* Added `$.contains(...)` (jugglinmike) -* formatting cleanup (davidchambers) -* Bug fix for `.children()` (jugglinmike & davidchambers) -* Remove global `render` bug (wvl) - -0.10.5 / 2012-12-18 -=================== - -* Fixed botched publish from 0.10.4 - changes should now be present - -0.10.4 / 2012-12-16 -================== - -* $.find should query descendants only (@jugglinmike) -* Tighter underscore dependency - -0.10.3 / 2012-11-18 -=================== - -* fixed outer html bug -* Updated documentation for $(...).html() and $.html() - -0.10.2 / 2012-11-17 -=================== - -* Added a toString() method (@bensheldon) -* use `_.each` and `_.map` to simplify cheerio namesakes (@davidchambers) -* Added filter() with tests and updated readme (@bensheldon & @davidchambers) -* Added spaces between attributes rewritten by removeClass (@jos3000) -* updated docs to remove reference to size method (@ironchefpython) -* removed HTML tidy/pretty print from cheerio - -0.10.1 / 2012-10-04 -=================== - -* Fixed regression, filtering with a context (#106) - -0.10.0 / 2012-09-24 -=================== - -* Greatly simplified and reorganized the library, reducing the loc by 30% -* Now supports mocha's test-coverage -* Deprecated self-closing tags (HTML5 doesn't require them) -* Fixed error thrown in removeClass(...) @robashton - -0.9.2 / 2012-08-10 -================== - -* added $(...).map(fn) -* manipulation: refactor `makeCheerioArray` -* make .removeClass() remove *all* occurrences (#64) - -0.9.1 / 2012-08-03 -================== - -* fixed bug causing options not to make it to the parser - -0.9.0 / 2012-07-24 -================== - -* Added node 8.x support -* Removed node 4.x support -* Add html(dom) support (@wvl) -* fixed xss vulnerabilities on .attr(), .text(), & .html() (@benatkin, @FB55) -* Rewrote tests into javascript, removing coffeescript dependency (@davidchambers) -* Tons of cleanup (@davidchambers) - -0.8.3 / 2012-06-12 -================== - -* Fixed minor package regression (closes #60) - -0.8.2 / 2012-06-11 -================== - -* Now fails gracefully in cases that involve special chars, which is inline with jQuery (closes #59) -* text() now decode special entities (closes #52) -* updated travis.yml to test node 4.x - -0.8.1 / 2012-06-02 -================== - -* fixed regression where if you created an element, it would update the root -* compatible with node 4.x (again) - -0.8.0 / 2012-05-27 -================== - -* Updated CSS parser to use FB55/CSSselect. Cheerio now supports most CSS3 psuedo selectors thanks to @FB55. -* ignoreWhitespace now on by default again. See #55 for context. -* Changed $(':root') to $.root(), cleaned up $.clone() -* Support for .eq(i) thanks to @alexbardas -* Removed support for node 0.4.x -* Fixed memory leak where package.json was continually loaded -* Tons more tests - -0.7.0 / 2012-04-08 -================== - -* Now testing with node v0.7.7 -* Added travis-ci integration -* Replaced should.js with expect.js. Browser testing to come -* Fixed spacing between attributes and their values -* Added HTML tidy/pretty print -* Exposed node-htmlparser2 parsing options -* Revert .replaceWith(...) to be consistent with jQuery - -0.6.2 / 2012-02-12 -================== - -* Fixed .replaceWith(...) regression - -0.6.1 / 2012-02-12 -================== - -* Added .first(), .last(), and .clone() commands. -* Option to parse using whitespace added to `.load`. -* Many bug fixes to make cheerio more aligned with jQuery. -* Added $(':root') to select the highest level element. - -Many thanks to the contributors that made this release happen: @ironchefpython and @siddMahen - -0.6.0 / 2012-02-07 -================== - -* *Important:* `$(...).html()` now returns inner HTML, which is in line with the jQuery spec -* `$.html()` returns the full HTML string. `$.html([cheerioObject])` will return the outer(selected element's tag) and inner HTML of that object -* Fixed bug that prevented HTML strings with depth (eg. `append('
')`) from getting `parent`, `next`, `prev` attributes. -* Halted [htmlparser2](https://github.com/FB55/node-htmlparser) at v2.2.2 until single attributes bug gets fixed. - -0.5.1 / 2012-02-05 -================== - -* Fixed minor regression: $(...).text(fn) would fail - -0.5.1 / 2012-02-05 -================== - -* Fixed regression: HTML pages with comments would fail - -0.5.0 / 2012-02-04 -================== - -* Transitioned from Coffeescript back to Javascript -* Parser now ignores whitespace -* Fixed issue with double slashes on self-enclosing tags -* Added boolean attributes to html rendering - -0.4.2 / 2012-01-16 -================== - -* Multiple selectors support: $('.apple, .orange'). Thanks @siddMahen! -* Update package.json to always use latest cheerio-soupselect -* Fix memory leak in index.js - -0.4.1 / 2011-12-19 -================== -* Minor packaging changes to allow `make test` to work from npm installation - -0.4.0 / 2011-12-19 -================== - -* Rewrote all unit tests as cheerio transitioned from vows -> mocha -* Internally, renderer.render -> render(...), parser.parse -> parse(...) -* Append, prepend, html, before, after all work with only text (no tags) -* Bugfix: Attributes can now be removed from script and style tags -* Added yield as a single tag -* Cheerio now compatible with node >=0.4.7 - -0.3.2 / 2011-12-1 -================= - -* Fixed $(...).text(...) to work with "root" element - -0.3.1 / 2011-11-25 -================== - -* Now relying on cheerio-soupselect instead of node-soupselect -* Removed all lingering htmlparser dependencies -* parser now returns parent "root" element. Root now never needs to be updated when there is multiple roots. This fixes ongoing issues with before(...), after(...) and other manipulation functions -* Added jQuery's $(...).replaceWith(...) - -0.3.0 / 2011-11-19 -================== - -* Now using htmlparser2 for parsing (2x speed increase, cleaner, actively developed) -* Added benchmark directory for future speed tests -* $('...').dom() was funky, so it was removed in favor of $('...').get(). $.dom() still works the same. -* $.root now correctly static across all instances of $ -* Added a screencast - -0.2.2 / 2011-11-9 -================= - -* Traversing will select `", - "expected": [ - { - "type": "tag", - "name": "head", - "attribs": {}, - "children": [ - { - "type": "script", - "name": "script", - "attribs": { - "language": "Javascript" - }, - "children": [ - { - "data": "var foo = \"\"; alert(2 > foo); var baz = 10 << 2; var zip = 10 >> 1; var yap = \"<<>>>><<\";", - "type": "text" - } - ] - } - ] - } - ] +{ + "name": "Unescaped chars in script", + "options": {}, + "html": "", + "expected": [ + { + "type": "tag", + "name": "head", + "attribs": {}, + "children": [ + { + "type": "script", + "name": "script", + "attribs": { + "language": "Javascript" + }, + "children": [ + { + "data": "var foo = \"\"; alert(2 > foo); var baz = 10 << 2; var zip = 10 >> 1; var yap = \"<<>>>><<\";", + "type": "text" + } + ] + } + ] + } + ] } \ No newline at end of file diff --git a/node_modules/domhandler/test/cases/05-tags_in_comment.json b/node_modules/domhandler/test/cases/05-tags_in_comment.json index 2d22d9e1d..4e774196b 100644 --- a/node_modules/domhandler/test/cases/05-tags_in_comment.json +++ b/node_modules/domhandler/test/cases/05-tags_in_comment.json @@ -1,18 +1,18 @@ -{ - "name": "Special char in comment", - "options": {}, - "html": "", - "expected": [ - { - "type": "tag", - "name": "head", - "attribs": {}, - "children": [ - { - "data": " commented out tags Test", - "type": "comment" - } - ] - } - ] +{ + "name": "Special char in comment", + "options": {}, + "html": "", + "expected": [ + { + "type": "tag", + "name": "head", + "attribs": {}, + "children": [ + { + "data": " commented out tags Test", + "type": "comment" + } + ] + } + ] } \ No newline at end of file diff --git a/node_modules/domhandler/test/cases/06-comment_in_script.json b/node_modules/domhandler/test/cases/06-comment_in_script.json index 9a21cdabf..062c33676 100644 --- a/node_modules/domhandler/test/cases/06-comment_in_script.json +++ b/node_modules/domhandler/test/cases/06-comment_in_script.json @@ -1,18 +1,18 @@ -{ - "name": "Script source in comment", - "options": {}, - "html": "", - "expected": [ - { - "type": "script", - "name": "script", - "attribs": {}, - "children": [ - { - "data": "", - "type": "text" - } - ] - } - ] +{ + "name": "Script source in comment", + "options": {}, + "html": "", + "expected": [ + { + "type": "script", + "name": "script", + "attribs": {}, + "children": [ + { + "data": "", + "type": "text" + } + ] + } + ] } \ No newline at end of file diff --git a/node_modules/domhandler/test/cases/07-unescaped_in_style.json b/node_modules/domhandler/test/cases/07-unescaped_in_style.json index 77438fdc1..c982ee693 100644 --- a/node_modules/domhandler/test/cases/07-unescaped_in_style.json +++ b/node_modules/domhandler/test/cases/07-unescaped_in_style.json @@ -1,20 +1,20 @@ -{ - "name": "Unescaped chars in style", - "options": {}, - "html": "", - "expected": [ - { - "type": "style", - "name": "style", - "attribs": { - "type": "text/css" - }, - "children": [ - { - "data": "\n body > p\n\t{ font-weight: bold; }", - "type": "text" - } - ] - } - ] +{ + "name": "Unescaped chars in style", + "options": {}, + "html": "", + "expected": [ + { + "type": "style", + "name": "style", + "attribs": { + "type": "text/css" + }, + "children": [ + { + "data": "\n body > p\n\t{ font-weight: bold; }", + "type": "text" + } + ] + } + ] } \ No newline at end of file diff --git a/node_modules/domhandler/test/cases/08-extra_spaces_in_tag.json b/node_modules/domhandler/test/cases/08-extra_spaces_in_tag.json index 5c2492e22..c6be55dea 100644 --- a/node_modules/domhandler/test/cases/08-extra_spaces_in_tag.json +++ b/node_modules/domhandler/test/cases/08-extra_spaces_in_tag.json @@ -1,20 +1,20 @@ -{ - "name": "Extra spaces in tag", - "options": {}, - "html": "the text", - "expected": [ - { - "type": "tag", - "name": "font", - "attribs": { - "size": "14" - }, - "children": [ - { - "data": "the text", - "type": "text" - } - ] - } - ] +{ + "name": "Extra spaces in tag", + "options": {}, + "html": "the text", + "expected": [ + { + "type": "tag", + "name": "font", + "attribs": { + "size": "14" + }, + "children": [ + { + "data": "the text", + "type": "text" + } + ] + } + ] } \ No newline at end of file diff --git a/node_modules/domhandler/test/cases/09-unquoted_attrib.json b/node_modules/domhandler/test/cases/09-unquoted_attrib.json index 543cceeed..3809ee2b3 100644 --- a/node_modules/domhandler/test/cases/09-unquoted_attrib.json +++ b/node_modules/domhandler/test/cases/09-unquoted_attrib.json @@ -1,20 +1,20 @@ -{ - "name": "Unquoted attributes", - "options": {}, - "html": "the text", - "expected": [ - { - "type": "tag", - "name": "font", - "attribs": { - "size": "14" - }, - "children": [ - { - "data": "the text", - "type": "text" - } - ] - } - ] +{ + "name": "Unquoted attributes", + "options": {}, + "html": "the text", + "expected": [ + { + "type": "tag", + "name": "font", + "attribs": { + "size": "14" + }, + "children": [ + { + "data": "the text", + "type": "text" + } + ] + } + ] } \ No newline at end of file diff --git a/node_modules/domhandler/test/cases/10-singular_attribute.json b/node_modules/domhandler/test/cases/10-singular_attribute.json index 544636e49..e01082694 100644 --- a/node_modules/domhandler/test/cases/10-singular_attribute.json +++ b/node_modules/domhandler/test/cases/10-singular_attribute.json @@ -1,15 +1,15 @@ -{ - "name": "Singular attribute", - "options": {}, - "html": "