本文的网课内容学习自B站左程云老师的算法详解课程,旨在对其中的知识进行整理和分享~
一.字符串的全部子序列
题目:字符串的全部子序列
算法原理
①generatePermutation1
方法相关
-
总体思路
- 对于给定的字符串,通过递归地考虑每个字符是否加入到子序列中来生成所有可能的子序列。由于要求去重,使用
HashSet
来存储中间结果以避免重复。
- 对于给定的字符串,通过递归地考虑每个字符是否加入到子序列中来生成所有可能的子序列。由于要求去重,使用
-
具体步骤
generatePermutation1
方法- 首先将输入字符串转换为字符数组
s
。 - 创建一个
HashSet<String>
用于去重存储子序列。 - 调用
f1
方法开始生成子序列,传入字符数组s
、起始索引0
、一个空的StringBuilder
用于构建子序列路径、以及用于去重的HashSet
。 - 根据
HashSet
中去重后的子序列数量创建结果数组ans
。 - 将
HashSet
中的子序列依次放入结果数组ans
中并返回。
- 首先将输入字符串转换为字符数组
f1
方法(递归生成子序列)- 递归终止条件(
if (i == s.length)
)- 当索引
i
等于字符数组s
的长度时,表示已经处理完字符串的所有字符。此时将当前构建的子序列路径(path.toString()
)添加到HashSet
中进行去重存储。
- 当索引
- 递归生成子序列(
else
)- 首先将当前字符
s[i]
添加到path
中(path.append(s[i])
),然后递归调用f1
方法,将索引i
加1,表示继续处理下一个字符,同时传入更新后的path
和HashSet
。这一步相当于选择将当前字符加入子序列。 - 接着,将刚刚添加到
path
中的字符移除(path.deleteCharAt(path.length() - 1)
),再次递归调用f1
方法,同样将索引i
加1,但此时传入的path
是移除了当前字符后的情况。这一步相当于不选择将当前字符加入子序列。
- 首先将当前字符
- 递归终止条件(
②generatePermutation2
方法相关
-
总体思路
- 同样是通过递归地考虑每个字符是否加入到子序列中来生成所有可能的子序列,不过这里使用一个固定大小的字符数组
path
来构建子序列,并且也使用HashSet
去重。
- 同样是通过递归地考虑每个字符是否加入到子序列中来生成所有可能的子序列,不过这里使用一个固定大小的字符数组
-
具体步骤
generatePermutation2
方法- 将输入字符串转换为字符数组
s
。 - 创建一个
HashSet<String>
用于去重存储子序列。 - 调用
f2
方法开始生成子序列,传入字符数组s
、起始索引0
、一个与输入字符串长度相同的字符数组path
、初始大小为0(表示当前path
中有效字符的数量)、以及用于去重的HashSet
。 - 根据
HashSet
中去重后的子序列数量创建结果数组ans
。 - 将
HashSet
中的子序列依次放入结果数组ans
中并返回。
- 将输入字符串转换为字符数组
f2
方法(递归生成子序列)- 递归终止条件(
if (i == s.length)
)- 当索引
i
等于字符数组s
的长度时,表示已经处理完字符串的所有字符。此时将当前构建的子序列(String.valueOf(path, 0, size)
,即根据有效字符数量size
从path
中获取子序列)添加到HashSet
中进行去重存储。
- 当索引
- 递归生成子序列(
else
)- 首先将当前字符
s[i]
放入path
数组的size
位置(path[size] = s[i]
),然后递归调用f2
方法,将索引i
加1,表示继续处理下一个字符,同时传入更新后的path
、size + 1
(因为加入了一个字符,有效字符数量加1)和HashSet
。这一步相当于选择将当前字符加入子序列。 - 接着,再次递归调用
f2
方法,将索引i
加1,但传入的size
不变(即不选择将当前字符加入子序列),同时传入更新后的path
和HashSet
。
- 首先将当前字符
- 递归终止条件(
代码实现
import java.util.HashSet;
// 字符串的全部子序列
// 子序列本身是可以有重复的,只是这个题目要求去重
// 测试链接 : https://www.nowcoder.com/practice/92e6247998294f2c933906fdedbc6e6a
public class Code01_Subsequences {
public static String[] generatePermutation1(String str) {
char[] s = str.toCharArray();
HashSet<String> set = new HashSet<>();
f1(s, 0, new StringBuilder(), set);
int m = set.size();
String[] ans = new String[m];
int i = 0;
for (String cur : set) {
ans[i++] = cur;
}
return ans;
}
// s[i...],之前决定的路径path,set收集结果时去重
public static void f1(char[] s, int i, StringBuilder path, HashSet<String> set) {
if (i == s.length) {
set.add(path.toString());
} else {
path.append(s[i]); // 加到路径中去
f1(s, i + 1, path, set);
path.deleteCharAt(path.length() - 1); // 从路径中移除
f1(s, i + 1, path, set);
}
}
public static String[] generatePermutation2(String str) {
char[] s = str.toCharArray();
HashSet<String> set = new HashSet<>();
f2(s, 0, new char[s.length], 0, set);
int m = set.size();
String[] ans = new String[m];
int i = 0;
for (String cur : set) {
ans[i++] = cur;
}
return ans;
}
public static void f2(char[] s, int i, char[] path, int size, HashSet<String> set) {
if (i == s.length) {
set.add(String.valueOf(path, 0, size));
} else {
path[size] = s[i];
f2(s, i + 1, path, size + 1, set);
f2(s, i + 1, path, size, set);
}
}
}
二.子集
题目:子集 II
算法原理
-
总体思路
- 先对输入数组进行排序,这样相同的元素会相邻。然后通过递归的方式构建所有可能的组合,在构建过程中避免生成重复的组合。
-
具体步骤
subsetsWithDup
方法- 创建一个空的结果列表
ans
,用于存储所有不重复的组合。 - 对输入的整数数组
nums
进行排序(Arrays.sort(nums)
),这是为了方便后续处理重复元素。 - 调用
f
方法开始构建组合,传入nums
数组、起始索引0
、一个与nums
长度相同的临时数组path
(用于构建组合路径)、初始大小为0
(表示path
中有效元素的数量)以及结果列表ans
。 - 最后返回结果列表
ans
。
- 创建一个空的结果列表
f
方法(递归构建组合)- 递归终止条件(
if (i == nums.length)
)- 当索引
i
等于nums
数组的长度时,表示已经处理完数组中的所有元素。此时根据path
数组中有效元素的数量(size
)构建一个ArrayList
,并将其添加到结果列表ans
中。
- 当索引
- 递归构建组合(
else
)- 首先确定下一组的第一个数的位置(
int j = i + 1; while (j < nums.length && nums[i] == nums[j]) {j++;}
),这里通过循环跳过与nums[i]
相同的连续元素,找到下一个不同元素的位置j
。 - 考虑当前数
nums[i]
要0
个的情况,即不包含当前数的组合,直接递归调用f
方法,传入j
作为新的起始索引,path
和size
不变(因为不包含当前数)以及ans
。 - 然后考虑当前数
nums[i]
要1
个、要2
个、要3
个等情况(for (; i < j; i++)
),在这个循环中,将当前数nums[i]
放入path
数组(path[size++] = nums[i]
),并递归调用f
方法,传入j
作为新的起始索引,更新后的path
(包含了当前数)、更新后的size
(因为加入了当前数)以及ans
。通过这种方式,构建包含不同数量当前数的组合,同时避免了重复组合的生成。例如,对于[1,2,2]
,当处理第一个2
时,会构建包含不同数量2
的组合,但由于已经跳过了后续相同的2
,不会产生重复的组合。
- 首先确定下一组的第一个数的位置(
- 递归终止条件(
代码实现
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
// 给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的组合
// 答案 不能 包含重复的组合。返回的答案中,组合可以按 任意顺序 排列
// 注意其实要求返回的不是子集,因为子集一定是不包含相同元素的,要返回的其实是不重复的组合
// 比如输入:nums = [1,2,2]
// 输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
// 测试链接 : https://leetcode.cn/problems/subsets-ii/
public class Code02_Combinations {
public static List<List<Integer>> subsetsWithDup(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
Arrays.sort(nums);
f(nums, 0, new int[nums.length], 0, ans);
return ans;
}
public static void f(int[] nums, int i, int[] path, int size, List<List<Integer>> ans) {
if (i == nums.length) {
ArrayList<Integer> cur = new ArrayList<>();
for (int j = 0; j < size; j++) {
cur.add(path[j]);
}
ans.add(cur);
} else {
// 下一组的第一个数的位置
int j = i + 1;
while (j < nums.length && nums[i] == nums[j]) {
j++;
}
// 当前数x,要0个
f(nums, j, path, size, ans);
// 当前数x,要1个、要2个、要3个...都尝试
for (; i < j; i++) {
path[size++] = nums[i];
f(nums, j, path, size, ans);
}
}
}
}
三.全排列
题目:全排列
算法原理
-
总体思路
- 通过递归交换数组元素的方式来生成所有可能的全排列。对于给定的数组,每次固定一个位置的元素,然后递归地对剩余元素进行全排列操作,最后通过交换操作还原数组状态,以进行下一轮的固定元素操作。
-
具体步骤
permute
方法- 创建一个空的结果列表
ans
,用于存储所有的全排列结果。 - 调用
f
方法开始生成全排列,传入待排列的数组nums
、起始索引0
以及结果列表ans
。 - 最后返回结果列表
ans
。
- 创建一个空的结果列表
f
方法(递归生成全排列)- 递归终止条件(
if (i == nums.length)
)- 当索引
i
等于数组nums
的长度时,表示已经完成了一个全排列的构建。此时创建一个新的ArrayList
,将数组nums
中的所有元素添加进去(因为此时nums
已经是一种全排列状态),然后将这个ArrayList
添加到结果列表ans
中。
- 当索引
- 递归生成全排列(
else
)- 对于当前索引
i
,通过一个循环(for (int j = i; j < nums.length; j++)
),将索引i
到数组末尾的每个元素依次与索引i
位置的元素进行交换(swap(nums, i, j)
)。这一步的目的是固定当前位置i
的元素为不同的值。 - 在交换元素后,递归调用
f
方法,将起始索引更新为i + 1
,继续对剩余的元素进行全排列操作。 - 完成递归调用后,再次交换元素(
swap(nums, i, j)
),将数组还原到交换之前的状态。这一步非常重要,因为如果不还原数组,后续的交换操作将基于错误的数组状态进行,无法生成正确的全排列。例如,对于数组{1, 2, 3}
,当i = 0
时,首先将1
与1
交换(无实际交换),然后递归对{2, 3}
进行全排列操作;当对{2, 3}
的全排列操作完成后,需要将数组还原到{1, 2, 3}
的状态,以便进行下一次交换(如将1
与2
交换),从而继续生成不同的全排列。
- 对于当前索引
- 递归终止条件(
代码实现
import java.util.ArrayList;
import java.util.List;
// 没有重复项数字的全排列
// 测试链接 : https://leetcode.cn/problems/permutations/
public class Code03_Permutations {
public static List<List<Integer>> permute(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
f(nums, 0, ans);
return ans;
}
public static void f(int[] nums, int i, List<List<Integer>> ans) {
if (i == nums.length) {
List<Integer> cur = new ArrayList<>();
for (int num : nums) {
cur.add(num);
}
ans.add(cur);
} else {
for (int j = i; j < nums.length; j++) {
swap(nums, i, j);
f(nums, i + 1, ans);
swap(nums, i, j); // 特别重要,课上进行了详细的图解
}
}
}
public static void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
public static void main(String[] args) {
int[] nums = { 1, 2, 3 };
List<List<Integer>> ans = permute(nums);
for (List<Integer> list : ans) {
for (int num : list) {
System.out.print(num + " ");
}
System.out.println();
}
}
}
四.全排列 II
题目:全排列 II
算法原理
-
总体思路
- 与无重复项数组的全排列类似,通过递归交换数组元素的方式来生成所有可能的全排列,但在交换元素时需要进行去重处理,以避免生成重复的全排列。
-
具体步骤
permuteUnique
方法- 创建一个空的结果列表
ans
,用于存储所有的去重全排列结果。 - 调用
f
方法开始生成去重全排列,传入待排列的数组nums
、起始索引0
以及结果列表ans
。 - 最后返回结果列表
ans
。
- 创建一个空的结果列表
f
方法(递归生成去重全排列)- 递归终止条件(
if (i == nums.length)
)- 当索引
i
等于数组nums
的长度时,表示已经完成了一个全排列的构建。此时创建一个新的ArrayList
,将数组nums
中的所有元素添加进去(因为此时nums
已经是一种全排列状态),然后将这个ArrayList
添加到结果列表ans
中。
- 当索引
- 递归生成去重全排列(
else
)- 创建一个
HashSet
集合set
,用于记录已经在当前位置i
使用过的元素,以避免重复使用。 - 通过一个循环(
for (int j = i; j < nums.length; j++)
),遍历索引i
到数组末尾的每个元素。 - 在循环中,首先判断当前元素
nums[j]
是否已经在set
中,如果不在(即nums[j]
没有来到过i
位置),则将其添加到set
中,然后交换nums[i]
和nums[j]
的位置(swap(nums, i, j)
),接着递归调用f
方法,将起始索引更新为i + 1
,继续对剩余的元素进行去重全排列操作。 - 完成递归调用后,再次交换元素(
swap(nums, i, j)
),将数组还原到交换之前的状态,以便进行下一次交换操作,生成不同的全排列。例如,对于数组{1, 1, 2}
,当i = 0
时,首先将第一个1
与自身交换(无实际交换),然后递归对{1, 2}
进行去重全排列操作;当对{1, 2}
的去重全排列操作完成后,需要将数组还原到{1, 1, 2}
的状态,接着将第一个1
与第二个1
交换,再次递归对{1, 2}
进行去重全排列操作,由于HashSet
的去重作用,不会生成重复的全排列。
- 创建一个
- 递归终止条件(
代码实现
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
// 有重复项数组的去重全排列
// 测试链接 : https://leetcode.cn/problems/permutations-ii/
public class Code04_PermutationWithoutRepetition {
public static List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
f(nums, 0, ans);
return ans;
}
public static void f(int[] nums, int i, List<List<Integer>> ans) {
if (i == nums.length) {
List<Integer> cur = new ArrayList<>();
for (int num : nums) {
cur.add(num);
}
ans.add(cur);
} else {
HashSet<Integer> set = new HashSet<>();
for (int j = i; j < nums.length; j++) {
// nums[j]没有来到过i位置,才会去尝试
if (!set.contains(nums[j])) {
set.add(nums[j]);
swap(nums, i, j);
f(nums, i + 1, ans);
swap(nums, i, j);
}
}
}
}
public static void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
五.用递归函数逆序栈
算法原理
总体思路
- 这个算法主要是通过递归操作来实现栈的逆序。首先定义了两个递归操作,一个是
reverse
函数用于整体逆序栈,另一个是bottomOut
函数用于获取栈底元素并调整栈结构。
具体步骤
reverse
函数原理- 递归终止条件(
if (stack.isEmpty())
)- 当栈为空时,表示已经完成了逆序操作(或者栈本身就是空的无需操作),直接返回。
- 递归操作
- 先调用
bottomOut
函数从栈中取出栈底元素(int num = bottomOut(stack)
)。这个操作会不断递归,直到找到栈底元素,并且在这个过程中会调整栈的结构,将栈底元素之上的元素依次下压一层。 - 然后再次调用
reverse
函数自身(reverse(stack)
),对剩余的栈(已经去除了栈底元素的栈)进行逆序操作。 - 最后将之前取出的栈底元素重新压入栈中(
stack.push(num)
)。通过这样的操作,每次将栈底元素取出,然后对剩余栈逆序,再将栈底元素放到栈顶,逐步实现整个栈的逆序。
- 先调用
- 递归终止条件(
bottomOut
函数原理- 递归终止条件(
if (stack.isEmpty())
)- 当栈为空时,说明已经找到了栈底元素(因为再没有元素可以弹出了),直接返回当前弹出的元素(
ans
)。
- 当栈为空时,说明已经找到了栈底元素(因为再没有元素可以弹出了),直接返回当前弹出的元素(
- 递归操作
- 先弹出栈顶元素并保存到
ans
中(int ans = stack.pop()
)。 - 然后递归调用
bottomOut
函数(if (stack.isEmpty())
判断不成立时),继续寻找下一个可能的栈底元素(此时的栈已经少了一个元素)。 - 在递归返回后,将之前保存的
ans
元素重新压入栈中(stack.push(ans)
),并返回找到的真正栈底元素(last
)。例如,对于栈[1, 2, 3]
,首先弹出3
保存到ans
,然后递归处理[1, 2]
,找到栈底元素1
,在返回过程中,将3
重新压入栈,最终返回1
作为栈底元素。
- 先弹出栈顶元素并保存到
- 递归终止条件(
代码实现
import java.util.Stack;
// 用递归函数逆序栈
public class Code05_ReverseStackWithRecursive {
public static void reverse(Stack<Integer> stack) {
if (stack.isEmpty()) {
return;
}
int num = bottomOut(stack);
reverse(stack);
stack.push(num);
}
// 栈底元素移除掉,上面的元素盖下来
// 返回移除掉的栈底元素
public static int bottomOut(Stack<Integer> stack) {
int ans = stack.pop();
if (stack.isEmpty()) {
return ans;
} else {
int last = bottomOut(stack);
stack.push(ans);
return last;
}
}
public static void main(String[] args) {
Stack<Integer> stack = new Stack<Integer>();
stack.push(1);
stack.push(2);
stack.push(3);
stack.push(4);
stack.push(5);
reverse(stack);
while (!stack.isEmpty()) {
System.out.println(stack.pop());
}
}
}
六.用递归函数排序栈
-
总体思路
- 这个算法通过一系列递归操作来对栈进行排序,在不使用额外容器(除了栈自身)的情况下,逐步将栈中的元素按照从小到大的顺序从栈顶到栈底排列。算法主要通过四个递归函数
deep
、max
、times
和down
来实现排序过程。
- 这个算法通过一系列递归操作来对栈进行排序,在不使用额外容器(除了栈自身)的情况下,逐步将栈中的元素按照从小到大的顺序从栈顶到栈底排列。算法主要通过四个递归函数
-
具体步骤
-
sort
函数原理- 首先,通过
deep
函数获取栈的深度(int deep = deep(stack)
)。 - 然后进入一个循环,只要栈的深度大于0就持续循环。在循环中:
- 调用
max
函数找到当前栈顶往下deep
层中的最大值(int max = max(stack, deep)
)。 - 调用
times
函数确定这个最大值在这deep
层中出现的次数(int k = times(stack, deep, max)
)。 - 调用
down
函数将这k
个最大值沉底(down(stack, deep, max, k)
)。 - 最后更新栈的深度(
deep -= k
),表示已经处理了k
个最大值,继续处理剩余的栈元素。
- 调用
- 首先,通过
-
deep
函数原理- 递归终止条件(
if (stack.isEmpty())
)- 当栈为空时,栈的深度为0,直接返回0。
- 递归操作
- 先弹出栈顶元素(
int num = stack.pop()
)。 - 然后递归调用
deep
函数来获取剩余栈的深度(int deep = deep(stack)
),此时得到的深度是剩余栈的深度,所以需要加1(deep = deep(stack) + 1
)。 - 最后将之前弹出的元素重新压入栈中(
stack.push(num)
),并返回栈的深度。
- 先弹出栈顶元素(
- 递归终止条件(
-
max
函数原理- 递归终止条件(
if (deep == 0)
)- 当要查找的层数为0时,表示已经没有元素可查找,返回
Integer.MIN_VALUE
,这是一个极小值,用于在比较中初始化为最小值。
- 当要查找的层数为0时,表示已经没有元素可查找,返回
- 递归操作
- 先弹出栈顶元素(
int num = stack.pop()
)。 - 然后递归调用
max
函数查找剩余deep - 1
层中的最大值(int restMax = max(stack, deep - 1)
)。 - 比较当前弹出的元素
num
和剩余栈中的最大值restMax
,取较大值作为新的最大值(int max = Math.max(num, restMax)
)。 - 最后将之前弹出的元素重新压入栈中(
stack.push(num)
),并返回最大值。
- 先弹出栈顶元素(
- 递归终止条件(
-
times
函数原理- 递归终止条件(
if (deep == 0)
)- 当要查找的层数为0时,表示已经没有元素可查找,返回0。
- 递归操作
- 先弹出栈顶元素(
int num = stack.pop()
)。 - 然后递归调用
times
函数查找剩余deep - 1
层中最大值(之前通过max
函数获取)出现的次数(int restTimes = times(stack, deep - 1, max)
)。 - 如果当前弹出的元素
num
等于最大值max
,则将剩余次数加1(int times = restTimes + (num == max? 1 : 0)
),否则保持剩余次数不变。 - 最后将之前弹出的元素重新压入栈中(
stack.push(num)
),并返回最大值出现的次数。
- 先弹出栈顶元素(
- 递归终止条件(
-
down
函数原理- 递归终止条件(
if (deep == 0)
)- 当要处理的层数为0时,表示已经到达了目标层,将
k
个最大值(之前通过times
函数获取)依次压入栈中(for (int i = 0; i < k; i++) {stack.push(max);}
)。
- 当要处理的层数为0时,表示已经到达了目标层,将
- 递归操作
- 先弹出栈顶元素(
int num = stack.pop()
)。 - 然后递归调用
down
函数处理剩余deep - 1
层,将最大值沉底(down(stack, deep - 1, max, k)
)。 - 如果弹出的元素
num
不等于最大值max
,则将其重新压入栈中(if (num!= max) {stack.push(num);}
),这样就保证了除最大值之外的元素顺序不变。
- 先弹出栈顶元素(
- 递归终止条件(
-
代码实现
import java.util.Stack;
// 用递归函数排序栈
// 栈只提供push、pop、isEmpty三个方法
// 请完成无序栈的排序,要求排完序之后,从栈顶到栈底从小到大
// 只能使用栈提供的push、pop、isEmpty三个方法、以及递归函数
// 除此之外不能使用任何的容器,数组也不行
// 就是排序过程中只能用:
// 1) 栈提供的push、pop、isEmpty三个方法
// 2) 递归函数,并且返回值最多为单个整数
public class Code06_SortStackWithRecursive {
public static void sort(Stack<Integer> stack) {
int deep = deep(stack);
while (deep > 0) {
int max = max(stack, deep);
int k = times(stack, deep, max);
down(stack, deep, max, k);
deep -= k;
}
}
// 返回栈的深度
// 不改变栈的数据状况
public static int deep(Stack<Integer> stack) {
if (stack.isEmpty()) {
return 0;
}
int num = stack.pop();
int deep = deep(stack) + 1;
stack.push(num);
return deep;
}
// 从栈当前的顶部开始,往下数deep层
// 返回这deep层里的最大值
public static int max(Stack<Integer> stack, int deep) {
if (deep == 0) {
return Integer.MIN_VALUE;
}
int num = stack.pop();
int restMax = max(stack, deep - 1);
int max = Math.max(num, restMax);
stack.push(num);
return max;
}
// 从栈当前的顶部开始,往下数deep层,已知最大值是max了
// 返回,max出现了几次,不改变栈的数据状况
public static int times(Stack<Integer> stack, int deep, int max) {
if (deep == 0) {
return 0;
}
int num = stack.pop();
int restTimes = times(stack, deep - 1, max);
int times = restTimes + (num == max ? 1 : 0);
stack.push(num);
return times;
}
// 从栈当前的顶部开始,往下数deep层,已知最大值是max,出现了k次
// 请把这k个最大值沉底,剩下的数据状况不变
public static void down(Stack<Integer> stack, int deep, int max, int k) {
if (deep == 0) {
for (int i = 0; i < k; i++) {
stack.push(max);
}
} else {
int num = stack.pop();
down(stack, deep - 1, max, k);
if (num != max) {
stack.push(num);
}
}
}
// 为了测试
// 生成随机栈
public static Stack<Integer> randomStack(int n, int v) {
Stack<Integer> ans = new Stack<Integer>();
for (int i = 0; i < n; i++) {
ans.add((int) (Math.random() * v));
}
return ans;
}
// 为了测试
// 检测栈是不是从顶到底依次有序
public static boolean isSorted(Stack<Integer> stack) {
int step = Integer.MIN_VALUE;
while (!stack.isEmpty()) {
if (step > stack.peek()) {
return false;
}
step = stack.pop();
}
return true;
}
// 为了测试
public static void main(String[] args) {
Stack<Integer> test = new Stack<Integer>();
test.add(1);
test.add(5);
test.add(4);
test.add(5);
test.add(3);
test.add(2);
test.add(3);
test.add(1);
test.add(4);
test.add(2);
sort(test);
while (!test.isEmpty()) {
System.out.println(test.pop());
}
// 随机测试
int N = 20;
int V = 20;
int testTimes = 20000;
System.out.println("测试开始");
for (int i = 0; i < testTimes; i++) {
int n = (int) (Math.random() * N);
Stack<Integer> stack = randomStack(n, V);
sort(stack);
if (!isSorted(stack)) {
System.out.println("出错了!");
break;
}
}
System.out.println("测试结束");
}
}
七. 打印n层汉诺塔问题的最优移动轨迹
算法原理
-
总体思路
- 汉诺塔问题是一个经典的递归问题。对于有(n)个圆盘的汉诺塔,其目标是将所有圆盘从起始柱(这里用“左”表示)移动到目标柱(这里用“右”表示),中间借助一个辅助柱(这里用“中”表示),并且在移动过程中要遵循大盘不能放在小盘上面的规则。
-
具体步骤
hanoi
函数原理- 首先判断(n)是否大于(0),如果是,则调用
f
函数开始解决汉诺塔问题,传入(n)以及表示起始柱、目标柱和辅助柱的字符串。
- 首先判断(n)是否大于(0),如果是,则调用
f
函数原理- 递归终止条件(
if (i == 1)
)- 当(i = 1)时,表示只剩下一个圆盘需要移动。此时直接输出移动这个圆盘的操作,即从起始柱移动到目标柱(
System.out.println("移动圆盘 1 从 " + from + " 到 " + to);
)。
- 当(i = 1)时,表示只剩下一个圆盘需要移动。此时直接输出移动这个圆盘的操作,即从起始柱移动到目标柱(
- 递归操作((i > 1))
- 首先,递归调用
f
函数来移动(i - 1)个圆盘,将这(i - 1)个圆盘从起始柱移动到辅助柱,此时目标柱作为辅助柱,辅助柱作为目标柱(f(i - 1, from, other, to);
)。 - 然后,输出移动最大圆盘(第(i)个圆盘)的操作,将这个最大圆盘从起始柱移动到目标柱(
System.out.println("移动圆盘 " + i + " 从 " + from + " 到 " + to);
)。 - 最后,再次递归调用
f
函数,将之前移动到辅助柱的(i - 1)个圆盘从辅助柱移动到目标柱,此时起始柱作为辅助柱(f(i - 1, other, to, from);
)。
- 首先,递归调用
- 递归终止条件(
- 例如,当(n = 3)时:
- 首先,通过
f(3, "左", "右", "中")
进入f
函数。 - 因为(i = 3 > 1),所以先执行
f(2, "左", "中", "右")
。 - 由于(i = 2 > 1),进一步执行
f(1, "左", "右", "中")
,此时满足i = 1
,输出“移动圆盘1从左到右”。 - 然后回到
f(2, "左", "中", "右")
,输出“移动圆盘2从左到中”,再执行f(1, "右", "中", "左")
,输出“移动圆盘1从右到中”。 - 最后回到
f(3, "左", "右", "中")
,输出“移动圆盘3从左到右”,接着执行f(2, "中", "右", "左")
,按照类似的递归过程完成剩下圆盘的移动。
代码实现
// 打印n层汉诺塔问题的最优移动轨迹
public class Code07_TowerOfHanoi {
public static void hanoi(int n) {
if (n > 0) {
f(n, "左", "右", "中");
}
}
public static void f(int i, String from, String to, String other) {
if (i == 1) {
System.out.println("移动圆盘 1 从 " + from + " 到 " + to);
} else {
f(i - 1, from, other, to);
System.out.println("移动圆盘 " + i + " 从 " + from + " 到 " + to);
f(i - 1, other, to, from);
}
}
public static void main(String[] args) {
int n = 3;
hanoi(n);
}
}