【算法】详细总结

C语言网教程

文章目录

1. 运算符优先级、ASCII编码表

说明

  • 运算符共分为15级,1级优先级最高,15级优先级最低。
  • 同一优先级的运算符,运算次序由结合方向所决定。(结合性:2 13 14 是从右至左 其他都是从左至右)
  • 简单记就是:! > 算术运算符 > 关系运算符> && > || > 赋值运算符

请添加图片描述
请添加图片描述
请添加图片描述

2. 递归

个人总结
递归:要清楚递归前的动作、递归后的动作(递的过程、归的过程)
回溯:就是递归前选择,递归后清除

解递归题的三部曲

  1. 找整个递归的终止条件:递归应该在什么时候结束?
  2. 找返回值:叶子节点应该给上一级返回什么信息?
  3. 本级递归应该做什么:在这一级递归中,应该完成什么任务?

2步和3步没有先后顺序,要清楚哪些是在递过程中做的,哪些是归的过程中做的

树和链表天生就是适合递归的结构:

简单的来说就是:用循环能实现,递归一般可以实现,但是能用递归实现的,循环不一定能。因为有些题目

  • ①只注重循环的结束条件和循环过程,而往往这个结束条件不易表达(也就是说用循环并不好写);
  • ②只注重循环的次数而不注重循环的开始条件和结束条件(这个循环更加无从下手了)。
图片名称
//18.二叉树的镜像
//比如:源二叉树 
            8
           /  \
          6   10
         / \  / \
        5  7 9 11
        镜像二叉树
            8
           /  \
          10   6
         / \  / \
        11 9 7  5
// 无返回值的类型        
     void Mirror(TreeNode *pRoot) {//有点类似于二叉树的层次遍历
        if(pRoot == nullptr) return;             
        Mirror(pRoot->left);
        Mirror(pRoot->right);
        swap(pRoot->left,pRoot->right);
    }
// 有返回值的类型  
     TreeNode* Mirror(TreeNode* pRoot) {
        // 递归解法
        if(pRoot == nullptr) return nullptr;
        pRoot->left = Mirror(pRoot->left); //有返回
        pRoot->right = Mirror(pRoot->right);
        swap(pRoot->left, pRoot->right);
        return pRoot;
    }
// 自己想的双层递归
class Solution {
public:
    int time = 0; //1出现的次数
    int NumberOf1Between1AndN_Solution(int n) {
        NextNum(n);
        return time;
    }
    void NextNum(int n){
        if(n == 1) time = time + 1;
        if(n <= 0) return; //递归结束条件
        if(n >= 10) RestNum(n);
        NextNum(n-1);
    }
    
    void RestNum(int n) {
        
        if(n == 1) time = time + 1; 
        if(n < 10) return;  //递归结束条件

        int temp = n; // 临时存放n
        int weight = 1; // 十进制权重
        //获取n的第一个数字,并判断是否为1
        while((temp/10) != 0) { // 必须是循环体
            temp = temp / 10;
            weight = weight * 10;
            if(temp == 1) time = time + 1; 
        }
        int restNum = n - temp * weight; //获取剩下的数字
        RestNum(restNum); //剩下数字递归
    }
};

3、DFS/BFS

3.1 dfs 借助辅助栈

// 中序遍历
// 迭代 dfs,借助赋值栈
 vector<int> inorderTraversal(TreeNode* root) {
        // 借助辅助栈
        stack<TreeNode*> s; // 只能存储指针,这样才能找到上一级的根节点
        vector<int> vec;
        while(root != nullptr || !s.empty()) { //只要有一个1就是1
            while(root != nullptr) {
                 s.push(root);
                 root = root->left;
            }
            root = s.top(); //返回上一级的根节点
            vec.push_back(root->val);
            s.pop();
            root = root->right;
        }
        return vec;
    }

3.2 bfs 借助辅助队列

  1. 把根节点放入队列中
  2. 从队列中取出一个元素
  3. 访问该元素的所有节点(先出栈再入栈)
  4. 若该元素左右节点非空,则将其左右节点顺序入队列
// 层序遍历,bfs解法,借助辅助队列
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> res;
        if(root == nullptr) return res;  //边界条件
        queue<TreeNode*> q;   //构造辅助队列,利用先进先出的特性
        q.push(root);
        while(!q.empty()){
            vector<int> vec;
            // 遍历队列,采用取头部数据的方式
            int len = q.size(); //必须存起来,size是动态变化的
            for(int i = 0; i < len; ++i) {
                root = q.front();  //恨重要,调整root指针的指向
                vec.push_back(root->val);
                q.pop();
                //每遍历一个节点就插入左右节点到队列中
                if(root->left != nullptr) q.push(root->left);
                if(root->right != nullptr) q.push(root->right);
            }
            res.push_back(vec); //插入数组
        }
        return res;
    }
};

4. 分治 回溯 贪心 动态规划 分支限界法

算法理论:
分治 动态规划 贪心 回溯 分支限界法

动态规划有以下几种分类

  1. 最值型动态规划,比如求最大,最小值是多少
  2. 计数型动态规划,比如换硬币,有多少种换法,最少的
  3. 坐标型动态规划,比如在m*n矩阵求最值型,计数型,一般是二维矩阵
  4. 区间型动态规划,比如在区间中求最值

就是什么样的题适合用暴力递归?

显然就是,可能的情况很多,需要枚举所有种情况。只不过动态规划,只记录子结构中最优的解

4.1 分治法(递归树)

  • 归并排序
  • 快速排序

