二叉搜索树(Binary Search Tree)

403 阅读8分钟

二叉树

二叉树(Binary tree)是每个节点最多只有两个分支(即不存在分支度大于2的节点)的树结构。通常分支被称作“左子树”或“右子树”。二叉树的分支具有左右次序,不能随意颠倒。

二叉树的存储方式是以节点和节点间以链的方式连接在一起的。可以看作是链表结构的升级版,因为链表是只有一个指针指向下一个节点,而二叉树是可以有两个向下的指针的。其实可以把链表理解为二叉树的一个极端情况(二叉树的每一层都只存在左子树或只存在右子树)。

二叉搜索树

定义

二叉搜索树(Binary Search Tree),也称为二叉查找树有序二叉树(ordered binary tree)或 排序二叉树(sorted binary tree),是指一棵空树或者具有下列性质的二叉树:

  1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值。
  2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值。
  3. 任意节点的左、右子树也分别为二叉查找树。

二叉排序树的查找时间复杂度在O(nlogn)O(n) 之间。

例如:

第一张样例图.png

属性及方法

二叉树节点实例的属性及方法

因为是节点和指针组成的树结构,所以对该树的任何操作,本质就是对树中的节点和节点与节点间关系的操作,可以为节点定义以下属性和方法。

节点值(node.value)

左子节点(node.left)

右子节点(node.right)

父节点(node.parent)

树高(node.height)

左子树高(node.leftHeight)

右子树高(node.rightHeight)

设置左子节点(node.setLeft(node))

设置右子节点(node.setRight(node))

设置节点值(node.setValue(value))

删除子节点(node.removeChild(value))

替换子节点(node.replaceChild(nodeToReplace, replacementNode))

复制节点(node.copyNode(node))

节点有序遍历,以中序遍历为例(node.traverseInOrder())

搜索二叉树节点实例的特有实例方法

因为搜索二叉树是满足某些条件的二叉树,所以在继承上面二叉树的这些基础节点的属性和方法外,还依据自身的特点定义一些操作方法。

插入(node.insert(value))

删除(node.remove(value))

查找(node.find(value))

查找最小值(node.findMin())

操作

搜索二叉树主体思想就是小的节点在左,大的在右。以当前节点为依据,要操作的值比当前节点值大(小),就在当前节点的右(左)分支进行操作,如果右(左)节点也有分支,就进行递归操作直至获取到目标结果。

插入(insert)

  1. 判断当前节点是否有值,如果没有值,设置当前节点的值(node.setValue(value))为传入的参数。

  2. 如果当前节点有值,让新增的值与当前节点的值进行比较:

    如果新增值大于(小于)当前节点值,且当前节点没有右(左)子节点,则创建一个二叉搜索树节点实例作为当前节点的右(左)子节点。如果当前节点存在右(左)子节点,调用右(左)子节点的insert方法,传入比较的参数,递归与下一分支树进行比较,最终确认新增节点位置。

例如:

向空二叉搜索树中依次插入2,1,3,7,4,6

屏幕快照 2021-12-21 下午4.46.26.png

第一步: 插入第一个节点2。

屏幕快照 2021-12-21 下午4.49.06.png

第二步: 需要新插入1。判断得新值1比2小,插入到节点2的左子节点位置。

屏幕快照 2021-12-21 下午4.54.30.png

第三步: 需要新插入3。判断得新值3比2大,插入到节点2的右子节点位置。

屏幕快照 2021-12-21 下午4.57.29.png

第四步: 需要新插入7。首先判断当前节点2比7小,所以再判断2的右子节点值和7的大小,3比7小,且3没有右子节点,就把7设置为3的右子节点。

屏幕快照 2021-12-21 下午5.05.39.png

第五步: 需要新插入4。首先判断当前节点2比4小,所以再判断2的右子节点值和4的大小,3比4小,然后再判断3的右子节点和4的大小,7大于4,且7没有左子节点,就把4设置为7的左子节点。

屏幕快照 2021-12-21 下午5.11.56.png

第六步: 需要新插入6。首先判断当前节点2比6小,所以再判断2的右子节点值和6的大小,3比6小,然后再判断3的右子节点和6的大小,7大于6,然后判断7的左子节点4与6大小,6大于4,且4没有右子节点,就把6设置为4的右子节点。

以上所有插入操作完成。

其实以上这个例子,完成插入后可以看出,这个树向右偏的很严重,对查找的性能是右影响下的,所以出现了平衡二叉树以优化二叉搜索树。

查找(find)

  1. 将当前节点值与入参查找值进行比较,如果相等,返回当前节点。

  2. 如果不相等,则比较当前节点的值与入参查找值的大小。

    如果当前节点值大(小)于查找值,且当前节点存在右(左)子节点,则调用右(左)子节点的find方法,传入查找值,进行新一轮查找,以此递归下去,如果找到目标节点就会返回,找不到返回null

例如:

屏幕快照 2021-12-21 下午5.31.08.png

在以上树中,查找是否存在值为5的节点。

屏幕快照 2021-12-21 下午5.28.22.png

第一步: 判断当前节点值是否和5相等,20不等于5且大于5,所以把当前节点20的左子节点递归查找。

