Explicación detallada de la operación de construcción del árbol de búsqueda binario de JavaScript

Tabla de contenido
  • prefacio
  • ¿Qué es un árbol de búsqueda binaria?
  • Construir un árbol de búsqueda binaria
  • Operaciones en árboles de búsqueda binarios
    • Insertar datos en el árbol de búsqueda binaria
    • Buscar datos en un árbol de búsqueda binaria
    • Eliminar un nodo en un árbol de búsqueda binaria
      • nodo sucesor predecesor
      • Tres casos de eliminación de un nodo
      • Código de implementación
  • código completo
  • Resumir

prefacio

Anteriormente presentamos la estructura de datos del árbol binario y el algoritmo transversal del árbol binario. En este artículo, aprenderemos sobre un árbol binario especial: el árbol de búsqueda binaria BST (árbol de búsqueda binaria BST), también llamado árbol de clasificación binaria y árbol binario. árbol de búsqueda

¿Qué es un árbol de búsqueda binaria?

Un árbol de búsqueda binario es ante todo un árbol binario, y además cumple las siguientes características:

  • Para cualquier nodo que no esté vacío, el valor en su subárbol izquierdo debe ser menor que el valor actual ;
  • Para cualquier nodo que no esté vacío, el valor en su subárbol derecho debe ser mayor que el valor actual ;
  • Cualquier subárbol satisface las condiciones anteriores;

Como se muestra abajo:

La imagen de arriba es un árbol de búsqueda binaria. Tomemos el nodo raíz como ejemplo. El valor del nodo raíz es 71, y los valores de su subárbol izquierdo son 22, 35, 46, 53 y 66 respectivamente, que satisfacen el subárbol izquierdo es más pequeño que el valor actual; los valores de su subárbol derecho son 78, 87 y 98 respectivamente, estos valores satisfacen el requisito de que el subárbol derecho es mayor que el valor actual; y así sucesivamente, así la figura anterior es un árbol de búsqueda binaria.

De acuerdo con las características del árbol de búsqueda binaria, también podemos obtener las siguientes conclusiones:

  • El subárbol izquierdo y el subárbol derecho de cualquier nodo del árbol de búsqueda binario son un árbol de búsqueda binario;
  • El nodo más pequeño del árbol de búsqueda binaria es el nodo hoja en la esquina inferior izquierda de todo el árbol ;
  • El nodo más grande del árbol de búsqueda binaria es el nodo hoja en la esquina inferior derecha de todo el árbol ;

Construir un árbol de búsqueda binaria

Ahora usamos JavaScript para construir un árbol de búsqueda binaria. Debemos saber que un árbol de búsqueda binaria también se compone de nodos. Aquí creamos classuna clase de nodo,

El código de ejemplo es el siguiente:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

dieciséis

class BinarySearchTree {

  constructor() {

    // 初始化根节点

    this.root = null

  }

  // 创建一个节点

  Node(val) {

    return {

      left: null, // 左子树

      right: null, // 右子树

      parent: null, // 父节点

      val,

    }

  }

}

Aquí, un nodo consta de cuatro partes, que son un puntero al subárbol izquierdo, un puntero al subárbol derecho, un puntero al nodo principal y el valor actual.

Operaciones en árboles de búsqueda binarios

Ya hemos introducido la operación de recorrido del árbol binario en el artículo anterior, y no la repetiremos aquí, aquí presentamos principalmente las siguientes operaciones:

  • operación de inserción
  • encontrar operación
  • borrar operación

Insertar datos en el árbol de búsqueda binaria

La idea de insertar datos en un árbol de búsqueda binario es la siguiente:

  • Determine rootsi está vacío, si está vacío, cree la raíz;
  • Si rootno está vacío, debe juzgar si el nodo insertado es más grande o más pequeño valque el nodo raíz ;val
  • Si es más pequeño que el nodo raíz, significa que es un nodo del subárbol izquierdo;
  • Si es mayor que el nodo raíz, significa que es el nodo del subárbol derecho;
  • Los dos pasos anteriores se repiten hasta que se encuentra un punto, si el punto es menor que el valor que queremos insertar y no hay un subárbol derecho, use este punto como su nodo de hoja derecha, si el punto es mayor que el valor que queremos para insertar y no existe Para el subárbol derecho, use este punto como su nodo de hoja izquierdo.

El código de ejemplo es el siguiente:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

dieciséis

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)

    }

  }

}

Se define un método en la clase insertNode, que acepta un valor o una matriz de tipos de valores y lo inserta en el árbol de búsqueda binaria; para el método de inserción, definimos un #insertNodemétodo privado para la inserción de nodos.

Para ver el efecto, aquí definimos un método estático para el recorrido en orden (porque el orden del recorrido en orden es izquierda-raíz-derecha, la clasificación en orden se usa en el árbol de búsqueda binaria y los resultados finales se ordenan de pequeño a grande) este árbol, y devuelve una matriz,

El código de ejemplo es el siguiente:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

dieciséis

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

}

El código de prueba es el siguiente:

const árbol = 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 ]

La estructura de árbol final es la siguiente:

Buscar datos en un árbol de búsqueda binaria

现在我们封装一个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指针指向要删除节点的父节点,然后将当前要删除节点的父节点指向子节点即可,

如下图所示:

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

  • 找到当前节点的前驱或者后继节点,这里选择后继;
  • Luego asigne el valor del nodo sucesor al nodo actual;
  • Eliminar el nodo sucesor.

Como se muestra abajo:

Ahora que hemos analizado estas situaciones, implementémoslas a través del código.

Código de implementación

El código de implementación es el siguiente:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

dieciséis

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

sesenta y cinco

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

}

código completo

El código completo de este artículo es el siguiente:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

dieciséis

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

sesenta y cinco

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 ]

Supongo que te gusta

Origin blog.csdn.net/qq_15509251/article/details/131652868
Recomendado
Clasificación