之前上C++/C#课上老师讲过这个问题,只不过当时主要是跟着老师的节奏与情形,从理论上基本了解了其原理。不过当自己写代码的时候,还是遇到了这个非常坑的问题。因此再来分析一下。
今天第一次做LeetCode,对这种指针的代码填空题个人感觉还是很有挑战性的。(作为一个数据结构课几乎很少用指针写代码、全是数组流的后遗症)
题目的大意很简单,就是给定一个二叉排序树(BST, binary search tree),再给定一个区间[l,r],要我们把在其中区间里的树给抠出来返回。
由于BST是给定的,我们显然只要递归进行求解就好了。递归思路很清晰的话,代码很短,5行左右就搞定(见本文最后)。我的解法比较麻烦,而且对指针的不熟悉踩了不少坑。因此慢慢填。
先写几点非常基础的原则(写的时候怎么还是这么容易掉坑)
函数里定义的局部变量是放在栈上的,函数结束后会自动释放。而malloc的空间是放在堆上的,除非程序要手动释放不会消失。
c++的指针变量需要首先赋值,否则会变成野指针。类似这种应该都报错,因为
任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。
然后回到题目标题的最大的坑。
先po一些基础概念。
所谓浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。大多情况下“浅拷贝”已经能很好地工作了,但是一旦对象存在了动态成员,那么浅拷贝就会出问题了。
在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间.
有几个需要注意的点,一是对于指针变量的初始化。TreeNode *lll = (TreeNode *)(sizeof(TreeNode));
这么写的原因是因为这样分配不会随着我们函数的结束这段空间被释放,因为我们结束后可能还会需要这段内存的值,第二:我一开始是这么写的 TreeNode tthh(0);
TreeNode* res = &tthh;
这么写是不可取的。直接将指针对象知道了一个新初始的TreeNode上,临时变量指的那个空间里的值,一出当前函数、这个值可能就从正确的变成了无穷,应该是c++某种释放机制。反正这样写在LeetCode上交的结果就是reference binding to misaligned address 0x7faf0d005d32 for type 'const int', which requires 4 byte alignment
的RunTime Error。
我们每次把lll或者rrr传到下一层递归的ans指针的目的是什么呢,是要其指向的值是正确的子树的结果。而如果此时我们只是做一句ans=lll;
它的意义是什么呢,这显然是一个浅拷贝,只把lll的值也就是指向的地址给了ans,至于里面的值呢,在当前的这一层中我们指向和lll相同的地方,无可厚非。不过一返回到上层就GG。举个例子,如下图:
比如在做到3节点的时候,我们分配给lll的地址为A,我们进入到左节点0的时候显然ans指向的地址也是A,我们在0的时候会得到ans=rrr;的一个新地址B,指向2->1这样的一棵子树。目前看没有问题域。但是!当从0这层返回到上层的时候,我们在3中的lll的地址还会变成我们想要的B吗?显然不会,因为我们知道,在C++中参数的传递方式是值传递,所以这个地址还会又变成A,然后此时我们就还是会指向了一片虚无的地方,然后就错了。因此避免的方法就是,我们在进行每次进行复制的时候进行一次深拷贝,因此就看到那三行的拷贝内容。也许,你又要问了,那为什么上面的ans->left = lll;
可以直接进行浅拷贝呢,那是因为我们是直接对ans的内容进行了改变,因此函数结束后其内容仍然改变了。当然进行深拷贝也行,只要记住不要往NULL里面写东西。即ans->left = (TreeNode*)(malloc(sizeof(TreeNode)));
ans->left->val = lll->val;
ans->left->left = lll->left;
ans->left->right = lll->right;
最终我AC的代码如下:
class Solution {
public:
bool dfs(TreeNode* root, int L, int R, TreeNode * ans){//浅拷贝,指针引用!
ans->left = ans->right = NULL;
bool zy = false;
bool h;
if (root->val >= L && root->val <= R){
ans->val = root->val;
zy = true;
}
if (root->left != NULL){
TreeNode* lll = (TreeNode*)(malloc(sizeof(TreeNode)));
lll->left = lll->right = NULL;//指针初始化,拒绝野指针
h = dfs(root->left, L, R, lll);
if (h){//判断坐标子树上有没有东西,有东西的话就把
if (root->val >= L && root->val <= R){
//ans->left = (TreeNode*)(malloc(sizeof(TreeNode)));
ans->left = lll;
}
else{
//ans = lll;
ans->val = lll->val;
ans->left = lll->left;
ans->right = lll->right;
}
}
zy |= h;
}
if (root->right){
TreeNode* rrr = (TreeNode*)(malloc(sizeof(TreeNode)));
rrr->left = rrr->right = NULL;
h = dfs(root->right, L, R, rrr);
if (h){
if (root->val >= L && root->val <= R){
//ans->right = (TreeNode*)(malloc(sizeof(TreeNode)));
ans->right = rrr;
}
else{
ans->val = rrr->val;
ans->left = rrr->left;
ans->right = rrr->right;
}
}
zy |= h;
}
return zy;
}
TreeNode* trimBST(TreeNode* root, int L, int R) {
cout << sizeof(TreeNode) << endl;
TreeNode* res = (TreeNode*)(malloc(sizeof(TreeNode)));
res->left = res->right = NULL;//指针初始化,拒绝野指针
dfs(root, L, R, res);
return res;
}
};
虽然过了,但我也进行了新的思考,能否就浅拷贝不复制其内容呢?显然也是可以的,只需要我们传递的是指针的引用就行了。改一下函数声明即可:
bool dfs(TreeNode* root, int L, int R, TreeNode *& ans)
到了这个程度,我想我对于这道题的理解也就告一段落。其实这道题比较好的思路没有这么绕,只需要我们直接在原来的这颗树上进行删减就好 。删减的原则如下:
- 当root的值位于L和R之间,则递归修剪其左右子树,返回root。
- 当root的值小于L,则其左子树的值都小于L,抛弃左子树,返回修剪过的右子树。
- 当root的值大于R,则其右子树的值都大于R,抛弃右子树,返回修剪过的左子树。
最终的代码也就非常简短与清晰了:
public TreeNode trimBST(TreeNode root, int L, int R) {
if (root == null) return null;
if (root.val < L){
return trimBST(root.right, L, R);
}
else if (root.val > R){
return trimBST(root.left, L, R);
}
else {
TreeNode ans = new TreeNode(root.val);
ans.left = trimBST(root.left, L, R);
ans.right = trimBST(root.right, L, R);
return ans;
}
}
最后总结一下,c++中函数参数进行传递的时候都是浅拷贝。如果是指针它的意思就是只复制它的内容给一个新的对象,这个函数结束后这个新对象就释放了。因此我们如果想要改变原来的这个参数的值就是通过引用的方式,(最后PS,引用的好的点在于用了同样的对象同样的空间,避免了复制,想想看如果是一个数组或者是vector并且这个函数多次调用的时候这个花销的时间得有多大,说多了都是泪。另一道题参数少写一个&,TLE大半天才发现对一个无需改变的vector自己拼命在调用、复制来复制去)