Leetcode热题Hot100-链表题综合

61-旋转链表

给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。
在这里插入图片描述

闭合环

把当前链表转换为闭合环,假设链表长度为n,旋转链表后,链表的末尾在n-1-(k mod n)处,索引从0开始。在此处将链表断开,即可实现链表的旋转。

class Solution {
public:
    ListNode* rotateRight(ListNode* head, int k) {
        if (k == 0 || head == nullptr || head->next == nullptr) {
            return head;
        }
        int n = 1;
        ListNode* iter = head;
        while (iter->next != nullptr) {
            iter = iter->next;
            n++;
        }
        int add = n - k % n;
        if (add == n) {
            return head;
        }
        iter->next = head;
        while (add--) {
            iter = iter->next;
        }
        ListNode* ret = iter->next;
        iter->next = nullptr;
        return ret;
    }
};
class Solution:
    def rotateRight(self, head: ListNode, k: int) -> ListNode:
        if k == 0 or not head or not head.next:
            return head
        
        n = 1
        cur = head
        while cur.next:
            cur = cur.next
            n += 1
        
        if (add := n - k % n) == n:
            return head
        
        cur.next = head
        while add:
            cur = cur.next
            add -= 1
        
        ret = cur.next
        cur.next = None
        return ret
  1. 时间复杂度:O(n),最坏情况下要遍历两次
  2. 空间复杂度:O(1)

24-两两交换链表中的节点

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
在这里插入图片描述

方法一:递归

可以通过递归的方式实现两两交换链表中的节点
递归的终止条件是链表中没有节点,或者链表中只有一个节点,此时无法进行交换。

class Solution:
    def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
        if head is None or head.next is None:
            return head
        one =head       //交换的第一个节点
        two=head.next  //交换的第二个节点
        three=two.next    //交换的第一个节点的next节点
        two.next=one
        one.next=self.swapPairs(three)
        return two
  1. 时间复杂度:O(n),其中n是链表的节点数量,需要对每个节点进行更新指针的操作
  2. 空间复杂度:O(n),其中n是链表的节点数量,空间复杂度主要取决于递归调用的栈空间。

方法二:迭代

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        if(!head)
            return nullptr;
        ListNode *pre=new ListNode(0,head);
        ListNode*node=pre;
        while(pre->next!=nullptr&&pre->next->next!=nullptr)
        {
            ListNode*one=pre->next;
            ListNode*two=pre->next->next;
            ListNode*three=nullptr;
            three=two->next;
            pre->next=two;
            two->next=one;
            one->next=three;
            pre=one;
        }
        return node->next;
    }
};

2-两数相加

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

在这里插入图片描述
提示:

  1. 每个链表的节点数在范围[1,100]内
  2. 0<=Node.val<=9
  3. 题目数据保证列表表示的数字不含前导零

解题思路

由于输入的两个链表都是逆序存储数字的位数的,因此两个链表中同一位置的数字可以直接相加。

我们同时遍历两个链表,逐位计算它们的和,并与当前位置的进位值相加。具体而言,如果当前两个链表处相应位置的数字为 n1,n2,进位值为carry,则它们的和为 n1+n2+carry;其中,答案链表处相应位置的数字为 (n1+n2+carry)%10,而新的进位值为 (n1+n2+carry)/10.
如果两个链表的长度不同,则可以认为长度短的链表的后面有若干个 0。
此外,如果链表遍历结束后,有 carry>0,还需要在答案链表的后面附加一个节点,节点的值为 carry。

C++实现

    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode*p1=l1;  //防止原链表l1被破坏
        ListNode*p2=l2;	 //防止原链表l2被破坏
        ListNode *dummy=new ListNode(-1);    // 虚节点
        ListNode *p=dummy;   // 保存虚节点,用于不断迭代
        int carry=0 ,newval=0;   // 保存进位值和新值 
        while(p1!=nullptr||p2!=nullptr)
        {
            newval=(p1==nullptr?0:p1->val)+(p2==nullptr?0:p2->val)+carry;
            carry=newval/10;  
            newval%=10;
            p->next=new ListNode(newval);
            p1=p1==nullptr?nullptr:p1->next;  // 移动链表的指针
            p2=p2==nullptr?nullptr:p2->next;
            p=p->next;
        }
        // 遍历结束之后,如果carry>0,仍需进一位,创建一个新的节点
        if(carry>0)
        {
            p->next=new ListNode(carry);
            
        }
        return dummy->next;
            
    }

