HuKai's Blog HuKai's Blog
首页
  • Java核心技术

    • Java基础
    • Java并发编程
    • JVM
    • Java新特性
  • Spring生态

    • Spring5
    • SpringMVC
    • SpringBoot
  • 开源框架

    • MyBatis
  • 计算机网络
  • 操作系统
  • 数据结构与算法
  • 设计模式
  • SQL数据库

    • MySQL
    • Oracle
  • NoSQL数据库

    • Redis
    • MongoDB
  • 页面样式

    • HTML
    • CSS
  • JavaScript

    • JavaScript基础
    • ECMAScript6教程
    • TypeScript
  • 前端框架

    • Vue
    • Webpack
  • NIO
  • Netty
  • RabbitMQ
  • 技术文档

    • GitHub技巧
    • 博客搭建
    • 技术笔记
  • 优质文章

    • 小技巧
    • 解决方案
GitHub (opens new window)

HuKai

梦想成为全栈的保安
首页
  • Java核心技术

    • Java基础
    • Java并发编程
    • JVM
    • Java新特性
  • Spring生态

    • Spring5
    • SpringMVC
    • SpringBoot
  • 开源框架

    • MyBatis
  • 计算机网络
  • 操作系统
  • 数据结构与算法
  • 设计模式
  • SQL数据库

    • MySQL
    • Oracle
  • NoSQL数据库

    • Redis
    • MongoDB
  • 页面样式

    • HTML
    • CSS
  • JavaScript

    • JavaScript基础
    • ECMAScript6教程
    • TypeScript
  • 前端框架

    • Vue
    • Webpack
  • NIO
  • Netty
  • RabbitMQ
  • 技术文档

    • GitHub技巧
    • 博客搭建
    • 技术笔记
  • 优质文章

    • 小技巧
    • 解决方案
GitHub (opens new window)
  • 计算机网络

  • 操作系统

  • 数据结构与算法

    • 数据结构概述
    • 算法概述
    • 时间空间复杂度
    • 线性表的顺序存储结构
    • 链表
    • 栈
    • 队列
    • 排序算法上篇
    • 查找算法
    • 哈希表
    • 树
    • 二叉树
    • 遍历二叉树
    • 线索二叉树
      • 线索二叉树原理
      • 线索化
      • 线索二叉树Java实现
    • 排序算法下篇
    • 赫夫曼树及其应用
    • 二叉排序树
    • 平衡二叉树(AVL树)
    • 多路查找树
    • 图
    • 图的遍历
  • 设计模式

  • 基本功
  • 数据结构与算法
HuKai
2022-03-20
目录

线索二叉树

# 线索二叉树原理

我们在有n个结点的二叉链表中,每个结点有指向左右2个孩子的指针域,所以有2n个指针域,而n个结点的二叉树一共有n-1条分支线,也就是说,其实存在2n-(n-1) = n+1 个空指针域。空间十分浪费。

如下图所示一棵二叉树一共有10个结点,空指针有11个。

另一方面,我们在做遍历时,比如对上图做中序遍历,结果为H、D、I、B、J、E、A、F、C、G。可以得知A的前驱结点为E,后继结点为F。但是,这种关系的获得是建立在完成遍历后得到的,那么可不可以在建立二叉树时就记录下前驱后继的关系呢?那么在后续寻找前驱结点和后继结点时将大大提升效率。

综合刚才两个角度的分析后,我们可以考虑利用那些空地址,存放指向结点在某种遍历次序下的前驱和后继结点的地址。我们把这种指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树。

# 线索化

为了存放指向结点在某种遍历次序下的前驱和后继结点的地址,我们定义如下规则:

  • 若结点的左子树为空,则该结点的左孩子指针指向其前驱结点
  • 若结点的右子树为空,则该结点的右孩子指针指向其后继结点

这种指向前驱和后继的指针称为线索。将一棵普通二叉树以某种次序遍历,并添加线索的过程称为线索化。

按照规则将上图所示二叉树中序线索化后如图所示:

图中黑色点画线为指向后继的线索,紫色虚线为指向前驱的线索。

可以看出通过线索化,既解决了空间浪费问题,又解决了前驱后继的记录问题。但是新问题又产生了,我们如何区分一个结点的leftChild指针是指向左孩子还是前驱结点呢?例如:对于上图所示的结点E,如何区分其leftChild的指向的结点J是其左孩子还是前驱结点呢?

为了解决这一问题,现需要添加标志位leftFlag,rightFlag。并定义规则如下:

  • leftFlag为0时,指向左孩子,为1时指向前驱
  • rightFlag为0时,指向右孩子,为1时指向后继

添加leftFlag和rightFlag属性后的结点结构如下:

将上图线索二叉树转变为如下图所示的二叉树:

# 线索二叉树Java实现

# 1. 线索二叉树结构实现

由此二叉树的线索存储结构定义代码如下:

/**
 * 线索二叉树结点结构
 * @author hukai
 *
 */
public class ThreadedNode<T> {
	
	private ThreadedNode<T> leftNode, rightNode;
	
