二叉树的基本概念
二叉树:
- 定义:满足树的定义,但每个结点最多两个子结点,二叉树是一个有序树。(区分左右子结点或者说左右孩子)
容易混淆的概念:
- 二叉树和度为2的有序树:二叉树可以为空树,但度为2的有序树至少有三个结点,以为度为2表示结点中最大的度为2;二叉树中的子结点始终都有左右之分,而度为2的有序树是相对的,也就是只有一个子结点的时候是不存在左右之分。
满二叉树:
- 定义:一颗高度为h的二叉树,且含有2^h-1个结点的二叉树称为满二叉树(每层都填满了)。
- 对于一个满二叉树,给每个结点从上层到下层、从左到右编号,存在着编号规律。对于编号为i的结点,父结点编号为i/2取下界,左孩子为2i、右孩子为2i+1。通过这个规律可以用结构体数组来构造二叉树。
完全二叉树:
- 定义:设一个高度为h有n个结点的二叉树,当且仅当每个结点都与高度为h的满二叉树1~n的结点一一对应时,称为完全二叉树。(只有最后一层可以不满,且排放从左到右没有缺失)
- 如果i<n/2的取下界,则该结点i为分支结点,否则为叶子结点。(通过满二叉树中,对于编号为i的结点,父结点编号为i/2,就可以理解了)
- 度为1的结点若存在,则可能有一个,且编号最大的分支结点,并且子结点一定是左结点。(度是连接子结点的数量)
二叉排序树:
- 一棵二叉树,若树非空,对任意结点都存在左子树和右子树,则左子树上的所有结点的关键字均小于该结点,右子树上的所有关键字均大于该结点。
平衡二叉树:
- 树上任意结点的左子树和右子树的深度值差不超过1。
二叉树的性质:
- 非空二叉树上的叶子结点数等于度为2的结点数加1,n0=n2+1 。(通过两个公式可以求出来,总结点数等于度为1、度为2、度为0的结点数相加n=n0+n1+n2;总结点数等于度为1、度为2的子节点数再加上根结点n=n1+2*n2+1。两个公式相减就好了。)
- 非空二叉树第k层至多2^(k-1)个结点。
- 高度为h的二叉树至多有2^h-1个结点。
- 结点i所在的层次为log2i取下界+1 。(通过满二叉树就可以知道每一层的编号大小范围)
- 具有n个(n>0)结点的完全二叉树的高度为log2n+1取下界或者log2(n+1)取上界。(等比数列求和)
- 对于一个满二叉树,给每个结点从上层到下层、从左到右编号,存在着编号规律。对于编号为i的结点,父结点编号为i/2取下界,左孩子为2i、右孩子为2i+1。通过这个编号规律可以用结构体数组来构造二叉树。
二叉树的存储结构
数组
编号规律:对于一个满二叉树,给每个结点从上层到下层、从左到右编号。对于编号为i的结点,父结点编号为i/2取下界,左孩子为2i、右孩子为2i+1。
构建思路:采用数组,根据编号规律,让数组下标来充当编号。这样对于某个数组下标为i的值,它的左节点数组下标为2i、右结点下标为2i+1、父结点下标为i/2,但是如果不是满二叉树的话,就会有空间浪费。
为什么线段树要开4倍区间大小的数组:数组是用满二叉树的编号规律来的,所以下面的公式用了满二叉树的公式。先规定一下层数的起始从1开始,区间范围大小为n,层数用k表示,满二叉树总结点数用N表示。一般线段树题目给的区间都是最后放在叶子结点的,通过层数和当层节点数公式 2 k − 1 2^{k-1} 2k−1求出层数 k = ⌈ log ( 2 ) ( n ) + 1 ⌉ ≤ log ( 2 ) ( n ) + 2 k=\lceil \log(2)(n)+1\rceil \leq \log(2)(n)+2 k=⌈log(2)(n)+1⌉≤log(2)(n)+2,通过总结点数和层数公式 N = 2 k − 1 N=2^k-1 N=2k−1求出总结点数 N ≤ 2 log ( 2 ) ( n ) + 2 − 1 = 4 ∗ n − 1 N\leq 2^{\log(2)(n)+2}-1=4*n-1 N≤2log(2)(n)+2−1=4∗n−1,所以开4倍不会越界。
注意:这里可以不用普通值类型的数组,如果存取的数据不止一个值的话,可以考虑用类数组或者结构体数组。数组的实现方法,不过就是把链表中结点的连接换成下标编号来关联。
链表
构建思路:每个结点一个结构体,创建两个指针指向左孩子和右孩子,就可以了,其它的需要添加的再加到结构体就可以了。
特性:
- 含有n个结点的二叉链表中,有n+1个空链域(每个结点都两个链域,其实就是左右孩子的指针,空链域指的是为不为空指针)。(计算思路直接2n-(n-1),每个结点链域-所有边)
C++代码
除了这里给出了C++代码,其它的代码都是C#。
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};
C#代码
C#中用类的引用代替了C++中的指针。
public class TreeNode<T> where T : IComparable<T>
{
private T data;
private TreeNode<T> lChild;
private TreeNode<T> rChild;
public TreeNode()
{
data = default(T);
this.lChild = null;
this.rChild = null;
}
public TreeNode(T val)
{
data = val;
lChild = null;
rChild = null;
}
public T Data
{
get { return data; }
set { data = value; }
}
public TreeNode<T> LChild
{
get { return lChild; }
set { lChild = value; }
}
public TreeNode<T> RChild
{
get { return rChild; }
set { rChild = value; }
}
}
补充IComparable< T >知识点:
- 为什么这里要用这个接口:一方面用泛型希望适用性能够更广泛,一方面树的操作涉及到比较,例如一个数大于另一个数、一个字符串可以在另一个字符串之前以字母顺序出现。
- 该接口的作用:此接口由其值可以排序或排序的类型实现,并提供强类型的比较方法以对泛型集合对象的成员进行排序。通常,不会直接从开发人员代码中调用方法。 相反,它由 List.Sort() 和 Add等方法自动调用。
- 所有数值类型(例如 Int 和 Double)都已经帮我们实现 IComparable,如 String、Char和 DateTime。 但是自定义类型还应提供自己的 IComparable 实现,以便对对象实例进行排序或排序。
- 比较使用的方法:A.CompareTo(B),大于零表示A排在B的前面,小于零表示A排在B的后面,相等表示A和B排的位置是一样的。
(理解来自对官方文档IComparable)
二叉树的遍历思路
遍历的分类:按照根和左右子树的遍历顺序来分的,无论那种都是先左子树再右子树,只是根节点(这里根结点也适用于每个子树)的顺序不同。
- 先序遍历:先遍历根节点,再遍历左子树,再遍历右子树。
- 中序遍历:先遍历左子树,再遍历根结点,再遍历右子树。
- 后序遍历:先遍历左子树,再遍历右子树,再遍历根结点。
先序遍历
递归
public void preorderTraversal(TreeNode<T> node)
{
if (node != null)
{
Console.Write(node.Data + " ");
preorderTraversal(node.LChild);
preorderTraversal(node.RChild);
}
}
非递归思路:利用栈,其实函数调用就是一种栈操作。栈后进先出,注意先放入右孩子再放入左孩子。
public void preorderWithoutRecursion()
{
Stack<TreeNode<T>> stack = new Stack<TreeNode<T>>();
TreeNode<T> node = rootNode;
stack.Push(node);
while (stack.Count() != 0)
{
node = stack.Pop();
Console.Write(node.Data + " ");
//注意是右孩子先进,以为栈是后进先出的
if (node.RChild != null)
{
stack.Push(node.RChild);
}
if (node.LChild != null)
{
stack.Push(node.LChild);
}
}
}
中序遍历
递归
public void inOrderTraversal(TreeNode<T> node)
{
if (node != null)
{
inOrderTraversal(node.LChild);
Console.Write(node.Data + " ");
inOrderTraversal(node.RChild);
}
}
非递归思路:因为需要先递归到右节点那边,所以对于每个子树需要对右节点进行循环入栈,直到null,输出右节点,再考虑当前右节点的左节点。如果右子树都遍历为null,但是栈不会0,就说明上面还有祖先结点,输出栈顶元素也就是父节点,然后又在考虑父节点的左节点。考虑左节点的时候就又回到子树的情况。
public void inOrderWithoutRecursion()
{
Stack<TreeNode<T>> stack = new Stack<TreeNode<T>>();
TreeNode<T> node = rootNode;
//两个顺序不能调换,因为先判断了node!=null为true,后面的就不会判断了
while (node != null || stack.Count() != 0)
{
while (node != null)
{
stack.Push(node);
node = node.LChild;
}
node = stack.Pop();
Console.Write(node.Data + " ");
node = node.RChild;
}
}
后序遍历
递归
public void postOrderTree(TreeNode<T> node)
{
if (node != null)
{
postOrderTree(node.LChild);
postOrderTree(node.RChild);
Console.Write(node.Data + " ");
}
}
非递归思路:用一个前驱记录当前结点上一个输出的结点,一开始就放入根节点,然后遍历放入右节点和左节点到栈中(栈后进先出)。每次遍历判断一下栈顶元素是不是没有孩子或者前驱记录的是它的孩子,如果没有孩子且上一个输出的是它的孩子,就说明可以直接输出该结点。
public void postOrderWithoutRecursion()
{
Stack<TreeNode<T>> stack = new Stack<TreeNode<T>>();
TreeNode<T> preNode = null;
TreeNode<T> node = rootNode;
stack.Push(node);
while (stack.Count() != 0)
{
node = stack.Peek();
if ((node.LChild == null && node.RChild == null) || (preNode == node.LChild || preNode == node.RChild))
{
Console.Write(node.Data + " ");
stack.Pop();
preNode = node;
}
else
{
if (node.RChild != null)
{
stack.Push(node.RChild);
}
if (node.LChild != null)
{
stack.Push(node.LChild);
}
}
}
}
层次遍历
思路:利用队列就可以做到了,这个时候先左孩子再右孩子,因为队列是先进先出。
public void hierarchyOrder()
{
Queue<TreeNode<T>> queue = new Queue<TreeNode<T>>();
TreeNode<T> node = rootNode;
queue.Enqueue(node);
while (queue.Count != 0)
{
node = queue.Dequeue();
Console.Write(node.Data + " ");
if (node.LChild != null)
{
queue.Enqueue(node.LChild);
}
if (node.RChild != null)
{
queue.Enqueue(node.RChild);
}
}
}
深度计算
递归思路:从下向上计算,取左右子树最大加一就好了。
public int treeDepth(TreeNode<T> node)
{
if (node == null)
{
return 0;
}
int l = treeDepth(node.LChild) + 1;
int r = treeDepth(node.RChild) + 1;
return l > r ? l : r;
}
非递归思路:层序遍历计算多少层就好了,树的深度等于层数。用一个nowLever存当前层的结点数,nextLever统计当前层数的左右孩子个数,每遍历一个当前层数结点就让nowLever–,当nowLever结束的时候就说明当前层结束了,让nowLever=nextLever并nextLeve=0,就可以开始下一层了。
非递归的思路还可以用来实现层序遍历按层输出每层的结点数。
public int treeDepthWithoutRecursion(TreeNode<T> node)
{
Queue<TreeNode<T>> queue = new Queue<TreeNode<T>>();
queue.Enqueue(node);
//下一层的结点数量,层数计算,当前层结点的剩余数量
int nextLever = 0, ans = 0, nowLever = 1;
while (queue.Count != 0)
{
node = queue.Dequeue();
nowLever--;
if (node.LChild != null)
{
queue.Enqueue(node.LChild);
nextLever++;
}
if (node.RChild != null)
{
queue.Enqueue(node.RChild);
nextLever++;
}
//当前层所有结点都遍历完毕
if (nowLever == 0)
{
ans++;
nowLever = nextLever;
nextLever = 0;
}
}
return ans;
}
平衡二叉树的判断
思路:递归计算左右子树深度的时候判断一下左右子树的深度差是否大于1。
bool isBalancedans;
public bool isBalancedSolution(TreeNode<T> node)
{
isBalancedans = true;
DepthDifference(node);
return isBalancedans;
}
public int DepthDifference(TreeNode<T> node)
{
if (node == null)
{
return 0;
}
int l = DepthDifference(node.LChild);
int r = DepthDifference(node.RChild);
if (Math.Abs(l - r) > 1)
{
isBalancedans = false;
}
return Math.Max(l + 1, r + 1);
}
代码注意
- C++写代码的时候涉及到指针,在使用的时候容易出现段错误,一般都是因为调用NULL指针或者野指针,注意检查。
- 对于C++在函数里面用new和malloc生成的内存是在堆上的,所以函数结束后是不会被当成局部变量而销毁内存分配,一般的局部变量的内存是在栈上的。
- C++的代码指针为空是用NULL,C#中类为空是用null。
- 上面除了链表存储哪里写了C++代码,其它都是C#代码。