赞
踩
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
二叉树是一种常见的数据结构,其中每个节点最多有两个子节点,通常称为左子节点和右子节点。二叉树具有广泛的应用,如表示树形结构、表达式解析、搜索算法等。
二叉树的遍历是指按照某种顺序访问树中的每一个节点,主要有以下几种方法:
/* 执行流程 1.初始化: current 指向根节点。 2.访问根节点: 将 current 节点的值添加到结果中。 3.处理左右子树: 如果 current 的右子节点不为空,将其压入栈。 如果 current 的左子节点不为空,将其压入栈。 4.继续遍历: 从栈中弹出一个节点,并将其设为 current。 重复步骤 2 和步骤 3,直到栈为空。 */ class Solution { public: vector<int> PreorderTraversal(TreeNode* root) { if (root == nullptr) return; std::stack<TreeNode*> stack; vector<int> res; TreeNode* current = root; stack.push(root); while (!stack.empty()) { current = stack.top(); stack.pop(); res.push_back(current->val); // 将节点值添加到结果中 // 注意这里的顺序:先右后左,这样在栈中处理时,左子树会先于右子树处理 if (current->right != nullptr) { stack.push(current->right); // 将右子节点压入栈 } if (current->left != nullptr) { stack.push(current->left); // 将左子节点压入栈 } } return res; } };
/* 执行流程 1.初始化:current 指向根节点,栈为空。 2.遍历左子树: 内层 while (current != nullptr) 循环将当前节点及其所有左子节点依次压入栈中。 这一步并没有访问任何节点,只是将节点保存起来。 3.访问节点: 当 current 变为 nullptr 时,表示已经到达左子树的尽头。 从栈中弹出一个节点,这个节点是最左的未访问节点。 访问该节点(打印节点值)。 4.遍历右子树: 将 current 移动到刚访问节点的右子节点,重复上述过程 */ class Solution { public: vector<int> inorderTraversal(TreeNode* root) { std::stack<TreeNode*> stack; vector<int> res; TreeNode* current = root; while (current != nullptr || !stack.empty()) { while (current != nullptr) { stack.push(current); current = current->left; // 将左子节点压入栈 } current = stack.top(); stack.pop(); res.push_back(current->val); // 将节点值添加到结果中 current = current->right; // 转向右子树 } return res; } };
/* 这种方式是笔者偷师一位大佬的,属于投机取巧,主要是将前序遍历的修改逻辑并通过反转结果来得到后序遍历的结果 */ class Solution { public: vector<int> postorderTraversal(TreeNode* root) { if (root == nullptr) return; std::stack<TreeNode*> stack; vector<int> res; TreeNode* current = root; stack.push(root); while (!stack.empty()) { current = stack.top(); stack.pop(); res.push_back(current->val); // 将节点值添加到结果中 // 注意这里的顺序:先右后左,这样在栈中处理时,左子树会先于右子树处理 if (current->right != nullptr) { stack.push(current->right); // 将右子节点压入栈 } if (current->left != nullptr) { stack.push(current->left); // 将左子节点压入栈 } } reverse(res.begin(), res.end()); return res; } };
除了上面的迭代法,我们也可以使用递归法。通过调换递归调用和处理当前节点的顺序,可以将前序遍历改写为前序遍历或后序遍历。
// 前序遍历 class Solution { public: vector<int> preorderTraversal(TreeNode* root) { vector<int> res; preorderHelper(root, res); return res; } void preorderHelper(TreeNode* root, vector<int>& res) { if (root == nullptr) return; // 处理当前节点 res.push_back(root->val); // 递归遍历左子树 preorderHelper(root->left, res); // 递归遍历右子树 preorderHelper(root->right, res); } }; // 中序遍历 class Solution { public: vector<int> inorderTraversal(TreeNode* root) { vector<int> res; inorderHelper(root, res); return res; } void inorderHelper(TreeNode* root, vector<int>& res) { if (root == nullptr) return; // 递归遍历左子树 inorderHelper(root->left, res); // 处理当前节点 res.push_back(root->val); // 递归遍历右子树 inorderHelper(root->right, res); } }; // 后序遍历 class Solution { public: vector<int> postorderTraversal(TreeNode* root) { vector<int> res; postorderHelper(root, res); return res; } void postorderHelper(TreeNode* root, vector<int>& res) { if (root == nullptr) return; // 递归遍历左子树 postorderHelper(root->left, res); // 递归遍历右子树 postorderHelper(root->right, res); // 处理当前节点 res.push_back(root->val); } };
与DFS不同,BFS是从一个起始节点开始,首先访问所有相邻的节点,然后逐层向外扩展,依次访问下一层的所有节点。通常使用队列(queue)来实现,常用于最短路径问题、社交网络分析、网络广播、寻找最小生成树等。这里以102.二叉树的层序遍历为例,采用队列的方法,其思路如下:
117.填充每个节点的下一个右侧节点指针II的思路也是如此,不同之处在于需要定义一个包含 next 指针的 Node 类,以便将同一层的节点连接起来。
/** * 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: vector<vector<int>> levelOrder(TreeNode* root) { vector<vector<int>> result; if (root == nullptr) return result; queue<TreeNode*> q; q.push(root); while (!q.empty()) { int levelSize = q.size(); vector<int> currentLevel; for (int i = 0; i < levelSize; ++i) { TreeNode* node = q.front(); q.pop(); currentLevel.push_back(node->val); if (node->left != nullptr) q.push(node->left); if (node->right != nullptr) q.push(node->right); } result.push_back(currentLevel); } return result; } };
在二叉树中我们简单的介绍了利用递归的遍历方法,这里我们详细学习一下递归。简而言之,递归就是函数的不断自我调用。递归是一种强大的编程技术,特别适用于树和图等结构的操作。递归法的使用思路包括以下几个步骤:
这里以101.对称二叉树为例,我们可以简单分析出来要满足对称,要求同时满足任意相对称的节点有相同的左、右子树,且他们本身也相等,这也就是我们的规律。
class Solution {
public:
bool isSymmetric(TreeNode* root) {
if (root == nullptr) return true;
return isMirror(root->left, root->right);
}
bool isMirror(TreeNode* left, TreeNode* right) {
//确定递归的终止条件
if (left == nullptr && right == nullptr) return true;
if (left == nullptr || right == nullptr) return false;
//分解问题:同时判断左、右子树及其本身的值
return (left->val == right->val) && isMirror(left->left, right->right) && isMirror(left->right, right->left);
}
};
我们以105.从前序与中序遍历序列构造二叉树为例,根据前面的知识我们知道前序遍历「根-左子树-右子树」,中序遍历「左子树-根-右子树」,根据题目示例:preorder = [3,9,20,15,7], inorder = [9,3,15,20,7],我们的思路大致如下:
class Solution { public: TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) { if(preorder.empty()) return nullptr; int left_size = 0; for(auto x : inorder){ if(x == preorder[0]) break; left_size++; } vector<int> pre_l(preorder.begin() + 1, preorder.begin() + 1 + left_size); vector<int> pre_r(preorder.begin() + 1 + left_size, preorder.end()); vector<int> in_l(inorder.begin(), inorder.begin() + left_size); vector<int> in_r(inorder.begin() + left_size + 1, inorder.end()); TreeNode *left = buildTree(pre_l, in_l); TreeNode *right = buildTree(pre_r, in_r); TreeNode *newroot = new TreeNode(preorder[0], left, right); return newroot; } };
我们以112.路径总和为例,可以利用递归从 targetSum开始,不断地减去路径上的节点值,如果走到叶子节点发现 targetSum=0,就说明我们找到了一条符合题目要求的路径。
class Solution {
public:
bool hasPathSum(TreeNode* root, int targetSum) {
if (root == nullptr) return false;
targetSum -= root->val;
if (root->left == nullptr && root->right == nullptr) return targetSum == 0;
return hasPathSum(root->left, targetSum) || hasPathSum(root->right, targetSum);
}
};
我们再以108.将有序数组转换为二叉搜索树为例,这里需要知道平衡二叉搜索树的定义( 是指该树所有节点的左右子树的深度相差不超过 1。),那么这个时候我们需要找到数组的中值(这里已经升序排列了),然后对其左右两边不断添加新的节点。
class Solution { public: TreeNode* sortedArrayToBST(vector<int>& nums) { return helper(nums, 0, nums.size() - 1); } TreeNode* helper(vector<int>& nums, int left, int right) { // 终止条件 if (left > right) return nullptr; // 获得中值 int mid = (left + right) / 2; // 创建新节点 TreeNode* root = new TreeNode(nums[mid]); // 进行调用,依次补充节点 root->left = helper(nums, left, mid - 1); root->right = helper(nums, mid + 1, right); return root; } };
我们以236.二叉树的最近公共祖先为例,利用递归在二叉树中自顶向下地查找节点 p 和 q,然后根据找到的位置来确定它们的公共祖先。根据题目要求,我们可以得出下图所示的几种情况:
初看可能会感觉条件判断有点复杂,我们来一条条看:
本质上是在自底向上查找,逐步确定最低公共祖先的条件。如果左右子树分别找到了 p 和 q,当前节点就是 LCA;如果都在一个子树中,则继续返回那一侧的结果;如果都没找到,则返回 nullptr。值得注意的是,在递归过程中返回值可能是空节点、节点 p、节点 q ,对于最外层的递归调用者来说,返回值是最近公共祖先。
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (root == nullptr || root == p || root == q) return root;
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
if(left != nullptr && right != nullptr) return root;
else if(left != nullptr) return left;
else return right;
}
};
二叉搜索树(Binary Search Tree, BST)是二叉树的一种特殊形式,但它满足特定的顺序性质:
class Solution { public: vector<int> res; int kthSmallest(TreeNode* root, int k) { inorderHelper(root, res); return res[k - 1]; } void inorderHelper(TreeNode* root, vector<int>& res){ if (root == nullptr) return; inorderHelper(root -> left,res); res.push_back(root -> val); inorderHelper(root -> right,res); } };
所谓元素映射就是将一组元素映射到另一组元素上,建立一一对应关系。这里我们需要先简单了解一下映射的相关定义:
简单来说,单射就是一个集合中两两不同的元素在另一个集合中的对应也两两不同。满射就是另一个对应的集合所有元素都是由一个集合映射过去的我们以290.单词规律为例,我们这里通过维护两个哈希表,分别用于记录 pattern 中的字母与 s 中单词之间的映射关系,另一个用于记录 s 中单词与 pattern 中字母之间的映射关系,这样可以确保双向连接的对应规律。
class Solution { public: bool wordPattern(string pattern, string s) { unordered_map<char, string> p2s; // pattern to string map unordered_map<string, char> s2p; // string to pattern map /* 这里需要着重了解一下 stringstream 的用法,它的作用是从字符串中读取数据 */ /* 容许使用标准的输入运算符(>>)来提取数据,按需要的格式读取字符串中的内容。*/ vector<string> words; string word; stringstream ss(s); // 将字符串 s 按空格拆分成单词存入 words 向量中 while (ss >> word) { words.push_back(word); } // 如果 pattern 长度和 words 长度不一致,直接返回 false if (pattern.size() != words.size()) return false; for (int i = 0; i < pattern.size(); i++) { char p = pattern[i]; string w = words[i]; // 检查 pattern -> string 映射是否一致 if (p2s.count(p)) { if (p2s[p] != w) return false; } else { p2s[p] = w; } // 检查 string -> pattern 映射是否一致 if (s2p.count(w)) { if (s2p[w] != p) return false; } else { s2p[w] = p; } } return true; } };
字母异位词(Anagram)是指由相同字符组成但排列顺序不同的字符串。例如:“listen” 和 “silent” 是字母异位词,因为它们包含的字符完全相同,只是顺序不同。我们这里以49.字母异位词分组为例,它的大体思路为先遍历字符串数组,再利用sort进行升序排列(这是为了将字母异位词统一到一个key下,例如:eat,ate,tea)并分组,之后将哈希表的各个分组输出出来即可。
class Solution { public: vector<vector<string>> groupAnagrams(vector<string>& strs) { unordered_map<string, vector<string>> hashmap; // 遍历字符串数组 for (const auto& x : strs) { string key = x; sort(key.begin(), key.end()); // 将字符串排序 hashmap[key].push_back(x); // 将原始字符串加入到排序后键对应的向量中 } vector<vector<string>> res; // 遍历哈希表,将每个字母异位词组加入结果中 for (const auto& t : hashmap) { res.push_back(t.second); } return res; } };
我们以题155.最小栈为例,这道题看起来复杂,其实只需要构建一个辅助栈(最小栈,来存储当前最小值)即可,唯一要注意的就是再void push(int val);
函数中,我们需要将当前值和最小栈顶元素进行对比,然后将小的一方压入最小栈的栈顶。可能你会疑问如果 st 的栈顶元素不是最小值,为什么 min_st 还要弹出?这是因为当你执行 push(val) 时,min_st 会将当前最小值 min(min_st.top(), val) 压入栈顶,所以min_st 的栈顶元素始终和 st 的栈顶元素是一一对应的。例如st = { 0,1,1,2},则此时min_st = { 0,0,0,0},所以在pop中弹出最小栈的元素时不会影响实际的最小值。
class MinStack { public: stack<int> st; stack<int> min_st; MinStack() { min_st.push(INT_MAX); } void push(int val) { st.push(val); min_st.push(min(min_st.top(), val)); } void pop() { st.pop(); min_st.pop(); } int top() { return st.top(); } int getMin() { return min_st.top(); } };
栈的相关内容我们还是比较了解了,主要是一个“先入后出”的概念,我们这里以150.逆波兰表达式求值为例。逆波兰表达式严格遵循「从左到右」的运算。计算逆波兰表达式的值时,使用一个栈存储操作数,从左到右遍历逆波兰表达式,如果遇到操作数,则将操作数入栈;如果遇到运算符,则将两个操作数出栈,其中先出栈的是右操作数,后出栈的是左操作数,使用运算符对两个操作数进行运算,将运算得到的新操作数入栈。
class Solution { public: int evalRPN(vector<string>& tokens) { stack<int> st; for(int i = 0; i < tokens.size(); i++){ if (tokens[i] == "+" || tokens[i] == "-" || tokens[i] == "*" || tokens[i] == "/") { int num1 = st.top(); st.pop(); int num2 = st.top(); st.pop(); if (tokens[i] == "+") st.push(num2 + num1); if (tokens[i] == "-") st.push(num2 - num1); if (tokens[i] == "*") st.push(num2 * num1); if (tokens[i] == "/") st.push(num2 / num1); } else st.push(tokens[i]); } int res = st.top(); return res; } };
单调栈(Monotonic Stack)是一种特殊类型的栈,它在操作过程中保持一定的单调性,这与普通的栈相比,增加了一个维护顺序的约束。我们先以739.每日温度为例,简单学习一下单调栈的性质。以下是本题的简单思路:
class Solution { public: vector<int> dailyTemperatures(vector<int>& temperatures) { int n = temperatures.size(); stack<int> st; // 递增栈 vector<int> result(n, 0); for (int i = 0; i < n; i++) { while (!st.empty() && temperatures[i] > temperatures[st.top()]) { // 注意栈不能为空 result[st.top()] = i - st.top(); st.pop(); } st.push(i); } return result; } };
我们以96.反转链表II为例,我们需要先知道反转的过程是怎样的,其思路大致如下:
class Solution { public: ListNode* reverseBetween(ListNode* head, int left, int right) { if (head == nullptr || left == right) return head; // 创建一个哨兵节点,便于处理头部边界 ListNode* dummy = new ListNode(0); dummy->next = head; ListNode* prev = dummy; // 移动 prev 到 left 的前一个节点 for (int i = 1; i < left; ++i) { prev = prev->next; } // current 是要反转的子链表的第一个节点 ListNode* current = prev->next; ListNode* next = nullptr; ListNode* pre = nullptr; // 反转子链表 for (int i = 0; i <= right - left; ++i) { next = current->next; // 1. 存储 current 的下一节点 current->next = pre; // 2. 将 current->next 指向 pre 节点 pre = current; // 3. 将 pre 更新为 current current = next; // 4. 将 current 更新为 next } // 连接反转后的链表部分 prev->next->next = current; // 将反转后的子链表尾部与 right 后的部分连接 prev->next = pre; // 将 prev 连接到反转后的链表头部 return dummy->next; // 返回新的头节点 } };
选择链表的主要思路就是先形成环再断开的过程,我们以61.旋转链表为例,其思路大致如下:
class Solution { public: ListNode* rotateRight(ListNode* head, int k) { if (k == 0 || head == nullptr || head->next == nullptr) return head; // 计算链表长度 int n = 1; // 起始为1,因为我们从head节点开始 ListNode* current = head; while (current->next != nullptr) { current = current->next; n++; } // 形成一个环 current->next = head; // 计算新的尾节点的位置 int offset = n - k % n; for (int i = 0; i < offset; i++) { current = current->next; } // 新的头节点是新尾节点的下一个节点 ListNode* newHead = current->next; // 断开环 current->next = nullptr; return newHead; } };
删除节点的过程就很简单了,以19.删除链表的倒数第N个节点为例,我们只需要先确定节点的总数,然后到达指定节点并修改其指向的下一节点,跳过要删除的节点即可。
/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode() : val(0), next(nullptr) {} * ListNode(int x) : val(x), next(nullptr) {} * ListNode(int x, ListNode *next) : val(x), next(next) {} * }; */ class Solution { public: ListNode* removeNthFromEnd(ListNode* head, int n) { if (head == nullptr) return head; // 计算链表长度 ListNode* p = head; int index = 0; while (p != nullptr) { p = p->next; index++; } ListNode* dummy = new ListNode(0); dummy->next = head; ListNode* current = dummy; for(int i = 0; i < index - n; i++){ current = current -> next; } current -> next = current -> next -> next; return dummy->next; } };
图是一种非线性数据结构,由节点(或称顶点,Vertices,图中的基本元素,用来表示对象或位置。) 和 边(Edges,连接两个节点的线,表示节点之间的关系或路径。)组成。它可以用于表示各种复杂的关系,比如社交网络中的朋友关系、交通网络中的路径、互联网中的连接等等。图的种类有以下4种:
这些问题可以被映射到图论中的连通性问题,通常使用深度优先搜索(DFS)或广度优先搜索(BFS)来解决。以题200.岛屿数量为例,这里是是构建了一个“传染辅助函数”,如果周围有陆地则进行“传染”并打上标记,最后再进行双循环遍历每一个地址的情况进行统计。
class Solution { public: int numIslands(vector<vector<char>>& grid) { int r = grid.size(); // 行数 if (r == 0) return 0; int c = grid[0].size(); // 列数 int count = 0; for (int i = 0; i < r; i++) { // 使用 i 来遍历行 for (int j = 0; j < c; j++) { // 使用 j 来遍历列 if (grid[i][j] == '1') { count++; dfs(grid, i, j); // 发现一个岛屿,开始深度优先搜索 } } } return count; } private: void dfs(vector<vector<char>>& grid, int r, int c) { int m = grid.size(); // 行数 int n = grid[0].size(); // 列数 // 标记当前土地为 '0',表示已经访问过 grid[r][c] = '0'; // 向上、下、左、右四个方向继续搜索 if (r - 1 >= 0 && grid[r - 1][c] == '1') dfs(grid, r - 1, c); // 向上检索 if (r + 1 < m && grid[r + 1][c] == '1') dfs(grid, r + 1, c); // 向下检索 if (c - 1 >= 0 && grid[r][c - 1] == '1') dfs(grid, r, c - 1); // 向左检索 if (c + 1 < n && grid[r][c + 1] == '1') dfs(grid, r, c + 1); // 向右检索 } };
字典树是一种树形数据结构,用于存储动态集合或数组中的字符串,每个节点代表字符串中的一个字符。每个节点可以有多个子节点,子节点的数量取决于字母表的大小(例如,英文的字典树有 26 个子节点)。通常被用于实现快速前缀查找、字符串搜索、单词自动补全、拼写检查等功能。字典树的主要特点如下:
//Trie 的结构
struct TreeNode {
VALUETYPE value; //结点值
TreeNode* children[NUM]; //指向孩子结点
};
以208.实现Trie(前缀树)为例,其核心在于将字符串拆分,在每一级子节点中进行查找是否含有当前字母,当我们存储的时候会给单词结尾的节点打上标记,表示已经存储了该单词。其具体思路如下:
class Trie { private: // 定义TrieNode节点 struct TrieNode { bool isEnd; // 标记是否是一个单词的结尾 unordered_map<char, TrieNode*> children; // 使用哈希表来存储子节点 }; TrieNode* root; // 根节点 public: // 初始化前缀树对象 Trie() { root = new TrieNode(); } // 向前缀树中插入字符串 word void insert(string word) { TrieNode* node = root; for (char c : word) { if (node->children.find(c) == node->children.end()) { node->children[c] = new TrieNode(); } node = node->children[c]; } node->isEnd = true; // 标记单词结束 } // 检查字符串 word 是否在前缀树中 bool search(string word) { TrieNode* node = root; for (char c : word) { if (node->children.find(c) == node->children.end()) { return false; } node = node->children[c]; } return node->isEnd; } // 检查是否存在以 prefix 为前缀的单词 bool startsWith(string prefix) { TrieNode* node = root; for (char c : prefix) { if (node->children.find(c) == node->children.end()) { return false; } node = node->children[c]; } return true; } };
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。