【算法】——全排列算法讲解

news2024/11/24 22:34:39

前言:

今天,我给大家讲解的是关于全排列算。我会从三个方面去进行展开:

  1. 首先,我会给大家分析关于全排列算法的思想和定义;
  2. 紧接着通过手动实现出一个全排列代码来带大家见见是怎么实现的;
  3. 最后我会给出两道题帮助大家去进行理解记忆。


目录

前情摘要

(一)定义和公式讲解

1、定义

2、公式

(二)全排列的初始思想

(三)代码实现

1、递归不去重

2、递归去重

3、非递归实现

(四)题目讲解

1、字符串的排列

(五)总结


前情摘要

  • 在今后的找工作中,全排列相对来说还是一个比较常见问题。不管是不是做算法岗的,在许多的大公司面试中都会考察应聘者有关全排列的问题(就算不让你编写完整代码,也会让你描述大致的思路)。接下来,我就带领大家去学习有关全排列的相关知识。

(一)定义和公式讲解

1、定义

  • 全排列就是从n个不同元素中任取m(m≤n)个元素,按照一定的顺序排列起来,叫做从n个不同元素中取出m个元素的一个排列。
  • 当m=n时所有的排列情况叫全排列
  • 简单点来说就是:全排列的含义就是一个序列所有的排序可能性

我们通过简单的举例带大家直观的认识一下:

💨 假设现在数组中有【1 2 3】这样的三个元素,那么对其进行排列之后结果是什么呢?

我相信聪明的小伙伴们已经知道答案了,具体的如下:

  1. 123
  2. 132
  3. 213
  4. 231
  5. 321
  6. 312

 

💨 假设现在数组中有【1 2 3 4】这样的四个元素,那么对其进行排列之后结果是什么呢?

具体的如下:

  1. 1234
  2. 1243
  3. 1324
  4. 1342
  5. 1432
  6. 1423
  7. 2134
  8. 2143
  9. 2314
  10. 2341
  11. 2431
  12. 2413
  13. 3214
  14. 3241
  15. 3124
  16. 3142
  17. 3412
  18. 3421
  19. 4231
  20. 4213
  21. 4321
  22. 4312
  23. 4132
  24. 4123

2、公式

因此,综上所述,我们可以发现全排列得到的结果的数量跟数学中的全排列问题是一样的

  • 对于给定的序列,假设序列中 N个元素,对其进行全排列之后我们可以得到的排序数量为 N!

(二)全排列的初始思想

1、上述我给出了一个序列的全排列的结果以及全排列得到的最终的结果数量。

2、接下来,我简单的讲述一下上述全排列是如何得到的,对其具体的实现过程进行描述。

💨 具体过程如下:

  • 1、我们以上述数组中的【1 2 3 4】,这里是排序好的,为了更好的说明,我们以【2 1 3 4】为例;
  • 2、首先,我们先保持第一个元素不变,即【2】不变,对数组中剩余的元素【1 3 4】进行排序;
  • 3、同上,继续上述过程。此时我们保持剩余数组中的【1】不变,对【3 4】进行排序;
  • 4、在往下遍历。保持【3】不变,对【4】对进行全排列,由于【4】只有一个,它的排列只有一种:4。因此上述完成之后我们可以得到一种排序结果,即【2 1 3 4】;
  • 5、接下来因为【4】已经遍历过,所以不能在以【4】打头了。此时需要交换【3 4】这两个元素的位置,得到新的一种排序,即【2 1 4 3】 ;
  • 6、完成上述的操作之后,此时【3 4】的情况都写完了,现在进行回溯,则不能以【1】打头了;
  • 7、所以现在我们需要交换的对象就是【1 3】,依次类推,最终我们可以得到以【2】开头的排列有以下几种:
2134
2143
2314
2341
2431
2413
  • 8、在接下来就是改变第一个位置的元素,即通过剩余的【 1 3 4 】作为头元素,在重复上述过程即可得到最终的全排列结果。

(三)代码实现

通过上述的分析,我们不难发现一个问题那就是:

  1. 对于给定的序列,刚开始时我们知道序列的第一个元素,剩余的序列元素此时又可以看成是一个全排列的例子;
  2. 我们可以对剩余的序列进行与开始相同的操作,直到中只一个元素为止。这样我们就获得了所有的可能性。因此不难看出这是一个递归的过程。

