主頁 >  其他 > 演算法 | 第4章 樹與圖相關《程式員面試金典》

演算法 | 第4章 樹與圖相關《程式員面試金典》

2021-10-26 06:29:32 其他

目錄
  • 前言
  • 0. *經驗總結
    • 0.1 程式員面試金典 P85
    • 0.2 各種樹的特點
      • 1. 二叉搜索樹(二叉排序樹)
      • 2. 平衡二叉樹
      • 3. 完整二叉樹
      • 4. 滿二叉樹
      • 5. 完滿二叉樹
      • 6. 二叉堆(小頂堆和大頂堆)
      • 7. 單次查找樹(前序樹)
      • 8. 順序存盤二叉樹
      • 9. 線索化二叉樹
      • 10. 霍夫曼樹
      • 11. B樹、B+樹和B*樹
    • 0.3 樹的三種遍歷方式
    • 0.4 圖的表示形式與搜索
  • 1. 節點間通路 [medium]
    • 1.1 考慮點
    • 1.2 解法
      • 1.2.1 廣度優先遍歷法(優)
      • 1.2.2 深度優先遍歷法
      • 1.2.3 鄰接矩陣的冪的性質
      • 1.2.4 當重復邊為雙向邊時
  • 2. 最小高度樹 [easy]
    • 2.1 考慮點
    • 2.2 解法
      • 2.2.1 遞回法(優)
      • 2.2.2 BFS廣度優先遍歷
  • 3. 特定深度節點鏈表 [medium]
    • 3.1 考慮點
    • 3.2 解法
      • 3.2.1 使用堆疊暴力法
      • 3.2.2 使用堆疊暴力法(代碼優化縮短)
      • 3.2.3 遞回法(優)
  • 4. 檢查平衡性 [easy]
    • 4.1 考慮點
    • 4.2 解法
      • 4.2.1 自頂向下的遞回
      • 4.2.2 自頂向下的遞回
      • 4.2.3 自底向上的遞回(優)
  • 5. 合法二叉搜索樹 [medium]
    • 5.1 考慮點
    • 5.2 解法
      • 5.2.1 遞回法
      • 5.2.2 中序遍歷非遞回
      • 5.2.3 中序遍歷遞回(優)
  • 6. 后繼者 [medium]
    • 6.1 考慮點
    • 6.2 解法
      • 6.2.1 中序遍歷遞回法
      • 6.2.2 中序遍歷非遞回法(優)
  • 8. 首個共同祖先 [medium]
    • 8.1 考慮點
    • 8.2 解法
      • 8.2.1 深度優先遍歷
      • 8.2.2 深度優先遍歷的簡便寫法(優)
  • 9. 二叉搜索樹序列 [hard]
    • 9.1 考慮點
    • 9.2 解法
      • 9.2.1 回溯法+廣度優先遍歷
  • 10. 檢查子樹 [medium]
    • 10.1 考慮點
    • 10.2 解法
      • 10.2.1 遞回法(優)
  • 12. 求和路徑 [medium]
    • 12.1 考慮點
    • 12.2 解法
      • 12.2.1 暴力法(優)
      • 12.2.2 回溯法
  • 最后


前言

本系列筆記主要記錄筆者刷《程式員面試金典》演算法的一些想法與經驗總結,按專題分類,主要由兩部分構成:經驗值點和經典題目,其中重點放在經典題目上;

本章有10題,標號到12,沒有第7和第11題;


0. *經驗總結

0.1 程式員面試金典 P85

  • 樹的基本組成單位是節點node,節點可以封裝左右指標、父節點指標等;
  • 可以使用一個名為Tree的類來封裝節點,但在面試中不常用;如果使用Tree類能使代碼簡單或完善,可以使用Tree類;
  • 注意區分以下概念,以及一些樹的基本特點與遍歷特點《詳情見第0.2點 各種樹的特點》:
    • 樹與二叉樹;
    • 二叉樹和二叉搜索樹(又稱二叉排序樹);
    • 平衡與不平衡;
    • 完整二叉樹;
    • 滿二叉樹;
    • 完美二叉樹;
    • 二叉堆(小頂堆和大頂堆)
    • 單次查找樹(前序樹)
    • 順序存盤二叉樹;
    • 線索化二叉樹;
    • 霍夫曼樹;
    • B樹、B+樹和B*樹;

0.2 各種樹的特點

1. 二叉搜索樹(二叉排序樹)

二叉搜索樹(二叉排序樹)

  • 全部左邊子孫節點 <= n <= 全部右邊子孫節點;
  • 碰到二叉樹時,很多面試者會假定面試官問的是二叉搜索樹,此時要務必問清是否的二叉搜索樹;

