剑指offer--链表

第1题:从尾到头打印链表

思路

递归

我们都知道链表无法逆序访问,那肯定无法直接遍历链表得到从尾到头的逆序结果。但是我们都知道递归是到达底层后才会往上回溯,因此我们可以考虑递归遍历链表,因此三段式如下:

终止条件: 递归进入链表尾,即节点为空节点时结束递归。
返回值: 每次返回子问题之后的全部输出。
本级任务: 每级子任务递归地进入下一级,等下一级的子问题输出数组返回时,将自己的节点值添加在数组末尾。
具体做法:

step 1:从表头开始往后递归进入每一个节点。
step 2:遇到尾节点后开始返回,每次返回依次添加一个值进入输出数组。
step 3:直到递归返回表头。

答案

class Solution {
public:
    void recursion(ListNode* head, vector<int> &res){
        if(head==nullptr) return;
        else{
            recursion(head->next,res);
            res.push_back(head->val);
        }
        return ;
    }
    vector<int> printListFromTailToHead(ListNode* head) {
        vector<int> res;
        recursion(head,res);
        return res;
    }
};

模拟栈

答案

class Solution {
public:
    vector<int> printListFromTailToHead(ListNode* head) {
        vector<int> res;
        stack<int> st;
        while(head!=nullptr){
            st.push(head->val);
            head = head->next;
        }
        while(!st.empty()){
            int val = st.top();
            st.pop();
            res.emplace_back(val);
        }
        return res;
    }
};

附加:翻转链表

思路

双指针

两个指针 pre cur
初始化:cur = head; pre = NULL
遍历终止条件:while(cur)
temp = cur->next
cur->next =pre
pre = cur
cur = temp
return pre

答案

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* temp; // 保存cur的下一个节点
        ListNode* cur = head;
        ListNode* pre = NULL;
        while(cur) {
            temp = cur->next;  // 保存一下 cur的下一个节点,因为接下来要改变cur->next
            cur->next = pre; // 翻转操作
            // 更新pre 和 cur指针
            pre = cur;
            cur = temp;
        }
        return pre;
    }
};

递归

reverse(head, NULL)
递归终止 if(cur==NULL) return pre
递归逻辑 temp = cur->next
cur->next = pre
reverse(temp,cur)

答案

class Solution {
public:
    ListNode* reverse(ListNode* pre,ListNode* cur){
        if(cur == NULL) return pre;
        ListNode* temp = cur->next;
        cur->next = pre;
        // 可以和双指针法的代码进行对比,如下递归的写法,其实就是做了这两步
        // pre = cur;
        // cur = temp;
        return reverse(cur,temp);
    }
    ListNode* reverseList(ListNode* head) {
        // 和双指针法初始化是一样的逻辑
        // ListNode* cur = head;
        // ListNode* pre = NULL;
        return reverse(NULL, head);
    }

};

第2题:合并两个排序的链表

思路

变量:准备四个指针
dummy, pre, l1, l2
逻辑:每一轮对l1和l2指向结点比较,让pre指向较小的那一个,并让较小的结点像后指
结束条件:l1或l2指向nullptr

答案

/**
 * struct ListNode {
 *	int val;
 *	struct ListNode *next;
 *	ListNode(int x) : val(x), next(nullptr) {}
 * };
 */
class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param pHead1 ListNode类 
     * @param pHead2 ListNode类 
     * @return ListNode类
     */
    ListNode* Merge(ListNode* pHead1, ListNode* pHead2) {
        // write code here
        // write code here
        auto dummy = new ListNode(-1);
        ListNode* pre = dummy;
        while(pHead1!=nullptr&&pHead2!=nullptr){
            if(pHead1->val <= pHead2->val){
                pre->next = pHead1;
                pHead1 = pHead1->next;
            }else{
                pre->next = pHead2;
                pHead2 = pHead2->next;
            }
            pre = pre->next;
            if(!dummy->next) dummy->next = pre;
        }

        //如果pHead1中还有结点
        if(pHead1!=nullptr) pre->next = pHead1;
        //如果pHead2中还有结点
        if(pHead2!=nullptr) pre->next = pHead2;
        return dummy->next;
    }
};

第3题:两个链表的第一个公共结点

https://www.nowcoder.com/practice/6ab1d9a29e88450685099d45c9e31e46?tpId=13&tqId=23257&ru=/exam/oj/ta&qru=/ta/coding-interviews/question-ranking&sourceUrl=%2Fexam%2Foj%2Fta%3Fpage%3D1%26tpId%3D13%26type%3D13%26topicNameBigMember%3D%25E5%2589%2591%25E6%258C%2587offer%26topicIdBigMember%3D13%26pageSourceBigMember%3Dnc04

描述

输入两个无环的单向链表,找出它们的第一个公共结点,如果没有公共节点则返回空。

第4题:两个链表的第一个公共结点

描述

输入两个无环的单向链表,找出它们的第一个公共结点,如果没有公共节点则返回空。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的)
TC: O(n)
SC: O(1)

