代码随想录刷题学习笔记——高级篇C++/Python

C++代码随想录高级篇–学习笔记


提示:本文章仅用于记录自己跟随代码随想录刷算法题的过程,对于其中的代码,理解比较清晰的会给出自己的一些想法,代码基本上与随想录中提供的一致。如有想要查看源码的小伙伴可以指路[代码随想录官网](https://programmercarl.com/)

文章目录

  • C++代码随想录高级篇--学习笔记
  • 前言
  • 一、二叉树
    • 1. 基本概念
    • 2. 二叉树递归遍历
    • 3.二叉树迭代遍历
    • 4. 二叉树层序遍历
    • 5. 二叉树的右视图
    • 6. 二叉树的层平均值
    • 7. N叉树的层序遍历
    • 8. 在每个树行中找最大值
    • 9. 填充每个节点的下一个右侧节点指针
    • 10. 二叉树的最大深度
    • 11. 二叉树的最小深度
    • 12. 翻转二叉树
    • 13. 对称二叉树
    • 14. 完全二叉树的节点个数
      • 二叉树节点的高度和深度
    • 15. 平衡二叉树
    • 16. 二叉树的所有路径
    • 17. 二叉树的左叶子节点的和
    • 18. 找树左下角的值
    • 19. 路径之和
    • 20. 路径之和加强版
    • 21. 从中序与后序遍历序列构造二叉树
    • 22. 最大二叉树
    • 23. 最大二叉树
  • 二、回溯算法
    • 1. 组合
    • 2. 组合总和 III
    • 3. 电话号码的字母组合
    • 5. 组合总和
    • 6. 组合总和II
    • 7. 分割回文串
      • 看不懂,重点复习的对象
    • 8. 全排列
    • 9. 全排列II
    • 10. N皇后
    • 11. 子集
    • 12. 子集II
    • 13. 递增子序列
    • 总结:回溯算法小模板
  • 三、贪心算法
    • 1. 分发饼干
    • 2. 摆动序列
    • 3. 最大子序和/最大子数组和
    • 4. 买卖股票的最佳时机II
    • 5. 跳跃游戏
    • 6. 跳跃游戏II
    • 7. K次取反后最大化的数组和
    • 8. 加油站
  • 四、动态规划
    • 1. 斐波那契数列
    • 2. 爬楼梯
  • 总结


前言

提示:这里可以添加本文要记录的大概内容:

算法大牛的博客之算法细节解说
https://blog.csdn.net/qq_22136439/article/details/122212173


提示:以下是本篇文章正文内容,下面案例可供参考

一、二叉树

1. 基本概念

  • C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是logn,需要注意的是unordered_map、unordered_set,unordered_map、unordered_set底层实现是哈希表。
  • 二叉树可以链式存储,也可以顺序存储。链式存储时,父节点通过左右指针将左右孩子节点数据次第存储;顺序存储时,数据按照二叉树的深度一层层存储。
  • 二叉树的种类:AVL树(平衡二叉树)、满二叉树、完全二叉树等等。
  • 二叉树的遍历方式:深度优先遍历和广度优先遍历。深度优先遍历,即对每个可能的分支往深处走,知道遇见叶子节点就返回,找寻下一个支路路径。广度优先遍历,将二叉树按层区分,一层层遍历数据。深度优先遍历又包括:前序(根左右)、中序(左根右)、后序遍历(左右根),根据遍历的动作特点,这三种递归迭代的方式可通过栈来实现。
  • 容器的基本使用:
容器名称 底层实现
vector 动态数组
list 双向链表
deque 双端队列
queue 队列
stack 堆栈
set 红黑树实现的集合容器
map 红黑树实现的键值对容器
unordered_set 哈希表实现的集合容器
unordered_map 哈希表实现的键值对容器
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main() {
    // 使用vector容器
    std::vector<int> vec = {1, 2, 3, 4, 5};
    for (int num : vec) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    // 使用list容器
    std::list<std::string> myList = {"Hello", "World", "OpenAI"};
    for (const std::string& str : myList) {
        std::cout << str << " ";
    }
    std::cout << std::endl;

    // 使用deque容器
    std::deque<double> myDeque = {1.1, 2.2, 3.3};
    for (double num : myDeque) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    // 使用queue容器
    std::queue<int> myQueue;
    myQueue.push(1);
    myQueue.push(2);
    myQueue.push(3);
    while (!myQueue.empty()) {
        std::cout << myQueue.front() << " ";
        myQueue.pop();
    }
    std::cout << std::endl;

    // 使用stack容器
    std::stack<char> myStack;
    myStack.push('A');
    myStack.push('B');
    myStack.push('C');
    while (!myStack.empty()) {
        std::cout << myStack.top() << " ";
        myStack.pop();
    }
    std::cout << std::endl;

    // 使用set容器
    std::set<int> mySet = {3, 1, 4, 1, 5}; // 自动去重并排序
    for (int num : mySet) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    // 使用map容器
    std::map<std::string, int> myMap = {{"Alice", 25}, {"Bob", 30}, {"Charlie", 35}};
    for (const auto& pair : myMap) {
        std::cout << pair.first << ": " << pair.second << std::endl;
    }

    // 使用unordered_set容器
    std::unordered_set<int> myUnorderedSet = {5, 2, 8, 2, 3}; // 无序且自动去重
    for (int num : myUnorderedSet) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    // 使用unordered_map容器
    std::unordered_map<std::string, int> myUnorderedMap = {{"Apple", 1}, {"Banana", 2}, {"Orange", 3}};
    for (const auto& pair : myUnorderedMap) {
        std::cout << pair.first << ": " << pair.second << std::endl;
    }

    return 0;
}

二叉树节点的结构体定义方法

定义一个二叉树结构体——链式定义法
struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(NULL), right(NULL) {}
    //在创建 TreeNode 类型的对象时,初始化该对象的成员变量 val、left 和 right 的值
};
写法二:
struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int x);
    //定义了一个名为 TreeNode 的结构体的构造函数,该构造函数有一个 int 类型的参数 x,
    //用来初始化结构体的成员变量 val
};
PS:
1. 在 C++ 中,结构体和类可以定义一个或多个构造函数,用来在创建结构体或类对象时初始化其成员变量。
在上述代码中,TreeNode 结构体定义了一个构造函数,当使用类似 new TreeNode(1) 的语句创建一个
TreeNode 对象时,该构造函数会被调用,用参数 1 来初始化 val 的值。
TreeNode::TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
2. 在这种写法中,结构体的成员变量仍然是在结构体内部进行声明的,
但是构造函数的定义被移到了结构体外部,通过作用域解析运算符 :: 连接到结构体中。

2. 二叉树递归遍历

前序遍历举例
class Solution {
public:
    void traversal(TreeNode* cur, vector<int>& vec) {//该函数定义遍历的顺序
    //输入参数为:当前节点和存储遍历结果的数组,数组使用的是传递引用参数
        if (cur == NULL) return;
        vec.push_back(cur->val);    // 中
        traversal(cur->left, vec);  // 左
        traversal(cur->right, vec); // 右
        通过函数的递归调用实现,当程序的输入为一个二叉树的根节点,只要后续不为空,
        会按照遍历原则继续找下去并存储在vec中。引用的vec会直接修改result中的内容
    }

    vector<int> preorderTraversal(TreeNode* root) {//给定一个根节点指针,开始遍历
    //结果存放在result中
        vector<int> result;
        traversal(root, result);
        return result;
    }
};
PS:
1. STL容器大多数都是动态分配存储空间,本次使用的vector就是的。设想一下,
它不需要像数组那样预定义空间大小,会根据存储状况动态变化。
因此,为了便于空间变化,我们每次存入数据都只能放在vector的尾部,于是出现了push_back。

