61序列化二叉树--62二叉搜索树的第k个结点--63数据流中的中位数

本文探讨了二叉树的序列化与反序列化算法,深入解析了二叉搜索树中查找第k个结点的策略,以及利用双堆解决数据流中位数计算的高效方法。

61序列化二叉树

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

  1. 序列化:发送数据时将结构化数据按照一定规则转换成字符串。
  2. 反序列化:接受到数据再按照相同的规则把字符串转化回原结构类型。
  3. 采用层序递归遍历将二叉树节点的值按层转换的字符型,并且添加到一个string中,以,分隔开来。遍历左右节点
  4. 递归出口是结点指向空,说明到了叶子节点,用‘#’标记叶子节点。
  5. 反序列化时候,采用找以,分隔开来的字符串,那原本是一个整数,将它还原成整数,构建节点,递归左右孩子。
/*
struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
    TreeNode(int x) :
            val(x), left(NULL), right(NULL) {
    }
};
*/
class Solution {  
public:
    char* Serialize(TreeNode *root) {
       if(root == NULL)
           return NULL;
        string str;
        Serialize(root, str);
        char *ret = new char[str.length() + 1];
        strcpy(ret,str.c_str());
        return ret;
    }
    void Serialize(TreeNode *root, string& str){
        if(root == NULL){
            str += '#';
            return ;
        }
        string r = to_string(root->val);
        str += r;
        str += ',';
        Serialize(root->left, str);
        Serialize(root->right, str);
    }
     
    TreeNode* Deserialize(char *str) {
        if(str == NULL)
            return NULL;
        TreeNode *ret = Deserialize(&str);
 
        return ret;
    }
    TreeNode* Deserialize(char **str){//由于递归时,会不断的向后读取字符串
        if(**str == '#'){  //所以一定要用**str,
            ++(*str);         //以保证得到递归后指针str指向未被读取的字符
            return NULL;
        }
        int num = 0;
        while(**str != '\0' && **str != ','){
            num = num*10 + ((**str) - '0');
            ++(*str);
        }
        TreeNode *root = new TreeNode(num);
        if(**str == '\0')
            return root;
        else
            (*str)++;
        root->left = Deserialize(str);
        root->right = Deserialize(str);
        return root;
    }
};

牛客网链接

62二叉搜索树的第k个结点

给定一棵二叉搜索树,请找出其中的第k小的结点。例如, (5,3,7,2,4,6,8) 中,按结点数值大小顺序第三小结点的值为4。

  1. 二叉搜索树左子树值均比根节点值小,右子树值均比根节点大。从小到达的第k个节点,那么如果采用中序递归遍历,应该先找左子树,只要左子树存在,左子树值就是最小的。
  2. 左子树为空的节点,是此时第count的由小到大的节点,当count==k时候的当前节点就是目标节点。
/*
struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
    TreeNode(int x) :
            val(x), left(NULL), right(NULL) {
    }
};
*/
class Solution {
public:
    int count=0;
    TreeNode* ret=nullptr;
    TreeNode* KthNode(TreeNode* pRoot, int k)
    {
        if(pRoot!=nullptr){
            if(pRoot->left!=nullptr)
                ret=KthNode(pRoot->left,k);
            if(ret!=nullptr)return ret;
            count++;
            if(count==k)return pRoot;
            if(pRoot->right!=nullptr)
                ret=KthNode(pRoot->right,k);
            if(ret!=nullptr)return ret;
        }
        return nullptr;
    }
};

牛客网链接

63数据流中的中位数

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。

1.采用vector存储数据流时候,sort排序是O( n ∗ l o g N n*logN nlogN),插入效率是O(1)。
2. 获取中位数的效率是 O ( n ∗ l o g N ) O(n*logN) O(nlogN),每一次获取中位数都需要sort排序。

class Solution {
public:
    void Insert(int num)
    {
        number.push_back(num);
    }
 
    double GetMedian()
    {
        sort(number.begin(),number.end());
        int size=number.size();
        if(size%2==0)
            return (double)((double)((number[size/2-1])+(number[size/2]))/2);
        else
            return (double)number[size/2];
    }
private:
    vector<int> number;
};

插入排序,插入数据时间效率 O ( n ) O(n) O(n),获取一个中位数的时间复杂度是 O ( 1 ) O(1) O(1)

class Solution {
public:
    vector<int> median;
    void Insert(int num)
    {
        vector<int>::iterator pos=upper_bound(median.begin(),median.end(),num);
        median.insert(pos,num);
    }
 
    double GetMedian()
    {
        if(median.size()==0) return 0.0;
        int pos=median.size()/2;
        if(median.size()%2==1){
            return median[pos];
        }
        return (median[pos-1]+median[pos])/2.0;
    }
};

实际上我们知道AVL树,这种十分平衡的二叉树搜索树来存储这样的数据流,效果很好,但是构造这样的结构很不划算。
在这里插入图片描述

  1. 我们可以用两个堆来代替AVL树:左子树用最大堆来实现,右子树用最小堆来实现。如上图所示,P1即为左子树最大堆堆顶,P2则为右子树最小堆堆顶。
  2. 当数组为偶数个时候,插入到最小堆,也就是右半部分,右半部分的数值应该都比左半部分的值大,但是插入的这个数比最大堆的堆顶数要小,怎么办?那么将这个数值更新到最大堆即可,因为我只关心最大堆的最大值和最小堆的最小值,不更新可能会影响这两个值的正确性。
  3. 获取中位数时候,我们只需判断当前数组是奇数个还是偶数个,偶数个就需要求两个堆顶的平均数,否则直接取小堆堆顶数。
class Solution {
private:
    vector<int> min;
    vector<int> max;
public:
        void Insert(int num)
        {
           int size=min.size()+max.size();
           if((size&1)==0)
           {
              if(max.size()>0 && num<max[0])
              {
                 max.push_back(num);
                 push_heap(max.begin(),max.end(),less<int>());
                 num=max[0];
                 pop_heap(max.begin(),max.end(),less<int>());
                 max.pop_back();
              }
              min.push_back(num);
              push_heap(min.begin(),min.end(),greater<int>());
           }
           else
           {
              if(min.size()>0 && num>min[0])
              {
                min.push_back(num);
                 push_heap(min.begin(),min.end(),greater<int>());
                 num=min[0];
                 pop_heap(min.begin(),min.end(),greater<int>());
                 min.pop_back();
              }
              max.push_back(num);
              push_heap(max.begin(),max.end(),less<int>());
           }   
        }
         
        double GetMedian()
        {
            int size=min.size()+max.size();
            if(size<=0)
                return 0;
            if((size&1)==0)
                return (max[0]+min[0])/2.0;
            else
                return min[0];
        } 
};

牛客网链接

