7. Sliding window routine algorithm framework -- Go language version

Posted by ccgorman on Sun, 26 Dec 2021 00:36:50 +0100

Antecedents: Go language learners. Article reference https://labuladong.gitee.io/algo , the code is written by yourself. If there is anything wrong, thank you for your correction

In order to facilitate downloading and sorting, articles on golang algorithm have been open source and placed in:

  • https://github.com/honlu/GoLabuladongAlgorithm
  • https://gitee.com/dreamzll/GoLabuladongAlgorithm

If it's convenient, please share, star! Remarks reprint address! Welcome to study and communicate together!

Topics involved

Leetcode 76. Minimum covering substring

Leetcode 567. Arrangement of strings

Leetcode 438. Find all alphabetic words in the string

Leetcode 3. Longest substring without duplicate characters

In view of the fact that the song "Bi search ascending words" in the previous [detailed explanation of Bi search framework] is highly praised and widely spread among the people, which has become a good prescription for sleeping and helping sleep, today, in the framework of sliding window algorithm, I write a little poem again to praise the greatness of sliding window algorithm:

For the usage of double pointer fast and slow pointer and left and right pointers, please refer to the framework of double pointer skill routine above. This paper solves a kind of double pointer skill that is the most difficult to master: sliding window skill. Summarizing a framework can ensure that you can write the correct solution with your eyes closed.

Speaking of sliding window algorithm, many readers will have a headache. The idea of this algorithm is very simple, that is, maintain a window, slide constantly, and then update the answer. LeetCode has at least 10 problems using sliding window algorithm, all of which are medium and difficult. The general logic of the algorithm is as follows:

left := 0
right := 0
for right < len(s){
    // Increase window
    window = append(window, s[right])
    right++
    
    for window needs shrink{
        // contract the window
        window.remove(s[left]) // Pseudo code
        left++
    }
}

The time complexity of this algorithm is O(N), which is much more efficient than string violence algorithm.

In fact, what puzzles everyone is not the idea of the algorithm, but various details. For example, how to add new elements to the window, how to narrow the window, and at which stage of window sliding to update the results. Even if you understand these details, it's easy to find bugs. You don't know how to find bugs. It's really annoying.

So today I'll write a set of code framework for sliding window algorithm. I'll even write you where to output debug. If you encounter relevant problems in the future, you can write the following framework by dictation, and then change three places. There will be no bugs yet:

// Sliding window algorithm framework
func slidingWindow(s string, t string){
    need, window := map[byte]int{}, map[byte]int{} // No char in go Also note that you can't just declare without creating
    for i:=0;i<len(t);i++{ // Use range to get rune, and use t[i] to get byte
        need[t[i]]++ // map[key] access the value corresponding to the key in the hash table. If the key does not exist, the key is automatically created and the map[key] is assigned to 0
    }
    left := 0
    right := 0
    valid := 0
    for right < len(s){
        // c is the character that will be moved into the window
        c := s[right]
        // Move window right
        right++
        // Perform a series of updates to the data in the window
        ...

        // Location of debug output
        fmt.Print("windows: [%d,%d]\n",left,right)
        //

        // Determine whether the left window should shrink
        for window needs shrink{
            // d is the character of a window
            d := s[left]
            // Move window left
            left++
            // Perform a series of updates to the data in the window
            ...
        }
    }
}

Two of them Represents the place where the window data is updated. You can just fill it in at that time.

And these two The operations at are move right and move left window update operations respectively. You will find that their operations are completely symmetrical.

As an aside, I find that many people like to stick to the appearance rather than explore the essence of the problem. For example, many people commented on my framework, saying that the speed of hash table is slow. It's better to use array instead of hash table; Many people like to write the code very short. They say that my code is too redundant and affects the compilation speed. The speed on LeetCode is not fast enough.

I'm overwhelmed. The algorithm looks at the time complexity. You can ensure that your time complexity is optimal. As for the so-called running speed of LeetCode, it's all metaphysics. As long as it's not too slow, there's no problem. It's not worth optimizing from the compilation level. Don't forget the basics

The focus of this paper is on the algorithm idea. You know the framework thinking in your heart, and then change the code with your magic. Well, you're happy.

To get back to business, let's go directly to the four original LeetCode questions to set this framework. The first question will explain its principle in detail, and the next four questions will directly close their eyes.

Because the sliding window is often dealing with string related problems, it is inconvenient for Java to deal with strings. The original reference article is implemented in C + +, but the code in this article is implemented in Go. We won't use any programming skills, but we'd like to briefly introduce some data structures used to avoid some readers' understanding of algorithm ideas due to language details:

Map implementation of go and unordered in C + +_ Like map, both are hash tables (dictionaries). Go and C + + can use square brackets to access the value map[key] corresponding to the key. It should be noted that if the key does not exist, go and C + + will automatically create the key and assign the map[key] to 0.

So the map[key] + + that appears many times in the code is equivalent to Java's map put(key, map.getOrDefault(key, 0) + 1).

1, Minimum covering substring

The title is not difficult to understand, that is, to find a substring containing all the letters in T(target) in S(source), the order does not matter, but this substring must be the shortest of all possible substrings.

If we use violence, the code is like this:

for i:=0; i<len(s);i++{
    for j:=i+1; j<len(s);j++{
        if s[i:j]contain t All letters of:
        	Update answer
    }
}

The idea is very direct, but obviously, the complexity of this algorithm must be greater than O(N^2), which is not good.

The idea of sliding window algorithm is as follows:

1. We use the left and right pointer technique in the double pointer in the string S, initialize left = right = 0, and call the index left closed right open interval [left, right) a "window".

2. We first continuously increase the right pointer to expand the window [left, right] until the string in the window meets the requirements (including all characters in T).

3. At this time, we stop adding right and continue to increase the left pointer to narrow the window (left, right) until the string in the window no longer meets the requirements (excluding all characters in T). At the same time, we need to update the results every time we increase left.

4. Repeat steps 2 and 3 until right reaches the end of string S.

In fact, this idea is not difficult. Step 2 is equivalent to looking for a "feasible solution", then step 3 is to optimize the "feasible solution", and finally find the optimal solution, that is, the shortest covering substring. The left and right pointers move forward in turn, the window size increases and decreases, and the window keeps sliding to the right. This is the origin of the name "sliding window".

Draw the picture below to understand that needs and window are equivalent to counters, which record the occurrence times of characters in T and the occurrence times of corresponding characters in "window" respectively.

Initial state:

Add right until the window [left, right] contains all the characters in T:

Now start adding left and narrowing the window [left, right]:

left will not continue to move until the string in the window no longer meets the requirements:

Then repeat the above process, first move right, then left... Until the right pointer reaches the end of string S, and the algorithm ends.

If you can understand the above process, Congratulations, you have fully mastered the idea of sliding window algorithm. Now let's see how the sliding window code framework works:

First, initialize the window and need hash tables to record the characters in the window and the characters to be rounded up:

var need,window map[char]int
for _,c := range t{
    need[c]++
}

Then, use the left and right variables to initialize both ends of the window. Don't forget that the interval [left, right) is closed on the left and open on the right. Therefore, initially, the window does not contain any elements:

left := 0
right := 0
valid := 0
for right < len(s){
	// Start sliding
}

The valid variable represents the number of characters that meet the need condition in the window. If valid and need If the size is the same, it means that the window has met the conditions and completely covered the string T.

Now to set up a template, just think about the following four questions:

1. What data should be updated when moving right to expand the window, that is, adding characters?

2. Under what conditions should the window pause to expand and start moving left to narrow the window?

3. What data should be updated when moving left to narrow the window, that is, moving out characters?

4. Should the results we want be updated when the window is enlarged or narrowed?

If a character enters the window, the window counter should be increased; If a character will move out of the window, the window counter should be reduced; When valid satisfies need, the window should be narrowed; The final result should be updated as the window shrinks.

Here is the complete code:

func minWindow(s string, t string) string{
    need, window := map[byte]int{}, map[byte]int{} // No char in go Also note that you can't just declare without creating
    for i:=0;i<len(t);i++{ // Use range to get rune, and use t[i] to get byte
        need[t[i]]++
    }
    
    left := 0
    right := 0
    valid := 0
    // Record the starting index and length of the minimum coverage substring
    start := 0
    temp := math.MaxInt32  // Save length
    for right < len(s){
        // c is the character that will be moved into the window
        c := s[right]
        // Move window right
        right++
        // Perform a series of updates to the data in the window
        if need[c]!=0{
            window[c]++
            if window[c] == need[c]{
                valid++
            }
        }
        // Determine whether the left window should shrink
        for valid == len(need){
            // Update the minimum coverage string here
            if right - left < temp{
                start = left
                temp = right - left
            }
            // d is the character that will remove the window
            d := s[left]
            // Move window left
            left++
            // Perform a series of updates to the data in the window
            if need[d]!=0{
                if window[d]==need[d]{
                    valid--
                }
                window[d]--
            }
        }
    }
    // Returns the minimum overlay string
    if temp == math.MaxInt32{
        return ""
    }else{
        return s[start:start+temp]
    }
}

PS: readers using java should be particularly alert to the pitfalls of language features. Java Integer, String and other types should use the equals method instead of the equal sign = =. This is an obscure detail of Java wrapper classes. Therefore, when moving the window left to update data, it cannot be directly rewritten to window get(d) == need. Get (d) instead of using window get(d). Equals (need. Get (d)), and the following title codes are the same.

It should be noted that when we find that the number of characters in the window meets the needs of need, we need to update the valid to indicate that one character has met the requirements. Moreover, you can find that the two update operations on the data in the window are completely symmetrical.

When valid = = need When size(), it indicates that all characters in T have been overwritten and a feasible coverage substring has been obtained. Now it is time to shrink the window to obtain the "minimum coverage substring".

When moving the left shrinking window, the characters in the window are feasible solutions, so the minimum coverage substring should be updated at the stage of shrinking the window, so as to find the final result with the shortest length from the feasible solution.

So far, we should be able to fully understand this framework. The sliding window algorithm is not difficult, but the details are very annoying. In the future, when you encounter the sliding window algorithm, you write code according to this framework to ensure that there are no bug s and save trouble.

Let's directly use this framework to kill a few questions. You can basically see your ideas at a glance.

2, String arrangement

LeetCode 567, Permutation in String, difficulty Medium:

Note that the entered s1 can contain repeated characters, so this question is not easy.

This problem is an obvious sliding window algorithm, which gives you an S and a T. do you have a substring in s that contains all the characters in T and does not contain other characters?

First, copy and paste the algorithm framework code before, and then specify the four questions just put forward to write the answer to this question:

// Sliding window algorithm framework -- judging whether there is an arrangement of t in s
func checkInclusion(t string, s string) bool{
    need, window := map[byte]int{}, map[byte]int{} // No char in go Also note that you can't just declare without creating
    for i:=0;i<len(t);i++{ // Use range to get rune, and use t[i] to get byte
        need[t[i]]++ // map[key] access the value corresponding to the key in the hash table. If the key does not exist, the key is automatically created and the map[key] is assigned to 0
    }
    left := 0
    right := 0
    valid := 0
    for right < len(s){
        // c is the character that will be moved into the window
        c := s[right]
        // Move window right
        right++
        // Carry out a series of updates of data in the window [key]
        if need[c]!=0{
            window[c]++
            if window[c]==need[c]{
                valid++
            }
        }

        // Determine whether the left window should shrink
        for right - left >= len(t){
            // Here you can judge whether a valid string is found [key]
            if valid == len(need){
                return true
            }
            // d is the character of a window
            d := s[left]
            // Move window left
            left++
            // Carry out a series of updates of data in the window [key]
            if need[d]!=0{
                if window[d] == need[d]{
                    valid--
                }
                window[d]--
            }
        }
    }
    // No substrings were found that match the criteria
    return false
}

Basically, the solution code as like as two peas of minimum covering is only two places to change.

1. When the window size is larger than t.size(), it should be arranged. Obviously, the length should be the same.

2. When it is found that valid = = need When size(), it means that there is a legal arrangement in the window, so it returns true immediately.

As for how to handle the expansion and reduction of the window, it is exactly the same as the minimum coverage substring.

3, Find all letter words

This is question 438 of LeetCode, find all analogs in a string, difficulty Medium:

Hehe, this so-called letter ectopic word is just arrangement. Can you fool people with a high-end statement? Equivalent to entering a string s and a string T, finding the arrangement of all T in S and returning their starting index.

Directly write down the framework, clarify the four questions just mentioned, and then you can kill this question:

// Sliding window algorithm framework - find all letter ectopic words
func findAnagrams(s string, t string) []int{
    need, window := map[byte]int{}, map[byte]int{} // No char in go Also note that you can't just declare without creating
    for i:=0;i<len(t);i++{ // Use range to get rune, and use t[i] to get byte
        need[t[i]]++ // map[key] access the value corresponding to the key in the hash table. If the key does not exist, the key is automatically created and the map[key] is assigned to 0
    }
    left := 0
    right := 0
    valid := 0
    res := []int{}  // [important]
    for right < len(s){
        // c is the character that will be moved into the window
        c := s[right]
        // Move window right
        right++
        // Carry out a series of updates of data in the window [important]
        if need[c]!=0{
            window[c]++
            if window[c] == need[c]{
                valid++
            }
        }

        // Determine whether the left window should shrink
        for right - left >= len(t){
            // When the window meets the conditions, add the starting index to res [important]
            if valid == len(need){
                res = append(res, left)
            }
            // d is the character of a window
            d := s[left]
            // Move window left
            left++
            // Carry out a series of updates of data in the window [important]
            if need[d]!=0{
                if window[d] == need[d]{
                    valid--
                }
                window[d]--
            }
        }
    }
    return res
}

It is the same as looking for the arrangement of strings, except that after finding a legal ectopic word (arrangement), add the starting index to res.

4, Longest non repeating substring

This is question 3 of LeetCode, long substring without repeating characters, difficulty Medium:

This question finally has some new ideas. It's not a set of framework to answer, but it's simpler. Just change the framework a little:

// Sliding window algorithm framework -- longest non repeating substring
func lengthOfLongestSubstring(s string) int{
    window := map[byte]int{} // No char in go Also note that you can't just declare without creating
    left := 0
    right := 0
    res := 0  // Record results
    for right < len(s){
        // c is the character that will be moved into the window
        c := s[right]
        // Move window right
        right++
        // Carry out a series of updates of data in the window [important]
        window[c]++

        // Determine whether the left window should shrink
        for window[c]>1{
            // d is the character of a window
            d := s[left]
            // Move window left
            left++
            // Carry out a series of updates of data in the window [important]
            window[d]--
        }
        // Update the answer here [important]
        if res < right-left{
            res = right -left
        }
    }
    return res
}

This makes it easier. You don't even need and valid, and you only need to update the counter window to update the data in the window.

When the window[c] value is greater than 1, it indicates that there are duplicate characters in the window, which does not meet the conditions. It's time to move left to narrow the window.

The only thing to note is where to update the result res? What we want is the longest non repeating substring. Which stage can ensure that the string in the window is not repeated?

Unlike before, res should be updated after shrinking the window, because the while condition of window shrinkage is that there are duplicate elements. In other words, after shrinking, there must be no duplication in the window.

5, Final summary

It is suggested to recite and write this framework by dictation, and recite the poem at the beginning of the article by the way. I won't be afraid of substring and subarray anymore, okay.

Topics: Go Algorithm