DS博客作业03--树

| 这个作业属于哪个班级 | 数据结构--网络2011/2012 |
| ---- | ---- | ---- |
| 这个作业的地址 | DS博客作业03--树 |
| 这个作业的目标 | 学习树结构设计及运算操作|
| 姓名 | 崔天赐|

0. PTA总分

1. 本周学习总结

1.1 二叉树结构

1.1.1 二叉树的2种存储结构

  • 顺序存储:顺序存储方法它是把逻辑上相邻的结点存储在物理位置相邻的存储单元里,结点间的逻辑关系由存储单元的邻接关系来体现,由此得到的存储表示称为顺序存储结构。顺序存储结构是一种最基本的存储表示方法,通常借助于程序设计语言中的数组来实现。
  • 链式存储结构:链接存储方法它不要求逻辑上相邻的结点在物理位置上亦相邻,结点间的逻辑关系是由附加的指针字段表示的。由此得到的存储表示称为链式存储结构,链式存储结构通常借助于程序设计语言中的指针类型来实现。
  • 二者的优缺点:
    二叉树的顺序存储结构
    缺点:
    1.具有顺序存储结构的固有缺陷,使得二叉树的插入,删除等运算操作十分不方便。(即需要移动大量数组元素)
    2.对于一般二叉树,采用此结构,需要增设大量空结点,造成空间浪费。
    优点:
    1.对于完全二叉树或者满二叉树采用此结构,能够最大可能节省存储空间,又可以利用数组元素的下标确定结点位置以及结点之间的关系,例如,
    查找一个结点的孩子,双亲结点,高度,编号为i的结点的层次为log2(i+1)。
    对于一个结点,如果有双亲,其双亲的下标编号为[i/2],如果有左孩子,则左孩子下标为[2i],如果有右孩子,则右孩子下标为[2i+1]。
    二叉树的链式存储结构
    优点:
    对于一般二叉树,可以较大节省存储空间,在二叉链中访问一个结点的孩子很方便,
    缺点:但访问他的双亲结点,则需要遍历所有节点。即,查找前驱结点很麻烦。

1.1.2 二叉树的构造

先序遍历建树

BTree CreatTree(string str,int &i)//每一次递归都改变i的值,以此达到先建立根结点再建立左子树最后建立右子树的目的;
{
   BTree bt;
   if(i>len-1)
      return NULL;
   if(str[i]=='#')
      return NULL;
   bt = new TNode;
   bt->data=str[i];
   bt->lchild= CreatTree(str,++i);
   bt->rchild= CreatTree(str,++i);
   return bt;
}

二叉树的顺序存储结构转换成二叉链

BTree CreateTree(string str, int i)
{
    BTree BT;
    if (i > str.size())
        return NULL;
    if (str[i] == '#')
        return NULL;
    BT = new BTNode;
    BT->data = str[i];
    BT->lchild = CreateTree(str, 2 * i);//如果i从0开始,2*i+1
    BT->rchild = CreateTree(str, 2 * i+1);//2*i+2
    return BT;
}

层次遍历建二叉树

while(队伍不空)
   队伍中出列一个节点T;
   去字符str[i];
   if(str[i]=='#')
      T->lchild=NULL;
   else
      生成T的左孩子结点,值为str[i],把T->lchild入队;
   end if
   取str下一个字符;
   if(str[i]=='#')
      T->rchild=NULL;
   else
      生成T的右孩子结点,值为str[i],把t->rchild入队;
   end if
end while

1.1.3 二叉树的遍历

  • 先序遍历
  1. 访问根节点;
  2. 访问当前节点的左子树;
  3. 若当前节点无左子树,则访问当前节点的右子树;
  • 中序遍历
  1. 访问当前节点的左子树;
  2. 访问根节点;
  3. 访问当前节点的右子树;
  • 层次遍历
    1.访问根节点,第一层
    2.从左到右访问第二层节点
    3.从左到右访问第三层节点,.....访问第n层节点;
  • 后序遍历
  1. 访问当前节点的左子树;
  2. 访问当前节点的右子树;
  3. 访问根节点;

1.1.4 线索二叉树

在二叉树的结点上加上线索的二叉树称为线索二叉树,对二叉树以某种遍历方式(如先序、中序、后序或层次等)进行遍历,使其变为线索二叉树的过程称为对二叉树进行线索化
线索化:
现将某结点的空指针域指向该结点的前驱后继,定义规则如下:

若结点的左子树为空,则该结点的左孩子指针指向其前驱结点。
若结点的右子树为空,则该结点的右孩子指针指向其后继结点。

这种指向前驱和后继的指针称为线索。将一棵普通二叉树以某种次序遍历,并添加线索的过程称为线索化。

中序线索二叉树特点:
加上线索的二叉树结构是一个双向链表结构,为了便于遍历线索二叉树,我们为其添加一个头结点,头结点左孩子指向原二叉树的根结点,右孩子指针指向中序遍历的最后一个结点。同时,将第一个结点左孩子指针指向头结点,最后一个结点的右孩子指针指向头结点。

1.1.5 二叉树的应用--表达式树

*表达式树构造

void InitExpTree(BTree& T, string str) //建二叉表达式树 
{
    stack<char>op;
    stack<BTree>t;
    int i = 0;
    BTree p = NULL, a, b;//结点
    while (str[i])
    {
        if (!In(str[i]))
        {
            p = new BTNode;
            p->data = str[i];
            p->lchild = NULL;
            p->rchild = NULL;
            t.push(p);
        }
        else
        {
            if (op.empty())
            {
                op.push(str[i]);
            }
            else
            {
                switch (Precede(op.top(), str[i]))
                {
                case'<':
                    op.push(str[i]); break;
                case'=':
                    op.pop(); break;
                case'>':
                    a = t.top();
                    t.pop();
                    b = t.top();
                    t.pop();
                    CreateExpTree(p, b, a, op.top());
                    op.pop();
                    t.push(p);
                    i--;
                    break;
                }
            }
        }
        i++;
    }
    while (!t.empty() && !op.empty())
    {
        b = t.top();
        t.pop();
        a = t.top();
        t.pop();
        CreateExpTree(p, a, b, op.top());
        op.pop();
        t.push(p);
    }
    T = p;
}
  • 计算表达式树
double EvaluateExTree(BTree T)//计算表达式树 
{
    double sum = 0;
    double a, b;
    if (T->lchild == NULL && T->rchild == NULL)
    {
        return T->data - '0';
    }
    a = EvaluateExTree(T->lchild);
    b = EvaluateExTree(T->rchild);
    switch (T->data)
    {
    case'+':
        return a + b;
        break;
    case'-':
        return a - b;
        break;
    case'*':
        return a * b;
        break;
    case'/':
        if (b==0)
        {
            cout << "divide 0 error!";
            exit(0);
        }
        return a / b;
        break;
    }
}

1.2 多叉树结构

1.2.1 多叉树结构

双亲存储结构:

typedef struct
{
	ElemType data;
	int parent;//存放双亲
};

孩子链存储结构:

typedef struct node
{
	ElemType data;
	struct node *son;//孩子结点
};

孩子兄弟链存储结构

typedef struct node
{
	ElemType data;
	struct node *son;//孩子结点
	struct node *brother;//兄弟结点
};

1.2.2 多叉树遍历

树的遍历运算是 指按某种方式访问树中的每一个结点且每一个结点只被访问一次。

  • 先根遍历(递归)(根左右)
    若树不空,则先访问根结点,然后依次先根遍历各棵子树。
  • 后根遍历(递归)(左右根)
    若树不空,则先依次后根遍历各棵子树,然后访问根结点。
  • 层次遍历
    若树不空,则自上而下、自左至右访问树中每个结点。

1.3 哈夫曼树

1.3.1 哈夫曼树定义

给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。

原则:权值越大的叶结点越靠近根结点;权值越小的叶结点越远离根结点。

1.3.2 哈夫曼树的结构体

typedef struct
{	char data;		//节点值
	float weight;	        //权重
	int parent;		//双亲节点
	int lchild;		//左孩子节点
	int rchild;		//右孩子节点
} HTNode;

