面試常備題---鏈表總結篇


      數據結構和算法,是我們程序設計最重要的兩大元素,可以說,我們的編程,都是在選擇和設計合適的數據結構來存放數據,然后再用合適的算法來處理這些數據。

      在面試中,最經常被提及的就是鏈表,因為它簡單,但又因為需要對指針進行操作,凡是涉及到指針的,都需要我們具有良好的編程基礎才能確保代碼沒有任何錯誤。

      鏈表是一種動態的數據結構,因為在創建鏈表時,我們不需要知道鏈表的長度,當插入一個結點時,只需要為該結點分配內存,然后調整指針的指向來確保新結點被連接到鏈表中。所以,它不像數組,內存是一次性分配完畢的,而是每添加一個結點分配一次內存。正是因為這點,所以它沒有閑置的內存,比起數組,空間效率更高。

      像是單向鏈表的結點定義如下:

struct ListNode
{
     int m_nValue;
     ListNode* m_pNext;
};

     那么我們往該鏈表的末尾添加一個結點的代碼如:

void AddToTail(ListNode** pHead, int value)
{
      ListNode* pNew = new ListNode();
      pNew->m_nValue = value;
      pNew->m_pNext = NULL;

      if(*pHead == NULL)
      {
           *pHead = pNew;
      }
      else
      {
           ListNode* pNode = *pHead;

           while(pNode->m_pNext != NULL)
           {
               pNode = pNode->m_pNext;
            }
           pNode->m_pNext = pNew;
      }
}

      我們傳遞一個鏈表時,通常是傳遞它的頭指針的指針。當我們往一個空鏈表插入一個結點時,新插入的結點就是鏈表的頭指針,那么此時就會修改頭指針,因此必須把pHead參數設置為指向指針的指針,否則出了這個函數,pHead指向的依然是空,因為我們傳遞的會是參數的一個副本。但這里又有一個問題,為什么我們必須將一個指向ListNode的指針賦值給一個指針呢?我們完全可以直接在函數中直接聲明一個ListNode而不是它的指針?注意,ListNode的結構中已經非常清楚了,它的組成中包括一個指向下一個結點的指針,如果我們直接聲明一個ListNode,那么我們是無法將它作為頭指針的下一個結點的,而且這樣也能防止棧溢出,因為我們無法知道ListNode中存儲了多大的數據,像是這樣的數據結構,最好的方式就是傳遞指針,這樣函數棧就不會溢出。
    對於java程序員來說,指針已經是遙遠的記憶了,因為java完全放棄了指針,但並不意味着我們不需要學習指針的一些基礎知識,畢竟這個世界上的代碼並不全部是由java所編寫,像是C/C++的程序依然運行在世界上大部分的機器上,像是一些系統的源碼,就是用它們編寫的,加上如果我們想要和底層打交道的話,學習C/C++是必要的,而指針就是其中一個必修的內容。

     就因為鏈表的內存不是一次性分配的,所以它並不像數組一樣,內存是連續的,所以如果我們想要在鏈表中查找某個元素,我們就只能從頭結點開始,而不能像數組那樣根據索引來,所以時間效率為O(N)。

    像是這樣:

void RemoveNode(ListNode** pHead, int value)
{
      if(pHead == NULL || *pHead == NULL)
      {
           return;
      }

      ListNode* pToBeDeleted = NULL;
      if((*pHead)->m_nValue == value)
      {
           pToBeDeleted = *pHead;
           *pHead = (*pHead)->m_pNext;
      }
      else 
      {
            ListNode* pNode = *pHead;
            while(pNode->m_pNext != NULL && pNode->m_pNext->m_nValue != value)
            {
                  pNode = pNode->m_pNext;
            }
            if(pNode->m_pNext != NULL && pNode->m_pNext->m_nValue == value)
            {
                  pToBeDeleted = pNode->m_pNext;
                  pNode->m_pNext = pNode->m_pNext->m_pNext;
            }
      }

      if(pToBeDeleted != NULL)
      {
             delete pToBeDeleted;
             pToBeDeleted = NULL;
       }
}

      上面的代碼用來在鏈表中找到第一個含有某值的結點並刪除該結點.
      常見的鏈表面試題目並不僅僅要求這么簡單的功能,像是下面這道題目:

題目一:輸入一個鏈表的頭結點,從尾到頭反過來打印出每個結點的值。

       首先我們必須明確的一點,就是我們無法像是數組那樣直接的逆序遍歷,因為鏈表並不是一次性分配內存,我們無法使用索引來獲取鏈表中的值,所以我們只能是從頭到尾的遍歷鏈表,然后我們的輸出是從尾到頭,也就是說,對於鏈表中的元素,是"先進后出",如果明白到這點,我們自然就能想到棧。

      事實上,鏈表確實是實現棧的基礎,所以這道題目的要求其實就是要求我們使用一個棧。

      代碼如下:

void PrintListReversingly(ListNode* pHead)
{
      std :: stack<ListNode*> nodes;

      ListNode* pNode = pHead;
      while(pNode != NULL)
      {
          nodes.push(pNode);
          pNode = pNode->m_pNext;
      }

      while(!nodes.empty())
      {
           pNode = nodes.top();
           printf("%d\t", pNode->m_nValue);
           nodes.pop();
      }
}

     既然都已經想到了用棧來實現這個函數,而遞歸在本質上就是一個棧,所以我們完全可以用遞歸來實現:

void PrintListReversingly(ListNode* pHead)
{
      if(pHead != NULL)
      {
           if(pHead->m_pNext != NULL)
           {
                  PrintListReversingly(pHead->m_pNext);
           }
       printf("%d\t", pHead->m_nValue);
      }
}

      但使用遞歸就意味着可能發生棧溢出的風險,尤其是鏈表非常長的時候。所以,基於循環實現的棧的魯棒性要好一些。

      利用棧來解決鏈表問題是非常常見的,因為單鏈表的特點是只能從頭開始遍歷,如果題目要求或者思路要求從尾結點開始遍歷,那么我們就可以考慮使用棧,因為它符合棧元素的特點:先進后出。

      鏈表的逆序是經常考察到的,因為要解決這個問題,必須要反過來思考,從而能夠考察到面試者是否具有逆思維的能力。

題目二:定義一個函數,輸入一個鏈表的頭結點,然后反轉該鏈表並輸出反轉后鏈表的頭結點。

      和上面一樣,我們都要對鏈表進行逆序,但不同的是這次我們要改變鏈表的結構。

       最直觀的的做法就是:遍歷該鏈表,將每個結點指向前面的結點。但這種做法會有個問題,舉個例子:我們一開始將頭指針指向NULL,也就是說,pHead->next = NULL,但是獲取后面結點的方法是:pHead->next->next,這時會是什么呢?pHead->next已經是NULL,NULL->next就是個錯誤!所以,我們自然就想到,要在遍歷的時候保留pHead->next。

      綜合上面的討論,代碼如:
ListNode* ReverseList(ListNode* pHead)
{
     ListNode* pReversedHead = NULL;
     ListNode* pNode = pHead;
     ListNode* pPrev = NULL;
     while(pNode != NULL)
     {
         ListNode* pNext = pNode->m_pNext;
         
         if(pNext == NULL)
         {
              pReversedHead = pNode;
          }
   
          pNode->m_pNext = pPrev;

          pPrev = pNode;
          pNode = pNext;
     }

     return pReversedHead;
}

     從最直觀的的做法開始,一步一步優化,並不是每個人都能第一時間想到最優解,要讓代碼在第一時間內正確的運行才是首要的,然后在不影響代碼的外觀行為下改進代碼。

     最優解往往來自於兩個方面:足夠的測試用例和輸出正確的運行代碼。
     還有一種形式的逆序題目:
題目三: 輸入一個鏈表,然后輸出它的倒數第K個結點的值,計數從1開始,也就是說,鏈表結尾的元素就是倒計數第1個元素。
     像是這樣的題目,我們的第一個想法就是要獲取鏈表的兩個元素:鏈表的總長度N和倒計數的K值。
     要獲取鏈表的總長度,我們需要遍歷該鏈表,然后再遍歷N- K + 1來獲取倒數第K個元素的值。這樣子需要遍歷鏈表兩次,雖然可行,但一般遍歷的次數應該下降到1次。
     既然是下降到1次,那么該下降的是哪一次呢?獲取元素需要遍歷是無可厚非的,因為鏈表不能逆序遍歷,只能從頭指針開始遍歷,而我們要獲取倒數第1個元素,就勢必要遍歷到末尾,所以,遍歷N次是無可厚非的。
     這種問題的考察是非常常見的,它的解決方法並不神秘,像是上面一開始的解決過程就是自然而然的思路,而更好的思路也往往是基於這樣基礎的認識上,只不過采用的方法不一樣而已。首先,要抓住基本思路的本質:遍歷兩次鏈表,其實就是兩次指針的移動,但它們並不是同時的,所以我們可以想想是否可以讓兩個指針的遍歷動作同時進行呢?
     我們的指針還是要從鏈表的頭指針開始,之所以要遍歷到最后,是為了獲取N,而N的作用就是N - K + 1,既然我們決定取消這個N的獲取,那么我們得想辦法得到N - K + 1。
     我們可以先讓一個指針從頭指針開始行動,等到行動到第K - 1步的時候,我們再讓第二個指針開始行動,這時它們之間的差距就是K - 1,等到第一個指針行動到末尾,也就是第N步的時候,第二個指針的位置剛好就是N - (K - 1) = N - K + 1。
     在編寫代碼前,我們最想知道的是,如何根據這樣的條件得出這樣的答案?知道答案是很簡單的一件事,但如何得出答案卻是很難的一件事。
     在推出答案前,我們先要知道我們的條件:N和K,然后要得到N - K + 1,然后是兩個指針同時行動,其中一個指針會達到N,所以另一個指針此時的位置就是N - K + 1,也就是說,它和這個指針的位置應該相差K,然后再加1。對於計算機而言,所謂的減法其實就是加法,所以我們可以將N - K + 1改寫為N - (K - 1),這樣我們的思路就變成另一個指針和第一個指針的位置相差K - 1。
     基於這樣的思路,我們可以讓第一個指針先行動到第K - 1個位置,然后第二個指針開始行動,接着它們兩個同時行動,這樣就能始終保持兩個指針相差K - 1了。
     能想到這樣的思路已經算是思維敏捷了,但我們必須充分考慮各種情況,像是N不一定大於K,鏈表可能是空指針,還有K可能是無效輸入,像是0或者負數。
     結合上面的考慮,我們的代碼如下:
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k)
{
    if(pListHead == NULL || k == 0)
    {
         return NULL;
    }

    ListNode* pAhead = pListHead;
    ListNode* pBehind = NULL;

   for(unsigned int i = 0; i < k - 1; ++i)
  {
       if(pAhead->m_pNext != NULL)
      {
            pAhead = pAhead->m_pNext;
      }
      else
      {
            return NULL;
      }
  }

   pBehind = pListHead;
   while(pAhead->m_pNext != NULL)
   {
        pAhead = pAhead->m_pNext;
        pBehind = pBehind->m_pNext;
   }

   return pBehind;
}

     魯棒性是非常重要的,所以在考慮一個問題的時候必須充分考慮各種情況,不要一開始想到思路就開始寫代碼,最好是先想好測試用例,然后再讓自己的代碼通過所有的測試用例。

     使用棧最大的問題就是空間復雜度,像是下面這道題目:

題目四:輸入兩個鏈表,找出它們的第一個公共結點。

     拿到這道題目,我們的第一個想法就是在每遍歷一個鏈表的結點時,再遍歷另一個鏈表。這樣大概的時間復雜度將會是O(M * N)。如果是數組,或許我們可以考慮一下使用二分查找來提高查找的效率,但是鏈表完全不能這樣。

     想想我們判斷一個結點是否是公共結點,不僅要比較值,還要比較它下一個結點是否是一樣,也就是說,就算找到該結點,判斷的依據還是要放在后面的結點是否相同,所以,可以倒過來思考:如果從尾結點開始,找到兩個結點的值完全相同,則可以認為前面的結點是公共結點。
     但鏈表是單鏈表,我們只能從頭開始遍歷,但是尾結點卻要先比較,這種做法就是所謂的"后進先出",也就是所謂的棧。但使用棧需要空間復雜度,現在我們可以將時間復雜度控制在O(M + N),但是空間復雜度卻是O(M + N)。要想辦法將空間復雜度降到最低,也就是減少兩個棧的比較次數。
     注意到一個事情:兩個鏈表的長度不一定相同,我們可以先遍歷兩個鏈表,得到它們的長度M和N,其中M < N,讓較長的鏈表先行N - M,然后再同時遍歷,這樣時間復雜度就是O(M + N),但根本就不需要棧,節省了空間。
      代碼如:
ListNode* FindFirstCommonNode(ListNode* pHead1, ListNode* pHead2)
{
     unsigned int len1 = GetListLength(pHead1);
     unsigned int len2 = GetListLength(pHead2);

    int lengthDif = len1 - len2;

    ListNode* pListHeadLong = pHead1;
    ListNode* pListHeadShort = pHead2;
    if(len2 > len1)
    {
         pListHeadLong = pHead2;
         pListHeadShort = pHead1;
         lengthDif = len2 - len1;
    }

    for(int i = 0; i < lengthDif; ++i)
    {
         pListHeadLong = pListHeadLong->m_pNext;
    }

    while((pListHeadLong != NULL) && (pListHeadShort != NULL) && (pListHeadLong != pListHeadShort))
    {
          pListHeadLong = pListHeadLong->m_pNext;
          pListHeadShort = pListHeadShort->m_pNext;
    }

    ListNode* pFirstCommonNode = pListHeadLong;

    return pFirstCommonNode;
}

unsigned int GetListLength(ListNode* pHead)
{
     unsigned int length = 0;
     ListNode* pNode = pHead;
     while(pNode != NULL)
     {
           ++length;
           pNode = pNode->m_pNext;
     }

     return length;
}

     就算是鏈表的基本操作,也會作為面試題目出現,這時就要求我們能夠寫出更快效率的代碼出來,像是下面這道題目:

題目五:給定單向鏈表的頭指針和一個結點指針,定義一個函數在O(1)時間刪除該結點。

      這個題目的要求是讓我們能夠像數組操作一樣,實現O(1),而根據一般鏈表的特點,是無法做到這點的,這就要求我們想辦法改進一般的刪除結點的做法。

      一般我們刪除結點,就像上面的做法,是從頭指針開始,然后遍歷整個鏈表,之所以要這樣做,是因為我們需要得到將被刪除的結點的前面一個結點,在單向鏈表中,結點中並沒有指向前一個結點的指針,所以我們才從鏈表的頭結點開始按順序查找。

      知道這點后,我們就可以想想其中的一個疑問:為什么我們一定要得到將被刪除結點前面的結點呢?事實上,比起得到前面的結點,我們更加容易得到后面的結點,因為一般的結點中就已經包含了指向后面結點的指針。我們可以把下一個結點的內容復制到需要刪除的結點上覆蓋原有的內容,再把下一個結點刪除,那其實也就是相當於將當前的結點刪除。

      根據這樣的思路,我們可以寫:

void DeleteNode(LisNode** pListHead, ListNode* pToDeleted)
{
      if(!pListHead || !pToBeDeleted)
      {
           return;
      }

      if(pToBeDeleted->m_pNext != NULL)
      {
           ListNode* pNext = pToBeDeleted->m_pNext;
           pToBeDeleted->m_nValue = pNext->m_nValue;
           pToBeDeleted->m_pNext = pNext->m_pNext;

           delete pNext;
           pNext = NULL;
       }

       else if(*pListHead == pToBeDeleted)
       {
            delete pToBeDeleted;
            pToBeDeleted = NULL;
            *pListHead = NULL;
       }

       else
       {
             ListNode* pNode = *pListHead;
             while(pNode->m_pNext != pToBeDeleted)
             {
                   pNode = pNode->m_pNext;
             }

             pNode->m_pNext = NULL;
             delete pToBeDeleted;
             pToBeDeleted = NULL;
        }
}

     首先我們需要注意幾個特殊情況:如果要刪除的結點位於鏈表的尾部,那么它就沒有下一個結點,這時我們就必須從鏈表的頭結點開始,順序遍歷得到該結點的前序結點,並完成刪除操作。還有,如果鏈表中只有一個結點,而我們又要刪除;;鏈表的頭結點,也就是尾結點,這時我們在刪除結點后,還需要把鏈表的頭結點設置為NULL,這種做法重要,因為頭指針是一個指針,當我們刪除一個指針后,如果沒有將它設置為NULL,就不能算是真正的刪除該指針。
     我們接着分析一下為什么該算法的時間復雜度為O(1)。

     對於n- 1個非尾結點而言,我們可以在O(1)時把下一個結點的內存復制覆蓋要刪除的結點,並刪除下一個結點,但對於尾結點而言,由於仍然需要順序查找,時間復雜度為O(N),因此總的時間復雜度為O[((N - 1) * O(1) + O(N)) / N] = O(1),這個也是需要我們會計算的,不然我們無法向面試官解釋,為什么這段代碼的時間復雜度就是O(1)。

     上面的代碼還是有缺點,就是基於要刪除的結點一定在鏈表中,事實上,不一定,但這份責任是交給函數的調用者。

題目六:輸入兩個遞增鏈表,合並為一個遞增鏈表。

      這種題目最直觀的做法就是將一個鏈表的值與其他鏈表的值一一比較。考察鏈表的題目不會要求我們時間復雜度,因為鏈表並不像是數組那樣,可以方便的使用各種排序算法和查找算法。因為鏈表涉及到大量的指針操作,所以鏈表的題目考察的主要是兩個方面:代碼的魯棒性和簡潔性。

      魯棒性就要求我們事先想好足夠的測試用例,事實上,代碼的設計時間並不比編寫時間短,而且設計時間越長,編寫的時間可以越短,只要設計是有效的。我們來想想空指針的情況。如果其中一個鏈表的頭指針是一個空指針,也就是說,該鏈表是一個空鏈表,那么合並后的鏈表應該是另一個鏈表。如果兩個鏈表都是空鏈表,那么合並后的鏈表應該是空鏈表。
     接着就是代碼的簡潔性。事實上,鏈表非常適合遞歸,因為我們在使用鏈表的時候都是使用指針,而不像數組那樣直接使用一個內存塊,因為遞歸的風險,也就是棧溢出可以避免,並且因為鏈表涉及到大量的指針操作,使用遞歸可以讓我們的代碼更加簡潔,而且簡潔的代碼更不容易犯錯,畢竟代碼量越大,可能犯錯的概率也就越大,尤其是操作指針。
ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
{
    if(pHead1 == NULL)
    {
          return pHead2;
    }
    else if(pHead == NULL)
    {
          return pHead1;
    }

    ListNode* pMergedHead = NULL;

    if(pHead->m_nValue < pHead->m_nValue)
    {
          pMergedHead = pHead1;
          pMergedHead->m_pNext = Merge(pHead->m_pNext, pHead2);
    }
    else
    {
          pMergedHead = pHead2;
          pMergedHead->m_pNext = Merge(pHead1, pHead2->m_pNext);
    }

    return pMergedHead;
}

    到現在為止,我們的鏈表都是單鏈表,並且結點的定義都是一般鏈表的定義,但如果面對的是自定義結點組成的鏈表呢?

    像是這樣的鏈表定義:
struct ComplexListNode
{
    int m_nValue;
    ComplexListNode* m_pNext;
    ComplexListNode* m_pSibling;
};

題目七:請實現一個函數實現該鏈表的復制,其中m_pSibling指向的是鏈表中任意一個結點或者NULL。

      這種題目就要求我們具有發現規律的能力了。

      復制鏈表並不難,但是我們會想到效率的問題。

      第一步肯定是要復制每個結點並按照m_pNext連接起來,第二步就是設置每個結點的m_pSibling。我們可以在第一步遍歷的時候就保存每個節點的m_pSibling,這樣就可以節省第二步的遍歷,將時間復雜度控制在O(N),但是這樣子的空間復雜度就是O(N),事實上,鏈表的問題求解和數組不一樣,數組更多考慮的是時間復雜度能否足夠低,而鏈表則考慮空間復雜度能否足夠低。
      一個鏈表的求解如果不能將空間復雜度控制在O(1),完全不能通過面試。
      我們完全可以不用專門用輔助空間來存放m_pSibling,直接就是將復制后的結點連接在原本結點后面,然后將這個鏈表按照奇數和偶數位置拆成兩個子鏈表,其中,偶數位置就是我們要的復制后的鏈表。
ComplexListNode* Clone(ComplexListNode* pHead)
{
      CloneNodes(pHead);
      ConnectSiblingNodes(pHead);
      return ReconnectNodes(pHead);
}

void CloneNodes(ComplexListNode* pHead)
{
      ComplexListNode* pNode = pHead;
      while(pNode != NULL)
      {
           ComplexListNode* pCloned = new ComplexListNode();
           pCloned->m_nValue = pNode->m_nValue;
           pCloned->m_pNext = pNode->m_pNext;
           pCloned->m_pSibling = NULL;
           
           pNode->m_pNext = pCloned;
           
           pNode = pCloned->m_pNext;
      }
}

void ConnectSiblingNode(ComplexListNode* pHead)
{
      ComplexListNode* pNode = pHead;
      while(pNode != NULL)
      {
            ComplexListNode* pCloned = pNode->m_pNext;
            if(pNode->m_pSibling != NULL)
            {
                  pCloned->m_pSibling = pNode->m_pSibling->m_pNext;
            }
            pNode = pCloned->m_pNext;
       }
}

ComplexListNode* ReconnectNode(ComplexListNode* pHead)
{
      ComplexListNode* pNode = pHead;
      ComplexListNode* pClonedHead = NULL;
      ComplexListNode* pClonedNode = NULL;

      if(pNode != NULL)
      {
            pClonedHead = pClonedNode = pNode->m_pNext;
            pNode->m_pNext = pClonedNode->m_pNext;
            pNode = pNode->m_pNext;
      }

      while(pNode != NULL)
      {
           pClonedNode->m_pNext = pNode->m_pNext;
           pClonedNode = pClonedNode->m_pNext;
           pNode->m_pNext = pClonedNode->m_pNext;
           pNode = pNode->m_pNext;
      }

      return pClonedHead;
}
     有些題目並不會直接提到鏈表,但它的解法卻需要我們用鏈表來解決。
題目八: 0,1,3...,n - 1這n個數字排成一個圓圈,從數字0開始每次從這個圓圈里刪除第m個數字。求出這個圓圈里剩下的最后一個數字。
     從題目要求中我們無法直觀的感知該問題,得從一個測試用例開始。
     假設0,1,2,3,4這5個數字組成一個圓圈,如果我們從數字0開始每次刪除第3個數字,則刪除的前四個數字是2,0,4,1,3。
     這就是有名的約瑟夫環問題,它有一個簡潔的數學公式,但除非我們有很深的數學素養和數學靈敏性,否則是很難一下子想出來的。
     程序員最普遍的方法就是想盡一切辦法讓我們的代碼通過測試用例。
     既然是一個圓圈,我們自然就會聯想到環形鏈表:
int LastRemaining(unsigned int n, unsigned int m)
{
    if(n < 1 || m < 1)
    {
         return -1;
    }

    unsigned int i = 0;

    lisg<int> numbers;
    for(i = 0; i < n; ++i)
    {
         numbers.push_back(i);
    }

    list<int> :: iterator current = numbers.begin();
    while(numbers.size() > 1)
    {
        for(int i = 1l i < m; ++i)
        {
              current++;
              if(current == numbers.end()){
                    current = number.begin();
              }
         }

         list<int> :: iterator next = ++current;
         if(next == numbers.end()){
               next = numbers.begin();
         }

         --current;
         numbers.erase(current);
         current = next;
    }

    return *(current);
}
int LastRemaining(unsigned int n, unsigned int m)
{
    if(n < 1 || m < 1)
    {
         return -1;
    }

    unsigned int i = 0;

    lisg<int> numbers;
    for(i = 0; i < n; ++i)
    {
         numbers.push_back(i);
    }

    list<int> :: iterator current = numbers.begin();
    while(numbers.size() > 1)
    {
        for(int i = 1l i < m; ++i)
        {
              current++;
              if(current == numbers.end()){
                    current = number.begin();
              }
         }

         list<int> :: iterator next = ++current;
         if(next == numbers.end()){
               next = numbers.begin();
         }

         --current;
         numbers.erase(current);
         current = next;
    }

    return *(current);
}

     我們可以用std :: list來模擬一個環形鏈表,但因為std :: list本身並不是一個環形結構,所以我們還要在迭代器掃描到鏈表末尾的時候,把迭代器移到鏈表的頭部。 

     如果是使用數學公式的話,代碼就會非常簡單:

int LastRemaining(unsigend int n, unsigned int m)
{
    if(n < 1 || m < 1)
    {
          return -1;
    }

    int last = 0;
    for(int i = 2; i <= n; ++i)
   {
       last = (last + m) % i;
   }
   
   return last;
}

     這就是數學的魅力,並且它的時間復雜度是O(N),空間復雜度是O(1)。

 

 

 

 

     

     

     


    

 


免責聲明!

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



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