二叉树
一、二叉树的种类
满二叉树、完全二叉树、二叉搜索树、平衡二叉搜索树(AVL树)
1、二叉搜索树:
二叉搜索树是有数值的了,二叉搜索树是一个有序树。
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉排序树
2、平衡二叉搜索树:
它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
二、存储方式
1、链式存储 – 常用的方式
2、数组存储
根据下标来遍历数组。如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。
三、遍历方式
1、节点定义
function TreeNode(val,left,right){
// 数值
this.val = (val === undefined ? 0 : val);
// 指针
this.left = (left === undefined ? null : left);
this.right = (right === undefined ? null : right);
}
2、深度优先遍历
不同遍历的先后顺序:
深度优先遍历使用递归和迭代两种形式。递归的时候隐式地维护了一个栈,而我们在迭代的时候需要显式地将这个栈模拟出来
解题模板 --递归实现
(1)前序遍历
a. 递归
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
*/
var preorderTraversal = function(root) {
// 定义一个数组存储二叉树中的节点
let res = [];
// 定义递归函数
function dfs(root){
// 节点为空
if(root === null) return;
// 前序遍历
res.push(root.val);
// 递归左子树
dfs(root.left);
// 递归右子树
dfs(root.right);
}
// 调用递归函数打印出结果
dfs(root);
return res;
};
b.迭代 – 非模板
var preorderTraversal = function(root) {
// 判空
if(root == null) return[];
// 定义一个栈,用来进行迭代。定义一个数组,用来存储遍历得到的数据
const stack = [], res=[];
// 初始化栈,根节点入栈
stack.push(root);
// 开始迭代,栈非空就弹出栈顶元素
while(stack.length){
// 弹出栈顶元素
const cur = stack.pop();
// 并且将节点对应的元素存入到res中
res.push(cur.val)
// 向栈中存入左右子树,先存右子树,然后存左子树。这样出栈的时候,就是先左,后右。
if(cur.right != null) stack.push(cur.right);
if(cur.left != null) stack.push(cur.left);
}
return res;
};
c.迭代 – 模板
var preorderTraversal = function(root) {
// 判空
if(root == null) return[];
// 定义一个栈,用来进行迭代。定义一个数组,用来存储遍历得到的数据
const stack = [], res=[];
// 外层循环时为了对右子树进行同样的操作,由于右子树可能为空,所以得加条件
while(root || stack.length){
// 一直将节点的左子树压入栈中
while(root){
// 前序遍历,所以入栈的时候,就可以放到res
res.push(root.val);
stack.push(root);
root = root.left;
}
// 然后弹出栈顶元素,并且堆这个元素的右子树进行同样的操作
root = stack.pop();
// 检查这个节点得右子树
root = root.right;
}
return res;
};
(2)中序遍历
a. 递归
var inorderTraversal = function(root) {
let res=[];
// 递归函数
function dfs(root){
if(!root) return [];
dfs(root.left);
res.push(root.val);
dfs(root.right);
}
dfs(root)
return res;
};
b.迭代
var inorderTraversal = function(root) {
if(!root) return [];
let stack=[],res=[];
while(root || stack.length){
while(root){
stack.push(root);
root = root.left;
}
root = stack.pop();
res.push(root.val);
root = root.right;
}
return res;
};
(3)后序遍历
a. 递归
var postorderTraversal = function(root) {
let res=[];
function dfs(root){
if(root == null) return [];
dfs(root.left);
dfs(root.right);
res.push(root.val);
}
dfs(root);
return res;
};
b.迭代
var postorderTraversal = function(root) {
if(!root) return[];
let stack=[],res=[];
while(root || stack.length){
while(root){
stack.push(root);
// 每次在res的头部插入
res.unshift(root.val);
root = root.right;
}
root = stack.pop();
root = root.left;
}
return res;
};
3、广度优先遍历
var levelOrder = function(root) {
// 定义队列,存储结果
let queue = [],res = [];
// 判空
if(!root) return res;
// 初始化队列
queue.push(root);
// 终止条件是队列为空
while(queue.length){
const arr = [];
// 记录当前值的节点数量,后续操作出入队数量会变化
const n = queue.length;
// 这里的n用来控制每一层需要从队头弹出元素的次数,有多少元素就进行多少次。并且每次都会检查弹出元素的左右子树,并且添加到队尾。
for(let i=0;i<n;i++){
// 从队列头部弹出节点,并且将节点对应的值存到数组当中
const node = queue.shift();
arr.push(node.val);
// 弹出元素之后检查左右子树,如果有,也只会添加到队尾。
if(node.left) queue.push(node.left);
if(node.right) queue.push(node.right);
}
res.push(arr);
}
return res;
};
四、有关题目
226. 翻转二叉树
题目链接
思想:在遍历的过程中,交换每个节点的左右子树即可。下面使用的是前序遍历,还可以使用中序遍历等方式。
// 前序遍历
var invertTree = function(root) {
if (!root) return null;
// 左右子树交换
[root.left, root.right] = [root.right, root.left]; // 根
// 左右子树内部交换
if(root.left) invertTree(root.left); // 左
if(root.right) invertTree(root.right); // 右
return root;
};
199.二叉树的右视图
使用层次遍历,每次存储每层路径的路径的时候,从头部存入path。然后最后把每个path的头部元素存入res中即可。
var rightSideView = function(root) {
let queue = [] ,res = []
if(!root) return res
// 初始化队列
queue.push(root)
while(queue.length){
let len = queue.length
let path = []
for(let i=0; i<len; i++){
// 从头部弹出节点
let node = queue.shift()
// 从path头部插入
path.unshift(node.val)
if(node.left) queue.push(node.left)
if(node.right) queue.push(node.right)
}
// 把path的头部元素弹出放入res
res.push(path.shift())
}
return res
};
103、二叉树的锯齿形层次遍历
在二叉树层次遍历的基础上的改进,添加一个控制层数的变量。队列中弹出元素还是从头部弹出,但是最后压入每层的数组path的时候,奇数层从头部添加,偶数层从尾部添加。从而实现锯齿形层序遍历。
var zigzagLevelOrder = function(root) {
// 定义一个队列用于存储每个节点的左右子树, 定义一个存储最后结果的数组
var queue=[], result = [];
// 判空操作,j为空树
if(!root) return result;
// 初始化队列,将根节点存储
queue.push(root);
// 用来控制每一层遍历的方向
var j = 0;
// 每次将每一层的元素弹出队列的时候,需要将这一层的所有元素全部弹出。
// 每次弹出元素的时候,要检查这个元素的左右子树是否存在
while(queue.length){
// 定义一个存储每一层元素的数组
const arr = [];
// 用n来控制每一层弹出的元素。当队列当中有几个元素,就要弹出多少元素。因为这个队列当中的元素是上一层节点的左右子节点。
const n = queue.length;
for(let i=0;i<n;i++){
// 从队头弹出元素
var node = queue.shift();
// 添加条件判断,奇数层从头部添加,偶数层从尾部添加
if(j%2 == 0){
arr.push(node.val);
}else{
arr.unshift(node.val);
}
// 检查这个元素的左右子树是否存在.存在就放进队列当中
if(node.left) queue.push(node.left);
if(node.right) queue.push(node.right);
}
// 每一层的元素遍历结束之后,存储到最后的结果中。
result.push(arr);
j = j + 1;
}
return result;
};