算法(六)链表

leetcode

[hot] 2. 两数相加

题目

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

示例 1:
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.

题解

题解需要注意进位的处理,示例代码如下所示:

/**
 * 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:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        int carry = 0;
        ListNode* head = new ListNode(-1);
        ListNode* cur = head;
        while (l1 || l2) {
            int sum = (l1 ? l1->val : 0) + (l2 ? l2->val : 0) + carry;
            carry = sum / 10;
            ListNode* node = new ListNode(sum % 10);
            cur->next = node;
            cur = cur->next;
            if (l1) {
                l1 = l1->next;
            }
            if (l2) {
                l2 = l2->next;
            }
        }

        if (carry) {
            ListNode* node = new ListNode(carry);
            cur->next = node;
        }

        return head->next;
    }
};

// 递归
class Solution {
public:
    ListNode *addTwoNumbers(ListNode *l1, ListNode *l2, int carry = 0) {
        if (l1 == nullptr && l2 == nullptr) {
            return carry ? new ListNode(carry) : nullptr;
        }

        if (l1 == nullptr) {
            swap(l1, l2);
        }
        carry += l1->val + (l2 ? l2->val : 0);
        l1->val = carry % 10;
        l1->next = addTwoNumbers(l1->next, (l2 ? l2->next : nullptr), carry / 10);

        return l1;

    }
};

复杂度

时间复杂度: O ( m a x ( m , n ) ) O(max(m, n)) O(max(m,n))
空间复杂度: O ( 1 ) O(1) O(1),如果返回值不计入空间复杂度

关键点

进位的判断

[hot] 19. 删除链表的倒数第 N 个结点

题目

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

进阶:你能尝试使用一趟扫描实现吗?
算法(六)链表_第1张图片

题解

解题方式和【剑指offer 22. 链表中倒数第k个节点】基本一致,双指针找到倒数第n+1个节点,关联倒数第n+1和n-1节点,即实现删除倒数第n个节点的任务。在这个过程中,需要额外指定dummy节点进行遍历,这样能够免去边界情况的特殊处理(demo case为[1], n = 1),示例代码如下:

/**
 * 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:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* pre = head;
        for (int i = 0; i < n; ++i) {
            if (pre == nullptr) {
                return nullptr;
            }
            pre = pre->next;
        }
        ListNode* dummy = new ListNode(0, head);
        ListNode* tail = dummy;
        while (pre) {
            pre = pre->next;
            tail = tail->next;
        }
        tail->next = tail->next->next;
        return dummy->next;
    }
};

关键点

先走n步时边界条件的判断

[hot] 23. 合并K个升序链表

题目

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

示例 1:
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
  1->4->5,
  1->3->4,
  2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6

题解

归并过程中调用<合并两个有序链表>,示例代码如下所示:

/**
 * 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:
    ListNode* merge2Lists(ListNode* l1, ListNode* l2) {
        if (!l1) {
            return l2;
        }
        if (!l2) {
            return l1;
        }
        ListNode* node = nullptr;
        if (l1->val < l2->val) {
            node = l1;
            node->next = merge2Lists(l1->next, l2);
        } else {
            node = l2;
            node->next = merge2Lists(l1, l2->next);
        }

        return node;
    }

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

    ListNode* mergeKLists(vector<ListNode*>& lists) {
        int length = lists.size();
        return merge(lists, 0, length - 1);
    }
};

// 完全迭代版本
class Solution {
public:
    ListNode* merge2l(ListNode* l1, ListNode* l2) {
        ListNode* head = new ListNode(-1);
        auto prev = head;
        while (l1 && l2) {
            if (l1->val < l2->val) {
                prev->next = l1;
                l1 = l1->next;
            } else {
                prev->next = l2;
                l2 = l2->next; 
            }
            prev = prev->next;
        }
        prev->next = l1 ? l1 : l2;
        return head->next;
    }

    ListNode* mergeKLists(vector<ListNode*>& lists) {
        if (lists.empty()) {
            return nullptr;
        }
        int k = lists.size();
        while (k > 1) {
            int idx = 0;
            for (int i = 0; i < k; i += 2) {
                if (i == k - 1) {
                    lists[idx++] = lists[i];
                } else {
                    lists[idx++] = merge2l(lists[i], lists[i + 1]);
                }
            }
            k = idx;
        }
        return lists[0];
        
    }
};

复杂度

时间复杂度: O ( k n ∗ l o g k ) O(kn * logk) O(knlogk),k条链表,每条链表都要被合并logk次,总共需要k * logk 次合并,每次合并的时间复杂度为n,因而总体时间复杂度为 O ( k n ∗ l o g k ) O(kn * logk) O(knlogk)
空间复杂度: O ( l o g k ) O(logk) O(logk),不用递归形式,则空间复杂度为 O ( 1 ) O(1) O(1)

关键点

  1. 迭代版本需要考虑链表节点个数的奇偶情况
  2. 时间复杂度的理解

[hot] 24. 两两交换链表中的节点

题目

给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

题解

递归完成两两交换,示例代码如下所示:

/**
 * 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:
    ListNode* swapPairs(ListNode* head) {
        if (!head || !head->next) {
            return head;
        }

        ListNode* new_head = head->next;
        head->next = swapPairs(new_head->next);
        new_head->next = head;
        return new_head;
    }
};

// 迭代版本
class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        auto dummy = new ListNode(0, head);
        auto temp = dummy;
        while (temp->next && temp->next->next) {
            auto node1 = temp->next;
            auto node2 = temp->next->next;
            node1->next = node2->next;
            node2->next = node1;
            temp->next = node2;
            temp = node1;
        }

        return dummy->next;
    }
};

复杂度

时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( n ) O(n) O(n),栈调用空间大小,迭代版本为 O ( 1 ) O(1) O(1),不需要额外空间

关键点

翻转前后节点之间的联系,能够保证翻转过程中不至于断链

[hot] 25. K 个一组翻转链表

题目

给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

进阶:
你可以设计一个只使用常数额外空间的算法来解决此问题吗?
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

算法(六)链表_第2张图片

题解

递归断链反转链表,整体流程为:

  1. 断链
  2. 反转当前链表段
  3. 递归反转下一链表段

示例代码如下所示:

/**
 * 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:
    ListNode* reverse_list(ListNode* head) {
        if (!head) {
            return nullptr;
        }

        ListNode* p = head;
        while (p->next) {
            auto q = p->next;
            p->next = q->next;
            q->next = head;
            head = q;
        }

        return head;
    }

    ListNode* reverseKGroup(ListNode* head, int k) {
        // 1. 断链
        auto temp = head;
        for (int i = 1; i < k && temp; ++i) {
            temp = temp->next;
        }

        // 到最后一个链表段
        if (temp == nullptr) {
            return head;
        }

        auto temp_n = temp->next;
        temp->next = nullptr;

        // 2. 反转当前链表段
        auto new_head = reverse_list(head);

		// 3. 递归反转下一链表段
        auto new_temp = reverseKGroup(temp_n, k);

        head->next = new_temp;

        return new_head;
    }

    pair<ListNode*, ListNode*> myReverse(ListNode* head, ListNode* tail) {
        ListNode* prev = tail->next;
        ListNode* p = head;
        while (prev != tail) {
            ListNode* nex = p->next;
            p->next = prev;
            prev = p;
            p = nex;
        }
        return {tail, head};
    }

    // 迭代形式,节省空间复杂度
    ListNode* reverseKGroup(ListNode* head, int k) {
        auto dummy = new ListNode(0, head);
        auto pre = dummy;
        while (head) {
            auto tail = pre;
            for (int i = 0; i < k; ++i) {
                tail = tail->next;
                if (!tail) {
                    return dummy->next;
                }
            }
            auto tail_n = tail->next;
            auto res = myReverse(head, tail);
            head = res.first;
            tail = res.second;
            pre->next = head;
            tail->next = tail_n;
            pre = tail;
            head = tail->next;
        }
        return dummy->next;
    }
};

复杂度

时间复杂度: O ( n ) O(n) O(n)
空间复杂度:个人理解为 O ( n ) O(n) O(n),因为递归要开辟额外的堆栈空间,且这个空间是和数组长度相关的,迭代形式为 O ( 1 ) O(1) O(1)

关键点

恢复翻转后的现场

61. 旋转链表

题目

给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。

输入:head = [1,2,3,4,5], k = 2
输出:[4,5,1,2,3]

算法(六)链表_第3张图片

题解

闭合为环,之后再断链,示例代码如下所示:

/**
 * 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:
    ListNode* rotateRight(ListNode* head, int k) {
        if (!head) {
            return head;
        }

		// 1. 遍历到链表尾部
        int n = 1;
        ListNode* iter = head;
        while (iter->next) {
            iter = iter->next;
            ++n;
        }

		// 2. 计算n - k
        int add = n - k % n;
        if (add == n) {
            return head;
        }

		// 3. 找到n - k位置
        iter->next = head;
        while (add--) {
            iter = iter->next;
        }

		// 4. 断链
        ListNode* ret = iter->next;
        iter->next = nullptr;
        
        return ret;
    }
};

复杂度

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

关键点

  1. 断链
  2. k边界条件判断

82. 删除排序链表中的重复元素 II

题目

存在一个按升序排列的链表,给你这个链表的头节点 head ,请你删除链表中所有存在数字重复情况的节点,只保留原始链表中 没有重复出现 的数字。返回同样按升序排列的结果链表。

输入:head = [1,2,3,3,4,4,5]
输出:[1,2,5]

题解

一遍遍历删除节点,示例代码如下所示:

/**
 * 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:
    ListNode* deleteDuplicates(ListNode* head) {
        if (!head) {
            return head;
        }

        ListNode* dummy = new ListNode(0, head);
        auto cur = dummy;
        while (cur->next && cur->next->next) {
            if (cur->next->val == cur->next->next->val) {
                int x = cur->next->val;
                while (cur->next && cur->next->val == x) {
                    cur->next = cur->next->next;
                }
            } else {
                cur = cur->next;
            }
        }

        return dummy->next;
    }
};

复杂度

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

关键点

判断下个节点和下下个节点是否相等

83. 删除排序链表中的重复元素

题目

给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。

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

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

题解

找到重复的两个节点pre和cur,且pre->next=cur,如果pre->val == cur->val,则pre->next指向cur->next。

示例代码如下

/**
 * 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:
    ListNode* deleteDuplicates(ListNode* head) {
        if (!head || !head->next) {
            return head;
        }
        auto cur = head;
        while (cur->next) {
            if (cur->val == cur->next->val) {
                cur->next = cur->next->next;
            } else {
                cur = cur->next;
            }
        }
        return head;
    }
};

关键点

前面节点已经处理好之后,最后一个节点是不用删除的

86. 分隔链表

题目

给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。

你应当 保留 两个分区中每个节点的初始相对位置。

输入:head = [1,4,3,2,5,2], x = 3
输出:[1,2,2,4,3,5]

算法(六)链表_第4张图片

题解

定义两个指针,分别指向存储比x小的元素组成的链表和比x大的元素组成的链表,示例代码如下所示:

/**
 * 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:
    ListNode* partition(ListNode* head, int x) {
        ListNode* small = new ListNode(0);
        ListNode* small_head = small;
        ListNode* large = new ListNode(0);
        ListNode* large_head = large;

        while (head) {
            if (head->val < x) {
                small->next = head;
                small = small->next;
            } else {
                large->next = head;
                large = large->next;
            }
            head = head->next;
        }

		// large链表的尾结点依然连接着原有链表的节点
		// 这里需要断绝与原有链表之间的关系
        large->next = nullptr;
        small->next = large_head->next;

        return small_head->next;
    }
};

复杂度

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

关键点

  1. 基于原先的空间直接提取small和large两条链表
  2. large表的断链和small表链接large表

92. 反转链表 II

题目

给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。
算法(六)链表_第5张图片

题解

正常反转链表思路,整体需要注意如下两点:

  1. 需要设置哨兵节点,这样能够兼容left=1的情况,因为这时链表的head节点会发生变化
  2. 最终返回指针是哨兵节点的next节点,而不是head节点,这也是为了兼容left=1的情况

示例代码如下所示:

class Solution {
public:
    ListNode *reverseBetween(ListNode *head, int left, int right) {
    	// 1. 设置哨兵节点
        ListNode* dummy = new ListNode(-1);
        dummy->next = head;

		// 2. 遍历到需要反转的位置
        ListNode* pre = dummy;
        for (int i = 0; i < left - 1; ++i) {
            pre = pre->next;
        }

		// 3. 反转链表
        ListNode* cur = pre->next;
        ListNode* next = nullptr;
        for (int i = 0; i < right - left; ++i) {
            next = cur->next;
            cur->next = next->next;
            next->next = pre->next;
            pre->next = next;
        }

		// 4. 返回哨兵->next
        ListNode* ret = dummy->next;
        delete dummy;

        return ret;
    }
};

复杂度

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

关键点

哨兵节点的设定,能够兼容头节点被删除的情况

109. 有序链表转换为二叉搜索树

题目

给定一个单链表的头节点 head ,其中的元素 按升序排序 ,将其转换为高度平衡的二叉搜索树。

本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差不超过 1。

题解

class Solution {
public:
    ListNode* getMedian(ListNode* left, ListNode* right) {
        ListNode* fast = left;
        ListNode* slow = left;
        while (fast != right && fast->next != right) {
            fast = fast->next;
            fast = fast->next;
            slow = slow->next;
        }
        return slow;
    }

    TreeNode* buildTree(ListNode* left, ListNode* right) {
        if (left == right) {
            return nullptr;
        }
        ListNode* mid = getMedian(left, right);
        TreeNode* root = new TreeNode(mid->val);
        root->left = buildTree(left, mid);
        root->right = buildTree(mid->next, right);
        return root;
    }

    TreeNode* sortedListToBST(ListNode* head) {
        return buildTree(head, nullptr);
    }
};

复杂度

时间: O ( n l o g n ) O(nlogn) O(nlogn)
空间: O ( l o g n ) O(logn) O(logn)

题解2

中序遍历:

class Solution {
public:
    int getLength(ListNode* head) {
        int ret = 0;
        for (; head != nullptr; ++ret, head = head->next);
        return ret;
    }

    TreeNode* buildTree(ListNode*& head, int left, int right) {
        if (left > right) {
            return nullptr;
        }
        int mid = (left + right) / 2;
        TreeNode* root = new TreeNode();
        root->left = buildTree(head, left, mid - 1);
        root->val = head->val;
        head = head->next;
        root->right = buildTree(head, mid + 1, right);
        return root;
    }

    TreeNode* sortedListToBST(ListNode* head) {
        int length = getLength(head);
        return buildTree(head, 0, length - 1);
    }
};

复杂度

时间: O ( n ) O(n) O(n)
空间: O ( l o g n ) O(logn) O(logn)

关键点

  1. 理解二叉搜索树的中序遍历是有序的,很关键
  2. 后填值操作

[hot] 141. 环形链表

题目

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

题解

正常快慢指针思路解决环形链表,示例代码如下。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    bool hasCycle(ListNode *head) {
        if (!head || !head->next) {
            return false;
        }

        ListNode* slow = head;
        ListNode* fast = head->next;
        while (fast != slow) {
            if (!fast || !fast->next) {
                return false;
            }
            slow = slow->next;
            fast = fast->next->next;
        }

        return true;
    }
};

复杂度

时间复杂度: O ( n ) O(n) O(n),n为链表长度

  • 如果链表不存在环,则快指针很快移动到链表尾部,时间为 n / 2 n/2 n/2
  • 如果链表存在环,则两个指针进入环之后,每移动一次,快慢指针的距离都缩短1,因而整体时间复杂度依然是 O ( n ) O(n) O(n)

空间复杂度: O ( 1 ) O(1) O(1)

关键点

fast在初始时必须要比slow超前一位,这样才能在进入环形链表后追平。

[hot] 142. 环形链表 II 入口节点

题目

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。

说明:不允许修改给定的链表。

算法(六)链表_第6张图片

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

题解

想象操场跑圈,假如A的速度是2,B的速度是1,那么如果A和B在同一起点跑,则A和B一定会在起点相遇。假如A和B还没有到操场的时候就开始同时跑,那么A肯定会在B还没有跑完一圈的时候追上B。基于上述描述,这里设起跑点到操场入口的距离是x,相遇点到操场入口的距离是y,那么这个时候B的路程为(x+y),A的路程为2(x+y),且操场的长度l=(x+y),那么很显然,B以1的速度从起跑点走到操场入口点和从相遇点到操场入口点的距离是一样的。

算法(六)链表_第7张图片

这样就有了如下示例代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        if (!head || !head->next) {
            return nullptr;
        }
        
        auto slow = head;
        auto fast = head;
        while (fast) {
            slow = slow->next;
            fast = fast->next;
            if (fast) {
                fast = fast->next;
            }
            if (fast == slow) {
                auto start = head;
                while (start != slow) {
                    start = start->next;
                    slow = slow->next;  
                }
                return start;
            }
        }

        return nullptr;
    }
};

复杂度

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

关键点

理解链表起始点到环起始点的距离,等于快慢指针相遇点到环起始点的距离。

[hot] 146. LRU缓存

题目

请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache 类:

LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。

函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。

示例:

输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]

解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1);    // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2);    // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1);    // 返回 -1 (未找到)
lRUCache.get(3);    // 返回 3
lRUCache.get(4);    // 返回 4

题解

哈希表和双向链表解决问题,

算法(六)链表_第8张图片示例代码如下所示:

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;
        }
        // 如果 key 存在,先通过哈希表定位,再移到头部
        DLinkedNode* node = cache[key];
        moveToHead(node);
        return node->value;
    }
    
    void put(int key, int value) {
        if (!cache.count(key)) {
            // 如果 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 {
            // 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
            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 removeNode(DLinkedNode* node) {
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }

    void moveToHead(DLinkedNode* node) {
        removeNode(node);
        addToHead(node);
    }

    DLinkedNode* removeTail() {
        DLinkedNode* node = tail->prev;
        removeNode(node);
        return node;
    }
};

复杂度

时间复杂度: O ( 1 ) O(1) O(1)
空间复杂度: O ( c a p a c i t y ) O(capacity) O(capacity)

关键点

  1. 设置head节点是为了方便插入头部节点
  2. 设置tail节点是为了方便删除尾部节点
  3. get和put完成之后,一定要把最近查询的这个节点移动到链表的最前方

[hot] 148. 排序链表

题目

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

算法(六)链表_第9张图片

题解1

自顶向下归并排序合并链表。首先遍历到链表中点,然后进行归并排序,示例代码如下所示:

/**
 * 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:
    ListNode* sortList(ListNode* head) {
        return merge(head, nullptr);
    }

    ListNode* merge(ListNode* head, ListNode* tail) {
        if (!head) {
            return nullptr;
        }
        
        // 如下代码意图为把一条链表断成两条,这样才能保证后续merge的正常进行
        if (head->next == tail) {
            head->next = nullptr;
            return head;
        }

        auto slow = head;
        auto fast = head;
        while (fast != tail) {
            slow = slow->next;
            fast = fast->next;
            if (fast != tail) {
                fast = fast->next;
            }
        }

        return merge2Lists(merge(head, slow), merge(slow, tail));
    }

    ListNode* merge2Lists(ListNode* l1, ListNode* l2) {
        if (!l1) {
            return l2;
        }
        if (!l2) {
            return l1;
        }
        ListNode* node = nullptr;
        if (l1->val < l2->val) {
            node = l1;
            node->next = merge2Lists(l1->next, l2);
        } else {
            node = l2;
            node->next = merge2Lists(l1, l2->next);
        }

        return node;
    }
};

复杂度

时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn),归并排序时间复杂度
空间复杂度: O ( l o g n ) O(logn) O(logn),递归栈辅助空间

题解2

自底向上实现归并排序,示例代码如下所示:

class Solution {
public:
    ListNode* sortList(ListNode* head) {
        if (!head) {
            return nullptr;
        }
        int length = 0;
        auto p = head;
        while (p) {
            p = p->next;
            ++length;
        }
        auto dummy = new ListNode(0, head);
        for (int sub_len = 1; sub_len < length; sub_len *= 2) {
            auto pre = dummy;
            auto cur = dummy->next;
            while (cur)  {
                auto head1 = cur;
                for (int i = 1; i < sub_len && cur->next; ++i) {
                    cur = cur->next;
                }
                auto head2 = cur->next;
                cur->next = nullptr;
                cur = head2;
                for (int i = 1; i < sub_len && cur && cur->next; ++i) {
                    cur = cur->next;
                }
                ListNode* next = nullptr;
                // 遍历到链表尾部,cur会为空,在这里需要加判断
                if (cur) {
                    next = cur->next;
                    cur->next = nullptr;
                }
                auto merged = merge(head1, head2);
                pre->next = merged;
                while (pre->next) {
                    pre = pre->next;
                }
                cur = next;
            }
        }

        return dummy->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 ( n l o g n ) O(nlogn) O(nlogn)
空间复杂度: O ( 1 ) O(1) O(1),因为没有引入额外的空间复杂度

题解3

快速排序完成链表排序:

class Solution {
public:
    ListNode* sortList(ListNode* head) {
        q_s(head, nullptr);

        return head;
    }

    void q_s(ListNode* head, ListNode* end) {
        if (!head || head == end) {
            return;
        }
        auto p1 = head;
        auto p2 = head->next;
        while (p2 != end) {
            if (p2->val < head->val) {
                p1 = p1->next;
                swap(p2->val, p1->val);
            }
            p2 = p2->next;
        }
        swap(head->val, p1->val);
        q_s(head, p1);
        q_s(p1->next, end);
    }
};

复杂度

时间: O ( n l o g n ) O(nlogn) O(nlogn)
空间: O ( l o g n ) O(logn) O(logn),递归的空间复杂度

关键点

  1. 归并排序需要断链,快速排序不用断链
  2. 迭代版的归并排序,需要额外写代码处理边界情况

[hot] 160. 两个链表的第一个公共节点

题目

输入两个链表,找出它们的第一个公共节点。
在这里插入图片描述

输入: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 个节点。

题解

两个指针同时遍历链表,当一个指针遍历到尾部的时候,跳到另一个链表的头部,这样如果有交点的话,则两个指针就会相遇。

短链表中指针走了 x + a + y x+a+y x+a+y这么长,长链表走了 y + a + x y+a+x y+a+x这么长, x x x代表的是短链表汇聚之前的长度, y y y代表的长链表汇聚之前的长度,而 a a a代表的是两个链表汇聚的长度,即长的到短的这边来,短的到长的这边来。但是必须要考虑两个链表根本没有公共节点的情况。

示例代码如下:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        auto a = headA;
        auto b = headB;
        // 如果headA和headB没有交集,最终跳出while循环时a和b同为nullptr,
        // 这时相当于headA和headB的交集为nullptr
        while (a != b) { 
            a = a == nullptr ? headB : a->next;
            b = b == nullptr ? headA : b->next;
        }

        return a;
    }
};

复杂度

时间复杂度: O ( m + n ) O(m + n) O(m+n),m和n分别是链表的长度
空间复杂度: O ( 1 ) O(1) O(1)

203. 移除链表元素

题目

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。

算法(六)链表_第10张图片

题解

注意这里和下面【剑指offer18. 删除链表的节点】的区别,这里是把所有符合要求的节点都删除掉,双指针完成任务,示例代码如下所示:

/**
 * 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:
    ListNode* removeElements(ListNode* head, int val) {
        if (head == nullptr) {
            return head;
        }
        head->next = removeElements(head->next, val);
        return head->val == val ? head->next : head;
    }
};

// 迭代版本
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        if (!head) {
            return head;
        }

        ListNode* dummy = new ListNode(0, head);
        auto pre = dummy;
        auto cur = dummy->next;

        while (cur) {
            if (cur->val == val) {
                pre->next = cur->next;
            } else {
                pre = cur;
            }
            cur = cur->next;
        }

        return dummy->next;
    }
};

复杂度

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

关键点

哨兵节点的设立

[hot] 206. 反转链表

题目

反转一个单链表。

示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

题解

从前到后逐步反转链表,以[1 2 3 4 5]这个链表为例,解释反转链表的过程:

1. 1 2 3 4 5 -> start
2. 2 1 3 4 5 
3. 3 2 1 4 5 
4. 4 3 2 1 5 
5. 5 4 3 2 1 -> end

示例代码如下所示:

/**
 * 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:
    ListNode* reverseList(ListNode* head) {
        if (!head) {
            return head;
        }
        ListNode* p = head;
        while (p->next) {
            ListNode* q = p->next;
            p->next = q->next;
            q->next = head;
            head = q;
        }

        return head;
    }
};

// 递归版本
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        if (!head || !head->next) {
            return head;
        }
        auto new_head = reverseList(head->next);
        head->next->next = head;
        head->next = nullptr;
        return new_head;
    }
};

复杂度

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

关键点

  1. 头节点(迭代版本的p)的不断后移
  2. 遍历节点(迭代版本的q)直接变为头节点

[hot] 234. 回文链表

题目

请判断一个链表是否为回文链表。

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

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

题解

先遍历到链表中点,然后翻转链表,然后同时从头节点和中点共同遍历,如果都相同则是一个回文串,示例代码如下:

/**
 * 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:
    ListNode* reverse_list(ListNode* head) {
        if (!head) {
            return nullptr;
        }

        ListNode* p = head;
        while (p->next) {
           auto q = p->next;
           p->next = q->next;
           q->next = head;
           head = q; 
        }

        return head;
    }

    ListNode* end_of_half(ListNode* head) {
        if (!head) {
            return nullptr;
        }

        ListNode* slow = head;
        ListNode* fast = head;
        // 如果想返回中间节点考前的那个节点,这里的判断条件应该是
        // fast-> next && fast->next->next
        while (fast && fast->next) {
            slow = slow->next;
            fast = fast->next->next;
        }

        return slow;
    }

    bool isPalindrome(ListNode* head) {
        if (!head) {
            return false;
        }
        auto end_of_half_first = end_of_half(head);
        auto end_of_half_reverse = reverse_list(end_of_half_first); 
        
        auto p1 = head;
        while (end_of_half_reverse) { // 细节
            if (p1->val != end_of_half_reverse->val) {
                return false;
            } 
            p1 = p1->next;
            end_of_half_reverse = end_of_half_reverse->next;
        }

        return true;
    }
};

// 递归版本 时间和空间复杂度都为O(n)
class Solution {
    ListNode* frontPointer;
public:
    bool recursivelyCheck(ListNode* currentNode) {
        if (currentNode != nullptr) {
            if (!recursivelyCheck(currentNode->next)) {
                return false;
            }
            if (currentNode->val != frontPointer->val) {
                return false;
            }
            frontPointer = frontPointer->next;
        }
        return true;
    }

    bool isPalindrome(ListNode* head) {
        frontPointer = head;
        return recursivelyCheck(head);
    }
};

复杂度

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

关键点

链表中间节点的定义

237. 删除链表的节点

题目

有一个单链表的 head,我们想删除它其中的一个节点 node。

给你一个需要删除的节点 node 。你将 无法访问 第一个节点 head。

链表的所有值都是 唯一的,并且保证给定的节点 node 不是链表中的最后一个节点。

删除给定的节点。注意,删除节点并不是指从内存中删除它。这里的意思是:

给定节点的值不应该存在于链表中。
链表中的节点数应该减少 1。
node 前面的所有值顺序相同。
node 后面的所有值顺序相同。

题解

class Solution {
public:
    void deleteNode(ListNode* node) {
        if (!node || !node->next) {
            return;
        }
        node->val = node->next->val;
        node->next = node->next->next;
    }
};

445. 两数相加 II

题目

给你两个 非空 链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。

你可以假设除了数字 0 之外,这两个数字都不会以零开头。

示例1:

输入:l1 = [7,2,4,3], l2 = [5,6,4]
输出:[7,8,0,7]
算法(六)链表_第11张图片

题解

先反转链表,再相加,迭代和递归如下所示。

// 递归版本,时间复杂度为O(n),空间复杂度为O(n)
class Solution {
    ListNode *reverseList(ListNode *head) {
        if (head == nullptr || head->next == nullptr)
            return head;
        auto new_head = reverseList(head->next);
        head->next->next = head; // 把下一个节点指向自己
        head->next = nullptr; // 断开指向下一个节点的连接,保证最终链表的末尾节点的 next 是空节点
        return new_head;
    }

    // l1 和 l2 为当前遍历的节点,carry 为进位
    ListNode *addTwo(ListNode *l1, ListNode *l2, int carry = 0) {
        if (l1 == nullptr && l2 == nullptr) // 递归边界:l1 和 l2 都是空节点
            return carry ? new ListNode(carry) : nullptr; // 如果进位了,就额外创建一个节点
        if (l1 == nullptr) // 如果 l1 是空的,那么此时 l2 一定不是空节点
            swap(l1, l2); // 交换 l1 与 l2,保证 l1 非空,从而简化代码
        carry += l1->val + (l2 ? l2->val : 0); // 节点值和进位加在一起
        l1->val = carry % 10; // 每个节点保存一个数位
        l1->next = addTwo(l1->next, (l2 ? l2->next : nullptr), carry / 10); // 进位
        return l1;
    }

public:
    ListNode *addTwoNumbers(ListNode *l1, ListNode *l2) {
        l1 = reverseList(l1);
        l2 = reverseList(l2); // l1 和 l2 反转后,就变成【2. 两数相加】了
        auto l3 = addTwo(l1, l2);
        return reverseList(l3);
    }
};

// 迭代版本,时间复杂度为O(n),空间复杂度为O(1)
class Solution {
    // 视频讲解 https://www.bilibili.com/video/BV1sd4y1x7KN/
    ListNode *reverseList(ListNode *head) {
        ListNode *pre = nullptr, *cur = head;
        while (cur) {
            ListNode *nxt = cur->next;
            cur->next = pre;
            pre = cur;
            cur = nxt;
        }
        return pre;
    }

    ListNode *addTwo(ListNode *l1, ListNode *l2) {
        auto dummy = new ListNode(); // 哨兵节点
        auto cur = dummy;
        int carry = 0; // 进位
        while (l1 || l2 || carry) { // 有一个不是空节点,或者还有进位,就继续迭代
            if (l1) carry += l1->val; // 节点值和进位加在一起
            if (l2) carry += l2->val; // 节点值和进位加在一起
            cur = cur->next = new ListNode(carry % 10); // 每个节点保存一个数位
            carry /= 10; // 新的进位
            if (l1) l1 = l1->next; // 下一个节点
            if (l2) l2 = l2->next; // 下一个节点
        }
        return dummy->next; // 哨兵节点的下一个节点就是头节点
    }

public:
    ListNode *addTwoNumbers(ListNode *l1, ListNode *l2) {
        l1 = reverseList(l1);
        l2 = reverseList(l2);
        auto l3 = addTwo(l1, l2);
        return reverseList(l3);
    }
};

725. 分隔链表

题目

给你一个头结点为 head 的单链表和一个整数 k ,请你设计一个算法将链表分隔为 k 个连续的部分。

每部分的长度应该尽可能的相等:任意两部分的长度差距不能超过 1 。这可能会导致有些部分为 null 。

这 k 个部分应该按照在链表中出现的顺序排列,并且排在前面的部分的长度应该大于或等于排在后面的长度。

返回一个由上述 k 部分组成的数组。

示例 1:
输入:head = [1,2,3], k = 5
输出:[[1],[2],[3],[],[]]
解释:
第一个元素 output[0] 为 output[0].val = 1 ,output[0].next = null 。
最后一个元素 output[4] 为 null ,但它作为 ListNode 的字符串表示是 [] 。

示例 2:
输入:head = [1,2,3,4,5,6,7,8,9,10], k = 3
输出:[[1,2,3,4],[5,6,7],[8,9,10]]
解释:
输入被分成了几个连续的部分,并且每部分的长度相差不超过 1 。前面部分的长度大于等于后面部分的长度。

题解

直接拆分,示例代码如下,

class Solution {
public:
    vector<ListNode*> splitListToParts(ListNode* head, int k) {
        int n = 0;
        ListNode *temp = head;
        while (temp != nullptr) {
            n++;
            temp = temp->next;
        }
        int quotient = n / k, remainder = n % k;

        vector<ListNode*> parts(k,nullptr);
        ListNode *curr = head;
        for (int i = 0; i < k && curr != nullptr; i++) {
            parts[i] = curr;
            // 前几个part承担除不尽的那部分
            int partSize = quotient + (i < remainder ? 1 : 0);
            for (int j = 1; j < partSize; j++) {
                curr = curr->next;
            }
            ListNode *next = curr->next;
            curr->next = nullptr;
            curr = next;
        }
        return parts;
    }
};

复杂度

时间: O ( n ) O(n) O(n)
空间: O ( 1 ) O(1) O(1)

[hot] 876. 链表的中间节点

题目

给你单链表的头结点 head ,请你找出并返回链表的中间结点。

如果有两个中间结点,则返回第二个中间结点。

示例 1:
输入:head = [1,2,3,4,5]
输出:[3,4,5]
解释:链表只有一个中间结点,值为 3 。

示例 2:
输入:head = [1,2,3,4,5,6]
输出:[4,5,6]
解释:该链表有两个中间结点,值分别为 3 和 4 ,返回第二个结点。

题解

直接上题解:

class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        if (!head) {
            return nullptr;
        }

        ListNode* slow = head;
        ListNode* fast = head;
        while (fast && fast->next) {
            slow = slow->next;
            fast = fast->next->next;
        }

        return slow;
    }
};

关键点

明确中间节点的含义,到底是偏左还是偏右,如果是偏右,则while的条件为fast && fast->next,如果是偏左,则为fast ->next && fast->next->next。

1721. 交换链表中的节点

题目

给你链表的头节点 head 和一个整数 k 。

交换 链表正数第 k 个节点和倒数第 k 个节点的值后,返回链表的头节点(链表 从 1 开始索引)。

题解

直接值交换,示例代码如下所示:

/**
 * 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:
    ListNode* swapNodes(ListNode* head, int k) {
        auto l = head;
        auto r = head;
        for (int i = 1; i < k; ++i) {
            l = l->next;
        }
        auto cur = l;
        while (cur->next) {
            cur = cur->next;
            r = r->next;
        }
        int m = l->val;
        l->val = r->val;
        r->val = m;

        return head;
    }
};

关键点

找到正数第k个节点和倒数第k个节点

1836. 从未排序的链表中移除重复元素

题目

如题

题解

哈希表储存每个节点出现的次数,删除出现大于一次的节点,示例代码如下所示:

class Solution {
public:
    ListNode* remove(ListNode* head) {
        unordered_map<int, int> m;
        auto cur = head;
        while (cur) {
            if (m.count(cur->val) == 0) {
                m[cur->val] = 1;
            } else {
                ++m[cur->val];
            }
            cur = cur->next;
        }
        
        auto dummy = new ListNode(0, head);
        auto pre = dummy;
        cur = head;
        while(cur) {
            if (m[cur->val] > 1) {
                pre->next = cur->next;
            } else {
                pre = cur;
            }
            cur = cur->next;
        }

        return dummy->next;
    }
};

关键点

无序链表一般都要借助哈希表

LCR 029. 训练有序列表的插入

题目

给定循环单调非递减列表中的一个点,写一个函数向这个列表中插入一个新元素 insertVal ,使这个列表仍然是循环升序的。

给定的可以是这个列表中任意一个顶点的指针,并不一定是这个列表中最小元素的指针。

如果有多个满足条件的插入位置,可以选择任意一个位置插入新的值,插入后整个列表仍然保持有序。

如果列表为空(给定的节点是 null),需要创建一个循环有序列表并返回这个节点。否则。请返回原先给定的节点。

示例 1:

算法(六)链表_第12张图片

输入:head = [3,4,1], insertVal = 2
输出:[3,4,1,2]
解释:在上图中,有一个包含三个元素的循环有序列表,你获得值为 3 的节点的指针,我们需要向表中插入元素 2 。新插入的节点应该在 1 和 3 之间,插入之后,整个列表如上图所示,最后返回节点 3 。

题解

能插入的条件有两种:

  1. 去环后的链表中部
  2. 去环后的链表首尾两侧

根据这两种情况,示例代码如下:

/*
// Definition for a Node.
class Node {
public:
    int val;
    Node* next;

    Node() {}

    Node(int _val) {
        val = _val;
        next = NULL;
    }

    Node(int _val, Node* _next) {
        val = _val;
        next = _next;
    }
};
*/

class Solution {
public:
    Node* insert(Node* head, int insertVal) {
        auto node = new Node(insertVal);
        if (!head) {
            node->next = node;
            return node;
        }
        if (head->next == head) {
            head->next = node;
            node->next = head;
            return head;
        }
        auto cur = head;
        auto next = head->next;
        while (next != head) {
            // 有序链表中间插入,可以有等号
            if (insertVal >= cur->val && insertVal <= next->val) {
                break;
            }
            // 有序链表两侧插入,不能有等号
            if (cur->val > next->val) {
                if (insertVal > cur->val || insertVal < next->val) {
                    break;
                }
            }
            cur = cur->next;
            next = next->next;
        }
        cur->next = node;
        node->next = next;

        return head;
    }
};

复杂度

时间: O ( n ) O(n) O(n)
空间: O ( 1 ) O(1) O(1)

关键点

中间插入时,可以有等号;两侧插入时不能有等号。

剑指offer

6. 从尾到头打印链表

题目

输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)

示例 1:
输入:head = [1,3,2]
输出:[2,3,1]

题解1

用栈来进行缓存

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    vector<int> reversePrint(ListNode* head) {
        stack<int> cache;
        vector<int> result;
        while (head) {
            cache.push(head->val);
            head = head->next;
        }

        while (!cache.empty()) {
            result.emplace_back(cache.top());
            cache.pop();
        }

        return result;
    }
};

题解2

递归解决问题,先递归到链表尾部,在返回的过程中将head->val push到result中。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    void core(ListNode* head, vector<int>& result) {
        if (head) {
            core(head->next, result);
            result.emplace_back(head->val);
        }
    }

    vector<int> reversePrint(ListNode* head) {
        vector<int> result;
        core(head, result);

        return result;
    }
};

22. 链表中倒数第k个节点

题目

输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。

例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。

示例:
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.

题解

双指针解决问题,pre = head && tail = head,pre先走k步,然后pre和tail一起走,这样当pre走到链表尾时,tail走到倒数第k个节点,示例代码如下:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* getKthFromEnd(ListNode* head, int k) {
        if (!head || k <= 0) {
            return nullptr;
        }

        auto pre = head;
        auto tail = head;
        for (int i = 0; i < k; ++i) {
            if (pre == nullptr) {
                return nullptr;
            } 
            pre = pre->next;
        }
        
        while (pre) {
            pre = pre->next;
            tail = tail->next;
        }

        return tail;
    }
};

关键点

边界条件的判断

[hot] 25. 合并两个有序链表

题目

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

算法(六)链表_第13张图片

示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

示例 2:
输入:l1 = [], l2 = []
输出:[]

示例 3:
输入:l1 = [], l2 = [0]
输出:[0]

题解

/**
 * 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:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        if (!l1) {
            return l2;
        }
        if (!l2) {
            return l1;
        }

        ListNode* node = nullptr;
        if (l1->val < l2->val) {
            node = l1;
            node->next = mergeTwoLists(l1->next, l2);
        } else {
            node = l2;
            node->next = mergeTwoLists(l1, l2->next);
        }

        return node;
    }
};

// 迭代
/**
 * 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:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        ListNode* head = new ListNode(-1);
        ListNode* prev = head;
        while (l1 && l2) {
            if (l1->val < l2->val) {
                prev->next = l1;
                l1 = l1->next;
            } else {
                prev->next = l2;
                l2 = l2->next;
            }
            prev = prev->next;
        } 

        prev->next = l1 == nullptr ? l2 : l1;

        return head->next;
    }
};

关键点

两个链表错开的位置该怎么搞

面试题

1. 美团1面 两个升序链表合成一个降序链表

题解

升序合并后再反转链表,示例代码如下所示:

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

ListNode* merge(ListNode* l1, ListNode* l2) {
    if (!l2) {
        return l1;
    }
    if (!l1) {
        return l2;
    }
    ListNode* node = nullptr;
    if (l1->val < l2->val) {
        node = l1;
        node->next = merge(l1->next, l2);
    } else {
        node = l2;
        node->next = merge(l1, l2->next);
    }
    
    return node;
}

ListNode* r_l(ListNode* l) {
    if (!l || !l->next) {
        return l;
    }
    ListNode* p = l;
    while (p->next) {
        ListNode* q = p->next;
        p->next = q->next;
        q->next = l;
        l = q;
    }
    
    return l;
}

ListNode* my_sort(ListNode* l1, ListNode* l2) {
    ListNode* l = merge(l1, l2);
    ListNode* head = r_l(l);
    
    return head;
}

你可能感兴趣的:(算法,算法,链表,leetcode,单链表)