首页 > 技术文章 > DS博客作业03--树

w-y-h-- 2020-04-12 20:19 原文

0.PTA得分截图


1.本周学习总结

1.1 总结树及串内容

串的BF\KMP算法:

  • 串的BF算法:

基本思路:
1.从第一个字符开始和另一串中的第一个字符比较
2.若相等,继续比较后续字符
3.从第一个串的第二个字符开始重新与模式串的第一个字符开始进行比较
4.若从s的n个字符开始,每个字符依次和目标串t中的对应字符相等,则匹配成功,该算法返回字符的下标值;

设串S=“ababcabcacb”,另一串串T="abcac",BF算法:
将S串底标从0开始,从a开始,第一次匹配:

当T未匹配就结束,返回,S串坐标+1,再从b匹配:

遍历到不同值,就向右移匹配,直到最后匹配成功,:

返回T串出现在S串的,其算法复杂度为O(mn)

  • 串的KMP算法

KMP算法较BF算法有较大改进。
改进之处:1.主串不需要回溯i指针
         2.将模式串向右滑动一段下标,往后移
没有了BF算法的回溯思路,是根据所求next数组的值进行改变的

KMP中next的求法:

关于next数组的问题:
next数组表示长度,下标从1开始;在遍历字符串,下标从0开始。

KMP算法如下:


int KMP(string S, string T, int* next)
{
     int i;
     int j;
     i=0;j=0;
     while (i < S.length && j < T.length)//当还没搜索结束就继续
     {
         if (j == -1 || S[i] == T[i])//当找到相同的数值
         {
             i++;//往右移
             j++;
         }
         else {
             j = next[i];

        }
     }
    
      return -1;
 }


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


  • 二叉树的存储结构
    1.二完全二叉树的顺序存储结构
    顺序存储结构


    i结点:父亲的下标:i/2;左孩子的标:2i;右孩子往后:2i+1

    2.非完全二叉树的顺序存储结构
    二叉树先让空结点补成一颗完全二叉树,然后对应


    3.二叉树的链式存储结构
    结点定义如下:


typedef struct node
{     ElemType data;
       struct node *lchild, *rchild;
}   BTNode;      
typedef BTNode *BTree;


typedef struct TNode *Position;
typedef Position BinTree; 
struct TNode{ 
	ElementType Data;
	BinTree Left;     
	BinTree Right;   
};

二叉树存储结构演示:
二叉链:有n个结点,2n个指针域;空指针域个数为2n-(n-1)=n+1个


  • 二叉树构造
    1.二叉树的基本运算及其实现

void CreateBTree(BTree &bt,string str);//创建二叉树 
void PreOrder( BTree bt );//先序遍历二叉树
void InOrder(BTree bt);//中序遍历二叉树
void PostOrder(BTree bt);//后序遍历二叉树 
void LevelOrder(BTree bt);//层次遍历二叉树

2.二叉树创建方法:
二叉树的顺序存储结构转成二叉链

先把二叉树补成完全的二叉树
伪代码:


BTree CreateBTree(string str,int i)
{    
      if(i>字符串长度 || i<=0) return NULL;
      if(str[i]=='#') return NULL;
      创建根结点bt,bt->data=str[i]
      创建左子树:
              bt->lchild =CreateBTree(str,2*i); 
      创建右子树:
             bt->rchild =CreateBTree(str,2*i+1); 
      return bt;	
} 

先序遍历递归建树
ABD#G###CEH###F#I##


BTree CreatTree(string str, int &i)
{
	BTree bt;
	if (i > len - 1) return NULL;
	if (str[i] == '#') return NULL;
	bt = new BTNode;
	bt->data = str[i];
	bt->lchild = CreatTree(str, ++i);
	bt->rchild = CreatTree(str, ++i);
	return bt;
}


  • 二叉树遍历
    1.概念:
    二叉树的遍历是按照先序,中序或者后续来访问结点的,是二叉树的学习最基础的遍历学习
    二叉树组成:
    NLR:先序遍历
    LNR:中序遍历
    LRN:后序遍历

2.先序遍历:

算法:

 访问根节点;
 先序遍历左子树;
 先序遍历右子树。
void PreOrder(BTree bt)
{     if (bt!=NULL)  
      {     printf("%c ",bt->data); 	//访问根结点
             PreOrder(bt->lchild);
             PreOrder(bt->rchild);
      }
}

1.递归调用从根节点开始,最后遍历完所有节点,在根节点结束。
2.结点访问2遍

3.中序遍历

算法:

 中序遍历左子树;
 访问根节点;
 中序遍历右子树。
void InOrder(BTree bt)
{       if (bt!=NULL)  
        {      InOrder(bt->lchild);
	 printf("%c ",bt->data); 	//访问根结点
	 InOrder(bt->rchild);
       }
}

4.后序遍历

算法:


后序遍历左子树;
 后序遍历右子树;
 访问根节点。
void PostOrder(BTree bt) 
{      if (bt!=NULL)  
        {      PostOrder(bt->lchild);
	 PostOrder(bt->rchild);
	 printf("%c ",bt->data); 	//访问根结点
       }
}

5.层次法建树

伪代码:


取字符串第一个数据,创建根结点,入队列
while(队列不空)
{    出队一个节点T
      取一个字符str[i]
     if(str[i]=='0')   T->lchild=NULL
     else  生成T的左孩子,值str[i],并把左孩子入队列。
     取一个字符str[i]  
      if(str[i]=='0')   T->rchild=NULL
     else  生成T的右孩子,值str[i],并把右孩子入队列。
}


  • 二叉树应用
    中缀表达式:A *B + C + D /E

利用两个栈即字符栈和跟结点的栈来运行
while(遍历表达式)
{
   为操作数,入树栈
   为运算符:
       若优先级>栈顶运算符,则入运算符栈
       若小于,出栈,树根栈弹出2个结点建树,新生成树根入树根栈
       若相等,则出栈栈顶运算符
}


  • 树的结构
    1.双亲存储结构
    树中任何结点只有唯一的双亲结点,找父亲容易,找孩子不容易

    uploading-image-873546.png

    定义:
typedef struct 
{  ElemType data;	//结点的值
    int parent;		//指向双亲的位置
} PTree[MaxSize];

2.孩子链式存储结构
缺点:空指针太多;找父亲不容易


定义:


typedef struct node
{      ElemType data;		  //结点的值
        struct node *sons[MaxSons];	      //指向孩子结点
}  TSonNode;


3.孩子兄弟链存储结构
孩子兄弟链存储结构是为每个结点设计3个域:一个数据元素域;第一个孩子结点指针域;一个兄弟结点指针域。每个结点国定只有两个指针域,找父亲不容易
定义:


typedef struct tnode 
{      ElemType data;	//结点的值
        struct tnode *son;  	//指向兄弟
        struct tnode *brother;  //指向孩子结点
} TSBNode;



  • 树的遍历
    树的遍历运算是指按某种方式访问树中的每一个结点且每一个结点只被访问一次。
    1.先根遍历
    若树不空,则先访问根结点,然后依次先根遍历各棵子树。

    遍历结点访问次序: A B E F C D G H I J K
    2.后根遍历
    若树不空,则先依次后根遍历各棵子树,然后访问根结点。
    遍历结点访问次序: E F B C I J K H G D A
    3.层次遍历
    若树不空,则自上而下、自左至右访问树中每个结点。
    先根和后根遍历算法都是递归的。
    遍历结点访问次序: A B C D E F G H I J K

  • 二叉树与树、森林之间的转换
    1.森林、树转换为二叉树


1.相邻兄弟节点加一水平连线
2.除了左孩子和叶子节点,删掉连线
3.水平连线以左边节点轴心旋转45度

2.二叉树还原为一颗树


1.任一节点k,搜索所有右孩子
2.删掉右孩子连线
3.若节点k父亲为k0,则k0和所有k右孩子连线


线索二叉树

  • 线索化二叉树
    1。若结点有左子树,则lchild指向其左孩子;
    否则, lchild指向其直接前驱(即线索);
    2.若结点有右子树,则rchild指向其右孩子;
    否则, rchild指向其直接后继(即线索) 。
    定义:

typedef struct node 
  {      ElemType data;		//结点数据域
         int ltag,rtag;      		//增加的线索标记
         struct node *lchild;		//左孩子或线索指针
         struct node *rchild;		//右孩子或线索指针
  }  TBTNode;		  //线索树结点类型定义 


  • 先序线索二叉树
    LTag=0, lchild域指向左孩子 LTag=1, lchild域指向其前驱
    RTag=0, rchild域指向右孩子 RTag=1, rchild域指向其后继

  • 遍历线索化二叉树
    中序线索二叉树可以找到对应树每个节点的前驱和后继节点。先序和后序线索二叉树无法做到。
    1.找中序遍历的第一个结点
    左子树上处于“最左下”(没有左子树)的结点。
    2.找中序线索化链表中结点的后继
    若无右子树,则为后继线索所指结点;
    否则为对其右子树进行中序遍历时访问的第一个结点。


void ThInOrder(TBTNode *tb)
{      TBTNode *p=tb->lchild;			//p指向根结点
       while (p!=tb)//tb头结点
       {     
              while (p->rtag==0)   p=p->lchild;		//找开始结点
	printf("%c",p->data);			//访问开始结点
	while (p->rtag==1 && p->rchild!=tb)
	{     p=p->rchild;
	       printf("%c",p->data);
	}
	p=p->rchild;
      }
} 

  • 中序线索二叉树创建

TBTNode *pre;		   		//全局变量
TBTNode *CreatThread(TBTNode *b)     //中序线索化二叉树
{    TBTNode *root;
     root=(TBTNode *)malloc(sizeof(TBTNode));  //创建头结点
     root->ltag=0; root->rtag=1;  root->rchild=b;
     if (b==NULL) root->lchild=root;	//空二叉树
     else
     {        root->lchild=b;
	pre=root;             	//pre是*p的前驱结点,供加线索用
	Thread(b);   		//中序遍历线索化二叉树
	pre->rchild=root;    	//最后处理,加入指向头结点的线索
	pre->rtag=1;
	root->rchild=pre;    	//头结点右线索化
     }
     return root;
} 
void  Thread(TBTNode *&p)    		//对二叉树b进行中序线索化
{    if (p!=NULL)	
     {  
             Thread(p->lchild);           		//左子树线索化
             if (p->lchild==NULL)          	//前驱线索化
             {     p->lchild=pre; p->ltag=1;  }	//建立当前结点的前驱线索
             else  p->ltag=0;
             if  (pre->rchild==NULL)	     	//后继线索化
            {     pre->rchild=p;pre->rtag=1;}	//建立前驱结点的后继线索
            else  pre->rtag=0;
            pre=p;
           Thread(p->rchild);  		//递归调用右子树线索化
     }
} 



哈夫曼树、并查集

  • 哈夫曼树
    1.什么是哈夫曼树
    如图:

    它们的带权路径长度分别为:
    图a: WPL=52+72+22+132=54
    图b: WPL=53+23+72+131=48

2.定义:
路径: 树中一个结点到另一个结点之间的分支构成这两个结点之间的路径。

路径长度:路径上的分枝数目称作路径长度。

树的路径长度:从树根到每一个结点的路径长度之和。

结点的带权路径长度:在一棵树中,如果其结点上附带有一个权值,通常把该结点的路径长度与该结点上的权值之积称为该结点的带权路径长度

树的带权路径长度:如果树中每个叶子上都带有一个权值,则把树中所有叶子的带权路径长度之和称为树的带权路径长度。 设某二叉树有n个带权值的叶子结点,则该二叉树的带权路径长度记为:

3.构造哈夫曼树原则:
权值越大的叶结点越靠近根结点
权值越小的叶结点越远离根结点

4.如何构造哈夫曼树
(1)将所有左,右子树都为空的作为根节点。

(2)在森林中选出两棵根节点的权值最小的树作为一棵新树的左,右子树,且置新树的附加根节点的权值为其左,右子树上根节点的权值之和。注意,左子树的权值应小于右子树的权值。

(3)从森林中删除这两棵树,同时把新树加入到森林中。

(4)重复2,3步骤,直到森林中只有一棵树为止,此树便是哈夫曼树。


  • 并查集

1.并查集是一种用于管理分组的数据结构。它具备两个操作:(1)查询元素a和元素b是否为同一组 (2) 将元素a和b合并为同一组。并查集不能将在同一组的元素拆分为两组。

2.并查集的结构
并查集可以使用树来实现。
使用树形结构来表示以后,每一组都对应一棵树,然而我们就可以将这个问题转化为树的问题了,我们看两个元素是否为一组我们只要看这两个元素的根是否一致。显然,使用树形结构将问题简单化了。合并时是我们只需要将一组的根与另一组的根相连即可。

3.并查集实现


int node[i]; //每个节点
 
//初始化n个节点
void Init(int n){
    for(int i = 0; i < n; i++){
        node[i] = i;
    }
}
//查找当前元素所在树的根节点(代表元素)
int find(int x){
    if(x == node[x])
        return x;
    return find(node[x]);
}
//合并元素x, y所处的集合
void Unite(int x, int y){
    //查找到x,y的根节点
    x = find(x);
    y = find(y);
    if(x == y) 
        return ;
    //将x的根节点与y的根节点相连
    node[x] = y;
}
//判断x,y是属于同一个集合
bool same(int x, int y){
    return find(x) == find(y);

}


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

在树的这块内容中,主要是对于二叉树有很大的学习点,关于树的构建:一种是直接给树的顺序存储结构的字符串,一种是通过先序遍历和中序遍历、或中序遍历和后序遍历来构造树,还 有一种哈夫曼树
 树的遍历:层次遍历要求比较高,层次遍历需要利用环形队列等操作来进行来进行操作。
 线索二叉树并没有去理解
 结构体的构建在树的内蓉是很关键的
 在树中常常会用到递归算法,递归口的设置也是一大难点。


2.阅读代码

2.1 题目及解题代码


class Solution {
public:
    int sumOfLeftLeaves(TreeNode* root) {
        if(root == NULL) return 0;
        if(root->left!=NULL&&root->left->right==NULL&&root->left->left==NULL) return sumOfLeftLeaves(root->right)+root->left->val;
        return sumOfLeftLeaves(root->left)+sumOfLeftLeaves(root->right);
    }
};

2.1.1 该题的设计思路


思路:递归先求左子树的和再求右子树的和,然后两个子树相加的值就是该树的结点值的和;另外把左右孩子又作为根节点,然后再来求其左右孩子,最后再相加就可以了。
先求叶子结点的值再相加,然后判断是不是左子树的结点,再判断左子树是否含有叶结点,然后就可以返回左孩子的叶结点的值再继续向右递归了。
空间复杂度和时间复杂度均为O(n).

2.1.2 该题的伪代码


class Solution {
public:
    int sumOfLeftLeaves(TreeNode* root) {
        if (树为空的条件) return 0;
        if (找到叶结点,即判断左孩子或者右孩子的存在)
            return  这个左孩子的值再加上向右边孩子进行递归计算的值
        return 左孩子为根节点的树的和加上右孩子为根节点的树的和
    }
};

2.1.3 运行结果

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


优势:该题的题目只需要按照递归的想法去完成就可以,所以解题的思路比较清晰
难点:该题有限制条件,即要再加判断为叶结点的条件


2.2 题目及解题代码


class Solution:
    def recoverTree(self, root: TreeNode):
        """
        :rtype: void Do not return anything, modify root in-place instead.
        """
        stack = []
        x = y = pred = None
        
        while stack or root:
            while root:
                stack.append(root)
                root = root.left
            root = stack.pop()
            if pred and root.val < pred.val:
                y = root
                if x is None:
                    x = pred 
                else:
                    break
            pred = root
            root = root.right

        x.val, y.val = y.val, x.val



2.2.1 该题的设计思路


迭代构造中序遍历,在遍历的时候找到相应的结点。尽可能的想左遍历,重复一直到结束。
找到结点后,就记录中序遍历最后一个节点 pred,然后进行比较。如果遍历到的结点值比较小,就说明是要查找的结点。
时间复杂度:O(N)。
空间复杂度:是O(H)




2.2.2 该题的伪代码


class Solution :
    def recoverTree(self, root: TreeNode) :
    """
    : rtype : void Do not return anything, modify root in - place instead.
    """
    stack = []
    x = y = pred = None

    while  栈内的元素或者根节点:
        while 根结点 :
            将根节点入栈;
            遍历左子树;
         取栈顶元素;
            if 如果当前节点的值小于前置节点 pred 的值 :
                交换结点;
                if x is None :
                  x = pred
                else :
                    终止遍历;
             pred = root
             root = root.right

      x.val, y.val = y.val, x.val


2.2.3 运行结果

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


优势:通过迭代构造中序遍历,可以再一次的遍历中找到交换的结点,是复杂度往下降。迭代顺序很简单:尽可能的向左走,然后向右走一步,重复一直到结束。
难点:该题主要用stack来用栈进行操作,比较考验对于栈的灵活应用,对于结点的交换也是难点之一


2.3题目及解题代码


class Solution {
	public List<Integer> inorderTraversal(TreeNode root) {
		List<Integer> res = new ArrayList<Integer>();
		Stack<TreeNode> stack = new Stack<TreeNode>();
		while(stack.size()>0 || root!=null) {
			//不断往左子树方向走,每走一次就将当前节点保存到栈中
			//这是模拟递归的调用
			if(root!=null) {
				stack.add(root);
				root = root.left;
			//当前节点为空,说明左边走到头了,从栈中弹出节点并保存
			//然后转向右边节点,继续上面整个过程
			} else {
				TreeNode tmp = stack.pop();
				res.add(tmp.val);
				root = tmp.right;
			}
		}
		return res;
	}
}

2.3.1该题的设计思路


递归实现时,时再次利用函数去递归调用下一次的循环一样,递归的调用过程是不断往下调用,当递归结束的时候,就打印节点,并转向另一边,然后继续递归调用。在迭代实现时,就可以用栈来模拟上面的调用过程。

时间复杂度:O(n)
空间复杂度:O(h)



2.3.2该题的伪代码


class Solution {
	public List<Integer> inorderTraversal(TreeNode root) {
		List<Integer> res = new ArrayList<Integer>();
		Stack<TreeNode> stack = new Stack<TreeNode>();
		while (树不为空或者栈有元素) {
			//往左子树遍历,遍历到就进栈
			if (树不为空) {
				结点入栈;
				遍历左子树;
				//当前节点为空,说明左子树遍历结束,从栈中取出结点保存下来
				//然后往右开始遍历
			}
			else {
				取出栈顶元素;
				res.add(tmp.val);
				遍历右子树;
			}
		}
		返回结果;
	}
}

2.3.3 运行结果

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


优势:用迭代实现利用栈来作为辅助,是一个很好的解题方法,用栈的调用是该题的优势
难点:难点在于如何用非递归的方式实现。遍历也相对比较复杂


2.4题目及解题代码


/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public boolean isSameTree(TreeNode p, TreeNode q) {
        if(p == null && q == null) 
            return true;
        if(p == null || q == null) 
            return false;
        if(p.val != q.val) 
            return false;
        return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
    }
}

2.4.1该题的设计思路


终止条件与返回值:
当查找到左右边的树的值都为空值的时候,就返回NULL
当其中只有一个树的值为NULL的时候,就返回错误
当两颗树都存在时但是两棵树是完全相同的,返回 false
当满足终止条件时进行返回,不满足时判断两棵树是否是相同的
时间复杂度:O(n)


终止条件:

执行过程:
满足终止条件时返回结果,不满足时判断其左右子树是否相同

2.4.2该题的伪代码


class Solution {
    public boolean isSameTree(TreeNode p, TreeNode q) {
        if (当两棵树的当前节点都为null时)
            return true;
        if (当其中一个为 null 另一个不为 null 时)
            return false;
        if (当两个都不为空但是值不相等时)
            return false;
        return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
    }
}

2.4.3运行结果


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


优势:该解题的判断条件很清晰,难度不大,对于结点的相同与否的判断也利用递归进行
难点:满足终止条件时进行返回,不满足时分别判断左子树和右子树是否相同,其中要注意代码中的短路效应,比较注意递归时的条件

推荐阅读