二叉搜索树进阶

基础二叉树
搜索二叉树
平衡搜索二叉树–>查找效率极高

实现搜索二叉树

我们开始走在搜索二叉树这步了。单纯的二叉树,存储数据是没有特别大的价值的
✳️二叉搜索定义
在这里插入图片描述
总结一下,任意一个子树都要满足, 左子树的值 < 根 < 右子树的值。

✳️当你满足搜索二叉树的时候你的优势是什么?
》我查找一个值非常的快,搜素搜索,不就专门进行查找吗。看下面图,我要查找7怎么去查找?我们以前的查找都是暴力的去查找,遍历一遍从头到尾。现在搜索二叉树是不是舒服很多了。7比8要小,我还需要到右子树去找吗?不用直接到左子树去找,按这样逻辑一路往下走就找到了。
》所以最多查找这棵树的高度次,效率很高。所以它叫做二叉搜索树或者搜索二叉树。

❓它这里叫做二叉排序树和排序二叉树啥意思?怎么走的?
》你看看中序是不是刚好就是有序的! 如果我们是走中序的话就是左子树、根、右子树。所以这棵树还可以用来排序。其实我们排序算法不错了,没必要用它。

❓我们看看查找的效率是多少,即它的时间复杂度是多少?
》很多同学都会觉的是logN,但它不是logN!它是O(N).因为这棵树它会退化,它会偏向成一边去即下图。所以后面给出平衡搜索二叉树,这样使两边能够均匀。满二叉树是logN,完全二叉树是接近logN,平衡树的左右均匀和完全二叉树差不多。
》所以要对现在的搜索二叉树进行改进,但还是比较复杂。
在这里插入图片描述

✳️树我们先实现出来再说
肯定是要用到模版的,模版参数的名称和你这块要表达的意思有关系的,就是你要干嘛。
在这里插入图片描述
》对一棵树进行insert()插入,好插入吗?要插入是非常容易的,若我在这里要插入16、9怎么插入呢?我们先用非递归版本,直接用循环就能搞定了。
》我们是不是可以直接定义一个cur,若newnode的值比cur大,那就说明要往右走,cur就像右走,反正就是按照二叉搜索树的规律来进行判断左子树的值 < 根 < 右子树的值。找到位置之后,能不能说直接new上一个节点给我们的cur呢?我new一个值为16的节点给我们cur,能不能完成插入?不能!因为这里的cur节点是一个局部变量!并且cur的位置是NULL位置,你给了也没用。所以我们再定义一个parent,当cur往下走之前先给给parent。当我的cur走到NULL的时候,parent是不是也刚好是我的父亲,我new一个节点是不是直接就链上了。这逻辑是不是很简单。我们默认是不能插入重复的值,后面会有针对它给变形。包括map和set都不允许冗余的。
》我们这块中序应该咋调用?因为中序我是要传根的,以前C语言是可以直接不传根,因为C语言不封装嘛,C++实现封装了那怎么传?所以要么就在BSTree里面提供一个GetRoot()函数 。我们更好的方式套一层,并且C++里面类写递归都是要嵌套一层,也就是加一个_Inorede()函数,就不需要我们传参了,我在类里面好不好拿?好拿呀,内里面又没有访问限制。最好递归的_Inorder()设置为私有,如果类外不访问和不用被继承。有同学说就在Inorder(Node* root)里面给,对呀就是要给呀,如果不嵌套的话就是要给,因为递归肯定是要传参数的!若你不想传参数,那么你就要解决要么能够在类外部得到根,要么你就再嵌套一层懂吗?做不到不传参数!
》Find()函数呢?非常容易,也就是从根节点开始走,再加上一个循环就行。
》严格来讲:不允许冗余的二叉搜索树有排序➕ 去重的功能。
》搜索树最难的点是删除Erase()。怎么删呢? 比起我们之前链表什么的都要难删,你链表只有一个前一个后,二叉树这里没那么简单。我们先来假设删除7、14、3、8 。
在这里插入图片描述
》7删起来很好处理,直接删就行了,但是不要忘了不止要找到7还要找到7的父亲,然后让其父亲指向空NULL,所以删叶子节点是最好删除的;
》下面删除14了,删除14是不是也挺好删除的,因为14只有一个孩子。只有一个孩子的也好删,我们把14干掉让其父亲10指向孩子13,就行了。
》3 和 8不好删除。先来看3:3为什么不好删除?因为它有两个孩子,我把3直接删除了,我二叉树能管没有孩子的我直接干掉,你有一个孩子我也方便帮你管,但是你有两个孩子的话我不好帮你管,我自己还有一个孩子呢。我再管就管不过来了,怎么删除呢?3的特点是2个孩子。7和14是直接删除,这里需要使用替换法删除,替换法删除是怎么样子的呢?能不能这样子,让别人来养,我直接删除的话有两个孩子留下来了,那怎办?我是不是可以这样子,我首先我自己这个位置不能断,孩子总得有人养吧,是不是找一个人来替代我这个位置!我们直接删除的场景是:没有或者只有一个孩子可以直接删除,然后将孩子托管给父亲。 **但是两个孩子没办法给父亲,父亲养不了,是不是可以找个人替代我养孩子。**那怎么找呢?那肯定也得遵循搜索树的规则!谁能来替代我呢?谁有条件来呢? 你来的这个人呢,你得比左边的孩子大,还得比右边的孩子小。去哪儿去找是比较合适的?**可以去找左子树的最大值节点,或者是右子树的最小值节点。**搜索有一个特点,一棵树的最左节点,走到左孩子为NULL的那个就是最小值,;一棵树的最右节点,走到右孩子为NULL的那个就是最大值。所以在这棵树中1或者4都可以。若是找4则是右树的最小,它替代3的位置,肯定比右树要小,比左树都要大。3还不是最明显,8还要明显一点。
》 8这里找谁来替代呢?找右树的最小节点,那是不是10,那怎么替代呢?是吧10的赋值给8这个节点,然后删除10这个节点。怎么删除呢?**因为右树的最左节点的左孩子一定是为NULL的相当于没有孩子,所以最多是只有一个孩子或者没有孩子。**然后再按照删除7、14的直接删除和托管的方式。或者我们不找右树的最小,去找左树的最大,左树的最大值节点呢,替换8之后他肯定会比左树的值大,但一定也会比右树的值小。所以左树的最大是不是去找这个7呀,然后转换成删除这个7节点,7节点的左树有没有我不知道,但我知道的事它右树肯定是没有!所以只要让6节点去托管7的左孩子就行。
》我们得把讲的给实现一下。首先我们得找到我们要删除的节点,那是不是定义一个cur,用while循环不断向下去找。
》这一块呢有人就去进行了总结, 其实分为这里的3类情况。右孩子为空NULL,就让父亲指向我的左孩子;若左孩子为NULL就让父亲指向我的右孩子;有同学说那叶子节点呢?其实叶子节点的话当成左孩子为空NULL或者右孩子为空NULL都可以。比如删掉这里的7去替代3的时候,我是不是确定了7的右孩子肯定为空NULL,所以不管7的左孩子是不是空NULL我都让父亲6去托管我的左孩子,虽然是空NULL的,我只不过托孤的是个空NULL孩子而已。
》我们定一个cur还不够好,我们是不是再定义一个parent呢,然后cur往下走之前都把cur的节点给parent。若我cur的值要比erase的值要大,就往右边走;如果要小就往左边走。我们找到的位置是树里面的任意一个位置。有可能是找到的值的节点左为空NULL,或者右为空NULL,也有可能是左右都不为空NULL这3种情况。若删除的值不在这里面有没有可能?当然有可能,当这个值不在树里面的话,那cur肯定是走到空NULL的位置,那就不删除了返回return false。
》我们说了4种情况是归为3种情况,若是叶子节点的话,就直接归为左为空NULL或者右为空 NULL的情况。所以左为空NULL或者右为空NULL就都是一个孩子的情况。
》若是左为空,那让父亲的那个指针指向右孩子呢?所以还不确定!你画图你当然知道是左还是右,但你现在是代码是不知道父亲的左指针还是右指针指向的你。所以还得比一下,不比大小也行,比较地址是否相等就好了。所以,如果我要被erase的节点是父亲的左孩子,那就让父亲的左指向我的右孩子,否则就让父亲的右指向我的右孩子。若erase的节点是右为空,则同上面逻辑。
》但是这样写还有一个bug!叶子删除是没有问题的,因为叶子节点走到if (cur->_left == nullptr)就进逻辑了被当成只有一个孩子且左为空NULL。这里的问题是若要erase的节点是我们根的情况,即根只有一个孩子的时候,要单独处理一下。若不处理的话,parent是NULL,你的cur是_root,是不是有空指针解引用的问题呀。其实简单处理就行,不用想复杂去用替代法,我直接让我的_root变为其右孩子,如果是根的左边为空的情况;若有右边为空的情况,就直接把左孩子给_root就行。
》开始处理两个孩子都不为空NULL的情况了,用替换法来删除。怎么写代码呢?我们不是说找右树的最左节点,或者找左树的最有节点嘛!我们就统一找右树的最左节点吧。我们要先定义两个变量right_min和parentRM。然后肯定是要while循环去找,并且第一次肯定不为空NULL,因为cur的两个孩子都不为空NULL,所以它至少会走一次,每次向下走之前都要将节点交个parentRM。找到之后呢,有同学有问题了,我们找到之后是不是说去覆盖,但有的同学想要去交换用swap()函数,它这样交换swap(cur, right_min),不对!因为cur和right_min都是两个局部变量的指针,局部变量的两个指针指向那两个节点,你现在只不过是让cur和right_min换一个指向的对象,并没有去改变这棵树里面的内容!那又有同学说能不能去交换地址?不能!你在一棵树里面变两个节点的关系,不是单纯的让两个变量 ,我的节点都是在堆上,我用指针能找到这个节点,我无论是交换cur和minright或者cur的地址和minright地址交换都不会改变这两个节点位置。你要改这链接关系是很复杂的,你是要去改链接关系的。 你想想在树里面和链表里面,节点并不代表只有我自己,还代表着前后节点跟我的关系。所以多画图就能发现交换节点是不对的。所以最后我们选择交换key即swap(cur->_key, minright->key)。这样之后有老铁脑洞大开用递归去删除即紧跟着return Erase(key);这个意思就是再调一次自己,转换成去删除3,你本来就要去删除3嘛。找到替换节点把他们两交换之后,就再去删除,因为这个节点肯定是左为空,因为它是右树的最左节点嘛,所以它此时的左子树一定为空,所以他肯定会转成只有一个孩子的情况去删除了。这样对吗?不对!它根本找不到这个3!第一它又多着找了一遍,这还不是最大问题;最大问题第二它根本找不到3,因为你的值在交换后的节点的右子树,而3这个值比你小按照我们代码逻辑它只会去左子树找,所以导致找不到。你已经不符合搜索树的规则了。其实是有递归的版本,但不是现在来讲。
》我们只能老老实实的去删除了,4去代替3之后,4的右子树可能不为空NULL我们不能确定,可能还会有节点,所以我们还要找到4这个节点原本的父亲,所以我们前面就说了还要一个minparent。很多同学会误以为我肯定是父亲的左孩子,所以让父亲6这个节点指向我的右就可以了,看起来好像是对的,但是这里有坑!
》删3这里,这样是没问题的,但是删8就有问题了。我10要去和8交换,此时8 的右子树的根10就是最小节点!此时你的minright指向的原来10的位置,minparent指向的是原来8这个节点,而你原本都认为肯定是用原来8这个节点的左指针指向我原来10这个节点的右孩子,因为你是找右子树的最小值节点,肯定认为是右子树的最左边那个,然后被找到的最小值肯定是被其父亲节点的左指针指向着的。那此时你用8原来节点的左指针去指向我10原来的右孩子的话就大错特错了!
》我们的代码是minparent一开始给的空nullptr,而我们的minright一开始给的是cur->right,可是我的minright有可能就是我cur的右子树的根,此时你直接minparent->left = minright->right的话就会出现解引用的错误,所以,我们的minparent不能一开始给空nullptr,应该给minparent = cur。其次也不应该是minparent的左指针指向你minright的右孩子,而是右指针指向你的右孩子!即minparent->right = minright->right。
》所以我们其一,minparent初始化时不能给空nullptr,应该是minparent = cur;其次我们还得判断如果minparent->left = minright那就是正常的父亲节点的左指针指向我的右孩子;但若是我minright一开始就是cur右子树的根的话,就是minparent->right = minright了,所以要加一个判断if (minparent->left = minright)免得清一色的都是minparent->left = minright->right。
》所以你只用3这个场景的话还不够,还得右8这个场景。所以说这一块写删除的点最大问题就是要那两个场景都去套,才能写出完善的代码,否则考虑的就不能周到了,所以一定要把图画好。
》替换法中两个节点的值可以是交换也可以是覆盖的。

我们现在来写写构造函数,其实按道理来讲不用谢构造函数,但是我们要用到拷贝构造。当然拷贝构造也是构造函数。你写了拷贝构造默认的构造就不会生成了
》我们不写构造函数那么编译器自动生成的是一个浅拷贝,此时你来拷贝的话就会多次析构。
》忘记了,还要写一下析构函数。析构呢,我们是不是得释放这一棵树呀,我们可以用递归来玩玩,但是换个角度递归也是有死穴的,递归的深度要是太深,栈会溢出。我们整上一个Destroy(Node* root)来递归,因为递归肯定是要传参数的,你单纯用析构函数写法是不好完成的。既然递归的话的,我们采用后序,先左右子树最后才是自己。
》有同学问析构不用弄成私有吗?不能!因为析构是给对象用的,你把析构弄成私有,对象怎么析构,对象都是定义在类外面的,你写成私有类外面的对象怎么用你私有的呢?所以析构还得是私有,但是你的Destroy()函数可以弄成私有。
》我们现在要拷贝一棵树如何拷贝?我们拷贝一棵树是不是得完成深拷贝,那深拷贝怎么拷?复用insert可以吗?不太好,因为你树的形状可能会变,因为你值插入的顺序不同就会导致构建树的形状不同,所以还得老老实实拷贝。这里是不是一样老老实实的拷贝呀。
》比如说我遇到一个8,我就去创建一个8,然后我要去递归创建左子树,然后我往左边走,遇到一个3我就去创建一个3,然后继续往左边走,遇到1创建一个1,再往左边走,遇到了是空NULL,那我就把空NULL返回到1的左边。然后去递归创建1的右子树,右为空NULL返回给1的右,这样1这棵树创建好了。1这棵是是作为3的左子树递归创建好的,然后回到3,递归创建3的右子树。是不是递归创建就好了呀。
》所以我们去写一个copy()函数去递归,就有些像走你的镜像一样。
》我们上面写的是拷贝构造BSTree(const BSTree& t);但是没还没有写构造函数,但是拷贝构造也是属于构造函数,我们拷贝构造是属于显示的去写了,所以相当于我们有了显示的构造函数了,编译器就不会去生成默认的构造函数了,此时你无法构造对象。所以我们要显示写一个构造函数
》C++11有一个关键字可以用BSTree() = default,就可以让它显示的默认生成。
》拷贝构造不一定能用现代写法,但是opeartor =,赋值重载这里一定能用现代写法。BSTree& opeartor=(BSTree t),然后你自己t不是已经被拷贝过了吗?那么t不就是我想要的吗,然后我们做一下交换不就好了吗swap(_root, t._root);然后你出了作用域就把我的原来给析构了。
》BSTree() = default这个在后面讲C++11里面会提,但是在这里提刚刚好。本来在这块默认生成的就挺好用的了,但是因为我们写了拷贝构造就不能生成了,所以这个场景讲它刚刚好,所以就提前来用用。我们显示写的拷贝构造影响了默认生成的构造函数,因为拷贝构造也是构造函数。

我们来写一些递归的版本:
》我们先来写写Find()的递归版本,它是最舒服也是最简单的。那么我们要Find,递归怎么弄呢? 我们以前是不是循环,比他大往右走,比他小往左走,然后指针迭代就可以了。这里Find也可以返回指针,只不过是会有隐患,会存在key的修改破坏了树 。返回节点的优势就是能修改。我们只前的vector和list,它们的fund是返回迭代器, 返回迭代器的意义就是同时充当了查和修改的作用,迭代器解引用也是能修改的。但是我们后面会讲一个KV模型,会改一下。这里的Find如何转换成子问题呢?以前是指针动。现在转换成子问题,我比你大,我就转换到你大右子树去找,我比你小转换到左子树去找。往子问题转换。递归函数叫FindR,然后我们也说了,类呢大递归函数嵌套一层好一点并且嵌套的那一个函数可以设置为私有_FindR(root, key)。
》如果走到空树还没找到,那就是没有返回return false;如果比它大就转换到右子树去找_FindR(root->right, key);如果比他小就转换到左子树去找_FindR(root->left, key);如果相等就返回return true。找到之后是不是一把就能将我们的true返回return到外面了呢?我13找到了是先return到14这个节点,我13找到的是作为14的左子树找到的。14又是作为10的右子树找到的,10又是作为8的右子树找到的,然后通过8这个节点返回return 到外面的。
》InsertR插入是比较麻烦的。比如说我要插入9,怎么走?9比8大是不是转化到它的右子树去插入,9比10小转换到左子树去插入,当root为空NULL的时候就可以去插入9这个节点了。关键点就是root等于空NULL的时候就可以插入咯。root等于空NULL的时候,你这样写root = new Node(9),但是root这里是一个局部变量呀!它出了作用域就会被销毁,则就没有指向你new的新节点9了。你创建一个9是要和你的父亲链接起来的,那我如何跟我的父亲链接起来呢?我们迭代的时候是怎么做的?迭代那时候是cur比你大我就往右边走,我cur走到空NULL的时候我new了一个节点给cur的同时我是能找到我自己的父亲节点的,所以我跟他的父亲进行了链接。但是我现在走到空了没有父亲。一种方式呢跟之前一样,递归函数增加一个指针,叫parent。我把右传给下面,把自己传给父亲。当root走到空的时候,正好我有parent,用它来做链接关系就可以了。 但是这种思路还是比较麻烦。有没有更简单的思路呢?
》有!我加一个引用处理就能搞定!bool _InsertR(Node*& root, const K& key),我没有选择用父亲,用指针的引用来解决了!比如说第一次插入8,刚开始是空树,那么_root也是空NULL,然后_root传参给root,那么root就是_root的别名,那么root是空NULL,我new了一个8的节点给root,那么你给root是不是就相当于给_root呢!root是_root的别名,是第一次起作用的时候。按照这个逻辑走是不是当走到root为空NULL的时候,这个root是上一个节点即父亲节点的左指针或者右指针的别名呀!这样不就链接上了吗。所以没有父指针胜似父指针。若没有引用就是拷贝,我改变root,是不会影响到链接关系的。
》EraseR()是不是也差不多呢?是的。bool _EraseR(Node*& root, const K& key),如果root是空NULL就说明不存在要删的返回return false;我比你大,就到你的右子树去删除_EraseR(root->right, const K& key);我比你小就到你的左子树去删除_EraseR(root->left, const K& key)。我和你相等就可以删除了。比如我要删除13,我找到13之后,,大思路和非递归也是一样的,我们也是要分3种情况的。递归只是用来查找它的位置,比他大往右边走,比他小往左边走。
》第一种情况,如果你要删除的节点左等于空NULL;第二种情况删除的节点右等于空NULL;第三种情况左右都不为空NULL。其中叶子节点属于第一种或者第二种。我们以前要删除的话还是比较麻烦的,比如我的左为空NULL,让父亲指向我的右,但是我不知道是父亲的左指向我,还是右指向我,所以我还得加一步判断。想想现在这个root递归找到13了怎么删除呢? 这里引用能不能起一下作用呢?我是左为空,让父亲指向我的右,但是我有没有父亲节点呢?有!我们用了引用,胜似有父亲!我们root就是父亲左指针或者右指针的别名,所以直接root = root->right就行了!但是在此之前要保存一份root,Node* del=root,不然后面找不到了。我们再来删除一个10这个节点,我们递归找到10之后,root就是指向10这个节点了,但是root也是8节点的右指针别名,所以先提前保存好root给Node* del,然后root = root->right;
在这里插入图片描述

》我现在要删除3这个节点要怎么删除呢,这是第三种情况。root指向3,同时root是作为8节点左指针的别名,我们找的替代的节点是右子树的最左节点4。此时单独一个引用是解决不了的,所以定义一个minright,minright是指向替代节点4的,当然是循环找到的哈。有同学突发奇想循环找替代节点4的时候,这样Node*& cur = root->right,然后while循环的的往下走的时候,cur = cur->left,这样可以吗?不可以!因为你cur如果是引用的话,你就不能改变自己了,因为引用有这么一个特点,我是你的别名,就永远是你的别名,是不能改的!
》所以我们引用在第三种情况下不起作用了。我们先和替代节点交换值:swap(root->_key,cur->_key)。我们这里不用覆盖是有原因的。现在变成删除原本是4个节点的位置了,那现在怎么删除呢?要么就和迭代的方法一样,在找minright的过程中顺便把minparent也给找到,但是就用不到别名的价值,虽然我们现在用不到别名价值,但是我们可以用别名做一件事情。我们前面讲非递归的时候说不能在交换两个值后直接再递归删除3,为什么不能,因为你交换值之后,就不符合二叉搜索的规则了,你永远无法找到3!
》但是现在可以用别名,转换到右子树去删除3,而不是从一开始的根去找3这个节点,能懂吗。我们交换后的值3,在6这棵树里面是符合二叉搜索的规则。然后转化到6这棵树里面删除3之后,它一定是左为空NULL的情况!是不是把第三种情况转换成第一种情况了呢!妙不可言!
在这里插入图片描述

template<class K>
//struct BinarySearchTreeNodestruct BSTreeNode//结点
{
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;

	K _key;

	BSTreeNode(const K& key)
		:_left(nullptr)
		, _right(nullptr)
		, _key(key)
	{}
};

free(cur);

template<class K>
class BSTree//树
{
	typedef BSTreeNode<K> Node;
private;
	void DestoryTree(Node* root)
	{
		if (root == nullptr)
			return;
		
		DestoryTree(root->_left);
		DestoryTree(root->_right);
		delete root;
	}

	Node* CopyTree(Node* root)
	{
		if (root == nullptr)
			return nullptr;

		Node* copyNode = new Node(root->_key);
		copyNode->_left = CopyTree(root->_left);
		copyNode->_right = CopyTree(root->_right);

		return copyNode;
	} 
public:
	// 强制编译器自己生成构造
	// C++11
	BSTree() = default;

	BSTree(const BSTree<K>& t)
	{
		_root = CopyTree(t._root);
	}

	// t1 = t2
	BSTree<K>& operator=(BSTree<K> t)
	{
		swap(_root, t._root);
		return *this;
	}

	~BSTree()
	{
		DestoryTree(_root);
		_root = nullptr;
	}
	bool Insert(const K& key)//可以写递归也可以写非递归,递归要难理解一点,我们先写一个非递归版本的
	{
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}

		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}

		cur = new Node(key);
		if (parent->_key < key)
		{ 			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}

		return true;
	}

	//const Node* Find(const K& key)
	bool Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_key < key)
			{
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				cur = cur->_left;
			}
			else
			{
				return true;
			}
		}

		return false;
	}

	bool Erase(const K& key)
	{
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				// 一个孩子--左为空 or 右为空
				// 两个孩子 -- 替换法
				if (cur->_left == nullptr)----➡️左为空,是让父亲的左还是右指针指向你的右孩子呢?不确定!所以还得比一下
				{
					//if (parent == nullptr)
					if (cur == _root)---➡️特殊情况:erase的节点是我们根_root
					{
						_root = cur->_right;
					}
					else
					{
						if (cur == parent->_left)---➡️若我是父亲的左孩子那就让父亲左指针指向我的右
						{
							parent->_left = cur->_right;
						}
						else---➡️否则就让父亲的右指针指向我的右孩子
						{
							parent->_right = cur->_right;
						}
					}
					delete cur;
				}
				else if (cur->_right == nullptr)--➡️如果是右为空,同样也得比一下
				{
					//if (parent == nullptr)
					if (cur == _root)--➡️单独处理erase为根的情况
					{
						_root = cur->_left;
					}
					else
					{
						if (cur == parent->_left)--➡️我是父亲的左孩子,就让父亲的左指针指向我的左孩子
						{
							parent->_left = cur->_left;
						}
						else---➡️我是父亲的右孩子,就让父亲的右指针指向我的左孩子
						{
							parent->_right = cur->_left;
						}
					}

					delete cur;
				}
				else ---➡️ 两个孩子都不为空
				{
					// 右子树的最小节点替代
					Node* minParent = cur;
					Node* minRight = cur->_right;
					while (minRight->_left)
					{
						minParent = minRight;
						minRight = minRight->_left;
					}
					//swap(minright, cur);----❌严重错误!
					swap(minRight->_key, cur->_key);
					//reruen Erase(key);----❌脑洞大开调用递归
					//cur->_key = minRight->_key;
					if (minParent->_left == minRight)
					{
						minParent->_left = minRight->_right;---➡️不要清一色的认为这样写就可以了!
					}
					else----➡️minright就是cur的右子树的根的情况!大坑大坑!
					{
						minParent->_right = minRight->_right;
					}

					delete minRight;
				}

				return true;
			}
		}

		return false;
	}
---------------------------------------------------------------------------------------------------
递归版本
bool FindR(const K& key)
	{
		return _FindR(_root, key);
	}

	bool InsertR(const K& key)
	{
		return _InsertR(_root, key);
	}

	bool EraseR(const K& key)
	{
		return _EraseR(_root, key);
	}

