《剑指 Offer》专项突破版 - 面试题 113、114 和 115 : 详解拓扑排序(C++ 实现)

news2025/1/12 23:04:21

目录

前言

面试题 113 : 课程顺序

面试题 114 : 外星文字典

面试题 115 : 重建序列


 


前言

拓扑排序是指对一个有向无环图的节点进行排序之后得到的序列。如果存在一条从节点 A 指向节点 B 的边,那么在拓扑排序的序列中节点 A 出现在节点 B 的前面。一个有向无环图可以有一个或多个拓扑排序序列,但无向图或有环的有向图不存在拓扑排序

在讨论有向无环图拓扑排序算法之前先介绍两个概念:入度出度节点 v 的入度指的是以节点 v 为终点的边的数目,而节点 v 的出度是指以节点 v 为起点的边的数目。例如,在下图 (a) 的有向图中,节点 2 的入度是 1,出度是 2。

一种常用的拓扑排序算法是每次从有向无环图中取出一个入度为 0 的节点添加到拓扑排序序列之中,然后删除该节点及所有以它为起点的边。重复这个步骤,直到图为空或图中不存在入度为 0 的节点。如果最终图为空,那么图是有向无环图,此时就找到了该图的一个拓扑排序序列;如果最终图不为空并且已经不存在入度为 0 的节点,那么图中一定有环

下面对下图 (a) 中的图进行拓扑排序,该图中节点 1 的入度为 0,将该节点添加到拓扑排序序列中,并删除该节点及所有以该节点为起点的边,如下图 (b) 所示。接下来重复这个步骤,依次找到入度为 0 的节点 2、节点 3、节点 4、节点 5,如下图 (c)、(d)、(e) 所示,在先后删除这些节点之后图为空。因此,下图 (a) 中的图是有向无环图,它的拓扑排序序列为 [1, 2, 3, 4, 5]。

上述算法也可以用来判断一个有向图是否有环。如果执行上述步骤最终得到一个非空的图,并且图中所有节点的入度都大于 0,那么该图一定包含环。例如,下图中的有向图中的 3 个节点的入度都为 1,它们形成一个环。


面试题 113 : 课程顺序

题目

n 门课程的编号为 0 ~ n - 1。输入一个数组 prerequisites,它的每个元素 prerequisites[i] 表示两门课程的先修顺序。如果 prerequisites[i] = [a_i, b_i],那么必须先修完 b_i 才能修 a_i。请根据总课程数 n 和表示先修顺序的 prerequisites 得出一个可行的修课序列。如果有多个可行的修课序列,则输出任意一个可行的序列;如果没有可行的修课序列,则输出空序列。

例如,总共有 4 门课程,先修顺序 prerequisites 为 [[1, 0], [2, 0], [3, 1], [3, 2]],一个可行的修课序列是 0->2->1->3。

分析

将课程看成图中的节点,如果两门课程存在先修顺序那么它们在图中对应的节点之间存在一条从先修课程到后修课程的边,因此这是一个有向图。例如,可以根据先修顺序 prerequisites 为 [[1, 0], [2, 0], [3, 1], [3, 2]] 构建出如下图所示的有向图。例如,课程先修顺序 [1, 0] 对应在图中就有一条从节点 0 到节点 1 的边。

可行的修课序列实际上是图的拓扑排序序列。图中的每条边都是从先修课程指向后修课程,而拓扑排序能够保证任意一条边的起始节点一定排在终止节点的前面,因此拓扑排序得到的序列与先修顺序一定不会存在冲突,于是这个问题转变成如何求有向图的拓扑排序序列

对有向图进行拓扑排序的算法是每次找出一个入度为 0 的节点添加到序列中,然后删除该节点及所有以该节点为起点的边。重复这个过程,直到图为空或图中不存在入度为 0 的节点

代码实现

class Solution {
public:
    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
        unordered_map<int, vector<int>> graph;
        for (int i = 0; i < numCourses; ++i)
        {
            graph.insert({ i, vector<int>() });
        }
​
        vector<int> inDegrees(numCourses, 0);
        for (vector<int>& prerequisite : prerequisites)
        {
            graph[prerequisite[1]].push_back(prerequisite[0]);
            ++inDegrees[prerequisite[0]];
        }
​
        queue<int> q;
        for (int i = 0; i < numCourses; ++i)
        {
            if (inDegrees[i] == 0)
                q.push(i);
        }
​
        vector<int> order;
        while (!q.empty())
        {
            int course = q.front();
            q.pop();
            order.push_back(course);
            for (int next : graph[course])
            {
                --inDegrees[next];
                if (inDegrees[next] == 0)
                    q.push(next);
            }
        }
        return order.size() == numCourses ? order : vector<int>();
    }
};

上述代码先根据先修顺序构建出有向图 graph,graph 用一个 unordered_map 表示邻接表,它的键是先修课程,它的值是必须在键对应的课程之后学习的所有课程。同时,将每个节点的入度保存到数组 inDegrees 中,inDegrees[i] 表示节点 i 的入度

接下来用广度优先搜索算法实现拓扑排序。队列中保存的是入度为 0 的节点。每次从队列中取出一个节点,将该节点添加到拓扑排序序列中,然后找到该课程的后修课程并将它们的节点的入度减 1,这相当于删除从先修课程到后修课程的边。如果发现新的入度为 0 的节点,则将其添加到队列中。重复这个过程直到队列为空,此时要么图中所有节点都已经访问完毕,已经得到了完整的拓扑排序序列;要么剩下的还没有搜索到的节点形成一个环,已经不存在入度为 0 的节点


面试题 114 : 外星文字典

题目

一种外星语言的字母都是英文字母,但字母的顺序未知。给定该语言排序的单词列表,请推测可能的字母顺序。如果有多个可能的顺序,则返回任意一个。如果没有满足条件的字母顺序,则返回空字符串。例如,如果输入排序的单词列表为 ["ac", "ab", "bc", "zc", "zb"],那么一个可能的字母顺序是 "acbz"。

分析

这个题目比较难。如果在面试中遇到比较难的问题,比较有效地分析、解决问题的思路是从具体的例子中总结出解题规律。

在排序的单词列表 ["ac", "ab", "bc", "zc", "zb"] 中,一共出现了 4 个字母,即 'a'、'b'、'c' 和 'z'。需要根据单词的顺序确定这 4 个字母的顺序。由于 "ac" 排在 "ab" 的前面,因此字母 'c' 应该排在字母 'b' 的前面(即 'c' < 'b'),这是因为这两个单词的第 1 个字母相同,第 2 个字母不同,那么它们的第 2 个字母的顺序确定了两个单词的顺序。接下来两个相邻的单词是 "ab" 和 "bc",它们的第 1 个字母就不同,那么它们的顺序由第 1 个字母确定,所以 'a' < 'b'。类似地,可以根据 "bc" 排在 "zc" 的前面得知 'b' < 'z',根据 "zc" 排在 "zb" 的前面得知 'c' < 'b'。

由比较排序的单词列表中两两相邻的单词可知 'c' < 'b'、'a' < 'b' 和 'b' < 'z',现在需要找出一个包含 4 个字母的字母序列满足已知的 3 个字母的大小顺序。这看起来就是一个关于拓扑排序的问题,可以将每个字母看成图中的一个节点,如果已知两个字母的大小关系,那么图中就有一条从较小的字母指向较大的字母的边。根据字母的大小关系 'c' < 'b'、'a' < 'b' 和 'b' < 'z' 构建出的有向图如下图所示,该有向图有两个拓扑排序序列,"acbz" 和 "cabz",相应地输入的单词列表就有两个可能的字母顺序。

如果能够得出该有向图的拓扑排序序列,那么任意一条边的起始节点(较小的字母)在拓扑排序序列中一定出现在终止节点(较大的字母)的前面。因此,这个问题实质上是一个关于拓扑排序的问题。

代码实现

class Solution {
public:
    string alienOrder(vector<string>& words) {
        unordered_map<char, unordered_set<char>> graph;
        unordered_map<char, int> inDegrees;
        for (string& word : words)
        {
            for (char ch : word)
            {
                if (!graph.count(ch))
                {
                    graph.insert({ ch, unordered_set<char>() });
                    inDegrees.insert({ ch, 0 });
                }
            }
        }
​
        for (int i = 0; i < words.size() - 1; ++i)
        {
            string w1 = words[i], w2 = words[i + 1];
            int j = 0;
            while (j < w1.size() && j < w2.size())
            {
                char ch1 = w1[j], ch2 = w2[j];
                if (ch1 != ch2)
                {
                    if (!graph[ch1].count(ch2))
                    {
                        graph[ch1].insert(ch2);
                        ++inDegrees[ch2];
                    }
                    break;
                }
                ++j;
            }
​
            if (j < w1.size() && j == w2.size())
                return "";
        }
​
        queue<char> q;
        for (auto& kv : inDegrees)
        {
            if (kv.second == 0)
                q.push(kv.first);
        }
​
        string order;
        while (!q.empty())
        {
            char ch = q.front();
            q.pop();
            order.push_back(ch);
            for (char next : graph[ch])
            {
                --inDegrees[next];
                if (inDegrees[next] == 0)
                    q.push(next);
            }
        }
        return order.size() == graph.size() ? order : "";
    }
};

在上述代码中,图用 unordered_map 类型的变量 graph 以邻接表的形式表示。与某节点相邻的节点(即比某字母大的节点)则用一个 unordered_set 保存(在比较排序的单词列表中两两相邻的单词时可能得到相同的两个字母的大小关系,需要快速判断图中是否存在这样的一条边,所以不使用 vector,而是 unordered_set)。unordered_map 类型的变量 inDegrees 保存每个节点的入度(这里也不适合使用 vector)。代码一开始找出单词列表 words 中出现的所有字母并做相应的初始化

接下来比较单词列表 words 中两两相邻的单词,从头找出第 1 组不同的两个字母,在图中添加一条从较小的字母(ch1)指向较大的字母(ch2)的边

这里有一类特殊的输入需要特别注意。如果排在后面的单词是排在前面的单词的前缀,那么无论什么样的字母顺序都是不可能的。例如,如果排序的单词列表是 ["abc", "ab"],不管是什么样的字母顺序,"abc" 都不可能排在 "ab" 的前面,因此这是一个无效的输入,此时可以直接返回空字符串表示无效的字母顺序


面试题 115 : 重建序列

题目

长度为 n 的数组 org 是数字 1 ~ n 的一个排列,seqs 是若干序列,请判断数组 org 是否为可以由 seqs 重建的唯一序列。重建的序列是指 seqs 所有序列的最短公共超序列,即 seqs 中的任意序列都是该序列的子序列

例如,如果数组 org 为 [4, 1, 5, 2, 6, 3],而 seqs 为 [[5, 2, 6, 3], [4, 1, 5, 2]],因为用 [[5, 2, 6, 3], [4, 1, 5, 2]] 可以重建出唯一的序列 [4, 1, 5, 2, 6, 3],所以返回 true。如果数组 org 为 [1, 2, 3],而 seqs 为 [[1, 2], [1, 3]],因为用 [[1, 2], [1, 3]] 可以重建出两个序列,[1, 2, 3] 或 [1, 3, 2],所以返回 false。

分析

超序列和子序列是两个相对的概念。如果序列 A 中的所有元素按照先后的顺序都在序列 B 中出现,那么序列 A 是序列 B 的子序列,序列 B 是序列 A 的超序列

按照题目的要求,如果在 seqs 的某个序列中数字 i 出现在数字 j 的前面,那么由 seqs 重建的序列中数字 i 一定也要出现在数字 j 的前面。也就是说,重建序列的数字顺序由 seqs 的所有序列定义

可以将 seqs 中每个序列的每个数字看成图中的一个节点,两个相邻的数字之间有一条从前面数字指向后面数字的边。例如,由 [[5, 2, 6, 3], [4, 1, 5, 2]] 构建的有向图如下图 (a) 所示,由 [[1, 2], [1, 3]] 构建的有向图如下图 (b) 所示。

如果得到的是有向图的拓扑排序序列,那么任意一条边的起始节点在拓扑排序序列中一定位于终止节点的前面。因此,由 seqs 重建的序列就是由 seqs 构建的有向图的拓扑排序的序列。这个问题就转变成判断一个有向图的拓扑排序序列是否唯一

上图 (a) 中的有向图的拓扑排序序列是唯一的,其中,节点 4 是图中唯一一个入度为 0 的节点,删除该节点和以该节点为起始节点的边之后节点 1 是下一个唯一的入度为 0 的节点,重复这个过程直到图为空,就可以得到唯一的拓扑排序序列 [4, 1, 5, 2, 6, 3]。

而上图 (b) 则不然,其中,节点 1 是图中唯一一个入度为 0 的节点,删除该节点和以它为起始节点的边之后,节点 2 和节点 3 的入度都为 0,因此这个有向图有两个拓扑排序序列,分别为 [1, 2, 3] 和 [1, 3, 2]。

代码实现

class Solution {
public:
    bool sequenceReconstruction(vector<int>& nums, vector<vector<int>>& sequences) {
        unordered_map<int, unordered_set<int>> graph;
        unordered_map<int, int> inDegrees;
        for (vector<int>& sequence : sequences)
        {
            for (int i = 0; i < sequence.size(); ++i)
            {
                if (sequence[i] < 1 || sequence[i] > nums.size())
                    return false;
                
                if (!graph.count(sequence[i]))
                {
                    graph.insert({ sequence[i], unordered_set<int>() });
                    inDegrees.insert({ sequence[i], 0 });
                }
            }
​
            for (int i = 1; i < sequence.size(); ++i)
            {
                int prev = sequence[i - 1], cur = sequence[i];
                if (!graph[prev].count(cur))
                {
                    graph[prev].insert(cur);
                    ++inDegrees[cur];
                }
            }
        }
​
        queue<int> q;
        for (auto& kv : inDegrees)
        {
            if (kv.second == 0)
                q.push(kv.first);
        }
​
        vector<int> reconstruction;
        while (q.size() == 1)
        {
            int num = q.front();
            q.pop();
            reconstruction.push_back(num);
            for (int next : graph[num])
            {
                --inDegrees[next];
                if (inDegrees[next] == 0)
                    q.push(next);
            }
        }
​
        return reconstruction == nums;
    }
};

上述代码首先根据序列列表 seqs 构建有向图,有向图以邻接表的形式用 unordered_map 类型的 graph 保存。同时,统计每个节点的入度并保存到另一个 unordered_map 类型的 inDegrees 中。

接下来对构建的有向图按照广度优先搜索进行拓扑排序。队列 q 中保存的是入度为 0 的节点。每次从队列中取出一个节点添加到拓扑排序序列中,然后将所有与该节点相邻的节点的入度减 1(相当于删除所有以该节点为起始节点的边),如果发现有新的入度为 0 的节点则添加到队列之中。由于目标是判断图的拓扑排序序列是否唯一,而当某个时刻队列中的节点数目大于 1 时,就知道此时有多少个入度为 0 的节点,那么按照任意顺序排列这个入度为 0 的节点都能生成有效的拓扑排序序列,因此拓扑排序的序列不是唯一的。由此可知,上述代码只在队列的大小为 1 的时候重复添加入度为 0 的节点

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

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

相关文章

javaweb-数据库

数据库管理系统&#xff08;DataBase Management System&#xff0c;简称DBMS&#xff09; MySQL 官网&#xff1a;MySQL :: Developer Zone 安装 官网下载地址&#xff1a;MySQL :: Download MySQL Community Server (Archived Versions) 图形化工具 通常为了提高开发效…

2001-2021年上市公司制造业智能制造词频统计数据

2001-2021年上市公司制造业智能制造词频统计数据 1、时间&#xff1a;2001-2021年 2、来源&#xff1a;上市公司年报 3、指标&#xff1a;年份、股票代码、行业名称、行业代码、所属省份、所属城市、智能制造词频、智能制造占比(%) 4、范围&#xff1a;上市公司 5、样本量…

第十六届“华中杯”B 题使用行车轨迹估计交通信号灯周期问题

某电子地图服务商希望获取城市路网中所有交通信号灯的红绿周期,以便为司机提供更好的导航服务。由于许多信号灯未接入网络,无法直接从交通管理部门获取所有信号灯的数据,也不可能在所有路口安排人工读取信号灯周期信息。所以,该公司计划使用大量客户的行车轨迹数据估计交通…

关于Modbus TCP 编码及解码方式分析

一.Modbus TCP 基本概念 1.基本概念 ①Coil和Register   Modbus中定义的两种数据类型。Coil是位&#xff08;bit&#xff09;变量&#xff1b;Register是整型&#xff08;Word&#xff0c;即16-bit&#xff09;变量。 ②Slave和Master与Server和Client   同一种设备在不同…

谷歌收录工具有什么好用的?

如果是想促进谷歌的收录&#xff0c;其实能用的手段无非就两个&#xff0c;谷歌GSC以及爬虫池 谷歌gsc就不用说了&#xff0c;作为谷歌官方提供的工具&#xff0c;他能提供最准确的数据&#xff0c;并且可以提交每天更新的链接&#xff0c;进而促进收录&#xff0c;只要你的页面…

跟着野火从零开始手搓FreeRTOS(6)多优先级的配置

在 FreeRTOS 中&#xff0c;数字优先级越小&#xff0c;逻辑优先级也越小。 之前提过&#xff0c;就绪列表其实就是一个数组&#xff0c; 里面存的是就绪任务的TCB&#xff08;准确来说是 TCB 里面的 xStateListItem 节点&#xff09;&#xff0c;数组的下标对应任务的优先级&a…

鸿蒙(HarmonyOS)性能优化实战-多线程共享内存

概述 在应用开发中&#xff0c;为了避免主线程阻塞&#xff0c;提高应用性能&#xff0c;需要将一些耗时操作放在子线程中执行。此时&#xff0c;子线程就需要访问主线程中的数据。ArkTS采用了基于消息通信的Actor并发模型&#xff0c;具有内存隔离的特性&#xff0c;所以跨线…

Redis底层数据结构之Dict

目录 一、概述二、Dict结构三、Dictht结构四、DictEntry结构五、核心特性 上一篇文章 reids底层数据结构之quicklist 一、概述 Redis 的 Dict 是一个高效的键值对映射数据结构&#xff0c;采用双哈希表实现以支持无锁的渐进式 Rehash&#xff0c;确保扩容或缩容时的高效性能。…

计算二维主应力的前端界面

<!DOCTYPE html> <html> <head> <title>二维主应力</title> </head> <body> <h2>计算二维主应力</h2> <form> <label for"input1">σ_1(Mpa):</label> <input type"t…

Docker搭建Maven仓库Nexus

文章目录 一、简介二、Docker部署三、仓库配置四、用户使用Maven五、管理Docker镜像 一、简介 Nexus Repository Manager&#xff08;简称Nexus&#xff09;是一个强大的仓库管理器。 Nexus3支持maven、docker、npm、yum、apt等多种仓库的管理。 建立了 Maven 私服后&#xf…

Android—— log的记忆

一、关键log 1.Java的 backtrace(堆栈log) 上述是一个空指针异常&#xff0c;问题出现在sgtc.settings&#xff0c;所以属于客户UI问题。 2.WindowManager(管理屏幕上的窗口和视图层次结构) 3.ActivityManager(管理应用程序生命周期和任务栈) 4.wifi操作 (1) 连接wifi&#…

初入单元测试

单元测试&#xff1a;针对最小的功能单元(方法)&#xff0c;编写测试代码对其进行正确性测试 Junit可以用来对方法进行测试&#xff0c;虽然是有第三方公司开发&#xff0c;但是很多开发工具已经集成了&#xff0c;如IDEA。 Junit 优点&#xff1a;可以灵活的编写测试代码&am…

互联网大佬座位排排坐:马化腾第一,雷军第二

关注卢松松&#xff0c;会经常给你分享一些我的经验和观点。 这是马化腾、雷军、张朝阳、周鸿祎的座位&#xff0c;我觉得是按照互联网地位排序的。 马化腾坐头把交椅&#xff0c;这个没毛病&#xff0c;有他在的地方&#xff0c;其他几位都得喊声“大哥”。雷军坐第二把交椅…

世界读书日,解决沟通问题或提升沟通能力,听书690本的我最推荐的3本书

前言 今天是世界读书日&#xff0c;好想找个图书馆泡一天&#xff0c;认认真真读一本书。从去年开始对读书感兴趣&#xff0c;前前后后目前为止一共听了 690 本书&#xff0c;有社科类&#xff0c;心理学类&#xff0c;历史类&#xff0c;脑科学类&#xff0c;管理类&#xff0…

深入探索Android Service:后台服务的终极指南(上)

引言 在Android应用开发中&#xff0c;Service是一个至关重要的组件&#xff0c;它允许开发者执行后台任务&#xff0c;而无需用户界面。然而&#xff0c;Service的启动方式、生命周期管理以及与其他组件的交互&#xff0c;对于很多开发者来说仍然是一个难点。本文将深入剖析S…

CyclicBarrier(循环屏障)源码解读与使用

&#x1f3f7;️个人主页&#xff1a;牵着猫散步的鼠鼠 &#x1f3f7;️系列专栏&#xff1a;Java全栈-专栏 &#x1f3f7;️个人学习笔记&#xff0c;若有缺误&#xff0c;欢迎评论区指正 目录 1. 前言 2. 什么是CyclicBarrier&#xff1f; 3. CyclicBarrier与CountDownL…

redis常用数据结构

redis常用数据结构 Redis 底层在实现下面数据结构的时候&#xff0c;会进行特定的优化&#xff0c;来达到节省时间/空间的效果。 内部结构 String raw&#xff08;最基本的字符串&#xff09;&#xff0c;int&#xff08;实现计数功能&#xff0c;当value为整数的时候会用整…

碳课堂|什么是碳市场?如何进行碳交易?

近年来&#xff0c;随着全球变暖问题日益受到重视&#xff0c;碳达峰、碳中和成为国际社会共识&#xff0c;为更好地减缓和适应气候变化&#xff0c;同时降低碳关税风险&#xff0c;以“二氧化碳的排放权利”为商品的碳交易和碳市场应时而生。 一、什么是碳交易、碳市场 各国…

JavaSE——程序逻辑控制

1. 顺序结构 顺序结构 比较简单&#xff0c;按照代码书写的顺序一行一行执行。 例如&#xff1a; public static void main(String[] args) {System.out.println(111);System.out.println(222);System.out.println(333);} 运行结果如下&#xff1a; 如果调整代码的书写顺序 , …

[Android]Jetpack Compose加载图标和图片

一、加载本地矢量图标 在 Android 开发中使用本地矢量图标是一种常见的做法&#xff0c;因为矢量图标&#xff08;通常保存为 SVG 或 Android 的 XML vector format&#xff09;具有可缩放性和较小的文件大小。 在 Jetpack Compose 中加载本地矢量图标可以使用内置的支持&…