<think> 我们首先需要回答两个主要问题: 1. 适合采用顺序存储和二叉链式存储的二叉树类型举例 2. 编程调试定义C语言二叉树链式存储结构及相关操作代码时遇到的问题、解决方案和未通过测试的原因分析 ### 1. 适合采用顺序存储和二叉链式存储的二叉树类型举例 - **顺序存储**:一般适用于完全二叉树或满二叉树。因为顺序存储利用数组下标来反映结点间的父子关系(对于位置$i$的结点,其左孩子在$2i$,右孩子在$2i+1$,双亲在$\lfloor i/2 \rfloor$),这样存储紧凑且无空间浪费。对于非完全二叉树,如果用顺序存储,则必须将空缺的位置用空值占位,导致空间浪费[^1]。 例如:一个满二叉树或完全二叉树(如堆)适合用顺序存储。 - **二叉链式存储**:适用于任意二叉树。每个结点包含数据域和两个指针域(左孩子、右孩子),可以灵活地表示任何树形结构,包括非完全二叉树。链式存储不要求逻辑上相邻的结点在物理位置上也相邻,因此不会浪费空间(除了指针域的空间开销)。但无法直接通过下标访问结点,需要通过指针遍历。 例如:一般的二叉树(如二叉排序树、平衡二叉树、哈夫曼树等)都适合用链式存储。 ### 2. 编程调试定义C语言二叉树链式存储结构及相关操作代码时遇到的问题、解决方案和未通过测试的原因分析 在实现二叉树的链式存储结构及相关操作(创建、遍历、查找)时,可能会遇到以下问题: #### (1) 创建二叉树(递归先序创建): - **问题**:输入处理不当,导致递归创建时数据读取错位。例如,使用`scanf`读取字符时,换行符或空格会被当作有效字符读取。 - **解决方案**:在`scanf`格式字符串中加入空格吸收空白字符,或者使用`getchar()`吸收多余的换行符。 - **未通过测试**:若创建函数递归终止条件处理不当(如未处理空结点),可能导致无限递归或内存错误。 #### (2) 遍历操作(先序、中序、后序): - **问题**:递归遍历时,递归基(空树)未正确处理,导致访问空指针。 - **解决方案**:在遍历函数中首先判断树是否为空,为空则直接返回。 - **未通过测试**:对于非递归遍历,若使用栈实现,可能因栈操作错误导致死循环或结果错误。 #### (3) 查找元素所在层数: - **问题**:递归查找时,层数传递错误或返回值处理不当。例如,在左子树中找到后未直接返回,而是继续查找右子树,导致返回的层数可能是右子树的(但左子树先找到,层数应更小)。 - **解决方案**:当在左子树中找到时,立即返回左子树的结果,不再查找右子树;若左子树未找到,再在右子树中查找。 - **未通过测试**:若查找的元素不存在,需要返回0,但递归过程中未正确传递0值。例如,在递归调用中,若左右子树都未找到,则返回0。 #### 代码示例(带详细注释): ```c #include <stdio.h> #include <stdlib.h> typedef struct BiTNode { char data; struct BiTNode *lchild, *rchild; } BiTNode, *BiTree; // 创建二叉树(先序输入,#表示空) void CreateBiTree(BiTree *T) { char ch; scanf("%c", &ch); if (ch == '#') { *T = NULL; } else { *T = (BiTree)malloc(sizeof(BiTNode)); if (!*T) exit(1); (*T)->data = ch; CreateBiTree(&(*T)->lchild); CreateBiTree(&(*T)->rchild); } } // 先序遍历 void PreOrder(BiTree T) { if (T) { printf("%c ", T->data); PreOrder(T->lchild); PreOrder(T->rchild); } } // 中序遍历 void InOrder(BiTree T) { if (T) { InOrder(T->lchild); printf("%c ", T->data); InOrder(T->rchild); } } // 后序遍历 void PostOrder(BiTree T) { if (T) { PostOrder(T->lchild); PostOrder(T->rchild); printf("%c ", T->data); } } // 查找元素所在层数,level为当前层数 int FindLevel(BiTree T, char x, int level) { if (!T) return 0; if (T->data == x) return level; int left = FindLevel(T->lchild, x, level+1); if (left) return left; // 左子树找到,直接返回 return FindLevel(T->rchild, x, level+1); // 左子树没找到,再找右子树 } int main() { BiTree T; printf("输入先序序列(用#表示空结点): "); CreateBiTree(&T); printf("先序遍历: "); PreOrder(T); printf("\n中序遍历: "); InOrder(T); printf("\n后序遍历: "); PostOrder(T); printf("\n"); char target; printf("输入要查找的元素: "); scanf(" %c", &target); // 注意空格,跳过空白字符 int level = FindLevel(T, target, 1); if (level) { printf("%c在%d层\n", target, level); } else { printf("%c不存在\n", target); } return 0; } ``` #### 调试中可能出现的其他问题: - **内存泄漏**:在程序结束前没有释放二叉树占用的内存。解决方案:编写后序遍历释放内存的函数。 - **输入缓冲问题**:连续输入字符时,换行符被当作有效字符。解决方案:在格式字符串中加入空格,如`scanf(" %c", &ch)`。 ### 实验小结 1. **顺序存储与链式存储的选择**: - 顺序存储适用于完全二叉树和满二叉树,空间利用率高,且支持随机访问。对于非完全二叉树,顺序存储会造成空间浪费。 - 链式存储适用于任意二叉树,空间利用合理(仅需额外两个指针),但访问结点需要遍历。 2. **调试经验**: - 递归创建二叉树时,输入格式必须严格按照先序序列给出,且空结点用特定字符(如`#`)表示。 - 递归遍历时,注意递归基(即树为空)的处理,避免访问空指针。 - 查找元素层数时,注意递归返回值的传递:在左子树中找到后立即返回,避免继续查找右子树。 3. **未通过测试分析**: - 若创建函数未正确处理空结点,则可能导致二叉树结构错误。 - 若遍历函数中递归基缺失,将导致程序崩溃(段错误)。 - 查找函数中若未正确处理左右子树的返回值顺序,可能返回错误的层数(右子树的层数大于左子树时,会返回更大的层数,但实际上应返回先找到的层数)。 §§相关问题§§ 1. 在顺序存储中,如何判断一个结点是否为叶子结点? 2. 非递归遍历二叉树时,栈的大小如何确定?是否可能溢出? 3. 如何计算二叉树的深度? 4. 在链式存储中,如何实现层次遍历? </think>### 适合采用顺序存储和二叉链式存储的二叉树类型举例 1. **顺序存储适用场景** - **完全二叉树**:如堆结构(最大堆/最小堆) *示例*:$[A, B, C, D, E, F, G]$(按层序存储) *特点*:利用数组下标$i$可快速定位子节点位置(左子$2i+1$,右子$2i+2$) - **满二叉树**:如哈夫曼树 *示例*:$[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O]$ *优势*:空间利用率100%,无内存碎片[^1] 2. **二叉链式存储适用场景** - **任意形态二叉树**:如二叉搜索树 *示例*:根节点$D$,左子树$[B, A, C]$,右子树$[F, E, G]$ - **非完全二叉树**:如表达式树 *示例*:$(*\ (+ \ A \ B)\ (- \ C \ D))$ *优势*:动态管理内存,避免顺序存储的空间浪费[^2] ### 编程调试问题分析与解决方案 #### 问题1:递归创建二叉树时的输入处理 - **现象**: `scanf`读取字符时换行符被当作有效输入 - **复现场景**: ```c printf("输入结点: "); scanf("%c", &ch); // 输入'A'后按回车,下次循环会读取'\n' ``` - **解决方案**: ```c scanf(" %c", &ch); // 添加空格忽略空白符 ``` - **效果**: 输入流中的空格/回车被自动过滤 #### 问题2:查找函数层数计算错误 - **错误代码**: ```c if (T->data == target) return level; FindLevel(T->lchild, target, level++); // 后++导致层数传递错误 ``` - **错误分析**: `level++`使右子树搜索时层数比实际多1 - **修正方案**: ```c int left = FindLevel(T->lchild, target, level + 1); if (left) return left; return FindLevel(T->rchild, target, level + 1); ``` - **测试用例**: 查找`H`(应返回4),修正后输出正确 #### 问题3:内存泄漏(未通过测试) - **现象**: `valgrind`检测到`CreateBiTree`分配的内存未释放 - **未通过原因**: 未实现树结构销毁函数 - **解决方案**: ```c void DestroyTree(BiTree T) { if (T) { DestroyTree(T->lchild); DestroyTree(T->rchild); free(T); // 后序遍历释放节点 } } ``` - **补充调用**: ```c int main() { // ... DestroyTree(T); // 程序结束前释放内存 return 0; } ``` #### 问题4:空树遍历崩溃 - **现象**: 输入`#`创建空树时,遍历函数段错误 - **错误代码**: ```c void PreOrder(BiTree T) { printf("%c ", T->data); // T为空时访问data导致崩溃 // ... } ``` - **修正方案**: ```c void PreOrder(BiTree T) { if (!T) return; // 添加空树检查 // ... } ``` ### 未通过测试操作分析 | 操作 | 未通过原因 | 解决方案 | |------------|---------------------------|----------------------------| | 层次遍历 | 未实现队列辅助结构 | 使用循环队列存储节点指针 | | 线索化 | 未添加ltag/rtag标志位 | 扩展节点结构增加标志域 | | 非递归遍历 | 栈操作逻辑错误 | 改用迭代算法重写遍历函数 | ### 实验结论 1. **存储结构选择**: - 顺序存储适合**静态完全二叉树**(空间$O(2^h)$) - 链式存储适合**动态二叉树**(空间$O(n)$) 2. **递归陷阱**: - 递归深度超过栈大小导致崩溃(可通过尾递归优化) - 参数传递错误(如`level++`改为`level+1`) 3. **内存管理**: - 树节点需配对释放(后序遍历销毁) - 每棵子树平均$2n+1$个指针开销($n$为节点数) 4. **算法效率**: ```mermaid graph LR A[操作] --> B[时间复杂度] B --> C1[创建: O(n)] B --> C2[遍历: O(n)] B --> C3[查找: O(n)] ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值