private:
	bool _EraseR(Node*& root, const K& key)
	{
		if (root == nullptr)
			return false;

		if (root->_key < key)
		{
			return _EraseR(root->_right, key);
		}
		else if (root->_key > key)
		{
			return _EraseR(root->_left, key);
		}
		else
		{
			Node* del = root;
			// 删除
			if (root->_left == nullptr)
			{
				root = root->_right;
			}
			else if (root->_right == nullptr)
			{
				root = root->_left;
			}
			else
			{
				Node* minRight = root->_right;
				while (minRight->_left)
				{
					minRight = minRight->_left;
				}

				swap(root->_key, minRight->_key);

				return _EraseR(root->_right, key);
			}

			delete del;
			return true;
		}
	}

	bool _InsertR(Node*& root, const K& key)------➡️引用使用的很巧妙!
	{
		if (root == nullptr)
		{
			root = new Node(key);
			return true;
		}

		if (root->_key < key)
			return _InsertR(root->_right, key);
		else if (root->_key > key)
			return _InsertR(root->_left, key);
		else
			return false;
	}

	bool _FindR(Node* root, const K& key)
	{
		if (root == nullptr)
			return false;

		if (root->_key < key)
		{
			return _FindR(root->_right, key);
		}
		else if (root->_key > key)
		{
			return _FindR(root->_left, key);
		}
		else
		{
			return true;
		}
	}

	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;

		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}

	void InOrder()
	{
		_InOrder(_root);
	}

private:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;

		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}
private:
	Node* _root = nullptr;
};

void TestBSTree()
{
	BSTree<int> t;
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	for (auto e : a)
	{
		t.Insert(e); 
	}
	t.InOrder();
 
	t.Insert(16);
	t.Insert(9);

	t.InOrder();
}

K/KV模型的应用场景和解释

递归作为学习还是要掌握的,有些地方用递归肯定还是比较好的。递归只有一个弱点就是深度太深容易溢出,其他问题呢都相对还好。下面呢,我们要讲二叉搜索树的相关应用。搜索有两种场景和模型,一种呢,叫key模型,一种叫KV模型,那什么是K模型,什么是KV模型呢,我们简单的把它们讲讲, 然后搜索二叉树给给。

K模型

》现实当中我们要查找有两种相关的场景,一种是K的搜索模型,一种是KV搜索模型,K简单的来说就是关键字在不在,你存的是什么,关键字就是什么。其最经典的场景就是车牌系统,比如说我是进这个小区的车,我们的系统里面肯定是要想办法存储所有的车牌。我简单一点就是把车牌信息放在文件里面,然后架一个摄像头,然后会拍照设计到图像识别,然后搜索有没有车牌,有的话就抬杆,否则就不动。我们在学校里面都有学生卡,然后去扫一下,机器去读,会去读你里面的东西,在的话就成功。都是用关键字来判断在不在。 有人会问,一个个的去比对自己列表的信息,你觉得效率高不高,肯定不高,那么比较优的方法就是,比如这里有一颗搜索树,我们把车牌都读进去,有人说,我们说的都是数字,你现在车牌是字符串了,也能放到树里面吗,当然,你搜索树的key变成什么就能把它变成string了,string能不能支持比较大小,当然可以,此时是不是比你在线性表里面块,从O(N)变成了logN了,当然搜索树有一个缺点是什么呢,万一建立的一些树有些高有些低,有些车牌抬的快,有些半天抬不起杆来,是不是说明这个棵树不够稳定,效率不是足够的高,所以后面呢还有一个优化的方向,保证它性能,就是我们下面要提到的“平衡树”。平衡树有两个,一个是AVL树,一个是红黑树。这个时候哪怕你车牌很多,它这种树两边均衡就能够达到logN。 logN相当于14亿车牌信息去扫描,我最多31次就能找到你,是不是效率非常快。但是我们的搜索还做不到,所以还要往后面去学,我们后面还要学更牛逼的数据结构,比如说是哈希、跳表等。在不在的模型大家还是很好理解,应用场景还是比较多的。 K模型呢还有些冗余的在里面,比如说你去重也可以用K模型,比如说名字,可能因为某些原因会导致名字重复,我想算有多少人,那怎么去重呢,所以它严格可以做到去重 ➕ 排序。你将所有的数据都插入进去,你再走中序出来都是有序的,这样就能达到去重,因为插入的时候会比较,在的话就直接丢弃掉,并且效率呢,可以做到N*logN,因为有N个数据,每个数据插入都是logN的效率。
》K的模型呢,大家有可能还有想象不到的玩法,比如说,我现在有一篇文章,我想让你写一个城西来检查有没有错误的单词,那么如何来检查呢?有的,并且思路也很简单。单词是不是有词库,那我是不是把词库里面的单词放到一个搜索树里面去,现在这篇文章来了之后呢,它肯定是以空格来一个一个的单词,读出来我去看,在不在树里面,在说明正确,不在说明错误。所以他可以去检查一个拼写是否正确。其实我们编译器给我们拼写做提示,是不是将标准库里面的关键字和类型放入一颗搜索树里面,包括你自己定义的函数都会放进去。在的话就不提示你,不在的话就会提示你拼写错误。这有可能是你不容易想到是用K模型来做的。
在这里插入图片描述

KV模型

》我们再看一个问题,我们现实当中,还有一种模型叫做KV模型,K模型是在不在,而KV模型是通过关键字找到另一个值,另一个值可以是内置或者自定义变量。KV模型最经典的就是中英字典,比如说,我的key是中文,那我输入中文是不是去查找中文所在的节点,我们这个节点除了存储了中文,还存储了这个中文对应的英文,所以我们上面写的一套东西其实K的模型。我们还可以再写KV模型,所以我们可以用我们现有的K模型来改造出一份KV模型来。我们可以先把一些无关痛痒的代码去掉,我们就只留下递归版本吧,非递归我们都删了,反正都是一样的,算了,包括拷贝构造和析构我们都删了算了。它除了存了一个key,还存了一个v value_,那这个时候是不是很简单了,value这个类型是什么我知不知道,不知道,是不是用模版去套更好,那么BSTreeNode是不是都得改成BSTreeNode<K,V>这样的。在构造的时候,你除了给我key,是不是还得给我value,大家要知道,虽然有key有value,但它实际查找的时候是以谁来查找的呢?是不是还是按key来找。就比如,我们查找中文是不是对应的英文就找到了,这个在现实中是不是非常的有意义。代码有些地方得跟着动一动,哪些地方呢,在插入的地方InsertR()除了给一个key值,是不是还得给一个对应的value,所以当我们构建一个新节点,即new Node(key,value);然后再看,比较大小和我们的value有没有关系?没有关系,记住它的整棵树的查找规则,遍历还是不变,还是以key为主的,但只是期望说,我找到key就是找到value了,但是不同的时候,我在插入key的时候还得把我们的value给带上。大家还得想到,key相同,value不同能不能进这个棵树?不能。比如说,你key是相同的,你再插入相同的key,是不行的,因为我虽然多了一个value,但是比较的话我只用key来比较,若你要相同的也进去得是另一个版本了。查找的时候也得变了,你就得返回节点的指针了,那为什么返回节点的指针比较好。因为你返回节点的指针的话你就可以去修改了,这个棵树不允许你修改key,但是允许你修改value,修改的场景是什么我们待会儿看。那删除呢?删除不需要动,查找和删除还是以key来查找和删除的。这个时候同学们就有一个小的疑问,是什么呢,那我们先写一个代码跑出来看。见void TestBSTree1(),最简单的字典就是先出来了,我们提一下这种写法,while(cin >> str),现在你输入一个单词我们是不是通过str就能来查找了,这个while(cin >> str)写法回头再说。 while(cin >> str) 和 while(scanf() !- EOF)底层差不多。我们是不是通过我们的void TestBSTree1()就看到了简单的KV模型应用场景,通过K就找到了对应的V。但是我们现在的这个设计有一个死穴,这个死穴是什么呢,你是不是返回的节点的指针,我们虽然说是修改value,但是你防不住有人修改key值,你能修改Key此时还对不对。是不是这棵树有可能就被打乱了,因为你是返回的节点的指针,在这里是可以修改Key的,是不合适的。那我们怎么去防止呢?如果你直接对我们的Key变量前面加const的话也是不妥的,因为我们插入时候用的替换法会修改到key,你加const的话那里就编译不过去了,所以还是不方便预防。
》我们在哪些地方是KV模型呢,比如说高铁站的进站,刷身份证的时候其实就是KV搜索场景,和我们的K模型搜索还不一样,我们K模型的车牌场景,搜索车牌在不在,我只是看一个值在不在,但是高铁站这里刷身份证,身份证上面是有票的信息,你没票能进站吗,你买票了以后,你身份证就有你票的信息吗?没有!大家知道,身份证是磁卡,要往你磁卡上面有数据的话都得去写数据,如果学校有些传统的还支持充卡,其实就是往你的磁卡上面写数据,你买票的时候有没有去刷身份证写信息进去,没有是不是。那你耍身份是怎么进站的,本质上是,高铁站的扎口是有通过网络请求的,拿你的身份证号去后台查找信息,查找的结果是通过网络给你返回的。你有没有票,有票,你的车次对不对,对的那就可以进去。这里的KV模型是不是用身份查车票信息
》再给大家讲一个没太见过的KV场景,假设有一堆的单词和一堆水果,我想让你统计水果出现的次数,单词出现的次数。见void TestBSTree2()。我们的Key就是string类型,value就是int类型,然后我们这棵树就叫做count_tree,即用BSTree<string, int>来定义一个棵树类型,即BSTree<string, int> count_tree;我们用范围for进行遍历,然后用FindR来进行查找,若不在则进行插入我们的Value,若在的话将我们的value_++;最后对应的水果类型的出现的次数是不是就有了。其实呢可以不用InOrder来打印,以后可以用迭代器来打印。统计次数是不是另一个KV模型的应用场景,不太容易想到。这确实是KV模型的场景,并且它在这还做了什么事呢,它这里是可以修改value值的,记住KV模型才允许修改,修改的是谁,修改的Value,其次有没有发现这么一个问题,这些数据结构修改,修改的接口通常不是提供一个函数,除了vector,它是可以通过operator []方扣号来进行修改,其他的修改都是通过Find来修改。Find呢,要么是返回节点的指针,节点的指针能不能修改,可以,但实际当中STL库都是通过迭代器,迭代器本质上是不是也是一个指针,底层还是指针。这个修改其实也是KV模型的经典场景,以后也是比较常用的。KV还有没有其他场景,有,只要你想通过一个值找到另一个值,哪怕这两个值一点关系都没有都是可以用它的。
》以后要统计次数,在不在的场景需要我们写吗?不需要哈,我们后面会学两个数据结构,一个K模型的场景对应的数据结构就是-------set;KV模型对应的数据结构就是------map,都是库里面帮我们实现的。学了这两个数据结构呢,我们后面做什么OJ题目都会舒服很多。
》再给大家说一个KV模型,还记得前面讲链表的题,有一个题目是复杂链表的复制,我们当时做的时候是一个听恶心的题目。当时我们讲这个题目用到了曲线救国的方式,我们要copy一个节点,但是我们要copy一个rando指针很不方便,你13这个节点的random指向的是7,我要找我的7,但是我不知道7在哪里,我不知道7节点的地址是什么,所以当时我们用一种什么方法来曲线救国,我们是不是将原节点各自copy在后面,这样之后呢就能去找关系了,这个时候是不是将copy节点和原节点建立了关系,因为所有的copy节点都在原节点的后面,我13找到我的random是7,那我们是不是可以找到7的拷贝。现在有了KV模型就简单多了,我们直接先copy这份链表,要7我们去拷贝7,要13我们去拷贝13将他们的next先链起来,那我们copy出来之后,random我们该怎么去解决呢,我每一个拷贝节点怎么去找到你的random呢,比如说,10这个节点我要找到我的random,那我是不是要找到你的拷贝节点,因为我可以根据的原节点找到你原节点的random,但是我遇到10以后,我拷贝节点,我要找到对应的random 11这个节点,但是我不知道他在哪儿。虽然你从图像看一眼就知道在哪里,但是你不知道地址是什么呀。所以如果用我们的搜索树就变成这样了, BSTree<Node*, Node*>,Key是节点指针,Value也是节点指针,BSTree<Node*, Node*> nodeCopyTree,是不是原节点和Copy节点的映射,相当于Key存的是原节点,第二个存的是copy节点,你通过原节点7找到他的random是空NUILL,那么我的Value正好是copy节点7这个节点,所以我将我copy节点7点random设置为空NULL,同理下面。那我是不是相当于用我们的Key去查找,去找到这个原节点的拷贝节点地址,然后给你的random就置上了。就是我找到原节点的random,找到这个random以后,我要去找的是random 的拷贝节点,之前我们方案是什么,我们关系是,rangdom的拷贝是连接在原节点的后面,现在不用把拷贝节点链接在原节点后面这个方案了,但是我们可以把他们存在这棵树里面,那么我存到树里面,能找到原节点的random的地址了,我是不是就可以找到random的拷贝节点的地址,拷贝节点的地址,是不是就是我要的random地址。比如我11点random是1,那我拷贝节点11点random是谁,是不是就是原节点1的拷贝,1的拷贝节点地址是不是和原节点1的地址在一个节点上,一个是Key,一个是Value,那是不是就相当于找到了我拷贝节点所需要的random节点地址。
在这里插入图片描述
》所以大家可以看到,任意的场景里面,你们两个值关联在一起,你需要通过一个值找到另外一个值,那么你就可以用这个KV模型,非常短实用。那么有人说,以后需要我们自己写一颗搜索树吗?当然不用,我们库里面有一个map的数据结构,map就是KV模型的搜索树,只不过它的底层封装的不是直接的搜索树,而是再优化过的平衡树,在这里的应用场景就是这样写,即map<Node*, Node*>.

namespace key_value
{
#pragma once

	template<class K, class V>
	struct BSTreeNode
	{
		BSTreeNode<K, V>* _left;
		BSTreeNode<K, V>* _right;

		const K _key;
		V _value;

		BSTreeNode(const K& key, const V& value)
			:_left(nullptr)
			, _right(nullptr)
			, _key(key)
			, _value(value)
		{}
	};

	template<class K, class V>
	class BSTree
	{
		typedef BSTreeNode<K, V> Node;
	public:

		void InOrder()
		{
			_InOrder(_root);
			cout << endl;
		}

		///
		Node* FindR(const K& key)
		{
			return _FindR(_root, key);
		}

		bool InsertR(const K& key, const V& value)
		{
			return _InsertR(_root, key, value);
		}

		bool EraseR(const K& key)
		{
			return _EraseR(_root, key);
		}

	private:
		bool _EraseR(Node*& root, const K& key)
		{
			if (root == nullptr)
				return false;

			if (root->_key < key)
			{
				return _EraseR(root->_right, key);
			}
			else if (root->_key > key)
			{
				return _EraseR(root->_left, key);
			}
			else
			{
				Node* del = root;
				// 删除
				if (root->_left == nullptr)
				{
					root = root->_right;
				}
				else if (root->_right == nullptr)
				{
					root = root->_left;
				}
				else
				{
					Node* minRight = root->_right;
					while (minRight->_left)
					{
						minRight = minRight->_left;
					}

					swap(root->_key, minRight->_key);

					return _EraseR(root->_right, key);
				}

				delete del;
				return true;
			}
		}

		bool _InsertR(Node*& root, const K& key, const V& value)
		{
			if (root == nullptr)
			{
				root = new Node(key, value);
				return true;
			}

			if (root->_key < key)
				return _InsertR(root->_right, key, value);
			else if (root->_key > key)
				return _InsertR(root->_left, key, value);
			else
				return false;
		}

		Node* _FindR(Node* root, const K& key)
		{
			if (root == nullptr)
				return nullptr;

			if (root->_key < key)
			{
				return _FindR(root->_right, key);
			}
			else if (root->_key > key)
			{
				return _FindR(root->_left, key);
			}
			else
			{
				return root;
			}
		}

		void _InOrder(Node* root)
		{
			if (root == nullptr)
				return;

			_InOrder(root->_left);
			cout << root->_key << ":" << root->_value << endl;
			_InOrder(root->_right);
		}
	private:
		Node* _root = nullptr;
	};

	void TestBSTree1()
	{
		BSTree<string, string> ECDict;
		ECDict.InsertR("root", "根");
		ECDict.InsertR("string", "字符串");
		ECDict.InsertR("left", "左边");
		ECDict.InsertR("insert", "插入");
		//...
		string str;
		while (cin >> str)  //while (scanf() != EOF)
		{
			//BSTreeNode<string, string>* ret = ECDict.FindR(str);
			auto ret = ECDict.FindR(str);
			if (ret != nullptr)
			{
				cout << "对应的中文:" << ret->_value << endl;
				//ret->_key = "";
				ret->_value = "";
			}
			else
			{
				cout << "无此单词,请重新输入" << endl;
			}
		}
	}

	void TestBSTree2()
	{
		string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
		// 水果出现的次数
		BSTree<string, int> countTree;
		for (const auto& str : arr)
		{
			auto ret = countTree.FindR(str);
			if (ret == nullptr)
			{
				countTree.InsertR(str, 1);
			}
			else
			{
				ret->_value++;  // 修改value
			}
		}

		countTree.InOrder();
	}

✳️解释一下whie(cin >> str),这个代码在有些OJ当中会有些使用,有些OJ题叫做IO型的OJ题,比如说把字符串转化成整数,它可能有多行,如果你只这样写,即cin >> str是过不了的。因为有多行,要求你的程序能够持续的去接收,那么它的测试用例是怎么给的呢,其实是将测试用例写到文件里面,这里会涉及Linux的重定向,cin会从文件里面读,文件读结束了就自动会结束。while(scanf() !- EOF)还能理解,scanf的返回值是什么,它的返回值本身就是一个整数,返回EOF的时候就是代表它读取出错了,那么就会返回EOF,即=1。那么while(cin >> str)是怎么判断结束呢?>>是不是流提取的运算符重载呀,相当于调用的是库里面的string的流提取运算符。那也就意味着,这里的返回值是什么,scanf是!-EOF是一个整形,那这里while(cin >> str)是怎么判断的呢?它的返回值是个啥?它的返回值是不是就是cin呀,它这里肯定是被转化成这样调用的,istream& operator >>(cin, str),相当于这里使用cin来做条件判断。那么cin如何做条件判断呢,你有见过cin、istream,一个类对象去做条件判断的吗?没有见过吧,istream这个对象咋去做条件判断的呀,这个如果是将IO流那节就会讲这个,IO流不算重要就被放到了后面去了。这里提前说说,其实istream类型对象呢重载了一个非常特殊的运算符,这个运算符是bool类型,在C++98里面是void*,C++11里面是bool,一个叫operator void*,一个叫operator bool,没见过吧,以前没办法提。有人问,operator void* 和 operator bool是运算符吗?注意它不是运算符,它代表的意思是,这个类型的对象可以强制转换成void或者bool,它其实是强制类型转换,当然C++98是void,然后C++11就是bool了,这是强制类型转换本质。也就是这里返回的对象是cin这个对象,这个对象呢会在做条件判断的时候,在C++98是强转成void*,void是不是可以做条件判断了, void指针嘛,或者整形可以做条件判断,0就是假,1就是真。bool这里更加是真假了。有人说强制类型转化不是应该是 ()这个符号嘛?但是大家想想,强制类型转换为什么不是这个符号?因为这个符号被别人用了,给谁用了呢?是不是仿函数用了这个符号呀!所以这里又涉及到运算符冲突的问题。这个转换什么意思呢,它会不断去我们缓冲区读取数据,你输入ctrl➕Z什么意思,相当于读取文件结束,你去调用operator bool就会返回false,ctrl ➕ C就是发送信号 。
》我们下面写一份代码来进一步帮助大家理解operator bool:我们没写operator bool函数的时候,你会发现在void Test()函数那里while(a)是编译不过去的,她会说A类型的条件表达式是非法的,你仔细看cin >> str这个表达式是一个函数调用,函数调用会有一个返回值,返回值是不是去做判断呀,cin istream类型能不能做条件判断? 不能!但怎么样就可以了呢,是不是重载一下bool,还有注意operator bool()是不用写返回值的,很特殊。把函数operator bool()写好之后他就编译通过了。神奇不神奇,本质上while(a)这里在干嘛?它本质上是去调用operator bool函数,条件控制由你程序员自己去控制,operator bool是可以让自定义类型隐式类型转换成bool值,bool的结果,你转换成bool值了,是真还是假就可以你自己控制,就可以让自定义类型去当条件判断,while(a)本质上等价于while(a.operatro bool())当然你可以显示的去调用。就像是把它转换成bool值一样,转换成bool值以后是真还是假呢,由这个函数的逻辑决定。他不是说是转换成bool,而是转换成去调用operator bool函数,就像是转换成bool值,因为bool值才能做条件判断。就像你用it,大家就要来理解运算符重载的这特点,当然这里的运算符又比较特殊一些,比如说list::iterator it; it;大家想一下这个it真的是去解引用吗?是对那个指针指向的内容去解引用吗?不是,是转化沉函数调用,函数调用的返回值作为该表达式的返回值,不是说真的去解引用。这里while(a)也是一样,它是转化成可以做逻辑条件判断的值,隐式转换成可以做逻辑条件判断的值。大家看这里是怎么去转换的,它是说把这个就转换成int吗?不是,它实际上是去调用函数,调用的这个函数是有一个返回值,这个返回值是bool,bool无非就是真或者是假,到底是真还是假由你的operator bool来决定。我们it这样写,其实就是it.operator*(),要理解清楚。我们还有另外一个问题,如果我们加一个关键字,explicit关键字,即explict operator bool()便就不会让你隐式类型转换了。我们加了explicit,他还是可以编译通过这是为什呢,我们都知道explicit在这里是干嘛的,是不是防止隐式类型转化发生的,那这里while(a)不应该不行了吗,为什么要加explicit,它的作用是什么呢?这块还有一个隐含的意思,隐含的意思是什么呢,我们加一下bool ret = a;这样就编译不过了,当我们去掉explicit之后又能编译过了。能明白什么意思了吗。A(int a = 0) {}和 operator bool()是不是都可以加explicit,我们以前单参数的构造函数,是不是可以支持这样的玩法,A aa = 10,整型10隐式类型转化成aa,它是不是10可以去构造一个A类型对象,然后再构造一个临时对象,临时对象去拷贝构造aa,但是编译器优化,它就会直接构造,若在构造函数前面加explicit,A aa = 10;就不能支持了。那么我们的operator bool不加explicit,则bool ret = a;是可以的,是能够支持自定义类型转化成bool类型,再换一个角度,我们不是operator bool,而是改成operator int(),那么 int ret = a;不是说一定要重载operator bool,operator int()也是可以的。那么自定义类型可以转化成内置类型int。你可以理解这块的类型转化是怎么发生的呀,这个转化本质是这样发生的,你可以理解,int ret = a;转化中间是可以产生临时变量的,这个临时变量是怎么发生的,这个临时变量不是直接产生的,它不是把a转化成了int,而是在转的过程中产生了一个临时变量,它会调用operator int(),然后函数的返回值会返回给ret。while(a)条件判断也是可以用operator int()去判断while条件的。如果你在operator int()前面加一个explicit,它就不支持while(a)中a的转化了,但是我们编译还是通过了,它应该是不支持你转化成对象,但是支持你调用函数,因为while(a)中的a不是真的去转化,而是去调用函数知道吗。explicit不支持你转化成对象。所以换一个角度,不只是operator bool,operator int,operator +内置类型都行,就可以让自定义类型转化成内置类型,但是加了explicit就不能支持转化成对象了。我们了解就行,看到这个就不要觉得奇怪就行了。这个我们就当成特例去了解,其实就是相当于支持了自定义类型转化成内置类型,我们以前构造含糊,A aa = 10;是内置类型转化成自定义类型,单参数的构造,也是隐式类型转化。若我来设计不太会设计这样的operator bool语法,用法违背常规运算符重载,这样支持后,只是看起来简洁了,但是用起来和理解不太舒服了。
》我们只要了解一下就可以,能够稍微知道为什么while(cin >> str)可以做条件判断就行。是因为cin的istream类型重载了operator bool。
在这里插入图片描述

#include<iostream>
using namespace std;

#include "BinarySearchTree.h"

class A
{
public:
	explicit A(int a = 0)
		:_a(a)
	{}

	//operator bool()
	//{
	//	if (_a < 10)
	//	{
	//		return true;
	//	}
	//	else
	//	{
	//		return false;
	//	}
	//}

	explicit operator int()
	{
		if (_a < 10)
		{
			return 1;
		}
		else
		{
			return 0;
		}
	}

	void Print()
	{
		cout << _a << endl;
	}

