【CPP】《程序员面试金典》习题(2)——链表

时间:2022-07-22
本文章向大家介绍【CPP】《程序员面试金典》习题(2)——链表,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

这次的题比较少,题目的主题是链表,最值得注意的是快慢指针的用法和最后一题的Floyd判圈算法。

链表这一章常用到的思路还有以下的四点,很多题目都要用到

  1. 遇到链表题时务必弄清是单向表还是双向表
  2. 删除链表节点时必须检查空指针,表头和表尾
  3. 快行指针是很常见的技巧,一个快一个慢可以很方便地得到链表到头的信息和中点的信息
  4. 找重复有一种很常见做法是用散列表

执行排名和时间受到LeetCode服务器的不稳定限制因而只有参考价值,重点不是题怎么写而是题目解法带来的算法启发。

这一次有的题目本身描述就很不清晰,有些遗憾。

代码和其他一些LeetCode代码我保存在了https://github.com/ZFhuang/LeetCodes

02.01 移除重复节点【简单】

编写代码,移除未排序链表中的重复节点。保留最开始出现的节点。

示例1:
 输入:[1, 2, 3, 3, 2, 1]
 输出:[1, 2, 3]
 
示例2:
 输入:[1, 1, 1, 1, 2]
 输出:[1, 2]
 
提示:
链表长度在[0, 20000]范围内。
链表元素在[0, 20000]范围内。

进阶:
如果不得使用临时缓冲区,该怎么解决?

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/remove-duplicate-node-lcci
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一

    //有使用缓冲区的单指针做法,96.29%,24ms
    //每次发下有一个重复就会更改一下当前链表,所以比较慢
    ListNode* removeDuplicateNodes(ListNode* head) {
        //空链表或单元素链表是不会重复的
        if (!head || !head->next)
            return head;

        //缓冲区
        bool dup[20001];
        memset(dup, 0, sizeof(bool) * 20001);
        ListNode* p = head;
        //先存入第一个元素
        dup[p->val] = true;
        //从第二个元素开始判断
        while (p->next) {
            //下一个元素无重复时
            if (!dup[p->next->val]) {
                //标记并进到下一个
                dup[p->next->val] = true;
                p = p->next;
            }
            //出现重复时
            else {
                //跳过这个元素
                p->next = p->next->next;
            }
        }

        return head;
    }

解法二

    //有使用缓冲区的双指针做法,99.5%,20ms
    //思路与解法一一样
    ListNode* removeDuplicateNodes(ListNode* head) {
        if (!head || !head->next)
            return head;

        bool visited[20001] = { false };

        ListNode* dummyHead = new ListNode(-65535);
        dummyHead->next = head;

        //一个滞后的指针来指向上一个元素
        ListNode* pre = dummyHead, * cur = head;

        while (cur) {
            if (visited[cur->val] == false) {
                visited[cur->val] = true;
                pre = cur;
                cur = cur->next;
            }
            else {
                pre->next = cur->next;
                cur = pre->next;
            }
        }

        return head;
    }

解法三

    //不使用缓冲区的做法,14.6%,484ms
    //此方法需要O(n2)的复杂度,用两个指针来暴力判断是否出现,很慢
    ListNode* removeDuplicateNodes(ListNode* head) {
        if (!head || !head->next)
            return head;

        //p指针是当前位置,s指针是用于在前面搜索是否有重复
        ListNode* p = head, * s = head;
        while (p->next) {
            s = head;
            bool dup = false;
            //从头到当前节点开始搜索是否有重复
            while (s != p->next) {
                if (s->val == p->next->val) {
                    dup = true;
                    break;
                }
                else {
                    s = s->next;
                }
            }
            //对重复进行处理
            if (dup) {
                p->next = p->next->next;
            }
            else {
                p = p->next;
            }
        }

        return head;
    }

02.02 返回倒数第 k 个节点【简单】

实现一种算法,找出单向链表中倒数第 k 个节点。返回该节点的值。

注意:本题相对原题稍作改动

示例:
输入: 1->2->3->4->5 和 k = 2
输出: 4
说明:

给定的 k 保证是有效的。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/kth-node-from-end-of-list-lcci
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一

    //双指针思路,77.4%,4ms
    int kthToLast(ListNode* head, int k) {
        //一个表示倒数k位的元素,一个是当前元素
        ListNode* p_k = NULL, * cur = head;
        int count = 1;
        //当当前元素到尾部跳出
        while (cur != NULL) {
            //达到倒数区时赋值
            if (count == k) {
                p_k = head;
            }
            //一同前进
            if (count > k) {
                p_k = p_k->next;
            }
            cur = cur->next;
            ++count;
        }
        return p_k->val;
    }

02.03 删除中间节点

实现一种算法,删除单向链表中间的某个节点(除了第一个和最后一个节点,不一定是中间节点),假定你只能访问该节点。

示例:

输入:单向链表a->b->c->d->e->f中的节点c
结果:不返回任何数据,但该链表变为a->b->d->e->f

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/delete-middle-node-lcci
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一

    //删除节点,91.4%,8ms
    //题意是要删去参数输入的这个节点,没有给出前置节点
    void deleteNode(ListNode* node) {
        //由于没有给出前置,选择替换删去下一个节点
        //先把下一个节点的值赋值到当前节点
        node->val = node->next->val;
        //然后跳过下个节点
        node->next = node->next->next;
    }

02.04 分割链表【中等】

编写程序以 x 为基准分割链表,使得所有小于 x 的节点排在大于或等于 x 的节点之前。如果链表中包含 x,x 只需出现在小于 x 的元素之后(如下所示)。分割元素 x 只需处于“右半部分”即可,其不需要被置于左右两部分之间。

示例:
输入: head = 3->5->8->5->10->2->1, x = 5
输出: 3->1->2->10->5->5->8

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/partition-list-lcci
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一

    //理解题意是关键,70.2%,8ms
    //题目说的很乱,其实就是要把链表大于的和小于的元素分立两边
    //然后不限制大于和小于部分的内部顺序,答案是不唯一的
    ListNode* partition(ListNode* head, int x) {
        //思路自然就是两个额外的链表
        ListNode* Less = new ListNode(0);
        ListNode* Larger = new ListNode(0);
        ListNode* p = head, * less = Less, * larger = Larger;
        while (p) {
            //小于的放一边
            if (p->val < x) {
                less->next = p;
                p = p->next;
                less = less->next;
                less->next = NULL;
            }
            //大于的放另一边
            else {
                larger->next = p;
                p = p->next;
                larger = larger->next;
                larger->next = NULL;
            }
        }
        //最后将两个链表连接起来
        less->next = Larger->next;
        return Less->next;
    }

02.05 链表求和【中等】

给定两个用链表表示的整数,每个节点包含一个数位。
这些数位是反向存放的,也就是个位排在链表首部。
编写函数对这两个整数求和,并用链表形式返回结果。

示例1:
输入:(7 -> 1 -> 6) + (5 -> 9 -> 2),即617 + 295
输出:2 -> 1 -> 9,即912
进阶:假设这些数位是正向存放的,请再做一遍。

示例2:
输入:(6 -> 1 -> 7) + (2 -> 9 -> 5),即617 + 295
输出:9 -> 1 -> 2,即912

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/sum-lists-lcci
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一

  //链表直接相加,95.04%,16ms
  //由于数据选得不好,这道题LeetCode判题机运行时间波动极大
  //思路是同时遍历两个链表,相加放到第三个链表中,要考虑进位
  ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
    ListNode* P = new ListNode(0);
    ListNode* p = P;
    while (l1 || l2) {
      //相加,为了让两个链表能完整写在一个循环中,选择这样的多个if结构
      int add = 0;
      if (l1 && l2)
        add = l1->val + l2->val;
      else if (l1)
        add = l1->val;
      else if (l2)
        add = l2->val;
      //放到第三个链表中
      p->next = new ListNode(add % 10);
      //链表一同前进
      if (l1)
        l1 = l1->next;
      if (l2)
        l2 = l2->next;
      p = p->next;
      //进位判断
      if (add / 10 > 0) {
        if (l1 && l2) {
          l1->val = l1->val + 1;
        }
        else if (l1) {
          l1->val = l1->val + 1;
        }
        else if (l2) {
          l2->val = l2->val + 1;
        }
        else {
          //都清零时退出
          p->next = new ListNode(1);
          break;
        }
      }
    }

    return P->next;
  }

02.06 回文链表【简单】

