面試常備---棧和隊列總結篇


      正式學習編程也就1年而已,在這1年里,要學習C/C++,Java,C#這些主流語言,還要熟悉JavaScript,HTML,CSS這些前端開發知識,加上一些Android應用軟件,網站站點的開發工作,導致我現在就是一個大雜燴,什么都知道一點,但又什么都不精通。現在又面臨畢業找工作壓力,不知道自己應該找什么工作,畢竟自己好像什么都碰過,心浮氣躁,原本基礎就是薄弱,還要在這段日子頂着壓力,將手頭上的項目努力完成,畢竟開發軟件不難,但維護軟件特別難,像是已經發布的網站,現在面臨服務器被攻擊而無法正常運行的問題。果然還是那句行內的老話:當軟件正式上線運行的時候,真正麻煩的事情才正式開始啊!!

     相信這也是現在應屆畢業生的現狀,困擾着是要准備筆試和面試,還是利用這短短的一個學期努力做出作品來。當然,兩者都可以兼顧,可惜我們都不是那種游刃有余的人,尤其是像我們這樣臨時過來的碼農臨時工。。。

      我學習的第一門語言是Java,雖然不精通,但算得上熟悉,無奈現在大部分的筆試和面試都是考察C/C++,尤其是指針,畢竟像是數據結構這些東西,很多都是用指針實現的。指針是我們學習C/C++的鬼門關,我剛從Java過來的時候,一看到指針就頭疼,現在依然頭疼,即使知道一些指針的高級技巧,像是一些看似非常強大的一句多重指針的代碼濃縮好幾行語句這類的東西,我是不支持的,也不認為自己能寫好,想起C#的編程宗旨:能夠朗讀的代碼,我就對指針有一種天生的恐懼:自己是否用錯了指針,該指針是否空指針,所指向的內存是否被釋放掉。。。在看一些C/C++的源碼時,我就經常為那些變量名所困擾:大量的宏定義導致我無法從變量名推敲出它的真正意思!經常需要翻閱頭文件查找相關的宏定義才能知道這個變量和類型是什么,也對那些意義不明的英文單詞縮寫的函數名感到頭疼,基本函數庫,像是字符串庫還好,縮寫還是很到位的,但一些第三方庫就不敢恭維了。我情願函數名寫得長一點,也不願為了所謂的短小寫出不精悍的函數名出來。

      我是一個學藝不精的碼農,自認自己沒有天賦,包括努力的天賦,可悲的是,努力也是需要天賦的,有些人就是非常懂得努力,他們能夠迅速的掌握努力的技巧並找到持續努力的動力,這些人大部分都是以興趣為起點,但像是我們這類的平庸碼農,可能就是為了一碗飯碗。。。

      不管怎樣,我們都已經開始啟動了,是慢跑,還是快跑,都已經不重要,最重要的是,能否堅持到終點,像是一些人,跑得很快,但也很快就沒有體力倒下了,一些人,就算是慢跑,也是跑着跑着就沒了,還在跑的,有哭的,也有笑的,更多的是像我們一樣在死磕着。。。

      畢竟,人生其實就是一場怎么也看不到終點的無限期馬拉松,對個人而言,只有死亡才能讓我們從這場比賽中脫身,但有誰知道死后是否還要在另一個世界中繼續跑呢?

     言歸正傳,棧和隊列也是非常常見的數據結構,它們本身的特點就非常適合用來解決一些實際的問題。

     棧對於學習計算機的人來說,是再熟悉不過的東西了,很多東西都需要用棧來存儲,像是操作系統就會給每個線程創建一個棧用來存儲函數調用時各個函數的參數,返回地址及臨時變量等,函數本身也有一個函數棧用來存儲函數的局部變量等。

     棧的特點就是后進先出,需要O(N)的時間才能找到棧中的最值。

     隊列和棧剛好相反,是先進先出,表面上和棧是一對矛盾體,但實際上,都可以利用對方來實現自己。

題目一:用兩個棧實現一個隊列,並分別實現在隊列尾部插入結點和在頭部刪除結點的功能。 

     這道題目要求我們用"先進后出"的棧來實現"先進先出"的隊列,實際上的確是有可能的:將幾個元素壓入其中一個棧的時候,的確是"先進后出",但是可以將這些元素再壓入另一個棧,這樣就可以將最后一個元素,也就是先壓入的元素放在棧頂,也就是"先進先出"了。
template <typename T> class CQueue
{
      public:
            CQueue(void);
            ~CQueue(void);
     
            void appendTail(const T& node);
            T deleteHead();

      private:
            stack<T> stack1;
            stack<T> stack2;
};

template<typename T> void CQueue<T> :: appendTail(const T& element)
{
      stack1.push(element);
}

template<typename T> T CQueue<T> :: deleteHead()
{
      if(stack2.size() <= 0)
      {
            while(stack1.size() > 0)
            {
                 T& data = stack1.top();
                 stack1.pop();
                 stack2.push(data);
            }
      }

      if(stack2.size() == 0)
      {
             throw new exception("queue is empty");
      }

      T head = stack2.top();
      stack2.pop();

      return head;
}

題目二:定義棧的數據結構,在該類型中實現一個能夠得到棧的最小元素的min函數,在該棧中,要求調用min,push和pop的時間復雜度都是O(1)。

     我們首先的第一想法就是在每次將元素壓入棧的時候,保留當前的最小值,但仔細想想,如果最小值已經被彈出棧了,又該如何得到剩下元素中的最小值呢?
     我們可以使用一個輔助棧,每次壓入元素的時候,將最小值壓入,每次彈出元素的時候,也將最小值彈出,確保這兩個棧的動作是同步的。
template <typename T> void StackWithMin<T> :: push(const T& value)
{
      m_data.push(value);

      if(m_min.size() == 0 || value < m_min.top())
     {
           m_min.push(value);
     }
     else
     {
           m_min.push(m_min.top());
     }
}

template <typename T> void StackWithMin<T> ::pop()
{
     assert(m_data.size() > 0 && m_min.size() > 0);
     
     m_data.pop();
     m_min.pop();
}

template <typename T> const T& StackWithMin<T> :: min() const
{
      assert(m_data.size() > 0 && m_min.size() > 0)
      
      return m_min.top();
}

      其中,m_data是數據棧,而m_min就是輔助棧,而assert函數就是斷言。所謂的斷言,就是我們會提出一種假設,像是這樣,就假設數據棧和輔助棧的大小都是大於0,這是用於測試使用,當然,我們也可以使用一般的if語句來代替。
     棧的彈出和壓入是棧最重要的兩個基本動作,也是經常要被考察的知識點。

題目三:輸入兩個整數序列,第一個序列表示壓棧順序,判斷第二個序列是否是彈出順序。

     既然是兩個整數序列,那么輔助棧是需要的。但是這里有個問題:壓棧順序在數字序列沒有被壓入棧的時候就已經確定了,根本不需要將整個數字序列完全壓入棧后才知道壓棧順序,所以,真正涉及到壓棧動作的是輔助棧。
     我們來看看一個簡單的數字序列:{1,2,3,4},彈出順序應該是{4,3,2,1}。在驗證后面的序列是否是彈出順序時,我們先看看數字1。既然1是第一個入棧的,那么在第二個序列中應該是最后一個元素,如果將第二個序列壓入輔助棧,應該是棧頂元素,然后我們將它彈出去。這樣一步一步的檢查輔助棧每次棧頂的元素是否和第一個序列對應的數字相同,就能知道第二個序列是否是彈出序列。
bool IsPopOrder(const int* pPush, const int* pPop, int nLength)
{
    bool bPossible = false;

    if(pPush != NULL && pPop != NULL && nLength > 0)
    {
          const int* pNextPush = pPush;
          const int* pNextPop = pPop;

          std :: stack<int> stackData;

          while(pNextPop - pPop < nLength)
          {
               while(stackData.empty() || stackData.top() != *pNextPop)
               {
                    if(pNextPush - pPush == nLength)
                    {
                          break;
                    }

                    stackData.push(*pNextPush);

                    pNextPush++;
               }

               if(stackData.top() != *pNextPop)
              {
                    break;
              }

              stackData.pop();
              pNextPop++'
          }

          if(stackData.empty() && pNextPop - pPop == nLength)
          {
               bPossible = true;
          }
    }

    return bPossible;
}

     考察棧並不一定考察我們是否會編寫關於棧的代碼,由於棧和計算機的內存存儲方式有關,所以也會有關於這些的基本知識。

     在計算機為一個程序段分配內存的時候,全局變量和靜態變量分配的內存是連續的,並且是存放在數據段中。對於一個進程的內存空間而言,可以在邏輯上分為3個部分:代碼區,靜態數據區和動態數據區,動態數據區就是我們常說的堆棧。棧和堆是兩種不同的動態數據區,棧是一種線性結構,而堆是一種鏈式結構。進程中的每個線程都有自己的棧,因此即使每個線程的代碼是一樣的,但是它們的局部變量是互不干擾的。

      一個堆棧可以通過基地址和棧頂地址來描述。全局變量和靜態變量分配在靜態數據區,局部變量分配在堆棧中,所以我們可以通過堆棧的基地址和偏移量來訪問局部變量。
      前面的討論是基於C,如果是C++和Java,還有一種通過new來分配內存的方式,這時就是存儲在堆中。
      棧和堆有什么區別呢?
      棧的內存空間由操作系統自動分配和釋放,但是堆上的內存空間就只能手動分配和釋放,所以我們在C++中經常是在new一個對象后,再顯式的刪除該對象。
      為什么要這樣呢?因為棧的空間是有限的,有序的,所以操作系統可以方便的對它進行管理,但是堆是一個很大的自由存儲區,無序的,要想正確的刪除某個對象所占有的內存,系統可能需要花點時間來查找,這在new的對象足夠多的情況下,是一個嚴重的效率問題,所以C++並不會幫我們自動處理,它將這個責任完全交給用戶,而Java通過垃圾回收器在一定的程度上緩解了這個問題,注意,是緩解而不是解決。
       C中的malloc函數分配的內存空間就是在堆上,而程序在編譯期對變量和函數分配內存都是在棧上,並且程序運行中函數調用參數的傳遞也是在棧上。

   

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM