链表刷题的常规题型

链表

优点:能灵活的分配内存空间,能在O(1)时间内删除或添加元素
缺点:查询元素需要O(n)时间
适用场景:数据元素个数不确定,经常进行数据的添加或删除

在leetcode中默认的链表结构是

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

在链表中比较常见的解题方法有:
1.对于求链表长度,或者判断链表是否有环等问题,可以使用快慢指针进行结局
2.对于需要插入或者删除的操作,建一个哑(哨兵)指针指向head节点会使对所有链表的操作统一起来,并且通常需要创建一个临时指针来记录前面被指向的节点
3.对一些依赖于后面节点才可以完成的操作,可以使用递归的方式来解决

1.链表反转
使用迭代法的思路需要构建三个指针,分别是当前节点的前驱pre,当前节点cur,和当前节点的后继next
在这里插入图片描述

在循环遍历的过程中,然后让cur指向pre ,再把cur和pre的位置往后移一位即可

在这里插入图片描述

    ListNode* pre = nullptr;//刚开始当前节点的上一个为空
    ListNode* cur = head; //当前节点指向head
    while (cur) {
        ListNode* next = cur->next;  //当前节点的下一个
        cur->next = pre;             //把当前节点指向当前节点上一个
        pre = cur;                   //向后移动
        cur = next;
    }
    return pre;

时间复杂度为O(n),空间复杂度为O(1);

使用递归法的思路是一直让链表递归到最后一个节点记为newHead然后返回
在这里插入图片描述

在返回过程中,让当前节点的下一个指向当前节点
在这里插入图片描述

并且把当前节点指向空
在这里插入图片描述

等到递归函数全部出栈后,递归完成

ListNode* reverseList(ListNode* head) {
    while (!head || !head->next) {
        return head;
    }
    ListNode* newHead = reverseList(head->next);
    head->next->next = head;
    head->next = nullptr;
    return newHead;
}

时间复杂度为O(n),空间复杂度为O(n)

2.合并有序链表
给定两个增序的链表,合成一个增序的链表

递归法:

ListNode* mergeTwoLists(ListNode* l1,ListNode* l2) {
    if (!l1) return l2;
    if (!l2) return l1;
    if (l1->val > l2->val) {
        l2->next = mergeTwoLists(l1, l2->next);
        return l2;
    }
    else {
        l1->next = mergeTwoLists(l1->next, l2);
        return l1;
    }
}

时间复杂度为O(n),空间复杂度为O(n)

迭代法:

ListNode* mergeTwoLists(ListNode* l1,ListNode* l2) {
    ListNode* dummy = new ListNode(0);//创建一个头节点
    ListNode* cur = dummy;
    while (l1 && l2) {
        if (l1->val > l2->val) {
            cur->next = l2;
            l2 = l2->next;
        }
        else {
            cur->next = l1;
            l1 = l1->next;
        }
        cur = cur->next;
    }
    cur->next = l1 ? l1 : l2;//循环结束的时候可能l1或l2有一个并不为空,所以需要把后续的部分也接上
    return dummy->next;
}

时间复杂度为O(n),空间复杂度为O(1)

3.交换相邻节点
给定一个链表,交换每个相邻的一对节点

迭代法:
首先创建一个虚拟节点,让虚拟节点dummy指向head

让node指向node2
在这里插入图片描述
node1指向node2的next

在这里插入图片描述
让node2指向node1
在这里插入图片描述
也就是这样
在这里插入图片描述
最后把node移动到node1的位置即可
在这里插入图片描述

ListNode* swapPairs(ListNode* head) {
    ListNode* dummy = new ListNode(0);
    dummy->next = head;
    ListNode* node = dummy;
    while (node->next != nullptr && node->next->next != nullptr) {
        ListNode* node1 = node->next;
        ListNode* node2 = node->next->next;
        node->next = node2;
        node1->next = node2->next;
        node2->next = node1;
        node = node1;
    }
    return dummy->next;
}

时间复杂度是O(n),空间复杂度是O(1)

递归法:
递归的终止条件是链表中没有节点或者只有一个节点,在这种情况下就不必进行交换了。首先设置head节点的next为newHead,让head->next去继续递归,让newHead的next指向head即可

ListNode* swapPairs(ListNode* head) {
    while (head == nullptr || head->next == nullptr) {
        return head;
    }

    ListNode* newHead = head->next;
    head->next = swapPairs(newHead->next);
    newHead->next = head;

    return newHead;
}

时间复杂度是O(n),空间复杂度是O(n)

4.链表相交
给定两个链表,判断它们是否相交于一点,并求这个相交节点。

思路:对于两个链表A,长度为m,链表B,长度为n来说
如果链表相交的话,并且m=n的话,那么遍历A和B的过程中会同时到达A和B相交的节点,此时A=B,返回该节点即可
如果链表相交,并且m!=n的话,那么pA指针先走一遍A再走一边B,同时Pb指针先走一边B再走一遍A,在过程中两个指针会指向他们的相交的节点,此时返回该节点即可。

如果链表不相交的话并且m=n的话,那么两个指针同时到达两个链表的尾节点,此时返回空
如果链表不相交且m!=n的话,pA会在移动m+n次,pB会在移动n+m次以后分别到达链表B和链表A的尾节点,此时返回空

ps:错的人就算走对方的路也还是会错

ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) {
    if (headA == nullptr && headB == nullptr) {
        return nullptr;
    }

    ListNode* pA = headA, * pB = headB;

    while (pA != pB) {  
        pA = pA == nullptr ? headB : pA->next;
        pB = pB == nullptr ? headA : pB->next;
    }
    return pA;
}

时间复杂度O(m+n),空间复杂度O(1)

5.删除排序链表重复元素
解法:先定义一个node指针指向head,遍历链表,如果两个节点相同,那么让node的next指向node的next的next,否则,node往后继续走

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

时间复杂度O(n),空间复杂度O(1)

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

ListNode* oddEvenList(ListNode* head) {
    if (head == nullptr || head->next == nullptr) {//head为空或者链表只有一个结点的话直接返回
        return head;
    }
    ListNode* p1 = head;
    ListNode* p2 = head->next;
    ListNode* head1 = head->next;//用来保存偶数节点的开头
    while (p2 && p2->next) {
        p1->next = p2->next;
        p1 = p1->next;
        p2->next = p1->next;
        p2 = p2->next;
    }
    p1->next = head1;//把奇数节点的结尾接到偶数节点的开头上
    return head;
}

时间复杂度O(n),空间复杂度O(1)

7.删除链表第n个节点
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点

思路:使用快慢指针,首先让fast指针先走n步,然后和slow指针同时走,直到fast指针走到尾节点位置,此时slow指针就会在要被删除的位置上。但是如果要删除指针的话还是最好能使slow节点移动到要被删除指针的前驱节点删除操作,所以本解法使用了哑指针dummy,这样做的好处一是可以移动n的时候直接移动到被删除节点的前驱节点,好处二是在链表只有一个节点,并要删除一个节点的情况下,可以使头节点能有一个哑节点作为前驱节点,从而方便删除操作

ListNode* removeNthFromEnd(ListNode* head, int n) {
    ListNode* dummy = new ListNode(0);//创建哑节点
    dummy->next = head;//哑节点作为head的前驱节点
    ListNode* fast = dummy;
    ListNode* slow = dummy;

    while (n > 0) {
        fast = fast->next;
        n--;
    }

    while (fast->next != nullptr) {//slow就是要被删除节点的前驱节点
        fast = fast->next;
        slow = slow->next;
    }

    slow->next = slow->next->next;//删除操作
    return dummy->next;
}

时间复杂度O(n),空间复杂度O(1)

8.回文链表
给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false

解法:使用快慢指针确定链表的中点,然后反转链表的后半段,然后对头尾遍历比较

ListNode* mid(ListNode* head) { //快慢指针
        ListNode* fast = head;
        ListNode* slow = head;
        while (fast->next != nullptr && fast->next->next != nullptr) {
            fast = fast->next->next;
            slow = slow->next;
        }
        return slow;
    }

    ListNode* reverse(ListNode* head) { //反转链表
        ListNode* pre = nullptr;
        ListNode* cur = head;
        while (cur) {
            ListNode* next = cur->next;
            cur->next = pre;
            pre = cur;
            cur = next;
        }
        return pre;
    }

    bool isSame(ListNode* l1, ListNode* l2) {
        while(l2){ //可能存在链表长度为1的情况下l2为空
            if(l1 -> val != l2 -> val){
                return false;
            }
            l1 = l1->next;
            l2 = l2->next;
        }
        return true;
    }

    bool isPalindrome(ListNode* head) {
        if(head == nullptr || head->next == nullptr) return true;
        ListNode* pMid = mid(head);
        ListNode* endHead = reverse(pMid->next);
        return isSame(head, endHead);
    }

时间复杂度O(n),空间复杂度O(1)

9.合并K个有序链表

解法:可以使用之前实现的合并两个有序链表的函数, 逐一合并两条链表

ListNode* mergeKLists(vector<ListNode*>& lists) {
    int n = lists.size();
    ListNode* dummy = nullptr;
    for (int i = 0; i < n; ++i) {
        dummy = mergeTwoLists(dummy, lists[i]);
    }
    return dummy;
}
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
    ListNode* dummy = new ListNode(0);//创建一个头节点
    ListNode* cur = dummy;
    while (l1 && l2) {
        if (l1->val > l2->val) {
            cur->next = l2;
            l2 = l2->next;
        }
        else {
            cur->next = l1;
            l1 = l1->next;
        }
        cur = cur->next;
    }
    cur->next = l1 ? l1 : l2;//循环结束的时候可能l1或l2有一个并不为空,所以需要把后续的部分也接上
    return dummy->next;
}

时间复杂度:假设链表最长的长度是n,第一次合并的时间复杂度是n,第二次合并的时间复杂度是2n,所以第K次合并的时间复杂度是kn,根据等差数列求和公式n(a1+an)/2,K次合并的总时间复杂度为O(k(n+kn)/2) = O(k²n)
空间复杂度:没有使用任何辅助空间,所以空间复杂度是O(1)

10.判断链表是否成环
解法:快慢指针,如果有环则快指针会追上慢指针,如果没有环,则快指针会指向空

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

时间复杂度O(n),空间复杂度O(1)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值