DS博客作业03--树

0.PTA得分截图


1.本周学习总结

1.1 总结树及串内容

串的BF\KMP算法

BF算法(蛮力法)
应用蛮力法解决串匹配问题的过程是:从主串S的第一个字符开始和子串T的第一个字符进行比较,若相等,则主串和子串都往后移一个字符进行比较;若不相等,则从主串的第二个字符开始和子串的第一个字符重新开始比较,重复上述过程,若子串T中的字符全部比较完毕,则说明本趟匹配成功;否则不成功。这个算法称为朴素的模式匹配算法,简称BF算法。

BF算法匹配过程如图所示:

伪代码描述思路如下:

while 主串字串均未结束
	if 主串和字串对应位置匹配
		继续匹配
	else 
		回溯到初始位置的下一位
	end for
end while
if 匹配成功 then 返回初始位置
else 返回-1(代表匹配失败)

附上具体代码:

int BF(string S,string sub)
{
    int i=0;
    int j=0;

    while(S[i]!='\0'&&sub[j]!='\0')
    {
        if(S[i]==sub[j])
        {
            i++;
            j++;
        }
        else
        {
            i=i-j+1;
            j=0;
        }
    }
    if(sub[j]=='\0') return i-j;
    else return -1;
}

时间复杂度:极端情况下,要比较 n-m+1 次,每次需要比对 m 个字符。所以最坏情况时间复杂度O(n*m)。

常用原因:
虽然复杂度高,但实际开发中很常用,原因如下:
1.大部分情况下,模式串和主串的长度都不会太长。每次模式串与主串中的子串匹配时,当中途遇到不能匹配的字符的时候,就可以就停止了,不需要把 m 个字符都比对;
2.算法思想简单,代码实现简单。简单意味着不容易出错,如果有 bug 也容易暴露和修复。

KMP算法

KMP算法要解决的问题就是在字符串(也叫主串)中的模式(pattern)定位问题。说简单点就是我们平时常说的关键字搜索。

KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是实现一个next()函数,函数本身包含了模式串的局部匹配信息。时间复杂度O(m+n)

使用KMP算法匹配的一个样例如下:

1.最开始匹配时前四个字符必定匹配失败,不断后移

  1. P[1]跟S[5]匹配成功,P[2]跟S[6]也匹配成功, ...,直到当匹配到P[6]处的字符D时失配(即S[10] != P[6]),由于P[6]处的D对应的next 值为2,所以下一步用P[2]处的字符C继续跟S[10]匹配

  2. 向右移动4位后,P[2]处的C再次失配,由于C对应的next值为0,所以下一步用P[0]处的字符继续跟S[10]匹配

  3. 移动两位之后,A 跟空格不匹配,模式串后移1 位。

  4. P[6]处的D再次失配,因为P[6]对应的next值为2,故下一步用P[2]继续跟文本串匹配

  5. 匹配成功,过程结束。

KMP算法的关键在于求得所需的next数组,这个不过多赘述,我们直接来看看nextval数组(next数组的改进)的求取过程
伪代码描述流程如下:

初始化nextval[0]为-1
while 模式串未结束
	if k==-1 或者 对应位置匹配
		i++;k++
		if 对应位置匹配
			修正nextval[i]的值
		else
			nextval[i]=k
		end if
	else
		移动到nextval[k]处比较
	end if
end while
返回nextval数组

附上具体实现代码:

int* GetNext(string str, int nextval[])
{
    int i = 0;
    int k = -1;
    int len = str.size();

    nextval[0] = -1; 
    while (i < len-1)
    {
        if (k == -1 || str[i] == str[k])
        {
            i++;k++;
            if (str[i] == str[k])
            {
                nextval[i] = nextval[k];
            }
            else
            {
                nextval[i] = k;
            }
        }
        else
        {
            k = nextval[k];
        }
    }
    return nextval;
}

得到nextval数组之后,使用KMP算法进行匹配就比较简单了
KMP算法伪代码如下:

获得模式串的nextval数组
while 主串模式串均未结束
	if j==-1 或者 对应位置匹配
		移动到下一位继续匹配
	else
		回溯到nextval[j]处匹配
	end if
end while
if 匹配成功 then 返回初始位置
else 返回-1
	

附上具体实现代码:

int KMP(string str, string sub)
{
    int i = 0;
    int j = 0;
    int str_len = str.size();
    int sub_len = sub.size();
    int nextval[MAXSIZE];

    GetNext(sub,nextval);
    while (i < str_len && j < sub_len)
    {
        if (j == -1 || str[i] == sub[j])
        {
            i++;
            j++;
        }
        else
        {
            j = nextval[j];
        }
    }
    if (j == sub_len)
    {
        return i - sub_len;
    }
    else
    {
        return -1;
    }
}

二叉树存储结构、建法、遍历及应用

二叉树存储结构

  • 顺序存储结构

二叉树的顺序存储结构是指用一组地址连续的存储单元依次自上而下、自左至右存储完全二叉树上的结点元素,即将完全二叉树上编号为i的结点元素存储在某个数组下标为i-1的分量中,然后通过一些方法确定结点在逻辑上的父子和兄弟关系,例如数组中的一个结点tree[i],其双亲为tree[i/2],其左孩子为tree[2i],其右孩子为tree[2i+1]

完全二叉树和满二叉树采用顺序存储比较合适,节省空间且数组元素下标值能确定结点在二叉树中的位置和结点之间的关系。但对于一般二叉树,则需要添加一些并不存在的空结点,所以效率并不高。

顺序存储示意图如下:

  • 链式存储结构

顺序存储的空间利用率比较低,所以二叉树一般都采用链式存储结构。链式结构是指用一个链表来存储一棵二叉树,二叉树中的每个结点用链表的一个链结点来存储。

在二叉树中,结点结构通常包括若干数据域和若干指针域。二叉链表至少包含3个域:数据域data、左指针域lchild和右指针域rchild,若下图所示:

常用的二叉链表存储结构如下图所示:

二叉树的链式存储结构描述如下:

typedef struct BTNode{
    ElemType data;                      //数据域
    struct BTNode *lchild, *rchild;    //左、右孩子指针
}BTNode, *BTree;

注意:
1.在含有n个结点的二叉链表中,含有n+1个空链域。
2.使用不同的存储结构时(此外还有孩子兄弟链等结构),实现二叉树操作的算法也会不同,因此要根据实际应用场合选择合适的存储结构。

二叉树建法

1.顺序存储结构转为二叉链存储结构

BTree CreateBTree(string str, int i)
{
    if (str[1] == '\0') return NULL;
    if (i > str.size() - 1) return NULL;
    if (str[i] == '#') return NULL;

    BTree bt;
    bt = new BTNode;
    bt->data = str[i];
    bt->lchild = CreateBTree(str, 2 * i);
    bt->rchild = CreateBTree(str, 2 * i + 1);
    return bt;
}

2.先序遍历创建二叉树

BTree CreateBTree(char* str, int& i)
{
    if (str[i] == '#') return NULL;
    if (str[i]=='\0') return NULL;

    BTree bt;
    bt = new BTNode;
    bt->data = str[i];
    bt->lchild = CreateBTree(str, ++i);
    bt->rchild = CreateBTree(str, ++i);
    return bt;
}

3.根据先序遍历序列和中序遍历序列创建二叉树

BTree CreateBTree(char* pre,char* in,int n)
{
    if (n <= 0) return NULL;

    BTree bt;
    char* pos;
    int k;

    bt = new BTNode;
    bt->data = pre[0];
    for (pos = in;pos < in + n;pos++)
    {
        if (*pos == *pre) break;
    }
    k = pos - in;
    bt->lchild = CreateBTree(pre + 1, in, k);
    bt->rchild = CreateBTree(pre + k + 1, pos + 1, n - k - 1);
    return bt;
}

创建过程如图所示:

4.根据后序遍历序列和中序遍历序列创建二叉树

BTree CreateBTree(int* post, int* in, int n)
{
    if (n <= 0) return NULL;

    BTree bt;
    int* pos;
    int k;

    bt = new BTNode;
    bt->data = post[n-1];
    for (pos = in;pos < in + n;pos++)
    {
        if (*pos == post[n-1]) break;
    }
    k = pos - in;
    bt->lchild = CreateBTree(post, in, k);
    bt->rchild = CreateBTree(post + k, pos + 1, n - k - 1);
    return bt;
}

创建过程如图所示:

5.层次遍历建树

void CreateBTree(BTree &BT,string str)
{     
	BTree  T;
	int i=0;
      	quueue<BTree> qu;
	
	if( str[0]!='0' )
	{
		BT =new BTNode;
		BT->data = str[0];
		BT->lchild=BT->rchild=NULL;
		qu.push(BT);
	}
	else BT=NULL; 
	while( !qu.empty())
	{
		T = qu.front();
		qu.pop();
		i++;
		if(str[i]=='0' )  T->lchild = NULL;
		else 
		{  
			T->lchild = new BTNode;
			T->lchild->data = str[i];
			T->lchild->lchild=T->lchild->rchild=NULL;
			qu.push(T->lchild);
		}
                       	i++; 
		if(str[i]=='0')  T->rchild = NULL;
		else 
		{  
			T->rchild = new BTNode;;
			T->rchild->data = str[i];
			T->rchild->lchild=T->rchild->rchild=NULL;
			qu.push(T->rchild);
		}
	} 
}

二叉树遍历

1.先序遍历
(注:中序及后序遍历只是改变输出语句的次序,不再赘述)

void PreOrder(BTree bt)
{
    if (bt == NULL) return;
    cout << " " << bt->data;
    PreOrder(bt->lchild);
    PreOrder(bt->rchild);
}

2.层次遍历

void LevelOrder(BTree root)
{
    if (root == NULL) 
    {
        cout << "NULL";
        return;
    }

    queue<BTree> qu;
    BTree tmp;
    int flag = 1;

    qu.push(root);
    while (!qu.empty())
    {
        tmp = qu.front();
        qu.pop();
        if (flag)
        {
            cout << tmp->data;
            flag = 0;
        }
        else cout << " " << tmp->data;
        if (tmp->lchild) qu.push(tmp->lchild);
        if (tmp->rchild) qu.push(tmp->rchild);
    }
}

二叉树应用

1.哈夫曼编码,来源于哈夫曼树(给定n个权值作为n个叶子结点,构造一棵二叉树,若带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为赫夫曼树(Huffman tree)。即带权路径长度最短的树),在数据压缩上有重要应用,提高了传输的有效性,详见《信息论与编码》。
2.海量数据并发查询,二叉树复杂度是O(K+LgN)。二叉排序树就既有链表的好处,也有数组的好处, 在处理大批量的动态的数据是比较有用。
3.C++ STL中的set/multiset、map,以及Linux虚拟内存的管理,都是通过红黑树去实现的。查找最大(最小)的k个数,红黑树,红黑树中查找/删除/插入,都只需要O(logk)。
4.B-Tree,B+-Tree在文件系统中的目录应用。
5.路由器中的路由搜索引擎。

树的结构、操作、遍历及应用

树的结构

  • 二叉排序树(二叉查找树)

1.左子树上所有结点的值均小于根结点
2.右子树上所有结点的值均大于根结点
3.左右子树均为二叉排序树

  • 平衡二叉树

1.二叉排序树
2.根节点的左右子树深度差最多相差1
3.根节点的左右子树也是平衡二叉树

  • LL 型:支撑点转换、顺时针旋转、旋转优先原则整理

  • RR 型:支撑点转换、逆时针旋转、旋转优先原则整理

  • LR 型:左子树支撑点转换、逆时针旋转、根支撑点转换、顺时针旋转

  • RL 型:右子树支撑点转换、顺时针旋转、根支撑点转换、逆时针旋转

  • 红黑树

1.每个结点要么是红色,要么是黑色
2.根节点必须是黑色
3.红色结点不能连续
4.任何一个结点到叶子的所有路径都包含相同数目的黑色节点
5.任何一个结点的左右子树高度差不会超过矮的一方(接近平衡的二叉树)

树的操作及遍历

1.先序创建树

bintree creatree(bintree &root)//先序创建树
{
	char a=getchar();
	if(a==' ')
	{
		root=NULL;	
		return root;
	}
	else
	{
		root=(bintree)malloc(sizeof(bitnode));
		root->data=a;
		
		creatree(root->lchild);
		creatree(root->rchild);	
		return root;
	}
}

2.求度为一节点的个数

int CountDegreeOne(bintree root)//度为一节点的个数
{
	if(root==NULL)
	return 0;
	else 
	{
		int a=CountDegreeOne(root->lchild);
		int b=CountDegreeOne(root->rchild);
		if(root->lchild&&!root->rchild||root->rchild&&!root->lchild)		
		return a+b+1;
		else
		return a+b;
	}	
 } 

3.树的深度

int DeepTree(bintree root)//树的深度
{
	if(root==NULL)
	return 0;
	else
	{
		int a=DeepTree(root->lchild);
		int b=DeepTree(root->rchild);
		if(a>=b)
		return a+1;
		else
		return b+1;
	}
}

4.判断两棵树是否相同

int same(bintree root,bintree root1)//判断两棵树是否相同
{
	if(root&&!root1||root1&&!root||!root&&!root1)
	{
		return 0;
	}
	else
	{
		if(root->data==root1->data)
		{
			int a=same(root->lchild,root1->lchild);
			int b=same(root->rchild,root1->rchild);
			if(a==b==1)
			return 1;
		}
		else		
		return 0;
	}
}

树的应用

1.二叉排序树
二叉排序树,也称二叉查找树,二叉搜索树,或BST。二叉排序树或者为一个空树,或者为一个满足下列特性的非空二叉树:
1)若左子树非空,则左子树上所有结点关键字值均小于根结点的关键字值。
2)若右子树非空,则右子树上所有结点关键字值均大于根结点的关键字值。
3)左、右子树本身也是一颗二叉排序树。
对二叉排序树进行中序遍历,可以得到一个递增的有序序列。
判断一颗二叉树是不是二叉排序树

2.平衡二叉树
为了避免树的高度增长过快,降低二叉排序树的性能,我们规定在插入和删除二叉树结点时,要保证任意结点的左、右子树高度差的绝对值不超过1,并将这样的二叉树称为平衡二叉树(AVL)。定义结点左子树和右子树的高度差为该结点的平衡因子,则平衡二叉树结点的平衡因子的值只可能是0、-1或1.

3.红黑树
红黑树也是被广泛运用的平衡二叉搜索树,RB-tree要满足如下规则:
1.每个节点不是红色就是黑色
2.根节点为黑色
3.如果节点为红,其子节点比为黑
4.任一节点至NULL(树尾端)的任何路径,所含之黑节点数必须相同。

4.哈夫曼树及哈夫曼编码
哈夫曼树的构造
给定N各权值分别为w1,w2,…,wN的节点。构造哈夫曼树的算法如下:
1)将这N个结点分别作为N颗仅含一个结点的二叉树,构成森林F.
2)构造一个新阶段,并从F中选取两个颗根节点权值最小的树作为新节点的左、右子树
3)从F中删除刚才选出的两棵树,同时将新得到的树加入F中。
4)重复步骤2)3),直到F中只剩下一棵树为止。

哈夫曼编码
构造哈夫曼编码首先要构造一颗哈夫曼树。首先,将每个出现的字符当做一个独立的结点,其权值为它出现的频度,然后构造出对于的哈夫曼树。其中边标记为0表示“转向左孩子”,标记为1表示“转向右孩子”。树中所有叶节点的带权路径长度之和称为该树的带权路径长度,记为:
WPL= ∑wi*li(i=1…n)
wi是第i个叶节点所带的权值,li是该叶节点到根节点的路径长度。
利用哈夫曼树可以设计出总长度最短的二进制前缀编码。

线索二叉树

通过考察各种二叉链表,不管儿叉树的形态如何,空链域的个数总是多过非空链域的个数。准确的说,n各结点的二叉链表共有2n个链域,非空链域为n-1个,但其中的空链域却有n+1个。如下图所示。

因此,提出了一种方法,利用原来的空链域存放指针,指向树中其他结点。这种指针称为线索。

记ptr指向二叉链表中的一个结点,以下是建立线索的规则:

  • 如果ptr->lchild为空,则存放指向中序遍历序列中该结点的前驱结点。这个结点称为ptr的中序前驱;
  • 如果ptr->rchild为空,则存放指向中序遍历序列中该结点的后继结点。这个结点称为ptr的中序后继;

显然,在决定lchild是指向左孩子还是前驱,rchild是指向右孩子还是后继,需要一个区分标志的。因此,我们在每个结点再增设两个标志域ltag和rtag,注意ltag和rtag只是区分0或1数字的布尔型变量,其占用内存空间要小于像lchild和rchild的指针变量。结点结构如下所示。

其中:

  • ltag为0时指向该结点的左孩子,为1时指向该结点的前驱;
  • rtag为0时指向该结点的右孩子,为1时指向该结点的后继;
  • 因此对于上图的二叉链表图可以修改为下图的样子。

哈夫曼树

哈夫曼树的学术定义为,带权路径长度最短的二叉树,即节点具有两个属性:

1、权值,可以看作节点表达出的数值大小,或者变换的表示为概率大小

2、路径,可以看作由根节点到达当前节点的分支数,左分支和右分支具有不同意义

带权路径长度即为权值与路径乘积的累加,所以哈夫曼树首先是一棵二叉树,其次通过调整二叉树节点位置,使得带权路径长度最小。

上图示例中,灰色节点为给出的原始节点,白色节点为在根据灰色节点构造哈夫曼树时而产生的辅助节点。

由上例可得出哈夫曼树的构造过程:

1、当节点序列中的根节点数量多于一个时,从当前节点序列中选择两个权值最小的根节点,分别作为左右子节点,创建新的根节点,如选择1,2节点,创建节点3

2、从序列中删除上一步选择的两个根节点,将新创建的根节点加入序列

重复执行以上两步,最终节点序列中剩余的一个节点即为最终的根节点。

以节点:[1,2,3,7],为例,创建过程如下

初始状态:四个节点,按照权值由小到大排列

第一步:选择两个权值最小的根节点,即a,b两节点,构建新根节点,规定左子节点权值不大于右子节点权值

第二步:按照规则,继续选择两个权值最小的根节点,构建新根节点

attention:在该步骤中,两个值为3的节点,选择哪一个作为左子节点,按照本程序中实现方式,新创建的节点在添加到序列中时,是从序列起始处进行判断,不大于当前节点权值则设置为当前节点的前面(选择最小权值节点时选择前两个,所以前面一个是左子节点,后面一个是右子节点,此处规矩设置不同,会产生不同的路径)

第三步:依照规则执行

至此,哈夫曼树已经建成

附上具体实现代码:

bool Compare(BTree bt1, BTree bt2)
{
    return bt1->weight < bt2->weight;
}
BTree CreateHuffmanTree(vector<BTree> nodes)
{
    BTree node;
    int i;

    sort(nodes.begin(), nodes.end(), Compare);
    while (nodes.size() != 1)
    {
        node = new BTNode;
        node->weight = nodes[0]->weight + nodes[1]->weight;
        node->lchild = nodes[0];
        node->rchild = nodes[1];
        nodes.erase(nodes.begin());
        nodes.erase(nodes.begin());
        for (i = 0;i < nodes.size();i++)
        {
            if (nodes[i]->weight > node->weight) break;
        }
        nodes.insert(nodes.begin() + i, node);
    }
    return nodes[0];
}

并查集

并查集是一种树型的高级数据结构,主要用于处理不相交集合的合并及查询问题。它在计算机科学中有着广泛的应用,例如求解最小生成树、亲戚关系的判定、确定无向图的连通子图个数、最小公共祖先问题等,都要用到并查集。

并查集的三种基本操作如下所示:
1.初始化

void MakeSet(UFSLink t, int n)
{
    int i;

    for (i = 1;i <= n;i++)
    {
        t[i].data = i;
        t[i].parent = i;
        t[i].rank = 0;
    }
}

2.查找所属集合

int FindSet(UFSLink t, int x)
{
    if (x != t[x].parent)
        return FindSet(t, t[x].parent);
    else return x;
}

3.合并两个集合

void Union(UFSLink t, int x, int y)
{
    x = FindSet(t, x);
    y = FindSet(t, y);
    if (t[x].rank > t[y].rank)
        t[y].parent = x;
    else
    {
        t[x].parent = y;
        if (t[x].rank == t[y].rank)
            t[y].rank++;
    }
}

1.2.谈谈你对树的认识及学习体会

树是一种应用广泛的数据结构,例如,可以用于哈夫曼编码,在数据压缩上有重要应用,提高了传输的有效性;可以用于海量数据并发查询,快速处理大批量的动态的数据。有些很复杂的问题,如果应用了树结构,可能就会迎刃而解。同时树结构也是比较抽象的一种非线性数据结构,在学习的过程中难免遇到难以理解的地方,学习起来自然会更难一些,不过只要肯花时间和精力下去,一定会有所收获的。


2.阅读代码

2.1 题目及解题代码

题目

题解

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    vector<int> v;
    vector<int> res;
    int max=1;
    int cur=1;
    vector<int> findMode(TreeNode* root) {
        inOrder(root);
        if(v.size()==0) return res;//处理输入为空的情况
        res.push_back(v[0]);//初始化res数组
        for(int i=1;i<v.size();i++)//求数组v的众数
        {
            if(v[i]==v[i-1])
                cur++;
            else
                cur=1;
            if(cur==max)
                res.push_back(v[i]);
            else if(cur>max)
            {
                res.clear();
                max=cur;
                res.push_back(v[i]);
            }
        }
        return res;
    }
    
    void inOrder(TreeNode* root)//中序遍历
    {
        if(root==NULL)  return;
        inOrder(root->left);
        v.push_back(root->val);
        inOrder(root->right);
    }
};

2.1.1 该题的设计思路

  • 设计思路
    二叉搜索树的遍历是一个升序序列,使用中序遍历将结点的值存到数组v中,然后将问题转化成求数组v的众数。
    求数组v的众数:
    用cur表示当前元素出现的次数,max表示元素出现的最大次数。
    逐个对比v中每一个元素和它之前的元素是否相等,若相等cur++,否则cur=1;
    若cur=max,则将元素添加到数组res中,因为遍历时从第二个元素开始,所以先将v[0]添加到res中,这是为了避免每个元素都出现一次(每个元素都是众数),而v[0]没有添加到res的情况,例如[1,null,2]。
    若cur>max,则清空res数组,将max更新为cur,将元素添加到res中。

  • 时间复杂度:O(n)。中序遍历一次树,也遍历了一次数组。

  • 空间复杂度:O(n)。用vector容器v来存储遍历的结果,用vector容器res来保存众数

2.1.2 该题的伪代码

定义vector容器v来保存中序遍历结果
定义vector容器res来保存众数

if 没有输入 then 返回res
初始化res数组
for 遍历v数组
	if 出现同一个数
		频数cur+1
	else
		重置频数cur
	end if
	if 当前频数和最大频数相同
		把v[i]放入res
	else if 当前频数大于最大频数
		清空res,更新max,把v[i]放入res
	end if
end while

2.1.3 运行结果

2.1.4分析该题目解题优势及难点

  • 仅仅对树进行操作难以找到众数,但是转化为数组之后思路就比较明朗了
  • 根据已知的二叉搜索树(BST)定义要注意到中序遍历的结果应该是升序序列
  • 题解先将v[0]添加到res数组中就可以避免每个元素都出现一次(每个元素都是众数),而v[0]没有添加到res的情况

2.2 题目及解题代码

题目

题解

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    void dfs(TreeNode* root, TreeNode* p, int d, int x, int& depth, TreeNode** parent) {
        if (root == NULL) return;
        if (root->val == x) {
            *parent = p;
            depth = d;
            return;
        }
        dfs(root->left, root, d + 1, x, depth, parent);
        dfs(root->right, root, d + 1, x,  depth, parent);
    }
    bool isCousins(TreeNode* root, int x, int y) {
        int dx = 0;
        int dy = 0;
        TreeNode* px = NULL;
        TreeNode* py = NULL;
        dfs(root, NULL, 0, x, dx, &px);
        dfs(root, NULL, 0, y, dy, &py);
        return (dx == dy) && (px != py);
    }
};

2.2.1 该题的设计思路

  • 设计思路
    利用深度优先搜索寻找到值为x和y的结点的深度和父节点,最后比较深度是否相同和父节点是否相同来判断是否是二叉树的堂兄弟节点

  • 时间复杂度:O(n)。遍历了一次树

  • 空间复杂度:O(1)。定义了dx和dy来记录深度,和px,py来记录父节点,使用了常数空间

2.2.2 该题的伪代码

bool isCousins(TreeNode* root, int x, int y) 
定义dx和dy来记录深度
定义px,py来记录父节点

dfs遍历树搜索x
dfs遍历树搜索y
若深度相同,父节点不相同,则是二叉树的堂兄弟节点
否则不是

void dfs(TreeNode* root, TreeNode* p, int d, int x, int& depth, TreeNode** parent)
if 结点是空结点 then 停止递归
if 结点值为x
	更新父节点和深度
	停止递归
end if
搜索左子树
搜索右子树

2.2.3 运行结果

2.2.4分析该题目解题优势及难点

  • 如何寻找结点值为x和y的父节点和深度是这道题目的难点,也是关键
  • 题解中灵活应用函数参数,在寻找到x和y的同时也记录下了它们的父节点和深度,巧妙解决问题
  • 递归的使用也使得代码简洁优雅

2.3 题目及解题代码

题目

题解

class Solution {
public:
	bool isBalanced(TreeNode* root) {
		if (root == NULL) return true;//如果该子树为空,则一定是平衡的(因为没有左右子树)
		if (abs(getHeight(root->left) - getHeight(root->right)) > 1) return false;
		return isBalanced(root->left)&& isBalanced(root->right);
	}
	int getHeight(TreeNode* root)
	{
		if (root == NULL) return 0;
		int leftHeight = getHeight(root->left);
		int rightHeight = getHeight(root->right);
		return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
	}
};

2.3.1 该题的设计思路

  • 设计思路
    先序遍历每一个节点,并比较左右子树高度,如果高度差>1则返回false

  • 时间复杂度:O(n²)。遍历到一个结点时都要递归计算它的左右子树高度来获得高度差

  • 空间复杂度:O(n)。层层递归都需要定义变量来记录左右子树的高度

2.3.2 该题的伪代码

bool isBalanced(TreeNode* root)
if 树空 then 平衡
if 左右子树高度差大于1 then 不平衡
递归求取左右子树的平衡性

int getHeight(TreeNode* root)
if 结点空 then 返回0
递归求左右子树高度
if 左子树高 then 返回左子树高度+1
else (右子树高) then 返回右子树高度+1

2.3.3 运行结果

2.3.4分析该题目解题优势及难点

  • 如何判断每个结点的平衡性是本题的关键
  • 题解将每个问题分解为左右子树两个子问题,将每个问题解决后,整个问题也就解决了

2.4 题目及解题代码

题目

题解

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    int helper(TreeNode* root,int &tilt)
    {
        if(root==NULL)  return 0;
        int left=helper(root->left,tilt);
        int right=helper(root->right,tilt);
        tilt+=abs(left-right);
        return left+right+root->val;
    }
    
    int findTilt(TreeNode* root) {
        int tilt=0;
        helper(root,tilt);
        return tilt;
    }
};

2.4.1 该题的设计思路

  • 设计思路
    计算整个树的坡度,需要将每个节点的坡度加起来,也就是说,我们需要找出每个节点的左子树上所有节点的值的和 以及 右子树上所有结点的值的和。
    定义一个helper函数,返回当前节点以及左右子树的节点值的和。
    定义一个变量tilt,保存整棵树的坡度,每次得到左右子树的节点和时,更新tilt。

  • 时间复杂度:O(n)。遍历了一遍树

  • 时间复杂度:O(n)。每一层递归都需要定义left和right来记录左右子树的结点之和

2.4.2 该题的伪代码

int helper(TreeNode* root,int &tilt)
if 树空 then 返回0
递归求取左右子树的结点值之和
累加它们的差的绝对值给tilt
返回 左右子树的结点值之和+该结点的值

2.4.3 运行结果

2.4.4分析该题目解题优势及难点

  • 如何计算每个结点的坡度的解决本题的难点,也是重点
  • 题解在求取左右子树结点值之和的同时也在计算坡度,因此整棵树遍历完之后总坡度也就计算出来了,问题也就得到解决了

posted @ 2020-04-12 16:38  朱振豪  阅读(189)  评论(0编辑  收藏  举报