14 二叉树的前序遍历(Binary Tree Preorder Traversal)

2023-11-12 00:38

本文主要是介绍14 二叉树的前序遍历(Binary Tree Preorder Traversal),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 1 题目
    • 2 描述
    • 3 解决方案
      • 3.1 递归算法
        • 3.1.1 遍历法(Traverse)
          • 思路
          • 源码
        • 3.1.2 分治法(Devide And Conquer)
          • 思路
          • 源码
      • 3.2 非递归算法
        • 3.2.1 二叉树遍历的非递归通用解法
          • 思路
          • 源码
          • 图解
        • 3.2.2 前序遍历的非递归解法二
          • 思路
          • 源码
        • 3.2.3 前序遍历的非递归解法三
          • 思路
          • 源码
      • 3.3 时间复杂度
      • 3.4 空间复杂度
    • 4 总结

1 题目

  二叉树的前序遍历(Binary Tree Preorder Traversal)

lintcode:题号——66,难度——easy

2 描述

  给出一棵二叉树,返回其节点值的前序遍历。

名词:

遍历

按照一定的顺序对树中所有节点进行访问的过程叫做树的遍历。

前序遍历

在二叉树中,首先访问根结点然后遍历左子树,最后遍历右子树,在遍历左右子树时,仍然采用这种规则,这样的遍历方式叫做二叉树的前序遍历。

序列化规则:

使用大括号包裹节点序列来表示二叉树,首个数据为根节点,后面接着是其左儿子和右儿子节点值,"#"表示不存在该子节点,之后以此类推。

样例1:

输入:二叉树 = {1,2,3}
输出:[1,2,3]
解释:上述{1,2,3}序列按照序列化规则表示的二叉树如下

     1/ \2   3

按照前序遍历规则,先根再左右子树,顺序即为[1,2,3]

样例2:

输入:二叉树 = {1,#,2,3}
输出:[1,2,3]
解释:上述{1,#,2,3}序列按照序列化规则表示的二叉树如下

        1/   \#       2/3

按照前序遍历规则,先根再左右子树,顺序同样为[1,2,3]

3 解决方案

  二叉树的遍历可以使用递归和非递归两种方式:

  • 使用递归的方式编写能够减少思维复杂度,但是在二叉树深度很深的情况下,由于编程语言一般会限制递归调用的深度,递归层数太深会导致程序调用的栈溢出。
  • 使用非递归的方式比较难于理解,虽然会使用到栈,但只是存储数值,不会产生上述程序调用的栈溢出情况。

3.1 递归算法

  程序在执行过程中调用自己,叫做递归。递归通常可以把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序代码就可描述出解题过程所需要的多次重复计算。

递归的三个要素(编程中):

  1. 递归的定义(递归函数的返回值、参数要如何定义)
  2. 递归的拆解(递归如何向下拆分)
  3. 递归的出口(递归的结束条件)

  递归又有两种形式,遍历和分治。

3.1.1 遍历法(Traverse)
思路

  遍历的整个过程可以看成是为了完成某项大任务,于是领导亲自下基层处理各项小任务,所有工作都“亲历亲为”,参与了所有工作最终完成了大任务。

遍历法的主要递归函数一般不带返回值,会拥有全局参数或者贯穿整个递归过程的函数参数用来处理数据。

源码

  C++版本:

/**
* @param root: A Tree
* @return: Preorder in ArrayList which contains node values.
*/
vector<int> preorderTraversal(TreeNode * root) {// write your code herevector<int> result;traversa(root, result);return result;
}// 遍历法的递归函数,没有返回值,函数参数result贯穿整个递归过程用来记录遍历的结果
void traversa(TreeNode * curNode, vector<int> & result) // 递归三要素之定义
{if (curNode == nullptr){return; // 递归三要素之出口}result.push_back(curNode->val);traversa(curNode->left, result); // 递归三要素之拆解,traversa(curNode->right, result);
}
3.1.2 分治法(Devide And Conquer)
思路

  分治法的整个过程可以看成是为了完成某项大人物,于是领导把各项工作分派给各个下属,各个下属又把工作继续细分层层向下分派给基层人员,每一级都只需要获取下级完成的任务结果即可,最终所有层级任务都完成了,大任务也对应完成了。

分治法的主要递归函数一般需要有返回值,用来向上一层提供结果。

源码

  C++版本:

/**
* @param root: A Tree
* @return: Preorder in ArrayList which contains node values.
*/
// 由于主函数的形式已经符合分治法需要的形式(具有合适的返回值),直接使用主函数做为递归函数
vector<int> preorderTraversal(TreeNode * root) { //递归三要素之定义// write your code herevector<int> result;if (root == nullptr){return result; // 递归三要素之出口}// 获取左子树的遍历结果vector<int> leftResult = preorderTraversal(root->left); // 递归三要素之拆解// 获取右子树的遍历结果vector<int> rightResult = preorderTraversal(root->right);// 组合左右子树的返回值result.push_back(root->val);result.insert(result.end(), leftResult.begin(), leftResult.end());result.insert(result.end(), rightResult.begin(), rightResult.end());return result;
}

3.2 非递归算法

3.2.1 二叉树遍历的非递归通用解法
思路

  在非递归算法中,需要用到数据结构“栈”来保存二叉树遍历过程中的状态,模拟整个前序遍历的路径。下面提供一种针对二叉树的前、中、后序遍历都比较通用的形式来解决遍历问题。

  1. 左穿入栈到底,边入栈边解析;(解析:即向结果序列插入节点值)
  2. 从栈内弹出当前节点,可能是左子树或者根节点(相对);(节点在步骤1已解析完成)
  3. 右子树的根节点入栈,重复3、4、5步骤(即对右子树做中序遍历),直到满足循环结束条件,该二叉树的前序遍历即在结果序列中。

初始条件:栈为空当前指针指向根节点
循环结束条件:栈为空当前指针为空

源码
/**
* @param root: A Tree
* @return: Preorder in ArrayList which contains node values.
*/
vector<int> preorderTraversal(TreeNode * root) {// write your code herevector<int> result;if (root == nullptr){return result;}stack<TreeNode *> nodeStack; // 空栈TreeNode * cur = root; // cur指针指向根节点while (!nodeStack.empty() || cur != nullptr) // 栈和cur指针都不为空,则继续循环{while (cur != nullptr) // while循环完成左穿入栈的步骤{result.push_back(cur->val);nodeStack.push(cur);cur = cur->left;}cur = nodeStack.top();nodeStack.pop(); // 弹出节点(左子树、根节点)cur = cur->right; // cur指向右支,目的是让下一次循环时将右支入栈}return result;
}
图解

  非递归的解法理解起来比较不容易一些,可以边调试边理解,逻辑比较绕。

主要规律就是“左穿入栈解析->弹出已解析的栈顶->压入未解析的右子树”不断重复。

样例:{1,2,3,4,5,6,7}

初始状态

         1/   \2     3/ \   / \4   5 6   7

nodeStack——(栈底)空(栈顶)
cur——1
result——空

第一轮while循环

图形表示栈nodeStack的状态1/2/4   左穿入栈到底,边入栈边解析

nodeStack——(栈底)1,2,4(栈顶)
cur——nullptr,cur此时指向4的左孩子
result——1,2,4(只在解析时才插入新值)

         1/2 弹出栈顶

nodeStack——(栈底)1,2(栈顶)
cur——nullptr,cur被指向之前的栈顶4,弹出栈顶之后,此时指向4的右孩子

第二轮while循环

         1/2 循环开始时cur指向空,跳过左穿入栈的过程

nodeStack——(栈底)1,2(栈顶)
cur——nullptr,cur指向4的右孩子

         1  弹出栈顶

nodeStack——(栈底)1(栈顶)
cur——5,cur被指向之前的栈顶2,弹出栈顶之后,此时指向2的右孩子

第三轮while循环

         1/空 节点2不在栈内\ 5   循环开始时cur指向5,左穿入栈到底,边入栈边解析

nodeStack——(栈底)1,5(栈顶)
cur——nullptr,cur指向5的左孩子
result——1,2,4,5

         1

nodeStack——(栈底)1(栈顶)
cur——nullptr,cur被指向之前的栈顶5,弹出栈顶之后,此时指向5的右孩子

第四轮while循环

         1  循环开始时cur指向空,跳过左穿入栈的过程

nodeStack——(栈底)1(栈顶)
cur——nullptr,cur指向5的右孩子

         空 弹出栈顶

nodeStack——(栈底)空(栈顶)
cur——3,cur被指向之前的栈顶1,弹出栈顶之后,此时指向1的右孩子

第五轮while循环

         空\3/6 左穿入栈,边入栈边解析

nodeStack——(栈底)3,6(栈顶)
cur——nullptr,cur指向6的左孩子
result——1,2,4,5,3,6

         空\3 弹出栈顶

nodeStack——(栈底)3(栈顶)
cur——nullptr,cur被指向之前的栈顶6,弹出栈顶之后,此时指向6的右孩子

第六轮while循环

         空\3 循环开始时cur指向空,跳过左穿入栈的过程

nodeStack——(栈底)3(栈顶)
cur——nullptr,cur指向6的右孩子

         空  弹出栈顶

nodeStack——(栈底)空(栈顶)
cur——7,cur被指向之前的栈顶3,弹出栈顶之后,此时指向3的右孩子

第七轮while循环

         空\空\7 左穿入栈,边入栈边解析

nodeStack——(栈底)7(栈顶)
cur——nullptr,cur指向7的左孩子
result——1,2,4,5,3,6,7

         空\空  弹出栈顶

nodeStack——(栈底)空(栈顶)
cur——nullptr,cur被指向之前的栈顶7,弹出栈顶之后,此时指向7的右孩子
result——1,2,4,5,3,6,7

此时,nodeStack和cur都为空,满足循环结束条件,跳出循环。

此时的result序列即为该二叉树的前序遍历。

3.2.2 前序遍历的非递归解法二
思路

  这个代码和通用解法的不同点有两个:

  1. 初始条件:栈初始化包含根节点当前指针指向根节点
  2. 循环结束条件:栈为空
源码
/**
* @param root: A Tree
* @return: Preorder in ArrayList which contains node values.
*/
vector<int> preorderTraversal(TreeNode * root) {// write your code herevector<int> result;if (root == nullptr){return result;}stack<TreeNode *> nodeStack;nodeStack.push(root); // 初始化时候,根节点入栈TreeNode * cur = root;while (!nodeStack.empty()) // 循环只需要判断栈是否为空{while (cur != nullptr){result.push_back(cur->val);nodeStack.push(cur);cur = cur->left;}cur = nodeStack.top();nodeStack.pop();cur = cur->right;}
}
3.2.3 前序遍历的非递归解法三
思路

  这个解法比前两种非递归解法要容易理解,但为了统一前、中、后序的遍历方式,建议记非递归的通用解法,按照前序遍历的规则来处理节点。步骤如下:

  1. 循环前向栈内压入根节点;
  2. 解析cur,弹出cur;
  3. 右子树入栈;
  4. 左子树入栈;
  5. 重复2、3、4步骤,直到栈空。

初始条件:栈初始化包含根节点当前指针指向空
循环结束条件:栈为空

源码
/**
* @param root: A Tree
* @return: Preorder in ArrayList which contains node values.
*/
vector<int> preorderTraversal(TreeNode * root){// write your code herevector<int> result;if (root == nullptr){return result;}stack<TreeNode *> nodeStack;nodeStack.push(root); // 循环前压入根节点TreeNode * cur = nullptr;while (!nodeStack.empty()){cur = nodeStack.top();result.push_back(cur->val); // 解析栈顶节点nodeStack.pop(); // 弹出已解析的节点if (cur->right != nullptr){nodeStack.push(cur->right); // 压入右子树}if (cur->left != nullptr){nodeStack.push(cur->left); // 压入左子树}}return result;
}

3.3 时间复杂度

  对二叉树的遍历,不管使用什么方式都是将整棵树中的所有节点走一遍,所以算法的时间复杂度都为O(n)。

  关于二叉树和分治法的时间复杂度,如果节点操作的的时间复杂度为O(1),总复杂度为O(n):

T(n) = 2 * T(n/2) + O(1)= 2 * (2*T(n/4) + O(1)) + O(1)= 4 * T(n/4) + 2 * O(1) + O(1)= 4 * (2*T(n/8) + O(1)) + O(1)= 8 * T(n/8) + 4 * O(1) + 2 * O(1) + O(1)= n * T(n/n) + O(n/2 + n/4 + …… + 1)= O(n) + O(n)= O(n)

  如果节点操作的的时间复杂度为O(n),总复杂度为O(nlogn):

T(n) = 2 * T(n/2) + O(n)= 2 * (2*T(n/4) + O(n/2)) + O(n)= 4 * T(n/4) + 2*O(n/2) + O(n)= 4 * (2*T(n/8) + O(n/4)) + 2*O(n/2) + O(n)= 8 * T(n/8) + 4*O(n/4) + 2*O(n/2) + O(n)= n * T(n/n) + O(n) + O(n) + …… + O(n)= O(n) + logn*O(n)= O(nlogn)

3.4 空间复杂度

  算法的空间复杂度,分治法为O(n),上述其余方法都为O(1)。

4 总结

  二叉树的遍历在一般使用递归算法,在二叉树较大的情况下才使用非递归的算法。

这篇关于14 二叉树的前序遍历(Binary Tree Preorder Traversal)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/393897

相关文章

uva 575 Skew Binary(位运算)

求第一个以(2^(k+1)-1)为进制的数。 数据不大,可以直接搞。 代码: #include <stdio.h>#include <string.h>const int maxn = 100 + 5;int main(){char num[maxn];while (scanf("%s", num) == 1){if (num[0] == '0')break;int len =

业务中14个需要进行A/B测试的时刻[信息图]

在本指南中,我们将全面了解有关 A/B测试 的所有内容。 我们将介绍不同类型的A/B测试,如何有效地规划和启动测试,如何评估测试是否成功,您应该关注哪些指标,多年来我们发现的常见错误等等。 什么是A/B测试? A/B测试(有时称为“分割测试”)是一种实验类型,其中您创建两种或多种内容变体——如登录页面、电子邮件或广告——并将它们显示给不同的受众群体,以查看哪一种效果最好。 本质上,A/B测

leetcode105 从前序与中序遍历序列构造二叉树

根据一棵树的前序遍历与中序遍历构造二叉树。 注意: 你可以假设树中没有重复的元素。 例如,给出 前序遍历 preorder = [3,9,20,15,7]中序遍历 inorder = [9,3,15,20,7] 返回如下的二叉树: 3/ \9 20/ \15 7   class Solution {public TreeNode buildTree(int[] pr

树(Tree)——《啊哈!算法》

树 什么是树?树是一种特殊的图,不包含回路的连通无向树。 正因为树有着“不包含回路”这个特点,所以树就被赋予了很多特性。 一棵树中的任意两个结点有且仅有唯一的一条路径连通。一棵树如果有n个结点,那么它一定恰好有n-1条边。在一棵树中加一条边将会构成一个回路。 一棵树有且只有一个根结点。 没有父结点的结点称为根结点(祖先)。没有子结点的结点称为叶结点。如果一个结点既不是根结点也不是叶

PHP实现二叉树遍历(非递归方式,栈模拟实现)

二叉树定义是这样的:一棵非空的二叉树由根结点及左、右子树这三个基本部分组成,根据节点的访问位置不同有三种遍历方式: ① NLR:前序遍历(PreorderTraversal亦称(先序遍历)) ——访问结点的操作发生在遍历其左右子树之前。 ② LNR:中序遍历(InorderTraversal) ——访问结点的操作发生在遍历其左右子树之中(间)。 ③ LRN:后序遍历(PostorderT

PMP–一、二、三模–分类–14.敏捷–技巧–看板面板与燃尽图燃起图

文章目录 技巧一模14.敏捷--方法--看板(类似卡片)1、 [单选] 根据项目的特点,项目经理建议选择一种敏捷方法,该方法限制团队成员在任何给定时间执行的任务数。此方法还允许团队提高工作过程中问题和瓶颈的可见性。项目经理建议采用以下哪种方法? 易错14.敏捷--精益、敏捷、看板(类似卡片)--敏捷、精益和看板方法共同的重点在于交付价值、尊重人、减少浪费、透明化、适应变更以及持续改善等方面。

react笔记 8-17 属性绑定 class绑定 引入图片 循环遍历

1、绑定属性 constructor(){super()this.state={name:"张三",title:'我是一个title'}}render() {return (<div><div>aaaaaaa{this.state.name}<div title={this.state.title}>我是一个title</div></div></div>)} 绑定属性直接使用花括号{}   注

2021-8-14 react笔记-2 创建组件 基本用法

1、目录解析 public中的index.html为入口文件 src目录中文件很乱,先整理文件夹。 新建components 放组件 新建assets放资源   ->/images      ->/css 把乱的文件放进去  修改App.js 根组件和index.js入口文件中的引入路径 2、新建组件 在components文件夹中新建[Name].js文件 //组件名首字母大写

2021-08-14 react笔记-1 安装、环境搭建、创建项目

1、环境 1、安装nodejs 2.安装react脚手架工具 //  cnpm install -g create-react-app 全局安装 2、创建项目 create-react-app [项目名称] 3、运行项目 npm strat  //cd到项目文件夹    进入这个页面  代表运行成功  4、打包 npm run build

用Python实现时间序列模型实战——Day 14: 向量自回归模型 (VAR) 与向量误差修正模型 (VECM)

一、学习内容 1. 向量自回归模型 (VAR) 的基本概念与应用 向量自回归模型 (VAR) 是多元时间序列分析中的一种模型,用于捕捉多个变量之间的相互依赖关系。与单变量自回归模型不同,VAR 模型将多个时间序列作为向量输入,同时对这些变量进行回归分析。 VAR 模型的一般形式为: 其中: ​ 是时间  的变量向量。 是常数向量。​ 是每个时间滞后的回归系数矩阵。​ 是误差项向量,假