两数之和
读完题目后,最容易想到的就是暴力解法:遍历数组中的每一个元素 num,同时查找剩余元素中值为 target-num 的元素。
代码:
kotlin
class Solution {
fun twoSum(nums: IntArray, target: Int): IntArray {
for (i in 0 until nums.size - 1) { // 最后一个元素无需遍历
val num = nums[i]
for (j in i + 1 until nums.size) {
if (num + nums[j] == target) {
return intArrayOf(i, j)
}
}
}
return intArrayOf()
}
}
因为 num 之前的元素已经相互配对过,所以每次都可以从 num 元素之后进行查找。
不过,线性查找 target-num 的过程非常耗时。我们可以使用哈希表(空间换时间)来快速查找:首先创建一个哈希表,接着遍历数组中的元素 num,如果哈希表中已经存在 target-num,就直接返回这两个元素的下标;如果不存在,就将当前元素插入到哈希表中保存起来。
代码:
kotlin
class Solution {
fun twoSum(nums: IntArray, target: Int): IntArray {
val hashMap = HashMap<Int, Int>()
for (i in 0..nums.size - 1) {
val num = nums[i]
if (hashMap.containsKey(target - num)) {
return intArrayOf(hashMap[target - num]!!, i)
} else {
// 存放下标
hashMap[num] = i
}
}
return intArrayOf()
}
}
字母异位词分组
这题同样运用了哈希的思想,关键在于如何让属于字母异位词的字符串拥有相同的键。
比较容易想到的是对字符串进行排序。这样一来,"eat" 和 "tea" 排序后得到的键都是 "aet",并且非字母异位词无法得到相同的键,例如 "beat" 和 "eat" 分别得到的是 "abet" 和 "aet"。
kotlin
class Solution {
fun groupAnagrams(strs: Array<String>): List<List<String>> {
val hashMap = HashMap<String, MutableList<String>>()
for (str in strs) {
// 对字符串进行排序
val array = str.toCharArray()
array.sort()
val key = String(chars = array)
if (hashMap.contains(key)) {
hashMap[key]!!.add(str)
} else {
// 字母异位词列表
hashMap[key] = mutableListOf(str)
}
}
return ArrayList(hashMap.values)
}
}
最长连续序列
先说说暴力解法:遍历数组中的每个元素 num,并且不断查找 num+1, num+2... 是否存在,这样就能找到当前元素能构成的最长连续序列长度。
我们还能预先对数组进行排序,并跳过已遍历的相同元素,进一步降低时间复杂度。
代码:
kotlin
class Solution {
fun longestConsecutive(nums: IntArray): Int {
if (nums.isEmpty()) {
return 0
}
nums.sort()
var max = 0
var tmp = 1 // 临时保存连续元素个数
for (i in 1..nums.size - 1) {
if (nums[i] == nums[i - 1]) { // 去除重复元素
continue
} else if (nums[i] == nums[i - 1] + 1) { // 连续元素
tmp++
} else { // 连续中断
max = max.coerceAtLeast(minimumValue = tmp)
// 重置最大长度
tmp = 1
}
}
return max.coerceAtLeast(minimumValue = tmp)
}
}
遇到需要频繁查找元素是否存在时,我们就可以联想到哈希表,将判断某个元素是否存在的时间复杂度直接降为 O(1)。
另外,和暴力算法中的优化一样,我们需要跳过不必要的元素遍历。当遇到一个 x, x+1, x+2, ..., x+y 的连续序列时,如果从 x+1 或 x+y 开始向后匹配,得到的结果肯定不如从起点 x 开始长,所以我们可以跳过这些中间数。
跳过原则: 如果遇到元素 num,集合中存在 num-1,说明 num 只是某个序列的中间节点,直接跳过即可。
代码:
kotlin
class Solution {
fun longestConsecutive(nums: IntArray): Int {
if (nums.isEmpty()) {
return 0
}
// 去重并存入哈希表
val set = HashSet<Int>()
for (num in nums) {
set.add(num)
}
var result = 1
for (num in set) {
// 跳过非序列起点的元素
if (set.contains(num - 1)) {
continue
} else {
// 临时连续序列长度
var count = 1
var next = num + 1
while (set.contains(next)) {
count++
next++
}
result = result.coerceAtLeast(count)
}
}
return result
}
}
思路沉淀:关于哈希
遍历元素时,如果发现之前的元素与当前元素能产生某种联系(例如相加等于目标值),就要立刻想到哈希。
在遍历的过程中,将之前的元素状态进行保存,后续就能进行 O(1) 的快速查找和匹配,这就是空间换时间的核心艺术。