思路

双指针

class Solution {
public:
    ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
        ListNode *ta = pHead1, *tb = pHead2;
        while (ta != tb) {
            ta = ta ? ta->next : pHead2;
            tb = tb ? tb->next : pHead1;
        }
        return ta;
    }
};

第5题:链表中环的入口结点

  • 题目描述:给定一条链表,若链表存在环,就请找到环的入口并返回入口的指针;若不存在环就返回null
  • 思路:快慢指针
  • 答案:
ListNode* EntryNodeOfLoop(ListNode* pHead) {
        ListNode *fast = pHead, *slow = pHead;    // 快慢指针一开始都指向头
        while(fast){
            slow = slow->next;    // 慢指针走一步
            if(fast->next == nullptr) return nullptr;    // 若快指针的下一步不能走,则说明两指针不会相遇
            fast = fast->next->next;    // 快指针向后走两步
            if(fast == slow){    // 找到相交节点, 此时慢指针已经走了nb步
                fast = pHead;    // 快指针重新移动到头
                while(fast != slow){    // 直到两指针相遇位置,每次向后走一步
                    fast = fast->next;
                    slow = slow->next;
                }
                return fast;    // 找到入口节点,直接返回
            }
        }
        return nullptr;
    }

第6题:链表中倒数最后k个结点

  • 题目描述
    输入一个长度为n的链表,设链表中的元素的值为\(a_i\),返回该链表中的第k个结点。
    如果该链表长度小于\(k\),请返回一个长度为0的链表

  • 思路
    双指针

    • step1: 准备一个快指针,从链表头开始,在链表上先走k步。
    • step2: 准备慢指针指向原始链表头,代表当前元素,则慢指针与快指针之间的距离一致都是k。
    • step3:快慢指针同步移动,当快指针到达链表尾部的时候,慢指针正好到了倒数k个元素的位置。
      特点:双指针的初始位置、快慢。
  • 答案

class Solution {
public:
    ListNode* FindKthToTail(ListNode* pHead, int k) {
        ListNode* fast = pHead; 
        ListNode* slow = pHead;
        //快指针先行k步
        for(int i = 0; i < k; i++){  
            if(fast != NULL)
                fast = fast->next;
            //达不到k步说明链表过短,没有倒数k
            else
                return slow = NULL;
        }
        //快慢指针同步,快指针先到底,慢指针指向倒数第k个
        while(fast != NULL){ 
            fast = fast->next;
            slow = slow->next;
        }
        return slow;
    }
};

第7题:复杂链表的复制

  • 题目描述
    输入一个复杂链表(每个结点有结点值,以及两个指针,一个指向下一个结点,另一个特殊指针random指向一个随机结点),请对此链表进行深拷贝。(注意:输出结果中请不要返回参数中的结点引用,否则判题程序会直接返回空)。下图是一个含有5个结点的复杂链表。图中实线箭头表示next指针,虚线箭头表示random指针。为简单起见,指向null的指针没有画出。
  • 思路
    组合链表(双指针)
    正常链表的复制,从头到尾遍历链表,对于每个结点创建新的结点,赋值,并将其连接好就可以了。这道题的不同之处在于我们还要将随机指针连接好,我们创建结点的时候,有可能这个结点创建了,但是它的随机指针指向的结点没有创建,因此创建的时候只能连接指向后面的指针,无法连接随机指针。
    等链表连接好了,再连接随机指针的话,我们又难以找到这个指针指向的位置,因为链表不支持随机访问。但是吧,我们待拷贝的链表可以随机指针访问节点,那么我们不如将拷贝后的每个结点插入到原始链表相应结点之后,这样连接random指针的时候,原始链表random指针后一个元素就是原始链表要找的随机节点,而该节点后一个就是它拷贝出来的新节点,则就可以连上了。
    • step1:遍历链表,对每个结点新建一个拷贝结点,并插入到该节点之后。
    • step2:使用双指针再次遍历链表,两个指针每次移动两步,一个指针遍历原始节点,一个指针遍历拷贝节点,拷贝节点的随机指针跟随原始节点,指向原始节点随机指针的下一位。
    • step3:再次使用双指针遍历链表,每次越过一位后相连,即拆分成两个链表。
  • 答案
public class Solution {
    public RandomListNode Clone(RandomListNode pHead) {
        //空节点直接返回
        if(pHead == null)
            return pHead;
        //添加一个头部节点
        RandomListNode cur = pHead;
        //遍历原始链表,开始复制
        while(cur != null){
            //拷贝节点
            RandomListNode clone = new RandomListNode(cur.label);
            //将新节点插入到被拷贝的节点后
            clone.next = cur.next;
            cur.next = clone;
            cur = clone.next;
        }
        cur = pHead;
        RandomListNode clone = pHead.next;
        RandomListNode res = pHead.next;
        //连接新链表的random节点
        while(cur != null){
            //跟随前一个连接random
            if(cur.random == null)
                clone.random = null;
            else
                //后一个节点才是拷贝的
                clone.random = cur.random.next;
            //cur.next必定不为空
            cur = cur.next.next;
            //检查末尾节点
            if(clone.next != null)
                clone = clone.next.next;
        }
        cur = pHead;
        clone = pHead.next;
        //拆分两个链表
        while(cur != null){
            //cur.next必定不为空
            cur.next = cur.next.next;
            cur = cur.next;
            //检查末尾节点
            if(clone.next != null)
                clone.next = clone.next.next;
            clone = clone.next;
        }
        return res;
    }
}

第8题:删除链表中重复的结点

描述

在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表 1->2->3->3->4->4->5 处理后为 1->2->5
TC: O(n)
SC: O(n)

思路

直接比较删除(推荐使用)

这是一个升序列表,重复的节点都连在一起,我们就可以很轻易地比较到重复的节点,然后将所有的连续相同的节点都跳过,连接不相同的第一个节点。
step1: 给链表前加上表头,方便可能的话删除第一个节点
step2: 遍历链表,每次比较相邻两个节点,如果遇到了两个相邻节点相同,则新开内循环将这一段所有的相同都遍历过去
step3: 在step2中这一连串相同的节点直接脸上后续第一个不相同值的节点。
step4:返回时去掉添加的表头

答案

class Solution {
public:
    ListNode* deleteDuplication(ListNode* pHead) {
        //空链表
        if(pHead == NULL) 
            return NULL;
        ListNode* res = new ListNode(0);
        //在链表前加一个表头
        res->next = pHead; 
        ListNode* cur = res;
        while(cur->next != NULL && cur->next->next != NULL){ 
            //遇到相邻两个节点值相同
            if(cur->next->val == cur->next->next->val){ 
                int temp = cur->next->val;
                //将所有相同的都跳过
                while (cur->next != NULL && cur->next->val == temp) 
                    cur->next = cur->next->next;
            }
            else 
                cur = cur->next;
        }
        //返回时去掉表头
        return res->next; 
    }
};

哈希表

这道题幸运的是链表有序,我们可以直接与旁边的元素比较,然后删除重复。那我们扩展一点,万一遇到的链表无序呢?我们这里给出一种通用的解法,有序无序都可以使用,即利用哈希表来统计是否重复。
step1: 遍历一次链表,用哈希表记录每个节点值出现的次数。
step2: 在链表前加一个节点值为0的表头,方便可能的话删除表头元素。
step3: 再次遍历该链表,对于每个节点值检查哈希表中的计数,只留下计数为1的,其他情况都删除
step4: 返回时去掉增加的表头。

答案

class Solution {
public:
    ListNode* deleteDuplication(ListNode* pHead) {
        //空链表
        if(pHead == NULL) 
            return NULL;
        unordered_map<int, int> mp;
        ListNode* cur = pHead;
        //遍历链表统计每个节点值出现的次数
        while(cur != NULL){ 
            mp[cur->val]++;
            cur = cur->next;
        }
        ListNode* res = new ListNode(0);
        //在链表前加一个表头
        res->next = pHead; 
        cur = res;
        //再次遍历链表
        while(cur->next != NULL){ 
            //如果节点值计数不为1
            if(mp[cur->next->val] != 1) 
                //删去该节点
                cur->next = cur->next->next; 
            else
                cur = cur->next; 
        }
        //去掉表头
        return res->next; 
    }
};

第9题 删除链表的节点

描述

给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。返回删除后的链表的头节点。
1.此题对比原题有改动
2.题目保证链表中节点的值互不相同
3.该题只会输出返回的链表和结果做对比,所以若使用 C 或 C++ 语言,你不需要 free 或 delete 被删除的节点

思路

迭代遍历(推荐使用)

既然是整个链表元素都不相同,我们要删除给定的一个元素,那我们首先肯定要找到这个元素,然后考虑删除它。
删除一个链表节点,肯定是断掉它的前一个节点指向它的指针,然后指向它的后一个节点,即越过了需要删除的这个节点。
step 1: 首先我们加入一个头部节点,方便于如果可能的话删除掉第一个元素
step 2: 准备两个指针遍历链表,一个指针指向当前要遍历的元素,另一个指针指向该元素的前序节点,便于获取它的指针。
step 3: 遍历链表。找到目标节点,则断开连接,指向后一个。
step 4: 返回时去掉我们加入的头节点。

答案

class Solution {
public:
    ListNode* deleteNode(ListNode* head, int val) {
        //加入一个头节点
        ListNode* res = new ListNode(0);
        res->next = head;
        //前序节点
        ListNode* pre = res;
        //当前节点
        ListNode* cur = head;
        //遍历链表
        while(cur != NULL){
            //找到目标节点
            if(cur->val == val){
                //断开连接
                pre->next = cur->next;
                break;
            }
            pre = cur;
            cur = cur->next;
        }
        //返回去掉头节点
        return res->next;
    }
};
posted @ 2023-07-18 15:13  智子lock  阅读(6)  评论(0编辑  收藏  举报