剑指Offer——链表

链表

链表是一种动态的数据结构,当需要插入一个节点的时候,我们只需要为新创建的节点分配内存空间,将当前节点的next指向新创建的节点,并没有闲置的内存空间。

例题解答

本文中所有的链表的定义如下:

struct ListNode{
    int val;
    ListNode *next;
    ListNode(int v):val(v),next(nullptr){}
};
从尾到头打印链表

题目:输入一个链表的头节点,从尾到头反过来打印每个节点的值

  • 首先,逆序很容易想到栈这个数据结构,在遍历链表的时候,把每个节点压入栈中,打印时从栈中弹出来,就能逆着打印
  • 其次,可以将值保存在vector中,调用reverse()函数即可逆序打印结果

代码如下

void printReverseList(ListNode *root){
    ListNode *p = root;
    stack<ListNode*>nodes;
    while(p != nullptr){
        nodes.push(p);
        p = p ->next;
    }
    while(!nodes.empty()){
        ListNode *t = nodes.top();
        printf("%d\t",t->val);
        nodes.pop();
    }
}
删除链表中的节点

在O(1)的时间内删除链表的某个节点
给定单向链表的头指针和一个节点的指针,定义一个函数在O(1)的时间内删除该节点

  • 通常,删除链表中的节点需要获取该节点的前一个节点,然而获取这个前一个节点需要遍历整个链表,时间为O(n)
  • 此方法中可以,将下一个节点复制到该节点中,再删除下一个节点,即可完成删除操作。
    这里写图片描述
    代码如下
void deleteNode(ListNode **root,ListNode *del_node){
//删除的节点不是尾节点
    if(del_node->next != nullptr){
        ListNode *p_next = del_node->next;
        del_node->val = p_next->val;
        del_node->next = p_next->next;
        delete p_next;
    }
    //链表中只含有唯一一个节点
    else if (*root == del_node){
        delete del_node;
    }
    //删除链表中的尾节点
    else{
        ListNode *p = *root;
        while(p->next != del_node){
            p = p->next;
        }
        p->next = nullptr;
        delete del_node;
    }
}
链表中倒数第K个节点

输入一个链表,输出该链表中倒数第K个节点。

  • 同样,常规的方法首先遍历链表,获取链表中节点的个数,计算下一次遍历时需要计算的次数。时间复杂度为O(n)
  • O(1)的做法为设置2个指针,第一个指针向前走k-1步后,第二个指针不动。接着让第一个指针指向末尾时,第二个指针便指向倒数第k个节点

代码如下:

int findKNode(ListNode *root,int k) {
    ListNode *p_node = root;
    ListNode *p_sec = root;
    for(int i = 0;i < k-1;i++){
        p_node = p_node->next;
    }
    while(p_node->next != nullptr) {
        p_node = p_node->next;
        p_sec = p_sec->next;
    }
    return p_sec->val;
}
链表中环的入口节点

题目:如果一个链表中包含环,如何找出环的入口节点?

  1. 首先,要判断链表中是否有环,可以设置快、慢两个指针,如果他们相遇了则说明链表中存在环
  2. 要找到入口节点可以新建一个指针,同时慢指针继续以一次一步的速度继续遍历,相遇节点即为环的入口节点,证明如下:

这里写图片描述

假设快慢节点相遇在c点,慢节点在这个过程中移动的距离为s,链表长度为L,环的长度为r,则快节点移动距离为a+n*r,而快节点的步长为慢节点的2倍:
a+n*r = 2s —> a+x = s
a+x = n*r —> a+x = (n-1)*r + r —>a+x = (n-1)*r + (L-a)
a = (n-1)*r + (L-a-x)
因此节点从h->d与节点从c->d会在d点相遇
因此环形链表可以分解成三个问题
1. 给定一个链表,判断链表中是否有环
2. 计算环的大小
3. 寻找环的入口

判断链表中是否有环

要点:设置快慢两个指针,慢指针一次走一步,快指针一次走两步,当他们相遇时,则说明链表中有环
代码如下;

 bool hasCycle(ListNode *head) {
    ListNode *slow,*fast;
    if(head == nullptr)
        return false;
    slow = head->next;
    if(slow == nullptr)
        return false;
    fast = slow->next;
    if(fast == nullptr)
        return false;
    while(slow != nullptr && fast != nullptr) {
        if(fast == slow)
            return true;
        slow = slow ->next;
        fast = fast->next;
        if(fast != nullptr)
            fast = fast->next;
        else
            return false;
    }
    return false;
}
计算环的大小

在相遇的节点后,慢节点遍历一圈后回到当前节点,判断节点相同即可

寻找环的入口节点

需要第一个问题中的相遇的节点作为参数,代码如下

ListNode *detectCycle(ListNode *head) {
    ListNode *circle_head = hasCycle(head);
    if(circle_head == nullptr)
        return nullptr;
    ListNode *p = head;
    while(p != circle_head) {
        p = p->next;
        circle_head = circle_head->next;
    }
    return p;
}
反转链表

题目:定义一个函数,输入一个链表的头节点,反转该链表并输入反转后的链表的头结点

为了防止链表的断裂,需要使用三个指针,表示当前节点、前一个节点、下一个节点。而反转之后的链表的头节点是反转之前next为null的节点。
代码如下:

ListNode* reverseList(ListNode *root) {
    ListNode *p_reverse_head = nullptr;
    ListNode *p_cur = root;
    ListNode *p_pre = nullptr;
    while(p_cur != nullptr) {
        ListNode *p_next = p_cur->next;
        if(p_next == nullptr)
            p_reverse_head = p_cur;
        p_cur -> next = p_pre;
        p_pre = p_cur;
        p_cur = p_next;
    }
    return p_reverse_head;
}
(LeetCode21) 合并两个有序链表

将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4

  • 使用遍历的办法,找出2个链表中值较小的那个节点链接到新的表头中
  • 使用递归的办法

代码如下:

ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
    if(l1 == nullptr) 
        return l2;
    else if(l2 == nullptr)
        return l1;
    ListNode *mergeHead = nullptr;
    if(l1->val > l2->val) {
        mergeHead = l2;
        mergeHead->next = mergeTwoLists(l1,l2->next);
    }else {
        mergeHead = l1;
        mergeHead->next = mergeTwoLists(l1->next,l2);
    }
    return mergeHead;  
}
(LeetCode23)合并K个有序链表

合并 k 个排序链表,返回合并后的排序链表

  • 可以每次两两排序,将排序后的链表重新加入到lists中去,并在lists中删除以及排序过的链表
    代码如下:
ListNode* mergeKLists(vector<ListNode*>& lists) {
    if(lists.empty())
        return nullptr;
    while(lists.size() > 1) {
        lists.push_back(mergeTwoLists(lists[0],lists[1]));
        lists.erase(lists.begin());
        lists.erase(lists.begin());
    }
    return lists.front();

}
复杂链表的复制
  • 为了实现O(1)的复杂度,先将链表在原链表中复制一遍
  • 在新的节点上链接siblingNodes
  • 断开链接,选择偶数位置的节点

代码如下:

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

void copyNodes(ListNode *root) {
    ListNode *head = root;
    while(head != nullptr) {
        ListNode *temp = new ListNode();
        temp->val = head->val;
        temp->next = head->next;
        head->next = temp;
        head = temp->next;
    }
}

void connectSiblingNodes(ListNode *root) {
    ListNode *head = root;
    while(head != nullptr) {
        ListNode *cloned = head->next;
        if(head->sibling != nullptr){
            cloned->sibling = head->sibling->next;
        }
        head = cloned->next;
    }
}

ListNode* reconnectNodes(ListNode *root){
    ListNode *node = root;
    ListNode *cloned_head = nullptr,*cloned_node = nullptr;
    if(node != nullptr) {
        cloned_head = cloned_node = node->next;
        node ->next = cloned_node ->next;
        node = node->next;
    }
    while(node != nullptr) {
        cloned_node->next = node ->next;
        cloned_node = cloned_node ->next;
        node->next = cloned_node ->next;
        node = node->next;

    }
    return cloned_head;
}

ListNode* clone(ListNode *head){
    copyNodes(head);
    connectSiblingNodes(head);
    ListNode *root = reconnectNodes(head);
    return root;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值