链表问题总结

链表问题是c/c++技术岗面试过程中的重点问题,通过对各种链表操作的考察,能够反映出面试者对编程语言掌握的熟练程度,对链表数据结构和指针、地址等概念的理解。

===

Index

链表定义

采用leetcode中的链表通用定义。

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

反转

单链表反转

题目描述: 给定一个单向链表的头节点,将链表反转,并返回头节点。

分析: 链表反转类问题一般需要用到三个临时指针,pre,cur,next,使用三个指针对链表进行一次循环即可反转,同时为了便于操作,一般会设一个dummyHead节点,其next指针指向head节点。
实现代码:

ListNode* reverseList(ListNode* head){
    if(head == nullptr || head->next == nullptr) return head;

    ListNode* pre = nullptr, *p = head, *next = p->next;
    while(p){
        next = p->next;
        p->next = pre;
        pre = p;
        p = next;
    }
    return pre;
}

递归写法:

ListNode* reverseList(ListNode* head) {
    if (!head || !head->next) return head;
    
    ListNode* res = reverseList(head->next);
    head->next->next = head;
    head->next = NULL;
    return res;
}

反转从位置m到n的链表

题目描述: 反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。1 ≤ m ≤ n ≤ 链表长度。 示例:输入:1->2->3->4->5->NULL, m = 2, n = 4 输出: 1->4->3->2->5->NULL

    ListNode* reverseBetween(ListNode* head, int m, int n) {
        auto dummy = ListNode(-1);
        dummy.next = head;
        auto p = &dummy;
        for (int i = 0; i < m - 1; ++i) p = p->next;

        auto cur = p->next, next = cur->next;
        for (int i = 0; i < n - m; ++i) {
            auto d = next->next;
            next->next = cur;
            cur = next, next = d;
        }
        p->next->next = next;
        p->next = cur;
        return dummy.next;
    }

k个一组翻转链表

题目描述: 给你一个链表,每k个节点一组进行翻转,k是不大于链表长度的正整数,如果节点总数不是k的整数倍,那么请将剩余的节点保持原有顺序。

1. 递归法

ListNode* reverseKGroup(ListNode* head, int k) {
    auto p  = head;
    for (int i = 0; i < k; ++i) {
        if (!p) return head; //不够k个,不反转
        p = p->next;
    }
    ListNode *cur = head, *pre = nullptr, *nxt;
    for (int i = 0; i < k; ++i) { // 反转k个
        nxt = cur->next;
        cur->next =  pre;
        pre = cur;
        cur = nxt;
    }
    head->next = reverseKGroup(cur, k); //递归处理后面部分
    return pre;
}

2. 迭代方法

ListNode* reverseKGroup(ListNode* head, int k) {
    if (!head || !head->next || k < 2) return head;

    ListNode preHead(-1);
    preHead.next = head;
    ListNode *cur = &preHead, *pre = &preHead, *next;
    int cnt = 0;
    while ((cur = cur->next)) cnt++;  //链表中节点个数
    while (cnt >= k) {
        cur = pre->next;
        next = cur->next;
        for (int i = 0; i < k - 1; ++i) {
            cur->next = next->next;
            next->next = pre->next;
            pre->next = next;
            next = cur->next;
        }
        pre = cur;  cnt -= k;
    }
    return preHead.next;
}

两两交换链表中的节点

leetcode 24

给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。

你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

示例

  • 输入 : 1 2 3 4
  • 输出 : 2 1 4 3

1.递归法

    ListNode* swapPairs(ListNode* head) {
        if (!head || !head->next) return head;
       
        ListNode *pre = head, *cur = head->next, *next = cur->next;
        pre->next = swapPairs(next);
        cur->next = pre;
        return cur;
    }

2.迭代法

    ListNode* swapPairs(ListNode* head) {
        if(!head || !head->next) return head;

        ListNode * dummyhead = new ListNode(-1);
        dummyhead->next = head;
        ListNode * prev = dummyhead, *p = head, * next_p = head->next;

        while(next_p){
            prev->next = next_p;
            p->next = next_p->next;
            next_p->next = p;
            prev = p;
            p = p->next;
            if(p)
                next_p = p->next;
            else
                next_p = nullptr;
        }
        return dummyhead->next;
    }

旋转链表

leetcode 61