编写一个函数,检查输入的链表是否是回文的。

示例 1:
输入: 1->2
输出: false

示例 2:
输入: 1->2->2->1
输出: true

进阶:
你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/palindrome-linked-list-lcci
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一

    //逆置链表法,67.6%,24ms
    //利用了回文串的一半逆序后与另一半会相同的特点
    bool isPalindrome(ListNode* head) {
        //先计算出链表的长度
        int len = 0;
        ListNode* p = head;
        while (p) {
            ++len;
            p = p->next;
        }
        //长度小于等于1的必然是回文
        if (len <= 1) {
            return true;
        }
        //获取第一个元素和第二个元素
        ListNode* q = head, * r = NULL;
        p = head->next;
        q->next = NULL;
        //长度等于2的直接判断两个元素是否相等即可
        if (len == 2) {
            if (p->val == q->val) {
                return true;
            }
            else {
                return false;
            }
        }
        //计算中点的序号和寄偶
        int count = 1, half = len / 2;
        bool odd = len % 2;
        //将中点前的链表逆序
        while (p) {
            if (count >= half) {
                break;
            }
            r = p->next;
            p->next = q;
            q = p;
            p = r;
            ++count;
        }
        //若长度为奇则跳过中间元素
        if (odd) {
            p = p->next;
        }
        //继续遍历,若有不相同的元素即不是回文
        while (p && q) {
            if (p->val != q->val) {
                return false;
            }
            p = p->next;
            q = q->next;
        }
        return true;
    }

解法二

    //快慢指针法,99.1%,16ms
    //优化的关键在于利用快慢指针来在一次遍历中找到中点并将前半段链表逆序
    bool isPalindrome(ListNode* head) {
        //空则返回
        if (!head) return true;
        //建立快慢指针,rh指向的是逆序的前半段链表开头
        ListNode* fast = head, * slow = head, * rh = new ListNode(0);
        //当快针存在且拥有下一针时循环
        //这个约束使得跳出时时快针是最后一个或空,慢针则是中间元素或中间两个的后一个
        while (fast && fast->next) {
            //快针走两步
            fast = fast->next->next;
            //慢针走一步且逆序
            slow = slow->next;
            head->next = rh->next;
            rh->next = head;
            head = slow;
        }
        //如果此时快针存在,代表慢针处于中间元素,慢再走一步
        if (fast) slow = slow->next;
        rh = rh->next;
        //从逆序链表开始遍历,和慢针一起遍历,因为此时慢针即是中位元素
        while (rh) {
            //判断返回
            if (rh->val != slow->val) return false;
            rh = rh->next;
            slow = slow->next;
        }

        return true;
    }

02.07 链表相交【简单】

给定两个(单向)链表,判定它们是否相交并返回交点。
请注意相交的定义基于节点的引用,而不是基于节点的值。
换句话说,如果一个链表的第k个节点与另一个链表的第j个节点是同一节点(引用完全相同),
则这两个链表相交。

示例 1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Reference of the node with value = 8
输入解释:相交节点的值为 8 (注意,如果两个列表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],
链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;
在 B 中,相交节点前有 3 个节点。

示例 2:
输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Reference of the node with value = 2
输入解释:相交节点的值为 2 (注意,如果两个列表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [0,9,1,2,4],
链表 B 为 [3,2,4]。在 A 中,相交节点前有 3 个节点;
在 B 中,相交节点前有 1 个节点。

示例 3:
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
输入解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
解释:这两个链表不相交,因此返回 null。

注意:
如果两个链表没有交点,返回 null 。
在返回结果后,两个链表仍须保持原有的结构。
可假定整个链表结构中没有循环。
程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/intersection-of-two-linked-lists-lcci
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一

    //暴力法,5.1%,680ms
    //最容易想到的方法,直接两重循环查找,非常慢,O(n2)时间复杂度
    ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) {
        ListNode* p = headA, * q = headB;
        while (p) {
            q = headB;
            while (q) {
                if (p == q) {
                    return p;
                }
                q = q->next;
            }
            p = p->next;
        }
        return NULL;
    }

解法二

    //长度法,54.1%,60ms
    //两节点相同意味着那个节点之后的节点都相同,即相同节点之后的长度都相同
    //那么遍历得到长度后将两链表调整到同个长度后同时遍历即可找到,O(n)复杂度
    ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) {
        if (!headA || !headB)
            return NULL;
        ListNode* p = headA, * q = headB;
        //查找两链表长度
        int lenA = 0, lenB = 0;
        while (p->next) {
            p = p->next;
            ++lenA;
        }
        while (q->next) {
            q = q->next;
            ++lenB;
        }
        //若最后一个元素不同则必然不相交
        if (p != q) {
            return NULL;
        }
        //回指
        p = headA;
        q = headB;
        //利用长度差异调整一次
        if (lenA > lenB) {
            while (lenA > lenB) {
                p = p->next;
                --lenA;
            }
        }
        else if (lenB > lenA) {
            while (lenB > lenA) {
                q = q->next;
                --lenB;
            }
        }
        //同时遍历查找相同节点
        while (p && q) {
            if (p == q) {
                return p;
            }
            p = p->next;
            q = q->next;
        }
        return NULL;
    }

解法三

    //双指针法,100%,40ms
    //让两指针互相追赶,利用两链表的长度差距改变指针的位置,省去匹配链表长度的一步
    //可以画图辅助理解,假设有下图,下链表比较短且在4处于上链表交叉
    //1-2-3-4-5
    //  6-7-|
    ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) {
        if (headA == NULL || headB == NULL)return NULL;
        ListNode* curA = headA, * curB = headB;
        //当两个指针相同时跳出
        while (curA != curB) {
            //如例:两链表同时前进时,下链表会先到中点
            if (curA == NULL && curB == NULL)break;
            else {
                //若某指针先到终点,则切换到另一串,省下的时间会在长串中被补偿
                //这样先到终点的指针和后到终点的指针会被调整为在同一速度上
                //也就是完成了上一种做法中的长度匹配步骤
                //同时到达终点还未交叉表示无交叉
                if (curA == NULL)curA = headB;
                else  curA = curA->next;
                if (curB == NULL)curB = headA;
                else curB = curB->next;
            }
        }
        //按照情况不同来返回
        if (curA == curB)return curA;
        else return NULL;
    }

02.08 环路检测【中等】

给定一个有环链表,实现一个算法返回环路的开头节点。
有环链表的定义:在链表中某个节点的next元素指向在它前面出现过的节点,则表明该链表存在环路。

示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:tail connects to node index 1
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:
输入:head = [1,2], pos = 0
输出:tail connects to node index 0
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:
输入:head = [1], pos = -1
输出:no cycle
解释:链表中没有环。

进阶:
你是否可以不用额外空间解决此题?

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/linked-list-cycle-lcci
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一

    //快慢指针找环入口,98.2%,8ms
    //也称Floyd判圈算法/龟兔赛跑算法
    //还有复杂度常数比较低的Brents算法,但不适用于这道题
    //详见:https://blog.csdn.net/u011221820/article/details/78821464
    //当链表中有环时,不同前进速度(设为2)的指针必然会在某处相遇(龟兔环形赛跑)
    //当相遇时,如果让慢指针再走一圈,快指针不动,再相遇时得到环的长度
    //而若相遇时让快指针返回起点再以慢指针的速度同时前进,则两指针会在环的入口处相遇
    //因为慢针走到旧相遇点时:
    //快针的行走距离是x1+x2+x3+x2,慢针的行走距离是x1+x2(x1是环外的距离)
    //由于2(x1+x2)=x1+x2+x3+x2,所以得x3=x1
    //因此当快针回到起点,以慢针的速度前进时,当走了x1的距离时,慢针也恰好走了x3的距离
    //也即是在环入口相遇
    ListNode* detectCycle(ListNode* head) {
        //先排除基础情况
        if (!head || !head->next) {
            return NULL;
        }
        if (head->next == head) {
            return head;
        }
        ListNode* fast = head, * slow = head;

        //类似0207的循环条件,这样保证了快指针慢指针都不会错误访问
        while (fast && fast->next) {
            //快指针的速度是慢指针的两倍
            fast = fast->next->next;
            slow = slow->next;
            //当两指针重叠时
            if (fast == slow) {
                //将快指针回到开头
                fast = head;
                while (slow != fast) {
                    //相同速度前进
                    slow = slow->next;
                    fast = fast->next;
                }
                return slow;
            }
        }

        return NULL;
    }