序列化是将数据结构或对象转换为一系列位的过程,以便它可以存储在文件或内存缓冲区中,或通过网络连接链路传输,以便稍后在同一个或另一个计算机环境中重建。
设计一个算法来序列化和反序列化 二叉搜索树 。 对序列化/反序列化算法的工作方式没有限制。 您只需确保二叉搜索树可以序列化为字符串,并且可以将该字符串反序列化为最初的二叉搜索树。
编码的字符串应尽可能紧凑。
示例 1:
输入:root = [2,1,3]
输出:[2,1,3]
示例 2:
输入:root = []
输出:[]
提示:
树中节点数范围是 [0, 104]
0 <= Node.val <= 104
题目数据 保证 输入的树是一棵二叉搜索树。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
public class Codec {
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
}
}
// Your Codec object will be instantiated and called as such:
// Codec ser = new Codec();
// Codec deser = new Codec();
// String tree = ser.serialize(root);
// TreeNode ans = deser.deserialize(tree);
// return ans;
思路分析:由于题目的要求是将一颗二叉搜索树的数据保存为字符串,然后可以根据这个字符串重建二叉搜索树,首先,什么是二叉搜索树。
二叉搜索树:1.每个节点最多有两个孩子(二叉树)2.左孩子一定比双亲节点小,右孩子一定比双亲节点大3.左孩子和右孩子如果不是叶子节点,也一定是二叉搜索树(也满足左小右大关系)。
向二叉搜索树插入节点的操作:依次比较节点,小走左,大走右,最后到的空节点就是待添加的位置。
假设现在向根节点为root的树添加节点s。函数代码如下:
public static boolean insertNode(TreeNode root,TreeNode s){
//加入节点为空,其实只有树为空才会出现
//因为后面有判断节点是否为空
//如果存在空节点,就是待插入的位置
if(root==null){
root = s;
return true;
}
//如果存在相同节点,插入失败
if(root.val == s.val) return false;
//比当前节点大
if(s.val>root.val){
//当前节点右节点为空,即插入的位置
if(root.right == null){
root.right = s;
return true;
}else {
//还有节点,就去比较右节点
insertNode(root.right,s);
}
}else {
//比当前节点小且左节点为空,直接添加
if(root.left == null){
root.left = s;
return true;
}else {
//继续去比较左节点
insertNode(root.left,s);
}
}
//这个执行不到,避免编译器报错写的
return false;
}
二叉树有四种遍历方式:前序遍历,中序遍历,后续遍历,层序遍历。其中前,中,后是根据根节点访问次序来的,先访问根节点就是前序。层序遍历目前没有遇到过,写一个前序遍历的代码:
static void printTree(TreeNode root){
if(root == null) return;
//打印根节点
System.out.println(root.val);
//访问左子树
printTree(root.left);
//再访问右子树
printTree(root.right);
}
测试代码:
public class Main {
public static void main(String[] args) {
TreeNode root = new TreeNode(15);
root.left = new TreeNode(3);
root.right = new TreeNode(18);
printTree(root);
Codec.insertNode(root,new TreeNode(7));
System.out.println("**************");
printTree(root);
}
static void printTree(TreeNode root){
if(root == null) return;
//打印根节点
System.out.println(root.val);
//访问左子树
printTree(root.left);
//再访问右子树
printTree(root.right);
}
}
输出:
15
3
18
**************
15
3
7
18
调试模式:
插入前节点情况:
插入后节点情况:
可以看到3的右边插入了7。
再拉回来看这道题目,序列化就是将二叉搜索树所有节点存的信息保存为字符串,反序列化就是将一个字符串恢复为一颗二叉搜索树。恢复为二叉搜索树可以看成是一个二叉搜索树构造的过程,如果将字符串分解为一个个节点的val,按照顺序构造二叉搜索树,那么新构造的二叉搜索树要和原二叉搜索树每个节点对应, 则字符串中节点顺序必须是前序遍历顺序。
举个例子,比如213,根节点的val为2,左孩子的val为1,右孩子的val为3。那么按照前序遍历的顺序为2 1 3,在构造的时候,按照2 1 3来构造二叉搜索树的结果一定是根节点的val为2,左孩子的val为1,右孩子的val为3。即和原二叉搜索树是一样的。
但如果按照中序来遍历,先遍历左子树,接着根节点,再右子树,那么此时顺序为1 2 3,那么按照这个顺序来构造二叉树,首先1为根节点,2放左边,3放右边,显然和原来根节点的val为2,左孩子的val为1,右孩子的val为3不同,无法完成反序列化的操作。
而为什么用前序遍历二叉搜索树得到的顺序来构造新的二叉搜索树就可以和得到两棵一样的树呢?
这是因为二叉搜索树在构造的时候,都是遍历根节点,再作为孩子节点插入到树中。
解释:可以将二叉搜索树看成是根节点带左子树和右子树(仅看成三个节点):那么新加入的节点一定先和根节点比较,如果比根节点大就往右子树看,如果比根节点小就往左子树看。
假设现在比根节点小,那么往左子树看,看的还是根节点,即左子树的根节点,现在左子树也依然可以看成三个节点,根节点,左子树和右子树。那么新加入的节点也一定是继续和根节点比较。
总之,每一次的比较,其实就是和每一棵子树的根节点比较,当和一个根节点比较后,出现可以插入的位置时(大于根节点且根节点的右孩子为空,小于根节点且根节点的左孩子为空),即将新加入的节点作为孩子节点插入到根节点下面。而前序遍历就是从根节点出发,无论遍历到哪棵子树,都是先遍历子树的根节点,这种顺序和二叉搜索树的构造顺序是一致的,都是先根节点再孩子节点,因此用前序遍历得到的顺序来构造二叉排序树,两棵树节点是一一对应的。
PS:当确定了根节点后,先构造左子树还是先构造右子树是没有关系的,例如根节点的val为2,左孩子的val为1,右孩子的val为3,前序遍历修改一下,遍历完根节点后再遍历右子树,再遍历左子树,那么顺序就是2 3 1
按照这个顺序构造二叉搜索树还是根节点的val为2,左孩子的val为1,右孩子的val为3。并且我在题目中前序遍历时,先遍历右子树再遍历左子树和先遍历左子树再遍历右子树结果是一样的,并且时间内存都几乎一样,也证明了当确定了根节点后,先构造左子树还是先构造右子树是没有关系的这一观点。
第一个和第二个前序遍历的时候先右孩子再左孩子,第三个是先左孩子再右孩子,结果都是一样的。
说的很啰嗦,但我觉得二叉树在数据结构中确实很重要,自己趁这道题目好好总结掌握下其中的遍历方法,构造树的方法,插入节点的方法。
那么经过上面一堆叙述,其实这道题目就先通过前序遍历这个二叉搜索树,再按这个顺序构造新的二叉搜索树即可。代码如下:
public class Codec {
//存放val
StringBuffer sb = new StringBuffer();
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
//输入为空树时
if(root == null) return null;
//前序遍历
preoderTraversal(root);
return sb.toString();
}
//前序遍历
private void preoderTraversal(TreeNode root) {
if(root == null) return;
sb.append(root.val);
sb.append(",");
//此时先遍历左子树和先遍历右子树是一样的
preoderTraversal(root.left);
preoderTraversal(root.right);
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
//当输入字符串为空时
if(data == null) return null;
//得到节点值的数组
String substring = data.substring(0, data.length() - 1);
String[] split = substring.split(",");
int[] nums = new int[split.length];
for (int i = 0; i < split.length; i++) {
nums[i] = Integer.parseInt(split[i]);
}
//先创建根节点
TreeNode root = new TreeNode(nums[0]);
//依次创建后续节点
for (int i = 1; i < nums.length; i++) {
buildTree(root,nums[i]);
}
return root;
}
//由于不会存在两个节点相等,因为是原二叉搜索树来的
//也不会存在空表,前面判定了
private void buildTree(TreeNode node, int num) {
//当前节点值小于num,即看右孩子
if(node.val<num){
//右孩子为空
if(node.right == null){
//进行插入
TreeNode n = new TreeNode(num);
node.right = n;
}else {
//进入右孩子去比较
buildTree(node.right,num);
}
}else {
//当前节点值大于num,看左孩子
if(node.left == null){
//进行插入
TreeNode n = new TreeNode(num);
node.left = n;
}else {
//进入左孩子去比较
buildTree(node.left,num);
}
}
}
}
总结一下:
1.二叉树的遍历:按照根节点的访问次序分为前序遍历,中序遍历,后序遍历。再加一个层序遍历。
2.二叉搜索树的特点:左孩子的值小于根节点的值,右孩子的值大于根节点的值,并且左右孩子节点依然是一棵
二叉搜索树。
3.二叉搜索树的插入。
4.二叉搜索树的建立。