	//左孩子是否为线索,右孩子是否为线索
	private boolean leftThreadFlag, rightThreadFlag;
	
	private T data;
	
	public ThreadedNode() {
		this.leftThreadFlag = false;
		this.rightThreadFlag = false;
	}

	public ThreadedNode(T data){
		this.data = data;
		this.leftThreadFlag = false;
		this.rightThreadFlag = false;
	}

	// getter  setter...

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 2. 线索二叉树实现

线索化的实质就是将二叉链表中的空指针改为指向前驱或后继的线索。由于前驱和后继的信息只有在遍历该二叉树时才能得到,所以线索化的过程就是在遍历的过程中修改空指针的过程。

/**
 * 线索二叉树
 * @author hukai
 *
 * @param <T>
 */
public class ThreadBinaryTree<T> {
	
	// 根结点
	private ThreadedNode<T> root;
	
	// 结点个数
	private int size;
	
	// 线索化的时候临时保存前驱
	private ThreadedNode<T> tempPre;
	
	// 以默认的构造器创建二叉树
	public ThreadBinaryTree() {
		this.root = new ThreadedNode<T>();
	}

	// 以指定根元素创建二叉树
	public ThreadBinaryTree(T rootData) {
		this.root = new ThreadedNode<T>(rootData);
	}
	
	// 以指定数组创建二叉树
	public ThreadBinaryTree(T[] datas) {
		this.tempPre = null;
		this.size = 0;
		this.root = createTree(datas, 0);
	}
	
	private ThreadedNode<T> createTree(T[] datas,int rootIndex){
		// 超出索引范围或者根结点为空,直接返回null
		if (rootIndex >= datas.length || datas[rootIndex] == null) return null;
		
		ThreadedNode<T> node = new ThreadedNode<T>(datas[rootIndex]);
		
		size++;
		
		ThreadedNode<T> leftNode = createTree(datas, 2*rootIndex+1);
		ThreadedNode<T> rightNode = createTree(datas, 2*rootIndex+2);
		
		node.setLeftNode(leftNode);
		node.setRightNode(rightNode);
		
		return node;
	}
	
	/**
	 * 中序线索化
	 */
	public void inThreading(ThreadedNode<T> root){
		if (root == null) return;
		// 线索化左孩子
		inThreading(root.getLeftNode());
		// 左孩子为空,将左孩子设置为线索
		if (root.getLeftNode() == null) {
			root.setLeftThreadFlag(true);
			root.setLeftNode(tempPre);
		}
		// 上一个节点右孩子为空,将右孩子设置为线索
		if (tempPre != null && tempPre.getRightNode() == null) {
			tempPre.setRightThreadFlag(true);
			tempPre.setRightNode(root);
		}
		
		tempPre = root;
		// 线索化右孩子
		inThreading(root.getRightNode());       
	}
	
	
	/**
	 * 返回根结点
	 * @return
	 */
	public ThreadedNode<T> root(){
		return root;
	}
	
	/**
	 * 返回元素个数
	 * @return
	 */
	public int size(){
		return size;
	}
	
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

注意上面的代码中,在使用数组初始化二叉树的时候,采用了递归的方式。而在将二叉树中序线索化时同样使用了递归的方式,代码结构也几乎完全与二叉树中序遍历的递归代码一样。所以同理也可以对比二叉树的前序遍历和后序遍历推导出前序线索化和后序线索化的方法。

# 3. 遍历线索二叉树

有了线索二叉树后,我们对它进行遍历时发现,其实就等于是操作一个双向链表结构。

/**
	 * 中序遍历线索二叉树
	 * @param root
	 */
	public void inThreadList(ThreadedNode<T> root) {
		if (root == null) return;
		
		// 查找中序遍历的起始节点
		while (root != null && !root.isLeftThreadFlag()) {
			root = root.getLeftNode();
		}
		do {
			System.out.print(root.getData() + "   ");
			// 如果右孩子是线索
			if (root.isRightThreadFlag()) {
				root = root.getRightNode();
			}else{
				// 有右孩子
				root = root.getRightNode();
				while(root != null && !root.isLeftThreadFlag()){
					root = root.getLeftNode();
				}
			}
			
		} while (root != null);
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

从这段代码也可以看出,它等于是一个链表的扫描,所以时间复杂度为0(n)。

由于它充分利用了空指针域的空间(这等于节省了空间),又保证了创建时的一次遍历就可以终生受用前驱后继的信息(这意味着节省了时间)。所以在实际问题中,如果所用的二叉树需经常遍历或査找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是非常不错的选择。

编辑 (opens new window)
#数据结构
上次更新: 2022/03/20, 16:39:15
遍历二叉树
排序算法下篇

← 遍历二叉树 排序算法下篇→

最近更新
01
MyBatisPlus
03-20
02
MyBatis源码剖析-延迟加载
03-20
03
MyBatis源码剖析-二级缓存
03-20
更多文章>
Theme by Vdoing | Copyright © 2021-2022 HuKai | 赣ICP备17016768号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式