Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Задачи на собеседованиях в Яндексе

Anna | 24.06.2014 | нет комментариев

Открытые вакансии на должность разработчика в Яндексе есть неизменно. Компания прогрессирует, и отличных программистов не хватает непрерывно. И претендентов на эти должности тоже хоть отбавляй. Основная трудность – отобрать подлинно подходящих кандидатов. И в этом плане Яндекс немного чем отличается от большинства больших IT-компаний. Так что базовые тезисы, описываемые в этой статье, могут быть применимы не только к Яндексу.

Впрочем стоит оговориться, что статья все же про подбор разработчиков. Т.е. собственно тех восьмидесяти процентов работников, на которых держится массовая разработка. Зачастую мы нанимаем людей на особые вакансии: скажем, разработчиков систем компьютерного зрения, лингвистов, специалистов по машинному обучению. В этом случае формат собеседования может приметно отличаться.

image

Чего ожидать на собеседовании

Резюме и «навык» работы разрешает составить первое ощущение о кандидате, впрочем есть некоторая задача: знание отлично писать резюме и знание программировать не крепко коррелируют. Так что есть большое число людей, которые при чудесном резюме и навыке не могут написать работающий код, и есть некоторое число кандидатов, резюме у которых – навевает тоску, а сами люди при этом стоящие специалисты. Следственно, Дабы подлинно отлично оценить вероятности разработчика, без прямого общения не обойтись.

Изредка, Дабы осознать, что кандидат не подходит, довольно десяти минут: если человека ставят в тупичок базовые вопросы о синтаксисе языка, на котором он по его заявлению писал несколько лет, последующий разговор смысла не имеет. Именно следственно Почаще каждого 1-й этап серии собеседований в Яндексе традиционно проводится через Skype. Все-таки отказать человеку, тот, что добирался до офиса час по пробкам на пятой минуте собеседования – плохо с точки зрения вежливости, а еще 2 часа его мучать, зная что, скорее каждого, не возьмешь – с точки зрения этики. Соответственно, удаленное интервью разрешает сэкономить время и нервы обеим сторонам.

С вопросами о синтаксисе основное – не перестараться, специально пытаясь подловить на каком-нибудь малоизвестном факте. Есть языки программирования с дюже длинной и непростой историей, у которых приблизительно половина их вероятностей – это какие-то исторически сложившиеся трудные и непотребные костыли. К таким, скажем, относится и наш любимый C . Если вы не разработчик компилятора C , примерно неизменно дозволено обнаружить что-то, чего вы в языке не знаете. Легко непостижимо, для чего это могло бы вам потребоваться.

Мы традиционно используем предварительно подготовленные тесты по языку, в которые входит 10-15 вопросов на познание синтаксиса, вероятностей языка, тезисов управления памятью и т.д. Почаще каждого для удачного прохождения результатов на все вопросы не требуется, довольно 70-80 процентов. Да и вообще сам тест – это скорее не тест, а комплект тем, на которые необходимо побеседовать, нас волнует скорее не сам результат (мы его и так знаем), а отчего кандидат его предпочел.

Значимый момент – познание алгорифмов. Тут тоже необходимо не перестараться, наверно дюже немного из людей, которые сами проводят собеседования, назубок помнят как вращать красно-черное дерево, но знать как устроены контейнеры вашего любимого языка для того Дабы знать их ограничения и уметь в выбирать положительные – нужно. На самом деле читать Кнута для того, Дабы исследовать эту область не необходимо, по сути, к этому этапу дозволено подготовиться, вдумчиво изучив приблизительно 30 страниц в Википедии.

Помимо прочтения статей из списка, нужно пройтись по каждому релевантным ссылкам в них, так же вдумчиво их исследовать, а при необходимости пойти еще глубже.

Пишем код

Но основное для разработчика – это, безусловно, знание писать отличный код. И принимать программиста на работу только на основе его теоретических познаний, мягко говоря, необычно. Здесь дозволено припомнить отрывок из книги Тома де Марко и Тимоти Листера «Человеческий фактор»:

Администратор: «Как давным-давно вы жонглируете?»
Кандидат: «Ну, приблизительно шесть лет».
Администратор: «Тремя, четырьмя, пятью шарами умеете жонглировать?»
Кандидат: «Да, да и да».
Администратор: «Трудитесь с горящими предметами?»
Кандидат: «Безусловно».
Администратор: «…ножами, топорами, открытыми коробками с сигарами, мягкими широкополыми шляпами?»
Кандидат: «Мне всё равно, чем жонглировать».
Администратор: «А какую-нибудь весёлую скороговорку знаете?»
Кандидат: «Онабесподобна».
Администратор: «Что ж, мне нравится. Думаю, мы вас возьмём».
Кандидат: «Аааа… Не хотите посмотреть, как я жонглирую?»
Администратор: «Хм, мне как-то это не пришло в голову».

Следственно, на последнем этапе кандидату предлагается исполнить утилитарное задание.

Теперь мы вам приведем разбор задачи сходственной тем, что могут попасться вам на собеседовании. Мы придумали ее намеренно для демонстрации среднего яруса трудности.

По условию задачи у вас есть формула с цифрами, операциями -*/ и скобками. Необходимо написать программу, которая ее вычисляет. Формула может быть огромный. Хочется, Дабы программу дозволено было легко дорабатывать, вводя функции, параметры и т.д.

Перед тем как оставить кандидата один на один с задачей, мы традиционно спрашиваем, как он собирается ее решать. Немного славного через 2 часа найти, что человек представления не имеет с какой стороны к задаче приступить. В момент обсуждения дозволено немножко направить и подсказать алгорифм.

Для проверки яруса трудности задачи мы дали ее двум нашим работникам: программисту и администратору, тот, что прежде был программистом, но не практикует теснее несколько лет. Обоих мы попросили не освежать теорию и писать по памяти. 1-й совладал с задачей за два часа, а у второго на решение ушло четыре часа. Задача получилась несколько труднее, чем типовые, на решение которых традиционно уходит от получаса до 2-х. Для примера разберем решение максимально наглядно. Бывалый разработчик поймет все это, не задумываясь.

Выходит, мы парсим формулу. В итоге хотим получить синтаксическое дерево, где в узлах будут операции, а в листьях – константы. Если бы скобок не было, дерево было бы дюже простым. У нас есть два приоритета операций, соответственно, дерево получается двухуровневым: сверху стоит и -, снизу * и /.

Но так как скобки присутствуют, дерево может иметь неограниченную вложенность. Наивное решение задачи выглядит дальнейшим образом: находим скобки и всецело выкусываем их из получившейся строки и заменяем, скажем, на имена переменных a1, a2, a3, a4… Позже разбора получаем двухуровневое дерево. После этого в всяком узле дерева, где осталась переменная, проводим разбор того, что было в скобках, и вставляем итоги взамен соответствующих кусков поддерева.

Впрочем у нас получился алгорифм с квадратичной временной трудностью в худшем случае, т.к. если у нас будет, скажем, 100 миллионов вложенных скобок, то на всяком шаге придется искатьзакрывающую скобку, а потом еще раз парсить эту строку, пройдя ее целиком без правого и левого символов. По огромному счету, это теснее дозволено посчитать за правильное решение, трудиться такая программа будет.

Но множество программистов отметет данный подход как слишком лобовой и неэффективный, сразу перейдя к поиску линейного алгорифма. Для этого, видимо, начиная рекурсию со скобками не необходимо сразу искать закрывающую. Оба наших тестовых кандидата пошли именно по этому пути. Действующий программист знал, как именно это сделать, а потому совладал с задачей значительно быстрей, а администратору пришлось с этим повозиться. В итоге у обоих получилось что-то схожее на recursive descent parser (подвид LL-парсера).

Код администратора

#include <string>
#include <cassert>
#include <memory>
#include <stdexcept>
#include <vector>
#include <iostream>
#include <cstdio>

using namespace std;

struct Token {
        enum Type { value, operation, opening_bracket, closing_bracket};
        Type type;
        string text;
};

struct Tokenizer {
        //I am too lazy to write real tokenizer, it is very simple. I hope formula generator for fake tokenizer will be ok. 
public:
        Tokenizer() { content=generate(); pos=content.begin(); } ;
        const Token* peek() { return pos!=content.end() ?&*pos:0; } ;
        const Token* get() { if (pos!=content.end()) { cout << pos->text << " "; } return pos!=content.end()?&*pos  :0; } ;
private:
    vector<Token> generate(int level=0);

private:
        vector<Token>::iterator pos;
        vector<Token> content;
};

//To be honest using classes for expression parsing is a bit overkill, old style could save some code. 
class Expression;
typedef struct auto_ptr<Expression> ExpressionPtr;

//Represents abstract expression
class Expression {
public:
        Expression() {}
        virtual ~Expression() {}
        //actually this static parse functions should be constructor in most classes. I.e. this is violation of principle 'Resource Acquisition Is Initialization'.
        //but some functions return different classes depending on context, i.e. this is some kind of 'virtual constructor' (see Operation::parse for example)
        //so I made decision to make static construction function in all classes, just for uniformity
        static ExpressionPtr parse(Tokenizer& tokens);
        virtual float calc()=0;
        virtual void debug(string prefix)=0;
};

//Represents single value: for example 3.1415
class Value: public Expression {
public:
        Value() {}
        virtual ~Value() {}
        static bool isItYou(Tokenizer& tokens);
        static ExpressionPtr parse(Tokenizer& tokens);
        virtual float calc() { return _value; }
        virtual void debug(string prefix) { cout << prefix << "Value(" <<  calc() <<"): " << _value << endl; } 
protected:
        float _value;
};

//Represents operation:   or -
class Operation: public Expression {
public:
        Operation() {};
        virtual ~Operation() {}
        static ExpressionPtr parse(Tokenizer& tokens, ExpressionPtr& l);
        virtual float calc();
        virtual void debug(string prefix) { cout << prefix << "Operation(" <<  calc() <<"): " << _operation << endl; if ( _left.get() ) _left->debug(prefix   "\t"); if ( _right.get() ) _right->debug(prefix   "\t"); } 
protected:
        std::auto_ptr<Expression> _left;
        std::auto_ptr<Expression> _right;
        string _operation;
};

//Represents operation: * or /
class PriorityOperation: public Operation {
public:
        PriorityOperation() {};
        virtual ~PriorityOperation() {}
        static ExpressionPtr parse(Tokenizer& tokens, ExpressionPtr& left);
        //virtual float calc(); inherit it
        virtual void debug(string prefix) { cout << prefix << "PriorityOperation(" <<  calc() <<"): " << _operation << endl; if ( _left.get() ) _left->debug(prefix   "\t"); if ( _right.get() ) _right->debug(prefix   "\t"); } 
};

//Represents bracketed expression: (expr)
class BracketExpression: public Expression {
public:
        static bool isItYou(Tokenizer& tokens);
        static ExpressionPtr parse(Tokenizer& tokens);
        virtual float calc() { return _internal->calc(); } ;
        virtual void debug(string prefix) { cout << prefix << "Brackets(" <<  calc() <<"): "  << endl; _internal->debug(prefix   "\t"); } 
protected:
        std::auto_ptr<Expression> _internal; 
};

ExpressionPtr Expression::parse(Tokenizer& tokens)
{
    //cout << "Expression::parse" << endl;

        if (!tokens.peek()) return ExpressionPtr();

        if ( BracketExpression::isItYou(tokens) )
        {
                return BracketExpression::parse(tokens);
        }
        else
        if ( Value::isItYou(tokens) )
        {        
                return Value::parse(tokens);
        }
        else
        {
                throw logic_error("(Expression) Wtf is that: "   tokens.peek()->text );
        }
}

bool Value::isItYou(Tokenizer& tokens) 
{
        const Token* t = tokens.peek();
        if ( !t || t->type != Token::value ) return false; 

        char* endptr;
        strtod( t->text.c_str(), &endptr);
        return *endptr == 0;
}

ExpressionPtr Value::parse(Tokenizer& tokens)
{
    //cout << "Value::parse" << endl;

        std::auto_ptr<Value> foo( new Value );

        const Token* t=tokens.get();
        assert( t && t->type == Token::value ); 

        char* endptr;
        foo->_value = strtod( t->text.c_str(), &endptr);
        return ExpressionPtr(foo.release()); //lack of heterosexual auto_ptr conversions is killing me
}

bool BracketExpression::isItYou(Tokenizer& tokens) 
{
        return tokens.peek() && tokens.peek()->type == Token::opening_bracket;
}

ExpressionPtr BracketExpression::parse(Tokenizer& tokens)
{
    //cout << "BracketExpression::parse" << endl;
        const Token* t=tokens.get();
        assert ( t->type == Token::opening_bracket );

        auto_ptr<BracketExpression> result ( new BracketExpression );
        ExpressionPtr null;
        result->_internal = Operation::parse(tokens, null);

        t = tokens.get();
        if ( t ==0 || t->type != Token::closing_bracket )
        {
                throw logic_error("(BracketExpression) mismatched brackets ");
        }

        return ExpressionPtr(result.release());
}

ExpressionPtr Operation::parse(Tokenizer& tokens, ExpressionPtr& l)
{
    //cout << "Operation::parse:" << l.get() << endl;
        ExpressionPtr left;

        if (l.get()) 
        {
                left=l;
                // left is assigned for us.
        }
        else
        {
                left=Expression::parse(tokens);
        }        

        const Token *t=tokens.peek();
        if (!t || t->type == Token::closing_bracket  ) return left; //just return Value, sorry no operation guys

        if (t->type == Token::operation && (t->text=="*" || t->text=="/") )
        {
                ExpressionPtr result = PriorityOperation::parse(tokens, left);                 
                //we got exit after priority operations will end, parse position will be on   or - or at end
                left = result;        

                t=tokens.peek();
                if (!t || t->type == Token::closing_bracket  ) return left; //just return Value, sorry no operation guys
        }

    //cout << "Operation::parse again" << endl;
        if (t->type == Token::operation && (t->text==" " || t->text=="-") )
        {
                tokens.get(); //just commit previous peek

                auto_ptr<Operation> result ( new Operation );
                result->_operation = t->text;
                result->_left=left; //smart ptr giveup ownership

        //cout << "Operation::parse before token" << endl;
        ExpressionPtr foo=Expression::parse(tokens);
        //cout << "Operation::parse after expression" << endl;

        const Token *t=tokens.peek();

        if (t != 0 && (t->text=="*" || t->text=="/"))
        {
            //cout << "Operation::parse to priority" << endl;
            foo = PriorityOperation::parse(tokens,foo);
        }

        result->_right=foo;

        ExpressionPtr bar(result.release());
        return Operation::parse(tokens, bar);

        }
        else
        {
                throw logic_error("(Operation) Wtf is that: "   tokens.peek()->text);
        }
}

ExpressionPtr PriorityOperation::parse(Tokenizer& tokens, ExpressionPtr& left)
{
    //cout << "PriorityOperation::parse" << endl;

    // left is already assigned for priority operation
        const Token *t=tokens.peek();
        if (!t || t->type == Token::closing_bracket  ) return left; //just return Value, sorry no operation guys

        if (t->type == Token::operation && (t->text=="*" || t->text=="/") )
        {
                tokens.get(); //commit previuos peek

                auto_ptr<PriorityOperation> result ( new PriorityOperation ); 
                result->_operation = t->text;
                result->_left=left;
                result->_right=Expression::parse(tokens);
                ExpressionPtr rs(result.release());

                return PriorityOperation::parse(tokens, rs);

        }
        else if (t->type == Token::operation && (t->text==" " || t->text=="-") )
        {
                return left;
        }
        else
        {
                throw logic_error("(PriorityOperation) Wtf is that: "   tokens.peek()->text );
        }
}

float Operation::calc()
{
        if (_operation == " ")
        {
                float l=_left.get()?_left->calc():0.0f;
                float r=_right.get()?_right->calc():0.0f;
                return l r;
        }
        else
        if (_operation == "-")
        {
                float l=_left.get()?_left->calc():0.0f;
                float r=_right.get()?_right->calc():0.0f;
                return l-r;
        }        
        else
        if (_operation == "*")
        {
                float l=_left.get()?_left->calc():1.0f;
                float r=_right.get()?_right->calc():1.0f;
                return  l*r; 
        }        
        else
        if (_operation == "/")
        {
                float l = _left.get()?_left->calc():1.0f;
                float r = _right.get()?_right->calc():1.0f;
                return l/r;
        }
        else
        {
                throw logic_error("Wft: operation udefined");
        }                
}

//returning vector by value, will be slow( O(n*n) actually ), but it is just testing code.
vector<Token> Tokenizer::generate(int level)
{
    //be careful with this value - formula size is approx 2^level
    if (level > 6)
    {
        Token t;
        char f[100];
        snprintf(f,100,"%d",int(rand() % 100));
        t.text=f;
        t.type=Token::value;
        return vector<Token>(&t,&t 1);
    }

    if (rand() % 10 == 0)
    {
            vector<Token> result;
            Token t1,t2;
            t1.type=Token::opening_bracket;
            t1.text="(";
            t2.type=Token::closing_bracket;
            t2.text=")";
            result.push_back(t1);
            vector<Token> r=generate(level 1);
            result.insert(result.end(),r.begin(),r.end());
            result.push_back(t2);

            return result;
    }        

    char op = " -*/"[rand()%4];
    Token t;
    t.type=Token::operation;
    t.text=op;

    vector<Token> result=generate(level 1);
    result.push_back(t);
    vector<Token> r2=generate(level 1);
    result.insert(result.end(),r2.begin(),r2.end());

    return result;
}

