1. 程式人生 > >金山WPS2016春季實習校園招聘筆試&面試問題回憶

金山WPS2016春季實習校園招聘筆試&面試問題回憶

下面將我在廣州參加的2016年春季金山WPS實習招聘的整個過程中遇到的問題記錄如下。不全,但是有些題目還是值得思考的。

1.筆試題

2016.4.11晚上在中山大學東校區(大學城校區)參加了金山WPS的筆試。記憶較為深刻的有如下幾題。

題目一:
以下程式碼片段,輸出的結果是什麼?

    vector<int> vec(5);
    cout<<vec.size()<<endl;    //1
    vec.reserve(100);
    cout<<vec.size()<<endl;    //2
    vec.resize(50
); cout<<vec.size()<<endl; //3 cout<<vec.capacity()<<endl;//4

本題考察的是vector向量容器的成員函式reserve()和resize()的作用和區別。reserve()用來改變vector向量容器的容量,即vec.capacity()的返回值。resize()用於改變vector的元素數量。所以程式碼中1,2,3,4的輸出依次是:5,5,50,100。

題目二:
這是一道程式設計題,求三個矩形的交集矩形。
給定矩形的定義如下:

struct Rect{
    int
x; //表示矩形的左上水平座標 int y; //表示矩形的左上垂直座標 int w; //表示矩形寬度 int h; //表示矩形高度 };

現在給三個矩形,求三個矩形的交集,如果沒有交集,那麼矩形的x,y,w和h均賦值為-1。例如下面示例圖,求出三個矩形相交的粗線線框表示的矩形。

這裡寫圖片描述

解題思路:
解題思路很重要,沒有集體思路,題目肯定是做出不來的。下面給出本人的解題思路:
(1)判斷三個矩形有沒有交集。這個是難點,該怎麼做呢?可以在x軸方向將三個矩形按x的大小從左到右排列,判斷兩兩矩形在x軸方向是否有交集,如果有任意一對沒有相交那麼三個矩形沒有交集。判斷方法是如果rectB.x>=rectA.x+rectA.w的話,那麼說明rectA和rectB之間沒有交集。

同理,在y軸方向做同樣的判斷;

(2)求出任意兩個矩形的交集矩形,再將交集矩形與第三個矩形再求交集,可得最後的交集矩形。

有了正確和清晰的思路,就可以寫程式碼了,下面給出本人的實現,可供網友參考。

#include <iostream>
using namespace std;
#include <vector>
#include <algorithm>

struct Rect
{
    int x; //表示矩形的左上水平座標
    int y; //表示矩形的左上垂直座標
    int w; //表示矩形寬度
    int h; //表示矩形高度
};

//按照x遞增排序
bool compareX(const Rect& rectA,const Rect& rectB ){
    return rectA.x<rectB.x;
}

//按照y遞增排序
bool compareY(const Rect& rectA,const Rect& rectB ){
    return rectA.y<rectB.y;
}

//判斷三個矩形是否相交
bool isIntersect(const Rect& rectA,const Rect& rectB,const Rect& rectC){
    Rect rectLeft,rectXMid,rectRight; //從左向右的矩形
    Rect rectTop,rectYMid,rectBelow;  //從上到下的矩形

    //將矩形按照x由左向右排序
    vector<const Rect> vec;
    vec.push_back(rectA);
    vec.push_back(rectB);
    vec.push_back(rectC);
    sort(vec.begin(),vec.end(),compareX);
    rectLeft=vec[0],rectXMid=vec[1],rectRight=vec[2];

    //水平方向任意兩個矩形沒有交集
    if(rectXMid.x>=rectLeft.x+rectLeft.w||rectRight.x>=rectXMid.x+rectXMid.w||rectRight.x>=rectLeft.x+rectLeft.w)
        return false;

    //同理將矩形按照y由上往下排序
    sort(vec.begin(),vec.end(),compareY);
    rectTop=vec[0],rectYMid=vec[1],rectBelow=vec[2];

    //垂直方向任意兩個矩形沒有交集
    if(rectYMid.y>=rectTop.y+rectTop.h||rectBelow.y>=rectYMid.y+rectYMid.h||rectBelow.y>=rectTop.y+rectTop.h)
        return false;
    return true; //三個矩形有交集
}

//兩個矩形的交集,前提是兩個矩形一定有交集
Rect intersection(const Rect& rectA,const Rect& rectB){
    Rect resRect;
    resRect.x=rectA.x>rectB.x?rectA.x:rectB.x; //選最右邊的矩形的x作為交集的x
    resRect.y=rectA.y>rectB.y?rectA.y:rectB.y; //選最下面的矩形的y作為交集的y
    //選擇左邊矩形(x座標較小者)的右邊的作為交集矩形的右邊,這樣就可以求出交集矩形的寬度
    resRect.w=rectA.x+rectA.w<rectB.x+rectB.w?rectA.x+rectA.w-resRect.x:rectB.x+rectB.w-resRect.x;
    //同理,選擇上面矩形(y座標較小者)的下邊的作為交集矩形的下邊,這樣就可以求出交集矩形的高度
    resRect.h=rectA.y+rectA.h<rectB.y+rectB.y?rectA.y+rectA.h-resRect.y:rectB.y+rectB.h-resRect.y;
    return resRect;
}


//求三個矩形的交集
Rect threeIntersection(const Rect& rectA,const Rect& rectB,const Rect& rectC){
    Rect res;
    bool isIntersectBool=isIntersect(rectA,rectB,rectC);
    if(isIntersectBool){ //有相交
        Rect rectAB=intersection(rectA,rectB);
        res=intersection(rectAB,rectC);
    }
    else
        res.x=res.y=res.w=res.h=-1;
    return res;
}

測試結果如下:


int main(){
    Rect rectA,rectB,rectC;
    //測試案例1
    //rectA.x=0,rectA.y=0,rectA.w=1,rectA.h=1;
    //rectB.x=1,rectB.y=1,rectB.w=1,rectB.h=1;
    //rectC.x=2,rectC.y=2,rectC.w=1,rectC.h=1;

    //測試案例2
    rectA.x=0,rectA.y=0,rectA.w=2,rectA.h=2;
    rectB.x=1,rectB.y=1,rectB.w=1,rectB.h=1;
    rectC.x=1,rectC.y=1,rectC.w=1,rectC.h=1;

    Rect resRect=threeIntersection(rectA,rectB,rectC);
    if(resRect.x!=-1){ //有相交
        cout<<"resRect.x:"<<resRect.x<<endl;
        cout<<"resRect.y:"<<resRect.x<<endl;
        cout<<"resRect.w:"<<resRect.x<<endl;
        cout<<"resRect.h:"<<resRect.x<<endl;
    }
    else
        cout<<"not intersect"<<endl;
    getchar();
}

測試案例1輸出:not intersect;
測試案例2輸出:
resRect.x:1
resRect.y:1
resRect.w:1
resRect.h:1

2.一面試題

2016.4.16日在大學城華工校內教學樓A5參加了XX的面試。有些題目記不太清了,簡要記錄我記得的題目。

一面大概經歷30分鐘的時間,問了C++基礎知識和專案的一些問題,總體來說難度不大。

問題一:
請先自我介紹吧!
答:
介紹了我是在校學生,在校期間主要學習和研究的方面。

問題二:
你用過define吧,define的作用以及inline與其的區別。
答:
define用於巨集定義,inline用於定義行內函數。二者的區別是inline定義的行內函數在使用時直接進行替換,(像巨集一樣展開),沒有了呼叫的開銷,效率也很高。但是行內函數也是一個真正的函式,編譯器在呼叫一個行內函數時,會首先檢查它的型別安全,避免了巨集定義容易出錯的缺點。

問題三:
申明一個返回值為void的函式原型,使得該函式能夠接受函式體內申請的char*字串。
答:
其實這一道題就是考察不通過返回值如何接受指標型別的變數。使用二重指標或者引用即可。函式原型可申明如下:
void func(char*& str);

問題四:
使用過C++的操作符過載吧,你現在申明一個類的賦值操作符過載成員函式的原型。
答:
加入給定類為class A,那麼賦值操作符過載成員函式的原型可申明如下:
A& operator=(const A& a);

問題五:
請問平時用什麼IDE進行開發,VS用過吧,你知道什麼是記憶體斷點嗎?
答:
記憶體斷點的介紹見:VS2012使用條件斷點和記憶體斷點

問題六:
你用過只能指標吧,寫一個簡單的使用示例。
答:

class A;
scopted_ptr<A> spA(new A());

其他問題是在是記不起來,不過都是一些基本的C++的基礎知識點而已。

3.二面試題

二面的整個過程是由一個問題展開的,主要是一些演算法和資料結構的描述。最開始的問題描述如下:

顏色可由RGB來表示,R,G,B對應的取值是0-255,那麼RGB色彩模式可以表示256256256=224=16M(1M=1024)中顏色,大概1600萬多種顏色。現在給一個文字檔案,裡面記錄的是RGB顏色資訊,記錄的格式大概如下:

255,0,0;255,0,0;0,255,0;0,0,255;...

現在要求你統計出檔案中顏色出現次數前十的顏色是什麼?

答:
(1)使用map容器,儲存顏色和顏色出現的次數。顏色的ID使用RGB三原色對應的數值拼接在一起構成一個字串。比如顏色255,0,0,那麼該顏色可以表示成:”255000000”。
(2)現在要做的就是對map中的鍵值對pair<colorID,count>按照count進行遞減排序,取出前十個count對應的顏色即可。但是由於map是按照鍵值的大小來排序的,所以要按照值來排序的話,需要進行拷貝至vector向量容器中再排序。

其實可以直接將鍵值對儲存在vector中,但是這樣每次查詢顏色的時候會時間複雜度會比較答,所以還是採取上面的策略。

問題二:
除了上面的這個辦法,還有什麼更好辦法呢?比如不適用STL的話。
答:
不使用STL中的容器的話,我們可以將顏色值作為陣列的下標,來統計每一個顏色出現的次數。具體做法是RGB對應的值作為一個int的低位的三個位元組,那麼陣列長度就是256256256=224=16M。如果使用int陣列來儲存顏色出現的次數,那麼這個陣列的空間大小就是16M*sizeof(int)=64M,這個空間對於堆來說完全沒有問題,最後再對陣列進行遍歷取出前十個次數最多的顏色即可。

注意,這裡是不能對陣列進行排序的,因為顏色使用陣列的下標進行表示的,如果排序那麼顏色出現的次數與顏色就不能相互對應了。

問題三:
既然你這麼喜歡map,那你寫一段map容器的刪除程式碼吧,用來刪除出現指定次數的顏色。
答:
平時沒怎麼使用map來刪除容器中的元素,根據記憶,不假思索的寫出瞭如下程式碼:

//假設刪除出現次數為2的顏色
map<string,int > countMap;//存放顏色與出現次數的map容器
for(map<string,int>::iterator it=countMap.begin();it!=countMap.end();++it)
{
    if(it->second==2)
    {
        countMap.erase(it);
    }
}

問題四:
(面試官看了一下)你覺的你寫的程式碼有問題嗎?
答:
面試官出這道背後肯定隱藏著坑,等著我去跳,主要考察我對STL容器的使用的熟練程度。當時沒有看出來有問題,回來一查,果然有個巨坑,STL容器的刪除和插入操作隱藏的陷阱主要有如下兩條。
(1)對於節點式容器(map, list, set)元素的刪除,插入操作會導致指向該元素的迭代器失效,其他元素迭代器不受影響;
(2)對於順序式容器(vector,string,deque)元素的刪除、插入操作會導致指向該元素以及後面的元素的迭代器失效。

所以,在刪除一個元素的時候,是沒有什麼問題的。即:

for(map<string,int>::iterator it=countMap.begin();it!=countMap.end();++it)
{
    if(it->second==2)
    {
        countMap.erase(it);
        break;
    }
}

但是,當刪除多個出現相同次數的顏色時,程式會出現崩潰。原因是通過迭代器刪除指定的元素時,指向那個元素的迭代器將失效,如果再次對失效的迭代器進行++操作,則會帶來未定義行為,程式崩潰。解決方法有二,還是以上面的map容器為例,示例刪除操作的正確實現:

方法一:當刪除特定值的元素時,刪除元素前儲存當前被刪除元素的下一個元素的迭代器。

map<string,int >::iterator nextIt=countMap.begin();
for(map<string,int>::iterator it=countMap.begin();;)
{
    if(nextIt!=countMap.end())
    {
        ++nextIt;
    }
    else
    { 
        break;
    }
    if(it->second==2)
    {
        countMap.erase(it);
    }
    it=nextIt;
}

如何更加簡潔的實現該方法呢?下面給出該方法的《Effective STL》一書的具體實現:

for(map<string,int>::iterator it=countMap.begin();it!=countMap.end();)
{
    if(it->second==2)
    {
        countMap.erase(it++);
    }
    else
    {
        ++it;
    }
}

該實現方式利用了後置++操作符的特性,在erase操作之前,迭代器已經指向了下一個元素。

再者map.erase()返回指向緊接著被刪除元素的下一個元素的迭代器,所以可以實現如下:

for(map<string,int>::iterator it=countMap.begin();it!=countMap.end();)
{
        if(it->second==2)
        {
            it=countMap.erase(it);
        }   
        else
        {
            ++it;
        }
}

方法二:當刪除滿足某些條件的元素,可以使用remove_copy_if & swap方法。先通過函式模板remove_copy_if 按照條件拷貝(copy)需要的元素到臨時容器中,剩下未被拷貝的元素就相當於被“刪除(remove)”了,然後在將兩個容器中的元素交換(swap)即可,可以直接呼叫map的成員函式swap。參考程式碼:

#include <iostream>
#include <string>
#include <map>
#include <algorithm>
#include <iterator>  

using namespace std;

map<string,int> mapCount;

//不拷貝的條件
bool notCopy(pair<string,int> key_value)
{
    return key_value.second==1;
}

int main()
{
    mapCount.insert(make_pair("000",0));
    mapCount.insert(make_pair("001",1));
    mapCount.insert(make_pair("002",2));
    mapCount.insert(make_pair("003",1));

    map<string,int> mapCountTemp;//臨時map容器
    //之所以要用迭代器介面卡inserter函式模板是因為通過呼叫insert()成員函式來插入元素,並由使用者指定插入位置
    remove_copy_if(mapCount.begin(),mapCount.end(),inserter(mapCountTemp,mapCountTemp.begin()),notCopy);

    mapCount.swap(mapCountTemp);//實現兩個容器的交換

    cout<<mapCount.size()<<endl;     //輸出2
    cout<<mapCountTemp.size()<<endl; //輸出4

    for(map<string,int>::iterator it=mapCount.begin();it!=mapCount.end();++it)
    {
        cout<<it->first<<" "<<it->second<<endl;
    }
}

程式輸出結果:

2
4
000 0
002 2

這種方法的缺點:雖然實現兩個map的交換的時間複雜度是常量級,一般情況下,拷貝帶來的時間開銷會大於刪除指定元素的時間開銷,並且臨時map容器也增加了空間的開銷。

關於容器的刪除,總結如下:
刪除容器中具有特定值的元素:
(1)如果容器是vector、string或者deque,使用erase-remove的慣用法。如果容器是list,使用list::remove。如果容器是標準關聯容器,使用它的erase成員函式。

刪除容器中滿足某些條件的元素:
(2)如果容器是vector、string或者deque,使用erase-remove_if的慣用法。如果容器是list,使用list::remove_if。如果容器是標準關聯容器,使用remove_copy_if & swap 組合演算法,或者自己協議個遍歷刪除演算法。
參考資料:李健《編寫高質量C++程式碼》第七章,用好STL這個大輪子。

問題五:
你知道STL中容器的迭代器的底層實現機制嗎?
答:
提到STL,必須要馬上想到其主要的6個組成部件,分別是:容器、迭代器、演算法、仿函式、介面卡和空間分配器,迭代器是連線容器和演算法的一種重要橋樑。

STL中容器迭代器的本質是類物件,其作用類似於資料庫中的遊標(cursor),除此之外迭代器也是一種設計模式。我們可以對它進行遞增(或選擇下一個)來訪問容器中的元素,而無需知道它內部是如何實現的。其行為很像指標,都可以用來訪問指定的元素。但是二者是完全不同的東西,指標代表元素的記憶體地址,即物件在記憶體中的儲存位置,而迭代器則代表元素在容器中的相對位置。

要自定義一個迭代器,就要過載迭代器一些基本操作符:*(解引用)、++(自增)、==(等於)、!=(不等於)、=(賦值),以便它在range for語句中使用。range for是C++11中新增的語句,如我們對一個集合使用語句for (auto i : collection ) 時,它的含義其實為:

for(auto __begin = collection.begin(),auto __end = collection.end();__begin!=__end;++__begin)
{ 
    i = *__begin;
    ...//迴圈體
}

begin和end是集合的成員函式,它返回一個迭代器。如果讓一個類可以有range for的操作,它必須滿足以下幾條:
(1)擁有begin和end函式,它們均返回迭代器 ,其中end函式返回一個指向集合末尾,但是不包含末尾元素的值,即用集合範圍來表示,一個迭代器的範圍是 [ begin, end ) 一個左閉右開區間。
(2)必須過載++、!=和解引用(*)運算子。迭代器看起來會像一個指標,但是不是指標。迭代器必須可以通過++最後滿足!=條件,這樣才能夠終止迴圈。

下面給出最簡單的實現程式碼。我們定義一個CPPCollection類,裡面有個字串陣列,我們讓它能夠通過range for將每個字串輸出來。

class CPPCollection 
{
public:
    //迭代器類
    class Iterator
    {
    public:
        int index;//元素下標
        CPPCollection& outer;
        Iterator(CPPCollection &o, int i):outer(o), index(i){}

        void operator++()
        {
            index++;
        }
        std::string operator*() const
        {
            return outer.str[index];
        }
        bool operator!=(Iterator i)
        {
            return i.index!=index;
        }
    };

public:
    CPPCollection()
    {
        string strTemp[10]={"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"};
        int i=0;
        for(auto strIt:strTemp)
        {
            str[i++]=strIt;
        }
    }

    Iterator begin()
    {
        return Iterator(*this,0);
    }
    Iterator end()
    {
        return Iterator(*this, 10);
    }

private:
    std::string str[10];
};

我們定義了個內部的巢狀類Iterator,併為它過載了++、*、!=運算子。由於C++中的內部巢狀類與外圍的類沒有聯絡,為了訪問外部類物件的值,我們必須要傳入一個引用(或指標,本例中傳入引用)。Iterator的自增方法其實就是增加內部的一個索引值。判斷!=的方法是和另外一個迭代器做比較,這個迭代器一般是集合的末尾,當我們的索引值等於末尾的索引值end時,認為迭代器已經達到了末尾。 在CPPCollection類中,定義了begin()、end()分別返回開頭、結束迭代器,呼叫如下程式碼:

  CPPCollection cpc;
  for (auto i : cpc)
  {
      std::cout <<i<<std::endl;
  }
  //或者
  CPPCollection cpc;
  for(CPPCollection::Iterator i= cpc.begin();i!=cpc.end();++i)
  {
        std::cout<<*i<<std::endl;
   }

即可遍歷集合中的所有元素了。

在泛型演算法中,為了對集合中的每一個元素進行操作,我們通常要傳入集合的迭代器頭、迭代器尾,以及謂詞,例如std::find_if(vec.begin(),vec.end(),…),這種泛型演算法其實就是在迭代器的首位反覆迭代,然後執行相應的行為。

4.小結

金山WPS的面試讓我發現了自己的很多知識盲點,給我本人也敲響了警鐘。好好學習,好好總結吧。斷斷續續歷時一個星期才完成了本blog。有點痛苦,靡不有初鮮克有終,凡是貴在堅持,最終還是堅持了下來。

面對求職應聘,豐富的專案經驗和專業性質的比賽獲獎會給簡歷錦上添花(簡歷篩選);紮實的演算法與資料結構基礎和過硬的程式設計能力是通過線上程式設計(筆試環節)的不二法門;全面而深入的程式語言知識點是通過面試的可靠保障(面試環節)。

靜心複習,努力備戰!

參考文獻