复杂度分析

  1. 时间复杂度:O(max(m,n)),其中 m 和 n 分别为两个链表的长度。我们要遍历两个链表的全部位置,而处理每个位置只需要 O(1) 的时间。
  2. 空间复杂度:O(1)。注意返回值不计入空间复杂度。

19-删除链表的倒数第N个节点

给定一个链表,删除链表的倒数第n个节点,并且返回链表的头节点。
在这里插入图片描述

链表中节点的数目为sz
1<=sz<=30
0<=Node.val<=100
1<=n<=sz

解题思路

由于我们需要找到倒数第 n 个节点,因此我们可以使用两个指针first 和 second 同时对链表进行遍历,并且 first 比 second 超前 n 个节点。当 first 遍历到链表的末尾时,second 就恰好处于倒数第 n 个节点。

具体地,初始时first 和 second 均指向头节点。我们首先使用 first 对链表进行遍历,遍历的次数为 n。此时,first 和 second 之间间隔了 n-1 个节点,即 first 比 second 超前了 n 个节点。

在这之后,我们同时使用first 和 second 对链表进行遍历。当 first 遍历到链表的末尾(即 first 为空指针)时,second 恰好指向倒数第 n个节点。

如果我们能够得到的是倒数第 n 个节点的前驱节点而不是倒数第 n 个节点的话,删除操作会更加方便。因此我们可以考虑在初始时将 second 指向哑节点,其余的操作步骤不变。这样一来,当 first 遍历到链表的末尾时,second 的下一个节点就是我们需要删除的节点。

C++实现

/**
 1. Definition for singly-linked list.
 2. struct ListNode {
 3.     int val;
 4.     ListNode *next;
 5.     ListNode() : val(0), next(nullptr) {}
 6.     ListNode(int x) : val(x), next(nullptr) {}
 7.     ListNode(int x, ListNode *next) : val(x), next(next) {}
 8. };
 */
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummy = new ListNode(0, head);//哑节点,对于只有一个节点的链表有用
        ListNode* first = head;
        ListNode* second = dummy;
        for (int i = 0; i < n; ++i) {  //比n-1的时间快50%
            first = first->next;
        }
        while (first) {
            first = first->next;
            second = second->next;
        }
        second->next = second->next->next;
        ListNode* ans = dummy->next;
        delete dummy;
        return ans;
    }
};

复杂度分析

1.时间复杂度:O(L),其中L是链表的长度
2.空间复杂度:O(1)
class Solution:
    def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
        dummy = ListNode(0, head)
        first = head
        second = dummy
        for i in range(n):
            first = first.next

        while first:
            first = first.next
            second = second.next
        
        second.next = second.next.next
        return dummy.next

23-合并K个升序链表

给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。

在这里插入图片描述
提示:

  1. k = lists.length
  2. 0 <= k <= 10^4
  3. 0 <= lists[i].length <= 500
  4. -10^4 <= lists[i][j] <= 10^4
  5. lists[i] 按 升序 排列
  6. lists[i].length 的总和不超过 10^4

解题思路

在解决「合并K个排序链表」这个问题之前,我们先来看一个更简单的问题:如何合并两个有序链表?假设链表 a 和 b的长度都是 n,如何在 O(n)的时间代价以及 O(1) 的空间代价完成合并? 这个问题在面试中常常出现,为了达到空间代价是O(1),我们的宗旨是「原地调整链表元素的 next 指针完成合并」。以下是合并的步骤和注意事项,对这个问题比较熟悉的读者可以跳过这一部分。此部分建议结合代码阅读。

首先我们需要一个变量 head 来保存合并之后链表的头部,你可以把 head 设置为一个虚拟的头(也就是 head 的 val 属性不保存任何值),这是为了方便代码的书写,在整个链表合并完之后,返回它的下一位置即可。
我们需要一个指针 tail 来记录下一个插入位置的前一个位置,以及两个指针 aPtr 和 bPtr 来记录 a 和 b 未合并部分的第一位。注意这里的描述,tail 不是下一个插入的位置,aPtr 和 bPtr 所指向的元素处于「待合并」的状态,也就是说它们还没有合并入最终的链表。 当然你也可以给他们赋予其他的定义,但是定义不同实现就会不同。
当 aPtr 和 bPtr 都不为空的时候,取 val 熟悉较小的合并;如果 aPtr 为空,则把整个 bPtr 以及后面的元素全部合并;bPtr 为空时同理。
在合并的时候,应该先调整 tail 的 next 属性,再后移 tail 和 *Ptr(aPtr 或者 bPtr)。那么这里 tail 和 *Ptr 是否存在先后顺序呢?它们谁先动谁后动都是一样的,不会改变任何元素的 next 指针。
要实现K个升序链表的实现,在合并两个有序链表的基础上,我们可以想到一种最朴素的方法:用一个变量 ans 来维护以及合并的链表,第 i 次循环把第 i 个链表和 ans 合并,答案保存到 ans 中。

