返回顶部

算法刷题:递归、栈/队列、树、回溯、DP(8.29,持续更)

算法刷题系列:


用队列实现栈
最小栈

队列

基础:

单调队列:

递归算法

  • 表达式计算问题:

基本计算器

链表遍历

  • 前序遍历:先处理当前节点,再处理子节点

直接打印链表

  • 后序遍历:

反转链表
七进制数

二叉树遍历

  • 前序遍历:先处理当前节点,再处理左子树、右子树

二叉树的前序遍历

  • 中序遍历:先记录左子树,再记录当前节点、右子树

二叉树的中序遍历
汉诺塔问题

  • 后序遍历:先记录左子树、右子树,再记录当前节点

二叉树的后序遍历
归并排序

  • 相关问题:
    合并两个有序数组
    有序数组的平方
    ijp模型:
    • 归并排序:i 指向 nums1 的头迭代,j 指向 nums2 的头迭代,p 指向 tmp 的头迭代
    • 合并数组:i 指向 nums1 的前半部分尾迭代,j 指向 nums2 的尾迭代,p 指向 nums1 的尾迭代
    • 平方后排序有序数组:把正区间和负区间当成两个数组,之后用 tmp 合并nums的正负两个区间
  • 层序遍历:

二叉树的层序遍历

N叉树遍历

  • 前序遍历:

N叉树的前序遍历

  • 中序遍历:

  • 后序遍历:

N叉树的后序遍历

  • 层序遍历:

回溯

括号生成
全排列
全排列 II
N 皇后
N 皇后 II


目录


递归思想

递:

  • 如果当前问题需要先解决子问题,则把当前问题压入栈,使子问题成为当前问题

归:

  • 如果当前问题没有子问题,可以直接解决,则直接处理,然后从栈顶弹出一个问题作为新的当前问题

栈提供了一个先进后出的结构,非常符合递归算法中进入和退出递归调用的规则,通过栈可以使用迭代的方式来模拟递归调用

用队列实现栈

基于递归的入栈过程分析

入栈:

  • r-当前问题/节点:入队等于入栈底
  • c-子问题:栈底之上的所有节点是子问题 c
  • 递归:将栈底之上的所有节点放到栈底之下,就处理完毕子问题 c 了

代码实现(单栈、双栈两种解法)

class MyStack {
    //Queue<Integer> help;
    Queue<Integer> q;
    public MyStack() {
        //help = new LinkedList<>();
        q = new LinkedList<>();
    }
    
    public void push(int x) {
        /* - 双栈解法
        help.offer(x);
        while(!q.isEmpty()){
            help.offer(q.poll());
        }
        Queue<Integer> tmp = help;
        help = q;
        q = tmp;
        */
		
        // - 单栈解法
        int tms = q.size();
        q.offer(x);
        for(int i = 0; i < tms; i++){
            int cur = q.poll();
            q.offer(cur);
        }
    }
    
    public int pop() {
        return q.poll();
    }
    
    public int top() {
        return q.peek();
    }
    
    public boolean empty() {
        return q.isEmpty();
    }
}

最小栈

额外空间\(O(N)\)

辅助栈解法(3ms)

class MinStack {
    Deque<Integer> xStack;
    Deque<Integer> minStack;
    public MinStack() {
        xStack = new LinkedList<Integer>();
        minStack = new LinkedList<Integer>();
        minStack.push(Integer.MAX_VALUE);
    }
    
    public void push(int val) {
        int x = minStack.peek();
        xStack.push(val);
        minStack.push(x < val ? x:val);
    }
    
    public void pop() {
        minStack.pop();
        xStack.pop();
    }
    
    public int top() {
        return xStack.peek();
    }
    
    public int getMin() {
        return minStack.peek();
    }
}

双值域节点链表解法

class MinStack {
    class Node{
        int val;
        int min;
        Node next;
        public Node(){
        }
        public Node(int val, int min, Node next){
            this.val = val;
            this.min = min;
            this.next = next;
        }
    }
    Node head;
    public MinStack(){
    }
    public void push(int val){
        if(head == null){
            head = new Node(val, val, null);
        } else {
            Node cur = new Node(val, (val < head.min?val:head.min), head);
            head = cur;
        }
    }
    public void pop(){
        head = head.next;
    }
    public int top(){
        return head.val;
    }
    public int getMin(){
        return head.min;
    }
 
}

无额外空间\(O(N)\)

差分解法,

使用Stack类(4ms)
class MinStack {
    Deque<Long> stk;
    long minVal;

    public MinStack() {
        stk = new ArrayDeque<>();
    }
    
    public void push(int val) {
        if(stk.isEmpty()){
            stk.addLast(0L);
            minVal = val;
        } else {
            long diff = val - minVal;
            stk.addLast(diff);
            if(diff < 0) minVal = val;
        }
    }
    
    public void pop() {
        long diff = stk.pollLast();
        if(diff < 0) minVal -= diff;
    }
    
    public int top() {
        long diff = stk.getLast();
        if(diff < 0) return (int) minVal;
        return (int)(minVal + diff);
    }
    
    public int getMin() {
        return (int) minVal;
    }
}
自定义 ListNode(3ms)
class MinStack {
    class Node {
        long diff;
        Node next;
        Node(long dif){
            diff = dif;
        }
        Node(long dif, Node nxt){
            diff = dif;
            next = nxt;
        }
    }
    Node hd;
    long minVal;
    public MinStack(){}
    
    public void push(int val) {
        if(hd == null){
            hd = new Node(0L);
            minVal = val;
        } else {
            long dif = val - minVal;
            Node cur = new Node(dif, hd);
            if(dif < 0) minVal = val;
            hd = cur;
        }
    }
    
    public void pop() {
        if(hd.diff < 0) minVal -= hd.diff;
        hd = hd.next;
    }
    
    public int top() {
        if(hd.diff < 0) return (int) minVal;
        return (int) (minVal + hd.diff);
    }
    
    public int getMin() {
        return (int) minVal;
    }
}

队列

基础

用栈实现队列

class MyQueue {
    Stack<Integer> stk;
    Stack<Integer> help;
    public MyQueue() {
        stk = new Stack<>();
        help = new Stack<>();
    }
    public void push(int x) {
        while(!stk.isEmpty()) help.push(stk.pop());
        stk.push(x);
        while(!help.isEmpty()) stk.push(help.pop());
    }
    public int pop() {
        return stk.pop();
    }
    public int peek() {
        return stk.peek();
    }
    public boolean empty() {
        return stk.isEmpty();
    }
}

单调 (递减) 队列

滑动窗口的最大值

递减队列存值(报错)

错误用例:

nums = [-7,-8,7,7,7,1,6,0],k = 4
nums = [-7,-8,7,4,7,1,6,0],k = 4

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if(nums.length == 0 || k == 0) return new int [0];
        Deque<Integer> dq = new ArrayDeque<>();
        int [] ans = new int [nums.length - k + 1];
        dq.addLast(nums[0]);
        // 未形成窗口
        for(int i = 1; i < k; i++){
            // 右边界进入
            // 待插入元素 比队头大
            if(nums[i] >= dq.getFirst()){
                dq.clear();
                dq.addLast(nums[i]);
                continue;
            }
            // 待插入元素 比队尾小
            if(nums[i] <= dq.getLast()){
                dq.addLast(nums[i]);
                continue;
            }
            // 待插入元素 是队伍中的元素
            while(!dq.isEmpty() && nums[i] > dq.getLast()){
                dq.removeLast();
            } dq.addLast(nums[i]);
        }
        ans[0] = dq.peekFirst();
        // 形成窗口
        for(int i = k; i < nums.length; i++){
            // 右边界进入
            // 待插入元素 比队头大
            if(nums[i] >= dq.getFirst()){
                dq.clear();
                dq.addLast(nums[i]);
            } else {
                // 待插入元素 是队伍中的元素
                while(!dq.isEmpty() && nums[i] > dq.getLast()){
                    dq.removeLast();
                } dq.addLast(nums[i]);
            }
            // 左边界弹出
            if(dq.peekFirst() == nums[i - k]){
                dq.removeFirst();
            }
            ans[i - k + 1] = dq.peekFirst();
        }
        return ans;
    }
}

值递减但存索引(30ms)

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if(nums.length == 0 || k == 0) return new int [0];
        Deque<Integer> dq = new ArrayDeque<>();
        int [] ans = new int [nums.length - k + 1];
        dq.addLast(0);
        for(int i = 1; i < k; i++){
            if(nums[i] >= nums[dq.getFirst()]){
                dq.clear();
                dq.addLast(i);
                continue;
            }
            while(!dq.isEmpty() && nums[i] > nums[dq.getLast()]){
                dq.removeLast();
            } dq.addLast(i);
        }
        ans[0] = nums[dq.peekFirst()];
        for(int i = k; i < nums.length; i++){
            if(nums[i] >= nums[dq.getFirst()]){
                dq.clear();
                dq.addLast(i);
            } else {
                while(!dq.isEmpty() && nums[i] > nums[dq.getLast()]){
                    dq.removeLast();
                } dq.addLast(i);
            }
            if(dq.peekFirst() == i - k){
                dq.removeFirst();
            }
            ans[i - k + 1] = nums[dq.peekFirst()];
        }
        return ans;
    }
}

优化细节(27ms)

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if(nums.length == 0 || k == 0) return new int [0];
        Deque<Integer> dq = new ArrayDeque<>();
        int [] ans = new int [nums.length - k + 1];
        dq.addLast(0);
        for(int i = 1; i < k; i++){
            while(!dq.isEmpty() && nums[i] > nums[dq.getLast()]){
                dq.removeLast();
            } dq.addLast(i);
        } ans[0] = nums[dq.peekFirst()];
        for(int i = k; i < nums.length; i++){
            while(!dq.isEmpty() && nums[i] > nums[dq.getLast()]){
                dq.removeLast();
            } dq.addLast(i);
            if(dq.peekFirst() == i - k){
                dq.removeFirst();
            } ans[i - k + 1] = nums[dq.peekFirst()];
        } return ans;
    }
}

递归算法

基本计算器

双栈解法(20ms,时空都是\(O(N)\)

class Solution {
    void calc(Deque<Integer> nums, Deque<Character> ops){
        if(nums.size() < 2) return; // 操作数 不足两个
        if(ops.isEmpty()) return; // 操作符 至少一个
        int b = nums.pop(), a = nums.pop();
        char op = ops.pop();
        nums.push(op == '+' ? a + b : a - b);
    }
    boolean isNum(char c){
        return Character.isDigit(c);
    }
    // 读取 表达式字符串
    public int calculate(String s) {
        // 操作数栈
        Deque<Integer> nums = new LinkedList<>();
        nums.push(0);

        // 操作符栈
        Deque<Character> ops = new LinkedList<>();
        
        // 表达式串处理
        s = s.replace(" ", "");
        char [] exp = s.toCharArray();
        int n = s.length();

        for(int i = 0; i < n; i++){
            char c = exp[i];
            if(c == '(') { // 开启下一层
                ops.push(c);
            } else 
            if(c == ')') {
                // 结束 / 计算当前层,返回上一层
                while(!ops.isEmpty()){
                    char op = ops.peek();
                    if(op != '('){
                        calc(nums, ops);
                    } else {
                        ops.pop(); break;
                    }
                }
            }  // 获取当前层的信息:操作数、操作符
            else { // 数字 + - 
                if(isNum(c)){ // 数字
                    int num = 0;
                    int j = i; // 表示当前数字的区间
                    while(j < n && isNum(exp[j]))
                        num = 10 * num + (int)(exp[j++] - '0');
                    nums.push(num);
                    i = j - 1;
                } else { // + -
                    if(i > 0 && (exp[i - 1] == '(' ||
                                 exp[i - 1] == '+' ||
                                 exp[i - 1] == '-')){
                        nums.push(0);
                    }
                    // 有一个新操作入栈时,可以先把之前的都算了
                    while(!ops.isEmpty() && ops.peek() != '(')
                        calc(nums, ops);
                    ops.push(c);
                }
            }
        }
        // 计算剩下的没有计算的
        while(!ops.isEmpty()) calc(nums, ops);
        return nums.peek();
    }
}

递归解法(2ms,\(O(N)\)

class Solution {
    public int calculate(String s) {
        where = 0;
        return recurr(s.replace(" ", "").toCharArray(), 0);
    }

    private int where;

    private int recurr(char[] str, int i) {
        int ans = 0;
        int cnt = 0;
        char ops = '+';
        while (i < str.length) {
            if (str[i] >= '0' && str[i] <= '9') {
                cnt = cnt * 10 + str[i] - '0';
            } else if (str[i] == '(') {
                int result = recurr(str, i + 1);
                ans += ops == '+' ? result : -result;
                i = where;
            } else if (str[i] == ')') {
                break;
            } else {
                ans += ops == '+' ? cnt : -cnt;
                ops = str[i];
                cnt = 0;
            }
            i++;
        }
        ans += ops == '+' ? cnt : -cnt;
        where = i;
        return ans;
    }
}

链表遍历

前序遍历

直接打印链表

public void print(ListNode head){
	if(head == null) return;
	
	Stack<ListNode> stk = new Stack<>();
	stk.push(head);
	while(!stk.isEmpty()){
		ListNode cur = stk.pop();
		if(cur == null) continue;
		
		System.out.println(cur.val);
		stk.push(cur.next);
	}
}

后序遍历

反转链表

class Solution {
    public ListNode reverseList(ListNode head) {
        if(head == null)return null;

        Stack<ListNode> stk = new Stack<>();
        ListNode p = head;
        // 在 反转当前节点 之前
        // 需要 先反转子节点
        while(p != null){
            stk.push(p);
            p = p.next;
        }
        // 当前指向 最后一个节点了
        // 无子节点/子问题
        // 可以反转当前节点
        p = stk.pop();
        head = p;
        // 子节点反转结束,
        // 意味着父节点可以反转
        while(!stk.isEmpty()){
            p.next = stk.pop();
            p = p.next;
        } p.next = null;
        return head;
    }
}

七进制数(浅浅敲段 python)

浅浅的来段 python 代码吧!

class Solution:
    def convertToBase7(self, num: int) -> str:
        if num == 0: return '0'
        self.s = ''
        if num < 0: self.s = '-'
        def dfs(c: int):
            if c == 0: return
            dfs(c // 7)
            self.s = self.s + str(c % 7)
        dfs(num if num > 0 else -num)
        return self.s

二叉树遍历

前序遍历 - 迭代实现

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        if(root == null) return new ArrayList<>();
        // 节点访问栈
        Stack<TreeNode> stk = new Stack<>();
        stk.push(root);
        // 结果集
        List<Integer> ans = new ArrayList<>();
        // 迭代器
        TreeNode p = null;
        while(!stk.isEmpty()){ // 栈空则遍历完毕
            // 迭代器指向 栈顶 / 当前节点
            p = stk.pop();
            ans.add(p.val);
            // 压入子节点
            if(p.right != null) stk.push(p.right);
            if(p.left != null)stk.push(p.left);
        }
        return ans;
    }
}

中序遍历

二叉树的中序遍历

递归实现

迭代实现

关键代码分析
  1. 循环条件
while(!stk.isEmpty()){
    // 栈未空
}
  1. 不断压左
if(root != null){
    while(root.left != null){
        root = root.left;
        stk.push(root);
    }
}
  1. 出栈当前指向最左的迭代器并记录
root = stk.pop();
ans.add(root.val);
// System.out.println(root.val);
  1. 当前迭代器指向右子树
    • 有右子树,迭代器指向右子树
root = root.right;
stack.push(root);
- 无右子树,重置迭代器(赋值 null),防止重复迭代左子树
root = null; // root.right
代码完整实现

优化部分细节:

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> ans = new ArrayList<>();
        if(root == null) return ans;

        Stack<TreeNode> stk = new Stack<>();

        while(root != null || !stk.isEmpty()){
            while(root != null){
                stk.push(root);
                root = root.left;
            }
            // 出栈 并记录
            root = stk.pop();
            ans.add(root.val);
            // System.out.println(root.val);

            // 当前迭代器 (子树树根) root
            // 有右子,root 指向 右子树
            // 无右子, 必须清除 root 的指向 
            // 否则当前出栈栈顶的左子还会压栈
            // 下面是简化写法:
            root = root.right; // 指向 右子树 或 null
        } return ans;
    }
}

汉诺塔问题

递归解法(0ms)

class Solution {
    public void hanota(List<Integer> A, List<Integer> B, List<Integer> C) {
        move(A.size(), A, B, C);
    }
    void move(int size, List<Integer> start, List<Integer> aux, List<Integer> tar){
        if(size == 1){
            tar.add(start.remove(start.size() - 1));
            return;
        }
        move(size - 1, start, tar, aux);
        tar.add(start.remove(start.size() - 1));
        move(size - 1, aux, start, tar);
    }
}

手动栈-中序遍历(7ms)

class Solution {
    public void hanota(List<Integer> A, List<Integer> B, List<Integer> C) {
        int as = A.size();
        if(as == 1){
            C.add(A.remove(0));
            return;
        }
        Stack<Node> stack = new Stack<>();// 空栈

        Node top = new Node(as, A, B, C);//stack.peek(); 栈顶 
        // 栈顶 初始化为 根节点
        stack.push(top); // 将根节点压栈

        Integer visit = null; // 记录节点
        
        // 本质是二叉堆的中序遍历 - 左父右
        // 记录:第二次被访问的关节点、首次访问的叶子节点
        while(!stack.isEmpty()){
            while(top.size > 2){
                // 第一次访问左子节点(关节)压栈
                top = new Node(top.size - 1, top.src, top.tar, top.aux);
                stack.push(top); // top = stack.peek();
            } //sout(stack.size());

            // 左 - 第一次访问左子节点(叶子) - 记录
            Node leaf = new Node(top.size - 1, top.src, top.tar, top.aux);
            visit = leaf.src.remove(leaf.src.size() - 1);
            leaf.tar.add(visit); //sout(leaf.size);

            // 父 - 第二次访问关节节点 - 出栈并记录
            top = stack.pop();
            visit = top.src.remove(top.src.size() - 1);
            top.tar.add(visit); //sout(top.size);
            
            // 右 - 处理右子节点
            if(top.size == 2) { // 如果是右叶子
                // 第一次访问右叶子节点 - 记录
                leaf = new Node(top.size - 1, top.aux, top.src, top.tar);
                visit = leaf.src.remove(leaf.src.size() - 1);
                leaf.tar.add(visit);//sout(leaf.size);
                
                // 此时右叶子的孩子已经找不到,应该去找长辈
                if(stack.isEmpty()) return; // 没有长辈未访问,退出

                top = stack.pop(); // 让下一个长辈出栈
                visit = top.src.remove(top.src.size() - 1);
                top.tar.add(visit); //sout(top.size);
            }
            // 如果是右子树
            top = new Node(top.size - 1, top.aux, top.src, top.tar);
            stack.push(top); // top = stack.peek();
        }
    }
    class Node{
        public int size;
        public List<Integer> src;
        public List<Integer> aux;
        public List<Integer> tar;
        public Node(int size, List<Integer> src, List<Integer> aux, List<Integer> tar){
            this.size = size;
            this.src = src;
            this.aux = aux;
            this.tar = tar;
        }
    }
}