4.2 回溯法(递归、暴力穷举法)

  • 题型:全排列、需要枚举所有情况的
  • 核心:穷举
  • 回溯算法框架:递归之前做选择,递归之后撤销选择
  • 回溯算法就是一种暴力穷举法,穷举的过程就是遍历一颗多叉树的过程(与多叉树遍历的代码框架类似)
// C++代码框架
void backtrack(路径, 被选择列表, 记录选择的数组){
    if(结束条件){
        result.push_back(路径); // 有时剪枝与结束条件融合
        return;
    }
    for(int i = 0; i < input.size(); ++i){
        // 跳出该层递归循环(类似结束条件)或者选择循环,  
        // 排除不合法 isVaild 的选择(剪枝),(构建 cut 函数)
        
        //做选择
         backtrack(路径, 被选择列表, 记录选择的数组); // 列举每一种可能的递归
       //撤销选择,回溯思想(回溯什么时候回,要满足一定条件)
    }
}

n皇后

// n 皇后,回溯解法
class Solution {
public:
        vector<vector<string>> res;
    //输入棋盘边长 n, 返回所有合法的放置
    vector<vector<string>> solveNQueens(int n) {
        //初始化 n x n 空棋盘
        vector<string> board(n, string(n, '.'));
        backtrack(board, 0); //初始化行 为0
        return res;
    }
    void backtrack(vector<string>& board, int row) {
       //每一行都成功放置了皇后,记录结果 
        if(row == board.size()) {
            res.push_back(board);
            return;
        }

        int n = board[row].size();
        //在当前行的每一列都可能放置皇后
        for(int col = 0; col < n; ++col) {
            //排除可以相互攻击的格子(剪枝)
            if(!isVaild(board, row, col)) continue;

            //尝试性做选择
            board[row][col] = 'Q';
            //递归,进入下一行放皇后
            backtrack(board, row+1);
            //撤销选择,回溯思想
            board[row][col] = '.';
        }
    }

    //判断是否可以在board[row][col]后放置皇后?
    bool isVaild(vector<string>& board, int row, int col) {
        int n = board.size();
        //检查列 是否有皇后相互冲突
        for(int i = 0; i < n; ++i) {
            if(board[i][col] == 'Q') return false;
        }
        //检查右上方是否有皇后相互冲突
        for(int i = row - 1, j = col + 1; i >= 0 && j < n; --i, ++j) {
            if(board[i][j] == 'Q') return false;
        }
        //检查左上方是否有皇后相互冲突
        for(int i = row - 1, j = col - 1; i >= 0 && j >= 0; --i, --j) {
            if(board[i][j] == 'Q') return false;
        }
        return true;
    }
};

字符串的排列(回溯法)

描述
输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则按字典序打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。
输入描述
输入一个字符串,长度不超过9(可能有字符重复),字符只包括大小写字母。

// 回溯算法
class Solution {
public:
    set<string> s; //插入重复元素,进行忽略处理,同时进行由小到大排序
    
    vector<string> Permutation(string str) {
        string path; //路径
        vector<bool> select(str.size(), false); //选择数组,用于判断字符串str中的元素是否被选择
        backtrack(str, path, select);
        vector<string> res(s.begin(), s.end()); //调用拷贝构造
        return res;
    }
    
    void backtrack(string& str, string& path, vector<bool>& select) { //记录全排列路径
        if(path.size() == str.size()) {  //终止条件,遍历完记录返回
            s.insert(path); //插入重复元素,进行忽略处理,同时进行由小到大排序
            return;
        }
        
        
        for(int i = 0; i < str.size(); ++i) {
           
            // 排除不合法的选择(剪枝),被选择除去
            if(select[i]) continue;
            
            // 尝试性做选择(枚举所有)
            path.push_back(str[i]);
            select[i] = true;
            
            // 本来递归后就完成选择了,但最后没有完成,故需要退回重新选择
            backtrack(str, path, select);  //自顶向下递归
            
            // 递归完成后撤销选择,回复到默认路径
            path.pop_back();
            select[i] = false;
        }
    }
}; 

矩阵中的路径

描述
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 例如矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。
在这里插入图片描述

// 回溯解法
//题目没理解:用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径
class Solution {
public:
    bool hasPath(vector<vector<char> >& matrix, string word) {
        // 路径中的元素访问到的做标记
        vector<vector<bool>> visit(matrix.size(), vector<bool>(matrix[0].size(), false));
        // 从矩阵的不同起点开始
        for(int i = 0; i < matrix.size(); ++i){
            for(int j = 0; j < matrix[0].size(); ++j){
                if(backtrack(matrix, visit, word, i, j, 0)) return true; //有一条路径可以就行
            }
        }
        return false;
    }

    bool backtrack(vector<vector<char> >& matrix, vector<vector<bool> >& visit, string& word, int row, int col, int index){
        // 结束条件
        if(index >= word.size()) return true; // 字符串结束,说明匹配完成
        
        // 剪枝
        if(row < 0 || row >= matrix.size() || col < 0 || col >= matrix[0].size() ||
           visit[row][col] || matrix[row][col] != word[index]){
            return false;
        }
        //做选择(枚举所有选择)
        visit[row][col] = true;
            
        // 本来递归后就完成选择了,但最后没有完成,故需要退回重新选择
        if(backtrack(matrix, visit, word, row-1, col, index+1) ||
          backtrack(matrix, visit, word, row+1, col, index+1) ||
          backtrack(matrix, visit, word, row, col-1, index+1) ||
          backtrack(matrix, visit, word, row, col+1, index+1)){
            return true;
        }
        // 撤销选择
        visit[row][col] = false;
        return false;
    }
};

二叉树和为整数的所有路径

描述
输入一颗二叉树的根节点和一个整数,按字典序打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。

