Skip to content

Commit 8d9ec40

Browse files
committed
fix: preserve KDTree deletion traversal and split invariants
Descend using the same split direction as insert and search, move remaining left-only children to the right of their minimum replacement, and stop traversal once the selected node is handled. Cover leaf and subtree deletion, nested successors, left-only replacement reuse, missing and final points, and deletion-order reference checks for bulk-built and inserted 1D/2D/3D trees. Fixes #7632
1 parent 2fdcde7 commit 8d9ec40

2 files changed

Lines changed: 169 additions & 2 deletions

File tree

‎src/main/java/com/thealgorithms/datastructures/trees/KDTree.java‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -406,12 +406,14 @@ private Node delete(Node root, Node node) {
406406
} else if (root.left != null) {
407407
Node min = findMin(root.left, root.getAxis());
408408
root.point = min.point;
409-
root.left = delete(root.left, min);
409+
root.right = delete(root.left, min);
410+
root.left = null;
410411
} else {
411412
return null;
412413
}
414+
return root;
413415
}
414-
if (root.getAxisCoordinate() < node.point.getCoordinate(root.getAxis())) {
416+
if (node.point.getCoordinate(root.getAxis()) < root.getAxisCoordinate()) {
415417
root.left = delete(root.left, node);
416418
} else {
417419
root.right = delete(root.right, node);
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package com.thealgorithms.datastructures.trees;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collections;
5+
import java.util.List;
6+
import java.util.Set;
7+
import org.junit.jupiter.api.Assertions;
8+
import org.junit.jupiter.api.Test;
9+
10+
class KDTreeDeletionTest {
11+
@Test
12+
void deleteLeavesOnBothSides() {
13+
for (int coordinate : new int[] {10, 30}) {
14+
List<KDTree.Point> points = pointsOf(20, 10, 30);
15+
KDTree tree = insertedTree(points);
16+
KDTree.Point deleted = pointOf(coordinate);
17+
tree.delete(deleted);
18+
points.remove(deleted);
19+
assertContents(tree, points);
20+
Assertions.assertFalse(tree.search(deleted).isPresent());
21+
}
22+
}
23+
24+
@Test
25+
void deleteNonRootWithDescendants() {
26+
for (int coordinate : new int[] {10, 30}) {
27+
List<KDTree.Point> points = pointsOf(20, 10, 30, 5, 15, 25, 35, 13, 27);
28+
KDTree tree = insertedTree(points);
29+
KDTree.Point deleted = pointOf(coordinate);
30+
tree.delete(deleted);
31+
points.remove(deleted);
32+
assertContents(tree, points);
33+
Assertions.assertFalse(tree.search(deleted).isPresent());
34+
}
35+
}
36+
37+
@Test
38+
void deleteRootWithOnlyLeftSubtree() {
39+
List<KDTree.Point> points = pointsOf(20, 10, 15, 5);
40+
KDTree tree = insertedTree(points);
41+
KDTree.Point deleted = points.removeFirst();
42+
tree.delete(deleted);
43+
assertContents(tree, points);
44+
Assertions.assertFalse(tree.search(deleted).isPresent());
45+
KDTree.Point inserted = pointOf(12);
46+
tree.insert(inserted);
47+
points.add(inserted);
48+
assertContents(tree, points);
49+
tree.delete(inserted);
50+
points.remove(inserted);
51+
assertContents(tree, points);
52+
}
53+
54+
@Test
55+
void deleteRootWithNestedRightSuccessor() {
56+
List<KDTree.Point> points = pointsOf(20, 30, 25, 27, 40);
57+
KDTree tree = insertedTree(points);
58+
KDTree.Point deleted = points.removeFirst();
59+
tree.delete(deleted);
60+
assertContents(tree, points);
61+
Assertions.assertFalse(tree.search(deleted).isPresent());
62+
KDTree.Point successor = pointOf(25);
63+
Assertions.assertEquals(successor, tree.getRoot().getPoint());
64+
tree.delete(successor);
65+
points.remove(successor);
66+
assertContents(tree, points);
67+
Assertions.assertFalse(tree.search(successor).isPresent());
68+
}
69+
70+
@Test
71+
void deletingAbsentPointPreservesContents() {
72+
List<KDTree.Point> points = pointsOf(20, 10, 30);
73+
KDTree tree = insertedTree(points);
74+
Assertions.assertThrows(IllegalArgumentException.class, () -> tree.delete(pointOf(15)));
75+
assertContents(tree, points);
76+
for (KDTree.Point point : List.copyOf(points)) {
77+
tree.delete(point);
78+
points.remove(point);
79+
assertContents(tree, points);
80+
}
81+
Assertions.assertNull(tree.getRoot());
82+
Assertions.assertThrows(IllegalArgumentException.class, () -> tree.delete(pointOf(20)));
83+
Assertions.assertNull(tree.getRoot());
84+
}
85+
86+
@Test
87+
void deletionOrdersPreserveBuiltAndInsertedTrees() {
88+
for (int dimension : new int[] {1, 2, 3}) {
89+
List<KDTree.Point> initial = new ArrayList<>();
90+
for (int index = 0; index < 15; index++) {
91+
int[] coordinates = new int[dimension];
92+
for (int axis = 0; axis < dimension; axis++) {
93+
coordinates[axis] = index * (axis + 1) % 17 - 8;
94+
}
95+
initial.add(new KDTree.Point(coordinates));
96+
}
97+
for (boolean bulkBuild : new boolean[] {false, true}) {
98+
for (int shift : new int[] {0, 4, 9}) {
99+
List<KDTree.Point> remaining = new ArrayList<>(initial);
100+
List<KDTree.Point> order = new ArrayList<>(initial);
101+
Collections.rotate(order, shift);
102+
KDTree tree = bulkBuild ? new KDTree(initial.toArray(KDTree.Point[] ::new)) : insertedTree(initial);
103+
assertContents(tree, remaining);
104+
for (KDTree.Point point : order) {
105+
tree.delete(point);
106+
remaining.remove(point);
107+
assertContents(tree, remaining);
108+
Assertions.assertFalse(tree.search(point).isPresent());
109+
}
110+
Assertions.assertNull(tree.getRoot());
111+
}
112+
}
113+
}
114+
}
115+
116+
private static KDTree.Point pointOf(int coordinate) {
117+
return new KDTree.Point(new int[] {coordinate, coordinate});
118+
}
119+
120+
private static List<KDTree.Point> pointsOf(int... coordinates) {
121+
List<KDTree.Point> points = new ArrayList<>();
122+
for (int coordinate : coordinates) {
123+
points.add(pointOf(coordinate));
124+
}
125+
return points;
126+
}
127+
128+
private static KDTree insertedTree(List<KDTree.Point> points) {
129+
KDTree tree = new KDTree(points.getFirst().getDimension());
130+
for (KDTree.Point point : points) {
131+
tree.insert(point);
132+
}
133+
return tree;
134+
}
135+
136+
private static void assertContents(KDTree tree, List<KDTree.Point> expected) {
137+
List<KDTree.Point> actual = new ArrayList<>();
138+
collectPoints(tree.getRoot(), actual);
139+
Assertions.assertEquals(expected.size(), actual.size());
140+
Assertions.assertEquals(Set.copyOf(expected), Set.copyOf(actual));
141+
for (KDTree.Point point : expected) {
142+
Assertions.assertTrue(tree.search(point).isPresent(), () -> "Surviving point is unreachable: " + point);
143+
}
144+
if (!expected.isEmpty()) {
145+
for (int axis = 0; axis < expected.getFirst().getDimension(); axis++) {
146+
int minimum = Integer.MAX_VALUE;
147+
int maximum = Integer.MIN_VALUE;
148+
for (KDTree.Point point : expected) {
149+
minimum = Math.min(minimum, point.getCoordinate(axis));
150+
maximum = Math.max(maximum, point.getCoordinate(axis));
151+
}
152+
Assertions.assertEquals(minimum, tree.findMin(axis).getCoordinate(axis));
153+
Assertions.assertEquals(maximum, tree.findMax(axis).getCoordinate(axis));
154+
}
155+
}
156+
}
157+
158+
private static void collectPoints(KDTree.Node node, List<KDTree.Point> points) {
159+
if (node != null) {
160+
points.add(node.getPoint());
161+
collectPoints(node.getLeft(), points);
162+
collectPoints(node.getRight(), points);
163+
}
164+
}
165+
}

0 commit comments

Comments
 (0)