本篇博文将总结关于树的一些算法,树部分的内容挺多,但是不难,主要考察的是coding能力,本人就是感觉自己coding能力太差,所以关于代码实现部分都要自己实现一遍,不会的在网上查资料跟着敲一遍。
树和二叉树的关系
一般地说,树的结点间是无序 的,即:一个结点有m个孩子,则 L 1 , L 2 , L 3 , . . . , L m {L}_{1},{L}_{2},{L}_{3},...,{L}_{m} L1,L2,L3,...,Lm 可以互换位置,仍然认为是同一颗树。
二叉树的两个孩子,一般称为左孩子、右孩子,不能互换位置。之所以这样定义,是因为有些算法,需要严格区分左右孩子,如前序-中序-后序遍历、堆排序等问题。
从这个意义上说,树和二叉树是两个概念,不能说二叉树是树的子集。
树转换成二叉树
-
任意一棵 树转换成二叉树,右孩子为空的数目为原树非叶结点+1。
对于一颗树(非二叉树),因为任何一个非叶结点必然有孩子,所以,它必然有最右孩子。从而,这个最右孩子转换到二叉树结点后,右指针必然为空。
同时,根结点必然没有兄弟,所以,根结点转换成二叉树结点后,必然右孩子为空。 -
任意棵 树转换成二叉树,右孩子为空的数目为原树非叶结点+1。
如果是若干个树转二叉树,这若干个树最右边的那个树,是没有右孩子的。
树基本操作
- 插入
- 删除
- 查找
实现的头文件代码:treeNode.h
#include<stdio.h>
#include <stdlib.h>
#include<iostream>
#include<algorithm>
using namespace std;
typedef struct tagsTreeNode{
int value;
tagsTreeNode* pLeft;
tagsTreeNode* pRight;
tagsTreeNode(int v) :value(v), pLeft(NULL), pRight(NULL){}
}STreeNode;
typedef void(*VISIT)(int value);
class CBinaryTree
{
private:
STreeNode* m_pRoot;
private:
STreeNode* Find(int value) ; //二叉查找树查找
void destroy(STreeNode* pRoot) ;
void Delete(int value);
bool insert(STreeNode* pRoot, int value) ;//递归
bool insert2(int vlaue) ;//非递归
void PreOrder(STreeNode* pRoot, VISIT Visit) ;
void PreOrder2(VISIT Visit) ;
void InOrder(STreeNode* pRoot, VISIT Visit) ;
void Inorder2(VISIT Visit) ;
void Inorder3(VISIT Visit) ;
void PostOrder(STreeNode* pRoot, VISIT Visit) ;
void PostOrder2(VISIT Visit) ;
void DeleteChildless(STreeNode* pParent, STreeNode* pNode);
void DeleteSingleSon(STreeNode* pParent, STreeNode* pNode);
void DeleteDoubleSon(STreeNode* pNode);
};
从上面的代码也可以看出,树在代码的实现上其实是利用了链表的形式。
二叉查找树
二叉查找树(二叉搜索树)是满足以下条件的二叉树:
- 左子树上的所有结点值均小于根结点值,
- 右子树上的所有结点值均不小于根结点值,
- 左右子树也满足上述两个条件。
查找
查找一般是针对二叉查找树而言。
查找步骤
给定一颗二叉查找树,查找某结点 p p p 的过程如下:
- 将当前结点 c u r cur cur 赋值为根结点 r o o t root root;
- 若 p p p 的值小于当前结点 c u r cur cur 的值,查找 c u r cur cur 的左子树;
- 若 p p p 的值不小于当前结点 c u r cur cur 的值,查找 c u r cur cur 的右子树;
- 递归 上述过程,直到
c
u
r
cur
cur 的值等于
p
p
p 的值或者
c
u
r
cur
cur 为空;
当然,若结点是结构体,注意定义“小于”“不小于”“等于”的具体函数。
#include<treeNode.h>
STreeNode* CBinaryTree::Find(int value)
{
if (!m_pRoot)
return NULL;
STreeNode* pNode = m_pRoot;
while (pNode)
{
if (value < pNode->value)
pNode = pNode->pLeft;
else if (value>pNode->value)
pNode = pNode->pRight;
else
return pNode;
}
return NULL;
}
插入
插入操作一般也是针对二叉查找树而言。
插入步骤:
- 若当前的二叉查找树为空,则插入的元素为根结点,
- 若插入的元素值小于根结点值,则将元素插入到左子树中,
- 若插入的元素值不小于根结点值,则将元素插入到右子树中,
- 递归 上述过程,直到找到插入点为叶子结点。
递归插入代码:
bool CBinaryTree::insert(STreeNode* pRoot, int value)
{
if (!pRoot)
{
pRoot = new STreeNode(value);//如果根结点为空,则插入元素即为根结点
return true;
}
if (value < pRoot->value)
return insert(pRoot->pLeft, value);
else if (value>pRoot->value)
return insert(pRoot->pRight, value);
return false;//如果插入元素的值和树中某结点的值相等,则说明该结点已经存在,返回false,不用进行插入
}
非递归插入代码:
bool CBinaryTree::insert2(int value)
{
if (!m_pRoot)
{
m_pRoot = new STreeNode(value);
}
STreeNode *pNode = m_pRoot;
STreeNode *pCur = m_pRoot;
while (pNode)
{
pCur = pNode;//只有pNode为空时才会停止循环,所以把pNode为空之前的那个结点给pCur保存
if (value < pNode->value)
pNode = pNode->pLeft;
else if (value>pNode->value)
pNode = pNode->pRight;
else
return false;
}
if (value < pCur->value)
pCur->pLeft = new STreeNode(value);
else if (value>pCur->value)
pCur->pRight = new STreeNode(value);
return true;
}
删除
记待删除的结点为 p p p ,分三种情况进行处理:
- p p p 为叶子结点
- p p p 为单支结点
- p p p 的左子树和右子树均不空
待删除点为叶子结点
p p p 为叶子结点,直接删除该结点,再修改 p p p 的父结点的指针。
代码实现:
void CBinaryTree::DeleteChildless(STreeNode* pParent, STreeNode* pNode)//pParen为pNode结点的父结点
{
if (m_pRoot == pNode)
m_pRoot = NULL;
else if (pParent->pLeft == pNode)
pParent->pLeft = NULL;
else
pParent->pRight = NULL;
delete pNode;
}
待删除点只有一个孩子
若 p p p 为单支结点(即只有左子树或右子树),则将 p p p 的子树与 p p p 的父亲结点相连,删除 p p p 即可
代码实现:
void CBinaryTree::DeleteSingleSon(STreeNode* pParent, STreeNode* pNode)
{
STreeNode* pGrandSon = pNode->pLeft ? pNode->pLeft : pNode->pRight;
if (m_pRoot == pNode)
m_pRoot = pGrandSon;
else if (pParent->pLeft = pNode)
pParent->pLeft = pGrandSon;
else
pParent->pRight = pGrandSon;
delete pNode;
}
待删除点有两个孩子
这个操作相比上面有点麻烦。
若 p p p 的左子树和右子树均不空,则找到 p p p 的 直接后继(中序遍历的直接后继) d d d (*** p p p 的右孩子的最左子孙*** ),因为 d d d 一定没有左子树,所以使用删除单支结点的方法:删除 d d d,并让 d d d 的父亲结点 d p dp dp 成为 d d d 的右子树的父亲结点;同时,用 d d d 的值代替 p p p 的值;
对偶的:可以找到 p p p 的 直接前驱 (中序遍历的直接前驱) x x x ( p p p 的左孩子的最右子孙** ), x x x 一定没有右子树,所以可以删除 x x x,并让 x x x 的父亲结点成为 x x x 的左子树的父亲结点。
- 将 p p p 的直接后继的值拷贝到 p p p 处
- 删除 p p p 的直接后继。
代码实现(这里是上面的对偶过程):
void CBinaryTree::DeleteDoubleSon(STreeNode* pNode)//要删除的结点pNode
{
STreeNode* pCur = pNode;//暂存待删结点
STreeNode* pParent = pNode;
//pNode的左孩子的最右子孙
pNode = pNode->pLeft;
while (pNode->pRight)
{
pParent = pNode;
pNode = pNode->pRight;
}
pCur->value = pNode->value;//将要删除结点的左孩子的最右子孙的值赋值给要删除结点的值
//然后将要删除结点的左孩子的最右子孙结点删除。
if (!pNode->pLeft)
DeleteChildless(pParent, pNode);
else
DeleteSingleSon(pParent, pNode);
}
整合一下上面的三种情况下的删除方法:
bool CBinaryTree::Delete(int value)//要删除结点的值为value
{
if (!m_pRoot)
return false;
STreeNode* pNode = m_pRoot;
STreeNode* pParent = NULL;
while (pNode)
{
if (value < pNode->value)
{
pParent = pNode;
pNode = pNode->pLeft;
}
else if (value > pNode->value)
{
pParent = pNode;
pNode = pNode->pRight;
}
else//找到要删除的结点pNode
break;
}
if (!pNode)
return false;
if (!pNode->pLeft&&!pNode->pRight)
DeleteChildless(pParent, pNode);//要删除的结点是叶子结点
else if (!pNode->pLeft || pNode->pRight)
DeleteSingleSon(pParent, pNode);//要删除的结点有一个子结点
else
DeleteDoubleSon(pNode);//要删除的结点有两个子结点。
return true;
}
二叉树的遍历
前序遍历:
- 访问根结点
- 前序遍历左子树
- 前序遍历右子树
中序遍历:
- 中序遍历左子树
- 访问根结点
- 中序遍历右子树
后序遍历:
- 后序遍历左子树
- 后序遍历右子树
- 访问根结点
前序遍历
前序遍历:
15
,
5
,
3
,
12
,
10
,
6
,
7
,
13
,
16
,
20
,
18
,
23
15,5,3,12,10,6,7,13,16,20,18,23
15,5,3,12,10,6,7,13,16,20,18,23
JAVA Node类定义
public class Node{
public int value;
public Node left;
public Node right;
public Node(int data){
this.value = data;
}
}
JAVA版 Mirror遍历(二叉树的神级遍历)其先中后序遍历时间复杂度O(N),空间复杂度O(1)
public class Morrirs {
public void func(Node head) {
if (head == null) {
return;
}
Node cur = head;
Node mostRight = null;
while (cur != null) {
if (cur.left != null) {
mostRight = cur.left;
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
}
if (mostRight.right != cur) {
System.out.println(cur.value);
mostRight.right = cur;
cur = cur.left;
continue;
} else {
mostRight.right = null;
}
}
System.out.println(cur.value);
cur = cur.right;
}
}
public static void main(String[] args) {
Morrirs obj = new Morrirs();
Node node4 = new Node(4);
Node node2 = new Node(2);
Node node1 = new Node(1);
Node node3 = new Node(3);
Node node6 = new Node(6);
Node node5 = new Node(5);
Node node7 = new Node(7);
node4.left = node2;
node4.right = node6;
node2.left = node1;
node2.right = node3;
node6.left = node5;
node6.right = node7;
node1.left = null;
node1.right = null;
node5.left = null;
node5.right = null;
node7.left = null;
node7.right = null;
obj.func(node4);
}
}
JAVA mirror先序遍历
public void func(Node head) {
if (head == null) {
return;
}
Node cur = head;
Node mostRight = null;
while (cur != null) {
if (cur.left != null) {
mostRight = cur.left;
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
}
if (mostRight.right != cur) {
System.out.println(cur.value);
mostRight.right = cur;
cur = cur.left;
continue;
} else {
mostRight.right = null;
}
}
else{
System.out.println(cur.value);
}
cur = cur.right;
}
}
JAVA 普通版 非递归先序遍历(时间复杂度O(N),空间复杂度O(N))
public void func(Node head) {
if (head == null)
return;
Stack<Node> stack = new Stack<>();
stack.push(head);
while (!stack.isEmpty()) {
Node cur = stack.pop();
System.out.println(cur.value);
if (cur.right != null)
stack.push(cur.right);
if (cur.left != null)
stack.push(cur.left);
}
}
c++ 递归代码:
void CBinaryTree::PreOrder(STreeNode* pRoot, VISIT Visit)
{
if (pRoot)
{
Visit(pRoot->value);
PreOrder(pRoot->pLeft, Visit);
PreOrder(pRoot->pRight, Visit);
}
}
c++ 非递归代码:
void CBinaryTree::PostOrder2(VISIT Visit)
{
if (!m_pRoot)
return;
stack<STreeNode*> s;
s.push(m_pRoot);//把根结点压进堆栈
STreeNode* pCur;
while (!s.empty())
{
pCur = s.top();//获取stack头元素
s.pop();//弹栈
Visit(pCur->value);//按照前序遍历顺序,先访问根节点
// 按照右左顺序压栈,然后弹栈顺序就是左右,那么访问顺序就是左右
if (pCur->pRight)
s.push(pCur->pRight);
if (pCur->pLeft)
s.push(pCur->pLeft);
}
}
中序遍历
中序遍历:
3
,
5
,
6
,
7
,
10
,
12
,
13
,
15
,
16
,
18
,
20
,
23
3,5,6,7,10,12,13,15,16,18,20,23
3,5,6,7,10,12,13,15,16,18,20,23
二叉查找树的中序遍历,即为数据的升序过程。
java版 mirror先序遍历
public void func(Node head) {
if (head == null) {
return;
}
Node cur = head;
Node mostRight = null;
while (cur != null) {
if (cur.left != null) {
mostRight = cur.left;
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
}
if (mostRight.right != cur) {
//System.out.println(cur.value);
mostRight.right = cur;
cur = cur.left;
continue;
} else {
mostRight.right = null;
}
}
//else{
System.out.println(cur.value);
//}
cur = cur.right;
}
}
JAVA 普通版非递归中序遍历,时间复杂度O(N), 空间复杂度O(N)
public void func(Node head) {
if (head == null)
return;
Stack<Node> stack = new Stack<>();
Node cur = head;
while (cur != null || !stack.isEmpty()) {
if (cur != null) {
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
}else{
cur = stack.pop();
System.out.println(cur.value);
cur = cur.right;
}
}
}
递归代码:
void CBinaryTree::InOrder(STreeNode* pRoot, VISIT Visit)
{
if (pRoot)
{
InOrder(pRoot->pLeft, Visit);
Visit(pRoot->value);
InOrder(pRoot->pRight, Visit);
}
}
非递归代码:
void CBinaryTree::InOrder2(VISIT Visit)
{
stack<STreeNode*> s;
STreeNode* pCur = m_pRoot;
while (pCur||!s.empty())
{
while (pCur)//寻找最左孩子
{
s.push(pCur);
pCur = pCur->pLeft;
}
if (!s.empty())
{
pCur = s.top();//访问左孩子为空的结点
s.pop();//栈顶元素弹栈
Visit(pCur->value);
pCur = pCur->pRight;//转向右孩子
}
}
}
还可以这样的进行非递归的中序遍历:
void CBinaryTree::InOrder3(VISIT Visit)
{
if (!m_pRoot)
return;
stack<pair<STreeNode*, int> > s;//结点和结点对应的压栈次数
s.push(make_pair(m_pRoot, 0));
int times;
STreeNode* pCur;
while (!s.empty())
{
pCur = s.top().first;
times = s.top().second;
s.pop();
if (times == 0)//第一次压栈
{
//中序访问顺序:左根右;则压栈顺序:右根左
if (pCur->pRight)
s.push(make_pair(pCur->pRight,0));
s.push(make_pair(pCur, 1));//第二次压栈
if (pCur->pLeft)
s.push(make_pair(pCur->pLeft, 0));
}
else//只有压栈次数大于0才能访问
{
Visit(pCur->value);
}
}
}
后序遍历
后序遍历:
3
,
7
,
6
,
10
,
13
,
12
,
5
,
18
,
23
,
20
,
16
,
15
3,7,6,10,13,12,5,18,23,20,16,15
3,7,6,10,13,12,5,18,23,20,16,15
Morri后序遍历
public void func(Node head) {
if (head == null) {
return;
}
Node cur = head;
Node mostRight = null;
while (cur != null) {
if (cur.left != null) {
mostRight = cur.left;
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
}
if (mostRight.right == null) {
mostRight.right = cur;
cur = cur.left;
continue;
} else {
mostRight.right = null;
printReverseVertex(cur.left);
}
}
cur = cur.right;
}
printReverseVertex(head);
}
public void printReverseVertex(Node node) {
Node tail = reverse(node);
Node cur = tail;
while(cur != null){
System.out.println(cur.value);
cur = cur.right;
}
reverse(tail);
}
public Node reverse(Node node) {
Node pre = null;
Node next = null;
while (node != null) {
next = node.right;
node.right = pre;
pre = node;
node = next;
}
return pre;
}
JAVA 普通版后序遍历 时间复杂度O(N),空间复杂度O(N)
public void func(Node head){
Stack<Node> stack = new Stack<>();
Stack<Node> stack2 = new Stack<>();
stack.push(head);
while(!stack.isEmpty()){
Node cur = stack.pop();
stack2.push(cur);
if(cur.left != null)
stack.push(cur.left);
if(cur.right != null)
stack.push(cur.right);
}
while(!stack2.isEmpty()){
Node cur = stack2.pop();
System.out.println(cur.value);
}
}
递归代码:
void CBinaryTree::PostOrder(STreeNode* pRoot, VISIT Visit)
{
if (pRoot)
{
InOrder(pRoot->pLeft, Visit);
InOrder(pRoot->pRight, Visit);
Visit(pRoot->value);
}
}
非递归代码:
void CBinaryTree::PostOrder2(VISIT Visit)
{
if (!m_pRoot)
return;
stack<pair<STreeNode*, int>> s;
s.push(make_pair(m_pRoot, 0));
STreeNode* pCur;
int time;
while (!s.empty())
{
pCur = s.top().first;
time = s.top().second;
s.pop();
if (time == 0)//第一次压栈
{
//后序访问顺序:左右根,则压栈顺序:根右左
s.push(make_pair(pCur, 1));//第二次压栈
if (pCur->pLeft)
s.push(make_pair(pCur->pLeft, 0));
if (pCur->pRight)
s.push(make_pair(pCur->pRight, 0));
}
else
{
Visit(pCur->value);
}
}
}
根据前序中序,计算后序
如:已知某二叉树的遍历结果如下,求它的后序遍历序列
前序遍历:
G
D
A
F
E
M
H
Z
GDAFEMHZ
GDAFEMHZ
中序遍历:
A
D
E
F
G
H
M
Z
ADEFGHMZ
ADEFGHMZ
两个步骤:
- 根据前序中序,构造二叉树
- 后序遍历二叉树
前序遍历:
G
D
A
F
E
M
H
Z
GDAFEMHZ
GDAFEMHZ
中序遍历:
A
D
E
F
G
H
M
Z
ADEFGHMZ
ADEFGHMZ
- 根据前序遍历的特点得知,根结点为 G G G;
- 根结点将中序遍历结果 A D E F G H M Z ADEFGHMZ ADEFGHMZ 分成 A D E F ADEF ADEF 和 H M Z HMZ HMZ 两个左子树、右子树。
- 递归 确定中序遍历序列 A D E F ADEF ADEF 和前序遍历序列 D A E F DAEF DAEF 的子树结构;
- 递归确定中序遍历序列 H M Z HMZ HMZ 和前序遍历序列 M H Z MHZ MHZ 的子树结构;
显然问题的关键就是不断的找到各部分的根结点。
根据前序中序,构造二叉树
前序遍历:
G
D
A
F
E
M
H
Z
GDAFEMHZ
GDAFEMHZ
中序遍历:
A
D
E
F
G
H
M
Z
ADEFGHMZ
ADEFGHMZ
代码实现:
#include<stdio.h>
#include <stdlib.h>
#include<iostream>
#include<algorithm>
using namespace std;
void InPre2Post(const char* pInOrder, const char* pPreOrder, int nLength, char* pPostOrder, int& nIndex)
{
if (nLength <= 0)
return;
if (nLength == 1)//长度为1,则直接把前序遍历的首字符作为后序遍历结果即可。
{
pPostOrder[nIndex] = *pPreOrder;
nIndex++;
return;
}
char root = *pPreOrder;//前序遍历首字符,也即是根结点
int nRoot = 0;
for (; nRoot < nLength; nRoot++)//查找根结点在中序遍历中的位置nRoot
{
if (pInOrder[nRoot] == root)
break;
}
//后序遍历访问顺序:左右根
InPre2Post(pInOrder, pPreOrder + 1, nRoot, pPostOrder, nIndex);//根结点的左子树部分再递归求,相当于访问左结点
InPre2Post(pInOrder + nRoot + 1, pPreOrder + nRoot + 1, nLength - nRoot - 1, pPostOrder, nIndex);//根结点的右子树部分再递归求,相当于访问右结点
pPostOrder[nIndex] = root;//访问根结点
nIndex++;
}
int main()
{
char pPreOrder[] = "GDAFEMHZ";
char pInOrder[] = "ADEFGHMZ";
int size = sizeof(pInOrder) / sizeof(char);
char* pPostOrder = new char[size];
int nIndex = 0;
InPre2Post(pInOrder, pPreOrder, size - 1, pPostOrder, nIndex);
pPostOrder[size - 1] = 0;
cout << pPostOrder << endl;
delete[] pPostOrder;
return 0;
}
同理已知中序遍历还后序遍历求前序遍历,由后序遍历知道根结点,然后找到左子树右子树,再在各部分递归。问题的核心还是每次递归确定各部分的根结点。
#include<stdio.h>
#include <stdlib.h>
#include<iostream>
#include<algorithm>
using namespace std;
void InPost2Pre(const char* pInOrder, const char* pPostOrder, int nLength, char* pPreOrder, int& nIndex)
{
if (nLength <= 0)
return;
if (nLength == 1)//长度为1,则直接把前序遍历的首字符作为后序遍历结果即可。
{
pPreOrder[nIndex] = *pPostOrder;
nIndex++;
return;
}
char root = *pPostOrder[size-1];//前序遍历首字符,也即是根结点
//前序遍历访问顺序:根左右
pPreOrder[nIndex]=root;
nIndex++;
int nRoot = 0;
for (; nRoot < nLength; nRoot++)//查找根结点在中序遍历中的位置nRoot
{
if (pInOrder[nRoot] == root)
break;
}
InPost2Pre(pInOrder, pPostOrder, nRoot, pPreOrder, nIndex);//根结点的左子树部分再递归求,相当于访问左结点
InPost2Pre(pInOrder + nRoot + 1, pPostOrder + nRoot , nLength - nRoot - 1, pPreOrder, nIndex);//根结点的右子树部分再递归求,相当于访问右结点
}
int main()
{
char pPostOrder[] = "AEFDHZMG";
char pInOrder[] = "ADEFGHMZ";
int size = sizeof(pInOrder) / sizeof(char);
char* pPostOrder = new char[size];
int nIndex = 0;
InPost2Pre(pInOrder, pPostOrder, size - 1, pPreOrder, nIndex);
pPostOrder[size - 1] = 0;
cout << pPostOrder << endl;
delete[] pPostOrder;
return 0;
}
判断数组有无可能是二叉查找树后序遍历结果
给定整数数组,判断该数组有无可能是一颗***二叉查找树*** 后序遍历的结果。假定数组中***没有重复元素***。
由于后序遍历的最后一个元素为根结点,根据该结点,将数组分成前后两段,使得前半段都小于根结点,后半段都大于根结点;如果不存在这样的划分,则不可能是后序遍历的结果。递归判断前后半段是否满足同样的要求。
#include<stdio.h>
#include <stdlib.h>
#include<iostream>
#include<algorithm>
using namespace std;
bool CanPostOrder(const int*a, int size)
{
if (size <= 1)
return true;
int root = a[size - 1];
int nLeft = 0;
while (nLeft<size-1)
{
if (a[nLeft] > root)
break;
nLeft++;
}
int nRight = size - 2;//size-1是根结点
while (nRight>=0)
{
if (a[nRight] < root)
break;
nRight--;
}
if (nRight != nLeft - 1)//无法根据root分成两部分
return false;
return CanPostOrder(a, nLeft) && CanPostOrder(a + nLeft, size - nLeft - 1);
}