// 回溯法(无法剪枝的回溯)
class Solution {
public:
    vector<vector<int> > ans;
    vector<vector<int> > FindPath(TreeNode* root,int expectNumber) {
        
        vector<int> path;
        int sum = 0;
        backtrack(root, expectNumber, path, sum);
        sort(ans.begin(), ans.end()); //按字典序进行排序
        return ans;
    }
    
    void backtrack(TreeNode* root, int& expectNumber, vector<int>& path, int& sum) {
        if(root == nullptr) {
            //if(sum != expectNumber) return;  // 剪枝与结束条件合在一起
            //ans.push_back(path); //完成一条路径后记录
            return;
        }
        
        // 剪枝 , 要先判断能不能剪
        path.push_back(root->val); //做选择
        sum = sum + root->val;
        if(root->left == nullptr && root->right == nullptr && sum == expectNumber) {
            ans.push_back(path);
        } 
        
        backtrack(root->left, expectNumber, path, sum); // 一般回溯中只有一个递归函数,若有多个需要去重
        backtrack(root->right, expectNumber, path, sum);
        
        path.pop_back();  //撤销选择
        sum = sum - root->val;
    }
};

机器人走格子

描述
地上有一个rows行和cols列的方格。坐标从 [0,0] 到 [rows-1,cols-1]。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于threshold的格子。 例如,当threshold为18时,机器人能够进入方格[35,37],因为3+5+3+7 = 18。但是,它不能进入方格[35,38],因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?
范围:
1 <= rows, cols<= 100
0 <= threshold <= 20

// 无返回值、无撤销回溯
class Solution {
public:
    int count = 0;
    
    int movingCount(int threshold, int rows, int cols) {
        if(threshold < 0) return 0;
        //pair<int, int> path(0, 0); // 记录机器人的横纵坐标
        vector<vector<bool> > select(rows, vector<bool>(cols, false));
        backtrack(threshold, rows, cols, 0, 0, select);// 机器人初始横纵坐标(0,0)
        return count;
    }
    
    void backtrack(int threshold, int rows, int cols, int x, int y, vector<vector<bool>>& select){
        if(x > rows - 1 || x < 0|| y > cols - 1 || y < 0){
            return; //表明遍历完成
        }
        
        if(cut(x, y, threshold) == true) return;   // 满足剪枝条件
        if(select[x][y]) return; // 去重
        count++;
        select[x][y] = true;
        backtrack(threshold, rows, cols, x-1, y, select);
        backtrack(threshold, rows, cols, x+1, y, select);
        backtrack(threshold, rows, cols, x, y-1, select);
        backtrack(threshold, rows, cols, x, y+1, select); // 是多个选择,故不撤销
       
        //count--;
        //select[x][y] = false; //满足一定条件才能撤销选择(不是全部撤销)
    }
    
    bool cut(int x, int y, int& threshold) {
        int row = x, col = y; 
        int sum = 0;
        while(row != 0){
            sum = sum + row % 10; // 取个位数求和
            row = row / 10;
        }
        while(col != 0){
            sum = sum + col % 10; 
            col = col / 10;
        }
        if(sum > threshold) return true; // 不符合,剪掉
        return false;
    }
};
// 有返回值、无撤销回溯
class Solution {
public:
    int movingCount(int threshold, int rows, int cols) {
        if(threshold < 0) return 0;
        //pair<int, int> path(0, 0); // 记录机器人的横纵坐标
        vector<vector<bool> > select(rows, vector<bool>(cols, false));
        return  backtrack(threshold, rows, cols, 0, 0, select);// 机器人初始横纵坐标(0,0)
    }
    
    int backtrack(int threshold, int rows, int cols, int x, int y, vector<vector<bool>>& select){
        if(x > rows - 1 || x < 0|| y > cols - 1 || y < 0){
            return 0; //表明遍历完成
        }
        
        if(cut(x, y, threshold) == true) return 0;   // 满足剪枝条件
        if(select[x][y]) return 0; // 去重
        select[x][y] = true;
        
        return backtrack(threshold, rows, cols, x-1, y, select)+
               backtrack(threshold, rows, cols, x+1, y, select)+
               backtrack(threshold, rows, cols, x, y-1, select)+
               backtrack(threshold, rows, cols, x, y+1, select)+1;
        //select[x][y] = false; //满足一定条件才能撤销选择(不是全部撤销)
    }
    
    bool cut(int x, int y, int& threshold) {
        int row = x, col = y; 
        int sum = 0;
        while(row != 0){
            sum = sum + row % 10; // 取个位数求和
            row = row / 10;
        }
        while(col != 0){
            sum = sum + col % 10; 
            col = col / 10;
        }
        if(sum > threshold) return true; // 不符合,剪掉
        return false;
    }
};

4.3 贪心算法(局部最优)(lambda)

  • 在一些最优化问题中常常需要运用贪心思想,像数据结构中的Huffman编码,Dijkstra算法,Kruskal算法和Prim算法
  • 贪心算法是优于动态规划的,因此其使用条件必定是更为苛刻的,所以我们可以料想到,贪心选择性质是很难证明的。
// 贪心算法(局部最优解 -> 全局最优解)
class Solution {
public:
    string PrintMinNumber(vector<int> numbers) {
        vector<string> vec;
        string str;
        for(auto it : numbers) {
            vec.push_back(to_string(it));
        }
        // 具名lambda表达式
        auto lam = [](string& a, string& b) { // lam 类似于函数名
            return a+b < b+a;
        }; // 这是条语句,需要加分号
        sort(vec.begin(), vec.end(), lam);
        // 匿名lambda表达式
        //sort(vec.begin(), vec.end(), [](string& a, string& b) -> bool{return a+b < b+a;});
        for(int i = 0; i < vec.size(); ++i){
            str += vec[i];
        }
        return str;
    }
};