int main()
{        
        try
        {
                //create fake tokenizer
                Tokenizer tk;

                //parse it
                ExpressionPtr null;
                ExpressionPtr foo = Operation::parse(tk,null);
                cout << endl;
                foo->debug("");
                float result = foo->calc();        
                cout << "result = " << result << endl;
        }
        catch(exception& e)
        {
                cout << e.what() << endl;
                return 1;                  
        }

        return 0;
}

Код разработчика

#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <string>
#include <vector>
#include <stdexcept>

struct token {
    enum { E_UNDEF, E_NUMBER, E_OPERATOR, E_LEVEL  } type;

    union {
        double d_val;
        int i_val;
        char c_val;
    } data;

    token() {
        type = E_UNDEF;
    }

    token(double val) : type(E_NUMBER) {
        data.d_val = val;
    }
    token(int val) : type(E_LEVEL) {
        data.i_val = val;
    }
    token(char val) : type(E_OPERATOR) {
        data.c_val = val;
    }
};

typedef std::vector<token> tokens;

void push_level(tokens &pr, int level) {
    if (pr.empty() || pr.back().type != token::E_LEVEL) {
        pr.push_back(token(level));
    } else {
        pr.back().data.i_val  = level;
    }
}

void push_operator(tokens &pr, char op) {
    pr.push_back(token(op));
}

void push_number(tokens &pr, int num) {
    if (pr.empty() || pr.back().type == token::E_LEVEL || (pr.back().type == token::E_OPERATOR && pr.size() > 1 && pr[pr.size() - 2].type == token::E_NUMBER) ) {
        pr.push_back(token((double)num));
    } else if (pr.back().type == token::E_OPERATOR && (pr.size() == 1 || pr[pr.size() - 2].type == token::E_LEVEL) ) {
        if (pr.back().data.c_val == '*' || pr.back().data.c_val == '/') {
            throw std::domain_error("unexpected operator");
        }
        if (pr.back().data.c_val == '-') {
            num = -num;
        }
        pr.pop_back();
        pr.push_back(token((double)num));
    } else {
        throw std::domain_error("unexpected number");
    }
}

void pop_level(tokens &pr, int level) {
    if (level <= 0) {
        return;
    }
    if (pr.empty() || pr.back().type == token::E_LEVEL || pr.back().type == token::E_OPERATOR) {
        throw std::domain_error("unexpected closing brace");
    } else if (pr.size() > 1 && pr[pr.size() - 2].type == token::E_LEVEL) {
        if (pr[pr.size() - 2].data.i_val > level) {
            pr[pr.size() - 2].data.i_val -= level;
        } else {
            int delta = level - pr[pr.size() - 2].data.i_val;
            token tmp = pr.back();
            pr.pop_back(); pr.pop_back();
            pr.push_back(tmp);
            pop_level(pr, delta);
        }
    } else if (pr.size() > 3) {
        token s2 = pr.back(); pr.pop_back();
        token op = pr.back(); pr.pop_back();
        token s1 = pr.back(); pr.pop_back();

        if (s1.type != token::E_NUMBER || op.type != token::E_OPERATOR || s2.type != token::E_NUMBER) {
            throw std::domain_error("unexpected closing brace");
        }

        switch (op.data.c_val) {
            case ' ':
                s1.data.d_val  = s2.data.d_val;
                break;
            case '-':
                s1.data.d_val -= s2.data.d_val;
                break;
            case '*':
                s1.data.d_val *= s2.data.d_val;
                break;
            case '/':
                s1.data.d_val /= s2.data.d_val;
                break;
            default:
            throw std::domain_error("internal error");
        }

        if (pr.back().type == token::E_LEVEL) {
            if (pr.back().data.i_val > level) {
                pr.back().data.i_val -= level;
                pr.push_back(s1);
            } else {
                int delta = level - pr.back().data.i_val;
                pr.pop_back();
                pr.push_back(s1);
                pop_level(pr, delta);
            }
        } else if (pr.back().type == token::E_OPERATOR) {
            pr.push_back(s1);
            pop_level(pr, level);
        } else {
            throw std::domain_error("unexpected closing brace");
        }
    } else {
        throw std::domain_error("unexpected closing brace");
    }
}