2.观察函数的参数定义,我们会发现,存放遍历结果的数组使用的是引用参数的方式,这样有什么好处呢?
vector<int>& vec 是一个引用类型,它表示对外部传入的vector<int> 对象的一个引用。
这意味着,当调用 traversal 函数时,实际上并没有创建一个新的 vector<int> 对象,
而是将外部传入的 vector<int> 对象的引用传递给了 traversal 函数,从而使得 traversal 函数可以
在引用的基础上对 vector<int> 对象进行修改。
如果使用的是 vector<int> 类型的参数,那么就会发生一次拷贝的操作。
具体来说,当调用 traversal 函数时,系统会创建一个新的 vector<int> 对象,
将外部传入的 vector<int>
对象的内容复制到新对象中,然后将这个新对象的引用传递给 traversal 函数。
在函数内部对 vector<int> 对象进行修改时,实际上是在修改新对象中的内容,
而不会对外部传入的对象产生
影响。当函数返回时,新对象会被销毁,从而造成一次内存的浪费。

3.在遍历存值的时候,我们只在指针访问到当前的节点为父节点时才赋值。
如果每次都对左右根节点赋值,那么在后续的遍历中会存在同一个节点上的数被添加了多次而导致错误。

在理解了前序遍历的设计思路和本质之后,那么中序和后序的设计就大同小异了。

中序遍历
void traversal(TreeNode* cur, vector<int>& vec) {
    if (cur == NULL) return;
    traversal(cur->left, vec);  // 左
    vec.push_back(cur->val);    // 中
    traversal(cur->right, vec); // 右
}
后序遍历
void traversal(TreeNode* cur, vector<int>& vec) {
    if (cur == NULL) return;
    traversal(cur->left, vec);  // 左
    traversal(cur->right, vec); // 右
    vec.push_back(cur->val);    // 中
}

3.二叉树迭代遍历

递归和迭代的区别:
1)递归是重复调用函数自身实现循环,遇到满足终止条件的情况时逐层返回来结束;迭代是函数内某段代码实现循环,循环代码中参与运算的变量同时是保存结果的变量,当前保存的结果作为下一次循环计算的初始值。

前序遍历-迭代法 利用栈实现
class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        stack<TreeNode*> st;
        vector<int> result;
        if (root == NULL) return result;
        st.push(root);
        while (!st.empty()) {
            TreeNode* node = st.top();                       // 中
            st.pop();
            result.push_back(node->val);
            if (node->right) st.push(node->right);           // 右(空节点不入栈)
            if (node->left) st.push(node->left);             // 左(空节点不入栈)
        }
        return result;
    }
};
中序遍历 基于栈的特殊性,没办法套用前序遍历的写法
class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> result;
        stack<TreeNode*> st;
        TreeNode* cur = root;
        while (cur != NULL || !st.empty()) {
            if (cur != NULL) { // 指针来访问节点,访问到最底层
                st.push(cur); // 将访问的节点放进栈
                cur = cur->left;                // 左
            } else {
                cur = st.top(); // 从栈里弹出的数据,就是要处理的数据(放进result数组里的数据)
                st.pop();
                result.push_back(cur->val);     // 中
                cur = cur->right;               // 右
            }
        }
        return result;
    }
};

代码随想录刷题学习笔记——高级篇C++/Python_第1张图片
图片摘自代码随想录

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        stack<TreeNode*> st;
        vector<int> result;
        if (root == NULL) return result;
        st.push(root);
        while (!st.empty()) {
            TreeNode* node = st.top();
            st.pop();
            result.push_back(node->val);
            if (node->left) st.push(node->left); // 相对于前序遍历,这更改一下入栈顺序 (空节点不入栈)
            if (node->right) st.push(node->right); // 空节点不入栈
        }
        reverse(result.begin(), result.end()); // 将结果反转之后就是左右中的顺序了
        return result;
    }
};

4. 二叉树层序遍历

我看明白了,但是我感觉自己写写不出来
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        queue<TreeNode*> que;
        if (root != NULL) que.push(root);
        vector<vector<int>> result;
        while (!que.empty()) {
            int size = que.size();
            vector<int> vec;
            //每一层设定一个数组存放数据,而且每一层大小不同,更加证明了使用vector的好处
            // 这里一定要使用固定大小size,不要使用que.size(),因为que.size是不断变化的
            for (int i = 0; i < size; i++) {
            //这里是对当前层的遍历,所以在这里是相对固定的值
                TreeNode* node = que.front();
                que.pop();
                vec.push_back(node->val);
                if (node->left) que.push(node->left);//这里说明了que.size会变化的原因
                if (node->right) que.push(node->right);

            }
            result.push_back(vec);
        }
        return result;
    }
};

PS:
1.需要注意的是,我们在实现代码的时候,熟练掌握容器的特性以及容器的使用方法,在形成代码的过程中并不是想着堆栈是怎样先进后出的,而是在基于它先进后出的基础之上,我要如何设置我的遍历执行过程才能完整的完成题目指定的任务。这样,困惑会减少很多,相当于,此时不需要想太多底层的问题,而是在更外面的一层实现功能。
2.在每一次循环执行过程中,队列中的front数值被取出来并删除,实现先进先出,后进后出。所以,语句相当于一边在将front指向的数值赋值给result,成为我们想要获取的数值,同时也在指向更新着下一层的队列,当本层遍历完毕的时候,下一层成为了front循环使用

5. 二叉树的右视图

搞懂前面的二叉树层序遍历的话就so easy 啦
class Solution {
public:
    vector<int> rightSideView(TreeNode* root) {
        queue<TreeNode*> que;
        if (root != NULL) que.push(root);
        vector<int> result;
        while (!que.empty()) {
            int size = que.size();
            for (int i = 0; i < size; i++) {
                TreeNode* node = que.front();
                que.pop();
                if (i == (size - 1)) result.push_back(node->val); // 将每一层的最后元素放入result数组中
                if (node->left) que.push(node->left);
                if (node->right) que.push(node->right);
            }
        }
        return result;
    }
};

6. 二叉树的层平均值

class Solution {
public:
    vector<double> averageOfLevels(TreeNode* root) {
        queue<TreeNode*> que;
        if (root != NULL) que.push(root);
        vector<double> result;
        while (!que.empty()) {
            int size = que.size();
            double sum = 0; // 统计每一层的和
            for (int i = 0; i < size; i++) {
                TreeNode* node = que.front();
                que.pop();
                sum += node->val;
                if (node->left) que.push(node->left);
                if (node->right) que.push(node->right);
            }
            result.push_back(sum / size); // 将每一层均值放进结果集
        }
        return result;
    }
};

7. N叉树的层序遍历

具有N个子节点的树节点结构体定义
class Node {
public:
    int val;
    vector<Node*> children;
    //以下为三种类内构造函数,它们没有真实实现,只是为了方便在类外使用时告诉对象有哪几种参数设置的类型
    Node() {}

    Node(int _val) {
        val = _val;
    }

    Node(int _val, vector<Node*> _children) {
        val = _val;
        children = _children;
    }
};

类中的定义说明:
1.类内设计构造函数的目的是为了方便对象的创建和初始化。构造函数是一种特殊的成员函数,当您创建一个对象时,它会被自动调用。它的主要作用是在对象创建时完成必要的初始化工作。
2.构造函数可以用来初始化对象的成员变量,执行必要的资源分配和清理操作,以及执行其他必要的初始化任务。

