算法-算法的基本框架思想



提示:

我想说算法的本质就是穷举
穷举有两个关键难点:无遗漏、无冗余。

算法的基本框架思想

一、二叉树的基本框架

二叉树题目的重要性,我提到二叉树算法是所有递归算法的根本,动态规划、回溯算法、图论算法等高级算法底层都是二叉树算法的思想。

1、很多动态规划问题就是在遍历一棵树,你如果对树的遍历操作烂熟于心,起码知道怎么把思路转化成代码,也知道如何提取别人解法的核心思路。

2、再看看回溯算法,后文 回溯算法详解 干脆直接说了,回溯算法就是个 N 叉树的前后序遍历问题,没有例外。

3、比如全排列问题吧,本质上全排列就是在遍历下面这棵树,到叶子节点的路径就是一个全排列:

1、二叉树的前序遍历

List<Integer> res = new LinkedList<>();

// 返回前序遍历结果
List<Integer> preorder(TreeNode root) {
    traverse(root);
    return res;
}

// 二叉树遍历函数
void traverse(TreeNode root) {
    if (root == null) {
        return;
    }
    // 前序遍历位置
    res.add(root.val);
    traverse(root.left);
    traverse(root.right);
}

2、二叉树的前序遍历优化

// 定义:输入一棵二叉树的根节点,返回这棵树的前序遍历结果
List<Integer> preorder(TreeNode root) {
    List<Integer> res = new LinkedList<>();
    if (root == null) {
        return res;
    }
    // 前序遍历的结果,root.val 在第一个
    res.add(root.val);
    // 后面接着左子树的前序遍历结果
    res.addAll(preorder(root.left));
    // 最后接着右子树的前序遍历结果
    res.addAll(preorder(root.right));
    return res;
}

2、二叉树的遍历基本框架

void traverse(TreeNode root) {
    if (root == null) {
        return;
    }
    // 前序位置
    traverse(root.left);
    // 中序位置
    traverse(root.right);
    // 后序位置
}

1、二叉树模型几乎是所有高级算法的基础,尤其是那么多人说对递归的理解不到位,更应该好好刷二叉树相关题目。

2、二叉树题目的递归解法可以分两类思路,第一类是遍历一遍二叉树得出答案,第二类是通过分解问题计算出答案,这两类思路分别对应着 回溯算法核心框架 和 动态规划核心框架。

二、回溯算法的基本框架

1、路径:也就是已经做出的选择。

2、选择列表:也就是你当前可以做的选择。

3、结束条件:也就是到达决策树底层,无法再做选择的条件

1、基本框架

result = []
def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return
    
    for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表)
        撤销选择

2、核心框架

###  回溯算法核心框架 

// 记录所有全排列
List<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> track = new LinkedList<>();

/* 主函数,输入一组不重复的数字,返回它们的全排列 */
List<List<Integer>> permute(int[] nums) {
    backtrack(nums);
    return res;
}

// 回溯算法框架
void backtrack(int[] nums) {
    if (track.size() == nums.length) {
		// 穷举完一个全排列
        res.add(new LinkedList(track));
        return;
    }
    
    for (int i = 0; i < nums.length; i++) {
        if (track.contains(nums[i]))
            continue;
		// 前序遍历位置做选择
        track.add(nums[i]);
        backtrack(nums);
        // 后序遍历位置取消选择
        track.removeLast();
    }
}

3、全排列的核心框架

## 全排序的主要算法 
void backtrack(int[] nums, LinkedList<Integer> track) {
    if (track.size() == nums.length) {
        res.add(new LinkedList(track));
        return;
    }
    
    for (int i = 0; i < nums.length; i++) {
        if (track.contains(nums[i]))
            continue;
        track.add(nums[i]);
        // 进入下一层决策树
        backtrack(nums, track);
        track.removeLast();
    }
}        

4、核心思想

1、回溯算法是对树形或者图形结构执行一次深度优先遍历,实际上类似枚举的搜索尝试过程,在遍历的过程中寻找问题的解。

2、深度优先遍历有个特点:当发现已不满足求解条件时,就返回,尝试别的路径。此时对象类型变量就需要重置成为和之前一样,称为「状态重置」。

3、许多复杂的,规模较大的问题都可以使用回溯法,有「通用解题方法」的美称。实际上,回溯算法就是暴力搜索算法,它是早期的人工智能里使用的算法,借助计算机强大的计算能力帮助我们找到问题的解。

三、动态规划的基本框架