对于第二步的排序规则,实现上,可以用仿函数,lambda表达式,函数指针,针对本题的实现分别如下:

//1.仿函数
struct Com {
    bool operator() (string a, string b) {
     return a + b < b + a;
    }
};
sort(str.begin(), str.end(), Com()); // Com()为临时对象

//2.lambda表达式
// 1. 匿名lambda表达式
sort(str.begin(), str.end(), [](string a, string b) {
     return a + b < b + a;
});
// 2.具名lambda表达式
auto lam = [](string a, string b) {
     return a + b < b + a;
 };
sort(str.begin(), str.end(), lam);;

//3.函数指针
bool static com(string a, string b) {
    return a + b < b + a;
}
//加static的原因:类成员函数有隐藏的this指针,static 可以去this指针
sort(str.begin(), str.end(), com);

4.4 动态规划(dp)

  • 题型:求最值,求最优解(不一定所以求最值都适用)
  • 核心:穷举

动态规划的三大步骤:

动态规划,无非就是利用历史记录,来避免我们的重复计算。而这些历史记录,我们得需要一些变量来保存,一般是用一维数组或者二维数组来保存

  1. 第一步骤:定义dp数组元素的含义,
  2. 第二步骤:找出dp数组元素之间的关系式,有一点类似于我们高中学习时的归纳法的,当我们要计算 dp[n] 时,是可以利用 dp[n-1],dp[n-2]……dp[1],来推出 dp[n] 的,也就是可以利用历史数据来推出新的元素值,所以我们要找出数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],这个就是他们的关系式了。而这一步,也是最难的一步,后面我会讲几种类型的题来说。
  3. 第三步骤:找出dp初始值。学过数学归纳法的都知道,虽然我们知道了数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],我们可以通过 dp[n-1] 和 dp[n-2] 来计算 dp[n],但是,我们得知道初始值啊,例如一直推下去的话,会由 dp[3] = dp[2] + dp[1]。而 dp[2] 和 dp[1] 是不能再分解的了,所以我们必须要能够直接获得 dp[2] 和 dp[1] 的值,而这,就是所谓的初始值。

由了初始值,并且有了数组元素之间的关系式,那么我们就可以得到 dp[n] 的值了,而 dp[n] 的含义是由你来定义的,你想求什么,就定义它是什么,这样,这道题也就解出来了。

动规五部曲分别为

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组
1.动态规划特点:
//1.重叠子问题
//2.状态转移方程(最关键)
//3.最优子结构

2.动态规划解法代码框架:
//初始化
    dp[0][0][...] = baseCase;
// 进行状态转移
    for(状态1 in 状态1的所有取值)
        for(状态2 in 状态2的所有取值)
            for...
                dp[状态1][状态2][...] = 求最值(选择1, 选择2...

在这里插入图片描述

连续子数组和

描述
输入一个整型数组,数组里有正数也有负数。数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为 O(n).

// 常规dp算法,类似于最大连续上升子序列
class Solution {
public:
    int FindGreatestSumOfSubArray(vector<int> array) {
        if(array.size() == 0) return 0; // 表示没有元素
        
        //1.明确dp数组的含义是:以i结尾的连续子数组的最大和
        vector<int> dp(array.size(), 0);
        int maxNum = array[0];
        dp[0] = array[0]; //2. 初始化
        for(int i = 1; i < array.size(); ++i) {
            // 3. 状态转移方程
            dp[i] = max(dp[i-1] + array[i], array[i]); //i=1,防止数组下标越界
            maxNum = max(dp[i], maxNum);
        }
        return maxNum;
    }
};

正则表达式

// 52 正则表达式匹配

  //1.状态定义: 设动态规划矩阵 dp , dp[i][j]代表字符串str的前 i 个字符和pattern的前 j 个字符能否匹配
 
 class Solution {
 public:
    bool match(string str, string pattern) {
           int m = str.size() + 1, n = pattern.size() + 1;
        vector<vector<bool>> dp(m, vector<bool>(n, false)); // (m X n)  1.定义DP数组元素的含义 , dp[i][j]代表字符串str的前 i 个字符和pattern的前 j 个字符能否匹配
        dp[0][0] = true; // 3.找出初始值
        for(int j = 2; j < n; j += 2)
            dp[0][j] = dp[0][j - 2] && pattern[j - 1] == '*'; // 3.找出初始值
        for(int i = 1; i < m; i++) {
            for(int j = 1; j < n; j++) {
                //判断p的第j位是不是*,是*则判断前一位与i是否相等,相等则可以匹配0次或者多次
                dp[i][j] = pattern[j - 1] == '*' ?
                    dp[i][j - 2] || dp[i - 1][j] && (str[i - 1] == pattern[j - 2] || pattern[j - 2] == '.'):
                    dp[i - 1][j - 1] && (pattern[j - 1] == '.' || str[i - 1] == pattern[j - 1]);
            }
        }
        return dp[m - 1][n - 1];
    }
};

5. 十大排序算法

十种常见排序算法可以分为两大类

  • 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
  • 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。

常见的时间复杂度量有

  1. O(1):常量阶,运行时间为常量
  2. O(logn):对数阶,如二分搜索算法
  3. O(n):线性阶,如n个数内找最大值
  4. O(nlogn):线性对数阶,如快速排序算法
  5. O(n^2):平方阶,如选择排序,冒泡排序
  6. O(n^3):立方阶,如两个n阶矩阵的乘法运算
  7. O(2^n):指数阶,如n个元素集合的所有子集的算法
  8. O(n!):阶乘阶,如n个元素全部排列的算法

相关概念

  • 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
  • 不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
  • 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
  • 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

5.1 冒泡排序(bubble,稳定,时间O(n^2),空间O(1))

  • 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  • 针对所有的元素重复以上的步骤,除了最后一个;
  • 重复步骤1~3,直到排序完成

在这里插入图片描述

void bubbleSort(vector<int> &v){
	int len = v.size();
	for (int i = 0; i < len - 1; i++){
		bool isSwap=false;
		for (int j = 0; j < len - 1 - i; j++){  //j<n-1-i,最后面的元素已是排好序的
			if (v[j] > v[j + 1]){
				//swap(v[j], v[j + 1]);
				int temp = v[j+1];
				v[j + 1] = v[j];
				v[j] = temp;
				isSwap=true;
			}
		}
		if (isSwap == false) {  //改进:设置一个flag,都没有进行交换,就停止
			break;//break会直接退出循环,而continue不会。
		}	
	}
}

5.2 选择排序(select,不稳定,时间O(n^2),空间O(1))(选择排序数据规模越小越好)(分有序区和无序区)

  • 选择排序一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕
  • 选择排序数据规模越小越好。唯一的好处可能就是不占用额外的内存空间

具体算法描述如下
n个数据可经过n-1趟选择排序得到有序结果。最后第n个数据就不用选择了

  • 初始状态:无序区为R[1…n],有序区为空;
  • 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1…i-1]和R(i…n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录进行交换,使有序区增加1个和无序区减少1个;
  • n-1趟结束,数组有序化了。

在这里插入图片描述

void selectSort(vector<int> &v){
	int len = v.size();
	for (int i = 0; i < len - 1; i++){ //最后第n个数据就不用选择了
		int min_idx = i; // 初始化最小的为有序的最后一个
		for (int j = i + 1; j < len; j++){
			if (v[min_idx] > v[j]){ //不能是v[i] > v[j],最小值在变
				min_idx = j; // 找最小值的下标
			}
		}
		if (min_idx != i){
			swap(v[i], v[min_idx]);
		}
	}
}

5.3 插入排序(insert,稳定,时间O(n^2),空间O(1))(也是交换)(分有序区和无序区)

  • std::sort原理:对要排序的元素数目有一个阈值,如果大于该阈值则是用快速排序,如果小于阈值则用插入排序
  • 工作原理:构建有序序列,将未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。把无序区的第一个元素插入到有序区的合适的位置。对数组:比较得少,换得多

插入排序思路

  1. 从第一个元素开始,该元素可以认为已经被排序
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
  3. 如果该元素(已排序)大于新元素,将已排序元素移到下一位置
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
  5. 将新元素插入到该位置后
  6. 重复步骤2~5

在这里插入图片描述

void insertSort(vector<int> &v){
	int len = v.size();
	for (int i = 1; i < len; i++){
		int temp = v[i]; //把v[i]存储,防止v[i]更改后失效,不能存下标
		for (int j = i; j > 0; j--){ // j表示往前扫描
			if (temp < v[j-1]){
				v[j] = v[j-1]; //就是与前面的交换
				v[j-1] = temp;                
			}
			else
				break;
		}
	}
}

5.4 希尔排序(shell,不稳定,时间O(n^1,3),空间O(1))(缩小增量排序,插入排序的改进版)

  • 简单插入排序的改进版,它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序

具体算法描述

将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,

  • 选择一个增量序列 t1,t2,…,tk,其中ti>tj,tk=1;
  • 按增量序列个数k,对序列进行k 趟排序;
  • 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m的子序列,分别对各子表进行直接插入排序。仅增量因子为1时,整个序列作为一个表来处理,表长度即为整个序列的长度。
    在这里插入图片描述
void shellSort(vector<int> &v){
	int len = v.size();
	int gap = len / 2; //组数、增量
	while (gap){
		for (int i = gap; i < len; i++){
			int temp = v[i];
			for (int j = i - gap; j >= 0; j = j - gap){
				if (v[j] > temp)
				{
					v[j+gap] = v[j];
					v[j] = temp;
					//swap(v[j], temp);不能与临时量交换
				}
				else
					break;
			}
		}
		gap = gap / 2;
	}
}

5.5 归并排序(merge,稳定,时间O(nlogn),空间O(n))(分治法,归的过程中处理)

该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。

  • 该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。先使每个子序列有递归(递去归来):递去分,归来并。因为当只有一个元素时,一定有序。
  • 和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。

算法描述:

  • 把长度为n的输入序列分成两个长度为n/2的子序列;
  • 递归划分,直到有序(也就是子序列只有一个元素)
  • 对两个子序列分别进行归并排序;在归来的过程中一直合并子序列;
  • 将两个排序好的子序列合并成一个最终的排序序列。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4gykLXJs-1656559406983)(https://note.youdao.com/yws/api/personal/file/C28E998A76B4474984F7CF2F82DEF776?method=download&shareKey=204c800ed56b82e89fdb7ddec6532e33)]

可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。

合并相邻有序子序列

再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。

在这里插入图片描述

//递归解法
//治
void Merge(vector<int> &v, int begin, int mid, int end){
	vector<int> temp; //把整理后的元素放在temp
	int leftStart = begin; //不能用指针类型,指针储存的是下标的地址,++后下标与原下标就不同了
	int rightStart = mid+1; 
	while (leftStart <= mid && rightStart<= end)//合并两个区间:都存在元素和一个区间已经为空
	{
		if (v[leftStart] > v[rightStart])
		{
			temp.push_back(v[rightStart]);
			rightStart++;
		}
		else
		{
			temp.push_back(v[leftStart]);
			leftStart++;
		}
	}
	while (leftStart <= mid)//左边序列
	{
		temp.push_back(v[leftStart]);
		leftStart++;
	}
	while (rightStart <= end)//右边序列
	{
		temp.push_back(v[rightStart]);
		rightStart++;
	}
	
	for (auto it : temp){
		//v.push_back(it);不能插入,要用必须先清空再插入,但是清空后数据没了
		v[begin++] = it;
	}
}
//分,在递的过程中没有任何处理
void mergeSort(vector<int> &v, int begin, int end){
	if (begin >= end) return;    //结束条件为子序列只有一个元素
	int mid = (begin + end) / 2;
	mergeSort(v, begin, mid);    //递归就是处理重复单元
	mergeSort(v, mid+1, end);
	Merge(v, begin, mid, end);   //比较、合并
}

5.6 快速排序(quick,不稳定,时间O(nlogn),空间O(nlogn))(使用最频繁)(分治法,递的过程中处理)

  • 该算法是目前实践中使用最频繁,实用高效的最好排序算法
  • 快速排序的核心思想也是分治法,分而治之。它的实现方式是每次从序列中选出一个基准值,其他数依次和基准值做比较,比基准值大的放右边,比基准值小的放左边,然后再对左边和右边的两组数分别选出一个基准值,进行同样的比较移动,重复步骤,直到最后都变成单个元素,整个数组就成了有序的序列
  • 快速排序联想成东拆西补或西拆东补,一边拆一边补,直到所有元素达到有序状态。

快速排序思路

  1. 选取第一个数为基准
  2. 将比基准小的数交换到前面,比基准大的数交换到后面
  3. 对左右区间重复第二步,直到各区间只有一个数

与归并算法的区别就在于快排为递去的过程解决问题,归并在归来的过程解决问题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J9gC2isi-1656559406983)(https://note.youdao.com/yws/api/personal/file/77DD7F5E2C184BB9B36F7A3DAC08B9C6?method=download&shareKey=a5658fae1f42c9a69ddfc3dcad4d71ba)]

/*
快速排序通常是实际应用中最好的选择:
因为它平均性能非常好,它的期望时间复杂度是O(nlogn),而且O(nlogn)隐含常数因子非常小
另外它能够进行原址排序  
*/
void quickSort(vector<int>& v, int left, int right) {
		if (v.empty()) return;
		if (left > right) return;
		int base = v[left]; //选取最左边为基准数,不能是下标,数会变
		int i = left, j = right;
		while (i < j) {
			// 从左扫描,找比基准小的数 
			while (i < j) {
				if (v[j] < base) {
					v[i++] = v[j]; //放在左边
					break;
				}
				else
					j--; //继续向左扫描
			}
			// 从右扫描,找比基准大的数
			while (i < j) {
				if (v[i] > base) {
					v[j--] = v[i]; //放在右边
					break;
				}
				else
					i++; //继续向右扫描
			}
		}
		v[i] = base;
		quickSort(v, left, i - 1);
		quickSort(v, i + 1, right);
   }

5.7 堆排序(heap,不稳定,时间O(nlogn),空间O(1),一种选择排序)

  • 堆排序是利用这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。
  • 堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)…1]逐步递减,近似为nlogn。所以堆排序时间复杂度一般认为就是O(nlogn)级。
该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]  
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2] 

 通常堆是通过一维数组来实现的。在数组起始位置为0的情形中,父结点和子结点的位置关系如下:
1.索引为i的左孩子的索引是 (2* i+1)
2.索引为i的右孩子的索引是 (2* i+2)
3.索引为i的父结点的索引是 (i -1)/2 

堆排序思路

  • 步骤一:建立大根堆–将n个元素组成的无序序列构建一个大根堆,
  • 步骤二:交换堆元素–交换堆尾元素和堆首元素,使堆尾元素为最大元素;
  • 步骤三:重建大根堆–将前n-1个元素组成的无序序列调整为大根堆
  • 重复执行步骤二和步骤三,直到整个序列有序。

堆排序图示说明

void adjust(vector<int>& vec, int len, int index) { //不用返回值,执行完就结束了
	int left = 2 * index + 1;    // index的左子节点
	int right = 2 * index + 2;  // index的右子节点

	int maxIdx = index; // 保存最大值的索引
	// 选出最大值的索引
	if (left < len && vec[left] > vec[maxIdx]) maxIdx = left;
	if (right < len && vec[right] > vec[maxIdx]) maxIdx = right;

	if (maxIdx != index){
		swap(vec[maxIdx], vec[index]);
		adjust(vec, len, maxIdx); //保证子树也是最大堆(自顶向下调整)
	}
}

void heapSort(vector<int> &vec){  
	int len = vec.size();
	// 建立大顶堆(从最后的一个非叶子节点向上创建)计算公式为 (n-1)  / 2, n为下标
	for (int i = len / 2 - 1; i >= 0; --i) {  //(自底向上构建)
		adjust(vec, len, i); 
	}

	//堆排序,把堆顶元素放在数组最后
	for (int i = len - 1; i > 0; --i) {
		swap(vec[0], vec[i]); // 将当前最大的放置到数组末尾
		adjust(vec, i, 0);  // 此时数组已经不是堆了,重新使调整为堆(自顶向下调整)
	}
}
#include<iostream>
#include<algorithm>
#include<vector>
#include<queue>
#include<functional> // 要包含内建函数
using namespace std;

void heapSort(vector<int> &vec){  
	int len = vec.size();
	priority_queue<int, vector<int>, greater<int> > pq; // 建立小顶堆
	for (int i = 0; i < len; ++i){
		pq.push(vec[i]);
	}
	vec.clear();
	while (!pq.empty())
	{
		vec.push_back(pq.top());
		pq.pop();
	}
}
int main(){
	vector<int> vec ;
	vec = { 38, 3, 44, 5, 36, 15, 27, 26, 2 };

	heapSort(vec);
	for (auto it : vec){
		cout << it << ' ';
	}
	cout << endl;
	system("pause");
	return 0;
}

6. 查找算法

6.1 二分查找(必须是有序数组,时间O(logn))

二分查找:又称折半查找,对排好序的数组,每次取这个数和数组中间的数进行比较,复杂度是O(logn) 如:设数组为a[n],查找的数x。(递增)

  • 如果x==a[n/2],则返回n/2;
  • 如果x < a[n/2],则在a[0]到a[n/2-1]中进行查找;
  • 如果x > a[n/2],则在a[n/2+1]到a[n-1]中进行查找;

优点:是比较次数少,查找速度快,平均性能好;其缺点是要求待查表为有序表,且插入删除困难

条件:查找的数组必须要为有序数组。

  • 二分查找场景:寻找一个数、寻找左侧边界、寻找右侧边界。
  • 不要出现 else,而是把所有情况用 else if 写清楚
  • 计算 mid 时需要技巧防止溢出,建议写成: mid = low + (high - low)/2 = (low + high)/2
//模板1 是二分查找的最基础和最基本的形式,用于查找可以通过访问数组中的单个索引来确定的元素或条件。
int binarySearch(vector<int>& nums, int target){
  if(nums.size() == 0)
    return -1;

  int left = 0, right = nums.size() - 1;
  while(left <= right){
    // Prevent (left + right) overflow
    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; 
  }

  // End Condition: left > right
  return -1;
}

1.1 关键属性
二分查找的最基础和最基本的形式。
查找条件可以在不与元素的两侧进行比较的情况下确定(或使用它周围的特定元素)。
不需要后处理,因为每一步中,你都在检查是否找到了元素。如果到达末尾,则知道未找到该元素。
1.2 区分语法
初始条件:left = 0, right = length-1
终止:left > right
向左查找:right = mid-1
向右查找:left = mid+1
// 模板2 是二分查找的高级模板,用于查找需要访问数组中当前索引及其直接右邻居索引的元素或条件。
int binarySearch(vector<int>& nums, int target){
  if(nums.size() == 0)
    return -1;

  int left = 0, right = nums.size();
  while(left < right){
    // Prevent (left + right) overflow
    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; 
  }

  // Post-processing:
  // End Condition: left == right
  if(left != nums.size() && nums[left] == target) return left;
  return -1;
}

2.1关键属性
一种实现二分查找的高级方法。
查找条件需要访问元素的直接右邻居。
使用元素的右邻居来确定是否满足条件,并决定是向左还是向右。
保证查找空间在每一步中至少有 2 个元素。
需要进行后处理。 当你剩下 1 个元素时,循环 / 递归结束。 需要评估剩余元素是否符合条件。
2.2 区分语法
初始条件:left = 0, right = length
终止:left == right
向左查找:right = mid
向右查找:left = mid+1
模板3 是二分查找的另一种独特形式,用于搜索需要访问当前索引及其在数组中的直接左右邻居索引的元素或条件。
int binarySearch(vector<int>& nums, int target){
    if (nums.size() == 0)
        return -1;

    int left = 0, right = nums.size() - 1;
    while (left + 1 < right){
        // Prevent (left + right) overflow
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) 
            return mid;
         else if (nums[mid] < target) //等于、小于、大于
            left = mid;
         else if (nums[mid] > target) 
            right = mid;    
    }

    // Post-processing:
    // End Condition: left + 1 == right
    if(nums[left] == target) return left;
    if(nums[right] == target) return right;
    return -1;
}

3.1 关键属性
实现二分查找的另一种方法。
搜索条件需要访问元素的直接左右邻居。
使用元素的邻居来确定它是向右还是向左。
保证查找空间在每个步骤中至少有 3 个元素。
需要进行后处理。 当剩下 2 个元素时,循环 / 递归结束。 需要评估其余元素是否符合条件。
3.2 区分语法
初始条件:left = 0, right = length-1
终止:left + 1 == right
向左查找:right = mid
向右查找:left = mid

7. 位运算(优先级较低)

  • 5种位运算符
  • 由于位运算直接对内存数据进行操作,不需要转换成十进制,因此处理速度非常快
  • 位运算符的优先级比较低,因此应尽量使用括号来保证运算顺序
  • 位运算符只能用于整型数据
图片名称

7.1 正整数变成负整数,负整数变成正整数,取反加1

-11变成11
1111 0101(二进制) 取反-> 0000 1010(二进制)1-> 0000 1011(二进制)
11变成-11
0000 1011(二进制) 取反-> 1111 0100(二进制)1-> 1111 0101(二进制)

7.2 &两个1才是1

n & 1 ;            //用来判断整数的奇偶,二进制末位为0是偶数,为1是奇数
if((n & 1) == 1)//判断n是否为奇数
1<<左移            //用来找n的二进制1的位置
(n & m) << 1;     //相当于求两个二进制的进位

7.3 |只要一个为1就是1

n | 1          //把二进制末位强行变为1。
(n | 1)-1    //变为0,之后减1就可以

7.4 ^不同为1,相同为0,其实就是不进位加法

异或的几条性质:

1**交换律**:a ^ b=b ^ a
2**结合律**(a ^ b) ^ c == a^ (b ^ c)
3(a ^ b) ^ b=a; //异或的逆运算是其本身,两次异或同一个数结果不变
4、a ^ b ^ a = a ^ a ^ b = b ^ a ^ a =b; 例:res = 0 ^ 1 ^ 4 ^ 1 ^ 6 = 4 ^ 6
5、n ^ m;          //两个二进制的相加,不进位加法

“异或运算”的特殊作用

1)使特定位翻转:例:低4位翻转,用10101110^00001111 = 1010 00012)交换二个数: a = a ^ b;     //a为其他临时值
                                b = a ^ b;     //a赋值给b  
                                a = a ^ b;     //b赋值给a

7.5 << 左移

//在左移n位的时候,最左边的n位将被丢弃,同时在最右边补上n个0。
1 << 3;      //左移3位

7.6 >> 右移

//如果数字是一个无符号数值,则用0填补最左边的n位;
//如果数字是一个有符号数值,则用1补最左边的n位:
1000 1010 >> 3 = 1111 0001; //右移3位
1 >> 3;                     //右移3位

7.7 bitset

#include <bitset>
using std::bitset;
bitset<32> (n).count();// 临时对象

bitset<32> bit(n);//将n初始化为32位
return bit.count();//返回其中1的个数

8. 双指针

8.1 快慢指针

快慢指针也是双指针,但是两个指针从同一侧开始遍历数组,将这两个指针分别定义为快指针(fast)和慢指针(slow),两个指针以不同的策略移动,直到两个指针的值相等(或其他特殊条件)为止,如 fast 每次增长两个,slow 每次增长一个

  1. 计算链表的中点:快慢指针从头节点出发,每轮迭代中,快指针向前移动两个节点,慢指针向前移动一个节点,最终当快指针到达终点的时候,慢指针刚好在中间的节点。
  2. 求链表倒数第k个元素:先让其中一个指针向前走k步,接着两个指针以同样的速度一起向前进,直到前面的指针走到尽头了,则后面的指针即为倒数第k个元素。(严格来说应该叫先后指针而非快慢指针
  3. 判断链表是否有环:如果链表中存在环,则在链表上不断前进的指针会一直在环里绕圈子,且不能知道链表是否有环。使用快慢指针,当链表中存在环时,两个指针最终会在环中相遇
  4. 判断链表中环的起点首先判断有环。并且知道了两个指针相遇的节点,我们可以让其中任一个指针指向头节点,然后让它俩以相同速度前进,再次相遇时所在的节点位置就是环开始的位置
  5. 求链表中环的长度首先判断有环。只要相遇后一个不动,另一个前进直到相遇算一下
55 链表中环的入口结点
// 快慢指针法
class Solution {
public:
    ListNode* EntryNodeOfLoop(ListNode* pHead) {
        if(pHead == nullptr) return nullptr;
        ListNode* slow = pHead, *fast = pHead; // 定义快慢指针
        
        //其中 fast->next 的检测为了防止链表溢出
        while(fast != nullptr && fast->next != nullptr){
            fast = fast->next->next;
            slow = slow->next;
            if(fast == slow){
                fast = pHead;
                break;
            }
        }
        if(fast == nullptr || fast->next == nullptr) return nullptr;
        while(fast != slow){
                fast = fast->next;
                slow = slow->next;
        }
        return fast;
    }
};

8.2 碰撞指针(排好序的数组或链表)

对撞指针是指在数组中,将指向最左侧的索引定义为左指针(left),最右侧的定义为右指针(right),然后从两头向中间进行数组遍历。

  1. 二分查找问题
  2. n数之和问题:比如两数之和问题,先对数组排序然后左右指针找到满足条件的两个数。如果是三数问题就转化为一个数和另外两个数的两数问题。以此类推。
//42 和为S的两个数字
// 双指针(碰撞指针)
class Solution {
public:
    vector<int> FindNumbersWithSum(vector<int> array,int sum) {
        vector<int> res;
        if(array.size() == 0) return vector<int> ();
        
        int left = 0, right = array.size() - 1; //构建碰撞指针
        //有点类似于二分查找
        while(left < right) {
            if(array[left] + array[right] == sum){
                res.push_back(array[left]);
                res.push_back(array[right]);
                return res;
            }
            else if(array[left] + array[right] < sum)
                ++left; //两数之和小于sum,移动左指针
            else if(array[left] + array[right] > sum)
                --right;//两数之和大于sum,移动右指针
        }
        return vector<int>();
    }
};

8.3 滑动窗口法

两个指针,一前一后组成滑动窗口,并计算滑动窗口中的元素的问题。

这类问题一般包括:

  1. 字符串匹配问题
  2. 子数组问题
41 和为S的连续正数序列

// 滑动窗口解法
class Solution {
public:
    vector<vector<int> > FindContinuousSequence(int sum) {
        vector<vector<int>> res;
        //两个起点,相当于动态窗口的两边,根据其窗口内的值的和来确定窗口的位置和大小
        int left = 1, right = 2; //是实值,不是下标
        while(left < right) { //左=右就结束了
           //由于是连续的,差为1的一个序列,那么求和公式是(a0+an)*n/2
            int arraySum = (left + right) * (right - left + 1) / 2; 
            if(arraySum == sum){ //相等,那么就将窗口范围的所有数添加进结果集
                vector<int> temp;
                for(int i = left; i <= right; ++i){
                    temp.push_back(i);
                }
                res.push_back(temp);
                ++left; //缩小窗口,继续找
            }
            else if(arraySum < sum) ++right;
            else if(arraySum > sum) ++left; //注意:这里不一样
        }
        return res;
    }
};
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宇光_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值