方法一:顺序合并

/**
 * Definition for singly-linked list.
 * 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) {}
 * };
 */
class Solution {
public:
	//&Lists取的是引用
    ListNode* mergeKLists(vector<ListNode*>& Lists) {
        ListNode *ans = nullptr;
		for (size_t i = 0; i < Lists.size(); ++i)
			ans = mergeTwoLists(ans, Lists[i]);
		return ans;
    }
    ListNode* mergeTwoLists(ListNode *a, ListNode *b) {
		if ((!a) || (!b)) return a ? a : b;// a空或b空,赋给a或b
		ListNode head;    //虚头节点 ,也可以写成ListNode* head=new ListNode(0)
		ListNode*tail =& head, *aPtr = a, *bPtr = b; //&head为取地址
		while (aPtr && bPtr) {
			if (aPtr->val < bPtr->val) {
				tail->next = aPtr; aPtr = aPtr->next;
			}
			else {
				tail->next = bPtr; bPtr = bPtr->next;
			}
			tail = tail->next;
		}
		tail->next = (aPtr ? aPtr : bPtr);
		return head.next;
	}
};
1.时间复杂度:O(k^2*n)
2.空间复杂度:没有用到与 k 和 n 规模相关的辅助空间,故渐进空间复杂度为 O(1)。

方法二:分治合并

class Solution {
public:
    ListNode* mergeTwoLists(ListNode *a, ListNode *b) {
        if ((!a) || (!b)) return a ? a : b;
        ListNode head, *tail = &head, *aPtr = a, *bPtr = b;
        while (aPtr && bPtr) {
            if (aPtr->val < bPtr->val) {
                tail->next = aPtr; aPtr = aPtr->next;
            } else {
                tail->next = bPtr; bPtr = bPtr->next;
            }
            tail = tail->next;
        }
        tail->next = (aPtr ? aPtr : bPtr);
        return head.next;
    }

    ListNode* merge(vector <ListNode*> &lists, int l, int r) {
        if (l == r) return lists[l];
        if (l > r) return nullptr;
        int mid = (l + r) >> 1;
        return mergeTwoLists(merge(lists, l, mid), merge(lists, mid + 1, r));
    }

    ListNode* mergeKLists(vector<ListNode*>& lists) {
        return merge(lists, 0, lists.size() - 1);
    }
};
  1. 时间复杂度:O(Kn×logK),分治合并总时间复杂度为O(logK),每个合并需要的平均时间复杂度为O(Kn)
  2. 空间复杂度:O(logK),递归会用到O(logK)的空间代价。

114-二叉树展开为链表

给你二叉树的根结点 root ,请你将它展开为一个单链表:
展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。
展开后的单链表应该与二叉树 先序遍历 顺序相同。

在这里插入图片描述
提示:

  1. 树中节点数在范围[0,2000]内
  2. -100<=Node.val<=100

解题思路

将二叉树展开为单链表之后,单链表中的节点顺序即为二叉树的前序遍历访问各节点的顺序。因此,可以对二叉树进行前序遍历,获得各节点被访问到的顺序。由于将二叉树展开为链表之后会破坏二叉树的结构,因此在前序遍历结束之后更新每个节点的左右子节点的信息,将二叉树展开为单链表。
前序遍历可以通过递归或者迭代的方式实现。

C++实现

#include<iostream>
#include<vector>
#include<stack>
using namespace std;

struct TreeNode {
     int val;
     TreeNode *left;
     TreeNode *right;
    TreeNode() : val(0), left(nullptr), right(nullptr) {}
      TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
      TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 };
class Solution {
public:
	//递归
    void flatten(TreeNode* root) {
        vector<TreeNode*> l;
        preorderTraversal(root, l);
        int n = l.size();
        for (int i = 1; i < n; i++) {
            TreeNode *prev = l.at(i - 1), *curr = l.at(i);
             prev->left = nullptr;
            prev->right = curr;
        }
    }

