1. 程式人生 > >關於連結串列演算法題的雙指標

關於連結串列演算法題的雙指標

       經常能夠碰到連結串列的題,當用一個指標遍歷來解決問題的時候,不是無法解決就是效率不佳,典型的就是需要多次遍歷且需要額外的儲存空間。在這種情況下,可以嘗試用兩個指標來遍歷連結串列,而兩指標遍歷連結串列又可以分為兩種情況:1、讓其中一個指標遍歷快一點,比如一次在連結串列中走上兩步;2、讓其中一個指標現在連結串列中走上若干步。

       這裡舉三個連結串列相關的題目。

1、 判定連結串列中是否環

       第一種方法:可以對連結串列的元素進行標記,如果在預見NULL節點之前再次碰見已標記節點就存在環。缺點:需要改變節點內容,而節點一般是隻讀的。

       第二種方法:訪問每一個元素將其儲存在陣列中,這個時候儲存的元素是什麼,如果連結串列中的元素有重複的值,難道儲存節點地址?並且開闢了O(n)的額外空間,如果記憶體不夠呢?

       第三種方法:如果假定連結串列存在環,那麼環在前N個元素之中,此時可以設定一個指標指向連結串列的頭部,然後遍歷後N-1個元素,看是否是指標所指元素,如果都不同,指標指向後一個元素,然後遍歷後N-2個元素。缺點:這個演算法複雜度為O(n^2),而且建立在一個前提條件之下。

       最優的答案:設定兩個指標,開始都指向連結串列頭,然後其中一個指標每次向前走一步,另一個指標每次向前走兩步,如果快的遇到NULL了,證明該連結串列中沒有環,如果有環,快的指標每次都要比慢的多走一步,最終兩個指標會相遇,(注意:這裡快指標不會跳過慢指標而不相遇,因為它每次都只比慢指標多走一個單位)

bool judge(list *head)
{
    if(head == NULL)
    {
        return false;//沒有環
    }

    list *pFast = head;
    list *pSlow = head;

    while(pFast->next != NULL && pFast->next->next != NULL)
    {
        pFast = pFast->next->next;
        pSlow = pSlow->next;

        if(pFast == pSlow)
        {
            return true;
        }
    }

    return false;
}

2、找到連結串列的中間節點

       受上一題的啟發可以運用兩個速度不同的指標來解決,快指標每次走兩步,慢指標每次走一步,這樣當快指標到達連結串列尾部的時候,慢指標就指向了連結串列的中間節點。

3、 輸出連結串列中倒數第K個數

第一種方法:單個指標遍歷兩次,首先遍歷一次連結串列統計總元素個數N,那麼所要找的倒數第K個元素即為第N-K+1個元素。

第二種方法:雙指標遍歷一次,首先前指標先向前走K-1步,即初始的時候前指標指向第K個元素,然後後指標指向第一個元素,然後同步向後單步走,當後指標指向NULL的時候,前指標指向倒數第K個元素。

//注意程式魯棒性,輸入引數檢查,元素個數不足檢查。
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                               //要考慮到連結串列的元素不足K個的情況
        {
            return NULL;
        }
    }

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

    return pBehind;
}

4、 兩連結串列的第一個公共結點——輸入兩個連結串列,找出它們的第一個公共結點。

unsigned int GetListLength(ListNode* pHead)
{
    unsigned int nLength = 0;
    ListNode* pNode = pHead;
    while(pNode != NULL)
    {
        ++ nLength;
        pNode = pNode->m_pNext;
    }
	
    return nLength;
}
ListNode* FindFirstCommonNode( ListNode *pHead1, ListNode *pHead2)
{
    // 得到兩個連結串列的長度
    unsigned int nLength1 = GetListLength(pHead1);
    unsigned int nLength2 = GetListLength(pHead2);
    int nLengthDif = nLength1 - nLength2;
 
    ListNode* pListHeadLong = pHead1;
    ListNode* pListHeadShort = pHead2;
    if(nLength2 > nLength1)
    {
        pListHeadLong = pHead2;
        pListHeadShort = pHead1;
        nLengthDif = nLength2 - nLength1;
    }
 
    // 先在長連結串列上走幾步,再同時在兩個連結串列上遍歷
    for(int i = 0; i < nLengthDif; ++ i)
        pListHeadLong = pListHeadLong->m_pNext;
 
    while((pListHeadLong != NULL) && 
        (pListHeadShort != NULL) &&
        (pListHeadLong != pListHeadShort))
    {
        pListHeadLong = pListHeadLong->m_pNext;
        pListHeadShort = pListHeadShort->m_pNext;
    }
 
    // 得到第一個公共結點
    ListNode* pFisrtCommonNode = pListHeadLong;
 
    return pFisrtCommonNode;
}

        順便說一句,單鏈表翻轉的演算法運用了3指標,這是為了記錄前後節點。