2. 平衡二叉樹

  • 它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,并且左右兩個子樹都是一棵平衡二叉樹;
  • 樹是否平衡需要跟面試官確認;
  • 思考方向可以有:“平衡”樹實際上多半意味著“不是非常不平衡的樹”,其平衡性足以保證執行insert和find操作可以在O(log n)的時間復雜度內完成,但不一定是嚴格意義上的平衡樹;
  • 平衡二叉樹的常用實作方法有紅黑樹、AVL、替罪羊樹、Treap、伸展樹等;

3. 完整二叉樹

完整二叉樹

  • 除最后一層,其它層被填滿,并且最后一層節點從左到右填充;
  • 中序遍歷結果按順序排列;

4. 滿二叉樹

滿二叉樹

  • 每個節點都有0個或兩個子節點,不存在只有一個子節點的節點;

5. 完滿二叉樹

完滿二叉樹

  • 完滿二叉樹既是完整二叉樹,又是滿二叉樹;
  • 正好有2k-1個節點;

6. 二叉堆(小頂堆和大頂堆)

二叉堆(小頂堆和大頂堆)

  • 一個二叉堆是一個完整二叉樹,每個節點值小于其左右子節點;
  • 最小堆插入元素,總是從最底部、最右邊節點開始,通過與其祖先節點交換來向上傳遞較小值;
  • 洗掉最小堆最小元素,將堆中最后一個元素放在頂節點,通過與子節點交換來向下傳遞較大值;
  • 上述兩個演算法時間復雜度為O(log n);

7. 單次查找樹(前序樹)

單次查找樹(前序樹)

  • *節點(有時稱空節點)代表一個完整單次;
  • 相比散串列可以識別字串是否是任何有效單次的前綴,可以在O(K)的時間復雜度內檢查字串是否為有效前綴;
  • 涉及一組有效單詞的問題可以使用單次查找樹進行優化;

8. 順序存盤二叉樹

順序存盤二叉樹

  • 從資料存盤來看,陣列存盤方式和樹的存盤方式可以相互轉換,即陣列可以轉換成樹,樹也可以轉換成陣列;
  • 順序二叉樹通常只考慮完全二叉樹;
  • 第n個元素的左子節點為 2*n+1;
  • 第n個元素的右子節點為 2*n+2;
  • 第n個元素的父節點為 (n-1)/2;
  • n : 表示二叉樹中的第幾個元素(按0開始編號如圖所示);

9. 線索化二叉樹

線索化二叉樹

  • n個結點的二叉鏈表中含有 2n-(n-1)=n+1 個空指標域,利用二叉鏈表中的空指標域,存放指向該結點在某種遍歷次序下的前驅和后繼結點的指標(這種附加的指標稱為"線索");

10. 霍夫曼樹

霍夫曼樹

  • 給定n個權值作為n個葉子結點,構造一棵二叉樹,若該樹的帶權路徑長度(wpl)達到最小,稱這樣的二叉樹為最優二叉樹,也稱為哈夫曼樹(Huffman Tree);

11. B樹、B+樹和B*樹

B樹
B+樹
B*樹

0.3 樹的三種遍歷方式

中序遍歷

void inOrderTraversal(TreeNode node){
	if(node != null){
		inOrderTraversal(node.left);
		visit(node);
		inOrderTraversal(node.right);
	}
}

前序遍歷

void preOrderTraversal(TreeNode node){
	if(node != null){
		visit(node);
		preOrderTraversal(node.left);
		preOrderTraversal(node.right);
	}
}

后序遍歷

void postOrderTraversal(TreeNode node){
	if(node != null){
		postOrderTraversal(node.left);
		postOrderTraversal(node.right);
		visit(node);
	}
}

0.4 圖的表示形式與搜索

圖的表示方式

  • 鄰接鏈表法;
  • 鄰接矩陣法;
  • 鄰接矩陣法使用BFS搜索,效率會比較低,因為需要迭代所有結點以便找到相鄰節點;

圖的搜索

  • 深度優先搜索(DFS);
    • 訪問圖中所有節點,或者訪問最少節點直至找到某節點,DFS一般比較簡單;
  • 廣度優先搜索(BFS);
    • 如果想找到兩個節點的最短路徑(或任意路徑),BFS一般比較適宜;
    • BFS是通過佇列實作;
  • 雙向搜索;
    • 雙向搜索用于查找起始節點和目的節點間的最短路徑;
    • 本質上是從起始節點和目的節點同時開始的兩個廣度優先搜索;
    • 當兩個搜索相遇時,即找到一條路徑

圖的搜索
雙向搜索

深度優先搜索(DFS):

  • 前序和樹遍歷的其他形式都是一種DFS;
void search(Node root){
	if(root == null){
		return;
	}
	visit(root);
	root.visited = true;
	for each(Node n in root.adjacent){
		if(n.visited == false){
			search(n);
		}
	}
}

廣度優先搜索(BFS):

void search(Node root){
	Queue queue = new Queue();
	root.marked = true;
	queue.enqueue(root); //加入隊尾
	
	while(!queue.isEmpty()){
		Node r = queue.dequeue(); //從佇列頭部洗掉
		visit(r);
		for each(Node n in r.adjacent){
			if(n.mark == false){
				n.marked = true;
				queue.enqueue(n);
			}
		}
	}
}


1. 節點間通路 [medium]

節點間通路

1.1 考慮點

  • 詢問面試官重復的邊是否是雙向邊,本題中不是,筆者以為是雙向邊,第五種解法給出思路;
  • 可以跟面試官討論廣度優先搜索和深度優先搜索的利弊:
    • 廣度優先搜索:適合查找最短路徑;
    • 深度優先搜索:實作比較簡單,遞回即可;在訪問鄰近節點之前,可能會深度遍歷其中一個鄰近節點;

1.2 解法

1.2.1 廣度優先遍歷法(優)

public boolean findWhetherExistsPath(int n, int[][] graph, int start, int target) {
    Queue<Integer> queue = new LinkedList<>();
    boolean[] isVisit = new boolean[n];
    isVisit[start] = true;
    queue.offer(start);
    while( !queue.isEmpty() ){
        int thisNode = queue.poll();
        int toNode;
        for(int i = 0; i < graph.length; i++){
            if(graph[i][0] == thisNode){
                toNode = graph[i][1];
                if(toNode == target){
                    return true;
                }
                if(!isVisit[toNode]){
                    isVisit[toNode] = true;
                    queue.offer(toNode);
                }
            }
        }
    }
    return false;
}
  • 執行時間:91.21%;記憶體消耗:92.42%;
  • 適用于n較少的時候,當n過大會超時;

1.2.2 深度優先遍歷法

private boolean[] visited = null;
public boolean findWhetherExistsPath(int n, int[][] graph, int start, int target) {
    this.visited = new boolean[graph.length];
    return helper(graph, start, target);
}

private boolean helper(int[][] graph, int start, int target) {
    for (int i = 0; i < graph.length; ++i) {
        // 確保當前路徑未被訪問(該判斷主要是為了防止圖中自環出現死回圈的情況)
        if (!visited[i]) {
            // 若當前路徑起點與終點相符,則直接回傳結果
            if (graph[i][0] == start && graph[i][1] == target) {
                return true;
            }
            // 設定訪問標志
            visited[i] = true;
            // DFS關鍵代碼,思路:同時逐漸壓縮搜索區間
            if (graph[i][1] == target && helper(graph, start, graph[i][0])) {
                return true;
            }
            // 清除訪問標志
            visited[i] = false;
        }
    }
    return false;
}
  • 執行時間:81.17%;記憶體消耗:97.41%;
  • 首先設定訪問狀態陣列
  • 使用DFS「深度優先搜索」進行遞回搜索,逐漸壓縮搜索區間,直至最終找到start與target在同一個路徑內,則說明查找成功;

1.2.3 鄰接矩陣的冪的性質

public boolean findWhetherExistsPath(int n, int[][] graph, int start, int target) {
    int[][] adjacentMatrix = new int[n][n];
    for (int[] edge : graph) {
        adjacentMatrix[edge[0]][edge[1]] = 1;
    }
    for (int i = 1; i <= n; i++) {
        int[][] matrix = matrixPow(adjacentMatrix, i);
        if (matrix[start][target] != 0) {
            return true;
        }
    }
    return false;
}

public int[][] matrixPow(int[][] matrix, int n) {
    int size = matrix.length;
    int[][] res = new int[size][];
    for (int i = 0; i < size; i++) {
        res[i] = Arrays.copyOf(matrix[i], size);
    }
    for (int i = 1; i < n; i++) {
        for (int row = 0; row < size; row++) {
            int[] tmp = new int[size];
            for (int col = 0; col < size; col++) {
                for (int j = 0; j < size; j++) {
                    tmp[col] += res[row][j] * matrix[j][col];
                }
            }
            res[row] = Arrays.copyOf(tmp, size);
        }
    }
    return res;
}
  • 在本測驗用例中超時;
  • 用到鄰接矩陣的冪的性質;

1.2.4 當重復邊為雙向邊時

public boolean findWhetherExistsPath(int n, int[][] graph, int start, int target) {
    int[] count = new int[n*(n-1)/2];
    for(int i = 0; i < graph.length; i++){
        int mark = (2*n-1-graph[i][0])*graph[i][0]/2-1+graph[i][1]-graph[i][0];
        count[mark]++;
        if(count[mark] == 2){
            int cache = graph[i][0];
            graph[i][0] = graph[i][1];
            graph[i][1] = cache;
        }
    }

    Queue<Integer> queue = new LinkedList<>();
    boolean[] isVisit = new boolean[n*(n-1)/2];
    queue.offer(start);

    while( !queue.isEmpty() ){
        int thisNode = queue.poll();
        int toNode;
        for(int i = 0; i < graph.length; i++){
            int mark = (2*n-1-graph[i][0])*graph[i][0]/2-1+graph[i][1]-graph[i][0];
            if(graph[i][0] == thisNode && !isVisit[mark]){
                toNode = graph[i][1];
                if(toNode == target){
                    return true;
                }
                queue.offer(toNode);
            }
        }
    }
    return false;
}
  • 相比前面做法增加了個方向倒轉,也就是把重復邊的方向倒轉;

2. 最小高度樹 [easy]

最小高度樹

2.1 考慮點

  • 要創建最小生成樹,就必須讓左右子樹的節點數量盡可能接近;

2.2 解法

2.2.1 遞回法(優)

public TreeNode sortedArrayToBST(int[] nums) {
    if(nums.length == 0){
        return null;
    }
    int l = 0;
    int r = nums.length-1;
    return recur(l, r, nums);
}

public TreeNode recur(int l, int r, int[] nums){
    if(l > r){
        return null;
    }
    int mid = (l+r)/2;
    int headVal = nums[mid];
    TreeNode head = new TreeNode(headVal);
    head.left  = recur(l, mid-1, nums);
    head.right = recur(mid+1, r, nums);
    return head;
}
  • 執行時間:100.00%;記憶體消耗:53.37%;
  • 時間復雜度:O(n),陣列中的元素都使用1次,時間復雜度為O(n);
  • 空間復雜度:O(log n ),遞回使用堆疊輔助空間,空間復雜度O(log n );
  • 注意遞回結束條件,沒有等于是因為遞回里有對mid增減進行操作;
  • 注意遞回的引數lr,表示需要遞回的范圍,不包括mid頭結點;

2.2.2 BFS廣度優先遍歷

public TreeNode sortedArrayToBST(int[] num) {
    if (num.length == 0)
        return null;
    Queue<int[]> rangeQueue = new LinkedList<>();
    Queue<TreeNode> nodeQueue = new LinkedList<>();
    int lo = 0;
    int hi = num.length - 1;
    int mid = (lo + hi) >> 1;
    TreeNode node = new TreeNode(num[mid]);
    rangeQueue.add(new int[]{lo, mid - 1});
    rangeQueue.add(new int[]{mid + 1, hi});
    nodeQueue.add(node);
    nodeQueue.add(node);
    while (!rangeQueue.isEmpty()) {
        int[] range = rangeQueue.poll();
        TreeNode currentNode = nodeQueue.poll();
        lo = range[0];
        hi = range[1];
        if (lo > hi) {
            continue;
        }
        mid = (lo + hi) >> 1;
        int midValue = https://www.cnblogs.com/dlhjw/p/num[mid];
        TreeNode newNode = new TreeNode(midValue);
        if (midValue > currentNode.val)
            currentNode.right = newNode;
        else
            currentNode.left = newNode;
        if (lo < hi) {
            rangeQueue.add(new int[]{lo, mid - 1});
            rangeQueue.add(new int[]{mid + 1, hi});
            nodeQueue.add(newNode);
            nodeQueue.add(newNode);
        }
    }
    return node;
}
  • 執行時間:100.00%;記憶體消耗:5.01%;
  • 把陣列不停的分為兩部分,保存在佇列中,然后不停的出隊,創建結點;

3. 特定深度節點鏈表 [medium]

特定深度節點鏈表

3.1 考慮點

  • 不需要逐一遍歷每一層,可以用任意方式遍歷整棵樹,只需要記住節點位于哪一層即可;

3.2 解法

3.2.1 使用堆疊暴力法

public ListNode[] listOfDepth(TreeNode tree) {
    if(tree == null){
        return null;
    }
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(tree);
    int floor = 1; //第x-1層節點個數
    List<ListNode> listArr = new ArrayList<>();
    //當佇列不為空
    while(!queue.isEmpty()){
        //按層處理
        int count = 0; //count用來儲存第x層節點個數
        ListNode head = null;
        ListNode cur = null;
        for(int i = 0; i < floor; i++){
            TreeNode node = queue.poll();
            //創建鏈表
            if(i == 0){
                head = new ListNode(node.val);
                cur = head;
            } else {
                ListNode nodeL = new ListNode(node.val);
                cur.next = nodeL;
                cur = nodeL;
            }
            //加入佇列
            if( node.left != null){
                count++;
                queue.offer(node.left);
            }
            if(node.right != null){
                count++;
                queue.offer(node.right);
            }
        }
        listArr.add(head);
        floor = count; //替換floor
    }
    ListNode[] list = new ListNode[listArr.size()];
    for(int i = 0; i < listArr.size(); i++){
        list[i] = listArr.get(i);
    }
    return list;
}
  • 執行時間:89.59%;記憶體消耗:75.08%;
  • 需要注意幾個細節:
    • ListNode cur = null:需要對cur初始化;
    • floor = count:記得要替換floor;
  • 由于事先不知道陣列個數,先用一個ArrayList裝起來,然后再轉換成陣列;

3.2.2 使用堆疊暴力法(代碼優化縮短)

public ListNode[] listOfDepth(TreeNode tree) {
    if(tree == null){
        return null;
    }
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(tree);
    List<ListNode> listArr = new ArrayList<>();
    //當佇列不為空
    while(!queue.isEmpty()){
        //按層處理
        int size = queue.size();
        ListNode head = new ListNode(0);
        ListNode cur = head;
        for(int i = 0; i < size; i++){
            TreeNode node = queue.poll();
            //創建鏈表
            cur.next = new ListNode(node.val);
            //加入佇列
            if( node.left != null){
                queue.offer(node.left);
            }
            if(node.right != null){
                queue.offer(node.right);
            }
            cur = cur.next;
        }
        listArr.add(head.next);
    }
    return listArr.toArray(new ListNode[] {});
}
  • 執行時間:89.59%;記憶體消耗:25.97%;
  • 思路同上,更多呼叫java的api簡化代碼;

3.2.3 遞回法(優)

public ListNode[] listOfDepth(TreeNode tree) {
    List<ListNode> list = new ArrayList<>();
    dfs(list, tree, 1);
    ListNode[] arr = new ListNode[list.size()];
    for (int i = 0; i < arr.length; i++) {
        arr[i] = list.get(i);
    }
    return arr;
}
private void dfs(List<ListNode> list, TreeNode node, int deep) {
    if (node == null) {
        return;
    }
    if (deep > list.size()) {
        list.add(new ListNode(node.val));
    } else {
        ListNode n = list.get(deep - 1);
        while (n.next != null) {
            n = n.next;
        }
        n.next = new ListNode(node.val);
    }
    dfs(list, node.left, deep + 1);
    dfs(list, node.right, deep + 1);
}
  • 執行時間:100.00%;記憶體消耗:96.63%;
  • 定義樹深度為deep,同一個深度的保存到同一個ListNode;

4. 檢查平衡性 [easy]

檢查平衡性

4.1 考慮點

  • 其實,只需要一個checkHeight函式即可,它既可以計算高度,也可以平衡檢查,可以使用整數回傳值表示兩者;

4.2 解法

4.2.1 自頂向下的遞回

public boolean isBalanced(TreeNode root) {
    if(root == null){
        return true;
    }
    int deep = 1;
    int rootLeftDeep = findDeep(root.left, deep);
    int rootRightDeep = findDeep(root.right, deep);
    if(rootLeftDeep == -1 || rootRightDeep == -1){
        return false;
    }
    int result = Math.abs(rootLeftDeep - rootRightDeep);
    return result<2; 
}

public int findDeep(TreeNode node, int deep){
    if(node == null){
        return deep-1;
    }
    if(node.left == null && node.right == null){
        return deep;
    }
    deep++;
    int leftDeep = findDeep(node.left,deep);
    int rightDeep = findDeep(node.right,deep);
    int deepDiff = Math.abs(leftDeep-rightDeep);
    return deepDiff>1 ? -1 : Math.max(leftDeep,rightDeep);
}
  • 執行時間:88.57%;記憶體消耗:20.48%;
  • 方法雖然可行,但不高效,Math.abs()方法會被反復呼叫計算同一個節點的高度;

4.2.2 自頂向下的遞回

