程式碼隨想錄演算法day4 - 連結串列2

酱油黑龙發表於2024-08-30

題目1 24. 兩兩交換連結串列中的節點

給你一個連結串列,兩兩交換其中相鄰的節點,並返回交換後連結串列的頭節點。你必須在不修改節點內部的值的情況下完成本題(即,只能進行節點交換)。

示例 1:

img

輸入:head = [1,2,3,4]
輸出:[2,1,4,3]

示例 2:

輸入:head = []
輸出:[]

示例 3:

輸入:head = [1]
輸出:[1]

提示:

  • 連結串列中節點的數目在範圍 [0, 100]
  • 0 <= Node.val <= 100

思路

無虛擬頭

這道題中每次交換兩個節點時要改變三個指標的指向,所以我們需要三個變數來記錄這三個相鄰節點。在第一次交換節點(在滿足節點數量>=2)時需要注意只需要更改頭節點和後一個節點就行了,第一次交換節點只需要兩個節點,之後的交換才需要三個節點。

程式碼

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        if(head == nullptr)
        {
            return head;
        }
        ListNode* preNode = head;
        //如果節點有兩個以上,則移動頭指標到第二個節點
        if(head->next != nullptr)
        {
            head = head->next;
        }
        else
        {
            return head;
        }
        ListNode* ppreNode = nullptr;
        ListNode* curNode = preNode->next;
        for(int i = 0; curNode != nullptr; i++)
        {
            //i % 2 == 0判斷表示隔一個節點後進行兩兩交換
            if(i % 2 == 0)
            {
                ListNode* tmpNode = curNode->next;
                curNode->next = preNode;
                preNode->next = tmpNode;
                if(ppreNode != nullptr)
                    ppreNode->next = curNode;
                curNode = tmpNode;
            }
            else
            {
                ppreNode = preNode;
                preNode = curNode;
                curNode = curNode->next;
            }
        }
        return head;
    }
};

有虛擬頭

有虛擬頭的情況比無虛擬頭簡單,因為不用額外考慮第一次只修改兩次指標的情況,所有交換的情況都是三個指標進行操作。要注意的就是在實際專案中要在返回前釋放掉虛擬頭節點的記憶體,避免記憶體洩漏。

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        if(head == nullptr || head->next == nullptr)
        {
            return head;
        }
        ListNode* dumNode = new ListNode(-1, head);
        ListNode* ppreNode = dumNode,
                * preNode  = dumNode->next,
                * curNode  = preNode->next;
        while(curNode != nullptr)
        {
            ppreNode->next = curNode;
            ListNode* tmpNode = curNode->next;
            curNode->next = preNode;
            preNode->next = tmpNode;
            if(tmpNode != nullptr && tmpNode->next != nullptr)
            {
                ppreNode = preNode;
                preNode = tmpNode;
                curNode = tmpNode->next;
            }
            else
            {
                break;
            }
        }
        head = dumNode->next;
        delete dumNode;
        return head;
    }
};

題目2 19. 刪除連結串列的倒數第 N 個結點

給你一個連結串列,刪除連結串列的倒數第 n 個結點,並且返回連結串列的頭結點。

示例 1:

img

輸入:head = [1,2,3,4,5], n = 2
輸出:[1,2,3,5]

示例 2:

輸入:head = [1], n = 1
輸出:[]

示例 3:

輸入:head = [1,2], n = 1
輸出:[1]

提示:

  • 連結串列中結點的數目為 sz
  • 1 <= sz <= 30
  • 0 <= Node.val <= 100
  • 1 <= n <= sz

思路

暴力迭代法(兩輪遍歷)

暴力迭代法透過遍歷兩輪連結串列來刪除倒數第n個節點,第一輪用來計算連結串列的節點總數,第二輪用於命中目標節點與前一個節點來進行刪除操作。

額外指標陣列(一輪遍歷)

因為題目中給出了結點的數量範圍,因此建立一個固定大小的listNode指標陣列,用一輪遍歷統計結點總數並將結點按序放入到陣列中。之後透過計算能命中倒數第n個節點的位置,最終透過指標陣列來刪除指標。

