ch6 - 鏈表Linked List

鏈表大部分是偏實踐的,偏算法的很少。

目錄:

  1. 鏈表基礎知識
  2. Reverse Nodes in k-Group (450 in linkcode)
  3. Copy List with Random Pointer (105 in linkcode)
  4. Linked List cycle (102 、103 in linkcode)
  5. Sort List (98 in linkcode)(★★★★★)
  6. merge-two-sorted-lists (165 in linkcode)
  7. insert-into-a-cyclic-sorted-list (599 in linkcode)

1.鏈表基礎知識

1.1 鏈表指針的賦值,不改變鏈表的結構和節點本身的值。如兩個指針n1,n2,當執行n1 = n2之後,n1本來指向的節點值不變,只是n1現在指向n2了。

1.2 如果要改變鏈表結構,一定要用 node.屬性 = xx

1.3 當需要訪問某節點的屬性時,無腦判斷一個節點或一個節點的next是否爲空。

1.4 指針佔用的內存空間大小是4byte, 鏈表節點佔用的空間不一定是結構體或類中的所有變量所佔內存的和。鏈表的內存空間在heap中,node1等指針是在stack中。如

class node{
public:
    int val;
    double x;
    node* next;
    node(int v){ val = v; }
};

cout<<sizeof(n)<<endl;
cout<<sizeof(n.val)<<endl;
cout<<sizeof(n.x)<<endl;
cout<<sizeof(n.next)<<endl;

在這裏插入圖片描述

class node{
public:
    int val;
    node* next;
    node(int v){ val = v; }
};

cout<<sizeof(n)<<endl;    //所佔內存空間是8字節。

2.Reverse Nodes in k-Group (450 in linkcode)

2.1.Reverse Linked List(35 in Linkcode)

基本思路:類似交換兩個整數的代碼。

1->2->3->null 變成:
null<-1<-2<-3

class Solution {
public:
    /*
     * @param head: n
     * @return: The new head of reversed linked list.
     */
    ListNode * reverse(ListNode * head) {
        ListNode * pre = NULL;
        ListNode * cur = head;

        while(cur){
            ListNode * temp = cur->next;
            cur->next = pre;
            pre = cur;
            cur = temp;
        }

        return pre;
    }
};

對於single linked list,只有next,所以需要pre和cur等指針

2.2 reverse linked list in k-group (450)

  1. 考慮overview,先考慮整體的框架,再細化裏面的每一個細節。

  2. 本題的框架及dummy node的使用:

    A. 定義子函數 reversekNodes(cur,k):反轉當前節點開始的k個節點,返回下一個節點,但是存在一個問題:0->1->2->3->4->null。如果反轉1,2,3,除了1,2,3本身發生變化以外,0也會改變,所以如果按照上面的子函數,反轉爲1->2->3之後,直接開始反轉4開始的節點,沒有去改變0的next。故:該子函數需要修改,每次翻轉k個節點,其實有k+1個節點發生變化。

    B. 鏈表結構發生變化時,藉助dummy node。口->0->1->2->3->4->null。在0前面增加dummy node,不需要關注其value值,只要將其next賦值爲head。

  • dummy node什麼時候用? – 當鏈表結構發生變化時。
  • c++的dummy node如何使用不需要刪除? –
ListNode * dummy = new ListNode(0); //不使用這種方式,這樣需要手動釋放內存空間。
       ListNode dummy; dummy.next = p; //推薦!!!這樣就是一個結構體,代碼執行完自動結束

使用dummy node的題目:
http://www.lintcode.com/en/problem/partition-list/
http://www.lintcode.com/en/problem/merge-two-sorted-lists/
http://www.lintcode.com/en/problem/reverse-linked-list-ii/
http://www.lintcode.com/en/problem/swap-two-nodes-in-linked-list/
http://www.lintcode.com/en/problem/reorder-list/
http://www.lintcode.com/en/problem/rotate-list/

3)本題的解題思路
定義子函數 reversekNodes(pre,k) ,每k個進行一次翻轉。實現的功能如下:

// head -> n1 -> n2 ... nk -> nk+1
   // =>
   // head -> nk -> nk-1 .. n1 -> nk+1
   // return n1
class Solution {
public:
    /*
     * @param head: a ListNode
     * @param k: An integer
     * @return: a ListNode
     */
    ListNode * reverseKGroup(ListNode * head, int k) {
        // write your code here
        ListNode dummy;
        dummy.next = head;

        head = &dummy;
        while(head){
            head = reverseK(head,k);
            if(!head){
                break;
            }
        }

        return dummy.next;
    }

    ListNode * reverseK(ListNode * head, int k){
        ListNode* nkplus = head;
        for(int i=0;i<=k;++i){
            if(!nkplus){
                return NULL;
            }
            nkplus = nkplus->next;
        }

        ListNode * n1 = head->next; //!!!重要,一定記得存k個節點的第一個,因爲翻轉之後,需修改該節點的next,使其指向下一個n1
        ListNode * pre = NULL;
        ListNode * cur = head->next;
        while(cur!=nkplus){
            ListNode * temp = cur->next;
            cur->next = pre;
            pre = cur;
            cur = temp;
        }

        head->next = pre;
        n1->next = nkplus;

        return n1;
    }
};

3.Copy List with Random Pointer (105 in linkcode)

深度拷貝的題目

思路一:(使用了額外空間)

可以認爲每個節點多了一條邊,這樣相當於圖的深度拷貝。
1). find all nodes;
2). copy nodes. hashmap(oldnode->newnode)
3). copy edges (neighbors)

缺點:使用了hashmap的額外空間。(額外空間指的是除了輸入輸出空間外的空間)

思路二:(使用O(1)的空間)

巧妙借用next。

1->2->3->4->null
=>
1->1’->2->2’->3->3’->4->4’->null

得到原鏈表中的next: x.next.next

得到節點x對應的新鏈表中的節點: x.next 等價於 hash[x]

步驟:
1). 增加x’,copy next
2). copy random.
3). split

代碼: 勸分不勸和

/**
 * Definition for singly-linked list with a random pointer.
 * struct RandomListNode {
 *     int label;
 *     RandomListNode *next, *random;
 *     RandomListNode(int x) : label(x), next(NULL), random(NULL) {}
 * };
 */

class Solution {
public:
    /**
     * @param head: The head of linked list with a random pointer.
     * @return: A new head of a deep copy of the list.
     */
    RandomListNode *copyRandomList(RandomListNode *head) {
        // write your code here
        if(!head){
            return NULL;
        }

        copyNext(head);
        copyRandom(head);
        return splitList(head);
    }

    void copyNext(RandomListNode *head){
        while(head){
            RandomListNode * nodecopy = new RandomListNode(head->label);
//            nodecopy->random = head->random;
            nodecopy->next = head->next;
            head->next = nodecopy;
            head = head->next->next;
        }
    }

    void copyRandom(RandomListNode *head){
        while(head){
            if(head->random){
                head->next->random = head->random->next;
            }
            else{
                head->next->random = NULL;  //注意判斷head->random是否爲空
            }
            head = head->next->next;
        }
    }

    RandomListNode * splitList(RandomListNode *head){
        RandomListNode * newhead = head->next; //注意此處!!!
        while(head){
            RandomListNode * temp = head->next;
            head->next = temp->next;
            if(temp->next){
                temp->next = temp->next->next;
            }
            head = head->next;
        }

        return newhead;
    }
};

4.Linked List cycle (102 、103 in linkcode)

4.1 證明求解方法

1)**以下的證明是fast和slow同時從head開始。**其實就推幾個遞推公式就好。。首先看圖(圖引用自CC150):
在這裏插入圖片描述

從鏈表起始處到環入口長度爲:a,從環入口到Faster和Slower相遇點長度爲:x,整個環長爲:c。

明確了以上信息,就可以開始做運算了。。

假設從開始到相遇,Slower走過的路程長爲s,由於Faster的步速是Slower的2倍,那麼Faster在這段時間走的路程長爲2s。

而對於Faster來說,他走的路程還等於之前繞整個環跑的n圈的路程nc,加上最後這一次遇見Slower的路程s。

所以我們有:

               2s = nc + s 

對於Slower來說,他走的路程長度s還等於他從鏈表起始處到相遇點的距離,所以有:

                s = a + x 

通過以上兩個式子代入化簡有:

                a + x = nc

                a = nc - x

                a = (n-1)c + c-x

                a = kc + (c-x)

那麼可以看出,c-x,就是從相遇點繼續走回到環入口的距離。上面整個式子可以看出,如果此時有個pointer1從起始點出發並且同時還有個pointer2從相遇點出發繼續往前走(都只邁一步),那麼繞過k圈以後, pointer2會和pointer1在環入口相遇。這樣,換入口就找到了。
從鏈表起始處到環入口長度爲:a,從環入口到Faster和Slower相遇點長度爲:x,整個環長爲:c。

明確了以上信息,就可以開始做運算了。。

假設從開始到相遇,Slower走過的路程長爲s,由於Faster的步速是Slower的2倍,那麼Faster在這段時間走的路程長爲2s。

