Algoritma Cari Julat di Jawa

1. Gambaran keseluruhan

Dalam tutorial ini, kita akan meneroka konsep mencari jiran dalam ruang dua dimensi . Kemudian, kita akan melalui pelaksanaannya di Jawa.

2. Pencarian Satu Dimensi vs Pencarian Dua Dimensi

Kami tahu bahawa carian binari adalah algoritma yang cekap untuk mencari padanan tepat dalam senarai item menggunakan pendekatan divide-and-win.

Sekarang mari kita pertimbangkan kawasan dua dimensi di mana setiap item diwakili oleh koordinat XY (titik) dalam satah .

Namun, bukannya persamaan yang tepat, anggaplah kita ingin mencari jiran pada titik tertentu dalam pesawat. Sudah jelas bahawa jika kita menginginkan padanan n terdekat , maka carian binari tidak akan berjaya . Ini kerana carian binari dapat membandingkan dua item dalam satu paksi sahaja, sedangkan kita perlu dapat membandingkannya dalam dua paksi.

Kami akan melihat alternatif untuk struktur data pokok binari di bahagian seterusnya.

3. Kuadrat

Quadtree adalah struktur data pokok spasial di mana setiap nod mempunyai empat anak. Setiap anak boleh menjadi titik atau senarai yang mengandungi empat sub-quadtrees.

A titik menyimpan data - sebagai contoh, koordinat XY. A rantau mewakili sempadan tertutup di mana mata boleh disimpan. Ini digunakan untuk menentukan luas jangkauan kuadtree.

Mari kita fahami ini dengan menggunakan contoh 10 koordinat dalam beberapa susunan sewenang-wenangnya:

(21,25), (55,53), (70,318), (98,302), (49,229), (135,229), (224,292), (206,321), (197,258), (245,238)

Tiga nilai pertama akan disimpan sebagai titik di bawah simpul akar seperti yang ditunjukkan dalam gambar paling kiri.

Node akar tidak dapat menampung titik baru sekarang kerana ia telah mencapai kapasiti tiga titik. Oleh itu, kami akan membahagikan kawasan nod akar menjadi empat kuadran yang sama .

Setiap kuadran ini dapat menyimpan tiga titik dan juga mengandungi empat kuadran dalam batasnya. Ini dapat dilakukan secara rekursif, menghasilkan pohon kuadran, di mana struktur data quadtree mendapat namanya.

Pada gambar tengah di atas, kita dapat melihat kuadran yang dibuat dari simpul akar dan bagaimana empat titik seterusnya disimpan di kuadran ini.

Akhirnya, gambar paling kanan menunjukkan bagaimana satu kuadran dibahagi lagi untuk menampung lebih banyak titik di rantau itu sementara kuadran lain masih dapat menerima poin baru.

Kita sekarang akan melihat bagaimana menerapkan algoritma ini di Java.

4. Struktur Data

Mari buat struktur data quadtree. Kami memerlukan tiga kelas domain.

Pertama, kami akan membuat kelas Point untuk menyimpan koordinat XY :

