剑指offer 序列化二叉树(C++)(加强对引用(reference)的理解)

本文详细解析了二叉树的序列化和反序列化过程,使用先序遍历的方式,介绍了如何将二叉树转换为字符串以及如何从字符串重建二叉树。特别强调了在递归过程中使用引用的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目描述

请实现两个函数,分别用来序列化和反序列化二叉树

二叉树的序列化是指:把一棵二叉树按照某种遍历方式的结果以某种格式保存为字符串,从而使得内存中建立起来的二叉树可以持久保存。序列化可以基于先序、中序、后序、层序的二叉树遍历方式来进行修改,序列化的结果是一个字符串,序列化时通过 某种符号表示空节点(#),以 ! 表示一个结点值的结束(value!)。

二叉树的反序列化是指:根据某种遍历顺序得到的序列化字符串结果str,重构二叉树。

解题思路

  • 此题搞得焦头烂额,题目并没有说二叉树元素一定是整型的,二叉树结点是字符型的情况也是常见的。题目是要求需要写出多种遍历方式还是要求自己任意选择一种遍历方式(虽然写出多种遍历方式真的不太可能),这个也是我最初读这道题目时候的疑问,试问剑指offer问什么不像LeetCode那样画出图呢?
  • 题目的真实要求是默认二叉树结点类型是int型的,而且是自己任意选择遍历方式。由于是整型,结点与结点之间如果不加某些字符作为间隔的话,很难区分各个结点的值,所以题目要求用感叹号!来表示结点与结点之间的间隔(用逗号岂不是更好吗?当然这个是无所谓的)
  • 由于char*类型并没有string类的实现功能强大,可以事先自己编写一个函数来实现序列化二叉树,再将string类转化为char*类即可。所用方法为:
strcpy(chBiTree, strBiTree.c_str());
  • string类型中将其余类型转化为string类型的函数为:to_string();
str += to_string(pRoot->val); // 将每个结点的val值转化为string类型存储在str上。
  • 反序列化二叉树时,一定要注意如果结点值大于9的话,每两个!之间除了#代表空结点(NULL)以外,这些字符串是要转换为int类型的,记得前段时间剑指offer上出现过这样的问题,具体代码为:
val = val * 10 + (*str - '0'); //数字型字符串中 每个字符数字逐渐向后遍历
  • 被调用函数中的参数要使用传址(by reference),即引用(reference),引用的特点是将对象作为函数参数传入时,对象本身并不复制出另一份,复制的是函数的地址,函数中对该对象进行任何操作,都相当于对传入的对象进行间接的操作,这样既可以直接对传入的对象进行操作修改,又降低了复制大型对象的额外负担(当然这只是计算机效率的问题),最主要的是能够达到想要的结果。
int i = 1024;
int &iName = i;
cout << iName << endl; // 完全就是i本身  就是给变量起别名 1024

代码实现

/*
struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
    TreeNode(int x) :
            val(x), left(NULL), right(NULL) {
    }
};
*/
class Solution {
public:
    void PreBiTree(TreeNode *root, string &strBiTree)  // 要使用引用
    {
        if(root == NULL)
        {
            strBiTree.append("#!");
            return;
        }
        strBiTree += to_string(root->val);
        strBiTree += "!";
        PreBiTree(root->left, strBiTree);
        PreBiTree(root->right, strBiTree);
        return;            
    }
    char* Serialize(TreeNode *root) {    
        string strBiTree;
        PreBiTree(root, strBiTree);
        char* chBiTree = new char[strBiTree.length() + 1];
        strcpy(chBiTree, strBiTree.c_str());
        chBiTree[strBiTree.length()] = '\0';
        return chBiTree;
    }
    TreeNode* Deserialize(char *&str) { // 使用引用    为什么必须加引用呢?	后文有解释
       	if(*str == '#')					
        {
            str += 2; // 遇到空结点指针加2
            return NULL;
        }            
        int val = 0;
        while(*str != '!' && *str) // 将非空结点转换为int型
        {
            val = val * 10 + (*str - '0');
            str++; // 遇到!停止
        }
        TreeNode* pRoot = new TreeNode(val);
        if(*str)
            str++; // 跳过!
        else
            return pRoot;
        pRoot->left = Deserialize(str); // 递归反序列化左子树
        pRoot->right = Deserialize(str); // 递归反序列化右子树
        return pRoot;
    }
};

本文重点:

上述TreeNode* Deserialize(char *&str)函数中参数为传址(引用),其实不可用传值替代。

传值本质是对对象进行复制,用对象的副本进行操作。对于上述问题,每次执行函数递归pRoot->left = Deserialize(str);pRoot->right = Deserialize(str);这两句话时都是对str的一次次的复制,也就是stack区存在了很多的str指针所指向的字符串,待到二叉树的左子树遍历结束后,正常思维本应该遍历二叉树的右子树,但递归函数回溯到哪一个,都会存在着与之对应的str字符串,也就是不能达到str指针一直向后移的效果,待每次递归时函数的结束,相对应的str字符串副本就会被释放。

传址即引用,就会很好地解决问题。传址的本质是在对象本身上进行操作。本题目要求str指针一直沿着str所指向的字符串后移,直到str指向空为止。所以加上引用后,即使是一次次的执行pRoot->left = Deserialize(str);pRoot->right = Deserialize(str);这两句也不会复制出很多的字符串副本,改变的一直都是从主函数传来的str字符串本身,向我们要求的方向随着语句的执行,str指针一直在后移,直到str指向空为止,从而达到我们想要的目的。

加入主函数后形成函数的模块化

在VS上重新实现了该功能。

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <vector>
#include <string>
using namespace std;

struct TreeNode {
	int val;
	struct TreeNode *left;
	struct TreeNode *right;
	TreeNode(int x) :
		val(x), left(NULL), right(NULL) {
	}
};

TreeNode* CreateTree() //新建二叉树 返回二叉树序列首地址
{
	TreeNode* root = new TreeNode(5); // 注意new的用法 初始化后必须赋值
	TreeNode* left1 = new TreeNode(3);
	TreeNode* right1 = new TreeNode(7);
	root->left = left1; // 赋值问题 老是出错
	root->right = right1;
	TreeNode* left2 = new TreeNode(2);
	TreeNode* right2 = new TreeNode(4);
	left1->left = left2;
	left1->right = right2;
	TreeNode* left3 = new TreeNode(6);
	TreeNode* right3 = new TreeNode(8);
	right1->left = left3;
	right1->right = right3;
	return root;
}

void PreBiTree(TreeNode *root, string &strBiTree) // 必须加引用  用string类满足要求先
{
	if (root == NULL)
	{
		strBiTree.append("#!");
		return;
	}
	strBiTree += to_string(root->val);
	strBiTree += "!";
	PreBiTree(root->left, strBiTree); // 二叉树的先序遍历
	PreBiTree(root->right, strBiTree);
	return;
}

char* Serialize(TreeNode *root) {  // 返回字符串数组首地址
	string strBiTree;
	PreBiTree(root, strBiTree);
	char* chBiTree = new char[strBiTree.length() + 1]; // 记得后面加1 用于'\0'的存储
	strcpy(chBiTree, strBiTree.c_str()); //  一个将string 转为 char* 的方法
	chBiTree[strBiTree.length()] = '\0'; // char数组最后不要忘记这个句子 否则报错
	return chBiTree;
}

TreeNode* Deserialize(char* &str) { //  就因为没有加引用(reference)导致出错 现在已改正 
	if (*str == '#')				
	{
		str += 2; // 遇到空结点指针加2
		return NULL;
	}
	int val = 0;
	while (*str != '!' && *str) // 将非空结点转换为int型
	{
		val = val * 10 + (*str - '0');
		str++; // 遇到!停止
	}
	TreeNode* pRoot = new TreeNode(val);
	if (*str)
		str++; // 跳过!
	else
		return pRoot;

	pRoot->left = Deserialize(str); // 递归反序列化左子树
	pRoot->right = Deserialize(str); // 递归反序列化右子树
	
 	return pRoot;
}

void PrePrintBiTree(TreeNode* pRoot) // 二叉树的先序递归遍历算法
{
	if (pRoot)
	{
		cout << pRoot->val << " ";
		if (pRoot->left)
			PrePrintBiTree(pRoot->left);
		if (pRoot->right)
			PrePrintBiTree(pRoot->right);
	}
}

void main()
{
	TreeNode* pRoot = CreateTree();
	PrePrintBiTree(pRoot);
	cout << endl;
	Serialize(pRoot); // 返回字符串指针
	char* str = Serialize(pRoot);
	cout << "str = " << str << endl;
	TreeNode* pRoot2 = Deserialize(str); // ? 为什么必须加参数的引用
	cout << "二叉树反序列化后的前序遍历序列为:";
	PrePrintBiTree(pRoot2);
	cout << endl;

	system("pause");
	return;
}

/*
5 3 2 4 7 6 8
str = 5!3!2!#!#!4!#!#!7!6!#!#!8!#!#!
二叉树反序列化后的前序遍历序列为:5 3 2 4 7 6 8
请按任意键继续. . .
*/

随便写写

iterator是泛型指针,可以指向任何一个容器。

void Change_string(string &str)
{
	bool even = true;
	cout << "我是局部变量 被调用函数中的str = " << str << endl;
	for (string::iterator it = str.begin(); it != str.end(); it++)
	{
		*it = ((even == true) ? *it : toupper(*it));
		even = !even;
	}
}

下述代码与上面的实现完全一致。

void Change_string(string &str)
{
	cout << "我是局部变量 被调用函数中的str = " << str << endl;
	for (int i = 0; str[i]; i++)
	{
		str[i] = ((str[i] % 2 == 0) ? str[i] : toupper(str[i]));
	}
}

下述代码中函数参数加入const关键字,则仅具有只读属性,下面对字符串的修改将会报错。

void Change_string(const string &str) // 加入const是错误的示范 const使得str 只具有只读属性 除非执行不改写str内容的操作
{
	cout << "我是局部变量 被调用函数中的str = " << str << endl;
	for (int i = 0; str[i]; i++)
	{
		str[i] = ((str[i] % 2 == 0) ? str[i] : toupper(str[i]));
	}
}

下述这段代码是错误的,针指所向的字符串不能被修改

void Change_string(char* &str) // 错误代码 除非主函数是字符数组 这样的话 参数不能传递引用
{
	cout << "我是局部变量 被调用函数中的str = " << str << endl;
	for (int i = 0; str[i]; i++)
	{
		str[i] = ((str[i] % 2 == 0) ? str[i] : toupper(str[i]));
	}
}

下述代码中的递归函数参数不能写成str++,这样的话函数永远无法停止。

void dosomethings(char* &str)
{
	if (*str)
	{
		cout << str << endl;
		dosomethings(++str); // 先加指针后移 再执行函数调用
	}
}

最后是主函数

void main()
{
	string str = "I love Bejing University of technology!";
	Change_string(str);
	cout << "str = " << str << endl; // str = I lOvE BeJiNg UNiVeRsItY Of tEcHnOlOgY! 要加引用
	char* str2 = "hello world";
	dosomethings(str2);
	system("pause");
} 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值