1.3.3 哈夫曼树构建及哈夫曼编码

  • 哈夫曼树构建:
    假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,则哈夫曼树的构造规则为:
    (1) 将w1、w2、…,wn看成是有n 棵树的森林(每棵树仅有一个结点);
    (2) 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
    (3)从森林中删除选取的两棵树,并将新树加入森林;
    (4)重复(2)、(3)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。
  • 哈夫曼编码:
    利用哈夫曼树求得的二进制编码称为哈夫曼编码。树中从根到每个叶子节点都有一条路径,对路径上的各分支约定指向左子树的分支表示”0”码,指向右子树的分支表示“1”码,取每条路径上的“0”或“1”的序列作为各个叶子节点对应的字符编码,即是哈夫曼编码。

1.4 并查集

并查集是一种树型的数据结构,用于处理一些不相交集合(disjoint sets)的合并及查询问题。常常在使用中以森林来表示。

  • 结构体
typedef struct node
{
     int data;
     int rank;
     int parent;
}UFSTree;
  • 初始化
int fa[MAXN];
inline void init(int n)
{
    for (int i = 1; i <= n; ++i)
        fa[i] = i;
}
  • 查询
int find(int x)
{
    if(fa[x] == x)
        return x;
    else
        return find(fa[x]);
}
  • 合并
inline void merge(int i, int j)
{
    fa[find(i)] = find(j);
}

1.5 对树的认识及学习体会

本章主要学习了树的结构,属于非线性结构。线性结构是一对一关系,而树结构是一对多的关系。树结构有二叉树,线索二叉树和哈夫曼树,二叉树由一个根节点和左子树和右子树组成。线索二叉树是利用空余的指针指向结点前驱,而哈夫曼树利用带权路径解决最优问题。树结构的运算大部分都是递归运算,所以只有用好递归才能更加的熟悉树的操作。一般递归体分为左子树和右子树,递归口为结点为空或者其他条件。对树结构有了一定的学习后,发现代码调试时很困难,因为有些复杂了,但也为多对多关系打了基础。

2. PTA实验作业

2.1 输出二叉树每层节点

2.1.1 解题思路及伪代码

解题思路:构建正常二叉树的结构体,加入h高度,后用先序建树的方法存放树,后需要用层次遍历的方法,新建一个队存放,并建立一个哈希数组进行换行的判断,while判断队列是否为空,存放队头的左右孩子,判断元素的位置并且输出,后出队继续操作。
伪代码:

BTree CreateBTree(string str, int &i, int h)//存放树
{
	length = str长度;
	if (i == length) return NULL;
	if (遇到'#') return NULL;
	新建bt结点;
	先序递归存放;
}
void level(BTree T)
{
	建队q;
	T入队;
	if (T为空) return NULL;
	static int A[10];
	int flag = 1;//第一行
	while (q不为空)
	{
		T左右孩子入队;
		if (flag == 1 && 哈希对应数值为0)
		{
			输出;
			flag = 0;
		}
		else if (flag == 0 && 哈希对应数值为0)
		{
			换行输出;
		}
		else
		{
			输出;
		}
		q出队;
	}
}

2.1.2 总结解题所用的知识点

该题在层次遍历的基础上进行输出操作,需要用到先序建树以及层次遍历树以及递归函数,层次遍历树则需要引入队列的操作,控制换行输出则用到哈希数组。

2.2 目录树

2.2.1 解题思路及伪代码

  • 分析
    此题目文件树需要用左孩子右兄弟的二叉链表存储

  • 建树
    注意输出的顺序,即同层目录排在文件前,同类按字典顺序输出

  • 插入作为孩子
    没有第一个孩子,直接生成第一个孩子结点
    有孩子,则需要判断是否需要更改孩子
    结点存在,即数据相等且目录文件相等,则不需要新建孩子结点,当前指针更改为孩子位置
    若孩子为文件内,新结点为目录则更改孩子为新结点
    若孩子文件属性为同新结点,但是值比新结点大,则需更改当前指针

  • 插入作为兄弟
    没有兄弟,则插入新结点为兄弟
    有兄弟,找新节点插入位置
    若新结点属性和兄弟属性相等且值大于兄弟,则继续遍历下个兄弟
    若新节点为文件,兄弟为目录,则继续遍历下个兄弟
    遍历中发现兄弟是文件,新结点是目录,退出循环
    遍历结束,兄弟结点值和新结点值先弄个等,则不需要插入新结点,更改当前指针为兄弟
    插入新结点,更改新结点兄弟关系,并把插入位置的前一个结点的兄弟改成新结点

  • 伪代码