在C++中,可以使用.和->两种方式来访问类的成员变量和成员函数。.操作符用于访问类的非指针类型对象的成员,而->操作符用于访问类的指针类型对象的成员。当您有一个非指针类型的对象时,可以使用.操作符来访问它的成员变量和成员函数,例如:

class MyClass {
public:
    int x;
    void foo() {}
};

MyClass obj;
obj.x = 42;
obj.foo();

当您有一个指向类对象的指针时,需要使用->操作符来访问该对象的成员变量和成员函数,例如:

MyClass* ptr = new MyClass;
ptr->x = 42;
ptr->foo();
delete ptr;

N叉树的层序遍历算法

class Solution {
public:
    vector<vector<int>> levelOrder(Node* root) {
        queue<Node*> que;
        if (root != NULL) que.push(root);
        vector<vector<int>> result;
        while (!que.empty()) {
            int size = que.size();
            vector<int> vec;
            for (int i = 0; i < size; i++) {
                Node* node = que.front();
                que.pop();
                vec.push_back(node->val);
                for (int i = 0; i < node->children.size(); i++) { // 将节点孩子加入队列
                    if (node->children[i]) que.push(node->children[i]);
                }
            }
            result.push_back(vec);
        }
        return result;

    }
};

8. 在每个树行中找最大值

class Solution {
public:
    vector<int> largestValues(TreeNode* root) {
        queue<TreeNode*> que;
        if (root != NULL) que.push(root);
        vector<int> result;
        while (!que.empty()) {
            int size = que.size();
            int maxValue = INT_MIN; // 取每一层的最大值
            for (int i = 0; i < size; i++) {
                TreeNode* node = que.front();
                que.pop();
                maxValue = node->val > maxValue ? node->val : maxValue;
                if (node->left) que.push(node->left);
                if (node->right) que.push(node->right);
            }
            result.push_back(maxValue); // 把最大值放进数组
        }
        return result;
    }
};

INT_MIN是C++中头文件中定义的常量,它表示int类型的最小值。INT_MIN=-2147483648,这是int类型可以表示的最小负整数。
在代码中,将maxValue初始化为INT_MIN的目的是为了将其赋值为比任何其他int类型的值都小的一个值,以便在后续的比较中找到最大值。这种做法是为了确保maxValue在循环中的第一个迭代中被更新为第一个元素的值,因为第一个元素可能是整个序列中的最大值。

9. 填充每个节点的下一个右侧节点指针

/*
// Definition for a Node.
class Node {
public:
    int val;
    Node* left;
    Node* right;
    Node* next;

    Node() : val(0), left(NULL), right(NULL), next(NULL) {}

    Node(int _val) : val(_val), left(NULL), right(NULL), next(NULL) {}

    Node(int _val, Node* _left, Node* _right, Node* _next)
        : val(_val), left(_left), right(_right), next(_next) {}
};
*/

class Solution {
public:
    Node* connect(Node* root) {
        queue<Node*> que;
        if (root != NULL) que.push(root);
        while (!que.empty()) {
            int size = que.size();
            // vector vec;
            Node* nodePre;
            Node* node;
            for (int i = 0; i < size; i++) {
                if (i == 0) {
                    nodePre = que.front(); // 取出一层的头结点
                    que.pop();
                    node = nodePre;
                } else {
                    node = que.front();
                    que.pop();
                    nodePre->next = node; // 本层前一个节点next指向本节点
                    nodePre = node;//Pre->next;
                }
                if (node->left) que.push(node->left);
                if (node->right) que.push(node->right);
            }
            nodePre->next = NULL; // 本层最后一个节点指向NULL
        }
        return root;

    }
};

10. 二叉树的最大深度

层序遍历的方法不变,不需要存储遍历得到的结果,添加一个计数器,每一次循环结束加1即可。
具体过程略

11. 二叉树的最小深度

只有当左右孩子都为空的时候,才说明遍历的最低点了,如果其中一个孩子为空则不是最低点。
层序遍历法不变,当首次遇见左右孩子都为空的时候,就停止遍历,将计数器的结果返回。这里与上面的不同之处在于,对于for (int i = 0; i < size; i++),这里面的遍历可能没有结束就给出返回值了,整个函数的操作结束。
具体代码过程略

12. 翻转二叉树

代码随想录刷题学习笔记——高级篇C++/Python_第2张图片
图片来源:LeetCode网站
起初想用层序遍历法,将每一层的遍历结果reverse就可以了,但是很快发现没审题。其实,这道题的意思就是将每个父节点的左右孩子节点位置翻转,与整体的reverse是不相同的。那么,其实就是简单直接的swap交换两个指针指向就可以了。

#include 
#include 
using namespace std;

struct TreeNode {
    int val;
    TreeNode* left;//结构体的嵌套使用
    TreeNode* right;
    TreeNode(int x) : val(x), left(NULL), right(NULL) {}//构造函数,初始化类对象
};

TreeNode* invertTree(TreeNode* root) {
    if (root == NULL) return NULL; // 如果根节点为空,直接返回
    queue<TreeNode*> que;
    que.push(root);
    while (!que.empty()) {
        TreeNode* node = que.front();
        que.pop();
        //我们会发现这里不需要存储值了,因为题目任务返回的是节点,不再是遍历的某种形式的结果
        swap(node->left, node->right); // 翻转当前节点的左右子节点
        //时刻要记得熟练使用现有函数呀,这样题目就简单了许多
        if (node->left != NULL) que.push(node->left); // 将左子节点入队
        if (node->right != NULL) que.push(node->right); // 将右子节点入队
    }
    return root;
}

void printTree(TreeNode* root) {
    if (root == NULL) return;
    cout << root->val << " ";
    printTree(root->left);
    printTree(root->right);
}

int main() {
    TreeNode* root = new TreeNode(4);
    root->left = new TreeNode(2);
    root->right = new TreeNode(7);
    root->left->left = new TreeNode(1);
    root->left->right = new TreeNode(3);
    root->right->left = new TreeNode(6);
    root->right->right = new TreeNode(9);
    cout << "Original tree: ";
    printTree(root);
    cout << endl;
    TreeNode* newRoot = invertTree(root);
    cout << "Inverted tree: ";
    printTree(newRoot);
    cout << endl;
    return 0;
}

用前后序遍历法也是可以的

#include 
using namespace std;

struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};

void invertTree(TreeNode* root) {
    if (root == NULL) return;
    swap(root->left, root->right); // 翻转当前节点的左右子节点
    invertTree(root->left); // 递归翻转左子树
    invertTree(root->right); // 递归翻转右子树
}

void printTree(TreeNode* root) {
    if (root == NULL) return;
    cout << root->val << " ";
    printTree(root->left);
    printTree(root->right);
}

int main() {
    TreeNode* root = new TreeNode(4);
    root->left = new TreeNode(2);
    root->right = new TreeNode(7);
    root->left->left = new TreeNode(1);
    root->left->right = new TreeNode(3);
    root->right->left = new TreeNode(6);
    root->right->right = new TreeNode(9);
    cout << "Original tree: ";
    printTree(root);
    cout << endl;
    invertTree(root);
    cout << "Inverted tree: ";
    printTree(root);
    cout << endl;
    return 0;
}

Python版就更加简洁啦

# 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 invertTree(self, root: TreeNode) -> TreeNode:
        if not root:
            return None
        root.left, root.right = root.right, root.left
        self.invertTree(root.left)
        self.invertTree(root.right)
        return root

13. 对称二叉树

代码随想录刷题学习笔记——高级篇C++/Python_第3张图片
图片来源:LeetCode
用递归法遍历二叉树可以解决,但我还是有点懵,暂不更新。
按照递归的做法,左右两个分支指针,刚好一个从最左往右,一个从最右往左,成对遍历,若数值一直相同,那么就是对称二叉树。

14. 完全二叉树的节点个数

代码随想录刷题学习笔记——高级篇C++/Python_第4张图片
截图来源:LeetCode

通过递归法可以解决大部分的二叉树问题,而且递归可以减少代码量,但是理解每一题条件进行的递归过程非常重要。

递归法解决问题
class Solution {
private:
    int getNodesNum(TreeNode* cur) {
        if (cur == NULL) return 0;
        int leftNum = getNodesNum(cur->left);      // 左
        int rightNum = getNodesNum(cur->right);    // 右
        int treeNum = leftNum + rightNum + 1;      // 中
        return treeNum;
    }
public:
    int countNodes(TreeNode* root) {
        return getNodesNum(root);
    }
};
如果你总是看上面的递归写法转不过弯,可以尝试这样写
class Solution {
private:
    int getNodesNum(TreeNode* cur) {//如果假设这是一个类
        if (cur == NULL) return 0;//补充特殊条件
        int leftNum;//成员变量1
        int rightNum;//成员变量2
        int treeNum;// 成员变量3
        
        leftNum = getNodesNum(cur->left);      // 类中的嵌套使用
        rightNum = getNodesNum(cur->right);    // 类中的嵌套使用
        treeNum = leftNum + rightNum + 1; //成员函数
        //这样是否比较好理解:1表示当前这个中间节点,没有子节点时,当前节点能够得到的计数就是1

        return treeNum;
    }
public:
    int countNodes(TreeNode* root) {
        return getNodesNum(root);
    }
};
PS:
所以总的计数情况就变为:
情况1:先自上而下,遍历到了最后一层子节点,所有的返回值都是1,打包上传
情况2:自下而上回归到中间某一层的父节点,收到来自子节点的返回值并加上自身,打包上传
情况3:回归到根节点,递归打包带回了所有的返回值
这些返回值不是累加式的,而是类似于f(f(x))获得的结果,因而传到根节点处只有一个值,记录了个数和,最后加上根节点自身1,记为最终结果
我认为这样会比较好理解一些

如果递归法不是很懂的话,用层序遍历也可以,嘿嘿
但可能显得不是那么的聪明

class Solution {
public:
    int countNodes(TreeNode* root) {
        queue<TreeNode*> que;
        if (root != NULL) que.push(root);
        int num=0;
        while (!que.empty()) {
            int size = que.size();
            num+=size;
            
            for (int i = 0; i < size; i++) {
                TreeNode* node = que.front();
                que.pop();
               
                if (node->left) que.push(node->left);
                if (node->right) que.push(node->right);
            }
            
        }
        return num;
    }
};

二叉树节点的高度和深度

在一棵二叉树中,节点的高度和深度是不同的概念。

节点的深度是指从根节点出发到该节点所经过的边的数量,也就是该节点所在的层数。根节点的深度为0,它的子节点的深度为1,以此类推。

节点的高度是指从该节点到树的叶子节点的最长路径上所经过的边的数量。叶子节点的高度为0,它的父节点的高度为1,以此类推。

因此,一般情况下,节点的高度和深度是不同的。只有在一些特殊情况下,例如满二叉树和完全二叉树中,所有叶子节点的高度和深度相同。但是对于一般的二叉树来说,节点的高度和深度是不同的。

实际做题中,还是要根据题目示范的意思,数对了就行了,不用太纠结。

15. 平衡二叉树

代码随想录刷题学习笔记——高级篇C++/Python_第5张图片
截图来自:LeetCode

class Solution {
public:
    // 返回以该节点为根节点的二叉树的高度,如果不是平衡二叉树了则返回-1
    int getHeight(TreeNode* node) {
        if (node == NULL) {
            return 0;
        }
        int leftHeight = getHeight(node->left);
        if (leftHeight == -1) return -1;
        int rightHeight = getHeight(node->right);
        if (rightHeight == -1) return -1;
        return abs(leftHeight - rightHeight) > 1 ? -1 : 1 + max(leftHeight, rightHeight);
    }
    bool isBalanced(TreeNode* root) {
        return getHeight(root) == -1 ? false : true;
    }
};

16. 二叉树的所有路径

代码随想录刷题学习笔记——高级篇C++/Python_第6张图片
截图来源:LeetCode
代码随想录刷题学习笔记——高级篇C++/Python_第7张图片
注意审题,这道题要求的返回值是具有一定格式要求的字符串

class Solution {
private:

    void traversal(TreeNode* cur, vector<int>& path, vector<string>& result) {
        path.push_back(cur->val); // 中,中为什么写在这里,因为最后一个节点也要加入到path中 
        // 这才到了叶子节点
        if (cur->left == NULL && cur->right == NULL) {
            string sPath;
            for (int i = 0; i < path.size() - 1; i++) {
                sPath += to_string(path[i]);
                sPath += "->";
            }
            sPath += to_string(path[path.size() - 1]);
            result.push_back(sPath);
            return;
        }
        if (cur->left) { // 左 
            traversal(cur->left, path, result);
            path.pop_back(); // 回溯
        }
        if (cur->right) { // 右
            traversal(cur->right, path, result);
            path.pop_back(); // 回溯
        }
    }

public:
    vector<string> binaryTreePaths(TreeNode* root) {
        vector<string> result;
        vector<int> path;
        if (root == NULL) return result;
        traversal(root, path, result);
        return result;
    }
};

还不是很懂,浅浅记录一下,以后再分析

17. 二叉树的左叶子节点的和

代码随想录刷题学习笔记——高级篇C++/Python_第8张图片
截图来自:LeetCode

为什么判断当前节点是不是左叶子是无法判断的,必须要通过节点的父节点来判断其左孩子是不是左叶子?
因为仅仅通过判断当前的节点是否具有左右子节点,只能判断是否为叶子节点,但是题目要求的是左叶子节点。所以,这个时候必须要通过当前节点的父节点来判断它的相对位置。如果没有认清楚这个关键问题,其实是你下意识忽略了这个相对位置的存在,实际上你已经在使用这种判断了,但在代码层面并没有让机器识别。

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    int sumOfLeftLeaves(TreeNode* root) {
        if (root == NULL) return 0;
        if (root->left == NULL && root->right== NULL) return 0;

        int leftValue = sumOfLeftLeaves(root->left);    // 左
        if (root->left && !root->left->left && !root->left->right) { // 左子树就是一个左叶子的情况
            leftValue = root->left->val;
        }
        int rightValue = sumOfLeftLeaves(root->right);  // 右

        int sum = leftValue + rightValue;               // 中
        return sum;
    }
};

18. 找树左下角的值

层序遍历,形象直接,但是怎么保证最后一行第一个元素就是左值而不是右值呢?没有想明白。

class Solution {
public:
    int findBottomLeftValue(TreeNode* root) {
        queue<TreeNode*> que;
        if (root != NULL) que.push(root);
        int result = 0;
        while (!que.empty()) {
            int size = que.size();
            for (int i = 0; i < size; i++) {
                TreeNode* node = que.front();
                que.pop();
                if (i == 0) result = node->val; // 记录最后一行第一个元素
                if (node->left) que.push(node->left);
                if (node->right) que.push(node->right);
            }
        }
        return result;
    }
};

19. 路径之和

懂是懂了,但还是感觉要分清楚使用的是什么顺序的深度遍历算法我还是有点懵嘞

class Solution {
private:
    bool traversal(TreeNode* cur, int count) {
        if (!cur->left && !cur->right && count == 0) return true; // 遇到叶子节点,并且计数为0
        if (!cur->left && !cur->right) return false; // 遇到叶子节点直接返回

        if (cur->left) { // 左
            count -= cur->left->val; // 递归,处理节点;
            if (traversal(cur->left, count)) return true;
            count += cur->left->val; // 回溯,撤销处理结果
        }
        if (cur->right) { // 右
            count -= cur->right->val; // 递归,处理节点;
            if (traversal(cur->right, count)) return true;
            count += cur->right->val; // 回溯,撤销处理结果
        }
        return false;
    }

public:
    bool hasPathSum(TreeNode* root, int sum) {
        if (root == NULL) return false;
        return traversal(root, sum - root->val);
    }
};

20. 路径之和加强版

给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
代码随想录刷题学习笔记——高级篇C++/Python_第9张图片
题目截图来自LeetCode

比之上一题,多了一个存储器的设置,如果会了上一题倒是没什么特别大的难度

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void traversal(TreeNode* cur, int count){
        if(!cur->left && !cur->right && count==0){
            result.push_back(path);
            return;
        }
        if(!cur->left && !cur->right)
            return;
        if(cur->left){
            count -= cur->left->val;
            path.push_back(cur->left->val);
            traversal(cur->left, count);
            count += cur->left->val;
            path.pop_back();
        }
        if(cur->right){
            count -= cur->right->val;
            path.push_back(cur->right->val);
            traversal(cur->right, count);
            count += cur->right->val;
            path.pop_back();
        }
    }

public:
    vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
       
        result.clear();//感觉没什么必要啊,什么时候会出错影响当前的存储吗,emmm
        path.clear();
        if (root == NULL) return result;
        path.push_back(root->val); 
        traversal(root, targetSum - root->val);
        return result;
    
    }
};

21. 从中序与后序遍历序列构造二叉树

题目描述:
根据一棵树的中序遍历与后序遍历构造二叉树。
注意: 你可以假设树中没有重复的元素。

太难了,不是很懂

class Solution {
private:
    TreeNode* traversal (vector<int>& inorder, vector<int>& postorder) {
        if (postorder.size() == 0) return NULL;

        // 后序遍历数组最后一个元素,就是当前的中间节点
        int rootValue = postorder[postorder.size() - 1];
        TreeNode* root = new TreeNode(rootValue);

        // 叶子节点
        if (postorder.size() == 1) return root;

        // 找到中序遍历的切割点
        int delimiterIndex;
        for (delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++) {
            if (inorder[delimiterIndex] == rootValue) break;
        }

        // 切割中序数组
        // 左闭右开区间:[0, delimiterIndex)
        vector<int> leftInorder(inorder.begin(), inorder.begin() + delimiterIndex);
        // [delimiterIndex + 1, end)
        vector<int> rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end() );

        // postorder 舍弃末尾元素
        postorder.resize(postorder.size() - 1);

        // 切割后序数组
        // 依然左闭右开,注意这里使用了左中序数组大小作为切割点
        // [0, leftInorder.size)
        vector<int> leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size());
        // [leftInorder.size(), end)
        vector<int> rightPostorder(postorder.begin() + leftInorder.size(), postorder.end());

        root->left = traversal(leftInorder, leftPostorder);
        root->right = traversal(rightInorder, rightPostorder);

        return root;
    }
public:
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        if (inorder.size() == 0 || postorder.size() == 0) return NULL;
        return traversal(inorder, postorder);
    }
};

22. 最大二叉树

题目指路
比较好理解也很好操作,直接将行动翻译成编程语言就行了

class Solution {
public:
    TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
        TreeNode* node = new TreeNode(0);
        if (nums.size() == 1) {
            node->val = nums[0];
            return node;
        }
        // 找到数组中最大的值和对应的下标
        int maxValue = 0;
        int maxValueIndex = 0;
        for (int i = 0; i < nums.size(); i++) {
            if (nums[i] > maxValue) {
                maxValue = nums[i];
                maxValueIndex = i;
            }
        }
        node->val = maxValue;
        // 最大值所在的下标左区间 构造左子树
        if (maxValueIndex > 0) {
            vector<int> newVec(nums.begin(), nums.begin() + maxValueIndex);
            node->left = constructMaximumBinaryTree(newVec);
        }
        // 最大值所在的下标右区间 构造右子树
        if (maxValueIndex < (nums.size() - 1)) {
            vector<int> newVec(nums.begin() + maxValueIndex + 1, nums.end());
            node->right = constructMaximumBinaryTree(newVec);
        }
        return node;
    }
};
在这里插入代码片

23. 最大二叉树

二、回溯算法

1. 组合

class Solution {
private:
    vector<vector<int>> result; // 存放符合条件结果的集合
    vector<int> path; // 用来存放符合条件结果
    void backtracking(int n, int k, int startIndex) {
        if (path.size() == k) {
            result.push_back(path);
            return;
        }
        for (int i = startIndex; i <= n; i++) {
            path.push_back(i); // 处理节点
            backtracking(n, k, i + 1); // 递归
            path.pop_back(); // 回溯,撤销处理的节点
        }
    }
public:
    vector<vector<int>> combine(int n, int k) {
        backtracking(n, k, 1);
        return result;
    }
};

2. 组合总和 III

题目描述
找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:
只使用数字1到9
每个数字 最多使用一次

返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
题目指路
我觉得回溯法还是很抽象,先了解了解,似懂非懂的,放在这里吧

class Solution {
private:
    vector<vector<int>> result; // 存放结果集
    vector<int> path; // 符合条件的结果
    // targetSum:目标和,也就是题目中的n。
    // k:题目中要求k个数的集合。
    // sum:已经收集的元素的总和,也就是path里元素的总和。
    // startIndex:下一层for循环搜索的起始位置。
    void backtracking(int targetSum, int k, int sum, int startIndex) {
        if (path.size() == k) {
            if (sum == targetSum) result.push_back(path);
            return; // 如果path.size() == k 但sum != targetSum 直接返回
        }
        for (int i = startIndex; i <= 9; i++) {
            sum += i; // 处理
            path.push_back(i); // 处理
            backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex
            sum -= i; // 回溯
            path.pop_back(); // 回溯
        }
    }

public:
    vector<vector<int>> combinationSum3(int k, int n) {
        result.clear(); // 可以不加
        path.clear();   // 可以不加
        backtracking(n, k, 0, 1);
        return result;
    }
};

3. 电话号码的字母组合

题目描述
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
LeetCode来源

class Solution {
private:
    const string letterMap[10] = {
        "", // 0
        "", // 1
        "abc", // 2
        "def", // 3
        "ghi", // 4
        "jkl", // 5
        "mno", // 6
        "pqrs", // 7
        "tuv", // 8
        "wxyz", // 9
    };
public:
    vector<string> result;
    string s;
    void backtracking(const string& digits, int index) {
        if (index == digits.size()) {
            result.push_back(s);
            return;
        }
        int digit = digits[index] - '0';        // 将index指向的数字转为int
        string letters = letterMap[digit];      // 取数字对应的字符集
        for (int i = 0; i < letters.size(); i++) {
            s.push_back(letters[i]);            // 处理
            backtracking(digits, index + 1);    // 递归,注意index+1,一下层要处理下一个数字了
            s.pop_back();                       // 回溯
        }
    }
    vector<string> letterCombinations(string digits) {
        s.clear();
        result.clear();
        if (digits.size() == 0) {
            return result;
        }
        backtracking(digits, 0);
        return result;
    }
};