后序遍历

回顾遍历的要点

root 指向当前需要处理的子树树根,但是遇到左子树需要暂时先处理root的子树,而root则保存在栈中等待找不到root时再取出处理

  • root 表示 当前处理点 (树根)
  • stk 存储 因等待处理子树 而暂缓处理的处理点
  • pre 存储上一次处理完毕的处理点(出栈的节点)

代码实现

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> ans = new ArrayList<>();
        if(root == null) return ans;
        Stack<TreeNode> stk = new Stack<>();
        TreeNode pre = null;
        
        while(root != null || !stk.isEmpty()){
            while(root != null){
                stk.push(root);
                root = root.left;
            }
            root = stk.pop();
            // 无右子树,当前子树树根root遍历结束
            if(root.right == null || root.right == pre){
                ans.add(root.val);
                pre = root;
                root = null;
                continue;
            } 
            // 有右子树,处理右子树(root指向右子节点)
            stk.push(root);
            root = root.right;
        } return ans;
    }
}

归并排序

数组的归并排序

class Solution {
    int [] tmp;
    public int[] sortArray(int[] nums) {
        tmp = new int[nums.length];
        // 后序 dfs
        dfs(nums, 0, nums.length - 1);
        return nums;
    }
    public void dfs(int [] nums, int l, int r){
        if(l >= r) return;

        int mid = (l + r) >> 1;
        dfs(nums, l, mid); // 闭区间
        dfs(nums, mid + 1, r);

        int i = l, j = mid + 1;
        int p = 0;
        // 两个区间共同迭代
        while(i <= mid && j <= r){
            if(nums[i] <= nums[j]) {
                tmp[p++] = nums[i++];
            } else {
                tmp[p++] = nums[j++];
            }
        }

        // 两个区间其中一方退出
        while(i <= mid) {
            tmp[p++] = nums[i++];
        }
        while(j <= r) {
            tmp[p++] = nums[j++];
        }
        for(int u = 0; u < p; u++){
            nums[u + l] = tmp[u];
        }
    }
}

链表的归并排序

见之前的刷题系列 - 链表篇;

指针技巧-ijp模型

合并两个有序数组(1ms 击败 100%)

class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        int i = m - 1, j = n - 1, p = nums1.length - 1;
        while(i >= 0 && j >= 0){
            nums1[p--] = nums1[i] > nums2[j] ? nums1[i--] : nums2[j--];
        }
        while(i >= 0){
            nums1[p--] = nums1[i--];
        }
        while(j >= 0){
            nums1[p--] = nums2[j--];
        }
    }
}

有序数组的平方(1ms 击败 100%)

class Solution {
    public int[] sortedSquares(int[] nums) {
        int pvt = 0, i = 0;
    /**** 从这里开始 多出来的部分 */
        // 寻找正负边界并平方
        while(i < nums.length){
            if(nums[i] >= 0) break;
            nums[i] = nums[i] * nums[i];
            i++;
        }
        pvt = i; // 找到正负的边界了

        // 将后面的值都平方
        while(i < nums.length){
            nums[i] = nums[i] * nums[i];
            i++;
        }
    /**** 在这里结束 多出来的部分 */
        int [] tmp = new int [i--];
        
        int p = i, j = 0;
        while(j < pvt && i >= pvt){
            tmp[p--] = nums[i] < nums[j] ? nums[j++] : nums[i--];
        }
        while(j < pvt){
            tmp[p--] = nums[j++];
        }
        while(i >= pvt){
            tmp[p--] = nums[i--];
        }
        return tmp;        
        
    }
}

二叉树的层序遍历

输出层序遍历序列 (队列BFS)

public List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> ans = new ArrayList<>();
    if(root == null) return ans;
    Deque<TreeNode> q = new ArrayDeque<>();// add / poll
    q.add(root);
    while(!q.isEmpty()){
        List<Integer> res = new ArrayList<>();
        root = q.poll();
        System.out.println(root.val);
        if(root.left != null) q.add(root.left);
        if(root.right != null) q.add(root.right);
    }
    return ans;
}

正解:将每一层单独分割为一个列表

class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> ans = new ArrayList<>();
        if(root == null) return ans;
        List<TreeNode> layer = new ArrayList<>();
        layer.add(root);
        while(!layer.isEmpty()){
            List<Integer> res = new ArrayList<>();
            layer = getLayer(layer, res);
            ans.add(res);
        }
        //Deque<TreeNode> q = new ArrayDeque<>();// add / poll
        //q.add(root);
        //while(!q.isEmpty()){
            //root = q.poll();
        return ans;
    }
    List<TreeNode> getLayer(List<TreeNode> preLayer, List<Integer> res){
        List<TreeNode> layer = new ArrayList<>();
        for(TreeNode nd : preLayer){
            res.add(nd.val);//System.out.println(nd.val);
            if(nd.left != null) layer.add(nd.left);
            if(nd.right != null) layer.add(nd.right);
        }
        return layer;
    }
}

反思:原问题的子问题

  1. 将 原问题 定义为 多个子问题
  2. 将 原问题压栈,先解决子问题 再回头 解决原问题

N叉树遍历

N叉树的前序遍历

递归解法

class Solution {
    private List<Integer> ans;
    public List<Integer> preorder(Node root) {
        ans = new ArrayList<>();
        if(root == null) return ans;
        process(root);
        return ans;
    }
    private void process(Node root){
        if(root == null) return;
        ans.add(root.val);
        for(Node node : root.children) process(node);
    }
}

迭代解法

class Solution {
    public List<Integer> preorder(Node root) {
        List<Integer> ans = new ArrayList<>();
        if(root == null) return ans;
        Stack<Node> stk = new Stack<>();
        stk.push(root);
        while(!stk.isEmpty()){
            root = stk.pop(); // get cur
            ans.add(root.val); // vst cur
            for(int i = root.children.size() - 1; i >= 0; i--)
                // psh children 
                stk.push(root.children.get(i));
        }
        return ans;
    }
}

N叉树的后序遍历

要点分析:

  • 当前处理节点 如何记录
    • 这次访问,当前节点 该指向 哪一个子树
    • 是否 上一个处理好了的/出栈了 的是最后一个子树(这样当前节点可以出栈;
      否则当前节点还不能被记录、没有被处理完毕:
      还需要等待再次访问、压入下一个子树)
// 子树迭代记录的方法:
node --> {3, node}
// 处理完毕、不需要缓存-没有子树待处理-可以出栈的判断
cnt == root.children.size()

完整代码实现:

class Solution {
    public List<Integer> postorder(Node root) {
        List<Integer> ans = new ArrayList<>();
        if(root == null) return ans;
        Stack<Object[]> stk = new Stack<>();
        // Deque<Obj[]> d = new ArrayDeque<>();
        stk.push(new Object[]{0, root});
        while(!stk.isEmpty()){
            // root = stk.pop; 
            // cnt 记录当前子树数组被迭代的位置
            Object[] pop = stk.pop();
            Integer cnt = (Integer) pop[0];
            root = (Node) pop[1];
            // tr == null continue
            if(root == null) continue;
            // prev == tr.right -> add(tr = stk.pop)
            if(cnt == root.children.size()) ans.add(root.val);
            else if(cnt < root.children.size()){
                // stk.push(tr)
                stk.push(new Object[]{cnt + 1, root});
                // tr = tr.right
                stk.push(new Object[]{0, root.children.get(cnt)}); 
            }
        }
        return ans;
    }
}

回溯/DP

括号生成

暴力递归解法 - 筛选合法的可构造串(\(O(2^n)\),2ms)

n 对括号就是一个大小为 2n 的字符串
而字符串的每位都可以是 \('('\)\(')'\)
n 位左右括号构成的字符串(合法或不合法)的数量:

\(2(第一位有两种可能)\times2(第二位有两种可能)\times……\times2(第n位有两种可能)\)

也就是说,如果从从字符簇依次选取 按每个字符位构造字符串角度,就可以推出暴力解法:

先不考虑 合法,先构造出 所有可构造字符串

但是构造出所有字符串并不能解决这个问题,需要思考 这里面哪个字符串合法
这时候就需要对每一个生成的字符串进行 逐字符验证
验证的逻辑就是 在其中一个迭代位置上,右括号数量不能比左括号数量多
也就是个维护一个变量,在每一个迭代位置校验: \(是否满足 blc\geq0\)(blc 是 blance 的缩写)
在迭代结束之后,再验证: \(是否满足 blc==0\)
时间复杂度:\(O(2^n)\)

  • 代码实现:
class Solution {
    public List<String> generateParenthesis(int n) {
        // 暴力 2^n
        List<String> ans = new LinkedList<>();
        recur(new char[2 * n], 0, ans);
        return ans;
        
    }
    public void recur(char [] cur, int pos, List<String> ans){
        if(pos == cur.length) {
            // 表示左括号 - 右括号的数量
            // 匹配到左括号++,匹配到右括号--
            int blc = 0;
            for(char c : cur){
                if(c == '(') ++blc;
                else --blc;
                if(blc < 0) return;
            }
            if(blc == 0) ans.add(new String(cur));
            return;
        }
        cur[pos] = '(';
        recur(cur, pos + 1, ans);
        cur[pos] = ')';
        recur(cur, pos + 1, ans);
    }
}

回溯解法 - 筛选合法的可构造串(0ms)

画图

DFS 解法(0ms,前序-剪枝-系统栈)

