1. บทนำ
ในบทความนี้เราจะพูดถึงการใช้งานไบนารีทรีใน Java
เพื่อประโยชน์ของบทความนี้เราจะใช้ต้นไม้ไบนารีเรียงลำดับที่จะมีintค่า
2. ต้นไม้ไบนารี
ต้นไม้ไบนารีเป็นโครงสร้างข้อมูลแบบวนซ้ำโดยแต่ละโหนดสามารถมีลูกได้สูงสุด 2 ลูก
ไบนารีทรีทั่วไปคือต้นไม้ค้นหาแบบทวิภาคซึ่งทุกโหนดมีค่าที่มากกว่าหรือเท่ากับค่าโหนดในแผนผังย่อยด้านซ้ายและน้อยกว่าหรือเท่ากับค่าโหนดในย่อยด้านขวา ต้นไม้.
นี่คือการแสดงภาพอย่างรวดเร็วของต้นไม้ไบนารีประเภทนี้:

สำหรับการนำไปใช้งานเราจะใช้คลาสNodeเสริมที่จะเก็บค่าintและอ้างอิงถึงลูกแต่ละคน:
class Node { int value; Node left; Node right; Node(int value) { this.value = value; right = null; left = null; } }
จากนั้นให้เพิ่มโหนดเริ่มต้นของต้นไม้ของเราซึ่งมักเรียกว่าroot:
public class BinaryTree { Node root; // ... }
3. การดำเนินการทั่วไป
ตอนนี้เรามาดูการดำเนินการทั่วไปที่เราสามารถทำได้บนต้นไม้ไบนารี
3.1. การแทรกองค์ประกอบ
การดำเนินการแรกที่เราจะกล่าวถึงคือการแทรกโหนดใหม่
ขั้นแรกเราต้องไปหาสถานที่ที่เราต้องการเพิ่มโหนดใหม่เพื่อให้ต้นไม้เรียง เราจะปฏิบัติตามกฎเหล่านี้โดยเริ่มจากโหนดรูท:
- หากค่าของโหนดใหม่ต่ำกว่าโหนดปัจจุบันเราไปที่ชายด์ทางซ้าย
- ถ้าค่าของโหนดใหม่มากกว่าโหนดปัจจุบันเราจะไปที่ลูกที่ถูกต้อง
- เมื่อโหนดปัจจุบันเป็นโมฆะเรามาถึงโหนดลีฟและเราสามารถแทรกโหนดใหม่ในตำแหน่งนั้นได้
ขั้นแรกเราจะสร้างวิธีการเรียกซ้ำเพื่อทำการแทรก:
private Node addRecursive(Node current, int value) { if (current == null) { return new Node(value); } if (value current.value) { current.right = addRecursive(current.right, value); } else { // value already exists return current; } return current; }
ต่อไปเราจะสร้างวิธีการสาธารณะที่เริ่มต้นการเรียกซ้ำจากโหนดรูท :
public void add(int value) { root = addRecursive(root, value); }
มาดูกันว่าเราจะใช้วิธีนี้สร้างต้นไม้ได้อย่างไรจากตัวอย่างของเรา:
private BinaryTree createBinaryTree() { BinaryTree bt = new BinaryTree(); bt.add(6); bt.add(4); bt.add(8); bt.add(3); bt.add(5); bt.add(7); bt.add(9); return bt; }
3.2. การค้นหาองค์ประกอบ
ตอนนี้ให้เพิ่มวิธีการตรวจสอบว่าต้นไม้มีค่าเฉพาะหรือไม่
ก่อนหน้านี้เราจะสร้างวิธีการเรียกซ้ำที่ข้ามต้นไม้ก่อน:
private boolean containsNodeRecursive(Node current, int value) { if (current == null) { return false; } if (value == current.value) { return true; } return value < current.value ? containsNodeRecursive(current.left, value) : containsNodeRecursive(current.right, value); }
ที่นี่เรากำลังค้นหาค่าโดยเปรียบเทียบกับค่าในโหนดปัจจุบันจากนั้นดำเนินการต่อในลูกทางซ้ายหรือขวาขึ้นอยู่กับว่า
ต่อไปมาสร้างวิธีการสาธารณะที่เริ่มต้นจากรูท :
public boolean containsNode(int value) { return containsNodeRecursive(root, value); }
ตอนนี้เรามาสร้างการทดสอบง่ายๆเพื่อตรวจสอบว่าต้นไม้มีองค์ประกอบที่แทรกอยู่จริงๆ:
@Test public void givenABinaryTree_WhenAddingElements_ThenTreeContainsThoseElements() { BinaryTree bt = createBinaryTree(); assertTrue(bt.containsNode(6)); assertTrue(bt.containsNode(4)); assertFalse(bt.containsNode(1)); }
โหนดทั้งหมดที่เพิ่มควรมีอยู่ในโครงสร้าง
3.3. การลบองค์ประกอบ
การดำเนินการทั่วไปอีกอย่างหนึ่งคือการลบโหนดออกจากทรี
ขั้นแรกเราต้องหาโหนดที่จะลบในลักษณะเดียวกันกับที่เราทำก่อนหน้านี้:
private Node deleteRecursive(Node current, int value) { if (current == null) { return null; } if (value == current.value) { // Node to delete found // ... code to delete the node will go here } if (value < current.value) { current.left = deleteRecursive(current.left, value); return current; } current.right = deleteRecursive(current.right, value); return current; }
เมื่อเราพบโหนดที่จะลบมี 3 กรณีหลักที่แตกต่างกัน:
- โหนดไม่มีลูก -นี่เป็นกรณีที่ง่ายที่สุด เราเพียงแค่ต้องแทนที่โหนดนี้ด้วยnullในโหนดแม่
- โหนดมีลูกเดียว -ในโหนดแม่เราแทนที่โหนดนี้ด้วยลูกคนเดียว
- โหนดมีลูกสองคน - นี่เป็นกรณีที่ซับซ้อนที่สุดเนื่องจากต้องมีการจัดโครงสร้างใหม่
มาดูกันว่าเราจะใช้กรณีแรกได้อย่างไรเมื่อโหนดเป็นโหนดลีฟ:
if (current.left == null && current.right == null) { return null; }
ทีนี้มาดูกรณีต่อไปเมื่อโหนดมีลูกเดียว:
if (current.right == null) { return current.left; } if (current.left == null) { return current.right; }
ที่นี่เรากำลังส่งคืนเด็กที่ไม่เป็นค่าว่างเพื่อให้สามารถกำหนดให้กับโหนดหลักได้
สุดท้ายเราต้องจัดการกรณีที่โหนดมีลูกสองคน
ขั้นแรกเราต้องหาโหนดที่จะแทนที่โหนดที่ถูกลบ เราจะใช้โหนดที่เล็กที่สุดของโหนดที่จะถูกลบโครงสร้างย่อยด้านขวา:
private int findSmallestValue(Node root) { return root.left == null ? root.value : findSmallestValue(root.left); }
จากนั้นเรากำหนดค่าที่น้อยที่สุดให้กับโหนดที่จะลบและหลังจากนั้นเราจะลบออกจากทรีย่อยด้านขวา:
int smallestValue = findSmallestValue(current.right); current.value = smallestValue; current.right = deleteRecursive(current.right, smallestValue); return current;
สุดท้ายมาสร้างวิธีการสาธารณะที่เริ่มต้นการลบจากราก :
public void delete(int value) { root = deleteRecursive(root, value); }
ตอนนี้ให้ตรวจสอบว่าการลบทำงานตามที่คาดไว้:
@Test public void givenABinaryTree_WhenDeletingElements_ThenTreeDoesNotContainThoseElements() { BinaryTree bt = createBinaryTree(); assertTrue(bt.containsNode(9)); bt.delete(9); assertFalse(bt.containsNode(9)); }
4. สำรวจต้นไม้
ในส่วนนี้เราจะเห็นวิธีต่างๆในการสำรวจต้นไม้โดยครอบคลุมรายละเอียดการค้นหาในเชิงลึกก่อนและเชิงกว้าง
We'll use the same tree that we used before and we'll show the traversal order for each case.
4.1. Depth-First Search
Depth-first search is a type of traversal that goes deep as much as possible in every child before exploring the next sibling.
There are several ways to perform a depth-first search: in-order, pre-order and post-order.
The in-order traversal consists of first visiting the left sub-tree, then the root node, and finally the right sub-tree:
public void traverseInOrder(Node node) { if (node != null) { traverseInOrder(node.left); System.out.print(" " + node.value); traverseInOrder(node.right); } }
If we call this method, the console output will show the in-order traversal:
3 4 5 6 7 8 9
Pre-order traversal visits first the root node, then the left subtree, and finally the right subtree:
public void traversePreOrder(Node node) { if (node != null) { System.out.print(" " + node.value); traversePreOrder(node.left); traversePreOrder(node.right); } }
And let's check the pre-order traversal in the console output:
6 4 3 5 8 7 9
Post-order traversal visits the left subtree, the right subtree, and the root node at the end:
public void traversePostOrder(Node node) { if (node != null) { traversePostOrder(node.left); traversePostOrder(node.right); System.out.print(" " + node.value); } }
Here are the nodes in post-order:
3 5 4 7 9 8 6
4.2. Breadth-First Search
นี้เป็นอีกหนึ่งชนิดที่พบบ่อยของการสำรวจเส้นทางที่ผู้เข้าชมโหนดทั้งหมดของระดับก่อนที่จะไปที่ระดับถัดไป
การส่งผ่านแบบนี้เรียกอีกอย่างว่าลำดับระดับและเยี่ยมชมทุกระดับของต้นไม้โดยเริ่มจากรากและจากซ้ายไปขวา
สำหรับการนำไปใช้งานเราจะใช้Queueเพื่อยึดโหนดจากแต่ละระดับตามลำดับ เราจะแยกแต่ละโหนดออกจากรายการพิมพ์ค่าจากนั้นเพิ่มลูก ๆ ในคิว:
public void traverseLevelOrder() { if (root == null) { return; } Queue nodes = new LinkedList(); nodes.add(root); while (!nodes.isEmpty()) { Node node = nodes.remove(); System.out.print(" " + node.value); if (node.left != null) { nodes.add(node.left); } if (node.right != null) { nodes.add(node.right); } } }
ในกรณีนี้ลำดับของโหนดจะเป็น:
6 4 8 3 5 7 9
5. สรุป
ในบทความนี้เราได้เห็นวิธีใช้ไบนารีทรีที่เรียงลำดับใน Java และการดำเนินการที่พบบ่อยที่สุด
ซอร์สโค้ดแบบเต็มสำหรับตัวอย่างมีอยู่บน GitHub