Introduction
We have discussed in a previous article, in most common situation if we have to maintain two Arrays working consistantly, we should better use a Map instead. And that's the reason why Map is introduced at the first place. But there are also some cases an Array can excel a Map.
The first question is always, why do we care? We already feel comfortable of using a Map. The answer is Array will work more quickly than a Map. If we want to improve the speed, in some situation we can go back to using an Array. Array is fast, but not always we can use it. That is, when we working with english alphabet(a~z). If we use a Map in these situation, of course, it will works fine. But let's have a second thought, there are only 26 english letters. If we can store them as 0 ~ 25 by an Array's index, other than store them as "a"~"z" in the key section of a Map? Yes, we can appoint/arrange 0 is a, 1 is b, 2 is c, and so on, instead of storing real "a", "b" and "c". Base on this idea, finally, we can use an Array to make our application works more efficently. Because an Array is stored in a contiguous memory.
Let's have a look at two LeetCode question, helping us make more sense of the above idea.
LeetCode 1160
"Find Words That Can Be Formed by Characters"
You are given an array of strings words
and a string chars
.
A string is good if it can be formed by characters from chars
(each character can only be used once).
Return the sum of lengths of all good strings in words
.
Example 1:
Input: words = ["cat","bt","hat","tree"], chars = "atach" Output: 6 Explanation: The strings that can be formed are "cat" and "hat" so the answer is 3 + 3 = 6.
Example 2:
Input: words = ["hello","world","leetcode"], chars = "welldonehoneyr" Output: 10 Explanation: The strings that can be formed are "hello" and "world" so the answer is 5 + 5 = 10.
If we are using a Map:
func countCharacters(words []string, chars string) int { lengths := 0 for _, word := range words { // reset charAvailable in each loop charAvailable:= make(map[string]int) for _, c := range chars { charAvailable[string(c)]++ } fail := false for _, c := range word { num, _ := charAvailable[string(c)] if num > 0 { charAvailable[string(c)]-- } else { fail = true break } } if !fail { lengths += len(word) } } return lengths }
This solution is correct. Website shows that the speed is around 100ms, and faster than only 5% of Go users.
We can see each loop we have to reset the "charAvailable" Map and it is very time consuming.
With a little help of make() function's argument, we can change it into charAvailable := make(map[string]int, len(chars)/2). This can help decrease 20ms of time consumping, but still have room for optimization.
We want two things's help to improve our speed:
1. If we can find other data struct faster than Map?
2. If we can initialize "charAvailable" only once, and copy it for each loop?
Firstly, here comes Slice backed by an Array. All the Other idea is exactly the same as above.
func countCharacters(words []string, chars string) int { lengths := 0 for _, word := range words { // reset charAvailable in each loop charAvailable:= make([]int, 26) for _, c := range chars { charAvailable[c-'a']++ } fail := false for _, c := range word { if charAvailable[c-'a'] > 0 { charAvailable[c-'a']-- } else { fail = true break } } if !fail { lengths += len(word) } } return lengths }
Website shows that the speed is around 10ms, and faster than 100% of Go users. It is good enough.
If we go further, we want to initialize "charAvailable" only once, instead of reset in reach loop.
So secondly, we will get to know a build in function called copy(). This function can make a value copy between two Slices, instead of pointer copy when we using "=" .
This function can make our code a little more clearly. We initialize "charAvailable" only once, before the loop. Website shows the speed of this process is also around 10ms.
func countCharacters(words []string, chars string) int { lengths := 0 charAvailable:= make([]int, 26) for _, c := range chars { charAvailable[c-'a']++ } for _, word := range words { charAv := make([]int, 26) copy(charAv, charAvailable) fail := false for _, c := range word { if charAv[c-'a'] > 0 { charAv[c-'a']-- } else { fail = true break } } if !fail { lengths += len(word) } } return lengths }
It is mostly the same as we use an Array. We already knew "=" between two Arrays will do value copy by default. Website shows speed is around 10ms also.
func countCharacters(words []string, chars string) int { lengths := 0 charAvailable:= [26]int{} for _, c := range chars { charAvailable[c-'a']++ } for _, word := range words { charAv := charAvailable fail := false for _, c := range word { if charAv[c-'a'] > 0 { charAv[c-'a']-- } else { fail = true break } } if !fail { lengths += len(word) } } return lengths }