5. 组合总和

代码随想录刷题学习笔记——高级篇C++/Python_第10张图片

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
        if (sum == target) {
            result.push_back(path);
            return;
        }

        // 如果 sum + candidates[i] > target 就终止遍历
        for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
            sum += candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates, target, sum, i);
            sum -= candidates[i];
            path.pop_back();

        }
    }
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        result.clear();
        path.clear();
        sort(candidates.begin(), candidates.end()); // 需要排序
        backtracking(candidates, target, 0, 0);
        return result;
    }
};

回溯专题我都是好像懂了又好像没懂.这道题无非就是在搞懂回溯算法的同时附加一个求和计数器,当和达到target值的时候就可以跳出来了。但是,如果直接对path中的数值求和,无法直接在沿途sum超过target的时候及时停止,所以最好的设计就是边加边判断。这个判断不仅要帮助遍历循环及时止损,还要帮助回溯算法记住当前的和,以便于决定下一个元素。

另外,在这里,纵向遍历二叉树的时候,层数是不固定的,需要通过target和sum来约束。

6. 组合总和II

代码随想录刷题学习笔记——高级篇C++/Python_第11张图片
这个问题重点在于去重问题的练习,但是题目意思没搞懂,先搁这儿

7. 分割回文串

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。

回文串 是正着读和反着读都一样的字符串。

题目来源:LeetCode

看不懂,重点复习的对象

8. 全排列

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
LeetCode题目来源
全排列还是很好理解的。这里每一层遍历只要排除上一层已经使用过的元素就可以了。这里标记已使用的方法就是使用一个used数组,通过布尔值进行标记。

class Solution {
public:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking (vector<int>& nums, vector<bool>& used) {
        // 此时说明找到了一组
        if (path.size() == nums.size()) {
            result.push_back(path);
            return;
        }
        for (int i = 0; i < nums.size(); i++) {
            if (used[i] == true) continue; // path里已经收录的元素,直接跳过
            used[i] = true;
            path.push_back(nums[i]);
            backtracking(nums, used);
            path.pop_back();
            used[i] = false;
        }
'''PS:
对于for循环,其实就可以简单看成是在进行单层回溯的时候需要标记上一层已经使用的元素,并将其排除。
由于每次选择一个数,在此基础之上的组合方案搭配完成之后,在新的分支中所有的数据都是可以重新开始组合
的,因此在该for循环中,出现了
            used[i] = true;
            path.push_back(nums[i]);

            path.pop_back();
            used[i] = false;
            也就是使用完后恢复原来的状态。
'''        
    }
    vector<vector<int>> permute(vector<int>& nums) {
        result.clear();
        path.clear();
        vector<bool> used(nums.size(), false);
        backtracking(nums, used);
        return result;
    }
};

9. 全排列II

上一题数组中的元素都是不同的,这一题数组中的元素会出现重复,那么组成的方案可能会看起来相同,因此需要给最终的方案去重。
其实我不太理解为什么不能用上一题的方法得到搜索结果之后,对结果进行map输入去重呢?这个问题我需要问一下ChatGPT来解答。
题目来源LeetCode官网

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking (vector<int> nums, vector<bool> used) {
        // 此时说明找到了一组
        if (path.size() == nums.size()) {
            result.push_back(path);
            return;
        }
        for (int i = 0; i < nums.size(); i++) {
        ''''
       其实有个奇怪的点就是,这里怎么知道重复的数字一定是在相邻位置呢?
       虽然LeetCode上给的测试用例大概大多数都是数值相同的元素放在一起,构成相邻关系。
       ''''
            // used[i - 1] == true,说明同一树枝nums[i - 1]使用过
            // used[i - 1] == false,说明同一树层nums[i - 1]使用过
            // 如果同一树层nums[i - 1]使用过则直接跳过
            if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
                continue;
            }
            if (used[i] == false) {
                used[i] = true;
                path.push_back(nums[i]);
                backtracking(nums, used);
                path.pop_back();
                used[i] = false;
            }
        }
    }
public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        result.clear();
        path.clear();
        sort(nums.begin(), nums.end()); // 排序
''''
        于是在这里作者解决了我上面的疑惑,还得是你呀,我的思想实在是太僵化了
        学以致用,要多多尝试
        保证使用的元素不变,怎样排列随便你咯
''''        
        vector<bool> used(nums.size(), false);
        backtracking(nums, used);
        return result;
    }
};
PS:
这个方法其实体现的一个规律就是:
我们在思考如何去重时候,往往努力在想如何横向的对元素进行排查去重。但是作者传递的信息就是,
去重首先要想到是什么操作上的疏漏导致存在重复方案的存在。一个是从结果出发,一个是从操作根源出发,
显然给根源问题做排查,添加更符合逻辑的约束,会极大提升解决问题的效率。

10. N皇后

题目描述
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
题目来源:LeetCode

著名的N皇后问题终于来啦✌
这可是一道hard题嗷
如果把前面回溯的模板理解清楚活用起来,那么看起来就还行,但是我觉得我自己可能不懂灵活设计,所以只能看懂,但是不保证自己真的能想到。那么,怎样才能让自己想到呢?我认为首先还是要积累,所有的方法都是学来的,自创那你就是神了。该记还是得记。

class Solution {
private:
vector<vector<string>> result;
// n 为输入的棋盘大小
// row 是当前递归到棋盘的第几行了
void backtracking(int n, int row, vector<string>& chessboard) {
    if (row == n) {
        result.push_back(chessboard);
        return;
    }
    for (int col = 0; col < n; col++) {
        if (isValid(row, col, chessboard, n)) { // 验证合法就可以放
            chessboard[row][col] = 'Q'; // 放置皇后
            backtracking(n, row + 1, chessboard);
            chessboard[row][col] = '.'; // 回溯,撤销皇后
        }
    }
}
bool isValid(int row, int col, vector<string>& chessboard, int n) {
    // 检查列
    for (int i = 0; i < row; i++) { // 这是一个剪枝
        if (chessboard[i][col] == 'Q') {
            return false;
        }
    }
    // 检查 45度角是否有皇后
    for (int i = row - 1, j = col - 1; i >=0 && j >= 0; i--, j--) {
        if (chessboard[i][j] == 'Q') {
            return false;
        }
    }
    // 检查 135度角是否有皇后
    for(int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
        if (chessboard[i][j] == 'Q') {
            return false;
        }
    }
    return true;
}
public:
    vector<vector<string>> solveNQueens(int n) {
        result.clear();
        std::vector<std::string> chessboard(n, std::string(n, '.'));
        backtracking(n, 0, chessboard);
        return result;
    }
};

11. 子集

题目描述
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
题目来源LLeetCode

这道题不仅包含回溯算法涉及到的基本问题,还需要处理的一个环节就是,每一层遍历得到的当前结果都是所要求的子集情况之一,需要对其进行存储并返回给最终的显示结果。

那么这就要追究到,关于回溯的过程,在哪里开始回溯,在哪里调用的具体过程你真的懂了吗?我们在每一层遍历中会使用递归,联络上一层的数据称之为递归,同时需要回溯,也就是撤销当前的操作以便于从上一层结点重新往下开始查找不同的方案。那么,在回溯算法的模板里,其实输入回溯模板的初始状态就是带有前几层二叉树遍历结果的暂存方案,也就是我们在本题目中想要得到的子集。