给定一个链表,旋转链表,将链表每个节点向右移动 k 个位置,其中 k 是非负数。

    ListNode* rotateRight(ListNode* head, int k) {
        if (!head || !head->next || k == 0) return head;
        ListNode *cur = head;
        int len = 1;
        while (cur->next && ++len) cur = cur->next;
        cur->next = head;
        k = len - k % len;
        while (k--) cur = cur->next;
        head = cur->next;
        cur->next = nullptr;
        return head; 
    }

删除节点

o1时间内删除链表节点

题目描述: 给定链表中间的某个节点指针,在O(1)时间内删除该节点。假定你只能访问该节点。
分析: 在这个问题里,由于访问不到该节点的前一个节点,所以 这题对尾节点是无解的 。要删除当前节点,直接将其next节点的值复制到当前节点,然后删除后继节点。

实现代码:

bool deleteListNode(ListNode * cur){
    if(cur == nullptr || cur->next == nullptr)
        return false;
    ListNode * pNext = cur->next;
    cur->val = pNext->val;
    cur->next = pNext->next;
    delete pNext;
    return true;
}

删除排序链表中重复元素

leetcode 83

题目描述: 给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。

分析: 双指针法,将相同的第一个节点的next指针指向下一个不同的节点或者NULL

    ListNode* deleteDuplicates(ListNode* head) {
        ListNode *cur = head;
        while (cur && cur->next) {
            if (cur->val == cur->next->val)
                cur->next = cur->next->next;
            else
                cur = cur->next;
        }
        return head;
    }

删除排序链表中的重复元素2

leetcode 82

题目描述: 给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 没有重复出现 的数字。

    ListNode* deleteDuplicates(ListNode* head) {
        if (!head|| !head->next)  return head;
        
        ListNode dummy(0);  dummy.next = head;
        ListNode* cur = head;   ListNode* prev = &dummy;
        while (cur && cur->next){
             if (cur->next->val == cur->val) {
                int val = cur->val;
                while (cur && cur->val == val) {
                    prev->next = cur->next;
                    delete cur;
                    cur = prev->next;
                }
             } else {
                 prev = cur;
                 cur = cur->next;
             }
        }
        return dummy.next;
    }

链表查找

求链表的倒数第k个节点

题目描述: 输入一个单向链表,输出该链表中倒数第k个节点。
分析: 快慢指针法,设置两个指针slow,fast,初始时都指向head,先让fast走k步,然后slow,fast同时走,当fast走到链表末尾时,slow就是倒数第k个节点。

ListNode* findkthNode(ListNode *head, int k){
    if(k < 0 || head == nullptr) return nullptr;
    ListNode* slow, *fast;
    slow = fast = head;
    int i = k;
    while(i > 0 && fast){
        fast = fast->next;
        i--;
    }
    if(i > 0) return nullptr;
    while(fast){
        slow = slow->next;
        fast = fast->next;
    } 
    return slow;
}

求链表的中间节点

题目描述: 求链表的中间节点,,如果链表长度为偶数,返回中间两个节点的任意一个,若为奇数,返回中间节点。
分析: 快慢指针法,fast每次移动两部,slow每次移动一步。

ListNode* findMiddleNode(Node* head){
    if(head == nullptr) return nullptr;

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

交换链表中的节点

leetcode 1721

给你链表的头节点 head 和一个整数 k 。

交换 链表正数第 k 个节点和倒数第 k 个节点的值后,返回链表的头节点(链表 从 1 开始索引)。

示例

  • 输入:1 2 3 4 2 k = 2
  • 输入:1 4 3 2 5
    ListNode* swapNodes(ListNode* head, int k) {
        auto fast = head;
        for(int i = 1 ; i < k ; i ++){
            fast = fast->next;
        }
        auto temp = fast, slow = head;
        while(fast->next){
            fast = fast->next;
            slow = slow->next;
        }
        swap(slow->val, temp->val);
        return head;
    }

环形/相交链表

判断单链表是否有环

题目描述: 输入一个单向链表,判断链表是否有环。 分析: 快慢指针,如果存在环,那么两个指针必会在环中相遇。

bool hasCircle(ListNode *head){
    Node *slow = head, *fast = head;
    while(fast && fast->next){
        fast = fast->next->next;
        slow = slow->next;
        if(fast == slow){
            return true;
        }
    }
    return false;
}

找到环的入口

leetcode 142

题目描述: 输入一个单向链表,判断链表是否有环,如果存在环,找到环的入口。
分析: 快慢指针,如果快慢指针相遇,则有环,这是将其任意一个设为head,按相同速度走,则其再次相遇时,即为环的入口。

    ListNode *detectCycle(ListNode *head) {
        ListNode * fast = head, *slow = head;
        while(fast && fast->next){
            fast = fast->next->next;
            slow = slow->next;
            if(fast == slow){
                fast = head;
                while(fast != slow){
                    fast = fast->next;
                    slow = slow->next;
                }
                return fast;
            }
        }
        return nullptr;
    }

判断两个链表是否相交

题目描述: 给出两个单链表的头指针,判断两个链表是否相交,假设两个链表均不带环。 分析: 如过两个链表相交,则从交点到链表末尾都是公共的,所以只需要判断两个链表的末尾节点是否相同。 时间复杂度O(len1+len2),空间复杂度O(1)。

bool isIntersect(ListNode* h1, ListNode* h2){
    if(h1 == nullptr || h2 == nullptr) return false;
    while(h1->next) h1 = h1->next;
    while(h2->next) h2 = h2->next;
    return h1 == h2;
}

有环链表判断相交

题目描述: 对第7题,如果链表是有环的,该怎么做?
分析: 如果两个链表有环且相交,则两个链表有共同一个环,因此可以一个链表上两个指针相遇的节点,在不在另一个链表上。

bool isIntersectWithCircle(ListNode* h1, ListNode* h2){
    ListNode * p1 = detectCycle(h1); *p2 = detectCycle(h2);
    if(!p1 || !p2) return false;

    ListNode * tmp = p2->next;
    while(tmp != p2){
        if(tmp == p1)
            return true;
        tmp = tmp->next;
    }
    return false;
}

两个链表相交的第一个公共节点

题目描述: 如果两个无环单链表相交,求出它们第一个公共节点。
分析: 计算两个链表长度l1和l2,将较长链表像后移动l2-l1个节点,然后同时移动两个指针,直到相等。

ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
    ListNode *h1 = headA, *h2 = headB;
    
    while(h1 != h2){
        h1 = h1 ? h1->next : headB;
        h2 = h2 ? h2->next : headA;
    }
    return h1;
}

链表合并

合并2个有序链表

leetcode 21

题目描述: 将两个升序链表合并为一个新的 升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        ListNode* dummy = new ListNode(0);
        ListNode* p = dummy, *cur;
        while (l1 && l2) {
            cur = (l1->val < l2->val) ? l1 : l2;
            p->next = cur;
            p = p->next;
            cur == l1 ? l1 = l1->next : l2 = l2->next;
        }
        p->next = l1 ? l1 : l2;
        return dummy->next;
    }

合并k个有序链表

leetcode 23

题目描述: 合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。

分析: 维护当前每个链表没有被合并的元素的最前面一个,k个链表就最多有 k个满足这样条件的元素,每次在这些元素里面选取 val 属性最小的元素合并到答案中。在选取最小元素的时候,我们可以用优先队列来优化这个过程。

复杂度:

  • 时间复杂度:考虑优先队列中的元素不超过 k 个,那么插入和删除的时间代价为 O(log k),这里最多有 kn 个点,对于每个点都被插入删除各一次,故总的时间代价即渐进时间复杂度为O(kn×logk)
  • 空间复杂度:这里用了优先队列,优先队列中的元素不超过 k个,故渐进空间复杂度为 O(k)。
    struct Status {
        int val;
        ListNode *ptr;
        bool operator < (const Status &rhs) const {
            return val > rhs.val;
        }
    };

    priority_queue <Status> q;

    ListNode* mergeKLists(vector<ListNode*>& lists) {
        for (auto node: lists) {
            if (node) q.push({node->val, node});
        }
        ListNode head, *tail = &head;
        while (!q.empty()) {
            auto f = q.top(); q.pop();
            tail->next = f.ptr; 
            tail = tail->next;
            if (f.ptr->next) q.push({f.ptr->next->val, f.ptr->next});
        }
        return head.next;
    }

合并奇偶排序链表

链表奇数位升序,偶数位降序,合并为排序链表
如: 1 8 3 6 5 4 7 2 9 排序后:1 2 3 4 5 6 7 8 9

方法

  • 对链表进行奇偶切分;
  • 反转降序链表
  • 合并两个升序链表
    ListNode* mergeList(ListNode* head) {
        if (!head || !head->next) return head;
        ListNode *p1 = head, *p = head->next, *p2 = p;
        while (p2 && p2->next) { //切分
            p1->next = p1->next->next;
            p2->next = p2->next->next;
            p1 = p1->next; p2 = p2->next;
        }
        p1->next = nullptr;

        if (p && p->next) {   //反转
            ListNode *pre = nullptr, *cur = p, *next;
            while (cur) {
                next = cur->next; cur->next = pre;
                pre = cur; cur = next;
            } p = pre;
        }
        //合并
        ListNode *dummy = new ListNode(-1), *cur;
        p1 = head; p2 = p; p = dummy;
        while (p1 && p2) {
            cur = (p1->val < p2->val) ? p1 : p2;
            p->next = cur; p = p->next;
            cur == p1 ? p1 = p1->next : p2 = p2->next;
        }
        p->next = p1 ? p1 : p2;
        return dummy->next;
    }

链表排序

重排链表

leetcode 143

给定链表 L : L0 -> L1 -> ... -> Ln-1 -> Ln,
重新排列为: L0 -> Ln -> L1 -> Ln-1 -> L2 -> Ln-2 -> ...

     void reorderList(ListNode *head) {
        if (!head || !head->next) return;
        // 1. find middle node, split
        ListNode *p1 = head, *p2 = head->next;
        while (p2 && p2->next) {
            p1 = p1->next;
            p2 = p2->next->next;
        }
        p2 = p1->next; p1->next = nullptr;

        //2. reverse p2
        ListNode *p = p2,  *pre = nullptr, *next;
        while (p) {
            next = p->next; p->next = pre;
            pre = p; p = next;
        }
        
        //3. merge
        ListNode *dummy = new ListNode(-1); dummy->next = head;
        p = dummy; p1 = head;  p2 = pre;
        while (p1 && p2) {
            p->next = p1;
            p1 = p1->next;
            p->next->next = p2;
            p = p2;
            p2 = p2->next;
        }
        if (p1) p->next = p1;
    }

链表归并

leetcode 148

在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。

    ListNode* sortList(ListNode* head) {
        if (!head || !head->next) return head;
        ListNode *fast = head, *slow = head, *pre = NULL;
        while(fast && fast->next) {
            pre = slow;
            slow = slow->next;
            fast = fast->next->next;
        }
        pre->next = NULL;
        return merge(sortList(head), sortList(slow));
    }
    
    ListNode* merge(ListNode *l1, ListNode *l2) {
        if (!l1 || !l2) return l1 ? l1 : l2;
        if (l1->val < l2->val) {
            l1->next = merge(l1->next, l2);
            return l1;
        }else {
            l2->next = merge(l1, l2->next);
            return l2;
        }
        return l2;
    }

链表快排

    // 链表范围为 [lo, hi), pre 为 lo 的前置节点
    void qsort(ListNode* pre, ListNode* lo, ListNode* hi) {  
        if (lo == hi || lo->next == hi) return;        // 至少有一个元素
            
        auto mid = partition(pre, lo, hi);
        qsort(pre, pre->next, mid);  // qsort(pre, lo, mid);
        qsort(mid, mid->next, hi);
    }

    ListNode* partition(ListNode* pre, ListNode* lo, ListNode* hi) {
        int key = lo->val;
        auto mid = lo;  // 不是必须的,直接使用 lo 也可以
        
        ListNode ll(0), rr(0);  // 创建两个新链表
        auto l = &ll, r = &rr;  // ListNode *l = &ll, *r = &rr;
        // i 从 lo 的下一个节点开始遍历,因为 lo 是枢纽不参与遍历
        for (auto i=lo->next; i != hi; i = i->next) {  
            if (i->val < key) {
                l = l->next = i;  // python 中不能这么写
            } else {
                r = r->next = i;  // python 中不能这么写
            }
        }  
        // 拼接
        r->next = hi;
        l->next = mid;  // 这里的 mid 实际上就是 lo,即 l->next = lo
        mid->next = rr.next;
        pre->next = ll.next;
        
        return mid;  // 返回中枢
    }

    ListNode* sortList(ListNode* head) {
        if(head == nullptr || head->next == nullptr)
            return head;
        
        ListNode pre(0);  // 设置一个新的头结点
        pre.next = head;
        qsort(&pre, head, nullptr);
        
        return pre.next;
    }

