今日内容:
● 理论基础
● 递归遍历
● 迭代遍历
● 统一迭代
1. 理论基础
-
二叉树的种类
- 两种主要形式:
-
满二叉树
-
定义:
- 满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
- 度:叶子结点
- 深度:树的层数:k;则满二叉树共有 2^k-1 个节点
- 如下图所示【图片来自代码随想录官网】
-
-
完全二叉树
-
定义
- 完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层(h从1开始),则该层包含 1~ 2^(h-1) 个节点。
- 上面和满二叉树一样的,最下一层先把左边填满的二叉树
- 如下图所示【图片来自代码随想录官网】
-
-
二叉搜索树【扩展】
-
二叉搜索树是有数值的了,二叉搜索树是一个有序树
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉排序树
-
图片示例【来自代码随想录官网】
-
-
平衡二叉搜索树【扩展】
-
平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
-
图片示例【来自代码随想录官网】
-
-
- 两种主要形式:
-
二叉树的存储方式
-
链式存储
-
指针方式
-
在内存中是散列分布的
-
图片示例【来自代码随想录官网】
-
-
顺序存储
-
数组方式
-
在内存中是连续分布的
-
节点下标间关系
- 父节点 Index:i
- 左孩子 Index:i*2+1
- 有孩子 Index:i*2+2
- 父节点 Index:i
-
图片示例【来自代码随想录官网】
-
-
-
遍历方式
- 深度优先遍历
- 先往深走,遇到叶子节点再往回走【从上往下直到走不动】
- 遍历方式【指的是 中间节点 所在的位置,左右永远是左右】
- 前序遍历(递归法,迭代法)【中左右】
- 中序遍历(递归法,迭代法)【左中右】
- 后序遍历(递归法,迭代法)【左右中】
- 广度优先遍历
- 一层一层的去遍历。【每层从左到右走完】
- 遍历方式
- 层次遍历(迭代法)
- 深度优先遍历
-
二叉树的定义
- 链式存储定义
- 节点值,左右孩子的指针
type TreeNode struct { Val int Left *TreeNode Right *TreeNode }
2. 递归遍历 (必须掌握)
关联 leetcode 144.二叉树的前序遍历
关联 leetcode 145.二叉树的后序遍历
关联 leetcode 94.二叉树的中序遍历
- 递归三部曲
- 确定递归函数的参数和返回值
- 没有必要一次性确定,可以逐步确认
- 大多数都是 根节点+存放结果的数组
- 返回值一般都是 void
- 结果直接放在参数里面了
- 返回值一般都是 void
- 确定终止条件
- 确定单层递归的逻辑
- 确定递归函数的参数和返回值
- 添加处理的都是 cur 当前节点
- 添加入数组的都是
- 题解
-
144.二叉树的前序遍历
- 写法一
func traversal(cur *TreeNode, res *[]int) {//每次的res是入参的副本,这里要用引用类型 if cur == nil { return } *res = append(*res, cur.Val) //中 traversal(cur.Left, res) //左 traversal(cur.Right, res) //右 } func preorderTraversal(root *TreeNode) []int { res := make([]int, 0) traversal(root, &res) return res }
- 写法二【闭包实现】
func preorderTraversal(root *TreeNode) []int { res := make([]int, 0) var traversal func(cur *TreeNode) traversal = func(cur *TreeNode) { if cur == nil { return } res = append(res, cur.Val) //中 traversal(cur.Left) //左 traversal(cur.Right) //右 } traversal(root) return res }
-
145.二叉树的后序遍历
func postorderTraversal(root *TreeNode) []int { res := make([]int, 0) var traversal func(cur *TreeNode) traversal = func(cur *TreeNode) { if cur == nil { return } traversal(cur.Left) //左 traversal(cur.Right) //右 res = append(res, cur.Val) //中 } traversal(root) return res }
-
94.二叉树的中序遍历
func inorderTraversal(root *TreeNode) []int { res := make([]int, 0) var traversal func(cur *TreeNode) traversal = func(cur *TreeNode) { if cur == nil { return } traversal(cur.Left) //左 res = append(res, cur.Val) //中 traversal(cur.Right) //右 } traversal(root) return res }
-
3. 迭代遍历 (基础不好的录友,迭代法可以放过)
-
操作系统实现递归:
- 也是用 栈 这种数据结构实现的
-
迭代法:【非递归遍历】
- 迭代法模拟递归
- 使用栈来实现
- 需要个人实现一个栈
-
144.二叉树的前序遍历
func preorderTraversal(root *TreeNode) []int { ret := make([]int, 0) if root == nil { return ret } stack := make([]*TreeNode,0)// 使用数组做栈的底层结构 stack = append(stack, root)// 初始化,追加根节点 for len(stack)!=0 { node:=stack[len(stack)-1]//取得栈顶元素节点[中间节点] val := node.Val ret = append(ret, val) stack=stack[:len(stack)-1] // 先压右节点,后左节点,栈:先进后出 if node.Right != nil { stack = append(stack, node.Right) } if node.Left != nil { stack = append(stack, node.Left) } } return ret }
-
145.二叉树的后序遍历
- 将前序递归的左右操作互换
- 得到结果元素序列:—> 中右左
- 对结果序列整体做一个翻转:—> 左右中
- 得到结果元素序列:—> 中右左
func postorderTraversal(root *TreeNode) []int { ret := make([]int, 0) if root == nil { return ret } stack := make([]*TreeNode,0)// 使用数组做栈的底层结构 stack = append(stack, root)// 初始化,追加根节点 for len(stack)!=0 { node:=stack[len(stack)-1]//取得栈顶元素[中间节点] val := node.Val ret = append(ret, val) stack=stack[:len(stack)-1] // 压栈操作 先左后右,出栈 右左 if node.Left != nil { stack = append(stack, node.Left) } if node.Right != nil { stack = append(stack, node.Right) } } //结束循环后,得到结果数组遍历顺序: 中右左 //整体翻转数组 中右左 --> 左右中 slices.Reverse(ret) return ret }
- 将前序递归的左右操作互换
-
94.二叉树的中序遍历
- 遍历的节点和要处理的节点不是同一个节点
- 不能像上面的 前后序那样简单处理
func inorderTraversal(root *TreeNode) []int { ret := make([]int, 0) stack := make([]*TreeNode, 0) cur := root for cur != nil || len(stack) != 0 { if cur != nil { stack = append(stack, cur)//当前节点入栈 cur=cur.Left//遍历当前节点左节点, 直到最底层最左子节点 }else { //当前节点已无左子节点了 //此时栈顶的就是当前最底层左子节点 cur=stack[len(stack)-1]//取出栈顶元素pop() stack=stack[:len(stack)-1] ret = append(ret, cur.Val)//中间节点, 它没有左子节点, 按照顺序: 左,中,右, 处理 中间节点 cur=cur.Right// 处理它的右子节点 } } return ret }
- 遍历的节点和要处理的节点不是同一个节点
4. 统一迭代 (基础不好的录友,迭代法可以放过)
- 统一迭代法
-
像递归那样,统一处理节点
- 注意压入栈的都是节点本身
-
前序:中左右
func preorderTraversal(root *TreeNode) []int { rets := make([]int, 0) //使用官方库的链表实现栈 stack := list.New() if root != nil { stack.PushBack(root) } //栈不为空 for stack.Len() != 0 { e := stack.Back() //取栈顶元素 stack.Remove(e) //弹出该元素, 避免重复操作 if e.Value != nil { //只用变动这里的压栈顺序即可 // 该节点的右中左节点添加到栈中 midNode := e.Value.(*TreeNode) //出栈顺序: 中左右 //压栈顺序: 右左中 rightSon := midNode.Right if rightSon != nil { stack.PushBack(rightSon) } leftSon := midNode.Left if leftSon != nil { stack.PushBack(leftSon) } //要弹出处理的节点,用空节点标识 stack.PushBack(midNode) stack.PushBack(nil) //用空节点来标记待操作元素 } else { //遇到操作标记的空节点 //处理节点逻辑不变 curNode := stack.Back() // 取出待操作的中间节点 stack.Remove(curNode) midNode := curNode.Value.(*TreeNode) // 当前要操作的节点 rets = append(rets, midNode.Val) } } return rets }
-
中序:左中右
func inorderTraversal(root *TreeNode) []int { rets := make([]int, 0) //使用官方库的链表实现栈 stack := list.New() if root != nil { stack.PushBack(root) } //栈不为空 for stack.Len() != 0 { e := stack.Back() //取栈顶元素 stack.Remove(e) //弹出该元素, 避免重复操作 if e.Value != nil { //只用变动这里的压栈顺序即可 // 该节点的右中左节点添加到栈中 midNode := e.Value.(*TreeNode) //出栈顺序: 左中右 //压栈顺序: 右中左 rightSon := midNode.Right if rightSon != nil { stack.PushBack(rightSon) } //要弹出处理的节点,用空节点标识 stack.PushBack(midNode) stack.PushBack(nil) //用空节点来标记待操作元素 leftSon := midNode.Left if leftSon != nil { stack.PushBack(leftSon) } } else { //遇到操作标记的空节点 //处理节点逻辑不变 curNode := stack.Back() // 取出待操作的中间节点 stack.Remove(curNode) midNode := curNode.Value.(*TreeNode) // 当前要操作的节点 rets = append(rets, midNode.Val) } } return rets }
-
后序:左中右
func postorderTraversal(root *TreeNode) []int { rets := make([]int, 0) //使用官方库的链表实现栈 stack := list.New() if root != nil { stack.PushBack(root) } //栈不为空 for stack.Len() != 0 { e := stack.Back() //取栈顶元素 stack.Remove(e) //弹出该元素, 避免重复操作 if e.Value != nil { //只用变动这里的压栈顺序即可 // 该节点的右中左节点添加到栈中 midNode := e.Value.(*TreeNode) //出栈顺序: 左右中 //压栈顺序: 中右左 //要弹出处理的节点,用空节点标识 stack.PushBack(midNode) stack.PushBack(nil) //用空节点来标记待操作元素 rightSon := midNode.Right if rightSon != nil { stack.PushBack(rightSon) } leftSon := midNode.Left if leftSon != nil { stack.PushBack(leftSon) } } else { //遇到操作标记的空节点 //处理节点逻辑不变 curNode := stack.Back() // 取出待操作的中间节点 stack.Remove(curNode) midNode := curNode.Value.(*TreeNode) // 当前要操作的节点 rets = append(rets, midNode.Val) } } return rets }
-