牛客网-算法学习直通套餐(附完整源码课件)

45 阅读8分钟

算法提分快车道:学习直通套餐(从基础到进阶无缝衔接)

在技术面试和编程竞赛中,算法能力是衡量一个开发者逻辑思维和问题解决能力的核心标尺。然而,许多学习者在算法之路上常常感到迷茫:知识点零散、难度跳跃大、不知如何进阶。本文为你量身打造一套“学习直通套餐”,通过清晰的路径和丰富的 Kotlin 代码示例,带你从基础平稳过渡到进阶,真正实现算法能力的“无缝衔接”与快速提分。


第一站:基础核心套餐——构建你的算法思维骨架

在开始刷题之前,必须掌握最基础的数据结构和算法。它们是解决一切复杂问题的基石。

1.1 复杂度分析:算法的“体检报告”

不了解时间复杂度和空间复杂度,刷题就如同盲人摸象。你必须知道你的算法在数据量增大时的表现。

  • O(1)  - 常数复杂度:最优。
  • O(log n)  - 对数复杂度:非常高效率,如二分查找。
  • O(n)  - 线性复杂度:良好,通常只能遍历一次数据。
  • O(n log n)  - 线性对数复杂度:高效的排序算法(如快速排序)的复杂度。
  • O(n²)  - 平方复杂度:警惕!通常涉及双层循环,数据量大时会非常慢。

1.2 核心数据结构实战

数组/列表

数组是最基础的结构,面试中常考察“双指针”技巧。

题目示例:两数之和 (Two Sum)

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值 target 的那两个整数,并返回它们的数组下标。

代码实现 (Kotlin):

fun twoSum(nums: IntArray, target: Int): IntArray {
    // 使用一个 HashMap 来存储 {数值 -> 索引}
    val map = mutableMapOf<Int, Int>()
    
    for (i in nums.indices) {
        val complement = target - nums[i]
        // 检查补数是否已在 map 中
        if (map.containsKey(complement)) {
            // 找到了,返回补数的索引和当前索引
            return intArrayOf(map[complement]!!, i)
        }
        // 将当前数字和其索引放入 map
        map[nums[i]] = i
    }
    
    // 题目保证有解,这里抛出异常或返回空数组
    throw IllegalArgumentException("No two sum solution")
}

// 测试
val nums = intArrayOf(2, 7, 11, 15)
val target = 9
println(twoSum(nums, target).contentToString()) // 输出: [0, 1]

核心思想:通过牺牲 O(n) 的空间,将查找时间从 O(n) 降低到 O(1),总时间复杂度为 O(n)。

链表

链表考察的是指针操作能力。

题目示例:反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

代码实现 (Kotlin):

// 首先定义链表节点
class ListNode(var `val`: Int) {
    var next: ListNode? = null
}

fun reverseList(head: ListNode?): ListNode? {
    var prev: ListNode? = null
    var curr = head
    
    while (curr != null) {
        val nextTemp = curr.next // 保存下一个节点
        curr.next = prev         // 反转指针
        prev = curr              // prev 前移
        curr = nextTemp          // curr 前移
    }
    
    return prev // prev 就是新的头节点
}

核心思想:迭代法,使用三个指针(prevcurrnextTemp)完成原地反转。空间复杂度 O(1)。


第二站:进阶提速套餐——掌握高效解题范式

掌握了基础,接下来要学习的是解决特定问题的“高级套路”,这些范式能让你在面对复杂问题时迅速找到突破口。

2.1 二分查找:不止于有序数组

二分查找的本质是“通过排除法缩小搜索空间”。

题目示例:搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

代码实现 (Kotlin):

fun searchInsert(nums: IntArray, target: Int): Int {
    var left = 0
    var right = nums.size - 1
    
    while (left <= right) {
        // 防止 (left + right) 溢出
        val mid = left + (right - left) / 2 
        
        when {
            nums[mid] == target -> return mid
            nums[mid] < target -> left = mid + 1 // 目标在右半区
            else -> right = mid - 1 // 目标在左半区
        }
    }
    
    // 循环结束时,left 就是插入位置
    return left
}

// 测试
println(searchInsert(intArrayOf(1, 3, 5, 6), 5)) // 输出: 2
println(searchInsert(intArrayOf(1, 3, 5, 6), 2)) // 输出: 1

核心思想:维护一个 [left, right] 的搜索区间,每次循环都将区间缩小一半。

2.2 滑动窗口:解决子数组/子串问题的利器

滑动窗口用于在一个大数组中寻找满足条件的连续子区间。

题目示例:最小覆盖子串

给你一个字符串 s、一个字符串 t。返回 s 中涵盖 t 所有字符的最小子串。

代码实现 (Kotlin):