    void preorderTraversal(TreeNode* root, vector<TreeNode*> &l) {
        if (root != NULL) {
            l.push_back(root);
            preorderTraversal(root->left, l);
            preorderTraversal(root->right, l);
        }
    }
	//迭代
	void flatten1(TreeNode* root) {
        auto v = vector<TreeNode*>();
        auto stk = stack<TreeNode*>();
        TreeNode *node = root;
        while (node != nullptr || !stk.empty()) {
            while (node != nullptr) {
                v.push_back(node);
                stk.push(node);
                node = node->left;
            }
            node = stk.top(); stk.pop();
            node = node->right;
        }
        int size = v.size();
        for (int i = 1; i < size; i++) {
            auto prev = v.at(i - 1), curr = v.at(i);
            prev->left = nullptr;
            prev->right = curr;
        }
    }
};
//测试用例
int main()
{TreeNode *root =new TreeNode(1);
	root->left= new TreeNode(2);
	root->right = new TreeNode(3);
	root->left->left = new TreeNode(4);
	root->left->right = new TreeNode(5);
	Solution s;
	s.flatten1(root);

system("pause");
return 0;
}

复杂度分析

  1. 时间复杂度:O(n),其中 n 是二叉树的节点数。前序遍历的时间复杂度是 O(n),前序遍历之后,需要对每个节点更新左右子节点的信息,时间复杂度也是 O(n)。
  2. 空间复杂度:O(n),其中 n 是二叉树的节点数。空间复杂度取决于栈(递归调用栈或者迭代中显性使用的栈)和存储前序遍历结果的列表的大小,栈内的元素个数不会超过 n,前序遍历列表中的元素个数是 n。
# 递归
class Solution:
    def flatten(self, root: TreeNode) -> None:  #->表示什么都不返回
        preorderList = list()

        def preorderTraversal(root: TreeNode):
            if root:
                preorderList.append(root)
                preorderTraversal(root.left)
                preorderTraversal(root.right)
        
        preorderTraversal(root)
        size = len(preorderList)
        for i in range(1, size):
            prev, curr = preorderList[i - 1], preorderList[i]
            prev.left = None
            prev.right = curr
# 迭代
class Solution:
    def flatten(self, root: TreeNode) -> None:
        preorderList = list()
        stack = list()
        node = root

        while node or stack:
            while node:
                preorderList.append(node)
                stack.append(node)
                node = node.left
            node = stack.pop()
            node = node.right
        
        size = len(preorderList)
        for i in range(1, size):
            prev, curr = preorderList[i - 1], preorderList[i]
            prev.left = None
            prev.right = curr

141-环形链表

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
如果链表中存在环,则返回 true 。 否则,返回 false 。

在这里插入图片描述

解题思路

我们可以通过双指针来解决本题。具体地,我们定义两个指针,一快一满。慢指针每次只移动一步,而快指针每次移动两步。初始时,慢指针在位置 head,而快指针在位置 head.next。这样一来,如果在移动的过程中,快指针反过来追上慢指针,就说明该链表为环形链表。否则快指针将到达链表尾部,该链表不为环形链表。

方法一:快慢指针

struct ListNode {
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(nullptr) {}
};
 
class Solution {
public:
    bool hasCycle(ListNode *head) {
    //假设快慢指针前面有一个虚拟节点
        ListNode *pSlow=head;   
        ListNode*pFast=pSlow->next;
        while(pSlow&&pFast)
        {
            if(pFast==pSlow)  // 所以要快指针先走一步,避免进入这个条件
                return true;
            pSlow=pSlow->next;
            pFast=pFast->next;
            // 快指针每次多走一步
            if(pFast)
                pFast=pFast->next;
        }
        return false;
    }
};
class Solution:
    def hasCycle(self, head: ListNode) -> bool:
        if not head or not head.next:
            return False
        
        slow = head
        fast = head.next

        while slow != fast:
            if not fast or not fast.next:
                return False
            slow = slow.next
            fast = fast.next.next
        
        return True

复杂度分析

  1. 时间复杂度:O(N),其中 N 是链表中的节点数。
    当链表中不存在环时,快指针将先于慢指针到达链表尾部,链表中每个节点至多被访问两次。当链表中存在环时,每一轮移动后,快慢指针的距离将减小一。而初始距离为环的长度,因此至多移动 N 轮。
  2. 空间复杂度:O(1)。我们只使用了两个指针的额外空间。

方法二:哈希表

class Solution {
public:
    bool hasCycle(ListNode *head) {
        unordered_set<ListNode*> seen;
        while (head != nullptr) {
            if (seen.count(head)) {
                return true;
            }
            seen.insert(head);
            head = head->next;
        }
        return false;
    }
};
class Solution:
    def hasCycle(self, head: ListNode) -> bool:
        seen = set()
        while head:
            if head in seen:
                return True
            seen.add(head)
            head = head.next
        return False
  1. 时间复杂度:O(n)
  2. 空间复杂度:O(n)

146-LRU缓存机制

运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 。
实现 LRUCache 类:

  1. LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
  2. int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
  3. void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
    进阶:你是否了可以在O(1)时间复杂度内完成这两种操作?
    在这里插入图片描述

解题思路

哈希表 + 双向链表
LRU 缓存机制可以通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。

双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的。

哈希表即为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双向链表中的位置。

这样一来,我们首先使用哈希表进行定位,找出缓存项在双向链表中的位置,随后将其移动到双向链表的头部,即可在 O(1) 的时间内完成 get 或者 put 操作。具体的方法如下:

对于 get 操作,首先判断 key 是否存在:

如果 key 不存在,则返回 −1;

如果 key 存在,则 key 对应的节点是最近被使用的节点。通过哈希表定位到该节点在双向链表中的位置,并将其移动到双向链表的头部,最后返回该节点的值。

对于 put 操作,首先判断 key 是否存在:

如果 key 不存在,使用 key 和 value 创建一个新的节点,在双向链表的头部添加该节点,并将 key 和该节点添加进哈希表中。然后判断双向链表的节点数是否超出容量,如果超出容量,则删除双向链表的尾部节点,并删除哈希表中对应的项;

如果 key 存在,则与 get 操作类似,先通过哈希表定位,再将对应的节点的值更新为 value,并将该节点移到双向链表的头部。

上述各项操作中,访问哈希表的时间复杂度为 O(1),在双向链表的头部添加节点、在双向链表的尾部删除节点的复杂度也为 O(1)。而将一个节点移到双向链表的头部,可以分成「删除该节点」和「在双向链表的头部添加节点」两步操作,都可以在 O(1) 时间内完成。

小贴士

在双向链表的实现中,使用一个伪头部(dummy head)和伪尾部(dummy tail)标记界限,这样在添加节点和删除节点的时候就不需要检查相邻的节点是否存在。

C++实现

struct DLinkedNode
{
    int key,value;
    DLinkedNode*prev;
    DLinkedNode*next;
    DLinkedNode():key(0),value(0),prev(nullptr),next(nullptr){}
    DLinkedNode(int _key,int _value):key(_key),value(_value),prev(nullptr),next(nullptr){}
};

class LRUCache {
private:
    unordered_map<int,DLinkedNode*>cache;
    //头尾虚节点
    DLinkedNode*head;
    DLinkedNode*tail;
    //链表元素数
    int size;
    int capacity;
public:
    LRUCache(int _capacity): capacity(_capacity),size(0){
        head=new DLinkedNode();
        tail=new DLinkedNode();
        head->next=tail;
        tail->prev=head;

    }
    
    int get(int key) {
        if(!cache.count(key))
            return -1;
        DLinkedNode*node=cache[key];
        moveToHead(node);
        return node->value;
    }
    
    void put(int key, int value) {
        if(!cache.count(key))
        {
            DLinkedNode*node=new DLinkedNode(key,value);
            //添加进哈希表
            cache[key]=node; 
            addToHead(node);
            ++size;
            if(size>capacity)
            {
                DLinkedNode*removed=removeTail();
                cache.erase(removed->key);
                //防止内存泄露
                delete removed;
                --size;
            }
        }
        else
        {
            DLinkedNode*node=cache[key];
            node->value=value;
            moveToHead(node);
        }
        
    }
    void addToHead(DLinkedNode *node)
    {
        node->prev=head;
        node->next=head->next;
        head->next->prev=node;
        head->next=node;
    }
    void removedNode(DLinkedNode*node)
    {
        node->prev->next=node->next;
        node->next->prev=node->prev;
        delete node;
        node=nullptr;
    }
    void moveToHead(DLinkedNode*node)
    {
        removedNode(node);
        addToHead(node);
    }
    DLinkedNode*removeTail(){
        DLinkedNode*node=tail->prev;
        removedNode(node);
        return node;
    }
};

复杂度分析

  1. 时间复杂度:对于 put 和 get 都是 O(1)。
  2. 空间复杂度:O(capacity),因为哈希表和双向链表最多存储 capacity+1 个元素。

148-排序列表

给你链表的头节点 head ,请将其按 升序 排列并返回 排序后的链表 。
进阶:
你可以在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?

