目录
题目1:给你一个字符串,要求打印打印出这个字符串的全部子序列(子序列不能重复)
题目2:打印一个字符串的全部排列。
题目3:针对题目2,要求去除重复元素
题目4:给定一个字符串,要求打印这个字符串中字符能够能够组成的全部子集,子集无重复值。
题目5:给你一个栈,请你逆序这个栈,不能申请额外的数据结构, 只能使用递归函数。
递归是一个过渡,为动态规划做准备。递归写好了,可以大量的优化代码结构,但是缺点是逻辑不好懂。一个号的递归,参数设计尤为重要。
题目1:给你一个字符串,要求打印打印出这个字符串的全部子序列(子序列不能重复)
这一题需要注意的是,给定了一个字符串,而字符串肯定是有顺序的,比如字符串为abc, 那么他的子序肯定不会出现cba。因为这样的顺序完全被颠倒了。所有,子序是不能改变原字符串的整体顺序的,在保持整体顺序的情况下自由组合。
package code03.递归_06;
import java.util.ArrayList;
import java.util.List;
/**
* 给你一个字符串,要求打印打印出这个字符串的全部子序列(子序列不能重复)
*/
public class StringSequence_02
{
public static List<String> getChildSuquence(String str)
{
char[] chars = str.toCharArray();
List<String> result = new ArrayList<>();
//path这个参数很重要,它就是一个逐层拼接完整的字符串
//根据递归遍历的层数,组成不同的字符串,非常灵巧
String path = "";
func(chars, result, 0, path);
return result;
}
/**
* 字符串是有序的,所以子序列必须要按照字符串的整体序列进行。 比如abc, 子序列肯定不能
* 出现ba或者ca这种情况。
*
* 当前递归的逻辑是从后往前,就是找到最后一个元素。 下标依次是:
* N-2... N
* N-3...N
* N-4...N
* 0...N
* N代表的是数组最后一个元素的下标。 而 N+1 就是代表的是数组的长度,
* 此时,已经没有对应的下标了,所以认为是出口
*
* 假设字符串为abc, 那么它的子集为
* 1.空字符串
* 2.c
* 3.b
* 4.bc
* 5.a
* 6. 52组合得到ac,53z组合河道ab,54组合得到abc
*/
static void func (char[] chars, List<String> list, int index, String path)
{
/**
* 递归的出口。
* index是最后一个字符的下标。而index+1就是数组的长度
* 所以就需要跳出递归了
*/
if (index == chars.length) {
//排除重复元素
if (!list.contains(path)) {
list.add(path);
}
return;
}
//一直往下找,直到数组的尽头,默认给个空字符串
func(chars, list, index + 1, path); //此处index+1用的很灵巧,因为index值本身是没有改变的
//此处会改变path的值,用的很灵巧
//每一个方法对弈一个方法栈,而变量path存在本地变量表,方法结束,本地变量表销毁
//因此,此处改变的path只是在当前方法中生效。不会影响其他方法中相同名称的path值
path = path + String.valueOf(chars[index]);
//此处开始根据路径进行拼接。
func(chars, list, index + 1, path);
}
public static void main(String[] args) {
String test = "abc";
//String test = "abcc";
List<String> result = getChildSuquence(test);
for (String str : result) {
System.out.println(str);
}
}
}
这一题解释一下:
设计思路是根据数组的下标索引进行的,而递归是一直找到下标最后一个元素为止。
此时,如果最后一个元素为c。如果c不被包括,那么path为空字符串,func搜集到空字符串。如果c被包括,那就 func 搜集到的就是 空字符串与c的拼接。 即c。递归结束,返回上一层
此时,返回到元素b处,此时不包括b的元素搜集完毕。即搜集到空字符串和c。path重新组合。 此时path为空字符串和b的组合。再次func操作。而func内部又分为不包含path和包含path两种逻辑,此时针对的是c。如果不包含c,那就搜集到b。如果包含c,那就收集到了b和c的组合,即bc
以此类推.......
整个流程图如下:
搜集结果为: 空字符串、c、b、bc、a、ac、ab、abc
题目2:打印一个字符串的全部排列。
这个字符串的排序指的是可以任意变换字符串的顺序,而不是子字符串。 比如: abc 其中一个就是cba 但是ab a ac等都不属于全部排序
package code03.递归_06;
import java.util.ArrayList;
import java.util.List;
/**
* 给定一个字符串,要求打印这个字符串的全部排序。
* 注意:这个字符串的排序指的是可以任意变换字符串的顺序,而不是子字符串
* 比如: abc 其中一个就是cba 但是ab a ac等都不属于全部排序
*/
public class PrintAllFullStr_03
{
public static List<String> getAllStr(String str)
{
List<String> result = new ArrayList<>();
String path = "";
char[] chars = str.toCharArray();
List<Character> list = new ArrayList<>();
for (char cha : chars) {
list.add(cha);
}
func(list, result, path);
return result;
}
static void func (List<Character> chars, List<String> result, String path)
{
if (chars.isEmpty()) {
result.add(path);
}
else {
for (int i = 0; i < chars.size(); i++) {
char cur = chars.get(i);
//清除现场
chars.remove(i);
func(chars, result, path + cur);
//恢复现场
chars.add(i, cur);
}
}
}
public static void main(String[] args) {
String test = "abc";
//String test = "acc";
List<String> result = getAllStr(test);
for (String str : result) {
System.out.println(str);
}
}
}
其实,这一道题,还有另一种比较通用的解法,思路与题目1基本一样。
package code03.递归_06;
import java.util.ArrayList;
import java.util.List;
/**
* 给定一个字符串,要求打印这个字符串的全部排序。
* 注意:这个字符串的排序指的是可以任意变换字符串的顺序,而不是子字符串
* 比如: abc 其中一个就是cba 但是ab a ac等都不属于全部排序
*/
public class PrintAllFullStr_03_opt
{
public static List<String> getAllStr(String str)
{
char[] chars = str.toCharArray();
List<String> result = new ArrayList<>();
String path = "";
func(chars, result, 0, path);
return result;
}
static void func (char[] chars, List<String> result, int index, String path)
{
if (index == chars.length) {
result.add(path);
}
else {
for (int i = index; i < chars.length; i++) {
swap(chars, index, i);
func(chars, result, index + 1, path + chars[index]);
swap(chars, index, i);
}
}
}
public static void swap(char[] chs, int i, int j) {
char tmp = chs[i];
chs[i] = chs[j];
chs[j] = tmp;
}
public static void main(String[] args) {
String test = "abc";
//String test = "acc";
List<String> result = getAllStr(test);
for (String str : result) {
System.out.println(str);
}
}
}
针对通用解法,我们发现,既然chs始终是动态排序的,但是内部元素并不会减少。因此,可以稍微优化一下参数:
package code03.递归_06;
import java.util.ArrayList;
import java.util.List;
/**
* 给定一个字符串,要求打印这个字符串的全部排序。
* 注意:这个字符串的排序指的是可以任意变换字符串的顺序,而不是子字符串
* 比如: abc 其中一个就是cba 但是ab a ac等都不属于全部排序
*/
public class PrintAllFullStr_03_opt2
{
public static List<String> getAllStr(String str)
{
char[] chars = str.toCharArray();
List<String> result = new ArrayList<>();
func(chars, result, 0);
return result;
}
static void func (char[] chars, List<String> result, int index)
{
if (index == chars.length) {
result.add(String.valueOf(chars));
}
else {
for (int i = index; i < chars.length; i++) {
swap(chars, index, i);
func(chars, result, index + 1);
swap(chars, index, i);
}
}
}
public static void swap(char[] chs, int i, int j) {
char tmp = chs[i];
chs[i] = chs[j];
chs[j] = tmp;
}
public static void main(String[] args) {
String test = "abc";
//String test = "acc";
List<String> result = getAllStr(test);
for (String str : result) {
System.out.println(str);
}
}
}
其实,这一道题的解题思路就是逐步确定每个位置,而记录位置的参数是index。然后for循环,把剩余的数组元素,依次填补当前位置。以此类推,典型的深度优先遍历算法。流程图如下:
题目3:针对题目2,要求去除重复元素
去除重复元素,其实分为结果过滤和条件过滤。结果过滤就是走完所有的流程,到最后搜集结果的时候通过结果集去重。而条件过滤则是从源头排除,性能更高。以题目2为例。如果字符串为acc. 下标为1的时候,c出现一次,下一个c再次出现的时候,直接排除掉。
结果过滤:
package code03.递归_06;
import java.util.ArrayList;
import java.util.List;
/**
* 给定一个字符串,要求打印这个字符串的全部排序, 并且
* 这些排完序的字符串无重复数据
*
* 结果过滤: 执行完流程,待放入集合中时判断是否重复进行过滤。
* 缺点很明显, 不管是否重复,所有流程都会走一遍,性能低
*
*/
public class PrintAllFullStrNoRepeat_04
{
public static List<String> getAllStr(String str)
{
char[] chars = str.toCharArray();
List<String> result = new ArrayList<>();
String path = "";
func(chars, result, 0);
return result;
}
static void func (char[] chars, List<String> result, int index)
{
if (index == chars.length) {
/**
* 结果过滤,可以直接使用set
* 此处使用的是list,需要判断集合是否存在重复元素
*/
if (!result.contains(String.valueOf(chars))) {
result.add(String.valueOf(chars));
}
}
else {
/**
* index代表当前位置开头。
* 比如index为0, 就是锁定0位置的开头数组。 for循环
* 是找到 a b c 分别占据0的位置作为开头。 比如 a**. b**, c**
*
* 当index为1时,代表分别找到所有数据作为1下标位置开头。 即*a*, *b*, *c*
*
*/
for (int i = index; i < chars.length; i++) {
swap(chars, index, i);
func(chars, result, index + 1);
swap(chars, index, i);
}
}
}
public static void swap(char[] chs, int i, int j) {
char tmp = chs[i];
chs[i] = chs[j];
chs[j] = tmp;
}
public static void main(String[] args) {
//String test = "abc";
String test = "acc";
List<String> result = getAllStr(test);
for (String str : result) {
System.out.println(str);
}
}
}
条件过滤:
package code03.递归_06;
import java.util.ArrayList;
import java.util.List;
/**
* 给定一个字符串,要求打印这个字符串的全部排序, 并且
* 这些排完序的字符串无重复数据
*
* 结果过滤: 执行完流程,待放入集合中时判断是否重复进行过滤。
* 缺点很明显, 不管是否重复,所有流程都会走一遍,性能低
*
*/
public class PrintAllFullStrNoRepeat_04_opt
{
public static List<String> getAllStr(String str)
{
char[] chars = str.toCharArray();
List<String> result = new ArrayList<>();
String path = "";
func(chars, result, 0);
return result;
}
static void func (char[] chars, List<String> result, int index)
{
if (index == chars.length) {
result.add(String.valueOf(chars));
}
else {
//ASCII码的范围是0-255, 它可以表示所有的字符
//每一次递归都new一个,用的很巧妙。需要仔细揣摩
boolean[] visited = new boolean[256];
for (int i = index; i < chars.length; i++) {
/**
* index是当前位置。第一次进入肯定是通过的。占据当前位置
*
* 原始字符数组为 a b c 原始数组为acc
* 1. a b c a c c 原始的c c位置没变。 也就是说第二个位置被第一个c占据了一遍
* 2. a c b a c c 此时的 c c是交换后的位置。也就是第二c想要再次来占据第二个位置作为后续数组的开头,重复占领该位置,直接pass掉
* 3. b a c c a c
* 4. b c a c c a
* 5. c a b c a c 同理
* 6 c b a c c a 同理
*/
if (!visited[chars[i]]) {
visited[chars[i]] = true;
swap(chars, index, i);
func(chars, result, index + 1);
swap(chars, index, i);
}
}
}
}
public static void swap(char[] chs, int i, int j) {
char tmp = chs[i];
chs[i] = chs[j];
chs[j] = tmp;
}
public static void main(String[] args) {
//String test = "abc";
String test = "accb";
List<String> result = getAllStr(test);
for (String str : result) {
System.out.println(str);
}
}
}
题目4:给定一个字符串,要求打印这个字符串中字符能够能够组成的全部子集,子集无重复值。
弄懂了题目2,这一题其实就很简单了。
package code03.递归_06;
import java.util.ArrayList;
import java.util.List;
/**
* 给定一个字符串,要求打印这个字符串的全部排序。
* 注意:这个字符串的排序指的是可以任意变换字符串的顺序,而不是子字符串
* 比如: abc 其中一个就是cba 但是ab a ac等都不属于全部排序
*/
public class PrintAllStr_05_opt
{
public static List<String> getAllStr(String str)
{
char[] chars = str.toCharArray();
List<String> result = new ArrayList<>();
String path = "";
func(chars, result, 0, path);
return result;
}
static void func (char[] chars, List<String> result, int index, String path)
{
if (index == chars.length) {
result.add(path);
}
else {
boolean[] visited = new boolean[256];
for (int i = index; i < chars.length; i++) {
visited[chars[i]] = true;
swap(chars, index, i);
func(chars, result, index + 1, path + chars[index]);
if (index < chars.length-1) {
result.add(path + chars[index]);
}
swap(chars, index, i);
}
}
}
public static void swap(char[] chs, int i, int j) {
char tmp = chs[i];
chs[i] = chs[j];
chs[j] = tmp;
}
public static void main(String[] args) {
String test = "abc";
//String test = "acc";
List<String> result = getAllStr(test);
System.out.println("length is " + result.size());
for (String str : result) {
System.out.println(str);
}
}
}
题目5:给你一个栈,请你逆序这个栈,不能申请额外的数据结构, 只能使用递归函数。
package code03.递归_06;
import java.util.Stack;
/**
* 题目:
* 给你一个栈,请你逆序这个栈,
* 不能申请额外的数据结构,
* 只能使用递归函数。 如何实现?
*/
public class StackReverse_06 {
public static void reverse (Stack<Integer> stack)
{
if (stack == null || stack.isEmpty()) {
return;
}
//获取栈底元素
int result = func(stack);
/**
* 继续逆转栈中剩余的元素
* 例如栈中从上到下为 3 2 1
* 第一次 获取result 1
* 第二次 获取result 2
* 第三次 获取result 3
*/
reverse(stack);
/**
* 那么最后一次递归结束后,放入元素
* 顺序是 放入 1 2 3. 顺序变为 1 2 3
*/
stack.push(result);
}
/**
* 当前递归,每次结束的时候找到的都是栈底元素
* @param stack
* @return
*/
public static int func(Stack<Integer> stack)
{
//如果cur是栈底元素,那么当前pop以后栈就空了
//直接返回cur
int cur = stack.pop();
if (stack.isEmpty()) {
return cur;
}
else {
//栈底元素
int last = func(stack);
//倒数第二个放入栈底
stack.push(cur);
return last;
}
}
public static void main(String[] args) {
Stack<Integer> test = new Stack<Integer>();
test.push(1);
test.push(2);
test.push(3);
test.push(4);
test.push(5);
System.out.println("逆转前顺序为: 5 4 3 2 1");
reverse(test);
System.out.print("逆转后顺序为: ");
while (!test.isEmpty()) {
System.out.print(test.pop());
System.out.print(" ");
}
}
}
这一题很难,难在不允许使用其他的结构来辅助。只能在栈中自己折腾。既然是逆序,那么放置的顺序就是要反过来,栈的特性是先进后出,这就复杂了起来,下面看下整个流程: