zoukankan      html  css  js  c++  java
  • 链表常见问题

    链表的存储方式使得它可以高效的在指定位置插入与删除,时间复杂度均为 O(1)。

    在结点 p 之后增加一个结点 q 总共分三步:

    申请一段内存用以存储 q (可以使用内存池避免频繁申请和销毁内存)。
    将 p 的指针域数据复制到 q 的指针域。
    更新 p 的指针域为 q 的地址。

    删除结点 p 之后的结点 q 总共分两步:

    将 q 的指针域复制到 p 的指针域。
    释放 q 结点的内存。

    链表的主要代码

    #include <bits/stdc++.h>
    
    using namespace std;
    
    //定义一个结点模板
    template<typename T>
    struct Node {
    	T data;
    	Node *next;
    	Node() : next(nullptr) {}
    	Node(const T &d) : data(d), next(nullptr) {}
    };
    
    //删除 p 结点后面的元素
    template<typename T>
    void Remove(Node<T> *p) {
    	if (p == nullptr || p->next == nullptr) {
    		return;
    	}
    	auto tmp = p->next->next;
    	delete p->next;
    	p->next = tmp;
    }
    
    //在 p 结点后面插入元素
    template<typename T>
    void Insert(Node<T> *p, const T &data) {
    	auto tmp = new Node<T>(data);
    	tmp->next = p->next;
    	p->next = tmp;
    }
    
    //遍历链表
    template<typename T, typename V>
    void Walk(Node<T> *p, const V &vistor) {
    	while(p != nullptr) {
    		vistor(p);
    		p = p->next;
    	}
    }
    
    int main() {
    	auto p = new Node<int>(1);
    	Insert(p, 2);
    	int sum = 0;
    	Walk(p, [&sum](const Node<int> *p) -> void { sum += p->data; });
    	cout << sum << endl;
    	Remove(p);
    	sum = 0;
    	Walk(p, [&sum](const Node<int> *p) -> void { sum += p->data; });
    	cout << sum << endl;
    	return 0;
    }
    

      

    面试问题总结
    无法高效获取长度,无法根据偏移快速访问元素,是链表的两个劣势。然而面试的时候经常碰见诸如获取倒数第k个元素,获取中间位置的元素,判断链表是否存在环,判断环的长度等和长度与位置有关的问题。这些问题都可以通过灵活运用双指针来解决。

    Tips:双指针并不是固定的公式,而是一种思维方式~

    先来看"倒数第k个元素的问题"。设有两个指针 p 和 q,初始时均指向头结点。首先,先让 p 沿着 next 移动 k 次。此时,p 指向第 k+1个结点,q 指向头节点,两个指针的距离为 k 。然后,同时移动 p 和 q,直到 p 指向空,此时 q 即指向倒数第 k 个结点。可以参考下图来理解:

     

    class Solution {
    public:
        ListNode* getKthFromEnd(ListNode* head, int k) {
            ListNode *p = head, *q = head; //初始化
            while(k--) {   //将 p指针移动 k 次
                p = p->next;
            }
            while(p != nullptr) {//同时移动,直到 p == nullptr
                p = p->next;
                q = q->next;
            }
            return q;
        }
    };


    获取中间元素的问题。设有两个指针 fast 和 slow,初始时指向头节点。每次移动时,fast向后走两次,slow向后走一次,直到 fast 无法向后走两次。这使得在每轮移动之后。fast 和 slow 的距离就会增加一。设链表有 n 个元素,那么最多移动 n/2 轮。当 n 为奇数时,slow 恰好指向中间结点,当 n 为 偶数时,slow 恰好指向中间两个结点的靠后一个(可以考虑下如何使其指向前一个结点呢?)。

    下述代码实现了 n 为偶数时慢指针指向靠后结点。

    class Solution {
    public:
        ListNode* middleNode(ListNode* head) {
            ListNode *p = head, *q = head;
            while(q != nullptr && q->next != nullptr) {
                p = p->next;
                q = q->next->next;
            }
            return p;
        } 
    };


    是否存在环的问题。如果将尾结点的 next 指针指向其他任意一个结点,那么链表就存在了一个环。

    上一部分中,总结快慢指针的特性 —— 每轮移动之后两者的距离会加一。下面会继续用该特性解决环的问题。
    当一个链表有环时,快慢指针都会陷入环中进行无限次移动,然后变成了追及问题。想象一下在操场跑步的场景,只要一直跑下去,快的总会追上慢的。当两个指针都进入环后,每轮移动使得慢指针到快指针的距离增加一,同时快指针到慢指针的距离也减少一,只要一直移动下去,快指针总会追上慢指针。

    根据上述表述得出,如果一个链表存在环,那么快慢指针必然会相遇。实现代码如下:

    class Solution {
    public:
        bool hasCycle(ListNode *head) {
            ListNode *slow = head;
            ListNode *fast = head;
            while(fast != nullptr) {
                fast = fast->next;
                if(fast != nullptr) {
                    fast = fast->next;
                }
                if(fast == slow) {
                    return true;
                }
                slow = slow->next;
            }
            return false;
        }
    };


    最后一个问题,如果存在环,如何判断环的长度呢?方法是,快慢指针相遇后继续移动,直到第二次相遇。两次相遇间的移动次数即为环的长度。

  • 相关阅读:
    ! JOISC2020DAY2变色龙之恋
    ! JOISC2020DAY1扫除
    JOISC2020DAY1汉堡肉
    JOISC2020DAY1建筑装饰4
    ! JLOI/SHOI2016随机序列
    JLOI/SHOI2016黑暗前的幻想乡
    ! JLOI/SHOI2016成绩比较
    JLOI/SHOI2016方
    JLOI/SHOI2016侦查守卫
    ! AHOI/HNOI2017抛硬币
  • 原文地址:https://www.cnblogs.com/wsw-seu/p/13357853.html
Copyright © 2011-2022 走看看