想清楚上面的逻辑,同时明白使用动态数组定义变量的好处,那么这一题就可以比较直白地解决了

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& nums, int startIndex) {
        result.push_back(path); // 收集子集,要放在终止添加的上面,否则会漏掉自己
        这一句程序语句的使用是在回溯基础之上解决这道题的关键,就看你懂不懂了。
        同时,要知道这句话执行的位置只能放在这里。
        if (startIndex >= nums.size()) { // 终止条件可以不加
            return;
        }
        for (int i = startIndex; i < nums.size(); i++) {
            path.push_back(nums[i]);
            backtracking(nums, i + 1);
            path.pop_back();
        }
    }
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        result.clear();
        path.clear();
        backtracking(nums, 0);
        return result;
    }
};

12. 子集II

题目描述
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
题目来源:LeetCode

需要对子集去重

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& nums, int startIndex, vector<bool>& used) {
        result.push_back(path);
        for (int i = startIndex; i < nums.size(); i++) {
            // used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
            // used[i - 1] == false,说明同一树层candidates[i - 1]使用过
            // 而我们要对同一树层使用过的元素进行跳过
            if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
                continue;
            }
            path.push_back(nums[i]);
            used[i] = true;
            backtracking(nums, i + 1, used);
            used[i] = false;
            path.pop_back();
        }
    }

public:
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        result.clear();
        path.clear();
        vector<bool> used(nums.size(), false);
        sort(nums.begin(), nums.end()); // 去重需要排序
        backtracking(nums, 0, used);
        return result;
    }
};
编写完整个类结尾要加分号别忘记了

13. 递增子序列

题目描述
给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。

数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。

这道题需要仔细看题目中给的测试用例举例,否则很容易理解错误。它不是单一的找到递增的元素就可以了,而是要建立在不改变已有的数组先后顺序基础之上查找子序列。

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& nums, int startIndex) {
        if (path.size() > 1) {
            result.push_back(path);
            // 注意这里不要加return,要取树上的节点
        }
        unordered_set<int> uset; // 使用set对本层元素进行去重
        for (int i = startIndex; i < nums.size(); i++) {
            if ((!path.empty() && nums[i] < path.back())
                    || uset.find(nums[i]) != uset.end()) {
                    continue;
            }
            uset.insert(nums[i]); // 记录这个元素在本层用过了,本层后面不能再用了
            path.push_back(nums[i]);
            backtracking(nums, i + 1);
            path.pop_back();
        }
    }
public:
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        result.clear();
        path.clear();
        backtracking(nums, 0);
        return result;
    }
};

总结:回溯算法小模板

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

三、贪心算法

贪心算法一般分为如下四步:

  • 将问题分解为若干个子问题
  • 找出适合的贪心策略
  • 求解每一个子问题的最优解
  • 将局部最优解堆叠成全局最优解

1. 分发饼干

题目描述
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

简单、不难

给饼干数组序列和胃口数组序列排一下序,后面就一个个遍历就完事儿了
class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        sort(g.begin(),g.end());//胃口
        sort(s.begin(),s.end());//饼干
        int result=0;//如果不初始化为0,在C++中会分配一个默认值,计算结果肯定不对
        int index=s.size()-1;
        for(int i=g.size()-1;i>=0;i--){
            if(index>=0 && s[index]>=g[i]){
                result++;
                index--;
            }
        }
        return result;
    }
};

2. 摆动序列

代码随想录刷题学习笔记——高级篇C++/Python_第12张图片
题目来源:LeetCode

思考:
这道题其实降低了难度,没有让你给出符合条件的最长子序列的具体内容,只要你给出长度即可。所以,这道题其实就简化为计算前后差值的结果序列里相邻符号为异的个数即可。这个时候思考如何设计变量存储差值。
第一反应是将数列中相邻两数的差值计算出来专门存放在一个数组中,那么这个数组是必须的吗?这个时候需要进一步理解题意。1)最终长度的数值总是比异号的数值个数多1;2)如果存在连续的差值处于同符号的状态,我们只需要记住第一个数,知道找到下一个异号的数值为止,重新更新差值数组,记录个数。
这种计算两个原数组元素之间的差值,并比较相邻差值,再判断长度是否加1的过程,需要三个变量,其中两个变量每次比较差值数组中的相邻变量,也就意味着当前变量在下一次比较中会被迭代为前次差值,这样的关系是不是让你想到斐波那契数列的写法?
于是这一题就变得很简单了(* ̄︶ ̄)

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        if (nums.size() <= 1) return nums.size();
        int curDiff = 0; // 当前差值
        int preDiff = 0; // 前次差值
        int result = 1;  // 符合条件的差值个数
        for (int i = 0; i < nums.size() - 1; i++) {
            curDiff = nums[i + 1] - nums[i];

            if ((preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0)) {
                result++;
                preDiff = curDiff; // 迭代更新
            }
        }
        return result;
    }
};

这道题代码随想录中给出的解释,是从山峰山谷的不同状态归纳分析得到,但是我们不妨明确任务中的核心内容,挖掘一下这个差值数组的规律。如果不符合条件,这一差值段必然是相同符号,只要找到下一个异号,我们仍然可以给这个子序列长度+1,直到遍历完整个原数组为止。

3. 最大子序和/最大子数组和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。
题目来源:LeetCode官网
这个题目想到用贪心算法的话,难度不大,代码也很好实现。容易产生的错误观念其实多少与算法设计认知有关,两方面如果连接不好,可能会以为需要分很多种情况。
需要明确以下几点:
1)题目要求返回的是最大子序列的和,也就是一个值。设计合适的变量定义,我们就不需要存储所有可能的结果,直接在不断比较中更新,找到最大值即可。
2)遇见负数即停止,还是等到序列和为负数的时候才停止?如果遇见负数停止,当前和为正数,如果负数后面还是正数,那么序列和仍然有增大的机会。当序列和为负数的时候,且遇见一个负数,更新后的和只会越来越小,于是对和清零,从下一个数开始重新开始求和。
这是我学习后得到的解释,但是事实上我认为还有一些问题并没有得到解决:
1)没能解释如果当前和为负数,后面遇见的都是较大的正数且可以将负数部分补回的可能。此时,将sum置为0是否存在一定差错呢?
2)当sum=0,直接从当前数值的下一个数开始继续寻找子序列,这时候遍历的子序列会与for循环结构的遍历产生重复。

总体来说,把握关键的含义,忽略一些细节考虑,是不容易出错的。

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int result=INT32_MIN;//这里一开始你可能会不太理解为什么
        //但是当你无法通过所有用例测试之后,你就会明白原因了
        int sum=0;
        //对于C++而言,与Python不一样,一定要记得给变量初始化赋值
        for(int i=0;i<nums.size();i++){
            sum+=nums[i];
            if(sum>result)
                result=sum;
            if(sum<=0) sum=0;  

        }
        return result;
    }
};

4. 买卖股票的最佳时机II

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润 。
题目来源:LeetCode

如果你把这道题想成公务员考试里的数量关系题,你是不是感觉轻松了一些呢。找准合适的算法,甚至不需要考虑很多实施的具体细节。从思维角度考察的题目,找准思维架构,就不会出错。
仍然是一道思维难于代码实现的题目,总体尚可。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int result = 0;
        for (int i = 1; i < prices.size(); i++) {
            result += max(prices[i] - prices[i - 1], 0);
            //明白为什么总是和0做比较吗?看清题目要求就知道啦
        }
        return result;
    }
};

5. 跳跃游戏

给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。
题目来源:LeetCode