在这里插入图片描述

解题思路

这道题考虑时间复杂度更低的排序算法。题目的进阶问题要求达到O(nlogn) 的时间复杂度和O(1) 的空间复杂度,时间复杂度是 O(nlogn) 的排序算法包括归并排序、堆排序和快速排序(快速排序的最差时间复杂度是 O(n^2)),其中最适合链表的排序算法是归并排序。

归并排序基于分治算法。最容易想到的实现方式是自顶向下的递归实现,考虑到递归调用的栈空间,自顶向下归并排序的空间复杂度是O(logn)。如果要达到O(1) 的空间复杂度,则需要使用自底向上的实现方式。

方法一:自顶向下归并排序
对链表自顶向下归并排序的过程如下。

  1. 找到链表的中点,以中点为分界,将链表拆分成两个子链表。寻找链表的中点可以使用快慢指针的做法,快指针每次移动 2 步,慢指针每次移动 1 步,当快指针到达链表末尾时,慢指针指向的链表节点即为链表的中点。
  2. 对两个子链表分别排序。
  3. 将两个排序后的子链表合并,得到完整的排序后的链表。可以使用合并两个有序链表的做法,将两个有序的子链表进行合并。

上述过程可以通过递归实现。递归的终止条件是链表的节点个数小于或等于 1,即当链表为空或者链表只包含 1 个节点时,不需要对链表进行拆分和排序。

	//自顶向下归并排序
	ListNode* sortList1(ListNode* head) {
	return sortList1(head, nullptr);
	}
	ListNode* sortList1(ListNode* head, ListNode* tail) {
		if (head == nullptr) {
			return head;
		}
		if (head->next == tail) {
			head->next = nullptr;
			return head;
		}
		ListNode* slow = head, *fast = head;
		while (fast != tail) {
			slow = slow->next;
			fast = fast->next;
			if (fast != tail) {
				fast = fast->next;
			}
		}
		ListNode* mid = slow;
		return merge(sortList(head, mid), sortList(mid, tail));  //左开右闭
	}

	ListNode* merge(ListNode* head1, ListNode* head2) {
		ListNode* dummyHead = new ListNode(0);
		ListNode* temp = dummyHead, *temp1 = head1, *temp2 = head2;
		while (temp1 != nullptr && temp2 != nullptr) {
			if (temp1->val <= temp2->val) {
				temp->next = temp1;
				temp1 = temp1->next;
			}
			else {
				temp->next = temp2;
				temp2 = temp2->next;
			}
			temp = temp->next;
		}
		if (temp1 != nullptr) {
			temp->next = temp1;
		}
		else if (temp2 != nullptr) {
			temp->next = temp2;
		}
		return dummyHead->next;
	}
class Solution:
    def sortList(self, head: ListNode) -> ListNode:
        def sortFunc(head: ListNode, tail: ListNode) -> ListNode:
            if not head:
                return head
            if head.next == tail:
                head.next = None
                return head
            slow = fast = head
            while fast != tail:
                slow = slow.next
                fast = fast.next
                if fast != tail:
                    fast = fast.next
            mid = slow
            return merge(sortFunc(head, mid), sortFunc(mid, tail))
            
        def merge(head1: ListNode, head2: ListNode) -> ListNode:
            dummyHead = ListNode(0)
            temp, temp1, temp2 = dummyHead, head1, head2
            while temp1 and temp2:
                if temp1.val <= temp2.val:
                    temp.next = temp1
                    temp1 = temp1.next
                else:
                    temp.next = temp2
                    temp2 = temp2.next
                temp = temp.next
            if temp1:
                temp.next = temp1
            elif temp2:
                temp.next = temp2
            return dummyHead.next
        
        return sortFunc(head, None)
  1. 时间复杂度:O(nlogn),其中 n 是链表的长度。
  2. 空间复杂度:O(logn),其中 n 是链表的长度。空间复杂度主要取决于递归调用的栈空间。

方法二:自底向上归并排序
使用自底向上的方法实现归并排序,则可以达到 O(1) 的空间复杂度。

首先求得链表的长度 length,然后将链表拆分成子链表进行合并。

具体做法如下。

  1. 用 subLength 表示每次需要排序的子链表的长度,初始时 subLength=1。
  2. 每次将链表拆分成若干个长度为subLength 的子链表(最后一个子链表的长度可以小于 subLength),按照每两个子链表一组进行合并,合并后即可得到若干个长度为 subLength×2 的有序子链表(最后一个子链表的长度可以小于 subLength×2)。合并两个子链表仍然使用合并两个有序链表的做法。
  3. 将subLength 的值加倍,重复第 2 步,对更长的有序子链表进行合并操作,直到有序子链表的长度大于或等于length,整个链表排序完毕。

如何保证每次合并之后得到的子链表都是有序的呢?可以通过数学归纳法证明。
初始时subLength=1,每个长度为 1 的子链表都是有序的。
如果每个长度为subLength 的子链表已经有序,合并两个长度为 subLength 的有序子链表,得到长度为subLength×2 的子链表,一定也是有序的。
当最后一个子链表的长度小于 subLength 时,该子链表也是有序的,合并两个有序子链表之后得到的子链表一定也是有序的。
因此可以保证最后得到的链表是有序的。

C++实现

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) {}
 };

class Solution {
public:
		//自下向顶归并排序
		ListNode *sortList(ListNode* head) {
			if (head == nullptr) {
				return head;
			}
			int length = 0;
			ListNode* node = head;
			while (node != nullptr) {
				length++;
				node = node->next;
			}
			ListNode* dummyHead = new ListNode(0, head);
			for (int subLength = 1; subLength < length; subLength <<= 1) {
				ListNode* prev = dummyHead, *curr = dummyHead->next;
				while (curr != nullptr) {
					ListNode* head1 = curr;
					for (int i = 1; i < subLength && curr->next != nullptr; i++) {
						curr = curr->next;
					}
					ListNode* head2 = curr->next;
					curr->next = nullptr;
					curr = head2;
					for (int i = 1; i < subLength && curr != nullptr && curr->next != nullptr; i++) {
						curr = curr->next;
					}
					ListNode* next = nullptr;
					if (curr != nullptr) {
						next = curr->next;
						curr->next = nullptr;
					}
					ListNode* merged = merge(head1, head2);
					prev->next = merged;
					while (prev->next != nullptr) {
						prev = prev->next;
					}
					curr = next;
				}
			}
			return dummyHead->next;
		}

		ListNode* merge(ListNode* head1, ListNode* head2) {
			ListNode* dummyHead = new ListNode(0);
			ListNode* temp = dummyHead, *temp1 = head1, *temp2 = head2;
			while (temp1 != nullptr && temp2 != nullptr) {
				if (temp1->val <= temp2->val) {
					temp->next = temp1;
					temp1 = temp1->next;
				}
				else {
					temp->next = temp2;
					temp2 = temp2->next;
				}
				temp = temp->next;
			}
			if (temp1 != nullptr) {
				temp->next = temp1;
			}
			else if (temp2 != nullptr) {
				temp->next = temp2;
			}
			return dummyHead->next;
		}
};

时间复杂度:O(nlogn),其中 n是链表的长度。
空间复杂度:O(1)。

234-回文链表

给你一个单链表的头节点head,请你判断该链表是否为回文链表。如果是,返回true;否则,返回false。
在这里插入图片描述

解题思路

方法一:将值复制到数组中后用双指针法

  1. 复制链表值到数组中
  2. 使用双指针法判断是否为回文
    第一步,我们需要遍历链表将值复制到数组列表中。我们用 currentNode 指向当前节点。每次迭代向数组添加 currentNode.val,并更新 currentNode = currentNode.next,当 currentNode = null 时停止循环。

执行第二步的最佳方法取决于你使用的语言。在 Python 中,很容易构造一个列表的反向副本,也很容易比较两个列表。而在其他语言中,就没有那么简单。因此最好使用双指针法来检查是否为回文。我们在起点放置一个指针,在结尾放置一个指针,每一次迭代判断两个指针指向的元素是否相同,若不同,返回 false;相同则将两个指针向内移动,并继续判断,直到两个指针相遇。

在编码的过程中,注意我们比较的是节点值的大小,而不是节点本身。正确的比较方式是:node_1.val == node_2.val,而 node_1 == node_2 是错误的。

C++实现

class Solution {
public:
    bool isPalindrome(ListNode* head) {
        vector<int> vals;
        while (head != nullptr) {
            vals.emplace_back(head->val);//在容器尾部构造元素
            head = head->next;
        }
        //A.size()默认存储类型为unsigned int,使用要强制转换为int类型
        for (int i = 0, j = (int)vals.size() - 1; i < j; ++i, --j) {
            if (vals[i] != vals[j]) {
                return false;
            }
        }
        return true;
    }
};
class Solution:
    def isPalindrome(self, head: ListNode) -> bool:
        vals = []
        current_node = head
        while current_node is not None:
            vals.append(current_node.val)
            current_node = current_node.next
        return vals == vals[::-1]

if __name__=='__main__':
    head = ListNode(1)
    head.next=ListNode(2)
    print(Solution().isPalindrome(head))

  1. 时间复杂度:O(n),其中 n 指的是链表的元素个数。第一步: 遍历链表并将值复制到数组中,O(n)。
    第二步:双指针判断是否为回文,执行了 O(n/2) 次的判断,即 O(n)。总的时间复杂度:O(2n)=O(n)。
  2. 空间复杂度:O(n),其中 n 指的是链表的元素个数,我们使用了一个数组列表存放链表的元素值。

方法二:快慢指针

避免使用O(n) 额外空间的方法就是改变输入。

我们可以将链表的后半部分反转(修改链表结构),然后将前半部分和后半部分进行比较。比较完成后我们应该将链表恢复原样。虽然不需要恢复也能通过测试用例,但是使用该函数的人通常不希望链表结构被更改。

该方法虽然可以将空间复杂度降到 O(1),但是在并发环境下,该方法也有缺点。在并发环境下,函数运行时需要锁定其他线程或进程对链表的访问,因为在函数执行过程中链表会被修改。
整个流程可以分为以下五个步骤:

  1. 找到前半部分链表的尾节点
  2. 反转后半部分链表
  3. 判断是否回文
  4. 恢复链表
  5. 返回结果
    执行步骤一,我们可以计算链表节点的数量,然后遍历链表找到前半部分的尾节点。

我们也可以使用快慢指针在一次遍历中找到:慢指针一次走一步,快指针一次走两步,快慢指针同时出发。当快指针移动到链表的末尾时,慢指针恰好到链表的中间。通过慢指针将链表分为两部分。

若链表有奇数个节点,则中间的节点应该看作是前半部分。

步骤二可以使用 反转链表问题中的解决方法来反转链表的后半部分。

步骤三比较两个部分的值,当后半部分到达末尾则比较完成,可以忽略计数情况中的中间节点。

步骤四与步骤二使用的函数相同,再反转一次恢复链表本身。

C++实现

class Solution {
public:
    bool isPalindrome(ListNode* head) {
        if (head == nullptr) {
            return true;
        }

        // 找到前半部分链表的尾节点并反转后半部分链表
        ListNode* firstHalfEnd = endOfFirstHalf(head);
        ListNode* secondHalfStart = reverseList(firstHalfEnd->next);

        // 判断是否回文
        ListNode* p1 = head;
        ListNode* p2 = secondHalfStart;
        bool result = true;
        while (result && p2 != nullptr) {
            if (p1->val != p2->val) {
                result = false;
            }
            p1 = p1->next;
            p2 = p2->next;
        }        

        // 还原链表并返回结果
        firstHalfEnd->next = reverseList(secondHalfStart);
        return result;
    }

    ListNode* reverseList(ListNode* head) {
        ListNode* prev = nullptr;
        ListNode* curr = head;
        while (curr != nullptr) {
            ListNode* nextTemp = curr->next;
            curr->next = prev;
            prev = curr;
            curr = nextTemp;
        }
        return prev;
    }

    ListNode* endOfFirstHalf(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;
    }
};

```python
class Solution:

    def isPalindrome(self, head: ListNode) -> bool:
        if head is None:
            return True

        # 找到前半部分链表的尾节点并反转后半部分链表
        first_half_end = self.end_of_first_half(head)
        second_half_start = self.reverse_list(first_half_end.next)

        # 判断是否回文
        result = True
        first_position = head
        second_position = second_half_start
        while result and second_position is not None:
            if first_position.val != second_position.val:
                result = False
            first_position = first_position.next
            second_position = second_position.next

        # 还原链表并返回结果
        first_half_end.next = self.reverse_list(second_half_start)
        return result    

    def end_of_first_half(self, head):
        fast = head
        slow = head
        while fast.next is not None and fast.next.next is not None:
            fast = fast.next.next
            slow = slow.next
        return slow

    def reverse_list(self, head):
        previous = None
        current = head
        while current is not None:
            next_node = current.next
            current.next = previous
            previous = current
            current = next_node
        return previous
  1. 时间复杂度:O(n),其中 n 指的是链表的大小。
  2. 空间复杂度:O(1)。我们只会修改原本链表中节点的指向,而在堆栈上的堆栈帧不超过 O(1)。
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值