From 57f8da32457bc21a9892be2f7d4a37ed061f0b5b Mon Sep 17 00:00:00 2001 From: yehong Date: Wed, 26 May 2021 00:21:58 +0800 Subject: [PATCH] add week08 --- week01/92.reverseBetween.go | 90 ++++++++++++++++++++++++++++++ week01/README.md | 1 + week04/200.numIslands.go | 32 ++++++++--- week04/46.permute.go | 1 + week04/47.permuteUnique.go | 5 +- week08/208.trie.go | 52 +++++++++++++++++ week08/208.trie_test.go | 21 +++++++ week08/212.findWords.go | 9 +++ week08/695.maxAreaOfIsland.go | 55 ++++++++++++++++++ week08/79.exist.go | 66 ++++++++++++++++++++++ week08/README.md | 20 +++++++ week08/offer38.permutation.go | 56 +++++++++++++++++++ week08/offer38.permutation_test.go | 32 +++++++++++ 13 files changed, 430 insertions(+), 10 deletions(-) create mode 100644 week01/92.reverseBetween.go create mode 100644 week08/208.trie.go create mode 100644 week08/208.trie_test.go create mode 100644 week08/212.findWords.go create mode 100644 week08/695.maxAreaOfIsland.go create mode 100644 week08/79.exist.go create mode 100644 week08/README.md create mode 100644 week08/offer38.permutation.go create mode 100644 week08/offer38.permutation_test.go diff --git a/week01/92.reverseBetween.go b/week01/92.reverseBetween.go new file mode 100644 index 0000000..3084519 --- /dev/null +++ b/week01/92.reverseBetween.go @@ -0,0 +1,90 @@ +package week01 + +// 92. 反转链表 II +// 给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回反转后的链表 。 +// 示例 1: +// 输入:head = [1,2,3,4,5], left = 2, right = 4 +// 输出:[1,4,3,2,5] +// @lc: https://leetcode-cn.com/problems/reverse-linked-list-ii/ + +// 方法一:找到[left,right]子区间,反转子链表后再拼接 +// 1. 从虚拟头节点走 left-1 步,来到 left 节点的前一个节点 +// 2. 从 pre 再走 right-left+1 步,来到 right 节点(注意此时是right节点,而不是right的前一个 或 后一个节点) +// 3. 切断出一个子链表(截取链表),同第 206 题,反转链表的子区间 +// 4. 接回到原来的链表中 +// 时间复杂度:O(n) +// 空间复杂度:O(1) +func reverseBetween1(head *ListNode, left, right int) *ListNode { + dummy := &ListNode{Val: -1} // 哑结点,链表通用解法 + dummy.Next = head + // 1. 从虚拟头节点走 left-1 步,来到 left 节点的前一个节点 + pre := dummy + for i := 0; i < left-1; i++ { + pre = pre.Next + } + + // 2. 从 pre 再走 right-left+1 步,来到 right 节点 + rightNode := pre + for i := 0; i < right-left+1; i++ { + rightNode = rightNode.Next + } + + // 3. 切断出一个子链表(截取链表),并反转 + leftNode := pre.Next // 子链表的头结点 + cur := rightNode.Next // right的下一个节点,先保存起来,便于第4步拼接 + + // 切断 + pre.Next = nil + rightNode.Next = nil + // 反转子链表 + reverseList(leftNode) + + // 4. 接回到原来的链表中 + pre.Next = rightNode + leftNode.Next = cur + + return dummy.Next +} + +// reverseList 反转单链表 +// 双指针,先保存下一个节点为tmp,然后把cur反转(cur.Next指向前一个),最后再同时更新pre和cur +func reverseList(head *ListNode) { + var pre *ListNode = nil + cur := head + for cur != nil { + tmp := cur.Next // 先保存下一个节点,因为马上要断开 + cur.Next = pre // 反转操作 + pre = cur // pre指针后移 + cur = tmp // cur指针后移 + } +} + +// 方法二:头部插入法,方法一的问题在于:如果left=1,right=n(链表长度)时,会遍历2次链表 +// 1. 从虚拟头节点走 left-1 步,来到 left 节点的前一个节点 +// 2. 从 left-1..right 依次将当前节点插入到 子区间的头部,也就是拼接到 pre后面 +// 第二步的具体步骤: +// 2.1. pre指针永远不动,指向的是left 的前一个节点 +// 2.2. cur指针指向 待反转区域的第一个节点 left +// 2.3. next指针永远指向 cur的下一个节点,循环过程中,cur 变化后 next 会随着变化 +// @ref: https://leetcode-cn.com/problems/reverse-linked-list-ii/solution/fan-zhuan-lian-biao-ii-by-leetcode-solut-teyq/ +// 时间复杂度:O(n) +// 空间复杂度:O(1) +func reverseBetween2(head *ListNode, left, right int) *ListNode { + dummy := &ListNode{Val: -1} // 哑结点,链表通用解法 + dummy.Next = head + + // 1. 从虚拟头节点走 left-1 步,来到 left 节点的前一个节点 + pre := dummy + for i := 0; i < left-1; i++ { + pre = pre.Next + } + + cur := pre.Next + for i := 0; i < right-left; i++ { + next := cur.Next + cur.Next = next.Next + next.Next = pre.Next + pre.Next = next + } + return dummy.Next +} diff --git a/week01/README.md b/week01/README.md index 2a029ef..09150b2 100644 --- a/week01/README.md +++ b/week01/README.md @@ -64,6 +64,7 @@ todos [61. 旋转链表](https://leetcode-cn.com/problems/rotate-list/)|[61.rotateRight.go](61.rotateRight.go)|M|双指针、取模| [66. 加一](https://leetcode-cn.com/problems/plus-one/)|[66.plusOne.go](66.plusOne)|S|x| [88. 合并两个有序数组](https://leetcode-cn.com/problems/merge-sorted-array/)|[88.merge.go](88.merge.go)|S|双指针| +[92. 反转链表 II](https://leetcode-cn.com/problems/reverse-linked-list-ii/)|[92.reverseBetween.go](92.reverseBetween.go)|M|双指针| [155. 最小栈](https://leetcode-cn.com/problems/min-stack/)|[155.minStack.go](155.minStack.go)|S|辅助栈| [189. 旋转数组](https://leetcode-cn.com/problems/rotate-array/)|[189.rotate.go](189.rotate.go)|S|取模、交换| [283. 移动零](https://leetcode-cn.com/problems/move-zeroes/)|[283.moveZeroes.go](283.moveZeroes.go)|S|双指针、快排思想| diff --git a/week04/200.numIslands.go b/week04/200.numIslands.go index 83d9146..56c5430 100644 --- a/week04/200.numIslands.go +++ b/week04/200.numIslands.go @@ -22,8 +22,15 @@ func numIslands(grid [][]byte) int { return 0 } - // 行、列 - row, col := len(grid), len(grid[0]) + // 行、列数 + rows, cols := len(grid), len(grid[0]) + // 上下左右 与当前位置的offset + directions := [][]int{{-1, 0}, {1, 0}, {0, -1}, {0, 1}} + + // 判断当前位置是否在网格内 + inGrid := func(i, j int) bool { + return i >= 0 && i < rows && j >= 0 && j < cols + } // dfsMarking 的作用是如果当前位置是'1',则把它的前后左右都标记为'0', // 以此类推,递归把他的子子孙孙都标记为'0',也就是把相邻的陆地都夷为平地, @@ -31,22 +38,29 @@ func numIslands(grid [][]byte) int { var dfsMarking func(int, int) // 谨遵泛型递归四部曲 dfsMarking = func(i, j int) { // 1. terminaort 递归终止条件,i、j越界或当前位置不是'1' - if i < 0 || j < 0 || i >= row || j >= col || '1' != grid[i][j] { + if !inGrid(i, j) || '1' != grid[i][j] { return } + // 2. process current logic grid[i][j] = '0' // 3. drill down 递推下探将它的上下左右以及子子孙孙的上下左右都做dfsMarking操作 - dfsMarking(i-1, j) // 上 - dfsMarking(i+1, j) // 下 - dfsMarking(i, j-1) // 左 - dfsMarking(i, j+1) // 右 + // dfsMarking(i-1, j) // 上 + // dfsMarking(i+1, j) // 下 + // dfsMarking(i, j-1) // 左 + // dfsMarking(i, j+1) // 右 + + // 以上4行代码可以简化,预先定义一个方向偏离量数组,防止循环里写即可 + for _, direct := range directions { + dfsMarking(i+direct[0], j+direct[1]) + } + // 4. revert states, nothing todo } var count int - for i := 0; i < row; i++ { - for j := 0; j < col; j++ { + for i := 0; i < rows; i++ { + for j := 0; j < cols; j++ { if '1' == grid[i][j] { count++ dfsMarking(i, j) diff --git a/week04/46.permute.go b/week04/46.permute.go index 58b6769..3d788cd 100644 --- a/week04/46.permute.go +++ b/week04/46.permute.go @@ -45,6 +45,7 @@ func permute(nums []int) [][]int { // 只有长度相等时,才是全排列 if len(path) == len(nums) { res = append(res, append([]int{}, path...)) // 注意:需要拷贝 + return } // 选择+标记,处理结果、再撤销选择+撤销标记 for i := 0; i < len(nums); i++ { diff --git a/week04/47.permuteUnique.go b/week04/47.permuteUnique.go index 9bb13b0..9be2e8b 100644 --- a/week04/47.permuteUnique.go +++ b/week04/47.permuteUnique.go @@ -42,6 +42,7 @@ func permuteUnique(nums []int) [][]int { backtrack = func(path []int) { if len(path) == len(nums) { res = append(res, append([]int{}, path...)) // 注意:需要拷贝 + return } // 选择、处理逻辑、回撤选择 for i := 0; i < len(nums); i++ { @@ -49,7 +50,9 @@ func permuteUnique(nums []int) [][]int { if visited[i] { continue } - // 上一个元素和当前相同,并且没有访问过就跳过,注意这里:去重逻辑 + // 当前值等于前一个值: 两种情况: + // 1. nums[i-1] 没用过 说明回溯到了同一层 此时接着用num[i] 则会与 同层用num[i-1] 重复 + // 2. nums[i-1] 用过了 说明此时在num[i-1]的下一层 相等不会重复 if i != 0 && nums[i] == nums[i-1] && !visited[i-1] { continue } diff --git a/week08/208.trie.go b/week08/208.trie.go new file mode 100644 index 0000000..bcbc7c6 --- /dev/null +++ b/week08/208.trie.go @@ -0,0 +1,52 @@ +package week08 + +// 208. 实现 Trie (前缀树) +// Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。 +// 请你实现 Trie 类: +// Trie() 初始化前缀树对象。 +// void insert(String word) 向前缀树中插入字符串 word。 +// boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。 +// boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。 + +// Trie是便于word插入和查找的数据结构 +type Trie struct { + children [26]*Trie + isLeaf bool +} + +func Constructor() *Trie { + return &Trie{} +} + +func (t *Trie) Insert(word string) { + node := t + for _, char := range word { + char -= 'a' // ASCII code + if node.children[char] == nil { + node.children[char] = &Trie{} + } + node = node.children[char] // 一层层进入子节点 + } + node.isLeaf = true // 最终的子节点 +} + +func (t *Trie) SearchPrefix(prefix string) *Trie { + node := t + for _, char := range prefix { + char -= 'a' + if node.children[char] == nil { + return nil + } + node = node.children[char] + } + return node +} + +func (t *Trie) Search(word string) bool { + node := t.SearchPrefix(word) + return node != nil && node.isLeaf +} + +func (t *Trie) StartsWith(prefix string) bool { + return t.SearchPrefix(prefix) != nil +} diff --git a/week08/208.trie_test.go b/week08/208.trie_test.go new file mode 100644 index 0000000..50523c9 --- /dev/null +++ b/week08/208.trie_test.go @@ -0,0 +1,21 @@ +package week08 + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// run: go test -v -run Test_trie +func Test_trie(t *testing.T) { + assert := assert.New(t) + trie := Constructor() + + trie.Insert("apple") + assert.True(trie.Search("apple"), "Search apple") + assert.False(trie.Search("app"), "Search app") + assert.True(trie.StartsWith("app"), "StartsWith app") + + trie.Insert("app") + assert.True(trie.Search("app"), "Search app") +} diff --git a/week08/212.findWords.go b/week08/212.findWords.go new file mode 100644 index 0000000..017d45b --- /dev/null +++ b/week08/212.findWords.go @@ -0,0 +1,9 @@ +package week08 + +// 212. 单词搜索 II +// 给定一个 m x n 二维字符网格 board 和一个单词(字符串)列表 words,找出所有同时在二维网格和字典中出现的单词。 +// 单词必须按照字母顺序,通过 相邻的单元格 内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。 +// @lc: https://leetcode-cn.com/problems/word-search-ii/ +func findWords(board [][]byte, words []string) []string { + return nil +} diff --git a/week08/695.maxAreaOfIsland.go b/week08/695.maxAreaOfIsland.go new file mode 100644 index 0000000..0847d53 --- /dev/null +++ b/week08/695.maxAreaOfIsland.go @@ -0,0 +1,55 @@ +package week08 + +// 695. 岛屿的最大面积 +// 给定一个包含了一些 0 和 1 的非空二维数组 grid 。 +// 一个 岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在水平或者竖直方向上相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。 +// 找到给定的二维数组中最大的岛屿面积。(如果没有岛屿,则返回面积为 0 。) +// @lc: https://leetcode-cn.com/problems/max-area-of-island/ + +// dfs递归 +func maxAreaOfIsland(grid [][]int) int { + // 合法性判断 + if len(grid) == 0 || len(grid[0]) == 0 { + return 0 + } + + // 行、列数 + rows, cols := len(grid), len(grid[0]) + // 上下左右 与当前位置的offset + directions := [][2]int{{-1, 0}, {1, 0}, {0, -1}, {0, 1}} + // 判断当前位置是否在网格内 + inGrid := func(i, j int) bool { + return i >= 0 && i < rows && j >= 0 && j < cols + } + + var dfsMarking func(i, j int) int + dfsMarking = func(i, j int) int { + // terminator, 跳出网格 或 当前位置不是陆地 '1',即被mark过了 + if !inGrid(i, j) || 1 != grid[i][j] { + return 0 + } + + // mark 为'2',且递归把他的上下左右已经子子孙孙都mark + grid[i][j] = 2 + area := 1 + for _, direct := range directions { + area += dfsMarking(i+direct[0], j+direct[1]) + } + return area + } + + max := func(a, b int) int { + if a > b { + return a + } + return b + } + // 遍历网格,求 maxArea + var res int + for i := 0; i < rows; i++ { + for j := 0; j < cols; j++ { + res = max(res, dfsMarking(i, j)) + } + } + return res +} diff --git a/week08/79.exist.go b/week08/79.exist.go new file mode 100644 index 0000000..5369569 --- /dev/null +++ b/week08/79.exist.go @@ -0,0 +1,66 @@ +package week08 + +// 79. 单词搜索 +// 给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。 +// 单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。 +// 示例 1: +// 输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED" +// 输出:true +// @lc: https://leetcode-cn.com/problems/word-search/ + +// DFS+回溯 +func exist(board [][]byte, word string) bool { + // 分别为 当前位置与 上、下、左、右 的offset + directions := [][]int{{-1, 0}, {1, 0}, {0, -1}, {0, 1}} + if len(board) == 0 || len(board[0]) == 0 { + return false + } + + // 初始化,rows,cols,visited + rows, cols := len(board), len(board[0]) + visited := make([][]bool, rows) + for i := 0; i < rows; i++ { + visited[i] = make([]bool, cols) + } + + // 坐标是否在 board 内,防止越界 + inArea := func(x, y int) bool { + return x >= 0 && x < rows && y >= 0 && y < cols + } + + // dfs marking func + n := len(word) + var dfs func(int, int, int) bool + dfs = func(i, j, begin int) bool { + if begin == n-1 { // terminator + return board[i][j] == word[begin] + } + if board[i][j] == word[begin] { + visited[i][j] = true // marking + for _, direction := range directions { // process + x := i + direction[0] + y := j + direction[1] + if inArea(x, y) && !visited[x][y] { + if dfs(x, y, begin+1) { + return true + } + } + } + visited[i][j] = false // revert + } + return false + } + + // dfs marking + for i := 0; i < rows; i++ { + for j := 0; j < cols; j++ { + if dfs(i, j, 0) { + return true + } + } + } + return false +} + +// AC过了,懒得写单测了 +// @lc https://leetcode-cn.com/problems/word-search/solution/79-dan-ci-sou-suo-dfshui-su-golangban-be-ekcn/ diff --git a/week08/README.md b/week08/README.md new file mode 100644 index 0000000..3769509 --- /dev/null +++ b/week08/README.md @@ -0,0 +1,20 @@ +# 字典树、并查集、红黑树和AVL树、位运算 + +## 字典树 + +## 并查集 + +## 红黑树和AVL树 + +## 位运算 + +## 练习题 + +| Title | Code | Difficulty | Points | +| ----- | ---- | -------------------------------- |--------| +|[208. 实现 Trie (前缀树)](https://leetcode-cn.com/problems/implement-trie-prefix-tree/)|[208.trie.go](208.trie.go)|M|trie| +|[212. 单词搜索 II](https://leetcode-cn.com/problems/word-search-ii/)|[212.findWords.go](212.findWords.go)|H|trie、dfs+回溯| +|为了解决212 Hard问题
刻意练几个 `dfs+回溯` 题找找感觉
其中岛屿问题、排列、组合是典型的回溯思想| +|[79. 单词搜索](https://leetcode-cn.com/problems/word-search/)|[79.exist.go](79.exist.go)|M|dfs+回溯| +|[剑指 Offer 38. 字符串的排列](https://leetcode-cn.com/problems/zi-fu-chuan-de-pai-lie-lcof/)|[offer38.permutation.go](offer38.permutation.go)|M|dfs+回溯| +|[695. 岛屿的最大面积](https://leetcode-cn.com/problems/max-area-of-island/)|[695.maxAreaOfIsland.go](695.maxAreaOfIsland.go)|M|dfs+回溯| diff --git a/week08/offer38.permutation.go b/week08/offer38.permutation.go new file mode 100644 index 0000000..eb92f19 --- /dev/null +++ b/week08/offer38.permutation.go @@ -0,0 +1,56 @@ +package week08 + +import "sort" + +// 剑指 Offer 38. 字符串的排列 +// 输入一个字符串,打印出该字符串中字符的所有排列。 +// 你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。 +// 示例: +// 输入:s = "abc" +// 输出:["abc","acb","bac","bca","cab","cba"] +// @lc: https://leetcode-cn.com/problems/zi-fu-chuan-de-pai-lie-lcof/ + +// dfs+回溯,类似数组全排列,[46.全排列](https://leetcode-cn.com/problems/permutations) +// 子集、排列 等问题首先要想到 dfs+回溯 +func permutation(s string) []string { + res, n := []string{}, len(s) + if n == 0 { + return res + } + + // 先排序,便于去重 + bs := []byte(s) + sort.Slice(bs, func(i, j int) bool { return bs[i] < bs[j] }) + s = string(bs) + + // 标记已访问过的字符 + visited := make([]bool, n) + var backtracking func(string) + backtracking = func(path string) { + // terminator,长度等于n的才是全排列字串 + if len(path) == n { + res = append(res, path) + return + } + // 选择,处理,回撤 + for i := 0; i < n; i++ { + if visited[i] { + continue + } + // 当前字符等于前一个字符: 有两种情况: + // 1. s[i-1] 没用过 说明回溯到了同一层 此时接着用num[i] 则会与 同层用num[i-1] 重复 + // 2. s[i-1] 用过了 说明此时在num[i-1]的下一层 相等不会重复 + if i > 0 && s[i] == s[i-1] && !visited[i-1] { + continue + } + + visited[i] = true // mark + path += string(s[i]) // process + backtracking(path) // drill down + visited[i] = false + path = path[:len(path)-1] + } + } + backtracking("") + return res +} diff --git a/week08/offer38.permutation_test.go b/week08/offer38.permutation_test.go new file mode 100644 index 0000000..bf16c41 --- /dev/null +++ b/week08/offer38.permutation_test.go @@ -0,0 +1,32 @@ +package week08 + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// run: go test -run Test_permutation +func Test_permutation(t *testing.T) { + cases := []struct { + name string + input string + expect []string + }{ + { + name: "x1", + input: "abc", + expect: []string{"abc", "acb", "bac", "bca", "cab", "cba"}, + }, + { + name: "x2", + input: "aba", + expect: []string{"aab", "aba", "baa"}, + }, + } + + assert := assert.New(t) + for _, c := range cases { + assert.Equal(c.expect, permutation(c.input), c.name) + } +}