代码随想录算法训练营 Day62 总结篇 开始的结束

感悟总结

磕磕绊绊跟完了一刷,转眼两个月过去了。感谢有卡哥系统化的课程让学习难度降低不少,也算是领着我到算法世界门口多看了几眼,感谢一路上卡哥的付出 o7
一刷仅仅是开始的结束,要想熟练掌握算法的道还需久久为功,坚持总结归纳直到完全掌握。
一路博客记录下来,如今来看也没白费,哈哈。在这里感谢你的观看,祝你做任何事情都有坚持到底的决心,不要放弃!

  1. 刷题常回顾,不回顾只有短期记忆
  2. 刷题必须要手动推理,才能记忆更牢固,尽可能自己打印输出、手动推理解决 BUG

全题型总结

数组

动态数组通过控制 idx 来实现算法解题,常用方法如下

二分法

确定好区间规范,使用左闭右开就一直使用,边界判断带入特殊值判断

// 核心判断在左在右
while (left < right) {
    
	middle = (left + right) / 2;
	// 根据比较情况更新左右边界
	if(nums[middle] > target) right = middle;
	else if(nums[middle] < target) left = middle + 1;
	else return middle;
}

双指针

同向双指针Ⅰ 快慢指针

无序情况下、本地判断
一快一慢 ∣ − − A − − ↓ s − − ↓ f − − − ∣ |--A--\downarrow_{s}--\downarrow_{f}---| Asf 对 A (快慢指针)操作

// 快指针right遍历,慢指针操作
for (int right = 0; right < N; ++right) {
	if (nums[right] != val) nums[left++] = nums[right];
}
同向双指针Ⅱ 滑动窗口

无序情况下、本地判断
一快一慢 ∣ − − ↓ s − − B − − ↓ f − − − ∣ |--\downarrow_{s}--B--\downarrow_{f}---| sBf 对 B (滑动窗口)操作

for (int right = 0; right < N; ++right) {
	// right - left 窗口操作
	while (Condition) {
	    minLen = std::min(...)
	}
}
对向双指针

有序情况下、对称情况下、缩小规模(夹逼)
指针碰头 if (left <= right)if (left > right)

while(left < right) {
    if (condition1) {
        left++;
    }
    else {
        right--;
    }
}

多指针

常常结合排序,有序后便可以通过多个指针判定数字和、

while (right > left) {
	sum = nums[i] + nums[left] + nums[right];
	if (sum > 0) right--;
	else if (sum < 0) left++;
	else { condition... }

链表

链表定义与操作

链表常用 struct 定义,可以定义构造函数便于初始化

// 单链表
struct ListNode {
    int val;  // 节点上存储的元素
    ListNode *next;  // 指向下一个节点的指针
    ListNode(int x) : val(x), next(NULL) {}  // 节点的构造函数
};
// 操作 想着链表操作写代码
void addAtTail(int val) {
	ListNode* node = new ListNode(val, dummyHead->pre, dummyHead);
	dummyHead->pre->next = node;
	dummyHead->pre = node;
	size++;
}

代码随想录算法训练营 Day62 总结篇 开始的结束_第1张图片

链表操作

建议链表处理都是用虚拟头节点(dummyhead)统一操作,避免处理头节点问题

ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
// 后续对节点的操作都一样

特殊题

环形链表

结论相遇点到入口 == 头到入口

哈希表

快速判断一个元素是否出现集合里。 如果算法有这个要求就可以使用哈希表。
哈希表另一个常用的功能是保障一组元素没有重复

判断元素是否存在

// 自定义哈希表 自定字符查询
for (int i = 0; i < t.length(); ++i) arr[t[i] - 'a']--;
// STL哈希表 查找元素值(可以是单个元素也可以是表达式)
if (hash.find(nums1[i]) != hash.end()) res.insert(nums1[i]);

字符串

类似于数组,但是存储内容为字符

反转

往往采用双指针实现

while (right > left) {
	tmp = s[left];
	s[left] = s[right];
	s[right] = tmp;

	left++;
	right--;
}

栈与队列

基础操作

栈实现队列:两个栈实现队列,出栈容器为空需要一次性将所有入栈容器数据挪入
队列实现栈: 1.两个队列模拟 pop 时将 size-1 元素挪动到第二个队列,剩下的就是出栈元素
2.一个队列模拟:可以将元素取出后再次加入队列从而实现,每次挪动 size-1 元素
括号匹配:入栈另一半括号 ),出栈直接比较与字符串是否相同

特征应用

栈存储操作记录

string str = "";
for (char c : s) {
	if (str.empty() || str.back() != c) str.push_back(c);
	else str.pop_back();
}