1、动态规划常常适用于有重叠子问题和最优子结构性质的问题,并且记录所有子问题的结果,因此动态规划方法所耗时间往往远少于朴素解法。

2、动态规划有自底向上和自顶向下两种解决问题的方式。自顶向下即记忆化递归,自底向上就是递推。

3、使用动态规划解决的问题有个明显的特点,一旦一个子问题的求解得到结果,以后的计算过程就不会修改它,这样的特点叫做无后效性,求解问题的过程形成了一张有向无环图。动态规划只解决每个子问题一次,具有天然剪枝的功能,从而减少计算量。

4、动态规划系列问题的核心原理,无非就是先写出暴力穷举解法(状态转移方程),加个备忘录就成自顶向下的递归解法了,再改一改就成自底向上的递推迭代解法了, 动态规划的降维打击 里也讲过如何分析优化动态规划算法的空间复杂度。

1、自顶向下递归的动态规划

def dp(状态1, 状态2, ...):
    for 选择 in 所有可能的选择:
        # 此时的状态已经因为做了选择而改变
        result = 求最值(result, dp(状态1, 状态2, ...))
    return result

2、自顶向下递归的动态规划

# 初始化 base case
dp[0][0][...] = base case
# 进行状态转移
for 状态1 in 状态1的所有取值:
    for 状态2 in 状态2的所有取值:
        for ...
            dp[状态1][状态2][...] = 求最值(选择1,选择2...

0-1 背包的解题框架

int knapsack(int W, int N, int[] wt, int[] val) {
    assert N == wt.length;
    // base case 已初始化
    int[][] dp = new int[N + 1][W + 1];
    for (int i = 1; i <= N; i++) {
        for (int w = 1; w <= W; w++) {
            if (w - wt[i - 1] < 0) {
                // 这种情况下只能选择不装入背包
                dp[i][w] = dp[i - 1][w];
            } else {
                // 装入或者不装入背包,择优
                dp[i][w] = Math.max(
                    dp[i - 1][w - wt[i-1]] + val[i-1], 
                    dp[i - 1][w]
                );
            }
        }
    }
    
    return dp[N][W];
}

四、链表的基本框架

1、单链表常考的技巧就是双指针

## 返回链表的倒数第 k 个节点
ListNode findFromEnd(ListNode head, int k) {
    ListNode p1 = head;
    // p1 先走 k 步
    for (int i = 0; i < k; i++) {
        p1 = p1.next;
    }
    ListNode p2 = head;
    // p1 和 p2 同时走 n - k 步
    while (p1 != null) {
        p2 = p2.next;
        p1 = p1.next;
    }
    // p2 现在指向第 n - k + 1 个节点,即倒数第 k 个节点
    return p2;
}

head->1->2->3->4->5->6->7->8->9->10->null
一共10个节点

1、迭代遍历单链表

void traverse(ListNode head) {
    for (ListNode p = head; p != null; p = p.next) {

    }
}

x

2、递归遍历单链表


void traverse(ListNode head) {
    if (head == null) {
        return;
    }
    // 前序位置
    traverse(head.next);
    // 后序位置
}

五、数组的基本框架

1、数组常用的技巧有很大一部分还是双指针相关的技巧,说白了是教你如何聪明地进行穷举。

2、数字组合的和等于目标和嘛。比较聪明的方式是先排序,利用双指针技巧快速计算结果。

1、迭代遍历数组

void traverse(int[] arr) {
    for (int i = 0; i < arr.length; i++) {

    }
}

2、递归遍历数组


void traverse(int[] arr, int i) {
    if (i == arr.length) {
        return;
    }
    // 前序位置
    traverse(arr, i + 1);
    // 后序位置
}

六、双指针的基本框架

1、双指针从广义上来说,是指用两个变量在线性结构上遍历而解决的问题。

2、对于数组,指两个变量在数组上相向移动解决的问题。

3、对于链表,指两个变量在链表上同向移动解决的问题,也称为「快慢指针」问题。

4、在处理数组和链表相关问题时,双指针技巧是经常用到的,

5、双指针技巧主要分为两类:左右指针和快慢指针。

6、所谓左右指针,就是两个指针相向而行或者相背而行。

7、而所谓快慢指针,就是两个指针同向而行,一快一慢。

8、只要数组有序,就应该想到双指针技巧。这道题的解法有点类似二分查找,通过调节 left 和 right 就可以调整 sum 的大小。

1、快慢指针框架

Node slow = head.next;

Node fast = head.next;

while(fast != null&& fast.next != null) {
    // 快指针走两步
    fast = fast.next.next;

    // 慢指针走一步
    slow = slow.next;
}

1、左右指针框架

int left = 0, right = 0;

while (right < s.size()) {
    // 增大窗口
    window.add(s[right]);
    right++;
    
    while (window needs shrink) {
        // 缩小窗口
        window.remove(s[left]);
        left++;
    }
}

七、滑动窗口的基本框架

1、滑动窗口算法技巧,典型的快慢双指针,快慢指针中间就是滑动的「窗口」,主要用于解决子串问题。

2、滑动窗口也是有其限制的,就是你必须明确的知道什么时候应该扩大窗口,什么时候该收缩窗口。

3、滑动窗口指的是这样一类问题的求解方法,在数组上通过双指针同向移动而解决的一类问题。

4、使用滑动窗口解决的问题通常是暴力解法的优化,掌握这一类问题最好的办法就是练习,然后思考清楚为什么可以使用滑动窗口。

1、滑动窗口算法框架

## 其中两处 ... 表示的更新窗口数据的地方,到时候你直接往里面填就行了。
## 这两个 ... 处的操作分别是扩大和缩小窗口的更新操作,等会你会发现它们操作是完全对称的
void slidingWindow(string s) {
    unordered_map<char, int> window;
    
    int left = 0, right = 0;
    while (right < s.size()) {
        // c 是将移入窗口的字符
        char c = s[right];
        // 增大窗口
        right++;
        // 进行窗口内数据的一系列更新
        ...

        /*** debug 输出的位置 ***/
        // 注意在最终的解法代码中不要 print
        // 因为 IO 操作很耗时,可能导致超时
        printf("window: [%d, %d)\n", left, right);
        /********************/
        
        // 判断左侧窗口是否要收缩
        while (window needs shrink) {
            // d 是将移出窗口的字符
            char d = s[left];
            // 缩小窗口
            left++;
            // 进行窗口内数据的一系列更新
            ...
        }
    }
}

八、分治算法

1、分治法是构建基于多项分支递归的一种很重要的算法范式。字面上的解释是「分而治之」,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。

2、这个技巧是很多高效算法的基础,如排序算法(快速排序、归并排序)、傅立叶变换(快速傅立叶变换)。

九、递归算法

1、递归是计算机科学中的一个重要概念。它是许多其他算法和数据结构的基础。

2、每当递归函数调用自身时,它都会将给定的问题拆解为子问题。递归调用继续进行,直到到子问题成为一个不可以拆分的、可以直接求解的最简单问题。

3、为了确保递归函数不会导致无限循环,它需要包含:
一个简单的基本案例(basic case)(或一些案例), 能够不使用递归来产生答案的终止方案。
一组规则,也称作递推关系(recurrence relation),可将所有其他情况拆分到基本案例。
注意,函数可能会有多个位置进行自我调用(这是分治算法)。

十、涉及字符串算法

字符串往往由特定字符集内有限的字符组合而成,根据其特点,对字符串的 操作 可以归结为以下几类:

1、字符串的比较、连接操作(不同编程语言实现方式有所不同);
2、涉及子串的操作,比如前缀,后缀等;
3、字符串间的匹配操作,如 KMP 算法、BM 算法等。

字符串排序,按字典排列字符串,可以调用Arrays.sort API,也可以使用PriorityQueue,还可以自己实现Comparator

十一、Rabin-Karp 算法基础

1、算法的核心思路就是不断向最低位(个位)添加数字

2、删除数字的最高位

用 R 表示数字的进制数,用 L 表示数字的位数,就可以总结出如下公式:

/* 在最低位添加一个数字 */
int number = 8264;
// number 的进制
int R = 10;
// 想在 number 的最低位添加的数字
int appendVal = 3;
// 运算,在最低位添加一位
number = R * number + appendVal;
// 此时 number = 82643
/* 在最高位删除一个数字 */
int number = 8264;
// number 的进制
int R = 10;
// number 最高位的数字
int removeVal = 8;
// 此时 number 的位数
int L = 4;
// 运算,删除最高位数字
number = number - removeVal * R^(L-1);
// 此时 number = 264

十二、二分算法基础

int binarySearch(int[] nums, int target) {
    int left = 0; 
    int 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 if (nums[mid] > target)
            right = mid - 1; // 注意
    }
    return -1;
}