	void Set(int a)
	{
		_a = a;
	}

private:
	int _a;
};

void test()
{
	//list<int>::iterator it;
	//*it; --> it.operator*()

	//A aa = 10;

	A a;
	//bool ret = a;
	//int y = a;
	int x;
	//while (a.operator bool())
	while (a)
	{
		a.Print();
		cin >> x;
		a.Set(x);
	}
}

二叉树OJ

题一:

我们下面要讲的最难的应该是非递归。题目会由简单变难。

》第一个题目是根据二叉树创建字符串,就是把它的前序转化成字符串,字符串是由数字和空括号一起组成,然后有些情况空括号是可以省略,有些情况还是要保留来区分左右子树。
》总结一下,1.左右都为空,或者右树是空树,那就可以将括号省略掉;另外一种,2.如果是左为空,右不为空那么括号就不能省略了,不然无法区分左右子树。
》这道题我们走的话去走一个前序就可以了,遇到前序的值就拿出来,在走左树之前加一个括号,左=树走完了就加一个右括号,右树也一样,然后再检查一下逻辑,这道题不难。如果用C去做非常恶心,用C很难去搞。你用C去搞,它是要你打印吗?它是要一个字符串,字符串你怎么去走,C里面库函数什么最合适呢,你是不是相当于在字符串里面不断添加东西呀, 遇到1插入1,遇到2插入左括号和数字2,不断往字符串插入东西呀,C语言库函数与难过strcat(),它有一个问题,第一个就是你要提前开好空间,它其实就是往尾部追加东西,我不知道开多大呀,我不知道树有多大,空间就不知道开多大了,所以要么开特别大,或者加两个变量size和capaity不够了就扩容,不就是实现strangl了吗;第二个strcat效率很低,它每次是不是都要寻找‘\0’呀,你插入一个找一次,插入一个找一次,那效率是不是n的平方。所以strcat每次都要找尾,就效率很低。
》这道题用C++完美契合了string的特点,尾插嘛。我们要用递归。前序递归我们先把前序写出来,空树不要直接去return 括号,因为空不一定是要去加括号的,而是去return一个空串。如果不是空树怎么办呢,是不是加等自己,str+=to_string(root->val),我们在递归左树之前是不是先加一个“(”左括号,然后呢,我再去递归我们的左树,紧接着就是添加右括号“)” ,然后就是要去递归我们的右树,所以先加左括号“(”str += ‘(’,然后去递归我们的右子树,str += tree2str(root->right);然后就是加右括号‘)’, 这是不去掉括号原生的版本。你从整体角度想想,你不是前序递归嘛,我先加到1,我递归左树,左树加括号,递归下去会把左树那块给我搞出来,然后从整体角度,我再递归右树,右树会把右树的串搞出来,然后我不断用+=就可以了。即

string tree2str(TreeNode* root) {
    if(root == nullptr)
        return "";------>返回空串
    string str += to_string(roo->val);
    str += "(";
    tree2str(root->left);
    str += ")";

    str += "(";
    tree2str(root->right);
    str += ")";

    }

但是我们空括号还没有处理,是不符合规则。我们还得把空去一下,是不是按我们的上面的总结1、2两个,如果左右都为空或者左不为空,右为空,就可以省去;那么我们可以先考虑到,右为空肯定是不用向下走了且不用加括号了;所以可以先if(root->right),然后我们去处理左子树,因为左子树有两种情况,左为空不一定没有括号, 所以if(root->left)不为空那左子树肯定要做,若else if(root->right)也就是右子树不为空,则左子树为空节点的括号不能少,所以得str += “()”,另一种情况root->right为空的话,其实放到判断右子树那里考虑,代码即:

string tree2str(TreeNode* root) {
    if(root == nullptr)
        return "";
    if(root->left)
    {
        str += "(";
        _tree2str(root->right);
        str += ")";
    }
    else if(root->right)//如果左为空,右不为空则还得走
    {
        str += "()";
    }
   
    if(root->right)
    {
        str += "(";
        1tree2str(root->right);
        str += ")";
    
    }
    return str;
    }

想问一下现在的这种写法有没有什么缺陷?也不能说是缺陷或者说值得优化的地方。其实就是题目给的时候函数的返回值不是引用而是要用拷贝构造,若是string&就更好了。我们能不能说是改成传引用?不能直接去传引用,因为我们这里睡局部变量,出了作用域就会被销毁。若你直接将string test2str(TreeNode* root)改成string& test2str(TreeNode* root)是不对的,会有越界错误。
》优化思路就是,写一个子函数然后套一层。我们的子函数就是,void _test2str(TreeNode* root, string& str),在整个递归的过程中只有一个str,所有都在往一个字符串str里面+=。代码即:

void _test2str(TreeNode* root, string& str)
    {
        if(root == nullptr)
        return;
        if(root->left)
        {
            tree2str(root->right);
            str += "(";
            str += ")";
        }
        else if(root->right)
        {
            str += "()";
        }
   
        if(root->right)
        {
            str += "(";
            1tree2str(root->right);
            str += ")";
    
        }

        return;
    } 
 string tree2str(TreeNode* root) {
 	string str;
    _test2str(root, str);
    return str;
};

在这里插入图片描述
在这里插入图片描述

题二:

二叉树的层序遍历
不是说直接层序遍历,而是要求把每一行放到一个中括号里面去。它这里不是打印,而是放到一个二维数组里面。所以C++里面的二维数组就用vector<vector<>>替代了。用C写得是二级指针,而且还得开辟动态二维数组,先开辟一个数组,malloc一个数组每一个类型是int*,再去malloc一个数组,挂到第一个数组的每一个int里面。你还得告诉有多少,每一行有多少个两个参数,用C太麻烦了。
》层序遍历怎么做了,就单独的层序遍历是不是很简单,一个队列就够了。但是层序遍历要把每一个放到一行该怎么做呢?你看我们有一个队列,每一层带入下一层。C++要队列有队列。你看你要一层一层控制是不好控制的,你先进去的是3,3进去以后,出来之后带谁,是不是带9 和 20,然后9出来不带,因为没有要带的。假如我就让9带一个吧,比如让他带一个7吧,你看这个地方存在两层交替的情况,两层交替什么意思呢,就是队列里面可能会同时存在两层的数据,那么你如何区分它是第几层的呢?是不是不好区分。这里有很多种方式,可以加一个层级控制,每一层的数据个数控制就可以了,加一个leveSIze。leveSize一开始是1,3进去,不要出一个进一个,而是一层出完,它是上一层带下一层嘛,你出队列是一层一层出,我们之前是出一个入一节,出一个入一节,是不是循环一直在走呀。如何控制一层一层出呢,第一层只有一个,将3出来,3出来之后把9和20带进去,levelSize是不是–,所以levelSize是不是减到0了。下一层的数据个数是不是就是下一层数据的个数。我们一开始是9出来就将它的子树即下面一层带进去,那队列里面是不是同时有两层的数据,现在呢levelSize–,就是我第二层有两个,我先出第一个9,然后我入9下面的16,这个时候停不停,不停,继续走。紧接着20出来,入15 和 7,levelSize减到0,levelSize控制每一层的循环次数,levelSize减到0才说明一层才出完。那么此时队列的个数是不是又又有了,第二层出完了,第三层是不是也带进去了。再紧接着16出,带不带值,不带值,15出带不带,不带,7出带不带,不带。这时候下一层的个数为0,那就结束了。现在我们用代码把我们的想法给实现出来。
在这里插入图片描述
》首先我们这里肯定是需要一个vector<vector>,还需要一个队列,队列里面存节点的指针,我队列是不是可以用模版呀,queue<TreeNode
> q;用C语言的话,还得去改typedef的类型,太麻烦了。我们再定一个int levelSize = 1;然后我们把root节点入进我们的队列里面去,q.push(root),当队列的个数不为空就继续入节点。while(!q.empty()),我们取队头的数据,TreeNode* front = q.front();然后q.pop()一下,把该节点的下一层入进去,但是下一层的特点是不为空我们才入进队列,所以if(front->left) q.push(front->left),右不为空,我们把右入进去,if(front->right) q.push(front->right),但是这样子的话,是不是我们同时无法控制一层一层的概念,此时我们用定义的变量levelSize控制一层一层出,是不是while(levelSize–)是不是只会走levelSize大小的次数呀,比如levelSize是第三层的数据是5个,就只会出5个数据,依次出来的同时是不是就把下一层给带进去了。那么上一层出完,下一层就都进队列里面了,那么谁就是下一层的数据个数呢,是不是就是队列的size就是数据的个数,即levelSize = q.size();队列里面的size就是下一层的个数了呀!那我们是不是提前定义一个vector levelVec,我们在q.pop()的时候是不是就可以把该front的数据入进我们的vector了,即levelVec.push_back(front->value);然后一层出完,一层出完的数据是不是都进我们的levelVect里面了,然后在while(levelSize())外面将我们的levelVec入进我们的vector<vector>里面了,即vv.push_back(levelVec)。然后我们没有考虑到树根就是空的情况,是不是判断一下,if(root == nullptr)return nullptr。以前呢是混着出,第一层混着第二层,第二层混着第三层,但是现在,我们现在通过一个变量控制,控制一层完整的出,出完一层顺便一层的数据就进入了一个vector里面去了,然后这个vector是不是就进入vector<vector<>>里面了。一层出完,下一层也都进入了队列面,那下一层的数据个数怎么知道呢,是不是队列的size()函数就可以拿到呀。是不是C++的vector<vector<>>、队列、容器函数都很方便,但是这些都是建立在你对底层还是比较清楚条件下。

vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> vv;
        if(root == nullptr)
            return vv;
        queue<TreeNode*> q;
        q.push(root);
        int levelSize = 1;
        while(!q.empty())
        {
            vector<int> levelVec;
            while(levelSize--)
            {
                TreeNode* front = q.front();
                q.pop();
                if(front->left)
                {
                    q.push(front->left);
                }
                if(front->right)
                {
                    q.push(front->right);
                }

                levelVec.push_back(front->val);
            }
            vv.push_back(levelVec);
            levelSize = q.size();
        }
        return vv;
    }

题三

二叉树的层序遍历 II
这个II要干嘛,它是不是自底向上放入,那怎么搞?是不是对我们的上面得到的结果逆置一下就可以了呀。reverse()函数是不是就可以了。

题四

二叉树的最近公共祖先
这些都是属于C++的特别经典的题目,二叉树这一块。我们先讲一个简单的。一个节点他有很多祖先但是它最近的是哪一个。就比如7和4是不是有很多的公共祖先,它们最近的是谁?2、5、3都是公共祖先,但谁是最近的?是不是2是最近的;再来一个,4和5的公共祖先是谁?4和5的公共祖先是谁,看起来是3,但是,是5!因为它题目说了,一个节点可以是自己的祖先。现在咋去做呀?这道题呢有很多种分析方法,我们在这儿就直接说结论哈。
》除了一个特殊情况,就是5和4的公共祖先是自己5,我们先来看看比较普通的。我们来看看其他的公共祖先有没有什么特征?是不是一个在我的左边,一个在我的右边就是公共祖先!其实就是走一个类似搜索树查找就可以了,但是又不完全像搜索树的查找,首先找到根,判断一下是不是我,它这里题目有前提,就是p、q两个节点都是在树里面的,且p、q不相等是不是很好判断。如果p、q就有一个是我,那我是不是就是公共祖先呀,如果p、q都不是我,怎么办?比如,p、q(p是7,q是4)都在我的左子树,那最近的公共祖先可不可能在我的右边?是不是绝对不可能,那我要到左树去找,然后跳到5节点找,p、q都在我的右边,那我是不是递归到我的右树去找,此时跳到节点2,p、q一个在左,一个在右,那我是不是就是公共祖先了。这样逻辑是不是就很清楚了,分分钟就能实现。我们来一个递归函数IsInSubTree()来判断你在不在我的子树里面,怎么判断呢,暴力查找呗 。就拿4、5这两个来说,首先根是3,然后4和5都在我的左子树,那我是不是递归到5这个左树节点去找,此时正好p就等于我root,那是不是直接return p了,即if(root == p || root = = q)return p/q;当然在此之前还要考虑到原先树就是空树if(root = =nullptr),return nullptr虽然这种情况不存在但最好也写一下。若p、q都不是我们的root节点,那说明p、q在我的子树里面,但是我不知道他们是在我的左子树还是右子树,那咋办?找呗,他一定在我的子树里面,所以这里有4种情况,bool pInLeft = IsInSubTree(root->left,p);就是如果p在我的左边那我就去我的左边找;p不是root,那么它在我的左边要么是真要么是假,如果p不在我的左边,还需不需要去我的右边找?不需要!因为题目说了p、q一定在我们的树里面,它既然一开始不是根,后面也不是左子树里面,那么它一定是在我们的右子树里面。那是不是bool pInRight = !pInLeft,是不是直接曲反就可以了。q是不是一样的逻辑。那么这里就有4种情况,肯定一个为真,一个为假,不可能都为假或者为真。if(pInLeft && pInRight || qInRight && qInLeft)那么我就是公共祖先,return root就行;那么还有两种情况,就是else if(pInLeft && qInLeft ) 那么他们都在我的左边怎么样呀,是不是递归到左子树去找呀,LowestCommonAncestor(root->left);else if(pInRight && qInRight) return LowestCommonAncestor(root->right),else理论上走不到,但是语法要满足,是不是assert(false) return nullptr;即代码是:
在这里插入图片描述

在这里插入图片描述

bool IsInSubTree(TreeNode* tree, TreeNode* node)
    {
        if(tree == nullptr)
            return false;
        
        if(tree == node)
            return true;
        else
            return IsInSubTree(tree->left, node) 
            || IsInSubTree(tree->right, node);
    }
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if(root == nullptr)//理论上是没有的,但是还是要写一下比较好
            return nullptr;
        
        if(root == p || root == q)//p、q其中有一个就是自己的公共的祖先
        {
            return root;
        }

        bool pInLeft = IsInSubTree(root->left, p);
        bool pInRight = !pInLeft;
        bool qInLeft = IsInSubTree(root->left, q);
        bool qInRight = !qInLeft;

        if(pInLeft && qInRight || pInRight && qInLeft)
            return root;

        else if(pInLeft && qInLeft)
            return lowestCommonAncestor(root->left, p, q);
        else if(pInRight && qInRight)
            lowestCommonAncestor(root->right, p, q);
        else//理论上是不会走到这里的,但是语法要满足
        {
            assert(false);
            return nullptr;
        }

        return root;
    }