而對於Faster來說,他走的路程還等於之前繞整個環跑的n圈的路程nc,加上最後這一次遇見Slower的路程s。

所以我們有:

               2s = nc + s 

對於Slower來說,他走的路程長度s還等於他從鏈表起始處到相遇點的距離,所以有:

                s = a + x 

通過以上兩個式子代入化簡有:

                a + x = nc

                a = nc - x

                a = (n-1)c + c-x

                a = kc + (c-x)

那麼可以看出,c-x,就是從相遇點繼續走回到環入口的距離。上面整個式子可以看出,如果此時有個pointer1從起始點出發並且同時還有個pointer2從相遇點出發繼續往前走(都只邁一步),那麼繞過k圈以後, pointer2會和pointer1在環入口相遇。這樣,換入口就找到了。

2)如果slow從head開始,fast從head->next開始。此時,寫代碼時的循環條件比較簡單,但是上述的遞推公式有點點變化。判斷是否有環時還是判斷fast和slow能否相遇,但是判斷環的入口時,則需要修改一下遞推公式。

因爲開始的時候fast比slow多開始一步,相當於slow多走了一步,所以,最終的遞推公式:a = kc + (c-x-1). 所以最終判斷的是:slow != fast->next。即fast與slow相遇的地方拒環入口相差:a+1的距離。(slow從head開始)

3)幾種特殊情況,注意邊際條件

 1->null、    1->2->null、      1->指向自身,有環

4.2 判斷鏈表是否是循環鏈表 linked-list-cycle (102 in lintcode)

class Solution {
public:
    /*
     * @param head: The first node of linked list.
     * @return: True if it has a cycle, or false
     */
    bool hasCycle(ListNode * head) {
        // write your code here
        if(!head || !head->next){  //特殊情況的處理
            return false;
        }

        ListNode * slow = head;
        ListNode * fast = head->next; //slow和fast不要放在同一位置,否則永遠進不去循環
        while(slow != fast){
            if(!fast || !fast->next){
                return false;
            }
            slow = slow->next;
            fast = fast->next->next;
        }

        return true;
    }
};

4.3 返回循環鏈表的入口 linked-list-cycle-ii (103 in lintcode)

class Solution {
public:
/*
* @param head: The first node of linked list.
* @return: The node where the cycle begins. if there is no cycle, return null
*/
ListNode * detectCycle(ListNode * head) {
// write your code here
if(!head || !head->next){
return NULL;
}

    ListNode * slow = head;
    ListNode * fast = head->next;
    while(slow!=fast){
        if(!fast || !fast->next){
            return NULL;
        }
        slow = slow->next;
        fast = fast->next->next;
    }

    slow = head;
    while(slow != fast->next){ //注意此處的判斷
        slow = slow->next;
        fast = fast->next;
    }
    return slow; //不能返回fast
}

};

5.Sort List (98 in linkcode)(★★★★★)

5.1 時間複雜度:O(nlog(n))

空間複雜度:O(1) /no extra memory / constant space / O(1) memory complexity

5.2 排序算法 - nlog(n)

quick sort - 期望時間複雜度/平均時間複雜度 nlog(n) - 空間複雜度 O(1)

merge sort - 空間複雜度O(n) 需要有一個新的數組來謄抄

heap(priority queue) sort - 如果在數組上做,並且自己來實現heap的話,空間複雜度O(1) 。在鏈表上做不到,因爲不能進行下標訪問。

鏈表大多數情況下都不需要額外空間。鏈表上的quick sort和merge sort不需要額外空間。

5.3 quick sort可以實現:

https://www.geeksforgeeks.org/quicksort-on-singly-linked-list/

5.4 merge sort更簡單

step 1:findMiddle(快慢指針);//對數據流問題,不可重新掃,數據實時到來;

step 2:merge two list;

step 3:總框架 { 找中點 -> 左排序(遞歸) ->右排序(遞歸) -> 合併 }

5.5 代碼

!!!注:
findMiddle函數中,如果fast和slow都從head開始,則 while(fast->next && fast->next->next)
如果slow從head開始,fast從head的下一個開始,則 while(fast && fast->next)

舉例:1 -> -1 ->null。mid的位置必須在1,而不是-1,因爲right的排序是從mid->next開始的。

/**
 * Definition of ListNode
 * class ListNode {
 * public:
 *     int val;
 *     ListNode *next;
 *     ListNode(int val) {
 *         this->val = val;
 *         this->next = NULL;
 *     }
 * }
 */

class Solution {
public:
    /*
     * @param head: The head of linked list.
     * @return: You should return the head of the sorted linked list, using constant space complexity.
     */
    ListNode * sortList(ListNode * head) {
        // write your code here
        if(!head || !head->next){
            return head;
        }

        ListNode * mid = findMiddle(head);
        ListNode * right = sortList(mid->next);
        mid->next = NULL;
        ListNode * left = sortList(head);

        return merge(left,right);
    }

    ListNode * findMiddle(ListNode * head){
        ListNode * slow = head; //slow和fast可以都從head開始,也可以一前一後
        ListNode * fast = head;

        while(fast->next && fast->next->next){
            slow = slow->next;
            fast = fast->next->next;
        }
        return slow;
    }

    ListNode * merge(ListNode * head1, ListNode * head2){
        ListNode dummy(0);
        ListNode * tail = &dummy;

        while(head1 && head2){
            if(head1->val < head2->val){
                tail->next = head1;
                head1 = head1->next;
            }
            else{
                tail->next = head2;
                head2 = head2->next;
            }
            tail = tail->next;
        }

        if(head1){
            tail->next = head1;
        }
        if(head2){
            tail->next = head2;
        }
        return dummy.next;
    }
};

5.6 相關問題

http://www.lintcode.com/problem/convert-sorted-list-to-balanced-bst/
http://www.lintcode.com/problem/delete-node-in-the-middle-of-singly-linked-list/
http://www.lintcode.com/problem/convert-binary-search-tree-to-doubly-linked-list/

6. merge-two-sorted-lists (165 in linkcode)

6.1題目

http://www.lintcode.com/zh-cn/problem/merge-two-sorted-lists/
http://www.jiuzhang.com/solution/merge-two-sorted-lists/
將兩個排序鏈表合併爲一個新的排序鏈表
給出 1->3->8->11->15->null,2->null, 返回 1->2->3->8->11->15->null。

6.2 思路

利用了 sorted List中的merge sort

6.3 代碼

/**
 * Definition of ListNode
 * class ListNode {
 * public:
 *     int val;
 *     ListNode *next;
 *     ListNode(int val) {
 *         this->val = val;
 *         this->next = NULL;
 *     }
 * }
 */


class Solution {
public:
    /*
     * @param l1: ListNode l1 is the head of the linked list
     * @param l2: ListNode l2 is the head of the linked list
     * @return: ListNode head of linked list
     */
    ListNode * mergeTwoLists(ListNode * l1, ListNode * l2) {
        // write your code here
        ListNode dummy(0);
        ListNode * tail = &dummy;

        while(l1 && l2){
            if(l1->val < l2->val){
                tail->next = l1;
                l1 = l1->next;
            }
            else{
                tail->next = l2;
                l2 = l2->next;
            }
            tail = tail->next;
        }

        if(l1){
            tail->next = l1; 
        }
        if(l2){
            tail->next = l2;
        }

        return dummy.next;
    }
};

insert-into-a-cyclic-sorted-list (599 in linkcode)

7.1 題目

http://www.lintcode.com/zh-cn/problem/insert-into-a-cyclic-sorted-list/
http://www.jiuzhang.com/solution/insert-into-a-cyclic-sorted-list/

給一個來自已經排過序的循環鏈表的節點,寫一個函數來將一個值插入到循環鏈表中,並且保持還是有序循環鏈表。給出的節點可以是鏈表中的任意一個單節點。返回插入後的新鏈表。

給一個鏈表:3->5->1
插入值 4
返回 5->1->3->4

7.2 思路

1) 注意幾個邊界條件的判斷

2)如果某個節點是需要返回的,則直接用new;如果是類似dummy這樣的節點,則用ListNode dummy這樣的局部變量。

7.3 代碼

/**
 * Definition of ListNode
 * class ListNode {
 * public:
 *     int val;
 *     ListNode *next;
 *     ListNode(int val) {
 *         this->val = val;
 *         this->next = NULL;
 *     }
 * }
 */


class Solution {
public:
    /*
     * @param node: a list node in the list
     * @param x: An integer
     * @return: the inserted new list node
     */
    ListNode * insert(ListNode * node, int x) {
        if(!node){
            node = new ListNode(x);
            node->next = node;
            return node;
        }

        ListNode * cur = node;
        ListNode * pre = NULL;
        do{
            pre = cur;
            cur = cur->next;
            if(x <= cur->val && x>=pre->val){
                break;
            }
            if((pre->val > cur->val) && (x > pre->val || x < cur->val)){
                break;
            }
        }while(cur != node);

        ListNode * newNode = new ListNode(x);
        newNode->next = cur;
        pre->next = newNode;
        return newNode;
    }
};
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章