Hot 100+
🔥 热情的挑战者们,欢迎你们!这里是你们智力与决心的试炼场,每一道题目都是你们通往成功的阶梯。在这里,我们不仅仅是解决问题,我们是在锻炼思维,培养毅力,为那些伟大的职业里程碑做准备。
💪 你们的每一次尝试,无论成功与否,都是前进的一大步。记住,每一个伟大的发明和突破都是从无数次的失败中孕育而来的。所以,即使面对困难,也不要气馁,因为每一次挑战都让你更接近目标。
🚀 让我们以鼓励的心态开始这段旅程,将每一次的努力都视为通往梦想的一部分。在这里,你会发现自己的潜力,认识到自己的坚韧不拔。每一行代码,每一个逻辑,都是你实现梦想的基石。
🌟 相信自己,你拥有征服这些挑战的能力。每当你解决了一个看似不可能的问题,你就会更加自信。这些经历将成为你职业生涯中最宝贵的财富。
🎉 所以,让我们带着热情开始吧!拿起你的武器,那些编程语言,那些算法和数据结构,让我们在代码的世界里勇往直前,不断进步,直到达到顶峰。
加油,勇士们!你们的旅程从这里开始,未来由你们书写!🚀🚀🚀
哈希
哈希表(Hash Table)是一种使用哈希函数组织数据,以支持快速插入和搜索的数据结构。解决哈希相关的算法题通常涉及以下几个方面:
- 哈希映射:使用哈希表来映射键和值,常用于需要快速访问数据的场景。
- 去重:利用哈希表存储已经出现过的元素,以实现快速去重。
- 计数:使用哈希表来统计元素出现的次数。
- 分组:将数据分组,组内数据具有相同的特征(如字母异位词分组)。
- 查找匹配:利用哈希表快速查找是否存在匹配的元素或者模式。
解题步骤通常包括:
- 确定哈希表的键(Key)和值(Value)应该是什么。
- 设计合适的哈希函数,以减少哈希碰撞。
- 根据题目要求实现哈希表的插入、删除、查找等操作。
下面是一些使用 Go 语言实现的哈希表相关的代码示例:
例 1:找到数组中第一个唯一的字符
func firstUniqChar(s string) int {
hash := make(map[rune]int)
for _, c := range s {
hash[c]++
}
for i, c := range s {
if hash[c] == 1 {
return i
}
}
return -1
}
例 2:两数之和
func twoSum(nums []int, target int) []int {
hash := make(map[int]int)
for i, num := range nums {
if j, ok := hash[target-num]; ok {
return []int{j, i}
}
hash[num] = i
}
return nil
}
例 3:字母异位词分组
func groupAnagrams(strs []string) [][]string {
hash := make(map[[26]int][]string)
for _, str := range strs {
count := [26]int{}
for _, c := range str {
count[c-'a']++
}
hash[count] = append(hash[count], str)
}
result := make([][]string, 0, len(hash))
for _, group := range hash {
result = append(result, group)
}
return result
}
在解决哈希表相关的算法题时,重要的是理解如何利用哈希表的特性来优化数据的存储和访问。Go 语言的map
类型提供了内建的哈希表实现,可以非常方便地用于解决这类问题。
两数之和
题目要求
给定一个整数数组 nums
和一个整数目标值 target
,需要找到数组中两个数,使得这两个数的和等于 target
。然后返回这两个数在数组中的下标。
题目的限制条件如下:
- 每种输入只对应一个答案,即数组中不会有多组不同的数对能够满足和为
target
的条件。 - 同一个元素不能在答案中重复出现,即不能使用数组中同一个位置的数两次。
- 返回的答案不限制顺序。
解题思路
解决这个问题的关键是如何有效地查找数组中是否存在两个数,它们的和为 target
。以下是解题的步骤:
-
哈希表法:这是一种时间复杂度为 O(n) 的解法。可以通过一次遍历数组,边遍历边记录已经访问过的数字及其下标,来检查是否存在一个值与当前遍历到的数字相加等于
target
。- 初始化一个哈希表(通常是一个字典或者映射结构)。
- 遍历数组
nums
,对于每个元素x
,计算complement = target - x
。 - 检查
complement
是否已经在哈希表中:- 如果在,那么找到了一对答案,返回
x
的下标和complement
的下标。 - 如果不在,将
x
及其下标存入哈希表中。
- 如果在,那么找到了一对答案,返回
- 如果遍历结束都没有找到答案,则说明不存在满足条件的两个数。
-
双指针法:如果数组是有序的,还可以使用双指针法。首先对数组进行排序,然后使用两个指针分别指向数组的开头和结尾,根据两指针所指元素之和与
target
的比较,移动指针,直到找到一对和为target
的元素。- 对数组
nums
进行排序。 - 初始化两个指针,
left
指向数组开始,right
指向数组末尾。 - 当
left < right
时,进行以下操作:- 如果
nums[left] + nums[right] == target
,返回这两个数的下标。 - 如果
nums[left] + nums[right] < target
,则left
指针右移,因为数组是有序的,右移可以增加和的值。 - 如果
nums[left] + nums[right] > target
,则right
指针左移,因为数组是有序的,左移可以减少和的值。
- 如果
- 如果没有找到答案,说明不存在满足条件的两个数。
- 对数组
注意:使用双指针法时,排序会改变元素的原始下标,因此需要额外的步骤来找回原始下标,或者在排序前就记录下每个元素的原始下标。
在实际应用中,由于题目要求不能重复使用同一个元素,哈希表法更为直接和高效,通常作为首选方法。
Golang 版本
package main
import "fmt"
func twoSum(nums []int, target int) []int {
numMap := make(map[int]int)
for i, num := range nums {
if j, ok := numMap[target-num]; ok {
return []int{j, i}
}
numMap[num] = i
}
return nil
}
func main() {
nums := []int{2, 7, 11, 15}
target := 9
result := twoSum(nums, target)
fmt.Println(result) // 输出 [0 1],因为 nums[0] + nums[1] = 2 + 7 = 9
}
这段代码实现了哈希表法来解决问题。它创建了一个名为 numMap
的哈希表来存储数组元素值到它们索引的映射。在遍历数组的过程中,对于每个元素,代码会检查 target
减去当前元素值的结果是否已经在 numMap
中。如果是,就找到了一对符合条件的元素,并返回它们的索引。如果遍历结束都没有找到,函数返回 nil
。
C++版本
#include <iostream>
#include <vector>
#include <unordered_map>
std::vector<int> twoSum(std::vector<int>& nums, int target) {
std::unordered_map<int, int> numMap;
for (int i = 0; i < nums.size(); ++i) {
auto it = numMap.find(target - nums[i]);
if (it != numMap.end()) {
return {it->second, i};
}
numMap[nums[i]] = i;
}
return {};
}
int main() {
std::vector<int> nums = {2, 7, 11, 15};
int target = 9;
std::vector<int> result = twoSum(nums, target);
std::cout << "Index1: " << result[0] << ", Index2: " << result[1] << std::endl;
return 0;
}
这段 C++代码使用了 std::unordered_map
来实现哈希表,用于存储数组元素值和它们的索引。在遍历数组的过程中,对于每个元素,代码会在哈希表中查找 target
减去当前元素值的结果。如果找到了,就返回一个包含两个索引的向量。如果没有找到符合条件的元素对,函数返回一个空的向量。在 main
函数中,我们定义了一个示例数组和目标值,调用 twoSum
函数,并打印出结果。
Python 版本
def two_sum(nums, target):
num_dict = {}
for i, num in enumerate(nums):
complement = target - num
if complement in num_dict:
return [num_dict[complement], i]
num_dict[num] = i
return []
# 测试代码
if __name__ == "__main__":
nums = [2, 7, 11, 15]
target = 9
print(two_sum(nums, target)) # 输出 [0, 1],因为 nums[0] + nums[1] = 2 + 7 = 9
这段 Python 代码实现了一个 two_sum
函数,它使用字典 num_dict
来存储遍历过程中每个数字的索引。在遍历数组 nums
的过程中,对于每个数字,它会计算与当前数字相加能得到 target
的补数(complement),然后检查这个补数是否已经在字典中。如果是,就返回一个包含两个索引的列表。如果遍历结束都没有找到符合条件的数字对,函数返回一个空列表。在主函数中,我们定义了一个示例数组和目标值,调用 two_sum
函数,并打印出结果。
Java 版本
import java.util.HashMap;
import java.util.Map;
public class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> numMap = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (numMap.containsKey(complement)) {
return new int[] { numMap.get(complement), i };
}
numMap.put(nums[i], i);
}
throw new IllegalArgumentException("No two sum solution");
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {2, 7, 11, 15};
int target = 9;
int[] result = solution.twoSum(nums, target);
System.out.println("Index1: " + result[0] + ", Index2: " + result[1]); // 输出 Index1: 0, Index2: 1
}
}
这段 Java 代码定义了一个 Solution
类,其中包含一个 twoSum
方法来解决两数之和的问题。它使用了一个 HashMap
来存储遍历过程中每个数字的索引。在遍历数组 nums
的过程中,对于每个数字,它会计算与当前数字相加能得到 target
的补数(complement),然后检查这个补数是否已经在 HashMap
中。如果是,就返回一个包含两个索引的数组。如果遍历结束都没有找到符合条件的数字对,方法会抛出一个异常。main
方法用于测试 twoSum
方法,定义了一个示例数组和目标值,调用 twoSum
方法,并打印出结果。
Rust 版本
use std::collections::HashMap; fn two_sum(nums: Vec<i32>, target: i32) -> Vec<i32> { let mut num_map = HashMap::new(); for (i, &num) in nums.iter().enumerate() { match num_map.get(&(target - num)) { Some(&index) => return vec![index as i32, i as i32], None => num_map.insert(num, i), }; } vec![] } fn main() { let nums = vec![2, 7, 11, 15]; let target = 9; let result = two_sum(nums, target); println!("Index1: {}, Index2: {}", result[0], result[1]); }
这段 Rust 代码实现了一个 two_sum
函数,它使用了一个 HashMap
来存储遍历过程中每个数字的索引。在遍历 Vec<i32>
类型的数组 nums
的过程中,对于每个数字,它会计算与当前数字相加能得到 target
的补数,并检查这个补数是否已经在 HashMap
中。如果是,就返回一个包含两个索引的 Vec<i32>
。如果遍历结束都没有找到符合条件的数字对,函数返回一个空的 Vec<i32>
。
main
函数用于测试 two_sum
函数,定义了一个示例数组和目标值,调用 two_sum
函数,并打印出结果。注意,Rust 使用模式匹配(match
)来处理 Option
类型,这是 Rust 中处理可能不存在值的惯用方式。
总结
上述解法采用了哈希表来优化查找过程,以实现在数组中找到两个数,它们的和等于目标值 target
的问题。具体步骤如下:
- 初始化一个哈希表(在 Go 中是
map
,在 Rust 中是HashMap
)来存储数组中每个元素的值和它们对应的索引。 - 遍历数组中的每个元素:
- 对于每个元素,计算它和目标值
target
之间的差值。 - 检查这个差值是否已经在哈希表中:
- 如果差值存在,那么当前元素和哈希表中的差值对应的元素就是我们要找的两个数。返回它们的索引。
- 如果差值不存在,将当前元素及其索引存入哈希表中。
- 对于每个元素,计算它和目标值
- 如果遍历结束后没有找到符合条件的元素对,则返回一个空数组或抛出异常(取决于语言特性)。
这种方法的时间复杂度为 O(n),因为它只需要遍历数组一次。空间复杂度也为 O(n),因为最坏的情况下,可能需要存储数组中所有元素的信息。这种方法比起简单的双重循环(时间复杂度为 O(n^2))要高效得多。
字母异位词分组
题目要求
编写一个函数,该函数接收一个字符串数组作为参数。函数的目的是将这个数组中的所有字母异位词分组。字母异位词指的是由相同字母以不同顺序组成的单词。例如,“listen”和“silent”就是一对字母异位词。函数需要将所有的字母异位词分到同一个子数组中。最终,函数返回一个数组,这个数组包含了所有分组后的子数组,子数组中的元素是字母异位词。返回的数组中的子数组可以按任意顺序排列。
解题思路
要解决这个问题,我们可以采取以下步骤:
-
初始化一个空的哈希表:这个哈希表用于将具有相同字母组合的字符串分组。哈希表的键将是字符串中字母的某种统一形式(例如,按字典顺序排序后的字符串),值将是一个数组,存储所有具有相同字母组合的字符串。
-
遍历字符串数组:对于数组中的每个字符串,我们将执行以下操作:
- 将字符串中的字母排序,这样可以得到一个标准形式,用于检查两个字符串是否是字母异位词。例如,对于字符串 "eat",排序后得到 "aet"。
- 检查排序后的字符串是否已经作为键存在于哈希表中。如果存在,将原始字符串添加到对应的数组中。如果不存在,以排序后的字符串为键,创建一个新的数组,并将原始字符串添加到这个数组中。
-
收集结果:遍历完成后,哈希表中的每个键都对应一个包含所有字母异位词的数组。将这些数组收集起来,形成一个列表,这个列表就是最终的结果。
-
返回结果:返回步骤 3 中收集到的包含所有分组字母异位词的数组列表。
通过这种方法,我们可以确保所有的字母异位词都被分到了同一个组中,而且这个过程的时间复杂度相对较低,因为排序操作和哈希表的查找/插入操作通常都比较高效。
Golang 版本
package main
import (
"fmt"
"sort"
"strings"
)
// 将字符串中的字符排序并返回
func sortString(s string) string {
slice := strings.Split(s, "")
sort.Strings(slice)
return strings.Join(slice, "")
}
// 主函数,用于将字母异位词组合在一起
func groupAnagrams(strs []string) [][]string {
anagrams := make(map[string][]string)
for _, str := range strs {
sortedStr := sortString(str)
anagrams[sortedStr] = append(anagrams[sortedStr], str)
}
result := make([][]string, 0, len(anagrams))
for _, group := range anagrams {
result = append(result, group)
}
return result
}
func main() {
strs := []string{"eat", "tea", "tan", "ate", "nat", "bat"}
groupedAnagrams := groupAnagrams(strs)
fmt.Println(groupedAnagrams)
}
这段代码定义了一个groupAnagrams
函数,它接收一个字符串数组,并返回一个二维字符串数组,其中包含了分组后的字母异位词。主函数main
展示了如何调用groupAnagrams
函数并打印结果。
C++版本
#include <iostream>
#include <vector>
#include <unordered_map>
#include <algorithm>
// 将字符串中的字符排序并返回
std::string sortString(std::string s) {
std::sort(s.begin(), s.end());
return s;
}
// 主函数,用于将字母异位词组合在一起
std::vector<std::vector<std::string>> groupAnagrams(std::vector<std::string>& strs) {
std::unordered_map<std::string, std::vector<std::string>> anagrams;
for (const std::string& str : strs) {
std::string sortedStr = sortString(str);
anagrams[sortedStr].push_back(str);
}
std::vector<std::vector<std::string>> result;
for (auto& pair : anagrams) {
result.push_back(pair.second);
}
return result;
}
int main() {
std::vector<std::string> strs = {"eat", "tea", "tan", "ate", "nat", "bat"};
std::vector<std::vector<std::string>> groupedAnagrams = groupAnagrams(strs);
// 打印结果
for (const auto& group : groupedAnagrams) {
for (const std::string& str : group) {
std::cout << str << " ";
}
std::cout << std::endl;
}
return 0;
}
这段代码定义了一个groupAnagrams
函数,它接收一个字符串数组的引用,并返回一个二维字符串数组,其中包含了分组后的字母异位词。main
函数展示了如何调用groupAnagrams
函数并打印结果。在 C++中,使用std::unordered_map
来存储键值对,其中键是排序后的字符串,值是原始字符串的列表。
Python 版本
from collections import defaultdict
def groupAnagrams(strs):
anagrams = defaultdict(list)
for s in strs:
# Sort the string and use it as a key
sorted_str = ''.join(sorted(s))
anagrams[sorted_str].append(s)
# Return the grouped anagrams as a list of lists
return list(anagrams.values())
# Example usage
if __name__ == "__main__":
strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
print(groupAnagrams(strs))
这段代码中,groupAnagrams
函数使用了 Python 的 defaultdict
来收集字母异位词。它将每个字符串排序后得到的结果作为键,原始字符串作为值添加到对应的列表中。最后,返回这个字典的值的列表,即分组后的字母异位词列表。在主函数中,我们定义了一个字符串列表,并调用 groupAnagrams
函数来打印分组后的结果。
Java 版本
import java.util.*;
public class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
if (strs == null || strs.length == 0) return new ArrayList<>();
Map<String, List<String>> map = new HashMap<>();
for (String s : strs) {
char[] ca = s.toCharArray();
Arrays.sort(ca);
String keyStr = String.valueOf(ca);
if (!map.containsKey(keyStr)) map.put(keyStr, new ArrayList<>());
map.get(keyStr).add(s);
}
return new ArrayList<>(map.values());
}
public static void main(String[] args) {
Solution solution = new Solution();
String[] strs = {"eat", "tea", "tan", "ate", "nat", "bat"};
List<List<String>> groupedAnagrams = solution.groupAnagrams(strs);
for (List<String> group : groupedAnagrams) {
System.out.println(group);
}
}
}
这段 Java 代码定义了一个Solution
类,其中包含了groupAnagrams
方法,用于将字符串数组中的字母异位词分组。它使用了一个哈希表map
来将排序后的字符串作为键,所有字母异位词作为值。main
方法中创建了Solution
的实例,并调用groupAnagrams
方法,然后打印出分组后的字母异位词列表。
Rust 版本
use std::collections::HashMap; fn group_anagrams(strs: Vec<String>) -> Vec<Vec<String>> { let mut map: HashMap<String, Vec<String>> = HashMap::new(); for s in strs.into_iter() { let mut chars: Vec<char> = s.chars().collect(); chars.sort_unstable(); let sorted_str: String = chars.into_iter().collect(); map.entry(sorted_str).or_insert(Vec::new()).push(s); } map.into_values().collect() } fn main() { let strs = vec![ "eat".to_string(), "tea".to_string(), "tan".to_string(), "ate".to_string(), "nat".to_string(), "bat".to_string(), ]; let grouped_anagrams = group_anagrams(strs); for group in grouped_anagrams { println!("{:?}", group); } }
这段 Rust 代码定义了一个group_anagrams
函数,它接收一个字符串向量,并返回一个包含分组后字母异位词的向量。它使用了一个哈希表来将字符排序后的字符串作为键,所有字母异位词作为值。main
函数中创建了一个字符串向量,并调用group_anagrams
函数,然后打印出分组后的字母异位词列表。
总结
不同编程语言的解法核心思想相同,都是利用哈希表(或字典)来分组字母异位词。以下是解题步骤的总结:
-
初始化哈希表:创建一个哈希表来存储分组信息,其中键是排序后的字符串,值是原始字符串的列表。
-
遍历字符串数组:对于数组中的每个字符串,执行以下操作:
- 将字符串的字符排序,以生成一个标准化的形式,这样所有的异位词都会有相同的排序字符串。
- 检查排序后的字符串是否已经作为键存在于哈希表中。
- 如果存在,将原始字符串添加到对应的值列表中。
- 如果不存在,创建一个新的列表,并将原始字符串添加进去。
-
收集分组:遍历完成后,哈希表中的每个键都对应一个包含所有字母异位词的列表。将这些列表收集起来,形成最终的分组列表。
在具体实现上,各编程语言有以下差异:
- Golang:使用
map
作为哈希表,通过字符串排序来作为键。 - C++:使用
std::unordered_map
作为哈希表,同样通过字符串排序来作为键。 - Python:使用
defaultdict
来简化哈希表的操作,利用排序后的字符串作为键。 - Java:使用
HashMap
作为哈希表,通过将字符串转换为字符数组并排序来获取键。 - Rust:使用
HashMap
作为哈希表,利用 Rust 的迭代器和收集器来处理字符串排序和分组。
所有解法都遵循了相同的算法逻辑,但是在语法和一些数据结构的使用上根据语言特性有所不同。
最长连续序列
题目要求
设计一个算法,找出给定未排序整数数组 nums
中数字连续的最长序列的长度。这里的连续序列指的是,序列中的所有数字都可以按升序排列,且每个数字之间的差值为 1。注意,序列中的元素在原数组中不需要是连续的。算法的时间复杂度应为 O(n)。
解题思路
要在 O(n) 的时间复杂度内解决这个问题,我们可以使用哈希表(Hash Table)来存储数组中每个元素是否已经被访问过,以及它们是否可以作为连续序列的一部分。解题步骤如下:
-
初始化哈希表:遍历数组
nums
,将每个元素作为键存入哈希表,值初始化为false
,表示该元素尚未被访问。 -
遍历数组:再次遍历数组
nums
,对于每个元素,执行以下步骤:- 如果当前元素已经被访问过(即哈希表中对应值为
true
),则跳过,因为它已经被包含在了某个序列中。 - 如果当前元素未被访问过,将其作为可能的序列起点,向两边扩展:
- 向左扩展:检查
num-1
是否存在于哈希表中,如果存在且未被访问过,继续向左扩展,直到找不到连续的数字为止。 - 向右扩展:检查
num+1
是否存在于哈希表中,如果存在且未被访问过,继续向右扩展,直到找不到连续的数字为止。
- 向左扩展:检查
- 在扩展过程中,更新哈希表中对应元素的值为
true
,表示这些元素已经被访问过,且已经被包含在了当前考虑的序列中。 - 记录并更新最长序列的长度。
- 如果当前元素已经被访问过(即哈希表中对应值为
-
返回结果:遍历完成后,返回记录的最长序列的长度。
这个算法之所以能保证 O(n) 的时间复杂度,是因为每个元素最多被访问两次:一次是在初始化哈希表时,另一次是在可能的序列扩展时。由于序列扩展是从未访问过的元素开始的,因此不会有重复的工作。
Golang 版本
package main
import "fmt"
func longestConsecutive(nums []int) int {
if len(nums) == 0 {
return 0
}
numSet := make(map[int]bool)
for _, num := range nums {
numSet[num] = false
}
longestStreak := 0
for num := range numSet {
// 如果num-1存在,说明num不是序列的起点
if _, ok := numSet[num-1]; !ok {
currentNum := num
currentStreak := 1
// 向右扩展序列
for {
if _, ok := numSet[currentNum+1]; ok {
currentNum++
currentStreak++
} else {
break
}
}
// 更新最长序列长度
if currentStreak > longestStreak {
longestStreak = currentStreak
}
}
}
return longestStreak
}
func main() {
nums := []int{100, 4, 200, 1, 3, 2}
fmt.Println(longestConsecutive(nums)) // 输出最长连续序列的长度
}
这段代码实现了题目要求的功能,首先创建一个哈希表来记录所有数字是否被访问过,然后遍历数组,对于每个作为新序列起点的数字(即前一个数字不存在的数字),向右扩展序列,最后返回找到的最长序列的长度。
C++版本
#include <iostream>
#include <vector>
#include <unordered_set>
int longestConsecutive(const std::vector<int>& nums) {
std::unordered_set<int> num_set(nums.begin(), nums.end());
int longestStreak = 0;
for (int num : num_set) {
// 如果num-1不存在,num可能是新序列的起点
if (num_set.find(num - 1) == num_set.end()) {
int currentNum = num;
int currentStreak = 1;
// 向右扩展序列
while (num_set.find(currentNum + 1) != num_set.end()) {
currentNum += 1;
currentStreak += 1;
}
// 更新最长序列长度
longestStreak = std::max(longestStreak, currentStreak);
}
}
return longestStreak;
}
int main() {
std::vector<int> nums = {100, 4, 200, 1, 3, 2};
std::cout << longestConsecutive(nums) << std::endl; // 输出最长连续序列的长度
return 0;
}
这段 C++代码实现了相同的逻辑:使用一个无序集合(std::unordered_set
)来存储所有的数字,并且迭代集合中的每个数字,如果当前数字的前一个数字不存在,则认为它可能是连续序列的起点,然后向右扩展序列,最后返回最长的序列长度。
Python 版本
def longest_consecutive(nums):
num_set = set(nums)
longest_streak = 0
for num in num_set:
# 如果num-1不存在,num可能是新序列的起点
if num - 1 not in num_set:
current_num = num
current_streak = 1
# 向右扩展序列
while current_num + 1 in num_set:
current_num += 1
current_streak += 1
# 更新最长序列长度
longest_streak = max(longest_streak, current_streak)
return longest_streak
# 示例
nums = [100, 4, 200, 1, 3, 2]
print(longest_consecutive(nums)) # 输出最长连续序列的长度
这段 Python 代码遵循了之前描述的算法逻辑:首先将所有数字存入一个集合中,然后遍历集合,对于每个可能成为序列起点的数字(即前一个数字不存在的数字),向右扩展序列,最后返回最长序列的长度。
Java 版本
import java.util.HashSet;
import java.util.Set;
public class Solution {
public int longestConsecutive(int[] nums) {
Set<Integer> numSet = new HashSet<>();
for (int num : nums) {
numSet.add(num);
}
int longestStreak = 0;
for (int num : numSet) {
if (!numSet.contains(num - 1)) {
int currentNum = num;
int currentStreak = 1;
while (numSet.contains(currentNum + 1)) {
currentNum += 1;
currentStreak += 1;
}
longestStreak = Math.max(longestStreak, currentStreak);
}
}
return longestStreak;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {100, 4, 200, 1, 3, 2};
System.out.println(solution.longestConsecutive(nums)); // 输出最长连续序列的长度
}
}
这段 Java 代码实现了相同的算法逻辑:首先将所有数字添加到一个 HashSet 中,然后遍历这个集合,对于每个可能成为序列起点的数字(即前一个数字不存在的数字),向右扩展序列,最后返回最长序列的长度。
Rust 版本
use std::collections::HashSet; fn longest_consecutive(nums: Vec<i32>) -> i32 { let num_set: HashSet<i32> = nums.into_iter().collect(); let mut longest_streak = 0; for &num in num_set.iter() { if !num_set.contains(&(num - 1)) { let mut current_num = num; let mut current_streak = 1; while num_set.contains(&(current_num + 1)) { current_num += 1; current_streak += 1; } longest_streak = longest_streak.max(current_streak); } } longest_streak } fn main() { let nums = vec![100, 4, 200, 1, 3, 2]; println!("{}", longest_consecutive(nums)); // 输出最长连续序列的长度 }
这段 Rust 代码实现了之前描述的算法逻辑:首先将所有数字存入一个 HashSet 中,然后遍历这个集合,对于每个可能成为序列起点的数字(即前一个数字不存在的数字),向右扩展序列,最后返回最长序列的长度。
总结
上述问题的解法在不同编程语言中的实现核心思想是一致的,即通过使用哈希表(在不同语言中可能是 HashSet、HashMap、Dictionary 或其他数据结构)来达到 O(n)时间复杂度的要求。解法的步骤可以总结如下:
-
创建哈希表:首先将所有的整数存入哈希表中,这样可以在 O(1)的时间复杂度内查询任何一个数是否存在。
-
寻找序列起点:遍历哈希表中的每个数字,对于每个数字,检查其前一个数字(即当前数字减一)是否存在于哈希表中。如果不存在,说明当前数字可能是一个连续序列的起点。
-
向右扩展序列:从当前序列的起点开始,不断检查下一个连续的数字(即当前数字加一)是否存在于哈希表中,如果存在,则序列可以继续向右扩展。同时,更新当前序列的长度。
-
更新最长序列长度:每次序列扩展结束后,如果当前序列的长度超过了之前记录的最长序列长度,则进行更新。
-
返回结果:所有数字遍历完成后,返回记录的最长序列长度作为结果。
这种解法的优势在于不需要对数组进行排序,从而避免了 O(n log n)的排序时间复杂度,实现了对时间复杂度的优化。不同编程语言的实现差异主要在于语法和数据结构的使用上,但算法的本质是相同的。
双指针
双指针算法是解决数组和链表问题的一种常用技巧,它主要用于减少不必要的计算,降低问题的复杂度。双指针算法通常有以下几种类型:
- 快慢指针:主要用于解决链表中的问题,如检测链表中的循环。
- 左右指针:主要用于有序数组或字符串的问题,如二分查找、合并两个有序数组。
- 对撞指针:主要用于计算有序数组中的两数之和、三数之和等问题。
双指针算法的通用思路包括:
- 确定指针的含义:在开始编写代码之前,你需要明确每个指针的作用,比如一个指针用来遍历,另一个指针用来检查条件等。
- 正确移动指针:根据问题的需要,决定指针是向前移动还是向后移动,以及何时移动指针。
- 维护指针状态:在移动指针的过程中,需要维护和更新指针的状态,以及它们之间的关系。
- 循环结束条件:明确循环结束的条件,确保程序不会进入无限循环,同时能够在正确的时候停止。
下面是一些使用 Go 语言实现的双指针算法的例子:
例 1:反转字符串中的元音字母
func reverseVowels(s string) string {
vowels := "aeiouAEIOU"
bytes := []byte(s)
left, right := 0, len(s)-1
for left < right {
for left < right && !strings.ContainsRune(vowels, rune(bytes[left])) {
left++
}
for left < right && !strings.ContainsRune(vowels, rune(bytes[right])) {
right--
}
bytes[left], bytes[right] = bytes[right], bytes[left]
left++
right--
}
return string(bytes)
}
例 2:有序数组的 Two Sum
func twoSum(numbers []int, target int) []int {
left, right := 0, len(numbers)-1
for left < right {
sum := numbers[left] + numbers[right]
if sum == target {
return []int{left + 1, right + 1}
} else if sum < target {
left++
} else {
right--
}
}
return []int{-1, -1} // 如果没有找到解
}
例 3:移除元素
func removeElement(nums []int, val int) int {
left := 0
for right := 0; right < len(nums); right++ {
if nums[right] != val {
nums[left] = nums[right]
left++
}
}
return left
}
在使用双指针时,关键是要理解如何移动指针以及如何根据问题的需求来更新指针的状态。每个问题可能需要不同的策略,但上述提供的示例代码可以作为一个起点来解决类似的问题。
移动零
题目要求
编写一个函数,该函数接受一个整数数组nums
作为参数。函数的目标是将数组中所有的 0 元素移动到数组的末尾,同时保持所有非零元素的原有顺序不变。在移动元素时,不能使用额外的数组来复制,必须在原数组上进行操作,即原地修改数组。
解题思路
解决这个问题的关键是要在遍历数组的同时,找到一种有效的方式来将非零元素向前移动,而将 0 元素留在数组的后部。以下是一个高效的解题步骤:
- 初始化一个指针(比如叫
nonZeroIndex
),用于记录下一个非零元素应该放置的位置。 - 遍历数组
nums
,对于每个元素执行以下操作:- 如果当前元素不是 0,将其移动到
nonZeroIndex
指示的位置,然后将nonZeroIndex
加 1。 - 如果当前元素是 0,则不做任何操作,继续遍历。
- 如果当前元素不是 0,将其移动到
- 遍历完成后,
nonZeroIndex
及其之后的所有位置都应该被填充为 0,因为所有的非零元素都已经移动到了数组的前部。
这个方法的关键点在于,nonZeroIndex
指针始终指向当前已处理序列的末尾的下一个位置,这样可以确保非零元素的相对顺序不变,并且所有的 0 都被推到数组的末尾。
这种方法的时间复杂度为 O(n),因为我们只需要遍历数组一次。空间复杂度为 O(1),因为我们没有使用额外的存储空间。
Golang 版本
package main
import "fmt"
// moveZeroes 函数将所有0移动到数组末尾,同时保持非零元素的相对顺序。
func moveZeroes(nums []int) {
nonZeroIndex := 0 // 初始化非零元素应该放置的位置
// 遍历数组,将非零元素移动到nonZeroIndex的位置
for _, num := range nums {
if num != 0 {
nums[nonZeroIndex] = num
nonZeroIndex++
}
}
// 将nonZeroIndex之后的所有元素设置为0
for i := nonZeroIndex; i < len(nums); i++ {
nums[i] = 0
}
}
func main() {
nums := []int{0, 1, 0, 3, 12}
moveZeroes(nums)
fmt.Println(nums) // 输出应为[1, 3, 12, 0, 0]
}
C++版本
#include <iostream>
#include <vector>
// moveZeroes函数将所有0移动到数组末尾,同时保持非零元素的相对顺序。
void moveZeroes(std::vector<int>& nums) {
int nonZeroIndex = 0; // 初始化非零元素应该放置的位置
// 遍历数组,将非零元素移动到nonZeroIndex的位置
for (int num : nums) {
if (num != 0) {
nums[nonZeroIndex++] = num;
}
}
// 将nonZeroIndex之后的所有元素设置为0
for (int i = nonZeroIndex; i < nums.size(); ++i) {
nums[i] = 0;
}
}
int main() {
std::vector<int> nums = {0, 1, 0, 3, 12};
moveZeroes(nums);
// 输出结果,应为[1, 3, 12, 0, 0]
for (int num : nums) {
std::cout << num << ' ';
}
std::cout << std::endl;
return 0;
}
Python 版本
def move_zeroes(nums):
non_zero_index = 0 # 初始化非零元素应该放置的位置
# 遍历数组,将非零元素移动到non_zero_index的位置
for num in nums:
if num != 0:
nums[non_zero_index] = num
non_zero_index += 1
# 将non_zero_index之后的所有元素设置为0
for i in range(non_zero_index, len(nums)):
nums[i] = 0
# 测试代码
if __name__ == "__main__":
nums = [0, 1, 0, 3, 12]
move_zeroes(nums)
print(nums) # 输出应为[1, 3, 12, 0, 0]
Java 版本
public class MoveZeroes {
public static void moveZeroes(int[] nums) {
int nonZeroIndex = 0; // 初始化非零元素应该放置的位置
// 遍历数组,将非零元素移动到nonZeroIndex的位置
for (int num : nums) {
if (num != 0) {
nums[nonZeroIndex++] = num;
}
}
// 将nonZeroIndex之后的所有元素设置为0
for (int i = nonZeroIndex; i < nums.length; i++) {
nums[i] = 0;
}
}
public static void main(String[] args) {
int[] nums = {0, 1, 0, 3, 12};
moveZeroes(nums);
// 输出结果,应为[1, 3, 12, 0, 0]
for (int num : nums) {
System.out.print(num + " ");
}
}
}
Rust 版本
fn move_zeroes(nums: &mut Vec<i32>) { let mut non_zero_index = 0; // 初始化非零元素应该放置的位置 // 遍历数组,将非零元素移动到non_zero_index的位置 for &num in nums.iter() { if num != 0 { nums[non_zero_index] = num; non_zero_index += 1; } } // 将non_zero_index之后的所有元素设置为0 for i in non_zero_index..nums.len() { nums[i] = 0; } } fn main() { let mut nums = vec![0, 1, 0, 3, 12]; move_zeroes(&mut nums); // 输出结果,应为[1, 3, 12, 0, 0] for num in nums { print!("{} ", num); } }
总结
无论是在 Go, C++, Python, Java 还是 Rust 语言中,解决将数组中所有 0 移动到末尾的问题的基本思路都是相同的。以下是这个问题的通用解决方案的步骤:
- 初始化一个变量(例如
nonZeroIndex
),用于跟踪数组中非零元素应该插入的位置。 - 遍历数组,对于每个非零元素,将其移动到
nonZeroIndex
指示的位置,并将nonZeroIndex
递增。 - 在数组剩余的位置上填充 0,这些位置从
nonZeroIndex
开始到数组的末尾。
这个算法的时间复杂度是 O(n),因为它只需要遍历数组一次。空间复杂度是 O(1),因为它不需要额外的存储空间,所有的操作都是在原地完成的。
在不同的编程语言中,这个算法的实现细节可能略有不同,例如在 Rust 中使用iter()
方法来遍历数组,在 Python 中直接遍历数组,在 C++和 Java 中使用范围基的 for 循环。但是,核心算法和逻辑是一致的。
盛最多水的容器
题目要求
给定一个长度为 n 的整数数组 height
,该数组代表了 n 条垂直线段的高度,每条线段的两个端点分别是 (i, 0)
和 (i, height[i])
。任务是从这些线段中找出两条线,这两条线与 x 轴一起形成的容器可以存储最大量的水。
需要返回的是这个容器能够存储的水的最大量。注意,构成容器的线段必须是垂直于 x 轴的,也就是说,容器的边不能倾斜。
解题思路
这个问题可以通过双指针法来解决。初始时,我们可以将两个指针分别放在数组的开始和结束位置,这代表了容器的两边。容器的容量由两个指针之间的距离(容器的宽度)和两个指针中较短线段的高度(容器的高度)的乘积来决定。
解题步骤如下:
- 初始化两个指针:left 指针在数组的开始位置,right 指针在数组的结束位置。
- 初始化最大容量 max_area 为 0。
- 当 left 指针小于 right 指针时,执行以下步骤:
- 计算当前 left 和 right 指针所指线段构成的容器的容量,计算方式为
min(height[left], height[right]) * (right - left)
。 - 更新 max_area,如果当前计算的容量大于 max_area,则将其赋值给 max_area。
- 移动指针以尝试找到更大的容量。由于容器的容量受限于较短的线段,我们应该移动较短的那个线段对应的指针。如果
height[left] < height[right]
,则移动 left 指针,否则移动 right 指针。
- 计算当前 left 和 right 指针所指线段构成的容器的容量,计算方式为
- 重复步骤 3,直到 left 指针不再小于 right 指针。
- 返回 max_area 作为结果。
这种方法的关键在于,我们总是尝试通过移动较短的线段来找到可能更高的线段,因为这是增加容器容量的唯一方式。容器的宽度随着指针的移动是单调递减的,所以我们需要通过提高高度来弥补宽度的减少,以此来找到可能的最大容量。
Golang 版本
package main
import "fmt"
func maxArea(height []int) int {
left, right := 0, len(height)-1
maxArea := 0
for left < right {
// Calculate the area with the current boundary
width := right - left
high := min(height[left], height[right])
currentArea := width * high
// Update maxArea if the current area is greater
maxArea = max(maxArea, currentArea)
// Move the shorter line to try and find a taller line
if height[left] < height[right] {
left++
} else {
right--
}
}
return maxArea
}
// Helper function to find the minimum of two integers
func min(x, y int) int {
if x < y {
return x
}
return y
}
// Helper function to find the maximum of two integers
func max(x, y int) int {
if x > y {
return x
}
return y
}
func main() {
// Example usage:
height := []int{1,8,6,2,5,4,8,3,7}
fmt.Println("The maximum amount of water that can be contained is:", maxArea(height))
}
这段代码定义了一个 maxArea
函数,它接受一个整数切片 height
作为参数,并返回一个整数,表示按照题目要求能够容纳的最大水量。主函数 main
提供了一个示例用法。
C++版本
#include <iostream>
#include <vector>
#include <algorithm>
int maxArea(std::vector<int>& height) {
int left = 0, right = height.size() - 1;
int maxArea = 0;
while (left < right) {
// Calculate the area with the current boundary
int width = right - left;
int high = std::min(height[left], height[right]);
int currentArea = width * high;
// Update maxArea if the current area is greater
maxArea = std::max(maxArea, currentArea);
// Move the shorter line to try and find a taller line
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return maxArea;
}
int main() {
// Example usage:
std::vector<int> height = {1,8,6,2,5,4,8,3,7};
std::cout << "The maximum amount of water that can be contained is: " << maxArea(height) << std::endl;
return 0;
}
这段代码定义了一个 maxArea
函数,它接受一个整数向量 height
作为参数,并返回一个整数,表示按照题目要求能够容纳的最大水量。main
函数提供了一个示例用法。
Python 版本
def max_area(height):
left, right = 0, len(height) - 1
max_area = 0
while left < right:
# Calculate the area with the current boundary
width = right - left
high = min(height[left], height[right])
current_area = width * high
# Update max_area if the current area is greater
max_area = max(max_area, current_area)
# Move the shorter line to try and find a taller line
if height[left] < height[right]:
left += 1
else:
right -= 1
return max_area
# Example usage:
if __name__ == "__main__":
height = [1, 8, 6, 2, 5, 4, 8, 3, 7]
print("The maximum amount of water that can be contained is:", max_area(height))
这段代码定义了一个 max_area
函数,它接受一个整数列表 height
作为参数,并返回一个整数,表示按照题目要求能够容纳的最大水量。if __name__ == "__main__":
部分提供了一个示例用法。
Java 版本
public class Solution {
public int maxArea(int[] height) {
int left = 0, right = height.length - 1;
int maxArea = 0;
while (left < right) {
// Calculate the area with the current boundary
int width = right - left;
int high = Math.min(height[left], height[right]);
int currentArea = width * high;
// Update maxArea if the current area is greater
maxArea = Math.max(maxArea, currentArea);
// Move the shorter line to try and find a taller line
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return maxArea;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] height = {1, 8, 6, 2, 5, 4, 8, 3, 7};
System.out.println("The maximum amount of water that can be contained is: " + solution.maxArea(height));
}
}
这段代码定义了一个名为 Solution
的类,其中包含了 maxArea
方法,该方法接受一个整数数组 height
作为参数,并返回一个整数,表示按照题目要求能够容纳的最大水量。main
方法提供了一个示例用法。
Rust 版本
fn max_area(height: Vec<i32>) -> i32 { let (mut left, mut right) = (0, height.len() - 1); let mut max_area = 0; while left < right { let width = (right - left) as i32; let high = std::cmp::min(height[left], height[right]); let current_area = width * high; max_area = std::cmp::max(max_area, current_area); if height[left] < height[right] { left += 1; } else { right -= 1; } } max_area } fn main() { let height = vec![1, 8, 6, 2, 5, 4, 8, 3, 7]; println!("The maximum amount of water that can be contained is: {}", max_area(height)); }
这段代码定义了一个 max_area
函数,它接受一个整数向量 height
作为参数,并返回一个整数,表示按照题目要求能够容纳的最大水量。main
函数提供了一个示例用法。
总结
上述问题是一个经典的数组操作问题,通常称为“盛最多水的容器”。解决这个问题的关键在于使用双指针法。以下是解决这个问题的步骤:
- 初始化两个指针,一个在数组的开始位置(left),另一个在数组的结束位置(right)。
- 初始化一个变量来跟踪最大面积(maxArea)。
- 当左指针小于右指针时,执行以下操作:
- 计算当前两个指针之间的宽度(right - left)。
- 找出两个指针指向的高度中较小的一个。
- 计算当前的面积(当前宽度 * 较小的高度)。
- 如果当前面积大于之前记录的最大面积,则更新最大面积。
- 移动较短的那个指针(如果左边的高度小于右边的高度,则移动左指针;否则,移动右指针)。
- 当左指针不再小于右指针时,结束循环。
- 返回记录的最大面积。
这个算法的时间复杂度是 O(n),因为每个元素只被访问一次。空间复杂度是 O(1),因为只使用了常数空间。
在不同的编程语言中,这个算法的实现细节可能略有不同,但核心逻辑是一致的。例如,在 Rust 中,数组的长度是通过 len()
方法获取的,而在 Python 中则是通过内置的 len()
函数。同样,最小值和最大值的函数在不同的语言中也有所不同(如 std::min
、std::max
、Math.min
、Math.max
等)。
三数之和
题目要求
给定一个整数数组 nums
,要求找出所有不同的三元组 [nums[i], nums[j], nums[k]]
,使得 i
、j
、k
互不相同,并且满足 nums[i] + nums[j] + nums[k] == 0
。需要注意的是,返回的三元组列表中不能包含重复的三元组。
解题思路
解决这个问题的一个有效方法是使用排序加双指针的策略。以下是解题的步骤:
-
排序数组:首先对数组
nums
进行排序。排序的目的是为了方便后续操作,可以使用双指针技术来避免重复的三元组,并且可以更快地跳过那些不可能组成和为 0 的三元组。 -
遍历数组:遍历排序后的数组,对于每个元素
nums[i]
,设置两个指针,一个指向i+1
(左指针),另一个指向数组的最后一个元素(右指针)。遍历的过程中,如果i
大于 0 且nums[i]
等于nums[i-1]
,则跳过这个元素以避免重复的三元组。 -
双指针查找:在固定了
nums[i]
之后,移动左右指针来寻找两个数,使得它们的和加上nums[i]
等于 0。如果找到了这样的两个数,就将它们与nums[i]
一起作为一个三元组添加到结果列表中。添加后,需要继续移动左右指针,并跳过所有重复的值,以防止添加重复的三元组。 -
左指针右移:如果当前的三个数的和小于 0,说明需要增加数值,因此将左指针向右移动。
-
右指针左移:如果当前的三个数的和大于 0,说明需要减少数值,因此将右指针向左移动。
-
重复上述步骤:重复步骤 3 到 5,直到左指针不再小于右指针。
-
返回结果:遍历完成后,返回存储三元组的结果列表。
通过以上步骤,可以确保找到所有不重复的三元组,且每个三元组的和为 0。
Golang 版本
package main
import (
"fmt"
"sort"
)
func threeSum(nums []int) [][]int {
sort.Ints(nums)
result := make([][]int, 0)
for i := 0; i < len(nums)-2; i++ {
// 跳过重复的数字
if i > 0 && nums[i] == nums[i-1] {
continue
}
left, right := i+1, len(nums)-1
for left < right {
sum := nums[i] + nums[left] + nums[right]
switch {
case sum < 0:
left++
case sum > 0:
right--
default:
result = append(result, []int{nums[i], nums[left], nums[right]})
// 跳过重复的数字
for left < right && nums[left] == nums[left+1] {
left++
}
for left < right && nums[right] == nums[right-1] {
right--
}
left++
right--
}
}
}
return result
}
func main() {
nums := []int{-1, 0, 1, 2, -1, -4}
result := threeSum(nums)
fmt.Println(result) // 输出找到的所有不重复的三元组
}
这段代码定义了一个 threeSum
函数,它接受一个整数切片 nums
作为输入,并返回一个二维切片,其中包含所有和为 0 的不重复三元组。主函数 main
提供了一个示例输入,并打印出 threeSum
函数的输出结果。
C++版本
#include <iostream>
#include <vector>
#include <algorithm>
std::vector<std::vector<int>> threeSum(std::vector<int>& nums) {
std::vector<std::vector<int>> result;
std::sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size(); ++i) {
if (i > 0 && nums[i] == nums[i - 1]) {
continue; // 跳过重复值
}
int left = i + 1, right = nums.size() - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum < 0) {
++left;
} else if (sum > 0) {
--right;
} else {
result.push_back({nums[i], nums[left], nums[right]});
// 跳过所有重复的left和right
while (left < right && nums[left] == nums[left + 1]) ++left;
while (left < right && nums[right] == nums[right - 1]) --right;
++left;
--right;
}
}
}
return result;
}
int main() {
std::vector<int> nums = {-1, 0, 1, 2, -1, -4};
std::vector<std::vector<int>> result = threeSum(nums);
for (const auto& triplet : result) {
std::cout << '[';
for (int i = 0; i < triplet.size(); ++i) {
std::cout << triplet[i];
if (i < triplet.size() - 1) std::cout << ", ";
}
std::cout << ']' << std::endl;
}
return 0;
}
这段 C++代码定义了一个 threeSum
函数,它接受一个整数向量 nums
作为输入,并返回一个二维向量,其中包含所有和为 0 的不重复三元组。main
函数提供了一个示例输入,并打印出 threeSum
函数的输出结果。
Python 版本
def three_sum(nums):
nums.sort()
result = []
length = len(nums)
for i in range(length - 2):
if i > 0 and nums[i] == nums[i - 1]:
continue # Skip the duplicate numbers
left, right = i + 1, length - 1
while left < right:
total = nums[i] + nums[left] + nums[right]
if total < 0:
left += 1
elif total > 0:
right -= 1
else:
result.append([nums[i], nums[left], nums[right]])
while left < right and nums[left] == nums[left + 1]:
left += 1 # Skip the duplicate numbers
while left < right and nums[right] == nums[right - 1]:
right -= 1 # Skip the duplicate numbers
left += 1
right -= 1
return result
# Example usage:
nums = [-1, 0, 1, 2, -1, -4]
print(three_sum(nums))
这段 Python 代码定义了一个 three_sum
函数,它接受一个整数列表 nums
作为输入,并返回一个列表,其中包含所有和为 0 的不重复三元组。示例用法展示了如何调用这个函数并打印结果。
Java 版本
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Solution {
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> result = new ArrayList<>();
for (int i = 0; i < nums.length - 2; i++) {
if (i > 0 && nums[i] == nums[i - 1]) {
continue; // Skip duplicate elements
}
int left = i + 1, right = nums.length - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) {
result.add(Arrays.asList(nums[i], nums[left], nums[right]));
while (left < right && nums[left] == nums[left + 1]) {
left++; // Skip duplicate elements
}
while (left < right && nums[right] == nums[right - 1]) {
right--; // Skip duplicate elements
}
left++;
right--;
} else if (sum < 0) {
left++;
} else {
right--;
}
}
}
return result;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {-1, 0, 1, 2, -1, -4};
List<List<Integer>> result = solution.threeSum(nums);
for (List<Integer> triplet : result) {
System.out.println(triplet);
}
}
}
这段 Java 代码定义了一个 Solution
类,其中包含一个 threeSum
方法,它接受一个整数数组 nums
作为输入,并返回一个列表,其中包含所有和为 0 的不重复三元组。main
方法提供了一个示例输入,并打印出 threeSum
方法的输出结果。
Rust 版本
use std::collections::HashSet; fn three_sum(nums: Vec<i32>) -> Vec<Vec<i32>> { let mut nums = nums; nums.sort_unstable(); let mut result = HashSet::new(); for i in 0..nums.len() { if i > 0 && nums[i] == nums[i - 1] { continue; // Skip the same result } let (mut start, mut end) = (i + 1, nums.len() - 1); while start < end { let sum = nums[i] + nums[start] + nums[end]; match sum.cmp(&0) { std::cmp::Ordering::Equal => { result.insert(vec![nums[i], nums[start], nums[end]]); start += 1; end -= 1; while start < end && nums[start] == nums[start - 1] { start += 1; // Skip the same number } while start < end && nums[end] == nums[end + 1] { end -= 1; // Skip the same number } } std::cmp::Ordering::Less => start += 1, std::cmp::Ordering::Greater => end -= 1, } } } result.into_iter().collect() } fn main() { let nums = vec![-1, 0, 1, 2, -1, -4]; let triplets = three_sum(nums); for triplet in triplets { println!("{:?}", triplet); } }
这段 Rust 代码定义了一个 three_sum
函数,它接受一个整数向量 nums
作为输入,并返回一个向量,其中包含所有和为 0 的不重复三元组。main
函数提供了一个示例输入,并打印出 three_sum
函数的输出结果。在这个实现中,使用了 HashSet
来自动去重复的三元组,最后将其转换为一个向量。
总结
上述 Rust 版本的解法采用了以下步骤来找出所有和为 0 的不重复三元组:
-
排序: 首先对输入的整数向量
nums
进行排序。这是为了方便后续的双指针遍历,同时也使得相同的数字聚集在一起,便于跳过重复的三元组。 -
遍历: 使用外层循环遍历排序后的数组,从第一个元素开始,直到倒数第三个元素。这个元素将作为三元组中的第一个数字。
-
跳过重复的元素: 在外层循环中,如果当前元素与前一个元素相同,则跳过当前元素,以避免找到重复的三元组。
-
双指针查找: 对于每个外层循环选定的元素,使用两个指针(
start
和end
)在剩余数组中寻找两个数,使得这两个数与当前选定的元素之和为 0。start
指针从当前元素的下一个元素开始,end
指针从数组的最后一个元素开始向前移动。 -
三数之和判断: 如果三个数的和小于 0,则将
start
指针向右移动;如果和大于 0,则将end
指针向左移动;如果和等于 0,则将这三个数作为一个三元组添加到结果集中。 -
处理指针重复: 在找到一个有效的三元组后,需要移动
start
和end
指针,同时跳过所有重复的元素,以防止添加重复的三元组到结果集中。 -
结果去重: 使用
HashSet
来存储三元组,这样可以自动去除重复的三元组。最后将HashSet
转换为向量形式返回。 -
输出结果: 在
main
函数中,调用three_sum
函数并打印出返回的所有三元组。
这种解法的时间复杂度主要由排序和双指针遍历决定,排序的时间复杂度为 O(n log n),双指针遍历的时间复杂度为 O(n^2),因此总的时间复杂度为 O(n^2)。空间复杂度主要由结果集的存储决定,最坏情况下为 O(n^2)。
接雨水
题目要求
给定一个数组,它的第 i 个元素表示一个宽度为 1 的柱子的高度。这些柱子按照顺序排列,形成一个高度图。当下雨时,柱子之间的凹槽可能会积水。需要编写一个算法来计算整个高度图在一场雨后能够接多少单位的雨水。
解题思路
要解决这个问题,我们可以通过以下步骤来逐步逼近解决方案:
-
理解问题:首先要明白雨水能够积存的地方是在连续的柱子之间形成的凹槽中。雨水的多少取决于凹槽的深度和宽度,而凹槽的深度又取决于凹槽两边柱子的高度。
-
找到边界:对于数组中的任意一个位置,能积水的量取决于它左边和右边最高的柱子的高度(不包括它自己)。因此,对于每个位置,我们需要知道其左边最高的柱子和右边最高的柱子。
-
计算积水量:对于数组中的每个元素,我们计算左边最高的柱子和右边最高的柱子,取二者之间较小的一个,然后减去当前柱子的高度,就得到了当前位置能积水的量。如果计算结果为负数,则表示当前位置不会积水,积水量为 0。
-
优化存储:为了避免重复计算,我们可以使用两个数组来分别存储每个位置左边最高的柱子和右边最高的柱子。这样,我们只需要遍历一次数组就可以计算出所有位置的积水量。
-
总结积水量:最后,我们将所有位置的积水量相加,就得到了整个高度图能接的雨水总量。
-
边界条件处理:在实际编码时,还需要注意处理数组的边界条件,例如数组为空或只有一个元素时,显然不会积水。
通过以上步骤,我们可以得到一个有效的算法来解决这个问题。注意,这个问题也可以通过栈或双指针等方法来解决,但上述方法是最直观和容易理解的。
Golang 版本
package main
import (
"fmt"
)
func trap(height []int) int {
if len(height) == 0 {
return 0
}
leftMax := make([]int, len(height))
rightMax := make([]int, len(height))
// Fill leftMax array
leftMax[0] = height[0]
for i := 1; i < len(height); i++ {
leftMax[i] = max(height[i], leftMax[i-1])
}
// Fill rightMax array
rightMax[len(height)-1] = height[len(height)-1]
for i := len(height) - 2; i >= 0; i-- {
rightMax[i] = max(height[i], rightMax[i+1])
}
// Calculate total trapped water
water := 0
for i := 0; i < len(height); i++ {
water += min(leftMax[i], rightMax[i]) - height[i]
}
return water
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func main() {
height := []int{0,1,0,2,1,0,1,3,2,1,2,1}
fmt.Println("Total trapped water:", trap(height))
}
这段代码定义了一个trap
函数,它接受一个整数数组height
作为输入,代表柱子的高度图,并返回一个整数,代表按此排列的柱子,下雨之后能接多少雨水。主函数main
中给出了一个示例输入,并打印出计算的结果。
C++版本
#include <iostream>
#include <vector>
#include <algorithm>
int trap(const std::vector<int>& height) {
if (height.empty()) {
return 0;
}
int n = height.size();
std::vector<int> leftMax(n), rightMax(n);
int waterTrapped = 0;
leftMax[0] = height[0];
for (int i = 1; i < n; ++i) {
leftMax[i] = std::max(leftMax[i - 1], height[i]);
}
rightMax[n - 1] = height[n - 1];
for (int i = n - 2; i >= 0; --i) {
rightMax[i] = std::max(rightMax[i + 1], height[i]);
}
for (int i = 0; i < n; ++i) {
waterTrapped += std::min(leftMax[i], rightMax[i]) - height[i];
}
return waterTrapped;
}
int main() {
std::vector<int> height = {0,1,0,2,1,0,1,3,2,1,2,1};
std::cout << "Total trapped water: " << trap(height) << std::endl;
return 0;
}
这段 C++代码实现了一个trap
函数,它接受一个整数类型的std::vector
作为输入,代表柱子的高度图,并返回一个整数,代表按此排列的柱子,下雨之后能接多少雨水。main
函数中给出了一个示例输入,并打印出计算的结果。
Python 版本
def trap(height):
if not height:
return 0
n = len(height)
left_max, right_max = [0] * n, [0] * n
water_trapped = 0
left_max[0] = height[0]
for i in range(1, n):
left_max[i] = max(left_max[i - 1], height[i])
right_max[n - 1] = height[n - 1]
for i in range(n - 2, -1, -1):
right_max[i] = max(right_max[i + 1], height[i])
for i in range(n):
water_trapped += min(left_max[i], right_max[i]) - height[i]
return water_trapped
# Example usage:
height = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]
print(f"Total trapped water: {trap(height)}")
这段 Python 代码定义了一个trap
函数,它接受一个列表height
作为输入,代表柱子的高度图,并返回一个整数,代表按此排列的柱子,下雨之后能接多少雨水。在代码的最后,给出了一个示例输入,并打印出计算的结果。
Java 版本
public class Solution {
public int trap(int[] height) {
if (height == null || height.length == 0) {
return 0;
}
int n = height.length;
int[] leftMax = new int[n];
int[] rightMax = new int[n];
int waterTrapped = 0;
leftMax[0] = height[0];
for (int i = 1; i < n; i++) {
leftMax[i] = Math.max(leftMax[i - 1], height[i]);
}
rightMax[n - 1] = height[n - 1];
for (int i = n - 2; i >= 0; i--) {
rightMax[i] = Math.max(rightMax[i + 1], height[i]);
}
for (int i = 0; i < n; i++) {
waterTrapped += Math.min(leftMax[i], rightMax[i]) - height[i];
}
return waterTrapped;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] height = {0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1};
System.out.println("Total trapped water: " + solution.trap(height));
}
}
这段 Java 代码定义了一个Solution
类,其中包含一个trap
方法用于计算接雨水的总量。trap
方法接受一个整型数组height
作为输入,代表柱子的高度图,并返回一个整数,代表按此排列的柱子,下雨之后能接多少雨水。main
方法中给出了一个示例输入,并打印出计算的结果。
Rust 版本
fn trap(height: Vec<i32>) -> i32 { let n = height.len(); if n == 0 { return 0; } let mut left_max = vec![0; n]; let mut right_max = vec![0; n]; let mut water_trapped = 0; left_max[0] = height[0]; for i in 1..n { left_max[i] = std::cmp::max(left_max[i - 1], height[i]); } right_max[n - 1] = height[n - 1]; for i in (0..n - 1).rev() { right_max[i] = std::cmp::max(right_max[i + 1], height[i]); } for i in 0..n { water_trapped += std::cmp::min(left_max[i], right_max[i]) - height[i]; } water_trapped } fn main() { let height = vec![0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]; println!("Total trapped water: {}", trap(height)); }
这段 Rust 代码定义了一个trap
函数,它接受一个Vec<i32>
类型的向量height
作为输入,代表柱子的高度图,并返回一个i32
类型的整数,代表按此排列的柱子,下雨之后能接多少雨水。main
函数中给出了一个示例输入,并打印出计算的结果。
总结
上述解法采用了动态规划的思想来解决接雨水问题。核心思路是对于高度图中的每个位置,找出其左侧和右侧的最大高度,这两个值决定了当前位置能够积水的最大高度。具体步骤如下:
- 初始化两个数组
left_max
和right_max
,分别用来存储每个位置左侧和右侧的最大高度。 - 从左到右遍历高度图,更新
left_max
数组,使得left_max[i]
存储height[0..i]
中的最大值。 - 从右到左遍历高度图,更新
right_max
数组,使得right_max[i]
存储height[i..n-1]
中的最大值。 - 再次遍历高度图,对于每个位置,计算左侧和右侧最大高度的较小值,然后减去当前位置的高度,得到当前位置的积水量。如果计算结果小于零,则当前位置积水量为零。
- 将所有位置的积水量累加,得到整个高度图的总积水量。
这种方法的时间复杂度为 O(n),因为每个元素只需要遍历一次来构建left_max
和right_max
数组,然后再遍历一次来计算积水量。空间复杂度也为 O(n),因为需要额外的空间来存储左侧和右侧的最大高度数组。
滑动窗口
滑动窗口算法是一种用于解决数组/字符串问题的技巧,特别适用于求解子数组/子字符串的问题,比如最长无重复字符的子字符串、有 K 个最大元素的子数组等问题。滑动窗口可以帮助我们在 O(N)的时间复杂度内解决这类问题。
滑动窗口算法的通用思路如下:
-
确定窗口的边界:窗口通常由两个指针定义,一个表示窗口的开始,另一个表示窗口的结束。
-
移动窗口的结束指针:增大窗口,直到窗口内的子数组/子字符串满足题目要求。
-
移动窗口的开始指针:在保持窗口内子数组/子字符串满足题目要求的前提下,缩小窗口以寻找更小的满足条件的窗口。
-
在移动窗口的过程中,根据题目要求计算所需的值。
-
记录所需的最优解。
下面是一个使用 Go 语言实现的滑动窗口算法的例子,该例子解决的是找出字符串中最长无重复字符的子字符串的长度:
package main
import (
"fmt"
)
func lengthOfLongestSubstring(s string) int {
charIndexMap := make(map[byte]int)
maxLength := 0
start := 0 // 窗口开始位置
for end := 0; end < len(s); end++ {
if index, ok := charIndexMap[s[end]]; ok && index >= start {
// 如果字符已经在窗口中,移动窗口的开始位置
start = index + 1
}
// 更新字符的最新位置
charIndexMap[s[end]] = end
// 更新最长无重复字符的子字符串的长度
if end-start+1 > maxLength {
maxLength = end - start + 1
}
}
return maxLength
}
func main() {
fmt.Println(lengthOfLongestSubstring("abcabcbb")) // 输出: 3
}
在这个例子中,我们使用了一个哈希表charIndexMap
来记录字符最后一次出现的位置。我们的窗口由start
和end
两个指针定义,分别表示窗口的开始和结束位置。我们遍历字符串,不断移动end
指针来扩大窗口。如果遇到一个已经在窗口中的字符,我们就更新start
指针来缩小窗口。在每一步中,我们都检查并更新最长无重复字符子字符串的长度。
滑动窗口算法的关键在于如何根据问题的需求来合理地移动窗口的两个指针,并在过程中维护和更新必要的数据结构和变量。
无重复字符的最长子串
题目要求
给定一个字符串 s,要求编写一个算法来找出这个字符串中最长的不包含重复字符的子串,并返回这个最长子串的长度。
解题思路
解决这个问题的一个有效方法是使用滑动窗口算法。滑动窗口是数组/字符串问题中常用的抽象概念。窗口通常是在数组/字符串中的一个子列表,并且可以在一定条件下向左或向右滑动。
具体到这个问题,我们可以定义一个窗口,它可以在字符串上从左向右滑动,窗口内是一个不包含重复字符的子串。我们的目标是找到最长的这样的窗口。
以下是解题步骤:
- 初始化两个指针,分别表示窗口的左边界和右边界,初始化为字符串的起始位置。
- 使用一个数据结构(如哈希表)来存储窗口内的字符及其索引,以便快速判断字符是否重复以及获取重复字符的索引。
- 移动右边界,扩展窗口,将新字符加入到哈希表中。如果字符不在哈希表中,更新当前无重复字符子串的长度,并与已知的最长长度进行比较,如果更长,则更新最长长度。
- 如果遇到哈希表中已存在的字符,说明当前子串中已经有了重复字符,此时需要移动左边界来缩小窗口。移动左边界到重复字符的下一个位置,并在哈希表中更新相关字符的索引。
- 重复步骤 3 和 4,直到右边界到达字符串的末尾。
- 返回记录的最长长度作为答案。
这个算法的关键在于,当我们遇到重复字符时,我们可以跳过窗口中所有重复字符之前的部分,因为这些部分不可能是最长的无重复字符子串。这样,我们就不需要对这些字符再次进行检查,从而提高了算法的效率。
Golang 版本
func lengthOfLongestSubstring(s string) int {
charIndexMap := make(map[byte]int)
maxLength := 0
start := 0 // 窗口开始位置
for i := 0; i < len(s); i++ {
if index, ok := charIndexMap[s[i]]; ok && index >= start {
// 如果字符s[i]在窗口中已存在,移动窗口的开始位置
start = index + 1
}
// 更新字符索引
charIndexMap[s[i]] = i
// 计算当前窗口长度,并更新最大长度
if i-start+1 > maxLength {
maxLength = i - start + 1
}
}
return maxLength
}
这段代码实现了上述的滑动窗口算法。它使用一个哈希表charIndexMap
来存储字符和它们最新的索引。变量start
表示当前窗口的开始位置,maxLength
用于记录遇到的最长子串的长度。遍历字符串s
时,如果当前字符在哈希表中存在且索引大于等于start
,则更新start
到重复字符的下一个位置。每次迭代都会更新哈希表,并可能更新maxLength
。最终返回maxLength
作为结果。
C++版本
#include <string>
#include <unordered_map>
#include <algorithm>
int lengthOfLongestSubstring(std::string s) {
std::unordered_map<char, int> charIndexMap;
int maxLength = 0;
int start = 0; // 窗口开始位置
for (int i = 0; i < s.length(); ++i) {
auto found = charIndexMap.find(s[i]);
if (found != charIndexMap.end() && found->second >= start) {
// 如果字符s[i]在窗口中已存在,移动窗口的开始位置
start = found->second + 1;
}
// 更新字符索引
charIndexMap[s[i]] = i;
// 计算当前窗口长度,并更新最大长度
maxLength = std::max(maxLength, i - start + 1);
}
return maxLength;
}
这段 C++代码实现了与 Golang 版本相同的滑动窗口算法。它使用std::unordered_map
来存储字符和它们最新的索引。变量start
表示当前窗口的开始位置,maxLength
用于记录遇到的最长子串的长度。在遍历字符串s
时,如果当前字符在哈希表中存在且索引大于等于start
,则更新start
到重复字符的下一个位置。每次迭代都会更新哈希表,并可能更新maxLength
。最终返回maxLength
作为结果。
Python 版本
def length_of_longest_substring(s: str) -> int:
char_index_map = {}
max_length = 0
start = 0 # 窗口开始位置
for i, char in enumerate(s):
if char in char_index_map and char_index_map[char] >= start:
start = char_index_map[char] + 1
char_index_map[char] = i
max_length = max(max_length, i - start + 1)
return max_length
这段 Python 代码实现了前面描述的滑动窗口算法。它使用一个字典char_index_map
来存储字符和它们最新的索引。变量start
表示当前窗口的开始位置,max_length
用于记录遇到的最长子串的长度。在遍历字符串s
时,如果当前字符在字典中存在且索引大于等于start
,则更新start
到重复字符的下一个位置。每次迭代都会更新字典,并可能更新max_length
。最终返回max_length
作为结果。
Java 版本
import java.util.HashMap;
public class Solution {
public int lengthOfLongestSubstring(String s) {
HashMap<Character, Integer> charIndexMap = new HashMap<>();
int maxLength = 0;
int start = 0; // 窗口开始位置
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
if (charIndexMap.containsKey(ch) && charIndexMap.get(ch) >= start) {
start = charIndexMap.get(ch) + 1;
}
charIndexMap.put(ch, i);
maxLength = Math.max(maxLength, i - start + 1);
}
return maxLength;
}
}
这段 Java 代码实现了滑动窗口算法。它使用一个HashMap
来存储字符和它们最新的索引。变量start
表示当前窗口的开始位置,maxLength
用于记录遇到的最长子串的长度。在遍历字符串s
时,如果当前字符在哈希表中存在且索引大于等于start
,则更新start
到重复字符的下一个位置。每次迭代都会更新哈希表,并可能更新maxLength
。最终返回maxLength
作为结果。
Rust 版本
#![allow(unused)] fn main() { use std::collections::HashMap; pub fn length_of_longest_substring(s: String) -> i32 { let mut char_index_map: HashMap<char, usize> = HashMap::new(); let mut max_length = 0; let mut start = 0; // 窗口开始位置 for (i, c) in s.chars().enumerate() { if let Some(&index) = char_index_map.get(&c) { if index >= start { start = index + 1; } } char_index_map.insert(c, i); max_length = max_length.max(i - start + 1); } max_length as i32 } }
这段 Rust 代码实现了滑动窗口算法。它使用一个HashMap
来存储字符和它们最新的索引。变量start
表示当前窗口的开始位置,max_length
用于记录遇到的最长子串的长度。在遍历字符串s
时,如果当前字符在哈希表中存在且索引大于等于start
,则更新start
到重复字符的下一个位置。每次迭代都会更新哈希表,并可能更新max_length
。最终返回max_length
作为结果。
总结
上述代码片段展示了如何在不同编程语言中实现寻找字符串中最长不含重复字符子串的长度的算法。核心思想是使用滑动窗口和哈希表(或字典)来跟踪字符最后出现的位置。算法的步骤如下:
- 初始化一个哈希表来存储字符和它们在字符串中的索引。
- 使用两个指针表示滑动窗口的开始和结束位置,开始时都指向字符串的起始位置。
- 遍历字符串,对于每个字符:
- 如果字符已经在哈希表中,并且其索引大于等于当前窗口的开始位置,更新窗口的开始位置到该重复字符的下一个位置。
- 将当前字符及其索引放入哈希表中。
- 更新最长子串的长度。
- 遍历完成后,返回记录的最长长度作为答案。
这种方法的时间复杂度为 O(n),其中 n 是字符串的长度,因为每个字符只被访问一次。空间复杂度为 O(min(m, n)),其中 m 是字符集的大小,这是因为哈希表最多存储 m 个键值对。对于大多数情况,m 远小于 n,因此可以认为空间复杂度接近于 O(1)。
不同编程语言的实现细节略有不同,但算法的核心逻辑是一致的。例如,Rust 使用HashMap
,C++使用std::unordered_map
,Python 使用字典,Java 使用HashMap
,而 Go 使用内置的 map 类型。尽管语法不同,但它们都提供了快速查找和更新键值对的功能,这对于算法的实现至关重要。
找到字符串中所有字母异位词
题目要求
本题目的目标是在给定的字符串 s 中寻找所有与字符串 p 为异位词的子串,并返回这些子串的起始索引。这里的异位词是指一个字符串的字符重新排列后可以得到另一个字符串,即两个字符串中的字符种类和字符数量都是相同的,但是字符的顺序可以不同。
解题思路
要解决这个问题,可以采用滑动窗口的策略,结合字符计数的方法。以下是解题的步骤:
-
字符计数:首先,对字符串 p 中的每个字符进行计数,得到一个计数器(通常是哈希表或固定大小的数组),记录每个字符出现的次数。
-
初始化滑动窗口:初始化一个与字符串 p 长度相同的滑动窗口,用于在字符串 s 中滑动。同时,创建一个计数器用于记录当前窗口内的字符出现次数。
-
滑动窗口:将滑动窗口从字符串 s 的起始位置滑动到结束位置,每次移动一位。对于每个新进入窗口的字符,增加该字符在窗口计数器中的计数;对于移出窗口的字符,减少该字符在窗口计数器中的计数。
-
匹配检查:在每次窗口滑动后,比较窗口计数器与字符串 p 的计数器是否相同。如果相同,说明当前窗口内的子串是字符串 p 的一个异位词,将当前窗口的起始索引添加到结果列表中。
-
返回结果:遍历完字符串 s 后,返回结果列表,即为所有异位词子串的起始索引。
这种方法的时间复杂度主要取决于字符串 s 的长度和字符集的大小,但由于每次窗口移动只需要常数时间的更新,因此是效率较高的解法。
Golang 版本
package main
import (
"fmt"
)
func findAnagrams(s string, p string) []int {
var res []int
pCount, sCount := [26]int{}, [26]int{}
pLen, sLen := len(p), len(s)
// 如果p的长度大于s的长度,直接返回空结果
if pLen > sLen {
return res
}
// 初始化字符计数器
for i := 0; i < pLen; i++ {
pCount[p[i]-'a']++
sCount[s[i]-'a']++
}
// 滑动窗口
for i := 0; i < sLen-pLen; i++ {
// 如果计数器相同,则添加起始索引
if pCount == sCount {
res = append(res, i)
}
// 窗口滑动,更新计数器
sCount[s[i]-'a']--
sCount[s[i+pLen]-'a']++
}
// 检查最后一个窗口
if pCount == sCount {
res = append(res, sLen-pLen)
}
return res
}
func main() {
s := "cbaebabacd"
p := "abc"
fmt.Println(findAnagrams(s, p)) // 输出异位词的起始索引
}
这段代码定义了一个findAnagrams
函数,它接受两个字符串s
和p
作为参数,并返回一个整数切片,包含了字符串s
中所有字符串p
的异位词子串的起始索引。在main
函数中,我们调用了findAnagrams
函数,并打印了返回的结果。
C++版本
#include <iostream>
#include <vector>
#include <string>
std::vector<int> findAnagrams(std::string s, std::string p) {
std::vector<int> res;
std::vector<int> pCount(26, 0), sCount(26, 0);
int pLen = p.length(), sLen = s.length();
// 如果p的长度大于s的长度,直接返回空结果
if (pLen > sLen) return res;
// 初始化字符计数器
for (int i = 0; i < pLen; ++i) {
++pCount[p[i] - 'a'];
++sCount[s[i] - 'a'];
}
// 滑动窗口
for (int i = 0; i < sLen - pLen; ++i) {
// 如果计数器相同,则添加起始索引
if (pCount == sCount) res.push_back(i);
// 窗口滑动,更新计数器
--sCount[s[i] - 'a'];
++sCount[s[i + pLen] - 'a'];
}
// 检查最后一个窗口
if (pCount == sCount) res.push_back(sLen - pLen);
return res;
}
int main() {
std::string s = "cbaebabacd";
std::string p = "abc";
std::vector<int> anagrams = findAnagrams(s, p);
for (int index : anagrams) {
std::cout << index << " ";
}
std::cout << std::endl;
return 0;
}
这段 C++代码实现了与 Golang 版本相同的功能。它定义了一个findAnagrams
函数,用于查找字符串s
中所有字符串p
的异位词子串的起始索引,并在main
函数中测试了这个功能。输出结果是所有异位词子串的起始索引列表。
Python 版本
from collections import Counter
def find_anagrams(s, p):
res = []
p_counter = Counter(p)
s_counter = Counter(s[:len(p) - 1])
for i in range(len(p) - 1, len(s)):
s_counter[s[i]] += 1 # include a new char in the window
if s_counter == p_counter: # This step is O(1) because there are at most 26 English letters
res.append(i - len(p) + 1) # append the starting index
s_counter[s[i - len(p) + 1]] -= 1 # decrease the count of the oldest char in the window
if s_counter[s[i - len(p) + 1]] == 0:
del s_counter[s[i - len(p) + 1]] # remove the count if it is 0
return res
# Example usage:
s = "cbaebabacd"
p = "abc"
print(find_anagrams(s, p)) # Output the starting indices of the anagrams of p in s
这段 Python 代码实现了查找字符串s
中所有字符串p
的异位词子串的起始索引的功能。它使用了collections.Counter
来进行字符计数,并通过滑动窗口的方式来遍历字符串s
。当窗口中的字符计数与字符串p
的字符计数相同时,将当前窗口的起始索引添加到结果列表中。
Java 版本
import java.util.ArrayList;
import java.util.List;
public class Solution {
public List<Integer> findAnagrams(String s, String p) {
List<Integer> result = new ArrayList<>();
if (s == null || p == null || s.length() < p.length()) {
return result;
}
int[] pCount = new int[26];
int[] sCount = new int[26];
for (int i = 0; i < p.length(); i++) {
pCount[p.charAt(i) - 'a']++;
}
for (int i = 0; i < s.length(); i++) {
if (i >= p.length()) {
sCount[s.charAt(i - p.length()) - 'a']--;
}
sCount[s.charAt(i) - 'a']++;
if (i >= p.length() - 1) {
if (matches(pCount, sCount)) {
result.add(i - p.length() + 1);
}
}
}
return result;
}
private boolean matches(int[] pCount, int[] sCount) {
for (int i = 0; i < 26; i++) {
if (pCount[i] != sCount[i]) {
return false;
}
}
return true;
}
public static void main(String[] args) {
Solution solution = new Solution();
String s = "cbaebabacd";
String p = "abc";
List<Integer> anagrams = solution.findAnagrams(s, p);
for (int index : anagrams) {
System.out.print(index + " ");
}
}
}
这段 Java 代码定义了一个Solution
类,其中包含了findAnagrams
方法,用于查找字符串s
中所有字符串p
的异位词子串的起始索引。它还包含了一个辅助方法matches
,用于比较两个字符计数数组是否相等。在main
方法中,我们创建了一个Solution
对象,并调用了findAnagrams
方法,最后打印出了所有异位词子串的起始索引。
Rust 版本
use std::collections::HashMap; fn find_anagrams(s: String, p: String) -> Vec<i32> { let mut res = Vec::new(); let (s_len, p_len) = (s.len(), p.len()); if s_len < p_len { return res; } let (s, p) = (s.as_bytes(), p.as_bytes()); let mut p_count = [0; 26]; let mut s_count = [0; 26]; for &c in &p[..p_len] { p_count[(c - b'a') as usize] += 1; } for i in 0..s_len { if i >= p_len { s_count[(s[i - p_len] - b'a') as usize] -= 1; } s_count[(s[i] - b'a') as usize] += 1; if p_count == s_count { res.push((i + 1 - p_len) as i32); } } res } fn main() { let s = String::from("cbaebabacd"); let p = String::from("abc"); let anagrams = find_anagrams(s, p); for start_index in anagrams { println!("{}", start_index); } }
这段 Rust 代码实现了查找字符串s
中所有字符串p
的异位词子串的起始索引的功能。它使用了固定大小的数组来存储字符计数,利用 Rust 的字节操作来更新计数器,并在滑动窗口中比较两个计数器数组是否相等。如果相等,则将当前索引添加到结果向量中。在main
函数中,我们测试了这个功能并打印了结果。
总结
上述 Rust 版本的代码实现了一个用于查找字符串中所有异位词起始索引的函数。这个解法主要包括以下几个步骤:
-
初始化变量:创建两个固定大小的数组
p_count
和s_count
来分别存储字符串p
的字符频率和字符串s
的滑动窗口中的字符频率。同时,初始化一个向量res
来存储结果。 -
计算字符频率:遍历字符串
p
,更新p_count
数组中对应字符的频率。 -
滑动窗口遍历:通过一个循环遍历字符串
s
,使用滑动窗口的方式来更新s_count
数组。窗口的大小与字符串p
的长度相同。 -
窗口内字符频率比较:在每次窗口滑动后,比较
p_count
和s_count
数组。如果两个数组相等,说明当前窗口内的子串是字符串p
的一个异位词,将当前窗口的起始索引添加到结果向量res
中。 -
返回结果:遍历完成后,返回向量
res
,它包含了所有异位词的起始索引。
这种解法的时间复杂度为 O(n),其中 n 是字符串s
的长度,因为每个字符只被访问常数次。空间复杂度为 O(1),因为字符频率数组的大小不依赖于输入字符串的大小,它只依赖于字符集的大小,对于英文字母字符集来说是常数。
和为 K 的子数组
题目要求
编写一个函数,该函数接收两个参数:一个整数数组 nums
和一个整数 k
。你需要在这个数组中找出和为 k
的子数组的数量。子数组定义为数组中的一个连续非空序列。
解题思路
解决这个问题的关键是使用累积和的概念,以及哈希表来优化查找过程。以下是解题的步骤:
-
初始化一个哈希表,用来存储各个累积和出现的次数,以及一个变量来记录累积和的总数。在哈希表中先放入一个键值对(0:1),表示累积和为 0 的情况出现了一次,这是为了处理那些从数组第一个元素开始累加就等于 k 的子数组。
-
遍历数组
nums
,计算从第一个元素到当前元素的累积和。对于每个累积和,计算当前累积和 - k
的值,查看这个值是否已经在哈希表中存在:- 如果存在,说明找到了一个子数组的累积和等于 k(因为当前累积和减去之前某个位置的累积和等于 k,那么这两个位置之间的子数组和就是 k)。
- 将这个差值对应的次数加到计数器上,因为可能存在多个不同的子数组累积和等于这个差值。
-
接下来,将当前的累积和作为键,其出现的次数作为值,存入哈希表。如果这个累积和之前已经出现过,则增加其计数。
-
完成数组的遍历后,计数器中的值就是和为 k 的子数组的总数。
这种方法的时间复杂度为 O(n),因为只需要遍历一次数组,空间复杂度为 O(n),因为最坏情况下可能需要存储所有累积和的情况。
Golang 版本
package main
import "fmt"
func subarraySum(nums []int, k int) int {
count := 0
sum := 0
sumMap := make(map[int]int)
sumMap[0] = 1 // 初始化,累积和为0的情况出现一次
for _, num := range nums {
sum += num // 累加当前元素
if prevCount, ok := sumMap[sum-k]; ok {
count += prevCount // 如果存在sum-k的累积和,累加对应的次数
}
sumMap[sum]++ // 更新累积和出现的次数
}
return count
}
func main() {
nums := []int{1, 1, 1}
k := 2
result := subarraySum(nums, k)
fmt.Println(result) // 输出应该为2
}
这段代码定义了一个subarraySum
函数,它接收一个整数数组nums
和一个整数k
作为参数,并返回和为k
的子数组的个数。在main
函数中,我们定义了一个示例数组和k
的值,调用subarraySum
函数,并打印出结果。
C++版本
#include <iostream>
#include <vector>
#include <unordered_map>
int subarraySum(std::vector<int>& nums, int k) {
int count = 0;
int sum = 0;
std::unordered_map<int, int> sumMap;
sumMap[0] = 1; // 初始化,累积和为0的情况出现一次
for (int num : nums) {
sum += num; // 累加当前元素
if (sumMap.find(sum - k) != sumMap.end()) {
count += sumMap[sum - k]; // 如果存在sum-k的累积和,累加对应的次数
}
sumMap[sum]++; // 更新累积和出现的次数
}
return count;
}
int main() {
std::vector<int> nums = {1, 1, 1};
int k = 2;
std::cout << subarraySum(nums, k) << std::endl; // 输出应该为2
return 0;
}
这段 C++代码实现了与 Golang 版本相同的功能。它定义了一个subarraySum
函数,用于计算和为k
的子数组的数量。在main
函数中,我们创建了一个示例数组和一个k
值,调用了subarraySum
函数,并打印出了结果。
Python 版本
def subarraySum(nums, k):
count = 0
current_sum = 0
sum_dict = {0: 1}
for num in nums:
current_sum += num
if current_sum - k in sum_dict:
count += sum_dict[current_sum - k]
sum_dict[current_sum] = sum_dict.get(current_sum, 0) + 1
return count
# 测试代码
nums = [1, 1, 1]
k = 2
print(subarraySum(nums, k)) # 应该输出2
Java 版本
import java.util.HashMap;
public class Solution {
public int subarraySum(int[] nums, int k) {
int count = 0;
int sum = 0;
HashMap<Integer, Integer> sumMap = new HashMap<>();
sumMap.put(0, 1); // 初始化,累积和为0的情况出现一次
for (int num : nums) {
sum += num; // 累加当前元素
if (sumMap.containsKey(sum - k)) {
count += sumMap.get(sum - k); // 如果存在sum-k的累积和,累加对应的次数
}
sumMap.put(sum, sumMap.getOrDefault(sum, 0) + 1); // 更新累积和出现的次数
}
return count;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {1, 1, 1};
int k = 2;
System.out.println(solution.subarraySum(nums, k)); // 应该输出2
}
}
这段 Java 代码定义了一个名为Solution
的类,其中包含了subarraySum
方法,用于计算和为k
的子数组的数量。在main
方法中,我们创建了一个示例数组和一个k
值,调用了subarraySum
方法,并打印出了结果。
Rust 版本
use std::collections::HashMap; fn subarray_sum(nums: Vec<i32>, k: i32) -> i32 { let mut count = 0; let mut sum = 0; let mut sum_map: HashMap<i32, i32> = HashMap::new(); sum_map.insert(0, 1); // 初始化,累积和为0的情况出现一次 for &num in &nums { sum += num; // 累加当前元素 if let Some(&value) = sum_map.get(&(sum - k)) { count += value; // 如果存在sum-k的累积和,累加对应的次数 } *sum_map.entry(sum).or_insert(0) += 1; // 更新累积和出现的次数 } count } fn main() { let nums = vec![1, 1, 1]; let k = 2; println!("{}", subarray_sum(nums, k)); // 应该输出2 }
这段 Rust 代码实现了一个subarray_sum
函数,它接收一个整数向量nums
和一个整数k
作为参数,并返回和为k
的子数组的数量。在main
函数中,我们定义了一个示例向量和k
的值,调用了subarray_sum
函数,并打印出了结果。
总结
上述解法采用了一种称为“前缀和”的技术,结合哈希表来优化查找过程。核心思想是遍历数组,计算从开始到当前元素的累积和,并在遍历的过程中使用哈希表记录下所有出现过的累积和及其出现的次数。
具体步骤如下:
- 初始化一个计数器
count
用于记录和为k
的子数组的数量,一个变量sum
用于存储从数组开始到当前元素的累积和。 - 创建一个哈希表
sumMap
,用于存储各个累积和出现的次数,初始时存入键值对(0, 1)
,表示累积和为 0 的情况出现了一次。 - 遍历数组
nums
,对于每个元素,将其加到sum
上更新当前的累积和。 - 查看
sumMap
中是否存在键为sum - k
的项,如果存在,说明从某个位置到当前位置的子数组和为k
,将该键对应的值加到count
上。 - 更新
sumMap
,将当前的累积和sum
作为键,其出现的次数作为值。如果这个累积和之前已经出现过,则增加其计数。 - 遍历完成后,
count
变量中存储的就是和为k
的子数组的数量。
这种方法的时间复杂度为 O(n),因为它只需要遍历数组一次。空间复杂度为 O(n),因为在最坏的情况下,可能需要存储数组中所有元素的累积和。这种方法比暴力解法(双重循环遍历所有子数组)要高效得多。
滑动窗口最大值
题目要求
给定一个整数数组nums
和一个正整数k
,定义一个大小为k
的滑动窗口,该窗口从数组的最左侧开始,每次向右移动一个元素,直至到达数组的最右侧。在每个窗口位置,需要找出窗口内的最大值。
要求编写一个算法,计算并返回一个数组,该数组中的每个元素代表了每次滑动窗口移动后窗口内的最大值。
解题思路
解决这个问题的关键是如何高效地从每个窗口中找到最大值。直观的方法是对每个窗口内的元素进行比较,但这样的时间复杂度较高。为了优化性能,可以采用以下策略:
-
双端队列(Deque):使用一个双端队列来存储窗口内的元素的索引,并保持队列内的元素索引对应的
nums
中的值是非递增的。这样,队列的头部始终是当前窗口内最大值的索引。- 当窗口向右移动时,首先检查队列头部的索引是否已经超出了窗口的范围(即头部索引小于当前索引减去窗口大小
k
),如果是,则将其从队列中移除。 - 然后,从队列尾部开始,移除所有小于当前元素值的索引,因为这些元素不可能是当前窗口或之后窗口的最大值。
- 将当前元素的索引添加到队列尾部。
- 队列头部的索引对应的
nums
中的值即为当前窗口的最大值。
- 当窗口向右移动时,首先检查队列头部的索引是否已经超出了窗口的范围(即头部索引小于当前索引减去窗口大小
-
窗口初始化:初始化时,先填充满第一个窗口的双端队列。
-
滑动窗口:从
k
开始遍历数组,每次移动窗口时,更新双端队列,并从队列头部获取最大值。 -
结果输出:按照窗口移动的顺序,将每次得到的最大值存入结果数组。
通过上述方法,可以将问题的时间复杂度从O(nk)
降低到O(n)
,其中n
是数组nums
的长度,因为每个元素最多被加入和移除队列一次。
Golang 版本
package main
import "fmt"
// 使用双端队列来存储索引
type Deque []int
// PushBack 在队列尾部添加元素
func (dq *Deque) PushBack(value int) {
*dq = append(*dq, value)
}
// PopFront 从队列头部移除元素
func (dq *Deque) PopFront() {
*dq = (*dq)[1:]
}
// PopBack 从队列尾部移除元素
func (dq *Deque) PopBack() {
*dq = (*dq)[:len(*dq)-1]
}
// Front 获取队列头部元素
func (dq *Deque) Front() int {
return (*dq)[0]
}
// Back 获取队列尾部元素
func (dq *Deque) Back() int {
return (*dq)[len(*dq)-1]
}
// MaxSlidingWindow 返回滑动窗口中的最大值
func MaxSlidingWindow(nums []int, k int) []int {
var result []int
var window Deque
for i, x := range nums {
// 移除所有小于当前元素值的索引
for len(window) > 0 && nums[window.Back()] <= x {
window.PopBack()
}
// 添加当前元素索引
window.PushBack(i)
// 确保队列头部的索引在窗口内
if window.Front() == i-k {
window.PopFront()
}
// 当窗口大小为k时,记录当前窗口的最大值
if i >= k-1 {
result = append(result, nums[window.Front()])
}
}
return result
}
func main() {
nums := []int{1, 3, -1, -3, 5, 3, 6, 7}
k := 3
fmt.Println(MaxSlidingWindow(nums, k))
}
这段代码实现了一个双端队列,并使用它来解决滑动窗口最大值的问题。在main
函数中,我们定义了一个示例数组nums
和窗口大小k
,然后调用MaxSlidingWindow
函数并打印结果。
C++版本
#include <iostream>
#include <vector>
#include <deque>
using namespace std;
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> window; // 双端队列,用于存储索引
vector<int> max_values; // 存储每个窗口的最大值
for (int i = 0; i < nums.size(); ++i) {
// 移除所有小于当前元素值的索引
while (!window.empty() && nums[i] >= nums[window.back()]) {
window.pop_back();
}
// 添加当前元素索引
window.push_back(i);
// 确保队列头部的索引在窗口内
if (window.front() == i - k) {
window.pop_front();
}
// 当窗口大小为k时,记录当前窗口的最大值
if (i >= k - 1) {
max_values.push_back(nums[window.front()]);
}
}
return max_values;
}
int main() {
vector<int> nums = {1, 3, -1, -3, 5, 3, 6, 7};
int k = 3;
vector<int> result = maxSlidingWindow(nums, k);
for (int num : result) {
cout << num << " ";
}
cout << endl;
return 0;
}
这段 C++代码定义了一个maxSlidingWindow
函数,它接受一个整数数组nums
和一个整数k
作为参数,并返回一个包含每个滑动窗口最大值的整数数组。在main
函数中,我们创建了一个示例数组和窗口大小,调用了maxSlidingWindow
函数,并打印了结果。
Python 版本
from collections import deque
def max_sliding_window(nums, k):
if not nums:
return []
window = deque()
max_values = []
for i, value in enumerate(nums):
# 移除所有小于当前元素值的索引
while window and nums[window[-1]] < value:
window.pop()
window.append(i)
# 移除不在滑动窗口内的元素的索引
if window[0] == i - k:
window.popleft()
# 当窗口大小为k时,记录当前窗口的最大值
if i >= k - 1:
max_values.append(nums[window[0]])
return max_values
# 示例
nums = [1, 3, -1, -3, 5, 3, 6, 7]
k = 3
print(max_sliding_window(nums, k))
这段 Python 代码实现了一个max_sliding_window
函数,它接受一个整数列表nums
和一个整数k
作为参数,并返回一个列表,其中包含每个滑动窗口的最大值。代码中使用了collections.deque
来高效地处理窗口内的索引。在示例调用中,我们定义了一个数组和窗口大小,然后打印了函数的返回值。
Java 版本
import java.util.ArrayDeque;
import java.util.Deque;
public class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || k <= 0) {
return new int[0];
}
int n = nums.length;
int[] result = new int[n - k + 1]; // 数组长度为 n - k + 1
int ri = 0; // 结果数组的索引
Deque<Integer> q = new ArrayDeque<>(); // 双端队列,用于存储索引
for (int i = 0; i < nums.length; i++) {
// 移除不在滑动窗口内的元素的索引
while (!q.isEmpty() && q.peek() < i - k + 1) {
q.poll();
}
// 移除所有小于当前元素值的索引
while (!q.isEmpty() && nums[q.peekLast()] < nums[i]) {
q.pollLast();
}
// 添加当前元素索引
q.offer(i);
// 当窗口大小为k时,记录当前窗口的最大值
if (i >= k - 1) {
result[ri++] = nums[q.peek()];
}
}
return result;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {1, 3, -1, -3, 5, 3, 6, 7};
int k = 3;
int[] result = solution.maxSlidingWindow(nums, k);
for (int num : result) {
System.out.print(num + " ");
}
}
}
这段 Java 代码定义了一个Solution
类,其中包含了maxSlidingWindow
方法,该方法接受一个整数数组nums
和一个整数k
作为参数,并返回一个数组,其中包含每个滑动窗口的最大值。在main
方法中,我们创建了一个Solution
对象,定义了一个示例数组和窗口大小,调用了maxSlidingWindow
方法,并打印了结果。
Rust 版本
use std::collections::VecDeque; pub fn max_sliding_window(nums: Vec<i32>, k: usize) -> Vec<i32> { let mut result = Vec::new(); let mut deque: VecDeque<usize> = VecDeque::new(); for (i, &num) in nums.iter().enumerate() { // 移除所有小于当前元素值的索引 while let Some(&back) = deque.back() { if nums[back] < num { deque.pop_back(); } else { break; } } deque.push_back(i); // 移除不在滑动窗口内的元素的索引 if let Some(&front) = deque.front() { if front <= i - k { deque.pop_front(); } } // 当窗口大小为k时,记录当前窗口的最大值 if i >= k - 1 { if let Some(&front) = deque.front() { result.push(nums[front]); } } } result } fn main() { let nums = vec![1, 3, -1, -3, 5, 3, 6, 7]; let k = 3; let max_values = max_sliding_window(nums, k); println!("{:?}", max_values); }
这段 Rust 代码定义了一个max_sliding_window
函数,它接受一个整数向量nums
和一个 usize 类型的k
作为参数,并返回一个向量,其中包含每个滑动窗口的最大值。在main
函数中,我们创建了一个示例向量和窗口大小,调用了max_sliding_window
函数,并打印了结果。
总结
上面的解法采用了一个有效的数据结构——双端队列(deque),来解决滑动窗口最大值的问题。这种方法的核心思想是保持双端队列的递减性质,使得队列的前端始终是当前窗口的最大值。以下是解法的步骤总结:
-
遍历输入数组
nums
,对于每个元素进行以下操作:- 如果双端队列不为空且当前元素索引与队列前端元素索引之差大于或等于窗口大小
k
,则将队列前端元素移除。这确保了队列中所有元素都在当前滑动窗口内。 - 从队列后端开始,移除所有小于当前元素值的元素索引。这是为了维护队列的递减性质,因为这些元素不可能是当前或未来窗口的最大值。
- 将当前元素的索引添加到队列的后端。
- 当遍历到第
k
个元素及以后时(即形成了第一个完整的滑动窗口),将队列前端的元素值(当前窗口的最大值)添加到结果数组中。
- 如果双端队列不为空且当前元素索引与队列前端元素索引之差大于或等于窗口大小
-
当所有元素都被处理后,返回结果数组,该数组包含了每个滑动窗口的最大值。
这种方法的时间复杂度是O(n)
,其中n
是数组nums
的长度,因为每个元素最多被加入和移除队列一次。空间复杂度是O(k)
,其中k
是滑动窗口的大小,这是因为双端队列在最坏情况下存储的元素数量不会超过窗口的大小。
最小覆盖子串
题目要求
编写一个算法,输入为两个字符串:s
和t
。目标是找到字符串s
中的最小子串,该子串必须包含字符串t
中的所有字符,并且如果t
中的字符在s
中有重复,那么在s
的子串中找到的这个字符的数量也必须不少于在t
中的数量。如果在s
中找不到这样的子串,则返回一个空字符串""
。题目还说明,如果存在满足条件的子串,那么这个子串是唯一的。
解题思路
这个问题可以通过滑动窗口的方法来解决。滑动窗口是一种常用的可以用来减少重复操作的方法,特别适合解决子字符串相关的问题。以下是解题的步骤:
- 初始化两个计数器:一个用于
t
的字符计数,另一个用于滑动窗口中的字符计数。 - 扩展窗口:从
s
的左端开始,向右移动窗口的右边界,直到窗口中包含了t
中所有的字符,并且每个字符的数量也满足要求。 - 收缩窗口:一旦我们有了一个包含
t
中所有字符的窗口,我们就可以尝试移动窗口的左边界以缩小窗口,同时保持窗口中包含t
中所有的字符。这一步是为了找到最小的满足条件的子串。 - 记录最小子串:在移动窗口的过程中,每次我们找到一个满足条件的子串,我们都检查它是否是到目前为止最小的一个,并记录其位置和长度。
- 重复步骤 2 和 3:继续移动窗口的右边界,重复步骤 2 和 3 的过程,直到右边界到达字符串
s
的末尾。 - 返回结果:根据记录的最小子串的位置和长度,返回最终的子串。如果没有找到满足条件的子串,则返回空字符串
""
。
在实现上述步骤时,需要注意的是如何高效地计数和比较字符。可以使用哈希表来存储字符的计数,这样可以在常数时间内完成字符的添加、删除和查找操作。此外,为了避免重复计算t
中每个字符的数量,我们可以在算法开始之前预先计算好,并存储在哈希表中。
这个问题的关键在于如何高效地移动滑动窗口的两个边界,并在整个过程中维护好字符的计数。通过不断地调整窗口的大小,直到找到最小的包含t
中所有字符的子串,我们就可以得到问题的答案。
Golang 版本
package main
import (
"fmt"
"math"
)
func minWindow(s string, t string) string {
if len(s) == 0 || len(t) == 0 {
return ""
}
// 记录t中每个字符出现的次数
tFreq := make(map[byte]int)
for i := 0; i < len(t); i++ {
tFreq[t[i]]++
}
// 记录当前窗口中每个字符出现的次数
windowFreq := make(map[byte]int)
// 滑动窗口的左右边界
left, right := 0, 0
// 记录已经找到的字符个数
found := 0
// 记录最小覆盖子串的起始索引及长度
minLeft, minSize := 0, math.MaxInt32
for right < len(s) {
// 如果右边的字符是t中的字符,则加入窗口
if _, ok := tFreq[s[right]]; ok {
windowFreq[s[right]]++
if windowFreq[s[right]] <= tFreq[s[right]] {
found++
}
}
// 当窗口已经包含了t中所有字符
for found == len(t) {
// 更新最小覆盖子串
if right-left+1 < minSize {
minLeft = left
minSize = right - left + 1
}
// 尝试缩小窗口
if _, ok := tFreq[s[left]]; ok {
windowFreq[s[left]]--
if windowFreq[s[left]] < tFreq[s[left]] {
found--
}
}
left++
}
right++
}
if minSize == math.MaxInt32 {
return ""
}
return s[minLeft : minLeft+minSize]
}
func main() {
s := "ADOBECODEBANC"
t := "ABC"
fmt.Println("Minimum window substring is:", minWindow(s, t))
}
这段代码实现了上述的滑动窗口算法,用于寻找字符串s
中包含字符串t
所有字符的最小子串。如果存在这样的子串,它将被打印出来;如果不存在,将打印出一个空字符串。
C++版本
#include <iostream>
#include <string>
#include <unordered_map>
#include <climits>
std::string minWindow(std::string s, std::string t) {
if (s.empty() || t.empty()) {
return "";
}
std::unordered_map<char, int> tFreq;
for (char c : t) {
++tFreq[c];
}
std::unordered_map<char, int> windowFreq;
int left = 0, right = 0;
int required = tFreq.size();
int formed = 0;
int minLen = INT_MAX, minStart = 0;
while (right < s.length()) {
char c = s[right];
if (tFreq.find(c) != tFreq.end()) {
windowFreq[c]++;
if (windowFreq[c] == tFreq[c]) {
formed++;
}
}
while (left <= right && formed == required) {
c = s[left];
if (right - left + 1 < minLen) {
minLen = right - left + 1;
minStart = left;
}
if (tFreq.find(c) != tFreq.end()) {
windowFreq[c]--;
if (windowFreq[c] < tFreq[c]) {
formed--;
}
}
left++;
}
right++;
}
return minLen == INT_MAX ? "" : s.substr(minStart, minLen);
}
int main() {
std::string s = "ADOBECODEBANC";
std::string t = "ABC";
std::string minSubstr = minWindow(s, t);
std::cout << "Minimum window substring is: " << minSubstr << std::endl;
return 0;
}
这段 C++代码实现了寻找字符串s
中包含字符串t
所有字符的最小子串的功能。如果找到了这样的子串,它会被输出;如果没有找到,输出将是一个空字符串。
Python 版本
from collections import Counter
def min_window(s, t):
if not t or not s:
return ""
dict_t = Counter(t)
required = len(dict_t)
l, r = 0, 0
formed = 0
window_counts = {}
ans = float("inf"), None, None
while r < len(s):
character = s[r]
window_counts[character] = window_counts.get(character, 0) + 1
if character in dict_t and window_counts[character] == dict_t[character]:
formed += 1
while l <= r and formed == required:
character = s[l]
if r - l + 1 < ans[0]:
ans = (r - l + 1, l, r)
window_counts[character] -= 1
if character in dict_t and window_counts[character] < dict_t[character]:
formed -= 1
l += 1
r += 1
return "" if ans[0] == float("inf") else s[ans[1]:ans[2] + 1]
# Example usage:
s = "ADOBECODEBANC"
t = "ABC"
print(min_window(s, t))
这段 Python 代码实现了寻找字符串s
中包含字符串t
所有字符的最小子串的功能。如果找到了这样的子串,它会被打印出来;如果没有找到,打印将是一个空字符串。
Java 版本
import java.util.HashMap;
import java.util.Map;
public class Solution {
public static String minWindow(String s, String t) {
if (s == null || t == null || s.length() < t.length() || s.length() == 0) {
return "";
}
Map<Character, Integer> dictT = new HashMap<Character, Integer>();
for (int i = 0; i < t.length(); i++) {
dictT.put(t.charAt(i), dictT.getOrDefault(t.charAt(i), 0) + 1);
}
int required = dictT.size();
int l = 0, r = 0;
int formed = 0;
Map<Character, Integer> windowCounts = new HashMap<Character, Integer>();
int[] ans = {-1, 0, 0};
while (r < s.length()) {
char c = s.charAt(r);
windowCounts.put(c, windowCounts.getOrDefault(c, 0) + 1);
if (dictT.containsKey(c) && windowCounts.get(c).intValue() == dictT.get(c).intValue()) {
formed++;
}
while (l <= r && formed == required) {
c = s.charAt(l);
if (ans[0] == -1 || r - l + 1 < ans[0]) {
ans[0] = r - l + 1;
ans[1] = l;
ans[2] = r;
}
windowCounts.put(c, windowCounts.get(c) - 1);
if (dictT.containsKey(c) && windowCounts.get(c).intValue() < dictT.get(c).intValue()) {
formed--;
}
l++;
}
r++;
}
return ans[0] == -1 ? "" : s.substring(ans[1], ans[2] + 1);
}
public static void main(String[] args) {
String s = "ADOBECODEBANC";
String t = "ABC";
System.out.println("Minimum window substring is: " + minWindow(s, t));
}
}
这段 Java 代码实现了寻找字符串s
中包含字符串t
所有字符的最小子串的功能。如果找到了这样的子串,它会被输出;如果没有找到,输出将是一个空字符串。
Rust 版本
use std::collections::HashMap; fn min_window(s: String, t: String) -> String { let mut t_freq = HashMap::new(); for c in t.chars() { *t_freq.entry(c).or_insert(0) += 1; } let (mut start, mut min_len, mut left, mut right, mut formed) = (0, usize::MAX, 0, 0, 0); let mut window_freq = HashMap::new(); let required = t_freq.len(); while right < s.len() { let c = s.chars().nth(right).unwrap(); *window_freq.entry(c).or_insert(0) += 1; if t_freq.contains_key(&c) && window_freq[&c] == t_freq[&c] { formed += 1; } while left <= right && formed == required { let c = s.chars().nth(left).unwrap(); if right - left + 1 < min_len { min_len = right - left + 1; start = left; } window_freq.entry(c).and_modify(|e| *e -= 1); if t_freq.contains_key(&c) && window_freq[&c] < t_freq[&c] { formed -= 1; } left += 1; } right += 1; } if min_len == usize::MAX { "".to_string() } else { s[start..start + min_len].to_string() } } fn main() { let s = String::from("ADOBECODEBANC"); let t = String::from("ABC"); println!("Minimum window substring is: {}", min_window(s, t)); }
这段 Rust 代码实现了寻找字符串s
中包含字符串t
所有字符的最小子串的功能。如果找到了这样的子串,它会被输出;如果没有找到,输出将是一个空字符串。
总结
上述问题的解法遵循了一种称为“滑动窗口”的算法策略。无论是在 Python、C++、Java 还是 Rust 版本的代码中,核心思想都是相同的,即通过维护一个窗口来寻找包含目标字符串t
所有字符的最小子串。以下是解法的关键步骤:
-
初始化两个指针:一个左指针(
left
或l
)和一个右指针(right
或r
),分别代表窗口的开始和结束位置。 -
字符频率映射:使用哈希表(或字典)来记录字符串
t
中每个字符的频率。 -
扩展右指针:移动右指针以扩大窗口,直到窗口包含了
t
中所有的字符。 -
收缩左指针:一旦找到了一个有效的窗口,即包含了
t
中所有字符的子串,就开始移动左指针以缩小窗口,同时更新最小子串的长度和起始位置。这一步是为了找到不包含额外字符的最小长度子串。 -
更新结果:在整个过程中,记录并更新满足条件的最小子串的长度和起始位置。
-
返回结果:根据记录的最小长度和起始位置,返回最终的最小子串。如果没有找到满足条件的子串,则返回空字符串。
这个算法的时间复杂度通常是 O(n),其中 n 是字符串s
的长度,因为每个字符最多被访问两次(一次是右指针向右移动,一次是左指针向右移动)。空间复杂度取决于字符串t
的长度,因为需要存储t
中每个字符的频率。
普通数组
解决关于普通数组的算法题通常可以遵循以下几个步骤:
-
理解问题:仔细阅读题目,理解题目的要求,包括输入输出格式、数组的特性(如是否有序)、限制条件等。
-
确定解题策略:根据问题的类型选择合适的策略。常见的策略有:
- 暴力法:遍历数组的所有可能性,适用于小规模数据。
- 双指针法:使用两个指针在数组中移动来解决问题,适用于有序数组或需要找到两个相关元素的问题。
- 分治法:将问题分解成多个小问题递归求解,适用于复杂问题,如快速排序。
- 动态规划:通过构建一个解的数组来逐步求解问题,适用于求最优解问题,如最大子数组和。
- 哈希表:使用哈希表来快速查找、统计元素,适用于需要快速访问数据的问题。
-
编写伪代码:在开始编写具体代码之前,可以先用伪代码梳理算法流程。
-
编写代码:根据伪代码将算法转换成具体的编程语言代码。
-
测试:使用不同的测试用例来测试代码的正确性。
下面是一些使用 Go 语言的代码实例:
例 1:找到数组中的最大元素
func findMax(nums []int) int {
maxNum := nums[0]
for _, num := range nums {
if num > maxNum {
maxNum = num
}
}
return maxNum
}
例 2:双指针法解决有序数组的两数之和问题
func twoSum(numbers []int, target int) (int, int) {
left, right := 0, len(numbers)-1
for left < right {
sum := numbers[left] + numbers[right]
if sum == target {
return left + 1, right + 1 // 题目可能要求返回的索引从1开始
} else if sum < target {
left++
} else {
right--
}
}
return -1, -1 // 如果没有找到解
}
例 3:动态规划解决最大子数组和问题
func maxSubArray(nums []int) int {
maxSum, currentSum := nums[0], nums[0]
for i := 1; i < len(nums); i++ {
if currentSum < 0 {
currentSum = nums[i]
} else {
currentSum += nums[i]
}
if currentSum > maxSum {
maxSum = currentSum
}
}
return maxSum
}
在解决实际问题时,还需要考虑代码的可读性和效率,以及边界条件的处理。记得在写完代码后,通过多个测试用例来验证代码的正确性和健壮性。
最大子数组和
题目要求
给定一个整数数组 nums
,任务是找到一个具有最大和的连续子数组(至少包含一个元素),并返回这个最大和。
解题思路
解决这个问题的一个经典方法是使用动态规划(DP)。以下是解题的步骤:
-
初始化状态:
- 创建一个名为
dp
的数组,用于存储到当前位置为止的最大子数组和。dp[i]
表示以nums[i]
结尾的最大子数组和。 - 将
dp[0]
初始化为nums[0]
,因为第一个元素本身就是一个子数组。 - 创建一个变量
maxSum
来跟踪遍历过程中遇到的最大子数组和,并将其初始化为dp[0]
。
- 创建一个名为
-
状态转移方程:
- 遍历数组
nums
,从索引1
到n-1
(n
是数组nums
的长度)。 - 更新
dp[i]
为max(nums[i], dp[i-1] + nums[i])
。这表示对于每个元素nums[i]
,你可以选择只包含nums[i]
自身,或者将nums[i]
添加到以nums[i-1]
结尾的最大子数组和中。 - 每次计算出新的
dp[i]
后,比较并更新maxSum
为max(maxSum, dp[i])
。
- 遍历数组
-
优化存储空间:
- 注意到在状态转移方程中,
dp[i]
只依赖于dp[i-1]
,因此不需要维护整个dp
数组,只需使用一个变量currentMax
来存储dp[i-1]
的值即可。 - 更新
currentMax
为max(nums[i], currentMax + nums[i])
,然后更新maxSum
。
- 注意到在状态转移方程中,
-
返回结果:
- 遍历完成后,
maxSum
将包含最大子数组和,返回maxSum
即可。
- 遍历完成后,
这个问题也被称为最大子序和问题,是一个经典的动态规划问题。Kadane 算法就是上述思路的实现,它的时间复杂度为 O(n),空间复杂度为 O(1),是解决这个问题的最有效方法。
Golang 版本
package main
import (
"fmt"
)
func maxSubArray(nums []int) int {
maxSum := nums[0]
currentMax := nums[0]
for i := 1; i < len(nums); i++ {
currentMax = max(nums[i], currentMax+nums[i])
maxSum = max(maxSum, currentMax)
}
return maxSum
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func main() {
nums := []int{-2, 1, -3, 4, -1, 2, 1, -5, 4}
fmt.Println("Maximum Subarray Sum:", maxSubArray(nums))
}
这段代码定义了一个 maxSubArray
函数,它接受一个整数数组 nums
并返回最大子数组的和。max
函数用于比较两个整数并返回较大的一个。在 main
函数中,我们定义了一个示例数组 nums
并调用 maxSubArray
函数,然后打印出最大子数组的和。
C++版本
#include <iostream>
#include <vector>
#include <algorithm>
int maxSubArray(std::vector<int>& nums) {
int maxSum = nums[0];
int currentMax = nums[0];
for (int i = 1; i < nums.size(); i++) {
currentMax = std::max(nums[i], currentMax + nums[i]);
maxSum = std::max(maxSum, currentMax);
}
return maxSum;
}
int main() {
std::vector<int> nums = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
std::cout << "Maximum Subarray Sum: " << maxSubArray(nums) << std::endl;
return 0;
}
这段 C++代码实现了同样的功能。它定义了一个 maxSubArray
函数,该函数接受一个整数类型的 vector
引用 nums
并返回最大子数组的和。在 main
函数中,我们创建了一个示例 vector
nums
并调用 maxSubArray
函数,然后输出最大子数组的和。这里使用了 <algorithm>
头文件中的 std::max
函数来简化代码。
Python 版本
def max_sub_array(nums):
max_sum = current_max = nums[0]
for num in nums[1:]:
current_max = max(num, current_max + num)
max_sum = max(max_sum, current_max)
return max_sum
if __name__ == "__main__":
nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
print("Maximum Subarray Sum:", max_sub_array(nums))
这段 Python 代码定义了一个 max_sub_array
函数,它接受一个整数列表 nums
并返回最大子数组的和。在 if __name__ == "__main__":
块中,我们定义了一个示例列表 nums
并调用 max_sub_array
函数,然后打印出最大子数组的和。
Java 版本
public class Main {
public static int maxSubArray(int[] nums) {
int maxSum = nums[0];
int currentMax = nums[0];
for (int i = 1; i < nums.length; i++) {
currentMax = Math.max(nums[i], currentMax + nums[i]);
maxSum = Math.max(maxSum, currentMax);
}
return maxSum;
}
public static void main(String[] args) {
int[] nums = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
System.out.println("Maximum Subarray Sum: " + maxSubArray(nums));
}
}
这段 Java 代码实现了最大子数组和的功能。maxSubArray
方法接受一个整数数组 nums
并返回最大子数组的和。在 main
方法中,我们创建了一个示例数组 nums
并调用 maxSubArray
方法,然后输出最大子数组的和。Java 中的 Math.max
方法用于比较两个值并返回较大的一个。
Rust 版本
fn max_sub_array(nums: Vec<i32>) -> i32 { let mut max_sum = nums[0]; let mut current_max = nums[0]; for &num in nums.iter().skip(1) { current_max = std::cmp::max(num, current_max + num); max_sum = std::cmp::max(max_sum, current_max); } max_sum } fn main() { let nums = vec![-2, 1, -3, 4, -1, 2, 1, -5, 4]; println!("Maximum Subarray Sum: {}", max_sub_array(nums)); }
这段 Rust 代码定义了一个 max_sub_array
函数,它接受一个整数向量 nums
并返回最大子数组的和。在 main
函数中,我们创建了一个示例向量 nums
并调用 max_sub_array
函数,然后打印出最大子数组的和。Rust 的 std::cmp::max
函数用于比较两个值并返回较大的一个。
总结
上述解法采用了著名的动态规划方法来解决最大子数组和的问题,这种方法也被称为卡迪兰(Kadane's)算法。核心思想是遍历数组,同时计算到当前位置为止的最大子数组和。算法可以分为以下几个步骤:
-
初始化两个变量,
maxSum
和currentMax
,分别用来存储迄今为止遇到的最大子数组和以及到当前位置为止的最大子数组和。这两个变量最初都被设置为数组的第一个元素。 -
从数组的第二个元素开始遍历,对于每个元素,更新
currentMax
的值。这个更新是基于一个判断:是选择当前元素作为新子数组的起点,还是将当前元素加到现有子数组和中。这可以通过比较currentMax + nums[i]
和nums[i]
的值来决定。 -
更新
maxSum
的值,如果currentMax
的新值大于maxSum
,则maxSum
应更新为currentMax
的值。 -
遍历完成后,
maxSum
将包含整个数组的最大子数组和。
这种方法的时间复杂度为 O(n),因为它只需要遍历数组一次。空间复杂度为 O(1),因为它只需要常数级别的额外空间。这种算法在各种编程语言中都很容易实现,如上所示的 Golang、C++、Python、Java 和 Rust 版本。
合并区间
题目要求
给定一个区间数组 intervals
,其中每个区间表示为形如 [starti, endi]
的子数组。要求编写一个算法,合并所有重叠的区间,并返回一个新的区间数组。新的区间数组中的每个区间应该是不重叠的,并且必须包含输入数组中的所有区间。
解题思路
解决这个问题的关键是要理解何时两个区间可以合并,以及如何合并这些区间。以下是解题的步骤:
-
排序:首先,按照区间的起始位置
starti
对所有区间进行排序。这样做的目的是为了让所有可能重叠的区间彼此靠近,便于合并。 -
初始化:创建一个空数组
merged
,用于存放合并后的区间。 -
遍历合并:遍历排序后的区间数组
intervals
,对于每个区间interval
:- 如果
merged
为空,或者当前区间的起始位置大于merged
中最后一个区间的结束位置,这意味着当前区间与merged
中的任何区间都不重叠,可以直接将当前区间添加到merged
中。 - 如果当前区间与
merged
中最后一个区间重叠(即当前区间的起始位置小于或等于merged
中最后一个区间的结束位置),则需要合并这两个区间。合并的方式是更新merged
中最后一个区间的结束位置为两个区间结束位置的较大值。
- 如果
-
返回结果:遍历完成后,
merged
数组中存放的就是合并后的不重叠区间。返回merged
数组作为结果。
通过以上步骤,可以有效地合并所有重叠的区间,并得到一个简洁的不重叠区间数组。
Golang 版本
package main
import (
"fmt"
"sort"
)
func merge(intervals [][]int) [][]int {
if len(intervals) == 0 {
return [][]int{}
}
// 按照区间的起始位置进行排序
sort.Slice(intervals, func(i, j int) bool {
return intervals[i][0] < intervals[j][0]
})
// 初始化merged数组,添加第一个区间
merged := [][]int{intervals[0]}
// 遍历并合并区间
for i := 1; i < len(intervals); i++ {
last := merged[len(merged)-1] // 获取merged中的最后一个区间
if intervals[i][0] > last[1] {
// 如果当前区间的起始位置大于merged中最后一个区间的结束位置
// 则不重叠,可以直接添加
merged = append(merged, intervals[i])
} else {
// 否则,有重叠,合并区间
last[1] = max(last[1], intervals[i][1])
}
}
return merged
}
// 辅助函数,返回两个整数中较大的一个
func max(a, b int) int {
if a > b {
return a
}
return b
}
func main() {
// 示例
intervals := [][]int{{1, 3}, {2, 6}, {8, 10}, {15, 18}}
mergedIntervals := merge(intervals)
fmt.Println(mergedIntervals) // 输出合并后的区间
}
这段代码定义了一个 merge
函数,它接受一个二维整数数组 intervals
作为参数,返回一个合并后的区间数组。在 main
函数中,我们定义了一个示例区间数组,并调用 merge
函数,然后打印出合并后的结果。
C++版本
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
if (intervals.empty()) {
return {};
}
// 按照区间的起始位置排序
sort(intervals.begin(), intervals.end(), [](const vector<int>& a, const vector<int>& b) {
return a[0] < b[0];
});
vector<vector<int>> merged;
merged.push_back(intervals[0]);
// 遍历区间并合并
for (int i = 1; i < intervals.size(); ++i) {
// 如果当前区间的起始位置小于等于merged中最后一个区间的结束位置,则合并
if (intervals[i][0] <= merged.back()[1]) {
merged.back()[1] = max(merged.back()[1], intervals[i][1]);
} else {
// 否则,将当前区间添加到merged中
merged.push_back(intervals[i]);
}
}
return merged;
}
};
int main() {
Solution solution;
vector<vector<int>> intervals = {{1, 3}, {2, 6}, {8, 10}, {15, 18}};
vector<vector<int>> mergedIntervals = solution.merge(intervals);
for (const auto& interval : mergedIntervals) {
cout << "[" << interval[0] << "," << interval[1] << "] ";
}
cout << endl;
return 0;
}
这段 C++代码定义了一个 Solution
类,其中包含了一个 merge
方法,用于合并区间。在 main
函数中,我们创建了一个 Solution
对象,并用一个示例区间数组调用了 merge
方法,最后打印出合并后的区间数组。
Python 版本
def merge(intervals):
if not intervals:
return []
# 按照区间的起始位置排序
intervals.sort(key=lambda x: x[0])
merged = [intervals[0]]
for current in intervals[1:]:
# 如果当前区间的起始位置小于等于merged中最后一个区间的结束位置,则合并
if current[0] <= merged[-1][1]:
merged[-1][1] = max(merged[-1][1], current[1])
else:
# 否则,将当前区间添加到merged中
merged.append(current)
return merged
# 示例
intervals = [[1, 3], [2, 6], [8, 10], [15, 18]]
print(merge(intervals))
这段 Python 代码定义了一个 merge
函数,它接受一个区间列表 intervals
作为参数,并返回合并后的区间列表。在示例中,我们定义了一个区间列表,并调用 merge
函数,然后打印出合并后的结果。
Java 版本
import java.util.Arrays;
import java.util.LinkedList;
public class Solution {
public int[][] merge(int[][] intervals) {
if (intervals.length <= 1) {
return intervals;
}
// 根据区间的起始位置排序
Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
LinkedList<int[]> merged = new LinkedList<>();
for (int[] interval : intervals) {
// 如果列表为空,或者当前区间与列表中最后一个区间不重叠,直接添加
if (merged.isEmpty() || merged.getLast()[1] < interval[0]) {
merged.add(interval);
} else {
// 否则,我们合并当前区间与列表中最后一个区间
merged.getLast()[1] = Math.max(merged.getLast()[1], interval[1]);
}
}
return merged.toArray(new int[merged.size()][]);
}
public static void main(String[] args) {
Solution solution = new Solution();
int[][] intervals = {{1, 3}, {2, 6}, {8, 10}, {15, 18}};
int[][] mergedIntervals = solution.merge(intervals);
for (int[] interval : mergedIntervals) {
System.out.println("[" + interval[0] + "," + interval[1] + "]");
}
}
}
这段 Java 代码定义了一个 Solution
类,其中包含了一个 merge
方法,用于合并区间。在 main
方法中,我们创建了一个 Solution
对象,并用一个示例区间数组调用了 merge
方法,最后打印出合并后的区间数组。
Rust 版本
fn merge(mut intervals: Vec<Vec<i32>>) -> Vec<Vec<i32>> { if intervals.is_empty() { return vec![]; } // 根据区间的起始位置排序 intervals.sort_by(|a, b| a[0].cmp(&b[0])); let mut merged: Vec<Vec<i32>> = Vec::new(); merged.push(intervals[0].clone()); for i in 1..intervals.len() { let last = merged.last_mut().unwrap(); if intervals[i][0] <= last[1] { last[1] = std::cmp::max(last[1], intervals[i][1]); } else { merged.push(intervals[i].clone()); } } merged } fn main() { let intervals = vec![vec![1, 3], vec![2, 6], vec![8, 10], vec![15, 18]]; let merged_intervals = merge(intervals); for interval in merged_intervals { println!("[{}, {}]", interval[0], interval[1]); } }
这段 Rust 代码定义了一个 merge
函数,它接受一个 Vec<Vec<i32>>
类型的区间列表 intervals
作为参数,并返回合并后的区间列表。在 main
函数中,我们定义了一个区间列表,并调用 merge
函数,然后打印出合并后的结果。
总结
合并区间的问题可以通过以下步骤解决:
-
排序:首先,根据每个区间的起始位置对区间列表进行排序。这是为了让所有可能需要合并的区间排列在一起,便于后续处理。
-
初始化:创建一个新的区间列表(在 Rust 中是
Vec<Vec<i32>>
,在 Java 中是LinkedList<int[]>
,在 C++中是vector<vector<int>>
,在 Python 中是列表),并将排序后的第一个区间添加到这个列表中。这个列表将用于存储最终合并后的区间。 -
遍历与合并:遍历排序后的区间列表,对于每个区间:
- 如果当前区间的起始位置小于或等于新区间列表中最后一个区间的结束位置,则说明这两个区间有重叠,需要合并。合并操作是将新区间列表中最后一个区间的结束位置更新为当前区间和最后一个区间结束位置的较大值。
- 如果当前区间的起始位置大于新区间列表中最后一个区间的结束位置,则说明这两个区间没有重叠,可以直接将当前区间添加到新区间列表中。
-
返回结果:遍历完成后,新区间列表中存储的就是合并后的区间。返回这个列表作为结果。
在不同的编程语言中,具体的语法和数据结构有所不同,但算法的核心思想是一致的。例如,在 Rust 中使用Vec
和sort_by
方法进行排序,在 Java 中使用LinkedList
和Arrays.sort
方法,在 C++中使用vector
和sort
函数,在 Python 中使用列表和sort
方法。
轮转数组
题目要求
给定一个整数数组 nums
,需要将数组中的所有元素向右移动 k
个位置,这里的 k
是一个非负整数。轮转操作是指将数组末尾的元素移动到数组的开头。
解题思路
解决这个问题的关键是找到一个高效的方法来进行数组元素的移动,因为直接按照定义进行移动会涉及到多次的数组遍历,这在 k
很大或数组很长时会非常低效。以下是解决这个问题的几个步骤:
-
处理
k
的值:由于数组的长度是固定的,所以当k
大于数组长度n
时,移动k
次和移动k % n
次的效果是相同的。因此,首先计算k = k % n
来简化问题。 -
数组翻转:可以通过三次翻转数组的方式来达到向右轮转的效果。
- 首先,翻转整个数组,这会将原本在数组末尾的元素移动到数组的开头。
- 然后,翻转数组的前
k
个元素。 - 最后,翻转数组剩余的
n - k
个元素。
这个方法的优点是只需要线性时间复杂度 O(n)
,并且可以在不需要额外空间的情况下(除了少量的临时变量)完成数组的轮转操作。
Golang 版本
package main
import (
"fmt"
)
func rotate(nums []int, k int) {
n := len(nums)
k = k % n
reverse(nums, 0, n-1)
reverse(nums, 0, k-1)
reverse(nums, k, n-1)
}
func reverse(nums []int, start int, end int) {
for start < end {
nums[start], nums[end] = nums[end], nums[start]
start++
end--
}
}
func main() {
nums := []int{1, 2, 3, 4, 5, 6, 7}
k := 3
rotate(nums, k)
fmt.Println(nums) // Output should be [5,6,7,1,2,3,4]
}
这段代码定义了一个 rotate
函数,它接受一个整数数组 nums
和一个整数 k
,并将 nums
中的元素向右轮转 k
个位置。reverse
函数用于翻转数组中从 start
到 end
位置的元素。main
函数提供了一个示例用法。
C++版本
#include <iostream>
#include <vector>
#include <algorithm>
void rotate(std::vector<int>& nums, int k) {
int n = nums.size();
k = k % n;
std::reverse(nums.begin(), nums.end());
std::reverse(nums.begin(), nums.begin() + k);
std::reverse(nums.begin() + k, nums.end());
}
int main() {
std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7};
int k = 3;
rotate(nums, k);
for (int num : nums) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
这段代码定义了一个 rotate
函数,它接受一个整数向量 nums
和一个整数 k
,并将 nums
中的元素向右轮转 k
个位置。这里使用了 C++ STL 中的 std::reverse
函数来翻转向量的部分区间。main
函数提供了一个示例用法,并打印出轮转后的数组。
Python 版本
def rotate(nums, k):
n = len(nums)
k = k % n
nums[:] = nums[-k:] + nums[:-k]
if __name__ == "__main__":
nums = [1, 2, 3, 4, 5, 6, 7]
k = 3
rotate(nums, k)
print(nums) # Output should be [5, 6, 7, 1, 2, 3, 4]
这段代码定义了一个 rotate
函数,它接受一个整数列表 nums
和一个整数 k
,并将 nums
中的元素向右轮转 k
个位置。这里使用了 Python 列表切片的特性来简化数组的旋转操作。if __name__ == "__main__":
部分提供了一个示例用法,并打印出轮转后的列表。
Java 版本
public class Solution {
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--;
}
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {1, 2, 3, 4, 5, 6, 7};
int k = 3;
solution.rotate(nums, k);
for (int num : nums) {
System.out.print(num + " ");
}
}
}
这段代码定义了一个 Solution
类,其中包含了一个 rotate
方法用于执行数组的旋转操作,以及一个辅助的 reverse
方法用于翻转数组中指定区间的元素。main
方法提供了一个示例用法,并打印出旋转后的数组。
Rust 版本
fn rotate(nums: &mut Vec<i32>, k: i32) { let n = nums.len() as i32; let k = k % n; nums.reverse(); nums[..k as usize].reverse(); nums[k as usize..].reverse(); } fn main() { let mut nums = vec![1, 2, 3, 4, 5, 6, 7]; let k = 3; rotate(&mut nums, k); println!("{:?}", nums); // Output should be [5, 6, 7, 1, 2, 3, 4] }
这段代码定义了一个 rotate
函数,它接受一个可变引用的整数向量 nums
和一个整数 k
,并将 nums
中的元素向右轮转 k
个位置。Rust 的切片和方法调用用于翻转向量的部分区间。main
函数提供了一个示例用法,并打印出轮转后的向量。
总结
上述解法中,无论是在 Golang、C++、Python 还是 Java 和 Rust 语言中,核心算法思想都是相同的,即通过三次数组翻转来实现数组的轮转操作。具体步骤如下:
- 首先,将整个数组翻转,这样数组末尾的元素就被移动到了数组的开头。
- 其次,翻转数组中前
k
个元素,其中k
是经过模数组长度后的轮转次数。 - 最后,翻转剩余的元素,即从第
k+1
个元素到数组的末尾。
这种方法的时间复杂度是 O(n),空间复杂度是 O(1),因为它只需要对数组进行几次遍历,并且不需要额外的存储空间(除了几个用于交换的临时变量)。
不同编程语言之间的主要区别在于语法和数组操作的具体实现。例如:
- 在 C++ 中,使用了 STL 中的
std::reverse
函数来翻转数组的一部分。 - 在 Python 中,利用了列表切片的特性来一步完成轮转。
- 在 Java 中,通过手动交换数组元素来实现翻转。
- 在 Rust 中,使用了切片和
reverse
方法来翻转向量的部分区间。 - 在 Golang 中,通过索引和循环来手动翻转切片的元素。
尽管实现的细节不同,但所有这些解法都遵循了相同的算法逻辑。
除自身以外数组的乘积
题目要求
给定一个整数数组nums
,要求构造一个新的数组answer
。对于answer
中的每个元素answer[i]
,其值应该是数组nums
中除了nums[i]
之外所有元素的乘积。题目的限制条件包括:
- 不可以使用除法操作。
- 必须在 O(n)的时间复杂度内解决问题。
- 题目保证所有元素的前缀乘积和后缀乘积都不会超过 32 位整数的范围。
解题思路
要在不使用除法的前提下求解这个问题,可以通过以下步骤来构造answer
数组:
-
初始化两个数组:创建两个数组
left
和right
,其中left[i]
用于存储nums[i]
左侧所有元素的乘积,right[i]
用于存储nums[i]
右侧所有元素的乘积。 -
计算前缀乘积:从左到右遍历
nums
数组,计算每个元素的前缀乘积并存储在left
数组中。left[0]
应该初始化为 1,因为第一个元素的左侧没有其他元素。对于left[i]
(i > 0),它应该等于left[i - 1] * nums[i - 1]
。 -
计算后缀乘积:从右到左遍历
nums
数组,计算每个元素的后缀乘积并存储在right
数组中。right[n - 1]
(其中 n 是nums
数组的长度)应该初始化为 1,因为最后一个元素的右侧没有其他元素。对于right[i]
(i < n - 1),它应该等于right[i + 1] * nums[i + 1]
。 -
构造答案数组:遍历
nums
数组,对于每个i
,answer[i]
应该是left[i]
和right[i]
的乘积,因为这两个值分别代表了nums[i]
左侧和右侧所有元素的乘积。 -
优化空间复杂度:注意到在构造答案时,实际上不需要同时保留
left
和right
两个数组。可以只使用一个answer
数组,并用一个变量来动态地计算后缀乘积。首先利用answer
数组按照left
数组的构造方法计算前缀乘积,然后再从右向左遍历一次,用一个变量R
来代表从右侧开始的累积乘积,并更新answer[i] = answer[i] * R
,同时更新R = R * nums[i]
。
通过上述步骤,可以在不使用除法且时间复杂度为 O(n)的情况下解决这个问题。
Golang 版本
func productExceptSelf(nums []int) []int {
length := len(nums)
// 初始化answer数组,用于存储结果
answer := make([]int, length)
// answer[i]先存储索引i左侧所有元素的乘积
answer[0] = 1
for i := 1; i < length; i++ {
answer[i] = nums[i-1] * answer[i-1]
}
// R用于存储从右侧开始的累积乘积
R := 1
for i := length - 1; i >= 0; i-- {
// 对于索引i,左侧的乘积已经在answer[i]中,乘以右侧的乘积R
answer[i] *= R
// 更新R,包括当前元素nums[i]
R *= nums[i]
}
return answer
}
这段代码实现了上述解题思路,首先计算了每个索引左侧所有元素的乘积,然后从数组的右侧开始遍历,计算右侧所有元素的乘积,并更新结果数组。这样就在 O(n)时间复杂度内,不使用除法,得到了最终的答案数组。
C++版本
#include <vector>
std::vector<int> productExceptSelf(std::vector<int>& nums) {
int length = nums.size();
std::vector<int> answer(length, 1);
// 计算左侧所有元素的乘积
for (int i = 1; i < length; i++) {
answer[i] = answer[i - 1] * nums[i - 1];
}
// R用于存储从右侧开始的累积乘积
int R = 1;
for (int i = length - 1; i >= 0; i--) {
// 对于索引i,左侧的乘积已经在answer[i]中,乘以右侧的乘积R
answer[i] *= R;
// 更新R,包括当前元素nums[i]
R *= nums[i];
}
return answer;
}
这段 C++代码遵循了之前描述的解题思路,首先通过一个循环计算了数组中每个元素左侧所有元素的乘积,然后在第二个循环中从数组的末尾开始,计算右侧所有元素的乘积并更新答案数组。这样就在不使用除法的情况下,以 O(n)的时间复杂度解决了问题。
Python 版本
def productExceptSelf(nums):
length = len(nums)
answer = [1] * length
# 计算左侧所有元素的乘积
for i in range(1, length):
answer[i] = answer[i - 1] * nums[i - 1]
# R用于存储从右侧开始的累积乘积
R = 1
for i in reversed(range(length)):
# 对于索引i,左侧的乘积已经在answer[i]中,乘以右侧的乘积R
answer[i] *= R
# 更新R,包括当前元素nums[i]
R *= nums[i]
return answer
这段 Python 代码实现了之前描述的算法,首先创建了一个与输入数组等长的答案数组,并初始化所有元素为 1。然后通过两次遍历,一次计算左侧所有元素的乘积,一次计算右侧所有元素的乘积并更新答案数组,从而得到最终结果。
Java 版本
public class Solution {
public int[] productExceptSelf(int[] nums) {
int length = nums.length;
int[] answer = new int[length];
// 计算左侧所有元素的乘积
answer[0] = 1;
for (int i = 1; i < length; i++) {
answer[i] = answer[i - 1] * nums[i - 1];
}
// R用于存储从右侧开始的累积乘积
int R = 1;
for (int i = length - 1; i >= 0; i--) {
// 对于索引i,左侧的乘积已经在answer[i]中,乘以右侧的乘积R
answer[i] = answer[i] * R;
// 更新R,包括当前元素nums[i]
R = R * nums[i];
}
return answer;
}
}
这段 Java 代码遵循了之前描述的算法逻辑,首先初始化一个答案数组,并设置第一个元素的左侧乘积为 1。然后通过两次遍历数组,一次计算左侧所有元素的乘积,一次从右侧开始计算右侧所有元素的乘积并更新答案数组。这样就在不使用除法的情况下,以 O(n)的时间复杂度解决了问题。
Rust 版本
fn product_except_self(nums: Vec<i32>) -> Vec<i32> { let length = nums.len(); let mut answer: Vec<i32> = vec![1; length]; // 计算左侧所有元素的乘积 for i in 1..length { answer[i] = answer[i - 1] * nums[i - 1]; } // R用于存储从右侧开始的累积乘积 let mut R = 1; for i in (0..length).rev() { // 对于索引i,左侧的乘积已经在answer[i]中,乘以右侧的乘积R answer[i] *= R; // 更新R,包括当前元素nums[i] R *= nums[i]; } answer } fn main() { let nums = vec![1, 2, 3, 4]; let result = product_except_self(nums); println!("{:?}", result); }
这段 Rust 代码实现了之前描述的算法,首先创建了一个与输入向量等长的答案向量,并初始化所有元素为 1。然后通过两次遍历,一次计算左侧所有元素的乘积,一次计算右侧所有元素的乘积并更新答案向量,从而得到最终结果。在main
函数中,我们创建了一个示例向量,并打印了调用product_except_self
函数的结果。
总结
这个问题要求我们对于给定的整数数组nums
,生成一个新的数组answer
,其中每个answer[i]
是除了nums[i]
之外所有元素的乘积。解决这个问题的关键是不使用除法,并且在 O(n)的时间复杂度内完成。
解题思路分为以下几个步骤:
- 初始化一个结果数组
answer
,并将其所有元素设为 1,这样不会影响乘积的计算。 - 从左到右遍历数组
nums
,计算每个位置左侧所有元素的乘积,存储在answer
数组相应位置。 - 从右到左遍历数组
nums
,计算每个位置右侧所有元素的乘积。为了不使用额外的空间,我们用一个变量R
来跟踪右侧元素的乘积,并且在遍历的过程中更新answer[i]
为answer[i] * R
。 - 在第二次遍历结束后,
answer
数组中的每个元素就是除了nums[i]
之外所有元素的乘积。
这种方法的优点是只需要 O(n)的时间复杂度和 O(1)的空间复杂度(不计算输出数组所占用的空间)。通过两次遍历数组,我们能够得到每个元素左侧和右侧元素的乘积,从而得到最终的答案数组。
缺失的第一个正数
题目要求
在这个问题中,我们需要处理一个未排序的整数数组 nums
。我们的目标是找到并返回这个数组中没有出现的最小的正整数。这个任务需要在 O(n) 时间复杂度内完成,并且只能使用常数级别的额外空间。
解题思路
要在 O(n) 时间复杂度内解决这个问题,同时又只能使用常数级别的额外空间,我们可以采用原地哈希(in-place hashing)的方法。以下是解题的步骤:
-
过滤负数和零:首先,我们可以忽略所有负数和零,因为它们不会影响我们寻找最小正整数的结果。
-
原地哈希:接下来,我们可以遍历数组,尝试将每个正整数放到其值对应的索引位置上。例如,如果我们遇到数字 3,我们就尝试将它放到索引为 2 的位置上(因为数组索引通常是从 0 开始的)。这个步骤中,我们需要注意的是,如果数字大于数组长度,或者要交换的两个数字相同,那么我们就不需要交换。
-
检查缺失的最小正整数:完成原地哈希后,我们再次遍历数组。这一次,我们检查每个索引位置是否放置了正确的数字。第一个不符合“索引 + 1 等于数字本身”的位置,其索引 + 1 的值就是我们要找的最小的缺失正整数。例如,如果在索引 0 的位置不是数字 1,那么最小的缺失正整数就是 1。
-
处理特殊情况:如果数组中的所有位置都放置了正确的数字,这意味着数组形如 [1, 2, 3, ..., n],那么缺失的最小正整数就是 n+1。
通过以上步骤,我们可以在 O(n) 的时间复杂度内找到缺失的最小正整数,同时只使用了常数级别的额外空间,因为我们没有使用额外的数组或数据结构来存储信息。
Golang 版本
package main
import (
"fmt"
)
func firstMissingPositive(nums []int) int {
n := len(nums)
// 将所有的负数转换为n+1(一个肯定不会是答案的数)
for i := 0; i < n; i++ {
if nums[i] <= 0 {
nums[i] = n + 1
}
}
// 使用索引作为哈希键,对于每个数字,如果它在1到n的范围内,就将对应索引位置的数字标记为负数
for i := 0; i < n; i++ {
num := abs(nums[i])
if num <= n {
nums[num-1] = -abs(nums[num-1])
}
}
// 第一个正数的索引+1就是我们要找的最小正整数
for i := 0; i < n; i++ {
if nums[i] > 0 {
return i + 1
}
}
// 如果所有的数都被标记了,说明数组是连续的1到n,那么答案就是n+1
return n + 1
}
// abs 返回一个整数的绝对值
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
func main() {
nums := []int{3, 4, -1, 1}
fmt.Println("The smallest missing positive integer is:", firstMissingPositive(nums))
}
这段代码实现了上述的解题思路,定义了 firstMissingPositive
函数来找出未排序整数数组中缺失的最小正整数,并在 main
函数中测试了这个功能。函数 abs
用于计算绝对值,以便在原地哈希过程中正确处理数字。
C++版本
#include <iostream>
#include <vector>
#include <algorithm>
int firstMissingPositive(std::vector<int>& nums) {
int n = nums.size();
// 将所有的负数和零转换为n+1(一个肯定不会是答案的数)
for (int& num : nums) {
if (num <= 0) {
num = n + 1;
}
}
// 使用索引作为哈希键,对于每个数字,如果它在1到n的范围内,就将对应索引位置的数字标记为负数
for (int i = 0; i < n; ++i) {
int num = abs(nums[i]);
if (num <= n) {
nums[num - 1] = -abs(nums[num - 1]);
}
}
// 第一个正数的索引+1就是我们要找的最小正整数
for (int i = 0; i < n; ++i) {
if (nums[i] > 0) {
return i + 1;
}
}
// 如果所有的数都被标记了,说明数组是连续的1到n,那么答案就是n+1
return n + 1;
}
int main() {
std::vector<int> nums = {3, 4, -1, 1};
std::cout << "The smallest missing positive integer is: " << firstMissingPositive(nums) << std::endl;
return 0;
}
这段 C++代码实现了寻找未排序整数数组中缺失的最小正整数的功能。它首先将所有非正整数转换为一个大于数组长度的数,然后通过将存在的正整数对应索引的数值变为负数来标记这些数已经存在。最后,遍历数组,第一个索引位置上数值为正的索引加 1 即为缺失的最小正整数。如果所有数值都被标记为负数,则说明数组中包含了从 1 到 n 的连续整数,因此返回 n+1。
Python 版本
def first_missing_positive(nums):
n = len(nums)
# 将所有的负数和零转换为n+1(一个肯定不会是答案的数)
for i in range(n):
if nums[i] <= 0:
nums[i] = n + 1
# 使用索引作为哈希键,对于每个数字,如果它在1到n的范围内,就将对应索引位置的数字标记为负数
for i in range(n):
num = abs(nums[i])
if num <= n:
nums[num - 1] = -abs(nums[num - 1])
# 第一个正数的索引+1就是我们要找的最小正整数
for i in range(n):
if nums[i] > 0:
return i + 1
# 如果所有的数都被标记了,说明数组是连续的1到n,那么答案就是n+1
return n + 1
# 测试代码
if __name__ == "__main__":
nums = [3, 4, -1, 1]
print("The smallest missing positive integer is:", first_missing_positive(nums))
这段 Python 代码实现了寻找未排序整数数组中缺失的最小正整数的功能。它首先将所有非正整数转换为一个大于数组长度的数,然后通过将存在的正整数对应索引的数值变为负数来标记这些数已经存在。最后,遍历数组,第一个索引位置上数值为正的索引加 1 即为缺失的最小正整数。如果所有数值都被标记为负数,则说明数组中包含了从 1 到 n 的连续整数,因此返回 n+1。
Java 版本
public class Solution {
public int firstMissingPositive(int[] nums) {
int n = nums.length;
// 将所有的负数和零转换为n+1(一个肯定不会是答案的数)
for (int i = 0; i < n; ++i) {
if (nums[i] <= 0) {
nums[i] = n + 1;
}
}
// 使用索引作为哈希键,对于每个数字,如果它在1到n的范围内,就将对应索引位置的数字标记为负数
for (int i = 0; i < n; ++i) {
int num = Math.abs(nums[i]);
if (num <= n) {
nums[num - 1] = -Math.abs(nums[num - 1]);
}
}
// 第一个正数的索引+1就是我们要找的最小正整数
for (int i = 0; i < n; ++i) {
if (nums[i] > 0) {
return i + 1;
}
}
// 如果所有的数都被标记了,说明数组是连续的1到n,那么答案就是n+1
return n + 1;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {3, 4, -1, 1};
System.out.println("The smallest missing positive integer is: " + solution.firstMissingPositive(nums));
}
}
这段 Java 代码实现了寻找未排序整数数组中缺失的最小正整数的功能。它首先将所有非正整数转换为一个大于数组长度的数,然后通过将存在的正整数对应索引的数值变为负数来标记这些数已经存在。最后,遍历数组,第一个索引位置上数值为正的索引加 1 即为缺失的最小正整数。如果所有数值都被标记为负数,则说明数组中包含了从 1 到 n 的连续整数,因此返回 n+1。
Rust 版本
fn first_missing_positive(nums: &mut Vec<i32>) -> i32 { let n = nums.len(); // 将所有的负数和零转换为n+1(一个肯定不会是答案的数) for num in nums.iter_mut() { if *num <= 0 { *num = n as i32 + 1; } } // 使用索引作为哈希键,对于每个数字,如果它在1到n的范围内,就将对应索引位置的数字标记为负数 for i in 0..n { let num = nums[i].abs(); if num <= n as i32 { let idx = (num - 1) as usize; nums[idx] = -nums[idx].abs(); } } // 第一个正数的索引+1就是我们要找的最小正整数 for (i, &num) in nums.iter().enumerate() { if num > 0 { return (i + 1) as i32; } } // 如果所有的数都被标记了,说明数组是连续的1到n,那么答案就是n+1 (n as i32) + 1 } fn main() { let mut nums = vec![3, 4, -1, 1]; println!("The smallest missing positive integer is: {}", first_missing_positive(&mut nums)); }
这段 Rust 代码实现了寻找未排序整数数组中缺失的最小正整数的功能。它首先将所有非正整数转换为一个大于数组长度的数,然后通过将存在的正整数对应索引的数值变为负数来标记这些数已经存在。最后,遍历数组,第一个索引位置上数值为正的索引加 1 即为缺失的最小正整数。如果所有数值都被标记为负数,则说明数组中包含了从 1 到 n 的连续整数,因此返回 n+1。
总结
上述解法采用了一个巧妙的方法来在 O(n)时间复杂度和常数空间复杂度内找到未排序数组中缺失的最小正整数。解法的核心思想是利用数组本身作为哈希表来记录信息。以下是解法的步骤总结:
-
替换非正数:首先,遍历数组,将所有的非正数(包括零和负数)替换为一个大于数组长度的数(例如 n+1)。这样做的目的是为了确保数组中所有的数都是正数,且不会影响后续的步骤。
-
标记存在的正数:接着,再次遍历数组,对于每个数 num(绝对值),如果它在 1 到 n 的范围内,就将数组中索引为 num-1 的位置的数标记为负数。这样做相当于记录了 num 这个数已经在数组中出现过。
-
寻找缺失的最小正数:最后,再次遍历数组,寻找第一个正数出现的位置。由于之前的标记,如果数组中缺少某个数,那么对应的索引位置上的数将不会被标记为负数,因此第一个正数的索引加 1 就是缺失的最小正数。
-
处理特殊情况:如果数组中的数是从 1 到 n 的连续正整数,那么上述步骤后,所有的数都会被标记为负数。在这种情况下,缺失的最小正整数就是 n+1。
这种解法巧妙地避免了额外空间的使用,同时保证了线性的时间复杂度,是解决这类问题的一个高效算法。
矩阵
解决关于矩阵的算法题通常涉及以下几个方面的思路:
-
遍历:矩阵题目的基础是遍历矩阵中的元素。常见的遍历方式有按行遍历、按列遍历、对角线遍历等。
-
动态规划:对于一些涉及最优解、路径计算的问题,动态规划是常用的方法。例如,计算从矩阵左上角到右下角的最小路径和。
-
深度优先搜索(DFS)和广度优先搜索(BFS):对于矩阵中的图搜索问题,如岛屿数量、迷宫问题等,DFS 和 BFS 是常用的策略。
-
双指针:在一些问题中,如旋转矩阵、搜索二维矩阵等,可以使用双指针技巧来减少时间复杂度。
-
数学规律:有些矩阵问题可以通过观察数学规律来解决,比如螺旋矩阵的输出。
-
辅助数据结构:使用栈、队列、集合等数据结构可以帮助解决一些矩阵问题。
下面是一些使用 Go 语言解决矩阵问题的代码示例:
例 1:螺旋矩阵遍历
func spiralOrder(matrix [][]int) []int {
if len(matrix) == 0 {
return []int{}
}
var result []int
top, bottom := 0, len(matrix)-1
left, right := 0, len(matrix[0])-1
for left <= right && top <= bottom {
// Traverse from left to right.
for i := left; i <= right; i++ {
result = append(result, matrix[top][i])
}
top++
// Traverse downwards.
for i := top; i <= bottom; i++ {
result = append(result, matrix[i][right])
}
right--
// Make sure we are now on a different row.
if top <= bottom {
// Traverse from right to left.
for i := right; i >= left; i-- {
result = append(result, matrix[bottom][i])
}
bottom--
}
// Make sure we are now on a different column.
if left <= right {
// Traverse upwards.
for i := bottom; i >= top; i-- {
result = append(result, matrix[i][left])
}
left++
}
}
return result
}
例 2:动态规划解决最小路径和
func minPathSum(grid [][]int) int {
if len(grid) == 0 || len(grid[0]) == 0 {
return 0
}
rows, cols := len(grid), len(grid[0])
// Initialize the first row and column.
for i := 1; i < rows; i++ {
grid[i][0] += grid[i-1][0]
}
for j := 1; j < cols; j++ {
grid[0][j] += grid[0][j-1]
}
// Fill up the dp table.
for i := 1; i < rows; i++ {
for j := 1; j < cols; j++ {
grid[i][j] += min(grid[i-1][j], grid[i][j-1])
}
}
return grid[rows-1][cols-1]
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
在解决矩阵问题时,理解问题的本质和选择合适的算法是关键。Go 语言的切片操作和简洁的语法结构使得它在处理这类问题时非常高效。
矩阵置零
题目要求
给定一个 m 行 n 列的矩阵,要求编写一个算法,当矩阵中的某个元素值为 0 时,将该元素所在的整行和整列的所有元素都设置为 0。这个过程需要在原矩阵上直接进行,即不允许使用额外的存储空间来创建矩阵的副本。
解题思路
由于题目要求不能使用额外的存储空间,我们不能直接创建一个同样大小的矩阵来标记 0 元素的位置。因此,我们需要在原矩阵上进行标记,以便知道哪些行和列需要被设置为 0。
-
首先遍历矩阵,我们可以使用矩阵的第一行和第一列来记录该行或该列是否需要被设置为 0。但在此之前,我们需要两个标记变量来分别记录第一行和第一列是否原本就包含 0(因为第一行和第一列也是矩阵的一部分,它们也可能需要被设置为 0)。
-
设置标记变量:通过遍历第一行和第一列,如果发现有 0,则将对应的标记变量设置为 true。
-
使用第一行和第一列记录其他行列是否包含 0:从第二行第二列开始遍历矩阵中的每个元素,如果遇到 0,则将该元素所在行的第一个元素和所在列的第一个元素设置为 0。这样做的目的是使用矩阵的边缘作为标记,来记录哪些行列需要被置 0。
-
根据第一行和第一列的标记来更新矩阵:再次遍历矩阵,这次是从第二行第二列开始,如果发现某行的第一个元素或某列的第一个元素为 0,则将当前遍历到的元素设置为 0。
-
根据标记变量更新第一行和第一列:最后,根据第一行和第一列的标记变量,如果它们是 true,则将整行或整列的元素设置为 0。
这样,我们就可以在不使用额外空间的情况下,将矩阵中的相应行列元素设置为 0。注意,在实际编码时,要小心处理第一行和第一列的标记,以免它们被提前更新导致信息丢失。
Golang 版本
package main
import "fmt"
func setZeroes(matrix [][]int) {
if len(matrix) == 0 || len(matrix[0]) == 0 {
return
}
m, n := len(matrix), len(matrix[0])
firstRowHasZero := false
firstColHasZero := false
// Check if the first row has a zero
for j := 0; j < n; j++ {
if matrix[0][j] == 0 {
firstRowHasZero = true
break
}
}
// Check if the first column has a zero
for i := 0; i < m; i++ {
if matrix[i][0] == 0 {
firstColHasZero = true
break
}
}
// Use first row and column as markers, set matrix[i][0] and matrix[0][j] to 0 if matrix[i][j] is 0
for i := 1; i < m; i++ {
for j := 1; j < n; j++ {
if matrix[i][j] == 0 {
matrix[i][0] = 0
matrix[0][j] = 0
}
}
}
// Zero out cells based on the markers in the first row and column
for i := 1; i < m; i++ {
for j := 1; j < n; j++ {
if matrix[i][0] == 0 || matrix[0][j] == 0 {
matrix[i][j] = 0
}
}
}
// Zero out the first row if needed
if firstRowHasZero {
for j := 0; j < n; j++ {
matrix[0][j] = 0
}
}
// Zero out the first column if needed
if firstColHasZero {
for i := 0; i < m; i++ {
matrix[i][0] = 0
}
}
}
func main() {
matrix := [][]int{
{1, 1, 1},
{1, 0, 1},
{1, 1, 1},
}
setZeroes(matrix)
fmt.Println(matrix)
}
这段代码实现了上述的解题思路。首先,它检查第一行和第一列是否包含 0,然后使用第一行和第一列作为标记,遍历剩余的矩阵元素,根据标记将需要置 0 的行和列元素更新。最后,根据第一行和第一列的标记变量,更新第一行和第一列本身。
C++版本
#include <vector>
#include <iostream>
void setZeroes(std::vector<std::vector<int>>& matrix) {
int m = matrix.size();
int n = matrix[0].size();
bool firstRowHasZero = false;
bool firstColHasZero = false;
// Check if the first row has a zero
for (int j = 0; j < n; ++j) {
if (matrix[0][j] == 0) {
firstRowHasZero = true;
break;
}
}
// Check if the first column has a zero
for (int i = 0; i < m; ++i) {
if (matrix[i][0] == 0) {
firstColHasZero = true;
break;
}
}
// Use first row and column as markers
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j) {
if (matrix[i][j] == 0) {
matrix[i][0] = 0;
matrix[0][j] = 0;
}
}
}
// Zero out cells based on the markers in the first row and column
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j) {
if (matrix[i][0] == 0 || matrix[0][j] == 0) {
matrix[i][j] = 0;
}
}
}
// Zero out the first row if needed
if (firstRowHasZero) {
for (int j = 0; j < n; ++j) {
matrix[0][j] = 0;
}
}
// Zero out the first column if needed
if (firstColHasZero) {
for (int i = 0; i < m; ++i) {
matrix[i][0] = 0;
}
}
}
int main() {
std::vector<std::vector<int>> matrix = {
{0, 1, 2, 0},
{3, 4, 5, 2},
{1, 3, 1, 5}
};
setZeroes(matrix);
for (const auto& row : matrix) {
for (int val : row) {
std::cout << val << " ";
}
std::cout << std::endl;
}
return 0;
}
这段 C++代码实现了与 Golang 版本相同的逻辑。首先检查第一行和第一列是否包含 0,然后使用第一行和第一列作为标记,遍历剩余的矩阵元素,根据标记将需要置 0 的行和列元素更新。最后,根据第一行和第一列的标记变量,更新第一行和第一列本身。
Python 版本
def setZeroes(matrix):
m, n = len(matrix), len(matrix[0])
firstRowHasZero = not all(matrix[0][j] != 0 for j in range(n))
firstColHasZero = not all(matrix[i][0] != 0 for i in range(m))
# Use first row and column as markers
for i in range(1, m):
for j in range(1, n):
if matrix[i][j] == 0:
matrix[0][j] = 0
matrix[i][0] = 0
# Zero out cells based on the markers in the first row and column
for i in range(1, m):
for j in range(1, n):
if matrix[0][j] == 0 or matrix[i][0] == 0:
matrix[i][j] = 0
# Zero out the first row if needed
if firstRowHasZero:
for j in range(n):
matrix[0][j] = 0
# Zero out the first column if needed
if firstColHasZero:
for i in range(m):
matrix[i][0] = 0
# Example usage:
matrix = [
[1, 1, 1],
[1, 0, 1],
[1, 1, 1]
]
setZeroes(matrix)
for row in matrix:
print(row)
这段 Python 代码实现了矩阵置零的功能。首先检查第一行和第一列是否包含 0,然后使用第一行和第一列作为标记,遍历剩余的矩阵元素,根据标记将需要置 0 的行和列元素更新。最后,根据第一行和第一列的标记变量,更新第一行和第一列本身。
Java 版本
public class Solution {
public void setZeroes(int[][] matrix) {
boolean firstRowHasZero = false;
boolean firstColumnHasZero = false;
int rows = matrix.length;
int columns = matrix[0].length;
// Check if the first row has any zeros
for (int j = 0; j < columns; j++) {
if (matrix[0][j] == 0) {
firstRowHasZero = true;
break;
}
}
// Check if the first column has any zeros
for (int i = 0; i < rows; i++) {
if (matrix[i][0] == 0) {
firstColumnHasZero = true;
break;
}
}
// Use first row and column as markers, set the markers
for (int i = 1; i < rows; i++) {
for (int j = 1; j < columns; j++) {
if (matrix[i][j] == 0) {
matrix[i][0] = 0;
matrix[0][j] = 0;
}
}
}
// Use the markers to set elements to zero
for (int i = 1; i < rows; i++) {
for (int j = 1; j < columns; j++) {
if (matrix[i][0] == 0 || matrix[0][j] == 0) {
matrix[i][j] = 0;
}
}
}
// Set first row to zero if needed
if (firstRowHasZero) {
for (int j = 0; j < columns; j++) {
matrix[0][j] = 0;
}
}
// Set first column to zero if needed
if (firstColumnHasZero) {
for (int i = 0; i < rows; i++) {
matrix[i][0] = 0;
}
}
}
public static void main(String[] args) {
Solution solution = new Solution();
int[][] matrix = {
{1, 1, 1},
{1, 0, 1},
{1, 1, 1}
};
solution.setZeroes(matrix);
for (int[] row : matrix) {
for (int val : row) {
System.out.print(val + " ");
}
System.out.println();
}
}
}
这段 Java 代码实现了矩阵置零的功能。首先检查第一行和第一列是否包含 0,然后使用第一行和第一列作为标记,遍历剩余的矩阵元素,根据标记将需要置 0 的行和列元素更新。最后,根据第一行和第一列的标记变量,更新第一行和第一列本身。
Rust 版本
fn set_zeroes(matrix: &mut Vec<Vec<i32>>) { let mut first_row_has_zero = false; let mut first_col_has_zero = false; let rows = matrix.len(); let cols = matrix[0].len(); // Check if the first row has any zeros for j in 0..cols { if matrix[0][j] == 0 { first_row_has_zero = true; break; } } // Check if the first column has any zeros for i in 0..rows { if matrix[i][0] == 0 { first_col_has_zero = true; break; } } // Use first row and column as markers, set the markers for i in 1..rows { for j in 1..cols { if matrix[i][j] == 0 { matrix[i][0] = 0; matrix[0][j] = 0; } } } // Use the markers to set elements to zero for i in 1..rows { for j in 1..cols { if matrix[i][0] == 0 || matrix[0][j] == 0 { matrix[i][j] = 0; } } } // Set first row to zero if needed if first_row_has_zero { for j in 0..cols { matrix[0][j] = 0; } } // Set first column to zero if needed if first_col_has_zero { for i in 0..rows { matrix[i][0] = 0; } } } fn main() { let mut matrix = vec![ vec![1, 1, 1], vec![1, 0, 1], vec![1, 1, 1], ]; set_zeroes(&mut matrix); for row in matrix { println!("{:?}", row); } }
这段 Rust 代码实现了矩阵置零的功能。首先检查第一行和第一列是否包含 0,然后使用第一行和第一列作为标记,遍历剩余的矩阵元素,根据标记将需要置 0 的行和列元素更新。最后,根据第一行和第一列的标记变量,更新第一行和第一列本身。在main
函数中,我们创建了一个示例矩阵,调用set_zeroes
函数,并打印出修改后的矩阵。
总结
上面的 Rust 代码解决了矩阵置零的问题,其核心思想是使用矩阵的第一行和第一列作为标记数组,以此来记录哪些行和列需要被置零。这样做的好处是不需要额外的存储空间。具体步骤如下:
-
首先遍历第一行和第一列,检查它们是否包含 0,这是因为后续的算法会改变第一行和第一列的值,所以需要先记录下它们原本是否应该被置零。
-
从第二行和第二列开始遍历矩阵的其余部分,如果发现一个元素的值为 0,则将其所在行的第一个元素和所在列的第一个元素设置为 0。这样做是为了使用矩阵的第一行和第一列来标记该行和该列是否需要被置零。
-
再次遍历矩阵(除了第一行和第一列),这次是根据第一行和第一列的标记来更新矩阵。如果某个元素所在的行的第一个元素或所在的列的第一个元素被设置为 0,则将该元素也设置为 0。
-
最后,根据第一步中记录的信息,决定是否需要将第一行和第一列置零。
这种方法的关键在于,它避免了使用额外的存储空间,而是巧妙地利用了矩阵本身的空间来存储信息。这对于空间复杂度要求高的场景非常有用。
螺旋矩阵
题目要求
编写一个函数,该函数接收一个m x n
的矩阵matrix
作为输入参数。你需要按照顺时针螺旋的顺序遍历矩阵,并返回一个数组,该数组依次包含矩阵中的所有元素。
解题思路
解决这个问题的关键是确定矩阵遍历的边界,并且在遍历过程中逐步缩小这些边界。以下是解题的步骤:
-
初始化四个指标,分别代表当前遍历的上下左右边界:
top
(初始值为 0),bottom
(初始值为 m-1),left
(初始值为 0),right
(初始值为 n-1)。 -
创建一个空数组
result
,用于存放按顺时针螺旋顺序遍历的矩阵元素。 -
当
top
小于等于bottom
且left
小于等于right
时,执行以下步骤:a. 从左到右遍历上边界,即遍历
top
行的left
到right
列,遍历后top
增加 1,因为上边界已经遍历过了。b. 从上到下遍历右边界,即遍历
top
到bottom
行的right
列,遍历后right
减少 1,因为右边界已经遍历过了。c. 如果
top
仍然小于等于bottom
,则从右到左遍历下边界,即遍历bottom
行的right
到left
列,遍历后bottom
减少 1,因为下边界已经遍历过了。d. 如果
left
仍然小于等于right
,则从下到上遍历左边界,即遍历bottom
到top
行的left
列,遍历后left
增加 1,因为左边界已经遍历过了。 -
重复步骤 3,直到所有的边界都已经遍历过,即
top
大于bottom
或者left
大于right
。 -
返回
result
数组,该数组包含了按顺时针螺旋顺序遍历的矩阵元素。
这个方法的核心在于不断地缩小遍历的范围,并且在每一步中都要检查边界条件,以确保每个元素只被访问一次。
Golang 版本
package main
import "fmt"
func spiralOrder(matrix [][]int) []int {
if len(matrix) == 0 {
return []int{}
}
var result []int
top, bottom := 0, len(matrix)-1
left, right := 0, len(matrix[0])-1
for left <= right && top <= bottom {
// Traverse from left to right
for i := left; i <= right; i++ {
result = append(result, matrix[top][i])
}
top++
// Traverse downwards
for i := top; i <= bottom; i++ {
result = append(result, matrix[i][right])
}
right--
// Make sure we are now on a different row
if top <= bottom {
// Traverse from right to left
for i := right; i >= left; i-- {
result = append(result, matrix[bottom][i])
}
bottom--
}
// Make sure we are now on a different column
if left <= right {
// Traverse upwards
for i := bottom; i >= top; i-- {
result = append(result, matrix[i][left])
}
left++
}
}
return result
}
func main() {
matrix := [][]int{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
}
result := spiralOrder(matrix)
fmt.Println(result)
}
这段代码定义了一个spiralOrder
函数,它接收一个二维整数数组matrix
,并返回一个一维整数数组,包含按顺时针螺旋顺序遍历的矩阵元素。main
函数中给出了一个示例矩阵,并打印出了spiralOrder
函数的结果。
C++版本
#include <iostream>
#include <vector>
std::vector<int> spiralOrder(const std::vector<std::vector<int>>& matrix) {
std::vector<int> result;
if (matrix.empty()) return result;
int top = 0, bottom = matrix.size() - 1;
int left = 0, right = matrix[0].size() - 1;
while (left <= right && top <= bottom) {
// Traverse from left to right
for (int i = left; i <= right; ++i) {
result.push_back(matrix[top][i]);
}
++top;
// Traverse downwards
for (int i = top; i <= bottom; ++i) {
result.push_back(matrix[i][right]);
}
--right;
if (top <= bottom) {
// Traverse from right to left
for (int i = right; i >= left; --i) {
result.push_back(matrix[bottom][i]);
}
--bottom;
}
if (left <= right) {
// Traverse upwards
for (int i = bottom; i >= top; --i) {
result.push_back(matrix[i][left]);
}
++left;
}
}
return result;
}
int main() {
std::vector<std::vector<int>> matrix = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
std::vector<int> result = spiralOrder(matrix);
for (int num : result) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
这段 C++代码定义了一个spiralOrder
函数,它接收一个二维整数向量matrix
作为参数,并返回一个一维整数向量,包含按顺时针螺旋顺序遍历的矩阵元素。main
函数中给出了一个示例矩阵,并打印出了spiralOrder
函数的结果。
Python 版本
def spiralOrder(matrix):
if not matrix or not matrix[0]:
return []
result = []
top, bottom, left, right = 0, len(matrix) - 1, 0, len(matrix[0]) - 1
while left <= right and top <= bottom:
# Traverse from left to right
for i in range(left, right + 1):
result.append(matrix[top][i])
top += 1
# Traverse downwards
for i in range(top, bottom + 1):
result.append(matrix[i][right])
right -= 1
if top <= bottom:
# Traverse from right to left
for i in range(right, left - 1, -1):
result.append(matrix[bottom][i])
bottom -= 1
if left <= right:
# Traverse upwards
for i in range(bottom, top - 1, -1):
result.append(matrix[i][left])
left += 1
return result
# Example usage:
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
print(spiralOrder(matrix))
这段 Python 代码定义了一个spiralOrder
函数,它接收一个二维列表matrix
作为参数,并返回一个列表,包含按顺时针螺旋顺序遍历的矩阵元素。代码中还包含了一个示例矩阵,并打印出了spiralOrder
函数的结果。
Java 版本
import java.util.ArrayList;
import java.util.List;
public class SpiralMatrix {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> result = new ArrayList<>();
if (matrix.length == 0) return result;
int top = 0, bottom = matrix.length - 1;
int left = 0, right = matrix[0].length - 1;
while (left <= right && top <= bottom) {
// Traverse from left to right
for (int i = left; i <= right; i++) {
result.add(matrix[top][i]);
}
top++;
// Traverse downwards
for (int i = top; i <= bottom; i++) {
result.add(matrix[i][right]);
}
right--;
if (top <= bottom) {
// Traverse from right to left
for (int i = right; i >= left; i--) {
result.add(matrix[bottom][i]);
}
bottom--;
}
if (left <= right) {
// Traverse upwards
for (int i = bottom; i >= top; i--) {
result.add(matrix[i][left]);
}
left++;
}
}
return result;
}
public static void main(String[] args) {
SpiralMatrix sm = new SpiralMatrix();
int[][] matrix = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
List<Integer> result = sm.spiralOrder(matrix);
for (int num : result) {
System.out.print(num + " ");
}
}
}
这段 Java 代码定义了一个SpiralMatrix
类,其中包含了一个spiralOrder
方法,它接收一个二维整数数组matrix
作为参数,并返回一个整数列表,包含按顺时针螺旋顺序遍历的矩阵元素。main
方法中给出了一个示例矩阵,并打印出了spiralOrder
方法的结果。
Rust 版本
fn spiral_order(matrix: Vec<Vec<i32>>) -> Vec<i32> { if matrix.is_empty() || matrix[0].is_empty() { return Vec::new(); } let mut result = Vec::new(); let (mut top, mut bottom) = (0, matrix.len() - 1); let (mut left, mut right) = (0, matrix[0].len() - 1); while left <= right && top <= bottom { // Traverse from left to right for i in left..=right { result.push(matrix[top][i]); } top += 1; // Traverse downwards if top <= bottom { for i in top..=bottom { result.push(matrix[i][right]); } right -= 1; } // Traverse from right to left if left <= right && top <= bottom { for i in (left..=right).rev() { result.push(matrix[bottom][i]); } bottom -= 1; } // Traverse upwards if top <= bottom { for i in (top..=bottom).rev() { result.push(matrix[i][left]); } left += 1; } } result } fn main() { let matrix = vec![ vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9], ]; let order = spiral_order(matrix); for num in order { print!("{} ", num); } }
这段 Rust 代码定义了一个spiral_order
函数,它接收一个二维 i32 向量matrix
作为参数,并返回一个 i32 向量,包含按顺时针螺旋顺序遍历的矩阵元素。main
函数中给出了一个示例矩阵,并打印出了spiral_order
函数的结果。
总结
上述代码提供了一个螺旋遍历二维矩阵的算法实现。无论是在 C++、Python、Java 还是 Rust 中,核心逻辑是相同的,主要步骤如下:
-
初始化四个指针,分别代表矩阵的上边界
top
、下边界bottom
、左边界left
和右边界right
。 -
使用一个循环,按照从左到右、从上到下、从右到左、从下到上的顺序遍历矩阵的边界,并逐步缩小边界范围。
-
在每一步遍历中,将遍历到的元素添加到结果列表中。
-
每完成一次边界的遍历后,根据遍历的方向更新对应的边界指针。
-
当左边界超过右边界或上边界超过下边界时,结束遍历。
这个算法的时间复杂度是 O(N),其中 N 是矩阵中元素的总数,因为每个元素都被访问一次。空间复杂度是 O(N),用于存储结果的空间。
不同编程语言的实现细节可能略有不同,例如在边界条件的检查和索引的更新上,但整体算法框架是一致的。
旋转图像
题目要求
给定一个 n × n 的二维矩阵 matrix
,代表一个图像。要求将图像顺时针旋转 90 度。必须在原地旋转图像,即直接修改输入的二维矩阵,不允许使用另一个矩阵来旋转图像。
解题思路
顺时针旋转图像 90 度,可以分解为两个步骤:
- 转置矩阵:将矩阵的行转换成列,即 matrix[i][j] 与 matrix[j][i] 交换。
- 反转每一行:将每一行的元素反向排列,即第一个元素与最后一个元素交换,这样做是为了实现顺时针旋转的效果。
具体步骤如下:
- 首先,遍历矩阵的上半部分(对角线以上的部分),对于矩阵中的元素 matrix[i][j],与其对称位置的元素 matrix[j][i] 进行交换。
- 然后,对矩阵的每一行进行反转操作,即将第 i 行的第 j 个元素与第 n-j-1 个元素交换,其中 n 是矩阵的大小。
这样,就可以实现在原地对矩阵进行顺时针旋转 90 度的操作。注意,这种方法只适用于 n × n 的矩阵,即宽度和高度相等的情况。
Golang 版本
package main
import "fmt"
func rotate(matrix [][]int) {
n := len(matrix)
// Transpose the matrix
for i := 0; i < n; i++ {
for j := i; j < n; j++ {
matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
}
}
// Reverse each row
for i := 0; i < n; i++ {
for j := 0; j < n/2; j++ {
matrix[i][j], matrix[i][n-j-1] = matrix[i][n-j-1], matrix[i][j]
}
}
}
func main() {
// Example matrix
matrix := [][]int{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
}
rotate(matrix)
fmt.Println(matrix)
}
这段 Go 语言代码实现了上述的解题思路,首先对矩阵进行转置,然后反转每一行,以达到顺时针旋转 90 度的效果。在 main
函数中,定义了一个示例矩阵,并调用 rotate
函数来旋转这个矩阵,最后打印出旋转后的矩阵。
C++版本
#include <iostream>
#include <vector>
void rotate(std::vector<std::vector<int>>& matrix) {
int n = matrix.size();
// Transpose the matrix
for (int i = 0; i < n; ++i) {
for (int j = i; j < n; ++j) {
std::swap(matrix[i][j], matrix[j][i]);
}
}
// Reverse each row
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n / 2; ++j) {
std::swap(matrix[i][j], matrix[i][n - j - 1]);
}
}
}
int main() {
std::vector<std::vector<int>> matrix = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
rotate(matrix);
// Output the rotated matrix
for (const auto &row : matrix) {
for (int val : row) {
std::cout << val << ' ';
}
std::cout << '\n';
}
return 0;
}
这段 C++ 代码实现了矩阵的原地顺时针旋转 90 度。首先,通过双重循环对矩阵进行转置,然后再通过双重循环将每一行反转。在 main
函数中,定义了一个示例矩阵,并调用 rotate
函数来旋转这个矩阵,最后通过嵌套循环打印出旋转后的矩阵。
Python 版本
def rotate(matrix):
n = len(matrix)
# Transpose the matrix
for i in range(n):
for j in range(i, n):
matrix[j][i], matrix[i][j] = matrix[i][j], matrix[j][i]
# Reverse each row
for i in range(n):
matrix[i].reverse()
# Example usage
if __name__ == "__main__":
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
rotate(matrix)
for row in matrix:
print(row)
这段 Python 代码实现了矩阵的原地顺时针旋转 90 度。首先,通过双重循环对矩阵进行转置,然后使用列表的 reverse()
方法将每一行反转。在 if __name__ == "__main__":
块中,定义了一个示例矩阵,并调用 rotate
函数来旋转这个矩阵,最后通过循环打印出旋转后的矩阵。
Java 版本
public class MatrixRotation {
public static void rotate(int[][] matrix) {
int n = matrix.length;
// Transpose the matrix
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
int temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
}
// Reverse each row
for (int i = 0; i < n; i++) {
for (int j = 0; j < n / 2; j++) {
int temp = matrix[i][j];
matrix[i][j] = matrix[i][n - 1 - j];
matrix[i][n - 1 - j] = temp;
}
}
}
public static void main(String[] args) {
int[][] matrix = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
rotate(matrix);
// Print the rotated matrix
for (int[] row : matrix) {
for (int val : row) {
System.out.print(val + " ");
}
System.out.println();
}
}
}
这段 Java 代码实现了矩阵的原地顺时针旋转 90 度。首先,通过双重循环对矩阵进行转置,然后再通过双重循环将每一行反转。在 main
方法中,定义了一个示例矩阵,并调用 rotate
方法来旋转这个矩阵,最后通过嵌套循环打印出旋转后的矩阵。
Rust 版本
fn rotate(matrix: &mut Vec<Vec<i32>>) { let n = matrix.len(); // Transpose the matrix for i in 0..n { for j in (i + 1)..n { let temp = matrix[i][j]; matrix[i][j] = matrix[j][i]; matrix[j][i] = temp; } } // Reverse each row for row in matrix { row.reverse(); } } fn main() { let mut matrix = vec![ vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9], ]; rotate(&mut matrix); for row in matrix { println!("{:?}", row); } }
这段 Rust 代码实现了矩阵的原地顺时针旋转 90 度。首先,通过两层循环对矩阵进行转置,然后使用 reverse()
方法将每一行反转。在 main
函数中,定义了一个示例矩阵,并调用 rotate
函数来旋转这个矩阵,最后通过循环打印出旋转后的矩阵。
总结
无论是在哪种编程语言中,顺时针旋转一个 n × n 的二维矩阵 90 度的问题,都可以通过两个步骤来解决:
-
转置矩阵:这一步涉及将矩阵的行转换成列。具体操作是遍历矩阵的上半部分(对角线以上的部分),交换位置在 (i, j) 和 (j, i) 的元素,其中 i 是行索引,j 是列索引。
-
反转每一行:转置后,每一行的元素都是旋转后列的逆序,因此需要将每一行的元素反转,以获得正确的顺序。
在实现代码时,首先通过嵌套循环对矩阵进行转置,然后在每一行上调用反转操作。在不同的编程语言中,这些操作可能有不同的语法和函数调用,但基本算法逻辑是一致的。
- 在 C++ 中,使用
std::swap
来交换元素,以及通过索引访问和修改矩阵。 - 在 Python 中,可以直接交换元素,并使用列表的
reverse()
方法或切片来反转每一行。 - 在 Java 中,使用临时变量来交换元素,并通过索引访问和修改矩阵。
- 在 Rust 中,使用模式匹配来交换元素,并使用
reverse()
方法来反转每一行。
这种解法的空间复杂度为 O(1),因为它是在原地进行操作,不需要额外的存储空间。时间复杂度为 O(n^2),因为需要遍历矩阵中的每个元素。
搜索二维矩阵 II
题目要求
设计一个高效的算法,用于在一个 m 行 n 列的矩阵中搜索指定的目标值 target。这个矩阵具备以下性质:
- 每一行的元素都是从左到右递增排序。
- 每一列的元素都是从上到下递增排序。
解题思路
由于矩阵的行和列都是排序的,我们可以利用这个性质来减少搜索空间。以下是解题的步骤:
- 从矩阵的右上角开始搜索。设当前位置的元素为 element。
- 比较 element 和目标值 target:
- 如果 element 等于 target,那么我们找到了目标值,算法结束。
- 如果 element 大于 target,由于 element 是所在列的最小值,所以 target 不可能在当前列中,向左移动一列。
- 如果 element 小于 target,由于 element 是所在行的最大值,所以 target 不可能在当前行中,向下移动一行。
- 重复步骤 2,直到找到目标值或者搜索范围为空。
这个算法的效率之所以高,是因为每一次比较都排除了一行或者一列的可能性,这样可以以线性时间复杂度(O(m+n))完成搜索。
Golang 版本
func searchMatrix(matrix [][]int, target int) bool {
if len(matrix) == 0 || len(matrix[0]) == 0 {
return false
}
// 从右上角开始
row := 0
col := len(matrix[0]) - 1
// 当前位置在矩阵内时循环
for row < len(matrix) && col >= 0 {
if matrix[row][col] == target {
// 找到目标值
return true
} else if matrix[row][col] > target {
// 当前值大于目标值,排除当前列
col--
} else {
// 当前值小于目标值,排除当前行
row++
}
}
// 没有找到目标值
return false
}
这段代码实现了上述的解题思路,通过从矩阵的右上角开始,根据当前元素与目标值的比较结果来移动搜索位置,直到找到目标值或者确定目标值不存在于矩阵中。
C++版本
#include <vector>
bool searchMatrix(std::vector<std::vector<int>>& matrix, int target) {
if (matrix.empty() || matrix[0].empty()) {
return false;
}
int m = matrix.size();
int n = matrix[0].size();
int row = 0;
int col = n - 1;
while (row < m && col >= 0) {
if (matrix[row][col] == target) {
return true;
} else if (matrix[row][col] > target) {
--col;
} else {
++row;
}
}
return false;
}
这段 C++代码遵循了之前描述的算法逻辑,从矩阵的右上角开始搜索,根据当前元素与目标值的比较结果来决定是向左移动还是向下移动,直到找到目标值或者遍历完矩阵。
Python 版本
def searchMatrix(matrix, target):
if not matrix or not matrix[0]:
return False
# 从右上角开始
row, col = 0, len(matrix[0]) - 1
while row < len(matrix) and col >= 0:
if matrix[row][col] == target:
return True
elif matrix[row][col] > target:
col -= 1
else:
row += 1
return False
这段 Python 代码实现了之前描述的算法思路,从矩阵的右上角开始搜索,根据当前元素与目标值的比较结果来移动搜索位置,直到找到目标值或者确定目标值不存在于矩阵中。
Java 版本
public class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
if (matrix == null || 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 (matrix[row][col] == target) {
return true;
} else if (matrix[row][col] > target) {
col--;
} else {
row++;
}
}
return false;
}
}
这段 Java 代码遵循了之前描述的算法逻辑,从矩阵的右上角开始搜索,根据当前元素与目标值的比较结果来决定是向左移动还是向下移动,直到找到目标值或者遍历完矩阵。
Rust 版本
#![allow(unused)] fn main() { pub fn search_matrix(matrix: Vec<Vec<i32>>, target: i32) -> bool { if matrix.is_empty() || matrix[0].is_empty() { return false; } let (mut row, mut col) = (0, matrix[0].len() as i32 - 1); while row < matrix.len() as i32 && col >= 0 { match matrix[row as usize][col as usize].cmp(&target) { std::cmp::Ordering::Equal => return true, std::cmp::Ordering::Greater => col -= 1, std::cmp::Ordering::Less => row += 1, } } false } }
这段 Rust 代码实现了之前描述的算法思路,从矩阵的右上角开始搜索,根据当前元素与目标值的比较结果来移动搜索位置,直到找到目标值或者确定目标值不存在于矩阵中。在 Rust 中,我们使用cmp
方法来比较值,并根据比较结果来控制搜索的方向。
总结
上述解法针对一个特定的矩阵搜索问题,其中矩阵具有每行从左到右递增以及每列从上到下递增的特性。解法的核心思想是利用矩阵的这些性质来减少搜索空间,从而提高搜索效率。
算法从矩阵的右上角开始搜索,根据当前元素与目标值的比较结果来决定搜索的方向:
- 如果当前元素等于目标值,则搜索成功,返回
true
。 - 如果当前元素大于目标值,说明目标值不可能出现在当前元素的列中,因此向左移动一列。
- 如果当前元素小于目标值,说明目标值不可能出现在当前元素的行中,因此向下移动一行。
这个过程会一直重复,直到找到目标值或者搜索范围为空(即搜索到了矩阵的左下角仍未找到目标值),这时返回false
。
这种方法的时间复杂度为 O(m+n),其中 m 是矩阵的行数,n 是矩阵的列数。这是因为在最坏的情况下,算法可能需要遍历矩阵的全部行或全部列。
不同编程语言的实现(Go, C++, Python, Java, Rust)都遵循了这一核心算法思想,只是在语法和一些细节上有所不同。这种解法是对于这类特定矩阵搜索问题的一个高效解决方案。
链表
解决链表问题的通用思路可以分为以下几个步骤:
-
理解问题:首先要准确理解题目的要求,链表的类型(单链表、双链表等),以及是否有环。
-
确定算法策略:根据问题的类型,决定是使用双指针、递归、迭代、哈希表等策略。
-
边界条件处理:在编写代码之前,考虑链表为空、只有一个节点、只有两个节点等边界情况。
-
编写代码:根据选定的策略编写代码,注意指针的移动和条件判断。
-
测试用例:考虑各种可能的测试用例,包括边界情况,确保代码的鲁棒性。
下面是一些常见的链表问题和用 Go 语言编写的代码示例:
例 1:反转链表
反转链表是最基本的链表操作之一,可以用迭代或递归的方式来实现。
type ListNode struct {
Val int
Next *ListNode
}
func reverseList(head *ListNode) *ListNode {
var prev *ListNode
curr := head
for curr != nil {
nextTemp := curr.Next
curr.Next = prev
prev = curr
curr = nextTemp
}
return prev
}
例 2:检测链表中是否有环
使用快慢指针的方法,如果快指针最终追上慢指针,则链表中存在环。
func hasCycle(head *ListNode) bool {
if head == nil || head.Next == nil {
return false
}
slow := head
fast := head.Next
for fast != nil && fast.Next != nil {
if slow == fast {
return true
}
slow = slow.Next
fast = fast.Next.Next
}
return false
}
例 3:合并两个排序的链表
将两个已排序的链表合并为一个新的排序链表。
func mergeTwoLists(l1 *ListNode, l2 *ListNode) *ListNode {
dummy := &ListNode{}
current := dummy
for l1 != nil && l2 != nil {
if l1.Val < l2.Val {
current.Next = l1
l1 = l1.Next
} else {
current.Next = l2
l2 = l2.Next
}
current = current.Next
}
if l1 != nil {
current.Next = l1
} else {
current.Next = l2
}
return dummy.Next
}
例 4:找到链表的中间节点
使用快慢指针找到链表的中间节点。
func middleNode(head *ListNode) *ListNode {
slow := head
fast := head
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
}
return slow
}
在解决链表问题时,重要的是要熟练掌握指针的使用,理解每个节点和指针的关系,以及在操作过程中可能出现的边界情况。练习和理解这些基本问题后,对于更复杂的链表问题也会有很大帮助。
相交链表
题目要求
编写一个函数,该函数接受两个单链表的头节点 headA 和 headB 作为输入参数。任务是找出这两个单链表相交的第一个节点。如果两个链表没有交点,则函数应返回 null。链表的相交是指从某个节点开始,它们共享所有后续节点,即从这一点开始,两个链表合并为一个。
解题思路
要解决这个问题,我们可以采用以下步骤:
-
双指针法:
- 初始化两个指针,分别指向两个链表的头节点 headA 和 headB。
- 同时遍历两个链表,当一个指针到达链表末尾时,将其重定向到另一个链表的头节点继续遍历。
- 如果两个链表相交,那么这两个指针最终会在相交节点处相遇。这是因为重定向指针后,两个指针到达相交节点的路径长度相同。
- 如果两个链表不相交,那么这两个指针最终都会同时到达各自链表的末尾(null),这时返回 null。
-
长度差法:
- 首先遍历两个链表,得到它们的长度和长度差。
- 然后将较长链表的指针向前移动长度差个节点。
- 接着同时遍历两个链表,比较当前指针指向的节点是否相同。
- 如果找到相同的节点,则该节点为相交的起始节点。
- 如果直到链表末尾都没有找到相同的节点,则返回 null。
-
哈希表法:
- 遍历第一个链表,将所有节点存储在哈希表中。
- 然后遍历第二个链表,检查每个节点是否已存在于哈希表中。
- 如果存在,则该节点为两个链表相交的起始节点。
- 如果遍历完第二个链表都没有找到存在于哈希表中的节点,则返回 null。
在实际应用中,双指针法因为其空间复杂度较低(O(1))和简洁性通常是首选方法。长度差法和哈希表法的空间复杂度分别为 O(1)和 O(N),其中 N 是链表节点的数量。
Golang 版本
package main
import (
"fmt"
)
// ListNode is a definition for singly-linked list.
type ListNode struct {
Val int
Next *ListNode
}
// getIntersectionNode finds the node at which the intersection of two singly linked lists begins.
func getIntersectionNode(headA, headB *ListNode) *ListNode {
if headA == nil || headB == nil {
return nil
}
// Create two pointers for each list.
pA := headA
pB := headB
// Continue until they either meet or end.
for pA != pB {
// If you reached the end of one list, go to the beginning of the other one.
// You only switch once for each pointer.
if pA == nil {
pA = headB
} else {
pA = pA.Next
}
if pB == nil {
pB = headA
} else {
pB = pB.Next
}
}
// If pA and pB meet, pA/pB is the intersection node.
return pA
}
func main() {
// Example usage:
// The linked lists setup and intersection node creation should be here.
// This is just a function usage example assuming 'headA' and 'headB' are already defined and intersect.
intersectionNode := getIntersectionNode(headA, headB)
fmt.Println("Intersection node:", intersectionNode)
}
请注意,这段代码假设headA
和headB
是已经定义好的变量,并且它们代表的链表可能在某个节点相交。在实际使用中,你需要根据具体情况来创建这些链表和它们的交点。这段代码只展示了getIntersectionNode
函数的实现和如何调用它。
C++版本
#include <iostream>
// Definition for singly-linked list.
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if (headA == NULL || headB == NULL) return NULL;
ListNode *pA = headA;
ListNode *pB = headB;
// After switching, both pointers will travel the same distance
while (pA != pB) {
// Switch to the head of the other list upon reaching the end
pA = pA == NULL ? headB : pA->next;
pB = pB == NULL ? headA : pB->next;
}
// If pA and pB meet, pA/pB is the intersection node, otherwise NULL
return pA;
}
};
int main() {
// Example usage:
// The linked lists setup and intersection node creation should be here.
// This is just a function usage example assuming 'headA' and 'headB' are already defined and intersect.
Solution solution;
ListNode *intersectionNode = solution.getIntersectionNode(headA, headB);
std::cout << "Intersection node: " << (intersectionNode ? intersectionNode->val : -1) << std::endl;
return 0;
}
这段代码提供了一个Solution
类,其中包含了getIntersectionNode
函数的实现。这个函数使用了双指针法来找出两个链表的交点。在main
函数中,你需要根据实际情况来创建链表和它们的交点,然后创建Solution
类的实例并调用getIntersectionNode
函数。这里的代码只是一个调用示例,假设headA
和headB
是已经定义好的变量,并且它们代表的链表可能在某个节点相交。
Python 版本
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
if not headA or not headB:
return None
# Set two pointers to each head
pA, pB = headA, headB
# Iterate through the lists
while pA is not pB:
# If either pointer hits the end, switch head and continue the second traversal,
# if not intersect, the second traversal will hit the end at the same time and return None
pA = pB if pA is None else pA.next
pB = pA if pB is None else pB.next
# If pA and pB meet, pA/pB is the intersection node, otherwise None
return pA
# Example usage:
# Assuming that 'headA' and 'headB' are the heads of the two linked lists which are already defined.
# solution = Solution()
# intersection_node = solution.getIntersectionNode(headA, headB)
# print(intersection_node.val if intersection_node else 'No intersection')
这段代码定义了一个ListNode
类用于表示链表节点,以及一个Solution
类,其中包含了getIntersectionNode
方法。这个方法使用了双指针技巧来找出两个链表的交点。在实际使用中,你需要创建两个链表的实例并且可能需要创建它们的交点,然后创建Solution
类的实例并调用getIntersectionNode
方法。这里的代码只是一个调用示例,假设headA
和headB
是已经定义好的变量,并且它们代表的链表可能在某个节点相交。
Java 版本
public class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) return null;
ListNode pA = headA;
ListNode pB = headB;
// After both pointers have traversed through their respective lists,
// they will have covered the same distance when they meet at the intersection point.
while (pA != pB) {
// If either pointer reaches the end, switch to the head of the other list.
// If there is no intersection, both will eventually become null.
pA = (pA == null) ? headB : pA.next;
pB = (pB == null) ? headA : pB.next;
}
// If pA and pB meet, pA/pB is the intersection node, otherwise null.
return pA;
}
}
// Example usage:
// Assuming that 'headA' and 'headB' are the heads of the two linked lists which are already defined.
// Solution solution = new Solution();
// ListNode intersectionNode = solution.getIntersectionNode(headA, headB);
// System.out.println(intersectionNode != null ? intersectionNode.val : "No intersection");
这段代码定义了一个ListNode
类用于表示链表节点,以及一个Solution
类,其中包含了getIntersectionNode
方法。这个方法使用了双指针技巧来找出两个链表的交点。在实际使用中,你需要创建两个链表的实例并且可能需要创建它们的交点,然后创建Solution
类的实例并调用getIntersectionNode
方法。这里的代码只是一个调用示例,假设headA
和headB
是已经定义好的变量,并且它们代表的链表可能在某个节点相交。
Rust 版本
#![allow(unused)] fn main() { // Definition for singly-linked list. #[derive(PartialEq, Eq, Clone, Debug)] pub struct ListNode { pub val: i32, pub next: Option<Box<ListNode>>, } impl ListNode { #[inline] fn new(val: i32) -> Self { ListNode { next: None, val } } } pub struct Solution; impl Solution { pub fn get_intersection_node( head_a: Option<Box<ListNode>>, head_b: Option<Box<ListNode>>, ) -> Option<Box<ListNode>> { // Convert the Option<Box<ListNode>> to raw pointers for comparison let mut p_a: *const ListNode = head_a.as_ref().map_or(std::ptr::null(), |n| &**n); let mut p_b: *const ListNode = head_b.as_ref().map_or(std::ptr::null(), |n| &**n); // We'll need to keep the original heads to reset the pointers after the first pass let mut head_a = head_a; let mut head_b = head_b; // If either pointer is null (i.e., the list is empty), there's no intersection if p_a.is_null() || p_b.is_null() { return None; } while p_a != p_b { // Move to the next node, or switch to the other list's head after reaching the end p_a = if let Some(node) = unsafe { p_a.as_ref() } { node.next.as_ref().map_or(std::ptr::null(), |n| &**n) } else { head_b.as_ref().map_or(std::ptr::null(), |n| &**n) }; p_b = if let Some(node) = unsafe { p_b.as_ref() } { node.next.as_ref().map_or(std::ptr::null(), |n| &**n) } else { head_a.as_ref().map_or(std::ptr::null(), |n| &**n) }; } // Once the pointers meet, return the intersection node as Option<Box<ListNode>> unsafe { p_a.as_ref() }.map(|node| Box::new(ListNode::new(node.val))) } } // Example usage: // Assuming that 'head_a' and 'head_b' are the heads of the two linked lists which are already defined. // let intersection_node = Solution::get_intersection_node(head_a, head_b); // println!("{:?}", intersection_node); }
这段代码定义了一个ListNode
结构体用于表示链表节点,以及一个Solution
结构体,其中包含了get_intersection_node
方法。这个方法使用了双指针技巧来找出两个链表的交点。在实际使用中,你需要创建两个链表的实例并且可能需要创建它们的交点,然后调用Solution::get_intersection_node
方法。这里的代码只是一个调用示例,假设head_a
和head_b
是已经定义好的变量,并且它们代表的链表可能在某个节点相交。
请注意,Rust 的安全性原则通常不推荐使用裸指针和unsafe
代码块。然而,在这个特定的例子中,为了模拟其他语言中的指针比较操作,我们不得不使用它们。在实际的 Rust 生产代码中,应该尽量避免这样的做法,而是寻找更安全的替代方案。
总结
上面给出了使用双指针法来解决两个单链表相交问题的代码示例。这种方法是一种高效的解决方案,不需要额外的空间,且时间复杂度为 O(m+n),其中 m 和 n 分别是两个链表的长度。
双指针法的基本思路是同时遍历两个链表,当一个指针到达链表末尾时,将其重定向到另一个链表的头节点继续遍历。如果两个链表相交,那么这两个指针最终会在相交节点处相遇。如果两个链表不相交,那么这两个指针最终都会同时到达各自链表的末尾(null)。
除了双指针法,还介绍了长度差法和哈希表法。长度差法首先遍历两个链表,得到它们的长度和长度差,然后将较长链表的指针向前移动长度差个节点,接着同时遍历两个链表,比较当前指针指向的节点是否相同。哈希表法则是遍历第一个链表,将所有节点存储在哈希表中,然后遍历第二个链表,检查每个节点是否已存在于哈希表中。
这些方法都可以有效地找出两个单链表的相交节点,选择合适的方法取决于具体的应用场景和需求。
反转链表
题目要求
你需要编写一个函数,该函数接收一个单链表的头节点 head
作为参数。你的任务是反转这个单链表,即改变链表中元素的顺序,使原本链表中的第一个元素变成最后一个元素,第二个元素变成倒数第二个元素,以此类推。最终,你需要返回新的头节点,即原链表的最后一个节点,这样我们就可以通过这个新的头节点遍历整个被反转的链表。
解题思路
反转链表是链表操作中的一个经典问题,可以通过迭代或递归的方式来解决。这里我们讨论迭代的方法,因为它的空间复杂度较低(O(1)),适合面试或考试时使用。
-
初始化三个指针:
prev
、curr
和next
。其中,prev
初始化为null
,curr
初始化为头节点head
,next
用于临时存储curr
的下一个节点。 -
遍历链表:当
curr
不为null
时,循环继续。在每次循环中,首先记录curr
的下一个节点到next
(因为后面要改变curr
的next
指针)。 -
反转指针:将
curr
的next
指针指向prev
。这一步实际上完成了当前节点的反转。 -
移动指针:将
prev
和curr
指针都向前移动一位。具体来说,prev
移动到curr
,curr
移动到next
。 -
循环结束:当
curr
为null
时,意味着我们已经处理完所有的节点,此时prev
指向的就是反转后的链表的头节点。 -
返回新的头节点:最后返回
prev
,因为当循环结束时,curr
是null
,而prev
此时指向的是原链表的最后一个节点,也就是反转后链表的头节点。
注意,这个过程中我们没有创建任何新的链表节点,只是改变了现有节点的指向,因此空间复杂度保持在 O(1),时间复杂度为 O(n),其中 n 是链表的长度。
Golang 版本
// ListNode is the definition for singly-linked list.
type ListNode struct {
Val int
Next *ListNode
}
// reverseList takes the head of a singly-linked list and returns the head of the reversed list.
func reverseList(head *ListNode) *ListNode {
var prev *ListNode
curr := head
for curr != nil {
nextTemp := curr.Next // temporarily store the next node
curr.Next = prev // reverse the current node's pointer
prev = curr // move prev and curr one step forward
curr = nextTemp
}
return prev // prev is the new head at the end of the loop
}
这段代码定义了一个 ListNode
结构体,用于表示链表节点。reverseList
函数接收一个链表的头节点 head
,并返回反转后链表的头节点。函数内部通过一个循环,逐个改变节点的 Next
指针,最终达到反转链表的目的。
C++版本
// Definition for singly-linked list.
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(nullptr) {}
};
// Function to reverse a singly-linked list.
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr;
ListNode* curr = head;
ListNode* next = nullptr;
while (curr != nullptr) {
next = curr->next; // temporarily store the next node
curr->next = prev; // reverse the current node's pointer
prev = curr; // move prev and curr one step forward
curr = next;
}
return prev; // prev is the new head at the end of the loop
}
这段代码提供了一个 C++版本的单链表反转函数。ListNode
结构体用于定义链表节点,包含一个整数值 val
和一个指向下一个节点的指针 next
。reverseList
函数接收链表的头节点 head
并返回反转后的链表的头节点。函数通过一个循环来逐个改变节点的 next
指针,从而实现链表的反转。
Python 版本
# Definition for singly-linked list.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# Function to reverse a singly-linked list.
def reverseList(head: ListNode) -> ListNode:
prev = None
curr = head
while curr:
next_temp = curr.next # temporarily store the next node
curr.next = prev # reverse the current node's pointer
prev = curr # move prev and curr one step forward
curr = next_temp
return prev # prev is the new head at the end of the loop
这段代码提供了一个 Python 版本的单链表反转函数。ListNode
类用于定义链表节点,包含一个整数值 val
和一个指向下一个节点的指针 next
。reverseList
函数接收链表的头节点 head
并返回反转后的链表的头节点。函数通过一个循环来逐个改变节点的 next
指针,从而实现链表的反转。
Java 版本
// Definition for singly-linked list.
class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
// Function to reverse a singly-linked list.
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
ListNode next = null;
while (curr != null) {
next = curr.next; // temporarily store the next node
curr.next = prev; // reverse the current node's pointer
prev = curr; // move prev and curr one step forward
curr = next;
}
return prev; // prev is the new head at the end of the loop
}
这段代码提供了一个 Java 版本的单链表反转函数。ListNode
类用于定义链表节点,包含一个整数值 val
和一个指向下一个节点的指针 next
。reverseList
方法接收链表的头节点 head
并返回反转后的链表的头节点。方法通过一个循环来逐个改变节点的 next
指针,从而实现链表的反转。
Rust 版本
#![allow(unused)] fn main() { // Definition for singly-linked list. #[derive(PartialEq, Eq, Clone, Debug)] pub struct ListNode { pub val: i32, pub next: Option<Box<ListNode>>, } impl ListNode { #[inline] fn new(val: i32) -> Self { ListNode { next: None, val } } } // Function to reverse a singly-linked list. pub fn reverse_list(mut head: Option<Box<ListNode>>) -> Option<Box<ListNode>> { let mut prev = None; let mut current = head; while let Some(mut node) = current { let next = node.next.take(); // temporarily store the next node node.next = prev; // reverse the current node's pointer prev = Some(node); // move prev and current one step forward current = next; } prev // prev is the new head at the end of the loop } }
这段代码提供了一个 Rust 版本的单链表反转函数。ListNode
结构体用于定义链表节点,包含一个整数值 val
和一个指向下一个节点的 Option<Box<ListNode>>
类型的 next
。reverse_list
函数接收链表的头节点 head
(一个 Option<Box<ListNode>>
类型)并返回反转后的链表的头节点。函数通过一个循环来逐个改变节点的 next
指针,从而实现链表的反转。
总结
上面给出了使用不同编程语言实现的单链表反转算法。无论是使用 Golang、C++、Python、Java 还是 Rust,算法的核心思想都是一致的:通过迭代遍历链表,逐个改变节点的指针指向,最终实现链表的反转。这个算法的时间复杂度为 O(n),其中 n 是链表的长度,空间复杂度为 O(1),因为只使用了有限的额外空间。这使得这个算法在面试或实际应用中都是一个高效且常用的链表操作。
回文链表
题目要求
你需要编写一个算法来判断一个单链表是否为回文结构。所谓回文链表,指的是这个链表的元素从前往后读和从后往前读是完全相同的。例如,链表 1->2->2->1 是回文的,而链表 1->2->3 不是。
解题思路
要判断一个链表是否为回文,我们可以采取以下步骤:
-
找到中点:首先,我们需要找到链表的中点。这可以通过快慢指针的方法来实现。快指针每次移动两步,慢指针每次移动一步,当快指针到达链表末尾时,慢指针就位于链表的中点。
-
链表后半部分反转:当我们找到链表的中点后,将链表的后半部分进行反转。这样我们就可以很容易地从中点向两边比较,判断是否为回文。
-
比较前后两半:从中点开始,将前半部分和反转后的后半部分进行元素比较。如果所有元素都相同,则链表是回文的。否则,不是。
-
恢复链表(可选):如果需要保持链表的原始结构,可以再次反转链表的后半部分,恢复链表的原始结构。
-
返回结果:根据比较的结果,返回 true 或 false。
这个方法的时间复杂度是 O(n),空间复杂度是 O(1),因为我们只需要改变一些指针的指向,而不需要分配任何额外的存储空间来存储链表的值。
Golang 版本
type ListNode struct {
Val int
Next *ListNode
}
func isPalindrome(head *ListNode) bool {
if head == nil || head.Next == nil {
return true
}
// 快慢指针寻找中点
slow, fast := head, head
for fast.Next != nil && fast.Next.Next != nil {
slow = slow.Next
fast = fast.Next.Next
}
// 反转后半部分链表
slow.Next = reverseList(slow.Next)
// 保存中点之后的链表,用于最后恢复链表
secondHalfStart := slow.Next
// 比较前半部分和反转后的后半部分
p1 := head
p2 := slow.Next
result := true
for result && p2 != nil {
if p1.Val != p2.Val {
result = false
}
p1 = p1.Next
p2 = p2.Next
}
// 恢复链表(如果需要)
slow.Next = reverseList(secondHalfStart)
return result
}
// 反转链表函数
func reverseList(head *ListNode) *ListNode {
var prev *ListNode
curr := head
for curr != nil {
nextTemp := curr.Next
curr.Next = prev
prev = curr
curr = nextTemp
}
return prev
}
这段代码定义了一个ListNode
结构体,实现了一个isPalindrome
函数来检查链表是否为回文。它首先使用快慢指针找到链表的中点,然后反转中点之后的链表部分,接着比较前半部分和反转后的后半部分是否相同。最后,它还包含了一个可选的步骤来恢复链表的原始结构。
C++版本
#include <iostream>
// 定义链表节点结构体
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
class Solution {
public:
bool isPalindrome(ListNode* head) {
if (head == nullptr || head->next == nullptr) return true;
// 快慢指针找到中点
ListNode *slow = head, *fast = head;
while (fast->next != nullptr && fast->next->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
}
// 反转后半部分链表
slow->next = reverseList(slow->next);
ListNode* secondHalfStart = slow->next;
// 比较前半部分和反转后的后半部分
ListNode *p1 = head;
ListNode *p2 = secondHalfStart;
bool result = true;
while (result && p2 != nullptr) {
if (p1->val != p2->val) result = false;
p1 = p1->next;
p2 = p2->next;
}
// 恢复链表(如果需要)
slow->next = reverseList(secondHalfStart);
return result;
}
private:
ListNode* reverseList(ListNode* head) {
ListNode *prev = nullptr, *curr = head, *next = nullptr;
while (curr != nullptr) {
next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
return prev;
}
};
这段 C++代码定义了一个ListNode
结构体和一个Solution
类。Solution
类中的isPalindrome
成员函数用于检查链表是否为回文。它首先使用快慢指针技术找到链表的中点,然后反转中点之后的链表部分,接着比较前半部分和反转后的后半部分是否相同。最后,它还包含了一个可选的步骤来恢复链表的原始结构。reverseList
是一个私有成员函数,用于反转链表。
Python 版本
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def isPalindrome(self, head: ListNode) -> bool:
if not head or not head.next:
return True
# 快慢指针找到中点
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# 反转后半部分链表
prev = None
while slow:
tmp = slow.next
slow.next = prev
prev = slow
slow = tmp
# 比较前半部分和反转后的后半部分
left, right = head, prev
while right: # 只需比较后半部分
if left.val != right.val:
return False
left, right = left.next, right.next
return True
这段 Python 代码定义了一个ListNode
类用于表示链表节点,以及一个Solution
类,其中包含了一个isPalindrome
方法用于判断链表是否为回文。该方法首先使用快慢指针找到链表的中点,然后反转中点之后的链表部分,最后比较前半部分和反转后的后半部分是否相同。如果相同,则链表是回文的,返回True
;否则,返回False
。
Java 版本
public class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
class Solution {
public boolean isPalindrome(ListNode head) {
if (head == null || head.next == null) return true;
// 快慢指针找到中点
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// 反转后半部分链表
slow = reverseList(slow);
fast = head;
// 比较前半部分和反转后的后半部分
while (slow != null) {
if (fast.val != slow.val) {
return false;
}
fast = fast.next;
slow = slow.next;
}
return true;
}
private ListNode reverseList(ListNode head) {
ListNode prev = null;
while (head != null) {
ListNode next = head.next;
head.next = prev;
prev = head;
head = next;
}
return prev;
}
}
这段 Java 代码定义了一个ListNode
类用于表示链表节点,以及一个Solution
类,其中包含了一个isPalindrome
方法用于判断链表是否为回文。该方法首先使用快慢指针找到链表的中点,然后反转中点之后的链表部分,最后比较前半部分和反转后的后半部分是否相同。如果相同,则链表是回文的,返回true
;否则,返回false
。reverseList
是一个辅助方法,用于反转链表。
Rust 版本
#![allow(unused)] fn main() { // Definition for singly-linked list. #[derive(PartialEq, Eq, Clone, Debug)] pub struct ListNode { pub val: i32, pub next: Option<Box<ListNode>>, } impl ListNode { #[inline] fn new(val: i32) -> Self { ListNode { next: None, val, } } } pub struct Solution; impl Solution { pub fn is_palindrome(head: Option<Box<ListNode>>) -> bool { let mut values = Vec::new(); let mut current = head.as_ref(); while let Some(node) = current { values.push(node.val); current = node.next.as_ref(); } let mut front = 0; let mut back = values.len() - 1; while front < back { if values[front] != values[back] { return false; } front += 1; back -= 1; } true } } }
这段 Rust 代码定义了一个ListNode
结构体用于表示链表节点,并实现了一个Solution
结构体,其中包含了一个is_palindrome
函数用于判断链表是否为回文。该函数首先将链表的值复制到一个向量中,然后使用双指针法在向量上从两端向中间遍历,比较对应的值是否相等。如果所有对应的值都相等,则链表是回文的,函数返回true
;否则,返回false
。
总结
上面给出的解法是用不同编程语言实现的判断链表是否为回文的算法。这些解法的共同思路是:
- 使用快慢指针找到链表的中点。
- 反转链表的后半部分。
- 比较前半部分和反转后的后半部分是否相同。
这些解法在实现细节上有所不同,比如在反转链表部分的实现方式上,以及在处理链表节点的方式上。但它们的核心思想是一致的,都是基于快慢指针和链表反转来判断链表是否为回文。
环形链表
题目要求
这个问题要求我们检测一个单向链表是否包含一个环。一个链表环指的是链表中的一个节点的next
指针指向的是链表中在它之前的某个节点,这样就形成了一个环。这意味着如果我们从头节点开始遍历链表,我们会因为环的存在而无法到达链表的尾部(因为它没有真正的尾部)。我们的任务是要编写一个算法,来检测链表是否包含这样的环。
解题思路
要解决这个问题,有几种不同的算法可以使用。以下是两种常见的方法:
-
哈希表法:
- 遍历链表的每个节点。
- 对于每个节点,检查它是否已经在一个哈希表(或者任何其他类型的数据结构)中。
- 如果当前节点已经出现在哈希表中,那么链表包含一个环。
- 如果遍历完整个链表都没有发现重复的节点,那么链表不包含环。
- 这种方法的时间复杂度是 O(n),空间复杂度也是 O(n),因为我们需要存储每个节点来检查是否有重复。
-
快慢指针法(Floyd 的循环检测算法):
- 使用两个指针,一个快指针和一个慢指针,初始时都指向头节点。
- 快指针每次移动两个节点,慢指针每次移动一个节点。
- 如果链表不包含环,快指针将到达链表的尾部并且遍历结束。
- 如果链表包含环,快指针最终会追上慢指针(因为每次循环它都比慢指针多走一步)。
- 当快指针与慢指针相遇时,我们可以确定链表包含一个环。
- 这种方法的时间复杂度是 O(n),但空间复杂度优于哈希表法,为 O(1),因为它不需要额外的存储空间。
通常,快慢指针法更受欢迎,因为它不需要额外的存储空间,而且实现起来相对简单。
Golang 版本
package main
import "fmt"
// ListNode is the definition for singly-linked list.
type ListNode struct {
Val int
Next *ListNode
}
// hasCycle uses Floyd's Tortoise and Hare algorithm to detect a cycle.
func hasCycle(head *ListNode) bool {
if head == nil || head.Next == nil {
return false
}
slow := head
fast := head.Next
for fast != nil && fast.Next != nil {
if slow == fast {
return true
}
slow = slow.Next
fast = fast.Next.Next
}
return false
}
func main() {
// Example usage:
// Create a list with a cycle for testing purposes.
// 1 -> 2 -> 3 -> 4 -> 2 ...
node4 := &ListNode{Val: 4}
node3 := &ListNode{Val: 3, Next: node4}
node2 := &ListNode{Val: 2, Next: node3}
node1 := &ListNode{Val: 1, Next: node2}
node4.Next = node2 // Create a cycle here
fmt.Println(hasCycle(node1)) // Should return true
}
这段代码定义了一个ListNode
结构体,代表链表的节点,并实现了hasCycle
函数来检测链表是否有环。hasCycle
函数使用了快慢指针法。在main
函数中,我们创建了一个带有环的链表作为示例,并调用hasCycle
函数来检测环。
C++版本
#include <iostream>
// Definition for singly-linked list.
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(nullptr) {}
};
class Solution {
public:
bool hasCycle(ListNode *head) {
if (head == nullptr || head->next == nullptr) {
return false;
}
ListNode *slow = head;
ListNode *fast = head->next;
while (fast != nullptr && fast->next != nullptr) {
if (slow == fast) {
return true;
}
slow = slow->next;
fast = fast->next->next;
}
return false;
}
};
int main() {
// Example usage:
// Create a list with a cycle for testing purposes.
// 1 -> 2 -> 3 -> 4 -> 2 ...
ListNode *node1 = new ListNode(1);
ListNode *node2 = new ListNode(2);
ListNode *node3 = new ListNode(3);
ListNode *node4 = new ListNode(4);
node1->next = node2;
node2->next = node3;
node3->next = node4;
node4->next = node2; // Create a cycle here
Solution solution;
std::cout << "Does the linked list have a cycle? "
<< (solution.hasCycle(node1) ? "Yes" : "No") << std::endl;
// Remember to avoid memory leak in production code.
// Here we would need to properly delete nodes, especially since we have a cycle.
// For this example, we'll skip this part.
return 0;
}
这段 C++代码定义了一个ListNode
结构体,代表链表的节点,并实现了一个Solution
类,其中包含了hasCycle
函数来检测链表是否有环。hasCycle
函数使用了快慢指针法。在main
函数中,我们创建了一个带有环的链表作为示例,并调用hasCycle
函数来检测环。最后,输出检测结果。
请注意,在实际的生产代码中,你需要确保没有内存泄漏。在这个例子中,由于链表中存在环,如果你想删除所有节点,你需要特别小心。这里为了简单起见,我们没有包含删除节点的代码。
Python 版本
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def hasCycle(self, head):
if not head or not head.next:
return False
slow = head
fast = head.next
while fast and fast.next:
if slow == fast:
return True
slow = slow.next
fast = fast.next.next
return False
# Example usage:
# Create a list with a cycle for testing purposes.
# 1 -> 2 -> 3 -> 4 -> 2 ...
node1 = ListNode(1)
node2 = ListNode(2)
node3 = ListNode(3)
node4 = ListNode(4)
node1.next = node2
node2.next = node3
node3.next = node4
node4.next = node2 # Create a cycle here
solution = Solution()
print("Does the linked list have a cycle?", solution.hasCycle(node1)) # Should return True
这段 Python 代码定义了一个ListNode
类,代表链表的节点,并实现了一个Solution
类,其中包含了hasCycle
方法来检测链表是否有环。hasCycle
方法使用了快慢指针法。在示例使用中,我们创建了一个带有环的链表,并调用hasCycle
方法来检测环,然后打印出检测结果。
Java 版本
public class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false;
}
ListNode slow = head;
ListNode fast = head.next;
while (fast != null && fast.next != null) {
if (slow == fast) {
return true;
}
slow = slow.next;
fast = fast.next.next;
}
return false;
}
}
public class Main {
public static void main(String[] args) {
// Example usage:
// Create a list with a cycle for testing purposes.
// 1 -> 2 -> 3 -> 4 -> 2 ...
ListNode node1 = new ListNode(1);
ListNode node2 = new ListNode(2);
ListNode node3 = new ListNode(3);
ListNode node4 = new ListNode(4);
node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node2; // Create a cycle here
Solution solution = new Solution();
System.out.println("Does the linked list have a cycle? " + solution.hasCycle(node1)); // Should return true
}
}
这段 Java 代码定义了一个ListNode
类,代表链表的节点,并实现了一个Solution
类,其中包含了hasCycle
方法来检测链表是否有环。hasCycle
方法使用了快慢指针法。在Main
类的main
方法中,我们创建了一个带有环的链表作为示例,并调用hasCycle
方法来检测环,然后打印出检测结果。
Rust 版本
use std::rc::Rc; use std::cell::RefCell; // Definition for singly-linked list. #[derive(Clone)] struct ListNode { val: i32, next: Option<Rc<RefCell<ListNode>>>, } impl ListNode { fn new(val: i32) -> Self { ListNode { val, next: None } } } struct Solution; impl Solution { pub fn has_cycle(head: Option<Rc<RefCell<ListNode>>>) -> bool { let mut slow = head.clone(); let mut fast = head; while let Some(fast_node) = fast { if let Some(next_fast) = fast_node.borrow().next.clone() { fast = next_fast.borrow().next.clone(); slow = slow.unwrap().borrow().next.clone(); if Rc::ptr_eq(&slow.as_ref().unwrap(), &fast.as_ref().unwrap()) { return true; } } else { break; } } false } } fn main() { // Example usage: // Create a list with a cycle for testing purposes. // 1 -> 2 -> 3 -> 4 -> 2 ... let node4 = Rc::new(RefCell::new(ListNode::new(4))); let node3 = Rc::new(RefCell::new(ListNode::new(3))); let node2 = Rc::new(RefCell::new(ListNode::new(2))); let node1 = Rc::new(RefCell::new(ListNode::new(1))); node1.borrow_mut().next = Some(node2.clone()); node2.borrow_mut().next = Some(node3.clone()); node3.borrow_mut().next = Some(node4.clone()); node4.borrow_mut().next = Some(node2.clone()); // Create a cycle here println!("Does the linked list have a cycle? {}", Solution::has_cycle(Some(node1))); // Should return true }
这段 Rust 代码定义了一个ListNode
结构体,代表链表的节点,并实现了一个Solution
结构体,其中包含了has_cycle
函数来检测链表是否有环。has_cycle
函数使用了快慢指针法。在main
函数中,我们创建了一个带有环的链表作为示例,并调用has_cycle
函数来检测环,然后打印出检测结果。
Rust 中的链表实现通常涉及到智能指针和借用检查器,这里使用了Rc
和RefCell
来允许节点被多个所有者共享,并在运行时借用其可变性。这是因为在 Rust 中,我们通常不能在编译时确定链表的生命周期和所有权模式。
总结
上面给出了使用不同编程语言实现的检测链表是否包含环的算法。这些算法主要包括了哈希表法和快慢指针法两种解决方案。
- 哈希表法:遍历链表的每个节点,检查每个节点是否已经在哈希表中。这种方法的时间复杂度是 O(n),空间复杂度也是 O(n)。
- 快慢指针法:使用两个指针,一个快指针和一个慢指针,通过不同的移动速度来检测环的存在。这种方法的时间复杂度是 O(n),但空间复杂度优于哈希表法,为 O(1)。
这些算法可以根据具体的编程语言特性进行实现。在实际应用中,快慢指针法通常更受欢迎,因为它不需要额外的存储空间,而且实现起来相对简单。
环形链表 II
题目要求
这个问题要求我们确定一个给定链表是否有环,如果有环,需要返回环的起始节点。如果没有环,则返回 null。链表的环是指链表中的一个节点的 next 指针指向链表中的一个先前的节点,这样就形成了一个环。题目中提到的pos
变量是用来表示链表中环的起始位置的索引,但这个值在实际的函数实现中并不会给出,它只是用来说明问题。
解题思路
解决这个问题的一个经典方法是使用快慢指针(Floyd 的循环检测算法)。以下是解题的步骤:
-
初始化两个指针,快指针
fast
和慢指针slow
,它们都指向链表的头节点head
。 -
移动这两个指针,
slow
指针每次移动一个节点,fast
指针每次移动两个节点。如果链表中没有环,那么fast
指针最终会遇到 null,这时我们可以返回 null。 -
如果链表中有环,那么
fast
指针最终会追上slow
指针(因为每次循环fast
比slow
多走一步,如果有环它们最终会在环内相遇)。 -
当
fast
和slow
相遇时,将fast
(或slow
)指针重新指向头节点head
,然后fast
和slow
都每次移动一个节点,当它们再次相遇时,相遇点就是环的起始节点。 -
返回相遇点,即为环的起始节点。
这个算法的关键在于第 4 步。当两个指针在环内相遇后,我们为什么要将其中一个指针重新指向头节点,然后两个指针都以相同的速度移动?这是因为从头节点到环起点的距离和从相遇点到环起点的距离是相等的。这个结论可以通过数学证明得到,是解决这类问题的关键。
设:
- 链表起点到环起点的距离为 \( D \)。
- 环起点到快慢指针相遇点的距离为 \( S_1 \)。
- 相遇点回到环起点的距离为 \( S_2 \)。
当快慢指针在环内相遇时:
- 慢指针走过的总距离为 \( D + S_1 \)。
- 快指针走过的总距离为 \( D + S_1 + S_2 + S_1 \),因为它已经在环里至少走了一圈才遇到慢指针。
由于快指针的速度是慢指针的两倍,我们可以得出以下等式:
\[ 2(D + S_1) = D + 2S_1 + S_2 \]
通过简化上述等式,我们得到:
\[ 2D + 2S_1 = D + 2S_1 + S_2 \]
\[ 2D + 2S_1 - 2S_1 = D + S_2 \]
\[ 2D = D + S_2 \]
从而:
\[ D = S_2 \]
这意味着从链表起点到环起点的距离 \( D \) 等于从相遇点回到环起点的距离 \( S_2 \)。
因此,如果我们将一个指针重新放回链表的起始点,而另一个指针保持在相遇点,然后两个指针都以相同的速度前进,那么:
- 当第一个指针从链表起点走到环起点时,它走过了距离 \( D \)。
- 同时,第二个指针从相遇点出发,也走过了距离 \( S_2 \)。
由于 \( D = S_2 \),两个指针将会在环的起点相遇。这就证明了当两个指针再次相遇时,它们一定是在环的起始点相遇。
Golang 版本
type ListNode struct {
Val int
Next *ListNode
}
func detectCycle(head *ListNode) *ListNode {
if head == nil || head.Next == nil {
return nil
}
slow := head
fast := head
hasCycle := false
// 快慢指针判断是否有环
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
if slow == fast {
hasCycle = true
break
}
}
// 如果有环,找到环的入口
if hasCycle {
slow = head
for slow != fast {
slow = slow.Next
fast = fast.Next
}
return slow
}
return nil
}
这段代码定义了一个ListNode
结构体,用于表示链表节点。detectCycle
函数用于检测链表中是否存在环,并返回环的起始节点。如果不存在环,则返回nil
。函数中首先对链表进行了空检查,然后使用快慢指针的方法来检测环,并在确认链表中存在环后找到并返回环的起始节点。
C++版本
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(nullptr) {}
};
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
if (head == nullptr || head->next == nullptr) {
return nullptr;
}
ListNode *slow = head;
ListNode *fast = head;
bool hasCycle = false;
// 使用快慢指针检测环
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
hasCycle = true;
break;
}
}
// 如果存在环,找到环的入口
if (hasCycle) {
slow = head;
while (slow != fast) {
slow = slow->next;
fast = fast->next;
}
return slow;
}
return nullptr;
}
};
这段 C++代码定义了一个ListNode
结构体和一个Solution
类。Solution
类中的detectCycle
成员函数用于检测链表中是否存在环,并返回环的起始节点。如果不存在环,则返回nullptr
。函数中首先对链表进行了空检查,然后使用快慢指针的方法来检测环,并在确认链表中存在环后找到并返回环的起始节点。
Python 版本
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def detectCycle(self, head: ListNode) -> ListNode:
slow = fast = head
has_cycle = False
# 使用快慢指针检测环
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
has_cycle = True
break
# 如果存在环,找到环的入口
if has_cycle:
slow = head
while slow != fast:
slow = slow.next
fast = fast.next
return slow
return None
这段 Python 代码定义了一个ListNode
类用于表示链表节点,以及一个Solution
类。Solution
类中的detectCycle
方法用于检测链表中是否存在环,并返回环的起始节点。如果不存在环,则返回None
。方法中首先使用快慢指针的方法来检测环,并在确认链表中存在环后找到并返回环的起始节点。
Java 版本
class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
public class Solution {
public ListNode detectCycle(ListNode head) {
if (head == null || head.next == null) {
return null;
}
ListNode slow = head;
ListNode fast = head;
boolean hasCycle = false;
// 使用快慢指针检测环
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
hasCycle = true;
break;
}
}
// 如果存在环,找到环的入口
if (hasCycle) {
slow = head;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
return null;
}
}
这段 Java 代码定义了一个ListNode
类用于表示链表节点,以及一个Solution
类。Solution
类中的detectCycle
方法用于检测链表中是否存在环,并返回环的起始节点。如果不存在环,则返回null
。方法中首先使用快慢指针的方法来检测环,并在确认链表中存在环后找到并返回环的起始节点。
Rust 版本
#![allow(unused)] fn main() { use std::rc::Rc; use std::cell::RefCell; // Definition for singly-linked list. #[derive(PartialEq, Eq, Clone, Debug)] pub struct ListNode { pub val: i32, pub next: Option<Rc<RefCell<ListNode>>>, } impl ListNode { #[inline] fn new(val: i32) -> Self { ListNode { next: None, val, } } } pub struct Solution; impl Solution { pub fn detect_cycle(head: Option<Rc<RefCell<ListNode>>>) -> Option<Rc<RefCell<ListNode>>> { let (mut slow, mut fast) = (head.clone(), head); while let Some(fast_node) = fast { slow = slow.unwrap().borrow().next.clone(); fast = fast_node.borrow().next.clone().and_then(|next| next.borrow().next.clone()); if Rc::ptr_eq(&slow.as_ref()?, &fast.as_ref()?) { let mut ptr = head; while !Rc::ptr_eq(&slow.as_ref()?, &ptr.as_ref()?) { slow = slow.unwrap().borrow().next.clone(); ptr = ptr.unwrap().borrow().next.clone(); } return ptr; } } None } } }
这段 Rust 代码定义了一个ListNode
结构体用于表示链表节点,以及一个Solution
结构体。Solution
结构体中的detect_cycle
方法用于检测链表中是否存在环,并返回环的起始节点。如果不存在环,则返回None
。方法中首先使用快慢指针的方法来检测环,并在确认链表中存在环后找到并返回环的起始节点。在 Rust 中,链表节点通常使用Rc<RefCell<T>>
来允许多个所有者和可变借用。
总结
上面给出的解法使用了快慢指针的方法来检测链表中是否存在环,并找到环的起始节点。这种方法在多种编程语言中都可以很好地实现。具体步骤包括:
- 初始化两个指针,一个慢指针和一个快指针,它们都指向链表的头节点。
- 使用快慢指针的方法来检测链表中是否存在环。如果存在环,快指针最终会追上慢指针。
- 如果存在环,将其中一个指针重新指向头节点,然后两个指针都以相同的速度移动。它们再次相遇的地方就是环的起始节点。
这种方法的时间复杂度为 O(n),空间复杂度为 O(1),是一种高效的解决链表环问题的方法。
合并两个有序链表
题目要求
合并两个已排序的链表为一个新的排序链表。具体来说,你需要按照升序将两个链表中的节点连接起来,形成一个新的链表。
解题思路
合并两个升序链表的基本思路是使用双指针技术,分别指向两个链表的头部,然后比较两个指针所指向的节点的值,将较小值的节点添加到新链表中,并移动该指针到下一个节点。重复这个过程,直到某一个链表的指针到达链表末尾。最后,将未结束的链表直接连接到新链表的末尾。
步骤如下:
- 初始化一个新的链表头节点,这将作为合并后链表的哨兵节点,以便于操作。
- 创建两个指针,分别指向两个链表的头部。
- 比较两个指针所指向的节点的值,将较小值的节点接在新链表的最后一个节点后面,并将对应的指针后移一位。
- 如果一个链表的指针先到达末尾,将另一个链表的剩余部分接在新链表后面。
- 由于第一步中创建了一个哨兵节点,最终返回哨兵节点的下一个节点作为合并后的链表的头节点。
注意事项:
- 在实际操作中,为了简化代码,通常会使用一个哨兵节点(dummy node),这样可以避免对头节点进行特殊处理。
- 在循环过程中,需要检查两个链表的指针是否已经到达链表末尾。
- 合并完成后,确保断开与原链表的连接,这样不会影响原链表的结构。
Golang 版本
type ListNode struct {
Val int
Next *ListNode
}
func mergeTwoLists(l1 *ListNode, l2 *ListNode) *ListNode {
dummy := &ListNode{} // 创建哨兵节点
current := dummy
// 遍历两个链表,直到至少一个到达末尾
for l1 != nil && l2 != nil {
if l1.Val < l2.Val {
current.Next = l1
l1 = l1.Next
} else {
current.Next = l2
l2 = l2.Next
}
current = current.Next
}
// 如果l1还有剩余节点,直接连接到新链表的末尾
if l1 != nil {
current.Next = l1
}
// 如果l2还有剩余节点,直接连接到新链表的末尾
if l2 != nil {
current.Next = l2
}
return dummy.Next // 返回哨兵节点的下一个节点,即合并后的链表的头节点
}
这段代码定义了一个ListNode
结构体,用于表示链表节点。mergeTwoLists
函数接受两个指向ListNode
的指针l1
和l2
作为参数,返回一个新的合并后的链表的头节点。函数内部使用了一个哨兵节点来简化链表操作,并通过比较节点值来决定节点的合并顺序。最后,函数处理了其中一个链表先遍历完的情况,将另一个链表剩余部分直接连接到新链表末尾。
C++版本
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
ListNode *dummy = new ListNode(0); // 创建哨兵节点
ListNode *current = dummy;
// 遍历两个链表,直到至少一个到达末尾
while (l1 != nullptr && l2 != nullptr) {
if (l1->val < l2->val) {
current->next = l1;
l1 = l1->next;
} else {
current->next = l2;
l2 = l2->next;
}
current = current->next;
}
// 如果l1还有剩余节点,直接连接到新链表的末尾
if (l1 != nullptr) {
current->next = l1;
}
// 如果l2还有剩余节点,直接连接到新链表的末尾
if (l2 != nullptr) {
current->next = l2;
}
ListNode *mergedHead = dummy->next;
delete dummy; // 删除哨兵节点
return mergedHead; // 返回合并后的链表的头节点
}
};
这段 C++代码定义了一个ListNode
结构体和一个Solution
类。Solution
类中的mergeTwoLists
方法接受两个指向ListNode
的指针l1
和l2
作为参数,并返回一个指向新合并链表头节点的指针。方法中使用了一个哨兵节点来简化链表操作,并通过循环比较节点值来决定节点的合并顺序。最后,方法处理了其中一个链表先遍历完的情况,并在返回前删除了哨兵节点以避免内存泄漏。
Python 版本
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
dummy = ListNode(0)
current = dummy
while l1 and l2:
if l1.val < l2.val:
current.next = l1
l1 = l1.next
else:
current.next = l2
l2 = l2.next
current = current.next
# 直接将未结束的链表连接到合并链表的末尾
current.next = l1 or l2
return dummy.next
这段 Python 代码定义了一个ListNode
类用于表示链表节点,以及一个Solution
类。Solution
类中的mergeTwoLists
方法接受两个ListNode
实例l1
和l2
作为参数,并返回一个新的合并后的链表的头节点。方法中使用了一个哨兵节点(dummy node)来简化链表操作,并通过循环比较节点值来决定节点的合并顺序。最后,方法处理了其中一个链表先遍历完的情况,将另一个链表剩余部分直接连接到新链表末尾。
Java 版本
public class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
ListNode current = dummy;
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
current.next = l1;
l1 = l1.next;
} else {
current.next = l2;
l2 = l2.next;
}
current = current.next;
}
// Attach the remaining elements
if (l1 != null) {
current.next = l1;
} else if (l2 != null) {
current.next = l2;
}
return dummy.next;
}
}
这段 Java 代码定义了一个ListNode
类用于表示链表节点,以及一个Solution
类。Solution
类中的mergeTwoLists
方法接受两个ListNode
对象l1
和l2
作为参数,并返回一个新的合并后的链表的头节点。方法中使用了一个哨兵节点(dummy node)来简化链表操作,并通过循环比较节点值来决定节点的合并顺序。最后,方法处理了其中一个链表先遍历完的情况,将另一个链表剩余部分直接连接到新链表末尾。
Rust 版本
#![allow(unused)] fn main() { // Definition for singly-linked list. #[derive(PartialEq, Eq, Clone, Debug)] pub struct ListNode { pub val: i32, pub next: Option<Box<ListNode>>, } impl ListNode { #[inline] fn new(val: i32) -> Self { ListNode { next: None, val, } } } impl Solution { pub fn merge_two_lists( l1: Option<Box<ListNode>>, l2: Option<Box<ListNode>>, ) -> Option<Box<ListNode>> { let mut dummy = Some(Box::new(ListNode::new(0))); let mut current = &mut dummy; let mut l1 = l1; let mut l2 = l2; while let (Some(n1), Some(n2)) = (&l1, &l2) { if n1.val < n2.val { if let Some(next) = l1 { current.as_mut().unwrap().next = Some(next); l1 = current.as_mut().unwrap().next.as_mut().unwrap().next.take(); } } else { if let Some(next) = l2 { current.as_mut().unwrap().next = Some(next); l2 = current.as_mut().unwrap().next.as_mut().unwrap().next.take(); } } current = &mut current.as_mut().unwrap().next; } if l1.is_some() { current.as_mut().unwrap().next = l1; } else if l2.is_some() { current.as_mut().unwrap().next = l2; } dummy.unwrap().next } } }
这段 Rust 代码定义了一个ListNode
结构体用于表示链表节点,并实现了一个new
函数用于创建新节点。Solution
结构体中的merge_two_lists
函数接受两个Option<Box<ListNode>>
类型的参数l1
和l2
,表示两个可能为空的链表,并返回一个合并后的链表。函数中使用了一个哨兵节点(dummy node)来简化链表操作,并通过循环比较节点值来决定节点的合并顺序。最后,函数处理了其中一个链表先遍历完的情况,将另一个链表剩余部分直接连接到新链表末尾。
总结
上面给出了使用不同编程语言实现的合并两个升序链表的解法。无论是使用哪种语言,基本的思路都是一致的:
- 创建一个哨兵节点(dummy node)来简化链表操作。
- 使用循环和条件判断来比较两个链表节点的值,将较小值的节点连接到新链表中。
- 处理其中一个链表先遍历完的情况,将另一个链表剩余部分直接连接到新链表末尾。
这些解法展示了如何在不同的编程语言中使用类、结构体和指针等不同的特性来实现相同的算法逻辑。
两数之和
题目要求
编写一个函数,该函数接收两个参数:两个非空链表的头节点。这两个链表代表两个非负整数,它们的位数以逆序方式存储,即链表的第一个节点表示数的个位,第二个节点表示数的十位,以此类推。每个节点只包含一个数字。函数的目标是将这两个数相加,并以同样的链表形式返回它们的和。
解题思路
-
初始化:创建一个新的链表,这个链表将用于存储结果。同时,设置一个变量
carry
用于存储进位,初始值为 0。 -
遍历链表:同时遍历两个链表,直到两个链表都到达尾部。对于每一对节点,执行以下操作:
- 将两个节点的值相加,并加上
carry
的值。 - 如果相加的结果大于或等于 10,则将
carry
设置为 1,并将结果减去 10;否则,将carry
设置为 0。 - 将计算的结果作为新节点添加到结果链表中。
- 将两个节点的值相加,并加上
-
处理链表长度不一:如果一个链表比另一个长,需要继续遍历较长的链表,将剩余的节点值加上
carry
,并将新节点添加到结果链表中。 -
处理最后的进位:遍历完两个链表后,如果
carry
的值为 1,则需要在结果链表的末尾添加一个值为 1 的节点。 -
返回结果:返回新创建的链表的头节点,这个链表就是两个输入链表所代表的数的和。
注意:在实现过程中,需要考虑几个边界情况,例如一个链表为空,或者两个链表的长度不同。此外,当两个链表都遍历完毕后,还需要检查carry
是否为 1,如果是,还需要在结果链表的末尾添加一个节点。
Golang 版本
// ListNode 是链表节点的定义。
type ListNode struct {
Val int
Next *ListNode
}
// addTwoNumbers 接受两个链表头节点 l1 和 l2,并返回一个新链表头节点,该链表表示数字之和。
func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {
// 创建一个哑节点,可以简化对链表的处理,dummyHead.Next 将会是结果链表的头节点。
dummyHead := &ListNode{}
// current 用于指向新链表的当前节点。
current := dummyHead
// carry 表示当前的进位。
carry := 0
// 当 l1 或 l2 还有节点,或者还有进位时,循环继续。
for l1 != nil || l2 != nil || carry > 0 {
// 计算当前位的和。
sum := carry
if l1 != nil {
sum += l1.Val
l1 = l1.Next
}
if l2 != nil {
sum += l2.Val
l2 = l2.Next
}
// 更新进位。
carry = sum / 10
// 创建新节点,值为当前位的和的个位数。
current.Next = &ListNode{Val: sum % 10}
// 移动 current 到下一个节点。
current = current.Next
}
// 返回哑节点的下一个节点,即结果链表的头节点。
return dummyHead.Next
}
这段代码定义了一个ListNode
结构体,用于表示链表节点。addTwoNumbers
函数接受两个链表的头节点作为输入,创建一个哑节点作为结果链表的前置节点,然后遍历两个输入链表,将对应的节点值相加,并处理进位。最后返回结果链表的头节点。
C++版本
// Definition for singly-linked list.
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(nullptr) {}
};
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
// 创建一个哑节点,可以简化对链表的处理,dummyHead.next 将会是结果链表的头节点。
ListNode *dummyHead = new ListNode(0);
// current 用于指向新链表的当前节点。
ListNode *current = dummyHead;
// carry 表示当前的进位。
int carry = 0;
// 当 l1 或 l2 还有节点,或者还有进位时,循环继续。
while (l1 != nullptr || l2 != nullptr || carry != 0) {
// 计算当前位的和。
int sum = carry;
if (l1 != nullptr) {
sum += l1->val;
l1 = l1->next;
}
if (l2 != nullptr) {
sum += l2->val;
l2 = l2->next;
}
// 更新进位。
carry = sum / 10;
// 创建新节点,值为当前位的和的个位数。
current->next = new ListNode(sum % 10);
// 移动 current 到下一个节点。
current = current->next;
}
// 返回哑节点的下一个节点,即结果链表的头节点。
ListNode *result = dummyHead->next;
delete dummyHead; // 删除哑节点。
return result;
}
};
这段 C++代码定义了一个ListNode
结构体,用于表示链表节点,并包含一个构造函数用于初始化节点值。Solution
类中的addTwoNumbers
成员函数实现了两个数相加的逻辑。函数创建了一个哑节点用于简化链表操作,通过遍历两个输入链表并处理进位来构建结果链表。最后,函数返回结果链表的头节点,并删除了哑节点以避免内存泄漏。
Python 版本
# Definition for singly-linked list.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
# 创建一个哑节点,可以简化对链表的处理,dummy_head.next 将会是结果链表的头节点。
dummy_head = ListNode(0)
# current 用于指向新链表的当前节点。
current = dummy_head
# carry 表示当前的进位。
carry = 0
# 当 l1 或 l2 还有节点,或者还有进位时,循环继续。
while l1 or l2 or carry:
# 计算当前位的和。
sum = carry
if l1:
sum += l1.val
l1 = l1.next
if l2:
sum += l2.val
l2 = l2.next
# 更新进位。
carry, val = divmod(sum, 10)
# 创建新节点,值为当前位的和的个位数。
current.next = ListNode(val)
# 移动 current 到下一个节点。
current = current.next
# 返回哑节点的下一个节点,即结果链表的头节点。
return dummy_head.next
这段 Python 代码定义了一个ListNode
类,用于表示链表节点,并包含一个构造函数用于初始化节点值。Solution
类中的addTwoNumbers
方法实现了两个数相加的逻辑。方法创建了一个哑节点用于简化链表操作,通过遍历两个输入链表并处理进位来构建结果链表。最后,方法返回结果链表的头节点。
Java 版本
// Definition for singly-linked list.
public class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
// 创建一个哑节点,可以简化对链表的处理,dummyHead.next 将会是结果链表的头节点。
ListNode dummyHead = new ListNode(0);
// current 用于指向新链表的当前节点。
ListNode current = dummyHead;
// carry 表示当前的进位。
int carry = 0;
// 当 l1 或 l2 还有节点,或者还有进位时,循环继续。
while (l1 != null || l2 != null || carry != 0) {
// 计算当前位的和。
int sum = carry;
if (l1 != null) {
sum += l1.val;
l1 = l1.next;
}
if (l2 != null) {
sum += l2.val;
l2 = l2.next;
}
// 更新进位。
carry = sum / 10;
// 创建新节点,值为当前位的和的个位数。
current.next = new ListNode(sum % 10);
// 移动 current 到下一个节点。
current = current.next;
}
// 返回哑节点的下一个节点,即结果链表的头节点。
return dummyHead.next;
}
}
这段 Java 代码定义了一个ListNode
类,用于表示链表节点,并包含几个构造函数用于初始化节点。Solution
类中的addTwoNumbers
方法实现了两个数相加的逻辑。方法创建了一个哑节点用于简化链表操作,通过遍历两个输入链表并处理进位来构建结果链表。最后,方法返回结果链表的头节点。
Rust 版本
#![allow(unused)] fn main() { // Definition for singly-linked list. #[derive(PartialEq, Eq, Clone, Debug)] pub struct ListNode { pub val: i32, pub next: Option<Box<ListNode>>, } impl ListNode { #[inline] fn new(val: i32) -> Self { ListNode { next: None, val, } } } impl Solution { pub fn add_two_numbers(l1: Option<Box<ListNode>>, l2: Option<Box<ListNode>>) -> Option<Box<ListNode>> { let mut dummy_head = Some(Box::new(ListNode::new(0))); let mut current = &mut dummy_head; let (mut l1, mut l2) = (l1, l2); let mut carry = 0; while l1.is_some() || l2.is_some() || carry != 0 { let sum = match (&l1, &l2) { (Some(node), None) | (None, Some(node)) => { node.val + carry }, (Some(node1), Some(node2)) => { node1.val + node2.val + carry }, (None, None) => carry, }; if let Some(node) = l1 { l1 = node.next; } if let Some(node) = l2 { l2 = node.next; } carry = sum / 10; current.as_mut().unwrap().next = Some(Box::new(ListNode::new(sum % 10))); current = &mut current.as_mut().unwrap().next; } dummy_head.unwrap().next } } }
这段 Rust 代码定义了一个ListNode
结构体,用于表示链表节点,并实现了一个new
函数用于创建新节点。Solution
结构体中的add_two_numbers
函数实现了两个数相加的逻辑。函数创建了一个哑节点用于简化链表操作,通过遍历两个输入链表并处理进位来构建结果链表。最后,函数返回结果链表的头节点。
总结
上面给出了使用不同编程语言(包括 Golang、C++、Python、Java 和 Rust)实现的两数相加的链表题目的解法。这道题目的解法思路大致相同,都是通过遍历两个链表,同时处理进位,然后构建新的链表来表示结果。每种语言的具体实现细节略有不同,但核心思想是一致的。这展示了不同编程语言之间的语法和实现风格的差异,同时也展示了这种常见问题的多种解决方法。
删除链表的倒数第 N 个节点
题目要求
你需要编写一个算法来删除一个给定链表的倒数第 n 个节点,并返回修改后链表的头节点。
解题思路
要删除链表的倒数第 n 个节点,我们可以采用双指针法来解决这个问题。具体步骤如下:
- 初始化两个指针
fast
和slow
都指向链表的头节点。 - 移动
fast
指针,使其与slow
指针之间相隔 n 个节点。 - 同时移动
fast
和slow
指针,直到fast
指针指向链表的最后一个节点。 - 此时,
slow
指针将指向要删除节点的前一个节点。修改slow
指针的next
指向,让它跳过下一个节点,即完成了删除操作。 - 如果要删除的是头节点(即
fast
指针在步骤 2 中已经指向链表末尾),则直接返回头节点的下一个节点作为新的头节点。 - 返回修改后的链表的头节点。
注意:在实际操作中,我们需要考虑链表长度小于 n 的情况,以及 n 为 0 或负数的非法输入。对于这些情况,应当有相应的错误处理机制。
Golang 版本
type ListNode struct {
Val int
Next *ListNode
}
func removeNthFromEnd(head *ListNode, n int) *ListNode {
dummy := &ListNode{0, head} // 创建一个哑节点,指向头节点
first := head
second := dummy // second将从哑节点开始
// 移动first,使得first和second之间相隔n个节点
for i := 0; i < n; i++ {
first = first.Next
}
// 移动first到末尾,同时移动second
for first != nil {
first = first.Next
second = second.Next
}
// 此时second的下一个节点就是我们要删除的节点
// 删除second的下一个节点
second.Next = second.Next.Next
// 返回哑节点的下一个节点,即新的头节点
return dummy.Next
}
这段代码定义了一个链表节点的结构体ListNode
,并实现了一个removeNthFromEnd
函数,该函数接收链表的头节点head
和一个整数n
,返回删除链表倒数第n
个节点后的头节点。代码中使用了一个哑节点来简化边界条件的处理,特别是当需要删除的节点是头节点时。
C++版本
#include <iostream>
// 定义链表节点结构体
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode *dummy = new ListNode(0); // 创建一个哑节点
dummy->next = head;
ListNode *first = head;
ListNode *second = dummy; // second从哑节点开始
// 移动first指针,使其与second之间相隔n个节点
for (int i = 0; i < n; ++i) {
first = first->next;
}
// 同时移动first和second指针,直到first指向链表末尾
while (first != nullptr) {
first = first->next;
second = second->next;
}
// 此时second的下一个节点就是我们要删除的节点
ListNode *toDelete = second->next;
second->next = second->next->next;
delete toDelete; // 释放被删除节点的内存
ListNode *newHead = dummy->next;
delete dummy; // 删除哑节点
return newHead; // 返回新的头节点
}
};
这段 C++代码定义了一个链表节点的结构体ListNode
和一个解决问题的类Solution
。Solution
类中的removeNthFromEnd
成员函数实现了删除链表倒数第n
个节点的功能,并返回新的头节点。代码中使用了一个哑节点来处理边界情况,特别是当需要删除的节点是头节点时。在删除节点后,代码还负责释放被删除节点的内存。
Python 版本
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
dummy = ListNode(0)
dummy.next = head
fast = slow = dummy
# Move fast ahead by n+1 steps to maintain the gap between fast and slow
for _ in range(n + 1):
fast = fast.next
# Move both pointers until fast reaches the end
while fast:
fast = fast.next
slow = slow.next
# Slow is now at the node before the one we want to remove
slow.next = slow.next.next
return dummy.next # Return the head of the modified list
这段 Python 代码定义了一个链表节点类ListNode
和一个解决问题的类Solution
。Solution
类中的removeNthFromEnd
方法实现了删除链表倒数第n
个节点的功能,并返回新的头节点。代码中使用了一个哑节点(dummy node)来简化边界条件的处理,特别是当需要删除的节点是头节点时。
Java 版本
public class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode first = dummy;
ListNode second = dummy;
// Move first to the n+1 position
for (int i = 1; i <= n + 1; i++) {
first = first.next;
}
// Move first to the end, maintaining the gap
while (first != null) {
first = first.next;
second = second.next;
}
// Skip the desired node
second.next = second.next.next;
return dummy.next;
}
}
这段 Java 代码定义了一个链表节点类ListNode
和一个解决问题的类Solution
。Solution
类中的removeNthFromEnd
方法实现了删除链表倒数第n
个节点的功能,并返回新的头节点。代码中使用了一个哑节点(dummy node)来简化边界条件的处理,特别是当需要删除的节点是头节点时。
Rust 版本
#![allow(unused)] fn main() { // Definition for singly-linked list. #[derive(PartialEq, Eq, Clone, Debug)] pub struct ListNode { pub val: i32, pub next: Option<Box<ListNode>>, } impl ListNode { #[inline] fn new(val: i32) -> Self { ListNode { next: None, val, } } } pub struct Solution; impl Solution { pub fn remove_nth_from_end(head: Option<Box<ListNode>>, n: i32) -> Option<Box<ListNode>> { let mut dummy = Some(Box::new(ListNode { val: 0, next: head })); let mut fast = dummy.as_ref(); let mut slow = &mut dummy; for _ in 0..n { if let Some(f) = fast { fast = f.next.as_ref(); } } while let Some(f) = fast { fast = f.next.as_ref(); slow = &mut slow.as_mut().unwrap().next; } let next = slow.as_mut().unwrap().next.take(); slow.as_mut().unwrap().next = next.and_then(|node| node.next); dummy.unwrap().next } } }
这段 Rust 代码定义了一个单链表节点结构体ListNode
和一个解决问题的结构体Solution
。Solution
结构体中的remove_nth_from_end
函数实现了删除链表倒数第n
个节点的功能,并返回新的头节点。代码中使用了Option<Box<ListNode>>
来处理 Rust 中的所有权和可空性,以及一个哑节点(dummy node)来简化边界条件的处理,特别是当需要删除的节点是头节点时。
总结
上面给出的解法使用了双指针法来删除链表的倒数第 n 个节点。无论是使用的是哪种编程语言,解题思路都是一致的:通过维护两个指针之间的固定间隔,实现对链表的一次遍历即可找到要删除的节点。这种方法的时间复杂度为 O(N),其中 N 是链表的长度,空间复杂度为 O(1)。在实际编码中,需要考虑链表长度小于 n 的情况,以及 n 为 0 或负数的非法输入,对这些情况应当有相应的错误处理机制。
两两交换链表中的节点
题目要求
本题的目标是实现一个算法,用于交换给定链表中相邻节点的位置。具体来说,就是将链表中的第 1 个节点与第 2 个节点交换,第 3 个节点与第 4 个节点交换,以此类推。如果链表的长度是奇数,那么最后一个节点不需要与前一个节点交换。重要的约束是不能改变节点内部的值,只能通过改变节点之间的链接(即指针)来实现节点的交换。算法完成后,需要返回交换后链表的头节点。
解题思路
解决这个问题的关键在于重新链接链表中的节点,以实现节点的两两交换。以下是解题的步骤:
-
创建一个哨兵节点(dummy node),它的 next 指针指向链表的头节点。这个哨兵节点可以帮助简化链表头部的交换操作,并且最后可以通过哨兵节点来返回新的头节点。
-
初始化两个指针,prev 指针指向哨兵节点,current 指针指向链表的头节点。
-
遍历链表,检查 current 及其 next 节点是否存在。如果只剩下一个或没有节点,则不需要交换,直接结束。
-
如果需要交换,执行以下步骤:
- 将 prev 的 next 指针指向 current 的 next 节点,即将第二个节点移到第一个位置。
- 将 current 的 next 指针指向 current 的 next 的 next 节点,即将第一个节点链接到第三个节点的位置。
- 将 current 的 next 的 next 指针指向 current,即将第二个节点链接到第一个节点的位置。
- 更新 prev 指针为当前的 current 节点。
-
将 current 指针更新为 prev 的 next,即移动到下一对需要交换的节点的第一个位置。
-
重复步骤 3 到 5,直到链表末尾。
-
返回哨兵节点的 next,即交换后的链表头节点。
通过这种方式,我们可以在一次遍历中完成所有的交换操作,而不需要修改任何节点的值。这个算法的时间复杂度是 O(n),空间复杂度是 O(1)。
Golang 版本
type ListNode struct {
Val int
Next *ListNode
}
func swapPairs(head *ListNode) *ListNode {
dummy := &ListNode{Next: head}
prev := dummy
for head != nil && head.Next != nil {
// Nodes to be swapped
firstNode := head
secondNode := head.Next
// Swapping
prev.Next = secondNode
firstNode.Next = secondNode.Next
secondNode.Next = firstNode
// Reinitializing the head and prev node for next swap
prev = firstNode
head = firstNode.Next // jump to the next pair
}
// Return the new head node.
return dummy.Next
}
这段代码定义了一个链表节点的结构体ListNode
,并实现了一个swapPairs
函数,该函数接收一个链表的头节点head
作为参数,返回两两交换后的链表的头节点。代码中使用了一个哨兵节点dummy
来简化边界条件的处理,通过迭代的方式遍历链表,并在每一步中交换相邻的节点。
C++版本
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
ListNode dummy(0);
dummy.next = head;
ListNode* prev = &dummy;
while (head && head->next) {
ListNode* firstNode = head;
ListNode* secondNode = head->next;
// Swapping
prev->next = secondNode;
firstNode->next = secondNode->next;
secondNode->next = firstNode;
// Reinitializing the pointers for next swap
prev = firstNode;
head = firstNode->next; // move to the next pair
}
return dummy.next;
}
};
这段 C++代码定义了一个链表节点的结构体ListNode
和一个解决方案类Solution
。Solution
类中的swapPairs
成员函数接收一个链表的头节点head
作为参数,并返回两两交换后的链表的头节点。代码中使用了一个哨兵节点dummy
来简化链表头部的处理,通过迭代的方式遍历链表,并在每一步中交换相邻的节点。
Python 版本
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def swapPairs(self, head: ListNode) -> ListNode:
dummy = ListNode(0)
dummy.next = head
prev, current = dummy, head
while current and current.next:
# Nodes to be swapped
first_node = current
second_node = current.next
# Swapping
prev.next = second_node
first_node.next = second_node.next
second_node.next = first_node
# Reinitializing the pointers for next swap
prev = first_node
current = first_node.next
return dummy.next
这段 Python 代码定义了一个链表节点的类ListNode
和一个解决方案类Solution
。Solution
类中的swapPairs
方法接收一个链表的头节点head
作为参数,并返回两两交换后的链表的头节点。代码中使用了一个哨兵节点dummy
来简化链表头部的处理,通过迭代的方式遍历链表,并在每一步中交换相邻的节点。
Java 版本
public class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode prev = dummy;
ListNode current = head;
while (current != null && current.next != null) {
// Nodes to be swapped
ListNode firstNode = current;
ListNode secondNode = current.next;
// Swapping
prev.next = secondNode;
firstNode.next = secondNode.next;
secondNode.next = firstNode;
// Reinitializing the pointers for next swap
prev = firstNode;
current = firstNode.next;
}
return dummy.next;
}
}
这段 Java 代码定义了一个链表节点的类ListNode
和一个解决方案类Solution
。Solution
类中的swapPairs
方法接收一个链表的头节点head
作为参数,并返回两两交换后的链表的头节点。代码中使用了一个哨兵节点dummy
来简化链表头部的处理,通过迭代的方式遍历链表,并在每一步中交换相邻的节点。
Rust 版本
#![allow(unused)] fn main() { // Definition for singly-linked list. #[derive(PartialEq, Eq, Clone, Debug)] pub struct ListNode { pub val: i32, pub next: Option<Box<ListNode>>, } impl ListNode { #[inline] fn new(val: i32) -> Self { ListNode { next: None, val } } } impl Solution { pub fn swap_pairs(head: Option<Box<ListNode>>) -> Option<Box<ListNode>> { let mut dummy = Some(Box::new(ListNode { val: 0, next: head })); let mut prev = &mut dummy; while prev.as_ref().unwrap().next.is_some() && prev.as_ref().unwrap().next.as_ref().unwrap().next.is_some() { let mut first = prev.as_mut().unwrap().next.take().unwrap(); let mut second = first.next.take().unwrap(); first.next = second.next.take(); second.next = Some(first); prev.as_mut().unwrap().next = Some(second); prev = &mut prev.as_mut().unwrap().next.as_mut().unwrap().next; } dummy.unwrap().next } } }
这段 Rust 代码定义了一个单链表节点的结构体ListNode
和一个解决方案模块Solution
。Solution
模块中的swap_pairs
函数接收一个链表的头节点head
作为参数,并返回两两交换后的链表的头节点。代码中使用了一个哨兵节点dummy
来简化链表头部的处理,通过迭代的方式遍历链表,并在每一步中交换相邻的节点。在 Rust 中,链表的节点通常通过Option<Box<ListNode>>
来表示,以便于处理所有权和可变性。
总结
上面给出了使用不同编程语言实现的解决方案,用于交换给定链表中相邻节点的位置。这些解决方案都采用了迭代的方式遍历链表,并在每一步中交换相邻的节点。它们都使用了一个哨兵节点来简化链表头部的处理,并在不修改节点内部的值的情况下完成了节点的交换。这些解决方案在不同的编程语言中展示了如何通过指针或引用来操作链表节点,实现了相同的算法逻辑。
K 个一组翻转链表
题目要求
编写一个函数,实现对链表的分组翻转功能。具体来说,给定一个链表的头节点 head
,应该将链表中的每 k
个节点看作一组进行翻转。如果链表中的节点数不是 k
的整数倍,那么最后剩余的节点应保持原有的顺序。在翻转过程中,不能仅仅改变节点的值,而是需要通过改变节点之间的链接关系来实现翻转。
解题思路
解决这个问题可以分为以下几个步骤:
-
遍历链表:首先需要遍历整个链表,确定链表的总长度,以便知道最后一组是否有足够的
k
个节点进行翻转。 -
分组翻转:从头节点开始,对链表中的每
k
个节点进行翻转。翻转的基本方法是,对于每一组内的节点,逐个将其摘下并插入到当前组的起始位置,直到该组内的节点全部翻转完毕。 -
处理剩余节点:如果链表的长度不是
k
的整数倍,那么最后剩余的节点不需要翻转,保持原样即可。 -
链接各组:在翻转每一组节点的同时,需要注意维护好相邻两组之间的链接关系,确保链表的连续性。
-
返回结果:最后返回新链表的头节点,这个头节点是第一次翻转后的头节点。
具体实现时,可以使用几个辅助指针来帮助标记和链接:
prev
:指向当前组翻转前的第一个节点的前一个节点,初始时为null
。current
:当前遍历到的节点。next
:指向current
节点的下一个节点,用于在翻转节点时不丢失链表的其余部分。tail
:当前组翻转前的第一个节点,翻转后将成为该组的尾节点,需要链接到下一组的头节点。
在实现时,需要注意边界条件的处理,如链表长度小于 k
,链表为空等情况。
Golang 版本
package main
import (
"fmt"
)
// ListNode is a definition for singly-linked list.
type ListNode struct {
Val int
Next *ListNode
}
func reverseKGroup(head *ListNode, k int) *ListNode {
if head == nil || k == 1 {
return head
}
// 创建一个虚拟头节点,便于处理头部翻转的情况
dummy := &ListNode{Next: head}
prevGroupEnd := dummy
// 遍历链表,确定链表长度
length := 0
for current := head; current != nil; current = current.Next {
length++
}
// 对每k个节点进行翻转
for length >= k {
current := prevGroupEnd.Next
next := current.Next
for i := 1; i < k; i++ {
current.Next = next.Next
next.Next = prevGroupEnd.Next
prevGroupEnd.Next = next
next = current.Next
}
prevGroupEnd = current
length -= k
}
return dummy.Next
}
func printList(head *ListNode) {
for head != nil {
fmt.Printf("%d ", head.Val)
head = head.Next
}
fmt.Println()
}
func main() {
// 创建一个示例链表 1->2->3->4->5
nodes := []int{1, 2, 3, 4, 5}
dummy := &ListNode{}
current := dummy
for _, val := range nodes {
current.Next = &ListNode{Val: val}
current = current.Next
}
// 应用翻转函数
k := 2
result := reverseKGroup(dummy.Next, k)
// 打印翻转后的链表
printList(result)
}
这段代码定义了一个链表节点 ListNode
,实现了一个 reverseKGroup
函数来解决题目中的问题。main
函数创建了一个示例链表,并调用 reverseKGroup
函数进行测试,最后打印出翻转后的链表。
C++版本
#include <iostream>
// Definition for singly-linked list.
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
if (head == nullptr || k == 1) {
return head;
}
ListNode dummy(0);
dummy.next = head;
ListNode *prevGroupEnd = &dummy;
while (head != nullptr) {
// Check if there are k nodes left in the list
ListNode *tail = head;
int i;
for (i = 0; i < k; ++i) {
if (tail == nullptr) break;
tail = tail->next;
}
if (i != k) break; // Less than k nodes, no need to reverse
// Reverse k nodes
ListNode *prev = tail, *curr = head, *next = nullptr;
while (curr != tail) {
next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
// Connect with previous part
prevGroupEnd->next = prev;
prevGroupEnd = head;
head = tail;
}
return dummy.next;
}
};
void printList(ListNode* head) {
while (head != nullptr) {
std::cout << head->val << " ";
head = head->next;
}
std::cout << std::endl;
}
int main() {
// Create a sample linked list 1->2->3->4->5
ListNode *list = new ListNode(1, new ListNode(2, new ListNode(3, new ListNode(4, new ListNode(5)))));
Solution solution;
int k = 2;
ListNode *result = solution.reverseKGroup(list, k);
// Print the reversed list
printList(result);
// Clean up memory
while (result != nullptr) {
ListNode *temp = result;
result = result->next;
delete temp;
}
return 0;
}
这段 C++代码定义了一个链表节点 ListNode
和一个解决问题的类 Solution
。Solution
类中的 reverseKGroup
方法实现了题目要求的链表分组翻转功能。main
函数创建了一个示例链表,并调用 reverseKGroup
方法进行测试,最后打印出翻转后的链表,并且在最后进行了内存清理。
Python 版本
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def reverseKGroup(self, head: ListNode, k: int) -> ListNode:
if head is None or k == 1:
return head
# Dummy node initialization
dummy = ListNode(0)
dummy.next = head
prev_group_end = dummy
# Count the number of nodes in the list
count = 0
current = head
while current:
count += 1
current = current.next
# Reverse nodes in k groups
while count >= k:
current = prev_group_end.next
next_node = current.next
for i in range(1, k):
current.next = next_node.next
next_node.next = prev_group_end.next
prev_group_end.next = next_node
next_node = current.next
prev_group_end = current
count -= k
return dummy.next
# Helper function to print the list
def print_list(node):
while node:
print(node.val, end=" ")
node = node.next
print()
# Example usage
if __name__ == "__main__":
# Create a sample linked list 1->2->3->4->5
nodes = [ListNode(i) for i in range(1, 6)]
for i in range(len(nodes) - 1):
nodes[i].next = nodes[i + 1]
head = nodes[0]
# Apply the reverse function
k = 2
solution = Solution()
reversed_list_head = solution.reverseKGroup(head, k)
# Print the reversed list
print_list(reversed_list_head)
这段 Python 代码定义了一个链表节点 ListNode
和一个解决问题的类 Solution
。Solution
类中的 reverseKGroup
方法实现了题目要求的链表分组翻转功能。在 if __name__ == "__main__":
部分,代码创建了一个示例链表,并调用 reverseKGroup
方法进行测试,最后打印出翻转后的链表。
Java 版本
public class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
if (head == null || k == 1) {
return head;
}
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode prevGroupEnd = dummy;
ListNode current = head;
// Count the total number of nodes in the list
int count = 0;
while (current != null) {
count++;
current = current.next;
}
// Reverse nodes in k groups
while (count >= k) {
current = prevGroupEnd.next;
ListNode next = current.next;
for (int i = 1; i < k; i++) {
current.next = next.next;
next.next = prevGroupEnd.next;
prevGroupEnd.next = next;
next = current.next;
}
prevGroupEnd = current;
count -= k;
}
return dummy.next;
}
}
public class Main {
public static void printList(ListNode node) {
while (node != null) {
System.out.print(node.val + " ");
node = node.next;
}
System.out.println();
}
public static void main(String[] args) {
// Create a sample linked list 1->2->3->4->5
ListNode head = new ListNode(1, new ListNode(2, new ListNode(3, new ListNode(4, new ListNode(5)))));
Solution solution = new Solution();
int k = 2;
ListNode result = solution.reverseKGroup(head, k);
// Print the reversed list
printList(result);
}
}
这段 Java 代码定义了一个链表节点 ListNode
和一个解决问题的类 Solution
。Solution
类中的 reverseKGroup
方法实现了题目要求的链表分组翻转功能。Main
类中的 main
方法创建了一个示例链表,并调用 reverseKGroup
方法进行测试,最后打印出翻转后的链表。
Rust 版本
use std::rc::Rc; use std::cell::RefCell; // Definition for singly-linked list. #[derive(PartialEq, Eq, Clone, Debug)] pub struct ListNode { pub val: i32, pub next: Option<Rc<RefCell<ListNode>>>, } impl ListNode { #[inline] fn new(val: i32) -> Self { ListNode { val, next: None, } } } pub struct Solution; impl Solution { pub fn reverse_k_group(head: Option<Rc<RefCell<ListNode>>>, k: i32) -> Option<Rc<RefCell<ListNode>>> { let mut head = head; let mut stack = Vec::new(); let mut current = head.clone(); // Push nodes to stack and pop to reverse in groups of k while let Some(node) = current { stack.push(node.clone()); if stack.len() == k as usize { while let Some(top) = stack.pop() { if stack.is_empty() { // Connect the last node of the reversed group to the next group top.borrow_mut().next = Self::reverse_k_group(top.borrow().next.clone(), k); return Some(top); } // Reverse the link top.borrow_mut().next = stack.last().cloned(); } } current = node.borrow().next.clone(); } // If the number of nodes is not a multiple of k, return the remaining nodes as is head } } fn main() { // Helper function to create a list from a Vec of i32 fn to_list(vec: Vec<i32>) -> Option<Rc<RefCell<ListNode>>> { let mut current = None; for &val in vec.iter().rev() { let mut node = ListNode::new(val); node.next = current; current = Some(Rc::new(RefCell::new(node))); } current } // Helper function to collect values from a list into a Vec<i32> fn collect_values(head: Option<Rc<RefCell<ListNode>>>) -> Vec<i32> { let mut values = Vec::new(); let mut current = head; while let Some(node) = current { values.push(node.borrow().val); current = node.borrow().next.clone(); } values } // Example usage let list = to_list(vec![1, 2, 3, 4, 5]); let k = 2; let reversed_list = Solution::reverse_k_group(list, k); println!("{:?}", collect_values(reversed_list)); }
这段 Rust 代码定义了一个链表节点 ListNode
和一个解决问题的结构体 Solution
。Solution
结构体中的 reverse_k_group
函数实现了题目要求的链表分组翻转功能。main
函数中创建了一个示例链表,并调用 reverse_k_group
函数进行测试,最后打印出翻转后的链表的值。
总结
上面给出的解法是针对链表分组翻转问题的不同编程语言版本的实现。无论是使用 Golang、C++、Python、Java 还是 Rust,解决问题的思路都是一致的:
- 首先,需要遍历链表,确定链表的长度,以便后续处理。
- 然后,对链表中的每 k 个节点一组进行翻转。在翻转过程中,需要维护好相邻两组之间的链接关系,确保链表的连续性。
- 如果节点总数不是 k 的整数倍,剩余的节点保持原有顺序。
这些解法都采用了类似的逻辑,但在具体实现上有一些语言特定的差异。例如,不同语言对于链表的表示和操作方式有所不同,因此在代码中会有相应的语言特定的语法和数据结构的使用。
随机链表的复制
题目要求
你需要实现一个算法来复制一个特殊的链表,在这个链表中,每个节点除了有一个next
指针指向下一个节点,还有一个random
指针,它可以指向链表中的任意节点或者是空节点。你的任务是创建这个链表的一个深拷贝,这意味着你需要创建一份完全新的节点集合,使得:
- 每个新节点的值与原链表中的对应节点的值相同。
- 新节点的
next
指针指向复制链表中的下一个新节点。 - 新节点的
random
指针指向复制链表中的新节点,这些新节点对应于原链表中被random
指针指向的节点。 - 不应有任何指针指向原链表中的节点,即复制链表应该是完全独立的。
最终,你需要返回新复制链表的头节点。
解题思路
解决这个问题的关键是如何处理random
指针的复制。以下是一个高效的解题步骤:
-
遍历原链表,创建新节点:
- 遍历原链表,对于每个原节点创建一个新节点。
- 将新节点插入到原节点和原节点的下一个节点之间。
- 例如,原链表是 A -> B -> C,变换后的链表是 A -> A' -> B -> B' -> C -> C',其中 A', B', C'是新创建的节点的深拷贝。
-
复制
random
指针:- 再次遍历链表,复制
random
指针。由于新节点是紧跟在原节点后面的,可以通过original_node.random.next
来设置新节点的random
指针(如果original_node.random
不为空的话)。
- 再次遍历链表,复制
-
拆分链表:
- 最后,将这个交错的链表拆分为原链表和复制链表。
- 遍历链表,将原节点的
next
指向next.next
,将复制节点的next
指向next.next
(如果next
不为空的话)。 - 同时恢复原链表的结构。
这个方法的优点是不需要额外的空间来存储节点间的对应关系,因为复制节点紧跟在原节点后面,可以通过原链表直接定位到复制链表的节点。这样,我们就可以在不使用额外空间的情况下完成random
指针的复制。
Golang 版本
type Node struct {
Val int
Next *Node
Random *Node
}
func copyRandomList(head *Node) *Node {
if head == nil {
return nil
}
// Step 1: Create a new node for each node and insert it right after the original one.
for current := head; current != nil; current = current.Next.Next {
newNode := &Node{Val: current.Val}
newNode.Next = current.Next
current.Next = newNode
}
// Step 2: Copy the random pointers for the new nodes.
for current := head; current != nil; current = current.Next.Next {
if current.Random != nil {
current.Next.Random = current.Random.Next
}
}
// Step 3: Separate the combined list into the original list and the copied list.
pseudoHead := &Node{}
copyCurrent := pseudoHead
for current := head; current != nil; current = current.Next {
// Extract the copy
copyCurrent.Next = current.Next
copyCurrent = copyCurrent.Next
// Restore the original list
current.Next = current.Next.Next
}
return pseudoHead.Next
}
这段代码实现了上述的解题思路,首先在原链表的每个节点后面插入了一个新的节点,然后复制了random
指针,最后将这个交错的链表拆分成了原链表和复制链表。这样,我们就得到了原链表的深拷贝。
C++版本
#include <iostream>
using namespace std;
// Definition for a Node.
class Node {
public:
int val;
Node* next;
Node* random;
Node(int _val) {
val = _val;
next = NULL;
random = NULL;
}
};
class Solution {
public:
Node* copyRandomList(Node* head) {
if (!head) return nullptr;
// Step 1: Create a new node for each node and insert it right after the original one.
for (Node* node = head; node != nullptr; node = node->next->next) {
Node* newNode = new Node(node->val);
newNode->next = node->next;
node->next = newNode;
}
// Step 2: Copy the random pointers for the new nodes.
for (Node* node = head; node != nullptr; node = node->next->next) {
if (node->random) {
node->next->random = node->random->next;
}
}
// Step 3: Separate the combined list into the original list and the copied list.
Node* newHead = head->next;
for (Node* node = head; node != nullptr; node = node->next) {
Node* newNode = node->next;
node->next = newNode->next;
if (newNode->next) newNode->next = newNode->next->next;
}
return newHead;
}
};
int main() {
// Use the Solution class as you want to test the copyRandomList function.
return 0;
}
这段 C++代码实现了链表的深拷贝,其中包含了对random
指针的特殊处理。它首先在每个原始节点后面插入一个新的节点,然后设置新节点的random
指针,最后将这个交错的链表拆分成原始链表和复制链表。
Python 版本
class Node:
def __init__(self, val=0, next=None, random=None):
self.val = val
self.next = next
self.random = random
def copyRandomList(head):
if not head:
return None
# Step 1: Create a new node for each node and insert it right after the original one.
current = head
while current:
new_node = Node(current.val, current.next)
current.next = new_node
current = new_node.next
# Step 2: Copy the random pointers for the new nodes.
current = head
while current:
if current.random:
current.next.random = current.random.next
current = current.next.next
# Step 3: Separate the combined list into the original list and the copied list.
pseudo_head = Node(0)
copy_current, current = pseudo_head, head
while current:
next_orig = current.next.next
# Extract the copy
copy_current.next = current.next
copy_current = copy_current.next
# Restore the original list
current.next = next_orig
current = next_orig
return pseudo_head.next
这段 Python 代码实现了链表的深拷贝,包括对random
指针的复制。它首先在每个原始节点后面插入一个新的节点,然后设置新节点的random
指针,最后将这个交错的链表拆分成原始链表和复制链表。
Java 版本
class Node {
int val;
Node next;
Node random;
public Node(int val) {
this.val = val;
this.next = null;
this.random = null;
}
}
public class Solution {
public Node copyRandomList(Node head) {
if (head == null) {
return null;
}
// Step 1: Create a new node for each node and insert it right after the original one.
Node current = head;
while (current != null) {
Node newNode = new Node(current.val);
newNode.next = current.next;
current.next = newNode;
current = newNode.next;
}
// Step 2: Copy the random pointers for the new nodes.
current = head;
while (current != null) {
if (current.random != null) {
current.next.random = current.random.next;
}
current = current.next.next;
}
// Step 3: Separate the combined list into the original list and the copied list.
Node pseudoHead = new Node(0);
Node copy, copyCurrent = pseudoHead;
current = head;
while (current != null) {
next = current.next.next;
// Extract the copy
copy = current.next;
copyCurrent.next = copy;
copyCurrent = copy;
// Restore the original list
current.next = next;
current = next;
}
return pseudoHead.next;
}
}
这段 Java 代码实现了链表的深拷贝,包括对random
指针的复制。它首先在每个原始节点后面插入一个新的节点,然后设置新节点的random
指针,最后将这个交错的链表拆分成原始链表和复制链表。
Rust 版本
#![allow(unused)] fn main() { // Definition for a Node. #[derive(Clone, Debug)] pub struct Node { pub val: i32, pub next: Option<Box<Node>>, pub random: Option<Box<Node>>, } impl Node { fn new(val: i32) -> Node { Node { val, next: None, random: None, } } } pub fn copy_random_list(head: Option<Box<Node>>) -> Option<Box<Node>> { let mut head = head; let mut current = head.as_mut(); // Step 1: Create a new node for each node and insert it right after the original one. while let Some(node) = current { let new_node = Box::new(Node::new(node.val)); let next = node.next.take(); node.next = Some(new_node); node.next.as_mut().unwrap().next = next; current = node.next.as_mut().unwrap().next.as_mut(); } // Step 2: Copy the random pointers for the new nodes. current = head.as_mut(); while let Some(node) = current { node.next.as_mut().unwrap().random = node.random.as_ref().map(|random_node| { Box::new(Node::new(random_node.val)) }); current = node.next.as_mut().unwrap().next.as_mut(); } // Step 3: Separate the combined list into the original list and the copied list. let mut pseudo_head = Some(Box::new(Node::new(0))); let mut tail = pseudo_head.as_mut(); current = head.as_mut(); while let Some(node) = current { let next = node.next.as_mut().unwrap().next.take(); tail.as_mut().unwrap().next = node.next.take(); tail = tail.unwrap().next.as_mut(); node.next = next; current = node.next.as_mut(); } pseudo_head.unwrap().next } }
这段 Rust 代码实现了链表的深拷贝,包括对random
指针的复制。它首先在每个原始节点后面插入一个新的节点,然后设置新节点的random
指针,最后将这个交错的链表拆分成原始链表和复制链表。由于 Rust 的所有权和借用规则,这段代码在处理指针和节点的所有权时需要特别注意。
总结
上面给出了针对复制带随机指针的链表的解法,涵盖了 Golang、C++、Python、Java 和 Rust 的代码实现。
这个问题的解决思路是通过三个步骤来完成链表的深拷贝:首先在每个原始节点后面插入一个新的节点,然后设置新节点的random
指针,最后将这个交错的链表拆分成原始链表和复制链表。这样就能够实现对带随机指针的链表进行深度复制,保持了原链表和复制链表的独立性。
排序链表
题目要求
你需要编写一个算法,对一个单向链表进行排序。输入是链表的头结点 head
,你需要对链表中的元素按照升序进行排序,然后返回排序后链表的头结点。
解题思路
要对链表进行排序,有几种不同的算法可以选择:
-
归并排序:这是链表排序中最常用的方法,因为它在链表上的时间复杂度是线性对数的,即 O(nlogn),而且它不需要额外的空间,这对链表来说是非常合适的。
- 分解:首先,使用快慢指针法找到链表的中点,将链表分成两半。快指针每次移动两步,慢指针每次移动一步,当快指针到达链表末尾时,慢指针就在链表的中点。
- 递归排序:递归地对两个子链表进行排序。
- 合并:将两个已排序的子链表合并成一个有序的链表。
-
插入排序:虽然插入排序在数组排序中效率不高(时间复杂度为 O(n^2)),但在链表排序中,由于插入操作是 O(1) 的,所以在链表不是很长或基本有序的情况下,插入排序也是一个不错的选择。
- 迭代:从头结点开始,对链表进行迭代,将每个节点插入到已排序部分的正确位置。
-
快速排序:链表上的快速排序实现起来比较复杂,且需要额外的空间来存储上一层递归的状态,因此不是最优选择。
在这些方法中,归并排序是最优的选择,因为它提供了最好的平均性能,并且是稳定的排序算法。具体步骤如下:
- 如果链表只有一个节点或为空,则不需要排序,直接返回。
- 使用快慢指针法找到链表的中间节点,将链表从中间分为两部分。
- 对这两部分分别进行归并排序。
- 将两个排序好的链表合并成一个有序链表。
- 返回排序后的链表头结点。
注意,在实际编码时,需要处理好边界条件,如链表为空或只有一个节点的情况,以及在分割和合并过程中对节点的正确处理。
Golang 版本
package main
import (
"fmt"
)
// ListNode is a node in a singly-linked list.
type ListNode struct {
Val int
Next *ListNode
}
// mergeTwoLists merges two sorted linked lists and returns the head of the sorted list.
func mergeTwoLists(l1 *ListNode, l2 *ListNode) *ListNode {
dummy := &ListNode{}
current := dummy
for l1 != nil && l2 != nil {
if l1.Val < l2.Val {
current.Next = l1
l1 = l1.Next
} else {
current.Next = l2
l2 = l2.Next
}
current = current.Next
}
if l1 != nil {
current.Next = l1
} else {
current.Next = l2
}
return dummy.Next
}
// sortList sorts the linked list in ascending order using merge sort and returns the head of the list.
func sortList(head *ListNode) *ListNode {
if head == nil || head.Next == nil {
return head
}
// Find the middle of the list
slow, fast := head, head.Next
for fast != nil && fast.Next != nil {
fast = fast.Next.Next
slow = slow.Next
}
// Split the list into two halves
mid := slow.Next
slow.Next = nil
// Sort each half
left := sortList(head)
right := sortList(mid)
// Merge the sorted halves
return mergeTwoLists(left, right)
}
// Helper function to print the list - for testing purposes.
func printList(head *ListNode) {
for head != nil {
fmt.Printf("%d ", head.Val)
head = head.Next
}
fmt.Println()
}
// main function to execute the code - for testing purposes.
func main() {
// Example usage:
// Create a linked list 4 -> 2 -> 1 -> 3 and sort it.
node3 := &ListNode{Val: 3, Next: nil}
node1 := &ListNode{Val: 1, Next: node3}
node2 := &ListNode{Val: 2, Next: node1}
head := &ListNode{Val: 4, Next: node2}
sortedHead := sortList(head)
printList(sortedHead) // Expected sorted list: 1 2 3 4
}
这段代码实现了链表的归并排序。sortList
函数是排序的主要入口,它使用递归将链表分成两半,然后对每一半进行排序,最后使用 mergeTwoLists
函数将两个有序链表合并成一个有序链表。main
函数提供了一个测试用例,创建了一个链表并调用 sortList
函数进行排序,然后打印排序后的链表。
C++版本
#include <iostream>
// Definition for singly-linked list.
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(nullptr) {}
};
class Solution {
public:
// Function to find the middle of the linked list
ListNode* findMiddle(ListNode* head) {
ListNode* slow = head;
ListNode* fast = head->next;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
// Function to merge two sorted linked lists
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
ListNode dummy(0);
ListNode* tail = &dummy;
while (l1 && l2) {
if (l1->val < l2->val) {
tail->next = l1;
l1 = l1->next;
} else {
tail->next = l2;
l2 = l2->next;
}
tail = tail->next;
}
tail->next = l1 ? l1 : l2;
return dummy.next;
}
// Main function to sort the linked list
ListNode* sortList(ListNode* head) {
if (!head || !head->next) return head;
ListNode* mid = findMiddle(head);
ListNode* left = head;
ListNode* right = mid->next;
mid->next = nullptr;
left = sortList(left);
right = sortList(right);
return mergeTwoLists(left, right);
}
};
// Helper function to print the list - for testing purposes.
void printList(ListNode* head) {
while (head) {
std::cout << head->val << " ";
head = head->next;
}
std::cout << std::endl;
}
// main function to execute the code - for testing purposes.
int main() {
// Example usage:
// Create a linked list 4 -> 2 -> 1 -> 3 and sort it.
ListNode* node3 = new ListNode(3);
ListNode* node1 = new ListNode(1);
node1->next = node3;
ListNode* node2 = new ListNode(2);
node2->next = node1;
ListNode* head = new ListNode(4);
head->next = node2;
Solution solution;
ListNode* sortedHead = solution.sortList(head);
printList(sortedHead); // Expected sorted list: 1 2 3 4
// Clean up memory
while (sortedHead) {
ListNode* temp = sortedHead;
sortedHead = sortedHead->next;
delete temp;
}
return 0;
}
这段 C++代码实现了链表的归并排序。Solution
类中的 sortList
函数是排序的主要入口,它使用递归将链表分成两半,然后对每一半进行排序,最后使用 mergeTwoLists
函数将两个有序链表合并成一个有序链表。main
函数提供了一个测试用例,创建了一个链表并调用 sortList
函数进行排序,然后打印排序后的链表,并在最后清理了分配的内存。
Python 版本
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def sortList(self, head: ListNode) -> ListNode:
if not head or not head.next:
return head
# Split the list into two halves
slow, fast = head, head.next
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# Split the list into two halves
mid = slow.next
slow.next = None
# Sort each half
left = self.sortList(head)
right = self.sortList(mid)
# Merge the sorted halves
return self.mergeTwoLists(left, right)
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
dummy = ListNode()
tail = dummy
while l1 and l2:
if l1.val < l2.val:
tail.next = l1
l1 = l1.next
else:
tail.next = l2
l2 = l2.next
tail = tail.next
tail.next = l1 or l2
return dummy.next
# Helper function to print the list - for testing purposes.
def printList(node):
while node:
print(node.val, end=" ")
node = node.next
print()
# Example usage:
if __name__ == "__main__":
# Create a linked list 4 -> 2 -> 1 -> 3 and sort it.
node3 = ListNode(3)
node1 = ListNode(1, node3)
node2 = ListNode(2, node1)
head = ListNode(4, node2)
solution = Solution()
sorted_head = solution.sortList(head)
printList(sorted_head) # Expected sorted list: 1 2 3 4
这段 Python 代码实现了链表的归并排序。Solution
类中的 sortList
方法是排序的主要入口,它使用递归将链表分成两半,然后对每一半进行排序,最后使用 mergeTwoLists
方法将两个有序链表合并成一个有序链表。printList
函数和 if __name__ == "__main__":
部分提供了一个测试用例,创建了一个链表并调用 sortList
方法进行排序,然后打印排序后的链表。
Java 版本
public class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
class Solution {
public ListNode sortList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
// Split the list into two halves
ListNode prev = null, slow = head, fast = head;
while (fast != null && fast.next != null) {
prev = slow;
slow = slow.next;
fast = fast.next.next;
}
prev.next = null; // Cut the list into two halves
// Sort each half
ListNode l1 = sortList(head);
ListNode l2 = sortList(slow);
// Merge the sorted halves
return mergeTwoLists(l1, l2);
}
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
ListNode current = dummy;
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
current.next = l1;
l1 = l1.next;
} else {
current.next = l2;
l2 = l2.next;
}
current = current.next;
}
current.next = (l1 != null) ? l1 : l2;
return dummy.next;
}
}
// Helper function to print the list - for testing purposes.
class Main {
public static void printList(ListNode node) {
while (node != null) {
System.out.print(node.val + " ");
node = node.next;
}
System.out.println();
}
public static void main(String[] args) {
// Example usage:
// Create a linked list 4 -> 2 -> 1 -> 3 and sort it.
ListNode node3 = new ListNode(3);
ListNode node1 = new ListNode(1, node3);
ListNode node2 = new ListNode(2, node1);
ListNode head = new ListNode(4, node2);
Solution solution = new Solution();
ListNode sortedHead = solution.sortList(head);
printList(sortedHead); // Expected sorted list: 1 2 3 4
}
}
这段 Java 代码实现了链表的归并排序。Solution
类中的 sortList
方法是排序的主要入口,它使用递归将链表分成两半,然后对每一半进行排序,最后使用 mergeTwoLists
方法将两个有序链表合并成一个有序链表。Main
类中的 printList
方法和 main
方法提供了一个测试用例,创建了一个链表并调用 sortList
方法进行排序,然后打印排序后的链表。
Rust 版本
use std::cell::RefCell; use std::rc::Rc; // Definition for singly-linked list. #[derive(PartialEq, Eq, Clone, Debug)] pub struct ListNode { pub val: i32, pub next: Option<Rc<RefCell<ListNode>>>, } impl ListNode { #[inline] fn new(val: i32) -> Self { ListNode { next: None, val } } } struct Solution; impl Solution { pub fn sort_list(head: Option<Rc<RefCell<ListNode>>>) -> Option<Rc<RefCell<ListNode>>> { if head.is_none() || head.as_ref().unwrap().borrow().next.is_none() { return head; } // Split the list into two halves let (left, right) = Self::split(head); // Sort each half let left_sorted = Self::sort_list(left); let right_sorted = Self::sort_list(right); // Merge the sorted halves Self::merge_two_lists(left_sorted, right_sorted) } fn split( head: Option<Rc<RefCell<ListNode>>>, ) -> (Option<Rc<RefCell<ListNode>>>, Option<Rc<RefCell<ListNode>>>) { let mut slow = head.clone(); let mut fast = head.unwrap().borrow().next.clone(); let mut prev = None; while let Some(fast_node) = fast { fast = fast_node.borrow().next.clone(); if let Some(fast_node) = fast { prev = slow.clone(); slow = slow.unwrap().borrow().next.clone(); fast = fast_node.borrow().next.clone(); } } let left = head; let right = slow.unwrap().borrow().next.take(); if let Some(prev_node) = prev { prev_node.borrow_mut().next = None; } (left, right) } fn merge_two_lists( l1: Option<Rc<RefCell<ListNode>>>, l2: Option<Rc<RefCell<ListNode>>>, ) -> Option<Rc<RefCell<ListNode>>> { match (l1, l2) { (None, None) => None, (None, Some(node)) => Some(node), (Some(node), None) => Some(node), (Some(node1), Some(node2)) => { if node1.borrow().val < node2.borrow().val { let next = Self::merge_two_lists(node1.borrow_mut().next.take(), Some(node2)); node1.borrow_mut().next = next; Some(node1) } else { let next = Self::merge_two_lists(Some(node1), node2.borrow_mut().next.take()); node2.borrow_mut().next = next; Some(node2) } } } } } fn main() { // Example usage: // Create a linked list 4 -> 2 -> 1 -> 3 and sort it. let node3 = Rc::new(RefCell::new(ListNode::new(3))); let node1 = Rc::new(RefCell::new(ListNode { val: 1, next: Some(node3.clone()) })); let node2 = Rc::new(RefCell::new(ListNode { val: 2, next: Some(node1.clone()) })); let head = Some(Rc::new(RefCell::new(ListNode { val: 4, next: Some(node2.clone()) }))); let sorted_head = Solution::sort_list(head); // Print the sorted list; expected: 1 -> 2 -> 3 -> 4 let mut node = sorted_head; while let Some(n) = node { print!("{} -> ", n.borrow().val); node = n.borrow().next.clone(); } }
这段 Rust 代码实现了链表的归并排序。Solution
结构体中的 sort_list
函数是排序的主要入口,它使用递归将链表分成两半,然后对每一半进行排序,最后使用 merge_two_lists
函数将两个有序链表合并成一个有序链表。main
函数提供了一个测试用例,创建了一个链表并调用 sort_list
函数进行排序,然后打印排序后的链表。
总结
上面给出了使用不同编程语言实现的链表排序算法。这些算法主要使用了归并排序的思想,通过递归将链表分成两半,然后对每一半进行排序,最后合并两个有序链表。每种语言的实现细节略有不同,但整体思路是一致的。这些算法都能有效地对链表进行排序,并且在不同的编程语言中都有相应的数据结构和语法支持。
合并 K 个升序链表
题目要求
你需要处理一个链表数组,其中每个链表的元素都是按照升序排列的。你的任务是将这些链表合并成一个新的、同样按照升序排列的单一链表,并返回这个合并后的链表。
解题思路
要解决这个问题,你可以遵循以下步骤:
-
初始化:创建一个新的链表头节点,用于存放合并后的链表。同时,维护一个指针,用于指向当前合并链表的最后一个节点。
-
比较头节点:比较所有链表的头节点,找出最小的节点。这一步可以通过建立一个最小堆(优先队列)来实现,以便高效地获取当前最小节点。
-
链接节点:将找到的最小节点链接到合并链表的末尾,并将该节点所在链表的头指针移动到下一个节点。
-
重复步骤:重复步骤 2 和步骤 3,直到所有链表都被完全遍历,即所有链表的头节点都变为 null。
-
返回结果:返回初始化时创建的新链表头节点的下一个节点,因为头节点是一个空节点,用于方便操作。
这个问题的关键在于如何高效地找到当前最小的节点。使用最小堆可以在 O(logn)的时间复杂度内完成这一操作(n 是链表的数量)。每次操作后,你需要更新堆,这样才能在下一次比较时获取正确的最小节点。当所有链表都遍历完毕时,你就得到了一个完整的、升序排列的链表。
Golang 版本
package main
import (
"container/heap"
)
// ListNode is the definition for singly-linked list.
type ListNode struct {
Val int
Next *ListNode
}
// MinHeap is a min-heap of ListNodes.
type MinHeap []*ListNode
func (h MinHeap) Len() int { return len(h) }
func (h MinHeap) Less(i, j int) bool { return h[i].Val < h[j].Val }
func (h MinHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *MinHeap) Push(x interface{}) {
*h = append(*h, x.(*ListNode))
}
func (h *MinHeap) Pop() interface{} {
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}
func mergeKLists(lists []*ListNode) *ListNode {
minHeap := &MinHeap{}
heap.Init(minHeap)
// Add the head of each list to the min heap
for _, list := range lists {
if list != nil {
heap.Push(minHeap, list)
}
}
// Create a dummy head for the result list
dummy := &ListNode{}
current := dummy
// Extract the smallest element from the heap and add it to the result list
for minHeap.Len() > 0 {
smallest := heap.Pop(minHeap).(*ListNode)
current.Next = smallest
current = current.Next
if smallest.Next != nil {
heap.Push(minHeap, smallest.Next)
}
}
return dummy.Next
}
func main() {
// Example usage:
// Assume we have an array of linked-list heads `lists`
// mergedList := mergeKLists(lists)
// Print or process the merged list as needed
}
这段代码定义了一个最小堆,并使用标准库中的 heap
接口来管理这个堆。mergeKLists
函数首先将每个链表的头节点加入到最小堆中。然后,它不断从堆中取出最小元素,将其加入到结果链表中,并将该元素的下一个节点(如果存在)加入到堆中。这个过程一直持续到堆为空,即所有链表的节点都已经被处理。最后,函数返回合并后的链表的头节点。
C++版本
#include <iostream>
#include <vector>
#include <queue>
// Definition for singly-linked list.
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(nullptr) {}
};
struct compare {
bool operator()(const ListNode* l1, const ListNode* l2) {
return l1->val > l2->val;
}
};
ListNode* mergeKLists(std::vector<ListNode*>& lists) {
std::priority_queue<ListNode*, std::vector<ListNode*>, compare> pq;
// Initialize the priority queue with the head of each list
for (ListNode* list : lists) {
if (list) pq.push(list);
}
// Create a new head for the merged list
ListNode dummy(0);
ListNode* tail = &dummy;
// Keep pulling the smallest item from the heap and add it to the merged list
while (!pq.empty()) {
tail->next = pq.top();
pq.pop();
tail = tail->next;
if (tail->next) pq.push(tail->next);
}
return dummy.next;
}
int main() {
// Example usage:
// Assume we have a vector of ListNode pointers `lists`
// ListNode* mergedList = mergeKLists(lists);
// Print or process the merged list as needed
}
这段代码使用 C++ 的 priority_queue
来实现最小堆,以维护当前最小节点。mergeKLists
函数首先将每个链表的头节点加入到最小堆中。然后,它不断从堆中取出最小元素,将其加入到结果链表中,并将该元素的下一个节点(如果存在)加入到堆中。这个过程一直持续到堆为空,即所有链表的节点都已经被处理。最后,函数返回合并后的链表的头节点。
Python 版本
from heapq import heappush, heappop
# Definition for singly-linked list.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# The `__lt__` method is necessary for the ListNode to be put in a heap
def __lt__(self, other):
return self.val < other.val
def merge_k_lists(lists):
min_heap = []
# Initialize the heap
for head in lists:
if head:
heappush(min_heap, head)
# Create a dummy node to start the merged list
dummy = ListNode(0)
current = dummy
# Extract the smallest node from the heap and add it to the merged list
while min_heap:
node = heappop(min_heap)
current.next = node
current = current.next
if node.next:
heappush(min_heap, node.next)
return dummy.next
# Example usage:
# lists = [ListNode(1), ListNode(2), ListNode(3)] # Replace with actual lists
# merged_list = merge_k_lists(lists)
# Print or process the merged list as needed
这段代码首先定义了一个 ListNode
类,用于表示链表节点,并实现了 __lt__
方法,以便节点可以被放入 Python 的 heapq
模块提供的最小堆中。merge_k_lists
函数使用最小堆来合并链表。它首先将所有链表的头节点放入堆中,然后不断从堆中取出最小节点,并将其添加到合并后的链表中。如果取出的节点后面还有节点,就将下一个节点放入堆中。当堆为空时,所有链表都已合并完毕,函数返回合并后链表的头节点。
Java 版本
import java.util.PriorityQueue;
// Definition for singly-linked list.
public class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
public class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if (lists == null || lists.length == 0) return null;
PriorityQueue<ListNode> queue = new PriorityQueue<>(lists.length, (a, b) -> a.val - b.val);
// Add the first node of each list to the priority queue
for (ListNode node : lists) {
if (node != null) {
queue.add(node);
}
}
ListNode dummy = new ListNode(0);
ListNode tail = dummy;
while (!queue.isEmpty()) {
tail.next = queue.poll();
tail = tail.next;
if (tail.next != null) {
queue.add(tail.next);
}
}
return dummy.next;
}
}
// Example usage:
// Solution solution = new Solution();
// ListNode[] lists = new ListNode[] { /* initialize your lists here */ };
// ListNode mergedList = solution.mergeKLists(lists);
// Print or process the merged list as needed.
这段代码使用 Java 的 PriorityQueue
类来实现最小堆,以维护当前最小节点。mergeKLists
方法首先检查输入数组是否为空,然后将每个链表的头节点加入到优先队列中。然后,它不断从队列中取出最小元素,将其加入到结果链表中,并将该元素的下一个节点(如果存在)加入到队列中。这个过程一直持续到队列为空,即所有链表的节点都已经被处理。最后,方法返回合并后的链表的头节点。
Rust 版本
#![allow(unused)] fn main() { use std::cmp::Ordering; use std::collections::BinaryHeap; // Definition for singly-linked list. #[derive(PartialEq, Eq, Clone)] pub struct ListNode { pub val: i32, pub next: Option<Box<ListNode>>, } impl ListNode { #[inline] fn new(val: i32) -> Self { ListNode { next: None, val } } } // To use ListNode in a BinaryHeap, we need to implement Ord and PartialOrd. impl Ord for ListNode { fn cmp(&self, other: &Self) -> Ordering { // Flip the ordering here because BinaryHeap is a max heap, // but we want to use it as a min heap. other.val.cmp(&self.val) } } impl PartialOrd for ListNode { fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) } } pub fn merge_k_lists(lists: Vec<Option<Box<ListNode>>>) -> Option<Box<ListNode>> { let mut heap = BinaryHeap::new(); // Push the first node of each list into the heap. for list in lists { if let Some(node) = list { heap.push(node); } } // Take the smallest element from the heap and add it to the result list. let mut head = Some(Box::new(ListNode::new(0))); let mut tail = &mut head; while let Some(mut node) = heap.pop() { if let Some(next) = node.next.take() { heap.push(next); } tail.as_mut().unwrap().next = Some(node); tail = &mut tail.as_mut().unwrap().next; } head.unwrap().next } // Example usage: // let lists = vec![Some(Box::new(ListNode::new(1))), Some(Box::new(ListNode::new(2))), Some(Box::new(ListNode::new(3)))]; // Replace with actual lists // let merged_list = merge_k_lists(lists); // Print or process the merged list as needed. }
在这个 Rust 版本的代码中,我们首先定义了一个 ListNode
结构体来表示链表节点,并为其实现了 Ord
和 PartialOrd
trait,这样我们就可以将 ListNode
存入 BinaryHeap
中。由于 BinaryHeap
是一个最大堆,我们在比较函数中反转了顺序,使其表现得像一个最小堆。
merge_k_lists
函数接受一个 ListNode
的 Vec<Option<Box<ListNode>>>
,将每个链表的头节点放入堆中,然后不断从堆中取出最小节点,并将其添加到结果链表中。如果取出的节点后面还有节点,就将下一个节点放入堆中。当堆为空时,所有链表都已合并完毕,函数返回合并后链表的头节点。
总结
上面给出的解法使用了堆(优先队列)来合并多个已排序链表。无论是使用 Python、Java、C++、Rust 还是 Golang,基本思路都是一致的:将每个链表的头节点加入到堆中,然后不断从堆中取出最小节点,将其加入到结果链表中,并将该元素的下一个节点(如果存在)加入到堆中。这个过程一直持续到堆为空,即所有链表的节点都已经被处理。这种方法的时间复杂度为 O(Nlogk),其中 N 是所有链表中元素的总和,k 是链表的个数。
LRU 缓存机制
题目要求
设计一个数据结构,它能够满足最近最少使用(LRU)缓存的要求。这个数据结构应该支持以下操作:
LRUCache(int capacity)
:通过给定的正整数容量capacity
初始化 LRU 缓存。int get(int key)
:如果缓存中存在键key
,则返回其对应的值;如果不存在,则返回-1
。void put(int key, int value)
:如果键key
已经存在,则更新其对应的值value
;如果不存在,则将键值对key-value
插入缓存。如果插入新的键值对导致缓存项的数量超过了capacity
,则需要按照 LRU 策略移除最久未使用的键值对。
这两个函数get
和put
的平均时间复杂度必须是 O(1)。
解题思路
为了满足题目要求,我们需要使用一种能够快速插入、删除和更新元素的数据结构,同时还要能够快速访问到每个元素。考虑到这些需求,我们可以使用哈希表和双向链表的组合来实现 LRU 缓存。
哈希表
哈希表支持平均时间复杂度为 O(1)的元素查找,这使得我们能够快速判断一个元素是否存在于缓存中,以及快速访问到该元素。
双向链表
双向链表支持 O(1)时间复杂度的元素添加、删除操作。通过维护一个双向链表,我们可以保证插入的元素总是在链表的头部,而每次访问元素时,我们可以将其移动到链表头部,这样就能保证链表尾部始终是最久未使用的元素。
LRU 缓存的实现
-
初始化:创建一个哈希表和一个双向链表。哈希表用于存储键和其在双向链表中对应节点的地址,双向链表中的每个节点包含键值对。
-
获取元素 (
get
操作):- 如果键存在于哈希表中,则返回对应节点的值,并将该节点移动到双向链表的头部。
- 如果键不存在,返回
-1
。
-
插入/更新元素 (
put
操作):- 如果键已存在于哈希表中,更新对应节点的值,并将该节点移动到双向链表的头部。
- 如果键不存在:
- 如果当前缓存未满,直接在双向链表头部插入新节点,并在哈希表中添加键和地址的映射。
- 如果当前缓存已满,删除双向链表尾部节点,并移除哈希表中对应的映射,然后在双向链表头部插入新节点,并在哈希表中添加键和地址的映射。
通过以上步骤,我们可以确保get
和put
操作都能在 O(1)的时间复杂度内完成,满足 LRU 缓存的要求。
Golang 版本
package main
import (
"container/list"
)
type LRUCache struct {
capacity int
cache map[int]*list.Element
list *list.List
}
type Pair struct {
key int
value int
}
func Constructor(capacity int) LRUCache {
return LRUCache{
capacity: capacity,
cache: make(map[int]*list.Element),
list: list.New(),
}
}
func (this *LRUCache) Get(key int) int {
if element, found := this.cache[key]; found {
this.list.MoveToFront(element)
return element.Value.(Pair).value
}
return -1
}
func (this *LRUCache) Put(key int, value int) {
if element, found := this.cache[key]; found {
this.list.MoveToFront(element)
element.Value = Pair{key, value}
} else {
if this.list.Len() == this.capacity {
oldest := this.list.Back()
delete(this.cache, oldest.Value.(Pair).key)
this.list.Remove(oldest)
}
this.cache[key] = this.list.PushFront(Pair{key, value})
}
}
func main() {
// Example of usage:
// cache := Constructor(2)
// cache.Put(1, 1)
// cache.Put(2, 2)
// fmt.Println(cache.Get(1)) // returns 1
// cache.Put(3, 3) // evicts key 2
// fmt.Println(cache.Get(2)) // returns -1 (not found)
// cache.Put(4, 4) // evicts key 1
// fmt.Println(cache.Get(1)) // returns -1 (not found)
// fmt.Println(cache.Get(3)) // returns 3
// fmt.Println(cache.Get(4)) // returns 4
}
这段代码定义了一个LRUCache
结构体,它包含一个整数型的capacity
表示缓存的容量,一个映射cache
用于存储键和对应链表节点的指针,以及一个list
表示双向链表。Pair
结构体用于存储键值对。
Constructor
函数用于初始化一个LRUCache
实例。Get
和Put
方法分别用于获取和添加键值对到缓存中,同时维护 LRU 顺序。如果缓存已满,Put
方法会移除最久未使用的元素。
在main
函数中,你可以按照注释中的示例来使用这个 LRUCache 类。
C++版本
#include <unordered_map>
#include <list>
#include <utility>
class LRUCache {
public:
LRUCache(int capacity) : _capacity(capacity) {}
int get(int key) {
auto it = _cache.find(key);
if (it == _cache.end()) {
return -1;
}
// Move the accessed item to the front of the list (head)
_items.splice(_items.begin(), _items, it->second);
return it->second->second;
}
void put(int key, int value) {
auto it = _cache.find(key);
if (it != _cache.end()) {
// Update the value
it->second->second = value;
// Move the updated item to the front of the list (head)
_items.splice(_items.begin(), _items, it->second);
return;
}
if (_cache.size() == _capacity) {
// Remove the least recently used item
auto last = _items.end();
last--;
_cache.erase(last->first);
_items.pop_back();
}
// Insert the new item at the front of the list (head)
_items.push_front(std::make_pair(key, value));
_cache[key] = _items.begin();
}
private:
int _capacity;
std::list<std::pair<int, int>> _items; // Pair of key and value
std::unordered_map<int, std::list<std::pair<int, int>>::iterator> _cache; // Map to store key and iterator to the list node
};
int main() {
// Example of usage:
// LRUCache cache(2);
// cache.put(1, 1);
// cache.put(2, 2);
// std::cout << cache.get(1) << std::endl; // returns 1
// cache.put(3, 3); // evicts key 2
// std::cout << cache.get(2) << std::endl; // returns -1 (not found)
// cache.put(4, 4); // evicts key 1
// std::cout << cache.get(1) << std::endl; // returns -1 (not found)
// std::cout << cache.get(3) << std::endl; // returns 3
// std::cout << cache.get(4) << std::endl; // returns 4
return 0;
}
这段 C++代码实现了一个 LRUCache 类,它使用一个std::list
来存储键值对,并使用一个std::unordered_map
来快速定位这些键值对在列表中的位置。get
方法用于获取键对应的值,如果键存在于缓存中,则将其移动到列表的前端。put
方法用于插入或更新键值对,如果键已存在,则更新其值并移动到列表前端;如果键不存在,则插入新的键值对到前端,如果此时缓存已满,则会移除最后端的键值对。这样可以确保最近最少使用的元素总是在列表的尾部,从而实现 LRU 缓存淘汰策略。
Python 版本
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity: int):
self.cache = OrderedDict()
self.capacity = capacity
def get(self, key: int) -> int:
if key not in self.cache:
return -1
else:
self.cache.move_to_end(key) # Move the key to the end to show that it was recently used
return self.cache[key]
def put(self, key: int, value: int) -> None:
if key in self.cache:
self.cache.move_to_end(key) # Move the key to the end to show that it was recently used
self.cache[key] = value
if len(self.cache) > self.capacity:
self.cache.popitem(last=False) # Pop the first item (least recently used)
# Example of usage:
# cache = LRUCache(2)
# cache.put(1, 1)
# cache.put(2, 2)
# print(cache.get(1)) # returns 1
# cache.put(3, 3) # evicts key 2
# print(cache.get(2)) # returns -1 (not found)
# cache.put(4, 4) # evicts key 1
# print(cache.get(1)) # returns -1 (not found)
# print(cache.get(3)) # returns 3
# print(cache.get(4)) # returns 4
这段 Python 代码使用了collections.OrderedDict
来实现 LRU 缓存。OrderedDict
记住了元素插入的顺序,因此可以用来实现 LRU(最近最少使用)缓存。当一个元素被访问时,它会被移动到OrderedDict
的末尾,表示这个元素是最近被使用过的。当缓存超出容量时,OrderedDict
的第一个元素(即最久未使用的元素)会被移除。
Java 版本
import java.util.HashMap;
public class LRUCache {
private HashMap<Integer, Node> map;
private DoubleList cache;
private int capacity;
public LRUCache(int capacity) {
this.capacity = capacity;
map = new HashMap<>();
cache = new DoubleList();
}
public int get(int key) {
if (!map.containsKey(key)) {
return -1;
}
int val = map.get(key).val;
// 利用 put 方法把该数据提前
put(key, val);
return val;
}
public void put(int key, int value) {
// 先把新节点 x 做出来
Node x = new Node(key, value);
if (map.containsKey(key)) {
// 删除旧的节点,新的插到头部
cache.remove(map.get(key));
cache.addFirst(x);
// 更新 map 中对应的数据
map.put(key, x);
} else {
if (capacity == cache.size()) {
// 删除链表最后一个数据
Node last = cache.removeLast();
map.remove(last.key);
}
// 直接添加到头部
cache.addFirst(x);
map.put(key, x);
}
}
private class Node {
public int key, val;
public Node next, prev;
public Node(int key, int val) {
this.key = key;
this.val = val;
}
}
private class DoubleList {
private Node head, tail; // 头尾虚节点
private int size; // 链表元素数
public DoubleList() {
// 初始化双向链表的数据
head = new Node(0, 0);
tail = new Node(0, 0);
head.next = tail;
tail.prev = head;
size = 0;
}
// 在链表头部添加节点 x,时间 O(1)
public void addFirst(Node x) {
x.next = head.next;
x.prev = head;
head.next.prev = x;
head.next = x;
size++;
}
// 删除链表中的 x 节点(x 一定存在)
// 由于是双链表且给的是目标 Node 节点,时间 O(1)
public void remove(Node x) {
x.prev.next = x.next;
x.next.prev = x.prev;
size--;
}
// 删除链表中最后一个节点,并返回该节点,时间 O(1)
public Node removeLast() {
if (tail.prev == head)
return null;
Node last = tail.prev;
remove(last);
return last;
}
// 返回链表长度,时间 O(1)
public int size() { return size; }
}
}
// Example of usage:
// LRUCache cache = new LRUCache(2);
// cache.put(1, 1);
// cache.put(2, 2);
// System.out.println(cache.get(1)); // returns 1
// cache.put(3, 3); // evicts key 2
// System.out.println(cache.get(2)); // returns -1 (not found)
// cache.put(4, 4); // evicts key 1
// System.out.println(cache.get(1)); // returns -1 (not found)
// System.out.println(cache.get(3)); // returns 3
// System.out.println(cache.get(4)); // returns 4
这段 Java 代码实现了一个 LRUCache 类,它包含一个内部的 Node 类来表示双向链表的节点,以及一个内部的 DoubleList 类来表示双向链表。LRUCache 使用一个 HashMap 来存储键和对应节点的映射,以及一个 DoubleList 来实现 O(1)时间复杂度的节点添加和删除。当访问或添加一个元素时,如果元素已存在,它会被移动到双向链表的头部;如果元素不存在,它会被添加到头部,如果此时缓存已满,则会从链表尾部移除最久未使用的元素。
Rust 版本
use std::collections::HashMap; struct LRUCache { capacity: usize, cache: HashMap<i32, Box<DLinkedNode>>, head: Box<DLinkedNode>, tail: Box<DLinkedNode>, size: usize, } struct DLinkedNode { key: i32, value: i32, prev: Option<i32>, next: Option<i32>, } impl DLinkedNode { fn new(key: i32, value: i32) -> Self { DLinkedNode { key, value, prev: None, next: None, } } } impl LRUCache { fn new(capacity: usize) -> Self { let head = Box::new(DLinkedNode::new(-1, -1)); let tail = Box::new(DLinkedNode::new(-1, -1)); let mut lru = LRUCache { capacity, cache: HashMap::with_capacity(capacity), head, tail, size: 0, }; lru.head.next = Some(lru.tail.key); lru.tail.prev = Some(lru.head.key); lru } fn get(&mut self, key: i32) -> i32 { match self.cache.get_mut(&key) { Some(node) => { self.move_to_head(node); node.value }, None => -1, } } fn put(&mut self, key: i32, value: i32) { match self.cache.get_mut(&key) { Some(node) => { node.value = value; self.move_to_head(node); }, None => { let mut node = Box::new(DLinkedNode::new(key, value)); self.cache.insert(key, node); self.add_node(&mut node); self.size += 1; if self.size > self.capacity { let tail = self.pop_tail(); self.cache.remove(&tail.key); self.size -= 1; } }, } } fn add_node(&mut self, node: &mut Box<DLinkedNode>) { node.prev = Some(self.head.key); node.next = self.head.next; if let Some(next_key) = self.head.next { if let Some(next_node) = self.cache.get_mut(&next_key) { next_node.prev = Some(node.key); } } self.head.next = Some(node.key); } fn remove_node(&mut self, node: &Box<DLinkedNode>) { if let Some(prev_key) = node.prev { if let Some(prev_node) = self.cache.get_mut(&prev_key) { prev_node.next = node.next; } } if let Some(next_key) = node.next { if let Some(next_node) = self.cache.get_mut(&next_key) { next_node.prev = node.prev; } } } fn move_to_head(&mut self, node: &mut Box<DLinkedNode>) { self.remove_node(node); self.add_node(node); } fn pop_tail(&mut self) -> Box<DLinkedNode> { let tail_key = self.tail.prev.unwrap(); let mut tail_node = self.cache.remove(&tail_key).unwrap(); self.remove_node(&tail_node); tail_node } } fn main() { let mut cache = LRUCache::new(2); cache.put(1, 1); cache.put(2, 2); println!("{}", cache.get(1)); // returns 1 cache.put(3, 3); // evicts key 2 println!("{}", cache.get(2)); // returns -1 (not found) cache.put(4, 4); // evicts key 1 println!("{}", cache.get(1)); // returns -1 (not found) println!("{}", cache.get(3)); // returns 3 println!("{}", cache.get(4)); // returns 4 }
这段 Rust 代码实现了一个 LRUCache 结构体,它包含一个 HashMap 来存储键和对应的双向链表节点,以及两个哨兵节点 head 和 tail 来标记双向链表的边界。LRUCache 支持 get 和 put 操作,它们都能在 O(1)时间复杂度内完成。当一个节点被访问或添加时,它会被移动到双向链表的头部。如果在添加一个新节点时超出了容量,它会从链表的尾部移除最不常用的节点。
总结
上面给出了使用哈希表和双向链表实现 LRU 缓存的代码。这种实现方式能够满足 LRU 缓存的要求,包括在 O(1)时间复杂度内完成 get 和 put 操作,并且能够按照 LRU 策略移除最久未使用的键值对。
具体实现包括以下几个关键点:
- 使用哈希表存储键值对,以支持 O(1)时间复杂度的元素查找和访问。
- 使用双向链表来维护键值对的顺序,确保最近访问的元素总是在链表头部,而最久未使用的元素总是在链表尾部。
- 在 get 操作中,如果元素存在,则将其移动到链表头部;在 put 操作中,如果元素存在则更新其值并移动到链表头部,如果不存在则插入新的键值对并检查是否需要移除最久未使用的元素。
这种实现方式能够高效地满足 LRU 缓存的要求,并且在实际应用中被广泛使用。
二叉树
解决关于二叉树的算法题通常可以通过递归或迭代的方式来处理。二叉树的问题大多可以归类为以下几种类型:
- 遍历问题:前序(根-左-右)、中序(左-根-右)、后序(左-右-根)和层次遍历。
- 属性问题:比如计算树的高度、判断二叉树是否平衡等。
- 路径问题:比如寻找从根到叶子的路径之和等。
- 结构问题:比如翻转二叉树、合并二叉树等。
- 动态规划问题:比如二叉树中的最大路径和。
下面我会给出几个 Go 语言的代码实例来说明这些通用思路。
1. 遍历问题 - 中序遍历
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
func inorderTraversal(root *TreeNode) []int {
var res []int
var inorder func(node *TreeNode)
inorder = func(node *TreeNode) {
if node == nil {
return
}
inorder(node.Left)
res = append(res, node.Val)
inorder(node.Right)
}
inorder(root)
return res
}
2. 属性问题 - 计算树的高度
func maxDepth(root *TreeNode) int {
if root == nil {
return 0
}
leftDepth := maxDepth(root.Left)
rightDepth := maxDepth(root.Right)
if leftDepth > rightDepth {
return leftDepth + 1
}
return rightDepth + 1
}
3. 路径问题 - 根到叶子的路径之和
func hasPathSum(root *TreeNode, sum int) bool {
if root == nil {
return false
}
if root.Left == nil && root.Right == nil {
return sum == root.Val
}
return hasPathSum(root.Left, sum-root.Val) || hasPathSum(root.Right, sum-root.Val)
}
4. 结构问题 - 翻转二叉树
func invertTree(root *TreeNode) *TreeNode {
if root == nil {
return nil
}
root.Left, root.Right = invertTree(root.Right), invertTree(root.Left)
return root
}
5. 动态规划问题 - 二叉树中的最大路径和
func maxPathSum(root *TreeNode) int {
maxSum := math.MinInt32
var maxGain func(node *TreeNode) int
maxGain = func(node *TreeNode) int {
if node == nil {
return 0
}
leftGain := max(0, maxGain(node.Left))
rightGain := max(0, maxGain(node.Right))
priceNewpath := node.Val + leftGain + rightGain
maxSum = max(maxSum, priceNewpath)
return node.Val + max(leftGain, rightGain)
}
maxGain(root)
return maxSum
}
func max(x, y int) int {
if x > y {
return x
}
return y
}
在解决二叉树的问题时,递归是一种非常自然和强大的方法,因为二叉树本身的结构就是递归定义的。迭代方法通常需要使用栈来模拟递归过程。在实际编码时,应该选择最适合问题的方法。
二叉树的中序遍历
题目要求
设计一个算法,实现对一棵给定的二叉树进行中序遍历,并返回遍历的结果。中序遍历的顺序是先遍历左子树,然后访问根节点,最后遍历右子树。
解题思路
中序遍历是二叉树遍历方式之一,具体的遍历步骤如下:
-
递归遍历左子树:从根节点开始,递归进入其左子节点,继续进行中序遍历,直到到达最左侧的节点。
-
访问根节点:在递归的过程中,当左子树被完全遍历后,访问当前的根节点。
-
递归遍历右子树:完成根节点的访问后,递归进入其右子节点,对右子树进行中序遍历。
这个过程可以通过递归实现,也可以通过迭代实现。递归实现较为直观,但是迭代实现可以避免递归带来的栈溢出问题,特别是在处理大型二叉树时。
递归实现的基本步骤:
-
如果当前节点为空,返回空列表(递归终止条件)。
-
递归调用左子节点,获取左子树的中序遍历结果。
-
将当前节点的值加入遍历结果列表中。
-
递归调用右子节点,获取右子树的中序遍历结果。
-
将左子树的遍历结果、当前节点值、右子树的遍历结果依次合并,返回最终的遍历结果列表。
迭代实现的基本步骤:
-
创建一个空栈和一个结果列表。
-
初始化一个指针,指向根节点。
-
当指针非空或栈非空时,执行以下操作:
a. 将指针指向的节点及其所有左子节点压入栈中,然后将指针指向最左侧节点的左子节点。
b. 当指针为空时,说明已经到达最左侧节点,此时从栈中弹出一个节点,将其值加入结果列表,并将指针指向该节点的右子节点。
c. 重复步骤 a 和 b,直到栈为空且指针为空。
-
返回结果列表,即为二叉树的中序遍历结果。
在实现时,需要注意处理好递归的终止条件以及迭代时栈的使用,确保所有节点都能按照中序遍历的顺序被访问到。
Golang 版本
package main
import "fmt"
// TreeNode is the structure for binary tree nodes
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
// inorderTraversal recursively traverses the tree in inorder and returns the traversal as a slice
func inorderTraversal(root *TreeNode) []int {
var result []int
var inorder func(node *TreeNode)
inorder = func(node *TreeNode) {
if node == nil {
return
}
inorder(node.Left) // Traverse left subtree
result = append(result, node.Val) // Visit node
inorder(node.Right) // Traverse right subtree
}
inorder(root)
return result
}
func main() {
// Example usage:
// Construct a binary tree: 1
// / \
// 2 3
// / / \
// 4 5 6
root := &TreeNode{1,
&TreeNode{2,
&TreeNode{4, nil, nil},
nil},
&TreeNode{3,
&TreeNode{5, nil, nil},
&TreeNode{6, nil, nil}}}
result := inorderTraversal(root)
fmt.Println(result) // Output: [4 2 1 5 3 6]
}
这段代码定义了一个TreeNode
结构体,用于构建二叉树,并实现了inorderTraversal
函数,该函数接收一个二叉树的根节点,并返回一个整数切片,包含了树的中序遍历结果。在main
函数中,我们构建了一个简单的二叉树,并调用inorderTraversal
函数来获取和打印中序遍历的结果。
C++版本
#include <iostream>
#include <vector>
#include <stack>
// Definition for a binary tree node.
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
class Solution {
public:
std::vector<int> inorderTraversal(TreeNode* root) {
std::vector<int> result;
std::stack<TreeNode*> stack;
TreeNode* current = root;
while (current != nullptr || !stack.empty()) {
// Reach the left most Node of the current Node
while (current != nullptr) {
stack.push(current);
current = current->left;
}
// Current must be nullptr at this point
current = stack.top();
stack.pop();
result.push_back(current->val); // Add the node value to result
// We have visited the node and its left subtree.
// Now, it's right subtree's turn
current = current->right;
}
return result;
}
};
int main() {
// Example usage:
// Construct a binary tree: 1
// / \
// 2 3
// / / \
// 4 5 6
TreeNode *root = new TreeNode(1);
root->left = new TreeNode(2);
root->left->left = new TreeNode(4);
root->right = new TreeNode(3);
root->right->left = new TreeNode(5);
root->right->right = new TreeNode(6);
Solution solution;
std::vector<int> result = solution.inorderTraversal(root);
for (int val : result) {
std::cout << val << " ";
}
std::cout << std::endl;
// Output: 4 2 1 5 3 6
// Clean up allocated nodes
delete root->left->left;
delete root->left;
delete root->right->left;
delete root->right->right;
delete root->right;
delete root;
return 0;
}
这段 C++代码定义了一个TreeNode
结构体,用于构建二叉树,并实现了一个Solution
类,其中包含了inorderTraversal
方法。这个方法使用了迭代的方式来进行中序遍历,利用了一个栈来存储节点。在main
函数中,我们构建了一个简单的二叉树,并调用inorderTraversal
方法来获取和打印中序遍历的结果。最后,代码中还包含了对动态分配的节点进行清理的步骤,以防止内存泄漏。
Python 版本
class TreeNode:
def __init__(self, x):
self.val = x
self.left = None
self.right = None
class Solution:
def inorderTraversal(self, root):
result, stack = [], []
current = root
while current or stack:
# Reach the left most Node of the current Node
while current:
stack.append(current)
current = current.left
# Current must be None at this point
current = stack.pop()
result.append(current.val) # Add the node value to result
# We have visited the node and its left subtree.
# Now, it's right subtree's turn
current = current.right
return result
# Example usage:
if __name__ == "__main__":
# Construct a binary tree: 1
# / \
# 2 3
# / / \
# 4 5 6
root = TreeNode(1)
root.left = TreeNode(2)
root.left.left = TreeNode(4)
root.right = TreeNode(3)
root.right.left = TreeNode(5)
root.right.right = TreeNode(6)
solution = Solution()
result = solution.inorderTraversal(root)
print(result) # Output: [4, 2, 1, 5, 3, 6]
这段 Python 代码定义了一个TreeNode
类,用于构建二叉树,以及一个Solution
类,其中包含了inorderTraversal
方法。这个方法使用了迭代的方式来进行中序遍历,利用了一个栈来存储节点。在if __name__ == "__main__":
部分,我们构建了一个简单的二叉树,并调用inorderTraversal
方法来获取和打印中序遍历的结果。
Java 版本
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
// Definition for a binary tree node.
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode current = root;
while (current != null || !stack.isEmpty()) {
while (current != null) {
stack.push(current);
current = current.left;
}
current = stack.pop();
result.add(current.val);
current = current.right;
}
return result;
}
}
public class Main {
public static void main(String[] args) {
// Example usage:
// Construct a binary tree: 1
// / \
// 2 3
// / / \
// 4 5 6
TreeNode root = new TreeNode(1);
root.left = new TreeNode(2);
root.left.left = new TreeNode(4);
root.right = new TreeNode(3);
root.right.left = new TreeNode(5);
root.right.right = new TreeNode(6);
Solution solution = new Solution();
List<Integer> result = solution.inorderTraversal(root);
for (int val : result) {
System.out.print(val + " ");
}
// Output: 4 2 1 5 3 6
}
}
这段 Java 代码定义了一个TreeNode
类,用于构建二叉树,以及一个Solution
类,其中包含了inorderTraversal
方法。这个方法使用了迭代的方式来进行中序遍历,利用了一个栈来存储节点。在Main
类的main
方法中,我们构建了一个简单的二叉树,并调用inorderTraversal
方法来获取和打印中序遍历的结果。
Rust 版本
use std::cell::RefCell; use std::rc::Rc; // Definition for a binary tree node. #[derive(Debug, PartialEq, Eq)] pub struct TreeNode { pub val: i32, pub left: Option<Rc<RefCell<TreeNode>>>, pub right: Option<Rc<RefCell<TreeNode>>>, } impl TreeNode { #[inline] pub fn new(val: i32) -> Self { TreeNode { val, left: None, right: None, } } } pub struct Solution {} impl Solution { pub fn inorder_traversal(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<i32> { let mut result = Vec::new(); let mut stack = Vec::new(); let mut r = root.clone(); while r.is_some() || !stack.is_empty() { while let Some(node) = r { stack.push(node.clone()); r = node.borrow().left.clone(); } r = stack.pop(); if let Some(node) = r { result.push(node.borrow().val); r = node.borrow().right.clone(); } } result } } fn main() { // Example usage: // Construct a binary tree: 1 // / \ // 2 3 // / / \ // 4 5 6 let root = Some(Rc::new(RefCell::new(TreeNode::new(1)))); let left = Some(Rc::new(RefCell::new(TreeNode::new(2)))); let right = Some(Rc::new(RefCell::new(TreeNode::new(3)))); let left_left = Some(Rc::new(RefCell::new(TreeNode::new(4)))); let right_left = Some(Rc::new(RefCell::new(TreeNode::new(5)))); let right_right = Some(Rc::new(RefCell::new(TreeNode::new(6)))); if let Some(ref root_node) = root { root_node.borrow_mut().left = left; root_node.borrow_mut().right = right; if let Some(ref left_node) = root_node.borrow().left { left_node.borrow_mut().left = left_left; } if let Some(ref right_node) = root_node.borrow().right { right_node.borrow_mut().left = right_left; right_node.borrow_mut().right = right_right; } } let result = Solution::inorder_traversal(root); println!("{:?}", result); // Output: [4, 2, 1, 5, 3, 6] }
这段 Rust 代码定义了一个TreeNode
结构体,用于构建二叉树,以及一个Solution
结构体,其中包含了inorder_traversal
方法。这个方法使用了迭代的方式来进行中序遍历,利用了一个向量作为栈来存储节点。在main
函数中,我们构建了一个简单的二叉树,并调用inorder_traversal
方法来获取和打印中序遍历的结果。
总结
上面给出了使用不同编程语言(包括 Golang、C++、Python、Java 和 Rust)实现二叉树中序遍历的代码示例。这些示例代码展示了如何使用递归或迭代的方式来遍历二叉树,并将遍历结果存储在一个数组或向量中。每种语言的代码都遵循了相似的逻辑:从根节点开始,先遍历左子树,然后访问根节点,最后遍历右子树。这些示例为理解和实现二叉树的中序遍历提供了参考。
二叉树的最大深度
题目要求
编写一个函数,该函数接收一个二叉树的根节点 root
作为参数。你需要计算并返回这棵二叉树的最大深度。
二叉树的最大深度定义为从根节点到最远叶子节点的最长路径上的节点数量。
解题思路
要解决这个问题,我们可以采用递归的方法。递归是一种通过函数自己调用自己来重复解决问题的方法。对于二叉树的最大深度问题,我们可以这样考虑:
-
基本情况:如果二叉树为空,即
root
为null
,那么其最大深度为 0。 -
递归步骤:如果二叉树不为空,我们需要分别计算左子树和右子树的最大深度。这可以通过递归地调用我们的函数来完成。
-
合并结果:一旦我们得到了左子树和右子树的最大深度,二叉树的最大深度就是左右子树深度的最大值加 1(加的 1 是因为我们需要加上根节点本身)。
具体步骤如下:
- 对于每个节点,我们都递归地求其左右子节点的最大深度。
- 对于每个非空节点,其最大深度是其左右子节点的最大深度的较大者加 1。
- 对于空节点,返回深度 0。
- 递归直到所有的叶子节点,叶子节点的最大深度为 1。
- 最后,返回根节点的最大深度,这就是整棵树的最大深度。
通过这种方式,我们可以自底向上地计算出每个节点的最大深度,最终得到整棵树的最大深度。
Golang 版本
package main
import (
"fmt"
)
// TreeNode is a struct for a binary tree node.
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
// maxDepth takes a TreeNode and returns the max depth of the tree.
func maxDepth(root *TreeNode) int {
if root == nil {
return 0
}
leftDepth := maxDepth(root.Left)
rightDepth := maxDepth(root.Right)
if leftDepth > rightDepth {
return leftDepth + 1
}
return rightDepth + 1
}
func main() {
// Example usage:
// Construct a binary tree as an example.
// 3
// / \
// 9 20
// / \
// 15 7
root := &TreeNode{3, &TreeNode{9, nil, nil}, &TreeNode{20, &TreeNode{15, nil, nil}, &TreeNode{7, nil, nil}}}
fmt.Println("Max depth of the tree:", maxDepth(root))
}
这段代码定义了一个二叉树节点的结构体TreeNode
,并实现了一个maxDepth
函数来计算二叉树的最大深度。main
函数中给出了一个示例二叉树,并调用maxDepth
函数来输出其最大深度。
C++版本
#include <iostream>
#include <algorithm>
// Definition for a binary tree node.
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution {
public:
int maxDepth(TreeNode* root) {
if (root == NULL) {
return 0;
} else {
int left_depth = maxDepth(root->left);
int right_depth = maxDepth(root->right);
return std::max(left_depth, right_depth) + 1;
}
}
};
int main() {
// Example usage:
TreeNode *root = new TreeNode(3);
root->left = new TreeNode(9);
root->right = new TreeNode(20);
root->right->left = new TreeNode(15);
root->right->right = new TreeNode(7);
Solution solution;
std::cout << "Max depth of the tree: " << solution.maxDepth(root) << std::endl;
// Clean up allocated nodes if necessary
delete root->right->right;
delete root->right->left;
delete root->right;
delete root->left;
delete root;
return 0;
}
这段代码定义了一个二叉树节点的结构体TreeNode
,并实现了一个Solution
类,其中包含了一个maxDepth
成员函数来计算二叉树的最大深度。main
函数中创建了一个示例二叉树,并调用maxDepth
函数来输出其最大深度。最后,代码中还包含了适当的内存清理操作。
Python 版本
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class Solution:
def maxDepth(self, root: TreeNode) -> int:
if not root:
return 0
else:
left_depth = self.maxDepth(root.left)
right_depth = self.maxDepth(root.right)
return max(left_depth, right_depth) + 1
# Example usage:
if __name__ == "__main__":
# Construct a binary tree as an example.
# 3
# / \
# 9 20
# / \
# 15 7
root = TreeNode(3)
root.left = TreeNode(9)
root.right = TreeNode(20)
root.right.left = TreeNode(15)
root.right.right = TreeNode(7)
solution = Solution()
print("Max depth of the tree:", solution.maxDepth(root))
这段代码定义了一个二叉树节点的类TreeNode
,以及一个解决方案类Solution
,其中包含了一个maxDepth
方法来计算二叉树的最大深度。在if __name__ == "__main__":
块中,我们创建了一个示例二叉树,并调用maxDepth
方法来输出其最大深度。
Java 版本
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
} else {
int leftDepth = maxDepth(root.left);
int rightDepth = maxDepth(root.right);
return Math.max(leftDepth, rightDepth) + 1;
}
}
}
public class Main {
public static void main(String[] args) {
// Example usage:
TreeNode root = new TreeNode(3);
root.left = new TreeNode(9);
root.right = new TreeNode(20);
root.right.left = new TreeNode(15);
root.right.right = new TreeNode(7);
Solution solution = new Solution();
System.out.println("Max depth of the tree: " + solution.maxDepth(root));
}
}
这段代码定义了一个二叉树节点的类TreeNode
,以及一个解决方案类Solution
,其中包含了一个maxDepth
方法来计算二叉树的最大深度。在Main
类的main
方法中,我们创建了一个示例二叉树,并调用maxDepth
方法来输出其最大深度。
Rust 版本
// Definition for a binary tree node. #[derive(Debug, PartialEq, Eq)] pub struct TreeNode { pub val: i32, pub left: Option<Rc<RefCell<TreeNode>>>, pub right: Option<Rc<RefCell<TreeNode>>>, } impl TreeNode { #[inline] pub fn new(val: i32) -> Self { TreeNode { val, left: None, right: None, } } } use std::rc::Rc; use std::cell::RefCell; use std::cmp::max; impl Solution { pub fn max_depth(root: Option<Rc<RefCell<TreeNode>>>) -> i32 { match root { Some(node) => { let left_depth = Solution::max_depth(node.borrow().left.clone()); let right_depth = Solution::max_depth(node.borrow().right.clone()); max(left_depth, right_depth) + 1 }, None => 0, } } } pub struct Solution; fn main() { // Example usage: let root = Some(Rc::new(RefCell::new(TreeNode::new(3)))); let left = Some(Rc::new(RefCell::new(TreeNode::new(9)))); let right = Some(Rc::new(RefCell::new(TreeNode::new(20)))); let left_leaf = Some(Rc::new(RefCell::new(TreeNode::new(15)))); let right_leaf = Some(Rc::new(RefCell::new(TreeNode::new(7)))); if let Some(ref root_node) = root { root_node.borrow_mut().left = left; root_node.borrow_mut().right = right; if let Some(ref right_node) = root_node.borrow().right { right_node.borrow_mut().left = left_leaf; right_node.borrow_mut().right = right_leaf; } } println!("Max depth of the tree: {}", Solution::max_depth(root)); }
这段代码首先定义了一个二叉树节点的结构体TreeNode
,以及一个解决方案结构体Solution
,其中包含了一个max_depth
静态方法来计算二叉树的最大深度。在main
函数中,我们创建了一个示例二叉树,并调用max_depth
方法来输出其最大深度。在 Rust 中,我们通常使用Rc<RefCell<T>>
来共享和修改可变数据,这在树结构中尤其常见。
总结
上面给出了使用不同编程语言(包括 Golang、C++、Python、Java 和 Rust)实现计算二叉树最大深度的解法。这些解法都采用了递归的方式来解决问题。递归的基本思路是对于每个节点,分别计算其左右子节点的最大深度,然后取两者中的较大值加 1 作为当前节点的最大深度。如果节点为空,则返回深度 0。这种递归的方式能够自底向上地计算出每个节点的最大深度,最终得到整棵树的最大深度。虽然使用了不同的编程语言,但基本的解题思路是一致的,这展示了递归在解决二叉树问题时的普适性。
翻转二叉树
题目要求
你需要编写一个函数,该函数接收一个二叉树的根节点 root
作为参数。你的任务是将这棵二叉树进行翻转,即交换每个节点的左右子节点,然后返回翻转后的二叉树的根节点。
解题思路
翻转二叉树是一道经典的二叉树问题,可以通过递归或迭代的方式来解决。以下是解题的基本思路:
-
递归法:
- 检查当前节点是否为空,如果为空,则不需要进行任何操作。
- 递归地对当前节点的左子树进行翻转。
- 递归地对当前节点的右子树进行翻转。
- 交换当前节点的左右子节点。
- 最后返回当前节点(现在它已经成为了翻转后的根节点)。
-
迭代法:
- 使用一个栈或队列来存储所有需要交换子节点的树节点。
- 将根节点放入栈或队列中。
- 当栈或队列不为空时,循环执行以下操作:
- 取出一个节点。
- 交换这个节点的左右子节点。
- 如果左子节点不为空,将它加入栈或队列。
- 如果右子节点不为空,将它加入栈或队列。
- 继续这个过程直到栈或队列为空,这时所有节点都已经翻转。
- 返回最初放入栈或队列的根节点,它现在是翻转后的根节点。
在实际编码时,递归法的代码通常更简洁,但是迭代法在处理非常深的树时可能会有更好的性能,因为它不会受到调用栈大小的限制。
Golang 版本
package main
import "fmt"
// TreeNode is a structure for tree nodes
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
// invertTree function inverts a binary tree
func invertTree(root *TreeNode) *TreeNode {
if root == nil {
return nil
}
// Swap the left and right children
root.Left, root.Right = root.Right, root.Left
// Recursively invert the left subtree
invertTree(root.Left)
// Recursively invert the right subtree
invertTree(root.Right)
return root
}
// Helper function to print the tree in-order (for verification)
func printTree(root *TreeNode) {
if root == nil {
return
}
printTree(root.Left)
fmt.Print(root.Val, " ")
printTree(root.Right)
}
func main() {
// Example usage:
// Constructing a binary tree:
// 1
// / \
// 2 3
// / \ / \
// 4 5 6 7
root := &TreeNode{1,
&TreeNode{2,
&TreeNode{4, nil, nil},
&TreeNode{5, nil, nil}},
&TreeNode{3,
&TreeNode{6, nil, nil},
&TreeNode{7, nil, nil}}}
fmt.Println("Original tree in-order:")
printTree(root)
fmt.Println()
invertedRoot := invertTree(root)
fmt.Println("Inverted tree in-order:")
printTree(invertedRoot)
fmt.Println()
}
这段代码定义了一个TreeNode
结构体,实现了一个invertTree
函数来翻转二叉树,并提供了一个printTree
辅助函数来按顺序打印树的节点值,以便验证翻转是否正确。main
函数中创建了一个简单的二叉树,并展示了翻转前后的结果。
C++版本
#include <iostream>
using namespace std;
// Definition for a binary tree node.
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
if (root == nullptr) {
return nullptr;
}
// Swap the left and right children
TreeNode* temp = root->left;
root->left = root->right;
root->right = temp;
// Recursively invert the left subtree
invertTree(root->left);
// Recursively invert the right subtree
invertTree(root->right);
return root;
}
};
// Helper function to print the tree in-order (for verification)
void printTree(TreeNode* root) {
if (root == nullptr) {
return;
}
printTree(root->left);
cout << root->val << " ";
printTree(root->right);
}
int main() {
// Example usage:
// Constructing a binary tree:
// 1
// / \
// 2 3
// / \ / \
// 4 5 6 7
TreeNode *root = new TreeNode(1);
root->left = new TreeNode(2);
root->right = new TreeNode(3);
root->left->left = new TreeNode(4);
root->left->right = new TreeNode(5);
root->right->left = new TreeNode(6);
root->right->right = new TreeNode(7);
cout << "Original tree in-order: ";
printTree(root);
cout << endl;
Solution solution;
TreeNode *invertedRoot = solution.invertTree(root);
cout << "Inverted tree in-order: ";
printTree(invertedRoot);
cout << endl;
// Clean up memory if needed
// Remember to delete the nodes of the tree to prevent memory leaks
// This part is omitted for brevity
return 0;
}
这段代码定义了一个TreeNode
结构体和一个Solution
类,其中包含了invertTree
方法来翻转二叉树。还有一个printTree
辅助函数用于按顺序打印树的节点值,以便验证翻转是否正确。main
函数中创建了一个简单的二叉树,并展示了翻转前后的结果。注意,在实际应用中,你可能需要在程序结束前删除所有动态分配的节点,以避免内存泄漏。这部分代码在这里为了简洁而省略。
Python 版本
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class Solution:
def invertTree(self, root: TreeNode) -> TreeNode:
if root is None:
return None
# Swap the left and right children
root.left, root.right = root.right, root.left
# Recursively invert the left and right subtree
self.invertTree(root.left)
self.invertTree(root.right)
return root
# Helper function to print the tree in-order (for verification)
def printTree(root: TreeNode):
if root is not None:
printTree(root.left)
print(root.val, end=' ')
printTree(root.right)
# Example usage:
if __name__ == "__main__":
# Constructing a binary tree:
# 1
# / \
# 2 3
# / \ / \
# 4 5 6 7
root = TreeNode(1,
TreeNode(2, TreeNode(4), TreeNode(5)),
TreeNode(3, TreeNode(6), TreeNode(7)))
print("Original tree in-order:")
printTree(root)
print()
solution = Solution()
invertedRoot = solution.invertTree(root)
print("Inverted tree in-order:")
printTree(invertedRoot)
print()
这段代码定义了一个TreeNode
类用于表示二叉树的节点,以及一个Solution
类,其中包含了invertTree
方法来翻转二叉树。还有一个printTree
辅助函数用于按顺序打印树的节点值,以便验证翻转是否正确。在__main__
部分,创建了一个简单的二叉树,并展示了翻转前后的结果。
Java 版本
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
// Swap the left and right children
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
// Recursively invert the left and right subtree
invertTree(root.left);
invertTree(root.right);
return root;
}
}
// Helper function to print the tree in-order (for verification)
class BinaryTreePrinter {
public void printTree(TreeNode root) {
if (root != null) {
printTree(root.left);
System.out.print(root.val + " ");
printTree(root.right);
}
}
}
public class Main {
public static void main(String[] args) {
// Example usage:
// Constructing a binary tree:
// 1
// / \
// 2 3
// / \ / \
// 4 5 6 7
TreeNode root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);
root.right.left = new TreeNode(6);
root.right.right = new TreeNode(7);
BinaryTreePrinter printer = new BinaryTreePrinter();
System.out.print("Original tree in-order: ");
printer.printTree(root);
System.out.println();
Solution solution = new Solution();
TreeNode invertedRoot = solution.invertTree(root);
System.out.print("Inverted tree in-order: ");
printer.printTree(invertedRoot);
System.out.println();
}
}
这段代码定义了一个TreeNode
类用于表示二叉树的节点,一个Solution
类包含了invertTree
方法来翻转二叉树,以及一个BinaryTreePrinter
类包含了printTree
方法用于按顺序打印树的节点值,以便验证翻转是否正确。在Main
类的main
方法中,创建了一个简单的二叉树,并展示了翻转前后的结果。
Rust 版本
use std::rc::Rc; use std::cell::RefCell; // Definition for a binary tree node. #[derive(Debug, PartialEq, Eq)] pub struct TreeNode { pub val: i32, pub left: Option<Rc<RefCell<TreeNode>>>, pub right: Option<Rc<RefCell<TreeNode>>>, } impl TreeNode { #[inline] pub fn new(val: i32) -> Self { TreeNode { val, left: None, right: None, } } } pub struct Solution {} impl Solution { pub fn invert_tree(root: Option<Rc<RefCell<TreeNode>>>) -> Option<Rc<RefCell<TreeNode>>> { if let Some(node) = root.clone() { let left = node.borrow().left.clone(); let right = node.borrow().right.clone(); node.borrow_mut().left = Self::invert_tree(right); node.borrow_mut().right = Self::invert_tree(left); } root } } fn main() { let root = Some(Rc::new(RefCell::new(TreeNode::new(1)))); let left = Some(Rc::new(RefCell::new(TreeNode::new(2)))); let right = Some(Rc::new(RefCell::new(TreeNode::new(3)))); let left_left = Some(Rc::new(RefCell::new(TreeNode::new(4)))); let left_right = Some(Rc::new(RefCell::new(TreeNode::new(5)))); let right_left = Some(Rc::new(RefCell::new(TreeNode::new(6)))); let right_right = Some(Rc::new(RefCell::new(TreeNode::new(7)))); if let Some(ref root_node) = root { root_node.borrow_mut().left = left; root_node.borrow_mut().right = right; if let Some(ref left_node) = root_node.borrow().left { left_node.borrow_mut().left = left_left; left_node.borrow_mut().right = left_right; } if let Some(ref right_node) = root_node.borrow().right { right_node.borrow_mut().left = right_left; right_node.borrow_mut().right = right_right; } } println!("Original tree: {:?}", root); let inverted_root = Solution::invert_tree(root); println!("Inverted tree: {:?}", inverted_root); }
这段代码使用 Rust 语言定义了一个二叉树节点结构体TreeNode
和一个解决方案结构体Solution
,其中包含了invert_tree
方法来翻转二叉树。在main
函数中,创建了一个简单的二叉树,并展示了翻转前后的结果。Rust 版本的代码使用了Rc
和RefCell
来实现可变性和所有权共享,这是 Rust 语言特有的特性。
总结
上面给出了针对翻转二叉树问题的不同编程语言的解法示例。无论是使用递归还是迭代,解决方案的核心思想都是交换每个节点的左右子节点,从而实现整棵二叉树的翻转。每种语言的代码示例都展示了如何定义二叉树节点的数据结构,以及如何实现翻转二叉树的算法。这些示例为不同语言的开发者提供了参考,帮助他们理解并实现翻转二叉树的算法。
对称二叉树
题目要求
设计一个算法,用于检查一个二叉树是否关于其主轴对称。这里的“轴对称”意味着二叉树的左子树是其右子树的镜像反射。如果一个二叉树看起来与其自身的镜像相同,那么它就是轴对称的。
解题思路
要判断一个二叉树是否轴对称,我们可以采用递归的方法。具体步骤如下:
- 基本情况:如果二叉树为空,那么它是对称的。
- 递归步骤:如果二叉树不为空,那么我们需要比较它的左子树和右子树。
- 首先检查左子树和右子树的根节点值是否相同。
- 然后检查左子树的左子树与右子树的右子树是否对称,这是通过递归调用来实现的。
- 接着检查左子树的右子树与右子树的左子树是否对称,同样通过递归调用来实现。
如果上述比较中的所有对称性检查都通过,则二叉树是轴对称的;如果任何一个检查失败,则二叉树不是轴对称的。
在实现时,我们可以定义一个辅助函数,该函数接受两个节点作为参数,并且递归地检查这两个节点的子树是否为镜像对称。这个辅助函数将被用于比较原始二叉树的左子树和右子树。
以下是辅助函数的递归逻辑:
- 如果两个节点都为空,返回
true
。 - 如果其中一个节点为空,另一个不为空,返回
false
。 - 如果两个节点的值不相同,返回
false
。 - 递归地比较第一个节点的左子树与第二个节点的右子树,以及第一个节点的右子树与第二个节点的左子树。
最后,调用这个辅助函数,传入二叉树的根节点的左右子节点作为参数,得到的结果即为整个二叉树是否对称的答案。
Golang 版本
package main
import "fmt"
// TreeNode is a structure for tree nodes
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
// isSymmetric checks if the tree is symmetric around its center
func isSymmetric(root *TreeNode) bool {
if root == nil {
return true
}
return isMirror(root.Left, root.Right)
}
// isMirror checks if two trees are mirror images of each other
func isMirror(t1, t2 *TreeNode) bool {
if t1 == nil && t2 == nil {
return true
}
if t1 == nil || t2 == nil {
return false
}
return t1.Val == t2.Val && isMirror(t1.Left, t2.Right) && isMirror(t1.Right, t2.Left)
}
func main() {
// Example usage:
// Construct a symmetric tree and test the function
n1 := &TreeNode{Val: 1}
n2 := &TreeNode{Val: 2}
n3 := &TreeNode{Val: 2}
n4 := &TreeNode{Val: 3}
n5 := &TreeNode{Val: 4}
n6 := &TreeNode{Val: 4}
n7 := &TreeNode{Val: 3}
n1.Left = n2
n1.Right = n3
n2.Left = n4
n2.Right = n5
n3.Left = n6
n3.Right = n7
fmt.Println("Is the tree symmetric? ", isSymmetric(n1))
}
这段代码定义了一个TreeNode
结构体,用于构建二叉树。isSymmetric
函数用于检查二叉树是否对称,它调用了isMirror
函数来递归地检查两个子树是否为彼此的镜像。在main
函数中,我们构建了一个示例二叉树,并调用isSymmetric
函数来验证它是否对称。
C++版本
#include <iostream>
// Definition for a binary tree node.
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution {
public:
bool isSymmetric(TreeNode* root) {
if (!root) return true;
return isMirror(root->left, root->right);
}
private:
bool isMirror(TreeNode* t1, TreeNode* t2) {
if (t1 == NULL && t2 == NULL) return true;
if (t1 == NULL || t2 == NULL) return false;
return (t1->val == t2->val) && isMirror(t1->right, t2->left) && isMirror(t1->left, t2->right);
}
};
int main() {
// Example usage:
TreeNode *root = new TreeNode(1);
root->left = new TreeNode(2);
root->right = new TreeNode(2);
root->left->left = new TreeNode(3);
root->left->right = new TreeNode(4);
root->right->left = new TreeNode(4);
root->right->right = new TreeNode(3);
Solution solution;
std::cout << "Is the tree symmetric? " << (solution.isSymmetric(root) ? "Yes" : "No") << std::endl;
// Remember to delete the allocated memory
delete root->left->left;
delete root->left->right;
delete root->right->left;
delete root->right->right;
delete root->left;
delete root->right;
delete root;
return 0;
}
这段 C++代码定义了一个TreeNode
结构体和一个Solution
类。Solution
类中的isSymmetric
方法用于检查二叉树是否对称,它调用了私有方法isMirror
来递归地检查两个子树是否为彼此的镜像。在main
函数中,我们构建了一个示例二叉树,并调用isSymmetric
方法来验证它是否对称。最后,我们清理了分配的内存。
Python 版本
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class Solution:
def isSymmetric(self, root: TreeNode) -> bool:
if not root:
return True
return self.isMirror(root.left, root.right)
def isMirror(self, t1: TreeNode, t2: TreeNode) -> bool:
if not t1 and not t2:
return True
if not t1 or not t2:
return False
return (t1.val == t2.val) and self.isMirror(t1.right, t2.left) and self.isMirror(t1.left, t2.right)
# Example usage:
if __name__ == "__main__":
# Construct a symmetric tree and test the function
n1 = TreeNode(1)
n2 = TreeNode(2)
n3 = TreeNode(2)
n4 = TreeNode(3)
n5 = TreeNode(4)
n6 = TreeNode(4)
n7 = TreeNode(3)
n1.left = n2
n1.right = n3
n2.left = n4
n2.right = n5
n3.left = n6
n3.right = n7
solution = Solution()
print("Is the tree symmetric?", solution.isSymmetric(n1))
这段 Python 代码定义了一个TreeNode
类用于创建二叉树节点,以及一个Solution
类,其中包含了isSymmetric
方法用于检查二叉树是否对称。isSymmetric
方法调用了辅助方法isMirror
,后者递归地检查两个子树是否互为镜像。在if __name__ == "__main__":
块中,我们构建了一个示例二叉树,并调用isSymmetric
方法来验证它是否对称。
Java 版本
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
public class Solution {
public boolean isSymmetric(TreeNode root) {
if (root == null) return true;
return isMirror(root.left, root.right);
}
private boolean isMirror(TreeNode t1, TreeNode t2) {
if (t1 == null && t2 == null) return true;
if (t1 == null || t2 == null) return false;
return (t1.val == t2.val) && isMirror(t1.right, t2.left) && isMirror(t1.left, t2.right);
}
}
public class Main {
public static void main(String[] args) {
// Example usage:
TreeNode root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(2);
root.left.left = new TreeNode(3);
root.left.right = new TreeNode(4);
root.right.left = new TreeNode(4);
root.right.right = new TreeNode(3);
Solution solution = new Solution();
System.out.println("Is the tree symmetric? " + solution.isSymmetric(root));
}
}
这段 Java 代码定义了一个TreeNode
类用于创建二叉树节点,以及一个Solution
类,其中包含了isSymmetric
方法用于检查二叉树是否对称。isSymmetric
方法调用了私有方法isMirror
,后者递归地检查两个子树是否互为镜像。在main
方法中,我们构建了一个示例二叉树,并调用isSymmetric
方法来验证它是否对称。
Rust 版本
// Definition for a binary tree node. #[derive(Debug, PartialEq, Eq)] pub struct TreeNode { pub val: i32, pub left: Option<Rc<RefCell<TreeNode>>>, pub right: Option<Rc<RefCell<TreeNode>>>, } impl TreeNode { #[inline] pub fn new(val: i32) -> Self { TreeNode { val, left: None, right: None, } } } use std::rc::Rc; use std::cell::RefCell; pub struct Solution {} impl Solution { pub fn is_symmetric(root: Option<Rc<RefCell<TreeNode>>>) -> bool { Self::is_mirror(&root, &root) } fn is_mirror(t1: &Option<Rc<RefCell<TreeNode>>>, t2: &Option<Rc<RefCell<TreeNode>>>) -> bool { match (t1, t2) { (Some(n1), Some(n2)) => { let n1 = n1.borrow(); let n2 = n2.borrow(); n1.val == n2.val && Self::is_mirror(&n1.left, &n2.right) && Self::is_mirror(&n1.right, &n2.left) } (None, None) => true, _ => false, } } } fn main() { // Example usage: let n1 = Rc::new(RefCell::new(TreeNode::new(1))); let n2 = Rc::new(RefCell::new(TreeNode::new(2))); let n3 = Rc::new(RefCell::new(TreeNode::new(2))); let n4 = Rc::new(RefCell::new(TreeNode::new(3))); let n5 = Rc::new(RefCell::new(TreeNode::new(4))); let n6 = Rc::new(RefCell::new(TreeNode::new(4))); let n7 = Rc::new(RefCell::new(TreeNode::new(3))); n1.borrow_mut().left = Some(n2.clone()); n1.borrow_mut().right = Some(n3.clone()); n2.borrow_mut().left = Some(n4.clone()); n2.borrow_mut().right = Some(n5.clone()); n3.borrow_mut().left = Some(n6.clone()); n3.borrow_mut().right = Some(n7.clone()); println!("Is the tree symmetric? {}", Solution::is_symmetric(Some(n1.clone()))); }
这段 Rust 代码定义了一个TreeNode
结构体用于创建二叉树节点,并使用Rc
和RefCell
来允许可变共享所有权。Solution
结构体包含了is_symmetric
函数用于检查二叉树是否对称,它调用了私有函数is_mirror
来递归地检查两个子树是否互为镜像。在main
函数中,我们构建了一个示例二叉树,并调用is_symmetric
函数来验证它是否对称。
总结
上面给出了使用不同编程语言(包括 Golang、C++、Python、Java 和 Rust)实现的检查二叉树是否对称的解法。这些解法都采用了递归的思路,通过比较二叉树的左子树和右子树来判断是否对称。具体步骤包括:
- 定义一个
TreeNode
结构体或类来表示二叉树节点。 - 编写一个
isSymmetric
或is_symmetric
函数来检查二叉树是否对称,该函数调用一个辅助函数isMirror
或is_mirror
来递归比较两个子树是否镜像对称。 - 在主函数中构建一个示例二叉树,并调用
isSymmetric
或is_symmetric
函数来验证是否对称。
这些解法都遵循了相似的逻辑,但在具体语法和数据结构上有所差异,适用于不同的编程语言。
二叉树的直径
题目要求
给定一棵二叉树的根节点,要求计算出这棵树的直径。这里的“直径”指的是树中任意两个节点之间最长路径的长度。值得注意的是,这条最长路径不一定要通过根节点。路径长度的定义是路径上边的数量。
解题思路
要解决这个问题,我们可以利用递归的方式来遍历二叉树。对于二叉树中的每一个节点,我们可以定义一个递归函数,该函数返回以该节点为根的子树的最大深度。在递归的过程中,我们可以更新一个全局变量来记录所遇到的最长路径的长度。
具体步骤如下:
-
初始化一个全局变量,比如命名为
maxDiameter
,用于记录遇到的最长路径的长度,初始值为 0。 -
对二叉树进行深度优先搜索(DFS),在搜索过程中,对于每个节点,计算其左子树的最大深度和右子树的最大深度。
-
对于每个节点,其左子树的最大深度加上右子树的最大深度的和就是通过该节点的最长路径的长度(即该节点的直径)。比较并更新全局变量
maxDiameter
。 -
递归函数应该返回节点的最大深度,即其左右子树的最大深度的最大值加 1(加的 1 是因为要包括当前节点到子节点的一条边)。
-
递归结束后,全局变量
maxDiameter
中存储的就是整棵树的直径。 -
返回
maxDiameter
作为结果。
通过这种方式,我们可以在遍历树的同时计算出每个节点为根的子树的最大深度,并且更新全局变量来得到整棵树的直径。这个方法的时间复杂度是 O(N),其中 N 是树中节点的数量,因为我们需要访问树中的每个节点。
Golang 版本
package main
import (
"fmt"
)
// TreeNode is the structure for binary tree nodes.
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
// maxDiameter is a global variable to keep track of the maximum diameter.
var maxDiameter int
func diameterOfBinaryTree(root *TreeNode) int {
maxDiameter = 0
depth(root)
return maxDiameter
}
// depth function returns the depth of the tree and updates the maxDiameter.
func depth(node *TreeNode) int {
if node == nil {
return 0
}
leftDepth := depth(node.Left)
rightDepth := depth(node.Right)
// Update maxDiameter at each node.
maxDiameter = max(maxDiameter, leftDepth+rightDepth)
// Return the depth of the tree rooted at the current node.
return 1 + max(leftDepth, rightDepth)
}
// max function returns the maximum of two integers.
func max(a, b int) int {
if a > b {
return a
}
return b
}
func main() {
// Example to create a binary tree:
// 1
// / \
// 2 3
// / \
// 4 5
root := &TreeNode{Val: 1}
root.Left = &TreeNode{Val: 2}
root.Right = &TreeNode{Val: 3}
root.Left.Left = &TreeNode{Val: 4}
root.Left.Right = &TreeNode{Val: 5}
// Call the function and print the result.
fmt.Println("Diameter of the binary tree is:", diameterOfBinaryTree(root))
}
这段代码定义了一个二叉树节点的结构体TreeNode
,并且实现了计算二叉树直径的函数diameterOfBinaryTree
。在这个函数中,我们使用了一个辅助函数depth
来计算树的深度,并在计算过程中更新全局变量maxDiameter
来记录遇到的最大直径。最后,main
函数中创建了一个示例二叉树,并调用diameterOfBinaryTree
函数来计算并打印出这棵树的直径。
C++版本
#include <iostream>
using namespace std;
// Definition for a binary tree node.
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution {
private:
int maxDiameter = 0;
int depth(TreeNode* node) {
if (node == nullptr) {
return 0;
}
int leftDepth = depth(node->left);
int rightDepth = depth(node->right);
maxDiameter = max(maxDiameter, leftDepth + rightDepth);
return max(leftDepth, rightDepth) + 1;
}
public:
int diameterOfBinaryTree(TreeNode* root) {
maxDiameter = 0;
depth(root);
return maxDiameter;
}
};
int main() {
// Example to create a binary tree:
// 1
// / \
// 2 3
// / \
// 4 5
TreeNode *root = new TreeNode(1);
root->left = new TreeNode(2);
root->right = new TreeNode(3);
root->left->left = new TreeNode(4);
root->left->right = new TreeNode(5);
Solution solution;
cout << "Diameter of the binary tree is: " << solution.diameterOfBinaryTree(root) << endl;
// Clean up memory if needed
delete root->left->left;
delete root->left->right;
delete root->left;
delete root->right;
delete root;
return 0;
}
这段 C++代码定义了一个二叉树节点的结构体TreeNode
,并且实现了一个解决方案类Solution
,其中包含了计算二叉树直径的函数diameterOfBinaryTree
。在这个函数中,我们使用了一个私有成员函数depth
来计算树的深度,并在计算过程中更新成员变量maxDiameter
来记录遇到的最大直径。main
函数中创建了一个示例二叉树,并调用diameterOfBinaryTree
函数来计算并打印出这棵树的直径。最后,代码中还包含了适当的内存清理操作。
Python 版本
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class Solution:
def diameterOfBinaryTree(self, root: TreeNode) -> int:
self.max_diameter = 0
def depth(node):
if not node:
return 0
left_depth = depth(node.left)
right_depth = depth(node.right)
self.max_diameter = max(self.max_diameter, left_depth + right_depth)
return max(left_depth, right_depth) + 1
depth(root)
return self.max_diameter
# Example usage:
if __name__ == "__main__":
# Create a binary tree
# 1
# / \
# 2 3
# / \
# 4 5
root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.left = TreeNode(4)
root.left.right = TreeNode(5)
solution = Solution()
print("Diameter of the binary tree is:", solution.diameterOfBinaryTree(root))
这段 Python 代码定义了一个二叉树节点的类TreeNode
,以及一个解决方案类Solution
,其中包含了计算二叉树直径的方法diameterOfBinaryTree
。在这个方法中,我们使用了一个嵌套函数depth
来计算树的深度,并在计算过程中更新self.max_diameter
属性来记录遇到的最大直径。__main__
部分创建了一个示例二叉树,并调用diameterOfBinaryTree
方法来计算并打印出这棵树的直径。
Java 版本
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
class Solution {
private int maxDiameter = 0;
public int diameterOfBinaryTree(TreeNode root) {
maxDepth(root);
return maxDiameter;
}
private int maxDepth(TreeNode node) {
if (node == null) {
return 0;
}
int leftDepth = maxDepth(node.left);
int rightDepth = maxDepth(node.right);
maxDiameter = Math.max(maxDiameter, leftDepth + rightDepth);
return Math.max(leftDepth, rightDepth) + 1;
}
}
public class Main {
public static void main(String[] args) {
// Example to create a binary tree:
// 1
// / \
// 2 3
// / \
// 4 5
TreeNode root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);
Solution solution = new Solution();
System.out.println("Diameter of the binary tree is: " + solution.diameterOfBinaryTree(root));
}
}
这段 Java 代码定义了一个二叉树节点的类TreeNode
,以及一个解决方案类Solution
,其中包含了计算二叉树直径的方法diameterOfBinaryTree
。在这个方法中,我们使用了一个私有辅助方法maxDepth
来计算树的深度,并在计算过程中更新maxDiameter
属性来记录遇到的最大直径。Main
类中的main
方法创建了一个示例二叉树,并调用diameterOfBinaryTree
方法来计算并打印出这棵树的直径。
Rust 版本
// Definition for a binary tree node. #[derive(Debug, PartialEq, Eq)] pub struct TreeNode { pub val: i32, pub left: Option<Rc<RefCell<TreeNode>>>, pub right: Option<Rc<RefCell<TreeNode>>>, } impl TreeNode { #[inline] pub fn new(val: i32) -> Self { TreeNode { val, left: None, right: None, } } } use std::cell::RefCell; use std::rc::Rc; struct Solution; impl Solution { pub fn diameter_of_binary_tree(root: Option<Rc<RefCell<TreeNode>>>) -> i32 { fn depth(node: &Option<Rc<RefCell<TreeNode>>>, max_diameter: &mut i32) -> i32 { if let Some(node) = node { let node = node.borrow(); let left_depth = depth(&node.left, max_diameter); let right_depth = depth(&node.right, max_diameter); *max_diameter = (*max_diameter).max(left_depth + right_depth); 1 + left_depth.max(right_depth) } else { 0 } } let mut max_diameter = 0; depth(&root, &mut max_diameter); max_diameter } } fn main() { // Example to create a binary tree: // 1 // / \ // 2 3 // / \ // 4 5 let root = Some(Rc::new(RefCell::new(TreeNode::new(1)))); let left = Some(Rc::new(RefCell::new(TreeNode::new(2)))); let right = Some(Rc::new(RefCell::new(TreeNode::new(3)))); let left_left = Some(Rc::new(RefCell::new(TreeNode::new(4)))); let left_right = Some(Rc::new(RefCell::new(TreeNode::new(5)))); if let Some(ref root_node) = root { root_node.borrow_mut().left = left; root_node.borrow_mut().right = right; } if let Some(ref left_node) = root.as_ref().unwrap().borrow().left { left_node.borrow_mut().left = left_left; left_node.borrow_mut().right = left_right; } println!("Diameter of the binary tree is: {}", Solution::diameter_of_binary_tree(root)); }
这段 Rust 代码定义了一个二叉树节点的结构体TreeNode
,以及一个解决方案结构体Solution
,其中包含了计算二叉树直径的函数diameter_of_binary_tree
。在这个函数中,我们使用了一个嵌套函数depth
来计算树的深度,并在计算过程中更新max_diameter
变量来记录遇到的最大直径。main
函数中创建了一个示例二叉树,并调用diameter_of_binary_tree
函数来计算并打印出这棵树的直径。
总结
上面给出了针对计算二叉树直径的解法,分别使用了不同编程语言的代码示例。这些解法都采用了递归的方式来计算二叉树的直径,通过计算树的深度并在计算过程中更新最大直径的方式来实现。每种语言的代码都定义了二叉树节点的结构,然后实现了一个解决方案类或函数来计算直径。这些解法在不同语言中的实现细节有所不同,但核心思想都是相似的,即利用递归遍历二叉树并更新最大直径的值。
二叉树的层序遍历
题目要求
设计一个算法,实现对一棵二叉树进行层序遍历。层序遍历是指按照从上到下、从左到右的顺序访问树中的每个节点。具体来说,就是先访问根节点,然后访问第二层的所有节点,接着是第三层的所有节点,以此类推,直到访问到最后一层的所有节点。
输入:一棵二叉树的根节点 root。 输出:一个列表,其中包含二叉树的层序遍历结果。
解题思路
层序遍历通常使用队列这一数据结构来实现。以下是解题的步骤:
- 初始化一个队列,将根节点入队。
- 当队列不为空时,进行以下操作: a. 记录当前队列的长度,这个长度表示当前层的节点数。 b. 循环遍历当前层的节点,对于每个节点,执行以下操作: i. 将节点的值加入到当前层的结果列表中。 ii. 如果该节点有左子节点,则将左子节点入队。 iii. 如果该节点有右子节点,则将右子节点入队。 c. 当前层遍历完成后,将当前层的结果列表加入到最终的层序遍历结果列表中。
- 当队列为空时,所有层的节点都已访问完毕,返回最终的层序遍历结果列表。
这种方法确保了每次只处理一层的节点,并且通过队列先进先出的特性来保证左右子节点的访问顺序。
Golang 版本
package main
import (
"fmt"
)
// TreeNode is a structure for tree nodes
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
// levelOrder performs a level-order traversal on a binary tree
func levelOrder(root *TreeNode) [][]int {
result := [][]int{}
if root == nil {
return result
}
queue := []*TreeNode{root}
for len(queue) > 0 {
level := []int{}
levelLength := len(queue)
for i := 0; i < levelLength; i++ {
node := queue[0]
queue = queue[1:]
level = append(level, node.Val)
if node.Left != nil {
queue = append(queue, node.Left)
}
if node.Right != nil {
queue = append(queue, node.Right)
}
}
result = append(result, level)
}
return result
}
func main() {
// Example usage:
// Construct a binary tree as an example
// 3
// / \
// 9 20
// / \
// 15 7
root := &TreeNode{3, &TreeNode{9, nil, nil}, &TreeNode{20, &TreeNode{15, nil, nil}, &TreeNode{7, nil, nil}}}
fmt.Println(levelOrder(root))
}
这段代码定义了一个TreeNode
结构体,用于表示二叉树的节点,并实现了levelOrder
函数来进行层序遍历。main
函数中给出了一个使用示例,构造了一棵简单的二叉树,并打印了其层序遍历的结果。
C++版本
#include <iostream>
#include <vector>
#include <queue>
// Definition for a binary tree node.
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution {
public:
std::vector<std::vector<int>> levelOrder(TreeNode* root) {
std::vector<std::vector<int>> result;
if (!root) return result;
std::queue<TreeNode*> nodesQueue;
nodesQueue.push(root);
while (!nodesQueue.empty()) {
int levelSize = nodesQueue.size();
std::vector<int> currentLevel;
for (int i = 0; i < levelSize; ++i) {
TreeNode* node = nodesQueue.front();
nodesQueue.pop();
currentLevel.push_back(node->val);
if (node->left) nodesQueue.push(node->left);
if (node->right) nodesQueue.push(node->right);
}
result.push_back(currentLevel);
}
return result;
}
};
int main() {
// Example usage:
// Construct a binary tree as an example
// 3
// / \
// 9 20
// / \
// 15 7
TreeNode *root = new TreeNode(3);
root->left = new TreeNode(9);
root->right = new TreeNode(20);
root->right->left = new TreeNode(15);
root->right->right = new TreeNode(7);
Solution solution;
std::vector<std::vector<int>> result = solution.levelOrder(root);
// Print the level order traversal
for (const auto& level : result) {
for (int val : level) {
std::cout << val << " ";
}
std::cout << std::endl;
}
// Clean up the allocated nodes
delete root->right->right;
delete root->right->left;
delete root->right;
delete root->left;
delete root;
return 0;
}
这段 C++代码定义了一个TreeNode
结构体,用于表示二叉树的节点,并实现了一个Solution
类,其中包含了levelOrder
函数来进行层序遍历。main
函数中给出了一个使用示例,构造了一棵简单的二叉树,并打印了其层序遍历的结果。最后,代码中还包含了适当的内存清理操作,以删除分配的节点。
Python 版本
from collections import deque
# Definition for a binary tree node.
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class Solution:
def levelOrder(self, root: TreeNode) -> [[int]]:
if not root:
return []
result = []
queue = deque([root])
while queue:
level_size = len(queue)
level = []
for _ in range(level_size):
node = queue.popleft()
level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(level)
return result
# Example usage:
if __name__ == "__main__":
# Construct a binary tree as an example
# 3
# / \
# 9 20
# / \
# 15 7
root = TreeNode(3)
root.left = TreeNode(9)
root.right = TreeNode(20)
root.right.left = TreeNode(15)
root.right.right = TreeNode(7)
solution = Solution()
print(solution.levelOrder(root))
这段 Python 代码定义了一个TreeNode
类,用于表示二叉树的节点,并实现了一个Solution
类,其中包含了levelOrder
方法来进行层序遍历。在__main__
部分中,给出了一个使用示例,构造了一棵简单的二叉树,并打印了其层序遍历的结果。
Java 版本
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
// Definition for a binary tree node.
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) {
return result;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int levelSize = queue.size();
List<Integer> currentLevel = new ArrayList<>();
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll();
currentLevel.add(node.val);
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
result.add(currentLevel);
}
return result;
}
}
// Example usage
class Main {
public static void main(String[] args) {
// Construct a binary tree as an example
// 3
// / \
// 9 20
// / \
// 15 7
TreeNode root = new TreeNode(3);
root.left = new TreeNode(9);
root.right = new TreeNode(20);
root.right.left = new TreeNode(15);
root.right.right = new TreeNode(7);
Solution solution = new Solution();
List<List<Integer>> result = solution.levelOrder(root);
// Print the level order traversal
for (List<Integer> level : result) {
for (int val : level) {
System.out.print(val + " ");
}
System.out.println();
}
}
}
这段 Java 代码定义了一个TreeNode
类,用于表示二叉树的节点,并实现了一个Solution
类,其中包含了levelOrder
方法来进行层序遍历。在Main
类的main
方法中,给出了一个使用示例,构造了一棵简单的二叉树,并打印了其层序遍历的结果。
Rust 版本
use std::cell::RefCell; use std::rc::Rc; use std::collections::VecDeque; // Definition for a binary tree node. #[derive(Debug, PartialEq, Eq)] pub struct TreeNode { pub val: i32, pub left: Option<Rc<RefCell<TreeNode>>>, pub right: Option<Rc<RefCell<TreeNode>>>, } impl TreeNode { #[inline] pub fn new(val: i32) -> Self { TreeNode { val, left: None, right: None, } } } struct Solution; impl Solution { pub fn level_order(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<Vec<i32>> { let mut result = Vec::new(); if root.is_none() { return result; } let mut queue = VecDeque::new(); queue.push_back(root); while let Some(node) = queue.pop_front() { let mut current_level = Vec::new(); let mut next_level = VecDeque::new(); for node in queue.iter() { if let Some(node) = node { let node = node.borrow(); current_level.push(node.val); if node.left.is_some() { next_level.push_back(node.left.clone()); } if node.right.is_some() { next_level.push_back(node.right.clone()); } } } result.push(current_level); queue = next_level; } result } } fn main() { let root = Some(Rc::new(RefCell::new(TreeNode::new(3)))); let left = Some(Rc::new(RefCell::new(TreeNode::new(9)))); let right = Some(Rc::new(RefCell::new(TreeNode::new(20)))); let left_leaf = Some(Rc::new(RefCell::new(TreeNode::new(15)))); let right_leaf = Some(Rc::new(RefCell::new(TreeNode::new(7)))); if let Some(ref root_node) = root { root_node.borrow_mut().left = left; root_node.borrow_mut().right = right; if let Some(ref right_node) = root_node.borrow().right { right_node.borrow_mut().left = left_leaf; right_node.borrow_mut().right = right_leaf; } } let result = Solution::level_order(root); for level in result { for val in level { print!("{} ", val); } println!(); } }
这段 Rust 代码定义了一个TreeNode
结构体,用于表示二叉树的节点,并实现了一个Solution
结构体,其中包含了level_order
函数来进行层序遍历。在main
函数中,给出了一个使用示例,构造了一棵简单的二叉树,并打印了其层序遍历的结果。
总结
以上给出了针对二叉树进行层序遍历的解法,涵盖了多种编程语言的实现。无论是使用队列迭代的方法还是递归的方法,层序遍历都是通过逐层访问节点来实现的。这些解法都遵循了先处理当前层节点,然后将下一层节点加入队列或递归调用的基本思路。在实际应用中,可以根据具体情况选择合适的解法和编程语言来实现二叉树的层序遍历。
将有序数组转换为二叉搜索树
题目要求
你需要编写一个算法,该算法接受一个已经按照升序排列的整数数组 nums
作为输入。你的任务是利用这个数组中的所有元素来创建一棵高度平衡的二叉搜索树(BST)。
在这个上下文中,一棵高度平衡的二叉搜索树定义为一棵二叉树,它满足以下条件:
- 二叉搜索树的性质:对于树中的任意节点,其左子树中的所有元素都小于该节点,其右子树中的所有元素都大于该节点。
- 高度平衡的性质:树中每个节点的左右子树的高度差的绝对值不超过 1。
你的算法需要确保树是平衡的,以避免最坏情况的性能,即退化成链表的情况。
解题思路
要创建一棵高度平衡的二叉搜索树,最直观的方法是始终将数组的中间元素作为二叉树的根节点,这样可以确保左右子树的元素数量尽可能相等,从而保持树的平衡。
具体步骤如下:
-
找到中间元素:首先,找到数组
nums
的中间元素,如果数组有偶数个元素,可以选择中间两个元素的任意一个。这个中间元素将成为当前二叉搜索树的根节点。 -
递归构建子树:然后,将中间元素左侧的数组元素用于构建当前根节点的左子树,将中间元素右侧的数组元素用于构建当前根节点的右子树。这一步骤可以递归地进行:
- 对于左子树,取中间元素左侧的子数组,重复步骤 1 和 2,找到子数组的中间元素,作为左子树的根节点。
- 对于右子树,取中间元素右侧的子数组,重复步骤 1 和 2,找到子数组的中间元素,作为右子树的根节点。
-
递归的终止条件:当子数组为空时,递归结束。这意味着当前节点没有子节点。
通过这种方式,可以确保每次都是平均分配元素到左右子树,从而保持整棵树的平衡。递归的过程会一直持续,直到所有的数组元素都已经被用来创建二叉搜索树的节点为止。
Golang 版本
package main
import (
"fmt"
)
// TreeNode is a structure for tree nodes
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
// sortedArrayToBST takes a sorted array and converts it to a balanced BST
func sortedArrayToBST(nums []int) *TreeNode {
if len(nums) == 0 {
return nil
}
return constructBST(nums, 0, len(nums)-1)
}
// constructBST is a helper function that constructs the BST recursively
func constructBST(nums []int, left int, right int) *TreeNode {
if left > right {
return nil
}
mid := left + (right-left)/2
current := &TreeNode{Val: nums[mid]}
current.Left = constructBST(nums, left, mid-1)
current.Right = constructBST(nums, mid+1, right)
return current
}
// printTree is a helper function to print the tree (in-order traversal)
func printTree(node *TreeNode) {
if node == nil {
return
}
printTree(node.Left)
fmt.Print(node.Val, " ")
printTree(node.Right)
}
func main() {
nums := []int{-10, -3, 0, 5, 9}
bst := sortedArrayToBST(nums)
printTree(bst) // Output should be the sorted array in ascending order
}
这段代码定义了一个TreeNode
结构体,用于表示二叉树的节点。sortedArrayToBST
函数接受一个升序排列的整数数组,并调用constructBST
辅助函数来递归地构建高度平衡的二叉搜索树。printTree
函数用于以中序遍历的方式打印出树的节点值,以验证树的结构是否正确。在main
函数中,我们创建了一个示例数组,并用它来构建 BST,然后打印出来。
C++版本
#include <iostream>
#include <vector>
using namespace std;
// Definition for a binary tree node.
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution {
public:
TreeNode* sortedArrayToBST(vector<int>& nums) {
return sortedArrayToBST(nums, 0, nums.size() - 1);
}
private:
TreeNode* sortedArrayToBST(vector<int>& nums, int start, int end) {
if (start > end) {
return nullptr;
}
int mid = start + (end - start) / 2;
TreeNode* node = new TreeNode(nums[mid]);
node->left = sortedArrayToBST(nums, start, mid - 1);
node->right = sortedArrayToBST(nums, mid + 1, end);
return node;
}
};
// Helper function to print the tree in-order
void printInOrder(TreeNode* node) {
if (node == nullptr) return;
printInOrder(node->left);
cout << node->val << " ";
printInOrder(node->right);
}
int main() {
vector<int> nums = {-10, -3, 0, 5, 9};
Solution solution;
TreeNode* bst = solution.sortedArrayToBST(nums);
printInOrder(bst); // Should print the numbers in ascending order
return 0;
}
这段 C++代码定义了一个TreeNode
结构体,用于表示二叉树的节点,以及一个Solution
类,其中包含了sortedArrayToBST
函数。这个函数接受一个升序排列的整数数组,并递归地构建一个高度平衡的二叉搜索树。printInOrder
函数用于以中序遍历的方式打印出树的节点值,以验证树的结构是否正确。在main
函数中,我们创建了一个示例数组,并用它来构建 BST,然后打印出来。
Python 版本
class TreeNode:
def __init__(self, x):
self.val = x
self.left = None
self.right = None
class Solution:
def sortedArrayToBST(self, nums):
if not nums:
return None
mid = len(nums) // 2
root = TreeNode(nums[mid])
root.left = self.sortedArrayToBST(nums[:mid])
root.right = self.sortedArrayToBST(nums[mid+1:])
return root
def preOrder(node):
if not node:
return
print(node.val, end=' ')
preOrder(node.left)
preOrder(node.right)
# Example usage:
if __name__ == "__main__":
nums = [-10, -3, 0, 5, 9]
sol = Solution()
root = sol.sortedArrayToBST(nums)
preOrder(root) # Pre-order traversal of the BST
这段 Python 代码定义了一个TreeNode
类,用于表示二叉树的节点,以及一个Solution
类,其中包含了sortedArrayToBST
方法。这个方法接受一个升序排列的整数列表,并递归地构建一个高度平衡的二叉搜索树。preOrder
函数用于以前序遍历的方式打印出树的节点值,以验证树的结构是否正确。在主函数中,我们创建了一个示例列表,并用它来构建 BST,然后打印出来。
Java 版本
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
public class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
if (nums == null || nums.length == 0) {
return null;
}
return constructBSTRecursive(nums, 0, nums.length - 1);
}
private TreeNode constructBSTRecursive(int[] nums, int left, int right) {
if (left > right) {
return null;
}
int mid = left + (right - left) / 2;
TreeNode node = new TreeNode(nums[mid]);
node.left = constructBSTRecursive(nums, left, mid - 1);
node.right = constructBSTRecursive(nums, mid + 1, right);
return node;
}
}
public class Main {
public static void printInOrder(TreeNode node) {
if (node == null) {
return;
}
printInOrder(node.left);
System.out.print(node.val + " ");
printInOrder(node.right);
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {-10, -3, 0, 5, 9};
TreeNode bst = solution.sortedArrayToBST(nums);
printInOrder(bst); // Should print the numbers in ascending order
}
}
这段 Java 代码定义了一个TreeNode
类,用于表示二叉树的节点,以及一个Solution
类,其中包含了sortedArrayToBST
方法。这个方法接受一个升序排列的整数数组,并递归地构建一个高度平衡的二叉搜索树。printInOrder
方法用于以中序遍历的方式打印出树的节点值,以验证树的结构是否正确。在Main
类的main
方法中,我们创建了一个示例数组,并用它来构建 BST,然后打印出来。
Rust 版本
// Definition for a binary tree node. #[derive(Debug, PartialEq, Eq)] pub struct TreeNode { pub val: i32, pub left: Option<Rc<RefCell<TreeNode>>>, pub right: Option<Rc<RefCell<TreeNode>>>, } impl TreeNode { #[inline] pub fn new(val: i32) -> Self { TreeNode { val, left: None, right: None, } } } use std::rc::Rc; use std::cell::RefCell; pub struct Solution; impl Solution { pub fn sorted_array_to_bst(nums: Vec<i32>) -> Option<Rc<RefCell<TreeNode>>> { Self::sorted_array_to_bst_helper(&nums[..]) } fn sorted_array_to_bst_helper(nums: &[i32]) -> Option<Rc<RefCell<TreeNode>>> { if nums.is_empty() { return None; } let mid = nums.len() / 2; Some(Rc::new(RefCell::new(TreeNode { val: nums[mid], left: Self::sorted_array_to_bst_helper(&nums[..mid]), right: Self::sorted_array_to_bst_helper(&nums[mid + 1..]), }))) } } fn main() { let nums = vec![-10, -3, 0, 5, 9]; let bst = Solution::sorted_array_to_bst(nums); // If you need to print the tree, you will need to implement a function // that traverses the tree and prints its values. }
这段 Rust 代码定义了一个TreeNode
结构体,用于表示二叉树的节点,并使用Rc
和RefCell
来允许可变共享所有权。Solution
结构体包含了sorted_array_to_bst
方法,该方法接受一个升序排列的 i32 类型向量,并递归地构建一个高度平衡的二叉搜索树。在main
函数中,我们创建了一个示例向量,并用它来构建 BST。由于 Rust 的所有权规则,打印树的函数需要特别处理,因此这里没有提供打印函数的实现。如果需要打印树,你需要实现一个遍历树并打印其值的函数。
总结
上面给出了使用不同编程语言(包括 Golang、C++、Python、Java 和 Rust)实现的解决方案,用于将给定的升序排列的整数数组转换为一棵高度平衡的二叉搜索树(BST)。这些解决方案都采用了递归的方法来构建平衡的 BST,具体步骤如下:
- 找到数组的中间元素作为当前子树的根节点。
- 递归地构建当前根节点的左子树和右子树,左子树由中间元素左侧的子数组构成,右子树由中间元素右侧的子数组构成。
- 递归的终止条件是子数组为空时,返回空节点。
这些解决方案都遵循了相似的思路,但在具体实现上有所差异,例如在处理递归终止条件、数组切片、节点的创建等方面。无论使用哪种语言,都可以通过递归的方式轻松地实现这一算法,从而创建一棵高度平衡的二叉搜索树。
验证二叉搜索树
题目要求
你需要编写一个算法来判断给定的二叉树是否满足二叉搜索树的条件。二叉搜索树(BST)是一种特殊的二叉树,它满足以下性质:
- 任意节点的左子树中的所有节点的值都小于该节点的值。
- 任意节点的右子树中的所有节点的值都大于该节点的值。
- 左子树和右子树也必须分别是二叉搜索树。
你的算法需要检查输入的二叉树根节点 root
是否符合上述所有条件。
解题思路
要判断一个二叉树是否是有效的二叉搜索树,我们可以采用以下几种思路:
-
递归法:
- 对于每个节点,检查其左子节点的值是否小于当前节点的值,右子节点的值是否大于当前节点的值。
- 递归地对左子树和右子树进行相同的检查。
- 注意,仅检查子节点的值是不够的,因为所有左子树中的节点都必须小于当前节点的值,所有右子树中的节点都必须大于当前节点的值。因此,我们需要维护当前值的有效最小值和最大值范围,并在递归过程中传递这些范围。
-
中序遍历法:
- 二叉搜索树的一个重要特性是,其中序遍历的结果是一个递增序列。
- 我们可以对树进行中序遍历,并将节点的值存储在一个数组中。
- 遍历完成后,检查数组是否是严格递增的。如果是,则树是一个有效的二叉搜索树;如果不是,树不是有效的二叉搜索树。
- 为了优化空间复杂度,我们不必存储整个中序遍历序列,而是可以在遍历过程中实时检查序列是否递增。这可以通过保存前一个访问的节点的值,并与当前节点的值进行比较来实现。
-
迭代法:
- 使用栈来模拟中序遍历的过程。
- 通过迭代而不是递归来遍历树,同时检查遍历的序列是否递增。
在实现上述任何一种方法时,都需要注意处理二叉树中可能存在的边界条件,例如树中的节点值可能包括整数的最大值和最小值。因此,初始化最大值和最小值时,不能简单地使用 int
或 long
的最大值和最小值,而应该使用更广泛的范围,或者使用特殊值来表示没有限制。
Golang 版本
package main
import (
"fmt"
"math"
)
// TreeNode defines a binary tree node.
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
// isValidBST is used to check if a tree is a valid binary search tree.
func isValidBST(root *TreeNode) bool {
return validate(root, math.MinInt64, math.MaxInt64)
}
// validate is a helper function that uses the limits to check the validity of the tree.
func validate(node *TreeNode, lower, upper int) bool {
if node == nil {
return true
}
val := node.Val
if val <= lower || val >= upper {
return false
}
if !validate(node.Right, val, upper) {
return false
}
if !validate(node.Left, lower, val) {
return false
}
return true
}
func main() {
// Example usage:
// Construct a binary search tree:
// 2
// / \
// 1 3
root := &TreeNode{Val: 2}
root.Left = &TreeNode{Val: 1}
root.Right = &TreeNode{Val: 3}
// Check if it is a valid BST
fmt.Println(isValidBST(root)) // Output: true
}
这段代码定义了一个TreeNode
结构体,表示二叉树的节点,并实现了isValidBST
函数来判断一个二叉树是否是有效的二叉搜索树。isValidBST
函数通过调用validate
辅助函数来递归地验证每个节点,同时传递当前节点值的有效范围。在main
函数中,我们构建了一个简单的二叉搜索树示例,并调用isValidBST
函数来验证它。
C++版本
#include <iostream>
#include <climits>
// Definition for a binary tree node.
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution {
public:
bool isValidBST(TreeNode* root) {
return validate(root, LONG_MIN, LONG_MAX);
}
private:
bool validate(TreeNode* node, long long lower, long long upper) {
if (node == nullptr) {
return true;
}
int val = node->val;
if (val <= lower || val >= upper) {
return false;
}
if (!validate(node->right, val, upper)) {
return false;
}
if (!validate(node->left, lower, val)) {
return false;
}
return true;
}
};
int main() {
// Example usage:
// Construct a binary search tree:
// 2
// / \
// 1 3
TreeNode *root = new TreeNode(2);
root->left = new TreeNode(1);
root->right = new TreeNode(3);
Solution solution;
std::cout << std::boolalpha << solution.isValidBST(root) << std::endl; // Output: true
// Clean up memory
delete root->left;
delete root->right;
delete root;
return 0;
}
这段 C++代码定义了一个TreeNode
结构体,表示二叉树的节点,并实现了一个Solution
类,其中包含了isValidBST
方法来判断一个二叉树是否是有效的二叉搜索树。isValidBST
方法通过调用私有的validate
方法来递归地验证每个节点,同时传递当前节点值的有效范围。在main
函数中,我们构建了一个简单的二叉搜索树示例,并调用isValidBST
方法来验证它。最后,代码中还包括了适当的内存清理操作。
Python 版本
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class Solution:
def isValidBST(self, root: TreeNode) -> bool:
def validate(node, low=float('-inf'), high=float('inf')):
if not node:
return True
if not (low < node.val < high):
return False
return (validate(node.left, low, node.val) and
validate(node.right, node.val, high))
return validate(root)
# Example usage:
if __name__ == "__main__":
# Construct a binary search tree:
# 2
# / \
# 1 3
root = TreeNode(2)
root.left = TreeNode(1)
root.right = TreeNode(3)
# Check if it is a valid BST
print(Solution().isValidBST(root)) # Output: True
这段 Python 代码定义了一个TreeNode
类,表示二叉树的节点,并实现了一个Solution
类,其中包含了isValidBST
方法来判断一个二叉树是否是有效的二叉搜索树。isValidBST
方法内部定义了一个嵌套的validate
函数,用于递归地验证每个节点,同时传递当前节点值的有效范围。在__main__
部分,我们构建了一个简单的二叉搜索树示例,并调用isValidBST
方法来验证它。
Java 版本
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
class Solution {
public boolean isValidBST(TreeNode root) {
return validate(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
private boolean validate(TreeNode node, long lower, long upper) {
if (node == null) {
return true;
}
if (node.val <= lower || node.val >= upper) {
return false;
}
return validate(node.left, lower, node.val) && validate(node.right, node.val, upper);
}
}
public class Main {
public static void main(String[] args) {
// Example usage:
// Construct a binary search tree:
// 2
// / \
// 1 3
TreeNode root = new TreeNode(2);
root.left = new TreeNode(1);
root.right = new TreeNode(3);
Solution solution = new Solution();
System.out.println(solution.isValidBST(root)); // Output: true
}
}
这段 Java 代码定义了一个TreeNode
类,表示二叉树的节点,并实现了一个Solution
类,其中包含了isValidBST
方法来判断一个二叉树是否是有效的二叉搜索树。isValidBST
方法通过调用私有的validate
方法来递归地验证每个节点,同时传递当前节点值的有效范围。在Main
类的main
方法中,我们构建了一个简单的二叉搜索树示例,并调用isValidBST
方法来验证它。
Rust 版本
// Definition for a binary tree node. #[derive(Debug, PartialEq, Eq)] pub struct TreeNode { pub val: i32, pub left: Option<Rc<RefCell<TreeNode>>>, pub right: Option<Rc<RefCell<TreeNode>>>, } impl TreeNode { #[inline] pub fn new(val: i32) -> Self { TreeNode { val, left: None, right: None, } } } use std::rc::Rc; use std::cell::RefCell; use std::i64; struct Solution; impl Solution { pub fn is_valid_bst(root: Option<Rc<RefCell<TreeNode>>>) -> bool { Solution::validate(&root, i64::MIN, i64::MAX) } fn validate(node: &Option<Rc<RefCell<TreeNode>>>, lower: i64, upper: i64) -> bool { match node { Some(n) => { let n_borrowed = n.borrow(); let val = n_borrowed.val as i64; if val <= lower || val >= upper { return false; } Solution::validate(&n_borrowed.left, lower, val) && Solution::validate(&n_borrowed.right, val, upper) }, None => true, } } } fn main() { // Example usage: // Construct a binary search tree: // 2 // / \ // 1 3 let root = Some(Rc::new(RefCell::new(TreeNode::new(2)))); let left = Some(Rc::new(RefCell::new(TreeNode::new(1)))); let right = Some(Rc::new(RefCell::new(TreeNode::new(3)))); if let Some(r) = root.as_ref() { r.borrow_mut().left = left; r.borrow_mut().right = right; } println!("{}", Solution::is_valid_bst(root)); // Output: true }
这段 Rust 代码定义了一个TreeNode
结构体,表示二叉树的节点,并实现了一个Solution
结构体,其中包含了is_valid_bst
方法来判断一个二叉树是否是有效的二叉搜索树。is_valid_bst
方法通过调用私有的validate
方法来递归地验证每个节点,同时传递当前节点值的有效范围。在main
函数中,我们构建了一个简单的二叉搜索树示例,并调用is_valid_bst
方法来验证它。
总结
无论是在 C++、Python、Java 还是 Rust 中,检查一个二叉树是否是二叉搜索树的基本算法思想是相同的。这个算法依赖于二叉搜索树的性质:对于树中的任意节点,其左子树中的所有节点的值都小于该节点的值,而其右子树中的所有节点的值都大于该节点的值。
为了实现这个算法,我们通常会使用递归函数,该函数会检查当前节点的值是否在允许的值范围内。这个范围是通过参数传递给递归调用的,初始调用时范围设置为整数类型能表示的最小值和最大值。对于每个节点,我们会更新这个范围:当我们向左子树递归时,我们将当前节点的值作为新的上界;当我们向右子树递归时,我们将当前节点的值作为新的下界。
如果在任何时候节点的值不在指定的范围内,或者左子树或右子树的递归调用返回false
,那么整个函数返回false
,表示这不是一个有效的二叉搜索树。如果递归检查通过所有节点都没有发现问题,那么函数返回true
,表示这是一个有效的二叉搜索树。
在不同的编程语言中,这个算法的实现细节略有不同,主要是因为语言本身的语法和类型系统的差异。例如,Rust 使用Option<Rc<RefCell<TreeNode>>>
来处理可能为空的节点和可变借用,而 C++使用nullptr
来表示空指针,Python 和 Java 则有自己的方式来表示和处理空值。尽管如此,核心算法和逻辑结构在所有语言中都是一致的。
二叉搜索树中第 K 小的元素
题目要求
设计一个算法,用于从给定的二叉搜索树中找到第 k 个最小的元素。这里的二叉搜索树是指一个每个节点的左子树只包含小于当前节点的数,右子树只包含大于当前节点的数的二叉树。算法需要接收两个输入参数:
root
:二叉搜索树的根节点。k
:需要查找的第 k 个最小元素的序号(序号从 1 开始)。
解题思路
由于二叉搜索树(BST)的性质,我们知道对其进行中序遍历可以得到一个升序排列的元素序列。因此,查找第 k 个最小元素的问题可以转化为执行中序遍历直到遍历到第 k 个节点。
以下是解题的步骤:
-
中序遍历:按照“左-根-右”的顺序递归遍历二叉搜索树。这样可以保证访问节点的顺序是按照节点值的升序。
-
计数:在遍历过程中,维护一个计数器,记录已经遍历过的节点数量。
-
查找第 k 个元素:当计数器的值达到 k 时,当前遍历到的节点值即为所求的第 k 个最小元素。
-
优化:由于只需要找到第 k 个最小元素,一旦找到,就可以停止遍历,不需要遍历整棵树。
-
边界情况处理:需要考虑树为空或 k 超出树中元素总数的情况,合理处理这些边界情况。
通过上述步骤,我们可以设计出一个高效的算法来解决这个问题,时间复杂度为 O(h + k),其中 h 是树的高度。
Golang 版本
package main
import (
"fmt"
)
// TreeNode is the structure for binary tree nodes
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
// kthSmallest function finds the kth smallest element in a BST
func kthSmallest(root *TreeNode, k int) int {
count := 0
result := 0
// Recursive function to perform inorder traversal
var inorder func(node *TreeNode)
inorder = func(node *TreeNode) {
if node == nil {
return
}
// Traverse the left subtree
inorder(node.Left)
// Increment count and check if the current node is the kth smallest
count++
if count == k {
result = node.Val
return
}
// Traverse the right subtree
inorder(node.Right)
}
// Start inorder traversal from the root
inorder(root)
return result
}
func main() {
// Example usage:
// Construct a binary search tree here and call kthSmallest with its root and k
}
这段代码定义了一个TreeNode
结构体,表示二叉树的节点,并实现了kthSmallest
函数,该函数通过中序遍历找到二叉搜索树中第 k 小的元素。main
函数中可以构造一个二叉搜索树,并调用kthSmallest
函数来测试功能。注意,这段代码只提供了核心函数的实现,实际使用时需要构建二叉树并提供root
和k
的值。
C++版本
#include <iostream>
// Definition for a binary tree node.
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution {
public:
int kthSmallest(TreeNode* root, int k) {
int count = 0;
int result = -1;
inorder(root, k, count, result);
return result;
}
private:
void inorder(TreeNode* node, int k, int& count, int& result) {
if (node == nullptr || count >= k) {
return;
}
inorder(node->left, k, count, result);
if (++count == k) {
result = node->val;
return;
}
inorder(node->right, k, count, result);
}
};
int main() {
// Example usage:
// TreeNode* root = new TreeNode(3);
// root->left = new TreeNode(1);
// root->right = new TreeNode(4);
// root->left->right = new TreeNode(2);
// Solution solution;
// int k = 1;
// std::cout << "Kth smallest element is: " << solution.kthSmallest(root, k) << std::endl;
// Output should be 1 for the given tree and k value.
// Remember to delete the created tree nodes to prevent memory leak.
}
这段代码定义了一个TreeNode
结构体,表示二叉树的节点,并实现了一个Solution
类,其中包含了kthSmallest
方法,该方法通过中序遍历找到二叉搜索树中第 k 小的元素。main
函数中可以构造一个二叉搜索树,并调用kthSmallest
方法来测试功能。请注意,示例用法中的树节点创建后,应当在适当的时候删除以防止内存泄漏。
Python 版本
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class Solution:
def kthSmallest(self, root: TreeNode, k: int) -> int:
# Initialize the count and the result placeholder
self.k = k
self.res = None
self._inorder(root)
return self.res
def _inorder(self, node):
# If node is None or result is already found, return
if not node or self.res is not None:
return
# Traverse the left subtree
self._inorder(node.left)
# Decrement k and check if we have reached the kth node
self.k -= 1
if self.k == 0:
self.res = node.val
return
# Traverse the right subtree
self._inorder(node.right)
# Example usage:
# root = TreeNode(3)
# root.left = TreeNode(1)
# root.right = TreeNode(4)
# root.left.right = TreeNode(2)
# solution = Solution()
# k = 1
# print(f"The {k}th smallest element is: {solution.kthSmallest(root, k)}")
这段代码定义了一个TreeNode
类,表示二叉树的节点,并实现了一个Solution
类,其中包含了kthSmallest
方法,该方法通过中序遍历找到二叉搜索树中第 k 小的元素。示例用法展示了如何创建一个二叉搜索树并调用kthSmallest
方法来获取第 k 小的元素。
Java 版本
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
class Solution {
private int count = 0;
private int result = Integer.MIN_VALUE;
public int kthSmallest(TreeNode root, int k) {
inorder(root, k);
return result;
}
private void inorder(TreeNode node, int k) {
if (node == null) {
return;
}
inorder(node.left, k);
if (++count == k) {
result = node.val;
return;
}
inorder(node.right, k);
}
}
public class Main {
public static void main(String[] args) {
// Example usage:
// TreeNode root = new TreeNode(3);
// root.left = new TreeNode(1);
// root.right = new TreeNode(4);
// root.left.right = new TreeNode(2);
// Solution solution = new Solution();
// int k = 1;
// System.out.println("The " + k + "th smallest element is: " + solution.kthSmallest(root, k));
}
}
这段代码定义了一个TreeNode
类,表示二叉树的节点,并实现了一个Solution
类,其中包含了kthSmallest
方法,该方法通过中序遍历找到二叉搜索树中第 k 小的元素。Main
类中的main
方法提供了如何创建一个二叉搜索树并调用kthSmallest
方法来获取第 k 小的元素的示例用法。注意,示例用法中的代码被注释掉了,以防止直接运行时出现错误。在实际使用时,应当取消注释并根据需要构造二叉树。
Rust 版本
// Definition for a binary tree node. #[derive(Debug, PartialEq, Eq)] pub struct TreeNode { pub val: i32, pub left: Option<Rc<RefCell<TreeNode>>>, pub right: Option<Rc<RefCell<TreeNode>>>, } impl TreeNode { #[inline] pub fn new(val: i32) -> Self { TreeNode { val, left: None, right: None, } } } use std::rc::Rc; use std::cell::RefCell; struct Solution; impl Solution { pub fn kth_smallest(root: Option<Rc<RefCell<TreeNode>>>, k: i32) -> i32 { let mut stack = Vec::new(); let mut current = root.clone(); let mut k = k; while current.is_some() || !stack.is_empty() { while let Some(node) = current { stack.push(node.clone()); current = node.borrow().left.clone(); } current = stack.pop(); if let Some(node) = current { k -= 1; if k == 0 { return node.borrow().val; } current = node.borrow().right.clone(); } } panic!("K is larger than the number of nodes in the tree"); } } fn main() { // Example usage: // let root = Some(Rc::new(RefCell::new(TreeNode::new(3)))); // root.as_ref().unwrap().borrow_mut().left = Some(Rc::new(RefCell::new(TreeNode::new(1)))); // root.as_ref().unwrap().borrow_mut().right = Some(Rc::new(RefCell::new(TreeNode::new(4)))); // root.as_ref().unwrap().borrow_mut().left.as_ref().unwrap().borrow_mut().right = Some(Rc::new(RefCell::new(TreeNode::new(2)))); // let k = 1; // println!("The {}th smallest element is: {}", k, Solution::kth_smallest(root, k)); }
这段代码使用 Rust 语言定义了一个TreeNode
结构体,表示二叉树的节点,并实现了一个Solution
结构体,其中包含了kth_smallest
函数,该函数通过中序遍历的迭代方式找到二叉搜索树中第 k 小的元素。main
函数中提供了如何创建一个二叉搜索树并调用kth_smallest
函数来获取第 k 小的元素的示例用法。注意,示例用法中的代码被注释掉了,以防止直接运行时出现错误。在实际使用时,应当取消注释并根据需要构造二叉树。
总结
上述代码段展示了如何在不同编程语言中实现查找二叉搜索树中第 k 小的元素的算法。核心思想是利用二叉搜索树的性质:中序遍历二叉搜索树可以得到一个递增的有序序列。因此,通过中序遍历直到遍历到第 k 个节点,就可以找到第 k 小的元素。
在不同的编程语言中,这一算法的实现有以下共同点:
-
定义二叉树节点:每种语言都定义了一个
TreeNode
结构(或类),包含值val
,左子节点left
和右子节点right
。 -
中序遍历:所有语言都使用了中序遍历的方法来遍历树。中序遍历首先访问左子树,然后访问当前节点,最后访问右子树。
-
计数器:在遍历过程中,使用一个计数器来记录当前已经遍历过的节点数。当计数器达到 k 时,当前节点的值即为所求的第 k 小的元素。
-
递归或迭代:根据语言的特性和编程风格的不同,中序遍历可以通过递归或迭代的方式实现。递归实现更为直观,而迭代实现通常需要手动维护一个栈。
-
返回结果:一旦找到第 k 小的元素,算法就会返回该元素的值。
不同编程语言的实现细节可能有所不同,例如在 Rust 中使用Option<Rc<RefCell<TreeNode>>>
来处理节点的所有权和可变性,而在 Java 和 Python 中则直接使用指针或引用。但无论语言如何变化,算法的核心逻辑和步骤是一致的。
二叉树的右视图
题目要求
给定一个二叉树的根节点root
,要求编写一个算法来模拟观察者站在树的右侧所看到的视图。具体来说,从树的顶部开始,直到底部结束,记录下每一层最右边节点的值。算法需要返回一个由这些节点值组成的列表。
解题思路
要解决这个问题,我们可以采用层次遍历(也称为广度优先搜索)的方法。层次遍历可以按照从上到下的顺序访问树中的每一层节点,并且可以容易地修改以仅记录每层最右侧的节点值。以下是解决这个问题的步骤:
- 初始化一个队列,用于存放待访问的节点,首先将根节点
root
入队。 - 当队列不为空时,进行以下操作:
- 获取当前队列的长度,这个长度相当于当前层的节点数。
- 遍历当前层的所有节点:
- 对于每一层,从左到右取出节点(即队列的前端),并将其子节点(先左后右)加入队列的后端。
- 记录下当前层最后一个节点的值,因为从右侧看过去,这是该层唯一可见的节点。
- 每层遍历结束后,将最后一个节点的值加入到结果列表中。
- 当所有层都遍历完毕后,返回结果列表。
这样,我们就可以得到从二叉树的右侧看到的节点值的列表。
Golang 版本
package main
import (
"fmt"
)
// TreeNode is a structure for a binary tree node.
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
// rightSideView returns the values of the nodes you can see ordered from top to bottom.
func rightSideView(root *TreeNode) []int {
if root == nil {
return []int{}
}
result := []int{}
queue := []*TreeNode{root}
for len(queue) > 0 {
levelLength := len(queue)
for i := 0; i < levelLength; i++ {
node := queue[0]
queue = queue[1:]
// When we reach the last node of the current level, add it to the result.
if i == levelLength-1 {
result = append(result, node.Val)
}
// Add child nodes to the queue for the next level.
if node.Left != nil {
queue = append(queue, node.Left)
}
if node.Right != nil {
queue = append(queue, node.Right)
}
}
}
return result
}
func main() {
// Example usage:
// Construct a binary tree as an example.
// 1
// / \
// 2 3
// \ \
// 5 4
root := &TreeNode{Val: 1}
root.Left = &TreeNode{Val: 2}
root.Right = &TreeNode{Val: 3}
root.Left.Right = &TreeNode{Val: 5}
root.Right.Right = &TreeNode{Val: 4}
// Get the right side view of the binary tree.
view := rightSideView(root)
fmt.Println(view) // Output: [1 3 4]
}
这段代码定义了一个TreeNode
结构体,实现了一个rightSideView
函数来解决问题,并提供了一个主函数main
来演示如何使用这个函数。代码中的示例二叉树与题目描述的功能相匹配,最终打印出从右侧看到的节点值。
C++版本
#include <iostream>
#include <vector>
#include <queue>
// Definition for a binary tree node.
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution {
public:
std::vector<int> rightSideView(TreeNode* root) {
std::vector<int> view;
if (!root) return view;
std::queue<TreeNode*> nodesQueue;
nodesQueue.push(root);
while (!nodesQueue.empty()) {
int size = nodesQueue.size();
for (int i = 0; i < size; ++i) {
TreeNode* node = nodesQueue.front();
nodesQueue.pop();
if (i == size - 1) view.push_back(node->val);
if (node->left) nodesQueue.push(node->left);
if (node->right) nodesQueue.push(node->right);
}
}
return view;
}
};
int main() {
// Example usage:
TreeNode *root = new TreeNode(1);
root->left = new TreeNode(2);
root->right = new TreeNode(3);
root->left->right = new TreeNode(5);
root->right->right = new TreeNode(4);
Solution solution;
std::vector<int> result = solution.rightSideView(root);
for (int val : result) {
std::cout << val << " ";
}
std::cout << std::endl;
// Clean up allocated nodes if necessary
delete root->left->right;
delete root->right->right;
delete root->left;
delete root->right;
delete root;
return 0;
}
这段 C++代码定义了一个TreeNode
结构体和一个Solution
类,其中包含了解决问题的rightSideView
方法。在main
函数中,我们创建了一个示例二叉树,并调用rightSideView
方法来获取从右侧看到的节点值,然后打印这些值。最后,代码中还包含了适当的内存清理操作。
Python 版本
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class Solution:
def rightSideView(self, root: TreeNode) -> list[int]:
if not root:
return []
rightside = []
queue = [root]
while queue:
level_length = len(queue)
for i in range(level_length):
node = queue.pop(0)
# If it's the last node in the current level, add it to the rightside view
if i == level_length - 1:
rightside.append(node.val)
# Add child nodes in the queue
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
return rightside
# Example usage:
if __name__ == "__main__":
# Construct a binary tree as an example.
# 1
# / \
# 2 3
# \ \
# 5 4
root = TreeNode(1, TreeNode(2, None, TreeNode(5)), TreeNode(3, None, TreeNode(4)))
# Get the right side view of the binary tree.
solution = Solution()
view = solution.rightSideView(root)
print(view) # Output: [1, 3, 4]
这段 Python 代码定义了一个TreeNode
类用于表示二叉树的节点,以及一个Solution
类,其中包含了解决问题的rightSideView
方法。在__main__
部分,我们创建了一个示例二叉树,并调用rightSideView
方法来获取从右侧看到的节点值,然后打印这些值。
Java 版本
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
// Definition for a binary tree node.
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
class Solution {
public List<Integer> rightSideView(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) return result;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int levelLength = queue.size();
for (int i = 0; i < levelLength; i++) {
TreeNode currentNode = queue.poll();
// If it's the last node of this level, add it to the result list
if (i == levelLength - 1) {
result.add(currentNode.val);
}
// Add left and right children of the current node to the queue
if (currentNode.left != null) {
queue.offer(currentNode.left);
}
if (currentNode.right != null) {
queue.offer(currentNode.right);
}
}
}
return result;
}
}
// Example usage
public class Main {
public static void main(String[] args) {
TreeNode root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.right = new TreeNode(5);
root.right.right = new TreeNode(4);
Solution solution = new Solution();
List<Integer> rightView = solution.rightSideView(root);
for (int val : rightView) {
System.out.print(val + " ");
}
}
}
这段 Java 代码定义了一个TreeNode
类用于表示二叉树的节点,以及一个Solution
类,其中包含了解决问题的rightSideView
方法。在Main
类的main
方法中,我们创建了一个示例二叉树,并调用rightSideView
方法来获取从右侧看到的节点值,然后打印这些值。
Rust 版本
use std::cell::RefCell; use std::rc::Rc; use std::collections::VecDeque; // Definition for a binary tree node. #[derive(Debug, PartialEq, Eq)] pub struct TreeNode { pub val: i32, pub left: Option<Rc<RefCell<TreeNode>>>, pub right: Option<Rc<RefCell<TreeNode>>>, } impl TreeNode { #[inline] pub fn new(val: i32) -> Self { TreeNode { val, left: None, right: None, } } } struct Solution; impl Solution { pub fn right_side_view(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<i32> { let mut view = Vec::new(); if root.is_none() { return view; } let mut queue = VecDeque::new(); queue.push_back(root); while let Some(node) = queue.pop_front() { let mut level_size = queue.len(); for i in 0..level_size { if let Some(current) = queue.pop_front() { if i == level_size - 1 { // If it's the last node in the current level, add it to the view view.push(current.borrow().val); } if let Some(left) = ¤t.borrow().left { queue.push_back(Some(left.clone())); } if let Some(right) = ¤t.borrow().right { queue.push_back(Some(right.clone())); } } } // Add the next level's nodes to the queue if let Some(node) = node { if let Some(left) = node.borrow().left.clone() { queue.push_back(Some(left)); } if let Some(right) = node.borrow().right.clone() { queue.push_back(Some(right)); } } } view } } fn main() { let root = Some(Rc::new(RefCell::new(TreeNode::new(1)))); let left = Some(Rc::new(RefCell::new(TreeNode::new(2)))); let right = Some(Rc::new(RefCell::new(TreeNode::new(3)))); let left_right = Some(Rc::new(RefCell::new(TreeNode::new(5)))); let right_right = Some(Rc::new(RefCell::new(TreeNode::new(4)))); if let Some(root_node) = &root { root_node.borrow_mut().left = left; root_node.borrow_mut().right = right; } if let Some(left_node) = &root.as_ref().unwrap().borrow().left { left_node.borrow_mut().right = left_right; } if let Some(right_node) = &root.as_ref().unwrap().borrow().right { right_node.borrow_mut().right = right_right; } let view = Solution::right_side_view(root); println!("{:?}", view); // Should print [1, 3, 4] }
这段 Rust 代码定义了一个TreeNode
结构体用于表示二叉树的节点,以及一个Solution
结构体,其中包含了解决问题的right_side_view
函数。在main
函数中,我们创建了一个示例二叉树,并调用right_side_view
函数来获取从右侧看到的节点值,然后打印这些值。
总结
上面的 Rust 解法实现了一个二叉树的右视图功能。这个功能通过以下步骤完成:
-
定义了一个
TreeNode
结构体,用于表示二叉树的节点,每个节点包含一个整数值、一个可选的左子节点和一个可选的右子节点。 -
实现了一个
Solution
结构体,其中包含了名为right_side_view
的函数。这个函数接受一个Option<Rc<RefCell<TreeNode>>>
类型的参数,代表二叉树的根节点。 -
在
right_side_view
函数中,首先检查根节点是否为空。如果为空,则直接返回一个空的向量。 -
使用
VecDeque
来实现一个队列,用于层序遍历二叉树。队列中存储的是Option<Rc<RefCell<TreeNode>>>
类型的元素。 -
遍历二叉树的每一层,对于每一层的节点,只将该层最后一个节点的值添加到结果向量中。这是通过检查当前节点是否是该层的最后一个节点来实现的。
-
对于每个节点,如果它有左子节点或右子节点,就将它们添加到队列中,以便在下一次循环中遍历它们。
-
最后,函数返回包含二叉树右视图的向量。
在main
函数中,创建了一个示例二叉树,并调用了Solution
结构体的right_side_view
函数来获取并打印从右侧看到的节点值。这个示例展示了如何使用 Rust 的Rc
和RefCell
来处理可变共享所有权,这在二叉树的上下文中非常有用。
二叉树展开为链表
题目要求
给定一个二叉树的根节点 root
,要求将这棵二叉树原地展开为一个单链表。这里的“原地”意味着不能使用额外的数据结构,只能通过修改原二叉树节点的指针来实现。展开规则如下:
- 展开后的单链表使用原二叉树的
TreeNode
结构,每个节点的right
指针指向链表中的下一个节点,而每个节点的left
指针必须指向null
。 - 展开后的单链表中节点的顺序应该与原二叉树的先序遍历(根-左-右)顺序相同。
解题思路
要将二叉树展开为一个单链表,并且要求展开后的顺序与先序遍历相同,我们可以按照以下步骤来实现:
-
先序遍历的模拟:我们可以模拟先序遍历的过程,这个过程中我们会访问每个节点。在访问的同时,我们可以修改节点的指针来达到展开的目的。
-
节点处理:对于每个节点,我们需要做的是:
- 将当前节点的左子树插入到当前节点和右子树之间。
- 将当前节点的左子树设置为
null
。 - 将原来的右子树接到当前左子树的最右边的节点上。
-
递归或迭代:这个过程可以通过递归或迭代的方式完成。递归的话,我们可以先递归处理左子树,然后递归处理右子树,最后进行节点的连接。迭代的话,我们可以使用栈来模拟递归过程。
-
原地操作:由于题目要求原地展开,我们不能创建新的节点,只能修改现有节点的指针。
-
细节处理:在实际操作中,我们需要注意:
- 在连接节点时,要考虑左子树为空的情况。
- 在找左子树最右边的节点时,要考虑左子树可能已经是一个展开的链表的情况。
通过以上步骤,我们可以将二叉树原地展开为一个单链表,且满足先序遍历的顺序。
Golang 版本
package main
// TreeNode is the definition for a binary tree node.
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
func flatten(root *TreeNode) {
// curr points to the current node being processed
curr := root
for curr != nil {
if curr.Left != nil {
// Find the rightmost node of the left subtree
prev := curr.Left
for prev.Right != nil {
prev = prev.Right
}
// Make the rightmost node of the left subtree point to the current right subtree
prev.Right = curr.Right
// Move the left subtree to the right, effectively flattening the left part
curr.Right = curr.Left
curr.Left = nil
}
// Move on to the right side of the tree
curr = curr.Right
}
}
func main() {
// Example usage:
// Construct the binary tree here and call flatten(root)
}
这段代码定义了一个TreeNode
结构体,代表二叉树的节点,并实现了flatten
函数,该函数接受一个二叉树的根节点,并原地将其展开为一个单链表。主要逻辑在于遍历每个节点,如果当前节点有左子树,则找到左子树的最右节点,将当前节点的右子树接到该最右节点的右指针上,然后将左子树移到右边,左指针置为nil
。然后继续处理当前节点的右子树,直到所有节点都被处理完毕。
C++版本
#include <iostream>
// Definition for a binary tree node.
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution {
public:
void flatten(TreeNode* root) {
if (!root) return;
TreeNode* node = root;
while (node) {
if (node->left) {
// Find the rightmost node of the left subtree
TreeNode* rightmost = node->left;
while (rightmost->right) {
rightmost = rightmost->right;
}
// Connect the rightmost node of the left subtree with the right subtree
rightmost->right = node->right;
// Move the left subtree to the right
node->right = node->left;
node->left = nullptr;
}
// Move to the right side of the tree
node = node->right;
}
}
};
int main() {
// Example usage:
// TreeNode* root = new TreeNode(1);
// root->left = new TreeNode(2);
// root->right = new TreeNode(5);
// ...
// Solution().flatten(root);
// The tree should now be flattened
return 0;
}
这段 C++代码定义了一个二叉树节点结构体TreeNode
和一个解决方案类Solution
。Solution
类中的flatten
函数接受一个二叉树的根节点,并原地将其展开为一个单链表。函数的主体是一个循环,遍历每个节点,如果当前节点有左子树,则找到左子树的最右节点,并将当前节点的右子树接到该最右节点的右指针上。然后将左子树移到右边,左指针置为nullptr
。之后继续处理当前节点的右子树,直到所有节点都被处理完毕。在main
函数中,你可以根据需要构造二叉树,并调用flatten
函数来展平它。
Python 版本
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class Solution:
def flatten(self, root: TreeNode) -> None:
"""
Do not return anything, modify root in-place instead.
"""
# Start with the root node
current = root
while current:
if current.left:
# Find the rightmost node of the left subtree
rightmost = current.left
while rightmost.right:
rightmost = rightmost.right
# Connect the rightmost node of the left subtree with the right subtree
rightmost.right = current.right
# Move the left subtree to the right
current.right = current.left
current.left = None
# Move to the right side of the tree
current = current.right
# Example usage:
# if __name__ == "__main__":
# root = TreeNode(1)
# root.left = TreeNode(2)
# root.right = TreeNode(5)
# root.left.left = TreeNode(3)
# root.left.right = TreeNode(4)
# root.right.right = TreeNode(6)
# Solution().flatten(root)
# # Now the tree is flattened
这段 Python 代码定义了一个二叉树节点类TreeNode
和一个解决方案类Solution
。Solution
类中的flatten
方法接受一个二叉树的根节点,并原地将其展开为一个单链表。方法的主体是一个循环,遍历每个节点,如果当前节点有左子树,则找到左子树的最右节点,并将当前节点的右子树接到该最右节点的右指针上。然后将左子树移到右边,左指针置为None
。之后继续处理当前节点的右子树,直到所有节点都被处理完毕。在if __name__ == "__main__":
部分,你可以构造一个二叉树,并调用flatten
方法来展平它。
Java 版本
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
public class Solution {
public void flatten(TreeNode root) {
TreeNode current = root;
while (current != null) {
if (current.left != null) {
// Find the rightmost node of the left subtree
TreeNode rightmost = current.left;
while (rightmost.right != null) {
rightmost = rightmost.right;
}
// Connect the rightmost node of the left subtree with the right subtree
rightmost.right = current.right;
// Move the left subtree to the right
current.right = current.left;
current.left = null;
}
// Move to the right side of the tree
current = current.right;
}
}
}
// Example usage:
// public static void main(String[] args) {
// Solution solution = new Solution();
// TreeNode root = new TreeNode(1);
// root.left = new TreeNode(2);
// root.right = new TreeNode(5);
// root.left.left = new TreeNode(3);
// root.left.right = new TreeNode(4);
// root.right.right = new TreeNode(6);
// solution.flatten(root);
// // Now the tree is flattened
// }
这段 Java 代码定义了一个二叉树节点类TreeNode
和一个解决方案类Solution
。Solution
类中的flatten
方法接受一个二叉树的根节点,并原地将其展开为一个单链表。方法的主体是一个循环,遍历每个节点,如果当前节点有左子树,则找到左子树的最右节点,并将当前节点的右子树接到该最右节点的右指针上。然后将左子树移到右边,左指针置为null
。之后继续处理当前节点的右子树,直到所有节点都被处理完毕。在main
方法中,你可以构造一个二叉树,并调用flatten
方法来展平它。
Rust 版本
// Definition for a binary tree node. #[derive(Debug, PartialEq, Eq)] pub struct TreeNode { pub val: i32, pub left: Option<Rc<RefCell<TreeNode>>>, pub right: Option<Rc<RefCell<TreeNode>>>, } impl TreeNode { #[inline] pub fn new(val: i32) -> Self { TreeNode { val, left: None, right: None, } } } use std::rc::Rc; use std::cell::RefCell; pub struct Solution {} impl Solution { pub fn flatten(root: &mut Option<Rc<RefCell<TreeNode>>>) { let mut current = root.clone(); while let Some(node) = current { let mut node_borrowed = node.borrow_mut(); if let Some(left) = node_borrowed.left.take() { let mut rightmost = left.clone(); // Find the rightmost node of the left subtree while let Some(right) = rightmost.borrow().right.clone() { rightmost = right; } // Connect the rightmost node of the left subtree with the right subtree rightmost.borrow_mut().right = node_borrowed.right.take(); // Move the left subtree to the right node_borrowed.right = Some(left); } current = node_borrowed.right.clone(); } } } // Example usage: // fn main() { // let mut root = Some(Rc::new(RefCell::new(TreeNode::new(1)))); // let left = Some(Rc::new(RefCell::new(TreeNode::new(2)))); // let right = Some(Rc::new(RefCell::new(TreeNode::new(5)))); // root.as_mut().unwrap().borrow_mut().left = left; // root.as_mut().unwrap().borrow_mut().right = right; // Solution::flatten(&mut root); // // Now the tree is flattened // }
这段 Rust 代码定义了一个二叉树节点结构体TreeNode
和一个解决方案结构体Solution
。Solution
结构体中的flatten
方法接受一个可变引用到一个Option<Rc<RefCell<TreeNode>>>
,这是 Rust 中常用的方式来共享可变引用。方法的主体是一个循环,遍历每个节点,如果当前节点有左子树,则找到左子树的最右节点,并将当前节点的右子树接到该最右节点的右指针上。然后将左子树移到右边,左指针置为None
。之后继续处理当前节点的右子树,直到所有节点都被处理完毕。在main
函数中,你可以构造一个二叉树,并调用flatten
方法来展平它。
总结
上述 Rust 版本的解法采用了递归展平二叉树的方法。这种方法的核心思想是将二叉树的左子树和右子树分别展平,然后将左子树作为右子树,原右子树接到展平后的左子树的最右侧节点。这样的操作顺序符合二叉树的先序遍历(根-左-右)。
解法步骤:
- 对当前节点,如果存在左子树,找到左子树的最右侧节点。
- 将当前节点的右子树接到找到的最右侧节点的右指针上。
- 将当前节点的左子树置为 None,并将左子树接到当前节点的右指针上。
- 移动到下一个右子节点,重复步骤 1-3,直到遍历完所有节点。
编码易错点:
- 所有权与借用:在 Rust 中,正确处理所有权和借用是非常关键的。在操作树的节点时,需要确保使用
Rc
和RefCell
来正确管理引用计数和内部可变性。 - 循环引用:在树的转换过程中,需要注意不要创建循环引用,否则会导致内存泄漏。
- Option 的处理:在 Rust 中,节点可能是
Some
或None
。在处理这些Option
值时,需要使用if let
或while let
来正确地解构Some
值,并在节点为None
时跳过操作。 - 可变借用规则:在同一时间,你只能拥有一个可变引用或任意数量的不可变引用之一。在
flatten
函数中,需要小心地处理可变引用,避免同时存在多个可变引用。 - 递归与迭代:虽然这个问题可以用递归解决,但上述解法使用了迭代。在实现时,要注意迭代和递归的选择,以及它们在栈空间上的影响。
- 节点连接错误:在将左子树的最右侧节点连接到原右子树时,需要确保不会丢失原右子树的引用。
- 修改后继续使用旧引用:在修改树的结构后,之前的引用可能不再有效。需要确保在修改树结构后使用最新的引用。
正确处理这些易错点是实现一个无 bug 的 Rust 程序的关键。在实际编码时,应该通过编译器的所有权检查和借用检查来确保代码的正确性。此外,单元测试和集成测试也是确保代码质量的重要手段。
从前序与中序遍历序列构造二叉树
题目要求
你需要根据给定的两个数组 preorder
和 inorder
来重建一棵二叉树。preorder
数组代表了这棵树的先序遍历结果,而 inorder
数组代表了同一棵树的中序遍历结果。你的任务是根据这两个遍历结果来构造原始的二叉树,并返回这棵树的根节点。
解题思路
为了根据 preorder
(先序遍历)和 inorder
(中序遍历)数组重建原始的二叉树,我们可以遵循以下步骤:
-
确定根节点:
- 先序遍历的第一个元素总是树的根节点。
-
在中序遍历中找到根节点:
- 在中序遍历数组中找到根节点的位置,这将数组分为两部分,左边是树的左子树,右边是树的右子树。
-
划分先序遍历数组:
- 根据中序遍历中左子树的节点数量,我们可以在先序遍历数组中划分出左子树和右子树对应的部分。
-
递归构造左右子树:
- 使用中序遍历中的左子树部分和先序遍历中对应的部分递归构造左子树。
- 使用中序遍历中的右子树部分和先序遍历中对应的部分递归构造右子树。
-
构建树并返回根节点:
- 递归地构建整棵树,最后返回根节点。
在实现时,我们需要注意以下几点:
- 递归的基本情况是当先序遍历或中序遍历数组为空时,返回
null
。 - 我们可以使用哈希表来存储中序遍历中每个值对应的索引,这样可以快速定位根节点在中序遍历中的位置,从而优化查找时间。
- 每次递归时,我们都需要更新先序遍历和中序遍历数组中的子数组范围,以便正确地划分左右子树。
通过以上步骤,我们可以高效地重建出原始的二叉树结构。
Golang 版本
package main
import (
"fmt"
)
// TreeNode is the structure for tree nodes
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
// buildTree constructs a binary tree from preorder and inorder traversal slices.
func buildTree(preorder []int, inorder []int) *TreeNode {
if len(preorder) == 0 || len(inorder) == 0 {
return nil
}
// The first element in preorder is always the root.
rootVal := preorder[0]
root := &TreeNode{Val: rootVal}
// Find the index of the root in inorder slice.
i := 0
for ; i < len(inorder); i++ {
if inorder[i] == rootVal {
break
}
}
// Recursively build the left and right subtrees.
root.Left = buildTree(preorder[1:i+1], inorder[:i])
root.Right = buildTree(preorder[i+1:], inorder[i+1:])
return root
}
func main() {
// Example usage:
preorder := []int{3, 9, 20, 15, 7}
inorder := []int{9, 3, 15, 20, 7}
root := buildTree(preorder, inorder)
fmt.Println("Root value of the tree is:", root.Val)
// Output will be the root of the tree constructed which is 3 in this case.
}
这段代码定义了一个TreeNode
结构体,表示树的节点,并实现了buildTree
函数来根据给定的preorder
和inorder
数组重建二叉树。在main
函数中,我们提供了一个使用示例,构建了一棵树,并打印出了树的根节点的值。
C++版本
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
// Definition for a binary tree node.
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution {
public:
unordered_map<int, int> inMap; // Maps value to its index in inorder
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
// Build a map to store the value-index pairs for quick access
for (int i = 0; i < inorder.size(); i++) {
inMap[inorder[i]] = i;
}
return buildTree(preorder, 0, preorder.size() - 1, 0);
}
TreeNode* buildTree(vector<int>& preorder, int preStart, int preEnd, int inStart) {
if (preStart > preEnd) return NULL;
TreeNode* root = new TreeNode(preorder[preStart]);
int inRoot = inMap[root->val];
int numsLeft = inRoot - inStart;
root->left = buildTree(preorder, preStart + 1, preStart + numsLeft, inStart);
root->right = buildTree(preorder, preStart + numsLeft + 1, preEnd, inRoot + 1);
return root;
}
};
int main() {
vector<int> preorder = {3, 9, 20, 15, 7};
vector<int> inorder = {9, 3, 15, 20, 7};
Solution solution;
TreeNode* root = solution.buildTree(preorder, inorder);
cout << "Root value of the tree is: " << root->val << endl;
// Output should be the root of the tree constructed which is 3 in this case.
// Remember to delete the allocated memory for the tree
// This part is omitted for brevity
return 0;
}
这段 C++代码定义了一个TreeNode
结构体,表示树的节点,并实现了一个Solution
类,其中包含了buildTree
函数来根据给定的preorder
和inorder
数组重建二叉树。在main
函数中,我们提供了一个使用示例,构建了一棵树,并打印出了树的根节点的值。注意,在实际应用中,你可能需要编写额外的代码来释放分配给树节点的内存。
Python 版本
class TreeNode:
def __init__(self, x):
self.val = x
self.left = None
self.right = None
class Solution:
def buildTree(self, preorder, inorder):
if not preorder or not inorder:
return None
# The first element of preorder is always the root.
root = TreeNode(preorder[0])
mid = inorder.index(preorder[0])
# Recursively build the left and right subtree.
root.left = self.buildTree(preorder[1:mid+1], inorder[:mid])
root.right = self.buildTree(preorder[mid+1:], inorder[mid+1:])
return root
# Example usage:
if __name__ == "__main__":
preorder = [3, 9, 20, 15, 7]
inorder = [9, 3, 15, 20, 7]
solution = Solution()
root = solution.buildTree(preorder, inorder)
print("Root value of the tree is:", root.val)
# Output should be the root of the tree constructed which is 3 in this case.
这段 Python 代码定义了一个TreeNode
类,表示树的节点,并实现了一个Solution
类,其中包含了buildTree
方法来根据给定的preorder
和inorder
列表重建二叉树。在脚本的主部分,我们提供了一个使用示例,构建了一棵树,并打印出了树的根节点的值。
Java 版本
import java.util.HashMap;
import java.util.Map;
// Definition for a binary tree node.
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
public class Solution {
private Map<Integer, Integer> inMap = new HashMap<>();
private int preIndex = 0;
public TreeNode buildTree(int[] preorder, int[] inorder) {
// Build a hashmap to store value -> its index relations
for (int i = 0; i < inorder.length; i++) {
inMap.put(inorder[i], i);
}
return buildTree(preorder, 0, preorder.length - 1);
}
private TreeNode buildTree(int[] preorder, int inStart, int inEnd) {
if (inStart > inEnd) return null;
// The current root is always the next element in the preorder sequence
TreeNode root = new TreeNode(preorder[preIndex++]);
// Root splits inorder list into left and right subtrees
int inIndex = inMap.get(root.val);
// Build left subtree
root.left = buildTree(preorder, inStart, inIndex - 1);
// Build right subtree
root.right = buildTree(preorder, inIndex + 1, inEnd);
return root;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] preorder = {3, 9, 20, 15, 7};
int[] inorder = {9, 3, 15, 20, 7};
TreeNode root = solution.buildTree(preorder, inorder);
System.out.println("Root value of the tree is: " + root.val);
// Output should be the root of the tree constructed which is 3 in this case.
}
}
这段 Java 代码定义了一个TreeNode
类,表示树的节点,并实现了一个Solution
类,其中包含了buildTree
方法来根据给定的preorder
和inorder
数组重建二叉树。在main
方法中,我们提供了一个使用示例,构建了一棵树,并打印出了树的根节点的值。
Rust 版本
use std::collections::HashMap; // Definition for a binary tree node. #[derive(Debug, PartialEq, Eq)] pub struct TreeNode { pub val: i32, pub left: Option<Rc<RefCell<TreeNode>>>, pub right: Option<Rc<RefCell<TreeNode>>>, } impl TreeNode { #[inline] pub fn new(val: i32) -> Self { TreeNode { val, left: None, right: None, } } } struct Solution; impl Solution { pub fn build_tree(preorder: Vec<i32>, inorder: Vec<i32>) -> Option<Rc<RefCell<TreeNode>>> { let mut in_map = HashMap::new(); for (i, &val) in inorder.iter().enumerate() { in_map.insert(val, i); } let mut pre_idx = 0; Solution::build(&preorder, &mut pre_idx, 0, inorder.len() as i32 - 1, &in_map) } fn build( preorder: &Vec<i32>, pre_idx: &mut usize, in_start: i32, in_end: i32, in_map: &HashMap<i32, usize>, ) -> Option<Rc<RefCell<TreeNode>>> { if in_start > in_end { return None; } let root_val = preorder[*pre_idx]; let root = Rc::new(RefCell::new(TreeNode::new(root_val))); *pre_idx += 1; let in_root_idx = *in_map.get(&root_val).unwrap() as i32; root.borrow_mut().left = Solution::build(preorder, pre_idx, in_start, in_root_idx - 1, in_map); root.borrow_mut().right = Solution::build(preorder, pre_idx, in_root_idx + 1, in_end, in_map); Some(root) } } fn main() { let preorder = vec![3, 9, 20, 15, 7]; let inorder = vec![9, 3, 15, 20, 7]; let tree = Solution::build_tree(preorder, inorder); println!("Root value of the tree is: {:?}", tree.as_ref().map(|node| node.borrow().val)); // Output should be the root of the tree constructed which is 3 in this case. }
这段 Rust 代码定义了一个TreeNode
结构体,表示树的节点,并实现了一个Solution
结构体,其中包含了build_tree
方法来根据给定的preorder
和inorder
向量重建二叉树。在main
函数中,我们提供了一个使用示例,构建了一棵树,并打印出了树的根节点的值。注意,Rust 中的TreeNode
使用了Rc
和RefCell
来允许可变共享所有权,这是 Rust 特有的内存安全策略的一部分。
总结
这种解法的关键在于理解先序遍历和中序遍历的特点,以及如何利用这些特点来确定树的结构。先序遍历给出了根节点的顺序,而中序遍历则确定了左右子树的分界。通过递归地应用这些规则,可以逐步构建出整棵树。
路径总和 III
题目要求
给定一个二叉树的根节点 root
和一个整数 targetSum
,要求计算在这棵二叉树中,节点值之和等于 targetSum
的路径数量。这里的路径定义为一系列节点组成的集合,这些节点在树中从上到下连接而成,不一定要从根节点开始,也不一定要在叶子节点结束。
解题思路
解决这个问题可以采用“双重递归”的策略:
- 外层递归 - 遍历每个节点,将每个节点都当作一次新的路径的起点。
- 内层递归 - 对于每个起点,执行深度优先搜索(DFS),探索所有可能的向下路径。在这个过程中,累加经过的节点值,并与
targetSum
进行比较。
具体步骤如下:
- 从根节点开始,对二叉树进行遍历。可以使用前序遍历(根-左-右),因为我们需要从每个节点出发去寻找可能的路径。
- 对于遍历到的每个节点,执行以下操作:
- 调用一个递归函数(内层递归),该函数尝试找出以当前节点为起点,向下延伸的所有路径中,节点值之和等于
targetSum
的路径数量。 - 在内层递归中,维护一个当前累计和的变量,每访问一个节点,就将该节点的值加到累计和上。
- 如果当前累计和等于
targetSum
,则找到了一条路径,路径计数加一。 - 无论当前累计和是否等于
targetSum
,都继续向下递归,直到叶子节点。 - 内层递归需要返回当前节点出发的有效路径数,这样可以将左右子树返回的路径数加到一起。
- 调用一个递归函数(内层递归),该函数尝试找出以当前节点为起点,向下延伸的所有路径中,节点值之和等于
- 将外层递归遍历到的每个节点作为起点得到的路径数累加,得到最终的路径总数。
注意事项:
- 路径方向必须是向下的,即只能从父节点到子节点。
- 路径不必从根节点开始,也不必在叶子节点结束。
- 需要考虑到负数的情况,即路径中可以包含节点值为负数的节点。
这个问题的关键在于理解“双重递归”的概念,即外层递归用于遍历树中的所有节点,内层递归用于从当前节点出发,向下寻找所有可能的路径。通过这种方式,我们可以确保考虑了从树中任意节点出发的所有可能路径。
Golang 版本
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
func pathSum(root *TreeNode, targetSum int) int {
if root == nil {
return 0
}
// 外层递归遍历每个节点
return dfs(root, targetSum) + pathSum(root.Left, targetSum) + pathSum(root.Right, targetSum)
}
// 内层递归函数,寻找以node为起点的路径数
func dfs(node *TreeNode, targetSum int) int {
if node == nil {
return 0
}
// 当前路径和
sum := targetSum - node.Val
// 如果当前累计和等于targetSum,则找到一条路径
result := 0
if sum == 0 {
result = 1
}
// 继续递归左右子树
return result + dfs(node.Left, sum) + dfs(node.Right, sum)
}
这段代码定义了一个二叉树节点的结构体TreeNode
,并实现了两个函数:pathSum
和dfs
。pathSum
函数是解决问题的主入口,它遍历每个节点,并对每个节点调用dfs
函数来寻找以该节点为起点的所有路径。dfs
函数则是一个递归函数,用于计算以当前节点为起点,向下延伸的路径中,节点值之和等于targetSum
的路径数量。如果当前累计和等于targetSum
,则找到一条路径,并继续递归左右子树以寻找更多的路径。最终,pathSum
函数返回所有找到的路径数量。
C++版本
#include <iostream>
// Definition for a binary tree node.
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution {
public:
int pathSum(TreeNode* root, int targetSum) {
if (!root) return 0;
// 外层递归遍历每个节点
return dfs(root, targetSum) + pathSum(root->left, targetSum) + pathSum(root->right, targetSum);
}
private:
// 内层递归函数,寻找以node为起点的路径数
int dfs(TreeNode* node, int targetSum) {
if (!node) return 0;
// 当前路径和
int sum = targetSum - node->val;
// 如果当前累计和等于targetSum,则找到一条路径
int result = sum == 0 ? 1 : 0;
// 继续递归左右子树
return result + dfs(node->left, sum) + dfs(node->right, sum);
}
};
int main() {
// Example of usage:
// TreeNode* root = new TreeNode(10);
// root->left = new TreeNode(5);
// root->right = new TreeNode(-3);
// ... (construct the rest of the tree)
// Solution solution;
// std::cout << solution.pathSum(root, 8) << std::endl;
// ... (remember to delete the allocated nodes to prevent memory leak)
return 0;
}
这段 C++代码定义了一个二叉树节点的结构体TreeNode
和一个解决问题的类Solution
。Solution
类中的pathSum
函数是解决问题的主入口,它遍历每个节点,并对每个节点调用dfs
函数来寻找以该节点为起点的所有路径。dfs
函数是一个递归函数,用于计算以当前节点为起点,向下延伸的路径中,节点值之和等于targetSum
的路径数量。如果当前累计和等于targetSum
,则找到一条路径,并继续递归左右子树以寻找更多的路径。最终,pathSum
函数返回所有找到的路径数量。
在main
函数中,你可以构造一个二叉树并调用Solution
类的pathSum
函数来测试代码。记得在使用完毕后删除分配的节点,以防止内存泄漏。
Python 版本
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class Solution:
def pathSum(self, root: TreeNode, targetSum: int) -> int:
def dfs(node, current_sum):
if not node:
return 0
current_sum += node.val
return (current_sum == targetSum) + dfs(node.left, current_sum) + dfs(node.right, current_sum)
if not root:
return 0
# 从当前节点开始的路径数
paths_from_root = dfs(root, 0)
# 递归子节点
paths_on_left = self.pathSum(root.left, targetSum)
paths_on_right = self.pathSum(root.right, targetSum)
# 总路径数为当前节点开始的路径数加上左右子树的路径数
return paths_from_root + paths_on_left + paths_on_right
# Example usage:
# root = TreeNode(10)
# root.left = TreeNode(5)
# root.right = TreeNode(-3)
# root.left.left = TreeNode(3)
# root.left.right = TreeNode(2)
# root.right.right = TreeNode(11)
# root.left.left.left = TreeNode(3)
# root.left.left.right = TreeNode(-2)
# root.left.right.right = TreeNode(1)
# sol = Solution()
# print(sol.pathSum(root, 8))
这段 Python 代码定义了一个二叉树节点的类TreeNode
和一个解决问题的类Solution
。Solution
类中的pathSum
方法是解决问题的主入口,它遍历每个节点,并对每个节点调用dfs
函数来寻找以该节点为起点的所有路径。dfs
函数是一个递归函数,用于计算以当前节点为起点,向下延伸的路径中,节点值之和等于targetSum
的路径数量。如果当前累计和等于targetSum
,则找到一条路径,并继续递归左右子树以寻找更多的路径。最终,pathSum
方法返回所有找到的路径数量。
在示例使用中,你可以构造一个二叉树并调用Solution
类的pathSum
方法来测试代码。
Java 版本
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
class Solution {
public int pathSum(TreeNode root, int targetSum) {
if (root == null) return 0;
return dfs(root, targetSum) + pathSum(root.left, targetSum) + pathSum(root.right, targetSum);
}
private int dfs(TreeNode node, int targetSum) {
if (node == null) return 0;
targetSum -= node.val;
return (targetSum == 0 ? 1 : 0) + dfs(node.left, targetSum) + dfs(node.right, targetSum);
}
}
// Example usage:
// public static void main(String[] args) {
// TreeNode root = new TreeNode(10);
// root.left = new TreeNode(5);
// root.right = new TreeNode(-3);
// root.left.left = new TreeNode(3);
// root.left.right = new TreeNode(2);
// root.right.right = new TreeNode(11);
// root.left.left.left = new TreeNode(3);
// root.left.left.right = new TreeNode(-2);
// root.left.right.right = new TreeNode(1);
// Solution solution = new Solution();
// System.out.println(solution.pathSum(root, 8));
// }
这段 Java 代码定义了一个二叉树节点的类TreeNode
和一个解决问题的类Solution
。Solution
类中的pathSum
方法是解决问题的主入口,它遍历每个节点,并对每个节点调用dfs
方法来寻找以该节点为起点的所有路径。dfs
方法是一个递归方法,用于计算以当前节点为起点,向下延伸的路径中,节点值之和等于targetSum
的路径数量。如果当前累计和等于targetSum
,则找到一条路径,并继续递归左右子树以寻找更多的路径。最终,pathSum
方法返回所有找到的路径数量。
在示例使用中,你可以构造一个二叉树并调用Solution
类的pathSum
方法来测试代码。
Rust 版本
// Definition for a binary tree node. #[derive(Debug, PartialEq, Eq)] pub struct TreeNode { pub val: i32, pub left: Option<Rc<RefCell<TreeNode>>>, pub right: Option<Rc<RefCell<TreeNode>>>, } impl TreeNode { #[inline] pub fn new(val: i32) -> Self { TreeNode { val, left: None, right: None, } } } use std::rc::Rc; use std::cell::RefCell; pub struct Solution {} impl Solution { pub fn path_sum(root: Option<Rc<RefCell<TreeNode>>>, target_sum: i32) -> i32 { Self::dfs(&root, target_sum) + Self::path_sum_helper(&root, target_sum) } fn path_sum_helper(node: &Option<Rc<RefCell<TreeNode>>>, target_sum: i32) -> i32 { if let Some(node) = node { let node = node.borrow(); Self::path_sum(node.left.clone(), target_sum) + Self::path_sum(node.right.clone(), target_sum) } else { 0 } } fn dfs(node: &Option<Rc<RefCell<TreeNode>>>, target_sum: i32) -> i32 { if let Some(node) = node { let node = node.borrow(); let val = node.val; let sum = target_sum - val; let mut total_paths = if sum == 0 { 1 } else { 0 }; total_paths += Self::dfs(&node.left, sum); total_paths += Self::dfs(&node.right, sum); total_paths } else { 0 } } } // Example usage: // fn main() { // let root = Some(Rc::new(RefCell::new(TreeNode::new(10)))); // let left = Some(Rc::new(RefCell::new(TreeNode::new(5)))); // let right = Some(Rc::new(RefCell::new(TreeNode::new(-3)))); // root.as_ref().unwrap().borrow_mut().left = left; // root.as_ref().unwrap().borrow_mut().right = right; // // ... continue building the tree ... // println!("{}", Solution::path_sum(root, 8)); // }
这段 Rust 代码定义了一个二叉树节点的结构体TreeNode
和一个解决问题的结构体Solution
。Solution
结构体中的path_sum
方法是解决问题的主入口,它遍历每个节点,并对每个节点调用dfs
方法来寻找以该节点为起点的所有路径。dfs
方法是一个递归方法,用于计算以当前节点为起点,向下延伸的路径中,节点值之和等于target_sum
的路径数量。如果当前累计和等于target_sum
,则找到一条路径,并继续递归左右子树以寻找更多的路径。最终,path_sum
方法返回所有找到的路径数量。
在示例使用中,你可以构造一个二叉树并调用Solution
结构体的path_sum
方法来测试代码。注意,由于 Rust 的所有权机制,我们通常使用Rc<RefCell<T>>
来共享和借用可变的树节点。
总结
这种解法的关键在于,它不仅仅考虑了从根节点到叶节点的路径,还包括了所有可能的从中间节点开始的路径。这是通过在每个节点处重新启动一个新的路径搜索来实现的,这样就能够覆盖树中所有可能的路径。这种方法的时间复杂度相对较高,因为它需要在每个节点处进行深度优先搜索,但它确保了所有可能的路径都被考虑到了。
二叉树的最近公共祖先
题目要求
给定一个二叉树,以及这个二叉树中的两个节点 p 和 q,要求编写一个函数来寻找并返回这两个节点的最近公共祖先(LCA)。根据定义,二叉树的每个节点都具有一个指向其父节点的引用,最近公共祖先是指在树中最低的节点,同时是 p 和 q 的祖先。需要注意的是,一个节点也可以是它自己的祖先,因此如果 p 是 q 的父节点或者 q 是 p 的父节点,那么 p 或 q 本身就是最近公共祖先。
解题思路
解决这个问题的关键是理解如何在树中寻找两个节点的最近公共祖先。这里有几种方法可以解决这个问题,但最直观的方法是使用递归。以下是解题的步骤:
-
递归的基本情况:如果当前节点为空或者等于 p 或 q 中的任何一个,那么返回当前节点。这是因为如果找到了 p 或 q,就不需要再继续往下搜索了。
-
递归搜索:对当前节点的左右子树进行递归调用,分别寻找 p 和 q。
-
后序遍历:在递归的过程中,我们实际上是在进行后序遍历。首先处理左右子树,然后处理当前节点。
-
判断逻辑:
- 如果在左子树中找到了 p 或 q(即左子树递归调用返回非空),并且在右子树中也找到了 p 或 q(即右子树递归调用返回非空),那么当前节点就是 p 和 q 的最近公共祖先。
- 如果只在左子树找到了 p 或 q,那么返回左子树的结果。
- 如果只在右子树找到了 p 或 q,那么返回右子树的结果。
- 如果左右子树都没有找到 p 和 q,那么返回空。
-
返回结果:递归将结果一层层传递回根节点,最终返回的结果就是 p 和 q 的最近公共祖先。
通过这种方式,我们可以保证在找到 p 和 q 之前,不会提前返回任何节点,从而确保找到的是最近的公共祖先。这个方法的时间复杂度是 O(n),因为我们需要遍历树中的每个节点。
Golang 版本
package main
import (
"fmt"
)
// TreeNode is the structure for tree nodes
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
// lowestCommonAncestor is the function to find the lowest common ancestor of two nodes p and q
func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {
if root == nil || root == p || root == q {
return root
}
left := lowestCommonAncestor(root.Left, p, q)
right := lowestCommonAncestor(root.Right, p, q)
if left != nil && right != nil {
return root
}
if left != nil {
return left
}
return right
}
func main() {
// Example usage:
// Construct a binary tree here to use the lowestCommonAncestor function.
// The TreeNode struct and lowestCommonAncestor function assume that the tree and nodes p and q are already defined and passed in as arguments.
// You would need to create the tree and nodes, then call lowestCommonAncestor with the root and two nodes.
}
这段代码没有包含创建二叉树和节点的部分,因为通常这些信息是由题目给出的,或者需要根据具体情况来构造。在实际使用中,你需要创建一个二叉树的实例,并且提供要查找最近公共祖先的两个节点作为参数来调用lowestCommonAncestor
函数。
C++版本
#include <iostream>
// Definition for a binary tree node.
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (!root || root == p || root == q) return root;
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
if (left && right) return root;
return left ? left : right;
}
};
int main() {
// Example usage:
// The TreeNode class and lowestCommonAncestor method assume that the tree and nodes p and q are already defined and passed in as arguments.
// You would need to create the tree and nodes, then call lowestCommonAncestor with the root and two nodes.
// TreeNode* root = new TreeNode(3);
// TreeNode* p = new TreeNode(5);
// TreeNode* q = new TreeNode(1);
// Solution solution;
// TreeNode* lca = solution.lowestCommonAncestor(root, p, q);
// std::cout << "LCA: " << lca->val << std::endl;
return 0;
}
这段代码提供了TreeNode
结构的定义和Solution
类的实现,其中包含了lowestCommonAncestor
方法。在main
函数中,你需要根据实际情况创建二叉树和节点,并调用lowestCommonAncestor
方法。请注意,上面的main
函数中的代码是注释掉的示例代码,展示了如何使用这个方法,你需要根据实际的二叉树结构来创建和初始化节点。
Python 版本
class TreeNode:
def __init__(self, x):
self.val = x
self.left = None
self.right = None
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
if root is None or root == p or root == q:
return root
left = self.lowestCommonAncestor(root.left, p, q)
right = self.lowestCommonAncestor(root.right, p, q)
if left and right:
return root
return left if left else right
# Example usage:
# if __name__ == "__main__":
# # Construct the binary tree here
# root = TreeNode(3)
# p = TreeNode(5)
# q = TreeNode(1)
# # Assume the tree is built and p, q are assigned
# solution = Solution()
# lca = solution.lowestCommonAncestor(root, p, q)
# print(f"LCA: {lca.val}")
这段代码定义了TreeNode
类用于表示二叉树的节点,以及Solution
类,其中包含了lowestCommonAncestor
方法。在实际使用中,你需要创建一个二叉树的实例,并且提供要查找最近公共祖先的两个节点作为参数来调用lowestCommonAncestor
方法。注释掉的部分是如何使用这个类的示例。
Java 版本
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
public class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q) {
return root;
}
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
if (left != null && right != null) {
return root;
}
return left != null ? left : right;
}
}
// Example usage:
// public static void main(String[] args) {
// TreeNode root = new TreeNode(3);
// TreeNode p = new TreeNode(5);
// TreeNode q = new TreeNode(1);
// Solution solution = new Solution();
// TreeNode lca = solution.lowestCommonAncestor(root, p, q);
// System.out.println("LCA: " + lca.val);
// }
这段代码定义了TreeNode
类用于表示二叉树的节点,以及Solution
类,其中包含了lowestCommonAncestor
方法。在实际使用中,你需要创建一个二叉树的实例,并且提供要查找最近公共祖先的两个节点作为参数来调用lowestCommonAncestor
方法。注释掉的部分是如何在main
方法中使用这个类的示例。
Rust 版本
// Definition for a binary tree node. #[derive(Debug, PartialEq, Eq)] pub struct TreeNode { pub val: i32, pub left: Option<Rc<RefCell<TreeNode>>>, pub right: Option<Rc<RefCell<TreeNode>>>, } impl TreeNode { #[inline] pub fn new(val: i32) -> Self { TreeNode { val, left: None, right: None, } } } use std::rc::Rc; use std::cell::RefCell; pub struct Solution {} impl Solution { pub fn lowest_common_ancestor( root: Option<Rc<RefCell<TreeNode>>>, p: Option<Rc<RefCell<TreeNode>>>, q: Option<Rc<RefCell<TreeNode>>>, ) -> Option<Rc<RefCell<TreeNode>>> { match (root, p, q) { (Some(node), Some(p), Some(q)) => { let left = Self::lowest_common_ancestor(node.borrow().left.clone(), Some(p.clone()), Some(q.clone())); let right = Self::lowest_common_ancestor(node.borrow().right.clone(), Some(p), Some(q)); if left.is_some() && right.is_some() { return Some(node); } left.or(right) }, _ => None, } } } // Example usage: // fn main() { // let root = Some(Rc::new(RefCell::new(TreeNode::new(3)))); // let p = Some(Rc::new(RefCell::new(TreeNode::new(5)))); // let q = Some(Rc::new(RefCell::new(TreeNode::new(1)))); // let lca = Solution::lowest_common_ancestor(root, p, q); // println!("LCA: {:?}", lca.map(|node| node.borrow().val)); // }
这段代码定义了TreeNode
结构体用于表示二叉树的节点,并使用Rc<RefCell<T>>
来处理树的可变共享所有权。Solution
结构体包含了lowest_common_ancestor
方法,用于查找二叉树中两个节点的最近公共祖先。在实际使用中,你需要创建一个二叉树的实例,并且提供要查找最近公共祖先的两个节点作为参数来调用lowest_common_ancestor
方法。注释掉的部分是如何在main
函数中使用这个方法的示例。
总结
上述解法采用了递归的方法来寻找二叉树中两个节点的最近公共祖先(LCA)。无论是在 C++、Java 还是 Rust 版本的代码中,核心逻辑都是相同的,只是语法和一些细节上有所不同。以下是解法的关键步骤:
-
递归的基本情况:如果当前节点为空,或者当前节点等于 p 或 q 中的任何一个,那么返回当前节点。这是因为如果找到了 p 或 q,就不需要再继续往下搜索了。
-
递归搜索:递归地在左右子树中搜索 p 和 q。调用递归函数
lowestCommonAncestor
分别对左子树和右子树进行搜索。 -
后序遍历:在后序遍历的过程中,当左右子树的递归调用都返回非空结果时,意味着在当前节点的两侧各找到了一个节点(p 和 q),因此当前节点就是它们的最近公共祖先。
-
返回结果:如果左子树或右子树的递归调用返回了一个非空节点,而另一侧返回空,这意味着两个节点都在返回的那一侧。因此,返回非空的那个节点。如果两侧都为空,返回空。
-
最终结果:递归的最终结果就是最近公共祖先的节点。
这种方法的时间复杂度是 O(n),其中 n 是树中的节点数,因为最坏的情况下需要遍历整棵树。空间复杂度取决于递归调用栈的深度,最坏情况下是 O(n)(在非平衡树的情况下)。
二叉树中的最大路径和
题目要求
你需要编写一个算法来找出给定二叉树中任意路径的最大路径和。这里的路径定义为一系列节点,其中每一对相邻节点之间都有一条边连接,且每个节点在路径中只能出现一次。路径不需要从根节点开始,也不需要在叶节点结束,但至少包含一个节点。
解题思路
要解决这个问题,我们可以采用递归的方式,对二叉树进行后序遍历(左-右-根)。在遍历的过程中,我们需要计算两个值:
- 经过当前节点的单边最大路径和(即从当前节点出发,向下延伸到任意节点的最大路径和)。这个值是为了提供给它的父节点计算使用的,因为在父节点看来,它只能从一个子节点接收路径。
- 经过当前节点的最大路径和,这个路径可以不经过父节点,即左子树的路径加上当前节点的值再加上右子树的路径。
对于每个节点,我们需要考虑以下几种情况:
- 当前节点单独构成一条路径。
- 当前节点加上左子树构成一条路径。
- 当前节点加上右子树构成一条路径。
- 当前节点加上左右子树构成一条路径,这种情况下,当前节点是这条路径的最高点。
对于第四种情况,由于路径不能分叉,所以这样的路径不会贡献给父节点的单边最大路径和,但是它可能是全局的最大路径和。
因此,我们需要一个全局变量来记录遍历过程中的最大路径和。在递归函数中,我们计算并返回当前节点作为单边路径的最大和,同时更新全局的最大路径和。
递归的基本步骤如下:
- 如果当前节点为空,返回 0(因为它不贡献任何路径和)。
- 递归计算左子节点的最大单边路径和,如果这个值小于 0,我们就不需要加上左子节点的值,因为它会减小路径和。
- 递归计算右子节点的最大单边路径和,同样地,如果这个值小于 0,我们就不需要加上右子节点的值。
- 计算通过当前节点的最大路径和,即左子节点的最大单边路径和加上当前节点的值再加上右子节点的最大单边路径和,与全局最大路径和比较,更新全局最大路径和。
- 返回当前节点的最大单边路径和,即当前节点的值加上左右子节点单边路径和的较大者。
递归结束后,全局变量中记录的就是最大路径和。
Golang 版本
package main
import (
"fmt"
"math"
)
// TreeNode is the structure for binary tree nodes.
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
// maxPathSum finds the maximum path sum for any path in the binary tree.
func maxPathSum(root *TreeNode) int {
maxSum := math.MinInt32
var maxGain func(node *TreeNode) int
maxGain = func(node *TreeNode) int {
if node == nil {
return 0
}
// Recursively call maxGain on node's left and right subtrees
leftGain := max(0, maxGain(node.Left))
rightGain := max(0, maxGain(node.Right))
// Price to start a new path where `node` is the highest node
priceNewpath := node.Val + leftGain + rightGain
// Update maxSum if it's better to start a new path
maxSum = max(maxSum, priceNewpath)
// For recursion return the max gain if continue the same path
return node.Val + max(leftGain, rightGain)
}
maxGain(root)
return maxSum
}
// max returns the larger of x or y.
func max(x, y int) int {
if x > y {
return x
}
return y
}
func main() {
// Example usage:
// Construct a binary tree:
// 1
// / \
// 2 3
root := &TreeNode{Val: 1}
root.Left = &TreeNode{Val: 2}
root.Right = &TreeNode{Val: 3}
// Find the maximum path sum
fmt.Println("Maximum Path Sum:", maxPathSum(root))
}
这段代码定义了一个二叉树节点的结构体TreeNode
,并实现了一个函数maxPathSum
来计算二叉树中任意路径的最大路径和。在main
函数中,我们构造了一个简单的二叉树作为示例,并调用maxPathSum
函数来计算和打印最大路径和。
C++版本
#include <iostream>
#include <algorithm>
#include <climits>
// Definition for a binary tree node.
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
class Solution {
public:
int maxPathSum(TreeNode* root) {
int maxSum = INT_MIN;
maxPathDown(root, maxSum);
return maxSum;
}
private:
int maxPathDown(TreeNode* node, int& maxSum) {
if (node == nullptr) return 0;
int left = std::max(0, maxPathDown(node->left, maxSum));
int right = std::max(0, maxPathDown(node->right, maxSum));
maxSum = std::max(maxSum, left + right + node->val);
return std::max(left, right) + node->val;
}
};
int main() {
// Example usage:
TreeNode *root = new TreeNode(1);
root->left = new TreeNode(2);
root->right = new TreeNode(3);
Solution solution;
std::cout << "Maximum Path Sum: " << solution.maxPathSum(root) << std::endl;
// Clean up memory
delete root->left;
delete root->right;
delete root;
return 0;
}
这段 C++代码定义了一个二叉树节点的结构体TreeNode
和一个解决方案类Solution
。Solution
类中有一个公共成员函数maxPathSum
用来计算二叉树中任意路径的最大路径和,以及一个私有成员函数maxPathDown
用来递归地计算从任意节点向下的最大路径和。在main
函数中,我们构造了一个简单的二叉树作为示例,并调用maxPathSum
函数来计算和打印最大路径和。最后,代码中还包含了适当的内存清理操作。
Python 版本
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class Solution:
def maxPathSum(self, root: TreeNode) -> int:
self.max_sum = float('-inf')
def max_gain(node):
if not node:
return 0
# Recursively call max_gain on node's left and right subtrees
left_gain = max(max_gain(node.left), 0)
right_gain = max(max_gain(node.right), 0)
# Price to start a new path where `node` is the highest node
price_newpath = node.val + left_gain + right_gain
# Update max_sum if it's better to start a new path
self.max_sum = max(self.max_sum, price_newpath)
# For recursion, return the max gain if continue the same path
return node.val + max(left_gain, right_gain)
max_gain(root)
return self.max_sum
# Example usage:
if __name__ == "__main__":
# Construct a binary tree:
# 1
# / \
# 2 3
root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
# Find the maximum path sum
solution = Solution()
print("Maximum Path Sum:", solution.maxPathSum(root))
这段 Python 代码定义了一个二叉树节点的类TreeNode
和一个解决方案类Solution
。Solution
类中有一个方法maxPathSum
用来计算二叉树中任意路径的最大路径和。在maxPathSum
方法中,定义了一个内部函数max_gain
用来递归地计算从任意节点向下的最大路径和,并更新一个类属性max_sum
作为全局最大路径和。在__main__
部分,我们构造了一个简单的二叉树作为示例,并调用maxPathSum
方法来计算和打印最大路径和。
Java 版本
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
class Solution {
private int maxSum = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
maxGain(root);
return maxSum;
}
private int maxGain(TreeNode node) {
if (node == null) return 0;
int leftGain = Math.max(maxGain(node.left), 0);
int rightGain = Math.max(maxGain(node.right), 0);
int priceNewpath = node.val + leftGain + rightGain;
maxSum = Math.max(maxSum, priceNewpath);
return node.val + Math.max(leftGain, rightGain);
}
}
public class Main {
public static void main(String[] args) {
// Example usage:
TreeNode root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
Solution solution = new Solution();
System.out.println("Maximum Path Sum: " + solution.maxPathSum(root));
}
}
这段 Java 代码定义了一个二叉树节点的类TreeNode
和一个解决方案类Solution
。Solution
类中有一个方法maxPathSum
用来计算二叉树中任意路径的最大路径和。在maxPathSum
方法中,定义了一个私有辅助方法maxGain
用来递归地计算从任意节点向下的最大路径和,并更新一个私有成员变量maxSum
作为全局最大路径和。在Main
类的main
方法中,我们构造了一个简单的二叉树作为示例,并调用maxPathSum
方法来计算和打印最大路径和。
Rust 版本
use std::cell::RefCell; use std::rc::Rc; use std::cmp; // Definition for a binary tree node. #[derive(Debug, PartialEq, Eq)] pub struct TreeNode { pub val: i32, pub left: Option<Rc<RefCell<TreeNode>>>, pub right: Option<Rc<RefCell<TreeNode>>>, } impl TreeNode { #[inline] pub fn new(val: i32) -> Self { TreeNode { val, left: None, right: None, } } } struct Solution; impl Solution { pub fn max_path_sum(root: Option<Rc<RefCell<TreeNode>>>) -> i32 { let mut max_sum = i32::MIN; Solution::max_gain(root, &mut max_sum); max_sum } fn max_gain(node: Option<Rc<RefCell<TreeNode>>>, max_sum: &mut i32) -> i32 { match node { Some(node) => { let n = node.borrow(); let left_gain = cmp::max(Self::max_gain(n.left.clone(), max_sum), 0); let right_gain = cmp::max(Self::max_gain(n.right.clone(), max_sum), 0); let price_newpath = n.val + left_gain + right_gain; *max_sum = cmp::max(*max_sum, price_newpath); cmp::max(n.val + left_gain, n.val + right_gain) } None => 0, } } } fn main() { // Example usage: let root = Some(Rc::new(RefCell::new(TreeNode::new(1)))); let left = Some(Rc::new(RefCell::new(TreeNode::new(2)))); let right = Some(Rc::new(RefCell::new(TreeNode::new(3)))); if let Some(r) = root.as_ref() { r.borrow_mut().left = left; r.borrow_mut().right = right; } println!("Maximum Path Sum: {}", Solution::max_path_sum(root)); }
这段 Rust 代码定义了一个二叉树节点的结构体TreeNode
和一个解决方案结构体Solution
。Solution
结构体中有一个公共关联函数max_path_sum
用来计算二叉树中任意路径的最大路径和。在max_path_sum
函数中,定义了一个私有关联函数max_gain
用来递归地计算从任意节点向下的最大路径和,并更新一个可变引用参数max_sum
作为全局最大路径和。在main
函数中,我们构造了一个简单的二叉树作为示例,并调用max_path_sum
函数来计算和打印最大路径和。
总结
上述解法采用了递归的方法来解决二叉树中的最大路径和问题。核心思想是对于每个节点,计算两个值:
-
节点的最大单边路径和:这是从当前节点出发,到任意子节点的最大路径和,不包括父节点。这个值是当前节点的值加上其左右子节点的最大单边路径和的较大者。如果子节点的最大单边路径和为负,则不包括该子节点,因为路径和会减小。
-
可能的最大路径和:这是以当前节点为根的子树能提供的最大路径和,可能包括左右子节点的最大单边路径和加上当前节点的值。这个值可能不会返回给父节点,但会用来更新全局的最大路径和。
算法的步骤如下:
- 从根节点开始递归。
- 对于每个节点,递归计算其左右子节点的最大单边路径和。
- 计算以当前节点为顶点的最大路径和(当前节点值 + 左侧最大单边路径和 + 右侧最大单边路径和)。
- 更新全局最大路径和(如果当前节点的最大路径和更大)。
- 返回当前节点的最大单边路径和给父节点。
这个算法的时间复杂度是 O(n),其中 n 是树中节点的数量,因为每个节点只被访问一次。空间复杂度是 O(h),其中 h 是树的高度,这是因为递归栈的深度取决于树的高度。
在不同的编程语言中,这个算法的实现细节可能略有不同,但核心逻辑是一致的。例如,在 Rust 中,由于其所有权模型,我们需要使用Rc<RefCell<T>>
来共享和修改树节点。而在 Java 中,我们可以直接修改对象的属性。
图论
图论是计算机科学中的一个重要领域,它在算法设计中占有举足轻重的地位。解决图论问题的通用思路通常包括以下几个步骤:
-
理解图的表示:图可以通过邻接矩阵或邻接表来表示。邻接矩阵适合表示稠密图,而邻接表适合表示稀疏图。
-
确定图的类型:图可以是无向或有向,加权或非加权。这将决定你选择哪种算法。
-
选择合适的遍历算法:深度优先搜索(DFS)和广度优先搜索(BFS)是两种基本的图遍历算法。DFS 适合用于寻找组件或检查环,而 BFS 适合于寻找最短路径。
-
考虑可能的优化:对于特定问题,可能需要使用特定的算法,如 Dijkstra 算法用于在加权图中找到最短路径,Bellman-Ford 算法可以处理负权重边,Floyd-Warshall 算法用于计算所有对最短路径,以及 Prim 或 Kruskal 算法用于找到最小生成树。
-
实现并优化:在实现算法时,考虑数据结构的选择以优化时间和空间复杂度。
下面是使用 Go 语言实现的一些图论算法的代码示例。
邻接表表示法:
type Graph struct {
vertices []*Vertex
}
type Vertex struct {
key int
adjacent []*Vertex
}
func (g *Graph) AddVertex(k int) {
if contains(g.vertices, k) {
err := fmt.Errorf("Vertex %v not added because it is an existing key", k)
fmt.Println(err.Error())
} else {
g.vertices = append(g.vertices, &Vertex{key: k})
}
}
func (g *Graph) AddEdge(from, to int) {
// get vertex
fromVertex := g.getVertex(from)
toVertex := g.getVertex(to)
// check error
if fromVertex == nil || toVertex == nil {
err := fmt.Errorf("Invalid edge (%v->%v)", from, to)
fmt.Println(err.Error())
} else if contains(fromVertex.adjacent, to) {
err := fmt.Errorf("Existing edge (%v->%v)", from, to)
fmt.Println(err.Error())
} else {
// add edge
fromVertex.adjacent = append(fromVertex.adjacent, toVertex)
}
}
func (g *Graph) getVertex(k int) *Vertex {
for i, v := range g.vertices {
if v.key == k {
return g.vertices[i]
}
}
return nil
}
func contains(s []*Vertex, k int) bool {
for _, v := range s {
if k == v.key {
return true
}
}
return false
}
深度优先搜索(DFS):
func (g *Graph) DFS(startingKey int) {
visited := make(map[int]bool)
g.dfsHelper(startingKey, visited)
}
func (g *Graph) dfsHelper(startingKey int, visited map[int]bool) {
visited[startingKey] = true
vertex := g.getVertex(startingKey)
fmt.Println(vertex.key)
for _, v := range vertex.adjacent {
if !visited[v.key] {
g.dfsHelper(v.key, visited)
}
}
}
广度优先搜索(BFS):
func (g *Graph) BFS(startingKey int) {
visited := make(map[int]bool)
queue := []*Vertex{g.getVertex(startingKey)}
for len(queue) > 0 {
vertex := queue[0]
queue = queue[1:]
if !visited[vertex.key] {
visited[vertex.key] = true
fmt.Println(vertex.key)
for _, v := range vertex.adjacent {
if !visited[v.key] {
queue = append(queue, v)
}
}
}
}
}
在实际应用中,你需要根据具体问题调整这些代码。例如,如果你需要处理加权图,你可能需要在你的Vertex
结构中添加一个表示权重的字段,并在添加边时考虑这个权重。同样,如果你在处理有向图,添加边的方式也会有所不同。
岛屿数量
题目要求
你需要编写一个算法来统计一个二维网格中的岛屿数量。这个网格由字符 '1'(代表陆地)和 '0'(代表水)组成。一个岛屿由上下左右相连的 '1' 形成,并且假设网格的四周都被 '0' 包围,即网格的边界之外全是水。
解题思路
解决这个问题的一个有效方法是使用深度优先搜索(DFS)。以下是解题的步骤:
- 遍历网格中的每一个单元格。
- 当你遇到一个值为 '1' 的单元格时,将其视为岛屿的一部分。
- 从这个单元格开始,使用深度优先搜索遍历所有相邻的、值为 '1' 的单元格,并将它们标记为已访问,以避免重复计算。这可以通过将单元格的值从 '1' 改为 '0' 或者其他非 '1' 的值来实现。
- 继续步骤 1 和步骤 2,直到网格中的所有单元格都被访问过。
- 每次启动一个新的深度优先搜索时,岛屿的数量加一。
这种方法的关键在于,每次通过深度优先搜索将一个岛屿的所有部分都标记为已访问后,就可以确保不会重复计算岛屿的数量。当遍历完整个网格后,就可以得到岛屿的总数。
Golang 版本
func numIslands(grid [][]byte) int {
if len(grid) == 0 || len(grid[0]) == 0 {
return 0
}
islandCount := 0
for i := 0; i < len(grid); i++ {
for j := 0; j < len(grid[i]); j++ {
if grid[i][j] == '1' {
dfs(grid, i, j)
islandCount++
}
}
}
return islandCount
}
func dfs(grid [][]byte, i, j int) {
if i < 0 || j < 0 || i >= len(grid) || j >= len(grid[i]) || grid[i][j] == '0' {
return
}
grid[i][j] = '0'
dfs(grid, i+1, j)
dfs(grid, i-1, j)
dfs(grid, i, j+1)
dfs(grid, i, j-1)
}
这段代码定义了一个名为numIslands
的函数,它接受一个二维字节切片grid
作为参数,并返回一个整数表示岛屿的数量。它还使用了一个辅助函数dfs
来执行深度优先搜索并标记访问过的岛屿部分。在numIslands
函数中,通过双层循环遍历网格中的每个单元格,当遇到值为'1'的单元格时,调用dfs
函数,并将岛屿计数器加一。dfs
函数负责将访问过的陆地部分标记为'0',并递归地对上下左右的单元格进行相同的操作。
C++版本
#include <vector>
class Solution {
public:
int numIslands(std::vector<std::vector<char>>& grid) {
int islandCount = 0;
for (int i = 0; i < grid.size(); ++i) {
for (int j = 0; j < grid[i].size(); ++j) {
if (grid[i][j] == '1') {
dfs(grid, i, j);
++islandCount;
}
}
}
return islandCount;
}
private:
void dfs(std::vector<std::vector<char>>& grid, int i, int j) {
if (i < 0 || i >= grid.size() || j < 0 || j >= grid[i].size() || grid[i][j] == '0') {
return;
}
grid[i][j] = '0';
dfs(grid, i + 1, j);
dfs(grid, i - 1, j);
dfs(grid, i, j + 1);
dfs(grid, i, j - 1);
}
};
这段 C++代码定义了一个名为Solution
的类,其中包含了两个方法:numIslands
和dfs
。numIslands
方法用于计算二维网格中岛屿的数量,它接受一个二维字符向量grid
作为参数。dfs
方法是一个私有辅助方法,用于执行深度优先搜索并将访问过的岛屿部分标记为'0'。在numIslands
方法中,通过嵌套循环遍历网格中的每个单元格,当遇到值为'1'的单元格时,调用dfs
方法,并将岛屿计数器加一。dfs
方法递归地对相邻的单元格进行搜索,直到所有相连的陆地都被访问和标记。
Python 版本
class Solution:
def numIslands(self, grid):
if not grid:
return 0
count = 0
for i in range(len(grid)):
for j in range(len(grid[0])):
if grid[i][j] == '1':
self.dfs(grid, i, j)
count += 1
return count
def dfs(self, grid, i, j):
if i < 0 or j < 0 or i >= len(grid) or j >= len(grid[0]) or grid[i][j] != '1':
return
grid[i][j] = '0'
self.dfs(grid, i + 1, j)
self.dfs(grid, i - 1, j)
self.dfs(grid, i, j + 1)
self.dfs(grid, i, j - 1)
# Example usage:
# grid = [
# ["1","1","1","0","0"],
# ["1","1","0","0","0"],
# ["0","0","1","0","0"],
# ["0","0","0","1","1"]
# ]
# sol = Solution()
# print(sol.numIslands(grid)) # Output: 3
这段 Python 代码定义了一个名为Solution
的类,其中包含了两个方法:numIslands
和dfs
。numIslands
方法用于计算二维网格中岛屿的数量,它接受一个二维字符列表grid
作为参数。dfs
方法是一个辅助方法,用于执行深度优先搜索并将访问过的岛屿部分标记为'0'。在numIslands
方法中,通过嵌套循环遍历网格中的每个单元格,当遇到值为'1'的单元格时,调用dfs
方法,并将岛屿计数器加一。dfs
方法递归地对相邻的单元格进行搜索,直到所有相连的陆地都被访问和标记。
Java 版本
public class Solution {
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) {
return 0;
}
int numIslands = 0;
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[i].length; j++) {
if (grid[i][j] == '1') {
dfs(grid, i, j);
numIslands++;
}
}
}
return numIslands;
}
private void dfs(char[][] grid, int i, int j) {
if (i < 0 || i >= grid.length || j < 0 || j >= grid[i].length || grid[i][j] == '0') {
return;
}
grid[i][j] = '0';
dfs(grid, i + 1, j);
dfs(grid, i - 1, j);
dfs(grid, i, j + 1);
dfs(grid, i, j - 1);
}
}
这段 Java 代码定义了一个名为Solution
的类,其中包含了两个方法:numIslands
和dfs
。numIslands
方法用于计算二维字符数组中岛屿的数量。dfs
方法是一个私有辅助方法,用于执行深度优先搜索并将访问过的岛屿部分标记为'0'。在numIslands
方法中,通过嵌套循环遍历数组中的每个元素,当遇到值为'1'的元素时,调用dfs
方法,并将岛屿计数器加一。dfs
方法递归地对相邻的元素进行搜索,直到所有相连的陆地都被访问和标记。
Rust 版本
#![allow(unused)] fn main() { pub struct Solution; impl Solution { pub fn num_islands(grid: Vec<Vec<char>>) -> i32 { let mut grid = grid; let mut count = 0; for i in 0..grid.len() { for j in 0..grid[i].len() { if grid[i][j] == '1' { Self::dfs(&mut grid, i, j); count += 1; } } } count } fn dfs(grid: &mut Vec<Vec<char>>, i: usize, j: usize) { if i >= grid.len() || j >= grid[i].len() || grid[i][j] == '0' { return; } grid[i][j] = '0'; if i > 0 { Self::dfs(grid, i - 1, j); } if j > 0 { Self::dfs(grid, i, j - 1); } if i + 1 < grid.len() { Self::dfs(grid, i + 1, j); } if j + 1 < grid[i].len() { Self::dfs(grid, i, j + 1); } } } // Example usage: // let grid = vec![ // vec!['1', '1', '1', '0', '0'], // vec!['1', '1', '0', '0', '0'], // vec!['0', '0', '1', '0', '0'], // vec!['0', '0', '0', '1', '1'], // ]; // println!("{}", Solution::num_islands(grid)); // Output: 3 }
这段 Rust 代码定义了一个名为Solution
的结构体和两个关联函数:num_islands
和dfs
。num_islands
函数用于计算二维字符向量中岛屿的数量。dfs
函数是一个私有辅助函数,用于执行深度优先搜索并将访问过的岛屿部分标记为'0'。在num_islands
函数中,通过嵌套循环遍历向量中的每个元素,当遇到值为'1'的元素时,调用dfs
函数,并将岛屿计数器加一。dfs
函数递归地对相邻的元素进行搜索,直到所有相连的陆地都被访问和标记。
总结
上述代码展示了如何在不同编程语言中实现岛屿数量的计算。核心算法是基于深度优先搜索(DFS)的。以下是算法的步骤总结:
- 遍历给定的二维网格。
- 当遇到一个值为'1'的单元格时,将其视为岛屿的一部分,并开始从该单元格进行深度优先搜索。
- 在 DFS 过程中,将访问过的'1'标记为'0',以避免重复计算。
- 对当前单元格的上下左右四个方向进行递归搜索,直到所有相连的'1'都被访问并标记为'0'。
- 每完成一次 DFS,岛屿计数器加一。
- 继续遍历直到网格中的所有单元格都被检查过。
- 返回岛屿计数器的值,即为岛屿的总数。
这种方法的时间复杂度通常是 O(MN),其中 M 是网格的行数,N 是网格的列数,因为它需要访问网格中的每个单元格。空间复杂度主要取决于递归调用栈的深度,最坏情况下可能达到 O(MN)。
在不同的编程语言中,尽管语法不同,但算法的核心逻辑保持一致。例如,在 Rust 中使用usize
来索引向量,而在 Java 和 C++中则使用int
类型。此外,Rust 的所有权和借用机制要求在递归调用时显式地传递可变引用。尽管这些细节上的差异存在,但整体算法框架和解决问题的思路是相同的。
腐烂的橘子
题目要求
在一个 m x n 的网格 grid
中,每个单元格可能包含以下三种值之一:
- 值 0 代表空单元格;
- 值 1 代表新鲜橘子;
- 值 2 代表腐烂的橘子。
每过一分钟,任何与腐烂橘子相邻(上下左右四个方向)的新鲜橘子都会变腐烂。
任务是计算并返回使网格中所有新鲜橘子变腐烂所需的最小分钟数。如果存在无法被腐烂的新鲜橘子,则返回 -1
。
解题思路
这个问题可以通过广度优先搜索(BFS)来解决。以下是解题步骤:
-
初始化队列和计数器:首先,遍历整个网格,将所有腐烂橘子的位置加入到队列中,并统计新鲜橘子的数量。
-
广度优先搜索:然后,进行 BFS,每次从队列中取出一个腐烂橘子的位置,查看其四周是否有新鲜橘子。如果有,则将新鲜橘子变为腐烂状态(即值从 1 变为 2),并将其位置加入队列中,同时新鲜橘子的数量减一。
-
时间计数:每完成一轮 BFS,表示过去了一分钟。在这一分钟内,所有与腐烂橘子相邻的新鲜橘子都会变腐烂。
-
检查新鲜橘子:当队列为空或新鲜橘子的数量变为 0 时,BFS 结束。如果还有新鲜橘子剩余,说明这些橘子无法被腐烂,返回
-1
。否则,返回所用的分钟数。 -
边界条件处理:如果一开始就没有新鲜橘子,那么直接返回 0 分钟。
这个问题的关键在于如何有效地模拟橘子腐烂的过程,并且能够在所有橘子都腐烂或确定有橘子无法腐烂时停止搜索。通过 BFS,我们可以确保每一轮都是在模拟一分钟内腐烂的传播,这样可以准确计算出所需的最小分钟数。
Golang 版本
func orangesRotting(grid [][]int) int {
rows, cols := len(grid), len(grid[0])
freshCount := 0
queue := make([][2]int, 0)
// 初始化队列,统计新鲜橘子数量
for r := 0; r < rows; r++ {
for c := 0; c < cols; c++ {
if grid[r][c] == 2 {
queue = append(queue, [2]int{r, c})
} else if grid[r][c] == 1 {
freshCount++
}
}
}
// 如果没有新鲜橘子,直接返回0
if freshCount == 0 {
return 0
}
// 四个方向的移动向量
directions := [][2]int{{-1, 0}, {1, 0}, {0, -1}, {0, 1}}
minutesPassed := 0
// 开始BFS
for len(queue) > 0 {
currentQueueSize := len(queue)
for i := 0; i < currentQueueSize; i++ {
orange := queue[0]
queue = queue[1:]
for _, d := range directions {
newRow, newCol := orange[0]+d[0], orange[1]+d[1]
// 检查新位置是否有效
if newRow >= 0 && newRow < rows && newCol >= 0 && newCol < cols {
if grid[newRow][newCol] == 1 {
grid[newRow][newCol] = 2
freshCount--
queue = append(queue, [2]int{newRow, newCol})
}
}
}
}
// 完成一轮BFS,时间增加
if len(queue) > 0 {
minutesPassed++
}
}
// 检查是否还有新鲜橘子
if freshCount == 0 {
return minutesPassed
}
return -1
}
这段代码定义了一个函数 orangesRotting
,它接受一个二维整数数组 grid
作为输入,代表网格状态,并返回一个整数,表示使所有新鲜橘子变腐烂所需的最小分钟数。如果存在无法被腐烂的新鲜橘子,则返回 -1
。代码使用广度优先搜索算法来模拟橘子腐烂的过程。
C++版本
#include <vector>
#include <queue>
using namespace std;
class Solution {
public:
int orangesRotting(vector<vector<int>>& grid) {
int rows = grid.size();
int cols = grid[0].size();
int freshCount = 0;
queue<pair<int, int>> q;
// 初始化队列,统计新鲜橘子数量
for (int r = 0; r < rows; ++r) {
for (int c = 0; c < cols; ++c) {
if (grid[r][c] == 2) {
q.push({r, c});
} else if (grid[r][c] == 1) {
freshCount++;
}
}
}
// 如果没有新鲜橘子,直接返回0
if (freshCount == 0) return 0;
// 四个方向的移动向量
vector<pair<int, int>> directions{{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
int minutesPassed = 0;
// 开始BFS
while (!q.empty()) {
int currentQueueSize = q.size();
for (int i = 0; i < currentQueueSize; ++i) {
auto orange = q.front();
q.pop();
for (auto d : directions) {
int newRow = orange.first + d.first;
int newCol = orange.second + d.second;
// 检查新位置是否有效
if (newRow >= 0 && newRow < rows && newCol >= 0 && newCol < cols) {
if (grid[newRow][newCol] == 1) {
grid[newRow][newCol] = 2;
freshCount--;
q.push({newRow, newCol});
}
}
}
}
// 完成一轮BFS,时间增加
if (!q.empty()) {
minutesPassed++;
}
}
// 检查是否还有新鲜橘子
if (freshCount == 0) {
return minutesPassed;
}
return -1;
}
};
这段 C++代码定义了一个类 Solution
,其中包含一个方法 orangesRotting
,它接受一个二维整数数组 grid
作为输入,并返回一个整数,表示使所有新鲜橘子变腐烂所需的最小分钟数。如果存在无法被腐烂的新鲜橘子,则返回 -1
。代码使用广度优先搜索算法来模拟橘子腐烂的过程。
Python 版本
from collections import deque
class Solution:
def orangesRotting(self, grid: List[List[int]]) -> int:
rows, cols = len(grid), len(grid[0])
fresh_count = 0
queue = deque()
# 初始化队列,统计新鲜橘子数量
for r in range(rows):
for c in range(cols):
if grid[r][c] == 2:
queue.append((r, c))
elif grid[r][c] == 1:
fresh_count += 1
# 如果没有新鲜橘子,直接返回0
if fresh_count == 0:
return 0
minutes_passed = 0
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
# 开始BFS
while queue:
for _ in range(len(queue)):
r, c = queue.popleft()
for dr, dc in directions:
nr, nc = r + dr, c + dc
# 检查新位置是否有效
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 1:
grid[nr][nc] = 2
fresh_count -= 1
queue.append((nr, nc))
if queue:
minutes_passed += 1
# 检查是否还有新鲜橘子
return minutes_passed if fresh_count == 0 else -1
这段 Python 代码定义了一个 Solution
类,其中包含一个方法 orangesRotting
,它接受一个二维整数列表 grid
作为输入,并返回一个整数,表示使所有新鲜橘子变腐烂所需的最小分钟数。如果存在无法被腐烂的新鲜橘子,则返回 -1
。代码使用广度优先搜索算法来模拟橘子腐烂的过程。
Java 版本
import java.util.*;
public class Solution {
public int orangesRotting(int[][] grid) {
if (grid == null || grid.length == 0) return 0;
int rows = grid.length, cols = grid[0].length;
Queue<int[]> queue = new LinkedList<>();
int freshCount = 0;
// 初始化队列,统计新鲜橘子数量
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (grid[r][c] == 2) {
queue.offer(new int[]{r, c});
} else if (grid[r][c] == 1) {
freshCount++;
}
}
}
// 如果没有新鲜橘子,直接返回0
if (freshCount == 0) return 0;
int minutesPassed = 0;
int[][] directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
// 开始BFS
while (!queue.isEmpty()) {
int currentQueueSize = queue.size();
for (int i = 0; i < currentQueueSize; i++) {
int[] orange = queue.poll();
for (int[] d : directions) {
int newRow = orange[0] + d[0];
int newCol = orange[1] + d[1];
// 检查新位置是否有效
if (newRow >= 0 && newRow < rows && newCol >= 0 && newCol < cols) {
if (grid[newRow][newCol] == 1) {
grid[newRow][newCol] = 2;
freshCount--;
queue.offer(new int[]{newRow, newCol});
}
}
}
}
if (!queue.isEmpty()) {
minutesPassed++;
}
}
// 检查是否还有新鲜橘子
return freshCount == 0 ? minutesPassed : -1;
}
}
这段 Java 代码定义了一个 Solution
类,其中包含一个方法 orangesRotting
,它接受一个二维整数数组 grid
作为输入,并返回一个整数,表示使所有新鲜橘子变腐烂所需的最小分钟数。如果存在无法被腐烂的新鲜橘子,则返回 -1
。代码使用广度优先搜索算法来模拟橘子腐烂的过程。
Rust 版本
#![allow(unused)] fn main() { use std::collections::VecDeque; pub fn oranges_rotting(grid: Vec<Vec<i32>>) -> i32 { let rows = grid.len(); let cols = grid[0].len(); let mut queue: VecDeque<(usize, usize)> = VecDeque::new(); let mut fresh_count = 0; // 初始化队列,统计新鲜橘子数量 for r in 0..rows { for c in 0..cols { match grid[r][c] { 2 => queue.push_back((r, c)), 1 => fresh_count += 1, _ => (), } } } // 如果没有新鲜橘子,直接返回0 if fresh_count == 0 { return 0; } let mut minutes_passed = 0; let directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]; // 开始BFS while let Some((r, c)) = queue.pop_front() { let mut added = false; for d in directions.iter() { let (dr, dc) = *d; let (new_r, new_c) = (r as i32 + dr, c as i32 + dc); if new_r >= 0 && new_r < rows as i32 && new_c >= 0 && new_c < cols as i32 { let (new_r, new_c) = (new_r as usize, new_c as usize); if grid[new_r][new_c] == 1 { grid[new_r][new_c] = 2; fresh_count -= 1; queue.push_back((new_r, new_c)); added = true; } } } if added { minutes_passed += 1; } } // 检查是否还有新鲜橘子 if fresh_count == 0 { minutes_passed } else { -1 } } }
这段 Rust 代码定义了一个函数 oranges_rotting
,它接受一个二维向量 grid
作为输入,并返回一个整数,表示使所有新鲜橘子变腐烂所需的最小分钟数。如果存在无法被腐烂的新鲜橘子,则返回 -1
。代码使用广度优先搜索算法来模拟橘子腐烂的过程。注意,Rust 中的索引必须是 usize
类型,因此在处理索引时需要进行类型转换。
总结
上面的解法采用了广度优先搜索(BFS)算法来解决“腐烂的橘子”问题。这个问题可以被看作是在一个二维网格上的感染扩散模型,其中橘子可以处于新鲜状态(1)或腐烂状态(2),空单元格(0)不参与感染过程。
解法的关键步骤如下:
-
初始化队列和统计新鲜橘子: 遍历整个网格,将所有腐烂的橘子的位置加入队列,并统计新鲜橘子的数量。
-
特殊情况处理: 如果没有新鲜橘子,直接返回 0,因为不需要时间来腐烂。
-
广度优先搜索(BFS): 使用队列来进行 BFS,每次从队列中取出一个腐烂的橘子的位置,然后查看其上下左右的邻居。如果邻居是新鲜橘子(1),则将其腐烂(变为 2),并将其位置加入队列,同时减少新鲜橘子的数量。
-
时间计数: 每完成一轮队列中的橘子处理,时间增加 1。这模拟了每分钟腐烂橘子影响其邻居的过程。
-
检查是否还有新鲜橘子: 完成 BFS 后,如果还有新鲜橘子,说明它们无法被腐烂的橘子感染到,返回-1。否则,返回所需的分钟数。
这个解法的时间复杂度通常是 O(mn),其中 m 是网格的行数,n 是网格的列数,因为每个单元格最多只会被访问一次。空间复杂度也是 O(mn),主要是因为在最坏的情况下,可能需要存储整个网格中所有的橘子位置。
课程表
题目要求
这个问题是关于课程规划的,可以抽象为一个有向图的问题,其中每个课程可以看作是图中的一个节点,而先修课程的要求可以看作是节点之间的有向边。具体来说:
- 有
numCourses
门课程,它们被编号为0
到numCourses - 1
。 - 先修课程的要求通过一个数组
prerequisites
给出,其中prerequisites[i] = [ai, bi]
表示要学习课程ai
必须先学习课程bi
。 - 任务是判断是否有可能完成所有课程的学习。
解题思路
要解决这个问题,可以采用以下步骤:
-
构建图表示:首先,我们需要将课程和先修关系转换成图的表示方法。这可以通过创建一个邻接表来实现,邻接表中的每个索引对应一个课程,每个索引处的列表包含了所有先修课程。
-
检测环:在有向图中,如果存在环,则意味着存在循环依赖,即某些课程的先修课程直接或间接地依赖于它们自己,这种情况下是不可能完成所有课程的学习的。检测环可以通过深度优先搜索(DFS)来实现。
-
深度优先搜索(DFS):对于图中的每一个节点(课程),执行 DFS,过程中维护一个访问状态的数组,该数组记录了每个节点的三种状态:未访问、访问中、已访问。访问中的状态用于检测环。如果在 DFS 过程中遇到一个访问中的节点,说明存在环。
-
拓扑排序:如果图中无环,那么可以进行拓扑排序。拓扑排序是对有向无环图(DAG)的顶点的一种线性排序,使得对于任何一条有向边 (u, v),顶点 u 都在顶点 v 之前。如果能够生成一种拓扑排序,则意味着可以完成所有课程的学习。
-
返回结果:如果在 DFS 过程中没有检测到环,那么返回
true
,表示可以完成所有课程的学习。如果检测到环,则返回false
。
通过上述步骤,我们可以判断是否可能完成所有课程的学习。这个问题实质上是在检查有向图是否有环,也就是检查是否可以进行拓扑排序。
Golang 版本
package main
import "fmt"
func canFinish(numCourses int, prerequisites [][]int) bool {
// 构建图的邻接表
graph := make([][]int, numCourses)
for _, pre := range prerequisites {
graph[pre[1]] = append(graph[pre[1]], pre[0])
}
// 记录访问状态的数组,0 未访问,1 访问中,2 已访问
visited := make([]int, numCourses)
// 定义 DFS 函数,用于检测图中是否有环
var dfs func(i int) bool
dfs = func(i int) bool {
if visited[i] == 1 { // 访问中的节点再次被访问,表示有环
return false
}
if visited[i] == 2 { // 已访问的节点无需再次访问
return true
}
visited[i] = 1 // 将节点标记为访问中
for _, v := range graph[i] {
if !dfs(v) { // 如果从节点 v 开始的 DFS 发现环,则返回 false
return false
}
}
visited[i] = 2 // 将节点标记为已访问
return true
}
// 对每个节点执行 DFS
for i := 0; i < numCourses; i++ {
if !dfs(i) {
return false
}
}
return true
}
func main() {
numCourses := 4
prerequisites := [][]int{{1, 0}, {2, 1}, {3, 2}}
if canFinish(numCourses, prerequisites) {
fmt.Println("可以完成所有课程的学习")
} else {
fmt.Println("无法完成所有课程的学习")
}
}
这段代码定义了一个 canFinish
函数,它接受课程总数和先修课程要求作为参数,返回一个布尔值表示是否可以完成所有课程的学习。主函数 main
中给出了一个示例调用。
C++版本
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<vector<int>> graph(numCourses);
vector<int> visit(numCourses, 0);
// 构建图的邻接表
for (const auto& pre : prerequisites) {
graph[pre[1]].push_back(pre[0]);
}
// 检测是否有环
for (int i = 0; i < numCourses; ++i) {
if (!dfs(graph, visit, i)) {
return false;
}
}
return true;
}
private:
bool dfs(const vector<vector<int>>& graph, vector<int>& visit, int i) {
if (visit[i] == 1) return false; // 如果节点在本轮DFS中被第二次访问,则说明有环
if (visit[i] == 2) return true; // 如果已经访问过,不需要再次访问
visit[i] = 1; // 标记为访问中
for (int j : graph[i]) {
if (!dfs(graph, visit, j)) return false;
}
visit[i] = 2; // 标记为已访问
return true;
}
};
int main() {
Solution solution;
int numCourses = 4;
vector<vector<int>> prerequisites = {{1, 0}, {2, 1}, {3, 2}};
if (solution.canFinish(numCourses, prerequisites)) {
cout << "可以完成所有课程的学习" << endl;
} else {
cout << "无法完成所有课程的学习" << endl;
}
return 0;
}
这段 C++ 代码定义了一个 Solution
类,其中包含了 canFinish
方法,用于判断是否可以完成所有课程的学习。main
函数中创建了一个 Solution
对象,并调用了 canFinish
方法,传入了课程总数和先修课程要求作为参数,然后输出了相应的结果。
Python 版本
from collections import defaultdict
class Solution:
def canFinish(self, numCourses: int, prerequisites: list[list[int]]) -> bool:
# 创建图的邻接表
graph = defaultdict(list)
for course, pre in prerequisites:
graph[pre].append(course)
# 访问状态标记,0=未访问,1=访问中,2=已访问
visit = [0] * numCourses
# 深度优先搜索检测环
def dfs(i):
if visit[i] == 1: # 访问中的节点再次被访问,表示有环
return False
if visit[i] == 2: # 已访问的节点无需再次访问
return True
visit[i] = 1 # 标记为访问中
for j in graph[i]:
if not dfs(j):
return False
visit[i] = 2 # 标记为已访问
return True
# 对每个节点尝试执行DFS
for i in range(numCourses):
if not dfs(i):
return False
return True
# 示例
numCourses = 4
prerequisites = [[1, 0], [2, 1], [3, 2]]
solution = Solution()
print("可以完成所有课程的学习" if solution.canFinish(numCourses, prerequisites) else "无法完成所有课程的学习")
这段 Python 代码定义了一个 Solution
类,其中包含了 canFinish
方法,用于判断是否可以完成所有课程的学习。在 canFinish
方法中,首先构建了课程的邻接表,然后通过深度优先搜索(DFS)检测图中是否存在环。如果不存在环,则可以完成所有课程的学习。最后,代码中给出了一个示例用于测试这个方法。
Java 版本
import java.util.ArrayList;
import java.util.List;
public class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < numCourses; i++) {
graph.add(new ArrayList<>());
}
for (int[] pair : prerequisites) {
graph.get(pair[1]).add(pair[0]);
}
int[] visited = new int[numCourses];
for (int i = 0; i < numCourses; i++) {
if (!dfs(graph, visited, i)) {
return false;
}
}
return true;
}
private boolean dfs(List<List<Integer>> graph, int[] visited, int course) {
if (visited[course] == 1) return false; // cycle detected
if (visited[course] == 2) return true; // already checked
visited[course] = 1; // mark as visiting
for (int nextCourse : graph.get(course)) {
if (!dfs(graph, visited, nextCourse)) {
return false;
}
}
visited[course] = 2; // mark as visited
return true;
}
public static void main(String[] args) {
Solution solution = new Solution();
int numCourses = 4;
int[][] prerequisites = {{1, 0}, {2, 1}, {3, 2}};
System.out.println(solution.canFinish(numCourses, prerequisites) ? "可以完成所有课程的学习" : "无法完成所有课程的学习");
}
}
这段 Java 代码定义了一个 Solution
类,其中包含了 canFinish
方法,用于判断是否可以完成所有课程的学习。在 canFinish
方法中,首先构建了课程的邻接表,然后通过深度优先搜索(DFS)检测图中是否存在环。如果不存在环,则可以完成所有课程的学习。main
方法提供了一个示例,用于测试 canFinish
方法。
Rust 版本
use std::collections::VecDeque; fn can_finish(num_courses: i32, prerequisites: Vec<Vec<i32>>) -> bool { let mut graph = vec![vec![]; num_courses as usize]; let mut in_degree = vec![0; num_courses as usize]; // 构建图并计算所有节点的入度 for pair in prerequisites { graph[pair[1] as usize].push(pair[0] as usize); in_degree[pair[0] as usize] += 1; } // 使用队列进行拓扑排序 let mut queue: VecDeque<usize> = VecDeque::new(); for i in 0..in_degree.len() { if in_degree[i] == 0 { queue.push_back(i); } } let mut count = 0; while let Some(course) = queue.pop_front() { count += 1; for &next_course in &graph[course] { in_degree[next_course] -= 1; if in_degree[next_course] == 0 { queue.push_back(next_course); } } } count == num_courses as usize } fn main() { let num_courses = 4; let prerequisites = vec![vec![1, 0], vec![2, 1], vec![3, 2]]; println!("{}", if can_finish(num_courses, prerequisites) { "可以完成所有课程的学习" } else { "无法完成所有课程的学习" }); }
这段 Rust 代码实现了一个检查是否可以完成所有课程的函数 can_finish
。它首先构建了一个图的邻接表和一个记录每个节点入度的数组。然后使用拓扑排序的方法,通过一个队列来处理所有入度为 0 的节点。如果最终处理的节点数量等于课程总数,说明没有环,可以完成所有课程的学习。在 main
函数中提供了一个示例来测试这个功能。
总结
上述代码片段展示了如何使用不同编程语言实现一个用于检测给定课程安排是否可行的算法。核心思想是使用图论中的拓扑排序来确定是否可以完成所有课程。拓扑排序是一种对有向图的顶点进行排序的方法,使得对于每一条有向边 (u, v),顶点 u 都在顶点 v 之前。这种排序只有在图中没有有向环的情况下才可能。
在这个特定的问题中,课程可以被看作是图的顶点,而某些课程需要作为先决条件的其他课程可以被看作是顶点之间的有向边。算法的目标是检查这个有向图是否有环。如果有环,那么至少有一个课程的先决条件无法满足,因此不可能完成所有课程的学习。
不同语言的实现细节如下:
- Python: 使用字典来构建邻接表,并通过递归的深度优先搜索(DFS)来检测环。
- Java: 使用邻接表和一个访问状态数组来进行 DFS,并通过状态变化来检测环。
- Rust: 使用邻接表和入度数组来实现拓扑排序,通过队列来处理所有入度为 0 的节点,并检测是否所有节点都被处理,从而判断是否有环。
在所有这些实现中,关键步骤是构建图,并通过图的遍历来检测环。如果能够进行拓扑排序,即所有节点都被处理,那么就可以完成所有课程的学习。如果在某个点无法继续进行拓扑排序(因为存在环),那么就不可能完成所有课程的学习。
实现 Trie (前缀树)
题目要求
你需要实现一个 Trie 类,这个类将提供以下功能:
Trie()
:构造函数,用于初始化前缀树对象。void insert(String word)
:将字符串word
插入到前缀树中。boolean search(String word)
:检查字符串word
是否存在于前缀树中。如果存在,返回true
;如果不存在,返回false
。boolean startsWith(String prefix)
:检查是否有任何已插入的字符串word
有以prefix
作为前缀的。如果有,返回true
;如果没有,返回false
。
解题思路
前缀树(Trie)是一种用于快速检索的树形数据结构,特别适合处理字符串集合。以下是实现 Trie 类的思路:
-
初始化 (
Trie()
):- 创建 Trie 类的实例时,需要初始化根节点,这个根节点不包含任何字符,但通常会包含指向子节点的链接(可以使用数组或哈希表实现)。
-
插入 (
insert(String word)
):- 从根节点开始,对于
word
中的每个字符,沿着树向下移动。 - 检查当前字符是否已经作为子节点存在:
- 如果存在,移动到该子节点,继续处理下一个字符。
- 如果不存在,创建一个新的子节点,并将其与当前节点链接起来,然后移动到这个新节点。
- 在单词的最后一个字符的节点上,标记这个节点代表一个单词的结束。
- 从根节点开始,对于
-
搜索 (
search(String word)
):- 从根节点开始,对于
word
中的每个字符,沿着树向下移动。 - 如果当前字符的子节点不存在,返回
false
。 - 如果所有字符都成功匹配,但最后的节点没有标记为一个单词的结束,也返回
false
。 - 如果所有字符都成功匹配,并且最后的节点标记为一个单词的结束,返回
true
。
- 从根节点开始,对于
-
前缀搜索 (
startsWith(String prefix)
):- 类似于
search
方法,但是不需要检查最后的节点是否标记为一个单词的结束。 - 如果能够对于
prefix
中的每个字符都找到对应的子节点,即使最后的节点没有标记为一个单词的结束,也返回true
。 - 如果在某个点上子节点不存在,返回
false
。
- 类似于
注意,Trie 的每个节点可能需要一个标记来表示该节点是否为某个单词的结束,以及可能的 26 个指向子节点的链接(假设只处理小写英文字母)。这些链接可以用数组或哈希表来实现,数组通常有固定大小(例如,26 个英文字母),而哈希表的大小则可以动态变化,适应不同的情况。
Golang 版本
type TrieNode struct {
children [26]*TrieNode
isEnd bool
}
type Trie struct {
root *TrieNode
}
func Constructor() Trie {
return Trie{root: &TrieNode{}}
}
func (this *Trie) Insert(word string) {
node := this.root
for _, ch := range word {
index := ch - 'a'
if node.children[index] == nil {
node.children[index] = &TrieNode{}
}
node = node.children[index]
}
node.isEnd = true
}
func (this *Trie) Search(word string) bool {
node := this.root
for _, ch := range word {
index := ch - 'a'
if node.children[index] == nil {
return false
}
node = node.children[index]
}
return node.isEnd
}
func (this *Trie) StartsWith(prefix string) bool {
node := this.root
for _, ch := range prefix {
index := ch - 'a'
if node.children[index] == nil {
return false
}
node = node.children[index]
}
return true
}
这段代码定义了一个 Trie
类型,包含 Insert
, Search
, 和 StartsWith
方法,以及一个 TrieNode
类型来表示前缀树的每个节点。每个节点包含一个固定大小的 children
数组用于存储子节点的引用,以及一个 isEnd
布尔值来标记是否有单词在此结束。Constructor
函数用于初始化一个 Trie
实例。
C++版本
#include <vector>
#include <string>
class TrieNode {
public:
bool isEnd;
vector<TrieNode*> children;
TrieNode() : isEnd(false), children(26, nullptr) {}
};
class Trie {
private:
TrieNode* root;
public:
/** Initialize your data structure here. */
Trie() {
root = new TrieNode();
}
/** Inserts a word into the trie. */
void insert(string word) {
TrieNode* node = root;
for (char c : word) {
if (node->children[c - 'a'] == nullptr) {
node->children[c - 'a'] = new TrieNode();
}
node = node->children[c - 'a'];
}
node->isEnd = true;
}
/** Returns if the word is in the trie. */
bool search(string word) {
TrieNode* node = root;
for (char c : word) {
node = node->children[c - 'a'];
if (node == nullptr) {
return false;
}
}
return node->isEnd;
}
/** Returns if there is any word in the trie that starts with the given prefix. */
bool startsWith(string prefix) {
TrieNode* node = root;
for (char c : prefix) {
node = node->children[c - 'a'];
if (node == nullptr) {
return false;
}
}
return true;
}
};
/**
* Your Trie object will be instantiated and called as such:
* Trie* obj = new Trie();
* obj->insert(word);
* bool param_2 = obj->search(word);
* bool param_3 = obj->startsWith(prefix);
*/
这段 C++ 代码定义了一个 Trie
类和一个 TrieNode
类。TrieNode
类包含一个 isEnd
标志位和一个 children
向量,用于存储指向子节点的指针。Trie
类包含 insert
, search
, 和 startsWith
方法,用于在前缀树中插入单词、搜索单词和检查是否存在以某前缀开始的单词。构造函数 Trie()
初始化一个新的前缀树。
Python 版本
class TrieNode:
def __init__(self):
self.children = {}
self.is_end_of_word = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word: str) -> None:
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end_of_word = True
def search(self, word: str) -> bool:
node = self.root
for char in word:
if char not in node.children:
return False
node = node.children[char]
return node.is_end_of_word
def starts_with(self, prefix: str) -> bool:
node = self.root
for char in prefix:
if char not in node.children:
return False
node = node.children[char]
return True
# Your Trie object will be instantiated and called as such:
# obj = Trie()
# obj.insert(word)
# param_2 = obj.search(word)
# param_3 = obj.starts_with(prefix)
这段 Python 代码定义了一个 Trie
类和一个辅助的 TrieNode
类。TrieNode
类用于表示前缀树的每个节点,包含一个字典 children
来存储子节点,以及一个布尔值 is_end_of_word
来标记节点是否是某个单词的结尾。Trie
类包含 insert
, search
, 和 starts_with
方法,分别用于向前缀树中插入单词、搜索单词和检查是否存在以某前缀开始的单词。
Java 版本
class TrieNode {
private TrieNode[] links;
private final int R = 26;
private boolean isEnd;
public TrieNode() {
links = new TrieNode[R];
}
public boolean containsKey(char ch) {
return links[ch -'a'] != null;
}
public TrieNode get(char ch) {
return links[ch -'a'];
}
public void put(char ch, TrieNode node) {
links[ch -'a'] = node;
}
public void setEnd() {
isEnd = true;
}
public boolean isEnd() {
return isEnd;
}
}
public class Trie {
private TrieNode root;
public Trie() {
root = new TrieNode();
}
public void insert(String word) {
TrieNode node = root;
for (int i = 0; i < word.length(); i++) {
char currentChar = word.charAt(i);
if (!node.containsKey(currentChar)) {
node.put(currentChar, new TrieNode());
}
node = node.get(currentChar);
}
node.setEnd();
}
private TrieNode searchPrefix(String word) {
TrieNode node = root;
for (int i = 0; i < word.length(); i++) {
char curLetter = word.charAt(i);
if (node.containsKey(curLetter)) {
node = node.get(curLetter);
} else {
return null;
}
}
return node;
}
public boolean search(String word) {
TrieNode node = searchPrefix(word);
return node != null && node.isEnd();
}
public boolean startsWith(String prefix) {
TrieNode node = searchPrefix(prefix);
return node != null;
}
}
// Your Trie object will be instantiated and called as such:
// Trie obj = new Trie();
// obj.insert(word);
// boolean param_2 = obj.search(word);
// boolean param_3 = obj.startsWith(prefix);
这段 Java 代码定义了一个 Trie
类和一个 TrieNode
类。TrieNode
类包含一个 TrieNode
数组来存储链接到子节点的引用,一个布尔值 isEnd
来标记是否有单词在此结束。Trie
类包含 insert
, search
, 和 startsWith
方法,用于在前缀树中插入单词、搜索单词和检查是否存在以某前缀开始的单词。此外,还有一个辅助的 searchPrefix
方法,用于搜索整个单词或前缀在前缀树中的位置。
Rust 版本
#![allow(unused)] fn main() { const ALPHABET_SIZE: usize = 26; struct TrieNode { children: [Option<Box<TrieNode>>; ALPHABET_SIZE], is_end_of_word: bool, } impl TrieNode { fn new() -> Self { TrieNode { children: Default::default(), is_end_of_word: false, } } } pub struct Trie { root: TrieNode, } impl Trie { pub fn new() -> Self { Trie { root: TrieNode::new(), } } pub fn insert(&mut self, word: String) { let mut node = &mut self.root; for c in word.chars() { let index = (c as usize) - ('a' as usize); node = node.children[index].get_or_insert_with(|| Box::new(TrieNode::new())); } node.is_end_of_word = true; } pub fn search(&self, word: String) -> bool { self.find(word).map_or(false, |node| node.is_end_of_word) } pub fn starts_with(&self, prefix: String) -> bool { self.find(prefix).is_some() } fn find(&self, prefix: String) -> Option<&TrieNode> { let mut node = &self.root; for c in prefix.chars() { match node.children[(c as usize) - ('a' as usize)] { Some(ref child) => node = child, None => return None, } } Some(node) } } }
这段 Rust 代码定义了一个 TrieNode
结构体,它包含一个固定大小的 children
数组,用于存储指向子节点的指针,以及一个 is_end_of_word
布尔值,用于标记节点是否是单词的结尾。Trie
结构体包含一个 root
TrieNode
,以及 insert
、search
和 starts_with
方法,用于在前缀树中插入单词、搜索单词和检查是否存在以某前缀开始的单词。此外,还有一个私有的 find
方法,用于查找整个单词或前缀在前缀树中的位置。
总结
上面的解法展示了如何在不同的编程语言中实现一个前缀树(Trie)。每种语言的实现都遵循了相同的基本原理,但是具体的语法和数据结构的使用根据语言的特性有所不同。
-
共同点:所有实现都包含了三个主要的操作:
insert
:向前缀树中插入一个新的单词。search
:搜索前缀树中是否存在一个完整的单词。startsWith
:检查前缀树中是否有单词以给定的前缀开始。
每个实现都使用了一个辅助的节点结构(
TrieNode
),该结构通常包含了一个指向子节点的数组(或映射)以及一个布尔值标记,用于表示是否有单词在该节点结束。 -
不同点:
- Python:使用字典来存储子节点,以字符作为键。
- Java:使用固定大小的数组来存储子节点,以字符的 ASCII 值减去 'a' 的 ASCII 值作为索引。
- Rust:使用固定大小的数组,其中每个元素是
Option<Box<TrieNode>>
类型,这允许 Rust 以值的形式拥有节点并提供空值的选项。
每种语言的实现都遵循了前缀树的基本概念,但是在内存管理、类型系统和语法糖方面各有特色。例如,Rust 版本使用了 Option
和 Box
来处理可空的指针和堆分配的内存,而 Python 版本则利用了其动态类型和内置的字典类型来简化节点的子项管理。Java 版本则介于两者之间,使用了更传统的 OOP 方法和数组来管理节点的子项。
回溯
回溯算法是一种通过探索所有可能的候选解来找出所有解的问题的算法。如果候选解被确认不是一个解(或者至少不是最后一个解),回溯算法会通过在上一步进行一些变化来丢弃它,即“回溯”并且尝试新的选项。回溯通常用于解决约束满足问题,其中包括组合问题、划分问题、子集问题等。
回溯算法的通用思路可以概括为以下几个步骤:
- 选择:从候选解中选择一个可能的分支。
- 约束:检查这个分支是否符合约束条件,如果不符合,剪枝。
- 目标:检查当前分支是否满足解的条件,如果满足,将其加入解集。
- 递归:对剩下的候选解重复上述步骤。
- 回溯:如果当前分支已经到达末端,或者不满足解的条件,则返回上一步,尝试其他可能的分支。
在 Go 语言中实现回溯算法,可以通过递归函数来模拟这个过程。下面是一个使用 Go 语言实现的回溯算法的代码示例,这个例子是解决组合问题的,即从 1 到 n 的整数中选出 k 个数的所有可能组合。
package main
import "fmt"
func combine(n int, k int) [][]int {
var result [][]int
var temp []int
backtrack(1, n, k, &temp, &result)
return result
}
func backtrack(start, n, k int, temp *[]int, result *[][]int) {
// 如果组合的长度已经满足k,则将其加入结果集
if len(*temp) == k {
comb := make([]int, k)
copy(comb, *temp)
*result = append(*result, comb)
return
}
// 从start开始尝试每个可能的选项
for i := start; i <= n; i++ {
*temp = append(*temp, i) // 选择当前数
backtrack(i+1, n, k, temp, result) // 递归
*temp = (*temp)[:len(*temp)-1] // 回溯,撤销选择
}
}
func main() {
n := 4
k := 2
combinations := combine(n, k)
for _, comb := range combinations {
fmt.Println(comb)
}
}
在这个代码中,combine
函数初始化结果集和临时数组,然后调用 backtrack
函数开始回溯。backtrack
函数是递归的,它会尝试所有可能的组合,并且在满足条件时将其添加到结果集中。当递归调用返回时,它会撤销当前的选择(这就是回溯的部分),然后尝试下一个选项。
这个通用的回溯框架可以应用于许多其他类型的问题,比如排列问题、子集问题等,只需要根据具体问题调整选择、约束和目标的处理即可。
全排列
题目要求
编写一个算法,输入一个不包含重复数字的数组 nums
,输出这个数组的所有可能的全排列。结果可以按照任意顺序返回。
解题思路
全排列问题是一个经典的递归问题,可以通过回溯算法来解决。回溯算法可以看作是通过试错来寻找所有可能解的算法,如果当前选择不满足条件,则回退到上一步进行重新选择,这个过程可以用递归函数来实现。
解决这个问题的步骤如下:
-
初始化路径: 创建一个空列表
path
,用于存储单个排列的路径。 -
递归与回溯: 设计一个递归函数,该函数接受当前路径
path
和一个待选择的数字列表options
作为参数。每次递归调用都会尝试将options
列表中的一个数字添加到path
中。 -
选择与撤销选择: 在递归函数中,遍历
options
列表,对于每个数字,我们做出选择,将其加入到path
中,并从options
中移除,然后递归调用函数。递归返回后,需要撤销之前的选择,将数字从path
中移除,并放回options
中,以便进行下一次选择。 -
终止条件: 当
options
列表为空时,说明已经完成了一个全排列的构建,此时将path
添加到结果列表中。 -
返回结果: 递归完成后,返回存储所有全排列的结果列表。
通过这种方式,我们可以遍历所有可能的排列组合,直到所有数字都被使用过,从而得到所有的全排列。
Golang 版本
package main
import "fmt"
func permute(nums []int) [][]int {
var res [][]int
var path []int
used := make([]bool, len(nums))
var backtrack func()
backtrack = func() {
if len(path) == len(nums) {
// Make a deep copy of the path, as it will be reused
temp := make([]int, len(path))
copy(temp, path)
res = append(res, temp)
return
}
for i, num := range nums {
if !used[i] {
// Make a choice
path = append(path, num)
used[i] = true
// Enter the next level of decision tree
backtrack()
// Undo the choice
path = path[:len(path)-1]
used[i] = false
}
}
}
backtrack()
return res
}
func main() {
nums := []int{1, 2, 3}
allPermutations := permute(nums)
fmt.Println(allPermutations)
}
这段代码定义了一个 permute
函数,它接受一个整数数组 nums
并返回该数组的所有全排列。在 main
函数中,我们定义了一个示例数组 nums
,调用 permute
函数,并打印出所有的全排列。代码中使用了回溯算法的框架,通过递归函数 backtrack
来生成所有可能的排列。
C++版本
#include <iostream>
#include <vector>
void backtrack(std::vector<int>& nums, std::vector<std::vector<int>>& result, std::vector<int>& permutation, std::vector<bool>& chosen) {
if (permutation.size() == nums.size()) {
result.push_back(permutation);
return;
}
for (int i = 0; i < nums.size(); ++i) {
if (chosen[i]) continue;
chosen[i] = true;
permutation.push_back(nums[i]);
backtrack(nums, result, permutation, chosen);
chosen[i] = false;
permutation.pop_back();
}
}
std::vector<std::vector<int>> permute(std::vector<int>& nums) {
std::vector<std::vector<int>> result;
std::vector<int> permutation;
std::vector<bool> chosen(nums.size(), false);
backtrack(nums, result, permutation, chosen);
return result;
}
int main() {
std::vector<int> nums = {1, 2, 3};
std::vector<std::vector<int>> result = permute(nums);
for (const auto& perm : result) {
for (int num : perm) {
std::cout << num << ' ';
}
std::cout << std::endl;
}
return 0;
}
这段 C++代码实现了一个 permute
函数,它接受一个整数向量 nums
并返回该向量的所有全排列。在 main
函数中,我们定义了一个示例向量 nums
,调用 permute
函数,并打印出所有的全排列。代码中使用了回溯算法的框架,通过递归函数 backtrack
来生成所有可能的排列。在 backtrack
函数中,我们通过一个布尔向量 chosen
来记录哪些数字已经被选择过,以避免重复选择。
Python 版本
from typing import List
def permute(nums: List[int]) -> List[List[int]]:
def backtrack(first=0):
# if all integers are used up
if first == n:
output.append(nums[:])
for i in range(first, n):
# place i-th integer first
# in the current permutation
nums[first], nums[i] = nums[i], nums[first]
# use next integers to complete the permutations
backtrack(first + 1)
# backtrack
nums[first], nums[i] = nums[i], nums[first]
n = len(nums)
output = []
backtrack()
return output
# Example usage:
if __name__ == '__main__':
nums = [1, 2, 3]
print(permute(nums))
这段 Python 代码定义了一个 permute
函数,它接受一个整数列表 nums
并返回该列表的所有全排列。在 permute
函数中,我们定义了一个嵌套的 backtrack
函数,它使用递归来生成全排列。backtrack
函数通过交换元素的方式,将每个元素都尝试放在当前排列的第一个位置,然后递归地处理剩余的位置。当达到排列的末尾时,将当前排列复制到输出列表中。在 backtrack
函数中,我们通过交换回来的操作来撤销之前的选择,这样就可以进行新的排列组合。在 if __name__ == '__main__':
部分,我们提供了一个示例用法,创建了一个列表 nums
,调用 permute
函数,并打印出所有的全排列。
Java 版本
import java.util.ArrayList;
import java.util.List;
public class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> results = new ArrayList<>();
if (nums == null || nums.length == 0) {
return results;
}
backtrack(nums, new boolean[nums.length], new ArrayList<>(), results);
return results;
}
private void backtrack(int[] nums, boolean[] used, List<Integer> current, List<List<Integer>> results) {
if (current.size() == nums.length) {
results.add(new ArrayList<>(current));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) continue;
used[i] = true;
current.add(nums[i]);
backtrack(nums, used, current, results);
current.remove(current.size() - 1);
used[i] = false;
}
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {1, 2, 3};
List<List<Integer>> permute = solution.permute(nums);
for (List<Integer> perm : permute) {
for (int num : perm) {
System.out.print(num + " ");
}
System.out.println();
}
}
}
这段 Java 代码定义了一个 Solution
类,其中包含了一个 permute
方法,用于计算一个数组的所有排列组合。permute
方法接受一个整数数组 nums
作为输入,并返回一个列表,其中包含所有可能的排列。
在 Solution
类中,还定义了一个私有的辅助方法 backtrack
,它使用回溯算法来生成所有的排列。这个方法接受当前的数组 nums
,一个布尔数组 used
来跟踪哪些数字已经被使用过,一个当前排列的列表 current
,以及一个结果列表 results
。
main
方法中创建了 Solution
的一个实例,并调用 permute
方法,然后打印出所有的排列结果。
Rust 版本
use std::collections::HashSet; fn permute(nums: Vec<i32>) -> Vec<Vec<i32>> { let mut results = Vec::new(); let mut current = Vec::new(); let mut used = HashSet::new(); backtrack(&nums, &mut used, &mut current, &mut results); results } fn backtrack(nums: &Vec<i32>, used: &mut HashSet<i32>, current: &mut Vec<i32>, results: &mut Vec<Vec<i32>>) { if current.len() == nums.len() { results.push(current.clone()); return; } for &num in nums { if !used.contains(&num) { used.insert(num); current.push(num); backtrack(nums, used, current, results); current.pop(); used.remove(&num); } } } fn main() { let nums = vec![1, 2, 3]; let permutations = permute(nums); for p in permutations { println!("{:?}", p); } }
这段 Rust 代码实现了一个 permute
函数,它接受一个整数向量 nums
并返回该向量的所有全排列。在 permute
函数中,我们定义了一个嵌套的 backtrack
函数,它使用递归来生成全排列。backtrack
函数通过使用一个 HashSet
来跟踪哪些数字已经被使用过,从而避免重复。
在 main
函数中,我们创建了一个向量 nums
,调用 permute
函数,并打印出所有的全排列。
总结
上述解法采用了回溯算法来求解不含重复数字的数组的所有可能全排列问题。回溯算法是一种通过探索所有可能的候选解来找出所有解的算法。如果候选解被确认不是一个解(或者至少不是最后一个解),回溯算法会丢弃该解,即回溯并且尝试另一种可能的解。
在这个问题中,算法的核心思想是:
- 从数组的第一个位置开始,递归地将每个数字放到当前位置,并对剩余的数字进行全排列。
- 通过递归调用自身,继续这个过程,直到当前排列中包含所有的数字。
- 每次递归调用都会尝试数组中的每个尚未使用的数字,以确保生成所有可能的排列。
- 当达到递归的底部,即当前排列包含所有数字时,将其添加到结果集中。
- 在每次递归返回之前,需要撤销上一步的操作,以便回溯到上一步的状态,这通常通过移除当前排列的最后一个数字和更新使用状态来实现。
在不同的编程语言实现中,可能会使用不同的数据结构来跟踪哪些数字已经被使用过。例如,在 C++和 Java 实现中,通常使用一个布尔数组来标记哪些数字已经被放入当前排列中。而在 Rust 实现中,则使用了HashSet
来存储已经被使用的数字。
无论使用哪种编程语言,算法的基本框架和逻辑是相同的,都是通过递归和回溯来实现的。
子集
题目要求
编写一个函数,输入为一个包含互不相同整数的数组 nums
。该函数的目标是找出数组 nums
的所有可能的子集,并将这些子集作为一个列表返回。这里的子集包括了从空集到包含所有元素的集合本身的所有可能组合。返回的子集列表中不应包含任何重复的子集,并且子集可以以任意顺序出现。
解题思路
解决这个问题的关键在于理解子集的概念以及如何构造所有可能的子集。一个集合的子集是由原集合中的元素组成的,且元素的个数可以从 0(空集)到集合元素个数的任意值。对于一个有 n
个元素的集合,它有 2^n
个子集,包括空集和它本身。
这个问题可以通过递归或迭代的方式来解决。以下是解题的一些思路:
-
递归(回溯)方法:
- 从空集开始,每次递归都向当前集合中添加一个新的元素,形成新的子集。
- 对于数组中的每个元素,都有两种选择:要么加入子集,要么不加入子集。
- 递归的基本情况是当考虑完所有元素后,将当前构建的子集添加到结果列表中。
- 递归的过程中,每次调用都会沿着树的一条路径向下,直到达到叶子节点,然后回溯到上一个节点,继续探索其他路径。
-
迭代方法:
- 从只包含空集的集合开始。
- 遍历输入数组中的每个数字,对于结果集中的每个现有子集,添加一个包含当前数字的新子集。
- 这样,每遍历一个数字,现有的子集数量就会翻倍,因为每个现有的子集都可以选择是否包含这个新的数字。
-
位运算方法:
- 利用位运算来生成所有可能的子集。每个子集都对应一个长度为
n
的二进制数,其中n
是数组的长度。 - 数组中的每个元素都对应二进制数的一个位。如果该位为 1,则表示子集包含对应的元素;如果该位为 0,则表示子集不包含对应的元素。
- 通过从
0
到2^n - 1
的整数,可以生成所有可能的二进制数,进而生成所有可能的子集。
- 利用位运算来生成所有可能的子集。每个子集都对应一个长度为
在实现上述任何方法时,都需要注意避免重复的子集,但由于题目已经说明数组中的元素互不相同,这里不需要额外的步骤来处理重复问题。
Golang 版本
package main
import "fmt"
func subsets(nums []int) [][]int {
result := [][]int{}
subset := []int{}
var dfs func(int)
dfs = func(start int) {
// Make a copy of subset, as the original subset will be backtracked
temp := make([]int, len(subset))
copy(temp, subset)
result = append(result, temp)
for i := start; i < len(nums); i++ {
// Include nums[i] into current subset
subset = append(subset, nums[i])
// Move on to the next element
dfs(i + 1)
// Exclude nums[i] from current subset (backtrack)
subset = subset[:len(subset)-1]
}
}
dfs(0)
return result
}
func main() {
nums := []int{1, 2, 3}
allSubsets := subsets(nums)
fmt.Println(allSubsets)
}
这段代码使用了回溯算法来生成所有可能的子集。subsets
函数初始化一个空的结果集 result
和一个空的当前子集 subset
。dfs
是一个定义在 subsets
函数内部的递归辅助函数,用于深度优先搜索所有可能的子集。每次递归调用 dfs
都会尝试包含从 start
开始的数组元素,并在内部循环中递归地调用自身,每次递归都会增加 start
的值,以避免重复。当回溯时,它会移除最后一个元素以尝试其他的组合。
main
函数中创建了一个示例数组 nums
,并调用 subsets
函数来获取所有子集,然后打印出来。
C++版本
#include <iostream>
#include <vector>
class Solution {
public:
std::vector<std::vector<int>> subsets(std::vector<int>& nums) {
std::vector<std::vector<int>> result;
std::vector<int> current;
generateSubsets(0, nums, current, result);
return result;
}
private:
void generateSubsets(int index, std::vector<int>& nums, std::vector<int>& current, std::vector<std::vector<int>>& result) {
result.push_back(current);
for (int i = index; i < nums.size(); i++) {
current.push_back(nums[i]);
generateSubsets(i + 1, nums, current, result);
current.pop_back();
}
}
};
int main() {
Solution solution;
std::vector<int> nums = {1, 2, 3};
std::vector<std::vector<int>> result = solution.subsets(nums);
for (const auto& subset : result) {
std::cout << "{ ";
for (int num : subset) {
std::cout << num << " ";
}
std::cout << "}" << std::endl;
}
return 0;
}
这段 C++代码定义了一个 Solution
类,其中包含了解决问题的核心函数 subsets
。subsets
函数初始化结果集 result
,并调用私有辅助函数 generateSubsets
来递归生成所有子集。generateSubsets
函数通过回溯算法遍历所有可能的子集组合。
在 main
函数中,我们创建了一个示例数组 nums
,并使用 Solution
类的实例来调用 subsets
函数。然后,我们遍历并打印出所有生成的子集。
Python 版本
class Solution:
def subsets(self, nums):
res = []
subset = []
def dfs(index):
if index == len(nums):
res.append(subset.copy())
return
# Include the number at the current index
subset.append(nums[index])
dfs(index + 1)
# Exclude the number at the current index
subset.pop()
dfs(index + 1)
dfs(0)
return res
# Example usage:
if __name__ == "__main__":
nums = [1, 2, 3]
solution = Solution()
result = solution.subsets(nums)
for subset in result:
print(subset)
这段 Python 代码定义了一个 Solution
类,其中包含了一个 subsets
方法,用于生成输入数组 nums
的所有子集。subsets
方法中使用了一个嵌套的 dfs
函数来进行深度优先搜索。dfs
函数通过递归的方式,每次决定是否将当前索引的数字包含到子集中,从而遍历所有可能的子集组合。
在 __main__
部分,我们创建了一个示例数组 nums
,并调用 Solution
类的 subsets
方法来获取所有子集,然后打印出来。
Java 版本
import java.util.ArrayList;
import java.util.List;
public class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> output = new ArrayList<>();
generateSubsets(0, nums, new ArrayList<>(), output);
return output;
}
private void generateSubsets(int index, int[] nums, List<Integer> current, List<List<Integer>> output) {
if (index == nums.length) {
output.add(new ArrayList<>(current));
return;
}
// Include the number at the current index
current.add(nums[index]);
generateSubsets(index + 1, nums, current, output);
// Exclude the number at the current index
current.remove(current.size() - 1);
generateSubsets(index + 1, nums, current, output);
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {1, 2, 3};
List<List<Integer>> subsets = solution.subsets(nums);
for (List<Integer> subset : subsets) {
System.out.println(subset);
}
}
}
这段 Java 代码定义了一个 Solution
类,其中包含了一个 subsets
方法,用于生成输入数组 nums
的所有子集。subsets
方法中使用了一个私有辅助方法 generateSubsets
来递归地生成所有可能的子集。generateSubsets
方法通过回溯算法遍历所有可能的子集组合。
在 main
方法中,我们创建了一个示例数组 nums
,并调用 Solution
类的 subsets
方法来获取所有子集,然后遍历并打印出来。
Rust 版本
use std::vec::Vec; pub struct Solution; impl Solution { pub fn subsets(nums: Vec<i32>) -> Vec<Vec<i32>> { let mut res = Vec::new(); let mut subset = Vec::new(); Self::backtrack(&nums, 0, &mut subset, &mut res); res } fn backtrack(nums: &Vec<i32>, start: usize, subset: &mut Vec<i32>, res: &mut Vec<Vec<i32>>) { res.push(subset.clone()); for i in start..nums.len() { subset.push(nums[i]); Self::backtrack(nums, i + 1, subset, res); subset.pop(); } } } fn main() { let nums = vec![1, 2, 3]; let subsets = Solution::subsets(nums); for subset in subsets { println!("{:?}", subset); } }
这段 Rust 代码定义了一个 Solution
结构体,其中包含了一个静态方法 subsets
,用于生成输入向量 nums
的所有子集。subsets
方法中使用了一个私有静态辅助方法 backtrack
来递归地生成所有可能的子集。backtrack
方法通过回溯算法遍历所有可能的子集组合。
在 main
函数中,我们创建了一个示例向量 nums
,并调用 Solution
结构体的 subsets
方法来获取所有子集,然后遍历并打印出来。
总结
上述代码段展示了如何使用不同编程语言(Go, Java, Rust)实现一个算法来生成一个整数数组的所有可能子集。这些整数互不相同,因此生成的子集也将是唯一的。所有的实现都采用了回溯算法的思想,以下是这些实现的共同点:
-
初始化:每种语言都定义了一个方法或函数来初始化结果集合,并开始递归过程。
-
递归函数:每种语言都实现了一个递归函数或方法,该函数负责:
- 将当前构建的子集添加到结果集合中。
- 遍历数组中剩余的元素,决定是否将每个元素包含到当前子集中。
- 递归地调用自身,一次包含一个元素,直到所有元素都被考虑过。
- 在每次递归调用后,回溯以移除最近添加的元素,以便为下一个元素的决策清空状态。
-
回溯:在递归过程中,算法通过添加和移除元素来探索所有可能的子集组合,这是回溯算法的典型特征。
-
结果返回:一旦所有的元素都被考虑过,当前的子集被添加到结果集合中,最终返回这个结果集合。
-
主函数/方法:每种语言都提供了一个主函数或方法来调用子集生成函数,并打印或返回最终的子集列表。
这些解法虽然在语法上有所不同,但在逻辑结构和算法实现上是一致的。通过这些代码,我们可以看到不同编程语言在处理相同算法问题时的语法和结构差异。
电话号码的字母组合
题目要求
给定一个字符串,该字符串由数字 2 到 9 组成。要求编写一个算法,找出这个字符串所能代表的所有可能的字母组合。字母组合与电话按键上的字母对应,即:
- 2 对应 a, b, c
- 3 对应 d, e, f
- 4 对应 g, h, i
- 5 对应 j, k, l
- 6 对应 m, n, o
- 7 对应 p, q, r, s
- 8 对应 t, u, v
- 9 对应 w, x, y, z
注意:
- 输入字符串的长度不固定,但仅包含数字 2-9。
- 输出的字母组合可以是任意顺序。
- 数字 1 不对应任何字母,因此在输入字符串中不会出现。
解题思路
这个问题可以通过回溯算法来解决。回溯算法是一种通过探索所有可能的候选解来找出所有解的算法。如果候选解被确认不是一个有效的解,回溯算法会丢弃它,并回溯到上一步然后尝试另一种可能。
对于这个问题,我们可以定义如下的回溯过程:
- 从输入字符串的第一个数字开始。
- 将当前数字对应到其能代表的所有字母。
- 对于当前数字对应的每一个字母,将其加到目前为止的组合中,并对输入字符串的下一个数字进行相同的处理。
- 当我们到达输入字符串的末尾时,我们将当前的字母组合加入到结果集中。
- 如果还没有到达字符串末尾,继续对剩余的数字进行步骤 2 到 4 的处理。
- 一旦处理完一个字母,回溯到前一个字母,移除当前字母,尝试下一个可能的字母组合。
通过这样的方式,我们可以确保探索了所有可能的字母组合,并且当我们到达字符串的末尾时,我们会有一个完整的组合加入到结果集中。这个过程会一直重复,直到所有的组合都被找到为止。
Golang 版本
package main
import "fmt"
func letterCombinations(digits string) []string {
if len(digits) == 0 {
return []string{}
}
phoneMap := map[byte]string{
'2': "abc", '3': "def", '4': "ghi", '5': "jkl",
'6': "mno", '7': "pqrs", '8': "tuv", '9': "wxyz",
}
var combinations []string
var backtrack func(index int, path []byte)
backtrack = func(index int, path []byte) {
if index == len(digits) {
combinations = append(combinations, string(path))
return
}
letters := phoneMap[digits[index]]
for i := 0; i < len(letters); i++ {
path = append(path, letters[i])
backtrack(index+1, path)
path = path[:len(path)-1]
}
}
backtrack(0, []byte{})
return combinations
}
func main() {
digits := "23"
fmt.Println(letterCombinations(digits))
}
这段代码定义了一个letterCombinations
函数,它接受一个字符串digits
作为输入,并返回一个字符串切片,包含了所有可能的字母组合。在main
函数中,我们调用了letterCombinations
函数,并打印出结果。这个实现使用了回溯算法,通过递归函数backtrack
来生成所有可能的字母组合。
C++版本
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
using namespace std;
class Solution {
public:
vector<string> letterCombinations(string digits) {
if (digits.empty()) return {};
unordered_map<char, string> phoneMap{
{'2', "abc"}, {'3', "def"}, {'4', "ghi"}, {'5', "jkl"},
{'6', "mno"}, {'7', "pqrs"}, {'8', "tuv"}, {'9', "wxyz"}
};
vector<string> combinations;
string currentCombination;
backtrack(combinations, phoneMap, digits, 0, currentCombination);
return combinations;
}
private:
void backtrack(vector<string>& combinations, const unordered_map<char, string>& phoneMap,
const string& digits, int index, string& currentCombination) {
if (index == digits.length()) {
combinations.push_back(currentCombination);
return;
}
const string& letters = phoneMap.at(digits[index]);
for (const char& letter : letters) {
currentCombination.push_back(letter);
backtrack(combinations, phoneMap, digits, index + 1, currentCombination);
currentCombination.pop_back();
}
}
};
int main() {
Solution solution;
string digits = "23";
vector<string> combinations = solution.letterCombinations(digits);
for (const string& combination : combinations) {
cout << combination << endl;
}
return 0;
}
这段 C++代码实现了一个Solution
类,其中包含了letterCombinations
方法,用于计算并返回所有可能的字母组合。main
函数中创建了一个Solution
对象,并调用了letterCombinations
方法,然后打印出所有的组合。这个实现同样使用了回溯算法,通过私有方法backtrack
来递归地构建所有可能的组合。
Python 版本
from typing import List
class Solution:
def letterCombinations(self, digits: str) -> List[str]:
if not digits:
return []
phone_map = {
'2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl',
'6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz'
}
def backtrack(index: int, path: str):
if index == len(digits):
combinations.append(path)
return
for letter in phone_map[digits[index]]:
backtrack(index + 1, path + letter)
combinations = []
backtrack(0, "")
return combinations
# Example usage:
if __name__ == '__main__':
solution = Solution()
digits = "23"
print(solution.letterCombinations(digits))
这段 Python 代码定义了一个Solution
类,其中包含了letterCombinations
方法,它接受一个字符串digits
作为输入,并返回一个字符串列表,包含了所有可能的字母组合。在if __name__ == '__main__':
块中,我们创建了一个Solution
实例,并调用了letterCombinations
方法,然后打印出结果。这个实现使用了回溯算法,通过嵌套的backtrack
函数来递归地生成所有可能的字母组合。
Java 版本
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
public class Solution {
private Map<Character, String> phoneMap = new HashMap<Character, String>() {{
put('2', "abc");
put('3', "def");
put('4', "ghi");
put('5', "jkl");
put('6', "mno");
put('7', "pqrs");
put('8', "tuv");
put('9', "wxyz");
}};
public List<String> letterCombinations(String digits) {
List<String> combinations = new ArrayList<>();
if (digits == null || digits.length() == 0) {
return combinations;
}
backtrack(combinations, digits, 0, new StringBuilder());
return combinations;
}
private void backtrack(List<String> combinations, String digits, int index, StringBuilder path) {
if (index == digits.length()) {
combinations.add(path.toString());
return;
}
String possibleLetters = phoneMap.get(digits.charAt(index));
for (char letter : possibleLetters.toCharArray()) {
path.append(letter);
backtrack(combinations, digits, index + 1, path);
path.deleteCharAt(path.length() - 1);
}
}
public static void main(String[] args) {
Solution solution = new Solution();
String digits = "23";
List<String> combinations = solution.letterCombinations(digits);
for (String combination : combinations) {
System.out.println(combination);
}
}
}
这段 Java 代码定义了一个Solution
类,其中包含了letterCombinations
方法,它接受一个字符串digits
作为输入,并返回一个字符串列表,包含了所有可能的字母组合。main
方法中创建了一个Solution
实例,并调用了letterCombinations
方法,然后打印出所有的组合。这个实现使用了回溯算法,通过私有方法backtrack
来递归地构建所有可能的组合。
Rust 版本
use std::collections::HashMap; fn letter_combinations(digits: String) -> Vec<String> { let mut res = Vec::new(); if digits.is_empty() { return res; } let phone_map: HashMap<char, &str> = [ ('2', "abc"), ('3', "def"), ('4', "ghi"), ('5', "jkl"), ('6', "mno"), ('7', "pqrs"), ('8', "tuv"), ('9', "wxyz"), ].iter().cloned().collect(); fn backtrack( res: &mut Vec<String>, phone_map: &HashMap<char, &str>, digits: &str, index: usize, combination: &mut String, ) { if index == digits.len() { res.push(combination.clone()); return; } if let Some(letters) = phone_map.get(&digits.chars().nth(index).unwrap()) { for letter in letters.chars() { combination.push(letter); backtrack(res, phone_map, digits, index + 1, combination); combination.pop(); } } } backtrack(&mut res, &phone_map, &digits, 0, &mut String::new()); res } fn main() { let digits = "23".to_string(); let combinations = letter_combinations(digits); for combination in combinations { println!("{}", combination); } }
这段 Rust 代码定义了一个letter_combinations
函数,它接受一个String
类型的digits
作为输入,并返回一个包含所有可能字母组合的Vec<String>
。在main
函数中,我们调用了letter_combinations
函数,并遍历打印出所有的组合。这个实现使用了回溯算法,通过一个嵌套的backtrack
函数来递归地生成所有可能的字母组合。
总结
上述解法在不同编程语言中实现了相同的功能:给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。这个问题通常通过回溯算法来解决,因为需要探索所有可能的字母组合,直到找到所有的解。
在每种语言的实现中,都定义了一个映射(字典、哈希表等),将每个数字映射到对应的字母字符串。然后,使用回溯的方法,从第一个数字开始,对于每个可能的字母,都递归地进行探索,直到达到字符串的末尾。每当到达字符串的末尾时,就将当前的字母组合添加到结果集中。
不同语言之间的主要区别在于语法和一些特定的 API 调用。例如:
- 在 C++中,使用了
std::vector
和std::unordered_map
来存储组合和映射。 - 在 Python 中,使用了列表和字典,并且有更简洁的循环和字符串处理方式。
- 在 Java 中,使用了
ArrayList
和HashMap
,以及StringBuilder
来高效地构建字符串。 - 在 Rust 中,使用了
Vec
和HashMap
,以及 Rust 特有的所有权和借用规则来管理内存。
尽管实现的细节不同,但所有这些解法都遵循了相同的算法逻辑。
组合总和
题目要求
你需要编写一个算法,该算法的输入是一个不包含重复元素的整数数组 candidates
和一个目标整数 target
。你的任务是找出所有不同的组合,这些组合中的数字加起来等于 target
。结果应以列表的形式返回,列表中的每个元素是一个可能的组合,且组合内的数字可以是 candidates
中的任意数字重复多次。需要注意的是,组合中数字的顺序不影响组合的唯一性,即 [2,2,3]
和 [2,3,2]
视为同一组合。
解题思路
这个问题可以通过回溯算法来解决。回溯算法是一种通过探索所有可能的候选解来找出所有解的算法。如果候选解被确认不是一个有效的解,或者不可能产生一个有效的解,那么这个算法会回溯到上一步,然后通过改变上一步的解继续尝试找出有效的解。
解题步骤如下:
-
排序(可选):首先,可以对数组
candidates
进行排序。这一步不是必须的,但有时可以帮助更快地剪枝,尤其是当目标数target
较小,而candidates
中的数较大时。 -
定义回溯函数:定义一个回溯函数,该函数将递归地尝试所有可能的组合方式。这个函数可以接受参数,如当前组合
combination
,当前组合的和current_sum
,以及当前考虑的candidates
的起始位置start_index
。 -
递归与回溯:从
start_index
开始,遍历candidates
数组。对于每个数,你可以选择“使用”或“不使用”它:- 如果当前组合的和加上这个数小于或等于
target
,则“使用”这个数,将其添加到当前组合中,并递归调用回溯函数,同时current_sum
加上这个数,start_index
保持不变(因为可以重复使用)。 - 如果当前组合的和加上这个数大于
target
,则“不使用”这个数,并停止遍历更大的数,因为数组是排序过的,更大的数只会使和更大。 - 每次从回溯函数返回时,需要将最后一个添加到组合中的数移除,以便尝试其他可能的数。
- 如果当前组合的和加上这个数小于或等于
-
终止条件:如果当前组合的和等于
target
,则将其添加到结果列表中。如果current_sum
大于target
或者没有更多的数可以添加,回溯。 -
去重:由于结果要求是不同的组合,需要确保不会添加重复的组合。由于我们从
start_index
开始,并且每次递归时都不减少start_index
,所以同一层递归中不会有重复的组合。 -
初始化和调用:最后,初始化一个空的结果列表,调用回溯函数,并传入初始参数。
通过以上步骤,你可以找到所有和为 target
的组合。
Golang 版本
package main
import "sort"
func combinationSum(candidates []int, target int) [][]int {
sort.Ints(candidates) // 可选的排序步骤
var res [][]int
var temp []int
// 定义回溯函数
var backtrack func(start int, target int)
backtrack = func(start int, target int) {
// 终止条件
if target == 0 {
// 添加当前组合的副本(因为temp后面会变)
res = append(res, append([]int(nil), temp...))
return
}
for i := start; i < len(candidates); i++ {
// 如果当前数大于target,则后面的数也不用尝试了
if candidates[i] > target {
break
}
// 选择当前数
temp = append(temp, candidates[i])
// 递归调用,由于可以重复使用,start仍然是i
backtrack(i, target-candidates[i])
// 撤销选择
temp = temp[:len(temp)-1]
}
}
// 调用回溯函数
backtrack(0, target)
return res
}
func main() {
// 示例
candidates := []int{2, 3, 6, 7}
target := 7
combinations := combinationSum(candidates, target)
// 输出结果
fmt.Println(combinations)
}
这段代码定义了一个 combinationSum
函数,它接受一个整数数组 candidates
和一个目标整数 target
,返回一个二维整数数组,其中包含所有加和为 target
的组合。在 main
函数中,我们给出了一个示例输入,并调用 combinationSum
函数,然后打印出结果。
C++版本
#include <vector>
#include <algorithm>
#include <iostream>
class Solution {
public:
void backtrack(std::vector<int>& candidates, int target, std::vector<std::vector<int>>& res, std::vector<int>& combination, int start) {
if (target == 0) {
res.push_back(combination);
return;
}
for (int i = start; i < candidates.size(); ++i) {
if (candidates[i] > target) // 如果当前数大于target,则后面的数也不用尝试了
break;
combination.push_back(candidates[i]);
backtrack(candidates, target - candidates[i], res, combination, i); // 因为可以重复使用,所以传入的是i而不是i+1
combination.pop_back(); // 撤销选择
}
}
std::vector<std::vector<int>> combinationSum(std::vector<int>& candidates, int target) {
std::vector<std::vector<int>> res;
std::vector<int> combination;
std::sort(candidates.begin(), candidates.end()); // 可选的排序步骤
backtrack(candidates, target, res, combination, 0);
return res;
}
};
int main() {
Solution solution;
std::vector<int> candidates = {2, 3, 6, 7};
int target = 7;
std::vector<std::vector<int>> combinations = solution.combinationSum(candidates, target);
// 打印结果
for (const auto& combination : combinations) {
std::cout << '[';
for (int i = 0; i < combination.size(); ++i) {
std::cout << combination[i];
if (i < combination.size() - 1) std::cout << ", ";
}
std::cout << ']' << std::endl;
}
return 0;
}
这段代码定义了一个 Solution
类,其中包含了 combinationSum
方法,用于找出所有和为 target
的组合。backtrack
是一个递归函数,用于执行回溯算法。在 main
函数中,我们创建了一个 Solution
对象,并给出了一个示例输入,然后调用 combinationSum
方法,并打印出结果。
Python 版本
from typing import List
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
def backtrack(start: int, target: int, path: List[int]):
if target == 0:
res.append(path)
return
for i in range(start, len(candidates)):
if candidates[i] > target:
break
backtrack(i, target - candidates[i], path + [candidates[i]])
candidates.sort() # 可选的排序步骤
res = []
backtrack(0, target, [])
return res
# 示例
if __name__ == '__main__':
candidates = [2, 3, 6, 7]
target = 7
solution = Solution()
combinations = solution.combinationSum(candidates, target)
print(combinations)
这段代码定义了一个 Solution
类,其中包含了 combinationSum
方法,用于找出所有和为 target
的组合。backtrack
是一个内部函数,用于执行回溯算法。在 if __name__ == '__main__':
部分,我们给出了一个示例输入,然后调用 combinationSum
方法,并打印出结果。
Java 版本
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates); // 可选的排序步骤
List<List<Integer>> results = new ArrayList<>();
backtrack(candidates, target, results, new ArrayList<>(), 0);
return results;
}
private void backtrack(int[] candidates, int target, List<List<Integer>> results, List<Integer> current, int start) {
if (target == 0) {
results.add(new ArrayList<>(current));
return;
}
for (int i = start; i < candidates.length; i++) {
if (candidates[i] > target) {
break;
}
current.add(candidates[i]);
backtrack(candidates, target - candidates[i], results, current, i); // 因为可以重复使用,所以传入的是i而不是i+1
current.remove(current.size() - 1); // 撤销选择
}
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] candidates = {2, 3, 6, 7};
int target = 7;
List<List<Integer>> combinations = solution.combinationSum(candidates, target);
for (List<Integer> combination : combinations) {
System.out.println(combination);
}
}
}
这段代码定义了一个 Solution
类,其中包含了 combinationSum
方法,用于找出所有和为 target
的组合。backtrack
是一个私有辅助方法,用于执行回溯算法。在 main
方法中,我们创建了一个 Solution
对象,并给出了一个示例输入,然后调用 combinationSum
方法,并打印出结果。
Rust 版本
use std::vec::Vec; fn combination_sum(candidates: Vec<i32>, target: i32) -> Vec<Vec<i32>> { let mut results = Vec::new(); let mut combination = Vec::new(); backtrack(&candidates, target, &mut results, &mut combination, 0); results } fn backtrack( candidates: &Vec<i32>, target: i32, results: &mut Vec<Vec<i32>>, combination: &mut Vec<i32>, start: usize, ) { if target == 0 { results.push(combination.clone()); return; } for i in start..candidates.len() { if candidates[i] > target { break; // Prune the recursion tree } combination.push(candidates[i]); backtrack(candidates, target - candidates[i], results, combination, i); // Not i + 1 because we can reuse same elements combination.pop(); // Remove last element before moving to next } } fn main() { let candidates = vec![2, 3, 6, 7]; let target = 7; let combinations = combination_sum(candidates, target); for combination in combinations { println!("{:?}", combination); } }
这段代码定义了一个 combination_sum
函数,它接受一个整数向量 candidates
和一个目标整数 target
,返回一个二维整数向量,其中包含所有加和为 target
的组合。backtrack
是一个辅助函数,用于执行回溯算法。在 main
函数中,我们给出了一个示例输入,并调用 combination_sum
函数,然后打印出结果。
总结
上述解法采用了回溯算法来解决组合总和问题。这个问题要求找出所有可能的组合,这些组合中的数字来自于一个候选数组,且它们的和等于一个目标值。在这些组合中,每个数字可以重复使用多次。
解法的关键步骤如下:
-
排序(可选):首先对候选数组进行排序,这样可以在回溯过程中更快地剪枝,因为一旦当前的数字加和已经超过目标值,就可以停止尝试更大的数字。
-
回溯函数:定义一个回溯函数
backtrack
,它将递归地尝试所有可能的组合。这个函数接受当前的组合combination
,当前的目标值target
,以及当前考虑的候选数字的起始索引start
。 -
递归终止条件:如果当前的目标值减到了 0,说明找到了一个有效的组合,将其添加到结果集中。
-
循环和递归:从
start
索引开始,遍历候选数组。对于每个数字,如果它不大于当前的目标值,就将它添加到当前组合中,并递归调用backtrack
函数,此时目标值减去当前数字的值,start
索引不变。 -
回溯:在每次递归调用之后,需要将最后一个加入到当前组合中的数字移除,这样才能回到之前的状态,尝试其他可能的数字。
-
结果返回:所有递归完成后,返回结果集,它包含了所有可能的组合。
这种解法在不同的编程语言中都可以实现,上面提供了 C++, Java, Python 和 Rust 版本的代码实现。每种语言的实现都遵循了相同的逻辑结构,只是在语法和一些数据结构的使用上有所不同。
括号生成
题目要求
设计一个函数,该函数接收一个整数 n 作为参数,这个整数代表括号对的数量。函数的目标是生成一个字符串列表,其中包含所有可能的、有效的括号组合。所谓“有效的括号组合”是指每一个组合中的括号都能正确闭合,没有任何多余或缺少的括号。
解题思路
生成有效括号组合的问题可以通过递归方法来解决。递归的基本思想是,我们在生成字符串的过程中,保持跟踪左括号和右括号的数量,并且确保在任何时刻插入括号的过程中,左括号的数量不小于右括号的数量(这样才能保证括号的有效性)。
以下是解题的步骤:
-
初始化:创建一个空列表来存储所有有效的括号组合。
-
递归函数设计:设计一个递归函数,该函数接收以下参数:
- 当前构造中的字符串
current
- 已使用的左括号数量
left
- 已使用的右括号数量
right
- 总的括号对数
n
- 当前构造中的字符串
-
递归终止条件:当
current
字符串的长度达到2 * n
时,说明已经构造了一个有效的括号组合,将其添加到列表中。 -
递归过程:
- 如果
left
的数量小于n
,则可以添加一个左括号'('
并递归调用函数,因为添加左括号总是安全的。 - 如果
right
的数量小于left
,则可以添加一个右括号')'
并递归调用函数,因为只有当右括号的数量少于左括号的数量时,添加右括号才是有效的。
- 如果
-
回溯:在每一步递归调用之后,需要回溯以撤销当前步骤的选择,继续尝试其他可能的路径。
-
返回结果:当递归完成后,返回存储所有有效括号组合的列表。
通过这种方式,我们可以确保生成的每个括号组合都是有效的,并且能够覆盖所有可能的组合。
Golang 版本
package main
import "fmt"
// generateParenthesis 生成所有有效的括号组合
func generateParenthesis(n int) []string {
var result []string
backtrack(&result, "", 0, 0, n)
return result
}
// backtrack 是一个递归辅助函数,用于生成括号
func backtrack(result *[]string, current string, open, close, max int) {
if len(current) == max*2 {
*result = append(*result, current)
return
}
if open < max {
backtrack(result, current+"(", open+1, close, max)
}
if close < open {
backtrack(result, current+")", open, close+1, max)
}
}
func main() {
n := 3
combinations := generateParenthesis(n)
for _, combination := range combinations {
fmt.Println(combination)
}
}
这段代码定义了一个 generateParenthesis
函数,它接收一个整数 n
并返回一个字符串切片,包含所有可能的有效括号组合。backtrack
函数是一个递归函数,用于构建括号组合。主函数 main
展示了如何调用 generateParenthesis
函数并打印结果。
C++版本
#include <iostream>
#include <vector>
#include <string>
class Solution {
public:
std::vector<std::string> generateParenthesis(int n) {
std::vector<std::string> result;
generate(result, "", n, n);
return result;
}
private:
void generate(std::vector<std::string> &result, std::string current, int open, int close) {
if (open == 0 && close == 0) {
result.push_back(current);
return;
}
if (open > 0) {
generate(result, current + "(", open - 1, close);
}
if (close > open) {
generate(result, current + ")", open, close - 1);
}
}
};
int main() {
Solution solution;
int n = 3;
std::vector<std::string> combinations = solution.generateParenthesis(n);
for (const std::string &combination : combinations) {
std::cout << combination << std::endl;
}
return 0;
}
这段 C++代码定义了一个 Solution
类,其中包含了 generateParenthesis
方法,用于生成所有有效的括号组合。generate
是一个私有辅助方法,用于递归生成括号组合。main
函数展示了如何创建 Solution
类的实例,调用 generateParenthesis
方法,并打印出所有的括号组合。
Python 版本
def generate_parenthesis(n):
def backtrack(s='', left=0, right=0):
if len(s) == 2 * n:
result.append(s)
return
if left < n:
backtrack(s + '(', left + 1, right)
if right < left:
backtrack(s + ')', left, right + 1)
result = []
backtrack()
return result
# 示例
n = 3
print(generate_parenthesis(n))
这段 Python 代码定义了一个 generate_parenthesis
函数,它接收一个整数 n
并返回一个列表,包含所有可能的有效括号组合。内部定义了一个 backtrack
辅助函数,用于递归地构建括号组合。最后,代码中包含了一个示例调用和打印结果的过程。
Java 版本
import java.util.ArrayList;
import java.util.List;
public class Solution {
public List<String> generateParenthesis(int n) {
List<String> result = new ArrayList<>();
backtrack(result, "", 0, 0, n);
return result;
}
private void backtrack(List<String> result, String current, int open, int close, int max) {
if (current.length() == max * 2) {
result.add(current);
return;
}
if (open < max) {
backtrack(result, current + "(", open + 1, close, max);
}
if (close < open) {
backtrack(result, current + ")", open, close + 1, max);
}
}
public static void main(String[] args) {
Solution solution = new Solution();
int n = 3;
List<String> combinations = solution.generateParenthesis(n);
for (String combination : combinations) {
System.out.println(combination);
}
}
}
这段 Java 代码定义了一个 Solution
类,其中包含了 generateParenthesis
方法,用于生成所有有效的括号组合。backtrack
是一个私有辅助方法,用于递归生成括号组合。main
方法展示了如何创建 Solution
类的实例,调用 generateParenthesis
方法,并打印出所有的括号组合。
Rust 版本
fn generate_parenthesis(n: i32) -> Vec<String> { fn backtrack( current: String, open: i32, close: i32, n: i32, result: &mut Vec<String>, ) { if current.len() as i32 == n * 2 { result.push(current); return; } if open < n { backtrack(current.clone() + "(", open + 1, close, n, result); } if close < open { backtrack(current.clone() + ")", open, close + 1, n, result); } } let mut result = Vec::new(); backtrack(String::new(), 0, 0, n, &mut result); result } fn main() { let n = 3; let combinations = generate_parenthesis(n); for combination in combinations { println!("{}", combination); } }
这段 Rust 代码定义了一个 generate_parenthesis
函数,它接收一个整数 n
并返回一个包含所有可能的有效括号组合的 Vec<String>
。内部定义了一个 backtrack
辅助函数,用于递归地构建括号组合。main
函数展示了如何调用 generate_parenthesis
函数并打印出所有的括号组合。
总结
生成所有可能的有效括号组合的问题可以通过递归回溯算法来解决。无论是在 Golang、C++、Python 还是 Rust 中,核心算法的逻辑是相同的,主要步骤如下:
-
定义一个递归函数
backtrack
,它接受当前构建的字符串current
,已放置的左括号数量open
,已放置的右括号数量close
,以及括号对的总数n
。 -
递归的终止条件是
current
字符串的长度等于n * 2
,这时将current
添加到结果集中。 -
在每一步递归中,可以选择添加一个左括号或一个右括号,但需要满足以下条件:
- 左括号的数量不能超过
n
。 - 右括号的数量不能超过左括号的数量,以确保括号的有效性。
- 左括号的数量不能超过
-
如果左括号的数量小于
n
,则添加一个左括号,并递归调用backtrack
。 -
如果右括号的数量小于左括号的数量,则添加一个右括号,并递归调用
backtrack
。 -
通过递归回溯,可以探索所有可能的括号组合,并最终生成一个包含所有有效组合的列表。
在实现上,每种语言都有其特定的语法和数据结构,但整体算法框架保持一致。例如,在 Rust 中使用 String
和 Vec<String>
,在 Python 中使用字符串和列表,而在 C++ 和 Java 中则使用 std::string
和 std::vector<std::string>
或 String
和 List<String>
。主函数通常用于演示如何调用生成括号组合的函数,并打印结果。
单词搜索
题目要求
给定一个 m x n 的二维字符网格 board
和一个字符串 word
。需要编写一个算法来判断 word
是否可以在网格中找到。在网格中寻找 word
时,可以按照字母顺序在相邻的单元格中移动来构建单词。这里的“相邻”单元格指的是上下左右四个方向与当前单元格相邻的单元格。在搜索过程中,同一个单元格中的字母不可以被重复使用。如果能够在网格中找到这样的单词路径,则返回 true
;如果找不到,则返回 false
。
解题思路
解决这个问题的一种方法是使用回溯算法。回溯算法是一种通过试错来寻找问题解决方法的算法。如果它发现已经选择的路径不可能是正确的解决方案,它将回退到上一个步骤,然后尝试其他的路径。对于这个问题,我们可以通过以下步骤来应用回溯算法:
-
从二维网格的每一个单元格开始,尝试寻找单词
word
的第一个字母。每次递归尝试都应该标记当前单元格的使用状态,以避免重复使用。 -
当找到第一个字母后,从当前单元格出发,递归地在所有四个相邻的单元格中寻找单词的下一个字母。
-
对于每个相邻的单元格,检查它是否未被访问过,并且其值是否与单词
word
的下一个字母相匹配。如果匹配,则递归地继续搜索下一个字母。 -
如果在任何递归步骤中,单词
word
的所有字母都被成功匹配,那么单词存在于网格中,返回true
。 -
如果当前单元格的字母与单词的相应字母不匹配,或者所有相邻单元格都已经被访问过,那么回溯到上一个单元格,撤销当前单元格的访问状态,并尝试其他路径。
-
如果所有的单元格都被尝试过后,仍然没有找到完整的单词
word
,则说明单词不存在于网格中,返回false
。 -
遍历完所有可能的起始位置后,如果没有任何一条路径能够匹配整个单词,则最终返回
false
。
这个算法的关键是要有一个有效的方法来标记已经访问过的单元格,并在回溯时取消这些标记。通常可以通过修改原网格中的内容来临时标记已访问的单元格,或者使用一个额外的同等大小的布尔网格来跟踪访问状态。
Golang 版本
func exist(board [][]byte, word string) bool {
rows, cols := len(board), len(board[0])
visited := make([][]bool, rows)
for i := range visited {
visited[i] = make([]bool, cols)
}
var backtrack func(row, col, index int) bool
backtrack = func(row, col, index int) bool {
// Check if we have found the word
if index == len(word) {
return true
}
// Check the boundaries and if the cell is already visited or not the right letter
if row < 0 || row >= rows || col < 0 || col >= cols || visited[row][col] || board[row][col] != word[index] {
return false
}
// Mark this cell as visited
visited[row][col] = true
// Explore all possible directions: right, left, down, up
if backtrack(row, col+1, index+1) || backtrack(row, col-1, index+1) ||
backtrack(row+1, col, index+1) || backtrack(row-1, col, index+1) {
return true
}
// Unmark this cell as visited (backtrack)
visited[row][col] = false
return false
}
// Start from each cell
for i := 0; i < rows; i++ {
for j := 0; j < cols; j++ {
if backtrack(i, j, 0) {
return true
}
}
}
return false
}
这段代码定义了一个名为 exist
的函数,它接受一个字符类型的二维切片 board
和一个字符串 word
作为参数。函数首先初始化一个 visited
二维切片来跟踪哪些单元格已经被访问过。然后定义了一个名为 backtrack
的递归辅助函数,用于执行回溯搜索。backtrack
函数会检查当前路径是否已经找到了单词,是否越界,是否已经访问过,或者是否不匹配当前需要的字母。如果当前单元格有效,它会继续探索所有可能的方向。如果在任何方向上成功找到了单词,它会返回 true
。如果当前路径不可行,它会回溯并尝试其他路径。最后,exist
函数遍历网格中的每个单元格,使用 backtrack
函数来寻找单词。如果在任何起点找到了单词,它会返回 true
,否则在遍历完所有单元格后返回 false
。
C++版本
#include <vector>
#include <string>
class Solution {
public:
bool exist(std::vector<std::vector<char>>& board, std::string word) {
int rows = board.size();
int cols = board[0].size();
std::vector<std::vector<bool>> visited(rows, std::vector<bool>(cols, false));
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < cols; ++j) {
if (search(board, word, 0, i, j, visited)) {
return true;
}
}
}
return false;
}
private:
bool search(std::vector<std::vector<char>>& board, const std::string& word, int index, int row, int col, std::vector<std::vector<bool>>& visited) {
if (index == word.size()) return true;
if (row < 0 || row >= board.size() || col < 0 || col >= board[0].size()) return false;
if (visited[row][col] || board[row][col] != word[index]) return false;
visited[row][col] = true;
bool found = search(board, word, index + 1, row + 1, col, visited) ||
search(board, word, index + 1, row - 1, col, visited) ||
search(board, word, index + 1, row, col + 1, visited) ||
search(board, word, index + 1, row, col - 1, visited);
visited[row][col] = false;
return found;
}
};
这段 C++代码定义了一个名为 Solution
的类,其中包含了解决问题的 exist
方法。这个方法接受一个字符类型的二维向量 board
和一个字符串 word
作为参数。它使用一个名为 visited
的二维布尔向量来跟踪哪些单元格已经被访问过。
exist
方法遍历网格中的每个单元格,并对每个单元格调用私有辅助方法 search
。search
方法是一个递归函数,它检查当前单元格是否是单词的一部分,并尝试在所有四个方向上继续搜索单词的下一个字符。如果找到了单词,search
方法会返回 true
,否则在尝试了所有可能的路径后返回 false
。
如果在网格中的任何位置都找不到单词,exist
方法最终返回 false
。如果找到了单词,它会在第一次找到时立即返回 true
。
Python 版本
class Solution:
def exist(self, board, word):
rows, cols = len(board), len(board[0])
visited = [[False] * cols for _ in range(rows)]
def backtrack(row, col, index):
if index == len(word):
return True
if row < 0 or row == rows or col < 0 or col == cols or visited[row][col] or board[row][col] != word[index]:
return False
visited[row][col] = True
# Explore in all directions: right, left, down, up
if (backtrack(row, col + 1, index + 1) or
backtrack(row, col - 1, index + 1) or
backtrack(row + 1, col, index + 1) or
backtrack(row - 1, col, index + 1)):
return True
visited[row][col] = False # Unmark this cell for backtracking
return False
for i in range(rows):
for j in range(cols):
if backtrack(i, j, 0): # Start the search from (i, j)
return True
return False
这段 Python 代码定义了一个名为 Solution
的类,其中包含了解决问题的 exist
方法。这个方法接受一个字符类型的二维列表 board
和一个字符串 word
作为参数。它使用一个名为 visited
的二维列表来跟踪哪些单元格已经被访问过。
exist
方法遍历网格中的每个单元格,并对每个单元格调用一个名为 backtrack
的辅助函数。backtrack
是一个递归函数,它检查当前单元格是否是单词的一部分,并尝试在所有四个方向上继续搜索单词的下一个字符。如果找到了单词,backtrack
函数会返回 True
,否则在尝试了所有可能的路径后返回 False
。
如果在网格中的任何位置都找不到单词,exist
方法最终返回 False
。如果找到了单词,它会在第一次找到时立即返回 True
。
Java 版本
public class Solution {
public boolean exist(char[][] board, String word) {
int rows = board.length;
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++) {
if (search(board, word, i, j, 0, visited)) {
return true;
}
}
}
return false;
}
private boolean search(char[][] board, String word, int row, int col, int index, boolean[][] visited) {
if (index == word.length()) {
return true;
}
if (row < 0 || row >= board.length || col < 0 || col >= board[0].length) {
return false;
}
if (visited[row][col] || board[row][col] != word.charAt(index)) {
return false;
}
visited[row][col] = true;
if (search(board, word, row - 1, col, index + 1, visited) ||
search(board, word, row + 1, col, index + 1, visited) ||
search(board, word, row, col - 1, index + 1, visited) ||
search(board, word, row, col + 1, index + 1, visited)) {
return true;
}
visited[row][col] = false; // backtrack
return false;
}
}
这段 Java 代码定义了一个名为 Solution
的类,其中包含了解决问题的 exist
方法。这个方法接受一个字符类型的二维数组 board
和一个字符串 word
作为参数。它使用一个名为 visited
的二维布尔数组来跟踪哪些单元格已经被访问过。
exist
方法遍历网格中的每个单元格,并对每个单元格调用一个名为 search
的私有辅助方法。search
是一个递归函数,它检查当前单元格是否是单词的一部分,并尝试在所有四个方向上继续搜索单词的下一个字符。如果找到了单词,search
方法会返回 true
,否则在尝试了所有可能的路径后返回 false
。
如果在网格中的任何位置都找不到单词,exist
方法最终返回 false
。如果找到了单词,它会在第一次找到时立即返回 true
。在回溯过程中,visited
数组的相应位置会被重置,以便可以重新使用该单元格。
Rust 版本
#![allow(unused)] fn main() { impl Solution { pub fn exist(board: Vec<Vec<char>>, word: String) -> bool { let rows = board.len(); let cols = board[0].len(); let word_chars: Vec<char> = word.chars().collect(); let mut visited = vec![vec![false; cols]; rows]; for i in 0..rows { for j in 0..cols { if Self::dfs(&board, &word_chars, i, j, 0, &mut visited) { return true; } } } false } fn dfs(board: &Vec<Vec<char>>, word: &Vec<char>, row: usize, col: usize, index: usize, visited: &mut Vec<Vec<bool>>) -> bool { if index == word.len() { return true; } if row >= board.len() || col >= board[0].len() || visited[row][col] || board[row][col] != word[index] { return false; } visited[row][col] = true; let row_offsets = [1, 0, -1, 0]; let col_offsets = [0, 1, 0, -1]; for k in 0..4 { let new_row = row as i32 + row_offsets[k]; let new_col = col as i32 + col_offsets[k]; if new_row >= 0 && new_col >= 0 && Self::dfs(board, word, new_row as usize, new_col as usize, index + 1, visited) { return true; } } visited[row][col] = false; false } } }
这段 Rust 代码定义了一个名为 Solution
的结构体,其中包含了解决问题的 exist
方法。这个方法接受一个字符类型的二维向量 board
和一个字符串 word
作为参数,并返回一个布尔值。它使用一个名为 visited
的二维向量来跟踪哪些单元格已经被访问过。
exist
方法遍历网格中的每个单元格,并对每个单元格调用一个名为 dfs
的私有辅助方法。dfs
是一个递归函数,它检查当前单元格是否是单词的一部分,并尝试在所有四个方向上继续搜索单词的下一个字符。如果找到了单词,dfs
方法会返回 true
,否则在尝试了所有可能的路径后返回 false
。
如果在网格中的任何位置都找不到单词,exist
方法最终返回 false
。如果找到了单词,它会在第一次找到时立即返回 true
。在回溯过程中,visited
向量的相应位置会被重置,以便可以重新使用该单元格。
请注意,Rust 中的索引必须是非负数,因此在处理行和列的偏移时,我们需要将它们从 i32
转换为 usize
,同时确保它们不会变成负数。
总结
上面的 Rust 代码实现了一个经典的回溯算法,用于在一个给定的字符网格中搜索一个指定的单词。这个算法的核心是深度优先搜索(DFS),它尝试从网格中的每个位置开始,探索所有可能的路径以找到单词。以下是解法的关键点:
-
二维访问数组:使用一个二维布尔数组
visited
来跟踪每个单元格是否已经被访问过,以防止重复访问。 -
递归 DFS 函数:
dfs
函数是一个递归函数,它接受当前位置、单词、当前索引和访问数组作为参数。它会检查当前位置是否可以作为单词的一部分,并递归地在四个方向(上、下、左、右)探索下一个字符。 -
边界和匹配条件检查:在每次递归调用中,都会检查当前位置是否越界,当前字符是否与单词中相应位置的字符匹配,以及该位置是否已经被访问过。
-
回溯:在探索完一个位置的所有可能方向后,如果没有找到正确的路径,算法会回溯,即撤销当前位置的访问状态,然后返回到上一个位置继续搜索。
-
成功条件:如果当前索引达到了单词的长度,意味着单词已经被完全找到,函数返回
true
。 -
循环启动 DFS:在
exist
函数中,通过双层循环遍历网格中的每个单元格,并从每个单元格启动 DFS 搜索。如果任何一个单元格的 DFS 返回true
,则整个函数返回true
。 -
返回结果:如果在整个网格中都没有找到单词,则函数最终返回
false
。
这种解法在解决二维网格搜索问题时非常有效,尤其是在需要验证多个可能性并且每个选择都依赖于前一个选择的情况下。回溯算法通过尝试所有可能的解决方案来找到问题的答案,如果当前尝试失败了,它会撤销最后的选择并尝试另一种可能的路径。
分割回文串
题目要求
给定一个字符串 s,要求编写一个算法,将字符串 s 分割成若干子串,使得每个子串都是回文串。算法需要返回字符串 s 所有可能的分割方案。
解题思路
解决这个问题可以采用回溯算法的思想。回溯算法是一种通过探索所有可能的候选解来找出所有解的算法。如果候选解被确认不是一个有效的解,或者不可能产生一个有效的解,那么回溯算法会丢弃该候选解。这个过程重复进行,直到找到所有可能的解。
对于这个问题,我们可以按照以下步骤来设计我们的回溯算法:
- 从字符串的开头开始,尝试所有可能的分割方式。
- 对于每一种分割方式,检查分割出来的前缀字符串是否是回文串。
- 如果前缀字符串是回文串,则将其加入到当前分割方案中,并对剩余的字符串继续进行分割。
- 如果前缀字符串不是回文串,则跳过这种分割方式,尝试下一种分割方式。
- 当到达字符串的末尾时,如果整个字符串都被分割成了回文串,则将当前分割方案加入到结果列表中。
- 在整个过程中,我们需要用一个变量来保存当前的分割方案,以便在达到字符串末尾时可以将其加入到结果列表中。
- 回溯的过程中,每次从结果中移除最后一个加入的回文串,以尝试新的分割方案。
通过这样的回溯过程,我们可以找到所有可能的将字符串分割成回文串的方案。
Golang 版本
package main
import "fmt"
func partition(s string) [][]string {
var result [][]string
var path []string
backtrack(s, path, &result)
return result
}
func backtrack(s string, path []string, result *[][]string) {
if len(s) == 0 {
temp := make([]string, len(path))
copy(temp, path)
*result = append(*result, temp)
return
}
for i := 1; i <= len(s); i++ {
if isPalindrome(s[:i]) {
path = append(path, s[:i])
backtrack(s[i:], path, result)
path = path[:len(path)-1]
}
}
}
func isPalindrome(s string) bool {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
if s[i] != s[j] {
return false
}
}
return true
}
func main() {
s := "aab"
result := partition(s)
fmt.Println(result)
}
这段代码定义了一个partition
函数,它接受一个字符串s
并返回一个二维字符串切片,其中包含了所有可能的将s
分割成回文串的方案。backtrack
是一个递归函数,用于执行回溯算法。isPalindrome
函数用于检查一个字符串是否是回文串。在main
函数中,我们调用partition
函数并打印出结果。
C++版本
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class Solution {
public:
vector<vector<string>> partition(string s) {
vector<vector<string>> result;
vector<string> path;
backtrack(s, 0, path, result);
return result;
}
private:
void backtrack(const string &s, int start, vector<string> &path, vector<vector<string>> &result) {
if (start == s.size()) {
result.push_back(path);
return;
}
for (int end = start; end < s.size(); ++end) {
if (isPalindrome(s, start, end)) {
path.push_back(s.substr(start, end - start + 1));
backtrack(s, end + 1, path, result);
path.pop_back();
}
}
}
bool isPalindrome(const string &s, int start, int end) {
while (start < end) {
if (s[start++] != s[end--]) {
return false;
}
}
return true;
}
};
int main() {
Solution solution;
string s = "aab";
vector<vector<string>> result = solution.partition(s);
for (const auto &partition : result) {
for (const auto &palindrome : partition) {
cout << palindrome << " ";
}
cout << endl;
}
return 0;
}
这段 C++代码定义了一个Solution
类,其中包含了解决问题的partition
方法。partition
方法接受一个字符串s
并返回一个二维字符串向量,其中包含了所有可能的将s
分割成回文串的方案。backtrack
是一个私有方法,用于执行回溯算法。isPalindrome
方法用于检查字符串的一个子串是否是回文串。在main
函数中,我们创建了一个Solution
对象,调用partition
方法并打印出结果。
Python 版本
def partition(s):
def backtrack(start, path):
if start == len(s):
result.append(path[:])
return
for end in range(start, len(s)):
if s[start:end+1] == s[start:end+1][::-1]:
path.append(s[start:end+1])
backtrack(end+1, path)
path.pop()
result = []
backtrack(0, [])
return result
# 测试代码
s = "aab"
print(partition(s))
这段 Python 代码定义了一个partition
函数,它接受一个字符串s
并返回一个列表,其中包含了所有可能的将s
分割成回文串的方案。backtrack
是一个嵌套函数,用于执行回溯算法。它检查从当前start
位置到任意end
位置的子串是否为回文串,如果是,则将其添加到当前路径path
中,并继续递归。当start
等于字符串长度时,将当前路径添加到结果列表中。在测试代码中,我们调用partition
函数并打印出结果。
Java 版本
import java.util.ArrayList;
import java.util.List;
public class Solution {
public List<List<String>> partition(String s) {
List<List<String>> result = new ArrayList<>();
backtrack(s, 0, new ArrayList<>(), result);
return result;
}
private void backtrack(String s, int start, List<String> path, List<List<String>> result) {
if (start == s.length()) {
result.add(new ArrayList<>(path));
return;
}
for (int end = start; end < s.length(); end++) {
if (isPalindrome(s, start, end)) {
path.add(s.substring(start, end + 1));
backtrack(s, end + 1, path, result);
path.remove(path.size() - 1);
}
}
}
private boolean isPalindrome(String s, int low, int high) {
while (low < high) {
if (s.charAt(low++) != s.charAt(high--)) {
return false;
}
}
return true;
}
public static void main(String[] args) {
Solution solution = new Solution();
String s = "aab";
List<List<String>> result = solution.partition(s);
for (List<String> partition : result) {
System.out.println(partition);
}
}
}
这段 Java 代码定义了一个Solution
类,其中包含了解决问题的partition
方法。partition
方法接受一个字符串s
并返回一个列表,其中包含了所有可能的将s
分割成回文串的方案。backtrack
是一个私有辅助方法,用于执行回溯算法。isPalindrome
方法用于检查字符串的一个子串是否是回文串。在main
方法中,我们创建了一个Solution
对象,调用partition
方法并打印出结果。
Rust 版本
fn partition(s: String) -> Vec<Vec<String>> { fn is_palindrome(s: &str) -> bool { s.chars().eq(s.chars().rev()) } fn backtrack(s: &str, start: usize, path: &mut Vec<String>, result: &mut Vec<Vec<String>>) { if start == s.len() { result.push(path.clone()); return; } for end in start + 1..=s.len() { if is_palindrome(&s[start..end]) { path.push(s[start..end].to_string()); backtrack(s, end, path, result); path.pop(); } } } let mut result = Vec::new(); backtrack(&s, 0, &mut Vec::new(), &mut result); result } fn main() { let s = "aab".to_string(); let partitions = partition(s); for p in partitions { println!("{:?}", p); } }
这段 Rust 代码定义了一个partition
函数,它接受一个字符串s
并返回一个向量,其中包含了所有可能的将s
分割成回文串的方案。is_palindrome
是一个辅助函数,用于检查一个字符串片段是否是回文。backtrack
是一个递归函数,用于执行回溯算法。在main
函数中,我们调用partition
函数并打印出结果。
总结
上述代码提供了一个字符串分割问题的解决方案,目标是将给定的字符串s
分割成若干个回文子串。无论是哪种编程语言,核心算法都是基于回溯的方法。以下是解法的关键步骤:
-
回溯函数:定义一个回溯函数
backtrack
,它递归地尝试所有可能的分割方法,并将当前位置之后的子串分割成回文串。 -
终止条件:当回溯函数的起始位置达到字符串的末尾时,说明找到了一种分割方法,将当前路径添加到结果集中。
-
递归分割:从当前位置开始,逐步增加子串的长度,如果当前子串是回文,则将其添加到当前路径,并从子串的下一个位置继续递归。
-
回文判断:定义一个辅助函数
is_palindrome
,用于判断当前子串是否是回文。 -
路径和结果:使用一个动态数组(或向量)来存储当前的分割路径,使用另一个动态数组(或向量)来存储所有可能的分割结果。
-
主函数:在主函数中,初始化结果集,调用回溯函数开始递归,并最终返回或打印所有可能的分割方案。
不同编程语言的实现细节有所不同,但算法的核心思想是一致的。例如,在 Rust 中使用Vec
来存储路径和结果,在 Python 中使用列表,而在 C++和 Java 中则使用vector
和ArrayList
。此外,字符串的处理方式也根据语言特性有所不同,如在 Rust 中使用切片,在 Python 中使用切片操作,在 C++和 Java 中使用substr
方法。
N 皇后
题目要求
这个问题是著名的 n 皇后问题,要求我们在一个 n×n 的棋盘上放置 n 个皇后,并且保证这些皇后之间不能互相攻击。在国际象棋中,皇后可以攻击同一行、同一列或同一斜线上的其他棋子。因此,n 皇后问题的解决方案要求在棋盘上的每一行、每一列以及每条对角线上都最多只有一个皇后。
具体来说,需要编写一个算法,输入一个整数 n,输出所有符合条件的 n 皇后摆放方案。每个方案都需要用一个 n×n 的矩阵来表示,其中'Q'表示一个皇后的位置,'.'表示空位。
解题思路
解决 n 皇后问题的一个常见方法是使用回溯算法。回溯算法是一种通过试错来寻找所有解决方案的算法,当它发现已不满足求解条件时,它会返回(即“回溯”)并尝试另一种可能的解决方案,直到找到所有可能的解决方案。以下是解决 n 皇后问题的回溯算法的基本步骤:
- 从棋盘的第一行开始,尝试在每一列中放置一个皇后。
- 在放置每一个皇后之前,检查当前的列、行以及两个对角线上是否已经有皇后。如果已经有皇后,则当前位置不能放置皇后,需要尝试下一列。
- 如果找到了一个合法的位置,则放置皇后,并递归地在下一行中寻找合法的皇后位置。
- 如果在当前行找不到合法的位置放置皇后,说明前面的某一步选择错误,需要回溯到上一行,移动皇后的位置。
- 当成功放置了最后一行的皇后时,就找到了一个解决方案。记录下当前棋盘的状态,然后回溯,继续寻找下一个解决方案。
- 重复以上步骤,直到棋盘的所有行都尝试过,找出所有可能的解决方案。
为了有效地检查皇后的放置是否冲突,可以使用三个辅助数组来记录每一列、每个主对角线和每个副对角线上是否已经放置了皇后。主对角线的特点是行号减列号为常数,副对角线的特点是行号加列号为常数,利用这个特性可以快速检查对角线上是否有冲突。
通过这种方法,可以找到所有的 n 皇后问题的解决方案,并以所需的格式返回。
Golang 版本
package main
import (
"fmt"
"strings"
)
func solveNQueens(n int) [][]string {
results := [][]string{}
board := make([][]string, n)
for i := range board {
board[i] = make([]string, n)
for j := range board[i] {
board[i][j] = "."
}
}
columns := make([]bool, n)
diagonals1 := make([]bool, 2*n)
diagonals2 := make([]bool, 2*n)
backtrack(board, 0, n, &results, columns, diagonals1, diagonals2)
return results
}
func backtrack(board [][]string, row, n int, results *[][]string, columns, diagonals1, diagonals2 []bool) {
if row == n {
temp := make([]string, n)
for i := range board {
temp[i] = strings.Join(board[i], "")
}
*results = append(*results, temp)
return
}
for i := 0; i < n; i++ {
if columns[i] || diagonals1[row+i] || diagonals2[row-i+n] {
continue
}
board[row][i] = "Q"
columns[i] = true
diagonals1[row+i] = true
diagonals2[row-i+n] = true
backtrack(board, row+1, n, results, columns, diagonals1, diagonals2)
board[row][i] = "."
columns[i] = false
diagonals1[row+i] = false
diagonals2[row-i+n] = false
}
}
func main() {
n := 4 // or any n you want to solve for
solutions := solveNQueens(n)
for _, solution := range solutions {
for _, row := range solution {
fmt.Println(row)
}
fmt.Println()
}
}
这段代码定义了一个solveNQueens
函数,它接受一个整数n
作为参数,并返回一个二维字符串切片,其中包含所有可能的 n 皇后解决方案。主要的逻辑是通过回溯算法来实现的,其中backtrack
函数是递归调用的核心,用于尝试放置皇后并检查是否有冲突。程序的入口是main
函数,它调用solveNQueens
函数并打印出所有解决方案。
C++版本
#include <iostream>
#include <vector>
#include <string>
class Solution {
public:
std::vector<std::vector<std::string>> solveNQueens(int n) {
std::vector<std::vector<std::string>> solutions;
std::vector<std::string> board(n, std::string(n, '.'));
std::vector<bool> column(n, false), diag1(2 * n, false), diag2(2 * n, false);
backtrack(0, n, board, solutions, column, diag1, diag2);
return solutions;
}
private:
void backtrack(int row, int n, std::vector<std::string>& board,
std::vector<std::vector<std::string>>& solutions,
std::vector<bool>& column, std::vector<bool>& diag1,
std::vector<bool>& diag2) {
if (row == n) {
solutions.push_back(board);
return;
}
for (int col = 0; col < n; ++col) {
if (column[col] || diag1[row + col] || diag2[row - col + n]) continue;
board[row][col] = 'Q';
column[col] = diag1[row + col] = diag2[row - col + n] = true;
backtrack(row + 1, n, board, solutions, column, diag1, diag2);
board[row][col] = '.';
column[col] = diag1[row + col] = diag2[row - col + n] = false;
}
}
};
int main() {
Solution solution;
int n = 4; // or any n you want to solve for
std::vector<std::vector<std::string>> solutions = solution.solveNQueens(n);
for (const auto& solution : solutions) {
for (const auto& row : solution) {
std::cout << row << std::endl;
}
std::cout << std::endl;
}
return 0;
}
这段 C++代码定义了一个Solution
类,其中包含解决 n 皇后问题的solveNQueens
方法。该方法返回一个二维字符串向量,其中包含所有可能的解决方案。backtrack
是一个私有方法,用于递归地放置皇后并检查是否有冲突。main
函数中创建了一个Solution
对象,并调用solveNQueens
方法,然后打印出所有的解决方案。
Python 版本
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
def create_board(state):
board = []
for row in state:
board.append('.' * row + 'Q' + '.' * (n - row - 1))
return board
def backtrack(row):
if row == n:
board = create_board(state)
solutions.append(board)
return
for col in range(n):
if col in cols or (row + col) in diag1 or (row - col) in diag2:
continue
cols.add(col)
diag1.add(row + col)
diag2.add(row - col)
state.append(col)
backtrack(row + 1)
cols.remove(col)
diag1.remove(row + col)
diag2.remove(row - col)
state.pop()
solutions = []
state = []
cols = set()
diag1 = set()
diag2 = set()
backtrack(0)
return solutions
# Example usage:
# sol = Solution()
# print(sol.solveNQueens(4))
这段 Python 代码定义了一个Solution
类,其中包含解决 n 皇后问题的solveNQueens
方法。该方法使用回溯算法找出所有可能的解决方案,并返回一个列表,其中每个元素代表一个解决方案的棋盘布局。棋盘布局用字符串列表表示,其中'Q'代表皇后,'.'代表空位。create_board
函数用于根据当前状态生成棋盘布局。backtrack
函数是递归的核心,用于尝试在每一行放置皇后并检查是否有冲突。
Java 版本
import java.util.ArrayList;
import java.util.List;
public class Solution {
public List<List<String>> solveNQueens(int n) {
char[][] board = new char[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
board[i][j] = '.';
}
}
List<List<String>> solutions = new ArrayList<>();
boolean[] cols = new boolean[n]; // columns |
boolean[] d1 = new boolean[2 * n]; // diagonals \
boolean[] d2 = new boolean[2 * n]; // diagonals /
backtrack(0, n, board, solutions, cols, d1, d2);
return solutions;
}
private void backtrack(int row, int n, char[][] board, List<List<String>> solutions, boolean[] cols, boolean[] d1, boolean[] d2) {
if (row == n) {
solutions.add(createBoard(board));
return;
}
for (int col = 0; col < n; col++) {
int id1 = col - row + n;
int id2 = col + row;
if (cols[col] || d1[id1] || d2[id2]) continue;
cols[col] = true; d1[id1] = true; d2[id2] = true;
board[row][col] = 'Q';
backtrack(row + 1, n, board, solutions, cols, d1, d2);
board[row][col] = '.';
cols[col] = false; d1[id1] = false; d2[id2] = false;
}
}
private List<String> createBoard(char[][] board) {
List<String> res = new ArrayList<>();
for (char[] row : board) {
res.add(new String(row));
}
return res;
}
public static void main(String[] args) {
Solution solution = new Solution();
List<List<String>> solutions = solution.solveNQueens(4);
for (List<String> sol : solutions) {
for (String row : sol) {
System.out.println(row);
}
System.out.println();
}
}
}
这段 Java 代码定义了一个Solution
类,其中包含解决 n 皇后问题的solveNQueens
方法。该方法使用回溯算法找出所有可能的解决方案,并返回一个列表,其中每个元素代表一个解决方案的棋盘布局。棋盘布局用字符串列表表示,其中'Q'代表皇后,'.'代表空位。createBoard
方法用于从字符数组生成棋盘布局。backtrack
方法是递归的核心,用于尝试在每一行放置皇后并检查是否有冲突。main
方法用于演示如何调用solveNQueens
方法并打印出所有解决方案。
Rust 版本
fn solve_n_queens(n: i32) -> Vec<Vec<String>> { let mut solutions = Vec::new(); let mut board = vec![vec!['.'; n as usize]; n as usize]; let mut cols = vec![false; n as usize]; let mut diag1 = vec![false; (2 * n) as usize]; let mut diag2 = vec![false; (2 * n) as usize]; backtrack(0, n, &mut board, &mut solutions, &mut cols, &mut diag1, &mut diag2); solutions } fn backtrack(row: i32, n: i32, board: &mut Vec<Vec<char>>, solutions: &mut Vec<Vec<String>>, cols: &mut Vec<bool>, diag1: &mut Vec<bool>, diag2: &mut Vec<bool>) { if row == n { solutions.push(board.iter().map(|r| r.iter().collect()).collect()); return; } for col in 0..n { let d1 = (col - row + n) as usize; let d2 = (col + row) as usize; if cols[col as usize] || diag1[d1] || diag2[d2] { continue; } cols[col as usize] = true; diag1[d1] = true; diag2[d2] = true; board[row as usize][col as usize] = 'Q'; backtrack(row + 1, n, board, solutions, cols, diag1, diag2); board[row as usize][col as usize] = '.'; cols[col as usize] = false; diag1[d1] = false; diag2[d2] = false; } } fn main() { let solutions = solve_n_queens(4); for solution in solutions { for row in solution { println!("{}", row); } println!(); } }
这段 Rust 代码定义了一个解决 n 皇后问题的函数solve_n_queens
,它使用回溯算法找出所有可能的解决方案,并返回一个包含解决方案的向量。每个解决方案都是一个字符串向量,其中每个字符串代表棋盘的一行,'Q'代表皇后,'.'代表空位。backtrack
函数是递归的核心,用于尝试在每一行放置皇后并检查是否有冲突。main
函数用于演示如何调用solve_n_queens
函数并打印出所有解决方案。
总结
上述 Rust 代码实现了经典的 N 皇后问题的解决方案。N 皇后问题要求在一个 N×N 的棋盘上放置 N 个皇后,使得它们互不攻击,即任意两个皇后都不在同一行、同一列或同一对角线上。解决方案采用了回溯算法,这是一种深度优先搜索算法,用于穷举所有可能的解决方案并回退到上一个状态,以尝试不同的解决路径。
具体步骤如下:
- 初始化一个空棋盘,棋盘的大小为 N×N,所有位置都用'.'表示空位。
- 使用三个布尔向量来跟踪哪些列以及两个方向的对角线上已经放置了皇后,以避免冲突。
- 从第一行开始,尝试在每一列放置一个皇后,并递归地在下一行中寻找合适的位置。
- 如果当前位置放置皇后会导致冲突(即同列或对角线上已有皇后),则跳过当前列,尝试下一列。
- 如果找到一个合法的位置,就在当前位置放置皇后(用'Q'表示),并更新列和对角线的占用状态。
- 递归调用回溯函数,尝试在下一行放置皇后。
- 如果所有行都成功放置了皇后,将当前棋盘的解决方案添加到解决方案集合中。
- 在回溯到上一行时,清除当前位置的皇后,并重置列和对角线的占用状态,以便尝试新的位置。
最终,solve_n_queens
函数返回所有可能的棋盘配置,每个配置都是一个字符串向量,代表了一个有效的 N 皇后解决方案。main
函数则用于演示如何调用这个函数并打印出所有的解决方案。
二分查找
二分查找是一种在有序数组中查找特定元素的高效算法。其基本思想是将待搜索区间分成两半,然后根据中间元素与目标值的比较结果来决定是继续在左半部分搜索还是右半部分搜索。二分查找的时间复杂度为 O(log n),其中 n 是数组的长度。
二分查找的通用思路如下:
- 确定搜索区间:开始时通常是整个数组。
- 计算中间位置:取搜索区间的中间位置。
- 比较中间元素与目标值:
- 如果中间元素正好等于目标值,则查找成功。
- 如果中间元素小于目标值,则目标值在中间元素的右侧,调整搜索区间为中间位置的右侧部分。
- 如果中间元素大于目标值,则目标值在中间元素的左侧,调整搜索区间为中间位置的左侧部分。
- 重复步骤 2 和 3,直到找到目标值或搜索区间为空。
下面是一个使用 Go 语言实现的二分查找算法的例子:
package main
import "fmt"
// 二分查找函数
func binarySearch(arr []int, target int) int {
left, right := 0, len(arr)-1
for left <= right {
mid := left + (right-left)/2 // 防止溢出的写法
if arr[mid] == target {
return mid // 找到目标值,返回索引
} else if arr[mid] < target {
left = mid + 1 // 调整左边界
} else {
right = mid - 1 // 调整右边界
}
}
return -1 // 没有找到目标值,返回-1
}
func main() {
arr := []int{2, 3, 4, 10, 40}
target := 10
result := binarySearch(arr, target)
if result != -1 {
fmt.Printf("元素在数组中的索引为: %d\n", result)
} else {
fmt.Println("元素不在数组中")
}
}
在实际应用中,二分查找算法可能需要根据具体问题进行适当的修改,例如处理查找第一个或最后一个等于目标值的元素,或者处理查找第一个大于等于或小于等于目标值的元素等情况。但基本的算法框架是相同的,都是通过不断缩小搜索区间来提高查找效率。
搜索插入位置
题目要求
你需要解决的问题是在一个已排序的数组中查找一个目标值的索引位置。如果这个目标值在数组中存在,直接返回它的索引。如果不存在,需要返回这个目标值按照数组的排序规则应该被插入的位置。这个位置的确定是使得数组依然保持排序状态。
此外,题目要求你实现的算法必须具有 O(log n) 的时间复杂度,这意味着你不能使用简单的线性查找来遍历整个数组,因为线性查找的时间复杂度是 O(n)。相反,你应该考虑使用二分查找算法,因为二分查找的时间复杂度是 O(log n),符合题目要求。
解题思路
由于数组是有序的,我们可以使用二分查找算法来优化搜索过程。二分查找算法的基本思想是,每次取中间位置的元素与目标值进行比较,根据比较结果缩小搜索范围,直到找到目标值或确定目标值不存在。
具体步骤如下:
- 初始化两个指针,一个指向数组的起始位置
left
,另一个指向数组的结束位置right
。 - 在
left
<=right
的条件下进行循环:- 计算中间位置
mid
= (left
+right
) / 2。 - 如果
mid
位置的元素等于目标值,直接返回mid
作为答案。 - 如果
mid
位置的元素小于目标值,说明目标值应该在mid
右侧的子数组中,因此将left
更新为mid + 1
。 - 如果
mid
位置的元素大于目标值,说明目标值应该在mid
左侧的子数组中,因此将right
更新为mid - 1
。
- 计算中间位置
- 如果循环结束后,没有找到目标值,此时
left
指针的位置就是目标值应该插入的位置。因为在二分查找的最后一步,如果目标值不存在,left
指针总是指向第一个大于目标值的元素位置,或者是数组的末尾(如果目标值比所有元素都大的情况)。
通过以上步骤,你可以在 O(log n) 的时间复杂度内找到目标值的索引或者应该插入的位置。
Golang 版本
package main
import "fmt"
func searchInsert(nums []int, target int) int {
left, right := 0, len(nums)-1
for left <= right {
mid := left + (right-left)/2
if nums[mid] == target {
return mid
} else if nums[mid] < target {
left = mid + 1
} else {
right = mid - 1
}
}
return left
}
func main() {
nums := []int{1, 3, 5, 6}
target := 5
fmt.Println("Index of target is:", searchInsert(nums, target))
}
这段代码实现了题目要求的功能,使用了二分查找算法来找到目标值的索引或者应该插入的位置。在 main
函数中,你可以通过更改 nums
和 target
的值来测试不同的情况。
C++版本
#include <iostream>
#include <vector>
int searchInsert(std::vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return left;
}
int main() {
std::vector<int> nums = {1, 3, 5, 6};
int target = 5;
std::cout << "Index of target is: " << searchInsert(nums, target) << std::endl;
return 0;
}
这段 C++代码实现了与 Golang 版本相同的功能,使用二分查找算法来确定目标值的索引或者插入位置。在 main
函数中,可以通过修改 nums
和 target
的值来测试代码。
Python 版本
def search_insert(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return left
# 测试代码
nums = [1, 3, 5, 6]
target = 5
print("Index of target is:", search_insert(nums, target))
这段 Python 代码使用了二分查找算法来寻找目标值的索引或者插入位置。你可以通过更改 nums
和 target
的值来测试不同的输入情况。
Java 版本
public class SearchInsertPosition {
public static int searchInsert(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return left;
}
public static void main(String[] args) {
int[] nums = {1, 3, 5, 6};
int target = 5;
System.out.println("Index of target is: " + searchInsert(nums, target));
}
}
这段 Java 代码实现了题目要求的功能,使用二分查找算法来找到目标值的索引或者应该插入的位置。在 main
方法中,可以通过修改 nums
数组和 target
变量的值来测试不同的情况。
Rust 版本
fn search_insert(nums: Vec<i32>, target: i32) -> i32 { let (mut left, mut right) = (0, nums.len() as i32 - 1); while left <= right { let mid = left + (right - left) / 2; if nums[mid as usize] == target { return mid; } else if nums[mid as usize] < target { left = mid + 1; } else { right = mid - 1; } } left } fn main() { let nums = vec![1, 3, 5, 6]; let target = 5; println!("Index of target is: {}", search_insert(nums, target)); }
这段 Rust 代码实现了二分查找算法,用于在有序数组中查找目标值的索引或者目标值应该插入的位置。在 main
函数中,你可以通过更改 nums
向量和 target
的值来测试代码。
总结
上述解法都采用了二分查找算法来解决问题。二分查找是一种在有序数组中查找特定元素的高效算法,其时间复杂度为 O(log n)。算法的核心思想是将搜索区间分成两半,然后根据中间元素与目标值的比较结果来决定是继续在左侧区间搜索还是右侧区间搜索。
无论是 Golang、C++、Python 还是 Java 和 Rust 版本的代码,基本步骤如下:
- 初始化两个指针
left
和right
,分别指向数组的起始位置和结束位置。 - 在
left
小于等于right
的条件下进行循环:- 计算中间位置
mid
。 - 如果
mid
位置的元素等于目标值,则返回mid
作为索引。 - 如果
mid
位置的元素小于目标值,则将left
更新为mid + 1
。 - 如果
mid
位置的元素大于目标值,则将right
更新为mid - 1
。
- 计算中间位置
- 如果循环结束还没有找到目标值,则
left
指针的位置表示目标值应该插入的位置。
这种方法不仅可以找到目标值的索引,如果目标值不存在于数组中,还能找到它按顺序应该被插入的位置。
搜索二维矩阵
题目要求
你需要处理的是一个特殊的 m x n 整数矩阵,这个矩阵有两个关键的属性:
- 矩阵的每一行都是非严格递增的,即每一行的元素从左到右可能相等或者递增。
- 矩阵的每一行的第一个元素都比上一行的最后一个元素要大。
你的任务是判断一个给定的整数 target
是否存在于这个矩阵中。如果存在,返回 true
;如果不存在,返回 false
。
解题思路
这个问题可以通过二分查找来解决,因为矩阵的两个属性使得它的每一行和每一列都是有序的。这意味着我们可以在矩阵的行和列上应用二分查找算法。
首先,我们可以对矩阵的行进行一次二分查找,确定 target
可能所在的行。由于每行的第一个元素都比上一行的最后一个元素大,我们可以比较 target
和每行最后一个元素的大小,来缩小搜索范围。
一旦我们确定了 target
可能所在的行,我们就可以在这一行上进行二分查找,来确定 target
是否存在于该行中。由于行内元素是非严格递增的,这一步也是可行的。
整个过程中,我们需要注意处理边界条件,确保不会出现索引越界的错误。
总结一下,解题步骤如下:
- 对矩阵的行使用二分查找,找到
target
可能所在的行。 - 在确定的行内使用二分查找,查找
target
是否存在。 - 如果在任一步骤中找到了
target
,返回true
。 - 如果两次二分查找都没有找到
target
,返回false
。
这种方法的时间复杂度是 O(log(m) + log(n)),其中 m 是矩阵的行数,n 是矩阵的列数。
Golang 版本
package main
import "fmt"
func searchMatrix(matrix [][]int, target int) bool {
if len(matrix) == 0 || len(matrix[0]) == 0 {
return false
}
m, n := len(matrix), len(matrix[0])
// 二分查找可能的行
top, bot := 0, m-1
for top <= bot {
row := (top + bot) / 2
if target > matrix[row][n-1] {
top = row + 1
} else if target < matrix[row][0] {
bot = row - 1
} else {
// 二分查找目标行中可能的列
l, r := 0, n-1
for l <= r {
mid := (l + r) / 2
if target > matrix[row][mid] {
l = mid + 1
} else if target < matrix[row][mid] {
r = mid - 1
} else {
return true
}
}
return false
}
}
return false
}
func main() {
matrix := [][]int{
{1, 3, 5, 7},
{10, 11, 16, 20},
{23, 30, 34, 60},
}
target := 3
fmt.Println(searchMatrix(matrix, target)) // 输出: true
}
这段代码定义了一个函数 searchMatrix
,它接受一个整数矩阵和一个目标值 target
作为参数,返回一个布尔值表示 target
是否在矩阵中。主函数 main
中包含了一个示例矩阵和目标值,用于调用 searchMatrix
函数并打印结果。
C++版本
#include <iostream>
#include <vector>
bool searchMatrix(std::vector<std::vector<int>>& matrix, int target) {
if (matrix.empty() || matrix[0].empty()) return false;
int m = matrix.size(), n = matrix[0].size();
int top = 0, bot = m - 1;
// 二分查找可能的行
while (top <= bot) {
int row = top + (bot - top) / 2;
if (target > matrix[row][n - 1]) {
top = row + 1;
} else if (target < matrix[row][0]) {
bot = row - 1;
} else {
// 二分查找目标行中可能的列
int l = 0, r = n - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (target > matrix[row][mid]) {
l = mid + 1;
} else if (target < matrix[row][mid]) {
r = mid - 1;
} else {
return true;
}
}
return false;
}
}
return false;
}
int main() {
std::vector<std::vector<int>> matrix = {
{1, 3, 5, 7},
{10, 11, 16, 20},
{23, 30, 34, 60}
};
int target = 3;
std::cout << std::boolalpha << searchMatrix(matrix, target) << std::endl; // 输出: true
return 0;
}
这段 C++代码定义了一个函数 searchMatrix
,它接受一个整数矩阵和一个目标值 target
作为参数,并返回一个布尔值表示 target
是否在矩阵中。main
函数中包含了一个示例矩阵和目标值,用于调用 searchMatrix
函数并打印结果。
Python 版本
def searchMatrix(matrix, target):
if not matrix or not matrix[0]:
return False
m, n = len(matrix), len(matrix[0])
top, bot = 0, m - 1
# 二分查找可能的行
while top <= bot:
row = (top + bot) // 2
if target > matrix[row][-1]:
top = row + 1
elif target < matrix[row][0]:
bot = row - 1
else:
# 二分查找目标行中可能的列
l, r = 0, n - 1
while l <= r:
mid = (l + r) // 2
if target > matrix[row][mid]:
l = mid + 1
elif target < matrix[row][mid]:
r = mid - 1
else:
return True
return False
return False
# 示例
matrix = [
[1, 3, 5, 7],
[10, 11, 16, 20],
[23, 30, 34, 60]
]
target = 3
print(searchMatrix(matrix, target)) # 输出: True
Java 版本
public class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return false;
}
int m = matrix.length, n = matrix[0].length;
int top = 0, bot = m - 1;
// 二分查找可能的行
while (top <= bot) {
int row = (top + bot) / 2;
if (target > matrix[row][n - 1]) {
top = row + 1;
} else if (target < matrix[row][0]) {
bot = row - 1;
} else {
// 二分查找目标行中可能的列
int l = 0, r = n - 1;
while (l <= r) {
int mid = (l + r) / 2;
if (target > matrix[row][mid]) {
l = mid + 1;
} else if (target < matrix[row][mid]) {
r = mid - 1;
} else {
return true;
}
}
return false;
}
}
return false;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[][] matrix = {
{1, 3, 5, 7},
{10, 11, 16, 20},
{23, 30, 34, 60}
};
int target = 3;
System.out.println(solution.searchMatrix(matrix, target)); // 输出: true
}
}
这段 Java 代码定义了一个名为 Solution
的类,其中包含了一个 searchMatrix
方法,用于判断目标值 target
是否存在于矩阵 matrix
中。main
方法用于演示如何调用 searchMatrix
方法并打印结果。
Rust 版本
fn search_matrix(matrix: Vec<Vec<i32>>, target: i32) -> bool { if matrix.is_empty() || matrix[0].is_empty() { return false; } let (m, n) = (matrix.len(), matrix[0].len()); let (mut top, mut bot) = (0, m as i32 - 1); // 二分查找可能的行 while top <= bot { let row = ((top + bot) / 2) as usize; if target > matrix[row][n - 1] { top = row as i32 + 1; } else if target < matrix[row][0] { bot = row as i32 - 1; } else { // 二分查找目标行中可能的列 let (mut l, mut r) = (0, n as i32 - 1); while l <= r { let mid = ((l + r) / 2) as usize; if target > matrix[row][mid] { l = mid as i32 + 1; } else if target < matrix[row][mid] { r = mid as i32 - 1; } else { return true; } } return false; } } false } fn main() { let matrix = vec![ vec![1, 3, 5, 7], vec![10, 11, 16, 20], vec![23, 30, 34, 60], ]; let target = 3; println!("{}", search_matrix(matrix, target)); // 输出: true }
这段 Rust 代码定义了一个函数 search_matrix
,它接受一个整数矩阵和一个目标值,然后判断目标值是否存在于矩阵中。main
函数创建了一个示例矩阵和目标值,调用 search_matrix
函数,并打印结果。
总结
上述解法采用了二分查找的策略来优化搜索效率。首先,我们确定了矩阵的行数和列数,然后使用二分查找来确定目标值可能所在的行。由于矩阵的行是按非递减顺序排列的,我们可以通过比较目标值与当前行的首尾元素来判断目标值是否可能位于该行。
一旦确定了可能的行,我们再次应用二分查找法来确定目标值是否存在于该行的列中。这一步同样利用了列的非递减特性,通过比较目标值与中间元素的大小来缩小搜索范围,直至找到目标值或确定目标值不存在。
这种方法的时间复杂度为 O(log m + log n),其中 m 是矩阵的行数,n 是矩阵的列数。这比简单的线性搜索要高效得多,特别是在处理大型矩阵时。
在不同的编程语言版本中,虽然语法有所不同,但核心算法和逻辑是一致的。每种语言都需要处理好边界条件,确保不会出现数组越界等错误。通过这种方法,我们可以高效地在一个特定的矩阵中搜索一个目标值。
在排序数组中查找元素的第一个和最后一个位置
题目要求
设计一个算法,输入为一个非递减顺序排列的整数数组 nums
和一个目标值 target
。要求算法输出目标值 target
在数组 nums
中的起始位置和结束位置的索引。
如果数组中没有目标值 target
,则输出 [-1, -1]
。
算法的时间复杂度必须是 O(log n),这意味着你需要使用比如二分查找这样的高效算法,而不是简单的线性扫描。
解题思路
由于题目要求算法的时间复杂度为 O(log n),我们可以考虑使用二分查找算法。二分查找算法适用于有序数组,而题目中的数组已经是非递减顺序排列,满足使用二分查找的前提条件。
解题步骤如下:
-
查找目标值的起始位置:通过修改二分查找的条件,当找到一个
target
时,不立即返回,而是继续在左侧区间查找,直到找到第一个target
的位置。 -
查找目标值的结束位置:同样使用二分查找,但这次当找到
target
时,继续在右侧区间查找,直到找到最后一个target
的位置。 -
返回结果:如果在第一步中没有找到
target
,则说明数组中没有目标值,返回[-1, -1]
。如果找到了,返回起始位置和结束位置的索引。
注意事项:
- 在实现二分查找时,要正确处理查找区间的边界条件,确保不会漏掉目标值的起始位置和结束位置。
- 要确保算法的时间复杂度不超过 O(log n),在每次查找时都应该是将查找区间减半,而不是逐个检查。
- 由于可能存在多个相同的
target
值,因此找到一个target
后不能立即停止查找,需要继续查找以确定是否有相邻的相同值。
Golang 版本
func searchRange(nums []int, target int) []int {
// 定义一个查找左边界的辅助函数
findLeft := func() int {
left, right := 0, len(nums)-1
for left <= right {
mid := left + (right-left)/2
if nums[mid] < target {
left = mid + 1
} else if nums[mid] > target {
right = mid - 1
} else {
if mid == 0 || nums[mid-1] != target {
return mid
}
right = mid - 1
}
}
return -1
}
// 定义一个查找右边界的辅助函数
findRight := func() int {
left, right := 0, len(nums)-1
for left <= right {
mid := left + (right-left)/2
if nums[mid] < target {
left = mid + 1
} else if nums[mid] > target {
right = mid - 1
} else {
if mid == len(nums)-1 || nums[mid+1] != target {
return mid
}
left = mid + 1
}
}
return -1
}
// 使用辅助函数查找左右边界
left, right := findLeft(), findRight()
// 如果左边界大于右边界,说明没有找到目标值
if left > right {
return []int{-1, -1}
}
return []int{left, right}
}
这段代码定义了两个辅助函数 findLeft
和 findRight
来分别查找目标值的起始位置和结束位置。主函数 searchRange
调用这两个函数,并根据返回的索引判断是否找到了目标值,最后返回结果数组。
C++版本
#include <vector>
class Solution {
public:
std::vector<int> searchRange(std::vector<int>& nums, int target) {
std::vector<int> result(2, -1); // 初始化结果为[-1, -1]
// 辅助函数,用于查找左边界
auto findLeft = [&](int target) -> int {
int left = 0, right = nums.size() - 1, mid;
while (left <= right) {
mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else {
if (mid == 0 || nums[mid - 1] != target) {
return mid;
}
right = mid - 1;
}
}
return -1;
};
// 辅助函数,用于查找右边界
auto findRight = [&](int target) -> int {
int left = 0, right = nums.size() - 1, mid;
while (left <= right) {
mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else {
if (mid == nums.size() - 1 || nums[mid + 1] != target) {
return mid;
}
left = mid + 1;
}
}
return -1;
};
// 查找左边界
result[0] = findLeft(target);
// 如果左边界存在,则查找右边界
if (result[0] != -1) {
result[1] = findRight(target);
}
return result;
}
};
这段 C++代码定义了一个名为 Solution
的类,其中包含一个 searchRange
方法,用于查找目标值 target
在数组 nums
中的起始和结束位置。它使用了两个 lambda 表达式 findLeft
和 findRight
来分别查找左边界和右边界。最后,根据查找结果返回一个包含两个元素的向量,如果没有找到目标值,则向量中的两个元素都是 -1
。
Python 版本
from typing import List
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
def findLeft():
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid - 1
else:
if mid == 0 or nums[mid - 1] != target:
return mid
right = mid - 1
return -1
def findRight():
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid - 1
else:
if mid == len(nums) - 1 or nums[mid + 1] != target:
return mid
left = mid + 1
return -1
left_idx = findLeft()
right_idx = findRight()
if left_idx == -1 or right_idx == -1:
return [-1, -1]
return [left_idx, right_idx]
# Example usage:
# nums = [5,7,7,8,8,10]
# target = 8
# sol = Solution()
# print(sol.searchRange(nums, target)) # Output: [3, 4]
这段 Python 代码定义了一个名为 Solution
的类,其中包含一个 searchRange
方法,用于查找目标值 target
在数组 nums
中的起始和结束位置。它定义了两个内部函数 findLeft
和 findRight
来分别查找左边界和右边界。最后,根据查找结果返回一个包含两个元素的列表,如果没有找到目标值,则列表中的两个元素都是 -1
。
Java 版本
public class Solution {
public int[] searchRange(int[] nums, int target) {
int[] result = new int[]{-1, -1}; // 初始化结果数组
// 查找左边界
int leftIndex = findBoundary(nums, target, true);
if (leftIndex == nums.length || nums[leftIndex] != target) {
return result; // 如果左边界不存在,则直接返回[-1, -1]
}
result[0] = leftIndex; // 设置左边界
result[1] = findBoundary(nums, target, false) - 1; // 设置右边界
return result;
}
// 辅助函数,用于查找左边界或右边界
private int findBoundary(int[] nums, int target, boolean left) {
int low = 0;
int high = nums.length;
while (low < high) {
int mid = low + (high - low) / 2;
if (nums[mid] > target || (left && nums[mid] == target)) {
high = mid;
} else {
low = mid + 1;
}
}
return left ? low : high;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {5, 7, 7, 8, 8, 10};
int target = 8;
int[] range = solution.searchRange(nums, target);
System.out.println("[" + range[0] + ", " + range[1] + "]");
}
}
这段 Java 代码定义了一个名为 Solution
的类,其中包含一个 searchRange
方法,用于查找目标值 target
在数组 nums
中的起始和结束位置。它使用了一个辅助方法 findBoundary
来查找左边界或右边界,根据传入的布尔值 left
确定是查找左边界还是右边界。最后,根据查找结果返回一个包含两个元素的数组,如果没有找到目标值,则数组中的两个元素都是 -1
。在 main
方法中,可以创建 Solution
的实例并调用 searchRange
方法来测试功能。
Rust 版本
fn search_range(nums: Vec<i32>, target: i32) -> Vec<i32> { fn find_boundary(nums: &Vec<i32>, target: i32, left: bool) -> i32 { let (mut low, mut high) = (0, nums.len() as i32); while low < high { let mid = low + (high - low) / 2; if nums[mid as usize] > target || (left && nums[mid as usize] == target) { high = mid; } else { low = mid + 1; } } low } let left_index = find_boundary(&nums, target, true); if left_index as usize == nums.len() || nums[left_index as usize] != target { return vec![-1, -1]; } let right_index = find_boundary(&nums, target, false) - 1; vec![left_index, right_index] } fn main() { let nums = vec![5, 7, 7, 8, 8, 10]; let target = 8; let range = search_range(nums, target); println!("[{}, {}]", range[0], range[1]); }
这段 Rust 代码定义了一个 search_range
函数,它接受一个整数向量 nums
和一个整数 target
作为参数,并返回一个包含两个整数的向量,表示 target
在 nums
中的起始和结束位置。它使用了一个内部函数 find_boundary
来查找左边界或右边界。如果 target
在 nums
中不存在,则返回 [-1, -1]
。在 main
函数中,可以调用 search_range
函数并打印结果来测试其功能。
总结
上面的 Rust 版本解法采用了二分查找算法来解决在排序数组中查找元素的起始和结束位置的问题。这种方法的时间复杂度为 O(log n)
,符合题目要求的效率。解法的核心在于定义了一个辅助函数 find_boundary
,它可以根据传入的参数决定是寻找左边界还是右边界。
find_boundary
函数接收三个参数:一个对数组的引用&Vec<i32>
,目标值 target
,以及一个布尔值 left
,指示查找的是左边界还是右边界。函数内部使用二分查找的方式,根据 left
的值来调整查找的条件,从而找到目标值的边界。
在 search_range
函数中,首先调用 find_boundary
函数寻找左边界。如果找到的左边界超出数组范围或者对应的元素值不等于目标值,则说明目标值不存在于数组中,直接返回[-1, -1]
。如果左边界存在,再次调用 find_boundary
函数寻找右边界,最后返回一个包含左右边界索引的向量。
在 main
函数中,通过实例化一个数组和目标值,调用 search_range
函数,并打印出返回的起始和结束位置,以此来验证函数的正确性。
搜索旋转排序数组
题目要求
给定一个升序排列的整数数组nums
,该数组在某个未知下标k
处进行了旋转。旋转后的数组形式为[nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]
。现在,给定一个目标值target
,要求编写一个时间复杂度为O(log n)
的算法,查找target
是否存在于数组nums
中,如果存在,返回其下标;如果不存在,返回-1
。
解题思路
由于题目要求算法的时间复杂度为O(log n)
,我们可以联想到二分查找算法,它符合这个时间复杂度要求。但是,由于数组经过了旋转,直接应用二分查找的常规方法是不可行的。我们需要对二分查找进行适当的修改,以适应旋转数组的特点。
解题步骤如下:
-
确定旋转点: 首先,我们需要确定数组的旋转点。由于数组原本是升序的,旋转后,数组被分为两个升序的子数组。我们可以通过比较中间元素与数组两端的元素来判断中间元素属于哪个子数组。
-
二分查找: 在确定了旋转点的情况下,我们可以判断
target
可能位于哪个子数组中。然后对可能包含target
的子数组应用二分查找。 -
判断条件: 在二分查找的每一步中,我们需要判断中间元素与
target
的关系,以及中间元素与数组两端元素的关系,来决定搜索范围是在左半部分还是右半部分。 -
循环或递归: 我们可以使用循环或递归的方式来实现二分查找,直到找到
target
或者确定target
不存在于数组中。 -
返回结果: 如果在某次二分查找中找到了
target
,则返回其下标;如果直到搜索范围为空都没有找到target
,则返回-1
。
通过以上步骤,我们可以在O(log n)
的时间复杂度内解决这个问题。需要注意的是,由于数组中的值互不相同,这简化了判断条件,因为我们不需要考虑重复元素带来的复杂性。
Golang 版本
package main
import "fmt"
func search(nums []int, target int) int {
left, right := 0, len(nums)-1
for left <= right {
mid := left + (right-left)/2
if nums[mid] == target {
return mid
}
// 判断mid是在左段还是右段
if nums[mid] >= nums[left] {
// 判断target是在mid的左边还是右边,来调整left和right
if target >= nums[left] && target < nums[mid] {
right = mid - 1
} else {
left = mid + 1
}
} else {
// 同上,根据情况调整left和right
if target > nums[mid] && target <= nums[right] {
left = mid + 1
} else {
right = mid - 1
}
}
}
return -1
}
func main() {
nums := []int{4,5,6,7,0,1,2}
target := 0
result := search(nums, target)
fmt.Printf("The target %d's index is: %d\n", target, result)
}
这段代码实现了题目要求的功能,使用了二分查找的变种来适应旋转数组的情况。在每次循环中,它首先判断中间元素的位置,然后根据target
的值调整搜索范围,直到找到target
或者确定target
不存在。
C++版本
#include <iostream>
#include <vector>
int search(const std::vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
}
// 判断mid是在左段还是右段
if (nums[mid] >= nums[left]) {
// 判断target是在mid的左边还是右边,来调整left和right
if (target >= nums[left] && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else {
// 同上,根据情况调整left和right
if (target > nums[mid] && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}
int main() {
std::vector<int> nums = {4, 5, 6, 7, 0, 1, 2};
int target = 0;
int result = search(nums, target);
std::cout << "The target " << target << "'s index is: " << result << std::endl;
return 0;
}
这段 C++代码实现了题目要求的功能,采用了二分查找算法来处理旋转排序数组中的搜索问题。代码首先确定搜索的左右边界,然后在循环中不断地将搜索范围缩小,直到找到目标值或者搜索范围为空。
Python 版本
def search(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
# 判断mid是在左段还是右段
if nums[mid] >= nums[left]:
# 判断target是在mid的左边还是右边,来调整left和right
if nums[left] <= target < nums[mid]:
right = mid - 1
else:
left = mid + 1
else:
# 同上,根据情况调整left和right
if nums[mid] < target <= nums[right]:
left = mid + 1
else:
right = mid - 1
return -1
# 测试代码
if __name__ == "__main__":
nums = [4, 5, 6, 7, 0, 1, 2]
target = 0
result = search(nums, target)
print(f"The target {target}'s index is: {result}")
这段 Python 代码实现了题目要求的功能,使用二分查找算法来处理旋转排序数组中的搜索问题。代码通过不断调整搜索的左右边界来缩小搜索范围,直到找到目标值或者确定目标值不在数组中。
Java 版本
public class Solution {
public int search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
}
// 判断mid是在左段还是右段
if (nums[mid] >= nums[left]) {
// 判断target是在mid的左边还是右边,来调整left和right
if (target >= nums[left] && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else {
// 同上,根据情况调整left和right
if (target > nums[mid] && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {4, 5, 6, 7, 0, 1, 2};
int target = 0;
int result = solution.search(nums, target);
System.out.println("The target " + target + "'s index is: " + result);
}
}
这段 Java 代码实现了题目要求的功能,采用了二分查找算法来处理旋转排序数组中的搜索问题。代码首先确定搜索的左右边界,然后在循环中不断地将搜索范围缩小,直到找到目标值或者搜索范围为空。
Rust 版本
fn search(nums: Vec<i32>, target: i32) -> i32 { let (mut left, mut right) = (0, nums.len() as i32 - 1); while left <= right { let mid = left + (right - left) / 2; if nums[mid as usize] == target { return mid; } if nums[left as usize] <= nums[mid as usize] { if target >= nums[left as usize] && target < nums[mid as usize] { right = mid - 1; } else { left = mid + 1; } } else { if target > nums[mid as usize] && target <= nums[right as usize] { left = mid + 1; } else { right = mid - 1; } } } -1 } fn main() { let nums = vec![4, 5, 6, 7, 0, 1, 2]; let target = 0; let result = search(nums, target); println!("The target {}'s index is: {}", target, result); }
这段 Rust 代码实现了题目要求的功能,使用二分查找算法来处理旋转排序数组中的搜索问题。代码通过不断调整搜索的左右边界来缩小搜索范围,直到找到目标值或者确定目标值不在数组中。
总结
上述解法采用了二分查找算法来解决在一个部分旋转的有序数组中查找特定目标值的问题。算法的核心思想是利用数组的部分有序性来判断目标值可能位于的区间,并相应地调整搜索范围。
具体步骤如下:
- 初始化两个指针
left
和right
,分别指向数组的起始位置和结束位置。 - 进入一个循环,在
left
小于等于right
的条件下执行。 - 计算中间位置
mid
,并检查nums[mid]
是否等于target
。如果等于,直接返回mid
作为找到的索引。 - 如果
nums[mid]
不等于target
,则判断mid
是在旋转数组的左段还是右段。- 如果
nums[mid]
大于等于nums[left]
,则mid
左侧是有序的。- 如果
target
在nums[left]
和nums[mid]
之间,调整right
为mid - 1
。 - 否则,调整
left
为mid + 1
。
- 如果
- 如果
nums[mid]
小于nums[left]
,则mid
右侧是有序的。- 如果
target
在nums[mid]
和nums[right]
之间,调整left
为mid + 1
。 - 否则,调整
right
为mid - 1
。
- 如果
- 如果
- 如果循环结束后没有找到
target
,返回-1
表示target
不在数组中。
这种方法的时间复杂度为O(log n)
,因为每次迭代都会将搜索范围减半,这是二分查找的特性。这种算法适用于复杂度要求较高的场景,能够有效地处理旋转数组的搜索问题。
寻找旋转排序数组中的最小值
题目要求
给定一个长度为 n 的数组 nums
,该数组原本是升序排列的,但后来经过了若干次(1 到 n 次)的旋转操作。旋转操作定义为将数组的最后一个元素移动到数组的第一个位置,其余元素依次后移。现在需要你找出并返回旋转后数组中的最小元素。
要求你设计一个算法,其时间复杂度为 O(log n),以找到这个最小元素。
解题思路
由于原数组是升序的,且旋转操作保持了数组中元素的相对顺序,我们可以利用二分查找的方法来找到最小元素,以满足时间复杂度为 O(log n) 的要求。
-
初始化指针:设置两个指针
left
和right
,分别指向数组的起始位置和结束位置。 -
二分查找:当
left
小于right
时,执行以下操作:- 找到中间位置
mid = (left + right) / 2
。 - 比较
mid
位置的元素和right
位置的元素:- 如果
nums[mid] < nums[right]
,则说明最小元素位于left
和mid
之间(包括mid
),因此将right
移动到mid
。 - 如果
nums[mid] > nums[right]
,则说明最小元素位于mid + 1
和right
之间,因此将left
移动到mid + 1
。
- 如果
- 找到中间位置
-
循环结束条件:当
left
等于right
时,循环结束,此时left
和right
指向的位置就是最小元素的位置。 -
返回结果:返回
nums[left]
或nums[right]
,此时它们指向同一个位置,即最小元素的位置。
通过上述步骤,我们可以在 O(log n) 的时间复杂度内找到旋转后数组中的最小元素。
Golang 版本
package main
import "fmt"
func findMin(nums []int) int {
left, right := 0, len(nums)-1
for left < right {
mid := left + (right-left)/2
if nums[mid] < nums[right] {
right = mid
} else {
left = mid + 1
}
}
return nums[left]
}
func main() {
nums := []int{4, 5, 6, 7, 0, 1, 2}
fmt.Println("The minimum element is:", findMin(nums))
}
这段代码实现了上述的二分查找算法,用于在一个旋转过的升序数组中找到最小的元素。在 main
函数中,我们定义了一个示例数组 nums
,并调用 findMin
函数来找到并打印出最小元素。
C++版本
#include <iostream>
#include <vector>
int findMin(std::vector<int>& nums) {
int left = 0, right = nums.size() - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] < nums[right]) {
right = mid;
} else {
left = mid + 1;
}
}
return nums[left];
}
int main() {
std::vector<int> nums = {4, 5, 6, 7, 0, 1, 2};
std::cout << "The minimum element is: " << findMin(nums) << std::endl;
return 0;
}
这段 C++代码实现了一个函数 findMin
,它接受一个整数类型的向量 nums
作为参数,并返回旋转后数组中的最小元素。在 main
函数中,我们创建了一个示例向量 nums
,并调用 findMin
函数来找到并输出最小元素。
Python 版本
def find_min(nums):
left, right = 0, len(nums) - 1
while left < right:
mid = (left + right) // 2
if nums[mid] < nums[right]:
right = mid
else:
left = mid + 1
return nums[left]
# 示例
nums = [4, 5, 6, 7, 0, 1, 2]
print("The minimum element is:", find_min(nums))
这段 Python 代码实现了一个函数 find_min
,它接受一个列表 nums
作为参数,并返回旋转后数组中的最小元素。在示例中,我们定义了一个列表 nums
并调用 find_min
函数来找到并打印出最小元素。
Java 版本
public class Solution {
public int findMin(int[] nums) {
int left = 0, right = nums.length - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] < nums[right]) {
right = mid;
} else {
left = mid + 1;
}
}
return nums[left];
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {4, 5, 6, 7, 0, 1, 2};
System.out.println("The minimum element is: " + solution.findMin(nums));
}
}
这段 Java 代码定义了一个 Solution
类,其中包含一个 findMin
方法,用于找到旋转后数组中的最小元素。main
方法中创建了一个示例数组 nums
,并调用 findMin
方法来输出最小元素。
Rust 版本
fn find_min(nums: Vec<i32>) -> i32 { let (mut left, mut right) = (0, nums.len() - 1); while left < right { let mid = left + (right - left) / 2; if nums[mid] < nums[right] { right = mid; } else { left = mid + 1; } } nums[left] } fn main() { let nums = vec![4, 5, 6, 7, 0, 1, 2]; println!("The minimum element is: {}", find_min(nums)); }
这段 Rust 代码定义了一个函数 find_min
,它接受一个 Vec<i32>
类型的向量 nums
作为参数,并返回旋转后数组中的最小元素。在 main
函数中,我们创建了一个示例向量 nums
,并调用 find_min
函数来找到并打印出最小元素。
总结
上述问题的解法基于二分查找算法,适用于在部分有序的数组中查找最小元素。由于数组原本是升序排列的,但经过若干次旋转,数组被分为两个升序的子数组。最小元素是这两个子数组的分界点。
解法的关键步骤如下:
- 初始化两个指针
left
和right
,分别指向数组的起始位置和结束位置。 - 进行循环,直到
left
指针小于right
指针:- 计算中间位置
mid
。 - 判断
mid
位置的元素与right
位置的元素的大小关系:- 如果
nums[mid]
小于nums[right]
,则说明最小元素位于左侧,将right
指针移动到mid
位置。 - 否则,最小元素位于右侧,将
left
指针移动到mid + 1
位置。
- 如果
- 计算中间位置
- 当
left
和right
指针相遇时,left
指向的位置即为最小元素所在的位置。
这种方法的时间复杂度为 O(log n),因为每次迭代都会将搜索范围减半,这符合二分查找的性质。无论是在 Python、Java 还是 Rust 语言中,核心算法思想是一致的,只是在语法和一些细节上有所不同。
寻找两个正序数组的中位数
题目要求
给定两个正序数组 nums1
和 nums2
,其大小分别为 m
和 n
。要求找出这两个数组合并后的中位数。要求算法的时间复杂度为 O(log(m+n))
。
解题思路
要在 O(log(m+n))
的时间复杂度内找到两个正序数组的中位数,可以使用二分查找的方法。以下是解题的步骤:
-
确定较短的数组:由于我们要进行二分查找,为了优化查找效率,我们应该在长度较短的数组上进行二分查找。假设
nums1
是较短的数组。 -
二分查找:在
nums1
上进行二分查找,找到一个位置i
,同时在nums2
中找到位置j
,使得nums1[i-1] <= nums2[j]
且nums2[j-1] <= nums1[i]
。这意味着左半部分的所有元素都小于等于右半部分的所有元素。 -
处理边界情况:在二分查找过程中,需要处理
i
或j
达到数组边界的情况。例如,当i
为 0 时,说明中位数在nums2
中;当i
为m
时,说明中位数在nums1
中。 -
计算中位数:
- 如果
m + n
是奇数,中位数是左半部分的最大值。 - 如果
m + n
是偶数,中位数是左半部分的最大值和右半部分的最小值的平均值。
- 如果
-
左半部分最大值和右半部分最小值的确定:
- 左半部分最大值是
max(nums1[i-1], nums2[j-1])
。 - 右半部分最小值是
min(nums1[i], nums2[j])
。
- 左半部分最大值是
-
循环条件:二分查找的循环条件是
i
在0
到m
的范围内变化,通过比较nums1[i-1]
、nums1[i]
、nums2[j-1]
和nums2[j]
来调整i
的值。
通过上述步骤,可以在 O(log(min(m,n)))
的时间复杂度内找到两个数组的中位数,满足题目要求的 O(log(m+n))
时间复杂度。
Golang 版本
package main
import (
"fmt"
"math"
)
func findMedianSortedArrays(nums1 []int, nums2 []int) float64 {
// 确保nums1是较短的数组
if len(nums1) > len(nums2) {
nums1, nums2 = nums2, nums1
}
m, n := len(nums1), len(nums2)
imin, imax := 0, m
halfLen := (m + n + 1) / 2
for imin <= imax {
i := (imin + imax) / 2
j := halfLen - i
if i < imax && nums2[j-1] > nums1[i] {
// i 的值太小,增加它
imin = i + 1
} else if i > imin && nums1[i-1] > nums2[j] {
// i 的值太大,减小它
imax = i - 1
} else {
// i 的值刚好合适
maxLeft := 0.0
if i == 0 {
maxLeft = float64(nums2[j-1])
} else if j == 0 {
maxLeft = float64(nums1[i-1])
} else {
maxLeft = math.Max(float64(nums1[i-1]), float64(nums2[j-1]))
}
if (m+n)%2 == 1 {
// 如果总长度是奇数,中位数是左半部分的最大值
return maxLeft
}
minRight := 0.0
if i == m {
minRight = float64(nums2[j])
} else if j == n {
minRight = float64(nums1[i])
} else {
minRight = math.Min(float64(nums1[i]), float64(nums2[j]))
}
// 如果总长度是偶数,中位数是左半部分的最大值和右半部分的最小值的平均值
return (maxLeft + minRight) / 2.0
}
}
return 0.0
}
func main() {
nums1 := []int{1, 3}
nums2 := []int{2}
fmt.Println("The median is:", findMedianSortedArrays(nums1, nums2))
}
这段代码实现了上述描述的算法,通过二分查找在两个正序数组中找到中位数。在主函数 main
中,给出了两个数组的示例,并调用 findMedianSortedArrays
函数来计算并打印中位数。
C++版本
#include <iostream>
#include <vector>
#include <algorithm>
double findMedianSortedArrays(std::vector<int>& nums1, std::vector<int>& nums2) {
if (nums1.size() > nums2.size()) {
return findMedianSortedArrays(nums2, nums1);
}
int m = nums1.size();
int n = nums2.size();
int imin = 0, imax = m, halfLen = (m + n + 1) / 2;
while (imin <= imax) {
int i = (imin + imax) / 2;
int j = halfLen - i;
if (i < imax && nums2[j - 1] > nums1[i]) {
imin = i + 1;
} else if (i > imin && nums1[i - 1] > nums2[j]) {
imax = i - 1;
} else {
int maxLeft = 0;
if (i == 0) {
maxLeft = nums2[j - 1];
} else if (j == 0) {
maxLeft = nums1[i - 1];
} else {
maxLeft = std::max(nums1[i - 1], nums2[j - 1]);
}
if ((m + n) % 2 == 1) {
return maxLeft;
}
int minRight = 0;
if (i == m) {
minRight = nums2[j];
} else if (j == n) {
minRight = nums1[i];
} else {
minRight = std::min(nums1[i], nums2[j]);
}
return (maxLeft + minRight) / 2.0;
}
}
return 0.0;
}
int main() {
std::vector<int> nums1 = {1, 3};
std::vector<int> nums2 = {2};
std::cout << "The median is: " << findMedianSortedArrays(nums1, nums2) << std::endl;
return 0;
}
这段 C++代码实现了寻找两个正序数组的中位数的功能,它首先确保第一个数组是较短的数组,然后使用二分查找算法来找到正确的中位数。在 main
函数中,提供了两个数组的示例,并调用 findMedianSortedArrays
函数来计算并输出中位数。
##Python 版本
def findMedianSortedArrays(nums1, nums2):
# Make sure nums1 is the smaller array
if len(nums1) > len(nums2):
nums1, nums2 = nums2, nums1
m, n = len(nums1), len(nums2)
imin, imax = 0, m
half_len = (m + n + 1) // 2
while imin <= imax:
i = (imin + imax) // 2
j = half_len - i
if i < m and nums2[j-1] > nums1[i]:
imin = i + 1
elif i > 0 and nums1[i-1] > nums2[j]:
imax = i - 1
else:
# i is perfect
if i == 0: max_of_left = nums2[j-1]
elif j == 0: max_of_left = nums1[i-1]
else: max_of_left = max(nums1[i-1], nums2[j-1])
if (m + n) % 2 == 1:
return max_of_left
# i is not perfect, but can be corrected
if i == m: min_of_right = nums2[j]
elif j == n: min_of_right = nums1[i]
else: min_of_right = min(nums1[i], nums2[j])
return (max_of_left + min_of_right) / 2.0
return -1
# Example usage:
nums1 = [1, 3]
nums2 = [2]
print("The median is:", findMedianSortedArrays(nums1, nums2))
这段 Python 代码实现了寻找两个有序数组的中位数的算法。它首先确保nums1
是较短的数组,然后使用二分查找来找到中位数。在示例用法中,给出了两个数组,并调用findMedianSortedArrays
函数来计算并打印中位数。
##Java 版本
public class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
if (nums1.length > nums2.length) {
int[] temp = nums1;
nums1 = nums2;
nums2 = temp;
}
int m = nums1.length;
int n = nums2.length;
int imin = 0, imax = m, halfLen = (m + n + 1) / 2;
while (imin <= imax) {
int i = (imin + imax) / 2;
int j = halfLen - i;
if (i < imax && nums2[j-1] > nums1[i]){
imin = i + 1;
} else if (i > imin && nums1[i-1] > nums2[j]) {
imax = i - 1;
} else {
int maxLeft = 0;
if (i == 0) { maxLeft = nums2[j-1]; }
else if (j == 0) { maxLeft = nums1[i-1]; }
else { maxLeft = Math.max(nums1[i-1], nums2[j-1]); }
if ( (m + n) % 2 == 1 ) { return maxLeft; }
int minRight = 0;
if (i == m) { minRight = nums2[j]; }
else if (j == n) { minRight = nums1[i]; }
else { minRight = Math.min(nums1[i], nums2[j]); }
return (maxLeft + minRight) / 2.0;
}
}
return 0.0;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums1 = {1, 3};
int[] nums2 = {2};
System.out.println("The median is: " + solution.findMedianSortedArrays(nums1, nums2));
}
}
这段 Java 代码实现了寻找两个有序数组的中位数的算法。它首先确保nums1
是较短的数组,然后使用二分查找来找到中位数。在main
方法中,创建了一个Solution
对象,并给出了两个数组的示例,调用findMedianSortedArrays
方法来计算并打印中位数。
##Rust 版本
fn find_median_sorted_arrays(nums1: Vec<i32>, nums2: Vec<i32>) -> f64 { let (mut nums1, mut nums2) = if nums1.len() <= nums2.len() { (nums1, nums2) } else { (nums2, nums1) }; let (m, n) = (nums1.len(), nums2.len()); let mut imin = 0; let mut imax = m; let half_len = (m + n + 1) / 2; while imin <= imax { let i = (imin + imax) / 2; let j = half_len - i; if i < imax && nums2[j - 1] > nums1[i] { imin = i + 1; } else if i > imin && nums1[i - 1] > nums2[j] { imax = i - 1; } else { let max_left = if i == 0 { nums2[j - 1] as f64 } else if j == 0 { nums1[i - 1] as f64 } else { nums1[i - 1].max(nums2[j - 1]) as f64 }; if (m + n) % 2 == 1 { return max_left; } let min_right = if i == m { nums2[j] as f64 } else if j == n { nums1[i] as f64 } else { nums1[i].min(nums2[j]) as f64 }; return (max_left + min_right) / 2.0; } } 0.0 } fn main() { let nums1 = vec![1, 3]; let nums2 = vec![2]; println!("The median is: {}", find_median_sorted_arrays(nums1, nums2)); }
这段 Rust 代码实现了寻找两个有序数组的中位数的算法。它首先确保nums1
是较短的数组,然后使用二分查找来找到中位数。在main
函数中,给出了两个数组的示例,并调用find_median_sorted_arrays
函数来计算并打印中位数。
总结
上述解法采用了二分查找算法来寻找两个有序数组的中位数,以满足时间复杂度为O(log(m+n))
的要求。这种方法不需要将两个数组合并,而是通过找到正确的分割点来直接计算中位数。以下是解法的关键步骤:
-
确保
nums1
是两个数组中较短的一个,这样可以保证二分查找的范围更小,从而优化性能。 -
初始化两个指针
imin
和imax
,分别指向nums1
的起始位置和结束位置。 -
计算两个数组的长度之和的一半
half_len
,这将帮助我们找到中位数的位置。 -
使用二分查找,通过调整
imin
和imax
来找到正确的分割点。分割点左侧的元素数量等于half_len
。 -
在每次迭代中,计算
nums1
的分割点i
和nums2
的分割点j
,以保证i + j = half_len
。 -
根据分割点两侧元素的大小关系调整
imin
和imax
的值,直到找到满足以下条件的分割点:nums1[i-1] <= nums2[j]
且nums2[j-1] <= nums1[i]
,这意味着左侧所有元素都小于等于右侧所有元素。
-
当找到正确的分割点后,中位数可以通过以下方式计算:
- 如果
m + n
是奇数,则中位数是左侧部分的最大值。 - 如果
m + n
是偶数,则中位数是左侧部分的最大值和右侧部分的最小值的平均值。
- 如果
-
如果
nums1
的分割点在数组的起始位置或结束位置,需要特别处理边界情况。
这种方法的优势在于它不需要合并数组,而是通过找到正确的分割点来直接计算中位数,从而大大减少了计算时间和空间复杂度。
栈
栈是一种遵循后进先出(LIFO)原则的数据结构。在解决栈相关的算法题时,可以遵循以下几个通用思路:
-
明确问题类型:栈通常用于解决需要逆序处理数据或者需要在数据流中寻找最近相关性的问题。
-
理解栈的操作:熟悉栈的基本操作,包括
push
(入栈)、pop
(出栈)、peek
/top
(访问栈顶元素)和isEmpty
(判断栈是否为空)。 -
边界条件处理:在实现栈的操作时,要注意边界条件的处理,如栈为空时的
pop
操作。 -
辅助栈的使用:有时候可能需要使用两个栈来协同完成任务,比如在实现一个能够返回最小值的栈时,可以用一个辅助栈来跟踪最小值。
-
递归与栈:递归本质上就是一个栈结构,有时可以通过递归来模拟栈操作。
-
字符串与栈:在处理括号匹配、后缀表达式(逆波兰表达式)等字符串问题时,栈是一个非常有用的工具。
-
空间换时间:有时候使用栈可以帮助我们减少时间复杂度,例如在遍历数组时,使用栈来记录之前的状态。
下面是一些使用 Go 语言实现的栈的算法例子:
例 1:实现一个基本的栈
type Stack []int
func (s *Stack) Push(v int) {
*s = append(*s, v)
}
func (s *Stack) Pop() int {
if s.IsEmpty() {
return 0 // 或者其他错误处理
}
index := len(*s) - 1
element := (*s)[index]
*s = (*s)[:index]
return element
}
func (s *Stack) Top() int {
if s.IsEmpty() {
return 0 // 或者其他错误处理
}
return (*s)[len(*s)-1]
}
func (s *Stack) IsEmpty() bool {
return len(*s) == 0
}
例 2:使用栈解决有效的括号问题
func isValid(s string) bool {
stack := make([]rune, 0)
match := map[rune]rune{')': '(', '}': '{', ']': '['}
for _, char := range s {
if char == '(' || char == '{' || char == '[' {
stack = append(stack, char)
} else {
if len(stack) == 0 || stack[len(stack)-1] != match[char] {
return false
}
stack = stack[:len(stack)-1]
}
}
return len(stack) == 0
}
例 3:使用栈实现一个队列
type MyQueue struct {
inputStack []int
outputStack []int
}
func Constructor() MyQueue {
return MyQueue{
inputStack: make([]int, 0),
outputStack: make([]int, 0),
}
}
func (this *MyQueue) Push(x int) {
this.inputStack = append(this.inputStack, x)
}
func (this *MyQueue) Pop() int {
this.Peek()
res := this.outputStack[len(this.outputStack)-1]
this.outputStack = this.outputStack[:len(this.outputStack)-1]
return res
}
func (this *MyQueue) Peek() int {
if len(this.outputStack) == 0 {
for len(this.inputStack) > 0 {
this.outputStack = append(this.outputStack, this.inputStack[len(this.inputStack)-1])
this.inputStack = this.inputStack[:len(this.inputStack)-1]
}
}
return this.outputStack[len(this.outputStack)-1]
}
func (this *MyQueue) Empty() bool {
return len(this.inputStack) == 0 && len(this.outputStack) == 0
}
在解决栈的算法题时,理解题目要求和栈的特性是关键。上述代码示例展示了如何使用 Go 语言的切片来实现栈的基本操作,并解决了一些常见的栈相关问题。在实际编码时,还需要考虑错误处理和优化性能等因素。
有效的括号
题目要求
编写一个算法来判断一个只包含括号字符的字符串是否为有效的括号组合。有效字符串的定义如下:
- 任何左括号 '(' 必须有一个对应的右括号 ')'。
- 任何左大括号 '{' 必须有一个对应的右大括号 '}'。
- 任何左方括号 '[' 必须有一个对应的右方括号 ']'。
- 左括号必须以正确的顺序闭合,这意味着括号之间不能交叉或不匹配。
解题思路
为了解决这个问题,我们可以使用栈(Stack)这一数据结构。栈是一种后进先出(LIFO)的数据结构,它允许我们只在一端(顶部)添加或移除数据。以下是解题的步骤:
- 初始化一个空栈。
- 遍历字符串中的每个字符:
- 如果当前字符是开括号('(','{' 或 '['),则将其推入栈中。
- 如果当前字符是闭括号(')','}' 或 ']'):
- 检查栈是否为空。如果为空,说明没有对应的开括号,因此字符串无效。
- 否则,弹出栈顶元素。如果弹出的开括号与当前闭括号类型不匹配,则字符串无效。
- 在遍历完所有字符后,如果栈为空,则说明所有开括号都找到了匹配的闭括号,字符串有效。
- 如果栈不为空,则说明有未匹配的开括号,字符串无效。
通过这种方式,我们可以确保所有的开括号都有对应的闭括号,并且它们的顺序是正确的。
Golang 版本
package main
import (
"fmt"
)
func isValid(s string) bool {
stack := []rune{}
for _, char := range s {
switch char {
case '(', '{', '[':
stack = append(stack, char)
case ')', '}', ']':
if len(stack) == 0 {
return false
}
top := stack[len(stack)-1]
stack = stack[:len(stack)-1]
if (char == ')' && top != '(') || (char == '}' && top != '{') || (char == ']' && top != '[') {
return false
}
}
}
return len(stack) == 0
}
func main() {
s := "([]{})"
fmt.Println(isValid(s)) // 输出应为 true
}
这段代码定义了一个isValid
函数,它接受一个字符串s
并返回一个布尔值,表示字符串中的括号是否有效。主函数main
中给出了一个示例字符串,并打印出了isValid
函数的返回结果。
C++版本
#include <iostream>
#include <stack>
#include <string>
bool isValid(std::string s) {
std::stack<char> stack;
for (char c : s) {
switch (c) {
case '(':
case '{':
case '[':
stack.push(c);
break;
case ')':
if (stack.empty() || stack.top() != '(') return false;
stack.pop();
break;
case '}':
if (stack.empty() || stack.top() != '{') return false;
stack.pop();
break;
case ']':
if (stack.empty() || stack.top() != '[') return false;
stack.pop();
break;
default:
// Invalid character
return false;
}
}
return stack.empty();
}
int main() {
std::string s = "([]{})";
std::cout << (isValid(s) ? "true" : "false") << std::endl; // 应输出 true
return 0;
}
这段 C++代码定义了一个isValid
函数,它接受一个字符串s
并返回一个布尔值,表示字符串中的括号是否有效。主函数main
中给出了一个示例字符串,并打印出了isValid
函数的返回结果。
Python 版本
def is_valid(s: str) -> bool:
stack = []
bracket_map = {')': '(', '}': '{', ']': '['}
for char in s:
if char in bracket_map:
top_element = stack.pop() if stack else '#'
if bracket_map[char] != top_element:
return False
else:
stack.append(char)
return not stack
# 测试代码
s = "([]{})"
print(is_valid(s)) # 应输出 True
Java 版本
import java.util.Stack;
public class Solution {
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
for (char c : s.toCharArray()) {
if (c == '(' || c == '{' || c == '[') {
stack.push(c);
} else {
if (stack.isEmpty()) return false;
char top = stack.pop();
if ((c == ')' && top != '(') || (c == '}' && top != '{') || (c == ']' && top != '[')) {
return false;
}
}
}
return stack.isEmpty();
}
public static void main(String[] args) {
Solution solution = new Solution();
String s = "([]{})";
System.out.println(solution.isValid(s)); // 应输出 true
}
}
这段 Java 代码定义了一个Solution
类,其中包含了一个isValid
方法,用于判断字符串中的括号是否有效。在main
方法中,创建了一个Solution
对象,并用一个示例字符串调用了isValid
方法,然后打印出了方法的返回结果。
Rust 版本
fn is_valid(s: String) -> bool { let mut stack: Vec<char> = Vec::new(); for c in s.chars() { match c { '(' | '{' | '[' => stack.push(c), ')' => if stack.pop() != Some('(') { return false; }, '}' => if stack.pop() != Some('{') { return false; }, ']' => if stack.pop() != Some('[') { return false; }, _ => return false, } } stack.is_empty() } fn main() { let s = String::from("([]{})"); println!("{}", is_valid(s)); // 应输出 true }
这段 Rust 代码定义了一个is_valid
函数,它接受一个字符串s
并返回一个布尔值,表示字符串中的括号是否有效。在main
函数中,创建了一个示例字符串,并调用了is_valid
函数,然后打印出了函数的返回结果。
总结
上述代码示例中,无论是 Golang、C++、Python、Java 还是 Rust 版本,核心算法思路都是相同的,即使用栈(Stack)数据结构来确保括号的正确匹配。算法的步骤可以总结如下:
- 创建一个空栈来存储开括号。
- 遍历输入字符串中的每个字符。
- 当遇到开括号('(','{','[')时,将其推入栈中。
- 当遇到闭括号(')','}',']')时,检查栈顶元素:
- 如果栈为空,或者栈顶元素与当前闭括号不匹配,则字符串无效。
- 如果栈顶元素与当前闭括号匹配,将栈顶元素弹出。
- 在遍历完成后,如果栈为空,则说明所有的开括号都找到了匹配的闭括号,字符串有效。
- 如果栈不为空,则还有未匹配的开括号,字符串无效。
不同语言在实现上有细微差别,主要体现在语法和数据结构的使用上。例如:
- Golang 使用切片(slice)模拟栈。
- C++使用
std::stack
标准库中的栈。 - Python 直接使用列表(list)作为栈。
- Java 使用
java.util.Stack
类。 - Rust 使用向量(Vec)来模拟栈。
尽管实现的细节不同,但所有这些解法都遵循了相同的逻辑和算法步骤。
最小栈
题目要求
设计一个栈,这个栈除了能够进行常规的压栈(push)、出栈(pop)、获取栈顶元素(top)操作外,还能够在常数时间内检索到栈中的最小元素。具体来说,需要实现以下功能:
MinStack()
:构造一个新的最小栈。void push(int val)
:将元素val
压入栈中。void pop()
:移除栈顶元素。int top()
:返回栈顶元素。int getMin()
:检索栈中的最小元素。
解题思路
为了在常数时间内检索到最小元素,我们需要在每个元素入栈时记录下当前栈的最小值。这意味着每个元素都需要携带一个额外的信息,即入栈时栈内的最小值。
这可以通过以下方式实现:
- 使用两个栈,一个用来存储所有的元素(我们称之为主栈),另一个用来存储每个状态下的最小值(我们称之为辅助栈)。
- 当一个元素要入栈时,我们将其压入主栈,并比较此元素与辅助栈的栈顶元素(当前的最小值)。如果辅助栈为空或者此元素更小,那么也将这个元素压入辅助栈。
- 当执行出栈操作时,同时从主栈和辅助栈中移除栈顶元素,但只有当两个栈顶元素相等时,才从辅助栈中移除元素,因为这表示移除的是当前的最小值。
- 获取栈顶元素时,直接返回主栈的栈顶元素。
- 获取最小元素时,返回辅助栈的栈顶元素,因为辅助栈的栈顶元素始终是当前栈中的最小值。
通过这种方式,我们可以确保push
、pop
、top
和getMin
操作的时间复杂度都是 O(1)。
Golang 版本
type MinStack struct {
stack []int
minStack []int
}
func Constructor() MinStack {
return MinStack{}
}
func (this *MinStack) Push(val int) {
this.stack = append(this.stack, val)
// 如果minStack为空或者val小于等于minStack的栈顶元素,则将val也推入minStack
if len(this.minStack) == 0 || val <= this.minStack[len(this.minStack)-1] {
this.minStack = append(this.minStack, val)
}
}
func (this *MinStack) Pop() {
// 如果stack的栈顶元素等于minStack的栈顶元素,则同时弹出
if this.stack[len(this.stack)-1] == this.minStack[len(this.minStack)-1] {
this.minStack = this.minStack[:len(this.minStack)-1]
}
this.stack = this.stack[:len(this.stack)-1]
}
func (this *MinStack) Top() int {
return this.stack[len(this.stack)-1]
}
func (this *MinStack) GetMin() int {
return this.minStack[len(this.minStack)-1]
}
/**
* Your MinStack object will be instantiated and called as such:
* obj := Constructor();
* obj.Push(val);
* obj.Pop();
* param_3 := obj.Top();
* param_4 := obj.GetMin();
*/
这段代码定义了一个MinStack
结构体,其中包含两个切片:stack
用于存储所有元素,minStack
用于存储每个状态下的最小值。Constructor
函数初始化一个MinStack
对象。Push
、Pop
、Top
和GetMin
方法分别实现了题目要求的功能。
C++版本
#include <stack>
using namespace std;
class MinStack {
private:
stack<int> s1; // 主栈,用于存储所有元素
stack<int> s2; // 辅助栈,用于存储每个状态下的最小值
public:
MinStack() {
}
void push(int val) {
s1.push(val);
// 如果s2为空或者val小于等于s2的栈顶元素,则将val也推入s2
if (s2.empty() || val <= s2.top()) {
s2.push(val);
}
}
void pop() {
// 如果s1的栈顶元素等于s2的栈顶元素,则同时弹出
if (s1.top() == s2.top()) {
s2.pop();
}
s1.pop();
}
int top() {
return s1.top();
}
int getMin() {
return s2.top();
}
};
/**
* Your MinStack object will be instantiated and called as such:
* MinStack* obj = new MinStack();
* obj->push(val);
* obj->pop();
* int param_3 = obj->top();
* int param_4 = obj->getMin();
*/
这段 C++代码定义了一个MinStack
类,其中包含两个私有成员:s1
和s2
,分别是两个栈,用于实现题目要求的功能。push
方法将元素压入s1
,并在必要时将元素压入s2
以跟踪最小值。pop
方法从s1
中弹出元素,并在弹出的元素是当前最小值时,也从s2
中弹出。top
方法返回s1
的栈顶元素,而getMin
方法返回s2
的栈顶元素,即当前的最小值。
Python 版本
class MinStack:
def __init__(self):
self.stack = []
self.min_stack = []
def push(self, val: int) -> None:
self.stack.append(val)
# 如果min_stack为空或者val小于等于min_stack的最后一个元素,则将val也添加到min_stack
if not self.min_stack or val <= self.min_stack[-1]:
self.min_stack.append(val)
def pop(self) -> None:
# 如果stack的最后一个元素等于min_stack的最后一个元素,则同时移除
if self.stack.pop() == self.min_stack[-1]:
self.min_stack.pop()
def top(self) -> int:
return self.stack[-1]
def getMin(self) -> int:
return self.min_stack[-1]
# Your MinStack object will be instantiated and called as such:
# obj = MinStack()
# obj.push(val)
# obj.pop()
# param_3 = obj.top()
# param_4 = obj.getMin()
这段 Python 代码定义了一个MinStack
类,其中包含两个列表:stack
用于存储所有元素,min_stack
用于存储每个状态下的最小值。__init__
方法初始化一个MinStack
对象。push
、pop
、top
和getMin
方法分别实现了题目要求的功能。
Java 版本
import java.util.Stack;
public class MinStack {
private Stack<Integer> stack;
private Stack<Integer> minStack;
public MinStack() {
stack = new Stack<>();
minStack = new Stack<>();
}
public void push(int val) {
stack.push(val);
// 如果minStack为空或者val小于等于minStack的栈顶元素,则将val也推入minStack
if (minStack.isEmpty() || val <= minStack.peek()) {
minStack.push(val);
}
}
public void pop() {
// 如果stack的栈顶元素等于minStack的栈顶元素,则同时弹出
if (stack.peek().equals(minStack.peek())) {
minStack.pop();
}
stack.pop();
}
public int top() {
return stack.peek();
}
public int getMin() {
return minStack.peek();
}
}
// Your MinStack object will be instantiated and called as such:
// MinStack obj = new MinStack();
// obj.push(val);
// obj.pop();
// int param_3 = obj.top();
// int param_4 = obj.getMin();
这段 Java 代码定义了一个MinStack
类,其中包含两个Stack<Integer>
类型的成员变量:stack
用于存储所有元素,minStack
用于存储每个状态下的最小值。MinStack
构造函数初始化这两个栈。push
方法将元素压入stack
,并在必要时将元素压入minStack
以跟踪最小值。pop
方法从stack
中弹出元素,并在弹出的元素是当前最小值时,也从minStack
中弹出。top
方法返回stack
的栈顶元素,而getMin
方法返回minStack
的栈顶元素,即当前的最小值。
Rust 版本
struct MinStack { stack: Vec<i32>, min_stack: Vec<i32>, } impl MinStack { fn new() -> Self { MinStack { stack: Vec::new(), min_stack: Vec::new(), } } fn push(&mut self, val: i32) { self.stack.push(val); if self.min_stack.is_empty() || val <= *self.min_stack.last().unwrap() { self.min_stack.push(val); } } fn pop(&mut self) { if let Some(val) = self.stack.pop() { if val == *self.min_stack.last().unwrap() { self.min_stack.pop(); } } } fn top(&self) -> i32 { *self.stack.last().unwrap() } fn get_min(&self) -> i32 { *self.min_stack.last().unwrap() } } fn main() { let mut min_stack = MinStack::new(); min_stack.push(-2); min_stack.push(0); min_stack.push(-3); println!("Min: {}", min_stack.get_min()); // Returns -3. min_stack.pop(); println!("Top: {}", min_stack.top()); // Returns 0. println!("Min: {}", min_stack.get_min()); // Returns -2. }
这段 Rust 代码定义了一个MinStack
结构体,其中包含两个字段:stack
和min_stack
,分别是两个动态数组,用于实现题目要求的功能。new
函数用于创建一个新的MinStack
实例。push
、pop
、top
和get_min
方法分别实现了题目要求的功能。main
函数展示了如何使用MinStack
。
总结
在上述的解法中,我们设计了一个特殊的栈结构,称为最小栈(MinStack),它能够在常数时间内检索到最小元素。无论是 Python、Java 还是 Rust 版本的实现,核心思想都是使用两个栈:
- 主栈(stack):用于存储所有的元素。
- 辅助栈(min_stack 或 minStack):用于存储当前栈中的最小元素。
每次元素入栈时,我们将元素推入主栈。同时,如果辅助栈为空或者入栈元素的值小于等于辅助栈的栈顶元素,我们也将这个元素推入辅助栈。这样,辅助栈的栈顶元素始终是当前栈中的最小值。
当元素出栈时,如果主栈的栈顶元素等于辅助栈的栈顶元素,我们同时将辅助栈的栈顶元素弹出。这保证了辅助栈的栈顶元素始终是当前栈中剩余元素的最小值。
top
方法和getMin
方法分别用于获取主栈的栈顶元素和辅助栈的栈顶元素,后者即为当前栈的最小值。
这种设计允许我们在 O(1)时间复杂度内完成push
、pop
、top
和getMin
操作,同时空间复杂度为 O(n),因为我们为每个元素在辅助栈中也存储了一份数据。
字符串解码
题目要求
给定一个字符串,这个字符串包含一些特定的编码规则。这些规则定义了如何重复字符串中的某些部分。具体来说,编码规则遵循这样的格式:k[encoded_string],这里的 k 是一个正整数,encoded_string 是需要被重复的字符串。当解码时,我们需要将 encoded_string 重复 k 次。
例如,字符串 "3[a]2[bc]" 解码后应该变成 "aaabcbc"。
解码的过程需要遵循以下几个原则:
- 输入的字符串总是有效的,不包含任何额外的空格。
- 方括号总是成对出现,并且格式正确。
- 原始数据(即不在方括号内的数据)不包含数字,数字总是表示重复次数。
- 输入的字符串可能包含嵌套的编码规则,例如 "2[3[a]b]",解码后应该变成 "aaabaaab"。
解题思路
要解决这个问题,我们可以采用栈(Stack)的数据结构来处理。栈是一种后进先出(LIFO)的数据结构,它可以帮助我们在遇到嵌套结构时保持跟踪状态。
- 初始化两个栈:一个用于保存数字(即重复次数),另一个用于保存当前处理的字符串。
- 遍历输入字符串:对于每个字符,我们需要根据其类型执行不同的操作:
- 如果是数字,我们需要计算出完整的数字(因为数字可能有多位),然后将其压入数字栈。
- 如果是字母,我们将其添加到当前处理的字符串中。
- 如果遇到左方括号 '[',说明我们遇到了一个新的 encoded_string 的开始,我们需要将当前的字符串压入栈中,并且重置当前字符串,以便于收集新的 encoded_string。
- 如果遇到右方括号 ']',说明一个 encoded_string 结束了,我们需要进行解码操作。这时,我们将数字栈的栈顶元素弹出,这个数字表示重复次数。然后我们将字符串栈的栈顶元素弹出,这是之前累积的字符串。我们将当前处理的字符串重复弹出的数字次数,然后将其与弹出的字符串拼接,作为新的当前处理的字符串。
- 重复步骤 2,直到遍历完整个输入字符串。
- 返回结果:遍历结束后,当前处理的字符串即为解码后的字符串。
通过这种方式,我们可以逐步构建出最终的解码字符串,即使在存在嵌套的情况下也能正确处理。
Golang 版本
package main
import (
"strconv"
"strings"
"unicode"
)
func decodeString(s string) string {
var numStack []int
var strStack []string
var currentNum int
var currentStr string
for _, char := range s {
if unicode.IsDigit(char) {
// Parse the number and update currentNum
num, _ := strconv.Atoi(string(char))
currentNum = currentNum*10 + num
} else if char == '[' {
// Push the currentNum and currentStr to their respective stacks
numStack = append(numStack, currentNum)
strStack = append(strStack, currentStr)
// Reset currentNum and currentStr
currentNum = 0
currentStr = ""
} else if char == ']' {
// Pop the number from numStack to get the repeat times
repeatTimes := numStack[len(numStack)-1]
numStack = numStack[:len(numStack)-1]
// Pop the string from strStack and append the currentStr repeated repeatTimes
prevStr := strStack[len(strStack)-1]
strStack = strStack[:len(strStack)-1]
currentStr = prevStr + strings.Repeat(currentStr, repeatTimes)
} else {
// Append the current character to currentStr
currentStr += string(char)
}
}
return currentStr
}
func main() {
// Example usage:
encodedString := "3[a]2[bc]"
decodedString := decodeString(encodedString)
println(decodedString) // Output: aaabcbc
}
这段代码定义了一个decodeString
函数,它接受一个编码的字符串并返回解码后的字符串。在main
函数中,我们提供了一个示例用法,调用decodeString
函数并打印结果。
C++版本
#include <iostream>
#include <stack>
#include <string>
std::string decodeString(const std::string& s) {
std::stack<int> counts;
std::stack<std::string> result;
std::string currentStr;
int k = 0;
for (char ch : s) {
if (isdigit(ch)) {
k = k * 10 + (ch - '0');
} else if (ch == '[') {
counts.push(k);
result.push(currentStr);
currentStr = "";
k = 0;
} else if (ch == ']') {
std::string temp = result.top();
result.pop();
for (int i = counts.top(); i > 0; --i) {
temp += currentStr;
}
counts.pop();
currentStr = temp;
} else {
currentStr += ch;
}
}
return currentStr;
}
int main() {
std::string encodedString = "3[a]2[bc]";
std::string decodedString = decodeString(encodedString);
std::cout << decodedString << std::endl; // Output: aaabcbc
return 0;
}
这段 C++代码实现了一个decodeString
函数,它接收一个编码的字符串并返回解码后的字符串。在main
函数中,我们提供了一个示例用法,调用decodeString
函数并打印结果。
Python 版本
def decodeString(s):
stack = []
current_num = 0
current_string = ''
for char in s:
if char.isdigit():
current_num = current_num * 10 + int(char)
elif char == '[':
stack.append((current_string, current_num))
current_string = ''
current_num = 0
elif char == ']':
last_string, num = stack.pop()
current_string = last_string + num * current_string
else:
current_string += char
return current_string
# Example usage:
encoded_string = "3[a]2[bc]"
decoded_string = decodeString(encoded_string)
print(decoded_string) # Output: aaabcbc
这段 Python 代码定义了一个decodeString
函数,它接受一个编码的字符串并返回解码后的字符串。在示例用法中,我们调用decodeString
函数并打印结果。
Java 版本
import java.util.Stack;
public class Solution {
public String decodeString(String s) {
Stack<Integer> countStack = new Stack<>();
Stack<StringBuilder> stringStack = new Stack<>();
StringBuilder currentString = new StringBuilder();
int k = 0;
for (char ch : s.toCharArray()) {
if (Character.isDigit(ch)) {
k = k * 10 + (ch - '0');
} else if (ch == '[') {
countStack.push(k);
stringStack.push(currentString);
currentString = new StringBuilder();
k = 0;
} else if (ch == ']') {
StringBuilder decodedString = stringStack.pop();
for (int currentK = countStack.pop(); currentK > 0; currentK--) {
decodedString.append(currentString);
}
currentString = decodedString;
} else {
currentString.append(ch);
}
}
return currentString.toString();
}
public static void main(String[] args) {
Solution solution = new Solution();
String encodedString = "3[a]2[bc]";
String decodedString = solution.decodeString(encodedString);
System.out.println(decodedString); // Output: aaabcbc
}
}
这段 Java 代码定义了一个Solution
类,其中包含一个decodeString
方法,用于解码给定的字符串。在main
方法中,我们创建了Solution
类的实例,并调用decodeString
方法,然后打印出解码后的字符串。
Rust 版本
use std::str::Chars; use std::iter::Peekable; fn decode_string(s: &str) -> String { let mut chars_iter = s.chars().peekable(); decode(&mut chars_iter) } fn decode(chars_iter: &mut Peekable<Chars>) -> String { let mut result = String::new(); let mut num = 0; while let Some(&ch) = chars_iter.peek() { match ch { '0'..='9' => { num = num * 10 + ch.to_digit(10).unwrap(); chars_iter.next(); } '[' => { chars_iter.next(); // skip '[' let part = decode(chars_iter); for _ in 0..num { result.push_str(&part); } num = 0; } ']' => { chars_iter.next(); // skip ']' break; } _ => { result.push(ch); chars_iter.next(); } } } result } fn main() { let encoded_string = "3[a]2[bc]"; let decoded_string = decode_string(encoded_string); println!("{}", decoded_string); // Output: aaabcbc }
这段 Rust 代码定义了一个decode_string
函数,它接受一个编码的字符串并返回解码后的字符串。decode
函数是一个辅助函数,它使用递归来处理嵌套的编码字符串。在main
函数中,我们调用decode_string
函数并打印结果。
总结
上述解法遵循了一个通用的算法框架,适用于多种编程语言。核心思想是使用两个栈:一个用于保存数字(即重复次数),另一个用于保存当前构建的字符串。算法的步骤如下:
- 遍历输入字符串中的每个字符。
- 当遇到数字时,计算出完整的数字(可能有多位),并将其保存在数字栈中。
- 遇到左括号 '[' 时,将当前的数字(重复次数)和当前构建的字符串分别压入各自的栈中,然后重置这两个变量,准备解析新的字符串片段。
- 遇到右括号 ']' 时,从数字栈中弹出一个数字,表示重复次数;从字符串栈中弹出一个字符串,表示之前累积的字符串。将当前构建的字符串重复相应的次数,并拼接到弹出的字符串后面,更新当前字符串。
- 遇到字母时,将其追加到当前构建的字符串中。
- 继续遍历,直到处理完所有字符。
- 最终,当前构建的字符串即为解码后的字符串。
这个算法在不同的编程语言中实现时,主要的区别在于语法和数据结构的使用。例如,在 Python 中使用列表来模拟栈的操作,在 C++中使用std::stack
,在 Java 中使用Stack
类,在 Rust 中使用Vec
作为栈,并利用迭代器和模式匹配来简化代码。尽管实现细节不同,但所有版本的核心算法逻辑是一致的。
每日温度
题目要求
你需要编写一个函数,该函数接收一个整数数组 temperatures
作为参数,这个数组中的每个元素代表每一天的气温。你的任务是返回一个新的整数数组 answer
。对于 answer
中的每个元素 answer[i]
,它应该表示从第 i
天开始,要经过多少天才会出现更高的气温。如果从第 i
天开始之后没有任何一天的气温比第 i
天高,那么 answer[i]
应该被设置为 0
。
解题思路
要解决这个问题,我们可以采用一个单调栈的数据结构来优化我们的搜索过程。单调栈是一种特殊的栈,用于处理下一个更大或更小元素的问题。在这个问题中,我们需要找到每个元素右边第一个更大的元素的位置。
- 创建一个栈,用于存储那些还没有找到下一个更高温度的天数的索引。
- 创建一个数组
answer
,初始化为与temperatures
相同长度的数组,并将所有元素初始化为0
。 - 遍历
temperatures
数组,对于每一天:- 当栈不为空,并且当前天的温度大于栈顶索引对应的温度时,说明我们找到了栈顶索引天数的下一个更高温度。
- 弹出栈顶元素,将当前天的索引与栈顶索引相减,得到的差值就是两天之间的距离,将这个差值赋值给
answer
数组对应栈顶索引的位置。 - 重复上述过程,直到栈为空或者当前天的温度不再大于栈顶索引对应的温度。
- 将当前天的索引压入栈中。
- 遍历完成后,栈中剩余的索引对应的天数都是没有更高温度的,它们在
answer
数组中已经默认为0
。 - 返回填充完毕的
answer
数组。
这种方法的时间复杂度是 O(n),因为每个元素最多被压入和弹出栈一次。空间复杂度也是 O(n),用于存储栈和结果数组。
Golang 版本
package main
import "fmt"
func dailyTemperatures(temperatures []int) []int {
length := len(temperatures)
answer := make([]int, length)
stack := []int{} // 使用切片模拟栈
for i, temp := range temperatures {
for len(stack) > 0 && temp > temperatures[stack[len(stack)-1]] {
prevIndex := stack[len(stack)-1]
answer[prevIndex] = i - prevIndex
stack = stack[:len(stack)-1] // 弹出栈顶元素
}
stack = append(stack, i) // 当前索引入栈
}
// 栈中剩余的索引对应的天数都是没有更高温度的,它们在answer数组中已经默认为0
return answer
}
func main() {
temperatures := []int{73, 74, 75, 71, 69, 72, 76, 73}
fmt.Println(dailyTemperatures(temperatures))
}
这段代码定义了一个 dailyTemperatures
函数,它接受一个整数切片 temperatures
作为输入,并返回一个整数切片作为输出,表示每天之后多少天会有更高的温度。在 main
函数中,我们给出了一个示例输入,并打印出了函数的输出结果。
C++版本
#include <iostream>
#include <vector>
#include <stack>
std::vector<int> dailyTemperatures(std::vector<int>& temperatures) {
std::vector<int> answer(temperatures.size(), 0);
std::stack<int> indexStack; // 使用栈来存储索引
for (int i = 0; i < temperatures.size(); ++i) {
while (!indexStack.empty() && temperatures[i] > temperatures[indexStack.top()]) {
int prevIndex = indexStack.top();
answer[prevIndex] = i - prevIndex;
indexStack.pop();
}
indexStack.push(i);
}
// 栈中剩余的索引对应的天数都是没有更高温度的,它们在answer数组中已经默认为0
return answer;
}
int main() {
std::vector<int> temperatures = {73, 74, 75, 71, 69, 72, 76, 73};
std::vector<int> answer = dailyTemperatures(temperatures);
for (int temp : answer) {
std::cout << temp << " ";
}
std::cout << std::endl;
return 0;
}
这段 C++代码实现了与 Golang 版本相同的功能。它定义了一个 dailyTemperatures
函数,该函数接受一个整数向量 temperatures
作为输入,并返回一个整数向量作为输出,表示每天之后多少天会有更高的温度。在 main
函数中,我们给出了一个示例输入,并打印出了函数的输出结果。
Python 版本
def dailyTemperatures(temperatures):
answer = [0] * len(temperatures)
stack = [] # This will store the indices of the temperatures
for i, temp in enumerate(temperatures):
while stack and temp > temperatures[stack[-1]]:
prev_index = stack.pop()
answer[prev_index] = i - prev_index
stack.append(i)
return answer
# Example usage:
if __name__ == "__main__":
temperatures = [73, 74, 75, 71, 69, 72, 76, 73]
print(dailyTemperatures(temperatures))
这段 Python 代码提供了一个 dailyTemperatures
函数,它接受一个温度列表作为输入,并返回一个整数列表作为输出,表示每天之后多少天会有更高的温度。在 if __name__ == "__main__":
部分,我们给出了一个示例输入,并打印出了函数的输出结果。
Java 版本
import java.util.Arrays;
import java.util.Stack;
public class Solution {
public int[] dailyTemperatures(int[] temperatures) {
int[] answer = new int[temperatures.length];
Stack<Integer> stack = new Stack<>(); // 使用栈来存储索引
for (int i = 0; i < temperatures.length; i++) {
while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
int prevIndex = stack.pop();
answer[prevIndex] = i - prevIndex;
}
stack.push(i);
}
// 栈中剩余的索引对应的天数都是没有更高温度的,它们在answer数组中已经默认为0
return answer;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] temperatures = {73, 74, 75, 71, 69, 72, 76, 73};
int[] answer = solution.dailyTemperatures(temperatures);
System.out.println(Arrays.toString(answer));
}
}
这段 Java 代码实现了与前面提到的其他语言版本相同的功能。它定义了一个 Solution
类,其中包含一个 dailyTemperatures
方法,该方法接受一个整数数组 temperatures
作为输入,并返回一个整数数组作为输出,表示每天之后多少天会有更高的温度。在 main
方法中,我们创建了 Solution
类的一个实例,并给出了一个示例输入,然后打印出了方法的输出结果。
Rust 版本
fn daily_temperatures(temperatures: Vec<i32>) -> Vec<i32> { let mut answer = vec![0; temperatures.len()]; let mut stack: Vec<usize> = Vec::new(); for (i, &temp) in temperatures.iter().enumerate() { while let Some(&last_index) = stack.last() { if temp <= temperatures[last_index] { break; } stack.pop(); answer[last_index] = (i - last_index) as i32; } stack.push(i); } answer } fn main() { let temperatures = vec![73, 74, 75, 71, 69, 72, 76, 73]; let answer = daily_temperatures(temperatures); println!("{:?}", answer); }
这段 Rust 代码实现了一个 daily_temperatures
函数,它接受一个 i32
类型的向量 temperatures
作为输入,并返回一个同样是 i32
类型的向量作为输出,表示每天之后多少天会有更高的温度。在 main
函数中,我们给出了一个示例输入,并打印出了函数的输出结果。
总结
上述代码段展示了如何使用不同编程语言(Go, C++, Python, Java, Rust)来解决同一个问题:给定一个表示每天温度的整数数组 temperatures
,计算对于每一天,还需要等待多少天才能得到更高的温度。如果不存在这样的未来一天,则对应的结果为 0
。
解决方案的核心思想是使用一个栈来跟踪尚未找到更高温度的天数的索引。算法的步骤如下:
- 初始化一个栈来存储索引,以及一个数组
answer
来存储结果,初始值为0
。 - 遍历
temperatures
数组,对于每个元素:- 当栈不为空且当前温度大于栈顶索引对应的温度时:
- 弹出栈顶元素,这是一个尚未找到更高温度的天的索引。
- 计算当前天与栈顶索引对应天的差值,这是等待的天数,将其存储在
answer
数组对应位置。
- 将当前索引压入栈中。
- 当栈不为空且当前温度大于栈顶索引对应的温度时:
- 遍历完成后,栈中剩余的索引对应的天数都是没有更高温度的,它们在
answer
数组中已经默认为0
。
这种方法的时间复杂度通常是 O(n),因为每个元素最多被压入和弹出栈一次。空间复杂度是 O(n),用于存储栈和结果数组。
不同语言的实现细节略有不同,但算法逻辑是一致的。例如,Go 使用切片模拟栈,C++ 使用 std::stack
,Python 使用列表,Java 使用 Stack
类,而 Rust 使用向量。尽管语法不同,但所有这些实现都遵循了上述算法步骤。
柱状图中最大的矩形
题目要求
给定一个由 n 个非负整数组成的数组,这些整数表示一个柱状图中每个柱子的高度,每个柱子的宽度假定为 1。要求计算在这个柱状图中,能够勾勒出的最大矩形的面积。
解题思路
要解决这个问题,我们可以采用以下几种思路:
-
暴力法:
- 遍历每对柱子,以较短的柱子为高,两柱子之间的距离为宽,计算出所有可能的矩形的面积。
- 在所有计算出的面积中找到最大值。
- 这种方法的时间复杂度较高,在最坏的情况下为 O(n^2)。
-
分治法:
- 找到最短的柱子,这个柱子将图分割为左右两部分。
- 最大矩形要么在左边,要么在右边,要么跨过最短的柱子。
- 递归地在左右两部分中寻找最大矩形,并与跨过最短柱子的矩形面积比较。
- 分治法的时间复杂度通常为 O(n log n),但在最坏情况下(数组已排序)仍然可能是 O(n^2)。
-
栈的应用:
- 使用一个栈来维护一个索引序列,这些索引对应的柱子的高度是非递减的。
- 遍历每个柱子:
- 如果当前柱子的高度大于栈顶索引对应的柱子的高度,将当前索引入栈。
- 如果当前柱子的高度小于栈顶索引对应的柱子的高度,则说明找到了一个局部最大的矩形,其高为栈顶索引对应的柱子的高度,宽为当前索引与栈顶索引之间的距离。此时,计算这个矩形的面积,并将栈顶元素出栈,重复这个过程,直到栈顶索引对应的柱子的高度小于或等于当前柱子的高度。
- 遍历完成后,栈中可能还有一些索引,这些对应的柱子的高度从栈底到栈顶是递增的。对于栈中的每个索引,其对应的矩形的宽可以认为是从该索引到数组末尾的距离。
- 这种方法的时间复杂度为 O(n),因为每个索引最多入栈和出栈一次。
通过以上任一种方法,我们都可以找到柱状图中能够勾勒出的最大矩形的面积。通常情况下,栈的应用方法因为其线性的时间复杂度而被优先考虑。
Golang 版本
package main
import "fmt"
func largestRectangleArea(heights []int) int {
maxArea := 0
stack := []int{-1} // 初始化栈,先放入一个哨兵元素
for i, height := range heights {
for len(stack) > 1 && heights[stack[len(stack)-1]] >= height {
// 计算面积
h := heights[stack[len(stack)-1]]
stack = stack[:len(stack)-1] // 出栈
w := i - stack[len(stack)-1] - 1
maxArea = max(maxArea, h*w)
}
stack = append(stack, i) // 当前柱子的索引入栈
}
// 清空栈中剩余的元素
for len(stack) > 1 {
h := heights[stack[len(stack)-1]]
stack = stack[:len(stack)-1]
w := len(heights) - stack[len(stack)-1] - 1
maxArea = max(maxArea, h*w)
}
return maxArea
}
func max(x, y int) int {
if x > y {
return x
}
return y
}
func main() {
heights := []int{2, 1, 5, 6, 2, 3}
fmt.Println(largestRectangleArea(heights))
}
这段代码实现了使用栈的方法来解决柱状图中最大矩形面积的问题。代码首先定义了一个largestRectangleArea
函数,该函数接收一个整数切片heights
作为输入,返回一个整数表示最大矩形的面积。在main
函数中,我们定义了一个示例输入,调用largestRectangleArea
函数,并打印出结果。
C++版本
#include <iostream>
#include <vector>
#include <stack>
int largestRectangleArea(std::vector<int>& heights) {
int maxArea = 0;
std::stack<int> indexStack;
indexStack.push(-1); // 添加一个哨兵,简化边界情况的处理
for (int i = 0; i < heights.size(); ++i) {
while (indexStack.top() != -1 && heights[indexStack.top()] >= heights[i]) {
int height = heights[indexStack.top()];
indexStack.pop();
int width = i - indexStack.top() - 1;
maxArea = std::max(maxArea, height * width);
}
indexStack.push(i);
}
// 清理栈中剩余的元素
while (indexStack.top() != -1) {
int height = heights[indexStack.top()];
indexStack.pop();
int width = heights.size() - indexStack.top() - 1;
maxArea = std::max(maxArea, height * width);
}
return maxArea;
}
int main() {
std::vector<int> heights = {2, 1, 5, 6, 2, 3};
std::cout << largestRectangleArea(heights) << std::endl;
return 0;
}
这段 C++代码实现了一个函数largestRectangleArea
,它接受一个整数向量heights
作为输入,并返回一个整数表示柱状图中最大矩形的面积。在main
函数中,我们定义了一个示例输入,调用largestRectangleArea
函数,并打印出结果。代码中使用了std::stack
来维护索引的栈,以及std::max
来计算最大面积。
Python 版本
def largestRectangleArea(heights):
stack = [-1]
max_area = 0
for i, h in enumerate(heights):
while stack[-1] != -1 and heights[stack[-1]] >= h:
height = heights[stack.pop()]
width = i - stack[-1] - 1
max_area = max(max_area, height * width)
stack.append(i)
while stack[-1] != -1:
height = heights[stack.pop()]
width = len(heights) - stack[-1] - 1
max_area = max(max_area, height * width)
return max_area
# Example usage:
if __name__ == "__main__":
heights = [2, 1, 5, 6, 2, 3]
print(largestRectangleArea(heights))
这段 Python 代码定义了一个函数largestRectangleArea
,它接受一个列表heights
作为输入,并返回一个整数表示柱状图中最大矩形的面积。在主函数中,我们定义了一个示例输入,调用largestRectangleArea
函数,并打印出结果。代码中使用了列表来模拟栈的操作。
Java 版本
import java.util.Stack;
public class Solution {
public int largestRectangleArea(int[] heights) {
Stack<Integer> stack = new Stack<>();
stack.push(-1);
int maxArea = 0;
for (int i = 0; i < heights.length; ++i) {
while (stack.peek() != -1 && heights[stack.peek()] >= heights[i]) {
int height = heights[stack.pop()];
int width = i - stack.peek() - 1;
maxArea = Math.max(maxArea, height * width);
}
stack.push(i);
}
while (stack.peek() != -1) {
int height = heights[stack.pop()];
int width = heights.length - stack.peek() - 1;
maxArea = Math.max(maxArea, height * width);
}
return maxArea;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] heights = {2, 1, 5, 6, 2, 3};
System.out.println(solution.largestRectangleArea(heights));
}
}
这段 Java 代码定义了一个名为Solution
的类,其中包含一个方法largestRectangleArea
,它接受一个整数数组heights
作为输入,并返回一个整数表示柱状图中最大矩形的面积。在main
方法中,我们创建了一个Solution
对象,定义了一个示例输入,调用largestRectangleArea
方法,并打印出结果。代码中使用了Stack<Integer>
来维护索引的栈。
Rust 版本
fn largest_rectangle_area(heights: Vec<i32>) -> i32 { let mut stack: Vec<i32> = vec![-1]; // 使用-1作为哨兵值 let mut max_area = 0; for (i, &height) in heights.iter().enumerate() { while *stack.last().unwrap() != -1 && heights[*stack.last().unwrap() as usize] >= height { let h = heights[stack.pop().unwrap() as usize]; let w = i as i32 - stack.last().unwrap() - 1; max_area = max_area.max(h * w); } stack.push(i as i32); } while *stack.last().unwrap() != -1 { let h = heights[stack.pop().unwrap() as usize]; let w = heights.len() as i32 - stack.last().unwrap() - 1; max_area = max_area.max(h * w); } max_area } fn main() { let heights = vec![2, 1, 5, 6, 2, 3]; println!("{}", largest_rectangle_area(heights)); }
这段 Rust 代码定义了一个函数largest_rectangle_area
,它接受一个整数向量heights
作为输入,并返回一个整数表示柱状图中最大矩形的面积。在main
函数中,我们定义了一个示例输入,调用largest_rectangle_area
函数,并打印出结果。代码中使用了向量来模拟栈的操作,并利用 Rust 的模式匹配和所有权系统来确保代码的安全性。
总结
解决这个柱状图中最大矩形面积的问题,我们采用了一个单调栈的数据结构来优化查找过程。单调栈是一种特殊的栈,它在这个问题中用来维护一个索引序列,这些索引对应的柱子高度是单调递增的。
算法的主要步骤如下:
- 初始化一个栈,并将一个哨兵值(通常是-1)压入栈中,表示栈的底部。
- 遍历输入数组
heights
中的每个元素:- 当栈不为空且当前元素小于栈顶索引对应的元素时,说明找到了一个局部最大矩形。此时,可以弹出栈顶元素,并计算以该栈顶元素为高的矩形面积(宽度为当前索引与新的栈顶索引之间的距离)。
- 将当前索引压入栈中。
- 遍历完成后,栈中可能还有一些元素。这些元素对应的柱子没有右边界,因此需要分别计算以这些柱子为高的矩形面积,直到栈为空。
- 在每一步中,计算出的矩形面积与当前最大面积进行比较,并更新最大面积。
这种方法的时间复杂度是 O(n),因为每个元素最多被压入和弹出栈一次。空间复杂度是 O(n),用于存储栈中的索引。
在实际代码实现中,我们需要注意栈操作的细节,以及在栈为空时如何处理。在 Rust 版本中,我们还需要注意所有权和生命周期的问题,确保代码的安全性。
堆
堆(Heap)是一种特殊的完全二叉树,所有的父节点都满足某种特定的顺序关系(大于或小于)与其子节点。堆通常有两种类型:最大堆和最小堆。在最大堆中,每个父节点的值都大于或等于其子节点的值;在最小堆中,每个父节点的值都小于或等于其子节点的值。
解决关于堆的算法题通常涉及以下几个操作:
-
建堆(Heapify):将一个无序的数组构建成一个堆。对于最小堆,通常从最后一个非叶子节点开始向前调整,确保所有的父节点都小于其子节点。
-
插入(Insert):向堆中插入一个新元素,然后上浮(或下沉)该元素,以保持堆的性质。
-
删除(Delete):通常删除堆顶元素(最大堆中的最大元素或最小堆中的最小元素),然后将最后一个元素移动到堆顶,并进行下沉操作,以保持堆的性质。
-
堆排序(Heap Sort):利用堆的性质进行排序。通过不断移除堆顶元素并重建堆来实现。
-
更新(Update):更新堆中的某个元素,然后根据其值的增减进行上浮或下沉操作。
在 Go 语言中,可以使用container/heap
包来实现堆的操作,但为了更好地理解堆的内部机制,我们可以手动实现堆的各种操作。下面是一个最小堆的 Go 语言实现示例:
package main
import (
"fmt"
)
// MinHeap struct has a slice that holds the array
type MinHeap struct {
array []int
}
// Insert adds an element to the heap
func (h *MinHeap) Insert(key int) {
h.array = append(h.array, key)
h.minHeapifyUp(len(h.array) - 1)
}
// Extract returns the smallest key, and removes it from the heap
func (h *MinHeap) Extract() (int, bool) {
if len(h.array) == 0 {
return 0, false
}
min := h.array[0]
lastIndex := len(h.array) - 1
// Move the last element to the top
h.array[0] = h.array[lastIndex]
h.array = h.array[:lastIndex]
h.minHeapifyDown(0)
return min, true
}
// minHeapifyUp will heapify from bottom top
func (h *MinHeap) minHeapifyUp(index int) {
for h.array[parent(index)] > h.array[index] {
h.swap(parent(index), index)
index = parent(index)
}
}
// minHeapifyDown will heapify from top bottom
func (h *MinHeap) minHeapifyDown(index int) {
lastIndex := len(h.array) - 1
l, r := left(index), right(index)
childToCompare := 0
for l <= lastIndex {
if l == lastIndex { // when left child is the only child
childToCompare = l
} else if h.array[l] < h.array[r] { // when left child is smaller
childToCompare = l
} else { // when right child is smaller
childToCompare = r
}
if h.array[index] > h.array[childToCompare] {
h.swap(index, childToCompare)
index = childToCompare
l, r = left(index), right(index)
} else {
return
}
}
}
// get the parent index
func parent(i int) int {
return (i - 1) / 2
}
// get the left child index
func left(i int) int {
return 2*i + 1
}
// get the right child index
func right(i int) int {
return 2*i + 2
}
// swap keys in the array
func (h *MinHeap) swap(i1, i2 int) {
h.array[i1], h.array[i2] = h.array[i2], h.array[i1]
}
func main() {
minHeap := &MinHeap{}
fmt.Println("Min Heap")
minHeap.Insert(10)
minHeap.Insert(23)
minHeap.Insert(36)
minHeap.Insert(18)
minHeap.Insert(2)
minHeap.Insert(3)
minHeap.Insert(41)
fmt.Println("Extract min:", minHeap.Extract())
}
在这个例子中,我们定义了一个MinHeap
结构体,它有一个array
切片来存储堆中的元素。我们实现了Insert
和Extract
方法来添加和移除元素,以及minHeapifyUp
和minHeapifyDown
方法来维护堆的性质。parent
、left
和right
函数用于计算父节点和子节点的索引。最后,swap
方法用于交换元素。
当解决堆相关问题时,通常需要根据问题的具体要求来调整堆的实现细节,比如可能需要实现最大堆,或者需要在堆中存储复杂的数据结构等。理解堆的基本操作和性质是解决这些问题的关键。
在 C++中,堆通常可以通过标准库中的priority_queue
来实现,它默认是一个最大堆。如果你想实现一个最小堆,你可以通过传递一个比较函数来改变排序的顺序。下面是一些使用 C++标准库中的priority_queue
来解决堆相关问题的例子。
最大堆的例子
#include <iostream>
#include <queue>
int main() {
// 默认是最大堆
std::priority_queue<int> maxHeap;
// 插入元素
maxHeap.push(30);
maxHeap.push(10);
maxHeap.push(20);
maxHeap.push(5);
// 删除并获取最大元素
while (!maxHeap.empty()) {
int maxElement = maxHeap.top(); // 获取最大元素
std::cout << maxElement << " ";
maxHeap.pop(); // 移除最大元素
}
return 0;
}
最小堆的例子
#include <iostream>
#include <queue>
#include <vector>
#include <functional>
int main() {
// 使用greater<>来创建最小堆
std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;
// 插入元素
minHeap.push(30);
minHeap.push(10);
minHeap.push(20);
minHeap.push(5);
// 删除并获取最小元素
while (!minHeap.empty()) {
int minElement = minHeap.top(); // 获取最小元素
std::cout << minElement << " ";
minHeap.pop(); // 移除最小元素
}
return 0;
}
如果你需要手动实现一个堆,而不是使用priority_queue
,你可以使用一个动态数组(如std::vector
)来存储堆的元素,并实现堆的各种操作。下面是一个手动实现的最小堆:
#include <iostream>
#include <vector>
class MinHeap {
private:
std::vector<int> data;
void heapifyUp(int index) {
while (index > 0 && data[parent(index)] > data[index]) {
std::swap(data[parent(index)], data[index]);
index = parent(index);
}
}
void heapifyDown(int index) {
int lastIndex = data.size() - 1;
int leftChild, rightChild, minIndex;
while (left(index) <= lastIndex) {
leftChild = left(index);
rightChild = right(index);
minIndex = index;
if (data[minIndex] > data[leftChild]) {
minIndex = leftChild;
}
if (rightChild <= lastIndex && data[minIndex] > data[rightChild]) {
minIndex = rightChild;
}
if (minIndex != index) {
std::swap(data[index], data[minIndex]);
index = minIndex;
} else {
break;
}
}
}
int left(int parent) {
return 2 * parent + 1;
}
int right(int parent) {
return 2 * parent + 2;
}
int parent(int child) {
return (child - 1) / 2;
}
public:
void insert(int key) {
data.push_back(key);
heapifyUp(data.size() - 1);
}
int getMin() {
return data.front();
}
void removeMin() {
if (data.size() == 0) {
throw std::out_of_range("Heap is empty");
}
data[0] = data.back();
data.pop_back();
heapifyDown(0);
}
};
int main() {
MinHeap minHeap;
minHeap.insert(3);
minHeap.insert(2);
minHeap.insert(15);
minHeap.insert(5);
minHeap.insert(4);
minHeap.insert(45);
std::cout << "Min value: " << minHeap.getMin() << std::endl;
minHeap.removeMin();
std::cout << "Min value: " << minHeap.getMin() << std::endl;
return 0;
}
在这个例子中,我们定义了一个MinHeap
类,它有一个私有的std::vector<int>
来存储堆中的元素。我们实现了insert
和removeMin
方法来添加和移除元素,以及heapifyUp
和heapifyDown
方法来维护堆的性质。left
、right
和parent
函数用于计算父节点和子节点的索引。
数组中的第 K 个最大元素
题目要求
给定一个整数数组 nums
和一个整数 k
,要求编写一个算法来找出并返回这个数组中第 k
个最大的元素。这里的第 k
个最大元素是指在数组完全排序后处于倒数第 k
个位置的元素。
需要注意的是,题目要求返回的是第 k
个最大的元素,而不是第 k
个不同的元素。此外,题目强调算法的时间复杂度必须为 O(n),这意味着不能直接对数组进行排序,因为常规的排序算法如快速排序、归并排序的平均时间复杂度为 O(n log n)。
解题思路
要在 O(n) 的时间复杂度内解决这个问题,可以采用以下几种思路:
-
快速选择算法(Quick Select):这是快速排序算法的变种,用于在未完全排序的数组中查找第
k
个最小(或最大)元素。算法的基本思想是随机选择一个“枢轴”元素,然后将数组分为两部分:一部分包含小于枢轴的元素,另一部分包含大于枢轴的元素。这时可以确定枢轴元素的确切位置,如果这个位置恰好是我们要找的第k
个最大元素的位置,我们就找到了答案。如果不是,我们可以递归地在较小或较大的部分中继续查找。由于每次可以排除一半的元素,所以平均时间复杂度为 O(n)。 -
堆(Heap):可以使用最小堆来解决这个问题。首先创建一个大小为
k
的最小堆,并将数组nums
的前k
个元素添加到堆中。然后遍历数组中剩余的元素,对于每个元素,如果它大于堆顶元素,则将堆顶元素移除并将当前元素添加到堆中。遍历完成后,堆顶元素即为第k
个最大的元素。虽然堆的插入和删除操作的时间复杂度是 O(log k),但由于我们只维护一个大小为k
的堆,并且每个元素最多只被插入和删除一次,因此总的时间复杂度仍然是 O(n)。 -
中位数的中位数算法(Median of Medians):这是快速选择算法的一个优化版本,它选择一个好的枢轴来保证算法的最坏情况时间复杂度为 O(n)。它通过选择一组元素的中位数作为枢轴来改进快速选择算法,这样可以保证每次至少有一定比例的元素被排除,从而避免了快速选择算法在最坏情况下退化到 O(n^2) 的风险。
在实际应用中,快速选择算法由于其实现简单和在平均情况下的高效性,通常是解决这类问题的首选方法。而中位数的中位数算法虽然可以保证最坏情况下的性能,但由于其常数因子较大,在实际中使用较少。
Golang 版本
package main
import (
"fmt"
"math/rand"
"time"
)
func findKthLargest(nums []int, k int) int {
target := len(nums) - k
left, right := 0, len(nums)-1
rand.Seed(time.Now().UnixNano())
for left <= right {
pivotIndex := rand.Intn(right-left+1) + left
newPivotIndex := partition(nums, left, right, pivotIndex)
if newPivotIndex == target {
return nums[newPivotIndex]
} else if newPivotIndex < target {
left = newPivotIndex + 1
} else {
right = newPivotIndex - 1
}
}
return -1
}
func partition(nums []int, left, right, pivotIndex int) int {
pivotValue := nums[pivotIndex]
nums[pivotIndex], nums[right] = nums[right], nums[pivotIndex]
storeIndex := left
for i := left; i < right; i++ {
if nums[i] < pivotValue {
nums[storeIndex], nums[i] = nums[i], nums[storeIndex]
storeIndex++
}
}
nums[right], nums[storeIndex] = nums[storeIndex], nums[right]
return storeIndex
}
func main() {
nums := []int{3, 2, 1, 5, 6, 4}
k := 2
fmt.Printf("The %dth largest element is %d\n", k, findKthLargest(nums, k))
}
这段代码实现了快速选择算法,用于在 O(n) 的平均时间复杂度内找到数组中第 k 个最大的元素。代码首先定义了 findKthLargest
函数,它使用了一个辅助函数 partition
来根据枢轴值对数组进行划分,并递归地在划分的子数组中查找第 k 个最大的元素。主函数 main
提供了一个示例数组和 k 值,并调用 findKthLargest
函数来输出结果。
C++版本
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstdlib>
#include <ctime>
int partition(std::vector<int>& nums, int left, int right) {
int pivot = nums[left];
int l = left + 1;
int r = right;
while (l <= r) {
if (nums[l] < pivot && nums[r] > pivot) {
std::swap(nums[l++], nums[r--]);
}
if (nums[l] >= pivot) l++;
if (nums[r] <= pivot) r--;
}
std::swap(nums[left], nums[r]);
return r;
}
int quickSelect(std::vector<int>& nums, int left, int right, int k) {
if (left == right) return nums[left];
srand(time(0));
int pivotIndex = left + rand() % (right - left + 1);
pivotIndex = partition(nums, left, right);
if (k == pivotIndex) {
return nums[k];
} else if (k < pivotIndex) {
return quickSelect(nums, left, pivotIndex - 1, k);
} else {
return quickSelect(nums, pivotIndex + 1, right, k);
}
}
int findKthLargest(std::vector<int>& nums, int k) {
int size = nums.size();
return quickSelect(nums, 0, size - 1, size - k);
}
int main() {
std::vector<int> nums = {3, 2, 1, 5, 6, 4};
int k = 2;
std::cout << "The " << k << "th largest element is " << findKthLargest(nums, k) << std::endl;
return 0;
}
这段 C++ 代码实现了快速选择算法,用于在 O(n) 的平均时间复杂度内找到数组中第 k 个最大的元素。代码中定义了 partition
函数用于根据枢轴值对数组进行划分,quickSelect
函数用于递归地在划分的子数组中查找第 k 个最大的元素,以及 findKthLargest
函数用于启动快速选择过程。主函数 main
提供了一个示例数组和 k 值,并调用 findKthLargest
函数来输出结果。
Python 版本
import random
def partition(nums, left, right):
pivot = nums[right]
i = left
for j in range(left, right):
if nums[j] < pivot:
nums[i], nums[j] = nums[j], nums[i]
i += 1
nums[i], nums[right] = nums[right], nums[i]
return i
def quick_select(nums, left, right, k):
if left == right:
return nums[left]
pivot_index = random.randint(left, right)
pivot_index = partition(nums, left, right)
if k == pivot_index:
return nums[k]
elif k < pivot_index:
return quick_select(nums, left, pivot_index - 1, k)
else:
return quick_select(nums, pivot_index + 1, right, k)
def find_kth_largest(nums, k):
return quick_select(nums, 0, len(nums) - 1, len(nums) - k)
# Example usage:
if __name__ == "__main__":
nums = [3, 2, 1, 5, 6, 4]
k = 2
print(f"The {k}th largest element is {find_kth_largest(nums, k)}")
这段 Python 代码实现了快速选择算法,用于在 O(n) 的平均时间复杂度内找到数组中第 k 个最大的元素。代码中定义了 partition
函数用于根据枢轴值对数组进行划分,quick_select
函数用于递归地在划分的子数组中查找第 k 个最大的元素,以及 find_kth_largest
函数用于启动快速选择过程。主函数部分提供了一个示例数组和 k 值,并调用 find_kth_largest
函数来输出结果。
Java 版本
import java.util.Random;
public class KthLargestElement {
public static int findKthLargest(int[] nums, int k) {
int n = nums.length;
k = n - k; // convert to kth smallest
int left = 0;
int right = n - 1;
Random random = new Random();
while (left <= right) {
int pivotIndex = partition(nums, left, right, random.nextInt(right - left + 1) + left);
if (pivotIndex < k) {
left = pivotIndex + 1;
} else if (pivotIndex > k) {
right = pivotIndex - 1;
} else {
return nums[k];
}
}
throw new IllegalArgumentException("k is out of bounds");
}
private static int partition(int[] nums, int left, int right, int pivotIndex) {
int pivotValue = nums[pivotIndex];
swap(nums, pivotIndex, right); // Move pivot to end
int storeIndex = left;
for (int i = left; i < right; i++) {
if (nums[i] < pivotValue) {
swap(nums, storeIndex, i);
storeIndex++;
}
}
swap(nums, right, storeIndex); // Move pivot to its final place
return storeIndex;
}
private static void swap(int[] nums, int a, int b) {
int tmp = nums[a];
nums[a] = nums[b];
nums[b] = tmp;
}
public static void main(String[] args) {
int[] nums = {3, 2, 1, 5, 6, 4};
int k = 2;
System.out.println("The " + k + "th largest element is " + findKthLargest(nums, k));
}
}
这段 Java 代码实现了快速选择算法,用于在 O(n) 的平均时间复杂度内找到数组中第 k 个最大的元素。代码中定义了 partition
方法用于根据枢轴值对数组进行划分,findKthLargest
方法用于启动快速选择过程。swap
方法用于在数组中交换两个元素的位置。主方法 main
提供了一个示例数组和 k 值,并调用 findKthLargest
方法来输出结果。
Rust 版本
use rand::Rng; use std::cmp::Ordering; fn partition(nums: &mut Vec<i32>, left: usize, right: usize, pivot_index: usize) -> usize { nums.swap(pivot_index, right); let pivot = nums[right]; let mut store_index = left; for i in left..right { if nums[i] < pivot { nums.swap(store_index, i); store_index += 1; } } nums.swap(right, store_index); store_index } fn quick_select(nums: &mut Vec<i32>, left: usize, right: usize, k: usize) -> i32 { if left == right { return nums[left]; } let mut rng = rand::thread_rng(); let pivot_index = rng.gen_range(left..=right); let pivot_index = partition(nums, left, right, pivot_index); match pivot_index.cmp(&k) { Ordering::Equal => nums[k], Ordering::Greater => quick_select(nums, left, pivot_index - 1, k), Ordering::Less => quick_select(nums, pivot_index + 1, right, k), } } pub fn find_kth_largest(nums: &mut Vec<i32>, k: i32) -> i32 { let size = nums.len(); quick_select(nums, 0, size - 1, size - k as usize) } fn main() { let mut nums = vec![3, 2, 1, 5, 6, 4]; let k = 2; println!("The {}th largest element is {}", k, find_kth_largest(&mut nums, k)); }
这段 Rust 代码实现了快速选择算法,用于在 O(n) 的平均时间复杂度内找到数组中第 k 个最大的元素。代码中定义了 partition
函数用于根据枢轴值对数组进行划分,quick_select
函数用于递归地在划分的子数组中查找第 k 个最大的元素,以及 find_kth_largest
函数用于启动快速选择过程。主函数 main
提供了一个示例数组和 k 值,并调用 find_kth_largest
函数来输出结果。
总结
上面的解法采用了快速选择算法,这是一种基于快速排序的选择算法,用于在未完全排序的数组中查找第 k 个最小(或最大)元素的高效算法。其核心思想是:
- 选择枢轴(Pivot):随机选择一个元素作为枢轴。
- 分区(Partitioning):重新排列数组,使得所有小于枢轴的元素都在其左侧,而所有大于枢轴的元素都在其右侧。枢轴的最终位置就是它如果数组被排序后应该在的位置。
- 递归(Recursion):递归地在枢轴的左侧或右侧子数组中查找第 k 个最小(或最大)元素。
- 如果枢轴的位置恰好是 k,那么它就是我们要找的元素。
- 如果 k 小于枢轴的位置,我们只需要在左侧子数组中查找。
- 如果 k 大于枢轴的位置,我们只需要在右侧子数组中查找。
快速选择算法的平均时间复杂度为 O(n),但最坏情况下会退化到 O(n^2)。通过随机选择枢轴可以减少这种最坏情况发生的概率。
在上面的代码实现中,我们定义了以下函数:
partition
:用于实现分区逻辑。quick_select
:用于递归地在数组中查找第 k 个最小(或最大)元素。find_kth_largest
:用于调整 k 的值以适应快速选择算法,并开始查找过程。
最后,main
函数或相应的入口点提供了一个示例数组和 k 值,并调用 find_kth_largest
函数来输出第 k 个最大元素的值。
前 K 个高频元素
题目要求
你需要编写一个函数,该函数接收两个参数:一个整数数组 nums
和一个整数 k
。你的任务是找出数组 nums
中出现频率最高的 k
个元素,并返回这些元素。结果的输出顺序不做要求。
解题思路
要解决这个问题,我们可以采用以下步骤:
-
统计元素频率:首先,我们需要遍历数组
nums
,使用一个哈希表(或字典)来统计每个元素出现的次数。 -
构建频率数组:然后,我们可以根据元素的频率,将它们放入一个“频率数组”中,其中索引对应于频率,值是具有该频率的元素列表。
-
使用堆(优先队列):为了不遍历整个频率数组,我们可以使用一个最小堆(或优先队列)来维护当前最高的
k
个频率的元素。堆中的每个元素是一个频率和元素值的对。 -
遍历哈希表:遍历哈希表,对于每个元素,我们将其频率和它本身作为一个对插入到最小堆中。如果堆的大小超过了
k
,我们就弹出堆顶元素(即频率最小的元素)。 -
提取结果:最后,当我们遍历完哈希表后,堆中剩下的就是频率最高的
k
个元素。我们只需要将这些元素取出并返回即可。
这种方法的时间复杂度主要在于哈希表的构建(O(n)),以及最小堆的维护(O(n log k)),因此总的时间复杂度为 O(n log k)。这是一个相对高效的解法,特别是当 k
远小于 n
的时候。
Golang 版本
package main
import (
"container/heap"
"fmt"
)
// An IntHeap is a min-heap of ints.
type IntHeap []int
func (h IntHeap) Len() int { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *IntHeap) Push(x interface{}) {
*h = append(*h, x.(int))
}
func (h *IntHeap) Pop() interface{} {
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}
// Function to return the k most frequent elements
func topKFrequent(nums []int, k int) []int {
// Frequency map
freqMap := make(map[int]int)
for _, num := range nums {
freqMap[num]++
}
// Initialize min heap
h := &IntHeap{}
heap.Init(h)
// Map to keep track of frequency to elements
freqToNums := make(map[int][]int)
for num, freq := range freqMap {
freqToNums[freq] = append(freqToNums[freq], num)
if h.Len() < k {
heap.Push(h, freq)
} else if (*h)[0] < freq {
heap.Push(h, freq)
heap.Pop(h)
}
}
// Extract the top k frequent elements
var topK []int
for h.Len() > 0 {
freq := heap.Pop(h).(int)
topK = append(topK, freqToNums[freq]...)
}
return topK
}
func main() {
nums := []int{1, 1, 1, 2, 2, 3}
k := 2
fmt.Println(topKFrequent(nums, k)) // Output: [1 2]
}
这段代码定义了一个最小堆,并使用 Go 语言的标准库 container/heap
来实现。topKFrequent
函数首先统计每个数字的频率,然后使用最小堆来找出频率最高的 k
个数字。最后,它返回这些数字。在 main
函数中,我们调用 topKFrequent
函数并打印结果。
C++版本
#include <iostream>
#include <vector>
#include <unordered_map>
#include <queue>
#include <functional>
using namespace std;
vector<int> topKFrequent(vector<int>& nums, int k) {
// Build the frequency map
unordered_map<int, int> freqMap;
for (int num : nums) {
freqMap[num]++;
}
// Comparator for the min heap
auto comp = [&freqMap](int n1, int n2) { return freqMap[n1] > freqMap[n2]; };
priority_queue<int, vector<int>, decltype(comp)> minHeap(comp);
// Build the min heap with the most frequent elements
for (auto& [num, freq] : freqMap) {
minHeap.push(num);
if (minHeap.size() > k) {
minHeap.pop();
}
}
// Extract the top k elements from the min heap
vector<int> topK;
while (!minHeap.empty()) {
topK.push_back(minHeap.top());
minHeap.pop();
}
return topK;
}
int main() {
vector<int> nums = {1, 1, 1, 2, 2, 3};
int k = 2;
vector<int> topK = topKFrequent(nums, k);
for (int num : topK) {
cout << num << " ";
}
cout << endl;
return 0;
}
这段代码首先使用一个 unordered_map
来统计每个数字出现的频率。然后,它定义了一个最小堆(使用 priority_queue
),并提供了一个自定义的比较器,该比较器基于频率来比较元素。接着,它将元素插入到最小堆中,保持堆的大小为 k
。最后,它从最小堆中提取出前 k
个最频繁的元素,并将它们存储在一个向量中返回。在 main
函数中,我们调用 topKFrequent
函数并打印结果。
Python 版本
from collections import Counter
import heapq
def topKFrequent(nums, k):
# Count the frequency of each element
freq_map = Counter(nums)
# Use a heap to store the frequency and corresponding element
# Since heapq in Python is a min-heap, we use negative frequencies
heap = [(-freq, num) for num, freq in freq_map.items()]
heapq.heapify(heap)
# Extract the top k elements with the highest frequency
top_k = [heapq.heappop(heap)[1] for _ in range(k)]
return top_k
# Example usage:
nums = [1, 1, 1, 2, 2, 3]
k = 2
print(topKFrequent(nums, k)) # Output: [1, 2]
这段代码使用 Python 的 collections.Counter
类来统计每个元素的频率。然后,它创建了一个由元素和其负频率组成的最小堆。由于 Python 的 heapq
模块实现的是最小堆,为了得到频率最高的元素,我们将频率取负。最后,它从堆中弹出前 k
个元素,这些元素就是频率最高的 k
个元素。在示例用法中,我们调用 topKFrequent
函数并打印结果。
Java 版本
import java.util.*;
public class Solution {
public List<Integer> topKFrequent(int[] nums, int k) {
// Count the frequency for each element
Map<Integer, Integer> frequencyMap = new HashMap<>();
for (int num : nums) {
frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) + 1);
}
// Create a min heap with a comparator based on frequency
PriorityQueue<Integer> heap = new PriorityQueue<>(Comparator.comparingInt(frequencyMap::get));
// Keep k top frequent elements in the heap
for (int num : frequencyMap.keySet()) {
heap.add(num);
if (heap.size() > k) {
heap.poll();
}
}
// Extract the top k frequent elements
List<Integer> topK = new ArrayList<>();
while (!heap.isEmpty()) {
topK.add(heap.poll());
}
Collections.reverse(topK); // The smallest frequency is at the front, so reverse to get correct order
return topK;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {1, 1, 1, 2, 2, 3};
int k = 2;
List<Integer> topK = solution.topKFrequent(nums, k);
for (int num : topK) {
System.out.print(num + " ");
}
}
}
这段代码首先使用一个 HashMap
来统计每个元素的频率。然后,它创建了一个最小堆(使用 PriorityQueue
),并提供了一个基于频率的比较器。接着,它将元素插入到最小堆中,保持堆的大小为 k
。最后,它从最小堆中提取出前 k
个最频繁的元素,并将它们存储在一个列表中返回。在 main
方法中,我们创建了 Solution
类的实例,调用 topKFrequent
方法并打印结果。
Rust 版本
use std::collections::{BinaryHeap, HashMap}; use std::cmp::Reverse; fn top_k_frequent(nums: Vec<i32>, k: i32) -> Vec<i32> { let mut freq_map = HashMap::new(); // Count the frequency of each element for num in nums { *freq_map.entry(num).or_insert(0) += 1; } // By default Rust provides a max heap, so we use Reverse to turn it into a min heap let mut heap = BinaryHeap::new(); // Keep only the k most frequent elements in the heap for (num, &freq) in freq_map.iter() { heap.push(Reverse((freq, *num))); if heap.len() as i32 > k { heap.pop(); } } // Extract the elements from the heap heap.into_iter().map(|Reverse((_, num))| num).collect() } fn main() { let nums = vec![1, 1, 1, 2, 2, 3]; let k = 2; let top_k = top_k_frequent(nums, k); println!("{:?}", top_k); // Output should be [1, 2] or [2, 1] }
这段代码首先使用 Rust 的 HashMap
来统计每个元素的频率。然后,它创建了一个 BinaryHeap
,Rust 的 BinaryHeap
是一个最大堆,为了实现最小堆的效果,我们使用 Reverse
结构体来反转比较的顺序。接着,它将元素以 (频率, 元素)
的形式插入到堆中,并保持堆的大小为 k
。最后,它从堆中提取出所有元素,并只保留元素部分,形成一个向量返回。在 main
函数中,我们调用 top_k_frequent
函数并打印结果。
总结
上述解法的核心思想是使用一个哈希表来统计每个元素在数组中出现的频率,然后利用一个堆(优先队列)来维护出现频率最高的 k
个元素。
-
哈希表统计频率:遍历数组,使用哈希表记录每个元素出现的次数。
-
构建堆:根据元素的频率构建一个最小堆(在 Rust 中使用最大堆的
Reverse
特性来实现)。堆中存储的是一个包含频率和元素值的元组(或在其他语言中是对象或结构体)。 -
维护堆的大小:在将元素添加到堆中时,一旦堆的大小超过
k
,就移除堆顶元素,这样可以确保堆中始终保留频率最高的k
个元素。 -
提取结果:最后,从堆中提取所有元素,这些就是出现频率最高的
k
个元素。在 Rust 中,由于使用了最大堆的Reverse
特性,提取时不需要额外操作;而在其他语言中,可能需要将最小堆中的元素反转以得到正确的顺序。
这种解法的时间复杂度通常是 O(n log k),其中 n
是数组的长度,因为每次堆操作(插入或删除)的时间复杂度是 O(log k)。这种方法在 k
远小于 n
的情况下特别有效,因为它不需要对整个数组进行排序。
数据流的中位数
题目要求
设计一个 MedianFinder
类,该类能够:
- 通过
addNum(int num)
方法接收一个数据流中的整数num
并将其加入到内部数据结构中。 - 通过
findMedian()
方法计算并返回当前所有加入的整数的中位数。
中位数的定义是:在一个有序的整数列表中,位于中间位置的数。如果列表的长度是奇数,则中位数是中间的数;如果列表的长度是偶数,则中位数是中间两个数的平均值。
解题思路
为了实现 MedianFinder
类,我们需要一个有效的内部数据结构来存储输入的整数,并且能够快速计算中位数。以下是解决这个问题的思路:
-
数据存储:使用两个优先队列(通常实现为堆),一个最大堆来存储当前所有数中较小的一半,一个最小堆来存储较大的一半。这样可以保证最大堆的最大元素和最小堆的最小元素是所有数中间的两个数。
-
平衡堆:为了快速找到中位数,需要保持两个堆的大小平衡(或者最大堆比最小堆多一个元素,这样当元素总数为奇数时,最大堆的顶部就是中位数)。
-
添加元素:当添加一个新元素时,可以先将其加入到最大堆,然后将最大堆的顶部元素移动到最小堆。这样做是为了保持元素分布的平衡。如果最大堆的大小超过了最小堆的大小超过 1,就将最小堆的顶部元素移动到最大堆。
-
计算中位数:如果两个堆的大小相等,则中位数是两个堆顶元素的平均值;如果最大堆的元素多,则中位数是最大堆的堆顶元素。
通过这种方式,addNum
操作可以在对数时间内完成,findMedian
操作可以在常数时间内完成,因为只涉及到获取堆顶元素和计算平均值。
Golang 版本
import (
"container/heap"
)
// An IntHeap is a min-heap of ints.
type IntHeap []int
func (h IntHeap) Len() int { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *IntHeap) Push(x interface{}) {
*h = append(*h, x.(int))
}
func (h *IntHeap) Pop() interface{} {
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}
// An IntMaxHeap is a max-heap of ints.
type IntMaxHeap struct {
IntHeap
}
func (h IntMaxHeap) Less(i, j int) bool { return h.IntHeap[i] > h.IntHeap[j] }
// MedianFinder holds two heaps.
type MedianFinder struct {
minHeap IntHeap
maxHeap IntMaxHeap
}
/** initialize your data structure here. */
func Constructor() MedianFinder {
return MedianFinder{
minHeap: make(IntHeap, 0),
maxHeap: IntMaxHeap{make(IntHeap, 0)},
}
}
func (this *MedianFinder) AddNum(num int) {
heap.Push(&this.maxHeap, num)
heap.Push(&this.minHeap, heap.Pop(&this.maxHeap))
if this.minHeap.Len() > this.maxHeap.Len() {
heap.Push(&this.maxHeap, heap.Pop(&this.minHeap))
}
}
func (this *MedianFinder) FindMedian() float64 {
if this.maxHeap.Len() > this.minHeap.Len() {
return float64(this.maxHeap.IntHeap[0])
}
return float64(this.minHeap.IntHeap[0]+this.maxHeap.IntHeap[0]) / 2.0
}
/**
* Your MedianFinder object will be instantiated and called as such:
* obj := Constructor();
* obj.AddNum(num);
* param_2 := obj.FindMedian();
*/
在这段代码中,我们定义了两个类型 IntHeap
和 IntMaxHeap
来实现最小堆和最大堆。MedianFinder
结构体包含了一个最小堆和一个最大堆,用于存储数据流中的数字。AddNum
方法将一个新的数字加入到合适的堆中,并保持两个堆的平衡。FindMedian
方法根据两个堆的顶部元素计算出中位数。
C++版本
#include <queue>
#include <vector>
#include <functional>
class MedianFinder {
private:
// 大根堆,存储较小的一半元素
std::priority_queue<int, std::vector<int>, std::less<int>> maxHeap;
// 小根堆,存储较大的一半元素
std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;
public:
/** initialize your data structure here. */
MedianFinder() {
}
void addNum(int num) {
// 先加入大根堆,再将大根堆的最大元素加入小根堆
maxHeap.push(num);
minHeap.push(maxHeap.top());
maxHeap.pop();
// 如果小根堆元素多,移动到大根堆
if (minHeap.size() > maxHeap.size()) {
maxHeap.push(minHeap.top());
minHeap.pop();
}
}
double findMedian() {
// 如果两个堆的大小相等,则取平均值;否则取大根堆的顶部元素
if (maxHeap.size() > minHeap.size()) {
return maxHeap.top();
} else {
return (maxHeap.top() + minHeap.top()) * 0.5;
}
}
};
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder* obj = new MedianFinder();
* obj->addNum(num);
* double param_2 = obj->findMedian();
*/
在这段 C++ 代码中,我们使用了 STL 中的 priority_queue
来实现最大堆和最小堆。MedianFinder
类中包含两个 priority_queue
,一个作为最大堆(maxHeap
),用于存储较小的一半元素,另一个作为最小堆(minHeap
),用于存储较大的一半元素。addNum
方法负责将新元素加入到适当的堆中,并保持两个堆的平衡。findMedian
方法则根据两个堆的顶部元素来计算当前的中位数。
Python 版本
from heapq import *
class MedianFinder:
def __init__(self):
# 小顶堆,存储较大的一半元素
self.minHeap = []
# 大顶堆,存储较小的一半元素,我们通过存储相反数来实现
self.maxHeap = []
def addNum(self, num):
# 先加入大顶堆,再将大顶堆的最大元素加入小顶堆
heappush(self.maxHeap, -num)
heappush(self.minHeap, -heappop(self.maxHeap))
# 如果小顶堆元素多,移动到大顶堆
if len(self.minHeap) > len(self.maxHeap):
heappush(self.maxHeap, -heappop(self.minHeap))
def findMedian(self):
# 如果两个堆的大小相等,则取平均值;否则取大顶堆的顶部元素
if len(self.minHeap) == len(self.maxHeap):
return (-self.maxHeap[0] + self.minHeap[0]) / 2.0
else:
return -self.maxHeap[0]
# Your MedianFinder object will be instantiated and called as such:
# obj = MedianFinder()
# obj.addNum(num)
# param_2 = obj.findMedian()
在这段 Python 代码中,我们使用了 Python 内置的heapq
模块来实现最小堆。为了实现最大堆,我们将元素的符号取反后存入最小堆中。MedianFinder
类中包含两个列表,一个表示最小堆(minHeap
),一个表示最大堆(maxHeap
)。addNum
方法用于将新元素添加到合适的堆中,并保持两个堆的平衡。findMedian
方法根据两个堆的顶部元素来计算当前的中位数。
Java 版本
import java.util.Collections;
import java.util.PriorityQueue;
public class MedianFinder {
private PriorityQueue<Integer> minHeap;
private PriorityQueue<Integer> maxHeap;
/** initialize your data structure here. */
public MedianFinder() {
// 小顶堆,存储较大的一半元素
minHeap = new PriorityQueue<>();
// 大顶堆,存储较小的一半元素
maxHeap = new PriorityQueue<>(Collections.reverseOrder());
}
public void addNum(int num) {
// 先加入大顶堆,再将大顶堆的最大元素加入小顶堆
maxHeap.offer(num);
minHeap.offer(maxHeap.poll());
// 如果小顶堆元素多,移动到大顶堆
if (minHeap.size() > maxHeap.size()) {
maxHeap.offer(minHeap.poll());
}
}
public double findMedian() {
// 如果两个堆的大小相等,则取平均值;否则取大顶堆的顶部元素
if (minHeap.size() == maxHeap.size()) {
return (minHeap.peek() + maxHeap.peek()) / 2.0;
} else {
return maxHeap.peek();
}
}
}
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder obj = new MedianFinder();
* obj.addNum(num);
* double param_2 = obj.findMedian();
*/
在这段 Java 代码中,我们使用了 Java 的PriorityQueue
类来实现最小堆和最大堆。PriorityQueue
默认实现的是最小堆,为了实现最大堆,我们在构造时传入了Collections.reverseOrder()
比较器。MedianFinder
类中包含两个PriorityQueue
,一个表示最小堆(minHeap
),一个表示最大堆(maxHeap
)。addNum
方法用于将新元素添加到合适的堆中,并保持两个堆的平衡。findMedian
方法根据两个堆的顶部元素来计算当前的中位数。
Rust 版本
use std::collections::BinaryHeap; use std::cmp::Reverse; struct MedianFinder { min_heap: BinaryHeap<Reverse<i32>>, // 小顶堆,存储较大的一半元素 max_heap: BinaryHeap<i32>, // 大顶堆,存储较小的一半元素 } impl MedianFinder { /** initialize your data structure here. */ fn new() -> Self { MedianFinder { min_heap: BinaryHeap::new(), max_heap: BinaryHeap::new(), } } fn add_num(&mut self, num: i32) { // 先加入大顶堆,再将大顶堆的最大元素加入小顶堆 self.max_heap.push(num); let max_heap_top = self.max_heap.pop().unwrap(); self.min_heap.push(Reverse(max_heap_top)); // 如果小顶堆元素多,移动到大顶堆 if self.min_heap.len() > self.max_heap.len() { let Reverse(min_heap_top) = self.min_heap.pop().unwrap(); self.max_heap.push(min_heap_top); } } fn find_median(&self) -> f64 { if self.min_heap.len() == self.max_heap.len() { // 如果两个堆的大小相等,则取平均值 let Reverse(min_heap_top) = *self.min_heap.peek().unwrap(); let max_heap_top = *self.max_heap.peek().unwrap(); (min_heap_top as f64 + max_heap_top as f64) / 2.0 } else { // 否则取大顶堆的顶部元素 *self.max_heap.peek().unwrap() as f64 } } } fn main() { let mut finder = MedianFinder::new(); finder.add_num(1); finder.add_num(2); println!("Median is {}", finder.find_median()); // 输出中位数 1.5 finder.add_num(3); println!("Median is {}", finder.find_median()); // 输出中位数 2 }
在这段 Rust 代码中,我们使用了 Rust 标准库中的BinaryHeap
来实现最大堆。为了实现最小堆,我们使用了Reverse
包装器来改变元素的排序方式。MedianFinder
结构体包含两个BinaryHeap
,一个表示最小堆(min_heap
),一个表示最大堆(max_heap
)。add_num
方法用于将新元素添加到合适的堆中,并保持两个堆的平衡。find_median
方法根据两个堆的顶部元素来计算当前的中位数。在main
函数中,我们创建了一个MedianFinder
实例,并添加了一些数字来演示如何计算中位数。
总结
上述 Rust 版本的解法中,我们实现了一个MedianFinder
结构体,它使用两个二叉堆来有效地计算动态整数集合的中位数。这种方法的核心思想是保持两个堆的元素数量平衡或接近平衡,这样中位数就可以容易地从堆顶元素中得到。
- 最大堆(
max_heap
):存储当前所有元素中较小的一半,使用 Rust 的BinaryHeap
直接实现。 - 最小堆(
min_heap
):存储当前所有元素中较大的一半,通过对元素应用Reverse
包装器来逆转BinaryHeap
的默认行为,从而实现最小堆。
添加元素(add_num
方法):
- 新元素首先被添加到最大堆中。
- 然后将最大堆的顶部元素(即最大元素)移动到最小堆中。
- 如果最小堆的元素数量超过了最大堆,将最小堆的顶部元素(即最小元素)移回最大堆。 这样做可以保证最大堆的所有元素都小于或等于最小堆的所有元素,并且两个堆的大小差不会超过 1。
计算中位数(find_median
方法):
- 如果两个堆的大小相等,中位数是两个堆顶元素的平均值。
- 如果两个堆的大小不等,中位数是元素较多的堆的顶部元素。
这种方法的时间复杂度是:
- 添加元素:O(log n),因为需要将元素插入堆中。
- 查找中位数:O(1),因为直接访问堆顶元素。
这个解法适用于数据流中位数的场景,因为它可以在接收每个新元素后立即计算中位数。
贪心算法
贪心算法是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是全局最好或最优的算法。贪心算法解决问题的通用思路可以概括为以下几个步骤:
-
**建模:**将问题抽象成数学模型,明确问题的约束条件和优化目标。
-
**选择标准:**确定贪心策略,即如何在每一步做出局部最优选择。这通常涉及到对问题的深入理解和对可能解空间的分析。
-
**证明贪心策略的正确性:**通过数学归纳法或反证法等证明局部最优解能够导致全局最优解。
-
**实现算法:**根据贪心策略编写代码,实现算法的具体步骤。
-
**测试和验证:**通过测试案例验证算法的正确性和效率。
下面是一个使用 Go 语言实现的贪心算法的例子。这个例子是解决经典的硬币找零问题,即给定不同面额的硬币和一个总金额,要求用最少的硬币凑成总金额。
package main
import (
"fmt"
"sort"
)
func coinChange(coins []int, amount int) int {
// 对硬币面额进行排序,从大到小
sort.Slice(coins, func(i, j int) bool {
return coins[i] > coins[j]
})
totalCoins := 0 // 使用的硬币数量
for _, coin := range coins {
// 使用当前面额的硬币数量
count := amount / coin
amount -= count * coin
totalCoins += count
// 如果已经凑够了总金额,直接返回结果
if amount == 0 {
return totalCoins
}
}
// 如果不能凑够总金额,返回-1
if amount > 0 {
return -1
}
return totalCoins
}
func main() {
coins := []int{1, 2, 5, 10, 20, 50}
amount := 99
fmt.Printf("Minimum coins needed: %d\n", coinChange(coins, amount))
}
在这个例子中,我们首先对硬币面额进行了排序,确保可以先使用大面额的硬币,这是贪心策略的一部分。然后,我们遍历每种硬币,尽可能多地使用当前面额的硬币,直到无法再使用该面额的硬币为止。如果最终能够凑齐总金额,则返回使用的硬币数量;如果不能,则返回-1。
需要注意的是,贪心算法并不总是能得到全局最优解,它的适用性依赖于问题本身的性质。在实际应用中,我们需要通过数学证明或经验判断来确定贪心策略是否适用于特定的问题。
以下是几个不同类型的贪心算法问题的 Go 语言实现例子:
例子 1:活动选择问题
假设你有一个活动列表,每个活动都有一个开始时间和结束时间。你的目标是选择最大数量的活动,使得它们不相互重叠。
package main
import (
"fmt"
"sort"
)
type Activity struct {
Start, Finish int
}
// 按照活动结束时间排序
func activitySelection(activities []Activity) []Activity {
sort.Slice(activities, func(i, j int) bool {
return activities[i].Finish < activities[j].Finish
})
var result []Activity
lastSelectedIndex := 0
result = append(result, activities[lastSelectedIndex])
for i := 1; i < len(activities); i++ {
if activities[i].Start >= activities[lastSelectedIndex].Finish {
result = append(result, activities[i])
lastSelectedIndex = i
}
}
return result
}
func main() {
activities := []Activity{
{Start: 1, Finish: 2},
{Start: 3, Finish: 4},
{Start: 0, Finish: 6},
{Start: 5, Finish: 7},
{Start: 8, Finish: 9},
{Start: 5, Finish: 9},
}
selectedActivities := activitySelection(activities)
for _, activity := range selectedActivities {
fmt.Printf("Activity starts at: %d and finishes at: %d\n", activity.Start, activity.Finish)
}
}
例子 2:分数背包问题
假设你是一个小偷,背着一个背包,背包有一个最大容量。你有一系列物品,每个物品都有重量和价值,你可以拿走物品的一部分。你的目标是在不超过背包容量的情况下,使得背包中物品的总价值最大。
package main
import (
"fmt"
"sort"
)
type Item struct {
Weight, Value float64
}
// 按照单位价值排序
func fractionalKnapsack(items []Item, capacity float64) float64 {
sort.Slice(items, func(i, j int) bool {
return (items[i].Value / items[i].Weight) > (items[j].Value / items[j].Weight)
})
totalValue := 0.0
for _, item := range items {
if capacity > item.Weight {
capacity -= item.Weight
totalValue += item.Value
} else {
totalValue += item.Value * (capacity / item.Weight)
break
}
}
return totalValue
}
func main() {
items := []Item{
{Weight: 10, Value: 60},
{Weight: 20, Value: 100},
{Weight: 30, Value: 120},
}
capacity := 50.0
maxValue := fractionalKnapsack(items, capacity)
fmt.Printf("Maximum value we can obtain = %f\n", maxValue)
}
例子 3:最小的字典序
给定一个字符串,通过删除一些字符,使得剩下的字符串按字典序最小。
package main
import (
"fmt"
)
func smallestSubsequence(s string) string {
stack := []byte{}
seen := make(map[byte]bool)
lastOccurrence := make(map[byte]int)
for i := range s {
lastOccurrence[s[i]] = i
}
for i := range s {
if seen[s[i]] {
continue
}
for len(stack) > 0 && stack[len(stack)-1] > s[i] && lastOccurrence[stack[len(stack)-1]] > i {
seen[stack[len(stack)-1]] = false
stack = stack[:len(stack)-1]
}
stack = append(stack, s[i])
seen[s[i]] = true
}
return string(stack)
}
func main() {
s := "cbacdcbc"
fmt.Println("Smallest subsequence is:", smallestSubsequence(s))
}
在这些例子中,贪心策略的选择是关键:在活动选择问题中,我们选择结束时间最早的活动;在分数背包问题中,我们选择单位价值最高的物品;在最小字典序问题中,我们选择在保持字典序最小的同时,能够保留的字符。这些策略都是基于问题的特定性质,而且通常需要通过数学证明来验证它们是否能够确保得到全局最优解。
买卖股票的最佳时机
题目要求
给定一个数组 prices
,其中 prices[i]
表示第 i
天的某支股票的价格。你的任务是找出在哪一天买入并在未来的某一天卖出能够获得的最大利润。你只能进行一次买入和一次卖出操作。如果无法获得利润,则返回 0。
解题思路
要解决这个问题,我们可以遵循以下步骤:
-
初始化两个变量,一个用于存储到目前为止的最小股价
minPrice
,另一个用于存储到目前为止的最大利润maxProfit
。 -
遍历
prices
数组,对于每一天的价格prices[i]
:- 更新
minPrice
为当前价格和minPrice
中的较小值。 - 计算如果在
minPrice
买入,在当前价格prices[i]
卖出的利润,即prices[i] - minPrice
。 - 更新
maxProfit
为当前计算出的利润和maxProfit
中的较大值。
- 更新
-
遍历完成后,
maxProfit
将包含可能的最大利润。如果maxProfit
大于 0,则返回该值;否则,返回 0。
这个问题的关键在于找到最低购买点和最高卖出点的组合,但这两个点不需要是数组中的连续元素,只要保证卖出的时间在买入之后即可。通过一次遍历即可找到最佳买卖时机,保证了算法的效率。
Golang 版本
package main
import (
"fmt"
)
func maxProfit(prices []int) int {
if len(prices) == 0 {
return 0
}
minPrice := prices[0]
maxProfit := 0
for _, price := range prices {
if price < minPrice {
minPrice = price
} else if profit := price - minPrice; profit > maxProfit {
maxProfit = profit
}
}
return maxProfit
}
func main() {
prices := []int{7, 1, 5, 3, 6, 4}
fmt.Println("Maximum profit:", maxProfit(prices))
}
这段代码定义了一个 maxProfit
函数,它接受一个整数数组 prices
作为参数,并返回一个整数,表示可以获得的最大利润。main
函数中给出了一个示例数组,并打印出调用 maxProfit
函数后的结果。
C++版本
#include <iostream>
#include <vector>
#include <algorithm>
int maxProfit(const std::vector<int>& prices) {
if (prices.empty()) return 0;
int minPrice = prices[0];
int maxProfit = 0;
for (int price : prices) {
minPrice = std::min(minPrice, price);
maxProfit = std::max(maxProfit, price - minPrice);
}
return maxProfit;
}
int main() {
std::vector<int> prices{7, 1, 5, 3, 6, 4};
std::cout << "Maximum profit: " << maxProfit(prices) << std::endl;
return 0;
}
这段代码定义了一个 maxProfit
函数,它接受一个整数类型的 std::vector
作为参数,并返回一个整数,表示可以获得的最大利润。main
函数中给出了一个示例数组,并打印出调用 maxProfit
函数后的结果。
Python 版本
def max_profit(prices):
if not prices:
return 0
min_price = prices[0]
max_profit = 0
for price in prices:
min_price = min(min_price, price)
max_profit = max(max_profit, price - min_price)
return max_profit
# Example usage:
prices = [7, 1, 5, 3, 6, 4]
print("Maximum profit:", max_profit(prices))
Java 版本
public class StockProfitCalculator {
public static int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int minPrice = prices[0];
int maxProfit = 0;
for (int price : prices) {
minPrice = Math.min(minPrice, price);
maxProfit = Math.max(maxProfit, price - minPrice);
}
return maxProfit;
}
public static void main(String[] args) {
int[] prices = {7, 1, 5, 3, 6, 4};
System.out.println("Maximum profit: " + maxProfit(prices));
}
}
这段代码定义了一个 StockProfitCalculator
类,其中包含一个 maxProfit
静态方法,用于计算并返回给定股票价格数组的最大利润。main
方法提供了一个示例数组,并打印出调用 maxProfit
方法后的结果。
Rust 版本
fn max_profit(prices: Vec<i32>) -> i32 { let mut min_price = i32::MAX; let mut max_profit = 0; for &price in prices.iter() { if price < min_price { min_price = price; } else { max_profit = max_profit.max(price - min_price); } } max_profit } fn main() { let prices = vec![7, 1, 5, 3, 6, 4]; println!("Maximum profit: {}", max_profit(prices)); }
这段代码定义了一个 max_profit
函数,它接受一个 Vec<i32>
类型的参数 prices
,并返回一个 i32
类型的值,表示可以获得的最大利润。main
函数中给出了一个示例向量,并打印出调用 max_profit
函数后的结果。
总结
上述问题的解法遵循一个简单的逻辑:在遍历数组的过程中,记录并更新到目前为止遇到的最低价格和最大利润。具体步骤如下:
- 初始化两个变量:
min_price
为最大可能的股票价格,max_profit
为 0。 - 遍历价格数组
prices
:- 对于每个价格
price
,如果price
小于min_price
,则更新min_price
为当前price
。 - 否则,计算当前
price
与min_price
之差,即当前可能的利润,并更新max_profit
为当前可能利润和max_profit
中的较大值。
- 对于每个价格
- 遍历完成后,
max_profit
将包含可能的最大利润。
这种方法的关键在于,它只需要一次遍历(时间复杂度为 O(n)),无需使用嵌套循环,因此效率较高。在不同的编程语言中,这个算法的实现思路是一致的,只是语法和一些函数的使用有所不同。
跳跃游戏
题目要求
给定一个非负整数数组nums
,其中每个元素表示从当前位置出发,能够跳跃的最大长度。你的任务是判断是否能够从数组的第一个元素开始,跳跃至数组的最后一个元素。如果能够到达数组的最后一个下标,则返回true
;如果不能到达,则返回false
。
解题思路
要解决这个问题,我们可以采用贪心算法的思想。具体的方法是:
-
初始化一个变量,比如命名为
maxReach
,用来记录在当前位置或之前的位置,我们能够到达的最远距离。初始时,maxReach
等于第一个元素的值,因为我们最初位于数组的第一个下标。 -
遍历数组
nums
,对于每一个位置,我们更新maxReach
。更新的方法是比较当前的maxReach
和当前下标 + nums[当前下标]
的值,取二者之间的较大值作为新的maxReach
。 -
在遍历的过程中,如果某一时刻
maxReach
已经大于或等于数组的最后一个下标,那么说明我们可以到达数组的最后一个元素,返回true
。 -
如果在遍历结束时,
maxReach
小于数组的最后一个下标,那么说明我们无法到达数组的最后一个元素,返回false
。 -
特别注意的是,在遍历过程中,如果当前下标大于
maxReach
,说明当前位置已经不可达,直接返回false
。
通过上述步骤,我们可以在遍历一次数组的过程中,得出是否能够到达数组的最后一个下标的结论。
Golang 版本
package main
import "fmt"
func canJump(nums []int) bool {
maxReach := 0
for i, num := range nums {
if i > maxReach {
return false
}
maxReach = max(maxReach, i+num)
if maxReach >= len(nums)-1 {
return true
}
}
return false
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func main() {
nums := []int{2, 3, 1, 1, 4}
fmt.Println(canJump(nums)) // 输出: true
}
C++版本
#include <iostream>
#include <vector>
using namespace std;
bool canJump(vector<int>& nums) {
int maxReach = 0;
for (int i = 0; i < nums.size(); ++i) {
if (i > maxReach) {
return false;
}
maxReach = max(maxReach, i + nums[i]);
if (maxReach >= nums.size() - 1) {
return true;
}
}
return false;
}
int main() {
vector<int> nums = {2, 3, 1, 1, 4};
cout << (canJump(nums) ? "true" : "false") << endl; // 输出: true
return 0;
}
Python 版本
def can_jump(nums):
max_reach = 0
for i, num in enumerate(nums):
if i > max_reach:
return False
max_reach = max(max_reach, i + num)
if max_reach >= len(nums) - 1:
return True
return False
# 示例
nums = [2, 3, 1, 1, 4]
print(can_jump(nums)) # 输出: True
Java 版本
public class Solution {
public boolean canJump(int[] nums) {
int maxReach = 0;
for (int i = 0; i < nums.length; i++) {
if (i > maxReach) {
return false;
}
maxReach = Math.max(maxReach, i + nums[i]);
if (maxReach >= nums.length - 1) {
return true;
}
}
return false;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {2, 3, 1, 1, 4};
System.out.println(solution.canJump(nums)); // 输出: true
}
}
Rust 版本
fn can_jump(nums: Vec<i32>) -> bool { let mut max_reach = 0; for (i, &num) in nums.iter().enumerate() { if i > max_reach { return false; } max_reach = std::cmp::max(max_reach, i + num as usize); if max_reach >= nums.len() - 1 { return true; } } false } fn main() { let nums = vec![2, 3, 1, 1, 4]; println!("{}", can_jump(nums)); // 输出: true }
总结
上述问题的解法核心在于使用贪心算法。无论是在 Go、C++、Python、Java 还是 Rust 版本的代码中,核心思想都是相同的:
-
定义一个变量(例如
maxReach
),用于记录在遍历数组过程中,能够到达的最远位置。 -
遍历数组中的每个元素,对于每个位置,更新
maxReach
。更新规则是取当前maxReach
和当前位置下标 + 当前位置的跳跃长度
两者之间的较大值。 -
在遍历过程中,如果发现某个位置已经不可达(即当前位置的下标大于
maxReach
),则直接返回false
。 -
如果在任意时刻
maxReach
已经大于或等于数组的最后一个下标,说明可以到达数组的末尾,返回true
。 -
如果遍历结束后,没有返回
true
,则意味着无法到达数组的末尾,返回false
。
这种方法的时间复杂度为 O(n),因为它只需要遍历数组一次。空间复杂度为 O(1),因为只使用了有限的几个变量。这种贪心算法的优势在于它的效率和简洁性,适用于各种编程语言实现。
跳跃游戏 II
题目要求
给定一个非负整数数组 nums
,数组的长度为 n
,数组的索引从 0
开始。数组中的每个元素 nums[i]
表示从位置 i
出发,能够向前跳跃的最大长度。在位置 i
,可以跳跃到位置 i + j
,其中 0 <= j <= nums[i]
且 i + j < n
。目标是计算从数组的起始位置 nums[0]
跳跃到数组的最后一个位置 nums[n - 1]
所需的最小跳跃次数。题目保证所有生成的测试用例都能够到达最后一个位置。
解题思路
这个问题可以通过贪心算法来解决。贪心算法的核心思想是,在每一步跳跃中,尽可能地跳到能够使得后续跳跃选择最多的位置。具体的解题步骤如下:
-
初始化当前能够到达的最远位置
maxReach
为nums[0]
,初始化当前跳跃的最远位置end
为nums[0]
,初始化跳跃次数jumps
为1
(因为至少需要跳跃一次)。 -
遍历数组
nums
,从索引1
到n - 1
:- 对于每个位置
i
,更新能够到达的最远位置maxReach
,即maxReach = max(maxReach, i + nums[i])
。 - 如果当前位置
i
等于当前跳跃的最远位置end
,说明需要进行一次新的跳跃:- 更新跳跃次数
jumps
,即jumps++
。 - 更新当前跳跃的最远位置
end
为当前能够到达的最远位置maxReach
。
- 更新跳跃次数
- 如果在遍历过程中
end
已经到达或超过了最后一个位置n - 1
,则可以停止遍历,因为已经可以到达最后一个位置。
- 对于每个位置
-
遍历完成后,
jumps
就是到达最后一个位置所需的最小跳跃次数。
这种方法的时间复杂度是 O(n),因为它只需要遍历数组一次。空间复杂度是 O(1),因为它只需要常数级别的额外空间。
Golang 版本
package main
import (
"fmt"
)
func jump(nums []int) int {
n := len(nums)
if n < 2 {
return 0
}
maxReach, end, jumps := nums[0], nums[0], 1
for i := 1; i < n; i++ {
if i > maxReach {
break
}
maxReach = max(maxReach, i+nums[i])
if i == end {
if i != n-1 {
jumps++
end = maxReach
}
}
}
return jumps
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func main() {
nums := []int{2, 3, 1, 1, 4}
fmt.Println("Minimum number of jumps to reach end is:", jump(nums))
}
这段代码定义了一个 jump
函数,它接受一个整数数组 nums
作为输入,并返回到达数组末尾所需的最小跳跃次数。main
函数中给出了一个示例数组,并打印出调用 jump
函数的结果。
C++版本
#include <iostream>
#include <vector>
#include <algorithm>
int jump(std::vector<int>& nums) {
int n = nums.size();
if (n < 2) return 0;
int maxReach = nums[0];
int step = nums[0];
int jump = 1;
for (int i = 1; i < n; ++i) {
if (i == n - 1) {
return jump;
}
maxReach = std::max(maxReach, i + nums[i]);
step--;
if (step == 0) {
jump++;
if (i >= maxReach) {
return -1; // Cannot move forward from current position
}
step = maxReach - i;
}
}
return jump;
}
int main() {
std::vector<int> nums = {2, 3, 1, 1, 4};
std::cout << "Minimum number of jumps to reach end is: " << jump(nums) << std::endl;
return 0;
}
这段 C++代码实现了一个 jump
函数,它接受一个整数类型的 vector
作为输入,并返回到达 vector
末尾所需的最小跳跃次数。main
函数中提供了一个示例 vector
并打印出调用 jump
函数的结果。代码中使用了 <algorithm>
头文件中的 std::max
函数来计算最远能到达的位置。
Python 版本
def jump(nums):
if len(nums) < 2:
return 0
max_reach, step, jump = nums[0], nums[0], 1
for i in range(1, len(nums)):
if max_reach >= len(nums) - 1:
return jump
if i > max_reach:
break
max_reach = max(max_reach, i + nums[i])
step -= 1
if step == 0:
jump += 1
step = max_reach - i
return jump
# Example usage:
nums = [2, 3, 1, 1, 4]
print("Minimum number of jumps to reach end is:", jump(nums))
这段 Python 代码定义了一个 jump
函数,它接受一个列表 nums
作为输入,并返回到达列表末尾所需的最小跳跃次数。在示例使用中,给出了一个列表并打印出调用 jump
函数的结果。
Java 版本
public class Solution {
public int jump(int[] nums) {
if (nums.length < 2) return 0;
int maxReach = nums[0];
int step = nums[0];
int jump = 1;
for (int i = 1; i < nums.length; i++) {
if (i == nums.length - 1) {
return jump;
}
maxReach = Math.max(maxReach, i + nums[i]);
step--;
if (step == 0) {
jump++;
step = maxReach - i;
}
}
return jump;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {2, 3, 1, 1, 4};
System.out.println("Minimum number of jumps to reach end is: " + solution.jump(nums));
}
}
这段 Java 代码定义了一个 Solution
类,其中包含一个 jump
方法,用于计算并返回到达数组末尾所需的最小跳跃次数。main
方法中创建了一个 Solution
对象,并使用一个示例数组来调用 jump
方法,然后打印出结果。
Rust 版本
fn jump(nums: Vec<i32>) -> i32 { let n = nums.len(); if n < 2 { return 0; } let (mut max_reach, mut step, mut jump) = (nums[0] as usize, nums[0] as usize, 1); for i in 1..n { if i == n - 1 { return jump; } max_reach = max_reach.max(i + nums[i] as usize); step -= 1; if step == 0 { jump += 1; if i >= max_reach { return -1; // Cannot move forward from current position } step = max_reach - i; } } jump } fn main() { let nums = vec![2, 3, 1, 1, 4]; println!("Minimum number of jumps to reach end is: {}", jump(nums)); }
这段 Rust 代码定义了一个 jump
函数,它接受一个 Vec<i32>
类型的参数 nums
并返回一个 i32
类型的结果,表示到达数组末尾所需的最小跳跃次数。main
函数中提供了一个示例数组,并打印出调用 jump
函数的结果。在 Rust 中,usize
类型用于索引和某些整数操作,因此在处理索引时将 i32
类型的数组元素转换为 usize
。
总结
上述代码段展示了如何解决一个典型的跳跃游戏问题,即给定一个非负整数数组 nums
,每个元素表示你在该位置可以跳跃的最大长度,目标是以最少的跳跃次数到达数组的最后一个位置。
解决方案的核心思想是使用贪心算法,通过以下步骤实现:
-
初始化三个变量:
max_reach
表示当前能够到达的最远位置,step
表示在不增加跳跃次数的情况下还能走的最远距离,jump
表示已经跳跃的次数。 -
遍历数组,对于每个位置:
- 更新
max_reach
为当前位置加上该位置能跳的最大长度和当前max_reach
的较大值。 - 每向前移动一步,
step
减一。 - 当
step
为零时,说明需要进行新的跳跃,因此jump
加一,并更新step
为当前max_reach
减去当前位置索引i
的值。
- 更新
-
当遍历到数组的最后一个位置时,返回
jump
作为结果。
这种方法的关键在于,它不需要遍历所有的跳跃路径,而是在每一步都做出局部最优的决策,从而保证了总体上的最优解。
在不同的编程语言版本中,这个算法的逻辑保持一致,只是语法和一些细节处理上有所不同。例如,在 Rust 中,索引操作使用 usize
类型,而在 Python 和 Java 中则直接使用整数类型。在 C++ 中,使用 std::max
函数来比较和更新最大值,而在 Python 中则使用内置的 max
函数。尽管实现的语言不同,但算法的核心思想和步骤是相同的。
划分字母区间
题目要求
编写一个算法,输入为一个字符串s
。目标是将字符串s
划分为尽可能多的子串,每个子串中的字符都是唯一的,即同一个字符不能在多个子串中出现。同时,这些子串连接起来后,应该能够还原成原始的字符串s
。
算法的输出应该是一个整数列表,列表中的每个元素代表每个子串的长度。
解题思路
为了解决这个问题,我们可以遵循以下步骤:
-
创建字符最后出现位置的映射:首先遍历字符串
s
,记录每个字符最后一次出现的索引位置。这将帮助我们确定每个字符能够存在的最远边界。 -
遍历字符串并划分子串:再次遍历字符串
s
,这次我们将维护两个变量,一个是当前子串的起始位置start
,另一个是当前子串的结束位置end
。对于每个遍历到的字符,我们更新end
为当前字符最后出现位置和end
的较大值。 -
确定子串的边界:当遍历到的字符索引等于当前
end
时,说明我们找到了一个子串的边界。这时,我们可以将子串的长度(end - start + 1
)添加到结果列表中,并更新start
为当前end
的下一个位置,继续寻找下一个子串。 -
重复步骤 2 和 3:继续遍历直到字符串
s
的末尾,每次找到子串边界时,都将子串长度添加到结果列表中。 -
返回结果:遍历完成后,返回存储子串长度的列表作为最终结果。
通过以上步骤,我们可以确保每个字符只出现在一个子串中,并且子串的连接顺序与原始字符串s
相同。
Golang 版本
package main
import (
"fmt"
)
func partitionLabels(s string) []int {
last := make(map[rune]int)
for i, c := range s {
last[c] = i
}
var result []int
start, end := 0, 0
for i, c := range s {
if last[c] > end {
end = last[c]
}
if i == end {
result = append(result, end-start+1)
start = i + 1
}
}
return result
}
func main() {
s := "ababcbacadefegdehijhklij"
result := partitionLabels(s)
fmt.Println(result)
}
这段代码实现了上述的算法思路。首先,它创建了一个映射来记录每个字符最后出现的位置。然后,它遍历字符串,使用start
和end
变量来追踪当前子串的边界,并在找到子串边界时更新结果列表。最后,它输出了每个子串的长度。
C++版本
#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>
std::vector<int> partitionLabels(std::string s) {
std::unordered_map<char, int> last;
for (int i = 0; i < s.length(); ++i) {
last[s[i]] = i;
}
std::vector<int> partitions;
int start = 0, end = 0;
for (int i = 0; i < s.length(); ++i) {
end = std::max(end, last[s[i]]);
if (i == end) {
partitions.push_back(end - start + 1);
start = i + 1;
}
}
return partitions;
}
int main() {
std::string s = "ababcbacadefegdehijhklij";
std::vector<int> result = partitionLabels(s);
for (int len : result) {
std::cout << len << " ";
}
std::cout << std::endl;
return 0;
}
这段 C++代码实现了相同的逻辑:首先创建一个哈希表来记录每个字符最后出现的位置,然后遍历字符串,使用start
和end
来确定每个片段的边界,并将片段的长度添加到结果向量中。最后,主函数中打印出每个片段的长度。
Python 版本
def partition_labels(s: str) -> [int]:
last = {c: i for i, c in enumerate(s)}
start = end = 0
result = []
for i, c in enumerate(s):
end = max(end, last[c])
if i == end:
result.append(end - start + 1)
start = i + 1
return result
if __name__ == "__main__":
s = "ababcbacadefegdehijhklij"
print(partition_labels(s))
这段 Python 代码遵循了之前描述的算法思路:首先创建一个字典来记录每个字符最后出现的位置,然后遍历字符串,使用start
和end
来确定每个片段的边界,并将片段的长度添加到结果列表中。最后,在主函数中打印出每个片段的长度。
Java 版本
import java.util.ArrayList;
import java.util.List;
public class Solution {
public List<Integer> partitionLabels(String s) {
int[] last = new int[26]; // Assuming 's' contains only lowercase letters.
for (int i = 0; i < s.length(); i++) {
last[s.charAt(i) - 'a'] = i;
}
List<Integer> partitionLengths = new ArrayList<>();
int start = 0, end = 0;
for (int i = 0; i < s.length(); i++) {
end = Math.max(end, last[s.charAt(i) - 'a']);
if (i == end) {
partitionLengths.add(end - start + 1);
start = i + 1;
}
}
return partitionLengths;
}
public static void main(String[] args) {
Solution solution = new Solution();
String s = "ababcbacadefegdehijhklij";
List<Integer> partitionLengths = solution.partitionLabels(s);
for (int length : partitionLengths) {
System.out.print(length + " ");
}
}
}
这段 Java 代码实现了相同的逻辑:首先创建一个数组来记录每个字符最后出现的位置,然后遍历字符串,使用start
和end
来确定每个片段的边界,并将片段的长度添加到结果列表中。最后,在main
方法中打印出每个片段的长度。
Rust 版本
use std::collections::HashMap; fn partition_labels(s: String) -> Vec<i32> { let mut last_occurrence = HashMap::new(); let s_chars: Vec<char> = s.chars().collect(); // Record the last occurrence of each character for (i, &item) in s_chars.iter().enumerate() { last_occurrence.insert(item, i); } let mut partitions = Vec::new(); let mut start = 0; let mut end = 0; for (i, &item) in s_chars.iter().enumerate() { if let Some(&last) = last_occurrence.get(&item) { end = end.max(last); } if i == end { partitions.push((end - start + 1) as i32); start = i + 1; } } partitions } fn main() { let s = String::from("ababcbacadefegdehijhklij"); let partition_lengths = partition_labels(s); for length in partition_lengths { print!("{} ", length); } }
这段 Rust 代码实现了相同的逻辑:首先创建一个哈希图来记录每个字符最后出现的位置,然后遍历字符串,使用start
和end
来确定每个片段的边界,并将片段的长度添加到结果向量中。最后,在main
函数中打印出每个片段的长度。
总结
上述问题的解法遵循了一个通用的算法思路,无论是在 Python、Java 还是 Rust 语言中。核心思想是利用贪心算法的策略来确定字符串中每个片段的边界。解法的步骤可以总结如下:
-
记录字符最后出现的位置:首先遍历整个字符串,记录下每个字符最后一次出现的索引位置。这可以通过哈希表(在 Rust 中是
HashMap
,在 Java 中是数组,因为输入限定为小写字母)来实现。 -
初始化起始和结束索引:定义两个变量
start
和end
,分别用来标记当前片段的起始和结束位置。初始时,start
和end
都设置为 0。 -
遍历字符串并更新结束索引:再次遍历字符串,对于每个字符,更新
end
为当前字符最后出现位置和end
的较大值。这确保了当前片段包含了该字符的所有出现。 -
确定片段边界并记录长度:当当前遍历的索引与
end
相等时,意味着已经到达了一个片段的边界。此时,将当前片段的长度(end - start + 1
)记录下来,并更新start
为end + 1
,以便开始下一个片段的查找。 -
重复步骤 3 和 4 直到结束:继续遍历直到字符串结束,每次找到一个片段边界时,都记录下该片段的长度。
-
返回结果:最终返回一个包含所有片段长度的列表。
这种方法的时间复杂度为 O(N),其中 N 是字符串的长度,因为每个字符被遍历了两次。空间复杂度取决于字符集的大小,在本题中是 O(1),因为输入字符串只包含小写字母,所以哈希表的大小最多为 26。
动态规划
动态规划(Dynamic Programming, DP)是一种算法设计技巧,它将一个复杂问题分解成若干个子问题,通过求解子问题来逐步求解原问题。动态规划通常用于求解最优化问题。解决动态规划问题的通用思路可以分为以下几个步骤:
-
定义状态:确定状态表示什么,每个状态需要包含哪些参数才能将问题描述完整。
-
状态转移方程:找出状态之间的关系,即如何从一个或多个较小的子问题的解得到当前问题的解。
-
初始化:确定边界条件,即最简单的子问题的解。
-
计算顺序:确定计算状态的顺序,以确保在计算一个状态时,所依赖的状态已经被计算。
-
答案重构(可选):如果问题要求具体的解决方案,而不仅仅是解的值,可能需要从最终状态反向追溯以构造解决方案。
-
优化(可选):对于空间复杂度,可以考虑状态压缩,只存储必要的状态。
下面是一个使用 Go 语言实现的动态规划问题的例子。我们将解决一个经典的问题:0-1 背包问题。在这个问题中,我们有一个背包和一些物品,每个物品都有一个重量和一个价值,我们需要选择一些物品放入背包中,使得背包中物品的总价值最大,同时不超过背包的最大承重。
package main
import (
"fmt"
)
// 0-1背包问题
func knapsack(weights []int, values []int, W int) int {
n := len(weights)
// dp[i][w] 表示对于前i个物品,当前背包容量为w时的最大价值
dp := make([][]int, n+1)
for i := range dp {
dp[i] = make([]int, W+1)
}
// 初始化已经在make的时候完成,所有值默认为0
// 构建dp表
for i := 1; i <= n; i++ {
for w := 1; w <= W; w++ {
if weights[i-1] <= w {
// 可以选择放入第i个物品或不放入,取决于哪种选择价值更大
dp[i][w] = max(dp[i-1][w], dp[i-1][w-weights[i-1]]+values[i-1])
} else {
// 当前背包容量无法放下第i个物品,只能选择不放入
dp[i][w] = dp[i-1][w]
}
}
}
// 最大价值在dp[n][W]
return dp[n][W]
}
// 辅助函数:求两个整数的最大值
func max(a, b int) int {
if a > b {
return a
}
return b
}
func main() {
weights := []int{1, 3, 4, 5} // 物品的重量
values := []int{1, 4, 5, 7} // 物品的价值
W := 7 // 背包的最大承重
maxValue := knapsack(weights, values, W)
fmt.Printf("The maximum value is %d\n", maxValue)
}
在这个例子中,我们定义了一个二维数组dp
,其中dp[i][w]
表示对于前i
个物品,当前背包容量为w
时的最大价值。我们通过比较放入和不放入当前物品的价值来填充这个表,并最终得到dp[n][W]
,即所有物品和背包最大容量下的最大价值。
1. 斐波那契数列
斐波那契数列是动态规划中的经典入门问题。我们可以使用动态规划来避免递归中的重复计算。
package main
import "fmt"
// 动态规划求解斐波那契数列
func fibonacci(n int) int {
if n <= 1 {
return n
}
dp := make([]int, n+1)
dp[0], dp[1] = 0, 1
for i := 2; i <= n; i++ {
dp[i] = dp[i-1] + dp[i-2]
}
return dp[n]
}
func main() {
fmt.Println(fibonacci(10)) // 输出: 55
}
2. 最长上升子序列(Longest Increasing Subsequence, LIS)
这个问题要求找到一个序列的最长上升子序列的长度。
package main
import "fmt"
// 动态规划求解最长上升子序列长度
func lengthOfLIS(nums []int) int {
if len(nums) == 0 {
return 0
}
dp := make([]int, len(nums))
maxLen := 1
for i := range dp {
dp[i] = 1
for j := 0; j < i; j++ {
if nums[i] > nums[j] {
dp[i] = max(dp[i], dp[j]+1)
}
}
maxLen = max(maxLen, dp[i])
}
return maxLen
}
// 辅助函数:求两个整数的最大值
func max(a, b int) int {
if a > b {
return a
}
return b
}
func main() {
nums := []int{10, 9, 2, 5, 3, 7, 101, 18}
fmt.Println(lengthOfLIS(nums)) // 输出: 4
}
3. 编辑距离(Edit Distance)
编辑距离问题要求计算将一个字符串转换成另一个字符串所需要的最少操作次数。
package main
import "fmt"
// 动态规划求解编辑距离
func minDistance(word1 string, word2 string) int {
m, n := len(word1), len(word2)
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
}
for i := 0; i <= m; i++ {
dp[i][0] = i
}
for j := 0; j <= n; j++ {
dp[0][j] = j
}
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if word1[i-1] == word2[j-1] {
dp[i][j] = dp[i-1][j-1]
} else {
dp[i][j] = min(dp[i-1][j-1], min(dp[i][j-1], dp[i-1][j])) + 1
}
}
}
return dp[m][n]
}
// 辅助函数:求三个整数的最小值
func min(a, b, c int) int {
if a < b {
if a < c {
return a
}
return c
}
if b < c {
return b
}
return c
}
func main() {
word1 := "horse"
word2 := "ros"
fmt.Println(minDistance(word1, word2)) // 输出: 3
}
这些例子展示了动态规划在解决不同类型问题时的应用。在实际编码中,你可能需要根据问题的具体情况调整状态定义和状态转移方程。动态规划是一个强大的工具,但它也需要一定的练习才能熟练掌握。
爬楼梯
题目要求
这个问题是一个经典的动态规划问题。给定一个整数 n,代表楼梯的总阶数,你可以一次爬 1 阶或 2 阶。你需要计算出所有不同的爬楼梯的方法数。
解题思路
要解决这个问题,我们可以采用动态规划的方法。动态规划是一种通过把原问题分解为相对简单的子问题的方式来求解复杂问题的方法。
-
定义状态: 首先,我们定义 dp[i] 为到达第 i 阶楼梯的方法数。
-
状态转移方程: 要到达第 i 阶楼梯,有两种方式:从第 i-1 阶爬上一阶,或者从第 i-2 阶爬上两阶。因此,到达第 i 阶的方法数就是到达第 i-1 阶和第 i-2 阶方法数的和,即 dp[i] = dp[i-1] + dp[i-2]。
-
初始化: 我们知道 dp[1] = 1(只有一种方法爬一阶),dp[2] = 2(有两种方法爬两阶:一次爬两阶或分两次各爬一阶)。
-
计算顺序: 由于状态转移方程的性质,我们应该从小到大计算 dp 数组。
-
结果: 最终,dp[n] 就是我们要求的答案,即到达第 n 阶楼梯的方法数。
这个问题也可以用数学的方法来解决,实际上它与斐波那契数列有关。在斐波那契数列中,每一个数都是前两个数的和,这与爬楼梯问题的状态转移方程是一致的。因此,我们也可以用斐波那契数列的递推或者闭合形式的公式来解决这个问题。但在实际编程中,动态规划的方法更为直观和易于实现。
Golang 版本
package main
import "fmt"
func climbStairs(n int) int {
if n == 1 {
return 1
}
if n == 2 {
return 2
}
dp := make([]int, n+1)
dp[1] = 1
dp[2] = 2
for i := 3; i <= n; i++ {
dp[i] = dp[i-1] + dp[i-2]
}
return dp[n]
}
func main() {
n := 10 // 示例,假设楼梯有10阶
fmt.Println("Total ways to climb:", climbStairs(n))
}
这段代码定义了一个 climbStairs
函数,它接受一个整数 n
作为参数,并返回到达第 n
阶楼梯的方法数。在 main
函数中,我们调用了 climbStairs
函数并打印出结果。这里以楼梯有 10 阶为例。
C++版本
#include <iostream>
#include <vector>
int climbStairs(int n) {
if (n == 1) {
return 1;
}
std::vector<int> dp(n + 1);
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; ++i) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
int main() {
int n = 10; // 示例,假设楼梯有10阶
std::cout << "Total ways to climb: " << climbStairs(n) << std::endl;
return 0;
}
这段代码使用 C++ 编写,实现了与 Golang 版本相同的功能。它定义了一个 climbStairs
函数,用于计算到达楼梯顶部的不同方法数,并在 main
函数中测试了这个函数。
Python 版本
def climb_stairs(n):
if n == 1:
return 1
if n == 2:
return 2
dp = [0] * (n + 1)
dp[1], dp[2] = 1, 2
for i in range(3, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
if __name__ == "__main__":
n = 10 # 示例,假设楼梯有10阶
print(f"Total ways to climb: {climb_stairs(n)}")
这段代码提供了一个 climb_stairs
函数,用于计算爬到楼梯顶部的不同方法数。在 if __name__ == "__main__":
块中,我们调用这个函数并打印出结果,这里以楼梯有 10 阶为例。
Java 版本
public class Solution {
public static int climbStairs(int n) {
if (n == 1) {
return 1;
}
if (n == 2) {
return 2;
}
int[] dp = new int[n + 1];
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
public static void main(String[] args) {
int n = 10; // 示例,假设楼梯有10阶
System.out.println("Total ways to climb: " + climbStairs(n));
}
}
这段 Java 代码定义了一个 climbStairs
方法,用于计算到达楼梯顶部的不同方法数。在 main
方法中,我们调用 climbStairs
方法并打印出结果,这里以楼梯有 10 阶为例。
Rust 版本
fn climb_stairs(n: i32) -> i32 { let mut dp = vec![0; (n + 1) as usize]; dp[1] = 1; dp[2] = 2; for i in 3..=n as usize { dp[i] = dp[i - 1] + dp[i - 2]; } dp[n as usize] } fn main() { let n = 10; // 示例,假设楼梯有10阶 println!("Total ways to climb: {}", climb_stairs(n)); }
这段 Rust 代码实现了一个 climb_stairs
函数,它接受一个 i32 类型的参数 n
并返回一个 i32 类型的结果,表示到达第 n
阶楼梯的方法数。在 main
函数中,我们调用了 climb_stairs
函数并打印出结果,这里以楼梯有 10 阶为例。
总结
上述问题是一个经典的动态规划问题,通常被称为“爬楼梯问题”。解决这个问题的关键在于找到一个递推关系,即每一步的解可以由之前的步骤推导出来。
解法的核心思想是使用一个数组(在某些语言中是列表或向量)来存储到达每一阶楼梯的方法数。这个数组被称为动态规划表(dp table)。
动态规划表的初始化非常重要,通常需要手动设置前几个值。在这个问题中,到达第一阶楼梯有 1 种方法,到达第二阶楼梯有 2 种方法。
状态转移方程是 dp[i] = dp[i-1] + dp[i-2],它表示到达第 i 阶楼梯的方法数等于到达第 i-1 阶楼梯的方法数加上到达第 i-2 阶楼梯的方法数。这是因为每次你可以爬 1 阶或 2 阶,所以到达第 i 阶的最后一步可以从第 i-1 阶爬 1 阶上来,或者从第 i-2 阶爬 2 阶上来。
在编程实现中,我们遍历从 3 到 n 的每一阶楼梯,并应用状态转移方程来填充动态规划表。最后,dp[n] 就是我们要找的答案,即到达楼顶的方法数。
不同编程语言的实现细节可能有所不同,但核心算法和逻辑是一致的。在 Golang、C++、Python、Java 和 Rust 的代码示例中,我们都定义了一个函数或方法来实现这个算法,并在主函数中调用该函数来获取结果。
杨辉三角
题目要求
编写一个函数,接收一个非负整数 numRows
作为参数,该函数的任务是生成一个包含「杨辉三角」前 numRows
行的列表。
「杨辉三角」是一个二维数组,其中第 n
行(行号从 0 开始)有 n + 1
个数字。对于三角形的每一行,第一个和最后一个数字总是 1。对于任何其他位置的数字,可以通过将上一行同一位置的数字和该位置前一个数字相加得到。
解题思路
-
初始化一个空列表
triangle
,用于存储整个「杨辉三角」。 -
从第 0 行开始,一直到
numRows - 1
行,按行构建「杨辉三角」。 -
对于每一行,创建一个新的子列表
row
,并将其添加到triangle
中。 -
设置每一行的第一个元素和最后一个元素为 1,因为「杨辉三角」的边界总是 1。
-
对于第
i
行(i
从 0 开始计数)的中间元素,即row[j]
(j
从 1 开始到i - 1
结束),可以通过triangle[i-1][j-1] + triangle[i-1][j]
来计算。这里triangle[i-1]
表示上一行,triangle[i-1][j-1]
和triangle[i-1][j]
分别表示上一行的左上方和右上方的数。 -
重复步骤 3 到 5,直到构建完所有的行。
-
返回
triangle
列表,该列表现在包含了「杨辉三角」的前numRows
行。
注意:在实际编码时,需要考虑 numRows
为 0 的特殊情况,此时应返回一个空列表。如果 numRows
为 1,则返回的列表中只包含一个元素 [1]
。
Golang 版本
package main
import "fmt"
func generate(numRows int) [][]int {
triangle := make([][]int, numRows) // 创建一个二维切片,用于存储杨辉三角的所有行
for i := 0; i < numRows; i++ {
triangle[i] = make([]int, i+1) // 初始化当前行,长度为 i+1
triangle[i][0], triangle[i][i] = 1, 1 // 第一个和最后一个元素设置为 1
// 从第二行开始,计算中间的元素
for j := 1; j < i; j++ {
// 上一行的左上方和右上方元素之和
triangle[i][j] = triangle[i-1][j-1] + triangle[i-1][j]
}
}
return triangle
}
func main() {
numRows := 5
result := generate(numRows)
for _, row := range result {
fmt.Println(row)
}
}
这段代码定义了一个 generate
函数,它接收一个 numRows
参数,并返回一个二维切片,其中包含了「杨辉三角」的前 numRows
行。在 main
函数中,我们调用了 generate
函数,并打印出返回的杨辉三角。
C++版本
#include <iostream>
#include <vector>
std::vector<std::vector<int>> generate(int numRows) {
std::vector<std::vector<int>> triangle;
for (int i = 0; i < numRows; ++i) {
// 每一行的大小是行号+1
std::vector<int> row(i + 1, 1); // 初始化当前行,并将所有元素设置为1
// 计算非边界位置的元素值
for (int j = 1; j < i; ++j) {
row[j] = triangle[i - 1][j - 1] + triangle[i - 1][j];
}
// 将当前行添加到杨辉三角中
triangle.push_back(row);
}
return triangle;
}
int main() {
int numRows = 5;
std::vector<std::vector<int>> result = generate(numRows);
// 打印杨辉三角
for (const auto &row : result) {
for (int num : row) {
std::cout << num << " ";
}
std::cout << std::endl;
}
return 0;
}
这段代码定义了一个 generate
函数,它接收一个 numRows
参数,并返回一个 vector
容器,其中包含了「杨辉三角」的前 numRows
行。在 main
函数中,我们调用了 generate
函数,并打印出返回的杨辉三角。
Python 版本
def generate(numRows):
triangle = []
for row_num in range(numRows):
# The first and last row elements are always 1.
row = [None for _ in range(row_num + 1)]
row[0], row[-1] = 1, 1
# Each triangle element (except the first and last of each row)
# is equal to the sum of the elements above-and-to-the-left and above-and-to-the-right.
for j in range(1, len(row) - 1):
row[j] = triangle[row_num - 1][j - 1] + triangle[row_num - 1][j]
triangle.append(row)
return triangle
# Example usage:
numRows = 5
triangle = generate(numRows)
for row in triangle:
print(row)
这段代码定义了一个 generate
函数,它接收一个 numRows
参数,并返回一个列表,其中包含了「杨辉三角」的前 numRows
行。在示例用法中,我们调用了 generate
函数,并打印出返回的杨辉三角。
Java 版本
import java.util.ArrayList;
import java.util.List;
public class Solution {
public List<List<Integer>> generate(int numRows) {
List<List<Integer>> triangle = new ArrayList<>();
for (int i = 0; i < numRows; i++) {
List<Integer> row = new ArrayList<>();
for (int j = 0; j <= i; j++) {
// First and last row elements are always 1.
if (j == 0 || j == i) {
row.add(1);
} else {
// Each triangle element is equal to the sum of the elements
// above-and-to-the-left and above-and-to-the-right.
int left = triangle.get(i - 1).get(j - 1);
int right = triangle.get(i - 1).get(j);
row.add(left + right);
}
}
triangle.add(row);
}
return triangle;
}
public static void main(String[] args) {
Solution solution = new Solution();
List<List<Integer>> result = solution.generate(5);
for (List<Integer> row : result) {
for (int num : row) {
System.out.print(num + " ");
}
System.out.println();
}
}
}
这段代码定义了一个 Solution
类,其中包含了一个 generate
方法,它接收一个 numRows
参数,并返回一个 List
,其中包含了「杨辉三角」的前 numRows
行。在 main
方法中,我们创建了 Solution
类的实例,并调用了 generate
方法,然后打印出返回的杨辉三角。
Rust 版本
fn generate(num_rows: i32) -> Vec<Vec<i32>> { let mut triangle: Vec<Vec<i32>> = Vec::new(); for row_number in 0..num_rows as usize { // Start each row with a vector containing one element, 1. let mut row: Vec<i32> = vec![1; row_number + 1]; // Each triangle element (except the first and last of each row) // is equal to the sum of the elements above-and-to-the-left and above-and-to-the-right. for j in 1..row_number { row[j] = triangle[row_number - 1][j - 1] + triangle[row_number - 1][j]; } // Add the current row to the triangle. triangle.push(row); } triangle } fn main() { let num_rows = 5; let triangle = generate(num_rows); for row in triangle { for num in row { print!("{} ", num); } println!(); } }
这段代码定义了一个 generate
函数,它接收一个 num_rows
参数,并返回一个 Vec<Vec<i32>>
类型的向量,其中包含了「杨辉三角」的前 num_rows
行。在 main
函数中,我们调用了 generate
函数,并打印出返回的杨辉三角。
总结
在上述的解法中,无论是使用 Golang、C++、Python 还是 Rust,核心算法的逻辑是一致的。我们首先创建一个能够存储整个杨辉三角的数据结构,然后按行逐步构建每一行的内容。每一行的第一个和最后一个元素始终是 1,而行内的其他元素则是通过将上一行对应位置的元素与其前一个元素相加得到的。
具体步骤如下:
- 初始化一个空的数据结构(在不同语言中可能是动态数组、向量等)来存储整个三角形。
- 遍历从 0 到
numRows
(不包括numRows
),对于每一行:- 初始化当前行,并设置所有元素的默认值为 1。
- 对于当前行的中间元素(即非首尾元素),通过上一行的
triangle[i-1][j-1] + triangle[i-1][j]
来计算当前元素的值。 - 将构建好的当前行添加到三角形结构中。
- 在主函数中调用构建杨辉三角的函数,并打印或返回结果。
不同语言之间的主要区别在于语法和数据结构的使用。例如:
- 在 C++ 中,使用
std::vector
来动态存储数据。 - 在 Python 中,使用列表(list)来存储数据,并且语法更为简洁。
- 在 Java 中,使用
ArrayList
来存储数据,并且需要更多的类型声明。 - 在 Rust 中,使用
Vec
来存储数据,并且有明确的所有权和借用规则。
尽管实现的细节不同,但所有解法都遵循了相同的算法逻辑和步骤。
打家劫舍
题目要求
这是一个经典的动态规划问题。题目描述了一个小偷在偷窃一排房屋时面临的问题。每个房屋都有一定数量的现金,但是相邻的房屋都有相互连接的防盗系统。如果小偷在同一晚上偷窃了两个相邻的房屋,防盗系统就会触发。小偷的目标是在不触发警报的情况下,从这些房屋中偷取尽可能多的现金。
具体来说,输入是一个非负整数数组,其中每个元素代表一个房屋中的现金数量。需要输出的是小偷在不触发警报系统的情况下,能够偷窃到的最大金额。
解题思路
这个问题可以通过动态规划(Dynamic Programming,DP)来解决。动态规划是一种将复杂问题分解成小问题求解的方法,通常用于求解最优化问题。在这个问题中,我们可以定义一个 DP 数组,其中dp[i]
表示到达第i
个房屋时,不触发警报装置情况下能偷窃到的最高金额。
我们可以按照以下步骤来构建解决方案:
-
初始化:创建一个 DP 数组,长度为输入数组的长度加一,
dp[0]
为 0,因为没有房屋可以偷窃时金额为 0;dp[1]
为第一个房屋中的金额,因为开始时只能偷窃第一个房屋。 -
状态转移方程:对于数组中的每个房屋
i
(从第二个房屋开始),小偷有两个选择:- 不偷这个房屋,那么最大金额就是到前一个房屋为止的最大金额,即
dp[i-1]
; - 偷这个房屋,那么小偷不能偷前一个房屋,但可以偷前前一个房屋,所以最大金额是当前房屋的金额加上到前前一个房屋为止的最大金额,即
nums[i] + dp[i-2]
。
对于每个房屋,我们都取这两种选择中的较大值作为
dp[i]
的值。 - 不偷这个房屋,那么最大金额就是到前一个房屋为止的最大金额,即
-
状态转移:按照上述状态转移方程,从前向后遍历数组,填充 DP 数组。
-
输出结果:数组的最后一个元素
dp[n]
(其中n
是输入数组的长度)就是小偷能偷窃到的最大金额。
通过这种方法,我们可以在O(n)
的时间复杂度和O(n)
的空间复杂度内解决问题。如果需要进一步优化空间复杂度,可以考虑使用滚动数组的方式,只存储必要的几个状态,将空间复杂度降低到O(1)
。
Golang 版本
package main
import "fmt"
func rob(nums []int) int {
n := len(nums)
if n == 0 {
return 0
}
if n == 1 {
return nums[0]
}
dp := make([]int, n)
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i := 2; i < n; i++ {
dp[i] = max(dp[i-1], dp[i-2]+nums[i])
}
return dp[n-1]
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func main() {
nums := []int{1, 2, 3, 1}
fmt.Println("Maximum amount that can be robbed is:", rob(nums))
}
这段代码定义了一个rob
函数,它接受一个整数数组nums
作为输入,返回不触发警报装置情况下能偷窃到的最高金额。main
函数中给出了一个示例数组,并打印出调用rob
函数的结果。
C++版本
#include <iostream>
#include <vector>
#include <algorithm>
int rob(std::vector<int>& nums) {
if (nums.empty()) return 0;
if (nums.size() == 1) return nums[0];
std::vector<int> dp(nums.size(), 0);
dp[0] = nums[0];
dp[1] = std::max(nums[0], nums[1]);
for (size_t i = 2; i < nums.size(); ++i) {
dp[i] = std::max(dp[i - 1], dp[i - 2] + nums[i]);
}
return dp.back();
}
int main() {
std::vector<int> nums = {1, 2, 3, 1};
std::cout << "Maximum amount that can be robbed is: " << rob(nums) << std::endl;
return 0;
}
这段 C++代码实现了与 Golang 版本相同的逻辑。rob
函数接受一个整数类型的vector
作为输入,并返回一个整数,表示在不触发警报的情况下,小偷能偷窃到的最大金额。main
函数中提供了一个示例数组,并打印出调用rob
函数的结果。
Python 版本
def rob(nums):
if not nums:
return 0
if len(nums) == 1:
return nums[0]
dp = [0] * len(nums)
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, len(nums)):
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])
return dp[-1]
if __name__ == "__main__":
nums = [1, 2, 3, 1]
print("Maximum amount that can be robbed is:", rob(nums))
这段 Python 代码实现了与前面 Golang 和 C++版本相同的逻辑。rob
函数接受一个列表nums
作为输入,并返回一个整数,表示在不触发警报的情况下,小偷能偷窃到的最大金额。在__main__
部分中,提供了一个示例列表,并打印出调用rob
函数的结果。
Java 版本
public class Solution {
public int rob(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
if (nums.length == 1) {
return nums[0];
}
int[] dp = new int[nums.length];
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for (int i = 2; i < nums.length; i++) {
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
}
return dp[nums.length - 1];
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {1, 2, 3, 1};
System.out.println("Maximum amount that can be robbed is: " + solution.rob(nums));
}
}
这段 Java 代码实现了与前面版本相同的逻辑。rob
方法接受一个整数数组nums
作为输入,并返回一个整数,表示在不触发警报的情况下,小偷能偷窃到的最大金额。main
方法中提供了一个示例数组,并打印出调用rob
方法的结果。
Rust 版本
fn rob(nums: Vec<i32>) -> i32 { let n = nums.len(); if n == 0 { return 0; } if n == 1 { return nums[0]; } let mut dp = vec![0; n]; dp[0] = nums[0]; dp[1] = std::cmp::max(nums[0], nums[1]); for i in 2..n { dp[i] = std::cmp::max(dp[i - 1], dp[i - 2] + nums[i]); } dp[n - 1] } fn main() { let nums = vec![1, 2, 3, 1]; println!("Maximum amount that can be robbed is: {}", rob(nums)); }
这段 Rust 代码实现了与前面版本相同的逻辑。rob
函数接受一个整数向量nums
作为输入,并返回一个整数,表示在不触发警报的情况下,小偷能偷窃到的最大金额。main
函数中提供了一个示例向量,并打印出调用rob
函数的结果。
总结
上述问题是一个典型的动态规划问题,其核心思想是通过构建一个数组(或列表)来保存到当前房屋为止能够偷窃到的最大金额。解法的步骤可以总结如下:
-
边界条件处理:首先处理一些基本情况,例如当没有房屋或只有一个房屋时的情况。
-
初始化动态规划数组:创建一个动态规划数组
dp
,其长度与房屋数量相同。dp[i]
表示到达第i
个房屋时能偷窃到的最大金额。 -
填充动态规划数组:
dp[0]
被初始化为第一个房屋中的金额。dp[1]
是第一个房屋和第二个房屋中金额的较大者,因为小偷不能连续偷窃相邻的房屋。- 对于
dp[i]
(其中i >= 2
),它是以下两个值的较大者:dp[i - 1]
,表示如果不偷第i
个房屋,那么最大金额就是到前一个房屋为止的最大金额。dp[i - 2] + nums[i]
,表示如果偷第i
个房屋,那么小偷不能偷第i - 1
个房屋,但可以偷第i - 2
个房屋,所以最大金额是第i
个房屋的金额加上到第i - 2
个房屋为止的最大金额。
-
返回结果:动态规划数组的最后一个元素
dp[n - 1]
(其中n
是房屋的数量)就是小偷能偷窃到的最大金额。
这个解法在不同的编程语言中都有相似的实现,只是语法上有所不同。在 Python、Java 和 Rust 中,都使用了一个数组(或向量)来实现动态规划的存储,并通过迭代来填充这个数组。最终,通过返回数组的最后一个元素来得到最终的答案。
完全平方数
题目要求
这个问题是一个经典的动态规划问题,要求解的是给定一个整数n
,找出使得它们的和等于n
的完全平方数的最小数量。完全平方数是指可以表示为某个整数的平方的数,例如 1(=1x1)、4(=2x2)、9(=3x3)等等。
解题思路
为了解决这个问题,我们可以采用动态规划的方法。动态规划是一种将复杂问题分解成更小的子问题来解决的方法,并且存储子问题的解,避免重复计算。
-
初始化动态规划数组:创建一个数组
dp
,其中dp[i]
表示和为i
的完全平方数的最小数量。数组的大小为n+1
,初始时,除了dp[0]
为 0 以外,其余的dp[i]
都设置为一个大于n
的数,因为和为i
的完全平方数的数量不可能大于n
。 -
预处理完全平方数:计算出所有小于等于
n
的完全平方数,并存储在一个列表中,这样我们就可以在后续的计算中直接使用这些数。 -
填充动态规划数组:对于数组
dp
中的每个元素dp[i]
(i
从 1 到n
),我们遍历所有的完全平方数square
,对于每个square
,如果square
小于等于i
,我们就计算dp[i - square] + 1
的值,并更新dp[i]
为当前的dp[i]
和dp[i - square] + 1
中的较小值。这里+1
表示加上这个完全平方数square
。 -
找到结果:经过上述步骤,
dp[n]
中存储的就是和为n
的完全平方数的最小数量。
这个动态规划的过程基本上是在尝试将n
分解为若干个完全平方数的和,并且在这个过程中尽可能地使用较少的完全平方数。通过比较所有可能的分解方法,我们可以找到最优的解。
Golang 版本
package main
import (
"fmt"
"math"
)
func numSquares(n int) int {
// 初始化dp数组,初始值设为一个比n大的数,这里用n+1
dp := make([]int, n+1)
for i := range dp {
dp[i] = n + 1
}
// base case
dp[0] = 0
// 预处理所有可能的完全平方数
maxSquareIndex := int(math.Sqrt(float64(n))) + 1
squares := make([]int, maxSquareIndex)
for i := 1; i < maxSquareIndex; i++ {
squares[i] = i * i
}
// 动态规划填充dp数组
for i := 1; i <= n; i++ {
for _, square := range squares {
if square <= i {
dp[i] = min(dp[i], dp[i-square]+1)
}
}
}
return dp[n]
}
// 辅助函数,返回两个整数中较小的一个
func min(a, b int) int {
if a < b {
return a
}
return b
}
func main() {
n := 12 // 示例
fmt.Println("The least number of perfect squares is:", numSquares(n))
}
这段代码定义了一个numSquares
函数,它接受一个整数n
,并返回和为n
的完全平方数的最少数量。在main
函数中,我们给出了一个示例,调用numSquares
函数并打印结果。
C++版本
#include <iostream>
#include <vector>
#include <cmath>
#include <climits>
int numSquares(int n) {
std::vector<int> dp(n + 1, INT_MAX);
dp[0] = 0;
// 预处理所有可能的完全平方数
int maxSquareIndex = std::sqrt(n) + 1;
std::vector<int> squares(maxSquareIndex);
for (int i = 1; i < maxSquareIndex; ++i) {
squares[i] = i * i;
}
// 动态规划填充dp数组
for (int i = 1; i <= n; ++i) {
for (int s = 1; s < maxSquareIndex; ++s) {
if (squares[s] <= i) {
dp[i] = std::min(dp[i], dp[i - squares[s]] + 1);
}
}
}
return dp[n];
}
int main() {
int n = 12; // 示例
std::cout << "The least number of perfect squares is: " << numSquares(n) << std::endl;
return 0;
}
这段 C++代码实现了与 Golang 版本相同的功能。它定义了一个numSquares
函数,用于计算和为n
的完全平方数的最少数量。在main
函数中,我们设置了一个示例值n = 12
,调用numSquares
函数,并输出结果。
Python 版本
import math
def numSquares(n):
# 初始化dp数组,初始值设为无穷大
dp = [float('inf')] * (n + 1)
# base case
dp[0] = 0
# 预处理所有可能的完全平方数
max_square_index = int(math.sqrt(n)) + 1
squares = [i**2 for i in range(1, max_square_index)]
# 动态规划填充dp数组
for i in range(1, n + 1):
for square in squares:
if i >= square:
dp[i] = min(dp[i], dp[i - square] + 1)
return dp[n]
# 示例
n = 12
print("The least number of perfect squares is:", numSquares(n))
这段 Python 代码实现了一个numSquares
函数,它接受一个整数n
作为输入,并返回和为n
的完全平方数的最少数量。在代码的最后,我们给出了一个示例,调用numSquares
函数并打印结果。
Java 版本
public class Solution {
public int numSquares(int n) {
int[] dp = new int[n + 1];
// 初始化dp数组,初始值设为最大整数
for (int i = 1; i <= n; i++) {
dp[i] = Integer.MAX_VALUE;
}
// base case
dp[0] = 0;
// 计算所有小于等于n的完全平方数
int maxSquareIndex = (int) Math.sqrt(n) + 1;
int[] squares = new int[maxSquareIndex];
for (int i = 1; i < maxSquareIndex; i++) {
squares[i] = i * i;
}
// 动态规划求解
for (int i = 1; i <= n; i++) {
for (int s = 1; s < maxSquareIndex; s++) {
if (i >= squares[s]) {
dp[i] = Math.min(dp[i], dp[i - squares[s]] + 1);
}
}
}
return dp[n];
}
public static void main(String[] args) {
Solution solution = new Solution();
int n = 12;
System.out.println("The least number of perfect squares is: " + solution.numSquares(n));
}
}
这段 Java 代码定义了一个Solution
类,其中包含了numSquares
方法,用于计算和为n
的完全平方数的最少数量。在main
方法中,我们创建了一个Solution
对象,并调用numSquares
方法,然后打印出结果。
Rust 版本
use std::cmp::min; use std::f64; fn num_squares(n: i32) -> i32 { let mut dp = vec![i32::MAX; (n + 1) as usize]; dp[0] = 0; for i in 1..=n as usize { let mut j = 1; while j * j <= i { dp[i] = min(dp[i], dp[i - j * j] + 1); j += 1; } } dp[n as usize] } fn main() { let n = 12; println!("The least number of perfect squares is: {}", num_squares(n)); }
这段 Rust 代码实现了一个num_squares
函数,它接受一个整数n
作为输入,并返回和为n
的完全平方数的最少数量。在main
函数中,我们设置了一个示例值n = 12
,调用num_squares
函数,并打印出结果。
总结
上述问题的解法涉及到了动态规划(Dynamic Programming, DP)的思想。核心思路是创建一个数组dp
,其中dp[i]
表示组成和为i
的完全平方数的最小数量。解法的步骤如下:
-
初始化一个数组
dp
,大小为n+1
,并将除了dp[0]
之外的所有元素设为一个大于n
的数,因为最坏情况下,和为i
的完全平方数的数量不会超过i
(即i
个 1 的平方和)。 -
设置
dp[0]
的值为 0,因为和为 0 的完全平方数的最小数量显然是 0。 -
预处理所有小于等于
n
的完全平方数,并存储在一个列表或数组中,以便后续快速查找。 -
使用两层循环来填充
dp
数组。外层循环遍历从 1 到n
的每个数i
,内层循环遍历所有的完全平方数。对于每个i
和完全平方数square
,如果square
小于等于i
,则更新dp[i]
为dp[i]
和dp[i - square] + 1
中的较小值。 -
最终,
dp[n]
中存储的就是和为n
的完全平方数的最小数量。
这个问题的解法在不同的编程语言中都是类似的,只是语法和一些细节上有所不同。在上面的回答中,我们提供了 Golang、C++、Python、Java 和 Rust 版本的代码实现。每种语言都遵循了动态规划的基本步骤,但是在数组初始化、循环结构和语言特定的函数调用等方面有所区别。
零钱兑换
题目要求
给定一个整数数组 coins
,代表不同面额的硬币,以及一个整数 amount
,代表需要凑成的总金额。要求编写一个算法,计算出凑成该总金额所需的最少硬币个数。如果无法凑出,则返回 -1
。假设每种硬币的数量是无限的。
解题思路
这个问题是一个典型的动态规划问题,可以通过构建一个动态规划表来逐步求解。以下是解题的步骤:
-
初始化动态规划数组:创建一个长度为
amount + 1
的数组dp
,其中dp[i]
表示凑成金额i
所需的最少硬币个数。初始化dp[0] = 0
,因为凑成金额 0 不需要任何硬币,其余元素初始化为一个大数,表示初始状态下无法凑齐。 -
填充动态规划表:遍历金额从
1
到amount
,对于每个金额i
,遍历每个硬币面额coin
,如果coin
小于或等于i
,则尝试用这个硬币来凑,更新dp[i]
的值为min(dp[i], dp[i - coin] + 1)
。这里dp[i - coin] + 1
表示如果前面的金额i - coin
已经被凑齐了,那么只需要再加一个面额为coin
的硬币即可凑齐金额i
。 -
返回结果:最后,检查
dp[amount]
的值,如果它不是初始化的大数,那么就返回dp[amount]
,表示最少硬币个数;如果仍然是大数,则返回-1
,表示无法凑齐。
这种方法的时间复杂度是 O(n*m),其中 n 是金额 amount
,m 是硬币种类数。空间复杂度是 O(n),因为需要一个长度为 amount + 1
的数组来存储状态。
Golang 版本
package main
import (
"fmt"
"math"
)
func coinChange(coins []int, amount int) int {
// 初始化dp数组,长度为amount+1,初始值为amount+1,因为最多的硬币数不会超过amount
dp := make([]int, amount+1)
for i := range dp {
dp[i] = amount + 1
}
// base case,凑成金额0的硬币数为0
dp[0] = 0
// 外层循环遍历所有状态的所有取值
for i := 1; i <= amount; i++ {
// 内层循环遍历所有选择的最小值
for _, coin := range coins {
if i-coin >= 0 {
dp[i] = min(dp[i], dp[i-coin]+1)
}
}
}
// 如果dp[amount]没有被更新,说明凑不出该金额,返回-1
if dp[amount] == amount+1 {
return -1
}
return dp[amount]
}
// 辅助函数,求两个整数的最小值
func min(a, b int) int {
if a < b {
return a
}
return b
}
func main() {
coins := []int{1, 2, 5}
amount := 11
fmt.Println("The minimum number of coins is:", coinChange(coins, amount))
}
这段代码定义了一个 coinChange
函数,它接受一个整数数组 coins
和一个整数 amount
,返回凑成总金额所需的最少硬币个数。如果无法凑出,则返回 -1
。在 main
函数中,我们给出了硬币数组和金额的示例,并调用 coinChange
函数来输出结果。
C++版本
#include <iostream>
#include <vector>
#include <algorithm>
class Solution {
public:
int coinChange(std::vector<int>& coins, int amount) {
std::vector<int> dp(amount + 1, amount + 1);
dp[0] = 0;
for (int i = 1; i <= amount; ++i) {
for (int coin : coins) {
if (i - coin >= 0) {
dp[i] = std::min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
};
int main() {
Solution solution;
std::vector<int> coins = {1, 2, 5};
int amount = 11;
std::cout << "The minimum number of coins is: " << solution.coinChange(coins, amount) << std::endl;
return 0;
}
这段 C++代码定义了一个 Solution
类,其中包含了 coinChange
方法,该方法接受一个整数向量 coins
和一个整数 amount
,返回凑成总金额所需的最少硬币个数。如果无法凑出,则返回 -1
。在 main
函数中,我们创建了一个 Solution
对象,并给出了硬币向量和金额的示例,然后调用 coinChange
方法并输出结果。
Python 版本
def coinChange(coins, amount):
# 初始化dp数组,长度为amount+1,初始值为amount+1,因为最多的硬币数不会超过amount
dp = [amount + 1] * (amount + 1)
dp[0] = 0 # base case
# 遍历所有金额,更新dp数组
for i in range(1, amount + 1):
for coin in coins:
if i - coin >= 0:
dp[i] = min(dp[i], dp[i - coin] + 1)
# 如果dp[amount]为初始值,则表示无法凑出,返回-1
return dp[amount] if dp[amount] != amount + 1 else -1
# 示例
coins = [1, 2, 5]
amount = 11
print("The minimum number of coins is:", coinChange(coins, amount))
这段 Python 代码定义了一个 coinChange
函数,它接受一个整数列表 coins
和一个整数 amount
,返回凑成总金额所需的最少硬币个数。如果无法凑出,则返回 -1
。在代码的最后,我们给出了硬币列表和金额的示例,并调用 coinChange
函数来输出结果。
Java 版本
public class Solution {
public int coinChange(int[] coins, int amount) {
int max = amount + 1;
int[] dp = new int[amount + 1];
java.util.Arrays.fill(dp, max);
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int coin : coins) {
if (coin <= i) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] coins = {1, 2, 5};
int amount = 11;
System.out.println("The minimum number of coins is: " + solution.coinChange(coins, amount));
}
}
这段 Java 代码定义了一个 Solution
类,其中包含了 coinChange
方法,该方法接受一个整数数组 coins
和一个整数 amount
,返回凑成总金额所需的最少硬币个数。如果无法凑出,则返回 -1
。在 main
方法中,我们创建了一个 Solution
对象,并给出了硬币数组和金额的示例,然后调用 coinChange
方法并输出结果。
Rust 版本
fn coin_change(coins: Vec<i32>, amount: i32) -> i32 { let mut dp = vec![amount + 1; (amount + 1) as usize]; dp[0] = 0; for i in 1..=amount as usize { for &coin in &coins { if coin as usize <= i { dp[i] = dp[i].min(dp[i - coin as usize] + 1); } } } if dp[amount as usize] > amount { -1 } else { dp[amount as usize] } } fn main() { let coins = vec![1, 2, 5]; let amount = 11; println!("The minimum number of coins is: {}", coin_change(coins, amount)); }
这段 Rust 代码定义了一个 coin_change
函数,它接受一个整数向量 coins
和一个整数 amount
,返回凑成总金额所需的最少硬币个数。如果无法凑出,则返回 -1
。在 main
函数中,我们创建了硬币向量和金额的示例,并调用 coin_change
函数来输出结果。
总结
上述问题是一个经典的动态规划问题,通常被称为硬币找零问题。解决这个问题的核心思想是构建一个数组 dp
,其中 dp[i]
表示凑成金额 i
所需的最少硬币数量。动态规划的转移方程是:
dp[i] = min(dp[i], dp[i - coin] + 1) for each coin in coins
这个方程意味着,要得到金额 i
的最少硬币数,我们应该查看每一个小于 i
的金额 j = i - coin
,并且 j
加上一枚面值为 coin
的硬币。我们在所有可能的 coin
中选择最小的 dp[i - coin] + 1
作为 dp[i]
的值。
初始化时,dp[0]
被设置为 0
,因为凑成金额 0
所需的硬币数显然是 0
。其他的 dp[i]
初始化为一个大数,通常是金额 amount
加 1
,这是因为最坏的情况下,凑成金额 i
的硬币数不会超过 i
(全用面值为 1
的硬币)。
最后,如果 dp[amount]
仍然是初始化的大数,这意味着没有任何硬币组合能够凑成总金额,因此返回 -1
。否则,dp[amount]
就是凑成总金额所需的最少硬币个数。
在不同的编程语言中,这个算法的实现细节可能略有不同,但核心逻辑是一致的。我们提供了 C++, Python, Java, 和 Rust 版本的代码实现,每种语言都遵循了上述动态规划的策略。
单词拆分
题目要求
你需要编写一个算法来判断给定的字符串 s
是否可以由字符串列表 wordDict
中的单词组合而成。字符串 s
可以由 wordDict
中的一个或多个单词重复拼接而成,但不必须使用列表中的所有单词。
解题思路
这个问题可以通过动态规划(Dynamic Programming, DP)来解决。动态规划是一种在计算机科学中常用的解决优化问题的方法,它将一个问题分解为相互依赖的子问题,并存储这些子问题的解,以避免重复计算。
以下是解决这个问题的步骤:
-
初始化 DP 数组:创建一个布尔型的 DP 数组
dp
,其长度为s.length() + 1
。dp[i]
表示字符串s
的前i
个字符能否由wordDict
中的单词组成。初始化dp[0] = true
,因为空字符串总是可以由字典中的零个单词组成。 -
填充 DP 数组:遍历字符串
s
的所有可能的子串,对于每个位置i
(从 1 到s.length()
),执行以下操作:- 遍历
wordDict
中的所有单词,对于每个单词word
,如果word
可以放在当前位置i
之前,并且dp[i - word.length()]
为true
(这意味着s
的前i - word.length()
个字符可以由wordDict
中的单词组成),那么将dp[i]
设置为true
。
- 遍历
-
检查结果:最后,检查
dp[s.length()]
的值。如果为true
,则表示字符串s
可以由wordDict
中的单词组合而成;如果为false
,则不可以。
这个算法的关键在于理解如何利用 DP 数组来保存中间结果,以及如何通过这些结果来判断整个字符串是否可以由字典中的单词组成。通过这种方式,我们可以避免对于每个子串的重复检查,从而提高算法的效率。
Golang 版本
package main
import "fmt"
func wordBreak(s string, wordDict []string) bool {
wordDictSet := make(map[string]bool)
for _, word := range wordDict {
wordDictSet[word] = true
}
dp := make([]bool, len(s)+1)
dp[0] = true
for i := 1; i <= len(s); i++ {
for j := 0; j < i; j++ {
if dp[j] && wordDictSet[s[j:i]] {
dp[i] = true
break
}
}
}
return dp[len(s)]
}
func main() {
s := "bestcode"
wordDict := []string{"best", "code"}
fmt.Println(wordBreak(s, wordDict)) // Output: true
}
这段代码定义了一个 wordBreak
函数,它接受一个字符串 s
和一个字符串切片 wordDict
作为输入,并返回一个布尔值,表示 s
是否可以由 wordDict
中的单词组成。在 main
函数中,我们调用了 wordBreak
函数,并打印出结果。
C++版本
#include <iostream>
#include <vector>
#include <string>
#include <unordered_set>
bool wordBreak(std::string s, std::vector<std::string>& wordDict) {
std::unordered_set<std::string> dict(wordDict.begin(), wordDict.end());
std::vector<bool> dp(s.size() + 1, false);
dp[0] = true;
for (int i = 1; i <= s.size(); ++i) {
for (int j = 0; j < i; ++j) {
if (dp[j] && dict.find(s.substr(j, i - j)) != dict.end()) {
dp[i] = true;
break;
}
}
}
return dp[s.size()];
}
int main() {
std::string s = "bestcode";
std::vector<std::string> wordDict = {"best", "code"};
std::cout << (wordBreak(s, wordDict) ? "true" : "false") << std::endl;
return 0;
}
这段 C++代码实现了与 Golang 版本相同的功能。它定义了一个 wordBreak
函数,该函数使用动态规划来确定字符串 s
是否可以由 wordDict
中的单词组成。在 main
函数中,我们创建了一个示例字符串和字典,然后调用 wordBreak
函数,并输出结果。
Python 版本
def wordBreak(s, wordDict):
word_set = set(wordDict)
dp = [False] * (len(s) + 1)
dp[0] = True
for i in range(1, len(s) + 1):
for j in range(i):
if dp[j] and s[j:i] in word_set:
dp[i] = True
break
return dp[len(s)]
# 测试代码
s = "bestcode"
wordDict = ["best", "code"]
print(wordBreak(s, wordDict)) # 应输出: True
这段 Python 代码实现了前面描述的动态规划算法。wordBreak
函数检查字符串 s
是否可以由 wordDict
中的单词组成。它使用了一个布尔列表 dp
来跟踪字符串的每个子串是否可以由字典中的单词构成。最后,返回 dp[len(s)]
的值,表示整个字符串 s
是否满足条件。在代码的最后,有一个测试示例来验证函数的正确性。
Java 版本
import java.util.List;
import java.util.HashSet;
import java.util.Set;
public class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> wordSet = new HashSet<>(wordDict);
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++) {
if (dp[j] && wordSet.contains(s.substring(j, i))) {
dp[i] = true;
break;
}
}
}
return dp[s.length()];
}
public static void main(String[] args) {
Solution solution = new Solution();
String s = "bestcode";
List<String> wordDict = List.of("best", "code");
System.out.println(solution.wordBreak(s, wordDict)); // 应输出: true
}
}
这段 Java 代码实现了一个 Solution
类,其中包含了 wordBreak
方法,该方法使用动态规划来判断字符串 s
是否可以由 wordDict
中的单词组成。在 main
方法中,我们创建了一个 Solution
实例,并调用 wordBreak
方法,然后打印出结果。这里使用了 List.of
方法来创建单词列表,这是 Java 9 及以上版本提供的便捷方法。如果你使用的是 Java 8 或更低版本,你需要使用 Arrays.asList
或其他方式来创建单词列表。
Rust 版本
use std::collections::HashSet; fn word_break(s: String, word_dict: Vec<String>) -> bool { let word_set: HashSet<String> = word_dict.into_iter().collect(); let mut dp = vec![false; s.len() + 1]; dp[0] = true; for i in 1..=s.len() { for j in 0..i { if dp[j] && word_set.contains(&s[j..i]) { dp[i] = true; break; } } } dp[s.len()] } fn main() { let s = String::from("bestcode") let word_dict = vec![String::from("best"), String::from("code")]; println!("{}", word_break(s, word_dict)); // 应输出: true }
这段 Rust 代码实现了 word break 问题的解决方案。它定义了一个word_break
函数,该函数接受一个字符串s
和一个字符串向量word_dict
作为输入,并返回一个布尔值,表示s
是否可以由word_dict
中的单词组成。在main
函数中,我们创建了一个示例字符串和字典,然后调用word_break
函数,并输出结果。
总结
上述解法采用了动态规划的方法来解决字符串分割问题。核心思想是使用一个布尔数组 dp
来记录字符串 s
的每个子串是否可以由字典 wordDict
中的单词组成。数组的每个位置 i
对应字符串 s
的前 i
个字符。如果 s
的前 i
个字符可以由 wordDict
中的单词组成,那么 dp[i]
就为 true
,否则为 false
。
算法的步骤如下:
- 初始化
dp
数组,长度为s.length() + 1
,并将dp[0]
设置为true
,表示空字符串总是可以由字典中的零个单词组成。 - 遍历字符串
s
,对于每个位置i
,再遍历wordDict
中的所有单词。如果某个单词word
可以放在当前位置i
之前,并且dp[i - word.length()]
为true
,则将dp[i]
设置为true
。 - 最终,
dp[s.length()]
的值即为整个字符串s
是否可以由wordDict
中的单词组成的答案。
这种方法的优势在于它避免了重复计算,并且能够高效地解决问题。在实际代码实现中,可以根据不同的编程语言特性进行适当的调整,但核心算法逻辑是一致的。
最长递增子序列
题目要求
给定一个整数数组 nums
,任务是找出这个数组中最长的严格递增子序列的长度。所谓的子序列是指可以通过删除原数组中的某些元素(也可以一个都不删),得到的一个新序列。在这个新序列中,元素的相对顺序保持不变。严格递增意味着子序列中的每个后续元素都比前一个元素大。
解题思路
解决这个问题的一个经典方法是使用动态规划。动态规划的基本思想是将问题分解成一系列相关的子问题,然后从最简单的子问题开始解决,逐步得到原问题的解。
-
初始化状态数组: 创建一个长度与原数组
nums
相同的数组dp
,其中dp[i]
表示以nums[i]
结尾的最长递增子序列的长度。初始时,每个元素的最长递增子序列至少包含它自己,所以将dp
数组的所有元素初始化为 1。 -
构建状态转移方程: 遍历数组
nums
,对于每个元素nums[i]
,再遍历其之前的所有元素nums[j]
(其中j < i
),如果nums[j] < nums[i]
,说明nums[i]
可以接在以nums[j]
结尾的递增子序列之后形成一个新的递增子序列。此时,我们应该更新dp[i]
,使其成为所有可能的递增子序列长度中的最大值,即dp[i] = max(dp[i], dp[j] + 1)
。 -
遍历状态数组: 在填充完整个
dp
数组后,最长递增子序列的长度即为dp
数组中的最大值。 -
优化: 如果需要进一步优化时间复杂度,可以使用二分查找的方法来优化内层循环。具体来说,可以维护一个数组
tails
,其中tails[k]
存储长度为k+1
的所有递增子序列的最小末尾元素。通过二分查找在tails
中找到nums[i]
应该插入的位置,可以在O(log n)
的时间复杂度内完成更新,从而将总体时间复杂度降低到O(n log n)
。
通过上述步骤,我们可以得到数组 nums
中最长严格递增子序列的长度。
Golang 版本
package main
import "fmt"
func lengthOfLIS(nums []int) int {
if len(nums) == 0 {
return 0
}
dp := make([]int, len(nums))
maxLen := 1
for i := range dp {
dp[i] = 1
}
for i := 1; i < len(nums); i++ {
for j := 0; j < i; j++ {
if nums[i] > nums[j] {
dp[i] = max(dp[i], dp[j]+1)
}
}
maxLen = max(maxLen, dp[i])
}
return maxLen
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func main() {
nums := []int{10, 9, 2, 5, 3, 7, 101, 18}
fmt.Println("Length of LIS is:", lengthOfLIS(nums))
}
这段代码实现了动态规划的方法来解决最长递增子序列的问题。它首先检查输入数组是否为空,然后初始化一个与输入数组相同长度的 dp
数组,用于存储到当前位置为止的最长递增子序列的长度。接着,它通过两层循环来填充 dp
数组,并在过程中记录下最长递增子序列的长度。最后,它返回这个最大长度。在 main
函数中,它定义了一个示例数组 nums
并调用 lengthOfLIS
函数来计算并打印最长递增子序列的长度。
C++版本
#include <iostream>
#include <vector>
#include <algorithm>
int lengthOfLIS(const std::vector<int>& nums) {
if (nums.empty()) return 0;
std::vector<int> dp(nums.size(), 1);
int maxLen = 1;
for (size_t i = 1; i < nums.size(); ++i) {
for (size_t j = 0; j < i; ++j) {
if (nums[i] > nums[j]) {
dp[i] = std::max(dp[i], dp[j] + 1);
}
}
maxLen = std::max(maxLen, dp[i]);
}
return maxLen;
}
int main() {
std::vector<int> nums = {10, 9, 2, 5, 3, 7, 101, 18};
std::cout << "Length of LIS is: " << lengthOfLIS(nums) << std::endl;
return 0;
}
这段 C++代码实现了动态规划算法来解决最长递增子序列问题。它首先检查输入的数组是否为空,然后创建一个动态规划数组 dp
来存储到当前元素为止的最长递增子序列的长度,并将其初始化为 1。接着,它使用两层循环来更新 dp
数组的值,并在过程中记录下最长递增子序列的长度。最后,它返回这个最大长度。在 main
函数中,它定义了一个示例数组 nums
并调用 lengthOfLIS
函数来计算并打印出最长递增子序列的长度。
Python 版本
def length_of_lis(nums):
if not nums:
return 0
dp = [1] * len(nums)
max_len = 1
for i in range(1, len(nums)):
for j in range(i):
if nums[i] > nums[j]:
dp[i] = max(dp[i], dp[j] + 1)
max_len = max(max_len, dp[i])
return max_len
# Example usage:
nums = [10, 9, 2, 5, 3, 7, 101, 18]
print("Length of LIS is:", length_of_lis(nums))
这段 Python 代码使用动态规划解决了最长递增子序列问题。它首先检查输入列表是否为空,然后创建一个列表 dp
来存储到当前元素为止的最长递增子序列的长度,并将其初始化为 1。接着,它使用两层循环来更新 dp
列表的值,并在过程中记录下最长递增子序列的长度。最后,它返回这个最大长度。代码的最后部分展示了如何使用这个函数,并打印出了示例列表的最长递增子序列的长度。
Java 版本
public class LIS {
public static int lengthOfLIS(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int[] dp = new int[nums.length];
int maxLen = 1;
for (int i = 0; i < nums.length; i++) {
dp[i] = 1;
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
maxLen = Math.max(maxLen, dp[i]);
}
return maxLen;
}
public static void main(String[] args) {
int[] nums = {10, 9, 2, 5, 3, 7, 101, 18};
System.out.println("Length of LIS is: " + lengthOfLIS(nums));
}
}
这段 Java 代码实现了动态规划算法来解决最长递增子序列问题。它首先检查输入的数组是否为空,然后创建一个动态规划数组 dp
来存储到当前元素为止的最长递增子序列的长度,并将其初始化为 1。接着,它使用两层循环来更新 dp
数组的值,并在过程中记录下最长递增子序列的长度。最后,它返回这个最大长度。在 main
方法中,它定义了一个示例数组 nums
并调用 lengthOfLIS
方法来计算并打印出最长递增子序列的长度。
Rust 版本
fn length_of_lis(nums: Vec<i32>) -> i32 { let mut dp = vec![1; nums.len()]; let mut max_len = 1; for i in 1..nums.len() { for j in 0..i { if nums[i] > nums[j] { dp[i] = dp[i].max(dp[j] + 1); } } max_len = max_len.max(dp[i]); } max_len } fn main() { let nums = vec![10, 9, 2, 5, 3, 7, 101, 18]; println!("Length of LIS is: {}", length_of_lis(nums)); }
这段 Rust 代码实现了动态规划算法来解决最长递增子序列问题。它首先创建一个动态规划向量 dp
,用于存储到当前元素为止的最长递增子序列的长度,并将其初始化为 1。接着,它使用两层循环来更新 dp
向量的值,并在过程中记录下最长递增子序列的长度。最后,它返回这个最大长度。在 main
函数中,它定义了一个示例向量 nums
并调用 length_of_lis
函数来计算并打印出最长递增子序列的长度。
总结
上述代码段展示了如何使用动态规划(Dynamic Programming, DP)算法来解决最长递增子序列(Longest Increasing Subsequence, LIS)的问题。无论是在 Go, C++, Python, Java 还是 Rust 语言中,核心算法的逻辑是一致的,主要步骤如下:
-
初始化一个数组(或向量)
dp
,其长度与输入数组nums
相同。dp[i]
表示以nums[i]
结尾的最长递增子序列的长度。初始时,每个元素至少可以构成长度为 1 的子序列,因此dp
数组的所有元素初始化为 1。 -
使用两层循环来填充
dp
数组:- 外层循环遍历
nums
数组的每个元素。 - 内层循环遍历当前元素之前的所有元素,检查是否可以形成递增序列。如果
nums[i]
大于nums[j]
(其中j < i
),则尝试更新dp[i]
为dp[j] + 1
和当前dp[i]
的较大值。
- 外层循环遍历
-
在内层循环中,每次更新
dp[i]
后,同时更新一个变量max_len
,该变量用于追踪到目前为止发现的最长递增子序列的长度。 -
最终,
max_len
将包含整个数组的最长递增子序列的长度,这个值被返回作为问题的解。
这种解法的时间复杂度为 O(n^2),其中 n 是输入数组 nums
的长度。这是因为需要双重循环遍历数组来构建 dp
数组。尽管这种方法不是最优的(存在 O(n log n) 的解法),但它相对简单且易于理解,适合作为动态规划入门问题的示例。
乘积最大子数组
题目要求
编写一个函数,该函数接收一个整数数组 nums
作为参数。你需要在这个数组中找到一个非空连续子数组,使得这个子数组中所有元素的乘积最大。返回这个最大乘积的值。结果必须是一个 32 位整数,即它必须在 [-2^31, 2^31 - 1]
的范围内。
解题思路
要解决这个问题,我们可以采用动态规划的方法。动态规划是一种通过把原问题分解为相对简单的子问题的方式来求解复杂问题的方法。对于这个问题,我们可以维护两个变量,分别记录到当前位置为止的最大乘积和最小乘积(因为一个负数乘以最小乘积可能会变成最大乘积)。
-
初始化两个变量
max_product
和min_product
,它们都等于数组的第一个元素,这代表了到目前为止的最大和最小乘积。同时,初始化最终的最大乘积max_result
为数组的第一个元素。 -
从数组的第二个元素开始遍历,对于每个元素
nums[i]
:- 计算包含当前元素的最大乘积
current_max
,它是以下三者中的最大值:nums[i]
、nums[i] * max_product
、nums[i] * min_product
。 - 计算包含当前元素的最小乘积
current_min
,它是以下三者中的最小值:nums[i]
、nums[i] * max_product
、nums[i] * min_product
。 - 更新
max_product
和min_product
的值为current_max
和current_min
。 - 更新
max_result
为max_product
和max_result
中的最大值。
- 计算包含当前元素的最大乘积
-
遍历完成后,
max_result
将包含数组中乘积最大的非空连续子数组的乘积。
注意:由于乘积可能会因为负数的存在而从最大变成最小,反之亦然,所以我们需要同时跟踪最大值和最小值。
这个问题的关键在于理解负数和零如何影响乘积,并且如何利用动态规划来跟踪和更新这些乘积。
Golang 版本
package main
import (
"fmt"
"math"
)
func maxProduct(nums []int) int {
if len(nums) == 0 {
return 0
}
maxProduct, minProduct, maxResult := nums[0], nums[0], nums[0]
for i := 1; i < len(nums); i++ {
if nums[i] < 0 {
maxProduct, minProduct = minProduct, maxProduct
}
maxProduct = max(nums[i], maxProduct*nums[i])
minProduct = min(nums[i], minProduct*nums[i])
maxResult = max(maxResult, maxProduct)
}
return maxResult
}
func max(x, y int) int {
if x > y {
return x
}
return y
}
func min(x, y int) int {
if x < y {
return x
}
return y
}
func main() {
nums := []int{2, 3, -2, 4}
fmt.Println("Maximum product of a subarray is:", maxProduct(nums))
}
这段代码定义了一个 maxProduct
函数,它接收一个整数数组 nums
并返回乘积最大的连续子数组的乘积。在 main
函数中,我们定义了一个测试用例,并打印出调用 maxProduct
函数的结果。
C++版本
#include <iostream>
#include <vector>
#include <algorithm>
int maxProduct(const std::vector<int>& nums) {
if (nums.empty()) return 0;
int maxProduct = nums[0];
int minProduct = nums[0];
int maxResult = nums[0];
for (int i = 1; i < nums.size(); ++i) {
if (nums[i] < 0) {
std::swap(maxProduct, minProduct);
}
maxProduct = std::max(nums[i], maxProduct * nums[i]);
minProduct = std::min(nums[i], minProduct * nums[i]);
maxResult = std::max(maxResult, maxProduct);
}
return maxResult;
}
int main() {
std::vector<int> nums = {2, 3, -2, 4};
std::cout << "Maximum product of a subarray is: " << maxProduct(nums) << std::endl;
return 0;
}
这段代码定义了一个 maxProduct
函数,它接收一个整数类型的 std::vector
作为参数,并返回乘积最大的连续子数组的乘积。在 main
函数中,我们定义了一个测试用例,并打印出调用 maxProduct
函数的结果。这里使用了 <algorithm>
头文件中的 std::max
和 std::min
函数来简化代码。
Python 版本
def max_product(nums):
if not nums:
return 0
max_product = min_product = max_result = nums[0]
for i in range(1, len(nums)):
if nums[i] < 0:
max_product, min_product = min_product, max_product
max_product = max(nums[i], max_product * nums[i])
min_product = min(nums[i], min_product * nums[i])
max_result = max(max_result, max_product)
return max_result
# 测试用例
nums = [2, 3, -2, 4]
print("Maximum product of a subarray is:", max_product(nums))
这段代码定义了一个 max_product
函数,它接收一个整数列表 nums
作为参数,并返回乘积最大的连续子数组的乘积。在测试用例中,我们定义了一个列表,并打印出调用 max_product
函数的结果。这里使用了 Python 内置的 max
和 min
函数来简化代码。
Java 版本
public class Solution {
public int maxProduct(int[] nums) {
if (nums == null || nums.length == 0) {
throw new IllegalArgumentException("Input array is null or empty");
}
int maxProduct = nums[0];
int minProduct = nums[0];
int maxResult = nums[0];
for (int i = 1; i < nums.length; i++) {
if (nums[i] < 0) {
int temp = maxProduct;
maxProduct = minProduct;
minProduct = temp;
}
maxProduct = Math.max(nums[i], maxProduct * nums[i]);
minProduct = Math.min(nums[i], minProduct * nums[i]);
maxResult = Math.max(maxResult, maxProduct);
}
return maxResult;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {2, 3, -2, 4};
System.out.println("Maximum product of a subarray is: " + solution.maxProduct(nums));
}
}
这段代码定义了一个名为 Solution
的类,其中包含一个 maxProduct
方法,该方法接收一个整数数组 nums
作为参数,并返回乘积最大的连续子数组的乘积。在 main
方法中,我们创建了一个 Solution
对象,并定义了一个测试用例,然后打印出调用 maxProduct
方法的结果。这里使用了 Java 的 Math.max
和 Math.min
方法来简化代码。
Rust 版本
fn max_product(nums: Vec<i32>) -> i32 { use std::cmp::{max, min}; let (mut max_product, mut min_product, mut max_result) = (nums[0], nums[0], nums[0]); for &num in nums.iter().skip(1) { let (temp_max, temp_min) = (max_product, min_product); max_product = max(num, max(temp_max * num, temp_min * num)); min_product = min(num, min(temp_max * num, temp_min * num)); max_result = max(max_result, max_product); } max_result } fn main() { let nums = vec![2, 3, -2, 4]; println!("Maximum product of a subarray is: {}", max_product(nums)); }
这段代码定义了一个 max_product
函数,它接收一个 Vec<i32>
类型的整数向量 nums
作为参数,并返回乘积最大的连续子数组的乘积。在 main
函数中,我们定义了一个测试用例,并打印出调用 max_product
函数的结果。这里使用了 Rust 的 std::cmp::{max, min}
来简化代码。
总结
上述代码段展示了如何在不同编程语言中实现寻找给定整数数组中乘积最大的连续子数组的问题。尽管每种语言的语法不同,但它们共享相同的核心算法逻辑:
- 初始化三个变量,分别用于存储当前的最大乘积、最小乘积(考虑到负数的影响)以及迄今为止的最大乘积结果。
- 遍历数组,对于每个元素:
- 如果当前元素是负数,则交换最大乘积和最小乘积的值,因为乘以负数会使得最大变最小,最小变最大。
- 更新当前最大乘积和最小乘积的值,这是通过将当前元素与当前元素与之前最大乘积和最小乘积的乘积进行比较得出的。
- 更新迄今为止的最大乘积结果。
- 遍历完成后,返回迄今为止的最大乘积结果。
这个问题的关键点在于理解如何处理负数对乘积的影响,以及如何在遍历数组的过程中更新和维护最大乘积和最小乘积。动态规划的思想在这里得到了应用,即每一步都基于前一步的结果进行决策和更新。
在实现上,每种语言都使用了其标准库中的最大值和最小值函数(如 max
和 min
),以及对数组或向量的迭代方法。尽管实现细节各有不同,但核心算法和逻辑是一致的。
分割等和子集
题目要求
这个问题是一个典型的判断型问题,要求我们确定一个只包含正整数的非空数组是否可以被分割成两个和相等的子集。这个问题实际上是一个背包问题的变种,可以通过动态规划来解决。
解题思路
要解决这个问题,我们可以采用动态规划的方法。以下是解题的步骤:
-
计算总和:首先,我们需要计算数组
nums
的总和sum
。如果sum
是奇数,那么不可能将数组分割成两个和相等的子集,因为两个整数和相等意味着每个子集的和必须是总和的一半,即sum / 2
,而奇数不能被平均分割成两个相等的整数。 -
初始化动态规划数组:如果
sum
是偶数,我们可以继续解题。我们创建一个布尔类型的动态规划数组dp
,大小为sum / 2 + 1
,用于存储子集和为索引值的可能性。dp[0]
是true
,因为不选择任何元素时,任何数组都可以形成和为 0 的子集。 -
填充动态规划数组:接下来,我们遍历数组
nums
,对于每个元素num
和每个可能的子集和i
(从sum / 2
到num
),我们更新dp[i]
。如果dp[i]
之前是false
,我们检查dp[i - num]
是否为true
,如果是,那么我们可以通过添加num
到和为i - num
的子集来形成和为i
的子集,因此我们将dp[i]
设置为true
。 -
检查结果:最后,我们检查
dp[sum / 2]
是否为true
。如果是,那么我们可以将数组分割成两个和相等的子集;如果不是,那么不可能分割。
这个动态规划解法的时间复杂度是 O(n * sum/2),其中 n 是数组 nums
的长度,空间复杂度是 O(sum/2),因为我们只需要一个一维数组来存储状态。
Golang 版本
package main
import "fmt"
func canPartition(nums []int) bool {
total := 0
for _, num := range nums {
total += num
}
// 如果总和是奇数,则不能平分
if total%2 != 0 {
return false
}
target := total / 2
dp := make([]bool, target+1)
dp[0] = true
for _, num := range nums {
for i := target; i >= num; i-- {
dp[i] = dp[i] || dp[i-num]
}
}
return dp[target]
}
func main() {
nums := []int{1, 5, 11, 5}
fmt.Println(canPartition(nums)) // 输出: true
}
这段代码定义了一个 canPartition
函数,它接受一个整数数组 nums
作为输入,并返回一个布尔值,表示是否可以将数组分割成两个和相等的子集。主函数 main
中给出了一个示例数组,并打印出调用 canPartition
函数的结果。
C++版本
#include <iostream>
#include <vector>
using namespace std;
bool canPartition(vector<int>& nums) {
int sum = 0;
for (int num : nums) {
sum += num;
}
if (sum % 2 != 0) return false; // 如果总和是奇数,则不能平分
int target = sum / 2;
vector<bool> dp(target + 1, false);
dp[0] = true;
for (int num : nums) {
for (int i = target; i >= num; --i) {
dp[i] = dp[i] || dp[i - num];
}
}
return dp[target];
}
int main() {
vector<int> nums = {1, 5, 11, 5};
cout << (canPartition(nums) ? "true" : "false") << endl; // 输出: true
return 0;
}
这段代码定义了一个 canPartition
函数,它接受一个整数向量 nums
作为输入,并返回一个布尔值,表示是否可以将数组分割成两个和相等的子集。main
函数中给出了一个示例向量,并打印出调用 canPartition
函数的结果。
Python 版本
def can_partition(nums):
total_sum = sum(nums)
# 如果总和是奇数,则不能平分
if total_sum % 2 != 0:
return False
target = total_sum // 2
dp = [False] * (target + 1)
dp[0] = True
for num in nums:
for i in range(target, num - 1, -1):
dp[i] = dp[i] or dp[i - num]
return dp[target]
# 示例
nums = [1, 5, 11, 5]
print(can_partition(nums)) # 输出: True
Java 版本
public class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
for (int num : nums) {
sum += num;
}
// 如果总和是奇数,则不能平分
if (sum % 2 != 0) return false;
int target = sum / 2;
boolean[] dp = new boolean[target + 1];
dp[0] = true;
for (int num : nums) {
for (int i = target; i >= num; i--) {
dp[i] = dp[i] || dp[i - num];
}
}
return dp[target];
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {1, 5, 11, 5};
System.out.println(solution.canPartition(nums)); // 输出: true
}
}
这段代码定义了一个 Solution
类,其中包含一个 canPartition
方法,该方法接受一个整数数组 nums
作为输入,并返回一个布尔值,表示是否可以将数组分割成两个和相等的子集。main
方法中给出了一个示例数组,并打印出调用 canPartition
方法的结果。
Rust 版本
fn can_partition(nums: Vec<i32>) -> bool { let total_sum: i32 = nums.iter().sum(); // 如果总和是奇数,则不能平分 if total_sum % 2 != 0 { return false; } let target = total_sum / 2; let mut dp = vec![false; (target + 1) as usize]; dp[0] = true; for &num in &nums { for i in (num..=target).rev() { if i >= num { dp[i as usize] = dp[i as usize] || dp[(i - num) as usize]; } } } dp[target as usize] } fn main() { let nums = vec![1, 5, 11, 5]; println!("{}", can_partition(nums)); // 输出: true }
这段代码定义了一个 can_partition
函数,它接受一个整数向量 nums
作为输入,并返回一个布尔值,表示是否可以将数组分割成两个和相等的子集。main
函数中给出了一个示例向量,并打印出调用 can_partition
函数的结果。
总结
上述问题是一个经典的动态规划问题,目标是确定一个只包含正整数的非空数组是否可以被分割成两个和相等的子集。解决方案遵循以下步骤:
-
计算数组总和:首先计算给定数组的总和。如果总和是奇数,直接返回
false
,因为奇数不能被平分成两个相等的整数。 -
确定目标子集和:将总和除以 2 得到目标子集和。
-
初始化动态规划数组:创建一个布尔类型的动态规划数组
dp
,其长度为目标子集和加一。这个数组用于存储从 0 到目标子集和的每个值是否可以通过数组中的数字组合得到。dp[0]
初始化为true
,因为总是可以组合出子集和为 0。 -
填充动态规划数组:遍历数组中的每个数字,然后逆序遍历从该数字到目标子集和的每个值。如果
dp[i]
在添加当前数字之前还不是true
,检查dp[i - num]
是否为true
。如果是,将dp[i]
设置为true
。 -
检查是否可以分割:最后,检查
dp[target]
是否为true
。如果是,说明可以将数组分割成两个和相等的子集。
这种方法在不同的编程语言中实现时,核心算法保持不变,只是语法和一些数据结构的使用有所差异。在所有给出的代码示例中,时间复杂度都是 O(n * sum/2),其中 n 是数组 nums
的长度,空间复杂度是 O(sum/2)。这是因为我们只需要一个一维数组来存储从 0 到目标子集和的可能性。
最长有效括号
题目要求
给定一个只包含字符 '(' 和 ')' 的字符串,任务是找出这个字符串中最长的有效括号子串的长度。有效括号子串指的是括号能正确匹配的连续序列。
解题思路
要解决这个问题,我们可以采用以下几种思路:
-
栈的应用:
- 创建一个栈来跟踪未匹配的括号。
- 遍历字符串,对于每个字符:
- 如果是 '(',将其索引压入栈。
- 如果是 ')':
- 弹出栈顶元素(如果栈不为空),这表示匹配了一个括号对。
- 如果栈为空,说明没有与当前 ')' 匹配的 '(',将当前索引压入栈作为新的起点。
- 如果栈不为空,计算当前有效子串的长度(当前索引 - 栈顶元素索引),并更新最长有效括号子串的长度。
- 栈中始终保持的是未匹配括号的索引,这样可以通过索引差来计算长度。
-
动态规划:
- 创建一个和输入字符串等长的数组 dp,dp[i] 表示以 i 结尾的最长有效括号子串的长度。
- 遍历字符串,对于每个 ')' 字符:
- 如果它前面的字符是 '(',则形成了一个有效对,dp[i] = dp[i-2] + 2。
- 如果它前面的字符也是 ')' 并且它前面的有效子串前面是 '(',则形成了一个更长的有效子串,dp[i] = dp[i-1] + dp[i - dp[i-1] - 2] + 2。
- 在遍历过程中,记录 dp 数组中的最大值,这就是最长有效括号子串的长度。
-
双向扫描:
- 从左到右和从右到左各扫描一次字符串。
- 在每次扫描中,用两个变量 left 和 right 分别记录遇到的 '(' 和 ')' 的数量。
- 在从左到右扫描时,每当 left 和 right 相等时,更新最长有效括号子串的长度;如果 right 大于 left,则重置两个计数器。
- 在从右到左扫描时,执行相同的操作,但如果 left 大于 right,则重置两个计数器。
- 这种方法的优点是不需要额外的空间,但需要两次遍历。
以上三种方法中,栈的方法和动态规划的方法都能得到准确的结果,而双向扫描的方法在某些情况下更高效,但需要注意正确处理计数器的重置。根据具体情况选择合适的方法来实现。
Golang 版本
package main
import (
"fmt"
)
func longestValidParentheses(s string) int {
var maxLen int
stack := []int{-1} // 初始化栈,先放入-1作为参照点
for i, char := range s {
if char == '(' {
// 如果是左括号,将索引压入栈
stack = append(stack, i)
} else {
// 如果是右括号,弹出栈顶元素
stack = stack[:len(stack)-1]
if len(stack) == 0 {
// 如果栈为空,将当前索引压入栈
stack = append(stack, i)
} else {
// 如果栈不为空,计算当前有效子串的长度
// 并更新最长有效括号子串的长度
currentLen := i - stack[len(stack)-1]
if currentLen > maxLen {
maxLen = currentLen
}
}
}
}
return maxLen
}
func main() {
// 示例
s := "(()())"
fmt.Println("最长有效括号子串的长度为:", longestValidParentheses(s))
}
这段代码使用了栈的方法来解决问题。它初始化一个栈,并在栈底预先放入一个-1 作为基准点。这样做是为了方便计算有效子串的长度。当遇到左括号时,将其索引压入栈中;遇到右括号时,尝试弹出栈顶元素,并根据栈的状态更新最长有效括号子串的长度。如果栈为空,则将当前索引压入栈中,这表示之前的所有括号都已匹配,当前的右括号没有匹配的左括号,需要重新设置一个新的起点。
C++版本
#include <iostream>
#include <stack>
#include <string>
using namespace std;
int longestValidParentheses(string s) {
int maxLen = 0;
stack<int> stk;
stk.push(-1); // 初始化栈,先放入-1作为参照点
for (int i = 0; i < s.length(); ++i) {
if (s[i] == '(') {
// 如果是左括号,将索引压入栈
stk.push(i);
} else {
// 如果是右括号,弹出栈顶元素
stk.pop();
if (stk.empty()) {
// 如果栈为空,将当前索引压入栈
stk.push(i);
} else {
// 如果栈不为空,计算当前有效子串的长度
// 并更新最长有效括号子串的长度
maxLen = max(maxLen, i - stk.top());
}
}
}
return maxLen;
}
int main() {
// 示例
string s = "(()())";
cout << "最长有效括号子串的长度为: " << longestValidParentheses(s) << endl;
return 0;
}
这段 C++代码同样采用了栈的方法来解决最长有效括号子串的问题。它首先创建一个栈,并将-1 压入栈中作为基准点。然后遍历字符串,遇到左括号时将其索引压入栈中,遇到右括号时尝试弹出栈顶元素。如果栈为空,则将当前索引压入栈中作为新的起点。如果栈不为空,则计算当前有效子串的长度,并更新最长有效括号子串的长度。最终返回最大长度作为结果。
Python 版本
def longest_valid_parentheses(s):
max_len = 0
stack = [-1] # 初始化栈,先放入-1作为参照点
for i, char in enumerate(s):
if char == '(':
# 如果是左括号,将索引压入栈
stack.append(i)
else:
# 如果是右括号,弹出栈顶元素
stack.pop()
if not stack:
# 如果栈为空,将当前索引压入栈
stack.append(i)
else:
# 如果栈不为空,计算当前有效子串的长度
# 并更新最长有效括号子串的长度
max_len = max(max_len, i - stack[-1])
return max_len
# 示例
s = "(()())"
print("最长有效括号子串的长度为:", longest_valid_parentheses(s))
这段 Python 代码使用了栈的方法来解决最长有效括号子串的问题。它首先创建一个栈,并将-1 压入栈中作为基准点。然后遍历字符串,遇到左括号时将其索引压入栈中,遇到右括号时尝试弹出栈顶元素。如果栈为空,则将当前索引压入栈中作为新的起点。如果栈不为空,则计算当前有效子串的长度,并更新最长有效括号子串的长度。最终返回最大长度作为结果。
Java 版本
import java.util.Stack;
public class Solution {
public int longestValidParentheses(String s) {
int maxLen = 0;
Stack<Integer> stack = new Stack<>();
stack.push(-1); // 初始化栈,先放入-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 {
// 如果栈不为空,计算当前有效子串的长度
// 并更新最长有效括号子串的长度
maxLen = Math.max(maxLen, i - stack.peek());
}
}
}
return maxLen;
}
public static void main(String[] args) {
Solution solution = new Solution();
String s = "(()())";
System.out.println("最长有效括号子串的长度为: " + solution.longestValidParentheses(s));
}
}
这段 Java 代码使用了栈的方法来解决最长有效括号子串的问题。它首先创建一个栈,并将-1 压入栈中作为基准点。然后遍历字符串,遇到左括号时将其索引压入栈中,遇到右括号时尝试弹出栈顶元素。如果栈为空,则将当前索引压入栈中作为新的起点。如果栈不为空,则计算当前有效子串的长度,并更新最长有效括号子串的长度。最终返回最大长度作为结果。
Rust 版本
fn longest_valid_parentheses(s: String) -> i32 { let mut max_len = 0; let mut stack: Vec<i32> = vec![-1]; // 初始化栈,先放入-1作为参照点 for (i, char) in s.chars().enumerate() { if char == '(' { // 如果是左括号,将索引压入栈 stack.push(i as i32); } else { // 如果是右括号,弹出栈顶元素 stack.pop(); if stack.is_empty() { // 如果栈为空,将当前索引压入栈 stack.push(i as i32); } else { // 如果栈不为空,计算当前有效子串的长度 // 并更新最长有效括号子串的长度 max_len = max_len.max(i as i32 - *stack.last().unwrap()); } } } max_len } fn main() { // 示例 let s = String::from("(()())"); println!("最长有效括号子串的长度为: {}", longest_valid_parentheses(s)); }
这段 Rust 代码使用了栈的方法来解决最长有效括号子串的问题。它首先创建一个栈,并将-1 压入栈中作为基准点。然后遍历字符串,遇到左括号时将其索引压入栈中,遇到右括号时尝试弹出栈顶元素。如果栈为空,则将当前索引压入栈中作为新的起点。如果栈不为空,则计算当前有效子串的长度,并更新最长有效括号子串的长度。最终返回最大长度作为结果。
总结
上述解法采用了栈(Stack)这一数据结构来解决最长有效括号子串的问题。核心思想是利用栈来跟踪和匹配每个括号的位置。具体步骤如下:
- 初始化一个栈,并将一个特殊值(通常是-1)压入栈中作为基准点,这个值代表的是最后一个没有被匹配的右括号的位置。
- 遍历字符串中的每个字符:
- 如果当前字符是左括号
'('
,则将其索引压入栈中。 - 如果当前字符是右括号
')'
:- 首先弹出栈顶元素,这代表尝试匹配一个左括号。
- 如果弹出栈顶元素后栈为空,说明没有左括号与当前右括号匹配,此时将当前字符的索引压入栈中,更新最后一个没有被匹配的右括号的位置。
- 如果栈不为空,则说明栈顶元素是与当前右括号匹配的左括号的索引,此时可以计算当前有效括号子串的长度(当前索引减去栈顶元素的值),并更新最长有效括号子串的长度。
- 如果当前字符是左括号
- 遍历完成后,得到的最长有效括号子串的长度即为所求。
这种方法的时间复杂度为 O(n),空间复杂度也为 O(n),其中 n 是字符串的长度。这是因为每个字符最多被压入和弹出栈一次。
这个算法的关键在于:
- 使用栈来处理括号的匹配问题。
- 使用栈底元素来记录最后一个未匹配的右括号的位置。
- 在遍历过程中,不断更新最长有效括号的长度。
这个解法是通用的,可以用在任何支持栈这一数据结构的编程语言中,如上面提供的 C++, Python, Java, 和 Rust 版本的代码。
多维动态规划
多维动态规划是动态规划的一种形式,它通常用于解决需要多个参数来定义状态的问题。这类问题的状态空间比一维动态规划要复杂得多,因此需要更多的细心和技巧来定义状态转移方程。
解决多维动态规划问题的通用思路可以分为以下几个步骤:
-
定义状态: 确定问题的状态,即解决问题所需的变量。在多维动态规划中,状态通常是一个多维数组,其中每个维度代表一个问题参数。
-
状态转移方程: 根据问题的具体情况,定义状态之间的转移关系。这是解决动态规划问题的核心,需要找到子问题与原问题之间的关系。
-
初始化状态: 确定初始条件,即动态规划表中已知的值。这些值通常对应于问题的边界情况。
-
确定遍历顺序: 根据状态转移方程的依赖关系,确定计算状态的顺序。在多维动态规划中,这可能涉及多个循环嵌套。
-
考虑优化: 对于一些特殊问题,可能存在空间优化的方法,例如使用滚动数组来减少空间复杂度。
-
解答构造: 根据动态规划表,构造问题的解答。这可能涉及从最终状态回溯到初始状态,或者直接从动态规划表中读取最终结果。
下面是一个使用 Go 语言实现的多维动态规划的例子。假设我们要解决一个经典的二维动态规划问题:给定一个二维网格,从左上角到右下角的最小路径和。
package main
import (
"fmt"
"math"
)
func minPathSum(grid [][]int) int {
if len(grid) == 0 || len(grid[0]) == 0 {
return 0
}
m, n := len(grid), len(grid[0])
// 创建一个二维dp数组
dp := make([][]int, m)
for i := range dp {
dp[i] = make([]int, n)
}
// 初始化状态
dp[0][0] = grid[0][0]
// 初始化第一行和第一列
for i := 1; i < m; i++ {
dp[i][0] = dp[i-1][0] + grid[i][0]
}
for j := 1; j < n; j++ {
dp[0][j] = dp[0][j-1] + grid[0][j]
}
// 计算dp数组的其余部分
for i := 1; i < m; i++ {
for j := 1; j < n; j++ {
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
}
}
// 返回右下角的值
return dp[m-1][n-1]
}
// 辅助函数,用于计算两个整数的最小值
func min(a, b int) int {
if a < b {
return a
}
return b
}
func main() {
grid := [][]int{
{1, 3, 1},
{1, 5, 1},
{4, 2, 1},
}
fmt.Println(minPathSum(grid)) // 输出应该是7
}
在这个例子中,我们定义了一个二维数组dp
,其中dp[i][j]
表示到达网格中第i
行第j
列位置的最小路径和。我们首先初始化了dp
数组的左上角元素以及第一行和第一列的值,然后使用两个嵌套循环来填充剩余的dp
数组。最后,我们返回dp
数组右下角的值,即整个网格的最小路径和。
让我们来看一个更复杂的多维动态规划问题:0-1 背包问题。在这个问题中,我们有一个背包和一系列物品,每个物品都有一个重量和一个价值。我们希望确定哪些物品应该被包含在背包中,以便背包中的物品总重量不超过给定的限制,同时物品的总价值最大化。
这个问题可以用一个二维动态规划数组来解决,其中dp[i][w]
表示考虑前i
个物品,在不超过重量w
的情况下能够获得的最大价值。
下面是用 Go 语言实现的 0-1 背包问题的代码:
package main
import (
"fmt"
)
// 0-1背包问题的动态规划解法
func knapsack(weights []int, values []int, W int) int {
n := len(weights)
// dp[i][w]表示考虑前i个物品,在不超过重量w的情况下能够获得的最大价值
dp := make([][]int, n+1)
for i := range dp {
dp[i] = make([]int, W+1)
}
// 构建动态规划表格
for i := 1; i <= n; i++ {
for w := 1; w <= W; w++ {
// 如果当前物品的重量超过了当前的重量限制,我们不能选择这个物品
if weights[i-1] > w {
dp[i][w] = dp[i-1][w]
} else {
// 否则,我们可以选择不拿当前物品,或者拿当前物品,两种情况下取价值较大的
dp[i][w] = max(dp[i-1][w], dp[i-1][w-weights[i-1]]+values[i-1])
}
}
}
// 返回最大价值
return dp[n][W]
}
// 辅助函数,用于计算两个整数的最大值
func max(a, b int) int {
if a > b {
return a
}
return b
}
func main() {
weights := []int{2, 3, 4, 5} // 物品的重量
values := []int{3, 4, 5, 6} // 物品的价值
W := 5 // 背包的最大承重
maxValue := knapsack(weights, values, W)
fmt.Println(maxValue) // 输出应该是7
}
在这个例子中,我们首先初始化了一个(n+1) x (W+1)
的二维切片dp
,其中n
是物品的数量,W
是背包的最大承重。然后我们使用两个嵌套循环来填充这个二维切片。对于每个物品和每个可能的重量限制,我们决定是选择当前物品还是不选择,以便最大化价值。最后,dp[n][W]
就是我们要找的最大价值。
这些例子展示了多维动态规划在解决具有多个决策维度的问题时的强大能力。通过定义状态、状态转移方程、初始化状态和确定遍历顺序,我们可以构建出解决问题的动态规划表,并最终得到问题的解。
不同路径
题目要求
这是一个经典的组合问题,可以用动态规划(Dynamic Programming)来解决。题目要求我们计算一个机器人从一个 m x n 网格的左上角(起点)移动到右下角(终点)的不同路径的总数。机器人在任何时刻只能向下或者向右移动。
解题思路
为了解决这个问题,我们可以采用以下步骤:
-
理解问题:首先,我们需要理解机器人每次只能向下或向右移动意味着什么。这意味着如果机器人在某个位置,它只能从它的上方或左方到达那里。
-
初始化边界:在网格的顶部行和最左侧列,机器人只有一种方式到达每个点(要么一直向右,要么一直向下)。因此,这些位置的路径数都是 1。
-
填充网格:对于网格中的其他点,机器人可以从上方或左方到达。因此,到达该点的路径总数是到达它上方格子的路径数加上到达它左方格子的路径数。
-
动态规划方程:我们可以定义一个二维数组 dp,其中 dp[i][j] 表示到达网格中第 i 行第 j 列的路径总数。状态转移方程为:dp[i][j] = dp[i-1][j] + dp[i][j-1]。
-
计算结果:按照从上到下,从左到右的顺序填充这个 dp 数组。最终,dp[m-1][n-1] 就是我们要找的答案,即到达网格右下角的路径总数。
-
优化空间复杂度:如果需要,我们可以只用一个一维数组来优化空间复杂度,因为更新 dp 数组的时候,我们只需要知道当前行的上一行的状态。
通过以上步骤,我们可以不使用递归(会导致大量重复计算),而是使用动态规划的方法,有效地计算出不同的路径总数。
Golang 版本
package main
import "fmt"
func uniquePaths(m int, n int) int {
// 创建一个二维切片来存储中间结果
dp := make([][]int, m)
for i := range dp {
dp[i] = make([]int, n)
}
// 初始化第一行和第一列的值为1,因为只有一条路径可以到达
for i := 0; i < m; i++ {
dp[i][0] = 1
}
for j := 0; j < n; j++ {
dp[0][j] = 1
}
// 从(1,1)开始,使用状态转移方程填充dp表
for i := 1; i < m; i++ {
for j := 1; j < n; j++ {
dp[i][j] = dp[i-1][j] + dp[i][j-1]
}
}
// 返回到达右下角的路径总数
return dp[m-1][n-1]
}
func main() {
m, n := 3, 7
fmt.Println("Total unique paths:", uniquePaths(m, n))
}
这段代码定义了一个uniquePaths
函数,它接受两个整数m
和n
作为输入,分别代表网格的行数和列数。然后,它使用动态规划的方法来计算从网格的左上角到右下角的唯一路径数,并在main
函数中调用这个函数并打印结果。
C++版本
#include <iostream>
#include <vector>
int uniquePaths(int m, int n) {
std::vector<std::vector<int>> dp(m, std::vector<int>(n, 1)); // 初始化dp数组,所有值为1
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; // 状态转移方程
}
}
return dp[m - 1][n - 1]; // 返回右下角的路径数
}
int main() {
int m = 3, n = 7;
std::cout << "Total unique paths: " << uniquePaths(m, n) << std::endl;
return 0;
}
这段 C++代码实现了与 Golang 版本相同的功能。它定义了一个uniquePaths
函数,使用动态规划来计算网格中的唯一路径数,并在main
函数中测试这个函数。
Python 版本
def unique_paths(m, n):
# 初始化dp表,所有值为1
dp = [[1] * n for _ in range(m)]
# 使用状态转移方程填充dp表
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
# 返回到达右下角的路径总数
return dp[-1][-1]
# 测试函数
if __name__ == "__main__":
m, n = 3, 7
print(f"Total unique paths: {unique_paths(m, n)}")
这段 Python 代码实现了与前面 Golang 和 C++版本相同的逻辑。它定义了一个unique_paths
函数来计算网格中的唯一路径数,并在脚本的主部分中测试了这个函数。
Java 版本
public class UniquePaths {
public static int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
// 初始化第一行和第一列
for (int i = 0; i < m; i++) {
dp[i][0] = 1;
}
for (int j = 0; j < n; j++) {
dp[0][j] = 1;
}
// 填充剩余的dp表
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
// 返回右下角的值
return dp[m - 1][n - 1];
}
public static void main(String[] args) {
int m = 3, n = 7;
System.out.println("Total unique paths: " + uniquePaths(m, n));
}
}
这段 Java 代码定义了一个UniquePaths
类,其中包含了一个uniquePaths
方法来计算网格中的唯一路径数。main
方法用于测试这个功能。
Rust 版本
fn unique_paths(m: i32, n: i32) -> i32 { let mut dp = vec![vec![1; n as usize]; m as usize]; for i in 1..m as usize { for j in 1..n as usize { dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; } } dp[(m - 1) as usize][(n - 1) as usize] } fn main() { let m = 3; let n = 7; println!("Total unique paths: {}", unique_paths(m, n)); }
这段 Rust 代码实现了一个unique_paths
函数,它计算从网格的左上角到右下角的不同路径的总数。在main
函数中,它测试了这个函数并打印出结果。
总结
上述代码示例展示了如何使用动态规划算法来解决机器人从网格左上角到右下角的不同路径问题。无论是在 Golang、C++、Python 还是 Java 和 Rust 语言中,核心思想都是相同的:
-
初始化一个二维数组(或切片、向量等),其大小与网格的行数和列数相同。这个数组用于存储到达每个格子的路径数。
-
将数组的第一行和第一列的所有元素设置为 1,因为到达第一行和第一列的任何格子都只有一条路径(只能一直向右或一直向下移动)。
-
对于数组中的其他元素,应用状态转移方程:
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
。这表示到达当前格子的路径数等于到达其正上方格子的路径数加上到达其左侧格子的路径数。 -
最后,数组的最后一个元素(
dp[m-1][n-1]
)即为到达网格右下角的总路径数。
这种方法避免了递归和重复计算,提供了一种高效的解决方案。每种语言的实现都遵循了这个算法框架,只是在语法和数据结构的使用上有所不同。
最小路径和
题目要求
设计一个算法,用于寻找一条从二维网格的左上角到右下角的路径,该路径上的数字之和最小。网格中包含的是非负整数,且在移动时只能向下或向右移动一步。
解题思路
这个问题是一个典型的动态规划问题。动态规划是一种通过把原问题分解为相对简单的子问题的方式来求解复杂问题的方法。
步骤一:定义状态
首先定义一个二维数组 dp
,其中 dp[i][j]
表示从左上角走到网格中的点 (i, j)
时,能够获得的最小路径和。
步骤二:状态转移方程
对于网格中的每一个点,有两种方式到达它:从左边的格子向右走一步,或者从上面的格子向下走一步。因此,到达点 (i, j)
的最小路径和就是从 (i-1, j)
和 (i, j-1)
中选择一个路径和较小的,再加上当前点的值 grid[i][j]
。
状态转移方程为:
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
步骤三:初始化边界条件
动态规划的边界条件是算法的起点。在这个问题中,边界条件是 dp
数组的第一行和第一列。
- 对于第一行
dp[0][j]
,它只能从左边的格子向右走到达,因此它的值是从(0, 0)
到(0, j)
的路径和。 - 对于第一列
dp[i][0]
,它只能从上面的格子向下走到达,因此它的值是从(0, 0)
到(i, 0)
的路径和。
步骤四:填充 dp 数组
按照从上到下,从左到右的顺序,依次计算 dp
数组中每个点的值。每个点的值依赖于它上方和左方的点的值,这样可以保证每个点的值在计算时它依赖的值都已经计算过了。
步骤五:返回结果
最后,dp
数组的最右下角的值 dp[m-1][n-1]
就是从左上角到右下角的最小路径和。
通过这个动态规划的方法,我们可以有效地解决这个问题,时间复杂度和空间复杂度通常都是 O(m*n),其中 m 和 n 分别是网格的行数和列数。
Golang 版本
package main
import (
"fmt"
"math"
)
func minPathSum(grid [][]int) int {
if len(grid) == 0 || len(grid[0]) == 0 {
return 0
}
m, n := len(grid), len(grid[0])
dp := make([][]int, m)
for i := range dp {
dp[i] = make([]int, n)
}
// 初始化边界条件
dp[0][0] = grid[0][0]
for i := 1; i < m; i++ {
dp[i][0] = dp[i-1][0] + grid[i][0]
}
for j := 1; j < n; j++ {
dp[0][j] = dp[0][j-1] + grid[0][j]
}
// 填充 dp 数组
for i := 1; i < m; i++ {
for j := 1; j < n; j++ {
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
}
}
// 返回结果
return dp[m-1][n-1]
}
// min 返回两个整数中较小的一个
func min(a, b int) int {
if a < b {
return a
}
return b
}
func main() {
grid := [][]int{
{1, 3, 1},
{1, 5, 1},
{4, 2, 1},
}
fmt.Println(minPathSum(grid)) // 输出应为 7
}
C++版本
#include <iostream>
#include <vector>
#include <algorithm>
int minPathSum(std::vector<std::vector<int>>& grid) {
if (grid.empty() || grid[0].empty()) {
return 0;
}
int m = grid.size(), n = grid[0].size();
std::vector<std::vector<int>> dp(m, std::vector<int>(n, 0));
// 初始化边界条件
dp[0][0] = grid[0][0];
for (int i = 1; i < m; ++i) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
for (int j = 1; j < n; ++j) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
// 填充 dp 数组
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j) {
dp[i][j] = std::min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
// 返回结果
return dp[m - 1][n - 1];
}
int main() {
std::vector<std::vector<int>> grid = {
{1, 3, 1},
{1, 5, 1},
{4, 2, 1}
};
std::cout << minPathSum(grid) << std::endl; // 输出应为 7
return 0;
}
Python 版本
def minPathSum(grid):
if not grid or not grid[0]:
return 0
m, n = len(grid), len(grid[0])
dp = [[0] * n for _ in range(m)]
# 初始化边界条件
dp[0][0] = grid[0][0]
for i in range(1, m):
dp[i][0] = dp[i - 1][0] + grid[i][0]
for j in range(1, n):
dp[0][j] = dp[0][j - 1] + grid[0][j]
# 填充 dp 数组
for i in range(1, m):
for j in range(1, n):
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]
# 返回结果
return dp[-1][-1]
# 测试代码
grid = [
[1, 3, 1],
[1, 5, 1],
[4, 2, 1]
]
print(minPathSum(grid)) # 输出应为 7
Java 版本
public class Solution {
public int minPathSum(int[][] grid) {
if (grid == null || grid.length == 0 || grid[0].length == 0) {
return 0;
}
int m = grid.length, n = grid[0].length;
int[][] dp = new int[m][n];
// 初始化边界条件
dp[0][0] = grid[0][0];
for (int i = 1; i < m; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
for (int j = 1; j < n; j++) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
// 填充 dp 数组
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
// 返回结果
return dp[m - 1][n - 1];
}
public static void main(String[] args) {
Solution solution = new Solution();
int[][] grid = {
{1, 3, 1},
{1, 5, 1},
{4, 2, 1}
};
System.out.println(solution.minPathSum(grid)); // 输出应为 7
}
}
Rust 版本
fn min_path_sum(grid: Vec<Vec<i32>>) -> i32 { let m = grid.len(); let n = grid[0].len(); let mut dp = vec![vec![0; n]; m]; // 初始化边界条件 dp[0][0] = grid[0][0]; for i in 1..m { dp[i][0] = dp[i - 1][0] + grid[i][0]; } for j in 1..n { dp[0][j] = dp[0][j - 1] + grid[0][j]; } // 填充 dp 数组 for i in 1..m { for j in 1..n { dp[i][j] = i32::min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]; } } // 返回结果 dp[m - 1][n - 1] } fn main() { let grid = vec![ vec![1, 3, 1], vec![1, 5, 1], vec![4, 2, 1], ]; println!("{}", min_path_sum(grid)); // 输出应为 7 }
总结
上述解法采用了动态规划的方法来解决最小路径和问题。这个问题的核心是找到从矩阵的左上角到右下角的最小路径和。解决方案遵循以下步骤:
-
定义状态: 创建一个二维数组
dp
,其中dp[i][j]
表示从左上角(0, 0)
到达(i, j)
的最小路径和。 -
初始化状态:
- 初始化
dp[0][0]
为grid[0][0]
,因为这是起点。 - 初始化第一行和第一列的
dp
值,因为第一行只能从左边走到右边,第一列只能从上面走到下面。
- 初始化
-
状态转移方程: 对于
dp
数组中的其他元素,计算到达每个点的最小路径和。这是通过取其上方和左方的两个邻居的最小值并加上当前格子的值来实现的,即dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
。 -
计算结果: 按照从上到下,从左到右的顺序,依次计算
dp
数组中每个点的值,直到填充完整个dp
数组。 -
返回结果: 最后,
dp[m-1][n-1]
(其中m
和n
分别是矩阵的行数和列数)就是从左上角到右下角的最小路径和。
这种方法的时间复杂度和空间复杂度都是 O(mn),其中 m 是矩阵的行数,n 是矩阵的列数。通过这种方式,我们可以有效地解决问题,而不需要递归或者复杂的算法。
最长回文子串
题目要求
编写一个算法来寻找给定字符串s
中的最长回文子串。回文子串是指一个子串正读和反读都相同的字符串。如果存在多个长度相同的最长回文子串,返回任意一个即可。
解题思路
解决这个问题的关键是要理解回文的特性,以及如何高效地检查子串是否为回文。以下是几种可能的解题思路:
-
暴力法:检查字符串
s
的所有可能子串,判断它们是否为回文,并记录下最长的回文子串。这种方法的时间复杂度是 O(n^3),因为需要 O(n^2)时间枚举所有子串,然后需要 O(n)时间来检查每个子串是否为回文。 -
动态规划:使用动态规划的方法,创建一个二维数组
dp[i][j]
,其中i
和j
表示字符串s
的索引,dp[i][j]
表示从索引i
到j
的子串是否为回文。通过状态转移方程来填充这个表格,可以在 O(n^2)时间内解决问题。 -
中心扩展法:考虑到回文串是对称的,我们可以选择一个中心点,向两边扩展来检查最长的回文子串。由于中心点可能是一个字符(奇数长度的回文串)或者两个字符之间的空隙(偶数长度的回文串),所以总共有
2n-1
个中心点。这种方法的时间复杂度是 O(n^2)。 -
Manacher 算法:这是一个专门用来寻找最长回文子串的线性时间算法(O(n))。算法首先对原始字符串进行预处理,每个字符之间插入一个特殊字符(例如
#
),这样可以将奇数长度和偶数长度的回文串统一处理。然后使用一系列的指针来记录当前访问到的最右端的回文串的信息,以及这个回文串的中心位置,从而在遍历字符串的时候高效地更新最长回文子串的信息。
在实际应用中,中心扩展法通常是实现起来最简单的,而 Manacher 算法虽然最为高效,但实现起来较为复杂。根据具体情况选择合适的算法。
Golang 版本
package main
import "fmt"
func longestPalindrome(s string) string {
if len(s) < 1 {
return ""
}
start, end := 0, 0
for i := 0; i < len(s); i++ {
len1 := expandAroundCenter(s, i, i)
len2 := expandAroundCenter(s, i, i+1)
len := max(len1, len2)
if len > end-start {
start = i - (len-1)/2
end = i + len/2
}
}
return s[start : end+1]
}
func expandAroundCenter(s string, left, right int) int {
L, R := left, right
for L >= 0 && R < len(s) && s[L] == s[R] {
L--
R++
}
return R - L - 1
}
func max(x, y int) int {
if x > y {
return x
}
return y
}
func main() {
fmt.Println("The longest palindromic substring is:", longestPalindrome("babad"))
}
这段代码实现了中心扩展法。longestPalindrome
函数找到并返回字符串s
中的最长回文子串。expandAroundCenter
函数用于给定中心进行扩展,检查以该中心为中心的最长回文子串的长度。max
函数用于比较两个整数的大小。在main
函数中,我们调用longestPalindrome
函数并打印结果。
C++版本
#include <iostream>
#include <string>
#include <vector>
using namespace std;
string longestPalindrome(string s) {
if (s.empty()) return "";
int n = s.size(), longestBegin = 0, maxLen = 1;
vector<vector<bool>> dp(n, vector<bool>(n, false));
for (int i = 0; i < n; i++) {
dp[i][i] = true; // 1 character is always a palindrome
}
for (int i = 0; i < n - 1; i++) {
if (s[i] == s[i + 1]) {
dp[i][i + 1] = true;
longestBegin = i;
maxLen = 2;
}
}
for (int len = 3; len <= n; len++) { // check for lengths greater than 2
for (int i = 0; i < n - len + 1; i++) {
int j = i + len - 1;
if (s[i] == s[j] && dp[i + 1][j - 1]) {
dp[i][j] = true;
longestBegin = i;
maxLen = len;
}
}
}
return s.substr(longestBegin, maxLen);
}
int main() {
string s = "babad";
cout << "The longest palindromic substring is: " << longestPalindrome(s) << endl;
return 0;
}
这段代码使用了动态规划的方法来解决最长回文子串的问题。dp[i][j]
表示子串s[i..j]
是否为回文串。首先初始化长度为 1 和 2 的子串的回文状态,然后使用两层循环来填充整个dp
表,外层循环控制子串的长度,内层循环控制子串的起始位置。如果一个子串的首尾字符相同,并且去掉首尾字符后的子串也是回文的,则该子串是回文的。每次找到更长的回文子串时,更新longestBegin
和maxLen
。最后,使用substr
函数返回最长的回文子串。
Python 版本
def longest_palindrome(s: str) -> str:
if len(s) == 0:
return ""
start = 0
end = 0
for i in range(len(s)):
len1 = expand_around_center(s, i, i)
len2 = expand_around_center(s, i, i + 1)
max_len = max(len1, len2)
if max_len > end - start:
start = i - (max_len - 1) // 2
end = i + max_len // 2
return s[start:end + 1]
def expand_around_center(s: str, left: int, right: int) -> int:
L, R = left, right
while L >= 0 and R < len(s) and s[L] == s[R]:
L -= 1
R += 1
return R - L - 1
# Example usage:
if __name__ == "__main__":
input_string = "babad"
print("The longest palindromic substring is:", longest_palindrome(input_string))
这段 Python 代码实现了中心扩展法来找到最长的回文子串。longest_palindrome
函数遍历每个字符,将其作为中心来扩展回文串。expand_around_center
函数用于计算以left
和right
为中心的最长回文串的长度。最后,通过切片返回最长的回文子串。在主函数中,我们调用longest_palindrome
函数并打印结果。
Java 版本
public class Solution {
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;
}
public static void main(String[] args) {
Solution solution = new Solution();
String s = "babad";
System.out.println("The longest palindromic substring is: " + solution.longestPalindrome(s));
}
}
这段 Java 代码使用了中心扩展算法来找到最长的回文子串。longestPalindrome
方法遍历字符串中的每个字符,并尝试将每个字符或两个相邻字符作为回文的中心,然后向两边扩展。expandAroundCenter
方法用于找到以给定的中心能扩展出的最长回文的长度。最后,使用substring
方法来获取并返回最长的回文子串。在main
方法中,创建了Solution
类的实例,并调用longestPalindrome
方法来输出最长的回文子串。
Rust 版本
fn longest_palindrome(s: String) -> String { let chars: Vec<char> = s.chars().collect(); let mut start = 0; let mut end = 0; for i in 0..chars.len() { let len1 = expand_around_center(&chars, i, i); let len2 = expand_around_center(&chars, i, i + 1); let len = len1.max(len2); if len > end - start { start = i - (len - 1) / 2; end = i + len / 2; } } chars[start..=end].iter().collect() } fn expand_around_center(chars: &[char], mut left: usize, mut right: usize) -> usize { while left > 0 && right < chars.len() && chars[left] == chars[right] { if left > 0 { left -= 1; } right += 1; } if chars[left] != chars[right] { left += 1; } // Adjust left boundary if not matching right - left } fn main() { let input_string = "babad".to_string(); println!("The longest palindromic substring is: {}", longest_palindrome(input_string)); }
这段 Rust 代码实现了寻找最长回文子串的功能。longest_palindrome
函数首先将输入的字符串转换为字符数组,然后遍历每个字符,使用expand_around_center
函数来找到以该字符为中心的最长回文子串的长度。expand_around_center
函数负责向两边扩展,直到不再是回文为止。最后,使用数组切片语法和iter().collect()
来构造并返回最长的回文子串。在main
函数中,我们调用longest_palindrome
函数并打印结果。
总结
上述代码示例中,无论是 Python、Java 还是 Rust 版本,核心算法都是基于中心扩展法来寻找最长回文子串。这种方法的基本思想是:
- 遍历字符串中的每个字符,将每个字符视为回文串的中心点。
- 对于每个中心点,考虑奇数长度和偶数长度的回文串两种情况,分别向两边扩展,直到不再满足回文条件。
- 在扩展过程中,记录并更新最长回文子串的起始和结束位置。
- 最后,根据记录的位置,提取并返回最长的回文子串。
不同语言的实现细节有所差异,但算法的核心逻辑是一致的。在 Python 和 Java 中,字符串可以直接通过索引访问和切片操作;而在 Rust 中,由于字符串的索引操作不同,需要将字符串转换为字符数组来进行操作。此外,Rust 的边界检查更为严格,需要额外注意索引操作时的边界条件。
这种中心扩展法的时间复杂度为 O(n^2),其中 n 是输入字符串的长度。空间复杂度为 O(1),因为除了输入字符串外,只需要常数空间来存储索引值。
最长公共子序列
题目要求
给定两个字符串 text1
和 text2
,目标是找到这两个字符串之间的最长公共子序列(Longest Common Subsequence, LCS)的长度。公共子序列指的是两个字符串中都出现的、并且在每个字符串内部字符顺序相同的序列。这个序列不需要在原字符串中连续出现。如果没有公共子序列,则返回长度为 0。
解题思路
解决这个问题的经典方法是使用动态规划(Dynamic Programming, DP)。动态规划是一种算法思想,它将一个问题分解为相似的子问题,通过解决子问题来递推得到最终问题的解。
-
初始化 DP 表格:创建一个二维数组
dp
,其中dp[i][j]
表示text1
中前i
个字符和text2
中前j
个字符的最长公共子序列的长度。由于子序列的长度可以为 0,所以dp
表的大小应该是(len(text1) + 1) x (len(text2) + 1)
,并且初始化第一行和第一列为 0,因为任何字符串与空字符串的最长公共子序列长度都是 0。 -
填充 DP 表格:遍历两个字符串,对于
dp
表中的每个元素dp[i][j]
,有两种情况:- 如果
text1[i - 1] == text2[j - 1]
,说明这两个字符匹配,那么dp[i][j]
应该是dp[i - 1][j - 1]
加 1,因为我们找到了一个额外的匹配字符。 - 如果
text1[i - 1] != text2[j - 1]
,说明这两个字符不匹配,那么dp[i][j]
应该是dp[i - 1][j]
和dp[i][j - 1]
中的较大值,因为我们要找到最长的子序列,所以我们需要考虑不包含text1[i - 1]
或不包含text2[j - 1]
的情况。
- 如果
-
遍历 DP 表格:从
dp[1][1]
开始,按行或按列遍历整个表格,根据上述规则填充每个dp[i][j]
。 -
返回结果:最终
dp[len(text1)][len(text2)]
将包含text1
和text2
的最长公共子序列的长度,这就是我们要找的答案。
通过这种方法,我们可以确保所有的子问题都被解决,并且每个子问题只解决一次,最终得到的解是最优的。
Golang 版本
package main
import (
"fmt"
"math"
)
// longestCommonSubsequence function calculates the length of the longest common subsequence
func longestCommonSubsequence(text1 string, text2 string) int {
m, n := len(text1), len(text2)
// Create a DP table with extra row and column for base case (empty string)
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
}
// Fill the DP table
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if text1[i-1] == text2[j-1] {
// If characters match, take diagonal value and add 1
dp[i][j] = dp[i-1][j-1] + 1
} else {
// If characters don't match, take the max value from left or top cell
dp[i][j] = int(math.Max(float64(dp[i-1][j]), float64(dp[i][j-1])))
}
}
}
// The bottom-right cell contains the length of LCS
return dp[m][n]
}
func main() {
text1 := "abcde"
text2 := "ace"
fmt.Println("The length of the longest common subsequence is:", longestCommonSubsequence(text1, text2))
}
这段代码定义了一个函数 longestCommonSubsequence
,它接受两个字符串 text1
和 text2
作为输入,并返回它们之间的最长公共子序列的长度。主函数 main
中提供了两个示例字符串,并打印出了它们的最长公共子序列长度。
C++版本
#include <iostream>
#include <vector>
#include <algorithm>
// Function to find the length of the longest common subsequence
int longestCommonSubsequence(std::string text1, std::string text2) {
int m = text1.length(), n = text2.length();
std::vector<std::vector<int>> dp(m + 1, std::vector<int>(n + 1));
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (text1[i - 1] == text2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = std::max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
int main() {
std::string text1 = "abcde";
std::string text2 = "ace";
std::cout << "The length of the longest common subsequence is: "
<< longestCommonSubsequence(text1, text2) << std::endl;
return 0;
}
这段 C++代码实现了一个函数 longestCommonSubsequence
,它计算两个给定字符串 text1
和 text2
的最长公共子序列的长度。在 main
函数中,我们定义了两个示例字符串,并输出了它们的最长公共子序列的长度。
Python 版本
def longest_common_subsequence(text1, text2):
m, n = len(text1), len(text2)
# Create a DP table with an extra row and column for the base case (empty string)
dp = [[0] * (n + 1) for _ in range(m + 1)]
# Fill the DP table
for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i - 1] == text2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
# The bottom-right cell contains the length of the LCS
return dp[m][n]
# Example usage
text1 = "abcde"
text2 = "ace"
print("The length of the longest common subsequence is:", longest_common_subsequence(text1, text2))
这段 Python 代码定义了一个函数 longest_common_subsequence
,它接受两个字符串 text1
和 text2
作为输入,并返回它们之间的最长公共子序列的长度。在示例用法中,我们定义了两个字符串,并打印出了它们的最长公共子序列的长度。
Java 版本
public class LCS {
// Function to find the length of the longest common subsequence
public static int longestCommonSubsequence(String text1, String text2) {
int m = text1.length();
int n = text2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
public static void main(String[] args) {
String text1 = "abcde";
String text2 = "ace";
System.out.println("The length of the longest common subsequence is: " + longestCommonSubsequence(text1, text2));
}
}
这段 Java 代码定义了一个名为 LCS
的类,其中包含一个静态方法 longestCommonSubsequence
,用于计算两个字符串 text1
和 text2
的最长公共子序列的长度。在 main
方法中,我们提供了两个示例字符串,并打印出了它们的最长公共子序列的长度。
Rust 版本
fn longest_common_subsequence(text1: &str, text2: &str) -> usize { let m = text1.len(); let n = text2.len(); let mut dp = vec![vec![0; n + 1]; m + 1]; let text1_bytes = text1.as_bytes(); let text2_bytes = text2.as_bytes(); for i in 1..=m { for j in 1..=n { if text1_bytes[i - 1] == text2_bytes[j - 1] { dp[i][j] = dp[i - 1][j - 1] + 1; } else { dp[i][j] = std::cmp::max(dp[i - 1][j], dp[i][j - 1]); } } } dp[m][n] } fn main() { let text1 = "abcde"; let text2 = "ace"; println!( "The length of the longest common subsequence is: {}", longest_common_subsequence(text1, text2) ); }
这段 Rust 代码定义了一个函数 longest_common_subsequence
,它接受两个字符串切片 text1
和 text2
作为参数,并返回它们的最长公共子序列的长度。在 main
函数中,我们定义了两个示例字符串,并打印出了它们的最长公共子序列的长度。
总结
上述解法采用了动态规划(Dynamic Programming, DP)的方法来解决最长公共子序列(Longest Common Subsequence, LCS)问题。无论是在 C++、Python、Java 还是 Rust 语言中,核心算法的逻辑是相同的,主要步骤如下:
-
初始化一个二维数组
dp
,其大小为(m+1) x (n+1)
,其中m
和n
分别是输入字符串text1
和text2
的长度。dp[i][j]
表示text1
的前i
个字符和text2
的前j
个字符的最长公共子序列的长度。 -
遍历两个字符串的每个字符,对于
dp
数组中的每个元素dp[i][j]
:- 如果
text1
的第i
个字符和text2
的第j
个字符相同,那么dp[i][j]
就是dp[i-1][j-1]
加上这两个匹配字符构成的子序列的长度,即dp[i][j] = dp[i-1][j-1] + 1
。 - 如果不相同,那么
dp[i][j]
就是dp[i-1][j]
和dp[i][j-1]
中的较大值,即不包含text1
的第i
个字符或不包含text2
的第j
个字符的最长公共子序列的长度。
- 如果
-
继续填充
dp
数组,直到遍历完所有字符。 -
dp[m][n]
就是最终的结果,即text1
和text2
的最长公共子序列的长度。
这种方法的时间复杂度和空间复杂度都是 O(mn),其中 m 和 n 是两个字符串的长度。动态规划是解决此类问题的经典方法,因为它将问题分解为子问题,并存储子问题的解,避免了重复计算。
编辑距离
题目要求
给定两个字符串 word1
和 word2
,要求通过以下三种操作将 word1
转换为 word2
:
- 插入一个字符
- 删除一个字符
- 替换一个字符
目标是找出并返回将 word1
转换成 word2
所需的最少操作次数。
解题思路
这个问题是一个经典的动态规划问题,通常被称为编辑距离问题(Edit Distance)。解决这个问题的思路可以分为以下几个步骤:
-
初始化二维数组 dp:创建一个
(m+1) x (n+1)
的二维数组dp
,其中m
和n
分别是word1
和word2
的长度。dp[i][j]
表示word1
的前i
个字符转换成word2
的前j
个字符所需的最少操作数。初始化dp[0][0]
为0
,dp[i][0]
为i
(即删除操作),dp[0][j]
为j
(即插入操作)。 -
填充 dp 数组:遍历
word1
和word2
,对于每一对字符word1[i]
和word2[j]
,计算dp[i][j]
。有三种情况:- 如果
word1[i]
等于word2[j]
,则dp[i][j]
应该等于dp[i-1][j-1]
(无需操作)。 - 如果
word1[i]
不等于word2[j]
,则dp[i][j]
应该是以下三个值中的最小值加一:dp[i-1][j]
(word1
删除一个字符后与word2
比较)dp[i][j-1]
(word1
插入一个字符后与word2
比较)dp[i-1][j-1]
(word1
替换一个字符后与word2
比较)
- 如果
-
返回结果:经过上述步骤后,
dp[m][n]
就是将word1
转换成word2
所需的最少操作数。
通过这种方法,我们可以确保每一步都是基于之前计算的最优解,从而确保最终结果是全局最优的。
Golang 版本
package main
import (
"fmt"
"math"
)
// minDistance calculates the minimum number of operations required to convert word1 to word2
func minDistance(word1 string, word2 string) int {
m, n := len(word1), len(word2)
// Create a DP table to store results of subproblems
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
}
// Initialize the DP table
for i := 0; i <= m; i++ {
dp[i][0] = i
}
for j := 0; j <= n; j++ {
dp[0][j] = j
}
// Fill the DP table
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if word1[i-1] == word2[j-1] {
dp[i][j] = dp[i-1][j-1]
} else {
dp[i][j] = 1 + min(
dp[i-1][j], // Delete
dp[i][j-1], // Insert
dp[i-1][j-1], // Replace
)
}
}
}
// The result is in dp[m][n]
return dp[m][n]
}
// min returns the minimum of three integers
func min(a, b, c int) int {
return int(math.Min(float64(a), math.Min(float64(b), float64(c))))
}
func main() {
word1 := "intention"
word2 := "execution"
fmt.Println("Minimum operations required to convert:", minDistance(word1, word2))
}
这段代码定义了一个 minDistance
函数,它接受两个字符串 word1
和 word2
并返回将 word1
转换为 word2
所需的最少操作数。它使用动态规划的方法来填充一个二维数组 dp
,其中 dp[i][j]
表示将 word1
的前 i
个字符转换为 word2
的前 j
个字符所需的最少操作数。最后,dp[m][n]
就是答案。主函数 main
提供了一个示例调用。
C++版本
#include <iostream>
#include <vector>
#include <algorithm>
// Function to find the minimum number of operations to convert word1 to word2
int minDistance(std::string word1, std::string word2) {
int m = word1.length(), n = word2.length();
std::vector<std::vector<int>> dp(m + 1, std::vector<int>(n + 1, 0));
// Initialize the dp array
for (int i = 0; i <= m; i++) dp[i][0] = i;
for (int j = 0; j <= n; j++) dp[0][j] = j;
// Compute the dp array
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1[i - 1] == word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = 1 + std::min({dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]});
}
}
}
// The answer is in dp[m][n]
return dp[m][n];
}
int main() {
std::string word1 = "intention";
std::string word2 = "execution";
std::cout << "Minimum operations required to convert: " << minDistance(word1, word2) << std::endl;
return 0;
}
这段 C++代码实现了一个 minDistance
函数,它计算并返回将字符串 word1
转换为字符串 word2
所需的最少操作数。它使用动态规划方法,通过构建一个二维的 dp
数组来存储中间结果。dp[i][j]
表示将 word1
的前 i
个字符转换为 word2
的前 j
个字符所需的最少操作数。最终结果存储在 dp[m][n]
中。主函数 main
提供了一个示例用于演示如何调用 minDistance
函数。
Python 版本
def minDistance(word1, word2):
m, n = len(word1), len(word2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
# Initialize the dp array
for i in range(m + 1):
dp[i][0] = i
for j in range(n + 1):
dp[0][j] = j
# Compute the dp array
for i in range(1, m + 1):
for j in range(1, n + 1):
if word1[i - 1] == word2[j - 1]:
dp[i][j] = dp[i - 1][j - 1]
else:
dp[i][j] = 1 + min(dp[i - 1][j], # Delete
dp[i][j - 1], # Insert
dp[i - 1][j - 1]) # Replace
# The answer is in dp[m][n]
return dp[m][n]
# Example usage
word1 = "intention"
word2 = "execution"
print("Minimum operations required to convert:", minDistance(word1, word2))
这段 Python 代码定义了一个 minDistance
函数,它接受两个字符串 word1
和 word2
并返回将 word1
转换为 word2
所需的最少操作数。它使用动态规划的方法来填充一个二维列表 dp
,其中 dp[i][j]
表示将 word1
的前 i
个字符转换为 word2
的前 j
个字符所需的最少操作数。最后,dp[m][n]
就是答案。代码的最后部分提供了一个示例调用。
Java 版本
public class Solution {
public int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
int[][] dp = new int[m + 1][n + 1];
// Initialize the dp array
for (int i = 0; i <= m; i++) {
dp[i][0] = i;
}
for (int j = 0; j <= n; j++) {
dp[0][j] = j;
}
// Compute the dp array
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = 1 + Math.min(dp[i - 1][j], // Delete
Math.min(dp[i][j - 1], // Insert
dp[i - 1][j - 1])); // Replace
}
}
}
// The answer is in dp[m][n]
return dp[m][n];
}
public static void main(String[] args) {
Solution solution = new Solution();
String word1 = "intention";
String word2 = "execution";
System.out.println("Minimum operations required to convert: " + solution.minDistance(word1, word2));
}
}
这段 Java 代码定义了一个 Solution
类,其中包含一个 minDistance
方法,用于计算将一个字符串转换为另一个字符串所需的最少操作数。它使用动态规划方法,通过构建一个二维数组 dp
来存储中间结果。dp[i][j]
表示将 word1
的前 i
个字符转换为 word2
的前 j
个字符所需的最少操作数。main
方法提供了一个示例,展示了如何使用 minDistance
方法。
Rust 版本
fn min_distance(word1: &str, word2: &str) -> i32 { let m = word1.len(); let n = word2.len(); let mut dp = vec![vec![0; n + 1]; m + 1]; // Initialize the dp array for i in 0..=m { dp[i][0] = i as i32; } for j in 0..=n { dp[0][j] = j as i32; } let word1_bytes = word1.as_bytes(); let word2_bytes = word2.as_bytes(); // Compute the dp array for i in 1..=m { for j in 1..=n { if word1_bytes[i - 1] == word2_bytes[j - 1] { dp[i][j] = dp[i - 1][j - 1]; } else { dp[i][j] = 1 + std::cmp::min( std::cmp::min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1], ); } } } // The answer is in dp[m][n] dp[m][n] } fn main() { let word1 = "intention"; let word2 = "execution"; println!("Minimum operations required to convert: {}", min_distance(word1, word2)); }
这段 Rust 代码定义了一个 min_distance
函数,它接受两个字符串切片 word1
和 word2
并返回一个 i32
类型的值,表示将 word1
转换为 word2
所需的最少操作数。它使用动态规划的方法来填充一个二维向量 dp
,其中 dp[i][j]
表示将 word1
的前 i
个字符转换为 word2
的前 j
个字符所需的最少操作数。最后,dp[m][n]
就是答案。main
函数提供了一个示例调用。
总结
上述代码片段展示了如何使用动态规划(Dynamic Programming, DP)算法来解决字符串编辑距离问题。这个问题要求我们找出将一个字符串(word1
)转换成另一个字符串(word2
)所需的最小操作次数,其中操作包括插入、删除和替换字符。
解决方案的核心思想是创建一个二维数组(在 Rust 中是一个向量的向量),记为 dp
,其中 dp[i][j]
表示将 word1
的前 i
个字符转换成 word2
的前 j
个字符所需的最小操作次数。数组的大小是 (m+1) x (n+1)
,其中 m
和 n
分别是 word1
和 word2
的长度。
初始化步骤如下:
dp[0][0]
初始化为 0,因为空字符串转换成空字符串不需要任何操作。dp[i][0]
初始化为i
,因为将长度为i
的字符串转换成空字符串需要i
次删除操作。dp[0][j]
初始化为j
,因为将空字符串转换成长度为j
的字符串需要j
次插入操作。
填充 dp
数组的过程遵循以下规则:
- 如果
word1[i - 1]
等于word2[j - 1]
,则dp[i][j]
等于dp[i - 1][j - 1]
,因为当前字符已经匹配,不需要额外操作。 - 如果不相等,则
dp[i][j]
等于以下三个值中的最小值加一:dp[i - 1][j]
:从word1
删除一个字符后的最小操作数。dp[i][j - 1]
:在word1
中插入一个字符后的最小操作数。dp[i - 1][j - 1]
:替换word1
中的一个字符后的最小操作数。
最终,dp[m][n]
就是将 word1
转换成 word2
所需的最小操作次数。这种方法确保了所有可能的字符串操作序列都被考虑到,并且找到了最优解。
技巧
解决 LeetCode 上的算法题通常需要遵循一些通用的思路和策略。以下是一些解题步骤和技巧,以及相应的 Go 语言代码示例。
通用解题思路:
- 理解题目:仔细阅读题目,理解题目的要求和限制条件。
- 确定算法策略:根据题目的特点选择合适的算法策略,如贪心、动态规划、回溯、分治等。
- 考虑边界情况:思考并处理输入数据的边界情况和特殊情况。
- 编写代码:根据选定的算法策略编写代码。
- 测试验证:对编写的代码进行测试,验证其正确性和性能。
Go 语言代码示例:
1. 只出现一次的数字
题目描述:给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
解题思路:使用位运算中的异或运算。异或运算有一个特性,两个相同的数字异或结果为 0,任何数字与 0 异或还是它本身。
Go 语言代码示例:
func singleNumber(nums []int) int {
result := 0
for _, num := range nums {
result ^= num
}
return result
}
2. 多数元素
题目描述:给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋
的元素。
解题思路:使用摩尔投票法。维护一个候选众数和它出现的次数,遍历数组,如果次数为 0,则换一个候选众数,否则如果遇到相同的数则次数加 1,不同则减 1。
Go 语言代码示例:
func majorityElement(nums []int) int {
count := 0
candidate := 0
for _, num := range nums {
if count == 0 {
candidate = num
}
if num == candidate {
count++
} else {
count--
}
}
return candidate
}
3. 颜色分类
题目描述:给定一个包含红色、白色和蓝色,一共 n
个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
解题思路:使用三指针方法,一个指针用于红色的右边界,一个指针用于蓝色的左边界,一个指针用于当前考虑的元素。
Go 语言代码示例:
func sortColors(nums []int) {
red, white, blue := 0, 0, len(nums)-1
for white <= blue {
switch nums[white] {
case 0:
nums[red], nums[white] = nums[white], nums[red]
red++
white++
case 1:
white++
case 2:
nums[white], nums[blue] = nums[blue], nums[white]
blue--
}
}
}
4. 下一个排列
题目描述:实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。
解题思路:从后向前查找第一个相邻升序的元素对 (i, i+1)
,然后从后向前查找第一个大于 nums[i]
的元素并交换,最后将 i+1
到末尾的部分翻转。
Go 语言代码示例:
func nextPermutation(nums []int) {
i := len(nums) - 2
for i >= 0 && nums[i] >= nums[i+1] {
i--
}
if i >= 0 {
j := len(nums) - 1
for j >= 0 && nums[i] >= nums[j] {
j--
}
nums[i], nums[j] = nums[j], nums[i]
}
reverse(nums, i+1)
}
func reverse(nums []int, start int) {
i, j := start, len(nums)-1
for i < j {
nums[i], nums[j] = nums[j], nums[i]
i++
j--
}
}
5. 寻找重复数
题目描述:给定一个包含 n + 1
个整数的数组 nums
,其数字都在 1
到 n
之间(包括 1
和 n
),假设只有一个重复的整数,找出这个重复的数。
解题思路:使用快慢指针法(Floyd's Tortoise and Hare),类似于链表找环的入口。
Go 语言代码示例:
func findDuplicate(nums []int) int {
slow := nums[0]
fast := nums[nums[0]]
// Find the intersection point of the two runners.
for slow != fast {
slow = nums[slow]
fast = nums[nums[fast]]
}
// Find the "entrance" to the cycle.
slow = 0
for slow != fast {
slow = nums[slow]
fast = nums[fast]
}
return slow
}
在解决算法问题时,理解问题和确定解题策略是至关重要的。上述代码示例展示了如何使用 Go 语言来实现这些策略。在实际编码时,还需要注意代码的整洁和性能优化。
只出现一次的数字
题目要求
给定一个非空的整数数组nums
,其中有一个元素只出现一次,其他元素都恰好出现两次。要求找出并返回那个只出现了一次的元素。
解题算法必须满足以下条件:
- 时间复杂度为线性(O(n)),即只能遍历数组一次。
- 空间复杂度为常量(O(1)),即算法在处理过程中只能使用固定大小的额外空间。
解题思路
要在线性时间内解决问题,并且只使用常量空间,可以使用位运算中的异或(XOR)操作。异或操作有以下几个性质:
- 任何数和 0 做异或运算,结果仍然是原来的数,即
a ^ 0 = a
。 - 任何数和其自身做异或运算,结果是 0,即
a ^ a = 0
。 - 异或运算满足交换律和结合律,即
a ^ b ^ a = (a ^ a) ^ b = 0 ^ b = b
。
根据以上性质,我们可以通过一次遍历数组,对数组中的所有元素进行异或运算。由于数组中除了一个元素只出现一次以外,其他元素都出现两次,那么出现两次的元素异或运算的结果会是 0,最终剩下的结果就是只出现一次的那个元素。
具体步骤如下:
- 初始化一个变量,用于存储异或运算的结果,初始值为 0。
- 遍历数组
nums
,对每个元素进行异或运算,即result ^= nums[i]
。 - 遍历完成后,
result
中存储的就是只出现一次的元素。
这种方法之所以有效,是因为异或运算能够消除成对的相同数字,最终留下的就是那个唯一的、未成对的数字。
Golang 版本
package main
import "fmt"
func singleNumber(nums []int) int {
result := 0
for _, num := range nums {
result ^= num
}
return result
}
func main() {
nums := []int{4, 1, 2, 1, 2}
fmt.Println("The single number is:", singleNumber(nums))
}
C++版本
#include <iostream>
#include <vector>
int singleNumber(std::vector<int>& nums) {
int result = 0;
for (int num : nums) {
result ^= num;
}
return result;
}
int main() {
std::vector<int> nums = {4, 1, 2, 1, 2};
std::cout << "The single number is: " << singleNumber(nums) << std::endl;
return 0;
}
Python 版本
def single_number(nums):
result = 0
for num in nums:
result ^= num
return result
if __name__ == "__main__":
nums = [4, 1, 2, 1, 2]
print("The single number is:", single_number(nums))
Java 版本
public class Solution {
public static int singleNumber(int[] nums) {
int result = 0;
for (int num : nums) {
result ^= num;
}
return result;
}
public static void main(String[] args) {
int[] nums = {4, 1, 2, 1, 2};
System.out.println("The single number is: " + singleNumber(nums));
}
}
Rust 版本
fn single_number(nums: Vec<i32>) -> i32 { nums.iter().fold(0, |acc, &x| acc ^ x) } fn main() { let nums = vec![4, 1, 2, 1, 2]; println!("The single number is: {}", single_number(nums)); }
总结
上述问题的解决方案是利用异或运算的性质来找出数组中唯一一个只出现一次的元素。异或运算(XOR)有几个关键性质:任何数与 0 异或仍然为自己,任何数与自己异或结果为 0,且异或运算满足交换律和结合律。
在各个版本的代码中(Go, C++, Python, Java, Rust),核心算法都是通过一个变量来累积数组中所有元素的异或结果。由于数组中的元素除了一个单独的数字外,其他数字都是成对出现的,成对的数字异或结果为 0,因此遍历完整个数组后,累积变量中的值即为那个唯一出现一次的数字。
具体步骤如下:
- 初始化一个变量用于存储结果,初始值为 0。
- 遍历数组中的每个元素,将其与结果变量进行异或运算。
- 遍历结束后,结果变量中存储的值即为只出现一次的元素。
这种方法的时间复杂度为 O(n),因为我们只需要遍历数组一次。空间复杂度为 O(1),因为我们只使用了一个额外的变量来存储结果,不依赖于输入数组的大小。因此,这种方法满足了题目要求的线性时间复杂度和常量空间复杂度的条件。
多数元素
题目要求
给定一个大小为 n 的数组 nums
,要求找出并返回这个数组中的“多数元素”。所谓“多数元素”,是指在数组中出现次数超过 n/2
的元素(这里的 n
是数组的长度)。题目假设条件为数组非空,并且保证一定存在一个多数元素。
解题思路
要解决这个问题,我们可以采用几种不同的方法:
-
哈希表法:遍历数组,使用哈希表记录每个元素出现的次数,然后再次遍历哈希表,找到出现次数大于
n/2
的元素。这种方法的时间复杂度为 O(n),空间复杂度也为 O(n)。 -
排序法:将数组排序,由于多数元素的数量超过
n/2
,因此排序后位于中间位置的元素(即索引为n/2
的元素)一定是多数元素。这种方法的时间复杂度为 O(nlogn),因为排序通常需要 O(nlogn) 的时间,空间复杂度取决于所使用的排序算法。 -
摩尔投票法:这是一种时间复杂度为 O(n) 且空间复杂度为 O(1) 的高效算法。算法的基本思想是维护一个候选多数元素和一个计数器,初始化时计数器为 0。遍历数组,对于每个元素,如果计数器为 0,则将当前元素作为候选多数元素,计数器设置为 1。如果计数器不为 0,当遇到与候选多数元素相同的元素时,计数器加 1,否则减 1。由于多数元素的数量超过了数组长度的一半,因此最后候选的元素一定是多数元素。
-
随机化:由于多数元素的数量超过了数组长度的一半,我们可以随机选取一个元素,然后检查它是否是多数元素,这样做的期望时间复杂度是 O(n),但最坏情况下可能会达到无限大,因为可能需要无限次随机尝试才能找到多数元素。
在实际应用中,摩尔投票法因其优秀的时间和空间复杂度表现通常是最佳选择。
Golang 版本
package main
import "fmt"
func majorityElement(nums []int) int {
candidate := nums[0]
count := 1
// 摩尔投票法
for _, num := range nums[1:] {
if count == 0 {
// 更换候选人
candidate = num
count = 1
} else if candidate == num {
// 候选人计数加一
count++
} else {
// 非候选人计数减一
count--
}
}
// candidate 即为多数元素
return candidate
}
func main() {
nums := []int{2, 2, 1, 1, 1, 2, 2}
fmt.Println("多数元素是:", majorityElement(nums))
}
这段代码实现了摩尔投票法,用于找出给定数组中的多数元素。在主函数 main
中,我们定义了一个数组 nums
并调用 majorityElement
函数来找出并打印多数元素。
C++版本
#include <iostream>
#include <vector>
int majorityElement(std::vector<int>& nums) {
int count = 0;
int candidate = 0;
// 使用摩尔投票法
for (int num : nums) {
if (count == 0) {
candidate = num;
}
count += (num == candidate) ? 1 : -1;
}
return candidate;
}
int main() {
std::vector<int> nums = {2, 2, 1, 1, 1, 2, 2};
std::cout << "多数元素是:" << majorityElement(nums) << std::endl;
return 0;
}
这段 C++代码同样实现了摩尔投票法,用于找出给定数组中的多数元素。在 main
函数中,我们定义了一个 vector
类型的数组 nums
并调用 majorityElement
函数来找出并打印多数元素。
Python 版本
def majority_element(nums):
count = 0
candidate = None
# 使用摩尔投票法
for num in nums:
if count == 0:
candidate = num
count += (1 if num == candidate else -1)
return candidate
# 测试代码
if __name__ == "__main__":
nums = [2, 2, 1, 1, 1, 2, 2]
print("多数元素是:", majority_element(nums))
Java 版本
public class MajorityElement {
public static int majorityElement(int[] nums) {
int count = 0;
Integer candidate = null;
for (int num : nums) {
if (count == 0) {
candidate = num;
}
count += (num == candidate) ? 1 : -1;
}
return candidate;
}
public static void main(String[] args) {
int[] nums = {2, 2, 1, 1, 1, 2, 2};
System.out.println("多数元素是:" + majorityElement(nums));
}
}
这段 Java 代码实现了摩尔投票法,用于找出给定数组中的多数元素。在 main
方法中,我们定义了一个数组 nums
并调用 majorityElement
方法来找出并打印多数元素。
Rust 版本
fn majority_element(nums: Vec<i32>) -> i32 { let mut count = 0; let mut candidate = nums[0]; for &num in nums.iter().skip(1) { if count == 0 { candidate = num; } if num == candidate { count += 1; } else { count -= 1; } } candidate } fn main() { let nums = vec![2, 2, 1, 1, 1, 2, 2]; println!("多数元素是:{}", majority_element(nums)); }
这段 Rust 代码实现了摩尔投票法,用于找出给定向量中的多数元素。在 main
函数中,我们定义了一个向量 nums
并调用 majority_element
函数来找出并打印多数元素。
总结
上述代码示例中,无论是 Golang、C++、Python 还是 Java 和 Rust 版本,核心算法都是基于摩尔投票法(Boyer-Moore Voting Algorithm)。这个算法的基本思想是通过一系列的对消过程,找到那个出现次数超过数组长度一半的元素,即“多数元素”。
摩尔投票法的步骤如下:
- 初始化一个候选多数元素
candidate
和一个计数器count
为 0。 - 遍历数组中的每个元素
num
:- 如果
count
为 0,将num
设为candidate
。 - 如果
num
等于candidate
,则count
加一。 - 如果
num
不等于candidate
,则count
减一。
- 如果
- 遍历完成后,
candidate
即为多数元素。
这种算法的优势在于它的空间复杂度为 O(1),即只需要常数级别的额外空间。时间复杂度为 O(n),因为它只需要遍历数组一次。这使得摩尔投票法成为解决这类问题的一个高效算法。
在实际代码实现中,各种语言的语法有所不同,但算法的核心逻辑是一致的。例如,在 Rust 中使用 iter()
和 skip()
来遍历数组,而在 Python 中则直接使用 for
循环。尽管实现细节不同,但所有版本的代码都遵循了摩尔投票法的基本原则,成功地解决了寻找多数元素的问题。
颜色分类
题目要求
给定一个数组 nums
,它包含三种元素,分别用整数 0、1 和 2 表示,这些整数分别代表红色、白色和蓝色。数组中的元素总数为 n
。要求编写一个算法,能够原地(不使用额外空间)对这个数组进行排序,使得排序后数组中相同颜色的元素相邻,并且按照红色、白色、蓝色的顺序排列。在这个问题中,你不能使用任何编程语言的库内置的排序函数。
解题思路
这个问题可以用三指针的方法来解决,通常被称为荷兰国旗问题,因为它涉及到三种颜色的排序,类似荷兰国旗的三色区分。以下是解题的步骤:
- 初始化三个指针:
p0
、p1
和p2
。其中p0
指向数组的开始位置,p1
指向数组的当前考察位置,p2
指向数组的末尾。 - 遍历数组,移动
p1
指针:- 如果
nums[p1] == 0
,说明是红色,应该放在数组的前面。将nums[p1]
和nums[p0]
交换,然后p0
和p1
都向前移动一位。 - 如果
nums[p1] == 1
,说明是白色,已经在正确的位置,只需要将p1
向前移动一位。 - 如果
nums[p1] == 2
,说明是蓝色,应该放在数组的后面。将nums[p1]
和nums[p2]
交换,然后p2
向后移动一位。这里不移动p1
,因为交换后的nums[p1]
可能是任何颜色,需要在下一次迭代中再次检查。
- 如果
- 当
p1
超过p2
时,遍历结束,此时数组已经按照红色、白色、蓝色的顺序排好。
这个算法的关键在于,p0
的左侧都是红色(0),p2
的右侧都是蓝色(2),而p1
在遍历中将数组中的白色(1)留在中间。通过交换元素,我们可以确保每次迭代后,所有颜色都被放置到它们正确的位置上。这个过程不需要额外的数组空间,因此是原地排序。
Golang 版本
package main
import "fmt"
func sortColors(nums []int) {
p0, p1 := 0, 0
p2 := len(nums) - 1
for p1 <= p2 {
switch nums[p1] {
case 0:
nums[p0], nums[p1] = nums[p1], nums[p0]
p0++
p1++
case 1:
p1++
case 2:
nums[p1], nums[p2] = nums[p2], nums[p1]
p2--
}
}
}
func main() {
nums := []int{2, 0, 2, 1, 1, 0}
sortColors(nums)
fmt.Println(nums) // Output should be [0, 0, 1, 1, 2, 2]
}
这段代码实现了上述的三指针方法,用于原地排序一个包含红色(0)、白色(1)和蓝色(2)的数组。在 main
函数中,我们定义了一个示例数组,并调用 sortColors
函数来对其进行排序,最后打印出排序后的数组。
C++版本
#include <iostream>
#include <vector>
void sortColors(std::vector<int>& nums) {
int p0 = 0, p1 = 0, p2 = nums.size() - 1;
while (p1 <= p2) {
if (nums[p1] == 0) {
std::swap(nums[p0++], nums[p1++]);
} else if (nums[p1] == 1) {
p1++;
} else if (nums[p1] == 2) {
std::swap(nums[p1], nums[p2--]);
}
}
}
int main() {
std::vector<int> nums = {2, 0, 2, 1, 1, 0};
sortColors(nums);
for (int num : nums) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
这段 C++代码实现了与 Golang 版本相同的逻辑。它定义了一个 sortColors
函数,该函数接受一个整数类型的向量引用 nums
,并使用三指针技术对其进行排序。在 main
函数中,我们创建了一个示例向量 nums
,调用 sortColors
函数对其进行排序,并打印出排序后的结果。
Python 版本
def sort_colors(nums):
p0, p1, p2 = 0, 0, len(nums) - 1
while p1 <= p2:
if nums[p1] == 0:
nums[p0], nums[p1] = nums[p1], nums[p0]
p0 += 1
p1 += 1
elif nums[p1] == 1:
p1 += 1
else:
nums[p1], nums[p2] = nums[p2], nums[p1]
p2 -= 1
if __name__ == "__main__":
nums = [2, 0, 2, 1, 1, 0]
sort_colors(nums)
print(nums) # Output should be [0, 0, 1, 1, 2, 2]
这段 Python 代码实现了前面描述的三指针方法,用于原地排序一个包含红色(0)、白色(1)和蓝色(2)的列表。在 if __name__ == "__main__":
块中,我们定义了一个示例列表 nums
,调用 sort_colors
函数来对其进行排序,并打印出排序后的列表。
Java 版本
public class Solution {
public void sortColors(int[] nums) {
int p0 = 0, p1 = 0, p2 = nums.length - 1;
while (p1 <= p2) {
if (nums[p1] == 0) {
int temp = nums[p0];
nums[p0] = nums[p1];
nums[p1] = temp;
p0++;
p1++;
} else if (nums[p1] == 1) {
p1++;
} else {
int temp = nums[p2];
nums[p2] = nums[p1];
nums[p1] = temp;
p2--;
}
}
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {2, 0, 2, 1, 1, 0};
solution.sortColors(nums);
for (int num : nums) {
System.out.print(num + " ");
}
}
}
这段 Java 代码实现了之前描述的三指针方法,用于原地排序一个包含红色(0)、白色(1)和蓝色(2)的数组。在 main
方法中,我们创建了一个示例数组 nums
,调用 sortColors
方法来对其进行排序,并打印出排序后的数组。
Rust 版本
fn sort_colors(nums: &mut Vec<i32>) { let (mut p0, mut p1, mut p2) = (0, 0, nums.len() as i32 - 1); while p1 <= p2 { match nums[p1 as usize] { 0 => { nums.swap(p0 as usize, p1 as usize); p0 += 1; p1 += 1; }, 1 => p1 += 1, 2 => { nums.swap(p1 as usize, p2 as usize); p2 -= 1; }, _ => (), } } } fn main() { let mut nums = vec![2, 0, 2, 1, 1, 0]; sort_colors(&mut nums); println!("{:?}", nums); // Output should be [0, 0, 1, 1, 2, 2] }
这段 Rust 代码实现了之前描述的三指针方法,用于原地排序一个包含红色(0)、白色(1)和蓝色(2)的向量。在 main
函数中,我们创建了一个示例向量 nums
,调用 sort_colors
函数来对其进行排序,并打印出排序后的向量。
总结
上述解法采用了一个称为“三指针”的方法来解决颜色排序问题,这个问题也被称为荷兰国旗问题。这种方法的核心思想是将数组分为三个部分:
- 红色区域(0),由指针
p0
定界。 - 白色区域(1),在指针
p1
和p2
之间。 - 蓝色区域(2),由指针
p2
定界。
算法的执行过程如下:
- 初始化三个指针:
p0
和p1
都设置为数组的起始位置,p2
设置为数组的末尾位置。 - 遍历数组,直到
p1
超过p2
。 - 根据
p1
指向的元素执行操作:- 如果元素是 0,将其与
p0
指向的元素交换,并将p0
和p1
都向前移动一位。 - 如果元素是 1,只将
p1
向前移动一位。 - 如果元素是 2,将其与
p2
指向的元素交换,并将p2
向后移动一位。
- 如果元素是 0,将其与
- 当
p1
超过p2
时,所有的 0 都在数组的前面,所有的 2 都在数组的后面,而 1 自然就位于中间,从而完成排序。
这种方法的优点是它能够在一次遍历中完成排序,时间复杂度为 O(n),并且不需要使用额外的存储空间,空间复杂度为 O(1),即原地排序。这种方法适用于有限数量的唯一值需要排序的情况,特别是在这些值表示特定类别或颜色时。
下一个排列
题目要求
给定一个整数数组nums
,要求找到按照字典序排列的该数组的下一个排列。如果当前排列已经是最大的,即不存在更大的排列,则需要将数组重排为最小的排列,即其元素按照升序排列。在找到下一个排列的过程中,必须在原数组上进行修改,且只能使用常数级别的额外空间。
解题思路
解决这个问题的关键是要理解字典序排列的规则。字典序的下一个排列是比当前排列大的最小的排列。为了找到这样的排列,我们可以从后向前查找,遵循以下步骤:
-
从后向前查找第一个相邻升序对:从数组的末尾开始向前查找,找到第一对相邻的元素
nums[i]
和nums[i+1]
,满足nums[i] < nums[i+1]
。这一步是为了找到需要进行交换的较小数。如果找不到这样的对,说明当前排列已经是最大的排列,直接跳到步骤 4。 -
从后向前查找第一个大于
nums[i]
的元素:再次从数组的末尾开始向前查找,找到第一个大于nums[i]
的元素nums[j]
。 -
交换
nums[i]
与nums[j]
:交换这两个元素的位置。由于nums[j]
是从数组末尾向前查找时第一个大于nums[i]
的元素,交换后可以保证这是比原排列大的最小排列的前缀。 -
翻转
nums[i+1]
之后的所有元素:由于nums[i+1]
到数组末尾的元素是降序排列的,我们需要将它们翻转成升序排列,这样可以得到这个前缀后面的最小排列。
按照以上步骤,我们可以得到原数组的下一个排列。如果在步骤 1 中没有找到相邻的升序对,说明当前排列是最大的排列,此时直接将整个数组翻转,得到最小的排列,即完成了重排。
Golang 版本
func nextPermutation(nums []int) {
if len(nums) <= 1 {
return
}
// Step 1: 从后向前查找第一个相邻升序对(i, i+1)
i := len(nums) - 2
for i >= 0 && nums[i] >= nums[i+1] {
i--
}
// 如果找到了升序对,执行Step 2和Step 3
if i >= 0 {
// Step 2: 从后向前查找第一个大于nums[i]的元素
j := len(nums) - 1
for j >= 0 && nums[j] <= nums[i] {
j--
}
// Step 3: 交换nums[i]和nums[j]
nums[i], nums[j] = nums[j], nums[i]
}
// Step 4: 翻转i之后的所有元素
reverse(nums, i+1, len(nums)-1)
}
// reverse用于翻转nums数组从start到end的部分
func reverse(nums []int, start int, end int) {
for start < end {
nums[start], nums[end] = nums[end], nums[start]
start++
end--
}
}
这段代码实现了上述解题思路,通过原地修改数组nums
来得到其字典序的下一个排列。如果数组已经是按照降序排列的,即不存在下一个更大的排列,那么这段代码将数组修改为升序排列,即字典序最小的排列。
C++版本
#include <vector>
#include <algorithm>
void nextPermutation(std::vector<int>& nums) {
int n = nums.size(), i, j;
// Step 1: 从后向前查找第一个相邻升序对(i, i+1)
for (i = n - 2; i >= 0 && nums[i] >= nums[i + 1]; --i);
// 如果找到了升序对,执行Step 2和Step 3
if (i >= 0) {
// Step 2: 从后向前查找第一个大于nums[i]的元素
for (j = n - 1; j >= 0 && nums[j] <= nums[i]; --j);
// Step 3: 交换nums[i]和nums[j]
std::swap(nums[i], nums[j]);
}
// Step 4: 翻转i之后的所有元素
std::reverse(nums.begin() + i + 1, nums.end());
}
这段 C++代码实现了寻找数组的下一个排列的功能。它首先从后向前寻找第一个相邻的升序对,然后在这个位置之后寻找一个比当前元素大的元素进行交换,并将交换后的后面部分翻转,以确保得到的新排列是比原排列大的最小排列。如果整个数组都是降序的,那么直接翻转整个数组,得到最小的排列。
Python 版本
from typing import List
def next_permutation(nums: List[int]) -> None:
def reverse(nums: List[int], start: int, end: int) -> None:
while start < end:
nums[start], nums[end] = nums[end], nums[start]
start, end = start + 1, end - 1
# Step 1: 从后向前查找第一个相邻升序对(i, i+1)
i = len(nums) - 2
while i >= 0 and nums[i] >= nums[i + 1]:
i -= 1
# 如果找到了升序对,执行Step 2和Step 3
if i >= 0:
# Step 2: 从后向前查找第一个大于nums[i]的元素
j = len(nums) - 1
while j >= 0 and nums[j] <= nums[i]:
j -= 1
# Step 3: 交换nums[i]和nums[j]
nums[i], nums[j] = nums[j], nums[i]
# Step 4: 翻转i之后的所有元素
reverse(nums, i + 1, len(nums) - 1)
这段 Python 代码实现了寻找给定整数数组的下一个排列的功能。它首先从后向前寻找第一个相邻的升序对,然后在这个位置之后寻找一个比当前元素大的元素进行交换,并将交换后的后面部分翻转,以确保得到的新排列是比原排列大的最小排列。如果整个数组都是降序的,那么直接翻转整个数组,得到最小的排列。
Java 版本
public class Solution {
public void nextPermutation(int[] nums) {
if (nums == null || nums.length <= 1) return;
int i = nums.length - 2;
// Step 1: 从后向前查找第一个相邻升序对(i, i+1)
while (i >= 0 && nums[i] >= nums[i + 1]) {
i--;
}
// 如果找到了升序对,执行Step 2和Step 3
if (i >= 0) {
int j = nums.length - 1;
// Step 2: 从后向前查找第一个大于nums[i]的元素
while (j >= 0 && nums[j] <= nums[i]) {
j--;
}
// Step 3: 交换nums[i]和nums[j]
swap(nums, i, j);
}
// Step 4: 翻转i之后的所有元素
reverse(nums, i + 1, nums.length - 1);
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
private void reverse(int[] nums, int start, int end) {
while (start < end) {
swap(nums, start++, end--);
}
}
}
这段 Java 代码实现了寻找给定整数数组的下一个排列的功能。它首先从后向前寻找第一个相邻的升序对,然后在这个位置之后寻找一个比当前元素大的元素进行交换,并将交换后的后面部分翻转,以确保得到的新排列是比原排列大的最小排列。如果整个数组都是降序的,那么直接翻转整个数组,得到最小的排列。
Rust 版本
fn next_permutation(nums: &mut Vec<i32>) { let mut i = nums.len() as i32 - 2; // Step 1: 从后向前查找第一个相邻升序对(i, i+1) while i >= 0 && nums[i as usize] >= nums[(i + 1) as usize] { i -= 1; } // 如果找到了升序对,执行Step 2和Step 3 if i >= 0 { let mut j = nums.len() as i32 - 1; // Step 2: 从后向前查找第一个大于nums[i]的元素 while j >= 0 && nums[i as usize] >= nums[j as usize] { j -= 1; } // Step 3: 交换nums[i]和nums[j] nums.swap(i as usize, j as usize); } // Step 4: 翻转i之后的所有元素 nums[(i + 1) as usize..].reverse(); } fn main() { let mut nums = vec![1, 2, 3]; next_permutation(&mut nums); println!("{:?}", nums); }
这段 Rust 代码实现了寻找给定整数向量的下一个排列的功能。它首先从后向前寻找第一个相邻的升序对,然后在这个位置之后寻找一个比当前元素大的元素进行交换,并将交换后的后面部分翻转,以确保得到的新排列是比原排列大的最小排列。如果整个向量都是降序的,那么直接翻转整个向量,得到最小的排列。
怎么样
上述解法遵循以下步骤来找到给定整数数组的下一个排列:
-
寻找升序对:从数组的末尾开始向前查找,找到第一个满足
nums[i] < nums[i + 1]
的索引i
,这意味着nums[i]
右边的元素都是按降序排列的。如果没有找到这样的i
(即整个数组是降序的),则跳到步骤 4。 -
寻找交换元素:再次从数组的末尾开始向前查找,找到第一个大于
nums[i]
的元素nums[j]
。 -
交换元素:交换
nums[i]
和nums[j]
。 -
翻转后缀:将索引
i
之后的元素翻转,如果步骤 1 中没有找到升序对,这意味着整个数组都需要翻转。
这个算法保证了找到的下一个排列是大于当前排列的最小排列。如果当前排列已经是最大的,那么这个算法会将数组重排为最小的排列,即其元素按照升序排列。这个过程只需要对原数组进行修改,不需要使用额外的空间,因此它的空间复杂度是常数级别的。
寻找重复数
题目要求
给定一个长度为 n + 1 的整数数组 nums
,其中的数字都在闭区间 [1, n] 内。根据鸽巢原理,至少存在一个数字是重复的。题目的要求是找出这个重复的数字。
需要注意的是:
- 数组
nums
中只有一个数字重复,但它可能重复多次。 - 需要找出这个重复的数字。
- 在寻找这个数字的过程中,不允许修改原数组
nums
。 - 解决方案中只能使用常量级别 O(1) 的额外空间。
解题思路
由于不能修改原数组且只能使用常量级别的额外空间,一些常规的排序或哈希表方法都不适用。这里可以使用一个类似于链表环路检测的方法——弗洛伊德的龟兔赛跑算法(Floyd's Tortoise and Hare Algorithm)。
-
初始化:设置两个指针,一个快指针(兔子)和一个慢指针(乌龟)。初始时,都位于数组的起始位置。
-
第一阶段:快指针每次移动两步,慢指针每次移动一步,直到它们在环内相遇。由于存在重复的数字,数组中的数字可以看作是指向下一个索引的指针,因此一定会形成一个环。
-
第二阶段:在快慢指针相遇后,将快指针重新置于起始位置,慢指针保持在相遇点。然后,两个指针都以相同的速度(每次移动一步)移动,当它们再次相遇时,相遇点就是重复数字的索引。
这个算法的关键在于理解数组中的数字可以表示为指向另一个索引的指针,从而将问题转化为链表环路检测问题。由于所有数字都在 [1, n] 范围内,所以数组中的每个数字都可以指向数组内的一个有效索引,这保证了环的存在。
Golang 版本
package main
func findDuplicate(nums []int) int {
// 初始化快慢指针
slow := nums[0]
fast := nums[0]
// 第一阶段:寻找快慢指针相遇点
do {
slow = nums[slow]
fast = nums[nums[fast]]
} while slow != fast
// 第二阶段:重置快指针,寻找环的入口
fast = nums[0]
for slow != fast {
slow = nums[slow]
fast = nums[fast]
}
// 返回重复的数字
return slow
}
func main() {
// 示例
nums := []int{1, 3, 4, 2, 2}
duplicate := findDuplicate(nums)
println("The duplicate number is:", duplicate)
}
这段代码实现了上述描述的弗洛伊德的龟兔赛跑算法,用于在不修改原数组且只使用常量级额外空间的条件下找出重复的数字。在 main
函数中,给出了一个示例数组,并调用 findDuplicate
函数来找出并打印重复的数字。
C++版本
#include <iostream>
#include <vector>
class Solution {
public:
int findDuplicate(std::vector<int>& nums) {
// 初始化快慢指针
int slow = nums[0];
int fast = nums[0];
// 第一阶段:寻找快慢指针相遇点
do {
slow = nums[slow];
fast = nums[nums[fast]];
} while (slow != fast);
// 第二阶段:重置快指针,寻找环的入口
fast = nums[0];
while (slow != fast) {
slow = nums[slow];
fast = nums[fast];
}
// 返回重复的数字
return slow;
}
};
int main() {
// 示例
std::vector<int> nums = {1, 3, 4, 2, 2};
Solution solution;
int duplicate = solution.findDuplicate(nums);
std::cout << "The duplicate number is: " << duplicate << std::endl;
return 0;
}
这段 C++代码实现了弗洛伊德的龟兔赛跑算法,用于在不修改原数组且只使用常量级额外空间的条件下找出重复的数字。在 main
函数中,给出了一个示例数组,并调用 findDuplicate
函数来找出并打印重复的数字。
Python 版本
def findDuplicate(nums):
# 初始化快慢指针
slow = fast = nums[0]
# 第一阶段:寻找快慢指针相遇点
while True:
slow = nums[slow]
fast = nums[nums[fast]]
if slow == fast:
break
# 第二阶段:重置快指针,寻找环的入口
fast = nums[0]
while slow != fast:
slow = nums[slow]
fast = nums[fast]
# 返回重复的数字
return slow
# 示例
nums = [1, 3, 4, 2, 2]
duplicate = findDuplicate(nums)
print(f"The duplicate number is: {duplicate}")
这段 Python 代码同样实现了弗洛伊德的龟兔赛跑算法,用于在不修改原数组且只使用常量级额外空间的条件下找出重复的数字。在示例中,定义了一个数组 nums
并调用 findDuplicate
函数来找出并打印重复的数字。
Java 版本
public class Solution {
public int findDuplicate(int[] nums) {
// 初始化快慢指针
int slow = nums[0];
int fast = nums[0];
// 第一阶段:寻找快慢指针相遇点
do {
slow = nums[slow];
fast = nums[nums[fast]];
} while (slow != fast);
// 第二阶段:重置快指针,寻找环的入口
fast = nums[0];
while (slow != fast) {
slow = nums[slow];
fast = nums[fast];
}
// 返回重复的数字
return slow;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {1, 3, 4, 2, 2};
int duplicate = solution.findDuplicate(nums);
System.out.println("The duplicate number is: " + duplicate);
}
}
这段 Java 代码实现了弗洛伊德的龟兔赛跑算法,用于在不修改原数组且只使用常量级额外空间的条件下找出重复的数字。在 main
方法中,创建了一个示例数组并调用 findDuplicate
方法来找出并打印重复的数字。
Rust 版本
fn find_duplicate(nums: Vec<i32>) -> i32 { let mut slow = nums[0]; let mut fast = nums[0]; // 第一阶段:寻找快慢指针相遇点 loop { slow = nums[slow as usize]; fast = nums[nums[fast as usize] as usize]; if slow == fast { break; } } // 第二阶段:重置快指针,寻找环的入口 fast = nums[0]; while slow != fast { slow = nums[slow as usize]; fast = nums[fast as usize]; } // 返回重复的数字 slow } fn main() { let nums = vec![1, 3, 4, 2, 2]; let duplicate = find_duplicate(nums); println!("The duplicate number is: {}", duplicate); }
这段 Rust 代码实现了弗洛伊德的龟兔赛跑算法,用于在不修改原数组且只使用常量级额外空间的条件下找出重复的数字。在 main
函数中,定义了一个示例向量 nums
并调用 find_duplicate
函数来找出并打印重复的数字。
总结
上述解法基于弗洛伊德的龟兔赛跑算法(Floyd's Tortoise and Hare Algorithm),这是一种用于检测链表中环的存在的算法。在这个问题中,我们将数组视为一个隐式的链表,其中每个位置可以看作是链表的一个节点,节点的值指向下一个节点的索引。
解法的核心思想是使用两个指针,一个快指针(每次移动两步)和一个慢指针(每次移动一步)。由于数组中有重复的数字,意味着至少有两个位置指向了同一个值,这就形成了一个环。按照以下步骤进行:
- 初始化:将两个指针都置于数组的起始位置。
- 第一阶段:移动快慢指针,直到它们相遇。由于快指针每次移动两步,慢指针每次移动一步,如果存在环,它们最终会在环内相遇。
- 第二阶段:将快指针重置回数组的起始位置,然后以相同的速度移动快慢指针(每次一步),当它们再次相遇时,相遇点即为环的入口,也就是重复的数字。
这种方法的优点是不需要修改原数组,也不需要使用额外的空间(除了两个指针变量),因此空间复杂度为 O(1)。时间复杂度为 O(n),因为指针最多会遍历两次数组。
在实际代码实现中,需要注意的是数组索引和值的转换,特别是在像 Rust 这样的语言中,需要将整数值转换为 usize 类型以用作索引。此外,由于这种方法依赖于数组中的值作为索引,因此它特别适用于题目中给出的条件,即数组中的数字都在 1 到 n 的范围内。