首页 > 编程语言 >C++ 树进阶系列之笛卡尔树的两面性

C++ 树进阶系列之笛卡尔树的两面性

时间:2023-03-28 22:03:48浏览次数:46  
标签:node 进阶 笛卡尔 int 元素 两面性 C++ 节点 DkeTree

1. 前言

笛卡尔树是一种特殊的二叉树数据结构,融合了二叉堆二叉搜索树两大特性。笛卡尔树可以把数列(组)对象映射成二叉树,便于使用笛卡尔树结构的逻辑求解数列的区间最值或区间排名等类似问题。

如有数列 {5,7,1,9,12,16,2,18,3,11},任一存储单元格均有 2 个属性:

1.png

  • 值:单元格中的数据。
  • 位置:单元格在数列中的位置(下标、索引)。

构建笛卡尔树要求节点至少有 2 个权重,把数列映射成树结构时,可使用这2 个属性作为笛卡尔树节点的权重。把线性结构的数列映射成笛卡尔树后,此树需要满足如下 2 个特征:

  • 如果观察,则在树中具有堆的有序性,即任一父节点的值大于(最大堆)或小于(最小堆)子节点的值。根据堆的特性,可以查询笛卡尔树子树上的最大值或最小值。且可以使用堆排序算法排序数列。

  • 如果观察位置,则在树中遵循搜索树的逻辑结构,或者说,对笛卡尔树进行中序遍历,可以得到原来的数组。

9.png

2. 构建笛卡尔树

可以说笛卡尔树是双权重的节点树。那么,如何构建才能保证数列最终能映射成笛卡尔树。

实现过程中,需要借助单调栈数据结构。

什么是单调栈?

本质指的就是栈,但在存储数据时,需要要保持栈中数据按递增或递减顺序,本文构建最小堆,要求栈单调递减。

2.1 构建流程

现把数组 {5,7,1,9,12,16,2,18,3,11}构建成笛卡尔树的过程详细讲解一下:

  • 首先,准备一个栈。把数组中位置为 0、值为5 的元素(后面统一以(0,5)格式描述)。将其压入栈中,且设置成树的根节点。

2.png

  • 把数组中的(1,7)和栈中上已有元素(0,5) 比较,因值比 5 大。因能保持栈的递减顺序,可以直接入栈。且把入栈的元素作为(0,5)的右子节点。

    Tips: 至此,总结一下:能压入栈的元素作为栈中比它小的元素的右子节点。

3.png

  • 从数组中取出(2,1),因比栈顶元素(1,7)小 ,不能直接入栈,则需要把比它大的元素从栈中移走,然后把最后一个比它大的元素作为它的左子结点。如下图所示:

4.png

  • 从数组中取出(3,9)、(4,12)、(5,16),因都比先入栈的元素大,可以直接依次入栈,且成为比它小的节点的右子节点。

    Tips: 可以观察到,栈中的元素均为右链(图中绿色颜色所标记的节点)上的节点。

5.png

  • 从数组中抽出(6,2)元素,为了维持栈的递减顺序,则需要把比它大的元素从栈中移走,直到留下(2,1)。把(6,2)作为(2,1)的右子节点,且让最后一个出栈的(3,9)成为(6,2)的左子节点。

6.png

  • 继续拿出(7,18),可以直接入栈,让其成为(6,2)的右子节点。

7.png

  • 拿出(8,3)。把比它大的元素出栈,成为最近比它小的元素的右子结点,最后出栈的元素成为它的左子节点。

8.png

  • 从数组中最后拿出(9,11)元素。因比栈中所有元素大,直接入栈,且成为栈顶元素的右子节点。

9.png

2.2 小结

使用单调栈构建笛卡尔树的原则:

  • 如果需要移走其它元素后,元素才能入栈,则移走的最后一个节点成为其左子节点。
  • 当数据入栈时,需要在栈中找到比它小的第一个元素,让会成为此元素的右节点。
  • 栈中始终存储的最新右链上的节点。
  • 构建完毕后,最后栈底的元素为根节点。

3. 笛卡尔树的物理结构

二叉树的物理实现有矩阵链式 两种方案,本文将使用这 2 种方案:

3.1 矩阵实现

笛卡尔树类结构:

类中主要API为构建函数和中序遍历,其它的为辅助性函数。代码中有详细注解。

#include <iostream>
#include <stack>
using namespace std;
/*
*笛卡尔树类
*/
class DkeTree {
	private:
		//原数列
		int nums[10]= {5,7,1,9,12,16,2,18,3,11};
		//矩阵,描述树节点之间的关系。列号 0 表示行编号对应节点的左节点,列号 1 表示右节点
		int matrix[10][2];
		//存储元素的下标
		stack<int> myStack;
	public:
         /*
         *构造函数
         */
		DkeTree() {
             //初始化关系为 -1
			for(int i=0; i<10; i++)
				for(int j=0; j<2; j++)
					this->matrix[i][j]=-1;
		}
		/*
		* 数据入栈前的检查栈中是否有比自己大的元素
		* 参数 idx:元素在数组中位置
		* 返回最后一个比此数据大的元素编号
		*/
		int check(int idx);
		/*
		* 构建笛卡尔树
		*/
		void buildDiKeErTree();
         /*
         *中序遍历
         */
        void inOrder(int node) ;
         /*
		*返回根节点 :栈中最后不为 -1 的元素的编号
		*/
		int getRoot() {
			int pos=-1;
			int root=pos;
			while(!myStack.empty() && (pos= myStack.top()) !=-1 ) {
				myStack.pop();
				root=pos;
			}
			return root;
		}
        /*
        *显示矩阵中节点之间的关系
        */
		void showTree() {
			cout<<"父节点"<<" 左子节点 "<<" 右子节点"<<endl;
			for(int i=0; i<10; i++ ) {
				cout<<"("<<i<<","<<nums[i]<<")->";
				for(int j=0; j<2; j++) {
					if(matrix[i][j]!=-1 )
						cout<<"("<<matrix[i][j]<<","<< nums[ matrix[i][j] ]  <<")\t";
					else
						cout<<"("<<matrix[i][j]<<","<< -1 <<")\t";
				}
				cout<<endl;
			}
		}
};

实现 buildDiKeErTree 函数:

buildDiKeErTree 函数是核心API,用来构建笛卡尔树。

实现此API之前,需先实现辅助API check,用来实现把数组中的数据压入栈中时,查询到比之大的最后一个数据。本质是使用了单调栈的逻辑特性。

int DkeTree::check(int idx) {
    int pos=-1;
    //最后一个比之大的下标
    int prePos=pos;
    while( !DkeTree::myStack.empty() && (pos=DkeTree::myStack.top())!=-1 ) {
        if( DkeTree::nums[pos]<DkeTree::nums[idx] ) break;
        DkeTree::myStack.pop();
        prePos=pos;
    }
    return prePos;
}

buildDiKeErTree 函数:

void DkeTree::buildDiKeErTree() {
    //初始栈,压入一个较小的值,方便比较
    DkeTree::myStack.push( -1 );
    int size=sizeof(DkeTree::nums)/4;
    //遍历数组
    for( int i=0; i<size; i++ ) {
        int topIdx=DkeTree::myStack.top();
        //设置为栈顶元素的右子节点
        if(DkeTree::nums[topIdx]<DkeTree::nums[i] && topIdx!=-1) DkeTree::matrix[topIdx][1]=i;
        //从栈中移走比当前元素大的元素
        int leftIdx=DkeTree::check(i);
        if(leftIdx!=-1) {
            //置为左节点
            DkeTree::matrix[i][0]=leftIdx;
            if(DkeTree::myStack.top()!=-1)
                DkeTree::matrix[DkeTree::myStack.top()][1]=i;
        }
        //栈中存储的是数据在数组中的索引号
        DkeTree::myStack.push(i);
    }
}

测试:

//测试
int main(int argc, char** argv) {
	DkeTree*  dkeTree=new DkeTree();
	dkeTree->buildDiKeErTree();
	dkeTree->showTree();
	return 0;
}

