1、hash表
哈希表是又称散列表,一种以 “key-value” 形式存储数据的数据结构。使用hash表是典型的牺牲空间换时间的算法。
-
给你一个整数数组 nums 。如果任一值在数组中出现至少两次,返回 true ;如果数组中每个元素互不相同,返回 false 。例如输入:nums = [1,2,3,1],输出:true (力扣217题)
func containsDuplicate(nums []int) bool { set := map[int]struct{}{} // hash表,空struct可以节省空间 for _, v := range nums { if _, has := set[v]; has { return true } set[v] = struct{}{} // 无记录则插入 } return false }
-
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。(力扣1题)
输入:nums = [2,7,11,15], target = 9, 输出:[0,1]。 因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
// 解题思路:hash表中存在target - nums[i]则符合题目要求,不存在则将nums[i]作为键,i作为值插入hash表中 func twoSum(nums []int, target int) []int { hashTable := map[int]int{} // hash表 for i, v := range nums { if p, ok := hashTable[target - v]; ok { // target - nums[i] return []int{p, i} // nums[p] + nums[i] = target } hashTable[v] = i } return nil }
-
给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。
如果可以,返回 true ;否则返回 false 。magazine 中的每个字符只能在 ransomNote 中使用一次。(力扣383题)
输入:ransomNote = “aa”, magazine = “ab”, 输出:false
输入:ransomNote = “aa”, magazine = “aab”, 输出:true
func canConstruct(ransomNote, magazine string) bool { if len(ransomNote) > len(magazine) { return false } cnt := [26]int{} // 字符集hash表 for _, ch := range magazine { // 统计magazine字符个数 cnt[ch-'a']++ } for _, ch := range ransomNote { cnt[ch-'a']-- if cnt[ch-'a'] < 0 { return false // magazine里的字符个数小于ransomNote } } return true }
2、双指针法
双指针顾名思义,就是同时使用两个指针,在序列、链表结构上指向的是位置,在树、图结构中指向的是节点,通过或同向移动,或相向移动来维护、统计信息。
-
给你一个升序排列的数组 nums,请你原地删除重复出现的元素,使每个元素只出现一次,返回删除后数组的新长度。元素的相对顺序应该保持一致 。不需要考虑数组中超出新长度后面的元素。(力扣26题)
func removeDuplicates(nums []int) int { n := len(nums) if n == 0 { return 0 } slow := 1 // 慢指针 for fast := 1; fast < n; fast++ { if nums[fast] != nums[fast-1] { nums[slow] = nums[fast] slow++ // 元素不重复,慢指针才往前走 } } return slow }
-
给你一个链表的头节点
head
,判断链表中是否有环,下图表示有环。(力扣141题)
// 解题思路:快指针每次走两格,慢指针每次走一个,如果有环则快指针会和慢指针相遇 func hasCycle(head *ListNode) bool { if head == nil || head.Next == nil { return false } slow, fast := head, head.Next // 快慢指针 for fast != slow { if fast == nil || fast.Next == nil { return false // 遍历到了尾节点,没有环 } slow = slow.Next fast = fast.Next.Next } return true // 快指针和慢指针相遇,有环 }
-
给你两个单链表的头节点 headA 和 headB且不存在环,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回null。图示两个链表在节点 c1 开始相交:(力扣160题)
// 解题思路:pa, pb同时开始遍历,遍历到末尾节点时交替遍历,当pa=pb则为相交节点
func getIntersectionNode(headA, headB *ListNode) *ListNode {
if headA == nil || headB == nil {
return nil
}
pa, pb := headA, headB // 双指针
for pa != pb {
if pa == nil {
pa = headB // 遍历到了尾节点,从B开始遍历
} else {
pa = pa.Next
}
if pb == nil {
pb = headA // 遍历到了尾节点,从A开始遍历
} else {
pb = pb.Next
}
}
return pa
}
-
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列(力扣88)
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3, 输出:[1,2,2,3,5,6]
解释:需要合并 [1,2,3] 和 [2,5,6] 。
func merge(nums1 []int, m int, nums2 []int, n int) {
for p1, p2, tail := m-1, n-1, m+n-1; p1 >= 0 || p2 >= 0; tail-- {
var cur int
if p1 == -1 { // nums1遍历结束
cur = nums2[p2]
p2--
} else if p2 == -1 { // nums2遍历结束
cur = nums1[p1]
p1--
} else if nums1[p1] > nums2[p2] {
cur = nums1[p1]
p1--
} else {
cur = nums2[p2]
p2--
}
nums1[tail] = cur // 尾部元素等于max(nums1[p1], nums2[p2])
}
}
-
给定一个数组
nums
,编写一个函数将所有0
移动到数组的末尾,同时保持非零元素的相对顺序,必须在不复制数组的情况下原地对数组进行操作。(力扣283题)输入: nums = [0,1,0,3,12], 输出: [1,3,12,0,0]
func moveZeroes(nums []int) { left, right, n := 0, 0, len(nums) // 左指针左边均为非零数;右指针左边直到左指针处均为零。 for right < n { if nums[right] != 0 { nums[left], nums[right] = nums[right], nums[left] // 指针值交换 left++ } right++ } }
3、贪心算法
原理:从问题最初状态出发,通过多次贪心选择,最终得到问题的解,局部最优解就是全局最优解
-
给定由一些正数(代表长度)组成的数组
nums
,返回 由其中三个长度组成的、面积不为零的三角形的最大周长。如果不能形成任何面积不为零的三角形,返回0
。(力扣976题)输入: nums = [2,1,2], 输出: 5
func largestPerimeter(a []int) int { sort.Ints(a) // 排序 for i := len(a) - 1; i >= 2; i-- { // 从最大的开始找 if a[i-2] + a[i-1] > a[i] { return a[i-2] + a[i-1] + a[i] // 能组成三角形就符合,局部最优解就是全局最优解 } } return 0 }
-
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。注意,一开始你手头没有任何零钱。给你一个整数数组
bills
,其中bills[i]
是第i
位顾客付的账。如果你能给每位顾客正确找零,返回true
,否则返回false
。(力扣860题)输入:bills = [5,5,5,10,20], 输出:true
func lemonadeChange(bills []int) bool { five, ten := 0, 0 for _, bill := range bills { if bill == 5 { five++ } else if bill == 10 { if five == 0 { return false // 顾客给10元但没有5元钱找,贪心直接返回 } five-- ten++ } else { if five > 0 && ten > 0 { five-- ten-- } else if five >= 3 { five -= 3 } else { return false // 顾客给20元但没有零钱找,贪心直接返回 } } } return true // 不符合的已经剔除,局部最优解的集合就是全局最优解 }
-
在一个 平衡字符串 中,‘L’ 和 ‘R’ 字符的数量是相同的。给你一个平衡字符串 s,请你将它分割成尽可能多的平衡字符串。注意:分割得到的每个字符串都必须是平衡字符串,且分割得到的平衡字符串是原平衡字符串的连续子串。返回可以通过分割得到的平衡字符串的 最大数量 。(力扣1221题)
输入:s = “RLRRLLRLRL”, 输出:4。 解释:s 可以分割为 “RL”、“RRLL”、“RL”、“RL” ,每个子字符串中都包含相同数量的 ‘L’ 和 ‘R’ 。
func balancedStringSplit(s string) (ans int) { d := 0 for _, ch := range s { if ch == 'L' { d++ } else { d-- } if d == 0 { ans++ // 前面已经是平衡字符串,局部最优解之和就是全局最优解 } } return }
-
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。返回你能获得的 最大 利润 。(力扣122题)
func maxProfit(prices []int) (ans int) { for i := 1; i < len(prices); i++ { ans += max(0, prices[i]-prices[i-1]) // 每次涨价就买和卖 } return } func max(a, b int) int { if a > b { return a } return b }
4、分治法
原理:分治法是最广泛的一类算法,采用递归的思想将较大规模的问题分成小问题求解。如果原问题可以拆分成n个子问题,并且这些子问题可以重复利用,并获得原问题的解就是分治的思想。(例如曹冲称象)
-
颠倒给定的 32 位无符号整数的二进制位。(力扣190题)
输入:n = 00000010100101000001111010011100
输出:964176192 (00111001011110000010100101000000)思路:若要翻转一个二进制串,可以将其均分成左右两部分,对每部分递归执行翻转操作,然后将左半部分拼在右半部分的后面,即完成了翻转。由于左右两部分的计算方式是相似的,利用位掩码和位移运算,我们可以自底向上地完成这一分治流程。
const ( // 由于题目给出的是32位数,所以交换4次即可 m1 = 0x55555555 // 01010101010101010101010101010101 m2 = 0x33333333 // 00110011001100110011001100110011 m4 = 0x0f0f0f0f // 00001111000011110000111100001111 m8 = 0x00ff00ff // 00000000111111110000000011111111 ) // 分治法:颠倒二进制位拆分成4个子问题,分成以下四部 func reverseBits(n uint32) uint32 { n = n>>1&m1 | n&m1<<1 // 0 和1互换位置(奇数和偶数位交换值) n = n>>2&m2 | n&m2<<2 // 00 和11互换位置 n = n>>4&m4 | n&m4<<4 // 000 和111互换位置 n = n>>8&m8 | n&m8<<8 // 0000 和1111互换位置 return n>>16 | n<<16 // 00000和11111互换位置 }
-
给你一个字符串
s
和一个整数k
,请你找出s
中的最长子串, 要求该子串中的每一字符出现次数都不少于k
。返回这一子串的长度。(力扣395题)输入:s = “aaabb”, k = 3, 输出:3。 解释:最长子串为 “aaa” ,其中 ‘a’ 重复了 3 次。
// 对于字符串s,如果存在某个字符ch,它的出现次数大于0且小于k,则任何包含ch的子串都不可能满足要求。也就是说,我们将字符串按照ch切分成若干段,则满足要求的最长子串一定出现在某个被切分的段内,而不能跨越一个或多个段。因此,可以考虑分治的方式求解本题。 func longestSubstring(s string, k int) (ans int) { if s == "" { return } cnt := [26]int{} for _, ch := range s { // hash表统计s中所有字母出现次数 cnt[ch-'a']++ } var split byte for i, c := range cnt[:] { if 0 < c && c < k { // 如果存在某个字符c出现次数大于0小于k,则任何包含c的子串都不可能满足要求 split = 'a' + byte(i) break } } if split == 0 { // 每个字母出现次数都大于k,满足要求!!! return len(s) } // 循环+递归将所有大于0小于k的字符拆分,剩余的为满足题意,求最大值即可 for _, subStr := range strings.Split(s, string(split)) { ans = max(ans, longestSubstring(subStr, k)) } return } func max(a, b int) int { // 求最大值 if a > b { return a } return b }
5、动态规划
动态规划是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
-
假设你正在爬楼梯。需要
n
阶你才能到达楼顶。每次你可以爬1
或2
个台阶。你有多少种不同的方法可以爬到楼顶呢?(力扣70题)// 解题思路:因为最后以此要么爬1个台阶,要么2个;所以爬n阶就楼梯就等于爬n-1阶楼梯次数 + 爬n-2阶楼梯次数和,即f(n)=f(n−1)+f(n−2),即斐波那契数和 func climbStairs(n int) int { p, q, r := 0, 0, 1 for i := 1; i <= n; i++ { p = q q = r r = p + q // f(n)=f(n−1)+f(n−2) } return r }
-
给定一个非负整数 *
numRows
,*生成「杨辉三角」的前numRows
行。(力扣118题)
func generate(numRows int) [][]int { ans := make([][]int, numRows) for i := range ans { ans[i] = make([]int, i+1) ans[i][0] = 1 ans[i][i] = 1 for j := 1; j < i; j++ { ans[i][j] = ans[i-1][j] + ans[i-1][j-1] // 通项公式 } } return ans }
-
给你一个整数 n ,对于 0 <= i <= n 中的每个 i ,计算其二进制表示中 1 的个数 ,返回一个长度为 n + 1 的数组 ans 作为答案。(力扣338题)
输入:n = 2, 输出:[0,1,1]。 解释:0 --> 0,1 --> 1,2 --> 10
func countBits(n int) []int { bits := make([]int, n+1) for i := 1; i <= n; i++ { bits[i] = bits[i>>1] + i&1 // 通项公式 } return bits }
6、滑动窗口
原理:顾名思义,就是有一个大小可变的窗口,左右两端方向一致的向前滑动。一般适用于字符串或者列表,类似于双指针。
-
给定一个含有 n 个正整数的数组和一个正整数 target 。找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。(力扣209题)
func minSubArrayLen(s int, nums []int) int { n := len(nums) if n == 0 { return 0 } ans := math.MaxInt32 start, end := 0, 0 // 窗口起点和终点 sum := 0 for end < n { sum += nums[end] for sum >= s { // 固定后缀找最小前缀(题目给的数组是正整数) ans = min(ans, end - start + 1) sum -= nums[start] start++ } end++ } if ans == math.MaxInt32 { return 0 // 不存在符合题意的数组 } return ans } func min(x, y int) int { if x < y { return x } return y }
-
给你一个整数数组 nums 和一个整数 k ,判断数组中是否存在两个不同的索引 i 和 j ,满足 nums[i] == nums[j] 且 abs(i - j) <= k 。如果存在,返回 true ;否则,返回 false 。(力扣219题)
输入:nums = [1,2,3,1], k = 3, 输出:true
解题思路:考虑数组nums 中的每个长度不超过 k+1 的滑动窗口,同一个滑动窗口中的任意两个下标差的绝对值不超过 k。如果存在一个滑动窗口,其中有重复元素,则存在两个不同的下标 i 和 j 满足 nums[i]=nums[j] 且∣i−j∣≤k。如果所有滑动窗口中都没有重复元素,则不存在符合要求的下标。因此,只要遍历每个滑动窗口,判断滑动窗口中是否有重复元素即可。如果一个滑动窗口的结束下标是 i,则该滑动窗口的开始下标是max(0,i−k)。可以使用哈希集合存储滑动窗口中的元素。从左到右遍历数组nums,当遍历到下标 i 时,具体操作如下:
-
如果 i>k,则下标 i−k−1 处的元素被移出滑动窗口,因此将 nums[i−k−1] 从哈希集合中删除;
-
判断 nums[i] 是否在哈希集合中,如果在哈希集合中则在同一个滑动窗口中有重复元素,返回true,如果不在哈希集合中则将其加入哈希集合。当遍历结束时,如果所有滑动窗口中都没有重复元素,返回false。
func containsNearbyDuplicate(nums []int, k int) bool { set := map[int]struct{}{} for i, num := range nums { if i > k { delete(set, nums[i-k-1]) } if _, ok := set[num]; ok { return true } set[num] = struct{}{} } return false }
-
7、DFS和BFS
DFS 全称是 Depth First Search,中文名是深度优先搜索,是为图论中的概念,在搜索算法中,该词常常指利用递归函数方便地实现暴力枚举的算法,与图论中的 DFS 算法有一定相似之处,但并不完全相同。
BFS 全称是 Breadth First Search,中文名是宽度优先搜索,也叫广度优先搜索,是图上最基础。所谓宽度优先。就是每次都尝试访问同一层的节点。 如果同一层都访问完了,再访问下一层。这样做的结果是,BFS 算法找到的路径是从起点开始的最短合法路径。换言之,这条路径所包含的边数最小。在 BFS 结束时,每个节点都是通过从起点到该点的最短路径访问的。
-
给你两棵二叉树的根节点
p
和q
,编写一个函数来检验这两棵树是否相同。如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。下面案例则返回false(力扣100题)
方法一:深度优先搜索
func isSameTree(p *TreeNode, q *TreeNode) bool { if p == nil && q == nil { return true } if p == nil || q == nil { return false } if p.Val != q.Val { return false } return isSameTree(p.Left, q.Left) && isSameTree(p.Right, q.Right) }
方法二:广度优先搜索
func isSameTree(p *TreeNode, q *TreeNode) bool { if p == nil && q == nil { return true } if p == nil || q == nil { return false } // 使用两个队列分别存储两个二叉树的节点。初始时将两个二叉树的根节点分别加入两个队列。每次从两个队列各取出一个节点 queue1, queue2 := []*TreeNode{p}, []*TreeNode{q} for len(queue1) > 0 && len(queue2) > 0 { node1, node2 := queue1[0], queue2[0] queue1, queue2 = queue1[1:], queue2[1:] // 出队 if node1.Val != node2.Val { // 如果两个节点的值不相同则两个二叉树一定不同; return false } left1, right1 := node1.Left, node1.Right left2, right2 := node2.Left, node2.Right if (left1 == nil && left2 != nil) || (left1 != nil && left2 == nil) { return false // 如果只有一个节点的左子节点为空,则两个二叉树的结构不同 } if (right1 == nil && right2 != nil) || (right1 != nil && right2 == nil) { return false // 只有一个节点的右子节点为空 } // 如果两个节点的子节点的结构相同,则将两个节点的非空子节点分别加入两个队列 if left1 != nil { // 先加入左子节点,后加入右子节点。 queue1 = append(queue1, left1) } if right1 != nil { queue1 = append(queue1, right1) } if left2 != nil { queue2 = append(queue2, left2) } if right2 != nil { queue2 = append(queue2, right2) } } return len(queue1) == 0 && len(queue2) == 0 // 如果搜索结束时两个队列同时为空,则两个二叉树相同 }
-
给定一个二叉树,找出其最大深度。二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。(力扣104题)
方法一:深度优先搜索
func maxDepth(root *TreeNode) int { if root == nil { return 0 } return max(maxDepth(root.Left), maxDepth(root.Right)) + 1 } func max(a, b int) int { if a > b { return a } return b }
方法二:广度优先搜索
func maxDepth(root *TreeNode) int { if root == nil { return 0 } queue := []*TreeNode{} queue = append(queue, root) // 广度优先搜索的队列里存放的是当前层的所有节点 ans := 0 for len(queue) > 0 { sz := len(queue) for sz > 0 { // 将队列里的所有节点都拿出来进行拓展,保证每次队列里存放的是当前层的所有节点 node := queue[0] queue = queue[1:] if node.Left != nil { queue = append(queue, node.Left) } if node.Right != nil { queue = append(queue, node.Right) } sz-- } ans++ } return ans }
8、前缀和
当算法题背景是整形数组且出现子数组和
或连续子数组
可以考虑使用前缀和方法。前缀和数组sum[i] = a[0]+a[1]+…a[i](a表示原数组)
-
给定一个含有 n 个正整数的数组和一个正整数 target 。找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0
func minSubArrayLen(s int, nums []int) int { n := len(nums) if n == 0 { return 0 } ans := math.MaxInt32 sums := make([]int, n + 1) for i := 1; i <= n; i++ { // 前缀和数组 sums[i] = sums[i - 1] + nums[i - 1] } for i := 1; i <= n; i++ { target := s + sums[i-1] bound := sort.SearchInts(sums, target) // 因为题目保证每个元素都为正,所以前缀和一定是递增的 if bound < 0 { bound = -bound - 1 } if bound <= n { ans = min(ans, bound - (i - 1)) } } if ans == math.MaxInt32 { return 0 // 没有符合题目要求的数组,返回0 } return ans } func min(x, y int) int { // 求最小值 if x < y { return x } return y }
9、回溯法
回溯法是一种经常被用在 深度优先搜索(DFS)和 广度优先搜索(BFS) 的技巧,就是一种递归的思想。
-
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。(力扣17题)
var phoneMap map[string]string = map[string]string{ // hash表存储键值 "2": "abc", "3": "def", "4": "ghi", "5": "jkl", "6": "mno", "7": "pqrs", "8": "tuv", "9": "wxyz", } var combinations []string func letterCombinations(digits string) []string { if len(digits) == 0 { return []string{} } combinations = []string{} backtrack(digits, 0, "") return combinations } func backtrack(digits string, index int, combination string) { if index == len(digits) { combinations = append(combinations, combination) } else { digit := string(digits[index]) letters := phoneMap[digit] lettersCount := len(letters) for i := 0; i < lettersCount; i++ { backtrack(digits, index + 1, combination + string(letters[i])) // 回溯 } } }
10、记忆化搜索
记忆化搜索是一种通过记录已经遍历过的状态的信息,从而避免对同一状态重复遍历的搜索实现方式。因为记忆化搜索确保了每个状态只访问一次,它也是一种常见的动态规划实现方式。(力扣397题)
-
给定一个正整数 n ,你可以做如下操作:
如果 n 是偶数,则用 n / 2替换 n 。
如果 n 是奇数,则可以用 n + 1或n - 1替换 n 。
返回 n 变为 1 所需的 最小替换次数 。class Solution { private: unordered_map<int, int> memo; // 记忆数组 public: int integerReplacement(int n) { // 递归+记忆化搜索 if (n == 1) { return 0; } if (memo.count(n)) { // 如果已经搜索过就直接返回 return memo[n]; } if (n % 2 == 0) { return memo[n] = 1 + integerReplacement(n / 2); } return memo[n] = 2 + min(integerReplacement(n / 2), integerReplacement(n / 2 + 1)); } };
10、字典树
字典树是哈希树的一种变种,主要用于同级、排序和存储大量的字符串,经常被搜索引擎用于文本单词统计。字典树利用字符串公共前缀减少查询时间,最大限度减少无畏的字符串比较,查询效率比哈希树高。
-
Trie(发音类似 “try”)或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。(力扣208题)
请你实现 Trie 类:
Trie() 初始化前缀树对象。
void insert(String word) 向前缀树中插入字符串 word 。
boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。
boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。type Trie struct { children [26]*Trie isEnd bool // 标志位,用于判断是否为完整的单词 } func Constructor() Trie { return Trie{} } func (t *Trie) Insert(word string) { // 插入 node := t for _, ch := range word { ch -= 'a' if node.children[ch] == nil { node.children[ch] = &Trie{} } node = node.children[ch] } node.isEnd = true // 标记为单词 } func (t *Trie) SearchPrefix(prefix string) *Trie { // 查找前缀 node := t for _, ch := range prefix { ch -= 'a' if node.children[ch] == nil { return nil // 不存在该前缀 } node = node.children[ch] } return node } func (t *Trie) Search(word string) bool { // 查找单词 node := t.SearchPrefix(word) return node != nil && node.isEnd } func (t *Trie) StartsWith(prefix string) bool { // 前缀是否存在 return t.SearchPrefix(prefix) != nil }
-
给你一个 不含重复 单词的字符串数组 words ,请你找出并返回 words 中的所有 连接词 。连接词为:一个完全由给定数组中的至少两个较短单词组成的字符串。(力扣472题)
示例 1:
输入:words = [“cat”,“cats”,“catsdogcats”,“dog”,“dogcatsdog”,“hippopotamuses”,“rat”,“ratcatdogcat”]
输出:[“catsdogcats”,“dogcatsdog”,“ratcatdogcat”]
解释:“catsdogcats” 由 “cats”, “dog” 和 “cats” 组成;
“dogcatsdog” 由 “dog”, “cats” 和 “dog” 组成;
“ratcatdogcat” 由 “rat”, “cat”, “dog” 和 “cat” 组成。示例 2:
输入:words = [“cat”,“dog”,“catdog”]
输出:[“catdog”]type trie struct { // 字典树 children [26]*trie isEnd bool } func (root *trie) insert(word string) { // 字典树插入 node := root for _, ch := range word { ch -= 'a' if node.children[ch] == nil { node.children[ch] = &trie{} } node = node.children[ch] } node.isEnd = true } func (root *trie) dfs(vis []bool, word string) bool { // 深度优先搜索dfs + 记忆化搜索 if word == "" { return true } if vis[len(word)-1] { // 记忆数组中有记录,直接返回,避免重复查找 return false } vis[len(word)-1] = true // 标记word为已搜索 node := root for i, ch := range word { node = node.children[ch-'a'] if node == nil { return false } if node.isEnd && root.dfs(vis, word[i+1:]) { return true } } return false } func findAllConcatenatedWordsInADict(words []string) (ans []string) { // 排序后可以确保当遍历到任意单词时,比该单词短的单词一定都已经遍历过 sort.Slice(words, func(i, j int) bool { return len(words[i]) < len(words[j]) }) root := &trie{} for _, word := range words { if word == "" { continue } vis := make([]bool, len(word)) // 记忆数组 if root.dfs(vis, word) { ans = append(ans, word) } else { root.insert(word) // 不是连接词,作为前缀插入字典树 } } return }