class Solution {
public:
    bool canJump(vector<int>& nums) {
        int length=0;
        for(int i=0;i<nums.size();i++){
                length +=nums[i];
                if(length>=nums.size()){
                    break;
                    
                }
 
        }
        if(length>=nums.size())
            return true;
        return false;
    }
};
这是我一开始的想法,只能说太鄙陋了。虽然想到要每次取最大覆盖范围,
满足条件了就可以退出,也思考过可能会在数值为0的位置陷落,
但还是缺乏了一些思考。不好评判这个漏洞应该怎么阐述。

学习到的更新优化方式就是,每次仍然会取最大的覆盖范围,但会在当次覆盖范围里找到能在接下来一步跨越最大的步长,用来继续增大实际的覆盖范围。这种方式没有设计每一次必须要如何执行步数跨越,应该说是对我原来想法的一种优化。我们都在找最大覆盖范围,但我宁肯冗余,觉得只要找到就行,但是作者提供的方案可以避免陷落在不该停止的0处。
具体的代码还是比较好理解的,但是容易写错嗷

class Solution {
public:
    bool canJump(vector<int>& nums) {
        int cover = 0;
        if (nums.size() == 1) return true; // 只有一个元素,就是能达到
        for (int i = 0; i <= cover; i++) { // 注意这里是小于等于cover
            cover = max(i + nums[i], cover);
            if (cover >= nums.size() - 1) return true; // 说明可以覆盖到终点了
        }
        return false;
    }
};

6. 跳跃游戏II

给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。

每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:

  • 0 <= j <= nums[i]
  • i + j < n

返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。
题目来源:LeetCode

这道题有点难,但是我感觉思路的出发点其实和题目5我自己想要的解决办法的出发点是相同的。如果这一题先写,然后稍加修改,也可以解决问题5.

class Solution {
public:
    int jump(vector<int>& nums) {
        if (nums.size() == 1) return 0;
        int curDistance = 0;    // 当前覆盖最远距离下标
        int ans = 0;            // 记录走的最大步数
        int nextDistance = 0;   // 下一步覆盖最远距离下标
        for (int i = 0; i < nums.size(); i++) {
            nextDistance = max(nums[i] + i, nextDistance);  // 更新下一步覆盖最远距离下标
            if (i == curDistance) {                         // 遇到当前覆盖最远距离下标
                ans++;                                  // 需要走下一步
                curDistance = nextDistance;             // 更新当前覆盖最远距离下标(相当于加油了)
                if (nextDistance >= nums.size() - 1) break;  // 当前覆盖最远距到达集合终点,不用做ans++操作了,直接结束
            }
        }
        return ans;
    }
};

7. K次取反后最大化的数组和

给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:
选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。
重复这个过程恰好 k 次。可以多次选择同一个下标 i 。
以这种方式修改数组后,返回数组 可能的最大和 。
题目来源:LeetCode

话题补充:
sort函数使用之设定排序的原则
C++ sort函数中cmp()比较函数的写法

一道比较直接的题目,所做即所想就能解决问题。反转的数字可以自选,反转的次数可以计数,做好这两个判别,细化一下可能存在的情况,问题就迎刃而解了。但是我未必能以一种较为简洁的方式写出来。

本来我想奖数组中的元素全部转化为绝对值保存在一个新建的数组变量中,但是这样要和原数组中的元素保持一致有些困难。所以,最终还是和卡哥教的方法一样了。

class Solution {
static bool cmp(int a, int b){
    return abs(a)>abs(b);
}
public:

    int largestSumAfterKNegations(vector<int>& nums, int k) {
        sort(nums.begin(),nums.end(),cmp);//这句的使用要好好积累,真的可以给刻板的思维减少很多麻烦
        //就跟语文写作一样,好的思维方式也是学要学习、记忆和积累的
        for(int i=0;i<nums.size();i++){//这个循环变量i只在循环内有用,跳出后这个变量就不见了
            if(nums[i]<0 && k>0){
                nums[i]=-nums[i];
                k--;
            }
        }
        if(k%2==1) nums[nums.size()-1]=-nums[nums.size()-1];
        //如果所有的负数都已经处理完毕,K值还没有结束,那么需要对绝对值最小的那个数进行处理
        //此时,第一反应还是遍历循环一次次减少,但是我们要善于抓住规律
        //总结较好的编程习惯,构成清晰有逻辑的思维,才是加分项
        int result=0;
        for(int a: nums) result+=a;//又积累一个简练语言的写法
        return result;
    }
};

8. 加油站

在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。

你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。

给定两个整数数组 gas 和 cost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。
题目来源:LeetCode官网

对所有的(gas[i]-cost[i])结果进行累加,如果累加结果大于0,那么说明就一定可以跑完一圈,但是这并不意味着从任何一点都可以跑完,中间如果油量耗尽,那么就无法完成后面的步骤。所以还是要将逻辑整理清楚。

最暴力的解法,对于给定的gas和cost数组,遍历所有的起点方案,找到满足条件的为止。

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        for(int i;i<gas.size();i++){
            int res=gas[i]-cost[i];
            int index=(i+1)%gas.size();
            while(res>0 && index!=i){//如果index==i,那就是回到原点了
            //如果判断res==0的状况,有可能会出现还跑在路上的状况
                res+=gas[index]-cost[index];
                index=(index+1)%gas.size();
            }
            if(res>=0 && index==i){
                return i;
            }
        }
        return -1;
    }
};

但是这个算法没有通过所有的测试用例的,如果输入数组很大的话,太占用时间啦。

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int curSum = 0;
        int min = INT_MAX; // 从起点出发,油箱里的油量最小值
        for (int i = 0; i < gas.size(); i++) {
            int rest = gas[i] - cost[i];
            curSum += rest;
            if (curSum < min) {
                min = curSum;
            }
        }
        if (curSum < 0) return -1;  // 情况1
        if (min >= 0) return 0;     // 情况2
                                    // 情况3
        for (int i = gas.size() - 1; i >= 0; i--) {
            int rest = gas[i] - cost[i];
            min += rest;
            if (min >= 0) {
                return i;
            }
        }
        return -1;
    }
};

四、动态规划

我感觉是状态转移设计的意思,关键是要找出状态转移的规律或者联系。

1. 斐波那契数列

经典题目不再赘述。
以该简单例题说明的问题就是,斐波那契数列中的规律我们可以用递归法解决,也可以看作是三个数一组的状态值转移过程,这时候就是动态规划。如何设计代码解决问题,是个人能力和风格的问题,但同时节省代码空间、考虑到所有的测试用例也是通过代码题的必要条件。

2. 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
题目来源:LeetCode

思路:暴力解法:将这个问题构造成x+2y=n的过程,求一元二次方程的解,大概率过不了所有的测试用例。而且,这个构造并不能正确表示题目含义,pass。

尝试n=1-5的情况,总结方案数,斐波那契数列的翻版咯,略

class Solution {
public:
    int climbStairs(int n) {
        if (n <= 1) return n;
        int dp[3];
        dp[1] = 1;
        dp[2] = 2;
        for (int i = 3; i <= n; i++) {
            int sum = dp[1] + dp[2];
            dp[1] = dp[2];
            dp[2] = sum;
        }
        return dp[2];
    }
};

总结

书到用时方恨少,博主觉得有时间还是多刷题吧。看懂了一遍之后,要学会自己手撕才是最好的。要让算法思维变成一种由内而外的生长,仿佛与生俱来的惯性,才能融会贯通。一定要学会思考!!!

你可能感兴趣的:(C++,学习,笔记,c++)