本文章纯属是根据最近所学记录的笔记,如果相关的资料或者文档有侵权,麻烦请联系我一下。当然,如果文章中有什么不对的地方,或者有更好的方法,请不吝赐教。
Morris序遍历二叉树
1、Morris序的过程
参考左神视屏:(从14:34开始的)
2、代码
void Mid_Morris(TreeNode* root) {
TreeNode* Cur = root;//当前的节点
TreeNode* Right_Most = nullptr;//左树最右的节点
//此树是空的二叉树
if (root == nullptr) {
return;
}
//循环结束的标志:就是找到Cur是nullptr。要是还有节点的话,会在前面就把右节点改掉。指向前面的节点
while (Cur) {
//Morris序打印
//cout << Cur->val << '\t';
//要找左树的最右节点,得先找到左树
Right_Most = Cur->left;
//说明此时是有左子树
if (Right_Most != nullptr) {
//找到最右节点
while ((Right_Most->right != nullptr) && (Right_Most->right != Cur)) {
Right_Most = Right_Most->right;
}
//情况1:找到最右节点退出来的:需要将指针指向当前的Cur
if (Right_Most->right == nullptr)
{
Right_Most->right = Cur;
}
//情况2:找到了Cur,说明已经改过一次节点了,需要将节点重新置为nullptr,且将当前的Cur = Cur->right;并使用continue结束此次循环。这样就不会执行下面那句Cur = Cur->left。
else if (Right_Most->right == Cur)//第二次来到节点
{
Right_Most->right = nullptr;
Cur = Cur->right;
continue;
}
Cur = Cur->left;
}
//无左树,直接找右树
else {
Cur = Cur->right;
}
}
}
3、Morris遍历二叉树
3.1 中序遍历
Morris算法中序遍历二叉树:有左树的,在Morris序中都会出现两遍,而,中序遍历的的关键就是,出现两次的,打印第二次。只出现一次的,就直接打印
void Mid_Morris(TreeNode* root) {
TreeNode* Cur = root;//当前的节点
TreeNode* Right_Most = nullptr;//左树最右的节点
//此树是空的二叉树
if (root == nullptr) {
return;
}
//循环结束的标志:就是找到Cur是nullptr。要是还有节点的话,会在前面就把右节点改掉。指向前面的节点
while (Cur) {
//Morris序打印
//cout << Cur->val << '\t';
//要找左树的最右节点,得先找到左树
Right_Most = Cur->left;
//说明此时是有左子树
if (Right_Most != nullptr) {
//找到最右节点
while ((Right_Most->right != nullptr) && (Right_Most->right != Cur)) {
Right_Most = Right_Most->right;
}
//情况1:找到最右节点退出来的:需要将指针指向当前的Cur
if (Right_Most->right == nullptr)
{
Right_Most->right = Cur;
}
//情况2:找到了Cur,说明已经改过一次节点了,需要将节点重新置为nullptr,且将当前的Cur = Cur->right;并使用continue结束此次循环。这样就不会执行下面那句Cur = Cur->left。
else if (Right_Most->right == Cur)//第二次来到节点
{
cout << Cur->val << '\t';
Right_Most->right = nullptr;
Cur = Cur->right;
continue;
}
Cur = Cur->left;
}
//无左树,直接找右树
else {
cout << Cur->val << '\t';
Cur = Cur->right;
}
}
}
3.2 前序遍历
Morris算法前序遍历二叉树:有左树的,在Morris序中都会出现两遍,而,前序遍历的的关键就是,出现两次的,出现第一次就直接打印
void Pre_Morris(TreeNode* root) { TreeNode* Cur = root;//当前的节点 TreeNode* Right_Most = nullptr;//左树最右的节点 //此树是空的二叉树 if (root == nullptr) { return; } //循环结束的标志:就是找到Cur是nullptr。要是还有节点的话,会在前面就把右节点改掉。指向前面的节点 while (Cur) { //Morris序打印 //cout << Cur->val << '\t'; //要找左树的最右节点,得先找到左树 Right_Most = Cur->left; //说明此时是有左子树 if (Right_Most != nullptr) { //找到最右节点。注意注意!!!要先判断Right_Most是否为NULL,才能访问它的左节点和右节点 while ((Right_Most->right != nullptr) && (Right_Most->right != Cur)) { Right_Most = Right_Most->right; } //情况1:找到最右节点退出来的:需要将指针指向当前的Cur if (Right_Most->right == nullptr) { cout << Cur->val << '\t'; Right_Most->right = Cur; } //情况2:找到了Cur,说明已经改过一次节点了,需要将节点重新置为nullptr,且将当前的Cur = Cur->right;\ 并使用continue结束此次循环。这样就不会执行下面那句Cur = Cur->left。 else if (Right_Most->right == Cur)//第二次来到节点 { Right_Most->right = nullptr; Cur = Cur->right; continue; } Cur = Cur->left; } //无左树,直接找右树 else { cout << Cur->val << '\t'; Cur = Cur->right; } } }
3.3 后序遍历
翻转链表
返回值为啥是指针?因为,需要将root指向头结点,要不然在内存中是无效的。因为没有头结点的话,翻转完了找不到这个链表到底在哪里。当然,如果知道最后那个元素的地址可以不用,即翻转完之后的头结点。TreeNode* Reverse_List(TreeNode* root) { //记录下下个节点地址 TreeNode* pre = root; //记录链表下一个节点地址 TreeNode* cur = nullptr; //空链表 if (root == nullptr) { return NULL; } //cur抓取到下一个节点的地址 cur = root->right; //此时可以将pre->right置nullptr,第一个需要置nullptr。 root->right = nullptr; while (cur != nullptr) { pre = cur->right; cur->right = root; root = cur; cur = pre; } return root; }
Morris算法后序遍历二叉树:有左树的,在Morris序中都会出现两遍,而,
后序遍历的关键:在第二次来到时,反向打印右边界。注意不是打印当前节点。是打印边界,还是反向打印
最后就是将整棵树的右边界打印一下。void Last_Morris(TreeNode* root) { TreeNode* Cur = root;//当前的节点 TreeNode* Right_Most = nullptr;//左树最右的节点 TreeNode* New = root;//当前的节点 TreeNode* New1 = root;//当前的节点 //此树是空的二叉树 if (root == nullptr) { return; } //循环结束的标志:就是找到Cur是nullptr。要是还有节点的话,会在前面就把右节点改掉。指向前面的节点 while (Cur) { //Morris序打印 //cout << Cur->val << '\t'; //要找左树的最右节点,得先找到左树 Right_Most = Cur->left; //说明此时是有左子树 if (Right_Most != nullptr) { //找到最右节点 while ((Right_Most->right != nullptr) && (Right_Most->right != Cur)) { Right_Most = Right_Most->right; } //情况1:找到最右节点退出来的:需要将指针指向当前的Cur if (Right_Most->right == nullptr) { Right_Most->right = Cur; } //情况2:找到了Cur,说明已经改过一次节点了,需要将节点重新置为nullptr,且将当前的Cur = Cur->right;\ 并使用continue结束此次循环。这样就不会执行下面那句Cur = Cur->left。 else if (Right_Most->right == Cur)//第二次来到节点 { Right_Most->right = nullptr; //此时将链表翻转一下 New = Reverse_List(Cur->left); New1 = New;//保存翻转完之后的链表 while(New) { cout << New->val << '\t'; New = New->right; } //翻转回来 New1 = Reverse_List(New1); Cur = Cur->right; continue; } Cur = Cur->left; } //无左树,直接找右树 else { Cur = Cur->right; } } //最后还要单独打印一下整棵树的右边界 New = Reverse_List(root); New1 = New; while (New) { cout << New->val << '\t'; New = New->right; } //翻转回来 New1 = Reverse_List(New1); }
二叉树遍历方法
1、递归
//中序遍历
void Midorder(TreeNode* root)
{
//遍历到最后的节点
if (NULL == root)
return;
Midorder(root->left);
cout<< root->val;
Midorder(root->right);
}
//前序遍历
void Preorder(TreeNode* root)
{
if (NULL == root)
return;
cout << root->val<<'\t';
Preorder(root->left);
Preorder(root->right);
}
//后序遍历
void Lastorder(TreeNode* root)
{
if (NULL == root)
return;
Lastorder(root->left);
Lastorder(root->right);
cout << root->val << '\t';
}
2、使用stack的方式
中序遍历
vector<int> inorderTraversal(TreeNode* root) {
int flag = -1;//记录当前stack里面有多少元素。 其实在C++里面完全没有这个必要去维护这样一个变量。可以使用arr.empty()判断里面是否为空
TreeNode* left = root;
TreeNode* pMove = root;
TreeNode* stack[10] = { root };//自己手动创建的一个stack空间。但是有stack<TreeNode*> stk//自动创建一个stack空间
vector<int> arr = {};
if (root == NULL)
return arr;
//找到最左边的数
do{
while (left != NULL)//用的是left不是left->left,因为,如果刚开始就是NULL的话。这样的访问就是无效的!!!
{
stack[++flag] = left;
left = left->left;
}
pMove = stack[flag];
cout << pMove->val<<'\t';
arr.push_back(pMove->val);
flag--;
left = pMove->right;
if (left != NULL)//和上面那个while语句重复了。使用的do...while加上while。表示两个都要满足。但是如果只用一个while的话。while((left != NULL)||(flag ! =-1))就可以了。
{
stack[++flag] = left;
left = left->left;
}
} while (flag != -1);
return arr;
}
前序遍历
vector<int> preorderTraversal(TreeNode* root) {
int flag = -1;//记录当前stack里面有多少元素。
TreeNode* left = root;
TreeNode* pMove = root;
TreeNode* stack[10] = { root };//自己手动创建的一个stack空间。但是有stack<TreeNode*> stk//自动创建一个stack空间
vector<int> arr = {};
if (root == NULL)
return arr;
//找到最左边的数
do {
while (left != NULL)//用的是left不是left->left,因为,如果刚开始就是NULL的话。这样的访问就是无效的!!!
{
stack[++flag] = left;
cout << left->val << '\t';
arr.push_back(left->val);
left = left->left;
}
pMove = stack[flag];
flag--;
left = pMove->right;
if (left != NULL)//和上面那个while语句重复了。使用的do...while加上while。表示两个都要满足。但是如果只用。一个while的话。while((left != NULL)||(flag ! =-1))就可以了。
{
stack[++flag] = left;
cout << left->val << '\t';
arr.push_back(left->val);
left = left->left;
}
} while (flag != -1);
return arr;
}
后序遍历(自己写的)
后序遍历相较于前序和中序都稍微难一点,现接触到的主要的思路有两个:
可以使用stack<TreeNode*> stk,自动创建一个stack空间,但是不知道为啥要放在第一行才行,当然要包含头文件#include,也可以自己用数组来模拟一个栈空间。
使用两个栈
思路:这个要参考左神的前序压栈方法:
前序的压栈方法:先压头,再把头弹出来。压完右节点再压左节点。然后利用栈的后进先出,再弹出左节点,将左节点的右节点压入栈,再压左节点的左节点。以此循环下去。
总结就是:先压头,再压右,最后压左。
参考完前序的方法之后,可以将头右左的顺序换成::头左右的顺序。此时得到的结果可以发现是后序遍历的,反过来的顺序。此时就是需要再利用一个栈来接收反过来的顺序。最后在输出的时候反过来即可。
使用一个栈加上一个指针变量
- 思路:打印的顺序是:左右头,因为是,压栈的过程,所以肯定是有往上走的,此时就可以用一个标志位来记录当前节点的右节点是否被打印,即变量的作用是跟踪被打印的节点。如果当前节点有右树且打印了,就直接打印当前节点。如果有右节点,但是没有被打印过,此时就要将右节点压入栈里面,注意不要被覆盖掉了。如果没有右节点直接打印。
- 要注意的地方
- 注意栈空间的覆盖,就是将右树压入到栈里面的时候,可能会把,当前节点给覆盖掉。因为有flag–的存在。此时可以选择将节点重新压栈。
- 防止二次 压栈,因为left的值是随时可变的额,比如2后面跟着4和5,当5打印完了之后,left返回到2,此时又会将4压入栈里面。这样就循环了。是不对的。所以,可以用New==left来防止这个问题。left如果是New(用来跟踪打印的变量),说明会是二次压栈了,此时将left的值置成nullptr,不让它压栈。
vector<int> lastorderTraversal(TreeNode* root) {
int flag = -1;//记录当前stack里面有多少元素。
TreeNode* left = root;
TreeNode* pMove = root;
TreeNode* stack[10] = { root };//自己手动创建的一个stack空间。
vector<int> arr;
TreeNode* New = nullptr;//记录右树是否被打印。跟踪打印的节点.
if (root == NULL)
return arr;
do {
//将左树全部压倒stack里面.这里要注意防止二次压栈。因为之后会弹出来节点,节点到时时候又会循环压栈。
while (left != NULL)//用的是left不是left->left,因为,如果left就是NULL的话。这样的访问就是无效的!!!
{
stack[++flag] = left;
left = left->left;
}
pMove = stack[flag];//弹出节点
flag--;
left = pMove->right;
//当前节点有右树,且当前节点右树还没有被打印。此时节点就不能被弹出来,因为弹出来了之后就找不到了。但是因为flag--,此时如果还是 stack[flag] = pMove,当前节点就被覆盖了。所以需要将当前节点重新压栈!!!,只有这样不会被覆盖掉,如果没有flag--的话,可能不会存在覆盖的情况.如果是自动创建的stack,可以使用stack.top(),访问栈顶元素,不会被弹出来。弹出来有另外的元素。
if ((left != NULL) && (New != pMove->right))
{
stack[++flag] = pMove;
stack[++flag] = left;
left = left->left;
}
//注意::pMove->right不可以用left替换,因为:上面那个if语句将left改了,如果改了的话,结果是不对的。
//当前节点有右节点,但是已经被被打印过了。此时也要打印
if ((pMove->right != nullptr) && (New == pMove->right))
{
cout << pMove->val << '\t';
arr.push_back(pMove->val);
New = pMove;
//防止flag超出范围。导致访问违法。
if (flag == -1)
break;
//此时的stack[flag]就是当前节点的上一个节点。因为打印完了,所以要换成右树,否则可能会再重新压栈。
left = stack[flag]->right;
}
//没有右节点,直接打印当前节点
else if(pMove->right == nullptr)
{
cout << pMove->val << '\t';
arr.push_back(pMove->val);
New = pMove;
}
//当进行二次压缩时,发现打印过的节点,又在被压栈,此时就要不应该再继续执行最上面的while。
//至于给left赋什么值好,只要不满足while的条件即可。因为while下面紧接着就是对left的重新赋值。
if (New == left)
{
left = nullptr;
}
} while (flag != -1);
return arr;
}
后序遍历(左神代码)
- 第一个if是将左树全部放到栈里面
- stack.peek(),表示取栈顶元素。
- 刚开始h的值可以随便取,只要不影响把左树的节点压到栈里即可。h是用来跟踪打印节点的。
- 第二个if是,当前节点有右树,且右树没有被处理的。
因为才学不是很久的c++,所以对vector的遍历不是很了解,此处记录一下新学的遍历vector的方法:
vector<int>::iterator it;//可以使用typedef把名字变短一点
vector<int> res;
res = solution.inorderTraversal(num1);
for (it = res.begin(); it != res.end(); ++it)
{
cout << *it << '\t';
}
参考左神视屏:(非递归:24:23开始)
注意:以上程序是依据自己的理解编写的,可能和视屏上的有些出入。但是,大致的思路是相似的。
3、Morris遍历
如最开始的阐述。
Morris遍历的有点:前面两个遍历的时间复杂度和空间复杂度都是O(n),但是Morris遍历的时间复杂度是O(n),空间复杂度是O(1).
二叉树的搭建
1、节点的构成及创建
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
//以下是构造函数,重载
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode* left, TreeNode* right) : val(x), left(left),right(right){}
};
//创建节点,或者说是开辟空间
TreeNode* AddNode(int val) {
//C6011警告:node->left = NULL.因为没有判断node是不是空指针。
TreeNode* node = (TreeNode*)malloc(sizeof(struct TreeNode));
if (NULL == node)
{
cout << "空指针" << endl;
}
else
{
node->left = NULL;
node->right = NULL;
node->val = val;
}
return node;
}
2、创建数的结构
//创建tree
void Creat_Tree(TreeNode* root, TreeNode* left, TreeNode* right) {
root->left = left;
root->right = right;
}
3、搭建数
//创建节点先
TreeNode* num1 = AddNode(1);
TreeNode* num2 = AddNode(2);
TreeNode* num3 = AddNode(3);
TreeNode* num4 = AddNode(4);
TreeNode* num5 = AddNode(5);
TreeNode* num6 = AddNode(6);
TreeNode* num7 = AddNode(7);
TreeNode* num8 = AddNode(8);
TreeNode* num9 = AddNode(9);
Creat_Tree(num1, num2, num3);
Creat_Tree(num2, num4, num5);
Creat_Tree(num3, num6, num7);
Creat_Tree(num4, num8, num9);
代码工程
94_BinaryTreeInorderTraversal
补充的知识点
1、传值和传址的区别
传值
传值是只把值传过去,函数那边是用,int来接收的。函数里面改变的是形参的值,改的并不是实参的值。因为函数会对实参进行一份拷贝,相当于在别的内存上开辟了一块空间,来存放拷贝的值。此时函数里面的改变,都是针对这块临时开辟的空间进行的。所以两者是互不干扰的。
传址
传址是把地址传过去,函数那边是用,指针接收的,当将指针解引用的话,就得到了值。此时函数里面改了,函数外面也会被改掉。因为操作的都是同一块地址。
2、数组传入函数的方式
当然,因为数组是一块连续的内存,要想把整个数组传过去,只能传地址过去。要不然怎么找到那块的内存。
数组名的几种含义
1、表示数组首元素的地址
基本上都是表示数组首元素的地址,只有以下两种情况不是。
2、sizeof(arr):整个数组的大小
表示数组里面有多少个元素
num = sizeof(arr)/sizeof(arr[0])
3、&arr:取的是整个数组的地址
虽然得到的结果和数组的首元素地址是一样的。但是注意,两者的步长是不一样的,即(&arr) + 1,跨越的是整个数组的长度,但是arr+1,得到的是第二个元素的地址。两者还是有点区别的。
函数接收数组的两种方式:
1、用指针变量接收。
这种方法是比较常见的,因为传过来的就是地址,所以用指针来接收很正常。
如果传过来的是一个指针数组,此时要拿二级指针来接收才对。因为数组是一个地址,而数组里面放的也是地址。