排列和组合

知识点补充

排列组合是面试中的热门考点 因为看似简单的排列组合可以有挺多的变形,根据变形,难度可以逐渐递增,而且排列组合本身有挺多的解法,能很好地区分一个侯选者的算法水平,排列组合如果用递归挺不容易理解的。

什么是排列

排列的定义:从n个不同元素中,任取 m (m≤n,m与n均为自然数,下同)个不同的元素按照一定的顺序排成一列,叫做从n个不同元素中取出m个元素的一个排列;从n个不同元素中取出m(m≤n)个元素的所有排列的个数,叫做从n个不同元素中取出m个元素的排列数,当 n = m 时,我们称这样的排列为全排列。

我们重新温习一下,以 1, 2, 3 这三个数字的全排列有多少种呢。

第一位我们可以选择 3 个数字,由于第二位不能与第一位相等,所以第二位只能选 2 个数字,第一,第二位既然选完了,那么第三位就只有 1 个数字可选了,所以总共有 3 x 2 x 1 = 6 种排列。
在这里插入图片描述
既然知道了什么是全排列,那我们来看看怎么用程序来打印全排列的所有情况:求 数字 1 到 n (n < 10) 的全排列。

排列的常用解法

这道题如果暂时没什么头绪,我们看看能否用最简单的方式来实现全排列,什么是最简单的方式,暴力穷举法!

暴力穷举法

大家仔细看上文中 1,2 ,3 的全排列,就是把所有情况全部列举出来了,所以我们用暴力穷举法怎么解呢,对每一位的每种情况都遍历出来组成所有的排列,再剔除重复的排列,就是我们要的全排列了。

/**
 * 求数字第 1 到 n 的全排列
 */
public void permutation(int n) {
    for(int i = 1; i < n + 1; i ++) {
        for(int j = 1; j < n + 1; j ++) {
            for(int k = 1; k < n + 1; k ++) {
                if (i != j && i != k && j != k) {
                    System.out.println(i + j + k);
                }
            }
        }
    }
}

时间复杂度是多少呢,做了三次循环,很显然是:n^3。

很多人一看时间复杂度这么高,多数都会嗤之以鼻,但是要我说,得看场景,就这题来说用暴力穷举法完全没问题,n 最大才 9 啊,总共也才循环了 9^3 = 729 次,这对现在的计算机性能来说简单不值一提,就这种场景来说,其实用暴力穷举法完全可行!

这里说句题外话,我们在学习的过程中一定要视场景选择合适的技术方案,有句话说:过早的性能优化是万恶之源,说的就是这个道理,这就好比,一个初创公司,dau 不过千,却要搞分布式,中间件,一个 mysql 表,记录不过一万,却要搞分库分表。。。这就闹笑话了,记住没有最牛逼的技术,只有最合适的技术!能解决当前实际问题的技术,就是好技术!

递归题解

我们先来观察一下规律,看下怎样才能找出排列是否符合递归的条件,因为如前文 所述,必须要找出题目是否能用递归才能再用递归四步曲来解题。
在这里插入图片描述
乍一看确实看不出什么所以然出来,那我们假设第一个数字已经选中了(假定为1),问题是不是转化为只求后面三位数的全排列了,发现没有,此时全排列从前面 n 位数的全排列转化成了求之后 n-1 位数的全排列了,问题从 n 变成了 n-1,规模变小了!而且变小的子问题与原问题具有相同的解决思路,都是从求某位开始的全排列!符合递归的条件!
在这里插入图片描述
既然我们发现排列符合递归条件,那我们就可以用递归四步曲来解了:

  1. 定义函数的功能 要求数字 1 到 n 的全排列,我们定义以下函数的功能为求从 k 位开始的全排列,数组 arr 存的是参与全排列的 1 到 n 这些数字
public void permutation(int arr[], k) {
}
  1. 寻找递推公式 注意上面形成递归的条件:第一个数字已经选中了!那第一位被选中有哪些情况呢,显然有以下几种情况:
    在这里插入图片描述
    即在第一位上把所有的数字都选一遍,怎么做才能把所有的数字都在第一位上都选一遍呢,把第一位与其他 n-1 位数分别交换即可注意每一次交换前都要保证是原始顺序),如下:
    在这里插入图片描述
    注意:第一步交换自己其实就是保持不变,因为我们要保证在第一位所有数字都能取到,如果移除了这一步,则第一位少了数字 1 ,全排列就漏了

这样我们就把第一位的所有数字都选了遍,之后只要对剩余的 n-1 位数做全排列即可(即调用第一步的函数)切忌再对 n-1 再做展开,只要我们发现递推关系就行了,千万不要陷入层层展开子问题的陷阱当中去注意要从函数的功能来理解,因为问题与子问题具有相同的解决思路,所以第 1 步定义的函数对子问题(求 n-1 ,n-2 … 的全排列)同样适用。

那递归的终止条件是什么呢 ,显然是从 n 缩小到对最后一位的全排列(此时 k 指向 arr 的最后一个元素)。
在这里插入图片描述

