LeetCode初級算法之數組:36 有效數獨

有效數獨

題目地址:https://leetcode-cn.com/problems/valid-sudoku/java

判斷一個 9x9 的數獨是否有效。只須要根據如下規則,驗證已經填入的數字是否有效便可。算法

  1. 數字 1-9 在每一行只能出現一次。
  2. 數字 1-9 在每一列只能出現一次。
  3. 數字 1-9 在每個以粗實線分隔的 3x3 宮內只能出現一次。

示例1:數組

輸入:
[
["5","3",".",".","7",".",".",".","."],
["6",".",".","1","9","5",".",".","."],
[".","9","8",".",".",".",".","6","."],
["8",".",".",".","6",".",".",".","3"],
["4",".",".","8",".","3",".",".","1"],
["7",".",".",".","2",".",".",".","6"],
[".","6",".",".",".",".","2","8","."],
[".",".",".","4","1","9",".",".","5"],
[".",".",".",".","8",".",".","7","9"]
]
輸出: true數據結構

示例2:優化

輸入:
[
["8","3",".",".","7",".",".",".","."],
["6",".",".","1","9","5",".",".","."],
[".","9","8",".",".",".",".","6","."],
["8",".",".",".","6",".",".",".","3"],
["4",".",".","8",".","3",".",".","1"],
["7",".",".",".","2",".",".",".","6"],
[".","6",".",".",".",".","2","8","."],
[".",".",".","4","1","9",".",".","5"],
[".",".",".",".","8",".",".","7","9"]
]
輸出: false編碼

說明:code

  • 一個有效的數獨(部分已被填充)不必定是可解的。
  • 只須要根據以上規則,驗證已經填入的數字是否有效便可。
  • 給定數獨序列只包含數字 1-9 和字符 '.' 。
  • 給定數獨永遠是 9×9 形式的。

暴力

最直觀的也就是按照題目流程的暴力解法,須要去判斷每行每列每塊有沒有重複,那就去拿到每行每列每塊的二維數組。判斷這三組二維數組中的每一個一維數組是有否重複。這裏舉個相似的4×4的例子(圖畫的少一點)索引

好比:leetcode

row:
[
['1','2','3','4'],
['5','.','6','.'],
['.','7','8','.'],
['7','.','.','3']
]get

col:
[
['1','5','.','7'],
['2','.','7','.'],
['3','6','8','.'],
['4','.','.','3']
]

box:
[
['1','2','5','.'],
['3','4','6','.'],
['.','7','7','.'],
['8','.','.','3']
]

解法一

public boolean isValidSudoku(char[][] board) {
    //傳入的board至關於就是row因此只須要初始化兩個數組
    char[][] col = new char[9][9];
    char[][] box = new char[9][9];
    //1.拿到三個二維數組分別表示多行數組,多列數組以及多塊數組
    for(int i = 0; i < 9; i++){
        for(int j = 0; j < 9; j++){
            col[j][i] = board[i][j];
            box[(i/3)*3 + j/3][(i%3)*3 + j%3] = board[i][j];
       	}
    }
    //2.遍歷每一個二維數組中的數組判斷有無重複
    for(int i = 0; i < 9; i++){
        if(checkRepeat(board[i])){
            return false;
        }
        if(checkRepeat(col[i])){
            return false;
        }
        if(checkRepeat(box[i])){
            return false;
        }
    }
    return true;
}
/**
檢測單數組是否重複
*/
public boolean checkRepeat(char[] arr){
    for(int i = 0; i < 9; i++){
        for(int j = i+1; j < 9; j++){
            if(arr[i] != '.' && arr[j] != '.')
            if(arr[i] == arr[j]){
                return true;
            }
        }
    }
    return false;
}

上面解法實際上存在一個問題就是咱們存完二維數組以後,再使用遍歷查重的方式對每一個單數組進行查重。

col[j][i] = board[i][j];
box[(i/3)*3 + j/3][(i%3)*3 + j%3] = board[i][j];

也就是說在存這兩個二維數組時只有只有第一個中括號的索引是有用的標記着是哪一列或者哪一塊,但它是在一列(塊/行)的哪一個位置是無所謂的,由於最後單獨用了單數組查重的方式(不管順序怎麼樣只要是在一個容器,最後容器單獨用方法判斷是否有重)。在編碼中第二個中括號寫的索引只不過是保留了在面板上咱們去數數的順序,換成別的0-9不重複的也能夠。

是否重複的關鍵也就是數值是否同樣,是不是同一塊(行/列)這些相同也就是無效數獨,和在具體行(列/塊)裏面的哪一個位置無關。也就能夠這樣寫

解法二

public boolean isValidSudoku(char[][] board) {
    HashSet<String> set = new HashSet();
    //遍歷存值
    for (int i = 0; i < 9; i++) {
        for (int j = 0; j < 9; j++) {
            char num = board[i][j];
            if (num != '.') {
                int n = num - '0';
                int box_index = (i / 3 ) * 3 + j / 3;
                String row = n + "是第"+i+"行";
                String col = n + "是第"+j+"列";
                String box = n + "是第"+box_index+"塊";
                if(!set.add(row) || !set.add(col) || !set.add(box)) return false;
            }
        }
    }
    return true;
}

其實這纔是最直接的方式,也是效率最低的。

Hash表

根據上述狀況第二層容器的索引沒有意義,只用第一層容器的索引判斷存到哪一個第二層容器(塊/列/行)最後對全部第二層容器查重因此才無關。那我這裏咱們能夠用上第二層容器的索引或者key把它的索引變得有意義,也就是等同於值。這樣值就與位置相關,再存時就能夠判斷重複與否。而不用先存完以後在單獨遍歷每一個第二層容器。

解法三

public boolean isValidSudoku(char[][] board) {
    //初始化數組
    HashMap<Integer, Integer> [] rows = new HashMap[9];
    HashMap<Integer, Integer> [] columns = new HashMap[9];
    HashMap<Integer, Integer> [] boxes = new HashMap[9];
    //初始化hashmap
    for (int i = 0; i < 9; i++) {
      rows[i] = new HashMap<Integer, Integer>();
      columns[i] = new HashMap<Integer, Integer>();
      boxes[i] = new HashMap<Integer, Integer>();
    }

    //遍歷存值
    for (int i = 0; i < 9; i++) {
    	for (int j = 0; j < 9; j++) {
            char num = board[i][j];
            if (num != '.') {
          	int n = (int)num;
          	int box_index = (i / 3 ) * 3 + j / 3;
		//以數值爲key存下
          	rows[i].put(n, rows[i].getOrDefault(n, 0) + 1);
          	columns[j].put(n, columns[j].getOrDefault(n, 0) + 1);
          	boxes[box_index].put(n, boxes[box_index].getOrDefault(n, 0) + 1);
            	//判斷當前(行、列、塊)此值有木有重複
          	if (rows[i].get(n) > 1 || columns[j].get(n) > 1 || boxes[box_index].get(n) > 1)
            	    return false;
            }
    	}
    }
    return true;
}

按照上述解法存值標記key,經過key來判斷有沒有存過。一樣也能夠用數組實現用索引標記值和用key標記值是同樣的而且效率上也會更快。因存的數值就是1-9,那就把9存到第九個也就是索引8。這樣存進一個容器同一個數字就必定在同一個地方,經過值能夠找到索引,經過索引又能夠找到目前存的值。那麼和map用key是同樣的。都是作到用數值標記位置,那麼同一個值即同一個地方在這個地方判斷是否有重、

解法四

public boolean isValidSudoku(char[][] board) {
    //初始化數組
    char[][] rows = new char[9][9];
    char[][] columns = new char[9][9];
    char[][] boxes = new char[9][9];

    //遍歷存值
    for (int i = 0; i < 9; i++) {
    	for (int j = 0; j < 9; j++) {
            char num = board[i][j];
            if (num != '.') {
          	int n = num - '0';
          	int box_index = (i / 3 ) * 3 + j / 3;
                //以數值爲索引存下
          	if(rows[i][n - 1] == num) return false;
          	if(columns[j][n - 1] == num) return false;
          	if(boxes[box_index][n - 1] == num) return false;
                rows[i][n - 1] = num;
                columns[j][n - 1] = num;
                boxes[box_index][n - 1] = num;
            }
    	}
    }
    return true;
}

兩種解法同一個思路只是選取的數據結構不一樣,一樣是使用值來標記位置,經過值就能查找位置。所以若是有一樣的值就在同一個位置能夠去判斷。map是以值爲key來實現,數組在此情景下由於數獨盤面是9×9,裏面的數字只能是1到9,因此數字若是是1就存在0位,是4就存在索引3的位置。經過值減一固定存的位置。

但上面數組解法還是存在疏漏浪費了空間,經過遍歷的一個數我拿到這個數找對應位置的數值是否和這個數相等。仔細想想後面這一部分就是廢話,我都存同一個索引或者同一個key了就是值相同,何須還去取再比較。只有兩種狀況這個地方沒有存過那就是那就是null和當前值不一樣,而後存事後再有一個往這個索引或者key存那就是重複了不用比。由於索引和key就是值。所以改爲boolean,沒存過都是false,存了就是true,再存同一個地方時發現是true就是這個值已經存過此次是重複的

解法五(解法四優化)

public boolean isValidSudoku(char[][] board) {
    //初始化數組
    boolean[][] rows = new boolean[9][9];
    boolean[][] columns = new boolean[9][9];
    boolean[][] boxes = new boolean[9][9];

    //遍歷存值
    for (int i = 0; i < 9; i++) {
    	for (int j = 0; j < 9; j++) {
            char num = board[i][j];
            if (num != '.') {
          	int n = num - '0';
          	int box_index = (i / 3 ) * 3 + j / 3;
                //以數值爲索引取看是否設過值
          	if(rows[i][n - 1]) return false;
          	if(columns[j][n - 1]) return false;
          	if(boxes[box_index][n - 1]) return false;
                rows[i][n - 1] = true;
                columns[j][n - 1] = true;
                boxes[box_index][n - 1] = true;
            }
    	}
    }
    return true;
}

有了解法四以後其實是更加能夠進行優化的,由於結果標記只存在是與否兩種狀態,那咱們何不用0/1位。能夠進一步減小空間那咱們須要一個9位的數字用byte類型只有1字節8位少了,因此只能用盡可能小的short類型2字節也就是16位咱們只用低9位。

如今用9位的一個數字(忽略高位)代替以前的9個元素的數組,也就沒有子數組了

先以一個4×4的面板數字1-4作例子

掃描到第一個元素1那麼它首先是第0塊,在第0塊裏存第0位(數值-1)。那麼上一個解法是子數組把裏面的第0個設爲true。如今不是子數組而是一個4位數把個位設成1。若是數字是3也就是把第2位設爲1(true)

//把第0位設爲1
 0000
+   1
-------
 0001

//把第二位設爲1
 0000
+ 100
-------
 0100

之前是數組,存一個9就找在這個數組索引8存下true,如今就是將1左移位運算8而後相加,一樣是將一個值的第8位改成1。當前存的值是不是重複之前是判斷這個地方是否已是true,如今與num進行與運算看是否是0,好比存一個4,要存在第三位

//第三位沒有值
 001000101
&     1000 
----------
 000000000

//第三位有值
 001001000
&     1000
----------
 000001000

解法六(解法五優化)

public boolean isValidSudoku(char[][] board) {
    //初始化數組
    short[] rows = new short[9];
    short[] columns = new short[9];
    short[] boxes = new short[9];

    //遍歷存值
    for (int i = 0; i < 9; i++) {
    	for (int j = 0; j < 9; j++) {
            char num = board[i][j];
            if (num != '.') {
          	num =(char)(1 << (num - '0'));
          	int box_index = (i / 3 ) * 3 + j / 3;
                if((rows[i] & num) != 0) return false;
                if((columns[j] & num) != 0) return false;
                if((boxes[box_index] & num) != 0) return false;
                rows[i] += num;
                columns[j] += num;
                boxes[box_index] += num;
            }
    	}
    }
    return true;
}

總結

前部分算法都是拋開標記暴力判斷,整理完信息而後才判斷有沒有重複信息。再以後經過值作第二層容器的索引或者key,同一個值若是是同一列(塊/行)就會存到同一個地方進而利用了第二層容器索引後能夠在存的過程就判斷是否有重,在以後這同一種思路在數據結構上有慢慢更好的選擇,最終達到一個最優解。

相關文章
相關標籤/搜索