0

Intel interview question: Checking if a string with 3 diff parenthesis is balanced without using stack.

Well, first question was just to implement it and I did it with ease using stack. However, if I was asked implementing it without O(n) space, I believe I'd be stuck.

Requirements: O(1) Space, and as efficient as possible in time, which should be O(n).

How would you approach this question? 3 pointers, 3 counters?

Ilan Aizelman WS
  • 1,630
  • 2
  • 21
  • 44
  • 5
    This is a bit too open, you should show your attempted solution. – unwind Aug 24 '17 at 08:33
  • 3
    It seems you haven't read [the help pages](http://stackoverflow.com/help), especially the sections named ["What topics can I ask about here?"](http://stackoverflow.com/help/on-topic) and ["What types of questions should I avoid asking?"](http://stackoverflow.com/help/dont-ask). Also [read about how to ask good questions](http://stackoverflow.com/help/how-to-ask), and learn how to create a [Minimal, Complete, and Verifiable Example](http://stackoverflow.com/help/mcve). – Some programmer dude Aug 24 '17 at 08:35
  • 3
    Silly question... but just to be sure... by "three diff parenthesis" do you mean three types, so "[", "{", "("? – Jimbo Aug 24 '17 at 08:36
  • In C#, I'd simply append each opening parenthesis to a string, and remove the last one when I encounter corresponding closing parenthesis. If I encounter a non-matching closing parenthesis, I'll err out. If I end up with a non-empty string, I'll err out. – dotNET Aug 24 '17 at 08:39
  • 3
    @dotNet: Am I misunderstanding? Thats basically a stack isn't it? – Jimbo Aug 24 '17 at 08:40
  • 3
    I'm not sure I understood what "3 diff parenthesis" is supposed to mean. Could you clarify? – Jabberwocky Aug 24 '17 at 08:42
  • @Jimbo: maybe yes. I was taking liberty of high-level languages where `Stack` is a full-blown class. But in theory what you're saying is correct. – dotNET Aug 24 '17 at 08:44
  • 1
    Use recursion instead. Of course, the implementation of the language used might very well do this on a ... stack :D –  Aug 24 '17 at 08:44
  • @FelixPalmen: Recursion creates a stack, a hefty one in fact. Much better would be to use simple char stack (or string) instead. – dotNET Aug 24 '17 at 08:46
  • Recursions wont work obviously.. – Ilan Aizelman WS Aug 24 '17 at 08:49
  • @dotNET with recursion, there's no stack *in your program*. I already stated that behind the scenes, there's **very likely** a stack involved. –  Aug 24 '17 at 08:57
  • 1
    @IlanAizelmanWS *obviously*, this **will** work very well. –  Aug 24 '17 at 08:57
  • It is your faulty assumption that without a stack meant O(1) space. – jxh Aug 24 '17 at 10:15
  • Are you allowed to change the input string? – joop Aug 24 '17 at 10:26
  • Possible duplicate: [How to find validity of a string of parentheses, curly brackets and square brackets?](https://stackoverflow.com/questions/2509358/how-to-find-validity-of-a-string-of-parentheses-curly-brackets-and-square-brack) – n. m. could be an AI Aug 24 '17 at 10:47
  • 1
    is modifying the string allowed? I guess not because if it was you could use that space as a stack. ?? – Jasen Aug 24 '17 at 11:10
  • I would recommend replacing "without using stack" in the title with "using O(1) memory" for clarity. – n. m. could be an AI Aug 24 '17 at 13:03
  • Does the string hold any thing other than the parantheses? – Ajay Brahmakshatriya Aug 24 '17 at 13:19

6 Answers6

2

It depends on the exact requirement. If you really only need balanced parantheses, an approach with three counters would work, e.g. like this:

#include <stdio.h>

int accept(char (*next)(void))
{
    char x;
    int b[3] = { 0 };
    while ( (x = next()) )
    {
        switch (x)
        {
            case '{':
                ++b[0];
                break;
            case '[':
                ++b[1];
                break;
            case '(':
                ++b[2];
                break;
            case '}':
                --b[0];
                break;
            case ']':
                --b[1];
                break;
            case ')':
                --b[2];
                break;
        }
        if (b[0] < 0 || b[1] < 0 || b[2] < 0) return 0;
    }
    return (b[0] == 0 && b[1] == 0 && b[2] == 0);
}


char getnextchar(void)
{
    int c = getchar();
    if (c == EOF) return 0;
    return (unsigned char)c;
}

int main(void)
{
    puts(accept(getnextchar) ? "balanced" : "unbalanced");
    return 0;
}

Note the getnextchar() function is only introduced for flexibility from where to get your string. You could simplify this to directly take a char * if you like.


If the requirement is that the nesting needs to be correct as well, the only option I can see without a stack in your program would be recursion. Of course, your implementation of C very likely uses a stack for function calls, so in practice, this is useless ... but hey, there's no stack in your program! Example:

#include <stdio.h>

int accept(char (*next)(void), char c)
{
    char x;
    int ok = 1;
    while ( (x = next()) )
    {
        switch (x)
        {
            case '{':
                ok = accept(next, '}');
                break;
            case '[':
                ok = accept(next, ']');
                break;
            case '(':
                ok = accept(next, ')');
                break;
            case '}':
            case ']':
            case ')':
                return x == c;
        }
        if (!ok) return 0;
    }
    return !c;
}


char getnextchar(void)
{
    int c = getchar();
    if (c == EOF) return 0;
    return (unsigned char)c;
}

int main(void)
{
    puts(accept(getnextchar, 0) ? "balanced" : "unbalanced");
    return 0;
}
  • @AkiSuihkonen then try it. The first program will accept that input (it's balanced for each kind of paranthesis), the second program will reject it (it's not correctly nested). –  Aug 24 '17 at 09:53
  • This still smells like recursion. – joop Aug 24 '17 at 09:55
  • 1
    @joop that's because it **is** recursion. The C standard doesn't say anything about how an implementation actually *implements* function calls, and the requirement was only "not to use a stack". Recursion is not using a stack, although the implementation of C will probably do just this. –  Aug 24 '17 at 09:57
  • 2
    The requirement is to use O(1) space, not to pretend to use O(1) space by hiding extra space in your call stack (yes there's a call stack wherer or not your C implementation uses a data structire called "stack" to implement it). – n. m. could be an AI Aug 24 '17 at 10:17
  • @n.m. That heavily depends on your point of view. The answer is **either** "recursion" **or** "not possible". –  Aug 24 '17 at 10:21
  • No, "recursion" is not an acceptable answer. Anyone who says otherwise simply ansswers an entirely different and totally uninteresting question. – n. m. could be an AI Aug 24 '17 at 10:27
  • @n.m. again, it depends on your point of view. In terms of the abstract construct the language C is, "recursion" **is** a solution. It's unclear what this "interview question" wants for a solution, as O(1) would be impossible to reach if your point of view is the *concrete machine* running the code. (that is, without the "dirty" trick to reuse the space your actual input occupies) –  Aug 24 '17 at 10:29
  • 1
    No, it's not a point of view question. It's a question of speaking the right language. In the language I speak "requirements: O(1) Space" precludes unbounded recusrion, full stop, nothing to discuss here. – n. m. could be an AI Aug 24 '17 at 10:32
  • @n.m. I don't share your *opinion*, so, yes, nothing to discuss here. –  Aug 24 '17 at 10:32
  • It's not a question of opinion. If you are saying "you can use unbounded recursion while staying within O(1) memory requirement" you are ether stating a falsehood or speaking a language I don't understand. I'm willing to try and learn it if you post a link to a textbook that teaches it. – n. m. could be an AI Aug 24 '17 at 10:39
  • @n.m. just downvote and move on if you don't like it. I'm pretty sure I'm answering the question though. One part of it was "without a stack". The only way to do this is recursive (or with some "stack in disguise"). No, that's not O(1) **in terms of the real machine**. It's just *impossible* to reach O(1) in these terms, as you need to store *some* information that potentially *grows* with the size of the input. –  Aug 24 '17 at 10:58
  • @n.m. if you can prove me wrong on this and actually show an algorithm with "real" O(1) space complexity, do so ;) I'll delete this answer if such a thing exists. –  Aug 24 '17 at 11:01
  • "It's just impossible to reach O(1) in these terms" This is most probably correct (see the link in my answer) but why do you think this justifies changing the terms or inventing your own terms? – n. m. could be an AI Aug 24 '17 at 11:04
  • @n.m. I don't change **any** terms. My answer doesn't even mention space complexity, it only elaborates on the "without a stack" part and shows what's possible and what's **not** possible, very well describing the practical limitation of recursion. –  Aug 24 '17 at 11:05
  • @n.m. looking at your answer, it indeed seems to confirm such a thing isn't possible. Therefore a good answer if we assume the "O(1) space complexity" was indeed part of the requirement of this interview question. I'd still argue the question here is at least a bit unclear. (title says "without a stack", the text behind "**Interview question:**" says the same, but **then** comes some "O(1)" requirement ...) –  Aug 24 '17 at 11:10
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/152739/discussion-between-n-m-and-felix-palmen). – n. m. could be an AI Aug 24 '17 at 11:12
1

You cannot even count one kind of parentheses in honest O(1) space because you need log(n) bits for the counter.

If you can cheat and pretend integers take O(1) space, you can build a stack within a single integer. For example, pretend that [ is 0, ( is 1 and { is 2 and represent your stack contents as a base 3 number. Naturally this requires integers with O(n) bits rather than O(log(n)) bits as with one kind of parentheses.

It is actually possible to do better: there is a O(log(n)) space algorithm (pdf) due to Ritchie and Springsteel, but it's a serious CS-theoretic work, you are not supposed to discover anything like that during a job interview.

Edit If you can modify the input string, there's a simple solution that uses the scanned portion of the string for storage, as in the answer by @jxh.

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243
1

O(n) does not mean a single pass. Scanning backwards to find the previous open parentheses certainly has to be considered "not a stack". To deal with the portions of the string that are "dead zones" because they are closed off, the algorithm can move newly encountered open parentheses backwards, overwriting the dead zones.

Some pseudocode:

bool matched_parens (char *s) {
    char *p = s;
    char *cur_open = s;
    if (s == NULL || *s == '\0') return true;
    if (!IS_OPEN(*s)) return false;
    while (*++p) {
        if (IS_OPEN(*p)) {
            *++cur_open = *p;
            continue;
        }
        if (!IS_MATCHED(*cur_open, *p)) return false;
        if (cur_open > s) --cur_open;
        else {
            cur_open = s = ++p;
            if (*p == '\0') return true;
            if (!IS_OPEN(*p)) return false;
        }
    }
    return false;
}

If you are forced to scan backwards through dead zones because the string cannot be overwritten, then the number backward scans is only bounded by the size of the string itself, and the algorithm becomes O(n2).

jxh
  • 69,070
  • 8
  • 110
  • 193
  • Seems to work well. – n. m. could be an AI Aug 24 '17 at 12:39
  • Well it is a stack, you are literally pushing your open paren onto the dead space `*++cur_open = *p`and then popping it when you see a match `--cur_open`. But who cares really. – n. m. could be an AI Aug 24 '17 at 12:47
  • @n.m.: You can map the code to an algorithm using a stack. Without copying into the dead zones, the `*++cur_open = *p` becomes `cur_open = p`, and `--cur_open` becomes a while loop searching for the previous unmatched open parentheses. But, a simple trio of counters can be used to properly implement the backward scan without implementing the match algorithm in reverse. – jxh Aug 24 '17 at 15:50
  • "cur_open becomes a while loop" then you won't have O(n) time. "a simple trio of counters can be used" don't see how, can you show? Or perhaps this will still be more than O(n)? – n. m. could be an AI Aug 24 '17 at 15:55
  • @n.m.: Right, the last paragraph states that the complexity increases if you can't copy. The trio of counters for the backward scan works because the dead zone is known to be properly matched. On second thought, you only need a single counter. Increment when you see a closed parentheses, decrement when you see an open parentheses. If the counter is negative, you found your previous open parentheses. – jxh Aug 24 '17 at 16:11
  • @n.m.: Also, to clarify, the algorithm as stated is not a true stack because of the jump into the string that skips over a dead zone that stretches all the way to the beginning of the string. `cur_open = s = ++p` – jxh Aug 24 '17 at 16:20
  • The jump just deletes an unneeded empty stack. You create a new one if/when needed. The jump is not actually necessary. – n. m. could be an AI Aug 24 '17 at 16:29
  • @n.m.: As I mentioned earlier, I understand that the algorithm can trivially map to one using a stack. But, to make my point, code that uses SHL and SHR operations one bit at a time can also map to a stack, but I would argue the code is not using a stack. – jxh Aug 24 '17 at 17:31
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/152787/discussion-between-n-m-and-jxh). – n. m. could be an AI Aug 24 '17 at 17:43
  • I don't think one can draw a clear line between "using a stack" and "there's an emergent stack-like structure, we didn't intend it, honest", nor I think it would be an important line. – n. m. could be an AI Aug 24 '17 at 17:48
1

This can be done by keeping counters for delimiters which are incremented each time an opening delimiter is encountered, and decremented each time a closing delimiter is encountered. To account for correct nesting, a function that iterates backwards through the string from the current position each time a closing delimiter is encountered can be used. This function finds the corresponding opening delimiter by counting opening and closing delimiters encountered during this backwards traversal.

This is O(1) in space, but not O(n) in time complexity:

#include <stdio.h>
#include <string.h>

int is_balanced(const char *str);
int is_corresponding_open(const char *to, const char *from, const char c);

int main(void)
{
    char input[4096];

    for (;;) {
        puts("Enter an expression:");
        fgets(input, sizeof input, stdin);
        if (input[0] == '\n') {
            break;
        }

        input[strcspn(input, "\r\n")] = '\0';

        printf("%s is %s\n",
               input,
               is_balanced(input) ? "balanced" : "not balanced");
    }

    return 0;
}

int is_balanced(const char *str_ptr)
{
    const char *start = str_ptr;
    int count_paren = 0;
    int count_brace = 0;
    int count_bracket = 0;
    int valid_nesting = 1;

    while (*str_ptr && valid_nesting) {

        switch (*str_ptr) {
        case '(':
            ++count_paren;
            break;

        case '{':
            ++count_brace;
            break;

        case '[':
            ++count_bracket;
            break;

        case ')':
            if (is_corresponding_open(start, str_ptr, '(')) {
                --count_paren;                
            } else {
                valid_nesting = 0;
            }
            break;

        case '}':
            if (is_corresponding_open(start, str_ptr, '{')) {
                --count_brace;                
            } else {
                valid_nesting = 0;
            }
            break;

        case ']':
            if (is_corresponding_open(start, str_ptr, '[')) {
                --count_bracket;                
            } else {
                valid_nesting = 0;
            }
            break;
        }

        ++str_ptr;
    }

    return !(count_paren || count_brace || count_bracket) && valid_nesting;
}

int is_corresponding_open(const char *to, const char *from, const char c)
{
    int validity = 0;
    int nesting = 0;

    while (to <= from) {

        if (nesting == 1 && *from == c) {
            validity = 1;
            break;
        }

        if (*from == ')' || *from == '}' || *from == ']') {
            ++nesting;
        } else if (*from == '(' || *from == '{' || *from == '[') {
            --nesting;
        }

        --from;
    }

    return validity;
}

Sample interaction:

Enter an expression:
(1 (2 (3)))
(1 (2 (3))) is balanced
Enter an expression:
(1 [2 {3 4}] 5)
(1 [2 {3 4}] 5) is balanced
Enter an expression:
(1 {2 [3}])
(1 {2 [3}]) is not balanced
Enter an expression:
1 [2 (3)]
1 [2 (3)] is balanced
Enter an expression:
ad absurdum
  • 19,498
  • 5
  • 37
  • 60
0

Assuming by "three diff parenthesis" do you mean three types, so "[", "{", "("...

Without using a stack I'd try this (pseudo code... yet to try to code it up)

OPEN_PARENS = [ "{", "[", "(" ]
Find last occurrence of one of OPEN_PARENS
Find next occurrence of corresponding closing paren. 
    If find another closer that does not correspond then error.
When closing paren found replace it with a space (and also the opening paren)
Repeat until no opening paren is found
Check there are no closing parens.

Just an idea, yet to test it. Also it assumes the original input string is mutable.

This does not use a stack and takes up no extra memory, but is now an O(n^2)-ish parse rather than O(n) parse.

EDIT: As Matt points out this still uses memory. It is the memory already allocated for a string but if the string were const, this wouldn't work. So... as Matt suggests just storing a pointer to the last removed open parenthesis would also work. I think you would have to add a count for each type of parenthesis seen too.

Err.. maybe something like this. I don't have time now but will re-check this logic in the evening.

numSq     = 0;
numCurley = 0;
numCurve  = 0;
OPEN_PARENS = [ "{", "[", "(" ]
Find last occurrence of one of OPEN_PARENS before the last seen (if any other wise search from end of string),
   store pointer to it and increment a count for that type of parenthesis.
If none found exit success
Find next occurrence of corresponding closing paren. 
    If find another closer that does not correspond then decrement count
    for that closer. If count reaches negative then there is an error in string.
When closing param found start again

It works on the principle that once you have the inner most matching brackets matched, matches futher out can "ignore" them, but need to track counts because there has to be some kind of "memory" in the system. Using one pointer and 3 counters however, would be O(1) memory. Still same O(n^2)-ish parse time.

Jimbo
  • 4,352
  • 3
  • 27
  • 44
  • Sort of like using the input string for your stack. – matt Aug 24 '17 at 08:54
  • 1
    hmm... no, I don't think so because you have no memory of what went before, which you would with a stack, which acts as a kind-of memory. – Jimbo Aug 24 '17 at 08:57
  • When you modify the string to change a parenthesis to a space you are treating that as a memory. You could probably get around that by keeping track of the index for the last removed open and close parenthesis. – matt Aug 24 '17 at 08:59
  • I see your point... technically it is memory. It's not a stack though. It is also memory you have already allocated, but if it is const then this wouldn't work. You could track the last index though, you're right. I think that would work... modifying answer... – Jimbo Aug 24 '17 at 09:04
  • I was thinking about the last index, and you'd have to be a bit more careful. Not use the 'last' open parethesis, but the last open parenthesis before a ). Eg. (...)(...). – matt Aug 24 '17 at 09:07
0

State machine to the rescue:

(hand-built, so there could be some typos ;-)

This only work for three different pairs of parentheses (well, at least not for nested sets of the same kind)


#include <stdio.h>
#include <string.h>

int check_parens(char *str)
{
int state;

        /*
        ** 1    (
        ** 2    ([
        ** 3    ([{
        ** 4    ({
        ** 5    ({[
        ** 6    [
        ** 7    [(
        ** 8    [({
        ** 9    [{
        ** 10   [{(
        ** 11   {
        ** 12   {(
        ** 13   {([
        ** 14   {[
        ** 15   {[(
        */
for (state=0; *str; str++) {
        if ( !strchr( "()[]{}", *str )) continue;
        switch(state) {
        case 0:
                if (*str == '(') { state =1; continue; }
                if (*str == '[') { state =6; continue; }
                if (*str == '{') { state =11; continue; }
                return -1;
        case 1: /* ( */
                if (*str == ')') { state =0;  continue; }
                if (*str == '[') { state =2;  continue; }
                if (*str == '{') { state = 4;  continue; }
                return state;
        case 2: /* ([ */
                if (*str == ']') { state = 1;  continue; }
                if (*str == '{') { state = 3;  continue; }
                return state;
        case 3: /* ([{ */
                if (*str == '}') { state = 2;  continue; }
                return state;
        case 4: /* ({ */
                if (*str == '}') { state = 1;  continue; }
                if (*str == '[') { state = 5;  continue; }
                return state;
        case 5: /* ({[ */
                if (*str == ']') { state = 4;  continue; }
                return state;
        case 6: /* [ */
                if (*str == ']') { state = 0;  continue; }
                if (*str == '(') { state = 7;  continue; }
                if (*str == '{') { state = 9;  continue; }
                return state;
        case 7: /* [( */
                if (*str == ')') { state = 6;  continue; }
                if (*str == '{') { state = 8;  continue; }
                return state;
        case 8: /* [({ */
                if (*str == '}') { state = 7;  continue; }
                return state;
        case 9: /* [{ */
                if (*str == '}') { state = 6;  continue; }
                if (*str == '(') { state = 10;  continue; }
                return state;
        case 10: /* [{( */
                if (*str == ')') { state = 9;  continue; }
                return state;
        case 11: /* { */
                if (*str == '}') { state = 0;  continue; }
                if (*str == '(') { state = 12;  continue; }
                if (*str == '[') { state = 14;  continue; }
                return state;
        case 12: /* {( */
                if (*str == ')') { state = 11;  continue; }
                if (*str == '[') { state = 13;  continue; }
                return state;
        case 13: /* {([ */
                if (*str == ']') { state = 12;  continue; }
                return state;
        case 14: /* {[ */
                if (*str == ']') { state = 11;  continue; }
                if (*str == '(') { state = 15;  continue; }
                return state;
        case 15: /* {[( */
                if (*str == ')') { state = 14;  continue; }
                return state;
                }
        }
return state;
}

int main(int argc, char **argv)
{
int result;

result= check_parens(argv[1]);
printf("%s := %d\n", argv[1], result);

return result;
}

Update: maching parentheses should either be adjecent, or at the outside of the string (ignoring normal characters) So we can reduce the string by just working from both sides.


int check_parens2(char *str, unsigned len)
{

while (len) {
        char *cp;
        str[len] = 0;
        fprintf(stderr, "%s\n", str);

                /* if there is any non-paren character at the beginning or end we can skip it */
        cp = strchr( "()[]{}", *str ) ;
        if (!cp) { len--; str++; continue; }

        cp = strchr( "()[]{}", str[len-1] ) ;
        if (!cp) { len--; continue; }

                /* if the first two characters match : we can skip them */
                /* TODO: we should also skip enclosed *normal* characters */
        if (str[0] == '(' && str[1] == ')' ) { str +=2; len -= 2; continue; }
        if (str[0] == '[' && str[1] == ']' ) { str +=2; len -= 2; continue; }
        if (str[0] == '{' && str[1] == '}' ) { str +=2; len -= 2; continue; }

                /* the same if the last two characters match */
        if (str[len-2] == '(' && str[len-1] == ')' ) { len -= 2; continue; }
        if (str[len-2] == '[' && str[len-1] == ']' ) { len -= 2; continue; }
        if (str[len-2] == '{' && str[len-1] == '}' ) { len -= 2; continue; }

                /* if begin&end match : we can skip them */
        if (str[0] == '(' && str[len-1] == ')' ) { str++; len -= 2; continue; }
        if (str[0] == '[' && str[len-1] == ']' ) { str++; len -= 2; continue; }
        if (str[0] == '{' && str[len-1] == '}' ) { str++; len -= 2; continue; }

        break;
        }
return len;
}
joop
  • 4,330
  • 1
  • 15
  • 26
  • 1
    And what about deeper levels of nesting? –  Aug 24 '17 at 09:55
  • You'll need an other kind of mixed-radix encoding. Still limited by the available number of bits. [Ergo: you'll need *some kind* of stack ...] – joop Aug 24 '17 at 10:01