Detailed explanation of JavaScript binary search tree construction operation

Table of contents
  • foreword
  • What is a binary search tree
  • Build a binary search tree
  • Operations on Binary Search Trees
    • Insert data into binary search tree
    • Find data in a binary search tree
    • Delete a node in a binary search tree
      • predecessor successor node
      • Three cases of deleting a node
      • Implementation code
  • full code
  • Summarize

foreword

Earlier we introduced the data structure of the binary tree and the traversal algorithm of the binary tree. In this article, we will learn about a special binary tree—BST Binary Search Tree (BST Binary Search Tree), also called binary sorting tree and binary search tree.

What is a binary search tree

A binary search tree is first of all a binary tree, and it also meets the following characteristics:

  • For any non-empty node, the value on its left subtree must be less than the current value ;
  • For any non-empty node, the value on its right subtree must be greater than the current value ;
  • Any subtree satisfies the above conditions;

As shown below:

The above picture is a binary search tree. Let’s take the root node as an example. The value of the root node is 71, and the values ​​of its left subtree are 22, 35, 46, 53 and 66 respectively, which satisfy the left The subtree is smaller than the current value; the values ​​of its right subtree are 78, 87 and 98 respectively, these values ​​satisfy the requirement that the right subtree is greater than the current value; and so on, so the above figure is a binary search tree.

According to the characteristics of the binary search tree, we can also get the following conclusions:

  • The left subtree and right subtree of any node of the binary search tree are a binary search tree;
  • The smallest node of the binary search tree is the leaf node in the lower left corner of the entire tree ;
  • The largest node of the binary search tree is the leaf node in the lower right corner of the whole tree ;

Build a binary search tree

We now use JavaScript to build a binary search tree. We must know that a binary search tree is also composed of nodes. Here we create classa node class,

The sample code is as follows:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

class BinarySearchTree {

  constructor() {

    // 初始化根节点

    this.root = null

  }

  // 创建一个节点

  Node(val) {

    return {

      left: null, // 左子树

      right: null, // 右子树

      parent: null, // 父节点

      val,

    }

  }

}

Here a node consists of four parts, which are a pointer to the left subtree, a pointer to the right subtree, a pointer to the parent node, and the current value.

Operations on Binary Search Trees

We have already introduced the traversal operation of the binary tree in the previous article, and we will not repeat it here. Here we mainly introduce the following operations:

  • insert operation
  • find operation
  • delete operation

Insert data into binary search tree

The idea of ​​inserting data into a binary search tree is as follows:

  • Determine rootwhether it is empty, if it is empty, create root;
  • If rootit is not empty, you need to judge whether the inserted node is larger or smaller valthan the root node ;val
  • If it is smaller than the root node, it means it is a node of the left subtree;
  • If it is larger than the root node, it means it is the node of the right subtree;
  • The above two steps are repeated until a point is found. If the point is smaller than the value we want to insert and there is no right subtree, use this point as its right leaf node; if the point is greater than the value we want to insert and does not exist For the right subtree, use this point as its left leaf node.

The sample code is as follows:

?

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

// 创建要给插入节点的方法

insertNode(val) {

  const that = this

  // 允许接受一个数组,批量插入

  if (Object.prototype.toString.call(val) === '[object Array]') {

    val.forEach(function (v) {

      that.insertNode(v)

    })

    return

  }

  if (typeof val !== 'number') throw Error('插入的值不是一个数字')

  const newNode = this.Node(val)

  if (this.root) {

    // 根节点非空

    this.#insertNode(this.root, newNode)

  } else {

    // 根节点是空的,直接创建

    this.root = newNode

  }

}

// 私有方法,插入节点

#insertNode(root, newNode) {

  if (newNode.val < root.val) {

    // 新节点比根节点小,左子树

    if (root.left === null) {

      // 如果左子树上没有内容,则直接插入,如果有,寻找下一个插入位置

      root.left = newNode

      root.left.parent = root

    } else {

      this.#insertNode(root.left, newNode)

    }

  } else {

    // 新节点比根节点大,右子树

    if (root.right === null) {

      // 如果右子树上没有内容,则直接插入,如果有,寻找下一个插入位置

      root.right = newNode

      root.right.parent = root

    } else {

      this.#insertNode(root.right, newNode)

    }

  }

}

A method is defined in the class insertNode, which accepts a value or an array of value types and inserts it into the binary search tree; for the insertion method, we define a private #insertNodemethod for node insertion.

In order to see the effect, we define a static method here for in-order traversal (because the order of in-order traversal is left-root-right, in-order sorting is used in the binary search tree, and the final results are sorted from small to large ) this tree, and returns an array,

The sample code is as follows:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

// 中序遍历这个树

static inorder(root) {

  if (!root) return

  const result = []

  const stack = []

  // 定义一个指针

  let p = root

  // 如果栈中有数据或者p不是null,则继续遍历

  while (stack.length || p) {

    // 如果p存在则一致将p入栈并移动指针

    while (p) {

      // 将 p 入栈,并以移动指针

      stack.push(p)

      p = p.left

    }

    const node = stack.pop()

    result.push(node.val)

    p = node.right

  }

  return result

}

The test code is as follows:

const tree = new BinarySearchTree()
tree.insertNode([71, 35, 87, 22, 53, 46, 66, 78, 98])
const arr = BinarySearchTree.inorder(tree.root)
console.log(arr) // [ 22, 35, 46, 53, 66,71, 78, 87, 98 ]

The final tree structure is as follows:

Find data in a binary search tree

现在我们封装一个find方法,用于查找二叉搜索树中的某个数据,假如我们查找66这个数据,利用上面那个树,

其查找思路如下图所示:

递归方式实现如下:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

/**

 * 根据 val 查找节点

 * @param {number} val 需要查找的数值

 * @returns 如果找到返回当前节点的引用,如果未找到返回 undefined

 */

find(val) {

  if (typeof val !== 'number') throw Error('插入的值不是一个数字')

  function r(node, val) {

    // console.log(node)

    if (!node) return

    if (node.val < val) {

      return r(node.right, val)

    } else if (node.val > val) {

      return r(node.left, val)

    } else {

      return node

    }

  }

  return r(this.root, val)

}

迭代方式实现如下:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

/**

 * 根据 val 查找节点

 * @param {number} val 需要查找的数值

 * @returns 如果找到返回当前节点的引用,如果未找到返回 undefined

 */

find(val) {

  if (typeof val !== 'number') throw Error('插入的值不是一个数字')

  let node = this.root

  while (node) {

    if (node.val < val) {

      // 进入右子树

      node = node.right

    } else if (node.val > val) {

      // 进入左子树

      node = node.left

    } else {

      return node

    }

  }

  return

}

两者相对来说,使用迭代的方式更优一些。

删除二叉搜索树的某个节点

前驱后继节点

在开始删除二叉搜索树中的某个节点之前,我们先来了解一下什么是前驱和后继节点;

  • 前驱节点指的是使用中序遍历当前二叉搜索树时,当前节点的上一个节点就是前驱节点,换一种说法就是在二叉搜索树中,当前节点的左子树的最大值,就是该节点的前驱节点
  • 后继节点指的是使用中序遍历当前二叉搜索树时,当前节点的下一个节点就是后继节点,换一种说法就是在二叉搜索树中,当前节点的右子树的最小值,就是该节点的后继节点

如下图所示:

了解了什么是前驱和后继节点之后,现在我们来开始删除某个节点。

删除一个节点的三种情况

当删除的节点是叶子节点时,只需要将指向它的指针修改为null,即可,如下图所示:

当需要删除的节点存在一个子节点时 需要将要删除节点的子节点的parent指针指向要删除节点的父节点,然后将当前要删除节点的父节点指向子节点即可,

如下图所示:

当需要删除的节点存在一个子节点时, 删除步骤如下:

  • 找到当前节点的前驱或者后继节点,这里选择后继;
  • Then assign the value of the successor node to the current node;
  • Delete the successor node.

As shown below:

Now that we have analyzed these situations, let's implement them through code.

Implementation code

The implementation code is as follows:

?

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

remove(val) {

  // 1. 删除节点

  const cur = this.find(val)

  if (!val) return false // 未找到需要删除的节点

  if (!cur.left && !cur.right) {

    // 1. 当前节点是叶子节点的情况

    this.#removeLeaf(cur)

  } else if (cur.left && cur.right) {

    // 2. 当前节点存在两个子节点

    // 2.1 找到当前节点的后继节点

    const successorNode = this.#minNode(cur.right)

    // 2.2 将后继节点的值赋值给当前值

    cur.val = successorNode.val

    if (!successorNode.left && !successorNode.right) {

      // 2.3 后继节点是叶子节点,直接删除

      this.#removeLeaf(successorNode)

    } else {

      // 2.4 后继节点不是叶子节点

      // 2.4.1记录该节点的子节点,

      let child =

        successorNode.left !== null ? successorNode.left : successorNode.right

      // 2.4.2 记录该节点的父节点

      let parent = successorNode.parent

      // 2.4.3 如果当前父节点的left是后继结点,则把后继结点的父节点的left指向后继结点的子节点

      if (parent.left === successorNode) {

        parent.left = child

      } else {

        // 2.4.4 如果不是,则让父节点的right指向后继结点的子节点

        parent.right = child

      }

      // 2.4.5 修改子节点的parent指针

      child.parent = parent

    }

    // 2.3 删除后继节点

  } else {

    // 记录当前节点的是否是父节点的左子树

    const isLeft = cur.val < cur.parent.val

    // 3. 仅存在一个子节点

    if (cur.left) {

      // 3.1 当前节点存在左子树

      cur.parent[isLeft ? 'left' : 'right'] = cur.left

      cur.left.parent = cur.parent

    } else if (cur.right) {

      // 3.2 当前节点存在右子树

      cur.parent[isLeft ? 'left' : 'right'] = cur.right

      cur.right.parent = cur.parent

    }

  }

}

// 删除叶子节点

#removeLeaf(node) {

  if (!node) return

  const parent = node.parent

  if (node.val < parent.val) {

    // 当前要删除的叶子节点是左节点

    parent.left = null

  } else {

    // 当前要删除的叶子节点是右节点

    parent.right = null

  }

}

// 查找最小值

#minNode(node) {

  if (!node) return

  if (!node.left) return node

  let p = node.left

  while (p.left) {

    p = p.left

  }

  return p

}

full code

The complete code in this article is as follows:

?

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

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

class BinarySearchTree {

  constructor() {

    // 初始化根节点

    this.root = null

  }

  // 创建一个节点

  Node(val) {

    return {

      left: null, // 左子树

      right: null, // 右子树

      parent: null, // 父节点

      val,

    }

  }

  /**

   * 创建要给插入节点的方法

   * @param {number | array[number]} val

   * @returns

   */

  insertNode(val) {

    const that = this

    // 允许接受一个数组,批量插入

    if (Object.prototype.toString.call(val) === '[object Array]') {

      val.forEach(function (v) {

        that.insertNode(v)

      })

      return

    }

    if (typeof val !== 'number') throw Error('插入的值不是一个数字')

    const newNode = this.Node(val)

    if (this.root) {

      // 根节点非空

      this.#insertNode(this.root, newNode)

    } else {

      // 根节点是空的,直接创建

      this.root = newNode

    }

  }

  /**

   * 私有方法,插入节点

   * @param {Object{Node}} root

   * @param {Object{Node}} newNode

   */

  #insertNode(root, newNode) {

    if (newNode.val < root.val) {

      // 新节点比根节点小,左子树

      if (root.left === null) {

        // 如果左子树上没有内容,则直接插入,如果有,寻找下一个插入位置

        root.left = newNode

        root.left.parent = root

      } else {

        this.#insertNode(root.left, newNode)

      }

    } else {

      // 新节点比根节点大,右子树

      if (root.right === null) {

        root.right = newNode

        root.right.parent = root

      } else {

        this.#insertNode(root.right, newNode)

      }

    }

  }

  /**

   * 根据 val 查找节点

   * @param {number} val 需要查找的数值

   * @returns 如果找到返回当前节点的引用,如果未找到返回 undefined

   */

  find(val) {

    if (typeof val !== 'number') throw Error('插入的值不是一个数字')

    let node = this.root

    while (node) {

      if (node.val < val) {

        // 进入右子树

        node = node.right

      } else if (node.val > val) {

        // 进入左子树

        node = node.left

      } else {

        return node

      }

    }

    return

  }

  // /**

  //  * 根据 val 查找节点 递归版

  //  * @param {number} val 需要查找的数值

  //  * @returns 如果找到返回当前节点的引用,如果未找到返回 undefined

  //  */

  // find(val) {

  //   if (typeof val !== 'number') throw Error('插入的值不是一个数字')

  //   function r(node, val) {

  //     // console.log(node)

  //     if (!node) return

  //     if (node.val < val) {

  //       return r(node.right, val)

  //     } else if (node.val > val) {

  //       return r(node.left, val)

  //     } else {

  //       return node

  //     }

  //   }

  //   return r(this.root, val)

  // }

  remove(val) {

    // 1. 删除节点

    const cur = this.find(val)

    if (!val) return false // 未找到需要删除的节点

    if (!cur.left && !cur.right) {

      // 1. 当前节点是叶子节点的情况

      this.#removeLeaf(cur)

    } else if (cur.left && cur.right) {

      // 2. 当前节点存在两个子节点

      // 2.1 找到当前节点的后继节点

      const successorNode = this.#minNode(cur.right)

      // 2.2 将后继节点的值赋值给当前值

      cur.val = successorNode.val

      if (!successorNode.left && !successorNode.right) {

        // 2.3 后继节点是叶子节点,直接删除

        this.#removeLeaf(successorNode)

      } else {

        // 2.4 后继节点不是叶子节点

        // 2.4.1记录该节点的子节点,

        let child =

          successorNode.left !== null ? successorNode.left : successorNode.right

        // 2.4.2 记录该节点的父节点

        let parent = successorNode.parent

        // 2.4.3 如果当前父节点的left是后继结点,则把后继结点的父节点的left指向后继结点的子节点

        if (parent.left === successorNode) {

          parent.left = child

        } else {

          // 2.4.4 如果不是,则让父节点的right指向后继结点的子节点

          parent.right = child

        }

        // 2.4.5 修改子节点的parent指针

        child.parent = parent

      }

      // 2.3 删除后继节点

    } else {

      // 记录当前节点的是否是父节点的左子树

      const isLeft = cur.val < cur.parent.val

      // 3. 仅存在一个子节点

      if (cur.left) {

        // 3.1 当前节点存在左子树

        cur.parent[isLeft ? 'left' : 'right'] = cur.left

        cur.left.parent = cur.parent

      } else if (cur.right) {

        // 3.2 当前节点存在右子树

        cur.parent[isLeft ? 'left' : 'right'] = cur.right

        cur.right.parent = cur.parent

      }

    }

  }

  // 删除叶子节点

  #removeLeaf(node) {

    if (!node) return

    const parent = node.parent

    if (node.val < parent.val) {

      // 当前要删除的叶子节点是左节点

      parent.left = null

    } else {

      // 当前要删除的叶子节点是右节点

      parent.right = null

    }

  }

  // 查找最小值

  #minNode(node) {

    if (!node) return

    if (!node.left) return node

    let p = node.left

    while (p.left) {

      p = p.left

    }

    return p

  }

  // 中序遍历这个树

  static inorder(root) {

    if (!root) return

    const result = []

    const stack = []

    // 定义一个指针

    let p = root

    // 如果栈中有数据或者p不是null,则继续遍历

    while (stack.length || p) {

      // 如果p存在则一致将p入栈并移动指针

      while (p) {

        // 将 p 入栈,并以移动指针

        stack.push(p)

        p = p.left

      }

      const node = stack.pop()

      result.push(node.val)

      p = node.right

    }

    return result

  }

}

const tree = new BinarySearchTree()

tree.insertNode([71, 35, 84, 22, 53, 46, 66, 81, 83, 82, 88, 98])

console.log(BinarySearchTree.inorder(tree.root)) // [ 22, 35, 46, 53, 66, 71, 81, 82, 83, 84, 88, 98 ]

tree.remove(71

console.log(BinarySearchTree.inorder(tree.root)) // [ 22, 35, 46, 53, 66, 81, 82, 83, 84, 88, 98 ]

Guess you like

Origin blog.csdn.net/qq_15509251/article/details/131652868