赣州新闻网

首页 / 科技

思必驰基于 TH1520 芯片的离在线语音模组:全面推进家电语音智能化

2020-03-31 · 赣州新闻网 · 科技

目录

什么是二叉树?

在实现二分搜索树之前,我们先思考一下,为什么要有树这种数据结构呢?我们通过企业的组织机构、文件存储、数据库索引等这些常见的应用会发现,将数据使用树结构存储后,会出奇的高效,树结构本身是一种天然的组织结l # }构。常见的树结构有:二分搜索树、平衡二叉树(常见的平衡二叉树有AVL和红黑树)、堆、并查集、线段树、Trie等。Trie又叫字典树或前缀树。

树和链表一样,都属于动态数据结构,由于二分搜索树是二叉树的一种,我们先来说说什么是二叉树。二叉树具有 唯一的根节点 ,二叉树每个节点 最多 有两个孩子节点,二叉树的每个节点 最多有一个父亲节点 ,二叉树具有天然递归结构,每个节点的左子数也是一棵二叉树,每个节t n % i @ % ` y点的右子树也是一颗二叉树。二叉树如下图:

二分搜索树(Binary Search Tree)

什么是二分搜索树?

二分搜索树也是一种二叉树,但二分搜索树种每T R 5 a Z z个节点的值x Q @ ( W 5 !都要大于其左子树所有节点的值,小于其右子树所有节点的值,每一棵子树也是二分搜索树。正因为二分搜索树的这种性质,二分搜l ) ` e A V K y g索树存储的元素必须具有可比较性。下图就是一棵二分搜索树:

二分搜索树(Binary Search Tree)

我们可以根据二分搜索树的特点,构建一颗二分搜索树,代码J p O B ! U实现如下:

/**
* 由于二分搜索树中的元素必须具有可比较性,所以二分搜索树中存储的数据必须要实现Comparable接口
* @param <E>
*/
public class BST&I % H _ . ^ 7lt;E extends Comparabl7 J ? ( 0 je<E>4 9 m;&g, V r %t; {

//E + U ; & I j由于用户并不需要知道我们二分搜索树的具体实现,所以这里把节点设置成内部类
private class Node {
public E e;
public Node left, right;

public Nop p ^ I t O 7de] m ^ ((E e) {
this.e = e;
left = null;
right = null;
}
}

//一棵树只有一个根节点T N E
private Node root;
//节点的个数
private int sizeY ! n 2 | 0 Y x;

//无参构造,这里不写也可以,因为和W l K J l q系统默认的无参构造是相同的
public BST(){
root = null# Y L 8 2 ] 8 $ @;
size = 0;
}

//获取节点的个数
public int size(){
return size;
}

//判断树是否为空
public boolean isEmpty(){
return size == 0;
}
}

二分搜索树的基本操作

二分搜索树添加新元素

我们在向二分搜索中添加元素时,需要保持二分搜索树的性质,即需要将我! | 6 J ^ G 6 &们添加的元素从根节点开始比P p @ o & Y较,若比根节点小,则去根节点的左子树递归c $ n b | * [ e v进行添加操作,若比根节点的右子树递归进行添加操作,若相等,则直接返回,因为本文实现的是一棵不包含重复元素的二分搜索树。具体代码实现如下:

// 向二分搜索树中添M j t C t n $ V {加新的元素ep n 8 { * ) 4  b ^ | ~ ~ xe
add(root, e);
}

//C x o 向以node为根的二分搜索树中插入元素e,递归算法
privg V t C R V tate void add(Node node, E e){
//如果被添加的元素与当前节点的值相等,则直接返回 -- 本文的二分搜l [ D K { Q # .索中不包含重复元素
if(e.equals(node.e))x W $ + & H
return;
//如果待添加元素e小于当前节点的值,并且当前节点没有左孩子
else if(e.compareTo(node.g | ? f `e) < 0 &&am- n 3 B E }p; nod$ d r I V `e.left == null){
//则将待添加元素e作为当前节点的左孩子
node.left = new Node(e);
size ++;
return;
}
//如果待添加元素e大于当前节点的值,并且当前节点没有右孩子
else if(e.compareTo(node.e) > 0 &am. | h G - ! fp;& node.righM 9 e g wt == null){
//则将待添加元素e作为当前节点的右孩子,并返回
node.right = new Node(e);
size ++;
return;
}
//如果以上条件都不满足,则根据| = $ H 8 ^ 1 Ce //e.compareTJ E % b { 3 Bo(node.e) > 0
add(node.right, e);
}

通过上面添加方法的代码实现中,可以看出有如下两l Q 7 o O 8 a点不足并且可以优化的地方:1.y / N ^ z @ w待添加元素e需要与当前节点的值进行两轮比较;2.递归终止条件太臃肿了.我d U 3 O p d =们可以来简化一下上面的添加元素的方法,如下:

// 向二分搜索树中添加新元素e
public void add(E e){
root = add(root,e);
}

//向以node为根的二分搜索树中插入元素e,递归算法
private Nodek O h B 2 add(Node node, E e) {
if (node == null){
node = new Node(e);
size ++;
return node;
}
if (e.compareTo(node.e) < 0){
node.left = add(nM 5 7 n ( v 0 H vode.left,e);
}else if (e.compareTo(node.e) > 0){
node.rW F 5 $ Light = add(node.right , e);
}
return node;
}

改进之后C C 4 6 g p : 7添加方法就简洁很多s # O - Q r u n了,现在我们完成了二分搜索树的添加后* 8 N h 9 l ),想一下如何在二分搜索中查找某个元素呢?我们可以用contains()方法来表示当前二分搜索中是否包含该元素,代码实现如下:

//看二分搜索树中是否包含元素e
public boolean contains(E e){
//B z & T b E _ 3使用递归查找
return contains(root,e);
}

private boolean conta8 % kins(Node node, E e) {
//如果根节点为空,则该二分搜索中肯L o g , u z 1 +定没有带查找元素e,直接返回false
if (node == null)
return false;
//如果当前节点的值和待查找元素e相等,则返回true
if (e.cS $ z 7 NompareTo(nT q Kode.e) == 0){
return true;
//如果待查找V _ u d q k元素e小于当前节点的值,则去当前节点的左子树进行递归查找
}else if (a v U [ m t H we.compareTo(node.e) < 0 ){
return contais X k Kns(node.left,ei ! 5 z @ ( b 4);
//如果待查找元素e大于当前节点的值,则去当前节点的右子树进行递归查找
}else { //(e.compareTo(node.e) >6 C E S; 0 )
return contains(node.right,e);
}
}

二分搜索树的遍历(包含o d ? W d非递归实现)

什么是遍历操作?

遍历操作就是把所有的节点都访问一遍,当然访问的原因和你如何访问都和你具体的业务相关,本文主要是通过在在控制台打印输出该节点的值,来完成访问的。我们知道在线性结构下,遍历是极其容易的,比如数组| K |和链表的遍历,当然在E o D ; + z A v树结构下,我们可以通过递Q [ } 4 e归来使二分搜索树的遍历变得非常简单。

  • 递归实现

1. 前序遍历

//二分搜索树的前序遍历 -- 递归算法
public void pren 3 ROrder(){
preOrder(root);
}

private void preOrder(Node node) {
if (node == nuV [ 9 f b Kll)
return;
//访问该节点
System.out.print(node.e +\"\\t\");
//递归遍历左子树
preOrder(node.left);
//递归遍历右子树
preOrde@ z 2 { ? 4 s Ar(node.right);
}

就几行Z : ` s S X c :代码,我们就已经4 w i C Y w v o [完成了我们的前序遍历,是不是很简单,我们@ d 9现在可以来测试一下我们前面写的添加方法和现在的前序遍历操作,为了更好在控制台看我们的打印结果,我们需要重写一下二分搜索树P T a Q X w e o的toString(),我们可D ( l r z以用“--”来表示节点所在的深度,让输出效果更直观,实现如下:

//重写toString()
@Override
public String t8 # ! I E # oString() {
StringBuilder builder = new StringBuilder();
generav E y ,teBSTString(root,0,builder);
return builder.toString();
}

//生成以node为根节点,深度为depth的p C P描O f 4 ; d 2 N述二叉树的字符串
private void generateBSTString(Node node, int depth, Stri# / L } $ 0 FngBuilder builder) {
if(node == null) - z 6 k{
builder.append(generateDepthStrN # e Fing(depth) + \"null\\n\");
return;
}
builder.apR @ J 2pend(generateDepthString(depth) + node.e + \"\\n\f q a A F g %");Z B N
gener i * _ +ateBSTString(node.left,depth+1,builder);
generateBSTString(node.right,depth+1,builder);
}

//添加深度标识符
private String generateDepthString(( P : 3 L |int depth) {
Strina ^ Y qgBuilder res = new StringBuilder();
for (int i=0; i<depth;) R A , S ! i! , W q c Y n U++){
res.append(\"--\");
}
return res.toString()T L | r S ?;
}

我们可以在主方法中添加我们的测试代码,测试代码如下:

public sR / A = 5 I {tatic void main(String args) {
BST<Integer> bst = new BST<IntegD p E 9 ? r *er>(_ 7 c ) ] ( B);
int nums = {5, 3, 6, 8, 4, 2};
for(int n! 4 j ?um: nums){
//二分搜索树的添加操作
bst.add(num);
}

//前序遍历 -- 递归算法
bst.preOrder();
Syst= J Yem.out.println();

System.out.println(bst);
}

我们现在根 g + !据上面的测试代码,来一起看下运行结果:

二分搜索树(Binary Search Tree)

我们根据运行结果画出的树结构,可以看出是满足二分搜索树的性质的,因此也证明了我们的添加方法和前序遍历是没有问题的。

2. 中0 _ * [序遍历

根据我们的前序遍历的递归实现,我们可以很容易地写出二分搜索树3 R o C C的中序遍历,具体实现如下:

//二分搜索树的中序遍历 -- 递归算法
pub0 H @ 1 l 9lic void$ O G inOrder(Z | /){
inOrder(root);
}

private void inOrder(Node node) {
if (node == null)
return;
//遍历左子树
inOrder(node.left);
//通过打印输出该节点的值的形式,访问该节点
System.out S P z k 3 ! O }路:当我们使用非递归来实现二分搜索树的前序遍历时z + 6 3 c T j d,我们可以借助栈这种数据结构,由于栈是后进先出的,我们需要先将当前节点的右孩子压入栈E o M V中,再将$ # o 7 f 9 P R当前节点的左孩子压入栈中,当我们的栈不为空时,我们就循环执行出栈操作,并且将出栈的元素作为当前节点。当然,如果你还不了解栈这种数据结构,你可以看我的上篇文章:常见的线性结构进行学习。非递归前序遍历@ o ~ q ( , E具体代码实现如下:

//前序遍历 -- 非递归实现
public List&lP , 7 y ` Qt;E> preOrderTraversal(Z . ^ ) Q 0 ^ l nTreeNode root) {z U s } = ? Q
//该集合用z S p $ F |来保存通过前序遍历后的元素
List<E> res = new ArrayLi? 8 g |st<E>();
if (root == null)
return res;
//借助栈这种数据结构进行实现
Stack<TreeNode<E>> stack = new Stack&@ / c o o ] s 7 nlt;Tre[ } h 9 w v Z teNode<E>>();
//由于前序遍历的一个元素一定是根节点% r r,所以我们可以先将根节点压入栈中
stack.push(root);
//判断栈是否为空
while ( !stack.isEmpty() ){
//将出栈的节点保存起来
TreeNU z l $ode<E> curr = stack.pop();
//将出栈节点的值添加到集合中
res.add(curr.val);
//若当前节点的右孩子不为空,就将该节点的右孩子压入栈中
if (curr.right != null)
stack.pusw t g h ; [ oh(curr.right);
/- 0 # g B/P 9 T L B o &若当前节点的左孩子不为空,就将该节点的左孩子压入栈中
if (curr.left != null)
stack.push(curr.left);
}
return res;
}

2w $ x x d | d. 中序遍历

二分搜索树中序遍历的非递归实现,这里还A N % E是通过借助栈来实现的,理解起来要比前序遍历的非递归实现复杂一些,希望大家可以认真思考,仔细体会,具体代码实现如下:

//中序遍历 -- 非递归实现
public List<E> inOrderTraversal(TreeNode root){
List<E> res = new ArrayList<E&ga , 2 i w C l D Dt;();

if (root == null)
return res;

TreeNode<E> cur = root;
Stack<T~ A y T c M GreeNode> stack = new Stack<TreeNode>();

while (cur != null || ! k xp();
res.@ ( Sadd(cur.val);
cur = cur.right;
}
}
retQ Q y Q O ( 5 c -urn res;
}

3. 后序遍历

我们通过前面的前序遍历和中序遍历的非递归算法的实现,都是采用的栈这X K s $ x j _ Y种数据结构进行实现,这也和我们程序调用的系统栈的工作原理相似,下面我们后序遍历v 9 n M ~ ; {的非递归算法仍然接站栈这种数据结构进行实现,这样可以帮助我们更加的熟悉栈k y Y T 7 i T这种数据结构的应$ = q 3 u o D b l用,具体代码实现如下:

//后序遍历 -- 非递归算法
public List<E> postOrderTraversal(T/ p I J P FreeNode root) {
List<E> res = new ArrayList<E>();

if (root == null)
returnT 7 ? R res;

//借助栈结构实现二分搜索树的非递归后序遍历
Stack<TreeNode> stack = new Stack<TreeNode>();
Ty Z AreeNode<E> curr = root;
TreeNode<E> pre = null;

while (curr != null || !stack.0 t R $ N l | 9 ~isEmpty()) {
if (curr != null){
stack.push(curr);
curr = curr.left;
}else {
curr = stack.pop();
if (cl ~ = ; o w m b r分搜索树的层序遍历如何实现呢,我们前面讲过队列这种数据s / d - = r H结构是先进先出的,我们可以B q 6 p将二分搜索树中的每层节点顺序放进队列中,然后再进行出, Y i $ d # V队操作就可以了,若你不清楚队列,你可以看我的上篇文章常见的线性结构进N k A X 1 A - U行学习,现在就让我们来看是如何使用队列实现二分搜索树的层序遍历吧,具体代码实现如下:

//层序遍历
public void levelOrder(){
if (root == null)
return;
Queue<Node> queue = new LinkedList X b 1 X<Node&g3 p m [ b 1 :\");
//递归调用获取二分搜索$ { 1中最大值所在的节点的方法
Node maxNode = maxNum(root);
return maxNode.e;
}

//以nodb ? e为根节点,获取二分搜索中最大值所在的节点
private& ~ & ^ b A t Node maxNum(Node node) {
//递归终止条件---如果当前节点的右孩子为空,则返回当前w ^ l G ; j B S节点
if (node.right == null)
returB j v % + 0 6 In node;
//递归从当前节点的右子树获取最大值所* 7 d p在的节点
return maxNum(node.right);
}


//寻找二分搜索数中的最小元素 -- 递归法
public E minNum(){
if (sd ; # HizV t # , N he == 0) // 也可以写为 root==null
throw new IllegalArgumentException(\"BST is empty\o . n 5 w");
Non w 1 Fde minNode = minNum(root);
return minNoQ j i 2de.e;
}

//以node为根节点搜索最小元素所在的节点
private Node minNum(Node node) {
if (node.left == null)
return node;
//递归从当前节点的左子树获取最小值所在的节点
return minNum(nod~ D d v C ~ ) Ae.left);
}

现在我们已经找出最小元素和最大元素所在的节点了,我们现在就可以是实现删除最小元素和最大元素所在的节点操作了,代码如下:

//从二分搜索树中删除最小元素所在的节点,并返回最小元素的值
public E removeMinN# _ T L - 9 Num(){
//获$ M d : A $ n取最小值
E e = minNum();

root = removeMinNum(roo| x 1 , p m Lt);

retur$ ! )n e;
}

//以no- E ^ ade@ ] ( #为根节点删除最小元素所在的节点
private Node removeMinNum(Node node) {
if (node.left == null) {
Node rightNode = node.right;
nC B mode.right = null;
size --;
return rightNode;2 } M i m
}

node.left = removeMinNum(node[ 8 8 i K o 0 E.left);
return node;
}

//删除二分搜索数中最大元素所在的节点,并返回该值
pubs f T _ - ilic E removeMaxNum(){
//获L : &取最大值
E e = maxNum();

root = removeMaxNum(root);

return e;
}

//删除以node为根节点的二分搜索中最大元素所在的节点
private Node removeMaxNum(Node node) {
if (node.right == null) {O A N @ Y z
Node leftNode = node.left;
node.lefs / c l - _ xt = null;
size --;
return leftNodeO Z r;
}

no2 V = S Qde.right = removeMaxNum(node.right);
return node;
}

在我们实现删除二分搜索树中的最大元素所在的节点和删除最小元素所在的节点操作后,我们可以写一个简单的测试用例,来验证下我a , I们的删除最大值和删除最小值操作是否正确:

public static void main(String args) {

BST<IntO t )eger> bst = new BST<Integer>();
Random r; 9 a K 1 B h xandomO v # 4 d ! _ , = new Random();

into 9 S n = 1000;

// 测试删除最小元素所在的节点
for(int i = 0 ; i < n ; i ++)
bc Y x f l ~st.ad4 D 0 ` p = ; { Bd(r. l 0 D I U ` h *andom.nextInt(10000));

List<Integer> nums = new ArrayList<Integer>();
while(!bst.isEmpty()){
//从二分搜索树中删8 h T * Y m :除最小元素所在的节点,并拿到该最小元素
Integer minNum = bst.remo) o @veMinNum();
//向我们的集合中添加该最小元素
nums.add(minNum);
}

//我们的nums集合中存储的是二分搜索树中所有节点的值按从小到大顺序排序后的元素
System.out.printC h 5ln(nums);
for(int i = 1 ; i < nums.size() ; i ++)
//如果该集合中前一个元素的值大于后一个元素的值,则不满足从小到大的排序规则,则抛出错异常` K 3 o
if(nums.get(i - 1) > nums.get(i4 z = m c))
throP ] k J !w n$ o E d U v . zew Ille@ + ~ M ( u | tgalArgumentException(\"Error!\");
System.out.println(\"removeMin test completed.\");


/x G ~ 4 D a O/ 测试删除最大元素所在的节点
for(int i = 0 ; i < n ; i ++)
bst.add(random.nextInt(10000));

nums = new ArrayList<Integer>()U 6 S 3 - ~;
while(!bst.isEmpty()){
//从二分搜索树中删除最大元素所在的Q z E } I f E S {e node, E e) {B / I H * - 6
if (node =, Z l + Z X d M= null)
return null;
if (e.compl L mareTo(node.e) <0 ){
node.left = removeElement(node.left, e);
return node;
}else if (e.compareTo(node.e) >0 ){
node.right = removeElement(node.right,e);
return node;
}else { //e.compareTo(node.e) == 0

//待删除节点左子树为空的情况
if (node.left ==: p : w d null){
Nod8 9 p ; H 2 te rightNode = node.right;
node.right = null;
size --;
return rightNode;
}

//待删除节点的右子树为空的情况
ic - ? M ~ - (f (node.right == null){
Node leftNode = node.left;
node.left = nuK = / R Y pll;
size -- ;t D 4 R v ( o
return leftNode;
}

//待删除节点的左右子树都不为空的情况
// 待删除节点的后继:找到比待删除节点大J # 6 4 d O O的最小节点, 即待删除节点右子树的最小节点J ? U p P F v i ^
// 用这个节点顶替待删除节点的位置
Node successor = min S n & DNum(node.right);$ 8 p % 5 : H 5 &
successor.right = removeMinNum(node.right);
successor.left = node.left;
node.r: U x 5 / k Kight = node.left = null;
return sl H | l q G !uccessor;
}
}

本} U _ A文实现的+ M *是一个不包含重复元素的二分搜索树,若你需要你的二分搜索树包含重复元素,在进行添加操作时只需要定义左子树小于等于m , m Y - r节_ : * l点;或者右子y ? o S ? a m & q树大于等于节点。二分搜索树的学习就到这里了,希% B ` H望本文能让你对二分搜索树有更深的理解。

测试宁都企业工作文章推进智能芯片智能化