我们当前的这个算法整体情况还是不差,但是面对一些极端情况还是有问题的吗,比如说一个值在左,一个值在右,还是很快的ON就行了,我们这颗树的时间复杂度是多少?有些同学会觉得有logN,走高度次就是logN,但是一定要记住,什么树才会有logN呢?是不是满二叉树、完全二叉树、平衡树是不是高度次才是logN。这棵树的复杂度是N^2。你每次去子树找是不是都要一个个去确认,然后他其实是n个节点,然后每个节点依次是查找n-1次,即为等差数列
在这里插入图片描述
》那这棵树如何优化呢?
1.如果是搜索二叉树,那么它的效率就是O(N)了,为什么搜索二叉树就是O(N)了,因为搜索二叉树不需要你跟我说的一个个去我的子树里面遍历找呢?那我是不是用大小就可以选择线路了,是不是最多只要找高度次呢?搜索树在最差的情况下,也只需要O(N)是不是。
在这里插入图片描述

》2.然后又有人提出概念,如果这棵树是三叉链呢?我们之前讲过什么是三叉链,是不是就是有父亲呀?如果还给了父亲节点好不好找?是不是非常好找了?是不是直接就是一个链表的相交了嘛,还需不要说比较大小或者遍历在左树还是在右树嘛?不需要了,直接父亲节点向上走,即链表相交问题! 那么这棵树是不是最多走高度次呀,这时候是不是也是O(N)。我们链表相交是怎么解决的?是不是先计算两边的长度,让长的先走差距步,再同时走,然后交点是不是就是公共祖先了,所以问题转换成链表相交找交集!我们不实现,但是思路一定要知道,这些思路在极端场景下是不是都能克服到O(N)呀。

》3.为什么要理解上面两个,因为我们还要进行变形,第三种:我们现在就是一个普通的二叉树,我们现在期望优化到O(N),我们上绝招。
》比如说4、6两个,刚刚有两个方法都是O(N),我肯定不能让你变成搜索二叉树,那个太难了。那我们能不能找出三叉链变形呢?我们现在没有三叉链就是普通的二叉树,能不能想办法找出节点到根的路径,如果我能找到p、q到根的路径不就能转换成链表相交了嘛,只不过他现在不是链起来的,而是用容器,数据结构存起来的。比如说我们现在搞一个栈,去存储,比如说一个pPath、qPath。我们用一个栈可以,用一个vector也可以,vector也能充当栈的功能嘛是不是。如果我们找出路径,6这个节点的路径比如说是:3、5、6,然后4的路径比如说是3、5、2、4,然后让长的先走,然后同时走。
》我们如何让我们找路径达到O(N)呢,教大家一个方法去找路径,其实就是写一个遍历的递归算法去找路径,其实就是去遍历去递归。首先我们在3,先不管3是不是,我们先找p,先把3入栈,3是不是不我们要找的节点,不是,那么开始前序递归了,那么就是往我们左子树去走,5是不是你的节点,不管是不是,先入栈。不是的话,继续递归到6这个节点,先入栈,是不是你要的节点,是,那第一个p节点是不是就找到了,然后就是retrun了,找到了就return true。然后我们再带大家怎么找q的,q也是类似找的,q有些不一样,p实在是有些简单。3先入栈,入了栈再检查是不是,是不是你要找的q节点,不是,继续递归你左子树,5先入栈,5是不是你要找的节点,不是 ,然后继续递归到左子树6,先入栈,是不是你要的,不是,继续递归,节点是空了,然后回来,回到6这个节点,回来返回的是false,为什么3、5、6都不是,却都入栈了,我们要找q,而不是你要找的节点,但是你要找的节点有可能是我的孩子或者孩子的节点,如果是这样的话,那么我就有可能是你分支路径当中的一个,所以我自己不是也要先入栈,比如说这里的3、5,先往下走,6是我的孩子的话,那我是不是它的分支呀。所以不是先入栈。现在找q的时候,已经递归到6度左子树了,有没有要找的?没有,那么是不是有可能在6的右边,但是6的右边也是空,没有。那么这个时候6就要出栈了,为什么出栈呢,6本身不是,左子树叶咩有,右子树也没有,那么6有没有可能是找q当中的一个节点呀。不可能了呀,自己、左边、右边都不是,这种情况是不是就要出栈了呀。然后我再往回走,走回到5了,5的左边有没有找到,没有找到是不是retrun了false,5的左边没有找到,是不是还得去右边找,所以它不管自己是不是先入栈,可能在它孩子里面,是不是到右子树,2那里去找,2的左边是谁,是7入栈,递归到7的左子树空,return fasle,7的右边也是空,return false然后7出栈,return false给2,再返回到2,递归到2的右子树去找,先将4入栈,入栈了以后再判断,是我们要找的,那就返回retrun true,那2出栈嘛?不出,返回retrun true给5,是不是在5度右边找到的,那5就不用出栈,5继续返回retrun true给3,q是不是在3的左子树中找的,是不是不用出栈,然后收到5返回的return true,是不是就不用去右子树去找了。是不是有些像FInd()函数,在左子树找到了就不用去右子树了,但是又有些不一样,不一样的是什么点,我们是利用了一个栈,把查找的路径给存储下来了,那么查找这个路径的时间复杂度是多少,是不是最差的情况也就是把整棵树遍历一遍,也就O(N)呀。然后我有两个路径了,此时我怎么走,是不是让长的那一个路径先走,然后同时走。因为是栈,所以后进先出,qPath中,先pop()4,然后一起同时走,2、6不相等,pop(),5、5相等,那p、q的公共祖先就是它5了。最坏的情况是不是3N,那也是O(N)。此时是普通树不怕,是极端的单边树也不怕。
》那这里的方式是由哪一个启发而来的呢,是不是三叉链启发而来的。就是找他们的交点,但是没有这个三叉链,我们需要记住的是,我们可以回溯往后走,也是能够找到路径的,其实也是一个Find()前序的过程,只是在此过程中顺便把我们的节点存在栈内部。首先我们需要一个FindPath()函数去帮我们递归将节点存到栈里面,参数呢就是root、要寻找的节点x,还有一个栈(用引用传参)。我们来把FIndPath(TreeNode* root, TreeNode* x, stack<TreeNode*>& path)写一下,其实它就是一个查找的过程,如果root == nullptr是不是就return false,如果该点节点不是空呢,不管三七二十一先将该节点入栈 ,先把节点入栈之后,就有几种情况了,第一种,如果这个节点入栈以后就和我们要找的节点相等了,那是不是就retrun true,层层的出去了,但是如果我们不相等怎么办呢,如果不相等的话先到我们的左子树去找,左边如果找到了有return true的了,还需不需要去右子树去找?是不是不用了。如果左边没有找到是不是再去右子树去找,右边找到了的话也return true。如果左右两边都没有找到,说明我刚刚root入栈,我root自己是不是要寻找节点的路径当中的?不是,即root自己本身不是要找的节点,左子树也没有要找的节点,并且右子树也没有要找的节点,那就说明root不是x路径中的节点,那么我就得出栈,然后还得返回return false;就比如寻找4,我走到了6,6自己、左子树、右子树都不是,是不是返回retrun false给5这个节点,然后5这个节点此时是不是要去右子树找,如果5点右边找到了,是不是return true层层返回就出去了。 其实就是我们以前写的Find()的前序遍历,只是在里面顺便加了一个path存储路径。我们在我们的lowestCommonAncestor()函数里面,定义两个栈,stack<TreeNode*> pPath,qPath;然后调用两次FindPath(),一个是寻找p的路径,一个是寻找q的路径;我们后面只需要让路径长的先走,然后再同时走,我们也不知道谁长谁短,我们可以搞一个比较大小的逻辑,while(pPath.size() > qPath.size())我们就循环让pPath先走差距步,while(pPath.size() < qPath.size())我拼命呢就循环让qPath先走差距步。然后后面理论上应该是一样的路径长度了,然后我们就同时走,while(pPath.pop() != qPath.pop()),然后任意返回其中一个的栈顶元素就是他们的公共祖先。 如果这个题用C语言去写的话会很烦,C++写会舒服很多。另外,我们做的题目多了之后,我们一说是链表的相交,你是不是就能理解了,虽然这棵树不是链表,但是大思路是不是不变的呀。形不似而神似。为什么不要链表去适配栈,但是不建议,因为你这里有频繁的调用size()函数,因为它得遍历,这里用vector去适配蛮好的。代码:
在这里插入图片描述
在这里插入图片描述

bool FindPath(TreeNode* root, TreeNode* x, stack<TreeNode*>& path)
    {
        if(root == nullptr)
            return false;
        
        path.push(x);

        if(root == x)
            return true;
        
        if(FindPath(root->left, x, path))
            return true;
        
        if(FindPath(root->right, x, path))
            return true;

        path.pop();
        return false;
    }

    TreeNode *lowestCommonAncestor(TreeNode *root, TreeNode *p, TreeNode *q)
    {
        stack<TreeNode*> pPath;
        stack<TreeNode*> qPath;

        FindPath(root, p, pPath);
        FindPath(root, q, pPath);

        while (pPath.size() > qPath.size())
        {
            pPath.pop();
        }

        while (pPath.size() < qPath.size())
        {
            qPath.pop();
        }

        while (pPath.top() != qPath.top())
        {
            pPath.pop();
            qPath.pop();
        }

        return pPath.top();
    }

题五:

二叉搜索树与双向链表,这道题出的非常的诡异,有人说二叉搜索树转换成双向链表多简单呀,我中序去遍历你,你是4我去创建节点,你是6我去创建一个节点 ,我遍历就是有序的嘛,我遍历一个就创建一个节点。但是这道题不是这样的,这道题有明确的说明你不能创建新节点!而是拿着这棵树去改,不能创建任何新节点。调整树中节点的指针指向,就相当于你要把树的左指针当成前驱,右指针当成后驱。这道题思路取自于一个操作叫做线索化,线索化在实际当中是没有用的,基本不会考察,这道题呢就是一个中序线索化稍微调整一下,线索化是什么呢,就是觉得二叉树不方便遍历,他要把空给利用起来,指向他的前一个和后一个,那么遍历这棵树的时候就可以直接遍历了,但是线索化在实际当中不实用,因为你插入节点后,你的线索化也得跟着改,而且很难改不好改。所以线索化在实际当中没什么用。我们来讲讲这道题。
》我们是不是走一个中序遍历就可以了呀,走中序遍历拿出来是不是有序的呀,那么每一个节点的left要指向谁?左是不是当前驱,我们走中序遍历知不知道前一个是谁?我知道,怎么就知道了呢,就像我们遍历二叉树一样我们用一个指针去记录一下我们就知道前一个是谁了呀。我们用指针去记录一下是不是就知道前一个是谁了,但是我不知道后一个是谁,不知道下一个是谁,这里是不是一个问题呀。比如说我走一个中序,定义一个cur和prev,中序访问的第一个是谁,一直往左走,第一个访问出现空的节点是4,那他前一个是空,左为空回来之后才访问的这个4,我当前节点的左就可以指向前一个prev,我cur要往回走,我先把cur给我们prev,也就是4这个节点给prev,那么此时cur为6这个节点,我6节点的左指针是不是可以指向我们的prev4这个节点了,我们6中序遍历,再走就是走到了8这个节点,那这个时候8为cur节点,prev为6这个节点,8的左指针就可以指向prev6了,然后我cur是不是再往回走,cur给到了10,prev是8 ,我10节点的左指针可以指向prev8这个节点,大家是不是发现左指针可以全部链起来,有了这么前一个变量prev是可以把左指针全部链起来的,现在有一个问题是,右指针怎么链接,我在当前位置知道前一个是谁,但是我知不知道后一个是谁呀?我不知道,我只知道你前一个是谁。我们知道昨天发生了什么,我们知不知道明天发生什么?那我们想知道怎么办,时间穿梭机是不是。所以是这么一个问题,我在当前节点可以让我的left指向前一个,我不知道的是当前节点的右节点是谁,后驱是谁。 但是我可以知道上一个节点的右是我,对不对。但前节点的左是前一个cur->left = prev,因为是中序遍历嘛,这个cur是中序遍历出现的,它每次prev = cur就能走起来,但同时我知道上一个节点prev ,我不知道明天是什么,但是我知道昨天,上一个的后继是我, 即prev->right = cur; 我在4的时候没有改4的右指针,我到6的时候就可以改4的右指针了,当我在6的时候没有改其右指针,而是当我走到8的时候才改6的右指针,我在8的时候,8的左指针指向我们6,拿上一个6的右指针指向我8这个节点,然后cur再回到10,prev = cur,prev就是8这个节点了。同时我们知道,我右不知道是谁,我改不了我的右,但是我下一个节点可以改上一个节点的右指针。理解了这一点就简单了,但是写起来会有些抽象。
在这里插入图片描述
我们写一个中序遍历就行了InOrederConvert(),但是我们不是为了写中序遍历,而是为了转换。我先定义一个cur变量,按我们前面讲的,如果cur – nullptr,那我就return,不为空NULL,那我递归走我左子树InOrederConvert(root->left),然后cur在InOrederConvert(root->right)之间是不是就中序呀,那我们现在还需要前一个节点,那我们怎么拿到钱一个节点呢?我们先提前在Conver()里面定义一个节点prev,TreeNode* prev = nullptr就可以了,传给我们的InOrederConvert(cur,prev),cur出现的顺序就是我们中序遍历的顺序4、6、8…那我们就可以做一件事情了,第一次是4,prev就是空NULL了,所以cur->left = prev;但是上一个prev为空NULL,所以我们要判断一下,直接让上一个prev右指针指向我就入坑了。所以if(prev),prev->right = cur;那我cur要更新,要往下一个节点走,不管它递归什么的,反正下一个不为空的节点就是新的一个了,那我就得先在递归之前就把自己cur格我们的prev,即prev = cur,但是这是在递归的过程中去给的,那你这样给的话,这个地方的改变可能又递归回来,就比如6递归完右子树8了就会返回到10,因为6是10左子树递归到的,所以6这棵树全部递归完会回到10,此时你的cur确实是10但是你的prev就还是原来的值为空NULL,prev没变,你cur是随着你的节点而变化,而prev就不一定了,所以我们得要prev的值随节点的变化则用引用传参!就是你全程prev只有一个,会随节点的变化而变化InOrederConvert(TreeNode* cur, TreeNode*& prev);
》可以再画一个图理解一下:最开始是10,10不是空先递归它的左,不是空递归左到6,不是空递归左到4,4不是空再递归到左,为空了就返回retrun回来了,所以cur第一次出现是在4这个节点位置,那是不是就让4的左指向空了呀,前一个节点是空嘛,这个时候把prev给cur的左指针,然后cur赋值给我们的prev,如果你不给引用,4节点这层的prev会不会改变下一层6这个节点的prev,是不是不会呀,所以要给引用传参!cur当前为4,继续递归它的右子树 ,4的右是空就回来了,然后退回到6这个节点,它cur的出现不是说在循环里面出现而是在栈里面出现,不同的调用里面出现,第一次cur出现是4,第二出线就是6了,然后此时就是让cur的左指针指向4,cur->left = prev,prev即4节点的右指针指向cur,prev->right = cur;此时再把6节点赋值给我们prev,prev = cur,然后继续往下走,递归6的右子树是8,所以prev一定要用引用,你4节点回到6这一层了,你prev肯定要是4,因为在4那层prev就已经被改了,你不用引用否则就不能拿到改过的值了。 递归4、6、8、10是不是在不同的栈帧里面呀。大家画递归展开图会更好的理解过程。
》此时我们就要返回链表的第一个节点了呀,我怎么返回呢?这个时候根节点是谁?是不是还是10,没有发生变化呀,就是你这个节点的左右指针指向被改了,但是我还是指向你10这个节点的,所以我们可以从10这个根节点开始往前走对不对。但是就目前写到这为止还不够,因为还要想到空树的情况,可以在前面单独处理if(root == nullptr) 那就直接返回return nullptr,不执行下面的代码了。他其实就是递归的过程中走了一前一后的两个节点 过程,然后更新我们的左右指针,我的左指向我的前一个,但是我无法知道我的右指针即后面一个,但是呢下一个节点是不是可以帮助我们改一下我的右指针呀。

class Solution {
public:
	void _InOrederConvert(TreeNode* cur, TreeNode*& prev)//要用引用
	{
		if(cur == nullptr)
			return;
		
		_InOrederConvert(cur->left, prev);
		cur->left = prev;

		if(prev)
			prev->right = cur;

		prev = cur;


		_InOrederConvert(cur->right, prev);
	}
    TreeNode* Convert(TreeNode* pRootOfTree) {
		if(pRootOfTree == nullptr)
			return nullptr;
        TreeNode* cur = pRootOfTree;
		TreeNode* prev = nullptr;
		_InOrederConvert(cur, prev);

		while(cur->left != nullptr)//返回头节点
		{
			cur = cur->left;
		}
		return cur;
    }
};

在这里插入图片描述
在这里插入图片描述

题六:

从前序与中序遍历序列构造二叉树:这道题是用前序和中序重建这棵树。那么前序和中序是如何重建这棵树呢,我们是讲过,前序和中序是能够重建,中序和后序能够唯一确定一棵树,给的值没有重复可以唯一。前序和后续能不能重建呢,不一定。这道题里面为什么能够重建这棵树呢,因为这里的前序序列和中序序列呢,先回顾一下前序和中序,前序是什么:根、左、右;中序:左、根、右。大家仔细想想这里面能够做一些什么东西呢,前序能干嘛,是不是可以确定根;中序可以干嘛,是不是可以划分左右区间呀!有了根之后就可以划分左右区间了。前序的第一个值一定是整棵树的根,那么下一个值一定是不是左子树的根呢,不一定。因为左子树有可能为空,如果不为空一定是左子树的根,那我们能不能确定9就是左子树根或者不是左子树的根呢?可以,因为中序可以根据根划分左右子树区间,如果左子树不为空,那么9一定是左子树的根,如果说3左子树为空,那么9一定不是左子树的根。我们是不是可以一次根据根就能前序重建树了。
》我们来看看:3可以确定整棵树的根,那么中序就可以划分左子树区间和右子树区间,那怎么办呢?我们先不用管右树,先管左树,因为前序是根、左子树、右子树,我们知道根之后就创建左子树,左子树创建完了前序是不是自然就去创建右子树了。所以我们根据划分出来的左区间呢去建立左子树,我们例子给的是,左区间只有一个值9,是不是就能确定左子树只有一个9呀,我们就要再去构建9这个节点的左右子树,但是其左右子树都没有,为什呢?因为你可以看中序,它的左区间有吗?没有,9的右区间有吗?没有。那么9的左右子树肯定都是空nullptr嘛。那么9这棵树就建立好了,然后链到3的左子树上面。3的左子树创建好了是不是可以创建右子树了呀,3的左子树建立好了,自然就走到了右子树上面去了,那么20肯定是右树的根,所以赶快创建20这个节点。20创建好了,就用中序来确定左右区间,然后去创建20的左子树,确定好之后只有一个值是15那就创建15,确定15之后,看到中序里面15的左子树没有,右区间也没有,那其左右都为空。假如我们改改,15的右区间有一个16的值,那么这个时候还能确定什么呢?确定15这个根以后,那么划分15的左右区间,左边没有就为空,其右区间有一个16,那是不是递归去创建16这棵树了呀,那是不是走到16把起创建出来,就是依次依次往下面确认了。就是我前序挨个确定根之后,然后结合中序依次去递归创建左右子树,我们就是添加一个16做一个例子,现在抹掉16哈,那就15点左右区间都没有值了,说明其左右都是空nullptr,然后返回链到20点左边。20的左树创建好了,是不是就去创建右树了,是不是前序就走到7了,创建20的右树是不是就拿中序里面20的右区间去创建右树。去创建7这个节点,然后去看7的左右区间,如果有的话继续向下递归创建,但是左右都没有,那其左右都为空nullptr,然后7这个节点链到20的右边。那么20的左右子树都创建好了,就返回链到3点右边,是不是整个都好了呀。也就是说我们通过构建过程,前序去确定子树的根,确定根之后要不要去创建起左右子树呢,那就用中序划分的区间来确定。我确定好根了,是不是就可以确定左右区间了呀,左区间为空那就是nullptr,不为空那就递归创建左子树。
》思路上呢好像不是很难,但还是有些难度的。这个题呢我们得套一下子函数,因为这个参数不是很方便我们去管理,所以我们得套一个子函数,子函数需要什么呢?是不是需要前序区间和中序区间呀,前序区间呢我们需要一个prei,int prei;就是我们前序走到哪个位置了,前序是不是只有一个位置从开始走到末尾呀,前序的每一个位置我们确定根,中序是用来确定区间的。我们每次递归都是需要前序去确定一个位置即一个节点,辅助中序去确定一个区间。所以说中序我们需要inBegin和inEnd,int inBegin和inEnd。然后前序这个下标需要一直往下走的,那这个过程用来递归,那一直往下走,前序pRei要一直往下走,但是inBegin、inEnd不需要,它是通过传值就行,那么每次创建再++往下走,那最方便的话给一个引用,这个我们待会儿再来分析也行。
在这里插入图片描述

》我们定义一个子函数,_buildTree()前序的下标是从0开始的,中序的左右值是0和end = inorder.size()-1;即buildTree(preorder, ignorer, pRei, inBegin, inEnd);我们对这图来走这个逻辑过程。什么时候为空就不用创建了?当区间不存在的时候是不是就不用创建了,是不是中序区间来确定这些东西呀。假设没有给9,那么左区间就不存在,我创建了3,我拿这个3去中序里面确定根的位置呀,根的位置呢是0.,我去递归创建3的左子树,3的左子树能确定出来嘛?不能,因为3的左区间有区间吗?现在创建出来的就是不存在的区间,我们要0这个位置的左区间,[inBegin. rooti-1]inBegin是0,然后rooti是多少,是不是我们3这个根的位置为9呀,那么0-1是不是-1,是一个不存在的区间呀,即inBegin > inEnd了,当然我们只是假设没有9这个节点,我们赶紧还是把它弄回来,我们可以先不考虑这个不存在的区间哈。我们是不是先确定根之后去创建根呀,是不是TreeNode* root = new TreeNode(preorder[pRei]),然后++pRei,因为已经创建好了。然后 我们现在要划分左右子树区间,如何去划分呢?是不是通过中序去划分,先去中序里面找到根的位置。我们拿到3了,并且也找到了其在中序的位置了,那么我们是不是要定义一个根的位置rooti,即size_t rooti = inBegin;while(rooti <= inEnd())遍历去找rooti嘛while里面是不是if(root->val == inOrder[rooti])那是不是就break呀,else 是不是就rooti++。找到之后是不是就能划分左右区间去了呀。此时根为3这个节点的左区间是谁,是不是【inBegin,rooti-1】;右区间是不是【rooti+1,inEnd】;那我们是不是可以递归去创建左右子树了呀。左子树有没有必要创建呢,我们可以去判断,也可以去依赖递归条件判断。就比如说我们刚才假设没有这个9,那么【inBegin,rooti-1】左区间是一个不存在的区间,因为是【0,-1】;不管怎么样我们做一个简单的事情,我们不管有没有,我们root->left = _buildTree(preorder, ignorer, pRei, inBegin, rooti-1),我们递归去创建左子树,万一左区间不存在呢,我们是不是可以提前在一开始就判断一下,if(inBegin > inEnd)是不是就直接return nullptr(也就是说其左区间根本不存在,是不是用的递归条件来判断了。)是不是左区间不存在,就直接返回空了,return nullptr,如果存在的话就去继续递归创建根然后连接到我的左边。然后我们再走一下右边,左边创建好了是不是自然就走到右边了,root->right= _buildTree(preorder, ignorer, pRei, rooti+1, inEnd),整个创建好之后我们是不是把root返回,return root。是不是就这么一回事就解决了。
》代码;

TreeNode* _buildTree(vector<int>& preorder, vector<int>& inorder,
     int& pRei, int inBegin, int inEnd)
     {
         if(inBegin > inEnd)
            return nullptr;
            
         int rooti = 0;

         TreeNode* root = new TreeNode(preorder[pRei]);

         ++pRei;

        while(root->val != inorder[rooti])
        {
            rooti++;
        } 

        root->left = _buildTree(preorder, inorder, pRei, inBegin, rooti-1);

        root->right =_buildTree(preorder, inorder, pRei, rooti+1, inEnd);
        return root;
     }
        
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
    int pRei = 0;
    int inBegin = 0;
    int inEnd = inorder.size() - 1;

    return _buildTree(preorder, inorder, pRei, inBegin, inEnd);
    }

》我们来画画图理解递归过程。递归我画一部分,感受一下。最开始inBegin和inEnd是0和4,前序第一个值一定是根,那么我们创建好这个节点,然后前序的下标pRei就++到1去了,仅仅加到1还没用,还得去划分左右区间,划分左区间是什么呢,是不是【0,0】呀,右区间是谁,是不是【2,4】然后是不是就是往下递归去创建我3的左右子树呀,我们只走一部分过程,我们的pRei是1,左区间是【0,0】,然后inBegin <= inEnd是不是可以执行下面的代码去创建9这个节点呀,否则是不是返回空return nullptr呀。9创建好之后,我们是不是又去划分区间呀,左区间是不是【0,-1】呀,我们传入inBegin和inEnd其实就是帮我们确定好当前根所在的位置,它的最左边已经固定好了,最右边其实也已经固定好了,在划分的时候不过就是传rooti-1和rooti+1,你自己好好理解一下。9的左区间是不是【0,-1】,右区间是不是【1,0】呀都是inBegin > inEnd然后是不是都是返回return nullptr。现在9这棵树是不是创建好了呀,然后返回给3这个节点,则3的左子树创建好了,然后去递归创建3的右子树,右边怎么创建,是不是你的pRei不断的往下走,是不是oRei加到了2这个下标位置了。我们是不是用了pRei的引用,pRei值上一层是不是会传给下一层呀,这个引用是不是用的很关键呀!我们递归创建3的右树是,是不是pRei是2,那此时对应的就是20这个值,那么20是不是就是我们下一个要创建的根呀,然后再找到20对应在中序里面的位置rooti,加到3,又去划分20这个节点的左右区间,这个区间肯定会被缩小到,后面一定会返回,因为它会被缩小到不存在,左右区间确定15,然后15点左右区间都不存在,然后15返回链到20点左子树,然后再去递归20的右边,是7,7确定好之后,其左边是空,右边也是空。那么他其实就是一个前序创建的过程。和我们之前讲的用#标识空节点那道题目一样,那个是遇到空就返回,这个是区间不存在就返回。然后我前序就去创建这棵树。创建好了是不是就好了,只不过它会返回链到上一个节点。
在这里插入图片描述
》另外一个题目就是中序和我们的后续构建二叉树,那是不是和我们前序和中序完全相似呀,只不是之前是前序确定根,现在是后续确定根,后续是左子树、右子树、根,只不过你这个3为整棵树的根创建好了,你不能马上去创建左子树,而是去先去创建右子树,拿中序的右区间去创建右子树。因为你后序是左、右、根,你从尾部确定根,依次是、根、右、左,是不是先去确定的是右子树的根呀!