输出结果:-1表示不存在左或右子节点。

10.png

因中序遍历笛卡尔树可以输出原数组,可用于验证笛卡尔树的构建是否正确。在DkeTree类中可添加中序遍历函数的实现。

void DkeTree::inOrder(int node) {
    if( node!=-1 ) {
        //遍历左树
        inOrder(DkeTree::matrix[node][0] );
        cout<<DkeTree::nums[node]<<"\t";
        inOrder(DkeTree::matrix[node][1] );
    }
}

测试中序遍历:

int main(int argc, char** argv) {
	DkeTree*  dkeTree=new DkeTree();
	dkeTree->buildDiKeErTree();
	int root=dkeTree->getRoot();
	dkeTree->inOrder(root);
	return 0;
}

输出结果:

11.png

3.2 链式实现

链式实现逻辑与矩阵实现本质没有什么不一样。不做过多解释,直接上完整代码。

#include <iostream>
#include <stack>
#include <vector>
using namespace std;
/*
*节点类型
*/
struct TreeNode {
	//编号
	int idx;
	//值
	int val;
	//左子节点
	TreeNode* leftChild;
	//右子节点
	TreeNode* rightChild;
	TreeNode() {
		this->leftChild=NULL;
		this->rightChild=NULL;
	}
	TreeNode(int idx,int val) {
		this->idx=idx;
		this->val=val;
		this->leftChild=NULL;
		this->rightChild=NULL;
	}
	void show() {
		cout<<"("<<this->idx<<","<<this->val<<")";
	}
};
/*
*笛卡尔树类
*/
class DkeTree {
	private:
		//根节点
		TreeNode* root;
		//原数列
		int nums[10]= {5,7,1,9,12,16,2,18,3,11};
		//存储元素的下标
		stack<int> myStack;
		//存储树上的所有节点
		vector< TreeNode*> allNodes;
	public:
		/*
		*构造函数
		*/
		DkeTree() {}
		/*
		*  栈中查找
		*/
		int check(int idx) {
			int pos=-1;
			int prePos=pos;
			while( !myStack.empty() && (pos=myStack.top())!=-1 ) {
				if( nums[pos]<nums[idx] ) break;
				myStack.pop();
				prePos=pos;
			}
			return prePos;
		}
		/*
		* 检查树上是否存在给定的节点
		* 有:返回
		* 没有:创建
		*/
		TreeNode* findNode(int idx ) {
			for(int i=0; i<this->allNodes.size(); i++ ) 
				if( this->allNodes[i]->idx==idx )return this->allNodes[i];
			TreeNode* tmp=new TreeNode(idx,nums[idx]);
			this->allNodes.push_back(tmp);
			return tmp;
		}
        /*
        *构建笛卡尔树 
        */
		void buildDiKeErTree() {
			//初始栈,压入一个较小的值,方便比较
			myStack.push( -1 );
			int size=sizeof(this->nums)/4;
			TreeNode* node=NULL;
			TreeNode* node_=NULL;
			//遍历数组
			for( int i=0; i<size; i++ ) {
				int topIdx=myStack.top();
				//设置为栈顶元素的右子节点
				if(nums[topIdx]<nums[i] && topIdx!=-1 ) {
					node=this->findNode(topIdx);
					node_=this->findNode(i);
					node->rightChild=node_;
				}
				//从栈中移走比当前元素大的元素
				int leftIdx=check(i);
				if(leftIdx!=-1) {
					//置为左节点
					node_=this->findNode(leftIdx);
					node=this->findNode(i);
					node->leftChild=node_;
					if(myStack.top()!=-1) {
						node_=this->findNode(myStack.top());
						node=this->findNode(i);
						node_->rightChild=node;
					}
				}
				myStack.push(i);
			}
		}
		/*
		*返回根节点 :栈中最后不为 -1 的元素的编号
		*/
		TreeNode* getRoot() {
			int pos=-1;
			int root=pos;
			while(!myStack.empty() && (pos= myStack.top()) !=-1 ) {
				myStack.pop();
				root=pos;
			}
			return this->findNode(root);
		}
		/*
		*中序遍历
		*/
		void inOrder(TreeNode* node) {
			if( node!=NULL ) {
				//遍历左树
				inOrder(node->leftChild );
				cout<<node->val<<"\t";
				inOrder(node->rightChild );
			}
		}
		void showTree() {
			for(int i=0; i<this->allNodes.size(); i++ ) {
				this->allNodes[i]->show();
				cout<<"->";
				if(this->allNodes[i]->leftChild!=NULL)this->allNodes[i]->leftChild->show();
				if(this->allNodes[i]->rightChild!=NULL)this->allNodes[i]->rightChild->show();
				cout<<endl;
			}
		}
};
/*
*测试 
*/ 
int main(int argc, char** argv) {
	DkeTree* dke=new DkeTree();
	dke->buildDiKeErTree();
	cout<<"显示所有节点"<<endl;
	dke->showTree();
	TreeNode* root= dke->getRoot();
	cout<<"中序遍历"<<endl;
	dke->inOrder(root);
	return 0;
}

4. 总结

本文讲解了笛卡尔树,对其特性作了较全面的分析,并且讲解了矩阵和链式两种实现方案。

本文同时收录在“编程驿站”公众号。

标签:node,进阶,笛卡尔,int,元素,两面性,C++,节点,DkeTree
From: https://blog.51cto.com/gkcode/6151870

相关文章

  • c++11 std::thread 线程实例在退出后管理线程调用join()后再新建线程将可能会产生相同
    [03-2816:52:54.372][info][vthread.cpp:92operator()()]createnewthread,id:4,tid:7f5cbb7fd640,inroduce:testvthread003[03-2816:52:54.372][info][vthread......
  • C++黑马程序员——P56-62. 指针
    P56.指针——指针的定义和使用P57.指针——指针所占内存空间P58.指针——空指针P59.指针——野指针P60.指针——const修饰指针P61.指针——指针和数组P62.指......
  • vue全家桶进阶之路23:Element UI
    ElementUI是一套基于Vue.js的组件库,它提供了一系列常用的UI组件,包括表单、弹窗、布局、导航等等。ElementUI的设计风格简洁、易用、美观,且易于定制。ElementUI......
  • C++核心编程笔记
    C++核心编程本内容主要针对C++面向对象编程技术做详细讲解1内存分区模型C++程序在执行时,将内存大方向划分为4个区域:代码区:存放函数体的二进制代码,由操作系统进行管理......
  • C++基础入门笔记
    C++基础入门1数据类型C++规定在创建一个变量或者常量时,必须要指定出相应的数据类型,否则无法给变量分配内存1.1整型作用:整型变量表示的是整数类型的数据C++中整数类......
  • c++14 读写锁
    读的时候用共享锁,写的时候用独占锁structotherSettingModel{inlinestaticconstchar*jsonFileSavePath="../data/otherSettingModel.json";inlinesta......
  • vue全家桶进阶之路22:Vue CLI脚手架
    VueCLI是一个基于Vue.js的官方脚手架工具,它可以帮助我们快速创建和管理Vue.js项目,提供了一些工具和配置来帮助我们开发和调试Vue.js应用。一切框架都是为了将开发......
  • C++智能指针、绑定器和函数对象、lambda表达式
    智能指针​ 智能指针可以保证资源的自动释放不带引用计数的智能指针auto_ptr只让最后一个指向的指针管理资源,之前的auto_ptr会被置为nullptrscoped_ptr删除了拷贝构造......
  • javaSE-day06(集合进阶)
    异常我们调用一个方法时,经常一部小心就出异常了,然后在控制台打印一些异常信息。其实打印的这些异常信息,就叫做异常。因为写代码时经常会出现问题,Java的设计者们早就为我......
  • vue全家桶进阶之路19:webpack资源打包工具
    Vue.js是一个前端开发框架,它可以帮助我们快速构建单页应用和复杂的交互界面。而Webpack则是一个前端资源打包工具,它可以将多个JavaScript、CSS、HTML、图片等资源打包......