1、初始化 right 的赋值是 nums.length - 1,即最后一个元素的索引,而不是 nums.length。

2、这二者可能出现在不同功能的二分查找中,区别是:前者相当于两端都闭区间 [left, right],后者相当于左闭右开区间 [left, right),因为索引大小为 nums.length 是越界的。

3、这个算法中使用的是前者 [left, right] 两端都闭的区间。这个区间其实就是每次进行搜索的区间。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Boosting(提升)算法是一种集成学习方法,通过结合多个弱分类器来构建一个强分类器,常用于分类和回归问题。以下是几种常见的Boosting算法: 1. AdaBoost(Adaptive Boosting,自适应提升):通过给分类错误的样本赋予更高的权重,逐步调整分类器的学习重点,直到最终形成强分类器。 2. Gradient Boosting(梯度提升):通过构建多个决策树,每个决策树的输出值是前一棵树的残差,逐步调整模型,最终生成一个强模型。 3. XGBoost(eXtreme Gradient Boosting):是基于梯度提升算法的一种优化版本,采用了更高效的算法和数据结构来提高模型的训练速度和准确性。 4. LightGBM(Light Gradient Boosting Machine):也是基于梯度提升算法的一种优化版本,通过使用直方图算法、带深度的决策树、稀疏特征优化等方法,提高了模型的训练速度和准确性。 5. CatBoost(Categorical Boosting):是一种适用于处理分类特征数据的梯度提升算法,采用对称树、动态学习速率和一些高效的优化技术,具有较高的训练速度和准确性。 ### 回答2: Boosting是一种集成学习方法,通过训练一系列弱分类器得到强分类器。常见的Boosting算法有Adaboost、Gradient Boosting和XGBoost。 1. Adaboost(自适应增强算法):Adaboost是一种迭代算法,通过一系列弱分类器进行训练,每次迭代都会调整数据样本的权重,使得前一次分类错误的样本在下一次迭代中得到更多关注。最终,基于弱分类器的加权投票将得到强分类器。它在处理二分类问题时表现良好。 2. Gradient Boosting(梯度提升算法):Gradient Boosting是一种通过迭代训练弱分类器的方式来减小残差误差的算法。它将一系列弱分类器组合成一个强分类器,每个弱分类器都是根据上一个分类器的残差来训练。与Adaboost不同,Gradient Boosting使用损失函数的负梯度进行训练,如平方误差损失函数。常见的Gradient Boosting算法有梯度提升树(GBDT)和XGBoost。 3. XGBoost(Extreme Gradient Boosting):XGBoost是基于Gradient Boosting思想,通过优化目标函数和正则化项来提高性能和可扩展性的算法。它具有高效的并行计算能力和多种正则化技术,能够处理大规模数据集和高维特征。XGBoost在机器学习竞赛中取得了很多优秀的成绩,并被广泛应用于实际问题中。 这些Boosting算法都是通过迭代训练一系列弱分类器,通过集成这些弱分类器来获取强分类器。它们在解决分类、回归等任务时表现良好,并在实际应用中具有广泛的应用价值。 ### 回答3: Boosting算法是一类基于集成学习的机器学习算法,主要用于改善弱分类器,使得它们能够组合成一个更强大的分类器。常见的Boosting算法有以下几种: 1. AdaBoost(Adaptive Boosting): AdaBoost是最早提出的Boosting算法之一。它通过反复训练弱分类器,并根据前一轮分类器的错误率来调整训练样本的权重,以提高分类的准确性。 2. Gradient Boosting: Gradient Boosting是一种基于梯度下降的Boosting算法。它通过迭代训练弱分类器,每一轮的模型都会在前一轮的残差上进行优化,以减少预测误差。 3. XGBoost(Extreme Gradient Boosting): XGBoost是一种改进的Gradient Boosting算法。它在Gradient Boosting的基础上增加了正则化策略和自定义损失函数,并使用了一种高效的增量训练方式,提高了模型的性能和训练速度。 4. LightGBM: LightGBM是基于梯度推进和直方图算法的Boosting框架。相比于传统的基于排序的算法,LightGBM使用了基于直方图的算法来构建模型,提高了训练和预测的速度。 5. CatBoost: CatBoost是一种特定于分类问题的Boosting算法。它具有内置的处理类别特征的能力,可以自动处理缺失值,并且具有较好的鲁棒性和高效性能。 这些Boosting算法在处理不同类型的数据和问题时具有各自的优势和特点,可以根据具体情况进行选择和应用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值