In Place Run Length Encoding Algorithm

I encountered an interview question:

Given a input String: aaaaabcddddee, convert it to a5b1c1d4e2.

One extra constraint is, this needs to be done in-place, means no extra space(array) should be used.

It is guaranteed that the encoded string will always fit in the original string. In other words, string like abcde will not occur, since it will be encoded to a1b1c1d1e1 which occupies more space than the original string.

One hint interviewer gave me was to traverse the string once and find the space that is saved.

Still I am stuck as some times, without using extra variables, some values in the input string may be overwritten.

Any suggestions will be appreciated?

Answers


This is a good interview question.

Key Points

There are 2 key points:

  1. Single character must be encoded as c1;
  2. The encoded length will always be smaller than the original array.

Since 1, we know each character requires at least 2 places to be encoded. This is to say, only single character will require more spaces to be encoded.

Simple Approach

From the key points, we notice that the single character causes us a lot problem during the encoding, because they might not have enough place to hold the encoded string. So how about we leave them first, and compressed the other characters first?

For example, we encode aaaaabcddddee from the back while leaving the single character first, we will get:

aaaaabcddddee
_____a5bcd4e2

Then we could safely start from the beginning and encoding the partly encoded sequence, given the key point 2 such that there will be enough spaces.

Analysis

Seems like we've got a solution, are we done? No. Consider this string:

aaa3dd11ee4ff666

The problem doesn't limit the range of characters, so we could use digit as well. In this case, if we still use the same approach, we will get this:

aaa3dd11ee4ff666
__a33d212e24f263

Ok, now tell me, how do you distinguish the run-length from those numbers in the original string?

Well, we need to try something else.

Let's define Encode Benefit (E) as: the length difference between the encoded sequence and the original consecutive character sequence..

For example, aa has E = 0, since aa will be encoded to a2, and they have no length difference; aaa has E = 1, since it will be encoded as a3, and the length difference between the encoded and the original is 1. Let's look at the single character case, what's its E? Yes, it's -1. From the definition, we could deduce the formula for E: E = ori_len - encoded_len.

Now let's go back to the problem. From key point 2, we know the encoded string will always be shorter than the original one. How do we use E to rephrase this key point?

Very simple: sigma(E_i) >= 0, where E_i is the Encode Benefit of the ith consecutive character substring.

For example, the sample you gave in your problem: aaaaabcddddee, can be broken down into 5 parts:

E(0) = 5 - 2 = 3  // aaaaa -> a5
E(1) = 1 - 2 = -1 // b -> b1
E(2) = 1 - 2 = -1 // c -> c1
E(3) = 4 - 2 = 2  // dddd -> d4
E(4) = 2 - 2 = 0  // ee -> e2

And the sigma will be: 3 + (-1) + (-1) + 2 + 0 = 3 > 0. This means there will be 3 spaces left after encoding.

However, from this example, we could see a potential problem: since we are doing summing, even if the final answer is bigger than 0, it's possible to get some negatives in the middle!

Yes, this is a problem, and it's quite serious. If we get E falls below 0, this means we do not have enough space to encode the current character and will overwrite some characters after it.

But but but, why do we need to sum it from the first group? Why can't we start summing from somewhere in the middle to skip the negative part? Let's look at an example:

2 0 -1 -1 -1 1 3 -1

If we sum up from the beginning, we will fall below 0 after adding the third -1 at index 4 (0-based); if we sum up from index 5, loop back to index 0 when we reach the end, we have no problem.

Algorithm

The analysis gives us an insight on the algorithm:

  1. Start from the beginning, calculate E of the current consecutive group, and add to the total E_total;
  2. If E_total is still non-negative (>= 0), we are fine and we could safely proceed to the next group;
  3. If the E_total falls below 0, we need to start over from the current position, i.e. clear E_total and proceed to the next position.

If we reach the end of the sequence and E_total is still non-negative, the last starting point is a good start! This step takes O(n) time. Usually we need to loop back and check again, but since key point 2, we will definitely have a valid answer, so we could safely stop here.

Then we could go back to the starting point and start traditional run-length encoding, after we reach the end we need to go back to the beginning of the sequence to finish the first part. The tricky part is, we need to make use the remaining spaces at the end of the string. After that, we need to do some shifting just in case we have some order issues, and remove any extra white spaces, then we are finally done :)

Therefore, we have a solution (the code is just a pseudo and hasn't been verified):

// find the position first
i = j = E_total = pos = 0;
while (i < s.length) {
    while (s[i] == s[j]) j ++;
    E_total += calculate_encode_benefit(i, j);
    if (E_total < 0) {
        E_total = 0;
        pos = j;
    }
    i = j;
}

// do run length encoding as usual:
// start from pos, end with len(s) - 1, the first available place is pos
int last_available_pos = runlength(s, pos, len(s)-1, pos);
// a tricky part here is to make use of the remaining spaces from the end!!!
int fin_pos = runlength(s, 0, pos-1, last_available_pos);
// eliminate the white
eliminate(s, fin_pos, pos);
// update last_available_pos because of elimination
last_available_pos -= pos - fin_pos < 0 ? 0 : pos - fin_pos;
// rotate back
rotate(s, last_available_pos);
Complexity

We have 4 parts in the algorithm:

  1. Find the starting place: O(n)
  2. Run-Length-Encoding on the whole string: O(n)
  3. White space elimination: O(n)
  4. In place string rotation: O(n)

Therefore we have O(n) in total.

Visualization

Suppose we need to encode this string: abccdddefggggghhhhh

First step, we need to find the starting position:

Group 1: a     -> E_total += -1 -> E_total = -1 < 0 -> E_total = 0, pos = 1;
Group 2: b     -> E_total += -1 -> E_total = -1 < 0 -> E_total = 0, pos = 2;
Group 3: cc    -> E_total += 0  -> E_total = 0 >= 0 -> proceed;
Group 4: ddd   -> E_total += 1  -> E_total = 1 >= 0 -> proceed;
Group 5: e     -> E_total += -1 -> E_total = 0 >= 0 -> proceed;
Group 6: f     -> E_total += -1 -> E_total = -1 < 0 -> E_total = 0, pos = 9;
Group 7: ggggg -> E_total += 3  -> E_total = 3 >= 0 -> proceed;
Group 8: hhhhh -> E_total += 3  -> E_total = 6 >= 0 -> end;

So the start position will be 9:

         v this is the starting point
abccdddefggggghhhhh
abccdddefg5h5______
             ^ last_available_pos, we need to make use of these remaining spaces
abccdddefg5h5a1b1c2
d3e1f1___g5h5a1b1c2
      ^^^ remove the white space
d3e1f1g5h5a1b1c2
          ^ last_available_pos, rotate
a1b1c2d3e1f1g5h5
Last Words

This question is not trivial, and actually glued several traditional coding interview questions together naturally. A suggested mind flow would be:

  1. observe the pattern and figure out the key points;
  2. realize the reason for insufficient space is because of encoding single character;
  3. quantize the benefit/cost of encoding on each consecutive characters group (a.k.a Encoding Benefit);
  4. use the quantization you proposed to explain the original statement;
  5. figure out the algorithm to find a good starting point;
  6. figure out how to do run-length-encoding with a good starting point;
  7. realize you need to rotate the encoded string and eliminate the white spaces;
  8. figure out the algorithm to do in place string rotation;
  9. figure out the algorithm to do in place white space elimination.

To be honest, it's a bit challenging for an interviewee to come up with a solid algorithm in a short time, so your analysis flow really matters. Don't say nothing, show your mind flow, this helps the interviewer to find out your current stage.


Maybe just encode it normally, but if you see that your output index overtakes the input index, just skip the "1". Then when you finish go backwards and insert 1 after all letters without a count, shifting the rest of the string back. It is O(N^2) in the worst case (no repeating letters), so I assume there might be better solutions.

EDIT: it appears I missed the part that the final string always fits into the source. With that restriction, yeah, this is not the optimal solution.

EDIT2: an O(N) version of it would be during the first pass also compute the final compressed length (which in the general case might be more than the source), set pointer p1 to it, a pointer p2 to the compressed string with 1s omitted (p2 is thus <= p1), then just keep going backwards on both pointers, copying p2 to p1 and adding 1s when necessary (when this happens the difference between p2 and p1 will decrease)


O(n) and in place

  1. set var = 0;
  2. Loop from 1-length and find the first non-matching character.
  3. The count would be the difference of the indices of both characters.

Let's run through an example

s = "wwwwaaadexxxxxxywww"

add a dummy letter to s

s = s + '#'

now our string becomes

s = "wwwwaaadexxxxxxywww#"

we'll come back to this step later.

j gives the first character of the string.

j = 0 // s[j] = w

now loop through 1 - length. The first non-matching character is 'a'

print(s[j], i - j) // i = 4, j = 0
j = i              // j = 4, s[j] = a

Output: w4

i becomes the next non-matching character which would be 'd'

print(s[j], i - j) // i = 7, j = 4 => a3
j = i              // j = 7, s[j] = d

Output: w4a3


.
.  (Skipping to the second last)
.

j = 15, s[j] = y, i = 16, s[i] = w
print(s[j], i - y) => y1

Output: w4a3d1e1x6y1

Okay so now we reached the last, assume that we didn't add any dummy letter

j = 16, s[j] = w and we cannot print it's count 
because we've no 'mis-matching' character

That's why need to add a dummy letter.

Here's a C++ implementation

void compress(string s){
    int j = 0;
    s = s + '#';
    for(int i=1; i < s.length(); i++){
        if(s[i] != s[j]){
            cout << s[j] << i - j;
            j = i;
        }
     }
}

int main(){
    string s = "wwwwaaadexxxxxxywww";
    compress(s);
    return 0;
}

Output: w4a3d1e1x6y1w3


Need Your Help

regex string followed by a number php string replace

php regex string replace

I'm not great with regex and I am trying to search a string for "&amp;option=" plus a number [0-100].

Why each class in .Net derives from System.Object? what are the benefits?

c# .net

Why each class in .Net derives from System.Object? what are the benefits?