LeetCode之路:383. Ransom Note

来源:互联网 发布:apache thrift book 编辑:程序博客网 时间:2024/05/22 13:46

一、引言

这是一道非常“时髦”的题目,正好最近也在发生勒索病毒的事情。因此这个标题:

Ransom Note

我百度翻译是“勒索信”的含义(英语水平不高,如果有其他的含义当我没说)。

而就这道题目来说,说实话仅仅是挂靠了一个Ransom Note 的背景而已,实际上的模型是非常简单的:

Given an arbitrary ransom note string and another string containing letters from all the magazines, write a function that will return true if the ransom note can be constructed from the magazines ; otherwise, it will return false.

Each letter in the magazine string can only be used once in your ransom note.

Note:
You may assume that both strings contain only lowercase letters.
canConstruct(“a”, “b”) -> false
canConstruct(“aa”, “ab”) -> false
canConstruct(“aa”, “aab”) -> true

题目信息比较少,简单翻译一下:

给定一个随机内容的勒索信字符串和一个包含着一些字母的来自于杂志的字符串,请写一个方法,当勒索信字符串可以完全由杂志字符串中的字母全部构造出来的时候返回真值,否则,返回假值。

来自于杂志中的每个字母只能在勒索信句子中使用一次。

注意:
你可以假定所有的句子中都只含有小写字母

上述的例子很简单,这里还是简单分析下:

  1. canConstruct(“a”, “b”) -> false :这里我们调用了 canConstruct 函数,输入了勒索信字符串 “a” 和杂志字符串 “b”,这里我们发现 后者并没有字母 “a”,于是我们返回假值

  2. canConstruct(“aa”, “ab”) -> false:这里我们输入了勒索信字符串 “aa” 和杂志字符串 “ab”,然后我们发现勒索信字符串中有两个字母 “a”,而杂志字符串中只有一个 “a”,因此我们很遗憾的返回了假值

  3. canConstruct(“aa”, “aab”) -> true:这里我们输入了勒索字符串 “aa” 和杂志字符串 “aab”,此时我们发现我们需要的字符 “a” 需要两个,并且在杂志字符串中存在,于是我们很高兴的返回了真值

之所以要将题目中给出的例子这么详尽地分析出来,是因为这是一个我们进行逻辑判断的过程,后面的程序设计过程中,就要对我们刚才的判断过程进行抽象。

二、经常遇到的问题:查找是否存在

我们在程序设计过程中,总会遇到这样的问题:

查找指定元素是否在指定集合中存在

而回答这个问题的答案是:

std::set 或者 std::map

这里我们先使用一个比较容易想到的方案进行分析,然后再一步步优化。

让我们看看这道题:

  1. 首先,我们拿到了一个字符串数组 ransomNote

  2. 然后,我们拿着这个 ransomNote 中的所有出现的元素去 magazines 中去查找是否存在,并且还要根据 ransomNote 中元素的出现次数进行比较

  3. 最后,我们根据比较的结果返回真假值,如果 ransomNote 中出现的所有的数值都在 magazines 字符串中存在,则返回真值,否则返回假值

按照上述分析,我写出了第一个版本的代码如下:

// my solution 1 use unordered_map , runtime = 35 msclass Solution1 {public:    bool canConstruct(string ransomNote, string magazine) {        unordered_map<char, int> char_count;        for (auto c : magazine) ++char_count[c];        for (auto c : ransomNote)            if (char_count.find(c) == char_count.end() || char_count[c] == 0) return false;            else --char_count[c];            return true;    }};

这里需要解释下:

  1. 首先,我使用了 std::unordered_map 对于 magazine 字符串中出现的字符进行了统计,以 “字符:出现次数”的键值对进行了初始化,拿到了杂志字符串中各个字符出现的次数映射表

  2. 然后,我对 ransomNote 字符串进行了字符为单位的遍历过程,对于每个字符,首先判断这个字符是否在 char_count 映射中,如果不存在,则直接返回假值,如果存在,还要检查它的出现次数是否为 0,,如果出现次数为 0 也要返回假值;如果当前这个字符在 char_count 映射中存在并且出现次数不为空,我们就对其进行次数的减 1 操作,以便后续的相同字符的比较

  3. 最后,如果在这个遍历过程中都没有 reuturn false,那么证明这个 ransomNote 字符串中的所有的字符都可以在 magazine 中找到并且构造出来,那么我们就可以放心的返回真值了

第一个方法的逻辑比较清晰,主要思路是使用 char_count 键值对中的第二个字段出现次数来记录当前这个字符是否被使用了,被使用了则直接删去这一次出现记录;这里使用了 std::unordered_map 来记录键值对,是为了提高访问的效率(底层实现 hash 表)。

那么我们接下来来看看这段代码是否存在可优化点呢?

答案是有的(此处应有掌声 ^_^)

要优化这段代码,首先让我们看看 std::map::operator[] (std::map 和 std::unordered_map 的大部分的函数都是一致的)的定义:

std::map::operator

这里主要看这一句话:

Returns a reference to the value that is mapped to a key equivalent to key, performing an insertion if such key does not already exist.

简单翻译下:

std::map::operator[] 的作用是:
返回一个与对应 key 值相等的映射的映射值的引用,如果当前 key 值不存在则执行一次插入操作

看到了吗?我们的 std::map 或者 std::unordered_map 在使用形如:

char_count[key]

调用的时候,如果 char_count 这个映射中存在这个 key 值的话,char_count 的 operator[] 操作就会返回 key 值对应的 value 值,但是如果不存在的话,就会初始化一个 key 对默认的 value 值的初始化的值(比如这里 char_count 的类型是 std::unordered_map<char ,int >,value 的初始化值就为 int 类型的初始化值 0)。也就是说:

std::map 或者说 std::unordered_map 可以访问不存在 key 值,如果使用了 operator[] 操作访问了一个不存在的 key 值,std::map 或者说 std::unordered_map 就会初始化创建一个 key 对于默认初始化的 value 值的映射关系

说了这么多,那么这一特性的应用在哪里呢?

其实应用是非常巧妙的,我们在第一个方法中使用了这么一句代码:

if (char_count.find(c) == char_count.end() || char_count[c] == 0) return false;

首先判断 c 字符是否在 char_count 中存在,然后还要查看如果 c 字符存在的话,它的出现次数是否为 0。

然而这一个逻辑完全可以简化为一次判断:

if (char_count[c] == 0) return false;

也就是说,不管 c 字符有没有在 char_count 中出现,我们也可以直接查询它的出现次数,如果为 0 ,我们直接返回假值即可(如果当前 key 值不存在,使用 std::unordered_map::operator[] 操作会初始化一个 std::pair<char, int> 类型的值为 “(c, 0)” 对象)。

因此,简化后的代码如下:

// my solution 2 use unordered_map , runtime = 32 ms// that std::map::operator[] returns a reference to the value// that is mapped to a key equivalent to key, performing an // insertion if such key does not already existclass Solution2 {public:    bool canConstruct(string ransomNote, string magazine) {        unordered_map<char, int> char_count;        for (auto c : magazine) ++char_count[c];        for (auto c : ransomNote)            if (char_count[c] == 0) return false;            else --char_count[c];            return true;    }};

尽管我废了这么大力气也就只简化了一个判断,但是 runtime 也确实很争气减少了 3ms,虽然提升不大,但是我们对于标准库的映射又有了新的认识,可谓收获颇丰。

三、寻找另一种思路:为什么一定要查找是否存在

这里,我又在想,为什么要陷入“查找是否存在”这一原子操作的思路中去呢?有没有其他的思路呢?

答案当然是有的。

这是我的方法三,直接上代码吧:

// my solution 3 use two unordered_map , runtime = 36 msclass Solution3 {public:    bool canConstruct(string ransomNote, string magazine) {        unordered_map<char, int> note_map;        unordered_map<char, int> magazine_map;        for (auto c : ransomNote) ++note_map[c];        for (auto c : magazine) ++magazine_map[c];        for (auto c : note_map)            if (magazine_map[c.first] < note_map[c.first]) return false;        return true;    }};

看到了代码,也许你已经理解了:

  1. 首先,我们将两个字符串分别创建了自己的字符对出现次数的映射

  2. 然后,我们遍历其中一个映射,拿到每一个字符进行这样的比较,杂志字符串中该字符的出现次数是否大于等于勒索信字符串中该字符的出现次数?如果大于的话,则表示可以构造出该字符,否则就不能构造,直接返回假值即可

  3. 最后,直到我们遍历结束都没有返回假值的话,证明全部字符都可以构造出来,安心地返回真值即可

这一个思路的来源主要是比较对应字符的出现次数,与上一个标题的方法中的思路截然不同。

然而,这个方法的 runtime 高达 36 ms

T_T ~~~

额,探索的过程总是有意义的~~~

四、总结

这道题非常简单,也许对于熟悉各个语言的容器的使用方法的人来说,这道题简直小菜一碟。可能有些同学会疑惑,这道题我居然没有说最高票答案,说实话我也看了看,发现可能是因为这道题太过于简单吧,最高票的 C++ 答案我觉得写得还不如我的答案逻辑清晰简洁 ^_^ ~~~

不过解题的时候探索的过程仍然可以收获很多,比如这里我踩到的坑 std::map::operator[] 的查询不存在 key 值后的处理逻辑。

简单不代表低陋,探索的过程仍然是最快乐的 : )

公司断网的一段时间内做的这道题,有感而发,故写此博客。

To be Stronger!