public class Point { private float x; private float y; public Point(float x, float y) { this.x = x; this.y = y; } // getters & toString() }

Kedua, mari buat kelas Wilayah untuk menentukan sempadan kuadran :

public class Region { private float x1; private float y1; private float x2; private float y2; public Region(float x1, float y1, float x2, float y2) { this.x1 = x1; this.y1 = y1; this.x2 = x2; this.y2 = y2; } // getters & toString() }

Akhirnya, mari kita ada kelas QuadTree untuk menyimpan data sebagai contoh Point dan anak-anak sebagai kelas QuadTree :

public class QuadTree { private static final int MAX_POINTS = 3; private Region area; private List points = new ArrayList(); private List quadTrees = new ArrayList(); public QuadTree(Region area) { this.area = area; } }

Untuk mewujudkan objek QuadTree , kami menentukan kawasannya menggunakan kelas Wilayah melalui konstruktor.

5. Algoritma

Sebelum kita menulis logik teras kita untuk menyimpan data, mari tambahkan beberapa kaedah pembantu. Ini akan terbukti berguna kemudian.

5.1. Kaedah Pembantu

Mari ubah kelas Wilayah kami .

Pertama, mari kita mempunyai kaedah containsPoint untuk menunjukkan jika diberi titik jatuh di dalam atau di luar daripada rantau ini kawasan :

public boolean containsPoint(Point point) { return point.getX() >= this.x1 && point.getX() = this.y1 && point.getY() < this.y2; }

Seterusnya, mari kita mempunyai kaedah doesOverlap untuk menunjukkan jika seorang diberikan kawasan pertindihan dengan lain kawasan :

public boolean doesOverlap(Region testRegion) { if (testRegion.getX2()  this.getX2()) { return false; } if (testRegion.getY1() > this.getY2()) { return false; } if (testRegion.getY2() < this.getY1()) { return false; } return true; }

Akhirnya, mari buat kaedah getQuadrant untuk membahagikan julat menjadi empat kuadran yang sama dan mengembalikan yang ditentukan:

public Region getQuadrant(int quadrantIndex) { float quadrantWidth = (this.x2 - this.x1) / 2; float quadrantHeight = (this.y2 - this.y1) / 2; // 0=SW, 1=NW, 2=NE, 3=SE switch (quadrantIndex) { case 0: return new Region(x1, y1, x1 + quadrantWidth, y1 + quadrantHeight); case 1: return new Region(x1, y1 + quadrantHeight, x1 + quadrantWidth, y2); case 2: return new Region(x1 + quadrantWidth, y1 + quadrantHeight, x2, y2); case 3: return new Region(x1 + quadrantWidth, y1, x2, y1 + quadrantHeight); } return null; }

5.2. Menyimpan Data

Kita sekarang boleh menulis logik kita untuk menyimpan data. Mari kita mulakan dengan menentukan kaedah baru addPoint pada kelas QuadTree untuk menambah titik baru . Kaedah ini akan kembali benar jika titik berjaya ditambahkan:

public boolean addPoint(Point point) { // ... }

Seterusnya, mari tulis logik untuk menangani intinya. Pertama, kita perlu memeriksa apakah intinya terkandung dalam batas contoh QuadTree . Kita juga perlu memastikan bahawa contoh QuadTree belum mencapai kapasiti MAX_POINTS mata.

Sekiranya kedua-dua syarat itu dipenuhi, kita boleh menambah titik baru:

if (this.area.containsPoint(point)) { if (this.points.size() < MAX_POINTS) { this.points.add(point); return true; } }

Sebaliknya, jika kita telah mencapai nilai MAX_POINTS , maka kita perlu menambahkan titik baru ke salah satu sub-kuadran . Untuk ini, kami melengkapkan senarai anak quadTrees dan memanggil kaedah addPoint yang sama yang akan mengembalikan nilai sebenar pada penambahan yang berjaya. Kemudian kami keluar dari gelung dengan segera kerana titik perlu ditambahkan tepat pada satu kuadran .

Kita boleh merangkumi semua logik ini dengan kaedah penolong:

private boolean addPointToOneQuadrant(Point point) { boolean isPointAdded; for (int i = 0; i < 4; i++) { isPointAdded = this.quadTrees.get(i) .addPoint(point); if (isPointAdded) return true; } return false; }

Selain itu, mari kita ada kaedah berguna createQuadrants untuk membahagikan kuadtree semasa menjadi empat kuadran:

private void createQuadrants() { Region region; for (int i = 0; i < 4; i++) { region = this.area.getQuadrant(i); quadTrees.add(new QuadTree(region)); } }

Kami akan memanggil kaedah ini untuk membuat kuadran hanya jika kita tidak lagi dapat menambahkan titik baru . Ini memastikan bahawa struktur data kami menggunakan ruang memori yang optimum.

Menggabungkan semuanya, kami mempunyai kaedah addPoint yang dikemas kini :

public boolean addPoint(Point point) { if (this.area.containsPoint(point)) { if (this.points.size() < MAX_POINTS) { this.points.add(point); return true; } else { if (this.quadTrees.size() == 0) { createQuadrants(); } return addPointToOneQuadrant(point); } } return false; }

5.3. Mencari Data

Dengan struktur quadtree kami yang ditentukan untuk menyimpan data, kini kita dapat memikirkan logik untuk melakukan carian.

Semasa kami mencari untuk mencari item bersebelahan, kami dapat menentukan searchRegion sebagai titik permulaan . Kemudian, kami periksa sama ada ia bertindih dengan kawasan akar. Sekiranya berlaku, maka kita tambahkan semua titik anak-anaknya yang berada di dalam searchRegion .

Selepas kawasan akar, kita masuk ke dalam setiap kuadran dan mengulangi prosesnya. Ini berterusan sehingga kita sampai di hujung pokok.

Mari tulis logik di atas sebagai kaedah rekursif di kelas QuadTree :

public List search(Region searchRegion, List matches) { if (matches == null) { matches = new ArrayList(); } if (!this.area.doesOverlap(searchRegion)) { return matches; } else { for (Point point : points) { if (searchRegion.containsPoint(point)) { matches.add(point); } } if (this.quadTrees.size() > 0) { for (int i = 0; i < 4; i++) { quadTrees.get(i) .search(searchRegion, matches); } } } return matches; }

6. Menguji

Sekarang kita mempunyai algoritma kita, mari kita mengujinya.

6.1. Mengisi Data

Pertama, mari isikan kuadtree dengan 10 koordinat yang sama yang kita gunakan sebelumnya:

Region area = new Region(0, 0, 400, 400); QuadTree quadTree = new QuadTree(area); float[][] points = new float[][] { { 21, 25 }, { 55, 53 }, { 70, 318 }, { 98, 302 }, { 49, 229 }, { 135, 229 }, { 224, 292 }, { 206, 321 }, { 197, 258 }, { 245, 238 } }; for (int i = 0; i < points.length; i++) { Point point = new Point(points[i][0], points[i][1]); quadTree.addPoint(point); }

6.2. Carian Julat

Seterusnya, mari kita lakukan carian jarak di kawasan yang ditutup oleh koordinat batas bawah (200, 200) dan koordinat batas atas (250, 250):

Region searchArea = new Region(200, 200, 250, 250); List result = quadTree.search(searchArea, null);

Menjalankan kod akan memberi kita satu koordinat berdekatan yang terdapat dalam kawasan carian:

[[245.0 , 238.0]]

Mari cuba kawasan carian yang berbeza antara koordinat (0, 0) dan (100, 100):

Region searchArea = new Region(0, 0, 100, 100); List result = quadTree.search(searchArea, null);

Menjalankan kod akan memberi kita dua koordinat berdekatan untuk kawasan carian yang ditentukan:

[[21.0 , 25.0], [55.0 , 53.0]]

We observe that depending on the size of the search area, we get zero, one or many points. So, if we're given a point and asked to find the nearest n neighbors, we could define a suitable search area where the given point is at the center.

Then, from all the resulting points of the search operation, we can calculate the Euclidean distances between the given points and sort them to get the nearest neighbors.

7. Time Complexity

The time complexity of a range query is simply O(n). The reason is that, in the worst-case scenario, it has to traverse through each item if the search area specified is equal to or bigger than the populated area.

8. Conclusion

Dalam artikel ini, kami mula-mula memahami konsep kuadtree dengan membandingkannya dengan pokok binari. Seterusnya, kami melihat bagaimana ia dapat digunakan dengan cekap untuk menyimpan data yang tersebar di ruang dua dimensi.

Kami kemudian melihat cara menyimpan data dan melakukan carian jarak jauh.

Seperti biasa, kod sumber dengan ujian tersedia di GitHub.