算法刷题:递归、栈/队列、树、回溯、DP(8.29,持续更)
作者:@罗一
本文为作者原创,转载请注明出处:https://www.cnblogs.com/luoyicode/p/17666537.html
算法刷题系列:
栈
队列
基础:
单调队列:
递归算法
- 表达式计算问题:
链表遍历
- 前序遍历:先处理当前节点,再处理子节点
- 后序遍历:
二叉树遍历
- 前序遍历:先处理当前节点,再处理左子树、右子树
- 中序遍历:先记录左子树,再记录当前节点、右子树
- 后序遍历:先记录左子树、右子树,再记录当前节点
- 层序遍历:
N叉树遍历
- 前序遍历:
-
中序遍历:
-
后序遍历:
- 层序遍历:
回溯
递归思想
递:
- 如果当前问题需要先解决子问题,则把当前问题压入栈,使子问题成为当前问题
归:
- 如果当前问题没有子问题,可以直接解决,则直接处理,然后从栈顶弹出一个问题作为新的当前问题
栈提供了一个先进后出的结构,非常符合递归算法中进入和退出递归调用的规则,通过栈可以使用迭代的方式来模拟递归调用
栈
用队列实现栈
基于递归的入栈过程分析
入栈:
- 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();
}
}
最小栈
额外空间
辅助栈解法(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;
}
}
无额外空间
差分解法,
使用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,时空都是 )
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, )
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;
}
}
中序遍历
二叉树的中序遍历
递归实现
略
迭代实现
关键代码分析
- 循环条件
while(!stk.isEmpty()){
// 栈未空
}
- 不断压左
if(root != null){
while(root.left != null){
root = root.left;
stk.push(root);
}
}
- 出栈当前指向最左的迭代器并记录
root = stk.pop();
ans.add(root.val);
// System.out.println(root.val);
- 当前迭代器指向右子树
- 有右子树,迭代器指向右子树
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;
}
}
反思:原问题的子问题
- 将 原问题 定义为 多个子问题
- 将 原问题压栈,先解决子问题 再回头 解决原问题
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
括号生成
暴力递归解法 - 筛选合法的可构造串( ,2ms)
n 对括号就是一个大小为 2n 的字符串
而字符串的每位都可以是
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皇后
思路分析:
数据定义域 - 操作 - 枚举思路 - (开始递归 - 如何向下递 / 递的时候做什么 / 归的时候做什么 - 递归层数 / 已选可选 - 递归结束) - 枚举结果 - 结果集
- 数据定义域:从题目中获取的存储容器。这里是从
宽度、 个位置的选择矩阵 - 操作:选择皇后的位置,并约束选中皇后位置后的 行、列、对角线
- (子集/排列/组合)枚举思路:从
宽度、 个位置的选择矩阵中,选出 n 个位置,使得这 n 个位置的行、对角线、列互相独立 - 可选定义域位置表:没 被 选中,或者 没 被 行-列-对角线 约束的,定义域中的位置
- 已选状态 / 进度 / 阶段 / 递归层数 / 递归树纵向路径:已选出的几个位置
- 枚举结果:选出固定个数的位置后,将排列加入结果集
- 结果集:所有的枚举结果
回溯代码(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);
}
}
}
如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!欢迎各位转载,但是未经作者本人同意,转载文章之后必须在文章页面明显位置给出作者和原文连接,否则保留追究法律责任的权利。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现