【二叉树进阶】--- 二叉搜索树转双向链表 最近公共祖先

news2024/11/15 23:48:02

 Welcome to 9ilk's Code World

       

(๑•́ ₃ •̀๑) 个人主页:        9ilk

(๑•́ ₃ •̀๑) 文章专栏:     数据结构


本篇博客我们继续了解一些二叉树的进阶算法。


🏠 二叉搜索 树转化为双向循环链表

📌 题目内容

将二叉搜索树转化为排序好的双向循环链表

 📌 题目解析

  • 双向循环链表所连接的结点是有序的。
  • 题目要求原地转换,也就是说不允许新new结点形成新的链表,而是改变搜索树中结点指针指向。
  • 搜索树中结点的值都是唯一的,我们无需担心出现重复值结点。

📌 算法原理

✏️ 思路一:

  题目要求链表中的节点是排好序的,因此结合二叉搜索树的性质(二叉搜索树中序遍历出来是有序的),我们可以按照对二叉树进行中序遍历,然后依次将节点指针存进vector里,最后遍历vector将各个节点的前驱和后继指针给处理好,最后别忘记头节点前驱指向尾节点,尾节点后继指向头节点。

动图演示:

参考代码:

class Solution {
public:
   void InOrder(Node* root,vector<Node*>& treev) //利用中序遍历 因为二叉搜索树中序是排好序的
   {
     if(root == nullptr)
       return;
     InOrder(root->left,treev);
     treev.push_back(root); //存进数组
     InOrder(root->right,treev);  
   }

    Node* treeToDoublyList(Node* root) 
    {
        if(root == nullptr)
         return root;
       vector<Node*> treev;
       InOrder(root,treev);
       int cur = 1 ;
       Node* prev = treev[0];
       Node* del = treev[1]; 
       while(cur < treev.size()) //调整好指针指向
       {
             del = treev[cur];
             prev->right = del;
             del->left = prev;
             prev = del;
             cur++;
       }
      treev[0]->left = treev[cur-1];
      treev[cur-1]->right = treev[0];   
      return treev[0];
    }
};

分析:这种思路简单,但是空间复杂度达到了O(N)(用vector存节点指针导致),是否有其他思路能优化到O(1),在遍历的同时修改指针指向呢?

✏️ 思路二:

 我们之前创建一个链表除了先提前new出节点再连接外,其实还有一个方法可以动态创建链表。

当我们中序遍历二叉搜索树时就可采取类似的做法。不同的是,我们需要记录前驱节点prev,ptail->next = node,此时的node就是我们中序遍历的当前访问节点,此时ptail需要更新成node(当前访问节点),prev就是上一个按中序被访问节点,所以我们需要在更新ptail之前记录prev,同时更新好前驱和后继指针的指向。

动画演示:

核心步骤:

       ptail->right =root; 
       Node* prev = ptail; //ptail其实就是前驱结点
       ptail = ptail->right; 
       ptail->left = prev

参考代码:

  Node* phead = nullptr;
   Node* ptail = nullptr;
   void InOrder(Node* root) //利用中序遍历 因为二叉搜索树中序是排好序的
   {
     if(root == nullptr)
       return;
     InOrder(root->left);
     if(phead == nullptr) //头结点 也就是搜索树的最左
      phead = ptail = root;
     else 
     {
       ptail->right =root; 
       Node* prev = ptail; //ptail其实就是前驱结点
       ptail = ptail->right; 
       ptail->left = prev;
     }  
     InOrder(root->right);  

   }

    Node* treeToDoublyList(Node* root) 
    {
        if(root == nullptr)
         return root;
         InOrder(root);
         phead->left = ptail;
         ptail->right = phead;
         return phead; 

    }

🏠 二叉树的最近公共祖先

📌 题目内容

二叉树的最近公共祖先

📌 题目解析

  • 注意点1:一个节点也可以是它自己的祖先
  • 注意点2:要找的祖先公共要是最近也就是深度最大。

📌 算法原理

✏️ 思路一:

1.题目要求我们找公共祖先,那我们首要任务是求出节点到根节点路径所经过的节点。

2.我们可以求出每个节点的祖先路径分别装进数组里。

3.求路径:我们可以设计一个递归函数,它的功能是判断子树是否存在目标节点,直到找到目标节点为止

4.找到目标节点之后,我们就可以利用两个哈希表分别遍历数组,表示他们出现过

5.最近的公共祖先一定出现在数组的后面部分,我们可以从后往前遍历祖先路径比较短的数组,发现两个映射关系都确立的就是我们要找的。

参考代码:

bool isLeft(TreeNode* node, TreeNode* del) //看是不是在子树
	{
		if (del == nullptr)
			return false;
		if (del == node) 
			return true;
		return isLeft(node, del->left) || isLeft(node,del->right);
	}

	void ancestor(vector<TreeNode*>& v, TreeNode* node, TreeNode* root, unordered_map<TreeNode*, int>& ump) //装路径进数组的函数
	{
		if (root == nullptr)
			return;
		v.push_back(root); //不论是不是都说明这是target node的祖先
		ump[root]++;
		if (root == node)
		{
			return;
		}
		
		if (isLeft(node, root->left)) //判断在左子树还是右子树
		{
			ancestor(v, node, root->left, ump);
		}
		else
		{
			ancestor(v, node, root->right, ump);
		}

	}


	TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q)
	{
		unordered_map<TreeNode*, int> ump;
		unordered_map<TreeNode*, int> umq;
		//分别找出各自的祖先再进行比较深度
		if (p == root || q == root)
			return root;
		vector<TreeNode*> vp;//存的是p的祖先
		vector<TreeNode*> vq;//存的是q的祖先
		vp.push_back(root);
		vq.push_back(root); 
		ump[root]++;
		umq[root]++;
		if (isLeft(p, root->left))
		{
			ancestor(vp, p, root->left, ump);//在左子树 递归进去
		}
		else
		{
			ancestor(vp, p, root->right, ump); //在右子树
		}
		if (isLeft(q, root->left))
		{
			ancestor(vq, q, root->left, umq);
		}
		else
		{
			ancestor(vq, q, root->right, umq);
		}
		//比较最近祖先
		
		vector<TreeNode*> min = vp;
		if (vp.size() > vq.size())
		{
			min = vq;
		}
	  
		TreeNode* near = nullptr;
		for (int i = min.size() - 1 ; i >= 0; --)
		{
			if (ump[min[i]] && umq[min[i]])
			{
				near = min[i];
                break;
			}
		}

		return near;

	}

这种思路比较简单,但是时间复杂度较大且调用栈空间较多,是一笔不小的开销。

✏️ 思路二:

仔细观察,我们发现思路1:仔细观察一下,两个结点,最近公共祖先的特征就是一个结点在最近公共祖先的左边,一个结点在最近公共祖先的右边。比如图一6和4的公共祖先有5和3,但是只有最近公共祖先5满足6在左边,4在右边。值得注意的是,对于图二这种情况,如果最近公共祖先是p和q其中一个,我们直接返回当前的root即可。

因此有:

  • 我们首先需要一个函数判断结点在哪个子树,这里注意的是,我们可以假设先这个结点在左子树,如果返回false,则说明结点在右子树了,反之在左子树。也就是下方第二个参数我们传root->left即可。
 bool isleftOright(TreeNode* node,TreeNode* root)
    {
        if(root == nullptr)
          return false;
        return root == node ||isleftOright(node,root->left) ||  
               isleftOright(node,root>right);  
    } //非空的话 如果当前节点是要找的直接返回否则在左右子树找 所以用||
  • 若两个结点分别在一左一右,直接返回当前root即可。
  • 若两个结点都在左子树或都在右子树,此时我们需要递归进当前子树的左子树或右子树,继续寻找公共祖先。
  • 在每次确定结点在左子树还是右子树,我们需要处理特殊情况看是否当前结点就是p或q.

参考代码:

class Solution {
public:
    bool isleftOright(TreeNode* node,TreeNode* root)
    {
        if(root == nullptr)
          return false;
        return root == node ||isleftOright(node,root->left) ||  isleftOright(node,root->right);  
    }
    
	TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q)
	{

		if (p == root || q == root)//最近公共祖先为p q其中一个
			return root;
        bool pinleft = isleftOright(p,root->left);
        bool pinright = !pinleft; //非左即右
        bool qinleft = isleftOright(q,root->left);
        bool qinright = !qinleft;
        //p q分别在左右子树
        if((pinleft && qinright) || (qinleft && pinright))
          return root;
       //p q都在左右子树
        else if(pinleft && qinleft)
          return lowestCommonAncestor(root->left,p,q);
         else
          return lowestCommonAncestor(root->right,p,q); 

	} 
  1. 这种思路比思路一调用栈层数少了许多,但也是有一定开销的。
  2. 这种思路最坏情况下时间复杂度是O(N^2).

✏️ 思路三:

   归根结底,找公共祖先也就是找公共节点,如果我们能求出两个节点的祖先路径,就能转化为链表相交问题了。问题是如何优化求路径呢?

1. 我们可以按照前序遍历的思路,找x结点的路径。

2.遇到root结点先push⼊栈,因为root就算不是x,但是root可能是根->x路径中⼀个分支结点,当这个节点左右子树都没有要找的节点的话,说明上面入栈的root不是根->x路径中⼀个分⽀结点,此时就可以pop出栈回退,继续去其他分⽀路径进行查找

3.链表相交问题我们可以先用哈希map遍历其中一条路径,再遍历另一条路径时,由于我们前序+栈得到的是从下到上的路径,所以第一次两个哈希表都有映射说明就是交点,也就是最近公共祖先。

参考代码:

    bool GetPath(TreeNode*root,TreeNode* p, stack<TreeNode*>& s)//求路径
    {  
        if(root == nullptr)
        return false;
         s.push(root);
        if(root == p)//找到目标节点
         return true;
        if(GetPath(root->left,p,s)) //左子树找 没有就去右子树
        {  
            return true; 
        }
        if(GetPath(root->right,p,s))
        {
            return true;
        }
        //左右子树都没有 回退
        s.pop();
        return false;

    } 



	TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q)
	{  
        stack<TreeNode*> sp;
        stack<TreeNode*> sq;

		unordered_map<TreeNode*, int> ump;
		unordered_map<TreeNode*, int> umq;
		if (p == root || q == root)
			return root;
       //求路径
		GetPath(root,p,sp);
		GetPath(root,q,sq);
       while(!sp.empty())
       {
            TreeNode* top = sp.top();
            ump[top]++;
            sp.pop();
       }
       TreeNode* near = nullptr;
       //链表相交 
       while(!sq.empty())
       {
           TreeNode* top = sq.top();
           umq[top]++;
           if(umq[top] && ump[top]) //两个都有映射
           {
              near = top;
              break;
           }
          sq.pop();
       }
      return near;
	} 


完(๑¯ω¯๑)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2039163.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

负载均衡之HAProxy超全内容!!!

一、负载均衡 1.1 负载均衡概念 负载均衡&#xff08;Load Balance&#xff0c;简称 LB&#xff09;是高并发、高可用系统必不可少的关键组件&#xff0c;目标是尽力将网络流量平均分发到多个服务器上&#xff0c;以提高系统整体的响应速度和可用性。 1.2 软件负载均衡 软件…

【C++深度探索】哈希表介绍与实现

&#x1f525; 个人主页&#xff1a;大耳朵土土垚 &#x1f525; 所属专栏&#xff1a;C从入门至进阶 这里将会不定期更新有关C/C的内容&#xff0c;欢迎大家点赞&#xff0c;收藏&#xff0c;评论&#x1f973;&#x1f973;&#x1f389;&#x1f389;&#x1f389; 文章目录…

量化策略开发步骤系列(4)参数分析和过度拟合

量化策略开发步骤系列&#xff08;4&#xff09;参数分析和过度拟合 参数分析过度拟合 这是量化交易系列文章的第二系列——量化策略开发步骤&#xff0c;第一系列请参考专栏&#xff1a; 量化交易系统。很多朋友反馈最近的文章代码太多&#xff0c;看不懂。 这一部分将实现零…

2 C 语言开发工具选择、 MinGW 的安装与配置、VS Code 的安装与配置、插件推荐

目录 1 开发工具选择 1.1 Visual Studio 1.2 Code::Block 1.3 Clion 1.4 VS Code 1.5 在线编辑工具 2 开发工具安装 2.1 安装 MinGW-w64 2.1.1 MinGW-w64 介绍 2.1.2 解压 MinGW 2.1.3 将 MinGW 添加至环境变量 2.1.4 验证安装 2.2 安装 VS Code 2.2.1 下载安装包…

汉光BMF6450复印机简易安装说明手册

汉光BMF6450基本参数: 产品类型:激光数码复合机 颜色类型:黑白 速度类型:中速 复印速度:45cpm 涵盖功能:复印/打印/扫描 最大原稿尺寸:A3 处理器:800MHZ Quad Core (800MHz Dual Core+533MHz Dual Core) 内存容量:2GB 供纸容量:基本供纸量:1100页(550张2…

Windows平台RTSP|RTMP播放器如何叠加OSD文字

技术背景 我们在做Windows平台RTSP|RTMP播放器的时候&#xff0c;特别是多路播放场景下&#xff0c;开发者希望可以给每一路RTSP或RTMP流添加个额外的OSD台标&#xff0c;以区分不同的设备信息&#xff08;比如添加摄像头所在位置&#xff09;&#xff0c;本文主要探讨&#x…

手写qiankun-页面渲染

registerMicroApps配置子应用 start读取配置&#xff0c;拉取子应用并完成渲染 //全局变量 let _app [];//更好的获取全局变量_app export const getApps () > _app;//app为传递过来的子应用数组 export const registerMicroApps (app) > {_app app; };export cons…

http中get和post怎么选

5.4.2.怎么选择1.如果你是想从服务器上获取资源&#xff0c;建议使用GET请求&#xff0c;如果你这个请求是为了向服务器提交数据&#xff0c;建议使用POST请求。2.大部分的form表单提交&#xff0c;都是post方式&#xff0c;因为form表单中要填写大量的数据&#xff0c;这些数据…

RK3399平台开发系列讲解(内核入门篇)详解内联汇编

🚀返回专栏总目录 文章目录 一、C语言实现加法二、使用汇编函数实现加法三、内联汇编语法四、使用案例沉淀、分享、成长,让自己和他人都能有所收获!😄 📢要深入理解Linux内核中的同步与互斥的实现,需要先了解一下内联汇编:在C函数中使用汇编代码。 现代编译器已经足…

Linux系统调试课:CPUFreq 中央处理器频率调节技术

文章目录 一、CPUFreq组成二、用户接口三、设备树配置沉淀、分享、成长,让自己和他人都能有所收获!😄 📢中央处理器频率调节(Central Processing Unit frequency,CPUFreq)技术可以降低ARM芯片的功耗,例如在系统对任务压力较小时,通过调整处理器工作频率与输入电压的…

【一图学技术】9.OAuth2.0授权框架SSO单点登录图解及关系区别、使用场景

OAuth2.0原理&SSO单点登录图解 一、单点登录SSO 1.概述 ​ 单点登录&#xff08;全称Single Sign On&#xff0c;简称就是SSO)是一种身份验证和授权机制&#xff0c;它允许用户在多个相关但相互独立的系统或应用程序之间进行无缝切换&#xff0c;而无需重复登录。在多个…

【3】MySQL的安装即启动

目录 一.下载 二.安装 三.启动 一.下载 二.安装 安装MySQL时遇到的Initializing database错误&#xff1a;推荐下面的博客&#xff08;简单就是电脑名不要出现中文&#xff09; https://blog.csdn.net/m0_52775858/article/details/123705566 三.启动 PS&#xff1a;cmd要…

多台USB 3.0相机启动时部分相机无法打开

在使用多台USB 3.0相机时&#xff0c;遇到启动时部分相机无法打开的问题是较为常见的情况。这个问题通常与带宽、供电、驱动程序、或系统资源管理有关。以下是一些优化建议&#xff0c;帮助你提高相机启动的可靠性&#xff1a; 1. USB带宽管理 USB 3.0的带宽虽然比USB 2.0高很…

自训Transformer模型:识别图像是否由AI生成?

背景 随着AI生成图像技术的迅猛发展&#xff0c;特别是生成对抗网络&#xff08;GANs&#xff09;和深度学习的不断进步&#xff0c;生成的图像变得越来越逼真。 这项技术不仅催生了许多创新应用&#xff0c;也带来了潜在的风险和挑战。 Transformer模型在图像识别中的作用 …

PHP初级栈进阶篇

小刘小刘&#xff0c;下雨不愁 (收藏&#xff0c;关注不迷路) 这里我会更新一些php进阶知识点&#xff0c;新手想再进一步可以有个方向&#xff0c;也有个知识图谱的普及 当然本篇不止写技术 会涉及一些进阶路线 我也是在这里积累&#xff0c;希望和同行者一起进步为后来者…

网络协议四 物理层,数据链路层,数字信号,模拟信号,信道,CSMA/CD协议-以太网帧协议,PPP协议,网卡

从这一节开始学习 五层模型。学习方法是从最底层物理层开始学习 七层模型 五层模型 各个层用的协议&#xff0c;以及加上协议后的称谓 各个层的作用 应用层&#xff1a;可以认为是原始数据&#xff0c;该数据称为 报文&#xff0c;用户数据。 运输层&#xff1a;也叫传输层&am…

猫头虎 分享:Python库 Pytest 的简介、安装、用法详解入门教程

猫头虎 分享&#xff1a;Python库 Pytest 的简介、安装、用法详解入门教程 &#x1f680; 今天猫头虎带您深入了解 Python 测试框架 Pytest 的强大功能&#xff0c;手把手教您从安装到实际使用&#xff0c;助您轻松提升代码质量&#xff01;&#x1f63a; 摘要 &#x1f4cb; …

Windows蓝屏事件:深入分析与未来启示

引言 在2024年7月19日&#xff0c;一起引发全球范围蓝屏问题的事件&#xff0c;将安全领域领先的公司CrowdStrike推向了舆论的风口浪尖。尽管事后CrowdStrike发布了一份长达12页的根本原因分析&#xff08;RCA&#xff09;&#xff0c;试图解释并缓解这一问题&#xff0c;但该…

学习笔记 韩顺平 零基础30天学会Java(2024.8.14)

P500 集合体系图 单列集合是指自己只有一个值&#xff0c;双列集合是像键值对这样的 P501 Collection方法 对于第三点&#xff0c;像Set这样的&#xff0c;存放进去的和取出来的顺序可能不是一样的&#xff0c;所以就叫无序的 P502 迭代器遍历 在调用iterator.next()方法之前必…

新160个crackme - 030-Acid Bytes.4

运行分析 需要破解Name和Serial PE分析 upx壳&#xff0c;32位 linux系统upx -d 脱壳 脱壳后发现是Delphi程序 静态分析&动态调试 ida搜索字符串&#xff0c;找到Your Name must be at least 6 Chars long !&#xff0c;双击进入 发现地址为红色&#xff0c;即函数未定义 选…