于是我们可以得出递推关系为: permutation(int arr[], k) = 选中第k位(将第k位与之后的 n- k 位分别交换) + permutation(int arr[], k+1)

  1. 将第二步的递推公式用代码表示出来补充到步骤 1 定义的函数中,补充后的函数如下:
 /**
     * 该函数实现数字1-n的全排列组合(递归实现)
     * 1、函数功能:题目要求数字 1 到 n 的全排列,我们定义函数的功能为求从 k 位开始的全排列,数组 arr 存的是参与全排列的 1到n这些数字
     * 2、递推公式:参考csdn相关的笔记
     * 3、结束条件:显然是从 n 缩小到对最后一位的全排列(此时 k 指向 arr 的最后一个元素)
     * @param arr
     * @param k
     */
    public static void permutation(int[] arr, int k){
        // 当 k 指向最后一个元素时,递归终止,打印此时的排列排列
        if(k==arr.length-1){
            System.out.println(Arrays.toString(arr));
        }
        for(int i=k;i<arr.length;i++){
            // 将 k 与之后的元素 i 依次交换,然后可以认为选中了第 k 位
            swap(arr,k,i);
            // 第 k 位选择完成后,求剩余元素的全排列
            permutation(arr, k+1);
            // 这一步很关键:将 k 与 i 换回来,保证是初始的顺序
            swap(arr,k,i);
        }

    }
    public static void swap(int[] arr,int k,int i){
        int tem=arr[k];
        arr[k]=arr[i];
        arr[i]=tem;
    }

注意
回过头去看上面的递归过程图中我们特意强调了注意每一次交换时都要保证是原始顺序,所以最后一个 swap 要做的事情就是每次交换第一个数字与后面被选中的那个数,做完之后元素的全排列之后,要把数字交换回来,以保证接下来再用第一位与其他位的数字进行交换前是原始的序列,这样才能保证第一位数字与之后的 n-1 个元素依次交换之后都是不重复的。

一定要从函数的功能去理解递归,全排列的函数从功能上可以这么理解,选中第 k 位 + 计算之后的 n-k 位的全排序, 而且由于是递归,之后的 n-k 位也可以重复调用同样的函数持续求解

什么是组合

看完了排列,我们来看看组合,首先我们还是先看看组合的定义:
组合(combination)是一个数学名词。一般地,从n个不同的元素中,任取m(m≤n)个元素为一组,叫作从n个不同元素中取出m个元素的一个组合。我们把有关求组合的个数的问题叫作组合问题

假设有数字1, 2, 3, 4, 要从中选择 2 个元素,共有多少种组合呢?共有 6 种
在这里插入图片描述
排列与组合最主要的区别就是排列是有序的,而组合是无序的,12 和 21 对组合来说是一样的。

现在我们来看看如果从 n 个元素中选出 m 的组合共有几种,之前详细地讲解了如何用递归解排列,相信大家应该对组合怎么使用递归应该有一个比较清晰的思路

我们一起来看看,假设要从 n 选 m 的组合的解题思路:
在这里插入图片描述
这里需要注意的是相对于全排列的每个元素都能参与排列不同,组合中的每个元素有两种状态,选中或未选中,所以形成递归分两种情况。

  1. 如果第一个元素选中,则要从之后的 n-1 个元素中选择 m-1 个元素
    在这里插入图片描述
  2. 如果第一个元素未被选中,则需要从之后的 n-1 个元素选择 m 个元素。
    在这里插入图片描述
    递归条件既然找到了,接下来我们就按递归三步曲来解下组合。

1、定义函数的功能:定义以下函数为从数组 arr 中第 k 个位置开始取 m 个元素(如下的 COMBINATION_CNT)

public static final int COMBINATION_CNT = 5;        // 组合中需要被选中的个数
public static void combination(int[] arr, int k, int[] select) {
}

这里我们额外引入了一个 select 数组,这个数组里的元素如果为1,则代表相应位置的元素被选中了,如果为 0 代表未选中:
在这里插入图片描述
如图示,以上表示 arr 的第 2,3 元素被选中作为组合。

2、递推公式:显然递推公式为,

combination(arr, k,m)  = (选中 k 位置的元素 +combination(arr, k+1) ) +  (不选中 k 位置的元素 +combination(arr, k+1) )

3、终止条件:终止条件有两个

  • 一个是被选中的元素已经等于我们要选择的组合个数了
  • 一个是 k (开始选取的数组索引) 超出数组范围了

代码为:

public static final int COMBINATION_CNT = 5;        // 组合中需要被选中的个数
public static void combination(int[] arr, int k, int[] select) {
    // 终止条件1:开始选取的数组索引 超出数组范围了
    if (k >= arr.length) {
        return;
    }

    int selectNum = selectedNum(select);
    // 终止条件2:选中的元素已经等于我们要选择的数组个数了
    if (selectNum == COMBINATION_CNT) {
        for (int j = 0; j < select.length; j++) {
            if (select[j] == 1) {
                System.out.print(arr[j]);
            }
        }
        System.out.print("\n");
     } else {
        // 第 k 位被选中
        select[k] = 1;
        combination(arr, k+1, select);

        // 第 k 位未被选中
        select[k] = 0;
        // 则从第 k+1 位选择 COMBINATION_CNT - selectNum 个元素
        combination(arr, k+1, select);
    }
}

public static void main(String[] args) {
    int[] arr = {1,2,3,4,5,6,7,8,9};
    int[] select = {0,0,0,0,0,0,0,0,0};
    // 一开始从 0 开始选 组合数
    combination(arr, 0, select);
}
  • 3
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值