public boolean isBalanced(TreeNode root) {
    if (root == null) {
        return true;
    } else {
        return Math.abs(height(root.left) - height(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right);
    }
}

public int height(TreeNode root) {
    if (root == null) {
        return 0;
    } else {
        return Math.max(height(root.left), height(root.right)) + 1;
    }
}
  • 執行時間:88.57%;記憶體消耗:74.16%;

  • 時間復雜度:O(n2),其中 n 是二叉樹中的節點個數,最壞情況下,二叉樹是滿二叉樹,需要遍歷二叉樹中的所有節點,時間復雜度是 O(n),對于節點 p,如果它的高度是 d,則 height(p) 最多會被呼叫 d 次(即遍歷到它的每一個祖先節點時),對于平均的情況,一棵樹的高度 h 滿足 O(h)=O(logn),因為 d≤h,所以總時間復雜度為 O(nlogn),對于最壞的情況,二叉樹形成鏈式結構,高度為 O(n),此時總時間復雜度為 O(n2);

  • 空間復雜度:O(n),其中 n 是二叉樹中的節點個數,空間復雜度主要取決于遞回呼叫的層數,遞回呼叫的層數不會超過 n;

4.2.3 自底向上的遞回(優)

public boolean isBalanced(TreeNode root) {
    return height(root) >= 0;
}

public int height(TreeNode root) {
    if (root == null) {
        return 0;
    }
    int leftHeight = height(root.left);
    int rightHeight = height(root.right);
    if (leftHeight == -1 || rightHeight == -1 || Math.abs(leftHeight - rightHeight) > 1) {
        return -1;
    } else {
        return Math.max(leftHeight, rightHeight) + 1;
    }
}
  • 執行時間:88.57%;記憶體消耗:96.72%;
  • 時間復雜度:O(n),其中 n 是二叉樹中的節點個數,使用自底向上的遞回,每個節點的計算高度和判斷是否平衡都只需要處理一次,最壞情況下需要遍歷二叉樹中的所有節點,因此時間復雜度是 O(n);
  • 空間復雜度:O(n),其中 n 是二叉樹中的節點個數,空間復雜度主要取決于遞回呼叫的層數,遞回呼叫的層數不會超過 n;

5. 合法二叉搜索樹 [medium]

合法二叉搜索樹

5.1 考慮點

  • 有兩種思路:
    • 一是利用中序遍歷;
    • 二是建立在 left <current<right 這項特性上;

5.2 解法

5.2.1 遞回法

class Solution {
    Stack<Integer> stack = new Stack<>();
    boolean isFlag = false;
    public boolean isValidBST(TreeNode root) {
        if(root == null){
            return true;
        }
        inOrderTraversal(root);
        return !isFlag;
    }

    public void inOrderTraversal(TreeNode node){
        if(isFlag){
            return;
        }
        if(node == null){
            return;
        }
        inOrderTraversal(node.left);
        if(stack.isEmpty()){
            stack.push(node.val);
        } else {
            if(stack.peek() >= node.val){
                isFlag = true;
                return;
            } else {
                stack.push(node.val);
            }       	
        }
        inOrderTraversal(node.right);
    }
}
  • 執行時間:32.30%;記憶體消耗:91.41%;

5.2.2 中序遍歷非遞回

public boolean isValidBST(TreeNode root) {
    Deque<TreeNode> stack = new LinkedList<TreeNode>();
    double inorder = -Double.MAX_VALUE;
    while (!stack.isEmpty() || root != null) {
        while (root != null) {
            stack.push(root);
            root = root.left;
        }
        root = stack.pop();
            // 如果中序遍歷得到的節點的值小于等于前一個 inorder,說明不是二叉搜索樹
        if (root.val <= inorder) {
            return false;
        }
        inorder = root.val;
        root = root.right;
    }
    return true;
}
  • 執行時間:24.37%;記憶體消耗:98.61%;
  • 時間復雜度 : O(n),其中 n 為二叉樹的節點個數,二叉樹的每個節點最多被訪問一次,因此時間復雜度為 O(n);
  • 空間復雜度 : O(n),其中 n 為二叉樹的節點個數,堆疊最多存盤 n 個節點,因此需要額外的 O(n) 的空間;

5.2.3 中序遍歷遞回(優)

//前一個結點,全域的
TreeNode prev;

public boolean isValidBST(TreeNode root) {
    if (root == null)
        return true;
    //訪問左子樹
    if (!isValidBST(root.left))
        return false;
    //訪問當前節點:如果當前節點小于等于中序遍歷的前一個節點直接回傳false,
    if (prev != null && prev.val >= root.val)
        return false;
    prev = root;
    //訪問右子樹
    if (!isValidBST(root.right))
        return false;
    return true;
}
  • 執行時間:100.00%;記憶體消耗:65.74%;
  • 中序遍歷時,判斷當前節點是否大于中序遍歷的前一個節點,也就是判斷是否有序,如果不大于直接回傳 false

6. 后繼者 [medium]

后繼者

6.1 考慮點

  • 因為是二叉搜索樹,可以很方便找到節點,再根據是否有右子樹分類判斷;

6.2 解法

6.2.1 中序遍歷遞回法

class Solution {
    Stack<TreeNode> stack = new Stack<>();
    TreeNode findNode = null;
    public TreeNode inorderSuccessor(TreeNode root, TreeNode p) {
        if(root == null || p == null){
            return null;
        }
        inOrderTravarsal(root,p);
        return findNode!=null ? findNode : null;
    }

    public void inOrderTravarsal(TreeNode node, TreeNode p){
        if(findNode != null){
            return;
        }
        if(node == null){
            return;
        }
        inOrderTravarsal(node.left, p);
        if(stack.isEmpty()){
            stack.push(node);
        } else {
			if(p.equals(stack.peek())){
				findNode = node;
                stack.pop(); //忘記pop
                return;
            } else {
                stack.push(node);
            }
        }
        inOrderTravarsal(node.right, p);
    }
}
  • 執行時間:72.07%;記憶體消耗:9.51%;

6.2.2 中序遍歷非遞回法(優)

public TreeNode inorderSuccessor(TreeNode root, TreeNode p) {
    TreeNode pre = null;
    while(root.val!=p.val){
        //右邊
        if(p.val > root.val){          
            root = root.right;
        }
        //左邊
        else{   
            pre = root;
            root = root.left;
        }
    }
    //假如沒有右子樹
    if(root.right==null){
        return pre;
    } else {
        root = root.right;
        while(root.left!=null){
            root = root.left;
        }
        return root;
    }  
}
  • 執行時間:100.00%;記憶體消耗:100.00%;
  • 找到節點后,如果右子樹存在,那就是右子樹最左邊的節點,如果右子樹不存在,表示已經訪問n層子樹,需要回到n的父節點q;如果n在q的左邊,那就是q,反之需要往上訪問,直到找到還未完全遍歷的節點x;

8. 首個共同祖先 [medium]

首個共同祖先

8.1 考慮點

  • 如果是二叉搜索樹,可以看看兩條路徑在哪開始分叉;

8.2 解法

8.2.1 深度優先遍歷

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    if(root == null){
        return null;
    } else if(root.equals(p)){
        return p;
    } else if(root.equals(q)){
        return q;
    }
    TreeNode leftNode = lowestCommonAncestor(root.left,p,q);
    TreeNode rightNode = lowestCommonAncestor(root.right,p,q);
    if(leftNode == null && rightNode == null){
        return null;
    }
    if(leftNode != null && rightNode == null){
        if(leftNode.equals(q)){
            return q;
        } else if(leftNode.equals(p)){
            return p;
        } else {
            return leftNode;
        }
    }
    if(leftNode == null && rightNode != null){
        if(rightNode.equals(q)){
            return q;
        } else if(rightNode.equals(p)){
            return p;
        } else {
            return rightNode;
        }
    }
    //注意非空校驗
    if((leftNode.equals(p) && rightNode.equals(q)) || (leftNode.equals(q) && rightNode.equals(p))){
        return root;
    } 
    return null;
}
  • 執行時間:52.31%;記憶體消耗:5.19%;

8.2.2 深度優先遍歷的簡便寫法(優)

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    // 到底了還沒找到,回傳 null
    if (root == null) {
        return null;
    }
    // 如果找到了 p 或 q,回傳它
    if (root == p || root == q) {
        return root;
    }
    TreeNode left = lowestCommonAncestor(root.left, p, q);  
    TreeNode right = lowestCommonAncestor(root.right, p, q); 
    // 如果 left 和 right 都記錄了找到的節點,那么肯定是一個記錄了 p ,另一個記錄了 q
    // 它們分別在以 root 為根的左右子樹中,所以 root 就是它們的最近公共祖先
    if (left != null && right != null) {
        return root;
    }
    // 由于節點 p,q 一定在二叉樹中,left和right不會同時為null
    // 若 left != null && right == null,說明在左子樹中找到 p 或 q,而在右子樹找不到 p 或 q,則剩下一個也在左子樹
    // 所以 left 就是最近公共祖先
    // 另一種情況同理
    return (left != null) ? left : right;
}
  • 執行時間:100.00%;記憶體消耗:91.55%;

9. 二叉搜索樹序列 [hard]

二叉搜索樹序列

9.1 考慮點

  • 陣列的第一個數必須為頂節點;
  • 與左右節點的插入順序無關緊要,但子節點的添加一定要在父節點之后;

9.2 解法

9.2.1 回溯法+廣度優先遍歷

private List<List<Integer>> ans;

public List<List<Integer>> BSTSequences(TreeNode root) {
    ans = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    // 如果 root==null 回傳 [[]]
    if (root == null) {
        ans.add(path);
        return ans;
    }
    List<TreeNode> queue = new LinkedList<>();
    queue.add(root);
    // 開始進行回溯
    bfs(queue, path);
    return ans;
}

