问题简述
递归给vector添加元素与赋值时,由于vector扩容后地址会变,程序中记录了vector的旧地址,导致赋值失效
代码
这个问题是在写线段树的时候发现的
刨除无关紧要的线段树的部分,留下能产生bug的代码
#include<bits/sdtc++.h>
using namespace std;
using LL = long long;
struct Tree {
std::vector<int> ls, rs;//ls[i]为i号节点的左子节点编号,rs[i]为i的右子节点编号
int dfs(int pos,int x) {//pos为当前节点编号
if (pos == 0) {//当前节点编号为0说明还没有这个节点,
ls.push_back(0);
rs.push_back(0);
printf("ls.size=%zu,ls.capacity=%zu\n", ls.size(), ls.capacity());
// printf("rs.size=%d,rs.capacity=%d\n",rs.size(),rs.capacity());
pos = (int) ((int) ls.size() - 1);//初始两个元素,确保不会负数溢出;
}
if (x < 10) {
ls[pos] = dfs(ls[pos], ++x);//问题在于这一行
}
// else if(x<20){
// rs[pos]=dfs(rs[pos],++x);
// }
printf("current node:%d ls:%d,rs:%d\n", pos, ls[pos], rs[pos]);
return pos;
}
};
int main() {
Tree tree;
tree.ls.resize(2);//0号节点不用
tree.rs.resize(2);
tree.dfs(1,1);//1为根节点
return 0;
}
这段代码递归地调用dfs函数,创建每个节点的左子节点,2是1的作子节点,3是2的左子节点…,10是9的左子节点.每次创建节点后,输出当前ls的大小以及容量,整个树创建完成后,递归返回时,倒序输出每个节点编号以及其左子节点和右子节点编号,没有则默认值为0.理论上输出是这样的
期望输出
实际上输出是这样的
实际输出
分析
经过了大概四个小时的调试研究,发现问题出现在
ls[pos] = dfs(ls[pos], ++x);
程序运行到这一行时,就已经读取的ls[pos]的地址并且暂存了起来,等待函数递归返回时赋值给这个地址,但是问题在于,在后续的递归中,ls可能会扩容,扩容后ls数组被放到了新的地方,刚刚暂存的地址指向了一块现在为开辟的空间,这样递归返回值就不能赋值到正确的地方.
以上猜想的证据就是在实际输出中,ls最后一次扩容是在ls.size=9的时候,此时最大节点编号为8,而 在递归返回的输出中,刚好节点8就是最后一个能正确记录自己左儿子的节点,节点7和以前的节点都没有记录自己的左儿子.
这是因为,节点8是在最后一次扩容后的节点,ls[8]的地址是最新的地址,是有效的地址,编号小于等于7的节点使用的都是ls最后一次扩容前的旧地址
验证
在每次创建新节点时打印ls的首地址
printf("ls.size=%zu,ls.capacity=%zu,ls.location=%d\n", ls.size(), ls.capacity(),&ls[0]);
结果
D:\CLionCode\LanQiaoCode\cmake-build-debug\test1.exe
ls.size=3,ls.capacity=4,ls.location=-536402288
ls.size=4,ls.capacity=4,ls.location=-536402288
ls.size=5,ls.capacity=8,ls.location=-536402864
ls.size=6,ls.capacity=8,ls.location=-536402864
ls.size=7,ls.capacity=8,ls.location=-536402864
ls.size=8,ls.capacity=8,ls.location=-536402864
ls.size=9,ls.capacity=16,ls.location=-536402240
ls.size=10,ls.capacity=16,ls.location=-536402240
ls.size=11,ls.capacity=16,ls.location=-536402240
current node:10 ls:0,rs:0
current node:9 ls:10,rs:0
current node:8 ls:9,rs:0
current node:7 ls:0,rs:0
current node:6 ls:0,rs:0
current node:5 ls:0,rs:0
current node:4 ls:0,rs:0
current node:3 ls:0,rs:0
current node:2 ls:0,rs:0
current node:1 ls:0,rs:2
可以看到,在扩容之后,ls的首地址改变了,猜想正确
解决方法
方法一
既然是由于ls扩容导致地址更新引起的问题,那么我直接在一开始就给vector充足的容量,这样ls就不用扩容,也就地址也就不用更新
int main() {
Tree tree;
tree.ls.reserve(100);//这个函数可以重置vector的容量,但是其已经有的元素数量不变
tree.rs.reserve(100);//预设充足的容量
tree.ls.resize(2);//0号节点不用
tree.rs.resize(2);
tree.dfs(1,1);//1为根节点
return 0;
}
输出
D:\CLionCode\LanQiaoCode\cmake-build-debug\test1.exe
ls.size=3,ls.capacity=100,ls.location=579036816
ls.size=4,ls.capacity=100,ls.location=579036816
ls.size=5,ls.capacity=100,ls.location=579036816
ls.size=6,ls.capacity=100,ls.location=579036816
ls.size=7,ls.capacity=100,ls.location=579036816
ls.size=8,ls.capacity=100,ls.location=579036816
ls.size=9,ls.capacity=100,ls.location=579036816
ls.size=10,ls.capacity=100,ls.location=579036816
ls.size=11,ls.capacity=100,ls.location=579036816
current node:10 ls:0,rs:0
current node:9 ls:10,rs:0
current node:8 ls:9,rs:0
current node:7 ls:8,rs:0
current node:6 ls:7,rs:0
current node:5 ls:6,rs:0
current node:4 ls:5,rs:0
current node:3 ls:4,rs:0
current node:2 ls:3,rs:0
current node:1 ls:2,rs:0
进程已结束,退出代码0
可以看到,ls没有扩容,首地址也没变,每个节点也都能找到它的左子节点
方法二
既然是由于程序运行时记录的ls[pos] 太旧了导致的问题,那么我让程序赋值的时候直接用最新的ls地址不就可以了吗
if (x <10) {
int a=dfs(ls[pos],++x);
ls[pos]=a;
// ls[pos] = dfs(ls[pos], ++x);
}
将递归用的语句改成这样,ls[pos]的地址就不会在递归之前被暂存起来,而是在赋值的时候直接取最新的地址用姐可以解决问题
** 以上两种办法可以混用,或者根据要写的程序的要求选择一种用**
完美的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
struct Tree {
std::vector<int> ls, rs;//ls[i]为i号节点的左子节点编号,rs[i]为i的右子节点编号
int dfs(int pos,int x) {//pos为当前节点编号
if (pos == 0) {//当前节点编号为0说明还没有这个节点,
ls.push_back(0);
rs.push_back(0);
printf("ls.size=%zu,ls.capacity=%zu,ls.location=%d\n", ls.size(), ls.capacity(),&ls[0]);
// printf("rs.size=%d,rs.capacity=%d\n",rs.size(),rs.capacity());
pos = (int) ((int) ls.size() - 1);//初始两个元素,确保不会负数溢出;
}
if (x <10) {
//方法二:地址现取现用
int a=dfs(ls[pos],++x);
ls[pos]=a;
// ls[pos] = dfs(ls[pos], ++x);
}
// else if(x<20){
// rs[pos]=dfs(rs[pos],++x);
// }
printf("current node:%d ls:%d,rs:%d\n", pos, ls[pos], rs[pos]);
return pos;
}
};
int main() {
Tree tree;
//方法1:给vector充足的容量
tree.ls.reserve(100);//这个函数可以重置vector的容量,但是其已经有的元素数量不变
tree.rs.reserve(100);//预设充足的容量
tree.ls.resize(2);//0号节点不用
tree.rs.resize(2);
tree.dfs(1,1);//1为根节点
return 0;
}
结语
本来想刷点数据结构的题,结果调个代码三四个小时差点调到汇编代码去了.
网上找了半天资料也没有关于这个bug的任何资料,纯纯折磨.
还有一个匪夷所思的点是,这份代码在clion中运行就会有问题,但是在vscode中运行就能够正常输出.用的同一个电脑,同一个mingw,同样的g++编译器路径.我也不知道为什么,不管怎么样,以后要用递归和vector的时候一定要留点心了