题七:

根据一棵树的中序遍历与后序遍历构造二叉树

题八:二叉树的非递归遍历!

二叉树的前序遍历:非递归不好搞,非递归的话前序中序和后序相似,但是好在理解其中一个另外两个都好理解。对大厂有追求的话,非递归内容是一定要掌握的。
》非递归要求从学习的角度,深一点的话是要去掌握非递归的,因为递归是存在问题的对于二叉树而言,会存在深度太深,栈会溢出,我们学了linxu就知道栈不大,深度太深,栈可能就爆了。基于这个呢,我们得掌握非递归,简单的递归我们能改,该循环就行,比如说就是斐波那契数列,稍微复杂一点的呢,归并改为循环难在边界。再复杂一点的呢就是快排改成非递归。二叉树的非递归更恶心
在这里插入图片描述
》我们先来看前序,前序:根、左子树、右子树。递归呢就是套娃,子问题套子问题,比如说这棵树根、左子树、右子树,左子树在怎么办,左子树是不是又被套娃,根、左子树、右子树,子问题嵌子问题。那我们这里要改为非递归,就保存区间就可以了嘛?像快排那里还好控制,快排为什么好控制呢,单趟确定了key、左区间、右区间,左区间、右区间入栈,你只需要将区间保存起来就可以了,这个二叉树非递归,你是要回溯的。那么这棵树呢我们怎么去划分呢,记住,这里的非递归呢用到了特殊的方式,我们把任何一棵树,无论是前序、中序、后序的非递归都是一样的,我们把它分成两个部分1.一个部分分成左路节点;前中后不过就是时机问题嘛**2.第二部分呢叫做左路节点的右子树。就比如我们拿这个图来说,前序是什么,根(8)、左子树(3),根(3)、左子树(1),根(1)。是不是就这样去访问的,8、3、1,我们圈起8、3、1是不是就是左路节点、左路节点的右子树。当1这个左子树被访问完了,是不是就是右子树了,右子树完了,是不是1就完了,然后返回到3,3的左子树完了,就去3的右子树,3的右树完了,就是返回到8,8的左子树完了,就是8的右子树。也就意味着,任何一棵树按照这样的角度去看的话,我们是可以理解成,只要当他的左路节点访问了,其实空不用访问,它就只用访问左路节点的右子树了。那么左路节点的右子树怎么去访问呢?是不是转换成循环子问题去访问,怎么就转成访问的子问题,就是再拆解成一个自己,我们右子树要当成什么去访问呢?右子树当成子树去访问嘛,子树怎么访问,是不是继续当成字问题,是不是也是拆解成1.左路节点;2.左路节点的右子树。空不用访问是为什么呢,相当于走到空就结束了嘛。
》我们从代码配合图来理解一遍:我们需要一个vector去存储序列,vector v,以前C语言是遍历这棵树算好大小再走。我们还需要一个栈,stack<TreeNode
> st;while()循环什么时候结束我们暂时不知道,在这个过程中我们要遍历左路节点,那么我们再定义一个cur来遍历,TreeNode
cur;先遍历左路节点并且左路节点先入栈,那是不是还得来一个while循环,while(cur),cur不为空是不是一路遍历到为空,是不是左路就被存储下来了呀,v.push_back(cur->val);然后节点还需要入栈,st.push(cur);为什么需要入栈呢,我们第一个遍历的左路节点是8、3、1 ,为什么遍历左路节点其实还蛮好理解,前序:根、左、右嘛,为什么入栈呢,也是因为前序嘛,比如1这个节点,1入了,然后左为空,然后是不是要回溯到1的右子树呀,那是不是1就完了,1就要回溯到3,然后开始访问3的右树了,然后3点右树也完了,是不是3和3点左右树都完了,就回溯到8了,开始遍历8的右树了呀,所以你还要依次取31、3、8的右树。我们while(cur)左路节点遍历完了,是不是就要依次取栈顶左路节点然后去遍历它的右子树,TreeNode* top = st.top();st.pop();那怎么访问右树呢,关键来了,其实访问左路节点的右子树—是通过子问题的方式,如何通过子问题呢?cur top->right,cur循环开始 就把树分成了两个部分左路节点与左路节点右子树。子问题怎么去访问的,前面说了,你是怎么访问8这棵树的就怎么访问你左路节点的右子树的树了。此时1被取出来了,是不是去访问它的右子树了呀,但是空NULL,那就不会入栈,然后再去取栈顶的节点3, 那么此时cur就是指的的3的右节点6,然后是不是又进入我们写的while(cur)循环里面了呀,所以把6这个节点入栈,然后是要顺着6取访问6这棵树的左路节点的,但是6的左为空NULL,那是不是6这棵树的左路节点访问完了呀,那是不是又到了6这棵树的右子树了呀,6的右子树只有一个就是7,那是不是又到了7这棵树了呀,是不是7这棵树的左路节点入栈,然后左为空,去取7的右子树,也为空是不是就将7出栈,是不是7这棵树访问完了,也就6的右子树完了,6也出栈,6这棵树访问完了,是不是3的右子树完了,是不是3也要出栈了… 我们遍历左路节点while(cur)------左路入栈,这里是不是访问任何一棵树的开始呀!我们再从栈顶取出原先保存的左路节点,然后将其右节点入栈,是不是又一棵树的左路节点需要我们遍历,是不是又回去执行我们的while(cur)了呀!就这么循环往复的去执行。任何一棵树,最开始的一棵树,我拆成左路节点和左路节点右子树;然后再一棵子树,是不是也是拆成左路节点和左路节点的右子树呀… 那什么时候结束呢?是不是cur为空NULL或者栈st不为空是不是得继续呀!
》这个非递归是不是很抽象呀,虽然它不是递归,但是它也嵌套着子问题,在某种思路上也是用递归的思想来搞定的。代码:

vector<int> preorderTraversal(TreeNode* root) {
        vector<int> v;
        stack<TreeNode*> st;
        TreeNode* cur = root;
        while(cur || !st.empty())//想继续的条件
        {
            while(cur)//访问一棵树(包括子树也其实是一棵树)的开始
            {
                v.push_back(cur->val);
                st.push(cur);
                cur = cur->left;
            }
            TreeNode* top = st.top();
            st.pop();
            cur = top->right;
        }
        return v;
    }

在这里插入图片描述

题九:二叉树的非递归遍历!

二叉树的中序遍历,非递归
中序是不是取出来的时候再访问这个节点的值呀。什么时候一个树的左路节点访问完了,第一个被访问完左树的是谁,是不是1,比如说我们要把1的右子树访问之前是不是要拿到1,当1这棵树访问完了,是不是要将3从栈里面拿出来,它的左子树访问完了是不是访问3然后再访问它的右子树呀…大家可以想到一节点从栈里面出来,它的左子树是不是就访问完了。

vector<int> inorderTraversal(TreeNode* root) {
        vector<int> v;
        stack<TreeNode*> st;
        TreeNode* cur = root;
        while(cur || !st.empty())
        {
            while(cur)
            {
                st.push(cur);
                cur = cur->left;
            }
            TreeNode* top = st.top();
            st.pop();
            v.push_back(top->val);
            cur = top->right;
        }
       return v;
    }

在这里插入图片描述

题十:二叉树的非递归遍历!

二叉树的后序遍历:后序是比较麻烦的,前序是入栈之前就访问,中序是从栈里面拿出来的时候访问,那么后序怎么搞呢?是不是左右子树都访问完了,才访问这个节点,那什么时候左右节点都访问完了呢?从栈里面去出来是不是意味着左子树完了,左子树完了能访问他吗,不能,除非它的右子树访问完了。其中一种它的右子树访问完的标志是什么呢,那就是其右子树为空,所以这棵树的1是可以直接访问的。
》还是一样的,先把左路节点入栈,我们从栈里面拿出1的节点,能不能访问,可以,因为它的右子树为空;那么下一个将3从栈里面取出来,能不能访问?不能,那我们就访问3的右,我们简化一点,把6下面的7这个节点抹掉。一样的嘛,将其右节点6入栈,然后其左为空,从栈中取出节点6,能不能访问?可以,因为它的右为空嘛。那后面是不是第二次取到3呀,那能不能访问3呢?你怎么知道能访问了。你前面取到能访问的标志是什么?是不是它的右为空呀,就能直接访问。那此时我3这个节点怎么知道它右节点已经访问了呢?
》我们第一次从栈顶取出3,3的右不为空,先访问3的右子树,再访问3,这是第二次取到3,那么我们要如歌区分3的右子树已经访问过了呢?有人说,我们把访问过的节点置为空,这可不行,你破坏了这棵树。怎么判断呢?这里有一个非常明显的标志,第一次到3的时候,3的右还没访问,它的上一个访问的节点是谁? 上一个访问的节点肯定不是3的右子树的根,是1!那这个vector里面存的back1是不是就它呀!你看第一次拿出3,上一个访问的节点不是我右子树的根,那我没有访问是不是必须得访问呀,第二次把3的右子树访问完了回来的时候再到3,3的右不空,3的右不为空怎么知道他已经访问过了呢?如果上一个访问的节点是3右子树的根,大家想想,后序遍历,上一个访问的一定是谁,你现在访问的是3,左子树、右子树、根,这个右子树里面最后被访问的是谁,右子树是不是也是按照,左子树、右子树、根这样的,是不是最后被访问你的一定是6这个根呀。所以,如果上一个访问的节点上是3的右子树的根,那么说明3的右子树已经访问过了就可以访问3了。但是这里不能用vector来搞,你如果用vector来记录这东西呢会有一个隐患,怕会有重复的值,因为我vector里面存的是int呀,你不能拿值去比而是拿节点的地址去比较!所以我们在这里做一件什么事情就可以了呢?其实很简单,我们在这里搞一个跟刚才一样的思路就可以了,我们不用vector来记录,我们记录一个叫prev的指针就可以了。prev指针是1这个节点,访问完6之后呢,prev是不是6这个节点的地址呀,哪怕你有重复值也不怕呀,因为地址是唯一的呀。
》我们用代码实现一下,和前面以前,遇到一棵树分为左路节点和左路节点的 右子树,先将左路节点入栈,st.push(cur);cur = cur->left;那么现在去取栈顶的左路节点,我们还不敢现在去poo哈,第一个我们在这里取到的是1,但是只是取到它,还不敢现在pop()掉,我们取到它,只是它的左路节点已经访问了完了,那我们能不能访问这个节点了呢?如果它的右为空,我们是不是就可以访问这个1节点了呀,因为它的右为空呀,这个节点同时可以出栈了,它访问完了它是不是属于上一个被访问完的节点呀,是不是得把它的地址给我们的prev记录下来呀!所以prev = st.top();else如果有节点不为空,且右子树还没访问,那么右节点的树迭代访问。那我们如何迭代访问呢?我们刚刚取完1访问完了,现在是取到3了,第一次取到3,3 的右不为空,我们是不是右节点为空top()->right == nullptr 或者 我右节点已经访问过了,即top()->right = =prev是不是就可以访问该节点了呀!否则就是cur = top->right,然后就开始新的这棵6的树了,分为左路节点和左路节点的右子树呀,循环起来了对不对!6的这棵树访问完了,是不是prev被改成指向6这个节点的指针了,此时再回去取到3这个节点,经过判断是不是top()->riight = =prev是不是等于上一个访问节点是不是就可以访问该节点了呀!是不是就走起来了呀!
》是不是大思路都是将一棵树分为左路节点和左路节点的右子树呀。无论你右子树多么复杂都会被拆成所说的两部分。

vector<int> postorderTraversal(TreeNode* root) {
        vector<int> v;
        stack<TreeNode*> st;
        TreeNode* cur = root;
        TreeNode* prev = nullptr;
        while(cur || !st.empty())
        {
            while(cur)
            {
                st.push(cur);
                cur = cur->left;
            }
            TreeNode* top = st.top();
            if(top->right == nullptr || top->right == prev)//第二次拿到,cur还是空的,所以它自己会再去执行while(cur)的时候不符合条件会自己再去取栈顶元素的
            {
                v.push_back(top->val);
                st.pop();
                prev = top;
            }
            else//第一次拿出
            {
                cur = top->right;
            }
        }
        return v;
    }

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值