递之前检验当前状态是否能符合目标
如果不能就马上归,这种递 中途就 归 回上层状态,就是回溯

class Solution {
    int max; // 左括号比右括号 数量上最多 多max个
    public List<String> generateParenthesis(int n) {
        max = n;
        // dfs 解法
        List<String> ans = new ArrayList<>();
        dfs(new char[n << 1], 0, 0, 0, ans);
        return ans;
    }
    public void dfs(char [] cur, int lft, int rgt, int pos, List<String> ans){
        // 比暴力多了实时检验
        // 归的时候 只回溯 正确的
        // 错误的在递的过程中就筛掉了
        if(lft > max || rgt > lft) return;

        // 后面的和递归一样
        if(pos == cur.length){
            ans.add(new String(cur));
            return;
        }
        cur[pos] = '(';
        dfs(cur, lft + 1, rgt, pos + 1, ans);
        cur[pos] = ')';
        dfs(cur, lft, rgt + 1, pos + 1, ans);

    }
}

BFS 解法(4ms,编写节点类 + 手动队列)

class Solution {
    class Node {
        String str;
        int lft;
        int rgt;
        Node(String s, int l, int r){
            str = s;
            lft = l;
            rgt = r;
        }
    }
    public List<String> generateParenthesis(int n) {
        // bfs 手动队列解法
        Deque<Node> q = new ArrayDeque<>();
        q.addLast(new Node("(", 1, 0));
        List<String> ans = new ArrayList<>();

        while(!q.isEmpty()){
            Node cur = q.pollFirst();
            if(cur.lft > n || cur.lft < cur.rgt) continue;
            if(cur.str.length() == 2 * n){
                if(cur.lft == cur.rgt) ans.add(cur.str);
                continue;
            }
            q.addLast(new Node(cur.str + "(", cur.lft + 1, cur.rgt));
            q.addLast(new Node(cur.str + ")", cur.lft, cur.rgt + 1));
        }
        return ans;
    }
}

动态规划解法

按排列组合(9ms)

class Solution {
    public List<String> generateParenthesis(int n) {
        // dp 解法
        List<List<String>> dp = new ArrayList<>();
        dp.add(new ArrayList<>());
        dp.get(0).add("");
        dp.add(new ArrayList<>());
        dp.get(1).add("()");
        for(int i = 2; i <= n; i++){
            dp.add(new ArrayList<>());
            for(int p = 0; p < i; p++){
                // dp.get(i)
                List<String> dpi = dp.get(i);
                int q = i - p - 1;
                // dp.get(p);dp.get(q);
                for(String ps : dp.get(p)){
                    for(String qs : dp.get(q)){
                        dpi.add("(" + ps + ")" + qs);
                    }
                }
            }
        }
        return dp.get(n);
    }
}

全排列

回溯解法 (1ms)

class Solution {
    public List<List<Integer>> permute(int[] s) {
        List<List<Integer>> ans = new LinkedList<>();
        List<Integer> res = new ArrayList<>();
        boolean [] used = new boolean[s.length];
        bt(s, used, res, ans, 0);
        return ans;
    }
    void bt(int [] s, boolean [] used, List<Integer> res, List<List<Integer>> ans, int start){// 路径,选择list
        // 递到起点 return
        // for 选择 in 选择list
        //   do 选择
        //   bt();
        //   undo 选择
        if(start == s.length){
            ans.add(new ArrayList<Integer>(res));
        }
        for(int i = 0; i < s.length; i++){
            if(used[i]) continue;
            used[i] = true;
            res.add(s[i]);
            bt(s, used, res, ans, start + 1);
            used[i] = false;
            res.remove(res.size() - 1);
        }
    }
}

全排列 II

回溯解法(1ms,击败99.71%)

class Solution {
    void bt(int [] chos, boolean [] visited, List<Integer> res, List<List<Integer>> ans){
        if(res.size() == chos.length){
            ans.add(new ArrayList<>(res));
            return;
        }
        for(int i = 0; i < chos.length; i++){
            if(visited[i]) continue;
            visited[i] = true;
            res.add(chos[i]);
            bt(chos, visited, res, ans);
            visited[i] = false;
            res.remove(res.size() - 1);
            // 这里 - 防止相同元素在 同一阶段 出现多次
            while(i < chos.length - 1 && chos[i + 1] == chos[i]) i++;
        }
    }
    public List<List<Integer>> permuteUnique(int[] chos) {
        // 确定区间边界的 前缀相同时,保证每个选择的每 种 元素只在该边界固定一次
        // 排序
        tmp = new int[chos.length];
        mrgSort(chos, 0, chos.length - 1);
        // 排序结束
        // 现在定义 临时结果res 和结果集ans
        List<Integer> res = new ArrayList<>();
        List<List<Integer>> ans = new LinkedList<>();
        // 定义访问数组
        boolean [] visited = new boolean [chos.length];
        // 开始回溯
        bt(chos, visited, res, ans);

        return ans;
    }
    int [] tmp;
    void mrgSort(int [] chos, int left, int right){
        if(left >= right) return;
        int mid = (left + right) >> 1;
        mrgSort(chos, left, mid);
        mrgSort(chos, mid + 1, right);
        int i = left, j = mid + 1, p = 0;
        while(i <= mid && j <= right){
            if(chos[i] < chos[j]) tmp[p++] = chos[i++];
            else tmp[p++] = chos[j++];
        }
        while(i <= mid) tmp[p++] = chos[i++];
        while(j <= right) tmp[p++] = chos[j++];
        for(int u = 0; u < p; u++) chos[u + left] = tmp[u];
    }
}

N皇后

思路分析:

数据定义域 - 操作 - 枚举思路 - (开始递归 - 如何向下递 / 递的时候做什么 / 归的时候做什么 - 递归层数 / 已选可选 - 递归结束) - 枚举结果 - 结果集

  1. 数据定义域:从题目中获取的存储容器。这里是从 \(n\) 宽度、\(n^2\) 个位置的选择矩阵
  2. 操作:选择皇后的位置,并约束选中皇后位置后的 行、列、对角线
  3. (子集/排列/组合)枚举思路:从 \(n\) 宽度、\(n^2\) 个位置的选择矩阵中,选出 n 个位置,使得这 n 个位置的行、对角线、列互相独立
  4. 可选定义域位置表:没 被 选中,或者 没 被 行-列-对角线 约束的,定义域中的位置
  5. 已选状态 / 进度 / 阶段 / 递归层数 / 递归树纵向路径:已选出的几个位置
  6. 枚举结果:选出固定个数的位置后,将排列加入结果集
  7. 结果集:所有的枚举结果

回溯代码(3ms)

class Solution {
    int [] rcd;
    boolean [] cvst;
    List<List<String>> ans;
    List<String> res;
    char [][] mtx;
    Set<Integer> dig1;
    Set<Integer> dig2;
    public List<List<String>> solveNQueens(int n) {
        // 放在哪,也就是操作想不通
        // 就先把 数据集 定义出来(棋盘)
        mtx = new char[n][n];
        // 分析:放一个之后
        // 下一个位置 不能 同行同列同对角线
        // 可以把 行 作为 顶点,递归 作为 边
        rcd = new int[n];
        cvst = new boolean[n];
        dig1 = new HashSet<>();
        dig2 = new HashSet<>();
        res = new ArrayList<>();
        ans = new LinkedList<>();
        bt(0);
        return ans;
    }
    void bt(int r){
        if(r == mtx.length){
            crtMtx();
            return;
        }
        for(int i = 0; i < mtx.length; i++){
            // row : r
            // col : i
            // cvst 决定 点 (r,i) 是否在路径上
            // 对角线判断 r + i / r - i
            if(cvst[i]) continue;
            int diago1 = r + i;
            int diago2 = r - i;
            if(dig1.contains(diago1)) continue;
            if(dig2.contains(diago2)) continue;
            dig1.add(diago1);
            dig2.add(diago2);
            // System.out.println("+" + diago1 +" "+diago2);
            
            cvst[i] = true;
            rcd[r] = i + 1;
            bt(r + 1);
            rcd[r] = 0;
            cvst[i] = false;
            dig1.remove(diago1);
            dig2.remove(diago2);
            // System.out.println("-" + diago1 +" "+diago2);
        }
    }
    void crtMtx(){
        for(int i = 0; i < mtx.length; i++){
            for(int j = 0; j < mtx.length; j++){
                mtx[i][j] = rcd[i] == j + 1 ? 'Q' : '.';
            } res.add(new String(mtx[i]));
        } ans.add(res);
        res = new ArrayList<>();
    }
}

N皇后 II

回溯代码(2ms)

class Solution {
    boolean [] cpath;
    // int [] path;
    Set<Integer> ldig;
    Set<Integer> rdig;
    int cnt = 0;
    public int totalNQueens(int n) {
        cpath = new boolean[n];
        // path = new int [n];
        ldig = new HashSet<>();
        rdig = new HashSet<>();
        bt(0);
        return cnt;
    }
    void bt(int row){
        if(row == cpath.length) {
            cnt++;
            return;
        }
        for(int c = 0; c < cpath.length; c++){
            if(cpath[c]) continue;
            int digl = row + c;
            int digr = row - c;
            if(ldig.contains(digl)) continue;
            if(rdig.contains(digr)) continue;
            ldig.add(digl);
            rdig.add(digr);
            
            cpath[c] = true;
            bt(row + 1);
            cpath[c] = false;
            ldig.remove(digl);
            rdig.remove(digr);
        }
    }
}
posted @ 2023-08-30 10:01  你好,一多  阅读(9)  评论(0编辑  收藏  举报