逆波兰表达式求值:根据输入入栈求解表达式值
滑动窗口:用队列存储每次操作

二叉树

基础操作

两种遍历方式:递归与迭代

vector<int> inorderTraversal(TreeNode* root) {
    std::vector<int> res;
    std::stack<TreeNode*> stack;
    if (root == nullptr) return res;
    TreeNode* cur = root;
    while (!stack.empty() || cur != nullptr) {
        if (cur != nullptr) {
            stack.push(cur);
            cur = cur->left;
        }
        else {
            cur = stack.top();
            stack.pop();
            res.push_back(cur->val);
            cur = cur->right;
        }
    }
    return res;
}
// 统一迭代法(null标记)(适用于前中后序遍历)
vector<int> inorderTraversal(TreeNode* root) {
    std::vector<int> res;
    std::stack<TreeNode*> stack;
    if (root == nullptr) return res;
    stack.push(root);
    while (!stack.empty()) {
        TreeNode* cur = stack.top();
        if (cur != nullptr) {
            stack.pop();
            // 添加右节点
            if (cur->right) stack.push(cur->right);
            stack.push(cur);
            stack.push(nullptr); // null作为标记说明还没有处理过
            if (cur->left) stack.push(cur->left);
        }
        else {
            // 遇到null标记了说明要处理之后的节点了
            stack.pop();
            cur = stack.top();
            stack.pop();
            res.push_back(cur->val);
        }
    }
    return res;
}

求深度与求高度
求深度,深度是任意节点到根节点距离,高度是任意节点到叶子节点距离。
求高度后续遍历,求深度前序遍历。

回溯

核心模板

回溯的核心是要试一下,试完了再还原回去

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

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

动态规划

核心步骤

状态转移公式(递推公式)是很重要,但动规不仅仅只有递推公式。
对于动态规划问题,我将拆解为如下五步曲,这五步都搞清楚了,才能说把动态规划真的掌握了!

  1. 确定 dp 数组(dp table)以及下标的含义
  2. 确定递推公式
  3. Dp 数组如何初始化
  4. 确定遍历顺序
  5. 举例推导 dp 数组

单调栈

核心模板

单调栈适合求当前元素左边或右边第一个比当前元素大或小的元素,找到元素进而确定数值下标等操作
单调栈指的是:栈内的元素是递增或递减的(从栈口到栈底),单调排列
栈内从栈口到栈底递增:求当前元素后面比他大的元素,遇到更大的元素就弹出之前的元素
只收留更小的元素,保证递增
栈内从栈口到栈底递减:求当前元素后比他小的元素
只收留更大的元素,保证递减
单调栈作用:存放遍历的元素

图论

DFS核心步骤

  1. 确认递归函数的参数与返回值
    一般情况,深搜需要二维数组数组结构保存所有路径,需要一维数组保存单一路径,这种保存结果的数组,我们可以定义一个全局变量,避免让我们的函数参数过多。
  2. 确定终止条件
  3. 处理同层搜索节点的逻辑,遍历当前节点的所有节点做搜索
void DFS(param) {
    if (终止条件) {
        存放结果;
        return;
    }
    
    for (选择本层节点的所有节点用于搜索) {
        节点内容的处理;
        DFS(, 选择的节点);
        回溯,撤销结果
    }
}

BFS 核心步骤

广度优先搜索的代码类似于二叉树的层序遍历

// 代码模板
vector<vector<int>> levelOrder(TreeNode* root) {
    std::queue<TreeNode*> que;
    std::vector<std::vector<int>> res;
    if (root != nullptr) que.push(root);
    
    while (!que.empty()) {
        // 记录每一层的size 控制弹出均为一层一层弹出
        int layerSize = que.size();
        TreeNode* cur;
        std::vector<int> layerRes;
        while (layerSize--) {
            cur = que.front();
            que.pop();
            layerRes.push_back(cur->val);
            if (cur->left != nullptr) que.push(cur->left);
            if (cur->right != nullptr) que.push(cur->right);
        }
        res.push_back(layerRes);
    }
    return res;
}
// 递归法
void Order(TreeNode* cur, std::vector<std::vector<int>>& res, int depth) {
    if (cur == nullptr) return;
    if (depth == res.size()) res.push_back(vector<int>());
    res[depth].push_back(cur->val);
    Order(cur->left, res, depth + 1);
    Order(cur->right, res, depth + 1);
}

vector<vector<int>> levelOrder(TreeNode* root) {
    std::vector<std::vector<int>> res;
    Order(root, res, 0);
    return res;
}

你可能感兴趣的:(算法)