最近業務在鞏固Java基礎,編寫了一個基於JavaFX的數獨小遊戲(隨後放連接)。寫到核心部分發現平時玩的數獨這個東西,還真有點意思:java
行、列、子宮格之間的數字互相影響,牽一髮而動全身,一不留神就碰撞衝突了,簡直都能搞出玄學的意味,怪不得古人能由此「九宮格」演繹出八卦和《周易》。git
因而本身想了很多算法,也查找了很多資料,可是都沒有找到理想的Java實現;最後無心間在Github發現一個國外大佬寫了這樣一個算法,體味一番,頓覺精闢!github
本篇就是把國外大佬的這個算法拿過來,進行一個深刻的解析,但願能幫助到用得上的人。算法
先上地址數組
數獨算法Github地址:https://github.com/a11n/sudokuide
數獨算法Github中文註解地址:https://github.com/JobsLeeGeek/sudoku性能
代碼只有三個類:this
生成器 -> 生成數獨格子code
解法器 -> 數獨求解對象
網格對象 -> 基礎數獨格子對象
直接上main方法看下基本調用:
public static void main(String[] args) { // 生成一個20個空格的9x9數獨 Generator generator = new Generator(); Grid grid = generator.generate(20); System.out.println(grid.toString()); // 9x9數獨求解 Solver solver = new Solver(); solver.solve(grid); System.out.println(grid.toString()); }
看下輸出結果(輸出方法我本身進行了修改):
生成的9x9數獨(0爲空格)
[9, 8, 0, 1, 0, 2, 5, 3, 7] [1, 4, 2, 5, 0, 7, 9, 8, 6] [0, 3, 7, 0, 8, 0, 1, 0, 0] [8, 9, 1, 0, 2, 4, 3, 0, 5] [6, 2, 0, 0, 0, 5, 8, 0, 0] [3, 7, 0, 8, 9, 1, 6, 2, 4] [4, 6, 9, 2, 1, 8, 7, 5, 3] [2, 1, 8, 0, 0, 0, 4, 6, 9] [0, 5, 3, 4, 6, 9, 2, 1, 8]
數獨求解
[9, 8, 6, 1, 4, 2, 5, 3, 7] [1, 4, 2, 5, 3, 7, 9, 8, 6] [5, 3, 7, 9, 8, 6, 1, 4, 2] [8, 9, 1, 6, 2, 4, 3, 7, 5] [6, 2, 4, 3, 7, 5, 8, 9, 1] [3, 7, 0, 8, 9, 1, 6, 2, 4] [4, 6, 9, 2, 1, 8, 7, 5, 3] [2, 1, 8, 7, 5, 3, 4, 6, 9] [7, 5, 3, 4, 6, 9, 2, 1, 8]
使用起來很簡單,速度也很快;其核心部分的代碼,其實只有三個點。
在Solver.java中solve方法實現,代碼我已經作了中文註釋:
/** * 求解方法 * * @param grid * @param cell * @return */ private boolean solve(Grid grid, Optional<Grid.Cell> cell) { // 空格子 說明遍歷處理完了 if (!cell.isPresent()) { return true; } // 遍歷隨機數值 嘗試填數 for (int value : values) { // 校驗填的數是否合理 合理的話嘗試下一個空格子 if (grid.isValidValueForCell(cell.get(), value)) { cell.get().setValue(value); // 遞歸嘗試下一個空格子 if (solve(grid, grid.getNextEmptyCellOf(cell.get()))) return true; // 嘗試失敗格子的填入0 繼續爲當前格子嘗試下一個隨機值 cell.get().setValue(EMPTY); } } return false; }
整個對象的構建在Grid.java中,其中涉及到兩個對象Grid和Cell,Grid由Cell[][]數組構成,Cell中記錄了格子的數值、行列子宮格維度的格子列表及下一個格子對象:
Grid對象
/** * 由數據格子構成的數獨格子 */ private final Cell[][] grid;
Cell對象
// 格子數值 private int value; // 行其餘格子列表 private Collection<Cell> rowNeighbors; // 列其餘格子列表 private Collection<Cell> columnNeighbors; // 子宮格其餘格子列表 private Collection<Cell> boxNeighbors; // 下一個格子對象 private Cell nextCell;
Grid初始化時,在Cell對象中,使用List構造了行、列、子宮格維度的引用(請注意這裏的引用,後面會講到這個引用的妙處),見以下代碼及中文註釋:
/** * 返回數獨格子的工廠方法 * * @param grid * @return */ public static Grid of(int[][] grid) { // 基礎校驗 verifyGrid(grid); // 初始化格子各維度統計List 9x9 行 列 子宮格 Cell[][] cells = new Cell[9][9]; List<List<Cell>> rows = new ArrayList<>(); List<List<Cell>> columns = new ArrayList<>(); List<List<Cell>> boxes = new ArrayList<>(); // 初始化List 9行 9列 9子宮格 for (int i = 0; i < 9; i++) { rows.add(new ArrayList<Cell>()); columns.add(new ArrayList<Cell>()); boxes.add(new ArrayList<Cell>()); } Cell lastCell = null; // 逐一遍歷數獨格子 往各維度統計List中填數 for (int row = 0; row < grid.length; row++) { for (int column = 0; column < grid[row].length; column++) { Cell cell = new Cell(grid[row][column]); cells[row][column] = cell; rows.get(row).add(cell); columns.get(column).add(cell); // 子宮格在List中的index計算 boxes.get((row / 3) * 3 + column / 3).add(cell); // 若是有上一次遍歷的格子 則當前格子爲上個格子的下一格子 if (lastCell != null) { lastCell.setNextCell(cell); } // 記錄上一次遍歷的格子 lastCell = cell; } } // 逐行 逐列 逐子宮格 遍歷 處理對應模塊的關聯鄰居List for (int i = 0; i < 9; i++) { // 逐行 List<Cell> row = rows.get(i); for (Cell cell : row) { List<Cell> rowNeighbors = new ArrayList<>(row); rowNeighbors.remove(cell); cell.setRowNeighbors(rowNeighbors); } // 逐列 List<Cell> column = columns.get(i); for (Cell cell : column) { List<Cell> columnNeighbors = new ArrayList<>(column); columnNeighbors.remove(cell); cell.setColumnNeighbors(columnNeighbors); } // 逐子宮格 List<Cell> box = boxes.get(i); for (Cell cell : box) { List<Cell> boxNeighbors = new ArrayList<>(box); boxNeighbors.remove(cell); cell.setBoxNeighbors(boxNeighbors); } } return new Grid(cells); }
看完代碼,其實不難發現,算法不是很複雜,簡潔易懂——經過隨機和遞歸進行枚舉和試錯;
因而本人經過使用基本數據int[][],不使用對象,按照其核心邏輯實現了本身的一套數獨,卻發現極度耗時(你們能夠本身嘗試下),好久沒有結果輸出。由此引起了對其性能的考量;
仔細思考,最後發現面向對象真的是個好東西,對象的引用從很大一層面上解決了數獨遞歸的性能問題。
寫一個有趣的例子來解釋下,用一個對象構建二維數組,初始化數值後,分別按照行維度和列維度關聯到對應的List中,打印數組和這些List;
而後咱們修改(0,0)位置的數值,注意,這裏不是new一個新的對象,而是直接使用對象的set方法操做其對應數值,再打印數組和這些List,代碼和結果以下:
示例代碼
public static void main(String[] args) { Entity[][] ee = new Entity[3][3]; for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { Entity e = new Entity(); e.setX(i); e.setY(j); ee[i][j] = e; } } System.out.println(Arrays.deepToString(ee)); List<List<Entity>> row = new ArrayList<>(); List<List<Entity>> column = new ArrayList<>(); for (int i = 0; i < 3; i++) { row.add(new ArrayList<>()); } for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { row.get(i).add(ee[i][j]); } } for (int j = 0; j < 3; j++) { column.add(new ArrayList<>()); } for (int j = 0; j < 3; j++) { for (int i = 0; i < 3; i++) { column.get(j).add(ee[i][j]); } } System.out.println(row); System.out.println(column); System.out.println(""); ee[0][0].setX(9); ee[0][0].setY(9); System.out.println(Arrays.deepToString(ee)); System.out.println(row); System.out.println(column); } static class Entity { private int x; private int y; public int getX() { return x; } public void setX(int x) { this.x = x; } public int getY() { return y; } public void setY(int y) { this.y = y; } @Override public String toString() { return "Entity{" + "x=" + x + ", y=" + y + '}'; } }
輸出結果
[[Entity{x=0, y=0}, Entity{x=0, y=1}, Entity{x=0, y=2}], [Entity{x=1, y=0}, Entity{x=1, y=1}, Entity{x=1, y=2}], [Entity{x=2, y=0}, Entity{x=2, y=1}, Entity{x=2, y=2}]] [[Entity{x=0, y=0}, Entity{x=0, y=1}, Entity{x=0, y=2}], [Entity{x=1, y=0}, Entity{x=1, y=1}, Entity{x=1, y=2}], [Entity{x=2, y=0}, Entity{x=2, y=1}, Entity{x=2, y=2}]] [[Entity{x=0, y=0}, Entity{x=1, y=0}, Entity{x=2, y=0}], [Entity{x=0, y=1}, Entity{x=1, y=1}, Entity{x=2, y=1}], [Entity{x=0, y=2}, Entity{x=1, y=2}, Entity{x=2, y=2}]] [[Entity{x=9, y=9}, Entity{x=0, y=1}, Entity{x=0, y=2}], [Entity{x=1, y=0}, Entity{x=1, y=1}, Entity{x=1, y=2}], [Entity{x=2, y=0}, Entity{x=2, y=1}, Entity{x=2, y=2}]] [[Entity{x=9, y=9}, Entity{x=0, y=1}, Entity{x=0, y=2}], [Entity{x=1, y=0}, Entity{x=1, y=1}, Entity{x=1, y=2}], [Entity{x=2, y=0}, Entity{x=2, y=1}, Entity{x=2, y=2}]] [[Entity{x=9, y=9}, Entity{x=1, y=0}, Entity{x=2, y=0}], [Entity{x=0, y=1}, Entity{x=1, y=1}, Entity{x=2, y=1}], [Entity{x=0, y=2}, Entity{x=1, y=2}, Entity{x=2, y=2}]]
神奇的地方就在這裏,行列關聯的List裏面的數值跟隨着一塊兒改變了。
這是爲何呢?
Java的集合中存放的類型
(1)若是是基本數據類型,則是value;
(2) 若是是複合數據類型,則是引用的地址;
List中放入對象時,實際放入的不是對象自己而是對象的引用;
對象數組只須要本身佔據一部份內存空間,List來引用對象,就不須要額外有數組內存的開支;
同時對原始數組中對象的修改(注意,修改並不是new一個對象,由於new一個就開闢了新的內存地址,引用還會指向原來的地址),就能夠作到遍歷一次、到處可見了!
這樣一來數組內存仍是原來的一塊數組內存,咱們只需用List關聯引用,就不用須要每次遍歷和判斷的時候開闢額外空間了;
而後每次對原始數格處理的時候,其各個維度List都不用手動再去修改;每次對各個維度數字進行判斷的時候,也就都是在對原始數格進行遍歷;其空間複雜度沒有增長。
這即是上面代碼構建的獨到之處!
妙哉妙哉!