搞定大廠演算法面試之leetcode精講11剪枝&回溯
阿新 • • 發佈:2021-11-30
大廠演算法面試之leetcode精講11剪枝&回溯
視訊講解(高效學習):點選學習
目錄:
剪枝
排除那些不符合條件的分支。提高程式的執行效率。
回溯:
一層層遞迴,嘗試搜素答案,
- 找到答案:返回結果,嘗試其他的分支
- 找不到答案:返回上一層,嘗試其他分支
回溯模版:
result = [];
function backtrack (path, list) {
if (滿足條件) {
result.push(path);
return
}
for () {
// 單層邏輯
backtrack (path, list)
// 撤銷選擇 重置狀態
}
}
回溯四部曲:
- 回溯引數
- 終止條件
- 單層遞迴邏輯
- 選擇其他分支(撤銷選擇 重置狀態)
22. 括號生成 (medium)
方法1:暴力
複雜度分析:時間複雜度O(2^2n*n)
2n
,每個位置有兩種選擇,選擇左或者右括號,驗證字串是否有效複雜度O(n)
,剪枝之後會優化,最壞的情況是O(2^2n*n)
。空間複雜度O(n)
,遞迴次數最多2n
方法2.遞迴dfs
- 思路:採用遞迴,終止條件是字串的長度等於
2n
,遞迴函式傳入構建的字串,左右括號剩餘多少,每個位置有兩種選擇,選擇左或者右括號,這裡可以進行剪枝優化,只有右括號的保有數量大於左括號的保有數量,才能選右括號,否則肯定不能構成有效括號
Js:
const generateParenthesis = (n) => { const res = []; // 輸出的結果陣列 const generate = (str, left, right) => { if (str.length == 2 * n) { // 字串構建完成 res.push(str); // 將字串加入res return; // 結束當前遞迴(結束當前搜尋分支) } if (left > 0) { // 只要左括號有剩,可以選它,繼續遞迴做選擇 generate(str + '(', left - 1, right); } if (right > left) { // 右括號的保有數量大於左括號的保有數量,才能選右括號 generate(str + ')', left, right - 1); } }; generate('', n, n); // 遞迴的入口,初始字串是空字串,初始括號數量都是n return res; };
Java:
class Solution {
List<String> res = new ArrayList<>();
public List<String> generateParenthesis(int n) {
generate(n, n, "");
return res;
}
private void generate(int left, int right, String curStr) {
if (left == 0 && right == 0) {
res.add(curStr);
return;
}
if (left > 0) {
generate(left - 1, right, curStr + "(");
}
if (right > left) {
generate(left, right - 1, curStr + ")");
}
}
}
方法3.回溯
- 思路:當左括號剩下的多,說明字串中的左括號數量少於右括號,不合法,對字串嘗試新增左括號,然後回溯,嘗試新增右括號,然後嘗試回溯
Js:
var generateParenthesis = function(n) {
if (n == 0) return []
const res = []
let track = []
backtrack(n, n, track, res)
return res
function backtrack(left, right, track, res) {
// 數量小於0,不合法
if (left < 0 || right < 0) return
// 若左括號剩下的多,說明不合法
if (right < left) return
// 所有括號用完,得到合法組合
if (left == 0 && right == 0) {
res.push(track.join(''))
return
}
// 嘗試新增左括號
track.push('(')
//這個地方一定要注意 需要拷貝一份track,也就是採用[...track], 不然會影響其他分支
backtrack(left - 1, right, [...track], res)
track.pop()
// 嘗試新增右括號
track.push(')')
backtrack(left, right - 1, [...track], res)
track.pop()
}
};
Java:
class Solution {
public List<String> generateParenthesis(int n) {
List<String> res = new ArrayList<String>();
backtrack(res, new StringBuilder(), 0, 0, n);
return res;
}
public void backtrack(List<String> res, StringBuilder cur, int left, int right, int max) {
if (cur.length() == max * 2) {
res.add(cur.toString());
return;
}
if (left < max) {
cur.append('(');
backtrack(res, cur, left + 1, right, max);
cur.deleteCharAt(cur.length() - 1);
}
if (right < left) {
cur.append(')');
backtrack(res, cur, left, right + 1, max);
cur.deleteCharAt(cur.length() - 1);
}
}
}
51. N 皇后 (hard)
方法1.回溯
- 思路:從上到下,從左到右遍歷棋盤,準備好三個set分別記錄列和兩個對角線可以攻擊到的座標,嘗試在每個空位放置皇后,放置之後更新三個可以攻擊到的set座標,然後繼續下一層遍歷,完成下一層之後,嘗試回溯當前層,也就是撤銷當前層放置的皇后,同時撤銷三個可以攻擊到的set座標,不斷回溯,直到遍歷完成,找到所有可能的解。
- 複雜度分析:時間複雜度:
O(N!)
,其中 N 是皇后數量,由於每個皇后必須位於不同列,因此已經放置的皇后所在的列不能放置別的皇后。第一個皇后有 N 列可以選擇,第二個皇后最多有 N-1列可以選擇...。空間複雜度:O(N)
,其中 N 是皇后數量,空間複雜度主要取決於遞迴呼叫層數、記錄每行放置的皇后列下標的陣列以及三個集合,遞迴呼叫層數不會超過 N,陣列的長度為 N,每個集合的元素個數都不會超過 N。
js:
const solveNQueens = (n) => {
const board = new Array(n);
for (let i = 0; i < n; i++) {
board[i] = new Array(n).fill('.');//生成board
}
const cols = new Set(); // 列集,記錄出現過皇后的列
const diag1 = new Set(); // 正對角線集
const diag2 = new Set(); // 反對角線集
const res = [];//結果陣列
const backtrack = (row) => {
if (row == n) {//終止條件
const stringsBoard = board.slice();
for (let i = 0; i < n; i++) {//生成字串
stringsBoard[i] = stringsBoard[i].join('');
}
res.push(stringsBoard);
return;
}
for (let col = 0; col < n; col++) {
// 如果當前點的所在的列,所在的對角線都沒有皇后,即可選擇,否則,跳過
if (!cols.has(col) && !diag1.has(row + col) && !diag2.has(row - col)) {
board[row][col] = 'Q'; // 放置皇后
cols.add(col); // 記錄放了皇后的列
diag2.add(row - col); // 記錄放了皇后的正對角線
diag1.add(row + col); // 記錄放了皇后的負對角線
backtrack(row + 1);
board[row][col] = '.'; // 撤銷該點的皇后
cols.delete(col); // 對應的記錄也刪一下
diag2.delete(row - col);
diag1.delete(row + col);
}
}
};
backtrack(0);
return res;
};
java:
class Solution {
public List<List<String>> solveNQueens(int n) {
List<List<String>> res = new ArrayList<List<String>>();
int[] queens = new int[n];
Arrays.fill(queens, -1);
Set<Integer> cols = new HashSet<Integer>();
Set<Integer> diag1 = new HashSet<Integer>();
Set<Integer> diag2 = new HashSet<Integer>();
backtrack(res, queens, n, 0, cols, diag1, diag2);
return res;
}
public void backtrack(List<List<String>> res, int[] queens, int n, int row, Set<Integer> cols, Set<Integer> diag1, Set<Integer> diag2) {
if (row == n) {
List<String> board = generateBoard(queens, n);
res.add(board);
} else {
for (int i = 0; i < n; i++) {
if (cols.contains(i)) {
continue;
}
int diagonal1 = row - i;
if (diag1.contains(diagonal1)) {
continue;
}
int diagonal2 = row + i;
if (diag2.contains(diagonal2)) {
continue;
}
queens[row] = i;
cols.add(i);
diag1.add(diagonal1);
diag2.add(diagonal2);
backtrack(res, queens, n, row + 1, cols, diag1, diag2);
queens[row] = -1;
cols.remove(i);
diag1.remove(diagonal1);
diag2.remove(diagonal2);
}
}
}
public List<String> generateBoard(int[] queens, int n) {
List<String> board = new ArrayList<String>();
for (int i = 0; i < n; i++) {
char[] row = new char[n];
Arrays.fill(row, '.');
row[queens[i]] = 'Q';
board.add(new String(row));
}
return board;
}
}
52. N皇后 II(hard)
方法1.位運算
js:
var totalNQueens = function (n) {
if (n < 1) return
let count = 0;
dfs(n, 0, 0, 0, 0)
return count
//n:皇后的數量
//row:當前行
//cols:放置皇后的位置
//diag1:可以攻擊的左傾斜對角線
//diag2:可以攻擊的右傾斜對角線
function dfs(n, row, cols, diag1, diag2) {
if (row >= n) {//遞迴終止 統計解法
count += 1;
return
}
//~(cols | diag1 | diag2):攻擊的位置合起來 取反之後,1的位置就是可以放置皇后的位置
//(1 << n) - 1:從右向左,大於n的位置都變成0
//(~(cols | diag1 | diag2)) & ((1 << n) - 1):從右向左,可以放置皇后的位置,大於n的位置都變成0
let bits = (~(cols | diag1 | diag2)) & ((1 << n) - 1)
while (bits) {
let p = bits & -bits//取到從右向左第一個1
bits = bits & (bits - 1)//去掉從右向左第一個1
//列和兩個對角線合上不可以放置的二進位制位
dfs(n, row + 1, cols | p, (diag1 | p) << 1, (diag2 | p) >>> 1)
}
}
};
Java:
class Solution {
public int totalNQueens(int n) {
return dfs(n, 0, 0, 0, 0);
}
public int dfs(int n, int row, int clos, int diag1, int diag2) {
if (row == n) {
return 1;
} else {
int count = 0;
int bits = ((1 << n) - 1) & (~(clos | diag1 | diag2));
while (bits != 0) {
int position = bits & (-bits);
bits = bits & (bits - 1);
count += dfs(n, row + 1, clos | position, (diag1 | position) << 1, (diag2 | position) >> 1);
}
return count;
}
}
}
36. 有效的數獨 (medium)
方法1:回溯
- 思路:準備行、列、3 * 3小方塊,三個雜湊表或者set或者9 * 9的二維陣列,都可以,只要能判重複即可,從上到下,從左到右迴圈,依次檢查行、列、3 * 3小方塊中是否有重複的數字,如果有則返回false,然後更新雜湊表或者set。
- 複雜度分析:時間複雜度:
O(1)
,數獨共有 81 個單元格,每個單元格遍歷一次即可。空間複雜度:O(1)
,數獨的大小固定,因此雜湊表的空間也是固定的。
Js:
var isValidSudoku = function(board) {
// 方向判重
let rows = {};//行
let columns = {};//列
let boxes = {};//3*3小方塊
// 遍歷數獨
for(let i = 0;i < 9;i++){
for(let j = 0;j < 9;j++){
let num = board[i][j];
if(num != '.'){//遇到有效的數字
let boxIndex = parseInt((i/3)) * 3 + parseInt(j/3);// 子數獨序號
if(rows[i+'-'+num] || columns[j+'-'+num] || boxes[boxIndex+'-'+num]){//重複檢測
return false;
}
// 方向 + 數字 組成唯一鍵值,若出現第二次,即為重複
// 更新三個物件
rows[i+'-'+num] = true;
columns[j+'-'+num] = true;
boxes[boxIndex+'-'+num] = true;
}
}
}
return true;
};
Java:
class Solution {
public boolean isValidSudoku(char[][] board) {
int[][] rows = new int[9][9];//用陣列同樣實現
int[][] columns = new int[9][9];
int[][][] boxes = new int[3][3][9];
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
char c = board[i][j];
if (c != '.') {
int index = c - '0' - 1;
rows[i][index]++;
columns[j][index]++;
boxes[i / 3][j / 3][index]++;
if (rows[i][index] > 1 || columns[j][index] > 1 || boxes[i / 3][j / 3][index] > 1) {
return false;
}
}
}
}
return true;
}
}
37. 解數獨(hard)
- 思路:迴圈行和列,嘗試在每個位置放置1-9,並檢驗合法性,包括行、列、3 * 3方塊的合法性,如果合法繼續迴圈,直到找到一個合法的解,如果不合法,則回溯狀態,並繼續嘗試其他的可能性
- 複雜度分析:同36題
js:
var solveSudoku = function(board) {
function isValid(row, col, val, board) {
let len = board.length
// 行中的數字不能重複
for(let i = 0; i < len; i++) {
if(board[row][i] === val) {
return false
}
}
// 列中的數字不能重複
for(let i = 0; i < len; i++) {
if(board[i][col] === val) {
return false
}
}
let startRow = Math.floor(row / 3) * 3
let startCol = Math.floor(col / 3) * 3
//方塊中的數字不能重複
for(let i = startRow; i < startRow + 3; i++) {
for(let j = startCol; j < startCol + 3; j++) {
if(board[i][j] === val) {
return false
}
}
}
return true
}
function backTracking() {//回溯函式
for(let i = 0; i < board.length; i++) {
for(let j = 0; j < board[0].length; j++) {//迴圈行和列
if(board[i][j] !== '.') continue
for(let val = 1; val <= 9; val++) {//嘗試在當前單元格放置1-9
if(isValid(i, j, `${val}`, board)) {//判斷放置數字的合法性
board[i][j] = `${val}`//放置數字
if (backTracking()) {//合法返回ture
return true
}
board[i][j] = `.`//不合法回溯狀態
}
}
return false//1-9的數字都不合法,返回false
}
}
return true//全部可能性都嘗試完成 返回true 說明有解
}
backTracking()
return board
};
Java:
class Solution {
public void solveSudoku(char[][] board) {
backTracking(board);
}
private boolean backTracking(char[][] board){
for (int i = 0; i < 9; i++){ // 遍歷行
for (int j = 0; j < 9; j++){ // 遍歷列
if (board[i][j] != '.'){
continue;
}
for (char k = '1'; k <= '9'; k++){ //嘗試在當前位置放置1-9
if (isValid(i, j, k, board)){
board[i][j] = k;//放置數字
if (backTracking(board)){ //合法返回ture
return true;
}
board[i][j] = '.';
}
}
return false;//1-9的數字都不合法,返回false
}
}
return true;//全部可能性都嘗試完成 返回true 說明有解
}
private boolean isValid(int row, int col, char val, char[][] board){
// 同行是否重複
for (int i = 0; i < 9; i++){
if (board[row][i] == val){
return false;
}
}
// 同列是否重複
for (int j = 0; j < 9; j++){
if (board[j][col] == val){
return false;
}
}
// 小方塊中的元素是否重複
int startRow = (row / 3) * 3;
int startCol = (col / 3) * 3;
for (int i = startRow; i < startRow + 3; i++){
for (int j = startCol; j < startCol + 3; j++){
if (board[i][j] == val){
return false;
}
}
}
return true;
}
}
79. 單詞搜尋(medium)
-
思路:從上到下,左到右遍歷網格,每個座標遞迴呼叫
check(i, j, k)
函式,i,j表示網格座標,k表示word的第k個字元,如果能搜尋到第k個字元返回true,否則返回false,check函式的終止條件有2種情況- 如果i,j位置的字元和字串位置k的字元不相等,則這條搜尋路徑搜尋失敗 返回false
- 如果搜尋到了字串的結尾,則找到了網格中的一條路徑,這條路徑上的字元正好可以組成字串s
兩種情況都不滿足則把當前網格節點加入
visited
陣列,visited
表示節點已經訪問過了,然後順著當前網格座標的四個方向繼續嘗試,如果沒找到k開始的子串,則回溯狀態visited[i] [j] = false
,繼續後面的嘗試。 -
複雜度分析:時間複雜度
O(MN⋅3^L)
,M,N 為網格的長度與寬度,L 為字串 word 的長度,第一次呼叫check
函式的時候,進行4個方向的檢查,其餘座標的節點都是3個方向檢查,走過來的分支不會反方向回去,所以check
函式的時間複雜度是3^L
,而網格有M*N
個座標,且存在剪枝,所以最壞的情況下時間複雜度是O(MN⋅3^L)
。空間複雜度是O(MN)
,visited
陣列空間是O(MN)
,check
遞迴棧的最大深度在最壞的情況下是O(MN)
方法1:回溯
Js:
var exist = function(board, word) {
const h = board.length, w = board[0].length;//網格的長和寬
const directions = [[0, 1], [0, -1], [1, 0], [-1, 0]];//方向陣列
const visited = new Array(h);//標記是否訪問過的陣列
for (let i = 0; i < visited.length; ++i) {//初始化visited陣列
visited[i] = new Array(w).fill(false);
}
const check = (i, j, s, k) => {//檢查從網格i,j出發是否能搜尋到0-k的字元組成的子串
//如果i,j位置的字元和第k個的字元不相等,則這條搜尋路徑搜尋失敗 返回false
if (board[i][j] != s.charAt(k)) {
return false;
//如果搜尋到了字串的結尾,則找到了網格中的一條路徑,這條路徑上的字元正好可以組成字串s
} else if (k == s.length - 1) {
return true;
}
visited[i][j] = true;//標記i,j被訪問過了
let result = false;
for (const [dx, dy] of directions) {//向i,j的四個方向繼續嘗試尋找
let newi = i + dx, newj = j + dy;
if (newi >= 0 && newi < h && newj >= 0 && newj < w) {//新的座標位置合法檢查
if (!visited[newi][newj]) {//新的座標不能存在於visited中,也就是不能是訪問過的
const flag = check(newi, newj, s, k + 1);//繼續檢查新的座標
if (flag) {//如果在網格中找到了字串 則跳過迴圈
result = true;
break;
}
}
}
}
visited[i][j] = false;//回溯狀態
return result;//返回結果
}
for (let i = 0; i < h; i++) {
for (let j = 0; j < w; j++) {
const flag = check(i, j, word, 0);
if (flag) {
return true;
}
}
}
return false;
};
Java:
class Solution {
public boolean exist(char[][] board, String word) {
int h = board.length, w = board[0].length;
boolean[][] visited = new boolean[h][w];
for (int i = 0; i < h; i++) {
for (int j = 0; j < w; j++) {
boolean flag = check(board, visited, i, j, word, 0);
if (flag) {
return true;
}
}
}
return false;
}
public boolean check(char[][] board, boolean[][] visited, int i, int j, String s, int k) {
if (board[i][j] != s.charAt(k)) {
return false;
} else if (k == s.length() - 1) {
return true;
}
visited[i][j] = true;
int[][] directions = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
boolean result = false;
for (int[] dir : directions) {
int newi = i + dir[0], newj = j + dir[1];
if (newi >= 0 && newi < board.length && newj >= 0 && newj < board[0].length) {
if (!visited[newi][newj]) {
boolean flag = check(board, visited, newi, newj, s, k + 1);
if (flag) {
result = true;
break;
}
}
}
}
visited[i][j] = false;
return result;
}
}
46. 全排列 (medium)
- 思路:準備path陣列,存放每一個回溯遞迴的分支中的數字排列,呼叫回溯函式 傳入nums,nums長度,used陣列,used表示已經使用的數字,回溯函式中迴圈nums中的數,每層迴圈將nums中的元素加入path中,然後遞迴呼叫回溯函式,呼叫完成之後,回溯之前的狀態,當path陣列的長度和nums的長度相同就找到了一種排列。
- 複雜度:時間複雜度
O(n*n!)
。空間複雜度O(n)
,遞迴棧深度
js:
var permute = function(nums) {
const res = [], path = [];
backtracking(nums, nums.length, []);//呼叫回溯函式 傳入nums,nums長度,used陣列
return res;
function backtracking(n, k, used) {
if(path.length === k) {//遞迴終止條件
res.push(Array.from(path));
return;
}
for (let i = 0; i < k; i++ ) {
if(used[i]) continue;//已經使用過了就跳過本輪迴圈
path.push(n[i]);
used[i] = true;
backtracking(n, k, used);//遞迴
path.pop();//回溯 將push進的元素pop出來 然後標記成未使用 繼續其他分支
used[i] = false;
}
}
};
java:
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
boolean[] used;
public List<List<Integer>> permute(int[] nums) {
if (nums.length == 0){
return result;
}
used = new boolean[nums.length];
permuteHelper(nums);
return result;
}
private void permuteHelper(int[] nums){
if (path.size() == nums.length){
result.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++){
if (used[i]){
continue;
}
used[i] = true;
path.add(nums[i]);
permuteHelper(nums);
path.removeLast();
used[i] = false;
}
}
}
77. 組合 (medium)
- 思路:回溯函式傳入n,k和選擇的元素位置startIndex,在每層遞迴中,從startIndex開始迴圈到
n - (k - path.length) + 1
的位置,將這些數加入path,然後startIndex加1,繼續遞迴函式進入下一個分支,完成呼叫之後回溯狀態,當path的長度等於k的時候終止這層分支,加入結果中。 - 複雜度:時間複雜度:
O(C(n, k) * k)
,列舉結果總數為C(n, k)
,每次得到一個結果需要O(k)
時間。空間複雜度:O(n)
,最大是n層遞迴棧。
js:
const combine = (n, k) => {
const res = [];
const helper = (startIndex, path) => { //startIndex表示搜尋的起點位置 path是每條分支的一個組合)
if (path.length == k) {
res.push(path.slice()); //需要拷貝一份 避免受其他分支的影響
return;
}
for (let i = startIndex; i <= n - (k - path.length) + 1; i++) {//剪枝
path.push(i); //加入path
helper(i + 1, path); //下一層遞迴
path.pop(); //回溯狀態
}
};
helper(1, []); //遞迴入口
return res;
}
java:
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
combineHelper(n, k, 1);
return result;
}
private void combineHelper(int n, int k, int startIndex){
if (path.size() == k){
result.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++){
path.add(i);
combineHelper(n, k, i + 1);
path.removeLast();
}
}
}
17. 電話號碼的字母組合 (medium)
方法1.dfs+回溯
- 思路:深度優先遍歷,遍歷函式傳入每一層形成的字串和一個指向字元的位置指標,打給你指標的位置到達字串的結尾時,將形成的字串加入結果陣列,遞迴的每一層遍歷這一層的數字對應的字元,然後傳入新的字元,指標向後移動一次,不斷遞迴
- 複雜度:時間複雜度
O(3^m * 4^n)
,m,n分別是三個字母和四個字母對應的數字個數。空間複雜度O(m+n)
,遞迴棧的深度,最大為m+n
js:
//輸入:digits = "23"
//輸出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
var letterCombinations = (digits) => {
if (digits.length == 0) return [];
const res = [];
const map = {//建立電話號碼和字母的對映關係
2: "abc",
3: "def",
4: "ghi",
5: "jkl",
6: "mno",
7: "pqrs",
8: "tuv",
9: "wxyz",
};
const dfs = (curStr, i) => {//curStr是遞迴每一層的字串,i是掃描的指標
if (i > digits.length - 1) {//邊界條件,遞迴的出口
res.push(curStr); //其中一個分支的解推入res
return; //結束遞迴分支,進入另一個分支
}
const letters = map[digits[i]]; //取出數字對應的字母
for (const l of letters) {
//進入不同字母的分支
dfs(curStr + l, i + 1); //引數傳入新的字串,i右移,繼續遞迴
}
};
dfs("", 0); // 遞迴入口,傳入空字串,i初始為0的位置
return res;
};
java:
class Solution {
String[] map = { " ", "*", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" };
public List<String> letterCombinations(String digits) {
if (digits == null || digits.length() == 0) {
return new ArrayList<>();
}
dfs(digits, new StringBuilder(), 0);
return res;
}
List<String> res = new ArrayList<>();
void dfs(String digits, StringBuilder curStr, int index) {
if (index == digits.length()) {
res.add(curStr.toString());
return;
}
char c = digits.charAt(index);
int pos = c - '0';
String map_string = map[pos];
for (int i = 0; i < map_string.length(); i++) {
curStr.append(map_string.charAt(i));
dfs(digits, curStr, index + 1);
curStr.deleteCharAt(curStr.length() - 1);
}
}
}
方法2.bfs
- 思路:用佇列廣度優先遍歷,先迴圈數字陣列,然後取出對應的字母,與當前層的字串組成新的字串加入佇列,遍歷完成之後,佇列的最後一層就是解。
- 複雜度:時間複雜度
O(3^m * 4^n)
,m,n分別是三個字元和四個字母對應的陣列個數。空間複雜度O(3^m * 4^n)
,佇列的空間大小,最大為3^m * 4^n
js:
var letterCombinations = (digits) => {
if (digits.length == 0) return [];
const map = {
2: "abc",
3: "def",
4: "ghi",
5: "jkl",
6: "mno",
7: "pqrs",
8: "tuv",
9: "wxyz",
};
const queue = [];
queue.push("");
for (let i = 0; i < digits.length; i++) {//迴圈數字的每個字元
const levelSize = queue.length; //當前層的節點個數
for (let j = 0; j < levelSize; j++) {
const curStr = queue.shift(); //當前層的字串
const letters = map[digits[i]];//獲取數字對應的字母字元
for (const l of letters) {
queue.push(curStr + l); //新生成的字串入列
}
}
}
return queue; //最後一層生成的字串就是解
};
java:
class Solution {
public List<String> letterCombinations(String digits) {
if (digits == null || digits.length() == 0) {
return new ArrayList<String>();
}
String[] map = { " ", "*", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" };
List<String> res = new ArrayList<>();
res.add("");
for (int i = 0; i < digits.length(); i++) {
String letters = map[digits.charAt(i) - '0'];
int levelSize = res.size();
for (int j = 0; j < levelSize; j++) {
String tmp = res.remove(0);
for (int k = 0; k < letters.length(); k++) {
res.add(tmp + letters.charAt(k));
}
}
}
return res;
}
}
78. 子集 (medium)
- 思路:回溯函式傳入字元開始的位置startIndex,不斷遞迴,每一層startIndex加1,當一個分支結束之後在,開始回溯,進入另一個分支。
- 複雜度:時間複雜度
O(n*2^n)
,如圖遞迴出來的狀態是2^n
個狀態,每個狀態構建path陣列複雜度是O(n)
。空間複雜度O(n)
,也就是遞迴棧的空間
js:
//例子:nums = [1,2,3]
var subsets = function(nums) {
let result = []//存放結果
let path = []//存放一個分支的結果
function backtracking(startIndex) {//startIndex字元遞迴開始的位置
result.push(path.slice())//path.slice()斷開和path的引用關係
for(let i = startIndex; i < nums.length; i++) {//從startIndex開始遞迴
path.push(nums[i])//當前字元推入path
backtracking(i + 1)//startIndex向後移動一個位置 繼續遞迴
path.pop()//回溯狀態
}
}
backtracking(0)
return result
};
java:
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> subsets(int[] nums) {
if (nums.length == 0){
result.add(new ArrayList<>());
return result;
}
backtracking(nums, 0);
return result;
}
private void backtracking(int[] nums, int startIndex){
result.add(new ArrayList<>(path));
if (startIndex >= nums.length){
return;
}
for (int i = startIndex; i < nums.length; i++){
path.add(nums[i]);
backtracking(nums, i + 1);
path.removeLast();
}
}
}
473. 火柴拼正方形 (medium)
- 思路 :排序nums陣列,減少回溯的次數。不斷嘗試將nums中的元素放入4個桶中,如果都能放的下,則能拼成正方形
js:
//例子:[1,2,2,2,1]
var makesquare = function (nums) {
function backtrack(i, nums, edge, bucket) {
if (i >= nums.length) {//遞迴結束條件
return true;
}
for (let j = 0; j < 4; j++) {//迴圈4個桶
if (bucket[j] + nums[i] > edge) {//這個桶裝不下 繼續找下一個桶
continue;
}
bucket[j] += nums[i];//將當前元素加入桶中
if (backtrack(i + 1, nums, edge, bucket)) {//索引i加1 繼續遞迴下一個nums中的元素
return true;//下一個元素能放進桶中
}
bucket[j] -= nums[i];//回溯狀態
}
return false;//迴圈結束都沒放進合適的桶 那不能構成正方形
}
if (nums.length < 4) {//nums長度小於4 直接不能構成正方形
return false;
}
let sum = 0;
for (let i = 0; i < nums.length; i++) {
sum += nums[i];
}
if (sum % 4) {//nums的和不能整除4 不能構成正方行
return false;
}
nums.sort((a, b) => b - a);//排序nums
let bucket = Array(4).fill(0);//準備4個桶
return backtrack(0, nums, sum / 4, bucket);//傳入nums元素的索引i,nums,一個邊長,和桶bucket
};