劍指Offer——鏈表

鏈表

鏈表是一種動態的數據結構,當需要插入一個節點的時候,我們只需要爲新創建的節點分配內存空間,將當前節點的next指向新創建的節點,並沒有閒置的內存空間。

例題解答

本文中所有的鏈表的定義如下:

struct ListNode{
    int val;
    ListNode *next;
    ListNode(int v):val(v),next(nullptr){}
};
從尾到頭打印鏈表

題目:輸入一個鏈表的頭節點,從尾到頭反過來打印每個節點的值

  • 首先,逆序很容易想到棧這個數據結構,在遍歷鏈表的時候,把每個節點壓入棧中,打印時從棧中彈出來,就能逆着打印
  • 其次,可以將值保存在vector中,調用reverse()函數即可逆序打印結果

代碼如下

void printReverseList(ListNode *root){
    ListNode *p = root;
    stack<ListNode*>nodes;
    while(p != nullptr){
        nodes.push(p);
        p = p ->next;
    }
    while(!nodes.empty()){
        ListNode *t = nodes.top();
        printf("%d\t",t->val);
        nodes.pop();
    }
}
刪除鏈表中的節點

在O(1)的時間內刪除鏈表的某個節點
給定單向鏈表的頭指針和一個節點的指針,定義一個函數在O(1)的時間內刪除該節點

  • 通常,刪除鏈表中的節點需要獲取該節點的前一個節點,然而獲取這個前一個節點需要遍歷整個鏈表,時間爲O(n)
  • 此方法中可以,將下一個節點複製到該節點中,再刪除下一個節點,即可完成刪除操作。
    這裏寫圖片描述
    代碼如下
void deleteNode(ListNode **root,ListNode *del_node){
//刪除的節點不是尾節點
    if(del_node->next != nullptr){
        ListNode *p_next = del_node->next;
        del_node->val = p_next->val;
        del_node->next = p_next->next;
        delete p_next;
    }
    //鏈表中只含有唯一一個節點
    else if (*root == del_node){
        delete del_node;
    }
    //刪除鏈表中的尾節點
    else{
        ListNode *p = *root;
        while(p->next != del_node){
            p = p->next;
        }
        p->next = nullptr;
        delete del_node;
    }
}
鏈表中倒數第K個節點

輸入一個鏈表,輸出該鏈表中倒數第K個節點。

  • 同樣,常規的方法首先遍歷鏈表,獲取鏈表中節點的個數,計算下一次遍歷時需要計算的次數。時間複雜度爲O(n)
  • O(1)的做法爲設置2個指針,第一個指針向前走k-1步後,第二個指針不動。接着讓第一個指針指向末尾時,第二個指針便指向倒數第k個節點

代碼如下:

int findKNode(ListNode *root,int k) {
    ListNode *p_node = root;
    ListNode *p_sec = root;
    for(int i = 0;i < k-1;i++){
        p_node = p_node->next;
    }
    while(p_node->next != nullptr) {
        p_node = p_node->next;
        p_sec = p_sec->next;
    }
    return p_sec->val;
}
鏈表中環的入口節點

題目:如果一個鏈表中包含環,如何找出環的入口節點?

  1. 首先,要判斷鏈表中是否有環,可以設置快、慢兩個指針,如果他們相遇了則說明鏈表中存在環
  2. 要找到入口節點可以新建一個指針,同時慢指針繼續以一次一步的速度繼續遍歷,相遇節點即爲環的入口節點,證明如下:

這裏寫圖片描述

假設快慢節點相遇在c點,慢節點在這個過程中移動的距離爲s,鏈表長度爲L,環的長度爲r,則快節點移動距離爲a+n*r,而快節點的步長爲慢節點的2倍:
a+n*r = 2s —> a+x = s
a+x = n*r —> a+x = (n-1)*r + r —>a+x = (n-1)*r + (L-a)
a = (n-1)*r + (L-a-x)
因此節點從h->d與節點從c->d會在d點相遇
因此環形鏈表可以分解成三個問題
1. 給定一個鏈表,判斷鏈表中是否有環
2. 計算環的大小
3. 尋找環的入口

判斷鏈表中是否有環

要點:設置快慢兩個指針,慢指針一次走一步,快指針一次走兩步,當他們相遇時,則說明鏈表中有環
代碼如下;

 bool hasCycle(ListNode *head) {
    ListNode *slow,*fast;
    if(head == nullptr)
        return false;
    slow = head->next;
    if(slow == nullptr)
        return false;
    fast = slow->next;
    if(fast == nullptr)
        return false;
    while(slow != nullptr && fast != nullptr) {
        if(fast == slow)
            return true;
        slow = slow ->next;
        fast = fast->next;
        if(fast != nullptr)
            fast = fast->next;
        else
            return false;
    }
    return false;
}
計算環的大小

在相遇的節點後,慢節點遍歷一圈後回到當前節點,判斷節點相同即可

尋找環的入口節點

需要第一個問題中的相遇的節點作爲參數,代碼如下

ListNode *detectCycle(ListNode *head) {
    ListNode *circle_head = hasCycle(head);
    if(circle_head == nullptr)
        return nullptr;
    ListNode *p = head;
    while(p != circle_head) {
        p = p->next;
        circle_head = circle_head->next;
    }
    return p;
}
反轉鏈表

題目:定義一個函數,輸入一個鏈表的頭節點,反轉該鏈表並輸入反轉後的鏈表的頭結點

爲了防止鏈表的斷裂,需要使用三個指針,表示當前節點、前一個節點、下一個節點。而反轉之後的鏈表的頭節點是反轉之前next爲null的節點。
代碼如下:

ListNode* reverseList(ListNode *root) {
    ListNode *p_reverse_head = nullptr;
    ListNode *p_cur = root;
    ListNode *p_pre = nullptr;
    while(p_cur != nullptr) {
        ListNode *p_next = p_cur->next;
        if(p_next == nullptr)
            p_reverse_head = p_cur;
        p_cur -> next = p_pre;
        p_pre = p_cur;
        p_cur = p_next;
    }
    return p_reverse_head;
}
(LeetCode21) 合併兩個有序鏈表

將兩個有序鏈表合併爲一個新的有序鏈表並返回。新鏈表是通過拼接給定的兩個鏈表的所有節點組成的。
輸入:1->2->4, 1->3->4
輸出:1->1->2->3->4->4

  • 使用遍歷的辦法,找出2個鏈表中值較小的那個節點鏈接到新的表頭中
  • 使用遞歸的辦法

代碼如下:

ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
    if(l1 == nullptr) 
        return l2;
    else if(l2 == nullptr)
        return l1;
    ListNode *mergeHead = nullptr;
    if(l1->val > l2->val) {
        mergeHead = l2;
        mergeHead->next = mergeTwoLists(l1,l2->next);
    }else {
        mergeHead = l1;
        mergeHead->next = mergeTwoLists(l1->next,l2);
    }
    return mergeHead;  
}
(LeetCode23)合併K個有序鏈表

合併 k 個排序鏈表,返回合併後的排序鏈表

  • 可以每次兩兩排序,將排序後的鏈表重新加入到lists中去,並在lists中刪除以及排序過的鏈表
    代碼如下:
ListNode* mergeKLists(vector<ListNode*>& lists) {
    if(lists.empty())
        return nullptr;
    while(lists.size() > 1) {
        lists.push_back(mergeTwoLists(lists[0],lists[1]));
        lists.erase(lists.begin());
        lists.erase(lists.begin());
    }
    return lists.front();

}
複雜鏈表的複製
  • 爲了實現O(1)的複雜度,先將鏈表在原鏈表中複製一遍
  • 在新的節點上鍊接siblingNodes
  • 斷開鏈接,選擇偶數位置的節點

代碼如下:

struct ListNode{
    int val;
    ListNode *next;
    ListNode *sibling;
    ListNode() = default;
    ListNode(int v):val(v),next(nullptr),sibling(nullptr){}
};

void copyNodes(ListNode *root) {
    ListNode *head = root;
    while(head != nullptr) {
        ListNode *temp = new ListNode();
        temp->val = head->val;
        temp->next = head->next;
        head->next = temp;
        head = temp->next;
    }
}

void connectSiblingNodes(ListNode *root) {
    ListNode *head = root;
    while(head != nullptr) {
        ListNode *cloned = head->next;
        if(head->sibling != nullptr){
            cloned->sibling = head->sibling->next;
        }
        head = cloned->next;
    }
}

ListNode* reconnectNodes(ListNode *root){
    ListNode *node = root;
    ListNode *cloned_head = nullptr,*cloned_node = nullptr;
    if(node != nullptr) {
        cloned_head = cloned_node = node->next;
        node ->next = cloned_node ->next;
        node = node->next;
    }
    while(node != nullptr) {
        cloned_node->next = node ->next;
        cloned_node = cloned_node ->next;
        node->next = cloned_node ->next;
        node = node->next;

    }
    return cloned_head;
}

ListNode* clone(ListNode *head){
    copyNodes(head);
    connectSiblingNodes(head);
    ListNode *root = reconnectNodes(head);
    return root;
}

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