void CreatTree(Tree& bt, string str, int i)//建树,
{
    设置字符
       if 该段字符串为目录,isfile改为false;
       if (temp为文件)
           InitFile(temp, bt);//插入文件
       else //temp为目录
           InitList(temp, bt);//插入目录
    CreatTree(temp, str, i);
}

void InitList(Tree& temp, Tree& bt)//插入目录
{
    定义结构体指针btr来遍历二叉树bt;
        btr = bt->child;//btr先指向bt的孩子;

        /*先对第一个兄弟结点进行判断*/
    if (没有第一个孩子或者 btr为文件 或者 第一个孩子字典序大于该结点)//可插入
        进行插入temp->brother = btr;bt->child = temp;//修改孩子指针
    else if (二者相等)
        直接使temp指向btr;
    else //查找兄弟节点
        while 为空
            if (兄弟节点为文件 || 兄弟节点字典序大于该节点)
                找到可插入位置,break;
            else if (二者相等)
                直接使temp指向btr->brother;break;
            else
          遍历下一兄弟结点;
    end if
        end while
        if (btr->brother为空 || btr->brother->name != temp->name)
            进行插入temp
    end if
      
}

void InitFile(Tree& temp, Tree& bt)//对文件temp找一个可插入位置
{
   
      结点btr先指向bt的孩子;

    if (第一个孩子为空 || btr为文件 && 结点字典序大于等于该节点)
        进行插入,修改bt的孩子指针;
    else //判断兄弟结点
            if (btr->brother为文件 并且 兄弟节点字典序大于该节点)
                找到可插入位置,break;
            else
         遍历下一个兄弟结点
    end if
        end while
        temp进行插入
    end if
}

3. 阅读代码

3.1 题目及解题代码

*题目
给你一棵 完全二叉树 的根节点 root ,求出该树的节点个数。
完全二叉树 的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2h 个节点。

bool exists(struct TreeNode* root, int level, int k) {
    int bits = 1 << (level - 1);
    struct TreeNode* node = root;
    while (node != NULL && bits > 0) {
        if (!(bits & k)) {
            node = node->left;
        } else {
            node = node->right;
        }
        bits >>= 1;
    }
    return node != NULL;
}

int countNodes(struct TreeNode* root) {
    if (root == NULL) {
        return 0;
    }
    int level = 0;
    struct TreeNode* node = root;
    while (node->left != NULL) {
        level++;
        node = node->left;
    }
    int low = 1 << level, high = (1 << (level + 1)) - 1;
    while (low < high) {
        int mid = (high - low + 1) / 2 + low;
        if (exists(root, level, mid)) {
            low = mid;
        } else {
            high = mid - 1;
        }
    }
    return low;
}

3.2 解题思路

除了遍历树来统计结点个数以外,该题可利用完全二叉树的特性,利用二分查找和位运算来得到结点个数的答案。
规定根节点为第0层,完全二叉树最大高度为h,即完全二叉树结点个数为[2h,2(h+1)-1]范围。则在该范围内通过二分查找来确定结点个数。即根据节点个数范围的上下界得到当前需要判断的节点个数 k,如果第 k 个节点存在,则节点个数一定大于或等于 k,如果第 k 个节点不存在,则节点个数一定小于 k,由此可以将查找的范围缩小一半,直到得到节点个数。
那么如何判断第k个结点是否存在呢?

使用位运算的方法,找到二进制数与结点之间的关系,即可使用位运算得到答案。

如果第 k 个节点位于第 h 层,则 k 的二进制表示包含 h+1 位,其中最高位是 1,其余各位从高到低表示从根节点到第 k 个节点的路径,0 表示移动到左子节点,1 表示移动到右子节点。通过位运算得到第 k 个节点对应的路径,判断该路径对应的节点是否存在,即可判断第 k 个节点是否存在。

3.3 知识点

  • 两个函数,一个使用位运算判断该数字结点是否存在,一个使用二分运算确认结点个数
  • 找到二进制树与二叉树结点位置之间的关系,利用其来判断结点是否存在
  • 使用二分运算来提高统计结点的效率
  • 时间复杂度:时间复杂度:O(\log^2 n),其中 n 是完全二叉树的节点数
  • 空间复杂度:O(1)
posted @ 2021-05-05 22:27  -——-  阅读(84)  评论(0编辑  收藏  举报