接下来,我们简单的实现一下这样的代码:

1、递归不去重

  • 思路如下:
  1. 求n位的字符串的全排列,先确定第0位,然后对后面n-1位进行全排列,在对n-1为进行全排列时,先确定第1位,然后对后面的n-2位进行全排列,...由此得到递归函数和递归的结束条件;
  2. 因此解决n-1位元素的全排列就能解决n位元素的全排列了。

  • 代码如下:
//生成向量的所有排列
void permute(string str, int left, int right)
{
     //如果遍历到 left == right ,说明全排列已经做完了只需输出即可
    if (left == right) 
        cout<<str<<endl;
    else {
        // 递归情况:生成剩余元素的排列
        for (int i = left; i <= right; i++) 
        {
            // 将当前元素与第一个元素交换
            //保持第一个元素固定并生成其余元素的排列
            swap(str[left], str[i]);
            // 递归调用
            permute(str, left+1, right);
            //进行回溯
            swap(str[left], str[i]);
        }
    }
}

int main() 
{
    string str = "211";
    int size = str.size();
    permute(str, 0, size-1);
    return 0;
}

💨 上面的程序乍一看没有任何问题了。但是当我们去想去打印输出时,看看运行的结果会怎么样:

  • 结果如下:

  • 现象解释:
  1. 从上我们可以发现出现了好多的重复。
  2. 重复的原因当然是因为我们列举了所有位置上的可能性,而没有太多地关注其可能存在重复的数值。

2、递归去重

那么此时很多小伙伴就要问了,我们想实现去重的排列可不可以呢?答案当然是可以的。具体如下:

  • 思路如下:

去重跟不去重相差的其实就是对于元素的值的比较,我们设置一个 flag 标志位,来判断当前元素之后的元素是否于当前元素相同,再利用标志位去对其设限即可实现递归去重操作。

  • 代码如下:
void generatePermute(vector<int>& arry, int start, vector<vector<int>>& tmp) 
{
    if (start == arry.size()) {
        tmp.push_back(arry);
        return;
    }
    
    for (int i = start; i < arry.size(); i++) {
        // 检查与前一个元素的交换是否会产生重复项
        bool flag = false;
        for (int j = start; j < i; j++) {
            if (arry[i] == arry[j]) {
                flag = true;
                break;
            }
        }
        if (!flag) {
            swap(arry[start], arry[i]);
            generatePermute(arry, start+1, tmp);
            swap(arry[start], arry[i]);
        }
    }
}

vector<vector<int>> permute(vector<int>& arry) 
{
    vector<vector<int>> tmp;
    sort(arry.begin(), arry.end());
    generatePermute(arry, 0, tmp);
    return tmp;
}

int main() 
{
    vector<int> arry = {1, 1, 2};
    vector<vector<int>> res = permute(arry);
    for (auto& e1 : res) 
    {
        for (auto& e2 : e1) 
        {
            cout << e2 << " ";
        }
        cout << endl;
    }
    return 0;
}
  • 运行结果:

 

💨  我们在把 arry数组中的元素换为【1 2 2】,看最终结果是否正确,具体如下:

 

  • 代码解释:
  1. 上述实现生成了输入向量 arry 的所有排列,同时避免了重复。
  2. permute 函数首先对输入向量进行排序,以将相同值的元素分组在一起。

  3. 然后,它调用 generatePermute函数,以递归方式生成从给定索引开始的所有排列。generatePermute函数将当前索引处的元素与所有后续元素交换以生成排列,同时还检查与以前交换的元素的重复项。

3、非递归实现

由于非递归的方法是基于对元素大小关系进行比较而实现的,所以这里暂时不考虑存在相同数据的情况。

  • 代码如下:
void permute(vector<int>& arry) 
{
    sort(arry.begin(), arry.end());
    stack<pair<int, int>> tmp;
    tmp.push({-1, 0});

    while (!tmp.empty()) 
    {
        int i = tmp.top().first, j = tmp.top().second;
        tmp.pop();
        if (i >= 0) 
            swap(arry[i], arry[j]);
        if (j == arry.size() - 1) {
            // 输出排列
            for (int k = 0; k < arry.size(); k++) 

            {
                cout << arry[k] << " ";
            }
            cout << endl;
        } 
        else 
        {
            for (int k = arry.size() - 1; k > j; k--) 
            {
                tmp.push({j, k});
            }
            tmp.push({-1, j+1});
        }
    }
}

int main() 
{
    vector<int> arry = {0,2,2};
    permute(arry);

    return 0;
}
  • 代码解释:
  1. main() 函数创建一个包含三个元素的向量 arry,并以 arry 作为输入调用 permute() 函数;
  2. 首先,函数void permute(vector<int>&arry)将整数向量的引用作为输入,该向量使用STL中的sort()函数按升序排序;
  3. 其次,维护一堆整数对 {i,j},其中 i 和 j 是数组 arry 的索引;
  4. 最初,堆栈包含对 {-1, 0};
  5. 然后,该算法从堆栈中重复弹出顶部对,通过交换索引 i 和 j 处的元素来更新 arry(如果 i >= 0),并将新对推送到堆栈上;
  6.  最后,当栈中元素全被弹出,栈顶和栈底相遇,则所有状态全被穷尽。arry 的所有排列都已生成并打印到标准输出。

(四)题目讲解

接下来,我通过LeetCode上的三道题目给大家练练手,帮助大家理解记忆!!!

1、字符串的排列

链接如下:字符串的排列

💨 题目如下:

 💨 思路讲解:

  1. 上述我们是通过数组求全排列,而本题是字符串进行相关操作;
  2. 其实字符串与数组没有区别,一个是数字全排列,一个是字符全排列,因此大致思路与上述是类似;
  3. 为了便于后序去重,我们先优先按照字典序排序,因为排序后重复的字符就会相邻,后续递归找起来也很方便;
  4. 首先使用临时变量去组装一个排列的情况:每当我们选取一个字符以后,就确定了其位置,相当于对字符串中剩下的元素进行全排列添加在该元素后面,给剩余部分进行全排列就是一个子问题,因此可以使用递归;

 💨 代码展示:

class Solution {
public:
    vector<int> arry;
    vector<string> res;

    void dfs(const string &s, int num, string &tmp)
    {
        //临时字符串满了加入输出
        if(num == s.size())
        {
            res.push_back(tmp);
            return;
        }

        //遍历所有元素选取一个加入
        for(int i=0; i<s.size(); ++i)
        {
            if(arry[i] == 0)
            {
                tmp+=s[i];
                arry[i]=1;

                dfs(s,num+1,tmp);

                arry[i] = 0;
                tmp.pop_back();

                while(i<s.size()-1  && s[i] == s[i+1])
                    i++;
            }
        }
    }

    vector<string> permutation(string s) {
        //先对字符串进行排序处理
        sort(s.begin(), s.end());
        //标记每个位置的字符是否被使用过s
        string tmp;
        arry=vector<int>(s.size());

        dfs(s,0,tmp);
        return res;
    }
};

💨 代码解释:

1️⃣ 首先优先按照字典顺序进行排序;

2️⃣ 创建一个 arry 数组标记字典中的字母是否被选择;

3️⃣紧接着就进入dfs 函数进行递归操作;

4️⃣写出递归的条件:

    ①如果填充的字符串已经到达该最后的长度,那么说明这个字符串就是我们需要的;

    ②紧接着进行遍历操作,查找还未使用的序列,并尝试使用该字母:

  • 如果该怎么标记为0 ,则表示没有使用过,如果没有使用过则把它加入到 【tmp】里面,紧接着标记为1,表示已经使用过该字母;
  • 然后从当前位置的下一个位置开始在进行遍历操作;
  • 等到它从下一个位置遍历后来之后,我们就需要弹出【tmp】中的元素,并把它标记为0在进行下一步遍历

    ③如果我们已经使用了【i】位置处的值,那么后面与它相等的就不在使用,直接进行 ++操作

 💨 结果展示:

 


还有两道题,一道是去重的,另外一道是不去重的,大家可以自己尝试做做看,如果以下两题自己会做了,那么对于全排列算法,大家基本上就掌握了。

链接如下:全排列

链接如下:全排列 ||


(五)总结

到此,关于全排列算法就讲到这里了。希望本文对大家有所帮助,感谢各位的观看!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/499588.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ESP32单片机入门篇

目录 一、ESP32单片机的基本概念 1.双核架构 2. Wi-Fi和蓝牙功能 3. 集成多种外设 4. 支持多种操作系统 二、开发环境 1. Arduino IDE 2. ESP-IDF 三、开发语言 四、注意事项 五、代码例程 &#xff08;1&#xff09;点亮LED灯 1. 电路图 2. 代码 3. 代码注释 …

【精品】Java-Stream流详解

Java-Stream流详解 如何学会JDK8中的Stream流&#xff0c;用它来提高开发效率&#xff1f;创建不可变的集合&#xff08;Immutable 不可变的&#xff09;场景方法 初试 Stream 流Stream 流的思想Stream 流的作用Stream 流的使用步骤Stream 流的中间方法Stream 流的终结方法 如何…

STM32:利用PWM波控制飞盈电调过程和注意事项

STM32&#xff1a;利用PWM波控制电调过程和注意事项 在进行模型控制的过程中&#xff0c;如四旋翼无人机等&#xff0c;需要用到电机&#xff0c;这些电机需要通过电调来控制电机的转速。在电调模块中带有的说明书一般都是利用遥控器进行控制&#xff0c;有些情况需要自己通过…

【自然语言处理】【大模型】CodeGeeX:用于代码生成的多语言预训练模型

CodeGeeX&#xff1a;用于代码生成的多语言预训练模型 《CodeGeeX: A Pre-Trained Model for Code Generation with Multilingual Evaluations on HumanEval-X》 论文地址&#xff1a;https://arxiv.org/pdf/2303.17568.pdf 相关博客 【自然语言处理】【大模型】CodeGeeX&#…

二叉排序树

二叉排序树 文章目录 二叉排序树创建遍历删除完整代码 假如给你一个数列 (7, 3, 10, 12, 5, 1, 9)&#xff0c;要求能够高效的完成对数据的查询和添加。 使用数组 数组未排序&#xff1a; 优点&#xff1a;直接在数组尾添加&#xff0c;速度快。 缺点&#xff1a;查找速度慢. 数…

[图形学] 射线和线段之间的最小距离

1 说在前面 本文的主要内容来自于Unity引擎中Spline功能的一个函数&#xff0c;一开始我难以理解这几个向量运算的作用和几何意义&#xff0c;经过一番思考后总结如下&#xff1a; 该段代码实际上更像是两个直线之间寻找最短距离&#xff0c;然后判断该距离对应的点在其中一条…

STM32利用USB的HID与QT上位机通信

之前使用kingst的逻辑分析仪&#xff0c;打开上位机软件&#xff0c;插上带usb的硬件就可以通信&#xff0c;也不需要打开串口什么的&#xff0c;感觉很方便&#xff0c;于是借用一个周末研究下这个技术。本文主要是用于记录自己学习的过程&#xff0c;顺便分享下学习感悟。 首…

大数据周会-本周学习内容总结012

开会时间&#xff1a;2023.05.07 16:00 线下会议 目录 01【es数据同步至mysql】 1.1【在es中插入数据后能够同步到mysql中】 1.2【修改与删除es中的数据】 02【nifi】 2.1【Nifi的单机及分布式集群部署】 2.2【nifi集群&#xff0c;getFile简单使用nifi】 2.3【nifi使用…

如何利用Requestly提升前端开发与测试的效率,让你事半功倍?

痛点 前端测试 在进行前端页面开发或者测试的时候&#xff0c;我们会遇到这一类场景&#xff1a; 在开发阶段&#xff0c;前端想通过调用真实的接口返回响应在开发或者生产阶段需要验证前端页面的一些 异常场景 或者 临界值 时在测试阶段&#xff0c;想直接通过修改接口响应来…

Nuvoton NK-980IOT开发板 u-boot 编译

前言 最近搭建了 Nuvoton NK-980IOT开发板 的开发编译环境&#xff0c;记录一下 u-boot 的 编译流程 Nuvoton NK-980IOT开发板 资源还是比较的丰富的&#xff0c;可以用于 嵌入式Linux 或者 RT-Thread 的学习开发 开发板上电比较的容易&#xff0c;两根 USB 线即可&#xff0…

进程与线程(二)

进程同步、进程互斥 同步亦称直接制约关系&#xff0c;是指为完成某种任务而建立的两个或多个进程&#xff0c;这些进程因为需要在某些位置上协调它们的工作次序而产生的制约关系。进程间的直接制约关系就是源于他们之间的相互合作。 操作系统要提供“进程同步机制”来解决异…

Oracle的学习心得和知识总结(二十四)|Oracle数据库DBMS程序包解密方法及SQL Developer和Unwrapper的安装与使用

目录结构 注&#xff1a;提前言明 本文借鉴了以下博主、书籍或网站的内容&#xff0c;其列表如下&#xff1a; 1、参考书籍&#xff1a;《Oracle Database SQL Language Reference》 2、参考书籍&#xff1a;《PostgreSQL中文手册》 3、EDB Postgres Advanced Server User Gui…

android 隐藏底部虚拟按键

方法一 滑动屏幕 可重新显示出来 protected void hideBottomUIMenu() { //隐藏虚拟按键&#xff0c;并且全屏 if (Build.VERSION.SDK_INT <11 && Build.VERSION.SDK_INT < 19) { // lower api View v this.getWindow().getDecorView(); v.setSyst…

大众软件组织人事地震:传董事会被裁,5000人的CARIAD何去何从?

作者 | 德新 编辑 | 王博 外媒Business Insider近日爆出一则重磅消息&#xff1a;大众汽车集团CEO Oliver Blume&#xff08;奥博穆&#xff09;有意裁掉旗下软件组织CARIAD的整个董事会。其影响的高层包括&#xff0c;CARIAD CEO Dirk Hilgenberg、CTO Lynn Longo&#xff0c;…

influxdb时序型数据库基础

文章目录 什么是InfluxDB时序数据特点常见应该场景时序数据库解决什么问题InfluxDB的优势InfluxDB常用命令 什么是InfluxDB InfluxDB是一个开源的、高性能的时序型数据库&#xff0c;在时序型数据库DB-Engines Ranking上排名第一。 在介绍InfluxDB之前&#xff0c;先来介绍下…

机器学习随记(5)—决策树

手搓决策树&#xff1a;用决策树将其应用于分类蘑菇是可食用还是有毒的任务 温馨提示&#xff1a;下面为不完全代码&#xff0c;只是每个步骤代码的实现&#xff0c;需要完整跑通代码的同学不建议花时间看&#xff1b;适合了解决策树各个流程及代码实现的同学复习使用。 1 数据…

MySQL锁机制

目录 表级锁&行级锁 排他锁&共享锁 InnoDB行级锁 行级锁&#xff08;record lock&#xff09;&#xff1a; 间隙锁&#xff08;gap lock&#xff09;&#xff1a; 意向锁 InnoDB表级锁 MVCC&#xff08;多版本并发控制&#xff09; 已提交读的MVCC&#xff1a…

Linux下的shell

NC反向shell 1、查看shell类型 echo $SHELLchsh -s 需要修改shell的类型cat /etc/shells 查看存在哪些shell 然后反弹对应的shell&#xff08;正向连接&#xff09; //被控制端 nc -lvvp 8989 -e /bin/bash //控制端 nc 192.168.222.146(被控端ip) 8989 2、没有-e参数反…

css链接悬停时滑动的下划线效果

要创建链接悬停时滑动的下划线效果&#xff0c;可以向锚点标记添加伪元素&#xff0c;并使用 CSS 过渡动画来显示它。 先看效果&#xff1a; 在提供的代码中&#xff0c;a::after 选择器创建了一个伪元素&#xff0c;该伪元素位于 a 标记后面。该伪元素具有绿色背景颜色和 1…

KVM 架构和部署

建议使用centos和ubuntu 系统做实验&#xff0c;rocky 系列有些不太支持 宿主机环境准备 KVM需要宿主机CPU必须支持虚拟化功能&#xff0c;因此如果是在vmware workstation上使用虚拟机做宿主机&#xff0c;那么必须要在虚拟机配置界面的处理器选项中开启虚拟机化功能。 验证…