程式碼

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        if(head == nullptr)
        {
            return head;
        }
        ListNode* curNode = new ListNode(0, head);
        ListNode* lst[32];
        lst[0] = curNode;
        int num = 0;
        for(; curNode != nullptr; num++)
        {
            curNode = curNode->next;
            lst[num + 1] = curNode;
        }
        //goal + n = num - > goal = num - n
        //num - n為要刪除的節點位置,對其-1和+1為前和後結點
        lst[num - n - 1]->next = lst[num - n + 1];
        delete lst[num - n];
        head = lst[0]->next;
        delete lst[0];
        return head;
    }
};

雙指標法

假設連結串列長度是n(n > 0),要刪除的結點時倒數第y( 0 < y <= n )個,左指標的位置為lft,右指標的位置為rht,在連結串列前用額外的結點(dummyNode)指向連結串列頭。讓lft和rht的初始位置都為dummyNode,先讓rht向後面的結點移動y次,此時lft和rht的正好相差y個結點。之後同時讓lft和rht向後面結點移動,直到rht指向為空終止,此時lft對應的指標指向的正好是倒數第y個結點。因此如果想刪除第n個結點就需要倒數第n+1個結點的位置,程式碼也因此能寫出來。

程式碼

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummyNode = new ListNode(0, head);
        ListNode* lft = dummyNode,
                * rht = dummyNode;
        for(int i = 0; i <= n; i++)
        {
            rht = rht->next;
        }
        while(rht != nullptr)
        {
            lft = lft->next;
            rht = rht->next;
        }
        lft->next = lft->next->next;
        head = dummyNode->next;
        delete dummyNode;
        return head;
    }
};

題目3 07. 連結串列相交

給你兩個單連結串列的頭節點 headAheadB ,請你找出並返回兩個單連結串列相交的起始節點。如果兩個連結串列沒有交點,返回 null

圖示兩個連結串列在節點 c1 開始相交

img

題目資料 保證 整個鏈式結構中不存在環。

注意,函式返回結果後,連結串列必須 保持其原始結構

示例 1:

img

輸入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
輸出:Intersected at '8'
解釋:相交節點的值為 8 (注意,如果兩個連結串列相交則不能為 0)。
從各自的表頭開始算起,連結串列 A 為 [4,1,8,4,5],連結串列 B 為 [5,0,1,8,4,5]。
在 A 中,相交節點前有 2 個節點;在 B 中,相交節點前有 3 個節點。

示例 2:

img

輸入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
輸出:Intersected at '2'
解釋:相交節點的值為 2 (注意,如果兩個連結串列相交則不能為 0)。
從各自的表頭開始算起,連結串列 A 為 [0,9,1,2,4],連結串列 B 為 [3,2,4]。
在 A 中,相交節點前有 3 個節點;在 B 中,相交節點前有 1 個節點。

示例 3:

img

輸入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
輸出:null
解釋:從各自的表頭開始算起,連結串列 A 為 [2,6,4],連結串列 B 為 [1,5]。
由於這兩個連結串列不相交,所以 intersectVal 必須為 0,而 skipA 和 skipB 可以是任意值。
這兩個連結串列不相交,因此返回 null 。

提示:

  • listA 中節點數目為 m
  • listB 中節點數目為 n
  • 0 <= m, n <= 3 * 104
  • 1 <= Node.val <= 105
  • 0 <= skipA <= m
  • 0 <= skipB <= n
  • 如果 listAlistB 沒有交點,intersectVal0
  • 如果 listAlistB 有交點,intersectVal == listA[skipA + 1] == listB[skipB + 1]

思路

這道題只要將兩個連結串列的尾部對齊就好做了,首先判斷兩個連結串列的長短並在第一次遍歷結束後透過最後一個結點判斷兩連結串列是否相交,若相交則用指標指向長連結串列的子連結串列結點,使得子連結串列長度和另一個短連結串列長度一致,最後一一比較就找到了相交的結點。

程式碼

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        if(headA == nullptr || headB == nullptr)
        {
            return nullptr;
        }
        ListNode* aNode = headA,
                * bNode = headB;
        int lenA = 0, lenB = 0;
        while(aNode->next != nullptr)
        {
            aNode = aNode->next;
            lenA++;
        }
        while(bNode->next != nullptr)
        {
            bNode = bNode->next;
            lenB++;
        }
        if(aNode != bNode)
        {
            return nullptr;
        }
        int len;
#define FIND_UNODE(LLIST, SLIST, LLEN, SLEN, LEN)\
        LEN = LLEN - SLEN;                       \
        while(LEN--){                            \
            LLIST = LLIST->next;                 \
        }                                        \
        while(LLIST != SLIST){                   \
            LLIST= LLIST->next;                  \
            SLIST = SLIST->next;                 \
        }                  
        if(lenA > lenB)
        {
            FIND_UNODE(headA, headB, lenA, lenB, len);
        }
        else
        {
            FIND_UNODE(headB, headA, lenB, lenA, len);
        }
#undef FIND_UNODE
        return headA;
    }
};

題目4 142. 環形連結串列 II

給定一個連結串列的頭節點 head ,返回連結串列開始入環的第一個節點。 如果連結串列無環,則返回 null

如果連結串列中有某個節點,可以透過連續跟蹤 next 指標再次到達,則連結串列中存在環。 為了表示給定連結串列中的環,評測系統內部使用整數 pos 來表示連結串列尾連線到連結串列中的位置(索引從 0 開始)。如果 pos-1,則在該連結串列中沒有環。注意:pos 不作為引數進行傳遞,僅僅是為了標識連結串列的實際情況。

不允許修改 連結串列。

示例 1:

img

輸入:head = [3,2,0,-4], pos = 1
輸出:返回索引為 1 的連結串列節點
解釋:連結串列中有一個環,其尾部連線到第二個節點。

示例 2:

img

輸入:head = [1,2], pos = 0
輸出:返回索引為 0 的連結串列節點
解釋:連結串列中有一個環,其尾部連線到第一個節點。

示例 3:

img

輸入:head = [1], pos = -1
輸出:返回 null
解釋:連結串列中沒有環。

提示:

  • 連結串列中節點的數目範圍在範圍 [0, 104]
  • -105 <= Node.val <= 105
  • pos 的值為 -1 或者連結串列中的一個有效索引

進階:你是否可以使用 O(1) 空間解決此題?

思路

雙指標

這道題更像是數學計算題。。。利用雙指標中快指標的一次多步和慢指標的一次一步來遍歷整個連結串列,直到兩個指標相遇,最後在透過新指標和快指標的進一步遍歷及相遇來確定環的入口。

程式碼

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        if(head == nullptr)
        {
            return head;
        }
        ListNode* fast = head,
                * slow = head;
        while(fast->next != nullptr && fast->next->next != nullptr)
        {
            fast = fast->next->next;
            slow = slow->next;
            if(slow == fast)
            {
                ListNode* markNode = head;
                while(markNode != fast)
                {
                    markNode = markNode->next;
                    fast = fast->next;
                }
                return markNode;
            }
        }
        return nullptr;
    }
};

set集合篩選法

眾所周知,在C++裡有set模板類可以篩除重複的元素,我們用這個類儲存連結串列裡的所有結點,並對每一次插值前和插值後比較set物件中指標的總數,若指標的總數沒發生變化就說明已經進入環入口。使用這種方法要額外消耗記憶體,但在接近最壞的情況時(即連結串列非常長,環也很大的情況下,用這種方法能節省最後雙指標法尋找遍歷的時間)。

程式碼

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        if(head == nullptr)
        {
            return head;
        }
        set<ListNode*> roundSet;
        int preNum = INT_MAX;
        while(head != nullptr)
        {
            preNum = roundSet.size();
            roundSet.insert(head);
            if(preNum == roundSet.size())
                break;
            head = head->next;
        }
        return head;
    }
};

相關文章