【轉】程序員面試題精選算法58題加答案 .

 

這篇文章總結的非常好,以防以後找不到,在此轉載。

 

程序員面試題精選(01)-把二元查找樹轉變成排序的雙向鏈表
  題目:輸入一棵二元查找樹,將該二元查找樹轉換成一個排序的雙向鏈表。要求不能創建任何新的結點,只調整指針的指向。
  比如將二元查找樹
                                            10
                                          /    /
                                        6       14
                                      /  /     /  /
                                    4     8  12    16

轉換成雙向鏈表
4=6=8=10=12=14=16。
  分析:本題是微軟的面試題。很多與樹相關的題目都是用遞歸的思路來解決,本題也不例外。下面我們用兩種不同的遞歸思路來分析。
  思路一:當我們到達某一結點準備調整以該結點爲根結點的子樹時,先調整其左子樹將左子樹轉換成一個排好序的左子鏈表,再調整其右子樹轉換右子鏈表。最近鏈接左子鏈表的最右結點(左子樹的最大結點)、當前結點和右子鏈表的最左結點(右子樹的最小結點)。從樹的根結點開始遞歸調整所有結點。
  思路二:我們可以中序遍歷整棵樹。按照這個方式遍歷樹,比較小的結點先訪問。如果我們每訪問一個結點,假設之前訪問過的結點已經調整成一個排序雙向鏈表,我們再把調整當前結點的指針將其鏈接到鏈表的末尾。當所有結點都訪問過之後,整棵樹也就轉換成一個排序雙向鏈表了。
參考代碼:
首先我們定義二元查找樹結點的數據結構如下:
    struct BSTreeNode // a node in the binary search tree
    {
        int          m_nValue; // value of node
        BSTreeNode  *m_pLeft;  // left child of node
        BSTreeNode  *m_pRight; // right child of node
    };
思路一對應的代碼:
///////////////////////////////////////////////////////////////////////
// Covert a sub binary-search-tree into a sorted double-linked list
// Input: pNode - the head of the sub tree
//        asRight - whether pNode is the right child of its parent
// Output: if asRight is true, return the least node in the sub-tree
//         else return the greatest node in the sub-tree
///////////////////////////////////////////////////////////////////////
BSTreeNode* ConvertNode(BSTreeNode* pNode, bool asRight)
{
      if(!pNode)
            return NULL;
      BSTreeNode *pLeft = NULL;
      BSTreeNode *pRight = NULL;

      // Convert the left sub-tree
      if(pNode->m_pLeft)
            pLeft = ConvertNode(pNode->m_pLeft, false);

      // Connect the greatest node in the left sub-tree to the current node
      if(pLeft)
      {
            pLeft->m_pRight = pNode;
            pNode->m_pLeft = pLeft;
      }
      // Convert the right sub-tree
      if(pNode->m_pRight)
            pRight = ConvertNode(pNode->m_pRight, true);
      // Connect the least node in the right sub-tree to the current node
      if(pRight)
      {
            pNode->m_pRight = pRight;
            pRight->m_pLeft = pNode;
      }

      BSTreeNode *pTemp = pNode;
      // If the current node is the right child of its parent,
      // return the least node in the tree whose root is the current node
      if(asRight)
      {
            while(pTemp->m_pLeft)
                  pTemp = pTemp->m_pLeft;
      }
      // If the current node is the left child of its parent,
      // return the greatest node in the tree whose root is the current node
      else
      {
            while(pTemp->m_pRight)
                  pTemp = pTemp->m_pRight;
      }

      return pTemp;
}

///////////////////////////////////////////////////////////////////////
// Covert a binary search tree into a sorted double-linked list
// Input: the head of tree
// Output: the head of sorted double-linked list
///////////////////////////////////////////////////////////////////////
BSTreeNode* Convert(BSTreeNode* pHeadOfTree)
{
      // As we want to return the head of the sorted double-linked list,
      // we set the second parameter to be true
      return ConvertNode(pHeadOfTree, true);
}
思路二對應的代碼:
///////////////////////////////////////////////////////////////////////
// Covert a sub binary-search-tree into a sorted double-linked list
// Input: pNode -           the head of the sub tree
//        pLastNodeInList - the tail of the double-linked list
///////////////////////////////////////////////////////////////////////
void ConvertNode(BSTreeNode* pNode, BSTreeNode*& pLastNodeInList)
{
      if(pNode == NULL)
            return;

      BSTreeNode *pCurrent = pNode;

      // Convert the left sub-tree
      if (pCurrent->m_pLeft != NULL)
            ConvertNode(pCurrent->m_pLeft, pLastNodeInList);

      // Put the current node into the double-linked list
      pCurrent->m_pLeft = pLastNodeInList;
      if(pLastNodeInList != NULL)
            pLastNodeInList->m_pRight = pCurrent;

      pLastNodeInList = pCurrent;

      // Convert the right sub-tree
      if (pCurrent->m_pRight != NULL)
            ConvertNode(pCurrent->m_pRight, pLastNodeInList);
}

///////////////////////////////////////////////////////////////////////
// Covert a binary search tree into a sorted double-linked list
// Input: pHeadOfTree - the head of tree
// Output: the head of sorted double-linked list
///////////////////////////////////////////////////////////////////////
BSTreeNode* Convert_Solution1(BSTreeNode* pHeadOfTree)
{
      BSTreeNode *pLastNodeInList = NULL;
      ConvertNode(pHeadOfTree, pLastNodeInList);

      // Get the head of the double-linked list
      BSTreeNode *pHeadOfList = pLastNodeInList;
      while(pHeadOfList && pHeadOfList->m_pLeft)
            pHeadOfList = pHeadOfList->m_pLeft;

      return pHeadOfList;
}

 

程序員面試題精選(02)-設計包含min函數的棧
題目:定義棧的數據結構,要求添加一個min函數,能夠得到棧的最小元素。要求函數min、push以及pop的時間複雜度都是O(1)。 分析:這是去年google的一道面試題。
我看到這道題目時,第一反應就是每次push一個新元素時,將棧裏所有逆序元素排序。這樣棧頂元素將是最小元素。但由於不能保證最後push進棧的元素最先出棧,這種思路設計的數據結構已經不是一個棧了。
在棧裏添加一個成員變量存放最小元素(或最小元素的位置)。每次push一個新元素進棧的時候,如果該元素比當前的最小元素還要小,則更新最小元素。
乍一看這樣思路挺好的。但仔細一想,該思路存在一個重要的問題:如果當前最小元素被pop出去,如何才能得到下一個最小元素?
因此僅僅只添加一個成員變量存放最小元素(或最小元素的位置)是不夠的。我們需要一個輔助棧。每次push一個新元素的時候,同時將最小元素(或最小元素的位置。考慮到棧元素的類型可能是複雜的數據結構,用最小元素的位置將能減少空間消耗)push到輔助棧中;每次pop一個元素出棧的時候,同時pop輔助棧。
參考代碼:
#include <deque>
#include <assert.h>
template <typename T> class CStackWithMin
{
public:
      CStackWithMin(void) {}
      virtual ~CStackWithMin(void) {}
      T& top(void);
      const T& top(void) const;

      void push(const T& value);
      void pop(void);

      const T& min(void) const;

private:
     T> m_data;               // the elements of stack
     size_t> m_minIndex;      // the indices of minimum elements
};

// get the last element of mutable stack
template <typename T> T& CStackWithMin<T>::top()
{
      return m_data.back();
}

// get the last element of non-mutable stack
template <typename T> const T& CStackWithMin<T>::top() const
{
      return m_data.back();
}

// insert an elment at the end of stack
template <typename T> void CStackWithMin<T>::push(const T& value)
{
      // append the data into the end of m_data
      m_data.push_back(value);

      // set the index of minimum elment in m_data at the end of m_minIndex
      if(m_minIndex.size() == 0)
            m_minIndex.push_back(0);
      else
      {
            if(value < m_data[m_minIndex.back()])
                  m_minIndex.push_back(m_data.size() - 1);
            else
                  m_minIndex.push_back(m_minIndex.back());
      }
}

// erease the element at the end of stack
template <typename T> void CStackWithMin<T>::pop()
{
      // pop m_data
      m_data.pop_back();
      // pop m_minIndex
      m_minIndex.pop_back();
}

// get the minimum element of stack
template <typename T> const T& CStackWithMin<T>::min() const
{
      assert(m_data.size() > 0);
      assert(m_minIndex.size() > 0);
      return m_data[m_minIndex.back()];
}
舉個例子演示上述代碼的運行過程:
  步驟              數據棧            輔助棧                最小值
1.push 3    3          0             3
2.push 4    3,4        0,0           3
3.push 2    3,4,2      0,0,2         2
4.push 1    3,4,2,1    0,0,2,3       1
5.pop       3,4,2      0,0,2         2
6.pop       3,4        0,0           3
7.push 0    3,4,0      0,0,2         0
討論:如果思路正確,編寫上述代碼不是一件很難的事情。但如果能注意一些細節無疑能在面試中加分。比如我在上面的代碼中做了如下的工作:
??????????用模板類實現。如果別人的元素類型只是int類型,模板將能給面試官帶來好印象;
??????????兩個版本的top函數。在很多類中,都需要提供const和非const版本的成員訪問函數;
??????????min函數中assert。把代碼寫的儘量安全是每個軟件公司對程序員的要求;
??????????添加一些註釋。註釋既能提高代碼的可讀性,又能增加代碼量,何樂而不爲?
總之,在面試時如果時間允許,儘量把代碼寫的漂亮一些。說不定代碼中的幾個小亮點就能讓自己輕鬆拿到心儀的Offer。

 

 

程序員面試題精選(03)-求子數組的最大和
題目:輸入一個整形數組,數組裏有正數也有負數。數組中連續的一個或多個整數組成一個子數組,每個子數組都有一個和。求所有子數組的和的最大值。要求時間複雜度爲O(n)。
例如輸入的數組爲1, -2, 3, 10, -4, 7, 2, -5,和最大的子數組爲3, 10, -4, 7, 2,因此輸出爲該子數組的和18。
分析:本題最初爲2005年浙江大學計算機系的考研題的最後一道程序設計題,在2006年裏包括google在內的很多知名公司都把本題當作面試題。由於本題在網絡中廣爲流傳,本題也順利成爲2006年程序員面試題中經典中的經典。
如果不考慮時間複雜度,我們可以枚舉出所有子數組並求出他們的和。不過非常遺憾的是,由於長度爲n的數組有O(n2)個子數組;而且求一個長度爲n的數組的和的時間複雜度爲O(n)。因此這種思路的時間是O(n3)。
很容易理解,當我們加上一個正數時,和會增加;當我們加上一個負數時,和會減少。如果當前得到的和是個負數,那麼這個和在接下來的累加中應該拋棄並重新清零,不然的話這個負數將會減少接下來的和。基於這樣的思路,我們可以寫出如下代碼。
參考代碼:
/////////////////////////////////////////////////////////////////////////////
// Find the greatest sum of all sub-arrays
// Return value: if the input is valid, return true, otherwise return false
/////////////////////////////////////////////////////////////////////////////
bool FindGreatestSumOfSubArray
(
      int *pData,           // an array
      unsigned int nLength, // the length of array
      int &nGreatestSum     // the greatest sum of all sub-arrays
)
{
      // if the input is invalid, return false
      if((pData == NULL) || (nLength == 0))
            return false;
      int nCurSum = nGreatestSum = 0;
      for(unsigned int i = 0; i < nLength; ++i)
      {
            nCurSum += pData;
            // if the current sum is negative, discard it
            if(nCurSum < 0)
                  nCurSum = 0;

            // if a greater sum is found, update the greatest sum
            if(nCurSum > nGreatestSum)
                  nGreatestSum = nCurSum;
      }
      // if all data are negative, find the greatest element in the array
      if(nGreatestSum == 0)
      {
            nGreatestSum = pData[0];
            for(unsigned int i = 1; i < nLength; ++i)
            {
                  if(pData > nGreatestSum)
                        nGreatestSum = pData;
            }
      }

      return true;
}

討論:上述代碼中有兩點值得和大家討論一下:
??????????函數的返回值不是子數組和的最大值,而是一個判斷輸入是否有效的標誌。如果函數返回值的是子數組和的最大值,那麼當輸入一個空指針是應該返回什麼呢?返回0?那這個函數的用戶怎麼區分輸入無效和子數組和的最大值剛好是0這兩中情況呢?基於這個考慮,本人認爲把子數組和的最大值以引用的方式放到參數列表中,同時讓函數返回一個函數是否正常執行的標誌。
??????????輸入有一類特殊情況需要特殊處理。當輸入數組中所有整數都是負數時,子數組和的最大值就是數組中的最大元素。

 


程序員面試題精選(04)-在二元樹中找出和爲某一值的所有路徑
題目:輸入一個整數和一棵二元樹。從樹的根結點開始往下訪問一直到葉結點所經過的所有結點形成一條路徑。打印出和與輸入整數相等的所有路徑。
例如輸入整數22和如下二元樹
                                            10
                                           /   /
                                          5     12
                                        /   /  
                                      4     7 
則打印出兩條路徑:10, 12和10, 5, 7。
二元樹結點的數據結構定義爲:
struct BinaryTreeNode // a node in the binary tree
{
      int              m_nValue; // value of node
      BinaryTreeNode  *m_pLeft;  // left child of node
      BinaryTreeNode  *m_pRight; // right child of node
};
分析:這是百度的一道筆試題,考查對樹這種基本數據結構以及遞歸函數的理解。
當訪問到某一結點時,把該結點添加到路徑上,並累加當前結點的值。如果當前結點爲葉結點並且當前路徑的和剛好等於輸入的整數,則當前的路徑符合要求,我們把它打印出來。如果當前結點不是葉結點,則繼續訪問它的子結點。當前結點訪問結束後,遞歸函數將自動回到父結點。因此我們在函數退出之前要在路徑上刪除當前結點並減去當前結點的值,以確保返回父結點時路徑剛好是根結點到父結點的路徑。我們不難看出保存路徑的數據結構實際上是一個棧結構,因爲路徑要與遞歸調用狀態一致,而遞歸調用本質就是一個壓棧和出棧的過程。
參考代碼:
///////////////////////////////////////////////////////////////////////
// Find paths whose sum equal to expected sum
///////////////////////////////////////////////////////////////////////
void FindPath
(
      BinaryTreeNode*   pTreeNode,    // a node of binary tree
      int               expectedSum,  // the expected sum
      std::vector<int>& path,         // a path from root to current node
      int&              currentSum    // the sum of path
)
{
      if(!pTreeNode)
            return;
      currentSum += pTreeNode->m_nValue;
      path.push_back(pTreeNode->m_nValue);
      // if the node is a leaf, and the sum is same as pre-defined,
      // the path is what we want. print the path
      bool isLeaf = (!pTreeNode->m_pLeft && !pTreeNode->m_pRight);
      if(currentSum == expectedSum && isLeaf)
      {   
           std::vector<int>::iterator iter = path.begin();
           for(; iter != path.end(); ++ iter)
                 std::cout << *iter << '/t';
           std::cout << std::endl;
      }

      // if the node is not a leaf, goto its children
      if(pTreeNode->m_pLeft)
            FindPath(pTreeNode->m_pLeft, expectedSum, path, currentSum);
      if(pTreeNode->m_pRight)
            FindPath(pTreeNode->m_pRight, expectedSum, path, currentSum);

      // when we finish visiting a node and return to its parent node,
      // we should delete this node from the path and
      // minus the node's value from the current sum
      currentSum -= pTreeNode->m_nValue;
      path.pop_back();
}

 


程序員面試題精選(05)-查找最小的k個元素
題目:輸入n個整數,輸出其中最小的k個。
例如輸入1,2,3,4,5,6,7和8這8個數字,則最小的4個數字爲1,2,3和4。
分析:這道題最簡單的思路莫過於把輸入的n個整數排序,這樣排在最前面的k個數就是最小的k個數。只是這種思路的時間複雜度爲O(nlogn)。我們試着尋找更快的解決思路。
我們可以開闢一個長度爲k的數組。每次從輸入的n個整數中讀入一個數。如果數組中已經插入的元素少於k個,則將讀入的整數直接放到數組中。否則長度爲k的數組已經滿了,不能再往數組裏插入元素,只能替換了。如果讀入的這個整數比數組中已有k個整數的最大值要小,則用讀入的這個整數替換這個最大值;如果讀入的整數比數組中已有k個整數的最大值還要大,則讀入的這個整數不可能是最小的k個整數之一,拋棄這個整數。這種思路相當於只要排序k個整數,因此時間複雜可以降到O(n+nlogk)。通常情況下k要遠小於n,所以這種辦法要優於前面的思路。
這是我能夠想出來的最快的解決方案。不過從給面試官留下更好印象的角度出發,我們可以進一步把代碼寫得更漂亮一些。從上面的分析,當長度爲k的數組已經滿了之後,如果需要替換,每次替換的都是數組中的最大值。在常用的數據結構中,能夠在O(1)時間裏得到最大值的數據結構爲最大堆。因此我們可以用堆(heap)來代替數組。
另外,自己重頭開始寫一個最大堆需要一定量的代碼。我們現在不需要重新去發明車輪,因爲前人早就發明出來了。同樣,STL中的set和multiset爲我們做了很好的堆的實現,我們可以拿過來用。既偷了懶,又給面試官留下熟悉STL的好印象,何樂而不爲之?
參考代碼:
#include <set>
#include <vector>
#include <iostream>
using namespace std;
typedef multiset<int, greater<int> >  IntHeap;
///////////////////////////////////////////////////////////////////////
// find k least numbers in a vector
///////////////////////////////////////////////////////////////////////
void FindKLeastNumbers
(
      const vector<int>& data,               // a vector of data
      IntHeap& leastNumbers,                 // k least numbers, output
      unsigned int k                             
)
{
      leastNumbers.clear();

      if(k == 0 || data.size() < k)
            return;
      vector<int>::const_iterator iter = data.begin();
      for(; iter != data.end(); ++ iter)
      {
            // if less than k numbers was inserted into leastNumbers
            if((leastNumbers.size()) < k)
                  leastNumbers.insert(*iter);

            // leastNumbers contains k numbers and it's full now
            else
            {
                  // first number in leastNumbers is the greatest one
                  IntHeap::iterator iterFirst = leastNumbers.begin();
                  // if is less than the previous greatest number
                  if(*iter < *(leastNumbers.begin()))
                  {
                        // replace the previous greatest number
                        leastNumbers.erase(iterFirst);
                        leastNumbers.insert(*iter);
                  }
            }
      }
}

 


程序員面試題精選(06)-判斷整數序列是不是二元查找樹的後序遍歷結果
題目:輸入一個整數數組,判斷該數組是不是某二元查找樹的後序遍歷的結果。如果是返回true,否則返回false。 例如輸入5、7、6、9、11、10、8,由於這一整數序列是如下樹的後序遍歷結果:
         8
       /  /
      6    10
    / /    / /
   5   7   9  11
因此返回true。
如果輸入7、4、6、5,沒有哪棵樹的後序遍歷的結果是這個序列,因此返回false。
分析:這是一道trilogy的筆試題,主要考查對二元查找樹的理解。
在後續遍歷得到的序列中,最後一個元素爲樹的根結點。從頭開始掃描這個序列,比根結點小的元素都應該位於序列的左半部分;從第一個大於跟結點開始到跟結點前面的一個元素爲止,所有元素都應該大於跟結點,因爲這部分元素對應的是樹的右子樹。根據這樣的劃分,把序列劃分爲左右兩部分,我們遞歸地確認序列的左、右兩部分是不是都是二元查找樹。
參考代碼:
using namespace std;
///////////////////////////////////////////////////////////////////////
// Verify whether a squence of integers are the post order traversal
// of a binary search tree (BST)
// Input: squence - the squence of integers
//        length  - the length of squence
// Return: return ture if the squence is traversal result of a BST,
//         otherwise, return false
///////////////////////////////////////////////////////////////////////
bool verifySquenceOfBST(int squence[], int length)
{
      if(squence == NULL || length <= 0)
            return false;
      // root of a BST is at the end of post order traversal squence
      int root = squence[length - 1];
      // the nodes in left sub-tree are less than the root
      int i = 0;
      for(; i < length - 1; ++ i)
      {
            if(squence > root)
                  break;
      }

      // the nodes in the right sub-tree are greater than the root
      int j = i;
      for(; j < length - 1; ++ j)
      {
            if(squence[j] < root)
                  return false;
      }

      // verify whether the left sub-tree is a BST
      bool left = true;
      if(i > 0)
            left = verifySquenceOfBST(squence, i);

      // verify whether the right sub-tree is a BST
      bool right = true;
      if(i < length - 1)
            right = verifySquenceOfBST(squence + i, length - i - 1);

      return (left && right);
}

 


程序員面試題精選(07)-翻轉句子中單詞的順序
題目:輸入一個英文句子,翻轉句子中單詞的順序,但單詞內字符的順序不變。句子中單詞以空格符隔開。爲簡單起見,標點符號和普通字母一樣處理。
例如輸入“I am a student.”,則輸出“student. a am I”。
分析:由於編寫字符串相關代碼能夠反映程序員的編程能力和編程習慣,與字符串相關的問題一直是程序員筆試、面試題的熱門題目。本題也曾多次受到包括微軟在內的大量公司的青睞。
由於本題需要翻轉句子,我們先顛倒句子中的所有字符。這時,不但翻轉了句子中單詞的順序,而且單詞內字符也被翻轉了。我們再顛倒每個單詞內的字符。由於單詞內的字符被翻轉兩次,因此順序仍然和輸入時的順序保持一致。
還是以上面的輸入爲例子。翻轉“I am a student.”中所有字符得到“.tneduts a ma I”,再翻轉每個單詞中字符的順序得到“students. a am I”,正是符合要求的輸出。
參考代碼:
///////////////////////////////////////////////////////////////////////
// Reverse a string between two pointers
// Input: pBegin - the begin pointer in a string
//        pEnd   - the end pointer in a string
///////////////////////////////////////////////////////////////////////
void Reverse(char *pBegin, char *pEnd)
{
      if(pBegin == NULL || pEnd == NULL)
            return;
      while(pBegin < pEnd)
      {
            char temp = *pBegin;
            *pBegin = *pEnd;
            *pEnd = temp;
            pBegin ++, pEnd --;
      }
}
///////////////////////////////////////////////////////////////////////
// Reverse the word order in a sentence, but maintain the character
// order inside a word
// Input: pData - the sentence to be reversed
///////////////////////////////////////////////////////////////////////
char* ReverseSentence(char *pData)
{
      if(pData == NULL)
            return NULL;

      char *pBegin = pData;
      char *pEnd = pData;
      while(*pEnd != '/0')
            pEnd ++;
      pEnd--;

      // Reverse the whole sentence
      Reverse(pBegin, pEnd);

      // Reverse every word in the sentence
      pBegin = pEnd = pData;
      while(*pBegin != '/0')
      {
            if(*pBegin == ' ')
            {
                  pBegin ++;
                  pEnd ++;
                  continue;
            }
            // A word is between with pBegin and pEnd, reverse it
            else if(*pEnd == ' ' || *pEnd == '/0')
            {
                  Reverse(pBegin, --pEnd);
                  pBegin = ++pEnd;
            }
            else
            {
                  pEnd ++;
            }
      }
      return pData;
}


程序員面試題精選(08)-求1+2+...+n
題目:求1+2+…+n,要求不能使用乘除法、for、while、if、else、switch、case等關鍵字以及條件判斷語句(A?B:C)。
分析:這道題沒有多少實際意義,因爲在軟件開發中不會有這麼變態的限制。但這道題卻能有效地考查發散思維能力,而發散思維能力能反映出對編程相關技術理解的深刻程度。
通常求1+2+…+n除了用公式n(n+1)/2之外,無外乎循環和遞歸兩種思路。由於已經明確限制for和while的使用,循環已經不能再用了。同樣,遞歸函數也需要用if語句或者條件判斷語句來判斷是繼續遞歸下去還是終止遞歸,但現在題目已經不允許使用這兩種語句了。
我們仍然圍繞循環做文章。循環只是讓相同的代碼執行n遍而已,我們完全可以不用for和while達到這個效果。比如定義一個類,我們new一含有n個這種類型元素的數組,那麼該類的構造函數將確定會被調用n次。我們可以將需要執行的代碼放到構造函數裏。如下代碼正是基於這個思路:
class Temp
{
public:
      Temp() { ++ N; Sum += N; }
      static void Reset() { N = 0; Sum = 0; }
      static int GetSum() { return Sum; }
private:
      static int N;
      static int Sum;
};

int Temp::N = 0;
int Temp::Sum = 0;
int solution1_Sum(int n)
{
      Temp::Reset();

      Temp *a = new Temp[n];
      delete []a;
      a = 0;

      return Temp::GetSum();
}
我們同樣也可以圍繞遞歸做文章。既然不能判斷是不是應該終止遞歸,我們不妨定義兩個函數。一個函數充當遞歸函數的角色,另一個函數處理終止遞歸的情況,我們需要做的就是在兩個函數裏二選一。從二選一我們很自然的想到布爾變量,比如ture(1)的時候調用第一個函數,false(0)的時候調用第二個函數。那現在的問題是如和把數值變量n轉換成布爾值。如果對n連續做兩次反運算,即!!n,那麼非零的n轉換爲true,0轉換爲false。有了上述分析,我們再來看下面的代碼:
class A;
A* Array[2];

class A
{
public:
      virtual int Sum (int n) { return 0; }
};
class B: public A
{
public:
      virtual int Sum (int n) { return Array[!!n]->Sum(n-1)+n; }
};
int solution2_Sum(int n)
{
      A a;
      B b;
      Array[0] = &a;
      Array[1] = &b;

      int value = Array[1]->Sum(n);
      return value;
}
這種方法是用虛函數來實現函數的選擇。當n不爲零時,執行函數B::Sum;當n爲0時,執行A::Sum。我們也可以直接用函數指針數組,這樣可能還更直接一些:
typedef int (*fun)(int);
int solution3_f1(int i)
{
      return 0;
}
int solution3_f2(int i)
{
      fun f[2]={solution3_f1, solution3_f2};
      return i+f[!!i](i-1);
}
另外我們還可以讓編譯器幫我們來完成類似於遞歸的運算,比如如下代碼:
template <int n> struct solution4_Sum
{
      enum Value { N = solution4_Sum<n - 1>::N + n};
};

template <> struct solution4_Sum<1>
{
      enum Value { N = 1};
};
solution4_Sum<100>::N就是1+2+...+100的結果。當編譯器看到solution4_Sum<100>時,就是爲模板類solution4_Sum以參數100生成該類型的代碼。但以100爲參數的類型需要得到以99爲參數的類型,因爲solution4_Sum<100>::N=solution4_Sum<99>::N+100。這個過程會遞歸一直到參數爲1的類型,由於該類型已經顯式定義,編譯器無需生成,遞歸編譯到此結束。由於這個過程是在編譯過程中完成的,因此要求輸入n必須是在編譯期間就能確定,不能動態輸入。這是該方法最大的缺點。而且編譯器對遞歸編譯代碼的遞歸深度是有限制的,也就是要求n不能太大。
大家還有更多、更巧妙的思路嗎?歡迎討論^_^

 


程序員面試題精選(09)-查找鏈表中倒數第k個結點
題目:輸入一個單向鏈表,輸出該鏈表中倒數第k個結點。鏈表的倒數第0個結點爲鏈表的尾指針。鏈表結點定義如下: struct ListNode
{
      int       m_nKey;
      ListNode* m_pNext;
};
分析:爲了得到倒數第k個結點,很自然的想法是先走到鏈表的尾端,再從尾端回溯k步。可是輸入的是單向鏈表,只有從前往後的指針而沒有從後往前的指針。因此我們需要打開我們的思路。
既然不能從尾結點開始遍歷這個鏈表,我們還是把思路回到頭結點上來。假設整個鏈表有n個結點,那麼倒數第k個結點是從頭結點開始的第n-k-1個結點(從0開始計數)。如果我們能夠得到鏈表中結點的個數n,那我們只要從頭結點開始往後走n-k-1步就可以了。如何得到結點數n?這個不難,只需要從頭開始遍歷鏈表,每經過一個結點,計數器加一就行了。
這種思路的時間複雜度是O(n),但需要遍歷鏈表兩次。第一次得到鏈表中結點個數n,第二次得到從頭結點開始的第n?-k-1個結點即倒數第k個結點。
如果鏈表的結點數不多,這是一種很好的方法。但如果輸入的鏈表的結點個數很多,有可能不能一次性把整個鏈表都從硬盤讀入物理內存,那麼遍歷兩遍意味着一個結點需要兩次從硬盤讀入到物理內存。我們知道把數據從硬盤讀入到內存是非常耗時間的操作。我們能不能把鏈表遍歷的次數減少到1?如果可以,將能有效地提高代碼執行的時間效率。
如果我們在遍歷時維持兩個指針,第一個指針從鏈表的頭指針開始遍歷,在第k-1步之前,第二個指針保持不動;在第k-1步開始,第二個指針也開始從鏈表的頭指針開始遍歷。由於兩個指針的距離保持在k-1,當第一個(走在前面的)指針到達鏈表的尾結點時,第二個指針(走在後面的)指針正好是倒數第k個結點。
這種思路只需要遍歷鏈表一次。對於很長的鏈表,只需要把每個結點從硬盤導入到內存一次。因此這一方法的時間效率前面的方法要高。
思路一的參考代碼:
///////////////////////////////////////////////////////////////////////
// Find the kth node from the tail of a list
// Input: pListHead - the head of list
//        k         - the distance to the tail
// Output: the kth node from the tail of a list
///////////////////////////////////////////////////////////////////////
ListNode* FindKthToTail_Solution1(ListNode* pListHead, unsigned int k)
{
      if(pListHead == NULL)
            return NULL;

      // count the nodes number in the list
      ListNode *pCur = pListHead;
      unsigned int nNum = 0;
      while(pCur->m_pNext != NULL)
      {
            pCur = pCur->m_pNext;
            nNum ++;
      }

      // if the number of nodes in the list is less than k
      // do nothing
      if(nNum < k)
            return NULL;

      // the kth node from the tail of a list
      // is the (n - k)th node from the head
      pCur = pListHead;
      for(unsigned int i = 0; i < nNum - k; ++ i)
            pCur = pCur->m_pNext;
      return pCur;
}
思路二的參考代碼:
///////////////////////////////////////////////////////////////////////
// Find the kth node from the tail of a list
// Input: pListHead - the head of list
//        k         - the distance to the tail
// Output: the kth node from the tail of a list
///////////////////////////////////////////////////////////////////////
ListNode* FindKthToTail_Solution2(ListNode* pListHead, unsigned int k)
{
      if(pListHead == NULL)
            return NULL;

      ListNode *pAhead = pListHead;
      ListNode *pBehind = NULL;
      for(unsigned int i = 0; i < k; ++ i)
      {
            if(pAhead->m_pNext != NULL)
                  pAhead = pAhead->m_pNext;
            else
            {
                  // if the number of nodes in the list is less than k,
                  // do nothing
                  return NULL;
            }
      }
      pBehind = pListHead;

      // the distance between pAhead and pBehind is k
      // when pAhead arrives at the tail, p
      // Behind is at the kth node from the tail
      while(pAhead->m_pNext != NULL)
      {
            pAhead = pAhead->m_pNext;
            pBehind = pBehind->m_pNext;
      }

      return pBehind;
}
討論:這道題的代碼有大量的指針操作。在軟件開發中,錯誤的指針操作是大部分問題的根源。因此每個公司都希望程序員在操作指針時有良好的習慣,比如使用指針之前判斷是不是空指針。這些都是編程的細節,但如果這些細節把握得不好,很有可能就會和心儀的公司失之交臂。
另外,這兩種思路對應的代碼都含有循環。含有循環的代碼經常出的問題是在循環結束條件的判斷。是該用小於還是小於等於?是該用k還是該用k-1?由於題目要求的是從0開始計數,而我們的習慣思維是從1開始計數,因此首先要想好這些邊界條件再開始編寫代碼,再者要在編寫完代碼之後再用邊界值、邊界值減1、邊界值加1都運行一次(在紙上寫代碼就只能在心裏運行了)。
擴展:和這道題類似的題目還有:輸入一個單向鏈表。如果該鏈表的結點數爲奇數,輸出中間的結點;如果鏈表結點數爲偶數,輸出中間兩個結點前面的一個。如果各位感興趣,請自己分析並編寫代碼。

 


程序員面試題精選(10)-在排序數組中查找和爲給定值的兩個數字
題目:輸入一個已經按升序排序過的數組和一個數字,在數組中查找兩個數,使得它們的和正好是輸入的那個數字。要求時間複雜度是O(n)。如果有多對數字的和等於輸入的數字,輸出任意一對即可。
例如輸入數組1、2、4、7、11、15和數字15。由於4+11=15,因此輸出4和11。
分析:如果我們不考慮時間複雜度,最簡單想法的莫過去先在數組中固定一個數字,再依次判斷數組中剩下的n-1個數字與它的和是不是等於輸入的數字。可惜這種思路需要的時間複雜度是O(n2)。
我們假設現在隨便在數組中找到兩個數。如果它們的和等於輸入的數字,那太好了,我們找到了要找的兩個數字;如果小於輸入的數字呢?我們希望兩個數字的和再大一點。由於數組已經排好序了,我們是不是可以把較小的數字的往後面移動一個數字?因爲排在後面的數字要大一些,那麼兩個數字的和也要大一些,就有可能等於輸入的數字了;同樣,當兩個數字的和大於輸入的數字的時候,我們把較大的數字往前移動,因爲排在數組前面的數字要小一些,它們的和就有可能等於輸入的數字了。
我們把前面的思路整理一下:最初我們找到數組的第一個數字和最後一個數字。當兩個數字的和大於輸入的數字時,把較大的數字往前移動;當兩個數字的和小於數字時,把較小的數字往後移動;當相等時,打完收工。這樣掃描的順序是從數組的兩端向數組的中間掃描。
問題是這樣的思路是不是正確的呢?這需要嚴格的數學證明。感興趣的讀者可以自行證明一下。
參考代碼:
///////////////////////////////////////////////////////////////////////
// Find two numbers with a sum in a sorted array
// Output: ture is found such two numbers, otherwise false
///////////////////////////////////////////////////////////////////////
bool FindTwoNumbersWithSum
(
      int data[],           // a sorted array
      unsigned int length,  // the length of the sorted array    
      int sum,              // the sum
      int& num1,            // the first number, output
      int& num2             // the second number, output
)
{

      bool found = false;
      if(length < 1)
            return found;

      int ahead = length - 1;
      int behind = 0;

      while(ahead > behind)
      {
            long long curSum = data[ahead] + data[behind];

            // if the sum of two numbers is equal to the input
            // we have found them
            if(curSum == sum)
            {
                  num1 = data[behind];
                  num2 = data[ahead];
                  found = true;
                  break;
            }
            // if the sum of two numbers is greater than the input
            // decrease the greater number
            else if(curSum > sum)
                  ahead --;
            // if the sum of two numbers is less than the input
            // increase the less number
            else
                  behind ++;
      }

      return found;
}
擴展:如果輸入的數組是沒有排序的,但知道里面數字的範圍,其他條件不變,如和在O(n)時間裏找到這兩個數字?

 


程序員面試題精選(11)-求二元查找樹的鏡像
題目:輸入一顆二元查找樹,將該樹轉換爲它的鏡像,即在轉換後的二元查找樹中,左子樹的結點都大於右子樹的結點。用遞歸和循環兩種方法完成樹的鏡像轉換。 例如輸入:
     8
    /  /
  6      10
//       //
5  7    9   11
輸出:
      8
    /  /
  10    6
//      //
11  9  7  5
定義二元查找樹的結點爲:
struct BSTreeNode // a node in the binary search tree (BST)
{
      int          m_nValue; // value of node
      BSTreeNode  *m_pLeft;  // left child of node
      BSTreeNode  *m_pRight; // right child of node
};
分析:儘管我們可能一下子不能理解鏡像是什麼意思,但上面的例子給我們的直觀感覺,就是交換結點的左右子樹。我們試着在遍歷例子中的二元查找樹的同時來交換每個結點的左右子樹。遍歷時首先訪問頭結點8,我們交換它的左右子樹得到:
      8
    /  /
  10    6
//      //
9  11  5  7
我們發現兩個結點6和10的左右子樹仍然是左結點的值小於右結點的值,我們再試着交換他們的左右子樹,得到:
      8
    /  /
  10    6
//      //
11  9 7   5
剛好就是要求的輸出。
上面的分析印證了我們的直覺:在遍歷二元查找樹時每訪問到一個結點,交換它的左右子樹。這種思路用遞歸不難實現,將遍歷二元查找樹的代碼稍作修改就可以了。參考代碼如下:
///////////////////////////////////////////////////////////////////////
// Mirror a BST (swap the left right child of each node) recursively
// the head of BST in initial call
///////////////////////////////////////////////////////////////////////
void MirrorRecursively(BSTreeNode *pNode)
{
      if(!pNode)
            return;

      // swap the right and left child sub-tree
      BSTreeNode *pTemp = pNode->m_pLeft;
      pNode->m_pLeft = pNode->m_pRight;
      pNode->m_pRight = pTemp;

      // mirror left child sub-tree if not null
      if(pNode->m_pLeft)
            MirrorRecursively(pNode->m_pLeft); 

      // mirror right child sub-tree if not null
      if(pNode->m_pRight)
            MirrorRecursively(pNode->m_pRight);
}
由於遞歸的本質是編譯器生成了一個函數調用的棧,因此用循環來完成同樣任務時最簡單的辦法就是用一個輔助棧來模擬遞歸。首先我們把樹的頭結點放入棧中。在循環中,只要棧不爲空,彈出棧的棧頂結點,交換它的左右子樹。如果它有左子樹,把它的左子樹壓入棧中;如果它有右子樹,把它的右子樹壓入棧中。這樣在下次循環中就能交換它兒子結點的左右子樹了。參考代碼如下:
///////////////////////////////////////////////////////////////////////
// Mirror a BST (swap the left right child of each node) Iteratively
// Input: pTreeHead: the head of BST
///////////////////////////////////////////////////////////////////////
void MirrorIteratively(BSTreeNode *pTreeHead)
{
      if(!pTreeHead)
            return;

      std::stack<BSTreeNode *> stackTreeNode;
      stackTreeNode.push(pTreeHead);

      while(stackTreeNode.size())
      {
            BSTreeNode *pNode = stackTreeNode.top();
            stackTreeNode.pop();

            // swap the right and left child sub-tree
            BSTreeNode *pTemp = pNode->m_pLeft;
            pNode->m_pLeft = pNode->m_pRight;
            pNode->m_pRight = pTemp;

            // push left child sub-tree into stack if not null
            if(pNode->m_pLeft)
                  stackTreeNode.push(pNode->m_pLeft);

            // push right child sub-tree into stack if not null
            if(pNode->m_pRight)
                  stackTreeNode.push(pNode->m_pRight);
      }
}

 


程序員面試題精選(12)-從上往下遍歷二元樹
題目:輸入一顆二元樹,從上往下按層打印樹的每個結點,同一層中按照從左往右的順序打印。 例如輸入
      8
    /  /
   6    10
  //     //
5  7   9  11
輸出8   6   10   5   7   9   11。
分析:這曾是微軟的一道面試題。這道題實質上是要求遍歷一棵二元樹,只不過不是我們熟悉的前序、中序或者後序遍歷。
我們從樹的根結點開始分析。自然先應該打印根結點8,同時爲了下次能夠打印8的兩個子結點,我們應該在遍歷到8時把子結點6和10保存到一個數據容器中。現在數據容器中就有兩個元素6 和10了。按照從左往右的要求,我們先取出6訪問。打印6的同時要把6的兩個子結點5和7放入數據容器中,此時數據容器中有三個元素10、5和7。接下來我們應該從數據容器中取出結點10訪問了。注意10比5和7先放入容器,此時又比5和7先取出,就是我們通常說的先入先出。因此不難看出這個數據容器的類型應該是個隊列。
既然已經確定數據容器是一個隊列,現在的問題變成怎麼實現隊列了。實際上我們無需自己動手實現一個,因爲STL已經爲我們實現了一個很好的deque(兩端都可以進出的隊列),我們只需要拿過來用就可以了。
我們知道樹是圖的一種特殊退化形式。同時如果對圖的深度優先遍歷和廣度優先遍歷有比較深刻的理解,將不難看出這種遍歷方式實際上是一種廣度優先遍歷。因此這道題的本質是在二元樹上實現廣度優先遍歷。
參考代碼:
#include <deque>
#include <iostream>
using namespace std;

struct BTreeNode // a node in the binary tree
{
      int         m_nValue; // value of node
      BTreeNode  *m_pLeft;  // left child of node
      BTreeNode  *m_pRight; // right child of node
};

///////////////////////////////////////////////////////////////////////
// Print a binary tree from top level to bottom level
// Input: pTreeRoot - the root of binary tree
///////////////////////////////////////////////////////////////////////
void PrintFromTopToBottom(BTreeNode *pTreeRoot)
{
      if(!pTreeRoot)
            return;

      // get a empty queue
      deque<BTreeNode *> dequeTreeNode;

      // insert the root at the tail of queue
      dequeTreeNode.push_back(pTreeRoot);
      while(dequeTreeNode.size())
      {
            // get a node from the head of queue
            BTreeNode *pNode = dequeTreeNode.front();
            dequeTreeNode.pop_front();

            // print the node
            cout << pNode->m_nValue << ' ';

            // print its left child sub-tree if it has
            if(pNode->m_pLeft)
                  dequeTreeNode.push_back(pNode->m_pLeft);
            // print its right child sub-tree if it has
            if(pNode->m_pRight)
                  dequeTreeNode.push_back(pNode->m_pRight);
      }
}


程序員面試題精選(13)-第一個只出現一次的字符
題目:在一個字符串中找到第一個只出現一次的字符。如輸入abaccdeff,則輸出b。 分析:這道題是2006年google的一道筆試題。
看到這道題時,最直觀的想法是從頭開始掃描這個字符串中的每個字符。當訪問到某字符時拿這個字符和後面的每個字符相比較,如果在後面沒有發現重複的字符,則該字符就是隻出現一次的字符。如果字符串有n個字符,每個字符可能與後面的O(n)個字符相比較,因此這種思路時間複雜度是O(n2)。我們試着去找一個更快的方法。
由於題目與字符出現的次數相關,我們是不是可以統計每個字符在該字符串中出現的次數?要達到這個目的,我們需要一個數據容器來存放每個字符的出現次數。在這個數據容器中可以根據字符來查找它出現的次數,也就是說這個容器的作用是把一個字符映射成一個數字。在常用的數據容器中,哈希表正是這個用途。
哈希表是一種比較複雜的數據結構。由於比較複雜,STL中沒有實現哈希表,因此需要我們自己實現一個。但由於本題的特殊性,我們只需要一個非常簡單的哈希表就能滿足要求。由於字符(char)是一個長度爲8的數據類型,因此總共有可能256 種可能。於是我們創建一個長度爲256的數組,每個字母根據其ASCII碼值作爲數組的下標對應數組的對應項,而數組中存儲的是每個字符對應的次數。這樣我們就創建了一個大小爲256,以字符ASCII碼爲鍵值的哈希表。
我們第一遍掃描這個數組時,每碰到一個字符,在哈希表中找到對應的項並把出現的次數增加一次。這樣在進行第二次掃描時,就能直接從哈希表中得到每個字符出現的次數了。
參考代碼如下:
///////////////////////////////////////////////////////////////////////
// Find the first char which appears only once in a string
// Input: pString - the string
// Output: the first not repeating char if the string has, otherwise 0
///////////////////////////////////////////////////////////////////////
char FirstNotRepeatingChar(char* pString)
{
      // invalid input
      if(!pString)
            return 0;

      // get a hash table, and initialize it
      const int tableSize = 256;
      unsigned int hashTable[tableSize];
      for(unsigned int i = 0; i < tableSize; ++ i)
            hashTable = 0;

      // get the how many times each char appears in the string
      char* pHashKey = pString;
      while(*(pHashKey) != '/0')
            hashTable[*(pHashKey++)] ++;

      // find the first char which appears only once in a string
      pHashKey = pString;
      while(*pHashKey != '/0')
      {
            if(hashTable[*pHashKey] == 1)
                  return *pHashKey;

            pHashKey++;
      }

      // if the string is empty
      // or every char in the string appears at least twice
      return 0;
}

 


程序員面試題精選(14)-圓圈中最後剩下的數字
題目:n個數字(0,1,…,n-1)形成一個圓圈,從數字0開始,每次從這個圓圈中刪除第m個數字(第一個爲當前數字本身,第二個爲當前數字的下一個數字)。當一個數字刪除後,從被刪除數字的下一個繼續刪除第m個數字。求出在這個圓圈中剩下的最後一個數字。
分析:既然題目有一個數字圓圈,很自然的想法是我們用一個數據結構來模擬這個圓圈。在常用的數據結構中,我們很容易想到用環形列表。我們可以創建一個總共有m個數字的環形列表,然後每次從這個列表中刪除第m個元素。
在參考代碼中,我們用STL中std::list來模擬這個環形列表。由於list並不是一個環形的結構,因此每次跌代器掃描到列表末尾的時候,要記得把跌代器移到列表的頭部。這樣就是按照一個圓圈的順序來遍歷這個列表了。
這種思路需要一個有n個結點的環形列表來模擬這個刪除的過程,因此內存開銷爲O(n)。而且這種方法每刪除一個數字需要m步運算,總共有n個數字,因此總的時間複雜度是O(mn)。當m和n都很大的時候,這種方法是很慢的。
接下來我們試着從數學上分析出一些規律。首先定義最初的n個數字(0,1,…,n-1)中最後剩下的數字是關於n和m的方程爲f(n,m)。
在這n個數字中,第一個被刪除的數字是m%n-1,爲簡單起見記爲k。那麼刪除k之後的剩下n-1的數字爲0,1,…,k-1,k+1,…,n-1,並且下一個開始計數的數字是k+1。相當於在剩下的序列中,k+1排到最前面,從而形成序列k+1,…,n-1,0,…k-1。該序列最後剩下的數字也應該是關於n和m的函數。由於這個序列的規律和前面最初的序列不一樣(最初的序列是從0開始的連續序列),因此該函數不同於前面函數,記爲f’(n-1,m)。最初序列最後剩下的數字f(n,m)一定是剩下序列的最後剩下數字f’(n-1,m),所以f(n,m)=f’(n-1,m)。
接下來我們把剩下的的這n-1個數字的序列k+1,…,n-1,0,…k-1作一個映射,映射的結果是形成一個從0到n-2的序列:
k+1    ->    0
k+2    ->    1

n-1    ->    n-k-2
0   ->    n-k-1

k-1   ->   n-2
把映射定義爲p,則p(x)= (x-k-1)%n,即如果映射前的數字是x,則映射後的數字是(x-k-1)%n。對應的逆映射是p-1(x)=(x+k+1)%n。
由於映射之後的序列和最初的序列有同樣的形式,都是從0開始的連續序列,因此仍然可以用函數f來表示,記爲f(n-1,m)。根據我們的映射規則,映射之前的序列最後剩下的數字f’(n-1,m)= p-1 [f(n-1,m)]=[f(n-1,m)+k+1]%n。把k=m%n-1代入得到f(n,m)=f’(n-1,m)=[f(n-1,m)+m]%n。
經過上面複雜的分析,我們終於找到一個遞歸的公式。要得到n個數字的序列的最後剩下的數字,只需要得到n-1個數字的序列的最後剩下的數字,並可以依此類推。當n=1時,也就是序列中開始只有一個數字0,那麼很顯然最後剩下的數字就是0。我們把這種關係表示爲:
         0                  n=1
f(n,m)={
         [f(n-1,m)+m]%n     n>1
儘管得到這個公式的分析過程非常複雜,但它用遞歸或者循環都很容易實現。最重要的是,這是一種時間複雜度爲O(n),空間複雜度爲O(1)的方法,因此無論在時間上還是空間上都優於前面的思路。
思路一的參考代碼:
///////////////////////////////////////////////////////////////////////
// n integers (0, 1, ... n - 1) form a circle. Remove the mth from
// the circle at every time. Find the last number remaining
// Input: n - the number of integers in the circle initially
//        m - remove the mth number at every time
// Output: the last number remaining when the input is valid,
//         otherwise -1
///////////////////////////////////////////////////////////////////////
int LastRemaining_Solution1(unsigned int n, unsigned int m)
{
      // invalid input
      if(n < 1 || m < 1)
            return -1;
      unsigned int i = 0;
      // initiate a list with n integers (0, 1, ... n - 1)
      list<int> integers;
      for(i = 0; i < n; ++ i)
            integers.push_back(i);
      list<int>::iterator curinteger = integers.begin();
      while(integers.size() > 1)
      {
            // find the mth integer. Note that std::list is not a circle
            // so we should handle it manually
            for(int i = 1; i < m; ++ i)
            {
                  curinteger ++;
                  if(curinteger == integers.end())
                        curinteger = integers.begin();
            }

            // remove the mth integer. Note that std::list is not a circle
            // so we should handle it manually
            list<int>::iterator nextinteger = ++ curinteger;
            if(nextinteger == integers.end())
                  nextinteger = integers.begin();
            -- curinteger;
            integers.erase(curinteger);
            curinteger = nextinteger;
      }

      return *(curinteger);
}

思路二的參考代碼:
///////////////////////////////////////////////////////////////////////
// n integers (0, 1, ... n - 1) form a circle. Remove the mth from
// the circle at every time. Find the last number remaining
// Input: n - the number of integers in the circle initially
//        m - remove the mth number at every time
// Output: the last number remaining when the input is valid,
//         otherwise -1
///////////////////////////////////////////////////////////////////////
int LastRemaining_Solution2(int n, unsigned int m)
{
      // invalid input
      if(n <= 0 || m < 0)
            return -1;

      // if there are only one integer in the circle initially,
      // of course the last remaining one is 0
      int lastinteger = 0;

      // find the last remaining one in the circle with n integers
      for (int i = 2; i <= n; i ++)
            lastinteger = (lastinteger + m) % i;

      return lastinteger;
}

如果對兩種思路的時間複雜度感興趣的讀者可以把n和m的值設的稍微大一點,比如十萬這個數量級的數字,運行的時候就能明顯感覺出這兩種思路寫出來的代碼時間效率大不一樣。

 


程序員面試題精選(15)-含有指針成員的類的拷貝
題目:下面是一個數組類的聲明與實現。請分析這個類有什麼問題,並針對存在的問題提出幾種解決方案。
template<typename T> class Array
{
public:
      Array(unsigned arraySize):data(0), size(arraySize)
      {
            if(size > 0)
                  data = new T[size];
      }

      ~Array()
      {
            if(data) delete[] data;
      }

      void setValue(unsigned index, const T& value)
      {
            if(index < size)
                  data[index] = value;
      }

      T getValue(unsigned index) const
      {
            if(index < size)
                  return data[index];
            else
                  return T();
      }

private:
      T* data;
      unsigned size;
};
分析:我們注意在類的內部封裝了用來存儲數組數據的指針。軟件存在的大部分問題通常都可以歸結指針的不正確處理。
這個類只提供了一個構造函數,而沒有定義構造拷貝函數和重載拷貝運算符函數。當這個類的用戶按照下面的方式聲明並實例化該類的一個實例
Array A(10);
Array B(A);
或者按照下面的方式把該類的一個實例賦值給另外一個實例
Array A(10);
Array B(10);
B=A;
編譯器將調用其自動生成的構造拷貝函數或者拷貝運算符的重載函數。在編譯器生成的缺省的構造拷貝函數和拷貝運算符的重載函數,對指針實行的是按位拷貝,僅僅只是拷貝指針的地址,而不會拷貝指針的內容。因此在執行完前面的代碼之後,A.data和B.data指向的同一地址。當A或者B中任意一個結束其生命週期調用析構函數時,會刪除data。由於他們的data指向的是同一個地方,兩個實例的data都被刪除了。但另外一個實例並不知道它的data已經被刪除了,當企圖再次用它的data的時候,程序就會不可避免地崩潰。
由於問題出現的根源是調用了編譯器生成的缺省構造拷貝函數和拷貝運算符的重載函數。一個最簡單的辦法就是禁止使用這兩個函數。於是我們可以把這兩個函數聲明爲私有函數,如果類的用戶企圖調用這兩個函數,將不能通過編譯。實現的代碼如下:
private:
      Array(const Array& copy);
      const Array& operator = (const Array& copy);
最初的代碼存在問題是因爲不同實例的data指向的同一地址,刪除一個實例的data會把另外一個實例的data也同時刪除。因此我們還可以讓構造拷貝函數或者拷貝運算符的重載函數拷貝的不只是地址,而是數據。由於我們重新存儲了一份數據,這樣一個實例刪除的時候,對另外一個實例沒有影響。這種思路我們稱之爲深度拷貝。實現的代碼如下:
public:
      Array(const Array& copy):data(0), size(copy.size)
      {
            if(size > 0)
            {
                  data = new T[size];
                  for(int i = 0; i < size; ++ i)
                        setValue(i, copy.getValue(i));
            }
      }

      const Array& operator = (const Array& copy)
      {
            if(this == ?)
                  return *this;

            if(data != NULL)
            {
                  delete []data;
                  data = NULL;
            }

            size = copy.size;
            if(size > 0)
            {
                  data = new T[size];
                  for(int i = 0; i < size; ++ i)
                        setValue(i, copy.getValue(i));
            }
      }
爲了防止有多個指針指向的數據被多次刪除,我們還可以保存究竟有多少個指針指向該數據。只有當沒有任何指針指向該數據的時候纔可以被刪除。這種思路通常被稱之爲引用計數技術。在構造函數中,引用計數初始化爲1;每當把這個實例賦值給其他實例或者以參數傳給其他實例的構造拷貝函數的時候,引用計數加1,因爲這意味着又多了一個實例指向它的data;每次需要調用析構函數或者需要把data賦值爲其他數據的時候,引用計數要減1,因爲這意味着指向它的data的指針少了一個。當引用計數減少到0的時候,data已經沒有任何實例指向它了,這個時候就可以安全地刪除。實現的代碼如下:
public:
      Array(unsigned arraySize)
            :data(0), size(arraySize), count(new unsigned int)
      {
            *count = 1;
            if(size > 0)
                  data = new T[size];
      }

      Array(const Array& copy)
            : size(copy.size), data(copy.data), count(copy.count)
      {
            ++ (*count);
      }

      ~Array()
      {
            Release();
      }

      const Array& operator = (const Array& copy)
      {
            if(data == copy.data)
                  return *this;

            Release();

            data = copy.data;
            size = copy.size;
            count = copy.count;
            ++(*count);
      }
private:
      void Release()
      {
            --(*count);
            if(*count == 0)
            {
                  if(data)
                  {
                        delete []data;
                        data = NULL;
                  }

                  delete count;
                  count = 0;
            }
      }

      unsigned int *count;

 


程序員面試題精選(16)-O(logn)求Fibonacci數列
題目:定義Fibonacci數列如下:         /  0                      n=0
f(n)=      1                      n=1
        /  f(n-1)+f(n-2)          n=2
輸入n,用最快的方法求該數列的第n項。
分析:在很多C語言教科書中講到遞歸函數的時候,都會用Fibonacci作爲例子。因此很多程序員對這道題的遞歸解法非常熟悉,看到題目就能寫出如下的遞歸求解的代碼。
///////////////////////////////////////////////////////////////////////
// Calculate the nth item of Fibonacci Series recursively
///////////////////////////////////////////////////////////////////////
long long Fibonacci_Solution1(unsigned int n)
{
      int result[2] = {0, 1};
      if(n < 2)
            return result[n];

      return Fibonacci_Solution1(n - 1) + Fibonacci_Solution1(n - 2);
}
但是,教科書上反覆用這個題目來講解遞歸函數,並不能說明遞歸解法最適合這道題目。我們以求解f(10)作爲例子來分析遞歸求解的過程。要求得f(10),需要求得f(9)和f(8)。同樣,要求得f(9),要先求得f(8)和f(7)……我們用樹形結構來表示這種依賴關係
                  f(10)
               /        /
            f(9)         f(8)
          /     /       /    /
       f(8)     f(7)  f(6)   f(5)
      /   /     /   /
   f(7)  f(6)  f(6) f(5)
我們不難發現在這棵樹中有很多結點會重複的,而且重複的結點數會隨着n的增大而急劇增加。這意味這計算量會隨着n的增大而急劇增大。事實上,用遞歸方法計算的時間複雜度是以n的指數的方式遞增的。大家可以求Fibonacci的第100項試試,感受一下這樣遞歸會慢到什麼程度。在我的機器上,連續運行了一個多小時也沒有出來結果。
其實改進的方法並不複雜。上述方法之所以慢是因爲重複的計算太多,只要避免重複計算就行了。比如我們可以把已經得到的數列中間項保存起來,如果下次需要計算的時候我們先查找一下,如果前面已經計算過了就不用再次計算了。
更簡單的辦法是從下往上計算,首先根據f(0)和f(1)算出f(2),在根據f(1)和f(2)算出f(3)……依此類推就可以算出第n項了。很容易理解,這種思路的時間複雜度是O(n)。
///////////////////////////////////////////////////////////////////////
// Calculate the nth item of Fibonacci Series iteratively
///////////////////////////////////////////////////////////////////////
long long Fibonacci_Solution2(unsigned n)
{
      int result[2] = {0, 1};
      if(n < 2)
            return result[n];
      long long  fibNMinusOne = 1;
      long long  fibNMinusTwo = 0;
      long long  fibN = 0;
      for(unsigned int i = 2; i <= n; ++ i)
      {
            fibN = fibNMinusOne + fibNMinusTwo;

            fibNMinusTwo = fibNMinusOne;
            fibNMinusOne = fibN;
      }

      return fibN;
}
這還不是最快的方法。下面介紹一種時間複雜度是O(logn)的方法。在介紹這種方法之前,先介紹一個數學公式:
{f(n), f(n-1), f(n-1), f(n-2)} ={1, 1, 1,0}n-1
(注:{f(n+1), f(n), f(n), f(n-1)}表示一個矩陣。在矩陣中第一行第一列是f(n+1),第一行第二列是f(n),第二行第一列是f(n),第二行第二列是f(n-1)。)
有了這個公式,要求得f(n),我們只需要求得矩陣{1, 1, 1,0}的n-1次方,因爲矩陣{1, 1, 1,0}的n-1次方的結果的第一行第一列就是f(n)。這個數學公式用數學歸納法不難證明。感興趣的朋友不妨自己證明一下。
現在的問題轉換爲求矩陣{1, 1, 1, 0}的乘方。如果簡單第從0開始循環,n次方將需要n次運算,並不比前面的方法要快。但我們可以考慮乘方的如下性質:
        /  an/2*an/2                      n爲偶數時
an=
        /  a(n-1)/2*a(n-1)/2            n爲奇數時
要求得n次方,我們先求得n/2次方,再把n/2的結果平方一下。如果把求n次方的問題看成一個大問題,把求n/2看成一個較小的問題。這種把大問題分解成一個或多個小問題的思路我們稱之爲分治法。這樣求n次方就只需要logn次運算了。
實現這種方式時,首先需要定義一個2×2的矩陣,並且定義好矩陣的乘法以及乘方運算。當這些運算定義好了之後,剩下的事情就變得非常簡單。完整的實現代碼如下所示。
#include <cassert>
///////////////////////////////////////////////////////////////////////
// A 2 by 2 matrix
///////////////////////////////////////////////////////////////////////
struct Matrix2By2
{
      Matrix2By2
      (
            long long m00 = 0,
            long long m01 = 0,
            long long m10 = 0,
            long long m11 = 0
      )
      :m_00(m00), m_01(m01), m_10(m10), m_11(m11)
      {
      }

      long long m_00;
      long long m_01;
      long long m_10;
      long long m_11;
};

///////////////////////////////////////////////////////////////////////
// Multiply two matrices
// Input: matrix1 - the first matrix
//        matrix2 - the second matrix
//Output: the production of two matrices
///////////////////////////////////////////////////////////////////////
Matrix2By2 MatrixMultiply
(
      const Matrix2By2& matrix1,
      const Matrix2By2& matrix2
)
{
      return Matrix2By2(
            matrix1.m_00 * matrix2.m_00 + matrix1.m_01 * matrix2.m_10,
            matrix1.m_00 * matrix2.m_01 + matrix1.m_01 * matrix2.m_11,
            matrix1.m_10 * matrix2.m_00 + matrix1.m_11 * matrix2.m_10,
            matrix1.m_10 * matrix2.m_01 + matrix1.m_11 * matrix2.m_11);
}

///////////////////////////////////////////////////////////////////////
// The nth power of matrix
// 1  1
// 1  0
///////////////////////////////////////////////////////////////////////
Matrix2By2 MatrixPower(unsigned int n)
{
      assert(n > 0);

      Matrix2By2 matrix;
      if(n == 1)
      {
            matrix = Matrix2By2(1, 1, 1, 0);
      }
      else if(n % 2 == 0)
      {
            matrix = MatrixPower(n / 2);
            matrix = MatrixMultiply(matrix, matrix);
      }
      else if(n % 2 == 1)
      {
            matrix = MatrixPower((n - 1) / 2);
            matrix = MatrixMultiply(matrix, matrix);
            matrix = MatrixMultiply(matrix, Matrix2By2(1, 1, 1, 0));
      }

      return matrix;
}

///////////////////////////////////////////////////////////////////////
// Calculate the nth item of Fibonacci Series using devide and conquer
///////////////////////////////////////////////////////////////////////
long long Fibonacci_Solution3(unsigned int n)
{
      int result[2] = {0, 1};
      if(n < 2)
            return result[n];

      Matrix2By2 PowerNMinus2 = MatrixPower(n - 1);
      return PowerNMinus2.m_00;
}

 


程序員面試題精選(17)-把字符串轉換成整數
題目:輸入一個表示整數的字符串,把該字符串轉換成整數並輸出。例如輸入字符串"345",則輸出整數345。 分析:這道題儘管不是很難,學過C/C++語言一般都能實現基本功能,但不同程序員就這道題寫出的代碼有很大區別,可以說這道題能夠很好地反應出程序員的思維和編程習慣,因此已經被包括微軟在內的多家公司用作面試題。建議讀者在往下看之前自己先編寫代碼,再比較自己寫的代碼和下面的參考代碼有哪些不同。
首先我們分析如何完成基本功能,即如何把表示整數的字符串正確地轉換成整數。還是以"345"作爲例子。當我們掃描到字符串的第一個字符'3'時,我們不知道後面還有多少位,僅僅知道這是第一位,因此此時得到的數字是3。當掃描到第二個數字'4'時,此時我們已經知道前面已經一個3了,再在後面加上一個數字4,那前面的3相當於30,因此得到的數字是3*10+4=34。接着我們又掃描到字符'5',我們已經知道了'5'的前面已經有了34,由於後面要加上一個5,前面的34就相當於340了,因此得到的數字就是34*10+5=345。
分析到這裏,我們不能得出一個轉換的思路:每掃描到一個字符,我們把在之前得到的數字乘以10再加上當前字符表示的數字。這個思路用循環不難實現。
由於整數可能不僅僅之含有數字,還有可能以'+'或者'-'開頭,表示整數的正負。因此我們需要把這個字符串的第一個字符做特殊處理。如果第一個字符是'+'號,則不需要做任何操作;如果第一個字符是'-'號,則表明這個整數是個負數,在最後的時候我們要把得到的數值變成負數。
接着我們試着處理非法輸入。由於輸入的是指針,在使用指針之前,我們要做的第一件是判斷這個指針是不是爲空。如果試着去訪問空指針,將不可避免地導致程序崩潰。另外,輸入的字符串中可能含有不是數字的字符。每當碰到這些非法的字符,我們就沒有必要再繼續轉換。最後一個需要考慮的問題是溢出問題。由於輸入的數字是以字符串的形式輸入,因此有可能輸入一個很大的數字轉換之後會超過能夠表示的最大的整數而溢出。
現在已經分析的差不多了,開始考慮編寫代碼。首先我們考慮如何聲明這個函數。由於是把字符串轉換成整數,很自然我們想到:
int StrToInt(const char* str);
這樣聲明看起來沒有問題。但當輸入的字符串是一個空指針或者含有非法的字符時,應該返回什麼值呢?0怎麼樣?那怎麼區分非法輸入和字符串本身就是”0”這兩種情況呢?
接下來我們考慮另外一種思路。我們可以返回一個布爾值來指示輸入是否有效,而把轉換後的整數放到參數列表中以引用或者指針的形式傳入。於是我們就可以聲明如下:
bool StrToInt(const char *str, int& num);
這種思路解決了前面的問題。但是這個函數的用戶使用這個函數的時候會覺得不是很方便,因爲他不能直接把得到的整數賦值給其他整形變臉,顯得不夠直觀。
前面的第一種聲明就很直觀。如何在保證直觀的前提下當碰到非法輸入的時候通知用戶呢?一種解決方案就是定義一個全局變量,每當碰到非法輸入的時候,就標記該全局變量。用戶在調用這個函數之後,就可以檢驗該全局變量來判斷轉換是不是成功。
下面我們寫出完整的實現代碼。參考代碼:
enum Status {kValid = 0, kInvalid};
int g_nStatus = kValid;

///////////////////////////////////////////////////////////////////////
// Convert a string into an integer
///////////////////////////////////////////////////////////////////////
int StrToInt(const char* str)
{
      g_nStatus = kInvalid;
      long long num = 0;

      if(str != NULL)
      {
            const char* digit = str;

            // the first char in the string maybe '+' or '-'
            bool minus = false;
            if(*digit == '+')
                  digit ++;
            else if(*digit == '-')
            {
                  digit ++;
                  minus = true;
            }

            // the remaining chars in the string
            while(*digit != '/0')
            {
                  if(*digit >= '0' && *digit <= '9')
                  {
                        num = num * 10 + (*digit - '0');

                        // overflow 
                        if(num > std::numeric_limits<int>::max())
                        {
                              num = 0;
                              break;
                        }

                        digit ++;
                  }
                  // if the char is not a digit, invalid input
                  else
                  {
                        num = 0;
                        break;
                  }
            }

            if(*digit == '/0')
            {
                  g_nStatus = kValid;
                  if(minus)
                        num = 0 - num;
            }
      }

      return static_cast<int>(num);
}

討論:在參考代碼中,我選用的是第一種聲明方式。不過在面試時,我們可以選用任意一種聲明方式進行實現。但當面試官問我們選擇的理由時,我們要對兩者的優缺點進行評價。第一種聲明方式對用戶而言非常直觀,但使用了全局變量,不夠優雅;而第二種思路是用返回值來表明輸入是否合法,在很多API中都用這種方法,但該方法聲明的函數使用起來不夠直觀。
最後值得一提的是,在C語言提供的庫函數中,函數atoi能夠把字符串轉換整數。它的聲明是int atoi(const char *str)。該函數就是用一個全局變量來標誌輸入是否合法的。

 

程序員面試題精選(18)-用兩個棧實現隊列
題目:某隊列的聲明如下:
template<typename T> class CQueue
{
public:
      CQueue() {}
      ~CQueue() {}

      void appendTail(const T& node);  // append a element to tail
      void deleteHead();               // remove a element from head

private:
     T> m_stack1;
     T> m_stack2;
};
 
分析:從上面的類的聲明中,我們發現在隊列中有兩個棧。因此這道題實質上是要求我們用兩個棧來實現一個隊列。相信大家對棧和隊列的基本性質都非常瞭解了:棧是一種後入先出的數據容器,因此對隊列進行的插入和刪除操作都是在棧頂上進行;隊列是一種先入先出的數據容器,我們總是把新元素插入到隊列的尾部,而從隊列的頭部刪除元素。
我們通過一個具體的例子來分析往該隊列插入和刪除元素的過程。首先插入一個元素a,不妨把先它插入到m_stack1。這個時候m_stack1中的元素有{a},m_stack2爲空。再插入兩個元素b和c,還是插入到m_stack1中,此時m_stack1中的元素有{a,b,c},m_stack2中仍然是空的。
這個時候我們試着從隊列中刪除一個元素。按照隊列先入先出的規則,由於a比b、c先插入到隊列中,這次被刪除的元素應該是a。元素a存儲在m_stack1中,但並不在棧頂上,因此不能直接進行刪除。注意到m_stack2我們還一直沒有使用過,現在是讓m_stack2起作用的時候了。如果我們把m_stack1中的元素逐個pop出來並push進入m_stack2,元素在m_stack2中的順序正好和原來在m_stack1中的順序相反。因此經過兩次pop和push之後,m_stack1爲空,而m_stack2中的元素是{c,b,a}。這個時候就可以pop出m_stack2的棧頂a了。pop之後的m_stack1爲空,而m_stack2的元素爲{c,b},其中b在棧頂。
這個時候如果我們還想繼續刪除應該怎麼辦呢?在剩下的兩個元素中b和c,b比c先進入隊列,因此b應該先刪除。而此時b恰好又在棧頂上,因此可以直接pop出去。這次pop之後,m_stack1中仍然爲空,而m_stack2爲{c}。
從上面的分析我們可以總結出刪除一個元素的步驟:當m_stack2中不爲空時,在m_stack2中的棧頂元素是最先進入隊列的元素,可以pop出去。如果m_stack2爲空時,我們把m_stack1中的元素逐個pop出來並push進入m_stack2。由於先進入隊列的元素被壓到m_stack1的底端,經過pop和push之後就處於m_stack2的頂端了,又可以直接pop出去。
接下來我們再插入一個元素d。我們是不是還可以把它push進m_stack1?這樣會不會有問題呢?我們說不會有問題。因爲在刪除元素的時候,如果m_stack2中不爲空,處於m_stack2中的棧頂元素是最先進入隊列的,可以直接pop;如果m_stack2爲空,我們把m_stack1中的元素pop出來並push進入m_stack2。由於m_stack2中元素的順序和m_stack1相反,最先進入隊列的元素還是處於m_stack2的棧頂,仍然可以直接pop。不會出現任何矛盾。
我們用一個表來總結一下前面的例子執行的步驟:
操作 m_stack1 m_stack2
append a {a} {}
append b {a,b} {}
append c {a,b,c} {}
delete head {} {b,c}
delete head {} {c}
append d {d} {c}
delete head {d} {}
總結完push和pop對應的過程之後,我們可以開始動手寫代碼了。參考代碼如下:
///////////////////////////////////////////////////////////////////////
// Append a element at the tail of the queue
///////////////////////////////////////////////////////////////////////
template<typename T> void CQueue<T>::appendTail(const T& element)
{
      // push the new element into m_stack1
      m_stack1.push(element);
}
///////////////////////////////////////////////////////////////////////
// Delete the head from the queue
///////////////////////////////////////////////////////////////////////
template<typename T> void CQueue<T>::deleteHead()
{
      // if m_stack2 is empty,
      // and there are some elements in m_stack1, push them in m_stack2
      if(m_stack2.size() <= 0)
      {
            while(m_stack1.size() > 0)
            {
                  T& data = m_stack1.top();
                  m_stack1.pop();
                  m_stack2.push(data);
            }
      }

      // push the element into m_stack2
      assert(m_stack2.size() > 0);
      m_stack2.pop();
}
擴展:這道題是用兩個棧實現一個隊列。反過來能不能用兩個隊列實現一個棧。如果可以,該如何實現?

 


程序員面試題精選(19)-反轉鏈表
題目:輸入一個鏈表的頭結點,反轉該鏈表,並返回反轉後鏈表的頭結點。鏈表結點定義如下:
struct ListNode
{
      int       m_nKey;
      ListNode* m_pNext;
};
分析:這是一道廣爲流傳的微軟面試題。由於這道題能夠很好的反應出程序員思維是否嚴密,在微軟之後已經有很多公司在面試時採用了這道題。
爲了正確地反轉一個鏈表,需要調整指針的指向。與指針操作相關代碼總是容易出錯的,因此最好在動手寫程序之前作全面的分析。在面試的時候不急於動手而是一開始做仔細的分析和設計,將會給面試官留下很好的印象,因爲在實際的軟件開發中,設計的時間總是比寫代碼的時間長。與其很快地寫出一段漏洞百出的代碼,遠不如用較多的時間寫出一段健壯的代碼。
爲了將調整指針這個複雜的過程分析清楚,我們可以藉助圖形來直觀地分析。假設下圖中l、m和n是三個相鄰的結點:
a?b?…?l  m?n?…
假設經過若干操作,我們已經把結點l之前的指針調整完畢,這些結點的m_pNext指針都指向前面一個結點。現在我們遍歷到結點m。當然,我們需要把調整結點的m_pNext指針讓它指向結點l。但注意一旦調整了指針的指向,鏈表就斷開了,如下圖所示:
a?b?…l?m  n?…
因爲已經沒有指針指向結點n,我們沒有辦法再遍歷到結點n了。因此爲了避免鏈表斷開,我們需要在調整m的m_pNext之前要把n保存下來。
接下來我們試着找到反轉後鏈表的頭結點。不難分析出反轉後鏈表的頭結點是原始鏈表的尾位結點。什麼結點是尾結點?就是m_pNext爲空指針的結點。
基於上述分析,我們不難寫出如下代碼:
///////////////////////////////////////////////////////////////////////
// Reverse a list iteratively
// Input: pHead - the head of the original list
// Output: the head of the reversed head
///////////////////////////////////////////////////////////////////////
ListNode* ReverseIteratively(ListNode* pHead)
{
      ListNode* pReversedHead = NULL;
      ListNode* pNode = pHead;
      ListNode* pPrev = NULL;
      while(pNode != NULL)
      {
            // get the next node, and save it at pNext
            ListNode* pNext = pNode->m_pNext;
            // if the next node is null, the currect is the end of original
            // list, and it's the head of the reversed list
            if(pNext == NULL)
                  pReversedHead = pNode;

            // reverse the linkage between nodes
            pNode->m_pNext = pPrev;

            // move forward on the the list
            pPrev = pNode;
            pNode = pNext;
      }

      return pReversedHead;
}
擴展:本題也可以遞歸實現。感興趣的讀者請自己編寫遞歸代碼。

 


程序員面試題精選(20)-最長公共子串
題目:如果字符串一的所有字符按其在字符串中的順序出現在另外一個字符串二中,則字符串一稱之爲字符串二的子串。注意,並不要求子串(字符串一)的字符必須連續出現在字符串二中。請編寫一個函數,輸入兩個字符串,求它們的最長公共子串,並打印出最長公共子串。 例如:輸入兩個字符串BDCABA和ABCBDAB,字符串BCBA和BDAB都是是它們的最長公共子串,則輸出它們的長度4,並打印任意一個子串。
分析:求最長公共子串(Longest Common Subsequence, LCS)是一道非常經典的動態規劃題,因此一些重視算法的公司像MicroStrategy都把它當作面試題。
完整介紹動態規劃將需要很長的篇幅,因此我不打算在此全面討論動態規劃相關的概念,只集中對LCS直接相關內容作討論。如果對動態規劃不是很熟悉,請參考相關算法書比如算法討論。
先介紹LCS問題的性質:記Xm={x0, x1,…xm-1}和Yn={y0,y1,…,yn-1}爲兩個字符串,而Zk={z0,z1,…zk-1}是它們的LCS,則:
1.       如果xm-1=yn-1,那麼zk-1=xm-1=yn-1,並且Zk-1是Xm-1和Yn-1的LCS;
2.       如果xm-1≠yn-1,那麼當zk-1≠xm-1時Z是Xm-1和Y的LCS;
3.       如果xm-1≠yn-1,那麼當zk-1≠yn-1時Z是Yn-1和X的LCS;
下面簡單證明一下這些性質:
1.       如果zk-1≠xm-1,那麼我們可以把xm-1(yn-1)加到Z中得到Z’,這樣就得到X和Y的一個長度爲k+1的公共子串Z’。這就與長度爲k的Z是X和Y的LCS相矛盾了。因此一定有zk-1=xm-1=yn-1。
既然zk-1=xm-1=yn-1,那如果我們刪除zk-1(xm-1、yn-1)得到的Zk-1,Xm-1和Yn-1,顯然Zk-1是Xm-1和Yn-1的一個公共子串,現在我們證明Zk-1是Xm-1和Yn-1的LCS。用反證法不難證明。假設有Xm-1和Yn-1有一個長度超過k-1的公共子串W,那麼我們把加到W中得到W’,那W’就是X和Y的公共子串,並且長度超過k,這就和已知條件相矛盾了。
2.       還是用反證法證明。假設Z不是Xm-1和Y的LCS,則存在一個長度超過k的W是Xm-1和Y的LCS,那W肯定也X和Y的公共子串,而已知條件中X和Y的公共子串的最大長度爲k。矛盾。
3.       證明同2。
有了上面的性質,我們可以得出如下的思路:求兩字符串Xm={x0, x1,…xm-1}和Yn={y0,y1,…,yn-1}的LCS,如果xm-1=yn-1,那麼只需求得Xm-1和Yn-1的LCS,並在其後添加xm-1(yn-1)即可;如果xm-1≠yn-1,我們分別求得Xm-1和Y的LCS和Yn-1和X的LCS,並且這兩個LCS中較長的一個爲X和Y的LCS。
如果我們記字符串Xi和Yj的LCS的長度爲c[i,j],我們可以遞歸地求c[i,j]:
          /      0                               if i<0 or j<0
c[i,j]=          c[i-1,j-1]+1                    if i,j>=0 and xi=xj
         /       max(c[i,j-1],c[i-1,j]           if i,j>=0 and xi≠xj
上面的公式用遞歸函數不難求得。但從前面求Fibonacci第n項(本面試題系列第16題)的分析中我們知道直接遞歸會有很多重複計算,我們用從底向上循環求解的思路效率更高。
爲了能夠採用循環求解的思路,我們用一個矩陣(參考代碼中的LCS_length)保存下來當前已經計算好了的c[i,j],當後面的計算需要這些數據時就可以直接從矩陣讀取。另外,求取c[i,j]可以從c[i-1,j-1] 、c[i,j-1]或者c[i-1,j]三個方向計算得到,相當於在矩陣LCS_length中是從c[i-1,j-1],c[i,j-1]或者c[i-1,j]的某一個各自移動到c[i,j],因此在矩陣中有三種不同的移動方向:向左、向上和向左上方,其中只有向左上方移動時才表明找到LCS中的一個字符。於是我們需要用另外一個矩陣(參考代碼中的LCS_direction)保存移動的方向。
參考代碼如下:
#include "string.h"

// directions of LCS generation
enum decreaseDir {kInit = 0, kLeft, kUp, kLeftUp};

/////////////////////////////////////////////////////////////////////////////
// Get the length of two strings' LCSs, and print one of the LCSs
// Input: pStr1         - the first string
//        pStr2         - the second string
// Output: the length of two strings' LCSs
/////////////////////////////////////////////////////////////////////////////
int LCS(char* pStr1, char* pStr2)
{
      if(!pStr1 || !pStr2)
            return 0;

      size_t length1 = strlen(pStr1);
      size_t length2 = strlen(pStr2);
      if(!length1 || !length2)
            return 0;

      size_t i, j;

      // initiate the length matrix
      int **LCS_length;
      LCS_length = (int**)(new int[length1]);
      for(i = 0; i < length1; ++ i)
            LCS_length = (int*)new int[length2];

      for(i = 0; i < length1; ++ i)
            for(j = 0; j < length2; ++ j)
                  LCS_length[j] = 0;

      // initiate the direction matrix
      int **LCS_direction;
      LCS_direction = (int**)(new int[length1]);
      for( i = 0; i < length1; ++ i)
            LCS_direction = (int*)new int[length2];

      for(i = 0; i < length1; ++ i)
            for(j = 0; j < length2; ++ j)
                  LCS_direction[j] = kInit;

      for(i = 0; i < length1; ++ i)
      {
            for(j = 0; j < length2; ++ j)
            {
                  if(i == 0 || j == 0)
                  {
                        if(pStr1 == pStr2[j])
                        {
                              LCS_length[j] = 1;
                              LCS_direction[j] = kLeftUp;
                        }
                        else
                              LCS_length[j] = 0;
                  }
                  // a char of LCS is found,
                  // it comes from the left up entry in the direction matrix
                  else if(pStr1 == pStr2[j])
                  {
                        LCS_length[j] = LCS_length[i - 1][j - 1] + 1;
                        LCS_direction[j] = kLeftUp;
                  }
                  // it comes from the up entry in the direction matrix
                  else if(LCS_length[i - 1][j] > LCS_length[j - 1])
                  {
                        LCS_length[j] = LCS_length[i - 1][j];
                        LCS_direction[j] = kUp;
                  }
                  // it comes from the left entry in the direction matrix
                  else
                  {
                        LCS_length[j] = LCS_length[j - 1];
                        LCS_direction[j] = kLeft;
                  }
            }
      }
      LCS_Print(LCS_direction, pStr1, pStr2, length1 - 1, length2 - 1);

      return LCS_length[length1 - 1][length2 - 1];
}

/////////////////////////////////////////////////////////////////////////////
// Print a LCS for two strings
// Input: LCS_direction - a 2d matrix which records the direction of
//                        LCS generation
//        pStr1         - the first string
//        pStr2         - the second string
//        row           - the row index in the matrix LCS_direction
//        col           - the column index in the matrix LCS_direction
/////////////////////////////////////////////////////////////////////////////
void LCS_Print(int **LCS_direction,
                    char* pStr1, char* pStr2,
                    size_t row, size_t col)
{
      if(pStr1 == NULL || pStr2 == NULL)
            return;

      size_t length1 = strlen(pStr1);
      size_t length2 = strlen(pStr2);

      if(length1 == 0 || length2 == 0 || !(row < length1 && col < length2))
            return;

      // kLeftUp implies a char in the LCS is found
      if(LCS_direction[row][col] == kLeftUp)
      {
            if(row > 0 && col > 0)
                  LCS_Print(LCS_direction, pStr1, pStr2, row - 1, col - 1);

            // print the char
            printf("%c", pStr1[row]);
      }
      else if(LCS_direction[row][col] == kLeft)
      {
            // move to the left entry in the direction matrix
            if(col > 0)
                  LCS_Print(LCS_direction, pStr1, pStr2, row, col - 1);
      }
      else if(LCS_direction[row][col] == kUp)
      {
            // move to the up entry in the direction matrix
            if(row > 0)
                  LCS_Print(LCS_direction, pStr1, pStr2, row - 1, col);
      }
}
擴展:如果題目改成求兩個字符串的最長公共子字符串,應該怎麼求?子字符串的定義和子串的定義類似,但要求是連續分佈在其他字符串中。比如輸入兩個字符串BDCABA和ABCBDAB的最長公共字符串有BD和AB,它們的長度都是2。

 

程序員面試題精選(21)-左旋轉字符串
題目:定義字符串的左旋轉操作:把字符串前面的若干個字符移動到字符串的尾部。如把字符串abcdef左旋轉2位得到字符串cdefab。請實現字符串左旋轉的函數。要求時間對長度爲n的字符串操作的複雜度爲O(n),輔助內存爲O(1)。
分析:如果不考慮時間和空間複雜度的限制,最簡單的方法莫過於把這道題看成是把字符串分成前後兩部分,通過旋轉操作把這兩個部分交換位置。於是我們可以新開闢一塊長度爲n+1的輔助空間,把原字符串後半部分拷貝到新空間的前半部分,在把原字符串的前半部分拷貝到新空間的後半部分。不難看出,這種思路的時間複雜度是O(n),需要的輔助空間也是O(n)。
接下來的一種思路可能要稍微麻煩一點。我們假設把字符串左旋轉m位。於是我們先把第0個字符保存起來,把第m個字符放到第0個的位置,在把第2m個字符放到第m個的位置…依次類推,一直移動到最後一個可以移動字符,最後在把原來的第0個字符放到剛纔移動的位置上。接着把第1個字符保存起來,把第m+1個元素移動到第1個位置…重複前面處理第0個字符的步驟,直到處理完前面的m個字符。
該思路還是比較容易理解,但當字符串的長度n不是m的整數倍的時候,寫程序會有些麻煩,感興趣的朋友可以自己試一下。由於下面還要介紹更好的方法,這種思路的代碼我就不提供了。
我們還是把字符串看成有兩段組成的,記位XY。左旋轉相當於要把字符串XY變成YX。我們先在字符串上定義一種翻轉的操作,就是翻轉字符串中字符的先後順序。把X翻轉後記爲XT。顯然有(XT)T=X。
我們首先對X和Y兩段分別進行翻轉操作,這樣就能得到XTYT。接着再對XTYT進行翻轉操作,得到(XTYT)T=(YT)T(XT)T=YX。正好是我們期待的結果。
分析到這裏我們再回到原來的題目。我們要做的僅僅是把字符串分成兩段,第一段爲前面m個字符,其餘的字符分到第二段。再定義一個翻轉字符串的函數,按照前面的步驟翻轉三次就行了。時間複雜度和空間複雜度都合乎要求。
參考代碼如下:
#include "string.h"
///////////////////////////////////////////////////////////////////////
// Move the first n chars in a string to its end
///////////////////////////////////////////////////////////////////////
char* LeftRotateString(char* pStr, unsigned int n)
{
      if(pStr != NULL)
      {
            int nLength = static_cast<int>(strlen(pStr));
            if(nLength > 0 || n == 0 || n > nLength)
            {
                  char* pFirstStart = pStr;
                  char* pFirstEnd = pStr + n - 1;
                  char* pSecondStart = pStr + n;
                  char* pSecondEnd = pStr + nLength - 1;

                  // reverse the first part of the string
                  ReverseString(pFirstStart, pFirstEnd);
                  // reverse the second part of the strint
                  ReverseString(pSecondStart, pSecondEnd);
                  // reverse the whole string
                  ReverseString(pFirstStart, pSecondEnd);
            }
      }

      return pStr;
}

///////////////////////////////////////////////////////////////////////
// Reverse the string between pStart and pEnd
///////////////////////////////////////////////////////////////////////
void ReverseString(char* pStart, char* pEnd)
{
      if(pStart == NULL || pEnd == NULL)
      {
            while(pStart <= pEnd)
            {
                  char temp = *pStart;
                  *pStart = *pEnd;
                  *pEnd = temp;

                  pStart ++;
                  pEnd --;
            }
      }
}

 


程序員面試題精選(22)-整數的二進制表示中1的個數
題目:輸入一個整數,求該整數的二進制表達中有多少個1。例如輸入10,由於其二進制表示爲1010,有兩個1,因此輸出2。
分析:這是一道很基本的考查位運算的面試題。包括微軟在內的很多公司都曾採用過這道題。
一個很基本的想法是,我們先判斷整數的最右邊一位是不是1。接着把整數右移一位,原來處於右邊第二位的數字現在被移到第一位了,再判斷是不是1。這樣每次移動一位,直到這個整數變成0爲止。現在的問題變成怎樣判斷一個整數的最右邊一位是不是1了。很簡單,如果它和整數1作與運算。由於1除了最右邊一位以外,其他所有位都爲0。因此如果與運算的結果爲1,表示整數的最右邊一位是1,否則是0。
得到的代碼如下:
///////////////////////////////////////////////////////////////////////
// Get how many 1s in an integer's binary expression
///////////////////////////////////////////////////////////////////////
int NumberOf1_Solution1(int i)
{
      int count = 0;
      while(i)
      {
            if(i & 1)
                  count ++;

            i = i >> 1;
      }

      return count;
}
可能有讀者會問,整數右移一位在數學上是和除以2是等價的。那可不可以把上面的代碼中的右移運算符換成除以2呢?答案是最好不要換成除法。因爲除法的效率比移位運算要低的多,在實際編程中如果可以應儘可能地用移位運算符代替乘除法。 
這個思路當輸入i是正數時沒有問題,但當輸入的i是一個負數時,不但不能得到正確的1的個數,還將導致死循環。以負數0x80000000爲例,右移一位的時候,並不是簡單地把最高位的1移到第二位變成0x40000000,而是0xC0000000。這是因爲移位前是個負數,仍然要保證移位後是個負數,因此移位後的最高位會設爲1。如果一直做右移運算,最終這個數字就會變成0xFFFFFFFF而陷入死循環。
爲了避免死循環,我們可以不右移輸入的數字i。首先i和1做與運算,判斷i的最低位是不是爲1。接着把1左移一位得到2,再和i做與運算,就能判斷i的次高位是不是1……這樣反覆左移,每次都能判斷i的其中一位是不是1。基於此,我們得到如下代碼:
///////////////////////////////////////////////////////////////////////
// Get how many 1s in an integer's binary expression
///////////////////////////////////////////////////////////////////////
int NumberOf1_Solution2(int i)
{
      int count = 0;
      unsigned int flag = 1;
      while(flag)
      {
            if(i & flag)
                  count ++;

            flag = flag << 1;
      }

      return count;
}
另外一種思路是如果一個整數不爲0,那麼這個整數至少有一位是1。如果我們把這個整數減去1,那麼原來處在整數最右邊的1就會變成0,原來在1後面的所有的0都會變成1。其餘的所有位將不受到影響。舉個例子:一個二進制數1100,從右邊數起的第三位是處於最右邊的一個1。減去1後,第三位變成0,它後面的兩位0變成1,而前面的1保持不變,因此得到結果是1011。
我們發現減1的結果是把從最右邊一個1開始的所有位都取反了。這個時候如果我們再把原來的整數和減去1之後的結果做與運算,從原來整數最右邊一個1那一位開始所有位都會變成0。如1100&1011=1000。也就是說,把一個整數減去1,再和原整數做與運算,會把該整數最右邊一個1變成0。那麼一個整數的二進制有多少個1,就可以進行多少次這樣的操作。
這種思路對應的代碼如下:
///////////////////////////////////////////////////////////////////////
// Get how many 1s in an integer's binary expression
///////////////////////////////////////////////////////////////////////
int NumberOf1_Solution3(int i)
{
      int count = 0;
      while (i)
      {
            ++ count;
            i = (i - 1) & i;
      }

      return count;
}
擴展:如何用一個語句判斷一個整數是不是二的整數次冪?

 


程序員面試題精選(23)-跳臺階問題
題目:一個臺階總共有n級,如果一次可以跳1級,也可以跳2級。求總共有多少總跳法,並分析算法的時間複雜度。
分析:這道題最近經常出現,包括MicroStrategy等比較重視算法的公司都曾先後選用過個這道題作爲面試題或者筆試題。
首先我們考慮最簡單的情況。如果只有1級臺階,那顯然只有一種跳法。如果有2級臺階,那就有兩種跳的方法了:一種是分兩次跳,每次跳1級;另外一種就是一次跳2級。
現在我們再來討論一般情況。我們把n級臺階時的跳法看成是n的函數,記爲f(n)。當n>2時,第一次跳的時候就有兩種不同的選擇:一是第一次只跳1級,此時跳法數目等於後面剩下的n-1級臺階的跳法數目,即爲f(n-1);另外一種選擇是第一次跳2級,此時跳法數目等於後面剩下的n-2級臺階的跳法數目,即爲f(n-2)。因此n級臺階時的不同跳法的總數f(n)=f(n-1)+(f-2)。
我們把上面的分析用一個公式總結如下:
        /  1                          n=1
f(n)=      2                          n=2
        /  f(n-1)+(f-2)               n>2
分析到這裏,相信很多人都能看出這就是我們熟悉的Fibonacci序列。至於怎麼求這個序列的第n項,請參考本面試題系列

題目:輸入兩個整數序列。其中一個序列表示棧的push順序,判斷另一個序列有沒有可能是對應的pop順序。爲了簡單起見,我們假設push序列的任意兩個整數都是不相等的。
比如輸入的push序列是1、2、3、4、5,那麼4、5、3、2、1就有可能是一個pop系列。因爲可以有如下的push和pop序列:push 1,push 2,push 3,push 4,pop,push 5,pop,pop,pop,pop,這樣得到的pop序列就是4、5、3、2、1。但序列4、3、5、1、2就不可能是push序列1、2、3、4、5的pop序列。
分析:這到題除了考查對棧這一基本數據結構的理解,還能考查我們的分析能力。
這道題的一個很直觀的想法就是建立一個輔助棧,每次push的時候就把一個整數push進入這個輔助棧,同樣需要pop的時候就把該棧的棧頂整數pop出來。
我們以前面的序列4、5、3、2、1爲例。第一個希望被pop出來的數字是4,因此4需要先push到棧裏面。由於push的順序已經由push序列確定了,也就是在把4 push進棧之前,數字1,2,3都需要push到棧裏面。此時棧裏的包含4個數字,分別是1,2,3,4,其中4位於棧頂。把4 pop出棧後,剩下三個數字1,2,3。接下來希望被pop的是5,由於仍然不是棧頂數字,我們接着在push序列中4以後的數字中尋找。找到數字5後再一次push進棧,這個時候5就是位於棧頂,可以被pop出來。接下來希望被pop的三個數字是3,2,1。每次操作前都位於棧頂,直接pop即可。
再來看序列4、3、5、1、2。pop數字4的情況和前面一樣。把4 pop出來之後,3位於棧頂,直接pop。接下來希望pop的數字是5,由於5不是棧頂數字,我們到push序列中沒有被push進棧的數字中去搜索該數字,幸運的時候能夠找到5,於是把5 push進入棧。此時pop 5之後,棧內包含兩個數字1、2,其中2位於棧頂。這個時候希望pop的數字是1,由於不是棧頂數字,我們需要到push序列中還沒有被push進棧的數字中去搜索該數字。但此時push序列中所有數字都已被push進入棧,因此該序列不可能是一個pop序列。
也就是說,如果我們希望pop的數字正好是棧頂數字,直接pop出棧即可;如果希望pop的數字目前不在棧頂,我們就到push序列中還沒有被push到棧裏的數字中去搜索這個數字,並把在它之前的所有數字都push進棧。如果所有的數字都被push進棧仍然沒有找到這個數字,表明該序列不可能是一個pop序列。
基於前面的分析,我們可以寫出如下的參考代碼:
#include <stack>

/////////////////////////////////////////////////////////////////////////////
// Given a push order of a stack, determine whether an array is possible to
// be its corresponding pop order
// Input: pPush   - an array of integers, the push order
//        pPop    - an array of integers, the pop order
//        nLength - the length of pPush and pPop
// Output: If pPop is possible to be the pop order of pPush, return true.
//         Otherwise return false
/////////////////////////////////////////////////////////////////////////////
bool IsPossiblePopOrder(const int* pPush, const int* pPop, int nLength)
{
  bool bPossible = false;

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

   // ancillary stack
  std::stack<int>stackData;

   // check every integers in pPop
   while(pNextPop - pPop < nLength)
   {
    // while the top of the ancillary stack is not the integer
    // to be poped, try to push some integers into the stack
    while(stackData.empty() || stackData.top() != *pNextPop)
    {
     // pNextPush == NULL means all integers have been
     // pushed into the stack, can't push any longer
     if(!pNextPush)
      break;

     stackData.push(*pNextPush);

     // if there are integers left in pPush, move
     // pNextPush forward, otherwise set it to be NULL
     if(pNextPush - pPush < nLength - 1)
      pNextPush ++;
     else
      pNextPush = NULL;
    }

    // After pushing, the top of stack is still not same as
    // pPextPop, pPextPop is not in a pop sequence
    // corresponding to pPush
    if(stackData.top() != *pNextPop)
     break;

    // Check the next integer in pPop
    stackData.pop();
    pNextPop ++;
   }

   // if all integers in pPop have been check successfully,
   // pPop is a pop sequence corresponding to pPush
   if(stackData.empty() && pNextPop - pPop == nLength)
    bPossible = true;
  }

  return bPossible;
}
? 依次檢查pop序列,當前棧頂不對就壓棧,直到滿足爲止。如果push序列空了,就返回false
不知道我寫的對不對。。。。
int good_order(int push[], int pop[], int size)
{
        int *tmp = (int *)malloc(size * sizeof(int));
        int top = 0, cur_push = 0, cur_pop = 0;
        tmp[top] = push[cur_push++];

        for(; cur_pop < size; cur_pop++) {
                while(cur_push < size && tmp[top] != pop[cur_pop])
                        tmp[++top] = push[cur_push++];
                if(tmp[top] == pop[cur_pop])
                        top--;
                else{
                        free(tmp);
                        tmp = NULL;
                        return 0;
                }
        }
        free(tmp);
        return 1;
}

程序員面試題精選100題(24)-棧的push、pop序列
  

題目:輸入兩個整數序列。其中一個序列表示棧的push順序,判斷另一個序列有沒有可能是對應的pop順序。爲了簡單起見,我們假設push序列的任意兩個整數都是不相等的。
比如輸入的push序列是1、2、3、4、5,那麼4、5、3、2、1就有可能是一個pop系列。因爲可以有如下的push和pop序列:push 1,push 2,push 3,push 4,pop,push 5,pop,pop,pop,pop,這樣得到的pop序列就是4、5、3、2、1。但序列4、3、5、1、2就不可能是push序列1、2、3、4、5的pop序列。
分析:這到題除了考查對棧這一基本數據結構的理解,還能考查我們的分析能力。
這道題的一個很直觀的想法就是建立一個輔助棧,每次push的時候就把一個整數push進入這個輔助棧,同樣需要pop的時候就把該棧的棧頂整數pop出來。
我們以前面的序列4、5、3、2、1爲例。第一個希望被pop出來的數字是4,因此4需要先push到棧裏面。由於push的順序已經由push序列確定了,也就是在把4 push進棧之前,數字1,2,3都需要push到棧裏面。此時棧裏的包含4個數字,分別是1,2,3,4,其中4位於棧頂。把4 pop出棧後,剩下三個數字1,2,3。接下來希望被pop的是5,由於仍然不是棧頂數字,我們接着在push序列中4以後的數字中尋找。找到數字5後再一次push進棧,這個時候5就是位於棧頂,可以被pop出來。接下來希望被pop的三個數字是3,2,1。每次操作前都位於棧頂,直接pop即可。
再來看序列4、3、5、1、2。pop數字4的情況和前面一樣。把4 pop出來之後,3位於棧頂,直接pop。接下來希望pop的數字是5,由於5不是棧頂數字,我們到push序列中沒有被push進棧的數字中去搜索該數字,幸運的時候能夠找到5,於是把5 push進入棧。此時pop 5之後,棧內包含兩個數字1、2,其中2位於棧頂。這個時候希望pop的數字是1,由於不是棧頂數字,我們需要到push序列中還沒有被push進棧的數字中去搜索該數字。但此時push序列中所有數字都已被push進入棧,因此該序列不可能是一個pop序列。
也就是說,如果我們希望pop的數字正好是棧頂數字,直接pop出棧即可;如果希望pop的數字目前不在棧頂,我們就到push序列中還沒有被push到棧裏的數字中去搜索這個數字,並把在它之前的所有數字都push進棧。如果所有的數字都被push進棧仍然沒有找到這個數字,表明該序列不可能是一個pop序列。
基於前面的分析,我們可以寫出如下的參考代碼:
#include <stack>

/////////////////////////////////////////////////////////////////////////////
// Given a push order of a stack, determine whether an array is possible to
// be its corresponding pop order
// Input: pPush   - an array of integers, the push order
//        pPop    - an array of integers, the pop order
//        nLength - the length of pPush and pPop
// Output: If pPop is possible to be the pop order of pPush, return true.
//         Otherwise return false
/////////////////////////////////////////////////////////////////////////////
bool IsPossiblePopOrder(const int* pPush, const int* pPop, int nLength)
{
 bool bPossible = false;

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

  // ancillary stack
  std::stack<int>stackData;

  // check every integers in pPop
  while(pNextPop - pPop < nLength)
  {
   // while the top of the ancillary stack is not the integer
   // to be poped, try to push some integers into the stack
   while(stackData.empty() || stackData.top() != *pNextPop)
   {
    // pNextPush == NULL means all integers have been
    // pushed into the stack, can't push any longer
    if(!pNextPush)
     break;

    stackData.push(*pNextPush);

    // if there are integers left in pPush, move
    // pNextPush forward, otherwise set it to be NULL
    if(pNextPush - pPush < nLength - 1)
     pNextPush ++;
    else
     pNextPush = NULL;
   }

   // After pushing, the top of stack is still not same as
   // pPextPop, pPextPop is not in a pop sequence
   // corresponding to pPush
   if(stackData.top() != *pNextPop)
    break;

   // Check the next integer in pPop
   stackData.pop();
   pNextPop ++;
  }

  // if all integers in pPop have been check successfully,
  // pPop is a pop sequence corresponding to pPush
  if(stackData.empty() && pNextPop - pPop == nLength)
   bPossible = true;
 }

 return bPossible;
程序員面試題精選100題(25)-在從1到n的正數中1出現的次數
  

題目:輸入一個整數n,求從1到n這n個整數的十進制表示中1出現的次數。
例如輸入12,從1到12這些整數中包含1 的數字有1,10,11和12,1一共出現了5次。
分析:這是一道廣爲流傳的google面試題。用最直觀的方法求解並不是很難,但遺憾的是效率不是很高;而要得出一個效率較高的算法,需要比較強的分析能力,並不是件很容易的事情。當然,google的面試題中簡單的也沒有幾道。
首先我們來看最直觀的方法,分別求得1到n中每個整數中1出現的次數。而求一個整數的十進制表示中1出現的次數,就和本面試題系列的第22題很相像了。我們每次判斷整數的個位數字是不是1。如果這個數字大於10,除以10之後再判斷個位數字是不是1。基於這個思路,不難寫出如下的代碼:
int NumberOf1(unsigned int n);

/////////////////////////////////////////////////////////////////////////////
// Find the number of 1 in the integers between 1 and n
// Input: n - an integer
// Output: the number of 1 in the integers between 1 and n
/////////////////////////////////////////////////////////////////////////////
int NumberOf1BeforeBetween1AndN_Solution1(unsigned int n)
{
 int number = 0;

 // Find the number of 1 in each integer between 1 and n
 for(unsigned int i = 1; i <= n; ++ i)
  number += NumberOf1(i);

 return number;
}

/////////////////////////////////////////////////////////////////////////////
// Find the number of 1 in an integer with radix 10
// Input: n - an integer
// Output: the number of 1 in n with radix
/////////////////////////////////////////////////////////////////////////////
int NumberOf1(unsigned int n)
{
 int number = 0;
 while(n)
 {
  if(n % 10 == 1)
   number ++;

  n = n / 10;
 }

 return number;
}
這個思路有一個非常明顯的缺點就是每個數字都要計算1在該數字中出現的次數,因此時間複雜度是O(n)。當輸入的n非常大的時候,需要大量的計算,運算效率很低。我們試着找出一些規律,來避免不必要的計算。
我們用一個稍微大一點的數字21345作爲例子來分析。我們把從1到21345的所有數字分成兩段,即1-1235和1346-21345。
先來看1346-21345中1出現的次數。1的出現分爲兩種情況:一種情況是1出現在最高位(萬位)。從1到21345的數字中,1出現在10000-19999這10000個數字的萬位中,一共出現了10000(104)次;另外一種情況是1出現在除了最高位之外的其他位中。例子中1346-21345,這20000個數字中後面四位中1出現的次數是2000次(2*103,其中2的第一位的數值,103是因爲數字的後四位數字其中一位爲1,其餘的三位數字可以在0到9這10個數字任意選擇,由排列組合可以得出總次數是2*103)。
至於從1到1345的所有數字中1出現的次數,我們就可以用遞歸地求得了。這也是我們爲什麼要把1-21345分爲1-1235和1346-21345兩段的原因。因爲把21345的最高位去掉就得到1345,便於我們採用遞歸的思路。
分析到這裏還有一種特殊情況需要注意:前面我們舉例子是最高位是一個比1大的數字,此時最高位1出現的次數104(對五位數而言)。但如果最高位是1呢?比如輸入12345,從10000到12345這些數字中,1在萬位出現的次數就不是104次,而是2346次了,也就是除去最高位數字之後剩下的數字再加上1。
基於前面的分析,我們可以寫出以下的代碼。在參考代碼中,爲了編程方便,我把數字轉換成字符串了。
#include "string.h"
#include "stdlib.h"

int NumberOf1(const char* strN);
int PowerBase10(unsigned int n);

/////////////////////////////////////////////////////////////////////////////
// Find the number of 1 in an integer with radix 10
// Input: n - an integer
// Output: the number of 1 in n with radix
/////////////////////////////////////////////////////////////////////////////
int NumberOf1BeforeBetween1AndN_Solution2(int n)
{
 if(n <= 0)
  return 0;

 // convert the integer into a string
 char strN[50];
 sprintf(strN, "%d", n);

 return NumberOf1(strN);
}
/////////////////////////////////////////////////////////////////////////////
// Find the number of 1 in an integer with radix 10
// Input: strN - a string, which represents an integer
// Output: the number of 1 in n with radix
/////////////////////////////////////////////////////////////////////////////
int NumberOf1(const char* strN)
{
 if(!strN || *strN < '0' || *strN > '9' || *strN == '/0')
  return 0;

 int firstDigit = *strN - '0';
 unsigned int length = static_cast<unsigned int>(strlen(strN));

 // the integer contains only one digit
 if(length == 1 && firstDigit == 0)
  return 0;

 if(length == 1 && firstDigit > 0)
  return 1;

 // suppose the integer is 21345
 // numFirstDigit is the number of 1 of 10000-19999 due to the first digit
 int numFirstDigit = 0;
 // numOtherDigits is the number of 1 01346-21345 due to all digits
 // except the first one
 int numOtherDigits = firstDigit * (length - 1) * PowerBase10(length - 2);
 // numRecursive is the number of 1 of integer 1345
 int numRecursive = NumberOf1(strN + 1);

 // if the first digit is greater than 1, suppose in integer 21345
 // number of 1 due to the first digit is 10^4. It's 10000-19999
 if(firstDigit > 1)
  numFirstDigit = PowerBase10(length - 1);

 // if the first digit equals to 1, suppose in integer 12345
 // number of 1 due to the first digit is 2346. It's 10000-12345
 else if(firstDigit == 1)
  numFirstDigit = atoi(strN + 1) + 1;

 return numFirstDigit + numOtherDigits + numRecursive;
}

/////////////////////////////////////////////////////////////////////////////
// Calculate 10^n
/////////////////////////////////////////////////////////////////////////////
int PowerBase10(unsigned int n)
{
 int result = 1;
 for(unsigned int i = 0; i < n; ++ i)
  result *= 10;

 return result;
}

程序員面試題精選100題(26)-和爲n連續正數序列
題目:輸入一個正數n,輸出所有和爲n連續正數序列。
例如輸入15,由於1+2+3+4+5=4+5+6=7+8=15,所以輸出3個連續序列1-5、4-6和7-8。
分析:這是網易的一道面試題。
這道題和本面試題系列的第10題有些類似。我們用兩個數small和big分別表示序列的最小值和最大值。首先把small初始化爲1,big初始化爲2。如果從small到big的序列的和大於n的話,我們向右移動small,相當於從序列中去掉較小的數字。如果從small到big的序列的和小於n的話,我們向右移動big,相當於向序列中添加big的下一個數字。一直到small等於(1+n)/2,因爲序列至少要有兩個數字。
基於這個思路,我們可以寫出如下代碼:
void PrintContinuousSequence(int small, int big);

/////////////////////////////////////////////////////////////////////////
// Find continuous sequence, whose sum is n
/////////////////////////////////////////////////////////////////////////
void FindContinuousSequence(int n)
{
 if(n < 3)
  return;

 int small = 1;
 int big = 2;
 int middle = (1 + n) / 2;
 int sum = small + big;

 while(small < middle)
 {
  // we are lucky and find the sequence
  if(sum == n)
   PrintContinuousSequence(small, big);

  // if the current sum is greater than n,
  // move small forward
  while(sum > n)
  {
   sum -= small;
   small ++;

   // we are lucky and find the sequence
   if(sum == n)
    PrintContinuousSequence(small, big);
  }

  // move big forward
  big ++;
  sum += big;
 }
}

/////////////////////////////////////////////////////////////////////////
// Print continuous sequence between small and big
/////////////////////////////////////////////////////////////////////////
void PrintContinuousSequence(int small, int big)
{
 for(int i = small; i <= big; ++ i)
  printf("%d ", i);

 printf("/n");
}
程序員面試題精選100題(27)-二元樹的深度
  

題目:輸入一棵二元樹的根結點,求該樹的深度。從根結點到葉結點依次經過的結點(含根、葉結點)形成樹的一條路徑,最長路徑的長度爲樹的深度。
例如:輸入二元樹:
                                            10
                                          /     /
                                        6        14
                                      /         /   /
                                    4         12     16
輸出該樹的深度3。
二元樹的結點定義如下:
struct SBinaryTreeNode // a node of the binary tree
{
 int               m_nValue; // value of node
 SBinaryTreeNode  *m_pLeft;  // left child of node
 SBinaryTreeNode  *m_pRight; // right child of node
};
分析:這道題本質上還是考查二元樹的遍歷。
題目給出了一種樹的深度的定義。當然,我們可以按照這種定義去得到樹的所有路徑,也就能得到最長路徑以及它的長度。只是這種思路用來寫程序有點麻煩。
我們還可以從另外一個角度來理解樹的深度。如果一棵樹只有一個結點,它的深度爲1。如果根結點只有左子樹而沒有右子樹,那麼樹的深度應該是其左子樹的深度加1;同樣如果根結點只有右子樹而沒有左子樹,那麼樹的深度應該是其右子樹的深度加1。如果既有右子樹又有左子樹呢?那該樹的深度就是其左、右子樹深度的較大值再加1。
上面的這個思路用遞歸的方法很容易實現,只需要對遍歷的代碼稍作修改即可。參考代碼如下:
///////////////////////////////////////////////////////////////////////
// Get depth of a binary tree
// Input: pTreeNode - the head of a binary tree
// Output: the depth of a binary tree
///////////////////////////////////////////////////////////////////////
int TreeDepth(SBinaryTreeNode *pTreeNode)
{
 // the depth of a empty tree is 0
 if(!pTreeNode)
  return 0;

 // the depth of left sub-tree
 int nLeft = TreeDepth(pTreeNode->m_pLeft);
 // the depth of right sub-tree
 int nRight = TreeDepth(pTreeNode->m_pRight);

 // depth is the binary tree
 return (nLeft > nRight) ? (nLeft + 1) : (nRight + 1);

程序員面試題精選100題(28)-字符串的排列
  

題目:輸入一個字符串,打印出該字符串中字符的所有排列。例如輸入字符串abc,則輸出由字符a、b、c所能排列出來的所有字符串abc、acb、bac、bca、cab和cba。
分析:這是一道很好的考查對遞歸理解的編程題,因此在過去一年中頻繁出現在各大公司的面試、筆試題中。
我們以三個字符abc爲例來分析一下求字符串排列的過程。首先我們固定第一個字符a,求後面兩個字符bc的排列。當兩個字符bc的排列求好之後,我們把第一個字符a和後面的b交換,得到bac,接着我們固定第一個字符b,求後面兩個字符ac的排列。現在是把c放到第一位置的時候了。記住前面我們已經把原先的第一個字符a和後面的b做了交換,爲了保證這次c仍然是和原先處在第一位置的a交換,我們在拿c和第一個字符交換之前,先要把b和a交換回來。在交換b和a之後,再拿c和處在第一位置的a進行交換,得到cba。我們再次固定第一個字符c,求後面兩個字符b、a的排列。
既然我們已經知道怎麼求三個字符的排列,那麼固定第一個字符之後求後面兩個字符的排列,就是典型的遞歸思路了。
基於前面的分析,我們可以得到如下的參考代碼:
void Permutation(char* pStr, char* pBegin);

/////////////////////////////////////////////////////////////////////////
// Get the permutation of a string,
// for example, input string abc, its permutation is
// abc acb bac bca cba cab
/////////////////////////////////////////////////////////////////////////
void Permutation(char* pStr)
{
 Permutation(pStr, pStr);
}

/////////////////////////////////////////////////////////////////////////
// Print the permutation of a string,
// Input: pStr   - input string
//        pBegin - points to the begin char of string
//                 which we want to permutate in this recursion
/////////////////////////////////////////////////////////////////////////
void Permutation(char* pStr, char* pBegin)
{
 if(!pStr || !pBegin)
  return;

 // if pBegin points to the end of string,
 // this round of permutation is finished,
 // print the permuted string
 if(*pBegin == '/0')
 {
  printf("%s/n", pStr);
 }
 // otherwise, permute string
 else
 {
  for(char* pCh = pBegin; *pCh != '/0'; ++ pCh)
  {
   // swap pCh and pBegin
   char temp = *pCh;
   *pCh = *pBegin;
   *pBegin = temp;

   Permutation(pStr, pBegin + 1);

   // restore pCh and pBegin
   temp = *pCh;
   *pCh = *pBegin;
   *pBegin = temp;
  }
 }
}
擴展1:如果不是求字符的所有排列,而是求字符的所有組合,應該怎麼辦呢?當輸入的字符串中含有相同的字符串時,相同的字符交換位置是不同的排列,但是同一個組合。舉個例子,如果輸入aaa,那麼它的排列是6個aaa,但對應的組合只有一個。
擴展2:輸入一個含有8個數字的數組,判斷有沒有可能把這8個數字分別放到正方體的8個頂點上,使得正方體上三組相對的面上的4個頂點的和相等。

程序員面試題精選100題(29)-調整數組順序使奇數位於偶數前面
  

題目:輸入一個整數數組,調整數組中數字的順序,使得所有奇數位於數組的前半部分,所有偶數位於數組的後半部分。要求時間複雜度爲O(n)。
分析:如果不考慮時間複雜度,最簡單的思路應該是從頭掃描這個數組,每碰到一個偶數時,拿出這個數字,並把位於這個數字後面的所有數字往前挪動一位。挪完之後在數組的末尾有一個空位,這時把該偶數放入這個空位。由於碰到一個偶數,需要移動O(n)個數字,因此總的時間複雜度是O(n2)。
要求的是把奇數放在數組的前半部分,偶數放在數組的後半部分,因此所有的奇數應該位於偶數的前面。也就是說我們在掃描這個數組的時候,如果發現有偶數出現在奇數的前面,我們可以交換他們的順序,交換之後就符合要求了。
因此我們可以維護兩個指針,第一個指針初始化爲數組的第一個數字,它只向後移動;第二個指針初始化爲數組的最後一個數字,它只向前移動。在兩個指針相遇之前,第一個指針總是位於第二個指針的前面。如果第一個指針指向的數字是偶數而第二個指針指向的數字是奇數,我們就交換這兩個數字。
基於這個思路,我們可以寫出如下的代碼:
void Reorder(int *pData, unsigned int length, bool (*func)(int));
bool isEven(int n);

/////////////////////////////////////////////////////////////////////////
// Devide an array of integers into two parts, odd in the first part,
// and even in the second part
// Input: pData  - an array of integers
//        length - the length of array
/////////////////////////////////////////////////////////////////////////
void ReorderOddEven(int *pData, unsigned int length)
{
 if(pData == NULL || length == 0)
  return;

 Reorder(pData, length, isEven);
}

/////////////////////////////////////////////////////////////////////////
// Devide an array of integers into two parts, the intergers which
// satisfy func in the first part, otherwise in the second part
// Input: pData  - an array of integers
//        length - the length of array
//        func   - a function
/////////////////////////////////////////////////////////////////////////
void Reorder(int *pData, unsigned int length, bool (*func)(int))
{
 if(pData == NULL || length == 0)
  return;

 int *pBegin = pData;
 int *pEnd = pData + length - 1;

 while(pBegin < pEnd)
 {
  // if *pBegin does not satisfy func, move forward
  if(!func(*pBegin))
  {
   pBegin ++;
   continue;
  }

  // if *pEnd does not satisfy func, move backward
  if(func(*pEnd))
  {
   pEnd --;
   continue;
  }

  // if *pBegin satisfy func while *pEnd does not,
  // swap these integers
  int temp = *pBegin;
  *pBegin = *pEnd;
  *pEnd = temp;
 }
}

/////////////////////////////////////////////////////////////////////////
// Determine whether an integer is even or not
// Input: an integer
// otherwise return false
/////////////////////////////////////////////////////////////////////////
bool isEven(int n)
{
 return (n & 1) == 0;
}
討論:
上面的代碼有三點值得提出來和大家討論:
1.函數isEven判斷一個數字是不是偶數並沒有用%運算符而是用&。理由是通常情況下位運算符比%要快一些;
2.這道題有很多變種。這裏要求是把奇數放在偶數的前面,如果把要求改成:把負數放在非負數的前面等,思路都是都一樣的。
3.在函數Reorder中,用函數指針func指向的函數來判斷一個數字是不是符合給定的條件,而不是用在代碼直接判斷(hard code)。這樣的好處是把調整順序的算法和調整的標準分開了(即解耦,decouple)。當調整的標準改變時,Reorder的代碼不需要修改,只需要提供一個新的確定調整標準的函數即可,提高了代碼的可維護性。例如要求把負數放在非負數的前面,我們不需要修改Reorder的代碼,只需添加一個函數來判斷整數是不是非負數。這樣的思路在很多庫中都有廣泛的應用,比如在STL的很多算法函數中都有一個仿函數(functor)的參數(當然仿函數不是函數指針,但其思想是一樣的)。如果在面試中能夠想到這一層,無疑能給面試官留下很好的印象。

 

程序員面試題精選100題(30)-異常安全的賦值運算符重載函數
  

題目:類CMyString的聲明如下:
class CMyString
{
public:
 CMyString(char* pData = NULL);
 CMyString(const CMyString& str);
 ~CMyString(void);
 CMyString& operator = (const CMyString& str);

private:
 char* m_pData;
};
請實現其賦值運算符的重載函數,要求異常安全,即當對一個對象進行賦值時發生異常,對象的狀態不能改變。
分析:首先我們來看一般C++教科書上給出的賦值運算符的重載函數:
CMyString& CMyString::operator =(const CMyString &str)
{
 if(this == &str)
  return *this;

 delete []m_pData;
 m_pData = NULL;

 m_pData = new char[strlen(str.m_pData) + 1];
 strcpy(m_pData, str.m_pData);

 return *this;
}
我們知道,在分配內存時有可能發生異常。當執行語句new char[strlen(str.m_pData) + 1]發生異常時,程序將從該賦值運算符的重載函數退出不再執行。注意到這個時候語句delete []m_pData已經執行了。也就是說賦值操作沒有完成,但原來對象的狀態已經改變。也就是說不滿足題目的異常安全的要求。
爲了滿足異常安全這個要求,一個簡單的辦法是掉換new、delete的順序。先把內存new出來用一個臨時指針保存起來,只有這個語句正常執行完成之後再執行delete。這樣就能夠保證異常安全了。
下面給出的是一個更加優雅的實現方案:
CMyString& CMyString::operator =(const CMyString &str)
{
 if(this != &str)
 {
  CMyString strTemp(str);

  char* pTemp = strTemp.m_pData;
  strTemp.m_pData = m_pData;
  m_pData = pTemp;
 }

 return *this;
}
該方案通過調用構造拷貝函數創建一個臨時對象來分配內存。此時即使發生異常,對原來對象的狀態沒有影響。交換臨時對象和需要賦值的對象的字符串指針之後,由於臨時對象的生命週期結束,自動調用其析構函數釋放需賦值對象的原來的字符串空間。整個函數不需要顯式用到new、delete,內存的分配和釋放都自動完成,因此代碼顯得比較優雅。

程序員面試題精選100題(31)-從尾到頭輸出鏈表
題目:輸入一個鏈表的頭結點,從尾到頭反過來輸出每個結點的值。鏈表結點定義如下:

struct ListNode

{

      int       m_nKey;

      ListNode* m_pNext;

};

分析:這是一道很有意思的面試題。該題以及它的變體經常出現在各大公司的面試、筆試題中。

看到這道題後,第一反應是從頭到尾輸出比較簡單。於是很自然地想到把鏈表中鏈接結點的指針反轉過來,改變鏈表的方向。然後就可以從頭到尾輸出了。反轉鏈表的算法詳見本人面試題精選系列的第19題,在此不再細述。但該方法需要額外的操作,應該還有更好的方法。

接下來的想法是從頭到尾遍歷鏈表,每經過一個結點的時候,把該結點放到一個棧中。當遍歷完整個鏈表後,再從棧頂開始輸出結點的值,此時輸出的結點的順序已經反轉過來了。該方法需要維護一個額外的棧,實現起來比較麻煩。

既然想到了棧來實現這個函數,而遞歸本質上就是一個棧結構。於是很自然的又想到了用遞歸來實現。要實現反過來輸出鏈表,我們每訪問到一個結點的時候,先遞歸輸出它後面的結點,再輸出該結點自身,這樣鏈表的輸出結果就反過來了。

基於這樣的思路,不難寫出如下代碼:

///////////////////////////////////////////////////////////////////////

// Print a list from end to beginning

// Input: pListHead - the head of list

///////////////////////////////////////////////////////////////////////

void PrintListReversely(ListNode* pListHead)

{

      if(pListHead != NULL)

      {

            // Print the next node first

            if (pListHead->m_pNext != NULL)

            {

                  PrintListReversely(pListHead->m_pNext);

            }

 

            // Print this node

            printf("%d", pListHead->m_nKey);

      }

}

擴展:該題還有兩個常見的變體:

1.       從尾到頭輸出一個字符串;

2.       定義一個函數求字符串的長度,要求該函數體內不能聲明任何變量。

程序員面試題精選100題(32)-不能被繼承的類
題目:用C++ 設計一個不能被繼承的類。
分析:這是Adobe 公司2007 年校園招聘的最新筆試題。這道題除了考察應聘者的C++ 基本功底外,還能考察反應能力,是一道很好的題目。
在Java 中定義了關鍵字final ,被final 修飾的類不能被繼承。但在C++ 中沒有final 這個關鍵字,要實現這個要求還是需要花費一些精力。
首先想到的是在C++ 中,子類的構造函數會自動調用父類的構造函數。同樣,子類的析構函數也會自動調用父類的析構函數。要想一個類不能被繼承,我們只要把它的構造函數和析構函數都定義爲私有函數。那麼當一個類試圖從它那繼承的時候,必然會由於試圖調用構造函數、析構函數而導致編譯錯誤。
可是這個類的構造函數和析構函數都是私有函數了,我們怎樣才能得到該類的實例呢?這難不倒我們,我們可以通過定義靜態來創建和釋放類的實例。基於這個思路,我們可以寫出如下的代碼:
///////////////////////////////////////////////////////////////////////
// Define a class which can't be derived from
///////////////////////////////////////////////////////////////////////
class FinalClass1
{
public :
      static FinalClass1* GetInstance()
      {
            return new FinalClass1;
      }
 
      static void DeleteInstance( FinalClass1* pInstance)
      {
            delete pInstance;
            pInstance = 0;
      }
 
private :
      FinalClass1() {}
      ~FinalClass1() {}
};
這個類是不能被繼承,但在總覺得它和一般的類有些不一樣,使用起來也有點不方便。比如,我們只能得到位於堆上的實例,而得不到位於棧上實例。
能不能實現一個和一般類除了不能被繼承之外其他用法都一樣的類呢?辦法總是有的,不過需要一些技巧。請看如下代碼:
///////////////////////////////////////////////////////////////////////
// Define a class which can't be derived from
///////////////////////////////////////////////////////////////////////
template <typename T> class MakeFinal
{
      friend T;
 
private :
      MakeFinal() {}
      ~MakeFinal() {}
};
 
class FinalClass2 : virtual public MakeFinal<FinalClass2>
{
public :
      FinalClass2() {}
      ~FinalClass2() {}
};
這個類使用起來和一般的類沒有區別,可以在棧上、也可以在堆上創建實例。儘管類 MakeFinal <FinalClass2> 的構造函數和析構函數都是私有的,但由於類 FinalClass2 是它的友元函數,因此在 FinalClass2 中調用 MakeFinal <FinalClass2> 的構造函數和析構函數都不會造成編譯錯誤。
但當我們試圖從 FinalClass2 繼承一個類並創建它的實例時,卻不同通過編譯。
class Try : public FinalClass2
{
public :
      Try() {}
      ~Try() {}
};
 
Try temp;
由於類 FinalClass2 是從類 MakeFinal <FinalClass2> 虛繼承過來的,在調用 Try 的構造函數的時候,會直接跳過 FinalClass2 而直接調用 MakeFinal <FinalClass2> 的構造函數。非常遺憾的是, Try 不是 MakeFinal <FinalClass2> 的友元,因此不能調用其私有的構造函數。
基於上面的分析,試圖從 FinalClass2 繼承的類,一旦實例化,都會導致編譯錯誤,因此是 FinalClass2 不能被繼承。這就滿足了我們設計要求。
程序員面試題精選100題(33)-在O(1)時間刪除鏈表結點
題目:給定鏈表的頭指針和一個結點指針,在O(1) 時間刪除該結點。鏈表結點的定義如下:
struct ListNode
{
int        m_nKey;
      ListNode*  m_pNext;
};
函數的聲明如下:
void DeleteNode(ListNode* pListHead, ListNode* pToBeDeleted);
分析:這是一道廣爲流傳的Google 面試題,能有效考察我們的編程基本功,還能考察我們的反應速度,更重要的是,還能考察我們對時間複雜度的理解。
在鏈表中刪除一個結點,最常規的做法是從鏈表的頭結點開始,順序查找要刪除的結點,找到之後再刪除。由於需要順序查找,時間複雜度自然就是O(n) 了。
我們之所以需要從頭結點開始查找要刪除的結點,是因爲我們需要得到要刪除的結點的前面一個結點。我們試着換一種思路。我們可以從給定的結點得到它的下一個 結點。這個時候我們實際刪除的是它的下一個結點,由於我們已經得到實際刪除的結點的前面一個結點,因此完全是可以實現的。當然,在刪除之前,我們需要需要 把給定的結點的下一個結點的數據拷貝到給定的結點中。此時,時間複雜度爲O(1) 。
上面的思路還有一個問題:如果刪除的結點位於鏈表的尾部,沒有下一個結點,怎麼辦?我們仍然從鏈表的頭結點開始,順便遍歷得到給定結點的前序結點,並完成刪除操作。這個時候時間複雜度是O(n) 。
那題目要求我們需要在O(1) 時間完成刪除操作,我們的算法是不是不符合要求?實際上,假設鏈表總共有n 個結點,我們的算法在n-1 總情況下時間複雜度是O(1) ,只有當給定的結點處於鏈表末尾的時候,時間複雜度爲O(n) 。那麼平均時間複雜度[(n-1)*O(1)+O(n)]/n ,仍然爲O(1) 。
基於前面的分析,我們不難寫出下面的代碼。
參考代碼:
///////////////////////////////////////////////////////////////////////
// Delete a node in a list
// Input: pListHead - the head of list
//        pToBeDeleted - the node to be deleted
///////////////////////////////////////////////////////////////////////
void DeleteNode(ListNode* pListHead, ListNode* pToBeDeleted)
{
      if(!pListHead || !pToBeDeleted)
            return;
 
      // if pToBeDeleted is not the last node in the list
      if(pToBeDeleted->m_pNext != NULL)
      {
            // copy data from the node next to pToBeDeleted
            ListNode* pNext = pToBeDeleted->m_pNext;
            pToBeDeleted->m_nKey = pNext->m_nKey;
            pToBeDeleted->m_pNext = pNext->m_pNext;
 
            // delete the node next to the pToBeDeleted
            delete pNext;
            pNext = NULL;
      }
      // if pToBeDeleted is the last node in the list
      else
      {
            // get the node prior to pToBeDeleted
            ListNode* pNode = pListHead;
            while(pNode->m_pNext != pToBeDeleted)
            {
                  pNode = pNode->m_pNext;           
            }
 
            // deleted pToBeDeleted
            pNode->m_pNext = NULL;
            delete pToBeDeleted;
            pToBeDeleted = NULL;
      }
}
值得注意的是,爲了讓代碼看起來簡潔一些,上面的代碼基於兩個假設:(1 )給定的結點的確在鏈表中;(2 )給定的要刪除的結點不是鏈表的頭結點。不考慮第一個假設對代碼的魯棒性是有影響的。至於第二個假設,當整個列表只有一個結點時,代碼會有問題。但這個假 設不算很過分,因爲在有些鏈表的實現中,會創建一個虛擬的鏈表頭,並不是一個實際的鏈表結點。這樣要刪除的結點就不可能是鏈表的頭結點了。當然,在面試 中,我們可以把這些假設和面試官交流。這樣,面試官還是會覺得我們考慮問題很周到的。
程序員面試題精選100題(35)-找兩個鏈表的第一個公共結點
題目:兩個單向鏈表,找出它們的第一個公共結點。
鏈表的結點定義爲:
struct ListNode
{
      int         m_nKey;
      ListNode*   m_pNext;
};
分析:這是一道微軟的面試題。微軟非常喜歡與鏈表相關的題目,因此在微軟的面試題中,鏈表出現的概率相當高。
如果兩個單向鏈表有公共的結點,也就是說兩個鏈表從某一結點開始,它們的m_pNext 都指向同一個結點。但由於是單向鏈表的結點,每個結點只有一個m_pNext ,因此從第一個公共結點開始,之後它們所有結點都是重合的,不可能再出現分叉。所以,兩個有公共結點而部分重合的鏈表,拓撲形狀看起來像一個Y ,而不可能像X 。
看到這個題目,第一反應就是蠻力法:在第一鏈表上順序遍歷每個結點。每遍歷一個結點的時候,在第二個鏈表上順序遍歷每個結點。如果此時兩個鏈表上的結點是一樣的,說明此時兩個鏈表重合,於是找到了它們的公共結點。如果第一個鏈表的長度爲m ,第二個鏈表的長度爲n ,顯然,該方法的時間複雜度爲O(mn) 。
接下來我們試着去尋找一個線性時間複雜度的算法。我們先把問題簡化:如何判斷兩個單向鏈表有沒有公共結點?前面已經提到, 如果兩個鏈表有一個公共結點,那麼該公共結點之後的所有結點都是重合的。那麼,它們的最後一個結點必然是重合的。因此,我們判斷兩個鏈表是不是有重合的部 分,只要分別遍歷兩個鏈表到最後一個結點。如果兩個尾結點是一樣的,說明它們用重合;否則兩個鏈表沒有公共的結點。
在上面的思路中,順序遍歷兩個鏈表到尾結點的時候,我們不能保證在兩個鏈表上同時到達尾結點。這是因爲兩個鏈表不一定長度一樣。但如果假設一個鏈表比另一個長l 個結點,我們先在長的鏈表上遍歷l 個結點,之後再同步遍歷,這個時候我們就能保證同時到達最後一個結點了。由於兩個鏈表從第一個公共結點考試到鏈表的尾結點,這一部分是重合的。因此,它們肯定也是同時到達第一公共結點的。於是在遍歷中,第一個相同的結點就是第一個公共的結點。
在這個思路中,我們先要分別遍歷兩個鏈表得到它們的長度,並求出兩個長度之差。在長的鏈表上先遍歷若干次之後,再同步遍歷兩個鏈表,知道找到相同的結點,或者一直到鏈表結束。此時,如果第一個鏈表的長度爲m ,第二個鏈表的長度爲n ,該方法的時間複雜度爲O(m+n) 。
基於這個思路,我們不難寫出如下的代碼:
///////////////////////////////////////////////////////////////////////
// Find the first common node in the list with head pHead1 and
// the list with head pHead2
// Input: pHead1 - the head of the first list
//        pHead2 - the head of the second list
// Return: the first common node in two list. If there is no common
//         nodes, return NULL
///////////////////////////////////////////////////////////////////////
ListNode * FindFirstCommonNode( ListNode *pHead1, ListNode *pHead2)
{
      // Get the length of two lists
      unsigned int nLength1 = ListLength(pHead1);
      unsigned int nLength2 = ListLength(pHead2);
      int nLengthDif = nLength1 - nLength2;
 
      // Get the longer list
      ListNode *pListHeadLong = pHead1;
      ListNode *pListHeadShort = pHead2;
      if(nLength2 > nLength1)
      {
            pListHeadLong = pHead2;
            pListHeadShort = pHead1;
            nLengthDif = nLength2 - nLength1;
      }
 
      // Move on the longer list
      for(int i = 0; i < nLengthDif; ++ i)
            pListHeadLong = pListHeadLong->m_pNext;
 
      // Move on both lists
      while((pListHeadLong != NULL) &&
            (pListHeadShort != NULL) &&
            (pListHeadLong != pListHeadShort))
      {
            pListHeadLong = pListHeadLong->m_pNext;
            pListHeadShort = pListHeadShort->m_pNext;
      }
 
      // Get the first common node in two lists
      ListNode *pFisrtCommonNode = NULL;
      if(pListHeadLong == pListHeadShort)
            pFisrtCommonNode = pListHeadLong;
 
      return pFisrtCommonNode;
}
 
///////////////////////////////////////////////////////////////////////
// Get the length of list with head pHead
// Input: pHead - the head of list
// Return: the length of list
///////////////////////////////////////////////////////////////////////
unsigned int ListLength(ListNode* pHead)
{
      unsigned int nLength = 0;
      ListNode* pNode = pHead;
      while(pNode != NULL)
      {
            ++ nLength;
            pNode = pNode->m_pNext;
      }
 
      return nLength;
}
 程序員面試題精選(35):一次遍歷鏈表求中間節點位置
思路:聲明兩指針p和q,p每往後移動兩節點,q往後移動一個節點,當p->next==NULL時,q便是中間節點的位置。
知道思路後,實現就比較簡單了,在此不給出代碼。

程序員面試題精選100題(36)-在字符串中刪除特定的字符
題目:輸入兩個字符串,從第一字符串中刪除第二個字符串中所有的字符。例如,輸入”They are students.” 和”aeiou” ,則刪除之後的第一個字符串變成”Thy r stdnts.” 。
分析:這是一道微軟面試題。在微軟的常見面試題中,與字符串相關的題目佔了很大的一部分,因爲寫程序操作字符串能很好的反映我們的編程基本功。
要編程完成這道題要求的功能可能並不難。畢竟,這道題的基本思路就是在第一個字符串中拿到一個字符,在第二個字符串中查找 一下,看它是不是在第二個字符串中。如果在的話,就從第一個字符串中刪除。但如何能夠把效率優化到讓人滿意的程度,卻也不是一件容易的事情。也就是說,如 何在第一個字符串中刪除一個字符,以及如何在第二字符串中查找一個字符,都是需要一些小技巧的。
首先我們考慮如何在字符串中刪除一個字符。由於字符串的內存分配方式是連續分配的。我們從字符串當中刪除一個字符,需要把後面所有的字符往前移動一個字節的位置。但如果每次刪除都需要移動字符串後面的字符的話,對於一個長度爲n 的字符串而言,刪除一個字符的時間複雜度爲O(n) 。而對於本題而言,有可能要刪除的字符的個數是n ,因此該方法就刪除而言的時間複雜度爲O(n2) 。
事實上,我們並不需要在每次刪除一個字符的時候都去移動後面所有的字符。我們可以設想,當一個字符需要被刪除的時候,我們把它所佔的位置讓它後面的字符來填補,也就相當於這個字符被刪除了。在具體實現中,我們可以定義兩個指針(pFast 和pSlow) ,初始的時候都指向第一字符的起始位置。當pFast 指向的字符是需要刪除的字符,則pFast 直接跳過,指向下一個字符。如果pFast 指向的字符是不需要刪除的字符,那麼把pFast 指向的字符賦值給pSlow 指向的字符,並且pFast 和pStart 同時向後移動指向下一個字符。這樣,前面被pFast 跳過的字符相當於被刪除了。用這種方法,整個刪除在O(n) 時間內就可以完成。
接下來我們考慮如何在一個字符串中查找一個字符。當然,最簡單的辦法就是從頭到尾掃描整個字符串。顯然,這種方法需要一個循環,對於一個長度爲n 的字符串,時間複雜度是O(n) 。
由於字符的總數是有限的。對於八位的char 型字符而言,總共只有28=256 個字符。我們可以新建一個大小爲256 的數組,把所有元素都初始化爲0 。然後對於字符串中每一個字符,把它的ASCII 碼映射成索引,把數組中該索引對應的元素設爲1。這個時候,要查找一個字符就變得很快了:根據這個字符的ASCII 碼,在數組中對應的下標找到該元素,如果爲0 ,表示字符串中沒有該字符,否則字符串中包含該字符。此時,查找一個字符的時間複雜度是O(1) 。其實,這個數組就是一個hash 表。這種思路的詳細說明,詳見 本面試題系列的第13 題 。
基於上述分析,我們可以寫出如下代碼:
///////////////////////////////////////////////////////////////////////
// Delete all characters in pStrDelete from pStrSource
///////////////////////////////////////////////////////////////////////
void DeleteChars(char* pStrSource, const char* pStrDelete)
{
      if(NULL == pStrSource || NULL == pStrDelete)
            return;
 
      // Initialize an array, the index in this array is ASCII value.
      // All entries in the array, whose index is ASCII value of a
      // character in the pStrDelete, will be set as 1.
      // Otherwise, they will be set as 0.
      const unsigned int nTableSize = 256;
      int hashTable[nTableSize];
      memset(hashTable, 0, sizeof(hashTable));
 
      const char* pTemp = pStrDelete;
      while ('/0' != *pTemp)
      {
            hashTable[*pTemp] = 1;
            ++ pTemp;
      }
 
      char* pSlow = pStrSource;
      char* pFast = pStrSource;
      while ('/0' != *pFast)
      {
            // if the character is in pStrDelete, move both pStart and
            // pEnd forward, and copy pEnd to pStart.
            // Otherwise, move only pEnd forward, and the character
            // pointed by pEnd is deleted
            if(1 != hashTable[*pFast])
            {
                  *pSlow = *pFast;
                  ++ pSlow;
            }
 
            ++pFast;
      }
 
      *pSlow = '/0';
}
程序員面試題精選(36):找出數組中唯一的重複元素
1-1000放在含有1001個元素的數組中,只有唯一的一個元素值重複,其它均只出現
一次。每個數組元素只能訪問一次,設計一個算法,將它找出來;不用輔助存儲空
間,能否設計一個算法實現?
將1001個元素相加減去1,2,3,……1000數列的和,得到的差即爲重複的元素。
  int   Find(int   *   a)  
  {  
  int   i;//變量  
  for   (i   =   0   ;i<=1000;i++)  
  {  
  a[1000]   +=   a[i];  
  }  
  a[1000]   -=   (i*(i-1))/2       //i的值爲1001  
  return   a[1000];  
  }
利用下標與單元中所存儲的內容之間的特殊關係,進行遍歷訪問單元,一旦訪問過的單
元賦予一個標記,利用標記作爲發現重複數字的關鍵。代碼如下:
void FindRepeat(int array[], int length)
{
    int index=array[length-1]-1;
    while ( true )
    {
       if ( array[index]<0 )
           break;
       array[index]*=-1;
       index=array[index]*(-1)-1;
    }
 
    cout<<"The repeat number is "<<index+1<<endl;
}
此種方法不非常的不錯,而且它具有可擴展性。在罈子上有人提出:
對於一個既定的自然數 N ,有一個 N + M 個元素的數組,其中存放了小於等於 N 的所有
自然數,求重複出現的自然數序列{X} 。
 
對於這個擴展需要,自己在A_B_C_ABC(黃瓜兒才起蒂蒂)的算法的基礎上得到了自己的算法
代碼:
按照A_B_C_ABC(黃瓜兒才起蒂蒂)的算法,易經標記過的單元在後面一定不會再訪問到,除非它是重複的數字,也就是說只要每次將重複數字中的一個改爲靠近N+M的自然數,讓遍歷能訪問到數組後面的單元,就能將整個數組遍歷完。
代碼:
*/
void FindRepeat(int array[], int length, int num)
{
 int index=array[length-1]-1;
 cout<<"The repeat number is ";
 while ( true )
 {
 if ( array[index]<0 )
 {
   num--;
   array[index]=length-num;
   cout<<index+1<<'t';
 }
 
 if ( num==0 )
 {
   cout<<endl;
  return;
 }
 array[index]*=-1;
 index=array[index]*(-1)-1;
 }
}

 程序員面試題精選(37):判斷字符串是否是迴文字符串或者是否含有迴文字符子串
題目來自BMY BBS算法版,原題如下:
不僅能判斷規則的中心對稱,如123454321,還要能判斷如123456547890中的45654的不規則部分中心對稱
算法思想
從第一個字符開始,逐個掃描,對每一個字符,查找下一個相同字符,判斷這兩個字符之間的字符串是否迴文。時間複雜度O(n^3),所以說是笨笨解,師弟說可以做到O(n^2)...
算法實現
/*================================================================================
功能:判斷字符串是否是迴文字符串或者是否含有迴文字符子串
作者:sunnyrain
日期:2008-09-11
編譯環境:VC++6.0
==================================================================
#include<iostream>
using namespace std;
int find(char ch,char *str,size_t length) //查找str開始的字符串中第一個ch出現的位置,沒找到返回-1
{
        for(int i=0;i<length;i++)
        {
                if(ch == str[i])
                        return i;
        }
        return -1;
}
int isSym(char *str,size_t length)  //判斷一個字符串是否全對稱,0對稱,-1不對稱
{
        for(int i=0;i<length/2;i++)
        {
                if(str[i] != str[length-i-1])
                        return -1;
        }
        return 0;
}
int isSymmetry(char *str,size_t length)//返回0表示全部對稱,-1表示無對稱,其他值表示局部對稱起始位置
{
        int begin=0,end;
        char ch;
        for(int i=0;i<length;i++)
        {
                ch = str[i];
                begin = end = i;
                while((begin = find(ch,str+end+1,length-end-1)) != -1)
                {
                        end = begin+end+1;
                        if(isSym(str+i,end-i+1) == 0)
                                return i;
                }
        }
        return -1;
}
int main()
{
        char aa1[] = "123454321";
        char aa2[] = "123456547890";
        char aa3[] = "781234327891";
        char aa4[] = "954612313217891";
        cout<<isSymmetry(aa1,sizeof(aa1)/sizeof(char)-1)<<endl;
        cout<<isSymmetry(aa2,sizeof(aa2)/sizeof(char)-1)<<endl;
        cout<<isSymmetry(aa3,sizeof(aa3)/sizeof(char)-1)<<endl;
        cout<<isSymmetry(aa4,sizeof(aa4)/sizeof(char)-1)<<endl;
        return 0;
}

 程序員面試題精選(38):2008百度校園招聘的一道筆試題
題目大意如下:
一排N(最大1M)個正整數+1遞增,亂序排列,第一個不是最小的,把它換成-1,最小數爲a且未知求第一個被
-1替換掉的數原來的值,並分析算法複雜度。
解題思路:
一般稍微有點算法知識的人想想就會很容易給出以下解法:
設 Sn = a + (a+1) + (a+2) + .........+ (a+n-1) = na +n(n-1)/2
掃一次數組即可找到最小值a,時間複雜度O(n)
設 S = 修改第一項後所有數組項之和,  求和複雜度爲O(n)
則被替換掉的第一項爲  a1=Sn-S-1
總的時間複雜度爲 O(1)+O(n)+O(n) = O(n)
根據該算法寫出程序很簡單,就不寫了
主要是解題過程中沒有太考慮題目中給的1M這個數字,一面的時候被問到求和溢出怎麼辦?
當時我一想,如果要考慮溢出,必然是要處理大數問題,以前沒有看到大數就頭疼……所以立馬想了個繞過大數加法的方法,如下:
設定另外一個數組b[N]
用 a, a+1,a+2....a+n-1依次分別減去原數組,得到的差放在該數組裏,此求差過程複雜度爲O(n)
對該數組各項求和即可得到Sn-S
面試官讓證明一下我的設想,當時還沒有給我紙和筆,用手在桌子上比劃了一下沒想出來,回來躺在牀上想了一會就想出來了,也沒什麼難度: 
相減求和後的數組,最差情況下應該是連續n/2個負數或者正數相加,如果不溢出,後面正負混合相加的話肯定不會溢出;這種情況下的最差特殊情況就 是,原數列按照降序排列(除了第一項被替換掉了),而我們減時所用數列是增序排列。所得結果將是1個正數,n/2-1個負數,n/2個正數;而且我們相當 於用最大的n/2個數減去最小的n/2個數,差值之和最大,取到了最差情況,我們只考慮後面一半求和的情況即可(前面有個-1不方便處理):
S(n/2) = (n-1) + (n-3) + (n-5)+ .....+ 1    (n爲奇數時最後一項是0,不影響我們討論數量級計算溢出)
   = [(n-1)+1] * n/4 = n^2/4
題目中給定n最大爲1M = 1024*1024
那麼S(n/2)的最大量級爲1024^4 = 2^40
而long long類型爲64位,可以存放下該和,成功避免大數問題。
直接求和辦法,一是和可能溢出,二是面試官要求把原始數組改稱long long的話(即a可以也可能很大,求和時稍微加一下就會溢出)就得考慮大數求解了;而這種差值辦法可以直接消掉a,求和只和n相關,和a無關。
 
程序員面試題精選(39):一道autodesk筆試題求解

去年5月參加Autodesk實習生招聘時的算法題,當時時間緊,題量相當大,沒有來得及做這道題,之後也因爲懶一直沒有做,小崔的答案發給我也沒看,今天突然想起來,就做了一下,比想像的要簡單一些:)
/*================================================================================
題目:給定一個M×N矩陣,從左上角元素開始,按照順時針螺旋狀打印所有矩陣元素
關鍵點:遍歷過程的方向控制和左右界控制、上下界控制
作者:sunnyrain
日期:2007.9.3下午
運行環境:vc++ 6.0
==================================================================================*/
#include<iostream>
#include<vector>
using namespace std;
//定義方向
enum Direction{ToRight,Down,ToLeft,Up};
//改變方向函數
void turnDirect(Direction& cur)
{
 switch(cur)
 {
 case ToRight:
  cur = Down;
  break;
 case Down:
  cur = ToLeft;
  break;
 case ToLeft:
  cur = Up;
  break;
 case Up:
  cur = ToRight;
  break;
 }
}
//遍歷矩陣
void traverse(vector<vector<int> >& vv)
{
 int m = vv[0].size();
 int n = vv.size();
 int up=0,down=n,left=0,right=m;
 int i=0,j=0;
 Direction curD = ToRight;
 for(;up !=down || left != right;)
 {
  switch(curD)
  {
  case ToRight: //從左向右遍歷
   for(j=left;j<right;j++)
    cout<<vv[i][j]<<" ";
   cout<<endl;
   --j;     //列座標出界回退
   up++;    //遍歷完一行上界下移
   turnDirect(curD);  //改變遍歷方向
   break;
  case Down:   //從上向下遍歷
   for(i=up;i<down;i++)
    cout<<vv[i][j]<<" ";
   cout<<endl;
   --i;     //出界回退
   right--; //遍歷完一列右界左移
   turnDirect(curD);
   break;
  case ToLeft:  //從右向左遍歷
   for(j=right-1;j>=left;--j)
    cout<<vv[i][j]<<" ";
   cout<<endl;
   ++j;      //出界回退
   down--;   //下界上移
   turnDirect(curD);
   break;
  case Up:      //從下向上遍歷
   for(i=down-1;i>=up;--i)
    cout<<vv[i][j]<<" ";
   cout<<endl;
   ++i;      //出界回退
   left++;   //左界右移
   turnDirect(curD);
   break;
  }
 }
}
int main()
{
 int m,n,i,j,k=0;
 cout<<"please input the m and n of the matrix(m*n):"<<endl;
 cin>>m>>n;
 vector<vector<int> > vec(n);
 
 //初始化矩陣
 for(i=0;i<n;i++)
  for(j=0;j<m;j++)
   vec[i].push_back(++k);
    //正常遍歷矩陣
 for(i=0;i<n;i++)
 {
  for(j=0;j<m;j++)
   cout<<vec[i][j]<<"/t";
  cout<<endl;
 }
 //順時針螺旋遍歷矩陣
 traverse(vec);
 return 0;
}


 程序員面試題精選(40):一道SPSS筆試題求解
/*================================================================================
題目:輸入四個點的座標,求證四個點是不是一個矩形
關鍵點:
1.相鄰兩邊斜率之積等於-1,
2.矩形邊與座標系平行的情況下,斜率無窮大不能用積判斷。
3.輸入四點可能不按順序,需要對四點排序
作者:sunnyrain
日期:2007.9.2 & 2007.9.3
運行環境:vc++ 6.0
==================================================================================*/
#include<iostream>
#include<limits>
using namespace std;
const double MAX = numeric_limits<double>::max();  //斜率最大值
class Point
{
 float x;
 float y;
public:
 Point(float _x = 0, float _y = 0):x(_x),y(_y){}
 float getX() const
 {
  return x;
 }
 float getY() const
 {
  return y;
 }
 //爲點重新設置座標
 void set(float _x,float _y)
 {
  x = _x;
  y = _y;
 }
 //重載 == 成員操作符
 bool operator == (Point& p)
 {
  return (this->x == p.x && this->y == p.y);
 }
 //重載流插入操作符
 friend istream & operator >> (istream& is, Point & p)
 {
  return is>>p.x>>p.y;
 }
};
class Line  //兩點形成一條直線/線段
{
 Point start;
 Point end;
 double k;  //斜率
public:
 Line(){}
 Line(Point s,Point e):start(s),end(e)
 {
  if(start.getX() - end.getX() != 0)
   k = (start.getY() - end.getY())/(start.getX() - end.getX()) ;
  else
   k = MAX;  //兩點x座標相等則斜率無窮大
 }
 double getK()
 {
  return k;
 }
};
//查找數組pp中是否存在點p,是返回數組序號,否返回-1
int findPoint(Point *pp,int size,Point &p)
{
 for(int i=0;i<size;i++)
 {
  if(pp[i] == p)
   return i;
 }
 return -1;
}
//主函數
int main()
{
 Point p[4];
 Point *s[4];
 int i;
 for(i=0; i<4; i++)
 {
  cout<<"Please input the coordinates of the "<<i+1<<" point in format /"x.xx y.yy/""<<endl;
  cin>>p[i];
 }
 
/* p[0].set(0,0);
 p[1].set(1,1);
 p[2].set(2,0);
 p[3].set(1,-1);*/
 float left,up,right,down;

 //獲取四點x座標最大值和最小值
 left = p[0].getX(); //left爲四點x座標最小值
 right = p[0].getX(); //right爲四點x座標最大值
 for(i=1;i<4;i++)
 {
  if(left>p[i].getX())
   left = p[i].getX();
  if(right<p[i].getX())
   right = p[i].getX();
 }
 //獲取四點y座標最大值和最小值
 up = p[0].getY(); //up爲四點y座標最大值
 down = p[0].getY(); //四點y座標最小值
 for(i=1;i<4;i++)
 {
  if(up<p[i].getY())
   up = p[i].getY();
  if(down > p[i].getY())
   down = p[i].getY();
 }
 //判斷矩形與座標系平行情況
 Point P1(left,up),P2(right,up),P3(right,down),P4(left,down);
 if(findPoint(p,4,P1) != -1 && findPoint(p,4,P2) != -1 && findPoint(p,4,P3) != -1 && findPoint(p,4,P4) != -1)
 {
  cout<<"是矩形"<<endl;
  return 0;
 }
 //按照順時針方向對四點排序
 for(i=0;i<4;i++)
 {
  if(p[i].getX() == left)
   s[0] = &p[i];
  else if(p[i].getY() == up)
   s[1] = &p[i];
  else if(p[i].getX() == right)
   s[2] = &p[i];
  else if(p[i].getY() == down)
   s[3] = &p[i];
 }
 //排序後的四點順時針相連組成矩形邊
 Line one(*s[0],*s[1]),two(*s[1],*s[2]),three(*s[2],*s[3]),four(*s[3],*s[0]);
 cout<<"k1 = "<<one.getK()<<endl;
 cout<<"k2 = "<<two.getK()<<endl;
 cout<<"k3 = "<<three.getK()<<endl;
 cout<<"k4 = "<<four.getK()<<endl;
 //判斷相鄰邊斜率之積是否都等於0
 if(one.getK()*two.getK() == -1 || (one.getK() == 0 && two.getK() == MAX) || (one.getK() == MAX && two.getK() == 0) )
  if(two.getK()*three.getK() == -1 || (two.getK() == 0 && three.getK() == MAX) || (two.getK() == MAX && three.getK() == 0) )
   if(three.getK()*four.getK() == -1 || (three.getK() == 0 && four.getK() == MAX) || (three.getK() == MAX && four.getK() == 0) )
    if(four.getK()*one.getK() == -1 || (four.getK() == 0 && one.getK() == MAX) || (four.getK() == MAX && one.getK() == 0) )
    {
     cout<<"是矩形!"<<endl;
     return 0;
    }
 cout<<"不是矩形"<<endl;
 return 0;
}
 程序員面試題精選(41):編譯器對內存填充長度之誤解
看了《C++ 對像模型》的人,往往會誤以爲編譯器填充是按照計算機字長填充的,如下:
class A
{
   double a;
   char b;
};
sizeof(A) == ?
    不瞭解填充的人會以爲是9,看了c++對象模型的(像我)往往會以爲是12,昨晚看《程序員面試寶典》一道類似題,開始以爲答案給錯了。。今天一試才知道,原來我錯了。。上題答案(在編譯器默認情況下)是 16,VC6.0、MinGW、VS.net均如此。。
    《程序員面試寶典》上如是說:CPU的優化原則大致是這樣的:對於n字節的元素(n=2、4、8……)它的首地址能被n整除,才能獲得最好的性能。設計編譯器的時候可以遵循這個原則。也就是說,默認情況下,編譯器往往以最大的變量的長度爲填充長度,而不是按字節長度。當然也可以通過 #pragma pack(n) 指定編譯器的填充長度。這時候應該不是cpu的效率最高的情況了。
    另外有個網友討論說道如果一個類中含有另一個類對象,是否按照包含類的長度填充呢?試驗了一下,不是這樣,而是按照語言中的基本類型的最大長度填充。沒想到,面試題中也會考到這麼bt的題目,長見識了。
程序員面試題精選(42):約瑟夫問題的數學方法
問題描述:N個人圍成圓圈,從1開始報數,到第M個人令其出列,然後下一個人繼續從1開
始報數,到第M個人令其出列,如此下去,直到只剩一個人爲止。顯示最後一個人爲剩者。

無論是用鏈表實現還是用數組實現都有一個共同點:要模擬整個遊戲過程,不僅程序寫起
來比較煩,而且時間複雜
度高達O(nm),當n,m非常大(例如上百萬,上千萬)的時候,幾乎是沒有辦法在短時間內出
結果的。

爲了討論方便,先把問題稍微改變一下,並不影響原意:

問題描述:n個人(編號0~(n-1)),從0開始報數,報到(m-1)的退出,剩下的人繼續從0開
始報數。求勝利者的編號。

我們知道第一個人(編號一定是m%n-1) 出列之後,剩下的n-1個人組成了一個新的約瑟夫環
(以編號爲k=m%n的人開
始):
  k  k+1  k+2  ... n-2, n-1, 0, 1, 2, ... k-2
並且從k開始報0。

現在我們把他們的編號做一下轉換:
k     --> 0
k+1   --> 1
k+2   --> 2
...
...
k-2   --> n-2
k-1   --> n-1

變換後就完完全全成爲了(n-1)個人報數的子問題,假如我們知道這個子問題的解:例如x
是最終的勝利者,那麼根
據上面這個表把這個x變回去不剛好就是n個人情況的解嗎?!!變回去的公式很簡單,相
信大家都可以推出來:x'
=(x+k)%n

如何知道(n-1)個人報數的問題的解?對,只要知道(n-2)個人的解就行了。(n-2)個人的解
呢?當然是先求(n-3)的
情況 ---- 這顯然就是一個倒推問題!好了,思路出來了,下面寫遞推公式:

令f[i]表示i個人玩遊戲報m退出最後勝利者的編號,最後的結果自然是f[n]

遞推公式
f[1]=0;
f[i]=(f[i-1]+m)%i;  (i>1)

有了這個公式,我們要做的就是從1-n順序算出f[i]的數值,最後結果是f[n]。因爲實際生
活中編號總是從1開始,
我們輸出f[n]+1

由於是逐級遞推,不需要保存每個f[i],程序也是異常簡單:

#include <stdio.h>

main()
{
  int n, m, i, s=0;
  printf ("N M = "); scanf("%d%d", &n, &m);
  for (i=2; i<=n; i++) s=(s+m)%i;
  printf ("The winner is %d/n", s+1);
}

這個算法的時間複雜度爲O(n),相對於模擬算法已經有了很大的提高。算n,m等於一百萬
,一千萬的情況不是問題
了。
 
程序員面試題精選(43):數組中連續元素相加和最小的元素序列
題目描述:  有一個集合{14,56,53,4,-9,34,...n}裏面共n個數  
  裏面可以有負數也可以沒有  
  用一個時間複雜度爲o(n)的算法找出其中的一個連續串象(53,4,-9)   這樣(串裏的數字個數任意)  
  使得這個連續串爲所有這樣連續串裏各個數字相加和最小的一個 
代碼實現如下(程序沒有考慮有多組解的情況)
#include <iostream>
using namespace std;
template<typename T>
int getMinSum(T* a,int n,T* pbegin,T* pend)
{
 T   min   =   a[0];  
 T   sum   =   a[0];    
 T   tempbegin   =   0;  
 *pbegin   =   0;  
 *pend   =   0;  
 for   (int   i   =   1;   i   <   n;   i++)  
 {  
  if(sum   <   0)  
   sum   =   sum   +   a[i];  
  else  
  {  
   tempbegin   =   i;  
   sum   =   a[i];  
  }     
  if   (sum   <   min)  
  {  
   min   =   sum;      
   *pbegin   =   tempbegin;  
   *pend   =   i;  
  }   
 }  
 return   min;  
}
int main()
{
 int   a[]   =   {8,   9,   -3   ,-10   ,7   ,0   ,8   ,-12,   9,   8   ,-1   ,-2   ,9};  
   
 int   begin;  
 int   end;  
 int   sum;  
 int   k   =   sizeof(a)   /   sizeof(int);  
 sum=getMinSum(a,k,&begin,&end);  
 cout<<"The   min   sum   is "<<sum<<endl;  
 cout<<"And   the   begin   is  "<<begin<<",and   the   end   is  "<<end<<endl;  
   
 return   0;  
}
程序員面試題精選(44):整數分割(即求一個數N由小於等於N的數相加所得的所有組合)
題目描述:比如給定一整數4,其有如下情況:4=4;
                                                                                  4=3+1;
                                                                                  4=2+2;
                                                                                 4=2+1+1;
                                                                                 4=1+1+1+1;
下面便是兩種版本的分割實現代碼。
#include "stdio.h"
 int  Compute( int  number,  int  maximum)
  {
     if  (number  ==   1   ||  maximum  ==   1 )
         return   1 ;
     else   if  (number  <  maximum)
         return  Compute(number, number);
     else   if  (number  ==  maximum)
         return   1   +  Compute(number, maximum  -   1 );
     else
         return  Compute(number, maximum  -   1 )  +  Compute(number  -  maximum, maximum);
 }
 
 int  IntPartionNo( int  n)///////////////////求組合總數版本;
 {
     return  Compute(n, n);
}
 int  IntegerPartition( int  n)///////////////求組合總數並打印出所有情況版本;
 {
     int   * partition  =   new   int [n]();
     int   * repeat  =   new   int [n]();
 
  partition[ 1 ]  =  n;
  repeat[ 1 ]  =   1 ;
     int  count  =   1 ;
     int  part  =   1 ;
 
     int  last, smaller, remainder;
 
  printf( " %3d " , partition[ 1 ]);
     do 
  {
   last  =  (partition[part]  ==   1 )  ?  (repeat[part -- ]  +   1 ) :  1 ;
   smaller  =  partition[part]  -   1 ;
         if  (repeat[part]  !=   1 )
             -- repeat[part ++ ];
   partition[part]  =  smaller;
   repeat[part]  =   1   +  last  /  smaller;
  
         if  ((remainder  =  last  %  smaller)  !=   0 )
   {
    partition[ ++ part]  =  remainder;
    repeat[part]  =   1 ;
   }
  
         ++ count;
  
   printf( " /n " );
         for  ( int  i  =   1 ; i  <=  part;  ++ i)
             for  ( int  j  =   1 ; j  <=  repeat[i];  ++ j)
     printf( " %3d " , partition[i]);
   
  }   while (repeat[part]  !=  n);
 
     if  (partition)
  {
   delete [] partition;
   partition  =   0 ;
  }
     if  (repeat)
  {
   delete [] repeat;
   repeat  =   0 ;
  }
 
     return  count;
}
int main()
{
 printf("%d/n",IntPartionNo(4));
 IntegerPartition(4);
 getchar();
}
程序員面試題精選(45):求給定整數其二進制形式含1的個數
題目描述:求給定整數其二進制形式含1的個數,比如255含8個1,因其二進制表示爲11111111;
下面給出了兩種求解代碼實現。
#include <iostream>
using namespace std;
int  CountOne( int  n)
{
 int  count  =   0 ;
 while  (n)
 {
  ++ count;
        n  &=  n  -   1 ;
    }
 
 return  count;
}
int  CountOnesUsingTable(unsigned  int  i)
{
 static   int  BIT_TABLES[ 256 ]  = 
 {
  0 , 1 , 1 , 2 , 1 , 2 , 2 , 3 , 1 , 2 , 2 , 3 , 2 , 3 , 3 , 4
   , 1 , 2 , 2 , 3 , 2 , 3 , 3 , 4 , 2 , 3 , 3 , 4 , 3 , 4 , 4 , 5 
   , 1 , 2 , 2 , 3 , 2 , 3 , 3 , 4 , 2 , 3 , 3 , 4 , 3 , 4 , 4 , 5 
   , 2 , 3 , 3 , 4 , 3 , 4 , 4 , 5 , 3 , 4 , 4 , 5 , 4 , 5 , 5 , 6 
   , 1 , 2 , 2 , 3 , 2 , 3 , 3 , 4 , 2 , 3 , 3 , 4 , 3 , 4 , 4 , 5 
   , 2 , 3 , 3 , 4 , 3 , 4 , 4 , 5 , 3 , 4 , 4 , 5 , 4 , 5 , 5 , 6 
   , 2 , 3 , 3 , 4 , 3 , 4 , 4 , 5 , 3 , 4 , 4 , 5 , 4 , 5 , 5 , 6 
   , 3 , 4 , 4 , 5 , 4 , 5 , 5 , 6 , 4 , 5 , 5 , 6 , 5 , 6 , 6 , 7 
   , 1 , 2 , 2 , 3 , 2 , 3 , 3 , 4 , 2 , 3 , 3 , 4 , 3 , 4 , 4 , 5 
   , 2 , 3 , 3 , 4 , 3 , 4 , 4 , 5 , 3 , 4 , 4 , 5 , 4 , 5 , 5 , 6 
   , 2 , 3 , 3 , 4 , 3 , 4 , 4 , 5 , 3 , 4 , 4 , 5 , 4 , 5 , 5 , 6 
   , 3 , 4 , 4 , 5 , 4 , 5 , 5 , 6 , 4 , 5 , 5 , 6 , 5 , 6 , 6 , 7 
   , 2 , 3 , 3 , 4 , 3 , 4 , 4 , 5 , 3 , 4 , 4 , 5 , 4 , 5 , 5 , 6 
   , 3 , 4 , 4 , 5 , 4 , 5 , 5 , 6 , 4 , 5 , 5 , 6 , 5 , 6 , 6 , 7 
   , 3 , 4 , 4 , 5 , 4 , 5 , 5 , 6 , 4 , 5 , 5 , 6 , 5 , 6 , 6 , 7 
   , 4 , 5 , 5 , 6 , 5 , 6 , 6 , 7 , 5 , 6 , 6 , 7 , 6 , 7 , 7 , 8
    } ;
 
 return  BIT_TABLES[i  &   255 ]  +  BIT_TABLES[i >> 8   &   255 ]  + 
 
        BIT_TABLES[i >> 16   &   255 ]  +  BIT_TABLES[i >> 24   &   255 ];
}
int main()
{
 cout<<"158 has : "<<CountOne(158)<<endl;
 cout<<"158 has : "<<CountOnesUsingTable(158)<<endl;
 getchar();
}
打印內存小技巧收藏
新一篇: 程序員面試題精選(46):矩陣式螺旋輸出 | 舊一篇: 程序員面試題精選(45):求給定整數其二進制形式含1的個數
在學習C++的時候,由於編譯器揹着我們幹了太多的事兒,所以看看那些高級數據結構在彙編級別是怎麼樣的,在內存中是如何的,對編寫高效代碼很有幫助。
下邊是一個小函數,幫你打印內存中的內容。如果使用微軟的編譯器,各個內存對象之間可能會有byte guard,即編譯器會在分配的數據對象之間插入空白bytes,便於檢測破壞鄰接對象,所以和教科書上的連續分配內存有些差異,注意一下就行了
以下是實現加測試代碼:
#include  < cstdio >
struct Test{
 int a;
 char b;
};
void  ShowBytes( void *  s,  int  n)
{
    unsigned  char *  start  =  (unsigned  char * )s;
 
    printf( " [OFFSET] ADDRESS: VALUE/n/n " );
 
 for  ( int  i  =   0 ; i  <  n; i ++ )
 {
        printf( "  [%.4d] %.8X: %.2X/n " , i, start  +  i,  * (start  +  i));
 
  if  ((i  +   1 )  %   4   ==   0 )
  {
            printf( " ----------------------/n " );
        }
    } // for
 }
int main()
{
 Test *t;
 Test a={12,'A'};
 t=&a;
 ShowBytes(t,8);
 getchar();
 return 0;
}
 程序員面試題精選(53):刪除鏈表結點(時間複雜度爲O(1))
題目:給定鏈表的頭指針和一個結點指針,在O(1)時間刪除該結點。鏈表結點的定義如下:
struct ListNode
{
      int        m_nKey;
      ListNode*  m_pNext;
};
函數的聲明如下:
void DeleteNode(ListNode* pListHead, ListNode* pToBeDeleted);
分析:這是一道廣爲流傳的Google面試題,能有效考察我們的編程基本功,還能考察我們的反應速度,更重要的是,還能考察我們對時間複雜度的理解。
在鏈表中刪除一個結點,最常規的做法是從鏈表的頭結點開始,順序查找要刪除的結點,找到之後再刪除。由於需要順序查找,時間複雜度自然就是O(n) 了。
我們之所以需要從頭結點開始查找要刪除的結點, 是因爲我們需要得到要刪除的結點的前面一個結點。我們試着換一種思路。我們可以從給定的結點得到它的下一個結點。這個時候我們實際刪除的是它的下一個結 點,由於我們已經得到實際刪除的結點的前面一個結點,因此完全是可以實現的。當然,在刪除之前,我們需要需要把給定的結點的下一個結點的數據拷貝到給定的 結點中。此時,時間複雜度爲O(1)。
上面的思路還有一個問題:如果刪除的結點位於鏈表的尾部,沒有下一個結點,怎麼辦?我們仍然從鏈表的頭結點開始,順便遍歷得到給定結點的前序結點,並完成刪除操作。這個時候時間複雜度是O(n)。
那題目要求我們需要在O(1)時間完成刪除操 作,我們的算法是不是不符合要求?實際上,假設鏈表總共有n個結點,我們的算法在n-1總情況下時間複雜度是O(1),只有當給定的結點處於鏈表末尾的時 候,時間複雜度爲O(n)。那麼平均時間複雜度[(n-1)*O(1)+O(n)]/n,仍然爲O(1)。
基於前面的分析,我們不難寫出下面的代碼。
參考代碼:
///////////////////////////////////////////////////////////////////////
// Delete a node in a list
// Input: pListHead - the head of list
//        pToBeDeleted - the node to be deleted
///////////////////////////////////////////////////////////////////////
void DeleteNode(ListNode* pListHead, ListNode* pToBeDeleted)
{
      if(!pListHead || !pToBeDeleted)
            return;
 
      // if pToBeDeleted is not the last node in the list
      if(pToBeDeleted->m_pNext != NULL)
      {
            // copy data from the node next to pToBeDeleted
            ListNode* pNext = pToBeDeleted->m_pNext;
            pToBeDeleted->m_nKey = pNext->m_nKey;
            pToBeDeleted->m_pNext = pNext->m_pNext;
 
            // delete the node next to the pToBeDeleted
            delete pNext;
            pNext = NULL;
      }
      // if pToBeDeleted is the last node in the list
      else
      {
            // get the node prior to pToBeDeleted
            ListNode* pNode = pListHead;
            while(pNode->m_pNext != pToBeDeleted)
            {
                  pNode = pNode->m_pNext;            
            }
 
            // deleted pToBeDeleted
            pNode->m_pNext = NULL;
            delete pToBeDeleted;
            pToBeDeleted = NULL;
      }
}
值得注意的是,爲了讓代碼看起來簡潔一些,上面 的代碼基於兩個假設:(1)給定的結點的確在鏈表中;(2)給定的要刪除的結點不是鏈表的頭結點。不考慮第一個假設對代碼的魯棒性是有影響的。至於第二個 假設,當整個列表只有一個結點時,代碼會有問題。但這個假設不算很過分,因爲在有些鏈表的實現中,會創建一個虛擬的鏈表頭,並不是一個實際的鏈表結點。這 樣要刪除的結點就不可能是鏈表的頭結點了。當然,在面試中,我們可以把這些假設和面試官交流。這樣,面試官還是會覺得我們考慮問題很周到的。
 


程序員面試題精選(54):找出數組中兩個只出現一次的數字收藏
新一篇: 部分it公司的筆試小算法題精選 | 舊一篇: 程序員面試題精選(53):刪除鏈表結點(時間複雜度爲O(1))
題目:一個整型數組裏除了兩個數字之外,其他的數字都出現了兩次。請寫程序找出這兩個只出現一次的數字。要求時間複雜度是O(n),空間複雜度是O(1)。
分析:這是一道很新穎的關於位運算的面試題。
首先我們考慮這個問題的一個簡單版本:一個數組裏除了一個數字之外,其他的數字都出現了兩次。請寫程序找出這個只出現一次的數字。
這個題目的突破口在哪裏?題目爲什麼要強調有一個數字出現一次,其他的出現兩次?我們想到了異或運算的性質:任何一個數字異或它自己都等於0。也就是說,如果我們從頭到尾依次異或數組中的每一個數字,那麼最終的結果剛好是那個只出現依次的數字,因爲那些出現兩次的數字全部在異或中抵消掉了。
有了上面簡單問題的解決方案之後,我們回到原始的問題。如果能夠把原數組分爲兩個子數組。在每個子數組中,包含一個只出現一次的數字,而其他數字都出現兩次。如果能夠這樣拆分原數組,按照前面的辦法就是分別求出這兩個只出現一次的數字了。
我們還是從頭到尾依次異或數組中的每一個數字,那麼最終得到的結果就是兩個只出現一次的數字的異或結果。因爲其他數字都出現了兩次,在異或中全部抵消掉了。由於這兩個數字肯定不一樣,那麼這個異或結果肯定不爲0,也就是說在這個結果數字的二進制表示中至少就有一位爲1。我們在結果數字中找到第一個爲1的位的位置,記爲第N位。現在我們以第N位是不是1爲標準把原數組中的數字分成兩個子數組,第一個子數組中每個數字的第N位都爲1,而第二個子數組的每個數字的第N位都爲0。
現在我們已經把原數組分成了兩個子數組,每個子數組都包含一個只出現一次的數字,而其他數字都出現了兩次。因此到此爲止,所有的問題我們都已經解決。
代碼如下:
#include <iostream>
using namespace std;
///////////////////////////////////////////////////////////////////////
// Find the index of first bit which is 1 in num (assuming not 0)
///////////////////////////////////////////////////////////////////////
unsigned int FindFirstBitIs1(int num)
{
 
 int indexBit = 0;
 
 while (((num & 1) == 0) && (indexBit < 32))
 
 {
 
  num = num >> 1;
 
  ++ indexBit;
 
 }
 
 
 
 return indexBit;
 
}
///////////////////////////////////////////////////////////////////////
// Is the indexBit bit of num 1?
///////////////////////////////////////////////////////////////////////
bool IsBit1(int num, unsigned int indexBit)
{
 
 num = num >> indexBit;
 
 
 
 return (num & 1);
 
}
///////////////////////////////////////////////////////////////////////
// Find two numbers which only appear once in an array
// Input: data - an array contains two number appearing exactly once,
//               while others appearing exactly twice
//        length - the length of data
// Output: num1 - the first number appearing once in data
//         num2 - the second number appearing once in data
///////////////////////////////////////////////////////////////////////
void FindNumsAppearOnce(int data[], int length, int &num1, int &num2)
{
      if (length < 2)
            return;
 
      // get num1 ^ num2
      int resultExclusiveOR = 0;
      for (int i = 0; i < length; ++ i)
            resultExclusiveOR ^= data[i];
 
      // get index of the first bit, which is 1 in resultExclusiveOR
      unsigned int indexOf1 = FindFirstBitIs1(resultExclusiveOR);
 
      num1 = num2 = 0;
      for (int j = 0; j < length; ++ j)
      {
            // divide the numbers in data into two groups,
            // the indexOf1 bit of numbers in the first group is 1,
            // while in the second group is 0
            if(IsBit1(data[j], indexOf1))
                  num1 ^= data[j];
            else
                  num2 ^= data[j];
      }
}
 
int main()
{
 int a[8]={2,3,6,8,3,2,7,7};
 int x,y;
 FindNumsAppearOnce(a,8,x,y);
 cout<<x<<"/t"<<y;
 getchar();
}
 一道百度面試題題解
問: A廠有1萬個工人,編號0-9999,( EE[10000] ), 1個廠長( GG )分派任務, 1個監工( MM )管理工人.廠子忙的時間不確定,可能突然很忙,1天接到任務5000多個,1個任務只能分配給1個工人做, 也可能好幾十天沒新任務.廠長分配任務給這1萬個工人幹,按工人編號一個一個來,到最後一個工人就又從頭開始,任務完成時間各不相同,可能一個工人在分配 任務的時候手裏還有任務, 就得換下一個。

  但是這1萬個工人都很懶,領到了任務先不做,需要監工1個1個去問,如果工人有任務,就 做,如果工人沒任務,則不做。廠長只管分任務,1個1個來,可能幾天也沒新任務,不累; 但是監工很累,監工每天都要看所有工人的情況,即使這些工人都沒有任務, 實際上每天工人(80%左右)是沒任務的,請問,怎麼讓監工的工作輕鬆下來. 比如說每天只問1小半工人.

  Peak Wong:

  分析如下:

  因爲“任務完成時間各不相同” ,所以有可能a,b,c某天都有任務,但b的任務最先完成,那麼當b的任務完成後,有任務的人的工號可能是不連續的;

  用一個數組表示1萬個工人是否有任務,並保存最後被分配任務的人的工號;

  1)從前一天“最後被分配任務的人的工號”開始,依次問下一個工號的人,置對應的工作狀態,直到碰到前一天無工作,且當天也無工作的人; 並更新當步最後有工作的人的工號爲當天的“最後被分配任務的人的工號”;

  2)從前一天“最後被分配任務的人的工號”開始,依次問上一個工號且前一天有工作的人;

  問題是監工可以知道那些信息,否則還不是一個一個接着去問。
  還有就是tailzhou的步驟1消耗的時間T1, 工人完成的時間T2,如果T2

  所以很多條件都沒有限制。

  可是仔細想想,就是監工要記錄所有工人的工作狀態,然後每天只查詢在工作的工人就可以了。(並且記錄誰還在工作中)

  其實最根本的解決之道是在每次廠長分配任務時,監工也被通知誰被分配了任務。而現在題目的假設是廠長太忙了,來不及通知監工(其實讓監工分配就行了)。

  解決這個問題很簡單,監工只要記錄(也可能是他猜測)上次廠長分配任務最後分配到那個人就可以了。

   然後每天查詢時,除了監督前一天來在工作的人外,還要查看從上次分配到任務的編號的下一個編號開始的工人,向後依次查詢,知道遇到一個沒有被分配工作的 人(這個工人後面不會再有人被分配任務了).同時監工還要記錄或猜測哪個工人是當天最後被分配任務的。在“查看從上次分配到任務的編號的下一個編號開始的 工人,向後依次查詢”過程中,如果工人的話可信,詢問他們就可以了。如果不可信,那麼可以猜測爲最後一個昨天空閒,今天忙的工人就可以了。

  其實,監工既然可以監督,他總得有渠道知道當天那個工人還有活要做(不然所有工人都可以說今天我沒有任務呀),所以沒有這麼複雜的問題。

  解決問題:

  有一問:監工是不是問了以後,工人就會一直把工作做完?

  如果這樣的話,最簡單的一個考慮,他第一次問的時候記住這個工人今天能否做完,不能做完的話,哪天才能做完。

  監工實際上只需要在工人做完了以後的第二天去問就可以了。因爲不做完,廠長不會給他分新任務。

  但問題說:但是監工很累,監工每天都要看所有工人的情況,即使這些工人都沒有任務那又不一樣了,就是說監工必須每天問,不然工人不會開始工作。

  這時候就是沒有工作的工人不需要問。

  首先工作隊列,每個人問一遍,今天能做完的移到空閒隊列。

  空閒隊列,二分查找,找到非空閒的,往前處理完,今天能做完的不動,不能做完的移到工作隊列。

  很簡單的問題阿。二分查找需要問的人非常少的。

  絕對能滿足樓主說的:比如說每天只問1小半工人,用我的方法廠長分任務的時候不需要通知監工。

  實際上通知也沒有意義,因爲是按順序分配,監工實際上需要知道的是今天有多少新任務。

  就算不知道,用二分查找10000人也最多需要10幾次。

  這10幾次,再根據實際上每天工人(80%左右)是沒任務的,所以實際上每天問的人只有20%左右。

  1.問題分析

   現在的情況是監工每天要查看所有的工人,催他們工作,因爲不催他們不開工,即要訪問EE[10000]的每個元素一次。目標是每天只問一小半的工人,實 際上沒有工作的工人是不需要問的,最理想的情況就是監工只問有工作的工人,或者儘可能少地問沒有工作的工人,即要儘可能少地訪問EE[10000]的元 素。

  怎麼辦呢?監工想了一個辦法,他做了一萬張卡片,每張卡片上寫着工人的編號,從0-9999,恰好和數組EE[10000]的下標對應。

   監工拿着他的祕密武器上陣了,0號,有工作沒?沒有。好,放右邊口袋。1號,有工作沒?有。今天能幹完嗎?能。好,放右邊口袋(並且放在0號後面)。2 號,有工作沒?有,今天能幹完嗎?不能。嗯,放左邊。3號,沒有。放右邊(1號後面)。4號,今天干不完,放左邊。……第二天,先看右邊(昨天沒事的), 0號,有工作沒,有,今天能幹完嗎,能,好,不動。1號有工作沒,有,幹不完,好,放左邊(接着昨天后面放)。3號,沒有,哦,廠長GG還沒有分配到這裏 阿,那明天檢查空的從這裏開始就可以了(記住),但今天仍然輕鬆不了,因爲可能是從後面的號碼過來,並且分配到前面來了。右邊全部查完了。再查左邊。2 號,今天能幹完,放右邊(並且放在0號後面)。阿,又碰到了1號了,今天的檢查結束。第三天,總算可以輕鬆了。從3號開始,先查右邊。今天做得完,不動, 做不完,移到左邊後面,碰到沒有任務的,或者碰到3號,右邊檢查完。再看左邊,做得完的,移到右邊,並且按順序插入其它卡片中間,做不完的,不動,直到碰 到今天新加入的做不完的,或者整個左邊的卡片都檢查完。

  說了這麼多,實際上就是左邊的是工作的隊列,右邊的是沒工作的隊列,左邊和右 邊的區別就是右邊的要保持按編號排序(因爲廠長GG分配任務是按順序分的)。再拿支鉛筆,在有的卡片上做做記號。工作輕鬆不少。以上都是假設工人必須每天 催纔會工作,並且監工每天都是在廠長分配任務之後纔去催。如果工人催一次就會一直工作,那麼簡單,監工只需要在卡片上記下還要幾天纔去問就行了。如果監工 是每天早上去催,廠長去可能在清早(問之前)或下午(問之後,但工人還沒工作完)去分任務。

  如果是前者,當然沒問題,如果是後者問題來了,因爲3號是今天才能完工的,但也是放在右邊,如果廠長剛好分了2號和4號,那麼按上面的邏輯,4號就催不到了。

  所以爲了避免這種情況,當天能完的還不能放在空閒裏。嗯,只要再準備一個口袋就行了。好在監工的衣服上面有兩個口袋,下面也有兩個,用了下面兩個,上面還有兩個沒有被使用。拿一個來用就行了。

   檢查下面的左右口袋時,凡是當天能做完的,都放在左上。OK,先右下,再左下,當天能做完的這會兒放右上,再把左上的按順序插入右下。應該沒問題了吧, 不管廠長何時分任務,監工只需要看自己的口袋就可以了。右下需要訪問的是從昨天訪問的沒工作的開始,再到一個新的沒工作的。左下都要訪問。右上或左上的卡 片只需要整理。因爲實際上每天工人(80%左右)是沒任務的,都在右下,並且也可能好幾十天沒新任務,這下輕鬆了,只用問小部分工人就能保證工作正常進行 了。

  好了,如果想先模擬一遍怎麼辦,用大腦模擬,10000人太多想不過來,累。寫個程序吧。要寫程序得先有算法。

  2.算法

  以下算法模擬最一般的情況,即監工不知道廠長何時分任務,廠長在一天的任何時候都可以分任務,工人每天都要問才工作,監工只在早上去催(爲了簡化工時的計算,即工時以天爲單位)。

  完全的隨機起點模擬,即此方法可從任何監工想要採用此方法的時間點開始。

  假設一個任務完成的時間是1-N天,廠子一天接到的新任務數是0-M個。用T分鐘模擬一天。定時器是精度毫秒級。

  A.用0-N之間的數初始化EE[10000],模擬當前工人的工作狀態。EE[i]表示工人還要多少天完成這個任務。EE[i]=0,表示沒任務。

  B.設置定時器,廠長分任務定時器爲1-T*60*1000毫秒之間某個時間,監工定時器設爲T*60*1000毫秒。

  C.廠長定時器到,廠長分任務。用c記錄廠長從哪裏開始。第一次時有個隨機初始化的工程,隨機一個0-9999之間的數,然後找到第一個EE[i]=0的i,從這個c=i開始分配。

  隨機產生0-M,如果=0,則c=i不變,如果是1-M之間的值,則一個個查,碰到EE[i]=0的,給他隨機一個1-N的值,直到分完這些任務,並且c=最後分到的+1。這個題目是研究監工的問題是,廠長比較輕鬆,下次讓他繼續從這裏找下去。

   重新設定廠長定時器,定時爲T*60*1000-上次定的時,再加上1-T*60*1000毫秒之間某個時間,因爲廠長也只會一天分一次,所以先要把今 天的時間用完,再加上下一天的某個時間,(從前面可以看出,廠長的定時器設成T也是一樣的,只要考慮一下訪問共享數據的問題,這裏先不考慮這個問題)。

  D.監工定時器到,監工問工人。

  新建四個鏈表。a(今天還不能做完的),b(沒有工作的),c(今天可以做完的),d(今天可以做完的),初始化爲空。但b爲有序鏈表。c和d輪流使用。

  第一次定時到,訪問EE[10000],今天不能做完的(EE[i]> 1)接到a的尾巴上,能做完的(EE[i]=1)接到c的尾巴上,沒有工作的(EE[i]=0)接b,d空。

  第二次定時到,訪問b,今天不能做完的接到a的尾巴上,能做完的接到d的尾巴上,並且記錄出現有工作的人後再出現沒有工作的人的結點指針p。如果沒有這樣的人,那就是鏈表第一個人或者爲空(大家都有工作)。但不管怎樣必須把整個鏈表b訪問一遍。

  訪問a,不能做完的不動,能做完的接到d的尾巴上,最後將c中的元素插入b。注意,鏈表中元素唯一,也就是說移到另一個鏈表的時候,也意味着從原鏈表刪除。

  第三次定時到,從p開始訪問b中的元素,今天不能做完的接到a的尾巴上,能做完的接到c的尾巴上,直到找到一個沒有工作的或者已經全部找了一遍(找 的時候調整p到新的位置)。到鏈表最後一個後,可能要從頭找。

  訪問a,不能做完的不動,能做完的接到c的尾巴上,最後將d中的元素插入b。

  第四次定時到,和第3次類似,只是c和d的位置對調了一下。到這時,監工的工作已經輕鬆了,整個系統將按這個新的方式一直運行下去。

  監工的定時器不需要重新設置。

  a,b,c,d中的元素內容爲工人編號,訪問時語法類似if(EE[p-> index]> 1)。

 騰訊部分筆試面試題
1、請定義一個宏,比較兩個數a、b的大小,不能使用大於、小於、if語句
2、如何輸出源文件的標題和目前執行行的行數
3、兩個數相乘,小數點後位數沒有限制,請寫一個高精度算法
4、寫一個病毒
5、有A、B、C、D四個人,要在夜裏過一座橋。他們通過這座橋分別需要耗時1、2、5、10分鐘,只有一支手電,並且同時最多隻能兩個人一起過橋。請問,如何安排,能夠在17分鐘內這四個人都過橋?

2008年騰訊招聘
選擇題(60)
c/c++ os linux 方面的基礎知識 c的Sizeof函數有好幾個!
程序填空(40)
1.(20) 4空x5
不使用額外空間,將 A,B兩鏈表的元素交叉歸併
2.(20) 4空x5
MFC 將樹序列化 轉存在數組或 鏈表中!

 

1, 計算 a^b << 2 (運算符優先級問題)

2 根據先序中序求後序

3 a[3][4]哪個不能表示 a[1][1]: *(&a[0][0]) *(*(a+1)+1) *(&a[1]+1) *(&a[0][0]+4)

4 for(int i...)
for(int j...)
printf(i,j);
printf(j)
會出現什麼問題

5 for(i=0;i<10;++i,sum+=i);的運行結果

6 10個數順序插入查找二叉樹,元素62的比較次數

7 10個數放入模10hash鏈表,最大長度是多少

8 fun((exp1,exp2),(exp3,exp4,exp5))有幾個實參

9 希爾 冒泡 快速 插入 哪個平均速度最快

10 二分查找是 順序存儲 鏈存儲 按value有序中的哪些

11 順序查找的平均時間

12 *p=NULL *p=new char[100] sizeof(p)各爲多少

13 頻繁的插入刪除操作使用什麼結構比較合適,鏈表還是數組

14 enum的聲明方式

15 1-20的兩個數把和告訴A,積告訴B,A說不知道是多少,

B也說不知道,這時A說我知道了,B接着說我也知道了,問這兩個數是多少


大題:

1 把字符串轉換爲小寫,不成功返回NULL,成功返回新串

char* toLower(char* sSrcStr)
{
char* sDest= NULL;
if( __1___)
{
int j;
sLen = strlen(sSrcStr);
sDest = new [_______2_____];
if(*sDest == NULL)
return NULL;
sDest[sLen] = '/0';
while(_____3____)
sDest[sLen] = toLowerChar(sSrcStr[sLen]);
}
return sDest;
}

2 把字符串轉換爲整數 例如:"-123" -> -123

main()
{
.....
if( *string == '-' )
n = ____1______;
else
n = num(string);
.....
}

int num(char* string)
{
for(;!(*string==0);string++)
{
int k;
k = __2_____;
j = --sLen;
while( __3__)
k = k * 10;
num = num + k;
}
return num;
}

附加題:

1 linux下調試core的命令,察看堆棧狀態命令
2 寫出socks套接字 服務端 客戶端 通訊程序
3 填空補全程序,按照我的理解是添入:win32調入dll的函數名
查找函數入口的函數名 找到函數的調用形式
把formView加到singledoc的聲明 將singledoc加到app的聲明

4 有關係 s(sno,sname) c(cno,cname) sc(sno,cno,grade)
1 問上課程 "db"的學生no
2 成績最高的學生號
3 每科大於90分的人數
 
 
主要是c/c++、數據結構、操作系統等方面的基礎知識。好像有sizeof、樹等選擇題。填空題是補充完整程序。附加題有寫算法的、編程的、數據庫sql語句查詢的。還有一張開放性問題。
請定義一個宏,比較兩個數a、b的大小,不能使用大於、小於、if語句
#define Max(a,b) ( a/b)?a:b
如何輸出源文件的標題和目前執行行的行數
int line = __LINE__;
char *file = __FILE__;
cout<<"file name is "<<(file)<<",line is "<<line<<endl;
兩個數相乘,小數點後位數沒有限制,請寫一個高精度算法

寫一個病毒
while (1)
        {
               int *p = new int[10000000];
        }

 不使用額外空間,將 A,B兩鏈表的元素交叉歸併
將樹序列化 轉存在數組或 鏈表中
struct st{
 int i;
 short s;
 char c;
};
sizeof(struct st);
8
   char * p1;
   void * p2;
   int p3;
   char p4[10];
   sizeof(p1...p4) =?
4,4,4,10
二分查找
快速排序
雙向鏈表的刪除結點
 
 
有12個小球,外形相同,其中一個小球的質量與其他11個不同
給一個天平,問如何用3次把這個小球找出來
並且求出這個小球是比其他的輕還是重
解答:
哈哈,據說這是微軟前幾年的一個面試題。很經典滴啊!三次一定能求出來,而且能確定是重還是輕。
數據結構的知識還沒怎麼學透,不過這個題我到是自己研究過,可以分析下。
將12個球分別編號爲a1,a2,a3.......a10,a11,a12.
第一步:將12球分開3撥,每撥4個,a1~a4第一撥,記爲b1, a5~a6第2撥,記爲b2,其餘第3撥,記爲b3;
第二步:將b1和b2放到天平兩盤上,記左盤爲c1,右爲c2;這時候分兩中情況:

1.c1和c2平衡,此時可以確定從a1到a8都是常球;然後把c2拿空,並從c1上拿下a4,從a9到a12四球裏隨便取三球,假設爲a9到a11,放到c2上。此時c1上是a1到a3,c2上是a9到a11。從這裏又分三種情況:
     A:天平平衡,很簡單,說明沒有放上去的a12就是異球,而到此步一共稱了兩次,所以將a12隨便跟11個常球再稱一次,也就是第三次,馬上就可以確定a12是重還是輕;
     B: 若c1上升,則這次稱說明異球爲a9到a11三球中的一個,而且是比常球重。取下c1所有的球,並將a8放到c1上,將a9取下,比較a8和a11(第三 次稱),如果平衡則說明從c2上取下的a9是偏重異球,如果不平衡,則偏向哪盤則哪盤裏放的就是偏重異球;
     C:若c1下降,說明a9到a11裏有一個是偏輕異球。次種情況和B類似,所以接下來的步驟照搬B就是;

2.c1和c2不平衡,這時候又分兩種情況,c1上升和c1下降,但是不管哪種情況都能說明a9到a12是常球。這步是解題的關鍵。也是這個題最妙的地方。
     A:c1上升,此時不能判斷異球在哪盤也不能判斷是輕還是重。取下c1中的a2到a4三球放一邊,將c2中的a5和a6放到c1上,然後將常球a9放到c2上。至此,c1上是a1,a5和a6,c2上是a7,a8和a9。此時又分三中情況:
         1) 如果平衡,說明天平上所有的球都是常球,異球在從c1上取下a2到a4中。而且可以斷定異球輕重。因爲a5到a8都是常球,而第2次稱的時候c1是上升 的,所以a2到a4裏必然有一個輕球。那麼第三次稱就用來從a2到a4中找到輕球。這很簡單,隨便拿兩球放到c1和c2,平衡則剩餘的爲要找球,不平衡則 哪邊低則哪個爲要找球;
         2)c1仍然保持上升,則說明要麼a1是要找的輕球, 要麼a7和a8兩球中有一個是重球(這步懂吧?好好想想,很簡單的。因爲a9是常球,而取下的a2到a4肯定也是常球,還可以推出換盤放置的a5和a6也 是常球。所以要麼a1輕,要麼a7或a8重)。至此,還剩一次稱的機會。只需把a7和a8放上兩盤,平衡則說明a1是要找的偏輕異球,如果不平衡,則哪邊 高說明哪個是偏重異球;
         3)如果換球稱第2次後天平平衡打破,並且c1降低了,這說明異球肯定在換過來的a5和a6兩求中,並且異球偏重,否則天平要麼平衡要麼保持c1上升。確定要找球是偏重之後,將a5和a6放到兩盤上稱第3次根據哪邊高可以判定a5和a6哪個是重球;
     B: 第1次稱後c1是下降的,此時可以將c1看成c2,其實以後的步驟都同A,所以就不必要再重複敘述了。至此,不管情況如何,用且只用三次就能稱出12個外 觀手感一模一樣的小球中有質量不同於其他11球的偏常的球。而且在稱的過程中可以判定其是偏輕還是偏重。
 
給一個奇數階N幻方,填入數字1,2,3...N*N,使得橫豎斜方向上的和都相同
答案:
#include<iostream>
#include<iomanip>
#include<cmath>
usingnamespace std;
int main()
{
 int n;
 cin>>n;
 int i;
 int **Matr=newint*[n];//動態分配二維數組
 for(i=0;i<n;++i)
      Matr[ i ]=newint[n];//動態分配二維數組
 //j=n/2代表首行中間數作爲起點,即1所在位置
 int j=n/2,num=1;//初始值
 i=0;
 while(num!=n*n+1)
 {
//往右上角延升,若超出則用%轉移到左下角
      Matr[(i%n+n)%n][(j%n+n)%n]=num;
    //斜行的長度和n是相等的,超出則轉至下一斜行
    if(num%n==0)
          i++;
   else
      {
          i--;
          j++;
      }
      num++;
 }
 for(i=0;i<n;i++)
 {
      for(j=0;j<n;++j)
         cout<<setw((int)log10(n*n)+4)<<Matr[ i][ j ];//格式控制
      cout<<endl<<endl;//格式控制
 }
for(i=0;i<n;++i)
      delete [ ]Matr[ i ];
return1;
}
 
騰訊的一道面試題:(與百度相似,可惜昨天百度死在這方面了)////
在一個文件中有 10G 個整數,亂序排列,要求找出中位數。內存限制爲 2G。只寫出思路即可。
答案:
1, 把整數分成256M段,每段可以用64位整數保存該段數據個數,256M*8 = 2G內存,先清0
2,讀10G整數,把整數映射到256M段中,增加相應段的記數
3,掃描256M段的記數,找到中位數的段和中位數的段前面所有段的記數,可以把其他段的內存釋放
4,因中位數段的可能整數取值已經比較小(如果是32bit整數,當然如果是64bit整數的話,可以再次分段),對每個整數做一個記數,再讀一次10G整數,只讀取中位數段對應的整數,並設置記數。
5,對新的記數掃描一次,即可找到中位數。
如果是32bit整數,讀10G整數2次,掃描256M記數一次,後一次記數因數量很小,可以忽略不記
(設是32bit整數,按無符號整數處理
整數分成256M段? 整數範圍是0 - 2^32 - 1 一共有4G種取值,4G/256M = 16,每16個數算一段 0-15是1段,16-31是一段,...
整數映射到256M段中? 如果整數是0-15,則增加第一段記數,如果整數是16-31,則增加第二段記數,...

其實可以不用分256M段,可以分的段數少一寫,這樣在掃描記數段時會快一些,還能節省一些內存)
 
騰訊題二:
一個文件中有40億個整數,每個整數爲四個字節,內存爲1GB,寫出一個算法:求出這個文件裏的整數裏不包含的一個整數
答:
方法一: 4個字節表示的整數,總共只有2^32約等於4G個可能。
爲了簡單起見,可以假設都是無符號整數。
分配500MB內存,每一bit代表一個整數,剛好可以表示完4個字節的整數,初始值爲0。基本思想每讀入一個數,就把它對應的bit位置爲1,處理完40G個數後,對500M的內存遍歷,找出一個bit爲0的位,輸出對應的整數就是未出現的。
算法流程:
1)分配500MB內存buf,初始化爲0
2)unsigned int x=0x1;
   for each int j in file
   buf=buf |x < <j;
   end
(3) for(unsigned int i=0; i  <= 0xffffffff; i++)
       if (!(buf & x < <i))
       {
           output(i);
           break;
       }
以上只是針對無符號的,有符號的整數可以依此類推。
 
方法二:
文件可以分段讀啊,這個是O(2n)算法,應該是很快的了,而且空間也允許的。
不過還可以構造更快的方法的,更快的方法主要是針對定位輸出的整數優化算法。
思路大概是這樣的,把值空間等分成若干個值段,比如值爲無符號數,則
00000000H-00000FFFH
00001000H-00001FFFH
......
0000F000H-0000FFFFH
.....
FFFFF000H-FFFFFFFFH
這樣可以訂立一個規則,在一個值段範圍內的數第一次出現時,對應值段指示值Xn=Xn+1,如果該值段的所有整數都出現過,則Xn=1000H,這樣後面輸出定位時就可以直接跳過這個值段了,因爲題目僅僅要求輸出一個,這樣可以大大減少後面對標誌數值的遍歷步驟。
理論上值段的劃分有一定的算法可以快速的實現,比如利用位運算直接定位值段對應值進行計算。
騰訊面試題:
有1到10w這10w個數,去除2個並打亂次序,如何找出那兩個數。(不準用位圖!!)
位圖解決:
  位圖的方法如下
假設待處理數組爲A[10w-2]
定義一個數組B[10w],這裏假設B中每個元素佔用1比特,並初始化爲全0
for(i=0;i <10w-2;i++)
{
 B[ A[i] ]=1
}
那麼B中不爲零的元素即爲缺少的數據
這種方法的效率非常高,是計算機中最常用的算法之一
其它方法:
    求和以及平方和可以得到結果,不過可能求平方和運算量比較大(用64位int不會溢出)
 
騰訊面試題:
騰訊服務器每秒有2w個QQ號同時上線,找出5min內重新登入的qq號並打印出來。
 
解答: 第二題如果空間足夠大,可以定義一個大的數組
a[qq號],初始爲零,然後這個qq號登陸了就a[qq號]++
最後統計大於等於2的QQ號
這個用空間來代替時間
 
第二個題目,有不成熟的想法。
2w x 300s
所以用 6,000,000 個桶。刪除超時的算法後面說,所以平均桶的大小是 1 。
假設 qq 號碼一共有 10^10 個,所以每個桶裝的 q 號碼是 10^10 / (6 * 10^6) 個,這個是插入時候的最壞效率(插入同一個桶的時候是順序查找插入位置的)。
qq的節點結構和上面大家討論的基本一樣,增加一個指針指向輸出列表,後面說。
struct QQstruct {
  num_type   qqnum;
  timestamp  last_logon_time;
  QQstruct   *pre;
  QQstruct   *next;
  OutPutList *out;    // 用於 free 節點的時候,順便更新一下輸出列表。
}

另外增加兩個指針列表。
第一個大小 300 的循環鏈表,自帶一個指向 QQStruct 的域,循環存 300 秒內的qq指針。時間一過
就 free 掉, 所以保證所有桶佔用的空間在 2w X 300 以內。
第二個是 輸出列表, 就是存放題目需要輸出的節點。
如果登陸的用戶,5分鐘內完全沒有重複的話,每秒 free 掉 2w 個節點。
不過在 free 的時候,要判斷一下時間是不是真的超時,因爲把節點入桶的時候,遇到重複的,會更
新一下最後登陸的時間。當然啦,這個時候,要把這個 qq 號碼放到需要輸出的列表裏面。
   程序員面試題精選(56):找出兩個鏈表的第一個公共結點收藏
新一篇: 1到n之間1的個數 | 舊一篇: vs下三個比較實用方便的小技巧
 
題目:兩個單向鏈表,找出它們的第一個公共結點。
鏈表的結點定義爲:
struct ListNode
{
      int        m_nKey;
      ListNode*   m_pNext;
};
分析:這是一道微軟的面試題。微軟非常喜歡與鏈表相關的題目,因此在微軟的面試題中,鏈表出現的概率相當高。
如果兩個單向鏈表有公共的結點,也就是說兩個鏈 表從某一結點開始,它們的m_pNext都指向同一個結點。但由於是單向鏈表的結點,每個結點只有一個m_pNext,因此從第一個公共結點開始,之後它 們所有結點都是重合的,不可能再出現分叉。所以,兩個有公共結點而部分重合的鏈表,拓撲形狀看起來像一個Y,而不可能像X。
看到這個題目,第一反應就是蠻力法:在第一鏈表 上順序遍歷每個結點。每遍歷一個結點的時候,在第二個鏈表上順序遍歷每個結點。如果此時兩個鏈表上的結點是一樣的,說明此時兩個鏈表重合,於是找到了它們 的公共結點。如果第一個鏈表的長度爲m,第二個鏈表的長度爲n,顯然,該方法的時間複雜度爲O(mn)。
接下來我們試着去尋找一個線性時間複雜度的算 法。我們先把問題簡化:如何判斷兩個單向鏈表有沒有公共結點?前面已經提到,如果兩個鏈表有一個公共結點,那麼該公共結點之後的所有結點都是重合的。那 麼,它們的最後一個結點必然是重合的。因此,我們判斷兩個鏈表是不是有重合的部分,只要分別遍歷兩個鏈表到最後一個結點。如果兩個尾結點是一樣的,說明它 們用重合;否則兩個鏈表沒有公共的結點。
在上面的思路中,順序遍歷兩個鏈表到尾結點的時 候,我們不能保證在兩個鏈表上同時到達尾結點。這是因爲兩個鏈表不一定長度一樣。但如果假設一個鏈表比另一個長l個結點,我們先在長的鏈表上遍歷l個結 點,之後再同步遍歷,這個時候我們就能保證同時到達最後一個結點了。由於兩個鏈表從第一個公共結點考試到鏈表的尾結點,這一部分是重合的。因此,它們肯定 也是同時到達第一公共結點的。於是在遍歷中,第一個相同的結點就是第一個公共的結點。
在這個思路中,我們先要分別遍歷兩個鏈表得到它們的長度,並求出兩個長度之差。在長的鏈表上先遍歷若干次之後,再同步遍歷兩個鏈表,知道找到相同的結點,或者一直到鏈表結束。此時,如果第一個鏈表的長度爲m,第二個鏈表的長度爲n,該方法的時間複雜度爲O(m+n)。
基於這個思路,我們不難寫出如下的代碼:
///////////////////////////////////////////////////////////////////////
// Find the first common node in the list with head pHead1 and
// the list with head pHead2
// Input: pHead1 - the head of the first list
//        pHead2 - the head of the second list
// Return: the first common node in two list. If there is no common
//         nodes, return NULL
///////////////////////////////////////////////////////////////////////
ListNode* FindFirstCommonNode( ListNode *pHead1, ListNode *pHead2)
{
      // Get the length of two lists
      unsigned int nLength1 = ListLength(pHead1);
      unsigned int nLength2 = ListLength(pHead2);
      int nLengthDif = nLength1 - nLength2;
 
      // Get the longer list
      ListNode *pListHeadLong = pHead1;
      ListNode *pListHeadShort = pHead2;
      if(nLength2 > nLength1)
      {
            pListHeadLong = pHead2;
            pListHeadShort = pHead1;
            nLengthDif = nLength2 - nLength1;
      }
 
      // Move on the longer list
      for(int i = 0; i < nLengthDif; ++ i)
            pListHeadLong = pListHeadLong->m_pNext;
 
      // Move on both lists
      while((pListHeadLong != NULL) &&
            (pListHeadShort != NULL) &&
            (pListHeadLong != pListHeadShort))
      {
            pListHeadLong = pListHeadLong->m_pNext;
            pListHeadShort = pListHeadShort->m_pNext;
      }
 
      // Get the first common node in two lists
      ListNode *pFisrtCommonNode = NULL;
      if(pListHeadLong == pListHeadShort)
            pFisrtCommonNode = pListHeadLong;
 
      return pFisrtCommonNode;
}
 
///////////////////////////////////////////////////////////////////////
// Get the length of list with head pHead
// Input: pHead - the head of list
// Return: the length of list
///////////////////////////////////////////////////////////////////////
unsigned int ListLength(ListNode* pHead)
{
      unsigned int nLength = 0;
      ListNode* pNode = pHead;
      while(pNode != NULL)
      {
            ++ nLength;
            pNode = pNode->m_pNext;
      }
 
      return nLength;
}
  求大數據量數組中不重複元素的個數收藏
新一篇: 幾道面試題的求解 | 舊一篇: 求n!末尾0的個數
有2.5億個整數(這2.5億個整數存儲在一個數組裏面,至於數組是放在外存還是內存,沒有進一步具體說明);
要求找出這2.5億個數字裏面,不重複的數字的個數(那些只出現一次的數字的數目);
另外,可用的內存限定爲600M;
要求算法儘量高效,最優;
1. caoxic的算法
BYTE    marks[2^29];//512M   // BYTE marks[2^32/8]; //用這個就更清楚了,標誌所有整數(2^32)出現的可能
BYTE    repmarks[2^25];//32M 32M*8>2.5億  //標誌2.5億個數字數組裏面的數字是否重複過
const BYTE bitmarks[8]={1,2,4,8,16,32,64,128};
DWORD    CalcDifNum(DWORD *pBuf,DWORD bufcount)
{
    DWORD dw ;
    DWORD count = 0 ;// 不重複的數字(包括出現多次的數字,只算一個)的個數,例子:1 2 2 3 5 3 4 算5個
    DWORD count2 = 0 ;//重複出現的數字的個數,例子:1 2 2 3 5 3 4 算2個
    memset(marks,0,sizeof(marks));
    memset(repmarks,0,sizeof(repmarks));
    ASSERT(sizeof(repmarks)*8>=bufcount);//斷言repmarks數組夠用
    for(dw=0;dw<bufcount;dw++)
    {
        if(marks[pBuf[dw]>>3]&bitmarks[pBuf[dw]&7])
        {
            repmarks[dw>>3] |= bitmarks[dw&7];//標誌pBuf[dw]這個數字出現重複
        }
        else
        {
            count ++;
            marks[pBuf[dw]>>3] |= bitmarks[pBuf[dw]&7];//標誌pBuf[dw]這個數字出現
        }
    }

    memset(marks,0,sizeof(marks));
    for(dw=0;dw<bufcount;dw++)
    {
        if(repmarks[dw>>3] & bitmarks[dw&7])//
        {
            if(marks[pBuf[dw]>>3]&bitmarks[pBuf[dw]&7])
            {
            }
            else
            {
                count2 ++; //重複的數字的數量
                marks[pBuf[dw]>>3] |= bitmarks[pBuf[dw]&7];
            }
        }
    }
    return count-count2;
}
2. 改一下,應該也可以:
BYTE    marks[2^29];//512M   // BYTE marks[2^32/8]; //用這個就更清楚了,標誌所有整數(2^32)出現的可能
BYTE    repmarks[2^25];//32M 32M*8>2.5億  //標誌2.5億個數字數組裏面的數字是否重複過
const BYTE bitmarks[8]={1,2,4,8,16,32,64,128};
DWORD    CalcDifNum(DWORD *pBuf,DWORD bufcount)
{
    DWORD dw ;
    DWORD count = 0 ;// 不重複的數字(包括出現多次的數字,只算一個)的個數,例子:1 2 2 3 5 3 4 算5個
    DWORD count2 = 0 ;//只出現一次數字的個數,例子:1 2 2 3 5 3 4 算3個
    memset(marks,0,sizeof(marks));
    memset(repmarks,0,sizeof(repmarks));
    ASSERT(sizeof(repmarks)*8>=bufcount);//斷言repmarks數組夠用
    for(dw=0;dw<bufcount;dw++)
    {
        if(marks[pBuf[dw]>>3]&bitmarks[pBuf[dw]&7])
        {
            repmarks[dw>>3] |= bitmarks[dw&7];//標誌pBuf[dw]這個數字出現重複
        }
        else
        {
            count ++;
            marks[pBuf[dw]>>3] |= bitmarks[pBuf[dw]&7];//標誌pBuf[dw]這個數字出現
        }
    }

    for(dw=0;dw<bufcount;dw++)
    {
        if(!(repmarks[dw>>3] & bitmarks[dw&7]))//非重複的位置
        {
                count2 ++; //只出現一次的數字的數量
        }
    }
    return count2;
}

3. 把數組裏面的數字分兩類,正數負數,marks數組分成兩部分標誌,1.該數已經標誌[0 -->  2^28-1] 2.標識該數已經重複[2^28 -->2^29-1]。

BYTE    marks[2^29];//512M   // BYTE marks[2^32/8]; //用這個就更清楚了,標誌所有整數(2^32)出現的可能
const BYTE bitmarks[8]={1,2,4,8,16,32,64,128};
DWORD    CalcDifNum(DWORD *pBuf,DWORD bufcount)
{
    DWORD dw ;
    DWORD count = 0 ;// 不重複的數字(包括出現多次的數字,只算一個)的個數,例子:1 2 2 3 5 3 4 算5個
    DWORD count2 = 0 ;//重複出現的數字的個數,例子:1 2 2 3 5 3 4 算2個
    memset(marks,0,sizeof(marks));
    for(dw=0;dw<bufcount;dw++)
    {
        if(pBuf[dw]>=0)
        {
         if(marks[pBuf[dw]>>3]&bitmarks[pBuf[dw]&7])
         {
            if(marks[2^28+pBuf[dw]>>3]&bitmarks[pBuf[dw]&7])
            {
            
            }else
            {
               marks[2^28+pBuf[dw]>>3] |= bitmarks[dw&7];//標誌pBuf[dw]這個數字出現重複
               count2 ++;
            }
         }
         else
         {
             count ++;
             marks[pBuf[dw]>>3] |= bitmarks[pBuf[dw]&7];//標誌pBuf[dw]這個數字出現
         }
        }
    }
   
    memset(marks,0,sizeof(marks));
    for(dw=0;dw<bufcount;dw++)
    {
        if(pBuf[dw]<0)
        {
         pBuf[dw] = -pBuf[dw];
         if(marks[pBuf[dw]>>3]&bitmarks[pBuf[dw]&7])
         {
            if(marks[2^28+pBuf[dw]>>3]&bitmarks[pBuf[dw]&7])
            {
            
            }else
            {
               marks[2^28+pBuf[dw]>>3] |= bitmarks[dw&7];//標誌pBuf[dw]這個數字出現重複
               count2 ++;
            }
         }
         else
         {
             count ++;
             marks[pBuf[dw]>>3] |= bitmarks[pBuf[dw]&7];//標誌pBuf[dw]這個數字出現
         }
        }
    }
   
    return count-count2;
}
  程序員面試題(60):不要被階乘嚇倒收藏
新一篇: 用fstream對二進制文件的讀寫  | 舊一篇: static_cast、dynamic_cast、reinterpret_cast、和const_cast
階乘(Factorial)是個很有意思的函數,但是不少人都比較怕它,我們來看看兩個與階乘相關的問題:
1. 給定一個整數N,那麼N的階乘N!末尾有多少個0呢?例如:N=10,N!=3 628 800,N!的末尾有兩個0。
2. 求N!的二進制表示中最低位1的位置。
請點擊"我要發言",提交您的解法或者問題。
我要看答案

有些人碰到這樣的題目會想:是不是要完整計算出N!的值?如果溢出怎麼辦?事實上,如果我們從"哪些數相乘能得到10"這個角度來考慮,問題就變得簡單了。
首 先考慮,如果N!= K×10M,且K不能被10整除,那麼N!末尾有M個0。再考慮對N!進行質因數分解,N!=(2x)×(3y)×(5z)…,由於10 = 2×5,所以M只跟X和Z相關,每一對2和5相乘可以得到一個10,於是M = min(X, Z)。不難看出X大於等於Z,因爲能被2整除的數出現的頻率比能被5整除的數高得多,所以把公式簡化爲M = Z。
根據上面的分析,只要計算出Z的值,就可以得到N!末尾0的個數。
【問題1的解法一】
要計算Z,最直接的方法,就是計算i(i =1, 2, …, N)的因式分解中5的指數,然後求和:
代碼清單2-6

--------------------------------------------------------------------------------
ret = 0;
for(i = 1; i <= N; i++)
{
j = i;
while(j % 5 ==0)
{
ret++;
j /= 5;
}
}

--------------------------------------------------------------------------------
【問題1的解法二】
公式:Z = [N/5] +[N/52] +[N/53] + …(不用擔心這會是一個無窮的運算,因爲總存在一個K,使得5K > N,[N/5K]=0。)
公式中,[N/5]表示不大於N的數中5的倍數貢獻一個5,[N/52]表示不大於N的數中52的倍數再貢獻一個5,……代碼如下:
ret = 0;
while(N)
{
ret += N / 5;
N /= 5;
}
問題2要求的是N!的二進制表示中最低位1的位置。給定一個整數N,求N!二進制表示的最低位1在第幾位?例如:給定N = 3,N!= 6,那麼N!的二進制表示(1 010)的最低位1在第二位。
爲了得到更好的解法,首先要對題目進行一下轉化。
首先來看一下一個二進制數除以2的計算過程和結果是怎樣的。
把一個二進制數除以2,實際過程如下:
判斷最後一個二進制位是否爲0,若爲0,則將此二進制數右移一位,即爲商值(爲什麼);反之,若爲1,則說明這個二進制數是奇數,無法被2整除(這又是爲什麼)。
所以,這個問題實際上等同於求N!含有質因數2的個數。即答案等於N!含有質因數2的個數加1。
【問題2的解法一】
由於N! 中含有質因數2的個數,等於 N/2 + N/4 + N/8 + N/16 + …[1],
根據上述分析,得到具體算法,如下所示:
代碼清單2-7

--------------------------------------------------------------------------------
int lowestOne(int N)
{
int Ret = 0;
while(N)
{
N >>= 1;
Ret += N;
}
return Ret;
}

--------------------------------------------------------------------------------
【問題2的解法二】
N!含有質因數2的個數,還等於N減去N的二進制表示中1的數目。我們還可以通過這個規律來求解。
下面對這個規律進行舉例說明,假設 N = 11011,那麼N!中含有質因數2的個數爲 N/2 + N/4 + N/8 + N/16 + …
即: 1101 + 110 + 11 + 1
=(1000 + 100 + 1)
+(100 + 10)
+(10 + 1)
+ 1
=(1000 + 100+ 10 + 1)+(100 + 10 + 1)+ 1
= 1111 + 111 + 1
=(10000 -1)+(1000 - 1)+(10-1)+(1-1)
= 11011-N二進制表示中1的個數
小結
任 意一個長度爲m的二進制數N可以表示爲N = b[1] + b[2] * 2 + b[3] * 22 + … + b[m] * 2(m-1),其中b [ i ]表示此二進制數第i位上的數字(1或0)。所以,若最低位b[1]爲1,則說明N爲奇數;反之爲偶數,將其除以2,即等於將整個二進制數向低位移一位。
相關題目
給定整數n,判斷它是否爲2的方冪(解答提示:n>0&&((n&(n-1))==0))。
--------------------------------------------------------------------------------
[1] 這個規律請讀者自己證明(提示N/k,等於1, 2, 3, …, N中能被k整除的數的個數)。
 一道經典的面試題:如何從N個數中選出最大(小)的n個數?收藏
新一篇: c/c++基本文件讀寫 | 舊一篇: 變態比賽規則
一道經典的面試題:如何從N個數中選出最大(小)的n個數?
北京交大LuoBin
這 個問題我前前後後考慮了有快一年了,也和不少人討論過。據我得到的消息,Google和微軟都面過這道題。這道題可能很多人都聽說過,或者知道答案(所謂 的“堆”),不過我想把我的答案寫出來。我的分析也許存有漏洞,以交流爲目的。但這是一個滿複雜的問題,蠻有趣的。看完本文,也許會啓發你一些沒有想過的 解決方案(我一直認爲堆也許不是最高效的算法)。在本文中,將會一直以尋找n個最“大”的數爲分析例子,以便統一。注:本文寫得會比較細節一些,以便於絕 大多數人都能看懂,別嫌我羅嗦:) 我很不確定多少人有耐心看完本文!
Naive 方法:
     首先,我們假設n和N都是內存可容納的,也就是說N個數可以一次load到內存裏存放在數組裏(如果非要存在鏈表估計又是另一個challenging的 問題了)。從最簡單的情況開始,如果n=1,那麼沒有任何疑惑,必須要進行N-1次的比較才能得到最大的那個數,直接遍歷N個數就可以了。如果n=2呢? 當然,可以直接遍歷2遍N數組,第一遍得到最大數max1,但是在遍歷第二遍求第二大數max2的時候,每次都要判斷從N所取的元素的下標不等於max1 的下標,這樣會大大增加比較次數。對此有一個解決辦法,可以以max1爲分割點將N數組分成前後兩部分,然後分別遍歷這兩部分得到兩個“最大數”,然後二 者取一得到max2。
 
     也可以遍歷一遍就解決此問題,首先維護兩個元素max1,max2(max1>=max2),取到N中的一個數以後,先和max1比,如果比 max1大(則肯定比max2大),直接替換max1,否則再和max2比較確定是否替換max2。採用類似的方法,對於n=2,3,4……一樣可以處 理。這樣的算法時間複雜度爲O(nN)。當n越來越大的時候(不可能超過N/2,否則可以變成是找N-n個最小的數的對偶問題),這個算法的效率會越來越 差。但是在n比較小的時候(具體多小不好說),這個算法由於簡單,不存在遞歸調用等系統損耗,實際效率應該很不錯.
堆:
     當n較大的時候採用什麼算法呢?首先我們分析上面的算法,當從N中取出一個新的數m的時候,它需要依次和max1,max2,max3……max n比較,一直找到一個比m小的max x,就用m來替換max x,平均比較次數是n/2。可不可以用更少的比較次數來實現替換呢?最直觀的方法是,也就是網上文章比較推崇的堆。堆有這麼一些好處:1.它是一個完全二 叉樹,樹的深度是相同節點的二叉樹中最少的,維護效率較高;2.它可以通過數組來實現,而且父節點p與左右子節l,r點的數組下標的關係是s[l] = 2*s[p]+1和s[r] = 2*s[p]+2。在計算機中2*s[p]這樣的運算可以用一個左移1位操作來實現,十分高效。再加上數組可以隨機存取,效率也很高。3.堆的 Extract操作,也就是將堆頂拿走並重新維護堆的時間複雜度是O(logn),這裏n是堆的大小。
     具體到我們的問題,如何具體實現呢?首先開闢一個大小爲n的數組區A,從N中讀入n個數填入到A中,然後將A維護成一個小頂堆(即堆頂A[0]中存放的是 A中最小的數)。然後從N中取出下一個數,即第n+1個數m,將m與堆頂A[0]比較,如果m<=A[0],直接丟棄m。否則應該用m替換 A[0]。但此時A的堆特性可能已被破壞,應該重新維護堆:從A[0]開始,將A[0]與左右子節點分別比較(特別注意,這裏需要比較“兩次”才能確定最 大數,在後面我會根據這個來和“敗者樹”比較),如果A[0]比左右子節點都小,則堆特性能夠保證,勿需繼續,否則如左(右)節點最大,則將A[0]與左 (右)節點交換,並繼續維護左(右)子樹。依次執行,直到遍歷完N,堆中保留的n個數就是N中最大的n個數。這都是堆排序的基本知識, 唯一的trick就是維護一個小頂堆,而不是大頂堆。不明白的稍微想一下。維護一次堆的時間複雜度爲O(logn),總體的複雜度是O(Nlogn)這樣 一來,比起上面的O(nN),當n足夠大時,堆的效率肯定是要高一些的。當然,直接對N數組建堆,然後提取n次堆頂就能得到結果,而且其複雜度是 O(nlogN),當n不是特別小的時候這樣會快很多。但是對於online數據就沒辦法了,比如N不能一次load進內存,甚至是一個流,根本不知道N 是多少。
敗者樹:
     有沒有別的算法呢?我先來說一說敗者樹(loser tree)。也許有些人對loser tree不是很瞭解,其實它是一個比較經典的外部排序方法,也就是有x個已經排序好的文件,將其歸併爲一個有序序列。敗者樹的思想咋一看有些繞,其實是爲 了減小比較次數。首先簡單介紹一下敗者樹:敗者樹的葉子節點是數據節點,然後兩兩分組(如果節點總數不是2的冪,可以用類似完全樹的結構構成樹),內部節 點用來記錄左右子樹的優勝者中的“敗者”(注意記錄的是輸的那一方),而優勝者則往上傳遞繼續比較,一直到根節點。如果我們的優勝者是兩個數中較小的數, 則根節點記錄的是最後一次比較中的“敗者”,也就是所有葉子節點中第二小的那個數,而最小的那個數記錄在一個獨立的變量中。這裏要注意,內部節點不但要記 錄敗者的數值,還要記錄對應的葉子節點。如果是用鏈表構成的樹,則內部節點需要有指針指向葉子節點。這裏可以有一個trick,就是內部節點只記錄“敗者 ”對應的葉子節點,具體的數值可以在需要的時候間接訪問(這一方法在用數組來實現敗者樹時十分有用,後面我會講到)。關鍵的來了,當把最小值輸出後,最小 值所對應的葉子節點需要變成一個新的數(或者改爲無窮大,在文件歸併的時候表示文件已讀完)。接下來維護敗者樹,從更新的葉子節點網上,依次與內部節點比 較,將“敗者”更新,勝者往上繼續比較。由於更新節點佔用的是之前的最小值的葉子節點,它往上一直到根節點的路徑與之前的最小值的路徑是完全相同的。內部 節點記錄的“敗者”雖然稱爲“敗者”,但卻是其所在子樹中最小的數。也就是說,只要與“敗者”比較得到的勝者,就是該子樹中最小的那個數(這裏講得有點繞 了,看不明白的還是找本書看吧,對照着圖比較容易理解)。
注:也可以直接對N構建敗者樹,但是敗者樹用數組實現時不能像堆一樣進行增量維護,當葉子節點的個數變動時需要完全重新構建整棵樹。爲了方便比較堆和敗者樹的性能,後面的分析都是對n個數構建的堆和敗者樹來分析的。
     總而言之,敗者樹在進行維護的時候,比較次數是logn+1。與堆不同的是,敗者樹是從下往上維護,每上一層,只需要和敗者節點比較“一次”即可。而堆在 維護的時候是從上往下,每下一層,需要和左右子節點都比較,需要比較兩次。從這個角度,敗者樹比堆更優一些。但是,請注意但是,敗者樹每一次維護必定需要 從葉子節點一直走到根節點,不可能中間停止;而堆維護時,“有可能”會在中間的某個層停止,不需要繼續往下。這樣一來,雖然每一層敗者樹需要的比較次數比 堆少一倍,但是走的層數堆會比敗者樹少。具體少多少,從平均意義上到底哪一個的效率會更好一些?那我就不知道了,這個分析起來有點麻煩。感興趣的人可以嘗 試一下,討論討論。但是至少說明了,也許堆並非是最優的。
     具體到我們的問題。類似的方法,先構建一棵有n個葉子節點的敗者樹,勝出者w是n箇中最小的那一個。從N中讀入一個新的數m後,和w比較,如果比w小,直 接丟棄,否則用m替換w所在的葉子節點的值,然後維護該敗者樹。依次執行,直到遍歷完N,敗者樹中保留的n個數就是N中最大的n個數。時間複雜度也是 O(Nlogn)
 
類快速排序方法:
     快速排序大家大家都不陌生了。主要思想是找一個“軸”節點,將數列交換變成兩部分,一部分全都小於等於“軸”,另一部分全都大於等於“軸”,然後對兩部分 遞歸處理。其平均時間複雜度是O(NlogN)。從中可以受到啓發,如果我們選擇的軸使得交換完的“較大”那一部分的數的個數j正好是n,不也就完成了在 N個數中尋找n個最大的數的任務嗎?當然,軸也許不能選得這麼恰好。可以這麼分析,如果j>n,則最大的n個數肯定在這j個數中,則問題變成在這j 個數中找出n個最大的數;否則如果j<n,則這j個數肯定是n個最大的數的一部分,而剩下的j-n個數在小於等於軸的那一部分中,同樣可遞歸處理。
    令人愉悅的是,這個算法的平均複雜度是O(N)的。怎麼樣?比堆的O(Nlogn)可能會好一些吧?!(n如果比較大肯定會好)
    需要注意的是,這裏的時間複雜度是平均意義上的,在最壞情況下,每次分割都分割成1:N-2,這種情況下的時間複雜度爲O(n)。但是我們還有殺手鐗,可 以有一個在最壞情況下時間複雜度爲O(N)的算法,這個算法是在分割數列的時候保證會按照比較均勻的比例分割,at least 3n/10-6。具體細節我就不再說了,感興趣的人蔘考算法導論(Introduction to Algorithms 第二版第九章 “Medians and Orders Statistics”)。
    還是那個結論,堆不見得會是最優的。
本文快要 結束了,但是還有一個問題:如果N非常大,存放在磁盤上,不能一次裝載進內存呢?怎麼辦?對於介紹的Naive方法,堆,敗者樹等等,依然適用,需要注意 的就是每次從磁盤上儘量多讀一些數到內存區,然後處理完之後再讀入一批。減少IO次數,自然能夠提高效率。而對於類快速排序方法,稍微要麻煩一些:分批讀 入,假設是M個數,然後從這M個數中選出n個最大的數緩存起來,直到所有的N個數都分批處理完之後,再將各批次緩存的n個數合併起來再進行一次類快速排序 得到最終的n個最大的數就可以了。在運行過程中,如果緩存數太多,可以不斷地將多個緩存合併,保留這些緩存中最大的n個數即可。由於類快速排序的時間複雜 度是O(N),這樣分批處理再合併的辦法,依然有極大的可能會比堆和敗者樹更優。當然,在空間上會佔用較多的內存。
總結:對於這個問題,我 想了很多,但是覺得還有一些地方可以繼續深挖:1. 堆和敗者樹到底哪一個更優?可以通過理論分析,也可以通過實驗來比較。也許會有人覺得這個很無聊;2. 有沒有近似的算法或者概率算法來解決這個問題?我對這方面實在不熟悉,如果有人有想法的話可以一塊交流。如果有分析錯誤或遺漏的地方,請告知,我不怕丟 人,呵呵!最後請時刻謹記,時間複雜度不等於實際的運行時間,一個常數因子很大的O(logN)算法也許會比常數因子小的O(N)算法慢很多。所以說,n 和N的具體值,以及編程實現的質量,都會影響到實際效率。我看過一篇論文,給出的算法在進行字符串查找時,比hash還要快,是不是難以想象?
  1到n之間1的個數收藏
新一篇: 程序員面試題精選{57):求n的加法組合 | 舊一篇: 程序員面試題精選(56):找出兩個鏈表的第一個公共結點
Consider a function which, for a given whole number n, returns the number of ones required when writing out all numbers between 0 and n.
For example, f(13)=6. Notice that f(1)=1. What is the next largest n such that f(n)=n?
 
算法思想:
 
循環求出每個數中1的個數,累計之,若滿足f(n)=n,則退出,否則繼續。
代碼如下:
  /************************************************************************
 * 0~n之間1的個數,如f(13)=6
 * 1,2,3,4,5,6,7,8,9,10,11,12,13.1的個數爲6
 * 要求:輸入一個正整數n,求出f(n),及求解f(n)=n
 ************************************************************************/
 
 #include <stdio.h>
 #include <string.h>
 #include <Windows.h>
 
 class CalculationTimes
  {
 public:
      CalculationTimes(){}
      ~CalculationTimes(){}
 
     int GetTimes(int n);
 };
 
 //計算正整數n中1的個數
 int CalculationTimes::GetTimes(int n)
  {
     int count=0;   
     while(n)
      {
         if(n%10==1)
             count++;
         n/=10;
     }
     return count;
 }
 
 //顯示菜單
 void show_menu()
  {
     printf("--------------------------------------------- ");
     printf("input command to test the program ");
     printf("   i or I : input n to test ");
     printf("   g or G : get n to enable f(n)=n ");
     printf("   q or Q : quit ");
     printf("--------------------------------------------- ");
     printf("$ input command >");
 }
 
 void main()
  {
     char sinput[10];
     int n;
 
     show_menu();
 
     scanf("%s",sinput);
     while(stricmp(sinput,"q")!=0)
      {
         int t=0,count=0;
         if(stricmp(sinput,"i")==0)
          {
             printf("  please input an integer:");
             scanf("%d",&n);
 
             count=0;
             CalculationTimes obj;
             t=GetTickCount();
             for(int i=1;i<=n;i++)
                 count+=obj.GetTimes(i);
             t=GetTickCount()-t;
             printf("   count=%d    time=%d ",count,t);
         }
         else if(stricmp(sinput,"g")==0)
          {
             CalculationTimes obj;
             n=2;
             count=1;
             t=GetTickCount();
             while(1)
              {
                 count+=obj.GetTimes(n);
                 if(count==n)
                     break;
                 n++;
             }
             t=GetTickCount()-t;
             printf("   f(n)=n=%d    time=%d ",n,t);
         }
 
 
         //輸入命令
         printf("$ input command >");
         scanf("%s",sinput);
     }
 }
  程序員面試題精選{57):求n的加法組合收藏
新一篇: 程序員面試題精選{57):求n的加法組合 | 舊一篇: 1到n之間1的個數
一個正整數n可以寫爲幾個正整數的和,如:
4=4
4=3+1
4=2+2
4=2+1+1
4=1+1+1+1
輸入一個正整數,找出符合這種要求的所有正整數序列(序列不重複)
算法思想:
 
以n=6爲例,將數繼序列暫時存於a[MAXCOL]中,且初始時值全爲1。
對數組中從jcol列開始的newn個元素進行操作f(6,0,0)  ——函數GetCombinations(newn,newj,col)
col記錄調用該函數時是在第col列。
 
初始化
count=6
j=0,newn=0
count=n
1. 如果count>1,令a[jcol]=count;
         若count>a[col],表明該嘗試不滿足條件,count=count-1,重複1;
         否則將該行第jcol+1列到jcol+count-1列的值改爲0;
    否則,退出;
2. newn=n-count
3. 如果new>1,則對從該行開始從jcol+count開始的newn個元素進行類似操作,並返回該新的newn對應的序列個數;
    否則,count=count-1,返回1。
算法比較:
“acm題目及我的程序(3)——正整數n的加法組合”——使用二維數組存放加法序列
        #define MAXROW 12000
        #define MAXCOL 20
        a[MAXROW][MAXCOL]
        算法效率低,空間浪費嚴重
“acm題目及我的程序(3)——正整數n的加法組合(改進)”——使用二維數組存放加法序列
        a[MAXROW][MAXCOL]
        #define MAXROW 6000
        #define MAXCOL 30
        算法效率高,空間浪費不嚴重
 
“acm題目及我的程序(3)——正整數n的加法組合(改進2)”——使用動態二維數組存放加法序列
        vector<vector<int> > m_venline
        算法效率高,空間浪費很少
    
  本文算法——使用一維數組存放加法序列,且計算每個n的加法序列的個數
        a[MAXCOL]
        算法效率最高,空間根本不浪費
代碼如下:
 
  /************************************************************************
  * 一個正整數n可以寫爲幾個正整數的和
  * 4=4
  * 4=3+1
  * 4=2+2
  * 4=2+1+1
  * 4=1+1+1+1
  * 要求:輸入一個正整數,找出符合這種要求的所有正整數序列(序列不重複)
  ************************************************************************/
 
 #include <stdio.h>
 #include <string.h>
 #include <CONIO.H>
 #include <vector>
 using namespace std;
 
 #define MAXCOL 80
 #define FILENAMELENGTH 100
 
 class AdditionCombination
  {
 public:
     int a[MAXCOL];
     int m_number;    //條用GetCombinations函數時count的值   
 
 public:
     AdditionCombination(int number)
      {
         m_number=number;
         for(int j=0;j<MAXCOL;j++)
             a[j]=1;
     }
 
      ~AdditionCombination(){}
 
     void Initialize()
      {
         for(int j=0;j<m_number;j++)
             a[j]=1;
     }
 
     void Initialize(int jcol)
      {
         for(int j=jcol;j<m_number;j++)
             a[j]=1;
     }
 
     int GetCombinations(int n,int jcol,int col);
     void display(int n);
 };
 
 //在數組中從irow行,jcol列開始對n階子矩陣進行
 //col記錄調用該函數前jcol的值
 int AdditionCombination::GetCombinations(int n,int jcol,int col)
  {
     int nTotalCount=0;
     int j=0,newn=0;
     int count=n;
    
     while(count>1)
      {
         if(jcol==0)
             Initialize();
         else
             Initialize(jcol);
         a[jcol]=count;
        
         //算法優化,刪除不滿足的序列
         if(a[jcol]>a[col])
          {
             count--;
             continue;
         }
 
         for(j=jcol+1;j<jcol+count;j++)
             a[j]=0;
 
         newn=n-count;
         if(newn>1)
          {
             int newj=jcol+count;
             int newcount=GetCombinations(newn,newj,jcol);
             nTotalCount+=newcount;
         }
        
         count--;
         //display(m_number);
         nTotalCount++;
     }
 
     if(jcol==0)
      {
         Initialize();
         //display(m_number);
         nTotalCount++;
     }
     else
         Initialize(jcol);
 
     return nTotalCount;
 }
 
 void AdditionCombination::display(int n)
  {
     printf("   %d=%d",n,a[0]);
     for(int j=1;j<n;j++)
      {
         if(a[j])
             printf("+%d",a[j]);
     }
 }
 
 //將結果寫入文件
 void WriteToFile(vector<vector<int> > info)
  {
     char filename[FILENAMELENGTH];
 
     int size=info.size();
     //構造文件名
     sprintf(filename,"%d-%d result.txt",info[0][0],info[size-1][0]);
     FILE *fp=fopen(filename,"w");
     if(fp==NULL)
      {
         printf("can not wirte file!");
         exit(0);
     }
 
     //寫入個數
     for(int i=0;i<size;i++)
         fprintf(fp,"n=%d    count=%d ",info[i][0],info[i][1]);
 
     fclose(fp);
 }
 
 //顯示菜單
 void show_menu()
  {
     printf("--------------------------------------------- ");
     printf("input command to test the program ");
     printf("   i or I : input n to test ");
     printf("   t or T : get count from n1 to n2 ");
     printf("   q or Q : quit ");   
     printf("--------------------------------------------- ");
     printf("$ input command >");
 }
 
 void main()
  {
     char sinput[10];
     int n;
 
     show_menu();
 
     scanf("%s",sinput);
     while(stricmp(sinput,"q")!=0)
      {
         if(stricmp(sinput,"i")==0)
          {
             printf("  please input an integer:");
             scanf("%d",&n);
 
             AdditionCombination obj(n);
             int count=obj.GetCombinations(n,0,0);
             printf("   count = %d ",count);
         }
         else if(stricmp(sinput,"t")==0)
          {
             int n1,n2;
             printf("  please input the begin number:");
             scanf("%d",&n1);
             printf("  please input the  end  number:");
             scanf("%d",&n2);
 
             printf("  press any key to start ... ");
             getch();
 
             vector<vector<int> > info;
             vector<int> line;
 
             AdditionCombination obj(n1);
             for(int i=n1;i<=n2;i++)
              {
                 obj.Initialize();
                 obj.m_number=i;
                 int count=obj.GetCombinations(i,0,0);
                 printf("  n=%d    count=%d ",i,count);
                 line.clear();
                 line.push_back(i);
                 line.push_back(count);
                 info.push_back(line);
             }
             printf(" ");
 
             //寫入文件
             printf("$ write the numbers to file(Y,N)? >");
             scanf("%s",sinput);
             if(stricmp(sinput,"y")==0)        //寫入文件
              {
                 WriteToFile(info);
                 printf("  write successfully! ");
             }
             printf(" ");
         }
 
         //輸入命令
         printf("$ input command >");
         scanf("%s",sinput);
     }
 }
google的一道面試題收藏
新一篇: 程序員面試題精選{58):字符串插入字符個數 | 舊一篇: 程序員面試題精選{57):求n的加法組合
 題目:
輸入a1,a2,...,an,b1,b2,...,bn, 在O(n)的時間,O(1)的空間將這個序列順序改爲a1,b1,a2,b2,a3,b3,...,an,bn
 
 
不需要移動,通過交換完成,只需一個交換空間
例如,N=9時,第2步執行後,實際上中間位置的兩邊對稱的4個元素基本配對,只需交換中間的兩個元素即可,如下表所示。顏色表示每次要交換的元素,左邊向右交換,右邊向左交換。交換過程如下表所示。
  1 2 3 4 5 6 7 8 9 n+1 n+2 n+3 n+4 n+5 n+6 n+7 n+8 n+9     
  n-8 n-7 n-6 n-5 n-4 n-3 n-2 n-1 N 2n-8 2n-7 2n-6 2n-5 2n-4 2n-3 2n-2 2n-1 2n 交換開始位置 交換個數
  a1 a2 a3 a4 a5 a6 a7 a8 a9 b1 b2 b3 b4 b5 b6 b7 b8 b9 2?n+1 2n-1?n 1
1 a1 b1 a3 a4 a5 a6 a7 a8 b8 a2 b2 b3 b4 b5 b6 b7 a9 b9 3?n+1 2n-2?n 2
2 a1 B1 a2 b2 a5 a6 a7 b6 b7 a3 a4 b3 b4 b5 a8 b8 a9 b9 5?n+1 2n-4?n 4
3 a1 B1 a2 b2 X1 Y1=(a6 a7 b7) X2 X3 Y2=(a4 b3 b4) X4 a8 b8 a9 b9 對稱交換   
  a1 B1 a2 b2 X3 Y2 X4 X1 Y1 X2 a8 b8 a9 b9     
  a1 B1 a2 b2 X3 Y2 X1 X4 Y1 X2 a8 b8 a9 b9     
4 a1 B1 a2 b2 A3 A4 B3 B4 A5 B5 A6 A7 B6 B7 a8 b8 a9 b9   
5 a1 B1 a2 b2 A3 B3 A4 B4 A5 B5 A6 B6 A7 B7 a8 b8 a9 b9   
交換x1,x3;交換x2,x4;再交換中間的x1,x4;交換y1,y2;
算法思想:
以N=9爲例(中間的豎表示中間位置):
   a1 a2 a3 a4 a5 a6 a7 a8 a9 | b1 b2 b3 b4 b5 b6 b7 b8 b9
  頭尾的元素不需任何操作
1. 左邊從位置left=2開始,右邊從位置n+1開始,向右交換count=1個元素,即a2,b1交換
  右邊從位置right=2n-1開始,左邊從位置n開始,向左交換count=1個元素,即b8,a9交換
  序列變爲:
   a1 b1 a3 a4 a5 a6 a7 a8 b8 | a2 b2 b3 b4 b5 b6 b7 a9 b9
  故已經成功放好位置的有(a1,b1),(a9,b9)
  其中(a8,b8),(a2,b2)也配對,只需將其交換到相應的位置即可
2. 左邊從位置left=3開始,右邊從位置n+1開始,向右交換count=2個元素,即a3 a4 和 a2 b2交換
  右邊從位置right=2n-2開始,左邊從位置n開始,向左交換count=2個元素,即b6 b7 和 a8 b8交換
  序列變爲:
   a1 b1 a2 b2 a5 a6 a7 b6 b7 | a3 a4 b3 b4 b5 a8 b8 a9 b9
  故又成功放好位置的有(a2,b2),(a8,b9)
3. 左邊從位置left=5開始,右邊從位置n開始,已不能滿足交換count=4個元素的要求,故退出循環
4. 序列縮小爲a5 a6 a7 b6 b7 | a3 a4 b3 b4 b5, 對序列中沒有放好的數據按快處理
將序列看作:x1=(a5) y1=(a6 a7 b6) x2=(b7) | x3=(a3) y2=(a4 b3 b4) x4=(b5)
交換x1,x3;交換x2,x4;再交換中間的x1,x4;交換y1,y2;
此時序列變爲S1:a3 a4 b3 b4 a5 b5 a6 a7 b6 b7
5. 若交換到左邊的b有配對的a,則中間的序列S2=(a5 b6)作爲新的序列;否則將S1作爲新的序列;對該序列進行上述操作,直到所有元素都放到正確的位置
此例中,(a6,a7,b6,b7),(a3,a4,b3,b4)以配對,只需交換中間的兩個元素即可,序列縮小a5 b5
 
=====================================================================
   爲方便比較,序列變化如下:
   a1 a2 a3 a4 a5 a6 a7 a8 a9 | b1 b2 b3 b4 b5 b6 b7 b8 b9
       a1 b1 a3 a4 a5 a6 a7 a8 b8 | a2 b2 b3 b4 b5 b6 b7 a9 b9
       a1 b1 a2 b2 a5 a6 a7 b6 b7 | a3 a4 b3 b4 b5 a8 b8 a9 b9
       a1 b1 a2 b2 a3 a4 b3 b4 a5 | b5 a6 b6 a7 b7 a8 b8 a9 b9
       a1 b1 a2 b2 a3 b3 a4 b4 a5 | b5 a6 b6 a7 b7 a8 b8 a9 b9
源代碼如下:
  /************************************************************************
  * 輸入a1,a2,...,an,b1,b2,...,bn
  * 在O(n)的時間,O(1)的空間
  * 將這個序列順序改爲a1,b1,a2,b2,a3,b3,...,an,bn
 ************************************************************************/
 
 #include <stdio.h>
 #include <string.h>
 #include <CONIO.H>
 
 #define MAXSIZE 2*100000
 
 int exchangetimes=0;    //交換次數
 
 //交換兩個數據
 void swap(int *x,int *y)
  {
     int t;
     t=*x;
     *x=*y;
     *y=t;
 
     exchangetimes++;
 }
 
 //按要求交換序列(假設元素從下標1開始存放)
 void exchange(int a[],int m)
  {
     int n=m/2;
 
     if(n==1)        //a1,b1 ==>不需交換
         return;
     else if(n==2)    //a1,a2,b1,b2 ==>只需交換中間的兩個數據
      {
         swap(a+1,a+2);
         return;
     }
 
     int done;        //已經處理的數據個數
     int left;        //左邊開始交換的位置
     int right;        //右邊開始交換的位置
     int count;        //每次交換的數據個數
     int lefta;        //左邊未處理的a個數
     int notmatch;    //左邊未匹配的a個數
 
     //初始化
     done=1;
     left=1;            //左邊從位置1開始向右交換
     right=2*n-2;    //右邊從位置2*n-2開始向左交換
     count=1;        //每次交換count個數據
     lefta=notmatch=n-1;
 
     while(1)
      {       
         //左邊從left開始和右邊從n開始向右交換count個數據       
         for(int j=0;j<count;j++)
             swap(a+left+j,a+n+j);
 
         //右邊從right開始和左邊從n-1開始向左交換count個數據
         for(j=0;j<count;j++)
             swap(a+right-j,a+n-1-j);
 
         //交換後將其調整爲要求的序列
         if(count>=4)
          {
             exchange(a+left,count);
             exchange(a+right-count+1,count);
         }
 
         //重新調整各變量
         done+=count;
         lefta=n-done-count;
         notmatch=lefta-count;
         left=left+count;
         right=right-count;
         count*=2;
 
         if(notmatch<count)
             break;
     }
 
     int x,y;
    
     //左邊剩下的a不能與從右邊交換過來的b配對
     //如n=13時,上面的循環結束後變爲: a9 (b6 b7 b8) b9 | a5 (a6 a7 a8) b5
     //此時,lefta=1,notmatch<0,分塊交換,各個塊如下
     //x1=(a9),  y1=(b6 b7 b8),  x2=(b9),  x3=(a5),  y2=(a6 a7 a8),  x4=(b5)
     //上述序列變爲 x1 y1 x2 x3 y2 x4, x的長度均爲1,y的長度均爲3
     //x1 x3交換,x2 x4交換==>x3 x4 x1 x2,然後中間的x4 x1交換==>x3 x1 x4 x2
     //y1 y2交換==>y2 y1
     //上述6塊經4次交換後變爲 x3 y2 x1 x4 y1 x2
     //n=13時,經上述交換後變爲 a5 a6 a7 a8 a9 b5 b6 b7 b8 b9
     if(notmatch<0)
      {
         count/=2;    //if n=13, then here count=4
         x=lefta;    //x塊的長度,if n=13, then heare x=1
     }
     else
      {
         //遞歸調用中間對稱的count個數據
         exchange(a+n-count,count);
         exchange(a+n,count);
 
         x=notmatch;
     }
 
      //////////////////////////////////////////////////////////////////////////   
     y=count-x;    //y塊的長度,if n=13, then heare y=3
     //左邊從left開始和右邊從n開始向右交換x個數據,即x1 x3交換
     //右邊從right開始和左邊從n-1開始向左交換x個數據,即x2 x4交換
     for(int j=0;j<x;j++)
      {
         swap(a+left+j,a+n+j);        //左邊向左交換
         swap(a+right-j,a+n-1-j);    //右邊向左交換
     }
 
     //交換到中間的x1 x4交換
     for(j=0;j<x;j++)
         swap(a+n-x+j,a+n+j);
    
     //交換y1 y2數據塊
     for(j=0;j<y;j++)
         swap(a+left+x+j,a+n+x+j);
      //////////////////////////////////////////////////////////////////////////
    
     //處理餘下的序列
     if(notmatch<0)
      {
         int newm=2*(n-left);    //或者size=x+count
         exchange(a+left,newm);
     }
     else if(notmatch>0)
      {
         int newm=2*notmatch;
         exchange(a+left+count,newm);
     }
 }
 
 //顯示菜單
 void show_menu()
  {
     printf("--------------------------------------------- ");
     printf("input command to test the program ");
     printf("   i or I : input n to test ");
     printf("   t or T : test program ");
     printf("   q or Q : quit ");   
     printf("--------------------------------------------- ");
     printf("$ input command >");
 }
 
 //顯示數據
 void display(int a[],int n)
  {
     for(int i=0; i<n;i++)
         printf("%3d",a[i]);
     printf(" ");
 }
 
 //檢查交換是否正確
 bool check(int a[],int n)
  {
     int i;
 
     for(i=0;i<n-2;i+=2)
      {
         if(a[i]!=i/2+1)
             return false;
     }
 
     for(i=1;i<n-1;i+=2)
      {
         if(a[i]!=(n+i+1)/2)
             return false;
     }
 
     return true;
 }
 
 void main()
  {
     int a[MAXSIZE];
     int n;
     char sinput[10];
 
     show_menu();
 
     scanf("%s",sinput);
     while(stricmp(sinput,"q")!=0)
      {
         if(stricmp(sinput,"i")==0)
          {
             printf("  please input n:");
             scanf("%d",&n);
 
             //且假設數組中的數據爲1,2,3,...,n,n+1,n+2,...,2n
             for(int i=0; i<2*n;i++)    //初始化
                 a[i]=i+1;
 
             display(a,2*n);
 
             exchangetimes=0;
 
             //交換
             exchange(a,2*n);
             display(a,2*n);
 
             printf("   exchange times: %d ",exchangetimes);
         }
         else if(stricmp(sinput,"t")==0)
          {
             int n1,n2;
             printf("  please input the begin number:");
             scanf("%d",&n1);
             printf("  please input the  end  number:");
             scanf("%d",&n2);
 
             printf("  press any key to start ... ");
             getch();
 
             for(int i=n1;i<=n2;i++)
              {
                 //初始化
                 for(int j=0; j<2*i;j++)
                     a[j]=j+1;
 
                 exchangetimes=0;
                 exchange(a,2*i);
 
                 if(check(a,2*i))
                     printf("  n=%d ... ok!    exchange times: %d ",i,exchangetimes);
                 else
                     printf("  n=%d ... wrong! ",i);
             }
 
             printf(" ");
         }
 
         //輸入命令
         printf("$ input command >");
         scanf("%s",sinput);
     }
 }

發佈了15 篇原創文章 · 獲贊 15 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章