// 回溯法+廣度優先遍歷
private void bfs(List<TreeNode> queue, List<Integer> path) {
    // queue 為空說明遍歷完了,可以回傳了
    if (queue.isEmpty()) {
        ans.add(new ArrayList<>(path));
        return;
    }
    // 將 queue 拷貝一份,用于稍后回溯
    List<TreeNode> copy = new ArrayList<>(queue);
    // 對 queue 進行回圈,每回圈考慮 “是否 「將當前 cur 節點從 queue 中取出并將其左右子
    // 節點加入 queue ,然后將 cur.val 加入到 path 末尾」 ” 的情況進行回溯
    for (int i = 0; i < queue.size(); i++) {
        TreeNode cur = queue.get(i);
        path.add(cur.val);
        queue.remove(i);
        // 將左右子節點加入佇列
        if (cur.left != null) queue.add(cur.left);
        if (cur.right != null) queue.add(cur.right);
        bfs(queue, path);
        // 恢復 path 和 queue ,進行回溯
        path.remove(path.size() - 1);
        queue = new ArrayList<>(copy);
    }
}
  • 執行時間:90.30%;記憶體消耗:93.98%;
  • 對于這種找出所有情況的題目,回溯法是最容易想到的方法之一了,這道題也可以用回溯法,可以發現剛才選元素的程序和層序遍歷的程序其實是一致的:
    • 最開始 queue 中只有 12 ,只能選12,將 12 出隊并將它的兩個子節點入隊,得到 [12];
      選了12之后 queue 中剩下 5、19 ,就從 5 和 19 中選一個,得到 [12,5],[12,19];
      • 如果選了 5 ,將 5 出隊并將它的兩個子節點入隊,那么此時 queue 中剩下 19、2、9,得到 [12,5,2],[12,5,9],[12,5,19];
      • 如果選了 19 ,將 19 出隊并將它的子節點入隊,那么此時 queue 中剩下 5、15,得到 [12,19,5],[12,19,15];
    • 后續同理;

10. 檢查子樹 [medium]

檢查子樹

10.1 考慮點

  • 這里的“萬”是干擾的;

10.2 解法

10.2.1 遞回法(優)

boolean isFound =false;
public boolean checkSubTree(TreeNode t1, TreeNode t2) {
    if(t1 == null){
        return false;
    }
    if(t1.val == t2.val){
        isFound = true;
        return true;
    } else {
        isFound = false;
    }
    boolean left;
    boolean right;
    if(isFound){
        left = checkSubTree(t1.left, t2.left);
        right = checkSubTree(t1.right, t2.right);
        return left && right;
    } else {
        left = checkSubTree(t1.left, t2);
        right = checkSubTree(t1.right, t2);
        if(left){
            return checkSubTree(t1.left, t2);
        }
        if(right){
            return checkSubTree(t1.right, t2);
        }
    }
    return false;   
}
  • 執行時間:100.00%;記憶體消耗:46.52%;

12. 求和路徑 [medium]

求和路徑

12.1 考慮點

  • 可以使用散串列優化演算法,下面解法沒給出;

12.2 解法

12.2.1 暴力法(優)

class Solution {
    int res = 0;

    public int pathSum(TreeNode root, int sum) {
        int dep = depth(root);
        int[] paths = new int[dep];
        pathSum(root, sum, 0, paths);
        return res;
    }
    public void pathSum(TreeNode root, int sum, int level, int[] paths) {
        if (root == null) {
            return;
        }
        paths[level] = root.val;
        int t = 0;
        for (int i = level; i >= 0; i--) {
            t += paths[i];
            if (t == sum) {
                res += 1;
            }
        }
        pathSum(root.left, sum, level + 1, paths);
        pathSum(root.right, sum, level + 1, paths);
    }
    public int depth(TreeNode root) {
        if (root == null) {
            return 0;
        }
        return Math.max(depth(root.left), depth(root.right)) + 1;
    }
}
  • 執行時間:100.00%;記憶體消耗:69.08%;

12.2.2 回溯法

private int res = 0;

public int pathSum(TreeNode root, int sum) {
    if(root == null) return res;

    helper(root, sum);
    pathSum(root.left, sum);
    pathSum(root.right, sum);
    return res;
}

private void helper(TreeNode node, int sum){
    if(node == null) return;

    sum -= node.val;
    if(sum == 0)
        res ++;
    helper(node.left, sum);
    helper(node.right, sum);
    sum += node.val;
}
  • 執行時間:58.34%;記憶體消耗:10.27%;


最后

新人制作,如有錯誤,歡迎指出,感激不盡!
歡迎關注公眾號,會分享一些更日常的東西!
如需轉載,請標注出處!

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/336149.html

標籤:其他

上一篇:求求你了,用Docker吧

下一篇:2021云棲大會丨阿里云發布第四代神龍架構,提供業界首個大規模彈性RDMA加速能力

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more