fun minWindow(s: String, t: String): String {
    if (s.isEmpty() || t.isEmpty() || s.length < t.length) return ""

    // 需要的字符计数
    val need = mutableMapOf<Char, Int>()
    t.forEach { need[it] = need.getOrDefault(it, 0) + 1 }

    // 滑动窗口 [left, right)
    var left = 0
    var valid = 0 // 窗口中满足 need 条件的字符个数
    var start = 0
    var minLen = Int.MAX_VALUE

    val window = mutableMapOf<Char, Int>()

    for (right in s.indices) {
        val c = s[right]
        window[c] = window.getOrDefault(c, 0) + 1

        // 如果当前字符的数量和 need 中相等,则 valid++
        if (need.containsKey(c) && window[c] == need[c]) {
            valid++
        }

        // 当窗口已包含所有需要的字符时,尝试收缩窗口
        while (valid == need.size) {
            // 更新最小覆盖子串
            if (right - left + 1 < minLen) {
                start = left
                minLen = right - left + 1
            }

            // 移除窗口最左边的字符
            val d = s[left]
            if (need.containsKey(d)) {
                if (window[d] == need[d]) {
                    valid-- // 不再满足条件
                }
                window[d] = window[d]!! - 1
            }
            left++ // 窗口左边界右移
        }
    }

    return if (minLen == Int.MAX_VALUE) "" else s.substring(start, start + minLen)
}

// 测试
println(minWindow("ADOBECODEBANC", "ABC")) // 输出: "BANC"

核心思想:用两个指针 left 和 right 构成窗口,right 不断右移扩大窗口,当窗口满足条件时,left 右移缩小窗口以寻找最优解。


第三站:高阶思维套餐——突破算法瓶颈

当你能熟练运用以上范式后,就需要学习更抽象、更强大的思想来攻克最难的题目。

3.1 动态规划:从暴力递归到最优解

DP 的核心是找到“状态”和“状态转移方程”,通常用于求最值问题。

题目示例:零钱兑换

给你一个整数数组 coins,表示不同面额的硬币;以及一个整数 amount,表示总金额。计算并返回可以凑成总金额所需的最少的硬币个数。

代码实现 (Kotlin):

fun coinChange(coins: IntArray, amount: Int): Int {
    // dp[i] 表示凑成金额 i 所需的最少硬币数
    val dp = IntArray(amount + 1) { Int.MAX_VALUE }
    dp[0] = 0 // 凑成金额 0 需要 0 个硬币

    for (i in 1..amount) {
        for (coin in coins) {
            if (i - coin >= 0 && dp[i - coin] != Int.MAX_VALUE) {
                // 状态转移方程
                dp[i] = minOf(dp[i], dp[i - coin] + 1)
            }
        }
    }

    return if (dp[amount] == Int.MAX_VALUE) -1 else dp[amount]
}

// 测试
println(coinChange(intArrayOf(1, 2, 5), 11)) // 输出: 3 (5+5+1)
println(coinChange(intArrayOf(2), 3))       // 输出: -1

核心思想

  1. 定义状态dp[i] 是什么?
  2. 状态转移dp[i] 如何从之前的状态推导出来?dp[i] = min(dp[i - coin1] + 1, dp[i - coin2] + 1, ...)
  3. 确定初始状态dp[0] = 0

3.2 回溯算法:深度优先搜索的暴力美学

回溯是解决排列、组合、子集、棋盘等问题的通用方法,本质是 DFS 的一种。

题目示例:全排列

给定一个不含重复数字的数组 nums,返回其所有可能的全排列。

代码实现 (Kotlin):

fun permute(nums: IntArray): List<List<Int>> {
    val res = mutableListOf<List<Int>>()
    val path = mutableListOf<Int>()
    val used = BooleanArray(nums.size)

    fun backtrack() {
        // 终止条件:路径长度等于数组长度
        if (path.size == nums.size) {
            res.add(ArrayList(path)) // 找到一个排列
            return
        }

        for (i in nums.indices) {
            if (used[i]) continue // 跳过已使用的元素

            // 做选择
            path.add(nums[i])
            used[i] = true

            // 进入下一层决策树
            backtrack()

            // 撤销选择 (回溯)
            path.removeLast()
            used[i] = false
        }
    }

    backtrack()
    return res
}

// 测试
println(permute(intArrayOf(1, 2, 3)))
// 输出: [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]

核心思想

  1. 路径:当前已选择的元素集合 path
  2. 选择列表:当前可以做的选择(未使用过的元素)。
  3. 结束条件path 的大小达到 nums 的大小。
  4. 核心框架for 循环遍历选择列表 -> 做选择 -> 递归 -> 撤销选择

总结:你的算法提分路线图

这套“学习直通套餐”为你规划了一条清晰的路径:

  1. 基础阶段:死磕复杂度、数组、链表、栈、队列、哈希表、树。目标是能不看代码,徒手写出这些结构的实现和基本操作。
  2. 进阶阶段:专项突破二分查找、滑动窗口、双指针、BFS/DFS。目标是识别题型,并熟练套用对应范式。
  3. 高阶阶段:挑战动态规划和回溯。目标是能抽象出问题状态,写出状态转移方程或回溯函数。

提分秘诀

  • 刻意练习:不要贪多,按主题刷题。一个主题下,做 10-15 道题,理解透彻。
  • 代码复盘:AC 不是终点。思考是否有更优解?代码能否写得更简洁?
  • 一题多解:尝试用不同方法解决同一问题,比如递归和迭代,暴力与DP。

遵循这条“快车道”,从基础到进阶,你会发现算法学习不再是零散的知识点堆砌,而是一个逻辑清晰、环环相扣的完整体系。你的算法能力,也必将在这条快车道上实现质的飞跃。