【算法与数据结构 02】二叉树的引入

193 阅读5分钟

相信大家对于树都不会太陌生,今天就来简单聊聊关于数据结构中的树的那些东西吧~

一、树的简单介绍

1.1 什么是树?

先来看看百度百科上对树的解释

树状图是一种数据结构,它是由n(n >= 1) 个有限结点组成一个具有层次关系的集合

把它叫做 “树” 是因为它看起来像一颗倒挂的树,也就是说它是根朝上,而叶朝下

它大多长这样:

再来看看一些结点的称呼或是概念性的东西

  • A 结点是 B 和 C 结点的上级,也就是说 A 结点是 B 和 C 结点的父节点,B 和 C 结点是 A 结点的子结点
  • B 和 C 结点同时是 A 结点的子结点,称为兄弟结点
  • A 结点没有父节点,称为根节点
  • D 、E 和 F 结点没有子结点,称为叶子节点
  • 树深即为树中结点的最大层次数(也称高度深度),如图深度即为 3

可以看出,树由根结点和若干颗子树构成,换句话说,剔除一颗树的根节点后,它的子结构也满足树的特性:

  • 每个结点有零个或多个结点
  • 每个非根结点有且仅有一个父节点
  • 树里面没有环路

提到树就不得不说起二叉树了

1.2 什么是二叉树

二叉树:每个结点最多含有两个结点的树,两个结点分别为左子结点和右子结点

来看看一个二叉树的简单实现

public class TreeNode {
	int val;
	TreeNode left;
	TreeNode right;
	TreeNode() {}
	TreeNode(int val) { this.val = val; }
	TreeNode(int val, TreeNode left, TreeNode right) {
		this.val = val;
		this.left = left;
		this.right = right;
	}
}

二叉树当中也有几种特殊的类型:

  • 满二叉树:所有结点都有两个子结点(当然除开最后一层的叶子结点)

  • 完全二叉树:除最后一层结点个数都达到最大,并且最后一层的叶子结点都往左排列

(从定义可以发现,一个满二叉树一定是完全二叉树)

之所以会称为完全二叉树,是从它存储空间利用率来看的

比如上面的完全二叉树:

012345678
ABCDEFGH

如果这里是一颗非完全二叉树,则会浪费比较多的存储空间:

01234567
ABCDEF
89101112131415
GHI
  • 二叉搜索树:满足下列约束条件:

    1. 若左子树不为空,则左子树上所有结点的值均小于它的根节点的值
    2. 若右子树不为空,则右子树上所有结点的值均大于它的根节点的值
    3. 左、右子树也必须是二叉搜索树

二叉查找树中,会尽可能避免两个结点数值相等的情况

二叉查找树也有几种优化,例如 AVL 树、红黑树、哈夫曼树 ... 之后再做介绍

二、二叉树的遍历方式

二叉树的遍历是指从根结点触发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问且仅被访问一次

二叉树的遍历方式大体上有四种经典方式:前序遍历、中序遍历、后序遍历、层序遍历

其中前序、中序和后序遍历本质上可以认为是深度优先遍历(DFS),而二叉树的层序遍历本质上可以认为是广度优先遍历(BFS),这里不作详细介绍,想了解的朋友可以期待下一篇文章哦~

DFS 简单来说就是对每一个可能的分支路径深入到最底的遍历方式;而 BFS 简单来说就是一层一层由内而外的遍历方式

  • 前序遍历:对于树中的任意结点,先打印这个结点,然后前序遍历它的左子树,最后前序遍历它的右子树

  • 中序遍历:对于树中的任意结点,先中序遍历它的左子树,然后打印这个结点,最后中序遍历它的右子树

  • 后序遍历:对于树中的任意结点,先后序遍历它的左子树,然后 后序遍历它的右子树,最后打印这个结点

    (也就是说,这里的序指的是父节点的遍历顺序)

实现前序、中序和后序遍历普遍用的还是递归,个人觉得递归相比非递归的方法更妙一些~,下面贴出代码片段方便大家理解

其实这三种遍历方式有很大的相同之处,正如它们的定义一般

2.1 前序遍历

public static void preOrderTraversal(TreeNode head) {
	if (head == null) {
		return;
	}
	System.out.print(head.val + " ");
	preOrderTraversal(head.left);
	preOrderTraversal(head.right);
}

2.2 中序遍历

public static void inOrderTraversal(TreeNode head) {
	if (head == null) {
		return;
	}
	inOrderTraversal(head.left);
	System.out.print(head.val + " ");
	inOrderTraversal(head.right);
}

这里额外提一下二叉查找树的中序遍历:

对二叉查找树进行中序遍历,就可以输出一个按数值从小到大的有序数据队列(这里就直接拿上面的图吧,偷懒~)

比如这里就会打印出:9、13、15、16、18、21、23、25

2.3 后序遍历

public static void postOrderTraversal(TreeNode head) {
	if (head == null) {
		return;
	}
	postOrderTraversal(head.left);
	postOrderTraversal(head.right);	
	System.out.print(head.val + " ");
}

2.4 层序遍历

层序遍历就像上面所提到的那样,即逐层的、从左到右访问所有结点

层序遍历的实现可以使用队列的特性,把每个还没有访问到的结点依次放入队列,然后再弹出队列的头部元素当作是当前的结点:

public static void levelOrder(TreeNode head){
	Queue<TreeNode> queue = new LinkedList<>();
	queue.add(node);
	while (!queue.isEmpty()){
		TreeNode cur = queue.poll();
		if(cur.left != null) {
			queue.add(cur.left);
		} 
		if(cur.right != null) {
			queue.add(cur.right);
		}
	}
}

使用队列保存每层的所有结点,每次把队列里原先的所有结点进行出队列操作,再把每个元素的非空左右子节点入队,即可得到每层的遍历


时隔五个月多自己又有了写博客的想法,希望自己能一直坚持下去,另外,朋友们的点赞和关注是对我最大的支持 :D