原文:https://loubobooo.com/2018/11/04/%E5%88%9D%E5%AD%A6%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84-%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2%E6%A0%91/
前言
之前我们一直专注在线性数据结构上,在这一章要开始学习计算机领域应用及其广泛的–树结构。
树的概括
下图的数据存储方式便是一种树结构。
和线性数据结构不同的是,线性的数据结构是把所有的数据排成一排,而树结构更像是自然界中树的枝杈,不断延伸,不断扩展,其本身也是一种天然的组织结构,类似于电脑中文件夹这种目录结构。
将数据使用树结构存储后,出奇的高效,其中包括但不限于二分搜索树,平衡二叉树,AVL,红黑树,堆,并查集,线段树,Trie(字典树,前缀树)
二分搜索树的基础
在了解二分搜索树之前,我们首先明确二叉树的概念,首先它和链表一样,同样属于一种动态数据结构。
可以看粗,二叉树中每个元素存在于节点内,和链表不同的是,除了要存放元素e,还要有指向其他节点的left和right两个变量(引用)。
- 对于树结构来说,二叉树也是最常用的一种数据结构,同时二分搜索树满足二叉树的性质(从本质上讲,二分搜索树也是一棵二叉树)
- 对于一棵二叉树来说,它只有一个根节点,并且这个根节点是唯一的(如下图中28便是这棵二叉树的根节点)
- 对于每一个节点来说,它最多只能有两个子节点,指向左和右的两个节点(也可以称之为左孩子和右孩子)
- 二叉树中,一个孩子都没有的节点,通常称之为叶子节点
在这里,不是所有二叉树都像如下图示的二叉树这样规整(每个节点都有两个孩子,叶子节点也在最底层)
叶子节点:这个节点没有任何孩子节点,这样的节点称之为叶子节点
- 二叉树有一个非常重要的性质,是具有天然的递归结构(对于每一个节点,它的左孩子同时也是一棵二分搜索树的根节点)。
如下图示,这是一棵满的二叉树。即对于每一个节点来说,除了叶子节点外都有两个孩子节点。
如下图所示,只有左孩子,或者只有右孩子,或者左右孩子都为空,甚至退化成链表等等,这些都满足二叉树的定义。
下面可以来看下什么是二分搜索树。二分搜索树除了满足二叉树的定义(只有一个根节点,有左右孩子节点或者左右孩子节点为空……),还有自己独特的性质,即每个节点的值都要大于其左子树下所有节点的值,同时每个节点的值小于其右子树下所有节点的值。
如下图示顾名思义,根节点28大于左子树中最大的值22,并且根节点28小于右子树最小的值29
注:二分搜索树当然有可能不是一棵满的二叉树
为了要能达到这样的性质,就必须让我们存储的元素具有可比较性,那么我们用二分搜索树来存储数据的话,然后来查找一个数据53就会非常简单。由于41是根节点,所以根据二分搜索树的性质,53这个节点就一定在41的右子树。这样一来,41的左子树所存储的这些数据就会被忽略,这就大大地加快了查询速度。
二分搜索树中添加元素
首先在最开始的时候,在二分搜索树里一个节点都没有
- 现在添加一个新的元素41,显然这个节点会成为根节点
- 再来一个新元素28,从根节点出发,判断28比根节点41小,根据二分搜索树的定义、因此添加到41的左子树中
- 利用二分搜索树的定义,每次添加一个新的元素,从根节点开始,如果小于根节点就插到根节点的左子树,反之插入到根节点的右子树
- 这个过程以此类推……
注:我们此时的二分搜索树不包含重复元素
如果想包含重复元素的话,只需定义:左子树小于等于节点;或者右子树大于等于节点
过程如图示:
- 代码如下:
private class Node{ public E e; public Node left, right; public Node(E e){ this.e = e; left = null; right = null; } } private Node add(Node node, E e){ if(node == null){ // 将null的节点连接起来 size++; return new Node(e); } // 这里用递归,不断调用左孩子节点,然后比较插入的元素和节点e if(e.compareTo(node.e) < 0){ node.left = add(node.left, e); }else if(e.compareTo(node.e) > 0){ //e.compareTo(node.e) > 0 node.right = add(node.right, e); } // 对相等的情况不做判断了 return node; }
二分搜索树的查询
对于查询元素操作来说,只需要看一下节点node所存的元素就好了,不必牵扯添加一个元素之后如何挂接到整个二分搜索树中,因此是相对简单的。
- 代码如下
// 看二分搜索树中是否包含元素e public boolean contains(E e){ return contains(root, e); } // 看以node为根的二分搜索树中是否包含元素e,递归算法 private boolean contains(Node node, E e){ if(node == null){ return false; } if(e.compareTo(node.e) == 0){ return true; }else if(e.compareTo(node.e) < 0){ return contains(node.left, e); }else{ // e.compareTo(node.e) > 0 return contains(node.right, e); } }
二分搜索树的前中后序遍历
前序遍历
- 对于一个数据结构的遍历,本质很简单,就是把这个数据结构中所存储的所有元素都访问一遍。对应到二分搜索树便是访问一遍所有节点。
- 对于线性数据(无论数组还是链表),从头到尾做一层循环就解决了,但是对于树结构来说却不是。要访问整个二叉树所有节点,需要考虑左子树所有节点,还要考虑右子树所有节点。
在访问完根节点,先访问了左子树,再访问了右子树,这就完成了二叉树的遍历操作,而这种遍历操作也称之为前序遍历。
- 用代码来实现:
// 二分搜索树的前序遍历 public void preOrder(){ preOrder(root); } // 前序遍历以node为根的二分搜索树,递归算法 private void preOrder(Node node){ if(node == null){ return; } preOrder(node.left); preOrder(node.right); }
中序遍历
通常而言,对于二分搜索来说。前序遍历是最自然的一种遍历方式,同时呢也是最常用的一种遍历方式。
如果我们改变节点的访问顺序,使之 先访问该节点的左子树,再访问该节点,最后访问该节点的右子树,这样的遍历操作称之为中序遍历。
- 用代码实现
//中序遍历以node为根的二分搜索树,递归算法 public void inOrder(){ inOrder(root); } private void inOrder(Node node){ if(node == null){ return; } inOrder(node.left); inOrder(node.right); }
后序遍历
同理,先访问该节点的左子树,再访问该节点的右子树,最后访问这个节点,这样的遍历操作便是后序遍历。
- 用代码实现
// 二分搜索树的后序遍历 public void postOrder(){ postOrder(root); } // 后序遍历以node为根的二分搜索树,递归算法 private void postOrder(Node node){ if(node == null){ return; } postOrder(node.left); postOrder(node.right); }
二分搜索树的层序(广度优先)遍历
上述二分搜索树的前中后序遍历,本质上都是深度优先的遍历。于此相对应的是广度优先遍历(即层序遍历)。
广度优先遍历
对于二分搜索树来说,每一个节点对应右一个深度值,通常会把根节点叫做深度为0相应的节点。层序遍历就是,先遍历第0层的节点(28),再遍历第1层的节点(16,30),最后遍历第2层的节点(13,22,29,42)。
如下图示:
- 用代码实现
// 二分搜索树的层序遍历 public void levelOrer(){ Queue<Node> queue = new LinkedList<>(); queue.add(root); while(!queue.isEmpty()){ // 当前遍历的节点 等于 队列中的元素出队之后的那个元素 Node cur = queue.remove(); System.out.println(cur.e); if(cur.left != null){ queue.add(cur.left); } if(cur.right != null){ queue.add(cur.right); } } }
意义
- 更快的找到问题的解
- 常用于算法设计中-最短路径
- 图中的深度优先遍历和广度优先遍历
二分搜索树删除节点
删除最大值和最小值
对于二分搜索树来说,删除一个节点相对是比较复杂的操作。因此分解删除操作,从最简单的,删除二分搜索树的最小值和最大值开始。
- 首先,要想删除二分搜索树的最小值和最大值,就需要先找到二分搜索树的最小值和最大值
- 对于二分搜索树来说,其一个节点的左子树中所有节点都小于该节点
- 因此删除最小值一定是从根节点开始,不停的向左走,直到向左走再也走不动为止,那个值一定是最小值
- 删除最大值同理
用代码实现:
|
删除任意节点
在二分搜索树中删除任意节点,会出现以下3种情况:
- 删除只有左孩子的节点
要删除的节点元素58这种情况,与删除最大值的逻辑相同,并在删除完成之后,让左孩子所在的二叉树挂接到原来父亲节点
-
用代码实现:
// 待删除节点右子树为空的情况 if(node.right == null){ Node leftNode = node.left; node.left = null; size--; return leftNode; }
-
删除只有右孩子的节点
要删除的节点元素58这种情况,与删除最大值的逻辑相同,并在删除完成之后,让左孩子所在的二叉树挂接到原来父亲节点
-
用代码实现:
// 待删除节点左子树为空的情况 if(node.left == null){ Node rightNode = node.right; node.right = null; size--; return rightNode; }
-
删除左右都有孩子的节点d
- 找到d节点右子树中最小的值,即s=min(d->right)
- 删除s节点,即delMin(d->right)
- 连接上之前d的左子树,即s->right。连接上之前d的右子树,即s->left
- 删除d,使s成为新的子树的根
-
用代码实现:
// 待删除节点左右子树均不为空的情况 // 找到比待删除节点大的最小节点,即待删除节点右子树的最小节点 // 用这个节点顶替待删除节点的位置,successor --> 后继 Node successor = minimum(node.right); successor.right = removeMin(node.right); successor.left = node.left; node.left = node.right = null; return successor;
-
完整代码实现
// 从二分搜索树中删除元素为e的节点 public void remove(E e){ root = remove(root, e); } // 删除以node为根的二分搜索树中值为e的节点,递归算法 // 返回删除节点后新的二分搜索树的根 private Node remove(Node node, E e){ if(node == null){ return null; } if(e.compareTo(node.e) < 0){ node.left = remove(node.left, e); return node; }else if(e.compareTo(node.e) > 0){ node.right = remove(node.right, e); return node; }else{ // e == node.e // 待删除节点左子树为空的情况 if(node.left == null){ Node rightNode = node.right; node.right = null; size--; return rightNode; } // 待删除节点右子树为空的情况 if(node.right == null){ Node leftNode = node.left; node.left = null; size--; return leftNode; } // 待删除节点左右子树均不为空的情况 // 找到比待删除节点大的最小节点,即待删除节点右子树的最小节点 // 用这个节点顶替待删除节点的位置,successor --> 后继 Node successor = minimum(node.right); successor.right = removeMin(node.right); successor.left = node.left; node.left = node.right = null; return successor; } }