之前上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自己拼命在調用、復制來復制去)