一、引言: 在AcWing做到这道题:823. 排列
在这道题目中,给定了一个整数 n,要求将数字 1 到 n 排成一排,并按照字典序输出所有的排列方案。这道题主要考察全排列和字典序的应用,通过深度优先搜索(DFS)算法来解决。
全排列是指从给定的一组元素中,选取一些或全部元素,按照一定顺序进行排列,从而得到不同的排列方式。而字典序则是按照字母或数字在字典中的顺序进行排列。在解决这道题时,我们需要找到所有的排列方案,并按照字典序输出。
为了实现这个目标,我们可以使用深度优先搜索算法。DFS是一种常用的搜索算法,它从初始状态开始,逐步向前探索,直到达到目标状态或无法继续前进为止。在全排列问题中,我们需要遍历每个数字,将其放入当前位置,然后递归地对剩下的数字进行排列。通过回溯法,我们可以找到所有的排列方案。
此外,Java内部类的概念在解决这道题中也起到了重要作用。内部类是定义在另一个类内部的类,它具有访问外部类私有成员的能力,并且可以被当做外部类的成员使用。在解决这道题时,我们可以使用内部类来保存当前的排列情况,并将其传递给下一层递归。这样,我们就可以方便地访问和修改当前的排列状态。
综上,通过深度优先搜索算法和Java内部类的结合运用,我们可以解决这道题目,找到所有的排列方案,并按照字典序输出。接下来,我们将详细讲解DFS算法的原理和Java内部类的使用方法,并给出完整的代码实现,帮助读者更好地理解和掌握这两个知识点。
二、深度优先搜索(DFS)
深度优先搜索(DFS)是一种重要的图遍历算法,它可以用于求解许多图论问题。它的搜索过程类似于树的先序遍历。在算法实现中,我们通过递归或栈的方式进行遍历,并在每一步尽可能深入地探索图中的分支,直到无法继续为止。
1.基本原理
DFS算法的基本原理是从一个起点开始,依次访问其所有邻居节点(也就是与这个节点有连接的其他节点),并将这些邻居节点标记为已访问过。接着,对于每个已经访问过的邻居节点,我们再递归访问它们的邻居节点,直到所有节点都被访问过为止。
在实现DFS算法时,可以使用递归或栈的方式进行遍历。如果使用递归的方式,我们需要在进入递归之前,将当前节点标记为已被访问过。如果使用栈的方式,我们需要在进栈之前,将当前节点标记为已被访问过。
2.应用场景
DFS算法可以用于求解许多图论问题,例如:
- 遍历图中的所有节点
- 判断图是否连通
- 求解最短路径
- 求解欧拉回路和哈密顿回路
此外,DFS算法还可以用于求解一些其他类型的问题,例如:
- 在排列、组合等问题中搜索所有可能的解
- 在数独、八皇后等问题中搜索所有可能的解
三、Java内部类
这里将详细讲解java内部类的概念和用法,并针对题目中的内部类示例进行说明。
Java内部类是指定义在另一个类内部的类。与常规的独立类不同,内部类具有访问外部类私有成员的能力,并且可以被当做外部类的成员使用。内部类提供了更好的封装性和代码组织性,可以在某些情况下提供更清晰和优雅的代码实现。
1.内部类的概念
Java内部类主要分为四种类型:成员内部类、静态内部类、局部内部类和匿名内部类。这里,我们将重点介绍 成员内部类 的 概念 和 用法。
成员内部类是定义在另一个类内部的普通类,它可以访问外部类的所有成员(包括私有成员),并且可以被外部类及其它类使用。成员内部类通过持有一个外部类的引用,实现了对外部类的访问。
2.内部类的用法
在题目中的内部类示例中,我们可以使用内部类来保存当前的排列情况,并将其传递给下一层递归。这样,我们就可以方便地访问和修改当前的排列状态。
以下是一个示例代码,演示了如何使用内部类来实现排列方案的生成:
static class PermutationSolver {
private int num;
private int[] nums;
private boolean[] vis;
private BufferedWriter bw;
public PermutationSolver(int num, int[] nums, boolean[] vis, BufferedWriter bw) {
this.num = num;
this.nums = nums;
this.vis = vis;
this.bw = bw;
}
public void solve() throws IOException {
dfs(0);
}
private void dfs(int u) throws IOException {
if (u == num) {
for (int i = 0; i < num; i++) {
bw.write((nums[i] + 1) + " ");
}
bw.write("\n");
} else {
for (int i = 0; i < num; i++) {
if (!vis[i]) {
vis[i] = true;
nums[u] = i;
dfs(u + 1);
vis[i] = false;
}
}
}
}
}
在上面的代码示例中,我们使用了成员内部类来展示Java内部类的用法。
内部类是嵌套在其他类内部的类。它具有以下特点:
-
内部类可以访问外部类的私有成员:在示例代码中,
PermutationSolver
内部类可以直接访问外部类PermutationDemo
中的私有成员变量num
、nums
、vis
和bw
。 -
外部类可以访问内部类的私有成员:在示例代码中,外部类
PermutationDemo
可以创建PermutationSolver
内部类的实例,并调用其中的方法。 -
内部类可以访问外部类的方法参数和局部变量:在示例代码的
solve
方法中,PermutationSolver
内部类可以访问dfs
方法的参数u
。 -
内部类可以拥有与外部类相同的变量名:在示例代码中,内部类的
PermutationSolver
类有与外部类相同名称的变量num
,但它们代表不同的含义。
通过使用内部类,我们可以实现更好的封装和组织代码结构。内部类通常用于实现与外部类紧密相关的辅助功能或实现某个接口。
补充:
内部类的四种类型介绍:
-
成员内部类(Member Inner Class):成员内部类是嵌套在外部类中,与外部类的成员变量和方法有着紧密的联系。它可以访问外部类的私有成员,并且可以被外部类直接访问。
-
静态内部类(Static Inner Class):静态内部类是使用
static
关键字修饰的内部类。它与外部类的实例无关,可以像普通类一样被创建和访问,不依赖于外部类的实例。 -
方法内部类(Method Local Inner Class):方法内部类是定义在方法内部的类。它的作用域限定在所在方法内部,只能在方法内部被访问。方法内部类可以访问外部方法的参数和局部变量,但这些变量必须声明为
final
或事实上的final
。 -
匿名内部类(Anonymous Inner Class):匿名内部类是没有具体类名的内部类。它通常用于实现某个接口或继承某个类,并且只能创建一个实例。匿名内部类一般在需要时立即定义,并且可以覆盖父类的方法或实现接口的方法。
内部类与外部类之间的访问权限如下:
-
外部类可以直接访问内部类的成员,即使是私有成员。
-
内部类可以访问外部类的所有成员,包括私有成员。
-
外部类想要访问内部类的成员,需要通过内部类的实例来访问。
内部类的优点和使用场景包括:
-
封装:内部类可以访问外部类的私有成员,提供了更好的封装性。
-
访问外部类的成员:可以轻松访问外部类的成员变量和方法。
-
实现多重继承:内部类可以实现多个接口,从而实现多重继承。
-
代码组织和可读性:内部类可以将相关的代码组织在一起,提高代码的可读性和维护性。
不同类型的内部类的区别和用法如下:
-
成员内部类常用于描述外部类的特定功能,与外部类之间有着密切的联系。
-
静态内部类可以独立于外部类创建和访问,适合满足某个特定功能的类的定义。
-
方法内部类通常用于在方法内部创建一个辅助类,实现某个特定功能。
-
匿名内部类可以简化代码,特别适用于只需要创建一个实例的情况。
创建内部类的对象并访问其方法和成员的方式如下:
-
成员内部类的实例化方式:外部类实例.new 内部类(),例如:
Outer outer = new Outer(); Outer.Inner inner = outer.new Inner();
-
静态内部类的实例化方式:外部类直接.new 内部类(),例如:
Outer.Inner inner = new Outer.Inner();
-
方法内部类的实例化方式:在方法内部直接使用方法内部类的构造器创建实例。
-
匿名内部类的实例化方式:通常使用匿名内部类的父类或接口的引用变量来创建实例。
内部类与外部类的关系和生命周期:
-
内部类与外部类之间存在着强关联,内部类可以访问外部类的成员和方法。
-
内部类的生命周期与外部类的对象绑定,只能在外部类的实例中创建内部类的实例。
四、题目解析及代码实现
依据题目分析: ”递归搜索树“
解题思路:使用深度优先搜索算法(DFS)求解。从下标为 0 开始,不断枚举未选择过的数,并递归向下搜索。当下标 u 等于 n 时,说明已经完成了一种排列,将该排列输出即可。
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.Scanner;
public class Main {
private static int[] path; // 已选过的数
private static boolean[] st; // 标记数组
private static int n; // 选择的数的个数
private static BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
public static void main(String[] args) throws IOException {
Scanner sca = new Scanner(System.in);
n = sca.nextInt();
path = new int[n];
st = new boolean[n];
dfs(0); // 从下标 0 开始搜索
bw.flush(); // 刷新缓存区
}
private static void dfs(int u) throws IOException {
if (u == n) { // 如果已经选满 n 个数,则输出该排列
for (int i = 0; i < n; i++) {
bw.write(path[i] + " ");
}
bw.write("\n");
} else {
for (int i = 0; i < n; ++i) { // 尝试枚举未选择过的数
if (!st[i]) {
st[i] = true;
path[u] = i + 1; // 将第 i 个数加入排列中
dfs(u + 1); // 递归向下搜索
st[i] = false; // 回溯(撤销选择)
}
}
}
}
}
结合java内部类实现:
在本题中,我们使用局部内部类来定义一个名为 Inner 的类,在其中实现 DFS 函数。
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.Scanner;
public class Main {
/**
* 使用内部类代替静态变量的方式
*/
public static void main(String[] args) throws IOException {
Scanner sca = new Scanner(System.in);
int num = sca.nextInt();
int[] nums = new int[num];
boolean[] vis = new boolean[num];
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
class Inner {
public void dfs(int u) throws IOException {
if (u == num) { // 如果已经选满 num 个数,则输出该排列
for (int i = 0; i < num; i++) {
bw.write((nums[i]) + " ");
}
bw.write("\n");
} else {
for (int i = 0; i < num; i++) { // 尝试枚举未选择过的数
if (!vis[i]) {
vis[i] = true;
nums[u] = i; // 将第 i 个数加入排列中
dfs(u + 1); // 递归向下搜索
vis[i] = false; // 回溯(撤销选择)
}
}
}
}
}
Inner inner = new Inner(); // 创建 Inner 类的对象
inner.dfs(0); // 调用 dfs 函数
bw.flush(); // 刷新缓存区
}
}