【LeetCode Cookbook(C++描述)】一刷链表(下)

本系列文章仅是 GitHub 大神 @halfrost 的刷题笔记 《LeetCode Cookbook》的提纲以及示例、题集的 C++转化。原书请自行下载学习。
本篇文章涉及新手应该优先刷的几道经典链表算法题,以后会更新“二刷”“三刷”等等,上一篇文章《一刷链表(上)》在这里

LeetCode #876:Middle of the Linked List 链表的中间节点

#876
给你单链表的头节点 head ,请你找出并返回链表的中间节点。

如果有两个中间节点,则返回第二个中间节点

问题的关键在于我们无法得到链表的长度 n ,常规的解法是两次遍历。正如我们在上一篇文章中的寻找倒数第 k 个节点的解决思路,我们让两个指针 fastslow 分别指向链表头节点 head每当慢指针 slow 前进一步,快指针 fast 就前进两步,那么当 fast 走向链表末尾的同时,slow 恰好指向链表的中点。

class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        ListNode* fast = head, *slow = head;

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

利用这一思路,我们可以解决链表是否成环的问题。

LeetCode #141:Linked List Cycle 环形链表

#141
给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false

同样地,每当慢指针 slow 前进一步,快指针 fast 就前进两步,如果过程中 fastslow 出现追及(相遇),则证明该链表包含环。

class Solution {
public:
    bool hasCycle(ListNode *head) {
        ListNode* fast = head, *slow = head;

        while (fast != nullptr && fast->next != nullptr) {
            slow = slow->next;
            fast = fast->next->next;

            if (slow == fast) return true;
        }

        return false;
    }
};

更多例子:找出环的起点

#142
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改链表。

假设快慢指针相遇时,slow 走了 k 步,那么 fast 一定走了 2k 步;fast 相对于 slow 多走的 k 实际上是环长度的整数倍

因此,假设相遇点距环起点的距离为 m ,从相遇点继续前进 k - m 步恰好到达环的起点,而环的起点距头节点 head 的距离也为 k - m 。此时我们只要将两个指针中的任意一个指针重新指向 head ,两个指针则同速前进,k - m 步后一定会相遇,相遇之处便是环的起点

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode* fast = head, *slow = head;

        while (fast != nullptr && fast->next != nullptr) {
            fast = fast->next->next;
            slow = slow->next;
            
            if (fast == slow) break;
        }
        //没有环
        if (fast == nullptr || fast->next == nullptr) return nullptr;

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

LeetCode #160:Intersection of Two Linked Lists 相交链表

#160
给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null

图示两个链表在节点 c1 开始相交:
PIC#160
题目数据保证整个链式结构中不存在环。

注意,函数返回结果后,链表必须保持其原始结构

自定义评测:
评测系统的输入如下(你设计的程序不适用此输入):

  • intersectVal - 相交的起始节点的值。如果不存在相交节点,这一值为 0
  • listA - 第一个链表
  • listB - 第二个链表
  • skipA - 在 listA 中(从头节点开始)跳到交叉节点的节点数
  • skipB - 在 listB 中(从头节点开始)跳到交叉节点的节点数

评测系统将根据这些输入创建链式数据结构,并将两个头节点 headAheadB 传递给你的程序。如果程序能够正确返回相交节点,那么你的解决方案将被视作正确答案

哈希集合解法

遍历链表 A,使用哈希集合存储链表 A 中的每个节点,随后和另一条链表对比,判断是否有节点在哈希集合中,第一个在哈希集合的节点就是所谓的相交节点 c1

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        unordered_set<ListNode *> visited;
        ListNode *temp = headA;
        while (temp != nullptr) {
            visited.emplace(temp);  //插入链表 A 的元素,也可以用 insert() 方法
            temp = temp->next;
        }
        temp = headB;
        while (temp != nullptr) {
            if (visited.count(temp)) {  //判断链表 B 中的元素是否存在于哈希集合中
                return temp;
            }
            temp = temp->next;
        }
        return nullptr;
    }
};

该算法的时间复杂度为   O ( n + m ) \ O(n+m)  O(n+m) ,空间复杂度为   O ( n ) \ O(n)  O(n)

双指针解法

双指针的算法可以将空间复杂度降至   O ( 1 ) \ O(1)  O(1)

由于两条链表长度可能不一致,两条链表之间的节点无法对应,如果只是让两个指针 p1p2 分别在两条链表上前进,并不能同时走到相交节点。因此,我们可以p1 遍历完 listA 后开始遍历 listB ,相应地,p2 遍历完 listB 后也开始遍历 listA ,这样两条链表在逻辑上接在了一起,消除了两者间的长度差,p1p2 就可以同时进入公共部分,即相交节点 c1

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode* p1 = headA, *p2 = headB;

        while (p1 != p2) {
            // p1 走到链表 A 末尾,转到链表 B
            if (p1 == nullptr) p1 = headB;
            else p1 = p1->next;
            // p2 走到链表 B 末尾,转到链表 A
            if (p2 == nullptr) p2 = headA;
            else p2 = p2->next;
        }
        return p1;
    }
};

另一种双指针解法

若两条链表相交,则必然存在一段公共段,也就意味着,走到公共段之前的对比没有必要。两条链表最终相交,显然末端对齐的两个链表中,两条链表可能会因为长度差而无法完全按照索引一一对应。那么,我们可以让长链表的指针 p1 先走两个链表长度的差值,接着短链表的指针 p2 定位到与长链表长度相同的位置,二者开始同步向后遍历,比较链表节点是否一致。

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode* p1 = headA, *p2 = headB;
        int lenA = 0, lenB = 0;

        while (p1 != nullptr) {  //求链表 A 的长度
            lenA++;
            p1 = p1->next;
        }
        while (p2 != nullptr) {  //求链表 B 的长度
            lenB++;
            p2 = p2->next;
        }
        //重置两个指针
        p1 = headA, p2 = headB;
        //确保 p1 为长链表的指针
        if (lenB > lenA) {
            int tempLen = lenA;
            lenA = lenB, lenB = tempLen;

            ListNode* tempNode = p1;
            p1 = p2, p2 = tempNode;
        }
        //求长度差
        int gap = lenA - lenB;
        //让 p1 和 p2 在同一起点上(末端对齐)
        while (gap-- > 0) {
            p1 = p1->next;
        }
        //遍历 p1 和 p2,遇到相同则直接返回
        while ( p1 != nullptr) {
            if (p1 == p2) {
                return p1;
            }
            p1 = p1->next;
            p2 = p2->next;
        }
        return nullptr;
    }
};

受到环链表启发的解法

如果把两条链表首尾相连,那么「寻找两条链表的交点」的问题转换成了前面提到的「寻找环起点」的问题:
在这里插入图片描述
需要注意,找到相交节点后应当将环断开,以避免返回错误结果。

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode* p = headB;
        //找到链表 B 的最后一个节点,即两链表公共部分的最后一个节点
        while (p->next != nullptr) p = p->next;
        //将公共链条的最后一个节点与 headB 头节点相连构成一个环
        p->next = headB;
        //找出环的起点,即链表 A 和 B 的相交节点
        ListNode* res = detectCycle(headA);
        //断开环
        p->next = nullptr;

        return res;
    }

private:
    ListNode *detectCycle(ListNode *head) {
        ListNode* fast = head, *slow = head;

        while (fast != nullptr && fast->next != nullptr) {
            fast = fast->next->next;
            slow = slow->next;
            
            if (fast == slow) break;
        }
        //没有环
        if (fast == nullptr || fast->next == nullptr) return nullptr;

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

LeetCode #707:Design Linked List 设计链表

#707
实现链表的查找、头插法、尾插法、通用插入、删除操作

  • get(index) :获取链表中第 index 个节点的值。如果索引无效,则返回 -1。
  • addAtHead(val) :在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
  • addAtTail(val) :将值为 val 的节点追加到链表的最后一个元素。
  • addAtIndex(index,val) :在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点;如果 index 小于 0,则在头部插入节点。
  • deleteAtIndex(index) :如果索引 index 有效,则删除链表中的第 index 个节点。

单向链表的实现

实现单向链表,即每个节点仅存储本身的值和后继节点。除此之外,我们还需要一个哨兵(   s e n t i n e l \ sentinel  sentinel)节点作为头节点,和一个 length 参数保存有效节点数:
在这里插入图片描述
先定义一个简单的节点类 Node

struct Node {
    int val;
    Node* next;
    Node(int x) : val(x), next(nullptr) {}  //构造函数
};

链表类里初始化头节点和链表长度:

class MyLinkedList {
private:
    Node* head; //头节点,不存储有效数据,仅作为哨兵节点
    int length; //链表长度

public:
    MyLinkedList() {
        head = new Node(0); //初始化头节点为哨兵节点
        length = 0;
    }
};

实现 get(index) 时,先判断有效性,再通过循环来找到对应的节点的值:

int get(int index) {
        if (index < 0 || index >= length) {
            return -1;
        }
        Node* curr = head->next; //从第一个有效节点开始遍历
        for (int i = 0; i < index; ++i) {
            curr = curr->next;
        }
        return curr->val;
    }

实现 addAtIndex(index, val) 时,如果 index 是有效值,则需要找到原来下标为 index 的节点的前驱节点 curr ,并创建新节点 newNode ,将 newNode 的后继节点设为 curr 的后继节点,将 curr 的后继节点更新为 newNode ,这样就将 newNode 插入到了链表中。最后需要更新 length
在这里插入图片描述

void addAtIndex(int index, int val) {
    if (index > length) return;
    if (index < 0) index = 0;

    Node* newNode = new Node(val);
    Node* curr = head;

    //找到第index个节点的前驱节点
    for (int i = 0; i < index; ++i) {
        curr = curr->next;
    }

    newNode->next = curr->next;
    curr->next = newNode;

    //插入节点后,链表长度 + 1
    length++;
}

addAtHead(val) 相当于 addAtIndex(0, val)addAtTail(val) 相当于 addAtIndex(length, val)

void addAtHead(int val) {
     addAtIndex(0, val);
}
    
void addAtTail(int val) {
     addAtIndex(length, val);
}

实现 deleteAtIndex(index) ,先判断参数有效性,然后找到下标为 index 的节点的前驱节点 curr ,通过将 curr 的后继节点更新为 curr 的后继节点的后继节点,来达到删除节点的效果。同时也要更新 length

void deleteAtIndex(int index) {
    if (index < 0 || index >= length) return;

    Node* curr = head;
    //找到第 index 个节点的前驱节点
    for (int i = 0; i < index; ++i) {
        curr = curr->next;
    }

    Node* toDelete = curr->next;
    curr->next = toDelete->next;
    //释放被删除节点的内存
    delete toDelete;
    //删除节点后,链表长度 - 1
    length--;
}

最终代码实现如下:

//定义单节点结构体
struct Node {
    int val;
    Node* next;
    Node(int _val) : val(_val), next(nullptr) {}
};

class MyLinkedList {
private:
    Node* head; //头节点,不存储有效数据,仅作为哨兵节点
    int length; //链表长度

public:
    MyLinkedList() {
        head = new Node(0); //初始化头节点为哨兵节点
        length = 0;
    }
    
    int get(int index) {
        if (index < 0 || index >= length) {
            return -1;
        }
        Node* curr = head->next; //从第一个有效节点开始遍历
        for (int i = 0; i < index; ++i) {
            curr = curr->next;
        }
        return curr->val;
    }
    
    void addAtHead(int val) {
        addAtIndex(0, val);
    }
    
    void addAtTail(int val) {
        addAtIndex(length, val);
    }
    
    void addAtIndex(int index, int val) {
        if (index > length) return;

        if (index < 0) index = 0;

        Node* newNode = new Node(val);
        Node* curr = head;

        //找到第index个节点的前驱节点
        for (int i = 0; i < index; ++i) {
            curr = curr->next;
        }

        newNode->next = curr->next;
        curr->next = newNode;

        //插入节点后,链表长度 + 1
        length++;
    }
    
    void deleteAtIndex(int index) {
        if (index < 0 || index >= length) return;

        Node* curr = head;
        //找到第 index 个节点的前驱节点
        for (int i = 0; i < index; ++i) {
            curr = curr->next;
        }

        Node* toDelete = curr->next;
        curr->next = toDelete->next;
        //释放被删除节点的内存
        delete toDelete;
        //删除节点后,链表长度 - 1
        length--;
    }
};

双向链表的实现

实现双向链表,即每个节点要存储本身的值、后继节点和前驱节点。除此之外,需要一个哨兵节点作为头节点 head 和一个哨兵节点作为尾节点 tail ,同时仍需要一个 size 参数保存有效节点数:
在这里插入图片描述
先定义节点类 DListNode :

struct DListNode {
    int val;
    DLinkListNode *prev, *next;
    DLinkListNode(int _val) : val(_val), prev(nullptr), next(nullptr) {}
};

初始化头节点 headsize

class MyLinkedList {
private:
    int size;
    DLinkListNode *head;
    DLinkListNode *tail;
    
public:
    MyLinkedList() {
        this->size = 0;
        this->head = new DLinkListNode(0);
        this->tail = new DLinkListNode(0);
        head->next = tail;
        tail->prev = head;
    }
};

实现 get(index) 时,先判断有效性,再通过循环来找到对应的节点的值:

int get(int index) {
    if (index < 0 || index >= size) return -1;
        
    DLinkListNode *curr;
    if (index + 1 < size - index) {
        curr = head;
        for (int i = 0; i <= index; i++) {
            curr = curr->next;
        }
    } else {
        curr = tail;
        for (int i = 0; i < size - index; i++) {
            curr = curr->prev;
        }
    }
    return curr->val;
}

同理实现增添删改的功能:

void addAtHead(int val) {
    addAtIndex(0, val);
}

void addAtTail(int val) {
    addAtIndex(size, val);
}

void addAtIndex(int index, int val) {
    if (index > size) return;
        
    index = max(0, index);
    DLinkListNode *pred, *succ;
    if (index < size - index) {
        pred = head;
        for (int i = 0; i < index; i++) pred = pred->next;
        succ = pred->next;
    } else {
        succ = tail;
        for (int i = 0; i < size - index; i++) succ = succ->prev;
        pred = succ->prev;
    }
    size++;
    DLinkListNode *newNode = new DLinkListNode(val);
    newNode->prev = pred;
    newNode->next = succ;
    pred->next = newNode;
    succ->prev = newNode;
}

void deleteAtIndex(int index) {
    if (index < 0 || index >= size) return;

    DLinkListNode *pred, *succ;
    if (index < size - index) {
        pred = head;
        for (int i = 0; i < index; i++) pred = pred->next;
        succ = pred->next->next;
    } else {
        succ = tail;
        for (int i = 0; i < size - index - 1; i++) succ = succ->prev;
        pred = succ->prev->prev;
    }
    size--;
    DLinkListNode *p = pred->next;
    pred->next = succ;
    succ->prev = pred;
    delete p;
}

最终代码实现如下:

struct DListNode {
    int val;
    DLinkListNode *prev, *next;
    DLinkListNode(int _val) : val(_val), prev(nullptr), next(nullptr) {}
};

class MyLinkedList {
private:
    int size;
    DLinkListNode *head;
    DLinkListNode *tail;
    
public:
    MyLinkedList() {
        this->size = 0;
        this->head = new DLinkListNode(0);
        this->tail = new DLinkListNode(0);
        head->next = tail;
        tail->prev = head;
    }
    
    int get(int index) {
        if (index < 0 || index >= size) {
            return -1;
        }
        DLinkListNode *curr;
        if (index + 1 < size - index) {
            curr = head;
            for (int i = 0; i <= index; i++) {
                curr = curr->next;
            }
        } else {
            curr = tail;
            for (int i = 0; i < size - index; i++) {
                curr = curr->prev;
            }
        }
        return curr->val;
    }
    
	void addAtHead(int val) {
	    addAtIndex(0, val);
	}
	
	void addAtTail(int val) {
	    addAtIndex(size, val);
	}
	
	void addAtIndex(int index, int val) {
	    if (index > size) return;
	        
	    index = max(0, index);
	    DLinkListNode *pred, *succ;
	    if (index < size - index) {
	        pred = head;
	        for (int i = 0; i < index; i++) pred = pred->next;
	        succ = pred->next;
	    } else {
	        succ = tail;
	        for (int i = 0; i < size - index; i++) succ = succ->prev;
	        pred = succ->prev;
	    }
	    size++;
	    DLinkListNode *newNode = new DLinkListNode(val);
	    newNode->prev = pred;
	    newNode->next = succ;
	    pred->next = newNode;
	    succ->prev = newNode;
	}
};

呜啊?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值