Leetcode #383. Ransom Note刷题手记

106 阅读5分钟

383. Ransom NoteLeetcode面试榜单Top150哈希表类第1题,给定两个字符串,问是否可以从其中一个字符串中取出组成另一个字符串所需要的所有字符。题目难度为Easy,在面试中如何发挥,争取加分?

首先要有一个刷题流程,这个流程越能模拟接近面试场景与面试官面对面,越有收获。第1步是听题审题,第2步用自己的话把理解到的问题做个定义,第3步对定义了的问题提出尽可能多想法(头脑风暴和奔驰SCAMPER工作法),第4步做出解法原型,第5步对原型测试。这既有从头到尾进行的5个步骤,也包含在其中某一个步骤往回完善增加先前步骤的工作,持续迭代和完善。

对于383这道题,1.在理解题意,2.明确待解决问题是已知条件是两个字符串,一个字符串中的所有字符个次,需要在另一个字符串中包含足够多的个次. 3.初步想出一两种或者更多可能得思路作为切入点 4.先用就简单明了的一种方法作为原型,遍历字符串中每个字符,在另一个字符串中查找到该字符的一个位置并删除掉,或者没查到则直接结束返回false。5.以时间和空间复杂度作为衡量解法的量化指标,评估算法时空效率,复盘抽取待改进方向的思路。

public boolean canConstruct(String ransomNote, String magazine) {
    // For each character, c,  in the ransom note.
    for (char c : ransomNote.toCharArray()) {
        // Find the index of the first occurrence of c in the magazine.
        int index = magazine.indexOf(c);
        // If there are none of c left in the String, return False.
        if (index == -1) {
            return false;
        }
        // Use substring to make a new string with the characters 
        // before "index" (but not including), and the characters 
        // after "index". 
        magazine = magazine.substring(0, index) + magazine.substring(index + 1);
    }
    // If we got this far, we can successfully build the note.
    return true;
}

O(m.n)的时间复杂度和不大于O(m)的空间复杂度,从代码行到算法构成自底向上的看,循环要把字符串转成字符数组,调用库函数找字符在字符串中位置,找到的话把该位置前后的子字符串连接组成新的字符串(删除了一个字符),否则找不到的话,就直接返回false了。在这个简单的算法实现中,对字符串中每个字符都毫无差别地去机械化的全部遍历另一个字符串,找字符出现位置的索引,然后删除该字符得到新的字符串给下次循环迭代使用。这里查字符索引和删除字符的操作会高频出现,也有些冗余之嫌,有没有办法针对性的优化呢?

如果不使用这种在另一字符串中查到字符后删除的方式,换一种思路,一个字符串是提供字符库存,另一个字符串则是需要从中消费,那么分别统计各自的字符及相应的数量,然后分别依次完成消费需求中的所有字符在库存中的数量比需要的数量要多。用两个哈希表分别保存两个字符串统计到的各自字符及数量,然后利用辅助哈希表中的统计字符数量判断,时间复杂度为O(m)+O(n)+O(n),空间复杂度为O(m)+O(n)。并且可以一开始就判断目标字符串如果比另一个字符串长,则直接返回,不需要尝试了。

class Solution {
    
    // Takes a String, and returns a HashMap with counts of
    // each character.
    private Map<Character, Integer> makeCountsMap(String s) {
        Map<Character, Integer> counts = new HashMap<>();
        for (char c : s.toCharArray()) {
            int currentCount = counts.getOrDefault(c, 0);
            counts.put(c, currentCount + 1);
        }
        return counts;
    }
    
    
    public boolean canConstruct(String ransomNote, String magazine) {
        
        // Check for obvious fail case.
        if (ransomNote.length() > magazine.length()) {
            return false;
        }

        // Make the count maps.
        Map<Character, Integer> ransomNoteCounts = makeCountsMap(ransomNote);
        Map<Character, Integer> magazineCounts = makeCountsMap(magazine);
        
        // For each unique character, c, in the ransom note:
        for (char c : ransomNoteCounts.keySet()) {
            // Check that the count of char in the magazine is equal
            // or higher than the count in the ransom note.
            int countInMagazine = magazineCounts.getOrDefault(c, 0);
            int countInRansomNote = ransomNoteCounts.get(c);
            if (countInMagazine < countInRansomNote) {
                return false;
            }
        }
        
        // If we got this far, we can successfully build the note.
        return true;
    }
}

这个方案里,两个哈希表看起来有些冗余,如果减少一个,消费一方在遍历时也修改同一个哈希表剩余可用字符数量。进一步优化内存使用空间。

class Solution {
    
    // Takes a String, and returns a HashMap with counts of
    // each character.
    private Map<Character, Integer> makeCountsMap(String s) {
        Map<Character, Integer> counts = new HashMap<>();
        for (char c : s.toCharArray()) {
            int currentCount = counts.getOrDefault(c, 0);
            counts.put(c, currentCount + 1);
        }
        return counts;
    }
    
    
    public boolean canConstruct(String ransomNote, String magazine) {
        
        // Check for obvious fail case.
        if (ransomNote.length() > magazine.length()) {
            return false;
        }

        // Make a counts map for the magazine.
        Map<Character, Integer> magazineCounts = makeCountsMap(magazine);
        
        // For each character in the ransom note:
        for (char c : ransomNote.toCharArray()) {
            // Get the current count for c in the magazine.
            int countInMagazine = magazineCounts.getOrDefault(c, 0);
            // If there are none of c left, return false.
            if (countInMagazine == 0) {
                return false;
            }
            // Put the updated count for c back into magazineCounts.
            magazineCounts.put(c, countInMagazine - 1);
        }
        
        // If we got this far, we can successfully build the note.
        return true;
    }
}

至此,效率上的优化基本完成,实在要利用字符中字符串均为小写英文这一特点,可以把哈希表这种数据结构改用简单的字符数组,进一步降低内存使用。

我们还可以尝试把字符串中的字符数组各自排序,然后放入两个栈中,利用双栈出栈的方式来解决这个问题,效率不如使用哈希表的方案高,但是最有特色的设计。

class Solution {
    
    // Please, if there's a nicer way of doing this, without getting tangled in
    // primitives vs Java's generics let me know in the article comments :-)
    private Stack<Character> sortedCharacterStack(String s) {
        char[] charArray = s.toCharArray();
        Arrays.sort(charArray);
        Stack<Character> stack = new Stack<>();
        for (int i = s.length() - 1; i >= 0; i--) {
            stack.push(charArray[i]);
        }
        return stack;
    }
    
    
    public boolean canConstruct(String ransomNote, String magazine) {
        
        // Check for obvious fail case.
        if (ransomNote.length() > magazine.length()) {
            return false;
        }
        
        // Reverse sort the characters of the note and magazine, and then
        // put them into stacks.
        Stack<Character> magazineStack = sortedCharacterStack(magazine);
        Stack<Character> ransomNoteStack = sortedCharacterStack(ransomNote);
        
        // And now process the stacks, while both have letters remaining.
        while (!magazineStack.isEmpty() && !ransomNoteStack.isEmpty()) {
            // If the tops are the same, pop both because we have found a match.
            if (magazineStack.peek().equals(ransomNoteStack.peek())) {
                ransomNoteStack.pop();
                magazineStack.pop();
            } 
            // If magazine's top is earlier in the alphabet, we should remove that 
            // character of magazine as we definitely won't need that letter.
            else if (magazineStack.peek() < ransomNoteStack.peek()) {
                magazineStack.pop();
            }
            // Otherwise, it's impossible for top of ransomNote to be in magazine.
            else {
                return false;
            }
        }
                
        // Return true iff the entire ransomNote was built.
        return ransomNoteStack.isEmpty();
        
    }
}

时间复杂度取决排序算法O(N.logN), 空间复杂度O(m)