其它

回文链表

leetcode 234

请判断一个链表是否为回文链表。

    bool isPalindrome(ListNode* head) {
        ListNode *slow = head, *fast = head, *rev = nullptr;
        while (fast && fast->next) {  //反转前半部分
            fast = fast->next->next;
            ListNode *tmp = slow->next;
            slow->next = rev;
            rev = slow;
            slow = tmp;
        }
        if (fast) slow = slow->next;
        while (slow && rev) {
            if (slow->val != rev->val) return false;
            slow = slow->next;
            rev = rev->next;
        }
        return true;
    }

复制带随机指针的链表

leetcode 138

给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。

要求返回这个链表的 深拷贝。

链表节点定义为:

class Node {
public:
    int val;
    Node* next;
    Node* random;
    
    Node(int _val) {
        val = _val;
        next = NULL;
        random = NULL;
    }
};
    Node* copyRandomList(Node* head) {
        Node *newHead, *l1, *l2;
        if (!head) return head; //将每个节点复制一遍
        for (l1 = head; l1; l1 = l1->next->next) {
            l2 = new Node(l1->val);
            l2->next = l1->next;
            l1->next = l2;
        }  
        newHead = head->next; //复制random指针
        for (l1 = head; l1; l1 = l1->next->next) {
            if (l1->random) l1->next->random = l1->random->next;
        }
        //分离复制的链表
        for (l1 = head; l1; l1 = l1->next) {
            l2 = l1->next;
            l1->next = l2->next;
            if (l2->next) l2->next = l2->next->next;
        }

        return newHead;
    }

二叉搜索树与双向链表

leetcode 426,剑指offer 36

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。

    Node* treeToDoublyList(Node* root) {
       if(!root) return root;
       stack<Node*> st;
       Node *pre = NULL, *p = root;
       while (p || !st.empty()) {
           while(p) {
               st.push(p);  p = p->left;
           } 
           if (!st.empty()) {
               p = st.top();  st.pop();
               if (!pre) {
                   pre = p; root = p;
               } else {
                   pre->right = p;
                   p->left = pre; pre = p;
               }
               p = p->right;
           }
       }
       pre->right = root;
       root->left = pre;
       return root;
    }

奇偶链表

leetcode 328

给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。

请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。

    ListNode* oddEvenList(ListNode* head) {
        if (!head || !head->next) return head;
        ListNode *p1 = head, *p = head->next, *p2 = p;
        while (p2 && p2->next) {
            p1->next = p1->next->next;
            p2->next = p2->next->next;
            p1 = p1->next;
            p2 = p2->next;
        }
        p1->next = p;
        return head;
    }

分割链表

leetcode 86

给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。

你应当 保留 两个分区中每个节点的初始相对位置。

示例

  • 输入:1 4 3 2 5 2 x = 3
  • 输出:1 2 2 4 3 5
    ListNode* partition(ListNode* head, int x) {
        if (!head || !head->next) return head;
        ListNode *p1 = nullptr, *p2 = nullptr, *p = head;
        ListNode *l = nullptr, *r = nullptr;
        while (p) {
            if (p->val < x) {
                if (!p1) {
                    p1 = p; l = p;
                } else {
                    l->next = p; l = p;
                }
            } else {
                if (!p2) {
                    p2 = p; r = p;
                } else {
                    r->next = p; r = r->next;
                }
            }
            p = p->next;
        }
        if (p2) r->next = nullptr;
        if (p1) l->next = p2;
        return p1 ? p1 : p2;
    }

链表采样

leetcode 382

给定一个单链表,随机选择链表的一个节点,并返回相应的节点值。保证每个节点被选的概率一样。

进阶: 如果链表十分大且长度未知,如何解决这个问题?你能否使用常数级空间复杂度实现?

class Solution {
public:
    Solution(ListNode* head) {
        list = head;
        ListNode* ptr = head;
        while (ptr) {
            len++;
            ptr = ptr->next;
        }
    }
    
    /** Returns a random node's value. */
    int getRandom() {
        int rand_index = rand()%len;
        ListNode* ptr = list;
        for (int i=0; i<rand_index; i++) {
            ptr = ptr->next;
        }
        return ptr->val;
    }

private:
    int len = 0;
    ListNode* list;
};

打赏一下

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