LeetCode刷題(十九)-----鏈表-------medium部分(Java、C++)

138. 複製帶隨機指針的鏈表

給定一個鏈表,每個節點包含一個額外增加的隨機指針,該指針可以指向鏈表中的任何節點或空節點。
要求返回這個鏈表的深拷貝。 
示例:

在這裏插入圖片描述
輸入:
{“KaTeX parse error: Expected '}', got 'EOF' at end of input: …:"1","next": {"id”:“2”,“next”:null,“random”:
{“KaTeX parse error: Expected 'EOF', got '}' at position 9: ref":"2"}̲,"val":2},"rand…ref”:“2”},“val”:1}
解釋:
節點 1 的值是 1,它的下一個指針和隨機指針都指向節點 2 。
節點 2 的值是 2,它的下一個指針指向 null,隨機指針指向它自己。
提示:你必須返回給定頭的拷貝作爲對克隆列表的引用。

思路一:
首先,我們來看一下有向鏈表:
在這裏插入圖片描述
在上圖中,對於一個節點,它的 next 指針指向鏈表中的下一個節點。 next 指針是通常有向鏈表中有的部分且將所有節點 鏈接 起來。圖中有趣的一點,也是這題有趣的一點在於 random 指針,正如名字所示,它可以指向鏈表中的任一節點也可以爲空。

方法 1:回溯
想法

回溯算法的第一想法是將鏈表想象成一張圖。鏈表中每個節點都有 2 個指針(圖中的邊)。因爲隨機指針給圖結構添加了隨機性,所以我們可能會訪問相同的節點多次,這樣就形成了環。
在這裏插入圖片描述
上圖中,我們可以看到隨機指針指向了前一個節點,因此成環。我們需要考慮這種環的實現。

此方法中,我們只需要遍歷整個圖並拷貝它。拷貝的意思是每當遇到一個新的未訪問過的節點,你都需要創造一個新的節點。遍歷按照深度優先進行。我們需要在回溯的過程中記錄已經訪問過的節點,否則因爲隨機指針的存在我們可能會產生死循環。

算法
1.從頭指針開始遍歷整個圖。
我們將鏈表看做一張圖。下圖對應的是上面的有向鏈表的例子,Head 是圖的出發節點。
在這裏插入圖片描述
2.當我們遍歷到某個點時,如果我們已經有了當前節點的一個拷貝,我們不需要重複進行拷貝。
3.如果我們還沒拷貝過當前節點,我們創造一個新的節點,並把該節點放到已訪問字典中,即:
visited_dictionary[current_node] = cloned_node_for_current_node.
4.我們針對兩種情況進行回溯調用:一個順着 random 指針調用,另一個沿着 next 指針調用。步驟 1 中將 random 和 next 指針分別紅紅色和藍色標註。然後我們分別對兩個指針進行函數遞歸調用:
cloned_node_for_current_node.next = copyRandomList(current_node.next);
cloned_node_for_current_node.random=copyRandomList(current_node.random);
在這裏插入圖片描述
複雜度分析
時間複雜度:O(N) ,其中 N是鏈表中節點的數目。
空間複雜度:O(N)。如果我們仔細分析,我們需要維護一個回溯的棧,同時也需要記錄已經被深拷貝過的節點,也就是維護一個已訪問字典。漸進時間複雜度爲 O(N)。

方法 2: O(N)空間的迭代
想法
迭代算法不需要將鏈表視爲一個圖。當我們在迭代鏈表時,我們只需要爲random指針和next指針指向的未訪問過節點創造新的節點並賦值即可。
算法
1.從 head 節點開始遍歷鏈表。下圖中,我們首先創造新的 head 拷貝節點。拷貝的節點如下圖虛線所示。實現中,我們將該新建節點的引用也放入已訪問字典中。
在這裏插入圖片描述
2.random 指針
如果當前節點 i的 random 指針只想一個節點 j且節點 j已經被拷貝過,我們將直接使用已訪問字典中該節點的引用而不會新建節點。

如果當前節點 i的 random 指針只想的節點 j還沒有被拷貝過,我們就對 j節點創建對應的新節點,並把它放入已訪問節點字典中。

下圖中,A 的 random 指針指向的節點 C。前圖中可以看出,節點 C還沒有被訪問過,所以我們創造一個拷貝的 C’節點與之對應,並將它添加到已訪問字典中。
在這裏插入圖片描述
3.next 指針
如果當前節點 ii 的 next 指針指向的節點 jj 在已訪問字典中已有拷貝,我們直接使用它的拷貝節點。

如果當前節點 ii 的next 指針指向的節點 jj 還沒有被訪問過,我們創建一個對應節點的拷貝,並放入已訪問字典。

下圖中,A節點的next 指針指向節點B。節點B在前面的圖中還沒有被訪問過,因此我們創造一個新的拷貝 B’節點,並放入已訪問字典中。
在這裏插入圖片描述
4.我們重複步驟 2 和步驟 3 ,直到我們到達鏈表的結尾。

下圖中,節點B的 random 指針指向的節點 A已經被訪問過了,因此在步驟 2 中,我們不會創建新的拷貝,只將節點 B’的 random 指針指向克隆節點 A’。
同樣的,節點B的next 指針指向的節點C 已經訪問過,因此在步驟 3 中,我們不會創建新的拷貝,而直接將 B’的 next 指針指向已經存在的拷貝節點 C’。
在這裏插入圖片描述
複雜度分析
時間複雜度:O(N)。因爲我們需要將原鏈表逐一遍歷。
空間複雜度:O(N)。 我們需要維護一個字典,保存舊的節點和新的節點的對應。因此總共需要 N個節點,需要 O(N)的空間複雜度。

方法 3:O(1)空間的迭代
想法
與上面提到的維護一箇舊節點和新節點對應的字典不同,我們通過扭曲原來的鏈表,並將每個拷貝節點都放在原來對應節點的旁邊。這種舊節點和新節點交錯的方法讓我們可以在不需要額外空間的情況下解決這個問題。讓我們看看這個算法如何工作
算法

  1. 遍歷原來的鏈表並拷貝每一個節點,將拷貝節點放在原來節點的旁邊,創造出一箇舊節點和新節點交錯的鏈表。
    在這裏插入圖片描述
    在這裏插入圖片描述
    如你所見,我們只是用了原來節點的值拷貝出新的節點。原節點 next 指向的都是新創造出來的節點。
    cloned_node.next = original_node.next
    original_node.next = cloned_node
    2.迭代這個新舊節點交錯的鏈表,並用舊節點的 random 指針去更新對應新節點的 random 指針。比方說, B 的 random 指針指向 A ,意味着 B’ 的 random 指針指向 A’ 。
    在這裏插入圖片描述
    3.現在random指針已經被賦值給正確的節點,next指針也需要被正確賦值,以便將新的節點正確鏈接同時將舊節點重新正確鏈接。
    在這裏插入圖片描述
    複雜度分析
    • 時間複雜度:O(N)
    • 空間複雜度:O(1)

作者:LeetCode
鏈接:https://leetcode-cn.com/problems/copy-list-with-random-pointer/solution/fu-zhi-dai-sui-ji-zhi-zhen-de-lian-biao-by-leetcod/
來源:力扣(LeetCode)
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

思路二:
想法一哈希
藉助哈希保存節點信息。
代碼
時間複雜度:O(n)
空間複雜度:O(n)
在這裏插入圖片描述
想法二:原地複製

  1. 複製節點,同時將複製節點鏈接到原節點後面,如A->B->C 變爲 A->A’->B->B’->C->C’。
  2. 設置節點random值。
  3. 將複製鏈表從原鏈表分離。
    代碼
    時間複雜度:O(n)
    空間複雜度:O(1)
    在這裏插入圖片描述
    在這裏插入圖片描述
    參考鏈接:https://leetcode-cn.com/problems/copy-list-with-random-pointer/solution/138-fu-zhi-dai-sui-ji-zhi-zhen-de-lian-biao-ha-xi-/

我的:

/*
// Definition for a Node.
class Node {
public:
    int val;
    Node* next;
    Node* random;
    
    Node(int _val) {
        val = _val;
        next = NULL;
        random = NULL;
    }
};
*/
class Solution {
public:
    Node* copyRandomList(Node* head) 
    {
        if(head == nullptr)
        {
            return head;
        }
        Node *node = head;
        //1.將複製節點添加到原節點後面
        while(node != nullptr)
        {
            Node *copy = new Node(node -> val,nullptr,nullptr);
            copy -> next = node -> next;
            node -> next = copy;
            node = copy -> next;
        }
        //2.複製random節點
        node = head;
        while(node != nullptr)
        {
            if(node -> random != nullptr)
            {
                node -> next -> random = node -> random -> next;
            }
            node = node -> next -> next;
        } 
        //3.分離鏈表
        node = head;
        Node *newHead = head -> next;
        Node *newNode = newHead;
        while(node != nullptr)
        {
            node -> next = node -> next -> next;
            if(newNode -> next != nullptr)
            {
                newNode -> next = newNode -> next -> next;
            }
            node = node -> next;
            newNode = newNode -> next;
        }
        return newHead;
    }
};

19. 刪除鏈表的倒數第N個節點

給定一個鏈表,刪除鏈表的倒數第n個節點,並且返回鏈表的頭結點。
示例:
給定一個鏈表: 1->2->3->4->5, 和 n = 2.
當刪除了倒數第二個節點後,鏈表變爲 1->2->3->5.
說明:
給定的n保證是有效的。
進階:
你能嘗試使用一趟掃描實現嗎?

思路一:
本文適用於初學者。它介紹了以下內容:鏈表的遍歷和刪除其末尾的第 n 個元素。
方法一:兩次遍歷算法
思路
我們注意到這個問題可以容易地簡化成另一個問題:刪除從列表開頭數起的第(L−n+1) 個結點,其中 L是列表的長度。只要我們找到列表的長度 L,這個問題就很容易解決。
算法
首先我們將添加一個啞結點作爲輔助,該結點位於列表頭部。啞結點用來簡化某些極端情況,例如列表中只含有一個結點,或需要刪除列表的頭部。在第一次遍歷中,我們找出列表的長度L。然後設置一個指向啞結點的指針,並移動它遍歷列表,直至它到達第 (L−n) 個結點那裏。我們把第 (L−n) 個結點的 next 指針重新鏈接至第 (L - n + 2)個結點,完成這個算法。
在這裏插入圖片描述
圖 1. 刪除列表中的第 L - n + 1 個元素
在這裏插入圖片描述
複雜度分析
時間複雜度: O(L),該算法對列表進行了兩次遍歷,首先計算了列表的長度 L其次找到第 (L−n)個結點。操作執行了2L-n步,時間複雜度爲 O(L)。
空間複雜度:O(1),我們只用了常量級的額外空間。
方法二:一次遍歷算法
算法

上述算法可以優化爲只使用一次遍歷。我們可以使用兩個指針而不是一個指針。第一個指針從列表的開頭向前移動 n+1步,而第二個指針將從列表的開頭出發。現在,這兩個指針被 n個結點分開。我們通過同時移動兩個指針向前來保持這個恆定的間隔,直到第一個指針到達最後一個結點。此時第二個指針將指向從最後一個結點數起的第 n個結點。我們重新鏈接第二個指針所引用的結點的 next 指針指向該結點的下下個結點。
在這裏插入圖片描述
圖 2. 刪除鏈表的倒數第 N 個元素
在這裏插入圖片描述
複雜度分析
時間複雜度:O(L),該算法對含有 L 個結點的列表進行了一次遍歷。因此時間複雜度爲 O(L)。
空間複雜度:O(1),我們只用了常量級的額外空間。

作者:LeetCode
鏈接:https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/solution/shan-chu-lian-biao-de-dao-shu-di-nge-jie-dian-by-l/
來源:力扣(LeetCode)
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

思路二:
採取雙重遍歷肯定是可以解決問題的,但題目要求我們一次遍歷解決問題,那我們的思路得發散一下。

我們可以設想假設設定了雙指針 p 和 q 的話,當 q 指向末尾的 NULL,p 與 q 之間相隔的元素個數爲 n 時,那麼刪除掉 p 的下一個指針就完成了要求。

  1. 設置虛擬節點 dummyHead 指向 head
  2. 設定雙指針 p 和 q,初始都指向虛擬節點 dummyHead
  3. 移動 q,直到 p 與 q 之間相隔的元素個數爲 n
  4. 同時移動 p 與 q,直到 q 指向的爲 NULL
  5. 將 p 的下一個節點指向下下個節點
    代碼實現
    在這裏插入圖片描述
    作者:MisterBooo
    鏈接:https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/solution/dong-hua-tu-jie-leetcode-di-19-hao-wen-ti-shan-chu/
    來源:力扣(LeetCode)
    著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
    我的:
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) 
    {
        ListNode* dummyHead = new ListNode(0);
        dummyHead -> next = head;

        ListNode* p = dummyHead;
        ListNode* q = dummyHead;
        for(int i = 0;i<n + 1 ; i++)
        {
            p = p -> next;
        }     
        while(p)
        {
            p = p -> next;
            q = q -> next;
        }
        q -> next = q -> next -> next;
        return dummyHead -> next;
    }
};

2. 兩數相加

給出兩個非空的鏈表用來表示兩個非負的整數。其中,它們各自的位數是按照逆序的方式存儲的,並且它們的每個節點只能存儲一位數字。
如果,我們將這兩個數相加起來,則會返回一個新的鏈表來表示它們的和。
您可以假設除了數字 0 之外,這兩個數都不會以 0開頭。

示例:
輸入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
輸出:7 -> 0 -> 8
原因:342 + 465 = 807

思路二:
解法1:
整體思路:
將長度較短的鏈表在末尾補零使得兩個連表長度相等,再一個一個元素對其相加(考慮進位)
具體步驟:

  1. 獲取兩個鏈表所對應的長度
  2. 在較短的鏈表末尾補零
  3. 對齊相加考慮進位
    實現代碼:
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        int len1=1;//記錄l1的長度
        int len2=1;//記錄l2的長度
        ListNode* p=l1;
        ListNode* q=l2;
        while(p->next!=NULL)//獲取l1的長度
        {
            len1++;
            p=p->next;
        }
        while(q->next!=NULL)//獲取l2的長度
        {
            len2++;
            q=q->next;
        }
        if(len1>len2)//l1較長,在l2末尾補零
        {
            for(int i=1;i<=len1-len2;i++)
            {
                q->next=new ListNode(0);
                q=q->next;
            }
        }
        else//l2較長,在l1末尾補零
        {
            for(int i=1;i<=len2-len1;i++)
            {
                p->next=new ListNode(0);
                p=p->next;
            }
        }
        p=l1;
        q=l2;
        bool count=false;//記錄進位
        ListNode* l3=new ListNode(-1);//存放結果的鏈表
        ListNode* w=l3;//l3的移動指針
        int i=0;//記錄相加結果
        while(p!=NULL&&q!=NULL)
        {
            i=count+p->val+q->val;
            w->next=new ListNode(i%10);
            count=i>=10?true:false;
            w=w->next;
            p=p->next;
            q=q->next;
        }
        if(count)//若最後還有進位
        {
            w->next=new ListNode(1);
            w=w->next;
        }
        return l3->next; 
    }
};

解法2:
整體思路:
不對齊補零,若鏈表不爲空則用sum(代表每個位的和的結果)加上,考慮進位。
實現代碼:

class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode* head=new ListNode(-1);//存放結果的鏈表
        ListNode* h=head;//移動指針
        int sum=0;//每個位的加和結果
        bool carry=false;//進位標誌
        while(l1!=NULL||l2!=NULL)
        {
            sum=0;
            if(l1!=NULL)
            {
                sum+=l1->val;
                l1=l1->next;
            }
            if(l2!=NULL)
            {
                sum+=l2->val;
                l2=l2->next;
            }
            if(carry)
                sum++;
            h->next=new ListNode(sum%10);
            h=h->next;
            carry=sum>=10?true:false;
        }
        if(carry)
        {
            h->next=new ListNode(1);
        }
        return head->next;
    }
};

作者:chenlele
鏈接:https://leetcode-cn.com/problems/add-two-numbers/solution/liang-shu-xiang-jia-by-gpe3dbjds1/
來源:力扣(LeetCode)
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

我的:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) 
    {    
        ListNode* head = new ListNode(-1);
        ListNode* h = head;
        bool carry = false;
        int sum = 0;
        while(l1 != NULL || l2 != NULL)
        {
            sum = 0;
            if(l1)
            {
                sum = sum + l1 -> val;
                l1 = l1 -> next;
            }
            if(l2)
            {
                sum = sum + l2->val;
                l2 = l2 -> next;
            }
            if(carry)
            {
                sum = sum + 1;
            }
            h -> next = new ListNode(sum%10);
            h = h -> next;
            carry = sum >= 10? true:false;
        }
        if(carry)
        {
            h -> next = new ListNode(1);
        }
        return head -> next;
    }
};

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章