屏幕快照 2021-12-21 下午5.40.33.png

第二步: 判断当前节点值是否和5相等,这里相等,直接返回。

查找最小值(findMin)

  1. 判断当前节点有无左子节点,如果没有,返回当前节点。
  2. 如果当前节点存在左子节点,调用左子节点的findMin方法。

删除(remove)

  1. 根据入参查找(find)获取到待删除的节点。如果查不到抛出异常。

  2. 待删除的节点,通常有三种情况:

    A. 待删除的节点是叶子节点,也就是说其没有左子节点也没有右子节点。

    i:如果待删除的节点是根节点,也就是没有父节点(parent=== null),直接抹去当前节点的值即可(node.setValue(undefined)

    ii:如果待删除的节点存在父节点,直接把其父节点指向该删除节点的指针移除掉即可(nodeToRemove.parent.removeChild(nodeToRemove))。

    B. 待删除的节点只有一个子节点。

    i:如果是待删除节点是根节点,把子节点的值、左节点和右节点复制给根节点即可(nodeToRemove.parent.copyNode(childNode, nodeToRemove))。

    ii:如果待删除节点存在父节点,就把父节点指向待删除节点的指针指向待删除节点的子节点即可( nodeToRemove.parent.replaceNode(nodeToRemove, childNode))。

    C. 待删除节点同时存在左子节点和右子节点。

    i:如果待删除节点的右子节点只有右子节点(nodeToRemove.right === nodeToRemove.right.findMin()),就把待删除节点的右子节点值替换删除节点的值,待删除节点的右子节点指针向其右节点的右节点即可。

    ii:如果待删除节点的右节点存在左子节点,递归找到待删除节点右子分支里最左边的值,也就右子分支里最小的值,替换为待删除节点的值。

删除叶子节点示例

屏幕快照 2021-12-21 下午6.01.19.png

删除该二叉搜索树中值为5的节点。

屏幕快照 2021-12-21 下午6.04.20.png

第一步: 找到值为5的节点,把指向该删除节点的其父节点的指针删除即可。

删除只有一个子节点的示例

例一:

屏幕快照 2021-12-22 上午11.59.09.png

删除二叉搜索树中值为20的节点。

屏幕快照 2021-12-22 下午12.01.25.png

第一步: 找到值为20的节点。

屏幕快照 2021-12-22 下午12.02.51.png

第二步: 把指向删除节点的其父节点的指针指向待删除节点的子节点即可。

例二:

屏幕快照 2021-12-22 下午12.11.34.png

删除二叉搜索树中值为10的节点。

屏幕快照 2021-12-22 下午12.12.04.png

第一步: 找到值为10的节点,这里是根节点。

屏幕快照 2021-12-22 下午2.16.15.png

第二步: 把根节点的值,左节点,右节点都替换为其子节点即可。

删除存在两个子节点的节点

示例一:

屏幕快照 2021-12-22 下午2.24.27.png

删除二叉搜索树中值为20的节点。

屏幕快照 2021-12-22 下午2.26.26.png

第一步: 找到值为20的节点。

屏幕快照 2021-12-22 下午2.29.58.png

第二步: 待删除的节点左右子分支都存在,找右子子分支里面最小的值的节点。

屏幕快照 2021-12-22 下午2.34.26.png

第三步: 用最小节点的值替换要删除的节点的值,然后删掉最小节点。

示例二:

屏幕快照 2021-12-22 下午2.36.47.png

删除二叉搜索树中值为25的节点。

屏幕快照 2021-12-22 下午2.37.19.png

第一步: 找到值为25的节点。

屏幕快照 2021-12-22 下午2.40.24.png

第二步: 待删除节点存在左右子分支,通过调用node.right.finMind()方法得知,右分支的最小节点就等于待删除节点的右节点。

屏幕快照 2021-12-22 下午2.41.23.png

第三步: 把右子节点替换待删除节点即可。

节点有序遍历(traverseInOrder)

搜索二叉树,是左小于中小于右,可以用中序遍历按顺序返回树的节点数组。

例如:

屏幕快照 2021-12-22 下午2.49.49.png

按顺序遍历输出该二叉搜索树。

屏幕快照 2021-12-22 下午2.52.24.png

第一步: 从根节点10开始查找,判断当前节点有无左子节点,这里有左子节点5,然后再递归判断5是否左子节点,没有就把5存入数组第一位。

屏幕快照 2021-12-22 下午2.58.11.png

第二步: 值为5的节点没有右子节点了,该节点已遍历完成,再向上一层对其父节点操作。

屏幕快照 2021-12-22 下午3.00.38.png

第三步: 值为10的节点存入数组第二位。

屏幕快照 2021-12-22 下午3.03.46.png

第四步: 值为10的节点存在右子节点,再将其右子节点按相同的遍历方法遍历,找到最小节点。

屏幕快照 2021-12-22 下午3.04.58.png

第五步: 把最小节点放入数组中。

屏幕快照 2021-12-22 下午3.06.21.png

第五步: 把最小节点父节点放入数组中。

屏幕快照 2021-12-22 下午3.07.40.png

第六步: 把右子节点放入数组中,有序遍历结束。