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

      數據結構和算法,是咱們程序設計最重要的兩大元素,能夠說,咱們的編程,都是在選擇和設計合適的數據結構來存放數據,而後再用合適的算法來處理這些數據。java

      在面試中,最常常被說起的就是鏈表,由於它簡單,但又由於須要對指針進行操做,凡是涉及到指針的,都須要咱們具備良好的編程基礎才能確保代碼沒有任何錯誤。node

      鏈表是一種動態的數據結構,由於在建立鏈表時,咱們不須要知道鏈表的長度,當插入一個結點時,只須要爲該結點分配內存,而後調整指針的指向來確保新結點被鏈接到鏈表中。因此,它不像數組,內存是一次性分配完畢的,而是每添加一個結點分配一次內存。正是由於這點,因此它沒有閒置的內存,比起數組,空間效率更高。程序員

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

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)。

相關文章
相關標籤/搜索