double process(const std::string &str) {
    tokens program;

    push_level(program, 3);
    for (std::string::const_iterator cit = str.begin(); cit != str.end();   cit) {
        switch (*cit) {
            case '(':
                push_level(program, 3);
                break;
            case ')':
                pop_level(program, 3);
                break;
            case '*':
            case '/':
                pop_level(program, 1);
                push_operator(program, *cit);
                push_level(program, 1);
                break;
            case ' ':
            case '-':
                if (cit == str.begin() || strchr("( /-*", *(cit-1))) {
                    push_operator(program, *cit);
                } else {
                    pop_level(program, 2);
                    push_operator(program, *cit);
                    push_level(program, 2);
                }
                break;
            case ' ':
                break;
            case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9':
                {
                    int curnum = 0;
                    while (cit != str.end()) {
                        curnum = 10*curnum   (*cit - '0');
                        if ((cit   1) == str.end() || !isdigit(*(cit 1))) {
                            break;
                        }
                          cit;
                    }
                    push_number(program, curnum);
                }
                break;
            default:
                throw std::domain_error("unexpected symbol");
        }
    }
    pop_level(program, 3);

    if (program.size() == 0 || program.size() > 1) {
        throw std::domain_error("incomplete expression");
    }

    return program.back().data.d_val;
}

int main() {
    std::string line;
    while (!std::cin.eof()) {
        std::getline(std::cin, line);

        if (line.length() > 0) {
            try {
                std::cout << process(line) << std::endl;
            } catch (std::exception &e) {
                std::cout << "error: " << e.what() << std::endl;
            }
        }
    }

    return 0;
}

Если вы знаете о существовании каких-нибудь классических алгорифмов в этой области, то задача становится проще. Впрочем что-то работающее может написать фактически всякий.

Если говорить об алгорифмах разбора, то типичным считается shunting yard algorithm. Также помимо Recursive descent parser знаменитыми является LR-парсер.

Помимо кода

Если же кандидат претендует на звание senior-разработчика, мы непременно постараемся осознать, имеет ли он представление о планировании архитектуры системы.

Зачастую мы говорим о подходе к разработке. Это исключительно значимо для людей, которые утверждают, что имеют навык управления планами, но также мы Зачастую разговариваем на эти темы и с обыкновенными разработчиками, в этом случае это теснее не тест, скорее мы легко хотим осознать, чего человек ожидает, и будет ли ему у нас удобно трудиться.

Значимая часть собеседования – обсуждение планов, над которыми кандидату довелось поработать. Необходимо осознать, какую роль он играл в их исполнении. В резюме может быть указан десяток мегапроектов, но традиционно довольно пары вопросов по всякому из них, Дабы осознать, подлинно ли человек играл в них значимую роль либо был на подхвате.

Увлекательно также послушать, о каких аспектах плана кандидат будет рассказывать подробнее каждого. Это может раскрыть некоторые детали о его специализации. В свою очередь, мы рассказываем о том, какие планы может предложить Яндекс. У всех есть свои интересы и предпочтения, а вероятность выбора – это неизменно славно.

В финальном результате, мы принимаем решение, подходит нам кандидат либо нет. Впрочем окончательно ясно, приживется ли человек в Яндексе, становится позже испытательного срока. Если бы о работнике все дозволено было осознать экстраординарно по собеседованию, мир бы выглядел вовсе напротив.

На тех, кто по каким-то причинам не прошел собеседование, мы за редкими исключениями крест не ставим. Как правило, мы готовы рассматривать кандидата вновь через некоторое время. Многие рекрутирующие разработчики дают кандидатам советы и список литературы. Правда, мы не можем поручиться, что так делают безусловно все, но Зачастую для разработчика отказ кандидату – не меньший стресс, чем для самого кандидата.

Список литературы

  • Вирт, «Алгорифмы конструкции данных = программы»
  • Кормен, Ривест, и другие «Алгорифмы: построение и обзор»
  • Липпман «Основы программирования на С »
  • Scott Meyers. Effective C . More Effective C . Effective STL
  • Herb Sutter «Exceptional C » «More Exceptional C »

За всеми обновлениями списка вакансий дозволено следить тут.

 

Источник: programmingmaster.ru

 

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB