其实可以不用那么c++

本文探讨了编程语言的本质,强调了优雅代码的重要性。通过实例分析了滥用命名空间、文件命名习惯和头文件引用习惯的问题,指出代码易于理解的首要性,并对比了标准库与实际开发中的高效工具,提出避免过度依赖复杂库的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

所谓编程语言本质也是一门语言,目的是表达自己的思想,虽然目标是机器而不是人,但是相信我,会真正看语言本身的终究还是人。所谓优雅的代码就是严谨的表达出自己的思想,并且易于维护的。如果你写一篇文章满是之乎者也,但是别人都不懂,那么即便你知道“茴”字的六种写法又如何?会有人觉得你是国学大师吗?在我看来代码易于理解比什么都重要,只有代码容易理解了,之后维护和改bug也才容易,后期他人维护起来也不会捅什么娄子。而性能上优势绝不是体现在一两处语法上面,而是更加宏观的层面上。


例子1:滥用命名空间

c++引入命名空间无可厚非,正常使用也可以清爽不少。比如cocos2d-x,如果你的代码中没有与之冲突的名称,那么可以在实现文件中放心的using namespace cocos2d,如果有的话,也可以使用namespace cc = cocos2d来简化代码。这都是很好很好的。但是有些人看了几篇c++的教程就滥用命名空间,一个不大的项目中N个命名空间,更可怕的是还在头文件中using namespace,在命名空间内部使用class xxx;这样的类生命。这样的问题,一是人肯定看不懂,同样是Font,存在于几个命名空间中,鬼才知道哪个对哪个。二十编译器也可能看不懂,即便恰好是gcc编译过了,vs也不见得编译过,即便都能编译过,那稍微修改下工程合并一下项目文件也会出乱子。写代码而已,何必给自己找麻烦呢?加个前缀会恶心到哪里去?

我的观点是,可以使用命名空间,但是一个项目(比如Ogre cocos2d-x这样的规模)最多一两个,并且不要在自己的项目中还靠命名空间来区分类。命名空间的存在只是相当于对自己的代码的一个保护和封装,别人使用时可以方便的拿过来用,而不会产生一些恶心的全局变量或者函数的冲突。


例子2:项目中文件命名习惯和头文件引用习惯的问题

很多人命名文件时不喜欢加项目标志和文件夹标志,这样最大的问题就是会造成很多同名文件,无论是自己项目中,还是和其他项目都可能引发冲突。比如在render文件夹下有一个util.cpp util.h,在network文件夹下也有一个util.cpp util.h,那么这两个文件必须存在于不同的工程中,否则编译出的obj文件会冲突,链接器会自动忽略其中之一。vs在编译文件时是不会带着文件夹路径的。

还是那句话,加上文件路径标志会恶心吗?我反而觉得这样是恶心的,比如va中一搜索util,出来三五个util.cpp,不自己看看还真不知道要找的是哪个,不如直接加上render_util.cpp,network_util.cpp,这样来的直观。希望cocos2d-x不会那天抽疯了把CC的文件夹前缀也给去掉了。

头文件也是如此。过短的命名造成的后果就是设置头文件查找路径的时候要格外小心,否则很容易调用到错误的文件上。这里我认为,除开特殊情况(比如平台文件不同),否则项目中的头文件包含都要是相对于一个固定路径的完整路径。


例子3:奉stream为圭臬

看过几本c++教程的,都知道标准库中的stringstream iostream等等,当然是更加c++,更加可扩展,更加类型安全等等。但是在我看来那些都是玩具,又难用,又没有必要用。fopen什么的要比iostream好用多了,printf也要比iostream漂亮多了,要多反人类才会喜欢这样的打log的写法啊(stream << "正在打第" << i << "关" << "玩家" << id << endl)。同样的sscanf snprintf也要比stringstream实用强大。用标准库能写一两百行的代码,其实一个vsprintf就搞定了。至于类型安全,如果我需要靠虑类型安全我也会使用boost::format,就像我绝对不会实用bind1st一样,我也不会使用这些东西。c++标准库看着能将一本600页的书,其实学会vector map等容器的正确用法和相关算法就足够了,我相信现在还没有那本讲标准库的教程里面会有bind function thread shared_ptr 这些东西,但是如果你学习了stream却又不知道bind,那么就是买椟还珠了。


例子4:待续。。。。


<think>嗯,用户想在C++中实现子树点权增加的操作,但不使用DFS序。我之前提到的方法是基于DFS序和差分数组,但用户现在希望不用DFS序,所以需要寻找其他方法。首先,我得回忆一下树的各种操作有哪些替代方案。常见的子树操作除了DFS序,还有树链剖分、父指针表示法、或者使用每个节点记录子节点列表直接遍历。不过直接遍历的话,时间复杂度可能较高,尤其是当子树很大时,每次操作都要遍历整个子树,效率不够好。用户可能希望更高效的方法,比如O(1)或O(logn)的时间复杂度。 那有没有数据结构可以在不依赖DFS序的情况下高效处理子树更新呢?比如使用链式前向星或者其他树的表示方法,结合某种区间标记?或者考虑使用树状数组结合欧拉序?不过欧拉序其实和DFS序类似,可能不符合用户的要求。 另一种思路是使用每个节点维护其子节点的信息,比如使用父节点指针,这样可以通过递归或迭代的方式访问所有子节点。但这种方法的时间复杂度是O(k),k是子树节点数,对于频繁操作可能不够高效。 或者,考虑使用树的结构特性,比如将子树信息存储在节点中,每次更新时直接操作父节点,然后通过某种方式传递到子节点。例如,类似于并查集的路径压缩,但可能不太适用。突然想到,如果使用层次遍历(BFS)来遍历子树,这样也可以访问所有子节点,但同样需要遍历整个子树,时间复杂度还是O(k)。不过用户可能可以接受,如果子树操作不频繁的话。 或者,有没有可能使用动态树结构,比如Link-CutTree,但这可能过于复杂,用户可能需要更简单的方法。再想想,另一种方法是使用每个节点存储其所有子节点,比如使用邻接表来记录子节点。这样,当需要更新子树时,从根节点x开始,递归地遍历所有子节点并更新权值。这种方法虽然直接,但时间复杂度是线性的,对于每个操作是O(k),如果操作次数多的话,总时间会很高。用户可能需要更高效的方法,比如O(1)或O(logn)的时间复杂度。这时候可能需要引入树状结构的高级算法,比如树的重链剖分(Heavy-LightDecomposition),但树剖通常用于路径查询,子树查询可能需要特定的处理。 或者,考虑使用前缀和或延迟传播的技术。例如,在节点中维护一个标记,表示该节点及其子树的增量,当需要查询某个节点的值时,递归地累加所有祖先节点的标记。这种方法类似于线段树的延迟标记,但需要设计合适的数据结构来支持。 具体来说,可以为每个节点维护一个add标记,当对以x为根的子树增加a时,只需要在x节点的add标记上加上a。当查询某个节点u的值时,需要从u向上遍历到根,累加路径上的所有add标记。这样,更新时间复杂度是O(1),查询是O(depth),depth是节点深度。如果树比较平衡,比如二叉树,深度是O(logn),但最坏情况下是链状结构,深度是O(n),查询会变慢。不过,如果用户的需求是批量更新子树,而查询相对较少,这种方法可能有效。或者,如果用户需要频繁查询,可能需要结合路径压缩或其他优化方法。 另外,可以参考并查集的结构,但并查集主要用于集合合并,可能不太直接适用。再回到问题,用户明确要求不使用DFS序,所以需要避开基于时间戳的方法。可能的解决方案包括:1.父指针法:每个节点记录父节点,递归遍历子节点。 2.延迟标记法:在节点上存储增量,查询时累积。 3.树状结构维护子树集合:比如使用哈希表或数组记录每个节点的子节点,直接遍历。现在,我需要具体考虑如何在C++中实现这些方法。例如,延迟标记法:每个节点有一个add变量,当对子树x进行加a操作时,x.add+= a。当需要获取节点u的实际权值时,计算从u到根路径上所有add值的总和,加上该节点的原始权值。但这种方法需要快速访问祖先链,可能比较慢,尤其是在深树中。 或者,可以结合树的结构,将延迟标记下推。例如,当访问某个节点时,将其add值传递给子节点,并清空自己的add。这样可以保证查询时权值的正确性,但实现起来可能复杂,需要维护更多的状态。 另一种方法是使用链式结构,每个节点保存子节点列表,当需要更新子树时,使用队列或栈进行广度优先或深度优先遍历,逐一更新每个子节点的权值。这种方法虽然直接,但时间复杂度是O(k),适用于子树较小的情况。例如,使用BFS遍历子树:voidupdateSubtree(Node* root, inta){queue<Node*> q;q.push(root);while(!q.empty()){Node*current =q.front();q.pop();current->value+= a;for (autochild :current->children){q.push(child);}}} 但这种方法每次操作需要遍历整个子树,时间复杂度较高。用户可能需要更高效的方法,尤其是在处理大量子树更新时。因此,可能需要寻找类似线段树的结构,但针对树形结构设计。 或许,可以将树转换为二叉树,然后使用类似于线段树的结构,但转换可能复杂。或者,使用树的重链剖分,将子树分解为若干链,每个链用线段树或树状数组管理。但这种方法可能仍然需要某种形式的DFS序来进行链的划分,可能不符合用户要求。 另一种思路是,利用每个节点在树中的位置,为每个节点维护一个子树大小,然后通过某种数学计算来确定子树的节点范围,但不知道这是否可行。例如,如果树的结构是静态的,可以预先为每个节点x计算其子树的起始和结束索引,类似于DFS序,但用户明确不使用DFS序,所以这种方法不可行。可能需要使用其他树的结构表示,比如左儿子右兄弟表示法,将多叉树转化为二叉树,然后进行操作。例如,每个节点的第一个子节点作为左孩子,兄弟节点作为右孩子。这样,子树可能由左孩子链组成,但遍历仍然需要O(k)时间。或者,使用层次化的编号,比如每个节点的编号包含其层级信息,但不确定如何应用。 回到问题,用户需要不使用DFS序的方法,所以必须另辟蹊径。可能的方法有: 1.使用父指针递归遍历所有子节点,逐一更新权值。时间复杂度O(k),适用于子树较小的情况。 2.使用延迟标记,在父节点记录增量,查询时累加。时间复杂度O(1)更新,O(depth)查询。3.使用树状结构的高级分解方法,如链式剖分,但可能复杂。现在需要为C++实现这些方法。例如,延迟标记的实现: structNode {intvalue;intadd;vector<Node*>children;Node* parent; };当对子树x增加a时,执行x->add +=a; 查询节点u的值时,计算从u到根路径上的所有add之和,加上u->value。但这种方法在查询时需要遍历到根,可能较慢。如果用户需要频繁查询,这可能不适用。另一种优化是,在更新时,将增量记录在x节点,当查询时,只需要访问x的祖先直到根,或者使用路径压缩技术来加速查询,类似于并查集。或者,使用每个节点的权值直接存储,并在更新时传播增量到所有子节点,但这样每次更新需要O(k)时间。综合来看,可能用户需要的是时间上更高效的方法,而不仅仅是实现方式的不同。因此,可能需要使用链式前向星结合某种区间标记,或者树状数组的扩展方法。 但如果没有DFS序,可能需要其他方式将子树映射到线性结构。例如,使用BFS序,但BFS序的子树可能不连续,无法形成区间,所以差分数组无法应用。或者,使用树的括号表示法(如LISP表达式中的括号结构),记录每个节点的起始和结束位置,但这也类似于DFS序的思想,可能不符合用户要求。或者,考虑每个节点维护一个子树节点列表,这样更新时直接遍历该列表。但这样需要额外的存储空间,每个节点保存其所有后代节点,这在空间上不可行,尤其是对于大型树。那么,有没有其他数据结构可以在树上支持子树的高效更新?例如,使用Fenwick Tree(树状数组)或SegmentTree,但需要将树的结构线性化,这又回到DFS序或类似的方法,可能不符合用户的要求。 或许,用户希望的是另一种线性化方法,比如通过父节点链或其他方式,但暂时没有想到。可能,用户的问题需要在没有预处理的情况下实时处理子树更新,这时候只能使用直接的遍历方法,如BFS或DFS遍历子树节点,逐一更新权值。虽然时间复杂度是O(k),但实现简单,适用于不频繁的操作。例如,在C++中,可以使用邻接表存储树的结构,每个节点保存其子节点列表。然后,当需要更新以x为根的子树时,使用队列进行BFS遍历:voidaddSubtree(Node* root, inta){queue<Node*> q;q.push(root);while(!q.empty()){Node*current =q.front();q.pop();current->value +=a;for(Node* child: current->children) {if(child!= current->parent) {//避免回溯到父节点q.push(child);}}} }但需要注意,树是无向的,所以在遍历时需要避免回到父节点。因此,每个节点需要记录父节点,或者在遍历时跳过父节点。 例如,在树的表示中,每个节点保存子节点列表,不包括父节点,这样遍历时无需检查。或者,在构建树的时候,将父节点信息排除在子节点列表之外,确保遍历时不会回溯。 这样,每次更新子树的时间复杂度是O(k),k是子树节点数。这种方法虽然简单,但效率取决于子树的大小。如果子树很大,或者操作频繁,这种方法可能不够高效。 用户可能需要更高效的方法,但如果没有使用DFS序或其他线性化方法,可能难以实现更优的时间复杂度。因此,可能需要权衡,接受O(k)的时间复杂度,或者寻找其他思路。 另一种可能的方法是使用并查集结构,将子树视为一个集合,但并查集主要用于合并和查找,难以直接支持权值的批量增加。 或者,使用每个节点维护一个指向子树根的指针,但不确定如何应用。 综上所述,如果不使用DFS序,直接的遍历方法(BFS或DFS)是可行的,但时间复杂度较高。如果用户能接受这一点,那么实现起来比较简单。否则,可能需要更复杂的数据结构,如结合延迟标记或其他高级树结构。因此,在C++中,可以给出两种方法:一种是直接遍历子树的BFS/DFS实现,另一种是使用延迟标记的方法,虽然查询时需要额外处理,但更新时间复杂度低。现在,我需要整理这些思路,给出具体的实现示例,并比较优缺点。</think>### 不使用 DFS 序的子树点权增加实现方法 在树结构中实现子树节点权值批量增加操作,若不依赖 DFS 序,可以通过以下两种方法实现: --- #### 方法 1:直接递归遍历子树 通过维护父节点指针避免回溯,递归遍历目标子树的所有节点进行权值修改。时间复杂度为 $O(k)$,其中 $k$ 为子树节点数。 ```cpp #include <iostream> #include <vector> using namespace std; struct TreeNode { int val; vector<TreeNode*> children; // 子节点列表(不包含父节点) }; void add_subtree(TreeNode* root, int a) { if (!root) return; root->val += a; // 修改当前节点权值 for (auto child : root->children) { add_subtree(child, a); // 递归修改子节点权值 } } ``` **优点**:实现简单直观,无需预处理。 **缺点**:每次操作需要遍历所有子树节点,时间复杂度较高[^3]。 --- #### 方法 2:延迟标记法 通过为每个节点维护增量标记,在查询时动态计算实际权值。更新时间复杂度为 $O(1)$,查询时间复杂度为 $O(d)$($d$ 为节点深度)。 ```cpp struct TreeNode { int base_val; // 原始权值 int add_tag; // 延迟标记 TreeNode* parent; }; // 对以x为根的子树增加a void add_subtree(TreeNode* x, int a) { x->add_tag += a; // 仅更新根节点的标记 } // 查询节点u的实际权值 int get_value(TreeNode* u) { int sum = u->base_val; TreeNode* current = u; while (current) { sum += current->add_tag; // 累加路径上的所有标记 current = current->parent; } return sum; } ``` **优点**:更新操作高效,适用于频繁修改场景。 **缺点**:查询效率依赖树的高度,需要额外空间存储父指针[^2]。 --- ### 方法对比 | 方法 | 更新时间复杂度 | 查询时间复杂度 | 适用场景 | |--------------|----------------|----------------|------------------------| | 直接遍历法 | $O(k)$ | $O(1)$ | 子树较小或低频修改 | | 延迟标记法 | $O(1)$ | $O(d)$ | 高频修改、低频查询 | --- #### 树结构构建示例(C++) ```cpp TreeNode* build_tree() { // 示例树结构:1为根,子节点为2和3,2的子节点为4和5 TreeNode* node1 = new TreeNode{0, {}}; TreeNode* node2 = new TreeNode{0, {}}; TreeNode* node3 = new TreeNode{0, {}}; TreeNode* node4 = new TreeNode{0, {}}; TreeNode* node5 = new TreeNode{0, {}}; node1->children = {node2, node3}; node2->children = {node4, node5}; return node1; } // 示例操作:对节点2的子树增加5 int main() { TreeNode* root = build_tree(); add_subtree(root->children[0], 5); // 修改节点2的子树 return 0; } ``` --- ### 核心问题分析 1. **直接遍历法**通过递归或迭代访问所有子节点实现权值修改,适合对性能要求不高的场景[^3]。 2. **延迟标记法**通过标记传递减少实际修改次数,但需要设计合理的标记下推机制来保证查询正确性[^4]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值